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