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

Xls::getNextToken()   F

Complexity

Conditions 333

Size

Total Lines 1580
Code Lines 1226

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 168
CRAP Score 71209.6315

Importance

Changes 0
Metric Value
cc 333
eloc 1226
c 0
b 0
f 0
nop 2
dl 0
loc 1580
rs 3.3333
ccs 168
cts 1212
cp 0.1386
crap 71209.6315

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

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

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

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

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