Failed Conditions
Pull Request — master (#4118)
by Owen
13:48
created

Xls::readDataValidation()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 3
Code Lines 1

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 2
CRAP Score 1

Importance

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