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