Slk.php 18 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496
  1. <?php
  2. namespace PhpOffice\PhpSpreadsheet\Reader;
  3. use PhpOffice\PhpSpreadsheet\Calculation\Calculation;
  4. use PhpOffice\PhpSpreadsheet\Cell\Coordinate;
  5. use PhpOffice\PhpSpreadsheet\Shared\StringHelper;
  6. use PhpOffice\PhpSpreadsheet\Spreadsheet;
  7. use PhpOffice\PhpSpreadsheet\Style\Border;
  8. class Slk extends BaseReader
  9. {
  10. /**
  11. * Input encoding.
  12. *
  13. * @var string
  14. */
  15. private $inputEncoding = 'ANSI';
  16. /**
  17. * Sheet index to read.
  18. *
  19. * @var int
  20. */
  21. private $sheetIndex = 0;
  22. /**
  23. * Formats.
  24. *
  25. * @var array
  26. */
  27. private $formats = [];
  28. /**
  29. * Format Count.
  30. *
  31. * @var int
  32. */
  33. private $format = 0;
  34. /**
  35. * Create a new SYLK Reader instance.
  36. */
  37. public function __construct()
  38. {
  39. $this->readFilter = new DefaultReadFilter();
  40. }
  41. /**
  42. * Validate that the current file is a SYLK file.
  43. *
  44. * @param string $pFilename
  45. *
  46. * @return bool
  47. */
  48. public function canRead($pFilename)
  49. {
  50. // Check if file exists
  51. try {
  52. $this->openFile($pFilename);
  53. } catch (Exception $e) {
  54. return false;
  55. }
  56. // Read sample data (first 2 KB will do)
  57. $data = fread($this->fileHandle, 2048);
  58. // Count delimiters in file
  59. $delimiterCount = substr_count($data, ';');
  60. $hasDelimiter = $delimiterCount > 0;
  61. // Analyze first line looking for ID; signature
  62. $lines = explode("\n", $data);
  63. $hasId = substr($lines[0], 0, 4) === 'ID;P';
  64. fclose($this->fileHandle);
  65. return $hasDelimiter && $hasId;
  66. }
  67. /**
  68. * Set input encoding.
  69. *
  70. * @param string $pValue Input encoding, eg: 'ANSI'
  71. *
  72. * @return Slk
  73. */
  74. public function setInputEncoding($pValue)
  75. {
  76. $this->inputEncoding = $pValue;
  77. return $this;
  78. }
  79. /**
  80. * Get input encoding.
  81. *
  82. * @return string
  83. */
  84. public function getInputEncoding()
  85. {
  86. return $this->inputEncoding;
  87. }
  88. /**
  89. * Return worksheet info (Name, Last Column Letter, Last Column Index, Total Rows, Total Columns).
  90. *
  91. * @param string $pFilename
  92. *
  93. * @throws Exception
  94. *
  95. * @return array
  96. */
  97. public function listWorksheetInfo($pFilename)
  98. {
  99. // Open file
  100. if (!$this->canRead($pFilename)) {
  101. throw new Exception($pFilename . ' is an Invalid Spreadsheet file.');
  102. }
  103. $this->openFile($pFilename);
  104. $fileHandle = $this->fileHandle;
  105. rewind($fileHandle);
  106. $worksheetInfo = [];
  107. $worksheetInfo[0]['worksheetName'] = 'Worksheet';
  108. $worksheetInfo[0]['lastColumnLetter'] = 'A';
  109. $worksheetInfo[0]['lastColumnIndex'] = 0;
  110. $worksheetInfo[0]['totalRows'] = 0;
  111. $worksheetInfo[0]['totalColumns'] = 0;
  112. // loop through one row (line) at a time in the file
  113. $rowIndex = 0;
  114. while (($rowData = fgets($fileHandle)) !== false) {
  115. $columnIndex = 0;
  116. // convert SYLK encoded $rowData to UTF-8
  117. $rowData = StringHelper::SYLKtoUTF8($rowData);
  118. // explode each row at semicolons while taking into account that literal semicolon (;)
  119. // is escaped like this (;;)
  120. $rowData = explode("\t", str_replace('¤', ';', str_replace(';', "\t", str_replace(';;', '¤', rtrim($rowData)))));
  121. $dataType = array_shift($rowData);
  122. if ($dataType == 'C') {
  123. // Read cell value data
  124. foreach ($rowData as $rowDatum) {
  125. switch ($rowDatum[0]) {
  126. case 'C':
  127. case 'X':
  128. $columnIndex = substr($rowDatum, 1) - 1;
  129. break;
  130. case 'R':
  131. case 'Y':
  132. $rowIndex = substr($rowDatum, 1);
  133. break;
  134. }
  135. $worksheetInfo[0]['totalRows'] = max($worksheetInfo[0]['totalRows'], $rowIndex);
  136. $worksheetInfo[0]['lastColumnIndex'] = max($worksheetInfo[0]['lastColumnIndex'], $columnIndex);
  137. }
  138. }
  139. }
  140. $worksheetInfo[0]['lastColumnLetter'] = Coordinate::stringFromColumnIndex($worksheetInfo[0]['lastColumnIndex'] + 1);
  141. $worksheetInfo[0]['totalColumns'] = $worksheetInfo[0]['lastColumnIndex'] + 1;
  142. // Close file
  143. fclose($fileHandle);
  144. return $worksheetInfo;
  145. }
  146. /**
  147. * Loads PhpSpreadsheet from file.
  148. *
  149. * @param string $pFilename
  150. *
  151. * @throws Exception
  152. *
  153. * @return Spreadsheet
  154. */
  155. public function load($pFilename)
  156. {
  157. // Create new Spreadsheet
  158. $spreadsheet = new Spreadsheet();
  159. // Load into this instance
  160. return $this->loadIntoExisting($pFilename, $spreadsheet);
  161. }
  162. /**
  163. * Loads PhpSpreadsheet from file into PhpSpreadsheet instance.
  164. *
  165. * @param string $pFilename
  166. * @param Spreadsheet $spreadsheet
  167. *
  168. * @throws Exception
  169. *
  170. * @return Spreadsheet
  171. */
  172. public function loadIntoExisting($pFilename, Spreadsheet $spreadsheet)
  173. {
  174. // Open file
  175. if (!$this->canRead($pFilename)) {
  176. throw new Exception($pFilename . ' is an Invalid Spreadsheet file.');
  177. }
  178. $this->openFile($pFilename);
  179. $fileHandle = $this->fileHandle;
  180. rewind($fileHandle);
  181. // Create new Worksheets
  182. while ($spreadsheet->getSheetCount() <= $this->sheetIndex) {
  183. $spreadsheet->createSheet();
  184. }
  185. $spreadsheet->setActiveSheetIndex($this->sheetIndex);
  186. $fromFormats = ['\-', '\ '];
  187. $toFormats = ['-', ' '];
  188. // Loop through file
  189. $column = $row = '';
  190. // loop through one row (line) at a time in the file
  191. while (($rowData = fgets($fileHandle)) !== false) {
  192. // convert SYLK encoded $rowData to UTF-8
  193. $rowData = StringHelper::SYLKtoUTF8($rowData);
  194. // explode each row at semicolons while taking into account that literal semicolon (;)
  195. // is escaped like this (;;)
  196. $rowData = explode("\t", str_replace('¤', ';', str_replace(';', "\t", str_replace(';;', '¤', rtrim($rowData)))));
  197. $dataType = array_shift($rowData);
  198. // Read shared styles
  199. if ($dataType == 'P') {
  200. $formatArray = [];
  201. foreach ($rowData as $rowDatum) {
  202. switch ($rowDatum[0]) {
  203. case 'P':
  204. $formatArray['numberFormat']['formatCode'] = str_replace($fromFormats, $toFormats, substr($rowDatum, 1));
  205. break;
  206. case 'E':
  207. case 'F':
  208. $formatArray['font']['name'] = substr($rowDatum, 1);
  209. break;
  210. case 'L':
  211. $formatArray['font']['size'] = substr($rowDatum, 1);
  212. break;
  213. case 'S':
  214. $styleSettings = substr($rowDatum, 1);
  215. $iMax = strlen($styleSettings);
  216. for ($i = 0; $i < $iMax; ++$i) {
  217. switch ($styleSettings[$i]) {
  218. case 'I':
  219. $formatArray['font']['italic'] = true;
  220. break;
  221. case 'D':
  222. $formatArray['font']['bold'] = true;
  223. break;
  224. case 'T':
  225. $formatArray['borders']['top']['borderStyle'] = Border::BORDER_THIN;
  226. break;
  227. case 'B':
  228. $formatArray['borders']['bottom']['borderStyle'] = Border::BORDER_THIN;
  229. break;
  230. case 'L':
  231. $formatArray['borders']['left']['borderStyle'] = Border::BORDER_THIN;
  232. break;
  233. case 'R':
  234. $formatArray['borders']['right']['borderStyle'] = Border::BORDER_THIN;
  235. break;
  236. }
  237. }
  238. break;
  239. }
  240. }
  241. $this->formats['P' . $this->format++] = $formatArray;
  242. // Read cell value data
  243. } elseif ($dataType == 'C') {
  244. $hasCalculatedValue = false;
  245. $cellData = $cellDataFormula = '';
  246. foreach ($rowData as $rowDatum) {
  247. switch ($rowDatum[0]) {
  248. case 'C':
  249. case 'X':
  250. $column = substr($rowDatum, 1);
  251. break;
  252. case 'R':
  253. case 'Y':
  254. $row = substr($rowDatum, 1);
  255. break;
  256. case 'K':
  257. $cellData = substr($rowDatum, 1);
  258. break;
  259. case 'E':
  260. $cellDataFormula = '=' . substr($rowDatum, 1);
  261. // Convert R1C1 style references to A1 style references (but only when not quoted)
  262. $temp = explode('"', $cellDataFormula);
  263. $key = false;
  264. foreach ($temp as &$value) {
  265. // Only count/replace in alternate array entries
  266. if ($key = !$key) {
  267. preg_match_all('/(R(\[?-?\d*\]?))(C(\[?-?\d*\]?))/', $value, $cellReferences, PREG_SET_ORDER + PREG_OFFSET_CAPTURE);
  268. // Reverse the matches array, otherwise all our offsets will become incorrect if we modify our way
  269. // through the formula from left to right. Reversing means that we work right to left.through
  270. // the formula
  271. $cellReferences = array_reverse($cellReferences);
  272. // Loop through each R1C1 style reference in turn, converting it to its A1 style equivalent,
  273. // then modify the formula to use that new reference
  274. foreach ($cellReferences as $cellReference) {
  275. $rowReference = $cellReference[2][0];
  276. // Empty R reference is the current row
  277. if ($rowReference == '') {
  278. $rowReference = $row;
  279. }
  280. // Bracketed R references are relative to the current row
  281. if ($rowReference[0] == '[') {
  282. $rowReference = $row + trim($rowReference, '[]');
  283. }
  284. $columnReference = $cellReference[4][0];
  285. // Empty C reference is the current column
  286. if ($columnReference == '') {
  287. $columnReference = $column;
  288. }
  289. // Bracketed C references are relative to the current column
  290. if ($columnReference[0] == '[') {
  291. $columnReference = $column + trim($columnReference, '[]');
  292. }
  293. $A1CellReference = Coordinate::stringFromColumnIndex($columnReference) . $rowReference;
  294. $value = substr_replace($value, $A1CellReference, $cellReference[0][1], strlen($cellReference[0][0]));
  295. }
  296. }
  297. }
  298. unset($value);
  299. // Then rebuild the formula string
  300. $cellDataFormula = implode('"', $temp);
  301. $hasCalculatedValue = true;
  302. break;
  303. }
  304. }
  305. $columnLetter = Coordinate::stringFromColumnIndex($column);
  306. $cellData = Calculation::unwrapResult($cellData);
  307. // Set cell value
  308. $spreadsheet->getActiveSheet()->getCell($columnLetter . $row)->setValue(($hasCalculatedValue) ? $cellDataFormula : $cellData);
  309. if ($hasCalculatedValue) {
  310. $cellData = Calculation::unwrapResult($cellData);
  311. $spreadsheet->getActiveSheet()->getCell($columnLetter . $row)->setCalculatedValue($cellData);
  312. }
  313. // Read cell formatting
  314. } elseif ($dataType == 'F') {
  315. $formatStyle = $columnWidth = $styleSettings = '';
  316. $styleData = [];
  317. foreach ($rowData as $rowDatum) {
  318. switch ($rowDatum[0]) {
  319. case 'C':
  320. case 'X':
  321. $column = substr($rowDatum, 1);
  322. break;
  323. case 'R':
  324. case 'Y':
  325. $row = substr($rowDatum, 1);
  326. break;
  327. case 'P':
  328. $formatStyle = $rowDatum;
  329. break;
  330. case 'W':
  331. list($startCol, $endCol, $columnWidth) = explode(' ', substr($rowDatum, 1));
  332. break;
  333. case 'S':
  334. $styleSettings = substr($rowDatum, 1);
  335. $iMax = strlen($styleSettings);
  336. for ($i = 0; $i < $iMax; ++$i) {
  337. switch ($styleSettings[$i]) {
  338. case 'I':
  339. $styleData['font']['italic'] = true;
  340. break;
  341. case 'D':
  342. $styleData['font']['bold'] = true;
  343. break;
  344. case 'T':
  345. $styleData['borders']['top']['borderStyle'] = Border::BORDER_THIN;
  346. break;
  347. case 'B':
  348. $styleData['borders']['bottom']['borderStyle'] = Border::BORDER_THIN;
  349. break;
  350. case 'L':
  351. $styleData['borders']['left']['borderStyle'] = Border::BORDER_THIN;
  352. break;
  353. case 'R':
  354. $styleData['borders']['right']['borderStyle'] = Border::BORDER_THIN;
  355. break;
  356. }
  357. }
  358. break;
  359. }
  360. }
  361. if (($formatStyle > '') && ($column > '') && ($row > '')) {
  362. $columnLetter = Coordinate::stringFromColumnIndex($column);
  363. if (isset($this->formats[$formatStyle])) {
  364. $spreadsheet->getActiveSheet()->getStyle($columnLetter . $row)->applyFromArray($this->formats[$formatStyle]);
  365. }
  366. }
  367. if ((!empty($styleData)) && ($column > '') && ($row > '')) {
  368. $columnLetter = Coordinate::stringFromColumnIndex($column);
  369. $spreadsheet->getActiveSheet()->getStyle($columnLetter . $row)->applyFromArray($styleData);
  370. }
  371. if ($columnWidth > '') {
  372. if ($startCol == $endCol) {
  373. $startCol = Coordinate::stringFromColumnIndex($startCol);
  374. $spreadsheet->getActiveSheet()->getColumnDimension($startCol)->setWidth($columnWidth);
  375. } else {
  376. $startCol = Coordinate::stringFromColumnIndex($startCol);
  377. $endCol = Coordinate::stringFromColumnIndex($endCol);
  378. $spreadsheet->getActiveSheet()->getColumnDimension($startCol)->setWidth($columnWidth);
  379. do {
  380. $spreadsheet->getActiveSheet()->getColumnDimension(++$startCol)->setWidth($columnWidth);
  381. } while ($startCol != $endCol);
  382. }
  383. }
  384. } else {
  385. foreach ($rowData as $rowDatum) {
  386. switch ($rowDatum[0]) {
  387. case 'C':
  388. case 'X':
  389. $column = substr($rowDatum, 1);
  390. break;
  391. case 'R':
  392. case 'Y':
  393. $row = substr($rowDatum, 1);
  394. break;
  395. }
  396. }
  397. }
  398. }
  399. // Close file
  400. fclose($fileHandle);
  401. // Return
  402. return $spreadsheet;
  403. }
  404. /**
  405. * Get sheet index.
  406. *
  407. * @return int
  408. */
  409. public function getSheetIndex()
  410. {
  411. return $this->sheetIndex;
  412. }
  413. /**
  414. * Set sheet index.
  415. *
  416. * @param int $pValue Sheet index
  417. *
  418. * @return Slk
  419. */
  420. public function setSheetIndex($pValue)
  421. {
  422. $this->sheetIndex = $pValue;
  423. return $this;
  424. }
  425. }