Workbook.php 56 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192119311941195119611971198119912001201120212031204120512061207120812091210121112121213121412151216121712181219122012211222122312241225122612271228122912301231123212331234123512361237123812391240124112421243124412451246124712481249125012511252125312541255125612571258125912601261126212631264126512661267126812691270127112721273127412751276127712781279128012811282128312841285128612871288128912901291129212931294129512961297129812991300130113021303130413051306130713081309131013111312131313141315131613171318131913201321132213231324132513261327132813291330133113321333133413351336133713381339134013411342134313441345134613471348134913501351135213531354135513561357135813591360136113621363136413651366136713681369137013711372137313741375137613771378137913801381138213831384138513861387138813891390139113921393139413951396139713981399140014011402140314041405140614071408140914101411141214131414141514161417141814191420142114221423142414251426142714281429143014311432143314341435143614371438143914401441144214431444144514461447144814491450145114521453145414551456145714581459146014611462146314641465146614671468146914701471147214731474147514761477147814791480148114821483148414851486148714881489149014911492149314941495149614971498149915001501150215031504150515061507150815091510151115121513151415151516151715181519152015211522152315241525152615271528152915301531153215331534153515361537153815391540154115421543154415451546154715481549155015511552155315541555155615571558155915601561156215631564156515661567156815691570157115721573157415751576157715781579158015811582158315841585158615871588158915901591
  1. <?php
  2. /*
  3. * Module written/ported by Xavier Noguer <xnoguer@rezebra.com>
  4. *
  5. * The majority of this is _NOT_ my code. I simply ported it from the
  6. * PERL Spreadsheet::WriteExcel module.
  7. *
  8. * The author of the Spreadsheet::WriteExcel module is John McNamara
  9. * <jmcnamara@cpan.org>
  10. *
  11. * I _DO_ maintain this code, and John McNamara has nothing to do with the
  12. * porting of this code to PHP. Any questions directly related to this
  13. * class library should be directed to me.
  14. *
  15. * License Information:
  16. *
  17. * Spreadsheet_Excel_Writer: A library for generating Excel Spreadsheets
  18. * Copyright (c) 2002-2003 Xavier Noguer xnoguer@rezebra.com
  19. *
  20. * This library is free software; you can redistribute it and/or
  21. * modify it under the terms of the GNU Lesser General Public
  22. * License as published by the Free Software Foundation; either
  23. * version 2.1 of the License, or (at your option) any later version.
  24. *
  25. * This library is distributed in the hope that it will be useful,
  26. * but WITHOUT ANY WARRANTY; without even the implied warranty of
  27. * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
  28. * Lesser General Public License for more details.
  29. *
  30. * You should have received a copy of the GNU Lesser General Public
  31. * License along with this library; if not, write to the Free Software
  32. * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
  33. */
  34. require_once api_get_path(LIBRARY_PATH).'pear/Spreadsheet_Excel_Writer/Writer/Format.php';
  35. require_once api_get_path(LIBRARY_PATH).'pear/Spreadsheet_Excel_Writer/Writer/BIFFwriter.php';
  36. require_once api_get_path(LIBRARY_PATH).'pear/Spreadsheet_Excel_Writer/Writer/Worksheet.php';
  37. require_once api_get_path(LIBRARY_PATH).'pear/Spreadsheet_Excel_Writer/Writer/Parser.php';
  38. require_once api_get_path(LIBRARY_PATH).'pear/OLE/PPS/Root.php';
  39. require_once api_get_path(LIBRARY_PATH).'pear/OLE/PPS/File.php';
  40. /**
  41. * Class for generating Excel Spreadsheets
  42. *
  43. * @author Xavier Noguer <xnoguer@rezebra.com>
  44. * @category FileFormats
  45. * @package Spreadsheet_Excel_Writer
  46. */
  47. class Spreadsheet_Excel_Writer_Workbook extends Spreadsheet_Excel_Writer_BIFFwriter
  48. {
  49. /**
  50. * Filename for the Workbook
  51. * @var string
  52. */
  53. var $_filename;
  54. /**
  55. * Formula parser
  56. * @var object Parser
  57. */
  58. var $_parser;
  59. /**
  60. * Flag for 1904 date system (0 => base date is 1900, 1 => base date is 1904)
  61. * @var integer
  62. */
  63. var $_1904;
  64. /**
  65. * The active worksheet of the workbook (0 indexed)
  66. * @var integer
  67. */
  68. var $_activesheet;
  69. /**
  70. * 1st displayed worksheet in the workbook (0 indexed)
  71. * @var integer
  72. */
  73. var $_firstsheet;
  74. /**
  75. * Number of workbook tabs selected
  76. * @var integer
  77. */
  78. var $_selected;
  79. /**
  80. * Index for creating adding new formats to the workbook
  81. * @var integer
  82. */
  83. var $_xf_index;
  84. /**
  85. * Flag for preventing close from being called twice.
  86. * @var integer
  87. * @see close()
  88. */
  89. var $_fileclosed;
  90. /**
  91. * The BIFF file size for the workbook.
  92. * @var integer
  93. * @see _calcSheetOffsets()
  94. */
  95. var $_biffsize;
  96. /**
  97. * The default sheetname for all sheets created.
  98. * @var string
  99. */
  100. var $_sheetname;
  101. /**
  102. * The default XF format.
  103. * @var object Format
  104. */
  105. var $_tmp_format;
  106. /**
  107. * Array containing references to all of this workbook's worksheets
  108. * @var array
  109. */
  110. var $_worksheets;
  111. /**
  112. * Array of sheetnames for creating the EXTERNSHEET records
  113. * @var array
  114. */
  115. var $_sheetnames;
  116. /**
  117. * Array containing references to all of this workbook's formats
  118. * @var array
  119. */
  120. var $_formats;
  121. /**
  122. * Array containing the colour palette
  123. * @var array
  124. */
  125. var $_palette;
  126. /**
  127. * The default format for URLs.
  128. * @var object Format
  129. */
  130. var $_url_format;
  131. /**
  132. * The codepage indicates the text encoding used for strings
  133. * @var integer
  134. */
  135. var $_codepage;
  136. /**
  137. * The country code used for localization
  138. * @var integer
  139. */
  140. var $_country_code;
  141. /**
  142. * number of bytes for sizeinfo of strings
  143. * @var integer
  144. */
  145. var $_string_sizeinfo_size;
  146. /**
  147. * Class constructor
  148. *
  149. * @param string filename for storing the workbook. "-" for writing to stdout.
  150. * @access public
  151. */
  152. function Spreadsheet_Excel_Writer_Workbook($filename)
  153. {
  154. // It needs to call its parent's constructor explicitly
  155. $this->Spreadsheet_Excel_Writer_BIFFwriter();
  156. $this->_filename = $filename;
  157. $this->_parser =& new Spreadsheet_Excel_Writer_Parser($this->_byte_order, $this->_BIFF_version);
  158. $this->_1904 = 0;
  159. $this->_activesheet = 0;
  160. $this->_firstsheet = 0;
  161. $this->_selected = 0;
  162. $this->_xf_index = 16; // 15 style XF's and 1 cell XF.
  163. $this->_fileclosed = 0;
  164. $this->_biffsize = 0;
  165. $this->_sheetname = 'Sheet';
  166. $this->_tmp_format =& new Spreadsheet_Excel_Writer_Format($this->_BIFF_version);
  167. $this->_worksheets = array();
  168. $this->_sheetnames = array();
  169. $this->_formats = array();
  170. $this->_palette = array();
  171. $this->_codepage = 0x04E4; // FIXME: should change for BIFF8
  172. $this->_country_code = -1;
  173. $this->_string_sizeinfo = 3;
  174. // Add the default format for hyperlinks
  175. $this->_url_format =& $this->addFormat(array('color' => 'blue', 'underline' => 1));
  176. $this->_str_total = 0;
  177. $this->_str_unique = 0;
  178. $this->_str_table = array();
  179. $this->_setPaletteXl97();
  180. }
  181. /**
  182. * Calls finalization methods.
  183. * This method should always be the last one to be called on every workbook
  184. *
  185. * @access public
  186. * @return mixed true on success. PEAR_Error on failure
  187. */
  188. function close()
  189. {
  190. if ($this->_fileclosed) { // Prevent close() from being called twice.
  191. return true;
  192. }
  193. $res = $this->_storeWorkbook();
  194. if ($this->isError($res)) {
  195. return $this->raiseError($res->getMessage());
  196. }
  197. $this->_fileclosed = 1;
  198. return true;
  199. }
  200. /**
  201. * An accessor for the _worksheets[] array
  202. * Returns an array of the worksheet objects in a workbook
  203. * It actually calls to worksheets()
  204. *
  205. * @access public
  206. * @see worksheets()
  207. * @return array
  208. */
  209. function sheets()
  210. {
  211. return $this->worksheets();
  212. }
  213. /**
  214. * An accessor for the _worksheets[] array.
  215. * Returns an array of the worksheet objects in a workbook
  216. *
  217. * @access public
  218. * @return array
  219. */
  220. function worksheets()
  221. {
  222. return $this->_worksheets;
  223. }
  224. /**
  225. * Sets the BIFF version.
  226. * This method exists just to access experimental functionality
  227. * from BIFF8. It will be deprecated !
  228. * Only possible value is 8 (Excel 97/2000).
  229. * For any other value it fails silently.
  230. *
  231. * @access public
  232. * @param integer $version The BIFF version
  233. */
  234. function setVersion($version)
  235. {
  236. if ($version == 8) { // only accept version 8
  237. $version = 0x0600;
  238. $this->_BIFF_version = $version;
  239. // change BIFFwriter limit for CONTINUE records
  240. $this->_limit = 8228;
  241. $this->_tmp_format->_BIFF_version = $version;
  242. $this->_url_format->_BIFF_version = $version;
  243. $this->_parser->_BIFF_version = $version;
  244. $this->_codepage = 0x04B0;
  245. $total_worksheets = count($this->_worksheets);
  246. // change version for all worksheets too
  247. for ($i = 0; $i < $total_worksheets; $i++) {
  248. $this->_worksheets[$i]->_BIFF_version = $version;
  249. }
  250. $total_formats = count($this->_formats);
  251. // change version for all formats too
  252. for ($i = 0; $i < $total_formats; $i++) {
  253. $this->_formats[$i]->_BIFF_version = $version;
  254. }
  255. }
  256. }
  257. /**
  258. * Set the country identifier for the workbook
  259. *
  260. * @access public
  261. * @param integer $code Is the international calling country code for the
  262. * chosen country.
  263. */
  264. function setCountry($code)
  265. {
  266. $this->_country_code = $code;
  267. }
  268. /**
  269. * Add a new worksheet to the Excel workbook.
  270. * If no name is given the name of the worksheet will be Sheeti$i, with
  271. * $i in [1..].
  272. *
  273. * @access public
  274. * @param string $name the optional name of the worksheet
  275. * @return mixed reference to a worksheet object on success, PEAR_Error
  276. * on failure
  277. */
  278. function &addWorksheet($name = '')
  279. {
  280. $index = count($this->_worksheets);
  281. $sheetname = $this->_sheetname;
  282. if ($name == '') {
  283. $name = $sheetname.($index+1);
  284. }
  285. // Check that sheetname is <= 31 chars (Excel limit before BIFF8).
  286. if ($this->_BIFF_version != 0x0600)
  287. {
  288. if (strlen($name) > 31) {
  289. return $this->raiseError("Sheetname $name must be <= 31 chars");
  290. }
  291. }
  292. // Check that the worksheet name doesn't already exist: a fatal Excel error.
  293. $total_worksheets = count($this->_worksheets);
  294. for ($i = 0; $i < $total_worksheets; $i++) {
  295. if ($this->_worksheets[$i]->getName() == $name) {
  296. return $this->raiseError("Worksheet '$name' already exists");
  297. }
  298. }
  299. $worksheet = new Spreadsheet_Excel_Writer_Worksheet($this->_BIFF_version,
  300. $name, $index,
  301. $this->_activesheet, $this->_firstsheet,
  302. $this->_str_total, $this->_str_unique,
  303. $this->_str_table, $this->_url_format,
  304. $this->_parser, $this->_tmp_dir);
  305. $this->_worksheets[$index] = &$worksheet; // Store ref for iterator
  306. $this->_sheetnames[$index] = $name; // Store EXTERNSHEET names
  307. $this->_parser->setExtSheet($name, $index); // Register worksheet name with parser
  308. return $worksheet;
  309. }
  310. /**
  311. * Add a new format to the Excel workbook.
  312. * Also, pass any properties to the Format constructor.
  313. *
  314. * @access public
  315. * @param array $properties array with properties for initializing the format.
  316. * @return &Spreadsheet_Excel_Writer_Format reference to an Excel Format
  317. */
  318. function &addFormat($properties = array())
  319. {
  320. $format = new Spreadsheet_Excel_Writer_Format($this->_BIFF_version, $this->_xf_index, $properties);
  321. $this->_xf_index += 1;
  322. $this->_formats[] = &$format;
  323. return $format;
  324. }
  325. /**
  326. * Create new validator.
  327. *
  328. * @access public
  329. * @return &Spreadsheet_Excel_Writer_Validator reference to a Validator
  330. */
  331. function &addValidator()
  332. {
  333. include_once api_get_path(LIBRARY_PATH).'pear/Spreadsheet_Excel_Writer/Writer/Validator.php';
  334. /* FIXME: check for successful inclusion*/
  335. $valid = new Spreadsheet_Excel_Writer_Validator($this->_parser);
  336. return $valid;
  337. }
  338. /**
  339. * Change the RGB components of the elements in the colour palette.
  340. *
  341. * @access public
  342. * @param integer $index colour index
  343. * @param integer $red red RGB value [0-255]
  344. * @param integer $green green RGB value [0-255]
  345. * @param integer $blue blue RGB value [0-255]
  346. * @return integer The palette index for the custom color
  347. */
  348. function setCustomColor($index, $red, $green, $blue)
  349. {
  350. // Match a HTML #xxyyzz style parameter
  351. /*if (defined $_[1] and $_[1] =~ /^#(\w\w)(\w\w)(\w\w)/ ) {
  352. @_ = ($_[0], hex $1, hex $2, hex $3);
  353. }*/
  354. // Check that the colour index is the right range
  355. if ($index < 8 or $index > 64) {
  356. // TODO: assign real error codes
  357. return $this->raiseError("Color index $index outside range: 8 <= index <= 64");
  358. }
  359. // Check that the colour components are in the right range
  360. if (($red < 0 or $red > 255) ||
  361. ($green < 0 or $green > 255) ||
  362. ($blue < 0 or $blue > 255))
  363. {
  364. return $this->raiseError("Color component outside range: 0 <= color <= 255");
  365. }
  366. $index -= 8; // Adjust colour index (wingless dragonfly)
  367. // Set the RGB value
  368. $this->_palette[$index] = array($red, $green, $blue, 0);
  369. return($index + 8);
  370. }
  371. /**
  372. * Sets the colour palette to the Excel 97+ default.
  373. *
  374. * @access private
  375. */
  376. function _setPaletteXl97()
  377. {
  378. $this->_palette = array(
  379. array(0x00, 0x00, 0x00, 0x00), // 8
  380. array(0xff, 0xff, 0xff, 0x00), // 9
  381. array(0xff, 0x00, 0x00, 0x00), // 10
  382. array(0x00, 0xff, 0x00, 0x00), // 11
  383. array(0x00, 0x00, 0xff, 0x00), // 12
  384. array(0xff, 0xff, 0x00, 0x00), // 13
  385. array(0xff, 0x00, 0xff, 0x00), // 14
  386. array(0x00, 0xff, 0xff, 0x00), // 15
  387. array(0x80, 0x00, 0x00, 0x00), // 16
  388. array(0x00, 0x80, 0x00, 0x00), // 17
  389. array(0x00, 0x00, 0x80, 0x00), // 18
  390. array(0x80, 0x80, 0x00, 0x00), // 19
  391. array(0x80, 0x00, 0x80, 0x00), // 20
  392. array(0x00, 0x80, 0x80, 0x00), // 21
  393. array(0xc0, 0xc0, 0xc0, 0x00), // 22
  394. array(0x80, 0x80, 0x80, 0x00), // 23
  395. array(0x99, 0x99, 0xff, 0x00), // 24
  396. array(0x99, 0x33, 0x66, 0x00), // 25
  397. array(0xff, 0xff, 0xcc, 0x00), // 26
  398. array(0xcc, 0xff, 0xff, 0x00), // 27
  399. array(0x66, 0x00, 0x66, 0x00), // 28
  400. array(0xff, 0x80, 0x80, 0x00), // 29
  401. array(0x00, 0x66, 0xcc, 0x00), // 30
  402. array(0xcc, 0xcc, 0xff, 0x00), // 31
  403. array(0x00, 0x00, 0x80, 0x00), // 32
  404. array(0xff, 0x00, 0xff, 0x00), // 33
  405. array(0xff, 0xff, 0x00, 0x00), // 34
  406. array(0x00, 0xff, 0xff, 0x00), // 35
  407. array(0x80, 0x00, 0x80, 0x00), // 36
  408. array(0x80, 0x00, 0x00, 0x00), // 37
  409. array(0x00, 0x80, 0x80, 0x00), // 38
  410. array(0x00, 0x00, 0xff, 0x00), // 39
  411. array(0x00, 0xcc, 0xff, 0x00), // 40
  412. array(0xcc, 0xff, 0xff, 0x00), // 41
  413. array(0xcc, 0xff, 0xcc, 0x00), // 42
  414. array(0xff, 0xff, 0x99, 0x00), // 43
  415. array(0x99, 0xcc, 0xff, 0x00), // 44
  416. array(0xff, 0x99, 0xcc, 0x00), // 45
  417. array(0xcc, 0x99, 0xff, 0x00), // 46
  418. array(0xff, 0xcc, 0x99, 0x00), // 47
  419. array(0x33, 0x66, 0xff, 0x00), // 48
  420. array(0x33, 0xcc, 0xcc, 0x00), // 49
  421. array(0x99, 0xcc, 0x00, 0x00), // 50
  422. array(0xff, 0xcc, 0x00, 0x00), // 51
  423. array(0xff, 0x99, 0x00, 0x00), // 52
  424. array(0xff, 0x66, 0x00, 0x00), // 53
  425. array(0x66, 0x66, 0x99, 0x00), // 54
  426. array(0x96, 0x96, 0x96, 0x00), // 55
  427. array(0x00, 0x33, 0x66, 0x00), // 56
  428. array(0x33, 0x99, 0x66, 0x00), // 57
  429. array(0x00, 0x33, 0x00, 0x00), // 58
  430. array(0x33, 0x33, 0x00, 0x00), // 59
  431. array(0x99, 0x33, 0x00, 0x00), // 60
  432. array(0x99, 0x33, 0x66, 0x00), // 61
  433. array(0x33, 0x33, 0x99, 0x00), // 62
  434. array(0x33, 0x33, 0x33, 0x00), // 63
  435. );
  436. }
  437. /**
  438. * Assemble worksheets into a workbook and send the BIFF data to an OLE
  439. * storage.
  440. *
  441. * @access private
  442. * @return mixed true on success. PEAR_Error on failure
  443. */
  444. function _storeWorkbook()
  445. {
  446. if (count($this->_worksheets) == 0) {
  447. return true;
  448. }
  449. // Ensure that at least one worksheet has been selected.
  450. if ($this->_activesheet == 0) {
  451. $this->_worksheets[0]->selected = 1;
  452. }
  453. // Calculate the number of selected worksheet tabs and call the finalization
  454. // methods for each worksheet
  455. $total_worksheets = count($this->_worksheets);
  456. for ($i = 0; $i < $total_worksheets; $i++) {
  457. if ($this->_worksheets[$i]->selected) {
  458. $this->_selected++;
  459. }
  460. $this->_worksheets[$i]->close($this->_sheetnames);
  461. }
  462. // Add Workbook globals
  463. $this->_storeBof(0x0005);
  464. $this->_storeCodepage();
  465. if ($this->_BIFF_version == 0x0600) {
  466. $this->_storeWindow1();
  467. }
  468. if ($this->_BIFF_version == 0x0500) {
  469. $this->_storeExterns(); // For print area and repeat rows
  470. }
  471. $this->_storeNames(); // For print area and repeat rows
  472. if ($this->_BIFF_version == 0x0500) {
  473. $this->_storeWindow1();
  474. }
  475. $this->_storeDatemode();
  476. $this->_storeAllFonts();
  477. $this->_storeAllNumFormats();
  478. $this->_storeAllXfs();
  479. $this->_storeAllStyles();
  480. $this->_storePalette();
  481. $this->_calcSheetOffsets();
  482. // Add BOUNDSHEET records
  483. for ($i = 0; $i < $total_worksheets; $i++) {
  484. $this->_storeBoundsheet($this->_worksheets[$i]->name,$this->_worksheets[$i]->offset);
  485. }
  486. if ($this->_country_code != -1) {
  487. $this->_storeCountry();
  488. }
  489. if ($this->_BIFF_version == 0x0600) {
  490. //$this->_storeSupbookInternal();
  491. /* TODO: store external SUPBOOK records and XCT and CRN records
  492. in case of external references for BIFF8 */
  493. //$this->_storeExternsheetBiff8();
  494. $this->_storeSharedStringsTable();
  495. }
  496. // End Workbook globals
  497. $this->_storeEof();
  498. // Store the workbook in an OLE container
  499. $res = $this->_storeOLEFile();
  500. if ($this->isError($res)) {
  501. return $this->raiseError($res->getMessage());
  502. }
  503. return true;
  504. }
  505. /**
  506. * Store the workbook in an OLE container
  507. *
  508. * @access private
  509. * @return mixed true on success. PEAR_Error on failure
  510. */
  511. function _storeOLEFile()
  512. {
  513. if($this->_BIFF_version == 0x0600) {
  514. $OLE = new OLE_PPS_File(OLE::Asc2Ucs('Workbook'));
  515. } else {
  516. $OLE = new OLE_PPS_File(OLE::Asc2Ucs('Book'));
  517. }
  518. if ($this->_tmp_dir != '') {
  519. $OLE->setTempDir($this->_tmp_dir);
  520. }
  521. $res = $OLE->init();
  522. if ($this->isError($res)) {
  523. return $this->raiseError("OLE Error: ".$res->getMessage());
  524. }
  525. $OLE->append($this->_data);
  526. $total_worksheets = count($this->_worksheets);
  527. for ($i = 0; $i < $total_worksheets; $i++) {
  528. while ($tmp = $this->_worksheets[$i]->getData()) {
  529. $OLE->append($tmp);
  530. }
  531. }
  532. $root = new OLE_PPS_Root(time(), time(), array($OLE));
  533. if ($this->_tmp_dir != '') {
  534. $root->setTempDir($this->_tmp_dir);
  535. }
  536. $res = $root->save($this->_filename);
  537. if ($this->isError($res)) {
  538. return $this->raiseError("OLE Error: ".$res->getMessage());
  539. }
  540. return true;
  541. }
  542. /**
  543. * Calculate offsets for Worksheet BOF records.
  544. *
  545. * @access private
  546. */
  547. function _calcSheetOffsets()
  548. {
  549. if ($this->_BIFF_version == 0x0600) {
  550. $boundsheet_length = 12; // fixed length for a BOUNDSHEET record
  551. } else {
  552. $boundsheet_length = 11;
  553. }
  554. $EOF = 4;
  555. $offset = $this->_datasize;
  556. if ($this->_BIFF_version == 0x0600) {
  557. // add the length of the SST
  558. /* TODO: check this works for a lot of strings (> 8224 bytes) */
  559. $offset += $this->_calculateSharedStringsSizes();
  560. if ($this->_country_code != -1) {
  561. $offset += 8; // adding COUNTRY record
  562. }
  563. // add the lenght of SUPBOOK, EXTERNSHEET and NAME records
  564. //$offset += 8; // FIXME: calculate real value when storing the records
  565. }
  566. $total_worksheets = count($this->_worksheets);
  567. // add the length of the BOUNDSHEET records
  568. for ($i = 0; $i < $total_worksheets; $i++) {
  569. $offset += $boundsheet_length + strlen($this->_worksheets[$i]->name);
  570. }
  571. $offset += $EOF;
  572. for ($i = 0; $i < $total_worksheets; $i++) {
  573. $this->_worksheets[$i]->offset = $offset;
  574. $offset += $this->_worksheets[$i]->_datasize;
  575. }
  576. $this->_biffsize = $offset;
  577. }
  578. /**
  579. * Store the Excel FONT records.
  580. *
  581. * @access private
  582. */
  583. function _storeAllFonts()
  584. {
  585. // tmp_format is added by the constructor. We use this to write the default XF's
  586. $format = $this->_tmp_format;
  587. $font = $format->getFont();
  588. // Note: Fonts are 0-indexed. According to the SDK there is no index 4,
  589. // so the following fonts are 0, 1, 2, 3, 5
  590. //
  591. for ($i = 1; $i <= 5; $i++){
  592. $this->_append($font);
  593. }
  594. // Iterate through the XF objects and write a FONT record if it isn't the
  595. // same as the default FONT and if it hasn't already been used.
  596. //
  597. $fonts = array();
  598. $index = 6; // The first user defined FONT
  599. $key = $format->getFontKey(); // The default font from _tmp_format
  600. $fonts[$key] = 0; // Index of the default font
  601. $total_formats = count($this->_formats);
  602. for ($i = 0; $i < $total_formats; $i++) {
  603. $key = $this->_formats[$i]->getFontKey();
  604. if (isset($fonts[$key])) {
  605. // FONT has already been used
  606. $this->_formats[$i]->font_index = $fonts[$key];
  607. } else {
  608. // Add a new FONT record
  609. $fonts[$key] = $index;
  610. $this->_formats[$i]->font_index = $index;
  611. $index++;
  612. $font = $this->_formats[$i]->getFont();
  613. $this->_append($font);
  614. }
  615. }
  616. }
  617. /**
  618. * Store user defined numerical formats i.e. FORMAT records
  619. *
  620. * @access private
  621. */
  622. function _storeAllNumFormats()
  623. {
  624. // Leaning num_format syndrome
  625. $hash_num_formats = array();
  626. $num_formats = array();
  627. $index = 164;
  628. // Iterate through the XF objects and write a FORMAT record if it isn't a
  629. // built-in format type and if the FORMAT string hasn't already been used.
  630. $total_formats = count($this->_formats);
  631. for ($i = 0; $i < $total_formats; $i++) {
  632. $num_format = $this->_formats[$i]->_num_format;
  633. // Check if $num_format is an index to a built-in format.
  634. // Also check for a string of zeros, which is a valid format string
  635. // but would evaluate to zero.
  636. //
  637. if (!preg_match("/^0+\d/", $num_format)) {
  638. if (preg_match("/^\d+$/", $num_format)) { // built-in format
  639. continue;
  640. }
  641. }
  642. if (isset($hash_num_formats[$num_format])) {
  643. // FORMAT has already been used
  644. $this->_formats[$i]->_num_format = $hash_num_formats[$num_format];
  645. } else{
  646. // Add a new FORMAT
  647. $hash_num_formats[$num_format] = $index;
  648. $this->_formats[$i]->_num_format = $index;
  649. array_push($num_formats,$num_format);
  650. $index++;
  651. }
  652. }
  653. // Write the new FORMAT records starting from 0xA4
  654. $index = 164;
  655. foreach ($num_formats as $num_format) {
  656. $this->_storeNumFormat($num_format,$index);
  657. $index++;
  658. }
  659. }
  660. /**
  661. * Write all XF records.
  662. *
  663. * @access private
  664. */
  665. function _storeAllXfs()
  666. {
  667. // _tmp_format is added by the constructor. We use this to write the default XF's
  668. // The default font index is 0
  669. //
  670. $format = $this->_tmp_format;
  671. for ($i = 0; $i <= 14; $i++) {
  672. $xf = $format->getXf('style'); // Style XF
  673. $this->_append($xf);
  674. }
  675. $xf = $format->getXf('cell'); // Cell XF
  676. $this->_append($xf);
  677. // User defined XFs
  678. $total_formats = count($this->_formats);
  679. for ($i = 0; $i < $total_formats; $i++) {
  680. $xf = $this->_formats[$i]->getXf('cell');
  681. $this->_append($xf);
  682. }
  683. }
  684. /**
  685. * Write all STYLE records.
  686. *
  687. * @access private
  688. */
  689. function _storeAllStyles()
  690. {
  691. $this->_storeStyle();
  692. }
  693. /**
  694. * Write the EXTERNCOUNT and EXTERNSHEET records. These are used as indexes for
  695. * the NAME records.
  696. *
  697. * @access private
  698. */
  699. function _storeExterns()
  700. {
  701. // Create EXTERNCOUNT with number of worksheets
  702. $this->_storeExterncount(count($this->_worksheets));
  703. // Create EXTERNSHEET for each worksheet
  704. foreach ($this->_sheetnames as $sheetname) {
  705. $this->_storeExternsheet($sheetname);
  706. }
  707. }
  708. /**
  709. * Write the NAME record to define the print area and the repeat rows and cols.
  710. *
  711. * @access private
  712. */
  713. function _storeNames()
  714. {
  715. // Create the print area NAME records
  716. $total_worksheets = count($this->_worksheets);
  717. for ($i = 0; $i < $total_worksheets; $i++) {
  718. // Write a Name record if the print area has been defined
  719. if (isset($this->_worksheets[$i]->print_rowmin)) {
  720. $this->_storeNameShort(
  721. $this->_worksheets[$i]->index,
  722. 0x06, // NAME type
  723. $this->_worksheets[$i]->print_rowmin,
  724. $this->_worksheets[$i]->print_rowmax,
  725. $this->_worksheets[$i]->print_colmin,
  726. $this->_worksheets[$i]->print_colmax
  727. );
  728. }
  729. }
  730. // Create the print title NAME records
  731. $total_worksheets = count($this->_worksheets);
  732. for ($i = 0; $i < $total_worksheets; $i++) {
  733. $rowmin = $this->_worksheets[$i]->title_rowmin;
  734. $rowmax = $this->_worksheets[$i]->title_rowmax;
  735. $colmin = $this->_worksheets[$i]->title_colmin;
  736. $colmax = $this->_worksheets[$i]->title_colmax;
  737. // Determine if row + col, row, col or nothing has been defined
  738. // and write the appropriate record
  739. //
  740. if (isset($rowmin) && isset($colmin)) {
  741. // Row and column titles have been defined.
  742. // Row title has been defined.
  743. $this->_storeNameLong(
  744. $this->_worksheets[$i]->index,
  745. 0x07, // NAME type
  746. $rowmin,
  747. $rowmax,
  748. $colmin,
  749. $colmax
  750. );
  751. } elseif (isset($rowmin)) {
  752. // Row title has been defined.
  753. $this->_storeNameShort(
  754. $this->_worksheets[$i]->index,
  755. 0x07, // NAME type
  756. $rowmin,
  757. $rowmax,
  758. 0x00,
  759. 0xff
  760. );
  761. } elseif (isset($colmin)) {
  762. // Column title has been defined.
  763. $this->_storeNameShort(
  764. $this->_worksheets[$i]->index,
  765. 0x07, // NAME type
  766. 0x0000,
  767. 0x3fff,
  768. $colmin,
  769. $colmax
  770. );
  771. } else {
  772. // Print title hasn't been defined.
  773. }
  774. }
  775. }
  776. /******************************************************************************
  777. *
  778. * BIFF RECORDS
  779. *
  780. */
  781. /**
  782. * Stores the CODEPAGE biff record.
  783. *
  784. * @access private
  785. */
  786. function _storeCodepage()
  787. {
  788. $record = 0x0042; // Record identifier
  789. $length = 0x0002; // Number of bytes to follow
  790. $cv = $this->_codepage; // The code page
  791. $header = pack('vv', $record, $length);
  792. $data = pack('v', $cv);
  793. $this->_append($header . $data);
  794. }
  795. /**
  796. * Write Excel BIFF WINDOW1 record.
  797. *
  798. * @access private
  799. */
  800. function _storeWindow1()
  801. {
  802. $record = 0x003D; // Record identifier
  803. $length = 0x0012; // Number of bytes to follow
  804. $xWn = 0x0000; // Horizontal position of window
  805. $yWn = 0x0000; // Vertical position of window
  806. $dxWn = 0x25BC; // Width of window
  807. $dyWn = 0x1572; // Height of window
  808. $grbit = 0x0038; // Option flags
  809. $ctabsel = $this->_selected; // Number of workbook tabs selected
  810. $wTabRatio = 0x0258; // Tab to scrollbar ratio
  811. $itabFirst = $this->_firstsheet; // 1st displayed worksheet
  812. $itabCur = $this->_activesheet; // Active worksheet
  813. $header = pack("vv", $record, $length);
  814. $data = pack("vvvvvvvvv", $xWn, $yWn, $dxWn, $dyWn,
  815. $grbit,
  816. $itabCur, $itabFirst,
  817. $ctabsel, $wTabRatio);
  818. $this->_append($header . $data);
  819. }
  820. /**
  821. * Writes Excel BIFF BOUNDSHEET record.
  822. * FIXME: inconsistent with BIFF documentation
  823. *
  824. * @param string $sheetname Worksheet name
  825. * @param integer $offset Location of worksheet BOF
  826. * @access private
  827. */
  828. function _storeBoundsheet($sheetname,$offset)
  829. {
  830. $record = 0x0085; // Record identifier
  831. if ($this->_BIFF_version == 0x0600) {
  832. $length = 0x08 + strlen($sheetname); // Number of bytes to follow
  833. } else {
  834. $length = 0x07 + strlen($sheetname); // Number of bytes to follow
  835. }
  836. $grbit = 0x0000; // Visibility and sheet type
  837. $cch = strlen($sheetname); // Length of sheet name
  838. $header = pack("vv", $record, $length);
  839. if ($this->_BIFF_version == 0x0600) {
  840. $data = pack("Vvv", $offset, $grbit, $cch);
  841. } else {
  842. $data = pack("VvC", $offset, $grbit, $cch);
  843. }
  844. $this->_append($header.$data.$sheetname);
  845. }
  846. /**
  847. * Write Internal SUPBOOK record
  848. *
  849. * @access private
  850. */
  851. function _storeSupbookInternal()
  852. {
  853. $record = 0x01AE; // Record identifier
  854. $length = 0x0004; // Bytes to follow
  855. $header = pack("vv", $record, $length);
  856. $data = pack("vv", count($this->_worksheets), 0x0104);
  857. $this->_append($header . $data);
  858. }
  859. /**
  860. * Writes the Excel BIFF EXTERNSHEET record. These references are used by
  861. * formulas.
  862. *
  863. * @param string $sheetname Worksheet name
  864. * @access private
  865. */
  866. function _storeExternsheetBiff8()
  867. {
  868. $total_references = count($this->_parser->_references);
  869. $record = 0x0017; // Record identifier
  870. $length = 2 + 6 * $total_references; // Number of bytes to follow
  871. $supbook_index = 0; // FIXME: only using internal SUPBOOK record
  872. $header = pack("vv", $record, $length);
  873. $data = pack('v', $total_references);
  874. for ($i = 0; $i < $total_references; $i++) {
  875. $data .= $this->_parser->_references[$i];
  876. }
  877. $this->_append($header . $data);
  878. }
  879. /**
  880. * Write Excel BIFF STYLE records.
  881. *
  882. * @access private
  883. */
  884. function _storeStyle()
  885. {
  886. $record = 0x0293; // Record identifier
  887. $length = 0x0004; // Bytes to follow
  888. $ixfe = 0x8000; // Index to style XF
  889. $BuiltIn = 0x00; // Built-in style
  890. $iLevel = 0xff; // Outline style level
  891. $header = pack("vv", $record, $length);
  892. $data = pack("vCC", $ixfe, $BuiltIn, $iLevel);
  893. $this->_append($header . $data);
  894. }
  895. /**
  896. * Writes Excel FORMAT record for non "built-in" numerical formats.
  897. *
  898. * @param string $format Custom format string
  899. * @param integer $ifmt Format index code
  900. * @access private
  901. */
  902. function _storeNumFormat($format, $ifmt)
  903. {
  904. $record = 0x041E; // Record identifier
  905. if ($this->_BIFF_version == 0x0600) {
  906. $length = 5 + strlen($format); // Number of bytes to follow
  907. $encoding = 0x0;
  908. } elseif ($this->_BIFF_version == 0x0500) {
  909. $length = 3 + strlen($format); // Number of bytes to follow
  910. }
  911. $cch = strlen($format); // Length of format string
  912. $header = pack("vv", $record, $length);
  913. if ($this->_BIFF_version == 0x0600) {
  914. $data = pack("vvC", $ifmt, $cch, $encoding);
  915. } elseif ($this->_BIFF_version == 0x0500) {
  916. $data = pack("vC", $ifmt, $cch);
  917. }
  918. $this->_append($header . $data . $format);
  919. }
  920. /**
  921. * Write DATEMODE record to indicate the date system in use (1904 or 1900).
  922. *
  923. * @access private
  924. */
  925. function _storeDatemode()
  926. {
  927. $record = 0x0022; // Record identifier
  928. $length = 0x0002; // Bytes to follow
  929. $f1904 = $this->_1904; // Flag for 1904 date system
  930. $header = pack("vv", $record, $length);
  931. $data = pack("v", $f1904);
  932. $this->_append($header . $data);
  933. }
  934. /**
  935. * Write BIFF record EXTERNCOUNT to indicate the number of external sheet
  936. * references in the workbook.
  937. *
  938. * Excel only stores references to external sheets that are used in NAME.
  939. * The workbook NAME record is required to define the print area and the repeat
  940. * rows and columns.
  941. *
  942. * A similar method is used in Worksheet.php for a slightly different purpose.
  943. *
  944. * @param integer $cxals Number of external references
  945. * @access private
  946. */
  947. function _storeExterncount($cxals)
  948. {
  949. $record = 0x0016; // Record identifier
  950. $length = 0x0002; // Number of bytes to follow
  951. $header = pack("vv", $record, $length);
  952. $data = pack("v", $cxals);
  953. $this->_append($header . $data);
  954. }
  955. /**
  956. * Writes the Excel BIFF EXTERNSHEET record. These references are used by
  957. * formulas. NAME record is required to define the print area and the repeat
  958. * rows and columns.
  959. *
  960. * A similar method is used in Worksheet.php for a slightly different purpose.
  961. *
  962. * @param string $sheetname Worksheet name
  963. * @access private
  964. */
  965. function _storeExternsheet($sheetname)
  966. {
  967. $record = 0x0017; // Record identifier
  968. $length = 0x02 + strlen($sheetname); // Number of bytes to follow
  969. $cch = strlen($sheetname); // Length of sheet name
  970. $rgch = 0x03; // Filename encoding
  971. $header = pack("vv", $record, $length);
  972. $data = pack("CC", $cch, $rgch);
  973. $this->_append($header . $data . $sheetname);
  974. }
  975. /**
  976. * Store the NAME record in the short format that is used for storing the print
  977. * area, repeat rows only and repeat columns only.
  978. *
  979. * @param integer $index Sheet index
  980. * @param integer $type Built-in name type
  981. * @param integer $rowmin Start row
  982. * @param integer $rowmax End row
  983. * @param integer $colmin Start colum
  984. * @param integer $colmax End column
  985. * @access private
  986. */
  987. function _storeNameShort($index, $type, $rowmin, $rowmax, $colmin, $colmax)
  988. {
  989. $record = 0x0018; // Record identifier
  990. $length = 0x0024; // Number of bytes to follow
  991. $grbit = 0x0020; // Option flags
  992. $chKey = 0x00; // Keyboard shortcut
  993. $cch = 0x01; // Length of text name
  994. $cce = 0x0015; // Length of text definition
  995. $ixals = $index + 1; // Sheet index
  996. $itab = $ixals; // Equal to ixals
  997. $cchCustMenu = 0x00; // Length of cust menu text
  998. $cchDescription = 0x00; // Length of description text
  999. $cchHelptopic = 0x00; // Length of help topic text
  1000. $cchStatustext = 0x00; // Length of status bar text
  1001. $rgch = $type; // Built-in name type
  1002. $unknown03 = 0x3b;
  1003. $unknown04 = 0xffff-$index;
  1004. $unknown05 = 0x0000;
  1005. $unknown06 = 0x0000;
  1006. $unknown07 = 0x1087;
  1007. $unknown08 = 0x8005;
  1008. $header = pack("vv", $record, $length);
  1009. $data = pack("v", $grbit);
  1010. $data .= pack("C", $chKey);
  1011. $data .= pack("C", $cch);
  1012. $data .= pack("v", $cce);
  1013. $data .= pack("v", $ixals);
  1014. $data .= pack("v", $itab);
  1015. $data .= pack("C", $cchCustMenu);
  1016. $data .= pack("C", $cchDescription);
  1017. $data .= pack("C", $cchHelptopic);
  1018. $data .= pack("C", $cchStatustext);
  1019. $data .= pack("C", $rgch);
  1020. $data .= pack("C", $unknown03);
  1021. $data .= pack("v", $unknown04);
  1022. $data .= pack("v", $unknown05);
  1023. $data .= pack("v", $unknown06);
  1024. $data .= pack("v", $unknown07);
  1025. $data .= pack("v", $unknown08);
  1026. $data .= pack("v", $index);
  1027. $data .= pack("v", $index);
  1028. $data .= pack("v", $rowmin);
  1029. $data .= pack("v", $rowmax);
  1030. $data .= pack("C", $colmin);
  1031. $data .= pack("C", $colmax);
  1032. $this->_append($header . $data);
  1033. }
  1034. /**
  1035. * Store the NAME record in the long format that is used for storing the repeat
  1036. * rows and columns when both are specified. This shares a lot of code with
  1037. * _storeNameShort() but we use a separate method to keep the code clean.
  1038. * Code abstraction for reuse can be carried too far, and I should know. ;-)
  1039. *
  1040. * @param integer $index Sheet index
  1041. * @param integer $type Built-in name type
  1042. * @param integer $rowmin Start row
  1043. * @param integer $rowmax End row
  1044. * @param integer $colmin Start colum
  1045. * @param integer $colmax End column
  1046. * @access private
  1047. */
  1048. function _storeNameLong($index, $type, $rowmin, $rowmax, $colmin, $colmax)
  1049. {
  1050. $record = 0x0018; // Record identifier
  1051. $length = 0x003d; // Number of bytes to follow
  1052. $grbit = 0x0020; // Option flags
  1053. $chKey = 0x00; // Keyboard shortcut
  1054. $cch = 0x01; // Length of text name
  1055. $cce = 0x002e; // Length of text definition
  1056. $ixals = $index + 1; // Sheet index
  1057. $itab = $ixals; // Equal to ixals
  1058. $cchCustMenu = 0x00; // Length of cust menu text
  1059. $cchDescription = 0x00; // Length of description text
  1060. $cchHelptopic = 0x00; // Length of help topic text
  1061. $cchStatustext = 0x00; // Length of status bar text
  1062. $rgch = $type; // Built-in name type
  1063. $unknown01 = 0x29;
  1064. $unknown02 = 0x002b;
  1065. $unknown03 = 0x3b;
  1066. $unknown04 = 0xffff-$index;
  1067. $unknown05 = 0x0000;
  1068. $unknown06 = 0x0000;
  1069. $unknown07 = 0x1087;
  1070. $unknown08 = 0x8008;
  1071. $header = pack("vv", $record, $length);
  1072. $data = pack("v", $grbit);
  1073. $data .= pack("C", $chKey);
  1074. $data .= pack("C", $cch);
  1075. $data .= pack("v", $cce);
  1076. $data .= pack("v", $ixals);
  1077. $data .= pack("v", $itab);
  1078. $data .= pack("C", $cchCustMenu);
  1079. $data .= pack("C", $cchDescription);
  1080. $data .= pack("C", $cchHelptopic);
  1081. $data .= pack("C", $cchStatustext);
  1082. $data .= pack("C", $rgch);
  1083. $data .= pack("C", $unknown01);
  1084. $data .= pack("v", $unknown02);
  1085. // Column definition
  1086. $data .= pack("C", $unknown03);
  1087. $data .= pack("v", $unknown04);
  1088. $data .= pack("v", $unknown05);
  1089. $data .= pack("v", $unknown06);
  1090. $data .= pack("v", $unknown07);
  1091. $data .= pack("v", $unknown08);
  1092. $data .= pack("v", $index);
  1093. $data .= pack("v", $index);
  1094. $data .= pack("v", 0x0000);
  1095. $data .= pack("v", 0x3fff);
  1096. $data .= pack("C", $colmin);
  1097. $data .= pack("C", $colmax);
  1098. // Row definition
  1099. $data .= pack("C", $unknown03);
  1100. $data .= pack("v", $unknown04);
  1101. $data .= pack("v", $unknown05);
  1102. $data .= pack("v", $unknown06);
  1103. $data .= pack("v", $unknown07);
  1104. $data .= pack("v", $unknown08);
  1105. $data .= pack("v", $index);
  1106. $data .= pack("v", $index);
  1107. $data .= pack("v", $rowmin);
  1108. $data .= pack("v", $rowmax);
  1109. $data .= pack("C", 0x00);
  1110. $data .= pack("C", 0xff);
  1111. // End of data
  1112. $data .= pack("C", 0x10);
  1113. $this->_append($header . $data);
  1114. }
  1115. /**
  1116. * Stores the COUNTRY record for localization
  1117. *
  1118. * @access private
  1119. */
  1120. function _storeCountry()
  1121. {
  1122. $record = 0x008C; // Record identifier
  1123. $length = 4; // Number of bytes to follow
  1124. $header = pack('vv', $record, $length);
  1125. /* using the same country code always for simplicity */
  1126. $data = pack('vv', $this->_country_code, $this->_country_code);
  1127. $this->_append($header . $data);
  1128. }
  1129. /**
  1130. * Stores the PALETTE biff record.
  1131. *
  1132. * @access private
  1133. */
  1134. function _storePalette()
  1135. {
  1136. $aref = $this->_palette;
  1137. $record = 0x0092; // Record identifier
  1138. $length = 2 + 4 * count($aref); // Number of bytes to follow
  1139. $ccv = count($aref); // Number of RGB values to follow
  1140. $data = ''; // The RGB data
  1141. // Pack the RGB data
  1142. foreach ($aref as $color) {
  1143. foreach ($color as $byte) {
  1144. $data .= pack("C",$byte);
  1145. }
  1146. }
  1147. $header = pack("vvv", $record, $length, $ccv);
  1148. $this->_append($header . $data);
  1149. }
  1150. /**
  1151. * Calculate
  1152. * Handling of the SST continue blocks is complicated by the need to include an
  1153. * additional continuation byte depending on whether the string is split between
  1154. * blocks or whether it starts at the beginning of the block. (There are also
  1155. * additional complications that will arise later when/if Rich Strings are
  1156. * supported).
  1157. *
  1158. * @access private
  1159. */
  1160. function _calculateSharedStringsSizes()
  1161. {
  1162. /* Iterate through the strings to calculate the CONTINUE block sizes.
  1163. For simplicity we use the same size for the SST and CONTINUE records:
  1164. 8228 : Maximum Excel97 block size
  1165. -4 : Length of block header
  1166. -8 : Length of additional SST header information
  1167. -8 : Arbitrary number to keep within _add_continue() limit = 8208
  1168. */
  1169. $continue_limit = 8208;
  1170. $block_length = 0;
  1171. $written = 0;
  1172. $this->_block_sizes = array();
  1173. $continue = 0;
  1174. foreach (array_keys($this->_str_table) as $string) {
  1175. $string_length = strlen($string);
  1176. $headerinfo = unpack("vlength/Cencoding", $string);
  1177. $encoding = $headerinfo["encoding"];
  1178. $split_string = 0;
  1179. // Block length is the total length of the strings that will be
  1180. // written out in a single SST or CONTINUE block.
  1181. $block_length += $string_length;
  1182. // We can write the string if it doesn't cross a CONTINUE boundary
  1183. if ($block_length < $continue_limit) {
  1184. $written += $string_length;
  1185. continue;
  1186. }
  1187. // Deal with the cases where the next string to be written will exceed
  1188. // the CONTINUE boundary. If the string is very long it may need to be
  1189. // written in more than one CONTINUE record.
  1190. while ($block_length >= $continue_limit) {
  1191. // We need to avoid the case where a string is continued in the first
  1192. // n bytes that contain the string header information.
  1193. $header_length = 3; // Min string + header size -1
  1194. $space_remaining = $continue_limit - $written - $continue;
  1195. /* TODO: Unicode data should only be split on char (2 byte)
  1196. boundaries. Therefore, in some cases we need to reduce the
  1197. amount of available
  1198. */
  1199. $align = 0;
  1200. // Only applies to Unicode strings
  1201. if ($encoding == 1) {
  1202. // Min string + header size -1
  1203. $header_length = 4;
  1204. if ($space_remaining > $header_length) {
  1205. // String contains 3 byte header => split on odd boundary
  1206. if (!$split_string && $space_remaining % 2 != 1) {
  1207. $space_remaining--;
  1208. $align = 1;
  1209. }
  1210. // Split section without header => split on even boundary
  1211. else if ($split_string && $space_remaining % 2 == 1) {
  1212. $space_remaining--;
  1213. $align = 1;
  1214. }
  1215. $split_string = 1;
  1216. }
  1217. }
  1218. if ($space_remaining > $header_length) {
  1219. // Write as much as possible of the string in the current block
  1220. $written += $space_remaining;
  1221. // Reduce the current block length by the amount written
  1222. $block_length -= $continue_limit - $continue - $align;
  1223. // Store the max size for this block
  1224. $this->_block_sizes[] = $continue_limit - $align;
  1225. // If the current string was split then the next CONTINUE block
  1226. // should have the string continue flag (grbit) set unless the
  1227. // split string fits exactly into the remaining space.
  1228. if ($block_length > 0) {
  1229. $continue = 1;
  1230. } else {
  1231. $continue = 0;
  1232. }
  1233. } else {
  1234. // Store the max size for this block
  1235. $this->_block_sizes[] = $written + $continue;
  1236. // Not enough space to start the string in the current block
  1237. $block_length -= $continue_limit - $space_remaining - $continue;
  1238. $continue = 0;
  1239. }
  1240. // If the string (or substr) is small enough we can write it in the
  1241. // new CONTINUE block. Else, go through the loop again to write it in
  1242. // one or more CONTINUE blocks
  1243. if ($block_length < $continue_limit) {
  1244. $written = $block_length;
  1245. } else {
  1246. $written = 0;
  1247. }
  1248. }
  1249. }
  1250. // Store the max size for the last block unless it is empty
  1251. if ($written + $continue) {
  1252. $this->_block_sizes[] = $written + $continue;
  1253. }
  1254. /* Calculate the total length of the SST and associated CONTINUEs (if any).
  1255. The SST record will have a length even if it contains no strings.
  1256. This length is required to set the offsets in the BOUNDSHEET records since
  1257. they must be written before the SST records
  1258. */
  1259. $tmp_block_sizes = array();
  1260. $tmp_block_sizes = $this->_block_sizes;
  1261. $length = 12;
  1262. if (!empty($tmp_block_sizes)) {
  1263. $length += array_shift($tmp_block_sizes); // SST
  1264. }
  1265. while (!empty($tmp_block_sizes)) {
  1266. $length += 4 + array_shift($tmp_block_sizes); // CONTINUEs
  1267. }
  1268. return $length;
  1269. }
  1270. /**
  1271. * Write all of the workbooks strings into an indexed array.
  1272. * See the comments in _calculate_shared_string_sizes() for more information.
  1273. *
  1274. * The Excel documentation says that the SST record should be followed by an
  1275. * EXTSST record. The EXTSST record is a hash table that is used to optimise
  1276. * access to SST. However, despite the documentation it doesn't seem to be
  1277. * required so we will ignore it.
  1278. *
  1279. * @access private
  1280. */
  1281. function _storeSharedStringsTable()
  1282. {
  1283. $record = 0x00fc; // Record identifier
  1284. $length = 0x0008; // Number of bytes to follow
  1285. $total = 0x0000;
  1286. // Iterate through the strings to calculate the CONTINUE block sizes
  1287. $continue_limit = 8208;
  1288. $block_length = 0;
  1289. $written = 0;
  1290. $continue = 0;
  1291. // sizes are upside down
  1292. $tmp_block_sizes = $this->_block_sizes;
  1293. // $tmp_block_sizes = array_reverse($this->_block_sizes);
  1294. // The SST record is required even if it contains no strings. Thus we will
  1295. // always have a length
  1296. //
  1297. if (!empty($tmp_block_sizes)) {
  1298. $length = 8 + array_shift($tmp_block_sizes);
  1299. }
  1300. else {
  1301. // No strings
  1302. $length = 8;
  1303. }
  1304. // Write the SST block header information
  1305. $header = pack("vv", $record, $length);
  1306. $data = pack("VV", $this->_str_total, $this->_str_unique);
  1307. $this->_append($header . $data);
  1308. /* TODO: not good for performance */
  1309. foreach (array_keys($this->_str_table) as $string) {
  1310. $string_length = strlen($string);
  1311. $headerinfo = unpack("vlength/Cencoding", $string);
  1312. $encoding = $headerinfo["encoding"];
  1313. $split_string = 0;
  1314. // Block length is the total length of the strings that will be
  1315. // written out in a single SST or CONTINUE block.
  1316. //
  1317. $block_length += $string_length;
  1318. // We can write the string if it doesn't cross a CONTINUE boundary
  1319. if ($block_length < $continue_limit) {
  1320. $this->_append($string);
  1321. $written += $string_length;
  1322. continue;
  1323. }
  1324. // Deal with the cases where the next string to be written will exceed
  1325. // the CONTINUE boundary. If the string is very long it may need to be
  1326. // written in more than one CONTINUE record.
  1327. //
  1328. while ($block_length >= $continue_limit) {
  1329. // We need to avoid the case where a string is continued in the first
  1330. // n bytes that contain the string header information.
  1331. //
  1332. $header_length = 3; // Min string + header size -1
  1333. $space_remaining = $continue_limit - $written - $continue;
  1334. // Unicode data should only be split on char (2 byte) boundaries.
  1335. // Therefore, in some cases we need to reduce the amount of available
  1336. // space by 1 byte to ensure the correct alignment.
  1337. $align = 0;
  1338. // Only applies to Unicode strings
  1339. if ($encoding == 1) {
  1340. // Min string + header size -1
  1341. $header_length = 4;
  1342. if ($space_remaining > $header_length) {
  1343. // String contains 3 byte header => split on odd boundary
  1344. if (!$split_string && $space_remaining % 2 != 1) {
  1345. $space_remaining--;
  1346. $align = 1;
  1347. }
  1348. // Split section without header => split on even boundary
  1349. else if ($split_string && $space_remaining % 2 == 1) {
  1350. $space_remaining--;
  1351. $align = 1;
  1352. }
  1353. $split_string = 1;
  1354. }
  1355. }
  1356. if ($space_remaining > $header_length) {
  1357. // Write as much as possible of the string in the current block
  1358. $tmp = substr($string, 0, $space_remaining);
  1359. $this->_append($tmp);
  1360. // The remainder will be written in the next block(s)
  1361. $string = substr($string, $space_remaining);
  1362. // Reduce the current block length by the amount written
  1363. $block_length -= $continue_limit - $continue - $align;
  1364. // If the current string was split then the next CONTINUE block
  1365. // should have the string continue flag (grbit) set unless the
  1366. // split string fits exactly into the remaining space.
  1367. //
  1368. if ($block_length > 0) {
  1369. $continue = 1;
  1370. } else {
  1371. $continue = 0;
  1372. }
  1373. } else {
  1374. // Not enough space to start the string in the current block
  1375. $block_length -= $continue_limit - $space_remaining - $continue;
  1376. $continue = 0;
  1377. }
  1378. // Write the CONTINUE block header
  1379. if (!empty($this->_block_sizes)) {
  1380. $record = 0x003C;
  1381. $length = array_shift($tmp_block_sizes);
  1382. $header = pack('vv', $record, $length);
  1383. if ($continue) {
  1384. $header .= pack('C', $encoding);
  1385. }
  1386. $this->_append($header);
  1387. }
  1388. // If the string (or substr) is small enough we can write it in the
  1389. // new CONTINUE block. Else, go through the loop again to write it in
  1390. // one or more CONTINUE blocks
  1391. //
  1392. if ($block_length < $continue_limit) {
  1393. $this->_append($string);
  1394. $written = $block_length;
  1395. } else {
  1396. $written = 0;
  1397. }
  1398. }
  1399. }
  1400. }
  1401. }