Workbook.php 39 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153
  1. <?php
  2. namespace PhpOffice\PhpSpreadsheet\Writer\Xls;
  3. require_once DOL_DOCUMENT_ROOT . '/includes/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Shared/Date.php';
  4. use PhpOffice\PhpSpreadsheet\Cell\Coordinate;
  5. use PhpOffice\PhpSpreadsheet\Exception as PhpSpreadsheetException;
  6. use PhpOffice\PhpSpreadsheet\Shared\Date;
  7. use PhpOffice\PhpSpreadsheet\Shared\StringHelper;
  8. use PhpOffice\PhpSpreadsheet\Spreadsheet;
  9. use PhpOffice\PhpSpreadsheet\Style\Style;
  10. // Original file header of PEAR::Spreadsheet_Excel_Writer_Workbook (used as the base for this class):
  11. // -----------------------------------------------------------------------------------------
  12. // /*
  13. // * Module written/ported by Xavier Noguer <xnoguer@rezebra.com>
  14. // *
  15. // * The majority of this is _NOT_ my code. I simply ported it from the
  16. // * PERL Spreadsheet::WriteExcel module.
  17. // *
  18. // * The author of the Spreadsheet::WriteExcel module is John McNamara
  19. // * <jmcnamara@cpan.org>
  20. // *
  21. // * I _DO_ maintain this code, and John McNamara has nothing to do with the
  22. // * porting of this code to PHP. Any questions directly related to this
  23. // * class library should be directed to me.
  24. // *
  25. // * License Information:
  26. // *
  27. // * Spreadsheet_Excel_Writer: A library for generating Excel Spreadsheets
  28. // * Copyright (c) 2002-2003 Xavier Noguer xnoguer@rezebra.com
  29. // *
  30. // * This library is free software; you can redistribute it and/or
  31. // * modify it under the terms of the GNU Lesser General Public
  32. // * License as published by the Free Software Foundation; either
  33. // * version 2.1 of the License, or (at your option) any later version.
  34. // *
  35. // * This library is distributed in the hope that it will be useful,
  36. // * but WITHOUT ANY WARRANTY; without even the implied warranty of
  37. // * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
  38. // * Lesser General Public License for more details.
  39. // *
  40. // * You should have received a copy of the GNU Lesser General Public
  41. // * License along with this library; if not, write to the Free Software
  42. // * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
  43. // */
  44. class Workbook extends BIFFwriter
  45. {
  46. /**
  47. * Formula parser.
  48. *
  49. * @var \PhpOffice\PhpSpreadsheet\Writer\Xls\Parser
  50. */
  51. private $parser;
  52. /**
  53. * The BIFF file size for the workbook.
  54. *
  55. * @var int
  56. *
  57. * @see calcSheetOffsets()
  58. */
  59. private $biffSize;
  60. /**
  61. * XF Writers.
  62. *
  63. * @var \PhpOffice\PhpSpreadsheet\Writer\Xls\Xf[]
  64. */
  65. private $xfWriters = [];
  66. /**
  67. * Array containing the colour palette.
  68. *
  69. * @var array
  70. */
  71. private $palette;
  72. /**
  73. * The codepage indicates the text encoding used for strings.
  74. *
  75. * @var int
  76. */
  77. private $codepage;
  78. /**
  79. * The country code used for localization.
  80. *
  81. * @var int
  82. */
  83. private $countryCode;
  84. /**
  85. * Workbook.
  86. *
  87. * @var Spreadsheet
  88. */
  89. private $spreadsheet;
  90. /**
  91. * Fonts writers.
  92. *
  93. * @var Font[]
  94. */
  95. private $fontWriters = [];
  96. /**
  97. * Added fonts. Maps from font's hash => index in workbook.
  98. *
  99. * @var array
  100. */
  101. private $addedFonts = [];
  102. /**
  103. * Shared number formats.
  104. *
  105. * @var array
  106. */
  107. private $numberFormats = [];
  108. /**
  109. * Added number formats. Maps from numberFormat's hash => index in workbook.
  110. *
  111. * @var array
  112. */
  113. private $addedNumberFormats = [];
  114. /**
  115. * Sizes of the binary worksheet streams.
  116. *
  117. * @var array
  118. */
  119. private $worksheetSizes = [];
  120. /**
  121. * Offsets of the binary worksheet streams relative to the start of the global workbook stream.
  122. *
  123. * @var array
  124. */
  125. private $worksheetOffsets = [];
  126. /**
  127. * Total number of shared strings in workbook.
  128. *
  129. * @var int
  130. */
  131. private $stringTotal;
  132. /**
  133. * Number of unique shared strings in workbook.
  134. *
  135. * @var int
  136. */
  137. private $stringUnique;
  138. /**
  139. * Array of unique shared strings in workbook.
  140. *
  141. * @var array
  142. */
  143. private $stringTable;
  144. /**
  145. * Color cache.
  146. */
  147. private $colors;
  148. /**
  149. * Escher object corresponding to MSODRAWINGGROUP.
  150. *
  151. * @var \PhpOffice\PhpSpreadsheet\Shared\Escher
  152. */
  153. private $escher;
  154. /**
  155. * Class constructor.
  156. *
  157. * @param Spreadsheet $spreadsheet The Workbook
  158. * @param int $str_total Total number of strings
  159. * @param int $str_unique Total number of unique strings
  160. * @param array $str_table String Table
  161. * @param array $colors Colour Table
  162. * @param Parser $parser The formula parser created for the Workbook
  163. */
  164. public function __construct(Spreadsheet $spreadsheet, &$str_total, &$str_unique, &$str_table, &$colors, Parser $parser)
  165. {
  166. // It needs to call its parent's constructor explicitly
  167. parent::__construct();
  168. $this->parser = $parser;
  169. $this->biffSize = 0;
  170. $this->palette = [];
  171. $this->countryCode = -1;
  172. $this->stringTotal = &$str_total;
  173. $this->stringUnique = &$str_unique;
  174. $this->stringTable = &$str_table;
  175. $this->colors = &$colors;
  176. $this->setPaletteXl97();
  177. $this->spreadsheet = $spreadsheet;
  178. $this->codepage = 0x04B0;
  179. // Add empty sheets and Build color cache
  180. $countSheets = $spreadsheet->getSheetCount();
  181. for ($i = 0; $i < $countSheets; ++$i) {
  182. $phpSheet = $spreadsheet->getSheet($i);
  183. $this->parser->setExtSheet($phpSheet->getTitle(), $i); // Register worksheet name with parser
  184. $supbook_index = 0x00;
  185. $ref = pack('vvv', $supbook_index, $i, $i);
  186. $this->parser->references[] = $ref; // Register reference with parser
  187. // Sheet tab colors?
  188. if ($phpSheet->isTabColorSet()) {
  189. $this->addColor($phpSheet->getTabColor()->getRGB());
  190. }
  191. }
  192. }
  193. /**
  194. * Add a new XF writer.
  195. *
  196. * @param Style $style
  197. * @param bool $isStyleXf Is it a style XF?
  198. *
  199. * @return int Index to XF record
  200. */
  201. public function addXfWriter(Style $style, $isStyleXf = false)
  202. {
  203. $xfWriter = new Xf($style);
  204. $xfWriter->setIsStyleXf($isStyleXf);
  205. // Add the font if not already added
  206. $fontIndex = $this->addFont($style->getFont());
  207. // Assign the font index to the xf record
  208. $xfWriter->setFontIndex($fontIndex);
  209. // Background colors, best to treat these after the font so black will come after white in custom palette
  210. $xfWriter->setFgColor($this->addColor($style->getFill()->getStartColor()->getRGB()));
  211. $xfWriter->setBgColor($this->addColor($style->getFill()->getEndColor()->getRGB()));
  212. $xfWriter->setBottomColor($this->addColor($style->getBorders()->getBottom()->getColor()->getRGB()));
  213. $xfWriter->setTopColor($this->addColor($style->getBorders()->getTop()->getColor()->getRGB()));
  214. $xfWriter->setRightColor($this->addColor($style->getBorders()->getRight()->getColor()->getRGB()));
  215. $xfWriter->setLeftColor($this->addColor($style->getBorders()->getLeft()->getColor()->getRGB()));
  216. $xfWriter->setDiagColor($this->addColor($style->getBorders()->getDiagonal()->getColor()->getRGB()));
  217. // Add the number format if it is not a built-in one and not already added
  218. if ($style->getNumberFormat()->getBuiltInFormatCode() === false) {
  219. $numberFormatHashCode = $style->getNumberFormat()->getHashCode();
  220. if (isset($this->addedNumberFormats[$numberFormatHashCode])) {
  221. $numberFormatIndex = $this->addedNumberFormats[$numberFormatHashCode];
  222. } else {
  223. $numberFormatIndex = 164 + count($this->numberFormats);
  224. $this->numberFormats[$numberFormatIndex] = $style->getNumberFormat();
  225. $this->addedNumberFormats[$numberFormatHashCode] = $numberFormatIndex;
  226. }
  227. } else {
  228. $numberFormatIndex = (int) $style->getNumberFormat()->getBuiltInFormatCode();
  229. }
  230. // Assign the number format index to xf record
  231. $xfWriter->setNumberFormatIndex($numberFormatIndex);
  232. $this->xfWriters[] = $xfWriter;
  233. return count($this->xfWriters) - 1;
  234. }
  235. /**
  236. * Add a font to added fonts.
  237. *
  238. * @param \PhpOffice\PhpSpreadsheet\Style\Font $font
  239. *
  240. * @return int Index to FONT record
  241. */
  242. public function addFont(\PhpOffice\PhpSpreadsheet\Style\Font $font)
  243. {
  244. $fontHashCode = $font->getHashCode();
  245. if (isset($this->addedFonts[$fontHashCode])) {
  246. $fontIndex = $this->addedFonts[$fontHashCode];
  247. } else {
  248. $countFonts = count($this->fontWriters);
  249. $fontIndex = ($countFonts < 4) ? $countFonts : $countFonts + 1;
  250. $fontWriter = new Font($font);
  251. $fontWriter->setColorIndex($this->addColor($font->getColor()->getRGB()));
  252. $this->fontWriters[] = $fontWriter;
  253. $this->addedFonts[$fontHashCode] = $fontIndex;
  254. }
  255. return $fontIndex;
  256. }
  257. /**
  258. * Alter color palette adding a custom color.
  259. *
  260. * @param string $rgb E.g. 'FF00AA'
  261. *
  262. * @return int Color index
  263. */
  264. private function addColor($rgb)
  265. {
  266. if (!isset($this->colors[$rgb])) {
  267. $color =
  268. [
  269. hexdec(substr($rgb, 0, 2)),
  270. hexdec(substr($rgb, 2, 2)),
  271. hexdec(substr($rgb, 4)),
  272. 0,
  273. ];
  274. $colorIndex = array_search($color, $this->palette);
  275. if ($colorIndex) {
  276. $this->colors[$rgb] = $colorIndex;
  277. } else {
  278. if (count($this->colors) === 0) {
  279. $lastColor = 7;
  280. } else {
  281. $lastColor = end($this->colors);
  282. }
  283. if ($lastColor < 57) {
  284. // then we add a custom color altering the palette
  285. $colorIndex = $lastColor + 1;
  286. $this->palette[$colorIndex] = $color;
  287. $this->colors[$rgb] = $colorIndex;
  288. } else {
  289. // no room for more custom colors, just map to black
  290. $colorIndex = 0;
  291. }
  292. }
  293. } else {
  294. // fetch already added custom color
  295. $colorIndex = $this->colors[$rgb];
  296. }
  297. return $colorIndex;
  298. }
  299. /**
  300. * Sets the colour palette to the Excel 97+ default.
  301. */
  302. private function setPaletteXl97()
  303. {
  304. $this->palette = [
  305. 0x08 => [0x00, 0x00, 0x00, 0x00],
  306. 0x09 => [0xff, 0xff, 0xff, 0x00],
  307. 0x0A => [0xff, 0x00, 0x00, 0x00],
  308. 0x0B => [0x00, 0xff, 0x00, 0x00],
  309. 0x0C => [0x00, 0x00, 0xff, 0x00],
  310. 0x0D => [0xff, 0xff, 0x00, 0x00],
  311. 0x0E => [0xff, 0x00, 0xff, 0x00],
  312. 0x0F => [0x00, 0xff, 0xff, 0x00],
  313. 0x10 => [0x80, 0x00, 0x00, 0x00],
  314. 0x11 => [0x00, 0x80, 0x00, 0x00],
  315. 0x12 => [0x00, 0x00, 0x80, 0x00],
  316. 0x13 => [0x80, 0x80, 0x00, 0x00],
  317. 0x14 => [0x80, 0x00, 0x80, 0x00],
  318. 0x15 => [0x00, 0x80, 0x80, 0x00],
  319. 0x16 => [0xc0, 0xc0, 0xc0, 0x00],
  320. 0x17 => [0x80, 0x80, 0x80, 0x00],
  321. 0x18 => [0x99, 0x99, 0xff, 0x00],
  322. 0x19 => [0x99, 0x33, 0x66, 0x00],
  323. 0x1A => [0xff, 0xff, 0xcc, 0x00],
  324. 0x1B => [0xcc, 0xff, 0xff, 0x00],
  325. 0x1C => [0x66, 0x00, 0x66, 0x00],
  326. 0x1D => [0xff, 0x80, 0x80, 0x00],
  327. 0x1E => [0x00, 0x66, 0xcc, 0x00],
  328. 0x1F => [0xcc, 0xcc, 0xff, 0x00],
  329. 0x20 => [0x00, 0x00, 0x80, 0x00],
  330. 0x21 => [0xff, 0x00, 0xff, 0x00],
  331. 0x22 => [0xff, 0xff, 0x00, 0x00],
  332. 0x23 => [0x00, 0xff, 0xff, 0x00],
  333. 0x24 => [0x80, 0x00, 0x80, 0x00],
  334. 0x25 => [0x80, 0x00, 0x00, 0x00],
  335. 0x26 => [0x00, 0x80, 0x80, 0x00],
  336. 0x27 => [0x00, 0x00, 0xff, 0x00],
  337. 0x28 => [0x00, 0xcc, 0xff, 0x00],
  338. 0x29 => [0xcc, 0xff, 0xff, 0x00],
  339. 0x2A => [0xcc, 0xff, 0xcc, 0x00],
  340. 0x2B => [0xff, 0xff, 0x99, 0x00],
  341. 0x2C => [0x99, 0xcc, 0xff, 0x00],
  342. 0x2D => [0xff, 0x99, 0xcc, 0x00],
  343. 0x2E => [0xcc, 0x99, 0xff, 0x00],
  344. 0x2F => [0xff, 0xcc, 0x99, 0x00],
  345. 0x30 => [0x33, 0x66, 0xff, 0x00],
  346. 0x31 => [0x33, 0xcc, 0xcc, 0x00],
  347. 0x32 => [0x99, 0xcc, 0x00, 0x00],
  348. 0x33 => [0xff, 0xcc, 0x00, 0x00],
  349. 0x34 => [0xff, 0x99, 0x00, 0x00],
  350. 0x35 => [0xff, 0x66, 0x00, 0x00],
  351. 0x36 => [0x66, 0x66, 0x99, 0x00],
  352. 0x37 => [0x96, 0x96, 0x96, 0x00],
  353. 0x38 => [0x00, 0x33, 0x66, 0x00],
  354. 0x39 => [0x33, 0x99, 0x66, 0x00],
  355. 0x3A => [0x00, 0x33, 0x00, 0x00],
  356. 0x3B => [0x33, 0x33, 0x00, 0x00],
  357. 0x3C => [0x99, 0x33, 0x00, 0x00],
  358. 0x3D => [0x99, 0x33, 0x66, 0x00],
  359. 0x3E => [0x33, 0x33, 0x99, 0x00],
  360. 0x3F => [0x33, 0x33, 0x33, 0x00],
  361. ];
  362. }
  363. /**
  364. * Assemble worksheets into a workbook and send the BIFF data to an OLE
  365. * storage.
  366. *
  367. * @param array $pWorksheetSizes The sizes in bytes of the binary worksheet streams
  368. *
  369. * @return string Binary data for workbook stream
  370. */
  371. public function writeWorkbook(array $pWorksheetSizes)
  372. {
  373. $this->worksheetSizes = $pWorksheetSizes;
  374. // Calculate the number of selected worksheet tabs and call the finalization
  375. // methods for each worksheet
  376. $total_worksheets = $this->spreadsheet->getSheetCount();
  377. // Add part 1 of the Workbook globals, what goes before the SHEET records
  378. $this->storeBof(0x0005);
  379. $this->writeCodepage();
  380. $this->writeWindow1();
  381. $this->writeDateMode();
  382. $this->writeAllFonts();
  383. $this->writeAllNumberFormats();
  384. $this->writeAllXfs();
  385. $this->writeAllStyles();
  386. $this->writePalette();
  387. // Prepare part 3 of the workbook global stream, what goes after the SHEET records
  388. $part3 = '';
  389. if ($this->countryCode !== -1) {
  390. $part3 .= $this->writeCountry();
  391. }
  392. $part3 .= $this->writeRecalcId();
  393. $part3 .= $this->writeSupbookInternal();
  394. /* TODO: store external SUPBOOK records and XCT and CRN records
  395. in case of external references for BIFF8 */
  396. $part3 .= $this->writeExternalsheetBiff8();
  397. $part3 .= $this->writeAllDefinedNamesBiff8();
  398. $part3 .= $this->writeMsoDrawingGroup();
  399. $part3 .= $this->writeSharedStringsTable();
  400. $part3 .= $this->writeEof();
  401. // Add part 2 of the Workbook globals, the SHEET records
  402. $this->calcSheetOffsets();
  403. for ($i = 0; $i < $total_worksheets; ++$i) {
  404. $this->writeBoundSheet($this->spreadsheet->getSheet($i), $this->worksheetOffsets[$i]);
  405. }
  406. // Add part 3 of the Workbook globals
  407. $this->_data .= $part3;
  408. return $this->_data;
  409. }
  410. /**
  411. * Calculate offsets for Worksheet BOF records.
  412. */
  413. private function calcSheetOffsets()
  414. {
  415. $boundsheet_length = 10; // fixed length for a BOUNDSHEET record
  416. // size of Workbook globals part 1 + 3
  417. $offset = $this->_datasize;
  418. // add size of Workbook globals part 2, the length of the SHEET records
  419. $total_worksheets = count($this->spreadsheet->getAllSheets());
  420. foreach ($this->spreadsheet->getWorksheetIterator() as $sheet) {
  421. $offset += $boundsheet_length + strlen(StringHelper::UTF8toBIFF8UnicodeShort($sheet->getTitle()));
  422. }
  423. // add the sizes of each of the Sheet substreams, respectively
  424. for ($i = 0; $i < $total_worksheets; ++$i) {
  425. $this->worksheetOffsets[$i] = $offset;
  426. $offset += $this->worksheetSizes[$i];
  427. }
  428. $this->biffSize = $offset;
  429. }
  430. /**
  431. * Store the Excel FONT records.
  432. */
  433. private function writeAllFonts()
  434. {
  435. foreach ($this->fontWriters as $fontWriter) {
  436. $this->append($fontWriter->writeFont());
  437. }
  438. }
  439. /**
  440. * Store user defined numerical formats i.e. FORMAT records.
  441. */
  442. private function writeAllNumberFormats()
  443. {
  444. foreach ($this->numberFormats as $numberFormatIndex => $numberFormat) {
  445. $this->writeNumberFormat($numberFormat->getFormatCode(), $numberFormatIndex);
  446. }
  447. }
  448. /**
  449. * Write all XF records.
  450. */
  451. private function writeAllXfs()
  452. {
  453. foreach ($this->xfWriters as $xfWriter) {
  454. $this->append($xfWriter->writeXf());
  455. }
  456. }
  457. /**
  458. * Write all STYLE records.
  459. */
  460. private function writeAllStyles()
  461. {
  462. $this->writeStyle();
  463. }
  464. /**
  465. * Writes all the DEFINEDNAME records (BIFF8).
  466. * So far this is only used for repeating rows/columns (print titles) and print areas.
  467. */
  468. private function writeAllDefinedNamesBiff8()
  469. {
  470. $chunk = '';
  471. // Named ranges
  472. if (count($this->spreadsheet->getNamedRanges()) > 0) {
  473. // Loop named ranges
  474. $namedRanges = $this->spreadsheet->getNamedRanges();
  475. foreach ($namedRanges as $namedRange) {
  476. // Create absolute coordinate
  477. $range = Coordinate::splitRange($namedRange->getRange());
  478. $iMax = count($range);
  479. for ($i = 0; $i < $iMax; ++$i) {
  480. $range[$i][0] = '\'' . str_replace("'", "''", $namedRange->getWorksheet()->getTitle()) . '\'!' . Coordinate::absoluteCoordinate($range[$i][0]);
  481. if (isset($range[$i][1])) {
  482. $range[$i][1] = Coordinate::absoluteCoordinate($range[$i][1]);
  483. }
  484. }
  485. $range = Coordinate::buildRange($range); // e.g. Sheet1!$A$1:$B$2
  486. // parse formula
  487. try {
  488. $error = $this->parser->parse($range);
  489. $formulaData = $this->parser->toReversePolish();
  490. // make sure tRef3d is of type tRef3dR (0x3A)
  491. if (isset($formulaData[0]) and ($formulaData[0] == "\x7A" or $formulaData[0] == "\x5A")) {
  492. $formulaData = "\x3A" . substr($formulaData, 1);
  493. }
  494. if ($namedRange->getLocalOnly()) {
  495. // local scope
  496. $scope = $this->spreadsheet->getIndex($namedRange->getScope()) + 1;
  497. } else {
  498. // global scope
  499. $scope = 0;
  500. }
  501. $chunk .= $this->writeData($this->writeDefinedNameBiff8($namedRange->getName(), $formulaData, $scope, false));
  502. } catch (PhpSpreadsheetException $e) {
  503. // do nothing
  504. }
  505. }
  506. }
  507. // total number of sheets
  508. $total_worksheets = $this->spreadsheet->getSheetCount();
  509. // write the print titles (repeating rows, columns), if any
  510. for ($i = 0; $i < $total_worksheets; ++$i) {
  511. $sheetSetup = $this->spreadsheet->getSheet($i)->getPageSetup();
  512. // simultaneous repeatColumns repeatRows
  513. if ($sheetSetup->isColumnsToRepeatAtLeftSet() && $sheetSetup->isRowsToRepeatAtTopSet()) {
  514. $repeat = $sheetSetup->getColumnsToRepeatAtLeft();
  515. $colmin = Coordinate::columnIndexFromString($repeat[0]) - 1;
  516. $colmax = Coordinate::columnIndexFromString($repeat[1]) - 1;
  517. $repeat = $sheetSetup->getRowsToRepeatAtTop();
  518. $rowmin = $repeat[0] - 1;
  519. $rowmax = $repeat[1] - 1;
  520. // construct formula data manually
  521. $formulaData = pack('Cv', 0x29, 0x17); // tMemFunc
  522. $formulaData .= pack('Cvvvvv', 0x3B, $i, 0, 65535, $colmin, $colmax); // tArea3d
  523. $formulaData .= pack('Cvvvvv', 0x3B, $i, $rowmin, $rowmax, 0, 255); // tArea3d
  524. $formulaData .= pack('C', 0x10); // tList
  525. // store the DEFINEDNAME record
  526. $chunk .= $this->writeData($this->writeDefinedNameBiff8(pack('C', 0x07), $formulaData, $i + 1, true));
  527. // (exclusive) either repeatColumns or repeatRows
  528. } elseif ($sheetSetup->isColumnsToRepeatAtLeftSet() || $sheetSetup->isRowsToRepeatAtTopSet()) {
  529. // Columns to repeat
  530. if ($sheetSetup->isColumnsToRepeatAtLeftSet()) {
  531. $repeat = $sheetSetup->getColumnsToRepeatAtLeft();
  532. $colmin = Coordinate::columnIndexFromString($repeat[0]) - 1;
  533. $colmax = Coordinate::columnIndexFromString($repeat[1]) - 1;
  534. } else {
  535. $colmin = 0;
  536. $colmax = 255;
  537. }
  538. // Rows to repeat
  539. if ($sheetSetup->isRowsToRepeatAtTopSet()) {
  540. $repeat = $sheetSetup->getRowsToRepeatAtTop();
  541. $rowmin = $repeat[0] - 1;
  542. $rowmax = $repeat[1] - 1;
  543. } else {
  544. $rowmin = 0;
  545. $rowmax = 65535;
  546. }
  547. // construct formula data manually because parser does not recognize absolute 3d cell references
  548. $formulaData = pack('Cvvvvv', 0x3B, $i, $rowmin, $rowmax, $colmin, $colmax);
  549. // store the DEFINEDNAME record
  550. $chunk .= $this->writeData($this->writeDefinedNameBiff8(pack('C', 0x07), $formulaData, $i + 1, true));
  551. }
  552. }
  553. // write the print areas, if any
  554. for ($i = 0; $i < $total_worksheets; ++$i) {
  555. $sheetSetup = $this->spreadsheet->getSheet($i)->getPageSetup();
  556. if ($sheetSetup->isPrintAreaSet()) {
  557. // Print area, e.g. A3:J6,H1:X20
  558. $printArea = Coordinate::splitRange($sheetSetup->getPrintArea());
  559. $countPrintArea = count($printArea);
  560. $formulaData = '';
  561. for ($j = 0; $j < $countPrintArea; ++$j) {
  562. $printAreaRect = $printArea[$j]; // e.g. A3:J6
  563. $printAreaRect[0] = Coordinate::coordinateFromString($printAreaRect[0]);
  564. $printAreaRect[1] = Coordinate::coordinateFromString($printAreaRect[1]);
  565. $print_rowmin = $printAreaRect[0][1] - 1;
  566. $print_rowmax = $printAreaRect[1][1] - 1;
  567. $print_colmin = Coordinate::columnIndexFromString($printAreaRect[0][0]) - 1;
  568. $print_colmax = Coordinate::columnIndexFromString($printAreaRect[1][0]) - 1;
  569. // construct formula data manually because parser does not recognize absolute 3d cell references
  570. $formulaData .= pack('Cvvvvv', 0x3B, $i, $print_rowmin, $print_rowmax, $print_colmin, $print_colmax);
  571. if ($j > 0) {
  572. $formulaData .= pack('C', 0x10); // list operator token ','
  573. }
  574. }
  575. // store the DEFINEDNAME record
  576. $chunk .= $this->writeData($this->writeDefinedNameBiff8(pack('C', 0x06), $formulaData, $i + 1, true));
  577. }
  578. }
  579. // write autofilters, if any
  580. for ($i = 0; $i < $total_worksheets; ++$i) {
  581. $sheetAutoFilter = $this->spreadsheet->getSheet($i)->getAutoFilter();
  582. $autoFilterRange = $sheetAutoFilter->getRange();
  583. if (!empty($autoFilterRange)) {
  584. $rangeBounds = Coordinate::rangeBoundaries($autoFilterRange);
  585. //Autofilter built in name
  586. $name = pack('C', 0x0D);
  587. $chunk .= $this->writeData($this->writeShortNameBiff8($name, $i + 1, $rangeBounds, true));
  588. }
  589. }
  590. return $chunk;
  591. }
  592. /**
  593. * Write a DEFINEDNAME record for BIFF8 using explicit binary formula data.
  594. *
  595. * @param string $name The name in UTF-8
  596. * @param string $formulaData The binary formula data
  597. * @param int $sheetIndex 1-based sheet index the defined name applies to. 0 = global
  598. * @param bool $isBuiltIn Built-in name?
  599. *
  600. * @return string Complete binary record data
  601. */
  602. private function writeDefinedNameBiff8($name, $formulaData, $sheetIndex = 0, $isBuiltIn = false)
  603. {
  604. $record = 0x0018;
  605. // option flags
  606. $options = $isBuiltIn ? 0x20 : 0x00;
  607. // length of the name, character count
  608. $nlen = StringHelper::countCharacters($name);
  609. // name with stripped length field
  610. $name = substr(StringHelper::UTF8toBIFF8UnicodeLong($name), 2);
  611. // size of the formula (in bytes)
  612. $sz = strlen($formulaData);
  613. // combine the parts
  614. $data = pack('vCCvvvCCCC', $options, 0, $nlen, $sz, 0, $sheetIndex, 0, 0, 0, 0)
  615. . $name . $formulaData;
  616. $length = strlen($data);
  617. $header = pack('vv', $record, $length);
  618. return $header . $data;
  619. }
  620. /**
  621. * Write a short NAME record.
  622. *
  623. * @param string $name
  624. * @param string $sheetIndex 1-based sheet index the defined name applies to. 0 = global
  625. * @param integer[][] $rangeBounds range boundaries
  626. * @param bool $isHidden
  627. *
  628. * @return string Complete binary record data
  629. * */
  630. private function writeShortNameBiff8($name, $sheetIndex, $rangeBounds, $isHidden = false)
  631. {
  632. $record = 0x0018;
  633. // option flags
  634. $options = ($isHidden ? 0x21 : 0x00);
  635. $extra = pack(
  636. 'Cvvvvv',
  637. 0x3B,
  638. $sheetIndex - 1,
  639. $rangeBounds[0][1] - 1,
  640. $rangeBounds[1][1] - 1,
  641. $rangeBounds[0][0] - 1,
  642. $rangeBounds[1][0] - 1
  643. );
  644. // size of the formula (in bytes)
  645. $sz = strlen($extra);
  646. // combine the parts
  647. $data = pack('vCCvvvCCCCC', $options, 0, 1, $sz, 0, $sheetIndex, 0, 0, 0, 0, 0)
  648. . $name . $extra;
  649. $length = strlen($data);
  650. $header = pack('vv', $record, $length);
  651. return $header . $data;
  652. }
  653. /**
  654. * Stores the CODEPAGE biff record.
  655. */
  656. private function writeCodepage()
  657. {
  658. $record = 0x0042; // Record identifier
  659. $length = 0x0002; // Number of bytes to follow
  660. $cv = $this->codepage; // The code page
  661. $header = pack('vv', $record, $length);
  662. $data = pack('v', $cv);
  663. $this->append($header . $data);
  664. }
  665. /**
  666. * Write Excel BIFF WINDOW1 record.
  667. */
  668. private function writeWindow1()
  669. {
  670. $record = 0x003D; // Record identifier
  671. $length = 0x0012; // Number of bytes to follow
  672. $xWn = 0x0000; // Horizontal position of window
  673. $yWn = 0x0000; // Vertical position of window
  674. $dxWn = 0x25BC; // Width of window
  675. $dyWn = 0x1572; // Height of window
  676. $grbit = 0x0038; // Option flags
  677. // not supported by PhpSpreadsheet, so there is only one selected sheet, the active
  678. $ctabsel = 1; // Number of workbook tabs selected
  679. $wTabRatio = 0x0258; // Tab to scrollbar ratio
  680. // not supported by PhpSpreadsheet, set to 0
  681. $itabFirst = 0; // 1st displayed worksheet
  682. $itabCur = $this->spreadsheet->getActiveSheetIndex(); // Active worksheet
  683. $header = pack('vv', $record, $length);
  684. $data = pack('vvvvvvvvv', $xWn, $yWn, $dxWn, $dyWn, $grbit, $itabCur, $itabFirst, $ctabsel, $wTabRatio);
  685. $this->append($header . $data);
  686. }
  687. /**
  688. * Writes Excel BIFF BOUNDSHEET record.
  689. *
  690. * @param Worksheet $sheet Worksheet name
  691. * @param int $offset Location of worksheet BOF
  692. */
  693. private function writeBoundSheet($sheet, $offset)
  694. {
  695. $sheetname = $sheet->getTitle();
  696. $record = 0x0085; // Record identifier
  697. // sheet state
  698. switch ($sheet->getSheetState()) {
  699. case \PhpOffice\PhpSpreadsheet\Worksheet\Worksheet::SHEETSTATE_VISIBLE:
  700. $ss = 0x00;
  701. break;
  702. case \PhpOffice\PhpSpreadsheet\Worksheet\Worksheet::SHEETSTATE_HIDDEN:
  703. $ss = 0x01;
  704. break;
  705. case \PhpOffice\PhpSpreadsheet\Worksheet\Worksheet::SHEETSTATE_VERYHIDDEN:
  706. $ss = 0x02;
  707. break;
  708. default:
  709. $ss = 0x00;
  710. break;
  711. }
  712. // sheet type
  713. $st = 0x00;
  714. $grbit = 0x0000; // Visibility and sheet type
  715. $data = pack('VCC', $offset, $ss, $st);
  716. $data .= StringHelper::UTF8toBIFF8UnicodeShort($sheetname);
  717. $length = strlen($data);
  718. $header = pack('vv', $record, $length);
  719. $this->append($header . $data);
  720. }
  721. /**
  722. * Write Internal SUPBOOK record.
  723. */
  724. private function writeSupbookInternal()
  725. {
  726. $record = 0x01AE; // Record identifier
  727. $length = 0x0004; // Bytes to follow
  728. $header = pack('vv', $record, $length);
  729. $data = pack('vv', $this->spreadsheet->getSheetCount(), 0x0401);
  730. return $this->writeData($header . $data);
  731. }
  732. /**
  733. * Writes the Excel BIFF EXTERNSHEET record. These references are used by
  734. * formulas.
  735. */
  736. private function writeExternalsheetBiff8()
  737. {
  738. $totalReferences = count($this->parser->references);
  739. $record = 0x0017; // Record identifier
  740. $length = 2 + 6 * $totalReferences; // Number of bytes to follow
  741. $supbook_index = 0; // FIXME: only using internal SUPBOOK record
  742. $header = pack('vv', $record, $length);
  743. $data = pack('v', $totalReferences);
  744. for ($i = 0; $i < $totalReferences; ++$i) {
  745. $data .= $this->parser->references[$i];
  746. }
  747. return $this->writeData($header . $data);
  748. }
  749. /**
  750. * Write Excel BIFF STYLE records.
  751. */
  752. private function writeStyle()
  753. {
  754. $record = 0x0293; // Record identifier
  755. $length = 0x0004; // Bytes to follow
  756. $ixfe = 0x8000; // Index to cell style XF
  757. $BuiltIn = 0x00; // Built-in style
  758. $iLevel = 0xff; // Outline style level
  759. $header = pack('vv', $record, $length);
  760. $data = pack('vCC', $ixfe, $BuiltIn, $iLevel);
  761. $this->append($header . $data);
  762. }
  763. /**
  764. * Writes Excel FORMAT record for non "built-in" numerical formats.
  765. *
  766. * @param string $format Custom format string
  767. * @param int $ifmt Format index code
  768. */
  769. private function writeNumberFormat($format, $ifmt)
  770. {
  771. $record = 0x041E; // Record identifier
  772. $numberFormatString = StringHelper::UTF8toBIFF8UnicodeLong($format);
  773. $length = 2 + strlen($numberFormatString); // Number of bytes to follow
  774. $header = pack('vv', $record, $length);
  775. $data = pack('v', $ifmt) . $numberFormatString;
  776. $this->append($header . $data);
  777. }
  778. /**
  779. * Write DATEMODE record to indicate the date system in use (1904 or 1900).
  780. */
  781. private function writeDateMode()
  782. {
  783. $record = 0x0022; // Record identifier
  784. $length = 0x0002; // Bytes to follow
  785. $f1904 = (Date::getExcelCalendar() === Date::CALENDAR_MAC_1904)
  786. ? 1
  787. : 0; // Flag for 1904 date system
  788. $header = pack('vv', $record, $length);
  789. $data = pack('v', $f1904);
  790. $this->append($header . $data);
  791. }
  792. /**
  793. * Stores the COUNTRY record for localization.
  794. *
  795. * @return string
  796. */
  797. private function writeCountry()
  798. {
  799. $record = 0x008C; // Record identifier
  800. $length = 4; // Number of bytes to follow
  801. $header = pack('vv', $record, $length);
  802. // using the same country code always for simplicity
  803. $data = pack('vv', $this->countryCode, $this->countryCode);
  804. return $this->writeData($header . $data);
  805. }
  806. /**
  807. * Write the RECALCID record.
  808. *
  809. * @return string
  810. */
  811. private function writeRecalcId()
  812. {
  813. $record = 0x01C1; // Record identifier
  814. $length = 8; // Number of bytes to follow
  815. $header = pack('vv', $record, $length);
  816. // by inspection of real Excel files, MS Office Excel 2007 writes this
  817. $data = pack('VV', 0x000001C1, 0x00001E667);
  818. return $this->writeData($header . $data);
  819. }
  820. /**
  821. * Stores the PALETTE biff record.
  822. */
  823. private function writePalette()
  824. {
  825. $aref = $this->palette;
  826. $record = 0x0092; // Record identifier
  827. $length = 2 + 4 * count($aref); // Number of bytes to follow
  828. $ccv = count($aref); // Number of RGB values to follow
  829. $data = ''; // The RGB data
  830. // Pack the RGB data
  831. foreach ($aref as $color) {
  832. foreach ($color as $byte) {
  833. $data .= pack('C', $byte);
  834. }
  835. }
  836. $header = pack('vvv', $record, $length, $ccv);
  837. $this->append($header . $data);
  838. }
  839. /**
  840. * Handling of the SST continue blocks is complicated by the need to include an
  841. * additional continuation byte depending on whether the string is split between
  842. * blocks or whether it starts at the beginning of the block. (There are also
  843. * additional complications that will arise later when/if Rich Strings are
  844. * supported).
  845. *
  846. * The Excel documentation says that the SST record should be followed by an
  847. * EXTSST record. The EXTSST record is a hash table that is used to optimise
  848. * access to SST. However, despite the documentation it doesn't seem to be
  849. * required so we will ignore it.
  850. *
  851. * @return string Binary data
  852. */
  853. private function writeSharedStringsTable()
  854. {
  855. // maximum size of record data (excluding record header)
  856. $continue_limit = 8224;
  857. // initialize array of record data blocks
  858. $recordDatas = [];
  859. // start SST record data block with total number of strings, total number of unique strings
  860. $recordData = pack('VV', $this->stringTotal, $this->stringUnique);
  861. // loop through all (unique) strings in shared strings table
  862. foreach (array_keys($this->stringTable) as $string) {
  863. // here $string is a BIFF8 encoded string
  864. // length = character count
  865. $headerinfo = unpack('vlength/Cencoding', $string);
  866. // currently, this is always 1 = uncompressed
  867. $encoding = $headerinfo['encoding'];
  868. // initialize finished writing current $string
  869. $finished = false;
  870. while ($finished === false) {
  871. // normally, there will be only one cycle, but if string cannot immediately be written as is
  872. // there will be need for more than one cylcle, if string longer than one record data block, there
  873. // may be need for even more cycles
  874. if (strlen($recordData) + strlen($string) <= $continue_limit) {
  875. // then we can write the string (or remainder of string) without any problems
  876. $recordData .= $string;
  877. if (strlen($recordData) + strlen($string) == $continue_limit) {
  878. // we close the record data block, and initialize a new one
  879. $recordDatas[] = $recordData;
  880. $recordData = '';
  881. }
  882. // we are finished writing this string
  883. $finished = true;
  884. } else {
  885. // special treatment writing the string (or remainder of the string)
  886. // If the string is very long it may need to be written in more than one CONTINUE record.
  887. // check how many bytes more there is room for in the current record
  888. $space_remaining = $continue_limit - strlen($recordData);
  889. // minimum space needed
  890. // uncompressed: 2 byte string length length field + 1 byte option flags + 2 byte character
  891. // compressed: 2 byte string length length field + 1 byte option flags + 1 byte character
  892. $min_space_needed = ($encoding == 1) ? 5 : 4;
  893. // We have two cases
  894. // 1. space remaining is less than minimum space needed
  895. // here we must waste the space remaining and move to next record data block
  896. // 2. space remaining is greater than or equal to minimum space needed
  897. // here we write as much as we can in the current block, then move to next record data block
  898. // 1. space remaining is less than minimum space needed
  899. if ($space_remaining < $min_space_needed) {
  900. // we close the block, store the block data
  901. $recordDatas[] = $recordData;
  902. // and start new record data block where we start writing the string
  903. $recordData = '';
  904. // 2. space remaining is greater than or equal to minimum space needed
  905. } else {
  906. // initialize effective remaining space, for Unicode strings this may need to be reduced by 1, see below
  907. $effective_space_remaining = $space_remaining;
  908. // for uncompressed strings, sometimes effective space remaining is reduced by 1
  909. if ($encoding == 1 && (strlen($string) - $space_remaining) % 2 == 1) {
  910. --$effective_space_remaining;
  911. }
  912. // one block fininshed, store the block data
  913. $recordData .= substr($string, 0, $effective_space_remaining);
  914. $string = substr($string, $effective_space_remaining); // for next cycle in while loop
  915. $recordDatas[] = $recordData;
  916. // start new record data block with the repeated option flags
  917. $recordData = pack('C', $encoding);
  918. }
  919. }
  920. }
  921. }
  922. // Store the last record data block unless it is empty
  923. // if there was no need for any continue records, this will be the for SST record data block itself
  924. if (strlen($recordData) > 0) {
  925. $recordDatas[] = $recordData;
  926. }
  927. // combine into one chunk with all the blocks SST, CONTINUE,...
  928. $chunk = '';
  929. foreach ($recordDatas as $i => $recordData) {
  930. // first block should have the SST record header, remaing should have CONTINUE header
  931. $record = ($i == 0) ? 0x00FC : 0x003C;
  932. $header = pack('vv', $record, strlen($recordData));
  933. $data = $header . $recordData;
  934. $chunk .= $this->writeData($data);
  935. }
  936. return $chunk;
  937. }
  938. /**
  939. * Writes the MSODRAWINGGROUP record if needed. Possibly split using CONTINUE records.
  940. */
  941. private function writeMsoDrawingGroup()
  942. {
  943. // write the Escher stream if necessary
  944. if (isset($this->escher)) {
  945. $writer = new Escher($this->escher);
  946. $data = $writer->close();
  947. $record = 0x00EB;
  948. $length = strlen($data);
  949. $header = pack('vv', $record, $length);
  950. return $this->writeData($header . $data);
  951. }
  952. return '';
  953. }
  954. /**
  955. * Get Escher object.
  956. *
  957. * @return \PhpOffice\PhpSpreadsheet\Shared\Escher
  958. */
  959. public function getEscher()
  960. {
  961. return $this->escher;
  962. }
  963. /**
  964. * Set Escher object.
  965. *
  966. * @param \PhpOffice\PhpSpreadsheet\Shared\Escher $pValue
  967. */
  968. public function setEscher(\PhpOffice\PhpSpreadsheet\Shared\Escher $pValue = null)
  969. {
  970. $this->escher = $pValue;
  971. }
  972. }