Workbook.php 39 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152
  1. <?php
  2. namespace PhpOffice\PhpSpreadsheet\Writer\Xls;
  3. use PhpOffice\PhpSpreadsheet\Cell\Coordinate;
  4. use PhpOffice\PhpSpreadsheet\Exception as PhpSpreadsheetException;
  5. use PhpOffice\PhpSpreadsheet\Shared\Date;
  6. use PhpOffice\PhpSpreadsheet\Shared\StringHelper;
  7. use PhpOffice\PhpSpreadsheet\Spreadsheet;
  8. use PhpOffice\PhpSpreadsheet\Style\Style;
  9. // Original file header of PEAR::Spreadsheet_Excel_Writer_Workbook (used as the base for this class):
  10. // -----------------------------------------------------------------------------------------
  11. // /*
  12. // * Module written/ported by Xavier Noguer <xnoguer@rezebra.com>
  13. // *
  14. // * The majority of this is _NOT_ my code. I simply ported it from the
  15. // * PERL Spreadsheet::WriteExcel module.
  16. // *
  17. // * The author of the Spreadsheet::WriteExcel module is John McNamara
  18. // * <jmcnamara@cpan.org>
  19. // *
  20. // * I _DO_ maintain this code, and John McNamara has nothing to do with the
  21. // * porting of this code to PHP. Any questions directly related to this
  22. // * class library should be directed to me.
  23. // *
  24. // * License Information:
  25. // *
  26. // * Spreadsheet_Excel_Writer: A library for generating Excel Spreadsheets
  27. // * Copyright (c) 2002-2003 Xavier Noguer xnoguer@rezebra.com
  28. // *
  29. // * This library is free software; you can redistribute it and/or
  30. // * modify it under the terms of the GNU Lesser General Public
  31. // * License as published by the Free Software Foundation; either
  32. // * version 2.1 of the License, or (at your option) any later version.
  33. // *
  34. // * This library is distributed in the hope that it will be useful,
  35. // * but WITHOUT ANY WARRANTY; without even the implied warranty of
  36. // * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
  37. // * Lesser General Public License for more details.
  38. // *
  39. // * You should have received a copy of the GNU Lesser General Public
  40. // * License along with this library; if not, write to the Free Software
  41. // * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
  42. // */
  43. class Workbook extends BIFFwriter
  44. {
  45. /**
  46. * Formula parser.
  47. *
  48. * @var \PhpOffice\PhpSpreadsheet\Writer\Xls\Parser
  49. */
  50. private $parser;
  51. /**
  52. * The BIFF file size for the workbook.
  53. *
  54. * @var int
  55. *
  56. * @see calcSheetOffsets()
  57. */
  58. private $biffSize;
  59. /**
  60. * XF Writers.
  61. *
  62. * @var \PhpOffice\PhpSpreadsheet\Writer\Xls\Xf[]
  63. */
  64. private $xfWriters = [];
  65. /**
  66. * Array containing the colour palette.
  67. *
  68. * @var array
  69. */
  70. private $palette;
  71. /**
  72. * The codepage indicates the text encoding used for strings.
  73. *
  74. * @var int
  75. */
  76. private $codepage;
  77. /**
  78. * The country code used for localization.
  79. *
  80. * @var int
  81. */
  82. private $countryCode;
  83. /**
  84. * Workbook.
  85. *
  86. * @var Spreadsheet
  87. */
  88. private $spreadsheet;
  89. /**
  90. * Fonts writers.
  91. *
  92. * @var Font[]
  93. */
  94. private $fontWriters = [];
  95. /**
  96. * Added fonts. Maps from font's hash => index in workbook.
  97. *
  98. * @var array
  99. */
  100. private $addedFonts = [];
  101. /**
  102. * Shared number formats.
  103. *
  104. * @var array
  105. */
  106. private $numberFormats = [];
  107. /**
  108. * Added number formats. Maps from numberFormat's hash => index in workbook.
  109. *
  110. * @var array
  111. */
  112. private $addedNumberFormats = [];
  113. /**
  114. * Sizes of the binary worksheet streams.
  115. *
  116. * @var array
  117. */
  118. private $worksheetSizes = [];
  119. /**
  120. * Offsets of the binary worksheet streams relative to the start of the global workbook stream.
  121. *
  122. * @var array
  123. */
  124. private $worksheetOffsets = [];
  125. /**
  126. * Total number of shared strings in workbook.
  127. *
  128. * @var int
  129. */
  130. private $stringTotal;
  131. /**
  132. * Number of unique shared strings in workbook.
  133. *
  134. * @var int
  135. */
  136. private $stringUnique;
  137. /**
  138. * Array of unique shared strings in workbook.
  139. *
  140. * @var array
  141. */
  142. private $stringTable;
  143. /**
  144. * Color cache.
  145. */
  146. private $colors;
  147. /**
  148. * Escher object corresponding to MSODRAWINGGROUP.
  149. *
  150. * @var \PhpOffice\PhpSpreadsheet\Shared\Escher
  151. */
  152. private $escher;
  153. /**
  154. * Class constructor.
  155. *
  156. * @param Spreadsheet $spreadsheet The Workbook
  157. * @param int $str_total Total number of strings
  158. * @param int $str_unique Total number of unique strings
  159. * @param array $str_table String Table
  160. * @param array $colors Colour Table
  161. * @param Parser $parser The formula parser created for the Workbook
  162. */
  163. public function __construct(Spreadsheet $spreadsheet, &$str_total, &$str_unique, &$str_table, &$colors, Parser $parser)
  164. {
  165. // It needs to call its parent's constructor explicitly
  166. parent::__construct();
  167. $this->parser = $parser;
  168. $this->biffSize = 0;
  169. $this->palette = [];
  170. $this->countryCode = -1;
  171. $this->stringTotal = &$str_total;
  172. $this->stringUnique = &$str_unique;
  173. $this->stringTable = &$str_table;
  174. $this->colors = &$colors;
  175. $this->setPaletteXl97();
  176. $this->spreadsheet = $spreadsheet;
  177. $this->codepage = 0x04B0;
  178. // Add empty sheets and Build color cache
  179. $countSheets = $spreadsheet->getSheetCount();
  180. for ($i = 0; $i < $countSheets; ++$i) {
  181. $phpSheet = $spreadsheet->getSheet($i);
  182. $this->parser->setExtSheet($phpSheet->getTitle(), $i); // Register worksheet name with parser
  183. $supbook_index = 0x00;
  184. $ref = pack('vvv', $supbook_index, $i, $i);
  185. $this->parser->references[] = $ref; // Register reference with parser
  186. // Sheet tab colors?
  187. if ($phpSheet->isTabColorSet()) {
  188. $this->addColor($phpSheet->getTabColor()->getRGB());
  189. }
  190. }
  191. }
  192. /**
  193. * Add a new XF writer.
  194. *
  195. * @param Style $style
  196. * @param bool $isStyleXf Is it a style XF?
  197. *
  198. * @return int Index to XF record
  199. */
  200. public function addXfWriter(Style $style, $isStyleXf = false)
  201. {
  202. $xfWriter = new Xf($style);
  203. $xfWriter->setIsStyleXf($isStyleXf);
  204. // Add the font if not already added
  205. $fontIndex = $this->addFont($style->getFont());
  206. // Assign the font index to the xf record
  207. $xfWriter->setFontIndex($fontIndex);
  208. // Background colors, best to treat these after the font so black will come after white in custom palette
  209. $xfWriter->setFgColor($this->addColor($style->getFill()->getStartColor()->getRGB()));
  210. $xfWriter->setBgColor($this->addColor($style->getFill()->getEndColor()->getRGB()));
  211. $xfWriter->setBottomColor($this->addColor($style->getBorders()->getBottom()->getColor()->getRGB()));
  212. $xfWriter->setTopColor($this->addColor($style->getBorders()->getTop()->getColor()->getRGB()));
  213. $xfWriter->setRightColor($this->addColor($style->getBorders()->getRight()->getColor()->getRGB()));
  214. $xfWriter->setLeftColor($this->addColor($style->getBorders()->getLeft()->getColor()->getRGB()));
  215. $xfWriter->setDiagColor($this->addColor($style->getBorders()->getDiagonal()->getColor()->getRGB()));
  216. // Add the number format if it is not a built-in one and not already added
  217. if ($style->getNumberFormat()->getBuiltInFormatCode() === false) {
  218. $numberFormatHashCode = $style->getNumberFormat()->getHashCode();
  219. if (isset($this->addedNumberFormats[$numberFormatHashCode])) {
  220. $numberFormatIndex = $this->addedNumberFormats[$numberFormatHashCode];
  221. } else {
  222. $numberFormatIndex = 164 + count($this->numberFormats);
  223. $this->numberFormats[$numberFormatIndex] = $style->getNumberFormat();
  224. $this->addedNumberFormats[$numberFormatHashCode] = $numberFormatIndex;
  225. }
  226. } else {
  227. $numberFormatIndex = (int) $style->getNumberFormat()->getBuiltInFormatCode();
  228. }
  229. // Assign the number format index to xf record
  230. $xfWriter->setNumberFormatIndex($numberFormatIndex);
  231. $this->xfWriters[] = $xfWriter;
  232. $xfIndex = count($this->xfWriters) - 1;
  233. return $xfIndex;
  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. }