Passed
Push — master ( fde2cc...f1d90a )
by Mark
17:04 queued 08:09
created

Workbook::writeBoundSheet()   A

Complexity

Conditions 4
Paths 4

Size

Total Lines 36
Code Lines 21

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 16
CRAP Score 4.0218

Importance

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