Failed Conditions
Push — master ( a2bb82...a189d9 )
by Adrien
10:27 queued 01:00
created

Xls::readDataValidations()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 7
Code Lines 3

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 2

Importance

Changes 0
Metric Value
cc 1
eloc 3
nc 1
nop 0
dl 0
loc 7
ccs 0
cts 4
cp 0
crap 2
rs 10
c 0
b 0
f 0
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\RichText\RichText;
11
use PhpOffice\PhpSpreadsheet\Shared\CodePage;
12
use PhpOffice\PhpSpreadsheet\Shared\Date;
13
use PhpOffice\PhpSpreadsheet\Shared\Escher;
14
use PhpOffice\PhpSpreadsheet\Shared\Escher\DggContainer\BstoreContainer\BSE;
15
use PhpOffice\PhpSpreadsheet\Shared\File;
16
use PhpOffice\PhpSpreadsheet\Shared\OLE;
17
use PhpOffice\PhpSpreadsheet\Shared\OLERead;
18
use PhpOffice\PhpSpreadsheet\Shared\StringHelper;
19
use PhpOffice\PhpSpreadsheet\Spreadsheet;
20
use PhpOffice\PhpSpreadsheet\Style\Alignment;
21
use PhpOffice\PhpSpreadsheet\Style\Borders;
22
use PhpOffice\PhpSpreadsheet\Style\Font;
23
use PhpOffice\PhpSpreadsheet\Style\NumberFormat;
24
use PhpOffice\PhpSpreadsheet\Style\Protection;
25
use PhpOffice\PhpSpreadsheet\Style\Style;
26
use PhpOffice\PhpSpreadsheet\Worksheet\MemoryDrawing;
27
use PhpOffice\PhpSpreadsheet\Worksheet\PageSetup;
28
use PhpOffice\PhpSpreadsheet\Worksheet\SheetView;
29
use PhpOffice\PhpSpreadsheet\Worksheet\Worksheet;
30
31
// Original file header of ParseXL (used as the base for this class):
32
// --------------------------------------------------------------------------------
33
// Adapted from Excel_Spreadsheet_Reader developed by users bizon153,
34
// trex005, and mmp11 (SourceForge.net)
35
// https://sourceforge.net/projects/phpexcelreader/
36
// Primary changes made by canyoncasa (dvc) for ParseXL 1.00 ...
37
//     Modelled moreso after Perl Excel Parse/Write modules
38
//     Added Parse_Excel_Spreadsheet object
39
//         Reads a whole worksheet or tab as row,column array or as
40
//         associated hash of indexed rows and named column fields
41
//     Added variables for worksheet (tab) indexes and names
42
//     Added an object call for loading individual woorksheets
43
//     Changed default indexing defaults to 0 based arrays
44
//     Fixed date/time and percent formats
45
//     Includes patches found at SourceForge...
46
//         unicode patch by nobody
47
//         unpack("d") machine depedency patch by matchy
48
//         boundsheet utf16 patch by bjaenichen
49
//     Renamed functions for shorter names
50
//     General code cleanup and rigor, including <80 column width
51
//     Included a testcase Excel file and PHP example calls
52
//     Code works for PHP 5.x
53
54
// Primary changes made by canyoncasa (dvc) for ParseXL 1.10 ...
55
// http://sourceforge.net/tracker/index.php?func=detail&aid=1466964&group_id=99160&atid=623334
56
//     Decoding of formula conditions, results, and tokens.
57
//     Support for user-defined named cells added as an array "namedcells"
58
//         Patch code for user-defined named cells supports single cells only.
59
//         NOTE: this patch only works for BIFF8 as BIFF5-7 use a different
60
//         external sheet reference structure
61
class Xls extends BaseReader
62
{
63
    // ParseXL definitions
64
    const XLS_BIFF8 = 0x0600;
65
    const XLS_BIFF7 = 0x0500;
66
    const XLS_WORKBOOKGLOBALS = 0x0005;
67
    const XLS_WORKSHEET = 0x0010;
68
69
    // record identifiers
70
    const XLS_TYPE_FORMULA = 0x0006;
71
    const XLS_TYPE_EOF = 0x000a;
72
    const XLS_TYPE_PROTECT = 0x0012;
73
    const XLS_TYPE_OBJECTPROTECT = 0x0063;
74
    const XLS_TYPE_SCENPROTECT = 0x00dd;
75
    const XLS_TYPE_PASSWORD = 0x0013;
76
    const XLS_TYPE_HEADER = 0x0014;
77
    const XLS_TYPE_FOOTER = 0x0015;
78
    const XLS_TYPE_EXTERNSHEET = 0x0017;
79
    const XLS_TYPE_DEFINEDNAME = 0x0018;
80
    const XLS_TYPE_VERTICALPAGEBREAKS = 0x001a;
81
    const XLS_TYPE_HORIZONTALPAGEBREAKS = 0x001b;
82
    const XLS_TYPE_NOTE = 0x001c;
83
    const XLS_TYPE_SELECTION = 0x001d;
84
    const XLS_TYPE_DATEMODE = 0x0022;
85
    const XLS_TYPE_EXTERNNAME = 0x0023;
86
    const XLS_TYPE_LEFTMARGIN = 0x0026;
87
    const XLS_TYPE_RIGHTMARGIN = 0x0027;
88
    const XLS_TYPE_TOPMARGIN = 0x0028;
89
    const XLS_TYPE_BOTTOMMARGIN = 0x0029;
90
    const XLS_TYPE_PRINTGRIDLINES = 0x002b;
91
    const XLS_TYPE_FILEPASS = 0x002f;
92
    const XLS_TYPE_FONT = 0x0031;
93
    const XLS_TYPE_CONTINUE = 0x003c;
94
    const XLS_TYPE_PANE = 0x0041;
95
    const XLS_TYPE_CODEPAGE = 0x0042;
96
    const XLS_TYPE_DEFCOLWIDTH = 0x0055;
97
    const XLS_TYPE_OBJ = 0x005d;
98
    const XLS_TYPE_COLINFO = 0x007d;
99
    const XLS_TYPE_IMDATA = 0x007f;
100
    const XLS_TYPE_SHEETPR = 0x0081;
101
    const XLS_TYPE_HCENTER = 0x0083;
102
    const XLS_TYPE_VCENTER = 0x0084;
103
    const XLS_TYPE_SHEET = 0x0085;
104
    const XLS_TYPE_PALETTE = 0x0092;
105
    const XLS_TYPE_SCL = 0x00a0;
106
    const XLS_TYPE_PAGESETUP = 0x00a1;
107
    const XLS_TYPE_MULRK = 0x00bd;
108
    const XLS_TYPE_MULBLANK = 0x00be;
109
    const XLS_TYPE_DBCELL = 0x00d7;
110
    const XLS_TYPE_XF = 0x00e0;
111
    const XLS_TYPE_MERGEDCELLS = 0x00e5;
112
    const XLS_TYPE_MSODRAWINGGROUP = 0x00eb;
113
    const XLS_TYPE_MSODRAWING = 0x00ec;
114
    const XLS_TYPE_SST = 0x00fc;
115
    const XLS_TYPE_LABELSST = 0x00fd;
116
    const XLS_TYPE_EXTSST = 0x00ff;
117
    const XLS_TYPE_EXTERNALBOOK = 0x01ae;
118
    const XLS_TYPE_DATAVALIDATIONS = 0x01b2;
119
    const XLS_TYPE_TXO = 0x01b6;
120
    const XLS_TYPE_HYPERLINK = 0x01b8;
121
    const XLS_TYPE_DATAVALIDATION = 0x01be;
122
    const XLS_TYPE_DIMENSION = 0x0200;
123
    const XLS_TYPE_BLANK = 0x0201;
124
    const XLS_TYPE_NUMBER = 0x0203;
125
    const XLS_TYPE_LABEL = 0x0204;
126
    const XLS_TYPE_BOOLERR = 0x0205;
127
    const XLS_TYPE_STRING = 0x0207;
128
    const XLS_TYPE_ROW = 0x0208;
129
    const XLS_TYPE_INDEX = 0x020b;
130
    const XLS_TYPE_ARRAY = 0x0221;
131
    const XLS_TYPE_DEFAULTROWHEIGHT = 0x0225;
132
    const XLS_TYPE_WINDOW2 = 0x023e;
133
    const XLS_TYPE_RK = 0x027e;
134
    const XLS_TYPE_STYLE = 0x0293;
135
    const XLS_TYPE_FORMAT = 0x041e;
136
    const XLS_TYPE_SHAREDFMLA = 0x04bc;
137
    const XLS_TYPE_BOF = 0x0809;
138
    const XLS_TYPE_SHEETPROTECTION = 0x0867;
139
    const XLS_TYPE_RANGEPROTECTION = 0x0868;
140
    const XLS_TYPE_SHEETLAYOUT = 0x0862;
141
    const XLS_TYPE_XFEXT = 0x087d;
142
    const XLS_TYPE_PAGELAYOUTVIEW = 0x088b;
143
    const XLS_TYPE_UNKNOWN = 0xffff;
144
145
    // Encryption type
146
    const MS_BIFF_CRYPTO_NONE = 0;
147
    const MS_BIFF_CRYPTO_XOR = 1;
148
    const MS_BIFF_CRYPTO_RC4 = 2;
149
150
    // Size of stream blocks when using RC4 encryption
151
    const REKEY_BLOCK = 0x400;
152
153
    /**
154
     * Summary Information stream data.
155
     *
156
     * @var string
157
     */
158
    private $summaryInformation;
159
160
    /**
161
     * Extended Summary Information stream data.
162
     *
163
     * @var string
164
     */
165
    private $documentSummaryInformation;
166
167
    /**
168
     * Workbook stream data. (Includes workbook globals substream as well as sheet substreams).
169
     *
170
     * @var string
171
     */
172
    private $data;
173
174
    /**
175
     * Size in bytes of $this->data.
176
     *
177
     * @var int
178
     */
179
    private $dataSize;
180
181
    /**
182
     * Current position in stream.
183
     *
184
     * @var int
185
     */
186
    private $pos;
187
188
    /**
189
     * Workbook to be returned by the reader.
190
     *
191
     * @var Spreadsheet
192
     */
193
    private $spreadsheet;
194
195
    /**
196
     * Worksheet that is currently being built by the reader.
197
     *
198
     * @var Worksheet
199
     */
200
    private $phpSheet;
201
202
    /**
203
     * BIFF version.
204
     *
205
     * @var int
206
     */
207
    private $version;
208
209
    /**
210
     * Codepage set in the Excel file being read. Only important for BIFF5 (Excel 5.0 - Excel 95)
211
     * For BIFF8 (Excel 97 - Excel 2003) this will always have the value 'UTF-16LE'.
212
     *
213
     * @var string
214
     */
215
    private $codepage;
216
217
    /**
218
     * Shared formats.
219
     *
220
     * @var array
221
     */
222
    private $formats;
223
224
    /**
225
     * Shared fonts.
226
     *
227
     * @var array
228
     */
229
    private $objFonts;
230
231
    /**
232
     * Color palette.
233
     *
234
     * @var array
235
     */
236
    private $palette;
237
238
    /**
239
     * Worksheets.
240
     *
241
     * @var array
242
     */
243
    private $sheets;
244
245
    /**
246
     * External books.
247
     *
248
     * @var array
249
     */
250
    private $externalBooks;
251
252
    /**
253
     * REF structures. Only applies to BIFF8.
254
     *
255
     * @var array
256
     */
257
    private $ref;
258
259
    /**
260
     * External names.
261
     *
262
     * @var array
263
     */
264
    private $externalNames;
265
266
    /**
267
     * Defined names.
268
     *
269
     * @var array
270
     */
271
    private $definedname;
272
273
    /**
274
     * Shared strings. Only applies to BIFF8.
275
     *
276
     * @var array
277
     */
278
    private $sst;
279
280
    /**
281
     * Panes are frozen? (in sheet currently being read). See WINDOW2 record.
282
     *
283
     * @var bool
284
     */
285
    private $frozen;
286
287
    /**
288
     * Fit printout to number of pages? (in sheet currently being read). See SHEETPR record.
289
     *
290
     * @var bool
291
     */
292
    private $isFitToPages;
293
294
    /**
295
     * Objects. One OBJ record contributes with one entry.
296
     *
297
     * @var array
298
     */
299
    private $objs;
300
301
    /**
302
     * Text Objects. One TXO record corresponds with one entry.
303
     *
304
     * @var array
305
     */
306
    private $textObjects;
307
308
    /**
309
     * Cell Annotations (BIFF8).
310
     *
311
     * @var array
312
     */
313
    private $cellNotes;
314
315
    /**
316
     * The combined MSODRAWINGGROUP data.
317
     *
318
     * @var string
319
     */
320
    private $drawingGroupData;
321
322
    /**
323
     * The combined MSODRAWING data (per sheet).
324
     *
325
     * @var string
326
     */
327
    private $drawingData;
328
329
    /**
330
     * Keep track of XF index.
331
     *
332
     * @var int
333
     */
334
    private $xfIndex;
335
336
    /**
337
     * Mapping of XF index (that is a cell XF) to final index in cellXf collection.
338
     *
339
     * @var array
340
     */
341
    private $mapCellXfIndex;
342
343
    /**
344
     * Mapping of XF index (that is a style XF) to final index in cellStyleXf collection.
345
     *
346
     * @var array
347
     */
348
    private $mapCellStyleXfIndex;
349
350
    /**
351
     * The shared formulas in a sheet. One SHAREDFMLA record contributes with one value.
352
     *
353
     * @var array
354
     */
355
    private $sharedFormulas;
356
357
    /**
358
     * The shared formula parts in a sheet. One FORMULA record contributes with one value if it
359
     * refers to a shared formula.
360
     *
361
     * @var array
362
     */
363
    private $sharedFormulaParts;
364
365
    /**
366
     * The type of encryption in use.
367
     *
368
     * @var int
369
     */
370
    private $encryption = 0;
371
372
    /**
373
     * The position in the stream after which contents are encrypted.
374
     *
375
     * @var int
376
     */
377
    private $encryptionStartPos = false;
378
379
    /**
380
     * The current RC4 decryption object.
381
     *
382
     * @var Xls\RC4
383
     */
384
    private $rc4Key;
385
386
    /**
387
     * The position in the stream that the RC4 decryption object was left at.
388
     *
389
     * @var int
390
     */
391
    private $rc4Pos = 0;
392
393
    /**
394
     * The current MD5 context state.
395
     *
396
     * @var string
397
     */
398
    private $md5Ctxt;
399
400
    /**
401
     * @var int
402
     */
403
    private $textObjRef;
404
405
    /**
406
     * @var string
407
     */
408
    private $baseCell;
409
410
    /**
411
     * Create a new Xls Reader instance.
412
     */
413 50
    public function __construct()
414
    {
415 50
        parent::__construct();
416 50
    }
417
418
    /**
419
     * Can the current IReader read the file?
420
     *
421
     * @param string $pFilename
422
     *
423
     * @return bool
424
     */
425 11
    public function canRead($pFilename)
426
    {
427 11
        File::assertFile($pFilename);
428
429
        try {
430
            // Use ParseXL for the hard work.
431 11
            $ole = new OLERead();
432
433
            // get excel data
434 11
            $ole->read($pFilename);
435
436 8
            return true;
437 3
        } catch (PhpSpreadsheetException $e) {
438 3
            return false;
439
        }
440
    }
441
442
    public function setCodepage(string $codepage): void
443
    {
444
        if (!CodePage::validate($codepage)) {
445
            throw new PhpSpreadsheetException('Unknown codepage: ' . $codepage);
446
        }
447
448
        $this->codepage = $codepage;
449
    }
450
451
    /**
452
     * Reads names of the worksheets from a file, without parsing the whole file to a PhpSpreadsheet object.
453
     *
454
     * @param string $pFilename
455
     *
456
     * @return array
457
     */
458 2
    public function listWorksheetNames($pFilename)
459
    {
460 2
        File::assertFile($pFilename);
461
462 2
        $worksheetNames = [];
463
464
        // Read the OLE file
465 2
        $this->loadOLE($pFilename);
466
467
        // total byte size of Excel data (workbook global substream + sheet substreams)
468 2
        $this->dataSize = strlen($this->data);
469
470 2
        $this->pos = 0;
471 2
        $this->sheets = [];
472
473
        // Parse Workbook Global Substream
474 2
        while ($this->pos < $this->dataSize) {
475 2
            $code = self::getUInt2d($this->data, $this->pos);
476
477
            switch ($code) {
478 2
                case self::XLS_TYPE_BOF:
479 2
                    $this->readBof();
480
481 2
                    break;
482 2
                case self::XLS_TYPE_SHEET:
483 2
                    $this->readSheet();
484
485 2
                    break;
486 2
                case self::XLS_TYPE_EOF:
487 2
                    $this->readDefault();
488
489 2
                    break 2;
490
                default:
491 2
                    $this->readDefault();
492
493 2
                    break;
494
            }
495
        }
496
497 2
        foreach ($this->sheets as $sheet) {
498 2
            if ($sheet['sheetType'] != 0x00) {
499
                // 0x00: Worksheet, 0x02: Chart, 0x06: Visual Basic module
500
                continue;
501
            }
502
503 2
            $worksheetNames[] = $sheet['name'];
504
        }
505
506 2
        return $worksheetNames;
507
    }
508
509
    /**
510
     * Return worksheet info (Name, Last Column Letter, Last Column Index, Total Rows, Total Columns).
511
     *
512
     * @param string $pFilename
513
     *
514
     * @return array
515
     */
516 1
    public function listWorksheetInfo($pFilename)
517
    {
518 1
        File::assertFile($pFilename);
519
520 1
        $worksheetInfo = [];
521
522
        // Read the OLE file
523 1
        $this->loadOLE($pFilename);
524
525
        // total byte size of Excel data (workbook global substream + sheet substreams)
526 1
        $this->dataSize = strlen($this->data);
527
528
        // initialize
529 1
        $this->pos = 0;
530 1
        $this->sheets = [];
531
532
        // Parse Workbook Global Substream
533 1
        while ($this->pos < $this->dataSize) {
534 1
            $code = self::getUInt2d($this->data, $this->pos);
535
536
            switch ($code) {
537 1
                case self::XLS_TYPE_BOF:
538 1
                    $this->readBof();
539
540 1
                    break;
541 1
                case self::XLS_TYPE_SHEET:
542 1
                    $this->readSheet();
543
544 1
                    break;
545 1
                case self::XLS_TYPE_EOF:
546 1
                    $this->readDefault();
547
548 1
                    break 2;
549
                default:
550 1
                    $this->readDefault();
551
552 1
                    break;
553
            }
554
        }
555
556
        // Parse the individual sheets
557 1
        foreach ($this->sheets as $sheet) {
558 1
            if ($sheet['sheetType'] != 0x00) {
559
                // 0x00: Worksheet
560
                // 0x02: Chart
561
                // 0x06: Visual Basic module
562
                continue;
563
            }
564
565 1
            $tmpInfo = [];
566 1
            $tmpInfo['worksheetName'] = $sheet['name'];
567 1
            $tmpInfo['lastColumnLetter'] = 'A';
568 1
            $tmpInfo['lastColumnIndex'] = 0;
569 1
            $tmpInfo['totalRows'] = 0;
570 1
            $tmpInfo['totalColumns'] = 0;
571
572 1
            $this->pos = $sheet['offset'];
573
574 1
            while ($this->pos <= $this->dataSize - 4) {
575 1
                $code = self::getUInt2d($this->data, $this->pos);
576
577
                switch ($code) {
578 1
                    case self::XLS_TYPE_RK:
579 1
                    case self::XLS_TYPE_LABELSST:
580 1
                    case self::XLS_TYPE_NUMBER:
581 1
                    case self::XLS_TYPE_FORMULA:
582 1
                    case self::XLS_TYPE_BOOLERR:
583 1
                    case self::XLS_TYPE_LABEL:
584 1
                        $length = self::getUInt2d($this->data, $this->pos + 2);
585 1
                        $recordData = $this->readRecordData($this->data, $this->pos + 4, $length);
586
587
                        // move stream pointer to next record
588 1
                        $this->pos += 4 + $length;
589
590 1
                        $rowIndex = self::getUInt2d($recordData, 0) + 1;
591 1
                        $columnIndex = self::getUInt2d($recordData, 2);
592
593 1
                        $tmpInfo['totalRows'] = max($tmpInfo['totalRows'], $rowIndex);
594 1
                        $tmpInfo['lastColumnIndex'] = max($tmpInfo['lastColumnIndex'], $columnIndex);
595
596 1
                        break;
597 1
                    case self::XLS_TYPE_BOF:
598 1
                        $this->readBof();
599
600 1
                        break;
601 1
                    case self::XLS_TYPE_EOF:
602 1
                        $this->readDefault();
603
604 1
                        break 2;
605
                    default:
606 1
                        $this->readDefault();
607
608 1
                        break;
609
                }
610
            }
611
612 1
            $tmpInfo['lastColumnLetter'] = Coordinate::stringFromColumnIndex($tmpInfo['lastColumnIndex'] + 1);
613 1
            $tmpInfo['totalColumns'] = $tmpInfo['lastColumnIndex'] + 1;
614
615 1
            $worksheetInfo[] = $tmpInfo;
616
        }
617
618 1
        return $worksheetInfo;
619
    }
620
621
    /**
622
     * Loads PhpSpreadsheet from file.
623
     *
624
     * @param string $pFilename
625
     *
626
     * @return Spreadsheet
627
     */
628 39
    public function load($pFilename)
629
    {
630
        // Read the OLE file
631 39
        $this->loadOLE($pFilename);
632
633
        // Initialisations
634 39
        $this->spreadsheet = new Spreadsheet();
635 39
        $this->spreadsheet->removeSheetByIndex(0); // remove 1st sheet
636 39
        if (!$this->readDataOnly) {
637 38
            $this->spreadsheet->removeCellStyleXfByIndex(0); // remove the default style
638 38
            $this->spreadsheet->removeCellXfByIndex(0); // remove the default style
639
        }
640
641
        // Read the summary information stream (containing meta data)
642 39
        $this->readSummaryInformation();
643
644
        // Read the Additional document summary information stream (containing application-specific meta data)
645 39
        $this->readDocumentSummaryInformation();
646
647
        // total byte size of Excel data (workbook global substream + sheet substreams)
648 39
        $this->dataSize = strlen($this->data);
649
650
        // initialize
651 39
        $this->pos = 0;
652 39
        $this->codepage = $this->codepage ?: CodePage::DEFAULT_CODE_PAGE;
653 39
        $this->formats = [];
654 39
        $this->objFonts = [];
655 39
        $this->palette = [];
656 39
        $this->sheets = [];
657 39
        $this->externalBooks = [];
658 39
        $this->ref = [];
659 39
        $this->definedname = [];
660 39
        $this->sst = [];
661 39
        $this->drawingGroupData = '';
662 39
        $this->xfIndex = '';
663 39
        $this->mapCellXfIndex = [];
664 39
        $this->mapCellStyleXfIndex = [];
665
666
        // Parse Workbook Global Substream
667 39
        while ($this->pos < $this->dataSize) {
668 39
            $code = self::getUInt2d($this->data, $this->pos);
669
670
            switch ($code) {
671 39
                case self::XLS_TYPE_BOF:
672 39
                    $this->readBof();
673
674 39
                    break;
675 39
                case self::XLS_TYPE_FILEPASS:
676
                    $this->readFilepass();
677
678
                    break;
679 39
                case self::XLS_TYPE_CODEPAGE:
680 36
                    $this->readCodepage();
681
682 36
                    break;
683 39
                case self::XLS_TYPE_DATEMODE:
684 38
                    $this->readDateMode();
685
686 38
                    break;
687 39
                case self::XLS_TYPE_FONT:
688 39
                    $this->readFont();
689
690 39
                    break;
691 39
                case self::XLS_TYPE_FORMAT:
692 24
                    $this->readFormat();
693
694 24
                    break;
695 39
                case self::XLS_TYPE_XF:
696 39
                    $this->readXf();
697
698 39
                    break;
699 39
                case self::XLS_TYPE_XFEXT:
700 6
                    $this->readXfExt();
701
702 6
                    break;
703 39
                case self::XLS_TYPE_STYLE:
704 39
                    $this->readStyle();
705
706 39
                    break;
707 39
                case self::XLS_TYPE_PALETTE:
708 22
                    $this->readPalette();
709
710 22
                    break;
711 39
                case self::XLS_TYPE_SHEET:
712 39
                    $this->readSheet();
713
714 39
                    break;
715 39
                case self::XLS_TYPE_EXTERNALBOOK:
716 24
                    $this->readExternalBook();
717
718 24
                    break;
719 39
                case self::XLS_TYPE_EXTERNNAME:
720
                    $this->readExternName();
721
722
                    break;
723 39
                case self::XLS_TYPE_EXTERNSHEET:
724 25
                    $this->readExternSheet();
725
726 25
                    break;
727 39
                case self::XLS_TYPE_DEFINEDNAME:
728 7
                    $this->readDefinedName();
729
730 7
                    break;
731 39
                case self::XLS_TYPE_MSODRAWINGGROUP:
732 6
                    $this->readMsoDrawingGroup();
733
734 6
                    break;
735 39
                case self::XLS_TYPE_SST:
736 37
                    $this->readSst();
737
738 37
                    break;
739 39
                case self::XLS_TYPE_EOF:
740 39
                    $this->readDefault();
741
742 39
                    break 2;
743
                default:
744 39
                    $this->readDefault();
745
746 39
                    break;
747
            }
748
        }
749
750
        // Resolve indexed colors for font, fill, and border colors
751
        // Cannot be resolved already in XF record, because PALETTE record comes afterwards
752 39
        if (!$this->readDataOnly) {
753 38
            foreach ($this->objFonts as $objFont) {
754 38
                if (isset($objFont->colorIndex)) {
755 38
                    $color = Xls\Color::map($objFont->colorIndex, $this->palette, $this->version);
756 38
                    $objFont->getColor()->setRGB($color['rgb']);
757
                }
758
            }
759
760 38
            foreach ($this->spreadsheet->getCellXfCollection() as $objStyle) {
761
                // fill start and end color
762 38
                $fill = $objStyle->getFill();
763
764 38
                if (isset($fill->startcolorIndex)) {
765 38
                    $startColor = Xls\Color::map($fill->startcolorIndex, $this->palette, $this->version);
766 38
                    $fill->getStartColor()->setRGB($startColor['rgb']);
767
                }
768 38
                if (isset($fill->endcolorIndex)) {
769 38
                    $endColor = Xls\Color::map($fill->endcolorIndex, $this->palette, $this->version);
770 38
                    $fill->getEndColor()->setRGB($endColor['rgb']);
771
                }
772
773
                // border colors
774 38
                $top = $objStyle->getBorders()->getTop();
775 38
                $right = $objStyle->getBorders()->getRight();
776 38
                $bottom = $objStyle->getBorders()->getBottom();
777 38
                $left = $objStyle->getBorders()->getLeft();
778 38
                $diagonal = $objStyle->getBorders()->getDiagonal();
779
780 38
                if (isset($top->colorIndex)) {
781 38
                    $borderTopColor = Xls\Color::map($top->colorIndex, $this->palette, $this->version);
782 38
                    $top->getColor()->setRGB($borderTopColor['rgb']);
783
                }
784 38
                if (isset($right->colorIndex)) {
785 38
                    $borderRightColor = Xls\Color::map($right->colorIndex, $this->palette, $this->version);
786 38
                    $right->getColor()->setRGB($borderRightColor['rgb']);
787
                }
788 38
                if (isset($bottom->colorIndex)) {
789 38
                    $borderBottomColor = Xls\Color::map($bottom->colorIndex, $this->palette, $this->version);
790 38
                    $bottom->getColor()->setRGB($borderBottomColor['rgb']);
791
                }
792 38
                if (isset($left->colorIndex)) {
793 38
                    $borderLeftColor = Xls\Color::map($left->colorIndex, $this->palette, $this->version);
794 38
                    $left->getColor()->setRGB($borderLeftColor['rgb']);
795
                }
796 38
                if (isset($diagonal->colorIndex)) {
797 37
                    $borderDiagonalColor = Xls\Color::map($diagonal->colorIndex, $this->palette, $this->version);
798 37
                    $diagonal->getColor()->setRGB($borderDiagonalColor['rgb']);
799
                }
800
            }
801
        }
802
803
        // treat MSODRAWINGGROUP records, workbook-level Escher
804 39
        $escherWorkbook = null;
805 39
        if (!$this->readDataOnly && $this->drawingGroupData) {
806 6
            $escher = new Escher();
807 6
            $reader = new Xls\Escher($escher);
808 6
            $escherWorkbook = $reader->load($this->drawingGroupData);
809
        }
810
811
        // Parse the individual sheets
812 39
        foreach ($this->sheets as $sheet) {
813 39
            if ($sheet['sheetType'] != 0x00) {
814
                // 0x00: Worksheet, 0x02: Chart, 0x06: Visual Basic module
815
                continue;
816
            }
817
818
            // check if sheet should be skipped
819 39
            if (isset($this->loadSheetsOnly) && !in_array($sheet['name'], $this->loadSheetsOnly)) {
820 5
                continue;
821
            }
822
823
            // add sheet to PhpSpreadsheet object
824 39
            $this->phpSheet = $this->spreadsheet->createSheet();
825
            //    Use false for $updateFormulaCellReferences to prevent adjustment of worksheet references in formula
826
            //        cells... during the load, all formulae should be correct, and we're simply bringing the worksheet
827
            //        name in line with the formula, not the reverse
828 39
            $this->phpSheet->setTitle($sheet['name'], false, false);
829 39
            $this->phpSheet->setSheetState($sheet['sheetState']);
830
831 39
            $this->pos = $sheet['offset'];
832
833
            // Initialize isFitToPages. May change after reading SHEETPR record.
834 39
            $this->isFitToPages = false;
835
836
            // Initialize drawingData
837 39
            $this->drawingData = '';
838
839
            // Initialize objs
840 39
            $this->objs = [];
841
842
            // Initialize shared formula parts
843 39
            $this->sharedFormulaParts = [];
844
845
            // Initialize shared formulas
846 39
            $this->sharedFormulas = [];
847
848
            // Initialize text objs
849 39
            $this->textObjects = [];
850
851
            // Initialize cell annotations
852 39
            $this->cellNotes = [];
853 39
            $this->textObjRef = -1;
854
855 39
            while ($this->pos <= $this->dataSize - 4) {
856 39
                $code = self::getUInt2d($this->data, $this->pos);
857
858
                switch ($code) {
859 39
                    case self::XLS_TYPE_BOF:
860 39
                        $this->readBof();
861
862 39
                        break;
863 39
                    case self::XLS_TYPE_PRINTGRIDLINES:
864 36
                        $this->readPrintGridlines();
865
866 36
                        break;
867 39
                    case self::XLS_TYPE_DEFAULTROWHEIGHT:
868 23
                        $this->readDefaultRowHeight();
869
870 23
                        break;
871 39
                    case self::XLS_TYPE_SHEETPR:
872 38
                        $this->readSheetPr();
873
874 38
                        break;
875 39
                    case self::XLS_TYPE_HORIZONTALPAGEBREAKS:
876 2
                        $this->readHorizontalPageBreaks();
877
878 2
                        break;
879 39
                    case self::XLS_TYPE_VERTICALPAGEBREAKS:
880 2
                        $this->readVerticalPageBreaks();
881
882 2
                        break;
883 39
                    case self::XLS_TYPE_HEADER:
884 36
                        $this->readHeader();
885
886 36
                        break;
887 39
                    case self::XLS_TYPE_FOOTER:
888 36
                        $this->readFooter();
889
890 36
                        break;
891 39
                    case self::XLS_TYPE_HCENTER:
892 36
                        $this->readHcenter();
893
894 36
                        break;
895 39
                    case self::XLS_TYPE_VCENTER:
896 36
                        $this->readVcenter();
897
898 36
                        break;
899 39
                    case self::XLS_TYPE_LEFTMARGIN:
900 23
                        $this->readLeftMargin();
901
902 23
                        break;
903 39
                    case self::XLS_TYPE_RIGHTMARGIN:
904 23
                        $this->readRightMargin();
905
906 23
                        break;
907 39
                    case self::XLS_TYPE_TOPMARGIN:
908 23
                        $this->readTopMargin();
909
910 23
                        break;
911 39
                    case self::XLS_TYPE_BOTTOMMARGIN:
912 23
                        $this->readBottomMargin();
913
914 23
                        break;
915 39
                    case self::XLS_TYPE_PAGESETUP:
916 38
                        $this->readPageSetup();
917
918 38
                        break;
919 39
                    case self::XLS_TYPE_PROTECT:
920 3
                        $this->readProtect();
921
922 3
                        break;
923 39
                    case self::XLS_TYPE_SCENPROTECT:
924
                        $this->readScenProtect();
925
926
                        break;
927 39
                    case self::XLS_TYPE_OBJECTPROTECT:
928
                        $this->readObjectProtect();
929
930
                        break;
931 39
                    case self::XLS_TYPE_PASSWORD:
932
                        $this->readPassword();
933
934
                        break;
935 39
                    case self::XLS_TYPE_DEFCOLWIDTH:
936 38
                        $this->readDefColWidth();
937
938 38
                        break;
939 39
                    case self::XLS_TYPE_COLINFO:
940 33
                        $this->readColInfo();
941
942 33
                        break;
943 39
                    case self::XLS_TYPE_DIMENSION:
944 39
                        $this->readDefault();
945
946 39
                        break;
947 39
                    case self::XLS_TYPE_ROW:
948 24
                        $this->readRow();
949
950 24
                        break;
951 39
                    case self::XLS_TYPE_DBCELL:
952 22
                        $this->readDefault();
953
954 22
                        break;
955 39
                    case self::XLS_TYPE_RK:
956 16
                        $this->readRk();
957
958 16
                        break;
959 39
                    case self::XLS_TYPE_LABELSST:
960 25
                        $this->readLabelSst();
961
962 25
                        break;
963 39
                    case self::XLS_TYPE_MULRK:
964 13
                        $this->readMulRk();
965
966 13
                        break;
967 39
                    case self::XLS_TYPE_NUMBER:
968 18
                        $this->readNumber();
969
970 18
                        break;
971 39
                    case self::XLS_TYPE_FORMULA:
972 17
                        $this->readFormula();
973
974 17
                        break;
975 39
                    case self::XLS_TYPE_SHAREDFMLA:
976
                        $this->readSharedFmla();
977
978
                        break;
979 39
                    case self::XLS_TYPE_BOOLERR:
980 9
                        $this->readBoolErr();
981
982 9
                        break;
983 39
                    case self::XLS_TYPE_MULBLANK:
984 13
                        $this->readMulBlank();
985
986 13
                        break;
987 39
                    case self::XLS_TYPE_LABEL:
988 2
                        $this->readLabel();
989
990 2
                        break;
991 39
                    case self::XLS_TYPE_BLANK:
992 8
                        $this->readBlank();
993
994 8
                        break;
995 39
                    case self::XLS_TYPE_MSODRAWING:
996 6
                        $this->readMsoDrawing();
997
998 6
                        break;
999 39
                    case self::XLS_TYPE_OBJ:
1000 7
                        $this->readObj();
1001
1002 7
                        break;
1003 39
                    case self::XLS_TYPE_WINDOW2:
1004 39
                        $this->readWindow2();
1005
1006 39
                        break;
1007 39
                    case self::XLS_TYPE_PAGELAYOUTVIEW:
1008 21
                        $this->readPageLayoutView();
1009
1010 21
                        break;
1011 39
                    case self::XLS_TYPE_SCL:
1012 2
                        $this->readScl();
1013
1014 2
                        break;
1015 39
                    case self::XLS_TYPE_PANE:
1016 5
                        $this->readPane();
1017
1018 5
                        break;
1019 39
                    case self::XLS_TYPE_SELECTION:
1020 36
                        $this->readSelection();
1021
1022 36
                        break;
1023 39
                    case self::XLS_TYPE_MERGEDCELLS:
1024 14
                        $this->readMergedCells();
1025
1026 14
                        break;
1027 39
                    case self::XLS_TYPE_HYPERLINK:
1028 3
                        $this->readHyperLink();
1029
1030 3
                        break;
1031 39
                    case self::XLS_TYPE_DATAVALIDATIONS:
1032
                        $this->readDataValidations();
1033
1034
                        break;
1035 39
                    case self::XLS_TYPE_DATAVALIDATION:
1036
                        $this->readDataValidation();
1037
1038
                        break;
1039 39
                    case self::XLS_TYPE_SHEETLAYOUT:
1040 3
                        $this->readSheetLayout();
1041
1042 3
                        break;
1043 39
                    case self::XLS_TYPE_SHEETPROTECTION:
1044 22
                        $this->readSheetProtection();
1045
1046 22
                        break;
1047 39
                    case self::XLS_TYPE_RANGEPROTECTION:
1048 1
                        $this->readRangeProtection();
1049
1050 1
                        break;
1051 39
                    case self::XLS_TYPE_NOTE:
1052 3
                        $this->readNote();
1053
1054 3
                        break;
1055 39
                    case self::XLS_TYPE_TXO:
1056 2
                        $this->readTextObject();
1057
1058 2
                        break;
1059 39
                    case self::XLS_TYPE_CONTINUE:
1060 1
                        $this->readContinue();
1061
1062 1
                        break;
1063 39
                    case self::XLS_TYPE_EOF:
1064 39
                        $this->readDefault();
1065
1066 39
                        break 2;
1067
                    default:
1068 38
                        $this->readDefault();
1069
1070 38
                        break;
1071
                }
1072
            }
1073
1074
            // treat MSODRAWING records, sheet-level Escher
1075 39
            if (!$this->readDataOnly && $this->drawingData) {
1076 6
                $escherWorksheet = new Escher();
1077 6
                $reader = new Xls\Escher($escherWorksheet);
1078 6
                $escherWorksheet = $reader->load($this->drawingData);
1079
1080
                // get all spContainers in one long array, so they can be mapped to OBJ records
1081 6
                $allSpContainers = $escherWorksheet->getDgContainer()->getSpgrContainer()->getAllSpContainers();
1082
            }
1083
1084
            // treat OBJ records
1085 39
            foreach ($this->objs as $n => $obj) {
1086
                // the first shape container never has a corresponding OBJ record, hence $n + 1
1087 6
                if (isset($allSpContainers[$n + 1]) && is_object($allSpContainers[$n + 1])) {
1088 6
                    $spContainer = $allSpContainers[$n + 1];
1089
1090
                    // we skip all spContainers that are a part of a group shape since we cannot yet handle those
1091 6
                    if ($spContainer->getNestingLevel() > 1) {
1092
                        continue;
1093
                    }
1094
1095
                    // calculate the width and height of the shape
1096 6
                    [$startColumn, $startRow] = Coordinate::coordinateFromString($spContainer->getStartCoordinates());
1097 6
                    [$endColumn, $endRow] = Coordinate::coordinateFromString($spContainer->getEndCoordinates());
1098
1099 6
                    $startOffsetX = $spContainer->getStartOffsetX();
1100 6
                    $startOffsetY = $spContainer->getStartOffsetY();
1101 6
                    $endOffsetX = $spContainer->getEndOffsetX();
1102 6
                    $endOffsetY = $spContainer->getEndOffsetY();
1103
1104 6
                    $width = \PhpOffice\PhpSpreadsheet\Shared\Xls::getDistanceX($this->phpSheet, $startColumn, $startOffsetX, $endColumn, $endOffsetX);
1105 6
                    $height = \PhpOffice\PhpSpreadsheet\Shared\Xls::getDistanceY($this->phpSheet, $startRow, $startOffsetY, $endRow, $endOffsetY);
1106
1107
                    // calculate offsetX and offsetY of the shape
1108 6
                    $offsetX = $startOffsetX * \PhpOffice\PhpSpreadsheet\Shared\Xls::sizeCol($this->phpSheet, $startColumn) / 1024;
1109 6
                    $offsetY = $startOffsetY * \PhpOffice\PhpSpreadsheet\Shared\Xls::sizeRow($this->phpSheet, $startRow) / 256;
1110
1111 6
                    switch ($obj['otObjType']) {
1112 6
                        case 0x19:
1113
                            // Note
1114 2
                            if (isset($this->cellNotes[$obj['idObjID']])) {
1115 2
                                $cellNote = $this->cellNotes[$obj['idObjID']];
1116
1117 2
                                if (isset($this->textObjects[$obj['idObjID']])) {
1118 2
                                    $textObject = $this->textObjects[$obj['idObjID']];
1119 2
                                    $this->cellNotes[$obj['idObjID']]['objTextData'] = $textObject;
1120
                                }
1121
                            }
1122
1123 2
                            break;
1124 6
                        case 0x08:
1125
                            // picture
1126
                            // get index to BSE entry (1-based)
1127 6
                            $BSEindex = $spContainer->getOPT(0x0104);
1128
1129
                            // If there is no BSE Index, we will fail here and other fields are not read.
1130
                            // Fix by checking here.
1131
                            // TODO: Why is there no BSE Index? Is this a new Office Version? Password protected field?
1132
                            // More likely : a uncompatible picture
1133 6
                            if (!$BSEindex) {
1134
                                continue 2;
1135
                            }
1136
1137 6
                            if ($escherWorkbook) {
1138 6
                                $BSECollection = $escherWorkbook->getDggContainer()->getBstoreContainer()->getBSECollection();
1139 6
                                $BSE = $BSECollection[$BSEindex - 1];
1140 6
                                $blipType = $BSE->getBlipType();
1141
1142
                                // need check because some blip types are not supported by Escher reader such as EMF
1143 6
                                if ($blip = $BSE->getBlip()) {
1144 6
                                    $ih = imagecreatefromstring($blip->getData());
1145 6
                                    $drawing = new MemoryDrawing();
1146 6
                                    $drawing->setImageResource($ih);
1147
1148
                                    // width, height, offsetX, offsetY
1149 6
                                    $drawing->setResizeProportional(false);
1150 6
                                    $drawing->setWidth($width);
1151 6
                                    $drawing->setHeight($height);
1152 6
                                    $drawing->setOffsetX($offsetX);
1153 6
                                    $drawing->setOffsetY($offsetY);
1154
1155 1
                                    switch ($blipType) {
1156 5
                                        case BSE::BLIPTYPE_JPEG:
1157 4
                                            $drawing->setRenderingFunction(MemoryDrawing::RENDERING_JPEG);
1158 4
                                            $drawing->setMimeType(MemoryDrawing::MIMETYPE_JPEG);
1159
1160 4
                                            break;
1161 5
                                        case BSE::BLIPTYPE_PNG:
1162 6
                                            $drawing->setRenderingFunction(MemoryDrawing::RENDERING_PNG);
1163 6
                                            $drawing->setMimeType(MemoryDrawing::MIMETYPE_PNG);
1164
1165 6
                                            break;
1166
                                    }
1167
1168 6
                                    $drawing->setWorksheet($this->phpSheet);
1169 6
                                    $drawing->setCoordinates($spContainer->getStartCoordinates());
1170
                                }
1171
                            }
1172
1173 6
                            break;
1174
                        default:
1175
                            // other object type
1176
                            break;
1177
                    }
1178
                }
1179
            }
1180
1181
            // treat SHAREDFMLA records
1182 39
            if ($this->version == self::XLS_BIFF8) {
1183 38
                foreach ($this->sharedFormulaParts as $cell => $baseCell) {
1184
                    [$column, $row] = Coordinate::coordinateFromString($cell);
1185
                    if (($this->getReadFilter() !== null) && $this->getReadFilter()->readCell($column, $row, $this->phpSheet->getTitle())) {
1186
                        $formula = $this->getFormulaFromStructure($this->sharedFormulas[$baseCell], $cell);
1187
                        $this->phpSheet->getCell($cell)->setValueExplicit('=' . $formula, DataType::TYPE_FORMULA);
1188
                    }
1189
                }
1190
            }
1191
1192 39
            if (!empty($this->cellNotes)) {
1193 2
                foreach ($this->cellNotes as $note => $noteDetails) {
1194 2
                    if (!isset($noteDetails['objTextData'])) {
1195
                        if (isset($this->textObjects[$note])) {
1196
                            $textObject = $this->textObjects[$note];
1197
                            $noteDetails['objTextData'] = $textObject;
1198
                        } else {
1199
                            $noteDetails['objTextData']['text'] = '';
1200
                        }
1201
                    }
1202 2
                    $cellAddress = str_replace('$', '', $noteDetails['cellRef']);
1203 2
                    $this->phpSheet->getComment($cellAddress)->setAuthor($noteDetails['author'])->setText($this->parseRichText($noteDetails['objTextData']['text']));
1204
                }
1205
            }
1206
        }
1207
1208
        // add the named ranges (defined names)
1209 39
        foreach ($this->definedname as $definedName) {
1210 6
            if ($definedName['isBuiltInName']) {
1211 3
                switch ($definedName['name']) {
1212 3
                    case pack('C', 0x06):
1213
                        // print area
1214
                        //    in general, formula looks like this: Foo!$C$7:$J$66,Bar!$A$1:$IV$2
1215 3
                        $ranges = explode(',', $definedName['formula']); // FIXME: what if sheetname contains comma?
1216
1217 3
                        $extractedRanges = [];
1218 3
                        foreach ($ranges as $range) {
1219
                            // $range should look like one of these
1220
                            //        Foo!$C$7:$J$66
1221
                            //        Bar!$A$1:$IV$2
1222 3
                            $explodes = Worksheet::extractSheetTitle($range, true);
1223 3
                            $sheetName = trim($explodes[0], "'");
1224 3
                            if (count($explodes) == 2) {
1225 3
                                if (strpos($explodes[1], ':') === false) {
1226
                                    $explodes[1] = $explodes[1] . ':' . $explodes[1];
1227
                                }
1228 3
                                $extractedRanges[] = str_replace('$', '', $explodes[1]); // C7:J66
1229
                            }
1230
                        }
1231 3
                        if ($docSheet = $this->spreadsheet->getSheetByName($sheetName)) {
1232 3
                            $docSheet->getPageSetup()->setPrintArea(implode(',', $extractedRanges)); // C7:J66,A1:IV2
1233
                        }
1234
1235 3
                        break;
1236
                    case pack('C', 0x07):
1237
                        // print titles (repeating rows)
1238
                        // Assuming BIFF8, there are 3 cases
1239
                        // 1. repeating rows
1240
                        //        formula looks like this: Sheet!$A$1:$IV$2
1241
                        //        rows 1-2 repeat
1242
                        // 2. repeating columns
1243
                        //        formula looks like this: Sheet!$A$1:$B$65536
1244
                        //        columns A-B repeat
1245
                        // 3. both repeating rows and repeating columns
1246
                        //        formula looks like this: Sheet!$A$1:$B$65536,Sheet!$A$1:$IV$2
1247
                        $ranges = explode(',', $definedName['formula']); // FIXME: what if sheetname contains comma?
1248
                        foreach ($ranges as $range) {
1249
                            // $range should look like this one of these
1250
                            //        Sheet!$A$1:$B$65536
1251
                            //        Sheet!$A$1:$IV$2
1252
                            if (strpos($range, '!') !== false) {
1253
                                $explodes = Worksheet::extractSheetTitle($range, true);
1254
                                if ($docSheet = $this->spreadsheet->getSheetByName($explodes[0])) {
1255
                                    $extractedRange = $explodes[1];
1256
                                    $extractedRange = str_replace('$', '', $extractedRange);
1257
1258
                                    $coordinateStrings = explode(':', $extractedRange);
1259
                                    if (count($coordinateStrings) == 2) {
1260
                                        [$firstColumn, $firstRow] = Coordinate::coordinateFromString($coordinateStrings[0]);
1261
                                        [$lastColumn, $lastRow] = Coordinate::coordinateFromString($coordinateStrings[1]);
1262
1263
                                        if ($firstColumn == 'A' && $lastColumn == 'IV') {
1264
                                            // then we have repeating rows
1265
                                            $docSheet->getPageSetup()->setRowsToRepeatAtTop([$firstRow, $lastRow]);
1266
                                        } elseif ($firstRow == 1 && $lastRow == 65536) {
1267
                                            // then we have repeating columns
1268
                                            $docSheet->getPageSetup()->setColumnsToRepeatAtLeft([$firstColumn, $lastColumn]);
1269
                                        }
1270
                                    }
1271
                                }
1272
                            }
1273
                        }
1274
1275 3
                        break;
1276
                }
1277
            } else {
1278
                // Extract range
1279 3
                if (strpos($definedName['formula'], '!') !== false) {
1280 3
                    $explodes = Worksheet::extractSheetTitle($definedName['formula'], true);
1281
                    if (
1282 3
                        ($docSheet = $this->spreadsheet->getSheetByName($explodes[0])) ||
1283 3
                        ($docSheet = $this->spreadsheet->getSheetByName(trim($explodes[0], "'")))
1284
                    ) {
1285 3
                        $extractedRange = $explodes[1];
1286 3
                        $extractedRange = str_replace('$', '', $extractedRange);
1287
1288 3
                        $localOnly = ($definedName['scope'] == 0) ? false : true;
1289
1290 3
                        $scope = ($definedName['scope'] == 0) ? null : $this->spreadsheet->getSheetByName($this->sheets[$definedName['scope'] - 1]['name']);
1291
1292 3
                        $this->spreadsheet->addNamedRange(new NamedRange((string) $definedName['name'], $docSheet, $extractedRange, $localOnly, $scope));
1293
                    }
1294
                }
1295
                //    Named Value
1296
                    //    TODO Provide support for named values
1297
            }
1298
        }
1299 39
        $this->data = null;
1300
1301 39
        return $this->spreadsheet;
1302
    }
1303
1304
    /**
1305
     * Read record data from stream, decrypting as required.
1306
     *
1307
     * @param string $data Data stream to read from
1308
     * @param int $pos Position to start reading from
1309
     * @param int $len Record data length
1310
     *
1311
     * @return string Record data
1312
     */
1313 42
    private function readRecordData($data, $pos, $len)
1314
    {
1315 42
        $data = substr($data, $pos, $len);
1316
1317
        // File not encrypted, or record before encryption start point
1318 42
        if ($this->encryption == self::MS_BIFF_CRYPTO_NONE || $pos < $this->encryptionStartPos) {
1319 42
            return $data;
1320
        }
1321
1322
        $recordData = '';
1323
        if ($this->encryption == self::MS_BIFF_CRYPTO_RC4) {
1324
            $oldBlock = floor($this->rc4Pos / self::REKEY_BLOCK);
1325
            $block = floor($pos / self::REKEY_BLOCK);
1326
            $endBlock = floor(($pos + $len) / self::REKEY_BLOCK);
1327
1328
            // Spin an RC4 decryptor to the right spot. If we have a decryptor sitting
1329
            // at a point earlier in the current block, re-use it as we can save some time.
1330
            if ($block != $oldBlock || $pos < $this->rc4Pos || !$this->rc4Key) {
1331
                $this->rc4Key = $this->makeKey($block, $this->md5Ctxt);
1332
                $step = $pos % self::REKEY_BLOCK;
1333
            } else {
1334
                $step = $pos - $this->rc4Pos;
1335
            }
1336
            $this->rc4Key->RC4(str_repeat("\0", $step));
1337
1338
            // Decrypt record data (re-keying at the end of every block)
1339
            while ($block != $endBlock) {
1340
                $step = self::REKEY_BLOCK - ($pos % self::REKEY_BLOCK);
1341
                $recordData .= $this->rc4Key->RC4(substr($data, 0, $step));
1342
                $data = substr($data, $step);
1343
                $pos += $step;
1344
                $len -= $step;
1345
                ++$block;
1346
                $this->rc4Key = $this->makeKey($block, $this->md5Ctxt);
1347
            }
1348
            $recordData .= $this->rc4Key->RC4(substr($data, 0, $len));
1349
1350
            // Keep track of the position of this decryptor.
1351
            // We'll try and re-use it later if we can to speed things up
1352
            $this->rc4Pos = $pos + $len;
1353
        } elseif ($this->encryption == self::MS_BIFF_CRYPTO_XOR) {
1354
            throw new Exception('XOr encryption not supported');
1355
        }
1356
1357
        return $recordData;
1358
    }
1359
1360
    /**
1361
     * Use OLE reader to extract the relevant data streams from the OLE file.
1362
     *
1363
     * @param string $pFilename
1364
     */
1365 42
    private function loadOLE($pFilename): void
1366
    {
1367
        // OLE reader
1368 42
        $ole = new OLERead();
1369
        // get excel data,
1370 42
        $ole->read($pFilename);
1371
        // Get workbook data: workbook stream + sheet streams
1372 42
        $this->data = $ole->getStream($ole->wrkbook);
1373
        // Get summary information data
1374 42
        $this->summaryInformation = $ole->getStream($ole->summaryInformation);
1375
        // Get additional document summary information data
1376 42
        $this->documentSummaryInformation = $ole->getStream($ole->documentSummaryInformation);
1377 42
    }
1378
1379
    /**
1380
     * Read summary information.
1381
     */
1382 39
    private function readSummaryInformation(): void
1383
    {
1384 39
        if (!isset($this->summaryInformation)) {
1385 3
            return;
1386
        }
1387
1388
        // offset: 0; size: 2; must be 0xFE 0xFF (UTF-16 LE byte order mark)
1389
        // offset: 2; size: 2;
1390
        // offset: 4; size: 2; OS version
1391
        // offset: 6; size: 2; OS indicator
1392
        // offset: 8; size: 16
1393
        // offset: 24; size: 4; section count
1394 36
        $secCount = self::getInt4d($this->summaryInformation, 24);
1395
1396
        // 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
1397
        // offset: 44; size: 4
1398 36
        $secOffset = self::getInt4d($this->summaryInformation, 44);
1399
1400
        // section header
1401
        // offset: $secOffset; size: 4; section length
1402 36
        $secLength = self::getInt4d($this->summaryInformation, $secOffset);
1403
1404
        // offset: $secOffset+4; size: 4; property count
1405 36
        $countProperties = self::getInt4d($this->summaryInformation, $secOffset + 4);
1406
1407
        // initialize code page (used to resolve string values)
1408 36
        $codePage = 'CP1252';
1409
1410
        // offset: ($secOffset+8); size: var
1411
        // loop through property decarations and properties
1412 36
        for ($i = 0; $i < $countProperties; ++$i) {
1413
            // offset: ($secOffset+8) + (8 * $i); size: 4; property ID
1414 36
            $id = self::getInt4d($this->summaryInformation, ($secOffset + 8) + (8 * $i));
1415
1416
            // Use value of property id as appropriate
1417
            // offset: ($secOffset+12) + (8 * $i); size: 4; offset from beginning of section (48)
1418 36
            $offset = self::getInt4d($this->summaryInformation, ($secOffset + 12) + (8 * $i));
1419
1420 36
            $type = self::getInt4d($this->summaryInformation, $secOffset + $offset);
1421
1422
            // initialize property value
1423 36
            $value = null;
1424
1425
            // extract property value based on property type
1426
            switch ($type) {
1427 36
                case 0x02: // 2 byte signed integer
1428 36
                    $value = self::getUInt2d($this->summaryInformation, $secOffset + 4 + $offset);
1429
1430 36
                    break;
1431 36
                case 0x03: // 4 byte signed integer
1432 35
                    $value = self::getInt4d($this->summaryInformation, $secOffset + 4 + $offset);
1433
1434 35
                    break;
1435 36
                case 0x13: // 4 byte unsigned integer
1436
                    // not needed yet, fix later if necessary
1437
                    break;
1438 36
                case 0x1E: // null-terminated string prepended by dword string length
1439 36
                    $byteLength = self::getInt4d($this->summaryInformation, $secOffset + 4 + $offset);
1440 36
                    $value = substr($this->summaryInformation, $secOffset + 8 + $offset, $byteLength);
1441 36
                    $value = StringHelper::convertEncoding($value, 'UTF-8', $codePage);
1442 36
                    $value = rtrim($value);
1443
1444 36
                    break;
1445 36
                case 0x40: // Filetime (64-bit value representing the number of 100-nanosecond intervals since January 1, 1601)
1446
                    // PHP-time
1447 36
                    $value = OLE::OLE2LocalDate(substr($this->summaryInformation, $secOffset + 4 + $offset, 8));
1448
1449 36
                    break;
1450
                case 0x47: // Clipboard format
1451
                    // not needed yet, fix later if necessary
1452
                    break;
1453
            }
1454
1455
            switch ($id) {
1456 36
                case 0x01:    //    Code Page
1457 36
                    $codePage = CodePage::numberToName($value);
1458
1459 36
                    break;
1460 36
                case 0x02:    //    Title
1461 20
                    $this->spreadsheet->getProperties()->setTitle($value);
1462
1463 20
                    break;
1464 36
                case 0x03:    //    Subject
1465 6
                    $this->spreadsheet->getProperties()->setSubject($value);
1466
1467 6
                    break;
1468 36
                case 0x04:    //    Author (Creator)
1469 34
                    $this->spreadsheet->getProperties()->setCreator($value);
1470
1471 34
                    break;
1472 36
                case 0x05:    //    Keywords
1473 6
                    $this->spreadsheet->getProperties()->setKeywords($value);
1474
1475 6
                    break;
1476 36
                case 0x06:    //    Comments (Description)
1477 6
                    $this->spreadsheet->getProperties()->setDescription($value);
1478
1479 6
                    break;
1480 36
                case 0x07:    //    Template
1481
                    //    Not supported by PhpSpreadsheet
1482
                    break;
1483 36
                case 0x08:    //    Last Saved By (LastModifiedBy)
1484 35
                    $this->spreadsheet->getProperties()->setLastModifiedBy($value);
1485
1486 35
                    break;
1487 36
                case 0x09:    //    Revision
1488
                    //    Not supported by PhpSpreadsheet
1489 1
                    break;
1490 36
                case 0x0A:    //    Total Editing Time
1491
                    //    Not supported by PhpSpreadsheet
1492 1
                    break;
1493 36
                case 0x0B:    //    Last Printed
1494
                    //    Not supported by PhpSpreadsheet
1495 3
                    break;
1496 36
                case 0x0C:    //    Created Date/Time
1497 36
                    $this->spreadsheet->getProperties()->setCreated($value);
1498
1499 36
                    break;
1500 36
                case 0x0D:    //    Modified Date/Time
1501 36
                    $this->spreadsheet->getProperties()->setModified($value);
1502
1503 36
                    break;
1504 35
                case 0x0E:    //    Number of Pages
1505
                    //    Not supported by PhpSpreadsheet
1506
                    break;
1507 35
                case 0x0F:    //    Number of Words
1508
                    //    Not supported by PhpSpreadsheet
1509
                    break;
1510 35
                case 0x10:    //    Number of Characters
1511
                    //    Not supported by PhpSpreadsheet
1512
                    break;
1513 35
                case 0x11:    //    Thumbnail
1514
                    //    Not supported by PhpSpreadsheet
1515
                    break;
1516 35
                case 0x12:    //    Name of creating application
1517
                    //    Not supported by PhpSpreadsheet
1518 16
                    break;
1519 35
                case 0x13:    //    Security
1520
                    //    Not supported by PhpSpreadsheet
1521 35
                    break;
1522
            }
1523
        }
1524 36
    }
1525
1526
    /**
1527
     * Read additional document summary information.
1528
     */
1529 39
    private function readDocumentSummaryInformation(): void
1530
    {
1531 39
        if (!isset($this->documentSummaryInformation)) {
1532 3
            return;
1533
        }
1534
1535
        //    offset: 0;    size: 2;    must be 0xFE 0xFF (UTF-16 LE byte order mark)
1536
        //    offset: 2;    size: 2;
1537
        //    offset: 4;    size: 2;    OS version
1538
        //    offset: 6;    size: 2;    OS indicator
1539
        //    offset: 8;    size: 16
1540
        //    offset: 24;    size: 4;    section count
1541 36
        $secCount = self::getInt4d($this->documentSummaryInformation, 24);
1542
1543
        // 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
1544
        // offset: 44;    size: 4;    first section offset
1545 36
        $secOffset = self::getInt4d($this->documentSummaryInformation, 44);
1546
1547
        //    section header
1548
        //    offset: $secOffset;    size: 4;    section length
1549 36
        $secLength = self::getInt4d($this->documentSummaryInformation, $secOffset);
1550
1551
        //    offset: $secOffset+4;    size: 4;    property count
1552 36
        $countProperties = self::getInt4d($this->documentSummaryInformation, $secOffset + 4);
1553
1554
        // initialize code page (used to resolve string values)
1555 36
        $codePage = 'CP1252';
1556
1557
        //    offset: ($secOffset+8);    size: var
1558
        //    loop through property decarations and properties
1559 36
        for ($i = 0; $i < $countProperties; ++$i) {
1560
            //    offset: ($secOffset+8) + (8 * $i);    size: 4;    property ID
1561 36
            $id = self::getInt4d($this->documentSummaryInformation, ($secOffset + 8) + (8 * $i));
1562
1563
            // Use value of property id as appropriate
1564
            // offset: 60 + 8 * $i;    size: 4;    offset from beginning of section (48)
1565 36
            $offset = self::getInt4d($this->documentSummaryInformation, ($secOffset + 12) + (8 * $i));
1566
1567 36
            $type = self::getInt4d($this->documentSummaryInformation, $secOffset + $offset);
1568
1569
            // initialize property value
1570 36
            $value = null;
1571
1572
            // extract property value based on property type
1573
            switch ($type) {
1574 36
                case 0x02:    //    2 byte signed integer
1575 36
                    $value = self::getUInt2d($this->documentSummaryInformation, $secOffset + 4 + $offset);
1576
1577 36
                    break;
1578 35
                case 0x03:    //    4 byte signed integer
1579 34
                    $value = self::getInt4d($this->documentSummaryInformation, $secOffset + 4 + $offset);
1580
1581 34
                    break;
1582 35
                case 0x0B:  // Boolean
1583 35
                    $value = self::getUInt2d($this->documentSummaryInformation, $secOffset + 4 + $offset);
1584 35
                    $value = ($value == 0 ? false : true);
1585
1586 35
                    break;
1587 35
                case 0x13:    //    4 byte unsigned integer
1588
                    // not needed yet, fix later if necessary
1589
                    break;
1590 35
                case 0x1E:    //    null-terminated string prepended by dword string length
1591 19
                    $byteLength = self::getInt4d($this->documentSummaryInformation, $secOffset + 4 + $offset);
1592 19
                    $value = substr($this->documentSummaryInformation, $secOffset + 8 + $offset, $byteLength);
1593 19
                    $value = StringHelper::convertEncoding($value, 'UTF-8', $codePage);
1594 19
                    $value = rtrim($value);
1595
1596 19
                    break;
1597 35
                case 0x40:    //    Filetime (64-bit value representing the number of 100-nanosecond intervals since January 1, 1601)
1598
                    // PHP-Time
1599
                    $value = OLE::OLE2LocalDate(substr($this->documentSummaryInformation, $secOffset + 4 + $offset, 8));
1600
1601
                    break;
1602 35
                case 0x47:    //    Clipboard format
1603
                    // not needed yet, fix later if necessary
1604
                    break;
1605
            }
1606
1607
            switch ($id) {
1608 36
                case 0x01:    //    Code Page
1609 36
                    $codePage = CodePage::numberToName($value);
1610
1611 36
                    break;
1612 35
                case 0x02:    //    Category
1613 6
                    $this->spreadsheet->getProperties()->setCategory($value);
1614
1615 6
                    break;
1616 35
                case 0x03:    //    Presentation Target
1617
                    //    Not supported by PhpSpreadsheet
1618
                    break;
1619 35
                case 0x04:    //    Bytes
1620
                    //    Not supported by PhpSpreadsheet
1621
                    break;
1622 35
                case 0x05:    //    Lines
1623
                    //    Not supported by PhpSpreadsheet
1624
                    break;
1625 35
                case 0x06:    //    Paragraphs
1626
                    //    Not supported by PhpSpreadsheet
1627
                    break;
1628 35
                case 0x07:    //    Slides
1629
                    //    Not supported by PhpSpreadsheet
1630
                    break;
1631 35
                case 0x08:    //    Notes
1632
                    //    Not supported by PhpSpreadsheet
1633
                    break;
1634 35
                case 0x09:    //    Hidden Slides
1635
                    //    Not supported by PhpSpreadsheet
1636
                    break;
1637 35
                case 0x0A:    //    MM Clips
1638
                    //    Not supported by PhpSpreadsheet
1639
                    break;
1640 35
                case 0x0B:    //    Scale Crop
1641
                    //    Not supported by PhpSpreadsheet
1642 35
                    break;
1643 35
                case 0x0C:    //    Heading Pairs
1644
                    //    Not supported by PhpSpreadsheet
1645 35
                    break;
1646 35
                case 0x0D:    //    Titles of Parts
1647
                    //    Not supported by PhpSpreadsheet
1648 35
                    break;
1649 35
                case 0x0E:    //    Manager
1650 2
                    $this->spreadsheet->getProperties()->setManager($value);
1651
1652 2
                    break;
1653 35
                case 0x0F:    //    Company
1654 18
                    $this->spreadsheet->getProperties()->setCompany($value);
1655
1656 18
                    break;
1657 35
                case 0x10:    //    Links up-to-date
1658
                    //    Not supported by PhpSpreadsheet
1659 35
                    break;
1660
            }
1661
        }
1662 36
    }
1663
1664
    /**
1665
     * Reads a general type of BIFF record. Does nothing except for moving stream pointer forward to next record.
1666
     */
1667 42
    private function readDefault(): void
1668
    {
1669 42
        $length = self::getUInt2d($this->data, $this->pos + 2);
1670
1671
        // move stream pointer to next record
1672 42
        $this->pos += 4 + $length;
1673 42
    }
1674
1675
    /**
1676
     *    The NOTE record specifies a comment associated with a particular cell. In Excel 95 (BIFF7) and earlier versions,
1677
     *        this record stores a note (cell note). This feature was significantly enhanced in Excel 97.
1678
     */
1679 3
    private function readNote(): void
1680
    {
1681 3
        $length = self::getUInt2d($this->data, $this->pos + 2);
1682 3
        $recordData = $this->readRecordData($this->data, $this->pos + 4, $length);
1683
1684
        // move stream pointer to next record
1685 3
        $this->pos += 4 + $length;
1686
1687 3
        if ($this->readDataOnly) {
1688
            return;
1689
        }
1690
1691 3
        $cellAddress = $this->readBIFF8CellAddress(substr($recordData, 0, 4));
1692 3
        if ($this->version == self::XLS_BIFF8) {
1693 2
            $noteObjID = self::getUInt2d($recordData, 6);
1694 2
            $noteAuthor = self::readUnicodeStringLong(substr($recordData, 8));
1695 2
            $noteAuthor = $noteAuthor['value'];
1696 2
            $this->cellNotes[$noteObjID] = [
1697 2
                'cellRef' => $cellAddress,
1698 2
                'objectID' => $noteObjID,
1699 2
                'author' => $noteAuthor,
1700
            ];
1701
        } else {
1702 1
            $extension = false;
1703 1
            if ($cellAddress == '$B$65536') {
1704
                //    If the address row is -1 and the column is 0, (which translates as $B$65536) then this is a continuation
1705
                //        note from the previous cell annotation. We're not yet handling this, so annotations longer than the
1706
                //        max 2048 bytes will probably throw a wobbly.
1707
                $row = self::getUInt2d($recordData, 0);
1708
                $extension = true;
1709
                $arrayKeys = array_keys($this->phpSheet->getComments());
1710
                $cellAddress = array_pop($arrayKeys);
1711
            }
1712
1713 1
            $cellAddress = str_replace('$', '', $cellAddress);
1714 1
            $noteLength = self::getUInt2d($recordData, 4);
1715 1
            $noteText = trim(substr($recordData, 6));
1716
1717 1
            if ($extension) {
1718
                //    Concatenate this extension with the currently set comment for the cell
1719
                $comment = $this->phpSheet->getComment($cellAddress);
1720
                $commentText = $comment->getText()->getPlainText();
1721
                $comment->setText($this->parseRichText($commentText . $noteText));
1722
            } else {
1723
                //    Set comment for the cell
1724 1
                $this->phpSheet->getComment($cellAddress)->setText($this->parseRichText($noteText));
1725
//                                                    ->setAuthor($author)
1726
            }
1727
        }
1728 3
    }
1729
1730
    /**
1731
     * The TEXT Object record contains the text associated with a cell annotation.
1732
     */
1733 2
    private function readTextObject(): void
1734
    {
1735 2
        $length = self::getUInt2d($this->data, $this->pos + 2);
1736 2
        $recordData = $this->readRecordData($this->data, $this->pos + 4, $length);
1737
1738
        // move stream pointer to next record
1739 2
        $this->pos += 4 + $length;
1740
1741 2
        if ($this->readDataOnly) {
1742
            return;
1743
        }
1744
1745
        // recordData consists of an array of subrecords looking like this:
1746
        //    grbit: 2 bytes; Option Flags
1747
        //    rot: 2 bytes; rotation
1748
        //    cchText: 2 bytes; length of the text (in the first continue record)
1749
        //    cbRuns: 2 bytes; length of the formatting (in the second continue record)
1750
        // followed by the continuation records containing the actual text and formatting
1751 2
        $grbitOpts = self::getUInt2d($recordData, 0);
1752 2
        $rot = self::getUInt2d($recordData, 2);
1753 2
        $cchText = self::getUInt2d($recordData, 10);
1754 2
        $cbRuns = self::getUInt2d($recordData, 12);
1755 2
        $text = $this->getSplicedRecordData();
1756
1757 2
        $textByte = $text['spliceOffsets'][1] - $text['spliceOffsets'][0] - 1;
1758 2
        $textStr = substr($text['recordData'], $text['spliceOffsets'][0] + 1, $textByte);
1759
        // get 1 byte
1760 2
        $is16Bit = ord($text['recordData'][0]);
1761
        // it is possible to use a compressed format,
1762
        // which omits the high bytes of all characters, if they are all zero
1763 2
        if (($is16Bit & 0x01) === 0) {
1764 2
            $textStr = StringHelper::ConvertEncoding($textStr, 'UTF-8', 'ISO-8859-1');
1765
        } else {
1766
            $textStr = $this->decodeCodepage($textStr);
1767
        }
1768
1769 2
        $this->textObjects[$this->textObjRef] = [
1770 2
            'text' => $textStr,
1771 2
            'format' => substr($text['recordData'], $text['spliceOffsets'][1], $cbRuns),
1772 2
            'alignment' => $grbitOpts,
1773 2
            'rotation' => $rot,
1774
        ];
1775 2
    }
1776
1777
    /**
1778
     * Read BOF.
1779
     */
1780 42
    private function readBof(): void
1781
    {
1782 42
        $length = self::getUInt2d($this->data, $this->pos + 2);
1783 42
        $recordData = substr($this->data, $this->pos + 4, $length);
1784
1785
        // move stream pointer to next record
1786 42
        $this->pos += 4 + $length;
1787
1788
        // offset: 2; size: 2; type of the following data
1789 42
        $substreamType = self::getUInt2d($recordData, 2);
1790
1791
        switch ($substreamType) {
1792 42
            case self::XLS_WORKBOOKGLOBALS:
1793 42
                $version = self::getUInt2d($recordData, 0);
1794 42
                if (($version != self::XLS_BIFF8) && ($version != self::XLS_BIFF7)) {
1795
                    throw new Exception('Cannot read this Excel file. Version is too old.');
1796
                }
1797 42
                $this->version = $version;
1798
1799 42
                break;
1800 40
            case self::XLS_WORKSHEET:
1801
                // do not use this version information for anything
1802
                // it is unreliable (OpenOffice doc, 5.8), use only version information from the global stream
1803 40
                break;
1804
            default:
1805
                // substream, e.g. chart
1806
                // just skip the entire substream
1807
                do {
1808
                    $code = self::getUInt2d($this->data, $this->pos);
1809
                    $this->readDefault();
1810
                } while ($code != self::XLS_TYPE_EOF && $this->pos < $this->dataSize);
1811
1812
                break;
1813
        }
1814 42
    }
1815
1816
    /**
1817
     * FILEPASS.
1818
     *
1819
     * This record is part of the File Protection Block. It
1820
     * contains information about the read/write password of the
1821
     * file. All record contents following this record will be
1822
     * encrypted.
1823
     *
1824
     * --    "OpenOffice.org's Documentation of the Microsoft
1825
     *         Excel File Format"
1826
     *
1827
     * The decryption functions and objects used from here on in
1828
     * are based on the source of Spreadsheet-ParseExcel:
1829
     * https://metacpan.org/release/Spreadsheet-ParseExcel
1830
     */
1831
    private function readFilepass(): void
1832
    {
1833
        $length = self::getUInt2d($this->data, $this->pos + 2);
1834
1835
        if ($length != 54) {
1836
            throw new Exception('Unexpected file pass record length');
1837
        }
1838
1839
        $recordData = $this->readRecordData($this->data, $this->pos + 4, $length);
1840
1841
        // move stream pointer to next record
1842
        $this->pos += 4 + $length;
1843
1844
        if (!$this->verifyPassword('VelvetSweatshop', substr($recordData, 6, 16), substr($recordData, 22, 16), substr($recordData, 38, 16), $this->md5Ctxt)) {
1845
            throw new Exception('Decryption password incorrect');
1846
        }
1847
1848
        $this->encryption = self::MS_BIFF_CRYPTO_RC4;
1849
1850
        // Decryption required from the record after next onwards
1851
        $this->encryptionStartPos = $this->pos + self::getUInt2d($this->data, $this->pos + 2);
1852
    }
1853
1854
    /**
1855
     * Make an RC4 decryptor for the given block.
1856
     *
1857
     * @param int $block Block for which to create decrypto
1858
     * @param string $valContext MD5 context state
1859
     *
1860
     * @return Xls\RC4
1861
     */
1862
    private function makeKey($block, $valContext)
1863
    {
1864
        $pwarray = str_repeat("\0", 64);
1865
1866
        for ($i = 0; $i < 5; ++$i) {
1867
            $pwarray[$i] = $valContext[$i];
1868
        }
1869
1870
        $pwarray[5] = chr($block & 0xff);
1871
        $pwarray[6] = chr(($block >> 8) & 0xff);
1872
        $pwarray[7] = chr(($block >> 16) & 0xff);
1873
        $pwarray[8] = chr(($block >> 24) & 0xff);
1874
1875
        $pwarray[9] = "\x80";
1876
        $pwarray[56] = "\x48";
1877
1878
        $md5 = new Xls\MD5();
1879
        $md5->add($pwarray);
1880
1881
        $s = $md5->getContext();
1882
1883
        return new Xls\RC4($s);
1884
    }
1885
1886
    /**
1887
     * Verify RC4 file password.
1888
     *
1889
     * @param string $password Password to check
1890
     * @param string $docid Document id
1891
     * @param string $salt_data Salt data
1892
     * @param string $hashedsalt_data Hashed salt data
1893
     * @param string $valContext Set to the MD5 context of the value
1894
     *
1895
     * @return bool Success
1896
     */
1897
    private function verifyPassword($password, $docid, $salt_data, $hashedsalt_data, &$valContext)
1898
    {
1899
        $pwarray = str_repeat("\0", 64);
1900
1901
        $iMax = strlen($password);
1902
        for ($i = 0; $i < $iMax; ++$i) {
1903
            $o = ord(substr($password, $i, 1));
1904
            $pwarray[2 * $i] = chr($o & 0xff);
1905
            $pwarray[2 * $i + 1] = chr(($o >> 8) & 0xff);
1906
        }
1907
        $pwarray[2 * $i] = chr(0x80);
1908
        $pwarray[56] = chr(($i << 4) & 0xff);
1909
1910
        $md5 = new Xls\MD5();
1911
        $md5->add($pwarray);
1912
1913
        $mdContext1 = $md5->getContext();
1914
1915
        $offset = 0;
1916
        $keyoffset = 0;
1917
        $tocopy = 5;
1918
1919
        $md5->reset();
1920
1921
        while ($offset != 16) {
1922
            if ((64 - $offset) < 5) {
1923
                $tocopy = 64 - $offset;
1924
            }
1925
            for ($i = 0; $i <= $tocopy; ++$i) {
1926
                $pwarray[$offset + $i] = $mdContext1[$keyoffset + $i];
1927
            }
1928
            $offset += $tocopy;
1929
1930
            if ($offset == 64) {
1931
                $md5->add($pwarray);
1932
                $keyoffset = $tocopy;
1933
                $tocopy = 5 - $tocopy;
1934
                $offset = 0;
1935
1936
                continue;
1937
            }
1938
1939
            $keyoffset = 0;
1940
            $tocopy = 5;
1941
            for ($i = 0; $i < 16; ++$i) {
1942
                $pwarray[$offset + $i] = $docid[$i];
1943
            }
1944
            $offset += 16;
1945
        }
1946
1947
        $pwarray[16] = "\x80";
1948
        for ($i = 0; $i < 47; ++$i) {
1949
            $pwarray[17 + $i] = "\0";
1950
        }
1951
        $pwarray[56] = "\x80";
1952
        $pwarray[57] = "\x0a";
1953
1954
        $md5->add($pwarray);
1955
        $valContext = $md5->getContext();
1956
1957
        $key = $this->makeKey(0, $valContext);
1958
1959
        $salt = $key->RC4($salt_data);
1960
        $hashedsalt = $key->RC4($hashedsalt_data);
1961
1962
        $salt .= "\x80" . str_repeat("\0", 47);
1963
        $salt[56] = "\x80";
1964
1965
        $md5->reset();
1966
        $md5->add($salt);
1967
        $mdContext2 = $md5->getContext();
1968
1969
        return $mdContext2 == $hashedsalt;
1970
    }
1971
1972
    /**
1973
     * CODEPAGE.
1974
     *
1975
     * This record stores the text encoding used to write byte
1976
     * strings, stored as MS Windows code page identifier.
1977
     *
1978
     * --    "OpenOffice.org's Documentation of the Microsoft
1979
     *         Excel File Format"
1980
     */
1981 36
    private function readCodepage(): void
1982
    {
1983 36
        $length = self::getUInt2d($this->data, $this->pos + 2);
1984 36
        $recordData = $this->readRecordData($this->data, $this->pos + 4, $length);
1985
1986
        // move stream pointer to next record
1987 36
        $this->pos += 4 + $length;
1988
1989
        // offset: 0; size: 2; code page identifier
1990 36
        $codepage = self::getUInt2d($recordData, 0);
1991
1992 36
        $this->codepage = CodePage::numberToName($codepage);
1993 36
    }
1994
1995
    /**
1996
     * DATEMODE.
1997
     *
1998
     * This record specifies the base date for displaying date
1999
     * values. All dates are stored as count of days past this
2000
     * base date. In BIFF2-BIFF4 this record is part of the
2001
     * Calculation Settings Block. In BIFF5-BIFF8 it is
2002
     * stored in the Workbook Globals Substream.
2003
     *
2004
     * --    "OpenOffice.org's Documentation of the Microsoft
2005
     *         Excel File Format"
2006
     */
2007 38
    private function readDateMode(): void
2008
    {
2009 38
        $length = self::getUInt2d($this->data, $this->pos + 2);
2010 38
        $recordData = $this->readRecordData($this->data, $this->pos + 4, $length);
2011
2012
        // move stream pointer to next record
2013 38
        $this->pos += 4 + $length;
2014
2015
        // offset: 0; size: 2; 0 = base 1900, 1 = base 1904
2016 38
        Date::setExcelCalendar(Date::CALENDAR_WINDOWS_1900);
2017 38
        if (ord($recordData[0]) == 1) {
2018
            Date::setExcelCalendar(Date::CALENDAR_MAC_1904);
2019
        }
2020 38
    }
2021
2022
    /**
2023
     * Read a FONT record.
2024
     */
2025 39
    private function readFont(): void
2026
    {
2027 39
        $length = self::getUInt2d($this->data, $this->pos + 2);
2028 39
        $recordData = $this->readRecordData($this->data, $this->pos + 4, $length);
2029
2030
        // move stream pointer to next record
2031 39
        $this->pos += 4 + $length;
2032
2033 39
        if (!$this->readDataOnly) {
2034 38
            $objFont = new Font();
2035
2036
            // offset: 0; size: 2; height of the font (in twips = 1/20 of a point)
2037 38
            $size = self::getUInt2d($recordData, 0);
2038 38
            $objFont->setSize($size / 20);
2039
2040
            // offset: 2; size: 2; option flags
2041
            // bit: 0; mask 0x0001; bold (redundant in BIFF5-BIFF8)
2042
            // bit: 1; mask 0x0002; italic
2043 38
            $isItalic = (0x0002 & self::getUInt2d($recordData, 2)) >> 1;
2044 38
            if ($isItalic) {
2045 9
                $objFont->setItalic(true);
2046
            }
2047
2048
            // bit: 2; mask 0x0004; underlined (redundant in BIFF5-BIFF8)
2049
            // bit: 3; mask 0x0008; strikethrough
2050 38
            $isStrike = (0x0008 & self::getUInt2d($recordData, 2)) >> 3;
2051 38
            if ($isStrike) {
2052
                $objFont->setStrikethrough(true);
2053
            }
2054
2055
            // offset: 4; size: 2; colour index
2056 38
            $colorIndex = self::getUInt2d($recordData, 4);
2057 38
            $objFont->colorIndex = $colorIndex;
2058
2059
            // offset: 6; size: 2; font weight
2060 38
            $weight = self::getUInt2d($recordData, 6);
2061
            switch ($weight) {
2062 38
                case 0x02BC:
2063 21
                    $objFont->setBold(true);
2064
2065 21
                    break;
2066
            }
2067
2068
            // offset: 8; size: 2; escapement type
2069 38
            $escapement = self::getUInt2d($recordData, 8);
2070
            switch ($escapement) {
2071 38
                case 0x0001:
2072
                    $objFont->setSuperscript(true);
2073
2074
                    break;
2075 38
                case 0x0002:
2076
                    $objFont->setSubscript(true);
2077
2078
                    break;
2079
            }
2080
2081
            // offset: 10; size: 1; underline type
2082 38
            $underlineType = ord($recordData[10]);
2083
            switch ($underlineType) {
2084 38
                case 0x00:
2085 38
                    break; // no underline
2086 4
                case 0x01:
2087 4
                    $objFont->setUnderline(Font::UNDERLINE_SINGLE);
2088
2089 4
                    break;
2090
                case 0x02:
2091
                    $objFont->setUnderline(Font::UNDERLINE_DOUBLE);
2092
2093
                    break;
2094
                case 0x21:
2095
                    $objFont->setUnderline(Font::UNDERLINE_SINGLEACCOUNTING);
2096
2097
                    break;
2098
                case 0x22:
2099
                    $objFont->setUnderline(Font::UNDERLINE_DOUBLEACCOUNTING);
2100
2101
                    break;
2102
            }
2103
2104
            // offset: 11; size: 1; font family
2105
            // offset: 12; size: 1; character set
2106
            // offset: 13; size: 1; not used
2107
            // offset: 14; size: var; font name
2108 38
            if ($this->version == self::XLS_BIFF8) {
2109 37
                $string = self::readUnicodeStringShort(substr($recordData, 14));
2110
            } else {
2111 1
                $string = $this->readByteStringShort(substr($recordData, 14));
2112
            }
2113 38
            $objFont->setName($string['value']);
2114
2115 38
            $this->objFonts[] = $objFont;
2116
        }
2117 39
    }
2118
2119
    /**
2120
     * FORMAT.
2121
     *
2122
     * This record contains information about a number format.
2123
     * All FORMAT records occur together in a sequential list.
2124
     *
2125
     * In BIFF2-BIFF4 other records referencing a FORMAT record
2126
     * contain a zero-based index into this list. From BIFF5 on
2127
     * the FORMAT record contains the index itself that will be
2128
     * used by other records.
2129
     *
2130
     * --    "OpenOffice.org's Documentation of the Microsoft
2131
     *         Excel File Format"
2132
     */
2133 24
    private function readFormat(): void
2134
    {
2135 24
        $length = self::getUInt2d($this->data, $this->pos + 2);
2136 24
        $recordData = $this->readRecordData($this->data, $this->pos + 4, $length);
2137
2138
        // move stream pointer to next record
2139 24
        $this->pos += 4 + $length;
2140
2141 24
        if (!$this->readDataOnly) {
2142 23
            $indexCode = self::getUInt2d($recordData, 0);
2143
2144 23
            if ($this->version == self::XLS_BIFF8) {
2145 22
                $string = self::readUnicodeStringLong(substr($recordData, 2));
2146
            } else {
2147
                // BIFF7
2148 1
                $string = $this->readByteStringShort(substr($recordData, 2));
2149
            }
2150
2151 23
            $formatString = $string['value'];
2152 23
            $this->formats[$indexCode] = $formatString;
2153
        }
2154 24
    }
2155
2156
    /**
2157
     * XF - Extended Format.
2158
     *
2159
     * This record contains formatting information for cells, rows, columns or styles.
2160
     * According to https://support.microsoft.com/en-us/help/147732 there are always at least 15 cell style XF
2161
     * and 1 cell XF.
2162
     * Inspection of Excel files generated by MS Office Excel shows that XF records 0-14 are cell style XF
2163
     * and XF record 15 is a cell XF
2164
     * We only read the first cell style XF and skip the remaining cell style XF records
2165
     * We read all cell XF records.
2166
     *
2167
     * --    "OpenOffice.org's Documentation of the Microsoft
2168
     *         Excel File Format"
2169
     */
2170 39
    private function readXf(): void
2171
    {
2172 39
        $length = self::getUInt2d($this->data, $this->pos + 2);
2173 39
        $recordData = $this->readRecordData($this->data, $this->pos + 4, $length);
2174
2175
        // move stream pointer to next record
2176 39
        $this->pos += 4 + $length;
2177
2178 39
        $objStyle = new Style();
2179
2180 39
        if (!$this->readDataOnly) {
2181
            // offset:  0; size: 2; Index to FONT record
2182 38
            if (self::getUInt2d($recordData, 0) < 4) {
2183 38
                $fontIndex = self::getUInt2d($recordData, 0);
2184
            } else {
2185
                // this has to do with that index 4 is omitted in all BIFF versions for some strange reason
2186
                // check the OpenOffice documentation of the FONT record
2187 24
                $fontIndex = self::getUInt2d($recordData, 0) - 1;
2188
            }
2189 38
            $objStyle->setFont($this->objFonts[$fontIndex]);
2190
2191
            // offset:  2; size: 2; Index to FORMAT record
2192 38
            $numberFormatIndex = self::getUInt2d($recordData, 2);
2193 38
            if (isset($this->formats[$numberFormatIndex])) {
2194
                // then we have user-defined format code
2195 21
                $numberFormat = ['formatCode' => $this->formats[$numberFormatIndex]];
2196 38
            } elseif (($code = NumberFormat::builtInFormatCode($numberFormatIndex)) !== '') {
2197
                // then we have built-in format code
2198 38
                $numberFormat = ['formatCode' => $code];
2199
            } else {
2200
                // we set the general format code
2201 2
                $numberFormat = ['formatCode' => 'General'];
2202
            }
2203 38
            $objStyle->getNumberFormat()->setFormatCode($numberFormat['formatCode']);
2204
2205
            // offset:  4; size: 2; XF type, cell protection, and parent style XF
2206
            // bit 2-0; mask 0x0007; XF_TYPE_PROT
2207 38
            $xfTypeProt = self::getUInt2d($recordData, 4);
2208
            // bit 0; mask 0x01; 1 = cell is locked
2209 38
            $isLocked = (0x01 & $xfTypeProt) >> 0;
2210 38
            $objStyle->getProtection()->setLocked($isLocked ? Protection::PROTECTION_INHERIT : Protection::PROTECTION_UNPROTECTED);
2211
2212
            // bit 1; mask 0x02; 1 = Formula is hidden
2213 38
            $isHidden = (0x02 & $xfTypeProt) >> 1;
2214 38
            $objStyle->getProtection()->setHidden($isHidden ? Protection::PROTECTION_PROTECTED : Protection::PROTECTION_UNPROTECTED);
2215
2216
            // bit 2; mask 0x04; 0 = Cell XF, 1 = Cell Style XF
2217 38
            $isCellStyleXf = (0x04 & $xfTypeProt) >> 2;
2218
2219
            // offset:  6; size: 1; Alignment and text break
2220
            // bit 2-0, mask 0x07; horizontal alignment
2221 38
            $horAlign = (0x07 & ord($recordData[6])) >> 0;
2222
            switch ($horAlign) {
2223 38
                case 0:
2224 38
                    $objStyle->getAlignment()->setHorizontal(Alignment::HORIZONTAL_GENERAL);
2225
2226 38
                    break;
2227 15
                case 1:
2228 5
                    $objStyle->getAlignment()->setHorizontal(Alignment::HORIZONTAL_LEFT);
2229
2230 5
                    break;
2231 15
                case 2:
2232 11
                    $objStyle->getAlignment()->setHorizontal(Alignment::HORIZONTAL_CENTER);
2233
2234 11
                    break;
2235 5
                case 3:
2236 5
                    $objStyle->getAlignment()->setHorizontal(Alignment::HORIZONTAL_RIGHT);
2237
2238 5
                    break;
2239 4
                case 4:
2240
                    $objStyle->getAlignment()->setHorizontal(Alignment::HORIZONTAL_FILL);
2241
2242
                    break;
2243 4
                case 5:
2244 4
                    $objStyle->getAlignment()->setHorizontal(Alignment::HORIZONTAL_JUSTIFY);
2245
2246 4
                    break;
2247
                case 6:
2248
                    $objStyle->getAlignment()->setHorizontal(Alignment::HORIZONTAL_CENTER_CONTINUOUS);
2249
2250
                    break;
2251
            }
2252
            // bit 3, mask 0x08; wrap text
2253 38
            $wrapText = (0x08 & ord($recordData[6])) >> 3;
2254
            switch ($wrapText) {
2255 38
                case 0:
2256 38
                    $objStyle->getAlignment()->setWrapText(false);
2257
2258 38
                    break;
2259 4
                case 1:
2260 4
                    $objStyle->getAlignment()->setWrapText(true);
2261
2262 4
                    break;
2263
            }
2264
            // bit 6-4, mask 0x70; vertical alignment
2265 38
            $vertAlign = (0x70 & ord($recordData[6])) >> 4;
2266
            switch ($vertAlign) {
2267 38
                case 0:
2268
                    $objStyle->getAlignment()->setVertical(Alignment::VERTICAL_TOP);
2269
2270
                    break;
2271 38
                case 1:
2272 5
                    $objStyle->getAlignment()->setVertical(Alignment::VERTICAL_CENTER);
2273
2274 5
                    break;
2275 38
                case 2:
2276 38
                    $objStyle->getAlignment()->setVertical(Alignment::VERTICAL_BOTTOM);
2277
2278 38
                    break;
2279
                case 3:
2280
                    $objStyle->getAlignment()->setVertical(Alignment::VERTICAL_JUSTIFY);
2281
2282
                    break;
2283
            }
2284
2285 38
            if ($this->version == self::XLS_BIFF8) {
2286
                // offset:  7; size: 1; XF_ROTATION: Text rotation angle
2287 37
                $angle = ord($recordData[7]);
2288 37
                $rotation = 0;
2289 37
                if ($angle <= 90) {
2290 37
                    $rotation = $angle;
2291 2
                } elseif ($angle <= 180) {
2292
                    $rotation = 90 - $angle;
2293 2
                } elseif ($angle == Alignment::TEXTROTATION_STACK_EXCEL) {
2294 2
                    $rotation = Alignment::TEXTROTATION_STACK_PHPSPREADSHEET;
2295
                }
2296 37
                $objStyle->getAlignment()->setTextRotation($rotation);
2297
2298
                // offset:  8; size: 1; Indentation, shrink to cell size, and text direction
2299
                // bit: 3-0; mask: 0x0F; indent level
2300 37
                $indent = (0x0F & ord($recordData[8])) >> 0;
2301 37
                $objStyle->getAlignment()->setIndent($indent);
2302
2303
                // bit: 4; mask: 0x10; 1 = shrink content to fit into cell
2304 37
                $shrinkToFit = (0x10 & ord($recordData[8])) >> 4;
2305
                switch ($shrinkToFit) {
2306 37
                    case 0:
2307 37
                        $objStyle->getAlignment()->setShrinkToFit(false);
2308
2309 37
                        break;
2310 1
                    case 1:
2311 1
                        $objStyle->getAlignment()->setShrinkToFit(true);
2312
2313 1
                        break;
2314
                }
2315
2316
                // offset:  9; size: 1; Flags used for attribute groups
2317
2318
                // offset: 10; size: 4; Cell border lines and background area
2319
                // bit: 3-0; mask: 0x0000000F; left style
2320 37
                if ($bordersLeftStyle = Xls\Style\Border::lookup((0x0000000F & self::getInt4d($recordData, 10)) >> 0)) {
2321 37
                    $objStyle->getBorders()->getLeft()->setBorderStyle($bordersLeftStyle);
2322
                }
2323
                // bit: 7-4; mask: 0x000000F0; right style
2324 37
                if ($bordersRightStyle = Xls\Style\Border::lookup((0x000000F0 & self::getInt4d($recordData, 10)) >> 4)) {
2325 37
                    $objStyle->getBorders()->getRight()->setBorderStyle($bordersRightStyle);
2326
                }
2327
                // bit: 11-8; mask: 0x00000F00; top style
2328 37
                if ($bordersTopStyle = Xls\Style\Border::lookup((0x00000F00 & self::getInt4d($recordData, 10)) >> 8)) {
2329 37
                    $objStyle->getBorders()->getTop()->setBorderStyle($bordersTopStyle);
2330
                }
2331
                // bit: 15-12; mask: 0x0000F000; bottom style
2332 37
                if ($bordersBottomStyle = Xls\Style\Border::lookup((0x0000F000 & self::getInt4d($recordData, 10)) >> 12)) {
2333 37
                    $objStyle->getBorders()->getBottom()->setBorderStyle($bordersBottomStyle);
2334
                }
2335
                // bit: 22-16; mask: 0x007F0000; left color
2336 37
                $objStyle->getBorders()->getLeft()->colorIndex = (0x007F0000 & self::getInt4d($recordData, 10)) >> 16;
2337
2338
                // bit: 29-23; mask: 0x3F800000; right color
2339 37
                $objStyle->getBorders()->getRight()->colorIndex = (0x3F800000 & self::getInt4d($recordData, 10)) >> 23;
2340
2341
                // bit: 30; mask: 0x40000000; 1 = diagonal line from top left to right bottom
2342 37
                $diagonalDown = (0x40000000 & self::getInt4d($recordData, 10)) >> 30 ? true : false;
2343
2344
                // bit: 31; mask: 0x80000000; 1 = diagonal line from bottom left to top right
2345 37
                $diagonalUp = (0x80000000 & self::getInt4d($recordData, 10)) >> 31 ? true : false;
2346
2347 37
                if ($diagonalUp == false && $diagonalDown == false) {
2348 37
                    $objStyle->getBorders()->setDiagonalDirection(Borders::DIAGONAL_NONE);
2349
                } elseif ($diagonalUp == true && $diagonalDown == false) {
2350
                    $objStyle->getBorders()->setDiagonalDirection(Borders::DIAGONAL_UP);
2351
                } elseif ($diagonalUp == false && $diagonalDown == true) {
2352
                    $objStyle->getBorders()->setDiagonalDirection(Borders::DIAGONAL_DOWN);
2353
                } elseif ($diagonalUp == true && $diagonalDown == true) {
2354
                    $objStyle->getBorders()->setDiagonalDirection(Borders::DIAGONAL_BOTH);
2355
                }
2356
2357
                // offset: 14; size: 4;
2358
                // bit: 6-0; mask: 0x0000007F; top color
2359 37
                $objStyle->getBorders()->getTop()->colorIndex = (0x0000007F & self::getInt4d($recordData, 14)) >> 0;
2360
2361
                // bit: 13-7; mask: 0x00003F80; bottom color
2362 37
                $objStyle->getBorders()->getBottom()->colorIndex = (0x00003F80 & self::getInt4d($recordData, 14)) >> 7;
2363
2364
                // bit: 20-14; mask: 0x001FC000; diagonal color
2365 37
                $objStyle->getBorders()->getDiagonal()->colorIndex = (0x001FC000 & self::getInt4d($recordData, 14)) >> 14;
2366
2367
                // bit: 24-21; mask: 0x01E00000; diagonal style
2368 37
                if ($bordersDiagonalStyle = Xls\Style\Border::lookup((0x01E00000 & self::getInt4d($recordData, 14)) >> 21)) {
2369 37
                    $objStyle->getBorders()->getDiagonal()->setBorderStyle($bordersDiagonalStyle);
2370
                }
2371
2372
                // bit: 31-26; mask: 0xFC000000 fill pattern
2373 37
                if ($fillType = Xls\Style\FillPattern::lookup((0xFC000000 & self::getInt4d($recordData, 14)) >> 26)) {
2374 37
                    $objStyle->getFill()->setFillType($fillType);
2375
                }
2376
                // offset: 18; size: 2; pattern and background colour
2377
                // bit: 6-0; mask: 0x007F; color index for pattern color
2378 37
                $objStyle->getFill()->startcolorIndex = (0x007F & self::getUInt2d($recordData, 18)) >> 0;
2379
2380
                // bit: 13-7; mask: 0x3F80; color index for pattern background
2381 37
                $objStyle->getFill()->endcolorIndex = (0x3F80 & self::getUInt2d($recordData, 18)) >> 7;
2382
            } else {
2383
                // BIFF5
2384
2385
                // offset: 7; size: 1; Text orientation and flags
2386 1
                $orientationAndFlags = ord($recordData[7]);
2387
2388
                // bit: 1-0; mask: 0x03; XF_ORIENTATION: Text orientation
2389 1
                $xfOrientation = (0x03 & $orientationAndFlags) >> 0;
2390
                switch ($xfOrientation) {
2391 1
                    case 0:
2392 1
                        $objStyle->getAlignment()->setTextRotation(0);
2393
2394 1
                        break;
2395 1
                    case 1:
2396 1
                        $objStyle->getAlignment()->setTextRotation(Alignment::TEXTROTATION_STACK_PHPSPREADSHEET);
2397
2398 1
                        break;
2399
                    case 2:
2400
                        $objStyle->getAlignment()->setTextRotation(90);
2401
2402
                        break;
2403
                    case 3:
2404
                        $objStyle->getAlignment()->setTextRotation(-90);
2405
2406
                        break;
2407
                }
2408
2409
                // offset: 8; size: 4; cell border lines and background area
2410 1
                $borderAndBackground = self::getInt4d($recordData, 8);
2411
2412
                // bit: 6-0; mask: 0x0000007F; color index for pattern color
2413 1
                $objStyle->getFill()->startcolorIndex = (0x0000007F & $borderAndBackground) >> 0;
2414
2415
                // bit: 13-7; mask: 0x00003F80; color index for pattern background
2416 1
                $objStyle->getFill()->endcolorIndex = (0x00003F80 & $borderAndBackground) >> 7;
2417
2418
                // bit: 21-16; mask: 0x003F0000; fill pattern
2419 1
                $objStyle->getFill()->setFillType(Xls\Style\FillPattern::lookup((0x003F0000 & $borderAndBackground) >> 16));
2420
2421
                // bit: 24-22; mask: 0x01C00000; bottom line style
2422 1
                $objStyle->getBorders()->getBottom()->setBorderStyle(Xls\Style\Border::lookup((0x01C00000 & $borderAndBackground) >> 22));
2423
2424
                // bit: 31-25; mask: 0xFE000000; bottom line color
2425 1
                $objStyle->getBorders()->getBottom()->colorIndex = (0xFE000000 & $borderAndBackground) >> 25;
2426
2427
                // offset: 12; size: 4; cell border lines
2428 1
                $borderLines = self::getInt4d($recordData, 12);
2429
2430
                // bit: 2-0; mask: 0x00000007; top line style
2431 1
                $objStyle->getBorders()->getTop()->setBorderStyle(Xls\Style\Border::lookup((0x00000007 & $borderLines) >> 0));
2432
2433
                // bit: 5-3; mask: 0x00000038; left line style
2434 1
                $objStyle->getBorders()->getLeft()->setBorderStyle(Xls\Style\Border::lookup((0x00000038 & $borderLines) >> 3));
2435
2436
                // bit: 8-6; mask: 0x000001C0; right line style
2437 1
                $objStyle->getBorders()->getRight()->setBorderStyle(Xls\Style\Border::lookup((0x000001C0 & $borderLines) >> 6));
2438
2439
                // bit: 15-9; mask: 0x0000FE00; top line color index
2440 1
                $objStyle->getBorders()->getTop()->colorIndex = (0x0000FE00 & $borderLines) >> 9;
2441
2442
                // bit: 22-16; mask: 0x007F0000; left line color index
2443 1
                $objStyle->getBorders()->getLeft()->colorIndex = (0x007F0000 & $borderLines) >> 16;
2444
2445
                // bit: 29-23; mask: 0x3F800000; right line color index
2446 1
                $objStyle->getBorders()->getRight()->colorIndex = (0x3F800000 & $borderLines) >> 23;
2447
            }
2448
2449
            // add cellStyleXf or cellXf and update mapping
2450 38
            if ($isCellStyleXf) {
2451
                // we only read one style XF record which is always the first
2452 38
                if ($this->xfIndex == 0) {
2453 38
                    $this->spreadsheet->addCellStyleXf($objStyle);
2454 38
                    $this->mapCellStyleXfIndex[$this->xfIndex] = 0;
2455
                }
2456
            } else {
2457
                // we read all cell XF records
2458 38
                $this->spreadsheet->addCellXf($objStyle);
2459 38
                $this->mapCellXfIndex[$this->xfIndex] = count($this->spreadsheet->getCellXfCollection()) - 1;
2460
            }
2461
2462
            // update XF index for when we read next record
2463 38
            ++$this->xfIndex;
2464
        }
2465 39
    }
2466
2467 6
    private function readXfExt(): void
2468
    {
2469 6
        $length = self::getUInt2d($this->data, $this->pos + 2);
2470 6
        $recordData = $this->readRecordData($this->data, $this->pos + 4, $length);
2471
2472
        // move stream pointer to next record
2473 6
        $this->pos += 4 + $length;
2474
2475 6
        if (!$this->readDataOnly) {
2476
            // offset: 0; size: 2; 0x087D = repeated header
2477
2478
            // offset: 2; size: 2
2479
2480
            // offset: 4; size: 8; not used
2481
2482
            // offset: 12; size: 2; record version
2483
2484
            // offset: 14; size: 2; index to XF record which this record modifies
2485 6
            $ixfe = self::getUInt2d($recordData, 14);
2486
2487
            // offset: 16; size: 2; not used
2488
2489
            // offset: 18; size: 2; number of extension properties that follow
2490 6
            $cexts = self::getUInt2d($recordData, 18);
2491
2492
            // start reading the actual extension data
2493 6
            $offset = 20;
2494 6
            while ($offset < $length) {
2495
                // extension type
2496 6
                $extType = self::getUInt2d($recordData, $offset);
2497
2498
                // extension length
2499 6
                $cb = self::getUInt2d($recordData, $offset + 2);
2500
2501
                // extension data
2502 6
                $extData = substr($recordData, $offset + 4, $cb);
2503
2504
                switch ($extType) {
2505 6
                    case 4:        // fill start color
2506 6
                        $xclfType = self::getUInt2d($extData, 0); // color type
2507 6
                        $xclrValue = substr($extData, 4, 4); // color value (value based on color type)
2508
2509 6
                        if ($xclfType == 2) {
2510 6
                            $rgb = sprintf('%02X%02X%02X', ord($xclrValue[0]), ord($xclrValue[1]), ord($xclrValue[2]));
2511
2512
                            // modify the relevant style property
2513 6
                            if (isset($this->mapCellXfIndex[$ixfe])) {
2514 2
                                $fill = $this->spreadsheet->getCellXfByIndex($this->mapCellXfIndex[$ixfe])->getFill();
2515 2
                                $fill->getStartColor()->setRGB($rgb);
2516 2
                                $fill->startcolorIndex = null; // normal color index does not apply, discard
2517
                            }
2518
                        }
2519
2520 6
                        break;
2521 6
                    case 5:        // fill end color
2522 2
                        $xclfType = self::getUInt2d($extData, 0); // color type
2523 2
                        $xclrValue = substr($extData, 4, 4); // color value (value based on color type)
2524
2525 2
                        if ($xclfType == 2) {
2526 2
                            $rgb = sprintf('%02X%02X%02X', ord($xclrValue[0]), ord($xclrValue[1]), ord($xclrValue[2]));
2527
2528
                            // modify the relevant style property
2529 2
                            if (isset($this->mapCellXfIndex[$ixfe])) {
2530 2
                                $fill = $this->spreadsheet->getCellXfByIndex($this->mapCellXfIndex[$ixfe])->getFill();
2531 2
                                $fill->getEndColor()->setRGB($rgb);
2532 2
                                $fill->endcolorIndex = null; // normal color index does not apply, discard
2533
                            }
2534
                        }
2535
2536 2
                        break;
2537 6
                    case 7:        // border color top
2538 6
                        $xclfType = self::getUInt2d($extData, 0); // color type
2539 6
                        $xclrValue = substr($extData, 4, 4); // color value (value based on color type)
2540
2541 6
                        if ($xclfType == 2) {
2542 6
                            $rgb = sprintf('%02X%02X%02X', ord($xclrValue[0]), ord($xclrValue[1]), ord($xclrValue[2]));
2543
2544
                            // modify the relevant style property
2545 6
                            if (isset($this->mapCellXfIndex[$ixfe])) {
2546 2
                                $top = $this->spreadsheet->getCellXfByIndex($this->mapCellXfIndex[$ixfe])->getBorders()->getTop();
2547 2
                                $top->getColor()->setRGB($rgb);
2548 2
                                $top->colorIndex = null; // normal color index does not apply, discard
2549
                            }
2550
                        }
2551
2552 6
                        break;
2553 6
                    case 8:        // border color bottom
2554 6
                        $xclfType = self::getUInt2d($extData, 0); // color type
2555 6
                        $xclrValue = substr($extData, 4, 4); // color value (value based on color type)
2556
2557 6
                        if ($xclfType == 2) {
2558 6
                            $rgb = sprintf('%02X%02X%02X', ord($xclrValue[0]), ord($xclrValue[1]), ord($xclrValue[2]));
2559
2560
                            // modify the relevant style property
2561 6
                            if (isset($this->mapCellXfIndex[$ixfe])) {
2562 2
                                $bottom = $this->spreadsheet->getCellXfByIndex($this->mapCellXfIndex[$ixfe])->getBorders()->getBottom();
2563 2
                                $bottom->getColor()->setRGB($rgb);
2564 2
                                $bottom->colorIndex = null; // normal color index does not apply, discard
2565
                            }
2566
                        }
2567
2568 6
                        break;
2569 6
                    case 9:        // border color left
2570 6
                        $xclfType = self::getUInt2d($extData, 0); // color type
2571 6
                        $xclrValue = substr($extData, 4, 4); // color value (value based on color type)
2572
2573 6
                        if ($xclfType == 2) {
2574 6
                            $rgb = sprintf('%02X%02X%02X', ord($xclrValue[0]), ord($xclrValue[1]), ord($xclrValue[2]));
2575
2576
                            // modify the relevant style property
2577 6
                            if (isset($this->mapCellXfIndex[$ixfe])) {
2578 2
                                $left = $this->spreadsheet->getCellXfByIndex($this->mapCellXfIndex[$ixfe])->getBorders()->getLeft();
2579 2
                                $left->getColor()->setRGB($rgb);
2580 2
                                $left->colorIndex = null; // normal color index does not apply, discard
2581
                            }
2582
                        }
2583
2584 6
                        break;
2585 6
                    case 10:        // border color right
2586 6
                        $xclfType = self::getUInt2d($extData, 0); // color type
2587 6
                        $xclrValue = substr($extData, 4, 4); // color value (value based on color type)
2588
2589 6
                        if ($xclfType == 2) {
2590 6
                            $rgb = sprintf('%02X%02X%02X', ord($xclrValue[0]), ord($xclrValue[1]), ord($xclrValue[2]));
2591
2592
                            // modify the relevant style property
2593 6
                            if (isset($this->mapCellXfIndex[$ixfe])) {
2594 2
                                $right = $this->spreadsheet->getCellXfByIndex($this->mapCellXfIndex[$ixfe])->getBorders()->getRight();
2595 2
                                $right->getColor()->setRGB($rgb);
2596 2
                                $right->colorIndex = null; // normal color index does not apply, discard
2597
                            }
2598
                        }
2599
2600 6
                        break;
2601 6
                    case 11:        // border color diagonal
2602
                        $xclfType = self::getUInt2d($extData, 0); // color type
2603
                        $xclrValue = substr($extData, 4, 4); // color value (value based on color type)
2604
2605
                        if ($xclfType == 2) {
2606
                            $rgb = sprintf('%02X%02X%02X', ord($xclrValue[0]), ord($xclrValue[1]), ord($xclrValue[2]));
2607
2608
                            // modify the relevant style property
2609
                            if (isset($this->mapCellXfIndex[$ixfe])) {
2610
                                $diagonal = $this->spreadsheet->getCellXfByIndex($this->mapCellXfIndex[$ixfe])->getBorders()->getDiagonal();
2611
                                $diagonal->getColor()->setRGB($rgb);
2612
                                $diagonal->colorIndex = null; // normal color index does not apply, discard
2613
                            }
2614
                        }
2615
2616
                        break;
2617 6
                    case 13:    // font color
2618 6
                        $xclfType = self::getUInt2d($extData, 0); // color type
2619 6
                        $xclrValue = substr($extData, 4, 4); // color value (value based on color type)
2620
2621 6
                        if ($xclfType == 2) {
2622 6
                            $rgb = sprintf('%02X%02X%02X', ord($xclrValue[0]), ord($xclrValue[1]), ord($xclrValue[2]));
2623
2624
                            // modify the relevant style property
2625 6
                            if (isset($this->mapCellXfIndex[$ixfe])) {
2626 2
                                $font = $this->spreadsheet->getCellXfByIndex($this->mapCellXfIndex[$ixfe])->getFont();
2627 2
                                $font->getColor()->setRGB($rgb);
2628 2
                                $font->colorIndex = null; // normal color index does not apply, discard
2629
                            }
2630
                        }
2631
2632 6
                        break;
2633
                }
2634
2635 6
                $offset += $cb;
2636
            }
2637
        }
2638 6
    }
2639
2640
    /**
2641
     * Read STYLE record.
2642
     */
2643 39
    private function readStyle(): void
2644
    {
2645 39
        $length = self::getUInt2d($this->data, $this->pos + 2);
2646 39
        $recordData = $this->readRecordData($this->data, $this->pos + 4, $length);
2647
2648
        // move stream pointer to next record
2649 39
        $this->pos += 4 + $length;
2650
2651 39
        if (!$this->readDataOnly) {
2652
            // offset: 0; size: 2; index to XF record and flag for built-in style
2653 38
            $ixfe = self::getUInt2d($recordData, 0);
2654
2655
            // bit: 11-0; mask 0x0FFF; index to XF record
2656 38
            $xfIndex = (0x0FFF & $ixfe) >> 0;
2657
2658
            // bit: 15; mask 0x8000; 0 = user-defined style, 1 = built-in style
2659 38
            $isBuiltIn = (bool) ((0x8000 & $ixfe) >> 15);
2660
2661 38
            if ($isBuiltIn) {
2662
                // offset: 2; size: 1; identifier for built-in style
2663 38
                $builtInId = ord($recordData[2]);
2664
2665
                switch ($builtInId) {
2666 38
                    case 0x00:
2667
                        // currently, we are not using this for anything
2668 38
                        break;
2669
                    default:
2670 21
                        break;
2671
                }
2672
            }
2673
            // user-defined; not supported by PhpSpreadsheet
2674
        }
2675 39
    }
2676
2677
    /**
2678
     * Read PALETTE record.
2679
     */
2680 22
    private function readPalette(): void
2681
    {
2682 22
        $length = self::getUInt2d($this->data, $this->pos + 2);
2683 22
        $recordData = $this->readRecordData($this->data, $this->pos + 4, $length);
2684
2685
        // move stream pointer to next record
2686 22
        $this->pos += 4 + $length;
2687
2688 22
        if (!$this->readDataOnly) {
2689
            // offset: 0; size: 2; number of following colors
2690 22
            $nm = self::getUInt2d($recordData, 0);
2691
2692
            // list of RGB colors
2693 22
            for ($i = 0; $i < $nm; ++$i) {
2694 22
                $rgb = substr($recordData, 2 + 4 * $i, 4);
2695 22
                $this->palette[] = self::readRGB($rgb);
2696
            }
2697
        }
2698 22
    }
2699
2700
    /**
2701
     * SHEET.
2702
     *
2703
     * This record is  located in the  Workbook Globals
2704
     * Substream  and represents a sheet inside the workbook.
2705
     * One SHEET record is written for each sheet. It stores the
2706
     * sheet name and a stream offset to the BOF record of the
2707
     * respective Sheet Substream within the Workbook Stream.
2708
     *
2709
     * --    "OpenOffice.org's Documentation of the Microsoft
2710
     *         Excel File Format"
2711
     */
2712 42
    private function readSheet(): void
2713
    {
2714 42
        $length = self::getUInt2d($this->data, $this->pos + 2);
2715 42
        $recordData = $this->readRecordData($this->data, $this->pos + 4, $length);
2716
2717
        // offset: 0; size: 4; absolute stream position of the BOF record of the sheet
2718
        // NOTE: not encrypted
2719 42
        $rec_offset = self::getInt4d($this->data, $this->pos + 4);
2720
2721
        // move stream pointer to next record
2722 42
        $this->pos += 4 + $length;
2723
2724
        // offset: 4; size: 1; sheet state
2725 42
        switch (ord($recordData[4])) {
2726 42
            case 0x00:
2727 42
                $sheetState = Worksheet::SHEETSTATE_VISIBLE;
2728
2729 42
                break;
2730
            case 0x01:
2731
                $sheetState = Worksheet::SHEETSTATE_HIDDEN;
2732
2733
                break;
2734
            case 0x02:
2735
                $sheetState = Worksheet::SHEETSTATE_VERYHIDDEN;
2736
2737
                break;
2738
            default:
2739
                $sheetState = Worksheet::SHEETSTATE_VISIBLE;
2740
2741
                break;
2742
        }
2743
2744
        // offset: 5; size: 1; sheet type
2745 42
        $sheetType = ord($recordData[5]);
2746
2747
        // offset: 6; size: var; sheet name
2748 42
        $rec_name = null;
2749 42
        if ($this->version == self::XLS_BIFF8) {
2750 41
            $string = self::readUnicodeStringShort(substr($recordData, 6));
2751 41
            $rec_name = $string['value'];
2752 1
        } elseif ($this->version == self::XLS_BIFF7) {
2753 1
            $string = $this->readByteStringShort(substr($recordData, 6));
2754 1
            $rec_name = $string['value'];
2755
        }
2756
2757 42
        $this->sheets[] = [
2758 42
            'name' => $rec_name,
2759 42
            'offset' => $rec_offset,
2760 42
            'sheetState' => $sheetState,
2761 42
            'sheetType' => $sheetType,
2762
        ];
2763 42
    }
2764
2765
    /**
2766
     * Read EXTERNALBOOK record.
2767
     */
2768 24
    private function readExternalBook(): void
2769
    {
2770 24
        $length = self::getUInt2d($this->data, $this->pos + 2);
2771 24
        $recordData = $this->readRecordData($this->data, $this->pos + 4, $length);
2772
2773
        // move stream pointer to next record
2774 24
        $this->pos += 4 + $length;
2775
2776
        // offset within record data
2777 24
        $offset = 0;
2778
2779
        // there are 4 types of records
2780 24
        if (strlen($recordData) > 4) {
2781
            // external reference
2782
            // offset: 0; size: 2; number of sheet names ($nm)
2783
            $nm = self::getUInt2d($recordData, 0);
2784
            $offset += 2;
2785
2786
            // offset: 2; size: var; encoded URL without sheet name (Unicode string, 16-bit length)
2787
            $encodedUrlString = self::readUnicodeStringLong(substr($recordData, 2));
2788
            $offset += $encodedUrlString['size'];
2789
2790
            // offset: var; size: var; list of $nm sheet names (Unicode strings, 16-bit length)
2791
            $externalSheetNames = [];
2792
            for ($i = 0; $i < $nm; ++$i) {
2793
                $externalSheetNameString = self::readUnicodeStringLong(substr($recordData, $offset));
2794
                $externalSheetNames[] = $externalSheetNameString['value'];
2795
                $offset += $externalSheetNameString['size'];
2796
            }
2797
2798
            // store the record data
2799
            $this->externalBooks[] = [
2800
                'type' => 'external',
2801
                'encodedUrl' => $encodedUrlString['value'],
2802
                'externalSheetNames' => $externalSheetNames,
2803
            ];
2804 24
        } elseif (substr($recordData, 2, 2) == pack('CC', 0x01, 0x04)) {
2805
            // internal reference
2806
            // offset: 0; size: 2; number of sheet in this document
2807
            // offset: 2; size: 2; 0x01 0x04
2808 24
            $this->externalBooks[] = [
2809 24
                'type' => 'internal',
2810
            ];
2811
        } elseif (substr($recordData, 0, 4) == pack('vCC', 0x0001, 0x01, 0x3A)) {
2812
            // add-in function
2813
            // offset: 0; size: 2; 0x0001
2814
            $this->externalBooks[] = [
2815
                'type' => 'addInFunction',
2816
            ];
2817
        } elseif (substr($recordData, 0, 2) == pack('v', 0x0000)) {
2818
            // DDE links, OLE links
2819
            // offset: 0; size: 2; 0x0000
2820
            // offset: 2; size: var; encoded source document name
2821
            $this->externalBooks[] = [
2822
                'type' => 'DDEorOLE',
2823
            ];
2824
        }
2825 24
    }
2826
2827
    /**
2828
     * Read EXTERNNAME record.
2829
     */
2830
    private function readExternName(): void
2831
    {
2832
        $length = self::getUInt2d($this->data, $this->pos + 2);
2833
        $recordData = $this->readRecordData($this->data, $this->pos + 4, $length);
2834
2835
        // move stream pointer to next record
2836
        $this->pos += 4 + $length;
2837
2838
        // external sheet references provided for named cells
2839
        if ($this->version == self::XLS_BIFF8) {
2840
            // offset: 0; size: 2; options
2841
            $options = self::getUInt2d($recordData, 0);
2842
2843
            // offset: 2; size: 2;
2844
2845
            // offset: 4; size: 2; not used
2846
2847
            // offset: 6; size: var
2848
            $nameString = self::readUnicodeStringShort(substr($recordData, 6));
2849
2850
            // offset: var; size: var; formula data
2851
            $offset = 6 + $nameString['size'];
2852
            $formula = $this->getFormulaFromStructure(substr($recordData, $offset));
2853
2854
            $this->externalNames[] = [
2855
                'name' => $nameString['value'],
2856
                'formula' => $formula,
2857
            ];
2858
        }
2859
    }
2860
2861
    /**
2862
     * Read EXTERNSHEET record.
2863
     */
2864 25
    private function readExternSheet(): void
2865
    {
2866 25
        $length = self::getUInt2d($this->data, $this->pos + 2);
2867 25
        $recordData = $this->readRecordData($this->data, $this->pos + 4, $length);
2868
2869
        // move stream pointer to next record
2870 25
        $this->pos += 4 + $length;
2871
2872
        // external sheet references provided for named cells
2873 25
        if ($this->version == self::XLS_BIFF8) {
2874
            // offset: 0; size: 2; number of following ref structures
2875 24
            $nm = self::getUInt2d($recordData, 0);
2876 24
            for ($i = 0; $i < $nm; ++$i) {
2877 22
                $this->ref[] = [
2878
                    // offset: 2 + 6 * $i; index to EXTERNALBOOK record
2879 22
                    'externalBookIndex' => self::getUInt2d($recordData, 2 + 6 * $i),
2880
                    // offset: 4 + 6 * $i; index to first sheet in EXTERNALBOOK record
2881 22
                    'firstSheetIndex' => self::getUInt2d($recordData, 4 + 6 * $i),
2882
                    // offset: 6 + 6 * $i; index to last sheet in EXTERNALBOOK record
2883 22
                    'lastSheetIndex' => self::getUInt2d($recordData, 6 + 6 * $i),
2884
                ];
2885
            }
2886
        }
2887 25
    }
2888
2889
    /**
2890
     * DEFINEDNAME.
2891
     *
2892
     * This record is part of a Link Table. It contains the name
2893
     * and the token array of an internal defined name. Token
2894
     * arrays of defined names contain tokens with aberrant
2895
     * token classes.
2896
     *
2897
     * --    "OpenOffice.org's Documentation of the Microsoft
2898
     *         Excel File Format"
2899
     */
2900 7
    private function readDefinedName(): void
2901
    {
2902 7
        $length = self::getUInt2d($this->data, $this->pos + 2);
2903 7
        $recordData = $this->readRecordData($this->data, $this->pos + 4, $length);
2904
2905
        // move stream pointer to next record
2906 7
        $this->pos += 4 + $length;
2907
2908 7
        if ($this->version == self::XLS_BIFF8) {
2909
            // retrieves named cells
2910
2911
            // offset: 0; size: 2; option flags
2912 6
            $opts = self::getUInt2d($recordData, 0);
2913
2914
            // bit: 5; mask: 0x0020; 0 = user-defined name, 1 = built-in-name
2915 6
            $isBuiltInName = (0x0020 & $opts) >> 5;
2916
2917
            // offset: 2; size: 1; keyboard shortcut
2918
2919
            // offset: 3; size: 1; length of the name (character count)
2920 6
            $nlen = ord($recordData[3]);
2921
2922
            // offset: 4; size: 2; size of the formula data (it can happen that this is zero)
2923
            // note: there can also be additional data, this is not included in $flen
2924 6
            $flen = self::getUInt2d($recordData, 4);
2925
2926
            // offset: 8; size: 2; 0=Global name, otherwise index to sheet (1-based)
2927 6
            $scope = self::getUInt2d($recordData, 8);
2928
2929
            // offset: 14; size: var; Name (Unicode string without length field)
2930 6
            $string = self::readUnicodeString(substr($recordData, 14), $nlen);
2931
2932
            // offset: var; size: $flen; formula data
2933 6
            $offset = 14 + $string['size'];
2934 6
            $formulaStructure = pack('v', $flen) . substr($recordData, $offset);
2935
2936
            try {
2937 6
                $formula = $this->getFormulaFromStructure($formulaStructure);
2938
            } catch (PhpSpreadsheetException $e) {
2939
                $formula = '';
2940
            }
2941
2942 6
            $this->definedname[] = [
2943 6
                'isBuiltInName' => $isBuiltInName,
2944 6
                'name' => $string['value'],
2945 6
                'formula' => $formula,
2946 6
                'scope' => $scope,
2947
            ];
2948
        }
2949 7
    }
2950
2951
    /**
2952
     * Read MSODRAWINGGROUP record.
2953
     */
2954 6
    private function readMsoDrawingGroup(): void
2955
    {
2956 6
        $length = self::getUInt2d($this->data, $this->pos + 2);
2957
2958
        // get spliced record data
2959 6
        $splicedRecordData = $this->getSplicedRecordData();
2960 6
        $recordData = $splicedRecordData['recordData'];
2961
2962 6
        $this->drawingGroupData .= $recordData;
2963 6
    }
2964
2965
    /**
2966
     * SST - Shared String Table.
2967
     *
2968
     * This record contains a list of all strings used anywhere
2969
     * in the workbook. Each string occurs only once. The
2970
     * workbook uses indexes into the list to reference the
2971
     * strings.
2972
     *
2973
     * --    "OpenOffice.org's Documentation of the Microsoft
2974
     *         Excel File Format"
2975
     */
2976 37
    private function readSst(): void
2977
    {
2978
        // offset within (spliced) record data
2979 37
        $pos = 0;
2980
2981
        // Limit global SST position, further control for bad SST Length in BIFF8 data
2982 37
        $limitposSST = 0;
2983
2984
        // get spliced record data
2985 37
        $splicedRecordData = $this->getSplicedRecordData();
2986
2987 37
        $recordData = $splicedRecordData['recordData'];
2988 37
        $spliceOffsets = $splicedRecordData['spliceOffsets'];
2989
2990
        // offset: 0; size: 4; total number of strings in the workbook
2991 37
        $pos += 4;
2992
2993
        // offset: 4; size: 4; number of following strings ($nm)
2994 37
        $nm = self::getInt4d($recordData, 4);
2995 37
        $pos += 4;
2996
2997
        // look up limit position
2998 37
        foreach ($spliceOffsets as $spliceOffset) {
2999
            // it can happen that the string is empty, therefore we need
3000
            // <= and not just <
3001 37
            if ($pos <= $spliceOffset) {
3002 37
                $limitposSST = $spliceOffset;
3003
            }
3004
        }
3005
3006
        // loop through the Unicode strings (16-bit length)
3007 37
        for ($i = 0; $i < $nm && $pos < $limitposSST; ++$i) {
3008
            // number of characters in the Unicode string
3009 26
            $numChars = self::getUInt2d($recordData, $pos);
3010 26
            $pos += 2;
3011
3012
            // option flags
3013 26
            $optionFlags = ord($recordData[$pos]);
3014 26
            ++$pos;
3015
3016
            // bit: 0; mask: 0x01; 0 = compressed; 1 = uncompressed
3017 26
            $isCompressed = (($optionFlags & 0x01) == 0);
3018
3019
            // bit: 2; mask: 0x02; 0 = ordinary; 1 = Asian phonetic
3020 26
            $hasAsian = (($optionFlags & 0x04) != 0);
3021
3022
            // bit: 3; mask: 0x03; 0 = ordinary; 1 = Rich-Text
3023 26
            $hasRichText = (($optionFlags & 0x08) != 0);
3024
3025 26
            $formattingRuns = 0;
3026 26
            if ($hasRichText) {
3027
                // number of Rich-Text formatting runs
3028 3
                $formattingRuns = self::getUInt2d($recordData, $pos);
3029 3
                $pos += 2;
3030
            }
3031
3032 26
            $extendedRunLength = 0;
3033 26
            if ($hasAsian) {
3034
                // size of Asian phonetic setting
3035
                $extendedRunLength = self::getInt4d($recordData, $pos);
3036
                $pos += 4;
3037
            }
3038
3039
            // expected byte length of character array if not split
3040 26
            $len = ($isCompressed) ? $numChars : $numChars * 2;
3041
3042
            // look up limit position - Check it again to be sure that no error occurs when parsing SST structure
3043 26
            $limitpos = null;
3044 26
            foreach ($spliceOffsets as $spliceOffset) {
3045
                // it can happen that the string is empty, therefore we need
3046
                // <= and not just <
3047 26
                if ($pos <= $spliceOffset) {
3048 26
                    $limitpos = $spliceOffset;
3049
3050 26
                    break;
3051
                }
3052
            }
3053
3054 26
            if ($pos + $len <= $limitpos) {
3055
                // character array is not split between records
3056
3057 26
                $retstr = substr($recordData, $pos, $len);
3058 26
                $pos += $len;
3059
            } else {
3060
                // character array is split between records
3061
3062
                // first part of character array
3063
                $retstr = substr($recordData, $pos, $limitpos - $pos);
3064
3065
                $bytesRead = $limitpos - $pos;
3066
3067
                // remaining characters in Unicode string
3068
                $charsLeft = $numChars - (($isCompressed) ? $bytesRead : ($bytesRead / 2));
3069
3070
                $pos = $limitpos;
3071
3072
                // keep reading the characters
3073
                while ($charsLeft > 0) {
3074
                    // look up next limit position, in case the string span more than one continue record
3075
                    foreach ($spliceOffsets as $spliceOffset) {
3076
                        if ($pos < $spliceOffset) {
3077
                            $limitpos = $spliceOffset;
3078
3079
                            break;
3080
                        }
3081
                    }
3082
3083
                    // repeated option flags
3084
                    // OpenOffice.org documentation 5.21
3085
                    $option = ord($recordData[$pos]);
3086
                    ++$pos;
3087
3088
                    if ($isCompressed && ($option == 0)) {
3089
                        // 1st fragment compressed
3090
                        // this fragment compressed
3091
                        $len = min($charsLeft, $limitpos - $pos);
3092
                        $retstr .= substr($recordData, $pos, $len);
3093
                        $charsLeft -= $len;
3094
                        $isCompressed = true;
3095
                    } elseif (!$isCompressed && ($option != 0)) {
3096
                        // 1st fragment uncompressed
3097
                        // this fragment uncompressed
3098
                        $len = min($charsLeft * 2, $limitpos - $pos);
3099
                        $retstr .= substr($recordData, $pos, $len);
3100
                        $charsLeft -= $len / 2;
3101
                        $isCompressed = false;
3102
                    } elseif (!$isCompressed && ($option == 0)) {
3103
                        // 1st fragment uncompressed
3104
                        // this fragment compressed
3105
                        $len = min($charsLeft, $limitpos - $pos);
3106
                        for ($j = 0; $j < $len; ++$j) {
3107
                            $retstr .= $recordData[$pos + $j]
3108
                            . chr(0);
3109
                        }
3110
                        $charsLeft -= $len;
3111
                        $isCompressed = false;
3112
                    } else {
3113
                        // 1st fragment compressed
3114
                        // this fragment uncompressed
3115
                        $newstr = '';
3116
                        $jMax = strlen($retstr);
3117
                        for ($j = 0; $j < $jMax; ++$j) {
3118
                            $newstr .= $retstr[$j] . chr(0);
3119
                        }
3120
                        $retstr = $newstr;
3121
                        $len = min($charsLeft * 2, $limitpos - $pos);
3122
                        $retstr .= substr($recordData, $pos, $len);
3123
                        $charsLeft -= $len / 2;
3124
                        $isCompressed = false;
3125
                    }
3126
3127
                    $pos += $len;
3128
                }
3129
            }
3130
3131
            // convert to UTF-8
3132 26
            $retstr = self::encodeUTF16($retstr, $isCompressed);
3133
3134
            // read additional Rich-Text information, if any
3135 26
            $fmtRuns = [];
3136 26
            if ($hasRichText) {
3137
                // list of formatting runs
3138 3
                for ($j = 0; $j < $formattingRuns; ++$j) {
3139
                    // first formatted character; zero-based
3140 3
                    $charPos = self::getUInt2d($recordData, $pos + $j * 4);
3141
3142
                    // index to font record
3143 3
                    $fontIndex = self::getUInt2d($recordData, $pos + 2 + $j * 4);
3144
3145 3
                    $fmtRuns[] = [
3146 3
                        'charPos' => $charPos,
3147 3
                        'fontIndex' => $fontIndex,
3148
                    ];
3149
                }
3150 3
                $pos += 4 * $formattingRuns;
3151
            }
3152
3153
            // read additional Asian phonetics information, if any
3154 26
            if ($hasAsian) {
3155
                // For Asian phonetic settings, we skip the extended string data
3156
                $pos += $extendedRunLength;
3157
            }
3158
3159
            // store the shared sting
3160 26
            $this->sst[] = [
3161 26
                'value' => $retstr,
3162 26
                'fmtRuns' => $fmtRuns,
3163
            ];
3164
        }
3165
3166
        // getSplicedRecordData() takes care of moving current position in data stream
3167 37
    }
3168
3169
    /**
3170
     * Read PRINTGRIDLINES record.
3171
     */
3172 36
    private function readPrintGridlines(): void
3173
    {
3174 36
        $length = self::getUInt2d($this->data, $this->pos + 2);
3175 36
        $recordData = $this->readRecordData($this->data, $this->pos + 4, $length);
3176
3177
        // move stream pointer to next record
3178 36
        $this->pos += 4 + $length;
3179
3180 36
        if ($this->version == self::XLS_BIFF8 && !$this->readDataOnly) {
3181
            // offset: 0; size: 2; 0 = do not print sheet grid lines; 1 = print sheet gridlines
3182 34
            $printGridlines = (bool) self::getUInt2d($recordData, 0);
3183 34
            $this->phpSheet->setPrintGridlines($printGridlines);
3184
        }
3185 36
    }
3186
3187
    /**
3188
     * Read DEFAULTROWHEIGHT record.
3189
     */
3190 23
    private function readDefaultRowHeight(): void
3191
    {
3192 23
        $length = self::getUInt2d($this->data, $this->pos + 2);
3193 23
        $recordData = $this->readRecordData($this->data, $this->pos + 4, $length);
3194
3195
        // move stream pointer to next record
3196 23
        $this->pos += 4 + $length;
3197
3198
        // offset: 0; size: 2; option flags
3199
        // offset: 2; size: 2; default height for unused rows, (twips 1/20 point)
3200 23
        $height = self::getUInt2d($recordData, 2);
3201 23
        $this->phpSheet->getDefaultRowDimension()->setRowHeight($height / 20);
3202 23
    }
3203
3204
    /**
3205
     * Read SHEETPR record.
3206
     */
3207 38
    private function readSheetPr(): void
3208
    {
3209 38
        $length = self::getUInt2d($this->data, $this->pos + 2);
3210 38
        $recordData = $this->readRecordData($this->data, $this->pos + 4, $length);
3211
3212
        // move stream pointer to next record
3213 38
        $this->pos += 4 + $length;
3214
3215
        // offset: 0; size: 2
3216
3217
        // bit: 6; mask: 0x0040; 0 = outline buttons above outline group
3218 38
        $isSummaryBelow = (0x0040 & self::getUInt2d($recordData, 0)) >> 6;
3219 38
        $this->phpSheet->setShowSummaryBelow($isSummaryBelow);
3220
3221
        // bit: 7; mask: 0x0080; 0 = outline buttons left of outline group
3222 38
        $isSummaryRight = (0x0080 & self::getUInt2d($recordData, 0)) >> 7;
3223 38
        $this->phpSheet->setShowSummaryRight($isSummaryRight);
3224
3225
        // bit: 8; mask: 0x100; 0 = scale printout in percent, 1 = fit printout to number of pages
3226
        // this corresponds to radio button setting in page setup dialog in Excel
3227 38
        $this->isFitToPages = (bool) ((0x0100 & self::getUInt2d($recordData, 0)) >> 8);
3228 38
    }
3229
3230
    /**
3231
     * Read HORIZONTALPAGEBREAKS record.
3232
     */
3233 2
    private function readHorizontalPageBreaks(): void
3234
    {
3235 2
        $length = self::getUInt2d($this->data, $this->pos + 2);
3236 2
        $recordData = $this->readRecordData($this->data, $this->pos + 4, $length);
3237
3238
        // move stream pointer to next record
3239 2
        $this->pos += 4 + $length;
3240
3241 2
        if ($this->version == self::XLS_BIFF8 && !$this->readDataOnly) {
3242
            // offset: 0; size: 2; number of the following row index structures
3243 2
            $nm = self::getUInt2d($recordData, 0);
3244
3245
            // offset: 2; size: 6 * $nm; list of $nm row index structures
3246 2
            for ($i = 0; $i < $nm; ++$i) {
3247
                $r = self::getUInt2d($recordData, 2 + 6 * $i);
3248
                $cf = self::getUInt2d($recordData, 2 + 6 * $i + 2);
3249
                $cl = self::getUInt2d($recordData, 2 + 6 * $i + 4);
3250
3251
                // not sure why two column indexes are necessary?
3252
                $this->phpSheet->setBreakByColumnAndRow($cf + 1, $r, Worksheet::BREAK_ROW);
3253
            }
3254
        }
3255 2
    }
3256
3257
    /**
3258
     * Read VERTICALPAGEBREAKS record.
3259
     */
3260 2
    private function readVerticalPageBreaks(): void
3261
    {
3262 2
        $length = self::getUInt2d($this->data, $this->pos + 2);
3263 2
        $recordData = $this->readRecordData($this->data, $this->pos + 4, $length);
3264
3265
        // move stream pointer to next record
3266 2
        $this->pos += 4 + $length;
3267
3268 2
        if ($this->version == self::XLS_BIFF8 && !$this->readDataOnly) {
3269
            // offset: 0; size: 2; number of the following column index structures
3270 2
            $nm = self::getUInt2d($recordData, 0);
3271
3272
            // offset: 2; size: 6 * $nm; list of $nm row index structures
3273 2
            for ($i = 0; $i < $nm; ++$i) {
3274
                $c = self::getUInt2d($recordData, 2 + 6 * $i);
3275
                $rf = self::getUInt2d($recordData, 2 + 6 * $i + 2);
3276
                $rl = self::getUInt2d($recordData, 2 + 6 * $i + 4);
3277
3278
                // not sure why two row indexes are necessary?
3279
                $this->phpSheet->setBreakByColumnAndRow($c + 1, $rf, Worksheet::BREAK_COLUMN);
3280
            }
3281
        }
3282 2
    }
3283
3284
    /**
3285
     * Read HEADER record.
3286
     */
3287 36
    private function readHeader(): void
3288
    {
3289 36
        $length = self::getUInt2d($this->data, $this->pos + 2);
3290 36
        $recordData = $this->readRecordData($this->data, $this->pos + 4, $length);
3291
3292
        // move stream pointer to next record
3293 36
        $this->pos += 4 + $length;
3294
3295 36
        if (!$this->readDataOnly) {
3296
            // offset: 0; size: var
3297
            // realized that $recordData can be empty even when record exists
3298 35
            if ($recordData) {
3299 19
                if ($this->version == self::XLS_BIFF8) {
3300 18
                    $string = self::readUnicodeStringLong($recordData);
3301
                } else {
3302 1
                    $string = $this->readByteStringShort($recordData);
3303
                }
3304
3305 19
                $this->phpSheet->getHeaderFooter()->setOddHeader($string['value']);
3306 19
                $this->phpSheet->getHeaderFooter()->setEvenHeader($string['value']);
3307
            }
3308
        }
3309 36
    }
3310
3311
    /**
3312
     * Read FOOTER record.
3313
     */
3314 36
    private function readFooter(): void
3315
    {
3316 36
        $length = self::getUInt2d($this->data, $this->pos + 2);
3317 36
        $recordData = $this->readRecordData($this->data, $this->pos + 4, $length);
3318
3319
        // move stream pointer to next record
3320 36
        $this->pos += 4 + $length;
3321
3322 36
        if (!$this->readDataOnly) {
3323
            // offset: 0; size: var
3324
            // realized that $recordData can be empty even when record exists
3325 35
            if ($recordData) {
3326 19
                if ($this->version == self::XLS_BIFF8) {
3327 18
                    $string = self::readUnicodeStringLong($recordData);
3328
                } else {
3329 1
                    $string = $this->readByteStringShort($recordData);
3330
                }
3331 19
                $this->phpSheet->getHeaderFooter()->setOddFooter($string['value']);
3332 19
                $this->phpSheet->getHeaderFooter()->setEvenFooter($string['value']);
3333
            }
3334
        }
3335 36
    }
3336
3337
    /**
3338
     * Read HCENTER record.
3339
     */
3340 36
    private function readHcenter(): void
3341
    {
3342 36
        $length = self::getUInt2d($this->data, $this->pos + 2);
3343 36
        $recordData = $this->readRecordData($this->data, $this->pos + 4, $length);
3344
3345
        // move stream pointer to next record
3346 36
        $this->pos += 4 + $length;
3347
3348 36
        if (!$this->readDataOnly) {
3349
            // offset: 0; size: 2; 0 = print sheet left aligned, 1 = print sheet centered horizontally
3350 35
            $isHorizontalCentered = (bool) self::getUInt2d($recordData, 0);
3351
3352 35
            $this->phpSheet->getPageSetup()->setHorizontalCentered($isHorizontalCentered);
3353
        }
3354 36
    }
3355
3356
    /**
3357
     * Read VCENTER record.
3358
     */
3359 36
    private function readVcenter(): void
3360
    {
3361 36
        $length = self::getUInt2d($this->data, $this->pos + 2);
3362 36
        $recordData = $this->readRecordData($this->data, $this->pos + 4, $length);
3363
3364
        // move stream pointer to next record
3365 36
        $this->pos += 4 + $length;
3366
3367 36
        if (!$this->readDataOnly) {
3368
            // offset: 0; size: 2; 0 = print sheet aligned at top page border, 1 = print sheet vertically centered
3369 35
            $isVerticalCentered = (bool) self::getUInt2d($recordData, 0);
3370
3371 35
            $this->phpSheet->getPageSetup()->setVerticalCentered($isVerticalCentered);
3372
        }
3373 36
    }
3374
3375
    /**
3376
     * Read LEFTMARGIN record.
3377
     */
3378 23
    private function readLeftMargin(): void
3379
    {
3380 23
        $length = self::getUInt2d($this->data, $this->pos + 2);
3381 23
        $recordData = $this->readRecordData($this->data, $this->pos + 4, $length);
3382
3383
        // move stream pointer to next record
3384 23
        $this->pos += 4 + $length;
3385
3386 23
        if (!$this->readDataOnly) {
3387
            // offset: 0; size: 8
3388 23
            $this->phpSheet->getPageMargins()->setLeft(self::extractNumber($recordData));
3389
        }
3390 23
    }
3391
3392
    /**
3393
     * Read RIGHTMARGIN record.
3394
     */
3395 23
    private function readRightMargin(): void
3396
    {
3397 23
        $length = self::getUInt2d($this->data, $this->pos + 2);
3398 23
        $recordData = $this->readRecordData($this->data, $this->pos + 4, $length);
3399
3400
        // move stream pointer to next record
3401 23
        $this->pos += 4 + $length;
3402
3403 23
        if (!$this->readDataOnly) {
3404
            // offset: 0; size: 8
3405 23
            $this->phpSheet->getPageMargins()->setRight(self::extractNumber($recordData));
3406
        }
3407 23
    }
3408
3409
    /**
3410
     * Read TOPMARGIN record.
3411
     */
3412 23
    private function readTopMargin(): void
3413
    {
3414 23
        $length = self::getUInt2d($this->data, $this->pos + 2);
3415 23
        $recordData = $this->readRecordData($this->data, $this->pos + 4, $length);
3416
3417
        // move stream pointer to next record
3418 23
        $this->pos += 4 + $length;
3419
3420 23
        if (!$this->readDataOnly) {
3421
            // offset: 0; size: 8
3422 23
            $this->phpSheet->getPageMargins()->setTop(self::extractNumber($recordData));
3423
        }
3424 23
    }
3425
3426
    /**
3427
     * Read BOTTOMMARGIN record.
3428
     */
3429 23
    private function readBottomMargin(): void
3430
    {
3431 23
        $length = self::getUInt2d($this->data, $this->pos + 2);
3432 23
        $recordData = $this->readRecordData($this->data, $this->pos + 4, $length);
3433
3434
        // move stream pointer to next record
3435 23
        $this->pos += 4 + $length;
3436
3437 23
        if (!$this->readDataOnly) {
3438
            // offset: 0; size: 8
3439 23
            $this->phpSheet->getPageMargins()->setBottom(self::extractNumber($recordData));
3440
        }
3441 23
    }
3442
3443
    /**
3444
     * Read PAGESETUP record.
3445
     */
3446 38
    private function readPageSetup(): void
3447
    {
3448 38
        $length = self::getUInt2d($this->data, $this->pos + 2);
3449 38
        $recordData = $this->readRecordData($this->data, $this->pos + 4, $length);
3450
3451
        // move stream pointer to next record
3452 38
        $this->pos += 4 + $length;
3453
3454 38
        if (!$this->readDataOnly) {
3455
            // offset: 0; size: 2; paper size
3456 37
            $paperSize = self::getUInt2d($recordData, 0);
3457
3458
            // offset: 2; size: 2; scaling factor
3459 37
            $scale = self::getUInt2d($recordData, 2);
3460
3461
            // offset: 6; size: 2; fit worksheet width to this number of pages, 0 = use as many as needed
3462 37
            $fitToWidth = self::getUInt2d($recordData, 6);
3463
3464
            // offset: 8; size: 2; fit worksheet height to this number of pages, 0 = use as many as needed
3465 37
            $fitToHeight = self::getUInt2d($recordData, 8);
3466
3467
            // offset: 10; size: 2; option flags
3468
3469
            // bit: 0; mask: 0x0001; 0=down then over, 1=over then down
3470 37
            $isOverThenDown = (0x0001 & self::getUInt2d($recordData, 10));
3471
3472
            // bit: 1; mask: 0x0002; 0=landscape, 1=portrait
3473 37
            $isPortrait = (0x0002 & self::getUInt2d($recordData, 10)) >> 1;
3474
3475
            // bit: 2; mask: 0x0004; 1= paper size, scaling factor, paper orient. not init
3476
            // when this bit is set, do not use flags for those properties
3477 37
            $isNotInit = (0x0004 & self::getUInt2d($recordData, 10)) >> 2;
3478
3479 37
            if (!$isNotInit) {
3480 36
                $this->phpSheet->getPageSetup()->setPaperSize($paperSize);
3481 36
                $this->phpSheet->getPageSetup()->setPageOrder(((bool) $isOverThenDown) ? PageSetup::PAGEORDER_OVER_THEN_DOWN : PageSetup::PAGEORDER_DOWN_THEN_OVER);
3482 36
                $this->phpSheet->getPageSetup()->setOrientation(((bool) $isPortrait) ? PageSetup::ORIENTATION_PORTRAIT : PageSetup::ORIENTATION_LANDSCAPE);
3483
3484 36
                $this->phpSheet->getPageSetup()->setScale($scale, false);
3485 36
                $this->phpSheet->getPageSetup()->setFitToPage((bool) $this->isFitToPages);
3486 36
                $this->phpSheet->getPageSetup()->setFitToWidth($fitToWidth, false);
3487 36
                $this->phpSheet->getPageSetup()->setFitToHeight($fitToHeight, false);
3488
            }
3489
3490
            // offset: 16; size: 8; header margin (IEEE 754 floating-point value)
3491 37
            $marginHeader = self::extractNumber(substr($recordData, 16, 8));
3492 37
            $this->phpSheet->getPageMargins()->setHeader($marginHeader);
3493
3494
            // offset: 24; size: 8; footer margin (IEEE 754 floating-point value)
3495 37
            $marginFooter = self::extractNumber(substr($recordData, 24, 8));
3496 37
            $this->phpSheet->getPageMargins()->setFooter($marginFooter);
3497
        }
3498 38
    }
3499
3500
    /**
3501
     * PROTECT - Sheet protection (BIFF2 through BIFF8)
3502
     *   if this record is omitted, then it also means no sheet protection.
3503
     */
3504 3
    private function readProtect(): void
3505
    {
3506 3
        $length = self::getUInt2d($this->data, $this->pos + 2);
3507 3
        $recordData = $this->readRecordData($this->data, $this->pos + 4, $length);
3508
3509
        // move stream pointer to next record
3510 3
        $this->pos += 4 + $length;
3511
3512 3
        if ($this->readDataOnly) {
3513
            return;
3514
        }
3515
3516
        // offset: 0; size: 2;
3517
3518
        // bit 0, mask 0x01; 1 = sheet is protected
3519 3
        $bool = (0x01 & self::getUInt2d($recordData, 0)) >> 0;
3520 3
        $this->phpSheet->getProtection()->setSheet((bool) $bool);
3521 3
    }
3522
3523
    /**
3524
     * SCENPROTECT.
3525
     */
3526
    private function readScenProtect(): void
3527
    {
3528
        $length = self::getUInt2d($this->data, $this->pos + 2);
3529
        $recordData = $this->readRecordData($this->data, $this->pos + 4, $length);
3530
3531
        // move stream pointer to next record
3532
        $this->pos += 4 + $length;
3533
3534
        if ($this->readDataOnly) {
3535
            return;
3536
        }
3537
3538
        // offset: 0; size: 2;
3539
3540
        // bit: 0, mask 0x01; 1 = scenarios are protected
3541
        $bool = (0x01 & self::getUInt2d($recordData, 0)) >> 0;
3542
3543
        $this->phpSheet->getProtection()->setScenarios((bool) $bool);
3544
    }
3545
3546
    /**
3547
     * OBJECTPROTECT.
3548
     */
3549
    private function readObjectProtect(): void
3550
    {
3551
        $length = self::getUInt2d($this->data, $this->pos + 2);
3552
        $recordData = $this->readRecordData($this->data, $this->pos + 4, $length);
3553
3554
        // move stream pointer to next record
3555
        $this->pos += 4 + $length;
3556
3557
        if ($this->readDataOnly) {
3558
            return;
3559
        }
3560
3561
        // offset: 0; size: 2;
3562
3563
        // bit: 0, mask 0x01; 1 = objects are protected
3564
        $bool = (0x01 & self::getUInt2d($recordData, 0)) >> 0;
3565
3566
        $this->phpSheet->getProtection()->setObjects((bool) $bool);
3567
    }
3568
3569
    /**
3570
     * PASSWORD - Sheet protection (hashed) password (BIFF2 through BIFF8).
3571
     */
3572
    private function readPassword(): void
3573
    {
3574
        $length = self::getUInt2d($this->data, $this->pos + 2);
3575
        $recordData = $this->readRecordData($this->data, $this->pos + 4, $length);
3576
3577
        // move stream pointer to next record
3578
        $this->pos += 4 + $length;
3579
3580
        if (!$this->readDataOnly) {
3581
            // offset: 0; size: 2; 16-bit hash value of password
3582
            $password = strtoupper(dechex(self::getUInt2d($recordData, 0))); // the hashed password
3583
            $this->phpSheet->getProtection()->setPassword($password, true);
3584
        }
3585
    }
3586
3587
    /**
3588
     * Read DEFCOLWIDTH record.
3589
     */
3590 38
    private function readDefColWidth(): void
3591
    {
3592 38
        $length = self::getUInt2d($this->data, $this->pos + 2);
3593 38
        $recordData = $this->readRecordData($this->data, $this->pos + 4, $length);
3594
3595
        // move stream pointer to next record
3596 38
        $this->pos += 4 + $length;
3597
3598
        // offset: 0; size: 2; default column width
3599 38
        $width = self::getUInt2d($recordData, 0);
3600 38
        if ($width != 8) {
3601 3
            $this->phpSheet->getDefaultColumnDimension()->setWidth($width);
3602
        }
3603 38
    }
3604
3605
    /**
3606
     * Read COLINFO record.
3607
     */
3608 33
    private function readColInfo(): void
3609
    {
3610 33
        $length = self::getUInt2d($this->data, $this->pos + 2);
3611 33
        $recordData = $this->readRecordData($this->data, $this->pos + 4, $length);
3612
3613
        // move stream pointer to next record
3614 33
        $this->pos += 4 + $length;
3615
3616 33
        if (!$this->readDataOnly) {
3617
            // offset: 0; size: 2; index to first column in range
3618 32
            $firstColumnIndex = self::getUInt2d($recordData, 0);
3619
3620
            // offset: 2; size: 2; index to last column in range
3621 32
            $lastColumnIndex = self::getUInt2d($recordData, 2);
3622
3623
            // offset: 4; size: 2; width of the column in 1/256 of the width of the zero character
3624 32
            $width = self::getUInt2d($recordData, 4);
3625
3626
            // offset: 6; size: 2; index to XF record for default column formatting
3627 32
            $xfIndex = self::getUInt2d($recordData, 6);
3628
3629
            // offset: 8; size: 2; option flags
3630
            // bit: 0; mask: 0x0001; 1= columns are hidden
3631 32
            $isHidden = (0x0001 & self::getUInt2d($recordData, 8)) >> 0;
3632
3633
            // bit: 10-8; mask: 0x0700; outline level of the columns (0 = no outline)
3634 32
            $level = (0x0700 & self::getUInt2d($recordData, 8)) >> 8;
3635
3636
            // bit: 12; mask: 0x1000; 1 = collapsed
3637 32
            $isCollapsed = (0x1000 & self::getUInt2d($recordData, 8)) >> 12;
3638
3639
            // offset: 10; size: 2; not used
3640
3641 32
            for ($i = $firstColumnIndex + 1; $i <= $lastColumnIndex + 1; ++$i) {
3642 32
                if ($lastColumnIndex == 255 || $lastColumnIndex == 256) {
3643 1
                    $this->phpSheet->getDefaultColumnDimension()->setWidth($width / 256);
3644
3645 1
                    break;
3646
                }
3647 31
                $this->phpSheet->getColumnDimensionByColumn($i)->setWidth($width / 256);
3648 31
                $this->phpSheet->getColumnDimensionByColumn($i)->setVisible(!$isHidden);
3649 31
                $this->phpSheet->getColumnDimensionByColumn($i)->setOutlineLevel($level);
3650 31
                $this->phpSheet->getColumnDimensionByColumn($i)->setCollapsed($isCollapsed);
3651 31
                if (isset($this->mapCellXfIndex[$xfIndex])) {
3652 29
                    $this->phpSheet->getColumnDimensionByColumn($i)->setXfIndex($this->mapCellXfIndex[$xfIndex]);
3653
                }
3654
            }
3655
        }
3656 33
    }
3657
3658
    /**
3659
     * ROW.
3660
     *
3661
     * This record contains the properties of a single row in a
3662
     * sheet. Rows and cells in a sheet are divided into blocks
3663
     * of 32 rows.
3664
     *
3665
     * --    "OpenOffice.org's Documentation of the Microsoft
3666
     *         Excel File Format"
3667
     */
3668 24
    private function readRow(): void
3669
    {
3670 24
        $length = self::getUInt2d($this->data, $this->pos + 2);
3671 24
        $recordData = $this->readRecordData($this->data, $this->pos + 4, $length);
3672
3673
        // move stream pointer to next record
3674 24
        $this->pos += 4 + $length;
3675
3676 24
        if (!$this->readDataOnly) {
3677
            // offset: 0; size: 2; index of this row
3678 23
            $r = self::getUInt2d($recordData, 0);
3679
3680
            // offset: 2; size: 2; index to column of the first cell which is described by a cell record
3681
3682
            // offset: 4; size: 2; index to column of the last cell which is described by a cell record, increased by 1
3683
3684
            // offset: 6; size: 2;
3685
3686
            // bit: 14-0; mask: 0x7FFF; height of the row, in twips = 1/20 of a point
3687 23
            $height = (0x7FFF & self::getUInt2d($recordData, 6)) >> 0;
3688
3689
            // bit: 15: mask: 0x8000; 0 = row has custom height; 1= row has default height
3690 23
            $useDefaultHeight = (0x8000 & self::getUInt2d($recordData, 6)) >> 15;
3691
3692 23
            if (!$useDefaultHeight) {
3693 21
                $this->phpSheet->getRowDimension($r + 1)->setRowHeight($height / 20);
3694
            }
3695
3696
            // offset: 8; size: 2; not used
3697
3698
            // offset: 10; size: 2; not used in BIFF5-BIFF8
3699
3700
            // offset: 12; size: 4; option flags and default row formatting
3701
3702
            // bit: 2-0: mask: 0x00000007; outline level of the row
3703 23
            $level = (0x00000007 & self::getInt4d($recordData, 12)) >> 0;
3704 23
            $this->phpSheet->getRowDimension($r + 1)->setOutlineLevel($level);
3705
3706
            // bit: 4; mask: 0x00000010; 1 = outline group start or ends here... and is collapsed
3707 23
            $isCollapsed = (0x00000010 & self::getInt4d($recordData, 12)) >> 4;
3708 23
            $this->phpSheet->getRowDimension($r + 1)->setCollapsed($isCollapsed);
3709
3710
            // bit: 5; mask: 0x00000020; 1 = row is hidden
3711 23
            $isHidden = (0x00000020 & self::getInt4d($recordData, 12)) >> 5;
3712 23
            $this->phpSheet->getRowDimension($r + 1)->setVisible(!$isHidden);
3713
3714
            // bit: 7; mask: 0x00000080; 1 = row has explicit format
3715 23
            $hasExplicitFormat = (0x00000080 & self::getInt4d($recordData, 12)) >> 7;
3716
3717
            // bit: 27-16; mask: 0x0FFF0000; only applies when hasExplicitFormat = 1; index to XF record
3718 23
            $xfIndex = (0x0FFF0000 & self::getInt4d($recordData, 12)) >> 16;
3719
3720 23
            if ($hasExplicitFormat && isset($this->mapCellXfIndex[$xfIndex])) {
3721 3
                $this->phpSheet->getRowDimension($r + 1)->setXfIndex($this->mapCellXfIndex[$xfIndex]);
3722
            }
3723
        }
3724 24
    }
3725
3726
    /**
3727
     * Read RK record
3728
     * This record represents a cell that contains an RK value
3729
     * (encoded integer or floating-point value). If a
3730
     * floating-point value cannot be encoded to an RK value,
3731
     * a NUMBER record will be written. This record replaces the
3732
     * record INTEGER written in BIFF2.
3733
     *
3734
     * --    "OpenOffice.org's Documentation of the Microsoft
3735
     *         Excel File Format"
3736
     */
3737 16
    private function readRk(): void
3738
    {
3739 16
        $length = self::getUInt2d($this->data, $this->pos + 2);
3740 16
        $recordData = $this->readRecordData($this->data, $this->pos + 4, $length);
3741
3742
        // move stream pointer to next record
3743 16
        $this->pos += 4 + $length;
3744
3745
        // offset: 0; size: 2; index to row
3746 16
        $row = self::getUInt2d($recordData, 0);
3747
3748
        // offset: 2; size: 2; index to column
3749 16
        $column = self::getUInt2d($recordData, 2);
3750 16
        $columnString = Coordinate::stringFromColumnIndex($column + 1);
3751
3752
        // Read cell?
3753 16
        if (($this->getReadFilter() !== null) && $this->getReadFilter()->readCell($columnString, $row + 1, $this->phpSheet->getTitle())) {
3754
            // offset: 4; size: 2; index to XF record
3755 16
            $xfIndex = self::getUInt2d($recordData, 4);
3756
3757
            // offset: 6; size: 4; RK value
3758 16
            $rknum = self::getInt4d($recordData, 6);
3759 16
            $numValue = self::getIEEE754($rknum);
3760
3761 16
            $cell = $this->phpSheet->getCell($columnString . ($row + 1));
3762 16
            if (!$this->readDataOnly && isset($this->mapCellXfIndex[$xfIndex])) {
3763
                // add style information
3764 13
                $cell->setXfIndex($this->mapCellXfIndex[$xfIndex]);
3765
            }
3766
3767
            // add cell
3768 16
            $cell->setValueExplicit($numValue, DataType::TYPE_NUMERIC);
3769
        }
3770 16
    }
3771
3772
    /**
3773
     * Read LABELSST record
3774
     * This record represents a cell that contains a string. It
3775
     * replaces the LABEL record and RSTRING record used in
3776
     * BIFF2-BIFF5.
3777
     *
3778
     * --    "OpenOffice.org's Documentation of the Microsoft
3779
     *         Excel File Format"
3780
     */
3781 25
    private function readLabelSst(): void
3782
    {
3783 25
        $length = self::getUInt2d($this->data, $this->pos + 2);
3784 25
        $recordData = $this->readRecordData($this->data, $this->pos + 4, $length);
3785
3786
        // move stream pointer to next record
3787 25
        $this->pos += 4 + $length;
3788
3789
        // offset: 0; size: 2; index to row
3790 25
        $row = self::getUInt2d($recordData, 0);
3791
3792
        // offset: 2; size: 2; index to column
3793 25
        $column = self::getUInt2d($recordData, 2);
3794 25
        $columnString = Coordinate::stringFromColumnIndex($column + 1);
3795
3796 25
        $emptyCell = true;
3797
        // Read cell?
3798 25
        if (($this->getReadFilter() !== null) && $this->getReadFilter()->readCell($columnString, $row + 1, $this->phpSheet->getTitle())) {
3799
            // offset: 4; size: 2; index to XF record
3800 25
            $xfIndex = self::getUInt2d($recordData, 4);
3801
3802
            // offset: 6; size: 4; index to SST record
3803 25
            $index = self::getInt4d($recordData, 6);
3804
3805
            // add cell
3806 25
            if (($fmtRuns = $this->sst[$index]['fmtRuns']) && !$this->readDataOnly) {
3807
                // then we should treat as rich text
3808 3
                $richText = new RichText();
3809 3
                $charPos = 0;
3810 3
                $sstCount = count($this->sst[$index]['fmtRuns']);
3811 3
                for ($i = 0; $i <= $sstCount; ++$i) {
3812 3
                    if (isset($fmtRuns[$i])) {
3813 3
                        $text = StringHelper::substring($this->sst[$index]['value'], $charPos, $fmtRuns[$i]['charPos'] - $charPos);
3814 3
                        $charPos = $fmtRuns[$i]['charPos'];
3815
                    } else {
3816 3
                        $text = StringHelper::substring($this->sst[$index]['value'], $charPos, StringHelper::countCharacters($this->sst[$index]['value']));
3817
                    }
3818
3819 3
                    if (StringHelper::countCharacters($text) > 0) {
3820 3
                        if ($i == 0) { // first text run, no style
3821 2
                            $richText->createText($text);
3822
                        } else {
3823 3
                            $textRun = $richText->createTextRun($text);
3824 3
                            if (isset($fmtRuns[$i - 1])) {
3825 3
                                if ($fmtRuns[$i - 1]['fontIndex'] < 4) {
3826 3
                                    $fontIndex = $fmtRuns[$i - 1]['fontIndex'];
3827
                                } else {
3828
                                    // this has to do with that index 4 is omitted in all BIFF versions for some strange reason
3829
                                    // check the OpenOffice documentation of the FONT record
3830 3
                                    $fontIndex = $fmtRuns[$i - 1]['fontIndex'] - 1;
3831
                                }
3832 3
                                $textRun->setFont(clone $this->objFonts[$fontIndex]);
3833
                            }
3834
                        }
3835
                    }
3836
                }
3837 3
                if ($this->readEmptyCells || trim($richText->getPlainText()) !== '') {
3838 3
                    $cell = $this->phpSheet->getCell($columnString . ($row + 1));
3839 3
                    $cell->setValueExplicit($richText, DataType::TYPE_STRING);
3840 3
                    $emptyCell = false;
3841
                }
3842
            } else {
3843 25
                if ($this->readEmptyCells || trim($this->sst[$index]['value']) !== '') {
3844 25
                    $cell = $this->phpSheet->getCell($columnString . ($row + 1));
3845 25
                    $cell->setValueExplicit($this->sst[$index]['value'], DataType::TYPE_STRING);
3846 25
                    $emptyCell = false;
3847
                }
3848
            }
3849
3850 25
            if (!$this->readDataOnly && !$emptyCell && isset($this->mapCellXfIndex[$xfIndex])) {
3851
                // add style information
3852 24
                $cell->setXfIndex($this->mapCellXfIndex[$xfIndex]);
3853
            }
3854
        }
3855 25
    }
3856
3857
    /**
3858
     * Read MULRK record
3859
     * This record represents a cell range containing RK value
3860
     * cells. All cells are located in the same row.
3861
     *
3862
     * --    "OpenOffice.org's Documentation of the Microsoft
3863
     *         Excel File Format"
3864
     */
3865 13
    private function readMulRk(): void
3866
    {
3867 13
        $length = self::getUInt2d($this->data, $this->pos + 2);
3868 13
        $recordData = $this->readRecordData($this->data, $this->pos + 4, $length);
3869
3870
        // move stream pointer to next record
3871 13
        $this->pos += 4 + $length;
3872
3873
        // offset: 0; size: 2; index to row
3874 13
        $row = self::getUInt2d($recordData, 0);
3875
3876
        // offset: 2; size: 2; index to first column
3877 13
        $colFirst = self::getUInt2d($recordData, 2);
3878
3879
        // offset: var; size: 2; index to last column
3880 13
        $colLast = self::getUInt2d($recordData, $length - 2);
3881 13
        $columns = $colLast - $colFirst + 1;
3882
3883
        // offset within record data
3884 13
        $offset = 4;
3885
3886 13
        for ($i = 1; $i <= $columns; ++$i) {
3887 13
            $columnString = Coordinate::stringFromColumnIndex($colFirst + $i);
3888
3889
            // Read cell?
3890 13
            if (($this->getReadFilter() !== null) && $this->getReadFilter()->readCell($columnString, $row + 1, $this->phpSheet->getTitle())) {
3891
                // offset: var; size: 2; index to XF record
3892 13
                $xfIndex = self::getUInt2d($recordData, $offset);
3893
3894
                // offset: var; size: 4; RK value
3895 13
                $numValue = self::getIEEE754(self::getInt4d($recordData, $offset + 2));
3896 13
                $cell = $this->phpSheet->getCell($columnString . ($row + 1));
3897 13
                if (!$this->readDataOnly && isset($this->mapCellXfIndex[$xfIndex])) {
3898
                    // add style
3899 12
                    $cell->setXfIndex($this->mapCellXfIndex[$xfIndex]);
3900
                }
3901
3902
                // add cell value
3903 13
                $cell->setValueExplicit($numValue, DataType::TYPE_NUMERIC);
3904
            }
3905
3906 13
            $offset += 6;
3907
        }
3908 13
    }
3909
3910
    /**
3911
     * Read NUMBER record
3912
     * This record represents a cell that contains a
3913
     * floating-point value.
3914
     *
3915
     * --    "OpenOffice.org's Documentation of the Microsoft
3916
     *         Excel File Format"
3917
     */
3918 18
    private function readNumber(): void
3919
    {
3920 18
        $length = self::getUInt2d($this->data, $this->pos + 2);
3921 18
        $recordData = $this->readRecordData($this->data, $this->pos + 4, $length);
3922
3923
        // move stream pointer to next record
3924 18
        $this->pos += 4 + $length;
3925
3926
        // offset: 0; size: 2; index to row
3927 18
        $row = self::getUInt2d($recordData, 0);
3928
3929
        // offset: 2; size 2; index to column
3930 18
        $column = self::getUInt2d($recordData, 2);
3931 18
        $columnString = Coordinate::stringFromColumnIndex($column + 1);
3932
3933
        // Read cell?
3934 18
        if (($this->getReadFilter() !== null) && $this->getReadFilter()->readCell($columnString, $row + 1, $this->phpSheet->getTitle())) {
3935
            // offset 4; size: 2; index to XF record
3936 18
            $xfIndex = self::getUInt2d($recordData, 4);
3937
3938 18
            $numValue = self::extractNumber(substr($recordData, 6, 8));
3939
3940 18
            $cell = $this->phpSheet->getCell($columnString . ($row + 1));
3941 18
            if (!$this->readDataOnly && isset($this->mapCellXfIndex[$xfIndex])) {
3942
                // add cell style
3943 17
                $cell->setXfIndex($this->mapCellXfIndex[$xfIndex]);
3944
            }
3945
3946
            // add cell value
3947 18
            $cell->setValueExplicit($numValue, DataType::TYPE_NUMERIC);
3948
        }
3949 18
    }
3950
3951
    /**
3952
     * Read FORMULA record + perhaps a following STRING record if formula result is a string
3953
     * This record contains the token array and the result of a
3954
     * formula cell.
3955
     *
3956
     * --    "OpenOffice.org's Documentation of the Microsoft
3957
     *         Excel File Format"
3958
     */
3959 17
    private function readFormula(): void
3960
    {
3961 17
        $length = self::getUInt2d($this->data, $this->pos + 2);
3962 17
        $recordData = $this->readRecordData($this->data, $this->pos + 4, $length);
3963
3964
        // move stream pointer to next record
3965 17
        $this->pos += 4 + $length;
3966
3967
        // offset: 0; size: 2; row index
3968 17
        $row = self::getUInt2d($recordData, 0);
3969
3970
        // offset: 2; size: 2; col index
3971 17
        $column = self::getUInt2d($recordData, 2);
3972 17
        $columnString = Coordinate::stringFromColumnIndex($column + 1);
3973
3974
        // offset: 20: size: variable; formula structure
3975 17
        $formulaStructure = substr($recordData, 20);
3976
3977
        // offset: 14: size: 2; option flags, recalculate always, recalculate on open etc.
3978 17
        $options = self::getUInt2d($recordData, 14);
3979
3980
        // bit: 0; mask: 0x0001; 1 = recalculate always
3981
        // bit: 1; mask: 0x0002; 1 = calculate on open
3982
        // bit: 2; mask: 0x0008; 1 = part of a shared formula
3983 17
        $isPartOfSharedFormula = (bool) (0x0008 & $options);
3984
3985
        // WARNING:
3986
        // We can apparently not rely on $isPartOfSharedFormula. Even when $isPartOfSharedFormula = true
3987
        // the formula data may be ordinary formula data, therefore we need to check
3988
        // explicitly for the tExp token (0x01)
3989 17
        $isPartOfSharedFormula = $isPartOfSharedFormula && ord($formulaStructure[2]) == 0x01;
3990
3991 17
        if ($isPartOfSharedFormula) {
3992
            // part of shared formula which means there will be a formula with a tExp token and nothing else
3993
            // get the base cell, grab tExp token
3994
            $baseRow = self::getUInt2d($formulaStructure, 3);
3995
            $baseCol = self::getUInt2d($formulaStructure, 5);
3996
            $this->baseCell = Coordinate::stringFromColumnIndex($baseCol + 1) . ($baseRow + 1);
3997
        }
3998
3999
        // Read cell?
4000 17
        if (($this->getReadFilter() !== null) && $this->getReadFilter()->readCell($columnString, $row + 1, $this->phpSheet->getTitle())) {
4001 17
            if ($isPartOfSharedFormula) {
4002
                // formula is added to this cell after the sheet has been read
4003
                $this->sharedFormulaParts[$columnString . ($row + 1)] = $this->baseCell;
4004
            }
4005
4006
            // offset: 16: size: 4; not used
4007
4008
            // offset: 4; size: 2; XF index
4009 17
            $xfIndex = self::getUInt2d($recordData, 4);
4010
4011
            // offset: 6; size: 8; result of the formula
4012 17
            if ((ord($recordData[6]) == 0) && (ord($recordData[12]) == 255) && (ord($recordData[13]) == 255)) {
4013
                // String formula. Result follows in appended STRING record
4014
                $dataType = DataType::TYPE_STRING;
4015
4016
                // read possible SHAREDFMLA record
4017
                $code = self::getUInt2d($this->data, $this->pos);
4018
                if ($code == self::XLS_TYPE_SHAREDFMLA) {
4019
                    $this->readSharedFmla();
4020
                }
4021
4022
                // read STRING record
4023
                $value = $this->readString();
4024
            } elseif (
4025 17
                (ord($recordData[6]) == 1)
4026 17
                && (ord($recordData[12]) == 255)
4027 17
                && (ord($recordData[13]) == 255)
4028
            ) {
4029
                // Boolean formula. Result is in +2; 0=false, 1=true
4030
                $dataType = DataType::TYPE_BOOL;
4031
                $value = (bool) ord($recordData[8]);
4032
            } elseif (
4033 17
                (ord($recordData[6]) == 2)
4034 17
                && (ord($recordData[12]) == 255)
4035 17
                && (ord($recordData[13]) == 255)
4036
            ) {
4037
                // Error formula. Error code is in +2
4038 11
                $dataType = DataType::TYPE_ERROR;
4039 11
                $value = Xls\ErrorCode::lookup(ord($recordData[8]));
4040
            } elseif (
4041 17
                (ord($recordData[6]) == 3)
4042 17
                && (ord($recordData[12]) == 255)
4043 17
                && (ord($recordData[13]) == 255)
4044
            ) {
4045
                // Formula result is a null string
4046 1
                $dataType = DataType::TYPE_NULL;
4047 1
                $value = '';
4048
            } else {
4049
                // forumla result is a number, first 14 bytes like _NUMBER record
4050 17
                $dataType = DataType::TYPE_NUMERIC;
4051 17
                $value = self::extractNumber(substr($recordData, 6, 8));
4052
            }
4053
4054 17
            $cell = $this->phpSheet->getCell($columnString . ($row + 1));
4055 17
            if (!$this->readDataOnly && isset($this->mapCellXfIndex[$xfIndex])) {
4056
                // add cell style
4057 16
                $cell->setXfIndex($this->mapCellXfIndex[$xfIndex]);
4058
            }
4059
4060
            // store the formula
4061 17
            if (!$isPartOfSharedFormula) {
4062
                // not part of shared formula
4063
                // add cell value. If we can read formula, populate with formula, otherwise just used cached value
4064
                try {
4065 17
                    if ($this->version != self::XLS_BIFF8) {
4066 1
                        throw new Exception('Not BIFF8. Can only read BIFF8 formulas');
4067
                    }
4068 16
                    $formula = $this->getFormulaFromStructure($formulaStructure); // get formula in human language
4069 16
                    $cell->setValueExplicit('=' . $formula, DataType::TYPE_FORMULA);
4070 1
                } catch (PhpSpreadsheetException $e) {
4071 17
                    $cell->setValueExplicit($value, $dataType);
4072
                }
4073
            } else {
4074
                if ($this->version == self::XLS_BIFF8) {
4075
                    // do nothing at this point, formula id added later in the code
4076
                } else {
4077
                    $cell->setValueExplicit($value, $dataType);
4078
                }
4079
            }
4080
4081
            // store the cached calculated value
4082 17
            $cell->setCalculatedValue($value);
4083
        }
4084 17
    }
4085
4086
    /**
4087
     * Read a SHAREDFMLA record. This function just stores the binary shared formula in the reader,
4088
     * which usually contains relative references.
4089
     * These will be used to construct the formula in each shared formula part after the sheet is read.
4090
     */
4091
    private function readSharedFmla(): void
4092
    {
4093
        $length = self::getUInt2d($this->data, $this->pos + 2);
4094
        $recordData = $this->readRecordData($this->data, $this->pos + 4, $length);
4095
4096
        // move stream pointer to next record
4097
        $this->pos += 4 + $length;
4098
4099
        // offset: 0, size: 6; cell range address of the area used by the shared formula, not used for anything
4100
        $cellRange = substr($recordData, 0, 6);
4101
        $cellRange = $this->readBIFF5CellRangeAddressFixed($cellRange); // note: even BIFF8 uses BIFF5 syntax
4102
4103
        // offset: 6, size: 1; not used
4104
4105
        // offset: 7, size: 1; number of existing FORMULA records for this shared formula
4106
        $no = ord($recordData[7]);
4107
4108
        // offset: 8, size: var; Binary token array of the shared formula
4109
        $formula = substr($recordData, 8);
4110
4111
        // at this point we only store the shared formula for later use
4112
        $this->sharedFormulas[$this->baseCell] = $formula;
4113
    }
4114
4115
    /**
4116
     * Read a STRING record from current stream position and advance the stream pointer to next record
4117
     * This record is used for storing result from FORMULA record when it is a string, and
4118
     * it occurs directly after the FORMULA record.
4119
     *
4120
     * @return string The string contents as UTF-8
4121
     */
4122
    private function readString()
4123
    {
4124
        $length = self::getUInt2d($this->data, $this->pos + 2);
4125
        $recordData = $this->readRecordData($this->data, $this->pos + 4, $length);
4126
4127
        // move stream pointer to next record
4128
        $this->pos += 4 + $length;
4129
4130
        if ($this->version == self::XLS_BIFF8) {
4131
            $string = self::readUnicodeStringLong($recordData);
4132
            $value = $string['value'];
4133
        } else {
4134
            $string = $this->readByteStringLong($recordData);
4135
            $value = $string['value'];
4136
        }
4137
4138
        return $value;
4139
    }
4140
4141
    /**
4142
     * Read BOOLERR record
4143
     * This record represents a Boolean value or error value
4144
     * cell.
4145
     *
4146
     * --    "OpenOffice.org's Documentation of the Microsoft
4147
     *         Excel File Format"
4148
     */
4149 9
    private function readBoolErr(): void
4150
    {
4151 9
        $length = self::getUInt2d($this->data, $this->pos + 2);
4152 9
        $recordData = $this->readRecordData($this->data, $this->pos + 4, $length);
4153
4154
        // move stream pointer to next record
4155 9
        $this->pos += 4 + $length;
4156
4157
        // offset: 0; size: 2; row index
4158 9
        $row = self::getUInt2d($recordData, 0);
4159
4160
        // offset: 2; size: 2; column index
4161 9
        $column = self::getUInt2d($recordData, 2);
4162 9
        $columnString = Coordinate::stringFromColumnIndex($column + 1);
4163
4164
        // Read cell?
4165 9
        if (($this->getReadFilter() !== null) && $this->getReadFilter()->readCell($columnString, $row + 1, $this->phpSheet->getTitle())) {
4166
            // offset: 4; size: 2; index to XF record
4167 9
            $xfIndex = self::getUInt2d($recordData, 4);
4168
4169
            // offset: 6; size: 1; the boolean value or error value
4170 9
            $boolErr = ord($recordData[6]);
4171
4172
            // offset: 7; size: 1; 0=boolean; 1=error
4173 9
            $isError = ord($recordData[7]);
4174
4175 9
            $cell = $this->phpSheet->getCell($columnString . ($row + 1));
4176
            switch ($isError) {
4177 9
                case 0: // boolean
4178 9
                    $value = (bool) $boolErr;
4179
4180
                    // add cell value
4181 9
                    $cell->setValueExplicit($value, DataType::TYPE_BOOL);
4182
4183 9
                    break;
4184
                case 1: // error type
4185
                    $value = Xls\ErrorCode::lookup($boolErr);
4186
4187
                    // add cell value
4188
                    $cell->setValueExplicit($value, DataType::TYPE_ERROR);
4189
4190
                    break;
4191
            }
4192
4193 9
            if (!$this->readDataOnly && isset($this->mapCellXfIndex[$xfIndex])) {
4194
                // add cell style
4195 8
                $cell->setXfIndex($this->mapCellXfIndex[$xfIndex]);
4196
            }
4197
        }
4198 9
    }
4199
4200
    /**
4201
     * Read MULBLANK record
4202
     * This record represents a cell range of empty cells. All
4203
     * cells are located in the same row.
4204
     *
4205
     * --    "OpenOffice.org's Documentation of the Microsoft
4206
     *         Excel File Format"
4207
     */
4208 13
    private function readMulBlank(): void
4209
    {
4210 13
        $length = self::getUInt2d($this->data, $this->pos + 2);
4211 13
        $recordData = $this->readRecordData($this->data, $this->pos + 4, $length);
4212
4213
        // move stream pointer to next record
4214 13
        $this->pos += 4 + $length;
4215
4216
        // offset: 0; size: 2; index to row
4217 13
        $row = self::getUInt2d($recordData, 0);
4218
4219
        // offset: 2; size: 2; index to first column
4220 13
        $fc = self::getUInt2d($recordData, 2);
4221
4222
        // offset: 4; size: 2 x nc; list of indexes to XF records
4223
        // add style information
4224 13
        if (!$this->readDataOnly && $this->readEmptyCells) {
4225 12
            for ($i = 0; $i < $length / 2 - 3; ++$i) {
4226 12
                $columnString = Coordinate::stringFromColumnIndex($fc + $i + 1);
4227
4228
                // Read cell?
4229 12
                if (($this->getReadFilter() !== null) && $this->getReadFilter()->readCell($columnString, $row + 1, $this->phpSheet->getTitle())) {
4230 12
                    $xfIndex = self::getUInt2d($recordData, 4 + 2 * $i);
4231 12
                    if (isset($this->mapCellXfIndex[$xfIndex])) {
4232 12
                        $this->phpSheet->getCell($columnString . ($row + 1))->setXfIndex($this->mapCellXfIndex[$xfIndex]);
4233
                    }
4234
                }
4235
            }
4236
        }
4237
4238
        // offset: 6; size 2; index to last column (not needed)
4239 13
    }
4240
4241
    /**
4242
     * Read LABEL record
4243
     * This record represents a cell that contains a string. In
4244
     * BIFF8 it is usually replaced by the LABELSST record.
4245
     * Excel still uses this record, if it copies unformatted
4246
     * text cells to the clipboard.
4247
     *
4248
     * --    "OpenOffice.org's Documentation of the Microsoft
4249
     *         Excel File Format"
4250
     */
4251 2
    private function readLabel(): void
4252
    {
4253 2
        $length = self::getUInt2d($this->data, $this->pos + 2);
4254 2
        $recordData = $this->readRecordData($this->data, $this->pos + 4, $length);
4255
4256
        // move stream pointer to next record
4257 2
        $this->pos += 4 + $length;
4258
4259
        // offset: 0; size: 2; index to row
4260 2
        $row = self::getUInt2d($recordData, 0);
4261
4262
        // offset: 2; size: 2; index to column
4263 2
        $column = self::getUInt2d($recordData, 2);
4264 2
        $columnString = Coordinate::stringFromColumnIndex($column + 1);
4265
4266
        // Read cell?
4267 2
        if (($this->getReadFilter() !== null) && $this->getReadFilter()->readCell($columnString, $row + 1, $this->phpSheet->getTitle())) {
4268
            // offset: 4; size: 2; XF index
4269 2
            $xfIndex = self::getUInt2d($recordData, 4);
4270
4271
            // add cell value
4272
            // todo: what if string is very long? continue record
4273 2
            if ($this->version == self::XLS_BIFF8) {
4274 1
                $string = self::readUnicodeStringLong(substr($recordData, 6));
4275 1
                $value = $string['value'];
4276
            } else {
4277 1
                $string = $this->readByteStringLong(substr($recordData, 6));
4278 1
                $value = $string['value'];
4279
            }
4280 2
            if ($this->readEmptyCells || trim($value) !== '') {
4281 2
                $cell = $this->phpSheet->getCell($columnString . ($row + 1));
4282 2
                $cell->setValueExplicit($value, DataType::TYPE_STRING);
4283
4284 2
                if (!$this->readDataOnly && isset($this->mapCellXfIndex[$xfIndex])) {
4285
                    // add cell style
4286 2
                    $cell->setXfIndex($this->mapCellXfIndex[$xfIndex]);
4287
                }
4288
            }
4289
        }
4290 2
    }
4291
4292
    /**
4293
     * Read BLANK record.
4294
     */
4295 8
    private function readBlank(): void
4296
    {
4297 8
        $length = self::getUInt2d($this->data, $this->pos + 2);
4298 8
        $recordData = $this->readRecordData($this->data, $this->pos + 4, $length);
4299
4300
        // move stream pointer to next record
4301 8
        $this->pos += 4 + $length;
4302
4303
        // offset: 0; size: 2; row index
4304 8
        $row = self::getUInt2d($recordData, 0);
4305
4306
        // offset: 2; size: 2; col index
4307 8
        $col = self::getUInt2d($recordData, 2);
4308 8
        $columnString = Coordinate::stringFromColumnIndex($col + 1);
4309
4310
        // Read cell?
4311 8
        if (($this->getReadFilter() !== null) && $this->getReadFilter()->readCell($columnString, $row + 1, $this->phpSheet->getTitle())) {
4312
            // offset: 4; size: 2; XF index
4313 8
            $xfIndex = self::getUInt2d($recordData, 4);
4314
4315
            // add style information
4316 8
            if (!$this->readDataOnly && $this->readEmptyCells && isset($this->mapCellXfIndex[$xfIndex])) {
4317 8
                $this->phpSheet->getCell($columnString . ($row + 1))->setXfIndex($this->mapCellXfIndex[$xfIndex]);
4318
            }
4319
        }
4320 8
    }
4321
4322
    /**
4323
     * Read MSODRAWING record.
4324
     */
4325 6
    private function readMsoDrawing(): void
4326
    {
4327 6
        $length = self::getUInt2d($this->data, $this->pos + 2);
4328
4329
        // get spliced record data
4330 6
        $splicedRecordData = $this->getSplicedRecordData();
4331 6
        $recordData = $splicedRecordData['recordData'];
4332
4333 6
        $this->drawingData .= $recordData;
4334 6
    }
4335
4336
    /**
4337
     * Read OBJ record.
4338
     */
4339 7
    private function readObj(): void
4340
    {
4341 7
        $length = self::getUInt2d($this->data, $this->pos + 2);
4342 7
        $recordData = $this->readRecordData($this->data, $this->pos + 4, $length);
4343
4344
        // move stream pointer to next record
4345 7
        $this->pos += 4 + $length;
4346
4347 7
        if ($this->readDataOnly || $this->version != self::XLS_BIFF8) {
4348 1
            return;
4349
        }
4350
4351
        // recordData consists of an array of subrecords looking like this:
4352
        //    ft: 2 bytes; ftCmo type (0x15)
4353
        //    cb: 2 bytes; size in bytes of ftCmo data
4354
        //    ot: 2 bytes; Object Type
4355
        //    id: 2 bytes; Object id number
4356
        //    grbit: 2 bytes; Option Flags
4357
        //    data: var; subrecord data
4358
4359
        // for now, we are just interested in the second subrecord containing the object type
4360 6
        $ftCmoType = self::getUInt2d($recordData, 0);
4361 6
        $cbCmoSize = self::getUInt2d($recordData, 2);
4362 6
        $otObjType = self::getUInt2d($recordData, 4);
4363 6
        $idObjID = self::getUInt2d($recordData, 6);
4364 6
        $grbitOpts = self::getUInt2d($recordData, 6);
4365
4366 6
        $this->objs[] = [
4367 6
            'ftCmoType' => $ftCmoType,
4368 6
            'cbCmoSize' => $cbCmoSize,
4369 6
            'otObjType' => $otObjType,
4370 6
            'idObjID' => $idObjID,
4371 6
            'grbitOpts' => $grbitOpts,
4372
        ];
4373 6
        $this->textObjRef = $idObjID;
4374 6
    }
4375
4376
    /**
4377
     * Read WINDOW2 record.
4378
     */
4379 39
    private function readWindow2(): void
4380
    {
4381 39
        $length = self::getUInt2d($this->data, $this->pos + 2);
4382 39
        $recordData = $this->readRecordData($this->data, $this->pos + 4, $length);
4383
4384
        // move stream pointer to next record
4385 39
        $this->pos += 4 + $length;
4386
4387
        // offset: 0; size: 2; option flags
4388 39
        $options = self::getUInt2d($recordData, 0);
4389
4390
        // offset: 2; size: 2; index to first visible row
4391 39
        $firstVisibleRow = self::getUInt2d($recordData, 2);
4392
4393
        // offset: 4; size: 2; index to first visible colum
4394 39
        $firstVisibleColumn = self::getUInt2d($recordData, 4);
4395 39
        $zoomscaleInPageBreakPreview = 0;
4396 39
        $zoomscaleInNormalView = 0;
4397 39
        if ($this->version === self::XLS_BIFF8) {
4398
            // offset:  8; size: 2; not used
4399
            // offset: 10; size: 2; cached magnification factor in page break preview (in percent); 0 = Default (60%)
4400
            // offset: 12; size: 2; cached magnification factor in normal view (in percent); 0 = Default (100%)
4401
            // offset: 14; size: 4; not used
4402 38
            if (!isset($recordData[10])) {
4403
                $zoomscaleInPageBreakPreview = 0;
4404
            } else {
4405 38
                $zoomscaleInPageBreakPreview = self::getUInt2d($recordData, 10);
4406
            }
4407
4408 38
            if ($zoomscaleInPageBreakPreview === 0) {
4409 38
                $zoomscaleInPageBreakPreview = 60;
4410
            }
4411
4412 38
            if (!isset($recordData[12])) {
4413
                $zoomscaleInNormalView = 0;
4414
            } else {
4415 38
                $zoomscaleInNormalView = self::getUInt2d($recordData, 12);
4416
            }
4417
4418 38
            if ($zoomscaleInNormalView === 0) {
4419 21
                $zoomscaleInNormalView = 100;
4420
            }
4421
        }
4422
4423
        // bit: 1; mask: 0x0002; 0 = do not show gridlines, 1 = show gridlines
4424 39
        $showGridlines = (bool) ((0x0002 & $options) >> 1);
4425 39
        $this->phpSheet->setShowGridlines($showGridlines);
4426
4427
        // bit: 2; mask: 0x0004; 0 = do not show headers, 1 = show headers
4428 39
        $showRowColHeaders = (bool) ((0x0004 & $options) >> 2);
4429 39
        $this->phpSheet->setShowRowColHeaders($showRowColHeaders);
4430
4431
        // bit: 3; mask: 0x0008; 0 = panes are not frozen, 1 = panes are frozen
4432 39
        $this->frozen = (bool) ((0x0008 & $options) >> 3);
4433
4434
        // bit: 6; mask: 0x0040; 0 = columns from left to right, 1 = columns from right to left
4435 39
        $this->phpSheet->setRightToLeft((bool) ((0x0040 & $options) >> 6));
4436
4437
        // bit: 10; mask: 0x0400; 0 = sheet not active, 1 = sheet active
4438 39
        $isActive = (bool) ((0x0400 & $options) >> 10);
4439 39
        if ($isActive) {
4440 36
            $this->spreadsheet->setActiveSheetIndex($this->spreadsheet->getIndex($this->phpSheet));
4441
        }
4442
4443
        // bit: 11; mask: 0x0800; 0 = normal view, 1 = page break view
4444 39
        $isPageBreakPreview = (bool) ((0x0800 & $options) >> 11);
4445
4446
        //FIXME: set $firstVisibleRow and $firstVisibleColumn
4447
4448 39
        if ($this->phpSheet->getSheetView()->getView() !== SheetView::SHEETVIEW_PAGE_LAYOUT) {
4449
            //NOTE: this setting is inferior to page layout view(Excel2007-)
4450 39
            $view = $isPageBreakPreview ? SheetView::SHEETVIEW_PAGE_BREAK_PREVIEW : SheetView::SHEETVIEW_NORMAL;
4451 39
            $this->phpSheet->getSheetView()->setView($view);
4452 39
            if ($this->version === self::XLS_BIFF8) {
4453 38
                $zoomScale = $isPageBreakPreview ? $zoomscaleInPageBreakPreview : $zoomscaleInNormalView;
4454 38
                $this->phpSheet->getSheetView()->setZoomScale($zoomScale);
4455 38
                $this->phpSheet->getSheetView()->setZoomScaleNormal($zoomscaleInNormalView);
4456
            }
4457
        }
4458 39
    }
4459
4460
    /**
4461
     * Read PLV Record(Created by Excel2007 or upper).
4462
     */
4463 21
    private function readPageLayoutView(): void
4464
    {
4465 21
        $length = self::getUInt2d($this->data, $this->pos + 2);
4466 21
        $recordData = $this->readRecordData($this->data, $this->pos + 4, $length);
4467
4468
        // move stream pointer to next record
4469 21
        $this->pos += 4 + $length;
4470
4471
        // offset: 0; size: 2; rt
4472
        //->ignore
4473 21
        $rt = self::getUInt2d($recordData, 0);
4474
        // offset: 2; size: 2; grbitfr
4475
        //->ignore
4476 21
        $grbitFrt = self::getUInt2d($recordData, 2);
4477
        // offset: 4; size: 8; reserved
4478
        //->ignore
4479
4480
        // offset: 12; size 2; zoom scale
4481 21
        $wScalePLV = self::getUInt2d($recordData, 12);
4482
        // offset: 14; size 2; grbit
4483 21
        $grbit = self::getUInt2d($recordData, 14);
4484
4485
        // decomprise grbit
4486 21
        $fPageLayoutView = $grbit & 0x01;
4487 21
        $fRulerVisible = ($grbit >> 1) & 0x01; //no support
4488 21
        $fWhitespaceHidden = ($grbit >> 3) & 0x01; //no support
4489
4490 21
        if ($fPageLayoutView === 1) {
4491
            $this->phpSheet->getSheetView()->setView(SheetView::SHEETVIEW_PAGE_LAYOUT);
4492
            $this->phpSheet->getSheetView()->setZoomScale($wScalePLV); //set by Excel2007 only if SHEETVIEW_PAGE_LAYOUT
4493
        }
4494
        //otherwise, we cannot know whether SHEETVIEW_PAGE_LAYOUT or SHEETVIEW_PAGE_BREAK_PREVIEW.
4495 21
    }
4496
4497
    /**
4498
     * Read SCL record.
4499
     */
4500 2
    private function readScl(): void
4501
    {
4502 2
        $length = self::getUInt2d($this->data, $this->pos + 2);
4503 2
        $recordData = $this->readRecordData($this->data, $this->pos + 4, $length);
4504
4505
        // move stream pointer to next record
4506 2
        $this->pos += 4 + $length;
4507
4508
        // offset: 0; size: 2; numerator of the view magnification
4509 2
        $numerator = self::getUInt2d($recordData, 0);
4510
4511
        // offset: 2; size: 2; numerator of the view magnification
4512 2
        $denumerator = self::getUInt2d($recordData, 2);
4513
4514
        // set the zoom scale (in percent)
4515 2
        $this->phpSheet->getSheetView()->setZoomScale($numerator * 100 / $denumerator);
4516 2
    }
4517
4518
    /**
4519
     * Read PANE record.
4520
     */
4521 5
    private function readPane(): void
4522
    {
4523 5
        $length = self::getUInt2d($this->data, $this->pos + 2);
4524 5
        $recordData = $this->readRecordData($this->data, $this->pos + 4, $length);
4525
4526
        // move stream pointer to next record
4527 5
        $this->pos += 4 + $length;
4528
4529 5
        if (!$this->readDataOnly) {
4530
            // offset: 0; size: 2; position of vertical split
4531 5
            $px = self::getUInt2d($recordData, 0);
4532
4533
            // offset: 2; size: 2; position of horizontal split
4534 5
            $py = self::getUInt2d($recordData, 2);
4535
4536
            // offset: 4; size: 2; top most visible row in the bottom pane
4537 5
            $rwTop = self::getUInt2d($recordData, 4);
4538
4539
            // offset: 6; size: 2; first visible left column in the right pane
4540 5
            $colLeft = self::getUInt2d($recordData, 6);
4541
4542 5
            if ($this->frozen) {
4543
                // frozen panes
4544 5
                $cell = Coordinate::stringFromColumnIndex($px + 1) . ($py + 1);
4545 5
                $topLeftCell = Coordinate::stringFromColumnIndex($colLeft + 1) . ($rwTop + 1);
4546 5
                $this->phpSheet->freezePane($cell, $topLeftCell);
4547
            }
4548
            // unfrozen panes; split windows; not supported by PhpSpreadsheet core
4549
        }
4550 5
    }
4551
4552
    /**
4553
     * Read SELECTION record. There is one such record for each pane in the sheet.
4554
     */
4555 36
    private function readSelection(): void
4556
    {
4557 36
        $length = self::getUInt2d($this->data, $this->pos + 2);
4558 36
        $recordData = $this->readRecordData($this->data, $this->pos + 4, $length);
4559
4560
        // move stream pointer to next record
4561 36
        $this->pos += 4 + $length;
4562
4563 36
        if (!$this->readDataOnly) {
4564
            // offset: 0; size: 1; pane identifier
4565 35
            $paneId = ord($recordData[0]);
4566
4567
            // offset: 1; size: 2; index to row of the active cell
4568 35
            $r = self::getUInt2d($recordData, 1);
4569
4570
            // offset: 3; size: 2; index to column of the active cell
4571 35
            $c = self::getUInt2d($recordData, 3);
4572
4573
            // offset: 5; size: 2; index into the following cell range list to the
4574
            //  entry that contains the active cell
4575 35
            $index = self::getUInt2d($recordData, 5);
4576
4577
            // offset: 7; size: var; cell range address list containing all selected cell ranges
4578 35
            $data = substr($recordData, 7);
4579 35
            $cellRangeAddressList = $this->readBIFF5CellRangeAddressList($data); // note: also BIFF8 uses BIFF5 syntax
4580
4581 35
            $selectedCells = $cellRangeAddressList['cellRangeAddresses'][0];
4582
4583
            // first row '1' + last row '16384' indicates that full column is selected (apparently also in BIFF8!)
4584 35
            if (preg_match('/^([A-Z]+1\:[A-Z]+)16384$/', $selectedCells)) {
4585
                $selectedCells = preg_replace('/^([A-Z]+1\:[A-Z]+)16384$/', '${1}1048576', $selectedCells);
4586
            }
4587
4588
            // first row '1' + last row '65536' indicates that full column is selected
4589 35
            if (preg_match('/^([A-Z]+1\:[A-Z]+)65536$/', $selectedCells)) {
4590
                $selectedCells = preg_replace('/^([A-Z]+1\:[A-Z]+)65536$/', '${1}1048576', $selectedCells);
4591
            }
4592
4593
            // first column 'A' + last column 'IV' indicates that full row is selected
4594 35
            if (preg_match('/^(A\d+\:)IV(\d+)$/', $selectedCells)) {
4595 2
                $selectedCells = preg_replace('/^(A\d+\:)IV(\d+)$/', '${1}XFD${2}', $selectedCells);
4596
            }
4597
4598 35
            $this->phpSheet->setSelectedCells($selectedCells);
4599
        }
4600 36
    }
4601
4602 13
    private function includeCellRangeFiltered($cellRangeAddress)
4603
    {
4604 13
        $includeCellRange = true;
4605 13
        if ($this->getReadFilter() !== null) {
4606 13
            $includeCellRange = false;
4607 13
            $rangeBoundaries = Coordinate::getRangeBoundaries($cellRangeAddress);
4608 13
            ++$rangeBoundaries[1][0];
4609 13
            for ($row = $rangeBoundaries[0][1]; $row <= $rangeBoundaries[1][1]; ++$row) {
4610 13
                for ($column = $rangeBoundaries[0][0]; $column != $rangeBoundaries[1][0]; ++$column) {
4611 13
                    if ($this->getReadFilter()->readCell($column, $row, $this->phpSheet->getTitle())) {
4612 13
                        $includeCellRange = true;
4613
4614 13
                        break 2;
4615
                    }
4616
                }
4617
            }
4618
        }
4619
4620 13
        return $includeCellRange;
4621
    }
4622
4623
    /**
4624
     * MERGEDCELLS.
4625
     *
4626
     * This record contains the addresses of merged cell ranges
4627
     * in the current sheet.
4628
     *
4629
     * --    "OpenOffice.org's Documentation of the Microsoft
4630
     *         Excel File Format"
4631
     */
4632 14
    private function readMergedCells(): void
4633
    {
4634 14
        $length = self::getUInt2d($this->data, $this->pos + 2);
4635 14
        $recordData = $this->readRecordData($this->data, $this->pos + 4, $length);
4636
4637
        // move stream pointer to next record
4638 14
        $this->pos += 4 + $length;
4639
4640 14
        if ($this->version == self::XLS_BIFF8 && !$this->readDataOnly) {
4641 13
            $cellRangeAddressList = $this->readBIFF8CellRangeAddressList($recordData);
4642 13
            foreach ($cellRangeAddressList['cellRangeAddresses'] as $cellRangeAddress) {
4643
                if (
4644 13
                    (strpos($cellRangeAddress, ':') !== false) &&
4645 13
                    ($this->includeCellRangeFiltered($cellRangeAddress))
4646
                ) {
4647 13
                    $this->phpSheet->mergeCells($cellRangeAddress);
4648
                }
4649
            }
4650
        }
4651 14
    }
4652
4653
    /**
4654
     * Read HYPERLINK record.
4655
     */
4656 3
    private function readHyperLink(): void
4657
    {
4658 3
        $length = self::getUInt2d($this->data, $this->pos + 2);
4659 3
        $recordData = $this->readRecordData($this->data, $this->pos + 4, $length);
4660
4661
        // move stream pointer forward to next record
4662 3
        $this->pos += 4 + $length;
4663
4664 3
        if (!$this->readDataOnly) {
4665
            // offset: 0; size: 8; cell range address of all cells containing this hyperlink
4666
            try {
4667 3
                $cellRange = $this->readBIFF8CellRangeAddressFixed($recordData);
4668
            } catch (PhpSpreadsheetException $e) {
4669
                return;
4670
            }
4671
4672
            // offset: 8, size: 16; GUID of StdLink
4673
4674
            // offset: 24, size: 4; unknown value
4675
4676
            // offset: 28, size: 4; option flags
4677
            // bit: 0; mask: 0x00000001; 0 = no link or extant, 1 = file link or URL
4678 3
            $isFileLinkOrUrl = (0x00000001 & self::getUInt2d($recordData, 28)) >> 0;
4679
4680
            // bit: 1; mask: 0x00000002; 0 = relative path, 1 = absolute path or URL
4681 3
            $isAbsPathOrUrl = (0x00000001 & self::getUInt2d($recordData, 28)) >> 1;
4682
4683
            // bit: 2 (and 4); mask: 0x00000014; 0 = no description
4684 3
            $hasDesc = (0x00000014 & self::getUInt2d($recordData, 28)) >> 2;
4685
4686
            // bit: 3; mask: 0x00000008; 0 = no text, 1 = has text
4687 3
            $hasText = (0x00000008 & self::getUInt2d($recordData, 28)) >> 3;
4688
4689
            // bit: 7; mask: 0x00000080; 0 = no target frame, 1 = has target frame
4690 3
            $hasFrame = (0x00000080 & self::getUInt2d($recordData, 28)) >> 7;
4691
4692
            // bit: 8; mask: 0x00000100; 0 = file link or URL, 1 = UNC path (inc. server name)
4693 3
            $isUNC = (0x00000100 & self::getUInt2d($recordData, 28)) >> 8;
4694
4695
            // offset within record data
4696 3
            $offset = 32;
4697
4698 3
            if ($hasDesc) {
4699
                // offset: 32; size: var; character count of description text
4700 2
                $dl = self::getInt4d($recordData, 32);
4701
                // offset: 36; size: var; character array of description text, no Unicode string header, always 16-bit characters, zero terminated
4702 2
                $desc = self::encodeUTF16(substr($recordData, 36, 2 * ($dl - 1)), false);
4703 2
                $offset += 4 + 2 * $dl;
4704
            }
4705 3
            if ($hasFrame) {
4706
                $fl = self::getInt4d($recordData, $offset);
4707
                $offset += 4 + 2 * $fl;
4708
            }
4709
4710
            // detect type of hyperlink (there are 4 types)
4711 3
            $hyperlinkType = null;
4712
4713 3
            if ($isUNC) {
4714
                $hyperlinkType = 'UNC';
4715 3
            } elseif (!$isFileLinkOrUrl) {
4716 3
                $hyperlinkType = 'workbook';
4717 3
            } elseif (ord($recordData[$offset]) == 0x03) {
4718
                $hyperlinkType = 'local';
4719 3
            } elseif (ord($recordData[$offset]) == 0xE0) {
4720 3
                $hyperlinkType = 'URL';
4721
            }
4722
4723
            switch ($hyperlinkType) {
4724 3
                case 'URL':
4725
                    // section 5.58.2: Hyperlink containing a URL
4726
                    // e.g. http://example.org/index.php
4727
4728
                    // offset: var; size: 16; GUID of URL Moniker
4729 3
                    $offset += 16;
4730
                    // offset: var; size: 4; size (in bytes) of character array of the URL including trailing zero word
4731 3
                    $us = self::getInt4d($recordData, $offset);
4732 3
                    $offset += 4;
4733
                    // offset: var; size: $us; character array of the URL, no Unicode string header, always 16-bit characters, zero-terminated
4734 3
                    $url = self::encodeUTF16(substr($recordData, $offset, $us - 2), false);
4735 3
                    $nullOffset = strpos($url, chr(0x00));
4736 3
                    if ($nullOffset) {
4737 2
                        $url = substr($url, 0, $nullOffset);
4738
                    }
4739 3
                    $url .= $hasText ? '#' : '';
4740 3
                    $offset += $us;
4741
4742 3
                    break;
4743 3
                case 'local':
4744
                    // section 5.58.3: Hyperlink to local file
4745
                    // examples:
4746
                    //   mydoc.txt
4747
                    //   ../../somedoc.xls#Sheet!A1
4748
4749
                    // offset: var; size: 16; GUI of File Moniker
4750
                    $offset += 16;
4751
4752
                    // offset: var; size: 2; directory up-level count.
4753
                    $upLevelCount = self::getUInt2d($recordData, $offset);
4754
                    $offset += 2;
4755
4756
                    // offset: var; size: 4; character count of the shortened file path and name, including trailing zero word
4757
                    $sl = self::getInt4d($recordData, $offset);
4758
                    $offset += 4;
4759
4760
                    // offset: var; size: sl; character array of the shortened file path and name in 8.3-DOS-format (compressed Unicode string)
4761
                    $shortenedFilePath = substr($recordData, $offset, $sl);
4762
                    $shortenedFilePath = self::encodeUTF16($shortenedFilePath, true);
4763
                    $shortenedFilePath = substr($shortenedFilePath, 0, -1); // remove trailing zero
4764
4765
                    $offset += $sl;
4766
4767
                    // offset: var; size: 24; unknown sequence
4768
                    $offset += 24;
4769
4770
                    // extended file path
4771
                    // offset: var; size: 4; size of the following file link field including string lenth mark
4772
                    $sz = self::getInt4d($recordData, $offset);
4773
                    $offset += 4;
4774
4775
                    // only present if $sz > 0
4776
                    if ($sz > 0) {
4777
                        // offset: var; size: 4; size of the character array of the extended file path and name
4778
                        $xl = self::getInt4d($recordData, $offset);
4779
                        $offset += 4;
4780
4781
                        // offset: var; size 2; unknown
4782
                        $offset += 2;
4783
4784
                        // offset: var; size $xl; character array of the extended file path and name.
4785
                        $extendedFilePath = substr($recordData, $offset, $xl);
4786
                        $extendedFilePath = self::encodeUTF16($extendedFilePath, false);
4787
                        $offset += $xl;
4788
                    }
4789
4790
                    // construct the path
4791
                    $url = str_repeat('..\\', $upLevelCount);
4792
                    $url .= ($sz > 0) ? $extendedFilePath : $shortenedFilePath; // use extended path if available
4793
                    $url .= $hasText ? '#' : '';
4794
4795
                    break;
4796 3
                case 'UNC':
4797
                    // section 5.58.4: Hyperlink to a File with UNC (Universal Naming Convention) Path
4798
                    // todo: implement
4799
                    return;
4800 3
                case 'workbook':
4801
                    // section 5.58.5: Hyperlink to the Current Workbook
4802
                    // e.g. Sheet2!B1:C2, stored in text mark field
4803 3
                    $url = 'sheet://';
4804
4805 3
                    break;
4806
                default:
4807
                    return;
4808
            }
4809
4810 3
            if ($hasText) {
4811
                // offset: var; size: 4; character count of text mark including trailing zero word
4812 3
                $tl = self::getInt4d($recordData, $offset);
4813 3
                $offset += 4;
4814
                // offset: var; size: var; character array of the text mark without the # sign, no Unicode header, always 16-bit characters, zero-terminated
4815 3
                $text = self::encodeUTF16(substr($recordData, $offset, 2 * ($tl - 1)), false);
4816 3
                $url .= $text;
4817
            }
4818
4819
            // apply the hyperlink to all the relevant cells
4820 3
            foreach (Coordinate::extractAllCellReferencesInRange($cellRange) as $coordinate) {
4821 3
                $this->phpSheet->getCell($coordinate)->getHyperLink()->setUrl($url);
4822
            }
4823
        }
4824 3
    }
4825
4826
    /**
4827
     * Read DATAVALIDATIONS record.
4828
     */
4829
    private function readDataValidations(): void
4830
    {
4831
        $length = self::getUInt2d($this->data, $this->pos + 2);
4832
        $recordData = $this->readRecordData($this->data, $this->pos + 4, $length);
4833
4834
        // move stream pointer forward to next record
4835
        $this->pos += 4 + $length;
4836
    }
4837
4838
    /**
4839
     * Read DATAVALIDATION record.
4840
     */
4841
    private function readDataValidation(): void
4842
    {
4843
        $length = self::getUInt2d($this->data, $this->pos + 2);
4844
        $recordData = $this->readRecordData($this->data, $this->pos + 4, $length);
4845
4846
        // move stream pointer forward to next record
4847
        $this->pos += 4 + $length;
4848
4849
        if ($this->readDataOnly) {
4850
            return;
4851
        }
4852
4853
        // offset: 0; size: 4; Options
4854
        $options = self::getInt4d($recordData, 0);
4855
4856
        // bit: 0-3; mask: 0x0000000F; type
4857
        $type = (0x0000000F & $options) >> 0;
4858
        switch ($type) {
4859
            case 0x00:
4860
                $type = DataValidation::TYPE_NONE;
4861
4862
                break;
4863
            case 0x01:
4864
                $type = DataValidation::TYPE_WHOLE;
4865
4866
                break;
4867
            case 0x02:
4868
                $type = DataValidation::TYPE_DECIMAL;
4869
4870
                break;
4871
            case 0x03:
4872
                $type = DataValidation::TYPE_LIST;
4873
4874
                break;
4875
            case 0x04:
4876
                $type = DataValidation::TYPE_DATE;
4877
4878
                break;
4879
            case 0x05:
4880
                $type = DataValidation::TYPE_TIME;
4881
4882
                break;
4883
            case 0x06:
4884
                $type = DataValidation::TYPE_TEXTLENGTH;
4885
4886
                break;
4887
            case 0x07:
4888
                $type = DataValidation::TYPE_CUSTOM;
4889
4890
                break;
4891
        }
4892
4893
        // bit: 4-6; mask: 0x00000070; error type
4894
        $errorStyle = (0x00000070 & $options) >> 4;
4895
        switch ($errorStyle) {
4896
            case 0x00:
4897
                $errorStyle = DataValidation::STYLE_STOP;
4898
4899
                break;
4900
            case 0x01:
4901
                $errorStyle = DataValidation::STYLE_WARNING;
4902
4903
                break;
4904
            case 0x02:
4905
                $errorStyle = DataValidation::STYLE_INFORMATION;
4906
4907
                break;
4908
        }
4909
4910
        // bit: 7; mask: 0x00000080; 1= formula is explicit (only applies to list)
4911
        // I have only seen cases where this is 1
4912
        $explicitFormula = (0x00000080 & $options) >> 7;
4913
4914
        // bit: 8; mask: 0x00000100; 1= empty cells allowed
4915
        $allowBlank = (0x00000100 & $options) >> 8;
4916
4917
        // bit: 9; mask: 0x00000200; 1= suppress drop down arrow in list type validity
4918
        $suppressDropDown = (0x00000200 & $options) >> 9;
4919
4920
        // bit: 18; mask: 0x00040000; 1= show prompt box if cell selected
4921
        $showInputMessage = (0x00040000 & $options) >> 18;
4922
4923
        // bit: 19; mask: 0x00080000; 1= show error box if invalid values entered
4924
        $showErrorMessage = (0x00080000 & $options) >> 19;
4925
4926
        // bit: 20-23; mask: 0x00F00000; condition operator
4927
        $operator = (0x00F00000 & $options) >> 20;
4928
        switch ($operator) {
4929
            case 0x00:
4930
                $operator = DataValidation::OPERATOR_BETWEEN;
4931
4932
                break;
4933
            case 0x01:
4934
                $operator = DataValidation::OPERATOR_NOTBETWEEN;
4935
4936
                break;
4937
            case 0x02:
4938
                $operator = DataValidation::OPERATOR_EQUAL;
4939
4940
                break;
4941
            case 0x03:
4942
                $operator = DataValidation::OPERATOR_NOTEQUAL;
4943
4944
                break;
4945
            case 0x04:
4946
                $operator = DataValidation::OPERATOR_GREATERTHAN;
4947
4948
                break;
4949
            case 0x05:
4950
                $operator = DataValidation::OPERATOR_LESSTHAN;
4951
4952
                break;
4953
            case 0x06:
4954
                $operator = DataValidation::OPERATOR_GREATERTHANOREQUAL;
4955
4956
                break;
4957
            case 0x07:
4958
                $operator = DataValidation::OPERATOR_LESSTHANOREQUAL;
4959
4960
                break;
4961
        }
4962
4963
        // offset: 4; size: var; title of the prompt box
4964
        $offset = 4;
4965
        $string = self::readUnicodeStringLong(substr($recordData, $offset));
4966
        $promptTitle = $string['value'] !== chr(0) ? $string['value'] : '';
4967
        $offset += $string['size'];
4968
4969
        // offset: var; size: var; title of the error box
4970
        $string = self::readUnicodeStringLong(substr($recordData, $offset));
4971
        $errorTitle = $string['value'] !== chr(0) ? $string['value'] : '';
4972
        $offset += $string['size'];
4973
4974
        // offset: var; size: var; text of the prompt box
4975
        $string = self::readUnicodeStringLong(substr($recordData, $offset));
4976
        $prompt = $string['value'] !== chr(0) ? $string['value'] : '';
4977
        $offset += $string['size'];
4978
4979
        // offset: var; size: var; text of the error box
4980
        $string = self::readUnicodeStringLong(substr($recordData, $offset));
4981
        $error = $string['value'] !== chr(0) ? $string['value'] : '';
4982
        $offset += $string['size'];
4983
4984
        // offset: var; size: 2; size of the formula data for the first condition
4985
        $sz1 = self::getUInt2d($recordData, $offset);
4986
        $offset += 2;
4987
4988
        // offset: var; size: 2; not used
4989
        $offset += 2;
4990
4991
        // offset: var; size: $sz1; formula data for first condition (without size field)
4992
        $formula1 = substr($recordData, $offset, $sz1);
4993
        $formula1 = pack('v', $sz1) . $formula1; // prepend the length
4994
4995
        try {
4996
            $formula1 = $this->getFormulaFromStructure($formula1);
4997
4998
            // in list type validity, null characters are used as item separators
4999
            if ($type == DataValidation::TYPE_LIST) {
5000
                $formula1 = str_replace(chr(0), ',', $formula1);
5001
            }
5002
        } catch (PhpSpreadsheetException $e) {
5003
            return;
5004
        }
5005
        $offset += $sz1;
5006
5007
        // offset: var; size: 2; size of the formula data for the first condition
5008
        $sz2 = self::getUInt2d($recordData, $offset);
5009
        $offset += 2;
5010
5011
        // offset: var; size: 2; not used
5012
        $offset += 2;
5013
5014
        // offset: var; size: $sz2; formula data for second condition (without size field)
5015
        $formula2 = substr($recordData, $offset, $sz2);
5016
        $formula2 = pack('v', $sz2) . $formula2; // prepend the length
5017
5018
        try {
5019
            $formula2 = $this->getFormulaFromStructure($formula2);
5020
        } catch (PhpSpreadsheetException $e) {
5021
            return;
5022
        }
5023
        $offset += $sz2;
5024
5025
        // offset: var; size: var; cell range address list with
5026
        $cellRangeAddressList = $this->readBIFF8CellRangeAddressList(substr($recordData, $offset));
5027
        $cellRangeAddresses = $cellRangeAddressList['cellRangeAddresses'];
5028
5029
        foreach ($cellRangeAddresses as $cellRange) {
5030
            $stRange = $this->phpSheet->shrinkRangeToFit($cellRange);
5031
            foreach (Coordinate::extractAllCellReferencesInRange($stRange) as $coordinate) {
5032
                $objValidation = $this->phpSheet->getCell($coordinate)->getDataValidation();
5033
                $objValidation->setType($type);
5034
                $objValidation->setErrorStyle($errorStyle);
5035
                $objValidation->setAllowBlank((bool) $allowBlank);
5036
                $objValidation->setShowInputMessage((bool) $showInputMessage);
5037
                $objValidation->setShowErrorMessage((bool) $showErrorMessage);
5038
                $objValidation->setShowDropDown(!$suppressDropDown);
5039
                $objValidation->setOperator($operator);
5040
                $objValidation->setErrorTitle($errorTitle);
5041
                $objValidation->setError($error);
5042
                $objValidation->setPromptTitle($promptTitle);
5043
                $objValidation->setPrompt($prompt);
5044
                $objValidation->setFormula1($formula1);
5045
                $objValidation->setFormula2($formula2);
5046
            }
5047
        }
5048
    }
5049
5050
    /**
5051
     * Read SHEETLAYOUT record. Stores sheet tab color information.
5052
     */
5053 3
    private function readSheetLayout(): void
5054
    {
5055 3
        $length = self::getUInt2d($this->data, $this->pos + 2);
5056 3
        $recordData = $this->readRecordData($this->data, $this->pos + 4, $length);
5057
5058
        // move stream pointer to next record
5059 3
        $this->pos += 4 + $length;
5060
5061
        // local pointer in record data
5062 3
        $offset = 0;
5063
5064 3
        if (!$this->readDataOnly) {
5065
            // offset: 0; size: 2; repeated record identifier 0x0862
5066
5067
            // offset: 2; size: 10; not used
5068
5069
            // offset: 12; size: 4; size of record data
5070
            // Excel 2003 uses size of 0x14 (documented), Excel 2007 uses size of 0x28 (not documented?)
5071 3
            $sz = self::getInt4d($recordData, 12);
5072
5073
            switch ($sz) {
5074 3
                case 0x14:
5075
                    // offset: 16; size: 2; color index for sheet tab
5076 1
                    $colorIndex = self::getUInt2d($recordData, 16);
5077 1
                    $color = Xls\Color::map($colorIndex, $this->palette, $this->version);
5078 1
                    $this->phpSheet->getTabColor()->setRGB($color['rgb']);
5079
5080 1
                    break;
5081 2
                case 0x28:
5082
                    // TODO: Investigate structure for .xls SHEETLAYOUT record as saved by MS Office Excel 2007
5083 2
                    return;
5084
5085
                    break;
0 ignored issues
show
Unused Code introduced by
break is not strictly necessary here and could be removed.

The break statement is not necessary if it is preceded for example by a return statement:

switch ($x) {
    case 1:
        return 'foo';
        break; // This break is not necessary and can be left off.
}

If you would like to keep this construct to be consistent with other case statements, you can safely mark this issue as a false-positive.

Loading history...
5086
            }
5087
        }
5088 1
    }
5089
5090
    /**
5091
     * Read SHEETPROTECTION record (FEATHEADR).
5092
     */
5093 22
    private function readSheetProtection(): void
5094
    {
5095 22
        $length = self::getUInt2d($this->data, $this->pos + 2);
5096 22
        $recordData = $this->readRecordData($this->data, $this->pos + 4, $length);
5097
5098
        // move stream pointer to next record
5099 22
        $this->pos += 4 + $length;
5100
5101 22
        if ($this->readDataOnly) {
5102
            return;
5103
        }
5104
5105
        // offset: 0; size: 2; repeated record header
5106
5107
        // offset: 2; size: 2; FRT cell reference flag (=0 currently)
5108
5109
        // offset: 4; size: 8; Currently not used and set to 0
5110
5111
        // offset: 12; size: 2; Shared feature type index (2=Enhanced Protetion, 4=SmartTag)
5112 22
        $isf = self::getUInt2d($recordData, 12);
5113 22
        if ($isf != 2) {
5114
            return;
5115
        }
5116
5117
        // offset: 14; size: 1; =1 since this is a feat header
5118
5119
        // offset: 15; size: 4; size of rgbHdrSData
5120
5121
        // rgbHdrSData, assume "Enhanced Protection"
5122
        // offset: 19; size: 2; option flags
5123 22
        $options = self::getUInt2d($recordData, 19);
5124
5125
        // bit: 0; mask 0x0001; 1 = user may edit objects, 0 = users must not edit objects
5126 22
        $bool = (0x0001 & $options) >> 0;
5127 22
        $this->phpSheet->getProtection()->setObjects(!$bool);
5128
5129
        // bit: 1; mask 0x0002; edit scenarios
5130 22
        $bool = (0x0002 & $options) >> 1;
5131 22
        $this->phpSheet->getProtection()->setScenarios(!$bool);
5132
5133
        // bit: 2; mask 0x0004; format cells
5134 22
        $bool = (0x0004 & $options) >> 2;
5135 22
        $this->phpSheet->getProtection()->setFormatCells(!$bool);
5136
5137
        // bit: 3; mask 0x0008; format columns
5138 22
        $bool = (0x0008 & $options) >> 3;
5139 22
        $this->phpSheet->getProtection()->setFormatColumns(!$bool);
5140
5141
        // bit: 4; mask 0x0010; format rows
5142 22
        $bool = (0x0010 & $options) >> 4;
5143 22
        $this->phpSheet->getProtection()->setFormatRows(!$bool);
5144
5145
        // bit: 5; mask 0x0020; insert columns
5146 22
        $bool = (0x0020 & $options) >> 5;
5147 22
        $this->phpSheet->getProtection()->setInsertColumns(!$bool);
5148
5149
        // bit: 6; mask 0x0040; insert rows
5150 22
        $bool = (0x0040 & $options) >> 6;
5151 22
        $this->phpSheet->getProtection()->setInsertRows(!$bool);
5152
5153
        // bit: 7; mask 0x0080; insert hyperlinks
5154 22
        $bool = (0x0080 & $options) >> 7;
5155 22
        $this->phpSheet->getProtection()->setInsertHyperlinks(!$bool);
5156
5157
        // bit: 8; mask 0x0100; delete columns
5158 22
        $bool = (0x0100 & $options) >> 8;
5159 22
        $this->phpSheet->getProtection()->setDeleteColumns(!$bool);
5160
5161
        // bit: 9; mask 0x0200; delete rows
5162 22
        $bool = (0x0200 & $options) >> 9;
5163 22
        $this->phpSheet->getProtection()->setDeleteRows(!$bool);
5164
5165
        // bit: 10; mask 0x0400; select locked cells
5166 22
        $bool = (0x0400 & $options) >> 10;
5167 22
        $this->phpSheet->getProtection()->setSelectLockedCells(!$bool);
5168
5169
        // bit: 11; mask 0x0800; sort cell range
5170 22
        $bool = (0x0800 & $options) >> 11;
5171 22
        $this->phpSheet->getProtection()->setSort(!$bool);
5172
5173
        // bit: 12; mask 0x1000; auto filter
5174 22
        $bool = (0x1000 & $options) >> 12;
5175 22
        $this->phpSheet->getProtection()->setAutoFilter(!$bool);
5176
5177
        // bit: 13; mask 0x2000; pivot tables
5178 22
        $bool = (0x2000 & $options) >> 13;
5179 22
        $this->phpSheet->getProtection()->setPivotTables(!$bool);
5180
5181
        // bit: 14; mask 0x4000; select unlocked cells
5182 22
        $bool = (0x4000 & $options) >> 14;
5183 22
        $this->phpSheet->getProtection()->setSelectUnlockedCells(!$bool);
5184
5185
        // offset: 21; size: 2; not used
5186 22
    }
5187
5188
    /**
5189
     * Read RANGEPROTECTION record
5190
     * Reading of this record is based on Microsoft Office Excel 97-2000 Binary File Format Specification,
5191
     * where it is referred to as FEAT record.
5192
     */
5193 1
    private function readRangeProtection(): void
5194
    {
5195 1
        $length = self::getUInt2d($this->data, $this->pos + 2);
5196 1
        $recordData = $this->readRecordData($this->data, $this->pos + 4, $length);
5197
5198
        // move stream pointer to next record
5199 1
        $this->pos += 4 + $length;
5200
5201
        // local pointer in record data
5202 1
        $offset = 0;
5203
5204 1
        if (!$this->readDataOnly) {
5205 1
            $offset += 12;
5206
5207
            // offset: 12; size: 2; shared feature type, 2 = enhanced protection, 4 = smart tag
5208 1
            $isf = self::getUInt2d($recordData, 12);
5209 1
            if ($isf != 2) {
5210
                // we only read FEAT records of type 2
5211
                return;
5212
            }
5213 1
            $offset += 2;
5214
5215 1
            $offset += 5;
5216
5217
            // offset: 19; size: 2; count of ref ranges this feature is on
5218 1
            $cref = self::getUInt2d($recordData, 19);
5219 1
            $offset += 2;
5220
5221 1
            $offset += 6;
5222
5223
            // offset: 27; size: 8 * $cref; list of cell ranges (like in hyperlink record)
5224 1
            $cellRanges = [];
5225 1
            for ($i = 0; $i < $cref; ++$i) {
5226
                try {
5227 1
                    $cellRange = $this->readBIFF8CellRangeAddressFixed(substr($recordData, 27 + 8 * $i, 8));
5228
                } catch (PhpSpreadsheetException $e) {
5229
                    return;
5230
                }
5231 1
                $cellRanges[] = $cellRange;
5232 1
                $offset += 8;
5233
            }
5234
5235
            // offset: var; size: var; variable length of feature specific data
5236 1
            $rgbFeat = substr($recordData, $offset);
5237 1
            $offset += 4;
5238
5239
            // offset: var; size: 4; the encrypted password (only 16-bit although field is 32-bit)
5240 1
            $wPassword = self::getInt4d($recordData, $offset);
5241 1
            $offset += 4;
5242
5243
            // Apply range protection to sheet
5244 1
            if ($cellRanges) {
5245 1
                $this->phpSheet->protectCells(implode(' ', $cellRanges), strtoupper(dechex($wPassword)), true);
5246
            }
5247
        }
5248 1
    }
5249
5250
    /**
5251
     * Read a free CONTINUE record. Free CONTINUE record may be a camouflaged MSODRAWING record
5252
     * When MSODRAWING data on a sheet exceeds 8224 bytes, CONTINUE records are used instead. Undocumented.
5253
     * In this case, we must treat the CONTINUE record as a MSODRAWING record.
5254
     */
5255 1
    private function readContinue(): void
5256
    {
5257 1
        $length = self::getUInt2d($this->data, $this->pos + 2);
5258 1
        $recordData = $this->readRecordData($this->data, $this->pos + 4, $length);
5259
5260
        // check if we are reading drawing data
5261
        // this is in case a free CONTINUE record occurs in other circumstances we are unaware of
5262 1
        if ($this->drawingData == '') {
5263
            // move stream pointer to next record
5264 1
            $this->pos += 4 + $length;
5265
5266 1
            return;
5267
        }
5268
5269
        // check if record data is at least 4 bytes long, otherwise there is no chance this is MSODRAWING data
5270
        if ($length < 4) {
5271
            // move stream pointer to next record
5272
            $this->pos += 4 + $length;
5273
5274
            return;
5275
        }
5276
5277
        // dirty check to see if CONTINUE record could be a camouflaged MSODRAWING record
5278
        // look inside CONTINUE record to see if it looks like a part of an Escher stream
5279
        // we know that Escher stream may be split at least at
5280
        //        0xF003 MsofbtSpgrContainer
5281
        //        0xF004 MsofbtSpContainer
5282
        //        0xF00D MsofbtClientTextbox
5283
        $validSplitPoints = [0xF003, 0xF004, 0xF00D]; // add identifiers if we find more
5284
5285
        $splitPoint = self::getUInt2d($recordData, 2);
5286
        if (in_array($splitPoint, $validSplitPoints)) {
5287
            // get spliced record data (and move pointer to next record)
5288
            $splicedRecordData = $this->getSplicedRecordData();
5289
            $this->drawingData .= $splicedRecordData['recordData'];
5290
5291
            return;
5292
        }
5293
5294
        // move stream pointer to next record
5295
        $this->pos += 4 + $length;
5296
    }
5297
5298
    /**
5299
     * Reads a record from current position in data stream and continues reading data as long as CONTINUE
5300
     * records are found. Splices the record data pieces and returns the combined string as if record data
5301
     * is in one piece.
5302
     * Moves to next current position in data stream to start of next record different from a CONtINUE record.
5303
     *
5304
     * @return array
5305
     */
5306 37
    private function getSplicedRecordData()
5307
    {
5308 37
        $data = '';
5309 37
        $spliceOffsets = [];
5310
5311 37
        $i = 0;
5312 37
        $spliceOffsets[0] = 0;
5313
5314
        do {
5315 37
            ++$i;
5316
5317
            // offset: 0; size: 2; identifier
5318 37
            $identifier = self::getUInt2d($this->data, $this->pos);
5319
            // offset: 2; size: 2; length
5320 37
            $length = self::getUInt2d($this->data, $this->pos + 2);
5321 37
            $data .= $this->readRecordData($this->data, $this->pos + 4, $length);
5322
5323 37
            $spliceOffsets[$i] = $spliceOffsets[$i - 1] + $length;
5324
5325 37
            $this->pos += 4 + $length;
5326 37
            $nextIdentifier = self::getUInt2d($this->data, $this->pos);
5327 37
        } while ($nextIdentifier == self::XLS_TYPE_CONTINUE);
5328
5329
        return [
5330 37
            'recordData' => $data,
5331 37
            'spliceOffsets' => $spliceOffsets,
5332
        ];
5333
    }
5334
5335
    /**
5336
     * Convert formula structure into human readable Excel formula like 'A3+A5*5'.
5337
     *
5338
     * @param string $formulaStructure The complete binary data for the formula
5339
     * @param string $baseCell Base cell, only needed when formula contains tRefN tokens, e.g. with shared formulas
5340
     *
5341
     * @return string Human readable formula
5342
     */
5343 18
    private function getFormulaFromStructure($formulaStructure, $baseCell = 'A1')
5344
    {
5345
        // offset: 0; size: 2; size of the following formula data
5346 18
        $sz = self::getUInt2d($formulaStructure, 0);
5347
5348
        // offset: 2; size: sz
5349 18
        $formulaData = substr($formulaStructure, 2, $sz);
5350
5351
        // offset: 2 + sz; size: variable (optional)
5352 18
        if (strlen($formulaStructure) > 2 + $sz) {
5353
            $additionalData = substr($formulaStructure, 2 + $sz);
5354
        } else {
5355 18
            $additionalData = '';
5356
        }
5357
5358 18
        return $this->getFormulaFromData($formulaData, $additionalData, $baseCell);
5359
    }
5360
5361
    /**
5362
     * Take formula data and additional data for formula and return human readable formula.
5363
     *
5364
     * @param string $formulaData The binary data for the formula itself
5365
     * @param string $additionalData Additional binary data going with the formula
5366
     * @param string $baseCell Base cell, only needed when formula contains tRefN tokens, e.g. with shared formulas
5367
     *
5368
     * @return string Human readable formula
5369
     */
5370 18
    private function getFormulaFromData($formulaData, $additionalData = '', $baseCell = 'A1')
5371
    {
5372
        // start parsing the formula data
5373 18
        $tokens = [];
5374
5375 18
        while (strlen($formulaData) > 0 && $token = $this->getNextToken($formulaData, $baseCell)) {
5376 18
            $tokens[] = $token;
5377 18
            $formulaData = substr($formulaData, $token['size']);
5378
        }
5379
5380 18
        $formulaString = $this->createFormulaFromTokens($tokens, $additionalData);
5381
5382 18
        return $formulaString;
5383
    }
5384
5385
    /**
5386
     * Take array of tokens together with additional data for formula and return human readable formula.
5387
     *
5388
     * @param array $tokens
5389
     * @param string $additionalData Additional binary data going with the formula
5390
     *
5391
     * @return string Human readable formula
5392
     */
5393 18
    private function createFormulaFromTokens($tokens, $additionalData)
5394
    {
5395
        // empty formula?
5396 18
        if (empty($tokens)) {
5397
            return '';
5398
        }
5399
5400 18
        $formulaStrings = [];
5401 18
        foreach ($tokens as $token) {
5402
            // initialize spaces
5403 18
            $space0 = $space0 ?? ''; // spaces before next token, not tParen
5404 18
            $space1 = $space1 ?? ''; // carriage returns before next token, not tParen
5405 18
            $space2 = $space2 ?? ''; // spaces before opening parenthesis
5406 18
            $space3 = $space3 ?? ''; // carriage returns before opening parenthesis
5407 18
            $space4 = $space4 ?? ''; // spaces before closing parenthesis
5408 18
            $space5 = $space5 ?? ''; // carriage returns before closing parenthesis
5409
5410 18
            switch ($token['name']) {
5411 18
                case 'tAdd': // addition
5412 18
                case 'tConcat': // addition
5413 18
                case 'tDiv': // division
5414 18
                case 'tEQ': // equality
5415 18
                case 'tGE': // greater than or equal
5416 18
                case 'tGT': // greater than
5417 18
                case 'tIsect': // intersection
5418 18
                case 'tLE': // less than or equal
5419 18
                case 'tList': // less than or equal
5420 18
                case 'tLT': // less than
5421 18
                case 'tMul': // multiplication
5422 18
                case 'tNE': // multiplication
5423 18
                case 'tPower': // power
5424 18
                case 'tRange': // range
5425 18
                case 'tSub': // subtraction
5426 12
                    $op2 = array_pop($formulaStrings);
5427 12
                    $op1 = array_pop($formulaStrings);
5428 12
                    $formulaStrings[] = "$op1$space1$space0{$token['data']}$op2";
5429 12
                    unset($space0, $space1);
5430
5431 12
                    break;
5432 18
                case 'tUplus': // unary plus
5433 18
                case 'tUminus': // unary minus
5434
                    $op = array_pop($formulaStrings);
5435
                    $formulaStrings[] = "$space1$space0{$token['data']}$op";
5436
                    unset($space0, $space1);
5437
5438
                    break;
5439 18
                case 'tPercent': // percent sign
5440
                    $op = array_pop($formulaStrings);
5441
                    $formulaStrings[] = "$op$space1$space0{$token['data']}";
5442
                    unset($space0, $space1);
5443
5444
                    break;
5445 18
                case 'tAttrVolatile': // indicates volatile function
5446 18
                case 'tAttrIf':
5447 18
                case 'tAttrSkip':
5448 18
                case 'tAttrChoose':
5449
                    // token is only important for Excel formula evaluator
5450
                    // do nothing
5451
                    break;
5452 18
                case 'tAttrSpace': // space / carriage return
5453
                    // space will be used when next token arrives, do not alter formulaString stack
5454
                    switch ($token['data']['spacetype']) {
5455
                        case 'type0':
5456
                            $space0 = str_repeat(' ', $token['data']['spacecount']);
5457
5458
                            break;
5459
                        case 'type1':
5460
                            $space1 = str_repeat("\n", $token['data']['spacecount']);
5461
5462
                            break;
5463
                        case 'type2':
5464
                            $space2 = str_repeat(' ', $token['data']['spacecount']);
5465
5466
                            break;
5467
                        case 'type3':
5468
                            $space3 = str_repeat("\n", $token['data']['spacecount']);
5469
5470
                            break;
5471
                        case 'type4':
5472
                            $space4 = str_repeat(' ', $token['data']['spacecount']);
5473
5474
                            break;
5475
                        case 'type5':
5476
                            $space5 = str_repeat("\n", $token['data']['spacecount']);
5477
5478
                            break;
5479
                    }
5480
5481
                    break;
5482 18
                case 'tAttrSum': // SUM function with one parameter
5483 10
                    $op = array_pop($formulaStrings);
5484 10
                    $formulaStrings[] = "{$space1}{$space0}SUM($op)";
5485 10
                    unset($space0, $space1);
5486
5487 10
                    break;
5488 18
                case 'tFunc': // function with fixed number of arguments
5489 18
                case 'tFuncV': // function with variable number of arguments
5490 14
                    if ($token['data']['function'] != '') {
5491
                        // normal function
5492 14
                        $ops = []; // array of operators
5493 14
                        for ($i = 0; $i < $token['data']['args']; ++$i) {
5494 6
                            $ops[] = array_pop($formulaStrings);
5495
                        }
5496 14
                        $ops = array_reverse($ops);
5497 14
                        $formulaStrings[] = "$space1$space0{$token['data']['function']}(" . implode(',', $ops) . ')';
5498 14
                        unset($space0, $space1);
5499
                    } else {
5500
                        // add-in function
5501
                        $ops = []; // array of operators
5502
                        for ($i = 0; $i < $token['data']['args'] - 1; ++$i) {
5503
                            $ops[] = array_pop($formulaStrings);
5504
                        }
5505
                        $ops = array_reverse($ops);
5506
                        $function = array_pop($formulaStrings);
5507
                        $formulaStrings[] = "$space1$space0$function(" . implode(',', $ops) . ')';
5508
                        unset($space0, $space1);
5509
                    }
5510
5511 14
                    break;
5512 18
                case 'tParen': // parenthesis
5513
                    $expression = array_pop($formulaStrings);
5514
                    $formulaStrings[] = "$space3$space2($expression$space5$space4)";
5515
                    unset($space2, $space3, $space4, $space5);
5516
5517
                    break;
5518 18
                case 'tArray': // array constant
5519
                    $constantArray = self::readBIFF8ConstantArray($additionalData);
5520
                    $formulaStrings[] = $space1 . $space0 . $constantArray['value'];
5521
                    $additionalData = substr($additionalData, $constantArray['size']); // bite of chunk of additional data
5522
                    unset($space0, $space1);
5523
5524
                    break;
5525 18
                case 'tMemArea':
5526
                    // bite off chunk of additional data
5527
                    $cellRangeAddressList = $this->readBIFF8CellRangeAddressList($additionalData);
5528
                    $additionalData = substr($additionalData, $cellRangeAddressList['size']);
5529
                    $formulaStrings[] = "$space1$space0{$token['data']}";
5530
                    unset($space0, $space1);
5531
5532
                    break;
5533 18
                case 'tArea': // cell range address
5534 16
                case 'tBool': // boolean
5535 16
                case 'tErr': // error code
5536 16
                case 'tInt': // integer
5537 7
                case 'tMemErr':
5538 7
                case 'tMemFunc':
5539 7
                case 'tMissArg':
5540 7
                case 'tName':
5541 7
                case 'tNameX':
5542 7
                case 'tNum': // number
5543 7
                case 'tRef': // single cell reference
5544 7
                case 'tRef3d': // 3d cell reference
5545 6
                case 'tArea3d': // 3d cell range reference
5546 1
                case 'tRefN':
5547 1
                case 'tAreaN':
5548 1
                case 'tStr': // string
5549 18
                    $formulaStrings[] = "$space1$space0{$token['data']}";
5550 18
                    unset($space0, $space1);
5551
5552 18
                    break;
5553
            }
5554
        }
5555 18
        $formulaString = $formulaStrings[0];
5556
5557 18
        return $formulaString;
5558
    }
5559
5560
    /**
5561
     * Fetch next token from binary formula data.
5562
     *
5563
     * @param string $formulaData Formula data
5564
     * @param string $baseCell Base cell, only needed when formula contains tRefN tokens, e.g. with shared formulas
5565
     *
5566
     * @return array
5567
     */
5568 18
    private function getNextToken($formulaData, $baseCell = 'A1')
5569
    {
5570
        // offset: 0; size: 1; token id
5571 18
        $id = ord($formulaData[0]); // token id
5572 18
        $name = false; // initialize token name
5573
5574
        switch ($id) {
5575 18
            case 0x03:
5576 1
                $name = 'tAdd';
5577 1
                $size = 1;
5578 1
                $data = '+';
5579
5580 1
                break;
5581 18
            case 0x04:
5582
                $name = 'tSub';
5583
                $size = 1;
5584
                $data = '-';
5585
5586
                break;
5587 18
            case 0x05:
5588 3
                $name = 'tMul';
5589 3
                $size = 1;
5590 3
                $data = '*';
5591
5592 3
                break;
5593 18
            case 0x06:
5594 8
                $name = 'tDiv';
5595 8
                $size = 1;
5596 8
                $data = '/';
5597
5598 8
                break;
5599 18
            case 0x07:
5600
                $name = 'tPower';
5601
                $size = 1;
5602
                $data = '^';
5603
5604
                break;
5605 18
            case 0x08:
5606
                $name = 'tConcat';
5607
                $size = 1;
5608
                $data = '&';
5609
5610
                break;
5611 18
            case 0x09:
5612
                $name = 'tLT';
5613
                $size = 1;
5614
                $data = '<';
5615
5616
                break;
5617 18
            case 0x0A:
5618
                $name = 'tLE';
5619
                $size = 1;
5620
                $data = '<=';
5621
5622
                break;
5623 18
            case 0x0B:
5624
                $name = 'tEQ';
5625
                $size = 1;
5626
                $data = '=';
5627
5628
                break;
5629 18
            case 0x0C:
5630
                $name = 'tGE';
5631
                $size = 1;
5632
                $data = '>=';
5633
5634
                break;
5635 18
            case 0x0D:
5636
                $name = 'tGT';
5637
                $size = 1;
5638
                $data = '>';
5639
5640
                break;
5641 18
            case 0x0E:
5642 1
                $name = 'tNE';
5643 1
                $size = 1;
5644 1
                $data = '<>';
5645
5646 1
                break;
5647 18
            case 0x0F:
5648
                $name = 'tIsect';
5649
                $size = 1;
5650
                $data = ' ';
5651
5652
                break;
5653 18
            case 0x10:
5654 1
                $name = 'tList';
5655 1
                $size = 1;
5656 1
                $data = ',';
5657
5658 1
                break;
5659 18
            case 0x11:
5660
                $name = 'tRange';
5661
                $size = 1;
5662
                $data = ':';
5663
5664
                break;
5665 18
            case 0x12:
5666
                $name = 'tUplus';
5667
                $size = 1;
5668
                $data = '+';
5669
5670
                break;
5671 18
            case 0x13:
5672
                $name = 'tUminus';
5673
                $size = 1;
5674
                $data = '-';
5675
5676
                break;
5677 18
            case 0x14:
5678
                $name = 'tPercent';
5679
                $size = 1;
5680
                $data = '%';
5681
5682
                break;
5683 18
            case 0x15:    //    parenthesis
5684
                $name = 'tParen';
5685
                $size = 1;
5686
                $data = null;
5687
5688
                break;
5689 18
            case 0x16:    //    missing argument
5690
                $name = 'tMissArg';
5691
                $size = 1;
5692
                $data = '';
5693
5694
                break;
5695 18
            case 0x17:    //    string
5696 1
                $name = 'tStr';
5697
                // offset: 1; size: var; Unicode string, 8-bit string length
5698 1
                $string = self::readUnicodeStringShort(substr($formulaData, 1));
5699 1
                $size = 1 + $string['size'];
5700 1
                $data = self::UTF8toExcelDoubleQuoted($string['value']);
5701
5702 1
                break;
5703 18
            case 0x19:    //    Special attribute
5704
                // offset: 1; size: 1; attribute type flags:
5705 10
                switch (ord($formulaData[1])) {
5706 10
                    case 0x01:
5707
                        $name = 'tAttrVolatile';
5708
                        $size = 4;
5709
                        $data = null;
5710
5711
                        break;
5712 10
                    case 0x02:
5713
                        $name = 'tAttrIf';
5714
                        $size = 4;
5715
                        $data = null;
5716
5717
                        break;
5718 10
                    case 0x04:
5719
                        $name = 'tAttrChoose';
5720
                        // offset: 2; size: 2; number of choices in the CHOOSE function ($nc, number of parameters decreased by 1)
5721
                        $nc = self::getUInt2d($formulaData, 2);
5722
                        // offset: 4; size: 2 * $nc
5723
                        // offset: 4 + 2 * $nc; size: 2
5724
                        $size = 2 * $nc + 6;
5725
                        $data = null;
5726
5727
                        break;
5728 10
                    case 0x08:
5729
                        $name = 'tAttrSkip';
5730
                        $size = 4;
5731
                        $data = null;
5732
5733
                        break;
5734 10
                    case 0x10:
5735 10
                        $name = 'tAttrSum';
5736 10
                        $size = 4;
5737 10
                        $data = null;
5738
5739 10
                        break;
5740
                    case 0x40:
5741
                    case 0x41:
5742
                        $name = 'tAttrSpace';
5743
                        $size = 4;
5744
                        // offset: 2; size: 2; space type and position
5745
                        switch (ord($formulaData[2])) {
5746
                            case 0x00:
5747
                                $spacetype = 'type0';
5748
5749
                                break;
5750
                            case 0x01:
5751
                                $spacetype = 'type1';
5752
5753
                                break;
5754
                            case 0x02:
5755
                                $spacetype = 'type2';
5756
5757
                                break;
5758
                            case 0x03:
5759
                                $spacetype = 'type3';
5760
5761
                                break;
5762
                            case 0x04:
5763
                                $spacetype = 'type4';
5764
5765
                                break;
5766
                            case 0x05:
5767
                                $spacetype = 'type5';
5768
5769
                                break;
5770
                            default:
5771
                                throw new Exception('Unrecognized space type in tAttrSpace token');
5772
5773
                                break;
5774
                        }
5775
                        // offset: 3; size: 1; number of inserted spaces/carriage returns
5776
                        $spacecount = ord($formulaData[3]);
5777
5778
                        $data = ['spacetype' => $spacetype, 'spacecount' => $spacecount];
5779
5780
                        break;
5781
                    default:
5782
                        throw new Exception('Unrecognized attribute flag in tAttr token');
5783
5784
                        break;
5785
                }
5786
5787 10
                break;
5788 18
            case 0x1C:    //    error code
5789
                // offset: 1; size: 1; error code
5790
                $name = 'tErr';
5791
                $size = 2;
5792
                $data = Xls\ErrorCode::lookup(ord($formulaData[1]));
5793
5794
                break;
5795 18
            case 0x1D:    //    boolean
5796
                // offset: 1; size: 1; 0 = false, 1 = true;
5797
                $name = 'tBool';
5798
                $size = 2;
5799
                $data = ord($formulaData[1]) ? 'TRUE' : 'FALSE';
5800
5801
                break;
5802 18
            case 0x1E:    //    integer
5803
                // offset: 1; size: 2; unsigned 16-bit integer
5804 9
                $name = 'tInt';
5805 9
                $size = 3;
5806 9
                $data = self::getUInt2d($formulaData, 1);
5807
5808 9
                break;
5809 18
            case 0x1F:    //    number
5810
                // offset: 1; size: 8;
5811 3
                $name = 'tNum';
5812 3
                $size = 9;
5813 3
                $data = self::extractNumber(substr($formulaData, 1));
5814 3
                $data = str_replace(',', '.', (string) $data); // in case non-English locale
5815
5816 3
                break;
5817 18
            case 0x20:    //    array constant
5818 18
            case 0x40:
5819 18
            case 0x60:
5820
                // offset: 1; size: 7; not used
5821
                $name = 'tArray';
5822
                $size = 8;
5823
                $data = null;
5824
5825
                break;
5826 18
            case 0x21:    //    function with fixed number of arguments
5827 18
            case 0x41:
5828 17
            case 0x61:
5829 9
                $name = 'tFunc';
5830 9
                $size = 3;
5831
                // offset: 1; size: 2; index to built-in sheet function
5832 9
                switch (self::getUInt2d($formulaData, 1)) {
5833 9
                    case 2:
5834
                        $function = 'ISNA';
5835
                        $args = 1;
5836
5837
                        break;
5838 9
                    case 3:
5839
                        $function = 'ISERROR';
5840
                        $args = 1;
5841
5842
                        break;
5843 9
                    case 10:
5844 8
                        $function = 'NA';
5845 8
                        $args = 0;
5846
5847 8
                        break;
5848 1
                    case 15:
5849 1
                        $function = 'SIN';
5850 1
                        $args = 1;
5851
5852 1
                        break;
5853
                    case 16:
5854
                        $function = 'COS';
5855
                        $args = 1;
5856
5857
                        break;
5858
                    case 17:
5859
                        $function = 'TAN';
5860
                        $args = 1;
5861
5862
                        break;
5863
                    case 18:
5864
                        $function = 'ATAN';
5865
                        $args = 1;
5866
5867
                        break;
5868
                    case 19:
5869
                        $function = 'PI';
5870
                        $args = 0;
5871
5872
                        break;
5873
                    case 20:
5874
                        $function = 'SQRT';
5875
                        $args = 1;
5876
5877
                        break;
5878
                    case 21:
5879
                        $function = 'EXP';
5880
                        $args = 1;
5881
5882
                        break;
5883
                    case 22:
5884
                        $function = 'LN';
5885
                        $args = 1;
5886
5887
                        break;
5888
                    case 23:
5889
                        $function = 'LOG10';
5890
                        $args = 1;
5891
5892
                        break;
5893
                    case 24:
5894
                        $function = 'ABS';
5895
                        $args = 1;
5896
5897
                        break;
5898
                    case 25:
5899
                        $function = 'INT';
5900
                        $args = 1;
5901
5902
                        break;
5903
                    case 26:
5904
                        $function = 'SIGN';
5905
                        $args = 1;
5906
5907
                        break;
5908
                    case 27:
5909
                        $function = 'ROUND';
5910
                        $args = 2;
5911
5912
                        break;
5913
                    case 30:
5914
                        $function = 'REPT';
5915
                        $args = 2;
5916
5917
                        break;
5918
                    case 31:
5919
                        $function = 'MID';
5920
                        $args = 3;
5921
5922
                        break;
5923
                    case 32:
5924
                        $function = 'LEN';
5925
                        $args = 1;
5926
5927
                        break;
5928
                    case 33:
5929
                        $function = 'VALUE';
5930
                        $args = 1;
5931
5932
                        break;
5933
                    case 34:
5934
                        $function = 'TRUE';
5935
                        $args = 0;
5936
5937
                        break;
5938
                    case 35:
5939
                        $function = 'FALSE';
5940
                        $args = 0;
5941
5942
                        break;
5943
                    case 38:
5944
                        $function = 'NOT';
5945
                        $args = 1;
5946
5947
                        break;
5948
                    case 39:
5949
                        $function = 'MOD';
5950
                        $args = 2;
5951
5952
                        break;
5953
                    case 40:
5954
                        $function = 'DCOUNT';
5955
                        $args = 3;
5956
5957
                        break;
5958
                    case 41:
5959
                        $function = 'DSUM';
5960
                        $args = 3;
5961
5962
                        break;
5963
                    case 42:
5964
                        $function = 'DAVERAGE';
5965
                        $args = 3;
5966
5967
                        break;
5968
                    case 43:
5969
                        $function = 'DMIN';
5970
                        $args = 3;
5971
5972
                        break;
5973
                    case 44:
5974
                        $function = 'DMAX';
5975
                        $args = 3;
5976
5977
                        break;
5978
                    case 45:
5979
                        $function = 'DSTDEV';
5980
                        $args = 3;
5981
5982
                        break;
5983
                    case 48:
5984
                        $function = 'TEXT';
5985
                        $args = 2;
5986
5987
                        break;
5988
                    case 61:
5989
                        $function = 'MIRR';
5990
                        $args = 3;
5991
5992
                        break;
5993
                    case 63:
5994
                        $function = 'RAND';
5995
                        $args = 0;
5996
5997
                        break;
5998
                    case 65:
5999
                        $function = 'DATE';
6000
                        $args = 3;
6001
6002
                        break;
6003
                    case 66:
6004
                        $function = 'TIME';
6005
                        $args = 3;
6006
6007
                        break;
6008
                    case 67:
6009
                        $function = 'DAY';
6010
                        $args = 1;
6011
6012
                        break;
6013
                    case 68:
6014
                        $function = 'MONTH';
6015
                        $args = 1;
6016
6017
                        break;
6018
                    case 69:
6019
                        $function = 'YEAR';
6020
                        $args = 1;
6021
6022
                        break;
6023
                    case 71:
6024
                        $function = 'HOUR';
6025
                        $args = 1;
6026
6027
                        break;
6028
                    case 72:
6029
                        $function = 'MINUTE';
6030
                        $args = 1;
6031
6032
                        break;
6033
                    case 73:
6034
                        $function = 'SECOND';
6035
                        $args = 1;
6036
6037
                        break;
6038
                    case 74:
6039
                        $function = 'NOW';
6040
                        $args = 0;
6041
6042
                        break;
6043
                    case 75:
6044
                        $function = 'AREAS';
6045
                        $args = 1;
6046
6047
                        break;
6048
                    case 76:
6049
                        $function = 'ROWS';
6050
                        $args = 1;
6051
6052
                        break;
6053
                    case 77:
6054
                        $function = 'COLUMNS';
6055
                        $args = 1;
6056
6057
                        break;
6058
                    case 83:
6059
                        $function = 'TRANSPOSE';
6060
                        $args = 1;
6061
6062
                        break;
6063
                    case 86:
6064
                        $function = 'TYPE';
6065
                        $args = 1;
6066
6067
                        break;
6068
                    case 97:
6069
                        $function = 'ATAN2';
6070
                        $args = 2;
6071
6072
                        break;
6073
                    case 98:
6074
                        $function = 'ASIN';
6075
                        $args = 1;
6076
6077
                        break;
6078
                    case 99:
6079
                        $function = 'ACOS';
6080
                        $args = 1;
6081
6082
                        break;
6083
                    case 105:
6084
                        $function = 'ISREF';
6085
                        $args = 1;
6086
6087
                        break;
6088
                    case 111:
6089
                        $function = 'CHAR';
6090
                        $args = 1;
6091
6092
                        break;
6093
                    case 112:
6094
                        $function = 'LOWER';
6095
                        $args = 1;
6096
6097
                        break;
6098
                    case 113:
6099
                        $function = 'UPPER';
6100
                        $args = 1;
6101
6102
                        break;
6103
                    case 114:
6104
                        $function = 'PROPER';
6105
                        $args = 1;
6106
6107
                        break;
6108
                    case 117:
6109
                        $function = 'EXACT';
6110
                        $args = 2;
6111
6112
                        break;
6113
                    case 118:
6114
                        $function = 'TRIM';
6115
                        $args = 1;
6116
6117
                        break;
6118
                    case 119:
6119
                        $function = 'REPLACE';
6120
                        $args = 4;
6121
6122
                        break;
6123
                    case 121:
6124
                        $function = 'CODE';
6125
                        $args = 1;
6126
6127
                        break;
6128
                    case 126:
6129
                        $function = 'ISERR';
6130
                        $args = 1;
6131
6132
                        break;
6133
                    case 127:
6134
                        $function = 'ISTEXT';
6135
                        $args = 1;
6136
6137
                        break;
6138
                    case 128:
6139
                        $function = 'ISNUMBER';
6140
                        $args = 1;
6141
6142
                        break;
6143
                    case 129:
6144
                        $function = 'ISBLANK';
6145
                        $args = 1;
6146
6147
                        break;
6148
                    case 130:
6149
                        $function = 'T';
6150
                        $args = 1;
6151
6152
                        break;
6153
                    case 131:
6154
                        $function = 'N';
6155
                        $args = 1;
6156
6157
                        break;
6158
                    case 140:
6159
                        $function = 'DATEVALUE';
6160
                        $args = 1;
6161
6162
                        break;
6163
                    case 141:
6164
                        $function = 'TIMEVALUE';
6165
                        $args = 1;
6166
6167
                        break;
6168
                    case 142:
6169
                        $function = 'SLN';
6170
                        $args = 3;
6171
6172
                        break;
6173
                    case 143:
6174
                        $function = 'SYD';
6175
                        $args = 4;
6176
6177
                        break;
6178
                    case 162:
6179
                        $function = 'CLEAN';
6180
                        $args = 1;
6181
6182
                        break;
6183
                    case 163:
6184
                        $function = 'MDETERM';
6185
                        $args = 1;
6186
6187
                        break;
6188
                    case 164:
6189
                        $function = 'MINVERSE';
6190
                        $args = 1;
6191
6192
                        break;
6193
                    case 165:
6194
                        $function = 'MMULT';
6195
                        $args = 2;
6196
6197
                        break;
6198
                    case 184:
6199
                        $function = 'FACT';
6200
                        $args = 1;
6201
6202
                        break;
6203
                    case 189:
6204
                        $function = 'DPRODUCT';
6205
                        $args = 3;
6206
6207
                        break;
6208
                    case 190:
6209
                        $function = 'ISNONTEXT';
6210
                        $args = 1;
6211
6212
                        break;
6213
                    case 195:
6214
                        $function = 'DSTDEVP';
6215
                        $args = 3;
6216
6217
                        break;
6218
                    case 196:
6219
                        $function = 'DVARP';
6220
                        $args = 3;
6221
6222
                        break;
6223
                    case 198:
6224
                        $function = 'ISLOGICAL';
6225
                        $args = 1;
6226
6227
                        break;
6228
                    case 199:
6229
                        $function = 'DCOUNTA';
6230
                        $args = 3;
6231
6232
                        break;
6233
                    case 207:
6234
                        $function = 'REPLACEB';
6235
                        $args = 4;
6236
6237
                        break;
6238
                    case 210:
6239
                        $function = 'MIDB';
6240
                        $args = 3;
6241
6242
                        break;
6243
                    case 211:
6244
                        $function = 'LENB';
6245
                        $args = 1;
6246
6247
                        break;
6248
                    case 212:
6249
                        $function = 'ROUNDUP';
6250
                        $args = 2;
6251
6252
                        break;
6253
                    case 213:
6254
                        $function = 'ROUNDDOWN';
6255
                        $args = 2;
6256
6257
                        break;
6258
                    case 214:
6259
                        $function = 'ASC';
6260
                        $args = 1;
6261
6262
                        break;
6263
                    case 215:
6264
                        $function = 'DBCS';
6265
                        $args = 1;
6266
6267
                        break;
6268
                    case 221:
6269
                        $function = 'TODAY';
6270
                        $args = 0;
6271
6272
                        break;
6273
                    case 229:
6274
                        $function = 'SINH';
6275
                        $args = 1;
6276
6277
                        break;
6278
                    case 230:
6279
                        $function = 'COSH';
6280
                        $args = 1;
6281
6282
                        break;
6283
                    case 231:
6284
                        $function = 'TANH';
6285
                        $args = 1;
6286
6287
                        break;
6288
                    case 232:
6289
                        $function = 'ASINH';
6290
                        $args = 1;
6291
6292
                        break;
6293
                    case 233:
6294
                        $function = 'ACOSH';
6295
                        $args = 1;
6296
6297
                        break;
6298
                    case 234:
6299
                        $function = 'ATANH';
6300
                        $args = 1;
6301
6302
                        break;
6303
                    case 235:
6304
                        $function = 'DGET';
6305
                        $args = 3;
6306
6307
                        break;
6308
                    case 244:
6309
                        $function = 'INFO';
6310
                        $args = 1;
6311
6312
                        break;
6313
                    case 252:
6314
                        $function = 'FREQUENCY';
6315
                        $args = 2;
6316
6317
                        break;
6318
                    case 261:
6319
                        $function = 'ERROR.TYPE';
6320
                        $args = 1;
6321
6322
                        break;
6323
                    case 271:
6324
                        $function = 'GAMMALN';
6325
                        $args = 1;
6326
6327
                        break;
6328
                    case 273:
6329
                        $function = 'BINOMDIST';
6330
                        $args = 4;
6331
6332
                        break;
6333
                    case 274:
6334
                        $function = 'CHIDIST';
6335
                        $args = 2;
6336
6337
                        break;
6338
                    case 275:
6339
                        $function = 'CHIINV';
6340
                        $args = 2;
6341
6342
                        break;
6343
                    case 276:
6344
                        $function = 'COMBIN';
6345
                        $args = 2;
6346
6347
                        break;
6348
                    case 277:
6349
                        $function = 'CONFIDENCE';
6350
                        $args = 3;
6351
6352
                        break;
6353
                    case 278:
6354
                        $function = 'CRITBINOM';
6355
                        $args = 3;
6356
6357
                        break;
6358
                    case 279:
6359
                        $function = 'EVEN';
6360
                        $args = 1;
6361
6362
                        break;
6363
                    case 280:
6364
                        $function = 'EXPONDIST';
6365
                        $args = 3;
6366
6367
                        break;
6368
                    case 281:
6369
                        $function = 'FDIST';
6370
                        $args = 3;
6371
6372
                        break;
6373
                    case 282:
6374
                        $function = 'FINV';
6375
                        $args = 3;
6376
6377
                        break;
6378
                    case 283:
6379
                        $function = 'FISHER';
6380
                        $args = 1;
6381
6382
                        break;
6383
                    case 284:
6384
                        $function = 'FISHERINV';
6385
                        $args = 1;
6386
6387
                        break;
6388
                    case 285:
6389
                        $function = 'FLOOR';
6390
                        $args = 2;
6391
6392
                        break;
6393
                    case 286:
6394
                        $function = 'GAMMADIST';
6395
                        $args = 4;
6396
6397
                        break;
6398
                    case 287:
6399
                        $function = 'GAMMAINV';
6400
                        $args = 3;
6401
6402
                        break;
6403
                    case 288:
6404
                        $function = 'CEILING';
6405
                        $args = 2;
6406
6407
                        break;
6408
                    case 289:
6409
                        $function = 'HYPGEOMDIST';
6410
                        $args = 4;
6411
6412
                        break;
6413
                    case 290:
6414
                        $function = 'LOGNORMDIST';
6415
                        $args = 3;
6416
6417
                        break;
6418
                    case 291:
6419
                        $function = 'LOGINV';
6420
                        $args = 3;
6421
6422
                        break;
6423
                    case 292:
6424
                        $function = 'NEGBINOMDIST';
6425
                        $args = 3;
6426
6427
                        break;
6428
                    case 293:
6429
                        $function = 'NORMDIST';
6430
                        $args = 4;
6431
6432
                        break;
6433
                    case 294:
6434
                        $function = 'NORMSDIST';
6435
                        $args = 1;
6436
6437
                        break;
6438
                    case 295:
6439
                        $function = 'NORMINV';
6440
                        $args = 3;
6441
6442
                        break;
6443
                    case 296:
6444
                        $function = 'NORMSINV';
6445
                        $args = 1;
6446
6447
                        break;
6448
                    case 297:
6449
                        $function = 'STANDARDIZE';
6450
                        $args = 3;
6451
6452
                        break;
6453
                    case 298:
6454
                        $function = 'ODD';
6455
                        $args = 1;
6456
6457
                        break;
6458
                    case 299:
6459
                        $function = 'PERMUT';
6460
                        $args = 2;
6461
6462
                        break;
6463
                    case 300:
6464
                        $function = 'POISSON';
6465
                        $args = 3;
6466
6467
                        break;
6468
                    case 301:
6469
                        $function = 'TDIST';
6470
                        $args = 3;
6471
6472
                        break;
6473
                    case 302:
6474
                        $function = 'WEIBULL';
6475
                        $args = 4;
6476
6477
                        break;
6478
                    case 303:
6479
                        $function = 'SUMXMY2';
6480
                        $args = 2;
6481
6482
                        break;
6483
                    case 304:
6484
                        $function = 'SUMX2MY2';
6485
                        $args = 2;
6486
6487
                        break;
6488
                    case 305:
6489
                        $function = 'SUMX2PY2';
6490
                        $args = 2;
6491
6492
                        break;
6493
                    case 306:
6494
                        $function = 'CHITEST';
6495
                        $args = 2;
6496
6497
                        break;
6498
                    case 307:
6499
                        $function = 'CORREL';
6500
                        $args = 2;
6501
6502
                        break;
6503
                    case 308:
6504
                        $function = 'COVAR';
6505
                        $args = 2;
6506
6507
                        break;
6508
                    case 309:
6509
                        $function = 'FORECAST';
6510
                        $args = 3;
6511
6512
                        break;
6513
                    case 310:
6514
                        $function = 'FTEST';
6515
                        $args = 2;
6516
6517
                        break;
6518
                    case 311:
6519
                        $function = 'INTERCEPT';
6520
                        $args = 2;
6521
6522
                        break;
6523
                    case 312:
6524
                        $function = 'PEARSON';
6525
                        $args = 2;
6526
6527
                        break;
6528
                    case 313:
6529
                        $function = 'RSQ';
6530
                        $args = 2;
6531
6532
                        break;
6533
                    case 314:
6534
                        $function = 'STEYX';
6535
                        $args = 2;
6536
6537
                        break;
6538
                    case 315:
6539
                        $function = 'SLOPE';
6540
                        $args = 2;
6541
6542
                        break;
6543
                    case 316:
6544
                        $function = 'TTEST';
6545
                        $args = 4;
6546
6547
                        break;
6548
                    case 325:
6549
                        $function = 'LARGE';
6550
                        $args = 2;
6551
6552
                        break;
6553
                    case 326:
6554
                        $function = 'SMALL';
6555
                        $args = 2;
6556
6557
                        break;
6558
                    case 327:
6559
                        $function = 'QUARTILE';
6560
                        $args = 2;
6561
6562
                        break;
6563
                    case 328:
6564
                        $function = 'PERCENTILE';
6565
                        $args = 2;
6566
6567
                        break;
6568
                    case 331:
6569
                        $function = 'TRIMMEAN';
6570
                        $args = 2;
6571
6572
                        break;
6573
                    case 332:
6574
                        $function = 'TINV';
6575
                        $args = 2;
6576
6577
                        break;
6578
                    case 337:
6579
                        $function = 'POWER';
6580
                        $args = 2;
6581
6582
                        break;
6583
                    case 342:
6584
                        $function = 'RADIANS';
6585
                        $args = 1;
6586
6587
                        break;
6588
                    case 343:
6589
                        $function = 'DEGREES';
6590
                        $args = 1;
6591
6592
                        break;
6593
                    case 346:
6594
                        $function = 'COUNTIF';
6595
                        $args = 2;
6596
6597
                        break;
6598
                    case 347:
6599
                        $function = 'COUNTBLANK';
6600
                        $args = 1;
6601
6602
                        break;
6603
                    case 350:
6604
                        $function = 'ISPMT';
6605
                        $args = 4;
6606
6607
                        break;
6608
                    case 351:
6609
                        $function = 'DATEDIF';
6610
                        $args = 3;
6611
6612
                        break;
6613
                    case 352:
6614
                        $function = 'DATESTRING';
6615
                        $args = 1;
6616
6617
                        break;
6618
                    case 353:
6619
                        $function = 'NUMBERSTRING';
6620
                        $args = 2;
6621
6622
                        break;
6623
                    case 360:
6624
                        $function = 'PHONETIC';
6625
                        $args = 1;
6626
6627
                        break;
6628
                    case 368:
6629
                        $function = 'BAHTTEXT';
6630
                        $args = 1;
6631
6632
                        break;
6633
                    default:
6634
                        throw new Exception('Unrecognized function in formula');
6635
6636
                        break;
6637
                }
6638 9
                $data = ['function' => $function, 'args' => $args];
6639
6640 9
                break;
6641 17
            case 0x22:    //    function with variable number of arguments
6642 17
            case 0x42:
6643 17
            case 0x62:
6644 5
                $name = 'tFuncV';
6645 5
                $size = 4;
6646
                // offset: 1; size: 1; number of arguments
6647 5
                $args = ord($formulaData[1]);
6648
                // offset: 2: size: 2; index to built-in sheet function
6649 5
                $index = self::getUInt2d($formulaData, 2);
6650
                switch ($index) {
6651 5
                    case 0:
6652 2
                        $function = 'COUNT';
6653
6654 2
                        break;
6655 5
                    case 1:
6656 1
                        $function = 'IF';
6657
6658 1
                        break;
6659 5
                    case 4:
6660 5
                        $function = 'SUM';
6661
6662 5
                        break;
6663
                    case 5:
6664
                        $function = 'AVERAGE';
6665
6666
                        break;
6667
                    case 6:
6668
                        $function = 'MIN';
6669
6670
                        break;
6671
                    case 7:
6672
                        $function = 'MAX';
6673
6674
                        break;
6675
                    case 8:
6676
                        $function = 'ROW';
6677
6678
                        break;
6679
                    case 9:
6680
                        $function = 'COLUMN';
6681
6682
                        break;
6683
                    case 11:
6684
                        $function = 'NPV';
6685
6686
                        break;
6687
                    case 12:
6688
                        $function = 'STDEV';
6689
6690
                        break;
6691
                    case 13:
6692
                        $function = 'DOLLAR';
6693
6694
                        break;
6695
                    case 14:
6696
                        $function = 'FIXED';
6697
6698
                        break;
6699
                    case 28:
6700
                        $function = 'LOOKUP';
6701
6702
                        break;
6703
                    case 29:
6704
                        $function = 'INDEX';
6705
6706
                        break;
6707
                    case 36:
6708
                        $function = 'AND';
6709
6710
                        break;
6711
                    case 37:
6712
                        $function = 'OR';
6713
6714
                        break;
6715
                    case 46:
6716
                        $function = 'VAR';
6717
6718
                        break;
6719
                    case 49:
6720
                        $function = 'LINEST';
6721
6722
                        break;
6723
                    case 50:
6724
                        $function = 'TREND';
6725
6726
                        break;
6727
                    case 51:
6728
                        $function = 'LOGEST';
6729
6730
                        break;
6731
                    case 52:
6732
                        $function = 'GROWTH';
6733
6734
                        break;
6735
                    case 56:
6736
                        $function = 'PV';
6737
6738
                        break;
6739
                    case 57:
6740
                        $function = 'FV';
6741
6742
                        break;
6743
                    case 58:
6744
                        $function = 'NPER';
6745
6746
                        break;
6747
                    case 59:
6748
                        $function = 'PMT';
6749
6750
                        break;
6751
                    case 60:
6752
                        $function = 'RATE';
6753
6754
                        break;
6755
                    case 62:
6756
                        $function = 'IRR';
6757
6758
                        break;
6759
                    case 64:
6760
                        $function = 'MATCH';
6761
6762
                        break;
6763
                    case 70:
6764
                        $function = 'WEEKDAY';
6765
6766
                        break;
6767
                    case 78:
6768
                        $function = 'OFFSET';
6769
6770
                        break;
6771
                    case 82:
6772
                        $function = 'SEARCH';
6773
6774
                        break;
6775
                    case 100:
6776
                        $function = 'CHOOSE';
6777
6778
                        break;
6779
                    case 101:
6780
                        $function = 'HLOOKUP';
6781
6782
                        break;
6783
                    case 102:
6784
                        $function = 'VLOOKUP';
6785
6786
                        break;
6787
                    case 109:
6788
                        $function = 'LOG';
6789
6790
                        break;
6791
                    case 115:
6792
                        $function = 'LEFT';
6793
6794
                        break;
6795
                    case 116:
6796
                        $function = 'RIGHT';
6797
6798
                        break;
6799
                    case 120:
6800
                        $function = 'SUBSTITUTE';
6801
6802
                        break;
6803
                    case 124:
6804
                        $function = 'FIND';
6805
6806
                        break;
6807
                    case 125:
6808
                        $function = 'CELL';
6809
6810
                        break;
6811
                    case 144:
6812
                        $function = 'DDB';
6813
6814
                        break;
6815
                    case 148:
6816
                        $function = 'INDIRECT';
6817
6818
                        break;
6819
                    case 167:
6820
                        $function = 'IPMT';
6821
6822
                        break;
6823
                    case 168:
6824
                        $function = 'PPMT';
6825
6826
                        break;
6827
                    case 169:
6828
                        $function = 'COUNTA';
6829
6830
                        break;
6831
                    case 183:
6832
                        $function = 'PRODUCT';
6833
6834
                        break;
6835
                    case 193:
6836
                        $function = 'STDEVP';
6837
6838
                        break;
6839
                    case 194:
6840
                        $function = 'VARP';
6841
6842
                        break;
6843
                    case 197:
6844
                        $function = 'TRUNC';
6845
6846
                        break;
6847
                    case 204:
6848
                        $function = 'USDOLLAR';
6849
6850
                        break;
6851
                    case 205:
6852
                        $function = 'FINDB';
6853
6854
                        break;
6855
                    case 206:
6856
                        $function = 'SEARCHB';
6857
6858
                        break;
6859
                    case 208:
6860
                        $function = 'LEFTB';
6861
6862
                        break;
6863
                    case 209:
6864
                        $function = 'RIGHTB';
6865
6866
                        break;
6867
                    case 216:
6868
                        $function = 'RANK';
6869
6870
                        break;
6871
                    case 219:
6872
                        $function = 'ADDRESS';
6873
6874
                        break;
6875
                    case 220:
6876
                        $function = 'DAYS360';
6877
6878
                        break;
6879
                    case 222:
6880
                        $function = 'VDB';
6881
6882
                        break;
6883
                    case 227:
6884
                        $function = 'MEDIAN';
6885
6886
                        break;
6887
                    case 228:
6888
                        $function = 'SUMPRODUCT';
6889
6890
                        break;
6891
                    case 247:
6892
                        $function = 'DB';
6893
6894
                        break;
6895
                    case 255:
6896
                        $function = '';
6897
6898
                        break;
6899
                    case 269:
6900
                        $function = 'AVEDEV';
6901
6902
                        break;
6903
                    case 270:
6904
                        $function = 'BETADIST';
6905
6906
                        break;
6907
                    case 272:
6908
                        $function = 'BETAINV';
6909
6910
                        break;
6911
                    case 317:
6912
                        $function = 'PROB';
6913
6914
                        break;
6915
                    case 318:
6916
                        $function = 'DEVSQ';
6917
6918
                        break;
6919
                    case 319:
6920
                        $function = 'GEOMEAN';
6921
6922
                        break;
6923
                    case 320:
6924
                        $function = 'HARMEAN';
6925
6926
                        break;
6927
                    case 321:
6928
                        $function = 'SUMSQ';
6929
6930
                        break;
6931
                    case 322:
6932
                        $function = 'KURT';
6933
6934
                        break;
6935
                    case 323:
6936
                        $function = 'SKEW';
6937
6938
                        break;
6939
                    case 324:
6940
                        $function = 'ZTEST';
6941
6942
                        break;
6943
                    case 329:
6944
                        $function = 'PERCENTRANK';
6945
6946
                        break;
6947
                    case 330:
6948
                        $function = 'MODE';
6949
6950
                        break;
6951
                    case 336:
6952
                        $function = 'CONCATENATE';
6953
6954
                        break;
6955
                    case 344:
6956
                        $function = 'SUBTOTAL';
6957
6958
                        break;
6959
                    case 345:
6960
                        $function = 'SUMIF';
6961
6962
                        break;
6963
                    case 354:
6964
                        $function = 'ROMAN';
6965
6966
                        break;
6967
                    case 358:
6968
                        $function = 'GETPIVOTDATA';
6969
6970
                        break;
6971
                    case 359:
6972
                        $function = 'HYPERLINK';
6973
6974
                        break;
6975
                    case 361:
6976
                        $function = 'AVERAGEA';
6977
6978
                        break;
6979
                    case 362:
6980
                        $function = 'MAXA';
6981
6982
                        break;
6983
                    case 363:
6984
                        $function = 'MINA';
6985
6986
                        break;
6987
                    case 364:
6988
                        $function = 'STDEVPA';
6989
6990
                        break;
6991
                    case 365:
6992
                        $function = 'VARPA';
6993
6994
                        break;
6995
                    case 366:
6996
                        $function = 'STDEVA';
6997
6998
                        break;
6999
                    case 367:
7000
                        $function = 'VARA';
7001
7002
                        break;
7003
                    default:
7004
                        throw new Exception('Unrecognized function in formula');
7005
7006
                        break;
7007
                }
7008 5
                $data = ['function' => $function, 'args' => $args];
7009
7010 5
                break;
7011 17
            case 0x23:    //    index to defined name
7012 17
            case 0x43:
7013 17
            case 0x63:
7014
                $name = 'tName';
7015
                $size = 5;
7016
                // offset: 1; size: 2; one-based index to definedname record
7017
                $definedNameIndex = self::getUInt2d($formulaData, 1) - 1;
7018
                // offset: 2; size: 2; not used
7019
                $data = $this->definedname[$definedNameIndex]['name'];
7020
7021
                break;
7022 17
            case 0x24:    //    single cell reference e.g. A5
7023 17
            case 0x44:
7024 17
            case 0x64:
7025 3
                $name = 'tRef';
7026 3
                $size = 5;
7027 3
                $data = $this->readBIFF8CellAddress(substr($formulaData, 1, 4));
7028
7029 3
                break;
7030 17
            case 0x25:    //    cell range reference to cells in the same sheet (2d)
7031 6
            case 0x45:
7032 6
            case 0x65:
7033 15
                $name = 'tArea';
7034 15
                $size = 9;
7035 15
                $data = $this->readBIFF8CellRangeAddress(substr($formulaData, 1, 8));
7036
7037 15
                break;
7038 6
            case 0x26:    //    Constant reference sub-expression
7039 6
            case 0x46:
7040 6
            case 0x66:
7041
                $name = 'tMemArea';
7042
                // offset: 1; size: 4; not used
7043
                // offset: 5; size: 2; size of the following subexpression
7044
                $subSize = self::getUInt2d($formulaData, 5);
7045
                $size = 7 + $subSize;
7046
                $data = $this->getFormulaFromData(substr($formulaData, 7, $subSize));
7047
7048
                break;
7049 6
            case 0x27:    //    Deleted constant reference sub-expression
7050 6
            case 0x47:
7051 6
            case 0x67:
7052
                $name = 'tMemErr';
7053
                // offset: 1; size: 4; not used
7054
                // offset: 5; size: 2; size of the following subexpression
7055
                $subSize = self::getUInt2d($formulaData, 5);
7056
                $size = 7 + $subSize;
7057
                $data = $this->getFormulaFromData(substr($formulaData, 7, $subSize));
7058
7059
                break;
7060 6
            case 0x29:    //    Variable reference sub-expression
7061 6
            case 0x49:
7062 6
            case 0x69:
7063
                $name = 'tMemFunc';
7064
                // offset: 1; size: 2; size of the following sub-expression
7065
                $subSize = self::getUInt2d($formulaData, 1);
7066
                $size = 3 + $subSize;
7067
                $data = $this->getFormulaFromData(substr($formulaData, 3, $subSize));
7068
7069
                break;
7070 6
            case 0x2C: // Relative 2d cell reference reference, used in shared formulas and some other places
7071 6
            case 0x4C:
7072 6
            case 0x6C:
7073
                $name = 'tRefN';
7074
                $size = 5;
7075
                $data = $this->readBIFF8CellAddressB(substr($formulaData, 1, 4), $baseCell);
7076
7077
                break;
7078 6
            case 0x2D:    //    Relative 2d range reference
7079 6
            case 0x4D:
7080 6
            case 0x6D:
7081
                $name = 'tAreaN';
7082
                $size = 9;
7083
                $data = $this->readBIFF8CellRangeAddressB(substr($formulaData, 1, 8), $baseCell);
7084
7085
                break;
7086 6
            case 0x39:    //    External name
7087 6
            case 0x59:
7088 6
            case 0x79:
7089
                $name = 'tNameX';
7090
                $size = 7;
7091
                // offset: 1; size: 2; index to REF entry in EXTERNSHEET record
7092
                // offset: 3; size: 2; one-based index to DEFINEDNAME or EXTERNNAME record
7093
                $index = self::getUInt2d($formulaData, 3);
7094
                // assume index is to EXTERNNAME record
7095
                $data = $this->externalNames[$index - 1]['name'];
7096
                // offset: 5; size: 2; not used
7097
                break;
7098 6
            case 0x3A:    //    3d reference to cell
7099 5
            case 0x5A:
7100 5
            case 0x7A:
7101 1
                $name = 'tRef3d';
7102 1
                $size = 7;
7103
7104
                try {
7105
                    // offset: 1; size: 2; index to REF entry
7106 1
                    $sheetRange = $this->readSheetRangeByRefIndex(self::getUInt2d($formulaData, 1));
7107
                    // offset: 3; size: 4; cell address
7108 1
                    $cellAddress = $this->readBIFF8CellAddress(substr($formulaData, 3, 4));
7109
7110 1
                    $data = "$sheetRange!$cellAddress";
7111
                } catch (PhpSpreadsheetException $e) {
7112
                    // deleted sheet reference
7113
                    $data = '#REF!';
7114
                }
7115
7116 1
                break;
7117 5
            case 0x3B:    //    3d reference to cell range
7118
            case 0x5B:
7119
            case 0x7B:
7120 5
                $name = 'tArea3d';
7121 5
                $size = 11;
7122
7123
                try {
7124
                    // offset: 1; size: 2; index to REF entry
7125 5
                    $sheetRange = $this->readSheetRangeByRefIndex(self::getUInt2d($formulaData, 1));
7126
                    // offset: 3; size: 8; cell address
7127 5
                    $cellRangeAddress = $this->readBIFF8CellRangeAddress(substr($formulaData, 3, 8));
7128
7129 5
                    $data = "$sheetRange!$cellRangeAddress";
7130
                } catch (PhpSpreadsheetException $e) {
7131
                    // deleted sheet reference
7132
                    $data = '#REF!';
7133
                }
7134
7135 5
                break;
7136
            // Unknown cases    // don't know how to deal with
7137
            default:
7138
                throw new Exception('Unrecognized token ' . sprintf('%02X', $id) . ' in formula');
7139
7140
                break;
7141
        }
7142
7143
        return [
7144 18
            'id' => $id,
7145 18
            'name' => $name,
7146 18
            'size' => $size,
7147 18
            'data' => $data,
7148
        ];
7149
    }
7150
7151
    /**
7152
     * Reads a cell address in BIFF8 e.g. 'A2' or '$A$2'
7153
     * section 3.3.4.
7154
     *
7155
     * @param string $cellAddressStructure
7156
     *
7157
     * @return string
7158
     */
7159 5
    private function readBIFF8CellAddress($cellAddressStructure)
7160
    {
7161
        // offset: 0; size: 2; index to row (0... 65535) (or offset (-32768... 32767))
7162 5
        $row = self::getUInt2d($cellAddressStructure, 0) + 1;
7163
7164
        // offset: 2; size: 2; index to column or column offset + relative flags
7165
        // bit: 7-0; mask 0x00FF; column index
7166 5
        $column = Coordinate::stringFromColumnIndex((0x00FF & self::getUInt2d($cellAddressStructure, 2)) + 1);
7167
7168
        // bit: 14; mask 0x4000; (1 = relative column index, 0 = absolute column index)
7169 5
        if (!(0x4000 & self::getUInt2d($cellAddressStructure, 2))) {
7170 4
            $column = '$' . $column;
7171
        }
7172
        // bit: 15; mask 0x8000; (1 = relative row index, 0 = absolute row index)
7173 5
        if (!(0x8000 & self::getUInt2d($cellAddressStructure, 2))) {
7174 4
            $row = '$' . $row;
7175
        }
7176
7177 5
        return $column . $row;
7178
    }
7179
7180
    /**
7181
     * Reads a cell address in BIFF8 for shared formulas. Uses positive and negative values for row and column
7182
     * to indicate offsets from a base cell
7183
     * section 3.3.4.
7184
     *
7185
     * @param string $cellAddressStructure
7186
     * @param string $baseCell Base cell, only needed when formula contains tRefN tokens, e.g. with shared formulas
7187
     *
7188
     * @return string
7189
     */
7190
    private function readBIFF8CellAddressB($cellAddressStructure, $baseCell = 'A1')
7191
    {
7192
        [$baseCol, $baseRow] = Coordinate::coordinateFromString($baseCell);
7193
        $baseCol = Coordinate::columnIndexFromString($baseCol) - 1;
7194
7195
        // offset: 0; size: 2; index to row (0... 65535) (or offset (-32768... 32767))
7196
        $rowIndex = self::getUInt2d($cellAddressStructure, 0);
7197
        $row = self::getUInt2d($cellAddressStructure, 0) + 1;
7198
7199
        // bit: 14; mask 0x4000; (1 = relative column index, 0 = absolute column index)
7200
        if (!(0x4000 & self::getUInt2d($cellAddressStructure, 2))) {
7201
            // offset: 2; size: 2; index to column or column offset + relative flags
7202
            // bit: 7-0; mask 0x00FF; column index
7203
            $colIndex = 0x00FF & self::getUInt2d($cellAddressStructure, 2);
7204
7205
            $column = Coordinate::stringFromColumnIndex($colIndex + 1);
7206
            $column = '$' . $column;
7207
        } else {
7208
            // offset: 2; size: 2; index to column or column offset + relative flags
7209
            // bit: 7-0; mask 0x00FF; column index
7210
            $relativeColIndex = 0x00FF & self::getInt2d($cellAddressStructure, 2);
7211
            $colIndex = $baseCol + $relativeColIndex;
7212
            $colIndex = ($colIndex < 256) ? $colIndex : $colIndex - 256;
7213
            $colIndex = ($colIndex >= 0) ? $colIndex : $colIndex + 256;
7214
            $column = Coordinate::stringFromColumnIndex($colIndex + 1);
7215
        }
7216
7217
        // bit: 15; mask 0x8000; (1 = relative row index, 0 = absolute row index)
7218
        if (!(0x8000 & self::getUInt2d($cellAddressStructure, 2))) {
7219
            $row = '$' . $row;
7220
        } else {
7221
            $rowIndex = ($rowIndex <= 32767) ? $rowIndex : $rowIndex - 65536;
7222
            $row = $baseRow + $rowIndex;
7223
        }
7224
7225
        return $column . $row;
7226
    }
7227
7228
    /**
7229
     * Reads a cell range address in BIFF5 e.g. 'A2:B6' or 'A1'
7230
     * always fixed range
7231
     * section 2.5.14.
7232
     *
7233
     * @param string $subData
7234
     *
7235
     * @return string
7236
     */
7237 35
    private function readBIFF5CellRangeAddressFixed($subData)
7238
    {
7239
        // offset: 0; size: 2; index to first row
7240 35
        $fr = self::getUInt2d($subData, 0) + 1;
7241
7242
        // offset: 2; size: 2; index to last row
7243 35
        $lr = self::getUInt2d($subData, 2) + 1;
7244
7245
        // offset: 4; size: 1; index to first column
7246 35
        $fc = ord($subData[4]);
7247
7248
        // offset: 5; size: 1; index to last column
7249 35
        $lc = ord($subData[5]);
7250
7251
        // check values
7252 35
        if ($fr > $lr || $fc > $lc) {
7253
            throw new Exception('Not a cell range address');
7254
        }
7255
7256
        // column index to letter
7257 35
        $fc = Coordinate::stringFromColumnIndex($fc + 1);
7258 35
        $lc = Coordinate::stringFromColumnIndex($lc + 1);
7259
7260 35
        if ($fr == $lr && $fc == $lc) {
7261 31
            return "$fc$fr";
7262
        }
7263
7264 16
        return "$fc$fr:$lc$lr";
7265
    }
7266
7267
    /**
7268
     * Reads a cell range address in BIFF8 e.g. 'A2:B6' or 'A1'
7269
     * always fixed range
7270
     * section 2.5.14.
7271
     *
7272
     * @param string $subData
7273
     *
7274
     * @return string
7275
     */
7276 13
    private function readBIFF8CellRangeAddressFixed($subData)
7277
    {
7278
        // offset: 0; size: 2; index to first row
7279 13
        $fr = self::getUInt2d($subData, 0) + 1;
7280
7281
        // offset: 2; size: 2; index to last row
7282 13
        $lr = self::getUInt2d($subData, 2) + 1;
7283
7284
        // offset: 4; size: 2; index to first column
7285 13
        $fc = self::getUInt2d($subData, 4);
7286
7287
        // offset: 6; size: 2; index to last column
7288 13
        $lc = self::getUInt2d($subData, 6);
7289
7290
        // check values
7291 13
        if ($fr > $lr || $fc > $lc) {
7292
            throw new Exception('Not a cell range address');
7293
        }
7294
7295
        // column index to letter
7296 13
        $fc = Coordinate::stringFromColumnIndex($fc + 1);
7297 13
        $lc = Coordinate::stringFromColumnIndex($lc + 1);
7298
7299 13
        if ($fr == $lr && $fc == $lc) {
7300 3
            return "$fc$fr";
7301
        }
7302
7303 13
        return "$fc$fr:$lc$lr";
7304
    }
7305
7306
    /**
7307
     * Reads a cell range address in BIFF8 e.g. 'A2:B6' or '$A$2:$B$6'
7308
     * there are flags indicating whether column/row index is relative
7309
     * section 3.3.4.
7310
     *
7311
     * @param string $subData
7312
     *
7313
     * @return string
7314
     */
7315 16
    private function readBIFF8CellRangeAddress($subData)
7316
    {
7317
        // todo: if cell range is just a single cell, should this funciton
7318
        // not just return e.g. 'A1' and not 'A1:A1' ?
7319
7320
        // offset: 0; size: 2; index to first row (0... 65535) (or offset (-32768... 32767))
7321 16
        $fr = self::getUInt2d($subData, 0) + 1;
7322
7323
        // offset: 2; size: 2; index to last row (0... 65535) (or offset (-32768... 32767))
7324 16
        $lr = self::getUInt2d($subData, 2) + 1;
7325
7326
        // offset: 4; size: 2; index to first column or column offset + relative flags
7327
7328
        // bit: 7-0; mask 0x00FF; column index
7329 16
        $fc = Coordinate::stringFromColumnIndex((0x00FF & self::getUInt2d($subData, 4)) + 1);
7330
7331
        // bit: 14; mask 0x4000; (1 = relative column index, 0 = absolute column index)
7332 16
        if (!(0x4000 & self::getUInt2d($subData, 4))) {
7333 5
            $fc = '$' . $fc;
7334
        }
7335
7336
        // bit: 15; mask 0x8000; (1 = relative row index, 0 = absolute row index)
7337 16
        if (!(0x8000 & self::getUInt2d($subData, 4))) {
7338 5
            $fr = '$' . $fr;
7339
        }
7340
7341
        // offset: 6; size: 2; index to last column or column offset + relative flags
7342
7343
        // bit: 7-0; mask 0x00FF; column index
7344 16
        $lc = Coordinate::stringFromColumnIndex((0x00FF & self::getUInt2d($subData, 6)) + 1);
7345
7346
        // bit: 14; mask 0x4000; (1 = relative column index, 0 = absolute column index)
7347 16
        if (!(0x4000 & self::getUInt2d($subData, 6))) {
7348 5
            $lc = '$' . $lc;
7349
        }
7350
7351
        // bit: 15; mask 0x8000; (1 = relative row index, 0 = absolute row index)
7352 16
        if (!(0x8000 & self::getUInt2d($subData, 6))) {
7353 5
            $lr = '$' . $lr;
7354
        }
7355
7356 16
        return "$fc$fr:$lc$lr";
7357
    }
7358
7359
    /**
7360
     * Reads a cell range address in BIFF8 for shared formulas. Uses positive and negative values for row and column
7361
     * to indicate offsets from a base cell
7362
     * section 3.3.4.
7363
     *
7364
     * @param string $subData
7365
     * @param string $baseCell Base cell
7366
     *
7367
     * @return string Cell range address
7368
     */
7369
    private function readBIFF8CellRangeAddressB($subData, $baseCell = 'A1')
7370
    {
7371
        [$baseCol, $baseRow] = Coordinate::coordinateFromString($baseCell);
7372
        $baseCol = Coordinate::columnIndexFromString($baseCol) - 1;
7373
7374
        // TODO: if cell range is just a single cell, should this funciton
7375
        // not just return e.g. 'A1' and not 'A1:A1' ?
7376
7377
        // offset: 0; size: 2; first row
7378
        $frIndex = self::getUInt2d($subData, 0); // adjust below
7379
7380
        // offset: 2; size: 2; relative index to first row (0... 65535) should be treated as offset (-32768... 32767)
7381
        $lrIndex = self::getUInt2d($subData, 2); // adjust below
7382
7383
        // bit: 14; mask 0x4000; (1 = relative column index, 0 = absolute column index)
7384
        if (!(0x4000 & self::getUInt2d($subData, 4))) {
7385
            // absolute column index
7386
            // offset: 4; size: 2; first column with relative/absolute flags
7387
            // bit: 7-0; mask 0x00FF; column index
7388
            $fcIndex = 0x00FF & self::getUInt2d($subData, 4);
7389
            $fc = Coordinate::stringFromColumnIndex($fcIndex + 1);
7390
            $fc = '$' . $fc;
7391
        } else {
7392
            // column offset
7393
            // offset: 4; size: 2; first column with relative/absolute flags
7394
            // bit: 7-0; mask 0x00FF; column index
7395
            $relativeFcIndex = 0x00FF & self::getInt2d($subData, 4);
7396
            $fcIndex = $baseCol + $relativeFcIndex;
7397
            $fcIndex = ($fcIndex < 256) ? $fcIndex : $fcIndex - 256;
7398
            $fcIndex = ($fcIndex >= 0) ? $fcIndex : $fcIndex + 256;
7399
            $fc = Coordinate::stringFromColumnIndex($fcIndex + 1);
7400
        }
7401
7402
        // bit: 15; mask 0x8000; (1 = relative row index, 0 = absolute row index)
7403
        if (!(0x8000 & self::getUInt2d($subData, 4))) {
7404
            // absolute row index
7405
            $fr = $frIndex + 1;
7406
            $fr = '$' . $fr;
7407
        } else {
7408
            // row offset
7409
            $frIndex = ($frIndex <= 32767) ? $frIndex : $frIndex - 65536;
7410
            $fr = $baseRow + $frIndex;
7411
        }
7412
7413
        // bit: 14; mask 0x4000; (1 = relative column index, 0 = absolute column index)
7414
        if (!(0x4000 & self::getUInt2d($subData, 6))) {
7415
            // absolute column index
7416
            // offset: 6; size: 2; last column with relative/absolute flags
7417
            // bit: 7-0; mask 0x00FF; column index
7418
            $lcIndex = 0x00FF & self::getUInt2d($subData, 6);
7419
            $lc = Coordinate::stringFromColumnIndex($lcIndex + 1);
7420
            $lc = '$' . $lc;
7421
        } else {
7422
            // column offset
7423
            // offset: 4; size: 2; first column with relative/absolute flags
7424
            // bit: 7-0; mask 0x00FF; column index
7425
            $relativeLcIndex = 0x00FF & self::getInt2d($subData, 4);
7426
            $lcIndex = $baseCol + $relativeLcIndex;
7427
            $lcIndex = ($lcIndex < 256) ? $lcIndex : $lcIndex - 256;
7428
            $lcIndex = ($lcIndex >= 0) ? $lcIndex : $lcIndex + 256;
7429
            $lc = Coordinate::stringFromColumnIndex($lcIndex + 1);
7430
        }
7431
7432
        // bit: 15; mask 0x8000; (1 = relative row index, 0 = absolute row index)
7433
        if (!(0x8000 & self::getUInt2d($subData, 6))) {
7434
            // absolute row index
7435
            $lr = $lrIndex + 1;
7436
            $lr = '$' . $lr;
7437
        } else {
7438
            // row offset
7439
            $lrIndex = ($lrIndex <= 32767) ? $lrIndex : $lrIndex - 65536;
7440
            $lr = $baseRow + $lrIndex;
7441
        }
7442
7443
        return "$fc$fr:$lc$lr";
7444
    }
7445
7446
    /**
7447
     * Read BIFF8 cell range address list
7448
     * section 2.5.15.
7449
     *
7450
     * @param string $subData
7451
     *
7452
     * @return array
7453
     */
7454 13
    private function readBIFF8CellRangeAddressList($subData)
7455
    {
7456 13
        $cellRangeAddresses = [];
7457
7458
        // offset: 0; size: 2; number of the following cell range addresses
7459 13
        $nm = self::getUInt2d($subData, 0);
7460
7461 13
        $offset = 2;
7462
        // offset: 2; size: 8 * $nm; list of $nm (fixed) cell range addresses
7463 13
        for ($i = 0; $i < $nm; ++$i) {
7464 13
            $cellRangeAddresses[] = $this->readBIFF8CellRangeAddressFixed(substr($subData, $offset, 8));
7465 13
            $offset += 8;
7466
        }
7467
7468
        return [
7469 13
            'size' => 2 + 8 * $nm,
7470 13
            'cellRangeAddresses' => $cellRangeAddresses,
7471
        ];
7472
    }
7473
7474
    /**
7475
     * Read BIFF5 cell range address list
7476
     * section 2.5.15.
7477
     *
7478
     * @param string $subData
7479
     *
7480
     * @return array
7481
     */
7482 35
    private function readBIFF5CellRangeAddressList($subData)
7483
    {
7484 35
        $cellRangeAddresses = [];
7485
7486
        // offset: 0; size: 2; number of the following cell range addresses
7487 35
        $nm = self::getUInt2d($subData, 0);
7488
7489 35
        $offset = 2;
7490
        // offset: 2; size: 6 * $nm; list of $nm (fixed) cell range addresses
7491 35
        for ($i = 0; $i < $nm; ++$i) {
7492 35
            $cellRangeAddresses[] = $this->readBIFF5CellRangeAddressFixed(substr($subData, $offset, 6));
7493 35
            $offset += 6;
7494
        }
7495
7496
        return [
7497 35
            'size' => 2 + 6 * $nm,
7498 35
            'cellRangeAddresses' => $cellRangeAddresses,
7499
        ];
7500
    }
7501
7502
    /**
7503
     * Get a sheet range like Sheet1:Sheet3 from REF index
7504
     * Note: If there is only one sheet in the range, one gets e.g Sheet1
7505
     * It can also happen that the REF structure uses the -1 (FFFF) code to indicate deleted sheets,
7506
     * in which case an Exception is thrown.
7507
     *
7508
     * @param int $index
7509
     *
7510
     * @return false|string
7511
     */
7512 6
    private function readSheetRangeByRefIndex($index)
7513
    {
7514 6
        if (isset($this->ref[$index])) {
7515 6
            $type = $this->externalBooks[$this->ref[$index]['externalBookIndex']]['type'];
7516
7517
            switch ($type) {
7518 6
                case 'internal':
7519
                    // check if we have a deleted 3d reference
7520 6
                    if ($this->ref[$index]['firstSheetIndex'] == 0xFFFF || $this->ref[$index]['lastSheetIndex'] == 0xFFFF) {
7521
                        throw new Exception('Deleted sheet reference');
7522
                    }
7523
7524
                    // we have normal sheet range (collapsed or uncollapsed)
7525 6
                    $firstSheetName = $this->sheets[$this->ref[$index]['firstSheetIndex']]['name'];
7526 6
                    $lastSheetName = $this->sheets[$this->ref[$index]['lastSheetIndex']]['name'];
7527
7528 6
                    if ($firstSheetName == $lastSheetName) {
7529
                        // collapsed sheet range
7530 6
                        $sheetRange = $firstSheetName;
7531
                    } else {
7532
                        $sheetRange = "$firstSheetName:$lastSheetName";
7533
                    }
7534
7535
                    // escape the single-quotes
7536 6
                    $sheetRange = str_replace("'", "''", $sheetRange);
7537
7538
                    // if there are special characters, we need to enclose the range in single-quotes
7539
                    // todo: check if we have identified the whole set of special characters
7540
                    // it seems that the following characters are not accepted for sheet names
7541
                    // and we may assume that they are not present: []*/:\?
7542 6
                    if (preg_match("/[ !\"@#£$%&{()}<>=+'|^,;-]/u", $sheetRange)) {
7543 1
                        $sheetRange = "'$sheetRange'";
7544
                    }
7545
7546 6
                    return $sheetRange;
7547
7548
                    break;
7549
                default:
7550
                    // TODO: external sheet support
7551
                    throw new Exception('Xls reader only supports internal sheets in formulas');
7552
7553
                    break;
7554
            }
7555
        }
7556
7557
        return false;
7558
    }
7559
7560
    /**
7561
     * read BIFF8 constant value array from array data
7562
     * returns e.g. ['value' => '{1,2;3,4}', 'size' => 40]
7563
     * section 2.5.8.
7564
     *
7565
     * @param string $arrayData
7566
     *
7567
     * @return array
7568
     */
7569
    private static function readBIFF8ConstantArray($arrayData)
7570
    {
7571
        // offset: 0; size: 1; number of columns decreased by 1
7572
        $nc = ord($arrayData[0]);
7573
7574
        // offset: 1; size: 2; number of rows decreased by 1
7575
        $nr = self::getUInt2d($arrayData, 1);
7576
        $size = 3; // initialize
7577
        $arrayData = substr($arrayData, 3);
7578
7579
        // offset: 3; size: var; list of ($nc + 1) * ($nr + 1) constant values
7580
        $matrixChunks = [];
7581
        for ($r = 1; $r <= $nr + 1; ++$r) {
7582
            $items = [];
7583
            for ($c = 1; $c <= $nc + 1; ++$c) {
7584
                $constant = self::readBIFF8Constant($arrayData);
7585
                $items[] = $constant['value'];
7586
                $arrayData = substr($arrayData, $constant['size']);
7587
                $size += $constant['size'];
7588
            }
7589
            $matrixChunks[] = implode(',', $items); // looks like e.g. '1,"hello"'
7590
        }
7591
        $matrix = '{' . implode(';', $matrixChunks) . '}';
7592
7593
        return [
7594
            'value' => $matrix,
7595
            'size' => $size,
7596
        ];
7597
    }
7598
7599
    /**
7600
     * read BIFF8 constant value which may be 'Empty Value', 'Number', 'String Value', 'Boolean Value', 'Error Value'
7601
     * section 2.5.7
7602
     * returns e.g. ['value' => '5', 'size' => 9].
7603
     *
7604
     * @param string $valueData
7605
     *
7606
     * @return array
7607
     */
7608
    private static function readBIFF8Constant($valueData)
7609
    {
7610
        // offset: 0; size: 1; identifier for type of constant
7611
        $identifier = ord($valueData[0]);
7612
7613
        switch ($identifier) {
7614
            case 0x00: // empty constant (what is this?)
7615
                $value = '';
7616
                $size = 9;
7617
7618
                break;
7619
            case 0x01: // number
7620
                // offset: 1; size: 8; IEEE 754 floating-point value
7621
                $value = self::extractNumber(substr($valueData, 1, 8));
7622
                $size = 9;
7623
7624
                break;
7625
            case 0x02: // string value
7626
                // offset: 1; size: var; Unicode string, 16-bit string length
7627
                $string = self::readUnicodeStringLong(substr($valueData, 1));
7628
                $value = '"' . $string['value'] . '"';
7629
                $size = 1 + $string['size'];
7630
7631
                break;
7632
            case 0x04: // boolean
7633
                // offset: 1; size: 1; 0 = FALSE, 1 = TRUE
7634
                if (ord($valueData[1])) {
7635
                    $value = 'TRUE';
7636
                } else {
7637
                    $value = 'FALSE';
7638
                }
7639
                $size = 9;
7640
7641
                break;
7642
            case 0x10: // error code
7643
                // offset: 1; size: 1; error code
7644
                $value = Xls\ErrorCode::lookup(ord($valueData[1]));
7645
                $size = 9;
7646
7647
                break;
7648
            default:
7649
                throw new PhpSpreadsheetException('Unsupported BIFF8 constant');
7650
        }
7651
7652
        return [
7653
            'value' => $value,
7654
            'size' => $size,
7655
        ];
7656
    }
7657
7658
    /**
7659
     * Extract RGB color
7660
     * OpenOffice.org's Documentation of the Microsoft Excel File Format, section 2.5.4.
7661
     *
7662
     * @param string $rgb Encoded RGB value (4 bytes)
7663
     *
7664
     * @return array
7665
     */
7666 22
    private static function readRGB($rgb)
7667
    {
7668
        // offset: 0; size 1; Red component
7669 22
        $r = ord($rgb[0]);
7670
7671
        // offset: 1; size: 1; Green component
7672 22
        $g = ord($rgb[1]);
7673
7674
        // offset: 2; size: 1; Blue component
7675 22
        $b = ord($rgb[2]);
7676
7677
        // HEX notation, e.g. 'FF00FC'
7678 22
        $rgb = sprintf('%02X%02X%02X', $r, $g, $b);
7679
7680 22
        return ['rgb' => $rgb];
7681
    }
7682
7683
    /**
7684
     * Read byte string (8-bit string length)
7685
     * OpenOffice documentation: 2.5.2.
7686
     *
7687
     * @param string $subData
7688
     *
7689
     * @return array
7690
     */
7691 1
    private function readByteStringShort($subData)
7692
    {
7693
        // offset: 0; size: 1; length of the string (character count)
7694 1
        $ln = ord($subData[0]);
7695
7696
        // offset: 1: size: var; character array (8-bit characters)
7697 1
        $value = $this->decodeCodepage(substr($subData, 1, $ln));
7698
7699
        return [
7700 1
            'value' => $value,
7701 1
            'size' => 1 + $ln, // size in bytes of data structure
7702
        ];
7703
    }
7704
7705
    /**
7706
     * Read byte string (16-bit string length)
7707
     * OpenOffice documentation: 2.5.2.
7708
     *
7709
     * @param string $subData
7710
     *
7711
     * @return array
7712
     */
7713 1
    private function readByteStringLong($subData)
7714
    {
7715
        // offset: 0; size: 2; length of the string (character count)
7716 1
        $ln = self::getUInt2d($subData, 0);
7717
7718
        // offset: 2: size: var; character array (8-bit characters)
7719 1
        $value = $this->decodeCodepage(substr($subData, 2));
7720
7721
        //return $string;
7722
        return [
7723 1
            'value' => $value,
7724 1
            'size' => 2 + $ln, // size in bytes of data structure
7725
        ];
7726
    }
7727
7728
    /**
7729
     * Extracts an Excel Unicode short string (8-bit string length)
7730
     * OpenOffice documentation: 2.5.3
7731
     * function will automatically find out where the Unicode string ends.
7732
     *
7733
     * @param string $subData
7734
     *
7735
     * @return array
7736
     */
7737 41
    private static function readUnicodeStringShort($subData)
7738
    {
7739 41
        $value = '';
7740
7741
        // offset: 0: size: 1; length of the string (character count)
7742 41
        $characterCount = ord($subData[0]);
7743
7744 41
        $string = self::readUnicodeString(substr($subData, 1), $characterCount);
7745
7746
        // add 1 for the string length
7747 41
        ++$string['size'];
7748
7749 41
        return $string;
7750
    }
7751
7752
    /**
7753
     * Extracts an Excel Unicode long string (16-bit string length)
7754
     * OpenOffice documentation: 2.5.3
7755
     * this function is under construction, needs to support rich text, and Asian phonetic settings.
7756
     *
7757
     * @param string $subData
7758
     *
7759
     * @return array
7760
     */
7761 37
    private static function readUnicodeStringLong($subData)
7762
    {
7763 37
        $value = '';
7764
7765
        // offset: 0: size: 2; length of the string (character count)
7766 37
        $characterCount = self::getUInt2d($subData, 0);
7767
7768 37
        $string = self::readUnicodeString(substr($subData, 2), $characterCount);
7769
7770
        // add 2 for the string length
7771 37
        $string['size'] += 2;
7772
7773 37
        return $string;
7774
    }
7775
7776
    /**
7777
     * Read Unicode string with no string length field, but with known character count
7778
     * this function is under construction, needs to support rich text, and Asian phonetic settings
7779
     * OpenOffice.org's Documentation of the Microsoft Excel File Format, section 2.5.3.
7780
     *
7781
     * @param string $subData
7782
     * @param int $characterCount
7783
     *
7784
     * @return array
7785
     */
7786 41
    private static function readUnicodeString($subData, $characterCount)
7787
    {
7788 41
        $value = '';
7789
7790
        // offset: 0: size: 1; option flags
7791
        // bit: 0; mask: 0x01; character compression (0 = compressed 8-bit, 1 = uncompressed 16-bit)
7792 41
        $isCompressed = !((0x01 & ord($subData[0])) >> 0);
7793
7794
        // bit: 2; mask: 0x04; Asian phonetic settings
7795 41
        $hasAsian = (0x04) & ord($subData[0]) >> 2;
7796
7797
        // bit: 3; mask: 0x08; Rich-Text settings
7798 41
        $hasRichText = (0x08) & ord($subData[0]) >> 3;
7799
7800
        // offset: 1: size: var; character array
7801
        // this offset assumes richtext and Asian phonetic settings are off which is generally wrong
7802
        // needs to be fixed
7803 41
        $value = self::encodeUTF16(substr($subData, 1, $isCompressed ? $characterCount : 2 * $characterCount), $isCompressed);
7804
7805
        return [
7806 41
            'value' => $value,
7807 41
            'size' => $isCompressed ? 1 + $characterCount : 1 + 2 * $characterCount, // the size in bytes including the option flags
7808
        ];
7809
    }
7810
7811
    /**
7812
     * Convert UTF-8 string to string surounded by double quotes. Used for explicit string tokens in formulas.
7813
     * Example:  hello"world  -->  "hello""world".
7814
     *
7815
     * @param string $value UTF-8 encoded string
7816
     *
7817
     * @return string
7818
     */
7819 1
    private static function UTF8toExcelDoubleQuoted($value)
7820
    {
7821 1
        return '"' . str_replace('"', '""', $value) . '"';
7822
    }
7823
7824
    /**
7825
     * Reads first 8 bytes of a string and return IEEE 754 float.
7826
     *
7827
     * @param string $data Binary string that is at least 8 bytes long
7828
     *
7829
     * @return float
7830
     */
7831 39
    private static function extractNumber($data)
7832
    {
7833 39
        $rknumhigh = self::getInt4d($data, 4);
7834 39
        $rknumlow = self::getInt4d($data, 0);
7835 39
        $sign = ($rknumhigh & 0x80000000) >> 31;
7836 39
        $exp = (($rknumhigh & 0x7ff00000) >> 20) - 1023;
7837 39
        $mantissa = (0x100000 | ($rknumhigh & 0x000fffff));
7838 39
        $mantissalow1 = ($rknumlow & 0x80000000) >> 31;
7839 39
        $mantissalow2 = ($rknumlow & 0x7fffffff);
7840 39
        $value = $mantissa / 2 ** (20 - $exp);
7841
7842 39
        if ($mantissalow1 != 0) {
7843 18
            $value += 1 / 2 ** (21 - $exp);
7844
        }
7845
7846 39
        $value += $mantissalow2 / 2 ** (52 - $exp);
7847 39
        if ($sign) {
7848 11
            $value *= -1;
7849
        }
7850
7851 39
        return $value;
7852
    }
7853
7854
    /**
7855
     * @param int $rknum
7856
     *
7857
     * @return float
7858
     */
7859 18
    private static function getIEEE754($rknum)
7860
    {
7861 18
        if (($rknum & 0x02) != 0) {
7862 2
            $value = $rknum >> 2;
7863
        } else {
7864
            // changes by mmp, info on IEEE754 encoding from
7865
            // research.microsoft.com/~hollasch/cgindex/coding/ieeefloat.html
7866
            // The RK format calls for using only the most significant 30 bits
7867
            // of the 64 bit floating point value. The other 34 bits are assumed
7868
            // to be 0 so we use the upper 30 bits of $rknum as follows...
7869 16
            $sign = ($rknum & 0x80000000) >> 31;
7870 16
            $exp = ($rknum & 0x7ff00000) >> 20;
7871 16
            $mantissa = (0x100000 | ($rknum & 0x000ffffc));
7872 16
            $value = $mantissa / 2 ** (20 - ($exp - 1023));
7873 16
            if ($sign) {
7874 10
                $value = -1 * $value;
7875
            }
7876
            //end of changes by mmp
7877
        }
7878 18
        if (($rknum & 0x01) != 0) {
7879 10
            $value /= 100;
7880
        }
7881
7882 18
        return $value;
7883
    }
7884
7885
    /**
7886
     * Get UTF-8 string from (compressed or uncompressed) UTF-16 string.
7887
     *
7888
     * @param string $string
7889
     * @param bool $compressed
7890
     *
7891
     * @return string
7892
     */
7893 41
    private static function encodeUTF16($string, $compressed = false)
7894
    {
7895 41
        if ($compressed) {
7896 25
            $string = self::uncompressByteString($string);
7897
        }
7898
7899 41
        return StringHelper::convertEncoding($string, 'UTF-8', 'UTF-16LE');
7900
    }
7901
7902
    /**
7903
     * Convert UTF-16 string in compressed notation to uncompressed form. Only used for BIFF8.
7904
     *
7905
     * @param string $string
7906
     *
7907
     * @return string
7908
     */
7909 25
    private static function uncompressByteString($string)
7910
    {
7911 25
        $uncompressedString = '';
7912 25
        $strLen = strlen($string);
7913 25
        for ($i = 0; $i < $strLen; ++$i) {
7914 25
            $uncompressedString .= $string[$i] . "\0";
7915
        }
7916
7917 25
        return $uncompressedString;
7918
    }
7919
7920
    /**
7921
     * Convert string to UTF-8. Only used for BIFF5.
7922
     *
7923
     * @param string $string
7924
     *
7925
     * @return string
7926
     */
7927 1
    private function decodeCodepage($string)
7928
    {
7929 1
        return StringHelper::convertEncoding($string, 'UTF-8', $this->codepage);
7930
    }
7931
7932
    /**
7933
     * Read 16-bit unsigned integer.
7934
     *
7935
     * @param string $data
7936
     * @param int $pos
7937
     *
7938
     * @return int
7939
     */
7940 42
    public static function getUInt2d($data, $pos)
7941
    {
7942 42
        return ord($data[$pos]) | (ord($data[$pos + 1]) << 8);
7943
    }
7944
7945
    /**
7946
     * Read 16-bit signed integer.
7947
     *
7948
     * @param string $data
7949
     * @param int $pos
7950
     *
7951
     * @return int
7952
     */
7953
    public static function getInt2d($data, $pos)
7954
    {
7955
        return unpack('s', $data[$pos] . $data[$pos + 1])[1];
7956
    }
7957
7958
    /**
7959
     * Read 32-bit signed integer.
7960
     *
7961
     * @param string $data
7962
     * @param int $pos
7963
     *
7964
     * @return int
7965
     */
7966 42
    public static function getInt4d($data, $pos)
7967
    {
7968
        // FIX: represent numbers correctly on 64-bit system
7969
        // http://sourceforge.net/tracker/index.php?func=detail&aid=1487372&group_id=99160&atid=623334
7970
        // Changed by Andreas Rehm 2006 to ensure correct result of the <<24 block on 32 and 64bit systems
7971 42
        $_or_24 = ord($data[$pos + 3]);
7972 42
        if ($_or_24 >= 128) {
7973
            // negative number
7974 18
            $_ord_24 = -abs((256 - $_or_24) << 24);
7975
        } else {
7976 42
            $_ord_24 = ($_or_24 & 127) << 24;
7977
        }
7978
7979 42
        return ord($data[$pos]) | (ord($data[$pos + 1]) << 8) | (ord($data[$pos + 2]) << 16) | $_ord_24;
7980
    }
7981
7982 3
    private function parseRichText($is)
7983
    {
7984 3
        $value = new RichText();
7985 3
        $value->createText($is);
7986
7987 3
        return $value;
7988
    }
7989
}
7990