Proyectos de Subversion Moodle

Rev

| Ultima modificación | Ver Log |

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