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

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

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