Completed
Push — master ( b636c5...3fc2fa )
by Adrien
35:37 queued 24:00
created

Xls::createFormulaFromTokens()   F

Complexity

Conditions 57
Paths 60

Size

Total Lines 165
Code Lines 129

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 78
CRAP Score 257.692

Importance

Changes 0
Metric Value
cc 57
eloc 129
c 0
b 0
f 0
nc 60
nop 2
dl 0
loc 165
ccs 78
cts 129
cp 0.6047
crap 257.692
rs 3.3333

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