Files
lulandia.pl/modules/import_api/lib/SimpleXLS.php
2025-04-01 00:38:54 +02:00

1224 lines
39 KiB
PHP

<?php
/*
* Example
echo '<pre>';
if ( $xls = SimpleXLS::parse('excel5book.xls')) {
print_r( $xls->rows() ); // dump first sheet
print_r( $xls->rows(1)); /// dump second sheet
} else {
echo SimpleXLSX::parseError();
}
echo '</pre>';
*/
/**
* A class for reading Microsoft Excel Spreadsheets.
*
* SimpleXLS version 2016-2020 packaged by Sergey Shuchkin <sergey.shuchkin@gmail.com> from
* Spreadsheet_Excel_Reader class developed by Vadim Tkachenko <vt@phpapache.com>
*/
class SimpleXLS {
const BIFF8 = 0x600;
const BIFF7 = 0x500;
const WORKBOOKGLOBALS = 0x5;
const WORKSHEET = 0x10;
//const TYPE_BOF = 0x809;
const TYPE_EOF = 0x0a;
const TYPE_BOUNDSHEET = 0x85;
const TYPE_DIMENSION = 0x200;
const TYPE_ROW = 0x208;
const TYPE_DBCELL = 0xd7;
const TYPE_FILEPASS = 0x2f;
//const TYPE_NOTE = 0x1c;
//const TYPE_TXO = 0x1b6;
const TYPE_RK = 0x7e;
const TYPE_RK2 = 0x27e;
const TYPE_MULRK = 0xbd;
const TYPE_MULBLANK = 0xbe;
//const TYPE_INDEX = 0x20b;
const TYPE_SST = 0xfc;
//const TYPE_EXTSST = 0xff;
//const TYPE_CONTINUE = 0x3c;
const TYPE_LABEL = 0x204;
const TYPE_LABELSST = 0xfd;
const TYPE_NUMBER = 0x203;
const TYPE_NAME = 0x18;
//const TYPE_ARRAY = 0x221;
//const TYPE_STRING = 0x207;
const TYPE_FORMULA = 0x406;
const TYPE_FORMULA2 = 0x6;
const TYPE_FORMAT = 0x41e;
const TYPE_XF = 0xe0;
const TYPE_BOOLERR = 0x205;
//const TYPE_UNKNOWN = 0xffff;
const TYPE_NINETEENFOUR = 0x22;
const TYPE_MERGEDCELLS = 0xE5;
//const DEF_NUM_FORMAT = "%.2f";
const DEF_NUM_FORMAT = '%s';
// OLE
const NUM_BIG_BLOCK_DEPOT_BLOCKS_POS = 0x2c;
const SMALL_BLOCK_DEPOT_BLOCK_POS = 0x3c;
const ROOT_START_BLOCK_POS = 0x30;
const BIG_BLOCK_SIZE = 0x200;
const SMALL_BLOCK_SIZE = 0x40;
const EXTENSION_BLOCK_POS = 0x44;
const NUM_EXTENSION_BLOCK_POS = 0x48;
const PROPERTY_STORAGE_BLOCK_SIZE = 0x80;
const BIG_BLOCK_DEPOT_BLOCKS_POS = 0x4c;
const SMALL_BLOCK_THRESHOLD = 0x1000;
// property storage offsets
const SIZE_OF_NAME_POS = 0x40;
const TYPE_POS = 0x42;
const START_BLOCK_POS = 0x74;
const SIZE_POS = 0x78;
/**
* Array of worksheets found
*
* @var array
* @access public
*/
public $boundsheets = array();
/**
* Array of format records found
*
* @var array
* @access public
*/
public $formatRecords = array();
/**
*
* @var array
* @access public
*/
public $sst = array();
/**
* Array of worksheets
*
* The data is stored in 'cells' and the meta-data is stored in an array
* called 'cellsInfo'
*
* Example:
*
* $sheets --> 'cells' --> row --> column --> Interpreted value
* --> 'cellsInfo' --> row --> column --> 'type' - Can be 'date', 'number', or 'unknown'
* --> 'raw' - The raw data that Excel stores for that data cell
*
* @var array
* @access public
*/
public $sheets = array();
/**
* List of default date formats used by Excel
*
* @var array
* @access public
*/
public $dateFormats = array(
0xe => 'd/m/Y',
0xf => 'd-M-Y',
0x10 => 'd-M',
0x11 => 'M-Y',
0x12 => 'h:i a',
0x13 => 'h:i:s a',
0x14 => 'H:i',
0x15 => 'H:i:s',
0x16 => 'd/m/Y H:i',
0x2d => 'i:s',
0x2e => 'H:i:s',
0x2f => 'i:s.S'
);
/**
* Default number formats used by Excel
*
* @var array
* @access public
*/
public $numberFormats = array(
0x1 => '%1.0f', // "0"
0x2 => '%1.2f', // "0.00",
0x3 => '%1.0f', //"#,##0",
0x4 => '%1.2f', //"#,##0.00",
0x5 => '%1.0f', /*"$#,##0;($#,##0)",*/
0x6 => '$%1.0f', /*"$#,##0;($#,##0)",*/
0x7 => '$%1.2f', //"$#,##0.00;($#,##0.00)",
0x8 => '$%1.2f', //"$#,##0.00;($#,##0.00)",
0x9 => '%1.0f%%', // "0%"
0xa => '%1.2f%%', // "0.00%"
0xb => '%1.2f', // 0.00E00",
0x25 => '%1.0f', // "#,##0;(#,##0)",
0x26 => '%1.0f', //"#,##0;(#,##0)",
0x27 => '%1.2f', //"#,##0.00;(#,##0.00)",
0x28 => '%1.2f', //"#,##0.00;(#,##0.00)",
0x29 => '%1.0f', //"#,##0;(#,##0)",
0x2a => '$%1.0f', //"$#,##0;($#,##0)",
0x2b => '%1.2f', //"#,##0.00;(#,##0.00)",
0x2c => '$%1.2f', //"$#,##0.00;($#,##0.00)",
0x30 => '%1.0f'
);
private $datetimeFormat = 'Y-m-d H:i:s';
/**
* Default encoding
*
* @var string
* @access private
*/
private $defaultEncoding = 'UTF-8';
/**
* Default number format
*
* @var integer
* @access private
*/
private $defaultFormat = self::DEF_NUM_FORMAT;
/**
* List of formats to use for each column
*
* @var array
* @access private
*/
private $columnsFormat = array();
/**
*
* @var integer
* @access private
*/
private $rowoffset = 1; //"##0.0E0";
// }}}
/**
*
* @var integer
* @access private
*/
private $colOffset = 1;
private $recType;
private $nineteenFour;
private $multiplier;
private $sn;
private $curFormat;
// OLERead
private $data;
private $numBigBlockDepotBlocks;
private $sbdStartBlock;
private $rootStartBlock;
private $extensionBlock;
private $numExtensionBlocks;
private $bigBlockChain;
private $smallBlockChain;
private $rootEntry;
private $entry;
private $props;
// sergey.shuchkin@gmail.com
private $wrkbook; // false - to use excel format
private $error = false;
private $debug;
// {{{ Spreadsheet_Excel_Reader()
/**
* Constructor
*
* @param string $filename XLS Filename or xls contents
* @param bool $isData If True then $filename is contents
* @param bool $debug Trigger PHP errors?
*/
public function __construct( $filename, $isData = false, $debug = false ) {
$this->debug = $debug;
$this->_oleread( $filename, $isData );
$this->_parse();
}
public static function parse( $filename, $isData = false, $debug = false ) {
$xlsx = new self( $filename, $isData, $debug );
if ( $xlsx->success() ) {
return $xlsx;
}
self::parseError( $xlsx->error() );
return false;
}
public static function parseError( $set = false ) {
static $error = false;
return $set ? $error = $set : $error;
}
public function error( $set = false ) {
if ( $set ) {
$this->error = $set;
if ( $this->debug ) {
trigger_error( $set );
}
}
return $this->error;
}
public function success() {
return ! $this->error;
}
public function rows( $sheetNum = 0 ) {
if ( $this->sheets[ $sheetNum ] ) {
$s = $this->sheets[ $sheetNum ];
$result = array();
for ( $i = 0; $i < $s['numRows']; $i ++ ) {
$r = array();
for ( $j = 0; $j < $s['numCols']; $j ++ ) {
$r[ $j ] = isset( $s['cells'][ $i + 1 ][ $j + 1 ] ) ? $s['cells'][ $i + 1 ][ $j + 1 ] : '';
}
$result[] = $r;
}
return $result;
}
return false;
}
public function setDateTimeFormat( $value ) {
$this->datetimeFormat = is_string( $value) ? $value : false;
}
// }}}
private function _oleread( $sFileName, $isData = false ) {
if ( $isData ) {
$this->data = $sFileName;
} else {
// check if file exist and is readable (Darko Miljanovic)
if ( ! is_readable( $sFileName ) ) {
$this->error( 'File not is readable ' . $sFileName );
return false;
}
$this->data = file_get_contents( $sFileName );
if ( ! $this->data ) {
$this->error( 'File reading error ' . $sFileName );
return false;
}
}
//echo IDENTIFIER_OLE;
//echo 'start';
if ( $this->_strpos( $this->data, pack( 'CCCCCCCC', 0xd0, 0xcf, 0x11, 0xe0, 0xa1, 0xb1, 0x1a, 0xe1 ) ) !== 0 ) {
$this->error( 'File is not XLS' );
return false;
}
$this->numBigBlockDepotBlocks = $this->_GetInt4d( $this->data, self::NUM_BIG_BLOCK_DEPOT_BLOCKS_POS );
$this->sbdStartBlock = $this->_GetInt4d( $this->data, self::SMALL_BLOCK_DEPOT_BLOCK_POS );
$this->rootStartBlock = $this->_GetInt4d( $this->data, self::ROOT_START_BLOCK_POS );
$this->extensionBlock = $this->_GetInt4d( $this->data, self::EXTENSION_BLOCK_POS );
$this->numExtensionBlocks = $this->_GetInt4d( $this->data, self::NUM_EXTENSION_BLOCK_POS );
/*
echo $this->numBigBlockDepotBlocks." ";
echo $this->sbdStartBlock." ";
echo $this->rootStartBlock." ";
echo $this->extensionBlock." ";
echo $this->numExtensionBlocks." ";
*/
//echo "sbdStartBlock = $this->sbdStartBlock\n";
$bigBlockDepotBlocks = array();
$pos = self::BIG_BLOCK_DEPOT_BLOCKS_POS;
// echo "pos = $pos";
$bbdBlocks = $this->numBigBlockDepotBlocks;
if ( $this->numExtensionBlocks !== 0 ) {
$bbdBlocks = ( self::BIG_BLOCK_SIZE - self::BIG_BLOCK_DEPOT_BLOCKS_POS ) / 4;
}
for ( $i = 0; $i < $bbdBlocks; $i ++ ) {
$bigBlockDepotBlocks[ $i ] = $this->_GetInt4d( $this->data, $pos );
$pos += 4;
}
for ( $j = 0; $j < $this->numExtensionBlocks; $j ++ ) {
$pos = ( $this->extensionBlock + 1 ) * self::BIG_BLOCK_SIZE;
$blocksToRead = min( $this->numBigBlockDepotBlocks - $bbdBlocks, self::BIG_BLOCK_SIZE / 4 - 1 );
for ( $i = $bbdBlocks; $i < $bbdBlocks + $blocksToRead; $i ++ ) {
$bigBlockDepotBlocks[ $i ] = $this->_GetInt4d( $this->data, $pos );
$pos += 4;
}
$bbdBlocks += $blocksToRead;
if ( $bbdBlocks < $this->numBigBlockDepotBlocks ) {
$this->extensionBlock = $this->_GetInt4d( $this->data, $pos );
}
}
// var_dump($bigBlockDepotBlocks);
// readBigBlockDepot
$index = 0;
$this->bigBlockChain = array();
for ( $i = 0; $i < $this->numBigBlockDepotBlocks; $i ++ ) {
$pos = ( $bigBlockDepotBlocks[ $i ] + 1 ) * self::BIG_BLOCK_SIZE;
//echo "pos = $pos";
for ( $j = 0; $j < self::BIG_BLOCK_SIZE / 4; $j ++ ) {
$this->bigBlockChain[ $index ] = $this->_GetInt4d( $this->data, $pos );
$pos += 4;
$index ++;
}
}
//var_dump($this->bigBlockChain);
//echo '=====2';
// readSmallBlockDepot();
$index = 0;
$sbdBlock = $this->sbdStartBlock;
$this->smallBlockChain = array();
while ( $sbdBlock !== - 2 ) {
$pos = ( $sbdBlock + 1 ) * self::BIG_BLOCK_SIZE;
for ( $j = 0; $j < self::BIG_BLOCK_SIZE / 4; $j ++ ) {
$this->smallBlockChain[ $index ] = $this->_GetInt4d( $this->data, $pos );
$pos += 4;
$index ++;
}
$sbdBlock = $this->bigBlockChain[ $sbdBlock ];
}
// readData(rootStartBlock)
$block = $this->rootStartBlock;
$this->entry = $this->_readData( $block );
/*
while ($block != -2) {
$pos = ($block + 1) * self::BIG_BLOCK_SIZE;
$this->entry = $this->entry.$this->_substr($this->_data, $pos, self::BIG_BLOCK_SIZE);
$block = $this->bigBlockChain[$block];
}
*/
//echo '==='.$this->entry."===";
$this->_readPropertySets();
$this->data = $this->_readWorkBook();
return true;
}
// {{{ setOutputEncoding()
public function _GetInt4d( $data, $pos ) {
$value = ord( $data[ $pos ] ) | ( ord( $data[ $pos + 1 ] ) << 8 ) | ( ord( $data[ $pos + 2 ] ) << 16 ) | ( ord( $data[ $pos + 3 ] ) << 24 );
return ($value > 0x7FFFFFFF) ? $value - 0x100000000 : $value;
}
// }}}
// {{{ setRowColOffset()
public function _readData( $bl ) {
$block = $bl;
$data = '';
while ( $block !== - 2 ) {
$pos = ( $block + 1 ) * self::BIG_BLOCK_SIZE;
$data .= $this->_substr( $this->data, $pos, self::BIG_BLOCK_SIZE );
//echo "pos = $pos data=$data\n";
$block = $this->bigBlockChain[ $block ];
}
return $data;
}
// }}}
// {{{ setDefaultFormat()
public function _readPropertySets() {
$offset = 0;
//var_dump($this->entry);
while ( $offset < $this->_strlen( $this->entry ) ) {
$d = $this->_substr( $this->entry, $offset, self::PROPERTY_STORAGE_BLOCK_SIZE );
$nameSize = ord( $d[ self::SIZE_OF_NAME_POS ] ) | ( ord( $d[ self::SIZE_OF_NAME_POS + 1 ] ) << 8 );
$type = ord( $d[ self::TYPE_POS ] );
//$maxBlock = $this->_strlen($d) / self::BIG_BLOCK_SIZE - 1;
$startBlock = $this->_GetInt4d( $d, self::START_BLOCK_POS );
$size = $this->_GetInt4d( $d, self::SIZE_POS );
$name = '';
for ( $i = 0; $i < $nameSize; $i ++ ) {
$name .= $d[ $i ];
}
$name = str_replace( "\x00", '', $name );
$this->props[] = array(
'name' => $name,
'type' => $type,
'startBlock' => $startBlock,
'size' => $size
);
if ( ( $name === 'Workbook' ) || ( $name === 'Book' ) ) {
$this->wrkbook = count( $this->props ) - 1;
}
if ( $name === 'Root Entry' ) {
$this->rootEntry = count( $this->props ) - 1;
}
//echo "name ==$name=\n";
$offset += self::PROPERTY_STORAGE_BLOCK_SIZE;
}
}
// }}}
// {{{ setColumnFormat()
private function _readWorkBook() {
if ( $this->props[ $this->wrkbook ]['size'] < self::SMALL_BLOCK_THRESHOLD ) {
// getSmallBlockStream(PropertyStorage ps)
$rootdata = $this->_readData( $this->props[ $this->rootEntry ]['startBlock'] );
$streamData = '';
$block = (int) $this->props[ $this->wrkbook ]['startBlock'];
//$count = 0;
while ( $block !== - 2 ) {
$pos = $block * self::SMALL_BLOCK_SIZE;
$streamData .= $this->_substr( $rootdata, $pos, self::SMALL_BLOCK_SIZE );
$block = $this->smallBlockChain[ $block ];
}
return $streamData;
}
$numBlocks = $this->props[ $this->wrkbook ]['size'] / self::BIG_BLOCK_SIZE;
if ( $this->props[ $this->wrkbook ]['size'] % self::BIG_BLOCK_SIZE !== 0 ) {
$numBlocks ++;
}
if ( $numBlocks === 0 ) {
return '';
}
//echo "numBlocks = $numBlocks\n";
//byte[] streamData = new byte[numBlocks * self::BIG_BLOCK_SIZE];
//print_r($this->wrkbook);
$streamData = '';
$block = $this->props[ $this->wrkbook ]['startBlock'];
//echo "block = $block";
while ( $block !== - 2 ) {
$pos = ( $block + 1 ) * self::BIG_BLOCK_SIZE;
$streamData .= $this->_substr( $this->data, $pos, self::BIG_BLOCK_SIZE );
$block = $this->bigBlockChain[ $block ];
}
//echo 'stream'.$streamData;
return $streamData;
}
// }}}
// {{{ _parse()
/**
* Parse a workbook
*
* @access private
* @return bool
*/
public function _parse() {
$pos = 0;
// $code = ord($this->data[$pos]) | ord($this->data[$pos+1])<<8;
$length = ord( $this->data[ $pos + 2 ] ) | ord( $this->data[ $pos + 3 ] ) << 8;
$version = ord( $this->data[ $pos + 4 ] ) | ord( $this->data[ $pos + 5 ] ) << 8;
$substreamType = ord( $this->data[ $pos + 6 ] ) | ord( $this->data[ $pos + 7 ] ) << 8;
// echo "Start parse code=".base_convert($code,10,16)." version=".base_convert($version,10,16)." substreamType=".base_convert($substreamType,10,16).""."\n";
// die();
if ( ( $version !== self::BIFF8 ) &&
( $version !== self::BIFF7 )
) {
return false;
}
if ( $substreamType !== self::WORKBOOKGLOBALS ) {
return false;
}
//print_r($rec);
$pos += $length + 4;
$code = ord( $this->data[ $pos ] ) | ord( $this->data[ $pos + 1 ] ) << 8;
$length = ord( $this->data[ $pos + 2 ] ) | ord( $this->data[ $pos + 3 ] ) << 8;
while ( $code !== self::TYPE_EOF ) {
switch ( $code ) {
case self::TYPE_SST:
//echo "Type_SST\n";
$formattingRuns = 0;
$extendedRunLength = 0;
$spos = $pos + 4;
$limitpos = $spos + $length;
$uniqueStrings = $this->_GetInt4d( $this->data, $spos + 4 );
$spos += 8;
for ( $i = 0; $i < $uniqueStrings; $i ++ ) {
// Read in the number of characters
if ( $spos === $limitpos ) {
$opcode = ord( $this->data[ $spos ] ) | ord( $this->data[ $spos + 1 ] ) << 8;
$conlength = ord( $this->data[ $spos + 2 ] ) | ord( $this->data[ $spos + 3 ] ) << 8;
if ( $opcode !== 0x3c ) {
return - 1;
}
$spos += 4;
$limitpos = $spos + $conlength;
}
$numChars = ord( $this->data[ $spos ] ) | ( ord( $this->data[ $spos + 1 ] ) << 8 );
//echo "i = $i pos = $pos numChars = $numChars ";
$spos += 2;
$optionFlags = ord( $this->data[ $spos ] );
$spos ++;
$asciiEncoding = ( ( $optionFlags & 0x01 ) === 0 );
$extendedString = ( ( $optionFlags & 0x04 ) !== 0 );
// See if string contains formatting information
$richString = ( ( $optionFlags & 0x08 ) !== 0 );
if ( $richString ) {
// Read in the crun
$formattingRuns = ord( $this->data[ $spos ] ) | ( ord( $this->data[ $spos + 1 ] ) << 8 );
$spos += 2;
}
if ( $extendedString ) {
// Read in cchExtRst
$extendedRunLength = $this->_GetInt4d( $this->data, $spos );
$spos += 4;
}
$len = $asciiEncoding ? $numChars : $numChars * 2;
if ( $spos + $len < $limitpos ) {
$retstr = $this->_substr( $this->data, $spos, $len );
$spos += $len;
} else {
// found countinue
$retstr = $this->_substr( $this->data, $spos, $limitpos - $spos );
$bytesRead = $limitpos - $spos;
$charsLeft = $numChars - ( $asciiEncoding ? $bytesRead : ( $bytesRead / 2 ) );
$spos = $limitpos;
while ( $charsLeft > 0 ) {
$opcode = ord( $this->data[ $spos ] ) | ord( $this->data[ $spos + 1 ] ) << 8;
$conlength = ord( $this->data[ $spos + 2 ] ) | ord( $this->data[ $spos + 3 ] ) << 8;
if ( $opcode !== 0x3c ) {
return - 1;
}
$spos += 4;
$limitpos = $spos + $conlength;
$option = ord( $this->data[ $spos ] );
$spos ++;
if ( $asciiEncoding && ( $option === 0 ) ) {
$len = min( $charsLeft, $limitpos - $spos ); // min($charsLeft, $conlength);
$retstr .= $this->_substr( $this->data, $spos, $len );
$charsLeft -= $len;
$asciiEncoding = true;
} elseif ( ! $asciiEncoding && ( $option !== 0 ) ) {
$len = min( $charsLeft * 2, $limitpos - $spos ); // min($charsLeft, $conlength);
$retstr .= $this->_substr( $this->data, $spos, $len );
$charsLeft -= $len / 2;
$asciiEncoding = false;
} elseif ( ! $asciiEncoding && ( $option === 0 ) ) {
// Bummer - the string starts off as Unicode, but after the
// continuation it is in straightforward ASCII encoding
$len = min( $charsLeft, $limitpos - $spos ); // min($charsLeft, $conlength);
for ( $j = 0; $j < $len; $j ++ ) {
$retstr .= $this->data[ $spos + $j ] . chr( 0 );
}
$charsLeft -= $len;
$asciiEncoding = false;
} else {
$newstr = '';
for ( $j = 0, $len_retstr = $this->_strlen( $retstr ); $j < $len_retstr; $j ++ ) {
$newstr = $retstr[ $j ] . chr( 0 );
}
$retstr = $newstr;
$len = min( $charsLeft * 2, $limitpos - $spos ); // min($charsLeft, $conlength);
$retstr .= $this->_substr( $this->data, $spos, $len );
$charsLeft -= $len / 2;
$asciiEncoding = false;
//echo "Izavrat\n";
}
$spos += $len;
}
}
$retstr = $asciiEncoding ? $retstr : $this->_encodeUTF16( $retstr );
// echo "Str $i = $retstr\n";
if ( $richString ) {
$spos += 4 * $formattingRuns;
}
// For extended strings, skip over the extended string data
if ( $extendedString ) {
$spos += $extendedRunLength;
}
//if ($retstr == 'Derby'){
// echo "bb\n";
//}
$this->sst[] = $retstr;
}
/*$continueRecords = array();
while ($this->getNextCode() == Type_CONTINUE) {
$continueRecords[] = &$this->nextRecord();
}
//echo " 1 Type_SST\n";
$this->shareStrings = new SSTRecord($r, $continueRecords);
//print_r($this->shareStrings->strings);
*/
// echo 'SST read: '.($time_end-$time_start)."\n";
break;
case self::TYPE_FILEPASS:
return false;
break;
case self::TYPE_NAME:
//echo "Type_NAME\n";
break;
case self::TYPE_FORMAT:
$indexCode = ord( $this->data[ $pos + 4 ] ) | ord( $this->data[ $pos + 5 ] ) << 8;
if ( $version === self::BIFF8 ) {
$numchars = ord( $this->data[ $pos + 6 ] ) | ord( $this->data[ $pos + 7 ] ) << 8;
if ( ord( $this->data[ $pos + 8 ] ) === 0 ) {
$formatString = $this->_substr( $this->data, $pos + 9, $numchars );
} else {
$formatString = $this->_substr( $this->data, $pos + 9, $numchars * 2 );
}
} else {
$numchars = ord( $this->data[ $pos + 6 ] );
$formatString = $this->_substr( $this->data, $pos + 7, $numchars * 2 );
}
$this->formatRecords[ $indexCode ] = $formatString;
// echo "Type.FORMAT\n";
break;
case self::TYPE_XF:
$formatstr = '';
//global $dateFormats, $numberFormats;
$indexCode = ord( $this->data[ $pos + 6 ] ) | ord( $this->data[ $pos + 7 ] ) << 8;
//echo "\nType.XF ".count($this->formatRecords['xfrecords'])." $indexCode ";
if ( array_key_exists( $indexCode, $this->dateFormats ) ) {
//echo "isdate ".$dateFormats[$indexCode];
$this->formatRecords['xfrecords'][] = array(
'type' => 'date',
'format' => $this->dateFormats[ $indexCode ]
);
} elseif ( array_key_exists( $indexCode, $this->numberFormats ) ) {
//echo "isnumber ".$this->numberFormats[$indexCode];
$this->formatRecords['xfrecords'][] = array(
'type' => 'number',
'format' => $this->numberFormats[ $indexCode ]
);
} else {
$isdate = false;
if ( $indexCode > 0 ) {
if ( isset( $this->formatRecords[ $indexCode ] ) ) {
$formatstr = $this->formatRecords[ $indexCode ];
}
//echo '.other.';
// echo "\ndate-time=$formatstr=\n";
if ( $formatstr && preg_match( "/^[hmsday\/\-:\s]+$/i", $formatstr ) === 1 ) { // found day and time format
$isdate = true;
$formatstr = str_replace( array( 'mm', 'h' ), array( 'i', 'H' ), $formatstr );
//echo "\ndate-time $formatstr \n";
}
}
if ( $isdate ) {
$this->formatRecords['xfrecords'][] = array(
'type' => 'date',
'format' => $formatstr,
);
} else {
$this->formatRecords['xfrecords'][] = array(
'type' => 'other',
'format' => '',
'code' => $indexCode
);
}
}
//echo "\n";
break;
case self::TYPE_NINETEENFOUR:
//echo "Type.NINETEENFOUR\n";
$this->nineteenFour = ( ord( $this->data[ $pos + 4 ] ) === 1 );
break;
case self::TYPE_BOUNDSHEET:
//echo "Type.BOUNDSHEET\n";
$rec_offset = $this->_GetInt4d( $this->data, $pos + 4 );
// $rec_typeFlag = ord($this->_data[$pos + 8]);
// $rec_visibilityFlag = ord($this->_data[$pos + 9]);
$rec_length = ord( $this->data[ $pos + 10 ] );
$rec_name = '';
if ( $version === self::BIFF8 ) {
$chartype = ord( $this->data[ $pos + 11 ] );
if ( $chartype === 0 ) {
$rec_name = $this->_substr( $this->data, $pos + 12, $rec_length );
} else {
$rec_name = $this->_encodeUTF16( $this->_substr( $this->data, $pos + 12, $rec_length * 2 ) );
}
} elseif ( $version === self::BIFF7 ) {
$rec_name = $this->_substr( $this->data, $pos + 11, $rec_length );
}
$this->boundsheets[] = array(
'name' => $rec_name,
'offset' => $rec_offset
);
break;
}
//echo "Code = ".base_convert($r['code'],10,16)."\n";
$pos += $length + 4;
$code = ord( $this->data[ $pos ] ) | ord( $this->data[ $pos + 1 ] ) << 8;
$length = ord( $this->data[ $pos + 2 ] ) | ord( $this->data[ $pos + 3 ] ) << 8;
//$r = &$this->nextRecord();
//echo "1 Code = ".base_convert($r['code'],10,16)."\n";
}
foreach ( $this->boundsheets as $key => $val ) {
$this->sn = $key;
$this->_parsesheet( $val['offset'] );
}
return true;
}
public function _encodeUTF16( $string ) {
$result = $string;
if ( $this->defaultEncoding ) {
$result = mb_convert_encoding( $string, $this->defaultEncoding, 'UTF-16LE' );
}
return $result;
}
public function _parsesheet( $spos ) {
$cont = true;
// read BOF
// $code = ord($this->_data[$spos]) | ord($this->_data[$spos + 1]) << 8;
$length = ord( $this->data[ $spos + 2 ] ) | ord( $this->data[ $spos + 3 ] ) << 8;
$version = ord( $this->data[ $spos + 4 ] ) | ord( $this->data[ $spos + 5 ] ) << 8;
$substreamType = ord( $this->data[ $spos + 6 ] ) | ord( $this->data[ $spos + 7 ] ) << 8;
if ( ( $version !== self::BIFF8 ) && ( $version !== self::BIFF7 ) ) {
return - 1;
}
if ( $substreamType !== self::WORKSHEET ) {
return - 2;
}
//echo "Start parse code=".base_convert($code,10,16)." version=".base_convert($version,10,16)." substreamType=".base_convert($substreamType,10,16).""."\n";
$spos += $length + 4;
//var_dump($this->formatRecords);
//echo "code $code $length";
while ( $cont ) {
//echo "mem= ".memory_get_usage()."\n";
// $r = &$this->file->nextRecord();
$lowcode = ord( $this->data[ $spos ] );
if ( $lowcode === self::TYPE_EOF ) {
break;
}
$code = $lowcode | ord( $this->data[ $spos + 1 ] ) << 8;
$length = ord( $this->data[ $spos + 2 ] ) | ord( $this->data[ $spos + 3 ] ) << 8;
$spos += 4;
$this->sheets[ $this->sn ]['maxrow'] = $this->rowoffset - 1;
$this->sheets[ $this->sn ]['maxcol'] = $this->colOffset - 1;
//echo "Code=".base_convert($code,10,16)." $code\n";
unset( $this->recType );
$this->multiplier = 1; // need for format with %
switch ( $code ) {
case self::TYPE_DIMENSION:
//echo 'Type_DIMENSION ';
if ( ! isset( $this->numRows ) ) {
if ( ( $length === 10 ) || ( $version === self::BIFF7 ) ) {
$this->sheets[ $this->sn ]['numRows'] = ord( $this->data[ $spos + 2 ] ) | ord( $this->data[ $spos + 3 ] ) << 8;
$this->sheets[ $this->sn ]['numCols'] = ord( $this->data[ $spos + 6 ] ) | ord( $this->data[ $spos + 7 ] ) << 8;
} else {
$this->sheets[ $this->sn ]['numRows'] = ord( $this->data[ $spos + 4 ] ) | ord( $this->data[ $spos + 5 ] ) << 8;
$this->sheets[ $this->sn ]['numCols'] = ord( $this->data[ $spos + 10 ] ) | ord( $this->data[ $spos + 11 ] ) << 8;
}
}
//echo 'numRows '.$this->numRows.' '.$this->numCols."\n";
break;
case self::TYPE_MERGEDCELLS:
$cellRanges = ord( $this->data[ $spos ] ) | ord( $this->data[ $spos + 1 ] ) << 8;
for ( $i = 0; $i < $cellRanges; $i ++ ) {
$fr = ord( $this->data[ $spos + 8 * $i + 2 ] ) | ord( $this->data[ $spos + 8 * $i + 3 ] ) << 8;
$lr = ord( $this->data[ $spos + 8 * $i + 4 ] ) | ord( $this->data[ $spos + 8 * $i + 5 ] ) << 8;
$fc = ord( $this->data[ $spos + 8 * $i + 6 ] ) | ord( $this->data[ $spos + 8 * $i + 7 ] ) << 8;
$lc = ord( $this->data[ $spos + 8 * $i + 8 ] ) | ord( $this->data[ $spos + 8 * $i + 9 ] ) << 8;
//$this->sheets[$this->sn]['mergedCells'][] = array($fr + 1, $fc + 1, $lr + 1, $lc + 1);
if ( $lr - $fr > 0 ) {
$this->sheets[ $this->sn ]['cellsInfo'][ $fr + 1 ][ $fc + 1 ]['rowspan'] = $lr - $fr + 1;
}
if ( $lc - $fc > 0 ) {
$this->sheets[ $this->sn ]['cellsInfo'][ $fr + 1 ][ $fc + 1 ]['colspan'] = $lc - $fc + 1;
}
}
//echo "Merged Cells $cellRanges $lr $fr $lc $fc\n";
break;
case self::TYPE_RK:
case self::TYPE_RK2:
//echo 'self::TYPE_RK'."\n";
$row = ord( $this->data[ $spos ] ) | ord( $this->data[ $spos + 1 ] ) << 8;
$column = ord( $this->data[ $spos + 2 ] ) | ord( $this->data[ $spos + 3 ] ) << 8;
$rknum = $this->_GetInt4d( $this->data, $spos + 6 );
$numValue = $this->_GetIEEE754( $rknum );
//echo $numValue." ";
if ( $this->isDate( $spos ) ) {
list( $string, $raw ) = $this->createDate( $numValue );
} else {
$raw = $numValue;
if ( isset( $this->columnsFormat[ $column + 1 ] ) ) {
$this->curFormat = $this->columnsFormat[ $column + 1 ];
}
$string = sprintf( $this->curFormat, $numValue * $this->multiplier );
//$this->addcell(RKRecord($r));
}
$this->addcell( $row, $column, $string, $raw );
//echo "Type_RK $row $column $string $raw {$this->curformat}\n";
break;
case self::TYPE_LABELSST:
$row = ord( $this->data[ $spos ] ) | ord( $this->data[ $spos + 1 ] ) << 8;
$column = ord( $this->data[ $spos + 2 ] ) | ord( $this->data[ $spos + 3 ] ) << 8;
// $xfindex = ord($this->_data[$spos + 4]) | ord($this->_data[$spos + 5]) << 8;
$index = $this->_GetInt4d( $this->data, $spos + 6 );
//var_dump($this->sst);
$this->addcell( $row, $column, $this->sst[ $index ] );
//echo "LabelSST $row $column $string\n";
break;
case self::TYPE_MULRK:
$row = ord( $this->data[ $spos ] ) | ord( $this->data[ $spos + 1 ] ) << 8;
$colFirst = ord( $this->data[ $spos + 2 ] ) | ord( $this->data[ $spos + 3 ] ) << 8;
$colLast = ord( $this->data[ $spos + $length - 2 ] ) | ord( $this->data[ $spos + $length - 1 ] ) << 8;
$columns = $colLast - $colFirst + 1;
$tmppos = $spos + 4;
for ( $i = 0; $i < $columns; $i ++ ) {
$numValue = $this->_GetIEEE754( $this->_GetInt4d( $this->data, $tmppos + 2 ) );
if ( $this->isDate( $tmppos - 4 ) ) {
list( $string, $raw ) = $this->createDate( $numValue );
} else {
$raw = $numValue;
if ( isset( $this->columnsFormat[ $colFirst + $i + 1 ] ) ) {
$this->curFormat = $this->columnsFormat[ $colFirst + $i + 1 ];
}
$string = sprintf( $this->curFormat, $numValue * $this->multiplier );
}
//$rec['rknumbers'][$i]['xfindex'] = ord($rec['data'][$pos]) | ord($rec['data'][$pos+1]) << 8;
$tmppos += 6;
$this->addcell( $row, $colFirst + $i, $string, $raw );
//echo "MULRK $row ".($colFirst + $i)." $string\n";
}
//MulRKRecord($r);
// Get the individual cell records from the multiple record
//$num = ;
break;
case self::TYPE_NUMBER:
$row = ord( $this->data[ $spos ] ) | ord( $this->data[ $spos + 1 ] ) << 8;
$column = ord( $this->data[ $spos + 2 ] ) | ord( $this->data[ $spos + 3 ] ) << 8;
$tmp = unpack( 'ddouble', $this->_substr( $this->data, $spos + 6, 8 ) ); // It machine machine dependent
if ( $this->isDate( $spos ) ) {
list( $string, $raw ) = $this->createDate( $tmp['double'] );
// $this->addcell(DateRecord($r, 1));
} else {
//$raw = $tmp[''];
if ( isset( $this->columnsFormat[ $column + 1 ] ) ) {
$this->curFormat = $this->columnsFormat[ $column + 1 ];
}
$raw = $this->createNumber( $spos );
$string = sprintf( $this->curFormat, $raw * $this->multiplier );
// $this->addcell(NumberRecord($r));
}
$this->addcell( $row, $column, $string, $raw );
//echo "Number $row $column $string\n";
break;
case self::TYPE_FORMULA:
case self::TYPE_FORMULA2:
$row = ord( $this->data[ $spos ] ) | ord( $this->data[ $spos + 1 ] ) << 8;
$column = ord( $this->data[ $spos + 2 ] ) | ord( $this->data[ $spos + 3 ] ) << 8;
/*
$byte6 = ord($this->_data[$spos + 6]);
$byte12 = ord($this->_data[$spos + 12]);
$byte13 = ord($this->_data[$spos + 13]);
if ( $byte6 === 0 && $byte12 === 255 && $byte13 === 255 ) {
//String formula. Result follows in a STRING record
//echo "FORMULA $row $column Formula with a string<br>\n";
} else if ($byte6 === 1 && $byte12 === 255 && $byte13 === 255 ) {
//Boolean formula. Result is in +2; 0=false,1=true
} else if ($byte6 === 2 && $byte12 === 255 && $byte13 === 255) {
//Error formula. Error code is in +2;
} else if ( $byte6 === 3 && $byte12 === 255 && $byte13 === 255) {
//Formula result is a null string.
*/
if ( ! ( ord( $this->data[ $spos + 6 ] ) < 4 && ord( $this->data[ $spos + 12 ] ) === 255 && ord( $this->data[ $spos + 13 ] ) === 255 ) ) {
// result is a number, so first 14 bytes are just like a _NUMBER record
$tmp = unpack( 'ddouble', $this->_substr( $this->data, $spos + 6, 8 ) ); // It machine machine dependent
if ( $this->isDate( $spos ) ) {
list( $string, $raw ) = $this->createDate( $tmp['double'] );
// $this->addcell(DateRecord($r, 1));
} else {
//$raw = $tmp[''];
if ( isset( $this->columnsFormat[ $column + 1 ] ) ) {
$this->curFormat = $this->columnsFormat[ $column + 1 ];
}
$raw = $this->createNumber( $spos );
$string = sprintf( $this->curFormat, $raw * $this->multiplier );
// $this->addcell(NumberRecord($r));
}
$this->addcell( $row, $column, $string, $raw );
//echo "Number $row $column $string\n";
}
break;
case self::TYPE_BOOLERR:
$row = ord( $this->data[ $spos ] ) | ord( $this->data[ $spos + 1 ] ) << 8;
$column = ord( $this->data[ $spos + 2 ] ) | ord( $this->data[ $spos + 3 ] ) << 8;
$string = ord( $this->data[ $spos + 6 ] );
$this->addcell( $row, $column, $string );
//echo 'Type_BOOLERR '."\n";
break;
case self::TYPE_ROW:
case self::TYPE_DBCELL:
case self::TYPE_MULBLANK:
break;
case self::TYPE_LABEL:
$row = ord( $this->data[ $spos ] ) | ord( $this->data[ $spos + 1 ] ) << 8;
$column = ord( $this->data[ $spos + 2 ] ) | ord( $this->data[ $spos + 3 ] ) << 8;
$this->addcell( $row, $column, $this->_substr( $this->data, $spos + 8, ord( $this->data[ $spos + 6 ] ) | ord( $this->data[ $spos + 7 ] ) << 8 ) );
// $this->addcell(LabelRecord($r));
break;
case self::TYPE_EOF:
$cont = false;
break;
default:
//echo ' unknown :'.base_convert($r['code'],10,16)."\n";
break;
}
$spos += $length;
}
if ( ! isset( $this->sheets[ $this->sn ]['numRows'] ) ) {
$this->sheets[ $this->sn ]['numRows'] = $this->sheets[ $this->sn ]['maxrow'];
}
if ( ! isset( $this->sheets[ $this->sn ]['numCols'] ) ) {
$this->sheets[ $this->sn ]['numCols'] = $this->sheets[ $this->sn ]['maxcol'];
}
return true;
}
//}}}
//{{{ createDate()
public function _GetIEEE754( $rknum ) {
if ( ( $rknum & 0x02 ) !== 0 ) {
$value = $rknum >> 2;
} else {
//mmp
// first comment out the previously existing 7 lines of code here
// $tmp = unpack("d", pack("VV", 0, ($rknum & 0xfffffffc)));
// //$value = $tmp[''];
// if (array_key_exists(1, $tmp)) {
// $value = $tmp[1];
// } else {
// $value = $tmp[''];
// }
// I got my info on IEEE754 encoding from
// http://research.microsoft.com/~hollasch/cgindex/coding/ieeefloat.html
// The RK format calls for using only the most significant 30 bits of the
// 64 bit floating point value. The other 34 bits are assumed to be 0
// So, we use the upper 30 bits of $rknum as follows...
$sign = ( $rknum & 0x80000000 ) >> 31;
$exp = ( $rknum & 0x7ff00000 ) >> 20;
$mantissa = ( 0x100000 | ( $rknum & 0x000ffffc ) );
$value = $mantissa / pow( 2, 20 - ( $exp - 1023 ) );
if ( $sign ) {
$value = - 1 * $value;
}
//end of changes by mmp
}
if ( ( $rknum & 0x01 ) !== 0 ) {
$value /= 100;
}
return $value;
}
public function isDate( $spos ) {
//$xfindex = GetInt2d(, 4);
$xfindex = ord( $this->data[ $spos + 4 ] ) | ord( $this->data[ $spos + 5 ] ) << 8;
//echo 'check is date '.$xfindex.' '.$this->formatRecords['xfrecords'][$xfindex]['type']."\n";
//var_dump($this->formatRecords['xfrecords'][$xfindex]);
if ( $this->formatRecords['xfrecords'][ $xfindex ]['type'] === 'date' ) {
$this->curFormat = $this->formatRecords['xfrecords'][ $xfindex ]['format'];
$this->recType = 'date';
return true;
}
if ( $this->formatRecords['xfrecords'][ $xfindex ]['type'] === 'number' ) {
$this->curFormat = $this->formatRecords['xfrecords'][ $xfindex ]['format'];
$this->recType = 'number';
if ( ( $xfindex === 0x9 ) || ( $xfindex === 0xa ) ) {
$this->multiplier = 100;
}
} else {
$this->curFormat = $this->defaultFormat;
$this->recType = 'unknown';
}
return false;
}
/**
* Convert the raw Excel date into a human readable format
*
* Dates in Excel are stored as number of seconds from an epoch. On
* Windows, the epoch is 30/12/1899 and on Mac it's 01/01/1904
*
* @param integer $timevalue The raw Excel value to convert
*
* @return array First element is the converted date, the second element is number a unix timestamp
*/
public function createDate( $timevalue ) {
// $offset = ($timeoffset===null)? date('Z') : $timeoffset * 3600;
if ($timevalue > 1) {
$timevalue -= ( $this->nineteenFour ? 24107 : 25569 );
}
$ts = round($timevalue * 24 * 3600);
$string = $this->datetimeFormat ? gmdate( $this->datetimeFormat, $ts ) : gmdate( $this->curFormat, $ts );
return array( $string, $ts );
}
public function addcell( $row, $col, $string, $raw = '' ) {
//echo "ADD cel $row-$col $string\n";
$this->sheets[ $this->sn ]['maxrow'] = max( $this->sheets[ $this->sn ]['maxrow'], $row + $this->rowoffset );
$this->sheets[ $this->sn ]['maxcol'] = max( $this->sheets[ $this->sn ]['maxcol'], $col + $this->colOffset );
$this->sheets[ $this->sn ]['cells'][ $row + $this->rowoffset ][ $col + $this->colOffset ] = $string;
if ( $raw ) {
$this->sheets[ $this->sn ]['cellsInfo'][ $row + $this->rowoffset ][ $col + $this->colOffset ]['raw'] = $raw;
}
if ( isset( $this->recType ) ) {
$this->sheets[ $this->sn ]['cellsInfo'][ $row + $this->rowoffset ][ $col + $this->colOffset ]['type'] = $this->recType;
}
}
public function createNumber( $spos ) {
$rknumhigh = $this->_GetInt4d( $this->data, $spos + 10 );
$rknumlow = $this->_GetInt4d( $this->data, $spos + 6 );
//for ($i=0; $i<8; $i++) { echo ord($this->_data[$i+$spos+6]) . " "; } echo "<br>";
$sign = ( $rknumhigh & 0x80000000 ) >> 31;
$exp = ( $rknumhigh & 0x7ff00000 ) >> 20;
$mantissa = ( 0x100000 | ( $rknumhigh & 0x000fffff ) );
$mantissalow1 = ( $rknumlow & 0x80000000 ) >> 31;
$mantissalow2 = ( $rknumlow & 0x7fffffff );
$value = $mantissa / pow( 2, 20 - ( $exp - 1023 ) );
if ( $mantissalow1 !== 0 ) {
$value += 1 / pow( 2, 21 - ( $exp - 1023 ) );
}
$value += $mantissalow2 / pow( 2, 52 - ( $exp - 1023 ) );
//echo "Sign = $sign, Exp = $exp, mantissahighx = $mantissa, mantissalow1 = $mantissalow1, mantissalow2 = $mantissalow2<br>\n";
if ( $sign ) {
$value = - 1 * $value;
}
return $value;
}
/**
* Set the encoding method
*
* @param string $encoding Encoding to use
*
* @access public
*/
public function setOutputEncoding( $encoding ) {
$this->defaultEncoding = $encoding;
}
public function setRowColOffset( $iOffset ) {
$this->rowoffset = $iOffset;
$this->colOffset = $iOffset;
}
/**
* Set the default number format
*
* @access public
*
* @param string $sFormat Default format
*/
public function setDefaultFormat( $sFormat ) {
$this->defaultFormat = $sFormat;
}
/**
* Force a column to use a certain format
*
* @access public
*
* @param integer $column Column number
* @param string $sFormat Format
*/
public function setColumnFormat( $column, $sFormat ) {
$this->columnsFormat[ $column ] = $sFormat;
}
private function _strlen( $str ) {
return (ini_get('mbstring.func_overload') & 2) ? mb_strlen($str , '8bit') : strlen($str);
}
private function _strpos( $haystack, $needle, $offset = 0 ) {
return (ini_get('mbstring.func_overload') & 2) ? mb_strpos( $haystack, $needle, $offset , '8bit') : strpos($haystack, $needle, $offset);
}
private function _substr( $str, $start, $length = null ) {
return (ini_get('mbstring.func_overload') & 2) ? mb_substr( $str, $start, ($length === null) ? mb_strlen($str,'8bit') : $length, '8bit') : substr($str, $start, ($length === null) ? strlen($str) : $length );
}
}
/* vim: set expandtab tabstop=4 shiftwidth=4 softtabstop=4: */