adding some strings
[moodle-linuxchix.git] / lib / pear / Spreadsheet / Excel / Writer / Workbook.php
blobac9d3d9a0982fae5b2ffb521e98e90d36250a0ce
1 <?php
2 /*
3 * Module written/ported by Xavier Noguer <xnoguer@rezebra.com>
5 * The majority of this is _NOT_ my code. I simply ported it from the
6 * PERL Spreadsheet::WriteExcel module.
8 * The author of the Spreadsheet::WriteExcel module is John McNamara
9 * <jmcnamara@cpan.org>
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.
15 * License Information:
17 * Spreadsheet_Excel_Writer: A library for generating Excel Spreadsheets
18 * Copyright (c) 2002-2003 Xavier Noguer xnoguer@rezebra.com
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.
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.
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
35 require_once 'Spreadsheet/Excel/Writer/Format.php';
36 require_once 'Spreadsheet/Excel/Writer/BIFFwriter.php';
37 require_once 'Spreadsheet/Excel/Writer/Worksheet.php';
38 require_once 'Spreadsheet/Excel/Writer/Parser.php';
39 require_once 'OLE/PPS/Root.php';
40 require_once 'OLE/PPS/File.php';
42 /**
43 * Class for generating Excel Spreadsheets
45 * @author Xavier Noguer <xnoguer@rezebra.com>
46 * @category FileFormats
47 * @package Spreadsheet_Excel_Writer
50 class Spreadsheet_Excel_Writer_Workbook extends Spreadsheet_Excel_Writer_BIFFwriter
52 /**
53 * Filename for the Workbook
54 * @var string
56 var $_filename;
58 /**
59 * Formula parser
60 * @var object Parser
62 var $_parser;
64 /**
65 * Flag for 1904 date system (0 => base date is 1900, 1 => base date is 1904)
66 * @var integer
68 var $_1904;
70 /**
71 * The active worksheet of the workbook (0 indexed)
72 * @var integer
74 var $_activesheet;
76 /**
77 * 1st displayed worksheet in the workbook (0 indexed)
78 * @var integer
80 var $_firstsheet;
82 /**
83 * Number of workbook tabs selected
84 * @var integer
86 var $_selected;
88 /**
89 * Index for creating adding new formats to the workbook
90 * @var integer
92 var $_xf_index;
94 /**
95 * Flag for preventing close from being called twice.
96 * @var integer
97 * @see close()
99 var $_fileclosed;
102 * The BIFF file size for the workbook.
103 * @var integer
104 * @see _calcSheetOffsets()
106 var $_biffsize;
109 * The default sheetname for all sheets created.
110 * @var string
112 var $_sheetname;
115 * The default XF format.
116 * @var object Format
118 var $_tmp_format;
121 * Array containing references to all of this workbook's worksheets
122 * @var array
124 var $_worksheets;
127 * Array of sheetnames for creating the EXTERNSHEET records
128 * @var array
130 var $_sheetnames;
133 * Array containing references to all of this workbook's formats
134 * @var array
136 var $_formats;
139 * Array containing the colour palette
140 * @var array
142 var $_palette;
145 * The default format for URLs.
146 * @var object Format
148 var $_url_format;
151 * The codepage indicates the text encoding used for strings
152 * @var integer
154 var $_codepage;
157 * The country code used for localization
158 * @var integer
160 var $_country_code;
163 * The temporary dir for storing the OLE file
164 * @var string
166 var $_tmp_dir;
169 * number of bytes for sizeinfo of strings
170 * @var integer
172 var $_string_sizeinfo_size;
175 * Class constructor
177 * @param string filename for storing the workbook. "-" for writing to stdout.
178 * @access public
180 function Spreadsheet_Excel_Writer_Workbook($filename)
182 // It needs to call its parent's constructor explicitly
183 $this->Spreadsheet_Excel_Writer_BIFFwriter();
185 $this->_filename = $filename;
186 $this->_parser =& new Spreadsheet_Excel_Writer_Parser($this->_byte_order, $this->_BIFF_version);
187 $this->_1904 = 0;
188 $this->_activesheet = 0;
189 $this->_firstsheet = 0;
190 $this->_selected = 0;
191 $this->_xf_index = 16; // 15 style XF's and 1 cell XF.
192 $this->_fileclosed = 0;
193 $this->_biffsize = 0;
194 $this->_sheetname = 'Sheet';
195 $this->_tmp_format =& new Spreadsheet_Excel_Writer_Format($this->_BIFF_version);
196 $this->_worksheets = array();
197 $this->_sheetnames = array();
198 $this->_formats = array();
199 $this->_palette = array();
200 $this->_codepage = 0x04E4; // FIXME: should change for BIFF8
201 $this->_country_code = -1;
202 $this->_string_sizeinfo = 3;
204 // Add the default format for hyperlinks
205 $this->_url_format =& $this->addFormat(array('color' => 'blue', 'underline' => 1));
206 $this->_str_total = 0;
207 $this->_str_unique = 0;
208 $this->_str_table = array();
209 $this->_setPaletteXl97();
210 $this->_tmp_dir = '';
214 * Calls finalization methods.
215 * This method should always be the last one to be called on every workbook
217 * @access public
218 * @return mixed true on success. PEAR_Error on failure
220 function close()
222 if ($this->_fileclosed) { // Prevent close() from being called twice.
223 return true;
225 $res = $this->_storeWorkbook();
226 if ($this->isError($res)) {
227 return $this->raiseError($res->getMessage());
229 $this->_fileclosed = 1;
230 return true;
234 * An accessor for the _worksheets[] array
235 * Returns an array of the worksheet objects in a workbook
236 * It actually calls to worksheets()
238 * @access public
239 * @see worksheets()
240 * @return array
242 function sheets()
244 return $this->worksheets();
248 * An accessor for the _worksheets[] array.
249 * Returns an array of the worksheet objects in a workbook
251 * @access public
252 * @return array
254 function worksheets()
256 return $this->_worksheets;
260 * Sets the BIFF version.
261 * This method exists just to access experimental functionality
262 * from BIFF8. It will be deprecated !
263 * Only possible value is 8 (Excel 97/2000).
264 * For any other value it fails silently.
266 * @access public
267 * @param integer $version The BIFF version
269 function setVersion($version)
271 if ($version == 8) { // only accept version 8
272 $version = 0x0600;
273 $this->_BIFF_version = $version;
274 // change BIFFwriter limit for CONTINUE records
275 $this->_limit = 8228;
276 $this->_tmp_format->_BIFF_version = $version;
277 $this->_url_format->_BIFF_version = $version;
278 $this->_parser->_BIFF_version = $version;
280 $total_worksheets = count($this->_worksheets);
281 // change version for all worksheets too
282 for ($i = 0; $i < $total_worksheets; $i++) {
283 $this->_worksheets[$i]->_BIFF_version = $version;
286 $total_formats = count($this->_formats);
287 // change version for all formats too
288 for ($i = 0; $i < $total_formats; $i++) {
289 $this->_formats[$i]->_BIFF_version = $version;
295 * Set the country identifier for the workbook
297 * @access public
298 * @param integer $code Is the international calling country code for the
299 * chosen country.
301 function setCountry($code)
303 $this->_country_code = $code;
307 * Add a new worksheet to the Excel workbook.
308 * If no name is given the name of the worksheet will be Sheeti$i, with
309 * $i in [1..].
311 * @access public
312 * @param string $name the optional name of the worksheet
313 * @return mixed reference to a worksheet object on success, PEAR_Error
314 * on failure
316 function &addWorksheet($name = '')
318 $index = count($this->_worksheets);
319 $sheetname = $this->_sheetname;
321 if ($name == '') {
322 $name = $sheetname.($index+1);
325 // Check that sheetname is <= 31 chars (Excel limit before BIFF8).
326 if ($this->_BIFF_version != 0x0600)
328 if (strlen($name) > 31) {
329 return $this->raiseError("Sheetname $name must be <= 31 chars");
333 // Check that the worksheet name doesn't already exist: a fatal Excel error.
334 $total_worksheets = count($this->_worksheets);
335 for ($i = 0; $i < $total_worksheets; $i++) {
336 if ($this->_worksheets[$i]->getName() == $name) {
337 return $this->raiseError("Worksheet '$name' already exists");
341 $worksheet = new Spreadsheet_Excel_Writer_Worksheet($this->_BIFF_version,
342 $name, $index,
343 $this->_activesheet, $this->_firstsheet,
344 $this->_str_total, $this->_str_unique,
345 $this->_str_table, $this->_url_format,
346 $this->_parser);
348 $this->_worksheets[$index] = &$worksheet; // Store ref for iterator
349 $this->_sheetnames[$index] = $name; // Store EXTERNSHEET names
350 $this->_parser->setExtSheet($name, $index); // Register worksheet name with parser
351 return $worksheet;
355 * Add a new format to the Excel workbook.
356 * Also, pass any properties to the Format constructor.
358 * @access public
359 * @param array $properties array with properties for initializing the format.
360 * @return &Spreadsheet_Excel_Writer_Format reference to an Excel Format
362 function &addFormat($properties = array())
364 $format = new Spreadsheet_Excel_Writer_Format($this->_BIFF_version, $this->_xf_index, $properties);
365 $this->_xf_index += 1;
366 $this->_formats[] = &$format;
367 return $format;
371 * Create new validator.
373 * @access public
374 * @return &Spreadsheet_Excel_Writer_Validator reference to a Validator
376 function &addValidator()
378 include_once 'Spreadsheet/Excel/Writer/Validator.php';
379 /* FIXME: check for successful inclusion*/
380 $valid = new Spreadsheet_Excel_Writer_Validator($this->_parser);
381 return $valid;
385 * Change the RGB components of the elements in the colour palette.
387 * @access public
388 * @param integer $index colour index
389 * @param integer $red red RGB value [0-255]
390 * @param integer $green green RGB value [0-255]
391 * @param integer $blue blue RGB value [0-255]
392 * @return integer The palette index for the custom color
394 function setCustomColor($index, $red, $green, $blue)
396 // Match a HTML #xxyyzz style parameter
397 /*if (defined $_[1] and $_[1] =~ /^#(\w\w)(\w\w)(\w\w)/ ) {
398 @_ = ($_[0], hex $1, hex $2, hex $3);
401 // Check that the colour index is the right range
402 if ($index < 8 or $index > 64) {
403 // TODO: assign real error codes
404 return $this->raiseError("Color index $index outside range: 8 <= index <= 64");
407 // Check that the colour components are in the right range
408 if (($red < 0 or $red > 255) ||
409 ($green < 0 or $green > 255) ||
410 ($blue < 0 or $blue > 255))
412 return $this->raiseError("Color component outside range: 0 <= color <= 255");
415 $index -= 8; // Adjust colour index (wingless dragonfly)
417 // Set the RGB value
418 $this->_palette[$index] = array($red, $green, $blue, 0);
419 return($index + 8);
423 * Sets the colour palette to the Excel 97+ default.
425 * @access private
427 function _setPaletteXl97()
429 $this->_palette = array(
430 array(0x00, 0x00, 0x00, 0x00), // 8
431 array(0xff, 0xff, 0xff, 0x00), // 9
432 array(0xff, 0x00, 0x00, 0x00), // 10
433 array(0x00, 0xff, 0x00, 0x00), // 11
434 array(0x00, 0x00, 0xff, 0x00), // 12
435 array(0xff, 0xff, 0x00, 0x00), // 13
436 array(0xff, 0x00, 0xff, 0x00), // 14
437 array(0x00, 0xff, 0xff, 0x00), // 15
438 array(0x80, 0x00, 0x00, 0x00), // 16
439 array(0x00, 0x80, 0x00, 0x00), // 17
440 array(0x00, 0x00, 0x80, 0x00), // 18
441 array(0x80, 0x80, 0x00, 0x00), // 19
442 array(0x80, 0x00, 0x80, 0x00), // 20
443 array(0x00, 0x80, 0x80, 0x00), // 21
444 array(0xc0, 0xc0, 0xc0, 0x00), // 22
445 array(0x80, 0x80, 0x80, 0x00), // 23
446 array(0x99, 0x99, 0xff, 0x00), // 24
447 array(0x99, 0x33, 0x66, 0x00), // 25
448 array(0xff, 0xff, 0xcc, 0x00), // 26
449 array(0xcc, 0xff, 0xff, 0x00), // 27
450 array(0x66, 0x00, 0x66, 0x00), // 28
451 array(0xff, 0x80, 0x80, 0x00), // 29
452 array(0x00, 0x66, 0xcc, 0x00), // 30
453 array(0xcc, 0xcc, 0xff, 0x00), // 31
454 array(0x00, 0x00, 0x80, 0x00), // 32
455 array(0xff, 0x00, 0xff, 0x00), // 33
456 array(0xff, 0xff, 0x00, 0x00), // 34
457 array(0x00, 0xff, 0xff, 0x00), // 35
458 array(0x80, 0x00, 0x80, 0x00), // 36
459 array(0x80, 0x00, 0x00, 0x00), // 37
460 array(0x00, 0x80, 0x80, 0x00), // 38
461 array(0x00, 0x00, 0xff, 0x00), // 39
462 array(0x00, 0xcc, 0xff, 0x00), // 40
463 array(0xcc, 0xff, 0xff, 0x00), // 41
464 array(0xcc, 0xff, 0xcc, 0x00), // 42
465 array(0xff, 0xff, 0x99, 0x00), // 43
466 array(0x99, 0xcc, 0xff, 0x00), // 44
467 array(0xff, 0x99, 0xcc, 0x00), // 45
468 array(0xcc, 0x99, 0xff, 0x00), // 46
469 array(0xff, 0xcc, 0x99, 0x00), // 47
470 array(0x33, 0x66, 0xff, 0x00), // 48
471 array(0x33, 0xcc, 0xcc, 0x00), // 49
472 array(0x99, 0xcc, 0x00, 0x00), // 50
473 array(0xff, 0xcc, 0x00, 0x00), // 51
474 array(0xff, 0x99, 0x00, 0x00), // 52
475 array(0xff, 0x66, 0x00, 0x00), // 53
476 array(0x66, 0x66, 0x99, 0x00), // 54
477 array(0x96, 0x96, 0x96, 0x00), // 55
478 array(0x00, 0x33, 0x66, 0x00), // 56
479 array(0x33, 0x99, 0x66, 0x00), // 57
480 array(0x00, 0x33, 0x00, 0x00), // 58
481 array(0x33, 0x33, 0x00, 0x00), // 59
482 array(0x99, 0x33, 0x00, 0x00), // 60
483 array(0x99, 0x33, 0x66, 0x00), // 61
484 array(0x33, 0x33, 0x99, 0x00), // 62
485 array(0x33, 0x33, 0x33, 0x00), // 63
490 * Assemble worksheets into a workbook and send the BIFF data to an OLE
491 * storage.
493 * @access private
494 * @return mixed true on success. PEAR_Error on failure
496 function _storeWorkbook()
498 // Ensure that at least one worksheet has been selected.
499 if ($this->_activesheet == 0) {
500 $this->_worksheets[0]->selected = 1;
503 // Calculate the number of selected worksheet tabs and call the finalization
504 // methods for each worksheet
505 $total_worksheets = count($this->_worksheets);
506 for ($i = 0; $i < $total_worksheets; $i++) {
507 if ($this->_worksheets[$i]->selected) {
508 $this->_selected++;
510 $this->_worksheets[$i]->close($this->_sheetnames);
513 // Add Workbook globals
514 $this->_storeBof(0x0005);
515 $this->_storeCodepage();
516 if ($this->_BIFF_version == 0x0600) {
517 $this->_storeWindow1();
519 if ($this->_BIFF_version == 0x0500) {
520 $this->_storeExterns(); // For print area and repeat rows
522 $this->_storeNames(); // For print area and repeat rows
523 if ($this->_BIFF_version == 0x0500) {
524 $this->_storeWindow1();
526 $this->_storeDatemode();
527 $this->_storeAllFonts();
528 $this->_storeAllNumFormats();
529 $this->_storeAllXfs();
530 $this->_storeAllStyles();
531 $this->_storePalette();
532 $this->_calcSheetOffsets();
534 // Add BOUNDSHEET records
535 for ($i = 0; $i < $total_worksheets; $i++) {
536 $this->_storeBoundsheet($this->_worksheets[$i]->name,$this->_worksheets[$i]->offset);
539 if ($this->_country_code != -1) {
540 $this->_storeCountry();
543 if ($this->_BIFF_version == 0x0600) {
544 //$this->_storeSupbookInternal();
545 /* TODO: store external SUPBOOK records and XCT and CRN records
546 in case of external references for BIFF8 */
547 //$this->_storeExternsheetBiff8();
548 $this->_storeSharedStringsTable();
551 // End Workbook globals
552 $this->_storeEof();
554 // Store the workbook in an OLE container
555 $res = $this->_storeOLEFile();
556 if ($this->isError($res)) {
557 return $this->raiseError($res->getMessage());
559 return true;
563 * Sets the temp dir used for storing the OLE file
565 * @access public
566 * @param string $dir The dir to be used as temp dir
567 * @return true if given dir is valid, false otherwise
569 function setTempDir($dir)
571 if (is_dir($dir)) {
572 $this->_tmp_dir = $dir;
573 return true;
575 return false;
579 * Store the workbook in an OLE container
581 * @access private
582 * @return mixed true on success. PEAR_Error on failure
584 function _storeOLEFile()
586 $OLE = new OLE_PPS_File(OLE::Asc2Ucs('Book'));
587 if ($this->_tmp_dir != '') {
588 $OLE->setTempDir($this->_tmp_dir);
590 $res = $OLE->init();
591 if ($this->isError($res)) {
592 return $this->raiseError("OLE Error: ".$res->getMessage());
594 $OLE->append($this->_data);
596 $total_worksheets = count($this->_worksheets);
597 for ($i = 0; $i < $total_worksheets; $i++) {
598 while ($tmp = $this->_worksheets[$i]->getData()) {
599 $OLE->append($tmp);
603 $root = new OLE_PPS_Root(time(), time(), array($OLE));
604 if ($this->_tmp_dir != '') {
605 $root->setTempDir($this->_tmp_dir);
608 $res = $root->save($this->_filename);
609 if ($this->isError($res)) {
610 return $this->raiseError("OLE Error: ".$res->getMessage());
612 return true;
616 * Calculate offsets for Worksheet BOF records.
618 * @access private
620 function _calcSheetOffsets()
622 if ($this->_BIFF_version == 0x0600) {
623 $boundsheet_length = 12; // fixed length for a BOUNDSHEET record
624 } else {
625 $boundsheet_length = 11;
627 $EOF = 4;
628 $offset = $this->_datasize;
630 if ($this->_BIFF_version == 0x0600) {
631 // add the length of the SST
632 /* TODO: check this works for a lot of strings (> 8224 bytes) */
633 $offset += $this->_calculateSharedStringsSizes();
634 if ($this->_country_code != -1) {
635 $offset += 8; // adding COUNTRY record
637 // add the lenght of SUPBOOK, EXTERNSHEET and NAME records
638 //$offset += 8; // FIXME: calculate real value when storing the records
640 $total_worksheets = count($this->_worksheets);
641 // add the length of the BOUNDSHEET records
642 for ($i = 0; $i < $total_worksheets; $i++) {
643 $offset += $boundsheet_length + strlen($this->_worksheets[$i]->name);
645 $offset += $EOF;
647 for ($i = 0; $i < $total_worksheets; $i++) {
648 $this->_worksheets[$i]->offset = $offset;
649 $offset += $this->_worksheets[$i]->_datasize;
651 $this->_biffsize = $offset;
655 * Store the Excel FONT records.
657 * @access private
659 function _storeAllFonts()
661 // tmp_format is added by the constructor. We use this to write the default XF's
662 $format = $this->_tmp_format;
663 $font = $format->getFont();
665 // Note: Fonts are 0-indexed. According to the SDK there is no index 4,
666 // so the following fonts are 0, 1, 2, 3, 5
668 for ($i = 1; $i <= 5; $i++){
669 $this->_append($font);
672 // Iterate through the XF objects and write a FONT record if it isn't the
673 // same as the default FONT and if it hasn't already been used.
675 $fonts = array();
676 $index = 6; // The first user defined FONT
678 $key = $format->getFontKey(); // The default font from _tmp_format
679 $fonts[$key] = 0; // Index of the default font
681 $total_formats = count($this->_formats);
682 for ($i = 0; $i < $total_formats; $i++) {
683 $key = $this->_formats[$i]->getFontKey();
684 if (isset($fonts[$key])) {
685 // FONT has already been used
686 $this->_formats[$i]->font_index = $fonts[$key];
687 } else {
688 // Add a new FONT record
689 $fonts[$key] = $index;
690 $this->_formats[$i]->font_index = $index;
691 $index++;
692 $font = $this->_formats[$i]->getFont();
693 $this->_append($font);
699 * Store user defined numerical formats i.e. FORMAT records
701 * @access private
703 function _storeAllNumFormats()
705 // Leaning num_format syndrome
706 $hash_num_formats = array();
707 $num_formats = array();
708 $index = 164;
710 // Iterate through the XF objects and write a FORMAT record if it isn't a
711 // built-in format type and if the FORMAT string hasn't already been used.
712 $total_formats = count($this->_formats);
713 for ($i = 0; $i < $total_formats; $i++) {
714 $num_format = $this->_formats[$i]->_num_format;
716 // Check if $num_format is an index to a built-in format.
717 // Also check for a string of zeros, which is a valid format string
718 // but would evaluate to zero.
720 if (!preg_match("/^0+\d/", $num_format)) {
721 if (preg_match("/^\d+$/", $num_format)) { // built-in format
722 continue;
726 if (isset($hash_num_formats[$num_format])) {
727 // FORMAT has already been used
728 $this->_formats[$i]->_num_format = $hash_num_formats[$num_format];
729 } else{
730 // Add a new FORMAT
731 $hash_num_formats[$num_format] = $index;
732 $this->_formats[$i]->_num_format = $index;
733 array_push($num_formats,$num_format);
734 $index++;
738 // Write the new FORMAT records starting from 0xA4
739 $index = 164;
740 foreach ($num_formats as $num_format) {
741 $this->_storeNumFormat($num_format,$index);
742 $index++;
747 * Write all XF records.
749 * @access private
751 function _storeAllXfs()
753 // _tmp_format is added by the constructor. We use this to write the default XF's
754 // The default font index is 0
756 $format = $this->_tmp_format;
757 for ($i = 0; $i <= 14; $i++) {
758 $xf = $format->getXf('style'); // Style XF
759 $this->_append($xf);
762 $xf = $format->getXf('cell'); // Cell XF
763 $this->_append($xf);
765 // User defined XFs
766 $total_formats = count($this->_formats);
767 for ($i = 0; $i < $total_formats; $i++) {
768 $xf = $this->_formats[$i]->getXf('cell');
769 $this->_append($xf);
774 * Write all STYLE records.
776 * @access private
778 function _storeAllStyles()
780 $this->_storeStyle();
784 * Write the EXTERNCOUNT and EXTERNSHEET records. These are used as indexes for
785 * the NAME records.
787 * @access private
789 function _storeExterns()
791 // Create EXTERNCOUNT with number of worksheets
792 $this->_storeExterncount(count($this->_worksheets));
794 // Create EXTERNSHEET for each worksheet
795 foreach ($this->_sheetnames as $sheetname) {
796 $this->_storeExternsheet($sheetname);
801 * Write the NAME record to define the print area and the repeat rows and cols.
803 * @access private
805 function _storeNames()
807 // Create the print area NAME records
808 $total_worksheets = count($this->_worksheets);
809 for ($i = 0; $i < $total_worksheets; $i++) {
810 // Write a Name record if the print area has been defined
811 if (isset($this->_worksheets[$i]->print_rowmin)) {
812 $this->_storeNameShort(
813 $this->_worksheets[$i]->index,
814 0x06, // NAME type
815 $this->_worksheets[$i]->print_rowmin,
816 $this->_worksheets[$i]->print_rowmax,
817 $this->_worksheets[$i]->print_colmin,
818 $this->_worksheets[$i]->print_colmax
823 // Create the print title NAME records
824 $total_worksheets = count($this->_worksheets);
825 for ($i = 0; $i < $total_worksheets; $i++) {
826 $rowmin = $this->_worksheets[$i]->title_rowmin;
827 $rowmax = $this->_worksheets[$i]->title_rowmax;
828 $colmin = $this->_worksheets[$i]->title_colmin;
829 $colmax = $this->_worksheets[$i]->title_colmax;
831 // Determine if row + col, row, col or nothing has been defined
832 // and write the appropriate record
834 if (isset($rowmin) && isset($colmin)) {
835 // Row and column titles have been defined.
836 // Row title has been defined.
837 $this->_storeNameLong(
838 $this->_worksheets[$i]->index,
839 0x07, // NAME type
840 $rowmin,
841 $rowmax,
842 $colmin,
843 $colmax
845 } elseif (isset($rowmin)) {
846 // Row title has been defined.
847 $this->_storeNameShort(
848 $this->_worksheets[$i]->index,
849 0x07, // NAME type
850 $rowmin,
851 $rowmax,
852 0x00,
853 0xff
855 } elseif (isset($colmin)) {
856 // Column title has been defined.
857 $this->_storeNameShort(
858 $this->_worksheets[$i]->index,
859 0x07, // NAME type
860 0x0000,
861 0x3fff,
862 $colmin,
863 $colmax
865 } else {
866 // Print title hasn't been defined.
874 /******************************************************************************
876 * BIFF RECORDS
881 * Stores the CODEPAGE biff record.
883 * @access private
885 function _storeCodepage()
887 $record = 0x0042; // Record identifier
888 $length = 0x0002; // Number of bytes to follow
889 $cv = $this->_codepage; // The code page
891 $header = pack('vv', $record, $length);
892 $data = pack('v', $cv);
894 $this->_append($header . $data);
898 * Write Excel BIFF WINDOW1 record.
900 * @access private
902 function _storeWindow1()
904 $record = 0x003D; // Record identifier
905 $length = 0x0012; // Number of bytes to follow
907 $xWn = 0x0000; // Horizontal position of window
908 $yWn = 0x0000; // Vertical position of window
909 $dxWn = 0x25BC; // Width of window
910 $dyWn = 0x1572; // Height of window
912 $grbit = 0x0038; // Option flags
913 $ctabsel = $this->_selected; // Number of workbook tabs selected
914 $wTabRatio = 0x0258; // Tab to scrollbar ratio
916 $itabFirst = $this->_firstsheet; // 1st displayed worksheet
917 $itabCur = $this->_activesheet; // Active worksheet
919 $header = pack("vv", $record, $length);
920 $data = pack("vvvvvvvvv", $xWn, $yWn, $dxWn, $dyWn,
921 $grbit,
922 $itabCur, $itabFirst,
923 $ctabsel, $wTabRatio);
924 $this->_append($header . $data);
928 * Writes Excel BIFF BOUNDSHEET record.
929 * FIXME: inconsistent with BIFF documentation
931 * @param string $sheetname Worksheet name
932 * @param integer $offset Location of worksheet BOF
933 * @access private
935 function _storeBoundsheet($sheetname,$offset)
937 $record = 0x0085; // Record identifier
938 if ($this->_BIFF_version == 0x0600) {
939 $length = 0x08 + strlen($sheetname); // Number of bytes to follow
940 } else {
941 $length = 0x07 + strlen($sheetname); // Number of bytes to follow
944 $grbit = 0x0000; // Visibility and sheet type
945 $cch = strlen($sheetname); // Length of sheet name
947 $header = pack("vv", $record, $length);
948 if ($this->_BIFF_version == 0x0600) {
949 $data = pack("Vvv", $offset, $grbit, $cch);
950 } else {
951 $data = pack("VvC", $offset, $grbit, $cch);
953 $this->_append($header.$data.$sheetname);
957 * Write Internal SUPBOOK record
959 * @access private
961 function _storeSupbookInternal()
963 $record = 0x01AE; // Record identifier
964 $length = 0x0004; // Bytes to follow
966 $header = pack("vv", $record, $length);
967 $data = pack("vv", count($this->_worksheets), 0x0104);
968 $this->_append($header . $data);
972 * Writes the Excel BIFF EXTERNSHEET record. These references are used by
973 * formulas.
975 * @param string $sheetname Worksheet name
976 * @access private
978 function _storeExternsheetBiff8()
980 $total_references = count($this->_parser->_references);
981 $record = 0x0017; // Record identifier
982 $length = 2 + 6 * $total_references; // Number of bytes to follow
984 $supbook_index = 0; // FIXME: only using internal SUPBOOK record
985 $header = pack("vv", $record, $length);
986 $data = pack('v', $total_references);
987 for ($i = 0; $i < $total_references; $i++) {
988 $data .= $this->_parser->_references[$i];
990 $this->_append($header . $data);
994 * Write Excel BIFF STYLE records.
996 * @access private
998 function _storeStyle()
1000 $record = 0x0293; // Record identifier
1001 $length = 0x0004; // Bytes to follow
1003 $ixfe = 0x8000; // Index to style XF
1004 $BuiltIn = 0x00; // Built-in style
1005 $iLevel = 0xff; // Outline style level
1007 $header = pack("vv", $record, $length);
1008 $data = pack("vCC", $ixfe, $BuiltIn, $iLevel);
1009 $this->_append($header . $data);
1014 * Writes Excel FORMAT record for non "built-in" numerical formats.
1016 * @param string $format Custom format string
1017 * @param integer $ifmt Format index code
1018 * @access private
1020 function _storeNumFormat($format, $ifmt)
1022 $record = 0x041E; // Record identifier
1024 if ($this->_BIFF_version == 0x0600) {
1025 $length = 5 + strlen($format); // Number of bytes to follow
1026 $encoding = 0x0;
1027 } elseif ($this->_BIFF_version == 0x0500) {
1028 $length = 3 + strlen($format); // Number of bytes to follow
1031 $cch = strlen($format); // Length of format string
1033 $header = pack("vv", $record, $length);
1034 if ($this->_BIFF_version == 0x0600) {
1035 $data = pack("vvC", $ifmt, $cch, $encoding);
1036 } elseif ($this->_BIFF_version == 0x0500) {
1037 $data = pack("vC", $ifmt, $cch);
1039 $this->_append($header . $data . $format);
1043 * Write DATEMODE record to indicate the date system in use (1904 or 1900).
1045 * @access private
1047 function _storeDatemode()
1049 $record = 0x0022; // Record identifier
1050 $length = 0x0002; // Bytes to follow
1052 $f1904 = $this->_1904; // Flag for 1904 date system
1054 $header = pack("vv", $record, $length);
1055 $data = pack("v", $f1904);
1056 $this->_append($header . $data);
1061 * Write BIFF record EXTERNCOUNT to indicate the number of external sheet
1062 * references in the workbook.
1064 * Excel only stores references to external sheets that are used in NAME.
1065 * The workbook NAME record is required to define the print area and the repeat
1066 * rows and columns.
1068 * A similar method is used in Worksheet.php for a slightly different purpose.
1070 * @param integer $cxals Number of external references
1071 * @access private
1073 function _storeExterncount($cxals)
1075 $record = 0x0016; // Record identifier
1076 $length = 0x0002; // Number of bytes to follow
1078 $header = pack("vv", $record, $length);
1079 $data = pack("v", $cxals);
1080 $this->_append($header . $data);
1085 * Writes the Excel BIFF EXTERNSHEET record. These references are used by
1086 * formulas. NAME record is required to define the print area and the repeat
1087 * rows and columns.
1089 * A similar method is used in Worksheet.php for a slightly different purpose.
1091 * @param string $sheetname Worksheet name
1092 * @access private
1094 function _storeExternsheet($sheetname)
1096 $record = 0x0017; // Record identifier
1097 $length = 0x02 + strlen($sheetname); // Number of bytes to follow
1099 $cch = strlen($sheetname); // Length of sheet name
1100 $rgch = 0x03; // Filename encoding
1102 $header = pack("vv", $record, $length);
1103 $data = pack("CC", $cch, $rgch);
1104 $this->_append($header . $data . $sheetname);
1109 * Store the NAME record in the short format that is used for storing the print
1110 * area, repeat rows only and repeat columns only.
1112 * @param integer $index Sheet index
1113 * @param integer $type Built-in name type
1114 * @param integer $rowmin Start row
1115 * @param integer $rowmax End row
1116 * @param integer $colmin Start colum
1117 * @param integer $colmax End column
1118 * @access private
1120 function _storeNameShort($index, $type, $rowmin, $rowmax, $colmin, $colmax)
1122 $record = 0x0018; // Record identifier
1123 $length = 0x0024; // Number of bytes to follow
1125 $grbit = 0x0020; // Option flags
1126 $chKey = 0x00; // Keyboard shortcut
1127 $cch = 0x01; // Length of text name
1128 $cce = 0x0015; // Length of text definition
1129 $ixals = $index + 1; // Sheet index
1130 $itab = $ixals; // Equal to ixals
1131 $cchCustMenu = 0x00; // Length of cust menu text
1132 $cchDescription = 0x00; // Length of description text
1133 $cchHelptopic = 0x00; // Length of help topic text
1134 $cchStatustext = 0x00; // Length of status bar text
1135 $rgch = $type; // Built-in name type
1137 $unknown03 = 0x3b;
1138 $unknown04 = 0xffff-$index;
1139 $unknown05 = 0x0000;
1140 $unknown06 = 0x0000;
1141 $unknown07 = 0x1087;
1142 $unknown08 = 0x8005;
1144 $header = pack("vv", $record, $length);
1145 $data = pack("v", $grbit);
1146 $data .= pack("C", $chKey);
1147 $data .= pack("C", $cch);
1148 $data .= pack("v", $cce);
1149 $data .= pack("v", $ixals);
1150 $data .= pack("v", $itab);
1151 $data .= pack("C", $cchCustMenu);
1152 $data .= pack("C", $cchDescription);
1153 $data .= pack("C", $cchHelptopic);
1154 $data .= pack("C", $cchStatustext);
1155 $data .= pack("C", $rgch);
1156 $data .= pack("C", $unknown03);
1157 $data .= pack("v", $unknown04);
1158 $data .= pack("v", $unknown05);
1159 $data .= pack("v", $unknown06);
1160 $data .= pack("v", $unknown07);
1161 $data .= pack("v", $unknown08);
1162 $data .= pack("v", $index);
1163 $data .= pack("v", $index);
1164 $data .= pack("v", $rowmin);
1165 $data .= pack("v", $rowmax);
1166 $data .= pack("C", $colmin);
1167 $data .= pack("C", $colmax);
1168 $this->_append($header . $data);
1173 * Store the NAME record in the long format that is used for storing the repeat
1174 * rows and columns when both are specified. This shares a lot of code with
1175 * _storeNameShort() but we use a separate method to keep the code clean.
1176 * Code abstraction for reuse can be carried too far, and I should know. ;-)
1178 * @param integer $index Sheet index
1179 * @param integer $type Built-in name type
1180 * @param integer $rowmin Start row
1181 * @param integer $rowmax End row
1182 * @param integer $colmin Start colum
1183 * @param integer $colmax End column
1184 * @access private
1186 function _storeNameLong($index, $type, $rowmin, $rowmax, $colmin, $colmax)
1188 $record = 0x0018; // Record identifier
1189 $length = 0x003d; // Number of bytes to follow
1190 $grbit = 0x0020; // Option flags
1191 $chKey = 0x00; // Keyboard shortcut
1192 $cch = 0x01; // Length of text name
1193 $cce = 0x002e; // Length of text definition
1194 $ixals = $index + 1; // Sheet index
1195 $itab = $ixals; // Equal to ixals
1196 $cchCustMenu = 0x00; // Length of cust menu text
1197 $cchDescription = 0x00; // Length of description text
1198 $cchHelptopic = 0x00; // Length of help topic text
1199 $cchStatustext = 0x00; // Length of status bar text
1200 $rgch = $type; // Built-in name type
1202 $unknown01 = 0x29;
1203 $unknown02 = 0x002b;
1204 $unknown03 = 0x3b;
1205 $unknown04 = 0xffff-$index;
1206 $unknown05 = 0x0000;
1207 $unknown06 = 0x0000;
1208 $unknown07 = 0x1087;
1209 $unknown08 = 0x8008;
1211 $header = pack("vv", $record, $length);
1212 $data = pack("v", $grbit);
1213 $data .= pack("C", $chKey);
1214 $data .= pack("C", $cch);
1215 $data .= pack("v", $cce);
1216 $data .= pack("v", $ixals);
1217 $data .= pack("v", $itab);
1218 $data .= pack("C", $cchCustMenu);
1219 $data .= pack("C", $cchDescription);
1220 $data .= pack("C", $cchHelptopic);
1221 $data .= pack("C", $cchStatustext);
1222 $data .= pack("C", $rgch);
1223 $data .= pack("C", $unknown01);
1224 $data .= pack("v", $unknown02);
1225 // Column definition
1226 $data .= pack("C", $unknown03);
1227 $data .= pack("v", $unknown04);
1228 $data .= pack("v", $unknown05);
1229 $data .= pack("v", $unknown06);
1230 $data .= pack("v", $unknown07);
1231 $data .= pack("v", $unknown08);
1232 $data .= pack("v", $index);
1233 $data .= pack("v", $index);
1234 $data .= pack("v", 0x0000);
1235 $data .= pack("v", 0x3fff);
1236 $data .= pack("C", $colmin);
1237 $data .= pack("C", $colmax);
1238 // Row definition
1239 $data .= pack("C", $unknown03);
1240 $data .= pack("v", $unknown04);
1241 $data .= pack("v", $unknown05);
1242 $data .= pack("v", $unknown06);
1243 $data .= pack("v", $unknown07);
1244 $data .= pack("v", $unknown08);
1245 $data .= pack("v", $index);
1246 $data .= pack("v", $index);
1247 $data .= pack("v", $rowmin);
1248 $data .= pack("v", $rowmax);
1249 $data .= pack("C", 0x00);
1250 $data .= pack("C", 0xff);
1251 // End of data
1252 $data .= pack("C", 0x10);
1253 $this->_append($header . $data);
1257 * Stores the COUNTRY record for localization
1259 * @access private
1261 function _storeCountry()
1263 $record = 0x008C; // Record identifier
1264 $length = 4; // Number of bytes to follow
1266 $header = pack('vv', $record, $length);
1267 /* using the same country code always for simplicity */
1268 $data = pack('vv', $this->_country_code, $this->_country_code);
1269 $this->_append($header . $data);
1273 * Stores the PALETTE biff record.
1275 * @access private
1277 function _storePalette()
1279 $aref = $this->_palette;
1281 $record = 0x0092; // Record identifier
1282 $length = 2 + 4 * count($aref); // Number of bytes to follow
1283 $ccv = count($aref); // Number of RGB values to follow
1284 $data = ''; // The RGB data
1286 // Pack the RGB data
1287 foreach ($aref as $color) {
1288 foreach ($color as $byte) {
1289 $data .= pack("C",$byte);
1293 $header = pack("vvv", $record, $length, $ccv);
1294 $this->_append($header . $data);
1298 * Calculate
1299 * Handling of the SST continue blocks is complicated by the need to include an
1300 * additional continuation byte depending on whether the string is split between
1301 * blocks or whether it starts at the beginning of the block. (There are also
1302 * additional complications that will arise later when/if Rich Strings are
1303 * supported).
1305 * @access private
1307 function _calculateSharedStringsSizes()
1309 /* Iterate through the strings to calculate the CONTINUE block sizes.
1310 For simplicity we use the same size for the SST and CONTINUE records:
1311 8228 : Maximum Excel97 block size
1312 -4 : Length of block header
1313 -8 : Length of additional SST header information
1314 = 8216
1316 $continue_limit = 8216;
1317 $block_length = 0;
1318 $written = 0;
1319 $this->_block_sizes = array();
1320 $continue = 0;
1322 foreach (array_keys($this->_str_table) as $string) {
1323 $string_length = strlen($string);
1325 // Block length is the total length of the strings that will be
1326 // written out in a single SST or CONTINUE block.
1327 $block_length += $string_length;
1329 // We can write the string if it doesn't cross a CONTINUE boundary
1330 if ($block_length < $continue_limit) {
1331 $written += $string_length;
1332 continue;
1335 // Deal with the cases where the next string to be written will exceed
1336 // the CONTINUE boundary. If the string is very long it may need to be
1337 // written in more than one CONTINUE record.
1338 while ($block_length >= $continue_limit) {
1340 // We need to avoid the case where a string is continued in the first
1341 // n bytes that contain the string header information.
1342 $header_length = 3; // Min string + header size -1
1343 $space_remaining = $continue_limit - $written - $continue;
1346 /* TODO: Unicode data should only be split on char (2 byte)
1347 boundaries. Therefore, in some cases we need to reduce the
1348 amount of available
1351 if ($space_remaining > $header_length) {
1352 // Write as much as possible of the string in the current block
1353 $written += $space_remaining;
1355 // Reduce the current block length by the amount written
1356 $block_length -= $continue_limit - $continue;
1358 // Store the max size for this block
1359 $this->_block_sizes[] = $continue_limit;
1361 // If the current string was split then the next CONTINUE block
1362 // should have the string continue flag (grbit) set unless the
1363 // split string fits exactly into the remaining space.
1364 if ($block_length > 0) {
1365 $continue = 1;
1366 } else {
1367 $continue = 0;
1369 } else {
1370 // Store the max size for this block
1371 $this->_block_sizes[] = $written + $continue;
1373 // Not enough space to start the string in the current block
1374 $block_length -= $continue_limit - $space_remaining - $continue;
1375 $continue = 0;
1379 // If the string (or substr) is small enough we can write it in the
1380 // new CONTINUE block. Else, go through the loop again to write it in
1381 // one or more CONTINUE blocks
1382 if ($block_length < $continue_limit) {
1383 $written = $block_length;
1384 } else {
1385 $written = 0;
1390 // Store the max size for the last block unless it is empty
1391 if ($written + $continue) {
1392 $this->_block_sizes[] = $written + $continue;
1396 /* Calculate the total length of the SST and associated CONTINUEs (if any).
1397 The SST record will have a length even if it contains no strings.
1398 This length is required to set the offsets in the BOUNDSHEET records since
1399 they must be written before the SST records
1401 $total_offset = array_sum($this->_block_sizes);
1402 // SST information
1403 $total_offset += 8;
1404 if (!empty($this->_block_sizes)) {
1405 $total_offset += (count($this->_block_sizes)) * 4; // add CONTINUE headers
1407 return $total_offset;
1411 * Write all of the workbooks strings into an indexed array.
1412 * See the comments in _calculate_shared_string_sizes() for more information.
1414 * The Excel documentation says that the SST record should be followed by an
1415 * EXTSST record. The EXTSST record is a hash table that is used to optimise
1416 * access to SST. However, despite the documentation it doesn't seem to be
1417 * required so we will ignore it.
1419 * @access private
1421 function _storeSharedStringsTable()
1423 $record = 0x00fc; // Record identifier
1424 // sizes are upside down
1425 $this->_block_sizes = array_reverse($this->_block_sizes);
1426 $length = array_pop($this->_block_sizes) + 8; // First block size plus SST information
1428 // Write the SST block header information
1429 $header = pack("vv", $record, $length);
1430 $data = pack("VV", $this->_str_total, $this->_str_unique);
1431 $this->_append($header . $data);
1434 // Iterate through the strings to calculate the CONTINUE block sizes
1435 $continue_limit = 8216;
1436 $block_length = 0;
1437 $written = 0;
1438 $continue = 0;
1441 /* TODO: not good for performance */
1442 foreach (array_keys($this->_str_table) as $string) {
1444 $string_length = strlen($string);
1445 $encoding = 0; // assume there are no Unicode strings
1446 $split_string = 0;
1448 // Block length is the total length of the strings that will be
1449 // written out in a single SST or CONTINUE block.
1451 $block_length += $string_length;
1454 // We can write the string if it doesn't cross a CONTINUE boundary
1455 if ($block_length < $continue_limit) {
1456 $this->_append($string);
1457 $written += $string_length;
1458 continue;
1461 // Deal with the cases where the next string to be written will exceed
1462 // the CONTINUE boundary. If the string is very long it may need to be
1463 // written in more than one CONTINUE record.
1465 while ($block_length >= $continue_limit) {
1467 // We need to avoid the case where a string is continued in the first
1468 // n bytes that contain the string header information.
1470 $header_length = 3; // Min string + header size -1
1471 $space_remaining = $continue_limit - $written - $continue;
1474 // Unicode data should only be split on char (2 byte) boundaries.
1475 // Therefore, in some cases we need to reduce the amount of available
1477 if ($space_remaining > $header_length) {
1478 // Write as much as possible of the string in the current block
1479 $tmp = substr($string, 0, $space_remaining);
1480 $this->_append($tmp);
1482 // The remainder will be written in the next block(s)
1483 $string = substr($string, $space_remaining);
1485 // Reduce the current block length by the amount written
1486 $block_length -= $continue_limit - $continue;
1488 // If the current string was split then the next CONTINUE block
1489 // should have the string continue flag (grbit) set unless the
1490 // split string fits exactly into the remaining space.
1492 if ($block_length > 0) {
1493 $continue = 1;
1494 } else {
1495 $continue = 0;
1497 } else {
1498 // Not enough space to start the string in the current block
1499 $block_length -= $continue_limit - $space_remaining - $continue;
1500 $continue = 0;
1503 // Write the CONTINUE block header
1504 if (!empty($this->_block_sizes)) {
1505 $record = 0x003C;
1506 $length = array_pop($this->_block_sizes);
1507 $header = pack('vv', $record, $length);
1508 if ($continue) {
1509 $header .= pack('C', $encoding);
1511 $this->_append($header);
1514 // If the string (or substr) is small enough we can write it in the
1515 // new CONTINUE block. Else, go through the loop again to write it in
1516 // one or more CONTINUE blocks
1518 if ($block_length < $continue_limit) {
1519 $this->_append($string);
1520 $written = $block_length;
1521 } else {
1522 $written = 0;