Passed
Pull Request — master (#4380)
by Owen
11:28
created

Xls::readSummaryInformation()   F

Complexity

Conditions 28
Paths 142

Size

Total Lines 140
Code Lines 78

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 71
CRAP Score 28.3706

Importance

Changes 0
Metric Value
eloc 78
c 0
b 0
f 0
dl 0
loc 140
ccs 71
cts 77
cp 0.9221
rs 3.8166
cc 28
nc 142
nop 0
crap 28.3706

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