PluginProbe
Visualizer – Tables & Charts Manager with Built-in AI Generator / 3.7.1
Visualizer – Tables & Charts Manager with Built-in AI Generator v3.7.1
4.0.8 4.0.7 4.0.6 4.0.5 4.0.4 4.0.3 3.0.5 3.0.6 3.0.7 3.0.8 3.0.9 3.1.0 3.1.1 3.1.2 3.1.3 3.10.0 3.10.1 3.10.10 3.10.11 3.10.12 3.10.13 3.10.14 3.10.15 3.10.2 3.10.3 All 149 releases
visualizer / vendor / phpoffice / phpspreadsheet / src / PhpSpreadsheet / Writer / Xls.php

Xls.php in Visualizer – Tables & Charts Manager with Built-in AI Generator 3.7.1, at vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Writer/Xls.php

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