Passed
Pull Request — master (#3382)
by Mark
21:01 queued 10:02
created

Worksheet::writeObjPicture()   B

Complexity

Conditions 1
Paths 1

Size

Total Lines 63
Code Lines 56

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 2

Importance

Changes 0
Metric Value
cc 1
eloc 56
nc 1
nop 8
dl 0
loc 63
ccs 0
cts 57
cp 0
crap 2
rs 8.9599
c 0
b 0
f 0

How to fix   Long Method    Many Parameters   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

Many Parameters

Methods with many parameters are not only hard to understand, but their parameters also often become inconsistent when you need more, or different data.

There are several approaches to avoid long parameter lists:

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