Completed
Push — develop ( d383bc...d9bd45 )
by Adrien
23:59
created

Workbook::writeAllFonts()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 4
Code Lines 2

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 3
CRAP Score 2

Importance

Changes 0
Metric Value
cc 2
eloc 2
nc 2
nop 0
dl 0
loc 4
ccs 3
cts 3
cp 1
crap 2
rs 10
c 0
b 0
f 0
1
<?php
2
3
namespace PhpOffice\PhpSpreadsheet\Writer\Xls;
4
5
use PhpOffice\PhpSpreadsheet\Cell\Coordinate;
6
use PhpOffice\PhpSpreadsheet\Exception as PhpSpreadsheetException;
7
use PhpOffice\PhpSpreadsheet\Shared\Date;
8
use PhpOffice\PhpSpreadsheet\Shared\StringHelper;
9
use PhpOffice\PhpSpreadsheet\Spreadsheet;
10
use PhpOffice\PhpSpreadsheet\Style\Style;
11
12
// Original file header of PEAR::Spreadsheet_Excel_Writer_Workbook (used as the base for this class):
13
// -----------------------------------------------------------------------------------------
14
// /*
15
// *  Module written/ported by Xavier Noguer <[email protected]>
16
// *
17
// *  The majority of this is _NOT_ my code.  I simply ported it from the
18
// *  PERL Spreadsheet::WriteExcel module.
19
// *
20
// *  The author of the Spreadsheet::WriteExcel module is John McNamara
21
// *  <[email protected]>
22
// *
23
// *  I _DO_ maintain this code, and John McNamara has nothing to do with the
24
// *  porting of this code to PHP.  Any questions directly related to this
25
// *  class library should be directed to me.
26
// *
27
// *  License Information:
28
// *
29
// *    Spreadsheet_Excel_Writer:  A library for generating Excel Spreadsheets
30
// *    Copyright (c) 2002-2003 Xavier Noguer [email protected]
31
// *
32
// *    This library is free software; you can redistribute it and/or
33
// *    modify it under the terms of the GNU Lesser General Public
34
// *    License as published by the Free Software Foundation; either
35
// *    version 2.1 of the License, or (at your option) any later version.
36
// *
37
// *    This library is distributed in the hope that it will be useful,
38
// *    but WITHOUT ANY WARRANTY; without even the implied warranty of
39
// *    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
40
// *    Lesser General Public License for more details.
41
// *
42
// *    You should have received a copy of the GNU Lesser General Public
43
// *    License along with this library; if not, write to the Free Software
44
// *    Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
45
// */
46
class Workbook extends BIFFwriter
47
{
48
    /**
49
     * Formula parser.
50
     *
51
     * @var \PhpOffice\PhpSpreadsheet\Writer\Xls\Parser
52
     */
53
    private $parser;
54
55
    /**
56
     * The BIFF file size for the workbook.
57
     *
58
     * @var int
59
     *
60
     * @see calcSheetOffsets()
61
     */
62
    private $biffSize;
63
64
    /**
65
     * XF Writers.
66
     *
67
     * @var \PhpOffice\PhpSpreadsheet\Writer\Xls\Xf[]
68
     */
69
    private $xfWriters = [];
70
71
    /**
72
     * Array containing the colour palette.
73
     *
74
     * @var array
75
     */
76
    private $palette;
77
78
    /**
79
     * The codepage indicates the text encoding used for strings.
80
     *
81
     * @var int
82
     */
83
    private $codepage;
84
85
    /**
86
     * The country code used for localization.
87
     *
88
     * @var int
89
     */
90
    private $countryCode;
91
92
    /**
93
     * Workbook.
94
     *
95
     * @var Spreadsheet
96
     */
97
    private $spreadsheet;
98
99
    /**
100
     * Fonts writers.
101
     *
102
     * @var Font[]
103
     */
104
    private $fontWriters = [];
105
106
    /**
107
     * Added fonts. Maps from font's hash => index in workbook.
108
     *
109
     * @var array
110
     */
111
    private $addedFonts = [];
112
113
    /**
114
     * Shared number formats.
115
     *
116
     * @var array
117
     */
118
    private $numberFormats = [];
119
120
    /**
121
     * Added number formats. Maps from numberFormat's hash => index in workbook.
122
     *
123
     * @var array
124
     */
125
    private $addedNumberFormats = [];
126
127
    /**
128
     * Sizes of the binary worksheet streams.
129
     *
130
     * @var array
131
     */
132
    private $worksheetSizes = [];
133
134
    /**
135
     * Offsets of the binary worksheet streams relative to the start of the global workbook stream.
136
     *
137
     * @var array
138
     */
139
    private $worksheetOffsets = [];
140
141
    /**
142
     * Total number of shared strings in workbook.
143
     *
144
     * @var int
145
     */
146
    private $stringTotal;
147
148
    /**
149
     * Number of unique shared strings in workbook.
150
     *
151
     * @var int
152
     */
153
    private $stringUnique;
154
155
    /**
156
     * Array of unique shared strings in workbook.
157
     *
158
     * @var array
159
     */
160
    private $stringTable;
161
162
    /**
163
     * Color cache.
164
     */
165
    private $colors;
166
167
    /**
168
     * Escher object corresponding to MSODRAWINGGROUP.
169
     *
170
     * @var \PhpOffice\PhpSpreadsheet\Shared\Escher
171
     */
172
    private $escher;
173
174
    /**
175
     * Class constructor.
176
     *
177
     * @param Spreadsheet $spreadsheet The Workbook
178
     * @param int $str_total Total number of strings
179
     * @param int $str_unique Total number of unique strings
180
     * @param array $str_table String Table
181
     * @param array $colors Colour Table
182
     * @param Parser $parser The formula parser created for the Workbook
183
     */
184 48
    public function __construct(Spreadsheet $spreadsheet, &$str_total, &$str_unique, &$str_table, &$colors, Parser $parser)
185
    {
186
        // It needs to call its parent's constructor explicitly
187 48
        parent::__construct();
188
189 48
        $this->parser = $parser;
190 48
        $this->biffSize = 0;
191 48
        $this->palette = [];
192 48
        $this->countryCode = -1;
193
194 48
        $this->stringTotal = &$str_total;
195 48
        $this->stringUnique = &$str_unique;
196 48
        $this->stringTable = &$str_table;
197 48
        $this->colors = &$colors;
198 48
        $this->setPaletteXl97();
199
200 48
        $this->spreadsheet = $spreadsheet;
201
202 48
        $this->codepage = 0x04B0;
203
204
        // Add empty sheets and Build color cache
205 48
        $countSheets = $spreadsheet->getSheetCount();
206 48
        for ($i = 0; $i < $countSheets; ++$i) {
207 48
            $phpSheet = $spreadsheet->getSheet($i);
208
209 48
            $this->parser->setExtSheet($phpSheet->getTitle(), $i); // Register worksheet name with parser
210
211 48
            $supbook_index = 0x00;
212 48
            $ref = pack('vvv', $supbook_index, $i, $i);
213 48
            $this->parser->references[] = $ref; // Register reference with parser
214
215
            // Sheet tab colors?
216 48
            if ($phpSheet->isTabColorSet()) {
217 5
                $this->addColor($phpSheet->getTabColor()->getRGB());
218
            }
219
        }
220 48
    }
221
222
    /**
223
     * Add a new XF writer.
224
     *
225
     * @param Style
226
     * @param bool Is it a style XF?
0 ignored issues
show
Bug introduced by
The type PhpOffice\PhpSpreadsheet\Writer\Xls\Is was not found. Maybe you did not declare it correctly or list all dependencies?

The issue could also be caused by a filter entry in the build configuration. If the path has been excluded in your configuration, e.g. excluded_paths: ["lib/*"], you can move it to the dependency path list as follows:

filter:
    dependency_paths: ["lib/*"]

For further information see https://scrutinizer-ci.com/docs/tools/php/php-scrutinizer/#list-dependency-paths

Loading history...
227
     * @param Style $style
228
     * @param bool $isStyleXf
229
     *
230
     * @return int Index to XF record
231
     */
232 39
    public function addXfWriter(Style $style, $isStyleXf = false)
233
    {
234 39
        $xfWriter = new Xf($style);
235 39
        $xfWriter->setIsStyleXf($isStyleXf);
236
237
        // Add the font if not already added
238 39
        $fontIndex = $this->addFont($style->getFont());
239
240
        // Assign the font index to the xf record
241 39
        $xfWriter->setFontIndex($fontIndex);
242
243
        // Background colors, best to treat these after the font so black will come after white in custom palette
244 39
        $xfWriter->setFgColor($this->addColor($style->getFill()->getStartColor()->getRGB()));
245 39
        $xfWriter->setBgColor($this->addColor($style->getFill()->getEndColor()->getRGB()));
246 39
        $xfWriter->setBottomColor($this->addColor($style->getBorders()->getBottom()->getColor()->getRGB()));
247 39
        $xfWriter->setTopColor($this->addColor($style->getBorders()->getTop()->getColor()->getRGB()));
248 39
        $xfWriter->setRightColor($this->addColor($style->getBorders()->getRight()->getColor()->getRGB()));
249 39
        $xfWriter->setLeftColor($this->addColor($style->getBorders()->getLeft()->getColor()->getRGB()));
250 39
        $xfWriter->setDiagColor($this->addColor($style->getBorders()->getDiagonal()->getColor()->getRGB()));
251
252
        // Add the number format if it is not a built-in one and not already added
253 39
        if ($style->getNumberFormat()->getBuiltInFormatCode() === false) {
254 14
            $numberFormatHashCode = $style->getNumberFormat()->getHashCode();
255
256 14
            if (isset($this->addedNumberFormats[$numberFormatHashCode])) {
257 6
                $numberFormatIndex = $this->addedNumberFormats[$numberFormatHashCode];
258
            } else {
259 14
                $numberFormatIndex = 164 + count($this->numberFormats);
260 14
                $this->numberFormats[$numberFormatIndex] = $style->getNumberFormat();
261 14
                $this->addedNumberFormats[$numberFormatHashCode] = $numberFormatIndex;
262
            }
263
        } else {
264 39
            $numberFormatIndex = (int) $style->getNumberFormat()->getBuiltInFormatCode();
265
        }
266
267
        // Assign the number format index to xf record
268 39
        $xfWriter->setNumberFormatIndex($numberFormatIndex);
269
270 39
        $this->xfWriters[] = $xfWriter;
271
272 39
        $xfIndex = count($this->xfWriters) - 1;
273
274 39
        return $xfIndex;
275
    }
276
277
    /**
278
     * Add a font to added fonts.
279
     *
280
     * @param \PhpOffice\PhpSpreadsheet\Style\Font $font
281
     *
282
     * @return int Index to FONT record
283
     */
284 39
    public function addFont(\PhpOffice\PhpSpreadsheet\Style\Font $font)
285
    {
286 39
        $fontHashCode = $font->getHashCode();
287 39
        if (isset($this->addedFonts[$fontHashCode])) {
288 39
            $fontIndex = $this->addedFonts[$fontHashCode];
289
        } else {
290 39
            $countFonts = count($this->fontWriters);
291 39
            $fontIndex = ($countFonts < 4) ? $countFonts : $countFonts + 1;
292
293 39
            $fontWriter = new Font($font);
294 39
            $fontWriter->setColorIndex($this->addColor($font->getColor()->getRGB()));
295 39
            $this->fontWriters[] = $fontWriter;
296
297 39
            $this->addedFonts[$fontHashCode] = $fontIndex;
298
        }
299
300 39
        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 48
    private function addColor($rgb)
311
    {
312 48
        if (!isset($this->colors[$rgb])) {
313
            $color =
314
                [
315 48
                    hexdec(substr($rgb, 0, 2)),
316 48
                    hexdec(substr($rgb, 2, 2)),
317 48
                    hexdec(substr($rgb, 4)),
318 48
                    0,
319
                ];
320 48
            $colorIndex = array_search($color, $this->palette);
321 48
            if ($colorIndex) {
322 45
                $this->colors[$rgb] = $colorIndex;
323
            } else {
324 17
                if (count($this->colors) == 0) {
325 9
                    $lastColor = 7;
326
                } else {
327 15
                    $lastColor = end($this->colors);
328
                }
329 17
                if ($lastColor < 57) {
330
                    // then we add a custom color altering the palette
331 15
                    $colorIndex = $lastColor + 1;
332 15
                    $this->palette[$colorIndex] = $color;
333 15
                    $this->colors[$rgb] = $colorIndex;
334
                } else {
335
                    // no room for more custom colors, just map to black
336 48
                    $colorIndex = 0;
337
                }
338
            }
339
        } else {
340
            // fetch already added custom color
341 41
            $colorIndex = $this->colors[$rgb];
342
        }
343
344 48
        return $colorIndex;
0 ignored issues
show
Bug Best Practice introduced by
The expression return $colorIndex also could return the type false|string which is incompatible with the documented return type integer.
Loading history...
345
    }
346
347
    /**
348
     * Sets the colour palette to the Excel 97+ default.
349
     */
350 48
    private function setPaletteXl97()
351
    {
352 48
        $this->palette = [
353
            0x08 => [0x00, 0x00, 0x00, 0x00],
354
            0x09 => [0xff, 0xff, 0xff, 0x00],
355
            0x0A => [0xff, 0x00, 0x00, 0x00],
356
            0x0B => [0x00, 0xff, 0x00, 0x00],
357
            0x0C => [0x00, 0x00, 0xff, 0x00],
358
            0x0D => [0xff, 0xff, 0x00, 0x00],
359
            0x0E => [0xff, 0x00, 0xff, 0x00],
360
            0x0F => [0x00, 0xff, 0xff, 0x00],
361
            0x10 => [0x80, 0x00, 0x00, 0x00],
362
            0x11 => [0x00, 0x80, 0x00, 0x00],
363
            0x12 => [0x00, 0x00, 0x80, 0x00],
364
            0x13 => [0x80, 0x80, 0x00, 0x00],
365
            0x14 => [0x80, 0x00, 0x80, 0x00],
366
            0x15 => [0x00, 0x80, 0x80, 0x00],
367
            0x16 => [0xc0, 0xc0, 0xc0, 0x00],
368
            0x17 => [0x80, 0x80, 0x80, 0x00],
369
            0x18 => [0x99, 0x99, 0xff, 0x00],
370
            0x19 => [0x99, 0x33, 0x66, 0x00],
371
            0x1A => [0xff, 0xff, 0xcc, 0x00],
372
            0x1B => [0xcc, 0xff, 0xff, 0x00],
373
            0x1C => [0x66, 0x00, 0x66, 0x00],
374
            0x1D => [0xff, 0x80, 0x80, 0x00],
375
            0x1E => [0x00, 0x66, 0xcc, 0x00],
376
            0x1F => [0xcc, 0xcc, 0xff, 0x00],
377
            0x20 => [0x00, 0x00, 0x80, 0x00],
378
            0x21 => [0xff, 0x00, 0xff, 0x00],
379
            0x22 => [0xff, 0xff, 0x00, 0x00],
380
            0x23 => [0x00, 0xff, 0xff, 0x00],
381
            0x24 => [0x80, 0x00, 0x80, 0x00],
382
            0x25 => [0x80, 0x00, 0x00, 0x00],
383
            0x26 => [0x00, 0x80, 0x80, 0x00],
384
            0x27 => [0x00, 0x00, 0xff, 0x00],
385
            0x28 => [0x00, 0xcc, 0xff, 0x00],
386
            0x29 => [0xcc, 0xff, 0xff, 0x00],
387
            0x2A => [0xcc, 0xff, 0xcc, 0x00],
388
            0x2B => [0xff, 0xff, 0x99, 0x00],
389
            0x2C => [0x99, 0xcc, 0xff, 0x00],
390
            0x2D => [0xff, 0x99, 0xcc, 0x00],
391
            0x2E => [0xcc, 0x99, 0xff, 0x00],
392
            0x2F => [0xff, 0xcc, 0x99, 0x00],
393
            0x30 => [0x33, 0x66, 0xff, 0x00],
394
            0x31 => [0x33, 0xcc, 0xcc, 0x00],
395
            0x32 => [0x99, 0xcc, 0x00, 0x00],
396
            0x33 => [0xff, 0xcc, 0x00, 0x00],
397
            0x34 => [0xff, 0x99, 0x00, 0x00],
398
            0x35 => [0xff, 0x66, 0x00, 0x00],
399
            0x36 => [0x66, 0x66, 0x99, 0x00],
400
            0x37 => [0x96, 0x96, 0x96, 0x00],
401
            0x38 => [0x00, 0x33, 0x66, 0x00],
402
            0x39 => [0x33, 0x99, 0x66, 0x00],
403
            0x3A => [0x00, 0x33, 0x00, 0x00],
404
            0x3B => [0x33, 0x33, 0x00, 0x00],
405
            0x3C => [0x99, 0x33, 0x00, 0x00],
406
            0x3D => [0x99, 0x33, 0x66, 0x00],
407
            0x3E => [0x33, 0x33, 0x99, 0x00],
408
            0x3F => [0x33, 0x33, 0x33, 0x00],
409
        ];
410 48
    }
411
412
    /**
413
     * Assemble worksheets into a workbook and send the BIFF data to an OLE
414
     * storage.
415
     *
416
     * @param array $pWorksheetSizes The sizes in bytes of the binary worksheet streams
417
     *
418
     * @return string Binary data for workbook stream
419
     */
420 39
    public function writeWorkbook(array $pWorksheetSizes)
421
    {
422 39
        $this->worksheetSizes = $pWorksheetSizes;
423
424
        // Calculate the number of selected worksheet tabs and call the finalization
425
        // methods for each worksheet
426 39
        $total_worksheets = $this->spreadsheet->getSheetCount();
427
428
        // Add part 1 of the Workbook globals, what goes before the SHEET records
429 39
        $this->storeBof(0x0005);
430 39
        $this->writeCodepage();
431 39
        $this->writeWindow1();
432
433 39
        $this->writeDateMode();
434 39
        $this->writeAllFonts();
435 39
        $this->writeAllNumberFormats();
436 39
        $this->writeAllXfs();
437 39
        $this->writeAllStyles();
438 39
        $this->writePalette();
439
440
        // Prepare part 3 of the workbook global stream, what goes after the SHEET records
441 39
        $part3 = '';
442 39
        if ($this->countryCode != -1) {
443
            $part3 .= $this->writeCountry();
444
        }
445 39
        $part3 .= $this->writeRecalcId();
446
447 39
        $part3 .= $this->writeSupbookInternal();
448
        /* TODO: store external SUPBOOK records and XCT and CRN records
449
        in case of external references for BIFF8 */
450 39
        $part3 .= $this->writeExternalsheetBiff8();
451 39
        $part3 .= $this->writeAllDefinedNamesBiff8();
452 39
        $part3 .= $this->writeMsoDrawingGroup();
453 39
        $part3 .= $this->writeSharedStringsTable();
454
455 39
        $part3 .= $this->writeEof();
456
457
        // Add part 2 of the Workbook globals, the SHEET records
458 39
        $this->calcSheetOffsets();
459 39
        for ($i = 0; $i < $total_worksheets; ++$i) {
460 39
            $this->writeBoundSheet($this->spreadsheet->getSheet($i), $this->worksheetOffsets[$i]);
0 ignored issues
show
Bug introduced by
$this->spreadsheet->getSheet($i) of type PhpOffice\PhpSpreadsheet\Worksheet\Worksheet is incompatible with the type PhpOffice\PhpSpreadsheet\Writer\Xls\Worksheet expected by parameter $sheet of PhpOffice\PhpSpreadsheet...book::writeBoundSheet(). ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

460
            $this->writeBoundSheet(/** @scrutinizer ignore-type */ $this->spreadsheet->getSheet($i), $this->worksheetOffsets[$i]);
Loading history...
461
        }
462
463
        // Add part 3 of the Workbook globals
464 39
        $this->_data .= $part3;
465
466 39
        return $this->_data;
467
    }
468
469
    /**
470
     * Calculate offsets for Worksheet BOF records.
471
     */
472 39
    private function calcSheetOffsets()
473
    {
474 39
        $boundsheet_length = 10; // fixed length for a BOUNDSHEET record
475
476
        // size of Workbook globals part 1 + 3
477 39
        $offset = $this->_datasize;
478
479
        // add size of Workbook globals part 2, the length of the SHEET records
480 39
        $total_worksheets = count($this->spreadsheet->getAllSheets());
481 39
        foreach ($this->spreadsheet->getWorksheetIterator() as $sheet) {
482 39
            $offset += $boundsheet_length + strlen(StringHelper::UTF8toBIFF8UnicodeShort($sheet->getTitle()));
483
        }
484
485
        // add the sizes of each of the Sheet substreams, respectively
486 39
        for ($i = 0; $i < $total_worksheets; ++$i) {
487 39
            $this->worksheetOffsets[$i] = $offset;
488 39
            $offset += $this->worksheetSizes[$i];
489
        }
490 39
        $this->biffSize = $offset;
491 39
    }
492
493
    /**
494
     * Store the Excel FONT records.
495
     */
496 39
    private function writeAllFonts()
497
    {
498 39
        foreach ($this->fontWriters as $fontWriter) {
499 39
            $this->append($fontWriter->writeFont());
500
        }
501 39
    }
502
503
    /**
504
     * Store user defined numerical formats i.e. FORMAT records.
505
     */
506 39
    private function writeAllNumberFormats()
507
    {
508 39
        foreach ($this->numberFormats as $numberFormatIndex => $numberFormat) {
509 14
            $this->writeNumberFormat($numberFormat->getFormatCode(), $numberFormatIndex);
510
        }
511 39
    }
512
513
    /**
514
     * Write all XF records.
515
     */
516 39
    private function writeAllXfs()
517
    {
518 39
        foreach ($this->xfWriters as $xfWriter) {
519 39
            $this->append($xfWriter->writeXf());
520
        }
521 39
    }
522
523
    /**
524
     * Write all STYLE records.
525
     */
526 39
    private function writeAllStyles()
527
    {
528 39
        $this->writeStyle();
529 39
    }
530
531
    /**
532
     * Writes all the DEFINEDNAME records (BIFF8).
533
     * So far this is only used for repeating rows/columns (print titles) and print areas.
534
     */
535 39
    private function writeAllDefinedNamesBiff8()
536
    {
537 39
        $chunk = '';
538
539
        // Named ranges
540 39
        if (count($this->spreadsheet->getNamedRanges()) > 0) {
541
            // Loop named ranges
542 4
            $namedRanges = $this->spreadsheet->getNamedRanges();
543 4
            foreach ($namedRanges as $namedRange) {
544
                // Create absolute coordinate
545 4
                $range = Coordinate::splitRange($namedRange->getRange());
546 4
                $iMax = count($range);
547 4 View Code Duplication
                for ($i = 0; $i < $iMax; ++$i) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
548 4
                    $range[$i][0] = '\'' . str_replace("'", "''", $namedRange->getWorksheet()->getTitle()) . '\'!' . Coordinate::absoluteCoordinate($range[$i][0]);
549 4
                    if (isset($range[$i][1])) {
550 3
                        $range[$i][1] = Coordinate::absoluteCoordinate($range[$i][1]);
551
                    }
552
                }
553 4
                $range = Coordinate::buildRange($range); // e.g. Sheet1!$A$1:$B$2
0 ignored issues
show
Unused Code Comprehensibility introduced by
54% of this comment could be valid code. Did you maybe forget this after debugging?

Sometimes obsolete code just ends up commented out instead of removed. In this case it is better to remove the code once you have checked you do not need it.

The code might also have been commented out for debugging purposes. In this case it is vital that someone uncomments it again or your project may behave in very unexpected ways in production.

This check looks for comments that seem to be mostly valid code and reports them.

Loading history...
554
555
                // parse formula
556
                try {
557 4
                    $error = $this->parser->parse($range);
0 ignored issues
show
Unused Code introduced by
The assignment to $error is dead and can be removed.
Loading history...
558 4
                    $formulaData = $this->parser->toReversePolish();
559
560
                    // make sure tRef3d is of type tRef3dR (0x3A)
561 4
                    if (isset($formulaData[0]) and ($formulaData[0] == "\x7A" or $formulaData[0] == "\x5A")) {
0 ignored issues
show
Comprehensibility Best Practice introduced by
Using logical operators such as and instead of && is generally not recommended.

PHP has two types of connecting operators (logical operators, and boolean operators):

  Logical Operators Boolean Operator
AND - meaning and &&
OR - meaning or ||

The difference between these is the order in which they are executed. In most cases, you would want to use a boolean operator like &&, or ||.

Let’s take a look at a few examples:

// Logical operators have lower precedence:
$f = false or true;

// is executed like this:
($f = false) or true;


// Boolean operators have higher precedence:
$f = false || true;

// is executed like this:
$f = (false || true);

Logical Operators are used for Control-Flow

One case where you explicitly want to use logical operators is for control-flow such as this:

$x === 5
    or die('$x must be 5.');

// Instead of
if ($x !== 5) {
    die('$x must be 5.');
}

Since die introduces problems of its own, f.e. it makes our code hardly testable, and prevents any kind of more sophisticated error handling; you probably do not want to use this in real-world code. Unfortunately, logical operators cannot be combined with throw at this point:

// The following is currently a parse error.
$x === 5
    or throw new RuntimeException('$x must be 5.');

These limitations lead to logical operators rarely being of use in current PHP code.

Loading history...
Comprehensibility Best Practice introduced by
Using logical operators such as or instead of || is generally not recommended.

PHP has two types of connecting operators (logical operators, and boolean operators):

  Logical Operators Boolean Operator
AND - meaning and &&
OR - meaning or ||

The difference between these is the order in which they are executed. In most cases, you would want to use a boolean operator like &&, or ||.

Let’s take a look at a few examples:

// Logical operators have lower precedence:
$f = false or true;

// is executed like this:
($f = false) or true;


// Boolean operators have higher precedence:
$f = false || true;

// is executed like this:
$f = (false || true);

Logical Operators are used for Control-Flow

One case where you explicitly want to use logical operators is for control-flow such as this:

$x === 5
    or die('$x must be 5.');

// Instead of
if ($x !== 5) {
    die('$x must be 5.');
}

Since die introduces problems of its own, f.e. it makes our code hardly testable, and prevents any kind of more sophisticated error handling; you probably do not want to use this in real-world code. Unfortunately, logical operators cannot be combined with throw at this point:

// The following is currently a parse error.
$x === 5
    or throw new RuntimeException('$x must be 5.');

These limitations lead to logical operators rarely being of use in current PHP code.

Loading history...
562 1
                        $formulaData = "\x3A" . substr($formulaData, 1);
563
                    }
564
565 4
                    if ($namedRange->getLocalOnly()) {
566
                        // local scope
567
                        $scope = $this->spreadsheet->getIndex($namedRange->getScope()) + 1;
568
                    } else {
569
                        // global scope
570 4
                        $scope = 0;
571
                    }
572 4
                    $chunk .= $this->writeData($this->writeDefinedNameBiff8($namedRange->getName(), $formulaData, $scope, false));
573 4
                } catch (PhpSpreadsheetException $e) {
574
                    // do nothing
575
                }
576
            }
577
        }
578
579
        // total number of sheets
580 39
        $total_worksheets = $this->spreadsheet->getSheetCount();
581
582
        // write the print titles (repeating rows, columns), if any
583 39
        for ($i = 0; $i < $total_worksheets; ++$i) {
584 39
            $sheetSetup = $this->spreadsheet->getSheet($i)->getPageSetup();
585
            // simultaneous repeatColumns repeatRows
586 39
            if ($sheetSetup->isColumnsToRepeatAtLeftSet() && $sheetSetup->isRowsToRepeatAtTopSet()) {
587
                $repeat = $sheetSetup->getColumnsToRepeatAtLeft();
588
                $colmin = Coordinate::columnIndexFromString($repeat[0]) - 1;
589
                $colmax = Coordinate::columnIndexFromString($repeat[1]) - 1;
590
591
                $repeat = $sheetSetup->getRowsToRepeatAtTop();
592
                $rowmin = $repeat[0] - 1;
593
                $rowmax = $repeat[1] - 1;
594
595
                // construct formula data manually
596
                $formulaData = pack('Cv', 0x29, 0x17); // tMemFunc
597
                $formulaData .= pack('Cvvvvv', 0x3B, $i, 0, 65535, $colmin, $colmax); // tArea3d
598
                $formulaData .= pack('Cvvvvv', 0x3B, $i, $rowmin, $rowmax, 0, 255); // tArea3d
599
                $formulaData .= pack('C', 0x10); // tList
600
601
                // store the DEFINEDNAME record
602
                $chunk .= $this->writeData($this->writeDefinedNameBiff8(pack('C', 0x07), $formulaData, $i + 1, true));
603
604
                // (exclusive) either repeatColumns or repeatRows
605 39
            } elseif ($sheetSetup->isColumnsToRepeatAtLeftSet() || $sheetSetup->isRowsToRepeatAtTopSet()) {
606
                // Columns to repeat
607 1
                if ($sheetSetup->isColumnsToRepeatAtLeftSet()) {
608
                    $repeat = $sheetSetup->getColumnsToRepeatAtLeft();
609
                    $colmin = Coordinate::columnIndexFromString($repeat[0]) - 1;
610
                    $colmax = Coordinate::columnIndexFromString($repeat[1]) - 1;
611
                } else {
612 1
                    $colmin = 0;
613 1
                    $colmax = 255;
614
                }
615
                // Rows to repeat
616 1
                if ($sheetSetup->isRowsToRepeatAtTopSet()) {
617 1
                    $repeat = $sheetSetup->getRowsToRepeatAtTop();
618 1
                    $rowmin = $repeat[0] - 1;
619 1
                    $rowmax = $repeat[1] - 1;
620
                } else {
621
                    $rowmin = 0;
622
                    $rowmax = 65535;
623
                }
624
625
                // construct formula data manually because parser does not recognize absolute 3d cell references
626 1
                $formulaData = pack('Cvvvvv', 0x3B, $i, $rowmin, $rowmax, $colmin, $colmax);
627
628
                // store the DEFINEDNAME record
629 1
                $chunk .= $this->writeData($this->writeDefinedNameBiff8(pack('C', 0x07), $formulaData, $i + 1, true));
630
            }
631
        }
632
633
        // write the print areas, if any
634 39
        for ($i = 0; $i < $total_worksheets; ++$i) {
635 39
            $sheetSetup = $this->spreadsheet->getSheet($i)->getPageSetup();
636 39
            if ($sheetSetup->isPrintAreaSet()) {
637
                // Print area, e.g. A3:J6,H1:X20
638
                $printArea = Coordinate::splitRange($sheetSetup->getPrintArea());
639
                $countPrintArea = count($printArea);
640
641
                $formulaData = '';
642
                for ($j = 0; $j < $countPrintArea; ++$j) {
643
                    $printAreaRect = $printArea[$j]; // e.g. A3:J6
644
                    $printAreaRect[0] = Coordinate::coordinateFromString($printAreaRect[0]);
645
                    $printAreaRect[1] = Coordinate::coordinateFromString($printAreaRect[1]);
646
647
                    $print_rowmin = $printAreaRect[0][1] - 1;
648
                    $print_rowmax = $printAreaRect[1][1] - 1;
649
                    $print_colmin = Coordinate::columnIndexFromString($printAreaRect[0][0]) - 1;
650
                    $print_colmax = Coordinate::columnIndexFromString($printAreaRect[1][0]) - 1;
651
652
                    // construct formula data manually because parser does not recognize absolute 3d cell references
653
                    $formulaData .= pack('Cvvvvv', 0x3B, $i, $print_rowmin, $print_rowmax, $print_colmin, $print_colmax);
654
655
                    if ($j > 0) {
656
                        $formulaData .= pack('C', 0x10); // list operator token ','
657
                    }
658
                }
659
660
                // store the DEFINEDNAME record
661
                $chunk .= $this->writeData($this->writeDefinedNameBiff8(pack('C', 0x06), $formulaData, $i + 1, true));
662
            }
663
        }
664
665
        // write autofilters, if any
666 39
        for ($i = 0; $i < $total_worksheets; ++$i) {
667 39
            $sheetAutoFilter = $this->spreadsheet->getSheet($i)->getAutoFilter();
668 39
            $autoFilterRange = $sheetAutoFilter->getRange();
669 39
            if (!empty($autoFilterRange)) {
670 3
                $rangeBounds = Coordinate::rangeBoundaries($autoFilterRange);
671
672
                //Autofilter built in name
673 3
                $name = pack('C', 0x0D);
674
675 3
                $chunk .= $this->writeData($this->writeShortNameBiff8($name, $i + 1, $rangeBounds, true));
676
            }
677
        }
678
679 39
        return $chunk;
680
    }
681
682
    /**
683
     * Write a DEFINEDNAME record for BIFF8 using explicit binary formula data.
684
     *
685
     * @param string $name The name in UTF-8
686
     * @param string $formulaData The binary formula data
687
     * @param int $sheetIndex 1-based sheet index the defined name applies to. 0 = global
688
     * @param bool $isBuiltIn Built-in name?
689
     *
690
     * @return string Complete binary record data
691
     */
692 5
    private function writeDefinedNameBiff8($name, $formulaData, $sheetIndex = 0, $isBuiltIn = false)
693
    {
694 5
        $record = 0x0018;
695
696
        // option flags
697 5
        $options = $isBuiltIn ? 0x20 : 0x00;
698
699
        // length of the name, character count
700 5
        $nlen = StringHelper::countCharacters($name);
701
702
        // name with stripped length field
703 5
        $name = substr(StringHelper::UTF8toBIFF8UnicodeLong($name), 2);
704
705
        // size of the formula (in bytes)
706 5
        $sz = strlen($formulaData);
707
708
        // combine the parts
709 5
        $data = pack('vCCvvvCCCC', $options, 0, $nlen, $sz, 0, $sheetIndex, 0, 0, 0, 0)
710 5
            . $name . $formulaData;
711 5
        $length = strlen($data);
712
713 5
        $header = pack('vv', $record, $length);
714
715 5
        return $header . $data;
716
    }
717
718
    /**
719
     * Write a short NAME record.
720
     *
721
     * @param string $name
722
     * @param string $sheetIndex 1-based sheet index the defined name applies to. 0 = global
723
     * @param integer[][] $rangeBounds range boundaries
724
     * @param bool $isHidden
725
     *
726
     * @return string Complete binary record data
727
     * */
728 3
    private function writeShortNameBiff8($name, $sheetIndex, $rangeBounds, $isHidden = false)
729
    {
730 3
        $record = 0x0018;
731
732
        // option flags
733 3
        $options = ($isHidden ? 0x21 : 0x00);
734
735 3
        $extra = pack(
736 3
            'Cvvvvv',
737 3
            0x3B,
738 3
            $sheetIndex - 1,
739 3
            $rangeBounds[0][1] - 1,
740 3
            $rangeBounds[1][1] - 1,
741 3
            $rangeBounds[0][0] - 1,
742 3
            $rangeBounds[1][0] - 1
743
        );
744
745
        // size of the formula (in bytes)
746 3
        $sz = strlen($extra);
747
748
        // combine the parts
749 3
        $data = pack('vCCvvvCCCCC', $options, 0, 1, $sz, 0, $sheetIndex, 0, 0, 0, 0, 0)
750 3
            . $name . $extra;
751 3
        $length = strlen($data);
752
753 3
        $header = pack('vv', $record, $length);
754
755 3
        return $header . $data;
756
    }
757
758
    /**
759
     * Stores the CODEPAGE biff record.
760
     */
761 39 View Code Duplication
    private function writeCodepage()
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
762
    {
763 39
        $record = 0x0042; // Record identifier
764 39
        $length = 0x0002; // Number of bytes to follow
765 39
        $cv = $this->codepage; // The code page
766
767 39
        $header = pack('vv', $record, $length);
768 39
        $data = pack('v', $cv);
769
770 39
        $this->append($header . $data);
771 39
    }
772
773
    /**
774
     * Write Excel BIFF WINDOW1 record.
775
     */
776 39
    private function writeWindow1()
777
    {
778 39
        $record = 0x003D; // Record identifier
779 39
        $length = 0x0012; // Number of bytes to follow
780
781 39
        $xWn = 0x0000; // Horizontal position of window
782 39
        $yWn = 0x0000; // Vertical position of window
783 39
        $dxWn = 0x25BC; // Width of window
784 39
        $dyWn = 0x1572; // Height of window
785
786 39
        $grbit = 0x0038; // Option flags
787
788
        // not supported by PhpSpreadsheet, so there is only one selected sheet, the active
789 39
        $ctabsel = 1; // Number of workbook tabs selected
790
791 39
        $wTabRatio = 0x0258; // Tab to scrollbar ratio
792
793
        // not supported by PhpSpreadsheet, set to 0
794 39
        $itabFirst = 0; // 1st displayed worksheet
795 39
        $itabCur = $this->spreadsheet->getActiveSheetIndex(); // Active worksheet
796
797 39
        $header = pack('vv', $record, $length);
798 39
        $data = pack('vvvvvvvvv', $xWn, $yWn, $dxWn, $dyWn, $grbit, $itabCur, $itabFirst, $ctabsel, $wTabRatio);
799 39
        $this->append($header . $data);
800 39
    }
801
802
    /**
803
     * Writes Excel BIFF BOUNDSHEET record.
804
     *
805
     * @param Worksheet $sheet Worksheet name
806
     * @param int $offset Location of worksheet BOF
807
     */
808 39
    private function writeBoundSheet($sheet, $offset)
809
    {
810 39
        $sheetname = $sheet->getTitle();
0 ignored issues
show
Bug introduced by
The method getTitle() does not exist on PhpOffice\PhpSpreadsheet\Writer\Xls\Worksheet. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

810
        /** @scrutinizer ignore-call */ 
811
        $sheetname = $sheet->getTitle();

This check looks for calls to methods that do not seem to exist on a given type. It looks for the method on the type itself as well as in inherited classes or implemented interfaces.

This is most likely a typographical error or the method has been renamed.

Loading history...
811 39
        $record = 0x0085; // Record identifier
812
813
        // sheet state
814 39
        switch ($sheet->getSheetState()) {
0 ignored issues
show
Bug introduced by
The method getSheetState() does not exist on PhpOffice\PhpSpreadsheet\Writer\Xls\Worksheet. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

814
        switch ($sheet->/** @scrutinizer ignore-call */ getSheetState()) {

This check looks for calls to methods that do not seem to exist on a given type. It looks for the method on the type itself as well as in inherited classes or implemented interfaces.

This is most likely a typographical error or the method has been renamed.

Loading history...
815 39
            case \PhpOffice\PhpSpreadsheet\Worksheet\Worksheet::SHEETSTATE_VISIBLE:
816 39
                $ss = 0x00;
817
818 39
                break;
819
            case \PhpOffice\PhpSpreadsheet\Worksheet\Worksheet::SHEETSTATE_HIDDEN:
820
                $ss = 0x01;
821
822
                break;
823
            case \PhpOffice\PhpSpreadsheet\Worksheet\Worksheet::SHEETSTATE_VERYHIDDEN:
824
                $ss = 0x02;
825
826
                break;
827
            default:
828
                $ss = 0x00;
829
830
                break;
831
        }
832
833
        // sheet type
834 39
        $st = 0x00;
835
836 39
        $grbit = 0x0000; // Visibility and sheet type
0 ignored issues
show
Unused Code introduced by
The assignment to $grbit is dead and can be removed.
Loading history...
837
838 39
        $data = pack('VCC', $offset, $ss, $st);
839 39
        $data .= StringHelper::UTF8toBIFF8UnicodeShort($sheetname);
840
841 39
        $length = strlen($data);
842 39
        $header = pack('vv', $record, $length);
843 39
        $this->append($header . $data);
844 39
    }
845
846
    /**
847
     * Write Internal SUPBOOK record.
848
     */
849 39 View Code Duplication
    private function writeSupbookInternal()
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
850
    {
851 39
        $record = 0x01AE; // Record identifier
852 39
        $length = 0x0004; // Bytes to follow
853
854 39
        $header = pack('vv', $record, $length);
855 39
        $data = pack('vv', $this->spreadsheet->getSheetCount(), 0x0401);
856
857 39
        return $this->writeData($header . $data);
858
    }
859
860
    /**
861
     * Writes the Excel BIFF EXTERNSHEET record. These references are used by
862
     * formulas.
863
     */
864 39
    private function writeExternalsheetBiff8()
865
    {
866 39
        $totalReferences = count($this->parser->references);
867 39
        $record = 0x0017; // Record identifier
868 39
        $length = 2 + 6 * $totalReferences; // Number of bytes to follow
869
870 39
        $supbook_index = 0; // FIXME: only using internal SUPBOOK record
0 ignored issues
show
Unused Code introduced by
The assignment to $supbook_index is dead and can be removed.
Loading history...
871 39
        $header = pack('vv', $record, $length);
872 39
        $data = pack('v', $totalReferences);
873 39
        for ($i = 0; $i < $totalReferences; ++$i) {
874 39
            $data .= $this->parser->references[$i];
875
        }
876
877 39
        return $this->writeData($header . $data);
878
    }
879
880
    /**
881
     * Write Excel BIFF STYLE records.
882
     */
883 39
    private function writeStyle()
884
    {
885 39
        $record = 0x0293; // Record identifier
886 39
        $length = 0x0004; // Bytes to follow
887
888 39
        $ixfe = 0x8000; // Index to cell style XF
889 39
        $BuiltIn = 0x00; // Built-in style
890 39
        $iLevel = 0xff; // Outline style level
891
892 39
        $header = pack('vv', $record, $length);
893 39
        $data = pack('vCC', $ixfe, $BuiltIn, $iLevel);
894 39
        $this->append($header . $data);
895 39
    }
896
897
    /**
898
     * Writes Excel FORMAT record for non "built-in" numerical formats.
899
     *
900
     * @param string $format Custom format string
901
     * @param int $ifmt Format index code
902
     */
903 14
    private function writeNumberFormat($format, $ifmt)
904
    {
905 14
        $record = 0x041E; // Record identifier
906
907 14
        $numberFormatString = StringHelper::UTF8toBIFF8UnicodeLong($format);
908 14
        $length = 2 + strlen($numberFormatString); // Number of bytes to follow
909
910 14
        $header = pack('vv', $record, $length);
911 14
        $data = pack('v', $ifmt) . $numberFormatString;
912 14
        $this->append($header . $data);
913 14
    }
914
915
    /**
916
     * Write DATEMODE record to indicate the date system in use (1904 or 1900).
917
     */
918 39
    private function writeDateMode()
919
    {
920 39
        $record = 0x0022; // Record identifier
921 39
        $length = 0x0002; // Bytes to follow
922
923 39
        $f1904 = (Date::getExcelCalendar() == Date::CALENDAR_MAC_1904)
924
            ? 1
925 39
            : 0; // Flag for 1904 date system
926
927 39
        $header = pack('vv', $record, $length);
928 39
        $data = pack('v', $f1904);
929 39
        $this->append($header . $data);
930 39
    }
931
932
    /**
933
     * Stores the COUNTRY record for localization.
934
     *
935
     * @return string
936
     */
937 View Code Duplication
    private function writeCountry()
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
938
    {
939
        $record = 0x008C; // Record identifier
940
        $length = 4; // Number of bytes to follow
941
942
        $header = pack('vv', $record, $length);
943
        // using the same country code always for simplicity
944
        $data = pack('vv', $this->countryCode, $this->countryCode);
945
946
        return $this->writeData($header . $data);
947
    }
948
949
    /**
950
     * Write the RECALCID record.
951
     *
952
     * @return string
953
     */
954 39 View Code Duplication
    private function writeRecalcId()
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
955
    {
956 39
        $record = 0x01C1; // Record identifier
957 39
        $length = 8; // Number of bytes to follow
958
959 39
        $header = pack('vv', $record, $length);
960
961
        // by inspection of real Excel files, MS Office Excel 2007 writes this
962 39
        $data = pack('VV', 0x000001C1, 0x00001E667);
963
964 39
        return $this->writeData($header . $data);
965
    }
966
967
    /**
968
     * Stores the PALETTE biff record.
969
     */
970 39
    private function writePalette()
971
    {
972 39
        $aref = $this->palette;
973
974 39
        $record = 0x0092; // Record identifier
975 39
        $length = 2 + 4 * count($aref); // Number of bytes to follow
976 39
        $ccv = count($aref); // Number of RGB values to follow
977 39
        $data = ''; // The RGB data
978
979
        // Pack the RGB data
980 39
        foreach ($aref as $color) {
981 39
            foreach ($color as $byte) {
982 39
                $data .= pack('C', $byte);
983
            }
984
        }
985
986 39
        $header = pack('vvv', $record, $length, $ccv);
987 39
        $this->append($header . $data);
988 39
    }
989
990
    /**
991
     * Handling of the SST continue blocks is complicated by the need to include an
992
     * additional continuation byte depending on whether the string is split between
993
     * blocks or whether it starts at the beginning of the block. (There are also
994
     * additional complications that will arise later when/if Rich Strings are
995
     * supported).
996
     *
997
     * The Excel documentation says that the SST record should be followed by an
998
     * EXTSST record. The EXTSST record is a hash table that is used to optimise
999
     * access to SST. However, despite the documentation it doesn't seem to be
1000
     * required so we will ignore it.
1001
     *
1002
     * @return string Binary data
1003
     */
1004 39
    private function writeSharedStringsTable()
1005
    {
1006
        // maximum size of record data (excluding record header)
1007 39
        $continue_limit = 8224;
1008
1009
        // initialize array of record data blocks
1010 39
        $recordDatas = [];
1011
1012
        // start SST record data block with total number of strings, total number of unique strings
1013 39
        $recordData = pack('VV', $this->stringTotal, $this->stringUnique);
1014
1015
        // loop through all (unique) strings in shared strings table
1016 39
        foreach (array_keys($this->stringTable) as $string) {
1017
            // here $string is a BIFF8 encoded string
1018
1019
            // length = character count
1020 35
            $headerinfo = unpack('vlength/Cencoding', $string);
0 ignored issues
show
Bug introduced by
The call to unpack() has too few arguments starting with offset. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

1020
            $headerinfo = /** @scrutinizer ignore-call */ unpack('vlength/Cencoding', $string);

This check compares calls to functions or methods with their respective definitions. If the call has less arguments than are defined, it raises an issue.

If a function is defined several times with a different number of parameters, the check may pick up the wrong definition and report false positives. One codebase where this has been known to happen is Wordpress. Please note the @ignore annotation hint above.

Loading history...
1021
1022
            // currently, this is always 1 = uncompressed
1023 35
            $encoding = $headerinfo['encoding'];
1024
1025
            // initialize finished writing current $string
1026 35
            $finished = false;
1027
1028 35
            while ($finished === false) {
1029
                // normally, there will be only one cycle, but if string cannot immediately be written as is
1030
                // there will be need for more than one cylcle, if string longer than one record data block, there
1031
                // may be need for even more cycles
1032
1033 35
                if (strlen($recordData) + strlen($string) <= $continue_limit) {
1034
                    // then we can write the string (or remainder of string) without any problems
1035 35
                    $recordData .= $string;
1036
1037 35
                    if (strlen($recordData) + strlen($string) == $continue_limit) {
1038
                        // we close the record data block, and initialize a new one
1039
                        $recordDatas[] = $recordData;
1040
                        $recordData = '';
1041
                    }
1042
1043
                    // we are finished writing this string
1044 35
                    $finished = true;
1045
                } else {
1046
                    // special treatment writing the string (or remainder of the string)
1047
                    // If the string is very long it may need to be written in more than one CONTINUE record.
1048
1049
                    // check how many bytes more there is room for in the current record
1050
                    $space_remaining = $continue_limit - strlen($recordData);
1051
1052
                    // minimum space needed
1053
                    // uncompressed: 2 byte string length length field + 1 byte option flags + 2 byte character
1054
                    // compressed:   2 byte string length length field + 1 byte option flags + 1 byte character
1055
                    $min_space_needed = ($encoding == 1) ? 5 : 4;
1056
1057
                    // We have two cases
1058
                    // 1. space remaining is less than minimum space needed
1059
                    //        here we must waste the space remaining and move to next record data block
1060
                    // 2. space remaining is greater than or equal to minimum space needed
1061
                    //        here we write as much as we can in the current block, then move to next record data block
1062
1063
                    // 1. space remaining is less than minimum space needed
1064
                    if ($space_remaining < $min_space_needed) {
1065
                        // we close the block, store the block data
1066
                        $recordDatas[] = $recordData;
1067
1068
                        // and start new record data block where we start writing the string
1069
                        $recordData = '';
1070
1071
                        // 2. space remaining is greater than or equal to minimum space needed
1072
                    } else {
1073
                        // initialize effective remaining space, for Unicode strings this may need to be reduced by 1, see below
1074
                        $effective_space_remaining = $space_remaining;
1075
1076
                        // for uncompressed strings, sometimes effective space remaining is reduced by 1
1077
                        if ($encoding == 1 && (strlen($string) - $space_remaining) % 2 == 1) {
1078
                            --$effective_space_remaining;
1079
                        }
1080
1081
                        // one block fininshed, store the block data
1082
                        $recordData .= substr($string, 0, $effective_space_remaining);
1083
1084
                        $string = substr($string, $effective_space_remaining); // for next cycle in while loop
1085
                        $recordDatas[] = $recordData;
1086
1087
                        // start new record data block with the repeated option flags
1088
                        $recordData = pack('C', $encoding);
1089
                    }
1090
                }
1091
            }
1092
        }
1093
1094
        // Store the last record data block unless it is empty
1095
        // if there was no need for any continue records, this will be the for SST record data block itself
1096 39
        if (strlen($recordData) > 0) {
1097 39
            $recordDatas[] = $recordData;
1098
        }
1099
1100
        // combine into one chunk with all the blocks SST, CONTINUE,...
1101 39
        $chunk = '';
1102 39
        foreach ($recordDatas as $i => $recordData) {
1103
            // first block should have the SST record header, remaing should have CONTINUE header
1104 39
            $record = ($i == 0) ? 0x00FC : 0x003C;
1105
1106 39
            $header = pack('vv', $record, strlen($recordData));
1107 39
            $data = $header . $recordData;
1108
1109 39
            $chunk .= $this->writeData($data);
1110
        }
1111
1112 39
        return $chunk;
1113
    }
1114
1115
    /**
1116
     * Writes the MSODRAWINGGROUP record if needed. Possibly split using CONTINUE records.
1117
     */
1118 39
    private function writeMsoDrawingGroup()
1119
    {
1120
        // write the Escher stream if necessary
1121 39
        if (isset($this->escher)) {
1122 10
            $writer = new Escher($this->escher);
1123 10
            $data = $writer->close();
1124
1125 10
            $record = 0x00EB;
1126 10
            $length = strlen($data);
1127 10
            $header = pack('vv', $record, $length);
1128
1129 10
            return $this->writeData($header . $data);
1130
        }
1131
1132 29
        return '';
1133
    }
1134
1135
    /**
1136
     * Get Escher object.
1137
     *
1138
     * @return \PhpOffice\PhpSpreadsheet\Shared\Escher
1139
     */
1140
    public function getEscher()
1141
    {
1142
        return $this->escher;
1143
    }
1144
1145
    /**
1146
     * Set Escher object.
1147
     *
1148
     * @param \PhpOffice\PhpSpreadsheet\Shared\Escher $pValue
1149
     */
1150 10
    public function setEscher(\PhpOffice\PhpSpreadsheet\Shared\Escher $pValue = null)
1151
    {
1152 10
        $this->escher = $pValue;
1153 10
    }
1154
}
1155