Excel5.php 76 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192119311941195119611971198119912001201120212031204120512061207120812091210121112121213121412151216121712181219122012211222122312241225122612271228122912301231123212331234123512361237123812391240124112421243124412451246124712481249125012511252125312541255125612571258125912601261126212631264126512661267126812691270127112721273127412751276127712781279128012811282128312841285128612871288128912901291129212931294129512961297129812991300130113021303130413051306130713081309131013111312131313141315131613171318131913201321132213231324132513261327132813291330133113321333133413351336133713381339134013411342134313441345134613471348134913501351135213531354135513561357135813591360136113621363136413651366136713681369137013711372137313741375137613771378137913801381138213831384138513861387138813891390139113921393139413951396139713981399140014011402140314041405140614071408140914101411141214131414141514161417141814191420142114221423142414251426142714281429143014311432143314341435143614371438143914401441144214431444144514461447144814491450145114521453145414551456145714581459146014611462146314641465146614671468146914701471147214731474147514761477147814791480148114821483148414851486148714881489149014911492149314941495149614971498149915001501150215031504150515061507150815091510151115121513151415151516151715181519152015211522152315241525152615271528152915301531153215331534153515361537153815391540154115421543154415451546154715481549155015511552155315541555155615571558155915601561156215631564156515661567156815691570157115721573157415751576157715781579158015811582158315841585158615871588158915901591159215931594159515961597159815991600160116021603160416051606160716081609161016111612161316141615161616171618161916201621162216231624162516261627162816291630163116321633163416351636163716381639164016411642164316441645164616471648164916501651165216531654165516561657165816591660166116621663166416651666166716681669167016711672167316741675167616771678167916801681168216831684168516861687168816891690169116921693169416951696169716981699170017011702170317041705170617071708170917101711171217131714171517161717171817191720172117221723172417251726172717281729173017311732173317341735173617371738173917401741174217431744174517461747174817491750175117521753175417551756175717581759176017611762176317641765176617671768176917701771177217731774177517761777177817791780178117821783178417851786178717881789179017911792179317941795179617971798179918001801180218031804180518061807180818091810181118121813181418151816181718181819182018211822182318241825182618271828182918301831183218331834183518361837183818391840184118421843184418451846184718481849185018511852185318541855185618571858185918601861186218631864186518661867186818691870187118721873187418751876187718781879188018811882188318841885188618871888188918901891189218931894189518961897189818991900190119021903190419051906190719081909191019111912191319141915191619171918191919201921192219231924192519261927192819291930193119321933193419351936193719381939194019411942194319441945194619471948194919501951195219531954195519561957195819591960196119621963196419651966196719681969197019711972197319741975197619771978197919801981198219831984198519861987198819891990199119921993199419951996199719981999200020012002200320042005200620072008200920102011201220132014201520162017201820192020202120222023202420252026202720282029203020312032203320342035203620372038203920402041204220432044204520462047204820492050205120522053205420552056205720582059206020612062206320642065206620672068206920702071207220732074207520762077207820792080208120822083208420852086208720882089209020912092209320942095209620972098209921002101210221032104210521062107210821092110211121122113211421152116211721182119212021212122212321242125212621272128212921302131213221332134213521362137213821392140214121422143214421452146214721482149215021512152215321542155215621572158215921602161216221632164216521662167216821692170217121722173217421752176217721782179218021812182218321842185218621872188218921902191219221932194219521962197219821992200220122022203220422052206220722082209221022112212221322142215221622172218221922202221222222232224222522262227222822292230223122322233223422352236223722382239224022412242224322442245224622472248224922502251225222532254225522562257225822592260226122622263226422652266226722682269227022712272227322742275227622772278227922802281228222832284228522862287228822892290229122922293229422952296229722982299230023012302230323042305230623072308230923102311231223132314231523162317231823192320232123222323232423252326232723282329233023312332233323342335233623372338233923402341234223432344234523462347234823492350235123522353235423552356235723582359236023612362236323642365236623672368236923702371237223732374237523762377237823792380238123822383238423852386238723882389239023912392239323942395239623972398239924002401240224032404240524062407240824092410
  1. <?php
  2. /**
  3. * Excel5 Parser
  4. *
  5. * @author Janson
  6. * @create 2017-11-27
  7. */
  8. namespace Asan\PHPExcel\Parser;
  9. use Asan\PHPExcel\Exception\ParserException;
  10. use Asan\PHPExcel\Parser\Excel5\OLERead;
  11. use Asan\PHPExcel\Parser\Excel5\RC4;
  12. class Excel5 {
  13. // ParseXL definitions
  14. const XLS_BIFF8 = 0x0600;
  15. const XLS_BIFF7 = 0x0500;
  16. const XLS_WORKBOOKGLOBALS = 0x0005;
  17. const XLS_WORKSHEET = 0x0010;
  18. // record identifiers
  19. const XLS_TYPE_FORMULA = 0x0006;
  20. const XLS_TYPE_EOF = 0x000a;
  21. const XLS_TYPE_DATEMODE = 0x0022;
  22. const XLS_TYPE_FILEPASS = 0x002f;
  23. const XLS_TYPE_CONTINUE = 0x003c;
  24. const XLS_TYPE_CODEPAGE = 0x0042;
  25. const XLS_TYPE_OBJ = 0x005d;
  26. const XLS_TYPE_SHEET = 0x0085;
  27. const XLS_TYPE_MULRK = 0x00bd;
  28. const XLS_TYPE_MULBLANK = 0x00be;
  29. const XLS_TYPE_XF = 0x00e0;
  30. const XLS_TYPE_SST = 0x00fc;
  31. const XLS_TYPE_LABELSST = 0x00fd;
  32. const XLS_TYPE_BLANK = 0x0201;
  33. const XLS_TYPE_NUMBER = 0x0203;
  34. const XLS_TYPE_LABEL = 0x0204;
  35. const XLS_TYPE_BOOLERR = 0x0205;
  36. const XLS_TYPE_STRING = 0x0207;
  37. const XLS_TYPE_ROW = 0x0208;
  38. const XLS_TYPE_INDEX = 0x020b;
  39. const XLS_TYPE_ARRAY = 0x0221;
  40. const XLS_TYPE_RK = 0x027e;
  41. const XLS_TYPE_FORMAT = 0x041e;
  42. const XLS_TYPE_BOF = 0x0809;
  43. // Encryption type
  44. const MS_BIFF_CRYPTO_NONE = 0;
  45. const MS_BIFF_CRYPTO_XOR = 1;
  46. const MS_BIFF_CRYPTO_RC4 = 2;
  47. // Size of stream blocks when using RC4 encryption
  48. const REKEY_BLOCK = 0x400;
  49. // Sheet state
  50. const SHEETSTATE_VISIBLE = 'visible';
  51. const SHEETSTATE_HIDDEN = 'hidden';
  52. const SHEETSTATE_VERYHIDDEN = 'veryHidden';
  53. private static $errorCode = [
  54. 0x00 => '#NULL!',
  55. 0x07 => '#DIV/0!',
  56. 0x0F => '#VALUE!',
  57. 0x17 => '#REF!',
  58. 0x1D => '#NAME?',
  59. 0x24 => '#NUM!',
  60. 0x2A => '#N/A'
  61. ];
  62. /**
  63. * Base calendar year to use for calculations
  64. *
  65. * @var int
  66. */
  67. private static $excelBaseDate = Format::CALENDAR_WINDOWS_1900;
  68. /**
  69. * Decimal separator
  70. *
  71. * @var string
  72. */
  73. private static $decimalSeparator;
  74. /**
  75. * Thousands separator
  76. *
  77. * @var string
  78. */
  79. private static $thousandsSeparator;
  80. /**
  81. * Currency code
  82. *
  83. * @var string
  84. */
  85. private static $currencyCode;
  86. /**
  87. * Workbook stream data
  88. *
  89. * @var string
  90. */
  91. private $data;
  92. /**
  93. * Size in bytes of $this->data
  94. *
  95. * @var int
  96. */
  97. private $dataSize;
  98. /**
  99. * Current position in stream
  100. *
  101. * @var integer
  102. */
  103. private $pos;
  104. /**
  105. * Worksheets
  106. *
  107. * @var array
  108. */
  109. private $sheets;
  110. /**
  111. * BIFF version
  112. *
  113. * @var int
  114. */
  115. private $version;
  116. /**
  117. * Codepage set in the Excel file being read. Only important for BIFF5 (Excel 5.0 - Excel 95)
  118. * For BIFF8 (Excel 97 - Excel 2003) this will always have the value 'UTF-16LE'
  119. *
  120. * @var string
  121. */
  122. private $codePage;
  123. /**
  124. * Row data
  125. *
  126. * @var array
  127. */
  128. private $row;
  129. /**
  130. * Shared formats
  131. *
  132. * @var array
  133. */
  134. private $formats;
  135. /**
  136. * The current sheet of the file
  137. *
  138. * @var int
  139. */
  140. private $sheetIndex = 0;
  141. /**
  142. * Ignore empty row
  143. *
  144. * @var bool
  145. */
  146. private $ignoreEmpty = false;
  147. /**
  148. * The current row index of the sheet
  149. *
  150. * @var int
  151. */
  152. private $rowIndex = 0;
  153. /**
  154. * Max column number
  155. *
  156. * @var int
  157. */
  158. private $columnLimit = 0;
  159. /**
  160. * Whether to the end of the row
  161. *
  162. * @var bool
  163. */
  164. private $eor = false;
  165. /**
  166. * Extended format record
  167. *
  168. * @var array
  169. */
  170. private $xfRecords = [];
  171. /**
  172. * Shared strings. Only applies to BIFF8.
  173. *
  174. * @var array
  175. */
  176. private $sst = [];
  177. /**
  178. * The type of encryption in use
  179. *
  180. * @var int
  181. */
  182. private $encryption = 0;
  183. /**
  184. * The position in the stream after which contents are encrypted
  185. *
  186. * @var int
  187. */
  188. private $encryptionStartPos = false;
  189. /**
  190. * The current RC4 decryption object
  191. *
  192. * @var RC4
  193. */
  194. private $rc4Key = null;
  195. /**
  196. * The position in the stream that the RC4 decryption object was left at
  197. *
  198. * @var int
  199. */
  200. private $rc4Pos = 0;
  201. /**
  202. * The current MD5 context state
  203. *
  204. * @var string
  205. */
  206. private $md5Ctxt = null;
  207. /**
  208. * Use OLE reader to extract the relevant data streams from the OLE file
  209. *
  210. * @param string $file
  211. */
  212. public function loadOLE($file) {
  213. $oleRead = new OLERead();
  214. $oleRead->read($file);
  215. $this->data = $oleRead->getStream($oleRead->workbook);
  216. }
  217. /**
  218. * Ignore empty row
  219. *
  220. * @param bool $ignoreEmpty
  221. *
  222. * @return $this
  223. */
  224. public function ignoreEmptyRow($ignoreEmpty) {
  225. $this->ignoreEmpty = $ignoreEmpty;
  226. return $this;
  227. }
  228. /**
  229. * Whether is ignore empty row
  230. *
  231. * @return bool
  232. */
  233. public function isIgnoreEmptyRow() {
  234. return $this->ignoreEmpty;
  235. }
  236. /**
  237. * Set sheet index
  238. *
  239. * @param int $index
  240. *
  241. * @return $this
  242. */
  243. public function setSheetIndex($index) {
  244. $this->sheetIndex = $index;
  245. return $this;
  246. }
  247. /**
  248. * Get sheet index
  249. *
  250. * @return int
  251. */
  252. public function getSheetIndex() {
  253. return $this->sheetIndex;
  254. }
  255. /**
  256. * Return worksheet info (Name, Last Column Letter, Last Column Index, Total Rows, Total Columns)
  257. *
  258. * @throws ParserException
  259. * @return array
  260. */
  261. public function parseWorksheetInfo() {
  262. if ($this->sheets === null) {
  263. // total byte size of Excel data (workbook global substream + sheet substreams)
  264. $this->dataSize = strlen($this->data);
  265. $this->pos = 0;
  266. $this->codePage = 'CP1252';
  267. $this->sheets = [];
  268. // Parse Workbook Global Substream
  269. while ($this->pos < $this->dataSize) {
  270. $code = Format::getUInt2d($this->data, $this->pos);
  271. switch ($code) {
  272. case self::XLS_TYPE_BOF:
  273. $this->readBof();
  274. break;
  275. case self::XLS_TYPE_FILEPASS:
  276. $this->readFilepass();
  277. break;
  278. case self::XLS_TYPE_CODEPAGE:
  279. $this->readCodepage();
  280. break;
  281. case self::XLS_TYPE_DATEMODE:
  282. $this->readDateMode();
  283. break;
  284. case self::XLS_TYPE_FORMAT:
  285. $this->readFormat();
  286. break;
  287. case self::XLS_TYPE_XF:
  288. $this->readXf();
  289. break;
  290. case self::XLS_TYPE_SST:
  291. $this->readSst();
  292. break;
  293. case self::XLS_TYPE_SHEET:
  294. $this->readSheet();
  295. break;
  296. case self::XLS_TYPE_EOF:
  297. $this->readDefault();
  298. break 2;
  299. default:
  300. $this->readDefault();
  301. break;
  302. }
  303. }
  304. // Parse the individual sheets
  305. foreach ($this->sheets as $key => $sheet) {
  306. if ($sheet['sheetType'] != 0x00) {
  307. // 0x00: Worksheet
  308. // 0x02: Chart
  309. // 0x06: Visual Basic module
  310. continue;
  311. }
  312. $sheet['lastColumnLetter'] = '';
  313. $sheet['lastColumnIndex'] = null;
  314. $sheet['totalRows'] = 0;
  315. $sheet['totalColumns'] = 0;
  316. $lastRowIndex = 0;
  317. $this->pos = $sheet['offset'];
  318. while ($this->pos <= $this->dataSize - 4) {
  319. $code = Format::getUInt2d($this->data, $this->pos);
  320. switch ($code) {
  321. case self::XLS_TYPE_RK:
  322. case self::XLS_TYPE_LABELSST:
  323. case self::XLS_TYPE_NUMBER:
  324. case self::XLS_TYPE_FORMULA:
  325. case self::XLS_TYPE_BOOLERR:
  326. case self::XLS_TYPE_LABEL:
  327. $length = Format::getUInt2d($this->data, $this->pos + 2);
  328. $recordData = substr($this->data, $this->pos + 4, $length);
  329. // move stream pointer to next record
  330. $this->pos += 4 + $length;
  331. $rowIndex = Format::getUInt2d($recordData, 0) + 1;
  332. $columnIndex = Format::getUInt2d($recordData, 2);
  333. if ($this->ignoreEmpty) {
  334. if ($lastRowIndex < $rowIndex) {
  335. $sheet['totalRows']++;
  336. }
  337. $lastRowIndex = $rowIndex;
  338. } else {
  339. $sheet['totalRows'] = max($sheet['totalRows'], $rowIndex);
  340. }
  341. $sheet['lastColumnIndex'] = max($columnIndex, $sheet['lastColumnIndex']);
  342. break;
  343. case self::XLS_TYPE_BOF:
  344. $this->readBof();
  345. break;
  346. case self::XLS_TYPE_EOF:
  347. $this->readDefault();
  348. break 2;
  349. default:
  350. $this->readDefault();
  351. break;
  352. }
  353. }
  354. if ($sheet['lastColumnIndex'] !== null) {
  355. $sheet['lastColumnLetter'] = Format::stringFromColumnIndex($sheet['lastColumnIndex']);
  356. } else {
  357. $sheet['lastColumnIndex'] = 0;
  358. }
  359. if ($sheet['lastColumnLetter']) {
  360. $sheet['totalColumns'] = $sheet['lastColumnIndex'] + 1;
  361. }
  362. $this->sheets[$key] = $sheet;
  363. }
  364. $this->pos = 0;
  365. }
  366. return $this->sheets;
  367. }
  368. /**
  369. * Get row data
  370. *
  371. * @param int $rowIndex
  372. * @param int $columnLimit
  373. *
  374. * @throws ParserException
  375. * @return array|bool
  376. */
  377. public function getRow($rowIndex, $columnLimit = 0) {
  378. $this->parseWorksheetInfo();
  379. // Rewind or change sheet
  380. if ($rowIndex === 0 || $this->pos < $this->sheets[$this->sheetIndex]['offset']) {
  381. $this->pos = $this->sheets[$this->sheetIndex]['offset'];
  382. }
  383. $endPos = $this->dataSize - 4;
  384. if (isset($this->sheets[$this->sheetIndex + 1]['offset'])) {
  385. $endPos = $this->sheets[$this->sheetIndex + 1]['offset'] - 4;
  386. }
  387. if ($this->pos >= $endPos) {
  388. return false;
  389. }
  390. $this->rowIndex = $rowIndex;
  391. $this->columnLimit = $columnLimit;
  392. $this->eor = false;
  393. $this->row = $columnLimit ? array_fill(0, $columnLimit, '') : [];
  394. while ($this->pos <= $endPos) {
  395. // Remember last position
  396. $lastPos = $this->pos;
  397. $code = Format::getUInt2d($this->data, $this->pos);
  398. switch ($code) {
  399. case self::XLS_TYPE_BOF:
  400. $this->readBof();
  401. break;
  402. case self::XLS_TYPE_RK:
  403. $this->readRk();
  404. break;
  405. case self::XLS_TYPE_LABELSST:
  406. $this->readLabelSst();
  407. break;
  408. case self::XLS_TYPE_MULRK:
  409. $this->readMulRk();
  410. break;
  411. case self::XLS_TYPE_NUMBER:
  412. $this->readNumber();
  413. break;
  414. case self::XLS_TYPE_FORMULA:
  415. $this->readFormula();
  416. break;
  417. case self::XLS_TYPE_BOOLERR:
  418. $this->readBoolErr();
  419. break;
  420. case self::XLS_TYPE_MULBLANK:
  421. case self::XLS_TYPE_BLANK:
  422. $this->readBlank();
  423. break;
  424. case self::XLS_TYPE_LABEL:
  425. $this->readLabel();
  426. break;
  427. case self::XLS_TYPE_EOF:
  428. $this->readDefault();
  429. break 2;
  430. default:
  431. $this->readDefault();
  432. break;
  433. }
  434. //End of row
  435. if ($this->eor) {
  436. //Recover current position
  437. $this->pos = $lastPos;
  438. break;
  439. }
  440. }
  441. return $this->row;
  442. }
  443. /**
  444. * Add cell data
  445. *
  446. * @param int $row
  447. * @param int $column
  448. * @param mixed $value
  449. * @param int $xfIndex
  450. * @return bool
  451. */
  452. private function addCell($row, $column, $value, $xfIndex) {
  453. if ($this->rowIndex != $row) {
  454. $this->eor = true;
  455. return false;
  456. }
  457. if (!$this->columnLimit || $column < $this->columnLimit) {
  458. $xfRecord = $this->xfRecords[$xfIndex];
  459. $this->row[$column] = self::toFormattedString($value, $xfRecord['format']);
  460. }
  461. return true;
  462. }
  463. /**
  464. * Read BOF
  465. *
  466. * @throws ParserException
  467. */
  468. private function readBof() {
  469. $length = Format::getUInt2d($this->data, $this->pos + 2);
  470. $recordData = substr($this->data, $this->pos + 4, $length);
  471. // move stream pointer to next record
  472. $this->pos += 4 + $length;
  473. // offset: 2; size: 2; type of the following data
  474. $substreamType = Format::getUInt2d($recordData, 2);
  475. switch ($substreamType) {
  476. case self::XLS_WORKBOOKGLOBALS:
  477. $version = Format::getUInt2d($recordData, 0);
  478. if (($version != self::XLS_BIFF8) && ($version != self::XLS_BIFF7)) {
  479. throw new ParserException('Cannot read this Excel file. Version is too old.', 1);
  480. }
  481. $this->version = $version;
  482. break;
  483. case self::XLS_WORKSHEET:
  484. // do not use this version information for anything
  485. // it is unreliable (OpenOffice doc, 5.8), use only version information from the global stream
  486. break;
  487. default:
  488. // substream, e.g. chart
  489. // just skip the entire substream
  490. do {
  491. $code = Format::getUInt2d($this->data, $this->pos);
  492. $this->readDefault();
  493. } while ($code != self::XLS_TYPE_EOF && $this->pos < $this->dataSize);
  494. break;
  495. }
  496. }
  497. /**
  498. * SHEET
  499. *
  500. * This record is located in the Workbook Globals Substream and represents a sheet inside the workbook.
  501. * One SHEET record is written for each sheet. It stores the sheet name and a stream offset to the BOF
  502. * record of the respective Sheet Substream within the Workbook Stream.
  503. */
  504. private function readSheet() {
  505. $length = Format::getUInt2d($this->data, $this->pos + 2);
  506. $recordData = substr($this->data, $this->pos + 4, $length);
  507. // offset: 0; size: 4; absolute stream position of the BOF record of the sheet
  508. // NOTE: not encrypted
  509. $offset = Format::getInt4d($this->data, $this->pos + 4);
  510. // move stream pointer to next record
  511. $this->pos += 4 + $length;
  512. // offset: 4; size: 1; sheet state
  513. switch (ord($recordData{4})) {
  514. case 0x00:
  515. $sheetState = self::SHEETSTATE_VISIBLE;
  516. break;
  517. case 0x01:
  518. $sheetState = self::SHEETSTATE_HIDDEN;
  519. break;
  520. case 0x02:
  521. $sheetState = self::SHEETSTATE_VERYHIDDEN;
  522. break;
  523. default:
  524. $sheetState = self::SHEETSTATE_VISIBLE;
  525. break;
  526. }
  527. // offset: 5; size: 1; sheet type
  528. $sheetType = ord($recordData{5});
  529. // offset: 6; size: var; sheet name
  530. $name = '';
  531. if ($this->version == self::XLS_BIFF8) {
  532. $string = self::readUnicodeStringShort(substr($recordData, 6));
  533. $name = $string['value'];
  534. } elseif ($this->version == self::XLS_BIFF7) {
  535. $string = $this->readByteStringShort(substr($recordData, 6));
  536. $name = $string['value'];
  537. }
  538. // ignore hidden sheet
  539. if ($sheetState == self::SHEETSTATE_VISIBLE) {
  540. $this->sheets[] = [
  541. 'name' => $name, 'offset' => $offset, 'sheetState' => $sheetState, 'sheetType' => $sheetType
  542. ];
  543. }
  544. }
  545. /**
  546. * Reads a general type of BIFF record.
  547. * Does nothing except for moving stream pointer forward to next record.
  548. */
  549. private function readDefault() {
  550. $length = Format::getUInt2d($this->data, $this->pos + 2);
  551. //$recordData = $this->readRecordData($this->data, $this->pos + 4, $length);
  552. // move stream pointer to next record
  553. $this->pos += 4 + $length;
  554. }
  555. /**
  556. * FILEPASS
  557. *
  558. * This record is part of the File Protection Block. It contains information about the read/write password of
  559. * the file. All record contents following this record will be encrypted.
  560. * The decryption functions and objects used from here on in are based on the source of Spreadsheet-ParseExcel:
  561. * http://search.cpan.org/~jmcnamara/Spreadsheet-ParseExcel/
  562. *
  563. * @throws ParserException
  564. */
  565. private function readFilepass() {
  566. $length = Format::getUInt2d($this->data, $this->pos + 2);
  567. if ($length != 54) {
  568. throw new ParserException('Unexpected file pass record length', 2);
  569. }
  570. $recordData = $this->readRecordData($this->data, $this->pos + 4, $length);
  571. // move stream pointer to next record
  572. $this->pos += 4 + $length;
  573. if (!$this->verifyPassword('VelvetSweatshop', substr($recordData, 6, 16), substr($recordData, 22, 16),
  574. substr($recordData, 38, 16), $this->md5Ctxt)) {
  575. throw new ParserException('Decryption password incorrect', 3);
  576. }
  577. $this->encryption = self::MS_BIFF_CRYPTO_RC4;
  578. // Decryption required from the record after next onwards
  579. $this->encryptionStartPos = $this->pos + Format::getUInt2d($this->data, $this->pos + 2);
  580. }
  581. /**
  582. * Read record data from stream, decrypting as required
  583. *
  584. * @param string $data Data stream to read from
  585. * @param int $pos Position to start reading from
  586. * @param int $len Record data length
  587. *
  588. * @throws ParserException
  589. * @return string Record data
  590. */
  591. private function readRecordData($data, $pos, $len) {
  592. $data = substr($data, $pos, $len);
  593. // File not encrypted, or record before encryption start point
  594. if ($this->encryption == self::MS_BIFF_CRYPTO_NONE || $pos < $this->encryptionStartPos) {
  595. return $data;
  596. }
  597. $recordData = '';
  598. if ($this->encryption == self::MS_BIFF_CRYPTO_RC4) {
  599. $oldBlock = floor($this->rc4Pos / self::REKEY_BLOCK);
  600. $block = floor($pos / self::REKEY_BLOCK);
  601. $endBlock = floor(($pos + $len) / self::REKEY_BLOCK);
  602. // Spin an RC4 decryptor to the right spot. If we have a decryptor sitting
  603. // at a point earlier in the current block, re-use it as we can save some time.
  604. if ($block != $oldBlock || $pos < $this->rc4Pos || !$this->rc4Key) {
  605. $this->rc4Key = $this->makeKey($block, $this->md5Ctxt);
  606. $step = $pos % self::REKEY_BLOCK;
  607. } else {
  608. $step = $pos - $this->rc4Pos;
  609. }
  610. $this->rc4Key->RC4(str_repeat("\0", $step));
  611. // Decrypt record data (re-keying at the end of every block)
  612. while ($block != $endBlock) {
  613. $step = self::REKEY_BLOCK - ($pos % self::REKEY_BLOCK);
  614. $recordData .= $this->rc4Key->RC4(substr($data, 0, $step));
  615. $data = substr($data, $step);
  616. $pos += $step;
  617. $len -= $step;
  618. $block++;
  619. $this->rc4Key = $this->makeKey($block, $this->md5Ctxt);
  620. }
  621. $recordData .= $this->rc4Key->RC4(substr($data, 0, $len));
  622. // Keep track of the position of this decryptor.
  623. // We'll try and re-use it later if we can to speed things up
  624. $this->rc4Pos = $pos + $len;
  625. } elseif ($this->encryption == self::MS_BIFF_CRYPTO_XOR) {
  626. throw new ParserException('XOr encryption not supported', 4);
  627. }
  628. return $recordData;
  629. }
  630. /**
  631. * Make an RC4 decryptor for the given block
  632. *
  633. * @param int $block Block for which to create decrypto
  634. * @param string $valContext MD5 context state
  635. *
  636. * @return RC4
  637. */
  638. private function makeKey($block, $valContext) {
  639. $pw = str_repeat("\0", 64);
  640. for ($i = 0; $i < 5; $i++) {
  641. $pw[$i] = $valContext[$i];
  642. }
  643. $pw[5] = chr($block & 0xff);
  644. $pw[6] = chr(($block >> 8) & 0xff);
  645. $pw[7] = chr(($block >> 16) & 0xff);
  646. $pw[8] = chr(($block >> 24) & 0xff);
  647. $pw[9] = "\x80";
  648. $pw[56] = "\x48";
  649. return new RC4(md5($pw));
  650. }
  651. /**
  652. * Verify RC4 file password
  653. *
  654. * @var string $password Password to check
  655. * @var string $docid Document id
  656. * @var string $salt_data Salt data
  657. * @var string $hashedsalt_data Hashed salt data
  658. * @var string &$valContext Set to the MD5 context of the value
  659. *
  660. * @return bool Success
  661. */
  662. private function verifyPassword($password, $docid, $salt_data, $hashedsalt_data, &$valContext) {
  663. $pw = str_repeat("\0", 64);
  664. for ($i = 0; $i < strlen($password); $i++) {
  665. $o = ord(substr($password, $i, 1));
  666. $pw[2 * $i] = chr($o & 0xff);
  667. $pw[2 * $i + 1] = chr(($o >> 8) & 0xff);
  668. }
  669. $pw[2 * $i] = chr(0x80);
  670. $pw[56] = chr(($i << 4) & 0xff);
  671. $mdContext1 = md5($pw);
  672. $offset = 0;
  673. $keyOffset = 0;
  674. $toCopy = 5;
  675. while ($offset != 16) {
  676. if ((64 - $offset) < 5) {
  677. $toCopy = 64 - $offset;
  678. }
  679. for ($i = 0; $i <= $toCopy; $i++) {
  680. $pw[$offset + $i] = $mdContext1[$keyOffset + $i];
  681. }
  682. $offset += $toCopy;
  683. if ($offset == 64) {
  684. $keyOffset = $toCopy;
  685. $toCopy = 5 - $toCopy;
  686. $offset = 0;
  687. continue;
  688. }
  689. $keyOffset = 0;
  690. $toCopy = 5;
  691. for ($i = 0; $i < 16; $i++) {
  692. $pw[$offset + $i] = $docid[$i];
  693. }
  694. $offset += 16;
  695. }
  696. $pw[16] = "\x80";
  697. for ($i = 0; $i < 47; $i++) {
  698. $pw[17 + $i] = "\0";
  699. }
  700. $pw[56] = "\x80";
  701. $pw[57] = "\x0a";
  702. $valContext = md5($pw);
  703. $key = $this->makeKey(0, $valContext);
  704. $salt = $key->RC4($salt_data);
  705. $hashedsalt = $key->RC4($hashedsalt_data);
  706. $salt .= "\x80" . str_repeat("\0", 47);
  707. $salt[56] = "\x80";
  708. $mdContext2 = md5($salt);
  709. return $mdContext2 == $hashedsalt;
  710. }
  711. /**
  712. * CODEPAGE
  713. *
  714. * This record stores the text encoding used to write byte strings, stored as MS Windows code page identifier.
  715. *
  716. * @throws ParserException
  717. */
  718. private function readCodepage() {
  719. $length = Format::getUInt2d($this->data, $this->pos + 2);
  720. $recordData = substr($this->data, $this->pos + 4, $length);
  721. // move stream pointer to next record
  722. $this->pos += 4 + $length;
  723. // offset: 0; size: 2; code page identifier
  724. $codePage = Format::getUInt2d($recordData, 0);
  725. $this->codePage = self::NumberToName($codePage);
  726. }
  727. /**
  728. * DATEMODE
  729. * This record specifies the base date for displaying date values. All dates are stored as count of days
  730. * past this base date. In BIFF2-BIFF4 this record is part of the Calculation Settings Block. In BIFF5-BIFF8
  731. * it is stored in the Workbook Globals Substream.
  732. */
  733. private function readDateMode() {
  734. $length = Format::getUInt2d($this->data, $this->pos + 2);
  735. $recordData = substr($this->data, $this->pos + 4, $length);
  736. // move stream pointer to next record
  737. $this->pos += 4 + $length;
  738. // offset: 0; size: 2; 0 = base 1900, 1 = base 1904
  739. self::$excelBaseDate = Format::CALENDAR_WINDOWS_1900;
  740. if (ord($recordData{0}) == 1) {
  741. self::$excelBaseDate = Format::CALENDAR_MAC_1904;
  742. }
  743. }
  744. /**
  745. * FORMAT
  746. *
  747. * This record contains information about a number format. All FORMAT records occur together in a sequential list.
  748. * In BIFF2-BIFF4 other records referencing a FORMAT record contain a zero-based index into this list. From BIFF5
  749. * on the FORMAT record contains the index itself that will be used by other records.
  750. */
  751. private function readFormat() {
  752. $length = Format::getUInt2d($this->data, $this->pos + 2);
  753. $recordData = substr($this->data, $this->pos + 4, $length);
  754. // move stream pointer to next record
  755. $this->pos += 4 + $length;
  756. $indexCode = Format::getUInt2d($recordData, 0);
  757. if ($this->version == self::XLS_BIFF8) {
  758. $string = self::readUnicodeStringLong(substr($recordData, 2));
  759. } else {
  760. // BIFF7
  761. $string = $this->readByteStringShort(substr($recordData, 2));
  762. }
  763. $formatString = $string['value'];
  764. $this->formats[$indexCode] = $formatString;
  765. }
  766. /**
  767. * XF - Extended Format
  768. *
  769. * This record contains formatting information for cells, rows, columns or styles.
  770. * According to http://support.microsoft.com/kb/147732 there are always at least 15 cell style XF and 1 cell XF.
  771. * Inspection of Excel files generated by MS Office Excel shows that XF records 0-14 are cell style XF and XF
  772. * record 15 is a cell XF. We only read the first cell style XF and skip the remaining cell style XF records
  773. * We read all cell XF records.
  774. */
  775. private function readXf() {
  776. $length = Format::getUInt2d($this->data, $this->pos + 2);
  777. $recordData = substr($this->data, $this->pos + 4, $length);
  778. // move stream pointer to next record
  779. $this->pos += 4 + $length;
  780. // offset: 2; size: 2; Index to FORMAT record
  781. $numberFormatIndex = Format::getUInt2d($recordData, 2);
  782. if (isset($this->formats[$numberFormatIndex])) {
  783. // then we have user-defined format code
  784. $numberFormat = $this->formats[$numberFormatIndex];
  785. } elseif (isset(Format::$buildInFormats[$numberFormatIndex])) {
  786. // then we have built-in format code
  787. $numberFormat = Format::$buildInFormats[$numberFormatIndex];
  788. } else {
  789. // we set the general format code
  790. $numberFormat = Format::FORMAT_GENERAL;
  791. }
  792. $this->xfRecords[] = ['index' => $numberFormatIndex, 'format' => $numberFormat];
  793. }
  794. /**
  795. * SST - Shared String Table
  796. *
  797. * This record contains a list of all strings used anywhere in the workbook. Each string occurs only once.
  798. * The workbook uses indexes into the list to reference the strings.
  799. **/
  800. private function readSst() {
  801. // offset within (spliced) record data
  802. $pos = 0;
  803. // get spliced record data
  804. $splicedRecordData = $this->getSplicedRecordData();
  805. $recordData = $splicedRecordData['recordData'];
  806. $spliceOffsets = $splicedRecordData['spliceOffsets'];
  807. // offset: 0; size: 4; total number of strings in the workbook
  808. $pos += 4;
  809. // offset: 4; size: 4; number of following strings ($nm)
  810. $nm = Format::getInt4d($recordData, 4);
  811. $pos += 4;
  812. // loop through the Unicode strings (16-bit length)
  813. for ($i = 0; $i < $nm; ++$i) {
  814. if (!isset($recordData[$pos + 2])) {
  815. break;
  816. }
  817. // number of characters in the Unicode string
  818. $numChars = Format::getUInt2d($recordData, $pos);
  819. $pos += 2;
  820. // option flags
  821. $optionFlags = ord($recordData[$pos]);
  822. ++$pos;
  823. // bit: 0; mask: 0x01; 0 = compressed; 1 = uncompressed
  824. $isCompressed = (($optionFlags & 0x01) == 0) ;
  825. // bit: 2; mask: 0x02; 0 = ordinary; 1 = Asian phonetic
  826. $hasAsian = (($optionFlags & 0x04) != 0);
  827. // bit: 3; mask: 0x03; 0 = ordinary; 1 = Rich-Text
  828. $formattingRuns = 0;
  829. $hasRichText = (($optionFlags & 0x08) != 0);
  830. if ($hasRichText && isset($recordData[$pos])) {
  831. // number of Rich-Text formatting runs
  832. $formattingRuns = Format::getUInt2d($recordData, $pos);
  833. $pos += 2;
  834. }
  835. $extendedRunLength = 0;
  836. if ($hasAsian && isset($recordData[$pos])) {
  837. // size of Asian phonetic setting
  838. $extendedRunLength = Format::getInt4d($recordData, $pos);
  839. $pos += 4;
  840. }
  841. // expected byte length of character array if not split
  842. $len = ($isCompressed) ? $numChars : $numChars * 2;
  843. // look up limit position
  844. $limitPos = 0;
  845. foreach ($spliceOffsets as $spliceOffset) {
  846. // it can happen that the string is empty, therefore we need
  847. // <= and not just <
  848. if ($pos <= $spliceOffset) {
  849. $limitPos = $spliceOffset;
  850. break;
  851. }
  852. }
  853. if ($pos + $len <= $limitPos) {
  854. // character array is not split between records
  855. $retStr = substr($recordData, $pos, $len);
  856. $pos += $len;
  857. } else {
  858. // character array is split between records
  859. // first part of character array
  860. $retStr = substr($recordData, $pos, $limitPos - $pos);
  861. $bytesRead = $limitPos - $pos;
  862. // remaining characters in Unicode string
  863. $charsLeft = $numChars - (($isCompressed) ? $bytesRead : ($bytesRead / 2));
  864. $pos = $limitPos;
  865. // keep reading the characters
  866. while ($charsLeft > 0) {
  867. // look up next limit position, in case the string span more than one continue record
  868. foreach ($spliceOffsets as $spliceOffset) {
  869. if ($pos < $spliceOffset) {
  870. $limitPos = $spliceOffset;
  871. break;
  872. }
  873. }
  874. if (!isset($recordData[$pos])) {
  875. break;
  876. }
  877. // repeated option flags
  878. // OpenOffice.org documentation 5.21
  879. $option = ord($recordData[$pos]);
  880. ++$pos;
  881. if ($isCompressed && ($option == 0)) {
  882. // 1st fragment compressed
  883. // this fragment compressed
  884. $len = min($charsLeft, $limitPos - $pos);
  885. $retStr .= substr($recordData, $pos, $len);
  886. $charsLeft -= $len;
  887. $isCompressed = true;
  888. } elseif (!$isCompressed && ($option != 0)) {
  889. // 1st fragment uncompressed
  890. // 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
  897. // this fragment compressed
  898. $len = min($charsLeft, $limitPos - $pos);
  899. for ($j = 0; $j < $len; ++$j) {
  900. if (!isset($recordData[$pos + $j])) {
  901. break;
  902. }
  903. $retStr .= $recordData[$pos + $j] . chr(0);
  904. }
  905. $charsLeft -= $len;
  906. $isCompressed = false;
  907. } else {
  908. // 1st fragment compressed
  909. // this fragment uncompressed
  910. $newStr = '';
  911. $jMax = strlen($retStr);
  912. for ($j = 0; $j < $jMax; ++$j) {
  913. $newStr .= $retStr[$j] . chr(0);
  914. }
  915. $retStr = $newStr;
  916. $len = min($charsLeft * 2, $limitPos - $pos);
  917. $retStr .= substr($recordData, $pos, $len);
  918. $charsLeft -= $len / 2;
  919. $isCompressed = false;
  920. }
  921. $pos += $len;
  922. }
  923. }
  924. // convert to UTF-8
  925. $retStr = self::encodeUTF16($retStr, $isCompressed);
  926. // read additional Rich-Text information, if any
  927. // $fmtRuns = [];
  928. if ($hasRichText) {
  929. // list of formatting runs
  930. /*for ($j = 0; $j < $formattingRuns; ++$j) {
  931. // first formatted character; zero-based
  932. $charPos = Format::getUInt2d($recordData, $pos + $j * 4);
  933. // index to font record
  934. $fontIndex = Format::getUInt2d($recordData, $pos + 2 + $j * 4);
  935. $fmtRuns[] = ['charPos' => $charPos, 'fontIndex' => $fontIndex];
  936. }*/
  937. $pos += 4 * $formattingRuns;
  938. }
  939. // read additional Asian phonetics information, if any
  940. if ($hasAsian) {
  941. // For Asian phonetic settings, we skip the extended string data
  942. $pos += $extendedRunLength;
  943. }
  944. // store the shared sting
  945. $this->sst[] = ['value' => $retStr];
  946. }
  947. }
  948. /**
  949. * Read RK record
  950. *
  951. * This record represents a cell that contains an RK value (encoded integer or floating-point value). If a
  952. * floating-point value cannot be encoded to an RK value, a NUMBER record will be written. This record replaces
  953. * the record INTEGER written in BIFF2.
  954. */
  955. private function readRk() {
  956. $length = Format::getUInt2d($this->data, $this->pos + 2);
  957. $recordData = substr($this->data, $this->pos + 4, $length);
  958. // move stream pointer to next record
  959. $this->pos += 4 + $length;
  960. // offset: 0; size: 2; index to row
  961. $row = Format::getUInt2d($recordData, 0);
  962. // offset: 2; size: 2; index to column
  963. $column = Format::getUInt2d($recordData, 2);
  964. // offset: 4; size: 2; index to XF record
  965. $xfIndex = Format::getUInt2d($recordData, 4);
  966. // offset: 6; size: 4; RK value
  967. $rkNum = Format::getInt4d($recordData, 6);
  968. $numValue = self::getIEEE754($rkNum);
  969. // add cell
  970. $this->addCell($row, $column, $numValue, $xfIndex);
  971. }
  972. /**
  973. * Read LABELSST record
  974. *
  975. * This record represents a cell that contains a string. It replaces the LABEL record and RSTRING record used in
  976. * BIFF2-BIFF5.
  977. */
  978. private function readLabelSst() {
  979. $length = Format::getUInt2d($this->data, $this->pos + 2);
  980. $recordData = substr($this->data, $this->pos + 4, $length);
  981. $this->pos += 4 + $length;
  982. $xfIndex = Format::getUInt2d($recordData, 4);
  983. $row = Format::getUInt2d($recordData, 0);
  984. $column = Format::getUInt2d($recordData, 2);
  985. // offset: 6; size: 4; index to SST record
  986. $index = Format::getInt4d($recordData, 6);
  987. $this->addCell($row, $column, $this->sst[$index]['value'], $xfIndex);
  988. }
  989. /**
  990. * Read MULRK record
  991. *
  992. * This record represents a cell range containing RK value cells. All cells are located in the same row.
  993. */
  994. private function readMulRk() {
  995. $length = Format::getUInt2d($this->data, $this->pos + 2);
  996. $recordData = substr($this->data, $this->pos + 4, $length);
  997. // move stream pointer to next record
  998. $this->pos += 4 + $length;
  999. // offset: 0; size: 2; index to row
  1000. $row = Format::getUInt2d($recordData, 0);
  1001. // offset: 2; size: 2; index to first column
  1002. $colFirst = Format::getUInt2d($recordData, 2);
  1003. // offset: var; size: 2; index to last column
  1004. $colLast = Format::getUInt2d($recordData, $length - 2);
  1005. $columns = $colLast - $colFirst + 1;
  1006. // offset within record data
  1007. $offset = 4;
  1008. for ($i = 0; $i < $columns; ++$i) {
  1009. // offset: var; size: 2; index to XF record
  1010. $xfIndex = Format::getUInt2d($recordData, $offset);
  1011. // offset: var; size: 4; RK value
  1012. $numValue = self::getIEEE754(Format::getInt4d($recordData, $offset + 2));
  1013. $this->addCell($row, $colFirst + $i, $numValue, $xfIndex);
  1014. $offset += 6;
  1015. }
  1016. }
  1017. /**
  1018. * Read NUMBER record
  1019. *
  1020. * This record represents a cell that contains a floating-point value.
  1021. */
  1022. private function readNumber() {
  1023. $length = Format::getUInt2d($this->data, $this->pos + 2);
  1024. $recordData = substr($this->data, $this->pos + 4, $length);
  1025. // move stream pointer to next record
  1026. $this->pos += 4 + $length;
  1027. // offset: 0; size: 2; index to row
  1028. $row = Format::getUInt2d($recordData, 0);
  1029. // offset: 2; size 2; index to column
  1030. $column = Format::getUInt2d($recordData, 2);
  1031. // offset 4; size: 2; index to XF record
  1032. $xfIndex = Format::getUInt2d($recordData, 4);
  1033. $numValue = self::extractNumber(substr($recordData, 6, 8));
  1034. $this->addCell($row, $column, $numValue, $xfIndex);
  1035. }
  1036. /**
  1037. * Read FORMULA record + perhaps a following STRING record if formula result is a string
  1038. * This record contains the token array and the result of a formula cell.
  1039. */
  1040. private function readFormula() {
  1041. $length = Format::getUInt2d($this->data, $this->pos + 2);
  1042. $recordData = substr($this->data, $this->pos + 4, $length);
  1043. // move stream pointer to next record
  1044. $this->pos += 4 + $length;
  1045. // offset: 0; size: 2; row index
  1046. $row = Format::getUInt2d($recordData, 0);
  1047. // offset: 2; size: 2; col index
  1048. $column = Format::getUInt2d($recordData, 2);
  1049. // offset 4; size: 2; index to XF record
  1050. $xfIndex = Format::getUInt2d($recordData, 4);
  1051. // offset: 6; size: 8; result of the formula
  1052. if ((ord($recordData{6}) == 0) && (ord($recordData{12}) == 255) && (ord($recordData{13}) == 255)) {
  1053. // read STRING record
  1054. $value = $this->readString();
  1055. } elseif ((ord($recordData{6}) == 1) && (ord($recordData{12}) == 255) && (ord($recordData{13}) == 255)) {
  1056. // Boolean formula. Result is in +2; 0=false, 1=true
  1057. $value = (bool) ord($recordData{8});
  1058. } elseif ((ord($recordData{6}) == 2) && (ord($recordData{12}) == 255) && (ord($recordData{13}) == 255)) {
  1059. // Error formula. Error code is in +2
  1060. $value = self::mapErrorCode(ord($recordData{8}));
  1061. } elseif ((ord($recordData{6}) == 3) && (ord($recordData{12}) == 255) && (ord($recordData{13}) == 255)) {
  1062. // Formula result is a null string
  1063. $value = '';
  1064. } else {
  1065. // forumla result is a number, first 14 bytes like _NUMBER record
  1066. $value = self::extractNumber(substr($recordData, 6, 8));
  1067. }
  1068. $this->addCell($row, $column, $value, $xfIndex);
  1069. }
  1070. /**
  1071. * Read a STRING record from current stream position and advance the stream pointer to next record.
  1072. * This record is used for storing result from FORMULA record when it is a string, and it occurs
  1073. * directly after the FORMULA record
  1074. *
  1075. * @return string The string contents as UTF-8
  1076. */
  1077. private function readString() {
  1078. $length = Format::getUInt2d($this->data, $this->pos + 2);
  1079. $recordData = substr($this->data, $this->pos + 4, $length);
  1080. // move stream pointer to next record
  1081. $this->pos += 4 + $length;
  1082. if ($this->version == self::XLS_BIFF8) {
  1083. $string = self::readUnicodeStringLong($recordData);
  1084. $value = $string['value'];
  1085. } else {
  1086. $string = $this->readByteStringLong($recordData);
  1087. $value = $string['value'];
  1088. }
  1089. return $value;
  1090. }
  1091. /**
  1092. * Read BOOLERR record
  1093. *
  1094. * This record represents a Boolean value or error value cell.
  1095. */
  1096. private function readBoolErr() {
  1097. $length = Format::getUInt2d($this->data, $this->pos + 2);
  1098. $recordData = substr($this->data, $this->pos + 4, $length);
  1099. // move stream pointer to next record
  1100. $this->pos += 4 + $length;
  1101. // offset: 0; size: 2; row index
  1102. $row = Format::getUInt2d($recordData, 0);
  1103. // offset: 2; size: 2; column index
  1104. $column = Format::getUInt2d($recordData, 2);
  1105. // offset: 4; size: 2; index to XF record
  1106. $xfIndex = Format::getUInt2d($recordData, 4);
  1107. // offset: 6; size: 1; the boolean value or error value
  1108. $boolError = ord($recordData{6});
  1109. // offset: 7; size: 1; 0=boolean; 1=error
  1110. $isError = ord($recordData{7});
  1111. switch ($isError) {
  1112. case 0: // boolean
  1113. $value = (bool)$boolError;
  1114. // add cell value
  1115. $this->addCell($row, $column, $value, $xfIndex);
  1116. break;
  1117. case 1: // error type
  1118. $value = self::mapErrorCode($boolError);
  1119. // add cell value
  1120. $this->addCell($row, $column, $value, $xfIndex);
  1121. break;
  1122. }
  1123. }
  1124. /**
  1125. * Read BLANK record
  1126. */
  1127. private function readBlank() {
  1128. $length = Format::getUInt2d($this->data, $this->pos + 2);
  1129. $recordData = substr($this->data, $this->pos + 4, $length);
  1130. // move stream pointer to next record
  1131. $this->pos += 4 + $length;
  1132. // offset: 0; size: 2; row index
  1133. $row = Format::getUInt2d($recordData, 0);
  1134. // offset: 2; size: 2; col index
  1135. $column = Format::getUInt2d($recordData, 2);
  1136. // offset: 4; size: 2; XF index
  1137. $xfIndex = Format::getUInt2d($recordData, 4);
  1138. $this->addCell($row, $column, '', $xfIndex);
  1139. }
  1140. /**
  1141. * Read LABEL record
  1142. *
  1143. * This record represents a cell that contains a string. In BIFF8 it is usually replaced by the LABELSST record.
  1144. * Excel still uses this record, if it copies unformatted text cells to the clipboard.
  1145. */
  1146. private function readLabel() {
  1147. $length = Format::getUInt2d($this->data, $this->pos + 2);
  1148. $recordData = substr($this->data, $this->pos + 4, $length);
  1149. // move stream pointer to next record
  1150. $this->pos += 4 + $length;
  1151. // offset: 0; size: 2; index to row
  1152. $row = Format::getUInt2d($recordData, 0);
  1153. // offset: 2; size: 2; index to column
  1154. $column = Format::getUInt2d($recordData, 2);
  1155. // offset: 4; size: 2; XF index
  1156. $xfIndex = Format::getUInt2d($recordData, 4);
  1157. // add cell value
  1158. if ($this->version == self::XLS_BIFF8) {
  1159. $string = self::readUnicodeStringLong(substr($recordData, 6));
  1160. $value = $string['value'];
  1161. } else {
  1162. $string = $this->readByteStringLong(substr($recordData, 6));
  1163. $value = $string['value'];
  1164. }
  1165. $this->addCell($row, $column, $value, $xfIndex);
  1166. }
  1167. /**
  1168. * Map error code, e.g. '#N/A'
  1169. *
  1170. * @param int $code
  1171. * @return string
  1172. */
  1173. private static function mapErrorCode($code) {
  1174. if (isset(self::$errorCode[$code])) {
  1175. return self::$errorCode[$code];
  1176. }
  1177. return false;
  1178. }
  1179. /**
  1180. * Convert a value in a pre-defined format to a PHP string
  1181. *
  1182. * @param mixed $value Value to format
  1183. * @param string $format Format code
  1184. * @return string
  1185. */
  1186. private static function toFormattedString($value = '0', $format = Format::FORMAT_GENERAL) {
  1187. // For now we do not treat strings although section 4 of a format code affects strings
  1188. if (!is_numeric($value)) {
  1189. return $value;
  1190. }
  1191. // For 'General' format code, we just pass the value although this is not entirely the way Excel does it,
  1192. // it seems to round numbers to a total of 10 digits.
  1193. if (($format === Format::FORMAT_GENERAL) || ($format === Format::FORMAT_TEXT)) {
  1194. return $value;
  1195. }
  1196. // Convert any other escaped characters to quoted strings, e.g. (\T to "T")
  1197. $format = preg_replace('/(\\\(.))(?=(?:[^"]|"[^"]*")*$)/u', '"${2}"', $format);
  1198. // Get the sections, there can be up to four sections, separated with a semi-colon (but only if not a quoted literal)
  1199. $sections = preg_split('/(;)(?=(?:[^"]|"[^"]*")*$)/u', $format);
  1200. // Extract the relevant section depending on whether number is positive, negative, or zero?
  1201. // Text not supported yet.
  1202. // Here is how the sections apply to various values in Excel:
  1203. // 1 section: [POSITIVE/NEGATIVE/ZERO/TEXT]
  1204. // 2 sections: [POSITIVE/ZERO/TEXT] [NEGATIVE]
  1205. // 3 sections: [POSITIVE/TEXT] [NEGATIVE] [ZERO]
  1206. // 4 sections: [POSITIVE] [NEGATIVE] [ZERO] [TEXT]
  1207. switch (count($sections)) {
  1208. case 1:
  1209. $format = $sections[0];
  1210. break;
  1211. case 2:
  1212. $format = ($value >= 0) ? $sections[0] : $sections[1];
  1213. $value = abs($value); // Use the absolute value
  1214. break;
  1215. case 3:
  1216. $format = ($value > 0) ? $sections[0] : ( ($value < 0) ? $sections[1] : $sections[2]);
  1217. $value = abs($value); // Use the absolute value
  1218. break;
  1219. case 4:
  1220. $format = ($value > 0) ? $sections[0] : ( ($value < 0) ? $sections[1] : $sections[2]);
  1221. $value = abs($value); // Use the absolute value
  1222. break;
  1223. default:
  1224. // something is wrong, just use first section
  1225. $format = $sections[0];
  1226. break;
  1227. }
  1228. // In Excel formats, "_" is used to add spacing,
  1229. // The following character indicates the size of the spacing, which we can't do in HTML, so we just use a standard space
  1230. $format = preg_replace('/_./', ' ', $format);
  1231. // Save format with color information for later use below
  1232. //$formatColor = $format;
  1233. // Strip color information
  1234. $colorRegex = '/^\\[[a-zA-Z]+\\]/';
  1235. $format = preg_replace($colorRegex, '', $format);
  1236. // Let's begin inspecting the format and converting the value to a formatted string
  1237. // Check for date/time characters (not inside quotes)
  1238. if (preg_match('/(\[\$[A-Z]*-[0-9A-F]*\])*[hmsdy](?=(?:[^"]|"[^"]*")*$)/miu', $format, $matches)) {
  1239. // datetime format
  1240. self::formatAsDate($value, $format);
  1241. } elseif (preg_match('/%$/', $format)) {
  1242. // % number format
  1243. self::formatAsPercentage($value, $format);
  1244. } else {
  1245. if ($format === Format::FORMAT_CURRENCY_EUR_SIMPLE) {
  1246. $value = 'EUR ' . sprintf('%1.2f', $value);
  1247. } else {
  1248. // Some non-number strings are quoted, so we'll get rid of the quotes, likewise any positional * symbols
  1249. $format = str_replace(['"', '*'], '', $format);
  1250. // Find out if we need thousands separator
  1251. // This is indicated by a comma enclosed by a digit placeholder:
  1252. // #,# or 0,0
  1253. $useThousands = preg_match('/(#,#|0,0)/', $format);
  1254. if ($useThousands) {
  1255. $format = preg_replace('/0,0/', '00', $format);
  1256. $format = preg_replace('/#,#/', '##', $format);
  1257. }
  1258. // Scale thousands, millions,...
  1259. // This is indicated by a number of commas after a digit placeholder:
  1260. // #, or 0.0,,
  1261. $scale = 1; // same as no scale
  1262. $matches = [];
  1263. if (preg_match('/(#|0)(,+)/', $format, $matches)) {
  1264. $scale = pow(1000, strlen($matches[2]));
  1265. // strip the commas
  1266. $format = preg_replace('/0,+/', '0', $format);
  1267. $format = preg_replace('/#,+/', '#', $format);
  1268. }
  1269. if (preg_match('/#?.*\?\/\?/', $format, $m)) {
  1270. //echo 'Format mask is fractional '.$format.' <br />';
  1271. if ($value != (int)$value) {
  1272. self::formatAsFraction($value, $format);
  1273. }
  1274. } else {
  1275. // Handle the number itself
  1276. // scale number
  1277. $value = $value / $scale;
  1278. // Strip #
  1279. $format = preg_replace('/\\#/', '0', $format);
  1280. $n = "/\[[^\]]+\]/";
  1281. $m = preg_replace($n, '', $format);
  1282. $numberRegex = "/(0+)(\.?)(0*)/";
  1283. if (preg_match($numberRegex, $m, $matches)) {
  1284. $left = $matches[1];
  1285. $dec = $matches[2];
  1286. $right = $matches[3];
  1287. // minimun width of formatted number (including dot)
  1288. $minWidth = strlen($left) + strlen($dec) + strlen($right);
  1289. if ($useThousands) {
  1290. $value = number_format(
  1291. $value,
  1292. strlen($right),
  1293. self::getDecimalSeparator(),
  1294. self::getThousandsSeparator()
  1295. );
  1296. $value = preg_replace($numberRegex, $value, $format);
  1297. } else {
  1298. if (preg_match('/[0#]E[+-]0/i', $format)) {
  1299. //Scientific format
  1300. $value = sprintf('%5.2E', $value);
  1301. } elseif (preg_match('/0([^\d\.]+)0/', $format)) {
  1302. $value = self::complexNumberFormatMask($value, $format);
  1303. } else {
  1304. $sprintfPattern = "%0$minWidth." . strlen($right) . "f";
  1305. $value = sprintf($sprintfPattern, $value);
  1306. $value = preg_replace($numberRegex, $value, $format);
  1307. }
  1308. }
  1309. }
  1310. }
  1311. if (preg_match('/\[\$(.*)\]/u', $format, $m)) {
  1312. // Currency or Accounting
  1313. //$currencyFormat = $m[0];
  1314. $currencyCode = $m[1];
  1315. list($currencyCode) = explode('-', $currencyCode);
  1316. if ($currencyCode == '') {
  1317. $currencyCode = self::getCurrencyCode();
  1318. }
  1319. $value = preg_replace('/\[\$([^\]]*)\]/u', $currencyCode, $value);
  1320. }
  1321. }
  1322. }
  1323. return $value;
  1324. }
  1325. /**
  1326. * Reads a record from current position in data stream and continues reading data as long as CONTINUE records
  1327. * are found. Splices the record data pieces and returns the combined string as if record data is in one piece.
  1328. * Moves to next current position in data stream to start of next record different from a CONtINUE record
  1329. *
  1330. * @return array
  1331. */
  1332. private function getSplicedRecordData() {
  1333. $i = 0;
  1334. $data = '';
  1335. $spliceOffsets = [0];
  1336. do {
  1337. ++$i;
  1338. // offset: 0; size: 2; identifier
  1339. //$identifier = Cell::getInt2d($this->data, $this->pos);
  1340. // offset: 2; size: 2; length
  1341. $length = Format::getUInt2d($this->data, $this->pos + 2);
  1342. $data .= substr($this->data, $this->pos + 4, $length);
  1343. $spliceOffsets[$i] = $spliceOffsets[$i - 1] + $length;
  1344. $this->pos += 4 + $length;
  1345. $nextIdentifier = Format::getUInt2d($this->data, $this->pos);
  1346. } while ($nextIdentifier == self::XLS_TYPE_CONTINUE);
  1347. return ['recordData' => $data, 'spliceOffsets' => $spliceOffsets];
  1348. }
  1349. /**
  1350. * Get the decimal separator. If it has not yet been set explicitly, try to obtain number formatting
  1351. * information from locale.
  1352. *
  1353. * @return string
  1354. */
  1355. private static function getDecimalSeparator() {
  1356. if (!isset(self::$decimalSeparator)) {
  1357. $localeconv = localeconv();
  1358. self::$decimalSeparator = ($localeconv['decimal_point'] != '') ? $localeconv['decimal_point']
  1359. : $localeconv['mon_decimal_point'];
  1360. if (self::$decimalSeparator == '') {
  1361. // Default to .
  1362. self::$decimalSeparator = '.';
  1363. }
  1364. }
  1365. return self::$decimalSeparator;
  1366. }
  1367. /**
  1368. * Get the thousands separator. If it has not yet been set explicitly, try to obtain number formatting
  1369. * information from locale.
  1370. *
  1371. * @return string
  1372. */
  1373. private static function getThousandsSeparator() {
  1374. if (!isset(self::$thousandsSeparator)) {
  1375. $localeconv = localeconv();
  1376. self::$thousandsSeparator = ($localeconv['thousands_sep'] != '') ? $localeconv['thousands_sep']
  1377. : $localeconv['mon_thousands_sep'];
  1378. if (self::$thousandsSeparator == '') {
  1379. // Default to .
  1380. self::$thousandsSeparator = ',';
  1381. }
  1382. }
  1383. return self::$thousandsSeparator;
  1384. }
  1385. /**
  1386. * Get the currency code. If it has not yet been set explicitly, try to obtain the symbol information from locale.
  1387. *
  1388. * @return string
  1389. */
  1390. private static function getCurrencyCode() {
  1391. if (!isset(self::$currencyCode)) {
  1392. $localeconv = localeconv();
  1393. self::$currencyCode = ($localeconv['currency_symbol'] != '') ? $localeconv['currency_symbol']
  1394. : $localeconv['int_curr_symbol'];
  1395. if (self::$currencyCode == '') {
  1396. // Default to $
  1397. self::$currencyCode = '$';
  1398. }
  1399. }
  1400. return self::$currencyCode;
  1401. }
  1402. private static function complexNumberFormatMask($number, $mask) {
  1403. $sign = ($number < 0.0);
  1404. $number = abs($number);
  1405. if (strpos($mask, '.') !== false) {
  1406. $numbers = explode('.', $number . '.0');
  1407. $masks = explode('.', $mask . '.0');
  1408. $result1 = self::complexNumberFormatMask($numbers[0], $masks[0]);
  1409. $result2 = strrev(self::complexNumberFormatMask(strrev($numbers[1]), strrev($masks[1])));
  1410. return (($sign) ? '-' : '') . $result1 . '.' . $result2;
  1411. }
  1412. $r = preg_match_all('/0+/', $mask, $result, PREG_OFFSET_CAPTURE);
  1413. if ($r > 1) {
  1414. $result = array_reverse($result[0]);
  1415. $offset = 0;
  1416. foreach ($result as $block) {
  1417. $divisor = 1 . $block[0];
  1418. $size = strlen($block[0]);
  1419. $offset = $block[1];
  1420. $blockValue = sprintf('%0' . $size . 'd', fmod($number, $divisor));
  1421. $number = floor($number / $divisor);
  1422. $mask = substr_replace($mask, $blockValue, $offset, $size);
  1423. }
  1424. if ($number > 0) {
  1425. $mask = substr_replace($mask, $number, $offset, 0);
  1426. }
  1427. $result = $mask;
  1428. } else {
  1429. $result = $number;
  1430. }
  1431. return (($sign) ? '-' : '') . $result;
  1432. }
  1433. /**
  1434. * Convert Microsoft Code Page Identifier to Code Page Name which iconv and mbstring understands
  1435. *
  1436. * @param int $codePage Microsoft Code Page Indentifier
  1437. *
  1438. * @throws ParserException
  1439. * @return string Code Page Name
  1440. */
  1441. private static function NumberToName($codePage = 1252) {
  1442. switch ($codePage) {
  1443. case 367:
  1444. return 'ASCII'; //ASCII
  1445. case 437:
  1446. return 'CP437'; //OEM US
  1447. case 720:
  1448. throw new ParserException('Code page 720 not supported.', 5); //OEM Arabic
  1449. case 737:
  1450. return 'CP737'; //OEM Greek
  1451. case 775:
  1452. return 'CP775'; //OEM Baltic
  1453. case 850:
  1454. return 'CP850'; //OEM Latin I
  1455. case 852:
  1456. return 'CP852'; //OEM Latin II (Central European)
  1457. case 855:
  1458. return 'CP855'; //OEM Cyrillic
  1459. case 857:
  1460. return 'CP857'; //OEM Turkish
  1461. case 858:
  1462. return 'CP858'; //OEM Multilingual Latin I with Euro
  1463. case 860:
  1464. return 'CP860'; //OEM Portugese
  1465. case 861:
  1466. return 'CP861'; //OEM Icelandic
  1467. case 862:
  1468. return 'CP862'; //OEM Hebrew
  1469. case 863:
  1470. return 'CP863'; //OEM Canadian (French)
  1471. case 864:
  1472. return 'CP864'; //OEM Arabic
  1473. case 865:
  1474. return 'CP865'; //OEM Nordic
  1475. case 866:
  1476. return 'CP866'; //OEM Cyrillic (Russian)
  1477. case 869:
  1478. return 'CP869'; //OEM Greek (Modern)
  1479. case 874:
  1480. return 'CP874'; //ANSI Thai
  1481. case 932:
  1482. return 'CP932'; //ANSI Japanese Shift-JIS
  1483. case 936:
  1484. return 'CP936'; //ANSI Chinese Simplified GBK
  1485. case 949:
  1486. return 'CP949'; //ANSI Korean (Wansung)
  1487. case 950:
  1488. return 'CP950'; //ANSI Chinese Traditional BIG5
  1489. case 1200:
  1490. return 'UTF-16LE'; //UTF-16 (BIFF8)
  1491. case 1250:
  1492. return 'CP1250'; //ANSI Latin II (Central European)
  1493. case 1251:
  1494. return 'CP1251'; //ANSI Cyrillic
  1495. case 0: //CodePage is not always correctly set when the xls file was saved by Apple's Numbers program
  1496. case 1252:
  1497. return 'CP1252'; //ANSI Latin I (BIFF4-BIFF7)
  1498. case 1253:
  1499. return 'CP1253'; //ANSI Greek
  1500. case 1254:
  1501. return 'CP1254'; //ANSI Turkish
  1502. case 1255:
  1503. return 'CP1255'; //ANSI Hebrew
  1504. case 1256:
  1505. return 'CP1256'; //ANSI Arabic
  1506. case 1257:
  1507. return 'CP1257'; //ANSI Baltic
  1508. case 1258:
  1509. return 'CP1258'; //ANSI Vietnamese
  1510. case 1361:
  1511. return 'CP1361'; //ANSI Korean (Johab)
  1512. case 10000:
  1513. return 'MAC'; //Apple Roman
  1514. case 10001:
  1515. return 'CP932'; //Macintosh Japanese
  1516. case 10002:
  1517. return 'CP950'; //Macintosh Chinese Traditional
  1518. case 10003:
  1519. return 'CP1361'; //Macintosh Korean
  1520. case 10004:
  1521. return 'MACARABIC'; // Apple Arabic
  1522. case 10005:
  1523. return 'MACHEBREW'; //Apple Hebrew
  1524. case 10006:
  1525. return 'MACGREEK'; //Macintosh Greek
  1526. case 10007:
  1527. return 'MACCYRILLIC'; //Macintosh Cyrillic
  1528. case 10008:
  1529. return 'CP936'; //Macintosh - Simplified Chinese (GB 2312)
  1530. case 10010:
  1531. return 'MACROMANIA'; //Macintosh Romania
  1532. case 10017:
  1533. return 'MACUKRAINE'; //Macintosh Ukraine
  1534. case 10021:
  1535. return 'MACTHAI'; //Macintosh Thai
  1536. case 10029:
  1537. return 'MACCENTRALEUROPE'; //Macintosh Central Europe
  1538. case 10079:
  1539. return 'MACICELAND'; //Macintosh Icelandic
  1540. case 10081:
  1541. return 'MACTURKISH'; //Macintosh Turkish
  1542. case 10082:
  1543. return 'MACCROATIAN'; //Macintosh Croatian
  1544. case 21010:
  1545. return 'UTF-16LE'; //UTF-16 (BIFF8) This isn't correct, but some Excel writer libraries erroneously
  1546. // use Codepage 21010 for UTF-16LE
  1547. case 32768:
  1548. return 'MAC'; //Apple Roman
  1549. case 32769:
  1550. throw new ParserException('Code page 32769 not supported.', 6); //ANSI Latin I (BIFF2-BIFF3)
  1551. case 65000:
  1552. return 'UTF-7'; //Unicode (UTF-7)
  1553. case 65001:
  1554. return 'UTF-8'; //Unicode (UTF-8)
  1555. }
  1556. throw new ParserException("Unknown codepage: $codePage", 7);
  1557. }
  1558. /**
  1559. * Read byte string (8-bit string length). OpenOffice documentation: 2.5.2
  1560. *
  1561. * @param string $subData
  1562. *
  1563. * @return array
  1564. */
  1565. private function readByteStringShort($subData) {
  1566. // offset: 0; size: 1; length of the string (character count)
  1567. $ln = ord($subData[0]);
  1568. // offset: 1: size: var; character array (8-bit characters)
  1569. $value = $this->decodeCodepage(substr($subData, 1, $ln));
  1570. // size in bytes of data structure
  1571. return ['value' => $value, 'size' => 1 + $ln];
  1572. }
  1573. /**
  1574. * Read byte string (16-bit string length). OpenOffice documentation: 2.5.2
  1575. *
  1576. * @param string $subData
  1577. * @return array
  1578. */
  1579. private function readByteStringLong($subData) {
  1580. // offset: 0; size: 2; length of the string (character count)
  1581. $ln = Format::getUInt2d($subData, 0);
  1582. // offset: 2: size: var; character array (8-bit characters)
  1583. $value = $this->decodeCodepage(substr($subData, 2));
  1584. // size in bytes of data structure
  1585. return ['value' => $value, 'size' => 2 + $ln];
  1586. }
  1587. private static function formatAsDate(&$value, &$format) {
  1588. // strip off first part containing e.g. [$-F800] or [$USD-409]
  1589. // general syntax: [$<Currency string>-<language info>]
  1590. // language info is in hexadecimal
  1591. $format = preg_replace('/^(\[\$[A-Z]*-[0-9A-F]*\])/i', '', $format);
  1592. // OpenOffice.org uses upper-case number formats, e.g. 'YYYY', convert to lower-case;
  1593. // but we don't want to change any quoted strings
  1594. $format = preg_replace_callback('/(?:^|")([^"]*)(?:$|")/', ['self', 'setLowercaseCallback'], $format);
  1595. // Only process the non-quoted blocks for date format characters
  1596. $blocks = explode('"', $format);
  1597. foreach($blocks as $key => &$block) {
  1598. if ($key % 2 == 0) {
  1599. $block = strtr($block, Format::$dateFormatReplacements);
  1600. if (strpos($block, 'A') === false) {
  1601. // 24-hour time format
  1602. $block = strtr($block, Format::$dateFormatReplacements24);
  1603. } else {
  1604. // 12-hour time format
  1605. $block = strtr($block, Format::$dateFormatReplacements12);
  1606. }
  1607. }
  1608. }
  1609. $format = implode('"', $blocks);
  1610. // escape any quoted characters so that DateTime format() will render them correctly
  1611. $format = preg_replace_callback('/"(.*)"/U', ['self', 'escapeQuotesCallback'], $format);
  1612. $dateObj = self::ExcelToPHPObject($value);
  1613. $value = $dateObj->format($format);
  1614. }
  1615. private static function setLowercaseCallback($matches) {
  1616. return mb_strtolower($matches[0]);
  1617. }
  1618. private static function escapeQuotesCallback($matches) {
  1619. return '\\' . implode('\\', str_split($matches[1]));
  1620. }
  1621. /**
  1622. * Convert a date from Excel to a PHP Date/Time object
  1623. *
  1624. * @param int $dateValue Excel date/time value
  1625. *
  1626. * @return \DateTime PHP date/time object
  1627. */
  1628. private static function ExcelToPHPObject($dateValue = 0) {
  1629. $dateTime = self::ExcelToPHP($dateValue);
  1630. $days = floor($dateTime / 86400);
  1631. $time = round((($dateTime / 86400) - $days) * 86400);
  1632. $hours = round($time / 3600);
  1633. $minutes = round($time / 60) - ($hours * 60);
  1634. $seconds = round($time) - ($hours * 3600) - ($minutes * 60);
  1635. $dateObj = new \DateTime("1-Jan-1970+$days days");
  1636. $dateObj->setTime($hours, $minutes, $seconds);
  1637. return $dateObj;
  1638. }
  1639. /**
  1640. * Convert a date from Excel to PHP
  1641. *
  1642. * @param int $dateValue Excel date/time value
  1643. *
  1644. * @return int PHP serialized date/time
  1645. */
  1646. private static function ExcelToPHP($dateValue = 0) {
  1647. if (self::$excelBaseDate == Format::CALENDAR_WINDOWS_1900) {
  1648. $excelBaseDate = 25569;
  1649. //Adjust for the spurious 29-Feb-1900 (Day 60)
  1650. if ($dateValue < 60) {
  1651. --$excelBaseDate;
  1652. }
  1653. } else {
  1654. $excelBaseDate = 24107;
  1655. }
  1656. // Perform conversion
  1657. if ($dateValue >= 1) {
  1658. $utcDays = $dateValue - $excelBaseDate;
  1659. $returnValue = round($utcDays * 86400);
  1660. if (($returnValue <= PHP_INT_MAX) && ($returnValue >= -PHP_INT_MAX)) {
  1661. $returnValue = (integer) $returnValue;
  1662. }
  1663. } else {
  1664. $hours = round($dateValue * 24);
  1665. $mins = round($dateValue * 1440) - round($hours * 60);
  1666. $secs = round($dateValue * 86400) - round($hours * 3600) - round($mins * 60);
  1667. $returnValue = (integer) gmmktime($hours, $mins, $secs);
  1668. }
  1669. return $returnValue;
  1670. }
  1671. private static function formatAsPercentage(&$value, &$format) {
  1672. if ($format === Format::FORMAT_PERCENTAGE) {
  1673. $value = round((100 * $value), 0) . '%';
  1674. } else {
  1675. if (preg_match('/\.[#0]+/i', $format, $m)) {
  1676. $s = substr($m[0], 0, 1) . (strlen($m[0]) - 1);
  1677. $format = str_replace($m[0], $s, $format);
  1678. }
  1679. if (preg_match('/^[#0]+/', $format, $m)) {
  1680. $format = str_replace($m[0], strlen($m[0]), $format);
  1681. }
  1682. $format = '%' . str_replace('%', 'f%%', $format);
  1683. $value = sprintf($format, 100 * $value);
  1684. }
  1685. }
  1686. private static function formatAsFraction(&$value, &$format) {
  1687. $sign = ($value < 0) ? '-' : '';
  1688. $integerPart = floor(abs($value));
  1689. $decimalPart = trim(fmod(abs($value), 1), '0.');
  1690. $decimalLength = strlen($decimalPart);
  1691. $decimalDivisor = pow(10, $decimalLength);
  1692. $GCD = self::GCD([$decimalPart, $decimalDivisor]);
  1693. $adjustedDecimalPart = $decimalPart/$GCD;
  1694. $adjustedDecimalDivisor = $decimalDivisor/$GCD;
  1695. if ((strpos($format, '0') !== false) || (strpos($format, '#') !== false) || (substr($format, 0, 3) == '? ?')) {
  1696. if ($integerPart == 0) {
  1697. $integerPart = '';
  1698. }
  1699. $value = "$sign$integerPart $adjustedDecimalPart/$adjustedDecimalDivisor";
  1700. } else {
  1701. $adjustedDecimalPart += $integerPart * $adjustedDecimalDivisor;
  1702. $value = "$sign$adjustedDecimalPart/$adjustedDecimalDivisor";
  1703. }
  1704. }
  1705. /**
  1706. * GCD
  1707. *
  1708. * Returns the greatest common divisor of a series of numbers. The greatest common divisor is the largest
  1709. * integer that divides both number1 and number2 without a remainder.
  1710. * Excel Function:
  1711. * GCD(number1[,number2[, ...]])
  1712. *
  1713. * @param array $params
  1714. *
  1715. * @return integer Greatest Common Divisor
  1716. */
  1717. private static function GCD($params) {
  1718. $returnValue = 1;
  1719. $allValuesFactors = [];
  1720. // Loop through arguments
  1721. $flattenArr = self::flattenArray($params);
  1722. foreach ($flattenArr as $value) {
  1723. if (!is_numeric($value)) {
  1724. return '#VALUE!';
  1725. } elseif ($value == 0) {
  1726. continue;
  1727. } elseif ($value < 0) {
  1728. return '#NULL!';
  1729. }
  1730. $factors = self::factors($value);
  1731. $countedFactors = array_count_values($factors);
  1732. $allValuesFactors[] = $countedFactors;
  1733. }
  1734. $allValuesCount = count($allValuesFactors);
  1735. if ($allValuesCount == 0) {
  1736. return 0;
  1737. }
  1738. $mergedArray = $allValuesFactors[0];
  1739. for ($i=1; $i < $allValuesCount; ++$i) {
  1740. $mergedArray = array_intersect_key($mergedArray, $allValuesFactors[$i]);
  1741. }
  1742. $mergedArrayValues = count($mergedArray);
  1743. if ($mergedArrayValues == 0) {
  1744. return $returnValue;
  1745. } elseif ($mergedArrayValues > 1) {
  1746. foreach ($mergedArray as $mergedKey => $mergedValue) {
  1747. foreach ($allValuesFactors as $highestPowerTest) {
  1748. foreach ($highestPowerTest as $testKey => $testValue) {
  1749. if (($testKey == $mergedKey) && ($testValue < $mergedValue)) {
  1750. $mergedArray[$mergedKey] = $testValue;
  1751. $mergedValue = $testValue;
  1752. }
  1753. }
  1754. }
  1755. }
  1756. $returnValue = 1;
  1757. foreach ($mergedArray as $key => $value) {
  1758. $returnValue *= pow($key, $value);
  1759. }
  1760. return $returnValue;
  1761. } else {
  1762. $keys = array_keys($mergedArray);
  1763. $key = $keys[0];
  1764. $value = $mergedArray[$key];
  1765. foreach ($allValuesFactors as $testValue) {
  1766. foreach ($testValue as $mergedKey => $mergedValue) {
  1767. if (($mergedKey == $key) && ($mergedValue < $value)) {
  1768. $value = $mergedValue;
  1769. }
  1770. }
  1771. }
  1772. return pow($key, $value);
  1773. }
  1774. }
  1775. /**
  1776. * Convert a multi-dimensional array to a simple 1-dimensional array
  1777. *
  1778. * @param array $array Array to be flattened
  1779. *
  1780. * @return array Flattened array
  1781. */
  1782. private static function flattenArray($array) {
  1783. if (!is_array($array)) {
  1784. return (array) $array;
  1785. }
  1786. $arrayValues = [];
  1787. foreach ($array as $value) {
  1788. if (is_array($value)) {
  1789. foreach ($value as $val) {
  1790. if (is_array($val)) {
  1791. foreach ($val as $v) {
  1792. $arrayValues[] = $v;
  1793. }
  1794. } else {
  1795. $arrayValues[] = $val;
  1796. }
  1797. }
  1798. } else {
  1799. $arrayValues[] = $value;
  1800. }
  1801. }
  1802. return $arrayValues;
  1803. }
  1804. /**
  1805. * Return an array of the factors of the input value
  1806. *
  1807. * @param int $value
  1808. *
  1809. * @return array
  1810. */
  1811. private static function factors($value) {
  1812. $startVal = floor(sqrt($value));
  1813. $factorArray = [];
  1814. for ($i = $startVal; $i > 1; --$i) {
  1815. if (($value % $i) == 0) {
  1816. $factorArray = array_merge($factorArray, self::factors($value / $i));
  1817. $factorArray = array_merge($factorArray, self::factors($i));
  1818. if ($i <= sqrt($value)) {
  1819. break;
  1820. }
  1821. }
  1822. }
  1823. if (!empty($factorArray)) {
  1824. rsort($factorArray);
  1825. return $factorArray;
  1826. }
  1827. return [(int) $value];
  1828. }
  1829. /**
  1830. * Read Unicode string with no string length field, but with known character count this function is under
  1831. * construction, needs to support rich text, and Asian phonetic settings
  1832. *
  1833. * @param string $subData
  1834. * @param int $characterCount
  1835. *
  1836. * @return array
  1837. */
  1838. private static function readUnicodeString($subData, $characterCount) {
  1839. // offset: 0: size: 1; option flags
  1840. // bit: 0; mask: 0x01; character compression (0 = compressed 8-bit, 1 = uncompressed 16-bit)
  1841. $isCompressed = !((0x01 & ord($subData[0])) >> 0);
  1842. // offset: 1: size: var; character array
  1843. // this offset assumes richtext and Asian phonetic settings are off which is generally wrong
  1844. // needs to be fixed
  1845. $value = self::encodeUTF16(
  1846. substr($subData, 1, $isCompressed ? $characterCount : 2 * $characterCount), $isCompressed
  1847. );
  1848. // the size in bytes including the option flags
  1849. return ['value' => $value, 'size' => $isCompressed ? 1 + $characterCount : 1 + 2 * $characterCount];
  1850. }
  1851. /**
  1852. * Extracts an Excel Unicode short string (8-bit string length), this function will automatically find out
  1853. * where the Unicode string ends.
  1854. *
  1855. * @param string $subData
  1856. *
  1857. * @return array
  1858. */
  1859. private static function readUnicodeStringShort($subData) {
  1860. // offset: 0: size: 1; length of the string (character count)
  1861. $characterCount = ord($subData[0]);
  1862. $string = self::readUnicodeString(substr($subData, 1), $characterCount);
  1863. // add 1 for the string length
  1864. $string['size'] += 1;
  1865. return $string;
  1866. }
  1867. /**
  1868. * Extracts an Excel Unicode long string (16-bit string length), this function is under construction,
  1869. * needs to support rich text, and Asian phonetic settings
  1870. *
  1871. * @param string $subData
  1872. *
  1873. * @return array
  1874. */
  1875. private static function readUnicodeStringLong($subData) {
  1876. // offset: 0: size: 2; length of the string (character count)
  1877. $characterCount = Format::getUInt2d($subData, 0);
  1878. $string = self::readUnicodeString(substr($subData, 2), $characterCount);
  1879. // add 2 for the string length
  1880. $string['size'] += 2;
  1881. return $string;
  1882. }
  1883. private static function getIEEE754($rkNum) {
  1884. if (($rkNum & 0x02) != 0) {
  1885. $value = $rkNum >> 2;
  1886. } else {
  1887. // changes by mmp, info on IEEE754 encoding from
  1888. // research.microsoft.com/~hollasch/cgindex/coding/ieeefloat.html
  1889. // The RK format calls for using only the most significant 30 bits of the 64 bit floating point value.
  1890. // The other 34 bits are assumed to be 0 so we use the upper 30 bits of $rknum as follows...
  1891. $sign = ($rkNum & 0x80000000) >> 31;
  1892. $exp = ($rkNum & 0x7ff00000) >> 20;
  1893. $mantissa = (0x100000 | ($rkNum & 0x000ffffc));
  1894. $value = $mantissa / pow(2, (20- ($exp - 1023)));
  1895. if ($sign) {
  1896. $value = -1 * $value;
  1897. }
  1898. //end of changes by mmp
  1899. }
  1900. if (($rkNum & 0x01) != 0) {
  1901. $value /= 100;
  1902. }
  1903. return $value;
  1904. }
  1905. /**
  1906. * Get UTF-8 string from (compressed or uncompressed) UTF-16 string
  1907. *
  1908. * @param string $string
  1909. * @param bool $compressed
  1910. *
  1911. * @return string
  1912. */
  1913. private static function encodeUTF16($string, $compressed = false) {
  1914. if ($compressed) {
  1915. $string = self::uncompressByteString($string);
  1916. }
  1917. return mb_convert_encoding($string, 'UTF-8', 'UTF-16LE');
  1918. }
  1919. /**
  1920. * Convert string to UTF-8. Only used for BIFF5.
  1921. *
  1922. * @param string $string
  1923. *
  1924. * @return string
  1925. */
  1926. private function decodeCodepage($string) {
  1927. return mb_convert_encoding($string, 'UTF-8', $this->codePage);
  1928. }
  1929. /**
  1930. * Convert UTF-16 string in compressed notation to uncompressed form. Only used for BIFF8.
  1931. *
  1932. * @param string $string
  1933. *
  1934. * @return string
  1935. */
  1936. private static function uncompressByteString($string) {
  1937. $uncompressedString = '';
  1938. $strLen = strlen($string);
  1939. for ($i = 0; $i < $strLen; ++$i) {
  1940. $uncompressedString .= $string[$i] . "\0";
  1941. }
  1942. return $uncompressedString;
  1943. }
  1944. /**
  1945. * Reads first 8 bytes of a string and return IEEE 754 float
  1946. *
  1947. * @param string $data Binary string that is at least 8 bytes long
  1948. *
  1949. * @return float
  1950. */
  1951. private static function extractNumber($data) {
  1952. $rkNumHigh = Format::getInt4d($data, 4);
  1953. $rkNumLow = Format::getInt4d($data, 0);
  1954. $sign = ($rkNumHigh & 0x80000000) >> 31;
  1955. $exp = (($rkNumHigh & 0x7ff00000) >> 20) - 1023;
  1956. $mantissa = (0x100000 | ($rkNumHigh & 0x000fffff));
  1957. $mantissaLow1 = ($rkNumLow & 0x80000000) >> 31;
  1958. $mantissaLow2 = ($rkNumLow & 0x7fffffff);
  1959. $value = $mantissa / pow(2, (20 - $exp));
  1960. if ($mantissaLow1 != 0) {
  1961. $value += 1 / pow(2, (21 - $exp));
  1962. }
  1963. $value += $mantissaLow2 / pow(2, (52 - $exp));
  1964. if ($sign) {
  1965. $value *= -1;
  1966. }
  1967. return $value;
  1968. }
  1969. }