1
|
|
|
<?php |
2
|
|
|
|
3
|
|
|
namespace PhpOffice\PhpSpreadsheet\Reader; |
4
|
|
|
|
5
|
|
|
use PhpOffice\PhpSpreadsheet\Cell\Coordinate; |
6
|
|
|
use PhpOffice\PhpSpreadsheet\Cell\DataType; |
7
|
|
|
use PhpOffice\PhpSpreadsheet\Cell\DataValidation; |
8
|
|
|
use PhpOffice\PhpSpreadsheet\Exception as PhpSpreadsheetException; |
9
|
|
|
use PhpOffice\PhpSpreadsheet\NamedRange; |
10
|
|
|
use PhpOffice\PhpSpreadsheet\Reader\Xls\ConditionalFormatting; |
11
|
|
|
use PhpOffice\PhpSpreadsheet\Reader\Xls\Style\CellFont; |
12
|
|
|
use PhpOffice\PhpSpreadsheet\Reader\Xls\Style\FillPattern; |
13
|
|
|
use PhpOffice\PhpSpreadsheet\RichText\RichText; |
14
|
|
|
use PhpOffice\PhpSpreadsheet\Shared\CodePage; |
15
|
|
|
use PhpOffice\PhpSpreadsheet\Shared\Date; |
|
|
|
|
16
|
|
|
use PhpOffice\PhpSpreadsheet\Shared\Escher; |
17
|
|
|
use PhpOffice\PhpSpreadsheet\Shared\Escher\DgContainer\SpgrContainer\SpContainer; |
18
|
|
|
use PhpOffice\PhpSpreadsheet\Shared\Escher\DggContainer\BstoreContainer\BSE; |
19
|
|
|
use PhpOffice\PhpSpreadsheet\Shared\File; |
20
|
|
|
use PhpOffice\PhpSpreadsheet\Shared\OLE; |
21
|
|
|
use PhpOffice\PhpSpreadsheet\Shared\OLERead; |
22
|
|
|
use PhpOffice\PhpSpreadsheet\Shared\StringHelper; |
23
|
|
|
use PhpOffice\PhpSpreadsheet\Shared\Xls as SharedXls; |
24
|
|
|
use PhpOffice\PhpSpreadsheet\Spreadsheet; |
25
|
|
|
use PhpOffice\PhpSpreadsheet\Style\Alignment; |
26
|
|
|
use PhpOffice\PhpSpreadsheet\Style\Borders; |
27
|
|
|
use PhpOffice\PhpSpreadsheet\Style\Conditional; |
28
|
|
|
use PhpOffice\PhpSpreadsheet\Style\Fill; |
29
|
|
|
use PhpOffice\PhpSpreadsheet\Style\Font; |
30
|
|
|
use PhpOffice\PhpSpreadsheet\Style\NumberFormat; |
31
|
|
|
use PhpOffice\PhpSpreadsheet\Style\Protection; |
32
|
|
|
use PhpOffice\PhpSpreadsheet\Style\Style; |
33
|
|
|
use PhpOffice\PhpSpreadsheet\Worksheet\MemoryDrawing; |
34
|
|
|
use PhpOffice\PhpSpreadsheet\Worksheet\PageSetup; |
35
|
|
|
use PhpOffice\PhpSpreadsheet\Worksheet\SheetView; |
36
|
|
|
use PhpOffice\PhpSpreadsheet\Worksheet\Worksheet; |
37
|
|
|
|
38
|
|
|
// Original file header of ParseXL (used as the base for this class): |
39
|
|
|
// -------------------------------------------------------------------------------- |
40
|
|
|
// Adapted from Excel_Spreadsheet_Reader developed by users bizon153, |
41
|
|
|
// trex005, and mmp11 (SourceForge.net) |
42
|
|
|
// https://sourceforge.net/projects/phpexcelreader/ |
43
|
|
|
// Primary changes made by canyoncasa (dvc) for ParseXL 1.00 ... |
44
|
|
|
// Modelled moreso after Perl Excel Parse/Write modules |
45
|
|
|
// Added Parse_Excel_Spreadsheet object |
46
|
|
|
// Reads a whole worksheet or tab as row,column array or as |
47
|
|
|
// associated hash of indexed rows and named column fields |
48
|
|
|
// Added variables for worksheet (tab) indexes and names |
49
|
|
|
// Added an object call for loading individual woorksheets |
50
|
|
|
// Changed default indexing defaults to 0 based arrays |
51
|
|
|
// Fixed date/time and percent formats |
52
|
|
|
// Includes patches found at SourceForge... |
53
|
|
|
// unicode patch by nobody |
54
|
|
|
// unpack("d") machine depedency patch by matchy |
55
|
|
|
// boundsheet utf16 patch by bjaenichen |
56
|
|
|
// Renamed functions for shorter names |
57
|
|
|
// General code cleanup and rigor, including <80 column width |
58
|
|
|
// Included a testcase Excel file and PHP example calls |
59
|
|
|
// Code works for PHP 5.x |
60
|
|
|
|
61
|
|
|
// Primary changes made by canyoncasa (dvc) for ParseXL 1.10 ... |
62
|
|
|
// http://sourceforge.net/tracker/index.php?func=detail&aid=1466964&group_id=99160&atid=623334 |
63
|
|
|
// Decoding of formula conditions, results, and tokens. |
64
|
|
|
// Support for user-defined named cells added as an array "namedcells" |
65
|
|
|
// Patch code for user-defined named cells supports single cells only. |
66
|
|
|
// NOTE: this patch only works for BIFF8 as BIFF5-7 use a different |
67
|
|
|
// external sheet reference structure |
68
|
|
|
class Xls extends BaseReader |
69
|
|
|
{ |
70
|
|
|
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 |
4306
|
|
|
{ |
4307
|
5 |
|
$length = self::getUInt2d($this->data, $this->pos + 2); |
4308
|
5 |
|
$recordData = $this->readRecordData($this->data, $this->pos + 4, $length); |
4309
|
|
|
|
4310
|
|
|
// move stream pointer to next record |
4311
|
5 |
|
$this->pos += 4 + $length; |
4312
|
|
|
|
4313
|
|
|
// offset: 0; size: 2; numerator of the view magnification |
4314
|
5 |
|
$numerator = self::getUInt2d($recordData, 0); |
4315
|
|
|
|
4316
|
|
|
// offset: 2; size: 2; numerator of the view magnification |
4317
|
5 |
|
$denumerator = self::getUInt2d($recordData, 2); |
4318
|
|
|
|
4319
|
|
|
// set the zoom scale (in percent) |
4320
|
5 |
|
$this->phpSheet->getSheetView()->setZoomScale($numerator * 100 / $denumerator); |
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 |
5035
|
|
|
{ |
5036
|
92 |
|
$data = ''; |
5037
|
92 |
|
$spliceOffsets = []; |
5038
|
|
|
|
5039
|
92 |
|
$i = 0; |
5040
|
92 |
|
$spliceOffsets[0] = 0; |
5041
|
|
|
|
5042
|
|
|
do { |
5043
|
92 |
|
++$i; |
5044
|
|
|
|
5045
|
|
|
// offset: 0; size: 2; identifier |
5046
|
|
|
//$identifier = self::getUInt2d($this->data, $this->pos); |
5047
|
|
|
// offset: 2; size: 2; length |
5048
|
92 |
|
$length = self::getUInt2d($this->data, $this->pos + 2); |
5049
|
92 |
|
$data .= $this->readRecordData($this->data, $this->pos + 4, $length); |
5050
|
|
|
|
5051
|
92 |
|
$spliceOffsets[$i] = $spliceOffsets[$i - 1] + $length; |
5052
|
|
|
|
5053
|
92 |
|
$this->pos += 4 + $length; |
5054
|
92 |
|
$nextIdentifier = self::getUInt2d($this->data, $this->pos); |
5055
|
92 |
|
} while ($nextIdentifier == self::XLS_TYPE_CONTINUE); |
5056
|
|
|
|
5057
|
92 |
|
return [ |
5058
|
92 |
|
'recordData' => $data, |
5059
|
92 |
|
'spliceOffsets' => $spliceOffsets, |
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 |
|
|
|
|
5131
|
47 |
|
$space1 ??= ''; // carriage returns before next token, not tParen |
|
|
|
|
5132
|
47 |
|
$space2 ??= ''; // spaces before opening parenthesis |
|
|
|
|
5133
|
47 |
|
$space3 ??= ''; // carriage returns before opening parenthesis |
|
|
|
|
5134
|
47 |
|
$space4 ??= ''; // spaces before closing parenthesis |
|
|
|
|
5135
|
47 |
|
$space5 ??= ''; // carriage returns before closing parenthesis |
|
|
|
|
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 |
7093
|
|
|
{ |
7094
|
|
|
// offset: 0; size: 2; length of the string (character count) |
7095
|
2 |
|
$ln = self::getUInt2d($subData, 0); |
7096
|
|
|
|
7097
|
|
|
// offset: 2: size: var; character array (8-bit characters) |
7098
|
2 |
|
$value = $this->decodeCodepage(substr($subData, 2)); |
7099
|
|
|
|
7100
|
|
|
//return $string; |
7101
|
2 |
|
return [ |
7102
|
2 |
|
'value' => $value, |
7103
|
2 |
|
'size' => 2 + $ln, // size in bytes of data structure |
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 |
7131
|
|
|
{ |
7132
|
|
|
// offset: 0: size: 2; length of the string (character count) |
7133
|
93 |
|
$characterCount = self::getUInt2d($subData, 0); |
7134
|
|
|
|
7135
|
93 |
|
$string = self::readUnicodeString(substr($subData, 2), $characterCount); |
7136
|
|
|
|
7137
|
|
|
// add 2 for the string length |
7138
|
93 |
|
$string['size'] += 2; |
7139
|
|
|
|
7140
|
93 |
|
return $string; |
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 |
7329
|
|
|
{ |
7330
|
16 |
|
$length = self::getUInt2d($this->data, $this->pos + 2); |
7331
|
16 |
|
$recordData = $this->readRecordData($this->data, $this->pos + 4, $length); |
7332
|
|
|
|
7333
|
|
|
// move stream pointer forward to next record |
7334
|
16 |
|
$this->pos += 4 + $length; |
7335
|
|
|
|
7336
|
16 |
|
if ($this->readDataOnly) { |
7337
|
1 |
|
return []; |
7338
|
|
|
} |
7339
|
|
|
|
7340
|
|
|
// offset: 0; size: 2; Rule Count |
7341
|
|
|
// $ruleCount = self::getUInt2d($recordData, 0); |
7342
|
|
|
|
7343
|
|
|
// offset: var; size: var; cell range address list with |
7344
|
15 |
|
$cellRangeAddressList = ($this->version == self::XLS_BIFF8) |
7345
|
15 |
|
? $this->readBIFF8CellRangeAddressList(substr($recordData, 12)) |
7346
|
|
|
: $this->readBIFF5CellRangeAddressList(substr($recordData, 12)); |
7347
|
15 |
|
$cellRangeAddresses = $cellRangeAddressList['cellRangeAddresses']; |
7348
|
|
|
|
7349
|
15 |
|
return $cellRangeAddresses; |
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 |
7456
|
|
|
{ |
7457
|
15 |
|
$fontSize = self::getInt4d($options, 64); |
7458
|
15 |
|
if ($fontSize !== -1) { |
7459
|
8 |
|
$style->getFont()->setSize($fontSize / 20); // Convert twips to points |
7460
|
|
|
} |
7461
|
|
|
|
7462
|
15 |
|
$bold = self::getUInt2d($options, 72) === 700; // 400 = normal, 700 = bold |
7463
|
15 |
|
$style->getFont()->setBold($bold); |
7464
|
|
|
|
7465
|
15 |
|
$color = self::getInt4d($options, 80); |
7466
|
|
|
|
7467
|
15 |
|
if ($color !== -1) { |
7468
|
15 |
|
$style->getFont()->getColor()->setRGB(Xls\Color::map($color, $this->palette, $this->version)['rgb']); |
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 |
7508
|
|
|
{ |
7509
|
|
|
try { |
7510
|
15 |
|
$formula = substr($recordData, $offset, $size); |
7511
|
15 |
|
$formula = pack('v', $size) . $formula; // prepend the length |
7512
|
|
|
|
7513
|
15 |
|
$formula = $this->getFormulaFromStructure($formula); |
7514
|
15 |
|
if (is_numeric($formula)) { |
7515
|
13 |
|
return (str_contains($formula, '.')) ? (float) $formula : (int) $formula; |
7516
|
|
|
} |
7517
|
|
|
|
7518
|
8 |
|
return $formula; |
7519
|
|
|
} catch (PhpSpreadsheetException) { |
7520
|
|
|
return 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) { |
7527
|
15 |
|
$conditional = new Conditional(); |
7528
|
15 |
|
$conditional->setConditionType($type); |
7529
|
15 |
|
$conditional->setOperatorType($operator); |
7530
|
15 |
|
if ($formula1 !== null) { |
7531
|
15 |
|
$conditional->addCondition($formula1); |
7532
|
|
|
} |
7533
|
15 |
|
if ($formula2 !== null) { |
7534
|
6 |
|
$conditional->addCondition($formula2); |
7535
|
|
|
} |
7536
|
15 |
|
$conditional->setStyle($style); |
7537
|
|
|
|
7538
|
15 |
|
$conditionalStyles = $this->phpSheet->getStyle($cellRange)->getConditionalStyles(); |
7539
|
15 |
|
$conditionalStyles[] = $conditional; |
7540
|
|
|
|
7541
|
15 |
|
$this->phpSheet->getStyle($cellRange)->setConditionalStyles($conditionalStyles); |
7542
|
|
|
} |
7543
|
|
|
} |
7544
|
|
|
|
7545
|
5 |
|
public function getVersion(): int |
7546
|
|
|
{ |
7547
|
5 |
|
return $this->version; |
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