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 Worksheet 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 Worksheet, and based on these observations, apply Extract Interface, too.
1 | <?php |
||
78 | class Worksheet extends BIFFwriter |
||
79 | { |
||
80 | /** |
||
81 | * Formula parser. |
||
82 | * |
||
83 | * @var \PhpOffice\PhpSpreadsheet\Writer\Xls\Parser |
||
84 | */ |
||
85 | private $parser; |
||
86 | |||
87 | /** |
||
88 | * Maximum number of characters for a string (LABEL record in BIFF5). |
||
89 | * |
||
90 | * @var int |
||
91 | */ |
||
92 | private $xlsStringMaxLength; |
||
93 | |||
94 | /** |
||
95 | * Array containing format information for columns. |
||
96 | * |
||
97 | * @var array |
||
98 | */ |
||
99 | private $columnInfo; |
||
100 | |||
101 | /** |
||
102 | * Array containing the selected area for the worksheet. |
||
103 | * |
||
104 | * @var array |
||
105 | */ |
||
106 | private $selection; |
||
107 | |||
108 | /** |
||
109 | * The active pane for the worksheet. |
||
110 | * |
||
111 | * @var int |
||
112 | */ |
||
113 | private $activePane; |
||
114 | |||
115 | /** |
||
116 | * Whether to use outline. |
||
117 | * |
||
118 | * @var int |
||
119 | */ |
||
120 | private $outlineOn; |
||
121 | |||
122 | /** |
||
123 | * Auto outline styles. |
||
124 | * |
||
125 | * @var bool |
||
126 | */ |
||
127 | private $outlineStyle; |
||
128 | |||
129 | /** |
||
130 | * Whether to have outline summary below. |
||
131 | * |
||
132 | * @var bool |
||
133 | */ |
||
134 | private $outlineBelow; |
||
135 | |||
136 | /** |
||
137 | * Whether to have outline summary at the right. |
||
138 | * |
||
139 | * @var bool |
||
140 | */ |
||
141 | private $outlineRight; |
||
142 | |||
143 | /** |
||
144 | * Reference to the total number of strings in the workbook. |
||
145 | * |
||
146 | * @var int |
||
147 | */ |
||
148 | private $stringTotal; |
||
149 | |||
150 | /** |
||
151 | * Reference to the number of unique strings in the workbook. |
||
152 | * |
||
153 | * @var int |
||
154 | */ |
||
155 | private $stringUnique; |
||
156 | |||
157 | /** |
||
158 | * Reference to the array containing all the unique strings in the workbook. |
||
159 | * |
||
160 | * @var array |
||
161 | */ |
||
162 | private $stringTable; |
||
163 | |||
164 | /** |
||
165 | * Color cache. |
||
166 | */ |
||
167 | private $colors; |
||
168 | |||
169 | /** |
||
170 | * Index of first used row (at least 0). |
||
171 | * |
||
172 | * @var int |
||
173 | */ |
||
174 | private $firstRowIndex; |
||
175 | |||
176 | /** |
||
177 | * Index of last used row. (no used rows means -1). |
||
178 | * |
||
179 | * @var int |
||
180 | */ |
||
181 | private $lastRowIndex; |
||
182 | |||
183 | /** |
||
184 | * Index of first used column (at least 0). |
||
185 | * |
||
186 | * @var int |
||
187 | */ |
||
188 | private $firstColumnIndex; |
||
189 | |||
190 | /** |
||
191 | * Index of last used column (no used columns means -1). |
||
192 | * |
||
193 | * @var int |
||
194 | */ |
||
195 | private $lastColumnIndex; |
||
196 | |||
197 | /** |
||
198 | * Sheet object. |
||
199 | * |
||
200 | * @var \PhpOffice\PhpSpreadsheet\Worksheet |
||
201 | */ |
||
202 | public $phpSheet; |
||
203 | |||
204 | /** |
||
205 | * Count cell style Xfs. |
||
206 | * |
||
207 | * @var int |
||
208 | */ |
||
209 | private $countCellStyleXfs; |
||
210 | |||
211 | /** |
||
212 | * Escher object corresponding to MSODRAWING. |
||
213 | * |
||
214 | * @var \PhpOffice\PhpSpreadsheet\Shared\Escher |
||
215 | */ |
||
216 | private $escher; |
||
217 | |||
218 | /** |
||
219 | * Array of font hashes associated to FONT records index. |
||
220 | * |
||
221 | * @var array |
||
222 | */ |
||
223 | public $fontHashIndex; |
||
224 | |||
225 | /** |
||
226 | * @var bool |
||
227 | */ |
||
228 | private $preCalculateFormulas; |
||
229 | |||
230 | /** |
||
231 | * @var int |
||
232 | */ |
||
233 | private $printHeaders; |
||
234 | |||
235 | /** |
||
236 | * Constructor. |
||
237 | * |
||
238 | * @param int &$str_total Total number of strings |
||
239 | * @param int &$str_unique Total number of unique strings |
||
240 | * @param array &$str_table String Table |
||
241 | * @param array &$colors Colour Table |
||
242 | * @param mixed $parser The formula parser created for the Workbook |
||
243 | * @param bool $preCalculateFormulas Flag indicating whether formulas should be calculated or just written |
||
244 | * @param string $phpSheet The worksheet to write |
||
245 | * @param \PhpOffice\PhpSpreadsheet\Worksheet $phpSheet |
||
246 | */ |
||
247 | 39 | public function __construct(&$str_total, &$str_unique, &$str_table, &$colors, $parser, $preCalculateFormulas, $phpSheet) |
|
295 | |||
296 | /** |
||
297 | * Add data to the beginning of the workbook (note the reverse order) |
||
298 | * and to the end of the workbook. |
||
299 | * |
||
300 | * @see \PhpOffice\PhpSpreadsheet\Writer\Xls\Workbook::storeWorkbook() |
||
301 | */ |
||
302 | 39 | public function close() |
|
548 | |||
549 | /** |
||
550 | * Write a cell range address in BIFF8 |
||
551 | * always fixed range |
||
552 | * See section 2.5.14 in OpenOffice.org's Documentation of the Microsoft Excel File Format. |
||
553 | * |
||
554 | * @param string $range E.g. 'A1' or 'A1:B6' |
||
555 | * |
||
556 | * @return string Binary data |
||
557 | */ |
||
558 | 7 | private function writeBIFF8CellRangeAddressFixed($range) |
|
577 | |||
578 | /** |
||
579 | * Retrieves data from memory in one chunk, or from disk in $buffer |
||
580 | * sized chunks. |
||
581 | * |
||
582 | * @return string The data |
||
583 | */ |
||
584 | 39 | public function getData() |
|
598 | |||
599 | /** |
||
600 | * Set the option to print the row and column headers on the printed page. |
||
601 | * |
||
602 | * @param int $print Whether to print the headers or not. Defaults to 1 (print). |
||
603 | */ |
||
604 | public function printRowColHeaders($print = 1) |
||
608 | |||
609 | /** |
||
610 | * This method sets the properties for outlining and grouping. The defaults |
||
611 | * correspond to Excel's defaults. |
||
612 | * |
||
613 | * @param bool $visible |
||
614 | * @param bool $symbols_below |
||
615 | * @param bool $symbols_right |
||
616 | * @param bool $auto_style |
||
617 | */ |
||
618 | public function setOutline($visible = true, $symbols_below = true, $symbols_right = true, $auto_style = false) |
||
630 | |||
631 | /** |
||
632 | * Write a double to the specified row and column (zero indexed). |
||
633 | * An integer can be written as a double. Excel will display an |
||
634 | * integer. $format is optional. |
||
635 | * |
||
636 | * Returns 0 : normal termination |
||
637 | * -2 : row or column out of range |
||
638 | * |
||
639 | * @param int $row Zero indexed row |
||
640 | * @param int $col Zero indexed column |
||
641 | * @param float $num The number to write |
||
642 | * @param mixed $xfIndex The optional XF format |
||
643 | * |
||
644 | * @return int |
||
645 | */ |
||
646 | 23 | private function writeNumber($row, $col, $num, $xfIndex) |
|
662 | |||
663 | /** |
||
664 | * Write a LABELSST record or a LABEL record. Which one depends on BIFF version. |
||
665 | * |
||
666 | * @param int $row Row index (0-based) |
||
667 | * @param int $col Column index (0-based) |
||
668 | * @param string $str The string |
||
669 | * @param int $xfIndex Index to XF record |
||
670 | */ |
||
671 | 34 | private function writeString($row, $col, $str, $xfIndex) |
|
675 | |||
676 | /** |
||
677 | * Write a LABELSST record or a LABEL record. Which one depends on BIFF version |
||
678 | * It differs from writeString by the writing of rich text strings. |
||
679 | * |
||
680 | * @param int $row Row index (0-based) |
||
681 | * @param int $col Column index (0-based) |
||
682 | * @param string $str The string |
||
683 | * @param int $xfIndex The XF format index for the cell |
||
684 | * @param array $arrcRun Index to Font record and characters beginning |
||
685 | */ |
||
686 | 9 | View Code Duplication | private function writeRichTextString($row, $col, $str, $xfIndex, $arrcRun) |
702 | |||
703 | /** |
||
704 | * Write a string to the specified row and column (zero indexed). |
||
705 | * NOTE: there is an Excel 5 defined limit of 255 characters. |
||
706 | * $format is optional. |
||
707 | * Returns 0 : normal termination |
||
708 | * -2 : row or column out of range |
||
709 | * -3 : long string truncated to 255 chars. |
||
710 | * |
||
711 | * @param int $row Zero indexed row |
||
712 | * @param int $col Zero indexed column |
||
713 | * @param string $str The string to write |
||
714 | * @param mixed $xfIndex The XF format index for the cell |
||
715 | * |
||
716 | * @return int |
||
717 | */ |
||
718 | private function writeLabel($row, $col, $str, $xfIndex) |
||
739 | |||
740 | /** |
||
741 | * Write a string to the specified row and column (zero indexed). |
||
742 | * This is the BIFF8 version (no 255 chars limit). |
||
743 | * $format is optional. |
||
744 | * Returns 0 : normal termination |
||
745 | * -2 : row or column out of range |
||
746 | * -3 : long string truncated to 255 chars. |
||
747 | * |
||
748 | * @param int $row Zero indexed row |
||
749 | * @param int $col Zero indexed column |
||
750 | * @param string $str The string to write |
||
751 | * @param mixed $xfIndex The XF format index for the cell |
||
752 | * |
||
753 | * @return int |
||
754 | */ |
||
755 | 34 | View Code Duplication | private function writeLabelSst($row, $col, $str, $xfIndex) |
772 | |||
773 | /** |
||
774 | * Writes a note associated with the cell given by the row and column. |
||
775 | * NOTE records don't have a length limit. |
||
776 | * |
||
777 | * @param int $row Zero indexed row |
||
778 | * @param int $col Zero indexed column |
||
779 | * @param string $note The note to write |
||
780 | */ |
||
781 | private function writeNote($row, $col, $note) |
||
803 | |||
804 | /** |
||
805 | * Write a blank cell to the specified row and column (zero indexed). |
||
806 | * A blank cell is used to specify formatting without adding a string |
||
807 | * or a number. |
||
808 | * |
||
809 | * A blank cell without a format serves no purpose. Therefore, we don't write |
||
810 | * a BLANK record unless a format is specified. |
||
811 | * |
||
812 | * Returns 0 : normal termination (including no format) |
||
813 | * -1 : insufficient number of arguments |
||
814 | * -2 : row or column out of range |
||
815 | * |
||
816 | * @param int $row Zero indexed row |
||
817 | * @param int $col Zero indexed column |
||
818 | * @param mixed $xfIndex The XF format index |
||
819 | */ |
||
820 | 19 | View Code Duplication | public function writeBlank($row, $col, $xfIndex) |
831 | |||
832 | /** |
||
833 | * Write a boolean or an error type to the specified row and column (zero indexed). |
||
834 | * |
||
835 | * @param int $row Row index (0-based) |
||
836 | * @param int $col Column index (0-based) |
||
837 | * @param int $value |
||
838 | * @param bool $isError Error or Boolean? |
||
839 | * @param int $xfIndex |
||
840 | */ |
||
841 | 8 | View Code Duplication | private function writeBoolErr($row, $col, $value, $isError, $xfIndex) |
852 | |||
853 | /** |
||
854 | * Write a formula to the specified row and column (zero indexed). |
||
855 | * The textual representation of the formula is passed to the parser in |
||
856 | * Parser.php which returns a packed binary string. |
||
857 | * |
||
858 | * Returns 0 : normal termination |
||
859 | * -1 : formula errors (bad formula) |
||
860 | * -2 : row or column out of range |
||
861 | * |
||
862 | * @param int $row Zero indexed row |
||
863 | * @param int $col Zero indexed column |
||
864 | * @param string $formula The formula text string |
||
865 | * @param mixed $xfIndex The XF format index |
||
866 | * @param mixed $calculatedValue Calculated value |
||
867 | * |
||
868 | * @return int |
||
869 | */ |
||
870 | 16 | private function writeFormula($row, $col, $formula, $xfIndex, $calculatedValue) |
|
946 | |||
947 | /** |
||
948 | * Write a STRING record. This. |
||
949 | * |
||
950 | * @param string $stringValue |
||
951 | */ |
||
952 | 7 | View Code Duplication | private function writeStringRecord($stringValue) |
962 | |||
963 | /** |
||
964 | * Write a hyperlink. |
||
965 | * This is comprised of two elements: the visible label and |
||
966 | * the invisible link. The visible label is the same as the link unless an |
||
967 | * alternative string is specified. The label is written using the |
||
968 | * writeString() method. Therefore the 255 characters string limit applies. |
||
969 | * $string and $format are optional. |
||
970 | * |
||
971 | * The hyperlink can be to a http, ftp, mail, internal sheet (not yet), or external |
||
972 | * directory url. |
||
973 | * |
||
974 | * Returns 0 : normal termination |
||
975 | * -2 : row or column out of range |
||
976 | * -3 : long string truncated to 255 chars |
||
977 | * |
||
978 | * @param int $row Row |
||
979 | * @param int $col Column |
||
980 | * @param string $url URL string |
||
981 | * |
||
982 | * @return int |
||
983 | */ |
||
984 | 8 | private function writeUrl($row, $col, $url) |
|
989 | |||
990 | /** |
||
991 | * This is the more general form of writeUrl(). It allows a hyperlink to be |
||
992 | * written to a range of cells. This function also decides the type of hyperlink |
||
993 | * to be written. These are either, Web (http, ftp, mailto), Internal |
||
994 | * (Sheet1!A1) or external ('c:\temp\foo.xls#Sheet1!A1'). |
||
995 | * |
||
996 | * @see writeUrl() |
||
997 | * |
||
998 | * @param int $row1 Start row |
||
999 | * @param int $col1 Start column |
||
1000 | * @param int $row2 End row |
||
1001 | * @param int $col2 End column |
||
1002 | * @param string $url URL string |
||
1003 | * |
||
1004 | * @return int |
||
1005 | */ |
||
1006 | 8 | public function writeUrlRange($row1, $col1, $row2, $col2, $url) |
|
1018 | |||
1019 | /** |
||
1020 | * Used to write http, ftp and mailto hyperlinks. |
||
1021 | * The link type ($options) is 0x03 is the same as absolute dir ref without |
||
1022 | * sheet. However it is differentiated by the $unknown2 data stream. |
||
1023 | * |
||
1024 | * @see writeUrl() |
||
1025 | * |
||
1026 | * @param int $row1 Start row |
||
1027 | * @param int $col1 Start column |
||
1028 | * @param int $row2 End row |
||
1029 | * @param int $col2 End column |
||
1030 | * @param string $url URL string |
||
1031 | * |
||
1032 | * @return int |
||
1033 | */ |
||
1034 | 8 | public function writeUrlWeb($row1, $col1, $row2, $col2, $url) |
|
1065 | |||
1066 | /** |
||
1067 | * Used to write internal reference hyperlinks such as "Sheet1!A1". |
||
1068 | * |
||
1069 | * @see writeUrl() |
||
1070 | * |
||
1071 | * @param int $row1 Start row |
||
1072 | * @param int $col1 Start column |
||
1073 | * @param int $row2 End row |
||
1074 | * @param int $col2 End column |
||
1075 | * @param string $url URL string |
||
1076 | * |
||
1077 | * @return int |
||
1078 | */ |
||
1079 | 6 | public function writeUrlInternal($row1, $col1, $row2, $col2, $url) |
|
1114 | |||
1115 | /** |
||
1116 | * Write links to external directory names such as 'c:\foo.xls', |
||
1117 | * c:\foo.xls#Sheet1!A1', '../../foo.xls'. and '../../foo.xls#Sheet1!A1'. |
||
1118 | * |
||
1119 | * Note: Excel writes some relative links with the $dir_long string. We ignore |
||
1120 | * these cases for the sake of simpler code. |
||
1121 | * |
||
1122 | * @see writeUrl() |
||
1123 | * |
||
1124 | * @param int $row1 Start row |
||
1125 | * @param int $col1 Start column |
||
1126 | * @param int $row2 End row |
||
1127 | * @param int $col2 End column |
||
1128 | * @param string $url URL string |
||
1129 | * |
||
1130 | * @return int |
||
1131 | */ |
||
1132 | public function writeUrlExternal($row1, $col1, $row2, $col2, $url) |
||
1216 | |||
1217 | /** |
||
1218 | * This method is used to set the height and format for a row. |
||
1219 | * |
||
1220 | * @param int $row The row to set |
||
1221 | * @param int $height Height we are giving to the row. |
||
1222 | * Use null to set XF without setting height |
||
1223 | * @param int $xfIndex The optional cell style Xf index to apply to the columns |
||
1224 | * @param bool $hidden The optional hidden attribute |
||
1225 | * @param int $level The optional outline level for row, in range [0,7] |
||
1226 | */ |
||
1227 | 38 | private function writeRow($row, $height, $xfIndex, $hidden = false, $level = 0) |
|
1272 | |||
1273 | /** |
||
1274 | * Writes Excel DIMENSIONS to define the area in which there is data. |
||
1275 | */ |
||
1276 | 39 | private function writeDimensions() |
|
1286 | |||
1287 | /** |
||
1288 | * Write BIFF record Window2. |
||
1289 | */ |
||
1290 | 39 | private function writeWindow2() |
|
1339 | |||
1340 | /** |
||
1341 | * Write BIFF record DEFAULTROWHEIGHT. |
||
1342 | */ |
||
1343 | 39 | private function writeDefaultRowHeight() |
|
1361 | |||
1362 | /** |
||
1363 | * Write BIFF record DEFCOLWIDTH if COLINFO records are in use. |
||
1364 | */ |
||
1365 | 39 | private function writeDefcol() |
|
1376 | |||
1377 | /** |
||
1378 | * Write BIFF record COLINFO to define column widths. |
||
1379 | * |
||
1380 | * Note: The SDK says the record length is 0x0B but Excel writes a 0x0C |
||
1381 | * length record. |
||
1382 | * |
||
1383 | * @param array $col_array This is the only parameter received and is composed of the following: |
||
1384 | * 0 => First formatted column, |
||
1385 | * 1 => Last formatted column, |
||
1386 | * 2 => Col width (8.43 is Excel default), |
||
1387 | * 3 => The optional XF format of the column, |
||
1388 | * 4 => Option flags. |
||
1389 | * 5 => Optional outline level |
||
1390 | */ |
||
1391 | 39 | private function writeColinfo($col_array) |
|
1434 | |||
1435 | /** |
||
1436 | * Write BIFF record SELECTION. |
||
1437 | */ |
||
1438 | 39 | private function writeSelection() |
|
1439 | { |
||
1440 | // look up the selected cell range |
||
1441 | 39 | $selectedCells = Cell::splitRange($this->phpSheet->getSelectedCells()); |
|
1442 | 39 | $selectedCells = $selectedCells[0]; |
|
1443 | 39 | if (count($selectedCells) == 2) { |
|
1444 | 13 | list($first, $last) = $selectedCells; |
|
1445 | } else { |
||
1446 | 32 | $first = $selectedCells[0]; |
|
1447 | 32 | $last = $selectedCells[0]; |
|
1448 | } |
||
1449 | |||
1450 | 39 | list($colFirst, $rwFirst) = Cell::coordinateFromString($first); |
|
1451 | 39 | $colFirst = Cell::columnIndexFromString($colFirst) - 1; // base 0 column index |
|
1452 | 39 | --$rwFirst; // base 0 row index |
|
1453 | |||
1454 | 39 | list($colLast, $rwLast) = Cell::coordinateFromString($last); |
|
1455 | 39 | $colLast = Cell::columnIndexFromString($colLast) - 1; // base 0 column index |
|
1456 | 39 | --$rwLast; // base 0 row index |
|
1457 | |||
1458 | // make sure we are not out of bounds |
||
1459 | 39 | $colFirst = min($colFirst, 255); |
|
1460 | 39 | $colLast = min($colLast, 255); |
|
1461 | |||
1462 | 39 | $rwFirst = min($rwFirst, 65535); |
|
1463 | 39 | $rwLast = min($rwLast, 65535); |
|
1464 | |||
1465 | 39 | $record = 0x001D; // Record identifier |
|
1466 | 39 | $length = 0x000F; // Number of bytes to follow |
|
1467 | |||
1468 | 39 | $pnn = $this->activePane; // Pane position |
|
1469 | 39 | $rwAct = $rwFirst; // Active row |
|
1470 | 39 | $colAct = $colFirst; // Active column |
|
1471 | 39 | $irefAct = 0; // Active cell ref |
|
1472 | 39 | $cref = 1; // Number of refs |
|
1473 | |||
1474 | 39 | if (!isset($rwLast)) { |
|
1475 | $rwLast = $rwFirst; // Last row in reference |
||
1476 | } |
||
1477 | 39 | if (!isset($colLast)) { |
|
1478 | $colLast = $colFirst; // Last col in reference |
||
1479 | } |
||
1480 | |||
1481 | // Swap last row/col for first row/col as necessary |
||
1482 | 39 | if ($rwFirst > $rwLast) { |
|
1483 | list($rwFirst, $rwLast) = [$rwLast, $rwFirst]; |
||
1484 | } |
||
1485 | |||
1486 | 39 | if ($colFirst > $colLast) { |
|
1487 | list($colFirst, $colLast) = [$colLast, $colFirst]; |
||
1488 | } |
||
1489 | |||
1490 | 39 | $header = pack('vv', $record, $length); |
|
1491 | 39 | $data = pack('CvvvvvvCC', $pnn, $rwAct, $colAct, $irefAct, $cref, $rwFirst, $rwLast, $colFirst, $colLast); |
|
1492 | 39 | $this->append($header . $data); |
|
1493 | 39 | } |
|
1494 | |||
1495 | /** |
||
1496 | * Store the MERGEDCELLS records for all ranges of merged cells. |
||
1497 | */ |
||
1498 | 39 | private function writeMergedCells() |
|
1499 | { |
||
1500 | 39 | $mergeCells = $this->phpSheet->getMergeCells(); |
|
1501 | 39 | $countMergeCells = count($mergeCells); |
|
1502 | |||
1503 | 39 | if ($countMergeCells == 0) { |
|
1504 | 38 | return; |
|
1505 | } |
||
1506 | |||
1507 | // maximum allowed number of merged cells per record |
||
1508 | 9 | $maxCountMergeCellsPerRecord = 1027; |
|
1509 | |||
1510 | // record identifier |
||
1511 | 9 | $record = 0x00E5; |
|
1512 | |||
1513 | // counter for total number of merged cells treated so far by the writer |
||
1514 | 9 | $i = 0; |
|
1515 | |||
1516 | // counter for number of merged cells written in record currently being written |
||
1517 | 9 | $j = 0; |
|
1518 | |||
1519 | // initialize record data |
||
1520 | 9 | $recordData = ''; |
|
1521 | |||
1522 | // loop through the merged cells |
||
1523 | 9 | foreach ($mergeCells as $mergeCell) { |
|
1524 | 9 | ++$i; |
|
1525 | 9 | ++$j; |
|
1526 | |||
1527 | // extract the row and column indexes |
||
1528 | 9 | $range = Cell::splitRange($mergeCell); |
|
1529 | 9 | list($first, $last) = $range[0]; |
|
1530 | 9 | list($firstColumn, $firstRow) = Cell::coordinateFromString($first); |
|
1531 | 9 | list($lastColumn, $lastRow) = Cell::coordinateFromString($last); |
|
1532 | |||
1533 | 9 | $recordData .= pack('vvvv', $firstRow - 1, $lastRow - 1, Cell::columnIndexFromString($firstColumn) - 1, Cell::columnIndexFromString($lastColumn) - 1); |
|
1534 | |||
1535 | // flush record if we have reached limit for number of merged cells, or reached final merged cell |
||
1536 | 9 | if ($j == $maxCountMergeCellsPerRecord or $i == $countMergeCells) { |
|
1537 | 9 | $recordData = pack('v', $j) . $recordData; |
|
1538 | 9 | $length = strlen($recordData); |
|
1539 | 9 | $header = pack('vv', $record, $length); |
|
1540 | 9 | $this->append($header . $recordData); |
|
1541 | |||
1542 | // initialize for next record, if any |
||
1543 | 9 | $recordData = ''; |
|
1544 | 9 | $j = 0; |
|
1545 | } |
||
1546 | } |
||
1547 | 9 | } |
|
1548 | |||
1549 | /** |
||
1550 | * Write SHEETLAYOUT record. |
||
1551 | */ |
||
1552 | 39 | private function writeSheetLayout() |
|
1553 | { |
||
1554 | 39 | if (!$this->phpSheet->isTabColorSet()) { |
|
1555 | 39 | return; |
|
1556 | } |
||
1557 | |||
1558 | 5 | $recordData = pack( |
|
1559 | 5 | 'vvVVVvv', |
|
1560 | 5 | 0x0862, |
|
1561 | 5 | 0x0000, // unused |
|
1562 | 5 | 0x00000000, // unused |
|
1563 | 5 | 0x00000000, // unused |
|
1564 | 5 | 0x00000014, // size of record data |
|
1565 | 5 | $this->colors[$this->phpSheet->getTabColor()->getRGB()], // color index |
|
1566 | 5 | 0x0000 // unused |
|
1567 | ); |
||
1568 | |||
1569 | 5 | $length = strlen($recordData); |
|
1570 | |||
1571 | 5 | $record = 0x0862; // Record identifier |
|
1572 | 5 | $header = pack('vv', $record, $length); |
|
1573 | 5 | $this->append($header . $recordData); |
|
1574 | 5 | } |
|
1575 | |||
1576 | /** |
||
1577 | * Write SHEETPROTECTION. |
||
1578 | */ |
||
1579 | 39 | private function writeSheetProtection() |
|
1580 | { |
||
1581 | // record identifier |
||
1582 | 39 | $record = 0x0867; |
|
1583 | |||
1584 | // prepare options |
||
1585 | 39 | $options = (int) !$this->phpSheet->getProtection()->getObjects() |
|
1586 | 39 | | (int) !$this->phpSheet->getProtection()->getScenarios() << 1 |
|
1587 | 39 | | (int) !$this->phpSheet->getProtection()->getFormatCells() << 2 |
|
1588 | 39 | | (int) !$this->phpSheet->getProtection()->getFormatColumns() << 3 |
|
1589 | 39 | | (int) !$this->phpSheet->getProtection()->getFormatRows() << 4 |
|
1590 | 39 | | (int) !$this->phpSheet->getProtection()->getInsertColumns() << 5 |
|
1591 | 39 | | (int) !$this->phpSheet->getProtection()->getInsertRows() << 6 |
|
1592 | 39 | | (int) !$this->phpSheet->getProtection()->getInsertHyperlinks() << 7 |
|
1593 | 39 | | (int) !$this->phpSheet->getProtection()->getDeleteColumns() << 8 |
|
1594 | 39 | | (int) !$this->phpSheet->getProtection()->getDeleteRows() << 9 |
|
1595 | 39 | | (int) !$this->phpSheet->getProtection()->getSelectLockedCells() << 10 |
|
1596 | 39 | | (int) !$this->phpSheet->getProtection()->getSort() << 11 |
|
1597 | 39 | | (int) !$this->phpSheet->getProtection()->getAutoFilter() << 12 |
|
1598 | 39 | | (int) !$this->phpSheet->getProtection()->getPivotTables() << 13 |
|
1599 | 39 | | (int) !$this->phpSheet->getProtection()->getSelectUnlockedCells() << 14; |
|
1600 | |||
1601 | // record data |
||
1602 | 39 | $recordData = pack( |
|
1603 | 39 | 'vVVCVVvv', |
|
1604 | 39 | 0x0867, // repeated record identifier |
|
1605 | 39 | 0x0000, // not used |
|
1606 | 39 | 0x0000, // not used |
|
1607 | 39 | 0x00, // not used |
|
1608 | 39 | 0x01000200, // unknown data |
|
1609 | 39 | 0xFFFFFFFF, // unknown data |
|
1610 | $options, // options |
||
1611 | 39 | 0x0000 // not used |
|
1612 | ); |
||
1613 | |||
1614 | 39 | $length = strlen($recordData); |
|
1615 | 39 | $header = pack('vv', $record, $length); |
|
1616 | |||
1617 | 39 | $this->append($header . $recordData); |
|
1618 | 39 | } |
|
1619 | |||
1620 | /** |
||
1621 | * Write BIFF record RANGEPROTECTION. |
||
1622 | * |
||
1623 | * Openoffice.org's Documentaion of the Microsoft Excel File Format uses term RANGEPROTECTION for these records |
||
1624 | * Microsoft Office Excel 97-2007 Binary File Format Specification uses term FEAT for these records |
||
1625 | */ |
||
1626 | 39 | private function writeRangeProtection() |
|
1627 | { |
||
1628 | 39 | foreach ($this->phpSheet->getProtectedCells() as $range => $password) { |
|
1629 | // number of ranges, e.g. 'A1:B3 C20:D25' |
||
1630 | 5 | $cellRanges = explode(' ', $range); |
|
1631 | 5 | $cref = count($cellRanges); |
|
1632 | |||
1633 | 5 | $recordData = pack( |
|
1634 | 5 | 'vvVVvCVvVv', |
|
1635 | 5 | 0x0868, |
|
1636 | 5 | 0x00, |
|
1637 | 5 | 0x0000, |
|
1638 | 5 | 0x0000, |
|
1639 | 5 | 0x02, |
|
1640 | 5 | 0x0, |
|
1641 | 5 | 0x0000, |
|
1642 | $cref, |
||
1643 | 5 | 0x0000, |
|
1644 | 5 | 0x00 |
|
1645 | ); |
||
1646 | |||
1647 | 5 | foreach ($cellRanges as $cellRange) { |
|
1648 | 5 | $recordData .= $this->writeBIFF8CellRangeAddressFixed($cellRange); |
|
1649 | } |
||
1650 | |||
1651 | // the rgbFeat structure |
||
1652 | 5 | $recordData .= pack( |
|
1653 | 5 | 'VV', |
|
1654 | 5 | 0x0000, |
|
1655 | hexdec($password) |
||
1656 | ); |
||
1657 | |||
1658 | 5 | $recordData .= StringHelper::UTF8toBIFF8UnicodeLong('p' . md5($recordData)); |
|
1659 | |||
1660 | 5 | $length = strlen($recordData); |
|
1661 | |||
1662 | 5 | $record = 0x0868; // Record identifier |
|
1663 | 5 | $header = pack('vv', $record, $length); |
|
1664 | 5 | $this->append($header . $recordData); |
|
1665 | } |
||
1666 | 39 | } |
|
1667 | |||
1668 | /** |
||
1669 | * Write BIFF record EXTERNCOUNT to indicate the number of external sheet |
||
1670 | * references in a worksheet. |
||
1671 | * |
||
1672 | * Excel only stores references to external sheets that are used in formulas. |
||
1673 | * For simplicity we store references to all the sheets in the workbook |
||
1674 | * regardless of whether they are used or not. This reduces the overall |
||
1675 | * complexity and eliminates the need for a two way dialogue between the formula |
||
1676 | * parser the worksheet objects. |
||
1677 | * |
||
1678 | * @param int $count The number of external sheet references in this worksheet |
||
1679 | */ |
||
1680 | View Code Duplication | private function writeExterncount($count) |
|
1681 | { |
||
1682 | $record = 0x0016; // Record identifier |
||
1683 | $length = 0x0002; // Number of bytes to follow |
||
1684 | |||
1685 | $header = pack('vv', $record, $length); |
||
1686 | $data = pack('v', $count); |
||
1687 | $this->append($header . $data); |
||
1688 | } |
||
1689 | |||
1690 | /** |
||
1691 | * Writes the Excel BIFF EXTERNSHEET record. These references are used by |
||
1692 | * formulas. A formula references a sheet name via an index. Since we store a |
||
1693 | * reference to all of the external worksheets the EXTERNSHEET index is the same |
||
1694 | * as the worksheet index. |
||
1695 | * |
||
1696 | * @param string $sheetname The name of a external worksheet |
||
1697 | */ |
||
1698 | private function writeExternsheet($sheetname) |
||
1699 | { |
||
1700 | $record = 0x0017; // Record identifier |
||
1701 | |||
1702 | // References to the current sheet are encoded differently to references to |
||
1703 | // external sheets. |
||
1704 | // |
||
1705 | if ($this->phpSheet->getTitle() == $sheetname) { |
||
1706 | $sheetname = ''; |
||
1707 | $length = 0x02; // The following 2 bytes |
||
1708 | $cch = 1; // The following byte |
||
1709 | $rgch = 0x02; // Self reference |
||
1710 | } else { |
||
1711 | $length = 0x02 + strlen($sheetname); |
||
1712 | $cch = strlen($sheetname); |
||
1713 | $rgch = 0x03; // Reference to a sheet in the current workbook |
||
1714 | } |
||
1715 | |||
1716 | $header = pack('vv', $record, $length); |
||
1717 | $data = pack('CC', $cch, $rgch); |
||
1718 | $this->append($header . $data . $sheetname); |
||
1719 | } |
||
1720 | |||
1721 | /** |
||
1722 | * Writes the Excel BIFF PANE record. |
||
1723 | * The panes can either be frozen or thawed (unfrozen). |
||
1724 | * Frozen panes are specified in terms of an integer number of rows and columns. |
||
1725 | * Thawed panes are specified in terms of Excel's units for rows and columns. |
||
1726 | */ |
||
1727 | 3 | private function writePanes() |
|
1728 | { |
||
1729 | 3 | $panes = []; |
|
1730 | 3 | if ($freezePane = $this->phpSheet->getFreezePane()) { |
|
1731 | 3 | list($column, $row) = Cell::coordinateFromString($freezePane); |
|
1732 | 3 | $panes[0] = $row - 1; |
|
1733 | 3 | $panes[1] = Cell::columnIndexFromString($column) - 1; |
|
1734 | } else { |
||
1735 | // thaw panes |
||
1736 | return; |
||
1737 | } |
||
1738 | |||
1739 | 3 | $y = isset($panes[0]) ? $panes[0] : null; |
|
1740 | 3 | $x = isset($panes[1]) ? $panes[1] : null; |
|
1741 | 3 | $rwTop = isset($panes[2]) ? $panes[2] : null; |
|
1742 | 3 | $colLeft = isset($panes[3]) ? $panes[3] : null; |
|
1743 | 3 | if (count($panes) > 4) { // if Active pane was received |
|
1744 | $pnnAct = $panes[4]; |
||
1745 | } else { |
||
1746 | 3 | $pnnAct = null; |
|
1747 | } |
||
1748 | 3 | $record = 0x0041; // Record identifier |
|
1749 | 3 | $length = 0x000A; // Number of bytes to follow |
|
1750 | |||
1751 | // Code specific to frozen or thawed panes. |
||
1752 | 3 | if ($this->phpSheet->getFreezePane()) { |
|
1753 | // Set default values for $rwTop and $colLeft |
||
1754 | 3 | if (!isset($rwTop)) { |
|
1755 | 3 | $rwTop = $y; |
|
1756 | } |
||
1757 | 3 | if (!isset($colLeft)) { |
|
1758 | 3 | $colLeft = $x; |
|
1759 | } |
||
1760 | } else { |
||
1761 | // Set default values for $rwTop and $colLeft |
||
1762 | if (!isset($rwTop)) { |
||
1763 | $rwTop = 0; |
||
1764 | } |
||
1765 | if (!isset($colLeft)) { |
||
1766 | $colLeft = 0; |
||
1767 | } |
||
1768 | |||
1769 | // Convert Excel's row and column units to the internal units. |
||
1770 | // The default row height is 12.75 |
||
1771 | // The default column width is 8.43 |
||
1772 | // The following slope and intersection values were interpolated. |
||
1773 | // |
||
1774 | $y = 20 * $y + 255; |
||
1775 | $x = 113.879 * $x + 390; |
||
1776 | } |
||
1777 | |||
1778 | // Determine which pane should be active. There is also the undocumented |
||
1779 | // option to override this should it be necessary: may be removed later. |
||
1780 | // |
||
1781 | 3 | if (!isset($pnnAct)) { |
|
1782 | 3 | if ($x != 0 && $y != 0) { |
|
1783 | $pnnAct = 0; // Bottom right |
||
1784 | } |
||
1785 | 3 | if ($x != 0 && $y == 0) { |
|
1786 | $pnnAct = 1; // Top right |
||
1787 | } |
||
1788 | 3 | if ($x == 0 && $y != 0) { |
|
1789 | 3 | $pnnAct = 2; // Bottom left |
|
1790 | } |
||
1791 | 3 | if ($x == 0 && $y == 0) { |
|
1792 | $pnnAct = 3; // Top left |
||
1793 | } |
||
1794 | } |
||
1795 | |||
1796 | 3 | $this->activePane = $pnnAct; // Used in writeSelection |
|
1797 | |||
1798 | 3 | $header = pack('vv', $record, $length); |
|
1799 | 3 | $data = pack('vvvvv', $x, $y, $rwTop, $colLeft, $pnnAct); |
|
1800 | 3 | $this->append($header . $data); |
|
1801 | 3 | } |
|
1802 | |||
1803 | /** |
||
1804 | * Store the page setup SETUP BIFF record. |
||
1805 | */ |
||
1806 | 39 | private function writeSetup() |
|
1807 | { |
||
1808 | 39 | $record = 0x00A1; // Record identifier |
|
1809 | 39 | $length = 0x0022; // Number of bytes to follow |
|
1810 | |||
1811 | 39 | $iPaperSize = $this->phpSheet->getPageSetup()->getPaperSize(); // Paper size |
|
1812 | |||
1813 | 39 | $iScale = $this->phpSheet->getPageSetup()->getScale() ? |
|
1814 | 39 | $this->phpSheet->getPageSetup()->getScale() : 100; // Print scaling factor |
|
1815 | |||
1816 | 39 | $iPageStart = 0x01; // Starting page number |
|
1817 | 39 | $iFitWidth = (int) $this->phpSheet->getPageSetup()->getFitToWidth(); // Fit to number of pages wide |
|
1818 | 39 | $iFitHeight = (int) $this->phpSheet->getPageSetup()->getFitToHeight(); // Fit to number of pages high |
|
1819 | 39 | $grbit = 0x00; // Option flags |
|
1820 | 39 | $iRes = 0x0258; // Print resolution |
|
1821 | 39 | $iVRes = 0x0258; // Vertical print resolution |
|
1822 | |||
1823 | 39 | $numHdr = $this->phpSheet->getPageMargins()->getHeader(); // Header Margin |
|
1824 | |||
1825 | 39 | $numFtr = $this->phpSheet->getPageMargins()->getFooter(); // Footer Margin |
|
1826 | 39 | $iCopies = 0x01; // Number of copies |
|
1827 | |||
1828 | 39 | $fLeftToRight = 0x0; // Print over then down |
|
1829 | |||
1830 | // Page orientation |
||
1831 | 39 | $fLandscape = ($this->phpSheet->getPageSetup()->getOrientation() == PageSetup::ORIENTATION_LANDSCAPE) ? |
|
1832 | 39 | 0x0 : 0x1; |
|
1833 | |||
1834 | 39 | $fNoPls = 0x0; // Setup not read from printer |
|
1835 | 39 | $fNoColor = 0x0; // Print black and white |
|
1836 | 39 | $fDraft = 0x0; // Print draft quality |
|
1837 | 39 | $fNotes = 0x0; // Print notes |
|
1838 | 39 | $fNoOrient = 0x0; // Orientation not set |
|
1839 | 39 | $fUsePage = 0x0; // Use custom starting page |
|
1840 | |||
1841 | 39 | $grbit = $fLeftToRight; |
|
1842 | 39 | $grbit |= $fLandscape << 1; |
|
1843 | 39 | $grbit |= $fNoPls << 2; |
|
1844 | 39 | $grbit |= $fNoColor << 3; |
|
1845 | 39 | $grbit |= $fDraft << 4; |
|
1846 | 39 | $grbit |= $fNotes << 5; |
|
1847 | 39 | $grbit |= $fNoOrient << 6; |
|
1848 | 39 | $grbit |= $fUsePage << 7; |
|
1849 | |||
1850 | 39 | $numHdr = pack('d', $numHdr); |
|
1851 | 39 | $numFtr = pack('d', $numFtr); |
|
1852 | 39 | if (self::getByteOrder()) { // if it's Big Endian |
|
1853 | $numHdr = strrev($numHdr); |
||
1854 | $numFtr = strrev($numFtr); |
||
1855 | } |
||
1856 | |||
1857 | 39 | $header = pack('vv', $record, $length); |
|
1858 | 39 | $data1 = pack('vvvvvvvv', $iPaperSize, $iScale, $iPageStart, $iFitWidth, $iFitHeight, $grbit, $iRes, $iVRes); |
|
1859 | 39 | $data2 = $numHdr . $numFtr; |
|
1860 | 39 | $data3 = pack('v', $iCopies); |
|
1861 | 39 | $this->append($header . $data1 . $data2 . $data3); |
|
1862 | 39 | } |
|
1863 | |||
1864 | /** |
||
1865 | * Store the header caption BIFF record. |
||
1866 | */ |
||
1867 | 39 | View Code Duplication | private function writeHeader() |
1868 | { |
||
1869 | 39 | $record = 0x0014; // Record identifier |
|
1870 | |||
1871 | /* removing for now |
||
1872 | // need to fix character count (multibyte!) |
||
1873 | if (strlen($this->phpSheet->getHeaderFooter()->getOddHeader()) <= 255) { |
||
1874 | $str = $this->phpSheet->getHeaderFooter()->getOddHeader(); // header string |
||
1875 | } else { |
||
1876 | $str = ''; |
||
1877 | } |
||
1878 | */ |
||
1879 | |||
1880 | 39 | $recordData = StringHelper::UTF8toBIFF8UnicodeLong($this->phpSheet->getHeaderFooter()->getOddHeader()); |
|
1881 | 39 | $length = strlen($recordData); |
|
1882 | |||
1883 | 39 | $header = pack('vv', $record, $length); |
|
1884 | |||
1885 | 39 | $this->append($header . $recordData); |
|
1886 | 39 | } |
|
1887 | |||
1888 | /** |
||
1889 | * Store the footer caption BIFF record. |
||
1890 | */ |
||
1891 | 39 | View Code Duplication | private function writeFooter() |
1892 | { |
||
1893 | 39 | $record = 0x0015; // Record identifier |
|
1894 | |||
1895 | /* removing for now |
||
1896 | // need to fix character count (multibyte!) |
||
1897 | if (strlen($this->phpSheet->getHeaderFooter()->getOddFooter()) <= 255) { |
||
1898 | $str = $this->phpSheet->getHeaderFooter()->getOddFooter(); |
||
1899 | } else { |
||
1900 | $str = ''; |
||
1901 | } |
||
1902 | */ |
||
1903 | |||
1904 | 39 | $recordData = StringHelper::UTF8toBIFF8UnicodeLong($this->phpSheet->getHeaderFooter()->getOddFooter()); |
|
1905 | 39 | $length = strlen($recordData); |
|
1906 | |||
1907 | 39 | $header = pack('vv', $record, $length); |
|
1908 | |||
1909 | 39 | $this->append($header . $recordData); |
|
1910 | 39 | } |
|
1911 | |||
1912 | /** |
||
1913 | * Store the horizontal centering HCENTER BIFF record. |
||
1914 | */ |
||
1915 | 39 | View Code Duplication | private function writeHcenter() |
1916 | { |
||
1917 | 39 | $record = 0x0083; // Record identifier |
|
1918 | 39 | $length = 0x0002; // Bytes to follow |
|
1919 | |||
1920 | 39 | $fHCenter = $this->phpSheet->getPageSetup()->getHorizontalCentered() ? 1 : 0; // Horizontal centering |
|
1921 | |||
1922 | 39 | $header = pack('vv', $record, $length); |
|
1923 | 39 | $data = pack('v', $fHCenter); |
|
1924 | |||
1925 | 39 | $this->append($header . $data); |
|
1926 | 39 | } |
|
1927 | |||
1928 | /** |
||
1929 | * Store the vertical centering VCENTER BIFF record. |
||
1930 | */ |
||
1931 | 39 | View Code Duplication | private function writeVcenter() |
1932 | { |
||
1933 | 39 | $record = 0x0084; // Record identifier |
|
1934 | 39 | $length = 0x0002; // Bytes to follow |
|
1935 | |||
1936 | 39 | $fVCenter = $this->phpSheet->getPageSetup()->getVerticalCentered() ? 1 : 0; // Horizontal centering |
|
1937 | |||
1938 | 39 | $header = pack('vv', $record, $length); |
|
1939 | 39 | $data = pack('v', $fVCenter); |
|
1940 | 39 | $this->append($header . $data); |
|
1941 | 39 | } |
|
1942 | |||
1943 | /** |
||
1944 | * Store the LEFTMARGIN BIFF record. |
||
1945 | */ |
||
1946 | 39 | View Code Duplication | private function writeMarginLeft() |
1947 | { |
||
1948 | 39 | $record = 0x0026; // Record identifier |
|
1949 | 39 | $length = 0x0008; // Bytes to follow |
|
1950 | |||
1951 | 39 | $margin = $this->phpSheet->getPageMargins()->getLeft(); // Margin in inches |
|
1952 | |||
1953 | 39 | $header = pack('vv', $record, $length); |
|
1954 | 39 | $data = pack('d', $margin); |
|
1955 | 39 | if (self::getByteOrder()) { // if it's Big Endian |
|
1956 | $data = strrev($data); |
||
1957 | } |
||
1958 | |||
1959 | 39 | $this->append($header . $data); |
|
1960 | 39 | } |
|
1961 | |||
1962 | /** |
||
1963 | * Store the RIGHTMARGIN BIFF record. |
||
1964 | */ |
||
1965 | 39 | View Code Duplication | private function writeMarginRight() |
1966 | { |
||
1967 | 39 | $record = 0x0027; // Record identifier |
|
1968 | 39 | $length = 0x0008; // Bytes to follow |
|
1969 | |||
1970 | 39 | $margin = $this->phpSheet->getPageMargins()->getRight(); // Margin in inches |
|
1971 | |||
1972 | 39 | $header = pack('vv', $record, $length); |
|
1973 | 39 | $data = pack('d', $margin); |
|
1974 | 39 | if (self::getByteOrder()) { // if it's Big Endian |
|
1975 | $data = strrev($data); |
||
1976 | } |
||
1977 | |||
1978 | 39 | $this->append($header . $data); |
|
1979 | 39 | } |
|
1980 | |||
1981 | /** |
||
1982 | * Store the TOPMARGIN BIFF record. |
||
1983 | */ |
||
1984 | 39 | View Code Duplication | private function writeMarginTop() |
1985 | { |
||
1986 | 39 | $record = 0x0028; // Record identifier |
|
1987 | 39 | $length = 0x0008; // Bytes to follow |
|
1988 | |||
1989 | 39 | $margin = $this->phpSheet->getPageMargins()->getTop(); // Margin in inches |
|
1990 | |||
1991 | 39 | $header = pack('vv', $record, $length); |
|
1992 | 39 | $data = pack('d', $margin); |
|
1993 | 39 | if (self::getByteOrder()) { // if it's Big Endian |
|
1994 | $data = strrev($data); |
||
1995 | } |
||
1996 | |||
1997 | 39 | $this->append($header . $data); |
|
1998 | 39 | } |
|
1999 | |||
2000 | /** |
||
2001 | * Store the BOTTOMMARGIN BIFF record. |
||
2002 | */ |
||
2003 | 39 | View Code Duplication | private function writeMarginBottom() |
2004 | { |
||
2005 | 39 | $record = 0x0029; // Record identifier |
|
2006 | 39 | $length = 0x0008; // Bytes to follow |
|
2007 | |||
2008 | 39 | $margin = $this->phpSheet->getPageMargins()->getBottom(); // Margin in inches |
|
2009 | |||
2010 | 39 | $header = pack('vv', $record, $length); |
|
2011 | 39 | $data = pack('d', $margin); |
|
2012 | 39 | if (self::getByteOrder()) { // if it's Big Endian |
|
2013 | $data = strrev($data); |
||
2014 | } |
||
2015 | |||
2016 | 39 | $this->append($header . $data); |
|
2017 | 39 | } |
|
2018 | |||
2019 | /** |
||
2020 | * Write the PRINTHEADERS BIFF record. |
||
2021 | */ |
||
2022 | 39 | View Code Duplication | private function writePrintHeaders() |
2023 | { |
||
2024 | 39 | $record = 0x002a; // Record identifier |
|
2025 | 39 | $length = 0x0002; // Bytes to follow |
|
2026 | |||
2027 | 39 | $fPrintRwCol = $this->printHeaders; // Boolean flag |
|
2028 | |||
2029 | 39 | $header = pack('vv', $record, $length); |
|
2030 | 39 | $data = pack('v', $fPrintRwCol); |
|
2031 | 39 | $this->append($header . $data); |
|
2032 | 39 | } |
|
2033 | |||
2034 | /** |
||
2035 | * Write the PRINTGRIDLINES BIFF record. Must be used in conjunction with the |
||
2036 | * GRIDSET record. |
||
2037 | */ |
||
2038 | 39 | View Code Duplication | private function writePrintGridlines() |
2039 | { |
||
2040 | 39 | $record = 0x002b; // Record identifier |
|
2041 | 39 | $length = 0x0002; // Bytes to follow |
|
2042 | |||
2043 | 39 | $fPrintGrid = $this->phpSheet->getPrintGridlines() ? 1 : 0; // Boolean flag |
|
2044 | |||
2045 | 39 | $header = pack('vv', $record, $length); |
|
2046 | 39 | $data = pack('v', $fPrintGrid); |
|
2047 | 39 | $this->append($header . $data); |
|
2048 | 39 | } |
|
2049 | |||
2050 | /** |
||
2051 | * Write the GRIDSET BIFF record. Must be used in conjunction with the |
||
2052 | * PRINTGRIDLINES record. |
||
2053 | */ |
||
2054 | 39 | private function writeGridset() |
|
2055 | { |
||
2056 | 39 | $record = 0x0082; // Record identifier |
|
2057 | 39 | $length = 0x0002; // Bytes to follow |
|
2058 | |||
2059 | 39 | $fGridSet = !$this->phpSheet->getPrintGridlines(); // Boolean flag |
|
2060 | |||
2061 | 39 | $header = pack('vv', $record, $length); |
|
2062 | 39 | $data = pack('v', $fGridSet); |
|
2063 | 39 | $this->append($header . $data); |
|
2064 | 39 | } |
|
2065 | |||
2066 | /** |
||
2067 | * Write the AUTOFILTERINFO BIFF record. This is used to configure the number of autofilter select used in the sheet. |
||
2068 | */ |
||
2069 | 3 | private function writeAutoFilterInfo() |
|
2070 | { |
||
2071 | 3 | $record = 0x009D; // Record identifier |
|
2072 | 3 | $length = 0x0002; // Bytes to follow |
|
2073 | |||
2074 | 3 | $rangeBounds = Cell::rangeBoundaries($this->phpSheet->getAutoFilter()->getRange()); |
|
2075 | 3 | $iNumFilters = 1 + $rangeBounds[1][0] - $rangeBounds[0][0]; |
|
2076 | |||
2077 | 3 | $header = pack('vv', $record, $length); |
|
2078 | 3 | $data = pack('v', $iNumFilters); |
|
2079 | 3 | $this->append($header . $data); |
|
2080 | 3 | } |
|
2081 | |||
2082 | /** |
||
2083 | * Write the GUTS BIFF record. This is used to configure the gutter margins |
||
2084 | * where Excel outline symbols are displayed. The visibility of the gutters is |
||
2085 | * controlled by a flag in WSBOOL. |
||
2086 | * |
||
2087 | * @see writeWsbool() |
||
2088 | */ |
||
2089 | 39 | private function writeGuts() |
|
2090 | { |
||
2091 | 39 | $record = 0x0080; // Record identifier |
|
2092 | 39 | $length = 0x0008; // Bytes to follow |
|
2093 | |||
2094 | 39 | $dxRwGut = 0x0000; // Size of row gutter |
|
2095 | 39 | $dxColGut = 0x0000; // Size of col gutter |
|
2096 | |||
2097 | // determine maximum row outline level |
||
2098 | 39 | $maxRowOutlineLevel = 0; |
|
2099 | 39 | foreach ($this->phpSheet->getRowDimensions() as $rowDimension) { |
|
2100 | 38 | $maxRowOutlineLevel = max($maxRowOutlineLevel, $rowDimension->getOutlineLevel()); |
|
2101 | } |
||
2102 | |||
2103 | 39 | $col_level = 0; |
|
2104 | |||
2105 | // Calculate the maximum column outline level. The equivalent calculation |
||
2106 | // for the row outline level is carried out in writeRow(). |
||
2107 | 39 | $colcount = count($this->columnInfo); |
|
2108 | 39 | for ($i = 0; $i < $colcount; ++$i) { |
|
2109 | 39 | $col_level = max($this->columnInfo[$i][5], $col_level); |
|
2110 | } |
||
2111 | |||
2112 | // Set the limits for the outline levels (0 <= x <= 7). |
||
2113 | 39 | $col_level = max(0, min($col_level, 7)); |
|
2114 | |||
2115 | // The displayed level is one greater than the max outline levels |
||
2116 | 39 | if ($maxRowOutlineLevel) { |
|
2117 | ++$maxRowOutlineLevel; |
||
2118 | } |
||
2119 | 39 | if ($col_level) { |
|
2120 | 1 | ++$col_level; |
|
2121 | } |
||
2122 | |||
2123 | 39 | $header = pack('vv', $record, $length); |
|
2124 | 39 | $data = pack('vvvv', $dxRwGut, $dxColGut, $maxRowOutlineLevel, $col_level); |
|
2125 | |||
2126 | 39 | $this->append($header . $data); |
|
2127 | 39 | } |
|
2128 | |||
2129 | /** |
||
2130 | * Write the WSBOOL BIFF record, mainly for fit-to-page. Used in conjunction |
||
2131 | * with the SETUP record. |
||
2132 | */ |
||
2133 | 39 | private function writeWsbool() |
|
2134 | { |
||
2135 | 39 | $record = 0x0081; // Record identifier |
|
2136 | 39 | $length = 0x0002; // Bytes to follow |
|
2137 | 39 | $grbit = 0x0000; |
|
2138 | |||
2139 | // The only option that is of interest is the flag for fit to page. So we |
||
2140 | // set all the options in one go. |
||
2141 | // |
||
2142 | // Set the option flags |
||
2143 | 39 | $grbit |= 0x0001; // Auto page breaks visible |
|
2144 | 39 | if ($this->outlineStyle) { |
|
2145 | $grbit |= 0x0020; // Auto outline styles |
||
2146 | } |
||
2147 | 39 | if ($this->phpSheet->getShowSummaryBelow()) { |
|
2148 | 39 | $grbit |= 0x0040; // Outline summary below |
|
2149 | } |
||
2150 | 39 | if ($this->phpSheet->getShowSummaryRight()) { |
|
2151 | 39 | $grbit |= 0x0080; // Outline summary right |
|
2152 | } |
||
2153 | 39 | if ($this->phpSheet->getPageSetup()->getFitToPage()) { |
|
2154 | $grbit |= 0x0100; // Page setup fit to page |
||
2155 | } |
||
2156 | 39 | if ($this->outlineOn) { |
|
2157 | 39 | $grbit |= 0x0400; // Outline symbols displayed |
|
2158 | } |
||
2159 | |||
2160 | 39 | $header = pack('vv', $record, $length); |
|
2161 | 39 | $data = pack('v', $grbit); |
|
2162 | 39 | $this->append($header . $data); |
|
2163 | 39 | } |
|
2164 | |||
2165 | /** |
||
2166 | * Write the HORIZONTALPAGEBREAKS and VERTICALPAGEBREAKS BIFF records. |
||
2167 | */ |
||
2168 | 39 | private function writeBreaks() |
|
2169 | { |
||
2170 | // initialize |
||
2171 | 39 | $vbreaks = []; |
|
2172 | 39 | $hbreaks = []; |
|
2173 | |||
2174 | 39 | foreach ($this->phpSheet->getBreaks() as $cell => $breakType) { |
|
2175 | // Fetch coordinates |
||
2176 | 1 | $coordinates = Cell::coordinateFromString($cell); |
|
2177 | |||
2178 | // Decide what to do by the type of break |
||
2179 | switch ($breakType) { |
||
2180 | 1 | case \PhpOffice\PhpSpreadsheet\Worksheet::BREAK_COLUMN: |
|
2181 | // Add to list of vertical breaks |
||
2182 | $vbreaks[] = Cell::columnIndexFromString($coordinates[0]) - 1; |
||
2183 | break; |
||
2184 | 1 | case \PhpOffice\PhpSpreadsheet\Worksheet::BREAK_ROW: |
|
2185 | // Add to list of horizontal breaks |
||
2186 | 1 | $hbreaks[] = $coordinates[1]; |
|
2187 | 1 | break; |
|
2188 | case \PhpOffice\PhpSpreadsheet\Worksheet::BREAK_NONE: |
||
2189 | default: |
||
2190 | // Nothing to do |
||
2191 | 1 | break; |
|
2192 | } |
||
2193 | } |
||
2194 | |||
2195 | //horizontal page breaks |
||
2196 | 39 | if (!empty($hbreaks)) { |
|
2197 | // Sort and filter array of page breaks |
||
2198 | 1 | sort($hbreaks, SORT_NUMERIC); |
|
2199 | 1 | if ($hbreaks[0] == 0) { // don't use first break if it's 0 |
|
2200 | array_shift($hbreaks); |
||
2201 | } |
||
2202 | |||
2203 | 1 | $record = 0x001b; // Record identifier |
|
2204 | 1 | $cbrk = count($hbreaks); // Number of page breaks |
|
2205 | 1 | $length = 2 + 6 * $cbrk; // Bytes to follow |
|
2206 | |||
2207 | 1 | $header = pack('vv', $record, $length); |
|
2208 | 1 | $data = pack('v', $cbrk); |
|
2209 | |||
2210 | // Append each page break |
||
2211 | 1 | foreach ($hbreaks as $hbreak) { |
|
2212 | 1 | $data .= pack('vvv', $hbreak, 0x0000, 0x00ff); |
|
2213 | } |
||
2214 | |||
2215 | 1 | $this->append($header . $data); |
|
2216 | } |
||
2217 | |||
2218 | // vertical page breaks |
||
2219 | 39 | if (!empty($vbreaks)) { |
|
2220 | // 1000 vertical pagebreaks appears to be an internal Excel 5 limit. |
||
2221 | // It is slightly higher in Excel 97/200, approx. 1026 |
||
2222 | $vbreaks = array_slice($vbreaks, 0, 1000); |
||
2223 | |||
2224 | // Sort and filter array of page breaks |
||
2225 | sort($vbreaks, SORT_NUMERIC); |
||
2226 | if ($vbreaks[0] == 0) { // don't use first break if it's 0 |
||
2227 | array_shift($vbreaks); |
||
2228 | } |
||
2229 | |||
2230 | $record = 0x001a; // Record identifier |
||
2231 | $cbrk = count($vbreaks); // Number of page breaks |
||
2232 | $length = 2 + 6 * $cbrk; // Bytes to follow |
||
2233 | |||
2234 | $header = pack('vv', $record, $length); |
||
2235 | $data = pack('v', $cbrk); |
||
2236 | |||
2237 | // Append each page break |
||
2238 | foreach ($vbreaks as $vbreak) { |
||
2239 | $data .= pack('vvv', $vbreak, 0x0000, 0xffff); |
||
2240 | } |
||
2241 | |||
2242 | $this->append($header . $data); |
||
2243 | } |
||
2244 | 39 | } |
|
2245 | |||
2246 | /** |
||
2247 | * Set the Biff PROTECT record to indicate that the worksheet is protected. |
||
2248 | */ |
||
2249 | 39 | View Code Duplication | private function writeProtect() |
2250 | { |
||
2251 | // Exit unless sheet protection has been specified |
||
2252 | 39 | if (!$this->phpSheet->getProtection()->getSheet()) { |
|
2253 | 37 | return; |
|
2254 | } |
||
2255 | |||
2256 | 7 | $record = 0x0012; // Record identifier |
|
2257 | 7 | $length = 0x0002; // Bytes to follow |
|
2258 | |||
2259 | 7 | $fLock = 1; // Worksheet is protected |
|
2260 | |||
2261 | 7 | $header = pack('vv', $record, $length); |
|
2262 | 7 | $data = pack('v', $fLock); |
|
2263 | |||
2264 | 7 | $this->append($header . $data); |
|
2265 | 7 | } |
|
2266 | |||
2267 | /** |
||
2268 | * Write SCENPROTECT. |
||
2269 | */ |
||
2270 | 39 | View Code Duplication | private function writeScenProtect() |
2271 | { |
||
2272 | // Exit if sheet protection is not active |
||
2273 | 39 | if (!$this->phpSheet->getProtection()->getSheet()) { |
|
2274 | 37 | return; |
|
2275 | } |
||
2276 | |||
2277 | // Exit if scenarios are not protected |
||
2278 | 7 | if (!$this->phpSheet->getProtection()->getScenarios()) { |
|
2279 | 7 | return; |
|
2280 | } |
||
2281 | |||
2282 | $record = 0x00DD; // Record identifier |
||
2283 | $length = 0x0002; // Bytes to follow |
||
2284 | |||
2285 | $header = pack('vv', $record, $length); |
||
2286 | $data = pack('v', 1); |
||
2287 | |||
2288 | $this->append($header . $data); |
||
2289 | } |
||
2290 | |||
2291 | /** |
||
2292 | * Write OBJECTPROTECT. |
||
2293 | */ |
||
2294 | 39 | View Code Duplication | private function writeObjectProtect() |
2295 | { |
||
2296 | // Exit if sheet protection is not active |
||
2297 | 39 | if (!$this->phpSheet->getProtection()->getSheet()) { |
|
2298 | 37 | return; |
|
2299 | } |
||
2300 | |||
2301 | // Exit if objects are not protected |
||
2302 | 7 | if (!$this->phpSheet->getProtection()->getObjects()) { |
|
2303 | 7 | return; |
|
2304 | } |
||
2305 | |||
2306 | $record = 0x0063; // Record identifier |
||
2307 | $length = 0x0002; // Bytes to follow |
||
2308 | |||
2309 | $header = pack('vv', $record, $length); |
||
2310 | $data = pack('v', 1); |
||
2311 | |||
2312 | $this->append($header . $data); |
||
2313 | } |
||
2314 | |||
2315 | /** |
||
2316 | * Write the worksheet PASSWORD record. |
||
2317 | */ |
||
2318 | 39 | private function writePassword() |
|
2319 | { |
||
2320 | // Exit unless sheet protection and password have been specified |
||
2321 | 39 | if (!$this->phpSheet->getProtection()->getSheet() || !$this->phpSheet->getProtection()->getPassword()) { |
|
2322 | 38 | return; |
|
2323 | } |
||
2324 | |||
2325 | 1 | $record = 0x0013; // Record identifier |
|
2326 | 1 | $length = 0x0002; // Bytes to follow |
|
2327 | |||
2328 | 1 | $wPassword = hexdec($this->phpSheet->getProtection()->getPassword()); // Encoded password |
|
2329 | |||
2330 | 1 | $header = pack('vv', $record, $length); |
|
2331 | 1 | $data = pack('v', $wPassword); |
|
2332 | |||
2333 | 1 | $this->append($header . $data); |
|
2334 | 1 | } |
|
2335 | |||
2336 | /** |
||
2337 | * Insert a 24bit bitmap image in a worksheet. |
||
2338 | * |
||
2339 | * @param int $row The row we are going to insert the bitmap into |
||
2340 | * @param int $col The column we are going to insert the bitmap into |
||
2341 | * @param mixed $bitmap The bitmap filename or GD-image resource |
||
2342 | * @param int $x the horizontal position (offset) of the image inside the cell |
||
2343 | * @param int $y the vertical position (offset) of the image inside the cell |
||
2344 | * @param float $scale_x The horizontal scale |
||
2345 | * @param float $scale_y The vertical scale |
||
2346 | */ |
||
2347 | public function insertBitmap($row, $col, $bitmap, $x = 0, $y = 0, $scale_x = 1, $scale_y = 1) |
||
2348 | { |
||
2349 | $bitmap_array = (is_resource($bitmap) ? $this->processBitmapGd($bitmap) : $this->processBitmap($bitmap)); |
||
2350 | list($width, $height, $size, $data) = $bitmap_array; |
||
2351 | |||
2352 | // Scale the frame of the image. |
||
2353 | $width *= $scale_x; |
||
2354 | $height *= $scale_y; |
||
2355 | |||
2356 | // Calculate the vertices of the image and write the OBJ record |
||
2357 | $this->positionImage($col, $row, $x, $y, $width, $height); |
||
2358 | |||
2359 | // Write the IMDATA record to store the bitmap data |
||
2360 | $record = 0x007f; |
||
2361 | $length = 8 + $size; |
||
2362 | $cf = 0x09; |
||
2363 | $env = 0x01; |
||
2364 | $lcb = $size; |
||
2365 | |||
2366 | $header = pack('vvvvV', $record, $length, $cf, $env, $lcb); |
||
2367 | $this->append($header . $data); |
||
2368 | } |
||
2369 | |||
2370 | /** |
||
2371 | * Calculate the vertices that define the position of the image as required by |
||
2372 | * the OBJ record. |
||
2373 | * |
||
2374 | * +------------+------------+ |
||
2375 | * | A | B | |
||
2376 | * +-----+------------+------------+ |
||
2377 | * | |(x1,y1) | | |
||
2378 | * | 1 |(A1)._______|______ | |
||
2379 | * | | | | | |
||
2380 | * | | | | | |
||
2381 | * +-----+----| BITMAP |-----+ |
||
2382 | * | | | | | |
||
2383 | * | 2 | |______________. | |
||
2384 | * | | | (B2)| |
||
2385 | * | | | (x2,y2)| |
||
2386 | * +---- +------------+------------+ |
||
2387 | * |
||
2388 | * Example of a bitmap that covers some of the area from cell A1 to cell B2. |
||
2389 | * |
||
2390 | * Based on the width and height of the bitmap we need to calculate 8 vars: |
||
2391 | * $col_start, $row_start, $col_end, $row_end, $x1, $y1, $x2, $y2. |
||
2392 | * The width and height of the cells are also variable and have to be taken into |
||
2393 | * account. |
||
2394 | * The values of $col_start and $row_start are passed in from the calling |
||
2395 | * function. The values of $col_end and $row_end are calculated by subtracting |
||
2396 | * the width and height of the bitmap from the width and height of the |
||
2397 | * underlying cells. |
||
2398 | * The vertices are expressed as a percentage of the underlying cell width as |
||
2399 | * follows (rhs values are in pixels): |
||
2400 | * |
||
2401 | * x1 = X / W *1024 |
||
2402 | * y1 = Y / H *256 |
||
2403 | * x2 = (X-1) / W *1024 |
||
2404 | * y2 = (Y-1) / H *256 |
||
2405 | * |
||
2406 | * Where: X is distance from the left side of the underlying cell |
||
2407 | * Y is distance from the top of the underlying cell |
||
2408 | * W is the width of the cell |
||
2409 | * H is the height of the cell |
||
2410 | * The SDK incorrectly states that the height should be expressed as a |
||
2411 | * percentage of 1024. |
||
2412 | * |
||
2413 | * @param int $col_start Col containing upper left corner of object |
||
2414 | * @param int $row_start Row containing top left corner of object |
||
2415 | * @param int $x1 Distance to left side of object |
||
2416 | * @param int $y1 Distance to top of object |
||
2417 | * @param int $width Width of image frame |
||
2418 | * @param int $height Height of image frame |
||
2419 | */ |
||
2420 | public function positionImage($col_start, $row_start, $x1, $y1, $width, $height) |
||
2421 | { |
||
2422 | // Initialise end cell to the same as the start cell |
||
2423 | $col_end = $col_start; // Col containing lower right corner of object |
||
2424 | $row_end = $row_start; // Row containing bottom right corner of object |
||
2425 | |||
2426 | // Zero the specified offset if greater than the cell dimensions |
||
2427 | if ($x1 >= Xls::sizeCol($this->phpSheet, Cell::stringFromColumnIndex($col_start))) { |
||
2428 | $x1 = 0; |
||
2429 | } |
||
2430 | if ($y1 >= Xls::sizeRow($this->phpSheet, $row_start + 1)) { |
||
2431 | $y1 = 0; |
||
2432 | } |
||
2433 | |||
2434 | $width = $width + $x1 - 1; |
||
2435 | $height = $height + $y1 - 1; |
||
2436 | |||
2437 | // Subtract the underlying cell widths to find the end cell of the image |
||
2438 | while ($width >= Xls::sizeCol($this->phpSheet, Cell::stringFromColumnIndex($col_end))) { |
||
2439 | $width -= Xls::sizeCol($this->phpSheet, Cell::stringFromColumnIndex($col_end)); |
||
2440 | ++$col_end; |
||
2441 | } |
||
2442 | |||
2443 | // Subtract the underlying cell heights to find the end cell of the image |
||
2444 | while ($height >= Xls::sizeRow($this->phpSheet, $row_end + 1)) { |
||
2445 | $height -= Xls::sizeRow($this->phpSheet, $row_end + 1); |
||
2446 | ++$row_end; |
||
2447 | } |
||
2448 | |||
2449 | // Bitmap isn't allowed to start or finish in a hidden cell, i.e. a cell |
||
2450 | // with zero eight or width. |
||
2451 | // |
||
2452 | if (Xls::sizeCol($this->phpSheet, Cell::stringFromColumnIndex($col_start)) == 0) { |
||
2453 | return; |
||
2454 | } |
||
2455 | if (Xls::sizeCol($this->phpSheet, Cell::stringFromColumnIndex($col_end)) == 0) { |
||
2456 | return; |
||
2457 | } |
||
2458 | if (Xls::sizeRow($this->phpSheet, $row_start + 1) == 0) { |
||
2459 | return; |
||
2460 | } |
||
2461 | if (Xls::sizeRow($this->phpSheet, $row_end + 1) == 0) { |
||
2462 | return; |
||
2463 | } |
||
2464 | |||
2465 | // Convert the pixel values to the percentage value expected by Excel |
||
2466 | $x1 = $x1 / Xls::sizeCol($this->phpSheet, Cell::stringFromColumnIndex($col_start)) * 1024; |
||
2467 | $y1 = $y1 / Xls::sizeRow($this->phpSheet, $row_start + 1) * 256; |
||
2468 | $x2 = $width / Xls::sizeCol($this->phpSheet, Cell::stringFromColumnIndex($col_end)) * 1024; // Distance to right side of object |
||
2469 | $y2 = $height / Xls::sizeRow($this->phpSheet, $row_end + 1) * 256; // Distance to bottom of object |
||
2470 | |||
2471 | $this->writeObjPicture($col_start, $x1, $row_start, $y1, $col_end, $x2, $row_end, $y2); |
||
2472 | } |
||
2473 | |||
2474 | /** |
||
2475 | * Store the OBJ record that precedes an IMDATA record. This could be generalise |
||
2476 | * to support other Excel objects. |
||
2477 | * |
||
2478 | * @param int $colL Column containing upper left corner of object |
||
2479 | * @param int $dxL Distance from left side of cell |
||
2480 | * @param int $rwT Row containing top left corner of object |
||
2481 | * @param int $dyT Distance from top of cell |
||
2482 | * @param int $colR Column containing lower right corner of object |
||
2483 | * @param int $dxR Distance from right of cell |
||
2484 | * @param int $rwB Row containing bottom right corner of object |
||
2485 | * @param int $dyB Distance from bottom of cell |
||
2486 | */ |
||
2487 | private function writeObjPicture($colL, $dxL, $rwT, $dyT, $colR, $dxR, $rwB, $dyB) |
||
2488 | { |
||
2489 | $record = 0x005d; // Record identifier |
||
2490 | $length = 0x003c; // Bytes to follow |
||
2491 | |||
2492 | $cObj = 0x0001; // Count of objects in file (set to 1) |
||
2493 | $OT = 0x0008; // Object type. 8 = Picture |
||
2494 | $id = 0x0001; // Object ID |
||
2495 | $grbit = 0x0614; // Option flags |
||
2496 | |||
2497 | $cbMacro = 0x0000; // Length of FMLA structure |
||
2498 | $Reserved1 = 0x0000; // Reserved |
||
2499 | $Reserved2 = 0x0000; // Reserved |
||
2500 | |||
2501 | $icvBack = 0x09; // Background colour |
||
2502 | $icvFore = 0x09; // Foreground colour |
||
2503 | $fls = 0x00; // Fill pattern |
||
2504 | $fAuto = 0x00; // Automatic fill |
||
2505 | $icv = 0x08; // Line colour |
||
2506 | $lns = 0xff; // Line style |
||
2507 | $lnw = 0x01; // Line weight |
||
2508 | $fAutoB = 0x00; // Automatic border |
||
2509 | $frs = 0x0000; // Frame style |
||
2510 | $cf = 0x0009; // Image format, 9 = bitmap |
||
2511 | $Reserved3 = 0x0000; // Reserved |
||
2512 | $cbPictFmla = 0x0000; // Length of FMLA structure |
||
2513 | $Reserved4 = 0x0000; // Reserved |
||
2514 | $grbit2 = 0x0001; // Option flags |
||
2515 | $Reserved5 = 0x0000; // Reserved |
||
2516 | |||
2517 | $header = pack('vv', $record, $length); |
||
2518 | $data = pack('V', $cObj); |
||
2519 | $data .= pack('v', $OT); |
||
2520 | $data .= pack('v', $id); |
||
2521 | $data .= pack('v', $grbit); |
||
2522 | $data .= pack('v', $colL); |
||
2523 | $data .= pack('v', $dxL); |
||
2524 | $data .= pack('v', $rwT); |
||
2525 | $data .= pack('v', $dyT); |
||
2526 | $data .= pack('v', $colR); |
||
2527 | $data .= pack('v', $dxR); |
||
2528 | $data .= pack('v', $rwB); |
||
2529 | $data .= pack('v', $dyB); |
||
2530 | $data .= pack('v', $cbMacro); |
||
2531 | $data .= pack('V', $Reserved1); |
||
2532 | $data .= pack('v', $Reserved2); |
||
2533 | $data .= pack('C', $icvBack); |
||
2534 | $data .= pack('C', $icvFore); |
||
2535 | $data .= pack('C', $fls); |
||
2536 | $data .= pack('C', $fAuto); |
||
2537 | $data .= pack('C', $icv); |
||
2538 | $data .= pack('C', $lns); |
||
2539 | $data .= pack('C', $lnw); |
||
2540 | $data .= pack('C', $fAutoB); |
||
2541 | $data .= pack('v', $frs); |
||
2542 | $data .= pack('V', $cf); |
||
2543 | $data .= pack('v', $Reserved3); |
||
2544 | $data .= pack('v', $cbPictFmla); |
||
2545 | $data .= pack('v', $Reserved4); |
||
2546 | $data .= pack('v', $grbit2); |
||
2547 | $data .= pack('V', $Reserved5); |
||
2548 | |||
2549 | $this->append($header . $data); |
||
2550 | } |
||
2551 | |||
2552 | /** |
||
2553 | * Convert a GD-image into the internal format. |
||
2554 | * |
||
2555 | * @param resource $image The image to process |
||
2556 | * |
||
2557 | * @return array Array with data and properties of the bitmap |
||
2558 | */ |
||
2559 | public function processBitmapGd($image) |
||
2560 | { |
||
2561 | $width = imagesx($image); |
||
2562 | $height = imagesy($image); |
||
2563 | |||
2564 | $data = pack('Vvvvv', 0x000c, $width, $height, 0x01, 0x18); |
||
2565 | for ($j = $height; --$j;) { |
||
2566 | for ($i = 0; $i < $width; ++$i) { |
||
2567 | $color = imagecolorsforindex($image, imagecolorat($image, $i, $j)); |
||
2568 | foreach (['red', 'green', 'blue'] as $key) { |
||
2569 | $color[$key] = $color[$key] + round((255 - $color[$key]) * $color['alpha'] / 127); |
||
2570 | } |
||
2571 | $data .= chr($color['blue']) . chr($color['green']) . chr($color['red']); |
||
2572 | } |
||
2573 | if (3 * $width % 4) { |
||
2574 | $data .= str_repeat("\x00", 4 - 3 * $width % 4); |
||
2575 | } |
||
2576 | } |
||
2577 | |||
2578 | return [$width, $height, strlen($data), $data]; |
||
2579 | } |
||
2580 | |||
2581 | /** |
||
2582 | * Convert a 24 bit bitmap into the modified internal format used by Windows. |
||
2583 | * This is described in BITMAPCOREHEADER and BITMAPCOREINFO structures in the |
||
2584 | * MSDN library. |
||
2585 | * |
||
2586 | * @param string $bitmap The bitmap to process |
||
2587 | * |
||
2588 | * @return array Array with data and properties of the bitmap |
||
2589 | */ |
||
2590 | public function processBitmap($bitmap) |
||
2591 | { |
||
2592 | // Open file. |
||
2593 | $bmp_fd = @fopen($bitmap, 'rb'); |
||
2594 | if (!$bmp_fd) { |
||
2595 | throw new WriterException("Couldn't import $bitmap"); |
||
2596 | } |
||
2597 | |||
2598 | // Slurp the file into a string. |
||
2599 | $data = fread($bmp_fd, filesize($bitmap)); |
||
2600 | |||
2601 | // Check that the file is big enough to be a bitmap. |
||
2602 | if (strlen($data) <= 0x36) { |
||
2603 | throw new WriterException("$bitmap doesn't contain enough data.\n"); |
||
2604 | } |
||
2605 | |||
2606 | // The first 2 bytes are used to identify the bitmap. |
||
2607 | $identity = unpack('A2ident', $data); |
||
2608 | if ($identity['ident'] != 'BM') { |
||
2609 | throw new WriterException("$bitmap doesn't appear to be a valid bitmap image.\n"); |
||
2610 | } |
||
2611 | |||
2612 | // Remove bitmap data: ID. |
||
2613 | $data = substr($data, 2); |
||
2614 | |||
2615 | // Read and remove the bitmap size. This is more reliable than reading |
||
2616 | // the data size at offset 0x22. |
||
2617 | // |
||
2618 | $size_array = unpack('Vsa', substr($data, 0, 4)); |
||
2619 | $size = $size_array['sa']; |
||
2620 | $data = substr($data, 4); |
||
2621 | $size -= 0x36; // Subtract size of bitmap header. |
||
2622 | $size += 0x0C; // Add size of BIFF header. |
||
2623 | |||
2624 | // Remove bitmap data: reserved, offset, header length. |
||
2625 | $data = substr($data, 12); |
||
2626 | |||
2627 | // Read and remove the bitmap width and height. Verify the sizes. |
||
2628 | $width_and_height = unpack('V2', substr($data, 0, 8)); |
||
2629 | $width = $width_and_height[1]; |
||
2630 | $height = $width_and_height[2]; |
||
2631 | $data = substr($data, 8); |
||
2632 | if ($width > 0xFFFF) { |
||
2633 | throw new WriterException("$bitmap: largest image width supported is 65k.\n"); |
||
2634 | } |
||
2635 | if ($height > 0xFFFF) { |
||
2636 | throw new WriterException("$bitmap: largest image height supported is 65k.\n"); |
||
2637 | } |
||
2638 | |||
2639 | // Read and remove the bitmap planes and bpp data. Verify them. |
||
2640 | $planes_and_bitcount = unpack('v2', substr($data, 0, 4)); |
||
2641 | $data = substr($data, 4); |
||
2642 | if ($planes_and_bitcount[2] != 24) { // Bitcount |
||
2643 | throw new WriterException("$bitmap isn't a 24bit true color bitmap.\n"); |
||
2644 | } |
||
2645 | if ($planes_and_bitcount[1] != 1) { |
||
2646 | throw new WriterException("$bitmap: only 1 plane supported in bitmap image.\n"); |
||
2647 | } |
||
2648 | |||
2649 | // Read and remove the bitmap compression. Verify compression. |
||
2650 | $compression = unpack('Vcomp', substr($data, 0, 4)); |
||
2651 | $data = substr($data, 4); |
||
2652 | |||
2653 | if ($compression['comp'] != 0) { |
||
2654 | throw new WriterException("$bitmap: compression not supported in bitmap image.\n"); |
||
2655 | } |
||
2656 | |||
2657 | // Remove bitmap data: data size, hres, vres, colours, imp. colours. |
||
2658 | $data = substr($data, 20); |
||
2659 | |||
2660 | // Add the BITMAPCOREHEADER data |
||
2661 | $header = pack('Vvvvv', 0x000c, $width, $height, 0x01, 0x18); |
||
2662 | $data = $header . $data; |
||
2663 | |||
2664 | return [$width, $height, $size, $data]; |
||
2665 | } |
||
2666 | |||
2667 | /** |
||
2668 | * Store the window zoom factor. This should be a reduced fraction but for |
||
2669 | * simplicity we will store all fractions with a numerator of 100. |
||
2670 | */ |
||
2671 | 39 | View Code Duplication | private function writeZoom() |
2672 | { |
||
2673 | // If scale is 100 we don't need to write a record |
||
2674 | 39 | if ($this->phpSheet->getSheetView()->getZoomScale() == 100) { |
|
2675 | 39 | return; |
|
2676 | } |
||
2677 | |||
2678 | $record = 0x00A0; // Record identifier |
||
2679 | $length = 0x0004; // Bytes to follow |
||
2680 | |||
2681 | $header = pack('vv', $record, $length); |
||
2682 | $data = pack('vv', $this->phpSheet->getSheetView()->getZoomScale(), 100); |
||
2683 | $this->append($header . $data); |
||
2684 | } |
||
2685 | |||
2686 | /** |
||
2687 | * Get Escher object. |
||
2688 | * |
||
2689 | * @return \PhpOffice\PhpSpreadsheet\Shared\Escher |
||
2690 | */ |
||
2691 | public function getEscher() |
||
2692 | { |
||
2693 | return $this->escher; |
||
2694 | } |
||
2695 | |||
2696 | /** |
||
2697 | * Set Escher object. |
||
2698 | * |
||
2699 | * @param \PhpOffice\PhpSpreadsheet\Shared\Escher $pValue |
||
2700 | */ |
||
2701 | 10 | public function setEscher(\PhpOffice\PhpSpreadsheet\Shared\Escher $pValue = null) |
|
2702 | { |
||
2703 | 10 | $this->escher = $pValue; |
|
2704 | 10 | } |
|
2705 | |||
2706 | /** |
||
2707 | * Write MSODRAWING record. |
||
2708 | */ |
||
2709 | 39 | private function writeMsoDrawing() |
|
2710 | { |
||
2711 | // write the Escher stream if necessary |
||
2712 | 39 | if (isset($this->escher)) { |
|
2713 | 10 | $writer = new Escher($this->escher); |
|
2714 | 10 | $data = $writer->close(); |
|
2715 | 10 | $spOffsets = $writer->getSpOffsets(); |
|
2716 | 10 | $spTypes = $writer->getSpTypes(); |
|
2717 | // write the neccesary MSODRAWING, OBJ records |
||
2718 | |||
2719 | // split the Escher stream |
||
2720 | 10 | $spOffsets[0] = 0; |
|
2721 | 10 | $nm = count($spOffsets) - 1; // number of shapes excluding first shape |
|
2722 | 10 | for ($i = 1; $i <= $nm; ++$i) { |
|
2723 | // MSODRAWING record |
||
2724 | 10 | $record = 0x00EC; // Record identifier |
|
2725 | |||
2726 | // chunk of Escher stream for one shape |
||
2727 | 10 | $dataChunk = substr($data, $spOffsets[$i - 1], $spOffsets[$i] - $spOffsets[$i - 1]); |
|
2728 | |||
2729 | 10 | $length = strlen($dataChunk); |
|
2730 | 10 | $header = pack('vv', $record, $length); |
|
2731 | |||
2732 | 10 | $this->append($header . $dataChunk); |
|
2733 | |||
2734 | // OBJ record |
||
2735 | 10 | $record = 0x005D; // record identifier |
|
2736 | 10 | $objData = ''; |
|
2737 | |||
2738 | // ftCmo |
||
2739 | 10 | if ($spTypes[$i] == 0x00C9) { |
|
2740 | // Add ftCmo (common object data) subobject |
||
2741 | $objData .= |
||
2742 | 3 | pack( |
|
2743 | 3 | 'vvvvvVVV', |
|
2744 | 3 | 0x0015, // 0x0015 = ftCmo |
|
2745 | 3 | 0x0012, // length of ftCmo data |
|
2746 | 3 | 0x0014, // object type, 0x0014 = filter |
|
2747 | $i, // object id number, Excel seems to use 1-based index, local for the sheet |
||
2748 | 3 | 0x2101, // option flags, 0x2001 is what OpenOffice.org uses |
|
2749 | 3 | 0, // reserved |
|
2750 | 3 | 0, // reserved |
|
2751 | 3 | 0 // reserved |
|
2752 | ); |
||
2753 | |||
2754 | // Add ftSbs Scroll bar subobject |
||
2755 | 3 | $objData .= pack('vv', 0x00C, 0x0014); |
|
2756 | 3 | $objData .= pack('H*', '0000000000000000640001000A00000010000100'); |
|
2757 | // Add ftLbsData (List box data) subobject |
||
2758 | 3 | $objData .= pack('vv', 0x0013, 0x1FEE); |
|
2759 | 3 | $objData .= pack('H*', '00000000010001030000020008005700'); |
|
2760 | } else { |
||
2761 | // Add ftCmo (common object data) subobject |
||
2762 | $objData .= |
||
2763 | 7 | pack( |
|
2764 | 7 | 'vvvvvVVV', |
|
2765 | 7 | 0x0015, // 0x0015 = ftCmo |
|
2766 | 7 | 0x0012, // length of ftCmo data |
|
2767 | 7 | 0x0008, // object type, 0x0008 = picture |
|
2768 | $i, // object id number, Excel seems to use 1-based index, local for the sheet |
||
2769 | 7 | 0x6011, // option flags, 0x6011 is what OpenOffice.org uses |
|
2770 | 7 | 0, // reserved |
|
2771 | 7 | 0, // reserved |
|
2772 | 7 | 0 // reserved |
|
2773 | ); |
||
2774 | } |
||
2775 | |||
2776 | // ftEnd |
||
2777 | $objData .= |
||
2778 | 10 | pack( |
|
2779 | 10 | 'vv', |
|
2780 | 10 | 0x0000, // 0x0000 = ftEnd |
|
2781 | 10 | 0x0000 // length of ftEnd data |
|
2782 | ); |
||
2783 | |||
2784 | 10 | $length = strlen($objData); |
|
2785 | 10 | $header = pack('vv', $record, $length); |
|
2786 | 10 | $this->append($header . $objData); |
|
2787 | } |
||
2788 | } |
||
2789 | 39 | } |
|
2790 | |||
2791 | /** |
||
2792 | * Store the DATAVALIDATIONS and DATAVALIDATION records. |
||
2793 | * |
||
2794 | * @throws \PhpOffice\PhpSpreadsheet\Writer\Exception |
||
2795 | */ |
||
2796 | 39 | private function writeDataValidity() |
|
2797 | { |
||
2798 | // Datavalidation collection |
||
2799 | 39 | $dataValidationCollection = $this->phpSheet->getDataValidationCollection(); |
|
2800 | |||
2801 | // Write data validations? |
||
2802 | 39 | if (!empty($dataValidationCollection)) { |
|
2803 | // DATAVALIDATIONS record |
||
2804 | 2 | $record = 0x01B2; // Record identifier |
|
2805 | 2 | $length = 0x0012; // Bytes to follow |
|
2806 | |||
2807 | 2 | $grbit = 0x0000; // Prompt box at cell, no cached validity data at DV records |
|
2808 | 2 | $horPos = 0x00000000; // Horizontal position of prompt box, if fixed position |
|
2809 | 2 | $verPos = 0x00000000; // Vertical position of prompt box, if fixed position |
|
2810 | 2 | $objId = 0xFFFFFFFF; // Object identifier of drop down arrow object, or -1 if not visible |
|
2811 | |||
2812 | 2 | $header = pack('vv', $record, $length); |
|
2813 | 2 | $data = pack('vVVVV', $grbit, $horPos, $verPos, $objId, count($dataValidationCollection)); |
|
2814 | 2 | $this->append($header . $data); |
|
2815 | |||
2816 | // DATAVALIDATION records |
||
2817 | 2 | $record = 0x01BE; // Record identifier |
|
2818 | |||
2819 | 2 | foreach ($dataValidationCollection as $cellCoordinate => $dataValidation) { |
|
2820 | // initialize record data |
||
2821 | 2 | $data = ''; |
|
2822 | |||
2823 | // options |
||
2824 | 2 | $options = 0x00000000; |
|
2825 | |||
2826 | // data type |
||
2827 | 2 | $type = 0x00; |
|
2828 | 2 | View Code Duplication | switch ($dataValidation->getType()) { |
2829 | 2 | case DataValidation::TYPE_NONE: |
|
2830 | $type = 0x00; |
||
2831 | break; |
||
2832 | 2 | case DataValidation::TYPE_WHOLE: |
|
2833 | $type = 0x01; |
||
2834 | break; |
||
2835 | 2 | case DataValidation::TYPE_DECIMAL: |
|
2836 | $type = 0x02; |
||
2837 | break; |
||
2838 | 2 | case DataValidation::TYPE_LIST: |
|
2839 | $type = 0x03; |
||
2840 | break; |
||
2841 | 2 | case DataValidation::TYPE_DATE: |
|
2842 | $type = 0x04; |
||
2843 | break; |
||
2844 | 2 | case DataValidation::TYPE_TIME: |
|
2845 | $type = 0x05; |
||
2846 | break; |
||
2847 | 2 | case DataValidation::TYPE_TEXTLENGTH: |
|
2848 | $type = 0x06; |
||
2849 | break; |
||
2850 | 2 | case DataValidation::TYPE_CUSTOM: |
|
2851 | $type = 0x07; |
||
2852 | break; |
||
2853 | } |
||
2854 | |||
2855 | 2 | $options |= $type << 0; |
|
2856 | |||
2857 | // error style |
||
2858 | 2 | $errorStyle = 0x00; |
|
2859 | 2 | View Code Duplication | switch ($dataValidation->getErrorStyle()) { |
2860 | 2 | case DataValidation::STYLE_STOP: |
|
2861 | $errorStyle = 0x00; |
||
2862 | break; |
||
2863 | 2 | case DataValidation::STYLE_WARNING: |
|
2864 | $errorStyle = 0x01; |
||
2865 | break; |
||
2866 | 2 | case DataValidation::STYLE_INFORMATION: |
|
2867 | $errorStyle = 0x02; |
||
2868 | break; |
||
2869 | } |
||
2870 | |||
2871 | 2 | $options |= $errorStyle << 4; |
|
2872 | |||
2873 | // explicit formula? |
||
2874 | 2 | if ($type == 0x03 && preg_match('/^\".*\"$/', $dataValidation->getFormula1())) { |
|
2875 | $options |= 0x01 << 7; |
||
2876 | } |
||
2877 | |||
2878 | // empty cells allowed |
||
2879 | 2 | $options |= $dataValidation->getAllowBlank() << 8; |
|
2880 | |||
2881 | // show drop down |
||
2882 | 2 | $options |= (!$dataValidation->getShowDropDown()) << 9; |
|
2883 | |||
2884 | // show input message |
||
2885 | 2 | $options |= $dataValidation->getShowInputMessage() << 18; |
|
2886 | |||
2887 | // show error message |
||
2888 | 2 | $options |= $dataValidation->getShowErrorMessage() << 19; |
|
2889 | |||
2890 | // condition operator |
||
2891 | 2 | $operator = 0x00; |
|
2892 | 2 | View Code Duplication | switch ($dataValidation->getOperator()) { |
2893 | 2 | case DataValidation::OPERATOR_BETWEEN: |
|
2894 | 2 | $operator = 0x00; |
|
2895 | 2 | break; |
|
2896 | case DataValidation::OPERATOR_NOTBETWEEN: |
||
2897 | $operator = 0x01; |
||
2898 | break; |
||
2899 | case DataValidation::OPERATOR_EQUAL: |
||
2900 | $operator = 0x02; |
||
2901 | break; |
||
2902 | case DataValidation::OPERATOR_NOTEQUAL: |
||
2903 | $operator = 0x03; |
||
2904 | break; |
||
2905 | case DataValidation::OPERATOR_GREATERTHAN: |
||
2906 | $operator = 0x04; |
||
2907 | break; |
||
2908 | case DataValidation::OPERATOR_LESSTHAN: |
||
2909 | $operator = 0x05; |
||
2910 | break; |
||
2911 | case DataValidation::OPERATOR_GREATERTHANOREQUAL: |
||
2912 | $operator = 0x06; |
||
2913 | break; |
||
2914 | case DataValidation::OPERATOR_LESSTHANOREQUAL: |
||
2915 | $operator = 0x07; |
||
2916 | break; |
||
2917 | } |
||
2918 | |||
2919 | 2 | $options |= $operator << 20; |
|
2920 | |||
2921 | 2 | $data = pack('V', $options); |
|
2922 | |||
2923 | // prompt title |
||
2924 | 2 | $promptTitle = $dataValidation->getPromptTitle() !== '' ? |
|
2925 | 2 | $dataValidation->getPromptTitle() : chr(0); |
|
2926 | 2 | $data .= StringHelper::UTF8toBIFF8UnicodeLong($promptTitle); |
|
2927 | |||
2928 | // error title |
||
2929 | 2 | $errorTitle = $dataValidation->getErrorTitle() !== '' ? |
|
2930 | 2 | $dataValidation->getErrorTitle() : chr(0); |
|
2931 | 2 | $data .= StringHelper::UTF8toBIFF8UnicodeLong($errorTitle); |
|
2932 | |||
2933 | // prompt text |
||
2934 | 2 | $prompt = $dataValidation->getPrompt() !== '' ? |
|
2935 | 2 | $dataValidation->getPrompt() : chr(0); |
|
2936 | 2 | $data .= StringHelper::UTF8toBIFF8UnicodeLong($prompt); |
|
2937 | |||
2938 | // error text |
||
2939 | 2 | $error = $dataValidation->getError() !== '' ? |
|
2940 | 2 | $dataValidation->getError() : chr(0); |
|
2941 | 2 | $data .= StringHelper::UTF8toBIFF8UnicodeLong($error); |
|
2942 | |||
2943 | // formula 1 |
||
2944 | try { |
||
2945 | 2 | $formula1 = $dataValidation->getFormula1(); |
|
2946 | 2 | if ($type == 0x03) { // list type |
|
2947 | $formula1 = str_replace(',', chr(0), $formula1); |
||
2948 | } |
||
2949 | 2 | $this->parser->parse($formula1); |
|
2950 | 1 | $formula1 = $this->parser->toReversePolish(); |
|
2951 | 1 | $sz1 = strlen($formula1); |
|
2952 | 2 | } catch (PhpSpreadsheetException $e) { |
|
2953 | 2 | $sz1 = 0; |
|
2954 | 2 | $formula1 = ''; |
|
2955 | } |
||
2956 | 2 | $data .= pack('vv', $sz1, 0x0000); |
|
2957 | 2 | $data .= $formula1; |
|
2958 | |||
2959 | // formula 2 |
||
2960 | try { |
||
2961 | 2 | $formula2 = $dataValidation->getFormula2(); |
|
2962 | 2 | if ($formula2 === '') { |
|
2963 | 2 | throw new WriterException('No formula2'); |
|
2964 | } |
||
2965 | 1 | $this->parser->parse($formula2); |
|
2966 | $formula2 = $this->parser->toReversePolish(); |
||
2967 | $sz2 = strlen($formula2); |
||
2968 | 2 | } catch (PhpSpreadsheetException $e) { |
|
2969 | 2 | $sz2 = 0; |
|
2970 | 2 | $formula2 = ''; |
|
2971 | } |
||
2972 | 2 | $data .= pack('vv', $sz2, 0x0000); |
|
2973 | 2 | $data .= $formula2; |
|
2974 | |||
2975 | // cell range address list |
||
2976 | 2 | $data .= pack('v', 0x0001); |
|
2977 | 2 | $data .= $this->writeBIFF8CellRangeAddressFixed($cellCoordinate); |
|
2978 | |||
2979 | 2 | $length = strlen($data); |
|
2980 | 2 | $header = pack('vv', $record, $length); |
|
2981 | |||
2982 | 2 | $this->append($header . $data); |
|
2983 | } |
||
2984 | } |
||
2985 | 39 | } |
|
2986 | |||
2987 | /** |
||
2988 | * Map Error code. |
||
2989 | * |
||
2990 | * @param string $errorCode |
||
2991 | * |
||
2992 | * @return int |
||
2993 | */ |
||
2994 | 4 | private static function mapErrorCode($errorCode) |
|
3015 | |||
3016 | /** |
||
3017 | * Write PLV Record. |
||
3018 | */ |
||
3019 | 39 | private function writePageLayoutView() |
|
3046 | |||
3047 | /** |
||
3048 | * Write CFRule Record. |
||
3049 | * |
||
3050 | * @param Conditional $conditional |
||
3051 | */ |
||
3052 | 2 | private function writeCFRule(Conditional $conditional) |
|
4224 | |||
4225 | /** |
||
4226 | * Write CFHeader record. |
||
4227 | */ |
||
4228 | 2 | private function writeCFHeader() |
|
4275 | } |
||
4276 |
Our type inference engine has found an assignment to a property that is incompatible with the declared type of that property.
Either this assignment is in error or the assigned type should be added to the documentation/type hint for that property..