1
|
|
|
<?php |
2
|
|
|
|
3
|
|
|
namespace PhpOffice\PhpSpreadsheet\Writer\Xls; |
4
|
|
|
|
5
|
|
|
use GdImage; |
6
|
|
|
use PhpOffice\PhpSpreadsheet\Cell\Cell; |
7
|
|
|
use PhpOffice\PhpSpreadsheet\Cell\Coordinate; |
8
|
|
|
use PhpOffice\PhpSpreadsheet\Cell\DataType; |
9
|
|
|
use PhpOffice\PhpSpreadsheet\Exception as PhpSpreadsheetException; |
10
|
|
|
use PhpOffice\PhpSpreadsheet\RichText\RichText; |
11
|
|
|
use PhpOffice\PhpSpreadsheet\RichText\Run; |
12
|
|
|
use PhpOffice\PhpSpreadsheet\Shared\StringHelper; |
13
|
|
|
use PhpOffice\PhpSpreadsheet\Shared\Xls; |
14
|
|
|
use PhpOffice\PhpSpreadsheet\Style\Border; |
15
|
|
|
use PhpOffice\PhpSpreadsheet\Style\Conditional; |
16
|
|
|
use PhpOffice\PhpSpreadsheet\Style\Protection; |
17
|
|
|
use PhpOffice\PhpSpreadsheet\Worksheet\PageSetup; |
18
|
|
|
use PhpOffice\PhpSpreadsheet\Worksheet\SheetView; |
19
|
|
|
use PhpOffice\PhpSpreadsheet\Writer\Exception as WriterException; |
20
|
|
|
|
21
|
|
|
// Original file header of PEAR::Spreadsheet_Excel_Writer_Worksheet (used as the base for this class): |
22
|
|
|
// ----------------------------------------------------------------------------------------- |
23
|
|
|
// /* |
24
|
|
|
// * Module written/ported by Xavier Noguer <[email protected]> |
25
|
|
|
// * |
26
|
|
|
// * The majority of this is _NOT_ my code. I simply ported it from the |
27
|
|
|
// * PERL Spreadsheet::WriteExcel module. |
28
|
|
|
// * |
29
|
|
|
// * The author of the Spreadsheet::WriteExcel module is John McNamara |
30
|
|
|
// * <[email protected]> |
31
|
|
|
// * |
32
|
|
|
// * I _DO_ maintain this code, and John McNamara has nothing to do with the |
33
|
|
|
// * porting of this code to PHP. Any questions directly related to this |
34
|
|
|
// * class library should be directed to me. |
35
|
|
|
// * |
36
|
|
|
// * License Information: |
37
|
|
|
// * |
38
|
|
|
// * Spreadsheet_Excel_Writer: A library for generating Excel Spreadsheets |
39
|
|
|
// * Copyright (c) 2002-2003 Xavier Noguer [email protected] |
40
|
|
|
// * |
41
|
|
|
// * This library is free software; you can redistribute it and/or |
42
|
|
|
// * modify it under the terms of the GNU Lesser General Public |
43
|
|
|
// * License as published by the Free Software Foundation; either |
44
|
|
|
// * version 2.1 of the License, or (at your option) any later version. |
45
|
|
|
// * |
46
|
|
|
// * This library is distributed in the hope that it will be useful, |
47
|
|
|
// * but WITHOUT ANY WARRANTY; without even the implied warranty of |
48
|
|
|
// * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU |
49
|
|
|
// * Lesser General Public License for more details. |
50
|
|
|
// * |
51
|
|
|
// * You should have received a copy of the GNU Lesser General Public |
52
|
|
|
// * License along with this library; if not, write to the Free Software |
53
|
|
|
// * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA |
54
|
|
|
// */ |
55
|
|
|
class Worksheet extends BIFFwriter |
56
|
|
|
{ |
57
|
|
|
/** @var int */ |
58
|
|
|
private static $always0 = 0; |
59
|
|
|
|
60
|
|
|
/** @var int */ |
61
|
|
|
private static $always1 = 1; |
62
|
|
|
|
63
|
|
|
/** |
64
|
|
|
* Formula parser. |
65
|
|
|
*/ |
66
|
|
|
private Parser $parser; |
67
|
|
|
|
68
|
|
|
/** |
69
|
|
|
* Array containing format information for columns. |
70
|
|
|
* |
71
|
|
|
* @var array |
72
|
|
|
*/ |
73
|
|
|
private $columnInfo; |
74
|
|
|
|
75
|
|
|
/** |
76
|
|
|
* The active pane for the worksheet. |
77
|
|
|
*/ |
78
|
|
|
private int $activePane; |
79
|
|
|
|
80
|
|
|
/** |
81
|
|
|
* Whether to use outline. |
82
|
|
|
*/ |
83
|
|
|
private bool $outlineOn; |
84
|
|
|
|
85
|
|
|
/** |
86
|
|
|
* Auto outline styles. |
87
|
|
|
*/ |
88
|
|
|
private bool $outlineStyle; |
89
|
|
|
|
90
|
|
|
/** |
91
|
|
|
* Whether to have outline summary below. |
92
|
|
|
* Not currently used. |
93
|
|
|
*/ |
94
|
|
|
private bool $outlineBelow; //* @phpstan-ignore-line |
95
|
|
|
|
96
|
|
|
/** |
97
|
|
|
* Whether to have outline summary at the right. |
98
|
|
|
* Not currently used. |
99
|
|
|
*/ |
100
|
|
|
private bool $outlineRight; //* @phpstan-ignore-line |
101
|
|
|
|
102
|
|
|
/** |
103
|
|
|
* Reference to the total number of strings in the workbook. |
104
|
|
|
* |
105
|
|
|
* @var int |
106
|
|
|
*/ |
107
|
|
|
private $stringTotal; |
108
|
|
|
|
109
|
|
|
/** |
110
|
|
|
* Reference to the number of unique strings in the workbook. |
111
|
|
|
* |
112
|
|
|
* @var int |
113
|
|
|
*/ |
114
|
|
|
private $stringUnique; |
115
|
|
|
|
116
|
|
|
/** |
117
|
|
|
* Reference to the array containing all the unique strings in the workbook. |
118
|
|
|
* |
119
|
|
|
* @var array |
120
|
|
|
*/ |
121
|
|
|
private $stringTable; |
122
|
|
|
|
123
|
|
|
/** |
124
|
|
|
* Color cache. |
125
|
|
|
* |
126
|
|
|
* @var array |
127
|
|
|
*/ |
128
|
|
|
private $colors; |
129
|
|
|
|
130
|
|
|
/** |
131
|
|
|
* Index of first used row (at least 0). |
132
|
|
|
*/ |
133
|
|
|
private int $firstRowIndex; |
134
|
|
|
|
135
|
|
|
/** |
136
|
|
|
* Index of last used row. (no used rows means -1). |
137
|
|
|
*/ |
138
|
|
|
private int $lastRowIndex; |
139
|
|
|
|
140
|
|
|
/** |
141
|
|
|
* Index of first used column (at least 0). |
142
|
|
|
*/ |
143
|
|
|
private int $firstColumnIndex; |
144
|
|
|
|
145
|
|
|
/** |
146
|
|
|
* Index of last used column (no used columns means -1). |
147
|
|
|
*/ |
148
|
|
|
private int $lastColumnIndex; |
149
|
|
|
|
150
|
|
|
/** |
151
|
|
|
* Sheet object. |
152
|
|
|
* |
153
|
|
|
* @var \PhpOffice\PhpSpreadsheet\Worksheet\Worksheet |
154
|
|
|
*/ |
155
|
|
|
public $phpSheet; |
156
|
|
|
|
157
|
|
|
/** |
158
|
|
|
* Escher object corresponding to MSODRAWING. |
159
|
|
|
*/ |
160
|
|
|
private ?\PhpOffice\PhpSpreadsheet\Shared\Escher $escher = null; |
161
|
|
|
|
162
|
|
|
/** |
163
|
|
|
* Array of font hashes associated to FONT records index. |
164
|
|
|
* |
165
|
|
|
* @var array |
166
|
|
|
*/ |
167
|
|
|
public $fontHashIndex; |
168
|
|
|
|
169
|
|
|
/** |
170
|
|
|
* @var bool |
171
|
|
|
*/ |
172
|
|
|
private $preCalculateFormulas; |
173
|
|
|
|
174
|
|
|
private int $printHeaders; |
175
|
|
|
|
176
|
|
|
/** |
177
|
|
|
* Constructor. |
178
|
|
|
* |
179
|
|
|
* @param int $str_total Total number of strings |
180
|
|
|
* @param int $str_unique Total number of unique strings |
181
|
|
|
* @param array $str_table String Table |
182
|
|
|
* @param array $colors Colour Table |
183
|
|
|
* @param Parser $parser The formula parser created for the Workbook |
184
|
|
|
* @param bool $preCalculateFormulas Flag indicating whether formulas should be calculated or just written |
185
|
|
|
* @param \PhpOffice\PhpSpreadsheet\Worksheet\Worksheet $phpSheet The worksheet to write |
186
|
|
|
*/ |
187
|
96 |
|
public function __construct(&$str_total, &$str_unique, &$str_table, &$colors, Parser $parser, $preCalculateFormulas, \PhpOffice\PhpSpreadsheet\Worksheet\Worksheet $phpSheet) |
188
|
|
|
{ |
189
|
|
|
// It needs to call its parent's constructor explicitly |
190
|
96 |
|
parent::__construct(); |
191
|
|
|
|
192
|
96 |
|
$this->preCalculateFormulas = $preCalculateFormulas; |
193
|
96 |
|
$this->stringTotal = &$str_total; |
194
|
96 |
|
$this->stringUnique = &$str_unique; |
195
|
96 |
|
$this->stringTable = &$str_table; |
196
|
96 |
|
$this->colors = &$colors; |
197
|
96 |
|
$this->parser = $parser; |
198
|
|
|
|
199
|
96 |
|
$this->phpSheet = $phpSheet; |
200
|
|
|
|
201
|
96 |
|
$this->columnInfo = []; |
202
|
96 |
|
$this->activePane = 3; |
203
|
|
|
|
204
|
96 |
|
$this->printHeaders = 0; |
205
|
|
|
|
206
|
96 |
|
$this->outlineStyle = false; |
207
|
96 |
|
$this->outlineBelow = true; |
208
|
96 |
|
$this->outlineRight = true; |
209
|
96 |
|
$this->outlineOn = true; |
210
|
|
|
|
211
|
96 |
|
$this->fontHashIndex = []; |
212
|
|
|
|
213
|
|
|
// calculate values for DIMENSIONS record |
214
|
96 |
|
$minR = 1; |
215
|
96 |
|
$minC = 'A'; |
216
|
|
|
|
217
|
96 |
|
$maxR = $this->phpSheet->getHighestRow(); |
218
|
96 |
|
$maxC = $this->phpSheet->getHighestColumn(); |
219
|
|
|
|
220
|
|
|
// Determine lowest and highest column and row |
221
|
96 |
|
$this->firstRowIndex = $minR; |
222
|
96 |
|
$this->lastRowIndex = ($maxR > 65535) ? 65535 : $maxR; |
223
|
|
|
|
224
|
96 |
|
$this->firstColumnIndex = Coordinate::columnIndexFromString($minC); |
225
|
96 |
|
$this->lastColumnIndex = Coordinate::columnIndexFromString($maxC); |
226
|
|
|
|
227
|
96 |
|
if ($this->lastColumnIndex > 255) { |
228
|
1 |
|
$this->lastColumnIndex = 255; |
229
|
|
|
} |
230
|
|
|
} |
231
|
|
|
|
232
|
|
|
/** |
233
|
|
|
* Add data to the beginning of the workbook (note the reverse order) |
234
|
|
|
* and to the end of the workbook. |
235
|
|
|
* |
236
|
|
|
* @see Workbook::storeWorkbook |
237
|
|
|
*/ |
238
|
96 |
|
public function close(): void |
239
|
|
|
{ |
240
|
96 |
|
$phpSheet = $this->phpSheet; |
241
|
|
|
|
242
|
|
|
// Storing selected cells and active sheet because it changes while parsing cells with formulas. |
243
|
96 |
|
$selectedCells = $this->phpSheet->getSelectedCells(); |
244
|
96 |
|
$activeSheetIndex = $this->phpSheet->getParentOrThrow()->getActiveSheetIndex(); |
245
|
|
|
|
246
|
|
|
// Write BOF record |
247
|
96 |
|
$this->storeBof(0x0010); |
248
|
|
|
|
249
|
|
|
// Write PRINTHEADERS |
250
|
96 |
|
$this->writePrintHeaders(); |
251
|
|
|
|
252
|
|
|
// Write PRINTGRIDLINES |
253
|
96 |
|
$this->writePrintGridlines(); |
254
|
|
|
|
255
|
|
|
// Write GRIDSET |
256
|
96 |
|
$this->writeGridset(); |
257
|
|
|
|
258
|
|
|
// Calculate column widths |
259
|
96 |
|
$phpSheet->calculateColumnWidths(); |
260
|
|
|
|
261
|
|
|
// Column dimensions |
262
|
96 |
|
if (($defaultWidth = $phpSheet->getDefaultColumnDimension()->getWidth()) < 0) { |
263
|
91 |
|
$defaultWidth = \PhpOffice\PhpSpreadsheet\Shared\Font::getDefaultColumnWidthByFont($phpSheet->getParentOrThrow()->getDefaultStyle()->getFont()); |
264
|
|
|
} |
265
|
|
|
|
266
|
96 |
|
$columnDimensions = $phpSheet->getColumnDimensions(); |
267
|
96 |
|
$maxCol = $this->lastColumnIndex - 1; |
268
|
96 |
|
for ($i = 0; $i <= $maxCol; ++$i) { |
269
|
96 |
|
$hidden = 0; |
270
|
96 |
|
$level = 0; |
271
|
96 |
|
$xfIndex = 15; // there are 15 cell style Xfs |
272
|
|
|
|
273
|
96 |
|
$width = $defaultWidth; |
274
|
|
|
|
275
|
96 |
|
$columnLetter = Coordinate::stringFromColumnIndex($i + 1); |
276
|
96 |
|
if (isset($columnDimensions[$columnLetter])) { |
277
|
37 |
|
$columnDimension = $columnDimensions[$columnLetter]; |
278
|
37 |
|
if ($columnDimension->getWidth() >= 0) { |
279
|
36 |
|
$width = $columnDimension->getWidth(); |
280
|
|
|
} |
281
|
37 |
|
$hidden = $columnDimension->getVisible() ? 0 : 1; |
282
|
37 |
|
$level = $columnDimension->getOutlineLevel(); |
283
|
37 |
|
$xfIndex = $columnDimension->getXfIndex() + 15; // there are 15 cell style Xfs |
284
|
|
|
} |
285
|
|
|
|
286
|
|
|
// Components of columnInfo: |
287
|
|
|
// $firstcol first column on the range |
288
|
|
|
// $lastcol last column on the range |
289
|
|
|
// $width width to set |
290
|
|
|
// $xfIndex The optional cell style Xf index to apply to the columns |
291
|
|
|
// $hidden The optional hidden atribute |
292
|
|
|
// $level The optional outline level |
293
|
96 |
|
$this->columnInfo[] = [$i, $i, $width, $xfIndex, $hidden, $level]; |
294
|
|
|
} |
295
|
|
|
|
296
|
|
|
// Write GUTS |
297
|
96 |
|
$this->writeGuts(); |
298
|
|
|
|
299
|
|
|
// Write DEFAULTROWHEIGHT |
300
|
96 |
|
$this->writeDefaultRowHeight(); |
301
|
|
|
// Write WSBOOL |
302
|
96 |
|
$this->writeWsbool(); |
303
|
|
|
// Write horizontal and vertical page breaks |
304
|
96 |
|
$this->writeBreaks(); |
305
|
|
|
// Write page header |
306
|
96 |
|
$this->writeHeader(); |
307
|
|
|
// Write page footer |
308
|
96 |
|
$this->writeFooter(); |
309
|
|
|
// Write page horizontal centering |
310
|
96 |
|
$this->writeHcenter(); |
311
|
|
|
// Write page vertical centering |
312
|
96 |
|
$this->writeVcenter(); |
313
|
|
|
// Write left margin |
314
|
96 |
|
$this->writeMarginLeft(); |
315
|
|
|
// Write right margin |
316
|
96 |
|
$this->writeMarginRight(); |
317
|
|
|
// Write top margin |
318
|
96 |
|
$this->writeMarginTop(); |
319
|
|
|
// Write bottom margin |
320
|
96 |
|
$this->writeMarginBottom(); |
321
|
|
|
// Write page setup |
322
|
96 |
|
$this->writeSetup(); |
323
|
|
|
// Write sheet protection |
324
|
96 |
|
$this->writeProtect(); |
325
|
|
|
// Write SCENPROTECT |
326
|
96 |
|
$this->writeScenProtect(); |
327
|
|
|
// Write OBJECTPROTECT |
328
|
96 |
|
$this->writeObjectProtect(); |
329
|
|
|
// Write sheet password |
330
|
96 |
|
$this->writePassword(); |
331
|
|
|
// Write DEFCOLWIDTH record |
332
|
96 |
|
$this->writeDefcol(); |
333
|
|
|
|
334
|
|
|
// Write the COLINFO records if they exist |
335
|
96 |
|
if (!empty($this->columnInfo)) { |
336
|
96 |
|
$colcount = count($this->columnInfo); |
337
|
96 |
|
for ($i = 0; $i < $colcount; ++$i) { |
338
|
96 |
|
$this->writeColinfo($this->columnInfo[$i]); |
339
|
|
|
} |
340
|
|
|
} |
341
|
96 |
|
$autoFilterRange = $phpSheet->getAutoFilter()->getRange(); |
342
|
96 |
|
if (!empty($autoFilterRange)) { |
343
|
|
|
// Write AUTOFILTERINFO |
344
|
3 |
|
$this->writeAutoFilterInfo(); |
345
|
|
|
} |
346
|
|
|
|
347
|
|
|
// Write sheet dimensions |
348
|
96 |
|
$this->writeDimensions(); |
349
|
|
|
|
350
|
|
|
// Row dimensions |
351
|
96 |
|
foreach ($phpSheet->getRowDimensions() as $rowDimension) { |
352
|
25 |
|
$xfIndex = $rowDimension->getXfIndex() + 15; // there are 15 cellXfs |
353
|
25 |
|
$this->writeRow( |
354
|
25 |
|
$rowDimension->getRowIndex() - 1, |
355
|
25 |
|
(int) $rowDimension->getRowHeight(), |
356
|
25 |
|
$xfIndex, |
357
|
25 |
|
!$rowDimension->getVisible(), |
358
|
25 |
|
$rowDimension->getOutlineLevel() |
359
|
25 |
|
); |
360
|
|
|
} |
361
|
|
|
|
362
|
|
|
// Write Cells |
363
|
96 |
|
foreach ($phpSheet->getCellCollection()->getSortedCoordinates() as $coordinate) { |
364
|
|
|
/** @var Cell $cell */ |
365
|
91 |
|
$cell = $phpSheet->getCellCollection()->get($coordinate); |
366
|
91 |
|
$row = $cell->getRow() - 1; |
367
|
91 |
|
$column = Coordinate::columnIndexFromString($cell->getColumn()) - 1; |
368
|
|
|
|
369
|
|
|
// Don't break Excel break the code! |
370
|
91 |
|
if ($row > 65535 || $column > 255) { |
371
|
|
|
throw new WriterException('Rows or columns overflow! Excel5 has limit to 65535 rows and 255 columns. Use XLSX instead.'); |
372
|
|
|
} |
373
|
|
|
|
374
|
|
|
// Write cell value |
375
|
91 |
|
$xfIndex = $cell->getXfIndex() + 15; // there are 15 cell style Xfs |
376
|
|
|
|
377
|
91 |
|
$cVal = $cell->getValue(); |
378
|
91 |
|
if ($cVal instanceof RichText) { |
379
|
11 |
|
$arrcRun = []; |
380
|
11 |
|
$str_pos = 0; |
381
|
11 |
|
$elements = $cVal->getRichTextElements(); |
382
|
11 |
|
foreach ($elements as $element) { |
383
|
|
|
// FONT Index |
384
|
11 |
|
$str_fontidx = 0; |
385
|
11 |
|
if ($element instanceof Run) { |
386
|
11 |
|
$getFont = $element->getFont(); |
387
|
11 |
|
if ($getFont !== null) { |
388
|
11 |
|
$str_fontidx = $this->fontHashIndex[$getFont->getHashCode()]; |
389
|
|
|
} |
390
|
|
|
} |
391
|
11 |
|
$arrcRun[] = ['strlen' => $str_pos, 'fontidx' => $str_fontidx]; |
392
|
|
|
// Position FROM |
393
|
11 |
|
$str_pos += StringHelper::countCharacters($element->getText(), 'UTF-8'); |
394
|
|
|
} |
395
|
11 |
|
$this->writeRichTextString($row, $column, $cVal->getPlainText(), $xfIndex, $arrcRun); |
396
|
|
|
} else { |
397
|
90 |
|
switch ($cell->getDatatype()) { |
398
|
|
|
case DataType::TYPE_STRING: |
399
|
|
|
case DataType::TYPE_INLINE: |
400
|
|
|
case DataType::TYPE_NULL: |
401
|
71 |
|
if ($cVal === '' || $cVal === null) { |
402
|
38 |
|
$this->writeBlank($row, $column, $xfIndex); |
403
|
|
|
} else { |
404
|
66 |
|
$this->writeString($row, $column, $cVal, $xfIndex); |
405
|
|
|
} |
406
|
|
|
|
407
|
71 |
|
break; |
408
|
|
|
case DataType::TYPE_NUMERIC: |
409
|
49 |
|
$this->writeNumber($row, $column, $cVal, $xfIndex); |
410
|
|
|
|
411
|
49 |
|
break; |
412
|
|
|
case DataType::TYPE_FORMULA: |
413
|
38 |
|
$calculatedValue = $this->preCalculateFormulas ? |
414
|
38 |
|
$cell->getCalculatedValue() : null; |
415
|
38 |
|
if (self::WRITE_FORMULA_EXCEPTION == $this->writeFormula($row, $column, $cVal, $xfIndex, $calculatedValue)) { |
416
|
7 |
|
if ($calculatedValue === null) { |
417
|
|
|
$calculatedValue = $cell->getCalculatedValue(); |
418
|
|
|
} |
419
|
7 |
|
$calctype = gettype($calculatedValue); |
420
|
7 |
|
match ($calctype) { |
421
|
7 |
|
'integer', 'double' => $this->writeNumber($row, $column, (float) $calculatedValue, $xfIndex), |
422
|
7 |
|
'string' => $this->writeString($row, $column, $calculatedValue, $xfIndex), |
|
|
|
|
423
|
7 |
|
'boolean' => $this->writeBoolErr($row, $column, (int) $calculatedValue, 0, $xfIndex), |
424
|
7 |
|
default => $this->writeString($row, $column, $cVal, $xfIndex), |
|
|
|
|
425
|
7 |
|
}; |
426
|
|
|
} |
427
|
|
|
|
428
|
37 |
|
break; |
429
|
|
|
case DataType::TYPE_BOOL: |
430
|
9 |
|
$this->writeBoolErr($row, $column, $cVal, 0, $xfIndex); |
431
|
|
|
|
432
|
9 |
|
break; |
433
|
|
|
case DataType::TYPE_ERROR: |
434
|
1 |
|
$this->writeBoolErr($row, $column, ErrorCode::error($cVal), 1, $xfIndex); |
435
|
|
|
|
436
|
1 |
|
break; |
437
|
|
|
} |
438
|
|
|
} |
439
|
|
|
} |
440
|
|
|
|
441
|
|
|
// Append |
442
|
95 |
|
$this->writeMsoDrawing(); |
443
|
|
|
|
444
|
|
|
// Restoring active sheet. |
445
|
95 |
|
$this->phpSheet->getParentOrThrow()->setActiveSheetIndex($activeSheetIndex); |
446
|
|
|
|
447
|
|
|
// Write WINDOW2 record |
448
|
95 |
|
$this->writeWindow2(); |
449
|
|
|
|
450
|
|
|
// Write PLV record |
451
|
95 |
|
$this->writePageLayoutView(); |
452
|
|
|
|
453
|
|
|
// Write ZOOM record |
454
|
95 |
|
$this->writeZoom(); |
455
|
95 |
|
if ($phpSheet->getFreezePane()) { |
456
|
8 |
|
$this->writePanes(); |
457
|
|
|
} |
458
|
|
|
|
459
|
|
|
// Restoring selected cells. |
460
|
95 |
|
$this->phpSheet->setSelectedCells($selectedCells); |
461
|
|
|
|
462
|
|
|
// Write SELECTION record |
463
|
95 |
|
$this->writeSelection(); |
464
|
|
|
|
465
|
|
|
// Write MergedCellsTable Record |
466
|
95 |
|
$this->writeMergedCells(); |
467
|
|
|
|
468
|
|
|
// Hyperlinks |
469
|
95 |
|
$phpParent = $phpSheet->getParent(); |
470
|
95 |
|
$hyperlinkbase = ($phpParent === null) ? '' : $phpParent->getProperties()->getHyperlinkBase(); |
471
|
95 |
|
foreach ($phpSheet->getHyperLinkCollection() as $coordinate => $hyperlink) { |
472
|
12 |
|
[$column, $row] = Coordinate::indexesFromString($coordinate); |
473
|
|
|
|
474
|
12 |
|
$url = $hyperlink->getUrl(); |
475
|
|
|
|
476
|
12 |
|
if (str_contains($url, 'sheet://')) { |
477
|
|
|
// internal to current workbook |
478
|
7 |
|
$url = str_replace('sheet://', 'internal:', $url); |
479
|
12 |
|
} elseif (preg_match('/^(http:|https:|ftp:|mailto:)/', $url)) { |
480
|
|
|
// URL |
481
|
2 |
|
} elseif (!empty($hyperlinkbase) && preg_match('~^([A-Za-z]:)?[/\\\\]~', $url) !== 1) { |
482
|
1 |
|
$url = "$hyperlinkbase$url"; |
483
|
1 |
|
if (preg_match('/^(http:|https:|ftp:|mailto:)/', $url) !== 1) { |
484
|
1 |
|
$url = 'external:' . $url; |
485
|
|
|
} |
486
|
|
|
} else { |
487
|
|
|
// external (local file) |
488
|
1 |
|
$url = 'external:' . $url; |
489
|
|
|
} |
490
|
|
|
|
491
|
12 |
|
$this->writeUrl($row - 1, $column - 1, $url); |
492
|
|
|
} |
493
|
|
|
|
494
|
95 |
|
$this->writeDataValidity(); |
495
|
95 |
|
$this->writeSheetLayout(); |
496
|
|
|
|
497
|
|
|
// Write SHEETPROTECTION record |
498
|
95 |
|
$this->writeSheetProtection(); |
499
|
95 |
|
$this->writeRangeProtection(); |
500
|
|
|
|
501
|
|
|
// Write Conditional Formatting Rules and Styles |
502
|
95 |
|
$this->writeConditionalFormatting(); |
503
|
|
|
|
504
|
95 |
|
$this->storeEof(); |
505
|
|
|
} |
506
|
|
|
|
507
|
95 |
|
private function writeConditionalFormatting(): void |
508
|
|
|
{ |
509
|
95 |
|
$conditionalFormulaHelper = new ConditionalHelper($this->parser); |
510
|
|
|
|
511
|
95 |
|
$arrConditionalStyles = $this->phpSheet->getConditionalStylesCollection(); |
512
|
95 |
|
if (!empty($arrConditionalStyles)) { |
513
|
9 |
|
$arrConditional = []; |
514
|
|
|
|
515
|
|
|
// Write ConditionalFormattingTable records |
516
|
9 |
|
foreach ($arrConditionalStyles as $cellCoordinate => $conditionalStyles) { |
517
|
9 |
|
$cfHeaderWritten = false; |
518
|
9 |
|
foreach ($conditionalStyles as $conditional) { |
519
|
|
|
/** @var Conditional $conditional */ |
520
|
|
|
if ( |
521
|
9 |
|
$conditional->getConditionType() === Conditional::CONDITION_EXPRESSION || |
522
|
9 |
|
$conditional->getConditionType() === Conditional::CONDITION_CELLIS |
523
|
|
|
) { |
524
|
|
|
// Write CFHEADER record (only if there are Conditional Styles that we are able to write) |
525
|
5 |
|
if ($cfHeaderWritten === false) { |
526
|
5 |
|
$cfHeaderWritten = $this->writeCFHeader($cellCoordinate, $conditionalStyles); |
527
|
|
|
} |
528
|
5 |
|
if ($cfHeaderWritten === true && !isset($arrConditional[$conditional->getHashCode()])) { |
529
|
|
|
// This hash code has been handled |
530
|
5 |
|
$arrConditional[$conditional->getHashCode()] = true; |
531
|
|
|
|
532
|
|
|
// Write CFRULE record |
533
|
5 |
|
$this->writeCFRule($conditionalFormulaHelper, $conditional, $cellCoordinate); |
534
|
|
|
} |
535
|
|
|
} |
536
|
|
|
} |
537
|
|
|
} |
538
|
|
|
} |
539
|
|
|
} |
540
|
|
|
|
541
|
|
|
/** |
542
|
|
|
* Write a cell range address in BIFF8 |
543
|
|
|
* always fixed range |
544
|
|
|
* See section 2.5.14 in OpenOffice.org's Documentation of the Microsoft Excel File Format. |
545
|
|
|
* |
546
|
|
|
* @param string $range E.g. 'A1' or 'A1:B6' |
547
|
|
|
* |
548
|
|
|
* @return string Binary data |
549
|
|
|
*/ |
550
|
9 |
|
private function writeBIFF8CellRangeAddressFixed($range): string |
551
|
|
|
{ |
552
|
9 |
|
$explodes = explode(':', $range); |
553
|
|
|
|
554
|
|
|
// extract first cell, e.g. 'A1' |
555
|
9 |
|
$firstCell = $explodes[0]; |
556
|
|
|
|
557
|
|
|
// extract last cell, e.g. 'B6' |
558
|
9 |
|
if (count($explodes) == 1) { |
559
|
3 |
|
$lastCell = $firstCell; |
560
|
|
|
} else { |
561
|
6 |
|
$lastCell = $explodes[1]; |
562
|
|
|
} |
563
|
|
|
|
564
|
9 |
|
$firstCellCoordinates = Coordinate::indexesFromString($firstCell); // e.g. [0, 1] |
565
|
9 |
|
$lastCellCoordinates = Coordinate::indexesFromString($lastCell); // e.g. [1, 6] |
566
|
|
|
|
567
|
9 |
|
return pack('vvvv', $firstCellCoordinates[1] - 1, $lastCellCoordinates[1] - 1, $firstCellCoordinates[0] - 1, $lastCellCoordinates[0] - 1); |
568
|
|
|
} |
569
|
|
|
|
570
|
|
|
/** |
571
|
|
|
* Retrieves data from memory in one chunk, or from disk |
572
|
|
|
* sized chunks. |
573
|
|
|
* |
574
|
|
|
* @return string The data |
575
|
|
|
*/ |
576
|
95 |
|
public function getData() |
577
|
|
|
{ |
578
|
|
|
// Return data stored in memory |
579
|
95 |
|
if (isset($this->_data)) { |
580
|
95 |
|
$tmp = $this->_data; |
581
|
95 |
|
$this->_data = null; |
582
|
|
|
|
583
|
95 |
|
return $tmp; |
584
|
|
|
} |
585
|
|
|
|
586
|
|
|
// No data to return |
587
|
|
|
return ''; |
588
|
|
|
} |
589
|
|
|
|
590
|
|
|
/** |
591
|
|
|
* Set the option to print the row and column headers on the printed page. |
592
|
|
|
* |
593
|
|
|
* @param int $print Whether to print the headers or not. Defaults to 1 (print). |
594
|
|
|
*/ |
595
|
|
|
public function printRowColHeaders($print = 1): void |
596
|
|
|
{ |
597
|
|
|
$this->printHeaders = $print; |
598
|
|
|
} |
599
|
|
|
|
600
|
|
|
/** |
601
|
|
|
* This method sets the properties for outlining and grouping. The defaults |
602
|
|
|
* correspond to Excel's defaults. |
603
|
|
|
* |
604
|
|
|
* @param bool $visible |
605
|
|
|
* @param bool $symbols_below |
606
|
|
|
* @param bool $symbols_right |
607
|
|
|
* @param bool $auto_style |
608
|
|
|
*/ |
609
|
|
|
public function setOutline($visible = true, $symbols_below = true, $symbols_right = true, $auto_style = false): void |
610
|
|
|
{ |
611
|
|
|
$this->outlineOn = $visible; |
612
|
|
|
$this->outlineBelow = $symbols_below; |
613
|
|
|
$this->outlineRight = $symbols_right; |
614
|
|
|
$this->outlineStyle = $auto_style; |
615
|
|
|
} |
616
|
|
|
|
617
|
|
|
/** |
618
|
|
|
* Write a double to the specified row and column (zero indexed). |
619
|
|
|
* An integer can be written as a double. Excel will display an |
620
|
|
|
* integer. $format is optional. |
621
|
|
|
* |
622
|
|
|
* Returns 0 : normal termination |
623
|
|
|
* -2 : row or column out of range |
624
|
|
|
* |
625
|
|
|
* @param int $row Zero indexed row |
626
|
|
|
* @param int $col Zero indexed column |
627
|
|
|
* @param float $num The number to write |
628
|
|
|
* @param int $xfIndex The optional XF format |
629
|
|
|
*/ |
630
|
50 |
|
private function writeNumber(int $row, int $col, $num, int $xfIndex): int |
631
|
|
|
{ |
632
|
50 |
|
$record = 0x0203; // Record identifier |
633
|
50 |
|
$length = 0x000E; // Number of bytes to follow |
634
|
|
|
|
635
|
50 |
|
$header = pack('vv', $record, $length); |
636
|
50 |
|
$data = pack('vvv', $row, $col, $xfIndex); |
637
|
50 |
|
$xl_double = pack('d', $num); |
638
|
50 |
|
if (self::getByteOrder()) { // if it's Big Endian |
639
|
|
|
$xl_double = strrev($xl_double); |
640
|
|
|
} |
641
|
|
|
|
642
|
50 |
|
$this->append($header . $data . $xl_double); |
643
|
|
|
|
644
|
50 |
|
return 0; |
645
|
|
|
} |
646
|
|
|
|
647
|
|
|
/** |
648
|
|
|
* Write a LABELSST record or a LABEL record. Which one depends on BIFF version. |
649
|
|
|
* |
650
|
|
|
* @param int $row Row index (0-based) |
651
|
|
|
* @param int $col Column index (0-based) |
652
|
|
|
* @param string $str The string |
653
|
|
|
* @param int $xfIndex Index to XF record |
654
|
|
|
*/ |
655
|
67 |
|
private function writeString(int $row, int $col, $str, int $xfIndex): void |
656
|
|
|
{ |
657
|
67 |
|
$this->writeLabelSst($row, $col, $str, $xfIndex); |
658
|
|
|
} |
659
|
|
|
|
660
|
|
|
/** |
661
|
|
|
* Write a LABELSST record or a LABEL record. Which one depends on BIFF version |
662
|
|
|
* It differs from writeString by the writing of rich text strings. |
663
|
|
|
* |
664
|
|
|
* @param int $row Row index (0-based) |
665
|
|
|
* @param int $col Column index (0-based) |
666
|
|
|
* @param string $str The string |
667
|
|
|
* @param int $xfIndex The XF format index for the cell |
668
|
|
|
* @param array $arrcRun Index to Font record and characters beginning |
669
|
|
|
*/ |
670
|
11 |
|
private function writeRichTextString(int $row, int $col, string $str, int $xfIndex, array $arrcRun): void |
671
|
|
|
{ |
672
|
11 |
|
$record = 0x00FD; // Record identifier |
673
|
11 |
|
$length = 0x000A; // Bytes to follow |
674
|
11 |
|
$str = StringHelper::UTF8toBIFF8UnicodeShort($str, $arrcRun); |
675
|
|
|
|
676
|
|
|
// check if string is already present |
677
|
11 |
|
if (!isset($this->stringTable[$str])) { |
678
|
11 |
|
$this->stringTable[$str] = $this->stringUnique++; |
679
|
|
|
} |
680
|
11 |
|
++$this->stringTotal; |
681
|
|
|
|
682
|
11 |
|
$header = pack('vv', $record, $length); |
683
|
11 |
|
$data = pack('vvvV', $row, $col, $xfIndex, $this->stringTable[$str]); |
684
|
11 |
|
$this->append($header . $data); |
685
|
|
|
} |
686
|
|
|
|
687
|
|
|
/** |
688
|
|
|
* Write a string to the specified row and column (zero indexed). |
689
|
|
|
* This is the BIFF8 version (no 255 chars limit). |
690
|
|
|
* $format is optional. |
691
|
|
|
* |
692
|
|
|
* @param int $row Zero indexed row |
693
|
|
|
* @param int $col Zero indexed column |
694
|
|
|
* @param string $str The string to write |
695
|
|
|
* @param int $xfIndex The XF format index for the cell |
696
|
|
|
*/ |
697
|
67 |
|
private function writeLabelSst(int $row, int $col, $str, int $xfIndex): void |
698
|
|
|
{ |
699
|
67 |
|
$record = 0x00FD; // Record identifier |
700
|
67 |
|
$length = 0x000A; // Bytes to follow |
701
|
|
|
|
702
|
67 |
|
$str = StringHelper::UTF8toBIFF8UnicodeLong($str); |
703
|
|
|
|
704
|
|
|
// check if string is already present |
705
|
67 |
|
if (!isset($this->stringTable[$str])) { |
706
|
67 |
|
$this->stringTable[$str] = $this->stringUnique++; |
707
|
|
|
} |
708
|
67 |
|
++$this->stringTotal; |
709
|
|
|
|
710
|
67 |
|
$header = pack('vv', $record, $length); |
711
|
67 |
|
$data = pack('vvvV', $row, $col, $xfIndex, $this->stringTable[$str]); |
712
|
67 |
|
$this->append($header . $data); |
713
|
|
|
} |
714
|
|
|
|
715
|
|
|
/** |
716
|
|
|
* Write a blank cell to the specified row and column (zero indexed). |
717
|
|
|
* A blank cell is used to specify formatting without adding a string |
718
|
|
|
* or a number. |
719
|
|
|
* |
720
|
|
|
* A blank cell without a format serves no purpose. Therefore, we don't write |
721
|
|
|
* a BLANK record unless a format is specified. |
722
|
|
|
* |
723
|
|
|
* Returns 0 : normal termination (including no format) |
724
|
|
|
* -1 : insufficient number of arguments |
725
|
|
|
* -2 : row or column out of range |
726
|
|
|
* |
727
|
|
|
* @param int $row Zero indexed row |
728
|
|
|
* @param int $col Zero indexed column |
729
|
|
|
* @param int $xfIndex The XF format index |
730
|
|
|
*/ |
731
|
38 |
|
public function writeBlank($row, $col, $xfIndex): int |
732
|
|
|
{ |
733
|
38 |
|
$record = 0x0201; // Record identifier |
734
|
38 |
|
$length = 0x0006; // Number of bytes to follow |
735
|
|
|
|
736
|
38 |
|
$header = pack('vv', $record, $length); |
737
|
38 |
|
$data = pack('vvv', $row, $col, $xfIndex); |
738
|
38 |
|
$this->append($header . $data); |
739
|
|
|
|
740
|
38 |
|
return 0; |
741
|
|
|
} |
742
|
|
|
|
743
|
|
|
/** |
744
|
|
|
* Write a boolean or an error type to the specified row and column (zero indexed). |
745
|
|
|
* |
746
|
|
|
* @param int $row Row index (0-based) |
747
|
|
|
* @param int $col Column index (0-based) |
748
|
|
|
* @param int $value |
749
|
|
|
* @param int $isError Error or Boolean? |
750
|
|
|
*/ |
751
|
10 |
|
private function writeBoolErr(int $row, int $col, $value, int $isError, int $xfIndex): int |
752
|
|
|
{ |
753
|
10 |
|
$record = 0x0205; |
754
|
10 |
|
$length = 8; |
755
|
|
|
|
756
|
10 |
|
$header = pack('vv', $record, $length); |
757
|
10 |
|
$data = pack('vvvCC', $row, $col, $xfIndex, $value, $isError); |
758
|
10 |
|
$this->append($header . $data); |
759
|
|
|
|
760
|
10 |
|
return 0; |
761
|
|
|
} |
762
|
|
|
|
763
|
|
|
const WRITE_FORMULA_NORMAL = 0; |
764
|
|
|
const WRITE_FORMULA_ERRORS = -1; |
765
|
|
|
const WRITE_FORMULA_RANGE = -2; |
766
|
|
|
const WRITE_FORMULA_EXCEPTION = -3; |
767
|
|
|
|
768
|
|
|
/** @var bool */ |
769
|
|
|
private static $allowThrow = false; |
770
|
|
|
|
771
|
2 |
|
public static function setAllowThrow(bool $allowThrow): void |
772
|
|
|
{ |
773
|
2 |
|
self::$allowThrow = $allowThrow; |
774
|
|
|
} |
775
|
|
|
|
776
|
2 |
|
public static function getAllowThrow(): bool |
777
|
|
|
{ |
778
|
2 |
|
return self::$allowThrow; |
779
|
|
|
} |
780
|
|
|
|
781
|
|
|
/** |
782
|
|
|
* Write a formula to the specified row and column (zero indexed). |
783
|
|
|
* The textual representation of the formula is passed to the parser in |
784
|
|
|
* Parser.php which returns a packed binary string. |
785
|
|
|
* |
786
|
|
|
* Returns 0 : WRITE_FORMULA_NORMAL normal termination |
787
|
|
|
* -1 : WRITE_FORMULA_ERRORS formula errors (bad formula) |
788
|
|
|
* -2 : WRITE_FORMULA_RANGE row or column out of range |
789
|
|
|
* -3 : WRITE_FORMULA_EXCEPTION parse raised exception, probably due to definedname |
790
|
|
|
* |
791
|
|
|
* @param int $row Zero indexed row |
792
|
|
|
* @param int $col Zero indexed column |
793
|
|
|
* @param string $formula The formula text string |
794
|
|
|
* @param int $xfIndex The XF format index |
795
|
|
|
* @param mixed $calculatedValue Calculated value |
796
|
|
|
* |
797
|
|
|
* @return int |
798
|
|
|
*/ |
799
|
38 |
|
private function writeFormula(int $row, int $col, string $formula, int $xfIndex, mixed $calculatedValue) |
800
|
|
|
{ |
801
|
38 |
|
$record = 0x0006; // Record identifier |
802
|
|
|
// Initialize possible additional value for STRING record that should be written after the FORMULA record? |
803
|
38 |
|
$stringValue = null; |
804
|
|
|
|
805
|
|
|
// calculated value |
806
|
38 |
|
if (isset($calculatedValue)) { |
807
|
|
|
// Since we can't yet get the data type of the calculated value, |
808
|
|
|
// we use best effort to determine data type |
809
|
36 |
|
if (is_bool($calculatedValue)) { |
810
|
|
|
// Boolean value |
811
|
6 |
|
$num = pack('CCCvCv', 0x01, 0x00, (int) $calculatedValue, 0x00, 0x00, 0xFFFF); |
812
|
35 |
|
} elseif (is_int($calculatedValue) || is_float($calculatedValue)) { |
813
|
|
|
// Numeric value |
814
|
30 |
|
$num = pack('d', $calculatedValue); |
815
|
22 |
|
} elseif (is_string($calculatedValue)) { |
816
|
22 |
|
$errorCodes = DataType::getErrorCodes(); |
817
|
22 |
|
if (isset($errorCodes[$calculatedValue])) { |
818
|
|
|
// Error value |
819
|
6 |
|
$num = pack('CCCvCv', 0x02, 0x00, ErrorCode::error($calculatedValue), 0x00, 0x00, 0xFFFF); |
820
|
20 |
|
} elseif ($calculatedValue === '') { |
821
|
|
|
// Empty string (and BIFF8) |
822
|
7 |
|
$num = pack('CCCvCv', 0x03, 0x00, 0x00, 0x00, 0x00, 0xFFFF); |
823
|
|
|
} else { |
824
|
|
|
// Non-empty string value (or empty string BIFF5) |
825
|
14 |
|
$stringValue = $calculatedValue; |
826
|
22 |
|
$num = pack('CCCvCv', 0x00, 0x00, 0x00, 0x00, 0x00, 0xFFFF); |
827
|
|
|
} |
828
|
|
|
} else { |
829
|
|
|
// We are really not supposed to reach here |
830
|
36 |
|
$num = pack('d', 0x00); |
831
|
|
|
} |
832
|
|
|
} else { |
833
|
2 |
|
$num = pack('d', 0x00); |
834
|
|
|
} |
835
|
|
|
|
836
|
38 |
|
$grbit = 0x03; // Option flags |
837
|
38 |
|
$unknown = 0x0000; // Must be zero |
838
|
|
|
|
839
|
|
|
// Strip the '=' or '@' sign at the beginning of the formula string |
840
|
38 |
|
if ($formula[0] == '=') { |
841
|
38 |
|
$formula = substr($formula, 1); |
842
|
|
|
} else { |
843
|
|
|
// Error handling |
844
|
|
|
$this->writeString($row, $col, 'Unrecognised character for formula', 0); |
845
|
|
|
|
846
|
|
|
return self::WRITE_FORMULA_ERRORS; |
847
|
|
|
} |
848
|
|
|
|
849
|
|
|
// Parse the formula using the parser in Parser.php |
850
|
|
|
try { |
851
|
38 |
|
$this->parser->parse($formula); |
852
|
38 |
|
$formula = $this->parser->toReversePolish(); |
853
|
|
|
|
854
|
36 |
|
$formlen = strlen($formula); // Length of the binary string |
855
|
36 |
|
$length = 0x16 + $formlen; // Length of the record data |
856
|
|
|
|
857
|
36 |
|
$header = pack('vv', $record, $length); |
858
|
|
|
|
859
|
36 |
|
$data = pack('vvv', $row, $col, $xfIndex) |
860
|
36 |
|
. $num |
861
|
36 |
|
. pack('vVv', $grbit, $unknown, $formlen); |
862
|
36 |
|
$this->append($header . $data . $formula); |
863
|
|
|
|
864
|
|
|
// Append also a STRING record if necessary |
865
|
36 |
|
if ($stringValue !== null) { |
866
|
13 |
|
$this->writeStringRecord($stringValue); |
867
|
|
|
} |
868
|
|
|
|
869
|
36 |
|
return self::WRITE_FORMULA_NORMAL; |
870
|
8 |
|
} catch (PhpSpreadsheetException $e) { |
871
|
8 |
|
if (self::$allowThrow) { |
872
|
1 |
|
throw $e; |
873
|
|
|
} |
874
|
|
|
|
875
|
7 |
|
return self::WRITE_FORMULA_EXCEPTION; |
876
|
|
|
} |
877
|
|
|
} |
878
|
|
|
|
879
|
|
|
/** |
880
|
|
|
* Write a STRING record. This. |
881
|
|
|
*/ |
882
|
13 |
|
private function writeStringRecord(string $stringValue): void |
883
|
|
|
{ |
884
|
13 |
|
$record = 0x0207; // Record identifier |
885
|
13 |
|
$data = StringHelper::UTF8toBIFF8UnicodeLong($stringValue); |
886
|
|
|
|
887
|
13 |
|
$length = strlen($data); |
888
|
13 |
|
$header = pack('vv', $record, $length); |
889
|
|
|
|
890
|
13 |
|
$this->append($header . $data); |
891
|
|
|
} |
892
|
|
|
|
893
|
|
|
/** |
894
|
|
|
* Write a hyperlink. |
895
|
|
|
* This is comprised of two elements: the visible label and |
896
|
|
|
* the invisible link. The visible label is the same as the link unless an |
897
|
|
|
* alternative string is specified. The label is written using the |
898
|
|
|
* writeString() method. Therefore the 255 characters string limit applies. |
899
|
|
|
* $string and $format are optional. |
900
|
|
|
* |
901
|
|
|
* The hyperlink can be to a http, ftp, mail, internal sheet (not yet), or external |
902
|
|
|
* directory url. |
903
|
|
|
* |
904
|
|
|
* @param int $row Row |
905
|
|
|
* @param int $col Column |
906
|
|
|
* @param string $url URL string |
907
|
|
|
*/ |
908
|
12 |
|
private function writeUrl(int $row, int $col, $url): void |
909
|
|
|
{ |
910
|
|
|
// Add start row and col to arg list |
911
|
12 |
|
$this->writeUrlRange($row, $col, $row, $col, $url); |
912
|
|
|
} |
913
|
|
|
|
914
|
|
|
/** |
915
|
|
|
* This is the more general form of writeUrl(). It allows a hyperlink to be |
916
|
|
|
* written to a range of cells. This function also decides the type of hyperlink |
917
|
|
|
* to be written. These are either, Web (http, ftp, mailto), Internal |
918
|
|
|
* (Sheet1!A1) or external ('c:\temp\foo.xls#Sheet1!A1'). |
919
|
|
|
* |
920
|
|
|
* @param int $row1 Start row |
921
|
|
|
* @param int $col1 Start column |
922
|
|
|
* @param int $row2 End row |
923
|
|
|
* @param int $col2 End column |
924
|
|
|
* @param string $url URL string |
925
|
|
|
* |
926
|
|
|
* @see writeUrl() |
927
|
|
|
*/ |
928
|
12 |
|
private function writeUrlRange(int $row1, int $col1, int $row2, int $col2, $url): void |
929
|
|
|
{ |
930
|
|
|
// Check for internal/external sheet links or default to web link |
931
|
12 |
|
if (preg_match('[^internal:]', $url)) { |
932
|
7 |
|
$this->writeUrlInternal($row1, $col1, $row2, $col2, $url); |
933
|
|
|
} |
934
|
12 |
|
if (preg_match('[^external:]', $url)) { |
935
|
1 |
|
$this->writeUrlExternal($row1, $col1, $row2, $col2, $url); |
936
|
|
|
} |
937
|
|
|
|
938
|
12 |
|
$this->writeUrlWeb($row1, $col1, $row2, $col2, $url); |
939
|
|
|
} |
940
|
|
|
|
941
|
|
|
/** |
942
|
|
|
* Used to write http, ftp and mailto hyperlinks. |
943
|
|
|
* The link type ($options) is 0x03 is the same as absolute dir ref without |
944
|
|
|
* sheet. However it is differentiated by the $unknown2 data stream. |
945
|
|
|
* |
946
|
|
|
* @param int $row1 Start row |
947
|
|
|
* @param int $col1 Start column |
948
|
|
|
* @param int $row2 End row |
949
|
|
|
* @param int $col2 End column |
950
|
|
|
* @param string $url URL string |
951
|
|
|
* |
952
|
|
|
* @see writeUrl() |
953
|
|
|
*/ |
954
|
12 |
|
public function writeUrlWeb($row1, $col1, $row2, $col2, $url): void |
955
|
|
|
{ |
956
|
12 |
|
$record = 0x01B8; // Record identifier |
957
|
|
|
|
958
|
|
|
// Pack the undocumented parts of the hyperlink stream |
959
|
12 |
|
$unknown1 = pack('H*', 'D0C9EA79F9BACE118C8200AA004BA90B02000000'); |
960
|
12 |
|
$unknown2 = pack('H*', 'E0C9EA79F9BACE118C8200AA004BA90B'); |
961
|
|
|
|
962
|
|
|
// Pack the option flags |
963
|
12 |
|
$options = pack('V', 0x03); |
964
|
|
|
|
965
|
|
|
// Convert URL to a null terminated wchar string |
966
|
|
|
|
967
|
|
|
/** @phpstan-ignore-next-line */ |
968
|
12 |
|
$url = implode("\0", preg_split("''", $url, -1, PREG_SPLIT_NO_EMPTY)); |
969
|
12 |
|
$url = $url . "\0\0\0"; |
970
|
|
|
|
971
|
|
|
// Pack the length of the URL |
972
|
12 |
|
$url_len = pack('V', strlen($url)); |
973
|
|
|
|
974
|
|
|
// Calculate the data length |
975
|
12 |
|
$length = 0x34 + strlen($url); |
976
|
|
|
|
977
|
|
|
// Pack the header data |
978
|
12 |
|
$header = pack('vv', $record, $length); |
979
|
12 |
|
$data = pack('vvvv', $row1, $row2, $col1, $col2); |
980
|
|
|
|
981
|
|
|
// Write the packed data |
982
|
12 |
|
$this->append($header . $data . $unknown1 . $options . $unknown2 . $url_len . $url); |
983
|
|
|
} |
984
|
|
|
|
985
|
|
|
/** |
986
|
|
|
* Used to write internal reference hyperlinks such as "Sheet1!A1". |
987
|
|
|
* |
988
|
|
|
* @param int $row1 Start row |
989
|
|
|
* @param int $col1 Start column |
990
|
|
|
* @param int $row2 End row |
991
|
|
|
* @param int $col2 End column |
992
|
|
|
* @param string $url URL string |
993
|
|
|
* |
994
|
|
|
* @see writeUrl() |
995
|
|
|
*/ |
996
|
7 |
|
private function writeUrlInternal(int $row1, int $col1, int $row2, int $col2, $url): void |
997
|
|
|
{ |
998
|
7 |
|
$record = 0x01B8; // Record identifier |
999
|
|
|
|
1000
|
|
|
// Strip URL type |
1001
|
7 |
|
$url = (string) preg_replace('/^internal:/', '', $url); |
1002
|
|
|
|
1003
|
|
|
// Pack the undocumented parts of the hyperlink stream |
1004
|
7 |
|
$unknown1 = pack('H*', 'D0C9EA79F9BACE118C8200AA004BA90B02000000'); |
1005
|
|
|
|
1006
|
|
|
// Pack the option flags |
1007
|
7 |
|
$options = pack('V', 0x08); |
1008
|
|
|
|
1009
|
|
|
// Convert the URL type and to a null terminated wchar string |
1010
|
7 |
|
$url .= "\0"; |
1011
|
|
|
|
1012
|
|
|
// character count |
1013
|
7 |
|
$url_len = StringHelper::countCharacters($url); |
1014
|
7 |
|
$url_len = pack('V', $url_len); |
1015
|
|
|
|
1016
|
7 |
|
$url = StringHelper::convertEncoding($url, 'UTF-16LE', 'UTF-8'); |
1017
|
|
|
|
1018
|
|
|
// Calculate the data length |
1019
|
7 |
|
$length = 0x24 + strlen($url); |
1020
|
|
|
|
1021
|
|
|
// Pack the header data |
1022
|
7 |
|
$header = pack('vv', $record, $length); |
1023
|
7 |
|
$data = pack('vvvv', $row1, $row2, $col1, $col2); |
1024
|
|
|
|
1025
|
|
|
// Write the packed data |
1026
|
7 |
|
$this->append($header . $data . $unknown1 . $options . $url_len . $url); |
1027
|
|
|
} |
1028
|
|
|
|
1029
|
|
|
/** |
1030
|
|
|
* Write links to external directory names such as 'c:\foo.xls', |
1031
|
|
|
* c:\foo.xls#Sheet1!A1', '../../foo.xls'. and '../../foo.xls#Sheet1!A1'. |
1032
|
|
|
* |
1033
|
|
|
* Note: Excel writes some relative links with the $dir_long string. We ignore |
1034
|
|
|
* these cases for the sake of simpler code. |
1035
|
|
|
* |
1036
|
|
|
* @param int $row1 Start row |
1037
|
|
|
* @param int $col1 Start column |
1038
|
|
|
* @param int $row2 End row |
1039
|
|
|
* @param int $col2 End column |
1040
|
|
|
* @param string $url URL string |
1041
|
|
|
* |
1042
|
|
|
* @see writeUrl() |
1043
|
|
|
*/ |
1044
|
1 |
|
private function writeUrlExternal(int $row1, int $col1, int $row2, int $col2, $url): void |
1045
|
|
|
{ |
1046
|
|
|
// Network drives are different. We will handle them separately |
1047
|
|
|
// MS/Novell network drives and shares start with \\ |
1048
|
1 |
|
if (preg_match('[^external:\\\\]', $url)) { |
1049
|
|
|
return; |
1050
|
|
|
} |
1051
|
|
|
|
1052
|
1 |
|
$record = 0x01B8; // Record identifier |
1053
|
|
|
|
1054
|
|
|
// Strip URL type and change Unix dir separator to Dos style (if needed) |
1055
|
|
|
// |
1056
|
1 |
|
$url = (string) preg_replace(['/^external:/', '/\//'], ['', '\\'], $url); |
1057
|
|
|
|
1058
|
|
|
// Determine if the link is relative or absolute: |
1059
|
|
|
// relative if link contains no dir separator, "somefile.xls" |
1060
|
|
|
// relative if link starts with up-dir, "..\..\somefile.xls" |
1061
|
|
|
// otherwise, absolute |
1062
|
|
|
|
1063
|
1 |
|
$absolute = 0x00; // relative path |
1064
|
1 |
|
if (preg_match('/^[A-Z]:/', $url)) { |
1065
|
|
|
$absolute = 0x02; // absolute path on Windows, e.g. C:\... |
1066
|
|
|
} |
1067
|
1 |
|
$link_type = 0x01 | $absolute; |
1068
|
|
|
|
1069
|
|
|
// Determine if the link contains a sheet reference and change some of the |
1070
|
|
|
// parameters accordingly. |
1071
|
|
|
// Split the dir name and sheet name (if it exists) |
1072
|
1 |
|
$dir_long = $url; |
1073
|
1 |
|
if (preg_match('/\\#/', $url)) { |
1074
|
|
|
$link_type |= 0x08; |
1075
|
|
|
} |
1076
|
|
|
|
1077
|
|
|
// Pack the link type |
1078
|
1 |
|
$link_type = pack('V', $link_type); |
1079
|
|
|
|
1080
|
|
|
// Calculate the up-level dir count e.g.. (..\..\..\ == 3) |
1081
|
1 |
|
$up_count = preg_match_all('/\\.\\.\\\\/', $dir_long, $useless); |
1082
|
1 |
|
$up_count = pack('v', $up_count); |
1083
|
|
|
|
1084
|
|
|
// Store the short dos dir name (null terminated) |
1085
|
1 |
|
$dir_short = (string) preg_replace('/\\.\\.\\\\/', '', $dir_long) . "\0"; |
1086
|
|
|
|
1087
|
|
|
// Store the long dir name as a wchar string (non-null terminated) |
1088
|
|
|
//$dir_long = $dir_long . "\0"; |
1089
|
|
|
|
1090
|
|
|
// Pack the lengths of the dir strings |
1091
|
1 |
|
$dir_short_len = pack('V', strlen($dir_short)); |
1092
|
|
|
//$dir_long_len = pack('V', strlen($dir_long)); |
1093
|
1 |
|
$stream_len = pack('V', 0); //strlen($dir_long) + 0x06); |
1094
|
|
|
|
1095
|
|
|
// Pack the undocumented parts of the hyperlink stream |
1096
|
1 |
|
$unknown1 = pack('H*', 'D0C9EA79F9BACE118C8200AA004BA90B02000000'); |
1097
|
1 |
|
$unknown2 = pack('H*', '0303000000000000C000000000000046'); |
1098
|
1 |
|
$unknown3 = pack('H*', 'FFFFADDE000000000000000000000000000000000000000'); |
1099
|
|
|
//$unknown4 = pack('v', 0x03); |
1100
|
|
|
|
1101
|
|
|
// Pack the main data stream |
1102
|
1 |
|
$data = pack('vvvv', $row1, $row2, $col1, $col2) . |
1103
|
1 |
|
$unknown1 . |
1104
|
1 |
|
$link_type . |
1105
|
1 |
|
$unknown2 . |
1106
|
1 |
|
$up_count . |
1107
|
1 |
|
$dir_short_len . |
1108
|
1 |
|
$dir_short . |
1109
|
1 |
|
$unknown3 . |
1110
|
1 |
|
$stream_len; /*. |
1111
|
|
|
$dir_long_len . |
1112
|
|
|
$unknown4 . |
1113
|
|
|
$dir_long . |
1114
|
|
|
$sheet_len . |
1115
|
|
|
$sheet ;*/ |
1116
|
|
|
|
1117
|
|
|
// Pack the header data |
1118
|
1 |
|
$length = strlen($data); |
1119
|
1 |
|
$header = pack('vv', $record, $length); |
1120
|
|
|
|
1121
|
|
|
// Write the packed data |
1122
|
1 |
|
$this->append($header . $data); |
1123
|
|
|
} |
1124
|
|
|
|
1125
|
|
|
/** |
1126
|
|
|
* This method is used to set the height and format for a row. |
1127
|
|
|
* |
1128
|
|
|
* @param int $row The row to set |
1129
|
|
|
* @param int $height Height we are giving to the row. |
1130
|
|
|
* Use null to set XF without setting height |
1131
|
|
|
* @param int $xfIndex The optional cell style Xf index to apply to the columns |
1132
|
|
|
* @param bool $hidden The optional hidden attribute |
1133
|
|
|
* @param int $level The optional outline level for row, in range [0,7] |
1134
|
|
|
*/ |
1135
|
25 |
|
private function writeRow(int $row, int $height, int $xfIndex, bool $hidden = false, $level = 0): void |
1136
|
|
|
{ |
1137
|
25 |
|
$record = 0x0208; // Record identifier |
1138
|
25 |
|
$length = 0x0010; // Number of bytes to follow |
1139
|
|
|
|
1140
|
25 |
|
$colMic = 0x0000; // First defined column |
1141
|
25 |
|
$colMac = 0x0000; // Last defined column |
1142
|
25 |
|
$irwMac = 0x0000; // Used by Excel to optimise loading |
1143
|
25 |
|
$reserved = 0x0000; // Reserved |
1144
|
25 |
|
$grbit = 0x0000; // Option flags |
1145
|
25 |
|
$ixfe = $xfIndex; |
1146
|
|
|
|
1147
|
25 |
|
if ($height < 0) { |
1148
|
8 |
|
$height = null; |
1149
|
|
|
} |
1150
|
|
|
|
1151
|
|
|
// Use writeRow($row, null, $XF) to set XF format without setting height |
1152
|
25 |
|
if ($height !== null) { |
1153
|
17 |
|
$miyRw = $height * 20; // row height |
1154
|
|
|
} else { |
1155
|
8 |
|
$miyRw = 0xff; // default row height is 256 |
1156
|
|
|
} |
1157
|
|
|
|
1158
|
|
|
// Set the options flags. fUnsynced is used to show that the font and row |
1159
|
|
|
// heights are not compatible. This is usually the case for WriteExcel. |
1160
|
|
|
// The collapsed flag 0x10 doesn't seem to be used to indicate that a row |
1161
|
|
|
// is collapsed. Instead it is used to indicate that the previous row is |
1162
|
|
|
// collapsed. The zero height flag, 0x20, is used to collapse a row. |
1163
|
|
|
|
1164
|
25 |
|
$grbit |= $level; |
1165
|
25 |
|
if ($hidden === true) { |
1166
|
7 |
|
$grbit |= 0x0030; |
1167
|
|
|
} |
1168
|
25 |
|
if ($height !== null) { |
1169
|
17 |
|
$grbit |= 0x0040; // fUnsynced |
1170
|
|
|
} |
1171
|
25 |
|
if ($xfIndex !== 0xF) { |
1172
|
3 |
|
$grbit |= 0x0080; |
1173
|
|
|
} |
1174
|
25 |
|
$grbit |= 0x0100; |
1175
|
|
|
|
1176
|
25 |
|
$header = pack('vv', $record, $length); |
1177
|
25 |
|
$data = pack('vvvvvvvv', $row, $colMic, $colMac, $miyRw, $irwMac, $reserved, $grbit, $ixfe); |
1178
|
25 |
|
$this->append($header . $data); |
1179
|
|
|
} |
1180
|
|
|
|
1181
|
|
|
/** |
1182
|
|
|
* Writes Excel DIMENSIONS to define the area in which there is data. |
1183
|
|
|
*/ |
1184
|
96 |
|
private function writeDimensions(): void |
1185
|
|
|
{ |
1186
|
96 |
|
$record = 0x0200; // Record identifier |
1187
|
|
|
|
1188
|
96 |
|
$length = 0x000E; |
1189
|
96 |
|
$data = pack('VVvvv', $this->firstRowIndex, $this->lastRowIndex + 1, $this->firstColumnIndex, $this->lastColumnIndex + 1, 0x0000); // reserved |
1190
|
|
|
|
1191
|
96 |
|
$header = pack('vv', $record, $length); |
1192
|
96 |
|
$this->append($header . $data); |
1193
|
|
|
} |
1194
|
|
|
|
1195
|
|
|
/** |
1196
|
|
|
* Write BIFF record Window2. |
1197
|
|
|
*/ |
1198
|
95 |
|
private function writeWindow2(): void |
1199
|
|
|
{ |
1200
|
95 |
|
$record = 0x023E; // Record identifier |
1201
|
95 |
|
$length = 0x0012; |
1202
|
|
|
|
1203
|
95 |
|
$rwTop = 0x0000; // Top row visible in window |
1204
|
95 |
|
$colLeft = 0x0000; // Leftmost column visible in window |
1205
|
|
|
|
1206
|
|
|
// The options flags that comprise $grbit |
1207
|
95 |
|
$fDspFmla = 0; // 0 - bit |
1208
|
95 |
|
$fDspGrid = $this->phpSheet->getShowGridlines() ? 1 : 0; // 1 |
1209
|
95 |
|
$fDspRwCol = $this->phpSheet->getShowRowColHeaders() ? 1 : 0; // 2 |
1210
|
95 |
|
$fFrozen = $this->phpSheet->getFreezePane() ? 1 : 0; // 3 |
1211
|
95 |
|
$fDspZeros = 1; // 4 |
1212
|
95 |
|
$fDefaultHdr = 1; // 5 |
1213
|
95 |
|
$fArabic = $this->phpSheet->getRightToLeft() ? 1 : 0; // 6 |
1214
|
95 |
|
$fDspGuts = $this->outlineOn; // 7 |
1215
|
95 |
|
$fFrozenNoSplit = 0; // 0 - bit |
1216
|
|
|
// no support in PhpSpreadsheet for selected sheet, therefore sheet is only selected if it is the active sheet |
1217
|
95 |
|
$fSelected = ($this->phpSheet === $this->phpSheet->getParentOrThrow()->getActiveSheet()) ? 1 : 0; |
1218
|
95 |
|
$fPageBreakPreview = $this->phpSheet->getSheetView()->getView() === SheetView::SHEETVIEW_PAGE_BREAK_PREVIEW; |
1219
|
|
|
|
1220
|
95 |
|
$grbit = $fDspFmla; |
1221
|
95 |
|
$grbit |= $fDspGrid << 1; |
1222
|
95 |
|
$grbit |= $fDspRwCol << 2; |
1223
|
95 |
|
$grbit |= $fFrozen << 3; |
1224
|
95 |
|
$grbit |= $fDspZeros << 4; |
1225
|
95 |
|
$grbit |= $fDefaultHdr << 5; |
1226
|
95 |
|
$grbit |= $fArabic << 6; |
1227
|
95 |
|
$grbit |= $fDspGuts << 7; |
1228
|
95 |
|
$grbit |= $fFrozenNoSplit << 8; |
1229
|
95 |
|
$grbit |= $fSelected << 9; // Selected sheets. |
1230
|
95 |
|
$grbit |= $fSelected << 10; // Active sheet. |
1231
|
95 |
|
$grbit |= $fPageBreakPreview << 11; |
1232
|
|
|
|
1233
|
95 |
|
$header = pack('vv', $record, $length); |
1234
|
95 |
|
$data = pack('vvv', $grbit, $rwTop, $colLeft); |
1235
|
|
|
|
1236
|
|
|
// FIXME !!! |
1237
|
95 |
|
$rgbHdr = 0x0040; // Row/column heading and gridline color index |
1238
|
95 |
|
$zoom_factor_page_break = ($fPageBreakPreview ? $this->phpSheet->getSheetView()->getZoomScale() : 0x0000); |
1239
|
95 |
|
$zoom_factor_normal = $this->phpSheet->getSheetView()->getZoomScaleNormal(); |
1240
|
|
|
|
1241
|
95 |
|
$data .= pack('vvvvV', $rgbHdr, 0x0000, $zoom_factor_page_break, $zoom_factor_normal, 0x00000000); |
1242
|
|
|
|
1243
|
95 |
|
$this->append($header . $data); |
1244
|
|
|
} |
1245
|
|
|
|
1246
|
|
|
/** |
1247
|
|
|
* Write BIFF record DEFAULTROWHEIGHT. |
1248
|
|
|
*/ |
1249
|
96 |
|
private function writeDefaultRowHeight(): void |
1250
|
|
|
{ |
1251
|
96 |
|
$defaultRowHeight = $this->phpSheet->getDefaultRowDimension()->getRowHeight(); |
1252
|
|
|
|
1253
|
96 |
|
if ($defaultRowHeight < 0) { |
1254
|
84 |
|
return; |
1255
|
|
|
} |
1256
|
|
|
|
1257
|
|
|
// convert to twips |
1258
|
12 |
|
$defaultRowHeight = (int) 20 * $defaultRowHeight; |
1259
|
|
|
|
1260
|
12 |
|
$record = 0x0225; // Record identifier |
1261
|
12 |
|
$length = 0x0004; // Number of bytes to follow |
1262
|
|
|
|
1263
|
12 |
|
$header = pack('vv', $record, $length); |
1264
|
12 |
|
$data = pack('vv', 1, $defaultRowHeight); |
1265
|
12 |
|
$this->append($header . $data); |
1266
|
|
|
} |
1267
|
|
|
|
1268
|
|
|
/** |
1269
|
|
|
* Write BIFF record DEFCOLWIDTH if COLINFO records are in use. |
1270
|
|
|
*/ |
1271
|
96 |
|
private function writeDefcol(): void |
1272
|
|
|
{ |
1273
|
96 |
|
$defaultColWidth = 8; |
1274
|
|
|
|
1275
|
96 |
|
$record = 0x0055; // Record identifier |
1276
|
96 |
|
$length = 0x0002; // Number of bytes to follow |
1277
|
|
|
|
1278
|
96 |
|
$header = pack('vv', $record, $length); |
1279
|
96 |
|
$data = pack('v', $defaultColWidth); |
1280
|
96 |
|
$this->append($header . $data); |
1281
|
|
|
} |
1282
|
|
|
|
1283
|
|
|
/** |
1284
|
|
|
* Write BIFF record COLINFO to define column widths. |
1285
|
|
|
* |
1286
|
|
|
* Note: The SDK says the record length is 0x0B but Excel writes a 0x0C |
1287
|
|
|
* length record. |
1288
|
|
|
* |
1289
|
|
|
* @param array $col_array This is the only parameter received and is composed of the following: |
1290
|
|
|
* 0 => First formatted column, |
1291
|
|
|
* 1 => Last formatted column, |
1292
|
|
|
* 2 => Col width (8.43 is Excel default), |
1293
|
|
|
* 3 => The optional XF format of the column, |
1294
|
|
|
* 4 => Option flags. |
1295
|
|
|
* 5 => Optional outline level |
1296
|
|
|
*/ |
1297
|
96 |
|
private function writeColinfo($col_array): void |
1298
|
|
|
{ |
1299
|
96 |
|
$colFirst = $col_array[0] ?? null; |
1300
|
96 |
|
$colLast = $col_array[1] ?? null; |
1301
|
96 |
|
$coldx = $col_array[2] ?? 8.43; |
1302
|
96 |
|
$xfIndex = $col_array[3] ?? 15; |
1303
|
96 |
|
$grbit = $col_array[4] ?? 0; |
1304
|
96 |
|
$level = $col_array[5] ?? 0; |
1305
|
|
|
|
1306
|
96 |
|
$record = 0x007D; // Record identifier |
1307
|
96 |
|
$length = 0x000C; // Number of bytes to follow |
1308
|
|
|
|
1309
|
96 |
|
$coldx *= 256; // Convert to units of 1/256 of a char |
1310
|
|
|
|
1311
|
96 |
|
$ixfe = $xfIndex; |
1312
|
96 |
|
$reserved = 0x0000; // Reserved |
1313
|
|
|
|
1314
|
96 |
|
$level = max(0, min($level, 7)); |
1315
|
96 |
|
$grbit |= $level << 8; |
1316
|
|
|
|
1317
|
96 |
|
$header = pack('vv', $record, $length); |
1318
|
96 |
|
$data = pack('vvvvvv', $colFirst, $colLast, $coldx, $ixfe, $grbit, $reserved); |
1319
|
96 |
|
$this->append($header . $data); |
1320
|
|
|
} |
1321
|
|
|
|
1322
|
|
|
/** |
1323
|
|
|
* Write BIFF record SELECTION. |
1324
|
|
|
*/ |
1325
|
95 |
|
private function writeSelection(): void |
1326
|
|
|
{ |
1327
|
|
|
// look up the selected cell range |
1328
|
95 |
|
$selectedCells = Coordinate::splitRange($this->phpSheet->getSelectedCells()); |
1329
|
95 |
|
$selectedCells = $selectedCells[0]; |
1330
|
95 |
|
if (count($selectedCells) == 2) { |
1331
|
18 |
|
[$first, $last] = $selectedCells; |
1332
|
|
|
} else { |
1333
|
86 |
|
$first = $selectedCells[0]; |
1334
|
86 |
|
$last = $selectedCells[0]; |
1335
|
|
|
} |
1336
|
|
|
|
1337
|
95 |
|
[$colFirst, $rwFirst] = Coordinate::coordinateFromString($first); |
1338
|
95 |
|
$colFirst = Coordinate::columnIndexFromString($colFirst) - 1; // base 0 column index |
1339
|
95 |
|
--$rwFirst; // base 0 row index |
1340
|
|
|
|
1341
|
95 |
|
[$colLast, $rwLast] = Coordinate::coordinateFromString($last); |
1342
|
95 |
|
$colLast = Coordinate::columnIndexFromString($colLast) - 1; // base 0 column index |
1343
|
95 |
|
--$rwLast; // base 0 row index |
1344
|
|
|
|
1345
|
|
|
// make sure we are not out of bounds |
1346
|
95 |
|
$colFirst = min($colFirst, 255); |
1347
|
95 |
|
$colLast = min($colLast, 255); |
1348
|
|
|
|
1349
|
95 |
|
$rwFirst = min($rwFirst, 65535); |
1350
|
95 |
|
$rwLast = min($rwLast, 65535); |
1351
|
|
|
|
1352
|
95 |
|
$record = 0x001D; // Record identifier |
1353
|
95 |
|
$length = 0x000F; // Number of bytes to follow |
1354
|
|
|
|
1355
|
95 |
|
$pnn = $this->activePane; // Pane position |
1356
|
95 |
|
$rwAct = $rwFirst; // Active row |
1357
|
95 |
|
$colAct = $colFirst; // Active column |
1358
|
95 |
|
$irefAct = 0; // Active cell ref |
1359
|
95 |
|
$cref = 1; // Number of refs |
1360
|
|
|
|
1361
|
|
|
// Swap last row/col for first row/col as necessary |
1362
|
95 |
|
if ($rwFirst > $rwLast) { |
1363
|
|
|
[$rwFirst, $rwLast] = [$rwLast, $rwFirst]; |
1364
|
|
|
} |
1365
|
|
|
|
1366
|
95 |
|
if ($colFirst > $colLast) { |
1367
|
|
|
[$colFirst, $colLast] = [$colLast, $colFirst]; |
1368
|
|
|
} |
1369
|
|
|
|
1370
|
95 |
|
$header = pack('vv', $record, $length); |
1371
|
95 |
|
$data = pack('CvvvvvvCC', $pnn, $rwAct, $colAct, $irefAct, $cref, $rwFirst, $rwLast, $colFirst, $colLast); |
1372
|
95 |
|
$this->append($header . $data); |
1373
|
|
|
} |
1374
|
|
|
|
1375
|
|
|
/** |
1376
|
|
|
* Store the MERGEDCELLS records for all ranges of merged cells. |
1377
|
|
|
*/ |
1378
|
95 |
|
private function writeMergedCells(): void |
1379
|
|
|
{ |
1380
|
95 |
|
$mergeCells = $this->phpSheet->getMergeCells(); |
1381
|
95 |
|
$countMergeCells = count($mergeCells); |
1382
|
|
|
|
1383
|
95 |
|
if ($countMergeCells == 0) { |
1384
|
92 |
|
return; |
1385
|
|
|
} |
1386
|
|
|
|
1387
|
|
|
// maximum allowed number of merged cells per record |
1388
|
13 |
|
$maxCountMergeCellsPerRecord = 1027; |
1389
|
|
|
|
1390
|
|
|
// record identifier |
1391
|
13 |
|
$record = 0x00E5; |
1392
|
|
|
|
1393
|
|
|
// counter for total number of merged cells treated so far by the writer |
1394
|
13 |
|
$i = 0; |
1395
|
|
|
|
1396
|
|
|
// counter for number of merged cells written in record currently being written |
1397
|
13 |
|
$j = 0; |
1398
|
|
|
|
1399
|
|
|
// initialize record data |
1400
|
13 |
|
$recordData = ''; |
1401
|
|
|
|
1402
|
|
|
// loop through the merged cells |
1403
|
13 |
|
foreach ($mergeCells as $mergeCell) { |
1404
|
13 |
|
++$i; |
1405
|
13 |
|
++$j; |
1406
|
|
|
|
1407
|
|
|
// extract the row and column indexes |
1408
|
13 |
|
$range = Coordinate::splitRange($mergeCell); |
1409
|
13 |
|
[$first, $last] = $range[0]; |
1410
|
13 |
|
[$firstColumn, $firstRow] = Coordinate::indexesFromString($first); |
1411
|
13 |
|
[$lastColumn, $lastRow] = Coordinate::indexesFromString($last); |
1412
|
|
|
|
1413
|
13 |
|
$recordData .= pack('vvvv', $firstRow - 1, $lastRow - 1, $firstColumn - 1, $lastColumn - 1); |
1414
|
|
|
|
1415
|
|
|
// flush record if we have reached limit for number of merged cells, or reached final merged cell |
1416
|
13 |
|
if ($j == $maxCountMergeCellsPerRecord || $i == $countMergeCells) { |
1417
|
13 |
|
$recordData = pack('v', $j) . $recordData; |
1418
|
13 |
|
$length = strlen($recordData); |
1419
|
13 |
|
$header = pack('vv', $record, $length); |
1420
|
13 |
|
$this->append($header . $recordData); |
1421
|
|
|
|
1422
|
|
|
// initialize for next record, if any |
1423
|
13 |
|
$recordData = ''; |
1424
|
13 |
|
$j = 0; |
1425
|
|
|
} |
1426
|
|
|
} |
1427
|
|
|
} |
1428
|
|
|
|
1429
|
|
|
/** |
1430
|
|
|
* Write SHEETLAYOUT record. |
1431
|
|
|
*/ |
1432
|
95 |
|
private function writeSheetLayout(): void |
1433
|
|
|
{ |
1434
|
95 |
|
if (!$this->phpSheet->isTabColorSet()) { |
1435
|
95 |
|
return; |
1436
|
|
|
} |
1437
|
|
|
|
1438
|
6 |
|
$recordData = pack( |
1439
|
6 |
|
'vvVVVvv', |
1440
|
6 |
|
0x0862, |
1441
|
6 |
|
0x0000, // unused |
1442
|
6 |
|
0x00000000, // unused |
1443
|
6 |
|
0x00000000, // unused |
1444
|
6 |
|
0x00000014, // size of record data |
1445
|
6 |
|
$this->colors[$this->phpSheet->getTabColor()->getRGB()], // color index |
1446
|
6 |
|
0x0000 // unused |
1447
|
6 |
|
); |
1448
|
|
|
|
1449
|
6 |
|
$length = strlen($recordData); |
1450
|
|
|
|
1451
|
6 |
|
$record = 0x0862; // Record identifier |
1452
|
6 |
|
$header = pack('vv', $record, $length); |
1453
|
6 |
|
$this->append($header . $recordData); |
1454
|
|
|
} |
1455
|
|
|
|
1456
|
95 |
|
private static function protectionBitsDefaultFalse(?bool $value, int $shift): int |
1457
|
|
|
{ |
1458
|
95 |
|
if ($value === false) { |
1459
|
4 |
|
return 1 << $shift; |
1460
|
|
|
} |
1461
|
|
|
|
1462
|
93 |
|
return 0; |
1463
|
|
|
} |
1464
|
|
|
|
1465
|
95 |
|
private static function protectionBitsDefaultTrue(?bool $value, int $shift): int |
1466
|
|
|
{ |
1467
|
95 |
|
if ($value !== false) { |
1468
|
95 |
|
return 1 << $shift; |
1469
|
|
|
} |
1470
|
|
|
|
1471
|
2 |
|
return 0; |
1472
|
|
|
} |
1473
|
|
|
|
1474
|
|
|
/** |
1475
|
|
|
* Write SHEETPROTECTION. |
1476
|
|
|
*/ |
1477
|
95 |
|
private function writeSheetProtection(): void |
1478
|
|
|
{ |
1479
|
|
|
// record identifier |
1480
|
95 |
|
$record = 0x0867; |
1481
|
|
|
|
1482
|
|
|
// prepare options |
1483
|
95 |
|
$protection = $this->phpSheet->getProtection(); |
1484
|
95 |
|
$options = self::protectionBitsDefaultTrue($protection->getObjects(), 0) |
1485
|
95 |
|
| self::protectionBitsDefaultTrue($protection->getScenarios(), 1) |
1486
|
95 |
|
| self::protectionBitsDefaultFalse($protection->getFormatCells(), 2) |
1487
|
95 |
|
| self::protectionBitsDefaultFalse($protection->getFormatColumns(), 3) |
1488
|
95 |
|
| self::protectionBitsDefaultFalse($protection->getFormatRows(), 4) |
1489
|
95 |
|
| self::protectionBitsDefaultFalse($protection->getInsertColumns(), 5) |
1490
|
95 |
|
| self::protectionBitsDefaultFalse($protection->getInsertRows(), 6) |
1491
|
95 |
|
| self::protectionBitsDefaultFalse($protection->getInsertHyperlinks(), 7) |
1492
|
95 |
|
| self::protectionBitsDefaultFalse($protection->getDeleteColumns(), 8) |
1493
|
95 |
|
| self::protectionBitsDefaultFalse($protection->getDeleteRows(), 9) |
1494
|
95 |
|
| self::protectionBitsDefaultTrue($protection->getSelectLockedCells(), 10) |
1495
|
95 |
|
| self::protectionBitsDefaultFalse($protection->getSort(), 11) |
1496
|
95 |
|
| self::protectionBitsDefaultFalse($protection->getAutoFilter(), 12) |
1497
|
95 |
|
| self::protectionBitsDefaultFalse($protection->getPivotTables(), 13) |
1498
|
95 |
|
| self::protectionBitsDefaultTrue($protection->getSelectUnlockedCells(), 14); |
1499
|
|
|
|
1500
|
|
|
// record data |
1501
|
95 |
|
$recordData = pack( |
1502
|
95 |
|
'vVVCVVvv', |
1503
|
95 |
|
0x0867, // repeated record identifier |
1504
|
95 |
|
0x0000, // not used |
1505
|
95 |
|
0x0000, // not used |
1506
|
95 |
|
0x00, // not used |
1507
|
95 |
|
0x01000200, // unknown data |
1508
|
95 |
|
0xFFFFFFFF, // unknown data |
1509
|
95 |
|
$options, // options |
1510
|
95 |
|
0x0000 // not used |
1511
|
95 |
|
); |
1512
|
|
|
|
1513
|
95 |
|
$length = strlen($recordData); |
1514
|
95 |
|
$header = pack('vv', $record, $length); |
1515
|
|
|
|
1516
|
95 |
|
$this->append($header . $recordData); |
1517
|
|
|
} |
1518
|
|
|
|
1519
|
|
|
/** |
1520
|
|
|
* Write BIFF record RANGEPROTECTION. |
1521
|
|
|
* |
1522
|
|
|
* Openoffice.org's Documentation of the Microsoft Excel File Format uses term RANGEPROTECTION for these records |
1523
|
|
|
* Microsoft Office Excel 97-2007 Binary File Format Specification uses term FEAT for these records |
1524
|
|
|
*/ |
1525
|
95 |
|
private function writeRangeProtection(): void |
1526
|
|
|
{ |
1527
|
95 |
|
foreach ($this->phpSheet->getProtectedCells() as $range => $password) { |
1528
|
|
|
// number of ranges, e.g. 'A1:B3 C20:D25' |
1529
|
6 |
|
$cellRanges = explode(' ', $range); |
1530
|
6 |
|
$cref = count($cellRanges); |
1531
|
|
|
|
1532
|
6 |
|
$recordData = pack( |
1533
|
6 |
|
'vvVVvCVvVv', |
1534
|
6 |
|
0x0868, |
1535
|
6 |
|
0x00, |
1536
|
6 |
|
0x0000, |
1537
|
6 |
|
0x0000, |
1538
|
6 |
|
0x02, |
1539
|
6 |
|
0x0, |
1540
|
6 |
|
0x0000, |
1541
|
6 |
|
$cref, |
1542
|
6 |
|
0x0000, |
1543
|
6 |
|
0x00 |
1544
|
6 |
|
); |
1545
|
|
|
|
1546
|
6 |
|
foreach ($cellRanges as $cellRange) { |
1547
|
6 |
|
$recordData .= $this->writeBIFF8CellRangeAddressFixed($cellRange); |
1548
|
|
|
} |
1549
|
|
|
|
1550
|
|
|
// the rgbFeat structure |
1551
|
6 |
|
$recordData .= pack( |
1552
|
6 |
|
'VV', |
1553
|
6 |
|
0x0000, |
1554
|
6 |
|
hexdec($password) |
1555
|
6 |
|
); |
1556
|
|
|
|
1557
|
6 |
|
$recordData .= StringHelper::UTF8toBIFF8UnicodeLong('p' . md5($recordData)); |
1558
|
|
|
|
1559
|
6 |
|
$length = strlen($recordData); |
1560
|
|
|
|
1561
|
6 |
|
$record = 0x0868; // Record identifier |
1562
|
6 |
|
$header = pack('vv', $record, $length); |
1563
|
6 |
|
$this->append($header . $recordData); |
1564
|
|
|
} |
1565
|
|
|
} |
1566
|
|
|
|
1567
|
|
|
/** |
1568
|
|
|
* Writes the Excel BIFF PANE record. |
1569
|
|
|
* The panes can either be frozen or thawed (unfrozen). |
1570
|
|
|
* Frozen panes are specified in terms of an integer number of rows and columns. |
1571
|
|
|
* Thawed panes are specified in terms of Excel's units for rows and columns. |
1572
|
|
|
*/ |
1573
|
8 |
|
private function writePanes(): void |
1574
|
|
|
{ |
1575
|
8 |
|
if (!$this->phpSheet->getFreezePane()) { |
1576
|
|
|
// thaw panes |
1577
|
|
|
return; |
1578
|
|
|
} |
1579
|
|
|
|
1580
|
8 |
|
[$column, $row] = Coordinate::indexesFromString($this->phpSheet->getFreezePane()); |
1581
|
8 |
|
$x = $column - 1; |
1582
|
8 |
|
$y = $row - 1; |
1583
|
|
|
|
1584
|
8 |
|
[$leftMostColumn, $topRow] = Coordinate::indexesFromString($this->phpSheet->getTopLeftCell() ?? ''); |
1585
|
|
|
//Coordinates are zero-based in xls files |
1586
|
8 |
|
$rwTop = $topRow - 1; |
1587
|
8 |
|
$colLeft = $leftMostColumn - 1; |
1588
|
|
|
|
1589
|
8 |
|
$record = 0x0041; // Record identifier |
1590
|
8 |
|
$length = 0x000A; // Number of bytes to follow |
1591
|
|
|
|
1592
|
|
|
// Determine which pane should be active. There is also the undocumented |
1593
|
|
|
// option to override this should it be necessary: may be removed later. |
1594
|
8 |
|
$pnnAct = 0; |
1595
|
8 |
|
if ($x != 0 && $y != 0) { |
1596
|
3 |
|
$pnnAct = 0; // Bottom right |
1597
|
|
|
} |
1598
|
8 |
|
if ($x != 0 && $y == 0) { |
1599
|
|
|
$pnnAct = 1; // Top right |
1600
|
|
|
} |
1601
|
8 |
|
if ($x == 0 && $y != 0) { |
1602
|
5 |
|
$pnnAct = 2; // Bottom left |
1603
|
|
|
} |
1604
|
8 |
|
if ($x == 0 && $y == 0) { |
1605
|
|
|
$pnnAct = 3; // Top left |
1606
|
|
|
} |
1607
|
|
|
|
1608
|
8 |
|
$this->activePane = $pnnAct; // Used in writeSelection |
1609
|
|
|
|
1610
|
8 |
|
$header = pack('vv', $record, $length); |
1611
|
8 |
|
$data = pack('vvvvv', $x, $y, $rwTop, $colLeft, $pnnAct); |
1612
|
8 |
|
$this->append($header . $data); |
1613
|
|
|
} |
1614
|
|
|
|
1615
|
|
|
/** |
1616
|
|
|
* Store the page setup SETUP BIFF record. |
1617
|
|
|
*/ |
1618
|
96 |
|
private function writeSetup(): void |
1619
|
|
|
{ |
1620
|
96 |
|
$record = 0x00A1; // Record identifier |
1621
|
96 |
|
$length = 0x0022; // Number of bytes to follow |
1622
|
|
|
|
1623
|
96 |
|
$iPaperSize = $this->phpSheet->getPageSetup()->getPaperSize(); // Paper size |
1624
|
96 |
|
$iScale = $this->phpSheet->getPageSetup()->getScale() ?: 100; // Print scaling factor |
1625
|
|
|
|
1626
|
96 |
|
$iPageStart = 0x01; // Starting page number |
1627
|
96 |
|
$iFitWidth = (int) $this->phpSheet->getPageSetup()->getFitToWidth(); // Fit to number of pages wide |
1628
|
96 |
|
$iFitHeight = (int) $this->phpSheet->getPageSetup()->getFitToHeight(); // Fit to number of pages high |
1629
|
96 |
|
$iRes = 0x0258; // Print resolution |
1630
|
96 |
|
$iVRes = 0x0258; // Vertical print resolution |
1631
|
|
|
|
1632
|
96 |
|
$numHdr = $this->phpSheet->getPageMargins()->getHeader(); // Header Margin |
1633
|
|
|
|
1634
|
96 |
|
$numFtr = $this->phpSheet->getPageMargins()->getFooter(); // Footer Margin |
1635
|
96 |
|
$iCopies = 0x01; // Number of copies |
1636
|
|
|
|
1637
|
|
|
// Order of printing pages |
1638
|
96 |
|
$fLeftToRight = $this->phpSheet->getPageSetup()->getPageOrder() === PageSetup::PAGEORDER_DOWN_THEN_OVER |
1639
|
96 |
|
? 0x0 : 0x1; |
1640
|
|
|
// Page orientation |
1641
|
96 |
|
$fLandscape = ($this->phpSheet->getPageSetup()->getOrientation() == PageSetup::ORIENTATION_LANDSCAPE) |
1642
|
96 |
|
? 0x0 : 0x1; |
1643
|
|
|
|
1644
|
96 |
|
$fNoPls = 0x0; // Setup not read from printer |
1645
|
96 |
|
$fNoColor = 0x0; // Print black and white |
1646
|
96 |
|
$fDraft = 0x0; // Print draft quality |
1647
|
96 |
|
$fNotes = 0x0; // Print notes |
1648
|
96 |
|
$fNoOrient = 0x0; // Orientation not set |
1649
|
96 |
|
$fUsePage = 0x0; // Use custom starting page |
1650
|
|
|
|
1651
|
96 |
|
$grbit = $fLeftToRight; |
1652
|
96 |
|
$grbit |= $fLandscape << 1; |
1653
|
96 |
|
$grbit |= $fNoPls << 2; |
1654
|
96 |
|
$grbit |= $fNoColor << 3; |
1655
|
96 |
|
$grbit |= $fDraft << 4; |
1656
|
96 |
|
$grbit |= $fNotes << 5; |
1657
|
96 |
|
$grbit |= $fNoOrient << 6; |
1658
|
96 |
|
$grbit |= $fUsePage << 7; |
1659
|
|
|
|
1660
|
96 |
|
$numHdr = pack('d', $numHdr); |
1661
|
96 |
|
$numFtr = pack('d', $numFtr); |
1662
|
96 |
|
if (self::getByteOrder()) { // if it's Big Endian |
1663
|
|
|
$numHdr = strrev($numHdr); |
1664
|
|
|
$numFtr = strrev($numFtr); |
1665
|
|
|
} |
1666
|
|
|
|
1667
|
96 |
|
$header = pack('vv', $record, $length); |
1668
|
96 |
|
$data1 = pack('vvvvvvvv', $iPaperSize, $iScale, $iPageStart, $iFitWidth, $iFitHeight, $grbit, $iRes, $iVRes); |
1669
|
96 |
|
$data2 = $numHdr . $numFtr; |
1670
|
96 |
|
$data3 = pack('v', $iCopies); |
1671
|
96 |
|
$this->append($header . $data1 . $data2 . $data3); |
1672
|
|
|
} |
1673
|
|
|
|
1674
|
|
|
/** |
1675
|
|
|
* Store the header caption BIFF record. |
1676
|
|
|
*/ |
1677
|
96 |
|
private function writeHeader(): void |
1678
|
|
|
{ |
1679
|
96 |
|
$record = 0x0014; // Record identifier |
1680
|
|
|
|
1681
|
|
|
/* removing for now |
1682
|
|
|
// need to fix character count (multibyte!) |
1683
|
|
|
if (strlen($this->phpSheet->getHeaderFooter()->getOddHeader()) <= 255) { |
1684
|
|
|
$str = $this->phpSheet->getHeaderFooter()->getOddHeader(); // header string |
1685
|
|
|
} else { |
1686
|
|
|
$str = ''; |
1687
|
|
|
} |
1688
|
|
|
*/ |
1689
|
|
|
|
1690
|
96 |
|
$recordData = StringHelper::UTF8toBIFF8UnicodeLong($this->phpSheet->getHeaderFooter()->getOddHeader()); |
1691
|
96 |
|
$length = strlen($recordData); |
1692
|
|
|
|
1693
|
96 |
|
$header = pack('vv', $record, $length); |
1694
|
|
|
|
1695
|
96 |
|
$this->append($header . $recordData); |
1696
|
|
|
} |
1697
|
|
|
|
1698
|
|
|
/** |
1699
|
|
|
* Store the footer caption BIFF record. |
1700
|
|
|
*/ |
1701
|
96 |
|
private function writeFooter(): void |
1702
|
|
|
{ |
1703
|
96 |
|
$record = 0x0015; // Record identifier |
1704
|
|
|
|
1705
|
|
|
/* removing for now |
1706
|
|
|
// need to fix character count (multibyte!) |
1707
|
|
|
if (strlen($this->phpSheet->getHeaderFooter()->getOddFooter()) <= 255) { |
1708
|
|
|
$str = $this->phpSheet->getHeaderFooter()->getOddFooter(); |
1709
|
|
|
} else { |
1710
|
|
|
$str = ''; |
1711
|
|
|
} |
1712
|
|
|
*/ |
1713
|
|
|
|
1714
|
96 |
|
$recordData = StringHelper::UTF8toBIFF8UnicodeLong($this->phpSheet->getHeaderFooter()->getOddFooter()); |
1715
|
96 |
|
$length = strlen($recordData); |
1716
|
|
|
|
1717
|
96 |
|
$header = pack('vv', $record, $length); |
1718
|
|
|
|
1719
|
96 |
|
$this->append($header . $recordData); |
1720
|
|
|
} |
1721
|
|
|
|
1722
|
|
|
/** |
1723
|
|
|
* Store the horizontal centering HCENTER BIFF record. |
1724
|
|
|
*/ |
1725
|
96 |
|
private function writeHcenter(): void |
1726
|
|
|
{ |
1727
|
96 |
|
$record = 0x0083; // Record identifier |
1728
|
96 |
|
$length = 0x0002; // Bytes to follow |
1729
|
|
|
|
1730
|
96 |
|
$fHCenter = $this->phpSheet->getPageSetup()->getHorizontalCentered() ? 1 : 0; // Horizontal centering |
1731
|
|
|
|
1732
|
96 |
|
$header = pack('vv', $record, $length); |
1733
|
96 |
|
$data = pack('v', $fHCenter); |
1734
|
|
|
|
1735
|
96 |
|
$this->append($header . $data); |
1736
|
|
|
} |
1737
|
|
|
|
1738
|
|
|
/** |
1739
|
|
|
* Store the vertical centering VCENTER BIFF record. |
1740
|
|
|
*/ |
1741
|
96 |
|
private function writeVcenter(): void |
1742
|
|
|
{ |
1743
|
96 |
|
$record = 0x0084; // Record identifier |
1744
|
96 |
|
$length = 0x0002; // Bytes to follow |
1745
|
|
|
|
1746
|
96 |
|
$fVCenter = $this->phpSheet->getPageSetup()->getVerticalCentered() ? 1 : 0; // Horizontal centering |
1747
|
|
|
|
1748
|
96 |
|
$header = pack('vv', $record, $length); |
1749
|
96 |
|
$data = pack('v', $fVCenter); |
1750
|
96 |
|
$this->append($header . $data); |
1751
|
|
|
} |
1752
|
|
|
|
1753
|
|
|
/** |
1754
|
|
|
* Store the LEFTMARGIN BIFF record. |
1755
|
|
|
*/ |
1756
|
96 |
|
private function writeMarginLeft(): void |
1757
|
|
|
{ |
1758
|
96 |
|
$record = 0x0026; // Record identifier |
1759
|
96 |
|
$length = 0x0008; // Bytes to follow |
1760
|
|
|
|
1761
|
96 |
|
$margin = $this->phpSheet->getPageMargins()->getLeft(); // Margin in inches |
1762
|
|
|
|
1763
|
96 |
|
$header = pack('vv', $record, $length); |
1764
|
96 |
|
$data = pack('d', $margin); |
1765
|
96 |
|
if (self::getByteOrder()) { // if it's Big Endian |
1766
|
|
|
$data = strrev($data); |
1767
|
|
|
} |
1768
|
|
|
|
1769
|
96 |
|
$this->append($header . $data); |
1770
|
|
|
} |
1771
|
|
|
|
1772
|
|
|
/** |
1773
|
|
|
* Store the RIGHTMARGIN BIFF record. |
1774
|
|
|
*/ |
1775
|
96 |
|
private function writeMarginRight(): void |
1776
|
|
|
{ |
1777
|
96 |
|
$record = 0x0027; // Record identifier |
1778
|
96 |
|
$length = 0x0008; // Bytes to follow |
1779
|
|
|
|
1780
|
96 |
|
$margin = $this->phpSheet->getPageMargins()->getRight(); // Margin in inches |
1781
|
|
|
|
1782
|
96 |
|
$header = pack('vv', $record, $length); |
1783
|
96 |
|
$data = pack('d', $margin); |
1784
|
96 |
|
if (self::getByteOrder()) { // if it's Big Endian |
1785
|
|
|
$data = strrev($data); |
1786
|
|
|
} |
1787
|
|
|
|
1788
|
96 |
|
$this->append($header . $data); |
1789
|
|
|
} |
1790
|
|
|
|
1791
|
|
|
/** |
1792
|
|
|
* Store the TOPMARGIN BIFF record. |
1793
|
|
|
*/ |
1794
|
96 |
|
private function writeMarginTop(): void |
1795
|
|
|
{ |
1796
|
96 |
|
$record = 0x0028; // Record identifier |
1797
|
96 |
|
$length = 0x0008; // Bytes to follow |
1798
|
|
|
|
1799
|
96 |
|
$margin = $this->phpSheet->getPageMargins()->getTop(); // Margin in inches |
1800
|
|
|
|
1801
|
96 |
|
$header = pack('vv', $record, $length); |
1802
|
96 |
|
$data = pack('d', $margin); |
1803
|
96 |
|
if (self::getByteOrder()) { // if it's Big Endian |
1804
|
|
|
$data = strrev($data); |
1805
|
|
|
} |
1806
|
|
|
|
1807
|
96 |
|
$this->append($header . $data); |
1808
|
|
|
} |
1809
|
|
|
|
1810
|
|
|
/** |
1811
|
|
|
* Store the BOTTOMMARGIN BIFF record. |
1812
|
|
|
*/ |
1813
|
96 |
|
private function writeMarginBottom(): void |
1814
|
|
|
{ |
1815
|
96 |
|
$record = 0x0029; // Record identifier |
1816
|
96 |
|
$length = 0x0008; // Bytes to follow |
1817
|
|
|
|
1818
|
96 |
|
$margin = $this->phpSheet->getPageMargins()->getBottom(); // Margin in inches |
1819
|
|
|
|
1820
|
96 |
|
$header = pack('vv', $record, $length); |
1821
|
96 |
|
$data = pack('d', $margin); |
1822
|
96 |
|
if (self::getByteOrder()) { // if it's Big Endian |
1823
|
|
|
$data = strrev($data); |
1824
|
|
|
} |
1825
|
|
|
|
1826
|
96 |
|
$this->append($header . $data); |
1827
|
|
|
} |
1828
|
|
|
|
1829
|
|
|
/** |
1830
|
|
|
* Write the PRINTHEADERS BIFF record. |
1831
|
|
|
*/ |
1832
|
96 |
|
private function writePrintHeaders(): void |
1833
|
|
|
{ |
1834
|
96 |
|
$record = 0x002a; // Record identifier |
1835
|
96 |
|
$length = 0x0002; // Bytes to follow |
1836
|
|
|
|
1837
|
96 |
|
$fPrintRwCol = $this->printHeaders; // Boolean flag |
1838
|
|
|
|
1839
|
96 |
|
$header = pack('vv', $record, $length); |
1840
|
96 |
|
$data = pack('v', $fPrintRwCol); |
1841
|
96 |
|
$this->append($header . $data); |
1842
|
|
|
} |
1843
|
|
|
|
1844
|
|
|
/** |
1845
|
|
|
* Write the PRINTGRIDLINES BIFF record. Must be used in conjunction with the |
1846
|
|
|
* GRIDSET record. |
1847
|
|
|
*/ |
1848
|
96 |
|
private function writePrintGridlines(): void |
1849
|
|
|
{ |
1850
|
96 |
|
$record = 0x002b; // Record identifier |
1851
|
96 |
|
$length = 0x0002; // Bytes to follow |
1852
|
|
|
|
1853
|
96 |
|
$fPrintGrid = $this->phpSheet->getPrintGridlines() ? 1 : 0; // Boolean flag |
1854
|
|
|
|
1855
|
96 |
|
$header = pack('vv', $record, $length); |
1856
|
96 |
|
$data = pack('v', $fPrintGrid); |
1857
|
96 |
|
$this->append($header . $data); |
1858
|
|
|
} |
1859
|
|
|
|
1860
|
|
|
/** |
1861
|
|
|
* Write the GRIDSET BIFF record. Must be used in conjunction with the |
1862
|
|
|
* PRINTGRIDLINES record. |
1863
|
|
|
*/ |
1864
|
96 |
|
private function writeGridset(): void |
1865
|
|
|
{ |
1866
|
96 |
|
$record = 0x0082; // Record identifier |
1867
|
96 |
|
$length = 0x0002; // Bytes to follow |
1868
|
|
|
|
1869
|
96 |
|
$fGridSet = !$this->phpSheet->getPrintGridlines(); // Boolean flag |
1870
|
|
|
|
1871
|
96 |
|
$header = pack('vv', $record, $length); |
1872
|
96 |
|
$data = pack('v', $fGridSet); |
1873
|
96 |
|
$this->append($header . $data); |
1874
|
|
|
} |
1875
|
|
|
|
1876
|
|
|
/** |
1877
|
|
|
* Write the AUTOFILTERINFO BIFF record. This is used to configure the number of autofilter select used in the sheet. |
1878
|
|
|
*/ |
1879
|
3 |
|
private function writeAutoFilterInfo(): void |
1880
|
|
|
{ |
1881
|
3 |
|
$record = 0x009D; // Record identifier |
1882
|
3 |
|
$length = 0x0002; // Bytes to follow |
1883
|
|
|
|
1884
|
3 |
|
$rangeBounds = Coordinate::rangeBoundaries($this->phpSheet->getAutoFilter()->getRange()); |
1885
|
3 |
|
$iNumFilters = 1 + $rangeBounds[1][0] - $rangeBounds[0][0]; |
1886
|
|
|
|
1887
|
3 |
|
$header = pack('vv', $record, $length); |
1888
|
3 |
|
$data = pack('v', $iNumFilters); |
1889
|
3 |
|
$this->append($header . $data); |
1890
|
|
|
} |
1891
|
|
|
|
1892
|
|
|
/** |
1893
|
|
|
* Write the GUTS BIFF record. This is used to configure the gutter margins |
1894
|
|
|
* where Excel outline symbols are displayed. The visibility of the gutters is |
1895
|
|
|
* controlled by a flag in WSBOOL. |
1896
|
|
|
* |
1897
|
|
|
* @see writeWsbool() |
1898
|
|
|
*/ |
1899
|
96 |
|
private function writeGuts(): void |
1900
|
|
|
{ |
1901
|
96 |
|
$record = 0x0080; // Record identifier |
1902
|
96 |
|
$length = 0x0008; // Bytes to follow |
1903
|
|
|
|
1904
|
96 |
|
$dxRwGut = 0x0000; // Size of row gutter |
1905
|
96 |
|
$dxColGut = 0x0000; // Size of col gutter |
1906
|
|
|
|
1907
|
|
|
// determine maximum row outline level |
1908
|
96 |
|
$maxRowOutlineLevel = 0; |
1909
|
96 |
|
foreach ($this->phpSheet->getRowDimensions() as $rowDimension) { |
1910
|
25 |
|
$maxRowOutlineLevel = max($maxRowOutlineLevel, $rowDimension->getOutlineLevel()); |
1911
|
|
|
} |
1912
|
|
|
|
1913
|
96 |
|
$col_level = 0; |
1914
|
|
|
|
1915
|
|
|
// Calculate the maximum column outline level. The equivalent calculation |
1916
|
|
|
// for the row outline level is carried out in writeRow(). |
1917
|
96 |
|
$colcount = count($this->columnInfo); |
1918
|
96 |
|
for ($i = 0; $i < $colcount; ++$i) { |
1919
|
96 |
|
$col_level = max($this->columnInfo[$i][5], $col_level); |
1920
|
|
|
} |
1921
|
|
|
|
1922
|
|
|
// Set the limits for the outline levels (0 <= x <= 7). |
1923
|
96 |
|
$col_level = max(0, min($col_level, 7)); |
1924
|
|
|
|
1925
|
|
|
// The displayed level is one greater than the max outline levels |
1926
|
96 |
|
if ($maxRowOutlineLevel) { |
1927
|
|
|
++$maxRowOutlineLevel; |
1928
|
|
|
} |
1929
|
96 |
|
if ($col_level) { |
1930
|
1 |
|
++$col_level; |
1931
|
|
|
} |
1932
|
|
|
|
1933
|
96 |
|
$header = pack('vv', $record, $length); |
1934
|
96 |
|
$data = pack('vvvv', $dxRwGut, $dxColGut, $maxRowOutlineLevel, $col_level); |
1935
|
|
|
|
1936
|
96 |
|
$this->append($header . $data); |
1937
|
|
|
} |
1938
|
|
|
|
1939
|
|
|
/** |
1940
|
|
|
* Write the WSBOOL BIFF record, mainly for fit-to-page. Used in conjunction |
1941
|
|
|
* with the SETUP record. |
1942
|
|
|
*/ |
1943
|
96 |
|
private function writeWsbool(): void |
1944
|
|
|
{ |
1945
|
96 |
|
$record = 0x0081; // Record identifier |
1946
|
96 |
|
$length = 0x0002; // Bytes to follow |
1947
|
96 |
|
$grbit = 0x0000; |
1948
|
|
|
|
1949
|
|
|
// The only option that is of interest is the flag for fit to page. So we |
1950
|
|
|
// set all the options in one go. |
1951
|
|
|
// |
1952
|
|
|
// Set the option flags |
1953
|
96 |
|
$grbit |= 0x0001; // Auto page breaks visible |
1954
|
96 |
|
if ($this->outlineStyle) { |
1955
|
|
|
$grbit |= 0x0020; // Auto outline styles |
1956
|
|
|
} |
1957
|
96 |
|
if ($this->phpSheet->getShowSummaryBelow()) { |
1958
|
96 |
|
$grbit |= 0x0040; // Outline summary below |
1959
|
|
|
} |
1960
|
96 |
|
if ($this->phpSheet->getShowSummaryRight()) { |
1961
|
96 |
|
$grbit |= 0x0080; // Outline summary right |
1962
|
|
|
} |
1963
|
96 |
|
if ($this->phpSheet->getPageSetup()->getFitToPage()) { |
1964
|
|
|
$grbit |= 0x0100; // Page setup fit to page |
1965
|
|
|
} |
1966
|
96 |
|
if ($this->outlineOn) { |
1967
|
96 |
|
$grbit |= 0x0400; // Outline symbols displayed |
1968
|
|
|
} |
1969
|
|
|
|
1970
|
96 |
|
$header = pack('vv', $record, $length); |
1971
|
96 |
|
$data = pack('v', $grbit); |
1972
|
96 |
|
$this->append($header . $data); |
1973
|
|
|
} |
1974
|
|
|
|
1975
|
|
|
/** |
1976
|
|
|
* Write the HORIZONTALPAGEBREAKS and VERTICALPAGEBREAKS BIFF records. |
1977
|
|
|
*/ |
1978
|
96 |
|
private function writeBreaks(): void |
1979
|
|
|
{ |
1980
|
|
|
// initialize |
1981
|
96 |
|
$vbreaks = []; |
1982
|
96 |
|
$hbreaks = []; |
1983
|
|
|
|
1984
|
96 |
|
foreach ($this->phpSheet->getRowBreaks() as $cell => $break) { |
1985
|
|
|
// Fetch coordinates |
1986
|
4 |
|
$coordinates = Coordinate::coordinateFromString($cell); |
1987
|
4 |
|
$hbreaks[] = $coordinates[1]; |
1988
|
|
|
} |
1989
|
96 |
|
foreach ($this->phpSheet->getColumnBreaks() as $cell => $break) { |
1990
|
|
|
// Fetch coordinates |
1991
|
2 |
|
$coordinates = Coordinate::indexesFromString($cell); |
1992
|
2 |
|
$vbreaks[] = $coordinates[0] - 1; |
1993
|
|
|
} |
1994
|
|
|
|
1995
|
|
|
//horizontal page breaks |
1996
|
96 |
|
if (!empty($hbreaks)) { |
1997
|
|
|
// Sort and filter array of page breaks |
1998
|
4 |
|
sort($hbreaks, SORT_NUMERIC); |
1999
|
4 |
|
if ($hbreaks[0] == 0) { // don't use first break if it's 0 |
2000
|
|
|
array_shift($hbreaks); |
2001
|
|
|
} |
2002
|
|
|
|
2003
|
4 |
|
$record = 0x001b; // Record identifier |
2004
|
4 |
|
$cbrk = count($hbreaks); // Number of page breaks |
2005
|
4 |
|
$length = 2 + 6 * $cbrk; // Bytes to follow |
2006
|
|
|
|
2007
|
4 |
|
$header = pack('vv', $record, $length); |
2008
|
4 |
|
$data = pack('v', $cbrk); |
2009
|
|
|
|
2010
|
|
|
// Append each page break |
2011
|
4 |
|
foreach ($hbreaks as $hbreak) { |
2012
|
4 |
|
$data .= pack('vvv', $hbreak, 0x0000, 0x00ff); |
2013
|
|
|
} |
2014
|
|
|
|
2015
|
4 |
|
$this->append($header . $data); |
2016
|
|
|
} |
2017
|
|
|
|
2018
|
|
|
// vertical page breaks |
2019
|
96 |
|
if (!empty($vbreaks)) { |
2020
|
|
|
// 1000 vertical pagebreaks appears to be an internal Excel 5 limit. |
2021
|
|
|
// It is slightly higher in Excel 97/200, approx. 1026 |
2022
|
2 |
|
$vbreaks = array_slice($vbreaks, 0, 1000); |
2023
|
|
|
|
2024
|
|
|
// Sort and filter array of page breaks |
2025
|
2 |
|
sort($vbreaks, SORT_NUMERIC); |
2026
|
2 |
|
if ($vbreaks[0] == 0) { // don't use first break if it's 0 |
2027
|
|
|
array_shift($vbreaks); |
2028
|
|
|
} |
2029
|
|
|
|
2030
|
2 |
|
$record = 0x001a; // Record identifier |
2031
|
2 |
|
$cbrk = count($vbreaks); // Number of page breaks |
2032
|
2 |
|
$length = 2 + 6 * $cbrk; // Bytes to follow |
2033
|
|
|
|
2034
|
2 |
|
$header = pack('vv', $record, $length); |
2035
|
2 |
|
$data = pack('v', $cbrk); |
2036
|
|
|
|
2037
|
|
|
// Append each page break |
2038
|
2 |
|
foreach ($vbreaks as $vbreak) { |
2039
|
2 |
|
$data .= pack('vvv', $vbreak, 0x0000, 0xffff); |
2040
|
|
|
} |
2041
|
|
|
|
2042
|
2 |
|
$this->append($header . $data); |
2043
|
|
|
} |
2044
|
|
|
} |
2045
|
|
|
|
2046
|
|
|
/** |
2047
|
|
|
* Set the Biff PROTECT record to indicate that the worksheet is protected. |
2048
|
|
|
*/ |
2049
|
96 |
|
private function writeProtect(): void |
2050
|
|
|
{ |
2051
|
|
|
// Exit unless sheet protection has been specified |
2052
|
96 |
|
if ($this->phpSheet->getProtection()->getSheet() !== true) { |
2053
|
91 |
|
return; |
2054
|
|
|
} |
2055
|
|
|
|
2056
|
11 |
|
$record = 0x0012; // Record identifier |
2057
|
11 |
|
$length = 0x0002; // Bytes to follow |
2058
|
|
|
|
2059
|
11 |
|
$fLock = 1; // Worksheet is protected |
2060
|
|
|
|
2061
|
11 |
|
$header = pack('vv', $record, $length); |
2062
|
11 |
|
$data = pack('v', $fLock); |
2063
|
|
|
|
2064
|
11 |
|
$this->append($header . $data); |
2065
|
|
|
} |
2066
|
|
|
|
2067
|
|
|
/** |
2068
|
|
|
* Write SCENPROTECT. |
2069
|
|
|
*/ |
2070
|
96 |
|
private function writeScenProtect(): void |
2071
|
|
|
{ |
2072
|
|
|
// Exit if sheet protection is not active |
2073
|
96 |
|
if ($this->phpSheet->getProtection()->getSheet() !== true) { |
2074
|
91 |
|
return; |
2075
|
|
|
} |
2076
|
|
|
|
2077
|
|
|
// Exit if scenarios are not protected |
2078
|
11 |
|
if ($this->phpSheet->getProtection()->getScenarios() !== true) { |
2079
|
11 |
|
return; |
2080
|
|
|
} |
2081
|
|
|
|
2082
|
1 |
|
$record = 0x00DD; // Record identifier |
2083
|
1 |
|
$length = 0x0002; // Bytes to follow |
2084
|
|
|
|
2085
|
1 |
|
$header = pack('vv', $record, $length); |
2086
|
1 |
|
$data = pack('v', 1); |
2087
|
|
|
|
2088
|
1 |
|
$this->append($header . $data); |
2089
|
|
|
} |
2090
|
|
|
|
2091
|
|
|
/** |
2092
|
|
|
* Write OBJECTPROTECT. |
2093
|
|
|
*/ |
2094
|
96 |
|
private function writeObjectProtect(): void |
2095
|
|
|
{ |
2096
|
|
|
// Exit if sheet protection is not active |
2097
|
96 |
|
if ($this->phpSheet->getProtection()->getSheet() !== true) { |
2098
|
91 |
|
return; |
2099
|
|
|
} |
2100
|
|
|
|
2101
|
|
|
// Exit if objects are not protected |
2102
|
11 |
|
if ($this->phpSheet->getProtection()->getObjects() !== true) { |
2103
|
10 |
|
return; |
2104
|
|
|
} |
2105
|
|
|
|
2106
|
2 |
|
$record = 0x0063; // Record identifier |
2107
|
2 |
|
$length = 0x0002; // Bytes to follow |
2108
|
|
|
|
2109
|
2 |
|
$header = pack('vv', $record, $length); |
2110
|
2 |
|
$data = pack('v', 1); |
2111
|
|
|
|
2112
|
2 |
|
$this->append($header . $data); |
2113
|
|
|
} |
2114
|
|
|
|
2115
|
|
|
/** |
2116
|
|
|
* Write the worksheet PASSWORD record. |
2117
|
|
|
*/ |
2118
|
96 |
|
private function writePassword(): void |
2119
|
|
|
{ |
2120
|
|
|
// Exit unless sheet protection and password have been specified |
2121
|
96 |
|
if ($this->phpSheet->getProtection()->getSheet() !== true || !$this->phpSheet->getProtection()->getPassword() || $this->phpSheet->getProtection()->getAlgorithm() !== '') { |
2122
|
93 |
|
return; |
2123
|
|
|
} |
2124
|
|
|
|
2125
|
3 |
|
$record = 0x0013; // Record identifier |
2126
|
3 |
|
$length = 0x0002; // Bytes to follow |
2127
|
|
|
|
2128
|
3 |
|
$wPassword = hexdec($this->phpSheet->getProtection()->getPassword()); // Encoded password |
2129
|
|
|
|
2130
|
3 |
|
$header = pack('vv', $record, $length); |
2131
|
3 |
|
$data = pack('v', $wPassword); |
2132
|
|
|
|
2133
|
3 |
|
$this->append($header . $data); |
2134
|
|
|
} |
2135
|
|
|
|
2136
|
|
|
/** |
2137
|
|
|
* Insert a 24bit bitmap image in a worksheet. |
2138
|
|
|
* |
2139
|
|
|
* @param int $row The row we are going to insert the bitmap into |
2140
|
|
|
* @param int $col The column we are going to insert the bitmap into |
2141
|
|
|
* @param GdImage|string $bitmap The bitmap filename or GD-image resource |
2142
|
|
|
* @param int $x the horizontal position (offset) of the image inside the cell |
2143
|
|
|
* @param int $y the vertical position (offset) of the image inside the cell |
2144
|
|
|
* @param float $scale_x The horizontal scale |
2145
|
|
|
* @param float $scale_y The vertical scale |
2146
|
|
|
*/ |
2147
|
|
|
public function insertBitmap($row, $col, GdImage|string $bitmap, $x = 0, $y = 0, $scale_x = 1, $scale_y = 1): void |
2148
|
|
|
{ |
2149
|
|
|
$bitmap_array = $bitmap instanceof GdImage |
2150
|
|
|
? $this->processBitmapGd($bitmap) |
2151
|
|
|
: $this->processBitmap($bitmap); |
2152
|
|
|
[$width, $height, $size, $data] = $bitmap_array; |
2153
|
|
|
|
2154
|
|
|
// Scale the frame of the image. |
2155
|
|
|
$width *= $scale_x; |
2156
|
|
|
$height *= $scale_y; |
2157
|
|
|
|
2158
|
|
|
// Calculate the vertices of the image and write the OBJ record |
2159
|
|
|
$this->positionImage($col, $row, $x, $y, (int) $width, (int) $height); |
2160
|
|
|
|
2161
|
|
|
// Write the IMDATA record to store the bitmap data |
2162
|
|
|
$record = 0x007f; |
2163
|
|
|
$length = 8 + $size; |
2164
|
|
|
$cf = 0x09; |
2165
|
|
|
$env = 0x01; |
2166
|
|
|
$lcb = $size; |
2167
|
|
|
|
2168
|
|
|
$header = pack('vvvvV', $record, $length, $cf, $env, $lcb); |
2169
|
|
|
$this->append($header . $data); |
2170
|
|
|
} |
2171
|
|
|
|
2172
|
|
|
/** |
2173
|
|
|
* Calculate the vertices that define the position of the image as required by |
2174
|
|
|
* the OBJ record. |
2175
|
|
|
* |
2176
|
|
|
* +------------+------------+ |
2177
|
|
|
* | A | B | |
2178
|
|
|
* +-----+------------+------------+ |
2179
|
|
|
* | |(x1,y1) | | |
2180
|
|
|
* | 1 |(A1)._______|______ | |
2181
|
|
|
* | | | | | |
2182
|
|
|
* | | | | | |
2183
|
|
|
* +-----+----| BITMAP |-----+ |
2184
|
|
|
* | | | | | |
2185
|
|
|
* | 2 | |______________. | |
2186
|
|
|
* | | | (B2)| |
2187
|
|
|
* | | | (x2,y2)| |
2188
|
|
|
* +---- +------------+------------+ |
2189
|
|
|
* |
2190
|
|
|
* Example of a bitmap that covers some of the area from cell A1 to cell B2. |
2191
|
|
|
* |
2192
|
|
|
* Based on the width and height of the bitmap we need to calculate 8 vars: |
2193
|
|
|
* $col_start, $row_start, $col_end, $row_end, $x1, $y1, $x2, $y2. |
2194
|
|
|
* The width and height of the cells are also variable and have to be taken into |
2195
|
|
|
* account. |
2196
|
|
|
* The values of $col_start and $row_start are passed in from the calling |
2197
|
|
|
* function. The values of $col_end and $row_end are calculated by subtracting |
2198
|
|
|
* the width and height of the bitmap from the width and height of the |
2199
|
|
|
* underlying cells. |
2200
|
|
|
* The vertices are expressed as a percentage of the underlying cell width as |
2201
|
|
|
* follows (rhs values are in pixels): |
2202
|
|
|
* |
2203
|
|
|
* x1 = X / W *1024 |
2204
|
|
|
* y1 = Y / H *256 |
2205
|
|
|
* x2 = (X-1) / W *1024 |
2206
|
|
|
* y2 = (Y-1) / H *256 |
2207
|
|
|
* |
2208
|
|
|
* Where: X is distance from the left side of the underlying cell |
2209
|
|
|
* Y is distance from the top of the underlying cell |
2210
|
|
|
* W is the width of the cell |
2211
|
|
|
* H is the height of the cell |
2212
|
|
|
* The SDK incorrectly states that the height should be expressed as a |
2213
|
|
|
* percentage of 1024. |
2214
|
|
|
* |
2215
|
|
|
* @param int $col_start Col containing upper left corner of object |
2216
|
|
|
* @param int $row_start Row containing top left corner of object |
2217
|
|
|
* @param int $x1 Distance to left side of object |
2218
|
|
|
* @param int $y1 Distance to top of object |
2219
|
|
|
* @param int $width Width of image frame |
2220
|
|
|
* @param int $height Height of image frame |
2221
|
|
|
*/ |
2222
|
|
|
public function positionImage($col_start, $row_start, $x1, $y1, $width, $height): void |
2223
|
|
|
{ |
2224
|
|
|
// Initialise end cell to the same as the start cell |
2225
|
|
|
$col_end = $col_start; // Col containing lower right corner of object |
2226
|
|
|
$row_end = $row_start; // Row containing bottom right corner of object |
2227
|
|
|
|
2228
|
|
|
// Zero the specified offset if greater than the cell dimensions |
2229
|
|
|
if ($x1 >= Xls::sizeCol($this->phpSheet, Coordinate::stringFromColumnIndex($col_start + 1))) { |
2230
|
|
|
$x1 = 0; |
2231
|
|
|
} |
2232
|
|
|
if ($y1 >= Xls::sizeRow($this->phpSheet, $row_start + 1)) { |
2233
|
|
|
$y1 = 0; |
2234
|
|
|
} |
2235
|
|
|
|
2236
|
|
|
$width = $width + $x1 - 1; |
2237
|
|
|
$height = $height + $y1 - 1; |
2238
|
|
|
|
2239
|
|
|
// Subtract the underlying cell widths to find the end cell of the image |
2240
|
|
|
while ($width >= Xls::sizeCol($this->phpSheet, Coordinate::stringFromColumnIndex($col_end + 1))) { |
2241
|
|
|
$width -= Xls::sizeCol($this->phpSheet, Coordinate::stringFromColumnIndex($col_end + 1)); |
2242
|
|
|
++$col_end; |
2243
|
|
|
} |
2244
|
|
|
|
2245
|
|
|
// Subtract the underlying cell heights to find the end cell of the image |
2246
|
|
|
while ($height >= Xls::sizeRow($this->phpSheet, $row_end + 1)) { |
2247
|
|
|
$height -= Xls::sizeRow($this->phpSheet, $row_end + 1); |
2248
|
|
|
++$row_end; |
2249
|
|
|
} |
2250
|
|
|
|
2251
|
|
|
// Bitmap isn't allowed to start or finish in a hidden cell, i.e. a cell |
2252
|
|
|
// with zero eight or width. |
2253
|
|
|
// |
2254
|
|
|
if (Xls::sizeCol($this->phpSheet, Coordinate::stringFromColumnIndex($col_start + 1)) == 0) { |
2255
|
|
|
return; |
2256
|
|
|
} |
2257
|
|
|
if (Xls::sizeCol($this->phpSheet, Coordinate::stringFromColumnIndex($col_end + 1)) == 0) { |
2258
|
|
|
return; |
2259
|
|
|
} |
2260
|
|
|
if (Xls::sizeRow($this->phpSheet, $row_start + 1) == 0) { |
2261
|
|
|
return; |
2262
|
|
|
} |
2263
|
|
|
if (Xls::sizeRow($this->phpSheet, $row_end + 1) == 0) { |
2264
|
|
|
return; |
2265
|
|
|
} |
2266
|
|
|
|
2267
|
|
|
// Convert the pixel values to the percentage value expected by Excel |
2268
|
|
|
$x1 = $x1 / Xls::sizeCol($this->phpSheet, Coordinate::stringFromColumnIndex($col_start + 1)) * 1024; |
2269
|
|
|
$y1 = $y1 / Xls::sizeRow($this->phpSheet, $row_start + 1) * 256; |
2270
|
|
|
$x2 = $width / Xls::sizeCol($this->phpSheet, Coordinate::stringFromColumnIndex($col_end + 1)) * 1024; // Distance to right side of object |
2271
|
|
|
$y2 = $height / Xls::sizeRow($this->phpSheet, $row_end + 1) * 256; // Distance to bottom of object |
2272
|
|
|
|
2273
|
|
|
$this->writeObjPicture($col_start, $x1, $row_start, $y1, $col_end, $x2, $row_end, $y2); |
2274
|
|
|
} |
2275
|
|
|
|
2276
|
|
|
/** |
2277
|
|
|
* Store the OBJ record that precedes an IMDATA record. This could be generalise |
2278
|
|
|
* to support other Excel objects. |
2279
|
|
|
* |
2280
|
|
|
* @param int $colL Column containing upper left corner of object |
2281
|
|
|
* @param int $dxL Distance from left side of cell |
2282
|
|
|
* @param int $rwT Row containing top left corner of object |
2283
|
|
|
* @param int $dyT Distance from top of cell |
2284
|
|
|
* @param int $colR Column containing lower right corner of object |
2285
|
|
|
* @param int $dxR Distance from right of cell |
2286
|
|
|
* @param int $rwB Row containing bottom right corner of object |
2287
|
|
|
* @param int $dyB Distance from bottom of cell |
2288
|
|
|
*/ |
2289
|
|
|
private function writeObjPicture(int $colL, int $dxL, int $rwT, int|float $dyT, int $colR, int $dxR, int $rwB, int $dyB): void |
2290
|
|
|
{ |
2291
|
|
|
$record = 0x005d; // Record identifier |
2292
|
|
|
$length = 0x003c; // Bytes to follow |
2293
|
|
|
|
2294
|
|
|
$cObj = 0x0001; // Count of objects in file (set to 1) |
2295
|
|
|
$OT = 0x0008; // Object type. 8 = Picture |
2296
|
|
|
$id = 0x0001; // Object ID |
2297
|
|
|
$grbit = 0x0614; // Option flags |
2298
|
|
|
|
2299
|
|
|
$cbMacro = 0x0000; // Length of FMLA structure |
2300
|
|
|
$Reserved1 = 0x0000; // Reserved |
2301
|
|
|
$Reserved2 = 0x0000; // Reserved |
2302
|
|
|
|
2303
|
|
|
$icvBack = 0x09; // Background colour |
2304
|
|
|
$icvFore = 0x09; // Foreground colour |
2305
|
|
|
$fls = 0x00; // Fill pattern |
2306
|
|
|
$fAuto = 0x00; // Automatic fill |
2307
|
|
|
$icv = 0x08; // Line colour |
2308
|
|
|
$lns = 0xff; // Line style |
2309
|
|
|
$lnw = 0x01; // Line weight |
2310
|
|
|
$fAutoB = 0x00; // Automatic border |
2311
|
|
|
$frs = 0x0000; // Frame style |
2312
|
|
|
$cf = 0x0009; // Image format, 9 = bitmap |
2313
|
|
|
$Reserved3 = 0x0000; // Reserved |
2314
|
|
|
$cbPictFmla = 0x0000; // Length of FMLA structure |
2315
|
|
|
$Reserved4 = 0x0000; // Reserved |
2316
|
|
|
$grbit2 = 0x0001; // Option flags |
2317
|
|
|
$Reserved5 = 0x0000; // Reserved |
2318
|
|
|
|
2319
|
|
|
$header = pack('vv', $record, $length); |
2320
|
|
|
$data = pack('V', $cObj); |
2321
|
|
|
$data .= pack('v', $OT); |
2322
|
|
|
$data .= pack('v', $id); |
2323
|
|
|
$data .= pack('v', $grbit); |
2324
|
|
|
$data .= pack('v', $colL); |
2325
|
|
|
$data .= pack('v', $dxL); |
2326
|
|
|
$data .= pack('v', $rwT); |
2327
|
|
|
$data .= pack('v', $dyT); |
2328
|
|
|
$data .= pack('v', $colR); |
2329
|
|
|
$data .= pack('v', $dxR); |
2330
|
|
|
$data .= pack('v', $rwB); |
2331
|
|
|
$data .= pack('v', $dyB); |
2332
|
|
|
$data .= pack('v', $cbMacro); |
2333
|
|
|
$data .= pack('V', $Reserved1); |
2334
|
|
|
$data .= pack('v', $Reserved2); |
2335
|
|
|
$data .= pack('C', $icvBack); |
2336
|
|
|
$data .= pack('C', $icvFore); |
2337
|
|
|
$data .= pack('C', $fls); |
2338
|
|
|
$data .= pack('C', $fAuto); |
2339
|
|
|
$data .= pack('C', $icv); |
2340
|
|
|
$data .= pack('C', $lns); |
2341
|
|
|
$data .= pack('C', $lnw); |
2342
|
|
|
$data .= pack('C', $fAutoB); |
2343
|
|
|
$data .= pack('v', $frs); |
2344
|
|
|
$data .= pack('V', $cf); |
2345
|
|
|
$data .= pack('v', $Reserved3); |
2346
|
|
|
$data .= pack('v', $cbPictFmla); |
2347
|
|
|
$data .= pack('v', $Reserved4); |
2348
|
|
|
$data .= pack('v', $grbit2); |
2349
|
|
|
$data .= pack('V', $Reserved5); |
2350
|
|
|
|
2351
|
|
|
$this->append($header . $data); |
2352
|
|
|
} |
2353
|
|
|
|
2354
|
|
|
/** |
2355
|
|
|
* Convert a GD-image into the internal format. |
2356
|
|
|
* |
2357
|
|
|
* @param GdImage $image The image to process |
2358
|
|
|
* |
2359
|
|
|
* @return array Array with data and properties of the bitmap |
2360
|
|
|
*/ |
2361
|
|
|
public function processBitmapGd(GdImage $image): array |
2362
|
|
|
{ |
2363
|
|
|
$width = imagesx($image); |
2364
|
|
|
$height = imagesy($image); |
2365
|
|
|
|
2366
|
|
|
$data = pack('Vvvvv', 0x000c, $width, $height, 0x01, 0x18); |
2367
|
|
|
for ($j = $height; --$j;) { |
2368
|
|
|
for ($i = 0; $i < $width; ++$i) { |
2369
|
|
|
/** @phpstan-ignore-next-line */ |
2370
|
|
|
$color = imagecolorsforindex($image, imagecolorat($image, $i, $j)); |
2371
|
|
|
if ($color !== false) { |
2372
|
|
|
foreach (['red', 'green', 'blue'] as $key) { |
2373
|
|
|
$color[$key] = $color[$key] + (int) round((255 - $color[$key]) * $color['alpha'] / 127); |
2374
|
|
|
} |
2375
|
|
|
$data .= chr($color['blue']) . chr($color['green']) . chr($color['red']); |
2376
|
|
|
} |
2377
|
|
|
} |
2378
|
|
|
if (3 * $width % 4) { |
2379
|
|
|
$data .= str_repeat("\x00", 4 - 3 * $width % 4); |
2380
|
|
|
} |
2381
|
|
|
} |
2382
|
|
|
|
2383
|
|
|
return [$width, $height, strlen($data), $data]; |
2384
|
|
|
} |
2385
|
|
|
|
2386
|
|
|
/** |
2387
|
|
|
* Convert a 24 bit bitmap into the modified internal format used by Windows. |
2388
|
|
|
* This is described in BITMAPCOREHEADER and BITMAPCOREINFO structures in the |
2389
|
|
|
* MSDN library. |
2390
|
|
|
* |
2391
|
|
|
* @param string $bitmap The bitmap to process |
2392
|
|
|
* |
2393
|
|
|
* @return array Array with data and properties of the bitmap |
2394
|
|
|
*/ |
2395
|
|
|
public function processBitmap($bitmap): array |
2396
|
|
|
{ |
2397
|
|
|
// Open file. |
2398
|
|
|
$bmp_fd = @fopen($bitmap, 'rb'); |
2399
|
|
|
if ($bmp_fd === false) { |
2400
|
|
|
throw new WriterException("Couldn't import $bitmap"); |
2401
|
|
|
} |
2402
|
|
|
|
2403
|
|
|
// Slurp the file into a string. |
2404
|
|
|
$data = (string) fread($bmp_fd, (int) filesize($bitmap)); |
2405
|
|
|
|
2406
|
|
|
// Check that the file is big enough to be a bitmap. |
2407
|
|
|
if (strlen($data) <= 0x36) { |
2408
|
|
|
throw new WriterException("$bitmap doesn't contain enough data.\n"); |
2409
|
|
|
} |
2410
|
|
|
|
2411
|
|
|
// The first 2 bytes are used to identify the bitmap. |
2412
|
|
|
|
2413
|
|
|
$identity = unpack('A2ident', $data); |
2414
|
|
|
if ($identity === false || $identity['ident'] != 'BM') { |
2415
|
|
|
throw new WriterException("$bitmap doesn't appear to be a valid bitmap image.\n"); |
2416
|
|
|
} |
2417
|
|
|
|
2418
|
|
|
// Remove bitmap data: ID. |
2419
|
|
|
$data = substr($data, 2); |
2420
|
|
|
|
2421
|
|
|
// Read and remove the bitmap size. This is more reliable than reading |
2422
|
|
|
// the data size at offset 0x22. |
2423
|
|
|
// |
2424
|
|
|
$size_array = unpack('Vsa', substr($data, 0, 4)) ?: []; |
2425
|
|
|
$size = $size_array['sa']; |
2426
|
|
|
$data = substr($data, 4); |
2427
|
|
|
$size -= 0x36; // Subtract size of bitmap header. |
2428
|
|
|
$size += 0x0C; // Add size of BIFF header. |
2429
|
|
|
|
2430
|
|
|
// Remove bitmap data: reserved, offset, header length. |
2431
|
|
|
$data = substr($data, 12); |
2432
|
|
|
|
2433
|
|
|
// Read and remove the bitmap width and height. Verify the sizes. |
2434
|
|
|
$width_and_height = unpack('V2', substr($data, 0, 8)) ?: []; |
2435
|
|
|
$width = $width_and_height[1]; |
2436
|
|
|
$height = $width_and_height[2]; |
2437
|
|
|
$data = substr($data, 8); |
2438
|
|
|
if ($width > 0xFFFF) { |
2439
|
|
|
throw new WriterException("$bitmap: largest image width supported is 65k.\n"); |
2440
|
|
|
} |
2441
|
|
|
if ($height > 0xFFFF) { |
2442
|
|
|
throw new WriterException("$bitmap: largest image height supported is 65k.\n"); |
2443
|
|
|
} |
2444
|
|
|
|
2445
|
|
|
// Read and remove the bitmap planes and bpp data. Verify them. |
2446
|
|
|
$planes_and_bitcount = unpack('v2', substr($data, 0, 4)); |
2447
|
|
|
$data = substr($data, 4); |
2448
|
|
|
if ($planes_and_bitcount === false || $planes_and_bitcount[2] != 24) { // Bitcount |
2449
|
|
|
throw new WriterException("$bitmap isn't a 24bit true color bitmap.\n"); |
2450
|
|
|
} |
2451
|
|
|
if ($planes_and_bitcount[1] != 1) { |
2452
|
|
|
throw new WriterException("$bitmap: only 1 plane supported in bitmap image.\n"); |
2453
|
|
|
} |
2454
|
|
|
|
2455
|
|
|
// Read and remove the bitmap compression. Verify compression. |
2456
|
|
|
$compression = unpack('Vcomp', substr($data, 0, 4)); |
2457
|
|
|
$data = substr($data, 4); |
2458
|
|
|
|
2459
|
|
|
if ($compression === false || $compression['comp'] != 0) { |
2460
|
|
|
throw new WriterException("$bitmap: compression not supported in bitmap image.\n"); |
2461
|
|
|
} |
2462
|
|
|
|
2463
|
|
|
// Remove bitmap data: data size, hres, vres, colours, imp. colours. |
2464
|
|
|
$data = substr($data, 20); |
2465
|
|
|
|
2466
|
|
|
// Add the BITMAPCOREHEADER data |
2467
|
|
|
$header = pack('Vvvvv', 0x000c, $width, $height, 0x01, 0x18); |
2468
|
|
|
$data = $header . $data; |
2469
|
|
|
|
2470
|
|
|
return [$width, $height, $size, $data]; |
2471
|
|
|
} |
2472
|
|
|
|
2473
|
|
|
/** |
2474
|
|
|
* Store the window zoom factor. This should be a reduced fraction but for |
2475
|
|
|
* simplicity we will store all fractions with a numerator of 100. |
2476
|
|
|
*/ |
2477
|
95 |
|
private function writeZoom(): void |
2478
|
|
|
{ |
2479
|
|
|
// If scale is 100 we don't need to write a record |
2480
|
95 |
|
if ($this->phpSheet->getSheetView()->getZoomScale() == 100) { |
2481
|
95 |
|
return; |
2482
|
|
|
} |
2483
|
|
|
|
2484
|
|
|
$record = 0x00A0; // Record identifier |
2485
|
|
|
$length = 0x0004; // Bytes to follow |
2486
|
|
|
|
2487
|
|
|
$header = pack('vv', $record, $length); |
2488
|
|
|
$data = pack('vv', $this->phpSheet->getSheetView()->getZoomScale(), 100); |
2489
|
|
|
$this->append($header . $data); |
2490
|
|
|
} |
2491
|
|
|
|
2492
|
|
|
/** |
2493
|
|
|
* Get Escher object. |
2494
|
|
|
*/ |
2495
|
|
|
public function getEscher(): ?\PhpOffice\PhpSpreadsheet\Shared\Escher |
2496
|
|
|
{ |
2497
|
|
|
return $this->escher; |
2498
|
|
|
} |
2499
|
|
|
|
2500
|
|
|
/** |
2501
|
|
|
* Set Escher object. |
2502
|
|
|
*/ |
2503
|
14 |
|
public function setEscher(?\PhpOffice\PhpSpreadsheet\Shared\Escher $escher): void |
2504
|
|
|
{ |
2505
|
14 |
|
$this->escher = $escher; |
2506
|
|
|
} |
2507
|
|
|
|
2508
|
|
|
/** |
2509
|
|
|
* Write MSODRAWING record. |
2510
|
|
|
*/ |
2511
|
95 |
|
private function writeMsoDrawing(): void |
2512
|
|
|
{ |
2513
|
|
|
// write the Escher stream if necessary |
2514
|
95 |
|
if (isset($this->escher)) { |
2515
|
14 |
|
$writer = new Escher($this->escher); |
2516
|
14 |
|
$data = $writer->close(); |
2517
|
14 |
|
$spOffsets = $writer->getSpOffsets(); |
2518
|
14 |
|
$spTypes = $writer->getSpTypes(); |
2519
|
|
|
// write the neccesary MSODRAWING, OBJ records |
2520
|
|
|
|
2521
|
|
|
// split the Escher stream |
2522
|
14 |
|
$spOffsets[0] = 0; |
2523
|
14 |
|
$nm = count($spOffsets) - 1; // number of shapes excluding first shape |
2524
|
14 |
|
for ($i = 1; $i <= $nm; ++$i) { |
2525
|
|
|
// MSODRAWING record |
2526
|
14 |
|
$record = 0x00EC; // Record identifier |
2527
|
|
|
|
2528
|
|
|
// chunk of Escher stream for one shape |
2529
|
14 |
|
$dataChunk = substr($data, $spOffsets[$i - 1], $spOffsets[$i] - $spOffsets[$i - 1]); |
2530
|
|
|
|
2531
|
14 |
|
$length = strlen($dataChunk); |
2532
|
14 |
|
$header = pack('vv', $record, $length); |
2533
|
|
|
|
2534
|
14 |
|
$this->append($header . $dataChunk); |
2535
|
|
|
|
2536
|
|
|
// OBJ record |
2537
|
14 |
|
$record = 0x005D; // record identifier |
2538
|
14 |
|
$objData = ''; |
2539
|
|
|
|
2540
|
|
|
// ftCmo |
2541
|
14 |
|
if ($spTypes[$i] == 0x00C9) { |
2542
|
|
|
// Add ftCmo (common object data) subobject |
2543
|
3 |
|
$objData .= |
2544
|
3 |
|
pack( |
2545
|
3 |
|
'vvvvvVVV', |
2546
|
3 |
|
0x0015, // 0x0015 = ftCmo |
2547
|
3 |
|
0x0012, // length of ftCmo data |
2548
|
3 |
|
0x0014, // object type, 0x0014 = filter |
2549
|
3 |
|
$i, // object id number, Excel seems to use 1-based index, local for the sheet |
2550
|
3 |
|
0x2101, // option flags, 0x2001 is what OpenOffice.org uses |
2551
|
3 |
|
0, // reserved |
2552
|
3 |
|
0, // reserved |
2553
|
3 |
|
0 // reserved |
2554
|
3 |
|
); |
2555
|
|
|
|
2556
|
|
|
// Add ftSbs Scroll bar subobject |
2557
|
3 |
|
$objData .= pack('vv', 0x00C, 0x0014); |
2558
|
3 |
|
$objData .= pack('H*', '0000000000000000640001000A00000010000100'); |
2559
|
|
|
// Add ftLbsData (List box data) subobject |
2560
|
3 |
|
$objData .= pack('vv', 0x0013, 0x1FEE); |
2561
|
3 |
|
$objData .= pack('H*', '00000000010001030000020008005700'); |
2562
|
|
|
} else { |
2563
|
|
|
// Add ftCmo (common object data) subobject |
2564
|
11 |
|
$objData .= |
2565
|
11 |
|
pack( |
2566
|
11 |
|
'vvvvvVVV', |
2567
|
11 |
|
0x0015, // 0x0015 = ftCmo |
2568
|
11 |
|
0x0012, // length of ftCmo data |
2569
|
11 |
|
0x0008, // object type, 0x0008 = picture |
2570
|
11 |
|
$i, // object id number, Excel seems to use 1-based index, local for the sheet |
2571
|
11 |
|
0x6011, // option flags, 0x6011 is what OpenOffice.org uses |
2572
|
11 |
|
0, // reserved |
2573
|
11 |
|
0, // reserved |
2574
|
11 |
|
0 // reserved |
2575
|
11 |
|
); |
2576
|
|
|
} |
2577
|
|
|
|
2578
|
|
|
// ftEnd |
2579
|
14 |
|
$objData .= |
2580
|
14 |
|
pack( |
2581
|
14 |
|
'vv', |
2582
|
14 |
|
0x0000, // 0x0000 = ftEnd |
2583
|
14 |
|
0x0000 // length of ftEnd data |
2584
|
14 |
|
); |
2585
|
|
|
|
2586
|
14 |
|
$length = strlen($objData); |
2587
|
14 |
|
$header = pack('vv', $record, $length); |
2588
|
14 |
|
$this->append($header . $objData); |
2589
|
|
|
} |
2590
|
|
|
} |
2591
|
|
|
} |
2592
|
|
|
|
2593
|
|
|
/** |
2594
|
|
|
* Store the DATAVALIDATIONS and DATAVALIDATION records. |
2595
|
|
|
*/ |
2596
|
95 |
|
private function writeDataValidity(): void |
2597
|
|
|
{ |
2598
|
|
|
// Datavalidation collection |
2599
|
95 |
|
$dataValidationCollection = $this->phpSheet->getDataValidationCollection(); |
2600
|
|
|
|
2601
|
|
|
// Write data validations? |
2602
|
95 |
|
if (!empty($dataValidationCollection)) { |
2603
|
|
|
// DATAVALIDATIONS record |
2604
|
3 |
|
$record = 0x01B2; // Record identifier |
2605
|
3 |
|
$length = 0x0012; // Bytes to follow |
2606
|
|
|
|
2607
|
3 |
|
$grbit = 0x0000; // Prompt box at cell, no cached validity data at DV records |
2608
|
3 |
|
$horPos = 0x00000000; // Horizontal position of prompt box, if fixed position |
2609
|
3 |
|
$verPos = 0x00000000; // Vertical position of prompt box, if fixed position |
2610
|
3 |
|
$objId = 0xFFFFFFFF; // Object identifier of drop down arrow object, or -1 if not visible |
2611
|
|
|
|
2612
|
3 |
|
$header = pack('vv', $record, $length); |
2613
|
3 |
|
$data = pack('vVVVV', $grbit, $horPos, $verPos, $objId, count($dataValidationCollection)); |
2614
|
3 |
|
$this->append($header . $data); |
2615
|
|
|
|
2616
|
|
|
// DATAVALIDATION records |
2617
|
3 |
|
$record = 0x01BE; // Record identifier |
2618
|
|
|
|
2619
|
3 |
|
foreach ($dataValidationCollection as $cellCoordinate => $dataValidation) { |
2620
|
|
|
// options |
2621
|
3 |
|
$options = 0x00000000; |
2622
|
|
|
|
2623
|
|
|
// data type |
2624
|
3 |
|
$type = CellDataValidation::type($dataValidation); |
2625
|
|
|
|
2626
|
3 |
|
$options |= $type << 0; |
2627
|
|
|
|
2628
|
|
|
// error style |
2629
|
3 |
|
$errorStyle = CellDataValidation::errorStyle($dataValidation); |
2630
|
|
|
|
2631
|
3 |
|
$options |= $errorStyle << 4; |
2632
|
|
|
|
2633
|
|
|
// explicit formula? |
2634
|
3 |
|
if ($type == 0x03 && preg_match('/^\".*\"$/', $dataValidation->getFormula1())) { |
2635
|
2 |
|
$options |= 0x01 << 7; |
2636
|
|
|
} |
2637
|
|
|
|
2638
|
|
|
// empty cells allowed |
2639
|
3 |
|
$options |= $dataValidation->getAllowBlank() << 8; |
2640
|
|
|
|
2641
|
|
|
// show drop down |
2642
|
3 |
|
$options |= (!$dataValidation->getShowDropDown()) << 9; |
2643
|
|
|
|
2644
|
|
|
// show input message |
2645
|
3 |
|
$options |= $dataValidation->getShowInputMessage() << 18; |
2646
|
|
|
|
2647
|
|
|
// show error message |
2648
|
3 |
|
$options |= $dataValidation->getShowErrorMessage() << 19; |
2649
|
|
|
|
2650
|
|
|
// condition operator |
2651
|
3 |
|
$operator = CellDataValidation::operator($dataValidation); |
2652
|
|
|
|
2653
|
3 |
|
$options |= $operator << 20; |
2654
|
|
|
|
2655
|
3 |
|
$data = pack('V', $options); |
2656
|
|
|
|
2657
|
|
|
// prompt title |
2658
|
3 |
|
$promptTitle = $dataValidation->getPromptTitle() !== '' ? |
2659
|
3 |
|
$dataValidation->getPromptTitle() : chr(0); |
2660
|
3 |
|
$data .= StringHelper::UTF8toBIFF8UnicodeLong($promptTitle); |
2661
|
|
|
|
2662
|
|
|
// error title |
2663
|
3 |
|
$errorTitle = $dataValidation->getErrorTitle() !== '' ? |
2664
|
3 |
|
$dataValidation->getErrorTitle() : chr(0); |
2665
|
3 |
|
$data .= StringHelper::UTF8toBIFF8UnicodeLong($errorTitle); |
2666
|
|
|
|
2667
|
|
|
// prompt text |
2668
|
3 |
|
$prompt = $dataValidation->getPrompt() !== '' ? |
2669
|
3 |
|
$dataValidation->getPrompt() : chr(0); |
2670
|
3 |
|
$data .= StringHelper::UTF8toBIFF8UnicodeLong($prompt); |
2671
|
|
|
|
2672
|
|
|
// error text |
2673
|
3 |
|
$error = $dataValidation->getError() !== '' ? |
2674
|
3 |
|
$dataValidation->getError() : chr(0); |
2675
|
3 |
|
$data .= StringHelper::UTF8toBIFF8UnicodeLong($error); |
2676
|
|
|
|
2677
|
|
|
// formula 1 |
2678
|
|
|
try { |
2679
|
3 |
|
$formula1 = $dataValidation->getFormula1(); |
2680
|
3 |
|
if ($type == 0x03) { // list type |
2681
|
3 |
|
$formula1 = str_replace(',', chr(0), $formula1); |
2682
|
|
|
} |
2683
|
3 |
|
$this->parser->parse($formula1); |
2684
|
3 |
|
$formula1 = $this->parser->toReversePolish(); |
2685
|
3 |
|
$sz1 = strlen($formula1); |
2686
|
|
|
} catch (PhpSpreadsheetException $e) { |
2687
|
|
|
$sz1 = 0; |
2688
|
|
|
$formula1 = ''; |
2689
|
|
|
} |
2690
|
3 |
|
$data .= pack('vv', $sz1, 0x0000); |
2691
|
3 |
|
$data .= $formula1; |
2692
|
|
|
|
2693
|
|
|
// formula 2 |
2694
|
|
|
try { |
2695
|
3 |
|
$formula2 = $dataValidation->getFormula2(); |
2696
|
3 |
|
if ($formula2 === '') { |
2697
|
3 |
|
throw new WriterException('No formula2'); |
2698
|
|
|
} |
2699
|
3 |
|
$this->parser->parse($formula2); |
2700
|
3 |
|
$formula2 = $this->parser->toReversePolish(); |
2701
|
3 |
|
$sz2 = strlen($formula2); |
2702
|
3 |
|
} catch (PhpSpreadsheetException) { |
2703
|
3 |
|
$sz2 = 0; |
2704
|
3 |
|
$formula2 = ''; |
2705
|
|
|
} |
2706
|
3 |
|
$data .= pack('vv', $sz2, 0x0000); |
2707
|
3 |
|
$data .= $formula2; |
2708
|
|
|
|
2709
|
|
|
// cell range address list |
2710
|
3 |
|
$data .= pack('v', 0x0001); |
2711
|
3 |
|
$data .= $this->writeBIFF8CellRangeAddressFixed($cellCoordinate); |
2712
|
|
|
|
2713
|
3 |
|
$length = strlen($data); |
2714
|
3 |
|
$header = pack('vv', $record, $length); |
2715
|
|
|
|
2716
|
3 |
|
$this->append($header . $data); |
2717
|
|
|
} |
2718
|
|
|
} |
2719
|
|
|
} |
2720
|
|
|
|
2721
|
|
|
/** |
2722
|
|
|
* Write PLV Record. |
2723
|
|
|
*/ |
2724
|
95 |
|
private function writePageLayoutView(): void |
2725
|
|
|
{ |
2726
|
95 |
|
$record = 0x088B; // Record identifier |
2727
|
95 |
|
$length = 0x0010; // Bytes to follow |
2728
|
|
|
|
2729
|
95 |
|
$rt = 0x088B; // 2 |
2730
|
95 |
|
$grbitFrt = 0x0000; // 2 |
2731
|
|
|
//$reserved = 0x0000000000000000; // 8 |
2732
|
95 |
|
$wScalvePLV = $this->phpSheet->getSheetView()->getZoomScale(); // 2 |
2733
|
|
|
|
2734
|
|
|
// The options flags that comprise $grbit |
2735
|
95 |
|
if ($this->phpSheet->getSheetView()->getView() == SheetView::SHEETVIEW_PAGE_LAYOUT) { |
2736
|
1 |
|
$fPageLayoutView = 1; |
2737
|
|
|
} else { |
2738
|
94 |
|
$fPageLayoutView = 0; |
2739
|
|
|
} |
2740
|
95 |
|
$fRulerVisible = 0; |
2741
|
95 |
|
$fWhitespaceHidden = 0; |
2742
|
|
|
|
2743
|
95 |
|
$grbit = $fPageLayoutView; // 2 |
2744
|
95 |
|
$grbit |= $fRulerVisible << 1; |
2745
|
95 |
|
$grbit |= $fWhitespaceHidden << 3; |
2746
|
|
|
|
2747
|
95 |
|
$header = pack('vv', $record, $length); |
2748
|
95 |
|
$data = pack('vvVVvv', $rt, $grbitFrt, 0x00000000, 0x00000000, $wScalvePLV, $grbit); |
2749
|
95 |
|
$this->append($header . $data); |
2750
|
|
|
} |
2751
|
|
|
|
2752
|
|
|
/** |
2753
|
|
|
* Write CFRule Record. |
2754
|
|
|
*/ |
2755
|
5 |
|
private function writeCFRule( |
2756
|
|
|
ConditionalHelper $conditionalFormulaHelper, |
2757
|
|
|
Conditional $conditional, |
2758
|
|
|
string $cellRange |
2759
|
|
|
): void { |
2760
|
5 |
|
$record = 0x01B1; // Record identifier |
2761
|
5 |
|
$type = null; // Type of the CF |
2762
|
5 |
|
$operatorType = null; // Comparison operator |
2763
|
|
|
|
2764
|
5 |
|
if ($conditional->getConditionType() == Conditional::CONDITION_EXPRESSION) { |
2765
|
1 |
|
$type = 0x02; |
2766
|
1 |
|
$operatorType = 0x00; |
2767
|
4 |
|
} elseif ($conditional->getConditionType() == Conditional::CONDITION_CELLIS) { |
2768
|
4 |
|
$type = 0x01; |
2769
|
|
|
|
2770
|
4 |
|
switch ($conditional->getOperatorType()) { |
2771
|
|
|
case Conditional::OPERATOR_NONE: |
2772
|
|
|
$operatorType = 0x00; |
2773
|
|
|
|
2774
|
|
|
break; |
2775
|
|
|
case Conditional::OPERATOR_EQUAL: |
2776
|
2 |
|
$operatorType = 0x03; |
2777
|
|
|
|
2778
|
2 |
|
break; |
2779
|
|
|
case Conditional::OPERATOR_GREATERTHAN: |
2780
|
1 |
|
$operatorType = 0x05; |
2781
|
|
|
|
2782
|
1 |
|
break; |
2783
|
|
|
case Conditional::OPERATOR_GREATERTHANOREQUAL: |
2784
|
2 |
|
$operatorType = 0x07; |
2785
|
|
|
|
2786
|
2 |
|
break; |
2787
|
|
|
case Conditional::OPERATOR_LESSTHAN: |
2788
|
3 |
|
$operatorType = 0x06; |
2789
|
|
|
|
2790
|
3 |
|
break; |
2791
|
|
|
case Conditional::OPERATOR_LESSTHANOREQUAL: |
2792
|
|
|
$operatorType = 0x08; |
2793
|
|
|
|
2794
|
|
|
break; |
2795
|
|
|
case Conditional::OPERATOR_NOTEQUAL: |
2796
|
|
|
$operatorType = 0x04; |
2797
|
|
|
|
2798
|
|
|
break; |
2799
|
|
|
case Conditional::OPERATOR_BETWEEN: |
2800
|
2 |
|
$operatorType = 0x01; |
2801
|
|
|
|
2802
|
2 |
|
break; |
2803
|
|
|
// not OPERATOR_NOTBETWEEN 0x02 |
2804
|
|
|
} |
2805
|
|
|
} |
2806
|
|
|
|
2807
|
|
|
// $szValue1 : size of the formula data for first value or formula |
2808
|
|
|
// $szValue2 : size of the formula data for second value or formula |
2809
|
5 |
|
$arrConditions = $conditional->getConditions(); |
2810
|
5 |
|
$numConditions = count($arrConditions); |
2811
|
|
|
|
2812
|
5 |
|
$szValue1 = 0x0000; |
2813
|
5 |
|
$szValue2 = 0x0000; |
2814
|
5 |
|
$operand1 = null; |
2815
|
5 |
|
$operand2 = null; |
2816
|
|
|
|
2817
|
5 |
|
if ($numConditions === 1) { |
2818
|
5 |
|
$conditionalFormulaHelper->processCondition($arrConditions[0], $cellRange); |
2819
|
5 |
|
$szValue1 = $conditionalFormulaHelper->size(); |
2820
|
5 |
|
$operand1 = $conditionalFormulaHelper->tokens(); |
2821
|
2 |
|
} elseif ($numConditions === 2 && ($conditional->getOperatorType() === Conditional::OPERATOR_BETWEEN)) { |
2822
|
2 |
|
$conditionalFormulaHelper->processCondition($arrConditions[0], $cellRange); |
2823
|
2 |
|
$szValue1 = $conditionalFormulaHelper->size(); |
2824
|
2 |
|
$operand1 = $conditionalFormulaHelper->tokens(); |
2825
|
2 |
|
$conditionalFormulaHelper->processCondition($arrConditions[1], $cellRange); |
2826
|
2 |
|
$szValue2 = $conditionalFormulaHelper->size(); |
2827
|
2 |
|
$operand2 = $conditionalFormulaHelper->tokens(); |
2828
|
|
|
} |
2829
|
|
|
|
2830
|
|
|
// $flags : Option flags |
2831
|
|
|
// Alignment |
2832
|
5 |
|
$bAlignHz = ($conditional->getStyle()->getAlignment()->getHorizontal() === null ? 1 : 0); |
2833
|
5 |
|
$bAlignVt = ($conditional->getStyle()->getAlignment()->getVertical() === null ? 1 : 0); |
2834
|
5 |
|
$bAlignWrapTx = ($conditional->getStyle()->getAlignment()->getWrapText() === false ? 1 : 0); |
2835
|
5 |
|
$bTxRotation = ($conditional->getStyle()->getAlignment()->getTextRotation() === null ? 1 : 0); |
2836
|
5 |
|
$bIndent = ($conditional->getStyle()->getAlignment()->getIndent() === 0 ? 1 : 0); |
2837
|
5 |
|
$bShrinkToFit = ($conditional->getStyle()->getAlignment()->getShrinkToFit() === false ? 1 : 0); |
2838
|
5 |
|
if ($bAlignHz == 0 || $bAlignVt == 0 || $bAlignWrapTx == 0 || $bTxRotation == 0 || $bIndent == 0 || $bShrinkToFit == 0) { |
2839
|
|
|
$bFormatAlign = 1; |
2840
|
|
|
} else { |
2841
|
5 |
|
$bFormatAlign = 0; |
2842
|
|
|
} |
2843
|
|
|
// Protection |
2844
|
5 |
|
$bProtLocked = ($conditional->getStyle()->getProtection()->getLocked() == null ? 1 : 0); |
|
|
|
|
2845
|
5 |
|
$bProtHidden = ($conditional->getStyle()->getProtection()->getHidden() == null ? 1 : 0); |
|
|
|
|
2846
|
5 |
|
if ($bProtLocked == 0 || $bProtHidden == 0) { |
2847
|
|
|
$bFormatProt = 1; |
2848
|
|
|
} else { |
2849
|
5 |
|
$bFormatProt = 0; |
2850
|
|
|
} |
2851
|
|
|
// Border |
2852
|
5 |
|
$bBorderLeft = ($conditional->getStyle()->getBorders()->getLeft()->getBorderStyle() !== Border::BORDER_OMIT) ? 1 : 0; |
2853
|
5 |
|
$bBorderRight = ($conditional->getStyle()->getBorders()->getRight()->getBorderStyle() !== Border::BORDER_OMIT) ? 1 : 0; |
2854
|
5 |
|
$bBorderTop = ($conditional->getStyle()->getBorders()->getTop()->getBorderStyle() !== Border::BORDER_OMIT) ? 1 : 0; |
2855
|
5 |
|
$bBorderBottom = ($conditional->getStyle()->getBorders()->getBottom()->getBorderStyle() !== Border::BORDER_OMIT) ? 1 : 0; |
2856
|
5 |
|
if ($bBorderLeft === 1 || $bBorderRight === 1 || $bBorderTop === 1 || $bBorderBottom === 1) { |
2857
|
|
|
$bFormatBorder = 1; |
2858
|
|
|
} else { |
2859
|
5 |
|
$bFormatBorder = 0; |
2860
|
|
|
} |
2861
|
|
|
// Pattern |
2862
|
5 |
|
$bFillStyle = ($conditional->getStyle()->getFill()->getFillType() === null ? 0 : 1); |
2863
|
5 |
|
$bFillColor = ($conditional->getStyle()->getFill()->getStartColor()->getARGB() === null ? 0 : 1); |
2864
|
5 |
|
$bFillColorBg = ($conditional->getStyle()->getFill()->getEndColor()->getARGB() === null ? 0 : 1); |
2865
|
5 |
|
if ($bFillStyle == 1 || $bFillColor == 1 || $bFillColorBg == 1) { |
2866
|
3 |
|
$bFormatFill = 1; |
2867
|
|
|
} else { |
2868
|
2 |
|
$bFormatFill = 0; |
2869
|
|
|
} |
2870
|
|
|
// Font |
2871
|
|
|
if ( |
2872
|
5 |
|
$conditional->getStyle()->getFont()->getName() !== null |
2873
|
5 |
|
|| $conditional->getStyle()->getFont()->getSize() !== null |
2874
|
5 |
|
|| $conditional->getStyle()->getFont()->getBold() !== null |
2875
|
5 |
|
|| $conditional->getStyle()->getFont()->getItalic() !== null |
2876
|
5 |
|
|| $conditional->getStyle()->getFont()->getSuperscript() !== null |
2877
|
5 |
|
|| $conditional->getStyle()->getFont()->getSubscript() !== null |
2878
|
5 |
|
|| $conditional->getStyle()->getFont()->getUnderline() !== null |
2879
|
5 |
|
|| $conditional->getStyle()->getFont()->getStrikethrough() !== null |
2880
|
5 |
|
|| $conditional->getStyle()->getFont()->getColor()->getARGB() !== null |
2881
|
|
|
) { |
2882
|
5 |
|
$bFormatFont = 1; |
2883
|
|
|
} else { |
2884
|
|
|
$bFormatFont = 0; |
2885
|
|
|
} |
2886
|
|
|
// Alignment |
2887
|
5 |
|
$flags = 0; |
2888
|
5 |
|
$flags |= (1 == $bAlignHz ? 0x00000001 : 0); |
2889
|
5 |
|
$flags |= (1 == $bAlignVt ? 0x00000002 : 0); |
2890
|
5 |
|
$flags |= (1 == $bAlignWrapTx ? 0x00000004 : 0); |
2891
|
5 |
|
$flags |= (1 == $bTxRotation ? 0x00000008 : 0); |
2892
|
|
|
// Justify last line flag |
2893
|
5 |
|
$flags |= (1 == self::$always1 ? 0x00000010 : 0); |
2894
|
5 |
|
$flags |= (1 == $bIndent ? 0x00000020 : 0); |
2895
|
5 |
|
$flags |= (1 == $bShrinkToFit ? 0x00000040 : 0); |
2896
|
|
|
// Default |
2897
|
5 |
|
$flags |= (1 == self::$always1 ? 0x00000080 : 0); |
2898
|
|
|
// Protection |
2899
|
5 |
|
$flags |= (1 == $bProtLocked ? 0x00000100 : 0); |
2900
|
5 |
|
$flags |= (1 == $bProtHidden ? 0x00000200 : 0); |
2901
|
|
|
// Border |
2902
|
5 |
|
$flags |= (1 == $bBorderLeft ? 0x00000400 : 0); |
2903
|
5 |
|
$flags |= (1 == $bBorderRight ? 0x00000800 : 0); |
2904
|
5 |
|
$flags |= (1 == $bBorderTop ? 0x00001000 : 0); |
2905
|
5 |
|
$flags |= (1 == $bBorderBottom ? 0x00002000 : 0); |
2906
|
5 |
|
$flags |= (1 == self::$always1 ? 0x00004000 : 0); // Top left to Bottom right border |
2907
|
5 |
|
$flags |= (1 == self::$always1 ? 0x00008000 : 0); // Bottom left to Top right border |
2908
|
|
|
// Pattern |
2909
|
5 |
|
$flags |= (1 == $bFillStyle ? 0x00010000 : 0); |
2910
|
5 |
|
$flags |= (1 == $bFillColor ? 0x00020000 : 0); |
2911
|
5 |
|
$flags |= (1 == $bFillColorBg ? 0x00040000 : 0); |
2912
|
5 |
|
$flags |= (1 == self::$always1 ? 0x00380000 : 0); |
2913
|
|
|
// Font |
2914
|
5 |
|
$flags |= (1 == $bFormatFont ? 0x04000000 : 0); |
2915
|
|
|
// Alignment: |
2916
|
5 |
|
$flags |= (1 == $bFormatAlign ? 0x08000000 : 0); |
2917
|
|
|
// Border |
2918
|
5 |
|
$flags |= (1 == $bFormatBorder ? 0x10000000 : 0); |
2919
|
|
|
// Pattern |
2920
|
5 |
|
$flags |= (1 == $bFormatFill ? 0x20000000 : 0); |
2921
|
|
|
// Protection |
2922
|
5 |
|
$flags |= (1 == $bFormatProt ? 0x40000000 : 0); |
2923
|
|
|
// Text direction |
2924
|
5 |
|
$flags |= (1 == self::$always0 ? 0x80000000 : 0); |
2925
|
|
|
|
2926
|
5 |
|
$dataBlockFont = null; |
2927
|
5 |
|
$dataBlockAlign = null; |
2928
|
5 |
|
$dataBlockBorder = null; |
2929
|
5 |
|
$dataBlockFill = null; |
2930
|
|
|
|
2931
|
|
|
// Data Blocks |
2932
|
5 |
|
if ($bFormatFont == 1) { |
2933
|
|
|
// Font Name |
2934
|
5 |
|
if ($conditional->getStyle()->getFont()->getName() === null) { |
2935
|
5 |
|
$dataBlockFont = pack('VVVVVVVV', 0x00000000, 0x00000000, 0x00000000, 0x00000000, 0x00000000, 0x00000000, 0x00000000, 0x00000000); |
2936
|
5 |
|
$dataBlockFont .= pack('VVVVVVVV', 0x00000000, 0x00000000, 0x00000000, 0x00000000, 0x00000000, 0x00000000, 0x00000000, 0x00000000); |
2937
|
|
|
} else { |
2938
|
|
|
$dataBlockFont = StringHelper::UTF8toBIFF8UnicodeLong($conditional->getStyle()->getFont()->getName()); |
2939
|
|
|
} |
2940
|
|
|
// Font Size |
2941
|
5 |
|
if ($conditional->getStyle()->getFont()->getSize() === null) { |
2942
|
5 |
|
$dataBlockFont .= pack('V', 20 * 11); |
2943
|
|
|
} else { |
2944
|
|
|
$dataBlockFont .= pack('V', 20 * $conditional->getStyle()->getFont()->getSize()); |
2945
|
|
|
} |
2946
|
|
|
// Font Options |
2947
|
5 |
|
$dataBlockFont .= pack('V', 0); |
2948
|
|
|
// Font weight |
2949
|
5 |
|
if ($conditional->getStyle()->getFont()->getBold() === true) { |
2950
|
1 |
|
$dataBlockFont .= pack('v', 0x02BC); |
2951
|
|
|
} else { |
2952
|
5 |
|
$dataBlockFont .= pack('v', 0x0190); |
2953
|
|
|
} |
2954
|
|
|
// Escapement type |
2955
|
5 |
|
if ($conditional->getStyle()->getFont()->getSubscript() === true) { |
2956
|
|
|
$dataBlockFont .= pack('v', 0x02); |
2957
|
|
|
$fontEscapement = 0; |
2958
|
5 |
|
} elseif ($conditional->getStyle()->getFont()->getSuperscript() === true) { |
2959
|
|
|
$dataBlockFont .= pack('v', 0x01); |
2960
|
|
|
$fontEscapement = 0; |
2961
|
|
|
} else { |
2962
|
5 |
|
$dataBlockFont .= pack('v', 0x00); |
2963
|
5 |
|
$fontEscapement = 1; |
2964
|
|
|
} |
2965
|
|
|
// Underline type |
2966
|
5 |
|
switch ($conditional->getStyle()->getFont()->getUnderline()) { |
2967
|
|
|
case \PhpOffice\PhpSpreadsheet\Style\Font::UNDERLINE_NONE: |
2968
|
|
|
$dataBlockFont .= pack('C', 0x00); |
2969
|
|
|
$fontUnderline = 0; |
2970
|
|
|
|
2971
|
|
|
break; |
2972
|
|
|
case \PhpOffice\PhpSpreadsheet\Style\Font::UNDERLINE_DOUBLE: |
2973
|
|
|
$dataBlockFont .= pack('C', 0x02); |
2974
|
|
|
$fontUnderline = 0; |
2975
|
|
|
|
2976
|
|
|
break; |
2977
|
|
|
case \PhpOffice\PhpSpreadsheet\Style\Font::UNDERLINE_DOUBLEACCOUNTING: |
2978
|
|
|
$dataBlockFont .= pack('C', 0x22); |
2979
|
|
|
$fontUnderline = 0; |
2980
|
|
|
|
2981
|
|
|
break; |
2982
|
|
|
case \PhpOffice\PhpSpreadsheet\Style\Font::UNDERLINE_SINGLE: |
2983
|
|
|
$dataBlockFont .= pack('C', 0x01); |
2984
|
|
|
$fontUnderline = 0; |
2985
|
|
|
|
2986
|
|
|
break; |
2987
|
|
|
case \PhpOffice\PhpSpreadsheet\Style\Font::UNDERLINE_SINGLEACCOUNTING: |
2988
|
|
|
$dataBlockFont .= pack('C', 0x21); |
2989
|
|
|
$fontUnderline = 0; |
2990
|
|
|
|
2991
|
|
|
break; |
2992
|
|
|
default: |
2993
|
5 |
|
$dataBlockFont .= pack('C', 0x00); |
2994
|
5 |
|
$fontUnderline = 1; |
2995
|
|
|
|
2996
|
5 |
|
break; |
2997
|
|
|
} |
2998
|
|
|
// Not used (3) |
2999
|
5 |
|
$dataBlockFont .= pack('vC', 0x0000, 0x00); |
3000
|
|
|
// Font color index |
3001
|
5 |
|
$colorIdx = Style\ColorMap::lookup($conditional->getStyle()->getFont()->getColor(), 0x00); |
3002
|
|
|
|
3003
|
5 |
|
$dataBlockFont .= pack('V', $colorIdx); |
3004
|
|
|
// Not used (4) |
3005
|
5 |
|
$dataBlockFont .= pack('V', 0x00000000); |
3006
|
|
|
// Options flags for modified font attributes |
3007
|
5 |
|
$optionsFlags = 0; |
3008
|
5 |
|
$optionsFlagsBold = ($conditional->getStyle()->getFont()->getBold() === null ? 1 : 0); |
3009
|
5 |
|
$optionsFlags |= (1 == $optionsFlagsBold ? 0x00000002 : 0); |
3010
|
5 |
|
$optionsFlags |= (1 == self::$always1 ? 0x00000008 : 0); |
3011
|
5 |
|
$optionsFlags |= (1 == self::$always1 ? 0x00000010 : 0); |
3012
|
5 |
|
$optionsFlags |= (1 == self::$always0 ? 0x00000020 : 0); |
3013
|
5 |
|
$optionsFlags |= (1 == self::$always1 ? 0x00000080 : 0); |
3014
|
5 |
|
$dataBlockFont .= pack('V', $optionsFlags); |
3015
|
|
|
// Escapement type |
3016
|
5 |
|
$dataBlockFont .= pack('V', $fontEscapement); |
3017
|
|
|
// Underline type |
3018
|
5 |
|
$dataBlockFont .= pack('V', $fontUnderline); |
3019
|
|
|
// Always |
3020
|
5 |
|
$dataBlockFont .= pack('V', 0x00000000); |
3021
|
|
|
// Always |
3022
|
5 |
|
$dataBlockFont .= pack('V', 0x00000000); |
3023
|
|
|
// Not used (8) |
3024
|
5 |
|
$dataBlockFont .= pack('VV', 0x00000000, 0x00000000); |
3025
|
|
|
// Always |
3026
|
5 |
|
$dataBlockFont .= pack('v', 0x0001); |
3027
|
|
|
} |
3028
|
5 |
|
if ($bFormatAlign === 1) { |
3029
|
|
|
// Alignment and text break |
3030
|
|
|
$blockAlign = Style\CellAlignment::horizontal($conditional->getStyle()->getAlignment()); |
3031
|
|
|
$blockAlign |= Style\CellAlignment::wrap($conditional->getStyle()->getAlignment()) << 3; |
3032
|
|
|
$blockAlign |= Style\CellAlignment::vertical($conditional->getStyle()->getAlignment()) << 4; |
3033
|
|
|
$blockAlign |= 0 << 7; |
3034
|
|
|
|
3035
|
|
|
// Text rotation angle |
3036
|
|
|
$blockRotation = $conditional->getStyle()->getAlignment()->getTextRotation(); |
3037
|
|
|
|
3038
|
|
|
// Indentation |
3039
|
|
|
$blockIndent = $conditional->getStyle()->getAlignment()->getIndent(); |
3040
|
|
|
if ($conditional->getStyle()->getAlignment()->getShrinkToFit() === true) { |
3041
|
|
|
$blockIndent |= 1 << 4; |
3042
|
|
|
} else { |
3043
|
|
|
$blockIndent |= 0 << 4; |
3044
|
|
|
} |
3045
|
|
|
$blockIndent |= 0 << 6; |
3046
|
|
|
|
3047
|
|
|
// Relative indentation |
3048
|
|
|
$blockIndentRelative = 255; |
3049
|
|
|
|
3050
|
|
|
$dataBlockAlign = pack('CCvvv', $blockAlign, $blockRotation, $blockIndent, $blockIndentRelative, 0x0000); |
3051
|
|
|
} |
3052
|
5 |
|
if ($bFormatBorder === 1) { |
3053
|
|
|
$blockLineStyle = Style\CellBorder::style($conditional->getStyle()->getBorders()->getLeft()); |
3054
|
|
|
$blockLineStyle |= Style\CellBorder::style($conditional->getStyle()->getBorders()->getRight()) << 4; |
3055
|
|
|
$blockLineStyle |= Style\CellBorder::style($conditional->getStyle()->getBorders()->getTop()) << 8; |
3056
|
|
|
$blockLineStyle |= Style\CellBorder::style($conditional->getStyle()->getBorders()->getBottom()) << 12; |
3057
|
|
|
|
3058
|
|
|
// TODO writeCFRule() => $blockLineStyle => Index Color for left line |
3059
|
|
|
// TODO writeCFRule() => $blockLineStyle => Index Color for right line |
3060
|
|
|
// TODO writeCFRule() => $blockLineStyle => Top-left to bottom-right on/off |
3061
|
|
|
// TODO writeCFRule() => $blockLineStyle => Bottom-left to top-right on/off |
3062
|
|
|
$blockColor = 0; |
3063
|
|
|
// TODO writeCFRule() => $blockColor => Index Color for top line |
3064
|
|
|
// TODO writeCFRule() => $blockColor => Index Color for bottom line |
3065
|
|
|
// TODO writeCFRule() => $blockColor => Index Color for diagonal line |
3066
|
|
|
$blockColor |= Style\CellBorder::style($conditional->getStyle()->getBorders()->getDiagonal()) << 21; |
3067
|
|
|
$dataBlockBorder = pack('vv', $blockLineStyle, $blockColor); |
3068
|
|
|
} |
3069
|
5 |
|
if ($bFormatFill === 1) { |
3070
|
|
|
// Fill Pattern Style |
3071
|
3 |
|
$blockFillPatternStyle = Style\CellFill::style($conditional->getStyle()->getFill()); |
3072
|
|
|
// Background Color |
3073
|
3 |
|
$colorIdxBg = Style\ColorMap::lookup($conditional->getStyle()->getFill()->getStartColor(), 0x41); |
3074
|
|
|
// Foreground Color |
3075
|
3 |
|
$colorIdxFg = Style\ColorMap::lookup($conditional->getStyle()->getFill()->getEndColor(), 0x40); |
3076
|
|
|
|
3077
|
3 |
|
$dataBlockFill = pack('v', $blockFillPatternStyle); |
3078
|
3 |
|
$dataBlockFill .= pack('v', $colorIdxFg | ($colorIdxBg << 7)); |
3079
|
|
|
} |
3080
|
|
|
|
3081
|
5 |
|
$data = pack('CCvvVv', $type, $operatorType, $szValue1, $szValue2, $flags, 0x0000); |
3082
|
5 |
|
if ($bFormatFont === 1) { // Block Formatting : OK |
3083
|
5 |
|
$data .= $dataBlockFont; |
3084
|
|
|
} |
3085
|
5 |
|
if ($bFormatAlign === 1) { |
3086
|
|
|
$data .= $dataBlockAlign; |
3087
|
|
|
} |
3088
|
5 |
|
if ($bFormatBorder === 1) { |
3089
|
|
|
$data .= $dataBlockBorder; |
3090
|
|
|
} |
3091
|
5 |
|
if ($bFormatFill === 1) { // Block Formatting : OK |
3092
|
3 |
|
$data .= $dataBlockFill; |
3093
|
|
|
} |
3094
|
5 |
|
if ($bFormatProt == 1) { |
3095
|
|
|
$data .= $this->getDataBlockProtection($conditional); |
3096
|
|
|
} |
3097
|
5 |
|
if ($operand1 !== null) { |
3098
|
5 |
|
$data .= $operand1; |
3099
|
|
|
} |
3100
|
5 |
|
if ($operand2 !== null) { |
3101
|
2 |
|
$data .= $operand2; |
3102
|
|
|
} |
3103
|
5 |
|
$header = pack('vv', $record, strlen($data)); |
3104
|
5 |
|
$this->append($header . $data); |
3105
|
|
|
} |
3106
|
|
|
|
3107
|
|
|
/** |
3108
|
|
|
* Write CFHeader record. |
3109
|
|
|
* |
3110
|
|
|
* @param Conditional[] $conditionalStyles |
3111
|
|
|
*/ |
3112
|
5 |
|
private function writeCFHeader(string $cellCoordinate, array $conditionalStyles): bool |
3113
|
|
|
{ |
3114
|
5 |
|
$record = 0x01B0; // Record identifier |
3115
|
5 |
|
$length = 0x0016; // Bytes to follow |
3116
|
|
|
|
3117
|
5 |
|
$numColumnMin = null; |
3118
|
5 |
|
$numColumnMax = null; |
3119
|
5 |
|
$numRowMin = null; |
3120
|
5 |
|
$numRowMax = null; |
3121
|
|
|
|
3122
|
5 |
|
$arrConditional = []; |
3123
|
5 |
|
foreach ($conditionalStyles as $conditional) { |
3124
|
5 |
|
if (!in_array($conditional->getHashCode(), $arrConditional)) { |
3125
|
5 |
|
$arrConditional[] = $conditional->getHashCode(); |
3126
|
|
|
} |
3127
|
|
|
// Cells |
3128
|
5 |
|
$rangeCoordinates = Coordinate::rangeBoundaries($cellCoordinate); |
3129
|
5 |
|
if ($numColumnMin === null || ($numColumnMin > $rangeCoordinates[0][0])) { |
3130
|
5 |
|
$numColumnMin = $rangeCoordinates[0][0]; |
3131
|
|
|
} |
3132
|
5 |
|
if ($numColumnMax === null || ($numColumnMax < $rangeCoordinates[1][0])) { |
3133
|
5 |
|
$numColumnMax = $rangeCoordinates[1][0]; |
3134
|
|
|
} |
3135
|
5 |
|
if ($numRowMin === null || ($numRowMin > $rangeCoordinates[0][1])) { |
3136
|
5 |
|
$numRowMin = (int) $rangeCoordinates[0][1]; |
3137
|
|
|
} |
3138
|
5 |
|
if ($numRowMax === null || ($numRowMax < $rangeCoordinates[1][1])) { |
3139
|
5 |
|
$numRowMax = (int) $rangeCoordinates[1][1]; |
3140
|
|
|
} |
3141
|
|
|
} |
3142
|
|
|
|
3143
|
5 |
|
if (count($arrConditional) === 0) { |
3144
|
|
|
return false; |
3145
|
|
|
} |
3146
|
|
|
|
3147
|
5 |
|
$needRedraw = 1; |
3148
|
5 |
|
$cellRange = pack('vvvv', $numRowMin - 1, $numRowMax - 1, $numColumnMin - 1, $numColumnMax - 1); |
3149
|
|
|
|
3150
|
5 |
|
$header = pack('vv', $record, $length); |
3151
|
5 |
|
$data = pack('vv', count($arrConditional), $needRedraw); |
3152
|
5 |
|
$data .= $cellRange; |
3153
|
5 |
|
$data .= pack('v', 0x0001); |
3154
|
5 |
|
$data .= $cellRange; |
3155
|
5 |
|
$this->append($header . $data); |
3156
|
|
|
|
3157
|
5 |
|
return true; |
3158
|
|
|
} |
3159
|
|
|
|
3160
|
|
|
private function getDataBlockProtection(Conditional $conditional): int |
3161
|
|
|
{ |
3162
|
|
|
$dataBlockProtection = 0; |
3163
|
|
|
if ($conditional->getStyle()->getProtection()->getLocked() == Protection::PROTECTION_PROTECTED) { |
3164
|
|
|
$dataBlockProtection = 1; |
3165
|
|
|
} |
3166
|
|
|
if ($conditional->getStyle()->getProtection()->getHidden() == Protection::PROTECTION_PROTECTED) { |
3167
|
|
|
$dataBlockProtection = 1 << 1; |
3168
|
|
|
} |
3169
|
|
|
|
3170
|
|
|
return $dataBlockProtection; |
3171
|
|
|
} |
3172
|
|
|
} |
3173
|
|
|
|
This check looks for function or method calls that always return null and whose return value is used.
The method
getObject()
can return nothing but null, so it makes no sense to use the return value.The reason is most likely that a function or method is imcomplete or has been reduced for debug purposes.