Переглянути джерело

PHPExcel 2.0 版本释出

Janson 7 роки тому
батько
коміт
5ba48f4164

+ 0 - 38
PHPExcelReader/PHPExcel/Autoloader.php

@@ -1,38 +0,0 @@
-<?php
-/**
- * autoload library
- */
-PHPExcel_Autoloader::Register();
-
-class PHPExcel_Autoloader {
-    /**
-     * Register the Autoloader with SPL
-     */
-    public static function Register() {
-        if (function_exists('__autoload')) {
-            spl_autoload_register('__autoload');
-        }
-
-        return spl_autoload_register(array('PHPExcel_Autoloader', 'Load'));
-    }
-
-    /**
-     * Autoload a class identified by name
-     *
-     * @param string $pClassName Name of the object to load
-     * @return string
-     */
-    public static function Load($pClassName) {
-        if ((class_exists($pClassName, false)) || (strpos($pClassName, 'PHPExcel') !== 0)) {
-            return false;
-        }
-
-        $pClassFilePath = PHPEXCEL_ROOT . str_replace('_', DIRECTORY_SEPARATOR, $pClassName) . '.php';
-
-        if ((file_exists($pClassFilePath) === false) || (is_readable($pClassFilePath) === false)) {
-            return false;
-        }
-
-        require $pClassFilePath;
-    }
-}

+ 0 - 228
PHPExcelReader/PHPExcel/Reader/CSV.php

@@ -1,228 +0,0 @@
-<?php
-
-class PHPExcel_Reader_CSV implements Iterator, Countable {
-    private $_fileHandle = null;
-
-    private $filePath = '';
-
-    private $_inputEncoding = 'UTF-8';
-
-    private $_delimiter = ',';
-
-    private $_enclosure = '"';
-
-    private $_filter = 0;
-
-    /**
-     * @param string $filePath
-     * @param int $filter filter empty row
-     *
-     * @throws Exception
-     */
-    public function __construct($filePath, $filter = 0) {
-        if (! file_exists($filePath)) {
-            throw new Exception("Could not open " . $filePath . " for reading! File does not exist.");
-        }
-
-        $this->filePath = $filePath;
-        $this->_filter = $filter;
-        ini_set('auto_detect_line_endings', true);
-
-        $this->_fileHandle = fopen($filePath, 'r');
-        $this->_detectEncoding();
-    }
-
-    /**
-     * Move filepointer past any BOM marker
-     */
-    private function _detectEncoding() {
-        $step = $BOMLength = 0;
-        while ($step < 3) {
-            $BOM = bin2hex(fread($this->_fileHandle, 2 + $step++));
-
-            rewind($this->_fileHandle);
-
-            if ($BOM == 'fffe' || $BOM == 'feff') {
-                $BOMLength = 2;
-                $this->_delimiter = "\t";
-                $this->_inputEncoding = 'UTF-16';
-                break;
-            } else {
-                if ($BOM == 'efbbbf') {
-                    $BOMLength = 3;
-                    break;
-                } else {
-                    if ($BOM == '0000feff' || $BOM == 'fffe0000') {
-                        $BOMLength = 4;
-                        $this->_delimiter = "\t";
-                        $this->_inputEncoding = 'UTF-32';
-                        break;
-                    }
-                }
-            }
-        }
-
-        if (! $BOMLength) {
-            $encoding = mb_detect_encoding(fgets($this->_fileHandle, 1024), 'ASCII, UTF-8, GB2312, GBK');
-            rewind($this->_fileHandle);
-            if ($encoding) {
-                if ($encoding == 'EUC-CN') {
-                    $this->_inputEncoding = 'GB2312';
-                } else {
-                    if ($encoding == 'CP936') {
-                        $this->_inputEncoding = 'GBK';
-                    } else {
-                        $this->_inputEncoding = $encoding;
-                    }
-                }
-            }
-        }
-
-        if ($this->_inputEncoding != 'UTF-8') {
-            stream_filter_register("convert_iconv.*", "convert_iconv_filter");
-            stream_filter_append($this->_fileHandle, 'convert_iconv.' . $this->_inputEncoding . '/UTF-8');
-        }
-    }
-
-    /**
-     * Returns information about sheets in the file.
-     * @return array
-     */
-    public function Sheets() {
-        return array(0 => basename($this->filePath));
-    }
-
-    /**
-     * Changes sheet to another.
-     *
-     * @param int $index
-     * @return bool
-     */
-    public function ChangeSheet($index) {
-        if ($index == 0) {
-            $this->rewind();
-
-            return true;
-        }
-
-        return false;
-    }
-
-    /**
-     * Rewind the Iterator to the first element.
-     */
-    public function rewind() {
-        rewind($this->_fileHandle);
-        $this->currentRow = null;
-        $this->index = 0;
-    }
-
-    /**
-     * Return the current element.
-     * @return mixed
-     */
-    public function current() {
-        if ($this->index == 0 && ! isset($this->currentRow)) {
-            $this->rewind();
-            $this->next();
-            $this->index = 0;
-        }
-
-        return $this->currentRow;
-    }
-
-    /**
-     * Move forward to next element.
-     */
-    public function next() {
-        $this->currentRow = array();
-
-        $this->index++;
-        while (($row = fgetcsv($this->_fileHandle, 0, $this->_delimiter, $this->_enclosure)) !== false) {
-            if (! $this->_filter || array_filter($row, array($this, 'filter'))) {
-                $this->currentRow = $row;
-                break;
-            }
-        }
-
-        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() {
-        if ($this->currentRow || ! feof($this->_fileHandle)) {
-            return true;
-        } else {
-            fclose($this->_fileHandle);
-
-            return false;
-        }
-    }
-
-    /**
-     * return the count of the contained items
-     * @return int
-     */
-    public function count() {
-        if (! isset($this->rowCount)) {
-            $total = 0;
-            rewind($this->_fileHandle);
-            while (($row = fgetcsv($this->_fileHandle, 0, $this->_delimiter, $this->_enclosure)) !== false) {
-                if (! $this->_filter || array_filter($row, array($this, 'filter'))) {
-                    $total++;
-                }
-            }
-
-            $this->rowCount = $total;
-        }
-
-        return $this->rowCount;
-    }
-
-    /**
-     * filter empty string
-     *
-     * @param mixed $value
-     *
-     * @return boolean
-     */
-    private function filter($value) {
-        return trim($value) !== '';
-    }
-}
-
-class convert_iconv_filter extends php_user_filter {
-    private $modes;
-
-    function filter($in, $out, &$consumed, $closing) {
-        while ($bucket = stream_bucket_make_writeable($in)) {
-            $bucket->data = mb_convert_encoding($bucket->data, $this->modes[1], $this->modes[0]);
-            $consumed += $bucket->datalen;
-            stream_bucket_append($out, $bucket);
-        }
-
-        return PSFS_PASS_ON;
-    }
-
-    function onCreate() {
-        $format = explode('/', substr($this->filtername, 14));
-        if (count($format) == 2) {
-            $this->modes = $format;
-
-            return true;
-        } else {
-            return false;
-        }
-    }
-}

+ 0 - 1644
PHPExcelReader/PHPExcel/Reader/Excel5.php

@@ -1,1644 +0,0 @@
-<?php
-
-class PHPExcel_Reader_Excel5 {
-	// 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 static $_codepage = 'CP1252';
-
-	private $index = 0;
-	private $curretSheet = 0;
-	private $builtInFormats = array(
-		0 => 'General',
-		1 => '0',
-		2 => '0.00',
-		3 => '#,##0',
-		4 => '#,##0.00',
-
-		9 => '0%',
-		10 => '0.00%',
-		11 => '0.00E+00',
-		12 => '# ?/?',
-		13 => '# ??/??',
-		14 => 'yyyy/m/d',
-		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 => 'yyyy/m/d h:mm',
-		28 => 'm月d日',
-		31 => 'yyyy年m月d日',
-		32 => 'h时mmi分',
-		33 => 'h时mmi分ss秒',
-		34 => 'AM/PM h时mmi分',
-		35 => 'AM/PM h时mmi分ss秒',
-
-		37 => '#,##0 ;(#,##0)',
-		38 => '#,##0 ;[Red](#,##0)',
-		39 => '#,##0.00;(#,##0.00)',
-		40 => '#,##0.00;[Red](#,##0.00)',
-
-		44 => '_("$"* #,##0.00_);_("$"* \(#,##0.00\);_("$"* "-"??_);_(@_)',
-		45 => 'mm:ss',
-		46 => '[h]:mm:ss',
-		47 => 'mm:ss.0',
-		48 => '##0.0E+0',
-		49 => '@',
-
-		// CHT
-		27 => 'yyyy年m月',
-		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# ??/??'	
-	);
-
-	public $error = false;
-
-	/**
-	 * Create a new Spreadsheet_Excel_Reader instance
-	 */
-	public function __construct($file) {
-		if ( ! file_exists($file)) {
-			throw new Exception("Could not open " . $file . " for reading! File does not exist.");
-		}
-
-		try {
-			$ole = new PHPExcel_Reader_OLERead();
-			$ole->read($file);
-			$this->_data = $ole->getStream($ole->wrkbook);
-
-			return true;
-		} catch (Exception $e) {
-			return false;
-		}
-	}
-
-	/**
-	 * 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_RK2:
-				case self::XLS_Type_LABELSST:
-				case self::XLS_Type_NUMBER:
-				case self::XLS_Type_FORMULA:
-				case self::XLS_Type_BOOLERR:
-				case self::XLS_Type_LABEL:
-					$length = self::_GetInt2d($this->_data, $this->_pos + 2);
-					$recordData = substr($this->_data, $this->_pos + 4, $length);
-
-					// move stream pointer to next record
-					$this->_pos += 4 + $length;
-
-					$rowIndex = self::_GetInt2d($recordData, 0) + 1;
-					$columnIndex = self::_GetInt2d($recordData, 2);
-
-					$sheetInfo['totalRows'] = max($sheetInfo['totalRows'], $rowIndex);
-					$sheetInfo['lastColumnIndex'] = max($sheetInfo['lastColumnIndex'], $columnIndex);
-					break;
-				case self::XLS_Type_BOF:      $this->_readBof();          break;
-				case self::XLS_Type_EOF:      $this->_readDefault();      break 2;
-				default:                      $this->_readDefault();      break;
-			}
-
-			$sheetInfo['totalColumns'] = $sheetInfo['lastColumnIndex'] + 1;
-			$sheetInfo['lastColumnLetter'] = self::_stringFromColumnIndex($sheetInfo['lastColumnIndex']);
-		}
-
-		return $sheetInfo;
-	}
-
-	private function _addCell($row, $column, $value, $xfIndex){
-		if(is_null($this->_key)){
-			$this->_key = $row;
-		}
-
-		if($row > $this->_key){
-			$this->_endRow = true;
-			return false;
-		}
-
-		$xfRecord = $this->xfRecords[$xfIndex];
-		$this->_lastPos = $this->_pos;
-		$this->_cell[$column] = $this->_format_value($value, $xfRecord['format']);
-	}
-
-	private function _format_value($value = '0', $format = 'General'){
-		if ( ! is_numeric($value) || $format == 'General' || $format == '@') return $value;
-
-		$sections = explode(';', $format);
-		switch (count($sections)) {
-			case 1:
-				$format = $sections[0];
-				break;
-
-			case 2:
-				$format = ($value >= 0) ? $sections[0] : $sections[1];
-				$value = abs($value); // Use the absolute value
-				break;
-
-			case 3:
-				$format = ($value > 0) ?
-				$sections[0] : ( ($value < 0) ?
-				$sections[1] : $sections[2]);
-				$value = abs($value); // Use the absolute value
-				break;
-
-			case 4:
-				$format = ($value > 0) ?
-				$sections[0] : ( ($value < 0) ?
-				$sections[1] : $sections[2]);
-				$value = abs($value); // Use the absolute value
-				break;
-
-			default:
-				// something is wrong, just use first section
-				$format = $sections[0];
-				break;
-		}
-
-		$color_regex = '/^\\[[a-zA-Z]+\\]/';
-		$format = preg_replace($color_regex, '', $format);
-
-		if (preg_match('/(\[\$[A-Z]*-[0-9A-F]*\])*[hmsdy]/i', $format)) { // datetime format
-			$value = $this->_formatAsDate($value, $format);
-		} else if (preg_match('/%$/', $format)) { // % number format
-			$value = self::_formatAsPercentage($value, $format);
-		} else {
-			if ($format === '[$EUR ]#,##0.00_-') {
-				$value = 'EUR ' . sprintf('%1.2f', $value);
-			} else {
-				// In Excel formats, "_" is used to add spacing, which we can't do in HTML
-				$format = preg_replace('/_./', '', $format);
-
-				// Some non-number characters are escaped with \, which we don't need
-				$format = preg_replace("/\\\\/", '', $format);
-
-				// Some non-number strings are quoted, so we'll get rid of the quotes, likewise any positional * symbols
-				$format = str_replace(array('"','*'), '', $format);
-
-				// Find out if we need thousands separator
-				// This is indicated by a comma enclosed by a digit placeholder:
-				//		#,#   or   0,0
-				$useThousands = preg_match('/(#,#|0,0)/', $format);
-				if ($useThousands) {
-					$format = preg_replace('/0,0/', '00', $format);
-					$format = preg_replace('/#,#/', '##', $format);
-				}
-
-				// Scale thousands, millions,...
-				// This is indicated by a number of commas after a digit placeholder:
-				//		#,   or	0.0,,
-				$scale = 1; // same as no scale
-				$matches = array();
-				if (preg_match('/(#|0)(,+)/', $format, $matches)) {
-					$scale = pow(1000, strlen($matches[2]));
-
-					// strip the commas
-					$format = preg_replace('/0,+/', '0', $format);
-					$format = preg_replace('/#,+/', '#', $format);
-				}
-
-				if (preg_match('/#?.*\?\/\?/', $format, $m)) {
-					//echo 'Format mask is fractional '.$format.' <br />';
-					if ($value != (int)$value) {
-						self::_formatAsFraction($value, $format);
-					}
-				} else {
-					// Handle the number itself
-
-					// scale number
-					$value = $value / $scale;
-
-					// Strip #
-					$format = preg_replace('/\\#/', '0', $format);
-
-					$n = "/\[[^\]]+\]/";
-					$m = preg_replace($n, '', $format);
-					$number_regex = "/(0+)(\.?)(0*)/";
-					if (preg_match($number_regex, $m, $matches)) {
-						$left = $matches[1];
-						$dec = $matches[2];
-						$right = $matches[3];
-
-						// minimun width of formatted number (including dot)
-						$minWidth = strlen($left) + strlen($dec) + strlen($right);
-						if ($useThousands) {
-							$value = number_format(
-									$value
-									, strlen($right)
-									, '.'
-									, ','
-							);
-							$value = preg_replace($number_regex, $value, $format);
-						} else {
-							if (preg_match('/[0#]E[+-]0/i', $format)) {
-								//	Scientific format
-								$value = sprintf('%5.2E', $value);
-							} elseif (preg_match('/0([^\d\.]+)0/', $format)) {
-								$value = self::_complexNumberFormatMask($value, $format);
-							} else {
-								$sprintf_pattern = "%0$minWidth." . strlen($right) . "f";
-								$value = sprintf($sprintf_pattern, $value);
-								$value = preg_replace($number_regex, $value, $format);
-							}
-						}
-					}
-				}
-
-				if (preg_match('/\[\$(.*)\]/u', $format, $m)) {
-					//	Currency or Accounting
-					$currencyFormat = $m[0];
-					$currencyCode = $m[1];
-					list($currencyCode) = explode('-',$currencyCode);
-					if ($currencyCode == '') {
-						$currencyCode = '$';
-					}
-					$value = preg_replace('/\[\$([^\]]*)\]/u',$currencyCode,$value);
-				}
-			}
-		}
-
-		return $value;
-	}
-
-	private static function _complexNumberFormatMask($number, $mask) {
-		if (strpos($mask,'.') !== false) {
-			$numbers = explode('.', $number . '.0');
-			$masks = explode('.', $mask . '.0');
-			$result1 = self::_complexNumberFormatMask($numbers[0], $masks[0]);
-			$result2 = strrev(self::_complexNumberFormatMask(strrev($numbers[1]), strrev($masks[1])));
-			return $result1 . '.' . $result2;
-		}
-
-		$r = preg_match_all('/0+/', $mask, $result, PREG_OFFSET_CAPTURE);
-		if ($r > 1) {
-			$result = array_reverse($result[0]);
-
-			foreach($result as $block) {
-				$divisor = 1 . $block[0];
-				$size = strlen($block[0]);
-				$offset = $block[1];
-
-				$blockValue = sprintf(
-						'%0' . $size . 'd',
-						fmod($number, $divisor)
-				);
-				$number = floor($number / $divisor);
-				$mask = substr_replace($mask,$blockValue, $offset, $size);
-			}
-			if ($number > 0) {
-				$mask = substr_replace($mask, $number, $offset, 0);
-			}
-			$result = $mask;
-		} else {
-			$result = $number;
-		}
-
-		return $result;
-	}
-
-	private static function _formatAsFraction(&$value, &$format)
-	{
-		$sign = ($value < 0) ? '-' : '';
-
-		$integerPart = floor(abs($value));
-		$decimalPart = trim(fmod(abs($value),1),'0.');
-		$decimalLength = strlen($decimalPart);
-		$decimalDivisor = pow(10,$decimalLength);
-
-		$GCD = self::GCD(array($decimalPart, $decimalDivisor));
-
-		$adjustedDecimalPart = $decimalPart/$GCD;
-		$adjustedDecimalDivisor = $decimalDivisor/$GCD;
-
-		if ((strpos($format,'0') !== false) || (strpos($format,'#') !== false) || (substr($format,0,3) == '? ?')) {
-			if ($integerPart == 0) {
-				$integerPart = '';
-			}
-			$value = "$sign$integerPart $adjustedDecimalPart/$adjustedDecimalDivisor";
-		} else {
-			$adjustedDecimalPart += $integerPart * $adjustedDecimalDivisor;
-			$value = "$sign$adjustedDecimalPart/$adjustedDecimalDivisor";
-		}
-	}
-
-	private static function GCD($params) {
-		$returnValue = 1;
-		$allValuesFactors = array();
-
-		$flattenArr = self::flattenArray($params);
-		foreach($flattenArr as $value) {
-			if (!is_numeric($value)) {
-				return '#VALUE!';
-			} elseif ($value == 0) {
-				continue;
-			} elseif($value < 0) {
-				return '#NULL!';
-			}
-			$myFactors = self::_factors($value);
-			$myCountedFactors = array_count_values($myFactors);
-			$allValuesFactors[] = $myCountedFactors;
-		}
-		$allValuesCount = count($allValuesFactors);
-		if ($allValuesCount == 0) {
-			return 0;
-		}
-
-		$mergedArray = $allValuesFactors[0];
-		for ($i=1;$i < $allValuesCount; ++$i) {
-			$mergedArray = array_intersect_key($mergedArray,$allValuesFactors[$i]);
-		}
-		$mergedArrayValues = count($mergedArray);
-		if ($mergedArrayValues == 0) {
-			return $returnValue;
-		} elseif ($mergedArrayValues > 1) {
-			foreach($mergedArray as $mergedKey => $mergedValue) {
-				foreach($allValuesFactors as $highestPowerTest) {
-					foreach($highestPowerTest as $testKey => $testValue) {
-						if (($testKey == $mergedKey) && ($testValue < $mergedValue)) {
-							$mergedArray[$mergedKey] = $testValue;
-							$mergedValue = $testValue;
-						}
-					}
-				}
-			}
-
-			$returnValue = 1;
-			foreach($mergedArray as $key => $value) {
-				$returnValue *= pow($key,$value);
-			}
-			return $returnValue;
-		} else {
-			$keys = array_keys($mergedArray);
-			$key = $keys[0];
-			$value = $mergedArray[$key];
-			foreach($allValuesFactors as $testValue) {
-				foreach($testValue as $mergedKey => $mergedValue) {
-					if (($mergedKey == $key) && ($mergedValue < $value)) {
-						$value = $mergedValue;
-					}
-				}
-			}
-			return pow($key,$value);
-		}
-	}
-
-	//
-	//	Private method to return an array of the factors of the input value
-	//
-	private static function _factors($value) {
-		$startVal = floor(sqrt($value));
-
-		$factorArray = array();
-		for ($i = $startVal; $i > 1; --$i) {
-			if (($value % $i) == 0) {
-				$factorArray = array_merge($factorArray,self::_factors($value / $i));
-				$factorArray = array_merge($factorArray,self::_factors($i));
-				if ($i <= sqrt($value)) {
-					break;
-				}
-			}
-		}
-		if (!empty($factorArray)) {
-			rsort($factorArray);
-			return $factorArray;
-		} else {
-			return array((integer) $value);
-		}
-	}
-
-	private static function flattenArray($array) {
-		if (!is_array($array)) {
-			return (array) $array;
-		}
-
-		$arrayValues = array();
-		foreach ($array as $value) {
-			if (is_array($value)) {
-				foreach ($value as $val) {
-					if (is_array($val)) {
-						foreach ($val as $v) {
-							$arrayValues[] = $v;
-						}
-					} else {
-						$arrayValues[] = $val;
-					}
-				}
-			} else {
-				$arrayValues[] = $value;
-			}
-		}
-
-		return $arrayValues;
-	}
-
-	private static function _formatAsPercentage(&$value, &$format)
-	{
-		if ($format === '0%') {
-			$value = round( (100 * $value), 0) . '%';
-		} else {
-			if (preg_match('/\.[#0]+/i', $format, $m)) {
-				$s = substr($m[0], 0, 1) . (strlen($m[0]) - 1);
-				$format = str_replace($m[0], $s, $format);
-			}
-			if (preg_match('/^[#0]+/', $format, $m)) {
-				$format = str_replace($m[0], strlen($m[0]), $format);
-			}
-			$format = '%' . str_replace('%', 'f%%', $format);
-	
-			$value = sprintf($format, 100 * $value);
-		}
-
-		return $value;
-	}
-
-	/**
-	 * Search/replace values to convert Excel date/time format masks to PHP format masks
-	 *
-	 * @var array
-	 */
-	private static $_dateFormatReplacements = array(
-			// first remove escapes related to non-format characters
-			'\\'	=> '',
-			//	12-hour suffix
-			'am/pm'	=> 'A',
-			//	4-digit year
-			'e'	=> 'Y',
-			'yyyy'	=> 'Y',
-			//	2-digit year
-			'yy'	=> 'y',
-			//	first letter of month - no php equivalent
-			'mmmmm'	=> 'M',
-			//	full month name
-			'mmmm'	=> 'F',
-			//	short month name
-			'mmm'	=> 'M',
-			//	mm is minutes if time, but can also be month w/leading zero
-			//	so we try to identify times be the inclusion of a : separator in the mask
-			//	It isn't perfect, but the best way I know how
-			':mm'	=> ':i',
-			'mm:'	=> 'i:',
-			'mmi'	=> 'i',
-			//	month leading zero
-			'mm'	=> 'm',
-			//	month no leading zero
-			'm'		=> 'n',
-			//	full day of week name
-			'dddd'	=> 'l',
-			//	short day of week name
-			'ddd'	=> 'D',
-			//	days leading zero
-			'dd'	=> 'd',
-			//	days no leading zero
-			'd'		=> 'j',
-			//	seconds
-			'ss'	=> 's',
-			//	fractional seconds - no php equivalent
-			'.s'	=> ''
-	);
-	/**
-	 * Search/replace values to convert Excel date/time format masks hours to PHP format masks (24 hr clock)
-	 *
-	 * @var array
-	*/
-	private static $_dateFormatReplacements24 = array(
-			'hh'	=> 'H',
-			'h'		=> 'G'
-	);
-	/**
-	 * Search/replace values to convert Excel date/time format masks hours to PHP format masks (12 hr clock)
-	 *
-	 * @var array
-	*/
-	private static $_dateFormatReplacements12 = array(
-			'hh'	=> 'h',
-			'h'		=> 'g'
-	);
-	
-	private function _formatAsDate(&$value, &$format)
-	{
-		// dvc: convert Excel formats to PHP date formats
-	
-		// strip off first part containing e.g. [$-F800] or [$USD-409]
-		// general syntax: [$<Currency string>-<language info>]
-		// language info is in hexadecimal
-		$format = preg_replace('/^(\[\$[A-Z]*-[0-9A-F]*\])/i', '', $format);
-	
-		// OpenOffice.org uses upper-case number formats, e.g. 'YYYY', convert to lower-case
-		$format = strtolower($format);
-
-		$format = strtr($format,self::$_dateFormatReplacements);
-		if (strpos($format,'A') === false) {	// 24-hour time format
-			$format = strtr($format,self::$_dateFormatReplacements24);
-		} else {					// 12-hour time format
-			$format = strtr($format,self::$_dateFormatReplacements12);
-		}
-		$dateTime = $this->ExcelToPHP($value);
-		$days = floor($dateTime / 86400);
-		$time = round((($dateTime / 86400) - $days) * 86400);
-		$hours = round($time / 3600);
-		$minutes = round($time / 60) - ($hours * 60);
-		$seconds = round($time) - ($hours * 3600) - ($minutes * 60);
-
-		$dateObj = date_create('1-Jan-1970+'.$days.' days');
-		$dateObj->setTime($hours,$minutes,$seconds);
-
-		return $dateObj->format($format);
-	}
-
-	private function ExcelToPHP($dateValue = 0) {
-		if ($this->_excelBaseDate == 1900) {
-			$my_excelBaseDate = 25569;
-			//	Adjust for the spurious 29-Feb-1900 (Day 60)
-			if ($dateValue < 60) {
-				--$my_excelBaseDate;
-			}
-		} else {
-			$my_excelBaseDate = 24107;
-		}
-
-		// Perform conversion
-		if ($dateValue >= 1) {
-			$utcDays = $dateValue - $my_excelBaseDate;
-			$returnValue = round($utcDays * 86400);
-			if (($returnValue <= PHP_INT_MAX) && ($returnValue >= -PHP_INT_MAX)) {
-				$returnValue = (integer) $returnValue;
-			}
-		} else {
-			$hours = round($dateValue * 24);
-			$mins = round($dateValue * 1440) - round($hours * 60);
-			$secs = round($dateValue * 86400) - round($hours * 3600) - round($mins * 60);
-			$returnValue = (integer) gmmktime($hours, $mins, $secs);
-		}
-
-		// Return
-		return $returnValue;
-	}
-
-	/**
-	 * 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);
-
-		self::$_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);
-		$recordData = substr($this->_data, $this->_pos + 4, $length);
-		
-		$this->_pos += 4 + $length;
-		$indexCode = self::_GetInt2d($recordData, 0);
-
-		if ($this->_version == self::XLS_BIFF8) {
-			$string = str_replace('"', '', self::_readUnicodeStringLong(substr($recordData, 2)));
-		} else {
-			// BIFF7
-			$string = self::_readByteStringShort(substr($recordData, 2));
-		}
-
-		$formatString = $string['value'];
-		$this->formatRecords[$indexCode] = $formatString;		
-	}
-
-	/**
-	 * 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);
-		$recordData = substr($this->_data, $this->_pos + 4, $length);
-
-		$this->_pos += 4 + $length;
-		$indexCode = self::_GetInt2d($recordData, 2);
-
-		if (isset($this->formatRecords[$indexCode])) {
-			$format = $this->formatRecords[$indexCode];
-		}
-		else if (isset($this->builtInFormats[$indexCode])) {
-			$format = $this->builtInFormats[$indexCode];
-		}
-		else {
-			$format = 'General';
-		}
-
-		$this->xfRecords[] = array(
-				'index'  => $indexCode,
-				'format' => $format
-		);
-	}
-
-	/**
-	 * 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);
-
-		$xfIndex = self::_GetInt2d($recordData, 4);
-		$rknum = self::_GetInt4d($recordData, 6);
-		$numValue = self::_GetIEEE754($rknum);
-		// add cell
-		$this->_addCell($row, $column, $numValue, $xfIndex);
-	}
-
-	/**
-	 * 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;
-		$xfIndex = self::_GetInt2d($recordData, 4);
-		$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'], $xfIndex);
-	}
-
-	/**
-	 * 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) {
-			$xfIndex = self::_GetInt2d($recordData, $offset);
-			$numValue = self::_GetIEEE754(self::_GetInt4d($recordData, $offset + 2));		
-			$this->_addCell($row, $colFirst + $i, $numValue, $xfIndex);
-
-			$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);
-		$xfIndex = self::_GetInt2d($recordData, 4);
-
-		$numValue = self::_extractNumber(substr($recordData, 6, 8));
-		$this->_addCell($row, $column, $numValue, $xfIndex);
-	}
-
-	/**
-	 * 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;
-		$xfIndex = self::_GetInt2d($recordData, 4);
-		$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
-			$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
-			$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
-			$value = '';
-		}
-		else {
-			// forumla result is a number, first 14 bytes like _NUMBER record
-			$value = self::_extractNumber(substr($recordData, 6, 8));
-		}
-
-		$this->_addCell($row, $column, $value, $xfIndex);
-	}
-
-	/**
-	 * 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;
-		$xfIndex = self::_GetInt2d($recordData, 4);
-
-		if ($this->_version == self::XLS_BIFF8) {
-			$string = self::_readUnicodeStringLong($recordData);
-			$value = $string['value'];
-		} else {
-			$string = self::_readByteStringLong($recordData);
-			$value = $string['value'];
-		}
-
-		$this->_addCell($this->_preRow, $this->_preColumn, $value, $xfIndex);
-	}
-
-	/**
-	 * 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;
-		$xfIndex = self::_GetInt2d($recordData, 4);
-		$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, $xfIndex);
-				break;
-
-			case 1: // error type
-				$value = self::_mapErrorCode($boolErr);
-
-				// add cell value
-				$this->_addCell($row, $column, $value, $xfIndex);
-				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;
-		$xfIndex = self::_GetInt2d($recordData, 4);
-		$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 = self::_readByteStringLong(substr($recordData, 6));
-			$value = $string['value'];
-		}
-		$this->_addCell($row, $column, $value, $xfIndex);
-	}
-
-	/**
-	 * 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;
-		$xfIndex = self::_GetInt2d($recordData, 4);
-		$row = self::_GetInt2d($recordData, 0);
-		$column = self::_GetInt2d($recordData, 2);
-		$this->_addCell($row, $column, '', $xfIndex);			
-	}
-
-	/**
-	 * 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 static 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 = self::_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', self::$_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;
-	}
-}

+ 0 - 315
PHPExcelReader/PHPExcel/Reader/OLERead.php

@@ -1,315 +0,0 @@
-<?php
-/**
- * PHPExcel
- *
- * Copyright (c) 2006 - 2014 PHPExcel
- *
- * This library is free software; you can redistribute it and/or
- * modify it under the terms of the GNU Lesser General Public
- * License as published by the Free Software Foundation; either
- * version 2.1 of the License, or (at your option) any later version.
- *
- * This library is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
- * Lesser General Public License for more details.
- *
- * You should have received a copy of the GNU Lesser General Public
- * License along with this library; if not, write to the Free Software
- * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
- *
- * @category   PHPExcel
- * @package    PHPExcel_Reader
- * @copyright  Copyright (c) 2006 - 2014 PHPExcel (http://www.codeplex.com/PHPExcel)
- * @license    http://www.gnu.org/licenses/old-licenses/lgpl-2.1.txt	LGPL
- * @version    1.8.0, 2014-03-02
- */
-
-defined('IDENTIFIER_OLE') ||
-    define('IDENTIFIER_OLE', pack('CCCCCCCC', 0xd0, 0xcf, 0x11, 0xe0, 0xa1, 0xb1, 0x1a, 0xe1));
-
-class PHPExcel_Reader_OLERead {
-	private $data = '';
-
-	// OLE identifier
-	const IDENTIFIER_OLE = IDENTIFIER_OLE;
-
-	// Size of a sector = 512 bytes
-	const BIG_BLOCK_SIZE					= 0x200;
-
-	// Size of a short sector = 64 bytes
-	const SMALL_BLOCK_SIZE					= 0x40;
-
-	// Size of a directory entry always = 128 bytes
-	const PROPERTY_STORAGE_BLOCK_SIZE		= 0x80;
-
-	// Minimum size of a standard stream = 4096 bytes, streams smaller than this are stored as short streams
-	const SMALL_BLOCK_THRESHOLD				= 0x1000;
-
-	// 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 $wrkbook						= null;
-	public $summaryInformation			= null;
-	public $documentSummaryInformation	= null;
-
-
-	/**
-	 * Read the file
-	 *
-	 * @param $sFileName string Filename
-	 * @throws Exception
-	 */
-	public function read($sFileName)
-	{
-		// Check if file exists and is readable
-		if(!is_readable($sFileName)) {
-			throw new Exception("Could not open " . $sFileName . " for reading! File does not exist, or it is not readable.");
-		}
-
-		// Get the file identifier
-		// Don't bother reading the whole file until we know it's a valid OLE file
-		$this->data = file_get_contents($sFileName, FALSE, NULL, 0, 8);
-
-		// Check OLE identifier
-		if ($this->data != self::IDENTIFIER_OLE) {
-			throw new Exception('The filename ' . $sFileName . ' is not recognised as an OLE file');
-		}
-
-		// Get the file data
-		$this->data = file_get_contents($sFileName);
-
-		// Total number of sectors used for the SAT
-		$this->numBigBlockDepotBlocks = self::_GetInt4d($this->data, self::NUM_BIG_BLOCK_DEPOT_BLOCKS_POS);
-
-		// SecID of the first sector of the directory stream
-		$this->rootStartBlock = self::_GetInt4d($this->data, self::ROOT_START_BLOCK_POS);
-
-		// SecID of the first sector of the SSAT (or -2 if not extant)
-		$this->sbdStartBlock = self::_GetInt4d($this->data, self::SMALL_BLOCK_DEPOT_BLOCK_POS);
-
-		// SecID of the first sector of the MSAT (or -2 if no additional sectors are used)
-		$this->extensionBlock = self::_GetInt4d($this->data, self::EXTENSION_BLOCK_POS);
-
-		// Total number of sectors used by MSAT
-		$this->numExtensionBlocks = self::_GetInt4d($this->data, self::NUM_EXTENSION_BLOCK_POS);
-
-		$bigBlockDepotBlocks = array();
-		$pos = self::BIG_BLOCK_DEPOT_BLOCKS_POS;
-
-		$bbdBlocks = $this->numBigBlockDepotBlocks;
-
-		if ($this->numExtensionBlocks != 0) {
-			$bbdBlocks = (self::BIG_BLOCK_SIZE - self::BIG_BLOCK_DEPOT_BLOCKS_POS)/4;
-		}
-
-		for ($i = 0; $i < $bbdBlocks; ++$i) {
-			  $bigBlockDepotBlocks[$i] = 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, $sbdBlock*4);
-		}
-
-		// read the directory stream
-		$block = $this->rootStartBlock;
-		$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);
-			}
-
-			return $streamData;
-		} else {
-			$numBlocks = $this->props[$stream]['size'] / self::BIG_BLOCK_SIZE;
-			if ($this->props[$stream]['size'] % self::BIG_BLOCK_SIZE != 0) {
-				++$numBlocks;
-			}
-
-			if ($numBlocks == 0) return '';
-
-			$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($bl)
-	{
-		$block = $bl;
-		$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, $block*4);
-		}
-		return $data;
-	 }
-
-	/**
-	 * Read entries in the directory stream.
-	 */
-	private function _readPropertySets() {
-		$offset = 0;
-
-		// loop through entires, each entry is 128 bytes
-		$entryLen = strlen($this->entry);
-		while ($offset < $entryLen) {
-			// entry data (128 bytes)
-			$d = substr($this->entry, $offset, self::PROPERTY_STORAGE_BLOCK_SIZE);
-
-			// size in bytes of name
-			$nameSize = ord($d[self::SIZE_OF_NAME_POS]) | (ord($d[self::SIZE_OF_NAME_POS+1]) << 8);
-
-			// type of entry
-			$type = ord($d[self::TYPE_POS]);
-
-			// sectorID of first sector or short sector, if this entry refers to a stream (the case with workbook)
-			// sectorID of first sector of the short-stream container stream, if this entry is root entry
-			$startBlock = self::_GetInt4d($d, self::START_BLOCK_POS);
-
-			$size = self::_GetInt4d($d, self::SIZE_POS);
-
-			$name = str_replace("\x00", "", substr($d,0,$nameSize));
-
-
-			$this->props[] = array (
-				'name' => $name,
-				'type' => $type,
-				'startBlock' => $startBlock,
-				'size' => $size);
-
-			// tmp helper to simplify checks
-			$upName = strtoupper($name);
-
-			// Workbook directory entry (BIFF5 uses Book, BIFF8 uses Workbook)
-			if (($upName === 'WORKBOOK') || ($upName === 'BOOK')) {
-				$this->wrkbook = count($this->props) - 1;
-			}
-			else if ( $upName === 'ROOT ENTRY' || $upName === 'R') {
-				// Root entry
-				$this->rootentry = count($this->props) - 1;
-			}
-
-			// Summary information
-			if ($name == chr(5) . 'SummaryInformation') {
-//				echo 'Summary Information<br />';
-				$this->summaryInformation = count($this->props) - 1;
-			}
-
-			// Additional Document Summary information
-			if ($name == chr(5) . 'DocumentSummaryInformation') {
-//				echo 'Document Summary Information<br />';
-				$this->documentSummaryInformation = count($this->props) - 1;
-			}
-
-			$offset += self::PROPERTY_STORAGE_BLOCK_SIZE;
-		}
-	}
-
-	/**
-	 * Read 4 bytes of data at specified position
-	 *
-	 * @param string $data
-	 * @param int $pos
-	 * @return int
-	 */
-	private static function _GetInt4d($data, $pos)
-	{
-		// FIX: represent numbers correctly on 64-bit system
-		// http://sourceforge.net/tracker/index.php?func=detail&aid=1487372&group_id=99160&atid=623334
-		// Hacked by Andreas Rehm 2006 to ensure correct result of the <<24 block on 32 and 64bit systems
-		$_or_24 = ord($data[$pos + 3]);
-		if ($_or_24 >= 128) {
-			// negative number
-			$_ord_24 = -abs((256 - $_or_24) << 24);
-		} else {
-			$_ord_24 = ($_or_24 & 127) << 24;
-		}
-		return ord($data[$pos]) | (ord($data[$pos + 1]) << 8) | (ord($data[$pos + 2]) << 16) | $_ord_24;
-	}
-}

+ 0 - 125
PHPExcelReader/PHPExcel/Reader/XLS.php

@@ -1,125 +0,0 @@
-<?php
-
-class PHPExcel_Reader_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) {
-		if ( ! file_exists($filePath)) {
-			throw new Exception("Could not open " . $filePath . " for reading! File does not exist.");
-		}
-
-		try {
-			$this->handle = new PHPExcel_Reader_Excel5($filePath);
-
-			return true;
-		} catch (Exception $e) {
-			$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 && ! isset($this->currentRow)) {
-			$this->rewind();
-			$this->next();
-			$this->Index--;
-		}
-
-		return $this->currentRow;
-	}
-
-	/**
-	 * Move forward to next element.
-	 */
-	public function next() {
-		$this->currentRow = array();
-		if( ! $this->sheetInfo) {
-			$this->Sheets();
-		}
-
-		$this->index++;
-		$cell = $this->handle->getCell();
-		for($i = 0; $i < $this->sheetInfo['totalColumns']; $i++) {
-			$this->currentRow[$i] = isset($cell[$i]) ? $cell[$i] : '';
-		}
-
-		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( ! isset($this->rowCount)){
-			$this->Sheets();
-		}
-
-		return $this->rowCount;
-	}
-}

+ 0 - 996
PHPExcelReader/PHPExcel/Reader/XLSX.php

@@ -1,996 +0,0 @@
-<?php
-
-class PHPExcel_Reader_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 => 'yyyy/m/d',
-        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 => 'yyyy/m/d h:mm',
-        31 => 'yyyy年m月d日',
-        32 => 'h时mmi分',
-        33 => 'h时mmi分ss秒',
-        37 => '#,##0 ;(#,##0)',
-        38 => '#,##0 ;[Red](#,##0)',
-        39 => '#,##0.00;(#,##0.00)',
-        40 => '#,##0.00;[Red](#,##0.00)',
-        44 => '_("$"* #,##0.00_);_("$"* \(#,##0.00\);_("$"* "-"??_);_(@_)',
-        45 => 'mm:ss',
-        46 => '[h]:mm:ss',
-        47 => 'mm:ss.0',
-        48 => '##0.0E+0',
-        49 => '@',
-        55 => 'AM/PM h时mmi分',
-        56 => 'AM/PM h时mmi分ss秒',
-        58 => 'm月d日', // CHT & CHS
-        27 => 'yyyy年m月',
-        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',
-            'e' => 'Y',
-            'yyyy' => 'Y',
-            'yy' => 'y',
-            'mmmmm' => 'M',
-            'mmmm' => 'F',
-            'mmm' => 'M',
-            ':mm' => ':i',
-            'mmi' => '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 $Filepath Path to file
-     * @param array $Options  Options:
-     *                        TempDir => string Temporary directory path
-     *                        ReturnDateTimeObjects => bool True => dates and times will be returned as PHP DateTime objects,
-     *                        false => as strings
-     * @throws Exception
-     */
-    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'] = str_replace('"', '', $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 = $Decimal/$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 (! isset($this->rowCount)) {
-            $total = 0;
-            $this->rewind();
-
-            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
-     * @return int
-     */
-    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;
-        }
-    }
-}

+ 0 - 283
PHPExcelReader/PHPExcelReader.php

@@ -1,283 +0,0 @@
-<?php
-/**
- * PHPExcelReader class
- * @version 1.0.0
- * @author  Janson Leung
- */
-
-/** PHPExcel root directory */
-if (! defined('PHPEXCEL_ROOT')) {
-    define('PHPEXCEL_ROOT', dirname(__FILE__) . '/');
-    require(PHPEXCEL_ROOT . 'PHPExcel/Autoloader.php');
-}
-
-class PHPExcelReader implements SeekableIterator, Countable {
-    const TYPE_XLSX = 'XLSX';
-    const TYPE_XLS = 'XLS';
-    const TYPE_CSV = 'CSV';
-
-    private $handle;
-
-    private $type;
-
-    private $index = 0;
-
-    /**
-     * @param string $filePath         Path to file
-     * @param string $originalFileName Original filename (in case of an uploaded file), used to determine file type,
-     *                                 optional
-     * @param string $mimeType         MIME type from an upload, used to determine file type, optional
-     * @throws Exception
-     */
-    public function __construct($filePath, $originalFileName = '', $mimeType = '') {
-        if (! is_readable($filePath)) {
-            throw new Exception('PHPExcel_Reader: 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('PHPExcel_Reader: Original file (2nd parameter) is not a string or a scalar value.');
-        }
-        if (! empty($mimeType) && ! is_scalar($mimeType)) {
-            throw new Exception('PHPExcel_Reader: Mime type (3nd parameter) is not a string or a scalar value.');
-        }
-
-        // 1. Determine type
-        if (! $originalFileName) {
-            $originalFileName = $filePath;
-        }
-
-        $mimeType = $mimeType ?: mime_content_type($filePath);
-        $Extension = strtolower(pathinfo($originalFileName, PATHINFO_EXTENSION));
-        if ($mimeType) {
-            switch ($mimeType) {
-                case 'application/octet-stream':
-                    $this->type = $Extension == 'xlsx' ? self::TYPE_XLSX : self::TYPE_CSV;
-                    break;
-                case 'text/x-comma-separated-values':
-                case 'text/comma-separated-values':
-                case 'application/x-csv':
-                case 'text/x-csv':
-                case 'text/csv':
-                case 'application/csv':
-                case 'application/vnd.msexcel':
-                case 'text/plain':
-                    $this->type = self::TYPE_CSV;
-                    break;
-                case 'application/msexcel':
-                case 'application/x-msexcel':
-                case 'application/x-ms-excel':
-                case 'application/x-excel':
-                case 'application/x-dos_ms_excel':
-                case 'application/xls':
-                case 'application/x-xls':
-                case 'application/download':
-                case 'application/vnd.ms-office':
-                case 'application/msword':
-                case 'application/xlt':
-                    $this->type = self::TYPE_XLS;
-                    break;
-                case 'application/vnd.ms-excel':
-                case 'application/excel':
-                    $this->type = $Extension == 'csv' ? self::TYPE_CSV : self::TYPE_XLS;
-                    break;
-                case 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet':
-                case 'application/vnd.openxmlformats-officedocument.spreadsheetml.template':
-                case 'application/zip':
-                case 'application/x-zip':
-                case 'application/xlsx':
-                case 'application/xltx':
-                    $this->type = self::TYPE_XLSX;
-                    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;
-                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) {
-            $this->handle = new PHPExcel_Reader_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:
-                $this->handle = new PHPExcel_Reader_XLSX($filePath);
-                break;
-            case self::TYPE_CSV:
-                $this->handle = new PHPExcel_Reader_CSV($filePath, 1);
-                break;
-            case self::TYPE_XLS:
-                // Everything already happens above
-                break;
-        }
-    }
-
-    /**
-     * get the type of file
-     * @return string
-     */
-    public function getFileType() {
-        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 $index 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);
-    }
-
-    /**
-     * 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 position in file
-     * @return null
-     */
-    public function seek($position) {
-        if (! $this->handle) {
-            throw new OutOfBoundsException('PHPExcel_Reader: 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('PHPExcel_Reader: position ' . $position . ' not found');
-            }
-        }
-
-        return null;
-    }
-}

+ 107 - 35
README.md

@@ -1,35 +1,107 @@
-**PHPExcelReader** is a lightweight Excel file reading class, support the CSV, XLS, XLSX file. It can read 
-line by line as needed.
-
-### Requirements:
-*  PHP 5.3.0 or newer
-*  PHP must have Zip file support (see http://php.net/manual/en/zip.installation.php)
-
-### Usage:
-
-All data is read from the file sequentially, with each row being returned as a numeric array.
-This is about the easiest way to read a file:
-
-	<?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());
-		}
+# phpexcel
+A lightweight PHP library for reading spreadsheet files
+  - Based on Generator、SeekableIterator and Countable
+  - Support for reading by line, read data only
+
+### Requirements
+
+  - PHP 7.0 or higher
+
+### Installation
+
+    composer require ecweb/phpexcel
+
+## Usage
+
+### csv
+
+```
+// Simple setting 
+$reader = Asan\PHPExcel\Excel::load('files/02.csv', 'GBK');
+
+// Flexible setting
+$reader = Asan\PHPExcel\Excel::load('files/01.csv', function(Asan\PHPExcel\Reader\Csv $reader) {
+    // Set row limit
+    $reader->setRowLimit(10);
+
+    // Set column limit
+    $reader->setColumnLimit(10);
+
+    // Ignore emoty row
+    $reader->ignoreEmptyRow(true);
+
+    // Set encoding
+    //$reader->setInputEncoding('GBK');
+
+    // Set delimiter
+    $reader->setDelimiter("\t");
+}, 'GBK');
+
+// skip to row 50 
+$reader->seek(50);
+
+// Get the current row data
+$current = $reader->current();
+
+// Get row count
+$count = $reader->count();
+```
+
+### xls
+
+```
+$reader = Asan\PHPExcel\Excel::load('files/01.xls', function(Asan\PHPExcel\Reader\Xls $reader) {
+    // Set row limit
+    $reader->setRowLimit(10);
+
+    // Set column limit
+    $reader->setColumnLimit(10);
+
+    // Ignore emoty row
+    $reader->ignoreEmptyRow(true);
+
+    // Select sheet index
+    $reader->setSheetIndex(1);
+});
+
+// skip to row 50 
+$reader->seek(50);
+
+// Get the current row data
+$current = $reader->current();
+
+// Get row count
+$count = $reader->count();
+
+// Get all sheets info
+$sheets = $reader->sheets();
+```
+
+### xlsx
+```
+$reader = Asan\PHPExcel\Excel::load('files/01.xlsx', function(Asan\PHPExcel\Reader\Xlsx $reader) {
+    // Set row limit
+    $reader->setRowLimit(10);
+
+    // Set column limit
+    $reader->setColumnLimit(10);
+
+    // Ignore emoty row
+    $reader->ignoreEmptyRow(true);
+
+    // Select sheet index
+    $reader->setSheetIndex(0);
+});
+
+// skip to row 50 
+$reader->seek(50);
+
+// Get the current row data
+$current = $reader->current();
+
+// Get row count
+$count = $reader->count();
+
+// Get all sheets info
+$sheets = $reader->sheets();
+```

+ 15 - 0
autoload.php

@@ -0,0 +1,15 @@
+<?php
+
+function classLoader($class) {
+    $path = str_replace(
+        ['\\', 'Asan' . DIRECTORY_SEPARATOR . 'PHPExcel' . DIRECTORY_SEPARATOR], [DIRECTORY_SEPARATOR, ''], $class
+    );
+
+    $file = __DIR__ . DIRECTORY_SEPARATOR . 'src' . DIRECTORY_SEPARATOR . $path . '.php';
+
+    if (file_exists($file)) {
+        require_once $file;
+    }
+}
+
+spl_autoload_register('classLoader');

+ 20 - 0
composer.json

@@ -0,0 +1,20 @@
+{
+    "name": "asan/phpexcel",
+    "description": "A lightweight PHP library for reading spreadsheet files",
+    "homepage": "https://github.com/Janson-Leung/PHPExcel",
+    "type": "library",
+    "license": "MIT",
+    "authors": [
+        {
+            "name": "Janson Leung",
+            "homepage": "https://github.com/Janson-Leung"
+        }
+    ],
+    "require": {
+        "php": ">=7.0"
+    },
+    "minimum-stability": "stable",
+    "autoload": {
+        "psr-4": {"Asan\\PHPExcel\\": "src/"}
+    }
+}

+ 0 - 23
demo.php

@@ -1,23 +0,0 @@
-<?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());
-}

+ 12 - 0
src/Contract/ReaderInterface.php

@@ -0,0 +1,12 @@
+<?php
+/**
+ * Reader Interface
+ *
+ * @author Janson
+ * @create 2017-11-23
+ */
+namespace Asan\PHPExcel\Contract;
+
+interface ReaderInterface extends \SeekableIterator, \Countable {
+
+}

+ 114 - 0
src/Excel.php

@@ -0,0 +1,114 @@
+<?php
+/**
+ * PHP Excel
+ *
+ * @author Janson
+ * @create 2017-11-23
+ */
+namespace Asan\PHPExcel;
+
+use Asan\PHPExcel\Exception\ReaderException;
+
+class Excel {
+    /**
+     * Load a file
+     *
+     * @param string $file
+     * @param callback|null $callback
+     * @param string|null $encoding
+     * @param string $ext
+     * @param string $logPath
+     *
+     * @throws ReaderException
+     * @return \Asan\PHPExcel\Reader\BaseReader
+     */
+    public static function load($file, $callback = null, $encoding = null, $ext = '', $logPath = '') {
+        set_error_handler(function($errorNo, $errorMsg, $errorFile, $errorLine) use ($logPath) {
+            if ($logPath) {
+                if (!file_exists($logPath)) {
+                    mkdir($logPath, 0755, true);
+                }
+
+                $content = sprintf(
+                    "%s\t%s.%s\t%s\t%s", date("Y-m-d H:i:s"), self::class, 'ERROR',
+                    "[$errorNo]$errorMsg in $errorFile:$errorLine", PHP_EOL
+                );
+
+                file_put_contents("$logPath/excel-" . date('Y-m-d'). '.log', $content, FILE_APPEND);
+            }
+        }, E_ALL ^ E_ERROR);
+
+        $ext = $ext ?: strtolower(pathinfo($file, PATHINFO_EXTENSION));
+
+        $format = self::getFormatByExtension($ext);
+
+        if (empty($format)) {
+            throw new ReaderException("Could not identify file format for file [$file] with extension [$ext]");
+        }
+
+        $class = __NAMESPACE__ . '\\Reader\\' . $format;
+        $reader = new $class;
+
+        if ($callback) {
+            if ($callback instanceof \Closure) {
+                // Do the callback
+                call_user_func($callback, $reader);
+            } elseif (is_string($callback)) {
+                // Set the encoding
+                $encoding = $callback;
+            }
+        }
+
+        if ($encoding && method_exists($reader, 'setInputEncoding')) {
+            $reader->setInputEncoding($encoding);
+        }
+
+        return $reader->load($file);
+    }
+
+    /**
+     * Identify file format
+     *
+     * @param string $ext
+     * @return string
+     */
+    protected static function getFormatByExtension($ext) {
+        $formart = '';
+
+        switch ($ext) {
+            /*
+            |--------------------------------------------------------------------------
+            | Excel 2007
+            |--------------------------------------------------------------------------
+            */
+            case 'xlsx':
+            case 'xlsm':
+            case 'xltx':
+            case 'xltm':
+                $formart = 'Xlsx';
+                break;
+
+            /*
+            |--------------------------------------------------------------------------
+            | Excel5
+            |--------------------------------------------------------------------------
+            */
+            case 'xls':
+            case 'xlt':
+                $formart = 'Xls';
+                break;
+
+            /*
+            |--------------------------------------------------------------------------
+            | CSV
+            |--------------------------------------------------------------------------
+            */
+            case 'csv':
+            case 'txt':
+                $formart = 'Csv';
+                break;
+        }
+
+        return $formart;
+    }
+}

+ 12 - 0
src/Exception/ParserException.php

@@ -0,0 +1,12 @@
+<?php
+/**
+ * Parser Exception
+ *
+ * @author Janson
+ * @create 2017-11-27
+ */
+namespace Asan\PHPExcel\Exception;
+
+class ParserException extends \Exception {
+
+}

+ 12 - 0
src/Exception/ReaderException.php

@@ -0,0 +1,12 @@
+<?php
+/**
+ * Reader Exception
+ *
+ * @author Janson
+ * @create 2017-11-23
+ */
+namespace Asan\PHPExcel\Exception;
+
+class ReaderException extends \Exception {
+
+}

+ 839 - 0
src/Parser/Excel2007.php

@@ -0,0 +1,839 @@
+<?php
+/**
+ * Excel2017 Parser
+ *
+ * @author Janson
+ * @create 2017-12-02
+ */
+namespace Asan\PHPExcel\Parser;
+
+use Asan\PHPExcel\Exception\ParserException;
+use Asan\PHPExcel\Exception\ReaderException;
+
+class Excel2007 {
+    const CELL_TYPE_SHARED_STR = 's';
+
+    /**
+     * Temporary directory
+     *
+     * @var string
+     */
+    protected $tmpDir;
+
+    /**
+     * ZipArchive reader
+     *
+     * @var \ZipArchive
+     */
+    protected $zip;
+
+    /**
+     * Worksheet reader
+     *
+     * @var \XMLReader
+     */
+    protected $worksheetXML;
+
+    /**
+     * SharedStrings reader
+     *
+     * @var \XMLReader
+     */
+    protected $sharedStringsXML;
+
+    /**
+     * SharedStrings position
+     *
+     * @var array
+     */
+    private $sharedStringsPosition = -1;
+
+    /**
+     * The current sheet of the file
+     *
+     * @var int
+     */
+    private $sheetIndex = 0;
+
+    /**
+     * Ignore empty row
+     *
+     * @var bool
+     */
+    private $ignoreEmpty = false;
+
+    /**
+     * Style xfs
+     *
+     * @var array
+     */
+    private $styleXfs;
+
+    /**
+     * Number formats
+     *
+     * @var array
+     */
+    private $formats;
+
+    /**
+     * Parsed number formats
+     *
+     * @var array
+     */
+    private $parsedFormats;
+
+    /**
+     * Worksheets
+     *
+     * @var array
+     */
+    private $sheets;
+
+    /**
+     * Default options for libxml loader
+     *
+     * @var int
+     */
+    private static $libXmlLoaderOptions;
+
+    /**
+     * Base date
+     *
+     * @var \DateTime
+     */
+    private static $baseDate;
+    private static $decimalSeparator = '.';
+    private static $thousandSeparator = ',';
+    private static $currencyCode = '';
+    private static $runtimeInfo = ['GMPSupported' => false];
+
+    /**
+     * Use ZipArchive reader to extract the relevant data streams from the ZipArchive file
+     *
+     * @throws ParserException|ReaderException
+     * @param string $file
+     */
+    public function loadZip($file) {
+        $this->openFile($file);
+
+        // 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);
+        }
+
+        if (function_exists('gmp_gcd')) {
+            self::$runtimeInfo['GMPSupported'] = true;
+        }
+    }
+
+    /**
+     * Ignore empty row
+     *
+     * @param bool $ignoreEmpty
+     *
+     * @return $this
+     */
+    public function ignoreEmptyRow($ignoreEmpty) {
+        $this->ignoreEmpty = $ignoreEmpty;
+
+        return $this;
+    }
+
+    /**
+     * Whether is ignore empty row
+     *
+     * @return bool
+     */
+    public function isIgnoreEmptyRow() {
+        return $this->ignoreEmpty;
+    }
+
+    /**
+     * Set sheet index
+     *
+     * @param int $index
+     *
+     * @return $this
+     */
+    public function setSheetIndex($index) {
+        if ($index != $this->sheetIndex) {
+            $this->sheetIndex = $index;
+
+            $this->getWorksheetXML();
+        }
+
+        return $this;
+    }
+
+    /**
+     * Get sheet index
+     *
+     * @return int
+     */
+    public function getSheetIndex() {
+        return $this->sheetIndex;
+    }
+
+    /**
+     * Return worksheet info (Name, Last Column Letter, Last Column Index, Total Rows, Total Columns)
+     *
+     * @throws ReaderException
+     * @return array
+     */
+    public function parseWorksheetInfo() {
+        if ($this->sheets === null) {
+            $workbookXML = simplexml_load_string(
+                $this->securityScan($this->zip->getFromName('xl/workbook.xml')), 'SimpleXMLElement', self::getLibXmlLoaderOptions()
+            );
+
+            $this->sheets = [];
+            if (isset($workbookXML->sheets) && $workbookXML->sheets) {
+                $xml = new \XMLReader();
+
+                $index = 0;
+                foreach ($workbookXML->sheets->sheet as $sheet) {
+                    $info = [
+                        'name' => (string)$sheet['name'], 'lastColumnLetter' => '', 'lastColumnIndex' => 0,
+                        'totalRows' => 0, 'totalColumns' => 0
+                    ];
+
+                    $this->zip->extractTo($this->tmpDir, $file = 'xl/worksheets/sheet' . (++$index) . '.xml');
+                    $xml->open($this->tmpDir . '/' . $file, null, self::getLibXmlLoaderOptions());
+
+                    $xml->setParserProperty(\XMLReader::DEFAULTATTRS, true);
+
+                    $nonEmpty = false;
+                    $columnLetter = '';
+                    while ($xml->read()) {
+                        if ($xml->name == 'row') {
+                            if (!$this->ignoreEmpty && $xml->nodeType == \XMLReader::ELEMENT) {
+                                $info['totalRows'] = (int)$xml->getAttribute('r');
+                            } elseif ($xml->nodeType == \XMLReader::END_ELEMENT) {
+                                if ($this->ignoreEmpty && $nonEmpty) {
+                                    $info['totalRows']++;
+                                    $nonEmpty = false;
+                                }
+
+                                if ($columnLetter > $info['lastColumnLetter']) {
+                                    $info['lastColumnLetter'] = $columnLetter;
+                                }
+                            }
+                        } elseif ($xml->name == 'c' && $xml->nodeType == \XMLReader::ELEMENT) {
+                            $columnLetter = preg_replace('{[^[:alpha:]]}S', '', $xml->getAttribute('r'));
+                        } elseif ($this->ignoreEmpty && !$nonEmpty && $xml->name == 'v'
+                            && $xml->nodeType == \XMLReader::ELEMENT && trim($xml->readString()) !== '') {
+
+                            $nonEmpty = true;
+                        }
+                    }
+
+                    if ($info['lastColumnLetter']) {
+                        $info['totalColumns'] = Format::columnIndexFromString($info['lastColumnLetter']);
+                        $info['lastColumnIndex'] = $info['totalColumns'] - 1;
+                    }
+
+                    $this->sheets[] = $info;
+                }
+
+                $xml->close();
+            }
+        }
+
+        return $this->sheets;
+    }
+
+    /**
+     * Get shared string
+     *
+     * @param int $position
+     * @return string
+     */
+    protected function getSharedString($position) {
+        $value = '';
+
+        $file = 'xl/sharedStrings.xml';
+        if ($this->sharedStringsXML === null) {
+            $this->sharedStringsXML = new \XMLReader();
+
+            $this->zip->extractTo($this->tmpDir, $file);
+        }
+
+        if ($this->sharedStringsPosition < 0 || $position < $this->sharedStringsPosition) {
+            $this->sharedStringsXML->open($this->tmpDir . '/' . $file, null, self::getLibXmlLoaderOptions());
+
+            $this->sharedStringsPosition = -1;
+        }
+
+        while ($this->sharedStringsXML->read()) {
+            $name = $this->sharedStringsXML->name;
+            $nodeType = $this->sharedStringsXML->nodeType;
+
+            if ($name == 'si') {
+                if ($nodeType == \XMLReader::ELEMENT) {
+                    $this->sharedStringsPosition++;
+                } elseif ($position == $this->sharedStringsPosition && $nodeType == \XMLReader::END_ELEMENT) {
+                    break;
+                }
+            } elseif ($name == 't' && $position == $this->sharedStringsPosition && $nodeType == \XMLReader::ELEMENT) {
+                $value .= trim($this->sharedStringsXML->readString());
+            }
+        }
+
+        return $value;
+    }
+
+    /**
+     * Parse styles info
+     *
+     * @throws ReaderException
+     */
+    protected function parseStyles() {
+        if ($this->styleXfs === null) {
+            $stylesXML = simplexml_load_string(
+                $this->securityScan($this->zip->getFromName('xl/styles.xml')), 'SimpleXMLElement', self::getLibXmlLoaderOptions()
+            );
+
+            $this->styleXfs = $this->formats = [];
+            if ($stylesXML) {
+                if (isset($stylesXML->cellXfs->xf) && $stylesXML->cellXfs->xf) {
+                    foreach ($stylesXML->cellXfs->xf as $xf) {
+                        $numFmtId = isset($xf['numFmtId']) ? (int)$xf['numFmtId'] : 0;
+                        if (isset($xf['applyNumberFormat']) || $numFmtId == 0) {
+                            // If format ID >= 164, it is a custom format and should be read from styleSheet\numFmts
+                            $this->styleXfs[] = $numFmtId;
+                        } else {
+                            // 0 for "General" format
+                            $this->styleXfs[] = Format::FORMAT_GENERAL;
+                        }
+                    }
+                }
+
+                if (isset($stylesXML->numFmts->numFmt) && $stylesXML->numFmts->numFmt) {
+                    foreach ($stylesXML->numFmts->numFmt as $numFmt) {
+                        if (isset($numFmt['numFmtId'], $numFmt['formatCode'])) {
+                            $this->formats[(int)$numFmt['numFmtId']] = (string)$numFmt['formatCode'];
+                        }
+                    }
+                }
+            }
+        }
+    }
+
+    /**
+     * Get worksheet XMLReader
+     */
+    protected function getWorksheetXML() {
+        if ($this->worksheetXML === null) {
+            $this->worksheetXML = new \XMLReader();
+        }
+
+        $this->worksheetXML->open(
+            $this->tmpDir . '/xl/worksheets/sheet' . ($this->getSheetIndex() + 1) . '.xml',
+            null, self::getLibXmlLoaderOptions()
+        );
+    }
+
+    /**
+     * Get row data
+     *
+     * @param int $rowIndex
+     * @param int $columnLimit
+     *
+     * @throws ReaderException
+     * @return array|bool
+     */
+    public function getRow($rowIndex, $columnLimit = 0) {
+        $this->parseStyles();
+        $rowIndex === 0 && $this->getWorksheetXML();
+
+        $sharedString = false;
+        $index = $styleId = 0;
+        $row = $columnLimit ? array_fill(0, $columnLimit, '') : [];
+
+        while ($canRead = $this->worksheetXML->read()) {
+            $name = $this->worksheetXML->name;
+            $type = $this->worksheetXML->nodeType;
+
+            // End of row
+            if ($name == 'row') {
+                if (!$this->ignoreEmpty && $type == \XMLReader::ELEMENT
+                    && $rowIndex+1 != (int)$this->worksheetXML->getAttribute('r')) {
+
+                    $this->worksheetXML->moveToElement();
+                    break;
+                }
+
+                if ($type == \XMLReader::END_ELEMENT) {
+                    break;
+                }
+            }
+
+            if ($columnLimit > 0 && $index >= $columnLimit) {
+                continue;
+            }
+
+            switch ($name) {
+                // Cell
+                case 'c':
+                    if ($type == \XMLReader::END_ELEMENT) {
+                        continue;
+                    }
+
+                    $styleId = (int)$this->worksheetXML->getAttribute('s');
+                    $letter = preg_replace('{[^[:alpha:]]}S', '', $this->worksheetXML->getAttribute('r'));
+                    $index = Format::columnIndexFromString($letter) - 1;
+
+                    // Determine cell type
+                    $sharedString = false;
+                    if ($this->worksheetXML->getAttribute('t') == self::CELL_TYPE_SHARED_STR) {
+                        $sharedString = true;
+                    }
+
+                    break;
+
+                // Cell value
+                case 'v':
+                case 'is':
+                    if ($type == \XMLReader::END_ELEMENT) {
+                        continue;
+                    }
+
+                    $value = $this->worksheetXML->readString();
+                    if ($sharedString) {
+                        $value = $this->getSharedString($value);
+                    }
+
+                    // Format value if necessary
+                    if ($value !== '' && $styleId && isset($this->styleXfs[$styleId])) {
+                        $value = $this->formatValue($value, $styleId);
+                    } elseif ($value && is_numeric($value)) {
+                        $value = (float)$value;
+                    }
+
+                    $row[$index] = $value;
+                    break;
+            }
+        }
+
+        if ($canRead === false) {
+            return false;
+        }
+
+        return $row;
+    }
+
+    /**
+     * Close ZipArchive、XMLReader and remove temp dir
+     */
+    public function __destruct() {
+        if ($this->zip && $this->tmpDir) {
+            $this->zip->close();
+        }
+
+        if ($this->worksheetXML) {
+            $this->worksheetXML->close();
+        }
+
+        if ($this->sharedStringsXML) {
+            $this->sharedStringsXML->close();
+        }
+
+        $this->removeDir($this->tmpDir);
+
+        $this->zip = null;
+        $this->worksheetXML = null;
+        $this->sharedStringsXML = null;
+        $this->tmpDir = null;
+    }
+
+    /**
+     * Remove dir
+     *
+     * @param string $dir
+     */
+    protected function removeDir($dir) {
+        if($dir && is_dir($dir)) {
+            $handle = opendir($dir);
+
+            while($item = readdir($handle)) {
+                if ($item != '.' && $item != '..') {
+                    is_file($item = $dir . '/' . $item) ? unlink($item) : $this->removeDir($item);
+                }
+            }
+
+            closedir($handle);
+            rmdir($dir);
+        }
+    }
+
+    /**
+     * Formats the value according to the index
+     *
+     * @param string $value
+     * @param int $index Format index
+     *
+     * @throws \Exception
+     * @return string Formatted cell value
+     */
+    private function formatValue($value, $index) {
+        if (!is_numeric($value)) {
+            return $value;
+        }
+
+        if (isset($this->styleXfs[$index]) && $this->styleXfs[$index] !== false) {
+            $index = $this->styleXfs[$index];
+        } else {
+            return $value;
+        }
+
+        // A special case for the "General" format
+        if ($index == 0) {
+            return is_numeric($value) ? (float)$value : $value;
+        }
+
+        $format = $this->parsedFormats[$index] ?? [];
+
+        if (empty($format)) {
+            $format = [
+                'code' => false, 'type' => false, 'scale' => 1, 'thousands' => false, 'currency' => false
+            ];
+
+            if (isset(Format::$buildInFormats[$index])) {
+                $format['code'] = Format::$buildInFormats[$index];
+            } elseif (isset($this->formats[$index])) {
+                $format['code'] = str_replace('"', '', $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'], Format::$dateFormatReplacements);
+
+                if (strpos($format['code'], 'A') === false) {
+                    $format['code'] = strtr($format['code'], Format::$dateFormatReplacements24);
+                } else {
+                    $format['code'] = strtr($format['code'], Format::$dateFormatReplacements12);
+                }
+            } 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(['"', '*'], '', $format['code']);
+
+                // Removing thousands separator
+                if (strpos($format['code'], '0,0') !== false || strpos($format['code'], '#,#') !== false) {
+                    $format['thousands'] = true;
+                }
+
+                $format['code'] = str_replace(['0,0', '#,#'], ['00', '##'], $format['code']);
+
+                // Scaling (Commas indicate the power)
+                $scale = 1;
+                $matches = [];
+
+                if (preg_match('/(0|#)(,+)/', $format['code'], $matches)) {
+                    $scale = pow(1000, strlen($matches[2]));
+
+                    // Removing the commas
+                    $format['code'] = preg_replace(['/0,+/', '/#,+/'], ['0', '#'], $format['code']);
+                }
+
+                $format['scale'] = $scale;
+                if (preg_match('/#?.*\?\/\?/', $format['code'])) {
+                    $format['type'] = 'Fraction';
+                } else {
+                    $format['code'] = str_replace('#', '', $format['code']);
+                    $matches = [];
+
+                    if (preg_match('/(0+)(\.?)(0*)/', preg_replace('/\[[^\]]+\]/', '', $format['code']), $matches)) {
+                        list(, $integer, $decimalPoint, $decimal) = $matches;
+
+                        $format['minWidth'] = strlen($integer) + strlen($decimalPoint) + strlen($decimal);
+                        $format['decimals'] = $decimal;
+                        $format['precision'] = strlen($format['decimals']);
+                        $format['pattern'] = '%0' . $format['minWidth'] . '.' . $format['precision'] . 'f';
+                    }
+                }
+
+                $matches = [];
+                if (preg_match('/\[\$(.*)\]/u', $format['code'], $matches)) {
+                    $currencyCode = explode('-', $matches[1]);
+                    if ($currencyCode) {
+                        $currencyCode = $currencyCode[0];
+                    }
+
+                    if (!$currencyCode) {
+                        $currencyCode = self::$currencyCode;
+                    }
+
+                    $format['currency'] = $currencyCode;
+                }
+
+                $format['code'] = trim($format['code']);
+            }
+
+            $this->parsedFormats[$index] = $format;
+        }
+
+        // Applying format to value
+        if ($format) {
+            if ($format['code'] == '@') {
+                return (string)$value;
+            } elseif ($format['type'] == 'Percentage') { // Percentages
+                if ($format['code'] === '0%') {
+                    $value = round(100*$value, 0) . '%';
+                } else {
+                    $value = sprintf('%.2f%%', round(100*$value, 2));
+                }
+            } elseif ($format['type'] == 'DateTime') { // Dates and times
+                $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);
+
+                // Here time is converted to seconds
+                // Some loss of precision will occur
+                $seconds = $time ? (int)($time*86400) : 0;
+
+                $value = clone self::$baseDate;
+                $value->add(new \DateInterval('P' . $days . 'D' . ($seconds ? 'T' . $seconds . 'S' : '')));
+
+                $value = $value->format($format['code']);
+            } 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 = $decimal/$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;
+    }
+
+    /**
+     * Greatest common divisor calculation in case GMP extension is not enabled
+     *
+     * @param int $number1
+     * @param int $number2
+     *
+     * @return int
+     */
+    private static function GCD($number1, $number2) {
+        $number1 = abs($number1);
+        $number2 = abs($number2);
+
+        if ($number1 + $number2 == 0) {
+            return 0;
+        }
+
+        $number = 1;
+        while ($number1 > 0) {
+            $number = $number1;
+            $number1 = $number2 % $number1;
+            $number2 = $number;
+        }
+
+        return $number;
+    }
+
+    /**
+     * Open file for reading
+     *
+     * @param string $file
+     *
+     * @throws ParserException|ReaderException
+     */
+    public function openFile($file) {
+        // Check if file exists
+        if (!file_exists($file) || !is_readable($file)) {
+            throw new ReaderException("Could not open file [$file] for reading! File does not exist.");
+        }
+
+        $this->zip = new \ZipArchive();
+
+        $xl = false;
+        if ($this->zip->open($file) === true) {
+            $this->tmpDir = sys_get_temp_dir() . '/' . uniqid();
+
+            // check if it is an OOXML archive
+            $rels = simplexml_load_string(
+                $this->securityScan($this->zip->getFromName('_rels/.rels')),
+                'SimpleXMLElement', self::getLibXmlLoaderOptions()
+            );
+
+            if ($rels !== false) {
+                foreach ($rels->Relationship as $rel) {
+                    switch ($rel["Type"]) {
+                        case "http://schemas.openxmlformats.org/officeDocument/2006/relationships/officeDocument":
+                            if ($rel["Target"] == 'xl/workbook.xml') {
+                                $xl = true;
+                            }
+
+                            break;
+                    }
+                }
+            }
+        }
+
+        if ($xl === false) {
+            throw new ParserException("The file [$file] is not recognised as a zip archive");
+        }
+    }
+
+    /**
+     * Scan theXML for use of <!ENTITY to prevent XXE/XEE attacks
+     *
+     * @param  string $xml
+     *
+     * @throws ReaderException
+     * @return string
+     */
+    protected function securityScan($xml) {
+        $pattern = sprintf('/\\0?%s\\0?/', implode('\\0?', str_split('<!DOCTYPE')));
+
+        if (preg_match($pattern, $xml)) {
+            throw new ReaderException(
+                'Detected use of ENTITY in XML, spreadsheet file load() aborted to prevent XXE/XEE attacks'
+            );
+        }
+
+        return $xml;
+    }
+
+    /**
+     * Set default options for libxml loader
+     *
+     * @param int $options Default options for libxml loader
+     */
+    public static function setLibXmlLoaderOptions($options = null) {
+        if (is_null($options) && defined(LIBXML_DTDLOAD)) {
+            $options = LIBXML_DTDLOAD | LIBXML_DTDATTR;
+        }
+
+        if (version_compare(PHP_VERSION, '5.2.11') >= 0) {
+            @libxml_disable_entity_loader($options == (LIBXML_DTDLOAD | LIBXML_DTDATTR));
+        }
+
+        self::$libXmlLoaderOptions = $options;
+    }
+
+    /**
+     * Get default options for libxml loader.
+     * Defaults to LIBXML_DTDLOAD | LIBXML_DTDATTR when not set explicitly.
+     *
+     * @return int Default options for libxml loader
+     */
+    public static function getLibXmlLoaderOptions() {
+        if (is_null(self::$libXmlLoaderOptions) && defined(LIBXML_DTDLOAD)) {
+            self::setLibXmlLoaderOptions(LIBXML_DTDLOAD | LIBXML_DTDATTR);
+        }
+
+        if (version_compare(PHP_VERSION, '5.2.11') >= 0) {
+            @libxml_disable_entity_loader(self::$libXmlLoaderOptions == (LIBXML_DTDLOAD | LIBXML_DTDATTR));
+        }
+
+        return self::$libXmlLoaderOptions;
+    }
+}

+ 2410 - 0
src/Parser/Excel5.php

@@ -0,0 +1,2410 @@
+<?php
+/**
+ * Excel5 Parser
+ *
+ * @author Janson
+ * @create 2017-11-27
+ */
+namespace Asan\PHPExcel\Parser;
+
+use Asan\PHPExcel\Exception\ParserException;
+use Asan\PHPExcel\Parser\Excel5\OLERead;
+use Asan\PHPExcel\Parser\Excel5\RC4;
+
+class Excel5 {
+    // 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_EOF = 0x000a;
+    const XLS_TYPE_DATEMODE = 0x0022;
+    const XLS_TYPE_FILEPASS = 0x002f;
+    const XLS_TYPE_CONTINUE = 0x003c;
+    const XLS_TYPE_CODEPAGE = 0x0042;
+    const XLS_TYPE_OBJ = 0x005d;
+    const XLS_TYPE_SHEET = 0x0085;
+    const XLS_TYPE_MULRK = 0x00bd;
+    const XLS_TYPE_MULBLANK = 0x00be;
+    const XLS_TYPE_XF = 0x00e0;
+    const XLS_TYPE_SST = 0x00fc;
+    const XLS_TYPE_LABELSST = 0x00fd;
+    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_RK = 0x027e;
+    const XLS_TYPE_FORMAT = 0x041e;
+    const XLS_TYPE_BOF = 0x0809;
+
+    // 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;
+
+    // Sheet state
+    const SHEETSTATE_VISIBLE = 'visible';
+    const SHEETSTATE_HIDDEN = 'hidden';
+    const SHEETSTATE_VERYHIDDEN = 'veryHidden';
+
+    private static $errorCode = [
+        0x00 => '#NULL!',
+        0x07 => '#DIV/0!',
+        0x0F => '#VALUE!',
+        0x17 => '#REF!',
+        0x1D => '#NAME?',
+        0x24 => '#NUM!',
+        0x2A => '#N/A'
+    ];
+
+    /**
+     * Base calendar year to use for calculations
+     *
+     * @var int
+     */
+    private static $excelBaseDate = Format::CALENDAR_WINDOWS_1900;
+
+    /**
+     * Decimal separator
+     *
+     * @var string
+     */
+    private static $decimalSeparator;
+
+    /**
+     * Thousands separator
+     *
+     * @var string
+     */
+    private static $thousandsSeparator;
+
+    /**
+     * Currency code
+     *
+     * @var string
+     */
+    private static $currencyCode;
+
+    /**
+     * Workbook stream data
+     *
+     * @var string
+     */
+    private $data;
+
+    /**
+     * Size in bytes of $this->data
+     *
+     * @var int
+     */
+    private $dataSize;
+
+    /**
+     * Current position in stream
+     *
+     * @var integer
+     */
+    private $pos;
+
+    /**
+     * Worksheets
+     *
+     * @var array
+     */
+    private $sheets;
+
+    /**
+     * BIFF version
+     *
+     * @var int
+     */
+    private $version;
+
+    /**
+     * Codepage set in the Excel file being read. Only important for BIFF5 (Excel 5.0 - Excel 95)
+     * For BIFF8 (Excel 97 - Excel 2003) this will always have the value 'UTF-16LE'
+     *
+     * @var string
+     */
+    private $codePage;
+
+    /**
+     * Row data
+     *
+     * @var array
+     */
+    private $row;
+
+    /**
+     * Shared formats
+     *
+     * @var array
+     */
+    private $formats;
+
+    /**
+     * The current sheet of the file
+     *
+     * @var int
+     */
+    private $sheetIndex = 0;
+
+    /**
+     * Ignore empty row
+     *
+     * @var bool
+     */
+    private $ignoreEmpty = false;
+
+    /**
+     * The current row index of the sheet
+     *
+     * @var int
+     */
+    private $rowIndex = 0;
+
+    /**
+     * Max column number
+     *
+     * @var int
+     */
+    private $columnLimit = 0;
+
+    /**
+     * Whether to the end of the row
+     *
+     * @var bool
+     */
+    private $eor = false;
+
+    /**
+     * Extended format record
+     *
+     * @var array
+     */
+    private $xfRecords = [];
+
+    /**
+     * Shared strings. Only applies to BIFF8.
+     *
+     * @var array
+     */
+    private $sst = [];
+
+    /**
+     * The type of encryption in use
+     *
+     * @var int
+     */
+    private $encryption = 0;
+
+    /**
+     * The position in the stream after which contents are encrypted
+     *
+     * @var int
+     */
+    private $encryptionStartPos = false;
+
+    /**
+     * The current RC4 decryption object
+     *
+     * @var RC4
+     */
+    private $rc4Key = null;
+
+    /**
+     * The position in the stream that the RC4 decryption object was left at
+     *
+     * @var int
+     */
+    private $rc4Pos = 0;
+
+    /**
+     * The current MD5 context state
+     *
+     * @var string
+     */
+    private $md5Ctxt = null;
+
+    /**
+     * Use OLE reader to extract the relevant data streams from the OLE file
+     *
+     * @param string $file
+     */
+    public function loadOLE($file) {
+        $oleRead = new OLERead();
+        $oleRead->read($file);
+        $this->data = $oleRead->getStream($oleRead->workbook);
+    }
+
+    /**
+     * Ignore empty row
+     *
+     * @param bool $ignoreEmpty
+     *
+     * @return $this
+     */
+    public function ignoreEmptyRow($ignoreEmpty) {
+        $this->ignoreEmpty = $ignoreEmpty;
+
+        return $this;
+    }
+
+    /**
+     * Whether is ignore empty row
+     *
+     * @return bool
+     */
+    public function isIgnoreEmptyRow() {
+        return $this->ignoreEmpty;
+    }
+
+    /**
+     * Set sheet index
+     *
+     * @param int $index
+     *
+     * @return $this
+     */
+    public function setSheetIndex($index) {
+        $this->sheetIndex = $index;
+
+        return $this;
+    }
+
+    /**
+     * Get sheet index
+     *
+     * @return int
+     */
+    public function getSheetIndex() {
+        return $this->sheetIndex;
+    }
+
+    /**
+     * Return worksheet info (Name, Last Column Letter, Last Column Index, Total Rows, Total Columns)
+     *
+     * @throws ParserException
+     * @return array
+     */
+    public function parseWorksheetInfo() {
+        if ($this->sheets === null) {
+            // total byte size of Excel data (workbook global substream + sheet substreams)
+            $this->dataSize = strlen($this->data);
+            $this->pos = 0;
+            $this->codePage = 'CP1252';
+            $this->sheets = [];
+
+            // Parse Workbook Global Substream
+            while ($this->pos < $this->dataSize) {
+                $code = Format::getUInt2d($this->data, $this->pos);
+
+                switch ($code) {
+                    case self::XLS_TYPE_BOF:
+                        $this->readBof();
+                        break;
+
+                    case self::XLS_TYPE_FILEPASS:
+                        $this->readFilepass();
+                        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_SST:
+                        $this->readSst();
+                        break;
+
+                    case self::XLS_TYPE_SHEET:
+                        $this->readSheet();
+                        break;
+
+                    case self::XLS_TYPE_EOF:
+                        $this->readDefault();
+                        break 2;
+
+                    default:
+                        $this->readDefault();
+                        break;
+                }
+            }
+
+            // Parse the individual sheets
+            foreach ($this->sheets as $key => $sheet) {
+                if ($sheet['sheetType'] != 0x00) {
+                    // 0x00: Worksheet
+                    // 0x02: Chart
+                    // 0x06: Visual Basic module
+                    continue;
+                }
+
+                $sheet['lastColumnLetter'] = '';
+                $sheet['lastColumnIndex'] = null;
+                $sheet['totalRows'] = 0;
+                $sheet['totalColumns'] = 0;
+
+                $lastRowIndex = 0;
+                $this->pos = $sheet['offset'];
+                while ($this->pos <= $this->dataSize - 4) {
+                    $code = Format::getUInt2d($this->data, $this->pos);
+
+                    switch ($code) {
+                        case self::XLS_TYPE_RK:
+                        case self::XLS_TYPE_LABELSST:
+                        case self::XLS_TYPE_NUMBER:
+                        case self::XLS_TYPE_FORMULA:
+                        case self::XLS_TYPE_BOOLERR:
+                        case self::XLS_TYPE_LABEL:
+                            $length = Format::getUInt2d($this->data, $this->pos + 2);
+                            $recordData = substr($this->data, $this->pos + 4, $length);
+
+                            // move stream pointer to next record
+                            $this->pos += 4 + $length;
+
+                            $rowIndex = Format::getUInt2d($recordData, 0) + 1;
+                            $columnIndex = Format::getUInt2d($recordData, 2);
+
+                            if ($this->ignoreEmpty) {
+                                if ($lastRowIndex < $rowIndex) {
+                                    $sheet['totalRows']++;
+                                }
+
+                                $lastRowIndex = $rowIndex;
+                            } else {
+                                $sheet['totalRows'] = max($sheet['totalRows'], $rowIndex);
+                            }
+
+                            $sheet['lastColumnIndex'] = max($columnIndex, $sheet['lastColumnIndex']);
+                            break;
+
+                        case self::XLS_TYPE_BOF:
+                            $this->readBof();
+                            break;
+
+                        case self::XLS_TYPE_EOF:
+                            $this->readDefault();
+                            break 2;
+
+                        default:
+                            $this->readDefault();
+                            break;
+                    }
+                }
+
+                if ($sheet['lastColumnIndex'] !== null) {
+                    $sheet['lastColumnLetter'] = Format::stringFromColumnIndex($sheet['lastColumnIndex']);
+                } else {
+                    $sheet['lastColumnIndex'] = 0;
+                }
+
+                if ($sheet['lastColumnLetter']) {
+                    $sheet['totalColumns'] = $sheet['lastColumnIndex'] + 1;
+                }
+
+                $this->sheets[$key] = $sheet;
+            }
+
+            $this->pos = 0;
+        }
+
+        return $this->sheets;
+    }
+
+    /**
+     * Get row data
+     *
+     * @param int $rowIndex
+     * @param int $columnLimit
+     *
+     * @throws ParserException
+     * @return array|bool
+     */
+    public function getRow($rowIndex, $columnLimit = 0) {
+        $this->parseWorksheetInfo();
+
+        // Rewind or change sheet
+        if ($rowIndex === 0 || $this->pos < $this->sheets[$this->sheetIndex]['offset']) {
+            $this->pos = $this->sheets[$this->sheetIndex]['offset'];
+        }
+
+        $endPos = $this->dataSize - 4;
+        if (isset($this->sheets[$this->sheetIndex + 1]['offset'])) {
+            $endPos = $this->sheets[$this->sheetIndex + 1]['offset'] - 4;
+        }
+
+        if ($this->pos >= $endPos) {
+            return false;
+        }
+
+        $this->rowIndex = $rowIndex;
+        $this->columnLimit = $columnLimit;
+        $this->eor = false;
+        $this->row = $columnLimit ? array_fill(0, $columnLimit, '') : [];
+
+        while ($this->pos <= $endPos) {
+            // Remember last position
+            $lastPos = $this->pos;
+            $code = Format::getUInt2d($this->data, $this->pos);
+
+            switch ($code) {
+                case self::XLS_TYPE_BOF:
+                    $this->readBof();
+                    break;
+
+                case self::XLS_TYPE_RK:
+                    $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:
+                    $this->readFormula();
+                    break;
+
+                case self::XLS_TYPE_BOOLERR:
+                    $this->readBoolErr();
+                    break;
+
+                case self::XLS_TYPE_MULBLANK:
+                case self::XLS_TYPE_BLANK:
+                    $this->readBlank();
+                    break;
+
+                case self::XLS_TYPE_LABEL:
+                    $this->readLabel();
+                    break;
+
+                case self::XLS_TYPE_EOF:
+                    $this->readDefault();
+                    break 2;
+
+                default:
+                    $this->readDefault();
+                    break;
+            }
+
+            //End of row
+            if ($this->eor) {
+                //Recover current position
+                $this->pos = $lastPos;
+                break;
+            }
+        }
+
+        return $this->row;
+    }
+
+    /**
+     * Add cell data
+     *
+     * @param int $row
+     * @param int $column
+     * @param mixed $value
+     * @param int $xfIndex
+     * @return bool
+     */
+    private function addCell($row, $column, $value, $xfIndex) {
+        if ($this->rowIndex != $row) {
+            $this->eor = true;
+
+            return false;
+        }
+
+        if (!$this->columnLimit || $column < $this->columnLimit) {
+            $xfRecord = $this->xfRecords[$xfIndex];
+            $this->row[$column] = self::toFormattedString($value, $xfRecord['format']);
+        }
+
+        return true;
+    }
+
+    /**
+     * Read BOF
+     *
+     * @throws ParserException
+     */
+    private function readBof() {
+        $length = Format::getUInt2d($this->data, $this->pos + 2);
+        $recordData = substr($this->data, $this->pos + 4, $length);
+
+        // move stream pointer to next record
+        $this->pos += 4 + $length;
+
+        // offset: 2; size: 2; type of the following data
+        $substreamType = Format::getUInt2d($recordData, 2);
+
+        switch ($substreamType) {
+            case self::XLS_WORKBOOKGLOBALS:
+                $version = Format::getUInt2d($recordData, 0);
+                if (($version != self::XLS_BIFF8) && ($version != self::XLS_BIFF7)) {
+                    throw new ParserException('Cannot read this Excel file. Version is too old.', 1);
+                }
+
+                $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 = Format::getUInt2d($this->data, $this->pos);
+                    $this->readDefault();
+                } while ($code != self::XLS_TYPE_EOF && $this->pos < $this->dataSize);
+
+                break;
+        }
+    }
+
+    /**
+     * SHEET
+     *
+     * This record is located in the Workbook Globals Substream and represents a sheet inside the workbook.
+     * One SHEET record is written for each sheet. It stores the sheet name and a stream offset to the BOF
+     * record of the respective Sheet Substream within the Workbook Stream.
+     */
+    private function readSheet() {
+        $length = Format::getUInt2d($this->data, $this->pos + 2);
+        $recordData = substr($this->data, $this->pos + 4, $length);
+
+        // offset: 0; size: 4; absolute stream position of the BOF record of the sheet
+        // NOTE: not encrypted
+        $offset = Format::getInt4d($this->data, $this->pos + 4);
+
+        // move stream pointer to next record
+        $this->pos += 4 + $length;
+
+        // offset: 4; size: 1; sheet state
+        switch (ord($recordData{4})) {
+            case 0x00:
+                $sheetState = self::SHEETSTATE_VISIBLE;
+                break;
+
+            case 0x01:
+                $sheetState = self::SHEETSTATE_HIDDEN;
+                break;
+
+            case 0x02:
+                $sheetState = self::SHEETSTATE_VERYHIDDEN;
+                break;
+
+            default:
+                $sheetState = self::SHEETSTATE_VISIBLE;
+                break;
+        }
+
+        // offset: 5; size: 1; sheet type
+        $sheetType = ord($recordData{5});
+
+        // offset: 6; size: var; sheet name
+        $name = '';
+        if ($this->version == self::XLS_BIFF8) {
+            $string = self::readUnicodeStringShort(substr($recordData, 6));
+            $name = $string['value'];
+        } elseif ($this->version == self::XLS_BIFF7) {
+            $string = $this->readByteStringShort(substr($recordData, 6));
+            $name = $string['value'];
+        }
+
+        // ignore hidden sheet
+        if ($sheetState == self::SHEETSTATE_VISIBLE) {
+            $this->sheets[] = [
+                'name' => $name, 'offset' => $offset, 'sheetState' => $sheetState, 'sheetType' => $sheetType
+            ];
+        }
+    }
+
+    /**
+     * Reads a general type of BIFF record.
+     * Does nothing except for moving stream pointer forward to next record.
+     */
+    private function readDefault() {
+        $length = Format::getUInt2d($this->data, $this->pos + 2);
+        //$recordData = $this->readRecordData($this->data, $this->pos + 4, $length);
+
+        // move stream pointer to next record
+        $this->pos += 4 + $length;
+    }
+
+    /**
+     * FILEPASS
+     *
+     * This record is part of the File Protection Block. It contains information about the read/write password of
+     * the file. All record contents following this record will be encrypted.
+     * The decryption functions and objects used from here on in are based on the source of Spreadsheet-ParseExcel:
+     * http://search.cpan.org/~jmcnamara/Spreadsheet-ParseExcel/
+     *
+     * @throws ParserException
+     */
+    private function readFilepass() {
+        $length = Format::getUInt2d($this->data, $this->pos + 2);
+
+        if ($length != 54) {
+            throw new ParserException('Unexpected file pass record length', 2);
+        }
+
+        $recordData = $this->readRecordData($this->data, $this->pos + 4, $length);
+
+        // move stream pointer to next record
+        $this->pos += 4 + $length;
+
+        if (!$this->verifyPassword('VelvetSweatshop', substr($recordData, 6,  16), substr($recordData, 22, 16),
+            substr($recordData, 38, 16), $this->md5Ctxt)) {
+
+            throw new ParserException('Decryption password incorrect', 3);
+        }
+
+        $this->encryption = self::MS_BIFF_CRYPTO_RC4;
+
+        // Decryption required from the record after next onwards
+        $this->encryptionStartPos = $this->pos + Format::getUInt2d($this->data, $this->pos + 2);
+    }
+
+    /**
+     * Read record data from stream, decrypting as required
+     *
+     * @param string $data Data stream to read from
+     * @param int $pos Position to start reading from
+     * @param int $len Record data length
+     *
+     * @throws ParserException
+     * @return string Record data
+     */
+    private function readRecordData($data, $pos, $len) {
+        $data = substr($data, $pos, $len);
+
+        // File not encrypted, or record before encryption start point
+        if ($this->encryption == self::MS_BIFF_CRYPTO_NONE || $pos < $this->encryptionStartPos) {
+            return $data;
+        }
+
+        $recordData = '';
+        if ($this->encryption == self::MS_BIFF_CRYPTO_RC4) {
+            $oldBlock = floor($this->rc4Pos / self::REKEY_BLOCK);
+            $block = floor($pos / self::REKEY_BLOCK);
+            $endBlock = floor(($pos + $len) / self::REKEY_BLOCK);
+
+            // Spin an RC4 decryptor to the right spot. If we have a decryptor sitting
+            // at a point earlier in the current block, re-use it as we can save some time.
+            if ($block != $oldBlock || $pos < $this->rc4Pos || !$this->rc4Key) {
+                $this->rc4Key = $this->makeKey($block, $this->md5Ctxt);
+                $step = $pos % self::REKEY_BLOCK;
+            } else {
+                $step = $pos - $this->rc4Pos;
+            }
+
+            $this->rc4Key->RC4(str_repeat("\0", $step));
+
+            // Decrypt record data (re-keying at the end of every block)
+            while ($block != $endBlock) {
+                $step = self::REKEY_BLOCK - ($pos % self::REKEY_BLOCK);
+                $recordData .= $this->rc4Key->RC4(substr($data, 0, $step));
+
+                $data = substr($data, $step);
+                $pos += $step;
+                $len -= $step;
+                $block++;
+
+                $this->rc4Key = $this->makeKey($block, $this->md5Ctxt);
+            }
+
+            $recordData .= $this->rc4Key->RC4(substr($data, 0, $len));
+
+            // Keep track of the position of this decryptor.
+            // We'll try and re-use it later if we can to speed things up
+            $this->rc4Pos = $pos + $len;
+
+        } elseif ($this->encryption == self::MS_BIFF_CRYPTO_XOR) {
+            throw new ParserException('XOr encryption not supported', 4);
+        }
+
+        return $recordData;
+    }
+
+    /**
+     * Make an RC4 decryptor for the given block
+     *
+     * @param int $block Block for which to create decrypto
+     * @param string $valContext MD5 context state
+     *
+     * @return RC4
+     */
+    private function makeKey($block, $valContext) {
+        $pw = str_repeat("\0", 64);
+
+        for ($i = 0; $i < 5; $i++) {
+            $pw[$i] = $valContext[$i];
+        }
+
+        $pw[5] = chr($block & 0xff);
+        $pw[6] = chr(($block >> 8) & 0xff);
+        $pw[7] = chr(($block >> 16) & 0xff);
+        $pw[8] = chr(($block >> 24) & 0xff);
+
+        $pw[9] = "\x80";
+        $pw[56] = "\x48";
+
+        return new RC4(md5($pw));
+    }
+
+    /**
+     * Verify RC4 file password
+     *
+     * @var string $password        Password to check
+     * @var string $docid           Document id
+     * @var string $salt_data       Salt data
+     * @var string $hashedsalt_data Hashed salt data
+     * @var string &$valContext     Set to the MD5 context of the value
+     *
+     * @return bool Success
+     */
+    private function verifyPassword($password, $docid, $salt_data, $hashedsalt_data, &$valContext) {
+        $pw = str_repeat("\0", 64);
+
+        for ($i = 0; $i < strlen($password); $i++) {
+            $o = ord(substr($password, $i, 1));
+            $pw[2 * $i] = chr($o & 0xff);
+            $pw[2 * $i + 1] = chr(($o >> 8) & 0xff);
+        }
+
+        $pw[2 * $i] = chr(0x80);
+        $pw[56] = chr(($i << 4) & 0xff);
+
+        $mdContext1 = md5($pw);
+
+        $offset = 0;
+        $keyOffset = 0;
+        $toCopy = 5;
+
+        while ($offset != 16) {
+            if ((64 - $offset) < 5) {
+                $toCopy = 64 - $offset;
+            }
+
+            for ($i = 0; $i <= $toCopy; $i++) {
+                $pw[$offset + $i] = $mdContext1[$keyOffset + $i];
+            }
+
+            $offset += $toCopy;
+
+            if ($offset == 64) {
+                $keyOffset = $toCopy;
+                $toCopy = 5 - $toCopy;
+                $offset = 0;
+                continue;
+            }
+
+            $keyOffset = 0;
+            $toCopy = 5;
+            for ($i = 0; $i < 16; $i++) {
+                $pw[$offset + $i] = $docid[$i];
+            }
+            $offset += 16;
+        }
+
+        $pw[16] = "\x80";
+        for ($i = 0; $i < 47; $i++) {
+            $pw[17 + $i] = "\0";
+        }
+        $pw[56] = "\x80";
+        $pw[57] = "\x0a";
+
+        $valContext = md5($pw);
+
+        $key = $this->makeKey(0, $valContext);
+
+        $salt = $key->RC4($salt_data);
+        $hashedsalt = $key->RC4($hashedsalt_data);
+
+        $salt .= "\x80" . str_repeat("\0", 47);
+        $salt[56] = "\x80";
+
+        $mdContext2 = md5($salt);
+
+        return $mdContext2 == $hashedsalt;
+    }
+
+    /**
+     * CODEPAGE
+     *
+     * This record stores the text encoding used to write byte strings, stored as MS Windows code page identifier.
+     *
+     * @throws ParserException
+     */
+    private function readCodepage() {
+        $length = Format::getUInt2d($this->data, $this->pos + 2);
+        $recordData = substr($this->data, $this->pos + 4, $length);
+
+        // move stream pointer to next record
+        $this->pos += 4 + $length;
+
+        // offset: 0; size: 2; code page identifier
+        $codePage = Format::getUInt2d($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 = Format::getUInt2d($this->data, $this->pos + 2);
+        $recordData = substr($this->data, $this->pos + 4, $length);
+
+        // move stream pointer to next record
+        $this->pos += 4 + $length;
+
+        // offset: 0; size: 2; 0 = base 1900, 1 = base 1904
+        self::$excelBaseDate = Format::CALENDAR_WINDOWS_1900;
+        if (ord($recordData{0}) == 1) {
+            self::$excelBaseDate = Format::CALENDAR_MAC_1904;
+        }
+    }
+
+    /**
+     * FORMAT
+     *
+     * This record contains information about a number format. All FORMAT records occur together in a sequential list.
+     * In BIFF2-BIFF4 other records referencing a FORMAT record contain a zero-based index into this list. From BIFF5
+     * on the FORMAT record contains the index itself that will be used by other records.
+     */
+    private function readFormat() {
+        $length = Format::getUInt2d($this->data, $this->pos + 2);
+        $recordData = substr($this->data, $this->pos + 4, $length);
+
+        // move stream pointer to next record
+        $this->pos += 4 + $length;
+
+        $indexCode = Format::getUInt2d($recordData, 0);
+        if ($this->version == self::XLS_BIFF8) {
+            $string = self::readUnicodeStringLong(substr($recordData, 2));
+        } else {
+            // BIFF7
+            $string = $this->readByteStringShort(substr($recordData, 2));
+        }
+
+        $formatString = $string['value'];
+        $this->formats[$indexCode] = $formatString;
+    }
+
+    /**
+     * 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
+     * We read all cell XF records.
+     */
+    private function readXf() {
+        $length = Format::getUInt2d($this->data, $this->pos + 2);
+        $recordData = substr($this->data, $this->pos + 4, $length);
+
+        // move stream pointer to next record
+        $this->pos += 4 + $length;
+
+        // offset: 2; size: 2; Index to FORMAT record
+        $numberFormatIndex = Format::getUInt2d($recordData, 2);
+        if (isset($this->formats[$numberFormatIndex])) {
+            // then we have user-defined format code
+            $numberFormat = $this->formats[$numberFormatIndex];
+        } elseif (isset(Format::$buildInFormats[$numberFormatIndex])) {
+            // then we have built-in format code
+            $numberFormat = Format::$buildInFormats[$numberFormatIndex];
+        } else {
+            // we set the general format code
+            $numberFormat = Format::FORMAT_GENERAL;
+        }
+
+        $this->xfRecords[] = ['index' => $numberFormatIndex, 'format' => $numberFormat];
+    }
+
+    /**
+     * SST - Shared String Table
+     *
+     * This record contains a list of all strings used anywhere in the workbook. Each string occurs only once.
+     * The workbook uses indexes into the list to reference the strings.
+     **/
+    private function readSst() {
+        // offset within (spliced) record data
+        $pos = 0;
+
+        // get spliced record data
+        $splicedRecordData = $this->getSplicedRecordData();
+        $recordData = $splicedRecordData['recordData'];
+        $spliceOffsets = $splicedRecordData['spliceOffsets'];
+
+        // offset: 0; size: 4; total number of strings in the workbook
+        $pos += 4;
+
+        // offset: 4; size: 4; number of following strings ($nm)
+        $nm = Format::getInt4d($recordData, 4);
+
+        $pos += 4;
+
+        // loop through the Unicode strings (16-bit length)
+        for ($i = 0; $i < $nm; ++$i) {
+            if (!isset($recordData[$pos + 2])) {
+                break;
+            }
+
+            // number of characters in the Unicode string
+            $numChars = Format::getUInt2d($recordData, $pos);
+            $pos += 2;
+
+            // option flags
+            $optionFlags = ord($recordData[$pos]);
+            ++$pos;
+
+            // bit: 0; mask: 0x01; 0 = compressed; 1 = uncompressed
+            $isCompressed = (($optionFlags & 0x01) == 0) ;
+
+            // bit: 2; mask: 0x02; 0 = ordinary; 1 = Asian phonetic
+            $hasAsian = (($optionFlags & 0x04) != 0);
+
+            // bit: 3; mask: 0x03; 0 = ordinary; 1 = Rich-Text
+            $formattingRuns = 0;
+            $hasRichText = (($optionFlags & 0x08) != 0);
+            if ($hasRichText && isset($recordData[$pos])) {
+                // number of Rich-Text formatting runs
+                $formattingRuns = Format::getUInt2d($recordData, $pos);
+                $pos += 2;
+            }
+
+            $extendedRunLength = 0;
+            if ($hasAsian && isset($recordData[$pos])) {
+                // size of Asian phonetic setting
+                $extendedRunLength = Format::getInt4d($recordData, $pos);
+                $pos += 4;
+            }
+
+            // expected byte length of character array if not split
+            $len = ($isCompressed) ? $numChars : $numChars * 2;
+
+            // look up limit position
+            $limitPos = 0;
+            foreach ($spliceOffsets as $spliceOffset) {
+                // it can happen that the string is empty, therefore we need
+                // <= and not just <
+                if ($pos <= $spliceOffset) {
+                    $limitPos = $spliceOffset;
+                    break;
+                }
+            }
+
+            if ($pos + $len <= $limitPos) {
+                // character array is not split between records
+                $retStr = substr($recordData, $pos, $len);
+                $pos += $len;
+            } else {
+                // character array is split between records
+                // first part of character array
+                $retStr = substr($recordData, $pos, $limitPos - $pos);
+                $bytesRead = $limitPos - $pos;
+
+                // remaining characters in Unicode string
+                $charsLeft = $numChars - (($isCompressed) ? $bytesRead : ($bytesRead / 2));
+                $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;
+                        }
+                    }
+
+                    if (!isset($recordData[$pos])) {
+                        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) {
+                            if (!isset($recordData[$pos + $j])) {
+                                break;
+                            }
+
+                            $retStr .= $recordData[$pos + $j] . chr(0);
+                        }
+
+                        $charsLeft -= $len;
+                        $isCompressed = false;
+                    } else {
+                        // 1st fragment compressed
+                        // this fragment uncompressed
+                        $newStr = '';
+                        $jMax = strlen($retStr);
+                        for ($j = 0; $j < $jMax; ++$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;
+                }
+            }
+
+            // convert to UTF-8
+            $retStr = self::encodeUTF16($retStr, $isCompressed);
+
+            // read additional Rich-Text information, if any
+            // $fmtRuns = [];
+            if ($hasRichText) {
+                // list of formatting runs
+                /*for ($j = 0; $j < $formattingRuns; ++$j) {
+                    // first formatted character; zero-based
+                    $charPos = Format::getUInt2d($recordData, $pos + $j * 4);
+
+                    // index to font record
+                    $fontIndex = Format::getUInt2d($recordData, $pos + 2 + $j * 4);
+                    $fmtRuns[] = ['charPos' => $charPos, 'fontIndex' => $fontIndex];
+                }*/
+
+                $pos += 4 * $formattingRuns;
+            }
+
+            // read additional Asian phonetics information, if any
+            if ($hasAsian) {
+                // For Asian phonetic settings, we skip the extended string data
+                $pos += $extendedRunLength;
+            }
+
+            // store the shared sting
+            $this->sst[] = ['value' => $retStr];
+        }
+    }
+
+    /**
+     * 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 = Format::getUInt2d($this->data, $this->pos + 2);
+        $recordData = substr($this->data, $this->pos + 4, $length);
+
+        // move stream pointer to next record
+        $this->pos += 4 + $length;
+
+        // offset: 0; size: 2; index to row
+        $row = Format::getUInt2d($recordData, 0);
+
+        // offset: 2; size: 2; index to column
+        $column = Format::getUInt2d($recordData, 2);
+
+        // offset: 4; size: 2; index to XF record
+        $xfIndex = Format::getUInt2d($recordData, 4);
+
+        // offset: 6; size: 4; RK value
+        $rkNum = Format::getInt4d($recordData, 6);
+        $numValue = self::getIEEE754($rkNum);
+
+        // add cell
+        $this->addCell($row, $column, $numValue, $xfIndex);
+    }
+
+    /**
+     * 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 = Format::getUInt2d($this->data, $this->pos + 2);
+        $recordData = substr($this->data, $this->pos + 4, $length);
+
+        $this->pos += 4 + $length;
+        $xfIndex = Format::getUInt2d($recordData, 4);
+        $row = Format::getUInt2d($recordData, 0);
+        $column = Format::getUInt2d($recordData, 2);
+
+        // offset: 6; size: 4; index to SST record
+        $index = Format::getInt4d($recordData, 6);
+        $this->addCell($row, $column, $this->sst[$index]['value'], $xfIndex);
+    }
+
+    /**
+     * 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 = Format::getUInt2d($this->data, $this->pos + 2);
+        $recordData = substr($this->data, $this->pos + 4, $length);
+
+        // move stream pointer to next record
+        $this->pos += 4 + $length;
+
+        // offset: 0; size: 2; index to row
+        $row = Format::getUInt2d($recordData, 0);
+
+        // offset: 2; size: 2; index to first column
+        $colFirst = Format::getUInt2d($recordData, 2);
+
+        // offset: var; size: 2; index to last column
+        $colLast = Format::getUInt2d($recordData, $length - 2);
+        $columns = $colLast - $colFirst + 1;
+
+        // offset within record data
+        $offset = 4;
+        for ($i = 0; $i < $columns; ++$i) {
+            // offset: var; size: 2; index to XF record
+            $xfIndex = Format::getUInt2d($recordData, $offset);
+
+            // offset: var; size: 4; RK value
+            $numValue = self::getIEEE754(Format::getInt4d($recordData, $offset + 2));
+
+            $this->addCell($row, $colFirst + $i, $numValue, $xfIndex);
+
+            $offset += 6;
+        }
+    }
+
+    /**
+     * Read NUMBER record
+     *
+     * This record represents a cell that contains a floating-point value.
+     */
+    private function readNumber() {
+        $length = Format::getUInt2d($this->data, $this->pos + 2);
+        $recordData = substr($this->data, $this->pos + 4, $length);
+
+        // move stream pointer to next record
+        $this->pos += 4 + $length;
+
+        // offset: 0; size: 2; index to row
+        $row = Format::getUInt2d($recordData, 0);
+
+        // offset: 2; size 2; index to column
+        $column = Format::getUInt2d($recordData, 2);
+
+        // offset 4; size: 2; index to XF record
+        $xfIndex = Format::getUInt2d($recordData, 4);
+        $numValue = self::extractNumber(substr($recordData, 6, 8));
+
+        $this->addCell($row, $column, $numValue, $xfIndex);
+    }
+
+    /**
+     * 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 = Format::getUInt2d($this->data, $this->pos + 2);
+        $recordData = substr($this->data, $this->pos + 4, $length);
+
+        // move stream pointer to next record
+        $this->pos += 4 + $length;
+
+        // offset: 0; size: 2; row index
+        $row = Format::getUInt2d($recordData, 0);
+
+        // offset: 2; size: 2; col index
+        $column = Format::getUInt2d($recordData, 2);
+
+        // offset 4; size: 2; index to XF record
+        $xfIndex = Format::getUInt2d($recordData, 4);
+
+        // offset: 6; size: 8; result of the formula
+        if ((ord($recordData{6}) == 0) && (ord($recordData{12}) == 255) && (ord($recordData{13}) == 255)) {
+            // read STRING record
+            $value = $this->readString();
+        } elseif ((ord($recordData{6}) == 1) && (ord($recordData{12}) == 255) && (ord($recordData{13}) == 255)) {
+            // Boolean formula. Result is in +2; 0=false, 1=true
+            $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
+            $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
+            $value = '';
+        } else {
+            // forumla result is a number, first 14 bytes like _NUMBER record
+            $value = self::extractNumber(substr($recordData, 6, 8));
+        }
+
+        $this->addCell($row, $column, $value, $xfIndex);
+    }
+
+    /**
+     * 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 = Format::getUInt2d($this->data, $this->pos + 2);
+        $recordData = substr($this->data, $this->pos + 4, $length);
+
+        // move stream pointer to next record
+        $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'];
+        }
+
+        return $value;
+    }
+
+    /**
+     * Read BOOLERR record
+     *
+     * This record represents a Boolean value or error value cell.
+     */
+    private function readBoolErr() {
+        $length = Format::getUInt2d($this->data, $this->pos + 2);
+        $recordData = substr($this->data, $this->pos + 4, $length);
+
+        // move stream pointer to next record
+        $this->pos += 4 + $length;
+
+        // offset: 0; size: 2; row index
+        $row = Format::getUInt2d($recordData, 0);
+
+        // offset: 2; size: 2; column index
+        $column = Format::getUInt2d($recordData, 2);
+
+        // offset: 4; size: 2; index to XF record
+        $xfIndex = Format::getUInt2d($recordData, 4);
+
+        // offset: 6; size: 1; the boolean value or error value
+        $boolError = ord($recordData{6});
+
+        // offset: 7; size: 1; 0=boolean; 1=error
+        $isError = ord($recordData{7});
+
+        switch ($isError) {
+            case 0: // boolean
+                $value = (bool)$boolError;
+
+                // add cell value
+                $this->addCell($row, $column, $value, $xfIndex);
+                break;
+            case 1: // error type
+                $value = self::mapErrorCode($boolError);
+
+                // add cell value
+                $this->addCell($row, $column, $value, $xfIndex);
+                break;
+        }
+    }
+
+    /**
+     * Read BLANK record
+     */
+    private function readBlank() {
+        $length = Format::getUInt2d($this->data, $this->pos + 2);
+        $recordData = substr($this->data, $this->pos + 4, $length);
+
+        // move stream pointer to next record
+        $this->pos += 4 + $length;
+
+        // offset: 0; size: 2; row index
+        $row = Format::getUInt2d($recordData, 0);
+
+        // offset: 2; size: 2; col index
+        $column = Format::getUInt2d($recordData, 2);
+
+        // offset: 4; size: 2; XF index
+        $xfIndex = Format::getUInt2d($recordData, 4);
+
+        $this->addCell($row, $column, '', $xfIndex);
+    }
+
+    /**
+     * 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 = Format::getUInt2d($this->data, $this->pos + 2);
+        $recordData = substr($this->data, $this->pos + 4, $length);
+
+        // move stream pointer to next record
+        $this->pos += 4 + $length;
+
+        // offset: 0; size: 2; index to row
+        $row = Format::getUInt2d($recordData, 0);
+
+        // offset: 2; size: 2; index to column
+        $column = Format::getUInt2d($recordData, 2);
+
+        // offset: 4; size: 2; XF index
+        $xfIndex = Format::getUInt2d($recordData, 4);
+
+        // add cell value
+        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, $xfIndex);
+    }
+
+    /**
+     * Map error code, e.g. '#N/A'
+     *
+     * @param int $code
+     * @return string
+     */
+    private static function mapErrorCode($code) {
+        if (isset(self::$errorCode[$code])) {
+            return self::$errorCode[$code];
+        }
+
+        return false;
+    }
+
+    /**
+     * Convert a value in a pre-defined format to a PHP string
+     *
+     * @param mixed $value    Value to format
+     * @param string $format  Format code
+     * @return string
+     */
+    private static function toFormattedString($value = '0', $format = Format::FORMAT_GENERAL) {
+        // For now we do not treat strings although section 4 of a format code affects strings
+        if (!is_numeric($value)) {
+            return $value;
+        }
+
+        // For 'General' format code, we just pass the value although this is not entirely the way Excel does it,
+        // it seems to round numbers to a total of 10 digits.
+        if (($format === Format::FORMAT_GENERAL) || ($format === Format::FORMAT_TEXT)) {
+            return $value;
+        }
+
+        // Convert any other escaped characters to quoted strings, e.g. (\T to "T")
+        $format = preg_replace('/(\\\(.))(?=(?:[^"]|"[^"]*")*$)/u', '"${2}"', $format);
+
+        // Get the sections, there can be up to four sections, separated with a semi-colon (but only if not a quoted literal)
+        $sections = preg_split('/(;)(?=(?:[^"]|"[^"]*")*$)/u', $format);
+
+        // Extract the relevant section depending on whether number is positive, negative, or zero?
+        // Text not supported yet.
+        // Here is how the sections apply to various values in Excel:
+        //   1 section:   [POSITIVE/NEGATIVE/ZERO/TEXT]
+        //   2 sections:  [POSITIVE/ZERO/TEXT] [NEGATIVE]
+        //   3 sections:  [POSITIVE/TEXT] [NEGATIVE] [ZERO]
+        //   4 sections:  [POSITIVE] [NEGATIVE] [ZERO] [TEXT]
+        switch (count($sections)) {
+            case 1:
+                $format = $sections[0];
+                break;
+
+            case 2:
+                $format = ($value >= 0) ? $sections[0] : $sections[1];
+                $value = abs($value); // Use the absolute value
+                break;
+
+            case 3:
+                $format = ($value > 0) ? $sections[0] : ( ($value < 0) ? $sections[1] : $sections[2]);
+                $value = abs($value); // Use the absolute value
+                break;
+
+            case 4:
+                $format = ($value > 0) ? $sections[0] : ( ($value < 0) ? $sections[1] : $sections[2]);
+                $value = abs($value); // Use the absolute value
+                break;
+
+            default:
+                // something is wrong, just use first section
+                $format = $sections[0];
+                break;
+        }
+
+        // In Excel formats, "_" is used to add spacing,
+        //    The following character indicates the size of the spacing, which we can't do in HTML, so we just use a standard space
+        $format = preg_replace('/_./', ' ', $format);
+
+        // Save format with color information for later use below
+        //$formatColor = $format;
+
+        // Strip color information
+        $colorRegex = '/^\\[[a-zA-Z]+\\]/';
+        $format = preg_replace($colorRegex, '', $format);
+
+        // Let's begin inspecting the format and converting the value to a formatted string
+        //  Check for date/time characters (not inside quotes)
+        if (preg_match('/(\[\$[A-Z]*-[0-9A-F]*\])*[hmsdy](?=(?:[^"]|"[^"]*")*$)/miu', $format, $matches)) {
+            // datetime format
+            self::formatAsDate($value, $format);
+        } elseif (preg_match('/%$/', $format)) {
+            // % number format
+            self::formatAsPercentage($value, $format);
+        } else {
+            if ($format === Format::FORMAT_CURRENCY_EUR_SIMPLE) {
+                $value = 'EUR ' . sprintf('%1.2f', $value);
+            } else {
+                // Some non-number strings are quoted, so we'll get rid of the quotes, likewise any positional * symbols
+                $format = str_replace(['"', '*'], '', $format);
+
+                // Find out if we need thousands separator
+                // This is indicated by a comma enclosed by a digit placeholder:
+                // #,# or 0,0
+                $useThousands = preg_match('/(#,#|0,0)/', $format);
+                if ($useThousands) {
+                    $format = preg_replace('/0,0/', '00', $format);
+                    $format = preg_replace('/#,#/', '##', $format);
+                }
+
+                // Scale thousands, millions,...
+                // This is indicated by a number of commas after a digit placeholder:
+                // #, or 0.0,,
+                $scale = 1; // same as no scale
+                $matches = [];
+                if (preg_match('/(#|0)(,+)/', $format, $matches)) {
+                    $scale = pow(1000, strlen($matches[2]));
+
+                    // strip the commas
+                    $format = preg_replace('/0,+/', '0', $format);
+                    $format = preg_replace('/#,+/', '#', $format);
+                }
+
+                if (preg_match('/#?.*\?\/\?/', $format, $m)) {
+                    //echo 'Format mask is fractional '.$format.' <br />';
+                    if ($value != (int)$value) {
+                        self::formatAsFraction($value, $format);
+                    }
+                } else {
+                    // Handle the number itself
+                    // scale number
+                    $value = $value / $scale;
+
+                    // Strip #
+                    $format = preg_replace('/\\#/', '0', $format);
+                    $n = "/\[[^\]]+\]/";
+                    $m = preg_replace($n, '', $format);
+                    $numberRegex = "/(0+)(\.?)(0*)/";
+                    if (preg_match($numberRegex, $m, $matches)) {
+                        $left = $matches[1];
+                        $dec = $matches[2];
+                        $right = $matches[3];
+
+                        // minimun width of formatted number (including dot)
+                        $minWidth = strlen($left) + strlen($dec) + strlen($right);
+                        if ($useThousands) {
+                            $value = number_format(
+                                $value,
+                                strlen($right),
+                                self::getDecimalSeparator(),
+                                self::getThousandsSeparator()
+                            );
+
+                            $value = preg_replace($numberRegex, $value, $format);
+                        } else {
+                            if (preg_match('/[0#]E[+-]0/i', $format)) {
+                                //Scientific format
+                                $value = sprintf('%5.2E', $value);
+                            } elseif (preg_match('/0([^\d\.]+)0/', $format)) {
+                                $value = self::complexNumberFormatMask($value, $format);
+                            } else {
+                                $sprintfPattern = "%0$minWidth." . strlen($right) . "f";
+                                $value = sprintf($sprintfPattern, $value);
+                                $value = preg_replace($numberRegex, $value, $format);
+                            }
+                        }
+                    }
+                }
+
+                if (preg_match('/\[\$(.*)\]/u', $format, $m)) {
+                    //  Currency or Accounting
+                    //$currencyFormat = $m[0];
+                    $currencyCode = $m[1];
+                    list($currencyCode) = explode('-', $currencyCode);
+
+                    if ($currencyCode == '') {
+                        $currencyCode = self::getCurrencyCode();
+                    }
+
+                    $value = preg_replace('/\[\$([^\]]*)\]/u', $currencyCode, $value);
+                }
+            }
+        }
+
+        return $value;
+    }
+
+    /**
+     * 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() {
+        $i = 0;
+        $data = '';
+        $spliceOffsets = [0];
+
+        do {
+            ++$i;
+            // offset: 0; size: 2; identifier
+            //$identifier = Cell::getInt2d($this->data, $this->pos);
+
+            // offset: 2; size: 2; length
+            $length = Format::getUInt2d($this->data, $this->pos + 2);
+            $data .= substr($this->data, $this->pos + 4, $length);
+            $spliceOffsets[$i] = $spliceOffsets[$i - 1] + $length;
+
+            $this->pos += 4 + $length;
+            $nextIdentifier = Format::getUInt2d($this->data, $this->pos);
+        } while ($nextIdentifier == self::XLS_TYPE_CONTINUE);
+
+        return ['recordData' => $data, 'spliceOffsets' => $spliceOffsets];
+    }
+
+    /**
+     * Get the decimal separator. If it has not yet been set explicitly, try to obtain number formatting
+     * information from locale.
+     *
+     * @return string
+     */
+    private static function getDecimalSeparator() {
+        if (!isset(self::$decimalSeparator)) {
+            $localeconv = localeconv();
+
+            self::$decimalSeparator = ($localeconv['decimal_point'] != '') ? $localeconv['decimal_point']
+                : $localeconv['mon_decimal_point'];
+
+            if (self::$decimalSeparator == '') {
+                // Default to .
+                self::$decimalSeparator = '.';
+            }
+        }
+
+        return self::$decimalSeparator;
+    }
+
+    /**
+     * Get the thousands separator. If it has not yet been set explicitly, try to obtain number formatting
+     * information from locale.
+     *
+     * @return string
+     */
+    private static function getThousandsSeparator() {
+        if (!isset(self::$thousandsSeparator)) {
+            $localeconv = localeconv();
+
+            self::$thousandsSeparator = ($localeconv['thousands_sep'] != '') ? $localeconv['thousands_sep']
+                : $localeconv['mon_thousands_sep'];
+
+            if (self::$thousandsSeparator == '') {
+                // Default to .
+                self::$thousandsSeparator = ',';
+            }
+        }
+
+        return self::$thousandsSeparator;
+    }
+
+    /**
+     * Get the currency code. If it has not yet been set explicitly, try to obtain the symbol information from locale.
+     *
+     * @return string
+     */
+    private static function getCurrencyCode() {
+        if (!isset(self::$currencyCode)) {
+            $localeconv = localeconv();
+
+            self::$currencyCode = ($localeconv['currency_symbol'] != '') ? $localeconv['currency_symbol']
+                : $localeconv['int_curr_symbol'];
+
+            if (self::$currencyCode == '') {
+                // Default to $
+                self::$currencyCode = '$';
+            }
+        }
+
+        return self::$currencyCode;
+    }
+
+    private static function complexNumberFormatMask($number, $mask) {
+        $sign = ($number < 0.0);
+        $number = abs($number);
+
+        if (strpos($mask, '.') !== false) {
+            $numbers = explode('.', $number . '.0');
+            $masks = explode('.', $mask . '.0');
+            $result1 = self::complexNumberFormatMask($numbers[0], $masks[0]);
+            $result2 = strrev(self::complexNumberFormatMask(strrev($numbers[1]), strrev($masks[1])));
+
+            return (($sign) ? '-' : '') . $result1 . '.' . $result2;
+        }
+
+        $r = preg_match_all('/0+/', $mask, $result, PREG_OFFSET_CAPTURE);
+        if ($r > 1) {
+            $result = array_reverse($result[0]);
+
+            $offset = 0;
+            foreach ($result as $block) {
+                $divisor = 1 . $block[0];
+                $size = strlen($block[0]);
+                $offset = $block[1];
+                $blockValue = sprintf('%0' . $size . 'd', fmod($number, $divisor));
+
+                $number = floor($number / $divisor);
+                $mask = substr_replace($mask, $blockValue, $offset, $size);
+            }
+
+            if ($number > 0) {
+                $mask = substr_replace($mask, $number, $offset, 0);
+            }
+
+            $result = $mask;
+        } else {
+            $result = $number;
+        }
+
+        return (($sign) ? '-' : '') . $result;
+    }
+
+    /**
+     * Convert Microsoft Code Page Identifier to Code Page Name which iconv and mbstring understands
+     *
+     * @param int $codePage Microsoft Code Page Indentifier
+     *
+     * @throws ParserException
+     * @return string Code Page Name
+     */
+    private static function NumberToName($codePage = 1252) {
+        switch ($codePage) {
+            case 367:
+                return 'ASCII'; //ASCII
+
+            case 437:
+                return 'CP437'; //OEM US
+
+            case 720:
+                throw new ParserException('Code page 720 not supported.', 5); //OEM Arabic
+
+            case 737:
+                return 'CP737'; //OEM Greek
+
+            case 775:
+                return 'CP775'; //OEM Baltic
+
+            case 850:
+                return 'CP850'; //OEM Latin I
+
+            case 852:
+                return 'CP852'; //OEM Latin II (Central European)
+
+            case 855:
+                return 'CP855'; //OEM Cyrillic
+
+            case 857:
+                return 'CP857'; //OEM Turkish
+
+            case 858:
+                return 'CP858'; //OEM Multilingual Latin I with Euro
+
+            case 860:
+                return 'CP860'; //OEM Portugese
+
+            case 861:
+                return 'CP861'; //OEM Icelandic
+
+            case 862:
+                return 'CP862'; //OEM Hebrew
+
+            case 863:
+                return 'CP863'; //OEM Canadian (French)
+
+            case 864:
+                return 'CP864'; //OEM Arabic
+
+            case 865:
+                return 'CP865'; //OEM Nordic
+
+            case 866:
+                return 'CP866'; //OEM Cyrillic (Russian)
+
+            case 869:
+                return 'CP869'; //OEM Greek (Modern)
+
+            case 874:
+                return 'CP874'; //ANSI Thai
+
+            case 932:
+                return 'CP932'; //ANSI Japanese Shift-JIS
+
+            case 936:
+                return 'CP936'; //ANSI Chinese Simplified GBK
+
+            case 949:
+                return 'CP949'; //ANSI Korean (Wansung)
+
+            case 950:
+                return 'CP950'; //ANSI Chinese Traditional BIG5
+
+            case 1200:
+                return 'UTF-16LE'; //UTF-16 (BIFF8)
+
+            case 1250:
+                return 'CP1250'; //ANSI Latin II (Central European)
+
+            case 1251:
+                return 'CP1251'; //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'; //ANSI Latin I (BIFF4-BIFF7)
+
+            case 1253:
+                return 'CP1253'; //ANSI Greek
+
+            case 1254:
+                return 'CP1254'; //ANSI Turkish
+
+            case 1255:
+                return 'CP1255'; //ANSI Hebrew
+
+            case 1256:
+                return 'CP1256'; //ANSI Arabic
+
+            case 1257:
+                return 'CP1257'; //ANSI Baltic
+
+            case 1258:
+                return 'CP1258'; //ANSI Vietnamese
+
+            case 1361:
+                return 'CP1361'; //ANSI Korean (Johab)
+
+            case 10000:
+                return 'MAC'; //Apple Roman
+
+            case 10001:
+                return 'CP932'; //Macintosh Japanese
+
+            case 10002:
+                return 'CP950'; //Macintosh Chinese Traditional
+
+            case 10003:
+                return 'CP1361'; //Macintosh Korean
+
+            case 10004:
+                return 'MACARABIC'; //	Apple Arabic
+
+            case 10005:
+                return 'MACHEBREW'; //Apple Hebrew
+
+            case 10006:
+                return 'MACGREEK'; //Macintosh Greek
+
+            case 10007:
+                return 'MACCYRILLIC'; //Macintosh Cyrillic
+
+            case 10008:
+                return 'CP936'; //Macintosh - Simplified Chinese (GB 2312)
+
+            case 10010:
+                return 'MACROMANIA'; //Macintosh Romania
+
+            case 10017:
+                return 'MACUKRAINE'; //Macintosh Ukraine
+
+            case 10021:
+                return 'MACTHAI'; //Macintosh Thai
+
+            case 10029:
+                return 'MACCENTRALEUROPE'; //Macintosh Central Europe
+
+            case 10079:
+                return 'MACICELAND'; //Macintosh Icelandic
+
+            case 10081:
+                return 'MACTURKISH'; //Macintosh Turkish
+
+            case 10082:
+                return 'MACCROATIAN'; //Macintosh Croatian
+
+            case 21010:
+                return 'UTF-16LE'; //UTF-16 (BIFF8) This isn't correct, but some Excel writer libraries erroneously
+                                   // use Codepage 21010 for UTF-16LE
+
+            case 32768:
+                return 'MAC'; //Apple Roman
+
+            case 32769:
+                throw new ParserException('Code page 32769 not supported.', 6); //ANSI Latin I (BIFF2-BIFF3)
+
+            case 65000:
+                return 'UTF-7'; //Unicode (UTF-7)
+
+            case 65001:
+                return 'UTF-8'; //Unicode (UTF-8)
+        }
+
+        throw new ParserException("Unknown codepage: $codePage", 7);
+    }
+
+    /**
+     * Read byte string (8-bit string length). OpenOffice documentation: 2.5.2
+     *
+     * @param string $subData
+     *
+     * @return array
+     */
+    private function readByteStringShort($subData) {
+        // offset: 0; size: 1; length of the string (character count)
+        $ln = ord($subData[0]);
+
+        // offset: 1: size: var; character array (8-bit characters)
+        $value = $this->decodeCodepage(substr($subData, 1, $ln));
+
+        // size in bytes of data structure
+        return ['value' => $value, 'size' => 1 + $ln];
+    }
+
+    /**
+     * 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 = Format::getUInt2d($subData, 0);
+
+        // offset: 2: size: var; character array (8-bit characters)
+        $value = $this->decodeCodepage(substr($subData, 2));
+
+        // size in bytes of data structure
+        return ['value' => $value, 'size' => 2 + $ln];
+    }
+
+    private static function formatAsDate(&$value, &$format) {
+        // strip off first part containing e.g. [$-F800] or [$USD-409]
+        // general syntax: [$<Currency string>-<language info>]
+        // language info is in hexadecimal
+        $format = preg_replace('/^(\[\$[A-Z]*-[0-9A-F]*\])/i', '', $format);
+
+        // OpenOffice.org uses upper-case number formats, e.g. 'YYYY', convert to lower-case;
+        // but we don't want to change any quoted strings
+        $format = preg_replace_callback('/(?:^|")([^"]*)(?:$|")/', ['self', 'setLowercaseCallback'], $format);
+
+        // Only process the non-quoted blocks for date format characters
+        $blocks = explode('"', $format);
+
+        foreach($blocks as $key => &$block) {
+            if ($key % 2 == 0) {
+                $block = strtr($block, Format::$dateFormatReplacements);
+                if (strpos($block, 'A') === false) {
+                    // 24-hour time format
+                    $block = strtr($block, Format::$dateFormatReplacements24);
+                } else {
+                    // 12-hour time format
+                    $block = strtr($block, Format::$dateFormatReplacements12);
+                }
+            }
+        }
+
+        $format = implode('"', $blocks);
+
+        // escape any quoted characters so that DateTime format() will render them correctly
+        $format = preg_replace_callback('/"(.*)"/U', ['self', 'escapeQuotesCallback'], $format);
+        $dateObj = self::ExcelToPHPObject($value);
+
+        $value = $dateObj->format($format);
+    }
+
+    private static function setLowercaseCallback($matches) {
+        return mb_strtolower($matches[0]);
+    }
+
+    private static function escapeQuotesCallback($matches) {
+        return '\\' . implode('\\', str_split($matches[1]));
+    }
+
+    /**
+     * Convert a date from Excel to a PHP Date/Time object
+     *
+     * @param int $dateValue Excel date/time value
+     *
+     * @return \DateTime PHP date/time object
+     */
+    private static function ExcelToPHPObject($dateValue = 0) {
+        $dateTime = self::ExcelToPHP($dateValue);
+
+        $days = floor($dateTime / 86400);
+        $time = round((($dateTime / 86400) - $days) * 86400);
+        $hours = round($time / 3600);
+        $minutes = round($time / 60) - ($hours * 60);
+        $seconds = round($time) - ($hours * 3600) - ($minutes * 60);
+
+        $dateObj = new \DateTime("1-Jan-1970+$days days");
+        $dateObj->setTime($hours, $minutes, $seconds);
+
+        return $dateObj;
+    }
+
+    /**
+     * Convert a date from Excel to PHP
+     *
+     * @param int $dateValue Excel date/time value
+     *
+     * @return int PHP serialized date/time
+     */
+    private static function ExcelToPHP($dateValue = 0) {
+        if (self::$excelBaseDate == Format::CALENDAR_WINDOWS_1900) {
+            $excelBaseDate = 25569;
+
+            //Adjust for the spurious 29-Feb-1900 (Day 60)
+            if ($dateValue < 60) {
+                --$excelBaseDate;
+            }
+        } else {
+            $excelBaseDate = 24107;
+        }
+
+        // Perform conversion
+        if ($dateValue >= 1) {
+            $utcDays = $dateValue - $excelBaseDate;
+            $returnValue = round($utcDays * 86400);
+
+            if (($returnValue <= PHP_INT_MAX) && ($returnValue >= -PHP_INT_MAX)) {
+                $returnValue = (integer) $returnValue;
+            }
+        } else {
+            $hours = round($dateValue * 24);
+            $mins = round($dateValue * 1440) - round($hours * 60);
+            $secs = round($dateValue * 86400) - round($hours * 3600) - round($mins * 60);
+
+            $returnValue = (integer) gmmktime($hours, $mins, $secs);
+        }
+
+        return $returnValue;
+    }
+
+    private static function formatAsPercentage(&$value, &$format) {
+        if ($format === Format::FORMAT_PERCENTAGE) {
+            $value = round((100 * $value), 0) . '%';
+        } else {
+            if (preg_match('/\.[#0]+/i', $format, $m)) {
+                $s = substr($m[0], 0, 1) . (strlen($m[0]) - 1);
+                $format = str_replace($m[0], $s, $format);
+            }
+
+            if (preg_match('/^[#0]+/', $format, $m)) {
+                $format = str_replace($m[0], strlen($m[0]), $format);
+            }
+
+            $format = '%' . str_replace('%', 'f%%', $format);
+            $value = sprintf($format, 100 * $value);
+        }
+    }
+
+    private static function formatAsFraction(&$value, &$format) {
+        $sign = ($value < 0) ? '-' : '';
+        $integerPart = floor(abs($value));
+        $decimalPart = trim(fmod(abs($value), 1), '0.');
+        $decimalLength = strlen($decimalPart);
+        $decimalDivisor = pow(10, $decimalLength);
+
+        $GCD = self::GCD([$decimalPart, $decimalDivisor]);
+        $adjustedDecimalPart = $decimalPart/$GCD;
+        $adjustedDecimalDivisor = $decimalDivisor/$GCD;
+
+        if ((strpos($format, '0') !== false) || (strpos($format, '#') !== false) || (substr($format, 0, 3) == '? ?')) {
+            if ($integerPart == 0) {
+                $integerPart = '';
+            }
+
+            $value = "$sign$integerPart $adjustedDecimalPart/$adjustedDecimalDivisor";
+        } else {
+            $adjustedDecimalPart += $integerPart * $adjustedDecimalDivisor;
+            $value = "$sign$adjustedDecimalPart/$adjustedDecimalDivisor";
+        }
+    }
+
+    /**
+     * GCD
+     *
+     * Returns the greatest common divisor of a series of numbers. The greatest common divisor is the largest
+     * integer that divides both number1 and number2 without a remainder.
+     * Excel Function:
+     *     GCD(number1[,number2[, ...]])
+     *
+     * @param array $params
+     *
+     * @return integer Greatest Common Divisor
+     */
+    private static function GCD($params) {
+        $returnValue = 1;
+        $allValuesFactors = [];
+
+        // Loop through arguments
+        $flattenArr = self::flattenArray($params);
+        foreach ($flattenArr as $value) {
+            if (!is_numeric($value)) {
+                return '#VALUE!';
+            } elseif ($value == 0) {
+                continue;
+            } elseif ($value < 0) {
+                return '#NULL!';
+            }
+
+            $factors = self::factors($value);
+            $countedFactors = array_count_values($factors);
+            $allValuesFactors[] = $countedFactors;
+        }
+
+        $allValuesCount = count($allValuesFactors);
+        if ($allValuesCount == 0) {
+            return 0;
+        }
+
+        $mergedArray = $allValuesFactors[0];
+        for ($i=1; $i < $allValuesCount; ++$i) {
+            $mergedArray = array_intersect_key($mergedArray, $allValuesFactors[$i]);
+        }
+
+        $mergedArrayValues = count($mergedArray);
+
+        if ($mergedArrayValues == 0) {
+            return $returnValue;
+        } elseif ($mergedArrayValues > 1) {
+            foreach ($mergedArray as $mergedKey => $mergedValue) {
+                foreach ($allValuesFactors as $highestPowerTest) {
+                    foreach ($highestPowerTest as $testKey => $testValue) {
+                        if (($testKey == $mergedKey) && ($testValue < $mergedValue)) {
+                            $mergedArray[$mergedKey] = $testValue;
+                            $mergedValue = $testValue;
+                        }
+                    }
+                }
+            }
+
+            $returnValue = 1;
+            foreach ($mergedArray as $key => $value) {
+                $returnValue *= pow($key, $value);
+            }
+
+            return $returnValue;
+        } else {
+            $keys = array_keys($mergedArray);
+            $key = $keys[0];
+            $value = $mergedArray[$key];
+
+            foreach ($allValuesFactors as $testValue) {
+                foreach ($testValue as $mergedKey => $mergedValue) {
+                    if (($mergedKey == $key) && ($mergedValue < $value)) {
+                        $value = $mergedValue;
+                    }
+                }
+            }
+
+            return pow($key, $value);
+        }
+    }
+
+    /**
+     * Convert a multi-dimensional array to a simple 1-dimensional array
+     *
+     * @param array $array Array to be flattened
+     *
+     * @return array Flattened array
+     */
+    private static function flattenArray($array) {
+        if (!is_array($array)) {
+            return (array) $array;
+        }
+
+        $arrayValues = [];
+        foreach ($array as $value) {
+            if (is_array($value)) {
+                foreach ($value as $val) {
+                    if (is_array($val)) {
+                        foreach ($val as $v) {
+                            $arrayValues[] = $v;
+                        }
+                    } else {
+                        $arrayValues[] = $val;
+                    }
+                }
+            } else {
+                $arrayValues[] = $value;
+            }
+        }
+
+        return $arrayValues;
+    }
+
+    /**
+     * Return an array of the factors of the input value
+     *
+     * @param int $value
+     *
+     * @return array
+     */
+    private static function factors($value) {
+        $startVal = floor(sqrt($value));
+        $factorArray = [];
+
+        for ($i = $startVal; $i > 1; --$i) {
+            if (($value % $i) == 0) {
+                $factorArray = array_merge($factorArray, self::factors($value / $i));
+                $factorArray = array_merge($factorArray, self::factors($i));
+
+                if ($i <= sqrt($value)) {
+                    break;
+                }
+            }
+        }
+
+        if (!empty($factorArray)) {
+            rsort($factorArray);
+
+            return $factorArray;
+        }
+
+        return [(int) $value];
+    }
+
+    /**
+     * 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
+     *
+     * @param string $subData
+     * @param int $characterCount
+     *
+     * @return array
+     */
+    private static function readUnicodeString($subData, $characterCount) {
+        // offset: 0: size: 1; option flags
+        // bit: 0; mask: 0x01; character compression (0 = compressed 8-bit, 1 = uncompressed 16-bit)
+        $isCompressed = !((0x01 & ord($subData[0])) >> 0);
+
+        // 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
+        );
+
+        // the size in bytes including the option flags
+        return ['value' => $value, 'size' => $isCompressed ? 1 + $characterCount : 1 + 2 * $characterCount];
+    }
+
+    /**
+     * Extracts an Excel Unicode short string (8-bit string length), this function will automatically find out
+     * where the Unicode string ends.
+     *
+     * @param string $subData
+     *
+     * @return array
+     */
+    private static function readUnicodeStringShort($subData) {
+        // offset: 0: size: 1; length of the string (character count)
+        $characterCount = ord($subData[0]);
+        $string = self::readUnicodeString(substr($subData, 1), $characterCount);
+
+        // add 1 for the string length
+        $string['size'] += 1;
+
+        return $string;
+    }
+
+    /**
+     * Extracts an Excel Unicode long string (16-bit string length), this function is under construction,
+     * needs to support rich text, and Asian phonetic settings
+     *
+     * @param string $subData
+     *
+     * @return array
+     */
+    private static function readUnicodeStringLong($subData) {
+        // offset: 0: size: 2; length of the string (character count)
+        $characterCount = Format::getUInt2d($subData, 0);
+        $string = self::readUnicodeString(substr($subData, 2), $characterCount);
+
+        // add 2 for the string length
+        $string['size'] += 2;
+
+        return $string;
+    }
+
+    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;
+    }
+
+    /**
+     * 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 = false) {
+        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 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;
+    }
+
+    /**
+     * 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 = Format::getInt4d($data, 4);
+        $rkNumLow = Format::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;
+    }
+}

+ 276 - 0
src/Parser/Excel5/OLERead.php

@@ -0,0 +1,276 @@
+<?php
+/**
+ * OLE File Read
+ *
+ * @author Janson
+ * @create 2017-11-27
+ */
+namespace Asan\PHPExcel\Parser\Excel5;
+
+use Asan\PHPExcel\Exception\ParserException;
+use Asan\PHPExcel\Exception\ReaderException;
+use Asan\PHPExcel\Parser\Format;
+
+defined('IDENTIFIER_OLE') ||
+    define('IDENTIFIER_OLE', pack('CCCCCCCC', 0xd0, 0xcf, 0x11, 0xe0, 0xa1, 0xb1, 0x1a, 0xe1));
+
+class OLERead {
+    // OLE identifier
+    const IDENTIFIER_OLE = IDENTIFIER_OLE;
+
+    // Size of a sector = 512 bytes
+    const BIG_BLOCK_SIZE = 0x200;
+
+    // Size of a short sector = 64 bytes
+    const SMALL_BLOCK_SIZE = 0x40;
+
+    // Size of a directory entry always = 128 bytes
+    const PROPERTY_STORAGE_BLOCK_SIZE = 0x80;
+
+    // Minimum size of a standard stream = 4096 bytes, streams smaller than this are stored as short streams
+    const SMALL_BLOCK_THRESHOLD = 0x1000;
+
+    // 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 $workbook = null;
+    public $summaryInformation = null;
+    public $documentSummaryInformation = null;
+
+    protected $data = '';
+    protected $bigBlockChain = '';
+    protected $smallBlockChain = '';
+    protected $entry = '';
+    protected $props = [];
+    protected $rootEntry = 0;
+    protected $sbdStartBlock = 0;
+    protected $extensionBlock = 0;
+    protected $rootStartBlock = 0;
+    protected $numExtensionBlocks = 0;
+    protected $numBigBlockDepotBlocks = 0;
+
+    /**
+     * Read the file
+     *
+     * @throws ParserException|ReaderException
+     * @param string $file
+     */
+    public function read($file) {
+        $this->openFile($file);
+
+        // Total number of sectors used for the SAT
+        $this->numBigBlockDepotBlocks = Format::getInt4d($this->data, self::NUM_BIG_BLOCK_DEPOT_BLOCKS_POS);
+
+        // SecID of the first sector of the directory stream
+        $this->rootStartBlock = Format::getInt4d($this->data, self::ROOT_START_BLOCK_POS);
+
+        // SecID of the first sector of the SSAT (or -2 if not extant)
+        $this->sbdStartBlock = Format::getInt4d($this->data, self::SMALL_BLOCK_DEPOT_BLOCK_POS);
+
+        // SecID of the first sector of the MSAT (or -2 if no additional sectors are used)
+        $this->extensionBlock = Format::getInt4d($this->data, self::EXTENSION_BLOCK_POS);
+
+        // Total number of sectors used by MSAT
+        $this->numExtensionBlocks = Format::getInt4d($this->data, self::NUM_EXTENSION_BLOCK_POS);
+
+        $bigBlockDepotBlocks = [];
+        $pos = self::BIG_BLOCK_DEPOT_BLOCKS_POS;
+        $bbdBlocks = $this->numBigBlockDepotBlocks;
+        if ($this->numExtensionBlocks != 0) {
+            $bbdBlocks = (self::BIG_BLOCK_SIZE - self::BIG_BLOCK_DEPOT_BLOCKS_POS) / 4;
+        }
+
+        for ($i = 0; $i < $bbdBlocks; ++$i) {
+            $bigBlockDepotBlocks[$i] = Format::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] = Format::getInt4d($this->data, $pos);
+                $pos += 4;
+            }
+
+            $bbdBlocks += $blocksToRead;
+            if ($bbdBlocks < $this->numBigBlockDepotBlocks) {
+                $this->extensionBlock = Format::getInt4d($this->data, $pos);
+            }
+        }
+
+        $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);
+        }
+
+        $sbdBlock = $this->sbdStartBlock;
+        $this->smallBlockChain = '';
+        while ($sbdBlock != -2) {
+            $pos = ($sbdBlock + 1) * self::BIG_BLOCK_SIZE;
+            $this->smallBlockChain .= substr($this->data, $pos, 4 * $bbs);
+
+            $sbdBlock = Format::getInt4d($this->bigBlockChain, $sbdBlock * 4);
+        }
+
+        // read the directory stream
+        $block = $this->rootStartBlock;
+        $this->entry = $this->readData($block);
+        $this->readPropertySets();
+    }
+
+    /**
+     * Open file for reading
+     *
+     * @param string $file
+     *
+     * @throws ReaderException|ParserException
+     */
+    public function openFile($file) {
+        // Check if file exists
+        if (!file_exists($file) || !is_readable($file)) {
+            throw new ReaderException("Could not open file [$file] for reading! File does not exist.");
+        }
+
+        // Get the file data
+        $this->data = file_get_contents($file);
+
+        // Check OLE identifier
+        if (empty($this->data) || substr($this->data, 0, 8) != self::IDENTIFIER_OLE) {
+            throw new ParserException("The file [$file] is not recognised as an OLE file");
+        }
+    }
+
+    /**
+     * Extract binary stream data.
+     *
+     * @param int $stream
+     *
+     * @return string|null
+     */
+    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 = Format::getInt4d($this->smallBlockChain, $block * 4);
+            }
+
+            return $streamData;
+        }
+
+        $numBlocks = $this->props[$stream]['size'] / self::BIG_BLOCK_SIZE;
+        if ($this->props[$stream]['size'] % self::BIG_BLOCK_SIZE != 0) {
+            ++$numBlocks;
+        }
+
+        if ($numBlocks == 0) {
+            return '';
+        }
+
+        $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 = Format::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
+     */
+    protected function readData($bl) {
+        $block = $bl;
+        $data = '';
+
+        while ($block != -2) {
+            $pos = ($block + 1) * self::BIG_BLOCK_SIZE;
+            $data .= substr($this->data, $pos, self::BIG_BLOCK_SIZE);
+            $block = Format::getInt4d($this->bigBlockChain, $block * 4);
+        }
+
+        return $data;
+    }
+
+    /**
+     * Read entries in the directory stream.
+     */
+    protected function readPropertySets() {
+        $offset = 0;
+
+        // loop through entires, each entry is 128 bytes
+        $entryLen = strlen($this->entry);
+        while ($offset < $entryLen) {
+            // entry data (128 bytes)
+            $d = substr($this->entry, $offset, self::PROPERTY_STORAGE_BLOCK_SIZE);
+
+            // size in bytes of name
+            $nameSize = ord($d[self::SIZE_OF_NAME_POS]) | (ord($d[self::SIZE_OF_NAME_POS + 1]) << 8);
+
+            // type of entry
+            $type = ord($d[self::TYPE_POS]);
+
+            // sectorID of first sector or short sector, if this entry refers to a stream (the case with workbook)
+            // sectorID of first sector of the short-stream container stream, if this entry is root entry
+            $startBlock = Format::getInt4d($d, self::START_BLOCK_POS);
+            $size = Format::getInt4d($d, self::SIZE_POS);
+            $name = str_replace("\x00", '', substr($d, 0, $nameSize));
+            $this->props[] = [
+                'name' => $name,
+                'type' => $type,
+                'startBlock' => $startBlock,
+                'size' => $size,
+            ];
+
+            // tmp helper to simplify checks
+            $upName = strtoupper($name);
+
+            // Workbook directory entry (BIFF5 uses Book, BIFF8 uses Workbook)
+            if (($upName === 'WORKBOOK') || ($upName === 'BOOK')) {
+                $this->workbook = count($this->props) - 1;
+            } elseif ($upName === 'ROOT ENTRY' || $upName === 'R') {
+                // Root entry
+                $this->rootEntry = count($this->props) - 1;
+            }
+
+            // Summary information
+            if ($name == chr(5) . 'SummaryInformation') {
+                $this->summaryInformation = count($this->props) - 1;
+            }
+
+            // Additional Document Summary information
+            if ($name == chr(5) . 'DocumentSummaryInformation') {
+                $this->documentSummaryInformation = count($this->props) - 1;
+            }
+
+            $offset += self::PROPERTY_STORAGE_BLOCK_SIZE;
+        }
+    }
+}

+ 63 - 0
src/Parser/Excel5/RC4.php

@@ -0,0 +1,63 @@
+<?php
+/**
+ * Excel5 RC4
+ *
+ * @author Janson
+ * @create 2017-11-29
+ */
+namespace Asan\PHPExcel\Parser\Excel5;
+
+class RC4 {
+    // Context
+    private $s = [];
+    private $i = 0;
+    private $j = 0;
+
+    /**
+     * RC4 stream decryption/encryption constrcutor
+     *
+     * @param string $key Encryption key/passphrase
+     */
+    public function __construct($key) {
+        $len = strlen($key);
+
+        for ($this->i = 0; $this->i < 256; $this->i++) {
+            $this->s[$this->i] = $this->i;
+        }
+
+        $this->j = 0;
+        for ($this->i = 0; $this->i < 256; $this->i++) {
+            $this->j = ($this->j + $this->s[$this->i] + ord($key[$this->i % $len])) % 256;
+            $t = $this->s[$this->i];
+            $this->s[$this->i] = $this->s[$this->j];
+            $this->s[$this->j] = $t;
+        }
+
+        $this->i = $this->j = 0;
+    }
+
+    /**
+     * Symmetric decryption/encryption function
+     *
+     * @param string $data Data to encrypt/decrypt
+     *
+     * @return string
+     */
+    public function RC4($data) {
+        $len = strlen($data);
+
+        for ($c = 0; $c < $len; $c++) {
+            $this->i = ($this->i + 1) % 256;
+            $this->j = ($this->j + $this->s[$this->i]) % 256;
+            $t = $this->s[$this->i];
+            $this->s[$this->i] = $this->s[$this->j];
+            $this->s[$this->j] = $t;
+
+            $t = ($this->s[$this->i] + $this->s[$this->j]) % 256;
+
+            $data[$c] = chr(ord($data[$c]) ^ $this->s[$t]);
+        }
+
+        return $data;
+    }
+}

+ 278 - 0
src/Parser/Format.php

@@ -0,0 +1,278 @@
+<?php
+/**
+ * Format helper
+ *
+ * @author Janson
+ * @create 2017-11-27
+ */
+namespace Asan\PHPExcel\Parser;
+
+use Asan\PHPExcel\Exception\ParserException;
+
+class Format {
+    //Base date of 1st Jan 1900 = 1.0
+    const CALENDAR_WINDOWS_1900 = 1900;
+
+    //Base date of 2nd Jan 1904 = 1.0
+    const CALENDAR_MAC_1904 = 1904;
+
+    // Pre-defined formats
+    const FORMAT_GENERAL = 'General';
+    const FORMAT_TEXT = '@';
+
+    const FORMAT_PERCENTAGE = '0%';
+    const FORMAT_PERCENTAGE_00 = '0.00%';
+    const FORMAT_CURRENCY_EUR_SIMPLE = '[$EUR ]#,##0.00_-';
+
+    public static $buildInFormats = [
+        0 => self::FORMAT_GENERAL,
+        1 => '0',
+        2 => '0.00',
+        3 => '#,##0',
+        4 => '#,##0.00',
+        5 => '"$"#,##0_),("$"#,##0)',
+        6 => '"$"#,##0_),[Red]("$"#,##0)',
+        7 => '"$"#,##0.00_),("$"#,##0.00)',
+        8 => '"$"#,##0.00_),[Red]("$"#,##0.00)',
+        9 => '0%',
+        10 => '0.00%',
+        //11 => '0.00E+00',
+        12 => '# ?/?',
+        13 => '# ??/??',
+        14 => 'yyyy/m/d',
+        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 => 'yyyy/m/d h:mm',
+
+        // 补充
+        28 => 'm月d日',
+        31 => 'yyyy年m月d日',
+        32 => 'h时i分',
+        33 => 'h时i分ss秒',
+        34 => 'AM/PM h时i分',
+        35 => 'AM/PM h时i分ss秒',
+        55 => 'AM/PM h时i分',
+        56 => 'AM/PM h时i分ss秒',
+        58 => 'm月d日',
+
+        37 => '#,##0_),(#,##0)',
+        38 => '#,##0_),[Red](#,##0)',
+        39 => '#,##0.00_),(#,##0.00)',
+        40 => '#,##0.00_),[Red](#,##0.00)',
+        41 => '_("$"* #,##0_),_("$"* (#,##0),_("$"* "-"_),_(@_)',
+        42 => '_(* #,##0_),_(* (#,##0),_(* "-"_),_(@_)',
+        43 => '_(* #,##0.00_),_(* (#,##0.00),_(* "-"??_),_(@_)',
+        44 => '_("$"* #,##0.00_),_("$"* \(#,##0.00\),_("$"* "-"??_),_(@_)',
+        45 => 'mm:ss',
+        46 => '[h]:mm:ss',
+        47 => 'mm:ss.0',
+        48 => '##0.0E+0',
+        49 => '@',
+
+        // CHT
+        27 => 'yyyy年m月',
+        30 => 'm/d/yy',
+        36 => '[$-404]e/m/d',
+        50 => '[$-404]e/m/d',
+        57 => 'yyyy年m月',
+
+        // THA
+        59 => 't0',
+        60 => 't0.00',
+        61 => 't#,##0',
+        62 => 't#,##0.00',
+        67 => 't0%',
+        68 => 't0.00%',
+        69 => 't# ?/?',
+        70 => 't# ??/??'
+    ];
+
+    /**
+     * Search/replace values to convert Excel date/time format masks to PHP format masks
+     *
+     * @var array
+     */
+    public static $dateFormatReplacements = [
+        // first remove escapes related to non-format characters
+        '\\' => '',
+
+        // 12-hour suffix
+        'am/pm' => 'A',
+
+        // 2-digit year
+        'e' => 'Y',
+        'yyyy' => 'Y',
+        'yy' => 'y',
+
+        // first letter of month - no php equivalent
+        'mmmmm' => 'M',
+
+        // full month name
+        'mmmm' => 'F',
+
+        // short month name
+        'mmm' => 'M',
+
+        // mm is minutes if time, but can also be month w/leading zero
+        // so we try to identify times be the inclusion of a : separator in the mask
+        // It isn't perfect, but the best way I know how
+        ':mm' => ':i',
+        'mm:' => 'i:',
+
+        // month leading zero
+        'mm' => 'm',
+        'm' => 'n',
+
+        // full day of week name
+        'dddd' => 'l',
+
+        // short day of week name
+        'ddd' => 'D',
+
+        // days leading zero
+        'dd' => 'd',
+        'd' => 'j',
+
+        // seconds
+        'ss' => 's',
+
+        // fractional seconds - no php equivalent
+        '.s' => ''
+    ];
+
+    /**
+     * Search/replace values to convert Excel date/time format masks hours to PHP format masks (24 hr clock)
+     *
+     * @var array
+     */
+    public static $dateFormatReplacements24 = [
+        'hh' => 'H',
+        'h'  => 'G'
+    ];
+
+    /**
+     * Search/replace values to convert Excel date/time format masks hours to PHP format masks (12 hr clock)
+     *
+     * @var array
+     */
+    public static $dateFormatReplacements12 = [
+        'hh' => 'h',
+        'h'  => 'g'
+    ];
+
+    /**
+     * Column index from string
+     *
+     * @param string $label
+     *
+     * @throws \Exception
+     * @return int
+     */
+    public static function columnIndexFromString($label = 'A') {
+        // Using a lookup cache adds a slight memory overhead, but boosts speed
+        // caching using a static within the method is faster than a class static,
+        // though it's additional memory overhead
+        static $indexCache = [];
+
+        if (isset($indexCache[$label])) {
+            return $indexCache[$label];
+        }
+
+        // It's surprising how costly the strtoupper() and ord() calls actually are, so we use a lookup array rather
+        // than use ord() and make it case insensitive to get rid of the strtoupper() as well. Because it's a static,
+        // there's no significant memory overhead either
+        static $columnLookup = [
+            'A' => 1, 'B' => 2, 'C' => 3, 'D' => 4, 'E' => 5, 'F' => 6, 'G' => 7, 'H' => 8, 'I' => 9, 'J' => 10,
+            'K' => 11, 'L' => 12, 'M' => 13, 'N' => 14, 'O' => 15, 'P' => 16, 'Q' => 17, 'R' => 18, 'S' => 19,
+            'T' => 20, 'U' => 21, 'V' => 22, 'W' => 23, 'X' => 24, 'Y' => 25, 'Z' => 26, 'a' => 1, 'b' => 2, 'c' => 3,
+            'd' => 4, 'e' => 5, 'f' => 6, 'g' => 7, 'h' => 8, 'i' => 9, 'j' => 10, 'k' => 11, 'l' => 12, 'm' => 13,
+            'n' => 14, 'o' => 15, 'p' => 16, 'q' => 17, 'r' => 18, 's' => 19, 't' => 20, 'u' => 21, 'v' => 22,
+            'w' => 23, 'x' => 24, 'y' => 25, 'z' => 26
+        ];
+
+        // We also use the language construct isset() rather than the more costly strlen() function to match the length
+        // of $pString for improved performance
+        if (!isset($indexCache[$label])) {
+            if (!isset($label{0}) || isset($label{3})) {
+                throw new ParserException('Column string can not be empty or longer than 3 characters');
+            }
+
+            if (!isset($label{1})) {
+                $indexCache[$label] = $columnLookup[$label];
+            } elseif (!isset($label{2})) {
+                $indexCache[$label] = $columnLookup[$label{0}] * 26 + $columnLookup[$label{1}];
+            } else {
+                $indexCache[$label] = $columnLookup[$label{0}] * 676 + $columnLookup[$label{1}] * 26
+                    + $columnLookup[$label{2}];
+            }
+        }
+
+        return $indexCache[$label];
+    }
+
+    /**
+     * String from columnindex
+     *
+     * @param int $column
+     * @return string
+     */
+    public static function stringFromColumnIndex($column = 0) {
+        // Using a lookup cache adds a slight memory overhead, but boosts speed
+        // caching using a static within the method is faster than a class static,
+        // though it's additional memory overhead
+        static $stringCache = [];
+
+        if (!isset($stringCache[$column])) {
+            // Determine column string
+            if ($column < 26) {
+                $stringCache[$column] = chr(65 + $column);
+            } elseif ($column < 702) {
+                $stringCache[$column] = chr(64 + ($column / 26)) . chr(65 + $column % 26);
+            } else {
+                $stringCache[$column] = chr(64 + (($column - 26) / 676)) . chr(65 + ((($column - 26) % 676) / 26))
+                    . chr(65 + $column % 26);
+            }
+        }
+
+        return $stringCache[$column];
+    }
+
+    /**
+     * Read 16-bit unsigned integer
+     *
+     * @param string $data
+     * @param int $pos
+     * @return int
+     */
+    public static function getUInt2d($data, $pos) {
+        return ord($data[$pos]) | (ord($data[$pos + 1]) << 8);
+    }
+
+    /**
+     * Read 32-bit signed integer
+     *
+     * @param string $data
+     * @param int $pos
+     * @return int
+     */
+    public static function getInt4d($data, $pos) {
+        // FIX: represent numbers correctly on 64-bit system
+        // http://sourceforge.net/tracker/index.php?func=detail&aid=1487372&group_id=99160&atid=623334
+        // Hacked by Andreas Rehm 2006 to ensure correct result of the <<24 block on 32 and 64bit systems
+        $ord24 = ord($data[$pos + 3]);
+
+        if ($ord24 >= 128) {
+            // negative number
+            $ord24 = -abs((256 - $ord24) << 24);
+        } else {
+            $ord24 = ($ord24 & 127) << 24;
+        }
+
+        return ord($data[$pos]) | (ord($data[$pos + 1]) << 8) | (ord($data[$pos + 2]) << 16) | $ord24;
+    }
+}

+ 162 - 0
src/Reader/BaseReader.php

@@ -0,0 +1,162 @@
+<?php
+/**
+ * Reader Abstract
+ *
+ * @author Janson
+ * @create 2017-11-23
+ */
+namespace Asan\PHPExcel\Reader;
+
+use Asan\PHPExcel\Contract\ReaderInterface;
+
+abstract class BaseReader implements ReaderInterface {
+    /**
+     * Generator
+     *
+     * @var \Generator
+     */
+    protected $generator;
+
+    /**
+     * File row count
+     *
+     * @var int
+     */
+    protected $count;
+
+    /**
+     * Max row number
+     *
+     * @var int
+     */
+    protected $rowLimit;
+
+    /**
+     * Max column number
+     *
+     * @var int
+     */
+    protected $columnLimit;
+
+    /**
+     * Return the current element
+     *
+     * @return array
+     */
+    public function current() {
+        return $this->generator->current();
+    }
+
+    /**
+     * Move forward to next element
+     */
+    public function next() {
+        $this->generator->next();
+    }
+
+    /**
+     * Return the key of the current element
+     *
+     * @return int
+     */
+    public function key() {
+        return $this->generator->key();
+    }
+
+    /**
+     * Checks if current position is valid
+     *
+     * @return bool
+     */
+    public function valid() {
+        return $this->generator->valid();
+    }
+
+    /**
+     * Rewind the Iterator to the first element
+     */
+    public function rewind() {
+        $this->generator = $this->makeGenerator();
+    }
+
+    /**
+     * Make the generator
+     */
+    protected function makeGenerator() {
+
+    }
+
+    /**
+     * Ignore empty row
+     *
+     * @param bool $ignoreEmpty
+     */
+    public function ignoreEmptyRow($ignoreEmpty = false) {
+
+    }
+
+    /**
+     * Set row limit
+     *
+     * @param int $limit
+     * @return $this
+     */
+    public function setRowLimit($limit = null) {
+        $this->rowLimit = $limit;
+
+        return $this;
+    }
+
+    /**
+     * Get row limit
+     *
+     * @return int
+     */
+    public function getRowLimit() {
+        return $this->rowLimit;
+    }
+
+    /**
+     * Set column limit
+     *
+     * @param int $limit
+     * @return $this
+     */
+    public function setColumnLimit($limit = null) {
+        $this->columnLimit = $limit;
+
+        return $this;
+    }
+
+    /**
+     * Takes a row and traverses the file to that row
+     *
+     * @param int $row
+     */
+    public function seek($row) {
+        if ($row <= 0) {
+            throw new \InvalidArgumentException("Row $row is invalid");
+        }
+
+        $key = $this->key();
+
+        if ($key !== --$row) {
+            if ($row < $key || is_null($key) || $row == 0) {
+                $this->rewind();
+            }
+
+            while ($this->valid() && $row > $this->key()) {
+                $this->next();
+            }
+        }
+    }
+
+    /**
+     * Get column limit
+     *
+     * @return int
+     */
+    public function getColumnLimit() {
+        return $this->columnLimit;
+    }
+}

+ 319 - 0
src/Reader/Csv.php

@@ -0,0 +1,319 @@
+<?php
+/**
+ * Csv Reader
+ *
+ * @author Janson
+ * @create 2017-11-23
+ */
+namespace Asan\PHPExcel\Reader;
+
+use Asan\PHPExcel\Exception\ReaderException;
+
+class Csv extends BaseReader {
+    /**
+     * File handle
+     *
+     * @var resource
+     */
+    protected $fileHandle;
+
+    /**
+     * File read start
+     *
+     * @var int
+     */
+    protected $start = 0;
+
+    /**
+     * Input encoding
+     *
+     * @var string
+     */
+    protected $inputEncoding;
+
+    /**
+     * Delimiter
+     *
+     * @var string
+     */
+    protected $delimiter;
+
+    /**
+     * Enclosure
+     *
+     * @var string
+     */
+    protected $enclosure = '"';
+
+    /**
+     * Ignore empty row
+     *
+     * @var bool
+     */
+    protected $ignoreEmpty = false;
+
+    /**
+     * Loads Excel from file
+     *
+     * @param string $file
+     *
+     * @throws ReaderException
+     * @return $this
+     */
+    public function load($file) {
+        // Open file
+        $this->openFile($file);
+
+        $this->autoDetection();
+
+        $this->generator = $this->makeGenerator();
+
+        return $this;
+    }
+
+    /**
+     * Count elements of the selected sheet
+     *
+     * @return int
+     */
+    public function count() {
+        if ($this->count === null) {
+            $position = ftell($this->fileHandle);
+            $this->count = iterator_count($this->makeGenerator(true));
+            fseek($this->fileHandle, $position);
+        }
+
+        return $this->count;
+    }
+
+    /**
+     * Make the generator
+     *
+     * @param bool $calculate
+     * @return \Generator
+     */
+    protected function makeGenerator($calculate = false) {
+        $lineEnding = ini_get('auto_detect_line_endings');
+        ini_set('auto_detect_line_endings', true);
+
+        fseek($this->fileHandle, $this->start);
+
+        $finish = 0;
+        while (($row = fgetcsv($this->fileHandle, 0, $this->delimiter, $this->enclosure)) !== false) {
+            if ($this->ignoreEmpty && (empty($row) || trim(implode('', $row)) === '')) {
+                continue;
+            }
+
+            if ($calculate) {
+                yield;
+                continue;
+            }
+
+            if ($this->rowLimit > 0 && ++$finish > $this->rowLimit) {
+                break;
+            }
+
+            if ($this->columnLimit > 0) {
+                $row = array_slice($row, 0, $this->columnLimit);
+            }
+
+            foreach ($row as &$value) {
+                if ($value != '') {
+                    if (is_numeric($value)) {
+                        $value = (float)$value;
+                    }
+
+                    // Convert encoding if necessary
+                    if ($this->inputEncoding !== 'UTF-8') {
+                        $value = mb_convert_encoding($value, 'UTF-8', $this->inputEncoding);
+                    }
+                }
+            }
+
+            unset($value);
+
+            yield $row;
+        }
+
+        ini_set('auto_detect_line_endings', $lineEnding);
+    }
+
+    /**
+     * Detect the file delimiter and encoding
+     */
+    protected function autoDetection() {
+        if (($this->delimiter !== null && $this->inputEncoding !== null)
+            || ($line = fgets($this->fileHandle)) === false) {
+
+            return;
+        }
+
+        if ($this->delimiter === null) {
+            $this->delimiter = ',';
+
+            if ((strlen(trim($line, "\r\n")) == 5) && (stripos($line, 'sep=') === 0)) {
+                $this->delimiter = substr($line, 4, 1);
+            }
+        }
+
+        if ($this->inputEncoding === null) {
+            $this->inputEncoding = 'UTF-8';
+
+            if (($bom = substr($line, 0, 4)) == "\xFF\xFE\x00\x00" || $bom == "\x00\x00\xFE\xFF") {
+                $this->start = 4;
+                $this->inputEncoding = 'UTF-32';
+            } elseif (($bom = substr($line, 0, 2)) == "\xFF\xFE" || $bom == "\xFE\xFF") {
+                $this->start = 2;
+                $this->inputEncoding = 'UTF-16';
+            } elseif (($bom = substr($line, 0, 3)) == "\xEF\xBB\xBF") {
+                $this->start = 3;
+            }
+
+            if (!$this->start) {
+                $encoding = mb_detect_encoding($line, 'ASCII, UTF-8, GB2312, GBK');
+
+                if ($encoding) {
+                    if ($encoding == 'EUC-CN') {
+                        $encoding = 'GB2312';
+                    } elseif ($encoding == 'CP936') {
+                        $encoding = 'GBK';
+                    }
+
+                    $this->inputEncoding = $encoding;
+                }
+            }
+        }
+
+        fseek($this->fileHandle, $this->start);
+    }
+
+    /**
+     * Ignore empty row
+     *
+     * @param bool $ignoreEmpty
+     *
+     * @return $this
+     */
+    public function ignoreEmptyRow($ignoreEmpty = false) {
+        $this->ignoreEmpty = $ignoreEmpty;
+
+        return $this;
+    }
+
+    /**
+     * Set input encoding
+     *
+     * @param string $encoding
+     * @return $this
+     */
+    public function setInputEncoding($encoding = 'UTF-8') {
+        $this->inputEncoding = $encoding;
+
+        return $this;
+    }
+
+    /**
+     * Get input encoding
+     *
+     * @return string
+     */
+    public function getInputEncoding() {
+        return $this->inputEncoding;
+    }
+
+    /**
+     * Set delimiter
+     *
+     * @param string $delimiter  Delimiter, defaults to ,
+     * @return $this
+     */
+    public function setDelimiter($delimiter = ',') {
+        $this->delimiter = $delimiter;
+
+        return $this;
+    }
+
+    /**
+     * Get delimiter
+     *
+     * @return string
+     */
+    public function getDelimiter() {
+        return $this->delimiter;
+    }
+
+    /**
+     * Set enclosure
+     *
+     * @param string $enclosure  Enclosure, defaults to "
+     * @return $this
+     */
+    public function setEnclosure($enclosure = '"') {
+        if ($enclosure == '') {
+            $enclosure = '"';
+        }
+
+        $this->enclosure = $enclosure;
+
+        return $this;
+    }
+
+    /**
+     * Get enclosure
+     *
+     * @return string
+     */
+    public function getEnclosure() {
+        return $this->enclosure;
+    }
+
+    /**
+     * Can the current Reader read the file?
+     *
+     * @param string $file
+     *
+     * @return bool
+     */
+    public function canRead($file) {
+        try {
+            $this->openFile($file);
+        } catch (\Exception $e) {
+            return false;
+        }
+
+        fclose($this->fileHandle);
+
+        return true;
+    }
+
+    /**
+     * Open file for reading
+     *
+     * @param string $file
+     *
+     * @throws ReaderException
+     */
+    protected function openFile($file) {
+        // Check if file exists
+        if (!file_exists($file) || !is_readable($file)) {
+            throw new ReaderException("Could not open file [$file] for reading! File does not exist.");
+        }
+
+        // Open file
+        $this->fileHandle = fopen($file, 'r');
+        if ($this->fileHandle === false) {
+            throw new ReaderException("Could not open file [$file] for reading.");
+        }
+    }
+
+    /**
+     * Close file and release generator
+     */
+    public function __destruct() {
+        if ($this->fileHandle) {
+            fclose($this->fileHandle);
+        }
+
+        $this->generator = null;
+    }
+}

+ 163 - 0
src/Reader/Xls.php

@@ -0,0 +1,163 @@
+<?php
+/**
+ * Xls Reader
+ *
+ * @author Janson
+ * @create 2017-11-23
+ */
+namespace Asan\PHPExcel\Reader;
+
+use Asan\PHPExcel\Parser\Excel5;
+use Asan\PHPExcel\Parser\Excel5\OLERead;
+
+class Xls extends BaseReader {
+    /**
+     * Xls parser
+     *
+     * @var Excel5
+     */
+    protected $parser;
+
+    /**
+     * File row、column count
+     *
+     * @var array|int
+     */
+    protected $count;
+
+    public function __construct() {
+        $this->parser = new Excel5();
+    }
+
+    /**
+     * Loads Excel from file
+     *
+     * @param string $file
+     *
+     * @return $this
+     */
+    public function load($file) {
+        $this->parser->loadOLE($file);
+
+        $this->generator = $this->makeGenerator();
+
+        return $this;
+    }
+
+    /**
+     * Count elements of the selected sheet
+     *
+     * @param bool $all
+     * @return int|array
+     */
+    public function count($all = false) {
+        if ($this->count === null) {
+            $row = $column = 0;
+            if ($sheet = $this->sheets($this->parser->getSheetIndex())) {
+                $row = $sheet['totalRows'] ?? 0;
+                $column = $sheet['totalColumns'] ?? 0;
+            }
+
+            $this->count = [
+                $this->rowLimit > 0 ? min($row, $this->rowLimit) : $row,
+                $this->columnLimit > 0 ? min($column, $this->columnLimit) : $column
+            ];
+        }
+
+        return $all ? $this->count : $this->count[0];
+    }
+
+    /**
+     * Get the work sheets info
+     *
+     * @param int $index
+     * @return array
+     */
+    public function sheets($index = null) {
+        $sheets = $this->parser->parseWorksheetInfo();
+
+        if ($index !== null) {
+            return $sheets[$index] ?? [];
+        }
+
+        return $sheets;
+    }
+
+    /**
+     * Make the generator
+     *
+     * @return \Generator
+     */
+    protected function makeGenerator() {
+        list($rowLimit, $columnLimit) = $this->count(true);
+
+        $line = $finish = 0;
+        while ($finish < $rowLimit && ($row = $this->parser->getRow($line++, $columnLimit)) !== false) {
+            if ($this->parser->isIgnoreEmptyRow() && trim(implode('', $row)) === '') {
+                continue;
+            }
+
+            $finish++;
+            yield $row;
+        }
+    }
+
+    /**
+     * Ignore empty row
+     *
+     * @param bool $ignoreEmpty
+     *
+     * @return $this
+     */
+    public function ignoreEmptyRow($ignoreEmpty = false) {
+        $this->parser->ignoreEmptyRow($ignoreEmpty);
+
+        return $this;
+    }
+
+    /**
+     * Set sheet index
+     *
+     * @param int $index
+     * @return $this
+     */
+    public function setSheetIndex($index) {
+        if ($index != $this->parser->getSheetIndex()) {
+            $this->parser->setSheetIndex($index);
+
+            $this->count = null;
+            $this->rewind();
+        }
+
+        return $this;
+    }
+
+    /**
+     * Can the current Reader read the file?
+     *
+     * @param string $file
+     *
+     * @return bool
+     */
+    public function canRead($file) {
+        try {
+            // Use ParseXL for the hard work.
+            $ole = new OLERead();
+
+            // open file
+            $ole->openFile($file);
+        } catch (\Exception $e) {
+            return false;
+        }
+
+        return true;
+    }
+
+    /**
+     * Release parser and generator
+     */
+    public function __destruct() {
+        $this->parser = null;
+        $this->generator = null;
+    }
+}

+ 161 - 0
src/Reader/Xlsx.php

@@ -0,0 +1,161 @@
+<?php
+/**
+ * Xlsx Reader
+ *
+ * @author Janson
+ * @create 2017-11-23
+ */
+namespace Asan\PHPExcel\Reader;
+
+use Asan\PHPExcel\Parser\Excel2007;
+
+class Xlsx extends BaseReader {
+    /**
+     * Xls parser
+     *
+     * @var Excel2007
+     */
+    protected $parser;
+
+    /**
+     * File row、column count
+     *
+     * @var array|int
+     */
+    protected $count;
+
+    public function __construct() {
+        $this->parser = new Excel2007();
+    }
+
+    /**
+     * Loads Excel from file
+     *
+     * @param string $file
+     *
+     * @return $this
+     */
+    public function load($file) {
+        $this->parser->loadZip($file);
+
+        $this->generator = $this->makeGenerator();
+
+        return $this;
+    }
+
+    /**
+     * Count elements of an object
+     *
+     * @param bool $all
+     * @return int|array
+     */
+    public function count($all = false) {
+        if ($this->count === null) {
+            $row = $column = 0;
+            if ($sheet = $this->sheets($this->parser->getSheetIndex())) {
+                $row = $sheet['totalRows'] ?? 0;
+                $column = $sheet['totalColumns'] ?? 0;
+            }
+
+            $this->count = [
+                $this->rowLimit > 0 ? min($row, $this->rowLimit) : $row,
+                $this->columnLimit > 0 ? min($column, $this->columnLimit) : $column
+            ];
+        }
+
+        return $all ? $this->count : $this->count[0];
+    }
+
+    /**
+     * Get the work sheets info
+     *
+     * @param int $index
+     * @return array
+     */
+    public function sheets($index = null) {
+        $sheets = $this->parser->parseWorksheetInfo();
+
+        if ($index !== null) {
+            return $sheets[$index] ?? [];
+        }
+
+        return $sheets;
+    }
+
+    /**
+     * Make the generator
+     *
+     * @return \Generator
+     */
+    protected function makeGenerator() {
+        list($rowLimit, $columnLimit) = $this->count(true);
+
+        $line = $finish = 0;
+        while ($finish < $rowLimit && ($row = $this->parser->getRow($line++, $columnLimit)) !== false) {
+            if ($this->parser->isIgnoreEmptyRow() && trim(implode('', $row)) === '') {
+                continue;
+            }
+
+            $finish++;
+            yield $row;
+        }
+    }
+
+    /**
+     * Ignore empty row
+     *
+     * @param bool $ignoreEmpty
+     *
+     * @return $this
+     */
+    public function ignoreEmptyRow($ignoreEmpty = false) {
+        $this->parser->ignoreEmptyRow($ignoreEmpty);
+
+        return $this;
+    }
+
+    /**
+     * Set sheet index
+     *
+     * @param int $index
+     * @return $this
+     */
+    public function setSheetIndex($index = 0) {
+        if ($index != $this->parser->getSheetIndex()) {
+            $this->parser->setSheetIndex($index);
+
+            $this->count = null;
+            $this->rewind();
+        }
+
+        return $this;
+    }
+
+    /**
+     * Can the current Reader read the file?
+     *
+     * @param string $file
+     *
+     * @return bool
+     */
+    public function canRead($file) {
+        try {
+            $parser = new Excel2007();
+
+            // open file
+            $parser->openFile($file);
+        } catch (\Exception $e) {
+            return false;
+        }
+
+        return true;
+    }
+
+    /**
+     * Release parser and generator
+     */
+    public function __destruct() {
+        $this->parser = null;
+        $this->generator = null;
+    }
+}


+ 35 - 0
tests/csvTest.php

@@ -0,0 +1,35 @@
+<?php
+/**
+ * Csv test
+ *
+ * @author Janson
+ * @create 2017-11-25
+ */
+require __DIR__ . '/../autoload.php';
+
+$start = microtime(true);
+$memory = memory_get_usage();
+
+$reader = Asan\PHPExcel\Excel::load('files/02.csv', function(Asan\PHPExcel\Reader\Csv $reader) {
+    $reader->setRowLimit(5);
+    $reader->setColumnLimit(10);
+
+    $reader->ignoreEmptyRow(true);
+
+    //$reader->setInputEncoding('UTF-8');
+    $reader->setDelimiter("\t");
+});
+
+foreach ($reader as $row) {
+    var_dump($row);
+}
+
+$reader->seek(2);
+
+$count = $reader->count();
+//$reader->seek(1);
+$current = $reader->current();
+
+$time = microtime(true) - $start;
+$use = memory_get_usage() - $memory;
+var_dump($current, $count, $time, $use/1024/1024);

+ 2 - 0
tests/files/01.csv

@@ -0,0 +1,2 @@
+姓名,称呼,性别,QQ,手机,电话,邮箱,传真,公司,职务,网址,地址,备注
+巴蒂,黑曼巴,男,654333,18643910100,0755-07551255,5310100@sina.com,7.55576E+11,xxxx有限公司,测试工程师,www.baidu1.com,美国洛杉矶,NBA球员

BIN
tests/files/01.xls


BIN
tests/files/01.xlsx


+ 10 - 0
tests/files/02.csv

@@ -0,0 +1,10 @@
+10																	
+	21	22															
+											311						
+																	
+							407										
+																	
+																	
+																	
+																	
+				104													

+ 35 - 0
tests/xlsTest.php

@@ -0,0 +1,35 @@
+<?php
+/**
+ * Xls Test
+ *
+ * @author Janson
+ * @create 2017-11-28
+ */
+require __DIR__ . '/../autoload.php';
+
+$start = microtime(true);
+$memory = memory_get_usage();
+
+$reader = Asan\PHPExcel\Excel::load('files/01.xls', function(Asan\PHPExcel\Reader\Xls $reader) {
+    //$reader->setRowLimit(5);
+    $reader->setColumnLimit(10);
+
+    //$reader->setSheetIndex(1);
+});
+
+foreach ($reader as $row) {
+    var_dump($row);
+}
+
+$reader->seek(50);
+
+//$reader->seek(5);
+$count = $reader->count();
+$current = $reader->current();
+
+$sheets = $reader->sheets();
+
+$time = microtime(true) - $start;
+$use = memory_get_usage() - $memory;
+
+var_dump($current, $count, $sheets, $time, $use/1024/1024);

+ 37 - 0
tests/xlsxTest.php

@@ -0,0 +1,37 @@
+<?php
+/**
+ * xlsx Test
+ *
+ * @author Janson
+ * @create 2017-11-28
+ */
+require __DIR__ . '/../autoload.php';
+
+$start = microtime(true);
+$memory = memory_get_usage();
+
+$reader = Asan\PHPExcel\Excel::load('files/01.xlsx', function(Asan\PHPExcel\Reader\Xlsx $reader) {
+    $reader->setRowLimit(10);
+    $reader->setColumnLimit(10);
+
+    $reader->ignoreEmptyRow(true);
+
+    //$reader->setSheetIndex(0);
+});
+
+foreach ($reader as $row) {
+    var_dump($row);
+}
+
+//$reader->seek(50);
+
+$count = $reader->count();
+$reader->seek(2);
+$current = $reader->current();
+
+$sheets = $reader->sheets();
+
+$time = microtime(true) - $start;
+$use = memory_get_usage() - $memory;
+
+var_dump($current, $count, $sheets, $time, $use/1024/1024);