Xls::buildWorkbookEscher()   B
last analyzed

Complexity

Conditions 6
Paths 10

Size

Total Lines 54
Code Lines 28

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 29
CRAP Score 6

Importance

Changes 0
Metric Value
cc 6
eloc 28
nc 10
nop 0
dl 0
loc 54
ccs 29
cts 29
cp 1
crap 6
rs 8.8497
c 0
b 0
f 0

How to fix   Long Method   

Long Method

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

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

Commonly applied refactorings include:

1
<?php
2
3
namespace PhpOffice\PhpSpreadsheet\Writer;
4
5
use PhpOffice\PhpSpreadsheet\Calculation\Calculation;
6
use PhpOffice\PhpSpreadsheet\Calculation\Functions;
7
use PhpOffice\PhpSpreadsheet\Cell\Cell;
8
use PhpOffice\PhpSpreadsheet\Cell\Coordinate;
9
use PhpOffice\PhpSpreadsheet\RichText\RichText;
10
use PhpOffice\PhpSpreadsheet\RichText\Run;
11
use PhpOffice\PhpSpreadsheet\Shared\Escher;
12
use PhpOffice\PhpSpreadsheet\Shared\Escher\DgContainer;
13
use PhpOffice\PhpSpreadsheet\Shared\Escher\DgContainer\SpgrContainer;
14
use PhpOffice\PhpSpreadsheet\Shared\Escher\DgContainer\SpgrContainer\SpContainer;
15
use PhpOffice\PhpSpreadsheet\Shared\Escher\DggContainer;
16
use PhpOffice\PhpSpreadsheet\Shared\Escher\DggContainer\BstoreContainer;
17
use PhpOffice\PhpSpreadsheet\Shared\Escher\DggContainer\BstoreContainer\BSE;
18
use PhpOffice\PhpSpreadsheet\Shared\Escher\DggContainer\BstoreContainer\BSE\Blip;
19
use PhpOffice\PhpSpreadsheet\Shared\OLE;
20
use PhpOffice\PhpSpreadsheet\Shared\OLE\PPS\File;
21
use PhpOffice\PhpSpreadsheet\Shared\OLE\PPS\Root;
22
use PhpOffice\PhpSpreadsheet\Spreadsheet;
23
use PhpOffice\PhpSpreadsheet\Worksheet\BaseDrawing;
24
use PhpOffice\PhpSpreadsheet\Worksheet\Drawing;
25
use PhpOffice\PhpSpreadsheet\Worksheet\MemoryDrawing;
26
use PhpOffice\PhpSpreadsheet\Writer\Xls\Parser;
27
use PhpOffice\PhpSpreadsheet\Writer\Xls\Workbook;
28
use PhpOffice\PhpSpreadsheet\Writer\Xls\Worksheet;
29
30
class Xls extends BaseWriter
31
{
32
    /**
33
     * PhpSpreadsheet object.
34
     */
35
    private Spreadsheet $spreadsheet;
36
37
    /**
38
     * Total number of shared strings in workbook.
39
     */
40
    private int $strTotal = 0;
41
42
    /**
43
     * Number of unique shared strings in workbook.
44
     */
45
    private int $strUnique = 0;
46
47
    /**
48
     * Array of unique shared strings in workbook.
49
     */
50
    private array $strTable = [];
51
52
    /**
53
     * Color cache. Mapping between RGB value and color index.
54
     */
55
    private array $colors;
56
57
    /**
58
     * Formula parser.
59
     */
60
    private Parser $parser;
61
62
    /**
63
     * Identifier clusters for drawings. Used in MSODRAWINGGROUP record.
64
     */
65
    private array $IDCLs;
66
67
    /**
68
     * Basic OLE object summary information.
69
     */
70
    private string $summaryInformation;
71
72
    /**
73
     * Extended OLE object document summary information.
74
     */
75
    private string $documentSummaryInformation;
76
77
    private Workbook $writerWorkbook;
78
79
    /**
80
     * @var Worksheet[]
81
     */
82
    private array $writerWorksheets;
83
84
    /**
85
     * Create a new Xls Writer.
86
     *
87
     * @param Spreadsheet $spreadsheet PhpSpreadsheet object
88
     */
89 102
    public function __construct(Spreadsheet $spreadsheet)
90
    {
91 102
        $this->spreadsheet = $spreadsheet;
92
93 102
        $this->parser = new Parser($spreadsheet);
94
    }
95
96
    /**
97
     * Save Spreadsheet to file.
98
     *
99
     * @param resource|string $filename
100
     */
101 101
    public function save($filename, int $flags = 0): void
102
    {
103 101
        $this->processFlags($flags);
104
105
        // garbage collect
106 101
        $this->spreadsheet->garbageCollect();
107
108 101
        $saveDebugLog = Calculation::getInstance($this->spreadsheet)->getDebugLog()->getWriteDebugLog();
109 101
        Calculation::getInstance($this->spreadsheet)->getDebugLog()->setWriteDebugLog(false);
110 101
        $saveDateReturnType = Functions::getReturnDateType();
111 101
        Functions::setReturnDateType(Functions::RETURNDATE_EXCEL);
112
113
        // initialize colors array
114 101
        $this->colors = [];
115
116
        // Initialise workbook writer
117 101
        $this->writerWorkbook = new Workbook($this->spreadsheet, $this->strTotal, $this->strUnique, $this->strTable, $this->colors, $this->parser);
118
119
        // Initialise worksheet writers
120 101
        $countSheets = $this->spreadsheet->getSheetCount();
121 101
        for ($i = 0; $i < $countSheets; ++$i) {
122 101
            $this->writerWorksheets[$i] = new Worksheet($this->strTotal, $this->strUnique, $this->strTable, $this->colors, $this->parser, $this->preCalculateFormulas, $this->spreadsheet->getSheet($i));
123
        }
124
125
        // build Escher objects. Escher objects for workbooks needs to be build before Escher object for workbook.
126 101
        $this->buildWorksheetEschers();
127 101
        $this->buildWorkbookEscher();
128
129
        // add 15 identical cell style Xfs
130
        // for now, we use the first cellXf instead of cellStyleXf
131 101
        $cellXfCollection = $this->spreadsheet->getCellXfCollection();
132 101
        for ($i = 0; $i < 15; ++$i) {
133 101
            $this->writerWorkbook->addXfWriter($cellXfCollection[0], true);
134
        }
135
136
        // add all the cell Xfs
137 101
        foreach ($this->spreadsheet->getCellXfCollection() as $style) {
138 101
            $this->writerWorkbook->addXfWriter($style, false);
139
        }
140
141
        // add fonts from rich text eleemnts
142 101
        for ($i = 0; $i < $countSheets; ++$i) {
143 101
            foreach ($this->writerWorksheets[$i]->phpSheet->getCellCollection()->getCoordinates() as $coordinate) {
144
                /** @var Cell $cell */
145 96
                $cell = $this->writerWorksheets[$i]->phpSheet->getCellCollection()->get($coordinate);
146 96
                $cVal = $cell->getValue();
147 96
                if ($cVal instanceof RichText) {
148 11
                    $elements = $cVal->getRichTextElements();
149 11
                    foreach ($elements as $element) {
150 11
                        if ($element instanceof Run) {
151 11
                            $font = $element->getFont();
152 11
                            if ($font !== null) {
153 11
                                $this->writerWorksheets[$i]->fontHashIndex[$font->getHashCode()] = $this->writerWorkbook->addFont($font);
154
                            }
155
                        }
156
                    }
157
                }
158
            }
159
        }
160
161
        // initialize OLE file
162 101
        $workbookStreamName = 'Workbook';
163 101
        $OLE = new File(OLE::ascToUcs($workbookStreamName));
164
165
        // Write the worksheet streams before the global workbook stream,
166
        // because the byte sizes of these are needed in the global workbook stream
167 101
        $worksheetSizes = [];
168 101
        for ($i = 0; $i < $countSheets; ++$i) {
169 101
            $this->writerWorksheets[$i]->close();
170 100
            $worksheetSizes[] = $this->writerWorksheets[$i]->_datasize;
171
        }
172
173
        // add binary data for global workbook stream
174 100
        $OLE->append($this->writerWorkbook->writeWorkbook($worksheetSizes));
175
176
        // add binary data for sheet streams
177 100
        for ($i = 0; $i < $countSheets; ++$i) {
178 100
            $OLE->append($this->writerWorksheets[$i]->getData());
179
        }
180
181 100
        $this->documentSummaryInformation = $this->writeDocumentSummaryInformation();
182
        // initialize OLE Document Summary Information
183 100
        if (!empty($this->documentSummaryInformation)) {
184 100
            $OLE_DocumentSummaryInformation = new File(OLE::ascToUcs(chr(5) . 'DocumentSummaryInformation'));
185 100
            $OLE_DocumentSummaryInformation->append($this->documentSummaryInformation);
186
        }
187
188 100
        $this->summaryInformation = $this->writeSummaryInformation();
189
        // initialize OLE Summary Information
190 100
        if (!empty($this->summaryInformation)) {
191 100
            $OLE_SummaryInformation = new File(OLE::ascToUcs(chr(5) . 'SummaryInformation'));
192 100
            $OLE_SummaryInformation->append($this->summaryInformation);
193
        }
194
195
        // define OLE Parts
196 100
        $arrRootData = [$OLE];
197
        // initialize OLE Properties file
198 100
        if (isset($OLE_SummaryInformation)) {
199 100
            $arrRootData[] = $OLE_SummaryInformation;
200
        }
201
        // initialize OLE Extended Properties file
202 100
        if (isset($OLE_DocumentSummaryInformation)) {
203 100
            $arrRootData[] = $OLE_DocumentSummaryInformation;
204
        }
205
206 100
        $time = $this->spreadsheet->getProperties()->getModified();
207 100
        $root = new Root($time, $time, $arrRootData);
208
        // save the OLE file
209 100
        $this->openFileHandle($filename);
210 100
        $root->save($this->fileHandle);
211 100
        $this->maybeCloseFileHandle();
212
213 100
        Functions::setReturnDateType($saveDateReturnType);
214 100
        Calculation::getInstance($this->spreadsheet)->getDebugLog()->setWriteDebugLog($saveDebugLog);
215
    }
216
217
    /**
218
     * Build the Worksheet Escher objects.
219
     */
220 101
    private function buildWorksheetEschers(): void
221
    {
222
        // 1-based index to BstoreContainer
223 101
        $blipIndex = 0;
224 101
        $lastReducedSpId = 0;
225 101
        $lastSpId = 0;
226
227 101
        foreach ($this->spreadsheet->getAllsheets() as $sheet) {
228
            // sheet index
229 101
            $sheetIndex = $sheet->getParentOrThrow()->getIndex($sheet);
230
231
            // check if there are any shapes for this sheet
232 101
            $filterRange = $sheet->getAutoFilter()->getRange();
233 101
            if (count($sheet->getDrawingCollection()) == 0 && empty($filterRange)) {
234 88
                continue;
235
            }
236
237
            // create intermediate Escher object
238 14
            $escher = new Escher();
239
240
            // dgContainer
241 14
            $dgContainer = new DgContainer();
242
243
            // set the drawing index (we use sheet index + 1)
244 14
            $dgId = $sheet->getParentOrThrow()->getIndex($sheet) + 1;
245 14
            $dgContainer->setDgId($dgId);
246 14
            $escher->setDgContainer($dgContainer);
247
248
            // spgrContainer
249 14
            $spgrContainer = new SpgrContainer();
250 14
            $dgContainer->setSpgrContainer($spgrContainer);
251
252
            // add one shape which is the group shape
253 14
            $spContainer = new SpContainer();
254 14
            $spContainer->setSpgr(true);
255 14
            $spContainer->setSpType(0);
256 14
            $spContainer->setSpId(($sheet->getParentOrThrow()->getIndex($sheet) + 1) << 10);
257 14
            $spgrContainer->addChild($spContainer);
258
259
            // add the shapes
260
261 14
            $countShapes[$sheetIndex] = 0; // count number of shapes (minus group shape), in sheet
262
263 14
            foreach ($sheet->getDrawingCollection() as $drawing) {
264 11
                ++$blipIndex;
265
266 11
                ++$countShapes[$sheetIndex];
267
268
                // add the shape
269 11
                $spContainer = new SpContainer();
270
271
                // set the shape type
272 11
                $spContainer->setSpType(0x004B);
273
                // set the shape flag
274 11
                $spContainer->setSpFlag(0x02);
275
276
                // set the shape index (we combine 1-based sheet index and $countShapes to create unique shape index)
277 11
                $reducedSpId = $countShapes[$sheetIndex];
278 11
                $spId = $reducedSpId | ($sheet->getParentOrThrow()->getIndex($sheet) + 1) << 10;
279 11
                $spContainer->setSpId($spId);
280
281
                // keep track of last reducedSpId
282 11
                $lastReducedSpId = $reducedSpId;
283
284
                // keep track of last spId
285 11
                $lastSpId = $spId;
286
287
                // set the BLIP index
288 11
                $spContainer->setOPT(0x4104, $blipIndex);
289
290
                // set coordinates and offsets, client anchor
291 11
                $coordinates = $drawing->getCoordinates();
292 11
                $offsetX = $drawing->getOffsetX();
293 11
                $offsetY = $drawing->getOffsetY();
294 11
                $width = $drawing->getWidth();
295 11
                $height = $drawing->getHeight();
296
297 11
                $twoAnchor = \PhpOffice\PhpSpreadsheet\Shared\Xls::oneAnchor2twoAnchor($sheet, $coordinates, $offsetX, $offsetY, $width, $height);
298
299 11
                if (is_array($twoAnchor)) {
300 11
                    $spContainer->setStartCoordinates($twoAnchor['startCoordinates']);
301 11
                    $spContainer->setStartOffsetX($twoAnchor['startOffsetX']);
302 11
                    $spContainer->setStartOffsetY($twoAnchor['startOffsetY']);
303 11
                    $spContainer->setEndCoordinates($twoAnchor['endCoordinates']);
304 11
                    $spContainer->setEndOffsetX($twoAnchor['endOffsetX']);
305 11
                    $spContainer->setEndOffsetY($twoAnchor['endOffsetY']);
306
307 11
                    $spgrContainer->addChild($spContainer);
308
                }
309
            }
310
311
            // AutoFilters
312 14
            if (!empty($filterRange)) {
313 3
                $rangeBounds = Coordinate::rangeBoundaries($filterRange);
314 3
                $iNumColStart = $rangeBounds[0][0];
315 3
                $iNumColEnd = $rangeBounds[1][0];
316
317 3
                $iInc = $iNumColStart;
318 3
                while ($iInc <= $iNumColEnd) {
319 3
                    ++$countShapes[$sheetIndex];
320
321
                    // create an Drawing Object for the dropdown
322 3
                    $oDrawing = new BaseDrawing();
323
                    // get the coordinates of drawing
324 3
                    $cDrawing = Coordinate::stringFromColumnIndex($iInc) . $rangeBounds[0][1];
325 3
                    $oDrawing->setCoordinates($cDrawing);
326 3
                    $oDrawing->setWorksheet($sheet);
327
328
                    // add the shape
329 3
                    $spContainer = new SpContainer();
330
                    // set the shape type
331 3
                    $spContainer->setSpType(0x00C9);
332
                    // set the shape flag
333 3
                    $spContainer->setSpFlag(0x01);
334
335
                    // set the shape index (we combine 1-based sheet index and $countShapes to create unique shape index)
336 3
                    $reducedSpId = $countShapes[$sheetIndex];
337 3
                    $spId = $reducedSpId | ($sheet->getParentOrThrow()->getIndex($sheet) + 1) << 10;
338 3
                    $spContainer->setSpId($spId);
339
340
                    // keep track of last reducedSpId
341 3
                    $lastReducedSpId = $reducedSpId;
342
343
                    // keep track of last spId
344 3
                    $lastSpId = $spId;
345
346 3
                    $spContainer->setOPT(0x007F, 0x01040104); // Protection -> fLockAgainstGrouping
347 3
                    $spContainer->setOPT(0x00BF, 0x00080008); // Text -> fFitTextToShape
348 3
                    $spContainer->setOPT(0x01BF, 0x00010000); // Fill Style -> fNoFillHitTest
349 3
                    $spContainer->setOPT(0x01FF, 0x00080000); // Line Style -> fNoLineDrawDash
350 3
                    $spContainer->setOPT(0x03BF, 0x000A0000); // Group Shape -> fPrint
351
352
                    // set coordinates and offsets, client anchor
353 3
                    $endCoordinates = Coordinate::stringFromColumnIndex($iInc);
354 3
                    $endCoordinates .= $rangeBounds[0][1] + 1;
355
356 3
                    $spContainer->setStartCoordinates($cDrawing);
357 3
                    $spContainer->setStartOffsetX(0);
358 3
                    $spContainer->setStartOffsetY(0);
359 3
                    $spContainer->setEndCoordinates($endCoordinates);
360 3
                    $spContainer->setEndOffsetX(0);
361 3
                    $spContainer->setEndOffsetY(0);
362
363 3
                    $spgrContainer->addChild($spContainer);
364 3
                    ++$iInc;
365
                }
366
            }
367
368
            // identifier clusters, used for workbook Escher object
369 14
            $this->IDCLs[$dgId] = $lastReducedSpId;
370
371
            // set last shape index
372 14
            $dgContainer->setLastSpId($lastSpId);
373
374
            // set the Escher object
375 14
            $this->writerWorksheets[$sheetIndex]->setEscher($escher);
376
        }
377
    }
378
379 3
    private function processMemoryDrawing(BstoreContainer &$bstoreContainer, MemoryDrawing $drawing, string $renderingFunctionx): void
380
    {
381
        switch ($renderingFunctionx) {
382 1
            case MemoryDrawing::RENDERING_JPEG:
383 3
                $blipType = BSE::BLIPTYPE_JPEG;
384 3
                $renderingFunction = 'imagejpeg';
385
386 3
                break;
387
            default:
388 3
                $blipType = BSE::BLIPTYPE_PNG;
389 3
                $renderingFunction = 'imagepng';
390
391 3
                break;
392
        }
393
394 3
        ob_start();
395 3
        call_user_func($renderingFunction, $drawing->getImageResource());
396 3
        $blipData = ob_get_contents();
397 3
        ob_end_clean();
398
399 3
        $blip = new Blip();
400 3
        $blip->setData("$blipData");
401
402 3
        $BSE = new BSE();
403 3
        $BSE->setBlipType($blipType);
404 3
        $BSE->setBlip($blip);
405
406 3
        $bstoreContainer->addBSE($BSE);
407
    }
408
409
    private static int $two = 2; // phpstan silliness
410
411 9
    private function processDrawing(BstoreContainer &$bstoreContainer, Drawing $drawing): void
412
    {
413 9
        $blipType = 0;
414 9
        $blipData = '';
415 9
        $filename = $drawing->getPath();
416
417 9
        $imageSize = getimagesize($filename);
418 9
        $imageFormat = empty($imageSize) ? 0 : ($imageSize[self::$two] ?? 0);
419
420
        switch ($imageFormat) {
421 9
            case 1: // GIF, not supported by BIFF8, we convert to PNG
422 2
                $blipType = BSE::BLIPTYPE_PNG;
423 2
                $newImage = @imagecreatefromgif($filename);
424 2
                if ($newImage === false) {
425
                    throw new Exception("Unable to create image from $filename");
426
                }
427 2
                ob_start();
428 2
                imagepng($newImage);
429 2
                $blipData = ob_get_contents();
430 2
                ob_end_clean();
431
432 2
                break;
433 8
            case 2: // JPEG
434 7
                $blipType = BSE::BLIPTYPE_JPEG;
435 7
                $blipData = file_get_contents($filename);
436
437 7
                break;
438 8
            case 3: // PNG
439 7
                $blipType = BSE::BLIPTYPE_PNG;
440 7
                $blipData = file_get_contents($filename);
441
442 7
                break;
443 2
            case 6: // Windows DIB (BMP), we convert to PNG
444 2
                $blipType = BSE::BLIPTYPE_PNG;
445 2
                $newImage = @imagecreatefrombmp($filename);
446 2
                if ($newImage === false) {
447
                    throw new Exception("Unable to create image from $filename");
448
                }
449 2
                ob_start();
450 2
                imagepng($newImage);
451 2
                $blipData = ob_get_contents();
452 2
                ob_end_clean();
453
454 2
                break;
455
        }
456 9
        if ($blipData) {
457 9
            $blip = new Blip();
458 9
            $blip->setData($blipData);
459
460 9
            $BSE = new BSE();
461 9
            $BSE->setBlipType($blipType);
462 9
            $BSE->setBlip($blip);
463
464 9
            $bstoreContainer->addBSE($BSE);
465
        }
466
    }
467
468 14
    private function processBaseDrawing(BstoreContainer &$bstoreContainer, BaseDrawing $drawing): void
469
    {
470 14
        if ($drawing instanceof Drawing) {
471 9
            $this->processDrawing($bstoreContainer, $drawing);
472 6
        } elseif ($drawing instanceof MemoryDrawing) {
473 3
            $this->processMemoryDrawing($bstoreContainer, $drawing, $drawing->getRenderingFunction());
474
        }
475
    }
476
477 101
    private function checkForDrawings(): bool
478
    {
479
        // any drawings in this workbook?
480 101
        $found = false;
481 101
        foreach ($this->spreadsheet->getAllSheets() as $sheet) {
482 101
            if (count($sheet->getDrawingCollection()) > 0) {
483 14
                $found = true;
484
485 14
                break;
486
            }
487
        }
488
489 101
        return $found;
490
    }
491
492
    /**
493
     * Build the Escher object corresponding to the MSODRAWINGGROUP record.
494
     */
495 101
    private function buildWorkbookEscher(): void
496
    {
497
        // nothing to do if there are no drawings
498 101
        if (!$this->checkForDrawings()) {
499 87
            return;
500
        }
501
502
        // if we reach here, then there are drawings in the workbook
503 14
        $escher = new Escher();
504
505
        // dggContainer
506 14
        $dggContainer = new DggContainer();
507 14
        $escher->setDggContainer($dggContainer);
508
509
        // set IDCLs (identifier clusters)
510 14
        $dggContainer->setIDCLs($this->IDCLs);
511
512
        // this loop is for determining maximum shape identifier of all drawing
513 14
        $spIdMax = 0;
514 14
        $totalCountShapes = 0;
515 14
        $countDrawings = 0;
516
517 14
        foreach ($this->spreadsheet->getAllsheets() as $sheet) {
518 14
            $sheetCountShapes = 0; // count number of shapes (minus group shape), in sheet
519
520 14
            $addCount = 0;
521 14
            foreach ($sheet->getDrawingCollection() as $drawing) {
522 14
                $addCount = 1;
523 14
                ++$sheetCountShapes;
524 14
                ++$totalCountShapes;
525
526 14
                $spId = $sheetCountShapes | ($this->spreadsheet->getIndex($sheet) + 1) << 10;
527 14
                $spIdMax = max($spId, $spIdMax);
528
            }
529 14
            $countDrawings += $addCount;
530
        }
531
532 14
        $dggContainer->setSpIdMax($spIdMax + 1);
533 14
        $dggContainer->setCDgSaved($countDrawings);
534 14
        $dggContainer->setCSpSaved($totalCountShapes + $countDrawings); // total number of shapes incl. one group shapes per drawing
535
536
        // bstoreContainer
537 14
        $bstoreContainer = new BstoreContainer();
538 14
        $dggContainer->setBstoreContainer($bstoreContainer);
539
540
        // the BSE's (all the images)
541 14
        foreach ($this->spreadsheet->getAllsheets() as $sheet) {
542 14
            foreach ($sheet->getDrawingCollection() as $drawing) {
543 14
                $this->processBaseDrawing($bstoreContainer, $drawing);
544
            }
545
        }
546
547
        // Set the Escher object
548 14
        $this->writerWorkbook->setEscher($escher);
549
    }
550
551
    /**
552
     * Build the OLE Part for DocumentSummary Information.
553
     */
554 100
    private function writeDocumentSummaryInformation(): string
555
    {
556
        // offset: 0; size: 2; must be 0xFE 0xFF (UTF-16 LE byte order mark)
557 100
        $data = pack('v', 0xFFFE);
558
        // offset: 2; size: 2;
559 100
        $data .= pack('v', 0x0000);
560
        // offset: 4; size: 2; OS version
561 100
        $data .= pack('v', 0x0106);
562
        // offset: 6; size: 2; OS indicator
563 100
        $data .= pack('v', 0x0002);
564
        // offset: 8; size: 16
565 100
        $data .= pack('VVVV', 0x00, 0x00, 0x00, 0x00);
566
        // offset: 24; size: 4; section count
567 100
        $data .= pack('V', 0x0001);
568
569
        // offset: 28; size: 16; first section's class id: 02 d5 cd d5 9c 2e 1b 10 93 97 08 00 2b 2c f9 ae
570 100
        $data .= pack('vvvvvvvv', 0xD502, 0xD5CD, 0x2E9C, 0x101B, 0x9793, 0x0008, 0x2C2B, 0xAEF9);
571
        // offset: 44; size: 4; offset of the start
572 100
        $data .= pack('V', 0x30);
573
574
        // SECTION
575 100
        $dataSection = [];
576 100
        $dataSection_NumProps = 0;
577 100
        $dataSection_Summary = '';
578 100
        $dataSection_Content = '';
579
580
        // GKPIDDSI_CODEPAGE: CodePage
581 100
        $dataSection[] = [
582 100
            'summary' => ['pack' => 'V', 'data' => 0x01],
583 100
            'offset' => ['pack' => 'V'],
584 100
            'type' => ['pack' => 'V', 'data' => 0x02], // 2 byte signed integer
585 100
            'data' => ['data' => 1252],
586 100
        ];
587 100
        ++$dataSection_NumProps;
588
589
        // GKPIDDSI_CATEGORY : Category
590 100
        $dataProp = $this->spreadsheet->getProperties()->getCategory();
591 100
        if ($dataProp) {
592 41
            $dataSection[] = [
593 41
                'summary' => ['pack' => 'V', 'data' => 0x02],
594 41
                'offset' => ['pack' => 'V'],
595 41
                'type' => ['pack' => 'V', 'data' => 0x1E],
596 41
                'data' => ['data' => $dataProp, 'length' => strlen($dataProp)],
597 41
            ];
598 41
            ++$dataSection_NumProps;
599
        }
600
        // GKPIDDSI_VERSION :Version of the application that wrote the property storage
601 100
        $dataSection[] = [
602 100
            'summary' => ['pack' => 'V', 'data' => 0x17],
603 100
            'offset' => ['pack' => 'V'],
604 100
            'type' => ['pack' => 'V', 'data' => 0x03],
605 100
            'data' => ['pack' => 'V', 'data' => 0x000C0000],
606 100
        ];
607 100
        ++$dataSection_NumProps;
608
        // GKPIDDSI_SCALE : FALSE
609 100
        $dataSection[] = [
610 100
            'summary' => ['pack' => 'V', 'data' => 0x0B],
611 100
            'offset' => ['pack' => 'V'],
612 100
            'type' => ['pack' => 'V', 'data' => 0x0B],
613 100
            'data' => ['data' => false],
614 100
        ];
615 100
        ++$dataSection_NumProps;
616
        // GKPIDDSI_LINKSDIRTY : True if any of the values for the linked properties have changed outside of the application
617 100
        $dataSection[] = [
618 100
            'summary' => ['pack' => 'V', 'data' => 0x10],
619 100
            'offset' => ['pack' => 'V'],
620 100
            'type' => ['pack' => 'V', 'data' => 0x0B],
621 100
            'data' => ['data' => false],
622 100
        ];
623 100
        ++$dataSection_NumProps;
624
        // GKPIDDSI_SHAREDOC : FALSE
625 100
        $dataSection[] = [
626 100
            'summary' => ['pack' => 'V', 'data' => 0x13],
627 100
            'offset' => ['pack' => 'V'],
628 100
            'type' => ['pack' => 'V', 'data' => 0x0B],
629 100
            'data' => ['data' => false],
630 100
        ];
631 100
        ++$dataSection_NumProps;
632
        // GKPIDDSI_HYPERLINKSCHANGED : True if any of the values for the _PID_LINKS (hyperlink text) have changed outside of the application
633 100
        $dataSection[] = [
634 100
            'summary' => ['pack' => 'V', 'data' => 0x16],
635 100
            'offset' => ['pack' => 'V'],
636 100
            'type' => ['pack' => 'V', 'data' => 0x0B],
637 100
            'data' => ['data' => false],
638 100
        ];
639 100
        ++$dataSection_NumProps;
640
641
        // GKPIDDSI_DOCSPARTS
642
        // MS-OSHARED p75 (2.3.3.2.2.1)
643
        // Structure is VtVecUnalignedLpstrValue (2.3.3.1.9)
644
        // cElements
645 100
        $dataProp = pack('v', 0x0001);
646 100
        $dataProp .= pack('v', 0x0000);
647
        // array of UnalignedLpstr
648
        // cch
649 100
        $dataProp .= pack('v', 0x000A);
650 100
        $dataProp .= pack('v', 0x0000);
651
        // value
652 100
        $dataProp .= 'Worksheet' . chr(0);
653
654 100
        $dataSection[] = [
655 100
            'summary' => ['pack' => 'V', 'data' => 0x0D],
656 100
            'offset' => ['pack' => 'V'],
657 100
            'type' => ['pack' => 'V', 'data' => 0x101E],
658 100
            'data' => ['data' => $dataProp, 'length' => strlen($dataProp)],
659 100
        ];
660 100
        ++$dataSection_NumProps;
661
662
        // GKPIDDSI_HEADINGPAIR
663
        // VtVecHeadingPairValue
664
        // cElements
665 100
        $dataProp = pack('v', 0x0002);
666 100
        $dataProp .= pack('v', 0x0000);
667
        // Array of vtHeadingPair
668
        // vtUnalignedString - headingString
669
        // stringType
670 100
        $dataProp .= pack('v', 0x001E);
671
        // padding
672 100
        $dataProp .= pack('v', 0x0000);
673
        // UnalignedLpstr
674
        // cch
675 100
        $dataProp .= pack('v', 0x0013);
676 100
        $dataProp .= pack('v', 0x0000);
677
        // value
678 100
        $dataProp .= 'Feuilles de calcul';
679
        // vtUnalignedString - headingParts
680
        // wType : 0x0003 = 32 bit signed integer
681 100
        $dataProp .= pack('v', 0x0300);
682
        // padding
683 100
        $dataProp .= pack('v', 0x0000);
684
        // value
685 100
        $dataProp .= pack('v', 0x0100);
686 100
        $dataProp .= pack('v', 0x0000);
687 100
        $dataProp .= pack('v', 0x0000);
688 100
        $dataProp .= pack('v', 0x0000);
689
690 100
        $dataSection[] = [
691 100
            'summary' => ['pack' => 'V', 'data' => 0x0C],
692 100
            'offset' => ['pack' => 'V'],
693 100
            'type' => ['pack' => 'V', 'data' => 0x100C],
694 100
            'data' => ['data' => $dataProp, 'length' => strlen($dataProp)],
695 100
        ];
696 100
        ++$dataSection_NumProps;
697
698
        //         4     Section Length
699
        //        4     Property count
700
        //        8 * $dataSection_NumProps (8 =  ID (4) + OffSet(4))
701 100
        $dataSection_Content_Offset = 8 + $dataSection_NumProps * 8;
702 100
        foreach ($dataSection as $dataProp) {
703
            // Summary
704 100
            $dataSection_Summary .= pack($dataProp['summary']['pack'], $dataProp['summary']['data']);
705
            // Offset
706 100
            $dataSection_Summary .= pack($dataProp['offset']['pack'], $dataSection_Content_Offset);
707
            // DataType
708 100
            $dataSection_Content .= pack($dataProp['type']['pack'], $dataProp['type']['data']);
709
            // Data
710 100
            if ($dataProp['type']['data'] == 0x02) { // 2 byte signed integer
711 100
                $dataSection_Content .= pack('V', $dataProp['data']['data']);
712
713 100
                $dataSection_Content_Offset += 4 + 4;
714 100
            } elseif ($dataProp['type']['data'] == 0x03) { // 4 byte signed integer
715 100
                $dataSection_Content .= pack('V', $dataProp['data']['data']);
716
717 100
                $dataSection_Content_Offset += 4 + 4;
718 100
            } elseif ($dataProp['type']['data'] == 0x0B) { // Boolean
719 100
                $dataSection_Content .= pack('V', (int) $dataProp['data']['data']);
720 100
                $dataSection_Content_Offset += 4 + 4;
721 100
            } elseif ($dataProp['type']['data'] == 0x1E) { // null-terminated string prepended by dword string length
722
                // Null-terminated string
723 41
                $dataProp['data']['data'] .= chr(0);
724 41
                ++$dataProp['data']['length'];
725
                // Complete the string with null string for being a %4
726 41
                $dataProp['data']['length'] = $dataProp['data']['length'] + ((4 - $dataProp['data']['length'] % 4) == 4 ? 0 : (4 - $dataProp['data']['length'] % 4));
727 41
                $dataProp['data']['data'] = str_pad($dataProp['data']['data'], $dataProp['data']['length'], chr(0), STR_PAD_RIGHT);
728
729 41
                $dataSection_Content .= pack('V', $dataProp['data']['length']);
730 41
                $dataSection_Content .= $dataProp['data']['data'];
731
732 41
                $dataSection_Content_Offset += 4 + 4 + strlen($dataProp['data']['data']);
733
            } else {
734 100
                $dataSection_Content .= $dataProp['data']['data'];
735
736 100
                $dataSection_Content_Offset += 4 + $dataProp['data']['length'];
737
            }
738
        }
739
        // Now $dataSection_Content_Offset contains the size of the content
740
741
        // section header
742
        // offset: $secOffset; size: 4; section length
743
        //         + x  Size of the content (summary + content)
744 100
        $data .= pack('V', $dataSection_Content_Offset);
745
        // offset: $secOffset+4; size: 4; property count
746 100
        $data .= pack('V', $dataSection_NumProps);
747
        // Section Summary
748 100
        $data .= $dataSection_Summary;
749
        // Section Content
750 100
        $data .= $dataSection_Content;
751
752 100
        return $data;
753
    }
754
755 100
    private function writeSummaryPropOle(float|int $dataProp, int &$dataSection_NumProps, array &$dataSection, int $sumdata, int $typdata): void
756
    {
757 100
        if ($dataProp) {
758 100
            $dataSection[] = [
759 100
                'summary' => ['pack' => 'V', 'data' => $sumdata],
760 100
                'offset' => ['pack' => 'V'],
761 100
                'type' => ['pack' => 'V', 'data' => $typdata], // null-terminated string prepended by dword string length
762 100
                'data' => ['data' => OLE::localDateToOLE($dataProp)],
763 100
            ];
764 100
            ++$dataSection_NumProps;
765
        }
766
    }
767
768 100
    private function writeSummaryProp(string $dataProp, int &$dataSection_NumProps, array &$dataSection, int $sumdata, int $typdata): void
769
    {
770 100
        if ($dataProp) {
771 100
            $dataSection[] = [
772 100
                'summary' => ['pack' => 'V', 'data' => $sumdata],
773 100
                'offset' => ['pack' => 'V'],
774 100
                'type' => ['pack' => 'V', 'data' => $typdata], // null-terminated string prepended by dword string length
775 100
                'data' => ['data' => $dataProp, 'length' => strlen($dataProp)],
776 100
            ];
777 100
            ++$dataSection_NumProps;
778
        }
779
    }
780
781
    /**
782
     * Build the OLE Part for Summary Information.
783
     */
784 100
    private function writeSummaryInformation(): string
785
    {
786
        // offset: 0; size: 2; must be 0xFE 0xFF (UTF-16 LE byte order mark)
787 100
        $data = pack('v', 0xFFFE);
788
        // offset: 2; size: 2;
789 100
        $data .= pack('v', 0x0000);
790
        // offset: 4; size: 2; OS version
791 100
        $data .= pack('v', 0x0106);
792
        // offset: 6; size: 2; OS indicator
793 100
        $data .= pack('v', 0x0002);
794
        // offset: 8; size: 16
795 100
        $data .= pack('VVVV', 0x00, 0x00, 0x00, 0x00);
796
        // offset: 24; size: 4; section count
797 100
        $data .= pack('V', 0x0001);
798
799
        // offset: 28; size: 16; first section's class id: e0 85 9f f2 f9 4f 68 10 ab 91 08 00 2b 27 b3 d9
800 100
        $data .= pack('vvvvvvvv', 0x85E0, 0xF29F, 0x4FF9, 0x1068, 0x91AB, 0x0008, 0x272B, 0xD9B3);
801
        // offset: 44; size: 4; offset of the start
802 100
        $data .= pack('V', 0x30);
803
804
        // SECTION
805 100
        $dataSection = [];
806 100
        $dataSection_NumProps = 0;
807 100
        $dataSection_Summary = '';
808 100
        $dataSection_Content = '';
809
810
        // CodePage : CP-1252
811 100
        $dataSection[] = [
812 100
            'summary' => ['pack' => 'V', 'data' => 0x01],
813 100
            'offset' => ['pack' => 'V'],
814 100
            'type' => ['pack' => 'V', 'data' => 0x02], // 2 byte signed integer
815 100
            'data' => ['data' => 1252],
816 100
        ];
817 100
        ++$dataSection_NumProps;
818
819 100
        $props = $this->spreadsheet->getProperties();
820 100
        $this->writeSummaryProp($props->getTitle(), $dataSection_NumProps, $dataSection, 0x02, 0x1E);
821 100
        $this->writeSummaryProp($props->getSubject(), $dataSection_NumProps, $dataSection, 0x03, 0x1E);
822 100
        $this->writeSummaryProp($props->getCreator(), $dataSection_NumProps, $dataSection, 0x04, 0x1E);
823 100
        $this->writeSummaryProp($props->getKeywords(), $dataSection_NumProps, $dataSection, 0x05, 0x1E);
824 100
        $this->writeSummaryProp($props->getDescription(), $dataSection_NumProps, $dataSection, 0x06, 0x1E);
825 100
        $this->writeSummaryProp($props->getLastModifiedBy(), $dataSection_NumProps, $dataSection, 0x08, 0x1E);
826 100
        $this->writeSummaryPropOle($props->getCreated(), $dataSection_NumProps, $dataSection, 0x0C, 0x40);
827 100
        $this->writeSummaryPropOle($props->getModified(), $dataSection_NumProps, $dataSection, 0x0D, 0x40);
828
829
        //    Security
830 100
        $dataSection[] = [
831 100
            'summary' => ['pack' => 'V', 'data' => 0x13],
832 100
            'offset' => ['pack' => 'V'],
833 100
            'type' => ['pack' => 'V', 'data' => 0x03], // 4 byte signed integer
834 100
            'data' => ['data' => 0x00],
835 100
        ];
836 100
        ++$dataSection_NumProps;
837
838
        //         4     Section Length
839
        //        4     Property count
840
        //        8 * $dataSection_NumProps (8 =  ID (4) + OffSet(4))
841 100
        $dataSection_Content_Offset = 8 + $dataSection_NumProps * 8;
842 100
        foreach ($dataSection as $dataProp) {
843
            // Summary
844 100
            $dataSection_Summary .= pack($dataProp['summary']['pack'], $dataProp['summary']['data']);
845
            // Offset
846 100
            $dataSection_Summary .= pack($dataProp['offset']['pack'], $dataSection_Content_Offset);
847
            // DataType
848 100
            $dataSection_Content .= pack($dataProp['type']['pack'], $dataProp['type']['data']);
849
            // Data
850 100
            if ($dataProp['type']['data'] == 0x02) { // 2 byte signed integer
851 100
                $dataSection_Content .= pack('V', $dataProp['data']['data']);
852
853 100
                $dataSection_Content_Offset += 4 + 4;
854 100
            } elseif ($dataProp['type']['data'] == 0x03) { // 4 byte signed integer
855 100
                $dataSection_Content .= pack('V', $dataProp['data']['data']);
856
857 100
                $dataSection_Content_Offset += 4 + 4;
858 100
            } elseif ($dataProp['type']['data'] == 0x1E) { // null-terminated string prepended by dword string length
859
                // Null-terminated string
860 100
                $dataProp['data']['data'] .= chr(0);
861 100
                ++$dataProp['data']['length'];
862
                // Complete the string with null string for being a %4
863 100
                $dataProp['data']['length'] = $dataProp['data']['length'] + ((4 - $dataProp['data']['length'] % 4) == 4 ? 0 : (4 - $dataProp['data']['length'] % 4));
864 100
                $dataProp['data']['data'] = str_pad($dataProp['data']['data'], $dataProp['data']['length'], chr(0), STR_PAD_RIGHT);
865
866 100
                $dataSection_Content .= pack('V', $dataProp['data']['length']);
867 100
                $dataSection_Content .= $dataProp['data']['data'];
868
869 100
                $dataSection_Content_Offset += 4 + 4 + strlen($dataProp['data']['data']);
870 100
            } elseif ($dataProp['type']['data'] == 0x40) { // Filetime (64-bit value representing the number of 100-nanosecond intervals since January 1, 1601)
871 100
                $dataSection_Content .= $dataProp['data']['data'];
872
873 100
                $dataSection_Content_Offset += 4 + 8;
874
            }
875
            // Data Type Not Used at the moment
876
        }
877
        // Now $dataSection_Content_Offset contains the size of the content
878
879
        // section header
880
        // offset: $secOffset; size: 4; section length
881
        //         + x  Size of the content (summary + content)
882 100
        $data .= pack('V', $dataSection_Content_Offset);
883
        // offset: $secOffset+4; size: 4; property count
884 100
        $data .= pack('V', $dataSection_NumProps);
885
        // Section Summary
886 100
        $data .= $dataSection_Summary;
887
        // Section Content
888 100
        $data .= $dataSection_Content;
889
890 100
        return $data;
891
    }
892
}
893