ILIAS  release_5-4 Revision v5.4.26-12-gabc799a52e6
Slk.php
Go to the documentation of this file.
1<?php
2
4
5use InvalidArgumentException;
13
14class Slk extends BaseReader
15{
21 private $inputEncoding = 'ANSI';
22
28 private $sheetIndex = 0;
29
35 private $formats = [];
36
42 private $format = 0;
43
49 private $fonts = [];
50
56 private $fontcount = 0;
57
61 public function __construct()
62 {
63 parent::__construct();
64 }
65
73 public function canRead($pFilename)
74 {
75 try {
76 $this->openFile($pFilename);
77 } catch (InvalidArgumentException $e) {
78 return false;
79 }
80
81 // Read sample data (first 2 KB will do)
82 $data = fread($this->fileHandle, 2048);
83
84 // Count delimiters in file
85 $delimiterCount = substr_count($data, ';');
86 $hasDelimiter = $delimiterCount > 0;
87
88 // Analyze first line looking for ID; signature
89 $lines = explode("\n", $data);
90 $hasId = substr($lines[0], 0, 4) === 'ID;P';
91
92 fclose($this->fileHandle);
93
94 return $hasDelimiter && $hasId;
95 }
96
97 private function canReadOrBust(string $pFilename): void
98 {
99 if (!$this->canRead($pFilename)) {
100 throw new ReaderException($pFilename . ' is an Invalid SYLK file.');
101 }
102 $this->openFile($pFilename);
103 }
104
116 public function setInputEncoding($pValue)
117 {
118 $this->inputEncoding = $pValue;
119
120 return $this;
121 }
122
132 public function getInputEncoding()
133 {
135 }
136
144 public function listWorksheetInfo($pFilename)
145 {
146 // Open file
147 $this->canReadOrBust($pFilename);
149 rewind($fileHandle);
150
151 $worksheetInfo = [];
152 $worksheetInfo[0]['worksheetName'] = basename($pFilename, '.slk');
153
154 // loop through one row (line) at a time in the file
155 $rowIndex = 0;
156 $columnIndex = 0;
157 while (($rowData = fgets($fileHandle)) !== false) {
158 $columnIndex = 0;
159
160 // convert SYLK encoded $rowData to UTF-8
161 $rowData = StringHelper::SYLKtoUTF8($rowData);
162
163 // explode each row at semicolons while taking into account that literal semicolon (;)
164 // is escaped like this (;;)
165 $rowData = explode("\t", str_replace('¤', ';', str_replace(';', "\t", str_replace(';;', '¤', rtrim($rowData)))));
166
167 $dataType = array_shift($rowData);
168 if ($dataType == 'B') {
169 foreach ($rowData as $rowDatum) {
170 switch ($rowDatum[0]) {
171 case 'X':
172 $columnIndex = (int) substr($rowDatum, 1) - 1;
173
174 break;
175 case 'Y':
176 $rowIndex = substr($rowDatum, 1);
177
178 break;
179 }
180 }
181
182 break;
183 }
184 }
185
186 $worksheetInfo[0]['lastColumnIndex'] = $columnIndex;
187 $worksheetInfo[0]['totalRows'] = $rowIndex;
188 $worksheetInfo[0]['lastColumnLetter'] = Coordinate::stringFromColumnIndex($worksheetInfo[0]['lastColumnIndex'] + 1);
189 $worksheetInfo[0]['totalColumns'] = $worksheetInfo[0]['lastColumnIndex'] + 1;
190
191 // Close file
192 fclose($fileHandle);
193
194 return $worksheetInfo;
195 }
196
204 public function load($pFilename)
205 {
206 // Create new Spreadsheet
207 $spreadsheet = new Spreadsheet();
208
209 // Load into this instance
210 return $this->loadIntoExisting($pFilename, $spreadsheet);
211 }
212
213 private $colorArray = [
214 'FF00FFFF', // 0 - cyan
215 'FF000000', // 1 - black
216 'FFFFFFFF', // 2 - white
217 'FFFF0000', // 3 - red
218 'FF00FF00', // 4 - green
219 'FF0000FF', // 5 - blue
220 'FFFFFF00', // 6 - yellow
221 'FFFF00FF', // 7 - magenta
222 ];
223
225 'B' => 'bold',
226 'I' => 'italic',
227 'U' => 'underline',
228 ];
229
230 private function processFormula(string $rowDatum, bool &$hasCalculatedValue, string &$cellDataFormula, string $row, string $column): void
231 {
232 $cellDataFormula = '=' . substr($rowDatum, 1);
233 // Convert R1C1 style references to A1 style references (but only when not quoted)
234 $temp = explode('"', $cellDataFormula);
235 $key = false;
236 foreach ($temp as &$value) {
237 // Only count/replace in alternate array entries
238 if ($key = !$key) {
239 preg_match_all('/(R(\[?-?\d*\]?))(C(\[?-?\d*\]?))/', $value, $cellReferences, PREG_SET_ORDER + PREG_OFFSET_CAPTURE);
240 // Reverse the matches array, otherwise all our offsets will become incorrect if we modify our way
241 // through the formula from left to right. Reversing means that we work right to left.through
242 // the formula
243 $cellReferences = array_reverse($cellReferences);
244 // Loop through each R1C1 style reference in turn, converting it to its A1 style equivalent,
245 // then modify the formula to use that new reference
246 foreach ($cellReferences as $cellReference) {
247 $rowReference = $cellReference[2][0];
248 // Empty R reference is the current row
249 if ($rowReference == '') {
250 $rowReference = $row;
251 }
252 // Bracketed R references are relative to the current row
253 if ($rowReference[0] == '[') {
254 $rowReference = (int) $row + (int) trim($rowReference, '[]');
255 }
256 $columnReference = $cellReference[4][0];
257 // Empty C reference is the current column
258 if ($columnReference == '') {
259 $columnReference = $column;
260 }
261 // Bracketed C references are relative to the current column
262 if ($columnReference[0] == '[') {
263 $columnReference = (int) $column + (int) trim($columnReference, '[]');
264 }
265 $A1CellReference = Coordinate::stringFromColumnIndex($columnReference) . $rowReference;
266
267 $value = substr_replace($value, $A1CellReference, $cellReference[0][1], strlen($cellReference[0][0]));
268 }
269 }
270 }
271 unset($value);
272 // Then rebuild the formula string
273 $cellDataFormula = implode('"', $temp);
274 $hasCalculatedValue = true;
275 }
276
277 private function processCRecord(array $rowData, Spreadsheet &$spreadsheet, string &$row, string &$column): void
278 {
279 // Read cell value data
280 $hasCalculatedValue = false;
281 $cellDataFormula = $cellData = '';
282 foreach ($rowData as $rowDatum) {
283 switch ($rowDatum[0]) {
284 case 'C':
285 case 'X':
286 $column = substr($rowDatum, 1);
287
288 break;
289 case 'R':
290 case 'Y':
291 $row = substr($rowDatum, 1);
292
293 break;
294 case 'K':
295 $cellData = substr($rowDatum, 1);
296
297 break;
298 case 'E':
299 $this->processFormula($rowDatum, $hasCalculatedValue, $cellDataFormula, $row, $column);
300
301 break;
302 }
303 }
304 $columnLetter = Coordinate::stringFromColumnIndex((int) $column);
305 $cellData = Calculation::unwrapResult($cellData);
306
307 // Set cell value
308 $this->processCFinal($spreadsheet, $hasCalculatedValue, $cellDataFormula, $cellData, "$columnLetter$row");
309 }
310
311 private function processCFinal(Spreadsheet &$spreadsheet, bool $hasCalculatedValue, string $cellDataFormula, string $cellData, string $coordinate): void
312 {
313 // Set cell value
314 $spreadsheet->getActiveSheet()->getCell($coordinate)->setValue(($hasCalculatedValue) ? $cellDataFormula : $cellData);
315 if ($hasCalculatedValue) {
316 $cellData = Calculation::unwrapResult($cellData);
317 $spreadsheet->getActiveSheet()->getCell($coordinate)->setCalculatedValue($cellData);
318 }
319 }
320
321 private function processFRecord(array $rowData, Spreadsheet &$spreadsheet, string &$row, string &$column): void
322 {
323 // Read cell formatting
324 $formatStyle = $columnWidth = '';
325 $startCol = $endCol = '';
326 $fontStyle = '';
327 $styleData = [];
328 foreach ($rowData as $rowDatum) {
329 switch ($rowDatum[0]) {
330 case 'C':
331 case 'X':
332 $column = substr($rowDatum, 1);
333
334 break;
335 case 'R':
336 case 'Y':
337 $row = substr($rowDatum, 1);
338
339 break;
340 case 'P':
341 $formatStyle = $rowDatum;
342
343 break;
344 case 'W':
345 [$startCol, $endCol, $columnWidth] = explode(' ', substr($rowDatum, 1));
346
347 break;
348 case 'S':
349 $this->styleSettings($rowDatum, $styleData, $fontStyle);
350
351 break;
352 }
353 }
354 $this->addFormats($spreadsheet, $formatStyle, $row, $column);
355 $this->addFonts($spreadsheet, $fontStyle, $row, $column);
356 $this->addStyle($spreadsheet, $styleData, $row, $column);
357 $this->addWidth($spreadsheet, $columnWidth, $startCol, $endCol);
358 }
359
360 private $styleSettingsFont = ['D' => 'bold', 'I' => 'italic'];
361
363 'B' => 'bottom',
364 'L' => 'left',
365 'R' => 'right',
366 'T' => 'top',
367 ];
368
369 private function styleSettings(string $rowDatum, array &$styleData, string &$fontStyle): void
370 {
371 $styleSettings = substr($rowDatum, 1);
372 $iMax = strlen($styleSettings);
373 for ($i = 0; $i < $iMax; ++$i) {
374 $char = $styleSettings[$i];
375 if (array_key_exists($char, $this->styleSettingsFont)) {
376 $styleData['font'][$this->styleSettingsFont[$char]] = true;
377 } elseif (array_key_exists($char, $this->styleSettingsBorder)) {
378 $styleData['borders'][$this->styleSettingsBorder[$char]]['borderStyle'] = Border::BORDER_THIN;
379 } elseif ($char == 'S') {
380 $styleData['fill']['fillType'] = \PhpOffice\PhpSpreadsheet\Style\Fill::FILL_PATTERN_GRAY125;
381 } elseif ($char == 'M') {
382 if (preg_match('/M([1-9]\\d*)/', $styleSettings, $matches)) {
383 $fontStyle = $matches[1];
384 }
385 }
386 }
387 }
388
389 private function addFormats(Spreadsheet &$spreadsheet, string $formatStyle, string $row, string $column): void
390 {
391 if ($formatStyle && $column > '' && $row > '') {
392 $columnLetter = Coordinate::stringFromColumnIndex((int) $column);
393 if (isset($this->formats[$formatStyle])) {
394 $spreadsheet->getActiveSheet()->getStyle($columnLetter . $row)->applyFromArray($this->formats[$formatStyle]);
395 }
396 }
397 }
398
399 private function addFonts(Spreadsheet &$spreadsheet, string $fontStyle, string $row, string $column): void
400 {
401 if ($fontStyle && $column > '' && $row > '') {
402 $columnLetter = Coordinate::stringFromColumnIndex((int) $column);
403 if (isset($this->fonts[$fontStyle])) {
404 $spreadsheet->getActiveSheet()->getStyle($columnLetter . $row)->applyFromArray($this->fonts[$fontStyle]);
405 }
406 }
407 }
408
409 private function addStyle(Spreadsheet &$spreadsheet, array $styleData, string $row, string $column): void
410 {
411 if ((!empty($styleData)) && $column > '' && $row > '') {
412 $columnLetter = Coordinate::stringFromColumnIndex($column);
413 $spreadsheet->getActiveSheet()->getStyle($columnLetter . $row)->applyFromArray($styleData);
414 }
415 }
416
417 private function addWidth(Spreadsheet $spreadsheet, string $columnWidth, string $startCol, string $endCol): void
418 {
419 if ($columnWidth > '') {
420 if ($startCol == $endCol) {
421 $startCol = Coordinate::stringFromColumnIndex((int) $startCol);
422 $spreadsheet->getActiveSheet()->getColumnDimension($startCol)->setWidth((float) $columnWidth);
423 } else {
424 $startCol = Coordinate::stringFromColumnIndex($startCol);
425 $endCol = Coordinate::stringFromColumnIndex($endCol);
426 $spreadsheet->getActiveSheet()->getColumnDimension($startCol)->setWidth((float) $columnWidth);
427 do {
428 $spreadsheet->getActiveSheet()->getColumnDimension(++$startCol)->setWidth((float) $columnWidth);
429 } while ($startCol !== $endCol);
430 }
431 }
432 }
433
434 private function processPRecord(array $rowData, Spreadsheet &$spreadsheet): void
435 {
436 // Read shared styles
437 $formatArray = [];
438 $fromFormats = ['\-', '\ '];
439 $toFormats = ['-', ' '];
440 foreach ($rowData as $rowDatum) {
441 switch ($rowDatum[0]) {
442 case 'P':
443 $formatArray['numberFormat']['formatCode'] = str_replace($fromFormats, $toFormats, substr($rowDatum, 1));
444
445 break;
446 case 'E':
447 case 'F':
448 $formatArray['font']['name'] = substr($rowDatum, 1);
449
450 break;
451 case 'M':
452 $formatArray['font']['size'] = substr($rowDatum, 1) / 20;
453
454 break;
455 case 'L':
456 $this->processPColors($rowDatum, $formatArray);
457
458 break;
459 case 'S':
460 $this->processPFontStyles($rowDatum, $formatArray);
461
462 break;
463 }
464 }
465 $this->processPFinal($spreadsheet, $formatArray);
466 }
467
468 private function processPColors(string $rowDatum, array &$formatArray): void
469 {
470 if (preg_match('/L([1-9]\\d*)/', $rowDatum, $matches)) {
471 $fontColor = $matches[1] % 8;
472 $formatArray['font']['color']['argb'] = $this->colorArray[$fontColor];
473 }
474 }
475
476 private function processPFontStyles(string $rowDatum, array &$formatArray): void
477 {
478 $styleSettings = substr($rowDatum, 1);
479 $iMax = strlen($styleSettings);
480 for ($i = 0; $i < $iMax; ++$i) {
481 if (array_key_exists($styleSettings[$i], $this->fontStyleMappings)) {
482 $formatArray['font'][$this->fontStyleMappings[$styleSettings[$i]]] = true;
483 }
484 }
485 }
486
487 private function processPFinal(Spreadsheet &$spreadsheet, array $formatArray): void
488 {
489 if (array_key_exists('numberFormat', $formatArray)) {
490 $this->formats['P' . $this->format] = $formatArray;
492 } elseif (array_key_exists('font', $formatArray)) {
494 $this->fonts[$this->fontcount] = $formatArray;
495 if ($this->fontcount === 1) {
496 $spreadsheet->getDefaultStyle()->applyFromArray($formatArray);
497 }
498 }
499 }
500
508 public function loadIntoExisting($pFilename, Spreadsheet $spreadsheet)
509 {
510 // Open file
511 $this->canReadOrBust($pFilename);
513 rewind($fileHandle);
514
515 // Create new Worksheets
516 while ($spreadsheet->getSheetCount() <= $this->sheetIndex) {
517 $spreadsheet->createSheet();
518 }
519 $spreadsheet->setActiveSheetIndex($this->sheetIndex);
520 $spreadsheet->getActiveSheet()->setTitle(substr(basename($pFilename, '.slk'), 0, Worksheet::SHEET_TITLE_MAXIMUM_LENGTH));
521
522 // Loop through file
523 $column = $row = '';
524
525 // loop through one row (line) at a time in the file
526 while (($rowDataTxt = fgets($fileHandle)) !== false) {
527 // convert SYLK encoded $rowData to UTF-8
528 $rowDataTxt = StringHelper::SYLKtoUTF8($rowDataTxt);
529
530 // explode each row at semicolons while taking into account that literal semicolon (;)
531 // is escaped like this (;;)
532 $rowData = explode("\t", str_replace('¤', ';', str_replace(';', "\t", str_replace(';;', '¤', rtrim($rowDataTxt)))));
533
534 $dataType = array_shift($rowData);
535 if ($dataType == 'P') {
536 // Read shared styles
537 $this->processPRecord($rowData, $spreadsheet);
538 } elseif ($dataType == 'C') {
539 // Read cell value data
540 $this->processCRecord($rowData, $spreadsheet, $row, $column);
541 } elseif ($dataType == 'F') {
542 // Read cell formatting
543 $this->processFRecord($rowData, $spreadsheet, $row, $column);
544 } else {
545 $this->columnRowFromRowData($rowData, $column, $row);
546 }
547 }
548
549 // Close file
550 fclose($fileHandle);
551
552 // Return
553 return $spreadsheet;
554 }
555
556 private function columnRowFromRowData(array $rowData, string &$column, string &$row): void
557 {
558 foreach ($rowData as $rowDatum) {
559 $char0 = $rowDatum[0];
560 if ($char0 === 'X' || $char0 == 'C') {
561 $column = substr($rowDatum, 1);
562 } elseif ($char0 === 'Y' || $char0 == 'R') {
563 $row = substr($rowDatum, 1);
564 }
565 }
566 }
567
573 public function getSheetIndex()
574 {
575 return $this->sheetIndex;
576 }
577
585 public function setSheetIndex($pValue)
586 {
587 $this->sheetIndex = $pValue;
588
589 return $this;
590 }
591}
An exception for terminatinating execution or to throw for unit testing.
static unwrapResult($value)
Remove quotes used as a wrapper to identify string values.
Helper class to manipulate cell coordinates.
Definition: Coordinate.php:15
static stringFromColumnIndex($columnIndex)
String from column index.
Definition: Coordinate.php:313
openFile($pFilename)
Open file for reading.
Definition: BaseReader.php:145
setInputEncoding($pValue)
Set input encoding.
Definition: Slk.php:116
getInputEncoding()
Get input encoding.
Definition: Slk.php:132
canReadOrBust(string $pFilename)
Definition: Slk.php:97
getSheetIndex()
Get sheet index.
Definition: Slk.php:573
addFormats(Spreadsheet &$spreadsheet, string $formatStyle, string $row, string $column)
Definition: Slk.php:389
addWidth(Spreadsheet $spreadsheet, string $columnWidth, string $startCol, string $endCol)
Definition: Slk.php:417
styleSettings(string $rowDatum, array &$styleData, string &$fontStyle)
Definition: Slk.php:369
columnRowFromRowData(array $rowData, string &$column, string &$row)
Definition: Slk.php:556
canRead($pFilename)
Validate that the current file is a SYLK file.
Definition: Slk.php:73
processFRecord(array $rowData, Spreadsheet &$spreadsheet, string &$row, string &$column)
Definition: Slk.php:321
processPRecord(array $rowData, Spreadsheet &$spreadsheet)
Definition: Slk.php:434
processPColors(string $rowDatum, array &$formatArray)
Definition: Slk.php:468
loadIntoExisting($pFilename, Spreadsheet $spreadsheet)
Loads PhpSpreadsheet from file into PhpSpreadsheet instance.
Definition: Slk.php:508
processCRecord(array $rowData, Spreadsheet &$spreadsheet, string &$row, string &$column)
Definition: Slk.php:277
setSheetIndex($pValue)
Set sheet index.
Definition: Slk.php:585
addFonts(Spreadsheet &$spreadsheet, string $fontStyle, string $row, string $column)
Definition: Slk.php:399
listWorksheetInfo($pFilename)
Return worksheet info (Name, Last Column Letter, Last Column Index, Total Rows, Total Columns).
Definition: Slk.php:144
processFormula(string $rowDatum, bool &$hasCalculatedValue, string &$cellDataFormula, string $row, string $column)
Definition: Slk.php:230
__construct()
Create a new SYLK Reader instance.
Definition: Slk.php:61
processPFontStyles(string $rowDatum, array &$formatArray)
Definition: Slk.php:476
processPFinal(Spreadsheet &$spreadsheet, array $formatArray)
Definition: Slk.php:487
load($pFilename)
Loads PhpSpreadsheet from file.
Definition: Slk.php:204
processCFinal(Spreadsheet &$spreadsheet, bool $hasCalculatedValue, string $cellDataFormula, string $cellData, string $coordinate)
Definition: Slk.php:311
addStyle(Spreadsheet &$spreadsheet, array $styleData, string $row, string $column)
Definition: Slk.php:409
static SYLKtoUTF8($pValue)
Convert SYLK encoded string to UTF-8.
createSheet($sheetIndex=null)
Create sheet and add it to this workbook.
setActiveSheetIndex($pIndex)
Set active sheet index.
$key
Definition: croninfo.php:18
$i
Definition: disco.tpl.php:19
$row
$data
Definition: bench.php:6