Fixed more php 8.2 deprecation bugs.
[fa-stable.git] / reporting / includes / Workbook.php
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
35 /**
36 * @const SPREADSHEET_EXCEL_WRITER_ADD token identifier for character "+"
37 */
38 define('SPREADSHEET_EXCEL_WRITER_ADD', "+");
39
40 /**
41 * @const SPREADSHEET_EXCEL_WRITER_SUB token identifier for character "-"
42 */
43 define('SPREADSHEET_EXCEL_WRITER_SUB', "-");
44
45 /**
46 * @const SPREADSHEET_EXCEL_WRITER_MUL token identifier for character "*"
47 */
48 define('SPREADSHEET_EXCEL_WRITER_MUL', "*");
49
50 /**
51 * @const SPREADSHEET_EXCEL_WRITER_DIV token identifier for character "/"
52 */
53 define('SPREADSHEET_EXCEL_WRITER_DIV', "/");
54
55 /**
56 * @const SPREADSHEET_EXCEL_WRITER_OPEN token identifier for character "("
57 */
58 define('SPREADSHEET_EXCEL_WRITER_OPEN', "(");
59
60 /**
61 * @const SPREADSHEET_EXCEL_WRITER_CLOSE token identifier for character ")"
62 */
63 define('SPREADSHEET_EXCEL_WRITER_CLOSE', ")");
64
65 /**
66 * @const SPREADSHEET_EXCEL_WRITER_COMA token identifier for character ","
67 */
68 define('SPREADSHEET_EXCEL_WRITER_COMA', ",");
69
70 /**
71 * @const SPREADSHEET_EXCEL_WRITER_SEMICOLON token identifier for character ";"
72 */
73 define('SPREADSHEET_EXCEL_WRITER_SEMICOLON', ";");
74
75 /**
76 * @const SPREADSHEET_EXCEL_WRITER_GT token identifier for character ">"
77 */
78 define('SPREADSHEET_EXCEL_WRITER_GT', ">");
79
80 /**
81 * @const SPREADSHEET_EXCEL_WRITER_LT token identifier for character "<"
82 */
83 define('SPREADSHEET_EXCEL_WRITER_LT', "<");
84
85 /**
86 * @const SPREADSHEET_EXCEL_WRITER_LE token identifier for character "<="
87 */
88 define('SPREADSHEET_EXCEL_WRITER_LE', "<=");
89
90 /**
91 * @const SPREADSHEET_EXCEL_WRITER_GE token identifier for character ">="
92 */
93 define('SPREADSHEET_EXCEL_WRITER_GE', ">=");
94
95 /**
96 * @const SPREADSHEET_EXCEL_WRITER_EQ token identifier for character "="
97 */
98 define('SPREADSHEET_EXCEL_WRITER_EQ', "=");
99
100 /**
101 * @const SPREADSHEET_EXCEL_WRITER_NE token identifier for character "<>"
102 */
103 define('SPREADSHEET_EXCEL_WRITER_NE', "<>");
104
105 define('PpsType_Root', 5);
106 define('PpsType_Dir', 1);
107 define('PpsType_File', 2);
108 define('DataSizeSmall', 0x1000);
109 define('LongIntSize', 4);
110 define('PpsSize', 0x80);
111
112 function Asc2Ucs($sAsc) 
113 {
114     return implode("\x00", (preg_split('//', $sAsc, -1, PREG_SPLIT_NO_EMPTY)))."\x00";
115 }
116
117 function Ucs2Asc($sUcs) 
118 {
119     $chars=explode("\x00", $sUcs);
120     array_pop($chars);
121     return implode("", $chars);
122 }
123
124 function OLEDate2Local($sDateTime) 
125 {
126 }
127
128 #------------------------------------------------------------------------------
129 # Localtime->OLE Date
130 #------------------------------------------------------------------------------
131 function LocalDate2OLE($raDate) 
132 {
133 }
134
135 function _leapYear($iYear) 
136 {
137     return ((($iYear % 4)==0) && (($iYear % 100) || ($iYear % 400)==0)) ? 1 : 0;
138 }
139
140 function _yearDays($iYear) 
141 {
142     return _leapYear($iYear) ? 366 : 365;
143 }
144
145 function _monthDays($iMon, $iYear) 
146 {
147     if ($iMon == 1 || $iMon ==  3 || $iMon ==  5 || $iMon == 7 ||
148         $iMon == 8 || $iMon == 10 || $iMon == 12) 
149         return 31;
150     elseif ($iMon == 4 || $iMon == 6 || $iMon == 9 || $iMon == 11) 
151         return 30;
152     elseif ($iMon == 2) 
153         return _leapYear($iYear) ? 29 : 28;
154 }
155
156 /*
157  * This is the OLE::Storage_Lite Perl package ported to PHP
158  * OLE::Storage_Lite was written by Kawai Takanori, kwitknr@cpan.org
159  */
160
161 class ole_pps 
162 {
163     var $No;
164     var $Name;
165     var $Type;
166     var $PrevPps;
167     var $NextPps;
168     var $DirPps;
169     var $Time1st;
170     var $Time2nd;
171     var $StartBlock;
172     var $Size;
173     var $Data;
174     var $Child;
175     var $_PPS_FILE;
176
177     #------------------------------------------------------------------------------
178     # _new (OLE::Storage_Lite::PPS)
179     #   for OLE::Storage_Lite
180     #------------------------------------------------------------------------------
181     function __construct($iNo, $sNm, $iType, $iPrev, $iNext, $iDir,
182                      $raTime1st, $raTime2nd, $iStart, $iSize,
183                      $sData=false, $raChild=false) 
184     {
185         #1. Constructor for OLE::Storage_Lite
186
187         $this->No         = $iNo;
188         $this->Name       = $sNm;
189         $this->Type       = $iType;
190         $this->PrevPps    = $iPrev;
191         $this->NextPps    = $iNext;
192         $this->DirPps     = $iDir;
193         $this->Time1st    = $raTime1st;
194         $this->Time2nd    = $raTime2nd;
195         $this->StartBlock = $iStart;
196         $this->Size       = $iSize;
197         $this->Data       = $sData;
198         $this->Child      = $raChild;
199         $this->_PPS_FILE  = NULL;
200     }
201
202     #------------------------------------------------------------------------------
203     # _DataLen (OLE::Storage_Lite::PPS)
204     # Check for update
205     #------------------------------------------------------------------------------
206     function _DataLen() 
207     {
208         if ($this->Data===false) 
209             return 0;
210
211         if ($this->_PPS_FILE) 
212             return filesize($this->_PPS_FILE);
213
214         return strlen($this->Data);
215     }
216
217     #------------------------------------------------------------------------------
218     # _makeSmallData (OLE::Storage_Lite::PPS)
219     #------------------------------------------------------------------------------
220     function _makeSmallData(&$aList, $rhInfo) 
221     {
222         //my ($sRes);
223         $FILE = $rhInfo->_FILEH_;
224         $iSmBlk = 0;
225         $sRes = '';
226
227         for ($c=0;$c<sizeof($aList);$c++) 
228         {
229             $oPps=&$aList[$c];
230
231             #1. Make SBD, small data string
232
233             if ($oPps->Type==PpsType_File) 
234             {
235                 if ($oPps->Size<=0) 
236                     continue;
237
238                 if($oPps->Size < $rhInfo->_SMALL_SIZE) 
239                 {
240                     $iSmbCnt = floor($oPps->Size / $rhInfo->_SMALL_BLOCK_SIZE) +
241                                (($oPps->Size % $rhInfo->_SMALL_BLOCK_SIZE) ? 1 : 0);
242                     #1.1 Add to SBD
243                     for ($i = 0; $i<($iSmbCnt-1); $i++) 
244                         fputs($FILE, pack("V", $i+$iSmBlk+1));
245                     fputs($FILE, pack("V", -2));
246
247                     #1.2 Add to Data String(this will be written for RootEntry)
248                     #Check for update
249                     if ($oPps->_PPS_FILE) 
250                     {
251                         //my $sBuff;
252                         fseek($oPps->_PPS_FILE, 0, SEEK_SET); #To The Top
253                         while ($sBuff=fread($oPps->_PPS_FILE, 4096)) 
254                             $sRes .= $sBuff;
255                     } 
256                     else 
257                         $sRes .= $oPps->Data;
258                     if($oPps->Size % $rhInfo->_SMALL_BLOCK_SIZE) 
259                     {
260                         $sRes .= (str_repeat("\x00",
261                                   ($rhInfo->_SMALL_BLOCK_SIZE -
262                                   ($oPps->Size % $rhInfo->_SMALL_BLOCK_SIZE))));
263                     }
264                     #1.3 Set for PPS
265                     $oPps->StartBlock = $iSmBlk;
266                     $iSmBlk += $iSmbCnt;
267                 }
268             }
269
270         }
271         $iSbCnt = floor($rhInfo->_BIG_BLOCK_SIZE / LongIntSize);
272         if ($iSmBlk  % $iSbCnt) 
273             fputs($FILE, str_repeat(pack("V", -1), $iSbCnt - ($iSmBlk % $iSbCnt)));
274         #2. Write SBD with adjusting length for block
275         return $sRes;
276     }
277
278     #------------------------------------------------------------------------------
279     # _savePpsWk (OLE::Storage_Lite::PPS)
280     #------------------------------------------------------------------------------
281     function _savePpsWk($rhInfo) 
282     {
283         #1. Write PPS
284         $FILE=$rhInfo->_FILEH_;
285         fputs($FILE,
286               $this->Name.
287               str_repeat("\x00", 64 - strlen($this->Name)).      #  64
288               pack("v", strlen($this->Name) + 2).                #  66
289               pack("c", $this->Type).                            #  67
290               pack("c", 0x00). #UK                               #  68
291               pack("V", $this->PrevPps). #Prev                   #  72
292               pack("V", $this->NextPps). #Next                   #  76
293               pack("V", $this->DirPps).  #Dir                    #  80
294               "\x00\x09\x02\x00".                                #  84
295               "\x00\x00\x00\x00".                                #  88
296               "\xc0\x00\x00\x00".                                #  92
297               "\x00\x00\x00\x46".                                #  96
298               "\x00\x00\x00\x00".                                # 100
299                           "\x00\x00\x00\x00\x00\x00\x00\x00".
300                           "\x00\x00\x00\x00\x00\x00\x00\x00".
301               pack("V", ($this->StartBlock!==false) ?
302                         $this->StartBlock : 0).                  # 120
303               pack("V", ($this->Size!==false) ?
304                         $this->Size : 0).                        # 124
305               pack("V", 0)                                       # 128
306         );
307     }
308
309 }
310
311 /*
312  * This is the OLE::Storage_Lite Perl package ported to PHP
313  * OLE::Storage_Lite was written by Kawai Takanori, kwitknr@cpan.org
314  */
315
316 class ole_pps_file extends ole_pps 
317 {
318     function __construct($sNm, $sData=false, $sFile=false) 
319     {
320         $this->No         = false;
321         $this->Name       = $sNm;
322         $this->Type       = PpsType_File;
323         $this->PrevPps    = false;
324         $this->NextPps    = false;
325         $this->DirPps     = false;
326         $this->Time1st    = false;
327         $this->Time2nd    = false;
328         $this->StartBlock = false;
329         $this->Size       = false;
330         $this->Data       = ($sFile===false) ? $sData : '';
331         $this->Child      = false;
332
333         if ($sFile!==false) 
334         {
335             if (is_ressource($sFile)) 
336                 $this->_PPS_FILE=$sFile;
337             elseif ($sFile=="") 
338             {
339                 $fname=tempnam("php_ole");
340                 $this->_PPS_FILE=fopen($fname, "r+b");
341             } 
342             else 
343             {
344                 $fname=$sFile;
345                 $this->_PPS_FILE=fopen($fname, "r+b");
346             }
347
348             if ($sData!==false) 
349                 fputs($this->_PPS_FILE, $sData);
350         }
351     }
352
353     function append ($sData) 
354     {
355         if ($this->_PPS_FILE) 
356             fputs($this->_PPS_FILE, $sData);
357         else 
358             $this->Data.=$sData;
359     }
360 }
361
362 /*
363  * This is the OLE::Storage_Lite Perl package ported to PHP
364  * OLE::Storage_Lite was written by Kawai Takanori, kwitknr@cpan.org
365  */
366
367 class ole_pps_root extends ole_pps 
368 {
369     function __construct($raTime1st=false, $raTime2nd=false, $raChild=false) 
370     {
371         $this->No         = false;
372         $this->Name       = Asc2Ucs('Root Entry');
373         $this->Type       = PpsType_Root;
374         $this->PrevPps    = false;
375         $this->NextPps    = false;
376         $this->DirPps     = false;
377         $this->Time1st    = $raTime1st;
378         $this->Time2nd    = $raTime2nd;
379         $this->StartBlock = false;
380         $this->Size       = false;
381         $this->Data       = false;
382         $this->Child      = $raChild;
383     }
384
385         #------------------------------------------------------------------------------
386         # save (OLE::Storage_Lite::PPS::Root)
387         #------------------------------------------------------------------------------
388         function save($sFile, $bNoAs=false, $rhInfo=false) 
389         {
390                 #0.Initial Setting for saving
391
392                 if (!$rhInfo) 
393                         $rhInfo=new stdClass();
394
395                 $rhInfo->_BIG_BLOCK_SIZE = $rhInfo->_SMALL_BLOCK_SIZE = 0;
396                 $rhInfo->_BIG_BLOCK_SIZE=pow(2, (($rhInfo->_BIG_BLOCK_SIZE) ?
397                         _adjust2($rhInfo->_BIG_BLOCK_SIZE) : 9));
398                 $rhInfo->_SMALL_BLOCK_SIZE=pow(2, (($rhInfo->_SMALL_BLOCK_SIZE) ?
399                         _adjust2($rhInfo->_SMALL_BLOCK_SIZE) : 6));
400                 $rhInfo->_SMALL_SIZE = 0x1000;
401                 $rhInfo->_PPS_SIZE = 0x80;
402
403                 #1.Open File
404                 #1.1 $sFile is Ref of scalar
405                 if(is_resource($sFile)) 
406                 {
407                         $oIo=$sFile;
408                         $rhInfo->_FILEH_ = $oIo;
409                 }
410                 #1.2 $sFile is a simple filename string
411                 else 
412                 {
413                         $oIo=fopen("$sFile", "wb");
414                         $rhInfo->_FILEH_ = $oIo;
415                 }
416
417                 $iBlk = 0;
418                 #1. Make an array of PPS (for Save)
419                 $aList=array();
420                 $list=array(&$this);
421                 if($bNoAs) 
422                         $this->_savePpsSetPnt2($list, $aList, $rhInfo);
423                 else 
424                         $this->_savePpsSetPnt($list, $aList, $rhInfo);
425                 list($iSBDcnt, $iBBcnt, $iPPScnt) = $this->_calcSize($aList, $rhInfo);
426                 #2.Save Header
427                 $this->_saveHeader($rhInfo, $iSBDcnt, $iBBcnt, $iPPScnt);
428
429                 #3.Make Small Data string (write SBD)
430                 $sSmWk = $this->_makeSmallData($aList, $rhInfo);
431                 $this->Data = $sSmWk;  #Small Datas become RootEntry Data
432
433                 #4. Write BB
434                 $iBBlk = $iSBDcnt;
435                 $this->_saveBigData($iBBlk, $aList, $rhInfo);
436                 #5. Write PPS
437                 $this->_savePps($aList, $rhInfo);
438                 #6. Write BD and BDList and Adding Header informations
439                 $this->_saveBbd($iSBDcnt, $iBBcnt, $iPPScnt,  $rhInfo); 
440                 #7.Close File
441                 fclose($rhInfo->_FILEH_);
442         }
443
444         #------------------------------------------------------------------------------
445         # _calcSize (OLE::Storage_Lite::PPS)
446         #------------------------------------------------------------------------------
447         function _calcSize(&$raList, $rhInfo) 
448         {
449                 #0. Calculate Basic Setting
450                 $iSBDcnt=0;
451                 $iBBcnt=0;
452                 $iPPScnt = 0;
453                 $iSmallLen = 0;
454                 $iSBcnt = 0;
455
456                 for ($c=0;$c<sizeof($raList);$c++) 
457                 {
458                 $oPps=&$raList[$c];
459
460                 if($oPps->Type==PpsType_File) 
461                 {
462                         $oPps->Size = $oPps->_DataLen();  #Mod
463                         if($oPps->Size < $rhInfo->_SMALL_SIZE) 
464                         {
465                                 $iSBcnt += floor($oPps->Size / $rhInfo->_SMALL_BLOCK_SIZE) +
466                                 (($oPps->Size % $rhInfo->_SMALL_BLOCK_SIZE) ? 1 : 0);
467                         } 
468                         else 
469                         {
470                                 $iBBcnt += 
471                                 (floor($oPps->Size/ $rhInfo->_BIG_BLOCK_SIZE) +
472                                 (($oPps->Size % $rhInfo->_BIG_BLOCK_SIZE)? 1: 0));
473                         }
474                 }
475                 }       
476                 $iSmallLen = $iSBcnt * $rhInfo->_SMALL_BLOCK_SIZE;
477                 $iSlCnt = floor($rhInfo->_BIG_BLOCK_SIZE / LongIntSize);
478                 $iSBDcnt = floor($iSBcnt / $iSlCnt)+ (($iSBcnt % $iSlCnt) ? 1 : 0);
479                 $iBBcnt +=  (floor($iSmallLen/ $rhInfo->_BIG_BLOCK_SIZE) +
480                 (( $iSmallLen% $rhInfo->_BIG_BLOCK_SIZE) ? 1 : 0));
481                 $iCnt = sizeof($raList);
482                 $iBdCnt = $rhInfo->_BIG_BLOCK_SIZE/PpsSize;
483                 $iPPScnt = (floor($iCnt/$iBdCnt) + (($iCnt % $iBdCnt) ? 1 : 0));
484
485                 return array($iSBDcnt, $iBBcnt, $iPPScnt);
486         }
487
488         #------------------------------------------------------------------------------
489         # _adjust2 (OLE::Storage_Lite::PPS::Root)
490         #------------------------------------------------------------------------------
491         function _adjust2($i2) 
492         {
493                 $iWk = log($i2)/log(2);
494                 return ($iWk > int($iWk)) ? floor($iWk)+1 : $iWk;
495         }
496
497         #------------------------------------------------------------------------------
498         # _saveHeader (OLE::Storage_Lite::PPS::Root)
499         #------------------------------------------------------------------------------
500         function _saveHeader($rhInfo, $iSBDcnt, $iBBcnt, $iPPScnt) 
501         {
502                 $FILE = $rhInfo->_FILEH_;
503
504                 #0. Calculate Basic Setting
505                 $iBlCnt = $rhInfo->_BIG_BLOCK_SIZE / LongIntSize;
506                 $i1stBdL = ($rhInfo->_BIG_BLOCK_SIZE - 0x4C) / LongIntSize;
507
508                 $iBdExL = 0;
509                 $iAll = $iBBcnt + $iPPScnt + $iSBDcnt;
510                 $iAllW = $iAll;
511                 $iBdCntW = floor($iAllW / $iBlCnt) + (($iAllW % $iBlCnt) ? 1 : 0);
512                 $iBdCnt = floor(($iAll + $iBdCntW) / $iBlCnt) + ((($iAllW+$iBdCntW) % $iBlCnt) ? 1 : 0);
513                 //my $i;
514
515                 #0.1 Calculate BD count
516                 if ($iBdCnt > $i1stBdL) 
517                 {
518                         // TODO: is do-while correct here?
519                         do 
520                         {
521                         $iBdExL++;
522                         $iAllW++;
523                         $iBdCntW = floor($iAllW / $iBlCnt) + (($iAllW % $iBlCnt) ? 1 : 0);
524                         $iBdCnt = floor(($iAllW + $iBdCntW) / $iBlCnt) + ((($iAllW+$iBdCntW) % $iBlCnt) ? 1 : 0);
525                 } 
526                 while($iBdCnt > ($iBdExL*$iBlCnt+ $i1stBdL));
527                 }
528
529                 #1.Save Header
530                 fputs($FILE,
531             "\xD0\xCF\x11\xE0\xA1\xB1\x1A\xE1".
532             "\x00\x00\x00\x00".
533             "\x00\x00\x00\x00".
534             "\x00\x00\x00\x00".
535             "\x00\x00\x00\x00".
536             pack("v", 0x3b).
537             pack("v", 0x03).
538             pack("v", -2).
539             pack("v", 9).
540             pack("v", 6).
541             pack("v", 0).
542             "\x00\x00\x00\x00".
543             "\x00\x00\x00\x00".
544             pack("V", $iBdCnt).
545             pack("V", $iBBcnt+$iSBDcnt). #ROOT START
546             pack("V", 0).
547             pack("V", 0x1000).
548             pack("V", 0).                  #Small Block Depot
549             pack("V", 1)
550         );
551                 #2. Extra BDList Start, Count
552                 if($iBdCnt < $i1stBdL) 
553                 {
554                 fputs($FILE, 
555                 pack("V", -2).     #Extra BDList Start
556                 pack("V", 0)       #Extra BDList Count
557                 );
558                 } 
559                 else 
560                 {
561                 fputs($FILE,
562                 pack("V", $iAll+$iBdCnt).
563                 pack("V", $iBdExL)
564             );
565                 }
566
567                 #3. BDList
568         for ($i=0;($i<$i1stBdL) && ($i < $iBdCnt); $i++) 
569                 fputs($FILE, pack("V", $iAll+$i));
570         if ($i<$i1stBdL) 
571         {
572                         // TODO: Check, if str_repeat is binary safe
573                 fputs($FILE, str_repeat((pack("V", -1)), ($i1stBdL-$i)));
574         }
575         }
576
577         #------------------------------------------------------------------------------
578         # _saveBigData (OLE::Storage_Lite::PPS)
579         #------------------------------------------------------------------------------
580         function _saveBigData(&$iStBlk, &$raList, $rhInfo) 
581         {
582
583                 //return;//!!!
584
585                 $iRes = 0;
586                 $FILE = $rhInfo->_FILEH_;
587
588                 #1.Write Big (ge 0x1000) Data into Block
589                 for ($c=0;$c<sizeof($raList);$c++) 
590                 {
591                 $oPps=&$raList[$c];
592                 if($oPps->Type!=PpsType_Dir) 
593                 {
594                                 #print "PPS: $oPps DEF:", defined($oPps->{Data}), "\n";
595                         $oPps->Size = $oPps->_DataLen();  #Mod
596                         if(($oPps->Size >= $rhInfo->_SMALL_SIZE) ||
597                         (($oPps->Type == PpsType_Root) && $oPps->Data!==false)) 
598                 {
599                         #1.1 Write Data
600                         #Check for update
601                         if($oPps->_PPS_FILE) 
602                         {
603                                 //my $sBuff;
604                                 $iLen = 0;
605                                 fseek($oPps->_PPS_FILE, 0, SEEK_SET); #To The Top
606                                 while ($sBuff=fread($oPps->_PPS_FILE, 4096)) 
607                                 {
608                                 $iLen += length($sBuff);
609                                 fputs($FILE, $sBuff);           #Check for update
610                                 }
611                         } 
612                         else 
613                                 fputs($FILE, $oPps->Data);
614                         if ($oPps->Size % $rhInfo->_BIG_BLOCK_SIZE) 
615                         {
616                                                 // TODO: Check, if str_repeat() is binary safe
617                                 fputs($FILE, str_repeat("\x00", 
618                                 ($rhInfo->_BIG_BLOCK_SIZE - 
619                             ($oPps->Size % $rhInfo->_BIG_BLOCK_SIZE)))
620                         );
621                         }
622                         #1.2 Set For PPS
623                         $oPps->StartBlock = $iStBlk;
624                         $iStBlk += 
625                         (floor($oPps->Size/ $rhInfo->_BIG_BLOCK_SIZE) +
626                         (($oPps->Size % $rhInfo->_BIG_BLOCK_SIZE) ? 1 : 0));
627                         }
628                 }
629                 }
630         }
631
632         #------------------------------------------------------------------------------
633         # _savePps (OLE::Storage_Lite::PPS::Root)
634         #------------------------------------------------------------------------------
635         function _savePps(&$raList, $rhInfo) 
636         {
637                 #0. Initial
638                 $FILE = $rhInfo->_FILEH_;
639                 #2. Save PPS
640                 for ($c=0;$c<sizeof($raList);$c++) 
641                 {
642                 $oItem=&$raList[$c];
643                 $oItem->_savePpsWk($rhInfo);
644                 }
645                 #3. Adjust for Block
646                 $iCnt = sizeof($raList);
647                 $iBCnt = $rhInfo->_BIG_BLOCK_SIZE / $rhInfo->_PPS_SIZE;
648                 if($iCnt % $iBCnt) 
649                 fputs($FILE, str_repeat("\x00", (($iBCnt - ($iCnt % $iBCnt)) * $rhInfo->_PPS_SIZE)));
650                 return (floor($iCnt / $iBCnt) + (($iCnt % $iBCnt) ? 1 : 0));
651         }
652
653         #------------------------------------------------------------------------------
654         # _savePpsSetPnt2 (OLE::Storage_Lite::PPS::Root)
655         #  For Test
656         #------------------------------------------------------------------------------
657         function _savePpsSetPnt2(&$aThis, &$raList, $rhInfo) 
658         {
659                 #1. make Array as Children-Relations
660                 #1.1 if No Children
661                 if (!is_array($aThis) || sizeof($aThis)==0) 
662                 return 0xFFFFFFFF;
663                 elseif (sizeof($aThis)==1) 
664                 {
665                         #1.2 Just Only one
666                 array_push($raList, $aThis[0]);
667                 $aThis[0]->No = sizeof($raList)-1;
668                 $aThis[0]->PrevPps = 0xFFFFFFFF;
669                 $aThis[0]->NextPps = 0xFFFFFFFF;
670                 $aThis[0]->DirPps = $this->_savePpsSetPnt2($aThis[0]->Child, $raList, $rhInfo);
671                 return $aThis[0]->No;
672                 } 
673                 else 
674                 {
675                         #1.3 Array
676                 $iCnt = sizeof($aThis);
677                         #1.3.1 Define Center
678                 $iPos = 0; #int($iCnt/ 2);     #$iCnt 
679
680                 $aWk = $aThis;
681                 $aPrev = (sizeof($aThis) > 2) ? array_splice($aWk, 1, 1) : array(); #$iPos);
682                 $aNext = array_splice($aWk, 1); #, $iCnt - $iPos -1);
683                 $aThis[$iPos]->PrevPps = $this->_savePpsSetPnt2($aPrev, $raList, $rhInfo);
684                 array_push($raList, $aThis[$iPos]);
685                 $aThis[$iPos]->No = sizeof($raList)-1;
686
687                         #1.3.2 Devide a array into Previous,Next
688                 $aThis[$iPos]->NextPps = $this->_savePpsSetPnt2($aNext, $raList, $rhInfo);
689                 $aThis[$iPos]->DirPps = $this->_savePpsSetPnt2($aThis[$iPos]->Child, $raList, $rhInfo);
690                 return $aThis[$iPos]->No;
691                 }
692         }
693
694         #------------------------------------------------------------------------------
695         # _savePpsSetPnt2 (OLE::Storage_Lite::PPS::Root)
696         #  For Test
697         #------------------------------------------------------------------------------
698         function _savePpsSetPnt2s(&$aThis, &$raList, $rhInfo) 
699         {
700                 #1. make Array as Children-Relations
701                 #1.1 if No Children
702                 if (!is_array($aThis) || sizeof($aThis)==0) 
703                 return 0xFFFFFFFF;
704                 elseif (sizeof($aThis)==1) 
705                 {
706                         #1.2 Just Only one
707                 array_push($raList, $aThis[0]);
708                 $aThis[0]->No = sizeof($raList)-1;
709                 $aThis[0]->PrevPps = 0xFFFFFFFF;
710                 $aThis[0]->NextPps = 0xFFFFFFFF;
711                 $aThis[0]->DirPps = $this->_savePpsSetPnt2($aThis[0]->Child, $raList, $rhInfo);
712                 return $aThis[0]->No;
713                 } 
714                 else 
715                 {
716                         #1.3 Array
717                 $iCnt = sizeof($aThis);
718                         #1.3.1 Define Center
719                 $iPos = 0; #int($iCnt/ 2);     #$iCnt 
720                 array_push($raList, $aThis[$iPos]);
721                 $aThis[$iPos]->No = sizeof($raList)-1;
722                 $aWk = $aThis;
723                         #1.3.2 Devide a array into Previous,Next
724                 $aPrev = array_splice($aWk, 0, $iPos);
725                 $aNext = array_splice($aWk, 1, $iCnt - $iPos - 1);
726                 $aThis[$iPos]->PrevPps = $this->_savePpsSetPnt2($aPrev, $raList, $rhInfo);
727                 $aThis[$iPos]->NextPps = $this->_savePpsSetPnt2($aNext, $raList, $rhInfo);
728                 $aThis[$iPos]->DirPps = $this->_savePpsSetPnt2($aThis[$iPos]->Child, $raList, $rhInfo);
729                 return $aThis[$iPos]->No;
730                 }
731         }
732
733         #------------------------------------------------------------------------------
734         # _savePpsSetPnt (OLE::Storage_Lite::PPS::Root)
735         #------------------------------------------------------------------------------
736         function _savePpsSetPnt(&$aThis, &$raList, $rhInfo) 
737         {
738
739                 //print "yyy type: ".gettype($aThis)."<br>\n";
740                 //print "yyy name: ".$aThis[0]->Name."<br>\n";
741
742                 #1. make Array as Children-Relations
743                 #1.1 if No Children
744                 if (!is_array($aThis) || sizeof($aThis)==0) 
745                 {
746                 return 0xFFFFFFFF;
747                 } 
748                 elseif (sizeof($aThis)==1) 
749                 {
750                         #1.2 Just Only one
751                 array_push($raList, $aThis[0]);
752                 $aThis[0]->No = sizeof($raList)-1;
753                 $aThis[0]->PrevPps = 0xFFFFFFFF;
754                 $aThis[0]->NextPps = 0xFFFFFFFF;
755                 $aThis[0]->DirPps = $this->_savePpsSetPnt($aThis[0]->Child, $raList, $rhInfo);
756                 return $aThis[0]->No;
757                 } 
758                 else 
759                 {       
760                         #1.3 Array
761                 $iCnt = sizeof($aThis);
762                         #1.3.1 Define Center
763                 $iPos = floor($iCnt/2);     #$iCnt 
764                 array_push($raList, $aThis[$iPos]);
765                 $aThis[$iPos]->No = sizeof($raList)-1;
766                 $aWk = $aThis;
767                         #1.3.2 Devide a array into Previous,Next
768                 $aPrev = splice($aWk, 0, $iPos);
769                 $aNext = splice($aWk, 1, $iCnt - $iPos - 1);
770                 $aThis[$iPos]->PrevPps = $this->_savePpsSetPnt($aPrev, $raList, $rhInfo);
771                 $aThis[$iPos]->NextPps = $this->_savePpsSetPnt($aNext, $raList, $rhInfo);
772                 $aThis[$iPos]->DirPps = $this->_savePpsSetPnt($aThis[$iPos]->Child, $raList, $rhInfo);
773                 return $aThis[$iPos]->No;
774                 }
775         }
776
777         #------------------------------------------------------------------------------
778         # _savePpsSetPnt (OLE::Storage_Lite::PPS::Root)
779         #------------------------------------------------------------------------------
780         function _savePpsSetPnt1(&$aThis, &$raList, $rhInfo) 
781         {
782                 #1. make Array as Children-Relations
783                 #1.1 if No Children
784                 if (!is_array($aThis) || sizeof($aThis)==0) 
785                 {
786                         return 0xFFFFFFFF;
787                 } 
788                 elseif (sizeof($aThis)==1) 
789                 {
790                         #1.2 Just Only one
791                         array_push($raList, $aThis[0]);
792                         $aThis[0]->No = sizeof($raList)-1;
793                         $aThis[0]->PrevPps = 0xFFFFFFFF;
794                         $aThis[0]->NextPps = 0xFFFFFFFF;
795                         $aThis[0]->DirPps = $this->_savePpsSetPnt($aThis[0]->Child, $raList, $rhInfo);
796                         return $aThis[0]->No;
797                 } 
798                 else 
799                 {
800                         #1.3 Array
801                         $iCnt = sizeof($aThis);
802                         #1.3.1 Define Center
803                         $iPos = floor($iCnt / 2);     #$iCnt 
804                         array_push($raList, $aThis[$iPos]);
805                         $aThis[$iPos]->No = sizeof($raList)-1;
806                         $aWk = $aThis;
807                         #1.3.2 Devide a array into Previous,Next
808                         $aPrev = splice($aWk, 0, $iPos);
809                         $aNext = splice($aWk, 1, $iCnt - $iPos - 1);
810                         $aThis[$iPos]->PrevPps = $this->_savePpsSetPnt($aPrev, $raList, $rhInfo);
811                         $aThis[$iPos]->NextPps = $this->_savePpsSetPnt($aNext, $raList, $rhInfo);
812                         $aThis[$iPos]->DirPps = $this->_savePpsSetPnt($aThis[$iPos]->Child, $raList, $rhInfo);
813                         return $aThis[$iPos]->No;
814                 }
815         }
816
817         #------------------------------------------------------------------------------
818         # _saveBbd (OLE::Storage_Lite)
819         #------------------------------------------------------------------------------
820         function _saveBbd($iSbdSize, $iBsize, $iPpsCnt, $rhInfo) 
821         {
822                 $FILE = $rhInfo->_FILEH_;
823                 #0. Calculate Basic Setting
824                 $iBbCnt = $rhInfo->_BIG_BLOCK_SIZE / LongIntSize;
825                 $i1stBdL = ($rhInfo->_BIG_BLOCK_SIZE - 0x4C) / LongIntSize;
826
827                 $iBdExL = 0;
828                 $iAll = $iBsize + $iPpsCnt + $iSbdSize;
829                 $iAllW = $iAll;
830                 $iBdCntW = floor($iAllW / $iBbCnt) + (($iAllW % $iBbCnt) ? 1 : 0);
831                 $iBdCnt = floor(($iAll + $iBdCntW) / $iBbCnt) + ((($iAllW+$iBdCntW) % $iBbCnt)? 1: 0);
832                 //my $i;
833                 #0.1 Calculate BD count
834                 if ($iBdCnt >$i1stBdL) 
835                 {
836                         // TODO: do-while correct here?
837                 do 
838                 {
839                         $iBdExL++;
840                         $iAllW++;
841                         $iBdCntW = floor($iAllW / $iBbCnt) + (($iAllW % $iBbCnt) ? 1 : 0);
842                         $iBdCnt = floor(($iAllW + $iBdCntW) / $iBbCnt) + ((($iAllW+$iBdCntW) % $iBbCnt) ? 1 : 0);
843                 } 
844                 while ($iBdCnt > ($iBdExL*$iBbCnt+$i1stBdL));
845                 }
846
847                 #1. Making BD
848                 #1.1 Set for SBD
849                 if($iSbdSize > 0) 
850                 {
851                 for ($i = 0; $i<($iSbdSize-1); $i++) 
852                         fputs($FILE, pack("V", $i+1));
853                 fputs($FILE, pack("V", -2));
854                 }
855                 #1.2 Set for B
856                 for ($i = 0; $i<($iBsize-1); $i++) 
857                 fputs($FILE, pack("V", $i+$iSbdSize+1));
858                 fputs($FILE, pack("V", -2));
859
860                 #1.3 Set for PPS
861                 for ($i = 0; $i<($iPpsCnt-1); $i++) 
862                 fputs($FILE, pack("V", $i+$iSbdSize+$iBsize+1));
863                 fputs($FILE, pack("V", -2));
864                 #1.4 Set for BBD itself ( 0xFFFFFFFD : BBD)
865                 for ($i=0; $i<$iBdCnt;$i++) 
866                 fputs($FILE, pack("V", 0xFFFFFFFD));
867                 #1.5 Set for ExtraBDList
868                 for ($i=0; $i<$iBdExL;$i++) 
869                 fputs($FILE, pack("V", 0xFFFFFFFC));
870                 #1.6 Adjust for Block
871                 if(($iAllW + $iBdCnt) % $iBbCnt) 
872                 fputs($FILE, str_repeat(pack("V", -1), ($iBbCnt - (($iAllW + $iBdCnt) % $iBbCnt))));
873
874                 #2.Extra BDList
875                 if($iBdCnt > $i1stBdL)  
876                 {
877                 $iN=0;
878                 $iNb=0;
879                 for ($i=$i1stBdL;$i<$iBdCnt; $i++, $iN++) 
880                 {
881                         if($iN>=($iBbCnt-1)) 
882                         {
883                                 $iN = 0;
884                                 $iNb++;
885                                 fputs($FILE, pack("V", $iAll+$iBdCnt+$iNb));
886                         }
887                         fputs($FILE, pack("V", $iBsize+$iSbdSize+$iPpsCnt+$i));
888                 }
889                 if(($iBdCnt-$i1stBdL) % ($iBbCnt-1)) 
890                         fputs($FILE, str_repeat(pack("V", -1), (($iBbCnt-1) - (($iBdCnt-$i1stBdL) % ($iBbCnt-1)))));
891                 fputs($FILE, pack("V", -2));
892                 }
893         }
894 }
895
896 /**
897 * Class for writing Excel BIFF records.
898 *
899 * From "MICROSOFT EXCEL BINARY FILE FORMAT" by Mark O'Brien (Microsoft Corporation):
900 *
901 * BIFF (BInary File Format) is the file format in which Excel documents are
902 * saved on disk.  A BIFF file is a complete description of an Excel document.
903 * BIFF files consist of sequences of variable-length records. There are many
904 * different types of BIFF records.  For example, one record type describes a
905 * formula entered into a cell; one describes the size and location of a
906 * window into a document; another describes a picture format.
907 *
908 * @author   Xavier Noguer <xnoguer@php.net>
909 * @category FileFormats
910 * @package  Spreadsheet_Excel_Writer
911 */
912
913 class Spreadsheet_Excel_Writer_BIFFwriter
914 {
915     /**
916     * The BIFF/Excel version (5).
917     * @var integer
918     */
919     var $_BIFF_version = 0x0500;
920
921     /**
922     * The byte order of this architecture. 0 => little endian, 1 => big endian
923     * @var integer
924     */
925     var $_byte_order;
926
927     /**
928     * The string containing the data of the BIFF stream
929     * @var string
930     */
931     var $_data;
932
933     /**
934     * The size of the data in bytes. Should be the same as strlen($this->_data)
935     * @var integer
936     */
937     var $_datasize;
938
939     /**
940     * The maximun length for a BIFF record. See _addContinue()
941     * @var integer
942     * @see _addContinue()
943     */
944     var $_limit;
945     var $_offset;
946
947     /**
948     * Constructor
949     *
950     * @access public
951     */
952     function __construct()
953     {
954         $this->_byte_order = '';
955         $this->_data       = '';
956         $this->_datasize   = 0;
957         $this->_limit      = 2080;
958         // Set the byte order
959         $this->_setByteOrder();
960     }
961
962     /**
963     * Determine the byte order and store it as class data to avoid
964     * recalculating it for each call to new().
965     *
966     * @access private
967     */
968     function _setByteOrder()
969     {
970         // Check if "pack" gives the required IEEE 64bit float
971         $teststr = pack("d", 1.2345);
972         $number  = pack("C8", 0x8D, 0x97, 0x6E, 0x12, 0x83, 0xC0, 0xF3, 0x3F);
973         if ($number == $teststr) {
974             $byte_order = 0;    // Little Endian
975         } elseif ($number == strrev($teststr)){
976             $byte_order = 1;    // Big Endian
977         } else {
978             // Give up. I'll fix this in a later version.
979             die("Required floating point format ".
980                                      "not supported on this platform.");
981         }
982         $this->_byte_order = $byte_order;
983     }
984
985     /**
986     * General storage function
987     *
988     * @param string $data binary data to prepend
989     * @access private
990     */
991     function _prepend($data)
992     {
993         if (strlen($data) > $this->_limit) {
994             $data = $this->_addContinue($data);
995         }
996         $this->_data      = $data.$this->_data;
997         $this->_datasize += strlen($data);
998     }
999
1000     /**
1001     * General storage function
1002     *
1003     * @param string $data binary data to append
1004     * @access private
1005     */
1006     function _append($data)
1007     {
1008         if (strlen($data) > $this->_limit) {
1009             $data = $this->_addContinue($data);
1010         }
1011         $this->_data      = $this->_data.$data;
1012         $this->_datasize += strlen($data);
1013     }
1014
1015     /**
1016     * Writes Excel BOF record to indicate the beginning of a stream or
1017     * sub-stream in the BIFF file.
1018     *
1019     * @param  integer $type Type of BIFF file to write: 0x0005 Workbook,
1020     *                       0x0010 Worksheet.
1021     * @access private
1022     */
1023     function _storeBof($type)
1024     {
1025         $record  = 0x0809;        // Record identifier
1026
1027         // According to the SDK $build and $year should be set to zero.
1028         // However, this throws a warning in Excel 5. So, use magic numbers.
1029         if ($this->_BIFF_version == 0x0500) {
1030             $length  = 0x0008;
1031             $unknown = '';
1032             $build   = 0x096C;
1033             $year    = 0x07C9;
1034         } elseif ($this->_BIFF_version == 0x0600) {
1035             $length  = 0x0010;
1036             $unknown = pack("VV", 0x00000041, 0x00000006); //unknown last 8 bytes for BIFF8
1037             $build   = 0x0DBB;
1038             $year    = 0x07CC;
1039         }
1040         $version = $this->_BIFF_version;
1041
1042         $header  = pack("vv",   $record, $length);
1043         $data    = pack("vvvv", $version, $type, $build, $year);
1044         $this->_prepend($header . $data . $unknown);
1045     }
1046
1047     /**
1048     * Writes Excel EOF record to indicate the end of a BIFF stream.
1049     *
1050     * @access private
1051     */
1052     function _storeEof()
1053     {
1054         $record    = 0x000A;   // Record identifier
1055         $length    = 0x0000;   // Number of bytes to follow
1056         $header    = pack("vv", $record, $length);
1057         $this->_append($header);
1058     }
1059
1060     /**
1061     * Excel limits the size of BIFF records. In Excel 5 the limit is 2084 bytes. In
1062     * Excel 97 the limit is 8228 bytes. Records that are longer than these limits
1063     * must be split up into CONTINUE blocks.
1064     *
1065     * This function takes a long BIFF record and inserts CONTINUE records as
1066     * necessary.
1067     *
1068     * @param  string  $data The original binary data to be written
1069     * @return string        A very convenient string of continue blocks
1070     * @access private
1071     */
1072     function _addContinue($data)
1073     {
1074         $limit  = $this->_limit;
1075         $record = 0x003C;         // Record identifier
1076
1077         // The first 2080/8224 bytes remain intact. However, we have to change
1078         // the length field of the record.
1079         $tmp = substr($data, 0, 2).pack("v", $limit-4).substr($data, 4, $limit - 4);
1080
1081         $header = pack("vv", $record, $limit);  // Headers for continue records
1082
1083         // Retrieve chunks of 2080/8224 bytes +4 for the header.
1084         $data_length = strlen($data);
1085         for ($i = $limit; $i <  ($data_length - $limit); $i += $limit) {
1086             $tmp .= $header;
1087             $tmp .= substr($data, $i, $limit);
1088         }
1089
1090         // Retrieve the last chunk of data
1091         $header  = pack("vv", $record, strlen($data) - $i);
1092         $tmp    .= $header;
1093         $tmp    .= substr($data, $i, strlen($data) - $i);
1094
1095         return $tmp;
1096     }
1097 }
1098
1099 /*
1100 FIXME: change prefixes
1101 */
1102 define("OP_BETWEEN",    0x00);
1103 define("OP_NOTBETWEEN", 0x01);
1104 define("OP_EQUAL",      0x02);
1105 define("OP_NOTEQUAL",   0x03);
1106 define("OP_GT",         0x04);
1107 define("OP_LT",         0x05);
1108 define("OP_GTE",        0x06);
1109 define("OP_LTE",        0x07);
1110
1111 /**
1112 * Baseclass for generating Excel DV records (validations)
1113 *
1114 * @author   Herman Kuiper
1115 * @category FileFormats
1116 * @package  Spreadsheet_Excel_Writer
1117 */
1118 class Spreadsheet_Excel_Writer_Validator
1119 {
1120    var $_type;
1121    var $_style;
1122    var $_fixedList;
1123    var $_blank;
1124    var $_incell;
1125    var $_showprompt;
1126    var $_showerror;
1127    var $_title_prompt;
1128    var $_descr_prompt;
1129    var $_title_error;
1130    var $_descr_error;
1131    var $_operator;
1132    var $_formula1;
1133    var $_formula2;
1134     /**
1135     * The parser from the workbook. Used to parse validation formulas also
1136     * @var Spreadsheet_Excel_Writer_Parser
1137     */
1138     var $_parser;
1139
1140     function __construct(&$parser)
1141     {
1142         $this->_parser       = $parser;
1143         $this->_type         = 0x01; // FIXME: add method for setting datatype
1144         $this->_style        = 0x00;
1145         $this->_fixedList    = false;
1146         $this->_blank        = false;
1147         $this->_incell       = false;
1148         $this->_showprompt   = false;
1149         $this->_showerror    = true;
1150         $this->_title_prompt = "\x00";
1151         $this->_descr_prompt = "\x00";
1152         $this->_title_error  = "\x00";
1153         $this->_descr_error  = "\x00";
1154         $this->_operator     = 0x00; // default is equal
1155         $this->_formula1    = '';
1156         $this->_formula2    = '';
1157     }
1158
1159    function setPrompt($promptTitle = "\x00", $promptDescription = "\x00", $showPrompt = true)
1160    {
1161       $this->_showprompt = $showPrompt;
1162       $this->_title_prompt = $promptTitle;
1163       $this->_descr_prompt = $promptDescription;
1164    }
1165
1166    function setError($errorTitle = "\x00", $errorDescription = "\x00", $showError = true)
1167    {
1168       $this->_showerror = $showError;
1169       $this->_title_error = $errorTitle;
1170       $this->_descr_error = $errorDescription;
1171    }
1172
1173    function allowBlank()
1174    {
1175       $this->_blank = true;
1176    }
1177
1178    function onInvalidStop()
1179    {
1180       $this->_style = 0x00;
1181    }
1182
1183     function onInvalidWarn()
1184     {
1185         $this->_style = 0x01;
1186     }
1187
1188     function onInvalidInfo()
1189     {
1190         $this->_style = 0x02;
1191     }
1192
1193     function setFormula1($formula)
1194     {
1195         // Parse the formula using the parser in Parser.php
1196         $this->_parser->parse($formula);
1197
1198         $this->_formula1 = $this->_parser->toReversePolish();
1199         return true;
1200     }
1201
1202     function setFormula2($formula)
1203     {
1204         // Parse the formula using the parser in Parser.php
1205         $this->_parser->parse($formula);
1206
1207         $this->_formula2 = $this->_parser->toReversePolish();
1208         return true;
1209     }
1210
1211     function _getOptions()
1212     {
1213         $options = $this->_type;
1214         $options |= $this->_style << 3;
1215         if ($this->_fixedList) {
1216             $options |= 0x80;
1217         }
1218         if ($this->_blank) {
1219             $options |= 0x100;
1220         }
1221         if (!$this->_incell) {
1222             $options |= 0x200;
1223         }
1224         if ($this->_showprompt) {
1225             $options |= 0x40000;
1226         }
1227         if ($this->_showerror) {
1228             $options |= 0x80000;
1229         }
1230       $options |= $this->_operator << 20;
1231
1232       return $options;
1233    }
1234
1235    function _getData()
1236    {
1237       $title_prompt_len = strlen($this->_title_prompt);
1238       $descr_prompt_len = strlen($this->_descr_prompt);
1239       $title_error_len = strlen($this->_title_error);
1240       $descr_error_len = strlen($this->_descr_error);
1241
1242       $formula1_size = strlen($this->_formula1);
1243       $formula2_size = strlen($this->_formula2);
1244
1245       $data  = pack("V", $this->_getOptions());
1246       $data .= pack("vC", $title_prompt_len, 0x00) . $this->_title_prompt;
1247       $data .= pack("vC", $title_error_len, 0x00) . $this->_title_error;
1248       $data .= pack("vC", $descr_prompt_len, 0x00) . $this->_descr_prompt;
1249       $data .= pack("vC", $descr_error_len, 0x00) . $this->_descr_error;
1250
1251       $data .= pack("vv", $formula1_size, 0x0000) . $this->_formula1;
1252       $data .= pack("vv", $formula2_size, 0x0000) . $this->_formula2;
1253
1254       return $data;
1255    }
1256 }
1257
1258 /**
1259 * Class for generating Excel XF records (formats)
1260 *
1261 * @author   Xavier Noguer <xnoguer@rezebra.com>
1262 * @category FileFormats
1263 * @package  Spreadsheet_Excel_Writer
1264 */
1265
1266 class Spreadsheet_Excel_Writer_Format
1267 {
1268     /**
1269     * The index given by the workbook when creating a new format.
1270     * @var integer
1271     */
1272     var $_xf_index;
1273
1274     /**
1275     * Index to the FONT record.
1276     * @var integer
1277     */
1278     var $font_index;
1279
1280     /**
1281     * The font name (ASCII).
1282     * @var string
1283     */
1284     var $_font_name;
1285
1286     /**
1287     * Height of font (1/20 of a point)
1288     * @var integer
1289     */
1290     var $_size;
1291
1292     /**
1293     * Bold style
1294     * @var integer
1295     */
1296     var $_bold;
1297
1298     /**
1299     * Bit specifiying if the font is italic.
1300     * @var integer
1301     */
1302     var $_italic;
1303
1304     /**
1305     * Index to the cell's color
1306     * @var integer
1307     */
1308     var $_color;
1309
1310     /**
1311     * The text underline property
1312     * @var integer
1313     */
1314     var $_underline;
1315
1316     /**
1317     * Bit specifiying if the font has strikeout.
1318     * @var integer
1319     */
1320     var $_font_strikeout;
1321
1322     /**
1323     * Bit specifiying if the font has outline.
1324     * @var integer
1325     */
1326     var $_font_outline;
1327
1328     /**
1329     * Bit specifiying if the font has shadow.
1330     * @var integer
1331     */
1332     var $_font_shadow;
1333
1334     /**
1335     * 2 bytes specifiying the script type for the font.
1336     * @var integer
1337     */
1338     var $_font_script;
1339
1340     /**
1341     * Byte specifiying the font family.
1342     * @var integer
1343     */
1344     var $_font_family;
1345
1346     /**
1347     * Byte specifiying the font charset.
1348     * @var integer
1349     */
1350     var $_font_charset;
1351
1352     /**
1353     * An index (2 bytes) to a FORMAT record (number format).
1354     * @var integer
1355     */
1356     var $_num_format;
1357
1358     /**
1359     * Bit specifying if formulas are hidden.
1360     * @var integer
1361     */
1362     var $_hidden;
1363
1364     /**
1365     * Bit specifying if the cell is locked.
1366     * @var integer
1367     */
1368     var $_locked;
1369
1370     /**
1371     * The three bits specifying the text horizontal alignment.
1372     * @var integer
1373     */
1374     var $_text_h_align;
1375
1376     /**
1377     * Bit specifying if the text is wrapped at the right border.
1378     * @var integer
1379     */
1380     var $_text_wrap;
1381
1382     /**
1383     * The three bits specifying the text vertical alignment.
1384     * @var integer
1385     */
1386     var $_text_v_align;
1387
1388     /**
1389     * 1 bit, apparently not used.
1390     * @var integer
1391     */
1392     var $_text_justlast;
1393
1394     /**
1395     * The two bits specifying the text rotation.
1396     * @var integer
1397     */
1398     var $_rotation;
1399
1400     /**
1401     * The cell's foreground color.
1402     * @var integer
1403     */
1404     var $_fg_color;
1405
1406     /**
1407     * The cell's background color.
1408     * @var integer
1409     */
1410     var $_bg_color;
1411
1412     /**
1413     * The cell's background fill pattern.
1414     * @var integer
1415     */
1416     var $_pattern;
1417
1418     /**
1419     * Style of the bottom border of the cell
1420     * @var integer
1421     */
1422     var $_bottom;
1423
1424     /**
1425     * Color of the bottom border of the cell.
1426     * @var integer
1427     */
1428     var $_bottom_color;
1429
1430     /**
1431     * Style of the top border of the cell
1432     * @var integer
1433     */
1434     var $_top;
1435
1436     /**
1437     * Color of the top border of the cell.
1438     * @var integer
1439     */
1440     var $_top_color;
1441
1442     /**
1443     * Style of the left border of the cell
1444     * @var integer
1445     */
1446     var $_left;
1447
1448     /**
1449     * Color of the left border of the cell.
1450     * @var integer
1451     */
1452     var $_left_color;
1453
1454     /**
1455     * Style of the right border of the cell
1456     * @var integer
1457     */
1458     var $_right;
1459
1460     /**
1461     * Color of the right border of the cell.
1462     * @var integer
1463     */
1464     var $_right_color;
1465
1466     // Dynamic members must be here (php 8.2)
1467     var $_BIFF_version;
1468     var $_diag;
1469     var $_diag_color;
1470     /**
1471     * Constructor
1472     *
1473     * @access private
1474     * @param integer $index the XF index for the format.
1475     * @param array   $properties array with properties to be set on initialization.
1476     */
1477     function __construct($BIFF_version, $index = 0, $properties =  array())
1478     {
1479         $this->_xf_index       = $index;
1480         $this->_BIFF_version   = $BIFF_version;
1481         $this->font_index      = 0;
1482         $this->_font_name      = 'Arial';
1483         $this->_size           = 10;
1484         $this->_bold           = 0x0190;
1485         $this->_italic         = 0;
1486         $this->_color          = 0x7FFF;
1487         $this->_underline      = 0;
1488         $this->_font_strikeout = 0;
1489         $this->_font_outline   = 0;
1490         $this->_font_shadow    = 0;
1491         $this->_font_script    = 0;
1492         $this->_font_family    = 0;
1493         $this->_font_charset   = 0;
1494
1495         $this->_num_format     = 0;
1496
1497         $this->_hidden         = 0;
1498         $this->_locked         = 0;
1499
1500         $this->_text_h_align   = 0;
1501         $this->_text_wrap      = 0;
1502         $this->_text_v_align   = 2;
1503         $this->_text_justlast  = 0;
1504         $this->_rotation       = 0;
1505
1506         $this->_fg_color       = 0x40;
1507         $this->_bg_color       = 0x41;
1508
1509         $this->_pattern        = 0;
1510
1511         $this->_bottom         = 0;
1512         $this->_top            = 0;
1513         $this->_left           = 0;
1514         $this->_right          = 0;
1515         $this->_diag           = 0;
1516
1517         $this->_bottom_color   = 0x40;
1518         $this->_top_color      = 0x40;
1519         $this->_left_color     = 0x40;
1520         $this->_right_color    = 0x40;
1521         $this->_diag_color     = 0x40;
1522
1523         // Set properties passed to Spreadsheet_Excel_Writer_Workbook::addFormat()
1524         foreach ($properties as $property => $value)
1525         {
1526             if (method_exists($this, 'set'.ucwords($property))) {
1527                 $method_name = 'set'.ucwords($property);
1528                 $this->$method_name($value);
1529             }
1530         }
1531     }
1532
1533
1534     /**
1535     * Generate an Excel BIFF XF record (style or cell).
1536     *
1537     * @param string $style The type of the XF record ('style' or 'cell').
1538     * @return string The XF record
1539     */
1540     function getXf($style)
1541     {
1542         // Set the type of the XF record and some of the attributes.
1543         if ($style == 'style') {
1544             $style = 0xFFF5;
1545         } else {
1546             $style   = $this->_locked;
1547             $style  |= $this->_hidden << 1;
1548         }
1549
1550         // Flags to indicate if attributes have been set.
1551         $atr_num     = ($this->_num_format != 0)?1:0;
1552         $atr_fnt     = ($this->font_index != 0)?1:0;
1553         $atr_alc     = ($this->_text_wrap)?1:0;
1554         $atr_bdr     = ($this->_bottom   ||
1555                         $this->_top      ||
1556                         $this->_left     ||
1557                         $this->_right)?1:0;
1558         $atr_pat     = (($this->_fg_color != 0x40) ||
1559                         ($this->_bg_color != 0x41) ||
1560                         $this->_pattern)?1:0;
1561         $atr_prot    = $this->_locked | $this->_hidden;
1562
1563         // Zero the default border colour if the border has not been set.
1564         if ($this->_bottom == 0) {
1565             $this->_bottom_color = 0;
1566         }
1567         if ($this->_top  == 0) {
1568             $this->_top_color = 0;
1569         }
1570         if ($this->_right == 0) {
1571             $this->_right_color = 0;
1572         }
1573         if ($this->_left == 0) {
1574             $this->_left_color = 0;
1575         }
1576         if ($this->_diag == 0) {
1577             $this->_diag_color = 0;
1578         }
1579
1580         $record         = 0x00E0;              // Record identifier
1581         if ($this->_BIFF_version == 0x0500) {
1582             $length         = 0x0010;              // Number of bytes to follow
1583         }
1584         if ($this->_BIFF_version == 0x0600) {
1585             $length         = 0x0014;
1586         }
1587
1588         $ifnt           = $this->font_index;   // Index to FONT record
1589         $ifmt           = $this->_num_format;  // Index to FORMAT record
1590         if ($this->_BIFF_version == 0x0500) {
1591             $align          = $this->_text_h_align;       // Alignment
1592             $align         |= $this->_text_wrap     << 3;
1593             $align         |= $this->_text_v_align  << 4;
1594             $align         |= $this->_text_justlast << 7;
1595             $align         |= $this->_rotation      << 8;
1596             $align         |= $atr_num                << 10;
1597             $align         |= $atr_fnt                << 11;
1598             $align         |= $atr_alc                << 12;
1599             $align         |= $atr_bdr                << 13;
1600             $align         |= $atr_pat                << 14;
1601             $align         |= $atr_prot               << 15;
1602
1603             $icv            = $this->_fg_color;       // fg and bg pattern colors
1604             $icv           |= $this->_bg_color      << 7;
1605
1606             $fill           = $this->_pattern;        // Fill and border line style
1607             $fill          |= $this->_bottom        << 6;
1608             $fill          |= $this->_bottom_color  << 9;
1609
1610             $border1        = $this->_top;            // Border line style and color
1611             $border1       |= $this->_left          << 3;
1612             $border1       |= $this->_right         << 6;
1613             $border1       |= $this->_top_color     << 9;
1614
1615             $border2        = $this->_left_color;     // Border color
1616             $border2       |= $this->_right_color   << 7;
1617
1618             $header      = pack("vv",       $record, $length);
1619             $data        = pack("vvvvvvvv", $ifnt, $ifmt, $style, $align,
1620                                             $icv, $fill,
1621                                             $border1, $border2);
1622         } elseif ($this->_BIFF_version == 0x0600) {
1623             $align          = $this->_text_h_align;       // Alignment
1624             $align         |= $this->_text_wrap     << 3;
1625             $align         |= $this->_text_v_align  << 4;
1626             $align         |= $this->_text_justlast << 7;
1627
1628             $used_attrib    = $atr_num              << 2;
1629             $used_attrib   |= $atr_fnt              << 3;
1630             $used_attrib   |= $atr_alc              << 4;
1631             $used_attrib   |= $atr_bdr              << 5;
1632             $used_attrib   |= $atr_pat              << 6;
1633             $used_attrib   |= $atr_prot             << 7;
1634
1635             $icv            = $this->_fg_color;      // fg and bg pattern colors
1636             $icv           |= $this->_bg_color      << 7;
1637
1638             $border1        = $this->_left;          // Border line style and color
1639             $border1       |= $this->_right         << 4;
1640             $border1       |= $this->_top           << 8;
1641             $border1       |= $this->_bottom        << 12;
1642             $border1       |= $this->_left_color    << 16;
1643             $border1       |= $this->_right_color   << 23;
1644             $diag_tl_to_rb = 0; // FIXME: add method
1645             $diag_tr_to_lb = 0; // FIXME: add method
1646             $border1       |= $diag_tl_to_rb        << 30;
1647             $border1       |= $diag_tr_to_lb        << 31;
1648
1649             $border2        = $this->_top_color;    // Border color
1650             $border2       |= $this->_bottom_color   << 7;
1651             $border2       |= $this->_diag_color     << 14;
1652             $border2       |= $this->_diag           << 21;
1653             $border2       |= $this->_pattern        << 26;
1654
1655             $header      = pack("vv",       $record, $length);
1656
1657             $rotation      = 0x00;
1658             $biff8_options = 0x00;
1659             $data  = pack("vvvC", $ifnt, $ifmt, $style, $align);
1660             $data .= pack("CCC", $rotation, $biff8_options, $used_attrib);
1661             $data .= pack("VVv", $border1, $border2, $icv);
1662         }
1663
1664         return($header . $data);
1665     }
1666
1667     /**
1668     * Generate an Excel BIFF FONT record.
1669     *
1670     * @return string The FONT record
1671     */
1672     function getFont()
1673     {
1674         $dyHeight   = $this->_size * 20;    // Height of font (1/20 of a point)
1675         $icv        = $this->_color;        // Index to color palette
1676         $bls        = $this->_bold;         // Bold style
1677         $sss        = $this->_font_script;  // Superscript/subscript
1678         $uls        = $this->_underline;    // Underline
1679         $bFamily    = $this->_font_family;  // Font family
1680         $bCharSet   = $this->_font_charset; // Character set
1681         $encoding   = 0;                    // TODO: Unicode support
1682
1683         $cch        = strlen($this->_font_name); // Length of font name
1684         $record     = 0x31;                      // Record identifier
1685         if ($this->_BIFF_version == 0x0500) {
1686             $length     = 0x0F + $cch;            // Record length
1687         } elseif ($this->_BIFF_version == 0x0600) {
1688             $length     = 0x10 + $cch;
1689         }
1690         $reserved   = 0x00;                // Reserved
1691         $grbit      = 0x00;                // Font attributes
1692         if ($this->_italic) {
1693             $grbit     |= 0x02;
1694         }
1695         if ($this->_font_strikeout) {
1696             $grbit     |= 0x08;
1697         }
1698         if ($this->_font_outline) {
1699             $grbit     |= 0x10;
1700         }
1701         if ($this->_font_shadow) {
1702             $grbit     |= 0x20;
1703         }
1704
1705         $header  = pack("vv",         $record, $length);
1706         if ($this->_BIFF_version == 0x0500) {
1707             $data    = pack("vvvvvCCCCC", $dyHeight, $grbit, $icv, $bls,
1708                                           $sss, $uls, $bFamily,
1709                                           $bCharSet, $reserved, $cch);
1710         } elseif ($this->_BIFF_version == 0x0600) {
1711             $data    = pack("vvvvvCCCCCC", $dyHeight, $grbit, $icv, $bls,
1712                                            $sss, $uls, $bFamily,
1713                                            $bCharSet, $reserved, $cch, $encoding);
1714         }
1715         return($header . $data . $this->_font_name);
1716     }
1717
1718     /**
1719     * Returns a unique hash key for a font.
1720     * Used by Spreadsheet_Excel_Writer_Workbook::_storeAllFonts()
1721     *
1722     * The elements that form the key are arranged to increase the probability of
1723     * generating a unique key. Elements that hold a large range of numbers
1724     * (eg. _color) are placed between two binary elements such as _italic
1725     *
1726     * @return string A key for this font
1727     */
1728     function getFontKey()
1729     {
1730         $key  = "$this->_font_name$this->_size";
1731         $key .= "$this->_font_script$this->_underline";
1732         $key .= "$this->_font_strikeout$this->_bold$this->_font_outline";
1733         $key .= "$this->_font_family$this->_font_charset";
1734         $key .= "$this->_font_shadow$this->_color$this->_italic";
1735         $key  = str_replace(' ', '_', $key);
1736         return ($key);
1737     }
1738
1739     /**
1740     * Returns the index used by Spreadsheet_Excel_Writer_Worksheet::_XF()
1741     *
1742     * @return integer The index for the XF record
1743     */
1744     function getXfIndex()
1745     {
1746         return($this->_xf_index);
1747     }
1748
1749     /**
1750     * Used in conjunction with the set_xxx_color methods to convert a color
1751     * string into a number. Color range is 0..63 but we will restrict it
1752     * to 8..63 to comply with Gnumeric. Colors 0..7 are repeated in 8..15.
1753     *
1754     * @access private
1755     * @param string $name_color name of the color (i.e.: 'blue', 'red', etc..). Optional.
1756     * @return integer The color index
1757     */
1758     function _getColor($name_color = '')
1759     {
1760         $colors = array(
1761                         'aqua'    => 0x0F,
1762                         'cyan'    => 0x0F,
1763                         'black'   => 0x08,
1764                         'blue'    => 0x0C,
1765                         'brown'   => 0x10,
1766                         'magenta' => 0x0E,
1767                         'fuchsia' => 0x0E,
1768                         'gray'    => 0x17,
1769                         'grey'    => 0x17,
1770                         'green'   => 0x11,
1771                         'lime'    => 0x0B,
1772                         'navy'    => 0x12,
1773                         'orange'  => 0x35,
1774                         'purple'  => 0x14,
1775                         'red'     => 0x0A,
1776                         'silver'  => 0x16,
1777                         'white'   => 0x09,
1778                         'yellow'  => 0x0D
1779                        );
1780
1781         // Return the default color, 0x7FFF, if undef,
1782         if ($name_color == '') {
1783             return(0x7FFF);
1784         }
1785
1786         // or the color string converted to an integer,
1787         if (isset($colors[$name_color])) {
1788             return($colors[$name_color]);
1789         }
1790
1791         // or the default color if string is unrecognised,
1792         if (preg_match("/\D/",$name_color)) {
1793             return(0x7FFF);
1794         }
1795
1796         // or an index < 8 mapped into the correct range,
1797         if ($name_color < 8) {
1798             return($name_color + 8);
1799         }
1800
1801         // or the default color if arg is outside range,
1802         if ($name_color > 63) {
1803             return(0x7FFF);
1804         }
1805
1806         // or an integer in the valid range
1807         return($name_color);
1808     }
1809
1810     /**
1811     * Set cell alignment.
1812     *
1813     * @access public
1814     * @param string $location alignment for the cell ('left', 'right', etc...).
1815     */
1816     function setAlign($location)
1817     {
1818         if (preg_match("/\d/",$location)) {
1819             return;                      // Ignore numbers
1820         }
1821
1822         $location = strtolower($location);
1823
1824         if ($location == 'left') {
1825             $this->_text_h_align = 1;
1826         }
1827         if ($location == 'centre') {
1828             $this->_text_h_align = 2;
1829         }
1830         if ($location == 'center') {
1831             $this->_text_h_align = 2;
1832         }
1833         if ($location == 'right') {
1834             $this->_text_h_align = 3;
1835         }
1836         if ($location == 'fill') {
1837             $this->_text_h_align = 4;
1838         }
1839         if ($location == 'justify') {
1840             $this->_text_h_align = 5;
1841         }
1842         if ($location == 'merge') {
1843             $this->_text_h_align = 6;
1844         }
1845         if ($location == 'equal_space') { // For T.K.
1846             $this->_text_h_align = 7;
1847         }
1848         if ($location == 'top') {
1849             $this->_text_v_align = 0;
1850         }
1851         if ($location == 'vcentre') {
1852             $this->_text_v_align = 1;
1853         }
1854         if ($location == 'vcenter') {
1855             $this->_text_v_align = 1;
1856         }
1857         if ($location == 'bottom') {
1858             $this->_text_v_align = 2;
1859         }
1860         if ($location == 'vjustify') {
1861             $this->_text_v_align = 3;
1862         }
1863         if ($location == 'vequal_space') { // For T.K.
1864             $this->_text_v_align = 4;
1865         }
1866     }
1867
1868     /**
1869     * Set cell horizontal alignment.
1870     *
1871     * @access public
1872     * @param string $location alignment for the cell ('left', 'right', etc...).
1873     */
1874     function setHAlign($location)
1875     {
1876         if (preg_match("/\d/",$location)) {
1877             return;                      // Ignore numbers
1878         }
1879     
1880         $location = strtolower($location);
1881     
1882         if ($location == 'left') {
1883             $this->_text_h_align = 1;
1884         }
1885         if ($location == 'centre') {
1886             $this->_text_h_align = 2;
1887         }
1888         if ($location == 'center') {
1889             $this->_text_h_align = 2;
1890         }
1891         if ($location == 'right') {
1892             $this->_text_h_align = 3;
1893         }
1894         if ($location == 'fill') {
1895             $this->_text_h_align = 4;
1896         }
1897         if ($location == 'justify') {
1898             $this->_text_h_align = 5;
1899         }
1900         if ($location == 'merge') {
1901             $this->_text_h_align = 6;
1902         }
1903         if ($location == 'equal_space') { // For T.K.
1904             $this->_text_h_align = 7;
1905         }
1906     }
1907
1908     /**
1909     * Set cell vertical alignment.
1910     *
1911     * @access public
1912     * @param string $location alignment for the cell ('top', 'vleft', 'vright', etc...).
1913     */
1914     function setVAlign($location)
1915     {
1916         if (preg_match("/\d/",$location)) {
1917             return;                      // Ignore numbers
1918         }
1919     
1920         $location = strtolower($location);
1921  
1922         if ($location == 'top') {
1923             $this->_text_v_align = 0;
1924         }
1925         if ($location == 'vcentre') {
1926             $this->_text_v_align = 1;
1927         }
1928         if ($location == 'vcenter') {
1929             $this->_text_v_align = 1;
1930         }
1931         if ($location == 'bottom') {
1932             $this->_text_v_align = 2;
1933         }
1934         if ($location == 'vjustify') {
1935             $this->_text_v_align = 3;
1936         }
1937         if ($location == 'vequal_space') { // For T.K.
1938             $this->_text_v_align = 4;
1939         }
1940     }
1941
1942     /**
1943     * This is an alias for the unintuitive setAlign('merge')
1944     *
1945     * @access public
1946     */
1947     function setMerge()
1948     {
1949         $this->setAlign('merge');
1950     }
1951
1952     /**
1953     * Sets the boldness of the text.
1954     * Bold has a range 100..1000.
1955     * 0 (400) is normal. 1 (700) is bold.
1956     *
1957     * @access public
1958     * @param integer $weight Weight for the text, 0 maps to 400 (normal text),
1959                              1 maps to 700 (bold text). Valid range is: 100-1000.
1960                              It's Optional, default is 1 (bold).
1961     */
1962     function setBold($weight = 1)
1963     {
1964         if ($weight == 1) {
1965             $weight = 0x2BC;  // Bold text
1966         }
1967         if ($weight == 0) {
1968             $weight = 0x190;  // Normal text
1969         }
1970         if ($weight <  0x064) {
1971             $weight = 0x190;  // Lower bound
1972         }
1973         if ($weight >  0x3E8) {
1974             $weight = 0x190;  // Upper bound
1975         }
1976         $this->_bold = $weight;
1977     }
1978
1979
1980     /************************************
1981     * FUNCTIONS FOR SETTING CELLS BORDERS
1982     */
1983
1984     /**
1985     * Sets the width for the bottom border of the cell
1986     *
1987     * @access public
1988     * @param integer $style style of the cell border. 1 => thin, 2 => thick.
1989     */
1990     function setBottom($style)
1991     {
1992         $this->_bottom = $style;
1993     }
1994
1995     /**
1996     * Sets the width for the top border of the cell
1997     *
1998     * @access public
1999     * @param integer $style style of the cell top border. 1 => thin, 2 => thick.
2000     */
2001     function setTop($style)
2002     {
2003         $this->_top = $style;
2004     }
2005
2006     /**
2007     * Sets the width for the left border of the cell
2008     *
2009     * @access public
2010     * @param integer $style style of the cell left border. 1 => thin, 2 => thick.
2011     */
2012     function setLeft($style)
2013     {
2014         $this->_left = $style;
2015     }
2016
2017     /**
2018     * Sets the width for the right border of the cell
2019     *
2020     * @access public
2021     * @param integer $style style of the cell right border. 1 => thin, 2 => thick.
2022     */
2023     function setRight($style)
2024     {
2025         $this->_right = $style;
2026     }
2027
2028
2029     /**
2030     * Set cells borders to the same style
2031     *
2032     * @access public
2033     * @param integer $style style to apply for all cell borders. 1 => thin, 2 => thick.
2034     */
2035     function setBorder($style)
2036     {
2037         $this->setBottom($style);
2038         $this->setTop($style);
2039         $this->setLeft($style);
2040         $this->setRight($style);
2041     }
2042
2043
2044     /*******************************************
2045     * FUNCTIONS FOR SETTING CELLS BORDERS COLORS
2046     */
2047
2048     /**
2049     * Sets all the cell's borders to the same color
2050     *
2051     * @access public
2052     * @param mixed $color The color we are setting. Either a string (like 'blue'),
2053     *                     or an integer (range is [8...63]).
2054     */
2055     function setBorderColor($color)
2056     {
2057         $this->setBottomColor($color);
2058         $this->setTopColor($color);
2059         $this->setLeftColor($color);
2060         $this->setRightColor($color);
2061     }
2062
2063     /**
2064     * Sets the cell's bottom border color
2065     *
2066     * @access public
2067     * @param mixed $color either a string (like 'blue'), or an integer (range is [8...63]).
2068     */
2069     function setBottomColor($color)
2070     {
2071         $value = $this->_getColor($color);
2072         $this->_bottom_color = $value;
2073     }
2074
2075     /**
2076     * Sets the cell's top border color
2077     *
2078     * @access public
2079     * @param mixed $color either a string (like 'blue'), or an integer (range is [8...63]).
2080     */
2081     function setTopColor($color)
2082     {
2083         $value = $this->_getColor($color);
2084         $this->_top_color = $value;
2085     }
2086
2087     /**
2088     * Sets the cell's left border color
2089     *
2090     * @access public
2091     * @param mixed $color either a string (like 'blue'), or an integer (range is [8...63]).
2092     */
2093     function setLeftColor($color)
2094     {
2095         $value = $this->_getColor($color);
2096         $this->_left_color = $value;
2097     }
2098
2099     /**
2100     * Sets the cell's right border color
2101     *
2102     * @access public
2103     * @param mixed $color either a string (like 'blue'), or an integer (range is [8...63]).
2104     */
2105     function setRightColor($color)
2106     {
2107         $value = $this->_getColor($color);
2108         $this->_right_color = $value;
2109     }
2110
2111
2112     /**
2113     * Sets the cell's foreground color
2114     *
2115     * @access public
2116     * @param mixed $color either a string (like 'blue'), or an integer (range is [8...63]).
2117     */
2118     function setFgColor($color)
2119     {
2120         $value = $this->_getColor($color);
2121         $this->_fg_color = $value;
2122         if ($this->_pattern == 0) { // force color to be seen
2123             $this->_pattern = 1;
2124         }
2125     }
2126
2127     /**
2128     * Sets the cell's background color
2129     *
2130     * @access public
2131     * @param mixed $color either a string (like 'blue'), or an integer (range is [8...63]).
2132     */
2133     function setBgColor($color)
2134     {
2135         $value = $this->_getColor($color);
2136         $this->_bg_color = $value;
2137         if ($this->_pattern == 0) { // force color to be seen
2138             $this->_pattern = 1;
2139         }
2140     }
2141
2142     /**
2143     * Sets the cell's color
2144     *
2145     * @access public
2146     * @param mixed $color either a string (like 'blue'), or an integer (range is [8...63]).
2147     */
2148     function setColor($color)
2149     {
2150         $value = $this->_getColor($color);
2151         $this->_color = $value;
2152     }
2153
2154     /**
2155     * Sets the fill pattern attribute of a cell
2156     *
2157     * @access public
2158     * @param integer $arg Optional. Defaults to 1. Meaningful values are: 0-18,
2159     *                     0 meaning no background.
2160     */
2161     function setPattern($arg = 1)
2162     {
2163         $this->_pattern = $arg;
2164     }
2165
2166     /**
2167     * Sets the underline of the text
2168     *
2169     * @access public
2170     * @param integer $underline The value for underline. Possible values are:
2171     *                          1 => underline, 2 => double underline.
2172     */
2173     function setUnderline($underline)
2174     {
2175         $this->_underline = $underline;
2176     }
2177
2178     /**
2179     * Sets the font style as italic
2180     *
2181     * @access public
2182     */
2183     function setItalic()
2184     {
2185         $this->_italic = 1;
2186     }
2187
2188     /**
2189     * Sets the font size
2190     *
2191     * @access public
2192     * @param integer $size The font size (in pixels I think).
2193     */
2194     function setSize($size)
2195     {
2196         $this->_size = $size;
2197     }
2198
2199     /**
2200     * Sets text wrapping
2201     *
2202     * @access public
2203     */
2204     function setTextWrap()
2205     {
2206         $this->_text_wrap = 1;
2207     }
2208
2209     /**
2210     * Sets the orientation of the text
2211     *
2212     * @access public
2213     * @param integer $angle The rotation angle for the text (clockwise). Possible
2214                             values are: 0, 90, 270 and -1 for stacking top-to-bottom.
2215     */
2216     function setTextRotation($angle)
2217     {
2218         switch ($angle)
2219         {
2220             case 0:
2221                 $this->_rotation = 0;
2222                 break;
2223             case 90:
2224                 $this->_rotation = 3;
2225                 break;
2226             case 270:
2227                 $this->_rotation = 2;
2228                 break;
2229             case -1:
2230                 $this->_rotation = 1;
2231                 break;
2232             default :
2233                 $this->_rotation = 0;
2234                 break;
2235         }
2236     }
2237
2238     /**
2239     * Sets the numeric format.
2240     * It can be date, time, currency, etc...
2241     *
2242     * @access public
2243     * @param integer $num_format The numeric format.
2244     */
2245     function setNumFormat($num_format)
2246     {
2247         $this->_num_format = $num_format;
2248     }
2249
2250     /**
2251     * Sets font as strikeout.
2252     *
2253     * @access public
2254     */
2255     function setStrikeOut()
2256     {
2257         $this->_font_strikeout = 1;
2258     }
2259
2260     /**
2261     * Sets outlining for a font.
2262     *
2263     * @access public
2264     */
2265     function setOutLine()
2266     {
2267         $this->_font_outline = 1;
2268     }
2269
2270     /**
2271     * Sets font as shadow.
2272     *
2273     * @access public
2274     */
2275     function setShadow()
2276     {
2277         $this->_font_shadow = 1;
2278     }
2279
2280     /**
2281     * Sets the script type of the text
2282     *
2283     * @access public
2284     * @param integer $script The value for script type. Possible values are:
2285     *                        1 => superscript, 2 => subscript.
2286     */
2287     function setScript($script)
2288     {
2289         $this->_font_script = $script;
2290     }
2291
2292      /**
2293      * Locks a cell.
2294      *
2295      * @access public
2296      */
2297      function setLocked()
2298      {
2299          $this->_locked = 1;
2300      }
2301
2302     /**
2303     * Unlocks a cell. Useful for unprotecting particular cells of a protected sheet.
2304     *
2305     * @access public
2306     */
2307     function setUnLocked()
2308     {
2309         $this->_locked = 0;
2310     }
2311
2312     /**
2313     * Sets the font family name.
2314     *
2315     * @access public
2316     * @param string $fontfamily The font family name. Possible values are:
2317     *                           'Times New Roman', 'Arial', 'Courier'.
2318     */
2319     function setFontFamily($font_family)
2320     {
2321         $this->_font_name = $font_family;
2322     }
2323 }
2324
2325 /**
2326 * Class for parsing Excel formulas
2327 *
2328 * @author   Xavier Noguer <xnoguer@rezebra.com>
2329 * @category FileFormats
2330 * @package  Spreadsheet_Excel_Writer
2331 */
2332
2333 class Spreadsheet_Excel_Writer_Parser
2334 {
2335     /**
2336     * The index of the character we are currently looking at
2337     * @var integer
2338     */
2339     var $_current_char;
2340
2341     /**
2342     * The token we are working on.
2343     * @var string
2344     */
2345     var $_current_token;
2346
2347     /**
2348     * The formula to parse
2349     * @var string
2350     */
2351     var $_formula;
2352
2353     /**
2354     * The character ahead of the current char
2355     * @var string
2356     */
2357     var $_lookahead;
2358
2359     /**
2360     * The parse tree to be generated
2361     * @var string
2362     */
2363     var $_parse_tree;
2364
2365     /**
2366     * The byte order. 1 => big endian, 0 => little endian.
2367     * @var integer
2368     */
2369     var $_byte_order;
2370
2371     /**
2372     * Array of external sheets
2373     * @var array
2374     */
2375     var $_ext_sheets;
2376
2377     /**
2378     * Array of sheet references in the form of REF structures
2379     * @var array
2380     */
2381     var $_references;
2382
2383     /**
2384     * The BIFF version for the workbook
2385     * @var integer
2386     */
2387     var $_BIFF_version;
2388
2389     // The Excel ptg indices. Dynamic members must be here (php 8.2)
2390     var $ptg;
2391         var $_functions;
2392     /**
2393     * The class constructor
2394     *
2395     * @param integer $byte_order The byte order (Little endian or Big endian) of the architecture
2396                                  (optional). 1 => big endian, 0 (default) little endian.
2397     */
2398     function __construct($byte_order, $biff_version)
2399     {
2400         $this->_current_char  = 0;
2401         $this->_BIFF_version  = $biff_version;
2402         $this->_current_token = '';       // The token we are working on.
2403         $this->_formula       = '';       // The formula to parse.
2404         $this->_lookahead     = '';       // The character ahead of the current char.
2405         $this->_parse_tree    = '';       // The parse tree to be generated.
2406         $this->_initializeHashes();      // Initialize the hashes: ptg's and function's ptg's
2407         $this->_byte_order = $byte_order; // Little Endian or Big Endian
2408         $this->_ext_sheets = array();
2409         $this->_references = array();
2410     }
2411
2412     /**
2413     * Initialize the ptg and function hashes.
2414     *
2415     * @access private
2416     */
2417     function _initializeHashes()
2418     {
2419         // The Excel ptg indices
2420         $this->ptg = array(
2421             'ptgExp'       => 0x01,
2422             'ptgTbl'       => 0x02,
2423             'ptgAdd'       => 0x03,
2424             'ptgSub'       => 0x04,
2425             'ptgMul'       => 0x05,
2426             'ptgDiv'       => 0x06,
2427             'ptgPower'     => 0x07,
2428             'ptgConcat'    => 0x08,
2429             'ptgLT'        => 0x09,
2430             'ptgLE'        => 0x0A,
2431             'ptgEQ'        => 0x0B,
2432             'ptgGE'        => 0x0C,
2433             'ptgGT'        => 0x0D,
2434             'ptgNE'        => 0x0E,
2435             'ptgIsect'     => 0x0F,
2436             'ptgUnion'     => 0x10,
2437             'ptgRange'     => 0x11,
2438             'ptgUplus'     => 0x12,
2439             'ptgUminus'    => 0x13,
2440             'ptgPercent'   => 0x14,
2441             'ptgParen'     => 0x15,
2442             'ptgMissArg'   => 0x16,
2443             'ptgStr'       => 0x17,
2444             'ptgAttr'      => 0x19,
2445             'ptgSheet'     => 0x1A,
2446             'ptgEndSheet'  => 0x1B,
2447             'ptgErr'       => 0x1C,
2448             'ptgBool'      => 0x1D,
2449             'ptgInt'       => 0x1E,
2450             'ptgNum'       => 0x1F,
2451             'ptgArray'     => 0x20,
2452             'ptgFunc'      => 0x21,
2453             'ptgFuncVar'   => 0x22,
2454             'ptgName'      => 0x23,
2455             'ptgRef'       => 0x24,
2456             'ptgArea'      => 0x25,
2457             'ptgMemArea'   => 0x26,
2458             'ptgMemErr'    => 0x27,
2459             'ptgMemNoMem'  => 0x28,
2460             'ptgMemFunc'   => 0x29,
2461             'ptgRefErr'    => 0x2A,
2462             'ptgAreaErr'   => 0x2B,
2463             'ptgRefN'      => 0x2C,
2464             'ptgAreaN'     => 0x2D,
2465             'ptgMemAreaN'  => 0x2E,
2466             'ptgMemNoMemN' => 0x2F,
2467             'ptgNameX'     => 0x39,
2468             'ptgRef3d'     => 0x3A,
2469             'ptgArea3d'    => 0x3B,
2470             'ptgRefErr3d'  => 0x3C,
2471             'ptgAreaErr3d' => 0x3D,
2472             'ptgArrayV'    => 0x40,
2473             'ptgFuncV'     => 0x41,
2474             'ptgFuncVarV'  => 0x42,
2475             'ptgNameV'     => 0x43,
2476             'ptgRefV'      => 0x44,
2477             'ptgAreaV'     => 0x45,
2478             'ptgMemAreaV'  => 0x46,
2479             'ptgMemErrV'   => 0x47,
2480             'ptgMemNoMemV' => 0x48,
2481             'ptgMemFuncV'  => 0x49,
2482             'ptgRefErrV'   => 0x4A,
2483             'ptgAreaErrV'  => 0x4B,
2484             'ptgRefNV'     => 0x4C,
2485             'ptgAreaNV'    => 0x4D,
2486             'ptgMemAreaNV' => 0x4E,
2487             'ptgMemNoMemN' => 0x4F,
2488             'ptgFuncCEV'   => 0x58,
2489             'ptgNameXV'    => 0x59,
2490             'ptgRef3dV'    => 0x5A,
2491             'ptgArea3dV'   => 0x5B,
2492             'ptgRefErr3dV' => 0x5C,
2493             'ptgAreaErr3d' => 0x5D,
2494             'ptgArrayA'    => 0x60,
2495             'ptgFuncA'     => 0x61,
2496             'ptgFuncVarA'  => 0x62,
2497             'ptgNameA'     => 0x63,
2498             'ptgRefA'      => 0x64,
2499             'ptgAreaA'     => 0x65,
2500             'ptgMemAreaA'  => 0x66,
2501             'ptgMemErrA'   => 0x67,
2502             'ptgMemNoMemA' => 0x68,
2503             'ptgMemFuncA'  => 0x69,
2504             'ptgRefErrA'   => 0x6A,
2505             'ptgAreaErrA'  => 0x6B,
2506             'ptgRefNA'     => 0x6C,
2507             'ptgAreaNA'    => 0x6D,
2508             'ptgMemAreaNA' => 0x6E,
2509             'ptgMemNoMemN' => 0x6F,
2510             'ptgFuncCEA'   => 0x78,
2511             'ptgNameXA'    => 0x79,
2512             'ptgRef3dA'    => 0x7A,
2513             'ptgArea3dA'   => 0x7B,
2514             'ptgRefErr3dA' => 0x7C,
2515             'ptgAreaErr3d' => 0x7D
2516             );
2517
2518         // Thanks to Michael Meeks and Gnumeric for the initial arg values.
2519         //
2520         // The following hash was generated by "function_locale.pl" in the distro.
2521         // Refer to function_locale.pl for non-English function names.
2522         //
2523         // The array elements are as follow:
2524         // ptg:   The Excel function ptg code.
2525         // args:  The number of arguments that the function takes:
2526         //           >=0 is a fixed number of arguments.
2527         //           -1  is a variable  number of arguments.
2528         // class: The reference, value or array class of the function args.
2529         // vol:   The function is volatile.
2530         //
2531         $this->_functions = array(
2532               // function                  ptg  args  class  vol
2533               'COUNT'           => array(   0,   -1,    0,    0 ),
2534               'IF'              => array(   1,   -1,    1,    0 ),
2535               'ISNA'            => array(   2,    1,    1,    0 ),
2536               'ISERROR'         => array(   3,    1,    1,    0 ),
2537               'SUM'             => array(   4,   -1,    0,    0 ),
2538               'AVERAGE'         => array(   5,   -1,    0,    0 ),
2539               'MIN'             => array(   6,   -1,    0,    0 ),
2540               'MAX'             => array(   7,   -1,    0,    0 ),
2541               'ROW'             => array(   8,   -1,    0,    0 ),
2542               'COLUMN'          => array(   9,   -1,    0,    0 ),
2543               'NA'              => array(  10,    0,    0,    0 ),
2544               'NPV'             => array(  11,   -1,    1,    0 ),
2545               'STDEV'           => array(  12,   -1,    0,    0 ),
2546               'DOLLAR'          => array(  13,   -1,    1,    0 ),
2547               'FIXED'           => array(  14,   -1,    1,    0 ),
2548               'SIN'             => array(  15,    1,    1,    0 ),
2549               'COS'             => array(  16,    1,    1,    0 ),
2550               'TAN'             => array(  17,    1,    1,    0 ),
2551               'ATAN'            => array(  18,    1,    1,    0 ),
2552               'PI'              => array(  19,    0,    1,    0 ),
2553               'SQRT'            => array(  20,    1,    1,    0 ),
2554               'EXP'             => array(  21,    1,    1,    0 ),
2555               'LN'              => array(  22,    1,    1,    0 ),
2556               'LOG10'           => array(  23,    1,    1,    0 ),
2557               'ABS'             => array(  24,    1,    1,    0 ),
2558               'INT'             => array(  25,    1,    1,    0 ),
2559               'SIGN'            => array(  26,    1,    1,    0 ),
2560               'ROUND'           => array(  27,    2,    1,    0 ),
2561               'LOOKUP'          => array(  28,   -1,    0,    0 ),
2562               'INDEX'           => array(  29,   -1,    0,    1 ),
2563               'REPT'            => array(  30,    2,    1,    0 ),
2564               'MID'             => array(  31,    3,    1,    0 ),
2565               'LEN'             => array(  32,    1,    1,    0 ),
2566               'VALUE'           => array(  33,    1,    1,    0 ),
2567               'TRUE'            => array(  34,    0,    1,    0 ),
2568               'FALSE'           => array(  35,    0,    1,    0 ),
2569               'AND'             => array(  36,   -1,    0,    0 ),
2570               'OR'              => array(  37,   -1,    0,    0 ),
2571               'NOT'             => array(  38,    1,    1,    0 ),
2572               'MOD'             => array(  39,    2,    1,    0 ),
2573               'DCOUNT'          => array(  40,    3,    0,    0 ),
2574               'DSUM'            => array(  41,    3,    0,    0 ),
2575               'DAVERAGE'        => array(  42,    3,    0,    0 ),
2576               'DMIN'            => array(  43,    3,    0,    0 ),
2577               'DMAX'            => array(  44,    3,    0,    0 ),
2578               'DSTDEV'          => array(  45,    3,    0,    0 ),
2579               'VAR'             => array(  46,   -1,    0,    0 ),
2580               'DVAR'            => array(  47,    3,    0,    0 ),
2581               'TEXT'            => array(  48,    2,    1,    0 ),
2582               'LINEST'          => array(  49,   -1,    0,    0 ),
2583               'TREND'           => array(  50,   -1,    0,    0 ),
2584               'LOGEST'          => array(  51,   -1,    0,    0 ),
2585               'GROWTH'          => array(  52,   -1,    0,    0 ),
2586               'PV'              => array(  56,   -1,    1,    0 ),
2587               'FV'              => array(  57,   -1,    1,    0 ),
2588               'NPER'            => array(  58,   -1,    1,    0 ),
2589               'PMT'             => array(  59,   -1,    1,    0 ),
2590               'RATE'            => array(  60,   -1,    1,    0 ),
2591               'MIRR'            => array(  61,    3,    0,    0 ),
2592               'IRR'             => array(  62,   -1,    0,    0 ),
2593               'RAND'            => array(  63,    0,    1,    1 ),
2594               'MATCH'           => array(  64,   -1,    0,    0 ),
2595               'DATE'            => array(  65,    3,    1,    0 ),
2596               'TIME'            => array(  66,    3,    1,    0 ),
2597               'DAY'             => array(  67,    1,    1,    0 ),
2598               'MONTH'           => array(  68,    1,    1,    0 ),
2599               'YEAR'            => array(  69,    1,    1,    0 ),
2600               'WEEKDAY'         => array(  70,   -1,    1,    0 ),
2601               'HOUR'            => array(  71,    1,    1,    0 ),
2602               'MINUTE'          => array(  72,    1,    1,    0 ),
2603               'SECOND'          => array(  73,    1,    1,    0 ),
2604               'NOW'             => array(  74,    0,    1,    1 ),
2605               'AREAS'           => array(  75,    1,    0,    1 ),
2606               'ROWS'            => array(  76,    1,    0,    1 ),
2607               'COLUMNS'         => array(  77,    1,    0,    1 ),
2608               'OFFSET'          => array(  78,   -1,    0,    1 ),
2609               'SEARCH'          => array(  82,   -1,    1,    0 ),
2610               'TRANSPOSE'       => array(  83,    1,    1,    0 ),
2611               'TYPE'            => array(  86,    1,    1,    0 ),
2612               'ATAN2'           => array(  97,    2,    1,    0 ),
2613               'ASIN'            => array(  98,    1,    1,    0 ),
2614               'ACOS'            => array(  99,    1,    1,    0 ),
2615               'CHOOSE'          => array( 100,   -1,    1,    0 ),
2616               'HLOOKUP'         => array( 101,   -1,    0,    0 ),
2617               'VLOOKUP'         => array( 102,   -1,    0,    0 ),
2618               'ISREF'           => array( 105,    1,    0,    0 ),
2619               'LOG'             => array( 109,   -1,    1,    0 ),
2620               'CHAR'            => array( 111,    1,    1,    0 ),
2621               'LOWER'           => array( 112,    1,    1,    0 ),
2622               'UPPER'           => array( 113,    1,    1,    0 ),
2623               'PROPER'          => array( 114,    1,    1,    0 ),
2624               'LEFT'            => array( 115,   -1,    1,    0 ),
2625               'RIGHT'           => array( 116,   -1,    1,    0 ),
2626               'EXACT'           => array( 117,    2,    1,    0 ),
2627               'TRIM'            => array( 118,    1,    1,    0 ),
2628               'REPLACE'         => array( 119,    4,    1,    0 ),
2629               'SUBSTITUTE'      => array( 120,   -1,    1,    0 ),
2630               'CODE'            => array( 121,    1,    1,    0 ),
2631               'FIND'            => array( 124,   -1,    1,    0 ),
2632               'CELL'            => array( 125,   -1,    0,    1 ),
2633               'ISERR'           => array( 126,    1,    1,    0 ),
2634               'ISTEXT'          => array( 127,    1,    1,    0 ),
2635               'ISNUMBER'        => array( 128,    1,    1,    0 ),
2636               'ISBLANK'         => array( 129,    1,    1,    0 ),
2637               'T'               => array( 130,    1,    0,    0 ),
2638               'N'               => array( 131,    1,    0,    0 ),
2639               'DATEVALUE'       => array( 140,    1,    1,    0 ),
2640               'TIMEVALUE'       => array( 141,    1,    1,    0 ),
2641               'SLN'             => array( 142,    3,    1,    0 ),
2642               'SYD'             => array( 143,    4,    1,    0 ),
2643               'DDB'             => array( 144,   -1,    1,    0 ),
2644               'INDIRECT'        => array( 148,   -1,    1,    1 ),
2645               'CALL'            => array( 150,   -1,    1,    0 ),
2646               'CLEAN'           => array( 162,    1,    1,    0 ),
2647               'MDETERM'         => array( 163,    1,    2,    0 ),
2648               'MINVERSE'        => array( 164,    1,    2,    0 ),
2649               'MMULT'           => array( 165,    2,    2,    0 ),
2650               'IPMT'            => array( 167,   -1,    1,    0 ),
2651               'PPMT'            => array( 168,   -1,    1,    0 ),
2652               'COUNTA'          => array( 169,   -1,    0,    0 ),
2653               'PRODUCT'         => array( 183,   -1,    0,    0 ),
2654               'FACT'            => array( 184,    1,    1,    0 ),
2655               'DPRODUCT'        => array( 189,    3,    0,    0 ),
2656               'ISNONTEXT'       => array( 190,    1,    1,    0 ),
2657               'STDEVP'          => array( 193,   -1,    0,    0 ),
2658               'VARP'            => array( 194,   -1,    0,    0 ),
2659               'DSTDEVP'         => array( 195,    3,    0,    0 ),
2660               'DVARP'           => array( 196,    3,    0,    0 ),
2661               'TRUNC'           => array( 197,   -1,    1,    0 ),
2662               'ISLOGICAL'       => array( 198,    1,    1,    0 ),
2663               'DCOUNTA'         => array( 199,    3,    0,    0 ),
2664               'ROUNDUP'         => array( 212,    2,    1,    0 ),
2665               'ROUNDDOWN'       => array( 213,    2,    1,    0 ),
2666               'RANK'            => array( 216,   -1,    0,    0 ),
2667               'ADDRESS'         => array( 219,   -1,    1,    0 ),
2668               'DAYS360'         => array( 220,   -1,    1,    0 ),
2669               'TODAY'           => array( 221,    0,    1,    1 ),
2670               'VDB'             => array( 222,   -1,    1,    0 ),
2671               'MEDIAN'          => array( 227,   -1,    0,    0 ),
2672               'SUMPRODUCT'      => array( 228,   -1,    2,    0 ),
2673               'SINH'            => array( 229,    1,    1,    0 ),
2674               'COSH'            => array( 230,    1,    1,    0 ),
2675               'TANH'            => array( 231,    1,    1,    0 ),
2676               'ASINH'           => array( 232,    1,    1,    0 ),
2677               'ACOSH'           => array( 233,    1,    1,    0 ),
2678               'ATANH'           => array( 234,    1,    1,    0 ),
2679               'DGET'            => array( 235,    3,    0,    0 ),
2680               'INFO'            => array( 244,    1,    1,    1 ),
2681               'DB'              => array( 247,   -1,    1,    0 ),
2682               'FREQUENCY'       => array( 252,    2,    0,    0 ),
2683               'ERROR.TYPE'      => array( 261,    1,    1,    0 ),
2684               'REGISTER.ID'     => array( 267,   -1,    1,    0 ),
2685               'AVEDEV'          => array( 269,   -1,    0,    0 ),
2686               'BETADIST'        => array( 270,   -1,    1,    0 ),
2687               'GAMMALN'         => array( 271,    1,    1,    0 ),
2688               'BETAINV'         => array( 272,   -1,    1,    0 ),
2689               'BINOMDIST'       => array( 273,    4,    1,    0 ),
2690               'CHIDIST'         => array( 274,    2,    1,    0 ),
2691               'CHIINV'          => array( 275,    2,    1,    0 ),
2692               'COMBIN'          => array( 276,    2,    1,    0 ),
2693               'CONFIDENCE'      => array( 277,    3,    1,    0 ),
2694               'CRITBINOM'       => array( 278,    3,    1,    0 ),
2695               'EVEN'            => array( 279,    1,    1,    0 ),
2696               'EXPONDIST'       => array( 280,    3,    1,    0 ),
2697               'FDIST'           => array( 281,    3,    1,    0 ),
2698               'FINV'            => array( 282,    3,    1,    0 ),
2699               'FISHER'          => array( 283,    1,    1,    0 ),
2700               'FISHERINV'       => array( 284,    1,    1,    0 ),
2701               'FLOOR'           => array( 285,    2,    1,    0 ),
2702               'GAMMADIST'       => array( 286,    4,    1,    0 ),
2703               'GAMMAINV'        => array( 287,    3,    1,    0 ),
2704               'CEILING'         => array( 288,    2,    1,    0 ),
2705               'HYPGEOMDIST'     => array( 289,    4,    1,    0 ),
2706               'LOGNORMDIST'     => array( 290,    3,    1,    0 ),
2707               'LOGINV'          => array( 291,    3,    1,    0 ),
2708               'NEGBINOMDIST'    => array( 292,    3,    1,    0 ),
2709               'NORMDIST'        => array( 293,    4,    1,    0 ),
2710               'NORMSDIST'       => array( 294,    1,    1,    0 ),
2711               'NORMINV'         => array( 295,    3,    1,    0 ),
2712               'NORMSINV'        => array( 296,    1,    1,    0 ),
2713               'STANDARDIZE'     => array( 297,    3,    1,    0 ),
2714               'ODD'             => array( 298,    1,    1,    0 ),
2715               'PERMUT'          => array( 299,    2,    1,    0 ),
2716               'POISSON'         => array( 300,    3,    1,    0 ),
2717               'TDIST'           => array( 301,    3,    1,    0 ),
2718               'WEIBULL'         => array( 302,    4,    1,    0 ),
2719               'SUMXMY2'         => array( 303,    2,    2,    0 ),
2720               'SUMX2MY2'        => array( 304,    2,    2,    0 ),
2721               'SUMX2PY2'        => array( 305,    2,    2,    0 ),
2722               'CHITEST'         => array( 306,    2,    2,    0 ),
2723               'CORREL'          => array( 307,    2,    2,    0 ),
2724               'COVAR'           => array( 308,    2,    2,    0 ),
2725               'FORECAST'        => array( 309,    3,    2,    0 ),
2726               'FTEST'           => array( 310,    2,    2,    0 ),
2727               'INTERCEPT'       => array( 311,    2,    2,    0 ),
2728               'PEARSON'         => array( 312,    2,    2,    0 ),
2729               'RSQ'             => array( 313,    2,    2,    0 ),
2730               'STEYX'           => array( 314,    2,    2,    0 ),
2731               'SLOPE'           => array( 315,    2,    2,    0 ),
2732               'TTEST'           => array( 316,    4,    2,    0 ),
2733               'PROB'            => array( 317,   -1,    2,    0 ),
2734               'DEVSQ'           => array( 318,   -1,    0,    0 ),
2735               'GEOMEAN'         => array( 319,   -1,    0,    0 ),
2736               'HARMEAN'         => array( 320,   -1,    0,    0 ),
2737               'SUMSQ'           => array( 321,   -1,    0,    0 ),
2738               'KURT'            => array( 322,   -1,    0,    0 ),
2739               'SKEW'            => array( 323,   -1,    0,    0 ),
2740               'ZTEST'           => array( 324,   -1,    0,    0 ),
2741               'LARGE'           => array( 325,    2,    0,    0 ),
2742               'SMALL'           => array( 326,    2,    0,    0 ),
2743               'QUARTILE'        => array( 327,    2,    0,    0 ),
2744               'PERCENTILE'      => array( 328,    2,    0,    0 ),
2745               'PERCENTRANK'     => array( 329,   -1,    0,    0 ),
2746               'MODE'            => array( 330,   -1,    2,    0 ),
2747               'TRIMMEAN'        => array( 331,    2,    0,    0 ),
2748               'TINV'            => array( 332,    2,    1,    0 ),
2749               'CONCATENATE'     => array( 336,   -1,    1,    0 ),
2750               'POWER'           => array( 337,    2,    1,    0 ),
2751               'RADIANS'         => array( 342,    1,    1,    0 ),
2752               'DEGREES'         => array( 343,    1,    1,    0 ),
2753               'SUBTOTAL'        => array( 344,   -1,    0,    0 ),
2754               'SUMIF'           => array( 345,   -1,    0,    0 ),
2755               'COUNTIF'         => array( 346,    2,    0,    0 ),
2756               'COUNTBLANK'      => array( 347,    1,    0,    0 ),
2757               'ROMAN'           => array( 354,   -1,    1,    0 )
2758               );
2759     }
2760
2761     /**
2762     * Convert a token to the proper ptg value.
2763     *
2764     * @access private
2765     * @param mixed $token The token to convert.
2766     * @return mixed the converted token on success. Die if the token
2767     *               is not recognized
2768     */
2769     function _convert($token)
2770     {
2771         if (preg_match("/^\"[^\"]{0,255}\"$/", $token)) {
2772             return $this->_convertString($token);
2773
2774         } elseif (is_numeric($token)) {
2775             return $this->_convertNumber($token);
2776
2777         // match references like A1 or $A$1
2778         } elseif (preg_match('/^\$?([A-Ia-i]?[A-Za-z])\$?(\d+)$/',$token)) {
2779             return $this->_convertRef2d($token);
2780
2781         // match external references like Sheet1!A1 or Sheet1:Sheet2!A1
2782         } elseif (preg_match("/^\w+(\:\w+)?\![A-Ia-i]?[A-Za-z](\d+)$/u",$token)) {
2783             return $this->_convertRef3d($token);
2784
2785         // match external references like 'Sheet1'!A1 or 'Sheet1:Sheet2'!A1
2786         } elseif (preg_match("/^'[\w -]+(\:[\w -]+)?'\![A-Ia-i]?[A-Za-z](\d+)$/u",$token)) {
2787             return $this->_convertRef3d($token);
2788
2789         // match ranges like A1:B2
2790         } elseif (preg_match("/^(\$)?[A-Ia-i]?[A-Za-z](\$)?(\d+)\:(\$)?[A-Ia-i]?[A-Za-z](\$)?(\d+)$/",$token)) {
2791             return $this->_convertRange2d($token);
2792
2793         // match ranges like A1..B2
2794         } elseif (preg_match("/^(\$)?[A-Ia-i]?[A-Za-z](\$)?(\d+)\.\.(\$)?[A-Ia-i]?[A-Za-z](\$)?(\d+)$/",$token)) {
2795             return $this->_convertRange2d($token);
2796
2797         // match external ranges like Sheet1!A1 or Sheet1:Sheet2!A1:B2
2798         } elseif (preg_match("/^\w+(\:\w+)?\!([A-Ia-i]?[A-Za-z])?(\d+)\:([A-Ia-i]?[A-Za-z])?(\d+)$/u",$token)) {
2799             return $this->_convertRange3d($token);
2800
2801         // match external ranges like 'Sheet1'!A1 or 'Sheet1:Sheet2'!A1:B2
2802         } elseif (preg_match("/^'[\w -]+(\:[\w -]+)?'\!([A-Ia-i]?[A-Za-z])?(\d+)\:([A-Ia-i]?[A-Za-z])?(\d+)$/u",$token)) {
2803             return $this->_convertRange3d($token);
2804
2805         // operators (including parentheses)
2806         } elseif (isset($this->ptg[$token])) {
2807             return pack("C", $this->ptg[$token]);
2808
2809         // commented so argument number can be processed correctly. See toReversePolish().
2810         /*elseif (preg_match("/[A-Z0-9\xc0-\xdc\.]+/",$token))
2811         {
2812             return($this->_convertFunction($token,$this->_func_args));
2813         }*/
2814
2815         // if it's an argument, ignore the token (the argument remains)
2816         } elseif ($token == 'arg') {
2817             return '';
2818         }
2819         // TODO: use real error codes
2820         die("Unknown token $token");
2821     }
2822
2823     /**
2824     * Convert a number token to ptgInt or ptgNum
2825     *
2826     * @access private
2827     * @param mixed $num an integer or double for conversion to its ptg value
2828     */
2829     function _convertNumber($num)
2830     {
2831         // Integer in the range 0..2**16-1
2832         if ((preg_match("/^\d+$/", $num)) and ($num <= 65535)) {
2833             return pack("Cv", $this->ptg['ptgInt'], $num);
2834         } else { // A float
2835             if ($this->_byte_order) { // if it's Big Endian
2836                 $num = strrev($num);
2837             }
2838             return pack("Cd", $this->ptg['ptgNum'], $num);
2839         }
2840     }
2841
2842     /**
2843     * Convert a string token to ptgStr
2844     *
2845     * @access private
2846     * @param string $string A string for conversion to its ptg value.
2847     * @return mixed the converted token on success. PEAR_Error if the string
2848     *               is longer than 255 characters.
2849     */
2850     function _convertString($string)
2851     {
2852         // chop away beggining and ending quotes
2853         $string = substr($string, 1, strlen($string) - 2);
2854         if (strlen($string) > 255) {
2855             die("String is too long");
2856         }
2857
2858         if ($this->_BIFF_version == 0x0500) {
2859             return pack("CC", $this->ptg['ptgStr'], strlen($string)).$string;
2860         } elseif ($this->_BIFF_version == 0x0600) {
2861             $encoding = 0;   // TODO: Unicode support
2862             return pack("CCC", $this->ptg['ptgStr'], strlen($string), $encoding).$string;
2863         }
2864     }
2865
2866     /**
2867     * Convert a function to a ptgFunc or ptgFuncVarV depending on the number of
2868     * args that it takes.
2869     *
2870     * @access private
2871     * @param string  $token    The name of the function for convertion to ptg value.
2872     * @param integer $num_args The number of arguments the function receives.
2873     * @return string The packed ptg for the function
2874     */
2875     function _convertFunction($token, $num_args)
2876     {
2877         $args     = $this->_functions[$token][1];
2878         $volatile = $this->_functions[$token][3];
2879
2880         // Fixed number of args eg. TIME($i,$j,$k).
2881         if ($args >= 0) {
2882             return pack("Cv", $this->ptg['ptgFuncV'], $this->_functions[$token][0]);
2883         }
2884         // Variable number of args eg. SUM($i,$j,$k, ..).
2885         if ($args == -1) {
2886             return pack("CCv", $this->ptg['ptgFuncVarV'], $num_args, $this->_functions[$token][0]);
2887         }
2888     }
2889
2890     /**
2891     * Convert an Excel range such as A1:D4 to a ptgRefV.
2892     *
2893     * @access private
2894     * @param string $range An Excel range in the A1:A2 or A1..A2 format.
2895     */
2896     function _convertRange2d($range)
2897     {
2898         $class = 2; // as far as I know, this is magick.
2899
2900         // Split the range into 2 cell refs
2901         if (preg_match("/^([A-Ia-i]?[A-Za-z])(\d+)\:([A-Ia-i]?[A-Za-z])(\d+)$/", $range)) {
2902             list($cell1, $cell2) = preg_split('/:/', $range);
2903         } elseif (preg_match("/^([A-Ia-i]?[A-Za-z])(\d+)\.\.([A-Ia-i]?[A-Za-z])(\d+)$/", $range)) {
2904             list($cell1, $cell2) = preg_split('/\.\./', $range);
2905
2906         } else {
2907             // TODO: use real error codes
2908             die("Unknown range separator");
2909         }
2910
2911         // Convert the cell references
2912         $cell_array1 = $this->_cellToPackedRowcol($cell1);
2913         list($row1, $col1) = $cell_array1;
2914         $cell_array2 = $this->_cellToPackedRowcol($cell2);
2915         list($row2, $col2) = $cell_array2;
2916
2917         // The ptg value depends on the class of the ptg.
2918         if ($class == 0) {
2919             $ptgArea = pack("C", $this->ptg['ptgArea']);
2920         } elseif ($class == 1) {
2921             $ptgArea = pack("C", $this->ptg['ptgAreaV']);
2922         } elseif ($class == 2) {
2923             $ptgArea = pack("C", $this->ptg['ptgAreaA']);
2924         } else {
2925             // TODO: use real error codes
2926             die("Unknown class $class");
2927         }
2928         return $ptgArea . $row1 . $row2 . $col1. $col2;
2929     }
2930
2931     /**
2932     * Convert an Excel 3d range such as "Sheet1!A1:D4" or "Sheet1:Sheet2!A1:D4" to
2933     * a ptgArea3d.
2934     *
2935     * @access private
2936     * @param string $token An Excel range in the Sheet1!A1:A2 format.
2937     * @return mixed The packed ptgArea3d token on success, PEAR_Error on failure.
2938     */
2939     function _convertRange3d($token)
2940     {
2941         $class = 2; // as far as I know, this is magick.
2942
2943         // Split the ref at the ! symbol
2944         list($ext_ref, $range) = preg_split('/!/', $token);
2945
2946         // Convert the external reference part (different for BIFF8)
2947         if ($this->_BIFF_version == 0x0500) {
2948             $ext_ref = $this->_packExtRef($ext_ref);
2949         } elseif ($this->_BIFF_version == 0x0600) {
2950              $ext_ref = $this->_getRefIndex($ext_ref);
2951         }
2952
2953         // Split the range into 2 cell refs
2954         list($cell1, $cell2) = preg_split('/:/', $range);
2955
2956         // Convert the cell references
2957         if (preg_match("/^(\$)?[A-Ia-i]?[A-Za-z](\$)?(\d+)$/", $cell1)) {
2958             $cell_array1 = $this->_cellToPackedRowcol($cell1);
2959             list($row1, $col1) = $cell_array1;
2960             $cell_array2 = $this->_cellToPackedRowcol($cell2);
2961             list($row2, $col2) = $cell_array2;
2962         } else { // It's a rows range (like 26:27)
2963              $cells_array = $this->_rangeToPackedRange($cell1.':'.$cell2);
2964              list($row1, $col1, $row2, $col2) = $cells_array;
2965         }
2966
2967         // The ptg value depends on the class of the ptg.
2968         if ($class == 0) {
2969             $ptgArea = pack("C", $this->ptg['ptgArea3d']);
2970         } elseif ($class == 1) {
2971             $ptgArea = pack("C", $this->ptg['ptgArea3dV']);
2972         } elseif ($class == 2) {
2973             $ptgArea = pack("C", $this->ptg['ptgArea3dA']);
2974         } else {
2975             die("Unknown class $class");
2976         }
2977
2978         return $ptgArea . $ext_ref . $row1 . $row2 . $col1. $col2;
2979     }
2980
2981     /**
2982     * Convert an Excel reference such as A1, $B2, C$3 or $D$4 to a ptgRefV.
2983     *
2984     * @access private
2985     * @param string $cell An Excel cell reference
2986     * @return string The cell in packed() format with the corresponding ptg
2987     */
2988     function _convertRef2d($cell)
2989     {
2990         $class = 2; // as far as I know, this is magick.
2991
2992         // Convert the cell reference
2993         $cell_array = $this->_cellToPackedRowcol($cell);
2994         list($row, $col) = $cell_array;
2995
2996         // The ptg value depends on the class of the ptg.
2997         if ($class == 0) {
2998             $ptgRef = pack("C", $this->ptg['ptgRef']);
2999         } elseif ($class == 1) {
3000             $ptgRef = pack("C", $this->ptg['ptgRefV']);
3001         } elseif ($class == 2) {
3002             $ptgRef = pack("C", $this->ptg['ptgRefA']);
3003         } else {
3004             // TODO: use real error codes
3005             die("Unknown class $class");
3006         }
3007         return $ptgRef.$row.$col;
3008     }
3009
3010     /**
3011     * Convert an Excel 3d reference such as "Sheet1!A1" or "Sheet1:Sheet2!A1" to a
3012     * ptgRef3d.
3013     *
3014     * @access private
3015     * @param string $cell An Excel cell reference
3016     * @return mixed The packed ptgRef3d token on success, PEAR_Error on failure.
3017     */
3018     function _convertRef3d($cell)
3019     {
3020         $class = 2; // as far as I know, this is magick.
3021
3022         // Split the ref at the ! symbol
3023         list($ext_ref, $cell) = preg_split('/!/', $cell);
3024
3025         // Convert the external reference part (different for BIFF8)
3026         if ($this->_BIFF_version == 0x0500) {
3027             $ext_ref = $this->_packExtRef($ext_ref);
3028         } elseif ($this->_BIFF_version == 0x0600) {
3029             $ext_ref = $this->_getRefIndex($ext_ref);
3030         }
3031
3032         // Convert the cell reference part
3033         list($row, $col) = $this->_cellToPackedRowcol($cell);
3034
3035         // The ptg value depends on the class of the ptg.
3036         if ($class == 0) {
3037             $ptgRef = pack("C", $this->ptg['ptgRef3d']);
3038         } elseif ($class == 1) {
3039             $ptgRef = pack("C", $this->ptg['ptgRef3dV']);
3040         } elseif ($class == 2) {
3041             $ptgRef = pack("C", $this->ptg['ptgRef3dA']);
3042         } else {
3043             die("Unknown class $class");
3044         }
3045
3046         return $ptgRef . $ext_ref. $row . $col;
3047     }
3048
3049     /**
3050     * Convert the sheet name part of an external reference, for example "Sheet1" or
3051     * "Sheet1:Sheet2", to a packed structure.
3052     *
3053     * @access private
3054     * @param string $ext_ref The name of the external reference
3055     * @return string The reference index in packed() format
3056     */
3057     function _packExtRef($ext_ref)
3058     {
3059         $ext_ref = preg_replace("/^'/", '', $ext_ref); // Remove leading  ' if any.
3060         $ext_ref = preg_replace("/'$/", '', $ext_ref); // Remove trailing ' if any.
3061
3062         // Check if there is a sheet range eg., Sheet1:Sheet2.
3063         if (preg_match("/:/", $ext_ref)) {
3064             list($sheet_name1, $sheet_name2) = preg_split('/:/', $ext_ref);
3065
3066             $sheet1 = $this->_getSheetIndex($sheet_name1);
3067             if ($sheet1 == -1) {
3068                 die("Unknown sheet name $sheet_name1 in formula");
3069             }
3070             $sheet2 = $this->_getSheetIndex($sheet_name2);
3071             if ($sheet2 == -1) {
3072                 die("Unknown sheet name $sheet_name2 in formula");
3073             }
3074
3075             // Reverse max and min sheet numbers if necessary
3076             if ($sheet1 > $sheet2) {
3077                 list($sheet1, $sheet2) = array($sheet2, $sheet1);
3078             }
3079         } else { // Single sheet name only.
3080             $sheet1 = $this->_getSheetIndex($ext_ref);
3081             if ($sheet1 == -1) {
3082                 die("Unknown sheet name $ext_ref in formula");
3083             }
3084             $sheet2 = $sheet1;
3085         }
3086
3087         // References are stored relative to 0xFFFF.
3088         $offset = -1 - $sheet1;
3089
3090         return pack('vdvv', $offset, 0x00, $sheet1, $sheet2);
3091     }
3092
3093     /**
3094     * Look up the REF index that corresponds to an external sheet name
3095     * (or range). If it doesn't exist yet add it to the workbook's references
3096     * array. It assumes all sheet names given must exist.
3097     *
3098     * @access private
3099     * @param string $ext_ref The name of the external reference
3100     * @return mixed The reference index in packed() format on success,
3101     *               PEAR_Error on failure
3102     */
3103     function _getRefIndex($ext_ref)
3104     {
3105         $ext_ref = preg_replace("/^'/", '', $ext_ref); // Remove leading  ' if any.
3106         $ext_ref = preg_replace("/'$/", '', $ext_ref); // Remove trailing ' if any.
3107
3108         // Check if there is a sheet range eg., Sheet1:Sheet2.
3109         if (preg_match("/:/", $ext_ref)) {
3110             list($sheet_name1, $sheet_name2) = preg_split('/:/', $ext_ref);
3111
3112             $sheet1 = $this->_getSheetIndex($sheet_name1);
3113             if ($sheet1 == -1) {
3114                 die("Unknown sheet name $sheet_name1 in formula");
3115             }
3116             $sheet2 = $this->_getSheetIndex($sheet_name2);
3117             if ($sheet2 == -1) {
3118                 die("Unknown sheet name $sheet_name2 in formula");
3119             }
3120
3121             // Reverse max and min sheet numbers if necessary
3122             if ($sheet1 > $sheet2) {
3123                 list($sheet1, $sheet2) = array($sheet2, $sheet1);
3124             }
3125         } else { // Single sheet name only.
3126             $sheet1 = $this->_getSheetIndex($ext_ref);
3127             if ($sheet1 == -1) {
3128                 die("Unknown sheet name $ext_ref in formula");
3129             }
3130             $sheet2 = $sheet1;
3131         }
3132
3133         // assume all references belong to this document
3134         $supbook_index = 0x00;
3135         $ref = pack('vvv', $supbook_index, $sheet1, $sheet2);
3136         $total_references = count($this->_references);
3137         $index = -1;
3138         for ($i = 0; $i < $total_references; $i++) {
3139             if ($ref == $this->_references[$i]) {
3140                 $index = $i;
3141                 break;
3142             }
3143         }
3144         // if REF was not found add it to references array
3145         if ($index == -1) {
3146             $this->_references[$total_references] = $ref;
3147             $index = $total_references;
3148         }
3149
3150         return pack('v', $index);
3151     }
3152
3153     /**
3154     * Look up the index that corresponds to an external sheet name. The hash of
3155     * sheet names is updated by the addworksheet() method of the
3156     * Spreadsheet_Excel_Writer_Workbook class.
3157     *
3158     * @access private
3159     * @return integer The sheet index, -1 if the sheet was not found
3160     */
3161     function _getSheetIndex($sheet_name)
3162     {
3163         if (!isset($this->_ext_sheets[$sheet_name])) {
3164             return -1;
3165         } else {
3166             return $this->_ext_sheets[$sheet_name];
3167         }
3168     }
3169
3170     /**
3171     * This method is used to update the array of sheet names. It is
3172     * called by the addWorksheet() method of the
3173     * Spreadsheet_Excel_Writer_Workbook class.
3174     *
3175     * @access public
3176     * @see Spreadsheet_Excel_Writer_Workbook::addWorksheet()
3177     * @param string  $name  The name of the worksheet being added
3178     * @param integer $index The index of the worksheet being added
3179     */
3180     function setExtSheet($name, $index)
3181     {
3182         $this->_ext_sheets[$name] = $index;
3183     }
3184
3185     /**
3186     * pack() row and column into the required 3 or 4 byte format.
3187     *
3188     * @access private
3189     * @param string $cell The Excel cell reference to be packed
3190     * @return array Array containing the row and column in packed() format
3191     */
3192     function _cellToPackedRowcol($cell)
3193     {
3194         $cell = strtoupper($cell);
3195         list($row, $col, $row_rel, $col_rel) = $this->_cellToRowcol($cell);
3196         if ($col >= 256) {
3197             die("Column in: $cell greater than 255");
3198         }
3199         // FIXME: change for BIFF8
3200         if ($row >= 16384) {
3201             die("Row in: $cell greater than 16384 ");
3202         }
3203
3204         // Set the high bits to indicate if row or col are relative.
3205         if ($this->_BIFF_version == 0x0500) {
3206             $row    |= $col_rel << 14;
3207             $row    |= $row_rel << 15;
3208             $col     = pack('C', $col);
3209         } elseif ($this->_BIFF_version == 0x0600) {
3210             $col    |= $col_rel << 14;
3211             $col    |= $row_rel << 15;
3212             $col     = pack('v', $col);
3213         }
3214         $row     = pack('v', $row);
3215
3216         return array($row, $col);
3217     }
3218
3219     /**
3220     * pack() row range into the required 3 or 4 byte format.
3221     * Just using maximum col/rows, which is probably not the correct solution
3222     *
3223     * @access private
3224     * @param string $range The Excel range to be packed
3225     * @return array Array containing (row1,col1,row2,col2) in packed() format
3226     */
3227     function _rangeToPackedRange($range)
3228     {
3229         preg_match('/(\$)?(\d+)\:(\$)?(\d+)/', $range, $match);
3230         // return absolute rows if there is a $ in the ref
3231         $row1_rel = empty($match[1]) ? 1 : 0;
3232         $row1     = $match[2];
3233         $row2_rel = empty($match[3]) ? 1 : 0;
3234         $row2     = $match[4];
3235         // Convert 1-index to zero-index
3236         $row1--;
3237         $row2--;
3238         // Trick poor inocent Excel
3239         $col1 = 0;
3240         $col2 = 16383; // FIXME: maximum possible value for Excel 5 (change this!!!)
3241
3242         // FIXME: this changes for BIFF8
3243         if (($row1 >= 16384) or ($row2 >= 16384)) {
3244             die("Row in: $range greater than 16384 ");
3245         }
3246
3247         // Set the high bits to indicate if rows are relative.
3248         if ($this->_BIFF_version == 0x0500) {
3249             $row1    |= $row1_rel << 14; // FIXME: probably a bug
3250             $row2    |= $row2_rel << 15;
3251             $col1     = pack('C', $col1);
3252             $col2     = pack('C', $col2);
3253         } elseif ($this->_BIFF_version == 0x0600) {
3254             $col1    |= $row1_rel << 15;
3255             $col2    |= $row2_rel << 15;
3256             $col1     = pack('v', $col1);
3257             $col2     = pack('v', $col2);
3258         }
3259         $row1     = pack('v', $row1);
3260         $row2     = pack('v', $row2);
3261
3262         return array($row1, $col1, $row2, $col2);
3263     }
3264
3265     /**
3266     * Convert an Excel cell reference such as A1 or $B2 or C$3 or $D$4 to a zero
3267     * indexed row and column number. Also returns two (0,1) values to indicate
3268     * whether the row or column are relative references.
3269     *
3270     * @access private
3271     * @param string $cell The Excel cell reference in A1 format.
3272     * @return array
3273     */
3274     function _cellToRowcol($cell)
3275     {
3276         preg_match('/(\$)?([A-I]?[A-Z])(\$)?(\d+)/',$cell,$match);
3277         // return absolute column if there is a $ in the ref
3278         $col_rel = empty($match[1]) ? 1 : 0;
3279         $col_ref = $match[2];
3280         $row_rel = empty($match[3]) ? 1 : 0;
3281         $row     = $match[4];
3282
3283         // Convert base26 column string to a number.
3284         $expn   = strlen($col_ref) - 1;
3285         $col    = 0;
3286         $col_ref_length = strlen($col_ref);
3287         for ($i = 0; $i < $col_ref_length; $i++) {
3288             $col += (ord($col_ref[$i]) - ord('A') + 1) * pow(26, $expn);
3289             $expn--;
3290         }
3291
3292         // Convert 1-index to zero-index
3293         $row--;
3294         $col--;
3295
3296         return array($row, $col, $row_rel, $col_rel);
3297     }
3298
3299     /**
3300     * Advance to the next valid token.
3301     *
3302     * @access private
3303     */
3304     function _advance()
3305     {
3306         $i = $this->_current_char;
3307         $formula_length = strlen($this->_formula);
3308         // eat up white spaces
3309         if ($i < $formula_length) {
3310             while ($this->_formula[$i] == " ") {
3311                 $i++;
3312             }
3313
3314             if ($i < ($formula_length - 1)) {
3315                 $this->_lookahead = $this->_formula[$i+1];
3316             }
3317             $token = '';
3318         }
3319
3320         while ($i < $formula_length) {
3321             $token .= $this->_formula[$i];
3322             if ($i < ($formula_length - 1)) {
3323                 $this->_lookahead = $this->_formula[$i+1];
3324             } else {
3325                 $this->_lookahead = '';
3326             }
3327
3328             if ($this->_match($token) != '') {
3329                 //if ($i < strlen($this->_formula) - 1) {
3330                 //    $this->_lookahead = $this->_formula{$i+1};
3331                 //}
3332                 $this->_current_char = $i + 1;
3333                 $this->_current_token = $token;
3334                 return 1;
3335             }
3336
3337             if ($i < ($formula_length - 2)) {
3338                 $this->_lookahead = $this->_formula[$i+2];
3339             } else { // if we run out of characters _lookahead becomes empty
3340                 $this->_lookahead = '';
3341             }
3342             $i++;
3343         }
3344         //die("Lexical error ".$this->_current_char);
3345     }
3346
3347     /**
3348     * Checks if it's a valid token.
3349     *
3350     * @access private
3351     * @param mixed $token The token to check.
3352     * @return mixed       The checked token or false on failure
3353     */
3354     function _match($token)
3355     {
3356         switch($token) {
3357             case SPREADSHEET_EXCEL_WRITER_ADD:
3358                 return $token;
3359                 break;
3360             case SPREADSHEET_EXCEL_WRITER_SUB:
3361                 return $token;
3362                 break;
3363             case SPREADSHEET_EXCEL_WRITER_MUL:
3364                 return $token;
3365                 break;
3366             case SPREADSHEET_EXCEL_WRITER_DIV:
3367                 return $token;
3368                 break;
3369             case SPREADSHEET_EXCEL_WRITER_OPEN:
3370                 return $token;
3371                 break;
3372             case SPREADSHEET_EXCEL_WRITER_CLOSE:
3373                 return $token;
3374                 break;
3375             case SPREADSHEET_EXCEL_WRITER_COMA:
3376                 return $token;
3377                 break;
3378             case SPREADSHEET_EXCEL_WRITER_SEMICOLON:
3379                 return $token;
3380                 break;
3381             case SPREADSHEET_EXCEL_WRITER_GT:
3382                 if ($this->_lookahead == '=') { // it's a GE token
3383                     break;
3384                 }
3385                 return $token;
3386                 break;
3387             case SPREADSHEET_EXCEL_WRITER_LT:
3388                 // it's a LE or a NE token
3389                 if (($this->_lookahead == '=') or ($this->_lookahead == '>')) {
3390                     break;
3391                 }
3392                 return $token;
3393                 break;
3394             case SPREADSHEET_EXCEL_WRITER_GE:
3395                 return $token;
3396                 break;
3397             case SPREADSHEET_EXCEL_WRITER_LE:
3398                 return $token;
3399                 break;
3400             case SPREADSHEET_EXCEL_WRITER_EQ:
3401                 return $token;
3402                 break;
3403             case SPREADSHEET_EXCEL_WRITER_NE:
3404                 return $token;
3405                 break;
3406             default:
3407                 // if it's a reference
3408                 if (preg_match('/^\$?[A-Ia-i]?[A-Za-z]\$?[0-9]+$/',$token) and
3409                    !preg_match("/[0-9]/",$this->_lookahead) and 
3410                    ($this->_lookahead != ':') and ($this->_lookahead != '.') and
3411                    ($this->_lookahead != '!'))
3412                 {
3413                     return $token;
3414                 }
3415                 // If it's an external reference (Sheet1!A1 or Sheet1:Sheet2!A1)
3416                 elseif (preg_match("/^\w+(\:\w+)?\![A-Ia-i]?[A-Za-z][0-9]+$/u",$token) and
3417                        !preg_match("/[0-9]/",$this->_lookahead) and
3418                        ($this->_lookahead != ':') and ($this->_lookahead != '.'))
3419                 {
3420                     return $token;
3421                 }
3422                 // If it's an external reference ('Sheet1'!A1 or 'Sheet1:Sheet2'!A1)
3423                 elseif (preg_match("/^'[\w -]+(\:[\w -]+)?'\![A-Ia-i]?[A-Za-z][0-9]+$/u",$token) and
3424                        !preg_match("/[0-9]/",$this->_lookahead) and
3425                        ($this->_lookahead != ':') and ($this->_lookahead != '.'))
3426                 {
3427                     return $token;
3428                 }
3429                 // if it's a range (A1:A2)
3430                 elseif (preg_match("/^(\$)?[A-Ia-i]?[A-Za-z](\$)?[0-9]+:(\$)?[A-Ia-i]?[A-Za-z](\$)?[0-9]+$/",$token) and 
3431                        !preg_match("/[0-9]/",$this->_lookahead))
3432                 {
3433                     return $token;
3434                 }
3435                 // if it's a range (A1..A2)
3436                 elseif (preg_match("/^(\$)?[A-Ia-i]?[A-Za-z](\$)?[0-9]+\.\.(\$)?[A-Ia-i]?[A-Za-z](\$)?[0-9]+$/",$token) and 
3437                        !preg_match("/[0-9]/",$this->_lookahead))
3438                 {
3439                     return $token;
3440                 }
3441                 // If it's an external range like Sheet1!A1 or Sheet1:Sheet2!A1:B2
3442                 elseif (preg_match("/^\w+(\:\w+)?\!([A-Ia-i]?[A-Za-z])?[0-9]+:([A-Ia-i]?[A-Za-z])?[0-9]+$/u",$token) and
3443                        !preg_match("/[0-9]/",$this->_lookahead))
3444                 {
3445                     return $token;
3446                 }
3447                 // If it's an external range like 'Sheet1'!A1 or 'Sheet1:Sheet2'!A1:B2
3448                 elseif (preg_match("/^'[\w -]+(\:[\w -]+)?'\!([A-Ia-i]?[A-Za-z])?[0-9]+:([A-Ia-i]?[A-Za-z])?[0-9]+$/u",$token) and
3449                        !preg_match("/[0-9]/",$this->_lookahead))
3450                 {
3451                     return $token;
3452                 }
3453                 // If it's a number (check that it's not a sheet name or range)
3454                 elseif (is_numeric($token) and 
3455                         (!is_numeric($token.$this->_lookahead) or ($this->_lookahead == '')) and
3456                         ($this->_lookahead != '!') and ($this->_lookahead != ':'))
3457                 {
3458                     return $token;
3459                 }
3460                 // If it's a string (of maximum 255 characters)
3461                 elseif (preg_match("/^\"[^\"]{0,255}\"$/",$token))
3462                 {
3463                     return $token;
3464                 }
3465                 // if it's a function call
3466                 elseif (preg_match("/^[A-Z0-9\xc0-\xdc\.]+$/i",$token) and ($this->_lookahead == "("))
3467                 {
3468                     return $token;
3469                 }
3470                 return '';
3471         }
3472     }
3473
3474     /**
3475     * The parsing method. It parses a formula.
3476     *
3477     * @access public
3478     * @param string $formula The formula to parse, without the initial equal
3479     *                        sign (=).
3480     * @return mixed true on success, PEAR_Error on failure
3481     */
3482     function parse($formula)
3483     {
3484         $this->_current_char = 0;
3485         $this->_formula      = $formula;
3486         $this->_lookahead    = $formula[1];
3487         $this->_advance();
3488         $this->_parse_tree   = $this->_condition();
3489     }
3490
3491     /**
3492     * It parses a condition. It assumes the following rule:
3493     * Cond -> Expr [(">" | "<") Expr]
3494     *
3495     * @access private
3496     * @return mixed The parsed ptg'd tree on success, PEAR_Error on failure
3497     */
3498     function _condition()
3499     {
3500         $result = $this->_expression();
3501         if ($this->_current_token == SPREADSHEET_EXCEL_WRITER_LT) {
3502             $this->_advance();
3503             $result2 = $this->_expression();
3504             $result = $this->_createTree('ptgLT', $result, $result2);
3505         } elseif ($this->_current_token == SPREADSHEET_EXCEL_WRITER_GT) {
3506             $this->_advance();
3507             $result2 = $this->_expression();
3508             $result = $this->_createTree('ptgGT', $result, $result2);
3509         } elseif ($this->_current_token == SPREADSHEET_EXCEL_WRITER_LE) {
3510             $this->_advance();
3511             $result2 = $this->_expression();
3512             $result = $this->_createTree('ptgLE', $result, $result2);
3513         } elseif ($this->_current_token == SPREADSHEET_EXCEL_WRITER_GE) {
3514             $this->_advance();
3515             $result2 = $this->_expression();
3516             $result = $this->_createTree('ptgGE', $result, $result2);
3517         } elseif ($this->_current_token == SPREADSHEET_EXCEL_WRITER_EQ) {
3518             $this->_advance();
3519             $result2 = $this->_expression();
3520             $result = $this->_createTree('ptgEQ', $result, $result2);
3521         } elseif ($this->_current_token == SPREADSHEET_EXCEL_WRITER_NE) {
3522             $this->_advance();
3523             $result2 = $this->_expression();
3524             $result = $this->_createTree('ptgNE', $result, $result2);
3525         }
3526         return $result;
3527     }
3528
3529     /**
3530     * It parses a expression. It assumes the following rule:
3531     * Expr -> Term [("+" | "-") Term]
3532     *      -> "string"
3533     *      -> "-" Term
3534     *
3535     * @access private
3536     * @return mixed The parsed ptg'd tree on success, PEAR_Error on failure
3537     */
3538     function _expression()
3539     {
3540         // If it's a string return a string node
3541         if (preg_match("/^\"[^\"]{0,255}\"$/", $this->_current_token)) {
3542             $result = $this->_createTree($this->_current_token, '', '');
3543             $this->_advance();
3544             return $result;
3545         } elseif ($this->_current_token == SPREADSHEET_EXCEL_WRITER_SUB) {
3546             // catch "-" Term
3547             $this->_advance();
3548             $result2 = $this->_expression();
3549             $result = $this->_createTree('ptgUminus', $result2, '');
3550             return $result;
3551         }
3552         $result = $this->_term();
3553         while (($this->_current_token == SPREADSHEET_EXCEL_WRITER_ADD) or
3554                ($this->_current_token == SPREADSHEET_EXCEL_WRITER_SUB)) {
3555         /**/
3556             if ($this->_current_token == SPREADSHEET_EXCEL_WRITER_ADD) {
3557                 $this->_advance();
3558                 $result2 = $this->_term();
3559                 $result = $this->_createTree('ptgAdd', $result, $result2);
3560             } else {
3561                 $this->_advance();
3562                 $result2 = $this->_term();
3563                 $result = $this->_createTree('ptgSub', $result, $result2);
3564             }
3565         }
3566         return $result;
3567     }
3568
3569     /**
3570     * This function just introduces a ptgParen element in the tree, so that Excel
3571     * doesn't get confused when working with a parenthesized formula afterwards.
3572     *
3573     * @access private
3574     * @see _fact()
3575     * @return array The parsed ptg'd tree
3576     */
3577     function _parenthesizedExpression()
3578     {
3579         $result = $this->_createTree('ptgParen', $this->_expression(), '');
3580         return $result;
3581     }
3582
3583     /**
3584     * It parses a term. It assumes the following rule:
3585     * Term -> Fact [("*" | "/") Fact]
3586     *
3587     * @access private
3588     * @return mixed The parsed ptg'd tree on success, PEAR_Error on failure
3589     */
3590     function _term()
3591     {
3592         $result = $this->_fact();
3593         while (($this->_current_token == SPREADSHEET_EXCEL_WRITER_MUL) or
3594                ($this->_current_token == SPREADSHEET_EXCEL_WRITER_DIV)) {
3595         /**/
3596             if ($this->_current_token == SPREADSHEET_EXCEL_WRITER_MUL) {
3597                 $this->_advance();
3598                 $result2 = $this->_fact();
3599                 $result = $this->_createTree('ptgMul', $result, $result2);
3600             } else {
3601                 $this->_advance();
3602                 $result2 = $this->_fact();
3603                 $result = $this->_createTree('ptgDiv', $result, $result2);
3604             }
3605         }
3606         return $result;
3607     }
3608
3609     /**
3610     * It parses a factor. It assumes the following rule:
3611     * Fact -> ( Expr )
3612     *       | CellRef
3613     *       | CellRange
3614     *       | Number
3615     *       | Function
3616     *
3617     * @access private
3618     * @return mixed The parsed ptg'd tree on success, PEAR_Error on failure
3619     */
3620     function _fact()
3621     {
3622         if ($this->_current_token == SPREADSHEET_EXCEL_WRITER_OPEN) {
3623             $this->_advance();         // eat the "("
3624             $result = $this->_parenthesizedExpression();
3625             if ($this->_current_token != SPREADSHEET_EXCEL_WRITER_CLOSE) {
3626                 die("')' token expected.");
3627             }
3628             $this->_advance();         // eat the ")"
3629             return $result;
3630         }
3631         // if it's a reference
3632         if (preg_match('/^\$?[A-Ia-i]?[A-Za-z]\$?[0-9]+$/',$this->_current_token))
3633         {
3634             $result = $this->_createTree($this->_current_token, '', '');
3635             $this->_advance();
3636             return $result;
3637         }
3638         // If it's an external reference (Sheet1!A1 or Sheet1:Sheet2!A1)
3639         elseif (preg_match("/^\w+(\:\w+)?\![A-Ia-i]?[A-Za-z][0-9]+$/u",$this->_current_token))
3640         {
3641             $result = $this->_createTree($this->_current_token, '', '');
3642             $this->_advance();
3643             return $result;
3644         }
3645         // If it's an external reference ('Sheet1'!A1 or 'Sheet1:Sheet2'!A1)
3646         elseif (preg_match("/^'[\w -]+(\:[\w -]+)?'\![A-Ia-i]?[A-Za-z][0-9]+$/u",$this->_current_token))
3647         {
3648             $result = $this->_createTree($this->_current_token, '', '');
3649             $this->_advance();
3650             return $result;
3651         }
3652         // if it's a range
3653         elseif (preg_match("/^(\$)?[A-Ia-i]?[A-Za-z](\$)?[0-9]+:(\$)?[A-Ia-i]?[A-Za-z](\$)?[0-9]+$/",$this->_current_token) or 
3654                 preg_match("/^(\$)?[A-Ia-i]?[A-Za-z](\$)?[0-9]+\.\.(\$)?[A-Ia-i]?[A-Za-z](\$)?[0-9]+$/",$this->_current_token))
3655         {
3656             $result = $this->_current_token;
3657             $this->_advance();
3658             return $result;
3659         }
3660         // If it's an external range (Sheet1!A1 or Sheet1!A1:B2)
3661         elseif (preg_match("/^\w+(\:\w+)?\!([A-Ia-i]?[A-Za-z])?[0-9]+:([A-Ia-i]?[A-Za-z])?[0-9]+$/u",$this->_current_token))
3662         {
3663             $result = $this->_current_token;
3664             $this->_advance();
3665             return $result;
3666         }
3667         // If it's an external range ('Sheet1'!A1 or 'Sheet1'!A1:B2)
3668         elseif (preg_match("/^'[\w -]+(\:[\w -]+)?'\!([A-Ia-i]?[A-Za-z])?[0-9]+:([A-Ia-i]?[A-Za-z])?[0-9]+$/u",$this->_current_token))
3669         {
3670             $result = $this->_current_token;
3671             $this->_advance();
3672             return $result;
3673         }
3674         elseif (is_numeric($this->_current_token))
3675         {
3676             $result = $this->_createTree($this->_current_token, '', '');
3677             $this->_advance();
3678             return $result;
3679         }
3680         // if it's a function call
3681         elseif (preg_match("/^[A-Z0-9\xc0-\xdc\.]+$/i",$this->_current_token))
3682         {
3683             $result = $this->_func();
3684             return $result;
3685         }
3686         die("Syntax error: ".$this->_current_token.
3687                                  ", lookahead: ".$this->_lookahead.
3688                                  ", current char: ".$this->_current_char);
3689     }
3690
3691     /**
3692     * It parses a function call. It assumes the following rule:
3693     * Func -> ( Expr [,Expr]* )
3694     *
3695     * @access private
3696     * @return mixed The parsed ptg'd tree on success, PEAR_Error on failure
3697     */
3698     function _func()
3699     {
3700         $num_args = 0; // number of arguments received
3701         $function = strtoupper($this->_current_token);
3702         $result   = ''; // initialize result
3703         $this->_advance();
3704         $this->_advance();         // eat the "("
3705         while ($this->_current_token != ')') {
3706         /**/
3707             if ($num_args > 0) {
3708                 if ($this->_current_token == SPREADSHEET_EXCEL_WRITER_COMA or
3709                     $this->_current_token == SPREADSHEET_EXCEL_WRITER_SEMICOLON)
3710                 {
3711                     $this->_advance();  // eat the "," or ";"
3712                 } else {
3713                     die("Syntax error: comma expected in ".
3714                                       "function $function, arg #{$num_args}");
3715                 }
3716                 $result2 = $this->_condition();
3717                 $result = $this->_createTree('arg', $result, $result2);
3718             } else { // first argument
3719                 $result2 = $this->_condition();
3720                 $result = $this->_createTree('arg', '', $result2);
3721             }
3722             $num_args++;
3723         }
3724         if (!isset($this->_functions[$function])) {
3725             die("Function $function() doesn't exist");
3726         }
3727         $args = $this->_functions[$function][1];
3728         // If fixed number of args eg. TIME($i,$j,$k). Check that the number of args is valid.
3729         if (($args >= 0) and ($args != $num_args)) {
3730             die("Incorrect number of arguments in function $function() ");
3731         }
3732
3733         $result = $this->_createTree($function, $result, $num_args);
3734         $this->_advance();         // eat the ")"
3735         return $result;
3736     }
3737
3738     /**
3739     * Creates a tree. In fact an array which may have one or two arrays (sub-trees)
3740     * as elements.
3741     *
3742     * @access private
3743     * @param mixed $value The value of this node.
3744     * @param mixed $left  The left array (sub-tree) or a final node.
3745     * @param mixed $right The right array (sub-tree) or a final node.
3746     * @return array A tree
3747     */
3748     function _createTree($value, $left, $right)
3749     {
3750         return array('value' => $value, 'left' => $left, 'right' => $right);
3751     }
3752
3753     /**
3754     * Builds a string containing the tree in reverse polish notation (What you
3755     * would use in a HP calculator stack).
3756     * The following tree:
3757     *
3758     *    +
3759     *   / \
3760     *  2   3
3761     *
3762     * produces: "23+"
3763     *
3764     * The following tree:
3765     *
3766     *    +
3767     *   / \
3768     *  3   *
3769     *     / \
3770     *    6   A1
3771     *
3772     * produces: "36A1*+"
3773     *
3774     * In fact all operands, functions, references, etc... are written as ptg's
3775     *
3776     * @access public
3777     * @param array $tree The optional tree to convert.
3778     * @return string The tree in reverse polish notation
3779     */
3780     function toReversePolish($tree = array())
3781     {
3782         $polish = ""; // the string we are going to return
3783         if (empty($tree)) { // If it's the first call use _parse_tree
3784             $tree = $this->_parse_tree;
3785         }
3786         if (is_array($tree['left'])) {
3787             $converted_tree = $this->toReversePolish($tree['left']);
3788             $polish .= $converted_tree;
3789         } elseif ($tree['left'] != '') { // It's a final node
3790             $converted_tree = $this->_convert($tree['left']);
3791             $polish .= $converted_tree;
3792         }
3793         if (is_array($tree['right'])) {
3794             $converted_tree = $this->toReversePolish($tree['right']);
3795             $polish .= $converted_tree;
3796         } elseif ($tree['right'] != '') { // It's a final node
3797             $converted_tree = $this->_convert($tree['right']);
3798             $polish .= $converted_tree;
3799         }
3800         // if it's a function convert it here (so we can set it's arguments)
3801         if (preg_match("/^[A-Z0-9\xc0-\xdc\.]+$/",$tree['value']) and
3802             !preg_match('/^([A-Ia-i]?[A-Za-z])(\d+)$/',$tree['value']) and
3803             !preg_match("/^[A-Ia-i]?[A-Za-z](\d+)\.\.[A-Ia-i]?[A-Za-z](\d+)$/",$tree['value']) and
3804             !is_numeric($tree['value']) and
3805             !isset($this->ptg[$tree['value']]))
3806         {
3807             // left subtree for a function is always an array.
3808             if ($tree['left'] != '') {
3809                 $left_tree = $this->toReversePolish($tree['left']);
3810             } else {
3811                 $left_tree = '';
3812             }
3813             // add it's left subtree and return.
3814             return $left_tree.$this->_convertFunction($tree['value'], $tree['right']);
3815         } else {
3816             $converted_tree = $this->_convert($tree['value']);
3817         }
3818         $polish .= $converted_tree;
3819         return $polish;
3820     }
3821 }
3822
3823 /**
3824 * Class for generating Excel Spreadsheets
3825 *
3826 * @author   Xavier Noguer <xnoguer@rezebra.com>
3827 * @category FileFormats
3828 * @package  Spreadsheet_Excel_Writer
3829 */
3830
3831 class Spreadsheet_Excel_Writer_Worksheet extends Spreadsheet_Excel_Writer_BIFFwriter
3832 {
3833     /**
3834     * Name of the Worksheet
3835     * @var string
3836     */
3837     var $name;
3838
3839     /**
3840     * Index for the Worksheet
3841     * @var integer
3842     */
3843     var $index;
3844
3845     /**
3846     * Reference to the (default) Format object for URLs
3847     * @var object Format
3848     */
3849     var $_url_format;
3850
3851     /**
3852     * Reference to the parser used for parsing formulas
3853     * @var object Format
3854     */
3855     var $_parser;
3856
3857     /**
3858     * Filehandle to the temporary file for storing data
3859     * @var resource
3860     */
3861     var $_filehandle;
3862
3863     /**
3864     * Boolean indicating if we are using a temporary file for storing data
3865     * @var bool
3866     */
3867     var $_using_tmpfile;
3868
3869     /**
3870     * Maximum number of rows for an Excel spreadsheet (BIFF5)
3871     * @var integer
3872     */
3873     var $_xls_rowmax;
3874
3875     /**
3876     * Maximum number of columns for an Excel spreadsheet (BIFF5)
3877     * @var integer
3878     */
3879     var $_xls_colmax;
3880
3881     /**
3882     * Maximum number of characters for a string (LABEL record in BIFF5)
3883     * @var integer
3884     */
3885     var $_xls_strmax;
3886
3887     /**
3888     * First row for the DIMENSIONS record
3889     * @var integer
3890     * @see _storeDimensions()
3891     */
3892     var $_dim_rowmin;
3893
3894     /**
3895     * Last row for the DIMENSIONS record
3896     * @var integer
3897     * @see _storeDimensions()
3898     */
3899     var $_dim_rowmax;
3900
3901     /**
3902     * First column for the DIMENSIONS record
3903     * @var integer
3904     * @see _storeDimensions()
3905     */
3906     var $_dim_colmin;
3907
3908     /**
3909     * Last column for the DIMENSIONS record
3910     * @var integer
3911     * @see _storeDimensions()
3912     */
3913     var $_dim_colmax;
3914
3915     /**
3916     * Array containing format information for columns
3917     * @var array
3918     */
3919     var $_colinfo;
3920
3921     /**
3922     * Array containing the selected area for the worksheet
3923     * @var array
3924     */
3925     var $_selection;
3926
3927     /**
3928     * Array containing the panes for the worksheet
3929     * @var array
3930     */
3931     var $_panes;
3932
3933     /**
3934     * The active pane for the worksheet
3935     * @var integer
3936     */
3937     var $_active_pane;
3938
3939     /**
3940     * Bit specifying if panes are frozen
3941     * @var integer
3942     */
3943     var $_frozen;
3944
3945     /**
3946     * Bit specifying if the worksheet is selected
3947     * @var integer
3948     */
3949     var $selected;
3950
3951     /**
3952     * The paper size (for printing) (DOCUMENT!!!)
3953     * @var integer
3954     */
3955     var $_paper_size;
3956
3957     /**
3958     * Bit specifying paper orientation (for printing). 0 => landscape, 1 => portrait
3959     * @var integer
3960     */
3961     var $_orientation;
3962
3963     /**
3964     * The page header caption
3965     * @var string
3966     */
3967     var $_header;
3968
3969     /**
3970     * The page footer caption
3971     * @var string
3972     */
3973     var $_footer;
3974
3975     /**
3976     * The horizontal centering value for the page
3977     * @var integer
3978     */
3979     var $_hcenter;
3980
3981     /**
3982     * The vertical centering value for the page
3983     * @var integer
3984     */
3985     var $_vcenter;
3986
3987     /**
3988     * The margin for the header
3989     * @var float
3990     */
3991     var $_margin_head;
3992
3993     /**
3994     * The margin for the footer
3995     * @var float
3996     */
3997     var $_margin_foot;
3998
3999     /**
4000     * The left margin for the worksheet in inches
4001     * @var float
4002     */
4003     var $_margin_left;
4004
4005     /**
4006     * The right margin for the worksheet in inches
4007     * @var float
4008     */
4009     var $_margin_right;
4010
4011     /**
4012     * The top margin for the worksheet in inches
4013     * @var float
4014     */
4015     var $_margin_top;
4016
4017     /**
4018     * The bottom margin for the worksheet in inches
4019     * @var float
4020     */
4021     var $_margin_bottom;
4022
4023     /**
4024     * First row to reapeat on each printed page
4025     * @var integer
4026     */
4027     var $title_rowmin;
4028
4029     /**
4030     * Last row to reapeat on each printed page
4031     * @var integer
4032     */
4033     var $title_rowmax;
4034
4035     /**
4036     * First column to reapeat on each printed page
4037     * @var integer
4038     */
4039     var $title_colmin;
4040
4041     /**
4042     * First row of the area to print
4043     * @var integer
4044     */
4045     var $print_rowmin;
4046
4047     /**
4048     * Last row to of the area to print
4049     * @var integer
4050     */
4051     var $print_rowmax;
4052
4053     /**
4054     * First column of the area to print
4055     * @var integer
4056     */
4057     var $print_colmin;
4058
4059     /**
4060     * Last column of the area to print
4061     * @var integer
4062     */
4063     var $print_colmax;
4064
4065     /**
4066     * Whether to use outline.
4067     * @var integer
4068     */
4069     var $_outline_on;
4070
4071     /**
4072     * Auto outline styles.
4073     * @var bool
4074     */
4075     var $_outline_style;
4076
4077     /**
4078     * Whether to have outline summary below.
4079     * @var bool
4080     */
4081     var $_outline_below;
4082
4083     /**
4084     * Whether to have outline summary at the right.
4085     * @var bool
4086     */
4087     var $_outline_right;
4088
4089     /**
4090     * Outline row level.
4091     * @var integer
4092     */
4093     var $_outline_row_level;
4094
4095     /**
4096     * Whether to fit to page when printing or not.
4097     * @var bool
4098     */
4099     var $_fit_page;
4100
4101     /**
4102     * Number of pages to fit wide
4103     * @var integer
4104     */
4105     var $_fit_width;
4106
4107     /**
4108     * Number of pages to fit high
4109     * @var integer
4110     */
4111     var $_fit_height;
4112
4113     /**
4114     * Reference to the total number of strings in the workbook
4115     * @var integer
4116     */
4117     var $_str_total;
4118
4119     /**
4120     * Reference to the number of unique strings in the workbook
4121     * @var integer
4122     */
4123     var $_str_unique;
4124
4125     /**
4126     * Reference to the array containing all the unique strings in the workbook
4127     * @var array
4128     */
4129     var $_str_table;
4130
4131     /**
4132     * Merged cell ranges
4133     * @var array
4134     */
4135     var $_merged_ranges;
4136
4137     /**
4138     * Charset encoding currently used when calling writeString()
4139     * @var string
4140     */
4141     var $_input_encoding;
4142     // Dynamic members must be here (php8.2)
4143     var $activesheet;
4144     var $firstsheet;
4145     var $title_colmax;
4146     var $_print_gridlines;
4147     var $_screen_gridlines;
4148     var $_print_headers;
4149     var $_hbreaks;
4150     var $_vbreaks;
4151     var $_protect;
4152     var $_password;
4153     var $col_sizes;
4154     var $_row_sizes;
4155     var $_zoom;
4156     var $_print_scale;
4157     var $_rtl;
4158     var $_dv;
4159
4160
4161     /**
4162     * Constructor
4163     *
4164     * @param string  $name         The name of the new worksheet
4165     * @param integer $index        The index of the new worksheet
4166     * @param mixed   &$activesheet The current activesheet of the workbook we belong to
4167     * @param mixed   &$firstsheet  The first worksheet in the workbook we belong to
4168     * @param mixed   &$url_format  The default format for hyperlinks
4169     * @param mixed   &$parser      The formula parser created for the Workbook
4170     * @access private
4171     */
4172     function                                    __construct($BIFF_version, $name,
4173                                                 $index, &$activesheet,
4174                                                 &$firstsheet, &$str_total,
4175                                                 &$str_unique, &$str_table,
4176                                                 &$url_format, &$parser)
4177     {
4178         // It needs to call its parent's constructor explicitly
4179         parent::__construct();
4180         $this->_BIFF_version   = $BIFF_version;
4181         $rowmax                = 65536; // 16384 in Excel 5
4182         $colmax                = 256;
4183
4184         $this->name            = $name;
4185         $this->index           = $index;
4186         $this->activesheet     = &$activesheet;
4187         $this->firstsheet      = &$firstsheet;
4188         $this->_str_total      = &$str_total;
4189         $this->_str_unique     = &$str_unique;
4190         $this->_str_table      = &$str_table;
4191         $this->_url_format     = &$url_format;
4192         $this->_parser         = &$parser;
4193
4194         //$this->ext_sheets      = array();
4195         $this->_filehandle     = '';
4196         $this->_using_tmpfile  = true;
4197         //$this->fileclosed      = 0;
4198         //$this->offset          = 0;
4199         $this->_xls_rowmax     = $rowmax;
4200         $this->_xls_colmax     = $colmax;
4201         $this->_xls_strmax     = 255;
4202         $this->_dim_rowmin     = $rowmax + 1;
4203         $this->_dim_rowmax     = 0;
4204         $this->_dim_colmin     = $colmax + 1;
4205         $this->_dim_colmax     = 0;
4206         $this->_colinfo        = array();
4207         $this->_selection      = array(0,0,0,0);
4208         $this->_panes          = array();
4209         $this->_active_pane    = 3;
4210         $this->_frozen         = 0;
4211         $this->selected        = 0;
4212
4213         $this->_paper_size      = 0x0;
4214         $this->_orientation     = 0x1;
4215         $this->_header          = '';
4216         $this->_footer          = '';
4217         $this->_hcenter         = 0;
4218         $this->_vcenter         = 0;
4219         $this->_margin_head     = 0.50;
4220         $this->_margin_foot     = 0.50;
4221         $this->_margin_left     = 0.75;
4222         $this->_margin_right    = 0.75;
4223         $this->_margin_top      = 1.00;
4224         $this->_margin_bottom   = 1.00;
4225
4226         $this->title_rowmin     = null;
4227         $this->title_rowmax     = null;
4228         $this->title_colmin     = null;
4229         $this->title_colmax     = null;
4230         $this->print_rowmin     = null;
4231         $this->print_rowmax     = null;
4232         $this->print_colmin     = null;
4233         $this->print_colmax     = null;
4234
4235         $this->_print_gridlines  = 1;
4236         $this->_screen_gridlines = 1;
4237         $this->_print_headers    = 0;
4238
4239         $this->_fit_page        = 0;
4240         $this->_fit_width       = 0;
4241         $this->_fit_height      = 0;
4242
4243         $this->_hbreaks         = array();
4244         $this->_vbreaks         = array();
4245
4246         $this->_protect         = 0;
4247         $this->_password        = null;
4248
4249         $this->col_sizes        = array();
4250         $this->_row_sizes        = array();
4251
4252         $this->_zoom            = 100;
4253         $this->_print_scale     = 100;
4254
4255         $this->_outline_row_level = 0;
4256         $this->_outline_style     = 0;
4257         $this->_outline_below     = 1;
4258         $this->_outline_right     = 1;
4259         $this->_outline_on        = 1;
4260
4261         $this->_merged_ranges     = array();
4262         
4263                 $this->_rtl                               = 0;  // Added by Joe Hunt 2009-03-05 for arabic languages
4264         $this->_input_encoding    = '';
4265
4266         $this->_dv                = array();
4267
4268         $this->_initialize();
4269     }
4270
4271     /**
4272     * Open a tmp file to store the majority of the Worksheet data. If this fails,
4273     * for example due to write permissions, store the data in memory. This can be
4274     * slow for large files.
4275     *
4276     * @access private
4277     */
4278     function _initialize()
4279     {
4280         // Open tmp file for storing Worksheet data
4281         $fh = tmpfile();
4282         if ($fh) {
4283             // Store filehandle
4284             $this->_filehandle = $fh;
4285         } else {
4286             // If tmpfile() fails store data in memory
4287             $this->_using_tmpfile = false;
4288         }
4289     }
4290
4291     /**
4292     * Add data to the beginning of the workbook (note the reverse order)
4293     * and to the end of the workbook.
4294     *
4295     * @access public
4296     * @see Spreadsheet_Excel_Writer_Workbook::storeWorkbook()
4297     * @param array $sheetnames The array of sheetnames from the Workbook this
4298     *                          worksheet belongs to
4299     */
4300     function close($sheetnames)
4301     {
4302         $num_sheets = count($sheetnames);
4303
4304         /***********************************************
4305         * Prepend in reverse order!!
4306         */
4307
4308         // Prepend the sheet dimensions
4309         $this->_storeDimensions();
4310
4311         // Prepend the sheet password
4312         $this->_storePassword();
4313
4314         // Prepend the sheet protection
4315         $this->_storeProtect();
4316
4317         // Prepend the page setup
4318         $this->_storeSetup();
4319
4320         /* FIXME: margins are actually appended */
4321         // Prepend the bottom margin
4322         $this->_storeMarginBottom();
4323
4324         // Prepend the top margin
4325         $this->_storeMarginTop();
4326
4327         // Prepend the right margin
4328         $this->_storeMarginRight();
4329
4330         // Prepend the left margin
4331         $this->_storeMarginLeft();
4332
4333         // Prepend the page vertical centering
4334         $this->_storeVcenter();
4335
4336         // Prepend the page horizontal centering
4337         $this->_storeHcenter();
4338
4339         // Prepend the page footer
4340         $this->_storeFooter();
4341
4342         // Prepend the page header
4343         $this->_storeHeader();
4344
4345         // Prepend the vertical page breaks
4346         $this->_storeVbreak();
4347
4348         // Prepend the horizontal page breaks
4349         $this->_storeHbreak();
4350
4351         // Prepend WSBOOL
4352         $this->_storeWsbool();
4353
4354         // Prepend GRIDSET
4355         $this->_storeGridset();
4356
4357          //  Prepend GUTS
4358         if ($this->_BIFF_version == 0x0500) {
4359             $this->_storeGuts();
4360         }
4361
4362         // Prepend PRINTGRIDLINES
4363         $this->_storePrintGridlines();
4364
4365         // Prepend PRINTHEADERS
4366         $this->_storePrintHeaders();
4367
4368         // Prepend EXTERNSHEET references
4369         if ($this->_BIFF_version == 0x0500) {
4370             for ($i = $num_sheets; $i > 0; $i--) {
4371                 $sheetname = $sheetnames[$i-1];
4372                 $this->_storeExternsheet($sheetname);
4373             }
4374         }
4375
4376         // Prepend the EXTERNCOUNT of external references.
4377         if ($this->_BIFF_version == 0x0500) {
4378             $this->_storeExterncount($num_sheets);
4379         }
4380
4381         // Prepend the COLINFO records if they exist
4382         if (!empty($this->_colinfo)) {
4383             $colcount = count($this->_colinfo);
4384             for ($i = 0; $i < $colcount; $i++) {
4385                 $this->_storeColinfo($this->_colinfo[$i]);
4386             }
4387             $this->_storeDefcol();
4388         }
4389
4390         // Prepend the BOF record
4391         $this->_storeBof(0x0010);
4392
4393         /*
4394         * End of prepend. Read upwards from here.
4395         ***********************************************/
4396
4397         // Append
4398         $this->_storeWindow2();
4399         $this->_storeZoom();
4400         if (!empty($this->_panes)) {
4401             $this->_storePanes($this->_panes);
4402         }
4403         $this->_storeSelection($this->_selection);
4404         $this->_storeMergedCells();
4405         /* TODO: add data validity */
4406         /*if ($this->_BIFF_version == 0x0600) {
4407             $this->_storeDataValidity();
4408         }*/
4409         $this->_storeEof();
4410     }
4411
4412     /**
4413     * Retrieve the worksheet name.
4414     * This is usefull when creating worksheets without a name.
4415     *
4416     * @access public
4417     * @return string The worksheet's name
4418     */
4419     function getName()
4420     {
4421         return $this->name;
4422     }
4423
4424     /**
4425     * Retrieves data from memory in one chunk, or from disk in $buffer
4426     * sized chunks.
4427     *
4428     * @return string The data
4429     */
4430     function getData()
4431     {
4432         $buffer = 4096;
4433
4434         // Return data stored in memory
4435         if (isset($this->_data)) {
4436             $tmp   = $this->_data;
4437             unset($this->_data);
4438             $fh    = $this->_filehandle;
4439             if ($this->_using_tmpfile) {
4440                 fseek($fh, 0);
4441             }
4442             return $tmp;
4443         }
4444         // Return data stored on disk
4445         if ($this->_using_tmpfile) {
4446             if ($tmp = fread($this->_filehandle, $buffer)) {
4447                 return $tmp;
4448             }
4449         }
4450
4451         // No data to return
4452         return '';
4453     }
4454
4455     /**
4456     * Sets a merged cell range
4457     *
4458     * @access public
4459     * @param integer $first_row First row of the area to merge
4460     * @param integer $first_col First column of the area to merge
4461     * @param integer $last_row  Last row of the area to merge
4462     * @param integer $last_col  Last column of the area to merge
4463     */
4464     function setMerge($first_row, $first_col, $last_row, $last_col)
4465     {
4466         if (($last_row < $first_row) || ($last_col < $first_col)) {
4467             return;
4468         }
4469         // don't check rowmin, rowmax, etc... because we don't know when this
4470         // is going to be called
4471         $this->_merged_ranges[] = array($first_row, $first_col, $last_row, $last_col);
4472     }
4473
4474     /**
4475     * Set this worksheet as a selected worksheet,
4476     * i.e. the worksheet has its tab highlighted.
4477     *
4478     * @access public
4479     */
4480     function select()
4481     {
4482         $this->selected = 1;
4483     }
4484
4485     /**
4486     * Set this worksheet as the active worksheet,
4487     * i.e. the worksheet that is displayed when the workbook is opened.
4488     * Also set it as selected.
4489     *
4490     * @access public
4491     */
4492     function activate()
4493     {
4494         $this->selected = 1;
4495         $this->activesheet = $this->index;
4496     }
4497
4498     /**
4499     * Set this worksheet as the first visible sheet.
4500     * This is necessary when there are a large number of worksheets and the
4501     * activated worksheet is not visible on the screen.
4502     *
4503     * @access public
4504     */
4505     function setFirstSheet()
4506     {
4507         $this->firstsheet = $this->index;
4508     }
4509
4510     /**
4511     * Set the worksheet protection flag
4512     * to prevent accidental modification and to
4513     * hide formulas if the locked and hidden format properties have been set.
4514     *
4515     * @access public
4516     * @param string $password The password to use for protecting the sheet.
4517     */
4518     function protect($password)
4519     {
4520         $this->_protect   = 1;
4521         $this->_password  = $this->_encodePassword($password);
4522     }
4523
4524     /**
4525     * Set the width of a single column or a range of columns.
4526     *
4527     * @access public
4528     * @param integer $firstcol first column on the range
4529     * @param integer $lastcol  last column on the range
4530     * @param integer $width    width to set
4531     * @param mixed   $format   The optional XF format to apply to the columns
4532     * @param integer $hidden   The optional hidden atribute
4533     * @param integer $level    The optional outline level
4534     */
4535     function setColumn($firstcol, $lastcol, $width, $format = null, $hidden = 0, $level = 0)
4536     {
4537         $this->_colinfo[] = array($firstcol, $lastcol, $width, &$format, $hidden, $level);
4538
4539         // Set width to zero if column is hidden
4540         $width = ($hidden) ? 0 : $width;
4541
4542         for ($col = $firstcol; $col <= $lastcol; $col++) {
4543             $this->col_sizes[$col] = $width;
4544         }
4545     }
4546
4547     /**
4548     * Set which cell or cells are selected in a worksheet
4549     *
4550     * @access public
4551     * @param integer $first_row    first row in the selected quadrant
4552     * @param integer $first_column first column in the selected quadrant
4553     * @param integer $last_row     last row in the selected quadrant
4554     * @param integer $last_column  last column in the selected quadrant
4555     */
4556     function setSelection($first_row,$first_column,$last_row,$last_column)
4557     {
4558         $this->_selection = array($first_row,$first_column,$last_row,$last_column);
4559     }
4560
4561     /**
4562     * Set panes and mark them as frozen.
4563     *
4564     * @access public
4565     * @param array $panes This is the only parameter received and is composed of the following:
4566     *                     0 => Vertical split position,
4567     *                     1 => Horizontal split position
4568     *                     2 => Top row visible
4569     *                     3 => Leftmost column visible
4570     *                     4 => Active pane
4571     */
4572     function freezePanes($panes)
4573     {
4574         $this->_frozen = 1;
4575         $this->_panes  = $panes;
4576     }
4577
4578     /**
4579     * Set panes and mark them as unfrozen.
4580     *
4581     * @access public
4582     * @param array $panes This is the only parameter received and is composed of the following:
4583     *                     0 => Vertical split position,
4584     *                     1 => Horizontal split position
4585     *                     2 => Top row visible
4586     *                     3 => Leftmost column visible
4587     *                     4 => Active pane
4588     */
4589     function thawPanes($panes)
4590     {
4591         $this->_frozen = 0;
4592         $this->_panes  = $panes;
4593     }
4594
4595     /**
4596     * Set the page orientation as portrait.
4597     *
4598     * @access public
4599     */
4600     function setPortrait()
4601     {
4602         $this->_orientation = 1;
4603     }
4604
4605     /**
4606     * Set the page orientation as landscape.
4607     *
4608     * @access public
4609     */
4610     function setLandscape()
4611     {
4612         $this->_orientation = 0;
4613     }
4614
4615     /**
4616     * Set the paper type. Ex. 1 = US Letter, 9 = A4
4617     *
4618     * @access public
4619     * @param integer $size The type of paper size to use
4620     */
4621     function setPaper($size = 0)
4622     {
4623         $this->_paper_size = $size;
4624     }
4625
4626
4627     /**
4628     * Set the page header caption and optional margin.
4629     *
4630     * @access public
4631     * @param string $string The header text
4632     * @param float  $margin optional head margin in inches.
4633     */
4634     function setHeader($string,$margin = 0.50)
4635     {
4636         if (strlen($string) >= 255) {
4637             //carp 'Header string must be less than 255 characters';
4638             return;
4639         }
4640         $this->_header      = $string;
4641         $this->_margin_head = $margin;
4642     }
4643
4644     /**
4645     * Set the page footer caption and optional margin.
4646     *
4647     * @access public
4648     * @param string $string The footer text
4649     * @param float  $margin optional foot margin in inches.
4650     */
4651     function setFooter($string,$margin = 0.50)
4652     {
4653         if (strlen($string) >= 255) {
4654             //carp 'Footer string must be less than 255 characters';
4655             return;
4656         }
4657         $this->_footer      = $string;
4658         $this->_margin_foot = $margin;
4659     }
4660
4661     /**
4662     * Center the page horinzontally.
4663     *
4664     * @access public
4665     * @param integer $center the optional value for centering. Defaults to 1 (center).
4666     */
4667     function centerHorizontally($center = 1)
4668     {
4669         $this->_hcenter = $center;
4670     }
4671
4672     /**
4673     * Center the page vertically.
4674     *
4675     * @access public
4676     * @param integer $center the optional value for centering. Defaults to 1 (center).
4677     */
4678     function centerVertically($center = 1)
4679     {
4680         $this->_vcenter = $center;
4681     }
4682
4683     /**
4684     * Set all the page margins to the same value in inches.
4685     *
4686     * @access public
4687     * @param float $margin The margin to set in inches
4688     */
4689     function setMargins($margin)
4690     {
4691         $this->setMarginLeft($margin);
4692         $this->setMarginRight($margin);
4693         $this->setMarginTop($margin);
4694         $this->setMarginBottom($margin);
4695     }
4696
4697     /**
4698     * Set the left and right margins to the same value in inches.
4699     *
4700     * @access public
4701     * @param float $margin The margin to set in inches
4702     */
4703     function setMargins_LR($margin)
4704     {
4705         $this->setMarginLeft($margin);
4706         $this->setMarginRight($margin);
4707     }
4708
4709     /**
4710     * Set the top and bottom margins to the same value in inches.
4711     *
4712     * @access public
4713     * @param float $margin The margin to set in inches
4714     */
4715     function setMargins_TB($margin)
4716     {
4717         $this->setMarginTop($margin);
4718         $this->setMarginBottom($margin);
4719     }
4720
4721     /**
4722     * Set the left margin in inches.
4723     *
4724     * @access public
4725     * @param float $margin The margin to set in inches
4726     */
4727     function setMarginLeft($margin = 0.75)
4728     {
4729         $this->_margin_left = $margin;
4730     }
4731
4732     /**
4733     * Set the right margin in inches.
4734     *
4735     * @access public
4736     * @param float $margin The margin to set in inches
4737     */
4738     function setMarginRight($margin = 0.75)
4739     {
4740         $this->_margin_right = $margin;
4741     }
4742
4743     /**
4744     * Set the top margin in inches.
4745     *
4746     * @access public
4747     * @param float $margin The margin to set in inches
4748     */
4749     function setMarginTop($margin = 1.00)
4750     {
4751         $this->_margin_top = $margin;
4752     }
4753
4754     /**
4755     * Set the bottom margin in inches.
4756     *
4757     * @access public
4758     * @param float $margin The margin to set in inches
4759     */
4760     function setMarginBottom($margin = 1.00)
4761     {
4762         $this->_margin_bottom = $margin;
4763     }
4764
4765     /**
4766     * Set the rows to repeat at the top of each printed page.
4767     *
4768     * @access public
4769     * @param integer $first_row First row to repeat
4770     * @param integer $last_row  Last row to repeat. Optional.
4771     */
4772     function repeatRows($first_row, $last_row = null)
4773     {
4774         $this->title_rowmin  = $first_row;
4775         if (isset($last_row)) { //Second row is optional
4776             $this->title_rowmax  = $last_row;
4777         } else {
4778             $this->title_rowmax  = $first_row;
4779         }
4780     }
4781
4782     /**
4783     * Set the columns to repeat at the left hand side of each printed page.
4784     *
4785     * @access public
4786     * @param integer $first_col First column to repeat
4787     * @param integer $last_col  Last column to repeat. Optional.
4788     */
4789     function repeatColumns($first_col, $last_col = null)
4790     {
4791         $this->title_colmin  = $first_col;
4792         if (isset($last_col)) { // Second col is optional
4793             $this->title_colmax  = $last_col;
4794         } else {
4795             $this->title_colmax  = $first_col;
4796         }
4797     }
4798
4799     /**
4800     * Set the area of each worksheet that will be printed.
4801     *
4802     * @access public
4803     * @param integer $first_row First row of the area to print
4804     * @param integer $first_col First column of the area to print
4805     * @param integer $last_row  Last row of the area to print
4806     * @param integer $last_col  Last column of the area to print
4807     */
4808     function printArea($first_row, $first_col, $last_row, $last_col)
4809     {
4810         $this->print_rowmin  = $first_row;
4811         $this->print_colmin  = $first_col;
4812         $this->print_rowmax  = $last_row;
4813         $this->print_colmax  = $last_col;
4814     }
4815
4816
4817     /**
4818     * Set the option to hide gridlines on the printed page.
4819     *
4820     * @access public
4821     */
4822     function hideGridlines()
4823     {
4824         $this->_print_gridlines = 0;
4825     }
4826
4827     /**
4828     * Set the option to hide gridlines on the worksheet (as seen on the screen).
4829     *
4830     * @access public
4831     */
4832     function hideScreenGridlines()
4833     {
4834         $this->_screen_gridlines = 0;
4835     }
4836
4837     /**
4838     * Set the option to print the row and column headers on the printed page.
4839     *
4840     * @access public
4841     * @param integer $print Whether to print the headers or not. Defaults to 1 (print).
4842     */
4843     function printRowColHeaders($print = 1)
4844     {
4845         $this->_print_headers = $print;
4846     }
4847
4848     /**
4849     * Set the vertical and horizontal number of pages that will define the maximum area printed.
4850     * It doesn't seem to work with OpenOffice.
4851     *
4852     * @access public
4853     * @param  integer $width  Maximun width of printed area in pages
4854     * @param  integer $height Maximun heigth of printed area in pages
4855     * @see setPrintScale()
4856     */
4857     function fitToPages($width, $height)
4858     {
4859         $this->_fit_page      = 1;
4860         $this->_fit_width     = $width;
4861         $this->_fit_height    = $height;
4862     }
4863
4864     /**
4865     * Store the horizontal page breaks on a worksheet (for printing).
4866     * The breaks represent the row after which the break is inserted.
4867     *
4868     * @access public
4869     * @param array $breaks Array containing the horizontal page breaks
4870     */
4871     function setHPagebreaks($breaks)
4872     {
4873         foreach ($breaks as $break) {
4874             array_push($this->_hbreaks, $break);
4875         }
4876     }
4877
4878     /**
4879     * Store the vertical page breaks on a worksheet (for printing).
4880     * The breaks represent the column after which the break is inserted.
4881     *
4882     * @access public
4883     * @param array $breaks Array containing the vertical page breaks
4884     */
4885     function setVPagebreaks($breaks)
4886     {
4887         foreach ($breaks as $break) {
4888             array_push($this->_vbreaks, $break);
4889         }
4890     }
4891
4892
4893     /**
4894     * Set the worksheet zoom factor.
4895     *
4896     * @access public
4897     * @param integer $scale The zoom factor
4898     */
4899     function setZoom($scale = 100)
4900     {
4901         // Confine the scale to Excel's range
4902         if ($scale < 10 || $scale > 400) {
4903             $scale = 100;
4904         }
4905
4906         $this->_zoom = floor($scale);
4907     }
4908
4909     /**
4910     * Set the scale factor for the printed page.
4911     * It turns off the "fit to page" option
4912     *
4913     * @access public
4914     * @param integer $scale The optional scale factor. Defaults to 100
4915     */
4916     function setPrintScale($scale = 100)
4917     {
4918         // Confine the scale to Excel's range
4919         if ($scale < 10 || $scale > 400) {
4920             $scale = 100;
4921         }
4922
4923         // Turn off "fit to page" option
4924         $this->_fit_page = 0;
4925
4926         $this->_print_scale = floor($scale);
4927     }
4928
4929     /**
4930     * Map to the appropriate write method acording to the token recieved.
4931     *
4932     * @access public
4933     * @param integer $row    The row of the cell we are writing to
4934     * @param integer $col    The column of the cell we are writing to
4935     * @param mixed   $token  What we are writing
4936     * @param mixed   $format The optional format to apply to the cell
4937     */
4938     function write($row, $col, $token, $format = null)
4939     {
4940         // Check for a cell reference in A1 notation and substitute row and column
4941         /*if ($_[0] =~ /^\D/) {
4942             @_ = $this->_substituteCellref(@_);
4943     }*/
4944
4945         if (preg_match("/^([+-]?)(?=\d|\.\d)\d*(\.\d*)?([Ee]([+-]?\d+))?$/", $token)) {
4946             // Match number
4947             return $this->writeNumber($row, $col, $token, $format);
4948         } elseif (preg_match("/^[fh]tt?p:\/\//", $token)) {
4949             // Match http or ftp URL
4950             return $this->writeUrl($row, $col, $token, '', $format);
4951         } elseif (preg_match("/^mailto:/", $token)) {
4952             // Match mailto:
4953             return $this->writeUrl($row, $col, $token, '', $format);
4954         } elseif (preg_match("/^(?:in|ex)ternal:/", $token)) {
4955             // Match internal or external sheet link
4956             return $this->writeUrl($row, $col, $token, '', $format);
4957         } elseif (preg_match("/^=/", $token)) {
4958             // Match formula
4959             return $this->writeFormula($row, $col, $token, $format);
4960         } elseif (preg_match("/^@/", $token)) {
4961             // Match formula
4962             return $this->writeFormula($row, $col, $token, $format);
4963         } elseif ($token == '') {
4964             // Match blank
4965             return $this->writeBlank($row, $col, $format);
4966         } else {
4967             // Default: match string
4968             return $this->writeString($row, $col, $token, $format);
4969         }
4970     }
4971
4972     /**
4973     * Write an array of values as a row
4974     *
4975     * @access public
4976     * @param integer $row    The row we are writing to
4977     * @param integer $col    The first col (leftmost col) we are writing to
4978     * @param array   $val    The array of values to write
4979     * @param mixed   $format The optional format to apply to the cell
4980     * @return mixed PEAR_Error on failure
4981     */
4982
4983     function writeRow($row, $col, $val, $format = null)
4984     {
4985         $retval = '';
4986         if (is_array($val)) {
4987             foreach ($val as $v) {
4988                 if (is_array($v)) {
4989                     $this->writeCol($row, $col, $v, $format);
4990                 } else {
4991                     $this->write($row, $col, $v, $format);
4992                 }
4993                 $col++;
4994             }
4995         } else {
4996             die('$val needs to be an array');
4997         }
4998         return($retval);
4999     }
5000
5001     /**
5002     * Write an array of values as a column
5003     *
5004     * @access public
5005     * @param integer $row    The first row (uppermost row) we are writing to
5006     * @param integer $col    The col we are writing to
5007     * @param array   $val    The array of values to write
5008     * @param mixed   $format The optional format to apply to the cell
5009     * @return mixed PEAR_Error on failure
5010     */
5011
5012     function writeCol($row, $col, $val, $format = null)
5013     {
5014         $retval = '';
5015         if (is_array($val)) {
5016             foreach ($val as $v) {
5017                 $this->write($row, $col, $v, $format);
5018                 $row++;
5019             }
5020         } else {
5021             die('$val needs to be an array');
5022         }
5023         return($retval);
5024     }
5025
5026     /**
5027     * Returns an index to the XF record in the workbook
5028     *
5029     * @access private
5030     * @param mixed &$format The optional XF format
5031     * @return integer The XF record index
5032     */
5033     function _XF(&$format)
5034     {
5035         if ($format) {
5036             return($format->getXfIndex());
5037         } else {
5038             return(0x0F);
5039         }
5040     }
5041
5042
5043     /******************************************************************************
5044     *******************************************************************************
5045     *
5046     * Internal methods
5047     */
5048
5049
5050     /**
5051     * Store Worksheet data in memory using the parent's class append() or to a
5052     * temporary file, the default.
5053     *
5054     * @access private
5055     * @param string $data The binary data to append
5056     */
5057     function _append($data)
5058     {
5059         if ($this->_using_tmpfile) {
5060             // Add CONTINUE records if necessary
5061             if (strlen($data) > $this->_limit) {
5062                 $data = $this->_addContinue($data);
5063             }
5064             fwrite($this->_filehandle, $data);
5065             $this->_datasize += strlen($data);
5066         } else {
5067             parent::_append($data);
5068         }
5069     }
5070
5071     /**
5072     * Substitute an Excel cell reference in A1 notation for  zero based row and
5073     * column values in an argument list.
5074     *
5075     * Ex: ("A4", "Hello") is converted to (3, 0, "Hello").
5076     *
5077     * @access private
5078     * @param string $cell The cell reference. Or range of cells.
5079     * @return array
5080     */
5081     function _substituteCellref($cell)
5082     {
5083         $cell = strtoupper($cell);
5084
5085         // Convert a column range: 'A:A' or 'B:G'
5086         if (preg_match("/([A-I]?[A-Z]):([A-I]?[A-Z])/", $cell, $match)) {
5087             list($no_use, $col1) =  $this->_cellToRowcol($match[1] .'1'); // Add a dummy row
5088             list($no_use, $col2) =  $this->_cellToRowcol($match[2] .'1'); // Add a dummy row
5089             return(array($col1, $col2));
5090         }
5091
5092         // Convert a cell range: 'A1:B7'
5093         if (preg_match("/\$?([A-I]?[A-Z]\$?\d+):\$?([A-I]?[A-Z]\$?\d+)/", $cell, $match)) {
5094             list($row1, $col1) =  $this->_cellToRowcol($match[1]);
5095             list($row2, $col2) =  $this->_cellToRowcol($match[2]);
5096             return(array($row1, $col1, $row2, $col2));
5097         }
5098
5099         // Convert a cell reference: 'A1' or 'AD2000'
5100         if (preg_match("/\$?([A-I]?[A-Z]\$?\d+)/", $cell)) {
5101             list($row1, $col1) =  $this->_cellToRowcol($match[1]);
5102             return(array($row1, $col1));
5103         }
5104
5105         // TODO use real error codes
5106         die("Unknown cell reference $cell");
5107     }
5108
5109     /**
5110     * Convert an Excel cell reference in A1 notation to a zero based row and column
5111     * reference; converts C1 to (0, 2).
5112     *
5113     * @access private
5114     * @param string $cell The cell reference.
5115     * @return array containing (row, column)
5116     */
5117     function _cellToRowcol($cell)
5118     {
5119         preg_match("/\$?([A-I]?[A-Z])\$?(\d+)/",$cell,$match);
5120         $col     = $match[1];
5121         $row     = $match[2];
5122
5123         // Convert base26 column string to number
5124         $chars = preg_split('//', $col);
5125         $expn  = 0;
5126         $col   = 0;
5127
5128         while ($chars) {
5129             $char = array_pop($chars);        // LS char first
5130             $col += (ord($char) -ord('A') +1) * pow(26,$expn);
5131             $expn++;
5132         }
5133
5134         // Convert 1-index to zero-index
5135         $row--;
5136         $col--;
5137
5138         return(array($row, $col));
5139     }
5140
5141     /**
5142     * Based on the algorithm provided by Daniel Rentz of OpenOffice.
5143     *
5144     * @access private
5145     * @param string $plaintext The password to be encoded in plaintext.
5146     * @return string The encoded password
5147     */
5148     function _encodePassword($plaintext)
5149     {
5150         $password = 0x0000;
5151         $i        = 1;       // char position
5152
5153         // split the plain text password in its component characters
5154         $chars = preg_split('//', $plaintext, -1, PREG_SPLIT_NO_EMPTY);
5155         foreach ($chars as $char) {
5156             $value        = ord($char) << $i;   // shifted ASCII value
5157             $rotated_bits = $value >> 15;       // rotated bits beyond bit 15
5158             $value       &= 0x7fff;             // first 15 bits
5159             $password    ^= ($value | $rotated_bits);
5160             $i++;
5161         }
5162
5163         $password ^= strlen($plaintext);
5164         $password ^= 0xCE4B;
5165
5166         return($password);
5167     }
5168
5169     /**
5170     * This method sets the properties for outlining and grouping. The defaults
5171     * correspond to Excel's defaults.
5172     *
5173     * @param bool $visible
5174     * @param bool $symbols_below
5175     * @param bool $symbols_right
5176     * @param bool $auto_style
5177     */
5178     function setOutline($visible = true, $symbols_below = true, $symbols_right = true, $auto_style = false)
5179     {
5180         $this->_outline_on    = $visible;
5181         $this->_outline_below = $symbols_below;
5182         $this->_outline_right = $symbols_right;
5183         $this->_outline_style = $auto_style;
5184
5185         // Ensure this is a boolean vale for Window2
5186         if ($this->_outline_on) {
5187             $this->_outline_on = 1;
5188         }
5189      }
5190
5191     /******************************************************************************
5192     *******************************************************************************
5193     *
5194     * BIFF RECORDS
5195     */
5196
5197
5198     /**
5199     * Write a double to the specified row and column (zero indexed).
5200     * An integer can be written as a double. Excel will display an
5201     * integer. $format is optional.
5202     *
5203     * Returns  0 : normal termination
5204     *         -2 : row or column out of range
5205     *
5206     * @access public
5207     * @param integer $row    Zero indexed row
5208     * @param integer $col    Zero indexed column
5209     * @param float   $num    The number to write
5210     * @param mixed   $format The optional XF format
5211     * @return integer
5212     */
5213     function writeNumber($row, $col, $num, $format = null)
5214     {
5215         $record    = 0x0203;                 // Record identifier
5216         $length    = 0x000E;                 // Number of bytes to follow
5217
5218         $xf        = $this->_XF($format);    // The cell format
5219
5220         // Check that row and col are valid and store max and min values
5221         if ($row >= $this->_xls_rowmax) {
5222             return(-2);
5223         }
5224         if ($col >= $this->_xls_colmax) {
5225             return(-2);
5226         }
5227         if ($row <  $this->_dim_rowmin)  {
5228             $this->_dim_rowmin = $row;
5229         }
5230         if ($row >  $this->_dim_rowmax)  {
5231             $this->_dim_rowmax = $row;
5232         }
5233         if ($col <  $this->_dim_colmin)  {
5234             $this->_dim_colmin = $col;
5235         }
5236         if ($col >  $this->_dim_colmax)  {
5237             $this->_dim_colmax = $col;
5238         }
5239
5240         $header    = pack("vv",  $record, $length);
5241         $data      = pack("vvv", $row, $col, $xf);
5242         $xl_double = pack("d",   $num);
5243         if ($this->_byte_order) { // if it's Big Endian
5244             $xl_double = strrev($xl_double);
5245         }
5246
5247         $this->_append($header.$data.$xl_double);
5248         return(0);
5249     }
5250
5251     /**
5252     * Write a string to the specified row and column (zero indexed).
5253     * NOTE: there is an Excel 5 defined limit of 255 characters.
5254     * $format is optional.
5255     * Returns  0 : normal termination
5256     *         -2 : row or column out of range
5257     *         -3 : long string truncated to 255 chars
5258     *
5259     * @access public
5260     * @param integer $row    Zero indexed row
5261     * @param integer $col    Zero indexed column
5262     * @param string  $str    The string to write
5263     * @param mixed   $format The XF format for the cell
5264     * @return integer
5265     */
5266     function writeString($row, $col, $str, $format = null)
5267     {
5268         if ($this->_BIFF_version == 0x0600) {
5269             return $this->writeStringBIFF8($row, $col, $str, $format);
5270         }
5271         $strlen    = strlen($str);
5272         $record    = 0x0204;                   // Record identifier
5273         $length    = 0x0008 + $strlen;         // Bytes to follow
5274         $xf        = $this->_XF($format);      // The cell format
5275
5276         $str_error = 0;
5277
5278         // Check that row and col are valid and store max and min values
5279         if ($row >= $this->_xls_rowmax) {
5280             return(-2);
5281         }
5282         if ($col >= $this->_xls_colmax) {
5283             return(-2);
5284         }
5285         if ($row <  $this->_dim_rowmin) {
5286             $this->_dim_rowmin = $row;
5287         }
5288         if ($row >  $this->_dim_rowmax) {
5289             $this->_dim_rowmax = $row;
5290         }
5291         if ($col <  $this->_dim_colmin) {
5292             $this->_dim_colmin = $col;
5293         }
5294         if ($col >  $this->_dim_colmax) {
5295             $this->_dim_colmax = $col;
5296         }
5297
5298         if ($strlen > $this->_xls_strmax) { // LABEL must be < 255 chars
5299             $str       = substr($str, 0, $this->_xls_strmax);
5300             $length    = 0x0008 + $this->_xls_strmax;
5301             $strlen    = $this->_xls_strmax;
5302             $str_error = -3;
5303         }
5304
5305         $header    = pack("vv",   $record, $length);
5306         $data      = pack("vvvv", $row, $col, $xf, $strlen);
5307         $this->_append($header . $data . $str);
5308         return($str_error);
5309     }
5310
5311     /**
5312     * Sets Input Encoding for writing strings
5313     *
5314     * @access public
5315     * @param string $encoding The encoding. Ex: 'UTF-16LE', 'utf-8', 'ISO-859-7'
5316     */
5317     function setInputEncoding($encoding)
5318     {
5319          if ($encoding != 'UTF-16LE' && !function_exists('iconv')) {
5320              die("Using an input encoding other than UTF-16LE requires PHP support for iconv");
5321          }
5322          $this->_input_encoding = $encoding;
5323     }
5324
5325     /** added 2009-03-05 by Joe Hunt, FA for arabic languages */
5326     function setRTL()
5327     {
5328         $this->_rtl = 1;
5329     }   
5330
5331     /**
5332     * Write a string to the specified row and column (zero indexed).
5333     * This is the BIFF8 version (no 255 chars limit).
5334     * $format is optional.
5335     * Returns  0 : normal termination
5336     *         -2 : row or column out of range
5337     *         -3 : long string truncated to 255 chars
5338     *
5339     * @access public
5340     * @param integer $row    Zero indexed row
5341     * @param integer $col    Zero indexed column
5342     * @param string  $str    The string to write
5343     * @param mixed   $format The XF format for the cell
5344     * @return integer
5345     */
5346     function writeStringBIFF8($row, $col, $str, $format = null)
5347     {
5348         if ($this->_input_encoding == 'UTF-16LE')
5349         {
5350             $strlen = function_exists('mb_strlen') ? mb_strlen($str, 'UTF-16LE') : (strlen($str) / 2);
5351             $encoding  = 0x1;
5352         }
5353         elseif ($this->_input_encoding != '')
5354         {
5355             $str = iconv($this->_input_encoding, 'UTF-16LE', $str);
5356             $strlen = function_exists('mb_strlen') ? mb_strlen($str, 'UTF-16LE') : (strlen($str) / 2);
5357             $encoding  = 0x1;
5358         }
5359         else
5360         {
5361             $strlen    = strlen($str);
5362             $encoding  = 0x0;
5363         }
5364         $record    = 0x00FD;                   // Record identifier
5365         $length    = 0x000A;                   // Bytes to follow
5366         $xf        = $this->_XF($format);      // The cell format
5367
5368         $str_error = 0;
5369
5370         // Check that row and col are valid and store max and min values
5371         if ($this->_checkRowCol($row, $col) == false) {
5372             return -2;
5373         }
5374
5375         $str = pack('vC', $strlen, $encoding).$str;
5376
5377         /* check if string is already present */
5378         if (!isset($this->_str_table[$str])) {
5379             $this->_str_table[$str] = $this->_str_unique++;
5380         }
5381         $this->_str_total++;
5382
5383         $header    = pack('vv',   $record, $length);
5384         $data      = pack('vvvV', $row, $col, $xf, $this->_str_table[$str]);
5385         $this->_append($header.$data);
5386         return $str_error;
5387     }
5388
5389     /**
5390     * Check row and col before writing to a cell, and update the sheet's
5391     * dimensions accordingly
5392     *
5393     * @access private
5394     * @param integer $row    Zero indexed row
5395     * @param integer $col    Zero indexed column
5396     * @return boolean true for success, false if row and/or col are grester
5397     *                 then maximums allowed.
5398     */
5399     function _checkRowCol($row, $col)
5400     {
5401         if ($row >= $this->_xls_rowmax) {
5402             return false;
5403         }
5404         if ($col >= $this->_xls_colmax) {
5405             return false;
5406         }
5407         if ($row <  $this->_dim_rowmin) {
5408             $this->_dim_rowmin = $row;
5409         }
5410         if ($row >  $this->_dim_rowmax) {
5411             $this->_dim_rowmax = $row;
5412         }
5413         if ($col <  $this->_dim_colmin) {
5414             $this->_dim_colmin = $col;
5415         }
5416         if ($col >  $this->_dim_colmax) {
5417             $this->_dim_colmax = $col;
5418         }
5419         return true;
5420     }
5421
5422     /**
5423     * Writes a note associated with the cell given by the row and column.
5424     * NOTE records don't have a length limit.
5425     *
5426     * @access public
5427     * @param integer $row    Zero indexed row
5428     * @param integer $col    Zero indexed column
5429     * @param string  $note   The note to write
5430     */
5431     function writeNote($row, $col, $note)
5432     {
5433         $note_length    = strlen($note);
5434         $record         = 0x001C;                // Record identifier
5435         $max_length     = 2048;                  // Maximun length for a NOTE record
5436         //$length      = 0x0006 + $note_length;    // Bytes to follow
5437
5438         // Check that row and col are valid and store max and min values
5439         if ($row >= $this->_xls_rowmax) {
5440             return(-2);
5441         }
5442         if ($col >= $this->_xls_colmax) {
5443             return(-2);
5444         }
5445         if ($row <  $this->_dim_rowmin) {
5446             $this->_dim_rowmin = $row;
5447         }
5448         if ($row >  $this->_dim_rowmax) {
5449             $this->_dim_rowmax = $row;
5450         }
5451         if ($col <  $this->_dim_colmin) {
5452             $this->_dim_colmin = $col;
5453         }
5454         if ($col >  $this->_dim_colmax) {
5455             $this->_dim_colmax = $col;
5456         }
5457
5458         // Length for this record is no more than 2048 + 6
5459         $length    = 0x0006 + min($note_length, 2048);
5460         $header    = pack("vv",   $record, $length);
5461         $data      = pack("vvv", $row, $col, $note_length);
5462         $this->_append($header . $data . substr($note, 0, 2048));
5463
5464         for ($i = $max_length; $i < $note_length; $i += $max_length) {
5465             $chunk  = substr($note, $i, $max_length);
5466             $length = 0x0006 + strlen($chunk);
5467             $header = pack("vv",   $record, $length);
5468             $data   = pack("vvv", -1, 0, strlen($chunk));
5469             $this->_append($header.$data.$chunk);
5470         }
5471         return(0);
5472     }
5473
5474     /**
5475     * Write a blank cell to the specified row and column (zero indexed).
5476     * A blank cell is used to specify formatting without adding a string
5477     * or a number.
5478     *
5479     * A blank cell without a format serves no purpose. Therefore, we don't write
5480     * a BLANK record unless a format is specified.
5481     *
5482     * Returns  0 : normal termination (including no format)
5483     *         -1 : insufficient number of arguments
5484     *         -2 : row or column out of range
5485     *
5486     * @access public
5487     * @param integer $row    Zero indexed row
5488     * @param integer $col    Zero indexed column
5489     * @param mixed   $format The XF format
5490     */
5491     function writeBlank($row, $col, $format)
5492     {
5493         // Don't write a blank cell unless it has a format
5494         if (!$format) {
5495             return(0);
5496         }
5497
5498         $record    = 0x0201;                 // Record identifier
5499         $length    = 0x0006;                 // Number of bytes to follow
5500         $xf        = $this->_XF($format);    // The cell format
5501
5502         // Check that row and col are valid and store max and min values
5503         if ($row >= $this->_xls_rowmax) {
5504             return(-2);
5505         }
5506         if ($col >= $this->_xls_colmax) {
5507             return(-2);
5508         }
5509         if ($row <  $this->_dim_rowmin) {
5510             $this->_dim_rowmin = $row;
5511         }
5512         if ($row >  $this->_dim_rowmax) {
5513             $this->_dim_rowmax = $row;
5514         }
5515         if ($col <  $this->_dim_colmin) {
5516             $this->_dim_colmin = $col;
5517         }
5518         if ($col >  $this->_dim_colmax) {
5519             $this->_dim_colmax = $col;
5520         }
5521
5522         $header    = pack("vv",  $record, $length);
5523         $data      = pack("vvv", $row, $col, $xf);
5524         $this->_append($header . $data);
5525         return 0;
5526     }
5527
5528     /**
5529     * Write a formula to the specified row and column (zero indexed).
5530     * The textual representation of the formula is passed to the parser in
5531     * Parser.php which returns a packed binary string.
5532     *
5533     * Returns  0 : normal termination
5534     *         -1 : formula errors (bad formula)
5535     *         -2 : row or column out of range
5536     *
5537     * @access public
5538     * @param integer $row     Zero indexed row
5539     * @param integer $col     Zero indexed column
5540     * @param string  $formula The formula text string
5541     * @param mixed   $format  The optional XF format
5542     * @return integer
5543     */
5544     function writeFormula($row, $col, $formula, $format = null)
5545     {
5546         $record    = 0x0006;     // Record identifier
5547
5548         // Excel normally stores the last calculated value of the formula in $num.
5549         // Clearly we are not in a position to calculate this a priori. Instead
5550         // we set $num to zero and set the option flags in $grbit to ensure
5551         // automatic calculation of the formula when the file is opened.
5552         //
5553         $xf        = $this->_XF($format); // The cell format
5554         $num       = 0x00;                // Current value of formula
5555         $grbit     = 0x03;                // Option flags
5556         $unknown   = 0x0000;              // Must be zero
5557
5558
5559         // Check that row and col are valid and store max and min values
5560         if ($this->_checkRowCol($row, $col) == false) {
5561             return -2;
5562         }
5563
5564         // Strip the '=' or '@' sign at the beginning of the formula string
5565         if (preg_match("/^=/", $formula)) {
5566             $formula = preg_replace("/(^=)/", "", $formula);
5567         } elseif (preg_match("/^@/", $formula)) {
5568             $formula = preg_replace("/(^@)/", "", $formula);
5569         } else {
5570             // Error handling
5571             $this->writeString($row, $col, 'Unrecognised character for formula');
5572             return -1;
5573         }
5574
5575         // Parse the formula using the parser in Parser.php
5576         $this->_parser->parse($formula);
5577  
5578         $formula = $this->_parser->toReversePolish();
5579  
5580         $formlen    = strlen($formula);    // Length of the binary string
5581         $length     = 0x16 + $formlen;     // Length of the record data
5582
5583         $header    = pack("vv",      $record, $length);
5584         $data      = pack("vvvdvVv", $row, $col, $xf, $num,
5585                                      $grbit, $unknown, $formlen);
5586
5587         $this->_append($header . $data . $formula);
5588         return 0;
5589     }
5590
5591     /**
5592     * Write a hyperlink.
5593     * This is comprised of two elements: the visible label and
5594     * the invisible link. The visible label is the same as the link unless an
5595     * alternative string is specified. The label is written using the
5596     * writeString() method. Therefore the 255 characters string limit applies.
5597     * $string and $format are optional.
5598     *
5599     * The hyperlink can be to a http, ftp, mail, internal sheet (not yet), or external
5600     * directory url.
5601     *
5602     * Returns  0 : normal termination
5603     *         -2 : row or column out of range
5604     *         -3 : long string truncated to 255 chars
5605     *
5606     * @access public
5607     * @param integer $row    Row
5608     * @param integer $col    Column
5609     * @param string  $url    URL string
5610     * @param string  $string Alternative label
5611     * @param mixed   $format The cell format
5612     * @return integer
5613     */
5614     function writeUrl($row, $col, $url, $string = '', $format = null)
5615     {
5616         // Add start row and col to arg list
5617         return($this->_writeUrlRange($row, $col, $row, $col, $url, $string, $format));
5618     }
5619
5620     /**
5621     * This is the more general form of writeUrl(). It allows a hyperlink to be
5622     * written to a range of cells. This function also decides the type of hyperlink
5623     * to be written. These are either, Web (http, ftp, mailto), Internal
5624     * (Sheet1!A1) or external ('c:\temp\foo.xls#Sheet1!A1').
5625     *
5626     * @access private
5627     * @see writeUrl()
5628     * @param integer $row1   Start row
5629     * @param integer $col1   Start column
5630     * @param integer $row2   End row
5631     * @param integer $col2   End column
5632     * @param string  $url    URL string
5633     * @param string  $string Alternative label
5634     * @param mixed   $format The cell format
5635     * @return integer
5636     */
5637
5638     function _writeUrlRange($row1, $col1, $row2, $col2, $url, $string = '', $format = null)
5639     {
5640
5641         // Check for internal/external sheet links or default to web link
5642         if (preg_match('[^internal:]', $url)) {
5643             return($this->_writeUrlInternal($row1, $col1, $row2, $col2, $url, $string, $format));
5644         }
5645         if (preg_match('[^external:]', $url)) {
5646             return($this->_writeUrlExternal($row1, $col1, $row2, $col2, $url, $string, $format));
5647         }
5648         return($this->_writeUrlWeb($row1, $col1, $row2, $col2, $url, $string, $format));
5649     }
5650
5651
5652     /**
5653     * Used to write http, ftp and mailto hyperlinks.
5654     * The link type ($options) is 0x03 is the same as absolute dir ref without
5655     * sheet. However it is differentiated by the $unknown2 data stream.
5656     *
5657     * @access private
5658     * @see writeUrl()
5659     * @param integer $row1   Start row
5660     * @param integer $col1   Start column
5661     * @param integer $row2   End row
5662     * @param integer $col2   End column
5663     * @param string  $url    URL string
5664     * @param string  $str    Alternative label
5665     * @param mixed   $format The cell format
5666     * @return integer
5667     */
5668     function _writeUrlWeb($row1, $col1, $row2, $col2, $url, $str, $format = null)
5669     {
5670         $record      = 0x01B8;                       // Record identifier
5671         $length      = 0x00000;                      // Bytes to follow
5672
5673         if (!$format) {
5674             $format = $this->_url_format;
5675         }
5676
5677         // Write the visible label using the writeString() method.
5678         if ($str == '') {
5679             $str = $url;
5680         }
5681         $str_error = $this->writeString($row1, $col1, $str, $format);
5682         if (($str_error == -2) || ($str_error == -3)) {
5683             return $str_error;
5684         }
5685
5686         // Pack the undocumented parts of the hyperlink stream
5687         $unknown1    = pack("H*", "D0C9EA79F9BACE118C8200AA004BA90B02000000");
5688         $unknown2    = pack("H*", "E0C9EA79F9BACE118C8200AA004BA90B");
5689
5690         // Pack the option flags
5691         $options     = pack("V", 0x03);
5692
5693         // Convert URL to a null terminated wchar string
5694         $url         = join("\0", preg_split("''", $url, -1, PREG_SPLIT_NO_EMPTY));
5695         $url         = $url . "\0\0\0";
5696
5697         // Pack the length of the URL
5698         $url_len     = pack("V", strlen($url));
5699
5700         // Calculate the data length
5701         $length      = 0x34 + strlen($url);
5702
5703         // Pack the header data
5704         $header      = pack("vv",   $record, $length);
5705         $data        = pack("vvvv", $row1, $row2, $col1, $col2);
5706
5707         // Write the packed data
5708         $this->_append($header . $data .
5709                        $unknown1 . $options .
5710                        $unknown2 . $url_len . $url);
5711         return($str_error);
5712     }
5713
5714     /**
5715     * Used to write internal reference hyperlinks such as "Sheet1!A1".
5716     *
5717     * @access private
5718     * @see writeUrl()
5719     * @param integer $row1   Start row
5720     * @param integer $col1   Start column
5721     * @param integer $row2   End row
5722     * @param integer $col2   End column
5723     * @param string  $url    URL string
5724     * @param string  $str    Alternative label
5725     * @param mixed   $format The cell format
5726     * @return integer
5727     */
5728     function _writeUrlInternal($row1, $col1, $row2, $col2, $url, $str, $format = null)
5729     {
5730         $record      = 0x01B8;                       // Record identifier
5731         $length      = 0x00000;                      // Bytes to follow
5732
5733         if (!$format) {
5734             $format = $this->_url_format;
5735         }
5736
5737         // Strip URL type
5738         $url = preg_replace('/^internal:/', '', $url);
5739
5740         // Write the visible label
5741         if ($str == '') {
5742             $str = $url;
5743         }
5744         $str_error = $this->writeString($row1, $col1, $str, $format);
5745         if (($str_error == -2) || ($str_error == -3)) {
5746             return $str_error;
5747         }
5748
5749         // Pack the undocumented parts of the hyperlink stream
5750         $unknown1    = pack("H*", "D0C9EA79F9BACE118C8200AA004BA90B02000000");
5751
5752         // Pack the option flags
5753         $options     = pack("V", 0x08);
5754
5755         // Convert the URL type and to a null terminated wchar string
5756         $url         = join("\0", preg_split("''", $url, -1, PREG_SPLIT_NO_EMPTY));
5757         $url         = $url . "\0\0\0";
5758
5759         // Pack the length of the URL as chars (not wchars)
5760         $url_len     = pack("V", floor(strlen($url)/2));
5761
5762         // Calculate the data length
5763         $length      = 0x24 + strlen($url);
5764
5765         // Pack the header data
5766         $header      = pack("vv",   $record, $length);
5767         $data        = pack("vvvv", $row1, $row2, $col1, $col2);
5768
5769         // Write the packed data
5770         $this->_append($header . $data .
5771                        $unknown1 . $options .
5772                        $url_len . $url);
5773         return($str_error);
5774     }
5775
5776     /**
5777     * Write links to external directory names such as 'c:\foo.xls',
5778     * c:\foo.xls#Sheet1!A1', '../../foo.xls'. and '../../foo.xls#Sheet1!A1'.
5779     *
5780     * Note: Excel writes some relative links with the $dir_long string. We ignore
5781     * these cases for the sake of simpler code.
5782     *
5783     * @access private
5784     * @see writeUrl()
5785     * @param integer $row1   Start row
5786     * @param integer $col1   Start column
5787     * @param integer $row2   End row
5788     * @param integer $col2   End column
5789     * @param string  $url    URL string
5790     * @param string  $str    Alternative label
5791     * @param mixed   $format The cell format
5792     * @return integer
5793     */
5794     function _writeUrlExternal($row1, $col1, $row2, $col2, $url, $str, $format = null)
5795     {
5796         // Network drives are different. We will handle them separately
5797         // MS/Novell network drives and shares start with \\
5798         if (preg_match('[^external:\\\\]', $url)) {
5799             return; //($this->_writeUrlExternal_net($row1, $col1, $row2, $col2, $url, $str, $format));
5800         }
5801     
5802         $record      = 0x01B8;                       // Record identifier
5803         $length      = 0x00000;                      // Bytes to follow
5804     
5805         if (!$format) {
5806             $format = $this->_url_format;
5807         }
5808     
5809         // Strip URL type and change Unix dir separator to Dos style (if needed)
5810         //
5811         $url = preg_replace('/^external:/', '', $url);
5812         $url = preg_replace('/\//', "\\", $url);
5813     
5814         // Write the visible label
5815         if ($str == '') {
5816             $str = preg_replace('/\#/', ' - ', $url);
5817         }
5818         $str_error = $this->writeString($row1, $col1, $str, $format);
5819         if (($str_error == -2) or ($str_error == -3)) {
5820             return $str_error;
5821         }
5822     
5823         // Determine if the link is relative or absolute:
5824         //   relative if link contains no dir separator, "somefile.xls"
5825         //   relative if link starts with up-dir, "..\..\somefile.xls"
5826         //   otherwise, absolute
5827         
5828         $absolute    = 0x02; // Bit mask
5829         if (!preg_match("/\\\/", $url)) {
5830             $absolute    = 0x00;
5831         }
5832         if (preg_match("/^\.\.\\\/", $url)) {
5833             $absolute    = 0x00;
5834         }
5835         $link_type               = 0x01 | $absolute;
5836     
5837         // Determine if the link contains a sheet reference and change some of the
5838         // parameters accordingly.
5839         // Split the dir name and sheet name (if it exists)
5840         /*if (preg_match("/\#/", $url)) {
5841             list($dir_long, $sheet) = preg_split("/\#/", $url);
5842         } else {
5843             $dir_long = $url;
5844         }
5845     
5846         if (isset($sheet)) {
5847             $link_type |= 0x08;
5848             $sheet_len  = pack("V", strlen($sheet) + 0x01);
5849             $sheet      = join("\0", preg_split('//', $sheet));
5850             $sheet     .= "\0\0\0";
5851         } else {
5852             $sheet_len   = '';
5853             $sheet       = '';
5854         }*/
5855         $dir_long = $url;
5856         if (preg_match("/\#/", $url)) {
5857             $link_type |= 0x08;
5858         }
5859
5860
5861     
5862         // Pack the link type
5863         $link_type   = pack("V", $link_type);
5864     
5865         // Calculate the up-level dir count e.g.. (..\..\..\ == 3)
5866         $up_count    = preg_match_all("/\.\.\\\/", $dir_long, $useless);
5867         $up_count    = pack("v", $up_count);
5868     
5869         // Store the short dos dir name (null terminated)
5870         $dir_short   = preg_replace("/\.\.\\\/", '', $dir_long) . "\0";
5871     
5872         // Store the long dir name as a wchar string (non-null terminated)
5873         //$dir_long       = join("\0", preg_split('//', $dir_long));
5874         $dir_long       = $dir_long . "\0";
5875     
5876         // Pack the lengths of the dir strings
5877         $dir_short_len = pack("V", strlen($dir_short)      );
5878         $dir_long_len  = pack("V", strlen($dir_long)       );
5879         $stream_len    = pack("V", 0);//strlen($dir_long) + 0x06);
5880     
5881         // Pack the undocumented parts of the hyperlink stream
5882         $unknown1 = pack("H*",'D0C9EA79F9BACE118C8200AA004BA90B02000000'       );
5883         $unknown2 = pack("H*",'0303000000000000C000000000000046'               );
5884         $unknown3 = pack("H*",'FFFFADDE000000000000000000000000000000000000000');
5885         $unknown4 = pack("v",  0x03                                            );
5886     
5887         // Pack the main data stream
5888         $data        = pack("vvvv", $row1, $row2, $col1, $col2) .
5889                           $unknown1     .
5890                           $link_type    .
5891                           $unknown2     .
5892                           $up_count     .
5893                           $dir_short_len.
5894                           $dir_short    .
5895                           $unknown3     .
5896                           $stream_len   .
5897                           $dir_long_len .
5898                           $unknown4     .
5899                           $dir_long     .
5900                           $sheet_len    .
5901                           $sheet        ;
5902     
5903         // Pack the header data
5904         $length   = strlen($data);
5905         $header   = pack("vv", $record, $length);
5906     
5907         // Write the packed data
5908         $this->_append($header. $data);
5909         return($str_error);
5910     }
5911
5912
5913     /**
5914     * This method is used to set the height and format for a row.
5915     *
5916     * @access public
5917     * @param integer $row    The row to set
5918     * @param integer $height Height we are giving to the row.
5919     *                        Use null to set XF without setting height
5920     * @param mixed   $format XF format we are giving to the row
5921     * @param bool    $hidden The optional hidden attribute
5922     * @param integer $level  The optional outline level for row, in range [0,7]
5923     */
5924     function setRow($row, $height, $format = null, $hidden = false, $level = 0)
5925     {
5926         $record      = 0x0208;               // Record identifier
5927         $length      = 0x0010;               // Number of bytes to follow
5928
5929         $colMic      = 0x0000;               // First defined column
5930         $colMac      = 0x0000;               // Last defined column
5931         $irwMac      = 0x0000;               // Used by Excel to optimise loading
5932         $reserved    = 0x0000;               // Reserved
5933         $grbit       = 0x0000;               // Option flags
5934         $ixfe        = $this->_XF($format);  // XF index
5935
5936         // set _row_sizes so _sizeRow() can use it
5937         $this->_row_sizes[$row] = $height;
5938
5939         // Use setRow($row, null, $XF) to set XF format without setting height
5940         if ($height != null) {
5941             $miyRw = $height * 20;  // row height
5942         } else {
5943             $miyRw = 0xff;          // default row height is 256
5944         }
5945
5946         $level = max(0, min($level, 7));  // level should be between 0 and 7
5947         $this->_outline_row_level = max($level, $this->_outline_row_level);
5948
5949
5950         // Set the options flags. fUnsynced is used to show that the font and row
5951         // heights are not compatible. This is usually the case for WriteExcel.
5952         // The collapsed flag 0x10 doesn't seem to be used to indicate that a row
5953         // is collapsed. Instead it is used to indicate that the previous row is
5954         // collapsed. The zero height flag, 0x20, is used to collapse a row.
5955
5956         $grbit |= $level;
5957         if ($hidden) {
5958             $grbit |= 0x0020;
5959         }
5960         $grbit |= 0x0040; // fUnsynced
5961         if ($format) {
5962             $grbit |= 0x0080;
5963         }
5964         $grbit |= 0x0100;
5965
5966         $header   = pack("vv",       $record, $length);
5967         $data     = pack("vvvvvvvv", $row, $colMic, $colMac, $miyRw,
5968                                      $irwMac,$reserved, $grbit, $ixfe);
5969         $this->_append($header.$data);
5970     }
5971
5972     /**
5973     * Writes Excel DIMENSIONS to define the area in which there is data.
5974     *
5975     * @access private
5976     */
5977     function _storeDimensions()
5978     {
5979         $record    = 0x0200;                 // Record identifier
5980         $row_min   = $this->_dim_rowmin;     // First row
5981         $row_max   = $this->_dim_rowmax + 1; // Last row plus 1
5982         $col_min   = $this->_dim_colmin;     // First column
5983         $col_max   = $this->_dim_colmax + 1; // Last column plus 1
5984         $reserved  = 0x0000;                 // Reserved by Excel
5985
5986         if ($this->_BIFF_version == 0x0500) {
5987             $length    = 0x000A;               // Number of bytes to follow
5988             $data      = pack("vvvvv", $row_min, $row_max,
5989                                        $col_min, $col_max, $reserved);
5990         } elseif ($this->_BIFF_version == 0x0600) {
5991             $length    = 0x000E;
5992             $data      = pack("VVvvv", $row_min, $row_max,
5993                                        $col_min, $col_max, $reserved);
5994         }
5995         $header = pack("vv", $record, $length);
5996         $this->_prepend($header.$data);
5997     }
5998
5999     /**
6000     * Write BIFF record Window2.
6001     *
6002     * @access private
6003     */
6004     function _storeWindow2()
6005     {
6006         $record         = 0x023E;     // Record identifier
6007         if ($this->_BIFF_version == 0x0500) {
6008             $length         = 0x000A;     // Number of bytes to follow
6009         } elseif ($this->_BIFF_version == 0x0600) {
6010             $length         = 0x0012;
6011         }
6012
6013         $grbit          = 0x00B6;     // Option flags
6014         $rwTop          = 0x0000;     // Top row visible in window
6015         $colLeft        = 0x0000;     // Leftmost column visible in window
6016
6017
6018         // The options flags that comprise $grbit
6019         $fDspFmla       = 0;                     // 0 - bit
6020         $fDspGrid       = $this->_screen_gridlines; // 1
6021         $fDspRwCol      = 1;                     // 2
6022         $fFrozen        = $this->_frozen;        // 3
6023         $fDspZeros      = 1;                     // 4
6024         $fDefaultHdr    = 1;                     // 5
6025         $fArabic        = $this->_rtl;           // 6
6026         $fDspGuts       = $this->_outline_on;    // 7
6027         $fFrozenNoSplit = 0;                     // 0 - bit
6028         $fSelected      = $this->selected;       // 1
6029         $fPaged         = 1;                     // 2
6030
6031         $grbit             = $fDspFmla;
6032         $grbit            |= $fDspGrid       << 1;
6033         $grbit            |= $fDspRwCol      << 2;
6034         $grbit            |= $fFrozen        << 3;
6035         $grbit            |= $fDspZeros      << 4;
6036         $grbit            |= $fDefaultHdr    << 5;
6037         $grbit            |= $fArabic        << 6;
6038         $grbit            |= $fDspGuts       << 7;
6039         $grbit            |= $fFrozenNoSplit << 8;
6040         $grbit            |= $fSelected      << 9;
6041         $grbit            |= $fPaged         << 10;
6042
6043         $header  = pack("vv",   $record, $length);
6044         $data    = pack("vvv", $grbit, $rwTop, $colLeft);
6045         // FIXME !!!
6046         if ($this->_BIFF_version == 0x0500) {
6047             $rgbHdr         = 0x00000000; // Row/column heading and gridline color
6048             $data .= pack("V", $rgbHdr);
6049         } elseif ($this->_BIFF_version == 0x0600) {
6050             $rgbHdr       = 0x0040; // Row/column heading and gridline color index
6051             $zoom_factor_page_break = 0x0000;
6052             $zoom_factor_normal     = 0x0000;
6053             $data .= pack("vvvvV", $rgbHdr, 0x0000, $zoom_factor_page_break, $zoom_factor_normal, 0x00000000);
6054         }
6055         $this->_append($header.$data);
6056     }
6057
6058     /**
6059     * Write BIFF record DEFCOLWIDTH if COLINFO records are in use.
6060     *
6061     * @access private
6062     */
6063     function _storeDefcol()
6064     {
6065         $record   = 0x0055;      // Record identifier
6066         $length   = 0x0002;      // Number of bytes to follow
6067         $colwidth = 0x0008;      // Default column width
6068
6069         $header   = pack("vv", $record, $length);
6070         $data     = pack("v",  $colwidth);
6071         $this->_prepend($header . $data);
6072     }
6073
6074     /**
6075     * Write BIFF record COLINFO to define column widths
6076     *
6077     * Note: The SDK says the record length is 0x0B but Excel writes a 0x0C
6078     * length record.
6079     *
6080     * @access private
6081     * @param array $col_array This is the only parameter received and is composed of the following:
6082     *                0 => First formatted column,
6083     *                1 => Last formatted column,
6084     *                2 => Col width (8.43 is Excel default),
6085     *                3 => The optional XF format of the column,
6086     *                4 => Option flags.
6087     *                5 => Optional outline level
6088     */
6089     function _storeColinfo($col_array)
6090     {
6091         if (isset($col_array[0])) {
6092             $colFirst = $col_array[0];
6093         }
6094         if (isset($col_array[1])) {
6095             $colLast = $col_array[1];
6096         }
6097         if (isset($col_array[2])) {
6098             $coldx = $col_array[2];
6099         } else {
6100             $coldx = 8.43;
6101         }
6102         if (isset($col_array[3])) {
6103             $format = $col_array[3];
6104         } else {
6105             $format = 0;
6106         }
6107         if (isset($col_array[4])) {
6108             $grbit = $col_array[4];
6109         } else {
6110             $grbit = 0;
6111         }
6112         if (isset($col_array[5])) {
6113             $level = $col_array[5];
6114         } else {
6115             $level = 0;
6116         }
6117         $record   = 0x007D;          // Record identifier
6118         $length   = 0x000B;          // Number of bytes to follow
6119
6120         $coldx   += 0.72;            // Fudge. Excel subtracts 0.72 !?
6121         $coldx   *= 256;             // Convert to units of 1/256 of a char
6122
6123         $ixfe     = $this->_XF($format);
6124         $reserved = 0x00;            // Reserved
6125
6126         $level = max(0, min($level, 7));
6127         $grbit |= $level << 8;
6128
6129         $header   = pack("vv",     $record, $length);
6130         $data     = pack("vvvvvC", $colFirst, $colLast, $coldx,
6131                                    $ixfe, $grbit, $reserved);
6132         $this->_prepend($header.$data);
6133     }
6134
6135     /**
6136     * Write BIFF record SELECTION.
6137     *
6138     * @access private
6139     * @param array $array array containing ($rwFirst,$colFirst,$rwLast,$colLast)
6140     * @see setSelection()
6141     */
6142     function _storeSelection($array)
6143     {
6144         list($rwFirst,$colFirst,$rwLast,$colLast) = $array;
6145         $record   = 0x001D;                  // Record identifier
6146         $length   = 0x000F;                  // Number of bytes to follow
6147
6148         $pnn      = $this->_active_pane;     // Pane position
6149         $rwAct    = $rwFirst;                // Active row
6150         $colAct   = $colFirst;               // Active column
6151         $irefAct  = 0;                       // Active cell ref
6152         $cref     = 1;                       // Number of refs
6153
6154         if (!isset($rwLast)) {
6155             $rwLast   = $rwFirst;       // Last  row in reference
6156         }
6157         if (!isset($colLast)) {
6158             $colLast  = $colFirst;      // Last  col in reference
6159         }
6160
6161         // Swap last row/col for first row/col as necessary
6162         if ($rwFirst > $rwLast) {
6163             list($rwFirst, $rwLast) = array($rwLast, $rwFirst);
6164         }
6165
6166         if ($colFirst > $colLast) {
6167             list($colFirst, $colLast) = array($colLast, $colFirst);
6168         }
6169
6170         $header   = pack("vv",         $record, $length);
6171         $data     = pack("CvvvvvvCC",  $pnn, $rwAct, $colAct,
6172                                        $irefAct, $cref,
6173                                        $rwFirst, $rwLast,
6174                                        $colFirst, $colLast);
6175         $this->_append($header . $data);
6176     }
6177
6178     /**
6179     * Store the MERGEDCELLS record for all ranges of merged cells
6180     *
6181     * @access private
6182     */
6183     function _storeMergedCells()
6184     {
6185         // if there are no merged cell ranges set, return
6186         if (count($this->_merged_ranges) == 0) {
6187             return;
6188         }
6189         $record   = 0x00E5;
6190         $length   = 2 + count($this->_merged_ranges) * 8;
6191
6192         $header   = pack('vv', $record, $length);
6193         $data     = pack('v',  count($this->_merged_ranges));
6194         foreach ($this->_merged_ranges as $range) {
6195             $data .= pack('vvvv', $range[0], $range[2], $range[1], $range[3]);
6196         }
6197         $this->_append($header . $data);
6198     }
6199
6200     /**
6201     * Write BIFF record EXTERNCOUNT to indicate the number of external sheet
6202     * references in a worksheet.
6203     *
6204     * Excel only stores references to external sheets that are used in formulas.
6205     * For simplicity we store references to all the sheets in the workbook
6206     * regardless of whether they are used or not. This reduces the overall
6207     * complexity and eliminates the need for a two way dialogue between the formula
6208     * parser the worksheet objects.
6209     *
6210     * @access private
6211     * @param integer $count The number of external sheet references in this worksheet
6212     */
6213     function _storeExterncount($count)
6214     {
6215         $record = 0x0016;          // Record identifier
6216         $length = 0x0002;          // Number of bytes to follow
6217
6218         $header = pack("vv", $record, $length);
6219         $data   = pack("v",  $count);
6220         $this->_prepend($header . $data);
6221     }
6222
6223     /**
6224     * Writes the Excel BIFF EXTERNSHEET record. These references are used by
6225     * formulas. A formula references a sheet name via an index. Since we store a
6226     * reference to all of the external worksheets the EXTERNSHEET index is the same
6227     * as the worksheet index.
6228     *
6229     * @access private
6230     * @param string $sheetname The name of a external worksheet
6231     */
6232     function _storeExternsheet($sheetname)
6233     {
6234         $record    = 0x0017;         // Record identifier
6235
6236         // References to the current sheet are encoded differently to references to
6237         // external sheets.
6238         //
6239         if ($this->name == $sheetname) {
6240             $sheetname = '';
6241             $length    = 0x02;  // The following 2 bytes
6242             $cch       = 1;     // The following byte
6243             $rgch      = 0x02;  // Self reference
6244         } else {
6245             $length    = 0x02 + strlen($sheetname);
6246             $cch       = strlen($sheetname);
6247             $rgch      = 0x03;  // Reference to a sheet in the current workbook
6248         }
6249
6250         $header = pack("vv",  $record, $length);
6251         $data   = pack("CC", $cch, $rgch);
6252         $this->_prepend($header . $data . $sheetname);
6253     }
6254
6255     /**
6256     * Writes the Excel BIFF PANE record.
6257     * The panes can either be frozen or thawed (unfrozen).
6258     * Frozen panes are specified in terms of an integer number of rows and columns.
6259     * Thawed panes are specified in terms of Excel's units for rows and columns.
6260     *
6261     * @access private
6262     * @param array $panes This is the only parameter received and is composed of the following:
6263     *                     0 => Vertical split position,
6264     *                     1 => Horizontal split position
6265     *                     2 => Top row visible
6266     *                     3 => Leftmost column visible
6267     *                     4 => Active pane
6268     */
6269     function _storePanes($panes)
6270     {
6271         $y       = $panes[0];
6272         $x       = $panes[1];
6273         $rwTop   = $panes[2];
6274         $colLeft = $panes[3];
6275         if (count($panes) > 4) { // if Active pane was received
6276             $pnnAct = $panes[4];
6277         } else {
6278             $pnnAct = null;
6279         }
6280         $record  = 0x0041;       // Record identifier
6281         $length  = 0x000A;       // Number of bytes to follow
6282
6283         // Code specific to frozen or thawed panes.
6284         if ($this->_frozen) {
6285             // Set default values for $rwTop and $colLeft
6286             if (!isset($rwTop)) {
6287                 $rwTop   = $y;
6288             }
6289             if (!isset($colLeft)) {
6290                 $colLeft = $x;
6291             }
6292         } else {
6293             // Set default values for $rwTop and $colLeft
6294             if (!isset($rwTop)) {
6295                 $rwTop   = 0;
6296             }
6297             if (!isset($colLeft)) {
6298                 $colLeft = 0;
6299             }
6300
6301             // Convert Excel's row and column units to the internal units.
6302             // The default row height is 12.75
6303             // The default column width is 8.43
6304             // The following slope and intersection values were interpolated.
6305             //
6306             $y = 20*$y      + 255;
6307             $x = 113.879*$x + 390;
6308         }
6309
6310
6311         // Determine which pane should be active. There is also the undocumented
6312         // option to override this should it be necessary: may be removed later.
6313         //
6314         if (!isset($pnnAct)) {
6315             if ($x != 0 && $y != 0) {
6316                 $pnnAct = 0; // Bottom right
6317             }
6318             if ($x != 0 && $y == 0) {
6319                 $pnnAct = 1; // Top right
6320             }
6321             if ($x == 0 && $y != 0) {
6322                 $pnnAct = 2; // Bottom left
6323             }
6324             if ($x == 0 && $y == 0) {
6325                 $pnnAct = 3; // Top left
6326             }
6327         }
6328
6329         $this->_active_pane = $pnnAct; // Used in _storeSelection
6330
6331         $header     = pack("vv",    $record, $length);
6332         $data       = pack("vvvvv", $x, $y, $rwTop, $colLeft, $pnnAct);
6333         $this->_append($header . $data);
6334     }
6335
6336     /**
6337     * Store the page setup SETUP BIFF record.
6338     *
6339     * @access private
6340     */
6341     function _storeSetup()
6342     {
6343         $record       = 0x00A1;                  // Record identifier
6344         $length       = 0x0022;                  // Number of bytes to follow
6345
6346         $iPaperSize   = $this->_paper_size;    // Paper size
6347         $iScale       = $this->_print_scale;   // Print scaling factor
6348         $iPageStart   = 0x01;                 // Starting page number
6349         $iFitWidth    = $this->_fit_width;    // Fit to number of pages wide
6350         $iFitHeight   = $this->_fit_height;   // Fit to number of pages high
6351         $grbit        = 0x00;                 // Option flags
6352         $iRes         = 0x0258;               // Print resolution
6353         $iVRes        = 0x0258;               // Vertical print resolution
6354         $numHdr       = $this->_margin_head;  // Header Margin
6355         $numFtr       = $this->_margin_foot;   // Footer Margin
6356         $iCopies      = 0x01;                 // Number of copies
6357
6358         $fLeftToRight = 0x0;                     // Print over then down
6359         $fLandscape   = $this->_orientation;     // Page orientation
6360         $fNoPls       = 0x0;                     // Setup not read from printer
6361         $fNoColor     = 0x0;                     // Print black and white
6362         $fDraft       = 0x0;                     // Print draft quality
6363         $fNotes       = 0x0;                     // Print notes
6364         $fNoOrient    = 0x0;                     // Orientation not set
6365         $fUsePage     = 0x0;                     // Use custom starting page
6366
6367         $grbit           = $fLeftToRight;
6368         $grbit          |= $fLandscape    << 1;
6369         $grbit          |= $fNoPls        << 2;
6370         $grbit          |= $fNoColor      << 3;
6371         $grbit          |= $fDraft        << 4;
6372         $grbit          |= $fNotes        << 5;
6373         $grbit          |= $fNoOrient     << 6;
6374         $grbit          |= $fUsePage      << 7;
6375
6376         $numHdr = pack("d", $numHdr);
6377         $numFtr = pack("d", $numFtr);
6378         if ($this->_byte_order) { // if it's Big Endian
6379             $numHdr = strrev($numHdr);
6380             $numFtr = strrev($numFtr);
6381         }
6382
6383         $header = pack("vv", $record, $length);
6384         $data1  = pack("vvvvvvvv", $iPaperSize,
6385                                    $iScale,
6386                                    $iPageStart,
6387                                    $iFitWidth,
6388                                    $iFitHeight,
6389                                    $grbit,
6390                                    $iRes,
6391                                    $iVRes);
6392         $data2  = $numHdr.$numFtr;
6393         $data3  = pack("v", $iCopies);
6394         $this->_prepend($header . $data1 . $data2 . $data3);
6395     }
6396
6397     /**
6398     * Store the header caption BIFF record.
6399     *
6400     * @access private
6401     */
6402     function _storeHeader()
6403     {
6404         $record  = 0x0014;               // Record identifier
6405
6406         $str      = $this->_header;       // header string
6407         $cch      = strlen($str);         // Length of header string
6408         if ($this->_BIFF_version == 0x0600) {
6409             $encoding = 0x0;                  // TODO: Unicode support
6410             $length   = 3 + $cch;             // Bytes to follow
6411         } else {
6412             $length  = 1 + $cch;             // Bytes to follow
6413         }
6414
6415         $header   = pack("vv", $record, $length);
6416         if ($this->_BIFF_version == 0x0600) {
6417             $data     = pack("vC",  $cch, $encoding);
6418         } else {
6419             $data      = pack("C",  $cch);
6420         }
6421
6422         $this->_prepend($header.$data.$str);
6423     }
6424
6425     /**
6426     * Store the footer caption BIFF record.
6427     *
6428     * @access private
6429     */
6430     function _storeFooter()
6431     {
6432         $record  = 0x0015;               // Record identifier
6433
6434         $str      = $this->_footer;       // Footer string
6435         $cch      = strlen($str);         // Length of footer string
6436         if ($this->_BIFF_version == 0x0600) {
6437             $encoding = 0x0;                  // TODO: Unicode support
6438             $length   = 3 + $cch;             // Bytes to follow
6439         } else {
6440             $length  = 1 + $cch;
6441         }
6442
6443         $header    = pack("vv", $record, $length);
6444         if ($this->_BIFF_version == 0x0600) {
6445             $data      = pack("vC",  $cch, $encoding);
6446         } else {
6447             $data      = pack("C",  $cch);
6448         }
6449
6450         $this->_prepend($header . $data . $str);
6451     }
6452
6453     /**
6454     * Store the horizontal centering HCENTER BIFF record.
6455     *
6456     * @access private
6457     */
6458     function _storeHcenter()
6459     {
6460         $record   = 0x0083;              // Record identifier
6461         $length   = 0x0002;              // Bytes to follow
6462
6463         $fHCenter = $this->_hcenter;     // Horizontal centering
6464
6465         $header    = pack("vv", $record, $length);
6466         $data      = pack("v",  $fHCenter);
6467
6468         $this->_prepend($header.$data);
6469     }
6470
6471     /**
6472     * Store the vertical centering VCENTER BIFF record.
6473     *
6474     * @access private
6475     */
6476     function _storeVcenter()
6477     {
6478         $record   = 0x0084;              // Record identifier
6479         $length   = 0x0002;              // Bytes to follow
6480
6481         $fVCenter = $this->_vcenter;     // Horizontal centering
6482
6483         $header    = pack("vv", $record, $length);
6484         $data      = pack("v",  $fVCenter);
6485         $this->_prepend($header . $data);
6486     }
6487
6488     /**
6489     * Store the LEFTMARGIN BIFF record.
6490     *
6491     * @access private
6492     */
6493     function _storeMarginLeft()
6494     {
6495         $record  = 0x0026;                   // Record identifier
6496         $length  = 0x0008;                   // Bytes to follow
6497
6498         $margin  = $this->_margin_left;       // Margin in inches
6499
6500         $header    = pack("vv",  $record, $length);
6501         $data      = pack("d",   $margin);
6502         if ($this->_byte_order) { // if it's Big Endian
6503             $data = strrev($data);
6504         }
6505
6506         $this->_prepend($header . $data);
6507     }
6508
6509     /**
6510     * Store the RIGHTMARGIN BIFF record.
6511     *
6512     * @access private
6513     */
6514     function _storeMarginRight()
6515     {
6516         $record  = 0x0027;                   // Record identifier
6517         $length  = 0x0008;                   // Bytes to follow
6518
6519         $margin  = $this->_margin_right;      // Margin in inches
6520
6521         $header    = pack("vv",  $record, $length);
6522         $data      = pack("d",   $margin);
6523         if ($this->_byte_order) { // if it's Big Endian
6524             $data = strrev($data);
6525         }
6526
6527         $this->_prepend($header . $data);
6528     }
6529
6530     /**
6531     * Store the TOPMARGIN BIFF record.
6532     *
6533     * @access private
6534     */
6535     function _storeMarginTop()
6536     {
6537         $record  = 0x0028;                   // Record identifier
6538         $length  = 0x0008;                   // Bytes to follow
6539
6540         $margin  = $this->_margin_top;        // Margin in inches
6541
6542         $header    = pack("vv",  $record, $length);
6543         $data      = pack("d",   $margin);
6544         if ($this->_byte_order) { // if it's Big Endian
6545             $data = strrev($data);
6546         }
6547
6548         $this->_prepend($header . $data);
6549     }
6550
6551     /**
6552     * Store the BOTTOMMARGIN BIFF record.
6553     *
6554     * @access private
6555     */
6556     function _storeMarginBottom()
6557     {
6558         $record  = 0x0029;                   // Record identifier
6559         $length  = 0x0008;                   // Bytes to follow
6560
6561         $margin  = $this->_margin_bottom;     // Margin in inches
6562
6563         $header    = pack("vv",  $record, $length);
6564         $data      = pack("d",   $margin);
6565         if ($this->_byte_order) { // if it's Big Endian
6566             $data = strrev($data);
6567         }
6568
6569         $this->_prepend($header . $data);
6570     }
6571
6572     /**
6573     * Merges the area given by its arguments.
6574     * This is an Excel97/2000 method. It is required to perform more complicated
6575     * merging than the normal setAlign('merge').
6576     *
6577     * @access public
6578     * @param integer $first_row First row of the area to merge
6579     * @param integer $first_col First column of the area to merge
6580     * @param integer $last_row  Last row of the area to merge
6581     * @param integer $last_col  Last column of the area to merge
6582     */
6583     function mergeCells($first_row, $first_col, $last_row, $last_col)
6584     {
6585         $record  = 0x00E5;                   // Record identifier
6586         $length  = 0x000A;                   // Bytes to follow
6587         $cref     = 1;                       // Number of refs
6588
6589         // Swap last row/col for first row/col as necessary
6590         if ($first_row > $last_row) {
6591             list($first_row, $last_row) = array($last_row, $first_row);
6592         }
6593
6594         if ($first_col > $last_col) {
6595             list($first_col, $last_col) = array($last_col, $first_col);
6596         }
6597
6598         $header   = pack("vv",    $record, $length);
6599         $data     = pack("vvvvv", $cref, $first_row, $last_row,
6600                                   $first_col, $last_col);
6601
6602         $this->_append($header.$data);
6603     }
6604
6605     /**
6606     * Write the PRINTHEADERS BIFF record.
6607     *
6608     * @access private
6609     */
6610     function _storePrintHeaders()
6611     {
6612         $record      = 0x002a;                   // Record identifier
6613         $length      = 0x0002;                   // Bytes to follow
6614
6615         $fPrintRwCol = $this->_print_headers;     // Boolean flag
6616
6617         $header      = pack("vv", $record, $length);
6618         $data        = pack("v", $fPrintRwCol);
6619         $this->_prepend($header . $data);
6620     }
6621
6622     /**
6623     * Write the PRINTGRIDLINES BIFF record. Must be used in conjunction with the
6624     * GRIDSET record.
6625     *
6626     * @access private
6627     */
6628     function _storePrintGridlines()
6629     {
6630         $record      = 0x002b;                    // Record identifier
6631         $length      = 0x0002;                    // Bytes to follow
6632
6633         $fPrintGrid  = $this->_print_gridlines;    // Boolean flag
6634
6635         $header      = pack("vv", $record, $length);
6636         $data        = pack("v", $fPrintGrid);
6637         $this->_prepend($header . $data);
6638     }
6639
6640     /**
6641     * Write the GRIDSET BIFF record. Must be used in conjunction with the
6642     * PRINTGRIDLINES record.
6643     *
6644     * @access private
6645     */
6646     function _storeGridset()
6647     {
6648         $record      = 0x0082;                        // Record identifier
6649         $length      = 0x0002;                        // Bytes to follow
6650
6651         $fGridSet    = !($this->_print_gridlines);     // Boolean flag
6652
6653         $header      = pack("vv",  $record, $length);
6654         $data        = pack("v",   $fGridSet);
6655         $this->_prepend($header . $data);
6656     }
6657
6658     /**
6659     * Write the GUTS BIFF record. This is used to configure the gutter margins
6660     * where Excel outline symbols are displayed. The visibility of the gutters is
6661     * controlled by a flag in WSBOOL.
6662     *
6663     * @see _storeWsbool()
6664     * @access private
6665     */
6666     function _storeGuts()
6667     {
6668         $record      = 0x0080;   // Record identifier
6669         $length      = 0x0008;   // Bytes to follow
6670
6671         $dxRwGut     = 0x0000;   // Size of row gutter
6672         $dxColGut    = 0x0000;   // Size of col gutter
6673
6674         $row_level   = $this->_outline_row_level;
6675         $col_level   = 0;
6676
6677         // Calculate the maximum column outline level. The equivalent calculation
6678         // for the row outline level is carried out in setRow().
6679         $colcount = count($this->_colinfo);
6680         for ($i = 0; $i < $colcount; $i++) {
6681            // Skip cols without outline level info.
6682            if (count_array($col_level) >= 6) {
6683               $col_level = max($this->_colinfo[$i][5], $col_level);
6684            }
6685         }
6686
6687         // Set the limits for the outline levels (0 <= x <= 7).
6688         $col_level = max(0, min($col_level, 7));
6689
6690         // The displayed level is one greater than the max outline levels
6691         if ($row_level) {
6692             $row_level++;
6693         }
6694         if ($col_level) {
6695             $col_level++;
6696         }
6697
6698         $header      = pack("vv",   $record, $length);
6699         $data        = pack("vvvv", $dxRwGut, $dxColGut, $row_level, $col_level);
6700
6701         $this->_prepend($header.$data);
6702     }
6703
6704
6705     /**
6706     * Write the WSBOOL BIFF record, mainly for fit-to-page. Used in conjunction
6707     * with the SETUP record.
6708     *
6709     * @access private
6710     */
6711     function _storeWsbool()
6712     {
6713         $record      = 0x0081;   // Record identifier
6714         $length      = 0x0002;   // Bytes to follow
6715         $grbit       = 0x0000;
6716
6717         // The only option that is of interest is the flag for fit to page. So we
6718         // set all the options in one go.
6719         //
6720         /*if ($this->_fit_page) {
6721             $grbit = 0x05c1;
6722         } else {
6723             $grbit = 0x04c1;
6724         }*/
6725         // Set the option flags
6726         $grbit |= 0x0001;                           // Auto page breaks visible
6727         if ($this->_outline_style) {
6728             $grbit |= 0x0020; // Auto outline styles
6729         }
6730         if ($this->_outline_below) {
6731             $grbit |= 0x0040; // Outline summary below
6732         }
6733         if ($this->_outline_right) {
6734             $grbit |= 0x0080; // Outline summary right
6735         }
6736         if ($this->_fit_page) {
6737             $grbit |= 0x0100; // Page setup fit to page
6738         }
6739         if ($this->_outline_on) {
6740             $grbit |= 0x0400; // Outline symbols displayed
6741         }
6742
6743         $header      = pack("vv", $record, $length);
6744         $data        = pack("v",  $grbit);
6745         $this->_prepend($header . $data);
6746     }
6747
6748     /**
6749     * Write the HORIZONTALPAGEBREAKS BIFF record.
6750     *
6751     * @access private
6752     */
6753     function _storeHbreak()
6754     {
6755         // Return if the user hasn't specified pagebreaks
6756         if (empty($this->_hbreaks)) {
6757             return;
6758         }
6759
6760         // Sort and filter array of page breaks
6761         $breaks = $this->_hbreaks;
6762         sort($breaks, SORT_NUMERIC);
6763         if ($breaks[0] == 0) { // don't use first break if it's 0
6764             array_shift($breaks);
6765         }
6766
6767         $record  = 0x001b;               // Record identifier
6768         $cbrk    = count($breaks);       // Number of page breaks
6769         if ($this->_BIFF_version == 0x0600) {
6770             $length  = 2 + 6*$cbrk;      // Bytes to follow
6771         } else {
6772             $length  = 2 + 2*$cbrk;      // Bytes to follow
6773         }
6774
6775         $header  = pack("vv", $record, $length);
6776         $data    = pack("v",  $cbrk);
6777
6778         // Append each page break
6779         foreach ($breaks as $break) {
6780             if ($this->_BIFF_version == 0x0600) {
6781                 $data .= pack("vvv", $break, 0x0000, 0x00ff);
6782             } else {
6783                 $data .= pack("v", $break);
6784             }
6785         }
6786
6787         $this->_prepend($header.$data);
6788     }
6789
6790
6791     /**
6792     * Write the VERTICALPAGEBREAKS BIFF record.
6793     *
6794     * @access private
6795     */
6796     function _storeVbreak()
6797     {
6798         // Return if the user hasn't specified pagebreaks
6799         if (empty($this->_vbreaks)) {
6800             return;
6801         }
6802
6803         // 1000 vertical pagebreaks appears to be an internal Excel 5 limit.
6804         // It is slightly higher in Excel 97/200, approx. 1026
6805         $breaks = array_slice($this->_vbreaks,0,1000);
6806
6807         // Sort and filter array of page breaks
6808         sort($breaks, SORT_NUMERIC);
6809         if ($breaks[0] == 0) { // don't use first break if it's 0
6810             array_shift($breaks);
6811         }
6812
6813         $record  = 0x001a;               // Record identifier
6814         $cbrk    = count($breaks);       // Number of page breaks
6815         if ($this->_BIFF_version == 0x0600) {
6816             $length  = 2 + 6*$cbrk;      // Bytes to follow
6817         } else {
6818             $length  = 2 + 2*$cbrk;      // Bytes to follow
6819         }
6820
6821         $header  = pack("vv",  $record, $length);
6822         $data    = pack("v",   $cbrk);
6823
6824         // Append each page break
6825         foreach ($breaks as $break) {
6826             if ($this->_BIFF_version == 0x0600) {
6827                 $data .= pack("vvv", $break, 0x0000, 0xffff);
6828             } else {
6829                 $data .= pack("v", $break);
6830             }
6831         }
6832
6833         $this->_prepend($header . $data);
6834     }
6835
6836     /**
6837     * Set the Biff PROTECT record to indicate that the worksheet is protected.
6838     *
6839     * @access private
6840     */
6841     function _storeProtect()
6842     {
6843         // Exit unless sheet protection has been specified
6844         if ($this->_protect == 0) {
6845             return;
6846         }
6847
6848         $record      = 0x0012;             // Record identifier
6849         $length      = 0x0002;             // Bytes to follow
6850
6851         $fLock       = $this->_protect;    // Worksheet is protected
6852
6853         $header      = pack("vv", $record, $length);
6854         $data        = pack("v",  $fLock);
6855
6856         $this->_prepend($header.$data);
6857     }
6858
6859     /**
6860     * Write the worksheet PASSWORD record.
6861     *
6862     * @access private
6863     */
6864     function _storePassword()
6865     {
6866         // Exit unless sheet protection and password have been specified
6867         if (($this->_protect == 0) || (!isset($this->_password))) {
6868             return;
6869         }
6870
6871         $record      = 0x0013;               // Record identifier
6872         $length      = 0x0002;               // Bytes to follow
6873
6874         $wPassword   = $this->_password;     // Encoded password
6875
6876         $header      = pack("vv", $record, $length);
6877         $data        = pack("v",  $wPassword);
6878
6879         $this->_prepend($header . $data);
6880     }
6881
6882
6883     /**
6884     * Insert a 24bit bitmap image in a worksheet.
6885     *
6886     * @access public
6887     * @param integer $row     The row we are going to insert the bitmap into
6888     * @param integer $col     The column we are going to insert the bitmap into
6889     * @param string  $bitmap  The bitmap filename
6890     * @param integer $x       The horizontal position (offset) of the image inside the cell.
6891     * @param integer $y       The vertical position (offset) of the image inside the cell.
6892     * @param integer $scale_x The horizontal scale
6893     * @param integer $scale_y The vertical scale
6894     */
6895     function insertBitmap($row, $col, $bitmap, $x = 0, $y = 0, $scale_x = 1, $scale_y = 1)
6896     {
6897         $bitmap_array = $this->_processBitmap($bitmap);
6898         if ($this->isError($bitmap_array)) {
6899             $this->writeString($row, $col, $bitmap_array->getMessage());
6900             return;
6901         }
6902         list($width, $height, $size, $data) = $bitmap_array; //$this->_processBitmap($bitmap);
6903
6904         // Scale the frame of the image.
6905         $width  *= $scale_x;
6906         $height *= $scale_y;
6907
6908         // Calculate the vertices of the image and write the OBJ record
6909         $this->_positionImage($col, $row, $x, $y, $width, $height);
6910
6911         // Write the IMDATA record to store the bitmap data
6912         $record      = 0x007f;
6913         $length      = 8 + $size;
6914         $cf          = 0x09;
6915         $env         = 0x01;
6916         $lcb         = $size;
6917
6918         $header      = pack("vvvvV", $record, $length, $cf, $env, $lcb);
6919         $this->_append($header.$data);
6920     }
6921
6922     /**
6923     * Calculate the vertices that define the position of the image as required by
6924     * the OBJ record.
6925     *
6926     *         +------------+------------+
6927     *         |     A      |      B     |
6928     *   +-----+------------+------------+
6929     *   |     |(x1,y1)     |            |
6930     *   |  1  |(A1)._______|______      |
6931     *   |     |    |              |     |
6932     *   |     |    |              |     |
6933     *   +-----+----|    BITMAP    |-----+
6934     *   |     |    |              |     |
6935     *   |  2  |    |______________.     |
6936     *   |     |            |        (B2)|
6937     *   |     |            |     (x2,y2)|
6938     *   +---- +------------+------------+
6939     *
6940     * Example of a bitmap that covers some of the area from cell A1 to cell B2.
6941     *
6942     * Based on the width and height of the bitmap we need to calculate 8 vars:
6943     *     $col_start, $row_start, $col_end, $row_end, $x1, $y1, $x2, $y2.
6944     * The width and height of the cells are also variable and have to be taken into
6945     * account.
6946     * The values of $col_start and $row_start are passed in from the calling
6947     * function. The values of $col_end and $row_end are calculated by subtracting
6948     * the width and height of the bitmap from the width and height of the
6949     * underlying cells.
6950     * The vertices are expressed as a percentage of the underlying cell width as
6951     * follows (rhs values are in pixels):
6952     *
6953     *       x1 = X / W *1024
6954     *       y1 = Y / H *256
6955     *       x2 = (X-1) / W *1024
6956     *       y2 = (Y-1) / H *256
6957     *
6958     *       Where:  X is distance from the left side of the underlying cell
6959     *               Y is distance from the top of the underlying cell
6960     *               W is the width of the cell
6961     *               H is the height of the cell
6962     *
6963     * @access private
6964     * @note  the SDK incorrectly states that the height should be expressed as a
6965     *        percentage of 1024.
6966     * @param integer $col_start Col containing upper left corner of object
6967     * @param integer $row_start Row containing top left corner of object
6968     * @param integer $x1        Distance to left side of object
6969     * @param integer $y1        Distance to top of object
6970     * @param integer $width     Width of image frame
6971     * @param integer $height    Height of image frame
6972     */
6973     function _positionImage($col_start, $row_start, $x1, $y1, $width, $height)
6974     {
6975         // Initialise end cell to the same as the start cell
6976         $col_end    = $col_start;  // Col containing lower right corner of object
6977         $row_end    = $row_start;  // Row containing bottom right corner of object
6978
6979         // Zero the specified offset if greater than the cell dimensions
6980         if ($x1 >= $this->_sizeCol($col_start)) {
6981             $x1 = 0;
6982         }
6983         if ($y1 >= $this->_sizeRow($row_start)) {
6984             $y1 = 0;
6985         }
6986
6987         $width      = $width  + $x1 -1;
6988         $height     = $height + $y1 -1;
6989
6990         // Subtract the underlying cell widths to find the end cell of the image
6991         while ($width >= $this->_sizeCol($col_end)) {
6992             $width -= $this->_sizeCol($col_end);
6993             $col_end++;
6994         }
6995
6996         // Subtract the underlying cell heights to find the end cell of the image
6997         while ($height >= $this->_sizeRow($row_end)) {
6998             $height -= $this->_sizeRow($row_end);
6999             $row_end++;
7000         }
7001
7002         // Bitmap isn't allowed to start or finish in a hidden cell, i.e. a cell
7003         // with zero eight or width.
7004         //
7005         if ($this->_sizeCol($col_start) == 0) {
7006             return;
7007         }
7008         if ($this->_sizeCol($col_end)   == 0) {
7009             return;
7010         }
7011         if ($this->_sizeRow($row_start) == 0) {
7012             return;
7013         }
7014         if ($this->_sizeRow($row_end)   == 0) {
7015             return;
7016         }
7017
7018         // Convert the pixel values to the percentage value expected by Excel
7019         $x1 = $x1     / $this->_sizeCol($col_start)   * 1024;
7020         $y1 = $y1     / $this->_sizeRow($row_start)   *  256;
7021         $x2 = $width  / $this->_sizeCol($col_end)     * 1024; // Distance to right side of object
7022         $y2 = $height / $this->_sizeRow($row_end)     *  256; // Distance to bottom of object
7023
7024         $this->_storeObjPicture($col_start, $x1,
7025                                  $row_start, $y1,
7026                                  $col_end, $x2,
7027                                  $row_end, $y2);
7028     }
7029
7030     /**
7031     * Convert the width of a cell from user's units to pixels. By interpolation
7032     * the relationship is: y = 7x +5. If the width hasn't been set by the user we
7033     * use the default value. If the col is hidden we use a value of zero.
7034     *
7035     * @access private
7036     * @param integer $col The column
7037     * @return integer The width in pixels
7038     */
7039     function _sizeCol($col)
7040     {
7041         // Look up the cell value to see if it has been changed
7042         if (isset($this->col_sizes[$col])) {
7043             if ($this->col_sizes[$col] == 0) {
7044                 return(0);
7045             } else {
7046                 return(floor(7 * $this->col_sizes[$col] + 5));
7047             }
7048         } else {
7049             return(64);
7050         }
7051     }
7052
7053     /**
7054     * Convert the height of a cell from user's units to pixels. By interpolation
7055     * the relationship is: y = 4/3x. If the height hasn't been set by the user we
7056     * use the default value. If the row is hidden we use a value of zero. (Not
7057     * possible to hide row yet).
7058     *
7059     * @access private
7060     * @param integer $row The row
7061     * @return integer The width in pixels
7062     */
7063     function _sizeRow($row)
7064     {
7065         // Look up the cell value to see if it has been changed
7066         if (isset($this->_row_sizes[$row])) {
7067             if ($this->_row_sizes[$row] == 0) {
7068                 return(0);
7069             } else {
7070                 return(floor(4/3 * $this->_row_sizes[$row]));
7071             }
7072         } else {
7073             return(17);
7074         }
7075     }
7076
7077     /**
7078     * Store the OBJ record that precedes an IMDATA record. This could be generalise
7079     * to support other Excel objects.
7080     *
7081     * @access private
7082     * @param integer $colL Column containing upper left corner of object
7083     * @param integer $dxL  Distance from left side of cell
7084     * @param integer $rwT  Row containing top left corner of object
7085     * @param integer $dyT  Distance from top of cell
7086     * @param integer $colR Column containing lower right corner of object
7087     * @param integer $dxR  Distance from right of cell
7088     * @param integer $rwB  Row containing bottom right corner of object
7089     * @param integer $dyB  Distance from bottom of cell
7090     */
7091     function _storeObjPicture($colL,$dxL,$rwT,$dyT,$colR,$dxR,$rwB,$dyB)
7092     {
7093         $record      = 0x005d;   // Record identifier
7094         $length      = 0x003c;   // Bytes to follow
7095
7096         $cObj        = 0x0001;   // Count of objects in file (set to 1)
7097         $OT          = 0x0008;   // Object type. 8 = Picture
7098         $id          = 0x0001;   // Object ID
7099         $grbit       = 0x0614;   // Option flags
7100
7101         $cbMacro     = 0x0000;   // Length of FMLA structure
7102         $Reserved1   = 0x0000;   // Reserved
7103         $Reserved2   = 0x0000;   // Reserved
7104
7105         $icvBack     = 0x09;     // Background colour
7106         $icvFore     = 0x09;     // Foreground colour
7107         $fls         = 0x00;     // Fill pattern
7108         $fAuto       = 0x00;     // Automatic fill
7109         $icv         = 0x08;     // Line colour
7110         $lns         = 0xff;     // Line style
7111         $lnw         = 0x01;     // Line weight
7112         $fAutoB      = 0x00;     // Automatic border
7113         $frs         = 0x0000;   // Frame style
7114         $cf          = 0x0009;   // Image format, 9 = bitmap
7115         $Reserved3   = 0x0000;   // Reserved
7116         $cbPictFmla  = 0x0000;   // Length of FMLA structure
7117         $Reserved4   = 0x0000;   // Reserved
7118         $grbit2      = 0x0001;   // Option flags
7119         $Reserved5   = 0x0000;   // Reserved
7120
7121
7122         $header      = pack("vv", $record, $length);
7123         $data        = pack("V", $cObj);
7124         $data       .= pack("v", $OT);
7125         $data       .= pack("v", $id);
7126         $data       .= pack("v", $grbit);
7127         $data       .= pack("v", $colL);
7128         $data       .= pack("v", $dxL);
7129         $data       .= pack("v", $rwT);
7130         $data       .= pack("v", $dyT);
7131         $data       .= pack("v", $colR);
7132         $data       .= pack("v", $dxR);
7133         $data       .= pack("v", $rwB);
7134         $data       .= pack("v", $dyB);
7135         $data       .= pack("v", $cbMacro);
7136         $data       .= pack("V", $Reserved1);
7137         $data       .= pack("v", $Reserved2);
7138         $data       .= pack("C", $icvBack);
7139         $data       .= pack("C", $icvFore);
7140         $data       .= pack("C", $fls);
7141         $data       .= pack("C", $fAuto);
7142         $data       .= pack("C", $icv);
7143         $data       .= pack("C", $lns);
7144         $data       .= pack("C", $lnw);
7145         $data       .= pack("C", $fAutoB);
7146         $data       .= pack("v", $frs);
7147         $data       .= pack("V", $cf);
7148         $data       .= pack("v", $Reserved3);
7149         $data       .= pack("v", $cbPictFmla);
7150         $data       .= pack("v", $Reserved4);
7151         $data       .= pack("v", $grbit2);
7152         $data       .= pack("V", $Reserved5);
7153
7154         $this->_append($header . $data);
7155     }
7156
7157     /**
7158     * Convert a 24 bit bitmap into the modified internal format used by Windows.
7159     * This is described in BITMAPCOREHEADER and BITMAPCOREINFO structures in the
7160     * MSDN library.
7161     *
7162     * @access private
7163     * @param string $bitmap The bitmap to process
7164     * @return array Array with data and properties of the bitmap
7165     */
7166     function _processBitmap($bitmap)
7167     {
7168         // Open file.
7169         $bmp_fd = @fopen($bitmap,"rb");
7170         if (!$bmp_fd) {
7171             die("Couldn't import $bitmap");
7172         }
7173
7174         // Slurp the file into a string.
7175         $data = fread($bmp_fd, filesize($bitmap));
7176
7177         // Check that the file is big enough to be a bitmap.
7178         if (strlen($data) <= 0x36) {
7179             die("$bitmap doesn't contain enough data.\n");
7180         }
7181
7182         // The first 2 bytes are used to identify the bitmap.
7183         $identity = unpack("A2ident", $data);
7184         if ($identity['ident'] != "BM") {
7185             die("$bitmap doesn't appear to be a valid bitmap image.\n");
7186         }
7187
7188         // Remove bitmap data: ID.
7189         $data = substr($data, 2);
7190
7191         // Read and remove the bitmap size. This is more reliable than reading
7192         // the data size at offset 0x22.
7193         //
7194         $size_array   = unpack("Vsa", substr($data, 0, 4));
7195         $size   = $size_array['sa'];
7196         $data   = substr($data, 4);
7197         $size  -= 0x36; // Subtract size of bitmap header.
7198         $size  += 0x0C; // Add size of BIFF header.
7199
7200         // Remove bitmap data: reserved, offset, header length.
7201         $data = substr($data, 12);
7202
7203         // Read and remove the bitmap width and height. Verify the sizes.
7204         $width_and_height = unpack("V2", substr($data, 0, 8));
7205         $width  = $width_and_height[1];
7206         $height = $width_and_height[2];
7207         $data   = substr($data, 8);
7208         if ($width > 0xFFFF) {
7209             die("$bitmap: largest image width supported is 65k.\n");
7210         }
7211         if ($height > 0xFFFF) {
7212             die("$bitmap: largest image height supported is 65k.\n");
7213         }
7214
7215         // Read and remove the bitmap planes and bpp data. Verify them.
7216         $planes_and_bitcount = unpack("v2", substr($data, 0, 4));
7217         $data = substr($data, 4);
7218         if ($planes_and_bitcount[2] != 24) { // Bitcount
7219             die("$bitmap isn't a 24bit true color bitmap.\n");
7220         }
7221         if ($planes_and_bitcount[1] != 1) {
7222             die("$bitmap: only 1 plane supported in bitmap image.\n");
7223         }
7224
7225         // Read and remove the bitmap compression. Verify compression.
7226         $compression = unpack("Vcomp", substr($data, 0, 4));
7227         $data = substr($data, 4);
7228
7229         //$compression = 0;
7230         if ($compression['comp'] != 0) {
7231             die("$bitmap: compression not supported in bitmap image.\n");
7232         }
7233
7234         // Remove bitmap data: data size, hres, vres, colours, imp. colours.
7235         $data = substr($data, 20);
7236
7237         // Add the BITMAPCOREHEADER data
7238         $header  = pack("Vvvvv", 0x000c, $width, $height, 0x01, 0x18);
7239         $data    = $header . $data;
7240
7241         return (array($width, $height, $size, $data));
7242     }
7243
7244     /**
7245     * Store the window zoom factor. This should be a reduced fraction but for
7246     * simplicity we will store all fractions with a numerator of 100.
7247     *
7248     * @access private
7249     */
7250     function _storeZoom()
7251     {
7252         // If scale is 100 we don't need to write a record
7253         if ($this->_zoom == 100) {
7254             return;
7255         }
7256
7257         $record      = 0x00A0;               // Record identifier
7258         $length      = 0x0004;               // Bytes to follow
7259
7260         $header      = pack("vv", $record, $length);
7261         $data        = pack("vv", $this->_zoom, 100);
7262         $this->_append($header . $data);
7263     }
7264
7265     /**
7266     * FIXME: add comments
7267     */
7268     function setValidation($row1, $col1, $row2, $col2, &$validator)
7269     {
7270         $this->_dv[] = $validator->_getData() .
7271                        pack("vvvvv", 1, $row1, $row2, $col1, $col2);
7272     }
7273
7274     /**
7275     * Store the DVAL and DV records.
7276     *
7277     * @access private
7278     */
7279     function _storeDataValidity()
7280     {
7281         $record      = 0x01b2;      // Record identifier
7282         $length      = 0x0012;      // Bytes to follow
7283
7284         $grbit       = 0x0002;      // Prompt box at cell, no cached validity data at DV records
7285         $horPos      = 0x00000000;  // Horizontal position of prompt box, if fixed position
7286         $verPos      = 0x00000000;  // Vertical position of prompt box, if fixed position
7287         $objId       = 0xffffffff;  // Object identifier of drop down arrow object, or -1 if not visible
7288
7289         $header      = pack('vv', $record, $length);
7290         $data        = pack('vVVVV', $grbit, $horPos, $verPos, $objId,
7291                                      count($this->_dv));
7292         $this->_append($header.$data);
7293
7294         $record = 0x01be;              // Record identifier
7295         foreach ($this->_dv as $dv) {
7296             $length = strlen($dv);      // Bytes to follow
7297             $header = pack("vv", $record, $length);
7298             $this->_append($header . $dv);
7299         }
7300     }
7301 }
7302
7303 /**
7304 * Class for generating Excel Spreadsheets
7305 *
7306 * @author   Xavier Noguer <xnoguer@rezebra.com>
7307 * @category FileFormats
7308 * @package  Spreadsheet_Excel_Writer
7309 */
7310
7311 class Spreadsheet_Excel_Writer_Workbook extends Spreadsheet_Excel_Writer_BIFFwriter
7312 {
7313     /**
7314     * Filename for the Workbook
7315     * @var string
7316     */
7317     var $_filename;
7318
7319     /**
7320     * Formula parser
7321     * @var object Parser
7322     */
7323     var $_parser;
7324
7325     /**
7326     * Flag for 1904 date system (0 => base date is 1900, 1 => base date is 1904)
7327     * @var integer
7328     */
7329     var $_1904;
7330
7331     /**
7332     * The active worksheet of the workbook (0 indexed)
7333     * @var integer
7334     */
7335     var $_activesheet;
7336
7337     /**
7338     * 1st displayed worksheet in the workbook (0 indexed)
7339     * @var integer
7340     */
7341     var $_firstsheet;
7342
7343     /**
7344     * Number of workbook tabs selected
7345     * @var integer
7346     */
7347     var $_selected;
7348
7349     /**
7350     * Index for creating adding new formats to the workbook
7351     * @var integer
7352     */
7353     var $_xf_index;
7354
7355     /**
7356     * Flag for preventing close from being called twice.
7357     * @var integer
7358     * @see close()
7359     */
7360     var $_fileclosed;
7361
7362     /**
7363     * The BIFF file size for the workbook.
7364     * @var integer
7365     * @see _calcSheetOffsets()
7366     */
7367     var $_biffsize;
7368
7369     /**
7370     * The default sheetname for all sheets created.
7371     * @var string
7372     */
7373     var $_sheetname;
7374
7375     /**
7376     * The default XF format.
7377     * @var object Format
7378     */
7379     var $_tmp_format;
7380
7381     /**
7382     * Array containing references to all of this workbook's worksheets
7383     * @var array
7384     */
7385     var $_worksheets;
7386
7387     /**
7388     * Array of sheetnames for creating the EXTERNSHEET records
7389     * @var array
7390     */
7391     var $_sheetnames;
7392
7393     /**
7394     * Array containing references to all of this workbook's formats
7395     * @var array
7396     */
7397     var $_formats;
7398
7399     /**
7400     * Array containing the colour palette
7401     * @var array
7402     */
7403     var $_palette;
7404
7405     /**
7406     * The default format for URLs.
7407     * @var object Format
7408     */
7409     var $_url_format;
7410
7411     /**
7412     * The codepage indicates the text encoding used for strings
7413     * @var integer
7414     */
7415     var $_codepage;
7416
7417     /**
7418     * The country code used for localization
7419     * @var integer
7420     */
7421     var $_country_code;
7422
7423     /**
7424     * The temporary dir for storing the OLE file
7425     * @var string
7426     */
7427     var $_tmp_dir;
7428
7429     /**
7430     * number of bytes for sizeinfo of strings
7431     * @var integer
7432     */
7433     var $_string_sizeinfo_size;
7434
7435     // Dynamic members must be here (php8.2)
7436     var $_string_sizeinfo;
7437     var $_str_total;
7438     var $_str_unique;
7439     var $_str_table;
7440     //var $_offset;
7441     /**
7442     * Class constructor
7443     *
7444     * @param string filename for storing the workbook. "-" for writing to stdout.
7445     * @access public
7446     */
7447     function __construct($filename)
7448     {
7449         // It needs to call its parent's constructor explicitly
7450         parent::__construct();
7451
7452         $this->_filename         = $filename;
7453         $this->_parser           = new Spreadsheet_Excel_Writer_Parser($this->_byte_order, $this->_BIFF_version);
7454         $this->_1904             = 0;
7455         $this->_activesheet      = 0;
7456         $this->_firstsheet       = 0;
7457         $this->_selected         = 0;
7458         $this->_xf_index         = 16; // 15 style XF's and 1 cell XF.
7459         $this->_fileclosed       = 0;
7460         $this->_biffsize         = 0;
7461         $this->_sheetname        = 'Sheet';
7462         $this->_tmp_format       = new Spreadsheet_Excel_Writer_Format($this->_BIFF_version);
7463         $this->_worksheets       = array();
7464         $this->_sheetnames       = array();
7465         $this->_formats          = array();
7466         $this->_palette          = array();
7467         $this->_codepage         = 0x04E4; // FIXME: should change for BIFF8
7468         $this->_country_code     = -1;
7469         $this->_string_sizeinfo  = 3;
7470
7471         // Add the default format for hyperlinks
7472         $this->_url_format =& $this->addFormat(array('color' => 'blue', 'underline' => 1));
7473         $this->_str_total       = 0;
7474         $this->_str_unique      = 0;
7475         $this->_str_table       = array();
7476         $this->_setPaletteXl97();
7477         $this->_tmp_dir         = '';
7478     }
7479
7480     /**
7481     * Calls finalization methods.
7482     * This method should always be the last one to be called on every workbook
7483     *
7484     * @access public
7485     * @return mixed true on success. PEAR_Error on failure
7486     */
7487     function close()
7488     {
7489         if ($this->_fileclosed) { // Prevent close() from being called twice.
7490             return true;
7491         }
7492         $this->_storeWorkbook();
7493         $this->_fileclosed = 1;
7494         return true;
7495     }
7496
7497     /**
7498     * An accessor for the _worksheets[] array
7499     * Returns an array of the worksheet objects in a workbook
7500     * It actually calls to worksheets()
7501     *
7502     * @access public
7503     * @see worksheets()
7504     * @return array
7505     */
7506     function sheets()
7507     {
7508         return $this->worksheets();
7509     }
7510
7511     /**
7512     * An accessor for the _worksheets[] array.
7513     * Returns an array of the worksheet objects in a workbook
7514     *
7515     * @access public
7516     * @return array
7517     */
7518     function worksheets()
7519     {
7520         return $this->_worksheets;
7521     }
7522
7523     /**
7524     * Sets the BIFF version.
7525     * This method exists just to access experimental functionality
7526     * from BIFF8. It will be deprecated !
7527     * Only possible value is 8 (Excel 97/2000).
7528     * For any other value it fails silently.
7529     *
7530     * @access public
7531     * @param integer $version The BIFF version
7532     */
7533     function setVersion($version)
7534     {
7535         if ($version == 8) { // only accept version 8
7536             $version = 0x0600;
7537             $this->_BIFF_version = $version;
7538             // change BIFFwriter limit for CONTINUE records
7539             $this->_limit = 8228;
7540             $this->_tmp_format->_BIFF_version = $version;
7541             $this->_url_format->_BIFF_version = $version;
7542             $this->_parser->_BIFF_version = $version;
7543
7544             $total_worksheets = count($this->_worksheets);
7545             // change version for all worksheets too
7546             for ($i = 0; $i < $total_worksheets; $i++) {
7547                 $this->_worksheets[$i]->_BIFF_version = $version;
7548             }
7549
7550             $total_formats = count($this->_formats);
7551             // change version for all formats too
7552             for ($i = 0; $i < $total_formats; $i++) {
7553                 $this->_formats[$i]->_BIFF_version = $version;
7554             }
7555         }
7556     }
7557
7558     /**
7559     * Set the country identifier for the workbook
7560     *
7561     * @access public
7562     * @param integer $code Is the international calling country code for the
7563     *                      chosen country.
7564     */
7565     function setCountry($code)
7566     {
7567         $this->_country_code = $code;
7568     }
7569
7570     /**
7571     * Add a new worksheet to the Excel workbook.
7572     * If no name is given the name of the worksheet will be Sheeti$i, with
7573     * $i in [1..].
7574     *
7575     * @access public
7576     * @param string $name the optional name of the worksheet
7577     * @return mixed reference to a worksheet object on success, PEAR_Error
7578     *               on failure
7579     */
7580     function &addWorksheet($name = '')
7581     {
7582         $index     = count($this->_worksheets);
7583         $sheetname = $this->_sheetname;
7584
7585         if ($name == '') {
7586             $name = $sheetname.($index+1);
7587         }
7588
7589         // Check that sheetname is <= 31 chars (Excel limit before BIFF8).
7590         if ($this->_BIFF_version != 0x0600)
7591         {
7592             if (strlen($name) > 31) {
7593                 die("Sheetname $name must be <= 31 chars");
7594             }
7595         }
7596
7597         // Check that the worksheet name doesn't already exist: a fatal Excel error.
7598         $total_worksheets = count($this->_worksheets);
7599         for ($i = 0; $i < $total_worksheets; $i++) {
7600             if ($this->_worksheets[$i]->getName() == $name) {
7601                 die("Worksheet '$name' already exists");
7602             }
7603         }
7604
7605         $worksheet = new Spreadsheet_Excel_Writer_Worksheet($this->_BIFF_version,
7606                                    $name, $index,
7607                                    $this->_activesheet, $this->_firstsheet,
7608                                    $this->_str_total, $this->_str_unique,
7609                                    $this->_str_table, $this->_url_format,
7610                                    $this->_parser);
7611
7612         $this->_worksheets[$index] = &$worksheet;    // Store ref for iterator
7613         $this->_sheetnames[$index] = $name;          // Store EXTERNSHEET names
7614         $this->_parser->setExtSheet($name, $index);  // Register worksheet name with parser
7615         return $worksheet;
7616     }
7617
7618     /**
7619     * Add a new format to the Excel workbook.
7620     * Also, pass any properties to the Format constructor.
7621     *
7622     * @access public
7623     * @param array $properties array with properties for initializing the format.
7624     * @return &Spreadsheet_Excel_Writer_Format reference to an Excel Format
7625     */
7626     function &addFormat($properties = array())
7627     {
7628         $format = new Spreadsheet_Excel_Writer_Format($this->_BIFF_version, $this->_xf_index, $properties);
7629         $this->_xf_index += 1;
7630         $this->_formats[] = &$format;
7631         return $format;
7632     }
7633
7634     /**
7635      * Create new validator.
7636      *
7637      * @access public
7638      * @return &Spreadsheet_Excel_Writer_Validator reference to a Validator
7639      */
7640     function &addValidator()
7641     {
7642         include_once 'Spreadsheet/Excel/Writer/Validator.php';
7643         /* FIXME: check for successful inclusion*/
7644         $valid = new Spreadsheet_Excel_Writer_Validator($this->_parser);
7645         return $valid;
7646     }
7647
7648     /**
7649     * Change the RGB components of the elements in the colour palette.
7650     *
7651     * @access public
7652     * @param integer $index colour index
7653     * @param integer $red   red RGB value [0-255]
7654     * @param integer $green green RGB value [0-255]
7655     * @param integer $blue  blue RGB value [0-255]
7656     * @return integer The palette index for the custom color
7657     */
7658     function setCustomColor($index, $red, $green, $blue)
7659     {
7660         // Match a HTML #xxyyzz style parameter
7661         /*if (defined $_[1] and $_[1] =~ /^#(\w\w)(\w\w)(\w\w)/ ) {
7662             @_ = ($_[0], hex $1, hex $2, hex $3);
7663         }*/
7664
7665         // Check that the colour index is the right range
7666         if ($index < 8 or $index > 64) {
7667             // TODO: assign real error codes
7668             die("Color index $index outside range: 8 <= index <= 64");
7669         }
7670
7671         // Check that the colour components are in the right range
7672         if (($red   < 0 or $red   > 255) ||
7673             ($green < 0 or $green > 255) ||
7674             ($blue  < 0 or $blue  > 255))
7675         {
7676             die("Color component outside range: 0 <= color <= 255");
7677         }
7678
7679         $index -= 8; // Adjust colour index (wingless dragonfly)
7680
7681         // Set the RGB value
7682         $this->_palette[$index] = array($red, $green, $blue, 0);
7683         return($index + 8);
7684     }
7685
7686     /**
7687     * Sets the colour palette to the Excel 97+ default.
7688     *
7689     * @access private
7690     */
7691     function _setPaletteXl97()
7692     {
7693         $this->_palette = array(
7694                            array(0x00, 0x00, 0x00, 0x00),   // 8
7695                            array(0xff, 0xff, 0xff, 0x00),   // 9
7696                            array(0xff, 0x00, 0x00, 0x00),   // 10
7697                            array(0x00, 0xff, 0x00, 0x00),   // 11
7698                            array(0x00, 0x00, 0xff, 0x00),   // 12
7699                            array(0xff, 0xff, 0x00, 0x00),   // 13
7700                            array(0xff, 0x00, 0xff, 0x00),   // 14
7701                            array(0x00, 0xff, 0xff, 0x00),   // 15
7702                            array(0x80, 0x00, 0x00, 0x00),   // 16
7703                            array(0x00, 0x80, 0x00, 0x00),   // 17
7704                            array(0x00, 0x00, 0x80, 0x00),   // 18
7705                            array(0x80, 0x80, 0x00, 0x00),   // 19
7706                            array(0x80, 0x00, 0x80, 0x00),   // 20
7707                            array(0x00, 0x80, 0x80, 0x00),   // 21
7708                            array(0xc0, 0xc0, 0xc0, 0x00),   // 22
7709                            array(0x80, 0x80, 0x80, 0x00),   // 23
7710                            array(0x99, 0x99, 0xff, 0x00),   // 24
7711                            array(0x99, 0x33, 0x66, 0x00),   // 25
7712                            array(0xff, 0xff, 0xcc, 0x00),   // 26
7713                            array(0xcc, 0xff, 0xff, 0x00),   // 27
7714                            array(0x66, 0x00, 0x66, 0x00),   // 28
7715                            array(0xff, 0x80, 0x80, 0x00),   // 29
7716                            array(0x00, 0x66, 0xcc, 0x00),   // 30
7717                            array(0xcc, 0xcc, 0xff, 0x00),   // 31
7718                            array(0x00, 0x00, 0x80, 0x00),   // 32
7719                            array(0xff, 0x00, 0xff, 0x00),   // 33
7720                            array(0xff, 0xff, 0x00, 0x00),   // 34
7721                            array(0x00, 0xff, 0xff, 0x00),   // 35
7722                            array(0x80, 0x00, 0x80, 0x00),   // 36
7723                            array(0x80, 0x00, 0x00, 0x00),   // 37
7724                            array(0x00, 0x80, 0x80, 0x00),   // 38
7725                            array(0x00, 0x00, 0xff, 0x00),   // 39
7726                            array(0x00, 0xcc, 0xff, 0x00),   // 40
7727                            array(0xcc, 0xff, 0xff, 0x00),   // 41
7728                            array(0xcc, 0xff, 0xcc, 0x00),   // 42
7729                            array(0xff, 0xff, 0x99, 0x00),   // 43
7730                            array(0x99, 0xcc, 0xff, 0x00),   // 44
7731                            array(0xff, 0x99, 0xcc, 0x00),   // 45
7732                            array(0xcc, 0x99, 0xff, 0x00),   // 46
7733                            array(0xff, 0xcc, 0x99, 0x00),   // 47
7734                            array(0x33, 0x66, 0xff, 0x00),   // 48
7735                            array(0x33, 0xcc, 0xcc, 0x00),   // 49
7736                            array(0x99, 0xcc, 0x00, 0x00),   // 50
7737                            array(0xff, 0xcc, 0x00, 0x00),   // 51
7738                            array(0xff, 0x99, 0x00, 0x00),   // 52
7739                            array(0xff, 0x66, 0x00, 0x00),   // 53
7740                            array(0x66, 0x66, 0x99, 0x00),   // 54
7741                            array(0x96, 0x96, 0x96, 0x00),   // 55
7742                            array(0x00, 0x33, 0x66, 0x00),   // 56
7743                            array(0x33, 0x99, 0x66, 0x00),   // 57
7744                            array(0x00, 0x33, 0x00, 0x00),   // 58
7745                            array(0x33, 0x33, 0x00, 0x00),   // 59
7746                            array(0x99, 0x33, 0x00, 0x00),   // 60
7747                            array(0x99, 0x33, 0x66, 0x00),   // 61
7748                            array(0x33, 0x33, 0x99, 0x00),   // 62
7749                            array(0x33, 0x33, 0x33, 0x00),   // 63
7750                          );
7751     }
7752
7753     /**
7754     * Assemble worksheets into a workbook and send the BIFF data to an OLE
7755     * storage.
7756     *
7757     * @access private
7758     * @return mixed true on success. PEAR_Error on failure
7759     */
7760     function _storeWorkbook()
7761     {
7762         // Ensure that at least one worksheet has been selected.
7763         if ($this->_activesheet == 0) {
7764             $this->_worksheets[0]->selected = 1;
7765         }
7766
7767         // Calculate the number of selected worksheet tabs and call the finalization
7768         // methods for each worksheet
7769         $total_worksheets = count($this->_worksheets);
7770         for ($i = 0; $i < $total_worksheets; $i++) {
7771             if ($this->_worksheets[$i]->selected) {
7772                 $this->_selected++;
7773             }
7774             $this->_worksheets[$i]->close($this->_sheetnames);
7775         }
7776
7777         // Add Workbook globals
7778         $this->_storeBof(0x0005);
7779         $this->_storeCodepage();
7780         if ($this->_BIFF_version == 0x0600) {
7781             $this->_storeWindow1();
7782         }
7783         if ($this->_BIFF_version == 0x0500) {
7784             $this->_storeExterns();    // For print area and repeat rows
7785         }
7786         $this->_storeNames();      // For print area and repeat rows
7787         if ($this->_BIFF_version == 0x0500) {
7788             $this->_storeWindow1();
7789         }
7790         $this->_storeDatemode();
7791         $this->_storeAllFonts();
7792         $this->_storeAllNumFormats();
7793         $this->_storeAllXfs();
7794         $this->_storeAllStyles();
7795         $this->_storePalette();
7796         $this->_calcSheetOffsets();
7797
7798         // Add BOUNDSHEET records
7799         for ($i = 0; $i < $total_worksheets; $i++) {
7800             $this->_storeBoundsheet($this->_worksheets[$i]->name,$this->_worksheets[$i]->_offset);
7801         }
7802
7803         if ($this->_country_code != -1) {
7804             $this->_storeCountry();
7805         }
7806
7807         if ($this->_BIFF_version == 0x0600) {
7808             //$this->_storeSupbookInternal();
7809             /* TODO: store external SUPBOOK records and XCT and CRN records
7810             in case of external references for BIFF8 */
7811             //$this->_storeExternsheetBiff8();
7812             $this->_storeSharedStringsTable();
7813         }
7814
7815         // End Workbook globals
7816         $this->_storeEof();
7817
7818         // Store the workbook in an OLE container
7819         $res = $this->_storeOLEFile();
7820         return true;
7821     }
7822
7823     /**
7824     * Sets the temp dir used for storing the OLE file
7825     *
7826     * @access public
7827     * @param string $dir The dir to be used as temp dir
7828     * @return true if given dir is valid, false otherwise
7829     */
7830     function setTempDir($dir)
7831     {
7832         if (is_dir($dir)) {
7833             $this->_tmp_dir = $dir;
7834             return true;
7835         }
7836         return false;
7837     }
7838
7839     /**
7840     * Store the workbook in an OLE container
7841     *
7842     * @access private
7843     * @return mixed true on success. PEAR_Error on failure
7844     */
7845     function _storeOLEFile()
7846     {
7847         if($this->_BIFF_version == 0x0600) {
7848             $OLE = new ole_pps_file(Asc2Ucs('Workbook'));
7849         } else {
7850             $OLE = new ole_pps_file(Asc2Ucs('Book'));
7851         }
7852
7853         $OLE->append($this->_data);
7854
7855         $total_worksheets = count($this->_worksheets);
7856         for ($i = 0; $i < $total_worksheets; $i++) {
7857             while ($tmp = $this->_worksheets[$i]->getData()) {
7858                 $OLE->append($tmp);
7859             }
7860         }
7861
7862         $root = new ole_pps_root(false, false, array($OLE));
7863
7864         $root->save($this->_filename);
7865     }
7866
7867     /**
7868     * Calculate offsets for Worksheet BOF records.
7869     *
7870     * @access private
7871     */
7872     function _calcSheetOffsets()
7873     {
7874         if ($this->_BIFF_version == 0x0600) {
7875             $boundsheet_length = 12;  // fixed length for a BOUNDSHEET record
7876         } else {
7877             $boundsheet_length = 11;
7878         }
7879         $EOF               = 4;
7880         $offset            = $this->_datasize;
7881
7882         if ($this->_BIFF_version == 0x0600) {
7883             // add the length of the SST
7884             /* TODO: check this works for a lot of strings (> 8224 bytes) */
7885             $offset += $this->_calculateSharedStringsSizes();
7886             if ($this->_country_code != -1) {
7887                 $offset += 8; // adding COUNTRY record
7888             }
7889             // add the lenght of SUPBOOK, EXTERNSHEET and NAME records
7890             //$offset += 8; // FIXME: calculate real value when storing the records
7891         }
7892         $total_worksheets = count($this->_worksheets);
7893         // add the length of the BOUNDSHEET records
7894         for ($i = 0; $i < $total_worksheets; $i++) {
7895             $offset += $boundsheet_length + strlen($this->_worksheets[$i]->name);
7896         }
7897         $offset += $EOF;
7898
7899         for ($i = 0; $i < $total_worksheets; $i++) {
7900             $this->_worksheets[$i]->_offset = $offset;
7901             $offset += $this->_worksheets[$i]->_datasize;
7902         }
7903         $this->_biffsize = $offset;
7904     }
7905
7906     /**
7907     * Store the Excel FONT records.
7908     *
7909     * @access private
7910     */
7911     function _storeAllFonts()
7912     {
7913         // tmp_format is added by the constructor. We use this to write the default XF's
7914         $format = $this->_tmp_format;
7915         $font   = $format->getFont();
7916
7917         // Note: Fonts are 0-indexed. According to the SDK there is no index 4,
7918         // so the following fonts are 0, 1, 2, 3, 5
7919         //
7920         for ($i = 1; $i <= 5; $i++){
7921             $this->_append($font);
7922         }
7923
7924         // Iterate through the XF objects and write a FONT record if it isn't the
7925         // same as the default FONT and if it hasn't already been used.
7926         //
7927         $fonts = array();
7928         $index = 6;                  // The first user defined FONT
7929
7930         $key = $format->getFontKey(); // The default font from _tmp_format
7931         $fonts[$key] = 0;             // Index of the default font
7932
7933         $total_formats = count($this->_formats);
7934         for ($i = 0; $i < $total_formats; $i++) {
7935             $key = $this->_formats[$i]->getFontKey();
7936             if (isset($fonts[$key])) {
7937                 // FONT has already been used
7938                 $this->_formats[$i]->font_index = $fonts[$key];
7939             } else {
7940                 // Add a new FONT record
7941                 $fonts[$key]        = $index;
7942                 $this->_formats[$i]->font_index = $index;
7943                 $index++;
7944                 $font = $this->_formats[$i]->getFont();
7945                 $this->_append($font);
7946             }
7947         }
7948     }
7949
7950     /**
7951     * Store user defined numerical formats i.e. FORMAT records
7952     *
7953     * @access private
7954     */
7955     function _storeAllNumFormats()
7956     {
7957         // Leaning num_format syndrome
7958         $hash_num_formats = array();
7959         $num_formats      = array();
7960         $index = 164;
7961
7962         // Iterate through the XF objects and write a FORMAT record if it isn't a
7963         // built-in format type and if the FORMAT string hasn't already been used.
7964         $total_formats = count($this->_formats);
7965         for ($i = 0; $i < $total_formats; $i++) {
7966             $num_format = $this->_formats[$i]->_num_format;
7967
7968             // Check if $num_format is an index to a built-in format.
7969             // Also check for a string of zeros, which is a valid format string
7970             // but would evaluate to zero.
7971             //
7972             if (!preg_match("/^0+\d/", $num_format)) {
7973                 if (preg_match("/^\d+$/", $num_format)) { // built-in format
7974                     continue;
7975                 }
7976             }
7977
7978             if (isset($hash_num_formats[$num_format])) {
7979                 // FORMAT has already been used
7980                 $this->_formats[$i]->_num_format = $hash_num_formats[$num_format];
7981             } else{
7982                 // Add a new FORMAT
7983                 $hash_num_formats[$num_format]  = $index;
7984                 $this->_formats[$i]->_num_format = $index;
7985                 array_push($num_formats,$num_format);
7986                 $index++;
7987             }
7988         }
7989
7990         // Write the new FORMAT records starting from 0xA4
7991         $index = 164;
7992         foreach ($num_formats as $num_format) {
7993             $this->_storeNumFormat($num_format,$index);
7994             $index++;
7995         }
7996     }
7997
7998     /**
7999     * Write all XF records.
8000     *
8001     * @access private
8002     */
8003     function _storeAllXfs()
8004     {
8005         // _tmp_format is added by the constructor. We use this to write the default XF's
8006         // The default font index is 0
8007         //
8008         $format = $this->_tmp_format;
8009         for ($i = 0; $i <= 14; $i++) {
8010             $xf = $format->getXf('style'); // Style XF
8011             $this->_append($xf);
8012         }
8013
8014         $xf = $format->getXf('cell');      // Cell XF
8015         $this->_append($xf);
8016
8017         // User defined XFs
8018         $total_formats = count($this->_formats);
8019         for ($i = 0; $i < $total_formats; $i++) {
8020             $xf = $this->_formats[$i]->getXf('cell');
8021             $this->_append($xf);
8022         }
8023     }
8024
8025     /**
8026     * Write all STYLE records.
8027     *
8028     * @access private
8029     */
8030     function _storeAllStyles()
8031     {
8032         $this->_storeStyle();
8033     }
8034
8035     /**
8036     * Write the EXTERNCOUNT and EXTERNSHEET records. These are used as indexes for
8037     * the NAME records.
8038     *
8039     * @access private
8040     */
8041     function _storeExterns()
8042     {
8043         // Create EXTERNCOUNT with number of worksheets
8044         $this->_storeExterncount(count($this->_worksheets));
8045
8046         // Create EXTERNSHEET for each worksheet
8047         foreach ($this->_sheetnames as $sheetname) {
8048             $this->_storeExternsheet($sheetname);
8049         }
8050     }
8051
8052     /**
8053     * Write the NAME record to define the print area and the repeat rows and cols.
8054     *
8055     * @access private
8056     */
8057     function _storeNames()
8058     {
8059         // Create the print area NAME records
8060         $total_worksheets = count($this->_worksheets);
8061         for ($i = 0; $i < $total_worksheets; $i++) {
8062             // Write a Name record if the print area has been defined
8063             if (isset($this->_worksheets[$i]->print_rowmin)) {
8064                 $this->_storeNameShort(
8065                     $this->_worksheets[$i]->index,
8066                     0x06, // NAME type
8067                     $this->_worksheets[$i]->print_rowmin,
8068                     $this->_worksheets[$i]->print_rowmax,
8069                     $this->_worksheets[$i]->print_colmin,
8070                     $this->_worksheets[$i]->print_colmax
8071                     );
8072             }
8073         }
8074
8075         // Create the print title NAME records
8076         $total_worksheets = count($this->_worksheets);
8077         for ($i = 0; $i < $total_worksheets; $i++) {
8078             $rowmin = $this->_worksheets[$i]->title_rowmin;
8079             $rowmax = $this->_worksheets[$i]->title_rowmax;
8080             $colmin = $this->_worksheets[$i]->title_colmin;
8081             $colmax = $this->_worksheets[$i]->title_colmax;
8082
8083             // Determine if row + col, row, col or nothing has been defined
8084             // and write the appropriate record
8085             //
8086             if (isset($rowmin) && isset($colmin)) {
8087                 // Row and column titles have been defined.
8088                 // Row title has been defined.
8089                 $this->_storeNameLong(
8090                     $this->_worksheets[$i]->index,
8091                     0x07, // NAME type
8092                     $rowmin,
8093                     $rowmax,
8094                     $colmin,
8095                     $colmax
8096                     );
8097             } elseif (isset($rowmin)) {
8098                 // Row title has been defined.
8099                 $this->_storeNameShort(
8100                     $this->_worksheets[$i]->index,
8101                     0x07, // NAME type
8102                     $rowmin,
8103                     $rowmax,
8104                     0x00,
8105                     0xff
8106                     );
8107             } elseif (isset($colmin)) {
8108                 // Column title has been defined.
8109                 $this->_storeNameShort(
8110                     $this->_worksheets[$i]->index,
8111                     0x07, // NAME type
8112                     0x0000,
8113                     0x3fff,
8114                     $colmin,
8115                     $colmax
8116                     );
8117             } else {
8118                 // Print title hasn't been defined.
8119             }
8120         }
8121     }
8122
8123
8124
8125
8126     /******************************************************************************
8127     *
8128     * BIFF RECORDS
8129     *
8130     */
8131
8132     /**
8133     * Stores the CODEPAGE biff record.
8134     *
8135     * @access private
8136     */
8137     function _storeCodepage()
8138     {
8139         $record          = 0x0042;             // Record identifier
8140         $length          = 0x0002;             // Number of bytes to follow
8141         $cv              = $this->_codepage;   // The code page
8142
8143         $header          = pack('vv', $record, $length);
8144         $data            = pack('v',  $cv);
8145
8146         $this->_append($header . $data);
8147     }
8148
8149     /**
8150     * Write Excel BIFF WINDOW1 record.
8151     *
8152     * @access private
8153     */
8154     function _storeWindow1()
8155     {
8156         $record    = 0x003D;                 // Record identifier
8157         $length    = 0x0012;                 // Number of bytes to follow
8158
8159         $xWn       = 0x0000;                 // Horizontal position of window
8160         $yWn       = 0x0000;                 // Vertical position of window
8161         $dxWn      = 0x25BC;                 // Width of window
8162         $dyWn      = 0x1572;                 // Height of window
8163
8164         $grbit     = 0x0038;                 // Option flags
8165         $ctabsel   = $this->_selected;       // Number of workbook tabs selected
8166         $wTabRatio = 0x0258;                 // Tab to scrollbar ratio
8167
8168         $itabFirst = $this->_firstsheet;     // 1st displayed worksheet
8169         $itabCur   = $this->_activesheet;    // Active worksheet
8170
8171         $header    = pack("vv",        $record, $length);
8172         $data      = pack("vvvvvvvvv", $xWn, $yWn, $dxWn, $dyWn,
8173                                        $grbit,
8174                                        $itabCur, $itabFirst,
8175                                        $ctabsel, $wTabRatio);
8176         $this->_append($header . $data);
8177     }
8178
8179     /**
8180     * Writes Excel BIFF BOUNDSHEET record.
8181     * FIXME: inconsistent with BIFF documentation
8182     *
8183     * @param string  $sheetname Worksheet name
8184     * @param integer $offset    Location of worksheet BOF
8185     * @access private
8186     */
8187     function _storeBoundsheet($sheetname,$offset)
8188     {
8189         $record    = 0x0085;                    // Record identifier
8190 /*        
8191         if ($this->_BIFF_version == 0x0600)     // Tried to fix the correct handling here, with the
8192         {                                                                               // corrected specification from M$ - Joe Hunt 2009-03-08
8193                 $encoding_string = $this->_input_encoding;
8194                 if ($encoding_string == 'UTF-16LE')
8195                 {
8196                     $strlen = function_exists('mb_strlen') ? mb_strlen($sheetname, 'UTF-16LE') : (strlen($sheetname) / 2);
8197                     $encoding  = 0x1;
8198                 }
8199                 else if ($encoding_string != '')
8200                 {
8201                     $sheetname = iconv($encoding_string, 'UTF-16LE', $sheetname);
8202                     $strlen = function_exists('mb_strlen') ? mb_strlen($sheetname, 'UTF-16LE') : (strlen($sheetname) / 2);
8203                     $encoding  = 0x1;
8204                 }
8205                 if ($strlen % 2 != 0)
8206                         $strlen++;
8207                 $encoding  = 0x1;
8208                 
8209             //$strlen = strlen($sheetname);
8210             $length    = 0x08 + $strlen;                // Number of bytes to follow
8211         } else {
8212                 $strlen = strlen($sheetname);
8213             $length = 0x07 + $strlen;                   // Number of bytes to follow
8214         }
8215
8216         $grbit     = 0x0000;                    // Visibility and sheet type
8217         $cch       = $strlen;                           // Length of sheet name
8218
8219         $header    = pack("vv",  $record, $length);
8220         if ($this->_BIFF_version == 0x0600) {
8221             $data      = pack("VvCC", $offset, $grbit, $cch, $encoding);
8222         } else {
8223             $data      = pack("VvC", $offset, $grbit, $cch);
8224         }
8225 */        
8226         if ($this->_BIFF_version == 0x0600) 
8227         {
8228             $strlen = strlen($sheetname);
8229             $length    = 0x08 + $strlen;                // Number of bytes to follow
8230         } else {
8231                 $strlen = strlen($sheetname);
8232             $length = 0x07 + $strlen;                   // Number of bytes to follow
8233         }
8234
8235         $grbit     = 0x0000;                    // Visibility and sheet type
8236         $cch       = $strlen;                           // Length of sheet name
8237
8238         $header    = pack("vv",  $record, $length);
8239         if ($this->_BIFF_version == 0x0600) {
8240             $data      = pack("Vvv", $offset, $grbit, $cch);
8241         } else {
8242             $data      = pack("VvC", $offset, $grbit, $cch);
8243         }
8244         $this->_append($header.$data.$sheetname);
8245     }
8246
8247     /**
8248     * Write Internal SUPBOOK record
8249     *
8250     * @access private
8251     */
8252     function _storeSupbookInternal()
8253     {
8254         $record    = 0x01AE;   // Record identifier
8255         $length    = 0x0004;   // Bytes to follow
8256
8257         $header    = pack("vv", $record, $length);
8258         $data      = pack("vv", count($this->_worksheets), 0x0104);
8259         $this->_append($header . $data);
8260     }
8261
8262     /**
8263     * Writes the Excel BIFF EXTERNSHEET record. These references are used by
8264     * formulas.
8265     *
8266     * @param string $sheetname Worksheet name
8267     * @access private
8268     */
8269     function _storeExternsheetBiff8()
8270     {
8271         $total_references = count($this->_parser->_references);
8272         $record   = 0x0017;                     // Record identifier
8273         $length   = 2 + 6 * $total_references;  // Number of bytes to follow
8274
8275         $supbook_index = 0;           // FIXME: only using internal SUPBOOK record
8276         $header           = pack("vv",  $record, $length);
8277         $data             = pack('v', $total_references);
8278         for ($i = 0; $i < $total_references; $i++) {
8279             $data .= $this->_parser->_references[$i];
8280         }
8281         $this->_append($header . $data);
8282     }
8283
8284     /**
8285     * Write Excel BIFF STYLE records.
8286     *
8287     * @access private
8288     */
8289     function _storeStyle()
8290     {
8291         $record    = 0x0293;   // Record identifier
8292         $length    = 0x0004;   // Bytes to follow
8293
8294         $ixfe      = 0x8000;   // Index to style XF
8295         $BuiltIn   = 0x00;     // Built-in style
8296         $iLevel    = 0xff;     // Outline style level
8297
8298         $header    = pack("vv",  $record, $length);
8299         $data      = pack("vCC", $ixfe, $BuiltIn, $iLevel);
8300         $this->_append($header . $data);
8301     }
8302
8303
8304     /**
8305     * Writes Excel FORMAT record for non "built-in" numerical formats.
8306     *
8307     * @param string  $format Custom format string
8308     * @param integer $ifmt   Format index code
8309     * @access private
8310     */
8311     function _storeNumFormat($format, $ifmt)
8312     {
8313         $record    = 0x041E;                      // Record identifier
8314
8315         if ($this->_BIFF_version == 0x0600) {
8316             $length    = 5 + strlen($format);      // Number of bytes to follow
8317             $encoding = 0x0;
8318         } elseif ($this->_BIFF_version == 0x0500) {
8319             $length    = 3 + strlen($format);      // Number of bytes to follow
8320         }
8321
8322         $cch       = strlen($format);             // Length of format string
8323
8324         $header    = pack("vv", $record, $length);
8325         if ($this->_BIFF_version == 0x0600) {
8326             $data      = pack("vvC", $ifmt, $cch, $encoding);
8327         } elseif ($this->_BIFF_version == 0x0500) {
8328             $data      = pack("vC", $ifmt, $cch);
8329         }
8330         $this->_append($header . $data . $format);
8331     }
8332
8333     /**
8334     * Write DATEMODE record to indicate the date system in use (1904 or 1900).
8335     *
8336     * @access private
8337     */
8338     function _storeDatemode()
8339     {
8340         $record    = 0x0022;         // Record identifier
8341         $length    = 0x0002;         // Bytes to follow
8342
8343         $f1904     = $this->_1904;   // Flag for 1904 date system
8344
8345         $header    = pack("vv", $record, $length);
8346         $data      = pack("v", $f1904);
8347         $this->_append($header . $data);
8348     }
8349
8350
8351     /**
8352     * Write BIFF record EXTERNCOUNT to indicate the number of external sheet
8353     * references in the workbook.
8354     *
8355     * Excel only stores references to external sheets that are used in NAME.
8356     * The workbook NAME record is required to define the print area and the repeat
8357     * rows and columns.
8358     *
8359     * A similar method is used in Worksheet.php for a slightly different purpose.
8360     *
8361     * @param integer $cxals Number of external references
8362     * @access private
8363     */
8364     function _storeExterncount($cxals)
8365     {
8366         $record   = 0x0016;          // Record identifier
8367         $length   = 0x0002;          // Number of bytes to follow
8368
8369         $header   = pack("vv", $record, $length);
8370         $data     = pack("v",  $cxals);
8371         $this->_append($header . $data);
8372     }
8373
8374
8375     /**
8376     * Writes the Excel BIFF EXTERNSHEET record. These references are used by
8377     * formulas. NAME record is required to define the print area and the repeat
8378     * rows and columns.
8379     *
8380     * A similar method is used in Worksheet.php for a slightly different purpose.
8381     *
8382     * @param string $sheetname Worksheet name
8383     * @access private
8384     */
8385     function _storeExternsheet($sheetname)
8386     {
8387         $record      = 0x0017;                     // Record identifier
8388         $length      = 0x02 + strlen($sheetname);  // Number of bytes to follow
8389
8390         $cch         = strlen($sheetname);         // Length of sheet name
8391         $rgch        = 0x03;                       // Filename encoding
8392
8393         $header      = pack("vv",  $record, $length);
8394         $data        = pack("CC", $cch, $rgch);
8395         $this->_append($header . $data . $sheetname);
8396     }
8397
8398
8399     /**
8400     * Store the NAME record in the short format that is used for storing the print
8401     * area, repeat rows only and repeat columns only.
8402     *
8403     * @param integer $index  Sheet index
8404     * @param integer $type   Built-in name type
8405     * @param integer $rowmin Start row
8406     * @param integer $rowmax End row
8407     * @param integer $colmin Start colum
8408     * @param integer $colmax End column
8409     * @access private
8410     */
8411     function _storeNameShort($index, $type, $rowmin, $rowmax, $colmin, $colmax)
8412     {
8413         $record          = 0x0018;       // Record identifier
8414         $length          = 0x0024;       // Number of bytes to follow
8415
8416         $grbit           = 0x0020;       // Option flags
8417         $chKey           = 0x00;         // Keyboard shortcut
8418         $cch             = 0x01;         // Length of text name
8419         $cce             = 0x0015;       // Length of text definition
8420         $ixals           = $index + 1;   // Sheet index
8421         $itab            = $ixals;       // Equal to ixals
8422         $cchCustMenu     = 0x00;         // Length of cust menu text
8423         $cchDescription  = 0x00;         // Length of description text
8424         $cchHelptopic    = 0x00;         // Length of help topic text
8425         $cchStatustext   = 0x00;         // Length of status bar text
8426         $rgch            = $type;        // Built-in name type
8427
8428         $unknown03       = 0x3b;
8429         $unknown04       = 0xffff-$index;
8430         $unknown05       = 0x0000;
8431         $unknown06       = 0x0000;
8432         $unknown07       = 0x1087;
8433         $unknown08       = 0x8005;
8434
8435         $header             = pack("vv", $record, $length);
8436         $data               = pack("v", $grbit);
8437         $data              .= pack("C", $chKey);
8438         $data              .= pack("C", $cch);
8439         $data              .= pack("v", $cce);
8440         $data              .= pack("v", $ixals);
8441         $data              .= pack("v", $itab);
8442         $data              .= pack("C", $cchCustMenu);
8443         $data              .= pack("C", $cchDescription);
8444         $data              .= pack("C", $cchHelptopic);
8445         $data              .= pack("C", $cchStatustext);
8446         $data              .= pack("C", $rgch);
8447         $data              .= pack("C", $unknown03);
8448         $data              .= pack("v", $unknown04);
8449         $data              .= pack("v", $unknown05);
8450         $data              .= pack("v", $unknown06);
8451         $data              .= pack("v", $unknown07);
8452         $data              .= pack("v", $unknown08);
8453         $data              .= pack("v", $index);
8454         $data              .= pack("v", $index);
8455         $data              .= pack("v", $rowmin);
8456         $data              .= pack("v", $rowmax);
8457         $data              .= pack("C", $colmin);
8458         $data              .= pack("C", $colmax);
8459         $this->_append($header . $data);
8460     }
8461
8462
8463     /**
8464     * Store the NAME record in the long format that is used for storing the repeat
8465     * rows and columns when both are specified. This shares a lot of code with
8466     * _storeNameShort() but we use a separate method to keep the code clean.
8467     * Code abstraction for reuse can be carried too far, and I should know. ;-)
8468     *
8469     * @param integer $index Sheet index
8470     * @param integer $type  Built-in name type
8471     * @param integer $rowmin Start row
8472     * @param integer $rowmax End row
8473     * @param integer $colmin Start colum
8474     * @param integer $colmax End column
8475     * @access private
8476     */
8477     function _storeNameLong($index, $type, $rowmin, $rowmax, $colmin, $colmax)
8478     {
8479         $record          = 0x0018;       // Record identifier
8480         $length          = 0x003d;       // Number of bytes to follow
8481         $grbit           = 0x0020;       // Option flags
8482         $chKey           = 0x00;         // Keyboard shortcut
8483         $cch             = 0x01;         // Length of text name
8484         $cce             = 0x002e;       // Length of text definition
8485         $ixals           = $index + 1;   // Sheet index
8486         $itab            = $ixals;       // Equal to ixals
8487         $cchCustMenu     = 0x00;         // Length of cust menu text
8488         $cchDescription  = 0x00;         // Length of description text
8489         $cchHelptopic    = 0x00;         // Length of help topic text
8490         $cchStatustext   = 0x00;         // Length of status bar text
8491         $rgch            = $type;        // Built-in name type
8492
8493         $unknown01       = 0x29;
8494         $unknown02       = 0x002b;
8495         $unknown03       = 0x3b;
8496         $unknown04       = 0xffff-$index;
8497         $unknown05       = 0x0000;
8498         $unknown06       = 0x0000;
8499         $unknown07       = 0x1087;
8500         $unknown08       = 0x8008;
8501
8502         $header             = pack("vv",  $record, $length);
8503         $data               = pack("v", $grbit);
8504         $data              .= pack("C", $chKey);
8505         $data              .= pack("C", $cch);
8506         $data              .= pack("v", $cce);
8507         $data              .= pack("v", $ixals);
8508         $data              .= pack("v", $itab);
8509         $data              .= pack("C", $cchCustMenu);
8510         $data              .= pack("C", $cchDescription);
8511         $data              .= pack("C", $cchHelptopic);
8512         $data              .= pack("C", $cchStatustext);
8513         $data              .= pack("C", $rgch);
8514         $data              .= pack("C", $unknown01);
8515         $data              .= pack("v", $unknown02);
8516         // Column definition
8517         $data              .= pack("C", $unknown03);
8518         $data              .= pack("v", $unknown04);
8519         $data              .= pack("v", $unknown05);
8520         $data              .= pack("v", $unknown06);
8521         $data              .= pack("v", $unknown07);
8522         $data              .= pack("v", $unknown08);
8523         $data              .= pack("v", $index);
8524         $data              .= pack("v", $index);
8525         $data              .= pack("v", 0x0000);
8526         $data              .= pack("v", 0x3fff);
8527         $data              .= pack("C", $colmin);
8528         $data              .= pack("C", $colmax);
8529         // Row definition
8530         $data              .= pack("C", $unknown03);
8531         $data              .= pack("v", $unknown04);
8532         $data              .= pack("v", $unknown05);
8533         $data              .= pack("v", $unknown06);
8534         $data              .= pack("v", $unknown07);
8535         $data              .= pack("v", $unknown08);
8536         $data              .= pack("v", $index);
8537         $data              .= pack("v", $index);
8538         $data              .= pack("v", $rowmin);
8539         $data              .= pack("v", $rowmax);
8540         $data              .= pack("C", 0x00);
8541         $data              .= pack("C", 0xff);
8542         // End of data
8543         $data              .= pack("C", 0x10);
8544         $this->_append($header . $data);
8545     }
8546
8547     /**
8548     * Stores the COUNTRY record for localization
8549     *
8550     * @access private
8551     */
8552     function _storeCountry()
8553     {
8554         $record          = 0x008C;    // Record identifier
8555         $length          = 4;         // Number of bytes to follow
8556
8557         $header = pack('vv',  $record, $length);
8558         /* using the same country code always for simplicity */
8559         $data = pack('vv', $this->_country_code, $this->_country_code);
8560         $this->_append($header . $data);
8561     }
8562
8563     /**
8564     * Stores the PALETTE biff record.
8565     *
8566     * @access private
8567     */
8568     function _storePalette()
8569     {
8570         $aref            = $this->_palette;
8571
8572         $record          = 0x0092;                 // Record identifier
8573         $length          = 2 + 4 * count($aref);   // Number of bytes to follow
8574         $ccv             =         count($aref);   // Number of RGB values to follow
8575         $data = '';                                // The RGB data
8576
8577         // Pack the RGB data
8578         foreach ($aref as $color) {
8579             foreach ($color as $byte) {
8580                 $data .= pack("C",$byte);
8581             }
8582         }
8583
8584         $header = pack("vvv",  $record, $length, $ccv);
8585         $this->_append($header . $data);
8586     }
8587
8588     /**
8589     * Calculate
8590     * Handling of the SST continue blocks is complicated by the need to include an
8591     * additional continuation byte depending on whether the string is split between
8592     * blocks or whether it starts at the beginning of the block. (There are also
8593     * additional complications that will arise later when/if Rich Strings are
8594     * supported).
8595     *
8596     * @access private
8597     */
8598     function _calculateSharedStringsSizes()
8599     {
8600         /* Iterate through the strings to calculate the CONTINUE block sizes.
8601            For simplicity we use the same size for the SST and CONTINUE records:
8602            8228 : Maximum Excel97 block size
8603              -4 : Length of block header
8604              -8 : Length of additional SST header information
8605              -8 : Arbitrary number to keep within _add_continue() limit = 8208
8606         */
8607         $continue_limit     = 8208;
8608         $block_length       = 0;
8609         $written            = 0;
8610         $this->_block_sizes = array();
8611         $continue           = 0;
8612
8613         foreach (array_keys($this->_str_table) as $string) {
8614             $string_length = strlen($string);
8615             $headerinfo    = unpack("vlength/Cencoding", $string);
8616             $encoding      = $headerinfo["encoding"];
8617             $split_string  = 0;
8618
8619             // Block length is the total length of the strings that will be
8620             // written out in a single SST or CONTINUE block.
8621             $block_length += $string_length;
8622
8623             // We can write the string if it doesn't cross a CONTINUE boundary
8624             if ($block_length < $continue_limit) {
8625                 $written      += $string_length;
8626                 continue;
8627             }
8628
8629             // Deal with the cases where the next string to be written will exceed
8630             // the CONTINUE boundary. If the string is very long it may need to be
8631             // written in more than one CONTINUE record.
8632             while ($block_length >= $continue_limit) {
8633
8634                 // We need to avoid the case where a string is continued in the first
8635                 // n bytes that contain the string header information.
8636                 $header_length   = 3; // Min string + header size -1
8637                 $space_remaining = $continue_limit - $written - $continue;
8638
8639
8640                 /* TODO: Unicode data should only be split on char (2 byte)
8641                 boundaries. Therefore, in some cases we need to reduce the
8642                 amount of available
8643                 */
8644                 $align = 0;
8645
8646                 // Only applies to Unicode strings
8647                 if ($encoding == 1) {
8648                     // Min string + header size -1
8649                     $header_length = 4;
8650
8651                     if ($space_remaining > $header_length) {
8652                         // String contains 3 byte header => split on odd boundary
8653                         if (!$split_string && $space_remaining % 2 != 1) {
8654                             $space_remaining--;
8655                             $align = 1;
8656                         }
8657                         // Split section without header => split on even boundary
8658                         else if ($split_string && $space_remaining % 2 == 1) {
8659                             $space_remaining--;
8660                             $align = 1;
8661                         }
8662
8663                         $split_string = 1;
8664                     }
8665                 }
8666
8667
8668                 if ($space_remaining > $header_length) {
8669                     // Write as much as possible of the string in the current block
8670                     $written      += $space_remaining;
8671
8672                     // Reduce the current block length by the amount written
8673                     $block_length -= $continue_limit - $continue - $align;
8674
8675                     // Store the max size for this block
8676                     $this->_block_sizes[] = $continue_limit - $align;
8677
8678                     // If the current string was split then the next CONTINUE block
8679                     // should have the string continue flag (grbit) set unless the
8680                     // split string fits exactly into the remaining space.
8681                     if ($block_length > 0) {
8682                         $continue = 1;
8683                     } else {
8684                         $continue = 0;
8685                     }
8686                 } else {
8687                     // Store the max size for this block
8688                     $this->_block_sizes[] = $written + $continue;
8689
8690                     // Not enough space to start the string in the current block
8691                     $block_length -= $continue_limit - $space_remaining - $continue;
8692                     $continue = 0;
8693
8694                 }
8695
8696                 // If the string (or substr) is small enough we can write it in the
8697                 // new CONTINUE block. Else, go through the loop again to write it in
8698                 // one or more CONTINUE blocks
8699                 if ($block_length < $continue_limit) {
8700                     $written = $block_length;
8701                 } else {
8702                     $written = 0;
8703                 }
8704             }
8705         }
8706
8707         // Store the max size for the last block unless it is empty
8708         if ($written + $continue) {
8709             $this->_block_sizes[] = $written + $continue;
8710         }
8711
8712
8713         /* Calculate the total length of the SST and associated CONTINUEs (if any).
8714          The SST record will have a length even if it contains no strings.
8715          This length is required to set the offsets in the BOUNDSHEET records since
8716          they must be written before the SST records
8717         */
8718
8719         $tmp_block_sizes = array();
8720         $tmp_block_sizes = $this->_block_sizes;
8721
8722         $length  = 12;
8723         if (!empty($tmp_block_sizes)) {
8724             $length += array_shift($tmp_block_sizes); // SST
8725         }
8726         while (!empty($tmp_block_sizes)) {
8727             $length += 4 + array_shift($tmp_block_sizes); // CONTINUEs
8728         }
8729
8730         return $length;
8731     }
8732
8733     /**
8734     * Write all of the workbooks strings into an indexed array.
8735     * See the comments in _calculate_shared_string_sizes() for more information.
8736     *
8737     * The Excel documentation says that the SST record should be followed by an
8738     * EXTSST record. The EXTSST record is a hash table that is used to optimise
8739     * access to SST. However, despite the documentation it doesn't seem to be
8740     * required so we will ignore it.
8741     *
8742     * @access private
8743     */
8744     function _storeSharedStringsTable()
8745     {
8746         $record  = 0x00fc;  // Record identifier
8747         $length  = 0x0008;  // Number of bytes to follow
8748         $total   = 0x0000;
8749
8750         // Iterate through the strings to calculate the CONTINUE block sizes
8751         $continue_limit = 8208;
8752         $block_length   = 0;
8753         $written        = 0;
8754         $continue       = 0;
8755
8756         // sizes are upside down
8757         $tmp_block_sizes = $this->_block_sizes;
8758         // $tmp_block_sizes = array_reverse($this->_block_sizes);
8759
8760         // The SST record is required even if it contains no strings. Thus we will
8761         // always have a length
8762         //
8763         if (!empty($tmp_block_sizes)) {
8764             $length = 8 + array_shift($tmp_block_sizes);
8765         }
8766         else {
8767             // No strings
8768             $length = 8;
8769         }
8770
8771         // Write the SST block header information
8772         $header      = pack("vv", $record, $length);
8773         $data        = pack("VV", $this->_str_total, $this->_str_unique);
8774         $this->_append($header . $data);
8775
8776         /* TODO: not good for performance */
8777         foreach (array_keys($this->_str_table) as $string) {
8778
8779             $string_length = strlen($string);
8780             $headerinfo    = unpack("vlength/Cencoding", $string);
8781             $encoding      = $headerinfo["encoding"];
8782             $split_string  = 0;
8783
8784             // Block length is the total length of the strings that will be
8785             // written out in a single SST or CONTINUE block.
8786             //
8787             $block_length += $string_length;
8788
8789
8790             // We can write the string if it doesn't cross a CONTINUE boundary
8791             if ($block_length < $continue_limit) {
8792                 $this->_append($string);
8793                 $written += $string_length;
8794                 continue;
8795             }
8796
8797             // Deal with the cases where the next string to be written will exceed
8798             // the CONTINUE boundary. If the string is very long it may need to be
8799             // written in more than one CONTINUE record.
8800             //
8801             while ($block_length >= $continue_limit) {
8802
8803                 // We need to avoid the case where a string is continued in the first
8804                 // n bytes that contain the string header information.
8805                 //
8806                 $header_length   = 3; // Min string + header size -1
8807                 $space_remaining = $continue_limit - $written - $continue;
8808
8809
8810                 // Unicode data should only be split on char (2 byte) boundaries.
8811                 // Therefore, in some cases we need to reduce the amount of available
8812                 // space by 1 byte to ensure the correct alignment.
8813                 $align = 0;
8814
8815                 // Only applies to Unicode strings
8816                 if ($encoding == 1) {
8817                     // Min string + header size -1
8818                     $header_length = 4;
8819
8820                     if ($space_remaining > $header_length) {
8821                         // String contains 3 byte header => split on odd boundary
8822                         if (!$split_string && $space_remaining % 2 != 1) {
8823                             $space_remaining--;
8824                             $align = 1;
8825                         }
8826                         // Split section without header => split on even boundary
8827                         else if ($split_string && $space_remaining % 2 == 1) {
8828                             $space_remaining--;
8829                             $align = 1;
8830                         }
8831
8832                         $split_string = 1;
8833                     }
8834                 }
8835
8836
8837                 if ($space_remaining > $header_length) {
8838                     // Write as much as possible of the string in the current block
8839                     $tmp = substr($string, 0, $space_remaining);
8840                     $this->_append($tmp);
8841
8842                     // The remainder will be written in the next block(s)
8843                     $string = substr($string, $space_remaining);
8844
8845                     // Reduce the current block length by the amount written
8846                     $block_length -= $continue_limit - $continue - $align;
8847
8848                     // If the current string was split then the next CONTINUE block
8849                     // should have the string continue flag (grbit) set unless the
8850                     // split string fits exactly into the remaining space.
8851                     //
8852                     if ($block_length > 0) {
8853                         $continue = 1;
8854                     } else {
8855                         $continue = 0;
8856                     }
8857                 } else {
8858                     // Not enough space to start the string in the current block
8859                     $block_length -= $continue_limit - $space_remaining - $continue;
8860                     $continue = 0;
8861                 }
8862
8863                 // Write the CONTINUE block header
8864                 if (!empty($this->_block_sizes)) {
8865                     $record  = 0x003C;
8866                     $length  = array_shift($tmp_block_sizes);
8867
8868                     $header  = pack('vv', $record, $length);
8869                     if ($continue) {
8870                         $header .= pack('C', $encoding);
8871                     }
8872                     $this->_append($header);
8873                 }
8874
8875                 // If the string (or substr) is small enough we can write it in the
8876                 // new CONTINUE block. Else, go through the loop again to write it in
8877                 // one or more CONTINUE blocks
8878                 //
8879                 if ($block_length < $continue_limit) {
8880                     $this->_append($string);
8881                     $written = $block_length;
8882                 } else {
8883                     $written = 0;
8884                 }
8885             }
8886         }
8887     }
8888 }