Xls::loadOLE()   A
last analyzed

Complexity

Conditions 1
Paths 1

Size

Total Lines 12
Code Lines 5

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 6
CRAP Score 1

Importance

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