ソースを参照

源码提交

the source code to commit
Janson Leung 10 年 前
コミット
62f017cfe4

+ 304 - 0
PHPExcelReader/PHPExcelReader.php

@@ -0,0 +1,304 @@
+<?php
+/**
+ * PHPExcelReader class
+ *
+ * @version 1.0.0
+ * @author Janson
+ */
+class PHPExcelReader implements SeekableIterator, Countable {
+	const TYPE_XLSX = 'XLSX';
+	const TYPE_XLS = 'XLS';
+	const TYPE_CSV = 'CSV';
+	const TYPE_ODS = 'ODS';
+
+	private $options = array(
+		'Delimiter' => '',
+		'Enclosure' => '"'
+	);
+
+	private $index = 0;
+	private $handle = array();
+	private $type = false;
+
+	/**
+	 * @param string Path to file
+	 * @param string Original filename (in case of an uploaded file), used to determine file type, optional
+	 * @param string MIME type from an upload, used to determine file type, optional
+	 */
+	public function __construct($filePath, $originalFileName = false, $mimeType = false) {
+		if ( ! is_readable($filePath)) {
+			throw new Exception('SpreadsheetReader: File (' . $filePath . ') not readable');
+		}
+
+		$defaultTimeZone = @date_default_timezone_get();
+		if ($defaultTimeZone)	{
+			date_default_timezone_set($defaultTimeZone);
+		}
+
+		// Checking the other parameters for correctness
+		// This should be a check for string but we're lenient
+		if ( ! empty($originalFileName) && ! is_scalar($originalFileName)) {
+			throw new Exception('SpreadsheetReader: Original file (2nd parameter) path is not a string or a scalar value.');
+		}
+		if ( ! empty($mimeType) && ! is_scalar($mimeType)) {
+			throw new Exception('SpreadsheetReader: Mime type (3nd parameter) path is not a string or a scalar value.');
+		}
+
+		// 1. Determine type
+		if ( ! $originalFileName) {
+			$originalFileName = $filePath;
+		}
+
+		$Extension = strtolower(pathinfo($originalFileName, PATHINFO_EXTENSION));
+		if($mimeType) {
+			switch ($mimeType) {
+				case 'text/csv':
+				case 'text/comma-separated-values':
+				case 'text/plain':
+					$this->type = self::TYPE_CSV;
+					break;
+				case 'application/vnd.ms-excel':
+				case 'application/msexcel':
+				case 'application/x-msexcel':
+				case 'application/x-ms-excel':
+				case 'application/vnd.ms-excel':
+				case 'application/x-excel':
+				case 'application/x-dos_ms_excel':
+				case 'application/xls':
+				case 'application/xlt':
+				case 'application/x-xls':
+					// Excel does weird stuff
+					if (in_array($Extension, array('csv', 'tsv', 'txt'))) {
+						$this->type = self::TYPE_CSV;
+					}
+					else {
+						$this->type = self::TYPE_XLS;
+					}
+					break;
+				case 'application/vnd.oasis.opendocument.spreadsheet':
+				case 'application/vnd.oasis.opendocument.spreadsheet-template':
+					$this->type = self::TYPE_ODS;
+					break;
+				case 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet':
+				case 'application/vnd.openxmlformats-officedocument.spreadsheetml.template':
+				case 'application/xlsx':
+				case 'application/xltx':
+					$this->type = self::TYPE_XLSX;
+					break;
+				case 'application/xml':
+					// Excel 2004 xml format uses this
+					break;
+			}
+		}
+
+		if ( ! $this->type)	{
+			switch ($Extension) {
+				case 'xlsx':
+				case 'xltx': // XLSX template
+				case 'xlsm': // Macro-enabled XLSX
+				case 'xltm': // Macro-enabled XLSX template
+					$this->type = self::TYPE_XLSX;
+					break;
+				case 'xls':
+				case 'xlt':
+					$this->type = self::TYPE_XLS;
+					break;
+				case 'ods':
+				case 'odt':
+					$this->type = self::TYPE_ODS;
+					break;
+				default:
+					$this->type = self::TYPE_CSV;
+					break;
+			}
+		}
+
+		// Pre-checking XLS files, in case they are renamed CSV or XLSX files
+		if ($this->type == self::TYPE_XLS) {
+			self::Load(self::TYPE_XLS);
+			$this->handle = new SpreadsheetReader_XLS($filePath);
+			if ($this->handle->error) {
+				$this->handle->__destruct();
+
+				if (is_resource($Ziphandle = zip_open($filePath))) {
+					$this->type = self::TYPE_XLSX;
+					zip_close($Ziphandle);
+				}
+				else {
+					$this->type = self::TYPE_CSV;
+				}
+			}
+		}
+
+		// 2. Create handle
+		switch ($this->type) {
+			case self::TYPE_XLSX:
+				self::Load(self::TYPE_XLSX);
+				$this->handle = new SpreadsheetReader_XLSX($filePath);
+				break;
+			case self::TYPE_CSV:
+				self::Load(self::TYPE_CSV);
+				$this->handle = new SpreadsheetReader_CSV($filePath, $this->options, 'GBK');
+				break;
+			case self::TYPE_XLS:
+				// Everything already happens above
+				break;
+			case self::TYPE_ODS:
+				self::Load(self::TYPE_ODS);
+				$this->handle = new SpreadsheetReader_ODS($filePath, $this->options);
+				break;
+		}
+	}
+	
+	/**
+	 * get the type of file
+	 * @return string
+	 */
+	public function getType() {
+		return $this->type;
+	}
+	
+	/**
+	 * Gets information about separate sheets in the given file
+	 *
+	 * @return array Associative array where key is sheet index and value is sheet name
+	 */
+	public function Sheets() {
+		return $this->handle->Sheets();
+	}
+
+	/**
+	 * Changes the current sheet to another from the file.
+	 *	Note that changing the sheet will rewind the file to the beginning, even if
+	 *	the current sheet index is provided.
+	 *
+	 * @param int Sheet index
+	 *
+	 * @return bool True if sheet could be changed to the specified one,
+	 *	false if not (for example, if incorrect index was provided.
+	 */
+	public function ChangeSheet($index) {
+		return $this->handle->ChangeSheet($index);
+	}
+
+	/**
+	 * Autoloads the required class for the particular spreadsheet type
+	 *
+	 * @param TYPE_* Spreadsheet type, one of TYPE_* constants of this class
+	 */
+	private static function Load($type) {
+		if ( ! in_array($type, array(self::TYPE_XLSX, self::TYPE_XLS, self::TYPE_CSV, self::TYPE_ODS))) {
+			throw new Exception('SpreadsheetReader: Invalid type (' . $type . ')');
+		}
+
+		// 2nd parameter is to prevent autoloading for the class.
+		// If autoload works, the require line is unnecessary, if it doesn't, it ends badly.
+		if ( ! class_exists('SpreadsheetReader_' . $type, false)) {
+			require(dirname(__FILE__) . DIRECTORY_SEPARATOR . 'SpreadsheetReader' . DIRECTORY_SEPARATOR . 'SpreadsheetReader_' . $type . '.php');
+		}
+	}
+
+	// !Iterator interface methods
+
+	/** 
+	 * Rewind the Iterator to the first element.
+	 * Similar to the reset() function for arrays in PHP
+	 */ 
+	public function rewind() {
+		$this->index = 0;
+		if ($this->handle) {
+			$this->handle->rewind();
+		}
+	}
+
+	/** 
+	 * Return the current element.
+	 * Similar to the current() function for arrays in PHP
+	 *
+	 * @return mixed current element from the collection
+	 */
+	public function current() {
+		if ($this->handle) {
+			return $this->handle->current();
+		}
+		return null;
+	}
+
+	/** 
+	 * Move forward to next element. 
+	 * Similar to the next() function for arrays in PHP 
+	 */ 
+	public function next() {
+		if ($this->handle) {
+			$this->index++;
+			return $this->handle->next();
+		}
+		return null;
+	}
+
+	/** 
+	 * Return the identifying key of the current element.
+	 * Similar to the key() function for arrays in PHP
+	 *
+	 * @return mixed either an integer or a string
+	 */ 
+	public function key() {
+		if ($this->handle) {
+			return $this->handle->key();
+		}
+		return null;
+	}
+
+	/** 
+	 * Check if there is a current element after calls to rewind() or next().
+	 * Used to check if we've iterated to the end of the collection
+	 *
+	 * @return boolean FALSE if there's nothing more to iterate over
+	 */ 
+	public function valid()	{
+		if ($this->handle) {
+			return $this->handle->valid();
+		}
+		return false;
+	}
+
+	/**
+	 * total of file number
+	 * return int
+	 */
+	public function count()	{
+		if ($this->handle) {
+			return $this->handle->count();
+		}
+		return 0;
+	}
+
+	/**
+	 * Method for SeekableIterator interface. Takes a posiiton and traverses the file to that position
+	 * The value can be retrieved with a `current()` call afterwards.
+	 *
+	 * @param int position in file
+	 */
+	public function seek($position)	{
+		if ( ! $this->handle) {
+			throw new OutOfBoundsException('SpreadsheetReader: No file opened');
+		}
+
+		$Currentindex = $this->handle->key();
+		if ($Currentindex != $position) {
+			if ($position < $Currentindex || is_null($Currentindex) || $position == 0) {
+				$this->rewind();
+			}
+
+			while ($this->handle->valid() && ($position > $this->handle->key())) {
+				$this->handle->next();
+			}
+
+			if ( ! $this->handle->valid()) {
+				throw new OutOfBoundsException('SpreadsheetError: position ' . $position . ' not found');
+			}
+		}
+
+		return null;
+	}
+}

+ 215 - 0
PHPExcelReader/SpreadsheetReader/OLERead.php

@@ -0,0 +1,215 @@
+<?php
+
+defined('IDENTIFIER_OLE') ||
+	define('IDENTIFIER_OLE', pack('CCCCCCCC', 0xd0, 0xcf, 0x11, 0xe0, 0xa1, 0xb1, 0x1a, 0xe1));
+
+class PHPExcel_Shared_OLERead {
+	private $data = '';
+	
+	const IDENTIFIER_OLE 					= IDENTIFIER_OLE;	// OLE identifier
+	const BIG_BLOCK_SIZE					= 0x200;
+	const SMALL_BLOCK_SIZE					= 0x40;				// Size of a short sector = 64 bytes
+	const PROPERTY_STORAGE_BLOCK_SIZE		= 0x80;				// Size of a directory entry always = 128 bytes
+	const SMALL_BLOCK_THRESHOLD				= 0x1000;			// Minimum size of a standard stream = 4096 bytes, streams smaller than this are stored as short streams
+	
+	// header offsets
+	const NUM_BIG_BLOCK_DEPOT_BLOCKS_POS	= 0x2c;
+	const ROOT_START_BLOCK_POS				= 0x30;
+	const SMALL_BLOCK_DEPOT_BLOCK_POS		= 0x3c;
+	const EXTENSION_BLOCK_POS				= 0x44;
+	const NUM_EXTENSION_BLOCK_POS			= 0x48;
+	const BIG_BLOCK_DEPOT_BLOCKS_POS		= 0x4c;
+	
+	// property storage offsets (directory offsets)
+	const SIZE_OF_NAME_POS					= 0x40;
+	const TYPE_POS							= 0x42;
+	const START_BLOCK_POS					= 0x74;
+	const SIZE_POS							= 0x78;
+	
+	public $error = false;
+	public $workbook = null;
+	public $summaryInformation = null;
+	public $documentSummaryInformation = null;
+	
+	public function read($file){
+		if( ! is_readable($file)) {
+			throw new Exception('SpreadsheetReader_XLS: File not readable (' . $file . ')');
+		}
+		
+		$this->data = file_get_contents($file);
+		if( ! $this->data || substr($this->data, 0, 8) != self::IDENTIFIER_OLE){
+			$this->error = true;
+			return false;
+		}
+		
+		$this->numBigBlockDepotBlocks = self::_GetInt4d($this->data, self::NUM_BIG_BLOCK_DEPOT_BLOCKS_POS);
+		$this->rootStartBlock = self::_GetInt4d($this->data, self::ROOT_START_BLOCK_POS);
+		$this->sbdStartBlock = self::_GetInt4d($this->data, self::SMALL_BLOCK_DEPOT_BLOCK_POS);
+		$this->extensionBlock = self::_GetInt4d($this->data, self::EXTENSION_BLOCK_POS);
+		$this->numExtensionBlocks = self::_GetInt4d($this->data, self::NUM_EXTENSION_BLOCK_POS);
+		
+		$bigBlockDepotBlocks = array();
+		$pos = self::BIG_BLOCK_DEPOT_BLOCKS_POS;
+		$bbdBlocks = $this->numExtensionBlocks == 0 ? $this->numBigBlockDepotBlocks : (self::BIG_BLOCK_SIZE - self::BIG_BLOCK_DEPOT_BLOCKS_POS) / 4;
+		for ($i = 0; $i < $bbdBlocks; ++$i) {
+			$bigBlockDepotBlocks[$i] = self::_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] = self::_GetInt4d($this->data, $pos);
+				$pos += 4;
+			}
+		
+			$bbdBlocks += $blocksToRead;
+			if ($bbdBlocks < $this->numBigBlockDepotBlocks) {
+				$this->extensionBlock = self::_GetInt4d($this->data, $pos);
+			}
+		}
+		
+		$pos = 0;
+		$this->bigBlockChain = '';
+		$bbs = self::BIG_BLOCK_SIZE / 4;
+		for ($i = 0; $i < $this->numBigBlockDepotBlocks; ++$i) {
+			$pos = ($bigBlockDepotBlocks[$i] + 1) * self::BIG_BLOCK_SIZE;
+		
+			$this->bigBlockChain .= substr($this->data, $pos, 4*$bbs);
+			$pos += 4*$bbs;
+		}
+		
+		$pos = 0;
+		$sbdBlock = $this->sbdStartBlock;
+		$this->smallBlockChain = '';
+		while ($sbdBlock != -2) {
+			$pos = ($sbdBlock + 1) * self::BIG_BLOCK_SIZE;
+			$this->smallBlockChain .= substr($this->data, $pos, 4*$bbs);
+			$pos += 4*$bbs;
+			$sbdBlock = self::_GetInt4d($this->bigBlockChain, 4*$sbdBlock);
+		}
+		
+		$block = $this->rootStartBlock;				// read the directory stream
+		$this->entry = $this->_readData($block);
+		
+		$this->_readPropertySets();
+	}
+	
+	/**
+	 * Extract binary stream data
+	 *
+	 * @return string
+	 */
+	public function getStream($stream) {
+		if ($stream === NULL) {
+			return null;
+		}
+	
+		$streamData = '';
+		if ($this->props[$stream]['size'] < self::SMALL_BLOCK_THRESHOLD) {
+			$rootdata = $this->_readData($this->props[$this->rootentry]['startBlock']);
+			$block = $this->props[$stream]['startBlock'];
+	
+			while ($block != -2) {
+				$pos = $block * self::SMALL_BLOCK_SIZE;
+				$streamData .= substr($rootdata, $pos, self::SMALL_BLOCK_SIZE);
+				$block = self::_GetInt4d($this->smallBlockChain, $block*4);
+			}
+		} 
+		else {
+			$numBlocks = $this->props[$stream]['size'] / self::BIG_BLOCK_SIZE;
+			if ($this->props[$stream]['size'] % self::BIG_BLOCK_SIZE != 0) {
+				++$numBlocks;
+			}
+	
+			if($numBlocks){
+				$block = $this->props[$stream]['startBlock'];
+		
+				while ($block != -2) {
+					$pos = ($block + 1) * self::BIG_BLOCK_SIZE;
+					$streamData .= substr($this->data, $pos, self::BIG_BLOCK_SIZE);
+					$block = self::_GetInt4d($this->bigBlockChain, $block*4);
+				}
+			}
+		}
+		
+		return $streamData;
+	}
+	
+	/**
+	 * Read a standard stream (by joining sectors using information from SAT)
+	 *
+	 * @param int $bl Sector ID where the stream starts
+	 * @return string Data for standard stream
+	 */
+	private function _readData($block) {
+		$data = '';
+	
+		while ($block != -2) {
+			$pos = ($block + 1) * self::BIG_BLOCK_SIZE;
+			$data .= substr($this->data, $pos, self::BIG_BLOCK_SIZE);
+			$block = self::_GetInt4d($this->bigBlockChain, 4*$block);
+		}
+		return $data;
+	}
+	
+	/**
+	 * Read entries in the directory stream.
+	 */
+	private function _readPropertySets() {
+		$offset = 0;
+	
+		$entryLen = strlen($this->entry);		// loop through entires, each entry is 128 bytes
+		while ($offset < $entryLen) {
+			$data = substr($this->entry, $offset, self::PROPERTY_STORAGE_BLOCK_SIZE);							// entry data (128 bytes)
+			$nameSize = ord($data[self::SIZE_OF_NAME_POS]) | (ord($data[self::SIZE_OF_NAME_POS + 1]) << 8);		// size in bytes of name
+			$name = str_replace("\x00", "", substr($data, 0, $nameSize));
+			$this->props[] = array (
+				'name' 		 =>	 $name,
+				'type' 		 =>	 ord($data[self::TYPE_POS]),					// type of entry
+				'size' 		 =>	 self::_GetInt4d($data, self::SIZE_POS),
+				'startBlock' =>	 self::_GetInt4d($data, self::START_BLOCK_POS)
+			);
+	
+			$upName = strtoupper($name);										// tmp helper to simplify checks
+			if (($upName === 'WORKBOOK') || ($upName === 'BOOK')) {				// Workbook directory entry (BIFF5 uses Book, BIFF8 uses Workbook)
+				$this->workbook = count($this->props) - 1;
+			}
+			else if ( $upName === 'ROOT ENTRY' || $upName === 'R') {
+				$this->rootentry = count($this->props) - 1;						// Root entry
+			}
+			
+			if ($name == chr(5) . 'SummaryInformation') {
+				$this->summaryInformation = count($this->props) - 1;			// Summary information
+			}
+	
+			if ($name == chr(5) . 'DocumentSummaryInformation') {
+				$this->documentSummaryInformation = count($this->props) - 1;	// Additional Document Summary information
+			}
+	
+			$offset += self::PROPERTY_STORAGE_BLOCK_SIZE;
+		}
+	
+	}
+	
+	/**
+	 * Read 4 bytes of data at specified position
+	 * FIX: represent numbers correctly on 64-bit system. Hacked by Andreas Rehm 2006 to ensure correct result of the <<24 block on 32 and 64bit systems
+	 * http://sourceforge.net/tracker/index.php?func=detail&aid=1487372&group_id=99160&atid=623334
+	 * 
+	 * @param string $data
+	 * @param int $pos
+	 * @return int
+	 */
+	private static function _GetInt4d($data, $pos){
+		$_or_24 = ord($data[$pos + 3]);
+		if ($_or_24 >= 128) {
+			$_ord_24 = -abs((256 - $_or_24) << 24);		// negative number
+		} else {
+			$_ord_24 = ($_or_24 & 127) << 24;
+		}
+		return ord($data[$pos]) | (ord($data[$pos + 1]) << 8) | (ord($data[$pos + 2]) << 16) | $_ord_24;
+	}
+}

+ 1361 - 0
PHPExcelReader/SpreadsheetReader/SpreadsheetReader.php

@@ -0,0 +1,1361 @@
+<?php
+
+class Spreadsheet_Excel_Reader {
+	// ParseXL definitions
+	const XLS_BIFF8						= 0x0600;
+	const XLS_BIFF7						= 0x0500;
+	const XLS_WorkbookGlobals			= 0x0005;
+	const XLS_Worksheet					= 0x0010;
+	
+	// record identifiers
+	const XLS_Type_FORMULA				= 0x0006;
+	const XLS_Type_FORMULA2				= 0x0406;
+	const XLS_Type_EOF					= 0x000a;
+	const XLS_Type_PROTECT				= 0x0012;
+	const XLS_Type_OBJECTPROTECT		= 0x0063;
+	const XLS_Type_SCENPROTECT			= 0x00dd;
+	const XLS_Type_PASSWORD				= 0x0013;
+	const XLS_Type_HEADER				= 0x0014;
+	const XLS_Type_FOOTER				= 0x0015;
+	const XLS_Type_EXTERNSHEET			= 0x0017;
+	const XLS_Type_DEFINEDNAME			= 0x0018;
+	const XLS_Type_VERTICALPAGEBREAKS	= 0x001a;
+	const XLS_Type_HORIZONTALPAGEBREAKS	= 0x001b;
+	const XLS_Type_NOTE					= 0x001c;
+	const XLS_Type_SELECTION			= 0x001d;
+	const XLS_Type_DATEMODE				= 0x0022;
+	const XLS_Type_EXTERNNAME			= 0x0023;
+	const XLS_Type_LEFTMARGIN			= 0x0026;
+	const XLS_Type_RIGHTMARGIN			= 0x0027;
+	const XLS_Type_TOPMARGIN			= 0x0028;
+	const XLS_Type_BOTTOMMARGIN			= 0x0029;
+	const XLS_Type_PRINTGRIDLINES		= 0x002b;
+	const XLS_Type_FILEPASS				= 0x002f;
+	const XLS_Type_FONT					= 0x0031;
+	const XLS_Type_CONTINUE				= 0x003c;
+	const XLS_Type_PANE					= 0x0041;
+	const XLS_Type_CODEPAGE				= 0x0042;
+	const XLS_Type_DEFCOLWIDTH 			= 0x0055;
+	const XLS_Type_OBJ					= 0x005d;
+	const XLS_Type_COLINFO				= 0x007d;
+	const XLS_Type_IMDATA				= 0x007f;
+	const XLS_Type_SHEETPR				= 0x0081;
+	const XLS_Type_HCENTER				= 0x0083;
+	const XLS_Type_VCENTER				= 0x0084;
+	const XLS_Type_SHEET				= 0x0085;
+	const XLS_Type_PALETTE				= 0x0092;
+	const XLS_Type_SCL					= 0x00a0;
+	const XLS_Type_PAGESETUP			= 0x00a1;
+	const XLS_Type_MULRK				= 0x00bd;
+	const XLS_Type_MULBLANK				= 0x00be;
+	const XLS_Type_DBCELL				= 0x00d7;
+	const XLS_Type_XF					= 0x00e0;
+	const XLS_Type_MERGEDCELLS			= 0x00e5;
+	const XLS_Type_MSODRAWINGGROUP		= 0x00eb;
+	const XLS_Type_MSODRAWING			= 0x00ec;
+	const XLS_Type_SST					= 0x00fc;
+	const XLS_Type_LABELSST				= 0x00fd;
+	const XLS_Type_EXTSST				= 0x00ff;
+	const XLS_Type_EXTERNALBOOK			= 0x01ae;
+	const XLS_Type_DATAVALIDATIONS		= 0x01b2;
+	const XLS_Type_TXO					= 0x01b6;
+	const XLS_Type_HYPERLINK			= 0x01b8;
+	const XLS_Type_DATAVALIDATION		= 0x01be;
+	const XLS_Type_DIMENSION			= 0x0200;
+	const XLS_Type_BLANK				= 0x0201;
+	const XLS_Type_NUMBER				= 0x0203;
+	const XLS_Type_LABEL				= 0x0204;
+	const XLS_Type_BOOLERR				= 0x0205;
+	const XLS_Type_STRING				= 0x0207;
+	const XLS_Type_ROW					= 0x0208;
+	const XLS_Type_INDEX				= 0x020b;
+	const XLS_Type_ARRAY				= 0x0221;
+	const XLS_Type_DEFAULTROWHEIGHT 	= 0x0225;
+	const XLS_Type_WINDOW2				= 0x023e;
+	const XLS_Type_RK					= 0x007e;
+	const XLS_Type_RK2					= 0x027e;
+	const XLS_Type_STYLE				= 0x0293;
+	const XLS_Type_FORMAT				= 0x041e;
+	const XLS_Type_SHAREDFMLA			= 0x04bc;
+	const XLS_Type_BOF					= 0x0809;
+	const XLS_Type_SHEETPROTECTION		= 0x0867;
+	const XLS_Type_RANGEPROTECTION		= 0x0868;
+	const XLS_Type_SHEETLAYOUT			= 0x0862;
+	const XLS_Type_XFEXT				= 0x087d;
+	const XLS_Type_PAGELAYOUTVIEW		= 0x088b;
+	const XLS_Type_UNKNOWN				= 0xffff;
+	
+	// Encryption type
+	const MS_BIFF_CRYPTO_NONE = 0;
+	const MS_BIFF_CRYPTO_XOR  = 1;
+	const MS_BIFF_CRYPTO_RC4  = 2;
+	
+	// Size of stream blocks when using RC4 encryption
+	const REKEY_BLOCK = 0x400;
+	
+	private $_pos;
+	private $_data;
+	private $_cell;
+	private $_sst;
+	private $_sheets;
+	private $_dataSize;
+	private $_codepage = 'CP1252';
+	
+	private $index = 0;
+	private $curretSheet = 0;
+	private $dateFormats = array (
+		0xe => "m/d/Y",
+		0xf => "M-d-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"
+	);
+	private $numberFormats = array(
+		0x1 => "0",
+		0x2 => "0.00",
+		0x3 => "#,##0",
+		0x4 => "#,##0.00",
+		0x5 => "\$#,##0;(\$#,##0)",
+		0x6 => "\$#,##0;[Red](\$#,##0)",
+		0x7 => "\$#,##0.00;(\$#,##0.00)",
+		0x8 => "\$#,##0.00;[Red](\$#,##0.00)",
+		0x9 => "0%",
+		0xa => "0.00%",
+		0xb => "0.00E+00",
+		0x25 => "#,##0;(#,##0)",
+		0x26 => "#,##0;[Red](#,##0)",
+		0x27 => "#,##0.00;(#,##0.00)",
+		0x28 => "#,##0.00;[Red](#,##0.00)",
+		0x29 => "#,##0;(#,##0)",  // Not exactly
+		0x2a => "\$#,##0;(\$#,##0)",  // Not exactly
+		0x2b => "#,##0.00;(#,##0.00)",  // Not exactly
+		0x2c => "\$#,##0.00;(\$#,##0.00)",  // Not exactly
+		0x30 => "##0.0E+0"
+	);
+	
+	public $error = false;
+	
+	/**
+	 * Create a new Spreadsheet_Excel_Reader instance
+	 */
+	public function __construct($file) {
+		$this->_loadOLE($file);		// Read the OLE file
+	}
+	
+	/**
+	 * Changes sheet to another.
+	 * @param bool
+	 */
+	public function ChangeSheet($index){
+		$this->curretSheet = $index;
+		return true;
+	}
+	
+	/**
+	 * 获取Cell数据
+	 */
+	public function getCell(){
+		$this->_cell = array();
+		$this->_endRow = false;
+		$this->_key = null;
+		
+		if( ! $this->_parse){
+			$this->_parse = true;
+			$this->_pos = 0;
+			
+			// Parse Workbook Global Substream
+			while ($this->_pos < $this->_dataSize) {
+				$code = self::_GetInt2d($this->_data, $this->_pos);
+				
+				switch ($code) {
+					case self::XLS_Type_SST:			$this->_readSst();				break;
+					case self::XLS_Type_CODEPAGE:		$this->_readCodepage();			break;
+					case self::XLS_Type_DATEMODE:		$this->_readDateMode();			break;
+					case self::XLS_Type_FORMAT:			$this->_readFormat();			break;
+					case self::XLS_Type_XF:				$this->_readXf();				break;
+					case self::XLS_Type_EOF:			$this->_readDefault();			break 2;
+					default:							$this->_readDefault();			break;
+				}
+			}
+		}
+
+		// Parse the individual sheet
+		$this->_pos = $this->_lastPos ? $this->_lastPos : $this->_sheets[$this->curretSheet]['offset'];
+		while ($this->_pos <= $this->_dataSize - 4) {
+			if($this->_endRow) break;
+			$code = self::_GetInt2d($this->_data, $this->_pos);
+
+			switch ($code) {
+				case self::XLS_Type_RK:
+				case self::XLS_Type_RK2:					$this->_readRk();						break;
+				case self::XLS_Type_LABELSST:				$this->_readLabelSst();					break;
+				case self::XLS_Type_MULRK:					$this->_readMulRk();					break;
+				case self::XLS_Type_NUMBER:					$this->_readNumber();					break;
+				case self::XLS_Type_FORMULA:
+				case self::XLS_Type_FORMULA2:				$this->_readFormula();					break;
+				case self::XLS_Type_BOOLERR:				$this->_readBoolErr();					break;
+				case self::XLS_Type_STRING:					$this->_readString();					break;
+				case self::XLS_Type_MULBLANK:				$this->_readBlank();					break;
+				case self::XLS_Type_LABEL:					$this->_readLabel();					break;
+				case self::XLS_Type_EOF:					$this->_readDefault();					break 2;
+				default:									$this->_readDefault();					break;
+			}
+		}
+		return $this->_cell;
+	}
+	
+	/**
+	 * Return worksheet info (Name, Last Column Letter, Last Column Index, Total Rows, Total Columns)
+	 */
+	public  function getWorksheetInfo() {	
+		if( ! $this->_sheets){
+			$this->_dataSize = strlen($this->_data);					// total byte size of Excel data (workbook global substream + sheet substreams)
+			$this->_pos      = 0;
+			$this->_sheets   = array();
+		
+			// Parse Workbook Global Substream
+			while ($this->_pos < $this->_dataSize) {
+				$code = self::_GetInt2d($this->_data, $this->_pos);
+		
+				switch ($code) {
+					case self::XLS_Type_BOF:	$this->_readBof();			break;
+					case self::XLS_Type_SHEET:	$this->_readSheet();		break;
+					case self::XLS_Type_EOF:	$this->_readDefault();		break 2;
+					default:					$this->_readDefault();		break;
+				}
+			}
+		}
+		
+		if( ! isset($this->_sheets[$this->curretSheet])){
+			return array();
+		}
+		
+		$sheetInfo = array(
+			'worksheetName'		=> $this->_sheets[$this->curretSheet]['name'],
+			'lastColumnLetter'	=> 'A',
+			'lastColumnIndex'	=> 0,
+			'totalRows'			=> 0,
+			'totalColumns'		=> 0
+		);
+		
+		// Parse the individual sheet
+		$this->_pos = $this->_sheets[$this->curretSheet]['offset'];
+		while ($this->_pos <= $this->_dataSize - 4) {
+			$code = self::_GetInt2d($this->_data, $this->_pos);
+		
+			switch ($code) {
+				case self::XLS_Type_DIMENSION:
+					$length = self::_GetInt2d($this->_data, $this->_pos + 2);
+					$this->_pos += 4;
+					if ($length == 10 && $this->_version == self::XLS_BIFF7) {
+						$sheetInfo['totalRows'] = self::_GetInt2d($this->_data, $this->_pos + 2);
+						$sheetInfo['totalColumns'] = self::_GetInt2d($this->_data, $this->_pos + 6);
+					}
+					else{
+						$sheetInfo['totalRows'] = self::_GetInt2d($this->_data, $this->_pos + 4);
+						$sheetInfo['totalColumns'] = self::_GetInt2d($this->_data, $this->_pos + 10);
+					}
+					break 2;
+				default:
+					$this->_readDefault();
+					break;
+			}
+		}
+		
+		if ($sheetInfo['totalColumns']) {
+			$sheetInfo['lastColumnIndex'] = $sheetInfo['totalColumns'] - 1;
+		}
+		$sheetInfo['lastColumnLetter'] = self::_stringFromColumnIndex($sheetInfo['lastColumnIndex']);
+	
+		return $sheetInfo;
+	}
+	
+	private function _addCell($row, $column, $value, $format){
+		if(is_null($this->_key)){
+			$this->_key = $row;
+		}
+		
+		if($row > $this->_key){
+			$this->_endRow = true;
+			return false;
+		}
+		
+		switch ($format) {
+			case 'NULL':
+				$_value = $value;
+				break;
+			case 'STRING2':				
+			case 'STRING':
+			case 'INLINE':
+				$value = substr($value, 0, 32767);
+				$_value = str_replace(array("\r\n", "\r"), "\n", $value);
+				break;
+			case 'NUMERIC':
+				$_value = (float)$value;
+				break;
+			case 'FORMULA':
+				$_value = (string)$value;
+				break;
+			case 'BOOL':
+				$_value = (bool)$value;
+				break;
+			case 'ERROR':
+				$_errorCodes = array(
+					'#NULL!'  => 0,
+					'#DIV/0!' => 1,
+					'#VALUE!' => 2,
+					'#REF!'   => 3,
+					'#NAME?'  => 4,
+					'#NUM!'   => 5,
+					'#N/A'    => 6
+				);
+				$_value = (string)$value;
+				
+				if ( ! array_key_exists($_value, $_errorCodes)) {
+					$_value = '#NULL!';
+				}				
+				break;
+			default:
+				$_value = '#NULL!';
+				break;
+		}
+		
+		$this->_lastPos = $this->_pos;
+		$this->_cell[$column] = $_value;
+	}
+	
+	/**
+	 * Use OLE reader to extract the relevant data streams from the OLE file
+	 */
+	private function _loadOLE($file)	{
+		self::_loadClass();
+		$ole = new PHPExcel_Shared_OLERead();				// OLE reader
+		$res = $ole->read($file);							// get excel data
+		if ($ole->error) {
+			$this->error = true;
+			return false;
+		}
+		
+		$this->_data = $ole->getStream($ole->workbook);		// Get workbook data: workbook stream + sheet streams
+	}
+	
+	private function _getFormatDetail($data, $pos, $value, $column){
+		$xfIndex = self::_GetInt2d($data, $pos + 4);
+		$xfRecord = $this->xfRecords[$xfIndex];
+		$type = $xfRecord['type'];
+		$format = $xfRecord['format'];
+		$formatIndex = $xfRecord['formatIndex'];
+
+		if ($type == 'date') {
+			$_type = 'STRING';
+			// Convert numeric value into a date
+			$utcDays = floor($value - ($this->_excelBaseDate == 1904 ? 24107 : 25569));
+			$utcValue = $utcDays * 86400;
+			$keys = array('seconds','minutes','hours','mday','wday','mon','year','yday','weekday','month',0);
+			$datas = explode(":", gmdate('s:i:G:j:w:n:Y:z:l:F:U', $utcValue));
+			foreach ($keys as $key => $value) {
+				$dateInfo[$value] = $datas[$key];
+			}
+		
+			$fractionalDay = $value - floor($value) + .0000001; // The .0000001 is to fix for php/excel fractional diffs
+			$totalSeconds = floor(86400 * $fractionalDay);
+			$secs = $totalSeconds % 60;
+			$totalSeconds -= $secs;
+			$hours = floor($totalSeconds / (60 * 60));
+			$mins = floor($totalSeconds / 60) % 60;
+			$_value = date ($format, mktime($hours, $mins, $secs, $dateInfo["mon"], $dateInfo["mday"], $dateInfo["year"]));
+		} 
+		else if ($type == 'number') {
+			$_type = 'NUMERIC';
+			$_value = $this->_format_value($format, $value, $formatIndex);
+		}
+		else {
+			$_type = 'STRING';
+			$_value = $this->_format_value("%s", $value, $formatIndex);
+		}
+		
+		return array(
+			'value' => $_value,
+			'type' 	=> $_type
+		);
+	}
+	
+	private function _format_value($format, $value, $formatIndex){
+		// 49==TEXT format
+		if ( ( ! $formatIndex && $format == "%s") || ($formatIndex == 49) || ($format == "GENERAL") ) {
+			return $value;
+		}
+		
+		// Custom pattern can be POSITIVE;NEGATIVE;ZERO
+		// The "text" option as 4th parameter is not handled
+		$parts = explode(";", $format);
+		$pattern = $parts[0];
+		
+		if (count($parts) > 2 && $value == 0) {	// Negative pattern
+			$pattern = $parts[2];
+		}
+		else if (count($parts) > 1 && $value < 0) {	// Zero pattern
+			$pattern = $parts[1];
+			$value = abs($value);
+		}
+		
+		// In Excel formats, "_" is used to add spacing, which we can't do in HTML
+		$pattern = preg_replace("/_./", "", $pattern);
+		
+		// Some non-number characters are escaped with \, which we don't need
+		$pattern = preg_replace("/\\\/", "", $pattern);
+		
+		// Some non-number strings are quoted, so we'll get rid of the quotes
+		$pattern = preg_replace("/\"/", "", $pattern);
+
+		// TEMPORARY - Convert # to 0
+		$pattern = preg_replace("/\#/", "0", $pattern);
+
+		// Find out if we need comma formatting
+		$has_commas = preg_match("/,/", $pattern);
+		if ($has_commas) {
+			$pattern = preg_replace("/,/", "", $pattern);
+		}
+		
+		// Handle Percentages
+		if (preg_match("/\d(\%)([^\%]|$)/", $pattern, $matches)) {
+			$value = $value * 100;
+			$pattern = preg_replace("/(\d)(\%)([^\%]|$)/", "$1%$3", $pattern);
+		}
+		
+		// Handle the number itself
+		$number_regex = "/(\d+)(\.?)(\d*)/";
+		if (preg_match($number_regex,$pattern,$matches)) {
+			$left = $matches[1];
+			$dec = $matches[2];
+			$right = $matches[3];
+			if ($has_commas) {
+				$formatted = number_format($value,strlen($right));
+			}
+			else {
+				$sprintf_pattern = "%1.".strlen($right)."f";
+				$formatted = sprintf($sprintf_pattern, $value);
+			}
+			$pattern = preg_replace($number_regex, $formatted, $pattern);
+		}
+		
+		return $pattern;
+	}
+	
+	/**
+	 * Read BOF
+	 */
+	private function _readBof()	{
+		$length = self::_GetInt2d($this->_data, $this->_pos + 2);
+		$recordData = substr($this->_data, $this->_pos + 4, $length);
+		
+		$this->_pos += 4 + $length;							// move stream pointer to next record
+		$substreamType = self::_GetInt2d($recordData, 2);	// offset: 2; size: 2; type of the following data
+		switch ($substreamType) {
+			case self::XLS_WorkbookGlobals:
+				$version = self::_GetInt2d($recordData, 0);
+				if (($version != self::XLS_BIFF8) && ($version != self::XLS_BIFF7)) {
+					die('Cannot read this Excel file. Version is too old.');
+				}
+				$this->_version = $version;
+				break;
+	
+			case self::XLS_Worksheet:
+				// do not use this version information for anything
+				// it is unreliable (OpenOffice doc, 5.8), use only version information from the global stream
+				break;
+	
+			default:
+				// substream, e.g. chart. just skip the entire substream
+				do {
+					$code = self::_GetInt2d($this->_data, $this->_pos);
+					$this->_readDefault();
+				} while ($code != self::XLS_Type_EOF && $this->_pos < $this->_dataSize);
+				break;
+		}
+	}
+	
+	/**
+	 * Read Sheet
+	 */
+	private function _readSheet() {
+		$length = self::_GetInt2d($this->_data, $this->_pos + 2);
+		$recordData = substr($this->_data, $this->_pos + 4, $length);
+		
+		$rec_offset = self::_GetInt4d($this->_data, $this->_pos + 4);	// offset: 0; size: 4; absolute stream position of the BOF record of the sheet
+		$this->_pos += 4 + $length;										// move stream pointer to next record
+	
+		// offset: 6; size: var; sheet name
+		if ($this->_version == self::XLS_BIFF8) {
+			$string = self::_readUnicodeStringShort(substr($recordData, 6));
+			$rec_name = $string['value'];
+		} elseif ($this->_version == self::XLS_BIFF7) {
+			$string = self::_readByteStringShort(substr($recordData, 6));
+			$rec_name = $string['value'];
+		}
+	
+		$this->_sheets[] = array(
+			'name' => $rec_name,
+			'offset' => $rec_offset
+		);
+	}
+	
+	/**
+	 * Reads a general type of BIFF record. Does nothing except for moving stream pointer forward to next record.
+	 */
+	private function _readDefault()	{
+		$length = self::_GetInt2d($this->_data, $this->_pos + 2);
+		
+		$this->_pos += 4 + $length;		// move stream pointer to next record
+	}
+	
+	/**
+	 * CODEPAGE
+	 *
+	 * This record stores the text encoding used to write byte
+	 * strings, stored as MS Windows code page identifier.
+	 */
+	private function _readCodepage() {
+		$length = self::_GetInt2d($this->_data, $this->_pos + 2);
+		$recordData = substr($this->_data, $this->_pos + 4, $length);
+		$this->_pos += 4 + $length;
+		$codepage = self::_GetInt2d($recordData, 0);
+	
+		$this->_codepage = self::NumberToName($codepage);
+	}
+	
+	/**
+	 * DATEMODE
+	 *
+	 * This record specifies the base date for displaying date values. All dates are stored as count of days past this base date. 
+	 * In BIFF2-BIFF4 this record is part of the Calculation Settings Block. In BIFF5-BIFF8 it is stored in the Workbook Globals Substream.
+	 */
+	private function _readDateMode() {
+		$length = self::_GetInt2d($this->_data, $this->_pos + 2);
+		$recordData = substr($this->_data, $this->_pos + 4, $length);
+		$this->_pos += 4 + $length;
+		if (ord($recordData{0}) == 1) {
+			$this->_excelBaseDate = 1904;
+		}
+		else{
+			$this->_excelBaseDate = 1900;
+		}
+	}
+	
+	/**
+	 * data format
+	 */
+	private function _readFormat(){
+		$length = self::_GetInt2d($this->_data, $this->_pos + 2);
+		$indexCode = self::_GetInt2d($this->_data, $this->_pos + 4);
+		if ($this->_version == self::XLS_BIFF8) {
+			$numchars = self::_GetInt2d($this->_data, $this->_pos + 6);
+			if (ord($this->_data[$this->_pos + 8]) == 0){
+				$formatString = substr($this->_data, $this->_pos + 9, $numchars);
+			} else {
+				$formatString = substr($this->_data, $this->_pos + 9, $numchars*2);
+			}
+		} 
+		else {
+			$numchars = ord($this->_data[$this->_pos + 6]);
+			$formatString = substr($this->_data, $this->_pos + 7, $numchars*2);
+		}
+		$this->formatRecords[$indexCode] = $formatString;
+		$this->_pos += 4 + $length;
+	}
+	
+	/**
+	 * XF - Extended Format
+	 *
+	 * This record contains formatting information for cells, rows, columns or styles.
+	 * According to http://support.microsoft.com/kb/147732 there are always at least 15 cell style XF and 1 cell XF.
+	 * Inspection of Excel files generated by MS Office Excel shows that XF records 0-14 are cell style XF and XF record 15 is a cell XF
+	 * We only read the first cell style XF and skip the remaining cell style XF records
+	 */
+	private function _readXf() {
+		$length = self::_GetInt2d($this->_data, $this->_pos + 2);
+		$indexCode = self::_GetInt2d($this->_data, $this->_pos + 6);
+		$this->_pos += 4 + $length;
+		$xf = array('formatIndex' => $indexCode);
+		if (isset($this->dateFormats[$indexCode])) {
+			$xf['type'] = 'date';
+			$xf['format'] = $this->dateFormats[$indexCode];
+		}
+		elseif (isset($this->numberFormats[$indexCode])) {
+			$xf['type'] = 'number';
+			$xf['format'] = $this->numberFormats[$indexCode];
+		}
+		else {
+			if ($indexCode > 0){
+				if (isset($this->formatRecords[$indexCode])) {
+					$formatStr = $this->formatRecords[$indexCode];
+				}
+				
+				if ($formatStr) {
+					$tmp = preg_replace("/\;.*/", "", $formatStr);
+					$tmp = preg_replace("/^\[[^\]]*\]/", "", $tmp);
+					if (preg_match("/[^hmsday\/\-:\s\\\,AMP]/i", $tmp) == 0) { // found day and time format
+						$isDate = TRUE;
+						$formatStr = $tmp;
+						$formatStr = str_replace(array('AM/PM','mmmm','mmm'), array('a','F','M'), $formatStr);
+						// m/mm are used for both minutes and months - oh SNAP!
+						// This mess tries to fix for that.
+						// 'm' == minutes only if following h/hh or preceding s/ss
+						$formatstr = preg_replace("/(h:?)mm?/","$1i", $formatStr);
+						$formatstr = preg_replace("/mm?(:?s)/","i$1", $formatStr);
+						// A single 'm' = n in PHP
+						$formatStr = preg_replace("/(^|[^m])m([^m]|$)/", '$1n$2', $formatStr);
+						$formatStr = preg_replace("/(^|[^m])m([^m]|$)/", '$1n$2', $formatStr);
+						// else it's months
+						$formatStr = str_replace('mm', 'm', $formatStr);
+						// Convert single 'd' to 'j'
+						$formatStr = preg_replace("/(^|[^d])d([^d]|$)/", '$1j$2', $formatStr);
+						$formatStr = str_replace(array('dddd','ddd','dd','yyyy','yy','hh','h'), array('l','D','d','Y','y','H','g'), $formatStr);
+						$formatStr = preg_replace("/ss?/", 's', $formatStr);
+					}
+				}
+			}
+			
+			if ($isDate){
+				$xf['type'] = 'date';
+				$xf['format'] = $formatStr;
+			}else{
+				if (preg_match("/[0#]/", $formatStr)) {
+					$xf['type'] = 'number';
+					$xf['format'] = $formatStr;
+				}
+				else {
+					$xf['type'] = 'other';
+					$xf['format'] = 0;
+				}
+			}
+		}
+		
+		$this->xfRecords[] = $xf;
+	}
+	
+	/**
+	 * SST - Shared String Table
+	 *
+	 * This record contains a list of all strings used anywherein the workbook. Each string occurs only once. The
+	 * workbook uses indexes into the list to reference the strings.
+	 **/
+	private function _readSst()	{
+		$pos = 0;												// offset within (spliced) record data
+		$splicedRecordData = $this->_getSplicedRecordData();	// get spliced record data
+		$recordData = $splicedRecordData['recordData'];
+		$spliceOffsets = $splicedRecordData['spliceOffsets'];
+	
+		$pos += 4;												// offset: 0; size: 4; total number of strings in the workbook
+		$nm = self::_GetInt4d($recordData, 4);					// offset: 4; size: 4; number of following strings ($nm)
+		$pos += 4;
+		
+		for ($i = 0; $i < $nm; ++$i) {							// loop through the Unicode strings (16-bit length)
+			$numChars = self::_GetInt2d($recordData, $pos);		// number of characters in the Unicode string
+			$pos += 2;
+			
+			$optionFlags = ord($recordData{$pos});				// option flags
+			++$pos;
+			
+			$isCompressed = (($optionFlags & 0x01) == 0) ;		// bit: 0; mask: 0x01; 0 = compressed; 1 = uncompressed
+			$hasAsian = (($optionFlags & 0x04) != 0);			// bit: 2; mask: 0x02; 0 = ordinary; 1 = Asian phonetic
+			$hasRichText = (($optionFlags & 0x08) != 0);		// bit: 3; mask: 0x03; 0 = ordinary; 1 = Rich-Text
+	
+			if ($hasRichText) {
+				$formattingRuns = self::_GetInt2d($recordData, $pos);		// number of Rich-Text formatting runs
+				$pos += 2;
+			}
+	
+			if ($hasAsian) {
+				$extendedRunLength = self::_GetInt4d($recordData, $pos);	// size of Asian phonetic setting
+				$pos += 4;
+			}
+	
+			$len = ($isCompressed) ? $numChars : $numChars * 2;				// expected byte length of character array if not split
+	
+			foreach ($spliceOffsets as $spliceOffset) {						// look up limit position
+				if ($pos <= $spliceOffset) {								// it can happen that the string is empty, therefore we need. <= and not just <
+					$limitpos = $spliceOffset;
+					break;
+				}
+			}
+	
+			if ($pos + $len <= $limitpos) {
+				$retstr = substr($recordData, $pos, $len);					// character array is not split between records
+				$pos += $len;
+			} else {
+				$retstr = substr($recordData, $pos, $limitpos - $pos);		// character array is split between records. first part of character array
+				$bytesRead = $limitpos - $pos;
+				$charsLeft = $numChars - (($isCompressed) ? $bytesRead : ($bytesRead / 2));		// remaining characters in Unicode string
+				$pos = $limitpos;
+	
+				// keep reading the characters
+				while ($charsLeft > 0) {
+					// look up next limit position, in case the string span more than one continue record
+					foreach ($spliceOffsets as $spliceOffset) {
+						if ($pos < $spliceOffset) {
+							$limitpos = $spliceOffset;
+							break;
+						}
+					}
+	
+					// repeated option flags. OpenOffice.org documentation 5.21
+					$option = ord($recordData{$pos});
+					++$pos;
+	
+					if ($isCompressed && ($option == 0)) {
+						// 1st fragment compressed. this fragment compressed
+						$len = min($charsLeft, $limitpos - $pos);
+						$retstr .= substr($recordData, $pos, $len);
+						$charsLeft -= $len;
+						$isCompressed = true;
+	
+					} elseif (!$isCompressed && ($option != 0)) {
+						// 1st fragment uncompressed. this fragment uncompressed
+						$len = min($charsLeft * 2, $limitpos - $pos);
+						$retstr .= substr($recordData, $pos, $len);
+						$charsLeft -= $len / 2;
+						$isCompressed = false;
+	
+					} elseif (!$isCompressed && ($option == 0)) {
+						// 1st fragment uncompressed. this fragment compressed
+						$len = min($charsLeft, $limitpos - $pos);
+						for ($j = 0; $j < $len; ++$j) {
+							$retstr .= $recordData{$pos + $j} . chr(0);
+						}
+						$charsLeft -= $len;
+						$isCompressed = false;
+	
+					} else {
+						// 1st fragment compressed. this fragment uncompressed
+						$newstr = '';
+						for ($j = 0; $j < strlen($retstr); ++$j) {
+							$newstr .= $retstr[$j] . chr(0);
+						}
+						$retstr = $newstr;
+						$len = min($charsLeft * 2, $limitpos - $pos);
+						$retstr .= substr($recordData, $pos, $len);
+						$charsLeft -= $len / 2;
+						$isCompressed = false;
+					}
+	
+					$pos += $len;
+				}
+			}
+			
+			$retstr = self::_encodeUTF16($retstr, $isCompressed);	// convert to UTF-8
+			$fmtRuns = array();										// read additional Rich-Text information, if any
+			if ($hasRichText) {
+				// list of formatting runs
+				for ($j = 0; $j < $formattingRuns; ++$j) {
+					$charPos = self::_GetInt2d($recordData, $pos + $j * 4);			// first formatted character; zero-based
+					$fontIndex = self::_GetInt2d($recordData, $pos + 2 + $j * 4);	// index to font record
+					$fmtRuns[] = array(
+						'charPos' => $charPos,
+						'fontIndex' => $fontIndex
+					);
+				}
+				$pos += 4 * $formattingRuns;
+			}
+	
+			// read additional Asian phonetics information, if any
+			if ($hasAsian) {
+				$pos += $extendedRunLength;		// For Asian phonetic settings, we skip the extended string data
+			}
+	
+			// store the shared sting
+			$this->_sst[] = array(
+				'value' => $retstr,
+				'fmtRuns' => $fmtRuns
+			);
+		}
+	
+	}
+	
+	/**
+	 * Read RK record
+	 * This record represents a cell that contains an RK value (encoded integer or floating-point value). If a floating-point value 
+	 * cannot be encoded to an RK value, a NUMBER record will be written. This record replaces the record INTEGER written in BIFF2.
+	 */
+	private function _readRk() {
+		$length = self::_GetInt2d($this->_data, $this->_pos + 2);
+		$recordData = substr($this->_data, $this->_pos + 4, $length);
+	
+		$this->_pos += 4 + $length;		
+		$row = self::_GetInt2d($recordData, 0);
+		$column = self::_GetInt2d($recordData, 2);
+		$rknum = self::_GetInt4d($recordData, 6);
+		$numValue = self::_GetIEEE754($rknum);
+	
+		// add cell
+		$this->_addCell($row, $column, $numValue, 'NUMERIC');
+	}
+	
+	/**
+	 * Read LABELSST record This record represents a cell that contains a string. It
+	 * replaces the LABEL record and RSTRING record used in BIFF2-BIFF5.
+	 */
+	private function _readLabelSst() {
+		$length = self::_GetInt2d($this->_data, $this->_pos + 2);
+		$recordData = substr($this->_data, $this->_pos + 4, $length);
+	
+		$this->_pos += 4 + $length;
+		$row = self::_GetInt2d($recordData, 0);
+		$column = self::_GetInt2d($recordData, 2);
+	
+		// offset: 6; size: 4; index to SST record
+		$index = self::_GetInt4d($recordData, 6);
+		$this->_addCell($row, $column, $this->_sst[$index]['value'], 'STRING');
+	}
+	
+	
+	/**
+	 * Read MULRK record
+	 * This record represents a cell range containing RK value cells. All cells are located in the same row.
+	 */
+	private function _readMulRk() {
+		$length = self::_GetInt2d($this->_data, $this->_pos + 2);
+		$recordData = substr($this->_data, $this->_pos + 4, $length);
+	
+		$this->_pos += 4 + $length;
+		$row = self::_GetInt2d($recordData, 0);
+		$colFirst = self::_GetInt2d($recordData, 2);
+		$colLast = self::_GetInt2d($recordData, $length - 2);
+		$columns = $colLast - $colFirst + 1;
+	
+		// offset within record data
+		$offset = 4;
+	
+		for ($i = 0; $i < $columns; ++$i) {
+			$numValue = self::_GetIEEE754(self::_GetInt4d($recordData, $offset + 2));
+			$info = $this->_getFormatDetail($recordData, $offset - 4, $numValue, $colFirst + $i + 1);
+			$this->_addCell($row, $colFirst + $i, $info['value'], $info['type']);
+
+			$offset += 6;
+		}
+	}
+	
+	
+	/**
+	 * Read NUMBER record
+	 * This record represents a cell that contains a floating-point value.
+	 */
+	private function _readNumber() {
+		$length = self::_GetInt2d($this->_data, $this->_pos + 2);
+		$recordData = substr($this->_data, $this->_pos + 4, $length);
+	
+		$this->_pos += 4 + $length;
+		$row = self::_GetInt2d($recordData, 0);
+		$column = self::_GetInt2d($recordData, 2);
+	
+		$numValue = self::_extractNumber(substr($recordData, 6, 8));
+		$this->_addCell($row, $column, $numValue, 'NUMERIC');
+	}
+	
+	/**
+	 * Read FORMULA record + perhaps a following STRING record if formula result is a string
+	 * This record contains the token array and the result of a formula cell.
+	 */
+	private function _readFormula()	{
+		$length = self::_GetInt2d($this->_data, $this->_pos + 2);
+		$recordData = substr($this->_data, $this->_pos + 4, $length);
+		
+		$this->_pos += 4 + $length;
+		$row = self::_GetInt2d($recordData, 0);
+		$column = self::_GetInt2d($recordData, 2);
+		
+		if ((ord($recordData{6}) == 0) && (ord($recordData{12}) == 255) && (ord($recordData{13}) == 255)) {
+			$this->_preRow = $row;
+			$this->_preColumn = $column;
+			return false;
+		}
+		elseif ((ord($recordData{6}) == 1) && (ord($recordData{12}) == 255)	&& (ord($recordData{13}) == 255)) {
+			// Boolean formula. Result is in +2; 0=false, 1=true
+			$dataType = 'BOOL';
+			$value = (bool) ord($recordData{8});
+		}
+		elseif ((ord($recordData{6}) == 2) && (ord($recordData{12}) == 255)	&& (ord($recordData{13}) == 255)) {
+			// Error formula. Error code is in +2
+			$dataType = 'ERROR';
+			$value = self::_mapErrorCode(ord($recordData{8}));
+		}
+		elseif ((ord($recordData{6}) == 3) && (ord($recordData{12}) == 255)	&& (ord($recordData{13}) == 255)) {
+			// Formula result is a null string
+			$dataType = 'NULL';
+			$value = '';
+		}
+		else {
+			// forumla result is a number, first 14 bytes like _NUMBER record
+			$dataType = 'NUMERIC';
+			$value = self::_extractNumber(substr($recordData, 6, 8));
+		}
+		
+		$this->_addCell($row, $column, $value, $dataType);
+	}	
+	
+	/**
+	 * Read a STRING record from current stream position and advance the stream pointer to next record
+	 * This record is used for storing result from FORMULA record when it is a string, and it occurs directly after the FORMULA record
+	 *
+	 * @return string The string contents as UTF-8
+	 */
+	private function _readString() {
+		$length = self::_GetInt2d($this->_data, $this->_pos + 2);
+		$recordData = substr($this->_data, $this->_pos + 4, $length);
+	
+		$this->_pos += 4 + $length;
+	
+		if ($this->_version == self::XLS_BIFF8) {
+			$string = self::_readUnicodeStringLong($recordData);
+			$value = $string['value'];
+		} else {
+			$string = $this->_readByteStringLong($recordData);
+			$value = $string['value'];
+		}
+	
+		$this->_addCell($this->_preRow, $this->_preColumn, $value, 'STRING');
+	}
+	
+	
+	/**
+	 * Read BOOLERR record
+	 * This record represents a Boolean value or error value cell.
+	 */
+	private function _readBoolErr()	{
+		$length = self::_GetInt2d($this->_data, $this->_pos + 2);
+		$recordData = substr($this->_data, $this->_pos + 4, $length);
+	
+		$this->_pos += 4 + $length;
+		$row = self::_GetInt2d($recordData, 0);
+		$column = self::_GetInt2d($recordData, 2);
+	
+		// offset: 6; size: 1; the boolean value or error value
+		$boolErr = ord($recordData{6});
+
+		// offset: 7; size: 1; 0=boolean; 1=error
+		$isError = ord($recordData{7});
+
+		switch ($isError) {
+			case 0: // boolean
+				$value = (bool) $boolErr;
+
+				// add cell value
+				$this->_addCell($row, $column, $value, 'BOOL');
+				break;
+
+			case 1: // error type
+				$value = self::_mapErrorCode($boolErr);
+
+				// add cell value
+				$this->_addCell($row, $column, $value, 'ERROR');
+				break;
+		}
+	}	
+	
+	/**
+	 * Read LABEL record
+	 * This record represents a cell that contains a string. In BIFF8 it is usually replaced by the LABELSST record.
+	 * Excel still uses this record, if it copies unformatted text cells to the clipboard.
+	 */
+	private function _readLabel() {
+		$length = self::_GetInt2d($this->_data, $this->_pos + 2);
+		$recordData = substr($this->_data, $this->_pos + 4, $length);
+	
+		$this->_pos += 4 + $length;
+		$row = self::_GetInt2d($recordData, 0);
+		$column = self::_GetInt2d($recordData, 2);
+	
+		if ($this->_version == self::XLS_BIFF8) {
+			$string = self::_readUnicodeStringLong(substr($recordData, 6));
+			$value = $string['value'];
+		} else {
+			$string = $this->_readByteStringLong(substr($recordData, 6));
+			$value = $string['value'];
+		}
+		$this->_addCell($row, $column, $value, 'STRING');
+	}
+	
+	
+	/**
+	 * Read BLANK record
+	 */
+	private function _readBlank() {
+		$length = self::_GetInt2d($this->_data, $this->_pos + 2);
+		$recordData = substr($this->_data, $this->_pos + 4, $length);
+	
+		$this->_pos += 4 + $length;
+		$row = self::_GetInt2d($recordData, 0);
+		$column = self::_GetInt2d($recordData, 2);
+		$this->_addCell($row, $column, '', 'NULL');			
+	}
+	
+	/**
+	 * Reads a record from current position in data stream and continues reading data as long as CONTINUE
+	 * records are found. Splices the record data pieces and returns the combined string as if record data is in one piece.
+	 * Moves to next current position in data stream to start of next record different from a CONtINUE record
+	 *
+	 * @return array
+	 */
+	private function _getSplicedRecordData() {
+		$data = '';
+		$spliceOffsets = array();
+	
+		$i = 0;
+		$spliceOffsets[0] = 0;
+		do {
+			++$i;
+			$identifier = self::_GetInt2d($this->_data, $this->_pos);	// offset: 0; size: 2; identifier
+			$length = self::_GetInt2d($this->_data, $this->_pos + 2);	// offset: 2; size: 2; length
+			$data .= substr($this->_data, $this->_pos + 4, $length);
+	
+			$spliceOffsets[$i] = $spliceOffsets[$i - 1] + $length;
+			$this->_pos += 4 + $length;
+			$nextIdentifier = self::_GetInt2d($this->_data, $this->_pos);
+		} while ($nextIdentifier == self::XLS_Type_CONTINUE);
+	
+		$splicedData = array(
+			'recordData' => $data,
+			'spliceOffsets' => $spliceOffsets,
+		);
+	
+		return $splicedData;
+	}
+	
+	/**
+	 * Read byte string (16-bit string length)
+	 * OpenOffice documentation: 2.5.2
+	 *
+	 * @param string $subData
+	 * @return array
+	 */
+	private function _readByteStringLong($subData) {
+		// offset: 0; size: 2; length of the string (character count)
+		$ln = self::_GetInt2d($subData, 0);
+	
+		// offset: 2: size: var; character array (8-bit characters)
+		$value = $this->_decodeCodepage(substr($subData, 2));
+	
+		//return $string;
+		return array(
+				'value' => $value,
+				'size' => 2 + $ln, // size in bytes of data structure
+		);
+	}
+	
+	/**
+	 * Map error code, e.g. '#N/A'
+	 *
+	 * @param int $subData
+	 * @return string
+	 */
+	private static function _mapErrorCode($subData)	{
+		switch ($subData) {
+			case 0x00: return '#NULL!';		break;
+			case 0x07: return '#DIV/0!';	break;
+			case 0x0F: return '#VALUE!';	break;
+			case 0x17: return '#REF!';		break;
+			case 0x1D: return '#NAME?';		break;
+			case 0x24: return '#NUM!';		break;
+			case 0x2A: return '#N/A';		break;
+			default: return false;
+		}
+	}
+	
+	/**
+	 * Convert Microsoft Code Page Identifier to Code Page Name which iconv
+	 * and mbstring understands
+	 *
+	 * @param integer $codePage Microsoft Code Page Indentifier
+	 * @return string Code Page Name
+	 */
+	private static function NumberToName($codePage = 1252) {
+		switch ($codePage) {
+			case 367:	return 'ASCII';				break;	//	ASCII
+			case 437:	return 'CP437';				break;	//	OEM US
+			//case 720:	throw new PHPExcel_Exception('Code page 720 not supported.');	break;	//	OEM Arabic
+			case 737:	return 'CP737';				break;	//	OEM Greek
+			case 775:	return 'CP775';				break;	//	OEM Baltic
+			case 850:	return 'CP850';				break;	//	OEM Latin I
+			case 852:	return 'CP852';				break;	//	OEM Latin II (Central European)
+			case 855:	return 'CP855';				break;	//	OEM Cyrillic
+			case 857:	return 'CP857';				break;	//	OEM Turkish
+			case 858:	return 'CP858';				break;	//	OEM Multilingual Latin I with Euro
+			case 860:	return 'CP860';				break;	//	OEM Portugese
+			case 861:	return 'CP861';				break;	//	OEM Icelandic
+			case 862:	return 'CP862';				break;	//	OEM Hebrew
+			case 863:	return 'CP863';				break;	//	OEM Canadian (French)
+			case 864:	return 'CP864';				break;	//	OEM Arabic
+			case 865:	return 'CP865';				break;	//	OEM Nordic
+			case 866:	return 'CP866';				break;	//	OEM Cyrillic (Russian)
+			case 869:	return 'CP869';				break;	//	OEM Greek (Modern)
+			case 874:	return 'CP874';				break;	//	ANSI Thai
+			case 932:	return 'CP932';				break;	//	ANSI Japanese Shift-JIS
+			case 936:	return 'CP936';				break;	//	ANSI Chinese Simplified GBK
+			case 949:	return 'CP949';				break;	//	ANSI Korean (Wansung)
+			case 950:	return 'CP950';				break;	//	ANSI Chinese Traditional BIG5
+			case 1200:	return 'UTF-16LE';			break;	//	UTF-16 (BIFF8)
+			case 1250:	return 'CP1250';			break;	//	ANSI Latin II (Central European)
+			case 1251:	return 'CP1251';			break;	//	ANSI Cyrillic
+			case 0:											//	CodePage is not always correctly set when the xls file was saved by Apple's Numbers program
+			case 1252:	return 'CP1252';			break;	//	ANSI Latin I (BIFF4-BIFF7)
+			case 1253:	return 'CP1253';			break;	//	ANSI Greek
+			case 1254:	return 'CP1254';			break;	//	ANSI Turkish
+			case 1255:	return 'CP1255';			break;	//	ANSI Hebrew
+			case 1256:	return 'CP1256';			break;	//	ANSI Arabic
+			case 1257:	return 'CP1257';			break;	//	ANSI Baltic
+			case 1258:	return 'CP1258';			break;	//	ANSI Vietnamese
+			case 1361:	return 'CP1361';			break;	//	ANSI Korean (Johab)
+			case 10000:	return 'MAC';				break;	//	Apple Roman
+			case 10006:	return 'MACGREEK';			break;	//	Macintosh Greek
+			case 10007:	return 'MACCYRILLIC';		break;	//	Macintosh Cyrillic
+			case 10008: return 'CP936';             break;  //  Macintosh - Simplified Chinese (GB 2312)
+			case 10029:	return 'MACCENTRALEUROPE';	break;	//	Macintosh Central Europe
+			case 10079: return 'MACICELAND';		break;	//	Macintosh Icelandic
+			case 10081: return 'MACTURKISH';		break;	//	Macintosh Turkish
+			case 32768:	return 'MAC';				break;	//	Apple Roman
+			//case 32769:	throw new PHPExcel_Exception('Code page 32769 not supported.');		break;	//	ANSI Latin I (BIFF2-BIFF3)
+			case 65000:	return 'UTF-7';				break;	//	Unicode (UTF-7)
+			case 65001:	return 'UTF-8';				break;	//	Unicode (UTF-8)
+			default:	return 'UTF-8';				break;
+		}
+	}
+	
+	/**
+	 *	String from columnindex
+	 *
+	 *	@param	int $pColumnIndex
+	 *	@return	string
+	 */
+	private static function _stringFromColumnIndex($pColumnIndex = 0)	{
+		static $_indexCache = array();
+	
+		if ( ! isset($_indexCache[$pColumnIndex])) {
+			if ($pColumnIndex < 26) {
+				$_indexCache[$pColumnIndex] = chr(65 + $pColumnIndex);
+			} elseif ($pColumnIndex < 702) {
+				$_indexCache[$pColumnIndex] = chr(64 + ($pColumnIndex / 26)) . chr(65 + $pColumnIndex % 26);
+			} else {
+				$_indexCache[$pColumnIndex] = chr(64 + (($pColumnIndex - 26) / 676)) . chr(65 + ((($pColumnIndex - 26) % 676) / 26)) . chr(65 + $pColumnIndex % 26);
+			}
+		}
+		return $_indexCache[$pColumnIndex];
+	}
+	
+	/**
+	 * Extracts an Excel Unicode short string (8-bit string length)
+	 * OpenOffice documentation: 2.5.3
+	 * function will automatically find out where the Unicode string ends.
+	 *
+	 * @param string $subData
+	 * @return array
+	 */
+	private static function _readUnicodeStringShort($subData) {
+		$characterCount = ord($subData[0]);		// offset: 0: size: 1; length of the string (character count)
+		$string = self::_readUnicodeString(substr($subData, 1), $characterCount);
+		$string['size'] += 1;					// add 1 for the string length
+	
+		return $string;
+	}
+	
+	/**
+	 * Read byte string (8-bit string length)
+	 * OpenOffice documentation: 2.5.2
+	 *
+	 * @param string $subData
+	 * @return array
+	 */
+	private static function _readByteStringShort($subData)	{
+		$ln = ord($subData[0]);		// offset: 0; size: 1; length of the string (character count)
+		$value = self::_decodeCodepage(substr($subData, 1, $ln));		// offset: 1: size: var; character array (8-bit characters)
+	
+		return array(
+			'value' => $value,
+			'size' => 1 + $ln, // size in bytes of data structure
+		);
+	}
+	
+	/**
+	 * Extracts an Excel Unicode long string (16-bit string length)
+	 * OpenOffice documentation: 2.5.3. this function is under construction, needs to support rich text, and Asian phonetic settings
+	 *
+	 * @param string $subData
+	 * @return array
+	 */
+	private static function _readUnicodeStringLong($subData) {
+		$value = '';
+	
+		// offset: 0: size: 2; length of the string (character count)
+		$characterCount = self::_GetInt2d($subData, 0);
+	
+		$string = self::_readUnicodeString(substr($subData, 2), $characterCount);
+	
+		// add 2 for the string length
+		$string['size'] += 2;
+	
+		return $string;
+	}
+	
+	/**
+	 * Read Unicode string with no string length field, but with known character count
+	 * this function is under construction, needs to support rich text, and Asian phonetic settings
+	 * OpenOffice.org's Documentation of the Microsoft Excel File Format, section 2.5.3
+	 *
+	 * @param string $subData
+	 * @param int $characterCount
+	 * @return array
+	 */
+	private static function _readUnicodeString($subData, $characterCount) {
+		$isCompressed = !((0x01 & ord($subData[0])) >> 0);		// bit: 0; mask: 0x01; character compression (0 = compressed 8-bit, 1 = uncompressed 16-bit)
+		$hasAsian = (0x04) & ord($subData[0]) >> 2;				// bit: 2; mask: 0x04; Asian phonetic settings
+		$hasRichText = (0x08) & ord($subData[0]) >> 3;			// bit: 3; mask: 0x08; Rich-Text settings
+	
+		// offset: 1: size: var; character array
+		// this offset assumes richtext and Asian phonetic settings are off which is generally wrong
+		// needs to be fixed
+		$value = self::_encodeUTF16(substr($subData, 1, $isCompressed ? $characterCount : 2 * $characterCount), $isCompressed);
+	
+		return array(
+			'value' => $value,
+			'size' => $isCompressed ? 1 + $characterCount : 1 + 2 * $characterCount, // the size in bytes including the option flags
+		);
+	}
+	
+	/**
+	 * Get UTF-8 string from (compressed or uncompressed) UTF-16 string
+	 *
+	 * @param string $string
+	 * @param bool $compressed
+	 * @return string
+	 */
+	private static function _encodeUTF16($string, $compressed = '')	{
+		if ($compressed) {
+			$string = self::_uncompressByteString($string);
+		}
+	
+		return mb_convert_encoding($string, 'UTF-8', 'UTF-16LE');
+	}
+	
+	/**
+	 * Convert string to UTF-8. Only used for BIFF5.
+	 *
+	 * @param string $string
+	 * @return string
+	 */
+	private static function _decodeCodepage($string) {
+		return mb_convert_encoding($string, 'UTF-8', $this->_codepage);
+	}
+	
+	/**
+	 * Convert UTF-16 string in compressed notation to uncompressed form. Only used for BIFF8.
+	 *
+	 * @param string $string
+	 * @return string
+	 */
+	private static function _uncompressByteString($string) {
+		$uncompressedString = '';
+		$strLen = strlen($string);
+		for ($i = 0; $i < $strLen; ++$i) {
+			$uncompressedString .= $string[$i] . "\0";
+		}
+	
+		return $uncompressedString;
+	}
+	
+	/**
+	 * Read 16-bit unsigned integer
+	 *
+	 * @param  string $data
+	 * @param  int $pos
+	 * @return int
+	 */
+	private static function _GetInt2d($data, $pos) {
+		return ord($data[$pos]) | (ord($data[$pos+1]) << 8);
+	}
+	
+	/**
+	 * Read 32-bit signed integer
+	 * FIX: represent numbers correctly on 64-bit system. Hacked by Andreas Rehm 2006 to ensure correct result of the <<24 block on 32 and 64bit systems
+	 * http://sourceforge.net/tracker/index.php?func=detail&aid=1487372&group_id=99160&atid=623334
+	 * 
+	 * @param  string $data
+	 * @param  int $pos
+	 * @return int
+	 */
+	private static function _GetInt4d($data, $pos) {
+		$_or_24 = ord($data[$pos + 3]);
+		if ($_or_24 >= 128) {
+			$_ord_24 = -abs((256 - $_or_24) << 24);		// negative number
+		} else {
+			$_ord_24 = ($_or_24 & 127) << 24;
+		}
+		return ord($data[$pos]) | (ord($data[$pos+1]) << 8) | (ord($data[$pos+2]) << 16) | $_ord_24;
+	}
+	
+	/**
+	 * Reads first 8 bytes of a string and return IEEE 754 float
+	 *
+	 * @param string $data Binary string that is at least 8 bytes long
+	 * @return float
+	 */
+	private static function _extractNumber($data) {
+		$rknumhigh = self::_GetInt4d($data, 4);
+		$rknumlow = self::_GetInt4d($data, 0);
+		$sign = ($rknumhigh & 0x80000000) >> 31;
+		$exp = (($rknumhigh & 0x7ff00000) >> 20) - 1023;
+		$mantissa = (0x100000 | ($rknumhigh & 0x000fffff));
+		$mantissalow1 = ($rknumlow & 0x80000000) >> 31;
+		$mantissalow2 = ($rknumlow & 0x7fffffff);
+		$value = $mantissa / pow( 2 , (20 - $exp));
+	
+		if ($mantissalow1 != 0) {
+			$value += 1 / pow (2 , (21 - $exp));
+		}
+	
+		$value += $mantissalow2 / pow (2 , (52 - $exp));
+		if ($sign) {
+			$value *= -1;
+		}
+	
+		return $value;
+	}
+	
+	private static function _GetIEEE754($rknum)	{
+		if (($rknum & 0x02) != 0) {
+			$value = $rknum >> 2;
+		} else {
+			// changes by mmp, info on IEEE754 encoding from
+			// 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;
+	}
+	
+	/**
+	 * load OLERead class
+	 */
+	private static function _loadClass() {
+		if ( ! class_exists('PHPExcel_Shared_OLERead', false)) {
+			require 'OLERead.php';
+		}
+	}
+}

+ 205 - 0
PHPExcelReader/SpreadsheetReader/SpreadsheetReader_CSV.php

@@ -0,0 +1,205 @@
+<?php
+
+class SpreadsheetReader_CSV implements Iterator, Countable {
+	private $options = array(
+		'Delimiter' => ';',
+		'Enclosure' => '"'
+	);
+	
+	private $encoding = 'UTF-8';
+	private $filePath = '';
+	private $handle = false;
+	private $index = 0;
+	private $currentRow = null;
+	private $rowCount = null;
+	
+	public function __construct($filePath, $options = null, $encoding = '') {
+		if ( ! is_readable($filePath)) {
+			throw new Exception('SpreadsheetReader_CSV: File not readable (' . $filePath . ')');
+		}
+		
+		$this->filePath = $filePath;
+		@ini_set('auto_detect_line_endings', true);
+		$this->options = array_merge($this->options, $options);
+		$encoding && $this->encoding = $encoding;
+		$this->handle = fopen($filePath, 'r');
+		
+		// Checking the file for byte-order mark to determine encoding
+		$BOM16 = bin2hex(fread($this->handle, 2));
+		if ($BOM16 == 'fffe') {
+			$this->Encoding = 'UTF-16LE';
+			$this->BOMLength = 2;
+		}
+		elseif ($BOM16 == 'feff') {
+			$this->Encoding = 'UTF-16BE';
+			$this->BOMLength = 2;
+		}
+		
+		if ( ! $this->BOMLength) {
+			fseek($this->handle, 0);
+			$BOM32 = bin2hex(fread($this->handle, 4));
+			if ($BOM32 == '0000feff') {
+				$this->Encoding = 'UTF-32';
+				$this->BOMLength = 4;
+			}
+			elseif ($BOM32 == 'fffe0000') {
+				$this->Encoding = 'UTF-32';
+				$this->BOMLength = 4;
+			}
+		}
+		
+		fseek($this->handle, 0);
+		$BOM8 = bin2hex(fread($this->handle, 3));
+		if ($BOM8 == 'efbbbf') {
+			$this->Encoding = 'UTF-8';
+			$this->BOMLength = 3;
+		}
+		
+		// Seeking the place right after BOM as the start of the real content
+		if ($this->BOMLength) {
+			fseek($this->handle, $this->BOMLength);
+		}
+		
+		// Checking for the delimiter if it should be determined automatically
+		if ( ! $this->options['Delimiter']) {
+			$Semicolon = ';';		// fgetcsv needs single-byte separators
+			$Tab = "\t";
+			$Comma = ',';
+		
+			// Reading the first row and checking if a specific separator character
+			// has more columns than others (it means that most likely that is the delimiter).
+			$SemicolonCount = count(fgetcsv($this->handle, null, $Semicolon));
+			fseek($this->handle, $this->BOMLength);
+			$TabCount = count(fgetcsv($this->handle, null, $Tab));
+			fseek($this->handle, $this->BOMLength);
+			$CommaCount = count(fgetcsv($this->handle, null, $Comma));
+			fseek($this->handle, $this->BOMLength);
+		
+			$Delimiter = $Semicolon;
+			if ($TabCount > $SemicolonCount || $CommaCount > $SemicolonCount) {
+				$Delimiter = $CommaCount > $TabCount ? $Comma : $Tab;
+			}
+		
+			$this->options['Delimiter'] = $Delimiter;
+		}
+	}
+	
+	/**
+	 * Returns information about sheets in the file.
+	 * @return array
+	 */
+	public function Sheets() {
+		return array(0 => basename($this->filePath));
+	}
+	
+	/**
+	 * Changes sheet to another.
+	 * @param bool
+	 */
+	public function ChangeSheet($index) {
+		if ($index == 0) {
+			$this->rewind();
+			return true;
+		}
+		
+		return false;
+	}
+	
+	/**
+	 * Rewind the Iterator to the first element.
+	 */
+	public function rewind() {
+		fseek($this->handle, 0);
+		$this->currentRow = null;
+		$this->index = 0;
+	}
+	
+	/**
+	 * Return the current element.
+	 * @return mixed
+	 */
+	public function current() {
+		if ($this->index == 0 && is_null($this->currentRow)) {
+			$this->rewind();
+			$this->next();
+			$this->index = 0;
+		}
+		
+		return $this->currentRow;
+	}
+	
+	/**
+	 * Move forward to next element.
+	 */
+	public function next() {
+		$this->currentRow = array();
+
+		if ($this->Encoding == 'UTF-16LE' || $this->Encoding == 'UTF-16BE')	{
+			while ( ! feof($this->handle)) {
+				$Char = ord(fgetc($this->handle));
+				if ( ! $Char || $Char == 10 || $Char == 13)	{
+					continue;												// While bytes are insignificant whitespace, do nothing
+				}
+				else {
+					if ($this->Encoding == 'UTF-16LE') {
+						fseek($this->handle, ftell($this->handle) - 1);		// When significant bytes are found, step back to the last place before them
+					}
+					else {
+						fseek($this->handle, ftell($this->handle) - 2);
+					}
+					break;
+				}
+			}
+		}
+		
+		$this->index++;
+		$this->currentRow = fgetcsv($this->handle, null, $this->options['Delimiter'], $this->options['Enclosure']);
+		if ($this->currentRow) {
+			if ($this->encoding != 'ASCII' && $this->encoding != 'UTF-8') {
+				foreach($this->currentRow as $key => $value) {
+					$this->currentRow[$key] =  trim(trim(
+						mb_convert_encoding($value, 'UTF-8', $this->encoding),
+						$this->options['Enclosure']
+					));
+				}
+			}
+		}
+	
+		return $this->currentRow;
+	}
+	
+	/**
+	 * Return the identifying key of the current element.
+	 * @return mixed
+	 */
+	public function key() {
+		return $this->index;
+	}
+	
+	/**
+	 * Check if there is a current element after calls to rewind() or next().
+	 * @return bool
+	 */
+	public function valid() {
+		return ($this->currentRow || ! feof($this->handle));
+	}
+	
+	/**
+	 * return the count of the contained items
+	 * @return int
+	 */
+	public function count() {
+		if (is_null($this->rowCount)) {
+			$total = 0;
+			
+			fseek($this->handle, 0);
+			while ($row = fgetcsv($this->handle, null, $this->options['Delimiter'], $this->options['Enclosure'])) {
+				$total++;
+			}
+			
+			$this->rowCount = $total;
+		}
+		
+		return $this->rowCount;
+	}
+}

+ 333 - 0
PHPExcelReader/SpreadsheetReader/SpreadsheetReader_ODS.php

@@ -0,0 +1,333 @@
+<?php
+
+class SpreadsheetReader_ODS implements Iterator, Countable {
+	private $Options = array(
+		'TempDir' => '',
+		'ReturnDateTimeObjects' => false
+	);
+
+	/**
+	 * @var string Path to temporary content file
+	 */
+	private $ContentPath = '';
+	/**
+	 * @var XMLReader XML reader object
+	 */
+	private $Content = false;
+
+	/**
+	 * @var array Data about separate sheets in the file
+	 */
+	private $Sheets = false;
+
+	private $CurrentRow = null;
+
+	/**
+	 * @var int Number of the sheet we're currently reading
+	 */
+	private $CurrentSheet = 0;
+
+	private $Index = 0;
+
+	private $TableOpen = false;
+	private $RowOpen = false;
+
+	/**
+	 * @param string Path to file
+	 * @param array Options:
+	 *	TempDir => string Temporary directory path
+	 *	ReturnDateTimeObjects => bool True => dates and times will be returned as PHP DateTime objects, false => as strings
+	 */
+	public function __construct($Filepath, array $Options = null)
+	{
+		if (!is_readable($Filepath))
+		{
+			throw new Exception('SpreadsheetReader_ODS: File not readable ('.$Filepath.')');
+		}
+
+		$this -> TempDir = isset($Options['TempDir']) && is_writable($Options['TempDir']) ?
+			$Options['TempDir'] :
+			sys_get_temp_dir();
+
+		$this -> TempDir = rtrim($this -> TempDir, DIRECTORY_SEPARATOR);
+		$this -> TempDir = $this -> TempDir.DIRECTORY_SEPARATOR.uniqid().DIRECTORY_SEPARATOR;
+
+		$Zip = new ZipArchive;
+		$Status = $Zip -> open($Filepath);
+
+		if ($Status !== true)
+		{
+			throw new Exception('SpreadsheetReader_ODS: File not readable ('.$Filepath.') (Error '.$Status.')');
+		}
+
+		if ($Zip -> locateName('content.xml') !== false)
+		{
+			$Zip -> extractTo($this -> TempDir, 'content.xml');
+			$this -> ContentPath = $this -> TempDir.'content.xml';
+		}
+
+		$Zip -> close();
+
+		if ($this -> ContentPath && is_readable($this -> ContentPath))
+		{
+			$this -> Content = new XMLReader;
+			$this -> Content -> open($this -> ContentPath);
+			$this -> Valid = true;
+		}
+	}
+
+	/**
+	 * Destructor, destroys all that remains (closes and deletes temp files)
+	 */
+	public function __destruct()
+	{
+		if ($this -> Content && $this -> Content instanceof XMLReader)
+		{
+			$this -> Content -> close();
+			unset($this -> Content);
+		}
+		if (file_exists($this -> ContentPath))
+		{
+			@unlink($this -> ContentPath);
+			unset($this -> ContentPath);
+		}
+	}
+
+	/**
+	 * Retrieves an array with information about sheets in the current file
+	 *
+	 * @return array List of sheets (key is sheet index, value is name)
+	 */
+	public function Sheets()
+	{
+		if ($this -> Sheets === false)
+		{
+			$this -> Sheets = array();
+
+			if ($this -> Valid)
+			{
+				$this -> SheetReader = new XMLReader;
+				$this -> SheetReader -> open($this -> ContentPath);
+
+				while ($this -> SheetReader -> read())
+				{
+					if ($this -> SheetReader -> name == 'table:table')
+					{
+						$this -> Sheets[] = $this -> SheetReader -> getAttribute('table:name');
+						$this -> SheetReader -> next();
+					}
+				}
+				
+				$this -> SheetReader -> close();
+			}
+		}
+		return $this -> Sheets;
+	}
+
+	/**
+	 * Changes the current sheet in the file to another
+	 *
+	 * @param int Sheet index
+	 *
+	 * @return bool True if sheet was successfully changed, false otherwise.
+	 */
+	public function ChangeSheet($Index)
+	{
+		$Index = (int)$Index;
+
+		$Sheets = $this -> Sheets();
+		if (isset($Sheets[$Index]))
+		{
+			$this -> CurrentSheet = $Index;
+			$this -> rewind();
+
+			return true;
+		}
+
+		return false;
+	}
+
+	// !Iterator interface methods
+	/** 
+	 * Rewind the Iterator to the first element.
+	 * Similar to the reset() function for arrays in PHP
+	 */ 
+	public function rewind()
+	{
+		if ($this -> Index > 0)
+		{
+			// If the worksheet was already iterated, XML file is reopened.
+			// Otherwise it should be at the beginning anyway
+			$this -> Content -> close();
+			$this -> Content -> open($this -> ContentPath);
+			$this -> Valid = true;
+
+			$this -> TableOpen = false;
+			$this -> RowOpen = false;
+
+			$this -> CurrentRow = null;
+		}
+
+		$this -> Index = 0;
+	}
+
+	/**
+	 * Return the current element.
+	 * Similar to the current() function for arrays in PHP
+	 *
+	 * @return mixed current element from the collection
+	 */
+	public function current()
+	{
+		if ($this -> Index == 0 && is_null($this -> CurrentRow))
+		{
+			$this -> next();
+			$this -> Index--;
+		}
+		return $this -> CurrentRow;
+	}
+
+	/** 
+	 * Move forward to next element. 
+	 * Similar to the next() function for arrays in PHP 
+	 */ 
+	public function next()
+	{
+		$this -> Index++;
+
+		$this -> CurrentRow = array();
+
+		if (!$this -> TableOpen)
+		{
+			$TableCounter = 0;
+			$SkipRead = false;
+
+			while ($this -> Valid = ($SkipRead || $this -> Content -> read()))
+			{
+				if ($SkipRead)
+				{
+					$SkipRead = false;
+				}
+
+				if ($this -> Content -> name == 'table:table' && $this -> Content -> nodeType != XMLReader::END_ELEMENT)
+				{
+					if ($TableCounter == $this -> CurrentSheet)
+					{
+						$this -> TableOpen = true;
+						break;
+					}
+
+					$TableCounter++;
+					$this -> Content -> next();
+					$SkipRead = true;
+				}
+			}
+		}
+
+		if ($this -> TableOpen && !$this -> RowOpen)
+		{
+			while ($this -> Valid = $this -> Content -> read())
+			{
+				switch ($this -> Content -> name)
+				{
+					case 'table:table':
+						$this -> TableOpen = false;
+						$this -> Content -> next('office:document-content');
+						$this -> Valid = false;
+						break 2;
+					case 'table:table-row':
+						if ($this -> Content -> nodeType != XMLReader::END_ELEMENT)
+						{
+							$this -> RowOpen = true;
+							break 2;
+						}
+						break;
+				}
+			}
+		}
+
+		if ($this -> RowOpen)
+		{
+			$LastCellContent = '';
+
+			while ($this -> Valid = $this -> Content -> read())
+			{
+				switch ($this -> Content -> name)
+				{
+					case 'table:table-cell':
+						if ($this -> Content -> nodeType == XMLReader::END_ELEMENT || $this -> Content -> isEmptyElement)
+						{
+							if ($this -> Content -> nodeType == XMLReader::END_ELEMENT)
+							{
+								$CellValue = $LastCellContent;
+							}
+							elseif ($this -> Content -> isEmptyElement)
+							{
+								$LastCellContent = '';
+								$CellValue = $LastCellContent;
+							}
+
+							$this -> CurrentRow[] = $LastCellContent;
+
+							if ($this -> Content -> getAttribute('table:number-columns-repeated') !== null)
+							{                                                                                            
+								$RepeatedColumnCount = $this -> Content -> getAttribute('table:number-columns-repeated');
+								// Checking if larger than one because the value is already added to the row once before
+								if ($RepeatedColumnCount > 1)
+								{
+									$this -> CurrentRow = array_pad($this -> CurrentRow, count($this -> CurrentRow) + $RepeatedColumnCount - 1, $LastCellContent);
+								}
+							}
+						}
+						else
+						{
+							$LastCellContent = '';
+						}
+					case 'text:p':
+						if ($this -> Content -> nodeType != XMLReader::END_ELEMENT)
+						{
+							$LastCellContent = $this -> Content -> readString();
+						}
+						break;
+					case 'table:table-row':
+						$this -> RowOpen = false;
+						break 2;
+				}
+			}
+		}
+
+		return $this -> CurrentRow;
+	}
+
+	/** 
+	 * Return the identifying key of the current element.
+	 * Similar to the key() function for arrays in PHP
+	 *
+	 * @return mixed either an integer or a string
+	 */ 
+	public function key()
+	{
+		return $this -> Index;
+	}
+
+	/** 
+	 * Check if there is a current element after calls to rewind() or next().
+	 * Used to check if we've iterated to the end of the collection
+	 *
+	 * @return boolean FALSE if there's nothing more to iterate over
+	 */ 
+	public function valid()
+	{
+		return $this -> Valid;
+	}
+
+	// !Countable interface method
+	/**
+	 * Ostensibly should return the count of the contained items but this just returns the number
+	 * of rows read so far. It's not really correct but at least coherent.
+	 */
+	public function count()
+	{
+		return $this -> Index + 1;
+	}
+}

+ 118 - 0
PHPExcelReader/SpreadsheetReader/SpreadsheetReader_XLS.php

@@ -0,0 +1,118 @@
+<?php
+
+class SpreadsheetReader_XLS implements Iterator, Countable {
+	private $handle = false;
+	private $index = 0;
+	private $rowCount = null;
+	private $currentSheet = 0;
+	private $currentRow = null;
+	
+	public  $error = false;
+	
+	public function __construct($filePath) {
+		self::classLoad();
+		$this->handle = new Spreadsheet_Excel_Reader($filePath);
+		if ($this->handle->error) {
+			$this->error = true;
+			return false;
+		}
+	}
+	
+	public function __destruct() {
+		unset($this->handle);
+	}
+	
+	/**
+	 * Retrieves an array with information about sheets in the current file
+	 *
+	 * @return array List of sheets (key is sheet index, value is name)
+	 */
+	public function Sheets() {
+		$this->sheetInfo = $this->handle->getWorksheetInfo();
+		$this->rowCount = $this->sheetInfo['totalRows'];
+		
+		return $this->sheetInfo;
+	}
+	
+	/**
+	 * Changes the current sheet in the file to another
+	 * @param $index int
+	 * @return bool
+	 */
+	public function ChangeSheet($index)	{
+		return $this->handle->ChangeSheet($index);
+	}
+	
+	/**
+	 * Rewind the Iterator to the first element.
+	 */
+	public function rewind() {
+		$this->index = 0;
+	}
+	
+	/**
+	 * Return the current element.
+	 * @return mixed
+	 */
+	public function current() {
+		if ($this->index == 0 && is_null($this->currentRow)) {
+			$this->next();
+			$this->index = 0;
+		}
+
+		return $this->currentRow;
+	}
+	
+	/**
+	 * Move forward to next element.
+	 */
+	public function next() {
+		$this->currentRow = array();
+		
+		$this->index++;
+		$this->currentRow = $this->handle->getCell();
+		
+		return $this->currentRow;
+	}
+	
+	/**
+	 * Return the identifying key of the current element.
+	 * @return mixed
+	 */
+	public function key() {
+		return $this->index;
+	}
+	
+	/**
+	 * Check if there is a current element after calls to rewind() or next().
+	 * @return boolean
+	 */
+	public function valid()	{
+		if ($this->error) {
+			return false;
+		}
+		
+		return ($this->index < $this->count());
+	}
+	
+	/**
+	 * return the count of the contained items
+	 */
+	public function count() {
+		if ($this->error) {
+			return 0;
+		}
+		
+		if(is_null($this->rowCount)){
+			$this->Sheets();
+		}
+		
+		return $this->rowCount;
+	}
+	
+	private static function classLoad()	{
+		if ( ! class_exists('Spreadsheet_Excel_Reader', false)) {
+			require dirname(__FILE__) . DIRECTORY_SEPARATOR . 'SpreadsheetReader.php';
+		}
+	}
+}

+ 1227 - 0
PHPExcelReader/SpreadsheetReader/SpreadsheetReader_XLSX.php

@@ -0,0 +1,1227 @@
+<?php
+
+class SpreadsheetReader_XLSX implements Iterator, Countable {
+	const CELL_TYPE_BOOL = 'b';
+	const CELL_TYPE_NUMBER = 'n';
+	const CELL_TYPE_ERROR = 'e';
+	const CELL_TYPE_SHARED_STR = 's';
+	const CELL_TYPE_STR = 'str';
+	const CELL_TYPE_INLINE_STR = 'inlineStr';
+
+	/**
+	 * Number of shared strings that can be reasonably cached, i.e., that aren't read from file but stored in memory.
+	 *	If the total number of shared strings is higher than this, caching is not used.
+	 *	If this value is null, shared strings are cached regardless of amount.
+	 *	With large shared string caches there are huge performance gains, however a lot of memory could be used which
+	 *	can be a problem, especially on shared hosting.
+	 */
+	const SHARED_STRING_CACHE_LIMIT = 50000;
+
+	private $Options = array(
+		'TempDir' => '',
+		'ReturnDateTimeObjects' => false
+	);
+
+	private static $RuntimeInfo = array(
+		'GMPSupported' => false
+	);
+
+	private $Valid = false;
+
+	/**
+	 * @var SpreadsheetReader_* Handle for the reader object
+	 */
+	private $Handle = false;
+
+	// Worksheet file
+	/**
+	 * @var string Path to the worksheet XML file
+	 */
+	private $WorksheetPath = false;
+	/**
+	 * @var XMLReader XML reader object for the worksheet XML file
+	 */
+	private $Worksheet = false;
+
+	// Shared strings file
+	/**
+	 * @var string Path to shared strings XML file
+	 */
+	private $SharedStringsPath = false;
+	/**
+	 * @var XMLReader XML reader object for the shared strings XML file
+	 */
+	private $SharedStrings = false;
+	/**
+	 * @var array Shared strings cache, if the number of shared strings is low enough
+	 */
+	private $SharedStringCache = array();
+
+	// Workbook data
+	/**
+	 * @var SimpleXMLElement XML object for the workbook XML file
+	 */
+	private $WorkbookXML = false;
+
+	// Style data
+	/**
+	 * @var SimpleXMLElement XML object for the styles XML file
+	 */
+	private $StylesXML = false;
+	/**
+	 * @var array Container for cell value style data
+	 */
+	private $Styles = array();
+
+	private $TempDir = '';
+	private $TempFiles = array();
+
+	private $CurrentRow = false;
+	private $rowCount = null;
+	
+	// Runtime parsing data
+	/**
+	 * @var int Current row in the file
+	 */
+	private $Index = 0;
+
+	/**
+	 * @var array Data about separate sheets in the file
+	 */
+	private $Sheets = false;
+
+	private $SharedStringCount = 0;
+	private $SharedStringIndex = 0;
+	private $LastSharedStringValue = null;
+
+	private $RowOpen = false;
+
+	private $SSOpen = false;
+	private $SSForwarded = false;
+
+	private static $BuiltinFormats = array(
+		0 => '',
+		1 => '0',
+		2 => '0.00',
+		3 => '#,##0',
+		4 => '#,##0.00',
+
+		9 => '0%',
+		10 => '0.00%',
+		11 => '0.00E+00',
+		12 => '# ?/?',
+		13 => '# ??/??',
+		14 => 'mm-dd-yy',
+		15 => 'd-mmm-yy',
+		16 => 'd-mmm',
+		17 => 'mmm-yy',
+		18 => 'h:mm AM/PM',
+		19 => 'h:mm:ss AM/PM',
+		20 => 'h:mm',
+		21 => 'h:mm:ss',
+		22 => 'm/d/yy h:mm',
+
+		37 => '#,##0 ;(#,##0)',
+		38 => '#,##0 ;[Red](#,##0)',
+		39 => '#,##0.00;(#,##0.00)',
+		40 => '#,##0.00;[Red](#,##0.00)',
+
+		45 => 'mm:ss',
+		46 => '[h]:mm:ss',
+		47 => 'mmss.0',
+		48 => '##0.0E+0',
+		49 => '@',
+
+		// CHT & CHS
+		27 => '[$-404]e/m/d',
+		30 => 'm/d/yy',
+		36 => '[$-404]e/m/d',
+		50 => '[$-404]e/m/d',
+		57 => '[$-404]e/m/d',
+
+		// THA
+		59 => 't0',
+		60 => 't0.00',
+		61 =>'t#,##0',
+		62 => 't#,##0.00',
+		67 => 't0%',
+		68 => 't0.00%',
+		69 => 't# ?/?',
+		70 => 't# ??/??'
+	);
+	private $Formats = array();
+
+	private static $DateReplacements = array(
+		'All' => array(
+			'\\' => '',
+			'am/pm' => 'A',
+			'yyyy' => 'Y',
+			'yy' => 'y',
+			'mmmmm' => 'M',
+			'mmmm' => 'F',
+			'mmm' => 'M',
+			':mm' => ':i',
+			'mm' => 'm',
+			'm' => 'n',
+			'dddd' => 'l',
+			'ddd' => 'D',
+			'dd' => 'd',
+			'd' => 'j',
+			'ss' => 's',
+			'.s' => ''
+		),
+		'24H' => array(
+			'hh' => 'H',
+			'h' => 'G'
+		),
+		'12H' => array(
+			'hh' => 'h',
+			'h' => 'G'
+		)
+	);
+
+	private static $BaseDate = false;
+	private static $DecimalSeparator = '.';
+	private static $ThousandSeparator = '';
+	private static $CurrencyCode = '';
+
+	/**
+	 * @var array Cache for already processed format strings
+	 */
+	private $ParsedFormatCache = array();
+
+	/**
+	 * @param string Path to file
+	 * @param array Options:
+	 *	TempDir => string Temporary directory path
+	 *	ReturnDateTimeObjects => bool True => dates and times will be returned as PHP DateTime objects, false => as strings
+	 */
+	public function __construct($Filepath, array $Options = null)
+	{
+		if (!is_readable($Filepath))
+		{
+			throw new Exception('SpreadsheetReader_XLSX: File not readable ('.$Filepath.')');
+		}
+
+		$this -> TempDir = isset($Options['TempDir']) && is_writable($Options['TempDir']) ?
+			$Options['TempDir'] :
+			sys_get_temp_dir();
+
+		$this -> TempDir = rtrim($this -> TempDir, DIRECTORY_SEPARATOR);
+		$this -> TempDir = $this -> TempDir.DIRECTORY_SEPARATOR.uniqid().DIRECTORY_SEPARATOR;
+
+		$Zip = new ZipArchive;
+		$Status = $Zip -> open($Filepath);
+
+		if ($Status !== true)
+		{
+			throw new Exception('SpreadsheetReader_XLSX: File not readable ('.$Filepath.') (Error '.$Status.')');
+		}
+
+		// Getting the general workbook information
+		if ($Zip -> locateName('xl/workbook.xml') !== false)
+		{
+			$this -> WorkbookXML = new SimpleXMLElement($Zip -> getFromName('xl/workbook.xml'));
+		}
+
+		// Extracting the XMLs from the XLSX zip file
+		if ($Zip -> locateName('xl/sharedStrings.xml') !== false)
+		{
+			$this -> SharedStringsPath = $this -> TempDir.'xl'.DIRECTORY_SEPARATOR.'sharedStrings.xml';
+			$Zip -> extractTo($this -> TempDir, 'xl/sharedStrings.xml');
+			$this -> TempFiles[] = $this -> TempDir.'xl'.DIRECTORY_SEPARATOR.'sharedStrings.xml';
+
+			if (is_readable($this -> SharedStringsPath))
+			{
+				$this -> SharedStrings = new XMLReader;
+				$this -> SharedStrings -> open($this -> SharedStringsPath);
+				$this -> PrepareSharedStringCache();
+			}
+		}
+
+		$Sheets = $this -> Sheets();
+
+		foreach ($this -> Sheets as $Index => $Name)
+		{
+			if ($Zip -> locateName('xl/worksheets/sheet'.$Index.'.xml') !== false)
+			{
+				$Zip -> extractTo($this -> TempDir, 'xl/worksheets/sheet'.$Index.'.xml');
+				$this -> TempFiles[] = $this -> TempDir.'xl'.DIRECTORY_SEPARATOR.'worksheets'.DIRECTORY_SEPARATOR.'sheet'.$Index.'.xml';
+			}
+		}
+
+		$this -> ChangeSheet(0);
+
+		// If worksheet is present and is OK, parse the styles already
+		if ($Zip -> locateName('xl/styles.xml') !== false)
+		{
+			$this -> StylesXML = new SimpleXMLElement($Zip -> getFromName('xl/styles.xml'));
+			if ($this -> StylesXML && $this -> StylesXML -> cellXfs && $this -> StylesXML -> cellXfs -> xf)
+			{
+				foreach ($this -> StylesXML -> cellXfs -> xf as $Index => $XF)
+				{
+					// Format #0 is a special case - it is the "General" format that is applied regardless of applyNumberFormat
+					if ($XF -> attributes() -> applyNumberFormat || (0 == (int)$XF -> attributes() -> numFmtId))
+					{
+						$FormatId = (int)$XF -> attributes() -> numFmtId;
+						// If format ID >= 164, it is a custom format and should be read from styleSheet\numFmts
+						$this -> Styles[] = $FormatId;
+					}
+					else
+					{
+						// 0 for "General" format
+						$this -> Styles[] = 0;
+					}
+				}
+			}
+			
+			if ($this -> StylesXML -> numFmts && $this -> StylesXML -> numFmts -> numFmt)
+			{
+				foreach ($this -> StylesXML -> numFmts -> numFmt as $Index => $NumFmt)
+				{
+					$this -> Formats[(int)$NumFmt -> attributes() -> numFmtId] = (string)$NumFmt -> attributes() -> formatCode;
+				}
+			}
+
+			unset($this -> StylesXML);
+		}
+
+		$Zip -> close();
+
+		// Setting base date
+		if (!self::$BaseDate)
+		{
+			self::$BaseDate = new DateTime;
+			self::$BaseDate -> setTimezone(new DateTimeZone('UTC'));
+			self::$BaseDate -> setDate(1900, 1, 0);
+			self::$BaseDate -> setTime(0, 0, 0);
+		}
+
+		// Decimal and thousand separators
+		if (!self::$DecimalSeparator && !self::$ThousandSeparator && !self::$CurrencyCode)
+		{
+			$Locale = localeconv();
+			self::$DecimalSeparator = $Locale['decimal_point'];
+			self::$ThousandSeparator = $Locale['thousands_sep'];
+			self::$CurrencyCode = $Locale['int_curr_symbol'];
+		}
+
+		if (function_exists('gmp_gcd'))
+		{
+			self::$RuntimeInfo['GMPSupported'] = true;
+		}
+	}
+
+	/**
+	 * Destructor, destroys all that remains (closes and deletes temp files)
+	 */
+	public function __destruct()
+	{
+		foreach ($this -> TempFiles as $TempFile)
+		{
+			@unlink($TempFile);
+		}
+
+		// Better safe than sorry - shouldn't try deleting '.' or '/', or '..'.
+		if (strlen($this -> TempDir) > 2)
+		{
+			@rmdir($this -> TempDir.'xl'.DIRECTORY_SEPARATOR.'worksheets');
+			@rmdir($this -> TempDir.'xl');
+			@rmdir($this -> TempDir);
+		}
+
+		if ($this -> Worksheet && $this -> Worksheet instanceof XMLReader)
+		{
+			$this -> Worksheet -> close();
+			unset($this -> Worksheet);
+		}
+		unset($this -> WorksheetPath);
+
+		if ($this -> SharedStrings && $this -> SharedStrings instanceof XMLReader)
+		{
+			$this -> SharedStrings -> close();
+			unset($this -> SharedStrings);
+		}
+		unset($this -> SharedStringsPath);
+
+		if (isset($this -> StylesXML))
+		{
+			unset($this -> StylesXML);
+		}
+		if ($this -> WorkbookXML)
+		{
+			unset($this -> WorkbookXML);
+		}
+	}
+
+	/**
+	 * Retrieves an array with information about sheets in the current file
+	 *
+	 * @return array List of sheets (key is sheet index, value is name)
+	 */
+	public function Sheets()
+	{
+		if ($this -> Sheets === false)
+		{
+			$this -> Sheets = array();
+			foreach ($this -> WorkbookXML -> sheets -> sheet as $Index => $Sheet)
+			{
+				$Attributes = $Sheet -> attributes('r', true);
+				foreach ($Attributes as $Name => $Value)
+				{
+					if ($Name == 'id')
+					{
+						$SheetID = (int)str_replace('rId', '', (string)$Value);
+						break;
+					}
+				}
+
+				$this -> Sheets[$SheetID] = (string)$Sheet['name'];
+			}
+			ksort($this -> Sheets);
+		}
+		return array_values($this -> Sheets);
+	}
+
+	/**
+	 * Changes the current sheet in the file to another
+	 *
+	 * @param int Sheet index
+	 *
+	 * @return bool True if sheet was successfully changed, false otherwise.
+	 */
+	public function ChangeSheet($Index)
+	{
+		$RealSheetIndex = false;
+		$Sheets = $this -> Sheets();
+		if (isset($Sheets[$Index]))
+		{
+			$SheetIndexes = array_keys($this -> Sheets);
+			$RealSheetIndex = $SheetIndexes[$Index];
+		}
+
+		$TempWorksheetPath = $this -> TempDir.'xl/worksheets/sheet'.$RealSheetIndex.'.xml';
+
+		if ($RealSheetIndex !== false && is_readable($TempWorksheetPath))
+		{
+			$this -> WorksheetPath = $TempWorksheetPath;
+
+			$this -> rewind();
+			return true;
+		}
+
+		return false;
+	}
+
+	/**
+	 * Creating shared string cache if the number of shared strings is acceptably low (or there is no limit on the amount
+	 */
+	private function PrepareSharedStringCache()
+	{
+		while ($this -> SharedStrings -> read())
+		{
+			if ($this -> SharedStrings -> name == 'sst')
+			{
+				$this -> SharedStringCount = $this -> SharedStrings -> getAttribute('count');
+				break;
+			}
+		}
+
+		if (!$this -> SharedStringCount || (self::SHARED_STRING_CACHE_LIMIT < $this -> SharedStringCount && self::SHARED_STRING_CACHE_LIMIT !== null))
+		{
+			return false;
+		}
+
+		$CacheIndex = 0;
+		$CacheValue = '';
+		while ($this -> SharedStrings -> read())
+		{
+			switch ($this -> SharedStrings -> name)
+			{
+				case 'si':
+					if ($this -> SharedStrings -> nodeType == XMLReader::END_ELEMENT)
+					{
+						$this -> SharedStringCache[$CacheIndex] = $CacheValue;
+						$CacheIndex++;
+						$CacheValue = '';
+					}
+					break;
+				case 't':
+					if ($this -> SharedStrings -> nodeType == XMLReader::END_ELEMENT)
+					{
+						continue;
+					}
+					$CacheValue .= $this -> SharedStrings -> readString();
+					break;
+			}
+		}
+
+		$this -> SharedStrings -> close();
+		return true;
+	}
+
+	/**
+	 * Retrieves a shared string value by its index
+	 *
+	 * @param int Shared string index
+	 *
+	 * @return string Value
+	 */
+	private function GetSharedString($Index)
+	{
+		if ((self::SHARED_STRING_CACHE_LIMIT === null || self::SHARED_STRING_CACHE_LIMIT > 0) && !empty($this -> SharedStringCache))
+		{
+			if (isset($this -> SharedStringCache[$Index]))
+			{
+				return $this -> SharedStringCache[$Index];
+			}
+			else
+			{
+				return '';
+			}
+		}
+
+		// If the desired index is before the current, rewind the XML
+		if ($this -> SharedStringIndex > $Index)
+		{
+			$this -> SSOpen = false;
+			$this -> SharedStrings -> close();
+			$this -> SharedStrings -> open($this -> SharedStringsPath);
+			$this -> SharedStringIndex = 0;
+			$this -> LastSharedStringValue = null;
+			$this -> SSForwarded = false;
+		}
+
+		// Finding the unique string count (if not already read)
+		if ($this -> SharedStringIndex == 0 && !$this -> SharedStringCount)
+		{
+			while ($this -> SharedStrings -> read())
+			{
+				if ($this -> SharedStrings -> name == 'sst')
+				{
+					$this -> SharedStringCount = $this -> SharedStrings -> getAttribute('uniqueCount');
+					break;
+				}
+			}
+		}
+
+		// If index of the desired string is larger than possible, don't even bother.
+		if ($this -> SharedStringCount && ($Index >= $this -> SharedStringCount))
+		{
+			return '';
+		}
+
+		// If an index with the same value as the last already fetched is requested
+		// (any further traversing the tree would get us further away from the node)
+		if (($Index == $this -> SharedStringIndex) && ($this -> LastSharedStringValue !== null))
+		{
+			return $this -> LastSharedStringValue;
+		}
+
+		// Find the correct <si> node with the desired index
+		while ($this -> SharedStringIndex <= $Index)
+		{
+			// SSForwarded is set further to avoid double reading in case nodes are skipped.
+			if ($this -> SSForwarded)
+			{
+				$this -> SSForwarded = false;
+			}
+			else
+			{
+				$ReadStatus = $this -> SharedStrings -> read();
+				if (!$ReadStatus)
+				{
+					break;
+				}
+			}
+
+			if ($this -> SharedStrings -> name == 'si')
+			{
+				if ($this -> SharedStrings -> nodeType == XMLReader::END_ELEMENT)
+				{
+					$this -> SSOpen = false;
+					$this -> SharedStringIndex++;
+				}
+				else
+				{
+					$this -> SSOpen = true;
+
+					if ($this -> SharedStringIndex < $Index)
+					{
+						$this -> SSOpen = false;
+						$this -> SharedStrings -> next('si');
+						$this -> SSForwarded = true;
+						$this -> SharedStringIndex++;
+						continue;
+					}
+					else
+					{
+						break;
+					}
+				}
+			}
+		}
+
+		$Value = '';
+
+		// Extract the value from the shared string
+		if ($this -> SSOpen && ($this -> SharedStringIndex == $Index))
+		{
+			while ($this -> SharedStrings -> read())
+			{
+				switch ($this -> SharedStrings -> name)
+				{
+					case 't':
+						if ($this -> SharedStrings -> nodeType == XMLReader::END_ELEMENT)
+						{
+							continue;
+						}
+						$Value .= $this -> SharedStrings -> readString();
+						break;
+					case 'si':
+						if ($this -> SharedStrings -> nodeType == XMLReader::END_ELEMENT)
+						{
+							$this -> SSOpen = false;
+							$this -> SSForwarded = true;
+							break 2;
+						}
+						break;
+				}
+			}
+		}
+
+		if ($Value)
+		{
+			$this -> LastSharedStringValue = $Value;
+		}
+		return $Value;
+	}
+
+	/**
+	 * Formats the value according to the index
+	 *
+	 * @param string Cell value
+	 * @param int Format index
+	 *
+	 * @return string Formatted cell value
+	 */
+	private function FormatValue($Value, $Index)
+	{
+		if (!is_numeric($Value))
+		{
+			return $Value;
+		}
+
+		if (isset($this -> Styles[$Index]) && ($this -> Styles[$Index] !== false))
+		{
+			$Index = $this -> Styles[$Index];
+		}
+		else
+		{
+			return $Value;
+		}
+
+		// A special case for the "General" format
+		if ($Index == 0)
+		{
+			return $this -> GeneralFormat($Value);
+		}
+
+		$Format = array();
+
+		if (isset($this -> ParsedFormatCache[$Index]))
+		{
+			$Format = $this -> ParsedFormatCache[$Index];
+		}
+
+		if (!$Format)
+		{
+			$Format = array(
+				'Code' => false,
+				'Type' => false,
+				'Scale' => 1,
+				'Thousands' => false,
+				'Currency' => false
+			);
+
+			if (isset(self::$BuiltinFormats[$Index]))
+			{
+				$Format['Code'] = self::$BuiltinFormats[$Index];
+			}
+			elseif (isset($this -> Formats[$Index]))
+			{
+				$Format['Code'] = $this -> Formats[$Index];
+			}
+
+			// Format code found, now parsing the format
+			if ($Format['Code'])
+			{
+				$Sections = explode(';', $Format['Code']);
+				$Format['Code'] = $Sections[0];
+				
+				switch (count($Sections))
+				{
+					case 2:
+						if ($Value < 0)
+						{
+							$Format['Code'] = $Sections[1];
+						}
+						$Value = abs($Value);
+						break;
+					case 3:
+					case 4:
+						if ($Value < 0)
+						{
+							$Format['Code'] = $Sections[1];
+						}
+						elseif ($Value == 0)
+						{
+							$Format['Code'] = $Sections[2];
+						}
+						$Value = abs($Value);
+						break;
+				}
+			}
+
+			// Stripping colors
+			$Format['Code'] = trim(preg_replace('/^\\[[a-zA-Z]+\\]/', '', $Format['Code']));
+
+			// Percentages
+			if (substr($Format['Code'], -1) == '%')
+			{
+				$Format['Type'] = 'Percentage';
+			}
+			elseif (preg_match('/^(\[\$[A-Z]*-[0-9A-F]*\])*[hmsdy]/i', $Format['Code']))
+			{
+				$Format['Type'] = 'DateTime';
+
+				$Format['Code'] = trim(preg_replace('/^(\[\$[A-Z]*-[0-9A-F]*\])/i', '', $Format['Code']));
+				$Format['Code'] = strtolower($Format['Code']);
+
+				$Format['Code'] = strtr($Format['Code'], self::$DateReplacements['All']);
+				if (strpos($Format['Code'], 'A') === false)
+				{
+					$Format['Code'] = strtr($Format['Code'], self::$DateReplacements['24H']);
+				}
+				else
+				{
+					$Format['Code'] = strtr($Format['Code'], self::$DateReplacements['12H']);
+				}
+			}
+			elseif ($Format['Code'] == '[$EUR ]#,##0.00_-')
+			{
+				$Format['Type'] = 'Euro';
+			}
+			else
+			{
+				// Removing skipped characters
+				$Format['Code'] = preg_replace('/_./', '', $Format['Code']);
+				// Removing unnecessary escaping
+				$Format['Code'] = preg_replace("/\\\\/", '', $Format['Code']);
+				// Removing string quotes
+				$Format['Code'] = str_replace(array('"', '*'), '', $Format['Code']);
+				// Removing thousands separator
+				if (strpos($Format['Code'], '0,0') !== false || strpos($Format['Code'], '#,#') !== false)
+				{
+					$Format['Thousands'] = true;
+				}
+				$Format['Code'] = str_replace(array('0,0', '#,#'), array('00', '##'), $Format['Code']);
+
+				// Scaling (Commas indicate the power)
+				$Scale = 1;
+				$Matches = array();
+				if (preg_match('/(0|#)(,+)/', $Format['Code'], $Matches))
+				{
+					$Scale = pow(1000, strlen($Matches[2]));
+					// Removing the commas
+					$Format['Code'] = preg_replace(array('/0,+/', '/#,+/'), array('0', '#'), $Format['Code']);
+				}
+
+				$Format['Scale'] = $Scale;
+
+				if (preg_match('/#?.*\?\/\?/', $Format['Code']))
+				{
+					$Format['Type'] = 'Fraction';
+				}
+				else
+				{
+					$Format['Code'] = str_replace('#', '', $Format['Code']);
+
+					$Matches = array();
+					if (preg_match('/(0+)(\.?)(0*)/', preg_replace('/\[[^\]]+\]/', '', $Format['Code']), $Matches))
+					{
+						$Integer = $Matches[1];
+						$DecimalPoint = $Matches[2];
+						$Decimals = $Matches[3];
+
+						$Format['MinWidth'] = strlen($Integer) + strlen($DecimalPoint) + strlen($Decimals);
+						$Format['Decimals'] = $Decimals;
+						$Format['Precision'] = strlen($Format['Decimals']);
+						$Format['Pattern'] = '%0'.$Format['MinWidth'].'.'.$Format['Precision'].'f';
+					}
+				}
+
+				$Matches = array();
+				if (preg_match('/\[\$(.*)\]/u', $Format['Code'], $Matches))
+				{
+					$CurrFormat = $Matches[0];
+					$CurrCode = $Matches[1];
+					$CurrCode = explode('-', $CurrCode);
+					if ($CurrCode)
+					{
+						$CurrCode = $CurrCode[0];
+					}
+
+					if (!$CurrCode)
+					{
+						$CurrCode = self::$CurrencyCode;
+					}
+
+					$Format['Currency'] = $CurrCode;
+				}
+				$Format['Code'] = trim($Format['Code']);
+			}
+
+			$this -> ParsedFormatCache[$Index] = $Format;
+		}
+
+		// Applying format to value
+		if ($Format)
+		{
+    		if ($Format['Code'] == '@')
+    		{
+        		return (string)$Value;
+    		}
+			// Percentages
+			elseif ($Format['Type'] == 'Percentage')
+			{
+				if ($Format['Code'] === '0%')
+				{
+					$Value = round(100 * $Value, 0).'%';
+				}
+				else
+				{
+					$Value = sprintf('%.2f%%', round(100 * $Value, 2));
+				}
+			}
+			// Dates and times
+			elseif ($Format['Type'] == 'DateTime')
+			{
+				$Days = (int)$Value;
+				// Correcting for Feb 29, 1900
+				if ($Days > 60)
+				{
+					$Days--;
+				}
+
+				// At this point time is a fraction of a day
+				$Time = ($Value - (int)$Value);
+				$Seconds = 0;
+				if ($Time)
+				{
+					// Here time is converted to seconds
+					// Some loss of precision will occur
+					$Seconds = (int)($Time * 86400);
+				}
+
+				$Value = clone self::$BaseDate;
+				$Value -> add(new DateInterval('P'.$Days.'D'.($Seconds ? 'T'.$Seconds.'S' : '')));
+
+				if (!$this -> Options['ReturnDateTimeObjects'])
+				{
+					$Value = $Value -> format($Format['Code']);
+				}
+				else
+				{
+					// A DateTime object is returned
+				}
+			}
+			elseif ($Format['Type'] == 'Euro')
+			{
+				$Value = 'EUR '.sprintf('%1.2f', $Value);
+			}
+			else
+			{
+				// Fractional numbers
+				if ($Format['Type'] == 'Fraction' && ($Value != (int)$Value))
+				{
+					$Integer = floor(abs($Value));
+					$Decimal = fmod(abs($Value), 1);
+					// Removing the integer part and decimal point
+					$Decimal *= pow(10, strlen($Decimal) - 2);
+					$DecimalDivisor = pow(10, strlen($Decimal));
+
+					if (self::$RuntimeInfo['GMPSupported'])
+					{
+						$GCD = gmp_strval(gmp_gcd($Decimal, $DecimalDivisor));
+					}
+					else
+					{
+						$GCD = self::GCD($Decimal, $DecimalDivisor);
+					}
+
+					$AdjDecimal = $DecimalPart/$GCD;
+					$AdjDecimalDivisor = $DecimalDivisor/$GCD;
+
+					if (
+						strpos($Format['Code'], '0') !== false || 
+						strpos($Format['Code'], '#') !== false ||
+						substr($Format['Code'], 0, 3) == '? ?'
+					)
+					{
+						// The integer part is shown separately apart from the fraction
+						$Value = ($Value < 0 ? '-' : '').
+							$Integer ? $Integer.' ' : ''.
+							$AdjDecimal.'/'.
+							$AdjDecimalDivisor;
+					}
+					else
+					{
+						// The fraction includes the integer part
+						$AdjDecimal += $Integer * $AdjDecimalDivisor;
+						$Value = ($Value < 0 ? '-' : '').
+							$AdjDecimal.'/'.
+							$AdjDecimalDivisor;
+					}
+				}
+				else
+				{
+					// Scaling
+					$Value = $Value / $Format['Scale'];
+
+					if (!empty($Format['MinWidth']) && $Format['Decimals'])
+					{
+						if ($Format['Thousands'])
+						{
+							$Value = number_format($Value, $Format['Precision'],
+								self::$DecimalSeparator, self::$ThousandSeparator);
+							
+							$Value = preg_replace('/(0+)(\.?)(0*)/', $Value, $Format['Code']);
+						}
+						else{
+							if (preg_match('/[0#]E[+-]0/i', $Format['Code'])) {
+								//	Scientific format
+								$Value = sprintf('%5.2E', $Value);
+							}
+							else {
+								$Value = sprintf($Format['Pattern'], $Value);
+								$Value = preg_replace('/(0+)(\.?)(0*)/', $Value, $Format['Code']);
+							}
+						}
+					}
+				}
+
+				// Currency/Accounting
+				if ($Format['Currency'])
+				{
+					$Value = preg_replace('', $Format['Currency'], $Value);
+				}
+			}
+			
+		}
+
+		return $Value;
+	}
+
+	/**
+	 * Attempts to approximate Excel's "general" format.
+	 *
+	 * @param mixed Value
+	 *
+	 * @return mixed Result
+	 */
+	public function GeneralFormat($Value)
+	{
+		// Numeric format
+		if (is_numeric($Value))
+		{
+			$Value = (float)$Value;
+		}
+		return $Value;
+	}
+
+	// !Iterator interface methods
+	/** 
+	 * Rewind the Iterator to the first element.
+	 * Similar to the reset() function for arrays in PHP
+	 */ 
+	public function rewind()
+	{
+		// Removed the check whether $this -> Index == 0 otherwise ChangeSheet doesn't work properly
+
+		// If the worksheet was already iterated, XML file is reopened.
+		// Otherwise it should be at the beginning anyway
+		if ($this -> Worksheet instanceof XMLReader)
+		{
+			$this -> Worksheet -> close();
+		}
+		else
+		{
+			$this -> Worksheet = new XMLReader;
+		}
+
+		$this -> Worksheet -> open($this -> WorksheetPath);
+
+		$this -> Valid = true;
+		$this -> RowOpen = false;
+		$this -> CurrentRow = false;
+		$this -> Index = 0;
+	}
+
+	/**
+	 * Return the current element.
+	 * Similar to the current() function for arrays in PHP
+	 *
+	 * @return mixed current element from the collection
+	 */
+	public function current()
+	{
+		if ($this -> Index == 0 && $this -> CurrentRow === false)
+		{
+			$this -> rewind();
+			$this -> next();
+			$this -> Index--;
+		}
+		return $this -> CurrentRow;
+	}
+
+	/** 
+	 * Move forward to next element. 
+	 * Similar to the next() function for arrays in PHP 
+	 */ 
+	public function next()
+	{
+		$this -> Index++;
+
+		$this -> CurrentRow = array();
+
+		if (!$this -> RowOpen)
+		{
+			while ($this -> Valid = $this -> Worksheet -> read())
+			{
+				if ($this -> Worksheet -> name == 'row')
+				{
+					// Getting the row spanning area (stored as e.g., 1:12)
+					// so that the last cells will be present, even if empty
+					$RowSpans = $this -> Worksheet -> getAttribute('spans');
+					if ($RowSpans)
+					{
+						$RowSpans = explode(':', $RowSpans);
+						$CurrentRowColumnCount = $RowSpans[1];
+					}
+					else
+					{
+						$CurrentRowColumnCount = 0;
+					}
+
+					if ($CurrentRowColumnCount > 0)
+					{
+						$this -> CurrentRow = array_fill(0, $CurrentRowColumnCount, '');
+					}
+
+					$this -> RowOpen = true;
+					break;
+				}
+			}
+		}
+
+		// Reading the necessary row, if found
+		if ($this -> RowOpen)
+		{
+			// These two are needed to control for empty cells
+			$MaxIndex = 0;
+			$CellCount = 0;
+
+			$CellHasSharedString = false;
+
+			while ($this -> Valid = $this -> Worksheet -> read())
+			{
+				switch ($this -> Worksheet -> name)
+				{
+					// End of row
+					case 'row':
+						if ($this -> Worksheet -> nodeType == XMLReader::END_ELEMENT)
+						{
+							$this -> RowOpen = false;
+							break 2;
+						}
+						break;
+					// Cell
+					case 'c':
+						// If it is a closing tag, skip it
+						if ($this -> Worksheet -> nodeType == XMLReader::END_ELEMENT)
+						{
+							continue;
+						}
+
+						$StyleId = (int)$this -> Worksheet -> getAttribute('s');
+
+						// Get the index of the cell
+						$Index = $this -> Worksheet -> getAttribute('r');
+						$Letter = preg_replace('{[^[:alpha:]]}S', '', $Index);
+						$Index = self::IndexFromColumnLetter($Letter);
+
+						// Determine cell type
+						if ($this -> Worksheet -> getAttribute('t') == self::CELL_TYPE_SHARED_STR)
+						{
+							$CellHasSharedString = true;
+						}
+						else
+						{
+							$CellHasSharedString = false;
+						}
+
+						$this -> CurrentRow[$Index] = '';
+
+						$CellCount++;
+						if ($Index > $MaxIndex)
+						{
+							$MaxIndex = $Index;
+						}
+
+						break;
+					// Cell value
+					case 'v':
+					case 'is':
+						if ($this -> Worksheet -> nodeType == XMLReader::END_ELEMENT)
+						{
+							continue;
+						}
+
+						$Value = $this -> Worksheet -> readString();
+
+						if ($CellHasSharedString)
+						{
+							$Value = $this -> GetSharedString($Value);
+						}
+
+						// Format value if necessary
+						if ($Value !== '' && $StyleId && isset($this -> Styles[$StyleId]))
+						{
+							$Value = $this -> FormatValue($Value, $StyleId);
+						}
+						elseif ($Value)
+						{
+							$Value = $this -> GeneralFormat($Value);
+						}
+
+						$this -> CurrentRow[$Index] = $Value;
+						break;
+				}
+			}
+
+			// Adding empty cells, if necessary
+			// Only empty cells inbetween and on the left side are added
+			if ($MaxIndex + 1 > $CellCount)
+			{
+				$this -> CurrentRow = $this -> CurrentRow + array_fill(0, $MaxIndex + 1, '');
+				ksort($this -> CurrentRow);
+			}
+		}
+
+		return $this -> CurrentRow;
+	}
+
+	/** 
+	 * Return the identifying key of the current element.
+	 * Similar to the key() function for arrays in PHP
+	 *
+	 * @return mixed either an integer or a string
+	 */ 
+	public function key()
+	{
+		return $this -> Index;
+	}
+
+	/** 
+	 * Check if there is a current element after calls to rewind() or next().
+	 * Used to check if we've iterated to the end of the collection
+	 *
+	 * @return boolean FALSE if there's nothing more to iterate over
+	 */ 
+	public function valid()
+	{
+		return $this -> Valid;
+	}
+
+	// !Countable interface method
+	/**
+	 * Ostensibly should return the count of the contained items but this just returns the number
+	 * of rows read so far. It's not really correct but at least coherent.
+	 */
+	public function count()
+	{
+		if(is_null($this->rowCount)){
+			$total = 0;
+			while($this -> Worksheet -> read()){
+				if ($this -> Worksheet -> name == 'row' && $this -> Worksheet -> nodeType != XMLReader::END_ELEMENT){
+					$total++;
+				}
+			}
+			
+			$this->rowCount = $total;
+		}
+		
+		
+		return $this->rowCount;
+	}
+
+	/**
+	 * Takes the column letter and converts it to a numerical index (0-based)
+	 *
+	 * @param string Letter(s) to convert
+	 *
+	 * @return mixed Numeric index (0-based) or boolean false if it cannot be calculated
+	 */
+	public static function IndexFromColumnLetter($Letter)
+	{
+		$Powers = array();
+
+		$Letter = strtoupper($Letter);
+
+		$Result = 0;
+		for ($i = strlen($Letter) - 1, $j = 0; $i >= 0; $i--, $j++)
+		{
+			$Ord = ord($Letter[$i]) - 64;
+			if ($Ord > 26)
+			{
+				// Something is very, very wrong
+				return false;
+			}
+			$Result += $Ord * pow(26, $j);
+		}
+		return $Result - 1;
+	}
+
+	/**
+	 * Helper function for greatest common divisor calculation in case GMP extension is
+	 *	not enabled
+	 *
+	 * @param int Number #1
+	 * @param int Number #2
+	 *
+	 * @param int Greatest common divisor
+	 */
+	public static function GCD($A, $B)
+	{
+		$A = abs($A);
+		$B = abs($B);
+		if ($A + $B == 0)
+		{
+			return 0;
+		}
+		else
+		{
+			$C = 1;
+
+			while ($A > 0)
+			{
+				$C = $A;
+				$A = $B % $A;
+				$B = $C;
+			}
+
+			return $C;
+		}
+	}
+}

+ 23 - 0
demo.php

@@ -0,0 +1,23 @@
+<?php
+require 'PHPExcelReader/PHPExcelReader.php';
+
+try{
+	$Reader = new PHPExcelReader('test.xls');
+	$total = $Reader->count();			// get the total rows of records
+	//$current = $Reader->current();	// get the current row data
+	
+	/*
+	$Reader->seek(4);					// skip to the 4th row 
+	$row = $Reader->current();			// get the 4th row data
+	*/
+	
+	/*
+	foreach($Reader as $key => $row){
+		$data[] = $row;					// loop obtain row data
+	}
+	*/
+	
+	var_dump($total);
+} catch (Exception $e) {
+	die($e->getMessage());
+}