XLSX.php 36 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996
  1. <?php
  2. class PHPExcel_Reader_XLSX implements Iterator, Countable {
  3. const CELL_TYPE_BOOL = 'b';
  4. const CELL_TYPE_NUMBER = 'n';
  5. const CELL_TYPE_ERROR = 'e';
  6. const CELL_TYPE_SHARED_STR = 's';
  7. const CELL_TYPE_STR = 'str';
  8. const CELL_TYPE_INLINE_STR = 'inlineStr';
  9. /**
  10. * Number of shared strings that can be reasonably cached, i.e., that aren't read from file but stored in memory.
  11. * If the total number of shared strings is higher than this, caching is not used.
  12. * If this value is null, shared strings are cached regardless of amount.
  13. * With large shared string caches there are huge performance gains, however a lot of memory could be used which
  14. * can be a problem, especially on shared hosting.
  15. */
  16. const SHARED_STRING_CACHE_LIMIT = 50000;
  17. private $Options = array(
  18. 'TempDir' => '',
  19. 'ReturnDateTimeObjects' => false
  20. );
  21. private static $RuntimeInfo = array('GMPSupported' => false);
  22. private $Valid = false;
  23. /**
  24. * @var SpreadsheetReader_* Handle for the reader object
  25. */
  26. private $Handle = false;
  27. // Worksheet file
  28. /**
  29. * @var string Path to the worksheet XML file
  30. */
  31. private $WorksheetPath = false;
  32. /**
  33. * @var XMLReader XML reader object for the worksheet XML file
  34. */
  35. private $Worksheet = false;
  36. // Shared strings file
  37. /**
  38. * @var string Path to shared strings XML file
  39. */
  40. private $SharedStringsPath = false;
  41. /**
  42. * @var XMLReader XML reader object for the shared strings XML file
  43. */
  44. private $SharedStrings = false;
  45. /**
  46. * @var array Shared strings cache, if the number of shared strings is low enough
  47. */
  48. private $SharedStringCache = array();
  49. // Workbook data
  50. /**
  51. * @var SimpleXMLElement XML object for the workbook XML file
  52. */
  53. private $WorkbookXML = false;
  54. // Style data
  55. /**
  56. * @var SimpleXMLElement XML object for the styles XML file
  57. */
  58. private $StylesXML = false;
  59. /**
  60. * @var array Container for cell value style data
  61. */
  62. private $Styles = array();
  63. private $TempDir = '';
  64. private $TempFiles = array();
  65. private $CurrentRow = false;
  66. private $rowCount = null;
  67. // Runtime parsing data
  68. /**
  69. * @var int Current row in the file
  70. */
  71. private $Index = 0;
  72. /**
  73. * @var array Data about separate sheets in the file
  74. */
  75. private $Sheets = false;
  76. private $SharedStringCount = 0;
  77. private $SharedStringIndex = 0;
  78. private $LastSharedStringValue = null;
  79. private $RowOpen = false;
  80. private $SSOpen = false;
  81. private $SSForwarded = false;
  82. private static $BuiltinFormats = array(
  83. 0 => '',
  84. 1 => '0',
  85. 2 => '0.00',
  86. 3 => '#,##0',
  87. 4 => '#,##0.00',
  88. 9 => '0%',
  89. 10 => '0.00%',
  90. 11 => '0.00E+00',
  91. 12 => '# ?/?',
  92. 13 => '# ??/??',
  93. 14 => 'yyyy/m/d',
  94. 15 => 'd-mmm-yy',
  95. 16 => 'd-mmm',
  96. 17 => 'mmm-yy',
  97. 18 => 'h:mm AM/PM',
  98. 19 => 'h:mm:ss AM/PM',
  99. 20 => 'h:mm',
  100. 21 => 'h:mm:ss',
  101. 22 => 'yyyy/m/d h:mm',
  102. 31 => 'yyyy年m月d日',
  103. 32 => 'h时mmi分',
  104. 33 => 'h时mmi分ss秒',
  105. 37 => '#,##0 ;(#,##0)',
  106. 38 => '#,##0 ;[Red](#,##0)',
  107. 39 => '#,##0.00;(#,##0.00)',
  108. 40 => '#,##0.00;[Red](#,##0.00)',
  109. 44 => '_("$"* #,##0.00_);_("$"* \(#,##0.00\);_("$"* "-"??_);_(@_)',
  110. 45 => 'mm:ss',
  111. 46 => '[h]:mm:ss',
  112. 47 => 'mm:ss.0',
  113. 48 => '##0.0E+0',
  114. 49 => '@',
  115. 55 => 'AM/PM h时mmi分',
  116. 56 => 'AM/PM h时mmi分ss秒',
  117. 58 => 'm月d日', // CHT & CHS
  118. 27 => 'yyyy年m月',
  119. 30 => 'm/d/yy',
  120. 36 => '[$-404]e/m/d',
  121. 50 => '[$-404]e/m/d',
  122. 57 => '[$-404]e/m/d', // THA
  123. 59 => 't0',
  124. 60 => 't0.00',
  125. 61 => 't#,##0',
  126. 62 => 't#,##0.00',
  127. 67 => 't0%',
  128. 68 => 't0.00%',
  129. 69 => 't# ?/?',
  130. 70 => 't# ??/??'
  131. );
  132. private $Formats = array();
  133. private static $DateReplacements = array(
  134. 'All' => array(
  135. '\\' => '',
  136. 'am/pm' => 'A',
  137. 'e' => 'Y',
  138. 'yyyy' => 'Y',
  139. 'yy' => 'y',
  140. 'mmmmm' => 'M',
  141. 'mmmm' => 'F',
  142. 'mmm' => 'M',
  143. ':mm' => ':i',
  144. 'mmi' => 'i',
  145. 'mm' => 'm',
  146. 'm' => 'n',
  147. 'dddd' => 'l',
  148. 'ddd' => 'D',
  149. 'dd' => 'd',
  150. 'd' => 'j',
  151. 'ss' => 's',
  152. '.s' => ''
  153. ),
  154. '24H' => array(
  155. 'hh' => 'H',
  156. 'h' => 'G'
  157. ),
  158. '12H' => array(
  159. 'hh' => 'h', 'h' => 'g'
  160. )
  161. );
  162. private static $BaseDate = false;
  163. private static $DecimalSeparator = '.';
  164. private static $ThousandSeparator = ',';
  165. private static $CurrencyCode = '';
  166. /**
  167. * @var array Cache for already processed format strings
  168. */
  169. private $ParsedFormatCache = array();
  170. /**
  171. * @param string $Filepath Path to file
  172. * @param array $Options Options:
  173. * TempDir => string Temporary directory path
  174. * ReturnDateTimeObjects => bool True => dates and times will be returned as PHP DateTime objects,
  175. * false => as strings
  176. * @throws Exception
  177. */
  178. public function __construct($Filepath, array $Options = null) {
  179. if (! is_readable($Filepath)) {
  180. throw new Exception('SpreadsheetReader_XLSX: File not readable (' . $Filepath . ')');
  181. }
  182. $this->TempDir = isset($Options['TempDir']) && is_writable($Options['TempDir']) ? $Options['TempDir'] : sys_get_temp_dir();
  183. $this->TempDir = rtrim($this->TempDir, DIRECTORY_SEPARATOR);
  184. $this->TempDir = $this->TempDir . DIRECTORY_SEPARATOR . uniqid() . DIRECTORY_SEPARATOR;
  185. $Zip = new ZipArchive;
  186. $Status = $Zip->open($Filepath);
  187. if ($Status !== true) {
  188. throw new Exception('SpreadsheetReader_XLSX: File not readable (' . $Filepath . ') (Error ' . $Status . ')');
  189. }
  190. // Getting the general workbook information
  191. if ($Zip->locateName('xl/workbook.xml') !== false) {
  192. $this->WorkbookXML = new SimpleXMLElement($Zip->getFromName('xl/workbook.xml'));
  193. }
  194. // Extracting the XMLs from the XLSX zip file
  195. if ($Zip->locateName('xl/sharedStrings.xml') !== false) {
  196. $this->SharedStringsPath = $this->TempDir . 'xl' . DIRECTORY_SEPARATOR . 'sharedStrings.xml';
  197. $Zip->extractTo($this->TempDir, 'xl/sharedStrings.xml');
  198. $this->TempFiles[] = $this->TempDir . 'xl' . DIRECTORY_SEPARATOR . 'sharedStrings.xml';
  199. if (is_readable($this->SharedStringsPath)) {
  200. $this->SharedStrings = new XMLReader;
  201. $this->SharedStrings->open($this->SharedStringsPath);
  202. $this->PrepareSharedStringCache();
  203. }
  204. }
  205. $Sheets = $this->Sheets();
  206. foreach ($this->Sheets as $Index => $Name) {
  207. if ($Zip->locateName('xl/worksheets/sheet' . $Index . '.xml') !== false) {
  208. $Zip->extractTo($this->TempDir, 'xl/worksheets/sheet' . $Index . '.xml');
  209. $this->TempFiles[] = $this->TempDir . 'xl' . DIRECTORY_SEPARATOR . 'worksheets' . DIRECTORY_SEPARATOR . 'sheet' . $Index . '.xml';
  210. }
  211. }
  212. $this->ChangeSheet(0);
  213. // If worksheet is present and is OK, parse the styles already
  214. if ($Zip->locateName('xl/styles.xml') !== false) {
  215. $this->StylesXML = new SimpleXMLElement($Zip->getFromName('xl/styles.xml'));
  216. if ($this->StylesXML && $this->StylesXML->cellXfs && $this->StylesXML->cellXfs->xf) {
  217. foreach ($this->StylesXML->cellXfs->xf as $Index => $XF) {
  218. // Format #0 is a special case - it is the "General" format that is applied regardless of applyNumberFormat
  219. if ($XF->attributes()->applyNumberFormat || (0 == (int)$XF->attributes()->numFmtId)) {
  220. $FormatId = (int)$XF->attributes()->numFmtId;
  221. // If format ID >= 164, it is a custom format and should be read from styleSheet\numFmts
  222. $this->Styles[] = $FormatId;
  223. } else {
  224. // 0 for "General" format
  225. $this->Styles[] = 0;
  226. }
  227. }
  228. }
  229. if ($this->StylesXML->numFmts && $this->StylesXML->numFmts->numFmt) {
  230. foreach ($this->StylesXML->numFmts->numFmt as $Index => $NumFmt) {
  231. $this->Formats[(int)$NumFmt->attributes()->numFmtId] = (string)$NumFmt->attributes()->formatCode;
  232. }
  233. }
  234. unset($this->StylesXML);
  235. }
  236. $Zip->close();
  237. // Setting base date
  238. if (! self::$BaseDate) {
  239. self::$BaseDate = new DateTime;
  240. self::$BaseDate->setTimezone(new DateTimeZone('UTC'));
  241. self::$BaseDate->setDate(1900, 1, 0);
  242. self::$BaseDate->setTime(0, 0, 0);
  243. }
  244. // Decimal and thousand separators
  245. if (! self::$DecimalSeparator && ! self::$ThousandSeparator && ! self::$CurrencyCode) {
  246. $Locale = localeconv();
  247. self::$DecimalSeparator = $Locale['decimal_point'];
  248. self::$ThousandSeparator = $Locale['thousands_sep'];
  249. self::$CurrencyCode = $Locale['int_curr_symbol'];
  250. }
  251. if (function_exists('gmp_gcd')) {
  252. self::$RuntimeInfo['GMPSupported'] = true;
  253. }
  254. }
  255. /**
  256. * Destructor, destroys all that remains (closes and deletes temp files)
  257. */
  258. public function __destruct() {
  259. foreach ($this->TempFiles as $TempFile) {
  260. @unlink($TempFile);
  261. }
  262. // Better safe than sorry - shouldn't try deleting '.' or '/', or '..'.
  263. if (strlen($this->TempDir) > 2) {
  264. @rmdir($this->TempDir . 'xl' . DIRECTORY_SEPARATOR . 'worksheets');
  265. @rmdir($this->TempDir . 'xl');
  266. @rmdir($this->TempDir);
  267. }
  268. if ($this->Worksheet && $this->Worksheet instanceof XMLReader) {
  269. $this->Worksheet->close();
  270. unset($this->Worksheet);
  271. }
  272. unset($this->WorksheetPath);
  273. if ($this->SharedStrings && $this->SharedStrings instanceof XMLReader) {
  274. $this->SharedStrings->close();
  275. unset($this->SharedStrings);
  276. }
  277. unset($this->SharedStringsPath);
  278. if (isset($this->StylesXML)) {
  279. unset($this->StylesXML);
  280. }
  281. if ($this->WorkbookXML) {
  282. unset($this->WorkbookXML);
  283. }
  284. }
  285. /**
  286. * Retrieves an array with information about sheets in the current file
  287. * @return array List of sheets (key is sheet index, value is name)
  288. */
  289. public function Sheets() {
  290. if ($this->Sheets === false) {
  291. $this->Sheets = array();
  292. foreach ($this->WorkbookXML->sheets->sheet as $Index => $Sheet) {
  293. $Attributes = $Sheet->attributes('r', true);
  294. foreach ($Attributes as $Name => $Value) {
  295. if ($Name == 'id') {
  296. $SheetID = (int)str_replace('rId', '', (string)$Value);
  297. break;
  298. }
  299. }
  300. $this->Sheets[$SheetID] = (string)$Sheet['name'];
  301. }
  302. ksort($this->Sheets);
  303. }
  304. return array_values($this->Sheets);
  305. }
  306. /**
  307. * Changes the current sheet in the file to another
  308. *
  309. * @param int Sheet index
  310. *
  311. * @return bool True if sheet was successfully changed, false otherwise.
  312. */
  313. public function ChangeSheet($Index) {
  314. $RealSheetIndex = false;
  315. $Sheets = $this->Sheets();
  316. if (isset($Sheets[$Index])) {
  317. $SheetIndexes = array_keys($this->Sheets);
  318. $RealSheetIndex = $SheetIndexes[$Index];
  319. }
  320. $TempWorksheetPath = $this->TempDir . 'xl/worksheets/sheet' . $RealSheetIndex . '.xml';
  321. if ($RealSheetIndex !== false && is_readable($TempWorksheetPath)) {
  322. $this->WorksheetPath = $TempWorksheetPath;
  323. $this->rewind();
  324. return true;
  325. }
  326. return false;
  327. }
  328. /**
  329. * Creating shared string cache if the number of shared strings is acceptably low (or there is no limit on the
  330. * amount
  331. */
  332. private function PrepareSharedStringCache() {
  333. while ($this->SharedStrings->read()) {
  334. if ($this->SharedStrings->name == 'sst') {
  335. $this->SharedStringCount = $this->SharedStrings->getAttribute('count');
  336. break;
  337. }
  338. }
  339. if (! $this->SharedStringCount || (self::SHARED_STRING_CACHE_LIMIT < $this->SharedStringCount && self::SHARED_STRING_CACHE_LIMIT !== null)) {
  340. return false;
  341. }
  342. $CacheIndex = 0;
  343. $CacheValue = '';
  344. while ($this->SharedStrings->read()) {
  345. switch ($this->SharedStrings->name) {
  346. case 'si':
  347. if ($this->SharedStrings->nodeType == XMLReader::END_ELEMENT) {
  348. $this->SharedStringCache[$CacheIndex] = $CacheValue;
  349. $CacheIndex++;
  350. $CacheValue = '';
  351. }
  352. break;
  353. case 't':
  354. if ($this->SharedStrings->nodeType == XMLReader::END_ELEMENT) {
  355. continue;
  356. }
  357. $CacheValue .= $this->SharedStrings->readString();
  358. break;
  359. }
  360. }
  361. $this->SharedStrings->close();
  362. return true;
  363. }
  364. /**
  365. * Retrieves a shared string value by its index
  366. *
  367. * @param int Shared string index
  368. *
  369. * @return string Value
  370. */
  371. private function GetSharedString($Index) {
  372. if ((self::SHARED_STRING_CACHE_LIMIT === null || self::SHARED_STRING_CACHE_LIMIT > 0) && ! empty($this->SharedStringCache)) {
  373. if (isset($this->SharedStringCache[$Index])) {
  374. return $this->SharedStringCache[$Index];
  375. } else {
  376. return '';
  377. }
  378. }
  379. // If the desired index is before the current, rewind the XML
  380. if ($this->SharedStringIndex > $Index) {
  381. $this->SSOpen = false;
  382. $this->SharedStrings->close();
  383. $this->SharedStrings->open($this->SharedStringsPath);
  384. $this->SharedStringIndex = 0;
  385. $this->LastSharedStringValue = null;
  386. $this->SSForwarded = false;
  387. }
  388. // Finding the unique string count (if not already read)
  389. if ($this->SharedStringIndex == 0 && ! $this->SharedStringCount) {
  390. while ($this->SharedStrings->read()) {
  391. if ($this->SharedStrings->name == 'sst') {
  392. $this->SharedStringCount = $this->SharedStrings->getAttribute('uniqueCount');
  393. break;
  394. }
  395. }
  396. }
  397. // If index of the desired string is larger than possible, don't even bother.
  398. if ($this->SharedStringCount && ($Index >= $this->SharedStringCount)) {
  399. return '';
  400. }
  401. // If an index with the same value as the last already fetched is requested
  402. // (any further traversing the tree would get us further away from the node)
  403. if (($Index == $this->SharedStringIndex) && ($this->LastSharedStringValue !== null)) {
  404. return $this->LastSharedStringValue;
  405. }
  406. // Find the correct <si> node with the desired index
  407. while ($this->SharedStringIndex <= $Index) {
  408. // SSForwarded is set further to avoid double reading in case nodes are skipped.
  409. if ($this->SSForwarded) {
  410. $this->SSForwarded = false;
  411. } else {
  412. $ReadStatus = $this->SharedStrings->read();
  413. if (! $ReadStatus) {
  414. break;
  415. }
  416. }
  417. if ($this->SharedStrings->name == 'si') {
  418. if ($this->SharedStrings->nodeType == XMLReader::END_ELEMENT) {
  419. $this->SSOpen = false;
  420. $this->SharedStringIndex++;
  421. } else {
  422. $this->SSOpen = true;
  423. if ($this->SharedStringIndex < $Index) {
  424. $this->SSOpen = false;
  425. $this->SharedStrings->next('si');
  426. $this->SSForwarded = true;
  427. $this->SharedStringIndex++;
  428. continue;
  429. } else {
  430. break;
  431. }
  432. }
  433. }
  434. }
  435. $Value = '';
  436. // Extract the value from the shared string
  437. if ($this->SSOpen && ($this->SharedStringIndex == $Index)) {
  438. while ($this->SharedStrings->read()) {
  439. switch ($this->SharedStrings->name) {
  440. case 't':
  441. if ($this->SharedStrings->nodeType == XMLReader::END_ELEMENT) {
  442. continue;
  443. }
  444. $Value .= $this->SharedStrings->readString();
  445. break;
  446. case 'si':
  447. if ($this->SharedStrings->nodeType == XMLReader::END_ELEMENT) {
  448. $this->SSOpen = false;
  449. $this->SSForwarded = true;
  450. break 2;
  451. }
  452. break;
  453. }
  454. }
  455. }
  456. if ($Value) {
  457. $this->LastSharedStringValue = $Value;
  458. }
  459. return $Value;
  460. }
  461. /**
  462. * Formats the value according to the index
  463. *
  464. * @param string Cell value
  465. * @param int Format index
  466. *
  467. * @return string Formatted cell value
  468. */
  469. private function FormatValue($Value, $Index) {
  470. if (! is_numeric($Value)) {
  471. return $Value;
  472. }
  473. if (isset($this->Styles[$Index]) && ($this->Styles[$Index] !== false)) {
  474. $Index = $this->Styles[$Index];
  475. } else {
  476. return $Value;
  477. }
  478. // A special case for the "General" format
  479. if ($Index == 0) {
  480. return $this->GeneralFormat($Value);
  481. }
  482. $Format = array();
  483. if (isset($this->ParsedFormatCache[$Index])) {
  484. $Format = $this->ParsedFormatCache[$Index];
  485. }
  486. if (! $Format) {
  487. $Format = array('Code' => false, 'Type' => false, 'Scale' => 1, 'Thousands' => false, 'Currency' => false);
  488. if (isset(self::$BuiltinFormats[$Index])) {
  489. $Format['Code'] = self::$BuiltinFormats[$Index];
  490. } elseif (isset($this->Formats[$Index])) {
  491. $Format['Code'] = str_replace('"', '', $this->Formats[$Index]);
  492. }
  493. // Format code found, now parsing the format
  494. if ($Format['Code']) {
  495. $Sections = explode(';', $Format['Code']);
  496. $Format['Code'] = $Sections[0];
  497. switch (count($Sections)) {
  498. case 2:
  499. if ($Value < 0) {
  500. $Format['Code'] = $Sections[1];
  501. }
  502. $Value = abs($Value);
  503. break;
  504. case 3:
  505. case 4:
  506. if ($Value < 0) {
  507. $Format['Code'] = $Sections[1];
  508. } elseif ($Value == 0) {
  509. $Format['Code'] = $Sections[2];
  510. }
  511. $Value = abs($Value);
  512. break;
  513. }
  514. }
  515. // Stripping colors
  516. $Format['Code'] = trim(preg_replace('/^\\[[a-zA-Z]+\\]/', '', $Format['Code']));
  517. // Percentages
  518. if (substr($Format['Code'], -1) == '%') {
  519. $Format['Type'] = 'Percentage';
  520. } elseif (preg_match('/(\[\$[A-Z]*-[0-9A-F]*\])*[hmsdy]/i', $Format['Code'])) {
  521. $Format['Type'] = 'DateTime';
  522. $Format['Code'] = trim(preg_replace('/^(\[\$[A-Z]*-[0-9A-F]*\])/i', '', $Format['Code']));
  523. $Format['Code'] = strtolower($Format['Code']);
  524. $Format['Code'] = strtr($Format['Code'], self::$DateReplacements['All']);
  525. if (strpos($Format['Code'], 'A') === false) {
  526. $Format['Code'] = strtr($Format['Code'], self::$DateReplacements['24H']);
  527. } else {
  528. $Format['Code'] = strtr($Format['Code'], self::$DateReplacements['12H']);
  529. }
  530. } elseif ($Format['Code'] == '[$EUR ]#,##0.00_-') {
  531. $Format['Type'] = 'Euro';
  532. } else {
  533. // Removing skipped characters
  534. $Format['Code'] = preg_replace('/_./', '', $Format['Code']);
  535. // Removing unnecessary escaping
  536. $Format['Code'] = preg_replace("/\\\\/", '', $Format['Code']);
  537. // Removing string quotes
  538. $Format['Code'] = str_replace(array('"', '*'), '', $Format['Code']);
  539. // Removing thousands separator
  540. if (strpos($Format['Code'], '0,0') !== false || strpos($Format['Code'], '#,#') !== false) {
  541. $Format['Thousands'] = true;
  542. }
  543. $Format['Code'] = str_replace(array('0,0', '#,#'), array('00', '##'), $Format['Code']);
  544. // Scaling (Commas indicate the power)
  545. $Scale = 1;
  546. $Matches = array();
  547. if (preg_match('/(0|#)(,+)/', $Format['Code'], $Matches)) {
  548. $Scale = pow(1000, strlen($Matches[2]));
  549. // Removing the commas
  550. $Format['Code'] = preg_replace(array('/0,+/', '/#,+/'), array('0', '#'), $Format['Code']);
  551. }
  552. $Format['Scale'] = $Scale;
  553. if (preg_match('/#?.*\?\/\?/', $Format['Code'])) {
  554. $Format['Type'] = 'Fraction';
  555. } else {
  556. $Format['Code'] = str_replace('#', '', $Format['Code']);
  557. $Matches = array();
  558. if (preg_match('/(0+)(\.?)(0*)/', preg_replace('/\[[^\]]+\]/', '', $Format['Code']), $Matches)) {
  559. $Integer = $Matches[1];
  560. $DecimalPoint = $Matches[2];
  561. $Decimals = $Matches[3];
  562. $Format['MinWidth'] = strlen($Integer) + strlen($DecimalPoint) + strlen($Decimals);
  563. $Format['Decimals'] = $Decimals;
  564. $Format['Precision'] = strlen($Format['Decimals']);
  565. $Format['Pattern'] = '%0' . $Format['MinWidth'] . '.' . $Format['Precision'] . 'f';
  566. }
  567. }
  568. $Matches = array();
  569. if (preg_match('/\[\$(.*)\]/u', $Format['Code'], $Matches)) {
  570. $CurrFormat = $Matches[0];
  571. $CurrCode = $Matches[1];
  572. $CurrCode = explode('-', $CurrCode);
  573. if ($CurrCode) {
  574. $CurrCode = $CurrCode[0];
  575. }
  576. if (! $CurrCode) {
  577. $CurrCode = self::$CurrencyCode;
  578. }
  579. $Format['Currency'] = $CurrCode;
  580. }
  581. $Format['Code'] = trim($Format['Code']);
  582. }
  583. $this->ParsedFormatCache[$Index] = $Format;
  584. }
  585. // Applying format to value
  586. if ($Format) {
  587. if ($Format['Code'] == '@') {
  588. return (string)$Value;
  589. } // Percentages
  590. elseif ($Format['Type'] == 'Percentage') {
  591. if ($Format['Code'] === '0%') {
  592. $Value = round(100*$Value, 0) . '%';
  593. } else {
  594. $Value = sprintf('%.2f%%', round(100*$Value, 2));
  595. }
  596. } // Dates and times
  597. elseif ($Format['Type'] == 'DateTime') {
  598. $Days = (int)$Value;
  599. // Correcting for Feb 29, 1900
  600. if ($Days > 60) {
  601. $Days--;
  602. }
  603. // At this point time is a fraction of a day
  604. $Time = ($Value - (int)$Value);
  605. $Seconds = 0;
  606. if ($Time) {
  607. // Here time is converted to seconds
  608. // Some loss of precision will occur
  609. $Seconds = (int)($Time*86400);
  610. }
  611. $Value = clone self::$BaseDate;
  612. $Value->add(new DateInterval('P' . $Days . 'D' . ($Seconds ? 'T' . $Seconds . 'S' : '')));
  613. if (! $this->Options['ReturnDateTimeObjects']) {
  614. $Value = $Value->format($Format['Code']);
  615. } else {
  616. // A DateTime object is returned
  617. }
  618. } elseif ($Format['Type'] == 'Euro') {
  619. $Value = 'EUR ' . sprintf('%1.2f', $Value);
  620. } else {
  621. // Fractional numbers
  622. if ($Format['Type'] == 'Fraction' && ($Value != (int)$Value)) {
  623. $Integer = floor(abs($Value));
  624. $Decimal = fmod(abs($Value), 1);
  625. // Removing the integer part and decimal point
  626. $Decimal *= pow(10, strlen($Decimal) - 2);
  627. $DecimalDivisor = pow(10, strlen($Decimal));
  628. if (self::$RuntimeInfo['GMPSupported']) {
  629. $GCD = gmp_strval(gmp_gcd($Decimal, $DecimalDivisor));
  630. } else {
  631. $GCD = self::GCD($Decimal, $DecimalDivisor);
  632. }
  633. $AdjDecimal = $Decimal/$GCD;
  634. $AdjDecimalDivisor = $DecimalDivisor/$GCD;
  635. if (strpos($Format['Code'], '0') !== false || strpos($Format['Code'], '#') !== false || substr($Format['Code'], 0, 3) == '? ?') {
  636. // The integer part is shown separately apart from the fraction
  637. $Value = ($Value < 0 ? '-' : '') . $Integer ? $Integer . ' ' : '' . $AdjDecimal . '/' . $AdjDecimalDivisor;
  638. } else {
  639. // The fraction includes the integer part
  640. $AdjDecimal += $Integer*$AdjDecimalDivisor;
  641. $Value = ($Value < 0 ? '-' : '') . $AdjDecimal . '/' . $AdjDecimalDivisor;
  642. }
  643. } else {
  644. // Scaling
  645. $Value = $Value/$Format['Scale'];
  646. if (! empty($Format['MinWidth']) && $Format['Decimals']) {
  647. if ($Format['Thousands']) {
  648. $Value = number_format($Value, $Format['Precision'], self::$DecimalSeparator, self::$ThousandSeparator);
  649. $Value = preg_replace('/(0+)(\.?)(0*)/', $Value, $Format['Code']);
  650. } else {
  651. if (preg_match('/[0#]E[+-]0/i', $Format['Code'])) {
  652. // Scientific format
  653. $Value = sprintf('%5.2E', $Value);
  654. } else {
  655. $Value = sprintf($Format['Pattern'], $Value);
  656. $Value = preg_replace('/(0+)(\.?)(0*)/', $Value, $Format['Code']);
  657. }
  658. }
  659. }
  660. }
  661. // Currency/Accounting
  662. if ($Format['Currency']) {
  663. $Value = preg_replace('', $Format['Currency'], $Value);
  664. }
  665. }
  666. }
  667. return $Value;
  668. }
  669. /**
  670. * Attempts to approximate Excel's "general" format.
  671. *
  672. * @param mixed Value
  673. *
  674. * @return mixed Result
  675. */
  676. public function GeneralFormat($Value) {
  677. // Numeric format
  678. if (is_numeric($Value)) {
  679. $Value = (float)$Value;
  680. }
  681. return $Value;
  682. }
  683. // !Iterator interface methods
  684. /**
  685. * Rewind the Iterator to the first element.
  686. * Similar to the reset() function for arrays in PHP
  687. */
  688. public function rewind() {
  689. // Removed the check whether $this -> Index == 0 otherwise ChangeSheet doesn't work properly
  690. // If the worksheet was already iterated, XML file is reopened.
  691. // Otherwise it should be at the beginning anyway
  692. if ($this->Worksheet instanceof XMLReader) {
  693. $this->Worksheet->close();
  694. } else {
  695. $this->Worksheet = new XMLReader;
  696. }
  697. $this->Worksheet->open($this->WorksheetPath);
  698. $this->Valid = true;
  699. $this->RowOpen = false;
  700. $this->CurrentRow = false;
  701. $this->Index = 0;
  702. }
  703. /**
  704. * Return the current element.
  705. * Similar to the current() function for arrays in PHP
  706. * @return mixed current element from the collection
  707. */
  708. public function current() {
  709. if ($this->Index == 0 && $this->CurrentRow === false) {
  710. $this->rewind();
  711. $this->next();
  712. $this->Index--;
  713. }
  714. return $this->CurrentRow;
  715. }
  716. /**
  717. * Move forward to next element.
  718. * Similar to the next() function for arrays in PHP
  719. */
  720. public function next() {
  721. $this->Index++;
  722. $this->CurrentRow = array();
  723. if (! $this->RowOpen) {
  724. while ($this->Valid = $this->Worksheet->read()) {
  725. if ($this->Worksheet->name == 'row') {
  726. // Getting the row spanning area (stored as e.g., 1:12)
  727. // so that the last cells will be present, even if empty
  728. $RowSpans = $this->Worksheet->getAttribute('spans');
  729. if ($RowSpans) {
  730. $RowSpans = explode(':', $RowSpans);
  731. $CurrentRowColumnCount = $RowSpans[1];
  732. } else {
  733. $CurrentRowColumnCount = 0;
  734. }
  735. if ($CurrentRowColumnCount > 0) {
  736. $this->CurrentRow = array_fill(0, $CurrentRowColumnCount, '');
  737. }
  738. $this->RowOpen = true;
  739. break;
  740. }
  741. }
  742. }
  743. // Reading the necessary row, if found
  744. if ($this->RowOpen) {
  745. // These two are needed to control for empty cells
  746. $MaxIndex = 0;
  747. $CellCount = 0;
  748. $CellHasSharedString = false;
  749. while ($this->Valid = $this->Worksheet->read()) {
  750. switch ($this->Worksheet->name) {
  751. // End of row
  752. case 'row':
  753. if ($this->Worksheet->nodeType == XMLReader::END_ELEMENT) {
  754. $this->RowOpen = false;
  755. break 2;
  756. }
  757. break;
  758. // Cell
  759. case 'c':
  760. // If it is a closing tag, skip it
  761. if ($this->Worksheet->nodeType == XMLReader::END_ELEMENT) {
  762. continue;
  763. }
  764. $StyleId = (int)$this->Worksheet->getAttribute('s');
  765. // Get the index of the cell
  766. $Index = $this->Worksheet->getAttribute('r');
  767. $Letter = preg_replace('{[^[:alpha:]]}S', '', $Index);
  768. $Index = self::IndexFromColumnLetter($Letter);
  769. // Determine cell type
  770. if ($this->Worksheet->getAttribute('t') == self::CELL_TYPE_SHARED_STR) {
  771. $CellHasSharedString = true;
  772. } else {
  773. $CellHasSharedString = false;
  774. }
  775. $this->CurrentRow[$Index] = '';
  776. $CellCount++;
  777. if ($Index > $MaxIndex) {
  778. $MaxIndex = $Index;
  779. }
  780. break;
  781. // Cell value
  782. case 'v':
  783. case 'is':
  784. if ($this->Worksheet->nodeType == XMLReader::END_ELEMENT) {
  785. continue;
  786. }
  787. $Value = $this->Worksheet->readString();
  788. if ($CellHasSharedString) {
  789. $Value = $this->GetSharedString($Value);
  790. }
  791. // Format value if necessary
  792. if ($Value !== '' && $StyleId && isset($this->Styles[$StyleId])) {
  793. $Value = $this->FormatValue($Value, $StyleId);
  794. } elseif ($Value) {
  795. $Value = $this->GeneralFormat($Value);
  796. }
  797. $this->CurrentRow[$Index] = $Value;
  798. break;
  799. }
  800. }
  801. // Adding empty cells, if necessary
  802. // Only empty cells inbetween and on the left side are added
  803. if ($MaxIndex + 1 > $CellCount) {
  804. $this->CurrentRow = $this->CurrentRow + array_fill(0, $MaxIndex + 1, '');
  805. ksort($this->CurrentRow);
  806. }
  807. }
  808. return $this->CurrentRow;
  809. }
  810. /**
  811. * Return the identifying key of the current element.
  812. * Similar to the key() function for arrays in PHP
  813. * @return mixed either an integer or a string
  814. */
  815. public function key() {
  816. return $this->Index;
  817. }
  818. /**
  819. * Check if there is a current element after calls to rewind() or next().
  820. * Used to check if we've iterated to the end of the collection
  821. * @return boolean FALSE if there's nothing more to iterate over
  822. */
  823. public function valid() {
  824. return $this->Valid;
  825. }
  826. // !Countable interface method
  827. /**
  828. * Ostensibly should return the count of the contained items but this just returns the number
  829. * of rows read so far. It's not really correct but at least coherent.
  830. */
  831. public function count() {
  832. if (! isset($this->rowCount)) {
  833. $total = 0;
  834. $this->rewind();
  835. while ($this->Worksheet->read()) {
  836. if ($this->Worksheet->name == 'row' && $this->Worksheet->nodeType != XMLReader::END_ELEMENT) {
  837. $total++;
  838. }
  839. }
  840. $this->rowCount = $total;
  841. }
  842. return $this->rowCount;
  843. }
  844. /**
  845. * Takes the column letter and converts it to a numerical index (0-based)
  846. *
  847. * @param string Letter(s) to convert
  848. *
  849. * @return mixed Numeric index (0-based) or boolean false if it cannot be calculated
  850. */
  851. public static function IndexFromColumnLetter($Letter) {
  852. $Powers = array();
  853. $Letter = strtoupper($Letter);
  854. $Result = 0;
  855. for ($i = strlen($Letter) - 1, $j = 0; $i >= 0; $i--, $j++) {
  856. $Ord = ord($Letter[$i]) - 64;
  857. if ($Ord > 26) {
  858. // Something is very, very wrong
  859. return false;
  860. }
  861. $Result += $Ord*pow(26, $j);
  862. }
  863. return $Result - 1;
  864. }
  865. /**
  866. * Helper function for greatest common divisor calculation in case GMP extension is not enabled
  867. *
  868. * @param int Number #1
  869. * @param int Number #2
  870. * @param int Greatest common divisor
  871. * @return int
  872. */
  873. public static function GCD($A, $B) {
  874. $A = abs($A);
  875. $B = abs($B);
  876. if ($A + $B == 0) {
  877. return 0;
  878. } else {
  879. $C = 1;
  880. while ($A > 0) {
  881. $C = $A;
  882. $A = $B%$A;
  883. $B = $C;
  884. }
  885. return $C;
  886. }
  887. }
  888. }