• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

PHPOffice / PhpSpreadsheet / 28790909368

06 Jul 2026 12:17PM UTC coverage: 97.18% (+0.01%) from 97.167%
28790909368

Pull #4833

github

web-flow
Merge 5db3842d9 into 0577c7488
Pull Request #4833: Optimize XLS (BIFF8) reader performance with reduced per-cell overhead

63 of 66 new or added lines in 3 files covered. (95.45%)

48379 of 49783 relevant lines covered (97.18%)

386.42 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

89.04
/src/PhpSpreadsheet/Reader/Xls.php
1
<?php
2

3
namespace PhpOffice\PhpSpreadsheet\Reader;
4

5
use Composer\Pcre\Preg;
6
use PhpOffice\PhpSpreadsheet\Cell\AddressRange;
7
use PhpOffice\PhpSpreadsheet\Cell\Coordinate;
8
use PhpOffice\PhpSpreadsheet\Cell\DataType;
9
use PhpOffice\PhpSpreadsheet\Cell\DataValidation;
10
use PhpOffice\PhpSpreadsheet\Exception as PhpSpreadsheetException;
11
use PhpOffice\PhpSpreadsheet\Reader\Xls\Style\CellFont;
12
use PhpOffice\PhpSpreadsheet\Reader\Xls\Style\FillPattern;
13
use PhpOffice\PhpSpreadsheet\RichText\RichText;
14
use PhpOffice\PhpSpreadsheet\Shared\CodePage;
15
use PhpOffice\PhpSpreadsheet\Shared\Date;
16
use PhpOffice\PhpSpreadsheet\Shared\Escher;
17
use PhpOffice\PhpSpreadsheet\Shared\File;
18
use PhpOffice\PhpSpreadsheet\Shared\OLE;
19
use PhpOffice\PhpSpreadsheet\Shared\OLERead;
20
use PhpOffice\PhpSpreadsheet\Shared\StringHelper;
21
use PhpOffice\PhpSpreadsheet\Spreadsheet;
22
use PhpOffice\PhpSpreadsheet\Style\Alignment;
23
use PhpOffice\PhpSpreadsheet\Style\Border;
24
use PhpOffice\PhpSpreadsheet\Style\Borders;
25
use PhpOffice\PhpSpreadsheet\Style\Conditional;
26
use PhpOffice\PhpSpreadsheet\Style\Fill;
27
use PhpOffice\PhpSpreadsheet\Style\Font;
28
use PhpOffice\PhpSpreadsheet\Style\NumberFormat;
29
use PhpOffice\PhpSpreadsheet\Style\Protection;
30
use PhpOffice\PhpSpreadsheet\Style\Style;
31
use PhpOffice\PhpSpreadsheet\Worksheet\PageSetup;
32
use PhpOffice\PhpSpreadsheet\Worksheet\SheetView;
33
use PhpOffice\PhpSpreadsheet\Worksheet\Worksheet;
34

35
// Original file header of ParseXL (used as the base for this class):
36
// --------------------------------------------------------------------------------
37
// Adapted from Excel_Spreadsheet_Reader developed by users bizon153,
38
// trex005, and mmp11 (SourceForge.net)
39
// https://sourceforge.net/projects/phpexcelreader/
40
// Primary changes made by canyoncasa (dvc) for ParseXL 1.00 ...
41
//     Modelled moreso after Perl Excel Parse/Write modules
42
//     Added Parse_Excel_Spreadsheet object
43
//         Reads a whole worksheet or tab as row,column array or as
44
//         associated hash of indexed rows and named column fields
45
//     Added variables for worksheet (tab) indexes and names
46
//     Added an object call for loading individual woorksheets
47
//     Changed default indexing defaults to 0 based arrays
48
//     Fixed date/time and percent formats
49
//     Includes patches found at SourceForge...
50
//         unicode patch by nobody
51
//         unpack("d") machine depedency patch by matchy
52
//         boundsheet utf16 patch by bjaenichen
53
//     Renamed functions for shorter names
54
//     General code cleanup and rigor, including <80 column width
55
//     Included a testcase Excel file and PHP example calls
56
//     Code works for PHP 5.x
57

58
// Primary changes made by canyoncasa (dvc) for ParseXL 1.10 ...
59
// http://sourceforge.net/tracker/index.php?func=detail&aid=1466964&group_id=99160&atid=623334
60
//     Decoding of formula conditions, results, and tokens.
61
//     Support for user-defined named cells added as an array "namedcells"
62
//         Patch code for user-defined named cells supports single cells only.
63
//         NOTE: this patch only works for BIFF8 as BIFF5-7 use a different
64
//         external sheet reference structure
65
class Xls extends XlsBase
66
{
67
    /**
68
     * Summary Information stream data.
69
     */
70
    protected ?string $summaryInformation = null;
71

72
    /**
73
     * Extended Summary Information stream data.
74
     */
75
    protected ?string $documentSummaryInformation = null;
76

77
    /**
78
     * Workbook stream data. (Includes workbook globals substream as well as sheet substreams).
79
     */
80
    protected string $data;
81

82
    /**
83
     * Size in bytes of $this->data.
84
     */
85
    protected int $dataSize;
86

87
    /**
88
     * Current position in stream.
89
     */
90
    protected int $pos;
91

92
    /**
93
     * Workbook to be returned by the reader.
94
     */
95
    protected Spreadsheet $spreadsheet;
96

97
    /**
98
     * Worksheet that is currently being built by the reader.
99
     */
100
    protected Worksheet $phpSheet;
101

102
    /**
103
     * Cached sheet title for the current sheet being parsed.
104
     * Avoids repeated getTitle() calls in per-cell read filter checks.
105
     */
106
    protected string $phpSheetTitle = '';
107

108
    /**
109
     * BIFF version.
110
     */
111
    protected int $version = 0;
112

113
    /**
114
     * Shared formats.
115
     *
116
     * @var mixed[]
117
     */
118
    protected array $formats;
119

120
    /**
121
     * Shared fonts.
122
     *
123
     * @var Font[]
124
     */
125
    protected array $objFonts;
126

127
    /**
128
     * Color palette.
129
     *
130
     * @var string[][]
131
     */
132
    protected array $palette;
133

134
    /**
135
     * Worksheets.
136
     *
137
     * @var array<array{name: string, offset: int, sheetState: string, sheetType: int|string}>
138
     */
139
    protected array $sheets;
140

141
    /**
142
     * External books.
143
     *
144
     * @var mixed[][]
145
     */
146
    protected array $externalBooks;
147

148
    /**
149
     * REF structures. Only applies to BIFF8.
150
     *
151
     * @var array<int, array{'externalBookIndex': int, 'firstSheetIndex': int, 'lastSheetIndex': int}>
152
     */
153
    protected array $ref;
154

155
    /**
156
     * External names.
157
     *
158
     * @var array<array<string, mixed>|string>
159
     */
160
    protected array $externalNames;
161

162
    /**
163
     * Defined names.
164
     *
165
     * @var array<int, array{isBuiltInName: int, name: string, formula: string, scope: int}>
166
     */
167
    protected array $definedname;
168

169
    /**
170
     * Shared strings. Only applies to BIFF8.
171
     *
172
     * @var array<array{value: string, fmtRuns: mixed[]}>
173
     */
174
    protected array $sst;
175

176
    /**
177
     * Panes are frozen? (in sheet currently being read). See WINDOW2 record.
178
     */
179
    protected bool $frozen;
180

181
    /**
182
     * Fit printout to number of pages? (in sheet currently being read). See SHEETPR record.
183
     */
184
    protected bool $isFitToPages;
185

186
    /**
187
     * Objects. One OBJ record contributes with one entry.
188
     *
189
     * @var mixed[]
190
     */
191
    protected array $objs;
192

193
    /**
194
     * Text Objects. One TXO record corresponds with one entry.
195
     *
196
     * @var array<array{text: string, format: string, alignment: int, rotation: int}>
197
     */
198
    protected array $textObjects;
199

200
    /**
201
     * Cell Annotations (BIFF8).
202
     *
203
     * @var mixed[]
204
     */
205
    protected array $cellNotes;
206

207
    /**
208
     * The combined MSODRAWINGGROUP data.
209
     */
210
    protected string $drawingGroupData;
211

212
    /**
213
     * The combined MSODRAWING data (per sheet).
214
     */
215
    protected string $drawingData;
216

217
    /**
218
     * Keep track of XF index.
219
     */
220
    protected int $xfIndex;
221

222
    /**
223
     * Mapping of XF index (that is a cell XF) to final index in cellXf collection.
224
     *
225
     * @var int[]
226
     */
227
    protected array $mapCellXfIndex;
228

229
    /**
230
     * Mapping of XF index (that is a style XF) to final index in cellStyleXf collection.
231
     *
232
     * @var int[]
233
     */
234
    protected array $mapCellStyleXfIndex;
235

236
    /**
237
     * The shared formulas in a sheet. One SHAREDFMLA record contributes with one value.
238
     *
239
     * @var mixed[]
240
     */
241
    protected array $sharedFormulas;
242

243
    /**
244
     * The shared formula parts in a sheet. One FORMULA record contributes with one value if it
245
     * refers to a shared formula.
246
     *
247
     * @var mixed[]
248
     */
249
    protected array $sharedFormulaParts;
250

251
    /**
252
     * The type of encryption in use.
253
     */
254
    protected int $encryption = 0;
255

256
    /**
257
     * The position in the stream after which contents are encrypted.
258
     */
259
    protected int $encryptionStartPos = 0;
260

261
    protected string $encryptionPassword = 'VelvetSweatshop';
262

263
    /**
264
     * The current RC4 decryption object.
265
     */
266
    protected ?Xls\RC4 $rc4Key = null;
267

268
    /**
269
     * The position in the stream that the RC4 decryption object was left at.
270
     */
271
    protected int $rc4Pos = 0;
272

273
    /**
274
     * The current MD5 context state.
275
     * It is set via call-by-reference to verifyPassword.
276
     */
277
    private string $md5Ctxt = '';
278

279
    protected int $textObjRef;
280

281
    protected string $baseCell;
282

283
    protected bool $activeSheetSet = false;
284

285
    /**
286
     * Reads names of the worksheets from a file, without parsing the whole file to a PhpSpreadsheet object.
287
     *
288
     * @return string[]
289
     */
290
    public function listWorksheetNames(string $filename): array
7✔
291
    {
292
        return (new Xls\ListFunctions())->listWorksheetNames2($filename, $this);
7✔
293
    }
294

295
    /**
296
     * Return worksheet info (Name, Last Column Letter, Last Column Index, Total Rows, Total Columns).
297
     *
298
     * @return array<int, array{worksheetName: string, lastColumnLetter: string, lastColumnIndex: int, totalRows: int, totalColumns: int, sheetState: string}>
299
     */
300
    public function listWorksheetInfo(string $filename): array
7✔
301
    {
302
        return (new Xls\ListFunctions())->listWorksheetInfo2($filename, $this);
7✔
303
    }
304

305
    /**
306
     * Return worksheet info (Name, Last Column Letter, Last Column Index, Total Rows, Total Columns).
307
     *
308
     * @return array<int, array{worksheetName: string, dimensionsMinR: int, dimensionsMinC: int, dimensionsMaxR: int, dimensionsMaxC: int, lastColumnLetter: string}>
309
     */
310
    public function listWorksheetDimensions(string $filename): array
2✔
311
    {
312
        return (new Xls\ListFunctions())->listWorksheetDimensions2($filename, $this);
2✔
313
    }
314

315
    /**
316
     * Loads PhpSpreadsheet from file.
317
     */
318
    protected function loadSpreadsheetFromFile(string $filename): Spreadsheet
142✔
319
    {
320
        return (new Xls\LoadSpreadsheet())->loadSpreadsheetFromFile2($filename, $this);
142✔
321
    }
322

323
    /**
324
     * Read record data from stream, decrypting as required.
325
     *
326
     * @param string $data Data stream to read from
327
     * @param int $pos Position to start reading from
328
     * @param int $len Record data length
329
     *
330
     * @return string Record data
331
     */
332
    protected function readRecordData(string $data, int $pos, int $len): string
154✔
333
    {
334
        $data = substr($data, $pos, $len);
154✔
335

336
        // File not encrypted, or record before encryption start point
337
        if ($this->encryption == self::MS_BIFF_CRYPTO_NONE || $pos < $this->encryptionStartPos) {
154✔
338
            return $data;
154✔
339
        }
340

341
        $recordData = '';
2✔
342
        if ($this->encryption == self::MS_BIFF_CRYPTO_RC4) {
2✔
343
            $oldBlock = floor($this->rc4Pos / self::REKEY_BLOCK);
2✔
344
            $block = (int) floor($pos / self::REKEY_BLOCK);
2✔
345
            $endBlock = (int) floor(($pos + $len) / self::REKEY_BLOCK);
2✔
346

347
            // Spin an RC4 decryptor to the right spot. If we have a decryptor sitting
348
            // at a point earlier in the current block, re-use it as we can save some time.
349
            if ($block != $oldBlock || $pos < $this->rc4Pos || !$this->rc4Key) {
2✔
350
                $this->rc4Key = $this->makeKey($block, $this->md5Ctxt);
2✔
351
                $step = $pos % self::REKEY_BLOCK;
2✔
352
            } else {
353
                $step = $pos - $this->rc4Pos;
2✔
354
            }
355
            $this->rc4Key->RC4(str_repeat("\0", $step));
2✔
356

357
            // Decrypt record data (re-keying at the end of every block)
358
            while ($block != $endBlock) {
2✔
359
                $step = self::REKEY_BLOCK - ($pos % self::REKEY_BLOCK);
1✔
360
                $recordData .= $this->rc4Key->RC4(substr($data, 0, $step));
1✔
361
                $data = substr($data, $step);
1✔
362
                $pos += $step;
1✔
363
                $len -= $step;
1✔
364
                ++$block;
1✔
365
                $this->rc4Key = $this->makeKey($block, $this->md5Ctxt);
1✔
366
            }
367
            $recordData .= $this->rc4Key->RC4(substr($data, 0, $len));
2✔
368

369
            // Keep track of the position of this decryptor.
370
            // We'll try and re-use it later if we can to speed things up
371
            $this->rc4Pos = $pos + $len;
2✔
372
        } elseif ($this->encryption == self::MS_BIFF_CRYPTO_XOR) {
×
373
            throw new Exception('XOr encryption not supported');
×
374
        }
375

376
        return $recordData;
2✔
377
    }
378

379
    /**
380
     * Use OLE reader to extract the relevant data streams from the OLE file.
381
     */
382
    protected function loadOLE(string $filename): void
155✔
383
    {
384
        // OLE reader
385
        $ole = new OLERead();
155✔
386
        // get excel data,
387
        $ole->read($filename);
155✔
388
        // Get workbook data: workbook stream + sheet streams
389
        $this->data = $ole->getStream($ole->wrkbook); // @phpstan-ignore-line
154✔
390
        // Get summary information data
391
        $this->summaryInformation = $ole->getStream($ole->summaryInformation);
154✔
392
        // Get additional document summary information data
393
        $this->documentSummaryInformation = $ole->getStream($ole->documentSummaryInformation);
154✔
394
    }
395

396
    /**
397
     * Read summary information.
398
     */
399
    protected function readSummaryInformation(): void
141✔
400
    {
401
        if (!isset($this->summaryInformation)) {
141✔
402
            return;
3✔
403
        }
404

405
        // offset: 0; size: 2; must be 0xFE 0xFF (UTF-16 LE byte order mark)
406
        // offset: 2; size: 2;
407
        // offset: 4; size: 2; OS version
408
        // offset: 6; size: 2; OS indicator
409
        // offset: 8; size: 16
410
        // offset: 24; size: 4; section count
411
        //$secCount = self::getInt4d($this->summaryInformation, 24);
412

413
        // 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
414
        // offset: 44; size: 4
415
        $secOffset = self::getInt4d($this->summaryInformation, 44);
138✔
416

417
        // section header
418
        // offset: $secOffset; size: 4; section length
419
        //$secLength = self::getInt4d($this->summaryInformation, $secOffset);
420

421
        // offset: $secOffset+4; size: 4; property count
422
        $countProperties = self::getInt4d($this->summaryInformation, $secOffset + 4);
138✔
423

424
        // initialize code page (used to resolve string values)
425
        $codePage = 'CP1252';
138✔
426

427
        // offset: ($secOffset+8); size: var
428
        // loop through property decarations and properties
429
        for ($i = 0; $i < $countProperties; ++$i) {
138✔
430
            // offset: ($secOffset+8) + (8 * $i); size: 4; property ID
431
            $id = self::getInt4d($this->summaryInformation, ($secOffset + 8) + (8 * $i));
138✔
432

433
            // Use value of property id as appropriate
434
            // offset: ($secOffset+12) + (8 * $i); size: 4; offset from beginning of section (48)
435
            $offset = self::getInt4d($this->summaryInformation, ($secOffset + 12) + (8 * $i));
138✔
436

437
            $type = self::getInt4d($this->summaryInformation, $secOffset + $offset);
138✔
438

439
            // initialize property value
440
            $value = null;
138✔
441

442
            // extract property value based on property type
443
            switch ($type) {
444
                case 0x02: // 2 byte signed integer
138✔
445
                    $value = self::getUInt2d($this->summaryInformation, $secOffset + 4 + $offset);
138✔
446

447
                    break;
138✔
448
                case 0x03: // 4 byte signed integer
138✔
449
                    $value = self::getInt4d($this->summaryInformation, $secOffset + 4 + $offset);
134✔
450

451
                    break;
134✔
452
                case 0x13: // 4 byte unsigned integer
138✔
453
                    // not needed yet, fix later if necessary
454
                    break;
1✔
455
                case 0x1E: // null-terminated string prepended by dword string length
138✔
456
                    $byteLength = self::getInt4d($this->summaryInformation, $secOffset + 4 + $offset);
136✔
457
                    $value = substr($this->summaryInformation, $secOffset + 8 + $offset, $byteLength);
136✔
458
                    $value = StringHelper::convertEncoding($value, 'UTF-8', $codePage);
136✔
459
                    $value = rtrim($value);
136✔
460

461
                    break;
136✔
462
                case 0x40: // Filetime (64-bit value representing the number of 100-nanosecond intervals since January 1, 1601)
138✔
463
                    // PHP-time
464
                    $value = OLE::OLE2LocalDate(substr($this->summaryInformation, $secOffset + 4 + $offset, 8));
138✔
465

466
                    break;
138✔
467
                case 0x47: // Clipboard format
2✔
468
                    // not needed yet, fix later if necessary
469
                    break;
×
470
            }
471

472
            switch ($id) {
473
                case 0x01:    //    Code Page
138✔
474
                    $codePage = CodePage::numberToName((int) $value);
138✔
475

476
                    break;
138✔
477
                case 0x02:    //    Title
138✔
478
                    $this->spreadsheet->getProperties()->setTitle("$value");
82✔
479

480
                    break;
82✔
481
                case 0x03:    //    Subject
138✔
482
                    $this->spreadsheet->getProperties()->setSubject("$value");
17✔
483

484
                    break;
17✔
485
                case 0x04:    //    Author (Creator)
138✔
486
                    $this->spreadsheet->getProperties()->setCreator("$value");
124✔
487

488
                    break;
124✔
489
                case 0x05:    //    Keywords
138✔
490
                    $this->spreadsheet->getProperties()->setKeywords("$value");
17✔
491

492
                    break;
17✔
493
                case 0x06:    //    Comments (Description)
138✔
494
                    $this->spreadsheet->getProperties()->setDescription("$value");
17✔
495

496
                    break;
17✔
497
                case 0x07:    //    Template
138✔
498
                    //    Not supported by PhpSpreadsheet
499
                    break;
×
500
                case 0x08:    //    Last Saved By (LastModifiedBy)
138✔
501
                    $this->spreadsheet->getProperties()->setLastModifiedBy("$value");
136✔
502

503
                    break;
136✔
504
                case 0x09:    //    Revision
138✔
505
                    //    Not supported by PhpSpreadsheet
506
                    break;
3✔
507
                case 0x0A:    //    Total Editing Time
138✔
508
                    //    Not supported by PhpSpreadsheet
509
                    break;
3✔
510
                case 0x0B:    //    Last Printed
138✔
511
                    //    Not supported by PhpSpreadsheet
512
                    break;
7✔
513
                case 0x0C:    //    Created Date/Time
138✔
514
                    $this->spreadsheet->getProperties()->setCreated($value);
131✔
515

516
                    break;
131✔
517
                case 0x0D:    //    Modified Date/Time
138✔
518
                    $this->spreadsheet->getProperties()->setModified($value);
137✔
519

520
                    break;
137✔
521
                case 0x0E:    //    Number of Pages
135✔
522
                    //    Not supported by PhpSpreadsheet
523
                    break;
×
524
                case 0x0F:    //    Number of Words
135✔
525
                    //    Not supported by PhpSpreadsheet
526
                    break;
×
527
                case 0x10:    //    Number of Characters
135✔
528
                    //    Not supported by PhpSpreadsheet
529
                    break;
×
530
                case 0x11:    //    Thumbnail
135✔
531
                    //    Not supported by PhpSpreadsheet
532
                    break;
×
533
                case 0x12:    //    Name of creating application
135✔
534
                    //    Not supported by PhpSpreadsheet
535
                    break;
50✔
536
                case 0x13:    //    Security
135✔
537
                    //    Not supported by PhpSpreadsheet
538
                    break;
134✔
539
            }
540
        }
541
    }
542

543
    /**
544
     * Read additional document summary information.
545
     */
546
    protected function readDocumentSummaryInformation(): void
141✔
547
    {
548
        if (!isset($this->documentSummaryInformation)) {
141✔
549
            return;
4✔
550
        }
551

552
        //    offset: 0;    size: 2;    must be 0xFE 0xFF (UTF-16 LE byte order mark)
553
        //    offset: 2;    size: 2;
554
        //    offset: 4;    size: 2;    OS version
555
        //    offset: 6;    size: 2;    OS indicator
556
        //    offset: 8;    size: 16
557
        //    offset: 24;    size: 4;    section count
558
        //$secCount = self::getInt4d($this->documentSummaryInformation, 24);
559

560
        // 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
561
        // offset: 44;    size: 4;    first section offset
562
        $secOffset = self::getInt4d($this->documentSummaryInformation, 44);
137✔
563

564
        //    section header
565
        //    offset: $secOffset;    size: 4;    section length
566
        //$secLength = self::getInt4d($this->documentSummaryInformation, $secOffset);
567

568
        //    offset: $secOffset+4;    size: 4;    property count
569
        $countProperties = self::getInt4d($this->documentSummaryInformation, $secOffset + 4);
137✔
570

571
        // initialize code page (used to resolve string values)
572
        $codePage = 'CP1252';
137✔
573

574
        //    offset: ($secOffset+8);    size: var
575
        //    loop through property decarations and properties
576
        for ($i = 0; $i < $countProperties; ++$i) {
137✔
577
            //    offset: ($secOffset+8) + (8 * $i);    size: 4;    property ID
578
            $id = self::getInt4d($this->documentSummaryInformation, ($secOffset + 8) + (8 * $i));
137✔
579

580
            // Use value of property id as appropriate
581
            // offset: 60 + 8 * $i;    size: 4;    offset from beginning of section (48)
582
            $offset = self::getInt4d($this->documentSummaryInformation, ($secOffset + 12) + (8 * $i));
137✔
583

584
            $type = self::getInt4d($this->documentSummaryInformation, $secOffset + $offset);
137✔
585

586
            // initialize property value
587
            $value = null;
137✔
588

589
            // extract property value based on property type
590
            switch ($type) {
591
                case 0x02:    //    2 byte signed integer
137✔
592
                    $value = self::getUInt2d($this->documentSummaryInformation, $secOffset + 4 + $offset);
137✔
593

594
                    break;
137✔
595
                case 0x03:    //    4 byte signed integer
134✔
596
                    $value = self::getInt4d($this->documentSummaryInformation, $secOffset + 4 + $offset);
131✔
597

598
                    break;
131✔
599
                case 0x0B:  // Boolean
134✔
600
                    $value = self::getUInt2d($this->documentSummaryInformation, $secOffset + 4 + $offset);
134✔
601
                    $value = ($value == 0 ? false : true);
134✔
602

603
                    break;
134✔
604
                case 0x13:    //    4 byte unsigned integer
133✔
605
                    // not needed yet, fix later if necessary
606
                    break;
1✔
607
                case 0x1E:    //    null-terminated string prepended by dword string length
132✔
608
                    $byteLength = self::getInt4d($this->documentSummaryInformation, $secOffset + 4 + $offset);
52✔
609
                    $value = substr($this->documentSummaryInformation, $secOffset + 8 + $offset, $byteLength);
52✔
610
                    $value = StringHelper::convertEncoding($value, 'UTF-8', $codePage);
52✔
611
                    $value = rtrim($value);
52✔
612

613
                    break;
52✔
614
                case 0x40:    //    Filetime (64-bit value representing the number of 100-nanosecond intervals since January 1, 1601)
132✔
615
                    // PHP-Time
616
                    $value = OLE::OLE2LocalDate(substr($this->documentSummaryInformation, $secOffset + 4 + $offset, 8));
×
617

618
                    break;
×
619
                case 0x47:    //    Clipboard format
132✔
620
                    // not needed yet, fix later if necessary
621
                    break;
×
622
            }
623

624
            switch ($id) {
625
                case 0x01:    //    Code Page
137✔
626
                    $codePage = CodePage::numberToName((int) $value);
137✔
627

628
                    break;
137✔
629
                case 0x02:    //    Category
134✔
630
                    $this->spreadsheet->getProperties()->setCategory("$value");
17✔
631

632
                    break;
17✔
633
                case 0x03:    //    Presentation Target
134✔
634
                    //    Not supported by PhpSpreadsheet
635
                    break;
×
636
                case 0x04:    //    Bytes
134✔
637
                    //    Not supported by PhpSpreadsheet
638
                    break;
×
639
                case 0x05:    //    Lines
134✔
640
                    //    Not supported by PhpSpreadsheet
641
                    break;
×
642
                case 0x06:    //    Paragraphs
134✔
643
                    //    Not supported by PhpSpreadsheet
644
                    break;
×
645
                case 0x07:    //    Slides
134✔
646
                    //    Not supported by PhpSpreadsheet
647
                    break;
×
648
                case 0x08:    //    Notes
134✔
649
                    //    Not supported by PhpSpreadsheet
650
                    break;
×
651
                case 0x09:    //    Hidden Slides
134✔
652
                    //    Not supported by PhpSpreadsheet
653
                    break;
×
654
                case 0x0A:    //    MM Clips
134✔
655
                    //    Not supported by PhpSpreadsheet
656
                    break;
×
657
                case 0x0B:    //    Scale Crop
134✔
658
                    //    Not supported by PhpSpreadsheet
659
                    break;
134✔
660
                case 0x0C:    //    Heading Pairs
134✔
661
                    //    Not supported by PhpSpreadsheet
662
                    break;
132✔
663
                case 0x0D:    //    Titles of Parts
134✔
664
                    //    Not supported by PhpSpreadsheet
665
                    break;
132✔
666
                case 0x0E:    //    Manager
134✔
667
                    $this->spreadsheet->getProperties()->setManager("$value");
2✔
668

669
                    break;
2✔
670
                case 0x0F:    //    Company
134✔
671
                    $this->spreadsheet->getProperties()->setCompany("$value");
42✔
672

673
                    break;
42✔
674
                case 0x10:    //    Links up-to-date
134✔
675
                    //    Not supported by PhpSpreadsheet
676
                    break;
134✔
677
            }
678
        }
679
    }
680

681
    /**
682
     * Reads a general type of BIFF record. Does nothing except for moving stream pointer forward to next record.
683
     */
684
    protected function readDefault(): void
152✔
685
    {
686
        $length = self::getUInt2d($this->data, $this->pos + 2);
152✔
687

688
        // move stream pointer to next record
689
        $this->pos += 4 + $length;
152✔
690
    }
691

692
    /**
693
     *    The NOTE record specifies a comment associated with a particular cell. In Excel 95 (BIFF7) and earlier versions,
694
     *        this record stores a note (cell note). This feature was significantly enhanced in Excel 97.
695
     */
696
    protected function readNote(): void
3✔
697
    {
698
        $length = self::getUInt2d($this->data, $this->pos + 2);
3✔
699
        $recordData = $this->readRecordData($this->data, $this->pos + 4, $length);
3✔
700

701
        // move stream pointer to next record
702
        $this->pos += 4 + $length;
3✔
703

704
        if ($this->readDataOnly) {
3✔
705
            return;
×
706
        }
707

708
        $cellAddress = Xls\Biff8::readBIFF8CellAddress(substr($recordData, 0, 4));
3✔
709
        if ($this->version == self::XLS_BIFF8) {
3✔
710
            $noteObjID = self::getUInt2d($recordData, 6);
2✔
711
            $noteAuthor = self::readUnicodeStringLong(substr($recordData, 8));
2✔
712
            $noteAuthor = $noteAuthor['value'];
2✔
713
            $this->cellNotes[$noteObjID] = [
2✔
714
                'cellRef' => $cellAddress,
2✔
715
                'objectID' => $noteObjID,
2✔
716
                'author' => $noteAuthor,
2✔
717
            ];
2✔
718
        } else {
719
            $extension = false;
1✔
720
            if ($cellAddress === '$B$' . AddressRange::MAX_ROW_XLS) {
1✔
721
                //    If the address row is -1 and the column is 0, (which translates as $B$65536) then this is a continuation
722
                //        note from the previous cell annotation. We're not yet handling this, so annotations longer than the
723
                //        max 2048 bytes will probably throw a wobbly.
724
                //$row = self::getUInt2d($recordData, 0);
725
                $extension = true;
×
726
                $arrayKeys = array_keys($this->phpSheet->getComments());
×
727
                $cellAddress = array_pop($arrayKeys);
×
728
            }
729

730
            $cellAddress = str_replace('$', '', (string) $cellAddress);
1✔
731
            //$noteLength = self::getUInt2d($recordData, 4);
732
            $noteText = trim(substr($recordData, 6));
1✔
733

734
            if ($extension) {
1✔
735
                //    Concatenate this extension with the currently set comment for the cell
736
                $comment = $this->phpSheet->getComment($cellAddress);
×
737
                $commentText = $comment->getText()->getPlainText();
×
738
                $comment->setText($this->parseRichText($commentText . $noteText));
×
739
            } else {
740
                //    Set comment for the cell
741
                $this->phpSheet->getComment($cellAddress)->setText($this->parseRichText($noteText));
1✔
742
//                                                    ->setAuthor($author)
743
            }
744
        }
745
    }
746

747
    /**
748
     * The TEXT Object record contains the text associated with a cell annotation.
749
     */
750
    protected function readTextObject(): void
2✔
751
    {
752
        $length = self::getUInt2d($this->data, $this->pos + 2);
2✔
753
        $recordData = $this->readRecordData($this->data, $this->pos + 4, $length);
2✔
754

755
        // move stream pointer to next record
756
        $this->pos += 4 + $length;
2✔
757

758
        if ($this->readDataOnly) {
2✔
759
            return;
×
760
        }
761

762
        // recordData consists of an array of subrecords looking like this:
763
        //    grbit: 2 bytes; Option Flags
764
        //    rot: 2 bytes; rotation
765
        //    cchText: 2 bytes; length of the text (in the first continue record)
766
        //    cbRuns: 2 bytes; length of the formatting (in the second continue record)
767
        // followed by the continuation records containing the actual text and formatting
768
        $grbitOpts = self::getUInt2d($recordData, 0);
2✔
769
        $rot = self::getUInt2d($recordData, 2);
2✔
770
        //$cchText = self::getUInt2d($recordData, 10);
771
        $cbRuns = self::getUInt2d($recordData, 12);
2✔
772
        $text = $this->getSplicedRecordData();
2✔
773

774
        /** @var int[] */
775
        $tempSplice = $text['spliceOffsets'];
2✔
776
        /** @var int */
777
        $temp = $tempSplice[0];
2✔
778
        /** @var int */
779
        $temp1 = $tempSplice[1];
2✔
780
        $textByte = $temp1 - $temp - 1;
2✔
781
        /** @var string */
782
        $textRecordData = $text['recordData'];
2✔
783
        $textStr = substr($textRecordData, $temp + 1, $textByte);
2✔
784
        // get 1 byte
785
        $is16Bit = ord($textRecordData[0]);
2✔
786
        // it is possible to use a compressed format,
787
        // which omits the high bytes of all characters, if they are all zero
788
        if (($is16Bit & 0x01) === 0) {
2✔
789
            $textStr = StringHelper::ConvertEncoding($textStr, 'UTF-8', 'ISO-8859-1');
2✔
790
        } else {
791
            $textStr = $this->decodeCodepage($textStr);
×
792
        }
793

794
        $this->textObjects[$this->textObjRef] = [
2✔
795
            'text' => $textStr,
2✔
796
            'format' => substr($textRecordData, $tempSplice[1], $cbRuns),
2✔
797
            'alignment' => $grbitOpts,
2✔
798
            'rotation' => $rot,
2✔
799
        ];
2✔
800
    }
801

802
    /**
803
     * Read BOF.
804
     */
805
    protected function readBof(): void
154✔
806
    {
807
        $length = self::getUInt2d($this->data, $this->pos + 2);
154✔
808
        $recordData = substr($this->data, $this->pos + 4, $length);
154✔
809

810
        // move stream pointer to next record
811
        $this->pos += 4 + $length;
154✔
812

813
        // offset: 2; size: 2; type of the following data
814
        $substreamType = self::getUInt2d($recordData, 2);
154✔
815

816
        switch ($substreamType) {
817
            case self::XLS_WORKBOOKGLOBALS:
154✔
818
                $version = self::getUInt2d($recordData, 0);
154✔
819
                if (($version != self::XLS_BIFF8) && ($version != self::XLS_BIFF7)) {
154✔
820
                    throw new Exception('Cannot read this Excel file. Version is too old.');
×
821
                }
822
                $this->version = $version;
154✔
823

824
                break;
154✔
825
            case self::XLS_WORKSHEET:
143✔
826
                // do not use this version information for anything
827
                // it is unreliable (OpenOffice doc, 5.8), use only version information from the global stream
828
                break;
143✔
829
            default:
830
                // substream, e.g. chart
831
                // just skip the entire substream
832
                do {
833
                    $code = self::getUInt2d($this->data, $this->pos);
×
834
                    $this->readDefault();
×
835
                } while ($code != self::XLS_TYPE_EOF && $this->pos < $this->dataSize);
×
836

837
                break;
×
838
        }
839
    }
840

841
    public function setEncryptionPassword(string $encryptionPassword): self
1✔
842
    {
843
        $this->encryptionPassword = $encryptionPassword;
1✔
844

845
        return $this;
1✔
846
    }
847

848
    /**
849
     * FILEPASS.
850
     *
851
     * This record is part of the File Protection Block. It
852
     * contains information about the read/write password of the
853
     * file. All record contents following this record will be
854
     * encrypted.
855
     *
856
     * --    "OpenOffice.org's Documentation of the Microsoft
857
     *         Excel File Format"
858
     *
859
     * The decryption functions and objects used from here on in
860
     * are based on the source of Spreadsheet-ParseExcel:
861
     * https://metacpan.org/release/Spreadsheet-ParseExcel
862
     */
863
    protected function readFilepass(): void
4✔
864
    {
865
        $length = self::getUInt2d($this->data, $this->pos + 2);
4✔
866

867
        if ($length < 54) {
4✔
868
            throw new Exception('Unexpected file pass record length');
×
869
        }
870

871
        $recordData = $this->readRecordData($this->data, $this->pos + 4, $length);
4✔
872

873
        // move stream pointer to next record
874
        $this->pos += 4 + $length;
4✔
875

876
        if (substr($recordData, 0, 2) !== "\x01\x00" || substr($recordData, 4, 2) !== "\x01\x00") {
4✔
877
            throw new Exception('Unsupported encryption algorithm');
1✔
878
        }
879
        if (!$this->verifyPassword($this->encryptionPassword, substr($recordData, 6, 16), substr($recordData, 22, 16), substr($recordData, 38, 16), $this->md5Ctxt)) {
3✔
880
            throw new Exception('Decryption password incorrect');
1✔
881
        }
882

883
        $this->encryption = self::MS_BIFF_CRYPTO_RC4;
2✔
884

885
        // Decryption required from the record after next onwards
886
        $this->encryptionStartPos = $this->pos + self::getUInt2d($this->data, $this->pos + 2);
2✔
887
    }
888

889
    /**
890
     * Make an RC4 decryptor for the given block.
891
     *
892
     * @param int $block Block for which to create decrypto
893
     * @param string $valContext MD5 context state
894
     */
895
    private function makeKey(int $block, string $valContext): Xls\RC4
3✔
896
    {
897
        $pwarray = str_repeat("\0", 64);
3✔
898

899
        for ($i = 0; $i < 5; ++$i) {
3✔
900
            $pwarray[$i] = $valContext[$i];
3✔
901
        }
902

903
        $pwarray[5] = chr($block & 0xFF);
3✔
904
        $pwarray[6] = chr(($block >> 8) & 0xFF);
3✔
905
        $pwarray[7] = chr(($block >> 16) & 0xFF);
3✔
906
        $pwarray[8] = chr(($block >> 24) & 0xFF);
3✔
907

908
        $pwarray[9] = "\x80";
3✔
909
        $pwarray[56] = "\x48";
3✔
910

911
        $md5 = new Xls\MD5();
3✔
912
        $md5->add($pwarray);
3✔
913

914
        $s = $md5->getContext();
3✔
915

916
        return new Xls\RC4($s);
3✔
917
    }
918

919
    /**
920
     * Verify RC4 file password.
921
     *
922
     * @param string $password Password to check
923
     * @param string $docid Document id
924
     * @param string $salt_data Salt data
925
     * @param string $hashedsalt_data Hashed salt data
926
     * @param string $valContext Set to the MD5 context of the value
927
     *
928
     * @return bool Success
929
     */
930
    private function verifyPassword(string $password, string $docid, string $salt_data, string $hashedsalt_data, string &$valContext): bool
3✔
931
    {
932
        $pwarray = str_repeat("\0", 64);
3✔
933

934
        $iMax = strlen($password);
3✔
935
        for ($i = 0; $i < $iMax; ++$i) {
3✔
936
            $o = ord(substr($password, $i, 1));
3✔
937
            $pwarray[2 * $i] = chr($o & 0xFF);
3✔
938
            $pwarray[2 * $i + 1] = chr(($o >> 8) & 0xFF);
3✔
939
        }
940
        $pwarray[2 * $i] = chr(0x80);
3✔
941
        $pwarray[56] = chr(($i << 4) & 0xFF);
3✔
942

943
        $md5 = new Xls\MD5();
3✔
944
        $md5->add($pwarray);
3✔
945

946
        $mdContext1 = $md5->getContext();
3✔
947

948
        $offset = 0;
3✔
949
        $keyoffset = 0;
3✔
950
        $tocopy = 5;
3✔
951

952
        $md5->reset();
3✔
953

954
        while ($offset != 16) {
3✔
955
            if ((64 - $offset) < 5) {
3✔
956
                $tocopy = 64 - $offset;
3✔
957
            }
958
            for ($i = 0; $i <= $tocopy; ++$i) {
3✔
959
                $pwarray[$offset + $i] = $mdContext1[$keyoffset + $i];
3✔
960
            }
961
            $offset += $tocopy;
3✔
962

963
            if ($offset == 64) {
3✔
964
                $md5->add($pwarray);
3✔
965
                $keyoffset = $tocopy;
3✔
966
                $tocopy = 5 - $tocopy;
3✔
967
                $offset = 0;
3✔
968

969
                continue;
3✔
970
            }
971

972
            $keyoffset = 0;
3✔
973
            $tocopy = 5;
3✔
974
            for ($i = 0; $i < 16; ++$i) {
3✔
975
                $pwarray[$offset + $i] = $docid[$i];
3✔
976
            }
977
            $offset += 16;
3✔
978
        }
979

980
        $pwarray[16] = "\x80";
3✔
981
        for ($i = 0; $i < 47; ++$i) {
3✔
982
            $pwarray[17 + $i] = "\0";
3✔
983
        }
984
        $pwarray[56] = "\x80";
3✔
985
        $pwarray[57] = "\x0a";
3✔
986

987
        $md5->add($pwarray);
3✔
988
        $valContext = $md5->getContext();
3✔
989

990
        $key = $this->makeKey(0, $valContext);
3✔
991

992
        $salt = $key->RC4($salt_data);
3✔
993
        $hashedsalt = $key->RC4($hashedsalt_data);
3✔
994

995
        $salt .= "\x80" . str_repeat("\0", 47);
3✔
996
        $salt[56] = "\x80";
3✔
997

998
        $md5->reset();
3✔
999
        $md5->add($salt);
3✔
1000
        $mdContext2 = $md5->getContext();
3✔
1001

1002
        return $mdContext2 == $hashedsalt;
3✔
1003
    }
1004

1005
    /**
1006
     * CODEPAGE.
1007
     *
1008
     * This record stores the text encoding used to write byte
1009
     * strings, stored as MS Windows code page identifier.
1010
     *
1011
     * --    "OpenOffice.org's Documentation of the Microsoft
1012
     *         Excel File Format"
1013
     */
1014
    protected function readCodepage(): void
148✔
1015
    {
1016
        $length = self::getUInt2d($this->data, $this->pos + 2);
148✔
1017
        $recordData = $this->readRecordData($this->data, $this->pos + 4, $length);
148✔
1018

1019
        // move stream pointer to next record
1020
        $this->pos += 4 + $length;
148✔
1021

1022
        // offset: 0; size: 2; code page identifier
1023
        $codepage = self::getUInt2d($recordData, 0);
148✔
1024

1025
        $this->codepage = CodePage::numberToName($codepage);
148✔
1026
    }
1027

1028
    /**
1029
     * DATEMODE.
1030
     *
1031
     * This record specifies the base date for displaying date
1032
     * values. All dates are stored as count of days past this
1033
     * base date. In BIFF2-BIFF4 this record is part of the
1034
     * Calculation Settings Block. In BIFF5-BIFF8 it is
1035
     * stored in the Workbook Globals Substream.
1036
     *
1037
     * --    "OpenOffice.org's Documentation of the Microsoft
1038
     *         Excel File Format"
1039
     */
1040
    protected function readDateMode(): void
138✔
1041
    {
1042
        $length = self::getUInt2d($this->data, $this->pos + 2);
138✔
1043
        $recordData = $this->readRecordData($this->data, $this->pos + 4, $length);
138✔
1044

1045
        // move stream pointer to next record
1046
        $this->pos += 4 + $length;
138✔
1047

1048
        // offset: 0; size: 2; 0 = base 1900, 1 = base 1904
1049
        Date::setExcelCalendar(Date::CALENDAR_WINDOWS_1900);
138✔
1050
        $this->spreadsheet->setExcelCalendar(Date::CALENDAR_WINDOWS_1900);
138✔
1051
        if (ord($recordData[0]) == 1) {
138✔
1052
            Date::setExcelCalendar(Date::CALENDAR_MAC_1904);
4✔
1053
            $this->spreadsheet->setExcelCalendar(Date::CALENDAR_MAC_1904);
4✔
1054
        }
1055
    }
1056

1057
    /**
1058
     * Read a FONT record.
1059
     */
1060
    protected function readFont(): void
138✔
1061
    {
1062
        $length = self::getUInt2d($this->data, $this->pos + 2);
138✔
1063
        $recordData = $this->readRecordData($this->data, $this->pos + 4, $length);
138✔
1064

1065
        // move stream pointer to next record
1066
        $this->pos += 4 + $length;
138✔
1067

1068
        if (!$this->readDataOnly) {
138✔
1069
            $objFont = new Font();
136✔
1070

1071
            // offset: 0; size: 2; height of the font (in twips = 1/20 of a point)
1072
            $size = self::getUInt2d($recordData, 0);
136✔
1073
            $objFont->setSize($size / 20);
136✔
1074

1075
            // offset: 2; size: 2; option flags
1076
            // bit: 0; mask 0x0001; bold (redundant in BIFF5-BIFF8)
1077
            // bit: 1; mask 0x0002; italic
1078
            $isItalic = (0x0002 & self::getUInt2d($recordData, 2)) >> 1;
136✔
1079
            if ($isItalic) {
136✔
1080
                $objFont->setItalic(true);
58✔
1081
            }
1082

1083
            // bit: 2; mask 0x0004; underlined (redundant in BIFF5-BIFF8)
1084
            // bit: 3; mask 0x0008; strikethrough
1085
            $isStrike = (0x0008 & self::getUInt2d($recordData, 2)) >> 3;
136✔
1086
            if ($isStrike) {
136✔
1087
                $objFont->setStrikethrough(true);
×
1088
            }
1089

1090
            // offset: 4; size: 2; colour index
1091
            $colorIndex = self::getUInt2d($recordData, 4);
136✔
1092
            $objFont->colorIndex = $colorIndex;
136✔
1093

1094
            // offset: 6; size: 2; font weight
1095
            $weight = self::getUInt2d($recordData, 6); // regular=400 bold=700
136✔
1096
            if ($weight >= 550) {
136✔
1097
                $objFont->setBold(true);
69✔
1098
            }
1099

1100
            // offset: 8; size: 2; escapement type
1101
            $escapement = self::getUInt2d($recordData, 8);
136✔
1102
            CellFont::escapement($objFont, $escapement);
136✔
1103

1104
            // offset: 10; size: 1; underline type
1105
            $underlineType = ord($recordData[10]);
136✔
1106
            CellFont::underline($objFont, $underlineType);
136✔
1107

1108
            // offset: 11; size: 1; font family
1109
            // offset: 12; size: 1; character set
1110
            // offset: 13; size: 1; not used
1111
            // offset: 14; size: var; font name
1112
            if ($this->version == self::XLS_BIFF8) {
136✔
1113
                $string = self::readUnicodeStringShort(substr($recordData, 14));
134✔
1114
            } else {
1115
                $string = $this->readByteStringShort(substr($recordData, 14));
2✔
1116
            }
1117
            /** @var string[] $string */
1118
            $objFont->setName($string['value']);
136✔
1119

1120
            $this->objFonts[] = $objFont;
136✔
1121
        }
1122
    }
1123

1124
    /**
1125
     * FORMAT.
1126
     *
1127
     * This record contains information about a number format.
1128
     * All FORMAT records occur together in a sequential list.
1129
     *
1130
     * In BIFF2-BIFF4 other records referencing a FORMAT record
1131
     * contain a zero-based index into this list. From BIFF5 on
1132
     * the FORMAT record contains the index itself that will be
1133
     * used by other records.
1134
     *
1135
     * --    "OpenOffice.org's Documentation of the Microsoft
1136
     *         Excel File Format"
1137
     */
1138
    protected function readFormat(): void
71✔
1139
    {
1140
        $length = self::getUInt2d($this->data, $this->pos + 2);
71✔
1141
        $recordData = $this->readRecordData($this->data, $this->pos + 4, $length);
71✔
1142

1143
        // move stream pointer to next record
1144
        $this->pos += 4 + $length;
71✔
1145

1146
        if (!$this->readDataOnly) {
71✔
1147
            $indexCode = self::getUInt2d($recordData, 0);
69✔
1148

1149
            if ($this->version == self::XLS_BIFF8) {
69✔
1150
                $string = self::readUnicodeStringLong(substr($recordData, 2));
67✔
1151
            } else {
1152
                // BIFF7
1153
                $string = $this->readByteStringShort(substr($recordData, 2));
2✔
1154
            }
1155

1156
            $formatString = $string['value'];
69✔
1157
            // Apache Open Office sets wrong case writing to xls - issue 2239
1158
            if ($formatString === 'GENERAL') {
69✔
1159
                $formatString = NumberFormat::FORMAT_GENERAL;
1✔
1160
            }
1161
            $this->formats[$indexCode] = $formatString;
69✔
1162
        }
1163
    }
1164

1165
    /**
1166
     * XF - Extended Format.
1167
     *
1168
     * This record contains formatting information for cells, rows, columns or styles.
1169
     * According to https://support.microsoft.com/en-us/help/147732 there are always at least 15 cell style XF
1170
     * and 1 cell XF.
1171
     * Inspection of Excel files generated by MS Office Excel shows that XF records 0-14 are cell style XF
1172
     * and XF record 15 is a cell XF
1173
     * We only read the first cell style XF and skip the remaining cell style XF records
1174
     * We read all cell XF records.
1175
     *
1176
     * --    "OpenOffice.org's Documentation of the Microsoft
1177
     *         Excel File Format"
1178
     */
1179
    protected function readXf(): void
139✔
1180
    {
1181
        $length = self::getUInt2d($this->data, $this->pos + 2);
139✔
1182
        $recordData = $this->readRecordData($this->data, $this->pos + 4, $length);
139✔
1183

1184
        // move stream pointer to next record
1185
        $this->pos += 4 + $length;
139✔
1186

1187
        $objStyle = new Style();
139✔
1188

1189
        if (!$this->readDataOnly) {
139✔
1190
            // offset:  0; size: 2; Index to FONT record
1191
            if (self::getUInt2d($recordData, 0) < 4) {
137✔
1192
                $fontIndex = self::getUInt2d($recordData, 0);
137✔
1193
            } else {
1194
                // this has to do with that index 4 is omitted in all BIFF versions for some strange reason
1195
                // check the OpenOffice documentation of the FONT record
1196
                $fontIndex = self::getUInt2d($recordData, 0) - 1;
63✔
1197
            }
1198
            if (isset($this->objFonts[$fontIndex])) {
137✔
1199
                $objStyle->setFont($this->objFonts[$fontIndex]);
136✔
1200
            }
1201

1202
            // offset:  2; size: 2; Index to FORMAT record
1203
            $numberFormatIndex = self::getUInt2d($recordData, 2);
137✔
1204
            if (isset($this->formats[$numberFormatIndex])) {
137✔
1205
                // then we have user-defined format code
1206
                $numberFormat = ['formatCode' => $this->formats[$numberFormatIndex]];
63✔
1207
            } elseif (($code = NumberFormat::builtInFormatCode($numberFormatIndex)) !== '') {
137✔
1208
                // then we have built-in format code
1209
                $numberFormat = ['formatCode' => $code];
137✔
1210
            } else {
1211
                // we set the general format code
1212
                $numberFormat = ['formatCode' => NumberFormat::FORMAT_GENERAL];
4✔
1213
            }
1214
            /** @var string[] $numberFormat */
1215
            $objStyle->getNumberFormat()
137✔
1216
                ->setFormatCode($numberFormat['formatCode']);
137✔
1217

1218
            // offset:  4; size: 2; XF type, cell protection, and parent style XF
1219
            // bit 2-0; mask 0x0007; XF_TYPE_PROT
1220
            $xfTypeProt = self::getUInt2d($recordData, 4);
137✔
1221
            // bit 0; mask 0x01; 1 = cell is locked
1222
            $isLocked = (0x01 & $xfTypeProt) >> 0;
137✔
1223
            $objStyle->getProtection()->setLocked($isLocked ? Protection::PROTECTION_INHERIT : Protection::PROTECTION_UNPROTECTED);
137✔
1224

1225
            // bit 1; mask 0x02; 1 = Formula is hidden
1226
            $isHidden = (0x02 & $xfTypeProt) >> 1;
137✔
1227
            $objStyle->getProtection()->setHidden($isHidden ? Protection::PROTECTION_PROTECTED : Protection::PROTECTION_UNPROTECTED);
137✔
1228

1229
            // bit 2; mask 0x04; 0 = Cell XF, 1 = Cell Style XF
1230
            $isCellStyleXf = (0x04 & $xfTypeProt) >> 2;
137✔
1231

1232
            // offset:  6; size: 1; Alignment and text break
1233
            // bit 2-0, mask 0x07; horizontal alignment
1234
            $horAlign = (0x07 & ord($recordData[6])) >> 0;
137✔
1235
            Xls\Style\CellAlignment::horizontal($objStyle->getAlignment(), $horAlign);
137✔
1236

1237
            // bit 3, mask 0x08; wrap text
1238
            $wrapText = (0x08 & ord($recordData[6])) >> 3;
137✔
1239
            Xls\Style\CellAlignment::wrap($objStyle->getAlignment(), $wrapText);
137✔
1240

1241
            // bit 6-4, mask 0x70; vertical alignment
1242
            $vertAlign = (0x70 & ord($recordData[6])) >> 4;
137✔
1243
            Xls\Style\CellAlignment::vertical($objStyle->getAlignment(), $vertAlign);
137✔
1244

1245
            if ($this->version == self::XLS_BIFF8) {
137✔
1246
                // offset:  7; size: 1; XF_ROTATION: Text rotation angle
1247
                $angle = ord($recordData[7]);
135✔
1248
                $rotation = 0;
135✔
1249
                if ($angle <= 90) {
135✔
1250
                    $rotation = $angle;
135✔
1251
                } elseif ($angle <= 180) {
4✔
1252
                    $rotation = 90 - $angle;
×
1253
                } elseif ($angle == Alignment::TEXTROTATION_STACK_EXCEL) {
4✔
1254
                    $rotation = Alignment::TEXTROTATION_STACK_PHPSPREADSHEET;
4✔
1255
                }
1256
                $objStyle->getAlignment()->setTextRotation($rotation);
135✔
1257

1258
                // offset:  8; size: 1; Indentation, shrink to cell size, and text direction
1259
                // bit: 3-0; mask: 0x0F; indent level
1260
                $indent = (0x0F & ord($recordData[8])) >> 0;
135✔
1261
                $objStyle->getAlignment()->setIndent($indent);
135✔
1262

1263
                // bit: 4; mask: 0x10; 1 = shrink content to fit into cell
1264
                $shrinkToFit = (0x10 & ord($recordData[8])) >> 4;
135✔
1265
                switch ($shrinkToFit) {
1266
                    case 0:
135✔
1267
                        $objStyle->getAlignment()->setShrinkToFit(false);
135✔
1268

1269
                        break;
135✔
1270
                    case 1:
1✔
1271
                        $objStyle->getAlignment()->setShrinkToFit(true);
1✔
1272

1273
                        break;
1✔
1274
                }
1275
                $readOrder = (0xC0 & ord($recordData[8])) >> 6;
135✔
1276
                $objStyle->getAlignment()->setReadOrder($readOrder);
135✔
1277

1278
                // offset:  9; size: 1; Flags used for attribute groups
1279

1280
                // offset: 10; size: 4; Cell border lines and background area
1281
                // bit: 3-0; mask: 0x0000000F; left style
1282
                if ($bordersLeftStyle = Xls\Style\Border::lookup((0x0000000F & self::getInt4d($recordData, 10)) >> 0)) {
135✔
1283
                    $objStyle->getBorders()->getLeft()->setBorderStyle($bordersLeftStyle);
135✔
1284
                }
1285
                // bit: 7-4; mask: 0x000000F0; right style
1286
                if ($bordersRightStyle = Xls\Style\Border::lookup((0x000000F0 & self::getInt4d($recordData, 10)) >> 4)) {
135✔
1287
                    $objStyle->getBorders()->getRight()->setBorderStyle($bordersRightStyle);
135✔
1288
                }
1289
                // bit: 11-8; mask: 0x00000F00; top style
1290
                if ($bordersTopStyle = Xls\Style\Border::lookup((0x00000F00 & self::getInt4d($recordData, 10)) >> 8)) {
135✔
1291
                    $objStyle->getBorders()->getTop()->setBorderStyle($bordersTopStyle);
135✔
1292
                }
1293
                // bit: 15-12; mask: 0x0000F000; bottom style
1294
                if ($bordersBottomStyle = Xls\Style\Border::lookup((0x0000F000 & self::getInt4d($recordData, 10)) >> 12)) {
135✔
1295
                    $objStyle->getBorders()->getBottom()->setBorderStyle($bordersBottomStyle);
135✔
1296
                }
1297
                // bit: 22-16; mask: 0x007F0000; left color
1298
                $objStyle->getBorders()->getLeft()->colorIndex = (0x007F0000 & self::getInt4d($recordData, 10)) >> 16;
135✔
1299

1300
                // bit: 29-23; mask: 0x3F800000; right color
1301
                $objStyle->getBorders()->getRight()->colorIndex = (0x3F800000 & self::getInt4d($recordData, 10)) >> 23;
135✔
1302

1303
                // bit: 30; mask: 0x40000000; 1 = diagonal line from top left to right bottom
1304
                $diagonalDown = (0x40000000 & self::getInt4d($recordData, 10)) >> 30 ? true : false;
135✔
1305

1306
                // bit: 31; mask: 0x800000; 1 = diagonal line from bottom left to top right
1307
                $diagonalUp = (self::HIGH_ORDER_BIT & self::getInt4d($recordData, 10)) >> 31 ? true : false;
135✔
1308

1309
                if ($diagonalUp === false) {
135✔
1310
                    if ($diagonalDown === false) {
135✔
1311
                        $objStyle->getBorders()->setDiagonalDirection(Borders::DIAGONAL_NONE);
135✔
1312
                    } else {
1313
                        $objStyle->getBorders()->setDiagonalDirection(Borders::DIAGONAL_DOWN);
1✔
1314
                    }
1315
                } elseif ($diagonalDown === false) {
1✔
1316
                    $objStyle->getBorders()->setDiagonalDirection(Borders::DIAGONAL_UP);
1✔
1317
                } else {
1318
                    $objStyle->getBorders()->setDiagonalDirection(Borders::DIAGONAL_BOTH);
1✔
1319
                }
1320

1321
                // offset: 14; size: 4;
1322
                // bit: 6-0; mask: 0x0000007F; top color
1323
                $objStyle->getBorders()->getTop()->colorIndex = (0x0000007F & self::getInt4d($recordData, 14)) >> 0;
135✔
1324

1325
                // bit: 13-7; mask: 0x00003F80; bottom color
1326
                $objStyle->getBorders()->getBottom()->colorIndex = (0x00003F80 & self::getInt4d($recordData, 14)) >> 7;
135✔
1327

1328
                // bit: 20-14; mask: 0x001FC000; diagonal color
1329
                $objStyle->getBorders()->getDiagonal()->colorIndex = (0x001FC000 & self::getInt4d($recordData, 14)) >> 14;
135✔
1330

1331
                // bit: 24-21; mask: 0x01E00000; diagonal style
1332
                if ($bordersDiagonalStyle = Xls\Style\Border::lookup((0x01E00000 & self::getInt4d($recordData, 14)) >> 21)) {
135✔
1333
                    $objStyle->getBorders()->getDiagonal()->setBorderStyle($bordersDiagonalStyle);
135✔
1334
                }
1335

1336
                // bit: 31-26; mask: 0xFC000000 fill pattern
1337
                if ($fillType = FillPattern::lookup((self::FC000000 & self::getInt4d($recordData, 14)) >> 26)) {
135✔
1338
                    $objStyle->getFill()->setFillType($fillType);
135✔
1339
                }
1340
                // offset: 18; size: 2; pattern and background colour
1341
                // bit: 6-0; mask: 0x007F; color index for pattern color
1342
                $objStyle->getFill()->startcolorIndex = (0x007F & self::getUInt2d($recordData, 18)) >> 0;
135✔
1343

1344
                // bit: 13-7; mask: 0x3F80; color index for pattern background
1345
                $objStyle->getFill()->endcolorIndex = (0x3F80 & self::getUInt2d($recordData, 18)) >> 7;
135✔
1346
            } else {
1347
                // BIFF5
1348

1349
                // offset: 7; size: 1; Text orientation and flags
1350
                $orientationAndFlags = ord($recordData[7]);
2✔
1351

1352
                // bit: 1-0; mask: 0x03; XF_ORIENTATION: Text orientation
1353
                $xfOrientation = (0x03 & $orientationAndFlags) >> 0;
2✔
1354
                switch ($xfOrientation) {
1355
                    case 0:
2✔
1356
                        $objStyle->getAlignment()->setTextRotation(0);
2✔
1357

1358
                        break;
2✔
1359
                    case 1:
1✔
1360
                        $objStyle->getAlignment()->setTextRotation(Alignment::TEXTROTATION_STACK_PHPSPREADSHEET);
1✔
1361

1362
                        break;
1✔
1363
                    case 2:
×
1364
                        $objStyle->getAlignment()->setTextRotation(90);
×
1365

1366
                        break;
×
1367
                    case 3:
×
1368
                        $objStyle->getAlignment()->setTextRotation(-90);
×
1369

1370
                        break;
×
1371
                }
1372

1373
                // offset: 8; size: 4; cell border lines and background area
1374
                $borderAndBackground = self::getInt4d($recordData, 8);
2✔
1375

1376
                // bit: 6-0; mask: 0x0000007F; color index for pattern color
1377
                $objStyle->getFill()->startcolorIndex = (0x0000007F & $borderAndBackground) >> 0;
2✔
1378

1379
                // bit: 13-7; mask: 0x00003F80; color index for pattern background
1380
                $objStyle->getFill()->endcolorIndex = (0x00003F80 & $borderAndBackground) >> 7;
2✔
1381

1382
                // bit: 21-16; mask: 0x003F0000; fill pattern
1383
                $objStyle->getFill()->setFillType(FillPattern::lookup((0x003F0000 & $borderAndBackground) >> 16));
2✔
1384

1385
                // bit: 24-22; mask: 0x01C00000; bottom line style
1386
                $objStyle->getBorders()->getBottom()->setBorderStyle(Xls\Style\Border::lookup((0x01C00000 & $borderAndBackground) >> 22));
2✔
1387

1388
                // bit: 31-25; mask: 0xFE000000; bottom line color
1389
                $objStyle->getBorders()->getBottom()->colorIndex = (self::FE000000 & $borderAndBackground) >> 25;
2✔
1390

1391
                // offset: 12; size: 4; cell border lines
1392
                $borderLines = self::getInt4d($recordData, 12);
2✔
1393

1394
                // bit: 2-0; mask: 0x00000007; top line style
1395
                $objStyle->getBorders()->getTop()->setBorderStyle(Xls\Style\Border::lookup((0x00000007 & $borderLines) >> 0));
2✔
1396

1397
                // bit: 5-3; mask: 0x00000038; left line style
1398
                $objStyle->getBorders()->getLeft()->setBorderStyle(Xls\Style\Border::lookup((0x00000038 & $borderLines) >> 3));
2✔
1399

1400
                // bit: 8-6; mask: 0x000001C0; right line style
1401
                $objStyle->getBorders()->getRight()->setBorderStyle(Xls\Style\Border::lookup((0x000001C0 & $borderLines) >> 6));
2✔
1402

1403
                // bit: 15-9; mask: 0x0000FE00; top line color index
1404
                $objStyle->getBorders()->getTop()->colorIndex = (0x0000FE00 & $borderLines) >> 9;
2✔
1405

1406
                // bit: 22-16; mask: 0x007F0000; left line color index
1407
                $objStyle->getBorders()->getLeft()->colorIndex = (0x007F0000 & $borderLines) >> 16;
2✔
1408

1409
                // bit: 29-23; mask: 0x3F800000; right line color index
1410
                $objStyle->getBorders()->getRight()->colorIndex = (0x3F800000 & $borderLines) >> 23;
2✔
1411
            }
1412

1413
            // add cellStyleXf or cellXf and update mapping
1414
            if ($isCellStyleXf) {
137✔
1415
                // we only read one style XF record which is always the first
1416
                if ($this->xfIndex == 0) {
137✔
1417
                    $this->spreadsheet->addCellStyleXf($objStyle);
137✔
1418
                    $this->mapCellStyleXfIndex[$this->xfIndex] = 0;
137✔
1419
                }
1420
            } else {
1421
                // we read all cell XF records
1422
                $this->spreadsheet->addCellXf($objStyle);
137✔
1423
                $this->mapCellXfIndex[$this->xfIndex] = count($this->spreadsheet->getCellXfCollection()) - 1;
137✔
1424
            }
1425

1426
            // update XF index for when we read next record
1427
            ++$this->xfIndex;
137✔
1428
        }
1429
    }
1430

1431
    protected function readXfExt(): void
54✔
1432
    {
1433
        $length = self::getUInt2d($this->data, $this->pos + 2);
54✔
1434
        $recordData = $this->readRecordData($this->data, $this->pos + 4, $length);
54✔
1435

1436
        // move stream pointer to next record
1437
        $this->pos += 4 + $length;
54✔
1438

1439
        if (!$this->readDataOnly) {
54✔
1440
            // offset: 0; size: 2; 0x087D = repeated header
1441

1442
            // offset: 2; size: 2
1443

1444
            // offset: 4; size: 8; not used
1445

1446
            // offset: 12; size: 2; record version
1447

1448
            // offset: 14; size: 2; index to XF record which this record modifies
1449
            $ixfe = self::getUInt2d($recordData, 14);
52✔
1450

1451
            // offset: 16; size: 2; not used
1452

1453
            // offset: 18; size: 2; number of extension properties that follow
1454
            //$cexts = self::getUInt2d($recordData, 18);
1455

1456
            // start reading the actual extension data
1457
            $offset = 20;
52✔
1458
            while ($offset < $length) {
52✔
1459
                // extension type
1460
                $extType = self::getUInt2d($recordData, $offset);
52✔
1461

1462
                // extension length
1463
                $cb = self::getUInt2d($recordData, $offset + 2);
52✔
1464

1465
                // extension data
1466
                $extData = substr($recordData, $offset + 4, $cb);
52✔
1467

1468
                switch ($extType) {
1469
                    case 4:        // fill start color
52✔
1470
                        $xclfType = self::getUInt2d($extData, 0); // color type
52✔
1471
                        $xclrValue = substr($extData, 4, 4); // color value (value based on color type)
52✔
1472

1473
                        if ($xclfType == 2) {
52✔
1474
                            $rgb = sprintf('%02X%02X%02X', ord($xclrValue[0]), ord($xclrValue[1]), ord($xclrValue[2]));
50✔
1475

1476
                            // modify the relevant style property
1477
                            if (isset($this->mapCellXfIndex[$ixfe])) {
50✔
1478
                                $fill = $this->spreadsheet->getCellXfByIndex($this->mapCellXfIndex[$ixfe])->getFill();
7✔
1479
                                $fill->getStartColor()->setRGB($rgb);
7✔
1480
                                $fill->startcolorIndex = null; // normal color index does not apply, discard
7✔
1481
                            }
1482
                        }
1483

1484
                        break;
52✔
1485
                    case 5:        // fill end color
50✔
1486
                        $xclfType = self::getUInt2d($extData, 0); // color type
5✔
1487
                        $xclrValue = substr($extData, 4, 4); // color value (value based on color type)
5✔
1488

1489
                        if ($xclfType == 2) {
5✔
1490
                            $rgb = sprintf('%02X%02X%02X', ord($xclrValue[0]), ord($xclrValue[1]), ord($xclrValue[2]));
5✔
1491

1492
                            // modify the relevant style property
1493
                            if (isset($this->mapCellXfIndex[$ixfe])) {
5✔
1494
                                $fill = $this->spreadsheet->getCellXfByIndex($this->mapCellXfIndex[$ixfe])->getFill();
5✔
1495
                                $fill->getEndColor()->setRGB($rgb);
5✔
1496
                                $fill->endcolorIndex = null; // normal color index does not apply, discard
5✔
1497
                            }
1498
                        }
1499

1500
                        break;
5✔
1501
                    case 7:        // border color top
50✔
1502
                        $xclfType = self::getUInt2d($extData, 0); // color type
50✔
1503
                        $xclrValue = substr($extData, 4, 4); // color value (value based on color type)
50✔
1504

1505
                        if ($xclfType == 2) {
50✔
1506
                            $rgb = sprintf('%02X%02X%02X', ord($xclrValue[0]), ord($xclrValue[1]), ord($xclrValue[2]));
50✔
1507

1508
                            // modify the relevant style property
1509
                            if (isset($this->mapCellXfIndex[$ixfe])) {
50✔
1510
                                $top = $this->spreadsheet->getCellXfByIndex($this->mapCellXfIndex[$ixfe])->getBorders()->getTop();
4✔
1511
                                $top->getColor()->setRGB($rgb);
4✔
1512
                                $top->colorIndex = null; // normal color index does not apply, discard
4✔
1513
                            }
1514
                        }
1515

1516
                        break;
50✔
1517
                    case 8:        // border color bottom
50✔
1518
                        $xclfType = self::getUInt2d($extData, 0); // color type
50✔
1519
                        $xclrValue = substr($extData, 4, 4); // color value (value based on color type)
50✔
1520

1521
                        if ($xclfType == 2) {
50✔
1522
                            $rgb = sprintf('%02X%02X%02X', ord($xclrValue[0]), ord($xclrValue[1]), ord($xclrValue[2]));
50✔
1523

1524
                            // modify the relevant style property
1525
                            if (isset($this->mapCellXfIndex[$ixfe])) {
50✔
1526
                                $bottom = $this->spreadsheet->getCellXfByIndex($this->mapCellXfIndex[$ixfe])->getBorders()->getBottom();
5✔
1527
                                $bottom->getColor()->setRGB($rgb);
5✔
1528
                                $bottom->colorIndex = null; // normal color index does not apply, discard
5✔
1529
                            }
1530
                        }
1531

1532
                        break;
50✔
1533
                    case 9:        // border color left
50✔
1534
                        $xclfType = self::getUInt2d($extData, 0); // color type
50✔
1535
                        $xclrValue = substr($extData, 4, 4); // color value (value based on color type)
50✔
1536

1537
                        if ($xclfType == 2) {
50✔
1538
                            $rgb = sprintf('%02X%02X%02X', ord($xclrValue[0]), ord($xclrValue[1]), ord($xclrValue[2]));
50✔
1539

1540
                            // modify the relevant style property
1541
                            if (isset($this->mapCellXfIndex[$ixfe])) {
50✔
1542
                                $left = $this->spreadsheet->getCellXfByIndex($this->mapCellXfIndex[$ixfe])->getBorders()->getLeft();
4✔
1543
                                $left->getColor()->setRGB($rgb);
4✔
1544
                                $left->colorIndex = null; // normal color index does not apply, discard
4✔
1545
                            }
1546
                        }
1547

1548
                        break;
50✔
1549
                    case 10:        // border color right
50✔
1550
                        $xclfType = self::getUInt2d($extData, 0); // color type
50✔
1551
                        $xclrValue = substr($extData, 4, 4); // color value (value based on color type)
50✔
1552

1553
                        if ($xclfType == 2) {
50✔
1554
                            $rgb = sprintf('%02X%02X%02X', ord($xclrValue[0]), ord($xclrValue[1]), ord($xclrValue[2]));
50✔
1555

1556
                            // modify the relevant style property
1557
                            if (isset($this->mapCellXfIndex[$ixfe])) {
50✔
1558
                                $right = $this->spreadsheet->getCellXfByIndex($this->mapCellXfIndex[$ixfe])->getBorders()->getRight();
4✔
1559
                                $right->getColor()->setRGB($rgb);
4✔
1560
                                $right->colorIndex = null; // normal color index does not apply, discard
4✔
1561
                            }
1562
                        }
1563

1564
                        break;
50✔
1565
                    case 11:        // border color diagonal
50✔
1566
                        $xclfType = self::getUInt2d($extData, 0); // color type
×
1567
                        $xclrValue = substr($extData, 4, 4); // color value (value based on color type)
×
1568

1569
                        if ($xclfType == 2) {
×
1570
                            $rgb = sprintf('%02X%02X%02X', ord($xclrValue[0]), ord($xclrValue[1]), ord($xclrValue[2]));
×
1571

1572
                            // modify the relevant style property
1573
                            if (isset($this->mapCellXfIndex[$ixfe])) {
×
1574
                                $diagonal = $this->spreadsheet->getCellXfByIndex($this->mapCellXfIndex[$ixfe])->getBorders()->getDiagonal();
×
1575
                                $diagonal->getColor()->setRGB($rgb);
×
1576
                                $diagonal->colorIndex = null; // normal color index does not apply, discard
×
1577
                            }
1578
                        }
1579

1580
                        break;
×
1581
                    case 13:    // font color
50✔
1582
                        $xclfType = self::getUInt2d($extData, 0); // color type
50✔
1583
                        $xclrValue = substr($extData, 4, 4); // color value (value based on color type)
50✔
1584

1585
                        if ($xclfType == 2) {
50✔
1586
                            $rgb = sprintf('%02X%02X%02X', ord($xclrValue[0]), ord($xclrValue[1]), ord($xclrValue[2]));
50✔
1587

1588
                            // modify the relevant style property
1589
                            if (isset($this->mapCellXfIndex[$ixfe])) {
50✔
1590
                                $font = $this->spreadsheet->getCellXfByIndex($this->mapCellXfIndex[$ixfe])->getFont();
9✔
1591
                                $font->getColor()->setRGB($rgb);
9✔
1592
                                $font->colorIndex = null; // normal color index does not apply, discard
9✔
1593
                            }
1594
                        }
1595

1596
                        break;
50✔
1597
                }
1598

1599
                $offset += $cb;
52✔
1600
            }
1601
        }
1602
    }
1603

1604
    /**
1605
     * Read STYLE record.
1606
     */
1607
    protected function readStyle(): void
139✔
1608
    {
1609
        $length = self::getUInt2d($this->data, $this->pos + 2);
139✔
1610
        $recordData = $this->readRecordData($this->data, $this->pos + 4, $length);
139✔
1611

1612
        // move stream pointer to next record
1613
        $this->pos += 4 + $length;
139✔
1614

1615
        if (!$this->readDataOnly) {
139✔
1616
            // offset: 0; size: 2; index to XF record and flag for built-in style
1617
            $ixfe = self::getUInt2d($recordData, 0);
137✔
1618

1619
            // bit: 11-0; mask 0x0FFF; index to XF record
1620
            //$xfIndex = (0x0FFF & $ixfe) >> 0;
1621

1622
            // bit: 15; mask 0x8000; 0 = user-defined style, 1 = built-in style
1623
            $isBuiltIn = (bool) ((0x8000 & $ixfe) >> 15);
137✔
1624

1625
            if ($isBuiltIn) {
137✔
1626
                // offset: 2; size: 1; identifier for built-in style
1627
                $builtInId = ord($recordData[2]);
137✔
1628

1629
                switch ($builtInId) {
1630
                    case 0x00:
137✔
1631
                        // currently, we are not using this for anything
1632
                        break;
137✔
1633
                    default:
1634
                        break;
60✔
1635
                }
1636
            }
1637
            // user-defined; not supported by PhpSpreadsheet
1638
        }
1639
    }
1640

1641
    /**
1642
     * Read PALETTE record.
1643
     */
1644
    protected function readPalette(): void
93✔
1645
    {
1646
        $length = self::getUInt2d($this->data, $this->pos + 2);
93✔
1647
        $recordData = $this->readRecordData($this->data, $this->pos + 4, $length);
93✔
1648

1649
        // move stream pointer to next record
1650
        $this->pos += 4 + $length;
93✔
1651

1652
        if (!$this->readDataOnly) {
93✔
1653
            // offset: 0; size: 2; number of following colors
1654
            $nm = self::getUInt2d($recordData, 0);
93✔
1655

1656
            // list of RGB colors
1657
            for ($i = 0; $i < $nm; ++$i) {
93✔
1658
                $rgb = substr($recordData, 2 + 4 * $i, 4);
93✔
1659
                $this->palette[] = self::readRGB($rgb);
93✔
1660
            }
1661
        }
1662
    }
1663

1664
    /**
1665
     * SHEET.
1666
     *
1667
     * This record is  located in the  Workbook Globals
1668
     * Substream  and represents a sheet inside the workbook.
1669
     * One SHEET record is written for each sheet. It stores the
1670
     * sheet name and a stream offset to the BOF record of the
1671
     * respective Sheet Substream within the Workbook Stream.
1672
     *
1673
     * --    "OpenOffice.org's Documentation of the Microsoft
1674
     *         Excel File Format"
1675
     */
1676
    protected function readSheet(): void
152✔
1677
    {
1678
        $length = self::getUInt2d($this->data, $this->pos + 2);
152✔
1679
        $recordData = $this->readRecordData($this->data, $this->pos + 4, $length);
152✔
1680

1681
        // offset: 0; size: 4; absolute stream position of the BOF record of the sheet
1682
        // NOTE: not encrypted
1683
        $rec_offset = self::getInt4d($this->data, $this->pos + 4);
152✔
1684

1685
        // move stream pointer to next record
1686
        $this->pos += 4 + $length;
152✔
1687

1688
        // offset: 4; size: 1; sheet state
1689
        $sheetState = match (ord($recordData[4])) {
152✔
1690
            0x00 => Worksheet::SHEETSTATE_VISIBLE,
152✔
1691
            0x01 => Worksheet::SHEETSTATE_HIDDEN,
6✔
1692
            0x02 => Worksheet::SHEETSTATE_VERYHIDDEN,
2✔
1693
            default => Worksheet::SHEETSTATE_VISIBLE,
×
1694
        };
152✔
1695

1696
        // offset: 5; size: 1; sheet type
1697
        $sheetType = ord($recordData[5]);
152✔
1698

1699
        // offset: 6; size: var; sheet name
1700
        $rec_name = null;
152✔
1701
        if ($this->version == self::XLS_BIFF8) {
152✔
1702
            $string = self::readUnicodeStringShort(substr($recordData, 6));
145✔
1703
            $rec_name = $string['value'];
145✔
1704
        } elseif ($this->version == self::XLS_BIFF7) {
7✔
1705
            $string = $this->readByteStringShort(substr($recordData, 6));
7✔
1706
            $rec_name = $string['value'];
7✔
1707
        }
1708
        /** @var string $rec_name */
1709
        $this->sheets[] = [
152✔
1710
            'name' => $rec_name,
152✔
1711
            'offset' => $rec_offset,
152✔
1712
            'sheetState' => $sheetState,
152✔
1713
            'sheetType' => $sheetType,
152✔
1714
        ];
152✔
1715
    }
1716

1717
    /**
1718
     * Read EXTERNALBOOK record.
1719
     */
1720
    protected function readExternalBook(): void
107✔
1721
    {
1722
        $length = self::getUInt2d($this->data, $this->pos + 2);
107✔
1723
        $recordData = $this->readRecordData($this->data, $this->pos + 4, $length);
107✔
1724

1725
        // move stream pointer to next record
1726
        $this->pos += 4 + $length;
107✔
1727

1728
        // offset within record data
1729
        $offset = 0;
107✔
1730

1731
        // there are 4 types of records
1732
        if (strlen($recordData) > 4) {
107✔
1733
            // external reference
1734
            // offset: 0; size: 2; number of sheet names ($nm)
1735
            $nm = self::getUInt2d($recordData, 0);
×
1736
            $offset += 2;
×
1737

1738
            // offset: 2; size: var; encoded URL without sheet name (Unicode string, 16-bit length)
1739
            $encodedUrlString = self::readUnicodeStringLong(substr($recordData, 2));
×
1740
            $offset += $encodedUrlString['size'];
×
1741

1742
            // offset: var; size: var; list of $nm sheet names (Unicode strings, 16-bit length)
1743
            $externalSheetNames = [];
×
1744
            for ($i = 0; $i < $nm; ++$i) {
×
1745
                $externalSheetNameString = self::readUnicodeStringLong(substr($recordData, $offset));
×
1746
                $externalSheetNames[] = $externalSheetNameString['value'];
×
1747
                $offset += $externalSheetNameString['size'];
×
1748
            }
1749

1750
            // store the record data
1751
            $this->externalBooks[] = [
×
1752
                'type' => 'external',
×
1753
                'encodedUrl' => $encodedUrlString['value'],
×
1754
                'externalSheetNames' => $externalSheetNames,
×
1755
            ];
×
1756
        } elseif (substr($recordData, 2, 2) == pack('CC', 0x01, 0x04)) {
107✔
1757
            // internal reference
1758
            // offset: 0; size: 2; number of sheet in this document
1759
            // offset: 2; size: 2; 0x01 0x04
1760
            $this->externalBooks[] = [
107✔
1761
                'type' => 'internal',
107✔
1762
            ];
107✔
1763
        } elseif (substr($recordData, 0, 4) == pack('vCC', 0x0001, 0x01, 0x3A)) {
1✔
1764
            // add-in function
1765
            // offset: 0; size: 2; 0x0001
1766
            $this->externalBooks[] = [
1✔
1767
                'type' => 'addInFunction',
1✔
1768
            ];
1✔
1769
        } elseif (substr($recordData, 0, 2) == pack('v', 0x0000)) {
×
1770
            // DDE links, OLE links
1771
            // offset: 0; size: 2; 0x0000
1772
            // offset: 2; size: var; encoded source document name
1773
            $this->externalBooks[] = [
×
1774
                'type' => 'DDEorOLE',
×
1775
            ];
×
1776
        }
1777
    }
1778

1779
    /**
1780
     * Read EXTERNNAME record.
1781
     */
1782
    protected function readExternName(): void
1✔
1783
    {
1784
        $length = self::getUInt2d($this->data, $this->pos + 2);
1✔
1785
        $recordData = $this->readRecordData($this->data, $this->pos + 4, $length);
1✔
1786

1787
        // move stream pointer to next record
1788
        $this->pos += 4 + $length;
1✔
1789

1790
        // external sheet references provided for named cells
1791
        if ($this->version == self::XLS_BIFF8) {
1✔
1792
            // offset: 0; size: 2; options
1793
            //$options = self::getUInt2d($recordData, 0);
1794

1795
            // offset: 2; size: 2;
1796

1797
            // offset: 4; size: 2; not used
1798

1799
            // offset: 6; size: var
1800
            $nameString = self::readUnicodeStringShort(substr($recordData, 6));
1✔
1801

1802
            // offset: var; size: var; formula data
1803
            $offset = 6 + $nameString['size'];
1✔
1804
            $formula = $this->getFormulaFromStructure(substr($recordData, $offset));
1✔
1805

1806
            $this->externalNames[] = [
1✔
1807
                'name' => $nameString['value'],
1✔
1808
                'formula' => $formula,
1✔
1809
            ];
1✔
1810
        }
1811
    }
1812

1813
    /**
1814
     * Read EXTERNSHEET record.
1815
     */
1816
    protected function readExternSheet(): void
108✔
1817
    {
1818
        $length = self::getUInt2d($this->data, $this->pos + 2);
108✔
1819
        $recordData = $this->readRecordData($this->data, $this->pos + 4, $length);
108✔
1820

1821
        // move stream pointer to next record
1822
        $this->pos += 4 + $length;
108✔
1823

1824
        // external sheet references provided for named cells
1825
        if ($this->version == self::XLS_BIFF8) {
108✔
1826
            // offset: 0; size: 2; number of following ref structures
1827
            $nm = self::getUInt2d($recordData, 0);
107✔
1828
            for ($i = 0; $i < $nm; ++$i) {
107✔
1829
                $this->ref[] = [
105✔
1830
                    // offset: 2 + 6 * $i; index to EXTERNALBOOK record
1831
                    'externalBookIndex' => self::getUInt2d($recordData, 2 + 6 * $i),
105✔
1832
                    // offset: 4 + 6 * $i; index to first sheet in EXTERNALBOOK record
1833
                    'firstSheetIndex' => self::getUInt2d($recordData, 4 + 6 * $i),
105✔
1834
                    // offset: 6 + 6 * $i; index to last sheet in EXTERNALBOOK record
1835
                    'lastSheetIndex' => self::getUInt2d($recordData, 6 + 6 * $i),
105✔
1836
                ];
105✔
1837
            }
1838
        }
1839
    }
1840

1841
    /**
1842
     * DEFINEDNAME.
1843
     *
1844
     * This record is part of a Link Table. It contains the name
1845
     * and the token array of an internal defined name. Token
1846
     * arrays of defined names contain tokens with aberrant
1847
     * token classes.
1848
     *
1849
     * --    "OpenOffice.org's Documentation of the Microsoft
1850
     *         Excel File Format"
1851
     */
1852
    protected function readDefinedName(): void
21✔
1853
    {
1854
        $length = self::getUInt2d($this->data, $this->pos + 2);
21✔
1855
        $recordData = $this->readRecordData($this->data, $this->pos + 4, $length);
21✔
1856

1857
        // move stream pointer to next record
1858
        $this->pos += 4 + $length;
21✔
1859

1860
        if ($this->version == self::XLS_BIFF8) {
21✔
1861
            // retrieves named cells
1862

1863
            // offset: 0; size: 2; option flags
1864
            $opts = self::getUInt2d($recordData, 0);
20✔
1865

1866
            // bit: 5; mask: 0x0020; 0 = user-defined name, 1 = built-in-name
1867
            $isBuiltInName = (0x0020 & $opts) >> 5;
20✔
1868

1869
            // offset: 2; size: 1; keyboard shortcut
1870

1871
            // offset: 3; size: 1; length of the name (character count)
1872
            $nlen = ord($recordData[3]);
20✔
1873

1874
            // offset: 4; size: 2; size of the formula data (it can happen that this is zero)
1875
            // note: there can also be additional data, this is not included in $flen
1876
            $flen = self::getUInt2d($recordData, 4);
20✔
1877

1878
            // offset: 8; size: 2; 0=Global name, otherwise index to sheet (1-based)
1879
            $scope = self::getUInt2d($recordData, 8);
20✔
1880

1881
            // offset: 14; size: var; Name (Unicode string without length field)
1882
            $string = self::readUnicodeString(substr($recordData, 14), $nlen);
20✔
1883

1884
            // offset: var; size: $flen; formula data
1885
            $offset = 14 + $string['size'];
20✔
1886
            $formulaStructure = pack('v', $flen) . substr($recordData, $offset);
20✔
1887

1888
            try {
1889
                $formula = $this->getFormulaFromStructure($formulaStructure);
20✔
1890
            } catch (PhpSpreadsheetException) {
1✔
1891
                $formula = '';
1✔
1892
                $isBuiltInName = 0;
1✔
1893
            }
1894

1895
            $this->definedname[] = [
20✔
1896
                'isBuiltInName' => $isBuiltInName,
20✔
1897
                'name' => $string['value'],
20✔
1898
                'formula' => $formula,
20✔
1899
                'scope' => $scope,
20✔
1900
            ];
20✔
1901
        }
1902
    }
1903

1904
    /**
1905
     * Read MSODRAWINGGROUP record.
1906
     */
1907
    protected function readMsoDrawingGroup(): void
22✔
1908
    {
1909
        //$length = self::getUInt2d($this->data, $this->pos + 2);
1910

1911
        // get spliced record data
1912
        $splicedRecordData = $this->getSplicedRecordData();
22✔
1913
        /** @var string */
1914
        $recordData = $splicedRecordData['recordData'];
22✔
1915

1916
        $this->drawingGroupData .= $recordData;
22✔
1917
    }
1918

1919
    /**
1920
     * SST - Shared String Table.
1921
     *
1922
     * This record contains a list of all strings used anywhere
1923
     * in the workbook. Each string occurs only once. The
1924
     * workbook uses indexes into the list to reference the
1925
     * strings.
1926
     *
1927
     * --    "OpenOffice.org's Documentation of the Microsoft
1928
     *         Excel File Format"
1929
     */
1930
    protected function readSst(): void
133✔
1931
    {
1932
        // offset within (spliced) record data
1933
        $pos = 0;
133✔
1934

1935
        // Limit global SST position, further control for bad SST Length in BIFF8 data
1936
        $limitposSST = 0;
133✔
1937

1938
        // get spliced record data
1939
        $splicedRecordData = $this->getSplicedRecordData();
133✔
1940

1941
        $recordData = $splicedRecordData['recordData'];
133✔
1942
        /** @var mixed[] */
1943
        $spliceOffsets = $splicedRecordData['spliceOffsets'];
133✔
1944

1945
        // offset: 0; size: 4; total number of strings in the workbook
1946
        $pos += 4;
133✔
1947

1948
        // offset: 4; size: 4; number of following strings ($nm)
1949
        /** @var string $recordData */
1950
        $nm = self::getInt4d($recordData, 4);
133✔
1951
        $pos += 4;
133✔
1952

1953
        // look up limit position (last splice offset where pos fits)
1954
        foreach ($spliceOffsets as $spliceOffset) {
133✔
1955
            // it can happen that the string is empty, therefore we need
1956
            // <= and not just <
1957
            if ($pos <= $spliceOffset) {
133✔
1958
                $limitposSST = $spliceOffset;
133✔
1959
            }
1960
        }
1961

1962
        // loop through the Unicode strings (16-bit length)
1963
        for ($i = 0; $i < $nm && $pos < $limitposSST; ++$i) {
133✔
1964
            // number of characters in the Unicode string
1965
            /** @var int $pos */
1966
            $numChars = self::getUInt2d($recordData, $pos);
80✔
1967
            /** @var int $pos */
1968
            $pos += 2;
80✔
1969

1970
            // option flags
1971
            /** @var string $recordData */
1972
            $optionFlags = ord($recordData[$pos]);
80✔
1973
            ++$pos;
80✔
1974

1975
            // bit: 0; mask: 0x01; 0 = compressed; 1 = uncompressed
1976
            $isCompressed = (($optionFlags & 0x01) == 0);
80✔
1977

1978
            // bit: 2; mask: 0x02; 0 = ordinary; 1 = Asian phonetic
1979
            $hasAsian = (($optionFlags & 0x04) != 0);
80✔
1980

1981
            // bit: 3; mask: 0x03; 0 = ordinary; 1 = Rich-Text
1982
            $hasRichText = (($optionFlags & 0x08) != 0);
80✔
1983

1984
            $formattingRuns = 0;
80✔
1985
            if ($hasRichText) {
80✔
1986
                // number of Rich-Text formatting runs
1987
                $formattingRuns = self::getUInt2d($recordData, $pos);
8✔
1988
                $pos += 2;
8✔
1989
            }
1990

1991
            $extendedRunLength = 0;
80✔
1992
            if ($hasAsian) {
80✔
1993
                // size of Asian phonetic setting
1994
                $extendedRunLength = self::getInt4d($recordData, $pos);
×
1995
                $pos += 4;
×
1996
            }
1997

1998
            // expected byte length of character array if not split
1999
            $len = ($isCompressed) ? $numChars : $numChars * 2;
80✔
2000

2001
            // look up limit position - find the first splice offset at or beyond current pos
2002
            $limitpos = null;
80✔
2003
            foreach ($spliceOffsets as $spliceOffset) {
80✔
2004
                // it can happen that the string is empty, therefore we need
2005
                // <= and not just <
2006
                if ($pos <= $spliceOffset) {
80✔
2007
                    $limitpos = $spliceOffset;
80✔
2008

2009
                    break;
80✔
2010
                }
2011
            }
2012

2013
            /** @var int $limitpos */
2014
            if ($pos + $len <= $limitpos) {
80✔
2015
                // character array is not split between records
2016

2017
                $retstr = substr($recordData, $pos, $len);
80✔
2018
                $pos += $len;
80✔
2019
            } else {
2020
                // character array is split between records
2021

2022
                // first part of character array
2023
                $retstr = substr($recordData, $pos, $limitpos - $pos);
1✔
2024

2025
                $bytesRead = $limitpos - $pos;
1✔
2026

2027
                // remaining characters in Unicode string
2028
                $charsLeft = $numChars - (($isCompressed) ? $bytesRead : ($bytesRead / 2));
1✔
2029

2030
                $pos = $limitpos;
1✔
2031

2032
                // keep reading the characters
2033
                while ($charsLeft > 0) {
1✔
2034
                    // look up next limit position, in case the string spans more than one continue record
2035
                    foreach ($spliceOffsets as $spliceOffset) {
1✔
2036
                        if ($pos < $spliceOffset) {
1✔
2037
                            $limitpos = $spliceOffset;
1✔
2038

2039
                            break;
1✔
2040
                        }
2041
                    }
2042

2043
                    // repeated option flags
2044
                    // OpenOffice.org documentation 5.21
2045
                    /** @var int $pos */
2046
                    $option = ord($recordData[$pos]);
1✔
2047
                    ++$pos;
1✔
2048

2049
                    /** @var int $limitpos */
2050
                    if ($isCompressed && ($option == 0)) {
1✔
2051
                        // 1st fragment compressed
2052
                        // this fragment compressed
2053
                        /** @var int */
2054
                        $len = min($charsLeft, $limitpos - $pos);
×
2055
                        $retstr .= substr($recordData, $pos, $len);
×
2056
                        $charsLeft -= $len;
×
2057
                        $isCompressed = true;
×
2058
                    } elseif (!$isCompressed && ($option != 0)) {
1✔
2059
                        // 1st fragment uncompressed
2060
                        // this fragment uncompressed
2061
                        /** @var int */
2062
                        $len = min($charsLeft * 2, $limitpos - $pos);
1✔
2063
                        $retstr .= substr($recordData, $pos, $len);
1✔
2064
                        $charsLeft -= $len / 2;
1✔
2065
                        $isCompressed = false;
1✔
2066
                    } elseif (!$isCompressed /*&& ($option == 0)*/) {
×
2067
                        // 1st fragment uncompressed
2068
                        // this fragment compressed
NEW
2069
                        $len = (int) min($charsLeft, $limitpos - $pos);
×
2070
                        // Pad each byte with a null byte to expand to UTF-16LE
NEW
2071
                        $retstr .= chunk_split(substr($recordData, $pos, $len), 1, "\x00");
×
2072
                        $charsLeft -= $len;
×
2073
                        $isCompressed = false;
×
2074
                    } else {
2075
                        // 1st fragment compressed
2076
                        // this fragment uncompressed
2077
                        // Pad existing compressed string bytes with null bytes
NEW
2078
                        $retstr = chunk_split($retstr, 1, "\x00");
×
2079
                        /** @var int */
2080
                        $len = min($charsLeft * 2, $limitpos - $pos);
×
2081
                        $retstr .= substr($recordData, $pos, $len);
×
2082
                        $charsLeft -= $len / 2;
×
2083
                        $isCompressed = false;
×
2084
                    }
2085

2086
                    $pos += $len;
1✔
2087
                }
2088
            }
2089

2090
            // convert to UTF-8
2091
            $retstr = self::encodeUTF16($retstr, $isCompressed);
80✔
2092

2093
            // read additional Rich-Text information, if any
2094
            $fmtRuns = [];
80✔
2095
            if ($hasRichText) {
80✔
2096
                // list of formatting runs
2097
                for ($j = 0; $j < $formattingRuns; ++$j) {
8✔
2098
                    // first formatted character; zero-based
2099
                    /** @var int $pos */
2100
                    $charPos = self::getUInt2d($recordData, $pos + $j * 4);
8✔
2101

2102
                    // index to font record
2103
                    $fontIndex = self::getUInt2d($recordData, $pos + 2 + $j * 4);
8✔
2104

2105
                    $fmtRuns[] = [
8✔
2106
                        'charPos' => $charPos,
8✔
2107
                        'fontIndex' => $fontIndex,
8✔
2108
                    ];
8✔
2109
                }
2110
                $pos += 4 * $formattingRuns;
8✔
2111
            }
2112

2113
            // read additional Asian phonetics information, if any
2114
            if ($hasAsian) {
80✔
2115
                // For Asian phonetic settings, we skip the extended string data
2116
                $pos += $extendedRunLength;
×
2117
            }
2118

2119
            // store the shared sting
2120
            $this->sst[] = [
80✔
2121
                'value' => $retstr,
80✔
2122
                'fmtRuns' => $fmtRuns,
80✔
2123
            ];
80✔
2124
        }
2125

2126
        // getSplicedRecordData() takes care of moving current position in data stream
2127
    }
2128

2129
    /**
2130
     * Read PRINTGRIDLINES record.
2131
     */
2132
    protected function readPrintGridlines(): void
133✔
2133
    {
2134
        $length = self::getUInt2d($this->data, $this->pos + 2);
133✔
2135
        $recordData = $this->readRecordData($this->data, $this->pos + 4, $length);
133✔
2136

2137
        // move stream pointer to next record
2138
        $this->pos += 4 + $length;
133✔
2139

2140
        if ($this->version == self::XLS_BIFF8 && !$this->readDataOnly) {
133✔
2141
            // offset: 0; size: 2; 0 = do not print sheet grid lines; 1 = print sheet gridlines
2142
            $printGridlines = (bool) self::getUInt2d($recordData, 0);
129✔
2143
            $this->phpSheet->setPrintGridlines($printGridlines);
129✔
2144
        }
2145
    }
2146

2147
    /**
2148
     * Read DEFAULTROWHEIGHT record.
2149
     */
2150
    protected function readDefaultRowHeight(): void
67✔
2151
    {
2152
        $length = self::getUInt2d($this->data, $this->pos + 2);
67✔
2153
        $recordData = $this->readRecordData($this->data, $this->pos + 4, $length);
67✔
2154

2155
        // move stream pointer to next record
2156
        $this->pos += 4 + $length;
67✔
2157

2158
        // offset: 0; size: 2; option flags
2159
        // offset: 2; size: 2; default height for unused rows, (twips 1/20 point)
2160
        $height = self::getUInt2d($recordData, 2);
67✔
2161
        $this->phpSheet->getDefaultRowDimension()->setRowHeight($height / 20);
67✔
2162
    }
2163

2164
    /**
2165
     * Read SHEETPR record.
2166
     */
2167
    protected function readSheetPr(): void
135✔
2168
    {
2169
        $length = self::getUInt2d($this->data, $this->pos + 2);
135✔
2170
        $recordData = $this->readRecordData($this->data, $this->pos + 4, $length);
135✔
2171

2172
        // move stream pointer to next record
2173
        $this->pos += 4 + $length;
135✔
2174

2175
        // offset: 0; size: 2
2176

2177
        // bit: 6; mask: 0x0040; 0 = outline buttons above outline group
2178
        $isSummaryBelow = (0x0040 & self::getUInt2d($recordData, 0)) >> 6;
135✔
2179
        $this->phpSheet->setShowSummaryBelow((bool) $isSummaryBelow);
135✔
2180

2181
        // bit: 7; mask: 0x0080; 0 = outline buttons left of outline group
2182
        $isSummaryRight = (0x0080 & self::getUInt2d($recordData, 0)) >> 7;
135✔
2183
        $this->phpSheet->setShowSummaryRight((bool) $isSummaryRight);
135✔
2184

2185
        // bit: 8; mask: 0x100; 0 = scale printout in percent, 1 = fit printout to number of pages
2186
        // this corresponds to radio button setting in page setup dialog in Excel
2187
        $this->isFitToPages = (bool) ((0x0100 & self::getUInt2d($recordData, 0)) >> 8);
135✔
2188
    }
2189

2190
    /**
2191
     * Read HORIZONTALPAGEBREAKS record.
2192
     */
2193
    protected function readHorizontalPageBreaks(): void
5✔
2194
    {
2195
        $length = self::getUInt2d($this->data, $this->pos + 2);
5✔
2196
        $recordData = $this->readRecordData($this->data, $this->pos + 4, $length);
5✔
2197

2198
        // move stream pointer to next record
2199
        $this->pos += 4 + $length;
5✔
2200

2201
        if ($this->version == self::XLS_BIFF8 && !$this->readDataOnly) {
5✔
2202
            // offset: 0; size: 2; number of the following row index structures
2203
            $nm = self::getUInt2d($recordData, 0);
5✔
2204

2205
            // offset: 2; size: 6 * $nm; list of $nm row index structures
2206
            for ($i = 0; $i < $nm; ++$i) {
5✔
2207
                $r = self::getUInt2d($recordData, 2 + 6 * $i);
3✔
2208
                $cf = self::getUInt2d($recordData, 2 + 6 * $i + 2);
3✔
2209
                //$cl = self::getUInt2d($recordData, 2 + 6 * $i + 4);
2210

2211
                // not sure why two column indexes are necessary?
2212
                $this->phpSheet->setBreak([$cf + 1, $r], Worksheet::BREAK_ROW);
3✔
2213
            }
2214
        }
2215
    }
2216

2217
    /**
2218
     * Read VERTICALPAGEBREAKS record.
2219
     */
2220
    protected function readVerticalPageBreaks(): void
5✔
2221
    {
2222
        $length = self::getUInt2d($this->data, $this->pos + 2);
5✔
2223
        $recordData = $this->readRecordData($this->data, $this->pos + 4, $length);
5✔
2224

2225
        // move stream pointer to next record
2226
        $this->pos += 4 + $length;
5✔
2227

2228
        if ($this->version == self::XLS_BIFF8 && !$this->readDataOnly) {
5✔
2229
            // offset: 0; size: 2; number of the following column index structures
2230
            $nm = self::getUInt2d($recordData, 0);
5✔
2231

2232
            // offset: 2; size: 6 * $nm; list of $nm row index structures
2233
            for ($i = 0; $i < $nm; ++$i) {
5✔
2234
                $c = self::getUInt2d($recordData, 2 + 6 * $i);
3✔
2235
                $rf = self::getUInt2d($recordData, 2 + 6 * $i + 2);
3✔
2236
                //$rl = self::getUInt2d($recordData, 2 + 6 * $i + 4);
2237

2238
                // not sure why two row indexes are necessary?
2239
                $this->phpSheet->setBreak([$c + 1, ($rf > 0) ? $rf : 1], Worksheet::BREAK_COLUMN);
3✔
2240
            }
2241
        }
2242
    }
2243

2244
    /**
2245
     * Read HEADER record.
2246
     */
2247
    protected function readHeader(): void
133✔
2248
    {
2249
        $length = self::getUInt2d($this->data, $this->pos + 2);
133✔
2250
        $recordData = $this->readRecordData($this->data, $this->pos + 4, $length);
133✔
2251

2252
        // move stream pointer to next record
2253
        $this->pos += 4 + $length;
133✔
2254

2255
        if (!$this->readDataOnly) {
133✔
2256
            // offset: 0; size: var
2257
            // realized that $recordData can be empty even when record exists
2258
            if ($recordData) {
131✔
2259
                if ($this->version == self::XLS_BIFF8) {
83✔
2260
                    $string = self::readUnicodeStringLong($recordData);
82✔
2261
                } else {
2262
                    $string = $this->readByteStringShort($recordData);
1✔
2263
                }
2264

2265
                /** @var string[] $string */
2266
                $this->phpSheet
83✔
2267
                    ->getHeaderFooter()
83✔
2268
                    ->setOddHeader($string['value']);
83✔
2269
                $this->phpSheet
83✔
2270
                    ->getHeaderFooter()
83✔
2271
                    ->setEvenHeader($string['value']);
83✔
2272
            }
2273
        }
2274
    }
2275

2276
    /**
2277
     * Read FOOTER record.
2278
     */
2279
    protected function readFooter(): void
133✔
2280
    {
2281
        $length = self::getUInt2d($this->data, $this->pos + 2);
133✔
2282
        $recordData = $this->readRecordData($this->data, $this->pos + 4, $length);
133✔
2283

2284
        // move stream pointer to next record
2285
        $this->pos += 4 + $length;
133✔
2286

2287
        if (!$this->readDataOnly) {
133✔
2288
            // offset: 0; size: var
2289
            // realized that $recordData can be empty even when record exists
2290
            if ($recordData) {
131✔
2291
                if ($this->version == self::XLS_BIFF8) {
85✔
2292
                    $string = self::readUnicodeStringLong($recordData);
83✔
2293
                } else {
2294
                    $string = $this->readByteStringShort($recordData);
2✔
2295
                }
2296
                /** @var string */
2297
                $temp = $string['value'];
85✔
2298
                $this->phpSheet
85✔
2299
                    ->getHeaderFooter()
85✔
2300
                    ->setOddFooter($temp);
85✔
2301
                $this->phpSheet
85✔
2302
                    ->getHeaderFooter()
85✔
2303
                    ->setEvenFooter($temp);
85✔
2304
            }
2305
        }
2306
    }
2307

2308
    /**
2309
     * Read HCENTER record.
2310
     */
2311
    protected function readHcenter(): void
133✔
2312
    {
2313
        $length = self::getUInt2d($this->data, $this->pos + 2);
133✔
2314
        $recordData = $this->readRecordData($this->data, $this->pos + 4, $length);
133✔
2315

2316
        // move stream pointer to next record
2317
        $this->pos += 4 + $length;
133✔
2318

2319
        if (!$this->readDataOnly) {
133✔
2320
            // offset: 0; size: 2; 0 = print sheet left aligned, 1 = print sheet centered horizontally
2321
            $isHorizontalCentered = (bool) self::getUInt2d($recordData, 0);
131✔
2322

2323
            $this->phpSheet->getPageSetup()->setHorizontalCentered($isHorizontalCentered);
131✔
2324
        }
2325
    }
2326

2327
    /**
2328
     * Read VCENTER record.
2329
     */
2330
    protected function readVcenter(): void
133✔
2331
    {
2332
        $length = self::getUInt2d($this->data, $this->pos + 2);
133✔
2333
        $recordData = $this->readRecordData($this->data, $this->pos + 4, $length);
133✔
2334

2335
        // move stream pointer to next record
2336
        $this->pos += 4 + $length;
133✔
2337

2338
        if (!$this->readDataOnly) {
133✔
2339
            // offset: 0; size: 2; 0 = print sheet aligned at top page border, 1 = print sheet vertically centered
2340
            $isVerticalCentered = (bool) self::getUInt2d($recordData, 0);
131✔
2341

2342
            $this->phpSheet->getPageSetup()->setVerticalCentered($isVerticalCentered);
131✔
2343
        }
2344
    }
2345

2346
    /**
2347
     * Read LEFTMARGIN record.
2348
     */
2349
    protected function readLeftMargin(): void
126✔
2350
    {
2351
        $length = self::getUInt2d($this->data, $this->pos + 2);
126✔
2352
        $recordData = $this->readRecordData($this->data, $this->pos + 4, $length);
126✔
2353

2354
        // move stream pointer to next record
2355
        $this->pos += 4 + $length;
126✔
2356

2357
        if (!$this->readDataOnly) {
126✔
2358
            // offset: 0; size: 8
2359
            $this->phpSheet->getPageMargins()->setLeft(self::extractNumber($recordData));
124✔
2360
        }
2361
    }
2362

2363
    /**
2364
     * Read RIGHTMARGIN record.
2365
     */
2366
    protected function readRightMargin(): void
126✔
2367
    {
2368
        $length = self::getUInt2d($this->data, $this->pos + 2);
126✔
2369
        $recordData = $this->readRecordData($this->data, $this->pos + 4, $length);
126✔
2370

2371
        // move stream pointer to next record
2372
        $this->pos += 4 + $length;
126✔
2373

2374
        if (!$this->readDataOnly) {
126✔
2375
            // offset: 0; size: 8
2376
            $this->phpSheet->getPageMargins()->setRight(self::extractNumber($recordData));
124✔
2377
        }
2378
    }
2379

2380
    /**
2381
     * Read TOPMARGIN record.
2382
     */
2383
    protected function readTopMargin(): void
126✔
2384
    {
2385
        $length = self::getUInt2d($this->data, $this->pos + 2);
126✔
2386
        $recordData = $this->readRecordData($this->data, $this->pos + 4, $length);
126✔
2387

2388
        // move stream pointer to next record
2389
        $this->pos += 4 + $length;
126✔
2390

2391
        if (!$this->readDataOnly) {
126✔
2392
            // offset: 0; size: 8
2393
            $this->phpSheet->getPageMargins()->setTop(self::extractNumber($recordData));
124✔
2394
        }
2395
    }
2396

2397
    /**
2398
     * Read BOTTOMMARGIN record.
2399
     */
2400
    protected function readBottomMargin(): void
126✔
2401
    {
2402
        $length = self::getUInt2d($this->data, $this->pos + 2);
126✔
2403
        $recordData = $this->readRecordData($this->data, $this->pos + 4, $length);
126✔
2404

2405
        // move stream pointer to next record
2406
        $this->pos += 4 + $length;
126✔
2407

2408
        if (!$this->readDataOnly) {
126✔
2409
            // offset: 0; size: 8
2410
            $this->phpSheet->getPageMargins()->setBottom(self::extractNumber($recordData));
124✔
2411
        }
2412
    }
2413

2414
    /**
2415
     * Read PAGESETUP record.
2416
     */
2417
    protected function readPageSetup(): void
135✔
2418
    {
2419
        $length = self::getUInt2d($this->data, $this->pos + 2);
135✔
2420
        $recordData = $this->readRecordData($this->data, $this->pos + 4, $length);
135✔
2421

2422
        // move stream pointer to next record
2423
        $this->pos += 4 + $length;
135✔
2424

2425
        if (!$this->readDataOnly) {
135✔
2426
            // offset: 0; size: 2; paper size
2427
            $paperSize = self::getUInt2d($recordData, 0);
133✔
2428

2429
            // offset: 2; size: 2; scaling factor
2430
            $scale = self::getUInt2d($recordData, 2);
133✔
2431

2432
            // offset: 6; size: 2; fit worksheet width to this number of pages, 0 = use as many as needed
2433
            $fitToWidth = self::getUInt2d($recordData, 6);
133✔
2434

2435
            // offset: 8; size: 2; fit worksheet height to this number of pages, 0 = use as many as needed
2436
            $fitToHeight = self::getUInt2d($recordData, 8);
133✔
2437

2438
            // offset: 10; size: 2; option flags
2439

2440
            // bit: 0; mask: 0x0001; 0=down then over, 1=over then down
2441
            $isOverThenDown = (0x0001 & self::getUInt2d($recordData, 10));
133✔
2442

2443
            // bit: 1; mask: 0x0002; 0=landscape, 1=portrait
2444
            $isPortrait = (0x0002 & self::getUInt2d($recordData, 10)) >> 1;
133✔
2445

2446
            // bit: 2; mask: 0x0004; 1= paper size, scaling factor, paper orient. not init
2447
            // when this bit is set, do not use flags for those properties
2448
            $isNotInit = (0x0004 & self::getUInt2d($recordData, 10)) >> 2;
133✔
2449

2450
            if (!$isNotInit) {
133✔
2451
                $this->phpSheet->getPageSetup()->setPaperSize($paperSize);
122✔
2452
                $this->phpSheet->getPageSetup()->setPageOrder(((bool) $isOverThenDown) ? PageSetup::PAGEORDER_OVER_THEN_DOWN : PageSetup::PAGEORDER_DOWN_THEN_OVER);
122✔
2453
                $this->phpSheet->getPageSetup()->setOrientation(((bool) $isPortrait) ? PageSetup::ORIENTATION_PORTRAIT : PageSetup::ORIENTATION_LANDSCAPE);
122✔
2454

2455
                $this->phpSheet->getPageSetup()->setScale($scale, false);
122✔
2456
                $this->phpSheet->getPageSetup()->setFitToPage((bool) $this->isFitToPages);
122✔
2457
                $this->phpSheet->getPageSetup()->setFitToWidth($fitToWidth, false);
122✔
2458
                $this->phpSheet->getPageSetup()->setFitToHeight($fitToHeight, false);
122✔
2459
            }
2460

2461
            // offset: 16; size: 8; header margin (IEEE 754 floating-point value)
2462
            $marginHeader = self::extractNumber(substr($recordData, 16, 8));
133✔
2463
            $this->phpSheet->getPageMargins()->setHeader($marginHeader);
133✔
2464

2465
            // offset: 24; size: 8; footer margin (IEEE 754 floating-point value)
2466
            $marginFooter = self::extractNumber(substr($recordData, 24, 8));
133✔
2467
            $this->phpSheet->getPageMargins()->setFooter($marginFooter);
133✔
2468
        }
2469
    }
2470

2471
    /**
2472
     * PROTECT - Sheet protection (BIFF2 through BIFF8)
2473
     *   if this record is omitted, then it also means no sheet protection.
2474
     */
2475
    protected function readProtect(): void
7✔
2476
    {
2477
        $length = self::getUInt2d($this->data, $this->pos + 2);
7✔
2478
        $recordData = $this->readRecordData($this->data, $this->pos + 4, $length);
7✔
2479

2480
        // move stream pointer to next record
2481
        $this->pos += 4 + $length;
7✔
2482

2483
        if ($this->readDataOnly) {
7✔
2484
            return;
×
2485
        }
2486

2487
        // offset: 0; size: 2;
2488

2489
        // bit 0, mask 0x01; 1 = sheet is protected
2490
        $bool = (0x01 & self::getUInt2d($recordData, 0)) >> 0;
7✔
2491
        $this->phpSheet->getProtection()->setSheet((bool) $bool);
7✔
2492
    }
2493

2494
    /**
2495
     * SCENPROTECT.
2496
     */
2497
    protected function readScenProtect(): void
×
2498
    {
2499
        $length = self::getUInt2d($this->data, $this->pos + 2);
×
2500
        $recordData = $this->readRecordData($this->data, $this->pos + 4, $length);
×
2501

2502
        // move stream pointer to next record
2503
        $this->pos += 4 + $length;
×
2504

2505
        if ($this->readDataOnly) {
×
2506
            return;
×
2507
        }
2508

2509
        // offset: 0; size: 2;
2510

2511
        // bit: 0, mask 0x01; 1 = scenarios are protected
2512
        $bool = (0x01 & self::getUInt2d($recordData, 0)) >> 0;
×
2513

2514
        $this->phpSheet->getProtection()->setScenarios((bool) $bool);
×
2515
    }
2516

2517
    /**
2518
     * OBJECTPROTECT.
2519
     */
2520
    protected function readObjectProtect(): void
2✔
2521
    {
2522
        $length = self::getUInt2d($this->data, $this->pos + 2);
2✔
2523
        $recordData = $this->readRecordData($this->data, $this->pos + 4, $length);
2✔
2524

2525
        // move stream pointer to next record
2526
        $this->pos += 4 + $length;
2✔
2527

2528
        if ($this->readDataOnly) {
2✔
2529
            return;
×
2530
        }
2531

2532
        // offset: 0; size: 2;
2533

2534
        // bit: 0, mask 0x01; 1 = objects are protected
2535
        $bool = (0x01 & self::getUInt2d($recordData, 0)) >> 0;
2✔
2536

2537
        $this->phpSheet->getProtection()->setObjects((bool) $bool);
2✔
2538
    }
2539

2540
    /**
2541
     * PASSWORD - Sheet protection (hashed) password (BIFF2 through BIFF8).
2542
     */
2543
    protected function readPassword(): void
3✔
2544
    {
2545
        $length = self::getUInt2d($this->data, $this->pos + 2);
3✔
2546
        $recordData = $this->readRecordData($this->data, $this->pos + 4, $length);
3✔
2547

2548
        // move stream pointer to next record
2549
        $this->pos += 4 + $length;
3✔
2550

2551
        if (!$this->readDataOnly) {
3✔
2552
            // offset: 0; size: 2; 16-bit hash value of password
2553
            $password = strtoupper(dechex(self::getUInt2d($recordData, 0))); // the hashed password
3✔
2554
            $this->phpSheet->getProtection()->setPassword($password, true);
3✔
2555
        }
2556
    }
2557

2558
    /**
2559
     * Read DEFCOLWIDTH record.
2560
     */
2561
    protected function readDefColWidth(): void
134✔
2562
    {
2563
        $length = self::getUInt2d($this->data, $this->pos + 2);
134✔
2564
        $recordData = $this->readRecordData($this->data, $this->pos + 4, $length);
134✔
2565

2566
        // move stream pointer to next record
2567
        $this->pos += 4 + $length;
134✔
2568

2569
        // offset: 0; size: 2; default column width
2570
        $width = self::getUInt2d($recordData, 0);
134✔
2571
        if ($width != 8) {
134✔
2572
            $this->phpSheet->getDefaultColumnDimension()->setWidth($width);
5✔
2573
        }
2574
    }
2575

2576
    /**
2577
     * Read COLINFO record.
2578
     */
2579
    protected function readColInfo(): void
120✔
2580
    {
2581
        $length = self::getUInt2d($this->data, $this->pos + 2);
120✔
2582
        $recordData = $this->readRecordData($this->data, $this->pos + 4, $length);
120✔
2583

2584
        // move stream pointer to next record
2585
        $this->pos += 4 + $length;
120✔
2586

2587
        if (!$this->readDataOnly) {
120✔
2588
            // offset: 0; size: 2; index to first column in range
2589
            $firstColumnIndex = self::getUInt2d($recordData, 0);
118✔
2590

2591
            // offset: 2; size: 2; index to last column in range
2592
            $lastColumnIndex = self::getUInt2d($recordData, 2);
118✔
2593

2594
            // offset: 4; size: 2; width of the column in 1/256 of the width of the zero character
2595
            $width = self::getUInt2d($recordData, 4);
118✔
2596

2597
            // offset: 6; size: 2; index to XF record for default column formatting
2598
            $xfIndex = self::getUInt2d($recordData, 6);
118✔
2599

2600
            // offset: 8; size: 2; option flags
2601
            // bit: 0; mask: 0x0001; 1= columns are hidden
2602
            $isHidden = (0x0001 & self::getUInt2d($recordData, 8)) >> 0;
118✔
2603

2604
            // bit: 10-8; mask: 0x0700; outline level of the columns (0 = no outline)
2605
            $level = (0x0700 & self::getUInt2d($recordData, 8)) >> 8;
118✔
2606

2607
            // bit: 12; mask: 0x1000; 1 = collapsed
2608
            $isCollapsed = (bool) ((0x1000 & self::getUInt2d($recordData, 8)) >> 12);
118✔
2609

2610
            // offset: 10; size: 2; not used
2611

2612
            for ($i = $firstColumnIndex + 1; $i <= $lastColumnIndex + 1; ++$i) {
118✔
2613
                if ($lastColumnIndex == AddressRange::MAX_COLUMN_INT_XLS - 1 || $lastColumnIndex == AddressRange::MAX_COLUMN_INT) {
118✔
2614
                    $this->phpSheet->getDefaultColumnDimension()->setWidth($width / 256);
1✔
2615

2616
                    break;
1✔
2617
                }
2618
                $this->phpSheet->getColumnDimensionByColumn($i)->setWidth($width / 256);
118✔
2619
                $this->phpSheet->getColumnDimensionByColumn($i)->setVisible(!$isHidden);
118✔
2620
                $this->phpSheet->getColumnDimensionByColumn($i)->setOutlineLevel($level);
118✔
2621
                $this->phpSheet->getColumnDimensionByColumn($i)->setCollapsed($isCollapsed);
118✔
2622
                if (isset($this->mapCellXfIndex[$xfIndex])) {
118✔
2623
                    $this->phpSheet->getColumnDimensionByColumn($i)->setXfIndex($this->mapCellXfIndex[$xfIndex]);
116✔
2624
                }
2625
            }
2626
        }
2627
    }
2628

2629
    /**
2630
     * ROW.
2631
     *
2632
     * This record contains the properties of a single row in a
2633
     * sheet. Rows and cells in a sheet are divided into blocks
2634
     * of 32 rows.
2635
     *
2636
     * --    "OpenOffice.org's Documentation of the Microsoft
2637
     *         Excel File Format"
2638
     */
2639
    protected function readRow(): void
76✔
2640
    {
2641
        $length = self::getUInt2d($this->data, $this->pos + 2);
76✔
2642
        $recordData = $this->readRecordData($this->data, $this->pos + 4, $length);
76✔
2643

2644
        // move stream pointer to next record
2645
        $this->pos += 4 + $length;
76✔
2646

2647
        if (!$this->readDataOnly) {
76✔
2648
            // offset: 0; size: 2; index of this row
2649
            $r = self::getUInt2d($recordData, 0);
74✔
2650

2651
            // offset: 2; size: 2; index to column of the first cell which is described by a cell record
2652

2653
            // offset: 4; size: 2; index to column of the last cell which is described by a cell record, increased by 1
2654

2655
            // offset: 6; size: 2;
2656

2657
            // bit: 14-0; mask: 0x7FFF; height of the row, in twips = 1/20 of a point
2658
            $height = (0x7FFF & self::getUInt2d($recordData, 6)) >> 0;
74✔
2659

2660
            // bit: 15: mask: 0x8000; 0 = row has custom height; 1= row has default height
2661
            $useDefaultHeight = (0x8000 & self::getUInt2d($recordData, 6)) >> 15;
74✔
2662

2663
            if (!$useDefaultHeight) {
74✔
2664
                if (
2665
                    $this->phpSheet->getDefaultRowDimension()->getRowHeight() > 0
72✔
2666
                ) {
2667
                    $this->phpSheet->getRowDimension($r + 1)
60✔
2668
                        ->setCustomFormat(true, ($height === 255) ? -1 : ($height / 20));
60✔
2669
                } else {
2670
                    $this->phpSheet->getRowDimension($r + 1)->setRowHeight($height / 20);
12✔
2671
                }
2672
            }
2673

2674
            // offset: 8; size: 2; not used
2675

2676
            // offset: 10; size: 2; not used in BIFF5-BIFF8
2677

2678
            // offset: 12; size: 4; option flags and default row formatting
2679

2680
            // bit: 2-0: mask: 0x00000007; outline level of the row
2681
            $level = (0x00000007 & self::getInt4d($recordData, 12)) >> 0;
74✔
2682
            $this->phpSheet->getRowDimension($r + 1)->setOutlineLevel($level);
74✔
2683

2684
            // bit: 4; mask: 0x00000010; 1 = outline group start or ends here... and is collapsed
2685
            $isCollapsed = (bool) ((0x00000010 & self::getInt4d($recordData, 12)) >> 4);
74✔
2686
            $this->phpSheet->getRowDimension($r + 1)->setCollapsed($isCollapsed);
74✔
2687

2688
            // bit: 5; mask: 0x00000020; 1 = row is hidden
2689
            $isHidden = (0x00000020 & self::getInt4d($recordData, 12)) >> 5;
74✔
2690
            $this->phpSheet->getRowDimension($r + 1)->setVisible(!$isHidden);
74✔
2691

2692
            // bit: 7; mask: 0x00000080; 1 = row has explicit format
2693
            $hasExplicitFormat = (0x00000080 & self::getInt4d($recordData, 12)) >> 7;
74✔
2694

2695
            // bit: 27-16; mask: 0x0FFF0000; only applies when hasExplicitFormat = 1; index to XF record
2696
            $xfIndex = (0x0FFF0000 & self::getInt4d($recordData, 12)) >> 16;
74✔
2697

2698
            if ($hasExplicitFormat && isset($this->mapCellXfIndex[$xfIndex])) {
74✔
2699
                $this->phpSheet->getRowDimension($r + 1)->setXfIndex($this->mapCellXfIndex[$xfIndex]);
6✔
2700
            }
2701
        }
2702
    }
2703

2704
    /**
2705
     * Read RK record
2706
     * This record represents a cell that contains an RK value
2707
     * (encoded integer or floating-point value). If a
2708
     * floating-point value cannot be encoded to an RK value,
2709
     * a NUMBER record will be written. This record replaces the
2710
     * record INTEGER written in BIFF2.
2711
     *
2712
     * --    "OpenOffice.org's Documentation of the Microsoft
2713
     *         Excel File Format"
2714
     */
2715
    protected function readRk(): void
37✔
2716
    {
2717
        $length = self::getUInt2d($this->data, $this->pos + 2);
37✔
2718
        $recordData = $this->readRecordData($this->data, $this->pos + 4, $length);
37✔
2719

2720
        // move stream pointer to next record
2721
        $this->pos += 4 + $length;
37✔
2722

2723
        // offset: 0; size: 2; index to row
2724
        $row = self::getUInt2d($recordData, 0);
37✔
2725

2726
        // offset: 2; size: 2; index to column
2727
        $column = self::getUInt2d($recordData, 2);
37✔
2728
        $columnString = Coordinate::stringFromColumnIndex($column + 1);
37✔
2729
        $cellCoordinate = $columnString . ($row + 1);
37✔
2730

2731
        // Read cell?
2732
        if ($this->readFilter->readCell($columnString, $row + 1, $this->phpSheetTitle)) {
37✔
2733
            // offset: 4; size: 2; index to XF record
2734
            $xfIndex = self::getUInt2d($recordData, 4);
37✔
2735

2736
            // offset: 6; size: 4; RK value
2737
            $rknum = self::getInt4d($recordData, 6);
37✔
2738
            $numValue = self::getIEEE754($rknum);
37✔
2739

2740
            $cell = $this->phpSheet->getCell($cellCoordinate);
37✔
2741
            if (!$this->readDataOnly && isset($this->mapCellXfIndex[$xfIndex])) {
37✔
2742
                // add style information
2743
                $cell->setXfIndexNoUpdate($this->mapCellXfIndex[$xfIndex]);
33✔
2744
            }
2745

2746
            // add cell
2747
            $cell->setValueExplicit($numValue, DataType::TYPE_NUMERIC);
37✔
2748
        }
2749
    }
2750

2751
    /**
2752
     * Read LABELSST record
2753
     * This record represents a cell that contains a string. It
2754
     * replaces the LABEL record and RSTRING record used in
2755
     * BIFF2-BIFF5.
2756
     *
2757
     * --    "OpenOffice.org's Documentation of the Microsoft
2758
     *         Excel File Format"
2759
     */
2760
    protected function readLabelSst(): void
77✔
2761
    {
2762
        $length = self::getUInt2d($this->data, $this->pos + 2);
77✔
2763
        $recordData = $this->readRecordData($this->data, $this->pos + 4, $length);
77✔
2764

2765
        // move stream pointer to next record
2766
        $this->pos += 4 + $length;
77✔
2767

2768
        // offset: 0; size: 2; index to row
2769
        $row = self::getUInt2d($recordData, 0);
77✔
2770

2771
        // offset: 2; size: 2; index to column
2772
        $column = self::getUInt2d($recordData, 2);
77✔
2773
        $columnString = Coordinate::stringFromColumnIndex($column + 1);
77✔
2774
        $cellCoordinate = $columnString . ($row + 1);
77✔
2775

2776
        // Read cell?
2777
        if ($this->readFilter->readCell($columnString, $row + 1, $this->phpSheetTitle)) {
77✔
2778
            // offset: 4; size: 2; index to XF record
2779
            $xfIndex = self::getUInt2d($recordData, 4);
77✔
2780

2781
            // offset: 6; size: 4; index to SST record
2782
            $index = self::getInt4d($recordData, 6);
77✔
2783

2784
            // cache SST entry locally to avoid repeated array lookups
2785
            $sstValue = $this->sst[$index]['value'];
77✔
2786
            $fmtRuns = $this->sst[$index]['fmtRuns'];
77✔
2787

2788
            // add cell
2789
            if ($fmtRuns && !$this->readDataOnly) {
77✔
2790
                // then we should treat as rich text
2791
                $richText = new RichText();
6✔
2792
                $charPos = 0;
6✔
2793
                $sstCount = count($fmtRuns);
6✔
2794
                $sstValueLength = StringHelper::countCharacters($sstValue);
6✔
2795
                $lastFontIndex = count($this->objFonts) - 1;
6✔
2796
                for ($i = 0; $i <= $sstCount; ++$i) {
6✔
2797
                    /** @var mixed[][] $fmtRuns */
2798
                    if (isset($fmtRuns[$i])) {
6✔
2799
                        /** @var int[] */
2800
                        $temp = $fmtRuns[$i];
6✔
2801
                        $temp = $temp['charPos'];
6✔
2802
                        /** @var int $charPos */
2803
                        $text = StringHelper::substring($sstValue, $charPos, $temp - $charPos);
6✔
2804
                        $charPos = $temp;
6✔
2805
                    } else {
2806
                        $text = StringHelper::substring($sstValue, $charPos, $sstValueLength);
6✔
2807
                    }
2808

2809
                    if (StringHelper::countCharacters($text) > 0) {
6✔
2810
                        if ($i == 0) { // first text run, no style
6✔
2811
                            $richText->createText($text);
3✔
2812
                        } else {
2813
                            $textRun = $richText->createTextRun($text);
6✔
2814
                            /** @var int[][] $fmtRuns */
2815
                            if (isset($fmtRuns[$i - 1])) {
6✔
2816
                                if ($fmtRuns[$i - 1]['fontIndex'] < 4) {
6✔
2817
                                    $fontIndex = $fmtRuns[$i - 1]['fontIndex'];
5✔
2818
                                } else {
2819
                                    // this has to do with that index 4 is omitted in all BIFF versions for some stra          nge reason
2820
                                    // check the OpenOffice documentation of the FONT record
2821
                                    /** @var int */
2822
                                    $temp = $fmtRuns[$i - 1]['fontIndex'];
4✔
2823
                                    $fontIndex = $temp - 1;
4✔
2824
                                }
2825
                                if ($fontIndex > $lastFontIndex) {
6✔
2826
                                    $fontIndex = $lastFontIndex;
1✔
2827
                                }
2828
                                $textRun->setFont(clone $this->objFonts[$fontIndex]);
6✔
2829
                            }
2830
                        }
2831
                    }
2832
                }
2833
                if ($this->readEmptyCells || trim($richText->getPlainText()) !== '') {
6✔
2834
                    $cell = $this->phpSheet->getCell($cellCoordinate);
6✔
2835
                    if (isset($this->mapCellXfIndex[$xfIndex])) {
6✔
2836
                        $cell->setXfIndexNoUpdate($this->mapCellXfIndex[$xfIndex]);
6✔
2837
                    }
2838
                    $cell->setValueExplicit($richText, DataType::TYPE_STRING);
6✔
2839
                }
2840
            } else {
2841
                if ($this->readEmptyCells || trim($sstValue) !== '') {
76✔
2842
                    $cell = $this->phpSheet->getCell($cellCoordinate);
76✔
2843
                    if (!$this->readDataOnly && isset($this->mapCellXfIndex[$xfIndex])) {
76✔
2844
                        $cell->setXfIndexNoUpdate($this->mapCellXfIndex[$xfIndex]);
74✔
2845
                    }
2846
                    $cell->setValueExplicit($sstValue, DataType::TYPE_STRING);
76✔
2847
                }
2848
            }
2849
        }
2850
    }
2851

2852
    /**
2853
     * Read MULRK record
2854
     * This record represents a cell range containing RK value
2855
     * cells. All cells are located in the same row.
2856
     *
2857
     * --    "OpenOffice.org's Documentation of the Microsoft
2858
     *         Excel File Format"
2859
     */
2860
    protected function readMulRk(): void
22✔
2861
    {
2862
        $length = self::getUInt2d($this->data, $this->pos + 2);
22✔
2863
        $recordData = $this->readRecordData($this->data, $this->pos + 4, $length);
22✔
2864

2865
        // move stream pointer to next record
2866
        $this->pos += 4 + $length;
22✔
2867

2868
        // offset: 0; size: 2; index to row
2869
        $row = self::getUInt2d($recordData, 0);
22✔
2870

2871
        // offset: 2; size: 2; index to first column
2872
        $colFirst = self::getUInt2d($recordData, 2);
22✔
2873

2874
        // offset: var; size: 2; index to last column
2875
        $colLast = self::getUInt2d($recordData, $length - 2);
22✔
2876
        $columns = $colLast - $colFirst + 1;
22✔
2877

2878
        // offset within record data
2879
        $offset = 4;
22✔
2880

2881
        $rowIndex = $row + 1;
22✔
2882
        for ($i = 1; $i <= $columns; ++$i) {
22✔
2883
            $columnString = Coordinate::stringFromColumnIndex($colFirst + $i);
22✔
2884

2885
            // Read cell?
2886
            if ($this->readFilter->readCell($columnString, $rowIndex, $this->phpSheetTitle)) {
22✔
2887
                // offset: var; size: 2; index to XF record
2888
                $xfIndex = self::getUInt2d($recordData, $offset);
22✔
2889

2890
                // offset: var; size: 4; RK value
2891
                $numValue = self::getIEEE754(self::getInt4d($recordData, $offset + 2));
22✔
2892
                $cell = $this->phpSheet->getCell($columnString . $rowIndex);
22✔
2893
                if (!$this->readDataOnly && isset($this->mapCellXfIndex[$xfIndex])) {
22✔
2894
                    // add style
2895
                    $cell->setXfIndexNoUpdate($this->mapCellXfIndex[$xfIndex]);
20✔
2896
                }
2897

2898
                // add cell value
2899
                $cell->setValueExplicit($numValue, DataType::TYPE_NUMERIC);
22✔
2900
            }
2901

2902
            $offset += 6;
22✔
2903
        }
2904
    }
2905

2906
    /**
2907
     * Read NUMBER record
2908
     * This record represents a cell that contains a
2909
     * floating-point value.
2910
     *
2911
     * --    "OpenOffice.org's Documentation of the Microsoft
2912
     *         Excel File Format"
2913
     */
2914
    protected function readNumber(): void
60✔
2915
    {
2916
        $length = self::getUInt2d($this->data, $this->pos + 2);
60✔
2917
        $recordData = $this->readRecordData($this->data, $this->pos + 4, $length);
60✔
2918

2919
        // move stream pointer to next record
2920
        $this->pos += 4 + $length;
60✔
2921

2922
        // offset: 0; size: 2; index to row
2923
        $row = self::getUInt2d($recordData, 0);
60✔
2924

2925
        // offset: 2; size 2; index to column
2926
        $column = self::getUInt2d($recordData, 2);
60✔
2927
        $columnString = Coordinate::stringFromColumnIndex($column + 1);
60✔
2928
        $cellCoordinate = $columnString . ($row + 1);
60✔
2929

2930
        // Read cell?
2931
        if ($this->readFilter->readCell($columnString, $row + 1, $this->phpSheetTitle)) {
60✔
2932
            // offset 4; size: 2; index to XF record
2933
            $xfIndex = self::getUInt2d($recordData, 4);
60✔
2934

2935
            $numValue = self::extractNumber(substr($recordData, 6, 8));
60✔
2936

2937
            $cell = $this->phpSheet->getCell($cellCoordinate);
60✔
2938
            if (!$this->readDataOnly && isset($this->mapCellXfIndex[$xfIndex])) {
60✔
2939
                // add cell style
2940
                $cell->setXfIndexNoUpdate($this->mapCellXfIndex[$xfIndex]);
58✔
2941
            }
2942

2943
            // add cell value
2944
            $cell->setValueExplicit($numValue, DataType::TYPE_NUMERIC);
60✔
2945
        }
2946
    }
2947

2948
    /**
2949
     * Read FORMULA record + perhaps a following STRING record if formula result is a string
2950
     * This record contains the token array and the result of a
2951
     * formula cell.
2952
     *
2953
     * --    "OpenOffice.org's Documentation of the Microsoft
2954
     *         Excel File Format"
2955
     */
2956
    protected function readFormula(): void
44✔
2957
    {
2958
        $length = self::getUInt2d($this->data, $this->pos + 2);
44✔
2959
        $recordData = $this->readRecordData($this->data, $this->pos + 4, $length);
44✔
2960

2961
        // move stream pointer to next record
2962
        $this->pos += 4 + $length;
44✔
2963

2964
        // offset: 0; size: 2; row index
2965
        $row = self::getUInt2d($recordData, 0);
44✔
2966

2967
        // offset: 2; size: 2; col index
2968
        $column = self::getUInt2d($recordData, 2);
44✔
2969
        $columnString = Coordinate::stringFromColumnIndex($column + 1);
44✔
2970
        $cellCoordinate = $columnString . ($row + 1);
44✔
2971

2972
        // offset: 20: size: variable; formula structure
2973
        $formulaStructure = substr($recordData, 20);
44✔
2974

2975
        // offset: 14: size: 2; option flags, recalculate always, recalculate on open etc.
2976
        $options = self::getUInt2d($recordData, 14);
44✔
2977

2978
        // bit: 0; mask: 0x0001; 1 = recalculate always
2979
        // bit: 1; mask: 0x0002; 1 = calculate on open
2980
        // bit: 2; mask: 0x0008; 1 = part of a shared formula
2981
        $isPartOfSharedFormula = (bool) (0x0008 & $options);
44✔
2982

2983
        // WARNING:
2984
        // We can apparently not rely on $isPartOfSharedFormula. Even when $isPartOfSharedFormula = true
2985
        // the formula data may be ordinary formula data, therefore we need to check
2986
        // explicitly for the tExp token (0x01)
2987
        $isPartOfSharedFormula = $isPartOfSharedFormula && ord($formulaStructure[2]) == 0x01;
44✔
2988

2989
        if ($isPartOfSharedFormula) {
44✔
2990
            // part of shared formula which means there will be a formula with a tExp token and nothing else
2991
            // get the base cell, grab tExp token
2992
            $baseRow = self::getUInt2d($formulaStructure, 3);
1✔
2993
            $baseCol = self::getUInt2d($formulaStructure, 5);
1✔
2994
            $this->baseCell = Coordinate::stringFromColumnIndex($baseCol + 1) . ($baseRow + 1);
1✔
2995
        }
2996

2997
        // Read cell?
2998
        if ($this->readFilter->readCell($columnString, $row + 1, $this->phpSheetTitle)) {
44✔
2999
            if ($isPartOfSharedFormula) {
44✔
3000
                // formula is added to this cell after the sheet has been read
3001
                $this->sharedFormulaParts[$cellCoordinate] = $this->baseCell;
1✔
3002
            }
3003

3004
            // offset: 16: size: 4; not used
3005

3006
            // offset: 4; size: 2; XF index
3007
            $xfIndex = self::getUInt2d($recordData, 4);
44✔
3008

3009
            // offset: 6; size: 8; result of the formula
3010
            $resultType = ord($recordData[6]);
44✔
3011
            $isSpecialResult = (ord($recordData[12]) == 255) && (ord($recordData[13]) == 255);
44✔
3012
            if (($resultType == 0) && $isSpecialResult) {
44✔
3013
                // String formula. Result follows in appended STRING record
3014
                $dataType = DataType::TYPE_STRING;
11✔
3015

3016
                // read possible SHAREDFMLA record
3017
                $code = self::getUInt2d($this->data, $this->pos);
11✔
3018
                if ($code == self::XLS_TYPE_SHAREDFMLA) {
11✔
3019
                    $this->readSharedFmla();
×
3020
                }
3021

3022
                // read STRING record
3023
                $value = $this->readString();
11✔
3024
            } elseif (($resultType == 1) && $isSpecialResult) {
38✔
3025
                // Boolean formula. Result is in +2; 0=false, 1=true
3026
                $dataType = DataType::TYPE_BOOL;
3✔
3027
                $value = (bool) ord($recordData[8]);
3✔
3028
            } elseif (($resultType == 2) && $isSpecialResult) {
36✔
3029
                // Error formula. Error code is in +2
3030
                $dataType = DataType::TYPE_ERROR;
11✔
3031
                $value = Xls\ErrorCode::lookup(ord($recordData[8]));
11✔
3032
            } elseif (($resultType == 3) && $isSpecialResult) {
36✔
3033
                // Formula result is a null string
3034
                $dataType = DataType::TYPE_NULL;
2✔
3035
                $value = '';
2✔
3036
            } else {
3037
                // forumla result is a number, first 14 bytes like _NUMBER record
3038
                $dataType = DataType::TYPE_NUMERIC;
36✔
3039
                $value = self::extractNumber(substr($recordData, 6, 8));
36✔
3040
            }
3041

3042
            $cell = $this->phpSheet->getCell($cellCoordinate);
44✔
3043
            if (!$this->readDataOnly && isset($this->mapCellXfIndex[$xfIndex])) {
44✔
3044
                // add cell style; skipping the collection update is safe here
3045
                // because every path below ends in setCalculatedValue(), which
3046
                // performs the update
3047
                $cell->setXfIndexNoUpdate($this->mapCellXfIndex[$xfIndex]);
42✔
3048
            }
3049

3050
            // store the formula
3051
            if (!$isPartOfSharedFormula) {
44✔
3052
                // not part of shared formula
3053
                // add cell value. If we can read formula, populate with formula, otherwise just used cached value
3054
                try {
3055
                    if ($this->version != self::XLS_BIFF8) {
44✔
3056
                        throw new Exception('Not BIFF8. Can only read BIFF8 formulas');
1✔
3057
                    }
3058
                    $formula = $this->getFormulaFromStructure($formulaStructure); // get formula in human language
43✔
3059
                    $cell->setValueExplicit('=' . $formula, DataType::TYPE_FORMULA);
43✔
3060
                } catch (PhpSpreadsheetException) {
2✔
3061
                    $cell->setValueExplicit($value, $dataType);
2✔
3062
                }
3063
            } else {
3064
                if ($this->version == self::XLS_BIFF8) {
1✔
3065
                    // do nothing at this point, formula id added later in the code
3066
                } else {
3067
                    $cell->setValueExplicit($value, $dataType);
×
3068
                }
3069
            }
3070

3071
            // store the cached calculated value
3072
            $cell->setCalculatedValue($value, $dataType === DataType::TYPE_NUMERIC);
44✔
3073
        }
3074
    }
3075

3076
    /**
3077
     * Read a SHAREDFMLA record. This function just stores the binary shared formula in the reader,
3078
     * which usually contains relative references.
3079
     * These will be used to construct the formula in each shared formula part after the sheet is read.
3080
     */
3081
    protected function readSharedFmla(): void
1✔
3082
    {
3083
        $length = self::getUInt2d($this->data, $this->pos + 2);
1✔
3084
        $recordData = $this->readRecordData($this->data, $this->pos + 4, $length);
1✔
3085

3086
        // move stream pointer to next record
3087
        $this->pos += 4 + $length;
1✔
3088

3089
        // offset: 0, size: 6; cell range address of the area used by the shared formula, not used for anything
3090
        //$cellRange = substr($recordData, 0, 6);
3091
        //$cellRange = Xls\Biff5::readBIFF5CellRangeAddressFixed($cellRange); // note: even BIFF8 uses BIFF5 syntax
3092

3093
        // offset: 6, size: 1; not used
3094

3095
        // offset: 7, size: 1; number of existing FORMULA records for this shared formula
3096
        //$no = ord($recordData[7]);
3097

3098
        // offset: 8, size: var; Binary token array of the shared formula
3099
        $formula = substr($recordData, 8);
1✔
3100

3101
        // at this point we only store the shared formula for later use
3102
        $this->sharedFormulas[$this->baseCell] = $formula;
1✔
3103
    }
3104

3105
    /**
3106
     * Read a STRING record from current stream position and advance the stream pointer to next record
3107
     * This record is used for storing result from FORMULA record when it is a string, and
3108
     * it occurs directly after the FORMULA record.
3109
     *
3110
     * @return string The string contents as UTF-8
3111
     */
3112
    protected function readString(): string
11✔
3113
    {
3114
        $length = self::getUInt2d($this->data, $this->pos + 2);
11✔
3115
        $recordData = $this->readRecordData($this->data, $this->pos + 4, $length);
11✔
3116

3117
        // move stream pointer to next record
3118
        $this->pos += 4 + $length;
11✔
3119

3120
        if ($this->version == self::XLS_BIFF8) {
11✔
3121
            $string = self::readUnicodeStringLong($recordData);
11✔
3122
            $value = $string['value'];
11✔
3123
        } else {
3124
            $string = $this->readByteStringLong($recordData);
×
3125
            $value = $string['value'];
×
3126
        }
3127
        /** @var string $value */
3128

3129
        return $value;
11✔
3130
    }
3131

3132
    /**
3133
     * Read BOOLERR record
3134
     * This record represents a Boolean value or error value
3135
     * cell.
3136
     *
3137
     * --    "OpenOffice.org's Documentation of the Microsoft
3138
     *         Excel File Format"
3139
     */
3140
    protected function readBoolErr(): void
11✔
3141
    {
3142
        $length = self::getUInt2d($this->data, $this->pos + 2);
11✔
3143
        $recordData = $this->readRecordData($this->data, $this->pos + 4, $length);
11✔
3144

3145
        // move stream pointer to next record
3146
        $this->pos += 4 + $length;
11✔
3147

3148
        // offset: 0; size: 2; row index
3149
        $row = self::getUInt2d($recordData, 0);
11✔
3150

3151
        // offset: 2; size: 2; column index
3152
        $column = self::getUInt2d($recordData, 2);
11✔
3153
        $columnString = Coordinate::stringFromColumnIndex($column + 1);
11✔
3154
        $cellCoordinate = $columnString . ($row + 1);
11✔
3155

3156
        // Read cell?
3157
        if ($this->readFilter->readCell($columnString, $row + 1, $this->phpSheetTitle)) {
11✔
3158
            // offset: 4; size: 2; index to XF record
3159
            $xfIndex = self::getUInt2d($recordData, 4);
11✔
3160

3161
            // offset: 6; size: 1; the boolean value or error value
3162
            $boolErr = ord($recordData[6]);
11✔
3163

3164
            // offset: 7; size: 1; 0=boolean; 1=error
3165
            $isError = ord($recordData[7]);
11✔
3166

3167
            $cell = $this->phpSheet->getCell($cellCoordinate);
11✔
3168
            if (!$this->readDataOnly && isset($this->mapCellXfIndex[$xfIndex])) {
11✔
3169
                // add cell style; a value write does not follow on every path,
3170
                // so use the updating setter to guarantee persistence
3171
                $cell->setXfIndex($this->mapCellXfIndex[$xfIndex]);
9✔
3172
            }
3173
            switch ($isError) {
3174
                case 0: // boolean
11✔
3175
                    $value = (bool) $boolErr;
11✔
3176

3177
                    // add cell value
3178
                    $cell->setValueExplicit($value, DataType::TYPE_BOOL);
11✔
3179

3180
                    break;
11✔
3181
                case 1: // error type
×
3182
                    $value = Xls\ErrorCode::lookup($boolErr);
×
3183

3184
                    // add cell value
3185
                    $cell->setValueExplicit($value, DataType::TYPE_ERROR);
×
3186

3187
                    break;
×
3188
            }
3189
        }
3190
    }
3191

3192
    /**
3193
     * Read MULBLANK record
3194
     * This record represents a cell range of empty cells. All
3195
     * cells are located in the same row.
3196
     *
3197
     * --    "OpenOffice.org's Documentation of the Microsoft
3198
     *         Excel File Format"
3199
     */
3200
    protected function readMulBlank(): void
26✔
3201
    {
3202
        $length = self::getUInt2d($this->data, $this->pos + 2);
26✔
3203
        $recordData = $this->readRecordData($this->data, $this->pos + 4, $length);
26✔
3204

3205
        // move stream pointer to next record
3206
        $this->pos += 4 + $length;
26✔
3207

3208
        // offset: 0; size: 2; index to row
3209
        $row = self::getUInt2d($recordData, 0);
26✔
3210

3211
        // offset: 2; size: 2; index to first column
3212
        $fc = self::getUInt2d($recordData, 2);
26✔
3213

3214
        // offset: 4; size: 2 x nc; list of indexes to XF records
3215
        // add style information
3216
        if (!$this->readDataOnly && $this->readEmptyCells) {
26✔
3217
            $rowIndex = $row + 1;
24✔
3218
            for ($i = 0; $i < $length / 2 - 3; ++$i) {
24✔
3219
                $columnString = Coordinate::stringFromColumnIndex($fc + $i + 1);
24✔
3220

3221
                // Read cell?
3222
                if ($this->readFilter->readCell($columnString, $rowIndex, $this->phpSheetTitle)) {
24✔
3223
                    $xfIndex = self::getUInt2d($recordData, 4 + 2 * $i);
24✔
3224
                    if (isset($this->mapCellXfIndex[$xfIndex])) {
24✔
3225
                        // blank cells never receive a value write, so use the
3226
                        // updating setter to guarantee persistence
3227
                        $this->phpSheet->getCell($columnString . $rowIndex)->setXfIndex($this->mapCellXfIndex[$xfIndex]);
24✔
3228
                    }
3229
                }
3230
            }
3231
        }
3232

3233
        // offset: 6; size 2; index to last column (not needed)
3234
    }
3235

3236
    /**
3237
     * Read LABEL record
3238
     * This record represents a cell that contains a string. In
3239
     * BIFF8 it is usually replaced by the LABELSST record.
3240
     * Excel still uses this record, if it copies unformatted
3241
     * text cells to the clipboard.
3242
     *
3243
     * --    "OpenOffice.org's Documentation of the Microsoft
3244
     *         Excel File Format"
3245
     */
3246
    protected function readLabel(): void
4✔
3247
    {
3248
        $length = self::getUInt2d($this->data, $this->pos + 2);
4✔
3249
        $recordData = $this->readRecordData($this->data, $this->pos + 4, $length);
4✔
3250

3251
        // move stream pointer to next record
3252
        $this->pos += 4 + $length;
4✔
3253

3254
        // offset: 0; size: 2; index to row
3255
        $row = self::getUInt2d($recordData, 0);
4✔
3256

3257
        // offset: 2; size: 2; index to column
3258
        $column = self::getUInt2d($recordData, 2);
4✔
3259
        $columnString = Coordinate::stringFromColumnIndex($column + 1);
4✔
3260
        $cellCoordinate = $columnString . ($row + 1);
4✔
3261

3262
        // Read cell?
3263
        if ($this->readFilter->readCell($columnString, $row + 1, $this->phpSheetTitle)) {
4✔
3264
            // offset: 4; size: 2; XF index
3265
            $xfIndex = self::getUInt2d($recordData, 4);
4✔
3266

3267
            // add cell value
3268
            // todo: what if string is very long? continue record
3269
            if ($this->version == self::XLS_BIFF8) {
4✔
3270
                $string = self::readUnicodeStringLong(substr($recordData, 6));
2✔
3271
                $value = $string['value'];
2✔
3272
            } else {
3273
                $string = $this->readByteStringLong(substr($recordData, 6));
2✔
3274
                $value = $string['value'];
2✔
3275
            }
3276
            /** @var string $value */
3277
            if ($this->readEmptyCells || trim($value) !== '') {
4✔
3278
                $cell = $this->phpSheet->getCell($cellCoordinate);
4✔
3279
                if (!$this->readDataOnly && isset($this->mapCellXfIndex[$xfIndex])) {
4✔
3280
                    // add cell style
3281
                    $cell->setXfIndexNoUpdate($this->mapCellXfIndex[$xfIndex]);
4✔
3282
                }
3283
                $cell->setValueExplicit($value, DataType::TYPE_STRING);
4✔
3284
            }
3285
        }
3286
    }
3287

3288
    /**
3289
     * Read BLANK record.
3290
     */
3291
    protected function readBlank(): void
27✔
3292
    {
3293
        $length = self::getUInt2d($this->data, $this->pos + 2);
27✔
3294
        $recordData = $this->readRecordData($this->data, $this->pos + 4, $length);
27✔
3295

3296
        // move stream pointer to next record
3297
        $this->pos += 4 + $length;
27✔
3298

3299
        // offset: 0; size: 2; row index
3300
        $row = self::getUInt2d($recordData, 0);
27✔
3301

3302
        // offset: 2; size: 2; col index
3303
        $col = self::getUInt2d($recordData, 2);
27✔
3304
        $columnString = Coordinate::stringFromColumnIndex($col + 1);
27✔
3305

3306
        $rowIndex = $row + 1;
27✔
3307

3308
        // Read cell?
3309
        if ($this->readFilter->readCell($columnString, $rowIndex, $this->phpSheetTitle)) {
27✔
3310
            // offset: 4; size: 2; XF index
3311
            $xfIndex = self::getUInt2d($recordData, 4);
27✔
3312

3313
            // add style information; blank cells never receive a value write,
3314
            // so use the updating setter to guarantee persistence
3315
            if (!$this->readDataOnly && $this->readEmptyCells && isset($this->mapCellXfIndex[$xfIndex])) {
27✔
3316
                $this->phpSheet->getCell($columnString . $rowIndex)->setXfIndex($this->mapCellXfIndex[$xfIndex]);
27✔
3317
            }
3318
        }
3319
    }
3320

3321
    /**
3322
     * Read MSODRAWING record.
3323
     */
3324
    protected function readMsoDrawing(): void
19✔
3325
    {
3326
        //$length = self::getUInt2d($this->data, $this->pos + 2);
3327

3328
        // get spliced record data
3329
        $splicedRecordData = $this->getSplicedRecordData();
19✔
3330
        $recordData = $splicedRecordData['recordData'];
19✔
3331

3332
        $this->drawingData .= StringHelper::convertToString($recordData);
19✔
3333
    }
3334

3335
    /**
3336
     * Read OBJ record.
3337
     */
3338
    protected function readObj(): void
15✔
3339
    {
3340
        $length = self::getUInt2d($this->data, $this->pos + 2);
15✔
3341
        $recordData = $this->readRecordData($this->data, $this->pos + 4, $length);
15✔
3342

3343
        // move stream pointer to next record
3344
        $this->pos += 4 + $length;
15✔
3345

3346
        if ($this->readDataOnly || $this->version != self::XLS_BIFF8) {
15✔
3347
            return;
1✔
3348
        }
3349

3350
        // recordData consists of an array of subrecords looking like this:
3351
        //    ft: 2 bytes; ftCmo type (0x15)
3352
        //    cb: 2 bytes; size in bytes of ftCmo data
3353
        //    ot: 2 bytes; Object Type
3354
        //    id: 2 bytes; Object id number
3355
        //    grbit: 2 bytes; Option Flags
3356
        //    data: var; subrecord data
3357

3358
        // for now, we are just interested in the second subrecord containing the object type
3359
        $ftCmoType = self::getUInt2d($recordData, 0);
14✔
3360
        $cbCmoSize = self::getUInt2d($recordData, 2);
14✔
3361
        $otObjType = self::getUInt2d($recordData, 4);
14✔
3362
        $idObjID = self::getUInt2d($recordData, 6);
14✔
3363
        $grbitOpts = self::getUInt2d($recordData, 6);
14✔
3364

3365
        $this->objs[] = [
14✔
3366
            'ftCmoType' => $ftCmoType,
14✔
3367
            'cbCmoSize' => $cbCmoSize,
14✔
3368
            'otObjType' => $otObjType,
14✔
3369
            'idObjID' => $idObjID,
14✔
3370
            'grbitOpts' => $grbitOpts,
14✔
3371
        ];
14✔
3372
        $this->textObjRef = $idObjID;
14✔
3373
    }
3374

3375
    /**
3376
     * Read WINDOW2 record.
3377
     */
3378
    protected function readWindow2(): void
136✔
3379
    {
3380
        $length = self::getUInt2d($this->data, $this->pos + 2);
136✔
3381
        $recordData = $this->readRecordData($this->data, $this->pos + 4, $length);
136✔
3382

3383
        // move stream pointer to next record
3384
        $this->pos += 4 + $length;
136✔
3385

3386
        // offset: 0; size: 2; option flags
3387
        $options = self::getUInt2d($recordData, 0);
136✔
3388

3389
        // offset: 2; size: 2; index to first visible row
3390
        //$firstVisibleRow = self::getUInt2d($recordData, 2);
3391

3392
        // offset: 4; size: 2; index to first visible colum
3393
        //$firstVisibleColumn = self::getUInt2d($recordData, 4);
3394
        $zoomscaleInPageBreakPreview = 0;
136✔
3395
        $zoomscaleInNormalView = 0;
136✔
3396
        if ($this->version === self::XLS_BIFF8) {
136✔
3397
            // offset:  8; size: 2; not used
3398
            // offset: 10; size: 2; cached magnification factor in page break preview (in percent); 0 = Default (60%)
3399
            // offset: 12; size: 2; cached magnification factor in normal view (in percent); 0 = Default (100%)
3400
            // offset: 14; size: 4; not used
3401
            if (!isset($recordData[10])) {
134✔
3402
                $zoomscaleInPageBreakPreview = 0;
×
3403
            } else {
3404
                $zoomscaleInPageBreakPreview = self::getUInt2d($recordData, 10);
134✔
3405
            }
3406

3407
            if ($zoomscaleInPageBreakPreview === 0) {
134✔
3408
                $zoomscaleInPageBreakPreview = 60;
130✔
3409
            }
3410

3411
            if (!isset($recordData[12])) {
134✔
3412
                $zoomscaleInNormalView = 0;
×
3413
            } else {
3414
                $zoomscaleInNormalView = self::getUInt2d($recordData, 12);
134✔
3415
            }
3416

3417
            if ($zoomscaleInNormalView === 0) {
134✔
3418
                $zoomscaleInNormalView = 100;
54✔
3419
            }
3420
        }
3421

3422
        // bit: 1; mask: 0x0002; 0 = do not show gridlines, 1 = show gridlines
3423
        $showGridlines = (bool) ((0x0002 & $options) >> 1);
136✔
3424
        $this->phpSheet->setShowGridlines($showGridlines);
136✔
3425

3426
        // bit: 2; mask: 0x0004; 0 = do not show headers, 1 = show headers
3427
        $showRowColHeaders = (bool) ((0x0004 & $options) >> 2);
136✔
3428
        $this->phpSheet->setShowRowColHeaders($showRowColHeaders);
136✔
3429

3430
        // bit: 3; mask: 0x0008; 0 = panes are not frozen, 1 = panes are frozen
3431
        $this->frozen = (bool) ((0x0008 & $options) >> 3);
136✔
3432

3433
        // bit: 6; mask: 0x0040; 0 = columns from left to right, 1 = columns from right to left
3434
        $this->phpSheet->setRightToLeft((bool) ((0x0040 & $options) >> 6));
136✔
3435

3436
        // bit: 10; mask: 0x0400; 0 = sheet not active, 1 = sheet active
3437
        $isActive = (bool) ((0x0400 & $options) >> 10);
136✔
3438
        if ($isActive) {
136✔
3439
            $this->spreadsheet->setActiveSheetIndex($this->spreadsheet->getIndex($this->phpSheet));
132✔
3440
            $this->activeSheetSet = true;
132✔
3441
        }
3442

3443
        // bit: 11; mask: 0x0800; 0 = normal view, 1 = page break view
3444
        $isPageBreakPreview = (bool) ((0x0800 & $options) >> 11);
136✔
3445

3446
        //FIXME: set $firstVisibleRow and $firstVisibleColumn
3447

3448
        if ($this->phpSheet->getSheetView()->getView() !== SheetView::SHEETVIEW_PAGE_LAYOUT) {
136✔
3449
            //NOTE: this setting is inferior to page layout view(Excel2007-)
3450
            $view = $isPageBreakPreview ? SheetView::SHEETVIEW_PAGE_BREAK_PREVIEW : SheetView::SHEETVIEW_NORMAL;
136✔
3451
            $this->phpSheet->getSheetView()->setView($view);
136✔
3452
            if ($this->version === self::XLS_BIFF8) {
136✔
3453
                $zoomScale = $isPageBreakPreview ? $zoomscaleInPageBreakPreview : $zoomscaleInNormalView;
134✔
3454
                $this->phpSheet->getSheetView()->setZoomScale($zoomScale);
134✔
3455
                $this->phpSheet->getSheetView()->setZoomScaleNormal($zoomscaleInNormalView);
134✔
3456
            }
3457
        }
3458
    }
3459

3460
    /**
3461
     * Read PLV Record(Created by Excel2007 or upper).
3462
     */
3463
    protected function readPageLayoutView(): void
121✔
3464
    {
3465
        $length = self::getUInt2d($this->data, $this->pos + 2);
121✔
3466
        $recordData = $this->readRecordData($this->data, $this->pos + 4, $length);
121✔
3467

3468
        // move stream pointer to next record
3469
        $this->pos += 4 + $length;
121✔
3470

3471
        // offset: 0; size: 2; rt
3472
        //->ignore
3473
        //$rt = self::getUInt2d($recordData, 0);
3474
        // offset: 2; size: 2; grbitfr
3475
        //->ignore
3476
        //$grbitFrt = self::getUInt2d($recordData, 2);
3477
        // offset: 4; size: 8; reserved
3478
        //->ignore
3479

3480
        // offset: 12; size 2; zoom scale
3481
        $wScalePLV = self::getUInt2d($recordData, 12);
121✔
3482
        // offset: 14; size 2; grbit
3483
        $grbit = self::getUInt2d($recordData, 14);
121✔
3484

3485
        // decomprise grbit
3486
        $fPageLayoutView = $grbit & 0x01;
121✔
3487
        //$fRulerVisible = ($grbit >> 1) & 0x01; //no support
3488
        //$fWhitespaceHidden = ($grbit >> 3) & 0x01; //no support
3489

3490
        if ($fPageLayoutView === 1) {
121✔
3491
            $this->phpSheet->getSheetView()->setView(SheetView::SHEETVIEW_PAGE_LAYOUT);
×
3492
            $this->phpSheet->getSheetView()->setZoomScale($wScalePLV); //set by Excel2007 only if SHEETVIEW_PAGE_LAYOUT
×
3493
        }
3494
        //otherwise, we cannot know whether SHEETVIEW_PAGE_LAYOUT or SHEETVIEW_PAGE_BREAK_PREVIEW.
3495
    }
3496

3497
    /**
3498
     * Read SCL record.
3499
     */
3500
    protected function readScl(): void
6✔
3501
    {
3502
        $length = self::getUInt2d($this->data, $this->pos + 2);
6✔
3503
        $recordData = $this->readRecordData($this->data, $this->pos + 4, $length);
6✔
3504

3505
        // move stream pointer to next record
3506
        $this->pos += 4 + $length;
6✔
3507

3508
        // offset: 0; size: 2; numerator of the view magnification
3509
        $numerator = self::getUInt2d($recordData, 0);
6✔
3510

3511
        // offset: 2; size: 2; numerator of the view magnification
3512
        $denumerator = self::getUInt2d($recordData, 2);
6✔
3513

3514
        // set the zoom scale (in percent)
3515
        $this->phpSheet->getSheetView()->setZoomScale($numerator * 100 / $denumerator);
6✔
3516
    }
3517

3518
    /**
3519
     * Read PANE record.
3520
     */
3521
    protected function readPane(): void
8✔
3522
    {
3523
        $length = self::getUInt2d($this->data, $this->pos + 2);
8✔
3524
        $recordData = $this->readRecordData($this->data, $this->pos + 4, $length);
8✔
3525

3526
        // move stream pointer to next record
3527
        $this->pos += 4 + $length;
8✔
3528

3529
        if (!$this->readDataOnly) {
8✔
3530
            // offset: 0; size: 2; position of vertical split
3531
            $px = self::getUInt2d($recordData, 0);
8✔
3532

3533
            // offset: 2; size: 2; position of horizontal split
3534
            $py = self::getUInt2d($recordData, 2);
8✔
3535

3536
            // offset: 4; size: 2; top most visible row in the bottom pane
3537
            $rwTop = self::getUInt2d($recordData, 4);
8✔
3538

3539
            // offset: 6; size: 2; first visible left column in the right pane
3540
            $colLeft = self::getUInt2d($recordData, 6);
8✔
3541

3542
            if ($this->frozen) {
8✔
3543
                // frozen panes
3544
                $cell = Coordinate::stringFromColumnIndex($px + 1) . ($py + 1);
8✔
3545
                $topLeftCell = Coordinate::stringFromColumnIndex($colLeft + 1) . ($rwTop + 1);
8✔
3546
                $this->phpSheet->freezePane($cell, $topLeftCell);
8✔
3547
            }
3548
            // unfrozen panes; split windows; not supported by PhpSpreadsheet core
3549
        }
3550
    }
3551

3552
    private const REGEX_WHOLE_COLUMN = '/^([A-Z]+1\:[A-Z]+)'
3553
        . '(' . AddressRange::MAX_ROW_XLS_OLD . '|' . AddressRange::MAX_ROW_XLS . ')'
3554
        . '$/';
3555
    private const REGEX_WHOLE_COLUMN_REPLACE = '${1}' . AddressRange::MAX_ROW;
3556
    private const REGEX_WHOLE_ROW = '/^(A\d+\:)'
3557
        . AddressRange::MAX_COLUMN_XLS
3558
        . '(\d+)$/';
3559
    private const REGEX_WHOLE_ROW_REPLACE = '${1}'
3560
        . AddressRange::MAX_COLUMN
3561
        . '${2}';
3562

3563
    /**
3564
     * Read SELECTION record. There is one such record for each pane in the sheet.
3565
     */
3566
    protected function readSelection(): string
133✔
3567
    {
3568
        $length = self::getUInt2d($this->data, $this->pos + 2);
133✔
3569
        $recordData = $this->readRecordData($this->data, $this->pos + 4, $length);
133✔
3570
        $selectedCells = '';
133✔
3571

3572
        // move stream pointer to next record
3573
        $this->pos += 4 + $length;
133✔
3574

3575
        if (!$this->readDataOnly) {
133✔
3576
            // offset: 0; size: 1; pane identifier
3577
            //$paneId = ord($recordData[0]);
3578

3579
            // offset: 1; size: 2; index to row of the active cell
3580
            //$r = self::getUInt2d($recordData, 1);
3581

3582
            // offset: 3; size: 2; index to column of the active cell
3583
            //$c = self::getUInt2d($recordData, 3);
3584

3585
            // offset: 5; size: 2; index into the following cell range list to the
3586
            //  entry that contains the active cell
3587
            //$index = self::getUInt2d($recordData, 5);
3588

3589
            // offset: 7; size: var; cell range address list containing all selected cell ranges
3590
            $data = substr($recordData, 7);
131✔
3591
            $cellRangeAddressList = Xls\Biff5::readBIFF5CellRangeAddressList($data); // note: also BIFF8 uses BIFF5 syntax
131✔
3592

3593
            $selectedCells = $cellRangeAddressList['cellRangeAddresses'][0];
131✔
3594

3595
            // first row '1' + last row '16384' or '65536' indicates that full column is selected (apparently also in BIFF8!)
3596
            if (Preg::isMatch(self::REGEX_WHOLE_COLUMN, $selectedCells)) {
131✔
3597
                $selectedCells = Preg::replace(self::REGEX_WHOLE_COLUMN, self::REGEX_WHOLE_COLUMN_REPLACE, $selectedCells);
1✔
3598
            }
3599

3600
            // first column 'A' + last column 'IV' indicates that full row is selected
3601
            if (Preg::isMatch(self::REGEX_WHOLE_ROW, $selectedCells)) {
131✔
3602
                $selectedCells = Preg::replace(self::REGEX_WHOLE_ROW, self::REGEX_WHOLE_ROW_REPLACE, $selectedCells);
3✔
3603
            }
3604

3605
            $this->phpSheet->setSelectedCells($selectedCells);
131✔
3606
        }
3607

3608
        return $selectedCells;
133✔
3609
    }
3610

3611
    private function includeCellRangeFiltered(string $cellRangeAddress): bool
17✔
3612
    {
3613
        $includeCellRange = false;
17✔
3614
        $rangeBoundaries = Coordinate::getRangeBoundaries($cellRangeAddress);
17✔
3615
        StringHelper::stringIncrement($rangeBoundaries[1][0]);
17✔
3616
        for ($row = $rangeBoundaries[0][1]; $row <= $rangeBoundaries[1][1]; ++$row) {
17✔
3617
            for ($column = $rangeBoundaries[0][0]; $column != $rangeBoundaries[1][0]; StringHelper::stringIncrement($column)) {
17✔
3618
                if ($this->readFilter->readCell($column, $row, $this->phpSheetTitle)) {
17✔
3619
                    $includeCellRange = true;
17✔
3620

3621
                    break 2;
17✔
3622
                }
3623
            }
3624
        }
3625

3626
        return $includeCellRange;
17✔
3627
    }
3628

3629
    /**
3630
     * MERGEDCELLS.
3631
     *
3632
     * This record contains the addresses of merged cell ranges
3633
     * in the current sheet.
3634
     *
3635
     * --    "OpenOffice.org's Documentation of the Microsoft
3636
     *         Excel File Format"
3637
     */
3638
    protected function readMergedCells(): void
19✔
3639
    {
3640
        $length = self::getUInt2d($this->data, $this->pos + 2);
19✔
3641
        $recordData = $this->readRecordData($this->data, $this->pos + 4, $length);
19✔
3642

3643
        // move stream pointer to next record
3644
        $this->pos += 4 + $length;
19✔
3645

3646
        if ($this->version == self::XLS_BIFF8 && !$this->readDataOnly) {
19✔
3647
            $cellRangeAddressList = Xls\Biff8::readBIFF8CellRangeAddressList($recordData);
17✔
3648
            foreach ($cellRangeAddressList['cellRangeAddresses'] as $cellRangeAddress) {
17✔
3649
                /** @var string $cellRangeAddress */
3650
                if (
3651
                    (str_contains($cellRangeAddress, ':'))
17✔
3652
                    && ($this->includeCellRangeFiltered($cellRangeAddress))
17✔
3653
                ) {
3654
                    $this->phpSheet->mergeCells($cellRangeAddress, Worksheet::MERGE_CELL_CONTENT_HIDE);
17✔
3655
                }
3656
            }
3657
        }
3658
    }
3659

3660
    /**
3661
     * Read HYPERLINK record.
3662
     */
3663
    protected function readHyperLink(): void
7✔
3664
    {
3665
        $length = self::getUInt2d($this->data, $this->pos + 2);
7✔
3666
        $recordData = $this->readRecordData($this->data, $this->pos + 4, $length);
7✔
3667

3668
        // move stream pointer forward to next record
3669
        $this->pos += 4 + $length;
7✔
3670

3671
        if (!$this->readDataOnly) {
7✔
3672
            // offset: 0; size: 8; cell range address of all cells containing this hyperlink
3673
            try {
3674
                $cellRange = Xls\Biff8::readBIFF8CellRangeAddressFixed($recordData);
7✔
3675
            } catch (PhpSpreadsheetException) {
×
3676
                return;
×
3677
            }
3678

3679
            // offset: 8, size: 16; GUID of StdLink
3680

3681
            // offset: 24, size: 4; unknown value
3682

3683
            // offset: 28, size: 4; option flags
3684
            // bit: 0; mask: 0x00000001; 0 = no link or extant, 1 = file link or URL
3685
            $isFileLinkOrUrl = (0x00000001 & self::getUInt2d($recordData, 28)) >> 0;
7✔
3686

3687
            // bit: 1; mask: 0x00000002; 0 = relative path, 1 = absolute path or URL
3688
            //$isAbsPathOrUrl = (0x00000001 & self::getUInt2d($recordData, 28)) >> 1;
3689

3690
            // bit: 2 (and 4); mask: 0x00000014; 0 = no description
3691
            $hasDesc = (0x00000014 & self::getUInt2d($recordData, 28)) >> 2;
7✔
3692

3693
            // bit: 3; mask: 0x00000008; 0 = no text, 1 = has text
3694
            $hasText = (0x00000008 & self::getUInt2d($recordData, 28)) >> 3;
7✔
3695

3696
            // bit: 7; mask: 0x00000080; 0 = no target frame, 1 = has target frame
3697
            $hasFrame = (0x00000080 & self::getUInt2d($recordData, 28)) >> 7;
7✔
3698

3699
            // bit: 8; mask: 0x00000100; 0 = file link or URL, 1 = UNC path (inc. server name)
3700
            $isUNC = (0x00000100 & self::getUInt2d($recordData, 28)) >> 8;
7✔
3701

3702
            // offset within record data
3703
            $offset = 32;
7✔
3704

3705
            if ($hasDesc) {
7✔
3706
                // offset: 32; size: var; character count of description text
3707
                $dl = self::getInt4d($recordData, 32);
3✔
3708
                // offset: 36; size: var; character array of description text, no Unicode string header, always 16-bit characters, zero terminated
3709
                //$desc = self::encodeUTF16(substr($recordData, 36, 2 * ($dl - 1)), false);
3710
                $offset += 4 + 2 * $dl;
3✔
3711
            }
3712
            if ($hasFrame) {
7✔
3713
                $fl = self::getInt4d($recordData, $offset);
×
3714
                $offset += 4 + 2 * $fl;
×
3715
            }
3716

3717
            // detect type of hyperlink (there are 4 types)
3718
            $hyperlinkType = null;
7✔
3719

3720
            if ($isUNC) {
7✔
3721
                $hyperlinkType = 'UNC';
×
3722
            } elseif (!$isFileLinkOrUrl) {
7✔
3723
                $hyperlinkType = 'workbook';
4✔
3724
            } elseif (ord($recordData[$offset]) == 0x03) {
7✔
3725
                $hyperlinkType = 'local';
×
3726
            } elseif (ord($recordData[$offset]) == 0xE0) {
7✔
3727
                $hyperlinkType = 'URL';
7✔
3728
            }
3729

3730
            switch ($hyperlinkType) {
3731
                case 'URL':
7✔
3732
                    // section 5.58.2: Hyperlink containing a URL
3733
                    // e.g. http://example.org/index.php
3734

3735
                    // offset: var; size: 16; GUID of URL Moniker
3736
                    $offset += 16;
7✔
3737
                    // offset: var; size: 4; size (in bytes) of character array of the URL including trailing zero word
3738
                    $us = self::getInt4d($recordData, $offset);
7✔
3739
                    $offset += 4;
7✔
3740
                    // offset: var; size: $us; character array of the URL, no Unicode string header, always 16-bit characters, zero-terminated
3741
                    $url = self::encodeUTF16(substr($recordData, $offset, $us - 2), false);
7✔
3742
                    $nullOffset = strpos($url, chr(0x00));
7✔
3743
                    if ($nullOffset) {
7✔
3744
                        $url = substr($url, 0, $nullOffset);
3✔
3745
                    }
3746
                    $url .= $hasText ? '#' : '';
7✔
3747
                    $offset += $us;
7✔
3748

3749
                    break;
7✔
3750
                case 'local':
4✔
3751
                    // section 5.58.3: Hyperlink to local file
3752
                    // examples:
3753
                    //   mydoc.txt
3754
                    //   ../../somedoc.xls#Sheet!A1
3755

3756
                    // offset: var; size: 16; GUI of File Moniker
3757
                    $offset += 16;
×
3758

3759
                    // offset: var; size: 2; directory up-level count.
3760
                    $upLevelCount = self::getUInt2d($recordData, $offset);
×
3761
                    $offset += 2;
×
3762

3763
                    // offset: var; size: 4; character count of the shortened file path and name, including trailing zero word
3764
                    $sl = self::getInt4d($recordData, $offset);
×
3765
                    $offset += 4;
×
3766

3767
                    // offset: var; size: sl; character array of the shortened file path and name in 8.3-DOS-format (compressed Unicode string)
3768
                    $shortenedFilePath = substr($recordData, $offset, $sl);
×
3769
                    $shortenedFilePath = self::encodeUTF16($shortenedFilePath, true);
×
3770
                    $shortenedFilePath = substr($shortenedFilePath, 0, -1); // remove trailing zero
×
3771

3772
                    $offset += $sl;
×
3773

3774
                    // offset: var; size: 24; unknown sequence
3775
                    $offset += 24;
×
3776

3777
                    // extended file path
3778
                    // offset: var; size: 4; size of the following file link field including string lenth mark
3779
                    $sz = self::getInt4d($recordData, $offset);
×
3780
                    $offset += 4;
×
3781

3782
                    $extendedFilePath = '';
×
3783
                    // only present if $sz > 0
3784
                    if ($sz > 0) {
×
3785
                        // offset: var; size: 4; size of the character array of the extended file path and name
3786
                        $xl = self::getInt4d($recordData, $offset);
×
3787
                        $offset += 4;
×
3788

3789
                        // offset: var; size 2; unknown
3790
                        $offset += 2;
×
3791

3792
                        // offset: var; size $xl; character array of the extended file path and name.
3793
                        $extendedFilePath = substr($recordData, $offset, $xl);
×
3794
                        $extendedFilePath = self::encodeUTF16($extendedFilePath, false);
×
3795
                        $offset += $xl;
×
3796
                    }
3797

3798
                    // construct the path
3799
                    $url = str_repeat('..\\', $upLevelCount);
×
3800
                    $url .= ($sz > 0) ? $extendedFilePath : $shortenedFilePath; // use extended path if available
×
3801
                    $url .= $hasText ? '#' : '';
×
3802

3803
                    break;
×
3804
                case 'UNC':
4✔
3805
                    // section 5.58.4: Hyperlink to a File with UNC (Universal Naming Convention) Path
3806
                    // todo: implement
3807
                    return;
×
3808
                case 'workbook':
4✔
3809
                    // section 5.58.5: Hyperlink to the Current Workbook
3810
                    // e.g. Sheet2!B1:C2, stored in text mark field
3811
                    $url = 'sheet://';
4✔
3812

3813
                    break;
4✔
3814
                default:
3815
                    return;
×
3816
            }
3817

3818
            if ($hasText) {
7✔
3819
                // offset: var; size: 4; character count of text mark including trailing zero word
3820
                $tl = self::getInt4d($recordData, $offset);
4✔
3821
                $offset += 4;
4✔
3822
                // offset: var; size: var; character array of the text mark without the # sign, no Unicode header, always 16-bit characters, zero-terminated
3823
                $text = self::encodeUTF16(substr($recordData, $offset, 2 * ($tl - 1)), false);
4✔
3824
                $url .= $text;
4✔
3825
            }
3826

3827
            // apply the hyperlink to all the relevant cells
3828
            foreach (Coordinate::extractAllCellReferencesInRange($cellRange) as $coordinate) {
7✔
3829
                $this->phpSheet->getCell($coordinate)->getHyperLink()->setUrl($url);
7✔
3830
            }
3831
        }
3832
    }
3833

3834
    /**
3835
     * Read DATAVALIDATIONS record.
3836
     */
3837
    protected function readDataValidations(): void
5✔
3838
    {
3839
        $length = self::getUInt2d($this->data, $this->pos + 2);
5✔
3840
        //$recordData = $this->readRecordData($this->data, $this->pos + 4, $length);
3841

3842
        // move stream pointer forward to next record
3843
        $this->pos += 4 + $length;
5✔
3844
    }
3845

3846
    /**
3847
     * Read DATAVALIDATION record.
3848
     */
3849
    protected function readDataValidation(): void
5✔
3850
    {
3851
        (new Xls\DataValidationHelper())->readDataValidation2($this);
5✔
3852
    }
3853

3854
    /**
3855
     * Read SHEETLAYOUT record. Stores sheet tab color information.
3856
     */
3857
    protected function readSheetLayout(): void
5✔
3858
    {
3859
        $length = self::getUInt2d($this->data, $this->pos + 2);
5✔
3860
        $recordData = $this->readRecordData($this->data, $this->pos + 4, $length);
5✔
3861

3862
        // move stream pointer to next record
3863
        $this->pos += 4 + $length;
5✔
3864

3865
        if (!$this->readDataOnly) {
5✔
3866
            // offset: 0; size: 2; repeated record identifier 0x0862
3867

3868
            // offset: 2; size: 10; not used
3869

3870
            // offset: 12; size: 4; size of record data
3871
            // Excel 2003 uses size of 0x14 (documented), Excel 2007 uses size of 0x28 (not documented?)
3872
            $sz = self::getInt4d($recordData, 12);
5✔
3873

3874
            switch ($sz) {
3875
                case 0x14:
5✔
3876
                    // offset: 16; size: 2; color index for sheet tab
3877
                    $colorIndex = self::getUInt2d($recordData, 16);
1✔
3878
                    /** @var string[] */
3879
                    $color = Xls\Color::map($colorIndex, $this->palette, $this->version);
1✔
3880
                    $this->phpSheet->getTabColor()->setRGB($color['rgb']);
1✔
3881

3882
                    break;
1✔
3883
                case 0x28:
4✔
3884
                    // TODO: Investigate structure for .xls SHEETLAYOUT record as saved by MS Office Excel 2007
3885
                    return;
4✔
3886
            }
3887
        }
3888
    }
3889

3890
    /**
3891
     * Read SHEETPROTECTION record (FEATHEADR).
3892
     */
3893
    protected function readSheetProtection(): void
125✔
3894
    {
3895
        $length = self::getUInt2d($this->data, $this->pos + 2);
125✔
3896
        $recordData = $this->readRecordData($this->data, $this->pos + 4, $length);
125✔
3897

3898
        // move stream pointer to next record
3899
        $this->pos += 4 + $length;
125✔
3900

3901
        if ($this->readDataOnly) {
125✔
3902
            return;
2✔
3903
        }
3904

3905
        // offset: 0; size: 2; repeated record header
3906

3907
        // offset: 2; size: 2; FRT cell reference flag (=0 currently)
3908

3909
        // offset: 4; size: 8; Currently not used and set to 0
3910

3911
        // offset: 12; size: 2; Shared feature type index (2=Enhanced Protetion, 4=SmartTag)
3912
        $isf = self::getUInt2d($recordData, 12);
123✔
3913
        if ($isf != 2) {
123✔
3914
            return;
×
3915
        }
3916

3917
        // offset: 14; size: 1; =1 since this is a feat header
3918

3919
        // offset: 15; size: 4; size of rgbHdrSData
3920

3921
        // rgbHdrSData, assume "Enhanced Protection"
3922
        // offset: 19; size: 2; option flags
3923
        $options = self::getUInt2d($recordData, 19);
123✔
3924

3925
        // bit: 0; mask 0x0001; 1 = user may edit objects, 0 = users must not edit objects
3926
        // Note - do not negate $bool
3927
        $bool = (0x0001 & $options) >> 0;
123✔
3928
        $this->phpSheet->getProtection()->setObjects((bool) $bool);
123✔
3929

3930
        // bit: 1; mask 0x0002; edit scenarios
3931
        // Note - do not negate $bool
3932
        $bool = (0x0002 & $options) >> 1;
123✔
3933
        $this->phpSheet->getProtection()->setScenarios((bool) $bool);
123✔
3934

3935
        // bit: 2; mask 0x0004; format cells
3936
        $bool = (0x0004 & $options) >> 2;
123✔
3937
        $this->phpSheet->getProtection()->setFormatCells(!$bool);
123✔
3938

3939
        // bit: 3; mask 0x0008; format columns
3940
        $bool = (0x0008 & $options) >> 3;
123✔
3941
        $this->phpSheet->getProtection()->setFormatColumns(!$bool);
123✔
3942

3943
        // bit: 4; mask 0x0010; format rows
3944
        $bool = (0x0010 & $options) >> 4;
123✔
3945
        $this->phpSheet->getProtection()->setFormatRows(!$bool);
123✔
3946

3947
        // bit: 5; mask 0x0020; insert columns
3948
        $bool = (0x0020 & $options) >> 5;
123✔
3949
        $this->phpSheet->getProtection()->setInsertColumns(!$bool);
123✔
3950

3951
        // bit: 6; mask 0x0040; insert rows
3952
        $bool = (0x0040 & $options) >> 6;
123✔
3953
        $this->phpSheet->getProtection()->setInsertRows(!$bool);
123✔
3954

3955
        // bit: 7; mask 0x0080; insert hyperlinks
3956
        $bool = (0x0080 & $options) >> 7;
123✔
3957
        $this->phpSheet->getProtection()->setInsertHyperlinks(!$bool);
123✔
3958

3959
        // bit: 8; mask 0x0100; delete columns
3960
        $bool = (0x0100 & $options) >> 8;
123✔
3961
        $this->phpSheet->getProtection()->setDeleteColumns(!$bool);
123✔
3962

3963
        // bit: 9; mask 0x0200; delete rows
3964
        $bool = (0x0200 & $options) >> 9;
123✔
3965
        $this->phpSheet->getProtection()->setDeleteRows(!$bool);
123✔
3966

3967
        // bit: 10; mask 0x0400; select locked cells
3968
        // Note that this is opposite of most of above.
3969
        $bool = (0x0400 & $options) >> 10;
123✔
3970
        $this->phpSheet->getProtection()->setSelectLockedCells((bool) $bool);
123✔
3971

3972
        // bit: 11; mask 0x0800; sort cell range
3973
        $bool = (0x0800 & $options) >> 11;
123✔
3974
        $this->phpSheet->getProtection()->setSort(!$bool);
123✔
3975

3976
        // bit: 12; mask 0x1000; auto filter
3977
        $bool = (0x1000 & $options) >> 12;
123✔
3978
        $this->phpSheet->getProtection()->setAutoFilter(!$bool);
123✔
3979

3980
        // bit: 13; mask 0x2000; pivot tables
3981
        $bool = (0x2000 & $options) >> 13;
123✔
3982
        $this->phpSheet->getProtection()->setPivotTables(!$bool);
123✔
3983

3984
        // bit: 14; mask 0x4000; select unlocked cells
3985
        // Note that this is opposite of most of above.
3986
        $bool = (0x4000 & $options) >> 14;
123✔
3987
        $this->phpSheet->getProtection()->setSelectUnlockedCells((bool) $bool);
123✔
3988

3989
        // offset: 21; size: 2; not used
3990
    }
3991

3992
    /**
3993
     * Read RANGEPROTECTION record
3994
     * Reading of this record is based on Microsoft Office Excel 97-2000 Binary File Format Specification,
3995
     * where it is referred to as FEAT record.
3996
     */
3997
    protected function readRangeProtection(): void
2✔
3998
    {
3999
        $length = self::getUInt2d($this->data, $this->pos + 2);
2✔
4000
        $recordData = $this->readRecordData($this->data, $this->pos + 4, $length);
2✔
4001

4002
        // move stream pointer to next record
4003
        $this->pos += 4 + $length;
2✔
4004

4005
        // local pointer in record data
4006
        $offset = 0;
2✔
4007

4008
        if (!$this->readDataOnly) {
2✔
4009
            $offset += 12;
2✔
4010

4011
            // offset: 12; size: 2; shared feature type, 2 = enhanced protection, 4 = smart tag
4012
            $isf = self::getUInt2d($recordData, 12);
2✔
4013
            if ($isf != 2) {
2✔
4014
                // we only read FEAT records of type 2
4015
                return;
×
4016
            }
4017
            $offset += 2;
2✔
4018

4019
            $offset += 5;
2✔
4020

4021
            // offset: 19; size: 2; count of ref ranges this feature is on
4022
            $cref = self::getUInt2d($recordData, 19);
2✔
4023
            $offset += 2;
2✔
4024

4025
            $offset += 6;
2✔
4026

4027
            // offset: 27; size: 8 * $cref; list of cell ranges (like in hyperlink record)
4028
            $cellRanges = [];
2✔
4029
            for ($i = 0; $i < $cref; ++$i) {
2✔
4030
                try {
4031
                    $cellRange = Xls\Biff8::readBIFF8CellRangeAddressFixed(substr($recordData, 27 + 8 * $i, 8));
2✔
4032
                } catch (PhpSpreadsheetException) {
×
4033
                    return;
×
4034
                }
4035
                $cellRanges[] = $cellRange;
2✔
4036
                $offset += 8;
2✔
4037
            }
4038

4039
            // offset: var; size: var; variable length of feature specific data
4040
            //$rgbFeat = substr($recordData, $offset);
4041
            $offset += 4;
2✔
4042

4043
            // offset: var; size: 4; the encrypted password (only 16-bit although field is 32-bit)
4044
            $wPassword = self::getInt4d($recordData, $offset);
2✔
4045
            $offset += 4;
2✔
4046

4047
            // Apply range protection to sheet
4048
            if ($cellRanges) {
2✔
4049
                $this->phpSheet->protectCells(implode(' ', $cellRanges), ($wPassword === 0) ? '' : strtoupper(dechex($wPassword)), true);
2✔
4050
            }
4051
        }
4052
    }
4053

4054
    /**
4055
     * Read a free CONTINUE record. Free CONTINUE record may be a camouflaged MSODRAWING record
4056
     * When MSODRAWING data on a sheet exceeds 8224 bytes, CONTINUE records are used instead. Undocumented.
4057
     * In this case, we must treat the CONTINUE record as a MSODRAWING record.
4058
     */
4059
    protected function readContinue(): void
1✔
4060
    {
4061
        $length = self::getUInt2d($this->data, $this->pos + 2);
1✔
4062
        $recordData = $this->readRecordData($this->data, $this->pos + 4, $length);
1✔
4063

4064
        // check if we are reading drawing data
4065
        // this is in case a free CONTINUE record occurs in other circumstances we are unaware of
4066
        if ($this->drawingData == '') {
1✔
4067
            // move stream pointer to next record
4068
            $this->pos += 4 + $length;
1✔
4069

4070
            return;
1✔
4071
        }
4072

4073
        // check if record data is at least 4 bytes long, otherwise there is no chance this is MSODRAWING data
4074
        if ($length < 4) {
×
4075
            // move stream pointer to next record
4076
            $this->pos += 4 + $length;
×
4077

4078
            return;
×
4079
        }
4080

4081
        // dirty check to see if CONTINUE record could be a camouflaged MSODRAWING record
4082
        // look inside CONTINUE record to see if it looks like a part of an Escher stream
4083
        // we know that Escher stream may be split at least at
4084
        //        0xF003 MsofbtSpgrContainer
4085
        //        0xF004 MsofbtSpContainer
4086
        //        0xF00D MsofbtClientTextbox
4087
        $validSplitPoints = [0xF003, 0xF004, 0xF00D]; // add identifiers if we find more
×
4088

4089
        $splitPoint = self::getUInt2d($recordData, 2);
×
4090
        if (in_array($splitPoint, $validSplitPoints)) {
×
4091
            // get spliced record data (and move pointer to next record)
4092
            $splicedRecordData = $this->getSplicedRecordData();
×
4093
            $this->drawingData .= StringHelper::convertToString($splicedRecordData['recordData']);
×
4094

4095
            return;
×
4096
        }
4097

4098
        // move stream pointer to next record
4099
        $this->pos += 4 + $length;
×
4100
    }
4101

4102
    /**
4103
     * Reads a record from current position in data stream and continues reading data as long as CONTINUE
4104
     * records are found. Splices the record data pieces and returns the combined string as if record data
4105
     * is in one piece.
4106
     * Moves to next current position in data stream to start of next record different from a CONtINUE record.
4107
     *
4108
     * @return mixed[]
4109
     */
4110
    private function getSplicedRecordData(): array
135✔
4111
    {
4112
        $data = '';
135✔
4113
        $spliceOffsets = [];
135✔
4114

4115
        $i = 0;
135✔
4116
        $spliceOffsets[0] = 0;
135✔
4117

4118
        do {
4119
            ++$i;
135✔
4120

4121
            // offset: 0; size: 2; identifier
4122
            //$identifier = self::getUInt2d($this->data, $this->pos);
4123
            // offset: 2; size: 2; length
4124
            $length = self::getUInt2d($this->data, $this->pos + 2);
135✔
4125
            $data .= $this->readRecordData($this->data, $this->pos + 4, $length);
135✔
4126

4127
            $spliceOffsets[$i] = $spliceOffsets[$i - 1] + $length;
135✔
4128

4129
            $this->pos += 4 + $length;
135✔
4130
            $nextIdentifier = self::getUInt2d($this->data, $this->pos);
135✔
4131
        } while ($nextIdentifier == self::XLS_TYPE_CONTINUE);
135✔
4132

4133
        return [
135✔
4134
            'recordData' => $data,
135✔
4135
            'spliceOffsets' => $spliceOffsets,
135✔
4136
        ];
135✔
4137
    }
4138

4139
    /**
4140
     * Convert formula structure into human readable Excel formula like 'A3+A5*5'.
4141
     *
4142
     * @param string $formulaStructure The complete binary data for the formula
4143
     * @param string $baseCell Base cell, only needed when formula contains tRefN tokens, e.g. with shared formulas
4144
     *
4145
     * @return string Human readable formula
4146
     */
4147
    protected function getFormulaFromStructure(string $formulaStructure, string $baseCell = 'A1'): string
74✔
4148
    {
4149
        // offset: 0; size: 2; size of the following formula data
4150
        $sz = self::getUInt2d($formulaStructure, 0);
74✔
4151

4152
        // offset: 2; size: sz
4153
        $formulaData = substr($formulaStructure, 2, $sz);
74✔
4154

4155
        // offset: 2 + sz; size: variable (optional)
4156
        if (strlen($formulaStructure) > 2 + $sz) {
74✔
4157
            $additionalData = substr($formulaStructure, 2 + $sz);
1✔
4158
        } else {
4159
            $additionalData = '';
73✔
4160
        }
4161

4162
        return $this->getFormulaFromData($formulaData, $additionalData, $baseCell);
74✔
4163
    }
4164

4165
    /**
4166
     * Take formula data and additional data for formula and return human readable formula.
4167
     *
4168
     * @param string $formulaData The binary data for the formula itself
4169
     * @param string $additionalData Additional binary data going with the formula
4170
     * @param string $baseCell Base cell, only needed when formula contains tRefN tokens, e.g. with shared formulas
4171
     *
4172
     * @return string Human readable formula
4173
     */
4174
    private function getFormulaFromData(string $formulaData, string $additionalData = '', string $baseCell = 'A1'): string
74✔
4175
    {
4176
        // start parsing the formula data
4177
        $tokens = [];
74✔
4178

4179
        while ($formulaData !== '' && $token = $this->getNextToken($formulaData, $baseCell)) {
74✔
4180
            $tokens[] = $token;
74✔
4181
            /** @var int[] $token */
4182
            $formulaData = substr($formulaData, $token['size']);
74✔
4183
        }
4184

4185
        $formulaString = $this->createFormulaFromTokens($tokens, $additionalData);
74✔
4186

4187
        return $formulaString;
74✔
4188
    }
4189

4190
    /**
4191
     * Take array of tokens together with additional data for formula and return human readable formula.
4192
     *
4193
     * @param mixed[][] $tokens
4194
     * @param string $additionalData Additional binary data going with the formula
4195
     *
4196
     * @return string Human readable formula
4197
     */
4198
    private function createFormulaFromTokens(array $tokens, string $additionalData): string
74✔
4199
    {
4200
        // empty formula?
4201
        if (empty($tokens)) {
74✔
4202
            return '';
5✔
4203
        }
4204

4205
        $formulaStrings = [];
74✔
4206
        foreach ($tokens as $token) {
74✔
4207
            // initialize spaces
4208
            $space0 = $space0 ?? ''; // spaces before next token, not tParen
74✔
4209
            $space1 = $space1 ?? ''; // carriage returns before next token, not tParen
74✔
4210
            $space2 = $space2 ?? ''; // spaces before opening parenthesis
74✔
4211
            $space3 = $space3 ?? ''; // carriage returns before opening parenthesis
74✔
4212
            $space4 = $space4 ?? ''; // spaces before closing parenthesis
74✔
4213
            $space5 = $space5 ?? ''; // carriage returns before closing parenthesis
74✔
4214
            /** @var string */
4215
            $tokenData = $token['data'] ?? '';
74✔
4216
            switch ($token['name']) {
74✔
4217
                case 'tAdd': // addition
74✔
4218
                case 'tConcat': // addition
74✔
4219
                case 'tDiv': // division
74✔
4220
                case 'tEQ': // equality
74✔
4221
                case 'tGE': // greater than or equal
74✔
4222
                case 'tGT': // greater than
74✔
4223
                case 'tIsect': // intersection
74✔
4224
                case 'tLE': // less than or equal
74✔
4225
                case 'tList': // less than or equal
74✔
4226
                case 'tLT': // less than
74✔
4227
                case 'tMul': // multiplication
74✔
4228
                case 'tNE': // multiplication
74✔
4229
                case 'tPower': // power
74✔
4230
                case 'tRange': // range
74✔
4231
                case 'tSub': // subtraction
74✔
4232
                    $op2 = array_pop($formulaStrings);
37✔
4233
                    $op1 = array_pop($formulaStrings);
37✔
4234
                    $formulaStrings[] = "$op1$space1$space0{$tokenData}$op2";
37✔
4235
                    unset($space0, $space1);
37✔
4236

4237
                    break;
37✔
4238
                case 'tUplus': // unary plus
74✔
4239
                case 'tUminus': // unary minus
74✔
4240
                    $op = array_pop($formulaStrings);
4✔
4241
                    $formulaStrings[] = "$space1$space0{$tokenData}$op";
4✔
4242
                    unset($space0, $space1);
4✔
4243

4244
                    break;
4✔
4245
                case 'tPercent': // percent sign
74✔
4246
                    $op = array_pop($formulaStrings);
1✔
4247
                    $formulaStrings[] = "$op$space1$space0{$tokenData}";
1✔
4248
                    unset($space0, $space1);
1✔
4249

4250
                    break;
1✔
4251
                case 'tAttrVolatile': // indicates volatile function
74✔
4252
                case 'tAttrIf':
74✔
4253
                case 'tAttrSkip':
74✔
4254
                case 'tAttrChoose':
74✔
4255
                    // token is only important for Excel formula evaluator
4256
                    // do nothing
4257
                    break;
3✔
4258
                case 'tAttrSpace': // space / carriage return
74✔
4259
                    // space will be used when next token arrives, do not alter formulaString stack
4260
                    /** @var string[][] $token */
4261
                    switch ($token['data']['spacetype']) {
×
4262
                        case 'type0':
×
4263
                            $space0 = str_repeat(' ', (int) $token['data']['spacecount']);
×
4264

4265
                            break;
×
4266
                        case 'type1':
×
4267
                            $space1 = str_repeat("\n", (int) $token['data']['spacecount']);
×
4268

4269
                            break;
×
4270
                        case 'type2':
×
4271
                            $space2 = str_repeat(' ', (int) $token['data']['spacecount']);
×
4272

4273
                            break;
×
4274
                        case 'type3':
×
4275
                            $space3 = str_repeat("\n", (int) $token['data']['spacecount']);
×
4276

4277
                            break;
×
4278
                        case 'type4':
×
4279
                            $space4 = str_repeat(' ', (int) $token['data']['spacecount']);
×
4280

4281
                            break;
×
4282
                        case 'type5':
×
4283
                            $space5 = str_repeat("\n", (int) $token['data']['spacecount']);
×
4284

4285
                            break;
×
4286
                    }
4287

4288
                    break;
×
4289
                case 'tAttrSum': // SUM function with one parameter
74✔
4290
                    $op = array_pop($formulaStrings);
15✔
4291
                    $formulaStrings[] = "{$space1}{$space0}SUM($op)";
15✔
4292
                    unset($space0, $space1);
15✔
4293

4294
                    break;
15✔
4295
                case 'tFunc': // function with fixed number of arguments
74✔
4296
                case 'tFuncV': // function with variable number of arguments
74✔
4297
                    /** @var string[] */
4298
                    $temp1 = $token['data'];
39✔
4299
                    $temp2 = $temp1['function'];
39✔
4300
                    if ($temp2 != '') {
39✔
4301
                        // normal function
4302
                        $ops = []; // array of operators
38✔
4303
                        $temp3 = (int) $temp1['args'];
38✔
4304
                        for ($i = 0; $i < $temp3; ++$i) {
38✔
4305
                            $ops[] = array_pop($formulaStrings);
29✔
4306
                        }
4307
                        $ops = array_reverse($ops);
38✔
4308
                        $formulaStrings[] = "$space1$space0{$temp2}(" . implode(',', $ops) . ')';
38✔
4309
                        unset($space0, $space1);
38✔
4310
                    } else {
4311
                        // add-in function
4312
                        $ops = []; // array of operators
1✔
4313
                        /** @var int[] */
4314
                        $temp = $token['data'];
1✔
4315
                        for ($i = 0; $i < $temp['args'] - 1; ++$i) {
1✔
4316
                            $ops[] = array_pop($formulaStrings);
1✔
4317
                        }
4318
                        $ops = array_reverse($ops);
1✔
4319
                        $function = array_pop($formulaStrings);
1✔
4320
                        $formulaStrings[] = "$space1$space0$function(" . implode(',', $ops) . ')';
1✔
4321
                        unset($space0, $space1);
1✔
4322
                    }
4323

4324
                    break;
39✔
4325
                case 'tParen': // parenthesis
74✔
4326
                    $expression = array_pop($formulaStrings);
1✔
4327
                    $formulaStrings[] = "$space3$space2($expression$space5$space4)";
1✔
4328
                    unset($space2, $space3, $space4, $space5);
1✔
4329

4330
                    break;
1✔
4331
                case 'tArray': // array constant
74✔
4332
                    $constantArray = Xls\Biff8::readBIFF8ConstantArray($additionalData);
1✔
4333
                    $formulaStrings[] = $space1 . $space0 . $constantArray['value'];
1✔
4334
                    $additionalData = substr($additionalData, $constantArray['size']); // bite of chunk of additional data
1✔
4335
                    unset($space0, $space1);
1✔
4336

4337
                    break;
1✔
4338
                case 'tMemArea':
74✔
4339
                    // bite off chunk of additional data
4340
                    $cellRangeAddressList = Xls\Biff8::readBIFF8CellRangeAddressList($additionalData);
×
4341
                    $additionalData = substr($additionalData, $cellRangeAddressList['size']);
×
4342
                    $formulaStrings[] = "$space1$space0{$tokenData}";
×
4343
                    unset($space0, $space1);
×
4344

4345
                    break;
×
4346
                case 'tArea': // cell range address
74✔
4347
                case 'tBool': // boolean
72✔
4348
                case 'tErr': // error code
71✔
4349
                case 'tInt': // integer
70✔
4350
                case 'tMemErr':
54✔
4351
                case 'tMemFunc':
54✔
4352
                case 'tMissArg':
54✔
4353
                case 'tName':
54✔
4354
                case 'tNameX':
54✔
4355
                case 'tNum': // number
53✔
4356
                case 'tRef': // single cell reference
51✔
4357
                case 'tRef3d': // 3d cell reference
41✔
4358
                case 'tArea3d': // 3d cell range reference
38✔
4359
                case 'tRefN':
27✔
4360
                case 'tAreaN':
26✔
4361
                case 'tStr': // string
25✔
4362
                    $formulaStrings[] = "$space1$space0{$tokenData}";
74✔
4363
                    unset($space0, $space1);
74✔
4364

4365
                    break;
74✔
4366
            }
4367
        }
4368
        $formulaString = $formulaStrings[0];
74✔
4369

4370
        return $formulaString;
74✔
4371
    }
4372

4373
    /**
4374
     * Fetch next token from binary formula data.
4375
     *
4376
     * @param string $formulaData Formula data
4377
     * @param string $baseCell Base cell, only needed when formula contains tRefN tokens, e.g. with shared formulas
4378
     *
4379
     * @return mixed[]
4380
     */
4381
    private function getNextToken(string $formulaData, string $baseCell = 'A1'): array
74✔
4382
    {
4383
        // offset: 0; size: 1; token id
4384
        $id = ord($formulaData[0]); // token id
74✔
4385
        $name = false; // initialize token name
74✔
4386

4387
        switch ($id) {
4388
            case 0x03:
74✔
4389
                $name = 'tAdd';
16✔
4390
                $size = 1;
16✔
4391
                $data = '+';
16✔
4392

4393
                break;
16✔
4394
            case 0x04:
74✔
4395
                $name = 'tSub';
14✔
4396
                $size = 1;
14✔
4397
                $data = '-';
14✔
4398

4399
                break;
14✔
4400
            case 0x05:
74✔
4401
                $name = 'tMul';
4✔
4402
                $size = 1;
4✔
4403
                $data = '*';
4✔
4404

4405
                break;
4✔
4406
            case 0x06:
74✔
4407
                $name = 'tDiv';
13✔
4408
                $size = 1;
13✔
4409
                $data = '/';
13✔
4410

4411
                break;
13✔
4412
            case 0x07:
74✔
4413
                $name = 'tPower';
1✔
4414
                $size = 1;
1✔
4415
                $data = '^';
1✔
4416

4417
                break;
1✔
4418
            case 0x08:
74✔
4419
                $name = 'tConcat';
4✔
4420
                $size = 1;
4✔
4421
                $data = '&';
4✔
4422

4423
                break;
4✔
4424
            case 0x09:
74✔
4425
                $name = 'tLT';
1✔
4426
                $size = 1;
1✔
4427
                $data = '<';
1✔
4428

4429
                break;
1✔
4430
            case 0x0A:
74✔
4431
                $name = 'tLE';
1✔
4432
                $size = 1;
1✔
4433
                $data = '<=';
1✔
4434

4435
                break;
1✔
4436
            case 0x0B:
74✔
4437
                $name = 'tEQ';
4✔
4438
                $size = 1;
4✔
4439
                $data = '=';
4✔
4440

4441
                break;
4✔
4442
            case 0x0C:
74✔
4443
                $name = 'tGE';
1✔
4444
                $size = 1;
1✔
4445
                $data = '>=';
1✔
4446

4447
                break;
1✔
4448
            case 0x0D:
74✔
4449
                $name = 'tGT';
2✔
4450
                $size = 1;
2✔
4451
                $data = '>';
2✔
4452

4453
                break;
2✔
4454
            case 0x0E:
74✔
4455
                $name = 'tNE';
2✔
4456
                $size = 1;
2✔
4457
                $data = '<>';
2✔
4458

4459
                break;
2✔
4460
            case 0x0F:
74✔
4461
                $name = 'tIsect';
×
4462
                $size = 1;
×
4463
                $data = ' ';
×
4464

4465
                break;
×
4466
            case 0x10:
74✔
4467
                $name = 'tList';
2✔
4468
                $size = 1;
2✔
4469
                $data = ',';
2✔
4470

4471
                break;
2✔
4472
            case 0x11:
74✔
4473
                $name = 'tRange';
×
4474
                $size = 1;
×
4475
                $data = ':';
×
4476

4477
                break;
×
4478
            case 0x12:
74✔
4479
                $name = 'tUplus';
1✔
4480
                $size = 1;
1✔
4481
                $data = '+';
1✔
4482

4483
                break;
1✔
4484
            case 0x13:
74✔
4485
                $name = 'tUminus';
4✔
4486
                $size = 1;
4✔
4487
                $data = '-';
4✔
4488

4489
                break;
4✔
4490
            case 0x14:
74✔
4491
                $name = 'tPercent';
1✔
4492
                $size = 1;
1✔
4493
                $data = '%';
1✔
4494

4495
                break;
1✔
4496
            case 0x15:    //    parenthesis
74✔
4497
                $name = 'tParen';
1✔
4498
                $size = 1;
1✔
4499
                $data = null;
1✔
4500

4501
                break;
1✔
4502
            case 0x16:    //    missing argument
74✔
4503
                $name = 'tMissArg';
×
4504
                $size = 1;
×
4505
                $data = '';
×
4506

4507
                break;
×
4508
            case 0x17:    //    string
74✔
4509
                $name = 'tStr';
25✔
4510
                // offset: 1; size: var; Unicode string, 8-bit string length
4511
                $string = self::readUnicodeStringShort(substr($formulaData, 1));
25✔
4512
                $size = 1 + $string['size'];
25✔
4513
                $data = self::UTF8toExcelDoubleQuoted($string['value']);
25✔
4514

4515
                break;
25✔
4516
            case 0x19:    //    Special attribute
73✔
4517
                // offset: 1; size: 1; attribute type flags:
4518
                switch (ord($formulaData[1])) {
17✔
4519
                    case 0x01:
17✔
4520
                        $name = 'tAttrVolatile';
3✔
4521
                        $size = 4;
3✔
4522
                        $data = null;
3✔
4523

4524
                        break;
3✔
4525
                    case 0x02:
15✔
4526
                        $name = 'tAttrIf';
1✔
4527
                        $size = 4;
1✔
4528
                        $data = null;
1✔
4529

4530
                        break;
1✔
4531
                    case 0x04:
15✔
4532
                        $name = 'tAttrChoose';
1✔
4533
                        // offset: 2; size: 2; number of choices in the CHOOSE function ($nc, number of parameters decreased by 1)
4534
                        $nc = self::getUInt2d($formulaData, 2);
1✔
4535
                        // offset: 4; size: 2 * $nc
4536
                        // offset: 4 + 2 * $nc; size: 2
4537
                        $size = 2 * $nc + 6;
1✔
4538
                        $data = null;
1✔
4539

4540
                        break;
1✔
4541
                    case 0x08:
15✔
4542
                        $name = 'tAttrSkip';
1✔
4543
                        $size = 4;
1✔
4544
                        $data = null;
1✔
4545

4546
                        break;
1✔
4547
                    case 0x10:
15✔
4548
                        $name = 'tAttrSum';
15✔
4549
                        $size = 4;
15✔
4550
                        $data = null;
15✔
4551

4552
                        break;
15✔
4553
                    case 0x40:
×
4554
                    case 0x41:
×
4555
                        $name = 'tAttrSpace';
×
4556
                        $size = 4;
×
4557
                        // offset: 2; size: 2; space type and position
4558
                        $spacetype = match (ord($formulaData[2])) {
×
4559
                            0x00 => 'type0',
×
4560
                            0x01 => 'type1',
×
4561
                            0x02 => 'type2',
×
4562
                            0x03 => 'type3',
×
4563
                            0x04 => 'type4',
×
4564
                            0x05 => 'type5',
×
4565
                            default => throw new Exception('Unrecognized space type in tAttrSpace token'),
×
4566
                        };
×
4567
                        // offset: 3; size: 1; number of inserted spaces/carriage returns
4568
                        $spacecount = ord($formulaData[3]);
×
4569

4570
                        $data = ['spacetype' => $spacetype, 'spacecount' => $spacecount];
×
4571

4572
                        break;
×
4573
                    default:
4574
                        throw new Exception('Unrecognized attribute flag in tAttr token');
×
4575
                }
4576

4577
                break;
17✔
4578
            case 0x1C:    //    error code
73✔
4579
                // offset: 1; size: 1; error code
4580
                $name = 'tErr';
5✔
4581
                $size = 2;
5✔
4582
                $data = Xls\ErrorCode::lookup(ord($formulaData[1]));
5✔
4583

4584
                break;
5✔
4585
            case 0x1D:    //    boolean
72✔
4586
                // offset: 1; size: 1; 0 = false, 1 = true;
4587
                $name = 'tBool';
3✔
4588
                $size = 2;
3✔
4589
                $data = ord($formulaData[1]) ? 'TRUE' : 'FALSE';
3✔
4590

4591
                break;
3✔
4592
            case 0x1E:    //    integer
72✔
4593
                // offset: 1; size: 2; unsigned 16-bit integer
4594
                $name = 'tInt';
46✔
4595
                $size = 3;
46✔
4596
                $data = self::getUInt2d($formulaData, 1);
46✔
4597

4598
                break;
46✔
4599
            case 0x1F:    //    number
65✔
4600
                // offset: 1; size: 8;
4601
                $name = 'tNum';
9✔
4602
                $size = 9;
9✔
4603
                $data = self::extractNumber(substr($formulaData, 1));
9✔
4604
                $data = str_replace(',', '.', (string) $data); // in case non-English locale
9✔
4605

4606
                break;
9✔
4607
            case 0x20:    //    array constant
64✔
4608
            case 0x40:
64✔
4609
            case 0x60:
64✔
4610
                // offset: 1; size: 7; not used
4611
                $name = 'tArray';
1✔
4612
                $size = 8;
1✔
4613
                $data = null;
1✔
4614

4615
                break;
1✔
4616
            case 0x21:    //    function with fixed number of arguments
64✔
4617
            case 0x41:
64✔
4618
            case 0x61:
60✔
4619
                $name = 'tFunc';
22✔
4620
                $size = 3;
22✔
4621
                // offset: 1; size: 2; index to built-in sheet function
4622
                $mapping = Xls\Mappings::TFUNC_MAPPINGS[self::getUInt2d($formulaData, 1)] ?? null;
22✔
4623
                if ($mapping === null) {
22✔
4624
                    throw new Exception('Unrecognized function in formula');
1✔
4625
                }
4626
                $data = ['function' => $mapping[0], 'args' => $mapping[1]];
22✔
4627

4628
                break;
22✔
4629
            case 0x22:    //    function with variable number of arguments
60✔
4630
            case 0x42:
60✔
4631
            case 0x62:
56✔
4632
                $name = 'tFuncV';
23✔
4633
                $size = 4;
23✔
4634
                // offset: 1; size: 1; number of arguments
4635
                $args = ord($formulaData[1]);
23✔
4636
                // offset: 2: size: 2; index to built-in sheet function
4637
                $index = self::getUInt2d($formulaData, 2);
23✔
4638
                $function = Xls\Mappings::TFUNCV_MAPPINGS[$index] ?? null;
23✔
4639
                if ($function === null) {
23✔
4640
                    throw new Exception('Unrecognized function in formula');
×
4641
                }
4642
                $data = ['function' => $function, 'args' => $args];
23✔
4643

4644
                break;
23✔
4645
            case 0x23:    //    index to defined name
56✔
4646
            case 0x43:
56✔
4647
            case 0x63:
56✔
4648
                $name = 'tName';
1✔
4649
                $size = 5;
1✔
4650
                // offset: 1; size: 2; one-based index to definedname record
4651
                $definedNameIndex = self::getUInt2d($formulaData, 1) - 1;
1✔
4652
                // offset: 2; size: 2; not used
4653
                /** @var string[] */
4654
                $data = $this->definedname[$definedNameIndex]['name'] ?? '';
1✔
4655

4656
                break;
1✔
4657
            case 0x24:    //    single cell reference e.g. A5
55✔
4658
            case 0x44:
55✔
4659
            case 0x64:
46✔
4660
                $name = 'tRef';
25✔
4661
                $size = 5;
25✔
4662
                $data = Xls\Biff8::readBIFF8CellAddress(substr($formulaData, 1, 4));
25✔
4663

4664
                break;
25✔
4665
            case 0x25:    //    cell range reference to cells in the same sheet (2d)
44✔
4666
            case 0x45:
22✔
4667
            case 0x65:
22✔
4668
                $name = 'tArea';
29✔
4669
                $size = 9;
29✔
4670
                $data = Xls\Biff8::readBIFF8CellRangeAddress(substr($formulaData, 1, 8));
29✔
4671

4672
                break;
29✔
4673
            case 0x26:    //    Constant reference sub-expression
21✔
4674
            case 0x46:
21✔
4675
            case 0x66:
21✔
4676
                $name = 'tMemArea';
×
4677
                // offset: 1; size: 4; not used
4678
                // offset: 5; size: 2; size of the following subexpression
4679
                $subSize = self::getUInt2d($formulaData, 5);
×
4680
                $size = 7 + $subSize;
×
4681
                $data = $this->getFormulaFromData(substr($formulaData, 7, $subSize));
×
4682

4683
                break;
×
4684
            case 0x27:    //    Deleted constant reference sub-expression
21✔
4685
            case 0x47:
21✔
4686
            case 0x67:
21✔
4687
                $name = 'tMemErr';
×
4688
                // offset: 1; size: 4; not used
4689
                // offset: 5; size: 2; size of the following subexpression
4690
                $subSize = self::getUInt2d($formulaData, 5);
×
4691
                $size = 7 + $subSize;
×
4692
                $data = $this->getFormulaFromData(substr($formulaData, 7, $subSize));
×
4693

4694
                break;
×
4695
            case 0x29:    //    Variable reference sub-expression
21✔
4696
            case 0x49:
21✔
4697
            case 0x69:
21✔
4698
                $name = 'tMemFunc';
1✔
4699
                // offset: 1; size: 2; size of the following sub-expression
4700
                $subSize = self::getUInt2d($formulaData, 1);
1✔
4701
                $size = 3 + $subSize;
1✔
4702
                $data = $this->getFormulaFromData(substr($formulaData, 3, $subSize));
1✔
4703

4704
                break;
1✔
4705
            case 0x2C: // Relative 2d cell reference reference, used in shared formulas and some other places
21✔
4706
            case 0x4C:
21✔
4707
            case 0x6C:
20✔
4708
                $name = 'tRefN';
4✔
4709
                $size = 5;
4✔
4710
                $data = Xls\Biff8::readBIFF8CellAddressB(substr($formulaData, 1, 4), $baseCell);
4✔
4711

4712
                break;
4✔
4713
            case 0x2D:    //    Relative 2d range reference
18✔
4714
            case 0x4D:
17✔
4715
            case 0x6D:
17✔
4716
                $name = 'tAreaN';
1✔
4717
                $size = 9;
1✔
4718
                $data = Xls\Biff8::readBIFF8CellRangeAddressB(substr($formulaData, 1, 8), $baseCell);
1✔
4719

4720
                break;
1✔
4721
            case 0x39:    //    External name
17✔
4722
            case 0x59:
16✔
4723
            case 0x79:
16✔
4724
                $name = 'tNameX';
1✔
4725
                $size = 7;
1✔
4726
                // offset: 1; size: 2; index to REF entry in EXTERNSHEET record
4727
                // offset: 3; size: 2; one-based index to DEFINEDNAME or EXTERNNAME record
4728
                $index = self::getUInt2d($formulaData, 3);
1✔
4729
                // assume index is to EXTERNNAME record
4730
                $data = $this->externalNames[$index - 1]['name'] ?? '';
1✔
4731

4732
                // offset: 5; size: 2; not used
4733
                break;
1✔
4734
            case 0x3A:    //    3d reference to cell
16✔
4735
            case 0x5A:
13✔
4736
            case 0x7A:
13✔
4737
                $name = 'tRef3d';
4✔
4738
                $size = 7;
4✔
4739

4740
                try {
4741
                    // offset: 1; size: 2; index to REF entry
4742
                    $sheetRange = $this->readSheetRangeByRefIndex(self::getUInt2d($formulaData, 1));
4✔
4743
                    // offset: 3; size: 4; cell address
4744
                    $cellAddress = Xls\Biff8::readBIFF8CellAddress(substr($formulaData, 3, 4));
4✔
4745

4746
                    $data = "$sheetRange!$cellAddress";
4✔
4747
                } catch (PhpSpreadsheetException) {
×
4748
                    // deleted sheet reference
4749
                    $data = '#REF!';
×
4750
                }
4751

4752
                break;
4✔
4753
            case 0x3B:    //    3d reference to cell range
12✔
4754
            case 0x5B:
1✔
4755
            case 0x7B:
1✔
4756
                $name = 'tArea3d';
11✔
4757
                $size = 11;
11✔
4758

4759
                try {
4760
                    // offset: 1; size: 2; index to REF entry
4761
                    $sheetRange = $this->readSheetRangeByRefIndex(self::getUInt2d($formulaData, 1));
11✔
4762
                    // offset: 3; size: 8; cell address
4763
                    $cellRangeAddress = Xls\Biff8::readBIFF8CellRangeAddress(substr($formulaData, 3, 8));
11✔
4764

4765
                    $data = "$sheetRange!$cellRangeAddress";
11✔
4766
                } catch (PhpSpreadsheetException) {
×
4767
                    // deleted sheet reference
4768
                    $data = '#REF!';
×
4769
                }
4770

4771
                break;
11✔
4772
                // Unknown cases    // don't know how to deal with
4773
            default:
4774
                throw new Exception('Unrecognized token ' . sprintf('%02X', $id) . ' in formula');
1✔
4775
        }
4776

4777
        return [
74✔
4778
            'id' => $id,
74✔
4779
            'name' => $name,
74✔
4780
            'size' => $size,
74✔
4781
            'data' => $data,
74✔
4782
        ];
74✔
4783
    }
4784

4785
    /**
4786
     * Get a sheet range like Sheet1:Sheet3 from REF index
4787
     * Note: If there is only one sheet in the range, one gets e.g Sheet1
4788
     * It can also happen that the REF structure uses the -1 (FFFF) code to indicate deleted sheets,
4789
     * in which case an Exception is thrown.
4790
     */
4791
    protected function readSheetRangeByRefIndex(int $index): string|false
15✔
4792
    {
4793
        if (isset($this->ref[$index])) {
15✔
4794
            $type = $this->externalBooks[$this->ref[$index]['externalBookIndex']]['type'];
15✔
4795

4796
            switch ($type) {
4797
                case 'internal':
15✔
4798
                    // check if we have a deleted 3d reference
4799
                    if ($this->ref[$index]['firstSheetIndex'] == 0xFFFF || $this->ref[$index]['lastSheetIndex'] == 0xFFFF) {
15✔
4800
                        throw new Exception('Deleted sheet reference');
×
4801
                    }
4802

4803
                    // we have normal sheet range (collapsed or uncollapsed)
4804
                    $firstSheetName = $this->sheets[$this->ref[$index]['firstSheetIndex']]['name'];
15✔
4805
                    $lastSheetName = $this->sheets[$this->ref[$index]['lastSheetIndex']]['name'];
15✔
4806

4807
                    if ($firstSheetName == $lastSheetName) {
15✔
4808
                        // collapsed sheet range
4809
                        $sheetRange = $firstSheetName;
15✔
4810
                    } else {
4811
                        $sheetRange = "$firstSheetName:$lastSheetName";
×
4812
                    }
4813

4814
                    // escape the single-quotes
4815
                    $sheetRange = str_replace("'", "''", $sheetRange);
15✔
4816

4817
                    // if there are special characters, we need to enclose the range in single-quotes
4818
                    // todo: check if we have identified the whole set of special characters
4819
                    // it seems that the following characters are not accepted for sheet names
4820
                    // and we may assume that they are not present: []*/:\?
4821
                    // 'u' qualifier makes it risky to use Preg::isMatch here
4822
                    if (preg_match("/[ !\"@#£$%&{()}<>=+'|^,;-]/u", $sheetRange)) {
15✔
4823
                        $sheetRange = "'$sheetRange'";
4✔
4824
                    }
4825

4826
                    return $sheetRange;
15✔
4827
                default:
4828
                    // TODO: external sheet support
4829
                    throw new Exception('Xls reader only supports internal sheets in formulas');
×
4830
            }
4831
        }
4832

4833
        return false;
×
4834
    }
4835

4836
    /**
4837
     * Read byte string (8-bit string length)
4838
     * OpenOffice documentation: 2.5.2.
4839
     *
4840
     * @return array{value: mixed, size: int}
4841
     */
4842
    protected function readByteStringShort(string $subData): array
7✔
4843
    {
4844
        // offset: 0; size: 1; length of the string (character count)
4845
        $ln = ord($subData[0]);
7✔
4846

4847
        // offset: 1: size: var; character array (8-bit characters)
4848
        $value = $this->decodeCodepage(substr($subData, 1, $ln));
7✔
4849

4850
        return [
7✔
4851
            'value' => $value,
7✔
4852
            'size' => 1 + $ln, // size in bytes of data structure
7✔
4853
        ];
7✔
4854
    }
4855

4856
    /**
4857
     * Read byte string (16-bit string length)
4858
     * OpenOffice documentation: 2.5.2.
4859
     *
4860
     * @return array{value: mixed, size: int}
4861
     */
4862
    protected function readByteStringLong(string $subData): array
2✔
4863
    {
4864
        // offset: 0; size: 2; length of the string (character count)
4865
        $ln = self::getUInt2d($subData, 0);
2✔
4866

4867
        // offset: 2: size: var; character array (8-bit characters)
4868
        $value = $this->decodeCodepage(substr($subData, 2));
2✔
4869

4870
        //return $string;
4871
        return [
2✔
4872
            'value' => $value,
2✔
4873
            'size' => 2 + $ln, // size in bytes of data structure
2✔
4874
        ];
2✔
4875
    }
4876

4877
    protected function parseRichText(string $is): RichText
3✔
4878
    {
4879
        $value = new RichText();
3✔
4880
        $value->createText($is);
3✔
4881

4882
        return $value;
3✔
4883
    }
4884

4885
    /**
4886
     * Phpstan 1.4.4 complains that this property is never read.
4887
     * So, we might be able to get rid of it altogether.
4888
     * For now, however, this function makes it readable,
4889
     * which satisfies Phpstan.
4890
     *
4891
     * @return mixed[]
4892
     *
4893
     * @codeCoverageIgnore
4894
     */
4895
    public function getMapCellStyleXfIndex(): array
4896
    {
4897
        return $this->mapCellStyleXfIndex;
4898
    }
4899

4900
    /**
4901
     * Parse conditional formatting blocks.
4902
     *
4903
     * @see https://www.openoffice.org/sc/excelfileformat.pdf Search for CFHEADER followed by CFRULE
4904
     *
4905
     * @return mixed[]
4906
     */
4907
    protected function readCFHeader(): array
24✔
4908
    {
4909
        return (new Xls\ConditionalFormatting())->readCFHeader2($this);
24✔
4910
    }
4911

4912
    /** @param string[] $cellRangeAddresses */
4913
    protected function readCFRule(array $cellRangeAddresses): void
24✔
4914
    {
4915
        (new Xls\ConditionalFormatting())->readCFRule2($cellRangeAddresses, $this);
24✔
4916
    }
4917

4918
    public function getVersion(): int
5✔
4919
    {
4920
        return $this->version;
5✔
4921
    }
4922
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE TRIAL · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc