PluginProbe
wpDataTables – WordPress Data Table, Dynamic Tables & Table Charts Plugin / 6.5.1.7
wpDataTables – WordPress Data Table, Dynamic Tables & Table Charts Plugin v6.5.1.7
6.5.1.7 6.5.1.6 6.5.1.5 6.5.1.4 6.5.1.3 6.5.1.2 6.5.1.1 6.5.0.9 6.5.0.8 6.5.0.7 6.5.0.6 trunk 3.4.2.40 3.4.2.41 3.4.2.42 3.4.2.43 3.4.2.44 3.4.2.45 3.4.2.46 3.4.2.47 3.4.2.48 3.4.2.49 3.4.2.50 6.3.2 6.3.3.1 All 47 releases
wpdatatables / lib / phpoffice / phpspreadsheet / src / PhpSpreadsheet / Reader / Xls.php

Xls.php in wpDataTables – WordPress Data Table, Dynamic Tables & Table Charts Plugin 6.5.1.7, at lib/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Reader/Xls.php

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