Xls.php 37 KB

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