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

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

1,191 lines 40.6 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2
3 namespace PhpOffice\PhpSpreadsheet\Writer\Xls;
4
5 use PhpOffice\PhpSpreadsheet\Calculation\Calculation;
6 use PhpOffice\PhpSpreadsheet\Cell\Coordinate;
7 use PhpOffice\PhpSpreadsheet\DefinedName;
8 use PhpOffice\PhpSpreadsheet\Exception as PhpSpreadsheetException;
9 use PhpOffice\PhpSpreadsheet\Shared\Date;
10 use PhpOffice\PhpSpreadsheet\Shared\StringHelper;
11 use PhpOffice\PhpSpreadsheet\Spreadsheet;
12 use PhpOffice\PhpSpreadsheet\Style\Style;
13
14 // Original file header of PEAR::Spreadsheet_Excel_Writer_Workbook (used as the base for this class):
15 // -----------------------------------------------------------------------------------------
16 // /*
17 // * Module written/ported by Xavier Noguer <xnoguer@rezebra.com>
18 // *
19 // * The majority of this is _NOT_ my code. I simply ported it from the
20 // * PERL Spreadsheet::WriteExcel module.
21 // *
22 // * The author of the Spreadsheet::WriteExcel module is John McNamara
23 // * <jmcnamara@cpan.org>
24 // *
25 // * I _DO_ maintain this code, and John McNamara has nothing to do with the
26 // * porting of this code to PHP. Any questions directly related to this
27 // * class library should be directed to me.
28 // *
29 // * License Information:
30 // *
31 // * Spreadsheet_Excel_Writer: A library for generating Excel Spreadsheets
32 // * Copyright (c) 2002-2003 Xavier Noguer xnoguer@rezebra.com
33 // *
34 // * This library is free software; you can redistribute it and/or
35 // * modify it under the terms of the GNU Lesser General Public
36 // * License as published by the Free Software Foundation; either
37 // * version 2.1 of the License, or (at your option) any later version.
38 // *
39 // * This library is distributed in the hope that it will be useful,
40 // * but WITHOUT ANY WARRANTY; without even the implied warranty of
41 // * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
42 // * Lesser General Public License for more details.
43 // *
44 // * You should have received a copy of the GNU Lesser General Public
45 // * License along with this library; if not, write to the Free Software
46 // * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
47 // */
48 class Workbook extends BIFFwriter
49 {
50 /**
51 * Formula parser.
52 *
53 * @var \PhpOffice\PhpSpreadsheet\Writer\Xls\Parser
54 */
55 private $parser;
56
57 /**
58 * The BIFF file size for the workbook. Not currently used.
59 *
60 * @var int
61 *
62 * @see calcSheetOffsets()
63 */
64 private $biffSize; // @phpstan-ignore-line
65
66 /**
67 * XF Writers.
68 *
69 * @var \PhpOffice\PhpSpreadsheet\Writer\Xls\Xf[]
70 */
71 private $xfWriters = [];
72
73 /**
74 * Array containing the colour palette.
75 *
76 * @var array
77 */
78 private $palette;
79
80 /**
81 * The codepage indicates the text encoding used for strings.
82 *
83 * @var int
84 */
85 private $codepage;
86
87 /**
88 * The country code used for localization.
89 *
90 * @var int
91 */
92 private $countryCode;
93
94 /**
95 * Workbook.
96 *
97 * @var Spreadsheet
98 */
99 private $spreadsheet;
100
101 /**
102 * Fonts writers.
103 *
104 * @var Font[]
105 */
106 private $fontWriters = [];
107
108 /**
109 * Added fonts. Maps from font's hash => index in workbook.
110 *
111 * @var array
112 */
113 private $addedFonts = [];
114
115 /**
116 * Shared number formats.
117 *
118 * @var array
119 */
120 private $numberFormats = [];
121
122 /**
123 * Added number formats. Maps from numberFormat's hash => index in workbook.
124 *
125 * @var array
126 */
127 private $addedNumberFormats = [];
128
129 /**
130 * Sizes of the binary worksheet streams.
131 *
132 * @var array
133 */
134 private $worksheetSizes = [];
135
136 /**
137 * Offsets of the binary worksheet streams relative to the start of the global workbook stream.
138 *
139 * @var array
140 */
141 private $worksheetOffsets = [];
142
143 /**
144 * Total number of shared strings in workbook.
145 *
146 * @var int
147 */
148 private $stringTotal;
149
150 /**
151 * Number of unique shared strings in workbook.
152 *
153 * @var int
154 */
155 private $stringUnique;
156
157 /**
158 * Array of unique shared strings in workbook.
159 *
160 * @var array
161 */
162 private $stringTable;
163
164 /**
165 * Color cache.
166 *
167 * @var array
168 */
169 private $colors;
170
171 /**
172 * Escher object corresponding to MSODRAWINGGROUP.
173 *
174 * @var null|\PhpOffice\PhpSpreadsheet\Shared\Escher
175 */
176 private $escher;
177
178 /** @var mixed */
179 private static $scrutinizerFalse = false;
180
181 /**
182 * Class constructor.
183 *
184 * @param Spreadsheet $spreadsheet The Workbook
185 * @param int $str_total Total number of strings
186 * @param int $str_unique Total number of unique strings
187 * @param array $str_table String Table
188 * @param array $colors Colour Table
189 * @param Parser $parser The formula parser created for the Workbook
190 */
191 public function __construct(Spreadsheet $spreadsheet, &$str_total, &$str_unique, &$str_table, &$colors, Parser $parser)
192 {
193 // It needs to call its parent's constructor explicitly
194 parent::__construct();
195
196 $this->parser = $parser;
197 $this->biffSize = 0;
198 $this->palette = [];
199 $this->countryCode = -1;
200
201 $this->stringTotal = &$str_total;
202 $this->stringUnique = &$str_unique;
203 $this->stringTable = &$str_table;
204 $this->colors = &$colors;
205 $this->setPaletteXl97();
206
207 $this->spreadsheet = $spreadsheet;
208
209 $this->codepage = 0x04B0;
210
211 // Add empty sheets and Build color cache
212 $countSheets = $spreadsheet->getSheetCount();
213 for ($i = 0; $i < $countSheets; ++$i) {
214 $phpSheet = $spreadsheet->getSheet($i);
215
216 $this->parser->setExtSheet($phpSheet->getTitle(), $i); // Register worksheet name with parser
217
218 $supbook_index = 0x00;
219 $ref = pack('vvv', $supbook_index, $i, $i);
220 $this->parser->references[] = $ref; // Register reference with parser
221
222 // Sheet tab colors?
223 if ($phpSheet->isTabColorSet()) {
224 $this->addColor($phpSheet->getTabColor()->getRGB());
225 }
226 }
227 }
228
229 /**
230 * Add a new XF writer.
231 *
232 * @param bool $isStyleXf Is it a style XF?
233 *
234 * @return int Index to XF record
235 */
236 public function addXfWriter(Style $style, $isStyleXf = false)
237 {
238 $xfWriter = new Xf($style);
239 $xfWriter->setIsStyleXf($isStyleXf);
240
241 // Add the font if not already added
242 $fontIndex = $this->addFont($style->getFont());
243
244 // Assign the font index to the xf record
245 $xfWriter->setFontIndex($fontIndex);
246
247 // Background colors, best to treat these after the font so black will come after white in custom palette
248 $xfWriter->setFgColor($this->addColor($style->getFill()->getStartColor()->getRGB()));
249 $xfWriter->setBgColor($this->addColor($style->getFill()->getEndColor()->getRGB()));
250 $xfWriter->setBottomColor($this->addColor($style->getBorders()->getBottom()->getColor()->getRGB()));
251 $xfWriter->setTopColor($this->addColor($style->getBorders()->getTop()->getColor()->getRGB()));
252 $xfWriter->setRightColor($this->addColor($style->getBorders()->getRight()->getColor()->getRGB()));
253 $xfWriter->setLeftColor($this->addColor($style->getBorders()->getLeft()->getColor()->getRGB()));
254 $xfWriter->setDiagColor($this->addColor($style->getBorders()->getDiagonal()->getColor()->getRGB()));
255
256 // Add the number format if it is not a built-in one and not already added
257 if ($style->getNumberFormat()->getBuiltInFormatCode() === self::$scrutinizerFalse) {
258 $numberFormatHashCode = $style->getNumberFormat()->getHashCode();
259
260 if (isset($this->addedNumberFormats[$numberFormatHashCode])) {
261 $numberFormatIndex = $this->addedNumberFormats[$numberFormatHashCode];
262 } else {
263 $numberFormatIndex = 164 + count($this->numberFormats);
264 $this->numberFormats[$numberFormatIndex] = $style->getNumberFormat();
265 $this->addedNumberFormats[$numberFormatHashCode] = $numberFormatIndex;
266 }
267 } else {
268 $numberFormatIndex = (int) $style->getNumberFormat()->getBuiltInFormatCode();
269 }
270
271 // Assign the number format index to xf record
272 $xfWriter->setNumberFormatIndex($numberFormatIndex);
273
274 $this->xfWriters[] = $xfWriter;
275
276 return count($this->xfWriters) - 1;
277 }
278
279 /**
280 * Add a font to added fonts.
281 *
282 * @return int Index to FONT record
283 */
284 public function addFont(\PhpOffice\PhpSpreadsheet\Style\Font $font)
285 {
286 $fontHashCode = $font->getHashCode();
287 if (isset($this->addedFonts[$fontHashCode])) {
288 $fontIndex = $this->addedFonts[$fontHashCode];
289 } else {
290 $countFonts = count($this->fontWriters);
291 $fontIndex = ($countFonts < 4) ? $countFonts : $countFonts + 1;
292
293 $fontWriter = new Font($font);
294 $fontWriter->setColorIndex($this->addColor($font->getColor()->getRGB()));
295 $this->fontWriters[] = $fontWriter;
296
297 $this->addedFonts[$fontHashCode] = $fontIndex;
298 }
299
300 return $fontIndex;
301 }
302
303 /**
304 * Alter color palette adding a custom color.
305 *
306 * @param string $rgb E.g. 'FF00AA'
307 *
308 * @return int Color index
309 */
310 private function addColor($rgb)
311 {
312 if (!isset($this->colors[$rgb])) {
313 $color =
314 [
315 hexdec(substr($rgb, 0, 2)),
316 hexdec(substr($rgb, 2, 2)),
317 hexdec(substr($rgb, 4)),
318 0,
319 ];
320 $colorIndex = array_search($color, $this->palette);
321 if ($colorIndex) {
322 $this->colors[$rgb] = $colorIndex;
323 } else {
324 if (count($this->colors) === 0) {
325 $lastColor = 7;
326 } else {
327 $lastColor = end($this->colors);
328 }
329 if ($lastColor < 57) {
330 // then we add a custom color altering the palette
331 $colorIndex = $lastColor + 1;
332 $this->palette[$colorIndex] = $color;
333 $this->colors[$rgb] = $colorIndex;
334 } else {
335 // no room for more custom colors, just map to black
336 $colorIndex = 0;
337 }
338 }
339 } else {
340 // fetch already added custom color
341 $colorIndex = $this->colors[$rgb];
342 }
343
344 return $colorIndex;
345 }
346
347 /**
348 * Sets the colour palette to the Excel 97+ default.
349 */
350 private function setPaletteXl97(): void
351 {
352 $this->palette = [
353 0x08 => [0x00, 0x00, 0x00, 0x00],
354 0x09 => [0xff, 0xff, 0xff, 0x00],
355 0x0A => [0xff, 0x00, 0x00, 0x00],
356 0x0B => [0x00, 0xff, 0x00, 0x00],
357 0x0C => [0x00, 0x00, 0xff, 0x00],
358 0x0D => [0xff, 0xff, 0x00, 0x00],
359 0x0E => [0xff, 0x00, 0xff, 0x00],
360 0x0F => [0x00, 0xff, 0xff, 0x00],
361 0x10 => [0x80, 0x00, 0x00, 0x00],
362 0x11 => [0x00, 0x80, 0x00, 0x00],
363 0x12 => [0x00, 0x00, 0x80, 0x00],
364 0x13 => [0x80, 0x80, 0x00, 0x00],
365 0x14 => [0x80, 0x00, 0x80, 0x00],
366 0x15 => [0x00, 0x80, 0x80, 0x00],
367 0x16 => [0xc0, 0xc0, 0xc0, 0x00],
368 0x17 => [0x80, 0x80, 0x80, 0x00],
369 0x18 => [0x99, 0x99, 0xff, 0x00],
370 0x19 => [0x99, 0x33, 0x66, 0x00],
371 0x1A => [0xff, 0xff, 0xcc, 0x00],
372 0x1B => [0xcc, 0xff, 0xff, 0x00],
373 0x1C => [0x66, 0x00, 0x66, 0x00],
374 0x1D => [0xff, 0x80, 0x80, 0x00],
375 0x1E => [0x00, 0x66, 0xcc, 0x00],
376 0x1F => [0xcc, 0xcc, 0xff, 0x00],
377 0x20 => [0x00, 0x00, 0x80, 0x00],
378 0x21 => [0xff, 0x00, 0xff, 0x00],
379 0x22 => [0xff, 0xff, 0x00, 0x00],
380 0x23 => [0x00, 0xff, 0xff, 0x00],
381 0x24 => [0x80, 0x00, 0x80, 0x00],
382 0x25 => [0x80, 0x00, 0x00, 0x00],
383 0x26 => [0x00, 0x80, 0x80, 0x00],
384 0x27 => [0x00, 0x00, 0xff, 0x00],
385 0x28 => [0x00, 0xcc, 0xff, 0x00],
386 0x29 => [0xcc, 0xff, 0xff, 0x00],
387 0x2A => [0xcc, 0xff, 0xcc, 0x00],
388 0x2B => [0xff, 0xff, 0x99, 0x00],
389 0x2C => [0x99, 0xcc, 0xff, 0x00],
390 0x2D => [0xff, 0x99, 0xcc, 0x00],
391 0x2E => [0xcc, 0x99, 0xff, 0x00],
392 0x2F => [0xff, 0xcc, 0x99, 0x00],
393 0x30 => [0x33, 0x66, 0xff, 0x00],
394 0x31 => [0x33, 0xcc, 0xcc, 0x00],
395 0x32 => [0x99, 0xcc, 0x00, 0x00],
396 0x33 => [0xff, 0xcc, 0x00, 0x00],
397 0x34 => [0xff, 0x99, 0x00, 0x00],
398 0x35 => [0xff, 0x66, 0x00, 0x00],
399 0x36 => [0x66, 0x66, 0x99, 0x00],
400 0x37 => [0x96, 0x96, 0x96, 0x00],
401 0x38 => [0x00, 0x33, 0x66, 0x00],
402 0x39 => [0x33, 0x99, 0x66, 0x00],
403 0x3A => [0x00, 0x33, 0x00, 0x00],
404 0x3B => [0x33, 0x33, 0x00, 0x00],
405 0x3C => [0x99, 0x33, 0x00, 0x00],
406 0x3D => [0x99, 0x33, 0x66, 0x00],
407 0x3E => [0x33, 0x33, 0x99, 0x00],
408 0x3F => [0x33, 0x33, 0x33, 0x00],
409 ];
410 }
411
412 /**
413 * Assemble worksheets into a workbook and send the BIFF data to an OLE
414 * storage.
415 *
416 * @param array $worksheetSizes The sizes in bytes of the binary worksheet streams
417 *
418 * @return string Binary data for workbook stream
419 */
420 public function writeWorkbook(array $worksheetSizes)
421 {
422 $this->worksheetSizes = $worksheetSizes;
423
424 // Calculate the number of selected worksheet tabs and call the finalization
425 // methods for each worksheet
426 $total_worksheets = $this->spreadsheet->getSheetCount();
427
428 // Add part 1 of the Workbook globals, what goes before the SHEET records
429 $this->storeBof(0x0005);
430 $this->writeCodepage();
431 $this->writeWindow1();
432
433 $this->writeDateMode();
434 $this->writeAllFonts();
435 $this->writeAllNumberFormats();
436 $this->writeAllXfs();
437 $this->writeAllStyles();
438 $this->writePalette();
439
440 // Prepare part 3 of the workbook global stream, what goes after the SHEET records
441 $part3 = '';
442 if ($this->countryCode !== -1) {
443 $part3 .= $this->writeCountry();
444 }
445 $part3 .= $this->writeRecalcId();
446
447 $part3 .= $this->writeSupbookInternal();
448 /* TODO: store external SUPBOOK records and XCT and CRN records
449 in case of external references for BIFF8 */
450 $part3 .= $this->writeExternalsheetBiff8();
451 $part3 .= $this->writeAllDefinedNamesBiff8();
452 $part3 .= $this->writeMsoDrawingGroup();
453 $part3 .= $this->writeSharedStringsTable();
454
455 $part3 .= $this->writeEof();
456
457 // Add part 2 of the Workbook globals, the SHEET records
458 $this->calcSheetOffsets();
459 for ($i = 0; $i < $total_worksheets; ++$i) {
460 $this->writeBoundSheet($this->spreadsheet->getSheet($i), $this->worksheetOffsets[$i]);
461 }
462
463 // Add part 3 of the Workbook globals
464 $this->_data .= $part3;
465
466 return $this->_data;
467 }
468
469 /**
470 * Calculate offsets for Worksheet BOF records.
471 */
472 private function calcSheetOffsets(): void
473 {
474 $boundsheet_length = 10; // fixed length for a BOUNDSHEET record
475
476 // size of Workbook globals part 1 + 3
477 $offset = $this->_datasize;
478
479 // add size of Workbook globals part 2, the length of the SHEET records
480 $total_worksheets = count($this->spreadsheet->getAllSheets());
481 foreach ($this->spreadsheet->getWorksheetIterator() as $sheet) {
482 $offset += $boundsheet_length + strlen(StringHelper::UTF8toBIFF8UnicodeShort($sheet->getTitle()));
483 }
484
485 // add the sizes of each of the Sheet substreams, respectively
486 for ($i = 0; $i < $total_worksheets; ++$i) {
487 $this->worksheetOffsets[$i] = $offset;
488 $offset += $this->worksheetSizes[$i];
489 }
490 $this->biffSize = $offset;
491 }
492
493 /**
494 * Store the Excel FONT records.
495 */
496 private function writeAllFonts(): void
497 {
498 foreach ($this->fontWriters as $fontWriter) {
499 $this->append($fontWriter->writeFont());
500 }
501 }
502
503 /**
504 * Store user defined numerical formats i.e. FORMAT records.
505 */
506 private function writeAllNumberFormats(): void
507 {
508 foreach ($this->numberFormats as $numberFormatIndex => $numberFormat) {
509 $this->writeNumberFormat($numberFormat->getFormatCode(), $numberFormatIndex);
510 }
511 }
512
513 /**
514 * Write all XF records.
515 */
516 private function writeAllXfs(): void
517 {
518 foreach ($this->xfWriters as $xfWriter) {
519 $this->append($xfWriter->writeXf());
520 }
521 }
522
523 /**
524 * Write all STYLE records.
525 */
526 private function writeAllStyles(): void
527 {
528 $this->writeStyle();
529 }
530
531 private function parseDefinedNameValue(DefinedName $definedName): string
532 {
533 $definedRange = $definedName->getValue();
534 $splitCount = preg_match_all(
535 '/' . Calculation::CALCULATION_REGEXP_CELLREF . '/mui',
536 $definedRange,
537 $splitRanges,
538 PREG_OFFSET_CAPTURE
539 );
540
541 $lengths = array_map('strlen', array_column($splitRanges[0], 0));
542 $offsets = array_column($splitRanges[0], 1);
543
544 $worksheets = $splitRanges[2];
545 $columns = $splitRanges[6];
546 $rows = $splitRanges[7];
547
548 while ($splitCount > 0) {
549 --$splitCount;
550 $length = $lengths[$splitCount];
551 $offset = $offsets[$splitCount];
552 $worksheet = $worksheets[$splitCount][0];
553 $column = $columns[$splitCount][0];
554 $row = $rows[$splitCount][0];
555
556 $newRange = '';
557 if (empty($worksheet)) {
558 if (($offset === 0) || ($definedRange[$offset - 1] !== ':')) {
559 // We should have a worksheet
560 $worksheet = $definedName->getWorksheet() ? $definedName->getWorksheet()->getTitle() : null;
561 }
562 } else {
563 $worksheet = str_replace("''", "'", trim($worksheet, "'"));
564 }
565 if (!empty($worksheet)) {
566 $newRange = "'" . str_replace("'", "''", $worksheet) . "'!";
567 }
568
569 if (!empty($column)) {
570 $newRange .= "\${$column}";
571 }
572 if (!empty($row)) {
573 $newRange .= "\${$row}";
574 }
575
576 $definedRange = substr($definedRange, 0, $offset) . $newRange . substr($definedRange, $offset + $length);
577 }
578
579 return $definedRange;
580 }
581
582 /**
583 * Writes all the DEFINEDNAME records (BIFF8).
584 * So far this is only used for repeating rows/columns (print titles) and print areas.
585 */
586 private function writeAllDefinedNamesBiff8(): string
587 {
588 $chunk = '';
589
590 // Named ranges
591 $definedNames = $this->spreadsheet->getDefinedNames();
592 if (count($definedNames) > 0) {
593 // Loop named ranges
594 foreach ($definedNames as $definedName) {
595 $range = $this->parseDefinedNameValue($definedName);
596
597 // parse formula
598 try {
599 $this->parser->parse($range);
600 $formulaData = $this->parser->toReversePolish();
601
602 // make sure tRef3d is of type tRef3dR (0x3A)
603 if (isset($formulaData[0]) && ($formulaData[0] == "\x7A" || $formulaData[0] == "\x5A")) {
604 $formulaData = "\x3A" . substr($formulaData, 1);
605 }
606
607 if ($definedName->getLocalOnly()) {
608 // local scope
609 $scopeWs = $definedName->getScope();
610 $scope = ($scopeWs === null) ? 0 : ($this->spreadsheet->getIndex($scopeWs) + 1);
611 } else {
612 // global scope
613 $scope = 0;
614 }
615 $chunk .= $this->writeData($this->writeDefinedNameBiff8($definedName->getName(), $formulaData, $scope, false));
616 } catch (PhpSpreadsheetException $e) {
617 // do nothing
618 }
619 }
620 }
621
622 // total number of sheets
623 $total_worksheets = $this->spreadsheet->getSheetCount();
624
625 // write the print titles (repeating rows, columns), if any
626 for ($i = 0; $i < $total_worksheets; ++$i) {
627 $sheetSetup = $this->spreadsheet->getSheet($i)->getPageSetup();
628 // simultaneous repeatColumns repeatRows
629 if ($sheetSetup->isColumnsToRepeatAtLeftSet() && $sheetSetup->isRowsToRepeatAtTopSet()) {
630 $repeat = $sheetSetup->getColumnsToRepeatAtLeft();
631 $colmin = Coordinate::columnIndexFromString($repeat[0]) - 1;
632 $colmax = Coordinate::columnIndexFromString($repeat[1]) - 1;
633
634 $repeat = $sheetSetup->getRowsToRepeatAtTop();
635 $rowmin = $repeat[0] - 1;
636 $rowmax = $repeat[1] - 1;
637
638 // construct formula data manually
639 $formulaData = pack('Cv', 0x29, 0x17); // tMemFunc
640 $formulaData .= pack('Cvvvvv', 0x3B, $i, 0, 65535, $colmin, $colmax); // tArea3d
641 $formulaData .= pack('Cvvvvv', 0x3B, $i, $rowmin, $rowmax, 0, 255); // tArea3d
642 $formulaData .= pack('C', 0x10); // tList
643
644 // store the DEFINEDNAME record
645 $chunk .= $this->writeData($this->writeDefinedNameBiff8(pack('C', 0x07), $formulaData, $i + 1, true));
646 } elseif ($sheetSetup->isColumnsToRepeatAtLeftSet() || $sheetSetup->isRowsToRepeatAtTopSet()) {
647 // (exclusive) either repeatColumns or repeatRows.
648 // Columns to repeat
649 if ($sheetSetup->isColumnsToRepeatAtLeftSet()) {
650 $repeat = $sheetSetup->getColumnsToRepeatAtLeft();
651 $colmin = Coordinate::columnIndexFromString($repeat[0]) - 1;
652 $colmax = Coordinate::columnIndexFromString($repeat[1]) - 1;
653 } else {
654 $colmin = 0;
655 $colmax = 255;
656 }
657 // Rows to repeat
658 if ($sheetSetup->isRowsToRepeatAtTopSet()) {
659 $repeat = $sheetSetup->getRowsToRepeatAtTop();
660 $rowmin = $repeat[0] - 1;
661 $rowmax = $repeat[1] - 1;
662 } else {
663 $rowmin = 0;
664 $rowmax = 65535;
665 }
666
667 // construct formula data manually because parser does not recognize absolute 3d cell references
668 $formulaData = pack('Cvvvvv', 0x3B, $i, $rowmin, $rowmax, $colmin, $colmax);
669
670 // store the DEFINEDNAME record
671 $chunk .= $this->writeData($this->writeDefinedNameBiff8(pack('C', 0x07), $formulaData, $i + 1, true));
672 }
673 }
674
675 // write the print areas, if any
676 for ($i = 0; $i < $total_worksheets; ++$i) {
677 $sheetSetup = $this->spreadsheet->getSheet($i)->getPageSetup();
678 if ($sheetSetup->isPrintAreaSet()) {
679 // Print area, e.g. A3:J6,H1:X20
680 $printArea = Coordinate::splitRange($sheetSetup->getPrintArea());
681 $countPrintArea = count($printArea);
682
683 $formulaData = '';
684 for ($j = 0; $j < $countPrintArea; ++$j) {
685 $printAreaRect = $printArea[$j]; // e.g. A3:J6
686 $printAreaRect[0] = Coordinate::indexesFromString($printAreaRect[0]);
687 $printAreaRect[1] = Coordinate::indexesFromString($printAreaRect[1]);
688
689 $print_rowmin = $printAreaRect[0][1] - 1;
690 $print_rowmax = $printAreaRect[1][1] - 1;
691 $print_colmin = $printAreaRect[0][0] - 1;
692 $print_colmax = $printAreaRect[1][0] - 1;
693
694 // construct formula data manually because parser does not recognize absolute 3d cell references
695 $formulaData .= pack('Cvvvvv', 0x3B, $i, $print_rowmin, $print_rowmax, $print_colmin, $print_colmax);
696
697 if ($j > 0) {
698 $formulaData .= pack('C', 0x10); // list operator token ','
699 }
700 }
701
702 // store the DEFINEDNAME record
703 $chunk .= $this->writeData($this->writeDefinedNameBiff8(pack('C', 0x06), $formulaData, $i + 1, true));
704 }
705 }
706
707 // write autofilters, if any
708 for ($i = 0; $i < $total_worksheets; ++$i) {
709 $sheetAutoFilter = $this->spreadsheet->getSheet($i)->getAutoFilter();
710 $autoFilterRange = $sheetAutoFilter->getRange();
711 if (!empty($autoFilterRange)) {
712 $rangeBounds = Coordinate::rangeBoundaries($autoFilterRange);
713
714 //Autofilter built in name
715 $name = pack('C', 0x0D);
716
717 $chunk .= $this->writeData($this->writeShortNameBiff8($name, $i + 1, $rangeBounds, true));
718 }
719 }
720
721 return $chunk;
722 }
723
724 /**
725 * Write a DEFINEDNAME record for BIFF8 using explicit binary formula data.
726 *
727 * @param string $name The name in UTF-8
728 * @param string $formulaData The binary formula data
729 * @param int $sheetIndex 1-based sheet index the defined name applies to. 0 = global
730 * @param bool $isBuiltIn Built-in name?
731 *
732 * @return string Complete binary record data
733 */
734 private function writeDefinedNameBiff8($name, $formulaData, $sheetIndex = 0, $isBuiltIn = false)
735 {
736 $record = 0x0018;
737
738 // option flags
739 $options = $isBuiltIn ? 0x20 : 0x00;
740
741 // length of the name, character count
742 $nlen = StringHelper::countCharacters($name);
743
744 // name with stripped length field
745 $name = substr(StringHelper::UTF8toBIFF8UnicodeLong($name), 2);
746
747 // size of the formula (in bytes)
748 $sz = strlen($formulaData);
749
750 // combine the parts
751 $data = pack('vCCvvvCCCC', $options, 0, $nlen, $sz, 0, $sheetIndex, 0, 0, 0, 0)
752 . $name . $formulaData;
753 $length = strlen($data);
754
755 $header = pack('vv', $record, $length);
756
757 return $header . $data;
758 }
759
760 /**
761 * Write a short NAME record.
762 *
763 * @param string $name
764 * @param int $sheetIndex 1-based sheet index the defined name applies to. 0 = global
765 * @param int[][] $rangeBounds range boundaries
766 * @param bool $isHidden
767 *
768 * @return string Complete binary record data
769 * */
770 private function writeShortNameBiff8($name, $sheetIndex, $rangeBounds, $isHidden = false)
771 {
772 $record = 0x0018;
773
774 // option flags
775 $options = ($isHidden ? 0x21 : 0x00);
776
777 $extra = pack(
778 'Cvvvvv',
779 0x3B,
780 $sheetIndex - 1,
781 $rangeBounds[0][1] - 1,
782 $rangeBounds[1][1] - 1,
783 $rangeBounds[0][0] - 1,
784 $rangeBounds[1][0] - 1
785 );
786
787 // size of the formula (in bytes)
788 $sz = strlen($extra);
789
790 // combine the parts
791 $data = pack('vCCvvvCCCCC', $options, 0, 1, $sz, 0, $sheetIndex, 0, 0, 0, 0, 0)
792 . $name . $extra;
793 $length = strlen($data);
794
795 $header = pack('vv', $record, $length);
796
797 return $header . $data;
798 }
799
800 /**
801 * Stores the CODEPAGE biff record.
802 */
803 private function writeCodepage(): void
804 {
805 $record = 0x0042; // Record identifier
806 $length = 0x0002; // Number of bytes to follow
807 $cv = $this->codepage; // The code page
808
809 $header = pack('vv', $record, $length);
810 $data = pack('v', $cv);
811
812 $this->append($header . $data);
813 }
814
815 /**
816 * Write Excel BIFF WINDOW1 record.
817 */
818 private function writeWindow1(): void
819 {
820 $record = 0x003D; // Record identifier
821 $length = 0x0012; // Number of bytes to follow
822
823 $xWn = 0x0000; // Horizontal position of window
824 $yWn = 0x0000; // Vertical position of window
825 $dxWn = 0x25BC; // Width of window
826 $dyWn = 0x1572; // Height of window
827
828 $grbit = 0x0038; // Option flags
829
830 // not supported by PhpSpreadsheet, so there is only one selected sheet, the active
831 $ctabsel = 1; // Number of workbook tabs selected
832
833 $wTabRatio = 0x0258; // Tab to scrollbar ratio
834
835 // not supported by PhpSpreadsheet, set to 0
836 $itabFirst = 0; // 1st displayed worksheet
837 $itabCur = $this->spreadsheet->getActiveSheetIndex(); // Active worksheet
838
839 $header = pack('vv', $record, $length);
840 $data = pack('vvvvvvvvv', $xWn, $yWn, $dxWn, $dyWn, $grbit, $itabCur, $itabFirst, $ctabsel, $wTabRatio);
841 $this->append($header . $data);
842 }
843
844 /**
845 * Writes Excel BIFF BOUNDSHEET record.
846 *
847 * @param int $offset Location of worksheet BOF
848 */
849 private function writeBoundSheet(\PhpOffice\PhpSpreadsheet\Worksheet\Worksheet $sheet, $offset): void
850 {
851 $sheetname = $sheet->getTitle();
852 $record = 0x0085; // Record identifier
853
854 // sheet state
855 switch ($sheet->getSheetState()) {
856 case \PhpOffice\PhpSpreadsheet\Worksheet\Worksheet::SHEETSTATE_VISIBLE:
857 $ss = 0x00;
858
859 break;
860 case \PhpOffice\PhpSpreadsheet\Worksheet\Worksheet::SHEETSTATE_HIDDEN:
861 $ss = 0x01;
862
863 break;
864 case \PhpOffice\PhpSpreadsheet\Worksheet\Worksheet::SHEETSTATE_VERYHIDDEN:
865 $ss = 0x02;
866
867 break;
868 default:
869 $ss = 0x00;
870
871 break;
872 }
873
874 // sheet type
875 $st = 0x00;
876
877 //$grbit = 0x0000; // Visibility and sheet type
878
879 $data = pack('VCC', $offset, $ss, $st);
880 $data .= StringHelper::UTF8toBIFF8UnicodeShort($sheetname);
881
882 $length = strlen($data);
883 $header = pack('vv', $record, $length);
884 $this->append($header . $data);
885 }
886
887 /**
888 * Write Internal SUPBOOK record.
889 */
890 private function writeSupbookInternal(): string
891 {
892 $record = 0x01AE; // Record identifier
893 $length = 0x0004; // Bytes to follow
894
895 $header = pack('vv', $record, $length);
896 $data = pack('vv', $this->spreadsheet->getSheetCount(), 0x0401);
897
898 return $this->writeData($header . $data);
899 }
900
901 /**
902 * Writes the Excel BIFF EXTERNSHEET record. These references are used by
903 * formulas.
904 */
905 private function writeExternalsheetBiff8(): string
906 {
907 $totalReferences = count($this->parser->references);
908 $record = 0x0017; // Record identifier
909 $length = 2 + 6 * $totalReferences; // Number of bytes to follow
910
911 //$supbook_index = 0; // FIXME: only using internal SUPBOOK record
912 $header = pack('vv', $record, $length);
913 $data = pack('v', $totalReferences);
914 for ($i = 0; $i < $totalReferences; ++$i) {
915 $data .= $this->parser->references[$i];
916 }
917
918 return $this->writeData($header . $data);
919 }
920
921 /**
922 * Write Excel BIFF STYLE records.
923 */
924 private function writeStyle(): void
925 {
926 $record = 0x0293; // Record identifier
927 $length = 0x0004; // Bytes to follow
928
929 $ixfe = 0x8000; // Index to cell style XF
930 $BuiltIn = 0x00; // Built-in style
931 $iLevel = 0xff; // Outline style level
932
933 $header = pack('vv', $record, $length);
934 $data = pack('vCC', $ixfe, $BuiltIn, $iLevel);
935 $this->append($header . $data);
936 }
937
938 /**
939 * Writes Excel FORMAT record for non "built-in" numerical formats.
940 *
941 * @param string $format Custom format string
942 * @param int $ifmt Format index code
943 */
944 private function writeNumberFormat($format, $ifmt): void
945 {
946 $record = 0x041E; // Record identifier
947
948 $numberFormatString = StringHelper::UTF8toBIFF8UnicodeLong($format);
949 $length = 2 + strlen($numberFormatString); // Number of bytes to follow
950
951 $header = pack('vv', $record, $length);
952 $data = pack('v', $ifmt) . $numberFormatString;
953 $this->append($header . $data);
954 }
955
956 /**
957 * Write DATEMODE record to indicate the date system in use (1904 or 1900).
958 */
959 private function writeDateMode(): void
960 {
961 $record = 0x0022; // Record identifier
962 $length = 0x0002; // Bytes to follow
963
964 $f1904 = (Date::getExcelCalendar() === Date::CALENDAR_MAC_1904)
965 ? 1
966 : 0; // Flag for 1904 date system
967
968 $header = pack('vv', $record, $length);
969 $data = pack('v', $f1904);
970 $this->append($header . $data);
971 }
972
973 /**
974 * Stores the COUNTRY record for localization.
975 *
976 * @return string
977 */
978 private function writeCountry()
979 {
980 $record = 0x008C; // Record identifier
981 $length = 4; // Number of bytes to follow
982
983 $header = pack('vv', $record, $length);
984 // using the same country code always for simplicity
985 $data = pack('vv', $this->countryCode, $this->countryCode);
986
987 return $this->writeData($header . $data);
988 }
989
990 /**
991 * Write the RECALCID record.
992 *
993 * @return string
994 */
995 private function writeRecalcId()
996 {
997 $record = 0x01C1; // Record identifier
998 $length = 8; // Number of bytes to follow
999
1000 $header = pack('vv', $record, $length);
1001
1002 // by inspection of real Excel files, MS Office Excel 2007 writes this
1003 $data = pack('VV', 0x000001C1, 0x00001E667);
1004
1005 return $this->writeData($header . $data);
1006 }
1007
1008 /**
1009 * Stores the PALETTE biff record.
1010 */
1011 private function writePalette(): void
1012 {
1013 $aref = $this->palette;
1014
1015 $record = 0x0092; // Record identifier
1016 $length = 2 + 4 * count($aref); // Number of bytes to follow
1017 $ccv = count($aref); // Number of RGB values to follow
1018 $data = ''; // The RGB data
1019
1020 // Pack the RGB data
1021 foreach ($aref as $color) {
1022 foreach ($color as $byte) {
1023 $data .= pack('C', $byte);
1024 }
1025 }
1026
1027 $header = pack('vvv', $record, $length, $ccv);
1028 $this->append($header . $data);
1029 }
1030
1031 /**
1032 * Handling of the SST continue blocks is complicated by the need to include an
1033 * additional continuation byte depending on whether the string is split between
1034 * blocks or whether it starts at the beginning of the block. (There are also
1035 * additional complications that will arise later when/if Rich Strings are
1036 * supported).
1037 *
1038 * The Excel documentation says that the SST record should be followed by an
1039 * EXTSST record. The EXTSST record is a hash table that is used to optimise
1040 * access to SST. However, despite the documentation it doesn't seem to be
1041 * required so we will ignore it.
1042 *
1043 * @return string Binary data
1044 */
1045 private function writeSharedStringsTable()
1046 {
1047 // maximum size of record data (excluding record header)
1048 $continue_limit = 8224;
1049
1050 // initialize array of record data blocks
1051 $recordDatas = [];
1052
1053 // start SST record data block with total number of strings, total number of unique strings
1054 $recordData = pack('VV', $this->stringTotal, $this->stringUnique);
1055
1056 // loop through all (unique) strings in shared strings table
1057 foreach (array_keys($this->stringTable) as $string) {
1058 // here $string is a BIFF8 encoded string
1059
1060 // length = character count
1061 $headerinfo = unpack('vlength/Cencoding', $string);
1062
1063 // currently, this is always 1 = uncompressed
1064 $encoding = $headerinfo['encoding'] ?? 1;
1065
1066 // initialize finished writing current $string
1067 $finished = false;
1068
1069 while ($finished === false) {
1070 // normally, there will be only one cycle, but if string cannot immediately be written as is
1071 // there will be need for more than one cylcle, if string longer than one record data block, there
1072 // may be need for even more cycles
1073
1074 if (strlen($recordData) + strlen($string) <= $continue_limit) {
1075 // then we can write the string (or remainder of string) without any problems
1076 $recordData .= $string;
1077
1078 if (strlen($recordData) + strlen($string) == $continue_limit) {
1079 // we close the record data block, and initialize a new one
1080 $recordDatas[] = $recordData;
1081 $recordData = '';
1082 }
1083
1084 // we are finished writing this string
1085 $finished = true;
1086 } else {
1087 // special treatment writing the string (or remainder of the string)
1088 // If the string is very long it may need to be written in more than one CONTINUE record.
1089
1090 // check how many bytes more there is room for in the current record
1091 $space_remaining = $continue_limit - strlen($recordData);
1092
1093 // minimum space needed
1094 // uncompressed: 2 byte string length length field + 1 byte option flags + 2 byte character
1095 // compressed: 2 byte string length length field + 1 byte option flags + 1 byte character
1096 $min_space_needed = ($encoding == 1) ? 5 : 4;
1097
1098 // We have two cases
1099 // 1. space remaining is less than minimum space needed
1100 // here we must waste the space remaining and move to next record data block
1101 // 2. space remaining is greater than or equal to minimum space needed
1102 // here we write as much as we can in the current block, then move to next record data block
1103
1104 if ($space_remaining < $min_space_needed) {
1105 // 1. space remaining is less than minimum space needed.
1106 // we close the block, store the block data
1107 $recordDatas[] = $recordData;
1108
1109 // and start new record data block where we start writing the string
1110 $recordData = '';
1111 } else {
1112 // 2. space remaining is greater than or equal to minimum space needed.
1113 // initialize effective remaining space, for Unicode strings this may need to be reduced by 1, see below
1114 $effective_space_remaining = $space_remaining;
1115
1116 // for uncompressed strings, sometimes effective space remaining is reduced by 1
1117 if ($encoding == 1 && (strlen($string) - $space_remaining) % 2 == 1) {
1118 --$effective_space_remaining;
1119 }
1120
1121 // one block fininshed, store the block data
1122 $recordData .= substr($string, 0, $effective_space_remaining);
1123
1124 $string = substr($string, $effective_space_remaining); // for next cycle in while loop
1125 $recordDatas[] = $recordData;
1126
1127 // start new record data block with the repeated option flags
1128 $recordData = pack('C', $encoding);
1129 }
1130 }
1131 }
1132 }
1133
1134 // Store the last record data block unless it is empty
1135 // if there was no need for any continue records, this will be the for SST record data block itself
1136 if (strlen($recordData) > 0) {
1137 $recordDatas[] = $recordData;
1138 }
1139
1140 // combine into one chunk with all the blocks SST, CONTINUE,...
1141 $chunk = '';
1142 foreach ($recordDatas as $i => $recordData) {
1143 // first block should have the SST record header, remaing should have CONTINUE header
1144 $record = ($i == 0) ? 0x00FC : 0x003C;
1145
1146 $header = pack('vv', $record, strlen($recordData));
1147 $data = $header . $recordData;
1148
1149 $chunk .= $this->writeData($data);
1150 }
1151
1152 return $chunk;
1153 }
1154
1155 /**
1156 * Writes the MSODRAWINGGROUP record if needed. Possibly split using CONTINUE records.
1157 */
1158 private function writeMsoDrawingGroup(): string
1159 {
1160 // write the Escher stream if necessary
1161 if (isset($this->escher)) {
1162 $writer = new Escher($this->escher);
1163 $data = $writer->close();
1164
1165 $record = 0x00EB;
1166 $length = strlen($data);
1167 $header = pack('vv', $record, $length);
1168
1169 return $this->writeData($header . $data);
1170 }
1171
1172 return '';
1173 }
1174
1175 /**
1176 * Get Escher object.
1177 */
1178 public function getEscher(): ?\PhpOffice\PhpSpreadsheet\Shared\Escher
1179 {
1180 return $this->escher;
1181 }
1182
1183 /**
1184 * Set Escher object.
1185 */
1186 public function setEscher(?\PhpOffice\PhpSpreadsheet\Shared\Escher $escher): void
1187 {
1188 $this->escher = $escher;
1189 }
1190 }
1191