Duplicate code is one of the most pungent code smells. A rule that is often used is to re-structure code once it is duplicated in three or more places.
Common duplication problems, and corresponding solutions are:
Complex classes like Workbook often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes. You can also have a look at the cohesion graph to spot any un-connected, or weakly-connected components.
Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.
While breaking up the class, it is a good idea to analyze how other classes use Workbook, and based on these observations, apply Extract Interface, too.
1 | <?php |
||
45 | class Workbook extends BIFFwriter |
||
46 | { |
||
47 | /** |
||
48 | * Formula parser. |
||
49 | * |
||
50 | * @var \PhpOffice\PhpSpreadsheet\Writer\Xls\Parser |
||
51 | */ |
||
52 | private $parser; |
||
53 | |||
54 | /** |
||
55 | * The BIFF file size for the workbook. |
||
56 | * |
||
57 | * @var int |
||
58 | * |
||
59 | * @see calcSheetOffsets() |
||
60 | */ |
||
61 | private $biffSize; |
||
62 | |||
63 | /** |
||
64 | * XF Writers. |
||
65 | * |
||
66 | * @var \PhpOffice\PhpSpreadsheet\Writer\Xls\Xf[] |
||
67 | */ |
||
68 | private $xfWriters = []; |
||
69 | |||
70 | /** |
||
71 | * Array containing the colour palette. |
||
72 | * |
||
73 | * @var array |
||
74 | */ |
||
75 | private $palette; |
||
76 | |||
77 | /** |
||
78 | * The codepage indicates the text encoding used for strings. |
||
79 | * |
||
80 | * @var int |
||
81 | */ |
||
82 | private $codepage; |
||
83 | |||
84 | /** |
||
85 | * The country code used for localization. |
||
86 | * |
||
87 | * @var int |
||
88 | */ |
||
89 | private $countryCode; |
||
90 | |||
91 | /** |
||
92 | * Workbook. |
||
93 | * |
||
94 | * @var Spreadsheet |
||
95 | */ |
||
96 | private $spreadsheet; |
||
97 | |||
98 | /** |
||
99 | * Fonts writers. |
||
100 | * |
||
101 | * @var Font[] |
||
102 | */ |
||
103 | private $fontWriters = []; |
||
104 | |||
105 | /** |
||
106 | * Added fonts. Maps from font's hash => index in workbook. |
||
107 | * |
||
108 | * @var array |
||
109 | */ |
||
110 | private $addedFonts = []; |
||
111 | |||
112 | /** |
||
113 | * Shared number formats. |
||
114 | * |
||
115 | * @var array |
||
116 | */ |
||
117 | private $numberFormats = []; |
||
118 | |||
119 | /** |
||
120 | * Added number formats. Maps from numberFormat's hash => index in workbook. |
||
121 | * |
||
122 | * @var array |
||
123 | */ |
||
124 | private $addedNumberFormats = []; |
||
125 | |||
126 | /** |
||
127 | * Sizes of the binary worksheet streams. |
||
128 | * |
||
129 | * @var array |
||
130 | */ |
||
131 | private $worksheetSizes = []; |
||
132 | |||
133 | /** |
||
134 | * Offsets of the binary worksheet streams relative to the start of the global workbook stream. |
||
135 | * |
||
136 | * @var array |
||
137 | */ |
||
138 | private $worksheetOffsets = []; |
||
139 | |||
140 | /** |
||
141 | * Total number of shared strings in workbook. |
||
142 | * |
||
143 | * @var int |
||
144 | */ |
||
145 | private $stringTotal; |
||
146 | |||
147 | /** |
||
148 | * Number of unique shared strings in workbook. |
||
149 | * |
||
150 | * @var int |
||
151 | */ |
||
152 | private $stringUnique; |
||
153 | |||
154 | /** |
||
155 | * Array of unique shared strings in workbook. |
||
156 | * |
||
157 | * @var array |
||
158 | */ |
||
159 | private $stringTable; |
||
160 | |||
161 | /** |
||
162 | * Color cache. |
||
163 | */ |
||
164 | private $colors; |
||
165 | |||
166 | /** |
||
167 | * Escher object corresponding to MSODRAWINGGROUP. |
||
168 | * |
||
169 | * @var \PhpOffice\PhpSpreadsheet\Shared\Escher |
||
170 | */ |
||
171 | private $escher; |
||
172 | |||
173 | /** |
||
174 | * Class constructor. |
||
175 | * |
||
176 | * @param Spreadsheet $spreadsheet The Workbook |
||
177 | * @param int $str_total Total number of strings |
||
178 | * @param int $str_unique Total number of unique strings |
||
179 | * @param array $str_table String Table |
||
180 | * @param array $colors Colour Table |
||
181 | * @param Parser $parser The formula parser created for the Workbook |
||
182 | */ |
||
183 | 9 | public function __construct(Spreadsheet $spreadsheet, &$str_total, &$str_unique, &$str_table, &$colors, Parser $parser) |
|
184 | { |
||
185 | // It needs to call its parent's constructor explicitly |
||
186 | 9 | parent::__construct(); |
|
187 | |||
188 | 9 | $this->parser = $parser; |
|
189 | 9 | $this->biffSize = 0; |
|
190 | 9 | $this->palette = []; |
|
191 | 9 | $this->countryCode = -1; |
|
192 | |||
193 | 9 | $this->stringTotal = &$str_total; |
|
194 | 9 | $this->stringUnique = &$str_unique; |
|
195 | 9 | $this->stringTable = &$str_table; |
|
196 | 9 | $this->colors = &$colors; |
|
197 | 9 | $this->setPaletteXl97(); |
|
198 | |||
199 | 9 | $this->spreadsheet = $spreadsheet; |
|
200 | |||
201 | 9 | $this->codepage = 0x04B0; |
|
202 | |||
203 | // Add empty sheets and Build color cache |
||
204 | 9 | $countSheets = $spreadsheet->getSheetCount(); |
|
205 | 9 | for ($i = 0; $i < $countSheets; ++$i) { |
|
206 | 9 | $phpSheet = $spreadsheet->getSheet($i); |
|
207 | |||
208 | 9 | $this->parser->setExtSheet($phpSheet->getTitle(), $i); // Register worksheet name with parser |
|
209 | |||
210 | 9 | $supbook_index = 0x00; |
|
211 | 9 | $ref = pack('vvv', $supbook_index, $i, $i); |
|
212 | 9 | $this->parser->references[] = $ref; // Register reference with parser |
|
213 | |||
214 | // Sheet tab colors? |
||
215 | 9 | if ($phpSheet->isTabColorSet()) { |
|
216 | $this->addColor($phpSheet->getTabColor()->getRGB()); |
||
217 | } |
||
218 | } |
||
219 | 9 | } |
|
220 | |||
221 | /** |
||
222 | * Add a new XF writer. |
||
223 | * |
||
224 | * @param \PhpOffice\PhpSpreadsheet\Style |
||
225 | * @param bool Is it a style XF? |
||
226 | * @param mixed $style |
||
227 | * @param mixed $isStyleXf |
||
228 | * |
||
229 | * @return int Index to XF record |
||
230 | */ |
||
231 | public function addXfWriter($style, $isStyleXf = false) |
||
232 | { |
||
233 | $xfWriter = new Xf($style); |
||
234 | $xfWriter->setIsStyleXf($isStyleXf); |
||
235 | |||
236 | // Add the font if not already added |
||
237 | $fontIndex = $this->addFont($style->getFont()); |
||
238 | |||
239 | // Assign the font index to the xf record |
||
240 | $xfWriter->setFontIndex($fontIndex); |
||
241 | |||
242 | // Background colors, best to treat these after the font so black will come after white in custom palette |
||
243 | $xfWriter->setFgColor($this->addColor($style->getFill()->getStartColor()->getRGB())); |
||
244 | $xfWriter->setBgColor($this->addColor($style->getFill()->getEndColor()->getRGB())); |
||
245 | $xfWriter->setBottomColor($this->addColor($style->getBorders()->getBottom()->getColor()->getRGB())); |
||
246 | $xfWriter->setTopColor($this->addColor($style->getBorders()->getTop()->getColor()->getRGB())); |
||
247 | $xfWriter->setRightColor($this->addColor($style->getBorders()->getRight()->getColor()->getRGB())); |
||
248 | $xfWriter->setLeftColor($this->addColor($style->getBorders()->getLeft()->getColor()->getRGB())); |
||
249 | $xfWriter->setDiagColor($this->addColor($style->getBorders()->getDiagonal()->getColor()->getRGB())); |
||
250 | |||
251 | // Add the number format if it is not a built-in one and not already added |
||
252 | if ($style->getNumberFormat()->getBuiltInFormatCode() === false) { |
||
253 | $numberFormatHashCode = $style->getNumberFormat()->getHashCode(); |
||
254 | |||
255 | if (isset($this->addedNumberFormats[$numberFormatHashCode])) { |
||
256 | $numberFormatIndex = $this->addedNumberFormats[$numberFormatHashCode]; |
||
257 | } else { |
||
258 | $numberFormatIndex = 164 + count($this->numberFormats); |
||
259 | $this->numberFormats[$numberFormatIndex] = $style->getNumberFormat(); |
||
260 | $this->addedNumberFormats[$numberFormatHashCode] = $numberFormatIndex; |
||
261 | } |
||
262 | } else { |
||
263 | $numberFormatIndex = (int) $style->getNumberFormat()->getBuiltInFormatCode(); |
||
264 | } |
||
265 | |||
266 | // Assign the number format index to xf record |
||
267 | $xfWriter->setNumberFormatIndex($numberFormatIndex); |
||
268 | |||
269 | $this->xfWriters[] = $xfWriter; |
||
270 | |||
271 | $xfIndex = count($this->xfWriters) - 1; |
||
272 | |||
273 | return $xfIndex; |
||
274 | } |
||
275 | |||
276 | /** |
||
277 | * Add a font to added fonts. |
||
278 | * |
||
279 | * @param \PhpOffice\PhpSpreadsheet\Style\Font $font |
||
280 | * |
||
281 | * @return int Index to FONT record |
||
282 | */ |
||
283 | public function addFont(\PhpOffice\PhpSpreadsheet\Style\Font $font) |
||
284 | { |
||
285 | $fontHashCode = $font->getHashCode(); |
||
286 | if (isset($this->addedFonts[$fontHashCode])) { |
||
287 | $fontIndex = $this->addedFonts[$fontHashCode]; |
||
288 | } else { |
||
289 | $countFonts = count($this->fontWriters); |
||
290 | $fontIndex = ($countFonts < 4) ? $countFonts : $countFonts + 1; |
||
291 | |||
292 | $fontWriter = new Font($font); |
||
293 | $fontWriter->setColorIndex($this->addColor($font->getColor()->getRGB())); |
||
294 | $this->fontWriters[] = $fontWriter; |
||
295 | |||
296 | $this->addedFonts[$fontHashCode] = $fontIndex; |
||
297 | } |
||
298 | |||
299 | return $fontIndex; |
||
300 | } |
||
301 | |||
302 | /** |
||
303 | * Alter color palette adding a custom color. |
||
304 | * |
||
305 | * @param string $rgb E.g. 'FF00AA' |
||
306 | * |
||
307 | * @return int Color index |
||
308 | */ |
||
309 | 9 | private function addColor($rgb) |
|
345 | |||
346 | /** |
||
347 | * Sets the colour palette to the Excel 97+ default. |
||
348 | */ |
||
349 | 9 | private function setPaletteXl97() |
|
410 | |||
411 | /** |
||
412 | * Assemble worksheets into a workbook and send the BIFF data to an OLE |
||
413 | * storage. |
||
414 | * |
||
415 | * @param array $pWorksheetSizes The sizes in bytes of the binary worksheet streams |
||
416 | * |
||
417 | * @return string Binary data for workbook stream |
||
418 | */ |
||
419 | public function writeWorkbook(array $pWorksheetSizes) |
||
420 | { |
||
421 | $this->worksheetSizes = $pWorksheetSizes; |
||
422 | |||
423 | // Calculate the number of selected worksheet tabs and call the finalization |
||
424 | // methods for each worksheet |
||
425 | $total_worksheets = $this->spreadsheet->getSheetCount(); |
||
426 | |||
427 | // Add part 1 of the Workbook globals, what goes before the SHEET records |
||
428 | $this->storeBof(0x0005); |
||
429 | $this->writeCodepage(); |
||
430 | $this->writeWindow1(); |
||
431 | |||
432 | $this->writeDateMode(); |
||
433 | $this->writeAllFonts(); |
||
434 | $this->writeAllNumberFormats(); |
||
435 | $this->writeAllXfs(); |
||
436 | $this->writeAllStyles(); |
||
437 | $this->writePalette(); |
||
438 | |||
439 | // Prepare part 3 of the workbook global stream, what goes after the SHEET records |
||
440 | $part3 = ''; |
||
441 | if ($this->countryCode != -1) { |
||
442 | $part3 .= $this->writeCountry(); |
||
443 | } |
||
444 | $part3 .= $this->writeRecalcId(); |
||
445 | |||
446 | $part3 .= $this->writeSupbookInternal(); |
||
447 | /* TODO: store external SUPBOOK records and XCT and CRN records |
||
448 | in case of external references for BIFF8 */ |
||
449 | $part3 .= $this->writeExternalsheetBiff8(); |
||
450 | $part3 .= $this->writeAllDefinedNamesBiff8(); |
||
451 | $part3 .= $this->writeMsoDrawingGroup(); |
||
452 | $part3 .= $this->writeSharedStringsTable(); |
||
453 | |||
454 | $part3 .= $this->writeEof(); |
||
455 | |||
456 | // Add part 2 of the Workbook globals, the SHEET records |
||
457 | $this->calcSheetOffsets(); |
||
458 | for ($i = 0; $i < $total_worksheets; ++$i) { |
||
459 | $this->writeBoundSheet($this->spreadsheet->getSheet($i), $this->worksheetOffsets[$i]); |
||
460 | } |
||
461 | |||
462 | // Add part 3 of the Workbook globals |
||
463 | $this->_data .= $part3; |
||
464 | |||
465 | return $this->_data; |
||
466 | } |
||
467 | |||
468 | /** |
||
469 | * Calculate offsets for Worksheet BOF records. |
||
470 | */ |
||
471 | private function calcSheetOffsets() |
||
472 | { |
||
473 | $boundsheet_length = 10; // fixed length for a BOUNDSHEET record |
||
474 | |||
475 | // size of Workbook globals part 1 + 3 |
||
476 | $offset = $this->_datasize; |
||
477 | |||
478 | // add size of Workbook globals part 2, the length of the SHEET records |
||
479 | $total_worksheets = count($this->spreadsheet->getAllSheets()); |
||
480 | foreach ($this->spreadsheet->getWorksheetIterator() as $sheet) { |
||
481 | $offset += $boundsheet_length + strlen(StringHelper::UTF8toBIFF8UnicodeShort($sheet->getTitle())); |
||
482 | } |
||
483 | |||
484 | // add the sizes of each of the Sheet substreams, respectively |
||
485 | for ($i = 0; $i < $total_worksheets; ++$i) { |
||
486 | $this->worksheetOffsets[$i] = $offset; |
||
487 | $offset += $this->worksheetSizes[$i]; |
||
488 | } |
||
489 | $this->biffSize = $offset; |
||
490 | } |
||
491 | |||
492 | /** |
||
493 | * Store the Excel FONT records. |
||
494 | */ |
||
495 | private function writeAllFonts() |
||
496 | { |
||
497 | foreach ($this->fontWriters as $fontWriter) { |
||
498 | $this->append($fontWriter->writeFont()); |
||
499 | } |
||
500 | } |
||
501 | |||
502 | /** |
||
503 | * Store user defined numerical formats i.e. FORMAT records. |
||
504 | */ |
||
505 | private function writeAllNumberFormats() |
||
506 | { |
||
507 | foreach ($this->numberFormats as $numberFormatIndex => $numberFormat) { |
||
508 | $this->writeNumberFormat($numberFormat->getFormatCode(), $numberFormatIndex); |
||
509 | } |
||
510 | } |
||
511 | |||
512 | /** |
||
513 | * Write all XF records. |
||
514 | */ |
||
515 | private function writeAllXfs() |
||
516 | { |
||
517 | foreach ($this->xfWriters as $xfWriter) { |
||
518 | $this->append($xfWriter->writeXf()); |
||
519 | } |
||
520 | } |
||
521 | |||
522 | /** |
||
523 | * Write all STYLE records. |
||
524 | */ |
||
525 | private function writeAllStyles() |
||
526 | { |
||
527 | $this->writeStyle(); |
||
528 | } |
||
529 | |||
530 | /** |
||
531 | * Write the EXTERNCOUNT and EXTERNSHEET records. These are used as indexes for |
||
532 | * the NAME records. |
||
533 | */ |
||
534 | private function writeExternals() |
||
545 | |||
546 | /** |
||
547 | * Write the NAME record to define the print area and the repeat rows and cols. |
||
548 | */ |
||
549 | private function writeNames() |
||
637 | |||
638 | /** |
||
639 | * Writes all the DEFINEDNAME records (BIFF8). |
||
640 | * So far this is only used for repeating rows/columns (print titles) and print areas. |
||
641 | */ |
||
642 | private function writeAllDefinedNamesBiff8() |
||
643 | { |
||
644 | $chunk = ''; |
||
645 | |||
646 | // Named ranges |
||
647 | if (count($this->spreadsheet->getNamedRanges()) > 0) { |
||
648 | // Loop named ranges |
||
649 | $namedRanges = $this->spreadsheet->getNamedRanges(); |
||
650 | foreach ($namedRanges as $namedRange) { |
||
651 | // Create absolute coordinate |
||
652 | $range = Cell::splitRange($namedRange->getRange()); |
||
653 | View Code Duplication | for ($i = 0; $i < count($range); ++$i) { |
|
654 | $range[$i][0] = '\'' . str_replace("'", "''", $namedRange->getWorksheet()->getTitle()) . '\'!' . Cell::absoluteCoordinate($range[$i][0]); |
||
655 | if (isset($range[$i][1])) { |
||
656 | $range[$i][1] = Cell::absoluteCoordinate($range[$i][1]); |
||
657 | } |
||
658 | } |
||
659 | $range = Cell::buildRange($range); // e.g. Sheet1!$A$1:$B$2 |
||
660 | |||
661 | // parse formula |
||
662 | try { |
||
663 | $error = $this->parser->parse($range); |
||
664 | $formulaData = $this->parser->toReversePolish(); |
||
665 | |||
666 | // make sure tRef3d is of type tRef3dR (0x3A) |
||
667 | if (isset($formulaData[0]) and ($formulaData[0] == "\x7A" or $formulaData[0] == "\x5A")) { |
||
668 | $formulaData = "\x3A" . substr($formulaData, 1); |
||
669 | } |
||
670 | |||
671 | if ($namedRange->getLocalOnly()) { |
||
672 | // local scope |
||
673 | $scope = $this->spreadsheet->getIndex($namedRange->getScope()) + 1; |
||
674 | } else { |
||
675 | // global scope |
||
676 | $scope = 0; |
||
677 | } |
||
678 | $chunk .= $this->writeData($this->writeDefinedNameBiff8($namedRange->getName(), $formulaData, $scope, false)); |
||
679 | } catch (PhpSpreadsheetException $e) { |
||
680 | // do nothing |
||
681 | } |
||
682 | } |
||
683 | } |
||
684 | |||
685 | // total number of sheets |
||
686 | $total_worksheets = $this->spreadsheet->getSheetCount(); |
||
687 | |||
688 | // write the print titles (repeating rows, columns), if any |
||
689 | for ($i = 0; $i < $total_worksheets; ++$i) { |
||
690 | $sheetSetup = $this->spreadsheet->getSheet($i)->getPageSetup(); |
||
691 | // simultaneous repeatColumns repeatRows |
||
692 | if ($sheetSetup->isColumnsToRepeatAtLeftSet() && $sheetSetup->isRowsToRepeatAtTopSet()) { |
||
693 | $repeat = $sheetSetup->getColumnsToRepeatAtLeft(); |
||
694 | $colmin = Cell::columnIndexFromString($repeat[0]) - 1; |
||
695 | $colmax = Cell::columnIndexFromString($repeat[1]) - 1; |
||
696 | |||
697 | $repeat = $sheetSetup->getRowsToRepeatAtTop(); |
||
698 | $rowmin = $repeat[0] - 1; |
||
699 | $rowmax = $repeat[1] - 1; |
||
700 | |||
701 | // construct formula data manually |
||
702 | $formulaData = pack('Cv', 0x29, 0x17); // tMemFunc |
||
703 | $formulaData .= pack('Cvvvvv', 0x3B, $i, 0, 65535, $colmin, $colmax); // tArea3d |
||
704 | $formulaData .= pack('Cvvvvv', 0x3B, $i, $rowmin, $rowmax, 0, 255); // tArea3d |
||
705 | $formulaData .= pack('C', 0x10); // tList |
||
706 | |||
707 | // store the DEFINEDNAME record |
||
708 | $chunk .= $this->writeData($this->writeDefinedNameBiff8(pack('C', 0x07), $formulaData, $i + 1, true)); |
||
709 | |||
710 | // (exclusive) either repeatColumns or repeatRows |
||
711 | } elseif ($sheetSetup->isColumnsToRepeatAtLeftSet() || $sheetSetup->isRowsToRepeatAtTopSet()) { |
||
712 | // Columns to repeat |
||
713 | View Code Duplication | if ($sheetSetup->isColumnsToRepeatAtLeftSet()) { |
|
714 | $repeat = $sheetSetup->getColumnsToRepeatAtLeft(); |
||
715 | $colmin = Cell::columnIndexFromString($repeat[0]) - 1; |
||
716 | $colmax = Cell::columnIndexFromString($repeat[1]) - 1; |
||
717 | } else { |
||
718 | $colmin = 0; |
||
719 | $colmax = 255; |
||
720 | } |
||
721 | // Rows to repeat |
||
722 | View Code Duplication | if ($sheetSetup->isRowsToRepeatAtTopSet()) { |
|
723 | $repeat = $sheetSetup->getRowsToRepeatAtTop(); |
||
724 | $rowmin = $repeat[0] - 1; |
||
725 | $rowmax = $repeat[1] - 1; |
||
726 | } else { |
||
727 | $rowmin = 0; |
||
728 | $rowmax = 65535; |
||
729 | } |
||
730 | |||
731 | // construct formula data manually because parser does not recognize absolute 3d cell references |
||
732 | $formulaData = pack('Cvvvvv', 0x3B, $i, $rowmin, $rowmax, $colmin, $colmax); |
||
733 | |||
734 | // store the DEFINEDNAME record |
||
735 | $chunk .= $this->writeData($this->writeDefinedNameBiff8(pack('C', 0x07), $formulaData, $i + 1, true)); |
||
736 | } |
||
737 | } |
||
738 | |||
739 | // write the print areas, if any |
||
740 | for ($i = 0; $i < $total_worksheets; ++$i) { |
||
741 | $sheetSetup = $this->spreadsheet->getSheet($i)->getPageSetup(); |
||
742 | if ($sheetSetup->isPrintAreaSet()) { |
||
743 | // Print area, e.g. A3:J6,H1:X20 |
||
744 | $printArea = Cell::splitRange($sheetSetup->getPrintArea()); |
||
745 | $countPrintArea = count($printArea); |
||
746 | |||
747 | $formulaData = ''; |
||
748 | for ($j = 0; $j < $countPrintArea; ++$j) { |
||
749 | $printAreaRect = $printArea[$j]; // e.g. A3:J6 |
||
750 | $printAreaRect[0] = Cell::coordinateFromString($printAreaRect[0]); |
||
751 | $printAreaRect[1] = Cell::coordinateFromString($printAreaRect[1]); |
||
752 | |||
753 | $print_rowmin = $printAreaRect[0][1] - 1; |
||
754 | $print_rowmax = $printAreaRect[1][1] - 1; |
||
755 | $print_colmin = Cell::columnIndexFromString($printAreaRect[0][0]) - 1; |
||
756 | $print_colmax = Cell::columnIndexFromString($printAreaRect[1][0]) - 1; |
||
757 | |||
758 | // construct formula data manually because parser does not recognize absolute 3d cell references |
||
759 | $formulaData .= pack('Cvvvvv', 0x3B, $i, $print_rowmin, $print_rowmax, $print_colmin, $print_colmax); |
||
760 | |||
761 | if ($j > 0) { |
||
762 | $formulaData .= pack('C', 0x10); // list operator token ',' |
||
763 | } |
||
764 | } |
||
765 | |||
766 | // store the DEFINEDNAME record |
||
767 | $chunk .= $this->writeData($this->writeDefinedNameBiff8(pack('C', 0x06), $formulaData, $i + 1, true)); |
||
768 | } |
||
769 | } |
||
770 | |||
771 | // write autofilters, if any |
||
772 | for ($i = 0; $i < $total_worksheets; ++$i) { |
||
773 | $sheetAutoFilter = $this->spreadsheet->getSheet($i)->getAutoFilter(); |
||
774 | $autoFilterRange = $sheetAutoFilter->getRange(); |
||
775 | if (!empty($autoFilterRange)) { |
||
776 | $rangeBounds = Cell::rangeBoundaries($autoFilterRange); |
||
777 | |||
778 | //Autofilter built in name |
||
779 | $name = pack('C', 0x0D); |
||
780 | |||
781 | $chunk .= $this->writeData($this->writeShortNameBiff8($name, $i + 1, $rangeBounds, true)); |
||
782 | } |
||
783 | } |
||
784 | |||
785 | return $chunk; |
||
786 | } |
||
787 | |||
788 | /** |
||
789 | * Write a DEFINEDNAME record for BIFF8 using explicit binary formula data. |
||
790 | * |
||
791 | * @param string $name The name in UTF-8 |
||
792 | * @param string $formulaData The binary formula data |
||
793 | * @param string $sheetIndex 1-based sheet index the defined name applies to. 0 = global |
||
794 | * @param bool $isBuiltIn Built-in name? |
||
795 | * |
||
796 | * @return string Complete binary record data |
||
797 | */ |
||
798 | private function writeDefinedNameBiff8($name, $formulaData, $sheetIndex = 0, $isBuiltIn = false) |
||
799 | { |
||
800 | $record = 0x0018; |
||
801 | |||
802 | // option flags |
||
803 | $options = $isBuiltIn ? 0x20 : 0x00; |
||
804 | |||
805 | // length of the name, character count |
||
806 | $nlen = StringHelper::countCharacters($name); |
||
807 | |||
808 | // name with stripped length field |
||
809 | $name = substr(StringHelper::UTF8toBIFF8UnicodeLong($name), 2); |
||
810 | |||
811 | // size of the formula (in bytes) |
||
812 | $sz = strlen($formulaData); |
||
813 | |||
814 | // combine the parts |
||
815 | $data = pack('vCCvvvCCCC', $options, 0, $nlen, $sz, 0, $sheetIndex, 0, 0, 0, 0) |
||
816 | . $name . $formulaData; |
||
817 | $length = strlen($data); |
||
818 | |||
819 | $header = pack('vv', $record, $length); |
||
820 | |||
821 | return $header . $data; |
||
822 | } |
||
823 | |||
824 | /** |
||
825 | * Write a short NAME record. |
||
826 | * |
||
827 | * @param string $name |
||
828 | * @param string $sheetIndex 1-based sheet index the defined name applies to. 0 = global |
||
829 | * @param integer[][] $rangeBounds range boundaries |
||
830 | * @param bool $isHidden |
||
831 | * |
||
832 | * @return string Complete binary record data |
||
833 | * */ |
||
834 | private function writeShortNameBiff8($name, $sheetIndex, $rangeBounds, $isHidden = false) |
||
835 | { |
||
836 | $record = 0x0018; |
||
837 | |||
838 | // option flags |
||
839 | $options = ($isHidden ? 0x21 : 0x00); |
||
840 | |||
841 | $extra = pack( |
||
842 | 'Cvvvvv', |
||
843 | 0x3B, |
||
844 | $sheetIndex - 1, |
||
845 | $rangeBounds[0][1] - 1, |
||
846 | $rangeBounds[1][1] - 1, |
||
847 | $rangeBounds[0][0] - 1, |
||
848 | $rangeBounds[1][0] - 1 |
||
849 | ); |
||
850 | |||
851 | // size of the formula (in bytes) |
||
852 | $sz = strlen($extra); |
||
853 | |||
854 | // combine the parts |
||
855 | $data = pack('vCCvvvCCCCC', $options, 0, 1, $sz, 0, $sheetIndex, 0, 0, 0, 0, 0) |
||
856 | . $name . $extra; |
||
857 | $length = strlen($data); |
||
858 | |||
859 | $header = pack('vv', $record, $length); |
||
860 | |||
861 | return $header . $data; |
||
862 | } |
||
863 | |||
864 | /** |
||
865 | * Stores the CODEPAGE biff record. |
||
866 | */ |
||
867 | private function writeCodepage() |
||
868 | { |
||
869 | $record = 0x0042; // Record identifier |
||
870 | $length = 0x0002; // Number of bytes to follow |
||
871 | $cv = $this->codepage; // The code page |
||
872 | |||
873 | $header = pack('vv', $record, $length); |
||
874 | $data = pack('v', $cv); |
||
875 | |||
876 | $this->append($header . $data); |
||
877 | } |
||
878 | |||
879 | /** |
||
880 | * Write Excel BIFF WINDOW1 record. |
||
881 | */ |
||
882 | private function writeWindow1() |
||
883 | { |
||
884 | $record = 0x003D; // Record identifier |
||
885 | $length = 0x0012; // Number of bytes to follow |
||
886 | |||
887 | $xWn = 0x0000; // Horizontal position of window |
||
888 | $yWn = 0x0000; // Vertical position of window |
||
889 | $dxWn = 0x25BC; // Width of window |
||
890 | $dyWn = 0x1572; // Height of window |
||
891 | |||
892 | $grbit = 0x0038; // Option flags |
||
893 | |||
894 | // not supported by PhpSpreadsheet, so there is only one selected sheet, the active |
||
895 | $ctabsel = 1; // Number of workbook tabs selected |
||
896 | |||
897 | $wTabRatio = 0x0258; // Tab to scrollbar ratio |
||
898 | |||
899 | // not supported by PhpSpreadsheet, set to 0 |
||
900 | $itabFirst = 0; // 1st displayed worksheet |
||
901 | $itabCur = $this->spreadsheet->getActiveSheetIndex(); // Active worksheet |
||
902 | |||
903 | $header = pack('vv', $record, $length); |
||
904 | $data = pack('vvvvvvvvv', $xWn, $yWn, $dxWn, $dyWn, $grbit, $itabCur, $itabFirst, $ctabsel, $wTabRatio); |
||
905 | $this->append($header . $data); |
||
906 | } |
||
907 | |||
908 | /** |
||
909 | * Writes Excel BIFF BOUNDSHEET record. |
||
910 | * |
||
911 | * @param \PhpOffice\PhpSpreadsheet\Worksheet $sheet Worksheet name |
||
912 | * @param int $offset Location of worksheet BOF |
||
913 | */ |
||
914 | private function writeBoundSheet($sheet, $offset) |
||
915 | { |
||
916 | $sheetname = $sheet->getTitle(); |
||
917 | $record = 0x0085; // Record identifier |
||
918 | |||
919 | // sheet state |
||
920 | switch ($sheet->getSheetState()) { |
||
921 | case \PhpOffice\PhpSpreadsheet\Worksheet::SHEETSTATE_VISIBLE: |
||
922 | $ss = 0x00; |
||
923 | break; |
||
924 | case \PhpOffice\PhpSpreadsheet\Worksheet::SHEETSTATE_HIDDEN: |
||
925 | $ss = 0x01; |
||
926 | break; |
||
927 | case \PhpOffice\PhpSpreadsheet\Worksheet::SHEETSTATE_VERYHIDDEN: |
||
928 | $ss = 0x02; |
||
929 | break; |
||
930 | default: |
||
931 | $ss = 0x00; |
||
932 | break; |
||
933 | } |
||
934 | |||
935 | // sheet type |
||
936 | $st = 0x00; |
||
937 | |||
938 | $grbit = 0x0000; // Visibility and sheet type |
||
939 | |||
940 | $data = pack('VCC', $offset, $ss, $st); |
||
941 | $data .= StringHelper::UTF8toBIFF8UnicodeShort($sheetname); |
||
942 | |||
943 | $length = strlen($data); |
||
944 | $header = pack('vv', $record, $length); |
||
945 | $this->append($header . $data); |
||
946 | } |
||
947 | |||
948 | /** |
||
949 | * Write Internal SUPBOOK record. |
||
950 | */ |
||
951 | View Code Duplication | private function writeSupbookInternal() |
|
952 | { |
||
953 | $record = 0x01AE; // Record identifier |
||
954 | $length = 0x0004; // Bytes to follow |
||
955 | |||
956 | $header = pack('vv', $record, $length); |
||
957 | $data = pack('vv', $this->spreadsheet->getSheetCount(), 0x0401); |
||
958 | |||
959 | return $this->writeData($header . $data); |
||
960 | } |
||
961 | |||
962 | /** |
||
963 | * Writes the Excel BIFF EXTERNSHEET record. These references are used by |
||
964 | * formulas. |
||
965 | */ |
||
966 | private function writeExternalsheetBiff8() |
||
967 | { |
||
968 | $totalReferences = count($this->parser->references); |
||
969 | $record = 0x0017; // Record identifier |
||
970 | $length = 2 + 6 * $totalReferences; // Number of bytes to follow |
||
971 | |||
972 | $supbook_index = 0; // FIXME: only using internal SUPBOOK record |
||
973 | $header = pack('vv', $record, $length); |
||
974 | $data = pack('v', $totalReferences); |
||
975 | for ($i = 0; $i < $totalReferences; ++$i) { |
||
976 | $data .= $this->parser->references[$i]; |
||
977 | } |
||
978 | |||
979 | return $this->writeData($header . $data); |
||
980 | } |
||
981 | |||
982 | /** |
||
983 | * Write Excel BIFF STYLE records. |
||
984 | */ |
||
985 | private function writeStyle() |
||
986 | { |
||
987 | $record = 0x0293; // Record identifier |
||
988 | $length = 0x0004; // Bytes to follow |
||
989 | |||
990 | $ixfe = 0x8000; // Index to cell style XF |
||
991 | $BuiltIn = 0x00; // Built-in style |
||
992 | $iLevel = 0xff; // Outline style level |
||
993 | |||
994 | $header = pack('vv', $record, $length); |
||
995 | $data = pack('vCC', $ixfe, $BuiltIn, $iLevel); |
||
996 | $this->append($header . $data); |
||
997 | } |
||
998 | |||
999 | /** |
||
1000 | * Writes Excel FORMAT record for non "built-in" numerical formats. |
||
1001 | * |
||
1002 | * @param string $format Custom format string |
||
1003 | * @param int $ifmt Format index code |
||
1004 | */ |
||
1005 | View Code Duplication | private function writeNumberFormat($format, $ifmt) |
|
1006 | { |
||
1007 | $record = 0x041E; // Record identifier |
||
1008 | |||
1009 | $numberFormatString = StringHelper::UTF8toBIFF8UnicodeLong($format); |
||
1010 | $length = 2 + strlen($numberFormatString); // Number of bytes to follow |
||
1011 | |||
1012 | $header = pack('vv', $record, $length); |
||
1013 | $data = pack('v', $ifmt) . $numberFormatString; |
||
1014 | $this->append($header . $data); |
||
1015 | } |
||
1016 | |||
1017 | /** |
||
1018 | * Write DATEMODE record to indicate the date system in use (1904 or 1900). |
||
1019 | */ |
||
1020 | private function writeDateMode() |
||
1021 | { |
||
1022 | $record = 0x0022; // Record identifier |
||
1023 | $length = 0x0002; // Bytes to follow |
||
1024 | |||
1025 | $f1904 = (Date::getExcelCalendar() == Date::CALENDAR_MAC_1904) |
||
1026 | ? 1 |
||
1027 | : 0; // Flag for 1904 date system |
||
1028 | |||
1029 | $header = pack('vv', $record, $length); |
||
1030 | $data = pack('v', $f1904); |
||
1031 | $this->append($header . $data); |
||
1032 | } |
||
1033 | |||
1034 | /** |
||
1035 | * Write BIFF record EXTERNCOUNT to indicate the number of external sheet |
||
1036 | * references in the workbook. |
||
1037 | * |
||
1038 | * Excel only stores references to external sheets that are used in NAME. |
||
1039 | * The workbook NAME record is required to define the print area and the repeat |
||
1040 | * rows and columns. |
||
1041 | * |
||
1042 | * A similar method is used in Worksheet.php for a slightly different purpose. |
||
1043 | * |
||
1044 | * @param int $cxals Number of external references |
||
1045 | */ |
||
1046 | private function writeExternalCount($cxals) |
||
1055 | |||
1056 | /** |
||
1057 | * Writes the Excel BIFF EXTERNSHEET record. These references are used by |
||
1058 | * formulas. NAME record is required to define the print area and the repeat |
||
1059 | * rows and columns. |
||
1060 | * |
||
1061 | * A similar method is used in Worksheet.php for a slightly different purpose. |
||
1062 | * |
||
1063 | * @param string $sheetname Worksheet name |
||
1064 | */ |
||
1065 | View Code Duplication | private function writeExternalSheet($sheetname) |
|
1077 | |||
1078 | /** |
||
1079 | * Store the NAME record in the short format that is used for storing the print |
||
1080 | * area, repeat rows only and repeat columns only. |
||
1081 | * |
||
1082 | * @param int $index Sheet index |
||
1083 | * @param int $type Built-in name type |
||
1084 | * @param int $rowmin Start row |
||
1085 | * @param int $rowmax End row |
||
1086 | * @param int $colmin Start colum |
||
1087 | * @param int $colmax End column |
||
1088 | */ |
||
1089 | private function writeNameShort($index, $type, $rowmin, $rowmax, $colmin, $colmax) |
||
1139 | |||
1140 | /** |
||
1141 | * Store the NAME record in the long format that is used for storing the repeat |
||
1142 | * rows and columns when both are specified. This shares a lot of code with |
||
1143 | * writeNameShort() but we use a separate method to keep the code clean. |
||
1144 | * Code abstraction for reuse can be carried too far, and I should know. ;-). |
||
1145 | * |
||
1146 | * @param int $index Sheet index |
||
1147 | * @param int $type Built-in name type |
||
1148 | * @param int $rowmin Start row |
||
1149 | * @param int $rowmax End row |
||
1150 | * @param int $colmin Start colum |
||
1151 | * @param int $colmax End column |
||
1152 | */ |
||
1153 | private function writeNameLong($index, $type, $rowmin, $rowmax, $colmin, $colmax) |
||
1222 | |||
1223 | /** |
||
1224 | * Stores the COUNTRY record for localization. |
||
1225 | * |
||
1226 | * @return string |
||
1227 | */ |
||
1228 | View Code Duplication | private function writeCountry() |
|
1239 | |||
1240 | /** |
||
1241 | * Write the RECALCID record. |
||
1242 | * |
||
1243 | * @return string |
||
1244 | */ |
||
1245 | View Code Duplication | private function writeRecalcId() |
|
1246 | { |
||
1247 | $record = 0x01C1; // Record identifier |
||
1248 | $length = 8; // Number of bytes to follow |
||
1249 | |||
1250 | $header = pack('vv', $record, $length); |
||
1251 | |||
1252 | // by inspection of real Excel files, MS Office Excel 2007 writes this |
||
1253 | $data = pack('VV', 0x000001C1, 0x00001E667); |
||
1254 | |||
1255 | return $this->writeData($header . $data); |
||
1256 | } |
||
1257 | |||
1258 | /** |
||
1259 | * Stores the PALETTE biff record. |
||
1260 | */ |
||
1261 | private function writePalette() |
||
1262 | { |
||
1263 | $aref = $this->palette; |
||
1264 | |||
1265 | $record = 0x0092; // Record identifier |
||
1266 | $length = 2 + 4 * count($aref); // Number of bytes to follow |
||
1267 | $ccv = count($aref); // Number of RGB values to follow |
||
1268 | $data = ''; // The RGB data |
||
1269 | |||
1270 | // Pack the RGB data |
||
1271 | foreach ($aref as $color) { |
||
1272 | foreach ($color as $byte) { |
||
1273 | $data .= pack('C', $byte); |
||
1274 | } |
||
1275 | } |
||
1276 | |||
1277 | $header = pack('vvv', $record, $length, $ccv); |
||
1278 | $this->append($header . $data); |
||
1279 | } |
||
1280 | |||
1281 | /** |
||
1282 | * Handling of the SST continue blocks is complicated by the need to include an |
||
1283 | * additional continuation byte depending on whether the string is split between |
||
1284 | * blocks or whether it starts at the beginning of the block. (There are also |
||
1285 | * additional complications that will arise later when/if Rich Strings are |
||
1286 | * supported). |
||
1287 | * |
||
1288 | * The Excel documentation says that the SST record should be followed by an |
||
1289 | * EXTSST record. The EXTSST record is a hash table that is used to optimise |
||
1290 | * access to SST. However, despite the documentation it doesn't seem to be |
||
1291 | * required so we will ignore it. |
||
1292 | * |
||
1293 | * @return string Binary data |
||
1294 | */ |
||
1295 | private function writeSharedStringsTable() |
||
1296 | { |
||
1297 | // maximum size of record data (excluding record header) |
||
1298 | $continue_limit = 8224; |
||
1299 | |||
1300 | // initialize array of record data blocks |
||
1301 | $recordDatas = []; |
||
1302 | |||
1303 | // start SST record data block with total number of strings, total number of unique strings |
||
1304 | $recordData = pack('VV', $this->stringTotal, $this->stringUnique); |
||
1305 | |||
1306 | // loop through all (unique) strings in shared strings table |
||
1307 | foreach (array_keys($this->stringTable) as $string) { |
||
1308 | // here $string is a BIFF8 encoded string |
||
1309 | |||
1310 | // length = character count |
||
1311 | $headerinfo = unpack('vlength/Cencoding', $string); |
||
1312 | |||
1313 | // currently, this is always 1 = uncompressed |
||
1314 | $encoding = $headerinfo['encoding']; |
||
1315 | |||
1316 | // initialize finished writing current $string |
||
1317 | $finished = false; |
||
1318 | |||
1319 | while ($finished === false) { |
||
1320 | // normally, there will be only one cycle, but if string cannot immediately be written as is |
||
1321 | // there will be need for more than one cylcle, if string longer than one record data block, there |
||
1322 | // may be need for even more cycles |
||
1323 | |||
1324 | if (strlen($recordData) + strlen($string) <= $continue_limit) { |
||
1325 | // then we can write the string (or remainder of string) without any problems |
||
1326 | $recordData .= $string; |
||
1327 | |||
1328 | if (strlen($recordData) + strlen($string) == $continue_limit) { |
||
1329 | // we close the record data block, and initialize a new one |
||
1330 | $recordDatas[] = $recordData; |
||
1331 | $recordData = ''; |
||
1332 | } |
||
1333 | |||
1334 | // we are finished writing this string |
||
1335 | $finished = true; |
||
1336 | } else { |
||
1337 | // special treatment writing the string (or remainder of the string) |
||
1338 | // If the string is very long it may need to be written in more than one CONTINUE record. |
||
1339 | |||
1340 | // check how many bytes more there is room for in the current record |
||
1341 | $space_remaining = $continue_limit - strlen($recordData); |
||
1342 | |||
1343 | // minimum space needed |
||
1344 | // uncompressed: 2 byte string length length field + 1 byte option flags + 2 byte character |
||
1345 | // compressed: 2 byte string length length field + 1 byte option flags + 1 byte character |
||
1346 | $min_space_needed = ($encoding == 1) ? 5 : 4; |
||
1347 | |||
1348 | // We have two cases |
||
1349 | // 1. space remaining is less than minimum space needed |
||
1350 | // here we must waste the space remaining and move to next record data block |
||
1351 | // 2. space remaining is greater than or equal to minimum space needed |
||
1352 | // here we write as much as we can in the current block, then move to next record data block |
||
1353 | |||
1354 | // 1. space remaining is less than minimum space needed |
||
1355 | if ($space_remaining < $min_space_needed) { |
||
1356 | // we close the block, store the block data |
||
1357 | $recordDatas[] = $recordData; |
||
1358 | |||
1359 | // and start new record data block where we start writing the string |
||
1360 | $recordData = ''; |
||
1361 | |||
1362 | // 2. space remaining is greater than or equal to minimum space needed |
||
1363 | } else { |
||
1364 | // initialize effective remaining space, for Unicode strings this may need to be reduced by 1, see below |
||
1365 | $effective_space_remaining = $space_remaining; |
||
1366 | |||
1367 | // for uncompressed strings, sometimes effective space remaining is reduced by 1 |
||
1368 | if ($encoding == 1 && (strlen($string) - $space_remaining) % 2 == 1) { |
||
1369 | --$effective_space_remaining; |
||
1370 | } |
||
1371 | |||
1372 | // one block fininshed, store the block data |
||
1373 | $recordData .= substr($string, 0, $effective_space_remaining); |
||
1374 | |||
1375 | $string = substr($string, $effective_space_remaining); // for next cycle in while loop |
||
1376 | $recordDatas[] = $recordData; |
||
1377 | |||
1378 | // start new record data block with the repeated option flags |
||
1379 | $recordData = pack('C', $encoding); |
||
1380 | } |
||
1381 | } |
||
1382 | } |
||
1383 | } |
||
1384 | |||
1385 | // Store the last record data block unless it is empty |
||
1386 | // if there was no need for any continue records, this will be the for SST record data block itself |
||
1387 | if (strlen($recordData) > 0) { |
||
1388 | $recordDatas[] = $recordData; |
||
1389 | } |
||
1390 | |||
1391 | // combine into one chunk with all the blocks SST, CONTINUE,... |
||
1392 | $chunk = ''; |
||
1393 | foreach ($recordDatas as $i => $recordData) { |
||
1394 | // first block should have the SST record header, remaing should have CONTINUE header |
||
1395 | $record = ($i == 0) ? 0x00FC : 0x003C; |
||
1396 | |||
1397 | $header = pack('vv', $record, strlen($recordData)); |
||
1398 | $data = $header . $recordData; |
||
1399 | |||
1400 | $chunk .= $this->writeData($data); |
||
1401 | } |
||
1402 | |||
1403 | return $chunk; |
||
1404 | } |
||
1405 | |||
1406 | /** |
||
1407 | * Writes the MSODRAWINGGROUP record if needed. Possibly split using CONTINUE records. |
||
1408 | */ |
||
1409 | private function writeMsoDrawingGroup() |
||
1410 | { |
||
1411 | // write the Escher stream if necessary |
||
1412 | if (isset($this->escher)) { |
||
1413 | $writer = new Escher($this->escher); |
||
1414 | $data = $writer->close(); |
||
1415 | |||
1416 | $record = 0x00EB; |
||
1417 | $length = strlen($data); |
||
1418 | $header = pack('vv', $record, $length); |
||
1419 | |||
1420 | return $this->writeData($header . $data); |
||
1421 | } |
||
1422 | |||
1423 | return ''; |
||
1424 | } |
||
1425 | |||
1426 | /** |
||
1427 | * Get Escher object. |
||
1428 | * |
||
1429 | * @return \PhpOffice\PhpSpreadsheet\Shared\Escher |
||
1430 | */ |
||
1431 | public function getEscher() |
||
1435 | |||
1436 | /** |
||
1437 | * Set Escher object. |
||
1438 | * |
||
1439 | * @param \PhpOffice\PhpSpreadsheet\Shared\Escher $pValue |
||
1440 | */ |
||
1441 | public function setEscher(\PhpOffice\PhpSpreadsheet\Shared\Escher $pValue = null) |
||
1442 | { |
||
1443 | $this->escher = $pValue; |
||
1444 | } |
||
1445 | } |
||
1446 |