Excel5.php 51 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224122512261227122812291230123112321233123412351236123712381239124012411242124312441245124612471248124912501251125212531254125512561257125812591260126112621263126412651266126712681269127012711272127312741275127612771278127912801281128212831284128512861287128812891290129112921293129412951296129712981299130013011302130313041305130613071308130913101311131213131314131513161317131813191320132113221323132413251326132713281329133013311332133313341335133613371338133913401341134213431344134513461347134813491350135113521353135413551356135713581359136013611362136313641365136613671368136913701371137213731374137513761377137813791380138113821383138413851386138713881389139013911392139313941395139613971398139914001401140214031404140514061407140814091410141114121413141414151416141714181419142014211422142314241425142614271428142914301431143214331434143514361437143814391440144114421443144414451446144714481449145014511452145314541455145614571458145914601461146214631464146514661467146814691470147114721473147414751476147714781479148014811482148314841485148614871488148914901491149214931494149514961497149814991500150115021503150415051506150715081509151015111512151315141515151615171518151915201521152215231524152515261527152815291530153115321533153415351536153715381539154015411542154315441545154615471548154915501551155215531554155515561557155815591560156115621563156415651566156715681569157015711572157315741575157615771578157915801581158215831584158515861587158815891590159115921593159415951596159715981599160016011602160316041605160616071608160916101611161216131614161516161617161816191620162116221623162416251626162716281629163016311632163316341635163616371638163916401641164216431644
  1. <?php
  2. class PHPExcel_Reader_Excel5 {
  3. // ParseXL definitions
  4. const XLS_BIFF8 = 0x0600;
  5. const XLS_BIFF7 = 0x0500;
  6. const XLS_WorkbookGlobals = 0x0005;
  7. const XLS_Worksheet = 0x0010;
  8. // record identifiers
  9. const XLS_Type_FORMULA = 0x0006;
  10. const XLS_Type_FORMULA2 = 0x0406;
  11. const XLS_Type_EOF = 0x000a;
  12. const XLS_Type_PROTECT = 0x0012;
  13. const XLS_Type_OBJECTPROTECT = 0x0063;
  14. const XLS_Type_SCENPROTECT = 0x00dd;
  15. const XLS_Type_PASSWORD = 0x0013;
  16. const XLS_Type_HEADER = 0x0014;
  17. const XLS_Type_FOOTER = 0x0015;
  18. const XLS_Type_EXTERNSHEET = 0x0017;
  19. const XLS_Type_DEFINEDNAME = 0x0018;
  20. const XLS_Type_VERTICALPAGEBREAKS = 0x001a;
  21. const XLS_Type_HORIZONTALPAGEBREAKS = 0x001b;
  22. const XLS_Type_NOTE = 0x001c;
  23. const XLS_Type_SELECTION = 0x001d;
  24. const XLS_Type_DATEMODE = 0x0022;
  25. const XLS_Type_EXTERNNAME = 0x0023;
  26. const XLS_Type_LEFTMARGIN = 0x0026;
  27. const XLS_Type_RIGHTMARGIN = 0x0027;
  28. const XLS_Type_TOPMARGIN = 0x0028;
  29. const XLS_Type_BOTTOMMARGIN = 0x0029;
  30. const XLS_Type_PRINTGRIDLINES = 0x002b;
  31. const XLS_Type_FILEPASS = 0x002f;
  32. const XLS_Type_FONT = 0x0031;
  33. const XLS_Type_CONTINUE = 0x003c;
  34. const XLS_Type_PANE = 0x0041;
  35. const XLS_Type_CODEPAGE = 0x0042;
  36. const XLS_Type_DEFCOLWIDTH = 0x0055;
  37. const XLS_Type_OBJ = 0x005d;
  38. const XLS_Type_COLINFO = 0x007d;
  39. const XLS_Type_IMDATA = 0x007f;
  40. const XLS_Type_SHEETPR = 0x0081;
  41. const XLS_Type_HCENTER = 0x0083;
  42. const XLS_Type_VCENTER = 0x0084;
  43. const XLS_Type_SHEET = 0x0085;
  44. const XLS_Type_PALETTE = 0x0092;
  45. const XLS_Type_SCL = 0x00a0;
  46. const XLS_Type_PAGESETUP = 0x00a1;
  47. const XLS_Type_MULRK = 0x00bd;
  48. const XLS_Type_MULBLANK = 0x00be;
  49. const XLS_Type_DBCELL = 0x00d7;
  50. const XLS_Type_XF = 0x00e0;
  51. const XLS_Type_MERGEDCELLS = 0x00e5;
  52. const XLS_Type_MSODRAWINGGROUP = 0x00eb;
  53. const XLS_Type_MSODRAWING = 0x00ec;
  54. const XLS_Type_SST = 0x00fc;
  55. const XLS_Type_LABELSST = 0x00fd;
  56. const XLS_Type_EXTSST = 0x00ff;
  57. const XLS_Type_EXTERNALBOOK = 0x01ae;
  58. const XLS_Type_DATAVALIDATIONS = 0x01b2;
  59. const XLS_Type_TXO = 0x01b6;
  60. const XLS_Type_HYPERLINK = 0x01b8;
  61. const XLS_Type_DATAVALIDATION = 0x01be;
  62. const XLS_Type_DIMENSION = 0x0200;
  63. const XLS_Type_BLANK = 0x0201;
  64. const XLS_Type_NUMBER = 0x0203;
  65. const XLS_Type_LABEL = 0x0204;
  66. const XLS_Type_BOOLERR = 0x0205;
  67. const XLS_Type_STRING = 0x0207;
  68. const XLS_Type_ROW = 0x0208;
  69. const XLS_Type_INDEX = 0x020b;
  70. const XLS_Type_ARRAY = 0x0221;
  71. const XLS_Type_DEFAULTROWHEIGHT = 0x0225;
  72. const XLS_Type_WINDOW2 = 0x023e;
  73. const XLS_Type_RK = 0x007e;
  74. const XLS_Type_RK2 = 0x027e;
  75. const XLS_Type_STYLE = 0x0293;
  76. const XLS_Type_FORMAT = 0x041e;
  77. const XLS_Type_SHAREDFMLA = 0x04bc;
  78. const XLS_Type_BOF = 0x0809;
  79. const XLS_Type_SHEETPROTECTION = 0x0867;
  80. const XLS_Type_RANGEPROTECTION = 0x0868;
  81. const XLS_Type_SHEETLAYOUT = 0x0862;
  82. const XLS_Type_XFEXT = 0x087d;
  83. const XLS_Type_PAGELAYOUTVIEW = 0x088b;
  84. const XLS_Type_UNKNOWN = 0xffff;
  85. // Encryption type
  86. const MS_BIFF_CRYPTO_NONE = 0;
  87. const MS_BIFF_CRYPTO_XOR = 1;
  88. const MS_BIFF_CRYPTO_RC4 = 2;
  89. // Size of stream blocks when using RC4 encryption
  90. const REKEY_BLOCK = 0x400;
  91. private $_pos;
  92. private $_data;
  93. private $_cell;
  94. private $_sst;
  95. private $_sheets;
  96. private $_dataSize;
  97. private static $_codepage = 'CP1252';
  98. private $index = 0;
  99. private $curretSheet = 0;
  100. private $builtInFormats = array(
  101. 0 => 'General',
  102. 1 => '0',
  103. 2 => '0.00',
  104. 3 => '#,##0',
  105. 4 => '#,##0.00',
  106. 9 => '0%',
  107. 10 => '0.00%',
  108. 11 => '0.00E+00',
  109. 12 => '# ?/?',
  110. 13 => '# ??/??',
  111. 14 => 'yyyy/m/d',
  112. 15 => 'd-mmm-yy',
  113. 16 => 'd-mmm',
  114. 17 => 'mmm-yy',
  115. 18 => 'h:mm AM/PM',
  116. 19 => 'h:mm:ss AM/PM',
  117. 20 => 'h:mm',
  118. 21 => 'h:mm:ss',
  119. 22 => 'yyyy/m/d h:mm',
  120. 28 => 'm月d日',
  121. 31 => 'yyyy年m月d日',
  122. 32 => 'h时mmi分',
  123. 33 => 'h时mmi分ss秒',
  124. 34 => 'AM/PM h时mmi分',
  125. 35 => 'AM/PM h时mmi分ss秒',
  126. 37 => '#,##0 ;(#,##0)',
  127. 38 => '#,##0 ;[Red](#,##0)',
  128. 39 => '#,##0.00;(#,##0.00)',
  129. 40 => '#,##0.00;[Red](#,##0.00)',
  130. 44 => '_("$"* #,##0.00_);_("$"* \(#,##0.00\);_("$"* "-"??_);_(@_)',
  131. 45 => 'mm:ss',
  132. 46 => '[h]:mm:ss',
  133. 47 => 'mm:ss.0',
  134. 48 => '##0.0E+0',
  135. 49 => '@',
  136. // CHT
  137. 27 => 'yyyy年m月',
  138. 30 => 'm/d/yy',
  139. 36 => '[$-404]e/m/d',
  140. 50 => '[$-404]e/m/d',
  141. 57 => '[$-404]e/m/d',
  142. // THA
  143. 59 => 't0',
  144. 60 => 't0.00',
  145. 61 => 't#,##0',
  146. 62 => 't#,##0.00',
  147. 67 => 't0%',
  148. 68 => 't0.00%',
  149. 69 => 't# ?/?',
  150. 70 => 't# ??/??'
  151. );
  152. public $error = false;
  153. /**
  154. * Create a new Spreadsheet_Excel_Reader instance
  155. */
  156. public function __construct($file) {
  157. if ( ! file_exists($file)) {
  158. throw new Exception("Could not open " . $file . " for reading! File does not exist.");
  159. }
  160. try {
  161. $ole = new PHPExcel_Reader_OLERead();
  162. $ole->read($file);
  163. $this->_data = $ole->getStream($ole->wrkbook);
  164. return true;
  165. } catch (Exception $e) {
  166. return false;
  167. }
  168. }
  169. /**
  170. * Changes sheet to another.
  171. * @param bool
  172. */
  173. public function ChangeSheet($index){
  174. $this->curretSheet = $index;
  175. return true;
  176. }
  177. /**
  178. * 获取Cell数据
  179. */
  180. public function getCell(){
  181. $this->_cell = array();
  182. $this->_endRow = false;
  183. $this->_key = null;
  184. if( ! $this->_parse){
  185. $this->_parse = true;
  186. $this->_pos = 0;
  187. // Parse Workbook Global Substream
  188. while ($this->_pos < $this->_dataSize) {
  189. $code = self::_GetInt2d($this->_data, $this->_pos);
  190. switch ($code) {
  191. case self::XLS_Type_SST: $this->_readSst(); break;
  192. case self::XLS_Type_CODEPAGE: $this->_readCodepage(); break;
  193. case self::XLS_Type_DATEMODE: $this->_readDateMode(); break;
  194. case self::XLS_Type_FORMAT: $this->_readFormat(); break;
  195. case self::XLS_Type_XF: $this->_readXf(); break;
  196. case self::XLS_Type_EOF: $this->_readDefault(); break 2;
  197. default: $this->_readDefault(); break;
  198. }
  199. }
  200. }
  201. // Parse the individual sheet
  202. $this->_pos = $this->_lastPos ? $this->_lastPos : $this->_sheets[$this->curretSheet]['offset'];
  203. while ($this->_pos <= $this->_dataSize - 4) {
  204. if($this->_endRow) break;
  205. $code = self::_GetInt2d($this->_data, $this->_pos);
  206. switch ($code) {
  207. //case self::XLS_Type_RK:
  208. case self::XLS_Type_RK2: $this->_readRk(); break;
  209. case self::XLS_Type_LABELSST: $this->_readLabelSst(); break;
  210. case self::XLS_Type_MULRK: $this->_readMulRk(); break;
  211. case self::XLS_Type_NUMBER: $this->_readNumber(); break;
  212. case self::XLS_Type_FORMULA:
  213. case self::XLS_Type_FORMULA2: $this->_readFormula(); break;
  214. case self::XLS_Type_BOOLERR: $this->_readBoolErr(); break;
  215. case self::XLS_Type_STRING: $this->_readString(); break;
  216. case self::XLS_Type_MULBLANK: $this->_readBlank(); break;
  217. case self::XLS_Type_LABEL: $this->_readLabel(); break;
  218. case self::XLS_Type_EOF: $this->_readDefault(); break 2;
  219. default: $this->_readDefault(); break;
  220. }
  221. }
  222. return $this->_cell;
  223. }
  224. /**
  225. * Return worksheet info (Name, Last Column Letter, Last Column Index, Total Rows, Total Columns)
  226. */
  227. public function getWorksheetInfo() {
  228. if( ! $this->_sheets){
  229. $this->_dataSize = strlen($this->_data); // total byte size of Excel data (workbook global substream + sheet substreams)
  230. $this->_pos = 0;
  231. $this->_sheets = array();
  232. // Parse Workbook Global Substream
  233. while ($this->_pos < $this->_dataSize) {
  234. $code = self::_GetInt2d($this->_data, $this->_pos);
  235. switch ($code) {
  236. case self::XLS_Type_BOF: $this->_readBof(); break;
  237. case self::XLS_Type_SHEET: $this->_readSheet(); break;
  238. case self::XLS_Type_EOF: $this->_readDefault(); break 2;
  239. default: $this->_readDefault(); break;
  240. }
  241. }
  242. }
  243. if( ! isset($this->_sheets[$this->curretSheet])){
  244. return array();
  245. }
  246. $sheetInfo = array(
  247. 'worksheetName' => $this->_sheets[$this->curretSheet]['name'],
  248. 'lastColumnLetter' => 'A',
  249. 'lastColumnIndex' => 0,
  250. 'totalRows' => 0,
  251. 'totalColumns' => 0
  252. );
  253. // Parse the individual sheet
  254. $this->_pos = $this->_sheets[$this->curretSheet]['offset'];
  255. while ($this->_pos <= $this->_dataSize - 4) {
  256. $code = self::_GetInt2d($this->_data, $this->_pos);
  257. switch ($code) {
  258. case self::XLS_Type_RK2:
  259. case self::XLS_Type_LABELSST:
  260. case self::XLS_Type_NUMBER:
  261. case self::XLS_Type_FORMULA:
  262. case self::XLS_Type_BOOLERR:
  263. case self::XLS_Type_LABEL:
  264. $length = self::_GetInt2d($this->_data, $this->_pos + 2);
  265. $recordData = substr($this->_data, $this->_pos + 4, $length);
  266. // move stream pointer to next record
  267. $this->_pos += 4 + $length;
  268. $rowIndex = self::_GetInt2d($recordData, 0) + 1;
  269. $columnIndex = self::_GetInt2d($recordData, 2);
  270. $sheetInfo['totalRows'] = max($sheetInfo['totalRows'], $rowIndex);
  271. $sheetInfo['lastColumnIndex'] = max($sheetInfo['lastColumnIndex'], $columnIndex);
  272. break;
  273. case self::XLS_Type_BOF: $this->_readBof(); break;
  274. case self::XLS_Type_EOF: $this->_readDefault(); break 2;
  275. default: $this->_readDefault(); break;
  276. }
  277. $sheetInfo['totalColumns'] = $sheetInfo['lastColumnIndex'] + 1;
  278. $sheetInfo['lastColumnLetter'] = self::_stringFromColumnIndex($sheetInfo['lastColumnIndex']);
  279. }
  280. return $sheetInfo;
  281. }
  282. private function _addCell($row, $column, $value, $xfIndex){
  283. if(is_null($this->_key)){
  284. $this->_key = $row;
  285. }
  286. if($row > $this->_key){
  287. $this->_endRow = true;
  288. return false;
  289. }
  290. $xfRecord = $this->xfRecords[$xfIndex];
  291. $this->_lastPos = $this->_pos;
  292. $this->_cell[$column] = $this->_format_value($value, $xfRecord['format']);
  293. }
  294. private function _format_value($value = '0', $format = 'General'){
  295. if ( ! is_numeric($value) || $format == 'General' || $format == '@') return $value;
  296. $sections = explode(';', $format);
  297. switch (count($sections)) {
  298. case 1:
  299. $format = $sections[0];
  300. break;
  301. case 2:
  302. $format = ($value >= 0) ? $sections[0] : $sections[1];
  303. $value = abs($value); // Use the absolute value
  304. break;
  305. case 3:
  306. $format = ($value > 0) ?
  307. $sections[0] : ( ($value < 0) ?
  308. $sections[1] : $sections[2]);
  309. $value = abs($value); // Use the absolute value
  310. break;
  311. case 4:
  312. $format = ($value > 0) ?
  313. $sections[0] : ( ($value < 0) ?
  314. $sections[1] : $sections[2]);
  315. $value = abs($value); // Use the absolute value
  316. break;
  317. default:
  318. // something is wrong, just use first section
  319. $format = $sections[0];
  320. break;
  321. }
  322. $color_regex = '/^\\[[a-zA-Z]+\\]/';
  323. $format = preg_replace($color_regex, '', $format);
  324. if (preg_match('/(\[\$[A-Z]*-[0-9A-F]*\])*[hmsdy]/i', $format)) { // datetime format
  325. $value = $this->_formatAsDate($value, $format);
  326. } else if (preg_match('/%$/', $format)) { // % number format
  327. $value = self::_formatAsPercentage($value, $format);
  328. } else {
  329. if ($format === '[$EUR ]#,##0.00_-') {
  330. $value = 'EUR ' . sprintf('%1.2f', $value);
  331. } else {
  332. // In Excel formats, "_" is used to add spacing, which we can't do in HTML
  333. $format = preg_replace('/_./', '', $format);
  334. // Some non-number characters are escaped with \, which we don't need
  335. $format = preg_replace("/\\\\/", '', $format);
  336. // Some non-number strings are quoted, so we'll get rid of the quotes, likewise any positional * symbols
  337. $format = str_replace(array('"','*'), '', $format);
  338. // Find out if we need thousands separator
  339. // This is indicated by a comma enclosed by a digit placeholder:
  340. // #,# or 0,0
  341. $useThousands = preg_match('/(#,#|0,0)/', $format);
  342. if ($useThousands) {
  343. $format = preg_replace('/0,0/', '00', $format);
  344. $format = preg_replace('/#,#/', '##', $format);
  345. }
  346. // Scale thousands, millions,...
  347. // This is indicated by a number of commas after a digit placeholder:
  348. // #, or 0.0,,
  349. $scale = 1; // same as no scale
  350. $matches = array();
  351. if (preg_match('/(#|0)(,+)/', $format, $matches)) {
  352. $scale = pow(1000, strlen($matches[2]));
  353. // strip the commas
  354. $format = preg_replace('/0,+/', '0', $format);
  355. $format = preg_replace('/#,+/', '#', $format);
  356. }
  357. if (preg_match('/#?.*\?\/\?/', $format, $m)) {
  358. //echo 'Format mask is fractional '.$format.' <br />';
  359. if ($value != (int)$value) {
  360. self::_formatAsFraction($value, $format);
  361. }
  362. } else {
  363. // Handle the number itself
  364. // scale number
  365. $value = $value / $scale;
  366. // Strip #
  367. $format = preg_replace('/\\#/', '0', $format);
  368. $n = "/\[[^\]]+\]/";
  369. $m = preg_replace($n, '', $format);
  370. $number_regex = "/(0+)(\.?)(0*)/";
  371. if (preg_match($number_regex, $m, $matches)) {
  372. $left = $matches[1];
  373. $dec = $matches[2];
  374. $right = $matches[3];
  375. // minimun width of formatted number (including dot)
  376. $minWidth = strlen($left) + strlen($dec) + strlen($right);
  377. if ($useThousands) {
  378. $value = number_format(
  379. $value
  380. , strlen($right)
  381. , '.'
  382. , ','
  383. );
  384. $value = preg_replace($number_regex, $value, $format);
  385. } else {
  386. if (preg_match('/[0#]E[+-]0/i', $format)) {
  387. // Scientific format
  388. $value = sprintf('%5.2E', $value);
  389. } elseif (preg_match('/0([^\d\.]+)0/', $format)) {
  390. $value = self::_complexNumberFormatMask($value, $format);
  391. } else {
  392. $sprintf_pattern = "%0$minWidth." . strlen($right) . "f";
  393. $value = sprintf($sprintf_pattern, $value);
  394. $value = preg_replace($number_regex, $value, $format);
  395. }
  396. }
  397. }
  398. }
  399. if (preg_match('/\[\$(.*)\]/u', $format, $m)) {
  400. // Currency or Accounting
  401. $currencyFormat = $m[0];
  402. $currencyCode = $m[1];
  403. list($currencyCode) = explode('-',$currencyCode);
  404. if ($currencyCode == '') {
  405. $currencyCode = '$';
  406. }
  407. $value = preg_replace('/\[\$([^\]]*)\]/u',$currencyCode,$value);
  408. }
  409. }
  410. }
  411. return $value;
  412. }
  413. private static function _complexNumberFormatMask($number, $mask) {
  414. if (strpos($mask,'.') !== false) {
  415. $numbers = explode('.', $number . '.0');
  416. $masks = explode('.', $mask . '.0');
  417. $result1 = self::_complexNumberFormatMask($numbers[0], $masks[0]);
  418. $result2 = strrev(self::_complexNumberFormatMask(strrev($numbers[1]), strrev($masks[1])));
  419. return $result1 . '.' . $result2;
  420. }
  421. $r = preg_match_all('/0+/', $mask, $result, PREG_OFFSET_CAPTURE);
  422. if ($r > 1) {
  423. $result = array_reverse($result[0]);
  424. foreach($result as $block) {
  425. $divisor = 1 . $block[0];
  426. $size = strlen($block[0]);
  427. $offset = $block[1];
  428. $blockValue = sprintf(
  429. '%0' . $size . 'd',
  430. fmod($number, $divisor)
  431. );
  432. $number = floor($number / $divisor);
  433. $mask = substr_replace($mask,$blockValue, $offset, $size);
  434. }
  435. if ($number > 0) {
  436. $mask = substr_replace($mask, $number, $offset, 0);
  437. }
  438. $result = $mask;
  439. } else {
  440. $result = $number;
  441. }
  442. return $result;
  443. }
  444. private static function _formatAsFraction(&$value, &$format)
  445. {
  446. $sign = ($value < 0) ? '-' : '';
  447. $integerPart = floor(abs($value));
  448. $decimalPart = trim(fmod(abs($value),1),'0.');
  449. $decimalLength = strlen($decimalPart);
  450. $decimalDivisor = pow(10,$decimalLength);
  451. $GCD = self::GCD(array($decimalPart, $decimalDivisor));
  452. $adjustedDecimalPart = $decimalPart/$GCD;
  453. $adjustedDecimalDivisor = $decimalDivisor/$GCD;
  454. if ((strpos($format,'0') !== false) || (strpos($format,'#') !== false) || (substr($format,0,3) == '? ?')) {
  455. if ($integerPart == 0) {
  456. $integerPart = '';
  457. }
  458. $value = "$sign$integerPart $adjustedDecimalPart/$adjustedDecimalDivisor";
  459. } else {
  460. $adjustedDecimalPart += $integerPart * $adjustedDecimalDivisor;
  461. $value = "$sign$adjustedDecimalPart/$adjustedDecimalDivisor";
  462. }
  463. }
  464. private static function GCD($params) {
  465. $returnValue = 1;
  466. $allValuesFactors = array();
  467. $flattenArr = self::flattenArray($params);
  468. foreach($flattenArr as $value) {
  469. if (!is_numeric($value)) {
  470. return '#VALUE!';
  471. } elseif ($value == 0) {
  472. continue;
  473. } elseif($value < 0) {
  474. return '#NULL!';
  475. }
  476. $myFactors = self::_factors($value);
  477. $myCountedFactors = array_count_values($myFactors);
  478. $allValuesFactors[] = $myCountedFactors;
  479. }
  480. $allValuesCount = count($allValuesFactors);
  481. if ($allValuesCount == 0) {
  482. return 0;
  483. }
  484. $mergedArray = $allValuesFactors[0];
  485. for ($i=1;$i < $allValuesCount; ++$i) {
  486. $mergedArray = array_intersect_key($mergedArray,$allValuesFactors[$i]);
  487. }
  488. $mergedArrayValues = count($mergedArray);
  489. if ($mergedArrayValues == 0) {
  490. return $returnValue;
  491. } elseif ($mergedArrayValues > 1) {
  492. foreach($mergedArray as $mergedKey => $mergedValue) {
  493. foreach($allValuesFactors as $highestPowerTest) {
  494. foreach($highestPowerTest as $testKey => $testValue) {
  495. if (($testKey == $mergedKey) && ($testValue < $mergedValue)) {
  496. $mergedArray[$mergedKey] = $testValue;
  497. $mergedValue = $testValue;
  498. }
  499. }
  500. }
  501. }
  502. $returnValue = 1;
  503. foreach($mergedArray as $key => $value) {
  504. $returnValue *= pow($key,$value);
  505. }
  506. return $returnValue;
  507. } else {
  508. $keys = array_keys($mergedArray);
  509. $key = $keys[0];
  510. $value = $mergedArray[$key];
  511. foreach($allValuesFactors as $testValue) {
  512. foreach($testValue as $mergedKey => $mergedValue) {
  513. if (($mergedKey == $key) && ($mergedValue < $value)) {
  514. $value = $mergedValue;
  515. }
  516. }
  517. }
  518. return pow($key,$value);
  519. }
  520. }
  521. //
  522. // Private method to return an array of the factors of the input value
  523. //
  524. private static function _factors($value) {
  525. $startVal = floor(sqrt($value));
  526. $factorArray = array();
  527. for ($i = $startVal; $i > 1; --$i) {
  528. if (($value % $i) == 0) {
  529. $factorArray = array_merge($factorArray,self::_factors($value / $i));
  530. $factorArray = array_merge($factorArray,self::_factors($i));
  531. if ($i <= sqrt($value)) {
  532. break;
  533. }
  534. }
  535. }
  536. if (!empty($factorArray)) {
  537. rsort($factorArray);
  538. return $factorArray;
  539. } else {
  540. return array((integer) $value);
  541. }
  542. }
  543. private static function flattenArray($array) {
  544. if (!is_array($array)) {
  545. return (array) $array;
  546. }
  547. $arrayValues = array();
  548. foreach ($array as $value) {
  549. if (is_array($value)) {
  550. foreach ($value as $val) {
  551. if (is_array($val)) {
  552. foreach ($val as $v) {
  553. $arrayValues[] = $v;
  554. }
  555. } else {
  556. $arrayValues[] = $val;
  557. }
  558. }
  559. } else {
  560. $arrayValues[] = $value;
  561. }
  562. }
  563. return $arrayValues;
  564. }
  565. private static function _formatAsPercentage(&$value, &$format)
  566. {
  567. if ($format === '0%') {
  568. $value = round( (100 * $value), 0) . '%';
  569. } else {
  570. if (preg_match('/\.[#0]+/i', $format, $m)) {
  571. $s = substr($m[0], 0, 1) . (strlen($m[0]) - 1);
  572. $format = str_replace($m[0], $s, $format);
  573. }
  574. if (preg_match('/^[#0]+/', $format, $m)) {
  575. $format = str_replace($m[0], strlen($m[0]), $format);
  576. }
  577. $format = '%' . str_replace('%', 'f%%', $format);
  578. $value = sprintf($format, 100 * $value);
  579. }
  580. return $value;
  581. }
  582. /**
  583. * Search/replace values to convert Excel date/time format masks to PHP format masks
  584. *
  585. * @var array
  586. */
  587. private static $_dateFormatReplacements = array(
  588. // first remove escapes related to non-format characters
  589. '\\' => '',
  590. // 12-hour suffix
  591. 'am/pm' => 'A',
  592. // 4-digit year
  593. 'e' => 'Y',
  594. 'yyyy' => 'Y',
  595. // 2-digit year
  596. 'yy' => 'y',
  597. // first letter of month - no php equivalent
  598. 'mmmmm' => 'M',
  599. // full month name
  600. 'mmmm' => 'F',
  601. // short month name
  602. 'mmm' => 'M',
  603. // mm is minutes if time, but can also be month w/leading zero
  604. // so we try to identify times be the inclusion of a : separator in the mask
  605. // It isn't perfect, but the best way I know how
  606. ':mm' => ':i',
  607. 'mm:' => 'i:',
  608. 'mmi' => 'i',
  609. // month leading zero
  610. 'mm' => 'm',
  611. // month no leading zero
  612. 'm' => 'n',
  613. // full day of week name
  614. 'dddd' => 'l',
  615. // short day of week name
  616. 'ddd' => 'D',
  617. // days leading zero
  618. 'dd' => 'd',
  619. // days no leading zero
  620. 'd' => 'j',
  621. // seconds
  622. 'ss' => 's',
  623. // fractional seconds - no php equivalent
  624. '.s' => ''
  625. );
  626. /**
  627. * Search/replace values to convert Excel date/time format masks hours to PHP format masks (24 hr clock)
  628. *
  629. * @var array
  630. */
  631. private static $_dateFormatReplacements24 = array(
  632. 'hh' => 'H',
  633. 'h' => 'G'
  634. );
  635. /**
  636. * Search/replace values to convert Excel date/time format masks hours to PHP format masks (12 hr clock)
  637. *
  638. * @var array
  639. */
  640. private static $_dateFormatReplacements12 = array(
  641. 'hh' => 'h',
  642. 'h' => 'g'
  643. );
  644. private function _formatAsDate(&$value, &$format)
  645. {
  646. // dvc: convert Excel formats to PHP date formats
  647. // strip off first part containing e.g. [$-F800] or [$USD-409]
  648. // general syntax: [$<Currency string>-<language info>]
  649. // language info is in hexadecimal
  650. $format = preg_replace('/^(\[\$[A-Z]*-[0-9A-F]*\])/i', '', $format);
  651. // OpenOffice.org uses upper-case number formats, e.g. 'YYYY', convert to lower-case
  652. $format = strtolower($format);
  653. $format = strtr($format,self::$_dateFormatReplacements);
  654. if (strpos($format,'A') === false) { // 24-hour time format
  655. $format = strtr($format,self::$_dateFormatReplacements24);
  656. } else { // 12-hour time format
  657. $format = strtr($format,self::$_dateFormatReplacements12);
  658. }
  659. $dateTime = $this->ExcelToPHP($value);
  660. $days = floor($dateTime / 86400);
  661. $time = round((($dateTime / 86400) - $days) * 86400);
  662. $hours = round($time / 3600);
  663. $minutes = round($time / 60) - ($hours * 60);
  664. $seconds = round($time) - ($hours * 3600) - ($minutes * 60);
  665. $dateObj = date_create('1-Jan-1970+'.$days.' days');
  666. $dateObj->setTime($hours,$minutes,$seconds);
  667. return $dateObj->format($format);
  668. }
  669. private function ExcelToPHP($dateValue = 0) {
  670. if ($this->_excelBaseDate == 1900) {
  671. $my_excelBaseDate = 25569;
  672. // Adjust for the spurious 29-Feb-1900 (Day 60)
  673. if ($dateValue < 60) {
  674. --$my_excelBaseDate;
  675. }
  676. } else {
  677. $my_excelBaseDate = 24107;
  678. }
  679. // Perform conversion
  680. if ($dateValue >= 1) {
  681. $utcDays = $dateValue - $my_excelBaseDate;
  682. $returnValue = round($utcDays * 86400);
  683. if (($returnValue <= PHP_INT_MAX) && ($returnValue >= -PHP_INT_MAX)) {
  684. $returnValue = (integer) $returnValue;
  685. }
  686. } else {
  687. $hours = round($dateValue * 24);
  688. $mins = round($dateValue * 1440) - round($hours * 60);
  689. $secs = round($dateValue * 86400) - round($hours * 3600) - round($mins * 60);
  690. $returnValue = (integer) gmmktime($hours, $mins, $secs);
  691. }
  692. // Return
  693. return $returnValue;
  694. }
  695. /**
  696. * Read BOF
  697. */
  698. private function _readBof() {
  699. $length = self::_GetInt2d($this->_data, $this->_pos + 2);
  700. $recordData = substr($this->_data, $this->_pos + 4, $length);
  701. $this->_pos += 4 + $length; // move stream pointer to next record
  702. $substreamType = self::_GetInt2d($recordData, 2); // offset: 2; size: 2; type of the following data
  703. switch ($substreamType) {
  704. case self::XLS_WorkbookGlobals:
  705. $version = self::_GetInt2d($recordData, 0);
  706. if (($version != self::XLS_BIFF8) && ($version != self::XLS_BIFF7)) {
  707. die('Cannot read this Excel file. Version is too old.');
  708. }
  709. $this->_version = $version;
  710. break;
  711. case self::XLS_Worksheet:
  712. // do not use this version information for anything
  713. // it is unreliable (OpenOffice doc, 5.8), use only version information from the global stream
  714. break;
  715. default:
  716. // substream, e.g. chart. just skip the entire substream
  717. do {
  718. $code = self::_GetInt2d($this->_data, $this->_pos);
  719. $this->_readDefault();
  720. } while ($code != self::XLS_Type_EOF && $this->_pos < $this->_dataSize);
  721. break;
  722. }
  723. }
  724. /**
  725. * Read Sheet
  726. */
  727. private function _readSheet() {
  728. $length = self::_GetInt2d($this->_data, $this->_pos + 2);
  729. $recordData = substr($this->_data, $this->_pos + 4, $length);
  730. $rec_offset = self::_GetInt4d($this->_data, $this->_pos + 4); // offset: 0; size: 4; absolute stream position of the BOF record of the sheet
  731. $this->_pos += 4 + $length; // move stream pointer to next record
  732. // offset: 6; size: var; sheet name
  733. if ($this->_version == self::XLS_BIFF8) {
  734. $string = self::_readUnicodeStringShort(substr($recordData, 6));
  735. $rec_name = $string['value'];
  736. } elseif ($this->_version == self::XLS_BIFF7) {
  737. $string = self::_readByteStringShort(substr($recordData, 6));
  738. $rec_name = $string['value'];
  739. }
  740. $this->_sheets[] = array(
  741. 'name' => $rec_name,
  742. 'offset' => $rec_offset
  743. );
  744. }
  745. /**
  746. * Reads a general type of BIFF record. Does nothing except for moving stream pointer forward to next record.
  747. */
  748. private function _readDefault() {
  749. $length = self::_GetInt2d($this->_data, $this->_pos + 2);
  750. $this->_pos += 4 + $length; // move stream pointer to next record
  751. }
  752. /**
  753. * CODEPAGE
  754. *
  755. * This record stores the text encoding used to write byte
  756. * strings, stored as MS Windows code page identifier.
  757. */
  758. private function _readCodepage() {
  759. $length = self::_GetInt2d($this->_data, $this->_pos + 2);
  760. $recordData = substr($this->_data, $this->_pos + 4, $length);
  761. $this->_pos += 4 + $length;
  762. $codepage = self::_GetInt2d($recordData, 0);
  763. self::$_codepage = self::NumberToName($codepage);
  764. }
  765. /**
  766. * DATEMODE
  767. *
  768. * This record specifies the base date for displaying date values. All dates are stored as count of days past this base date.
  769. * In BIFF2-BIFF4 this record is part of the Calculation Settings Block. In BIFF5-BIFF8 it is stored in the Workbook Globals Substream.
  770. */
  771. private function _readDateMode() {
  772. $length = self::_GetInt2d($this->_data, $this->_pos + 2);
  773. $recordData = substr($this->_data, $this->_pos + 4, $length);
  774. $this->_pos += 4 + $length;
  775. if (ord($recordData{0}) == 1) {
  776. $this->_excelBaseDate = 1904;
  777. }
  778. else{
  779. $this->_excelBaseDate = 1900;
  780. }
  781. }
  782. /**
  783. * data format
  784. */
  785. private function _readFormat(){
  786. $length = self::_GetInt2d($this->_data, $this->_pos + 2);
  787. $recordData = substr($this->_data, $this->_pos + 4, $length);
  788. $this->_pos += 4 + $length;
  789. $indexCode = self::_GetInt2d($recordData, 0);
  790. if ($this->_version == self::XLS_BIFF8) {
  791. $string = str_replace('"', '', self::_readUnicodeStringLong(substr($recordData, 2)));
  792. } else {
  793. // BIFF7
  794. $string = self::_readByteStringShort(substr($recordData, 2));
  795. }
  796. $formatString = $string['value'];
  797. $this->formatRecords[$indexCode] = $formatString;
  798. }
  799. /**
  800. * XF - Extended Format
  801. *
  802. * This record contains formatting information for cells, rows, columns or styles.
  803. * According to http://support.microsoft.com/kb/147732 there are always at least 15 cell style XF and 1 cell XF.
  804. * 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
  805. * We only read the first cell style XF and skip the remaining cell style XF records
  806. */
  807. private function _readXf() {
  808. $length = self::_GetInt2d($this->_data, $this->_pos + 2);
  809. $recordData = substr($this->_data, $this->_pos + 4, $length);
  810. $this->_pos += 4 + $length;
  811. $indexCode = self::_GetInt2d($recordData, 2);
  812. if (isset($this->formatRecords[$indexCode])) {
  813. $format = $this->formatRecords[$indexCode];
  814. }
  815. else if (isset($this->builtInFormats[$indexCode])) {
  816. $format = $this->builtInFormats[$indexCode];
  817. }
  818. else {
  819. $format = 'General';
  820. }
  821. $this->xfRecords[] = array(
  822. 'index' => $indexCode,
  823. 'format' => $format
  824. );
  825. }
  826. /**
  827. * SST - Shared String Table
  828. *
  829. * This record contains a list of all strings used anywherein the workbook. Each string occurs only once. The
  830. * workbook uses indexes into the list to reference the strings.
  831. **/
  832. private function _readSst() {
  833. $pos = 0; // offset within (spliced) record data
  834. $splicedRecordData = $this->_getSplicedRecordData(); // get spliced record data
  835. $recordData = $splicedRecordData['recordData'];
  836. $spliceOffsets = $splicedRecordData['spliceOffsets'];
  837. $pos += 4; // offset: 0; size: 4; total number of strings in the workbook
  838. $nm = self::_GetInt4d($recordData, 4); // offset: 4; size: 4; number of following strings ($nm)
  839. $pos += 4;
  840. for ($i = 0; $i < $nm; ++$i) { // loop through the Unicode strings (16-bit length)
  841. $numChars = self::_GetInt2d($recordData, $pos); // number of characters in the Unicode string
  842. $pos += 2;
  843. $optionFlags = ord($recordData{$pos}); // option flags
  844. ++$pos;
  845. $isCompressed = (($optionFlags & 0x01) == 0) ; // bit: 0; mask: 0x01; 0 = compressed; 1 = uncompressed
  846. $hasAsian = (($optionFlags & 0x04) != 0); // bit: 2; mask: 0x02; 0 = ordinary; 1 = Asian phonetic
  847. $hasRichText = (($optionFlags & 0x08) != 0); // bit: 3; mask: 0x03; 0 = ordinary; 1 = Rich-Text
  848. if ($hasRichText) {
  849. $formattingRuns = self::_GetInt2d($recordData, $pos); // number of Rich-Text formatting runs
  850. $pos += 2;
  851. }
  852. if ($hasAsian) {
  853. $extendedRunLength = self::_GetInt4d($recordData, $pos); // size of Asian phonetic setting
  854. $pos += 4;
  855. }
  856. $len = ($isCompressed) ? $numChars : $numChars * 2; // expected byte length of character array if not split
  857. foreach ($spliceOffsets as $spliceOffset) { // look up limit position
  858. if ($pos <= $spliceOffset) { // it can happen that the string is empty, therefore we need. <= and not just <
  859. $limitpos = $spliceOffset;
  860. break;
  861. }
  862. }
  863. if ($pos + $len <= $limitpos) {
  864. $retstr = substr($recordData, $pos, $len); // character array is not split between records
  865. $pos += $len;
  866. } else {
  867. $retstr = substr($recordData, $pos, $limitpos - $pos); // character array is split between records. first part of character array
  868. $bytesRead = $limitpos - $pos;
  869. $charsLeft = $numChars - (($isCompressed) ? $bytesRead : ($bytesRead / 2)); // remaining characters in Unicode string
  870. $pos = $limitpos;
  871. // keep reading the characters
  872. while ($charsLeft > 0) {
  873. // look up next limit position, in case the string span more than one continue record
  874. foreach ($spliceOffsets as $spliceOffset) {
  875. if ($pos < $spliceOffset) {
  876. $limitpos = $spliceOffset;
  877. break;
  878. }
  879. }
  880. // repeated option flags. OpenOffice.org documentation 5.21
  881. $option = ord($recordData{$pos});
  882. ++$pos;
  883. if ($isCompressed && ($option == 0)) {
  884. // 1st fragment compressed. this fragment compressed
  885. $len = min($charsLeft, $limitpos - $pos);
  886. $retstr .= substr($recordData, $pos, $len);
  887. $charsLeft -= $len;
  888. $isCompressed = true;
  889. } elseif (!$isCompressed && ($option != 0)) {
  890. // 1st fragment uncompressed. this fragment uncompressed
  891. $len = min($charsLeft * 2, $limitpos - $pos);
  892. $retstr .= substr($recordData, $pos, $len);
  893. $charsLeft -= $len / 2;
  894. $isCompressed = false;
  895. } elseif (!$isCompressed && ($option == 0)) {
  896. // 1st fragment uncompressed. this fragment compressed
  897. $len = min($charsLeft, $limitpos - $pos);
  898. for ($j = 0; $j < $len; ++$j) {
  899. $retstr .= $recordData{$pos + $j} . chr(0);
  900. }
  901. $charsLeft -= $len;
  902. $isCompressed = false;
  903. } else {
  904. // 1st fragment compressed. this fragment uncompressed
  905. $newstr = '';
  906. for ($j = 0; $j < strlen($retstr); ++$j) {
  907. $newstr .= $retstr[$j] . chr(0);
  908. }
  909. $retstr = $newstr;
  910. $len = min($charsLeft * 2, $limitpos - $pos);
  911. $retstr .= substr($recordData, $pos, $len);
  912. $charsLeft -= $len / 2;
  913. $isCompressed = false;
  914. }
  915. $pos += $len;
  916. }
  917. }
  918. $retstr = self::_encodeUTF16($retstr, $isCompressed); // convert to UTF-8
  919. $fmtRuns = array(); // read additional Rich-Text information, if any
  920. if ($hasRichText) {
  921. // list of formatting runs
  922. for ($j = 0; $j < $formattingRuns; ++$j) {
  923. $charPos = self::_GetInt2d($recordData, $pos + $j * 4); // first formatted character; zero-based
  924. $fontIndex = self::_GetInt2d($recordData, $pos + 2 + $j * 4); // index to font record
  925. $fmtRuns[] = array(
  926. 'charPos' => $charPos,
  927. 'fontIndex' => $fontIndex
  928. );
  929. }
  930. $pos += 4 * $formattingRuns;
  931. }
  932. // read additional Asian phonetics information, if any
  933. if ($hasAsian) {
  934. $pos += $extendedRunLength; // For Asian phonetic settings, we skip the extended string data
  935. }
  936. // store the shared sting
  937. $this->_sst[] = array(
  938. 'value' => $retstr,
  939. 'fmtRuns' => $fmtRuns
  940. );
  941. }
  942. }
  943. /**
  944. * Read RK record
  945. * This record represents a cell that contains an RK value (encoded integer or floating-point value). If a floating-point value
  946. * cannot be encoded to an RK value, a NUMBER record will be written. This record replaces the record INTEGER written in BIFF2.
  947. */
  948. private function _readRk() {
  949. $length = self::_GetInt2d($this->_data, $this->_pos + 2);
  950. $recordData = substr($this->_data, $this->_pos + 4, $length);
  951. $this->_pos += 4 + $length;
  952. $row = self::_GetInt2d($recordData, 0);
  953. $column = self::_GetInt2d($recordData, 2);
  954. $xfIndex = self::_GetInt2d($recordData, 4);
  955. $rknum = self::_GetInt4d($recordData, 6);
  956. $numValue = self::_GetIEEE754($rknum);
  957. // add cell
  958. $this->_addCell($row, $column, $numValue, $xfIndex);
  959. }
  960. /**
  961. * Read LABELSST record This record represents a cell that contains a string. It
  962. * replaces the LABEL record and RSTRING record used in BIFF2-BIFF5.
  963. */
  964. private function _readLabelSst() {
  965. $length = self::_GetInt2d($this->_data, $this->_pos + 2);
  966. $recordData = substr($this->_data, $this->_pos + 4, $length);
  967. $this->_pos += 4 + $length;
  968. $xfIndex = self::_GetInt2d($recordData, 4);
  969. $row = self::_GetInt2d($recordData, 0);
  970. $column = self::_GetInt2d($recordData, 2);
  971. // offset: 6; size: 4; index to SST record
  972. $index = self::_GetInt4d($recordData, 6);
  973. $this->_addCell($row, $column, $this->_sst[$index]['value'], $xfIndex);
  974. }
  975. /**
  976. * Read MULRK record
  977. * This record represents a cell range containing RK value cells. All cells are located in the same row.
  978. */
  979. private function _readMulRk() {
  980. $length = self::_GetInt2d($this->_data, $this->_pos + 2);
  981. $recordData = substr($this->_data, $this->_pos + 4, $length);
  982. $this->_pos += 4 + $length;
  983. $row = self::_GetInt2d($recordData, 0);
  984. $colFirst = self::_GetInt2d($recordData, 2);
  985. $colLast = self::_GetInt2d($recordData, $length - 2);
  986. $columns = $colLast - $colFirst + 1;
  987. // offset within record data
  988. $offset = 4;
  989. for ($i = 0; $i < $columns; ++$i) {
  990. $xfIndex = self::_GetInt2d($recordData, $offset);
  991. $numValue = self::_GetIEEE754(self::_GetInt4d($recordData, $offset + 2));
  992. $this->_addCell($row, $colFirst + $i, $numValue, $xfIndex);
  993. $offset += 6;
  994. }
  995. }
  996. /**
  997. * Read NUMBER record
  998. * This record represents a cell that contains a floating-point value.
  999. */
  1000. private function _readNumber() {
  1001. $length = self::_GetInt2d($this->_data, $this->_pos + 2);
  1002. $recordData = substr($this->_data, $this->_pos + 4, $length);
  1003. $this->_pos += 4 + $length;
  1004. $row = self::_GetInt2d($recordData, 0);
  1005. $column = self::_GetInt2d($recordData, 2);
  1006. $xfIndex = self::_GetInt2d($recordData, 4);
  1007. $numValue = self::_extractNumber(substr($recordData, 6, 8));
  1008. $this->_addCell($row, $column, $numValue, $xfIndex);
  1009. }
  1010. /**
  1011. * Read FORMULA record + perhaps a following STRING record if formula result is a string
  1012. * This record contains the token array and the result of a formula cell.
  1013. */
  1014. private function _readFormula() {
  1015. $length = self::_GetInt2d($this->_data, $this->_pos + 2);
  1016. $recordData = substr($this->_data, $this->_pos + 4, $length);
  1017. $this->_pos += 4 + $length;
  1018. $xfIndex = self::_GetInt2d($recordData, 4);
  1019. $row = self::_GetInt2d($recordData, 0);
  1020. $column = self::_GetInt2d($recordData, 2);
  1021. if ((ord($recordData{6}) == 0) && (ord($recordData{12}) == 255) && (ord($recordData{13}) == 255)) {
  1022. $this->_preRow = $row;
  1023. $this->_preColumn = $column;
  1024. return false;
  1025. }
  1026. elseif ((ord($recordData{6}) == 1) && (ord($recordData{12}) == 255) && (ord($recordData{13}) == 255)) {
  1027. // Boolean formula. Result is in +2; 0=false, 1=true
  1028. $value = (bool) ord($recordData{8});
  1029. }
  1030. elseif ((ord($recordData{6}) == 2) && (ord($recordData{12}) == 255) && (ord($recordData{13}) == 255)) {
  1031. // Error formula. Error code is in +2
  1032. $value = self::_mapErrorCode(ord($recordData{8}));
  1033. }
  1034. elseif ((ord($recordData{6}) == 3) && (ord($recordData{12}) == 255) && (ord($recordData{13}) == 255)) {
  1035. // Formula result is a null string
  1036. $value = '';
  1037. }
  1038. else {
  1039. // forumla result is a number, first 14 bytes like _NUMBER record
  1040. $value = self::_extractNumber(substr($recordData, 6, 8));
  1041. }
  1042. $this->_addCell($row, $column, $value, $xfIndex);
  1043. }
  1044. /**
  1045. * Read a STRING record from current stream position and advance the stream pointer to next record
  1046. * This record is used for storing result from FORMULA record when it is a string, and it occurs directly after the FORMULA record
  1047. *
  1048. * @return string The string contents as UTF-8
  1049. */
  1050. private function _readString() {
  1051. $length = self::_GetInt2d($this->_data, $this->_pos + 2);
  1052. $recordData = substr($this->_data, $this->_pos + 4, $length);
  1053. $this->_pos += 4 + $length;
  1054. $xfIndex = self::_GetInt2d($recordData, 4);
  1055. if ($this->_version == self::XLS_BIFF8) {
  1056. $string = self::_readUnicodeStringLong($recordData);
  1057. $value = $string['value'];
  1058. } else {
  1059. $string = self::_readByteStringLong($recordData);
  1060. $value = $string['value'];
  1061. }
  1062. $this->_addCell($this->_preRow, $this->_preColumn, $value, $xfIndex);
  1063. }
  1064. /**
  1065. * Read BOOLERR record
  1066. * This record represents a Boolean value or error value cell.
  1067. */
  1068. private function _readBoolErr() {
  1069. $length = self::_GetInt2d($this->_data, $this->_pos + 2);
  1070. $recordData = substr($this->_data, $this->_pos + 4, $length);
  1071. $this->_pos += 4 + $length;
  1072. $xfIndex = self::_GetInt2d($recordData, 4);
  1073. $row = self::_GetInt2d($recordData, 0);
  1074. $column = self::_GetInt2d($recordData, 2);
  1075. // offset: 6; size: 1; the boolean value or error value
  1076. $boolErr = ord($recordData{6});
  1077. // offset: 7; size: 1; 0=boolean; 1=error
  1078. $isError = ord($recordData{7});
  1079. switch ($isError) {
  1080. case 0: // boolean
  1081. $value = (bool) $boolErr;
  1082. // add cell value
  1083. $this->_addCell($row, $column, $value, $xfIndex);
  1084. break;
  1085. case 1: // error type
  1086. $value = self::_mapErrorCode($boolErr);
  1087. // add cell value
  1088. $this->_addCell($row, $column, $value, $xfIndex);
  1089. break;
  1090. }
  1091. }
  1092. /**
  1093. * Read LABEL record
  1094. * This record represents a cell that contains a string. In BIFF8 it is usually replaced by the LABELSST record.
  1095. * Excel still uses this record, if it copies unformatted text cells to the clipboard.
  1096. */
  1097. private function _readLabel() {
  1098. $length = self::_GetInt2d($this->_data, $this->_pos + 2);
  1099. $recordData = substr($this->_data, $this->_pos + 4, $length);
  1100. $this->_pos += 4 + $length;
  1101. $xfIndex = self::_GetInt2d($recordData, 4);
  1102. $row = self::_GetInt2d($recordData, 0);
  1103. $column = self::_GetInt2d($recordData, 2);
  1104. if ($this->_version == self::XLS_BIFF8) {
  1105. $string = self::_readUnicodeStringLong(substr($recordData, 6));
  1106. $value = $string['value'];
  1107. } else {
  1108. $string = self::_readByteStringLong(substr($recordData, 6));
  1109. $value = $string['value'];
  1110. }
  1111. $this->_addCell($row, $column, $value, $xfIndex);
  1112. }
  1113. /**
  1114. * Read BLANK record
  1115. */
  1116. private function _readBlank() {
  1117. $length = self::_GetInt2d($this->_data, $this->_pos + 2);
  1118. $recordData = substr($this->_data, $this->_pos + 4, $length);
  1119. $this->_pos += 4 + $length;
  1120. $xfIndex = self::_GetInt2d($recordData, 4);
  1121. $row = self::_GetInt2d($recordData, 0);
  1122. $column = self::_GetInt2d($recordData, 2);
  1123. $this->_addCell($row, $column, '', $xfIndex);
  1124. }
  1125. /**
  1126. * Reads a record from current position in data stream and continues reading data as long as CONTINUE
  1127. * records are found. Splices the record data pieces and returns the combined string as if record data is in one piece.
  1128. * Moves to next current position in data stream to start of next record different from a CONtINUE record
  1129. *
  1130. * @return array
  1131. */
  1132. private function _getSplicedRecordData() {
  1133. $data = '';
  1134. $spliceOffsets = array();
  1135. $i = 0;
  1136. $spliceOffsets[0] = 0;
  1137. do {
  1138. ++$i;
  1139. $identifier = self::_GetInt2d($this->_data, $this->_pos); // offset: 0; size: 2; identifier
  1140. $length = self::_GetInt2d($this->_data, $this->_pos + 2); // offset: 2; size: 2; length
  1141. $data .= substr($this->_data, $this->_pos + 4, $length);
  1142. $spliceOffsets[$i] = $spliceOffsets[$i - 1] + $length;
  1143. $this->_pos += 4 + $length;
  1144. $nextIdentifier = self::_GetInt2d($this->_data, $this->_pos);
  1145. } while ($nextIdentifier == self::XLS_Type_CONTINUE);
  1146. $splicedData = array(
  1147. 'recordData' => $data,
  1148. 'spliceOffsets' => $spliceOffsets,
  1149. );
  1150. return $splicedData;
  1151. }
  1152. /**
  1153. * Read byte string (16-bit string length)
  1154. * OpenOffice documentation: 2.5.2
  1155. *
  1156. * @param string $subData
  1157. * @return array
  1158. */
  1159. private static function _readByteStringLong($subData) {
  1160. // offset: 0; size: 2; length of the string (character count)
  1161. $ln = self::_GetInt2d($subData, 0);
  1162. // offset: 2: size: var; character array (8-bit characters)
  1163. $value = self::_decodeCodepage(substr($subData, 2));
  1164. //return $string;
  1165. return array(
  1166. 'value' => $value,
  1167. 'size' => 2 + $ln, // size in bytes of data structure
  1168. );
  1169. }
  1170. /**
  1171. * Map error code, e.g. '#N/A'
  1172. *
  1173. * @param int $subData
  1174. * @return string
  1175. */
  1176. private static function _mapErrorCode($subData) {
  1177. switch ($subData) {
  1178. case 0x00: return '#NULL!'; break;
  1179. case 0x07: return '#DIV/0!'; break;
  1180. case 0x0F: return '#VALUE!'; break;
  1181. case 0x17: return '#REF!'; break;
  1182. case 0x1D: return '#NAME?'; break;
  1183. case 0x24: return '#NUM!'; break;
  1184. case 0x2A: return '#N/A'; break;
  1185. default: return false;
  1186. }
  1187. }
  1188. /**
  1189. * Convert Microsoft Code Page Identifier to Code Page Name which iconv
  1190. * and mbstring understands
  1191. *
  1192. * @param integer $codePage Microsoft Code Page Indentifier
  1193. * @return string Code Page Name
  1194. */
  1195. private static function NumberToName($codePage = 1252) {
  1196. switch ($codePage) {
  1197. case 367: return 'ASCII'; break; // ASCII
  1198. case 437: return 'CP437'; break; // OEM US
  1199. //case 720: throw new PHPExcel_Exception('Code page 720 not supported.'); break; // OEM Arabic
  1200. case 737: return 'CP737'; break; // OEM Greek
  1201. case 775: return 'CP775'; break; // OEM Baltic
  1202. case 850: return 'CP850'; break; // OEM Latin I
  1203. case 852: return 'CP852'; break; // OEM Latin II (Central European)
  1204. case 855: return 'CP855'; break; // OEM Cyrillic
  1205. case 857: return 'CP857'; break; // OEM Turkish
  1206. case 858: return 'CP858'; break; // OEM Multilingual Latin I with Euro
  1207. case 860: return 'CP860'; break; // OEM Portugese
  1208. case 861: return 'CP861'; break; // OEM Icelandic
  1209. case 862: return 'CP862'; break; // OEM Hebrew
  1210. case 863: return 'CP863'; break; // OEM Canadian (French)
  1211. case 864: return 'CP864'; break; // OEM Arabic
  1212. case 865: return 'CP865'; break; // OEM Nordic
  1213. case 866: return 'CP866'; break; // OEM Cyrillic (Russian)
  1214. case 869: return 'CP869'; break; // OEM Greek (Modern)
  1215. case 874: return 'CP874'; break; // ANSI Thai
  1216. case 932: return 'CP932'; break; // ANSI Japanese Shift-JIS
  1217. case 936: return 'CP936'; break; // ANSI Chinese Simplified GBK
  1218. case 949: return 'CP949'; break; // ANSI Korean (Wansung)
  1219. case 950: return 'CP950'; break; // ANSI Chinese Traditional BIG5
  1220. case 1200: return 'UTF-16LE'; break; // UTF-16 (BIFF8)
  1221. case 1250: return 'CP1250'; break; // ANSI Latin II (Central European)
  1222. case 1251: return 'CP1251'; break; // ANSI Cyrillic
  1223. case 0: // CodePage is not always correctly set when the xls file was saved by Apple's Numbers program
  1224. case 1252: return 'CP1252'; break; // ANSI Latin I (BIFF4-BIFF7)
  1225. case 1253: return 'CP1253'; break; // ANSI Greek
  1226. case 1254: return 'CP1254'; break; // ANSI Turkish
  1227. case 1255: return 'CP1255'; break; // ANSI Hebrew
  1228. case 1256: return 'CP1256'; break; // ANSI Arabic
  1229. case 1257: return 'CP1257'; break; // ANSI Baltic
  1230. case 1258: return 'CP1258'; break; // ANSI Vietnamese
  1231. case 1361: return 'CP1361'; break; // ANSI Korean (Johab)
  1232. case 10000: return 'MAC'; break; // Apple Roman
  1233. case 10006: return 'MACGREEK'; break; // Macintosh Greek
  1234. case 10007: return 'MACCYRILLIC'; break; // Macintosh Cyrillic
  1235. case 10008: return 'CP936'; break; // Macintosh - Simplified Chinese (GB 2312)
  1236. case 10029: return 'MACCENTRALEUROPE'; break; // Macintosh Central Europe
  1237. case 10079: return 'MACICELAND'; break; // Macintosh Icelandic
  1238. case 10081: return 'MACTURKISH'; break; // Macintosh Turkish
  1239. case 32768: return 'MAC'; break; // Apple Roman
  1240. //case 32769: throw new PHPExcel_Exception('Code page 32769 not supported.'); break; // ANSI Latin I (BIFF2-BIFF3)
  1241. case 65000: return 'UTF-7'; break; // Unicode (UTF-7)
  1242. case 65001: return 'UTF-8'; break; // Unicode (UTF-8)
  1243. default: return 'UTF-8'; break;
  1244. }
  1245. }
  1246. /**
  1247. * String from columnindex
  1248. *
  1249. * @param int $pColumnIndex
  1250. * @return string
  1251. */
  1252. private static function _stringFromColumnIndex($pColumnIndex = 0) {
  1253. static $_indexCache = array();
  1254. if ( ! isset($_indexCache[$pColumnIndex])) {
  1255. if ($pColumnIndex < 26) {
  1256. $_indexCache[$pColumnIndex] = chr(65 + $pColumnIndex);
  1257. } elseif ($pColumnIndex < 702) {
  1258. $_indexCache[$pColumnIndex] = chr(64 + ($pColumnIndex / 26)) . chr(65 + $pColumnIndex % 26);
  1259. } else {
  1260. $_indexCache[$pColumnIndex] = chr(64 + (($pColumnIndex - 26) / 676)) . chr(65 + ((($pColumnIndex - 26) % 676) / 26)) . chr(65 + $pColumnIndex % 26);
  1261. }
  1262. }
  1263. return $_indexCache[$pColumnIndex];
  1264. }
  1265. /**
  1266. * Extracts an Excel Unicode short string (8-bit string length)
  1267. * OpenOffice documentation: 2.5.3
  1268. * function will automatically find out where the Unicode string ends.
  1269. *
  1270. * @param string $subData
  1271. * @return array
  1272. */
  1273. private static function _readUnicodeStringShort($subData) {
  1274. $characterCount = ord($subData[0]); // offset: 0: size: 1; length of the string (character count)
  1275. $string = self::_readUnicodeString(substr($subData, 1), $characterCount);
  1276. $string['size'] += 1; // add 1 for the string length
  1277. return $string;
  1278. }
  1279. /**
  1280. * Read byte string (8-bit string length)
  1281. * OpenOffice documentation: 2.5.2
  1282. *
  1283. * @param string $subData
  1284. * @return array
  1285. */
  1286. private static function _readByteStringShort($subData) {
  1287. $ln = ord($subData[0]); // offset: 0; size: 1; length of the string (character count)
  1288. $value = self::_decodeCodepage(substr($subData, 1, $ln)); // offset: 1: size: var; character array (8-bit characters)
  1289. return array(
  1290. 'value' => $value,
  1291. 'size' => 1 + $ln, // size in bytes of data structure
  1292. );
  1293. }
  1294. /**
  1295. * Extracts an Excel Unicode long string (16-bit string length)
  1296. * OpenOffice documentation: 2.5.3. this function is under construction, needs to support rich text, and Asian phonetic settings
  1297. *
  1298. * @param string $subData
  1299. * @return array
  1300. */
  1301. private static function _readUnicodeStringLong($subData) {
  1302. $value = '';
  1303. // offset: 0: size: 2; length of the string (character count)
  1304. $characterCount = self::_GetInt2d($subData, 0);
  1305. $string = self::_readUnicodeString(substr($subData, 2), $characterCount);
  1306. // add 2 for the string length
  1307. $string['size'] += 2;
  1308. return $string;
  1309. }
  1310. /**
  1311. * Read Unicode string with no string length field, but with known character count
  1312. * this function is under construction, needs to support rich text, and Asian phonetic settings
  1313. * OpenOffice.org's Documentation of the Microsoft Excel File Format, section 2.5.3
  1314. *
  1315. * @param string $subData
  1316. * @param int $characterCount
  1317. * @return array
  1318. */
  1319. private static function _readUnicodeString($subData, $characterCount) {
  1320. $isCompressed = !((0x01 & ord($subData[0])) >> 0); // bit: 0; mask: 0x01; character compression (0 = compressed 8-bit, 1 = uncompressed 16-bit)
  1321. $hasAsian = (0x04) & ord($subData[0]) >> 2; // bit: 2; mask: 0x04; Asian phonetic settings
  1322. $hasRichText = (0x08) & ord($subData[0]) >> 3; // bit: 3; mask: 0x08; Rich-Text settings
  1323. // offset: 1: size: var; character array
  1324. // this offset assumes richtext and Asian phonetic settings are off which is generally wrong
  1325. // needs to be fixed
  1326. $value = self::_encodeUTF16(substr($subData, 1, $isCompressed ? $characterCount : 2 * $characterCount), $isCompressed);
  1327. return array(
  1328. 'value' => $value,
  1329. 'size' => $isCompressed ? 1 + $characterCount : 1 + 2 * $characterCount, // the size in bytes including the option flags
  1330. );
  1331. }
  1332. /**
  1333. * Get UTF-8 string from (compressed or uncompressed) UTF-16 string
  1334. *
  1335. * @param string $string
  1336. * @param bool $compressed
  1337. * @return string
  1338. */
  1339. private static function _encodeUTF16($string, $compressed = '') {
  1340. if ($compressed) {
  1341. $string = self::_uncompressByteString($string);
  1342. }
  1343. return mb_convert_encoding($string, 'UTF-8', 'UTF-16LE');
  1344. }
  1345. /**
  1346. * Convert string to UTF-8. Only used for BIFF5.
  1347. *
  1348. * @param string $string
  1349. * @return string
  1350. */
  1351. private static function _decodeCodepage($string) {
  1352. return mb_convert_encoding($string, 'UTF-8', self::$_codepage);
  1353. }
  1354. /**
  1355. * Convert UTF-16 string in compressed notation to uncompressed form. Only used for BIFF8.
  1356. *
  1357. * @param string $string
  1358. * @return string
  1359. */
  1360. private static function _uncompressByteString($string) {
  1361. $uncompressedString = '';
  1362. $strLen = strlen($string);
  1363. for ($i = 0; $i < $strLen; ++$i) {
  1364. $uncompressedString .= $string[$i] . "\0";
  1365. }
  1366. return $uncompressedString;
  1367. }
  1368. /**
  1369. * Read 16-bit unsigned integer
  1370. *
  1371. * @param string $data
  1372. * @param int $pos
  1373. * @return int
  1374. */
  1375. private static function _GetInt2d($data, $pos) {
  1376. return ord($data[$pos]) | (ord($data[$pos+1]) << 8);
  1377. }
  1378. /**
  1379. * Read 32-bit signed integer
  1380. * 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
  1381. * http://sourceforge.net/tracker/index.php?func=detail&aid=1487372&group_id=99160&atid=623334
  1382. *
  1383. * @param string $data
  1384. * @param int $pos
  1385. * @return int
  1386. */
  1387. private static function _GetInt4d($data, $pos) {
  1388. $_or_24 = ord($data[$pos + 3]);
  1389. if ($_or_24 >= 128) {
  1390. $_ord_24 = -abs((256 - $_or_24) << 24); // negative number
  1391. } else {
  1392. $_ord_24 = ($_or_24 & 127) << 24;
  1393. }
  1394. return ord($data[$pos]) | (ord($data[$pos+1]) << 8) | (ord($data[$pos+2]) << 16) | $_ord_24;
  1395. }
  1396. /**
  1397. * Reads first 8 bytes of a string and return IEEE 754 float
  1398. *
  1399. * @param string $data Binary string that is at least 8 bytes long
  1400. * @return float
  1401. */
  1402. private static function _extractNumber($data) {
  1403. $rknumhigh = self::_GetInt4d($data, 4);
  1404. $rknumlow = self::_GetInt4d($data, 0);
  1405. $sign = ($rknumhigh & 0x80000000) >> 31;
  1406. $exp = (($rknumhigh & 0x7ff00000) >> 20) - 1023;
  1407. $mantissa = (0x100000 | ($rknumhigh & 0x000fffff));
  1408. $mantissalow1 = ($rknumlow & 0x80000000) >> 31;
  1409. $mantissalow2 = ($rknumlow & 0x7fffffff);
  1410. $value = $mantissa / pow( 2 , (20 - $exp));
  1411. if ($mantissalow1 != 0) {
  1412. $value += 1 / pow (2 , (21 - $exp));
  1413. }
  1414. $value += $mantissalow2 / pow (2 , (52 - $exp));
  1415. if ($sign) {
  1416. $value *= -1;
  1417. }
  1418. return $value;
  1419. }
  1420. private static function _GetIEEE754($rknum) {
  1421. if (($rknum & 0x02) != 0) {
  1422. $value = $rknum >> 2;
  1423. } else {
  1424. // changes by mmp, info on IEEE754 encoding from
  1425. // research.microsoft.com/~hollasch/cgindex/coding/ieeefloat.html
  1426. // The RK format calls for using only the most significant 30 bits
  1427. // of the 64 bit floating point value. The other 34 bits are assumed
  1428. // to be 0 so we use the upper 30 bits of $rknum as follows...
  1429. $sign = ($rknum & 0x80000000) >> 31;
  1430. $exp = ($rknum & 0x7ff00000) >> 20;
  1431. $mantissa = (0x100000 | ($rknum & 0x000ffffc));
  1432. $value = $mantissa / pow( 2 , (20- ($exp - 1023)));
  1433. if ($sign) {
  1434. $value = -1 * $value;
  1435. }
  1436. //end of changes by mmp
  1437. }
  1438. if (($rknum & 0x01) != 0) {
  1439. $value /= 100;
  1440. }
  1441. return $value;
  1442. }
  1443. }