PluginProbe
TablePress – Tables in WordPress made easy / 3.4
TablePress – Tables in WordPress made easy v3.4
3.4 3.3.4 3.3.3 3.3.2 3.3.1 trunk 1.12 1.14 1.9.2 2.0.4 2.1.7 2.1.8 2.2 2.2.1 2.2.2 2.2.3 2.2.4 2.2.5 2.3 2.3.1 2.3.2 2.4 2.4.1 2.4.2 2.4.3 All 45 releases
tablepress / libraries / vendor / PhpSpreadsheet / Reader / Slk.php

Slk.php in TablePress – Tables in WordPress made easy 3.4, at libraries/vendor/PhpSpreadsheet/Reader/Slk.php

594 lines 18.0 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2
3 namespace TablePress\PhpOffice\PhpSpreadsheet\Reader;
4
5 use TablePress\PhpOffice\PhpSpreadsheet\Calculation\Calculation;
6 use TablePress\PhpOffice\PhpSpreadsheet\Cell\Coordinate;
7 use TablePress\PhpOffice\PhpSpreadsheet\Reader\Exception as ReaderException;
8 use TablePress\PhpOffice\PhpSpreadsheet\ReferenceHelper;
9 use TablePress\PhpOffice\PhpSpreadsheet\Shared\StringHelper;
10 use TablePress\PhpOffice\PhpSpreadsheet\Spreadsheet;
11 use TablePress\PhpOffice\PhpSpreadsheet\Style\Border;
12 use TablePress\PhpOffice\PhpSpreadsheet\Style\Fill;
13 use TablePress\PhpOffice\PhpSpreadsheet\Worksheet\Worksheet;
14
15 class Slk extends BaseReader
16 {
17 /**
18 * Sheet index to read.
19 */
20 private int $sheetIndex = 0;
21
22 /**
23 * Formats.
24 *
25 * @var mixed[]
26 */
27 private array $formats = [];
28
29 /**
30 * Format Count.
31 */
32 private int $format = 0;
33
34 /**
35 * Fonts.
36 *
37 * @var mixed[]
38 */
39 private array $fonts = [];
40
41 /**
42 * Font Count.
43 */
44 private int $fontcount = 0;
45
46 /**
47 * Create a new SYLK Reader instance.
48 */
49 public function __construct()
50 {
51 parent::__construct();
52 }
53
54 /**
55 * Validate that the current file is a SYLK file.
56 */
57 public function canRead(string $filename): bool
58 {
59 try {
60 $this->openFile($filename);
61 } catch (ReaderException $exception) {
62 return false;
63 }
64
65 // Read sample data (first 2 KB will do)
66 $data = (string) fread($this->fileHandle, 2048);
67
68 // Count delimiters in file
69 $delimiterCount = substr_count($data, ';');
70 $hasDelimiter = $delimiterCount > 0;
71
72 // Analyze first line looking for ID; signature
73 $lines = explode("\n", $data);
74 $hasId = str_starts_with($lines[0], 'ID;P');
75
76 fclose($this->fileHandle);
77
78 return $hasDelimiter && $hasId;
79 }
80
81 private function canReadOrBust(string $filename): void
82 {
83 if (!$this->canRead($filename)) {
84 throw new ReaderException($filename . ' is an Invalid SYLK file.');
85 }
86 $this->openFile($filename);
87 }
88
89 /**
90 * Return worksheet info (Name, Last Column Letter, Last Column Index, Total Rows, Total Columns).
91 *
92 * @return array<int, array{worksheetName: string, lastColumnLetter: string, lastColumnIndex: int, totalRows: int, totalColumns: int, sheetState: string}>
93 */
94 public function listWorksheetInfo(string $filename): array
95 {
96 // Open file
97 $this->canReadOrBust($filename);
98 $fileHandle = $this->fileHandle;
99 rewind($fileHandle);
100
101 $worksheetInfo = [['worksheetName' => basename($filename, '.slk')]];
102
103 // loop through one row (line) at a time in the file
104 $rowIndex = 0;
105 $columnIndex = 0;
106 while (($rowData = fgets($fileHandle)) !== false) {
107 $columnIndex = 0;
108
109 // convert SYLK encoded $rowData to UTF-8
110 $rowData = StringHelper::SYLKtoUTF8($rowData);
111
112 // explode each row at semicolons while taking into account that literal semicolon (;)
113 // is escaped like this (;;)
114 $rowData = explode("\t", str_replace('¤', ';', str_replace(';', "\t", str_replace(';;', '¤', rtrim($rowData)))));
115
116 $dataType = array_shift($rowData);
117 if ($dataType == 'B') {
118 foreach ($rowData as $rowDatum) {
119 switch ($rowDatum[0]) {
120 case 'X':
121 $columnIndex = (int) substr($rowDatum, 1) - 1;
122
123 break;
124 case 'Y':
125 $rowIndex = (int) substr($rowDatum, 1);
126
127 break;
128 }
129 }
130
131 break;
132 }
133 }
134
135 $worksheetInfo[0]['lastColumnIndex'] = $columnIndex;
136 $worksheetInfo[0]['totalRows'] = $rowIndex;
137 $worksheetInfo[0]['lastColumnLetter'] = Coordinate::stringFromColumnIndex($worksheetInfo[0]['lastColumnIndex'] + 1, true);
138 $worksheetInfo[0]['totalColumns'] = $worksheetInfo[0]['lastColumnIndex'] + 1;
139 $worksheetInfo[0]['sheetState'] = Worksheet::SHEETSTATE_VISIBLE;
140
141 // Close file
142 fclose($fileHandle);
143
144 return $worksheetInfo;
145 }
146
147 /**
148 * Loads PhpSpreadsheet from file.
149 */
150 protected function loadSpreadsheetFromFile(string $filename): Spreadsheet
151 {
152 $spreadsheet = $this->newSpreadsheet();
153 $spreadsheet->setValueBinder($this->valueBinder);
154
155 // Load into this instance
156 return $this->loadIntoExisting($filename, $spreadsheet);
157 }
158
159 private const COLOR_ARRAY = [
160 'FF00FFFF', // 0 - cyan
161 'FF000000', // 1 - black
162 'FFFFFFFF', // 2 - white
163 'FFFF0000', // 3 - red
164 'FF00FF00', // 4 - green
165 'FF0000FF', // 5 - blue
166 'FFFFFF00', // 6 - yellow
167 'FFFF00FF', // 7 - magenta
168 ];
169
170 private const FONT_STYLE_MAPPINGS = [
171 'B' => 'bold',
172 'I' => 'italic',
173 'U' => 'underline',
174 ];
175
176 /**
177 * @param-out true $hasCalculatedValue
178 */
179 private function processFormula(string $rowDatum, bool &$hasCalculatedValue, string &$cellDataFormula, string $row, string $column): void
180 {
181 $cellDataFormula = '=' . substr($rowDatum, 1);
182 // Convert R1C1 style references to A1 style references (but only when not quoted)
183 $temp = explode('"', $cellDataFormula);
184 $key = false;
185 foreach ($temp as &$value) {
186 // Only count/replace in alternate array entries
187 $key = $key === false;
188 if ($key) {
189 preg_match_all('/(R(\[?-?\d*\]?))(C(\[?-?\d*\]?))/', $value, $cellReferences, PREG_SET_ORDER + PREG_OFFSET_CAPTURE);
190 // Reverse the matches array, otherwise all our offsets will become incorrect if we modify our way
191 // through the formula from left to right. Reversing means that we work right to left.through
192 // the formula
193 $cellReferences = array_reverse($cellReferences);
194 // Loop through each R1C1 style reference in turn, converting it to its A1 style equivalent,
195 // then modify the formula to use that new reference
196 foreach ($cellReferences as $cellReference) {
197 $rowReference = $cellReference[2][0];
198 // Empty R reference is the current row
199 if ($rowReference == '') {
200 $rowReference = $row;
201 }
202 // Bracketed R references are relative to the current row
203 if ($rowReference[0] == '[') {
204 $rowReference = (int) $row + (int) trim($rowReference, '[]');
205 }
206 $columnReference = $cellReference[4][0];
207 // Empty C reference is the current column
208 if ($columnReference == '') {
209 $columnReference = $column;
210 }
211 // Bracketed C references are relative to the current column
212 if ($columnReference[0] == '[') {
213 $columnReference = (int) $column + (int) trim($columnReference, '[]');
214 }
215 $A1CellReference = Coordinate::stringFromColumnIndex((int) $columnReference) . $rowReference;
216
217 $value = substr_replace($value, $A1CellReference, $cellReference[0][1], strlen($cellReference[0][0]));
218 }
219 }
220 }
221 unset($value);
222 // Then rebuild the formula string
223 $cellDataFormula = implode('"', $temp);
224 $hasCalculatedValue = true;
225 }
226
227 /** @param mixed[] $rowData */
228 private function processCRecord(array $rowData, Spreadsheet &$spreadsheet, string &$row, string &$column): void
229 {
230 // Read cell value data
231 $hasCalculatedValue = false;
232 $tryNumeric = false;
233 $cellDataFormula = $cellData = '';
234 $sharedColumn = $sharedRow = -1;
235 $sharedFormula = false;
236 foreach ($rowData as $rowDatum) {
237 /** @var string $rowDatum */
238 switch ($rowDatum[0]) {
239 case 'X':
240 $column = (string) substr($rowDatum, 1);
241
242 break;
243 case 'Y':
244 $row = (string) substr($rowDatum, 1);
245
246 break;
247 case 'K':
248 $cellData = (string) substr($rowDatum, 1);
249 $tryNumeric = is_numeric($cellData);
250
251 break;
252 case 'E':
253 $this->processFormula($rowDatum, $hasCalculatedValue, $cellDataFormula, $row, $column);
254
255 break;
256 case 'A':
257 $comment = (string) substr($rowDatum, 1);
258 $columnLetter = Coordinate::stringFromColumnIndex((int) $column);
259 $spreadsheet->getActiveSheet()
260 ->getComment("$columnLetter$row")
261 ->getText()
262 ->createText($comment);
263
264 break;
265 case 'C':
266 $sharedColumn = (int) substr($rowDatum, 1);
267
268 break;
269 case 'R':
270 $sharedRow = (int) substr($rowDatum, 1);
271
272 break;
273 case 'S':
274 $sharedFormula = true;
275
276 break;
277 }
278 }
279 if ($sharedFormula === true && $sharedRow >= 0 && $sharedColumn >= 0) {
280 $thisCoordinate = Coordinate::stringFromColumnIndex((int) $column) . $row;
281 $sharedCoordinate = Coordinate::stringFromColumnIndex($sharedColumn) . $sharedRow;
282 /** @var string */
283 $formula = $spreadsheet->getActiveSheet()->getCell($sharedCoordinate)->getValue();
284 $spreadsheet->getActiveSheet()->getCell($thisCoordinate)->setValue($formula);
285 $referenceHelper = ReferenceHelper::getInstance();
286 $newFormula = $referenceHelper->updateFormulaReferences($formula, 'A1', (int) $column - $sharedColumn, (int) $row - $sharedRow, '', true, false);
287 $spreadsheet->getActiveSheet()->getCell($thisCoordinate)->setValue($newFormula);
288 //$calc = $spreadsheet->getActiveSheet()->getCell($thisCoordinate)->getCalculatedValue();
289 //$spreadsheet->getActiveSheet()->getCell($thisCoordinate)->setCalculatedValue($calc);
290 $cellData = Calculation::unwrapResult($cellData);
291 $spreadsheet->getActiveSheet()->getCell($thisCoordinate)->setCalculatedValue($cellData, $tryNumeric);
292
293 return;
294 }
295 $columnLetter = Coordinate::stringFromColumnIndex((int) $column);
296 /** @var string */
297 $cellData = Calculation::unwrapResult($cellData);
298
299 // Set cell value
300 $this->processCFinal($spreadsheet, $hasCalculatedValue, $cellDataFormula, $cellData, "$columnLetter$row", $tryNumeric);
301 }
302
303 private function processCFinal(Spreadsheet &$spreadsheet, bool $hasCalculatedValue, string $cellDataFormula, string $cellData, string $coordinate, bool $tryNumeric): void
304 {
305 // Set cell value
306 $spreadsheet->getActiveSheet()->getCell($coordinate)->setValue(($hasCalculatedValue) ? $cellDataFormula : $cellData);
307 if ($hasCalculatedValue) {
308 $cellData = Calculation::unwrapResult($cellData);
309 $spreadsheet->getActiveSheet()->getCell($coordinate)->setCalculatedValue($cellData, $tryNumeric);
310 }
311 }
312
313 /** @param mixed[] $rowData */
314 private function processFRecord(array $rowData, Spreadsheet &$spreadsheet, string &$row, string &$column): void
315 {
316 // Read cell formatting
317 $formatStyle = $columnWidth = '';
318 $startCol = $endCol = '';
319 $fontStyle = '';
320 $styleData = [];
321 foreach ($rowData as $rowDatum) {
322 /** @var string $rowDatum */
323 switch ($rowDatum[0]) {
324 case 'C':
325 case 'X':
326 $column = (string) substr($rowDatum, 1);
327
328 break;
329 case 'R':
330 case 'Y':
331 $row = (string) substr($rowDatum, 1);
332
333 break;
334 case 'P':
335 $formatStyle = $rowDatum;
336
337 break;
338 case 'W':
339 [$startCol, $endCol, $columnWidth] = explode(' ', (string) substr($rowDatum, 1));
340
341 break;
342 case 'S':
343 $this->styleSettings($rowDatum, $styleData, $fontStyle);
344
345 break;
346 }
347 }
348 /** @var string $formatStyle */
349 $this->addFormats($spreadsheet, $formatStyle, $row, $column);
350 $this->addFonts($spreadsheet, $fontStyle, $row, $column);
351 $this->addStyle($spreadsheet, $styleData, $row, $column);
352 $this->addWidth($spreadsheet, $columnWidth, $startCol, $endCol);
353 }
354
355 private const STYLE_SETTINGS_FONT = ['D' => 'bold', 'I' => 'italic'];
356
357 private const STYLE_SETTINGS_BORDER = [
358 'B' => 'bottom',
359 'L' => 'left',
360 'R' => 'right',
361 'T' => 'top',
362 ];
363
364 /** @param mixed[][] $styleData */
365 private function styleSettings(string $rowDatum, array &$styleData, string &$fontStyle): void
366 {
367 $styleSettings = (string) substr($rowDatum, 1);
368 $iMax = strlen($styleSettings);
369 for ($i = 0; $i < $iMax; ++$i) {
370 $char = $styleSettings[$i];
371 if (array_key_exists($char, self::STYLE_SETTINGS_FONT)) {
372 $styleData['font'][self::STYLE_SETTINGS_FONT[$char]] = true;
373 } elseif (array_key_exists($char, self::STYLE_SETTINGS_BORDER)) {
374 $styleData['borders'][self::STYLE_SETTINGS_BORDER[$char]]['borderStyle'] = Border::BORDER_THIN; //* @phpstan-ignore offsetAccess.nonOffsetAccessible (I don't know how to fix this)
375 } elseif ($char == 'S') {
376 $styleData['fill']['fillType'] = Fill::FILL_PATTERN_GRAY125;
377 } elseif ($char == 'M') {
378 if (preg_match('/M([1-9]\d*)/', $styleSettings, $matches)) {
379 $fontStyle = $matches[1];
380 }
381 }
382 }
383 }
384
385 private function addFormats(Spreadsheet &$spreadsheet, string $formatStyle, string $row, string $column): void
386 {
387 if ($formatStyle && $column > '' && $row > '') {
388 $columnLetter = Coordinate::stringFromColumnIndex((int) $column);
389 if (isset($this->formats[$formatStyle]) && is_array($this->formats[$formatStyle])) {
390 $spreadsheet->getActiveSheet()->getStyle($columnLetter . $row)->applyFromArray($this->formats[$formatStyle]);
391 }
392 }
393 }
394
395 private function addFonts(Spreadsheet &$spreadsheet, string $fontStyle, string $row, string $column): void
396 {
397 if ($fontStyle && $column > '' && $row > '') {
398 $columnLetter = Coordinate::stringFromColumnIndex((int) $column);
399 if (isset($this->fonts[$fontStyle]) && is_array($this->fonts[$fontStyle])) {
400 $spreadsheet->getActiveSheet()->getStyle($columnLetter . $row)->applyFromArray($this->fonts[$fontStyle]);
401 }
402 }
403 }
404
405 /** @param mixed[] $styleData */
406 private function addStyle(Spreadsheet &$spreadsheet, array $styleData, string $row, string $column): void
407 {
408 if ((!empty($styleData)) && $column > '' && $row > '') {
409 $columnLetter = Coordinate::stringFromColumnIndex((int) $column);
410 $spreadsheet->getActiveSheet()->getStyle($columnLetter . $row)->applyFromArray($styleData);
411 }
412 }
413
414 private function addWidth(Spreadsheet $spreadsheet, string $columnWidth, string $startCol, string $endCol): void
415 {
416 if ($columnWidth > '') {
417 if ($startCol == $endCol) {
418 $startCol = Coordinate::stringFromColumnIndex((int) $startCol);
419 $spreadsheet->getActiveSheet()->getColumnDimension($startCol)->setWidth((float) $columnWidth);
420 } else {
421 $startCol = Coordinate::stringFromColumnIndex((int) $startCol);
422 $endCol = Coordinate::stringFromColumnIndex((int) $endCol);
423 $spreadsheet->getActiveSheet()->getColumnDimension($startCol)->setWidth((float) $columnWidth);
424 do {
425 /** @var string $startCol */
426 $spreadsheet->getActiveSheet()
427 ->getColumnDimension(
428 StringHelper::stringIncrement($startCol)
429 )
430 ->setWidth((float) $columnWidth);
431 } while ($startCol !== $endCol);
432 }
433 }
434 }
435
436 /** @param string[] $rowData */
437 private function processPRecord(array $rowData, Spreadsheet &$spreadsheet): void
438 {
439 // Read shared styles
440 $formatArray = [];
441 $fromFormats = ['\-', '\ '];
442 $toFormats = ['-', ' '];
443 foreach ($rowData as $rowDatum) {
444 switch ($rowDatum[0]) {
445 case 'P':
446 $formatArray['numberFormat']['formatCode'] = str_replace($fromFormats, $toFormats, (string) substr($rowDatum, 1));
447
448 break;
449 case 'E':
450 case 'F':
451 $formatArray['font']['name'] = (string) substr($rowDatum, 1);
452
453 break;
454 case 'M':
455 $formatArray['font']['size'] = ((float) substr($rowDatum, 1)) / 20;
456
457 break;
458 case 'L':
459 /** @var mixed[][][] $formatArray */
460 $this->processPColors($rowDatum, $formatArray);
461
462 break;
463 case 'S':
464 $this->processPFontStyles($rowDatum, $formatArray);
465
466 break;
467 }
468 }
469 $this->processPFinal($spreadsheet, $formatArray);
470 }
471
472 /** @param mixed[][][] $formatArray */
473 private function processPColors(string $rowDatum, array &$formatArray): void
474 {
475 if (preg_match('/L([1-9]\d*)/', $rowDatum, $matches)) {
476 $fontColor = ((int) $matches[1]) % 8;
477 $formatArray['font']['color']['argb'] = self::COLOR_ARRAY[$fontColor];
478 }
479 }
480
481 /** @param mixed[][] $formatArray */
482 private function processPFontStyles(string $rowDatum, array &$formatArray): void
483 {
484 $styleSettings = (string) substr($rowDatum, 1);
485 $iMax = strlen($styleSettings);
486 for ($i = 0; $i < $iMax; ++$i) {
487 if (array_key_exists($styleSettings[$i], self::FONT_STYLE_MAPPINGS)) {
488 $formatArray['font'][self::FONT_STYLE_MAPPINGS[$styleSettings[$i]]] = true;
489 }
490 }
491 }
492
493 /** @param mixed[] $formatArray */
494 private function processPFinal(Spreadsheet &$spreadsheet, array $formatArray): void
495 {
496 if (array_key_exists('numberFormat', $formatArray)) {
497 $this->formats['P' . $this->format] = $formatArray;
498 ++$this->format;
499 } elseif (array_key_exists('font', $formatArray)) {
500 ++$this->fontcount;
501 $this->fonts[$this->fontcount] = $formatArray;
502 if ($this->fontcount === 1) {
503 $spreadsheet->getDefaultStyle()->applyFromArray($formatArray);
504 }
505 }
506 }
507
508 /**
509 * Loads PhpSpreadsheet from file into PhpSpreadsheet instance.
510 */
511 public function loadIntoExisting(string $filename, Spreadsheet $spreadsheet): Spreadsheet
512 {
513 // Open file
514 $this->canReadOrBust($filename);
515 $fileHandle = $this->fileHandle;
516 rewind($fileHandle);
517
518 // Create new Worksheets
519 while ($spreadsheet->getSheetCount() <= $this->sheetIndex) {
520 $spreadsheet->createSheet();
521 }
522 $spreadsheet->setActiveSheetIndex($this->sheetIndex);
523 $spreadsheet->getActiveSheet()->setTitle(substr(basename($filename, '.slk'), 0, Worksheet::SHEET_TITLE_MAXIMUM_LENGTH));
524
525 // Loop through file
526 $column = $row = '';
527
528 // loop through one row (line) at a time in the file
529 while (($rowDataTxt = fgets($fileHandle)) !== false) {
530 // convert SYLK encoded $rowData to UTF-8
531 $rowDataTxt = StringHelper::SYLKtoUTF8($rowDataTxt);
532
533 // explode each row at semicolons while taking into account that literal semicolon (;)
534 // is escaped like this (;;)
535 $rowData = explode("\t", str_replace('¤', ';', str_replace(';', "\t", str_replace(';;', '¤', rtrim($rowDataTxt)))));
536
537 $dataType = array_shift($rowData);
538 if ($dataType == 'P') {
539 // Read shared styles
540 $this->processPRecord($rowData, $spreadsheet);
541 } elseif ($dataType == 'C') {
542 // Read cell value data
543 $this->processCRecord($rowData, $spreadsheet, $row, $column);
544 } elseif ($dataType == 'F') {
545 // Read cell formatting
546 $this->processFRecord($rowData, $spreadsheet, $row, $column);
547 } else {
548 $this->columnRowFromRowData($rowData, $column, $row);
549 }
550 }
551
552 // Close file
553 fclose($fileHandle);
554
555 // Return
556 return $spreadsheet;
557 }
558
559 /** @param string[] $rowData */
560 private function columnRowFromRowData(array $rowData, string &$column, string &$row): void
561 {
562 foreach ($rowData as $rowDatum) {
563 $char0 = $rowDatum[0];
564 if ($char0 === 'X' || $char0 == 'C') {
565 $column = (string) substr($rowDatum, 1);
566 } elseif ($char0 === 'Y' || $char0 == 'R') {
567 $row = (string) substr($rowDatum, 1);
568 }
569 }
570 }
571
572 /**
573 * Get sheet index.
574 */
575 public function getSheetIndex(): int
576 {
577 return $this->sheetIndex;
578 }
579
580 /**
581 * Set sheet index.
582 *
583 * @param int $sheetIndex Sheet index
584 *
585 * @return $this
586 */
587 public function setSheetIndex(int $sheetIndex)
588 {
589 $this->sheetIndex = $sheetIndex;
590
591 return $this;
592 }
593 }
594