PluginProbe
Visualizer – Tables & Charts Manager with Built-in AI Generator / 3.7.1
Visualizer – Tables & Charts Manager with Built-in AI Generator v3.7.1
4.0.8 4.0.7 4.0.6 4.0.5 4.0.4 4.0.3 3.0.5 3.0.6 3.0.7 3.0.8 3.0.9 3.1.0 3.1.1 3.1.2 3.1.3 3.10.0 3.10.1 3.10.10 3.10.11 3.10.12 3.10.13 3.10.14 3.10.15 3.10.2 3.10.3 All 149 releases
visualizer / vendor / phpoffice / phpspreadsheet / src / PhpSpreadsheet / Writer / Xls / Workbook.php

Workbook.php in Visualizer – Tables & Charts Manager with Built-in AI Generator 3.7.1, at vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Writer/Xls/Workbook.php

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