Failed Conditions
Push — master ( 92389c...96e843 )
by Adrien
26:46 queued 17:47
created

Xls::readWindow2()   C

Complexity

Conditions 11
Paths 238

Size

Total Lines 75
Code Lines 36

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 33
CRAP Score 11.0225

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 11
eloc 36
c 1
b 0
f 0
nc 238
nop 0
dl 0
loc 75
ccs 33
cts 35
cp 0.9429
crap 11.0225
rs 6.0083

How to fix   Long Method    Complexity   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

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