simpleXLS.class.php 51 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224122512261227122812291230123112321233123412351236123712381239124012411242124312441245124612471248124912501251125212531254125512561257125812591260126112621263126412651266126712681269127012711272127312741275127612771278127912801281128212831284128512861287128812891290129112921293129412951296129712981299130013011302130313041305130613071308
  1. <?php
  2. /*
  3. * Example
  4. echo '<pre>';
  5. if ( $xls = SimpleXLS::parse('excel5book.xls')) {
  6. print_r( $xls->rows() ); // dump first sheet
  7. print_r( $xls->rows(1)); /// dump second sheet
  8. } else {
  9. echo SimpleXLS::parseError();
  10. }
  11. echo '</pre>';
  12. */
  13. class simpleXLS {
  14. public const BIFF8 = 0x600;
  15. public const BIFF7 = 0x500;
  16. public const WORKBOOKGLOBALS = 0x5;
  17. public const WORKSHEET = 0x10;
  18. //const TYPE_BOF = 0x809;
  19. public const TYPE_EOF = 0x0a;
  20. public const TYPE_BOUNDSHEET = 0x85;
  21. public const TYPE_DIMENSION = 0x200;
  22. public const TYPE_ROW = 0x208;
  23. public const TYPE_DBCELL = 0xd7;
  24. public const TYPE_FILEPASS = 0x2f;
  25. //const TYPE_NOTE = 0x1c;
  26. //const TYPE_TXO = 0x1b6;
  27. public const TYPE_RK = 0x7e;
  28. public const TYPE_RK2 = 0x27e;
  29. public const TYPE_MULRK = 0xbd;
  30. public const TYPE_MULBLANK = 0xbe;
  31. //const TYPE_INDEX = 0x20b;
  32. public const TYPE_SST = 0xfc;
  33. //const TYPE_EXTSST = 0xff;
  34. //const TYPE_CONTINUE = 0x3c;
  35. public const TYPE_LABEL = 0x204;
  36. public const TYPE_LABELSST = 0xfd;
  37. public const TYPE_NUMBER = 0x203;
  38. public const TYPE_NAME = 0x18;
  39. //const TYPE_ARRAY = 0x221;
  40. //const TYPE_STRING = 0x207;
  41. public const TYPE_FORMULA = 0x406;
  42. public const TYPE_FORMULA2 = 0x6;
  43. public const TYPE_FORMAT = 0x41e;
  44. public const TYPE_XF = 0xe0;
  45. public const TYPE_BOOLERR = 0x205;
  46. //const TYPE_UNKNOWN = 0xffff;
  47. public const TYPE_NINETEENFOUR = 0x22;
  48. public const TYPE_MERGEDCELLS = 0xE5;
  49. public const TYPE_WINDOW1 = 0x3D;
  50. //const DEF_NUM_FORMAT = "%.2f";
  51. public const DEF_NUM_FORMAT = '%s';
  52. // OLE
  53. public const NUM_BIG_BLOCK_DEPOT_BLOCKS_POS = 0x2c;
  54. public const SMALL_BLOCK_DEPOT_BLOCK_POS = 0x3c;
  55. public const ROOT_START_BLOCK_POS = 0x30;
  56. public const BIG_BLOCK_SIZE = 0x200;
  57. public const SMALL_BLOCK_SIZE = 0x40;
  58. public const EXTENSION_BLOCK_POS = 0x44;
  59. public const NUM_EXTENSION_BLOCK_POS = 0x48;
  60. public const PROPERTY_STORAGE_BLOCK_SIZE = 0x80;
  61. public const BIG_BLOCK_DEPOT_BLOCKS_POS = 0x4c;
  62. public const SMALL_BLOCK_THRESHOLD = 0x1000;
  63. // property storage offsets
  64. public const SIZE_OF_NAME_POS = 0x40;
  65. public const TYPE_POS = 0x42;
  66. public const START_BLOCK_POS = 0x74;
  67. public const SIZE_POS = 0x78;
  68. /**
  69. * Array of worksheets found
  70. *
  71. * @var array
  72. * @access public
  73. */
  74. public $boundsheets = array();
  75. public $activeSheet = 0;
  76. /**
  77. * Array of format records found
  78. *
  79. * @var array
  80. * @access public
  81. */
  82. public $formatRecords = array();
  83. /**
  84. *
  85. * @var array
  86. * @access public
  87. */
  88. public $sst = array();
  89. /**
  90. * Array of worksheets
  91. *
  92. * The data is stored in 'cells' and the meta-data is stored in an array
  93. * called 'cellsInfo'
  94. *
  95. * Example:
  96. *
  97. * $sheets --> 'cells' --> row --> column --> Interpreted value
  98. * --> 'cellsInfo' --> row --> column --> 'type' - Can be 'date', 'number', or 'unknown'
  99. * --> 'raw' - The raw data that Excel stores for that data cell
  100. *
  101. * @var array
  102. * @access public
  103. */
  104. public $sheets = array();
  105. /**
  106. * List of default date formats used by Excel
  107. *
  108. * @var array
  109. * @access public
  110. */
  111. public $dateFormats = array(
  112. 0xe => 'd/m/Y',
  113. 0xf => 'd-M-Y',
  114. 0x10 => 'd-M',
  115. 0x11 => 'M-Y',
  116. 0x12 => 'h:i a',
  117. 0x13 => 'h:i:s a',
  118. 0x14 => 'H:i',
  119. 0x15 => 'H:i:s',
  120. 0x16 => 'd/m/Y H:i',
  121. 0x2d => 'i:s',
  122. 0x2e => 'H:i:s',
  123. 0x2f => 'i:s.S'
  124. );
  125. /**
  126. * Default number formats used by Excel
  127. *
  128. * @var array
  129. * @access public
  130. */
  131. public $numberFormats = array(
  132. 0x1 => '%1.0f', // "0"
  133. 0x2 => '%1.2f', // "0.00",
  134. 0x3 => '%1.0f', //"#,##0",
  135. 0x4 => '%1.2f', //"#,##0.00",
  136. 0x5 => '%1.0f', /*"$#,##0;($#,##0)",*/
  137. 0x6 => '$%1.0f', /*"$#,##0;($#,##0)",*/
  138. 0x7 => '$%1.2f', //"$#,##0.00;($#,##0.00)",
  139. 0x8 => '$%1.2f', //"$#,##0.00;($#,##0.00)",
  140. 0x9 => '%1.0f%%', // "0%"
  141. 0xa => '%1.2f%%', // "0.00%"
  142. 0xb => '%1.2f', // 0.00E00",
  143. 0x25 => '%1.0f', // "#,##0;(#,##0)",
  144. 0x26 => '%1.0f', //"#,##0;(#,##0)",
  145. 0x27 => '%1.2f', //"#,##0.00;(#,##0.00)",
  146. 0x28 => '%1.2f', //"#,##0.00;(#,##0.00)",
  147. 0x29 => '%1.0f', //"#,##0;(#,##0)",
  148. 0x2a => '$%1.0f', //"$#,##0;($#,##0)",
  149. 0x2b => '%1.2f', //"#,##0.00;(#,##0.00)",
  150. 0x2c => '$%1.2f', //"$#,##0.00;($#,##0.00)",
  151. 0x30 => '%1.0f'
  152. );
  153. protected $datetimeFormat = 'Y-m-d H:i:s';
  154. /**
  155. * Default encoding
  156. *
  157. * @var string
  158. * @access private
  159. */
  160. protected $defaultEncoding = 'UTF-8';
  161. /**
  162. * Default number format
  163. *
  164. * @var integer
  165. * @access private
  166. */
  167. protected $defaultFormat = self::DEF_NUM_FORMAT;
  168. /**
  169. * List of formats to use for each column
  170. *
  171. * @var array
  172. * @access private
  173. */
  174. protected $columnsFormat = array();
  175. protected $nineteenFour;
  176. protected $multiplier;
  177. protected $sn;
  178. protected $curFormat;
  179. // OLERead
  180. protected $data;
  181. protected $bigBlockChain;
  182. protected $smallBlockChain;
  183. protected $rootEntry;
  184. protected $entry;
  185. protected $props;
  186. // sergey.shuchkin@gmail.com
  187. protected $wrkbook; // false - to use excel format
  188. protected $error = false;
  189. protected $debug;
  190. // {{{ Spreadsheet_Excel_Reader()
  191. /**
  192. * Constructor
  193. *
  194. * @param string $filename XLS Filename or xls contents
  195. * @param bool $isData If True then $filename is contents
  196. * @param bool $debug Trigger PHP errors?
  197. */
  198. public function __construct(string $filename, bool $isData = false, bool $debug = false)
  199. {
  200. $this->debug = $debug;
  201. $this->_oleread($filename, $isData);
  202. $this->_parse();
  203. }
  204. public static function parseFile($filename, $debug = false)
  205. {
  206. return self::parse($filename, false, $debug);
  207. }
  208. public static function parseData($data, $debug = false)
  209. {
  210. return self::parse($data, true, $debug);
  211. }
  212. public static function parse($filename, $isData = false, $debug = false)
  213. {
  214. $xls = new self($filename, $isData, $debug);
  215. if ($xls->success()) {
  216. return $xls;
  217. }
  218. self::parseError($xls->error());
  219. return false;
  220. }
  221. public static function parseError($set = false)
  222. {
  223. static $error = false;
  224. return $set ? $error = $set : $error;
  225. }
  226. public function error($set = false)
  227. {
  228. if ($set) {
  229. $this->error = $set;
  230. if ($this->debug) {
  231. trigger_error($set);
  232. }
  233. }
  234. return $this->error;
  235. }
  236. public function success(): bool
  237. {
  238. return ! $this->error;
  239. }
  240. public function rows($sheetNum = 0, $limit = 0)
  241. {
  242. if ($this->sheets[ $sheetNum ]) {
  243. $s = $this->sheets[ $sheetNum ];
  244. $result = array();
  245. for ($i = 0; $i < $s['numRows']; $i ++) {
  246. $r = array();
  247. for ($j = 0; $j < $s['numCols']; $j ++) {
  248. $r[ $j ] = $s['cells'][$i][$j] ?? '';
  249. }
  250. $result[] = $r;
  251. $limit--;
  252. if ($limit === 0) {
  253. break;
  254. }
  255. }
  256. return $result;
  257. }
  258. return false;
  259. }
  260. public function rowsEx($sheetNum = 0, $limit = 0): array
  261. {
  262. if ($this->sheets[ $sheetNum ]) {
  263. $s = $this->sheets[ $sheetNum ];
  264. $result = array();
  265. for ($i = 0; $i < $s['numRows']; $i ++) {
  266. $r = array();
  267. for ($j = 0; $j < $s['numCols']; $j ++) {
  268. $v = $s['cellsInfo'][$i][$j] ?? array();
  269. // if ( $v['type'] === self::TYPE_RK || $v['type'] === self::TYPE_RK2 ||
  270. $v['value'] = $s['cells'][$i][$j] ?? '';
  271. $r[ $j ] = $v;
  272. }
  273. $result[] = $r;
  274. $limit--;
  275. if ($limit === 0) {
  276. break;
  277. }
  278. }
  279. return $result;
  280. }
  281. return [];
  282. }
  283. public function toHTML($worksheetIndex = 0): string
  284. {
  285. $s = '<table class=excel>';
  286. foreach ($this->rows($worksheetIndex) as $r) {
  287. $s .= '<tr>';
  288. foreach ($r as $c) {
  289. $s .= '<td nowrap>' . ( $c === '' ? '&nbsp' : htmlspecialchars($c, ENT_QUOTES) ) . '</td>';
  290. }
  291. $s .= "</tr>\r\n";
  292. }
  293. $s .= '</table>';
  294. return $s;
  295. }
  296. public function setDateTimeFormat($value): SimpleXLS
  297. {
  298. $this->datetimeFormat = is_string($value) ? $value : false;
  299. return $this;
  300. }
  301. public function sheetNames(): array
  302. {
  303. $result = array();
  304. foreach ($this->boundsheets as $k => $v) {
  305. $result[ $k ] = $v['name'];
  306. }
  307. return $result;
  308. }
  309. public function sheetName($index)
  310. {
  311. return isset($this->boundsheets[ $index ]) ? $this->boundsheets[ $index ]['name'] : null;
  312. }
  313. // }}}
  314. protected function _oleread($sFileName, $isData = false): bool
  315. {
  316. if ($isData) {
  317. $this->data = $sFileName;
  318. } else {
  319. // check if file exist and is readable (Darko Miljanovic)
  320. if (! is_readable($sFileName)) {
  321. $this->error('File not is readable ' . $sFileName);
  322. return false;
  323. }
  324. $this->data = file_get_contents($sFileName);
  325. if (! $this->data) {
  326. $this->error('File reading error ' . $sFileName);
  327. return false;
  328. }
  329. }
  330. //echo IDENTIFIER_OLE;
  331. //echo 'start';
  332. if (strpos($this->data, pack('CCCCCCCC', 0xd0, 0xcf, 0x11, 0xe0, 0xa1, 0xb1, 0x1a, 0xe1)) !== 0) {
  333. $this->error('File is not XLS');
  334. return false;
  335. }
  336. $numBigBlockDepotBlocks = $this->_getInt4d(self::NUM_BIG_BLOCK_DEPOT_BLOCKS_POS);
  337. $sbdStartBlock = $this->_getInt4d(self::SMALL_BLOCK_DEPOT_BLOCK_POS);
  338. $rootStartBlock = $this->_getInt4d(self::ROOT_START_BLOCK_POS);
  339. $extensionBlock = $this->_getInt4d(self::EXTENSION_BLOCK_POS);
  340. $numExtensionBlocks = $this->_getInt4d(self::NUM_EXTENSION_BLOCK_POS);
  341. /*
  342. echo $this->numBigBlockDepotBlocks." ";
  343. echo $this->sbdStartBlock." ";
  344. echo $this->rootStartBlock." ";
  345. echo $this->extensionBlock." ";
  346. echo $this->numExtensionBlocks." ";
  347. */
  348. //echo "sbdStartBlock = $this->sbdStartBlock\n";
  349. $bigBlockDepotBlocks = array();
  350. $pos = self::BIG_BLOCK_DEPOT_BLOCKS_POS;
  351. // echo "pos = $pos";
  352. $bbdBlocks = $numBigBlockDepotBlocks;
  353. if ($numExtensionBlocks !== 0) {
  354. $bbdBlocks = ( self::BIG_BLOCK_SIZE - self::BIG_BLOCK_DEPOT_BLOCKS_POS ) / 4;
  355. }
  356. for ($i = 0; $i < $bbdBlocks; $i ++) {
  357. $bigBlockDepotBlocks[ $i ] = $this->_getInt4d($pos);
  358. $pos += 4;
  359. }
  360. for ($j = 0; $j < $numExtensionBlocks; $j ++) {
  361. $pos = ( $extensionBlock + 1 ) * self::BIG_BLOCK_SIZE;
  362. $blocksToRead = min($numBigBlockDepotBlocks - $bbdBlocks, self::BIG_BLOCK_SIZE / 4 - 1);
  363. for ($i = $bbdBlocks; $i < $bbdBlocks + $blocksToRead; $i ++) {
  364. $bigBlockDepotBlocks[ $i ] = $this->_getInt4d($pos);
  365. $pos += 4;
  366. }
  367. $bbdBlocks += $blocksToRead;
  368. if ($bbdBlocks < $numBigBlockDepotBlocks) {
  369. $extensionBlock = $this->_getInt4d($pos);
  370. }
  371. }
  372. // var_dump($bigBlockDepotBlocks);
  373. // readBigBlockDepot
  374. $index = 0;
  375. $this->bigBlockChain = array();
  376. for ($i = 0; $i < $numBigBlockDepotBlocks; $i ++) {
  377. $pos = ( $bigBlockDepotBlocks[ $i ] + 1 ) * self::BIG_BLOCK_SIZE;
  378. //echo "pos = $pos";
  379. for ($j = 0; $j < self::BIG_BLOCK_SIZE / 4; $j ++) {
  380. $this->bigBlockChain[ $index ] = $this->_getInt4d($pos);
  381. $pos += 4;
  382. $index ++;
  383. }
  384. }
  385. //var_dump($this->bigBlockChain);
  386. //echo '=====2';
  387. // readSmallBlockDepot();
  388. $index = 0;
  389. $sbdBlock = $sbdStartBlock;
  390. $this->smallBlockChain = array();
  391. while ($sbdBlock !== - 2) {
  392. $pos = ( $sbdBlock + 1 ) * self::BIG_BLOCK_SIZE;
  393. for ($j = 0; $j < self::BIG_BLOCK_SIZE / 4; $j ++) {
  394. $this->smallBlockChain[ $index ] = $this->_getInt4d($pos);
  395. $pos += 4;
  396. $index ++;
  397. }
  398. $sbdBlock = $this->bigBlockChain[ $sbdBlock ];
  399. }
  400. // readData(rootStartBlock)
  401. $block = $rootStartBlock;
  402. $this->entry = $this->_readData($block);
  403. /*
  404. while ($block != -2) {
  405. $pos = ($block + 1) * self::BIG_BLOCK_SIZE;
  406. $this->entry = $this->entry.substr($this->_data, $pos, self::BIG_BLOCK_SIZE);
  407. $block = $this->bigBlockChain[$block];
  408. }
  409. */
  410. //echo '==='.$this->entry."===";
  411. $this->_readPropertySets();
  412. $this->data = $this->_readWorkBook();
  413. return true;
  414. }
  415. // {{{ setOutputEncoding()
  416. protected function _getInt2d($pos): int
  417. {
  418. return ord($this->data[ $pos ]) | ord($this->data[ $pos + 1 ]) << 8;
  419. // return ($value > 0x7FFFFFFF) ? $value - 0x100000000 : $value;
  420. }
  421. protected function _getInt4d($pos): int
  422. {
  423. $value = ord($this->data[ $pos ]) | ( ord($this->data[ $pos + 1 ]) << 8 ) | ( ord($this->data[ $pos + 2 ]) << 16 ) | ( ord($this->data[ $pos + 3 ]) << 24 );
  424. return ($value > 0x7FFFFFFF) ? $value - 0x100000000 : $value;
  425. }
  426. // }}}
  427. // {{{ setRowColOffset()
  428. protected function _readData($bl): string
  429. {
  430. $block = $bl;
  431. $data = '';
  432. while ($block !== - 2) {
  433. $pos = ( $block + 1 ) * self::BIG_BLOCK_SIZE;
  434. $data .= substr($this->data, $pos, self::BIG_BLOCK_SIZE);
  435. //echo "pos = $pos data=$data\n";
  436. $block = $this->bigBlockChain[ $block ];
  437. }
  438. return $data;
  439. }
  440. // }}}
  441. // {{{ setDefaultFormat()
  442. protected function _readPropertySets(): void
  443. {
  444. $offset = 0;
  445. //var_dump($this->entry);
  446. while ($offset < strlen($this->entry)) {
  447. $d = substr($this->entry, $offset, self::PROPERTY_STORAGE_BLOCK_SIZE);
  448. $nameSize = ord($d[ self::SIZE_OF_NAME_POS ]) | ( ord($d[ self::SIZE_OF_NAME_POS + 1 ]) << 8 );
  449. $type = ord($d[ self::TYPE_POS ]);
  450. //$maxBlock = $this->_strlen($d) / self::BIG_BLOCK_SIZE - 1;
  451. $startBlock = ord($d[ self::START_BLOCK_POS]) | ( ord($d[ self::START_BLOCK_POS + 1 ]) << 8 ) | ( ord($d[ self::START_BLOCK_POS + 2 ]) << 16 ) | ( ord($d[ self::START_BLOCK_POS + 3 ]) << 24 );
  452. $size = ord($d[ self::SIZE_POS]) | ( ord($d[ self::SIZE_POS + 1 ]) << 8 ) | ( ord($d[ self::SIZE_POS + 2 ]) << 16 ) | ( ord($d[ self::SIZE_POS + 3 ]) << 24 );
  453. $name = '';
  454. for ($i = 0; $i < $nameSize; $i ++) {
  455. $name .= $d[ $i ];
  456. }
  457. $name = str_replace("\x00", '', $name);
  458. $this->props[] = array(
  459. 'name' => $name,
  460. 'type' => $type,
  461. 'startBlock' => $startBlock,
  462. 'size' => $size
  463. );
  464. if (( $name === 'Workbook' ) || ( $name === 'Book' )) {
  465. $this->wrkbook = count($this->props) - 1;
  466. }
  467. if ($name === 'Root Entry') {
  468. $this->rootEntry = count($this->props) - 1;
  469. }
  470. //echo "name ==$name=\n";
  471. $offset += self::PROPERTY_STORAGE_BLOCK_SIZE;
  472. }
  473. }
  474. // }}}
  475. // {{{ setColumnFormat()
  476. protected function _readWorkBook(): string
  477. {
  478. if ($this->props[ $this->wrkbook ]['size'] < self::SMALL_BLOCK_THRESHOLD) {
  479. // getSmallBlockStream(PropertyStorage ps)
  480. $rootdata = $this->_readData($this->props[ $this->rootEntry ]['startBlock']);
  481. $streamData = '';
  482. $block = (int) $this->props[ $this->wrkbook ]['startBlock'];
  483. //$count = 0;
  484. while ($block !== - 2) {
  485. $pos = $block * self::SMALL_BLOCK_SIZE;
  486. $streamData .= substr($rootdata, $pos, self::SMALL_BLOCK_SIZE);
  487. $block = $this->smallBlockChain[ $block ];
  488. }
  489. return $streamData;
  490. }
  491. $numBlocks = $this->props[ $this->wrkbook ]['size'] / self::BIG_BLOCK_SIZE;
  492. if ($this->props[ $this->wrkbook ]['size'] % self::BIG_BLOCK_SIZE !== 0) {
  493. $numBlocks ++;
  494. }
  495. if ($numBlocks === 0) {
  496. return '';
  497. }
  498. //echo "numBlocks = $numBlocks\n";
  499. //byte[] streamData = new byte[numBlocks * self::BIG_BLOCK_SIZE];
  500. //print_r($this->wrkbook);
  501. $streamData = '';
  502. $block = $this->props[ $this->wrkbook ]['startBlock'];
  503. //echo "block = $block";
  504. while ($block !== - 2) {
  505. $pos = ( $block + 1 ) * self::BIG_BLOCK_SIZE;
  506. $streamData .= substr($this->data, $pos, self::BIG_BLOCK_SIZE);
  507. $block = $this->bigBlockChain[ $block ];
  508. }
  509. //echo 'stream'.$streamData;
  510. return $streamData;
  511. }
  512. // }}}
  513. protected function parseSubstreamHeader($pos): array
  514. {
  515. $length = $this->_getInt2d($pos + 2);
  516. $version = $this->_getInt2d($pos + 4);
  517. $substreamType = $this->_getInt2d($pos + 6);
  518. return array( $length, $version, $substreamType );
  519. }
  520. // {{{ _parse()
  521. /**
  522. * Parse a workbook
  523. *
  524. * @access private
  525. * @return bool
  526. */
  527. protected function _parse()
  528. {
  529. $pos = 0;
  530. // $code = ord($this->data[$pos]) | ord($this->data[$pos+1])<<8;
  531. [$length, $version, $substreamType] = $this->parseSubstreamHeader($pos);
  532. // echo "Start parse code=".base_convert($code,10,16)." version=".base_convert($version,10,16)." substreamType=".base_convert($substreamType,10,16).""."\n";
  533. // die();
  534. if (( $version !== self::BIFF8 ) &&
  535. ( $version !== self::BIFF7 )
  536. ) {
  537. return false;
  538. }
  539. if ($substreamType !== self::WORKBOOKGLOBALS) {
  540. return false;
  541. }
  542. //print_r($rec);
  543. $pos += $length + 4;
  544. $code = ord($this->data[ $pos ]) | ord($this->data[ $pos + 1 ]) << 8;
  545. $length = ord($this->data[ $pos + 2 ]) | ord($this->data[ $pos + 3 ]) << 8;
  546. while ($code !== self::TYPE_EOF) {
  547. switch ($code) {
  548. case self::TYPE_SST:
  549. //echo "Type_SST\n";
  550. $formattingRuns = 0;
  551. $extendedRunLength = 0;
  552. $spos = $pos + 4;
  553. $limitpos = $spos + $length;
  554. $uniqueStrings = $this->_getInt4d($spos + 4);
  555. $spos += 8;
  556. for ($i = 0; $i < $uniqueStrings; $i ++) {
  557. // Read in the number of characters
  558. if ($spos === $limitpos) {
  559. $opcode = ord($this->data[ $spos ]) | ord($this->data[ $spos + 1 ]) << 8;
  560. $conlength = ord($this->data[ $spos + 2 ]) | ord($this->data[ $spos + 3 ]) << 8;
  561. if ($opcode !== 0x3c) {
  562. return - 1;
  563. }
  564. $spos += 4;
  565. $limitpos = $spos + $conlength;
  566. }
  567. $numChars = ord($this->data[ $spos ]) | ( ord($this->data[ $spos + 1 ]) << 8 );
  568. //echo "i = $i pos = $pos numChars = $numChars ";
  569. $spos += 2;
  570. $optionFlags = ord($this->data[ $spos ]);
  571. $spos ++;
  572. $asciiEncoding = ( ( $optionFlags & 0x01 ) === 0 );
  573. $extendedString = ( ( $optionFlags & 0x04 ) !== 0 );
  574. // See if string contains formatting information
  575. $richString = ( ( $optionFlags & 0x08 ) !== 0 );
  576. if ($richString) {
  577. // Read in the crun
  578. $formattingRuns = $this->_getInt2d($spos);
  579. $spos += 2;
  580. }
  581. if ($extendedString) {
  582. // Read in cchExtRst
  583. $extendedRunLength = $this->_getInt4d($spos);
  584. $spos += 4;
  585. }
  586. $len = $asciiEncoding ? $numChars : $numChars * 2;
  587. if ($spos + $len < $limitpos) {
  588. $retstr = substr($this->data, $spos, $len);
  589. $spos += $len;
  590. } else {
  591. // found countinue
  592. $retstr = substr($this->data, $spos, $limitpos - $spos);
  593. $bytesRead = $limitpos - $spos;
  594. $charsLeft = $numChars - ( $asciiEncoding ? $bytesRead : ( $bytesRead / 2 ) );
  595. $spos = $limitpos;
  596. while ($charsLeft > 0) {
  597. $opcode = $this->_getInt2d($spos);
  598. $conlength = $this->_getInt2d($spos + 2);
  599. if ($opcode !== 0x3c) {
  600. return - 1;
  601. }
  602. $spos += 4;
  603. $limitpos = $spos + $conlength;
  604. $option = ord($this->data[ $spos ]);
  605. $spos ++;
  606. if ($asciiEncoding && ( $option === 0 )) {
  607. $len = min($charsLeft, $limitpos - $spos); // min($charsLeft, $conlength);
  608. $retstr .= substr($this->data, $spos, $len);
  609. $charsLeft -= $len;
  610. $asciiEncoding = true;
  611. } elseif (! $asciiEncoding && ( $option !== 0 )) {
  612. $len = min($charsLeft * 2, $limitpos - $spos); // min($charsLeft, $conlength);
  613. $retstr .= substr($this->data, $spos, $len);
  614. $charsLeft -= $len / 2;
  615. $asciiEncoding = false;
  616. } elseif (! $asciiEncoding && ( $option === 0 )) {
  617. // Bummer - the string starts off as Unicode, but after the
  618. // continuation it is in straightforward ASCII encoding
  619. $len = min($charsLeft, $limitpos - $spos); // min($charsLeft, $conlength);
  620. for ($j = 0; $j < $len; $j ++) {
  621. $retstr .= $this->data[ $spos + $j ] . chr(0);
  622. }
  623. $charsLeft -= $len;
  624. $asciiEncoding = false;
  625. } else {
  626. $newstr = '';
  627. for ($j = 0, $len_retstr = strlen($retstr); $j < $len_retstr; $j ++) {
  628. $newstr = $retstr[ $j ] . chr(0);
  629. }
  630. $retstr = $newstr;
  631. $len = min($charsLeft * 2, $limitpos - $spos); // min($charsLeft, $conlength);
  632. $retstr .= substr($this->data, $spos, $len);
  633. $charsLeft -= $len / 2;
  634. $asciiEncoding = false;
  635. //echo "Izavrat\n";
  636. }
  637. $spos += $len;
  638. }
  639. }
  640. $retstr = $asciiEncoding ? $this->_latin1toDef($retstr) : $this->_UTF16toDef($retstr);
  641. // echo "Str $i = $retstr\n";
  642. if ($richString) {
  643. $spos += 4 * $formattingRuns;
  644. }
  645. // For extended strings, skip over the extended string data
  646. if ($extendedString) {
  647. $spos += $extendedRunLength;
  648. }
  649. //if ($retstr == 'Derby'){
  650. // echo "bb\n";
  651. //}
  652. $this->sst[] = $retstr;
  653. }
  654. /*$continueRecords = array();
  655. while ($this->getNextCode() == Type_CONTINUE) {
  656. $continueRecords[] = &$this->nextRecord();
  657. }
  658. //echo " 1 Type_SST\n";
  659. $this->shareStrings = new SSTRecord($r, $continueRecords);
  660. //print_r($this->shareStrings->strings);
  661. */
  662. // echo 'SST read: '.($time_end-$time_start)."\n";
  663. break;
  664. case self::TYPE_FILEPASS:
  665. return false;
  666. case self::TYPE_NAME:
  667. //echo "Type_NAME\n";
  668. break;
  669. case self::TYPE_FORMAT:
  670. $indexCode = $this->_getInt2d($pos + 4);
  671. if ($version === self::BIFF8) {
  672. $numchars = $this->_getInt2d($pos + 6);
  673. if (ord($this->data[ $pos + 8 ]) === 0) { // ascii
  674. $formatString = substr($this->data, $pos + 9, $numchars);
  675. $formatString = $this->_latin1toDef($formatString);
  676. } else {
  677. $formatString = substr($this->data, $pos + 9, $numchars * 2);
  678. $formatString = $this->_UTF16toDef($formatString);
  679. }
  680. } else {
  681. $numchars = ord($this->data[ $pos + 6 ]);
  682. $formatString = substr($this->data, $pos + 7, $numchars * 2);
  683. $formatString = $this->_latin1toDef($formatString);
  684. }
  685. $this->formatRecords[ $indexCode ] = $formatString;
  686. // echo "Type.FORMAT[$indexCode]=$formatString\n";
  687. break;
  688. case self::TYPE_XF:
  689. $formatstr = '';
  690. $indexCode = $this->_getInt2d($pos + 6);
  691. // echo "\nType.XF code=".$indexCode." dateFormat=".$this->dateFormats[ $indexCode ]." numberFormats=".$this->numberFormats[ $indexCode ].PHP_EOL;
  692. if (array_key_exists($indexCode, $this->dateFormats)) {
  693. //echo "isdate ".$dateFormats[$indexCode];
  694. $this->formatRecords['xfrecords'][] = array(
  695. 'type' => 'date',
  696. 'format' => $this->dateFormats[ $indexCode ]
  697. );
  698. } elseif (array_key_exists($indexCode, $this->numberFormats)) {
  699. //echo "isnumber ".$this->numberFormats[$indexCode];
  700. $this->formatRecords['xfrecords'][] = array(
  701. 'type' => 'number',
  702. 'format' => $this->numberFormats[ $indexCode ]
  703. );
  704. } else {
  705. $isdate = false;
  706. if ($indexCode > 0) {
  707. if (isset($this->formatRecords[ $indexCode ])) {
  708. // die( 'L:'.__LINE__ );
  709. $formatstr = $this->formatRecords[ $indexCode ];
  710. }
  711. //echo '.other.';
  712. // echo "\nfl=".strlen( $formatstr)." fs=$formatstr=\n";
  713. // echo "\ncode=".$indexCode." fl=".strlen( $formatstr)." fs=$formatstr=\n";
  714. $fs = str_replace('\\', '', $formatstr);
  715. if ($fs && preg_match('/^[hmsday\/\-:\., ]+$/i', $fs)) { // found day and time format
  716. $isdate = true;
  717. $formatstr = str_replace(array( 'yyyy',':mm','mm','dddd','dd', 'h','ss' ), array('Y',':i','m','l','d', 'H','s' ), $fs);
  718. }
  719. }
  720. if ($isdate) {
  721. $this->formatRecords['xfrecords'][] = array(
  722. 'type' => 'date',
  723. 'format' => $formatstr,
  724. 'code' => $indexCode
  725. );
  726. } else {
  727. // echo 'fs='.$formatstr.PHP_EOL;
  728. $this->formatRecords['xfrecords'][] = array(
  729. 'type' => 'other',
  730. 'format' => '',
  731. 'code' => $indexCode
  732. );
  733. }
  734. }
  735. // echo count( $this->formatRecords['xfrecords'] ).' fs='.$formatstr.' ' . PHP_EOL;
  736. //echo "\n";
  737. break;
  738. case self::TYPE_NINETEENFOUR:
  739. //echo "Type.NINETEENFOUR\n";
  740. $this->nineteenFour = ( ord($this->data[ $pos + 4 ]) === 1 );
  741. break;
  742. case self::TYPE_BOUNDSHEET:
  743. //echo "Type.BOUNDSHEET\n";
  744. $rec_offset = $this->_getInt4d($pos + 4);
  745. // $rec_typeFlag = ord($this->_data[$pos + 8]);
  746. $rec_length = ord($this->data[ $pos + 10 ]);
  747. $hidden = false;
  748. $rec_name = '';
  749. if ($version === self::BIFF8) {
  750. //ord($this->data[$pos + 9])
  751. $hidden = ord($this->data[$pos + 8]) === 1;
  752. $chartype = ord($this->data[ $pos + 11 ]);
  753. if ($chartype === 0) {
  754. $rec_name = substr($this->data, $pos + 12, $rec_length);
  755. $rec_name = $this->_latin1toDef($rec_name);
  756. } else {
  757. $rec_name = substr($this->data, $pos + 12, $rec_length * 2);
  758. $rec_name = $this->_UTF16toDef($rec_name);
  759. }
  760. } elseif ($version === self::BIFF7) {
  761. $rec_name = substr($this->data, $pos + 11, $rec_length);
  762. }
  763. $this->boundsheets[] = array(
  764. 'name' => $rec_name,
  765. 'offset' => $rec_offset,
  766. 'hidden' => $hidden,
  767. 'active' => false
  768. );
  769. break;
  770. case self::TYPE_WINDOW1:
  771. $this->activeSheet = $this->_getInt2d($pos + 14);
  772. break;
  773. }
  774. //echo "Code = ".base_convert($r['code'],10,16)."\n";
  775. $pos += $length + 4;
  776. $code = $this->_getInt2d($pos);
  777. $length = $this->_getInt2d($pos + 2);
  778. //$r = &$this->nextRecord();
  779. //echo "1 Code = ".base_convert($r['code'],10,16)."\n";
  780. }
  781. foreach ($this->boundsheets as $key => $val) {
  782. $this->sn = $key;
  783. $this->_parseSheet($val['offset']);
  784. if ($key === $this->activeSheet) {
  785. $this->boundsheets[ $key ]['active'] = true;
  786. }
  787. }
  788. return true;
  789. }
  790. protected function _latin1toDef($string)
  791. {
  792. $result = $string;
  793. if ($this->defaultEncoding) {
  794. $result = mb_convert_encoding($string, $this->defaultEncoding, 'ISO-8859-1');
  795. }
  796. return $result;
  797. }
  798. protected function _UTF16toDef($string)
  799. {
  800. $result = $string;
  801. if ($this->defaultEncoding && $this->defaultEncoding !== 'UTF-16LE') {
  802. $result = mb_convert_encoding($string, $this->defaultEncoding, 'UTF-16LE');
  803. }
  804. return $result;
  805. }
  806. protected function _parseSheet($spos): bool
  807. {
  808. $cont = true;
  809. // read BOF
  810. // $code = ord($this->_data[$spos]) | ord($this->_data[$spos + 1]) << 8;
  811. [$length, $version, $substreamType] = $this->parseSubstreamHeader($spos);
  812. if (( $version !== self::BIFF8 ) && ( $version !== self::BIFF7 )) {
  813. return false;
  814. }
  815. if ($substreamType !== self::WORKSHEET) {
  816. return false;
  817. }
  818. //echo "Start parse code=".base_convert($code,10,16)." version=".base_convert($version,10,16)." substreamType=".base_convert($substreamType,10,16).""."\n";
  819. $spos += $length + 4;
  820. //var_dump($this->formatRecords);
  821. //echo "code $code $length";
  822. $this->sheets[ $this->sn ]['maxrow'] = 0;
  823. $this->sheets[ $this->sn ]['maxcol'] = 0;
  824. $this->sheets[ $this->sn ]['numRows'] = 0;
  825. $this->sheets[ $this->sn ]['numCols'] = 0;
  826. while ($cont) {
  827. //echo "mem= ".memory_get_usage()."\n";
  828. // $r = &$this->file->nextRecord();
  829. $lowcode = ord($this->data[ $spos ]);
  830. if ($lowcode === self::TYPE_EOF) {
  831. break;
  832. }
  833. $t_code = $lowcode | ord($this->data[ $spos + 1 ]) << 8;
  834. $length = ord($this->data[ $spos + 2 ]) | ord($this->data[ $spos + 3 ]) << 8;
  835. $spos += 4;
  836. //echo "Code=".base_convert($code,10,16)." $code\n";
  837. $this->multiplier = 1; // need for format with %
  838. switch ($t_code) {
  839. case self::TYPE_DIMENSION:
  840. //echo 'Type_DIMENSION ';
  841. if (!isset($this->numRows)) {
  842. if (( $length === 10 ) || ( $version === self::BIFF7 )) {
  843. $this->sheets[ $this->sn ]['numRows'] = ord($this->data[ $spos + 2 ]) | ord($this->data[ $spos + 3 ]) << 8;
  844. $this->sheets[ $this->sn ]['numCols'] = ord($this->data[ $spos + 6 ]) | ord($this->data[ $spos + 7 ]) << 8;
  845. } else {
  846. $this->sheets[ $this->sn ]['numRows'] = ord($this->data[ $spos + 4 ]) | ord($this->data[ $spos + 5 ]) << 8;
  847. $this->sheets[ $this->sn ]['numCols'] = ord($this->data[ $spos + 10 ]) | ord($this->data[ $spos + 11 ]) << 8;
  848. }
  849. }
  850. //echo 'numRows '.$this->numRows.' '.$this->numCols."\n";
  851. break;
  852. case self::TYPE_MERGEDCELLS:
  853. $cellRanges = ord($this->data[ $spos ]) | ord($this->data[ $spos + 1 ]) << 8;
  854. for ($i = 0; $i < $cellRanges; $i ++) {
  855. $fr = ord($this->data[ $spos + 8 * $i + 2 ]) | ord($this->data[ $spos + 8 * $i + 3 ]) << 8;
  856. $lr = ord($this->data[ $spos + 8 * $i + 4 ]) | ord($this->data[ $spos + 8 * $i + 5 ]) << 8;
  857. $fc = ord($this->data[ $spos + 8 * $i + 6 ]) | ord($this->data[ $spos + 8 * $i + 7 ]) << 8;
  858. $lc = ord($this->data[ $spos + 8 * $i + 8 ]) | ord($this->data[ $spos + 8 * $i + 9 ]) << 8;
  859. //$this->sheets[$this->sn]['mergedCells'][] = array($fr + 1, $fc + 1, $lr + 1, $lc + 1);
  860. if ($lr - $fr > 0) {
  861. $this->sheets[ $this->sn ]['cellsInfo'][ $fr + 1 ][ $fc + 1 ]['rowspan'] = $lr - $fr + 1;
  862. }
  863. if ($lc - $fc > 0) {
  864. $this->sheets[ $this->sn ]['cellsInfo'][ $fr + 1 ][ $fc + 1 ]['colspan'] = $lc - $fc + 1;
  865. }
  866. }
  867. //echo "Merged Cells $cellRanges $lr $fr $lc $fc\n";
  868. break;
  869. case self::TYPE_RK:
  870. case self::TYPE_RK2:
  871. //echo 'self::TYPE_RK'."\n";
  872. $row = ord($this->data[ $spos ]) | ord($this->data[ $spos + 1 ]) << 8;
  873. $column = ord($this->data[ $spos + 2 ]) | ord($this->data[ $spos + 3 ]) << 8;
  874. $rknum = $this->_getInt4d($spos + 6);
  875. $numValue = $this->_getIEEE754($rknum);
  876. //echo $numValue." ";
  877. $t_alias = 'n';
  878. if ($this->isDate($spos)) {
  879. [$string, $raw] = $this->createDate($numValue);
  880. $t_alias = 'd';
  881. } else {
  882. $raw = $numValue;
  883. if (isset($this->columnsFormat[ $column + 1 ])) {
  884. $this->curFormat = $this->columnsFormat[ $column + 1 ];
  885. }
  886. $string = sprintf($this->curFormat, $numValue * $this->multiplier);
  887. //$this->addcell(RKRecord($r));
  888. }
  889. $this->addCell($row, $column, $string, $raw, $t_code, $t_alias);
  890. //echo "Type_RK $row $column $string $raw {$this->curformat}\n";
  891. break;
  892. case self::TYPE_LABELSST:
  893. $row = ord($this->data[ $spos ]) | ord($this->data[ $spos + 1 ]) << 8;
  894. $column = ord($this->data[ $spos + 2 ]) | ord($this->data[ $spos + 3 ]) << 8;
  895. // $xfindex = ord($this->_data[$spos + 4]) | ord($this->_data[$spos + 5]) << 8;
  896. $index = $this->_getInt4d($spos + 6);
  897. //var_dump($this->sst);
  898. $this->addCell($row, $column, $this->sst[ $index ], $index, $t_code, 's');
  899. //echo "LabelSST $row $column $string\n";
  900. break;
  901. case self::TYPE_MULRK:
  902. $row = ord($this->data[ $spos ]) | ord($this->data[ $spos + 1 ]) << 8;
  903. $colFirst = ord($this->data[ $spos + 2 ]) | ord($this->data[ $spos + 3 ]) << 8;
  904. $colLast = ord($this->data[ $spos + $length - 2 ]) | ord($this->data[ $spos + $length - 1 ]) << 8;
  905. $columns = $colLast - $colFirst + 1;
  906. $tmppos = $spos + 4;
  907. $t_alias = 'n';
  908. for ($i = 0; $i < $columns; $i ++) {
  909. $numValue = $this->_getIEEE754($this->_getInt4d($tmppos + 2));
  910. if ($this->isDate($tmppos - 4)) {
  911. [$string, $raw] = $this->createDate($numValue);
  912. $t_alias = 'd';
  913. } else {
  914. $raw = $numValue;
  915. if (isset($this->columnsFormat[ $colFirst + $i + 1 ])) {
  916. $this->curFormat = $this->columnsFormat[ $colFirst + $i + 1 ];
  917. }
  918. $string = sprintf($this->curFormat, $numValue * $this->multiplier);
  919. }
  920. //$rec['rknumbers'][$i]['xfindex'] = ord($rec['data'][$pos]) | ord($rec['data'][$pos+1]) << 8;
  921. $tmppos += 6;
  922. $this->addCell($row, $colFirst + $i, $string, $raw, $t_code, $t_alias);
  923. //echo "MULRK $row ".($colFirst + $i)." $string\n";
  924. }
  925. //MulRKRecord($r);
  926. // Get the individual cell records from the multiple record
  927. //$num = ;
  928. break;
  929. case self::TYPE_NUMBER:
  930. $row = ord($this->data[ $spos ]) | ord($this->data[ $spos + 1 ]) << 8;
  931. $column = ord($this->data[ $spos + 2 ]) | ord($this->data[ $spos + 3 ]) << 8;
  932. $tmp = unpack('ddouble', substr($this->data, $spos + 6, 8)); // It machine machine dependent
  933. $t_alias = 'n';
  934. if ($this->isDate($spos)) {
  935. [$string, $raw] = $this->createDate($tmp['double']);
  936. $t_alias = 'd';
  937. // $this->addcell(DateRecord($r, 1));
  938. } else {
  939. //$raw = $tmp[''];
  940. if (isset($this->columnsFormat[ $column + 1 ])) {
  941. $this->curFormat = $this->columnsFormat[ $column + 1 ];
  942. }
  943. $raw = $this->createNumber($spos);
  944. $string = sprintf($this->curFormat, $raw * $this->multiplier);
  945. // $this->addcell(NumberRecord($r));
  946. }
  947. $this->addCell($row, $column, $string, $raw, $t_code, $t_alias);
  948. //echo "Number $row $column $string\n";
  949. break;
  950. case self::TYPE_FORMULA:
  951. case self::TYPE_FORMULA2:
  952. $row = ord($this->data[ $spos ]) | ord($this->data[ $spos + 1 ]) << 8;
  953. $column = ord($this->data[ $spos + 2 ]) | ord($this->data[ $spos + 3 ]) << 8;
  954. /*
  955. $byte6 = ord($this->_data[$spos + 6]);
  956. $byte12 = ord($this->_data[$spos + 12]);
  957. $byte13 = ord($this->_data[$spos + 13]);
  958. if ( $byte6 === 0 && $byte12 === 255 && $byte13 === 255 ) {
  959. //String formula. Result follows in a STRING record
  960. //echo "FORMULA $row $column Formula with a string<br>\n";
  961. } else if ($byte6 === 1 && $byte12 === 255 && $byte13 === 255 ) {
  962. //Boolean formula. Result is in +2; 0=false,1=true
  963. } else if ($byte6 === 2 && $byte12 === 255 && $byte13 === 255) {
  964. //Error formula. Error code is in +2;
  965. } else if ( $byte6 === 3 && $byte12 === 255 && $byte13 === 255) {
  966. //Formula result is a null string.
  967. */
  968. if (! ( ord($this->data[ $spos + 6 ]) < 4 && ord($this->data[ $spos + 12 ]) === 255 && ord($this->data[ $spos + 13 ]) === 255 )) {
  969. // result is a number, so first 14 bytes are just like a _NUMBER record
  970. $tmp = unpack('ddouble', substr($this->data, $spos + 6, 8)); // It machine machine dependent
  971. if ($this->isDate($spos)) {
  972. [$string, $raw] = $this->createDate($tmp['double']);
  973. // $this->addcell(DateRecord($r, 1));
  974. } else {
  975. //$raw = $tmp[''];
  976. if (isset($this->columnsFormat[ $column + 1 ])) {
  977. $this->curFormat = $this->columnsFormat[ $column + 1 ];
  978. }
  979. $raw = $this->createNumber($spos);
  980. $string = sprintf($this->curFormat, $raw * $this->multiplier);
  981. // $this->addcell(NumberRecord($r));
  982. }
  983. $this->addCell($row, $column, $string, $raw, $t_code, 'f');
  984. //echo "Number $row $column $string\n";
  985. }
  986. break;
  987. case self::TYPE_BOOLERR:
  988. $row = $this->_getInt2d($spos);
  989. $column = $this->_getInt2d($spos + 2);
  990. $string = ord($this->data[ $spos + 6 ]);
  991. $this->addCell($row, $column, $string, $string, $t_code, 'b');
  992. //echo 'Type_BOOLERR '."\n";
  993. break;
  994. case self::TYPE_ROW:
  995. case self::TYPE_DBCELL:
  996. case self::TYPE_MULBLANK:
  997. break;
  998. case self::TYPE_LABEL:
  999. $row = $this->_getInt2d($spos);
  1000. $column = $this->_getInt2d($spos);
  1001. $string = substr($this->data, $spos + 8, ord($this->data[ $spos + 6 ]) | ord($this->data[ $spos + 7 ]) << 8);
  1002. $this->addCell($row, $column, $string, '', $t_code, 'inlineStr');
  1003. // $this->addcell(LabelRecord($r));
  1004. break;
  1005. case self::TYPE_EOF:
  1006. $cont = false;
  1007. break;
  1008. default:
  1009. //echo ' unknown :'.base_convert($r['code'],10,16)."\n";
  1010. break;
  1011. }
  1012. $spos += $length;
  1013. }
  1014. if ($this->sheets[ $this->sn ]['numRows'] === 0) {
  1015. $this->sheets[ $this->sn ]['numRows'] = $this->sheets[ $this->sn ]['maxrow'];
  1016. }
  1017. if ($this->sheets[ $this->sn ]['numCols'] === 0) {
  1018. $this->sheets[ $this->sn ]['numCols'] = $this->sheets[ $this->sn ]['maxcol'];
  1019. }
  1020. return true;
  1021. }
  1022. //}}}
  1023. //{{{ createDate()
  1024. protected function _getIEEE754($rknum)
  1025. {
  1026. if (( $rknum & 0x02 ) !== 0) {
  1027. $value = $rknum >> 2;
  1028. } else {
  1029. //mmp
  1030. // first comment out the previously existing 7 lines of code here
  1031. // $tmp = unpack("d", pack("VV", 0, ($rknum & 0xfffffffc)));
  1032. // //$value = $tmp[''];
  1033. // if (array_key_exists(1, $tmp)) {
  1034. // $value = $tmp[1];
  1035. // } else {
  1036. // $value = $tmp[''];
  1037. // }
  1038. // I got my info on IEEE754 encoding from
  1039. // http://research.microsoft.com/~hollasch/cgindex/coding/ieeefloat.html
  1040. // The RK format calls for using only the most significant 30 bits of the
  1041. // 64 bit floating point value. The other 34 bits are assumed to be 0
  1042. // So, we use the upper 30 bits of $rknum as follows...
  1043. $sign = ( $rknum & 0x80000000 ) >> 31;
  1044. $exp = ( $rknum & 0x7ff00000 ) >> 20;
  1045. $mantissa = ( 0x100000 | ( $rknum & 0x000ffffc ) );
  1046. $value = $mantissa / (2 ** (20 - ($exp - 1023)));
  1047. if ($sign) {
  1048. $value = - 1 * $value;
  1049. }
  1050. //end of changes by mmp
  1051. }
  1052. if (( $rknum & 0x01 ) !== 0) {
  1053. $value /= 100;
  1054. }
  1055. return $value;
  1056. }
  1057. protected function isDate($spos): bool
  1058. {
  1059. //$xfindex = GetInt2d(, 4);
  1060. $xfindex = ord($this->data[ $spos + 4 ]) | ord($this->data[ $spos + 5 ]) << 8;
  1061. // echo 'check is date '.$xfindex.' '.$this->formatRecords['xfrecords'][$xfindex]['type']." ".$this->formatRecords['xfrecords'][ $xfindex ]['format']."\n";
  1062. if ($this->formatRecords['xfrecords'][ $xfindex ]['type'] === 'date') {
  1063. $this->curFormat = $this->formatRecords['xfrecords'][ $xfindex ]['format'];
  1064. return true;
  1065. }
  1066. if ($this->formatRecords['xfrecords'][ $xfindex ]['type'] === 'number') {
  1067. $this->curFormat = $this->formatRecords['xfrecords'][ $xfindex ]['format'];
  1068. if (strpos($this->curFormat, '%%') !== false) {
  1069. $this->multiplier = 100;
  1070. }
  1071. } else {
  1072. $this->curFormat = $this->defaultFormat;
  1073. }
  1074. return false;
  1075. }
  1076. /**
  1077. * Convert the raw Excel date into a human readable format
  1078. *
  1079. * Dates in Excel are stored as number of seconds from an epoch. On
  1080. * Windows, the epoch is 30/12/1899 and on Mac it's 01/01/1904
  1081. *
  1082. * @param integer $timevalue The raw Excel value to convert
  1083. *
  1084. * @return array First element is the converted date, the second element is number a unix timestamp
  1085. */
  1086. public function createDate(int $timevalue): array
  1087. {
  1088. // $offset = ($timeoffset===null)? date('Z') : $timeoffset * 3600;
  1089. if ($timevalue > 1) {
  1090. $timevalue -= ( $this->nineteenFour ? 24107 : 25569 );
  1091. }
  1092. $ts = round($timevalue * 24 * 3600);
  1093. $string = $this->datetimeFormat ? gmdate($this->datetimeFormat, $ts) : gmdate($this->curFormat, $ts);
  1094. return array( $string, $ts );
  1095. }
  1096. protected function addCell($row, $col, $string, $raw = '', $type_code = 0, $type_alias = ''): void
  1097. {
  1098. //echo "ADD cel $row-$col $string\n";
  1099. $this->sheets[ $this->sn ]['maxrow'] = max($this->sheets[ $this->sn ]['maxrow'], $row);
  1100. $this->sheets[ $this->sn ]['maxcol'] = max($this->sheets[ $this->sn ]['maxcol'], $col);
  1101. $this->sheets[ $this->sn ]['cells'][ $row ][ $col ] = $string;
  1102. if ($raw) {
  1103. $this->sheets[ $this->sn ]['cellsInfo'][ $row ][ $col ]['raw'] = $raw;
  1104. }
  1105. if ($type_code) {
  1106. $this->sheets[$this->sn]['cellsInfo'][$row][$col]['type'] = $type_code;
  1107. $this->sheets[$this->sn]['cellsInfo'][$row][$col]['t'] = $type_alias;
  1108. }
  1109. }
  1110. protected function createNumber($spos)
  1111. {
  1112. $rknumhigh = $this->_getInt4d($spos + 10);
  1113. $rknumlow = $this->_getInt4d($spos + 6);
  1114. //for ($i=0; $i<8; $i++) { echo ord($this->_data[$i+$spos+6]) . " "; } echo "<br>";
  1115. $sign = ( $rknumhigh & 0x80000000 ) >> 31;
  1116. $exp = ( $rknumhigh & 0x7ff00000 ) >> 20;
  1117. $mantissa = ( 0x100000 | ( $rknumhigh & 0x000fffff ) );
  1118. $mantissalow1 = ( $rknumlow & 0x80000000 ) >> 31;
  1119. $mantissalow2 = ( $rknumlow & 0x7fffffff );
  1120. $value = $mantissa / (2 ** (20 - ($exp - 1023)));
  1121. if ($mantissalow1 !== 0) {
  1122. $value += 1 / (2 ** (21 - ($exp - 1023)));
  1123. }
  1124. $value += $mantissalow2 / (2 ** (52 - ($exp - 1023)));
  1125. //echo "Sign = $sign, Exp = $exp, mantissahighx = $mantissa, mantissalow1 = $mantissalow1, mantissalow2 = $mantissalow2<br>\n";
  1126. if ($sign) {
  1127. $value = - 1 * $value;
  1128. }
  1129. return $value;
  1130. }
  1131. /**
  1132. * Set the encoding method
  1133. *
  1134. * @param string $encoding Encoding to use
  1135. *
  1136. * @access public
  1137. */
  1138. public function setOutputEncoding(string $encoding): SimpleXLS
  1139. {
  1140. $this->defaultEncoding = $encoding;
  1141. return $this;
  1142. }
  1143. /**
  1144. * Set the default number format, ex. "%.2f"
  1145. *
  1146. * @access public
  1147. *
  1148. * @param string $sFormat Default format
  1149. */
  1150. public function setDefaultFormat(string $sFormat): SimpleXLS
  1151. {
  1152. $this->defaultFormat = $sFormat;
  1153. return $this;
  1154. }
  1155. /**
  1156. * Force a column to use a certain format
  1157. *
  1158. * @access public
  1159. *
  1160. * @param integer $column Column number
  1161. * @param string $sFormat Format
  1162. */
  1163. public function setColumnFormat(int $column, string $sFormat): SimpleXLS
  1164. {
  1165. $this->columnsFormat[ $column ] = $sFormat;
  1166. return $this;
  1167. }
  1168. }