├── demo ├── files │ ├── img1.jpg │ ├── img2.jpg │ ├── img3.jpg │ ├── demo-00-test.xlsx │ ├── demo-01-base.xlsx │ ├── demo-03-images.xlsx │ ├── demo-04-styles.xlsx │ ├── demo-100k-rows.xlsx │ ├── demo-02-advanced.xlsx │ ├── demo-05-datetime.xlsx │ ├── demo-03-images-excel-365.xlsx │ ├── demo-06-data-validation.xlsx │ ├── demo-07-size-freeze-tabs.xlsx │ └── worksheet-referenced-with-absolute-path.xlsx ├── demo-01-base.php ├── index.php ├── demo-03-read-100k-rows.php └── demo-02-advanced.php ├── tests ├── Files │ ├── colors.xlsx │ ├── formulas.xlsx │ ├── standard-file.xlsx │ ├── spec#name%sym _.xlsx │ ├── wrong-dimension.xlsx │ └── nonstandard-file.xlsx ├── bootstrap.php ├── ColorsTest.php ├── NonstandardFileHandlingTest.php ├── DimensionTest.php ├── ReadRewindTest.php └── FastExcelReaderTest.php ├── src ├── FastExcelReader │ ├── Interfaces │ │ ├── InterfaceXmlReader.php │ │ ├── InterfaceBookReader.php │ │ └── InterfaceSheetReader.php │ ├── Exception.php │ ├── Reader.php │ ├── Excel.php │ └── Sheet.php └── autoload.php ├── phpunit.xml ├── composer.json ├── LICENSE └── README.md /demo/files/img1.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aVadim483/fast-excel-reader/HEAD/demo/files/img1.jpg -------------------------------------------------------------------------------- /demo/files/img2.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aVadim483/fast-excel-reader/HEAD/demo/files/img2.jpg -------------------------------------------------------------------------------- /demo/files/img3.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aVadim483/fast-excel-reader/HEAD/demo/files/img3.jpg -------------------------------------------------------------------------------- /tests/Files/colors.xlsx: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aVadim483/fast-excel-reader/HEAD/tests/Files/colors.xlsx -------------------------------------------------------------------------------- /tests/Files/formulas.xlsx: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aVadim483/fast-excel-reader/HEAD/tests/Files/formulas.xlsx -------------------------------------------------------------------------------- /demo/files/demo-00-test.xlsx: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aVadim483/fast-excel-reader/HEAD/demo/files/demo-00-test.xlsx -------------------------------------------------------------------------------- /demo/files/demo-01-base.xlsx: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aVadim483/fast-excel-reader/HEAD/demo/files/demo-01-base.xlsx -------------------------------------------------------------------------------- /demo/files/demo-03-images.xlsx: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aVadim483/fast-excel-reader/HEAD/demo/files/demo-03-images.xlsx -------------------------------------------------------------------------------- /demo/files/demo-04-styles.xlsx: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aVadim483/fast-excel-reader/HEAD/demo/files/demo-04-styles.xlsx -------------------------------------------------------------------------------- /demo/files/demo-100k-rows.xlsx: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aVadim483/fast-excel-reader/HEAD/demo/files/demo-100k-rows.xlsx -------------------------------------------------------------------------------- /tests/Files/standard-file.xlsx: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aVadim483/fast-excel-reader/HEAD/tests/Files/standard-file.xlsx -------------------------------------------------------------------------------- /demo/files/demo-02-advanced.xlsx: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aVadim483/fast-excel-reader/HEAD/demo/files/demo-02-advanced.xlsx -------------------------------------------------------------------------------- /demo/files/demo-05-datetime.xlsx: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aVadim483/fast-excel-reader/HEAD/demo/files/demo-05-datetime.xlsx -------------------------------------------------------------------------------- /tests/Files/spec#name%sym _.xlsx: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aVadim483/fast-excel-reader/HEAD/tests/Files/spec#name%sym _.xlsx -------------------------------------------------------------------------------- /tests/Files/wrong-dimension.xlsx: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aVadim483/fast-excel-reader/HEAD/tests/Files/wrong-dimension.xlsx -------------------------------------------------------------------------------- /tests/Files/nonstandard-file.xlsx: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aVadim483/fast-excel-reader/HEAD/tests/Files/nonstandard-file.xlsx -------------------------------------------------------------------------------- /demo/files/demo-03-images-excel-365.xlsx: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aVadim483/fast-excel-reader/HEAD/demo/files/demo-03-images-excel-365.xlsx -------------------------------------------------------------------------------- /demo/files/demo-06-data-validation.xlsx: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aVadim483/fast-excel-reader/HEAD/demo/files/demo-06-data-validation.xlsx -------------------------------------------------------------------------------- /demo/files/demo-07-size-freeze-tabs.xlsx: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aVadim483/fast-excel-reader/HEAD/demo/files/demo-07-size-freeze-tabs.xlsx -------------------------------------------------------------------------------- /src/FastExcelReader/Interfaces/InterfaceXmlReader.php: -------------------------------------------------------------------------------- 1 | readRows(true); 11 | 12 | echo '
', print_r($result);
13 | 
14 | // EOF


--------------------------------------------------------------------------------
/tests/bootstrap.php:
--------------------------------------------------------------------------------
 1 | 
 2 | 
 3 | 
 4 |     
 5 |     FastExcelWriter Demo
 6 | 
 7 | 
 8 | $name
" ; 14 | } 15 | ?> 16 | 17 | -------------------------------------------------------------------------------- /phpunit.xml: -------------------------------------------------------------------------------- 1 | 4 | 5 | 6 | tests 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /demo/demo-03-read-100k-rows.php: -------------------------------------------------------------------------------- 1 | sheet()->nextRow() as $rowNum => $rowData) { 12 | $cnt++; 13 | } 14 | 15 | echo 'Read: ', $cnt, ' rows
'; 16 | echo 'Elapsed time: ', round(microtime(true) - $timer, 3), ' sec
'; 17 | 18 | // EOF -------------------------------------------------------------------------------- /demo/demo-02-advanced.php: -------------------------------------------------------------------------------- 1 | getSheetNames(); 13 | 14 | $result['#1'] = $excel 15 | ->selectSheet('Demo1') 16 | ->setReadArea('B4:D11', true) 17 | ->setDateFormat('Y-m-d') 18 | ->readRows(['C' => 'Birthday']); 19 | 20 | $columnKeys = ['B' => 'year', 'C' => 'value1', 'D' => 'value2']; 21 | $data2 = $excel 22 | ->selectSheet('Demo2', 'B5:D13') 23 | ->readRows($columnKeys); 24 | 25 | $data3 = $excel 26 | ->setReadArea('F5:H13') 27 | ->readRows($columnKeys); 28 | 29 | $result['#2'] = array_merge($data2, $data3); 30 | 31 | echo '
', print_r($result);
32 | 
33 | // EOF


--------------------------------------------------------------------------------
/composer.json:
--------------------------------------------------------------------------------
 1 | {
 2 |     "name": "avadim/fast-excel-reader",
 3 |     "description": "Lightweight and very fast XLSX Excel Spreadsheet Reader in PHP",
 4 |     "keywords": [
 5 |         "php",
 6 |         "library",
 7 |         "xls",
 8 |         "xlsx",
 9 |         "excel",
10 |         "phpexcel",
11 |         "parser",
12 |         "reader",
13 |         "import",
14 | 		"spreadsheet",
15 | 		"ms office",
16 | 		"office 2007"
17 |     ],
18 |     "type": "library",
19 |     "homepage": "https://github.com/aVadim483/fast-excel-reader",
20 |     "license": "MIT",
21 |     "autoload": {
22 |         "psr-4": {
23 |             "avadim\\FastExcelReader\\": "./src/FastExcelReader"
24 |         }
25 |     },
26 |     "require": {
27 |         "php": ">=7.4",
28 |         "ext-zip": "*",
29 |         "ext-mbstring": "*",
30 |         "ext-ctype": "*",
31 |         "ext-xmlreader": "*",
32 |         "avadim/fast-excel-helper": "^1.2.3"
33 |     },
34 |     "require-dev": {
35 |         "phpunit/phpunit": "^9.0"
36 |     },
37 |     "scripts": {
38 |         "test": "vendor/bin/phpunit tests"
39 |     }
40 | }
41 | 


--------------------------------------------------------------------------------
/tests/ColorsTest.php:
--------------------------------------------------------------------------------
 1 | sheet();
14 | 
15 |         $data = $sheet->readCells(true);
16 | 
17 |         $colors = [];
18 |         foreach ($data as $cell) {
19 |             $colors[] = $excel->getCompleteStyleByIdx($cell['s'])['fill']['fill-color'];
20 |         }
21 | 
22 |         $checkColors = [
23 |             '#F2F2F2',
24 |             '#D9D9D9',
25 |             '#BFBFBF',
26 |             '#A6A6A6',
27 |             '#808080',
28 |             '#F3F2F2',
29 |             '#D9D8D9',
30 |             '#BFBFC0',
31 |             '#A7A5A6',
32 |             '#817F81',
33 |         ];
34 | 
35 |         foreach ($colors as $key => $color) {
36 |             $this->assertEquals($checkColors[$key], $color);
37 |         }
38 |     }
39 | }


--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
 1 | MIT License
 2 | 
 3 | Copyright (c) 2020 Vadim Shemarov
 4 | 
 5 | Permission is hereby granted, free of charge, to any person obtaining a copy
 6 | of this software and associated documentation files (the "Software"), to deal
 7 | in the Software without restriction, including without limitation the rights
 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
 9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 | 
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 | 
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 | 


--------------------------------------------------------------------------------
/tests/NonstandardFileHandlingTest.php:
--------------------------------------------------------------------------------
 1 | sheet();
14 |         self::assertNotNull($standardSheet);
15 | 
16 |         $nonStandardFilePath = __DIR__ . '/Files/nonstandard-file.xlsx';
17 |         self::assertFileExists($nonStandardFilePath);
18 | 
19 |         $nonStandardExcel = Excel::open($nonStandardFilePath);
20 |         $nonStandardSheet = $nonStandardExcel->sheet();
21 |         self::assertNotNull($nonStandardSheet);
22 | 
23 |         $specSymbolFilePath = __DIR__ . '/Files/spec#name%sym _.xlsx';
24 |         self::assertFileExists($specSymbolFilePath);
25 |         $excel = Excel::open($specSymbolFilePath);
26 |         $sheet = $excel->sheet();
27 | 
28 |         $cells = $sheet->readCells();
29 |         self::assertEquals('qwerty string', $cells['B2']);
30 |     }
31 | }
32 | 


--------------------------------------------------------------------------------
/tests/DimensionTest.php:
--------------------------------------------------------------------------------
 1 | sheet();
15 | 
16 |         $this->assertEquals('D4', $sheet->dimension());
17 |         $this->assertEquals('C3:E5', $sheet->actualDimension());
18 | 
19 |         $this->assertEquals(4, $sheet->minRow());
20 |         $this->assertEquals(3, $sheet->minActualRow());
21 | 
22 |         $this->assertEquals(4, $sheet->maxRow());
23 |         $this->assertEquals(5, $sheet->maxActualRow());
24 | 
25 |         $this->assertEquals(1, $sheet->countRows());
26 |         $this->assertEquals(3, $sheet->countActualRows());
27 | 
28 |         $this->assertEquals('D', $sheet->minColumn());
29 |         $this->assertEquals('C', $sheet->minActualColumn());
30 | 
31 |         $this->assertEquals('D', $sheet->maxColumn());
32 |         $this->assertEquals('E', $sheet->maxActualColumn());
33 | 
34 |         $this->assertEquals(1, $sheet->countColumns());
35 |         $this->assertEquals(3, $sheet->countActualColumns());
36 | 
37 |     }
38 | }


--------------------------------------------------------------------------------
/tests/ReadRewindTest.php:
--------------------------------------------------------------------------------
 1 | sheet();
14 | 
15 |         $data1 = [];
16 |         foreach ($sheet->nextRow(false, Excel::KEYS_ORIGINAL) as $rowIndex => $row) {
17 |             $data1[$rowIndex] = $row;
18 |             if ($rowIndex >= 3) {
19 |                 break;
20 |             }
21 |         }
22 | 
23 |         $data2 = [];
24 |         foreach ($sheet->nextRow(false, Excel::KEYS_ORIGINAL) as $rowIndex => $row) {
25 |             $data2[$rowIndex] = $row;
26 |             if ($rowIndex >= 3) {
27 |                 break;
28 |             }
29 |         }
30 |         self::assertEquals($data1, $data2);
31 | 
32 |         $data3 = [];
33 |         for($i = 1; $i <= 3; $i++) {
34 |             $data3[$i] = $sheet->readNextRow();
35 |         }
36 |         self::assertEquals($data2, $data3);
37 |         self::assertEquals('Invoice Date', $data3[1]['A']);
38 |         $data3 = [];
39 |         for($i = 1; $i <= 3; $i++) {
40 |             $data3[$i] = $sheet->readNextRow();
41 |         }
42 |         self::assertNotEquals($data2, $data3);
43 |         self::assertNotEquals('Invoice Date', $data3[1]['A']);
44 | 
45 |         $sheet->reset();
46 |         $data3 = [];
47 |         for($i = 1; $i <= 3; $i++) {
48 |             $data3[$i] = $sheet->readNextRow();
49 |         }
50 |         self::assertEquals($data2, $data3);
51 |         self::assertEquals('Invoice Date', $data3[1]['A']);
52 |     }
53 | }


--------------------------------------------------------------------------------
/src/FastExcelReader/Reader.php:
--------------------------------------------------------------------------------
  1 | xlsxFile = $file;
 38 |         $this->zip = new \ZipArchive();
 39 |         if ($parserProperties) {
 40 |             $this->xmlParserProperties = $parserProperties;
 41 |         }
 42 |     }
 43 | 
 44 |     public function __destruct()
 45 |     {
 46 |         $this->close();
 47 |     }
 48 | 
 49 |     /**
 50 |      * @param string|null $tempDir
 51 |      */
 52 |     public static function setTempDir(?string $tempDir = '')
 53 |     {
 54 |         if ($tempDir) {
 55 |             self::$tempDir = $tempDir;
 56 |             if (!is_dir($tempDir)) {
 57 |                 $res = @mkdir($tempDir, 0755, true);
 58 |                 if (!$res) {
 59 |                     throw new Exception('Cannot create directory "' . $tempDir . '"');
 60 |                 }
 61 |             }
 62 |             self::$tempDir = realpath($tempDir);
 63 |         }
 64 |         else {
 65 |             self::$tempDir = '';
 66 |         }
 67 |     }
 68 | 
 69 |     /**
 70 |      * @return bool|string
 71 |      */
 72 |     protected function makeTempFile()
 73 |     {
 74 |         $name = uniqid('xlsx_reader_', true);
 75 |         if (!self::$tempDir) {
 76 |             $tempDir = sys_get_temp_dir();
 77 |             if (!is_writable($tempDir)) {
 78 |                 $tempDir = getcwd();
 79 |             }
 80 |         }
 81 |         else {
 82 |             $tempDir = self::$tempDir;
 83 |         }
 84 |         $filename = $tempDir . '/' . $name . '.tmp';
 85 |         if (touch($filename, time(), time()) && is_writable($filename)) {
 86 |             $filename = realpath($filename);
 87 |             $this->tmpFiles[] = $filename;
 88 |             return $filename;
 89 |         }
 90 |         else {
 91 |             $error = 'Warning: tempdir ' . $tempDir . ' is not writeable';
 92 |             if (!self::$tempDir) {
 93 |                 $error .= ', use ->setTempDir()';
 94 |             }
 95 |             throw new Exception($error);
 96 |         }
 97 |     }
 98 | 
 99 |     /**
100 |      * @return array
101 |      */
102 |     public function entryList(): array
103 |     {
104 |         $result = [];
105 | 
106 |         $zip = new \ZipArchive();
107 |         if (defined('\ZipArchive::RDONLY')) {
108 |             $res = $zip->open($this->xlsxFile, \ZipArchive::RDONLY);
109 |         }
110 |         else {
111 |             $res = $zip->open($this->xlsxFile);
112 |         }
113 |         if ($res === true) {
114 |             for ($i = 0; $i < $zip->numFiles; $i++) {
115 |                 $result[] = $zip->getNameIndex($i);
116 |             }
117 |             $zip->close();
118 |         }
119 |         else {
120 |             switch ($res) {
121 |                 case \ZipArchive::ER_NOENT:
122 |                     $error = 'No such file';
123 |                     $code = $res;
124 |                     break;
125 |                 case \ZipArchive::ER_OPEN:
126 |                     $error = 'Can\'t open file';
127 |                     $code = $res;
128 |                     break;
129 |                 case \ZipArchive::ER_READ:
130 |                     $error = '';
131 |                     $code = $res;
132 |                     break;
133 |                 case \ZipArchive::ER_NOZIP:
134 |                     $error = 'Not a zip archive';
135 |                     $code = $res;
136 |                     break;
137 |                 case \ZipArchive::ER_INCONS:
138 |                     $error = 'Zip archive inconsistent';
139 |                     $code = $res;
140 |                     break;
141 |                 case \ZipArchive::ER_MEMORY:
142 |                     $error = 'Malloc failure';
143 |                     $code = $res;
144 |                     break;
145 |                 default:
146 |                     $error = 'Unknown error';
147 |                     $code = -1;
148 |             }
149 |             $error = 'Error reading file "' . $this->xlsxFile . '" - ' . $error;
150 |             throw new Exception($error, $code);
151 |         }
152 | 
153 |         return $result;
154 |     }
155 | 
156 |     /**
157 |      * @return array
158 |      */
159 |     public function fileList(): array
160 |     {
161 |         $result = [];
162 |         foreach ($this->entryList() as $entry) {
163 |             if (substr($entry, -1) !== '/') {
164 |                 $result[] = $entry;
165 |             }
166 |         }
167 | 
168 |         return $result;
169 |     }
170 | 
171 |     /**
172 |      * @param string $innerFile
173 |      * @param string|null $encoding
174 |      * @param int|null $options
175 |      *
176 |      * @return bool
177 |      */
178 |     public function openZip(string $innerFile, ?string $encoding = null, ?int $options = null): bool
179 |     {
180 |         if ($options === null) {
181 |             $options = 0;
182 |             if (defined('LIBXML_NONET')) {
183 |                 $options = $options | LIBXML_NONET;
184 |             }
185 |             if (defined('LIBXML_COMPACT')) {
186 |                 $options = $options | LIBXML_COMPACT;
187 |             }
188 |         }
189 |         $result = (!$this->alterMode) && $this->openXmlWrapper($innerFile, $encoding, $options);
190 |         if (!$result) {
191 |             $result = $this->openXmlStream($innerFile, $encoding, $options);
192 |             $this->alterMode = $result;
193 |         }
194 | 
195 |         return $result;
196 |     }
197 | 
198 |     /**
199 |      * @param string $innerFile
200 |      * @param string|null $encoding
201 |      * @param int|null $options
202 |      *
203 |      * @return bool
204 |      */
205 |     protected function openXmlWrapper(string $innerFile, ?string $encoding = null, ?int $options = 0): bool
206 |     {
207 |         $this->innerFile = $innerFile;
208 |         $result = @$this->open('zip://' . $this->xlsxFile . '#' . $innerFile, $encoding, $options);
209 |         if ($result) {
210 |             foreach ($this->xmlParserProperties as $property => $value) {
211 |                 $this->setParserProperty($property, $value);
212 |             }
213 |         }
214 | 
215 |         return (bool)$result;
216 |     }
217 | 
218 |     /**
219 |      * Opens the INTERNAL XML file from XLSX as XMLReader
220 |      * Example: openXml('xl/workbook.xml')
221 |      *
222 |      * @param string $innerPath
223 |      * @param string|null $encoding
224 |      * @param int|null $options
225 |      *
226 |      * @return bool
227 |      */
228 |     protected function openXmlStream(string $innerPath, ?string $encoding = null, ?int $options = 0): bool
229 |     {
230 |         $this->zip = new \ZipArchive();
231 | 
232 |         if ($this->zip->open($this->xlsxFile) !== true) {
233 |             throw new Exception('Failed to open archive: ' . $this->xlsxFile);
234 |         }
235 | 
236 |         $st = $this->zip->getStream($innerPath);
237 |         if ($st === false) {
238 |             throw new Exception("Internal file not found: {$innerPath}");
239 |         }
240 | 
241 |         $tmp = $this->makeTempFile();
242 |         $out = fopen($tmp, 'wb');
243 |         if (!$out) {
244 |             fclose($st);
245 |             throw new Exception("Failed to create temporary file: {$tmp}");
246 |         }
247 | 
248 |         stream_copy_to_stream($st, $out);
249 |         fclose($st);
250 |         fclose($out);
251 | 
252 |         if (!$this->open($tmp, $encoding, $options)) {
253 |             throw new Exception("XMLReader::open() failed to open {$tmp}");
254 |         }
255 | 
256 |         return true;
257 |     }
258 | 
259 |     /**
260 |      * @return bool
261 |      */
262 |     #[\ReturnTypeWillChange]
263 |     public function close(): bool
264 |     {
265 |         $result = parent::close();
266 |         if ($result) {
267 |             if ($this->innerFile) {
268 |                 $this->innerFile = null;
269 |             }
270 |             foreach ($this->tmpFiles as $tmp) {
271 |                 if (is_file($tmp)) {
272 |                     @unlink($tmp);
273 |                 }
274 |             }
275 |         }
276 | 
277 |         return $result;
278 |     }
279 | 
280 |     /**
281 |      * xl/workbook.xml
282 |      *
283 |      * @return bool
284 |      */
285 |     public function openWorkbook(): bool
286 |     {
287 |         return $this->openZip('xl/workbook.xml');
288 |     }
289 | 
290 |     /**
291 |      * xl/sharedStrings.xml (the file may be missing)
292 |      *
293 |      * @return bool
294 |      */
295 |     public function openSharedStrings(): bool
296 |     {
297 |         return $this->zip->locateName('xl/sharedStrings.xml') !== false
298 |             && $this->openZip('xl/sharedStrings.xml');
299 |     }
300 | 
301 |     /**
302 |      * Returns a list of sheets from workbook.xml: [[name, sheetId, rId] ...]
303 |      *
304 |      * @return array
305 |      */
306 |     public function sheetList(): array
307 |     {
308 |         $sheets = [];
309 |         $this->openWorkbook();
310 | 
311 |         while ($this->read()) {
312 |             if ($this->nodeType === \XMLReader::ELEMENT && $this->name === 'sheet') {
313 |                 $sheets[] = [
314 |                     'name' => $this->getAttribute('name'),
315 |                     'sheetId' => $this->getAttribute('sheetId'),
316 |                     'rId' => $this->getAttribute('r:id'),
317 |                 ];
318 |             }
319 |         }
320 |         $this->close();
321 | 
322 |         return $sheets;
323 |     }
324 | 
325 |     /**
326 |      * Open a sheet by index (0..n-1) or name (string).
327 |      * Automatically reads workbook.xml.rels to map rId -> worksheets/sheetN.xml
328 |      *
329 |      * @param int $index
330 |      *
331 |      * @return bool
332 |      */
333 |     public function openSheetByIndex(int $index): bool
334 |     {
335 |         $sheets = $this->sheetList();
336 |         if (!isset($sheets[$index])) {
337 |             throw new Exception("Sheet with index {$index} not found");
338 |         }
339 |         return $this->openSheetByRelId($sheets[$index]['rId']);
340 |     }
341 | 
342 |     /**
343 |      * @param string $name
344 |      *
345 |      * @return bool
346 |      */
347 |     public function openSheetByName(string $name): bool
348 |     {
349 |         foreach ($this->sheetList() as $s) {
350 |             if ($s['name'] === $name) {
351 |                 return $this->openSheetByRelId($s['rId']);
352 |             }
353 |         }
354 |         throw new Exception("Sheet named '{$name}' not found");
355 |     }
356 | 
357 |     /**
358 |      * Opens xl/_rels/workbook.xml.rels and finds Target by rId
359 |      *
360 |      * @param string $rId
361 |      *
362 |      * @return bool
363 |      */
364 |     protected function openSheetByRelId(string $rId): bool
365 |     {
366 |         $this->openZip('xl/_rels/workbook.xml.rels');
367 |         $target = null;
368 | 
369 |         while ($this->read()) {
370 |             if ($this->nodeType === \XMLReader::ELEMENT && $this->name === 'Relationship') {
371 |                 if ($this->getAttribute('Id') === $rId) {
372 |                     $target = $this->getAttribute('Target'); // "worksheets/sheet1.xml"
373 |                     break;
374 |                 }
375 |             }
376 |         }
377 |         $this->close();
378 | 
379 |         if ($target === null) {
380 |             throw new Exception("Target not found by rId={$rId} in workbook.xml.rels");
381 |         }
382 | 
383 |         // относительный путь от xl/
384 |         $inner = 'xl/' . ltrim($target, '/');
385 | 
386 |         return $this->openZip($inner);
387 |     }
388 | 
389 |     /**
390 |      * @param string $tagName
391 |      *
392 |      * @return bool
393 |      */
394 |     public function seekOpenTag(string $tagName): bool
395 |     {
396 |         while ($this->read()) {
397 |             if ($this->nodeType === \XMLReader::ELEMENT && $this->name === $tagName) {
398 |                 return true;
399 |             }
400 |         }
401 |         return false;
402 |     }
403 | 
404 |     public function validate()
405 |     {
406 |         $this->setParserProperty(self::VALIDATE, true);
407 |         foreach ($this->fileList() as $file) {
408 |             echo $file, '
'; 409 | } 410 | } 411 | } 412 | 413 | // EOF -------------------------------------------------------------------------------- /tests/FastExcelReaderTest.php: -------------------------------------------------------------------------------- 1 | assertEquals('A1:D4', $excel->sheet()->dimension()); 20 | 21 | $result = $excel->readCells(); 22 | $this->assertTrue(isset($result['A1']) && $result['A1'] === '#'); 23 | $this->assertTrue(isset($result['B1']) && $result['B1'] === 'name'); 24 | $this->assertTrue(isset($result['A2']) && $result['A2'] === 1); 25 | $this->assertTrue(isset($result['C3']) && $result['C3'] === 6614697600); 26 | 27 | $result = $excel->readRows(); 28 | $this->assertEquals(count($result), $excel->sheet()->countRows()); 29 | $this->assertTrue(isset($result['1']['B']) && $result['1']['B'] === 'name'); 30 | $this->assertTrue(isset($result['3']['C']) && $result['3']['C'] === 6614697600); 31 | 32 | $result = $excel->readColumns(); 33 | $this->assertEquals(count($result), $excel->sheet()->countCols()); 34 | $this->assertTrue(isset($result['B']['1']) && $result['B']['1'] === 'name'); 35 | $this->assertTrue(isset($result['C']['4']) && $result['C']['4'] === -6845212800); 36 | 37 | // Read rows and use the first row as column keys 38 | $result = $excel->readRows(true); 39 | $this->assertTrue(isset($result['2']['name']) && $result['2']['name'] === 'James Bond'); 40 | $this->assertTrue(isset($result['3']['birthday']) && $result['3']['birthday'] === 6614697600); 41 | 42 | $result = $excel->readRows(true, Excel::KEYS_SWAP); 43 | $this->assertTrue(isset($result['name']['3']) && $result['name']['3'] === 'Ellen Louise Ripley'); 44 | 45 | $result = $excel->readRows(false, Excel::KEYS_ZERO_BASED); 46 | $this->assertTrue(isset($result[3][1]) && $result[3][1] === 'Captain Jack Sparrow'); 47 | 48 | $result = $excel->readRows(false, Excel::KEYS_ONE_BASED); 49 | $this->assertTrue(isset($result[1][2]) && $result[1][2] === 'name'); 50 | 51 | $result = $excel->readRows(['A' => 'Number', 'B' => 'Hero', 'D' => 'Secret']); 52 | $this->assertFalse(isset($result[0]['Hero'])); 53 | $this->assertTrue(isset($result[1]['Number']) && $result[1]['Number'] === '#'); 54 | $this->assertTrue(isset($result[1]['Hero']) && $result[1]['Hero'] === 'name'); 55 | $this->assertTrue(isset($result[2]['Hero']) && $result[2]['Hero'] === 'James Bond'); 56 | $this->assertTrue(isset($result[2]['C']) && $result[2]['C'] === -2205187200); 57 | $this->assertTrue(isset($result[2]['Secret']) && $result[2]['Secret'] === 4573); 58 | 59 | $result = $excel->readRows(['A' => 'Number', 'B' => 'Hero', 'D' => 'Secret'], Excel::KEYS_FIRST_ROW); 60 | $this->assertFalse(isset($result[0]['Hero'])); 61 | $this->assertFalse(isset($result[1]['Hero'])); 62 | $this->assertTrue(isset($result[2]['Hero']) && $result[2]['Hero'] === 'James Bond'); 63 | $this->assertTrue(isset($result[2]['birthday']) && $result[2]['birthday'] === -2205187200); 64 | 65 | $result = $excel->readRows(['B' => 'Hero', 'D' => 'Secret'], Excel::KEYS_FIRST_ROW | Excel::KEYS_ROW_ZERO_BASED); 66 | $this->assertTrue(isset($result[0]['Hero']) && $result[0]['Hero'] === 'James Bond'); 67 | $this->assertTrue(isset($result[0]['birthday']) && $result[0]['birthday'] === -2205187200); 68 | 69 | $result = $excel->readRows(['B' => 'Hero', 'D' => 'Secret'], Excel::KEYS_ROW_ZERO_BASED); 70 | $this->assertTrue(isset($result[0]['Hero']) && $result[0]['Hero'] === 'name'); 71 | $this->assertTrue(isset($result[0]['C']) && $result[0]['C'] === 'birthday'); 72 | $this->assertTrue(isset($result[1]['Hero']) && $result[1]['Hero'] === 'James Bond'); 73 | $this->assertTrue(isset($result[1]['C']) && $result[1]['C'] === -2205187200); 74 | 75 | $result = $excel->readRows(); 76 | $this->assertTrue(isset($result[1]['B']) && $result[1]['B'] === 'name'); 77 | 78 | $result = $excel->readRows([], Excel::KEYS_FIRST_ROW); 79 | $this->assertTrue(isset($result[2]['name']) && $result[2]['name'] === 'James Bond'); 80 | 81 | $result = []; 82 | $sheet = $excel->setReadArea('c2'); 83 | foreach ($sheet->nextRow() as $row => $rowData) { 84 | $result[$row] = $rowData; 85 | } 86 | $this->assertCount(3, $result); 87 | $this->assertFalse(isset($result[1])); 88 | $this->assertFalse(isset($result[2]['A'])); 89 | $this->assertTrue(isset($result[2]['C']) && $result[2]['C'] === -2205187200); 90 | $this->assertTrue(isset($result[2]['D']) && $result[2]['D'] === 4573); 91 | $this->assertFalse(isset($result[2]['E'])); 92 | $this->assertFalse(isset($result[5])); 93 | 94 | $sheet->reset(); 95 | $result = []; 96 | $sheet = $excel->setReadArea('c2'); 97 | while ($rowData = $sheet->readNextRow()) { 98 | $result[] = $rowData; 99 | } 100 | $this->assertCount(3, $result); 101 | $this->assertTrue(isset($result[0]['C']) && $result[0]['C'] === -2205187200); 102 | $this->assertTrue(isset($result[0]['D']) && $result[0]['D'] === 4573); 103 | $this->assertFalse(isset($result[2]['E'])); 104 | $this->assertFalse(isset($result[5])); 105 | 106 | $excel->setDateFormat('Y-m-d'); 107 | $result = $excel->readCells(); 108 | $this->assertEquals('1900-02-14', $result['C2']); 109 | $this->assertEquals('2179-08-12', $result['C3']); 110 | $this->assertEquals('1753-01-31', $result['C4']); 111 | 112 | $sheet = $excel->sheet('Sheet2'); 113 | $result = $sheet->readCells(); 114 | $this->assertEquals('_x1000_', $result['B11']); 115 | 116 | $excel->dateFormatter(false); 117 | $sheet = $excel->sheet('Sheet3'); 118 | $result = $sheet->readCells(); 119 | $this->assertEquals(['A1' => 1706918400, 'A2' => 1706918400, 'A3' => '3', 'A4' => '3.2', 'A5' => '3.2.24', 'A6' => '3.2.24.7', 'A7' => '3.2.24.d', ], $result); 120 | } 121 | 122 | public function testExcelReader01(): void 123 | { 124 | // ===================== 125 | $file = self::DEMO_DIR . 'demo-01-base.xlsx'; 126 | $excel = Excel::open($file); 127 | 128 | $cells = $excel->sheet()->readCells(true); 129 | $this->assertEquals('A1', array_key_first($cells)); 130 | $this->assertCount(4216, $cells); 131 | $this->assertEquals(142408, $cells['H2']['v']); 132 | 133 | $cells = $excel->sheet()->setReadArea('c10')->readCells(); 134 | $this->assertEquals('C10', array_key_first($cells)); 135 | $this->assertCount(3108, $cells); 136 | 137 | $cells = $excel->selectSheet('report', 'd10:e18')->readCells(); 138 | $this->assertEquals('D10', array_key_first($cells)); 139 | $this->assertCount(18, $cells); 140 | 141 | } 142 | 143 | public function testExcelReader02() 144 | { 145 | $file = self::DEMO_DIR . 'demo-02-advanced.xlsx'; 146 | $excel = Excel::open($file); 147 | 148 | $result = $excel 149 | ->selectSheet('Demo2', 'B5:D13') 150 | ->readRows(); 151 | $this->assertTrue(isset($result[5]['B']) && $result[5]['B'] === 2000); 152 | $this->assertTrue(isset($result[13]['D']) && round($result[13]['D']) === 104.0); 153 | 154 | $columnKeys = ['B' => 'year', 'C' => 'value1', 'D' => 'value2']; 155 | $result = $excel 156 | ->selectSheet('Demo2', 'B5:D13') 157 | ->readRows($columnKeys, Excel::KEYS_ONE_BASED); 158 | $this->assertTrue(isset($result[5]['year']) && $result[5]['year'] === 2004); 159 | $this->assertTrue(isset($result[9]['value1']) && $result[9]['value1'] === 674); 160 | 161 | // default sheet is Demo2 162 | $sheet = $excel->getSheet()->setReadArea('b4:d13'); 163 | $result = $sheet->readCellsWithStyles(); 164 | 165 | $this->assertEquals('Lorem', $result['C4']['v']); 166 | $this->assertEquals('thin', $result['C4']['s']['border']['border-left-style']); 167 | 168 | $excel->selectSheet('Demo1'); 169 | $this->assertEquals('Demo1', $excel->sheet()->name()); 170 | 171 | $excel->selectSheet('Demo2'); 172 | $this->assertEquals('Demo2', $excel->sheet()->name()); 173 | 174 | $sheet = $excel->sheet('WrongSheet'); 175 | $this->assertEquals(null, $sheet); 176 | 177 | $sheet = $excel->getSheet('Demo2', 'B4:D13', true); 178 | $result = $sheet->readRows(); 179 | $this->assertTrue(isset($result[5]['Year']) && $result[5]['Year'] === 2000); 180 | $this->assertTrue(isset($result[5]['Lorem']) && $result[5]['Lorem'] === 235); 181 | 182 | $sheet = $excel->getSheet('Demo2', 'b:c'); 183 | $result = $sheet->readRows(); 184 | $this->assertTrue(isset($result[6]['B']) && $result[6]['B'] === 2001); 185 | $this->assertFalse(isset($result[6]['D'])); 186 | 187 | $sheet = $excel->getFirstSheet(); 188 | $result = $sheet->readRows(false, Excel::KEYS_ZERO_BASED); 189 | $this->assertTrue(isset($result[3][0]) && $result[3][0] === 'Giovanni'); 190 | 191 | $this->assertEquals('Demo2', $excel->sheet()->name()); 192 | 193 | $excel->setReadArea('Values'); 194 | $result = $excel->readCells(); 195 | $this->assertEquals('Giovanni', $result['B5']); 196 | 197 | $sheet = $excel->getSheet('Demo1')->setReadArea('Headers'); 198 | $result = $sheet->readCells(); 199 | $this->assertEquals('Name', $result['B4']); 200 | 201 | $this->expectException(\avadim\FastExcelReader\Exception::class); 202 | $excel->getSheet('Demo2')->setReadArea('Values'); 203 | } 204 | 205 | public function testExcelReader03(): void 206 | { 207 | $file = self::DEMO_DIR . 'demo-03-images.xlsx'; 208 | $excel = Excel::open($file); 209 | $this->assertEquals(2, $excel->countImages()); 210 | 211 | $this->assertFalse($excel->sheet()->hasImage('c1')); 212 | $this->assertTrue($excel->sheet()->hasImage('c2')); 213 | 214 | $result = $excel->getImageList(); 215 | $this->assertTrue(isset($result['Sheet1']['C2'])); 216 | $this->assertEquals('image1.jpeg', $result['Sheet1']['C2']['file_name']); 217 | 218 | $sheet = $excel->sheet(); 219 | $images = $sheet->getImageList(); 220 | $this->assertEquals('image1.jpeg', $images['C2']['file_name']); 221 | $dir = __DIR__ . '/Files'; 222 | $file = $sheet->saveImageTo('C2', $dir); 223 | $this->assertNotNull($file); 224 | $this->assertTrue(is_file($file)); 225 | unlink($file); 226 | } 227 | 228 | public function testExcelReader03Excel365(): void 229 | { 230 | $file = self::DEMO_DIR . 'demo-03-images-excel-365.xlsx'; 231 | $excel = Excel::open($file); 232 | $this->assertEquals(2, $excel->countImages()); 233 | 234 | $this->assertFalse($excel->sheet()->hasImage('c1')); 235 | $this->assertTrue($excel->sheet()->hasImage('c2')); 236 | $this->assertTrue($excel->sheet()->hasImage('C3')); 237 | } 238 | 239 | public function testExcelReader04(): void 240 | { 241 | $file = self::DEMO_DIR . 'demo-04-styles.xlsx'; 242 | $excel = Excel::open($file); 243 | $cells = $excel->readCellsWithStyles(); 244 | $this->assertEquals('#9FC63C', $cells['A1']['s']['fill']['fill-color']); 245 | $this->assertEquals([ 246 | 'border-left-style' => 'thick', 247 | 'border-right-style' => 'thin', 248 | 'border-top-style' => 'thick', 249 | 'border-bottom-style' => 'thin', 250 | 'border-diagonal-style' => null, 251 | 'border-left-color' => '#000000', 252 | 'border-right-color' => '#000000', 253 | 'border-top-color' => '#000000', 254 | 'border-bottom-color' => '#000000', 255 | ], $cells['A6']['s']['border']); 256 | 257 | $cells = $excel->readCellStyles(true); 258 | $this->assertEquals([ 259 | 'format-num-id' => 0, 260 | 'format-pattern' => 'General', 261 | 'format-category' => 'general', 262 | 'font-size' => '10', 263 | 'font-name' => 'Arial', 264 | 'font-family' => '2', 265 | 'font-charset' => '1', 266 | 'fill-pattern' => 'solid', 267 | 'fill-color' => '#9FC63C', 268 | 'border-left-style' => null, 269 | 'border-right-style' => null, 270 | 'border-top-style' => null, 271 | 'border-bottom-style' => null, 272 | 'border-diagonal-style' => null, 273 | ], $cells['A1']); 274 | $this->assertEquals('thick', $cells['A6']['border-left-style']); 275 | $this->assertEquals('thin', $cells['A6']['border-bottom-style']); 276 | $this->assertEquals('#000000', $cells['A6']['border-top-color']); 277 | } 278 | 279 | public function testExcelReader06(): void 280 | { 281 | $file = self::DEMO_DIR . 'demo-06-data-validation.xlsx'; 282 | $excel = Excel::open($file); 283 | $sheet = $excel->getSheet('report'); 284 | 285 | $validations = $sheet->getDataValidations(); 286 | 287 | $expected = [ 288 | [ 289 | 'type' => 'decimal', 290 | 'sqref' => 'G2:G527', 291 | 'formula1' => '0.0', 292 | 'formula2' => '999999.0', 293 | ], [ 294 | 'type' => 'list', 295 | 'sqref' => 'E2:E527', 296 | 'formula1' => '"Berlin,Cape Town,Mexico City,Moscow,Sydney,Tokyo"', 297 | 'formula2' => null, 298 | ], [ 299 | 'type' => 'custom', 300 | 'sqref' => 'D2:D527', 301 | 'formula1' => 'OR(NOT(ISERROR(DATEVALUE(D2))), AND(ISNUMBER(D2), LEFT(CELL("format", D2))="D"))', 302 | 'formula2' => null, 303 | ], 304 | ]; 305 | 306 | $this->assertEquals($expected, $validations); 307 | } 308 | 309 | public function testDateFormatter(): void 310 | { 311 | // ===================== 312 | $file = self::DEMO_DIR . 'demo-02-advanced.xlsx'; 313 | $excel = Excel::open($file); 314 | 315 | $cells = $excel->sheet()->readCells(); 316 | $this->assertEquals(18316800, $cells['C5']); 317 | $this->assertEquals(-777600, $cells['C6']); 318 | $this->assertEquals(-62121600, $cells['C7']); 319 | $this->assertEquals(38707200, $cells['C8']); 320 | 321 | $excel->setDateFormat('Y-m-d'); 322 | $cells = $excel->sheet()->readCells(); 323 | $this->assertEquals('1970-08-01', $cells['C5']); 324 | $this->assertEquals('1969-12-23', $cells['C6']); 325 | $this->assertEquals('1968-01-13', $cells['C7']); 326 | $this->assertEquals('1971-03-25', $cells['C8']); 327 | 328 | $excel->dateFormatter(fn($value) => gmdate('m/d/Y', $value)); 329 | $cells = $excel->sheet()->readCells(); 330 | $this->assertEquals('08/01/1970', $cells['C5']); 331 | $this->assertEquals('12/23/1969', $cells['C6']); 332 | $this->assertEquals('01/13/1968', $cells['C7']); 333 | $this->assertEquals('03/25/1971', $cells['C8']); 334 | 335 | $excel->dateFormatter(fn($value) => (new \DateTime())->setTimestamp($value)->format('z')); 336 | $cells = $excel->sheet()->readCells(); 337 | $this->assertEquals('212', $cells['C5']); 338 | $this->assertEquals('356', $cells['C6']); 339 | $this->assertEquals('12', $cells['C7']); 340 | $this->assertEquals('83', $cells['C8']); 341 | 342 | $file = self::DEMO_DIR . 'demo-05-datetime.xlsx'; 343 | $excel = Excel::open($file); 344 | 345 | $sheet = $excel->sheet()->setReadArea('B2:B2'); 346 | $cells = $sheet->readCells(); 347 | $this->assertEquals(441696063, $cells['B2']); 348 | 349 | $excel->dateFormatter(true); 350 | $cells = $sheet->readCells(); 351 | $this->assertEquals('1983-12-31 05:21:03', $cells['B2']); 352 | 353 | $excel->dateFormatter('Y-m-d'); 354 | $cells = $sheet->readCells(); 355 | $this->assertEquals('1983-12-31', $cells['B2']); 356 | 357 | $excel->dateFormatter(fn($v) => gmdate('d/m/y', $v)); 358 | $cells = $sheet->readCells(); 359 | $this->assertEquals('31/12/83', $cells['B2']); 360 | } 361 | 362 | public function testFillRow(): void 363 | { 364 | // ===================== 365 | $file = self::DEMO_DIR . 'demo-02-advanced.xlsx'; 366 | $excel = Excel::open($file); 367 | $sheet = $excel->sheet('Demo3'); 368 | 369 | $cells = $sheet->readCells(); 370 | $this->assertCount(14, $cells); 371 | 372 | $sheet->setReadArea('a:f'); 373 | $cells = $sheet->readCells(); 374 | $this->assertCount(30, $cells); 375 | 376 | $sheet->setReadArea('a5:d6'); 377 | $cells = $sheet->readCells(); 378 | $this->assertCount(8, $cells); 379 | 380 | $excel = Excel::open($file); 381 | $sheet = $excel->sheet('Demo3'); 382 | $rows = $sheet->readRows(); 383 | $this->assertEquals(['A' => 'aaa', 'B' => 'bbb', 'C' => 'ccc', 'D' => 'ddd'], $rows[2]); 384 | $this->assertEquals(['A' => 6], $rows[6]); 385 | 386 | $sheet->setReadArea('a:f'); 387 | $rows = $sheet->readRows(null, Excel::KEYS_ROW_ZERO_BASED); 388 | $this->assertEquals(['A' => 'aaa', 'B' => 'bbb', 'C' => 'ccc', 'D' => 'ddd', 'E' => null, 'F' => null], $rows[0]); 389 | $this->assertEquals(['A' => 6, 'B' => null, 'C' => null, 'D' => null, 'E' => null, 'F' => null], $rows[4]); 390 | 391 | $excel = Excel::open($file); 392 | $sheet = $excel->sheet('Demo3'); 393 | $row = $sheet->readFirstRow(); 394 | $this->assertEquals(['A' => 'aaa', 'B' => 'bbb', 'C' => 'ccc', 'D' => 'ddd'], $row); 395 | 396 | $row = $sheet->readFirstRowCells(); 397 | $this->assertEquals(['A2' => 'aaa', 'B2' => 'bbb', 'C2' => 'ccc', 'D2' => 'ddd'], $row); 398 | 399 | $excel = Excel::open($file); 400 | $sheet = $excel->sheet('Demo3'); 401 | $this->assertEquals(['A', 2], [$sheet->firstCol(), $sheet->firstRow()]); 402 | 403 | $file = self::DEMO_DIR . 'demo-00-test.xlsx'; 404 | $excel = Excel::open($file); 405 | $sheet = $excel->sheet(); 406 | $sheet->setReadArea('a:e'); 407 | $rows = $sheet->readRows(); 408 | $this->assertEquals(['A' => '#', 'B' => 'name', 'C' => 'birthday', 'D' => 'random_int', 'E' => null], $rows[1]); 409 | 410 | $excel = Excel::open($file); 411 | $sheet = $excel->sheet(); 412 | $rows = $sheet->readRows(Excel::KEYS_FIRST_ROW); 413 | $this->assertEquals(['#' => 1, 'name' => 'James Bond', 'birthday' => -2205187200, 'random_int' => 4573], $rows[2]); 414 | 415 | $rows = []; 416 | foreach ($sheet->nextRow([], Excel::KEYS_FIRST_ROW) as $n => $rowData) { 417 | $rows[$n] = $rowData; 418 | } 419 | $this->assertEquals(['#' => 1, 'name' => 'James Bond', 'birthday' => -2205187200, 'random_int' => 4573], $rows[2]); 420 | } 421 | 422 | public function testGetColumnWidth(): void 423 | { 424 | $file = self::DEMO_DIR . 'demo-07-size-freeze-tabs.xlsx'; 425 | $excel = Excel::open($file); 426 | $width_1 = $excel->selectSheet('report')->getColumnWidth(1); 427 | $width_3 = $excel->selectSheet('report')->getColumnWidth(3); 428 | 429 | $this->assertEquals(11.85546875, $width_1); 430 | $this->assertEquals(27.85546875, $width_3); 431 | } 432 | 433 | public function testGetRowHeight(): void 434 | { 435 | $file = self::DEMO_DIR . 'demo-07-size-freeze-tabs.xlsx'; 436 | $excel = Excel::open($file); 437 | $height_1 = $excel->selectSheet('report')->getRowHeight(1); 438 | $height_3 = $excel->selectSheet('report')->getRowHeight(3); 439 | 440 | $this->assertEquals(15, $height_1); 441 | $this->assertEquals(35.25, $height_3); 442 | } 443 | 444 | public function testGetFreezePane(): void 445 | { 446 | $file = self::DEMO_DIR . 'demo-07-size-freeze-tabs.xlsx'; 447 | $excel = Excel::open($file); 448 | $freezePane = $excel->selectSheet('report')->getFreezePaneInfo(); 449 | 450 | $this->assertEquals([ 451 | 'xSplit' => 0, 452 | 'ySplit' => 1, 453 | 'topLeftCell' => 'A2' 454 | ], $freezePane); 455 | } 456 | 457 | public function testGetTabColorInfo(): void 458 | { 459 | $file = self::DEMO_DIR . 'demo-07-size-freeze-tabs.xlsx'; 460 | $excel = Excel::open($file); 461 | $config = $excel->selectSheet('report')->getTabColorInfo(); 462 | 463 | $this->assertEquals([ 464 | 'theme' => '2', 465 | 'tint' => '-0.499984740745262' 466 | ], $config); 467 | } 468 | 469 | public function testRefPath(): void 470 | { 471 | // ===================== 472 | $file = self::DEMO_DIR . 'worksheet-referenced-with-absolute-path.xlsx'; 473 | $excel = Excel::open($file); 474 | $result = $excel->readRows(true, Excel::KEYS_ROW_ZERO_BASED); 475 | $this->assertEquals('983ST13', $result[1]['code']); 476 | $this->assertEquals(821, $result[1]['price']); 477 | } 478 | 479 | public function testExcelReaderDimension(): void 480 | { 481 | // ===================== 482 | $file = self::DEMO_DIR . 'demo-00-test.xlsx'; 483 | $excel = Excel::open($file); 484 | 485 | $this->assertEquals('A1:D4', $excel->sheet()->dimension()); 486 | 487 | $result = $excel->readRows(); 488 | $this->assertEquals(count($result), $excel->sheet()->countRows()); 489 | 490 | $this->assertEquals(4, $excel->sheet()->countRows('C3:E6')); 491 | $this->assertEquals(3, $excel->sheet()->minRow('C3:E6')); 492 | $this->assertEquals(6, $excel->sheet()->maxRow('C3:E6')); 493 | 494 | $this->assertEquals(1, $excel->sheet()->countRows('C3')); 495 | $this->assertEquals(6, $excel->sheet()->minRow('E6')); 496 | $this->assertEquals(3, $excel->sheet()->maxRow('C3')); 497 | 498 | $result = $excel->readColumns(); 499 | $this->assertEquals(count($result), $excel->sheet()->countCols()); 500 | 501 | $this->assertEquals(3, $excel->sheet()->countColumns('C3:E6')); 502 | $this->assertEquals('C', $excel->sheet()->minColumn('C3:E6')); 503 | $this->assertEquals('E', $excel->sheet()->maxColumn('C3:E6')); 504 | } 505 | 506 | public function testExcelReaderFormulas(): void 507 | { 508 | // ===================== 509 | $file = __DIR__ . '/Files/formulas.xlsx';; 510 | $excel = Excel::open($file); 511 | $sheet = $excel->sheet(); 512 | 513 | $cells = $sheet->readCellsWithStyles(); 514 | 515 | $this->assertEquals(null, $cells['A2']['f']); 516 | $this->assertEquals('=A2+1', $cells['B2']['f']); 517 | $this->assertEquals('=B2+1', $cells['C2']['f']); 518 | $this->assertEquals('=C2+1', $cells['D2']['f']); 519 | $this->assertEquals('=SUM(A2:D2)', $cells['E2']['f']); 520 | 521 | $this->assertEquals('=B3+1', $cells['A4']['f']); 522 | $this->assertEquals('=A4+1', $cells['B4']['f']); 523 | $this->assertEquals('=B4+1', $cells['C4']['f']); 524 | $this->assertEquals('=C4+1', $cells['D4']['f']); 525 | $this->assertEquals('=SUM(A4:D4)', $cells['E4']['f']); 526 | 527 | $this->assertEquals('=B8+1', $cells['A9']['f']); 528 | $this->assertEquals('=A9+1', $cells['B9']['f']); 529 | $this->assertEquals('=B9+1', $cells['C9']['f']); 530 | $this->assertEquals('=C9+1', $cells['D9']['f']); 531 | $this->assertEquals('=SUM(A9:D9)', $cells['E9']['f']); 532 | } 533 | } 534 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![GitHub Release](https://img.shields.io/github/v/release/aVadim483/fast-excel-reader)](https://packagist.org/packages/avadim/fast-excel-reader) 2 | [![Packagist Downloads](https://img.shields.io/packagist/dt/avadim/fast-excel-reader?color=%23aa00aa)](https://packagist.org/packages/avadim/fast-excel-reader) 3 | [![GitHub License](https://img.shields.io/github/license/aVadim483/fast-excel-reader)](https://packagist.org/packages/avadim/fast-excel-reader) 4 | [![Static Badge](https://img.shields.io/badge/php-%3E%3D7.4-005fc7)](https://packagist.org/packages/avadim/fast-excel-reader) 5 | 6 | # FastExcelReader 7 | 8 | **FastExcelReader** is a part of the FastExcelPhp Project which consists of 9 | 10 | * [FastExcelWriter](https://packagist.org/packages/avadim/fast-excel-writer) - to create Excel spreadsheets 11 | * [FastExcelReader](https://packagist.org/packages/avadim/fast-excel-reader) - to reader Excel spreadsheets 12 | * [FastExcelTemplator](https://packagist.org/packages/avadim/fast-excel-templator) - to generate Excel spreadsheets from XLSX templates 13 | * [FastExcelLaravel](https://packagist.org/packages/avadim/fast-excel-laravel) - special **Laravel** edition 14 | 15 | ## Introduction 16 | 17 | This library is designed to be lightweight, super-fast and requires minimal memory usage. 18 | 19 | **FastExcelReader** can read Excel compatible spreadsheets in XLSX format (Office 2007+). 20 | It only reads data, but it does it very quickly and with minimal memory usage. 21 | 22 | **Features** 23 | 24 | * Supports XLSX format only (Office 2007+) with multiple worksheets 25 | * Supports autodetect currency/numeric/date types 26 | * Supports auto formatter and custom formatter of datetime values 27 | * The library can define and extract images from XLSX files 28 | * The library can read styling options of cells - formatting patterns, colors, borders, fonts, etc. 29 | 30 | ## Installation 31 | 32 | Use `composer` to install **FastExcelReader** into your project: 33 | 34 | ``` 35 | composer require avadim/fast-excel-reader 36 | ``` 37 | 38 | Jump to: 39 | * [Simple example](#simple-example) 40 | * [Read values row by row in loop](#read-values-row-by-row-in-loop) 41 | * [Keys in resulting arrays](#keys-in-resulting-arrays) 42 | * [Empty cells & rows](#empty-cells--rows) 43 | * [Advanced example](#advanced-example) 44 | * [Date Formatter](#date-formatter) 45 | * [Images functions](#images-functions) 46 | * [Cell value types](#cell-value-types) 47 | * [How to get complete info about the cell style](#how-to-get-complete-info-about-the-cell-style) 48 | * [Retrieve data validation rules](#retrieve-data-validation-rules) 49 | * [Column Widths](#column-widths) 50 | * [Row Heights](#row-heights) 51 | * [Freeze Pane Info](#freeze-pane-info) 52 | * [Tab Color Info](#tab-color-info) 53 | * [Info about merged cells](#info-about-merged-cells) 54 | * [Some useful methods](#some-useful-methods) 55 | 56 | ## Usage 57 | 58 | You can find more examples in */demo* folder 59 | 60 | ### Simple example 61 | ![demo file](demo/files/img1.jpg) 62 | ```php 63 | use \avadim\FastExcelReader\Excel; 64 | 65 | $file = __DIR__ . '/files/demo-00-simple.xlsx'; 66 | 67 | // Open XLSX-file 68 | $excel = Excel::open($file); 69 | // Read all values as a flat array from current sheet 70 | $result = $excel->readCells(); 71 | ``` 72 | You will get this array: 73 | ```text 74 | Array 75 | ( 76 | [A1] => 'col1' 77 | [B1] => 'col2' 78 | [A2] => 111 79 | [B2] => 'aaa' 80 | [A3] => 222 81 | [B3] => 'bbb' 82 | ) 83 | ``` 84 | 85 | ```php 86 | // Read all rows in two-dimensional array (ROW x COL) 87 | $result = $excel->readRows(); 88 | ``` 89 | You will get this array: 90 | ```text 91 | Array 92 | ( 93 | [1] => Array 94 | ( 95 | ['A'] => 'col1' 96 | ['B'] => 'col2' 97 | ) 98 | [2] => Array 99 | ( 100 | ['A'] => 111 101 | ['B'] => 'aaa' 102 | ) 103 | [3] => Array 104 | ( 105 | ['A'] => 222 106 | ['B'] => 'bbb' 107 | ) 108 | ) 109 | ``` 110 | 111 | ```php 112 | // Read all columns in two-dimensional array (COL x ROW) 113 | $result = $excel->readColumns(); 114 | ``` 115 | You will get this array: 116 | ```text 117 | Array 118 | ( 119 | [A] => Array 120 | ( 121 | [1] => 'col1' 122 | [2] => 111 123 | [3] => 222 124 | ) 125 | 126 | [B] => Array 127 | ( 128 | [1] => 'col2' 129 | [2] => 'aaa' 130 | [3] => 'bbb' 131 | ) 132 | 133 | ) 134 | ``` 135 | 136 | ### Read values row by row in loop 137 | ```php 138 | $sheet = $excel->sheet(); 139 | foreach ($sheet->nextRow() as $rowNum => $rowData) { 140 | // $rowData is array ['A' => ..., 'B' => ...] 141 | $addr = 'C' . $rowNum; 142 | if ($sheet->hasImage($addr)) { 143 | $sheet->saveImageTo($addr, $fullDirectoryPath); 144 | } 145 | // handling of $rowData here 146 | // ... 147 | } 148 | 149 | // OR 150 | foreach ($sheet->nextRow() as $rowNum => $rowData) { 151 | // handling of $rowData here 152 | // ... 153 | // get image list from current row 154 | $imageList = $sheet->getImageListByRow(); 155 | foreach ($imageList as $imageInfo) { 156 | $imageBlob = $sheet->getImageBlob($imageInfo['address']); 157 | } 158 | } 159 | 160 | // OR 161 | foreach ($sheet->nextRow(['A' => 'One', 'B' => 'Two'], Excel::KEYS_FIRST_ROW) as $rowNum => $rowData) { 162 | // $rowData is array ['One' => ..., 'Two' => ...] 163 | // ... 164 | } 165 | ``` 166 | NOTE: Every time you call the ```foreach ($sheet->nextRow() as $rowIndex => $row)``` loop, 167 | reading data starts from the first row. 168 | 169 | But there is an alternative way to read row by row - using the readNextRow() method. 170 | In this case, you first need to call the ```$sheet->reset(...)``` method with the required reading parameters, 171 | and then you can call `````$sheet-readNextRow()`````. If at some point you need to start reading data from the beginning, 172 | you need to call ```$sheet->reset(...)``` again. 173 | 174 | ```php 175 | // Init the internal read generator 176 | $sheet->reset(['A' => 'One', 'B' => 'Two'], Excel::KEYS_FIRST_ROW); 177 | 178 | // read the first row 179 | $rowData = $sheet->readNextRow(); 180 | var_dump($rowData); 181 | 182 | // Read the next 3 rows 183 | for ($i = 0; $i < 3; $i++) { 184 | $rowData = $sheet->readNextRow(); 185 | var_dump($rowData); 186 | } 187 | 188 | // Reset the internal generator and read all rows starting from the first one 189 | $sheet->reset(['A' => 'One', 'B' => 'Two'], Excel::KEYS_FIRST_ROW); 190 | $result = []; 191 | while ($rowData = $sheet->readNextRow()) { 192 | $result[] = $rowData; 193 | } 194 | var_dump($result); 195 | ``` 196 | 197 | ### Keys in resulting arrays 198 | ```php 199 | // Read rows and use the first row as column keys 200 | $result = $excel->readRows(true); 201 | ``` 202 | You will get this result: 203 | ```text 204 | Array 205 | ( 206 | [2] => Array 207 | ( 208 | ['col1'] => 111 209 | ['col2'] => 'aaa' 210 | ) 211 | [3] => Array 212 | ( 213 | ['col1'] => 222 214 | ['col2'] => 'bbb' 215 | ) 216 | ) 217 | ``` 218 | The optional second argument specifies the result array keys 219 | ```php 220 | 221 | // Rows and cols start from zero 222 | $result = $excel->readRows(false, Excel::KEYS_ZERO_BASED); 223 | ``` 224 | You will get this result: 225 | ```text 226 | Array 227 | ( 228 | [0] => Array 229 | ( 230 | [0] => 'col1' 231 | [1] => 'col2' 232 | ) 233 | [1] => Array 234 | ( 235 | [0] => 111 236 | [1] => 'aaa' 237 | ) 238 | [2] => Array 239 | ( 240 | [0] => 222 241 | [1] => 'bbb' 242 | ) 243 | ) 244 | ``` 245 | Allowed values of result mode 246 | 247 | | mode options | descriptions | 248 | |---------------------|---------------------------------------------------------------------------------| 249 | | KEYS_ORIGINAL | rows from '1', columns from 'A' (default) | 250 | | KEYS_ROW_ZERO_BASED | rows from 0 | 251 | | KEYS_COL_ZERO_BASED | columns from 0 | 252 | | KEYS_ZERO_BASED | rows from 0, columns from 0 (same as KEYS_ROW_ZERO_BASED + KEYS_COL_ZERO_BASED) | 253 | | KEYS_ROW_ONE_BASED | rows from 1 | 254 | | KEYS_COL_ONE_BASED | columns from 1 | 255 | | KEYS_ONE_BASED | rows from 1, columns from 1 (same as KEYS_ROW_ONE_BASED + KEYS_COL_ONE_BASED) | 256 | 257 | Additional options that can be combined with result modes 258 | 259 | | options | descriptions | 260 | |-----------------|----------------------------------------------| 261 | | KEYS_FIRST_ROW | the same as _true_ in the first argument | 262 | | KEYS_RELATIVE | index from top left cell of area (not sheet) | 263 | | KEYS_SWAP | swap rows and columns | 264 | 265 | For example 266 | ```php 267 | 268 | $result = $excel->readRows(['A' => 'bee', 'B' => 'honey'], Excel::KEYS_FIRST_ROW | Excel::KEYS_ROW_ZERO_BASED); 269 | ``` 270 | You will get this result: 271 | ```text 272 | Array 273 | ( 274 | [0] => Array 275 | ( 276 | [bee] => 111 277 | [honey] => 'aaa' 278 | ) 279 | 280 | [1] => Array 281 | ( 282 | [bee] => 222 283 | [honey] => 'bbb' 284 | ) 285 | 286 | ) 287 | ``` 288 | 289 | ### Empty cells & rows 290 | 291 | The library already skips empty cells and empty rows by default. Empty cells are cells where nothing is written, 292 | and empty rows are rows where all cells are empty. If a cell contains an empty string, it is not considered empty. 293 | But you can change this behavior and skip cells with empty strings. 294 | 295 | ```php 296 | $sheet = $excel->sheet(); 297 | 298 | // Skip empty cells and empty rows 299 | foreach ($sheet->nextRow() as $rowNum => $rowData) { 300 | // handle $rowData 301 | } 302 | 303 | // Skip empty cells and cells with empty strings 304 | foreach ($sheet->nextRow([], Excel::TREAT_EMPTY_STRING_AS_EMPTY_CELL) as $rowNum => $rowData) { 305 | // handle $rowData 306 | } 307 | 308 | // Skip empty cells and empty rows (rows containing only whitespace characters are also considered empty) 309 | foreach ($sheet->nextRow([], Excel::TRIM_STRINGS | Excel::TREAT_EMPTY_STRING_AS_EMPTY_CELL) as $rowNum => $rowData) { 310 | // handle $rowData 311 | } 312 | ``` 313 | Other way 314 | ```php 315 | $sheet->reset([], Excel::TRIM_STRINGS | Excel::TREAT_EMPTY_STRING_AS_EMPTY_CELL); 316 | $rowData = $sheet->readNextRow(); 317 | // do something 318 | 319 | $rowData = $sheet->readNextRow(); 320 | // handle next row 321 | 322 | // ... 323 | ``` 324 | 325 | 326 | ### Advanced example 327 | ```php 328 | use \avadim\FastExcelReader\Excel; 329 | 330 | $file = __DIR__ . '/files/demo-02-advanced.xlsx'; 331 | 332 | $excel = Excel::open($file); 333 | 334 | $result = [ 335 | 'sheets' => $excel->getSheetNames() // get all sheet names 336 | ]; 337 | 338 | $result['#1'] = $excel 339 | // select sheet by name 340 | ->selectSheet('Demo1') 341 | // select area with data where the first row contains column keys 342 | ->setReadArea('B4:D11', true) 343 | // set date format 344 | ->setDateFormat('Y-m-d') 345 | // set key for column 'C' to 'Birthday' 346 | ->readRows(['C' => 'Birthday']); 347 | 348 | // read other arrays with custom column keys 349 | // and in this case we define range by columns only 350 | $columnKeys = ['B' => 'year', 'C' => 'value1', 'D' => 'value2']; 351 | $result['#2'] = $excel 352 | ->selectSheet('Demo2', 'B:D') 353 | ->readRows($columnKeys); 354 | 355 | $result['#3'] = $excel 356 | ->setReadArea('F5:H13') 357 | ->readRows($columnKeys); 358 | ``` 359 | You can set read area by defined names in workbook. For example if workbook has defined name **Headers** with range **Demo1!$B$4:$D$4** 360 | then you can read cells by this name 361 | 362 | ```php 363 | $excel->setReadArea('Values'); 364 | $cells = $excel->readCells(); 365 | ``` 366 | Note that since the value contains the sheet name, this sheet becomes the default sheet. 367 | 368 | You can set read area in the sheet 369 | ```php 370 | $sheet = $excel->getSheet('Demo1')->setReadArea('Headers'); 371 | $cells = $sheet->readCells(); 372 | ``` 373 | But if you try to use this name on another sheet, you will get an error 374 | ```php 375 | $sheet = $excel->getSheet('Demo2')->setReadArea('Headers'); 376 | // Exception: Wrong address or range "Values" 377 | 378 | ``` 379 | 380 | If necessary, you can fully control the reading process using the method ```readSheetCallback()``` with callback-function 381 | ```php 382 | use \avadim\FastExcelReader\Excel; 383 | 384 | $excel = Excel::open($file); 385 | 386 | $result = []; 387 | $excel->readCallback(function ($row, $col, $val) use(&$result) { 388 | // Any manipulation here 389 | $result[$row][$col] = (string)$val; 390 | 391 | // if the function returns true then data reading is interrupted 392 | return false; 393 | }); 394 | var_dump($result); 395 | ``` 396 | 397 | ### Date Formatter 398 | By default, all datetime values returns as timestamp. But you can change this behavior using ```dateFormatter()``` 399 | 400 | ![demo date](demo/files/img2.jpg) 401 | ```php 402 | $excel = Excel::open($file); 403 | $sheet = $excel->sheet()->setReadArea('B5:D7'); 404 | $cells = $sheet->readCells(); 405 | echo $cells['C5']; // -2205187200 406 | 407 | // If argument TRUE is passed, then all dates will be formatted as specified in cell styles 408 | // IMPORTANT! The datetime format depends on the locale 409 | $excel->dateFormatter(true); 410 | $cells = $sheet->readCells(); 411 | echo $cells['C5']; // '14.02.1900' 412 | 413 | // You can specify date format pattern 414 | $excel->dateFormatter('Y-m-d'); 415 | $cells = $sheet->readCells(); 416 | echo $cells['C5']; // '1900-02-14' 417 | 418 | // set date formatter function 419 | $excel->dateFormatter(fn($value) => gmdate('m/d/Y', $value)); 420 | $cells = $sheet->readCells(); 421 | echo $cells['C5']; // '02/14/1900' 422 | 423 | // returns DateTime instance 424 | $excel->dateFormatter(fn($value) => (new \DateTime())->setTimestamp($value)); 425 | $cells = $sheet->readCells(); 426 | echo get_class($cells['C5']); // 'DateTime' 427 | 428 | // custom manipulations with datetime values 429 | $excel->dateFormatter(function($value, $format, $styleIdx) use($excel) { 430 | // get Excel format of the cell, e.g. '[$-F400]h:mm:ss\ AM/PM' 431 | $excelFormat = $excel->getFormatPattern($styleIdx); 432 | 433 | // get format converted for use in php functions date(), gmdate(), etc 434 | // for example the Excel pattern above would be converted to 'g:i:s A' 435 | $phpFormat = $excel->getDateFormatPattern($styleIdx); 436 | 437 | // and if you need you can get value of numFmtId for this cell 438 | $style = $excel->getCompleteStyleByIdx($styleIdx, true); 439 | $numFmtId = $style['format-num-id']; 440 | 441 | // do something and write to $result 442 | $result = gmdate($phpFormat, $value); 443 | 444 | return $result; 445 | }); 446 | ``` 447 | Sometimes, if a cell's format is specified as a date but does not contain a date, the library may misinterpret this value. To avoid this, you can disable date formatting 448 | 449 | ![demo date](demo/files/img3.jpg) 450 | 451 | Here, cell B1 contains the string "3.2" and cell B2 contains the date 2024-02-03, but both cells are set to the date format 452 | 453 | ```php 454 | $excel = Excel::open($file); 455 | // default mode 456 | $cells = $sheet->readCells(); 457 | echo $cell['B1']; // -2208798720 - the library tries to interpret the number 3.2 as a timestamp 458 | echo $cell['B2']; // 1706918400 - timestamp of 2024-02-03 459 | 460 | // date formatter is on 461 | $excel->dateFormatter(true); 462 | $cells = $sheet->readCells(); 463 | echo $cell['B1']; // '03.01.1900' 464 | echo $cell['B2']; // '3.2' 465 | 466 | // date formatter is off 467 | $excel->dateFormatter(false); 468 | $cells = $sheet->readCells(); 469 | echo $cell['B1']; // '3.2' 470 | echo $cell['B2']; // 1706918400 - timestamp of 2024-02-03 471 | 472 | ``` 473 | 474 | ### Images functions 475 | ```php 476 | // Returns count images on all sheets 477 | $excel->countImages(); 478 | 479 | // Returns count images on sheet 480 | $sheet->countImages(); 481 | 482 | // Returns image list of sheet 483 | $sheet->getImageList(); 484 | 485 | // Returns image list of specified row 486 | $sheet->getImageListByRow($rowNumber); 487 | 488 | // Returns TRUE if the specified cell has an image 489 | $sheet->hasImage($cellAddress); 490 | 491 | // Returns mime type of image in the specified cell (or NULL) 492 | $sheet->getImageMimeType($cellAddress); 493 | 494 | // Returns inner name of image in the specified cell (or NULL) 495 | $sheet->getImageName($cellAddress); 496 | 497 | // Returns an image from the cell as a blob (if exists) or NULL 498 | $sheet->getImageBlob($cellAddress); 499 | 500 | // Writes an image from the cell to the specified filename 501 | $sheet->saveImage($cellAddress, $fullFilenamePath); 502 | 503 | // Writes an image from the cell to the specified directory 504 | $sheet->saveImageTo($cellAddress, $fullDirectoryPath); 505 | ``` 506 | 507 | ## Cell value types 508 | 509 | The library tries to determine the types of cell values, and in most cases it does it right. 510 | Therefore, you get numeric or string values. Date values are returned as a timestamp by default. 511 | But you can change this behavior by setting the date format (see the formatting options for the date() php function). 512 | 513 | ```php 514 | $excel = Excel::open($file); 515 | $result = $excel->readCells(); 516 | print_r($result); 517 | ``` 518 | The above example will output: 519 | ```text 520 | Array 521 | ( 522 | [B2] => -2205187200 523 | [B3] => 6614697600 524 | [B4] => -6845212800 525 | ) 526 | ``` 527 | ```php 528 | $excel = Excel::open($file); 529 | $excel->setDateFormat('Y-m-d'); 530 | $result = $excel->readCells(); 531 | print_r($result); 532 | ``` 533 | The above example will output: 534 | ```text 535 | Array 536 | ( 537 | [B2] => '1900-02-14' 538 | [B3] => '2179-08-12' 539 | [B4] => '1753-01-31' 540 | ) 541 | ``` 542 | 543 | ## How to get complete info about the cell style 544 | 545 | Usually read functions return just cell values, but you can read the values with styles. 546 | In this case, for each cell, not a scalar value will be returned, but an array 547 | like ['v' => _scalar_value_, 's' => _style_array_, 'f' => _formula_] 548 | 549 | ```php 550 | $excel = Excel::open($file); 551 | 552 | $sheet = $excel->sheet(); 553 | 554 | $rows = $sheet->readRowsWithStyles(); 555 | $columns = $sheet->readColumnsWithStyles(); 556 | $cells = $sheet->readCellsWithStyles(); 557 | 558 | $cells = $sheet->readCellsWithStyles(); 559 | ``` 560 | Or you can read styles only (without values) 561 | ```php 562 | $cells = $sheet->readCellStyles(); 563 | /* 564 | array ( 565 | 'format' => 566 | array ( 567 | 'format-num-id' => 0, 568 | 'format-pattern' => 'General', 569 | ), 570 | 'font' => 571 | array ( 572 | 'font-size' => '10', 573 | 'font-name' => 'Arial', 574 | 'font-family' => '2', 575 | 'font-charset' => '1', 576 | ), 577 | 'fill' => 578 | array ( 579 | 'fill-pattern' => 'solid', 580 | 'fill-color' => '#9FC63C', 581 | ), 582 | 'border' => 583 | array ( 584 | 'border-left-style' => NULL, 585 | 'border-right-style' => NULL, 586 | 'border-top-style' => NULL, 587 | 'border-bottom-style' => NULL, 588 | 'border-diagonal-style' => NULL, 589 | ), 590 | ) 591 | */ 592 | $cells = $sheet->readCellStyles(true); 593 | /* 594 | array ( 595 | 'format-num-id' => 0, 596 | 'format-pattern' => 'General', 597 | 'font-size' => '10', 598 | 'font-name' => 'Arial', 599 | 'font-family' => '2', 600 | 'font-charset' => '1', 601 | 'fill-pattern' => 'solid', 602 | 'fill-color' => '#9FC63C', 603 | 'border-left-style' => NULL, 604 | 'border-right-style' => NULL, 605 | 'border-top-style' => NULL, 606 | 'border-bottom-style' => NULL, 607 | 'border-diagonal-style' => NULL, 608 | ) 609 | */ 610 | ``` 611 | But we do not recommend using these methods with large files 612 | 613 | ## Retrieve data validation rules 614 | Every sheet in your XLSX file can contain a set of data validation rules. To retrieve them, you can imply call `getDataValidations` on your sheet 615 | 616 | ```php 617 | $excel = Excel::open($file); 618 | 619 | $sheet = $excel->sheet(); 620 | 621 | $validations = $sheet->getDataValidations(); 622 | /* 623 | [ 624 | [ 625 | 'type' => 'list', 626 | 'sqref' => 'E2:E527', 627 | 'formula1' => '"Berlin,Cape Town,Mexico City,Moscow,Sydney,Tokyo"', 628 | 'formula2' => null, 629 | ], [ 630 | 'type' => 'decimal', 631 | 'sqref' => 'G2:G527', 632 | 'formula1' => '0.0', 633 | 'formula2' => '999999.0', 634 | ], 635 | ] 636 | */ 637 | ``` 638 | 639 | ## Column Widths 640 | Retrieve the width of a specific column in a sheet: 641 | 642 | ```php 643 | $excel = Excel::open($file); 644 | $sheet = $excel->selectSheet('SheetName'); 645 | 646 | // Get the width of column 1 (column 'A') 647 | $columnWidth = $sheet->getColumnWidth(1); 648 | 649 | echo $columnWidth; // Example: 11.85 650 | ``` 651 | 652 | ## Row Heights 653 | Retrieve the height of a specific row in a sheet: 654 | 655 | ```php 656 | $excel = Excel::open($file); 657 | $sheet = $excel->selectSheet('SheetName'); 658 | 659 | // Get the height of row 1 660 | $rowHeight = $sheet->getRowHeight(1); 661 | 662 | echo $rowHeight; // Example: 15 663 | ``` 664 | 665 | ## Freeze Pane Info 666 | Retrieve the freeze pane info for a sheet: 667 | 668 | ```php 669 | $excel = Excel::open($file); 670 | $sheet = $excel->selectSheet('SheetName'); 671 | 672 | // Get the freeze pane configuration 673 | $freezePaneConfig = $sheet->getFreezePaneInfo(); 674 | 675 | print_r($freezePaneConfig); 676 | /* 677 | Example Output: 678 | Array 679 | ( 680 | [xSplit] => 0 681 | [ySplit] => 1 682 | [topLeftCell] => 'A2' 683 | ) 684 | */ 685 | ``` 686 | 687 | ## Tab Color Info 688 | Retrieve the tab color info for a sheet: 689 | 690 | ```php 691 | Copy code 692 | $excel = Excel::open($file); 693 | $sheet = $excel->selectSheet('SheetName'); 694 | 695 | // Get the tab color configuration 696 | $tabColorConfig = $sheet->getTabColorInfo(); 697 | 698 | print_r($tabColorConfig); 699 | /* 700 | Example Output: 701 | Array 702 | ( 703 | [theme] => '2' 704 | [tint] => '-0.499984740745262' 705 | ) 706 | */ 707 | ``` 708 | 709 | ## Info about merged cells 710 | 711 | You can use the following methods: 712 | 713 | * ```Sheet::getMergedCells()``` -- Returns all merged ranges 714 | * ```Sheet::isMerged(string $cellAddress)``` -- Checks if a cell is merged 715 | * ```Sheet::mergedRange(string $cellAddress)``` -- Returns merge range of specified cell 716 | 717 | For example 718 | ```php 719 | if ($sheet->isMerged('B3')) { 720 | $range = $sheet->mergedRange('B3'); 721 | } 722 | ``` 723 | 724 | ## Count rows and columns 725 | 726 | Each sheet contains the ```dimension``` property with the range of the area in which the data is written. 727 | If only one cell is filled on the sheet, then there should be an address of only this cell of the form "B2", 728 | otherwise it is a range of the form "B2:E10". 729 | 730 | There are several methods that get data from this property: 731 | * ```dimension()``` -- Returns dimension of default work area from sheet properties 732 | * ```countRows()``` -- Count rows from dimension 733 | * ```countColumns()``` -- Count columns from dimension 734 | * ```minRow()``` -- The minimal row number from sheet properties 735 | * ```maxRows()``` -- The maximal row number from sheet properties 736 | * ```minColumn()``` -- The minimal column letter from sheet properties 737 | * ```maxColumn()``` -- The maximal column letter from sheet properties 738 | 739 | But sometimes the ```dimension``` property contains incorrect information. 740 | For example, it may contain the address of only the first cell of the data range or the address of only the last cell. 741 | In such cases, you can use methods that scan the entire sheet and count the actual number of rows and columns with data on the sheet. 742 | 743 | IMPORTANT: these methods are slower than methods using the ```dimension``` property 744 | 745 | * ```actualDimension()``` -- Returns dimension of the actual work area 746 | * ```countActualRows()``` -- Count actual rows from the sheet 747 | * ```minActualRow()``` -- The minimal actual row number 748 | * ```maxActualRow()``` -- The maximal actual row number 749 | * ```countActualColumns()``` -- Count actual columns from the sheet 750 | * ```minActualColumn()``` -- The minimal actual column letter 751 | * ```maxActualColumn()``` -- The maximal actual column letter 752 | 753 | ## Some useful methods 754 | ### Excel object 755 | * ```getSheetNames()``` -- Returns names array of all sheets 756 | * ```sheet(?string $name = null)``` -- Returns default or specified sheet 757 | * ```getSheet(string $name, ?string $areaRange = null, ?bool $firstRowKeys = false)``` -- Get sheet by name 758 | * ```getSheetById(int $sheetId, ?string $areaRange = null, ?bool $firstRowKeys = false)``` -- Get sheet by id 759 | * ```getFirstSheet(?string $areaRange = null, ?bool $firstRowKeys = false)``` -- Get the first sheet 760 | * ```selectSheet(string $name, ?string $areaRange = null, ?bool $firstRowKeys = false)``` -- Select default sheet by name and returns it 761 | * ```selectSheetById(int $sheetId, ?string $areaRange = null, ?bool $firstRowKeys = false)``` -- Select default sheet by id and returns it 762 | * ```selectFirstSheet(?string $areaRange = null, ?bool $firstRowKeys = false)``` -- Select the first sheet as default and returns it 763 | * ```getDefinedNames()``` -- Returns defined names of workbook 764 | 765 | ### Sheet object 766 | * ```name()``` -- Returns name of string 767 | * ```isActive()``` -- Active worksheet 768 | * ```isHidden()``` -- If worksheet is hidden 769 | * ```isVisible()``` -- If worksheet is visible 770 | * ```state()``` -- Returns string state of worksheet (used in ```isHidden()``` and ```isVisible()```) 771 | * ```maxColumn()``` -- The maximal column letter from sheet properties 772 | * ```firstRow()``` -- The actual number of the first row from the sheet data area (may not match the value from ```minRow()```) 773 | * ```firstCol()``` -- The actual letter of the first column from the sheet data area (may not match the value from ```minColumn()```) 774 | * ```readFirstRow()``` -- Returns values of cells of 1st row as array 775 | * ```readFirstRowWithStyles()``` -- Returns values and styles of cells of 1st row as array 776 | * ```getColumnWidth(int)``` -- Returns the width of a given column number 777 | * ```getFreezePaneConfig()``` -- Returns an array containing freeze pane configuration 778 | * ```getTabColorConfiguration()``` -- Returns an array containing tab color configuration 779 | 780 | ## Do you want to support FastExcelReader? 781 | 782 | if you find this package useful you can give me a star on GitHub. 783 | 784 | Or you can donate me :) 785 | * USDT (TRC20) TSsUFvJehQBJCKeYgNNR1cpswY6JZnbZK7 786 | * USDT (ERC20) 0x5244519D65035aF868a010C2f68a086F473FC82b 787 | * ETH 0x5244519D65035aF868a010C2f68a086F473FC82b -------------------------------------------------------------------------------- /src/FastExcelReader/Excel.php: -------------------------------------------------------------------------------- 1 | [...], '__row' => [...]] 33 | public const RESULT_MODE_ROW = 1024; 34 | 35 | public const TRIM_STRINGS = 2048; 36 | public const TREAT_EMPTY_STRING_AS_EMPTY_CELL = 4096; 37 | 38 | 39 | 40 | protected string $file; 41 | 42 | /** @var Reader */ 43 | protected Reader $xmlReader; 44 | 45 | protected array $fileList = []; 46 | 47 | protected array $relations = []; 48 | 49 | protected array $sharedStrings = []; 50 | 51 | protected array $styles = []; 52 | 53 | protected array $valueMetadataImages = []; 54 | 55 | /** @var Sheet[] */ 56 | protected array $sheets = []; 57 | 58 | protected int $defaultSheetId; 59 | 60 | protected ?string $dateFormat = null; 61 | 62 | /** @var \Closure|callable|bool|null */ 63 | protected $dateFormatter = null; 64 | 65 | protected bool $date1904 = false; 66 | protected string $timezone; 67 | 68 | protected array $builtinFormats = []; 69 | 70 | protected array $names = []; 71 | 72 | protected ?array $themeColors = null; 73 | 74 | protected int $countImages = -1; // -1 - unknown 75 | 76 | 77 | /** 78 | * Excel constructor 79 | * 80 | * @param string|null $file 81 | * @param string|null $tempDir 82 | */ 83 | public function __construct(?string $file = null, ?string $tempDir = '') 84 | { 85 | $this->builtinFormats = [ 86 | 0 => ['pattern' => 'General', 'category' => 'general'], 87 | 1 => ['pattern' => '0', 'category' => 'number'], 88 | 2 => ['pattern' => '0.00', 'category' => 'number'], 89 | 3 => ['pattern' => '#,##0', 'category' => 'number'], 90 | 4 => ['pattern' => '#,##0.00', 'category' => 'number'], 91 | 9 => ['pattern' => '0%', 'category' => 'number'], 92 | 10 => ['pattern' => '0.00%', 'category' => 'number'], 93 | 11 => ['pattern' => '0.00E+00', 'category' => 'number'], 94 | 12 => ['pattern' => '# ?/?', 'category' => 'general'], 95 | 13 => ['pattern' => '# ??/??', 'category' => 'general'], 96 | 14 => ['pattern' => 'mm-dd-yy', 'category' => 'date'], // Short date 97 | 15 => ['pattern' => 'd-mmm-yy', 'category' => 'date'], 98 | 16 => ['pattern' => 'd-mmm', 'category' => 'date'], 99 | 17 => ['pattern' => 'mmm-yy', 'category' => 'date'], 100 | 18 => ['pattern' => 'h:mm AM/PM', 'category' => 'date'], 101 | 19 => ['pattern' => 'h:mm:ss AM/PM', 'category' => 'date'], 102 | 20 => ['pattern' => 'h:mm', 'category' => 'date'], // Short time 103 | 21 => ['pattern' => 'h:mm:ss', 'category' => 'date'], // Long time 104 | 22 => ['pattern' => 'm/d/yy h:mm', 'category' => 'date'], // Date-time 105 | 37 => ['pattern' => '#,##0 ;(#,##0)', 'category' => 'number'], 106 | 38 => ['pattern' => '#,##0 ;[Red](#,##0)', 'category' => 'number'], 107 | 39 => ['pattern' => '#,##0.00;(#,##0.00)', 'category' => 'number'], 108 | 40 => ['pattern' => '#,##0.00;[Red](#,##0.00)', 'category' => 'number'], 109 | 45 => ['pattern' => 'mm:ss', 'category' => 'date'], 110 | 46 => ['pattern' => '[h]:mm:ss', 'category' => 'date'], 111 | 47 => ['pattern' => 'mmss.0', 'category' => 'date'], 112 | 48 => ['pattern' => '##0.0E+0', 'category' => 'number'], 113 | 49 => ['pattern' => '@', 'category' => 'string'], 114 | ]; 115 | 116 | if (class_exists('IntlDateFormatter', false)) { 117 | $formatter = new \IntlDateFormatter(null, \IntlDateFormatter::SHORT, \IntlDateFormatter::NONE); 118 | $pattern = $formatter->getPattern(); 119 | $this->builtinFormats[14]['pattern'] = str_replace('#', 'yy', str_replace(['M', 'y'], ['m', 'yyyy'], str_replace('yy', '#', $pattern))); 120 | if (preg_match('/([^a-z])/i', $pattern, $m)) { 121 | $dateDelim = $m[1]; 122 | $this->builtinFormats[15]['pattern'] = str_replace('-', $dateDelim, $this->builtinFormats[15]['pattern']); 123 | $this->builtinFormats[16]['pattern'] = str_replace('-', $dateDelim, $this->builtinFormats[16]['pattern']); 124 | $this->builtinFormats[17]['pattern'] = str_replace('-', $dateDelim, $this->builtinFormats[17]['pattern']); 125 | } 126 | 127 | $formatter = new \IntlDateFormatter(null, \IntlDateFormatter::NONE, \IntlDateFormatter::SHORT); 128 | $this->builtinFormats[20]['pattern'] = str_replace('HH', 'h', $formatter->getPattern()); 129 | 130 | $formatter = new \IntlDateFormatter(null, \IntlDateFormatter::NONE, \IntlDateFormatter::MEDIUM); 131 | $this->builtinFormats[21]['pattern'] = str_replace('HH', 'h', $formatter->getPattern()); 132 | 133 | $this->builtinFormats[22]['pattern'] = $this->builtinFormats[14]['pattern'] . ' ' . $this->builtinFormats[20]['pattern']; 134 | } 135 | 136 | $this->timezone = date_default_timezone_get(); 137 | $this->dateFormatter = function ($value, $format = null) { 138 | if ($format || $this->dateFormat) { 139 | return gmdate($format ?: $this->dateFormat, $value); 140 | } 141 | return $value; 142 | }; 143 | 144 | if (!empty($tempDir)) { 145 | self::setTempDir($tempDir); 146 | } 147 | 148 | if ($file) { 149 | $this->file = $file; 150 | $this->_prepare($file); 151 | } 152 | } 153 | 154 | /** 155 | * Set dir for temporary files 156 | * 157 | * @param $tempDir 158 | */ 159 | public static function setTempDir($tempDir) 160 | { 161 | Reader::setTempDir($tempDir); 162 | } 163 | 164 | 165 | /** 166 | * @param string $file 167 | */ 168 | protected function _prepare(string $file): void 169 | { 170 | $this->xmlReader = static::createReader($file); 171 | $this->fileList = $this->xmlReader->fileList(); 172 | foreach ($this->fileList as $fileName) { 173 | if (strpos($fileName, 'xl/drawings/drawing') === 0) { 174 | $this->relations['drawings'][] = $fileName; 175 | } 176 | elseif (strpos($fileName, 'xl/media/') === 0) { 177 | $this->relations['media'][] = $fileName; 178 | } 179 | } 180 | 181 | $innerFile = 'xl/_rels/workbook.xml.rels'; 182 | $this->xmlReader->openZip($innerFile); 183 | while ($this->xmlReader->read()) { 184 | if ($this->xmlReader->nodeType === \XMLReader::ELEMENT && $this->xmlReader->name === 'Relationship') { 185 | $type = basename($this->xmlReader->getAttribute('Type')); 186 | if ($type) { 187 | $this->relations[$type][$this->xmlReader->getAttribute('Id')] = 'xl/' . ltrim($this->xmlReader->getAttribute('Target'), '/xl'); 188 | } 189 | } 190 | } 191 | $this->xmlReader->close(); 192 | 193 | if (isset($this->relations['worksheet'])) { 194 | $this->_loadSheets(); 195 | } 196 | 197 | if (isset($this->relations['sharedStrings'])) { 198 | $innerFile = $this->checkInnerFile(reset($this->relations['sharedStrings'])); 199 | if ($innerFile) { 200 | $this->_loadSharedStrings($innerFile); 201 | } 202 | } 203 | 204 | if (isset($this->relations['theme'])) { 205 | $innerFile = $this->checkInnerFile(reset($this->relations['theme'])); 206 | if ($innerFile) { 207 | $this->_loadThemes($innerFile); 208 | } 209 | } 210 | 211 | if (isset($this->relations['styles'])) { 212 | $innerFile = $this->checkInnerFile(reset($this->relations['styles'])); 213 | if ($innerFile) { 214 | $this->_loadStyles($innerFile); 215 | } 216 | } 217 | 218 | if (isset($this->relations['sheetMetadata'], $this->relations['richValueRel'])) { 219 | $metadataFile = $this->checkInnerFile(reset($this->relations['sheetMetadata'])); 220 | $richValueRelFile = $this->checkInnerFile(reset($this->relations['richValueRel'])); 221 | $this->_loadMetadataImages($metadataFile, $richValueRelFile); 222 | } 223 | 224 | if ($this->sheets) { 225 | // set current sheet 226 | $this->selectFirstSheet(); 227 | } 228 | } 229 | 230 | /** 231 | * @param string $innerFile 232 | * 233 | * @return null|string 234 | */ 235 | protected function checkInnerFile(string $innerFile): ?string 236 | { 237 | foreach ($this->fileList as $filename) { 238 | if (strcasecmp($innerFile, $filename) === 0) { 239 | return $filename; 240 | } 241 | } 242 | return null; 243 | } 244 | 245 | protected function _loadSheets(): void 246 | { 247 | $innerFile = $this->checkInnerFile('xl/workbook.xml'); 248 | $this->xmlReader->openZip($innerFile); 249 | 250 | while ($this->xmlReader->read()) { 251 | if ($this->xmlReader->nodeType === \XMLReader::ELEMENT) { 252 | $xmlReaderName = $this->xmlReader->name; 253 | if ($xmlReaderName === 'workbookPr') { 254 | $date1904 = (string)$this->xmlReader->getAttribute('date1904'); 255 | if ($date1904 === '1' || $date1904 === 'true') { 256 | $this->date1904 = true; 257 | } 258 | } 259 | elseif ($xmlReaderName === 'sheet' || $xmlReaderName === 'x:sheet') { 260 | $rId = $this->xmlReader->getAttribute('r:id'); 261 | $sheetId = $this->xmlReader->getAttribute('sheetId'); 262 | $path = $this->relations['worksheet'][$rId] ?? null; 263 | // ignoring non-existent sheets 264 | if ($path) { 265 | $sheetName = $this->xmlReader->getAttribute('name'); 266 | $this->sheets[$sheetId] = static::createSheet($sheetName, $sheetId, $this->file, $this->relations['worksheet'][$rId], $this); 267 | //$this->sheets[$sheetId]->excel = $this; 268 | if ($this->sheets[$sheetId]->isActive()) { 269 | $this->defaultSheetId = $sheetId; 270 | } 271 | if ($state = $this->xmlReader->getAttribute('state')) { 272 | $this->sheets[$sheetId]->setState($state); 273 | } 274 | } 275 | } 276 | elseif ($xmlReaderName === 'definedName') { 277 | $name = $this->xmlReader->getAttribute('name'); 278 | $address = $this->xmlReader->readString(); 279 | $this->names[$name] = $address; 280 | } 281 | } 282 | } 283 | $this->xmlReader->close(); 284 | } 285 | 286 | /** 287 | * @param string $innerFile 288 | */ 289 | protected function _loadSharedStrings(string $innerFile) 290 | { 291 | $this->xmlReader->openZip($innerFile); 292 | while ($this->xmlReader->read()) { 293 | if ($this->xmlReader->nodeType === \XMLReader::ELEMENT && $this->xmlReader->name === 'si' && $node = $this->xmlReader->expand()) { 294 | $this->sharedStrings[] = $node->textContent; 295 | } 296 | } 297 | $this->xmlReader->close(); 298 | } 299 | 300 | /** 301 | * @param string|null $innerFile 302 | * 303 | * @return void 304 | */ 305 | protected function _loadThemes(?string $innerFile = null) 306 | { 307 | $innerFile = $this->checkInnerFile($innerFile ?: 'xl/theme/theme1.xml'); 308 | $this->xmlReader->openZip($innerFile); 309 | while ($this->xmlReader->read()) { 310 | if ($this->xmlReader->nodeType === \XMLReader::ELEMENT && $this->xmlReader->localName === 'clrScheme') { 311 | break; 312 | } 313 | } 314 | while ($this->xmlReader->read()) { 315 | if ($this->xmlReader->nodeType === \XMLReader::END_ELEMENT && $this->xmlReader->localName === 'clrScheme') { 316 | break; 317 | } 318 | if ($this->xmlReader->nodeType === \XMLReader::ELEMENT && $this->xmlReader->localName === 'srgbClr') { 319 | $this->themeColors[] = '#' . $this->xmlReader->getAttribute('val'); 320 | } 321 | elseif ($this->xmlReader->nodeType === \XMLReader::ELEMENT && $this->xmlReader->localName === 'sysClr') { 322 | if ($this->xmlReader->getAttribute('val') === 'windowText') { 323 | $this->themeColors[] = '#ffffff'; 324 | } 325 | elseif ($this->xmlReader->getAttribute('val') === 'window') { 326 | $this->themeColors[] = '#202020'; 327 | } 328 | elseif ($lastClr = $this->xmlReader->getAttribute('lastClr')) { 329 | $this->themeColors[] = '#' . $lastClr; 330 | } 331 | else { 332 | $this->themeColors[] = ''; 333 | } 334 | } 335 | } 336 | } 337 | 338 | /** 339 | * @param string|null $innerFile 340 | */ 341 | protected function _loadStyles(?string $innerFile = null) 342 | { 343 | $innerFile = $this->checkInnerFile($innerFile ?: 'xl/styles.xml'); 344 | $this->xmlReader->openZip($innerFile); 345 | $styleType = ''; 346 | while ($this->xmlReader->read()) { 347 | if ($this->xmlReader->nodeType === \XMLReader::ELEMENT) { 348 | $nodeName = $this->xmlReader->name; 349 | if ($nodeName === 'cellStyleXfs' || $nodeName === 'cellXfs') { 350 | $styleType = $nodeName; 351 | continue; 352 | } 353 | if ($nodeName === 'numFmt') { 354 | $numFmtId = (int)$this->xmlReader->getAttribute('numFmtId'); 355 | $formatCode = $this->xmlReader->getAttribute('formatCode'); 356 | $numFmts[$numFmtId] = $formatCode; 357 | } 358 | elseif ($nodeName === 'xf') { 359 | $numFmtId = (int)$this->xmlReader->getAttribute('numFmtId'); 360 | $formatCode = $numFmts[$numFmtId] ?? ''; 361 | if ($this->_isDatePattern($numFmtId, $formatCode)) { 362 | $this->styles[$styleType][] = ['format' => $formatCode, 'formatType' => 'd']; 363 | } 364 | elseif ($formatCode) { 365 | if ($this->_isNumberPattern($numFmtId, $formatCode)) { 366 | $this->styles[$styleType][] = ['format' => $formatCode, 'formatType' => 'n']; 367 | } 368 | else { 369 | $this->styles[$styleType][] = ['format' => $formatCode]; 370 | } 371 | } 372 | elseif ($numFmtId > 0 && isset($this->builtinFormats[$numFmtId]['category'])) { 373 | $this->styles[$styleType][] = ['formatType' => $this->builtinFormats[$numFmtId]['category']]; 374 | } 375 | else { 376 | $this->styles[$styleType][] = null; 377 | } 378 | } 379 | } 380 | } 381 | $this->xmlReader->close(); 382 | } 383 | 384 | /** 385 | * @param string|null $metadataFile 386 | */ 387 | protected function _loadMetadataImages(string $metadataFile, string $richValueRelFile) 388 | { 389 | $this->xmlReader->openZip($metadataFile); 390 | $metadataTypesCount = 0; 391 | $metadataTypes = []; 392 | while ($this->xmlReader->read()) { 393 | if ($this->xmlReader->name === 'metadataType') { 394 | if ($this->xmlReader->nodeType === \XMLReader::ELEMENT) { 395 | $metadataTypesCount++; 396 | if ((string)$this->xmlReader->getAttribute('name') === 'XLRICHVALUE') { 397 | // we need only 398 | $metadataTypes[$metadataTypesCount] = 'XLRICHVALUE'; 399 | } 400 | } 401 | else { 402 | break; 403 | } 404 | } 405 | } 406 | $futureMetadata = []; 407 | while ($this->xmlReader->read()) { 408 | if ($this->xmlReader->name === 'futureMetadata') { 409 | if ($this->xmlReader->nodeType === \XMLReader::ELEMENT && (string)$this->xmlReader->getAttribute('name') === 'XLRICHVALUE') { 410 | while ($this->xmlReader->read()) { 411 | if ($this->xmlReader->name === 'xlrd:rvb') { 412 | $futureMetadata[] = (int)$this->xmlReader->getAttribute('i'); 413 | } 414 | elseif ($this->xmlReader->name === 'futureMetadata' && $this->xmlReader->nodeType === \XMLReader::END_ELEMENT) { 415 | break 2; 416 | } 417 | } 418 | } 419 | elseif ($this->xmlReader->nodeType === \XMLReader::END_ELEMENT) { 420 | break; 421 | } 422 | } 423 | } 424 | 425 | while ($this->xmlReader->read()) { 426 | if ($this->xmlReader->name === 'rc') { 427 | $type = (int)$this->xmlReader->getAttribute('t'); 428 | $value = (int)$this->xmlReader->getAttribute('v'); 429 | if (isset($metadataTypes[$type])) { // metadataType name="XLRICHVALUE" 430 | if (isset($futureMetadata[$value])) { 431 | $this->valueMetadataImages[] = ['i' => $futureMetadata[$value]]; 432 | } 433 | } 434 | } 435 | } 436 | $this->xmlReader->close(); 437 | 438 | $this->xmlReader->openZip($richValueRelFile); 439 | $count = 0; 440 | while ($this->xmlReader->read()) { 441 | if ($this->xmlReader->name === 'rel' && ($rId = $this->xmlReader->getAttribute('r:id'))) { 442 | $this->valueMetadataImages[$count++]['r_id'] = $rId; 443 | } 444 | } 445 | $this->xmlReader->close(); 446 | 447 | $images = []; 448 | $xmlRels = 'xl/richData/_rels/richValueRel.xml.rels'; 449 | $this->xmlReader->openZip($xmlRels); 450 | while ($this->xmlReader->read()) { 451 | if ($this->xmlReader->name === 'Relationship' && $this->xmlReader->nodeType === \XMLReader::ELEMENT && ($Id = (string)$this->xmlReader->getAttribute('Id'))) { 452 | if (substr((string)$this->xmlReader->getAttribute('Type'), -6) === '/image') { 453 | $images[$Id] = (string)$this->xmlReader->getAttribute('Target'); 454 | } 455 | } 456 | } 457 | $this->xmlReader->close(); 458 | 459 | foreach ($this->valueMetadataImages as $index => $metadataImage) { 460 | $rId = $this->valueMetadataImages[$index]['r_id']; 461 | if (isset($images[$rId])) { 462 | $this->valueMetadataImages[$index]['file_name'] = str_replace('../media/', 'xl/media/', $images[$rId]); 463 | } 464 | } 465 | } 466 | 467 | /** 468 | * @param int $vmIndex 469 | * 470 | * @return string|null 471 | */ 472 | public function metadataImage(int $vmIndex): ?string 473 | { 474 | return $this->valueMetadataImages[$vmIndex - 1]['file_name'] ?? null; 475 | } 476 | 477 | /** 478 | * @param int|null $numFmtId 479 | * @param string $pattern 480 | * 481 | * @return bool 482 | */ 483 | protected function _isDatePattern(?int $numFmtId, string $pattern): bool 484 | { 485 | if ($numFmtId && ( 486 | ($numFmtId >= 14 && $numFmtId <= 22) 487 | || ($numFmtId >= 45 && $numFmtId <= 47) 488 | || ($numFmtId >= 27 && $numFmtId <= 36) 489 | || ($numFmtId >= 50 && $numFmtId <= 58) 490 | || ($numFmtId >= 71 && $numFmtId <= 81) 491 | )) { 492 | return true; 493 | } 494 | if ($pattern) { 495 | if (preg_match('/^\[\$-[0-9A-F]{3,4}].+/', $pattern)) { 496 | return true; 497 | } 498 | return (bool)preg_match('/yy|mm|dd|h|MM|ss|[\/\.][dm](;.+)?/', $pattern); 499 | } 500 | 501 | return false; 502 | } 503 | 504 | /** 505 | * @param int|null $numFmtId 506 | * @param string $pattern 507 | * 508 | * @return bool 509 | */ 510 | protected function _isNumberPattern(?int $numFmtId, string $pattern): bool 511 | { 512 | if (preg_match('/^0+(\.0+)?$/', $pattern)) { 513 | return true; 514 | } 515 | 516 | return false; 517 | } 518 | 519 | /** 520 | * @param $root 521 | * @param $tagName 522 | * 523 | * @return void 524 | */ 525 | protected function _loadStyleNumFmts($root, $tagName) 526 | { 527 | foreach ($this->builtinFormats as $key => $val) { 528 | $this->styles['_'][$tagName][$key] = [ 529 | 'format-num-id' => $key, 530 | 'format-pattern' => $val['pattern'], 531 | 'format-category' => $val['category'], 532 | ]; 533 | } 534 | if ($root) { 535 | foreach ($root->childNodes as $child) { 536 | $numFmtId = $child->getAttribute('numFmtId'); 537 | $formatCode = $child->getAttribute('formatCode'); 538 | if ($numFmtId !== '' && $formatCode !== '') { 539 | $node = [ 540 | 'format-num-id' => (int)$numFmtId, 541 | 'format-pattern' => $formatCode, 542 | 'format-category' => $this->_isDatePattern($numFmtId, $formatCode) ? 'date' : '', 543 | ]; 544 | $this->styles['_'][$tagName][$node['format-num-id']] = $node; 545 | } 546 | } 547 | } 548 | } 549 | 550 | /** 551 | * @param $root 552 | * @param $tagName 553 | * 554 | * @return void 555 | */ 556 | protected function _loadStyleFonts($root, $tagName) 557 | { 558 | foreach ($root->childNodes as $font) { 559 | $node = []; 560 | foreach ($font->childNodes as $fontStyle) { 561 | if ($fontStyle->nodeName === 'b') { 562 | $node['font-style-bold'] = 1; 563 | } 564 | elseif ($fontStyle->nodeName === 'u') { 565 | $node['font-style-underline'] = ($fontStyle->getAttribute('formatCode') === 'double' ? 2 : 1); 566 | } 567 | elseif ($fontStyle->nodeName === 'i') { 568 | $node['font-style-italic'] = 1; 569 | } 570 | elseif ($fontStyle->nodeName === 'strike') { 571 | $node['font-style-strike'] = 1; 572 | } 573 | elseif ($fontStyle->nodeName === 'color') { 574 | $color = $this->_extractColor($fontStyle); 575 | if ($color) { 576 | $node['font-color'] = $color; 577 | } 578 | } 579 | elseif (($v = $fontStyle->getAttribute('val')) !== '') { 580 | if ($fontStyle->nodeName === 'sz') { 581 | $name = 'font-size'; 582 | } 583 | else { 584 | $name = 'font-' . $fontStyle->nodeName; 585 | } 586 | $node[$name] = $v; 587 | } 588 | } 589 | $this->styles['_'][$tagName][] = $node; 590 | } 591 | } 592 | 593 | /** 594 | * @param $root 595 | * @param $tagName 596 | * 597 | * @return void 598 | */ 599 | protected function _loadStyleFills($root, $tagName) 600 | { 601 | foreach ($root->childNodes as $fill) { 602 | $node = []; 603 | foreach ($fill->childNodes as $patternFill) { 604 | if (($v = $patternFill->getAttribute('patternType')) !== '') { 605 | $node['fill-pattern'] = $v; 606 | } 607 | foreach ($patternFill->childNodes as $child) { 608 | if ($child->nodeName === 'fgColor') { 609 | $color = $this->_extractColor($child); 610 | if ($color) { 611 | $node['fill-color'] = $color; 612 | } 613 | } 614 | } 615 | } 616 | $this->styles['_'][$tagName][] = $node; 617 | } 618 | } 619 | 620 | /** 621 | * @param $node 622 | * 623 | * @return string 624 | */ 625 | protected function _extractColor($node): string 626 | { 627 | if ($rgb = $node->getAttribute('rgb')) { 628 | return '#' . substr($rgb, 2); 629 | } 630 | $theme = $node->getAttribute('theme'); 631 | if ($theme !== null && $theme !== '') { 632 | $color = $this->themeColors[(int)$theme] ?? ''; 633 | if ($color) { 634 | $tint = (float)$node->getAttribute('tint'); 635 | /* 636 | if (!empty($tint)) { 637 | $color0 = Helper::correctColor($color, $tint); 638 | } 639 | */ 640 | if ($tint !== 0.0) { 641 | if ($tint === 1.0) { 642 | $color = '#FFFFFF'; 643 | } 644 | elseif ($tint === -1.0) { 645 | $color = '#000000'; 646 | } 647 | else { 648 | $r = hexdec(substr($color, 1, 2)); 649 | $g = hexdec(substr($color, 3, 2)); 650 | $b = hexdec(substr($color, 5, 2)); 651 | if ($tint > 0) { 652 | $r = round($r + (255 - $r) * $tint); 653 | $g = round($g + (255 - $g) * $tint); 654 | $b = round($b + (255 - $b) * $tint); 655 | } 656 | else { 657 | $r = round($r * (1 + $tint)); 658 | $g = round($g * (1 + $tint)); 659 | $b = round($b * (1 + $tint)); 660 | } 661 | $color = '#' 662 | . strtoupper(str_pad(dechex($r), 2, '0', STR_PAD_LEFT)) 663 | . strtoupper(str_pad(dechex($g), 2, '0', STR_PAD_LEFT)) 664 | . strtoupper(str_pad(dechex($b), 2, '0', STR_PAD_LEFT)); 665 | } 666 | } 667 | } 668 | return $color; 669 | } 670 | 671 | return ''; 672 | } 673 | 674 | /** 675 | * @param $root 676 | * @param $tagName 677 | * 678 | * @return void 679 | */ 680 | protected function _loadStyleBorders($root, $tagName) 681 | { 682 | foreach ($root->childNodes as $border) { 683 | $node = []; 684 | foreach ($border->childNodes as $side) { 685 | if (($v = $side->getAttribute('style')) !== '') { 686 | $node['border-' . $side->nodeName . '-style'] = $v; 687 | } 688 | else { 689 | $node['border-' . $side->nodeName . '-style'] = null; 690 | } 691 | foreach ($side->childNodes as $child) { 692 | if ($child->nodeName === 'color') { 693 | $node['border-' . $side->nodeName . '-color'] = '#' . substr($child->getAttribute('rgb'), 2); 694 | } 695 | } 696 | } 697 | $this->styles['_'][$tagName][] = $node; 698 | } 699 | } 700 | 701 | /** 702 | * @param $root 703 | * @param $tagName 704 | * 705 | * @return void 706 | */ 707 | protected function _loadStyleCellXfs($root, $tagName) 708 | { 709 | $attributes = ['numFmtId', 'fontId', 'fillId', 'borderId', 'xfId']; 710 | foreach ($root->childNodes as $xf) { 711 | $node = []; 712 | foreach ($attributes as $attribute) { 713 | if (($v = $xf->getAttribute($attribute)) !== '') { 714 | if (substr($attribute, -2) === 'Id') { 715 | $node[$attribute] = (int)$v; 716 | } 717 | else { 718 | $node[$attribute] = $v; 719 | } 720 | } 721 | } 722 | foreach ($xf->childNodes as $child) { 723 | if ($child->nodeName === 'alignment') { 724 | if ($v = $child->getAttribute('horizontal')) { 725 | $node['format']['format-align-horizontal'] = $v; 726 | } 727 | if ($v = $child->getAttribute('vertical')) { 728 | $node['format']['format-align-vertical'] = $v; 729 | } 730 | if (($v = $child->getAttribute('wrapText')) && ($v === 'true')) { 731 | $node['format']['format-wrap-text'] = 1; 732 | } 733 | } 734 | } 735 | $this->styles['_'][$tagName][] = $node; 736 | } 737 | } 738 | 739 | /** 740 | * @param string|null $innerFile 741 | */ 742 | protected function _loadCompleteStyles(?string $innerFile = null) 743 | { 744 | if (!$innerFile) { 745 | $innerFile = 'xl/styles.xml'; 746 | } 747 | $this->xmlReader->openZip($innerFile); 748 | 749 | while ($this->xmlReader->read()) { 750 | if ($this->xmlReader->nodeType === \XMLReader::ELEMENT) { 751 | switch ($this->xmlReader->name) { 752 | case 'numFmts': 753 | $this->_loadStyleNumFmts($this->xmlReader->expand(), 'numFmts'); 754 | break; 755 | case 'fonts': 756 | $this->_loadStyleFonts($this->xmlReader->expand(), 'fonts'); 757 | break; 758 | case 'fills': 759 | $this->_loadStyleFills($this->xmlReader->expand(), 'fills'); 760 | break; 761 | case 'borders': 762 | $this->_loadStyleBorders($this->xmlReader->expand(), 'borders'); 763 | break; 764 | case 'cellStyleXfs': 765 | $this->_loadStyleCellXfs($this->xmlReader->expand(), 'cellStyleXfs'); 766 | break; 767 | case 'cellXfs': 768 | $this->_loadStyleCellXfs($this->xmlReader->expand(), 'cellXfs'); 769 | break; 770 | default: 771 | // 772 | } 773 | } 774 | } 775 | if (empty($this->styles['_']['numFmts'])) { 776 | $this->_loadStyleNumFmts(null, 'numFmts'); 777 | } 778 | $this->xmlReader->close(); 779 | } 780 | 781 | /** 782 | * Open XLSX file 783 | * 784 | * @param string $file 785 | * 786 | * @return Excel 787 | */ 788 | public static function open(string $file): Excel 789 | { 790 | return new self($file); 791 | } 792 | 793 | /** 794 | * @param string $file 795 | * @param array|null $errors 796 | * 797 | * @return bool 798 | */ 799 | public static function validate(string $file, ?array &$errors = []): bool 800 | { 801 | $result = true; 802 | $xmlReader = self::createReader($file, [\XMLReader::VALIDATE => true]); 803 | 804 | if (extension_loaded('dom') && extension_loaded('libxml') && function_exists('libxml_use_internal_errors')) { 805 | $fileList = $xmlReader->fileList(); 806 | \libxml_use_internal_errors(true); 807 | foreach ($fileList as $innerFile) { 808 | $ext = pathinfo($innerFile, PATHINFO_EXTENSION); 809 | if (in_array($ext, ['xml', 'rels', 'vml'])) { 810 | $zipFile = 'zip://' . $file . '#' . $innerFile; 811 | $dom = new \DOMDocument; 812 | $dom->load($zipFile); 813 | $errors = \libxml_get_errors(); 814 | if ($errors) { 815 | $result = false; 816 | } 817 | } 818 | } 819 | } 820 | 821 | return $result; 822 | } 823 | 824 | /** 825 | * @param string $sheetName 826 | * @param $sheetId 827 | * @param $file 828 | * @param $path 829 | * @param $excel 830 | * 831 | * @return Sheet 832 | */ 833 | public static function createSheet(string $sheetName, $sheetId, $file, $path, $excel): InterfaceSheetReader 834 | { 835 | return new Sheet($sheetName, $sheetId, $file, $path, $excel); 836 | } 837 | 838 | /** 839 | * @param string $file 840 | * @param array|null $parserProperties 841 | * 842 | * @return Reader 843 | */ 844 | public static function createReader(string $file, ?array $parserProperties = []): InterfaceXmlReader 845 | { 846 | return new Reader($file, $parserProperties); 847 | } 848 | 849 | /** 850 | * Converts an alphabetic column index to a numeric 851 | * 852 | * @param string $colLetter 853 | * 854 | * @return int 855 | */ 856 | public static function colNum(string $colLetter): int 857 | { 858 | 859 | return Helper::colNumber($colLetter); 860 | } 861 | 862 | /** 863 | * Convert column number to letter 864 | * 865 | * @param int $colNumber ONE based 866 | * 867 | * @return string 868 | */ 869 | public static function colLetter(int $colNumber): string 870 | { 871 | 872 | return Helper::colLetter($colNumber); 873 | } 874 | 875 | /** 876 | * Convert date to timestamp 877 | * 878 | * @param $excelDateTime 879 | * 880 | * @return int 881 | */ 882 | public function timestamp($excelDateTime): int 883 | { 884 | $excelDateTime = trim($excelDateTime); 885 | if (is_numeric($excelDateTime)) { 886 | $d = floor($excelDateTime); 887 | $t = $excelDateTime - $d; 888 | if ($this->date1904) { 889 | $d += 1462; // days since 1904 890 | } 891 | 892 | // Adjust for Excel erroneously treating 1900 as a leap year. 893 | if ($d <= 59) { 894 | $d++; 895 | } 896 | $t = (abs($d) > 0) ? ($d - 25569) * 86400 + round($t * 86400) : round($t * 86400); 897 | } 898 | elseif (preg_match('/^[\d\.\-\/:\s]+$/', $excelDateTime)) { 899 | if ($this->timezone !== 'UTC') { 900 | date_default_timezone_set('UTC'); 901 | } 902 | $t = strtotime($excelDateTime); 903 | if ($this->timezone !== 'UTC') { 904 | date_default_timezone_set($this->timezone); 905 | } 906 | } 907 | else { 908 | // string is not a date 909 | $t = 0; 910 | } 911 | 912 | return (int)$t; 913 | } 914 | 915 | /** 916 | * @param string $dateFormat 917 | * 918 | * @return $this 919 | */ 920 | public function setDateFormat(string $dateFormat): Excel 921 | { 922 | $this->dateFormat = $dateFormat; 923 | 924 | return $this; 925 | } 926 | 927 | /** 928 | * @return string|null 929 | */ 930 | public function getDateFormat(): ?string 931 | { 932 | return $this->dateFormat; 933 | } 934 | 935 | /** 936 | * @param $value 937 | * @param $format 938 | * @param $styleIdx 939 | * 940 | * @return false|mixed|string 941 | */ 942 | public function formatDate($value, $format = null, $styleIdx = null) 943 | { 944 | if ($this->dateFormatter && $this->dateFormatter !== true) { 945 | return ($this->dateFormatter)($value, $format, $styleIdx); 946 | } 947 | 948 | return $value; 949 | } 950 | 951 | /** 952 | * Sets custom date formatter 953 | * 954 | * @param \Closure|callable|string|bool $formatter 955 | * 956 | * @return $this 957 | */ 958 | public function dateFormatter($formatter): Excel 959 | { 960 | if ($formatter === false || $formatter === null) { 961 | $this->dateFormatter = $formatter; 962 | } 963 | elseif ($formatter === true) { 964 | $this->dateFormatter = function ($value, $format = null, $styleIdx = null) { 965 | if ($styleIdx !== null && $pattern = $this->getDateFormatPattern($styleIdx)) { 966 | return gmdate($pattern, $value); 967 | } 968 | elseif ($format || $this->dateFormat) { 969 | return gmdate($format ?: $this->dateFormat, $value); 970 | } 971 | return $value; 972 | }; 973 | } 974 | elseif (is_string($formatter)) { 975 | $this->dateFormat = $formatter; 976 | $this->dateFormatter = function ($value, $format = null) { 977 | if ($format || $this->dateFormat) { 978 | return gmdate($format ?: $this->dateFormat, $value); 979 | } 980 | return $value; 981 | }; 982 | } 983 | else { 984 | $this->dateFormatter = $formatter; 985 | } 986 | 987 | return $this; 988 | } 989 | 990 | /** 991 | * @return callable|\Closure|bool|null 992 | */ 993 | public function getDateFormatter() 994 | { 995 | return $this->dateFormatter; 996 | } 997 | 998 | /** 999 | * Returns a style array by style Idx 1000 | * 1001 | * @param $styleIdx 1002 | * 1003 | * @return array 1004 | */ 1005 | public function styleByIdx($styleIdx): array 1006 | { 1007 | return $this->styles['cellXfs'][$styleIdx] ?? []; 1008 | } 1009 | 1010 | /** 1011 | * Returns string by index 1012 | * 1013 | * @param $stringId 1014 | * 1015 | * @return string|null 1016 | */ 1017 | public function sharedString($stringId): ?string 1018 | { 1019 | return $this->sharedStrings[$stringId] ?? null; 1020 | } 1021 | 1022 | /** 1023 | * Returns defined names of workbook 1024 | * 1025 | * @return array 1026 | */ 1027 | public function getDefinedNames(): array 1028 | { 1029 | return $this->names; 1030 | } 1031 | 1032 | /** 1033 | * Returns names array of all sheets 1034 | * 1035 | * @return array 1036 | */ 1037 | public function getSheetNames(): array 1038 | { 1039 | $result = []; 1040 | foreach ($this->sheets as $sheetId => $sheet) { 1041 | $result[$sheetId] = $sheet->name(); 1042 | } 1043 | return $result; 1044 | } 1045 | 1046 | /** 1047 | * Returns current or specified sheet 1048 | * 1049 | * @param string|null $name 1050 | * 1051 | * @return Sheet|null 1052 | */ 1053 | public function sheet(?string $name = null): ?Sheet 1054 | { 1055 | $resultId = null; 1056 | if (!$name) { 1057 | $resultId = $this->defaultSheetId; 1058 | } 1059 | else { 1060 | foreach ($this->sheets as $sheetId => $sheet) { 1061 | if ($sheet->isName($name)) { 1062 | $resultId = $sheetId; 1063 | break; 1064 | } 1065 | } 1066 | } 1067 | if ($resultId && isset($this->sheets[$resultId])) { 1068 | return $this->sheets[$resultId]; 1069 | } 1070 | 1071 | return null; 1072 | } 1073 | 1074 | /** 1075 | * Returns a sheet by name 1076 | * 1077 | * @param string|null $name 1078 | * @param string|null $areaRange 1079 | * @param bool|null $firstRowKeys 1080 | * 1081 | * @return Sheet 1082 | */ 1083 | public function getSheet(?string $name = null, ?string $areaRange = null, ?bool $firstRowKeys = false): Sheet 1084 | { 1085 | $sheet = null; 1086 | if (!$name) { 1087 | $sheet = $this->sheet(); 1088 | } 1089 | else { 1090 | foreach ($this->sheets as $foundSheet) { 1091 | if ($foundSheet->isName($name)) { 1092 | $sheet = $foundSheet; 1093 | break; 1094 | } 1095 | } 1096 | if (!$sheet) { 1097 | throw new Exception('Sheet name "' . $name . '" not found'); 1098 | } 1099 | } 1100 | 1101 | if ($areaRange) { 1102 | $sheet->setReadArea($areaRange, $firstRowKeys); 1103 | } 1104 | 1105 | return $sheet; 1106 | } 1107 | 1108 | /** 1109 | * Returns a sheet by ID 1110 | * 1111 | * @param int $sheetId 1112 | * @param string|null $areaRange 1113 | * @param bool|null $firstRowKeys 1114 | * 1115 | * @return Sheet 1116 | */ 1117 | public function getSheetById(int $sheetId, ?string $areaRange = null, ?bool $firstRowKeys = false): Sheet 1118 | { 1119 | if (!isset($this->sheets[$sheetId])) { 1120 | throw new Exception('Sheet ID "' . $sheetId . '" not found'); 1121 | } 1122 | if ($areaRange) { 1123 | $this->sheets[$sheetId]->setReadArea($areaRange, $firstRowKeys); 1124 | } 1125 | 1126 | return $this->sheets[$sheetId]; 1127 | } 1128 | 1129 | /** 1130 | * Returns the first sheet as default 1131 | * 1132 | * @param string|null $areaRange 1133 | * @param bool|null $firstRowKeys 1134 | * 1135 | * @return Sheet 1136 | */ 1137 | public function getFirstSheet(?string $areaRange = null, ?bool $firstRowKeys = false): Sheet 1138 | { 1139 | $sheetId = array_key_first($this->sheets); 1140 | $sheet = $this->sheets[$sheetId]; 1141 | if ($areaRange) { 1142 | $sheet->setReadArea($areaRange, $firstRowKeys); 1143 | } 1144 | 1145 | return $sheet; 1146 | } 1147 | 1148 | /** 1149 | * Selects default sheet by name 1150 | * 1151 | * @param string $name 1152 | * @param string|null $areaRange 1153 | * @param bool|null $firstRowKeys 1154 | * 1155 | * @return Sheet 1156 | */ 1157 | public function selectSheet(string $name, ?string $areaRange = null, ?bool $firstRowKeys = false): Sheet 1158 | { 1159 | $sheet = $this->getSheet($name, $areaRange, $firstRowKeys); 1160 | $this->defaultSheetId = $sheet->id(); 1161 | 1162 | return $sheet; 1163 | } 1164 | 1165 | /** 1166 | * Selects default sheet by ID 1167 | * 1168 | * @param int $sheetId 1169 | * @param string|null $areaRange 1170 | * @param bool|null $firstRowKeys 1171 | * 1172 | * @return Sheet 1173 | */ 1174 | public function selectSheetById(int $sheetId, ?string $areaRange = null, ?bool $firstRowKeys = false): Sheet 1175 | { 1176 | $sheet = $this->getSheetById($sheetId, $areaRange, $firstRowKeys); 1177 | $this->defaultSheetId = $sheet->id(); 1178 | 1179 | return $sheet; 1180 | } 1181 | 1182 | /** 1183 | * Selects the first sheet as default 1184 | * 1185 | * @param string|null $areaRange 1186 | * @param bool|null $firstRowKeys 1187 | * 1188 | * @return Sheet 1189 | */ 1190 | public function selectFirstSheet(?string $areaRange = null, ?bool $firstRowKeys = false): Sheet 1191 | { 1192 | $sheet = $this->getFirstSheet($areaRange, $firstRowKeys); 1193 | $this->defaultSheetId = $sheet->id(); 1194 | 1195 | return $sheet; 1196 | } 1197 | 1198 | /** 1199 | * Array of all sheets 1200 | * 1201 | * @return Sheet[] 1202 | */ 1203 | public function sheets(): array 1204 | { 1205 | return $this->sheets; 1206 | } 1207 | 1208 | /** 1209 | * @param string $areaRange 1210 | * @param bool|null $firstRowKeys 1211 | * 1212 | * @return Sheet 1213 | */ 1214 | public function setReadArea(string $areaRange, ?bool $firstRowKeys = false): Sheet 1215 | { 1216 | $sheet = $this->sheets[$this->defaultSheetId]; 1217 | if (preg_match('/^\w+$/', $areaRange)) { 1218 | foreach ($this->getDefinedNames() as $name => $range) { 1219 | if ($name === $areaRange) { 1220 | [$sheetName, $definedRange] = explode('!', $range); 1221 | $sheet = $this->selectSheet($sheetName); 1222 | $areaRange = $definedRange; 1223 | break; 1224 | } 1225 | } 1226 | } 1227 | 1228 | return $sheet->setReadArea($areaRange, $firstRowKeys); 1229 | } 1230 | 1231 | /** 1232 | * Reads cell values and passes them to a callback function 1233 | * 1234 | * @param callback $callback 1235 | * @param int|null $resultMode 1236 | */ 1237 | public function readCallback(callable $callback, ?int $resultMode = null, ?bool $styleIdxInclude = null) 1238 | { 1239 | $this->sheets[$this->defaultSheetId]->readCallback($callback, $resultMode); 1240 | } 1241 | 1242 | /** 1243 | * Returns cell values as a two-dimensional array from default sheet [row][col] 1244 | * 1245 | * readRows() 1246 | * readRows(true) 1247 | * readRows(false, Excel::KEYS_ZERO_BASED) 1248 | * readRows(Excel::KEYS_ZERO_BASED | Excel::KEYS_RELATIVE) 1249 | * 1250 | * @param array|bool|int|null $columnKeys 1251 | * @param int|null $resultMode 1252 | * @param bool|null $styleIdxInclude 1253 | * 1254 | * @return array 1255 | */ 1256 | public function readRows($columnKeys = [], ?int $resultMode = null, ?bool $styleIdxInclude = null): array 1257 | { 1258 | return $this->sheets[$this->defaultSheetId]->readRows($columnKeys, $resultMode, $styleIdxInclude); 1259 | } 1260 | 1261 | /** 1262 | * Returns cell values and styles as a two-dimensional array from default sheet [row][col] 1263 | * 1264 | * @param array|bool|int|null $columnKeys 1265 | * @param int|null $resultMode 1266 | * 1267 | * @return array 1268 | */ 1269 | public function readRowsWithStyles($columnKeys = [], ?int $resultMode = null): array 1270 | { 1271 | return $this->sheets[$this->defaultSheetId]->readRowsWithStyles($columnKeys, $resultMode); 1272 | } 1273 | 1274 | /** 1275 | * Returns cell values as a two-dimensional array from default sheet [col][row] 1276 | * 1277 | * @param array|bool|int|null $columnKeys 1278 | * @param int|null $resultMode 1279 | * 1280 | * @return array 1281 | */ 1282 | public function readColumns($columnKeys = null, ?int $resultMode = null): array 1283 | { 1284 | return $this->sheets[$this->defaultSheetId]->readColumns($columnKeys, $resultMode); 1285 | } 1286 | 1287 | /** 1288 | * Returns cell values and styles as a two-dimensional array from default sheet [col][row] 1289 | * 1290 | * @param array|bool|int|null $columnKeys 1291 | * @param int|null $resultMode 1292 | * 1293 | * @return array 1294 | */ 1295 | public function readColumnsWithStyles($columnKeys = null, ?int $resultMode = null): array 1296 | { 1297 | return $this->sheets[$this->defaultSheetId]->readColumnsWithStyles($columnKeys, $resultMode); 1298 | } 1299 | 1300 | /** 1301 | * Returns the values of all cells as array 1302 | * 1303 | * @return array 1304 | */ 1305 | public function readCells(): array 1306 | { 1307 | return $this->sheets[$this->defaultSheetId]->readCells(); 1308 | } 1309 | 1310 | /** 1311 | * Returns the values and styles of all cells as array 1312 | * 1313 | * @return array 1314 | */ 1315 | public function readCellsWithStyles(): array 1316 | { 1317 | return $this->sheets[$this->defaultSheetId]->readCellsWithStyles(); 1318 | } 1319 | 1320 | /** 1321 | * Returns the styles of all cells as array 1322 | * 1323 | * @param bool|null $flat 1324 | * 1325 | * @return array 1326 | */ 1327 | public function readCellStyles(?bool $flat = false): array 1328 | { 1329 | return $this->sheets[$this->defaultSheetId]->readCellStyles($flat); 1330 | } 1331 | 1332 | public function innerFileList(): array 1333 | { 1334 | return $this->fileList; 1335 | } 1336 | 1337 | /** 1338 | * Returns TRUE if the workbook contains an any draw objects (not images only) 1339 | * 1340 | * @return bool 1341 | */ 1342 | public function hasDrawings(): bool 1343 | { 1344 | return !empty($this->relations['drawings']); 1345 | } 1346 | 1347 | /** 1348 | * Returns TRUE if any sheet contains an image object 1349 | * 1350 | * @return bool 1351 | */ 1352 | public function hasImages(): bool 1353 | { 1354 | if ($this->hasDrawings()) { 1355 | foreach ($this->sheets as $sheet) { 1356 | if ($sheet->countImages()) { 1357 | return true; 1358 | } 1359 | } 1360 | } 1361 | 1362 | return false; 1363 | } 1364 | 1365 | /** 1366 | * @return array 1367 | */ 1368 | public function mediaImageFiles(): array 1369 | { 1370 | $result = []; 1371 | if (!empty($this->relations['media'])) { 1372 | foreach ($this->relations['media'] as $mediaFile) { 1373 | $extension = strtolower(pathinfo($mediaFile, PATHINFO_EXTENSION)); 1374 | if (in_array($extension, ['jpg', 'jpeg', 'png', 'bmp', 'ico', 'webp', 'tif', 'tiff', 'gif'])) { 1375 | $result[] = basename($mediaFile); 1376 | } 1377 | } 1378 | } 1379 | 1380 | return $result; 1381 | } 1382 | 1383 | /** 1384 | * Returns the total count of images in the workbook 1385 | * 1386 | * @return int 1387 | */ 1388 | public function countImages(): int 1389 | { 1390 | if ($this->countImages === -1) { 1391 | $this->countImages = 0; 1392 | if ($this->hasDrawings() || $this->mediaImageFiles()) { 1393 | foreach ($this->sheets as $sheet) { 1394 | $this->countImages += $sheet->countImages(); 1395 | } 1396 | } 1397 | } 1398 | 1399 | return $this->countImages; 1400 | } 1401 | 1402 | /** 1403 | * Returns the list of images from the workbook 1404 | * 1405 | * @return array 1406 | */ 1407 | public function getImageList(): array 1408 | { 1409 | $result = []; 1410 | if ($this->countImages()) { 1411 | foreach ($this->sheets as $sheet) { 1412 | $result[$sheet->name()] = $sheet->getImageList(); 1413 | } 1414 | } 1415 | 1416 | return $result; 1417 | } 1418 | 1419 | /** 1420 | * @return int 1421 | */ 1422 | public function countExtraImages(): int 1423 | { 1424 | $drawingImageFiles = []; 1425 | if ($this->hasDrawings()) { 1426 | foreach ($this->sheets as $sheet) { 1427 | $imageFiles = $sheet->_getDrawingsImageFiles(); 1428 | if ($imageFiles) { 1429 | $drawingImageFiles += $imageFiles; 1430 | } 1431 | } 1432 | } 1433 | $imageFiles = $this->mediaImageFiles(); 1434 | 1435 | return (count($imageFiles) - count($drawingImageFiles)); 1436 | } 1437 | 1438 | /** 1439 | * @return bool 1440 | */ 1441 | public function hasExtraImages(): bool 1442 | { 1443 | return $this->countExtraImages() > 0; 1444 | } 1445 | 1446 | /** 1447 | * @return array 1448 | */ 1449 | public function readStyles(): array 1450 | { 1451 | if (!isset($this->styles['_'])) { 1452 | $this->styles['_'] = []; 1453 | $this->_loadCompleteStyles(); 1454 | } 1455 | 1456 | return $this->styles['_']; 1457 | } 1458 | 1459 | /** 1460 | * @param int $styleIdx 1461 | * @param bool|null $flat 1462 | * 1463 | * @return array 1464 | */ 1465 | public function getCompleteStyleByIdx(int $styleIdx, ?bool $flat = false): array 1466 | { 1467 | static $completedStyles = []; 1468 | 1469 | if (![$this->file]) { 1470 | return []; 1471 | } 1472 | 1473 | if (!isset($completedStyles[$this->file][$styleIdx])) { 1474 | if ($styleIdx !== 0) { 1475 | $result = $this->getCompleteStyleByIdx(0); 1476 | } 1477 | else { 1478 | $result = []; 1479 | } 1480 | $styles = $this->readStyles(); 1481 | if (isset($styles['cellXfs'][$styleIdx])) { 1482 | // Excel first takes the style settings with the xfId number from 1483 | // and then applies the changes specified directly in 1484 | $baseStyleId = $styles['cellXfs'][$styleIdx]['xfId'] ?? -1; 1485 | if ($baseStyleId >= 0 && isset($styles['cellStyleXfs'][$baseStyleId])) { 1486 | $baseStyle = $styles['cellStyleXfs'][$baseStyleId]; 1487 | } 1488 | else { 1489 | $baseStyle = []; 1490 | } 1491 | $result = array_replace_recursive($result, $baseStyle, $styles['cellXfs'][$styleIdx]); 1492 | if (isset($result['xfId'])) { 1493 | unset($result['xfId']); 1494 | } 1495 | } 1496 | 1497 | if (isset($result['numFmtId']) && isset($styles['numFmts'][$result['numFmtId']])) { 1498 | if (isset($result['format'])) { 1499 | $result['format'] = array_replace_recursive($result['format'], $styles['numFmts'][$result['numFmtId']]); 1500 | } 1501 | else { 1502 | $result['format'] = $styles['numFmts'][$result['numFmtId']]; 1503 | } 1504 | unset($result['numFmtId']); 1505 | } 1506 | 1507 | if (isset($result['fontId']) && isset($styles['fonts'][$result['fontId']])) { 1508 | if (isset($result['font'])) { 1509 | $result['font'] = array_replace_recursive($result['font'], $styles['fonts'][$result['fontId']]); 1510 | } 1511 | else { 1512 | $result['font'] = $styles['fonts'][$result['fontId']]; 1513 | } 1514 | unset($result['fontId']); 1515 | } 1516 | 1517 | if (isset($result['fillId']) && isset($styles['fills'][$result['fillId']])) { 1518 | if (isset($result['fill'])) { 1519 | $result['fill'] = array_replace_recursive($result['fill'], $styles['fills'][$result['fillId']]); 1520 | } 1521 | else { 1522 | $result['fill'] = $styles['fills'][$result['fillId']]; 1523 | } 1524 | unset($result['fillId']); 1525 | } 1526 | 1527 | if (isset($result['borderId']) && isset($styles['borders'][$result['borderId']])) { 1528 | if (isset($result['border'])) { 1529 | $result['border'] = array_replace_recursive($result['border'], $styles['borders'][$result['borderId']]); 1530 | } 1531 | else { 1532 | $result['border'] = $styles['borders'][$result['borderId']]; 1533 | } 1534 | unset($result['borderId']); 1535 | } 1536 | 1537 | $completedStyles[$this->file][$styleIdx] = $result; 1538 | } 1539 | else { 1540 | $result = $completedStyles[$this->file][$styleIdx]; 1541 | } 1542 | 1543 | if ($flat && $result) { 1544 | $result = array_merge(...array_values($result)); 1545 | } 1546 | 1547 | return $result; 1548 | } 1549 | 1550 | /** 1551 | * @param int $styleIdx 1552 | * 1553 | * @return mixed|string 1554 | */ 1555 | public function getFormatPattern(int $styleIdx) 1556 | { 1557 | $style = $this->getCompleteStyleByIdx($styleIdx); 1558 | 1559 | return $style['format']['format-pattern'] ?? ''; 1560 | } 1561 | 1562 | /** 1563 | * @param $pattern 1564 | * 1565 | * @return string|null 1566 | */ 1567 | public function _convertDateFormatPattern($pattern): ?string 1568 | { 1569 | static $patterns = []; 1570 | 1571 | if (isset($patterns[$pattern])) { 1572 | return $patterns[$pattern]; 1573 | } 1574 | 1575 | if ($this->_isDatePattern(null, $pattern) && preg_match('/^(\[.+])?([^;]+)(;.*)?/', $pattern, $m)) { 1576 | if (strpos($m[2], 'AM/PM')) { 1577 | $am = true; 1578 | $pattern = str_replace('AM/PM', 'A', $m[2]); 1579 | } 1580 | elseif (strpos($m[1], 'am/pm')) { 1581 | $am = true; 1582 | $pattern = str_replace('am/pm', 'a', $m[2]); 1583 | } 1584 | else { 1585 | $am = false; 1586 | $pattern = $m[2]; 1587 | } 1588 | $pattern = str_replace(['\\ ', '\\-', '\\/'], [' ', '-', '/'], $pattern); 1589 | $pattern = preg_replace(['/^mm(\W)s/', '/h(\W)mm$/', '/h(\W)mm([^m])/', '/([^m])mm(\W)s/'], ['i$1s', 'h$1i', 'h$1i$2', '$1i$1s'], $pattern); 1590 | if ($am) { 1591 | $pattern = str_replace(['hh', 'h'], ['h', 'g'], $pattern); 1592 | } 1593 | else { 1594 | $pattern = str_replace(['hh', 'h'], ['H', 'G'], $pattern); 1595 | } 1596 | if (strpos($pattern, 'dd') !== false) { 1597 | $pattern = str_replace('dd', 'd', $pattern); 1598 | } 1599 | else { 1600 | $pattern = str_replace('d', 'j', $pattern); 1601 | } 1602 | $pattern = str_replace('mmmm', 'F', $pattern); 1603 | $pattern = str_replace('mmm', 'M', $pattern); 1604 | if (strpos($pattern, 'mm') !== false) { 1605 | $pattern = str_replace('mm', 'm', $pattern); 1606 | } 1607 | else { 1608 | $pattern = str_replace('m', 'n', $pattern); 1609 | } 1610 | $convert = [ 1611 | 'ss' => 's', 1612 | 'dddd' => 'l', 1613 | 'ddd' => 'D', 1614 | 'mmmm' => 'F', 1615 | 'mmm' => 'M', 1616 | 'yyyy' => 'Y', 1617 | 'yy' => 'y', 1618 | ]; 1619 | $patterns[$pattern] = str_replace(array_keys($convert), array_values($convert), $pattern); 1620 | 1621 | return $patterns[$pattern]; 1622 | } 1623 | 1624 | return null; 1625 | } 1626 | 1627 | /** 1628 | * @param int $styleIdx 1629 | * 1630 | * @return string|null 1631 | */ 1632 | public function getDateFormatPattern(int $styleIdx): ?string 1633 | { 1634 | $pattern = $this->getFormatPattern($styleIdx); 1635 | if ($pattern) { 1636 | return $this->_convertDateFormatPattern($pattern); 1637 | } 1638 | 1639 | return null; 1640 | } 1641 | } 1642 | 1643 | // EOF -------------------------------------------------------------------------------- /src/FastExcelReader/Sheet.php: -------------------------------------------------------------------------------- 1 | |null 72 | */ 73 | protected ?array $validations = null; 74 | 75 | protected ?array $conditionals = null; 76 | 77 | protected ?array $rowHeights = null; 78 | 79 | protected ?array $colWidths = null; 80 | 81 | protected float $defaultRowHeight = 15.0; 82 | 83 | protected ?array $tabProperties = null; 84 | 85 | /** 86 | * @param string $sheetName 87 | * @param string $sheetId 88 | * @param string $file 89 | * @param string $path 90 | * @param $excel 91 | */ 92 | public function __construct(string $sheetName, string $sheetId, string $file, string $path, $excel) 93 | { 94 | $this->excel = $excel; 95 | $this->name = $sheetName; 96 | $this->sheetId = $sheetId; 97 | $this->zipFilename = $file; 98 | $this->pathInZip = $path; 99 | 100 | $this->area = [ 101 | 'row_min' => 1, 102 | 'col_min' => 1, 103 | 'row_max' => Helper::EXCEL_2007_MAX_ROW, 104 | 'col_max' => Helper::EXCEL_2007_MAX_COL, 105 | 'first_row_keys' => false, 106 | 'col_keys' => [], 107 | ]; 108 | } 109 | 110 | /** 111 | * @param $cell 112 | * @param array|null $additionalData 113 | * 114 | * @return mixed 115 | */ 116 | protected function _cellValue($cell, ?array &$additionalData = []) 117 | { 118 | // Determine data type and style index 119 | $attributeT = $dataType = (string)$cell->getAttribute('t'); 120 | $styleIdx = (int)$cell->getAttribute('s'); 121 | $address = $cell->attributes['r']->value; 122 | 123 | $cellValue = $formula = null; 124 | if ($cell->hasChildNodes()) { 125 | foreach($cell->childNodes as $node) { 126 | if ($node->nodeName === 'v') { 127 | $cellValue = $node->nodeValue; 128 | break; 129 | } 130 | } 131 | foreach($cell->childNodes as $node) { 132 | if ($node->nodeName === 'f') { 133 | $formula = $this->_cellFormula($node, $address); 134 | break; 135 | } 136 | } 137 | if ($cellValue === null) { 138 | $cellValue = $formula; 139 | } 140 | } 141 | elseif ($styleIdx) { 142 | $cellValue = ''; 143 | } 144 | 145 | // Value is a shared string 146 | if ($dataType === 's') { 147 | if (is_numeric($cellValue) && null !== ($str = $this->excel->sharedString((int)$cellValue))) { 148 | $cellValue = $str; 149 | } 150 | } 151 | $formatCode = null; 152 | if (($cellValue !== null) && ($cellValue !== '') && ($dataType === '' || $dataType === 'n' || $dataType === 's')) { // number or date as string 153 | if ($styleIdx > 0 && ($style = $this->excel->styleByIdx($styleIdx))) { 154 | if (isset($style['formatType'])) { 155 | $dataType = $style['formatType']; 156 | } 157 | if (isset($style['format'])) { 158 | $formatCode = $style['format']; 159 | } 160 | } 161 | } 162 | 163 | $originalValue = $cellValue; 164 | $value = ''; 165 | 166 | switch ( $dataType ) { 167 | case 'b': 168 | // Value is boolean 169 | $value = (bool)$cellValue; 170 | $dataType = 'bool'; 171 | break; 172 | 173 | case 'inlineStr': 174 | // Value is rich text inline 175 | $value = $cell->textContent; 176 | if ($value && $originalValue === null) { 177 | $originalValue = $value; 178 | } 179 | $dataType = 'string'; 180 | break; 181 | 182 | case 'e': 183 | // Value is an error message 184 | $value = (string)$cellValue; 185 | $dataType = 'error'; 186 | break; 187 | 188 | case 'd': 189 | case 'date': 190 | if (($cellValue === null) || (trim($cellValue) === '')) { 191 | $dataType = 'date'; 192 | } 193 | elseif ($this->excel->getDateFormatter() === false) { 194 | if ($attributeT !== 's' && is_numeric($cellValue)) { 195 | $value = $this->excel->timestamp($cellValue); 196 | } 197 | else { 198 | $value = $originalValue; 199 | } 200 | } 201 | elseif (($timestamp = $this->excel->timestamp($cellValue))) { 202 | // Value is a date and non-empty 203 | $value = $this->excel->formatDate($timestamp, null, $styleIdx); 204 | $dataType = 'date'; 205 | } 206 | else { 207 | // Value is not a date, load its original value 208 | $value = (string)$cellValue; 209 | //$dataType = 'string'; 210 | } 211 | $dataType = 'date'; 212 | break; 213 | 214 | default: 215 | if ($dataType === 'n' || $dataType === 'number') { 216 | $dataType = 'number'; 217 | } 218 | elseif ($dataType === 's' || $dataType === 'string') { 219 | $dataType = 'string'; 220 | } 221 | if ($cellValue === null) { 222 | $value = null; 223 | } 224 | else { 225 | // Value is a string 226 | $value = (string)$cellValue; 227 | 228 | // Check for numeric values 229 | if ($dataType !== 'string' && is_numeric($value)) { 230 | if (false !== $castedValue = filter_var($value, FILTER_VALIDATE_INT)) { 231 | $value = $castedValue; 232 | $dataType = 'number'; 233 | } 234 | elseif (strlen($value) > 2 && !($value[0] === '0' && $value[1] !== '.') && false !== $castedValue = filter_var($value, FILTER_VALIDATE_FLOAT)) { 235 | $value = $castedValue; 236 | $dataType = 'number'; 237 | } 238 | /* 239 | if ($formatCode && preg_match('/\.(0+)$/', $formatCode, $m)) { 240 | $value = round($value, strlen($m[1])); 241 | } 242 | */ 243 | } 244 | } 245 | } 246 | if ($value && $dataType === 'string') { 247 | $value = Helper::unescapeString($value); 248 | } 249 | $additionalData = ['v' => $value, 's' => $styleIdx, 'f' => $formula, 't' => $dataType, 'o' => $originalValue]; 250 | 251 | return $value; 252 | } 253 | 254 | /** 255 | * @param $node 256 | * @param string $address 257 | * 258 | * @return string 259 | */ 260 | protected function _cellFormula($node, string $address): string 261 | { 262 | $shared = (string)$node->getAttribute('t') === 'shared'; 263 | $si = (string)$node->getAttribute('si'); 264 | $formula = $node->nodeValue; 265 | if ($formula) { 266 | if ($formula[0] !== '=') { 267 | $formula = '=' . $formula; 268 | } 269 | if ($shared && $si > '') { 270 | $ref = (string)$node->getAttribute('ref'); 271 | if ($ref && preg_match('/^([a-z]+)\$?(\d+)(:\$?([a-z]+)\$?(\d+))?$/i', $ref, $m)) { 272 | $ref = ['col_num' => Helper::colNumber($m[1]), 'row_num' => (int)$m[2]]; 273 | } 274 | else { 275 | $ref = ['col_num' => 0, 'row_num' => 0]; 276 | } 277 | $this->sharedFormulas[$si] = ['ref' => $ref, 'formula' => $formula]; 278 | } 279 | } 280 | elseif ($shared && $si > '' && isset($this->sharedFormulas[$si])) { 281 | $formula = $this->sharedFormulas[$si]['formula']; 282 | $ref = $this->sharedFormulas[$si]['ref']; 283 | if (preg_match('/^\$?([a-z]+)\$?(\d+)(:\$?([a-z]+)\$?(\d+))?$/i', $address, $m)) { 284 | $addressNum = [ 285 | 'col_num' => Helper::colNumber($m[1]), 286 | 'row_num' => (int)$m[2], 287 | ]; 288 | $formula = preg_replace_callback('/([A-Z]+)([0-9]+)/', function ($matches) use ($addressNum, $ref) { 289 | $colNum = Helper::colNumber($matches[1]); 290 | $rowNum = (int)$matches[2]; 291 | $colOffset = $addressNum['col_num'] - $ref['col_num']; 292 | $rowOffset = $addressNum['row_num'] - $ref['row_num']; 293 | 294 | return Helper::colLetter($colNum + $colOffset) . ($rowNum + $rowOffset); 295 | }, $formula); 296 | } 297 | } 298 | 299 | return $formula; 300 | } 301 | 302 | /** 303 | * @return string 304 | */ 305 | public function id(): string 306 | { 307 | return $this->sheetId; 308 | } 309 | 310 | /** 311 | * @return string 312 | */ 313 | public function name(): string 314 | { 315 | return $this->name; 316 | } 317 | 318 | /** 319 | * @return string 320 | */ 321 | public function path(): string 322 | { 323 | return $this->pathInZip; 324 | } 325 | 326 | /** 327 | * Case-insensitive name checking 328 | * 329 | * @param string $name 330 | * 331 | * @return bool 332 | */ 333 | public function isName(string $name): bool 334 | { 335 | return strcasecmp($this->name, $name) === 0; 336 | } 337 | 338 | /** 339 | * @return bool 340 | */ 341 | public function isActive(): bool 342 | { 343 | if ($this->active === null) { 344 | $this->_readHeader(); 345 | 346 | if ($this->active === null) { 347 | $this->active = false; 348 | } 349 | } 350 | 351 | return $this->active; 352 | } 353 | 354 | /** 355 | * @param string $state 356 | * 357 | * @return $this 358 | */ 359 | public function setState(string $state): Sheet 360 | { 361 | $this->state = $state; 362 | 363 | return $this; 364 | } 365 | 366 | /** 367 | * @return string 368 | */ 369 | public function state(): string 370 | { 371 | return $this->state; 372 | } 373 | 374 | /** 375 | * @return bool 376 | */ 377 | public function isVisible(): bool 378 | { 379 | return !$this->state || $this->state === 'visible'; 380 | } 381 | 382 | /** 383 | * @return bool 384 | */ 385 | public function isHidden(): bool 386 | { 387 | return $this->state === 'hidden' || $this->state === 'veryHidden'; 388 | } 389 | 390 | /** 391 | * @param string|null $file 392 | * 393 | * @return Reader 394 | */ 395 | protected function getReader(?string $file = null): InterfaceXmlReader 396 | { 397 | if (empty($this->xmlReader)) { 398 | if (!$file) { 399 | $file = $this->zipFilename; 400 | } 401 | $this->xmlReader = Excel::createReader($file); 402 | } 403 | 404 | return $this->xmlReader; 405 | } 406 | 407 | /** 408 | * @param string $pathInZip 409 | * 410 | * @return InterfaceXmlReader|Reader 411 | */ 412 | protected function xmlReaderOpenZip(string $pathInZip) 413 | { 414 | $xmlReader = $this->getReader(); 415 | $xmlReader->openZip($pathInZip); 416 | 417 | return $xmlReader; 418 | } 419 | 420 | /** 421 | * @param $xmlReader 422 | * 423 | * @return void 424 | */ 425 | protected function xmlReaderClose(&$xmlReader) 426 | { 427 | $xmlReader->close(); 428 | $xmlReader = null; 429 | } 430 | 431 | protected function _readHeader() 432 | { 433 | if (!isset($this->dimension['range'])) { 434 | $this->dimension = [ 435 | 'range' => '', 436 | ]; 437 | //$xmlReader = $this->getReader(); 438 | //$xmlReader->openZip($this->pathInZip); 439 | $xmlReader = $this->xmlReaderOpenZip($this->pathInZip); 440 | while ($xmlReader->read()) { 441 | if ($xmlReader->nodeType === \XMLReader::ELEMENT && $xmlReader->name === 'dimension') { 442 | $range = (string)$xmlReader->getAttribute('ref'); 443 | if ($range) { 444 | $this->dimension = Helper::rangeArray($range); 445 | $this->dimension['range'] = $range; 446 | } 447 | } 448 | if ($xmlReader->nodeType === \XMLReader::ELEMENT && $xmlReader->name === 'sheetView') { 449 | $this->active = (int)$xmlReader->getAttribute('tabSelected'); 450 | } 451 | if ($xmlReader->nodeType === \XMLReader::ELEMENT && $xmlReader->name === 'col') { 452 | if ($xmlReader->hasAttributes) { 453 | $colAttributes = []; 454 | while ($xmlReader->moveToNextAttribute()) { 455 | $colAttributes[$xmlReader->name] = $xmlReader->value; 456 | } 457 | $this->cols[] = $colAttributes; 458 | $xmlReader->moveToElement(); 459 | } 460 | 461 | } 462 | if ($xmlReader->name === 'sheetData') { 463 | break; 464 | } 465 | } 466 | //$xmlReader->close(); 467 | $this->xmlReaderClose($xmlReader);; 468 | } 469 | } 470 | 471 | protected function _readBottom() 472 | { 473 | if ($this->mergedCells === null) { 474 | //$xmlReader = $this->getReader(); 475 | //$xmlReader->openZip($this->pathInZip); 476 | $xmlReader = $this->xmlReaderOpenZip($this->pathInZip); 477 | while ($xmlReader->read()) { 478 | if ($xmlReader->nodeType === \XMLReader::END_ELEMENT && $xmlReader->name === 'sheetData') { 479 | break; 480 | } 481 | } 482 | $this->mergedCells = []; 483 | while ($xmlReader->read()) { 484 | if ($xmlReader->nodeType === \XMLReader::ELEMENT && $xmlReader->name === 'mergeCell') { 485 | $ref = (string)$xmlReader->getAttribute('ref'); 486 | if ($ref) { 487 | $arr = Helper::rangeArray($ref); 488 | $this->mergedCells[$arr['min_cell']] = $ref; 489 | } 490 | } 491 | } 492 | //$xmlReader->close(); 493 | $this->xmlReaderClose($xmlReader);; 494 | } 495 | } 496 | 497 | /** 498 | * @return string|null 499 | */ 500 | public function dimension(): ?string 501 | { 502 | if (!isset($this->dimension['range'])) { 503 | $this->_readHeader(); 504 | } 505 | 506 | return $this->dimension['range']; 507 | } 508 | 509 | /** 510 | * @return array 511 | */ 512 | public function dimensionArray(): array 513 | { 514 | if (!isset($this->dimension['range'])) { 515 | $this->_readHeader(); 516 | } 517 | 518 | return $this->dimension; 519 | } 520 | 521 | /** 522 | * Count rows by dimension value 523 | * 524 | * @param string|null $range 525 | * 526 | * @return int 527 | */ 528 | public function countRows(?string $range = null): int 529 | { 530 | // A1:C3 || A1 531 | $areaRange = $range ?: $this->dimension(); 532 | if ($areaRange && preg_match('/^([A-Za-z]+)(\d+)(:([A-Za-z]+)(\d+))?$/', $areaRange, $matches)) { 533 | return count($matches) === 6 ? ((int)$matches[5] - (int)$matches[2] + 1) : 1; 534 | } 535 | 536 | return 0; 537 | } 538 | 539 | /** 540 | * Count columns by dimension value 541 | * 542 | * @param string|null $range 543 | * 544 | * @return int 545 | */ 546 | public function countColumns(?string $range = null): int 547 | { 548 | $areaRange = $range ?: $this->dimension(); 549 | if ($areaRange && preg_match('/^([A-Za-z]+)(\d+)(:([A-Za-z]+)(\d+))?$/', $areaRange, $matches)) { 550 | return !empty($matches[4]) ? (Excel::colNum($matches[4]) - Excel::colNum($matches[1]) + 1) : 1; 551 | } 552 | 553 | return 0; 554 | } 555 | 556 | /** 557 | * Min row number from dimension value 558 | * 559 | * @param string|null $range 560 | * 561 | * @return int 562 | */ 563 | public function minRow(?string $range = null): int 564 | { 565 | $areaRange = $range ?: $this->dimension(); 566 | if ($areaRange && preg_match('/^([A-Za-z]+)(\d+)(:([A-Za-z]+)(\d+))?$/', $areaRange, $matches)) { 567 | return (int)$matches[2]; 568 | } 569 | 570 | return 0; 571 | } 572 | 573 | /** 574 | * Max row number from dimension value 575 | * 576 | * @param string|null $range 577 | * 578 | * @return int 579 | */ 580 | public function maxRow(?string $range = null): int 581 | { 582 | $areaRange = $range ?: $this->dimension(); 583 | if ($areaRange && preg_match('/^([A-Za-z]+)(\d+)(:([A-Za-z]+)(\d+))?$/', $areaRange, $matches)) { 584 | return count($matches) === 6 ? (int)$matches[5] : (int)$matches[2]; 585 | } 586 | 587 | return 0; 588 | } 589 | 590 | /** 591 | * Min column from dimension value 592 | * 593 | * @param string|null $range 594 | * 595 | * @return string 596 | */ 597 | public function minColumn(?string $range = null): string 598 | { 599 | $areaRange = $range ?: $this->dimension(); 600 | if ($areaRange && preg_match('/^([A-Za-z]+)(\d+)(:([A-Za-z]+)(\d+))?$/', $areaRange, $matches)) { 601 | return $matches[1] ?? ''; 602 | } 603 | 604 | return ''; 605 | } 606 | 607 | /** 608 | * Max column from dimension value 609 | * 610 | * @param string|null $range 611 | * 612 | * @return string 613 | */ 614 | public function maxColumn(?string $range = null): string 615 | { 616 | $areaRange = $range ?: $this->dimension(); 617 | if ($areaRange && preg_match('/^([A-Za-z]+)(\d+)(:([A-Za-z]+)(\d+))?$/', $areaRange, $matches)) { 618 | return $matches[4] ?? $this->minColumn($range); 619 | } 620 | 621 | return $this->minColumn($range); 622 | } 623 | 624 | /** 625 | * Count columns by dimension value, alias of countColumns() 626 | * 627 | * @param string|null $range 628 | * 629 | * @return int 630 | */ 631 | public function countCols(?string $range = null): int 632 | { 633 | return $this->countColumns($range); 634 | } 635 | 636 | /** 637 | * @param bool $countColumns 638 | * @param bool $countRows 639 | * @param int $blockSize 640 | * 641 | * @return array 642 | */ 643 | public function countActualDimension(bool $countColumns = true, bool $countRows = true, int $blockSize = 4096): array 644 | { 645 | $block1 = $block2 = null; 646 | $fp = fopen('zip://' . $this->zipFilename . '#' . $this->pathInZip, 'r'); 647 | $minRow = $maxRow = 0; 648 | $columns = []; 649 | $cntBlocks = 0; 650 | while (!feof($fp)) { 651 | $str = fread($fp, $blockSize); 652 | if ($str === false) { 653 | break; 654 | } 655 | 656 | if ($block1 === null) { 657 | $block1 = $str; 658 | $block2 = (string)fread($fp, $blockSize); 659 | } 660 | else { 661 | $block2 = $str; 662 | } 663 | 664 | $txt = $block1 . $block2; 665 | if (!$txt) { 666 | break; 667 | } 668 | 669 | if ($countRows && !$this->actualRows) { 670 | if (preg_match_all('/]+)/', $txt, $matches)) { 671 | if ($minRow === 0) { 672 | $attr = reset($matches[1]); 673 | if ($attr && preg_match('/r\s*=\s*"?(\d+)"?/', $attr, $m)) { 674 | $minRow = (int)$m[1]; 675 | } 676 | } 677 | $attr = end($matches[1]); 678 | if ($attr && preg_match('/r\s*=\s*"?(\d+)"?/', $attr, $m)) { 679 | $rowNum = (int)$m[1]; 680 | if ($maxRow === 0 || $rowNum >= $maxRow) { 681 | $maxRow = $rowNum; 682 | } 683 | } 684 | } 685 | } 686 | 687 | if ($countColumns && !$this->actualCols) { 688 | if (preg_match_all('/]+)/', $txt, $matches)) { 689 | foreach ($matches[1] as $attr) { 690 | if (preg_match('/r\s*=\s*"?([A-Z]+)(\d+)"?/', $attr, $m) && !empty($m[1]) && !isset($columns[$m[1]])) { 691 | $columns[$m[1]] = \avadim\FastExcelHelper\Helper::colNumber($m[1]); 692 | } 693 | } 694 | } 695 | } 696 | 697 | $block1 = $block2; 698 | } 699 | fclose($fp); 700 | 701 | if ($countColumns && !$this->actualCols) { 702 | asort($columns); 703 | $this->actualCols['min'] = array_key_first($columns); 704 | $this->actualCols['max'] = array_key_last($columns); 705 | $this->actualCols['count'] = $columns[$this->actualCols['max']] - $columns[$this->actualCols['min']] + 1; 706 | } 707 | if ($countRows && !$this->actualRows) { 708 | $this->actualRows['min'] = $minRow; 709 | $this->actualRows['max'] = $maxRow; 710 | $this->actualRows['count'] = $maxRow - $minRow + 1; 711 | } 712 | 713 | return [ 714 | 'rows' => $this->actualRows, 715 | 'cols' => $this->actualCols, 716 | ]; 717 | } 718 | 719 | /** 720 | * Returns the actual number of rows from the sheet data area 721 | * 722 | * @return int 723 | */ 724 | public function countActualRows(): int 725 | { 726 | if (!$this->actualRows) { 727 | $this->countActualDimension(false); 728 | } 729 | 730 | return $this->actualRows['count'] ?? 0; 731 | } 732 | 733 | /** 734 | * @return int 735 | */ 736 | public function minActualRow(): int 737 | { 738 | if (!$this->actualRows) { 739 | $this->countActualDimension(false); 740 | } 741 | 742 | return $this->actualRows['min'] ?? 0; 743 | } 744 | 745 | /** 746 | * @return int 747 | */ 748 | public function maxActualRow(): int 749 | { 750 | if (!$this->actualRows) { 751 | $this->countActualDimension(false); 752 | } 753 | 754 | return $this->actualRows['max'] ?? 0; 755 | } 756 | 757 | /** 758 | * Returns the actual number of columns from the sheet data area 759 | * 760 | * @return int 761 | */ 762 | public function countActualColumns(): int 763 | { 764 | if (!$this->actualCols) { 765 | $this->countActualDimension(true, false); 766 | } 767 | 768 | return $this->actualCols['count'] ?? 0; 769 | } 770 | 771 | /** 772 | * @return string 773 | */ 774 | public function minActualColumn(): string 775 | { 776 | if (!$this->actualCols) { 777 | $this->countActualDimension(true, false); 778 | } 779 | 780 | return $this->actualCols['min'] ?? ''; 781 | } 782 | 783 | /** 784 | * @return string 785 | */ 786 | public function maxActualColumn(): string 787 | { 788 | if (!$this->actualCols) { 789 | $this->countActualDimension(true, false); 790 | } 791 | 792 | return $this->actualCols['max'] ?? ''; 793 | } 794 | 795 | /** 796 | * @return string 797 | */ 798 | public function actualDimension(): string 799 | { 800 | $minCell = $maxCell = ''; 801 | $dim = $this->countActualDimension(); 802 | if (isset($dim['rows']['min'], $dim['cols']['min'])) { 803 | $minCell = $dim['cols']['min'] . $dim['rows']['min']; 804 | } 805 | if (isset($dim['rows']['max'], $dim['cols']['max'])) { 806 | $maxCell = $dim['cols']['max'] . $dim['rows']['max']; 807 | } 808 | if ($minCell && !$maxCell) { 809 | return $minCell; 810 | } 811 | if (!$minCell && $maxCell) { 812 | return $maxCell; 813 | } 814 | 815 | return $minCell . ':' . $maxCell; 816 | } 817 | 818 | /** 819 | * @return array 820 | */ 821 | public function getColAttributes(): array 822 | { 823 | $result = []; 824 | if ($this->cols) { 825 | foreach ($this->cols as $colAttributes) { 826 | if (isset($colAttributes['min'])) { 827 | $col = Helper::colLetter($colAttributes['min']); 828 | $result[$col] = $colAttributes; 829 | } 830 | else { 831 | $result[] = $colAttributes; 832 | } 833 | } 834 | } 835 | 836 | return $result; 837 | } 838 | 839 | /** 840 | * @param $dateFormat 841 | * 842 | * @return $this 843 | */ 844 | public function setDateFormat($dateFormat): Sheet 845 | { 846 | $this->excel->setDateFormat($dateFormat); 847 | 848 | return $this; 849 | } 850 | 851 | protected static function _areaRange(string $areaRange): array 852 | { 853 | $area = []; 854 | $area['col_keys'] = []; 855 | if (preg_match('/^\$?([A-Za-z]+)\$?(\d+)(:\$?([A-Za-z]+)\$?(\d+))?$/', $areaRange, $matches)) { 856 | $area['col_min'] = Helper::colNumber($matches[1]); 857 | $area['row_min'] = (int)$matches[2]; 858 | if (empty($matches[3])) { 859 | $area['col_max'] = Helper::EXCEL_2007_MAX_COL; 860 | $area['row_max'] = Helper::EXCEL_2007_MAX_ROW; 861 | } 862 | else { 863 | $area['col_max'] = Helper::colNumber($matches[4]); 864 | $area['row_max'] = (int)$matches[5]; 865 | for ($col = $area['col_min']; $col <= $area['col_max']; $col++) { 866 | $area['col_keys'][Helper::colLetter($col)] = null; 867 | } 868 | } 869 | } 870 | elseif (preg_match('/^([A-Za-z]+)(:([A-Za-z]+))?$/', $areaRange, $matches)) { 871 | $area['col_min'] = Helper::colNumber($matches[1]); 872 | if (empty($matches[2])) { 873 | $area['col_max'] = Helper::EXCEL_2007_MAX_COL; 874 | } 875 | else { 876 | $area['col_max'] = Helper::colNumber($matches[3]); 877 | for ($col = $area['col_min']; $col <= $area['col_max']; $col++) { 878 | $area['col_keys'][Helper::colLetter($col)] = null; 879 | } 880 | } 881 | $area['row_min'] = 1; 882 | $area['row_max'] = Helper::EXCEL_2007_MAX_ROW; 883 | } 884 | if (isset($area['col_min'], $area['col_max']) && ($area['col_min'] < 0 || $area['col_max'] < 0)) { 885 | return []; 886 | } 887 | 888 | return $area; 889 | } 890 | 891 | /** 892 | * setReadArea('C3:AZ28') - set top left and right bottom of read area 893 | * setReadArea('C3') - set top left only 894 | * 895 | * @param string $areaRange 896 | * @param bool|null $firstRowKeys 897 | * 898 | * @return $this 899 | */ 900 | public function setReadArea(string $areaRange, ?bool $firstRowKeys = false): Sheet 901 | { 902 | if (preg_match('/^\w+$/', $areaRange)) { 903 | foreach ($this->excel->getDefinedNames() as $name => $range) { 904 | if ($name === $areaRange && strpos($range, $this->name . '!') === 0) { 905 | [$sheetName, $definedRange] = explode('!', $range); 906 | $areaRange = $definedRange; 907 | break; 908 | } 909 | } 910 | } 911 | $area = self::_areaRange($areaRange); 912 | if ($area && isset($area['row_max'])) { 913 | $this->area = $area; 914 | $this->area['first_row_keys'] = $firstRowKeys; 915 | 916 | return $this; 917 | } 918 | throw new Exception('Wrong address or range "' . $areaRange . '"'); 919 | } 920 | 921 | /** 922 | * setReadArea('C:AZ') - set left and right columns of read area 923 | * setReadArea('C') - set left column only 924 | * 925 | * @param string $columnsRange 926 | * @param bool|null $firstRowKeys 927 | * 928 | * @return $this 929 | */ 930 | public function setReadAreaColumns(string $columnsRange, ?bool $firstRowKeys = false): Sheet 931 | { 932 | $area = self::_areaRange($columnsRange); 933 | if ($area) { 934 | $this->area = $area; 935 | $this->area['first_row_keys'] = $firstRowKeys; 936 | 937 | return $this; 938 | } 939 | throw new Exception('Wrong address or range "' . $columnsRange . '"'); 940 | } 941 | 942 | /** 943 | * Returns cell values as a two-dimensional array 944 | * [1 => ['A' => _value_A1_], ['B' => _value_B1_]], 945 | * [2 => ['A' => _value_A2_], ['B' => _value_B2_]] 946 | * 947 | * readRows() 948 | * readRows(true) 949 | * readRows(false, Excel::KEYS_ZERO_BASED) 950 | * readRows(Excel::KEYS_ZERO_BASED | Excel::KEYS_RELATIVE) 951 | * 952 | * @param array|bool|int|null $columnKeys 953 | * @param int|null $resultMode 954 | * @param bool|null $styleIdxInclude 955 | * 956 | * @return array 957 | */ 958 | public function readRows($columnKeys = [], ?int $resultMode = null, ?bool $styleIdxInclude = null): array 959 | { 960 | $data = []; 961 | if (is_int($columnKeys) && !is_int($resultMode)) { 962 | $resultMode = $columnKeys; 963 | $columnKeys = []; 964 | } 965 | $this->readCallback(static function($row, $col, $val) use (&$columnKeys, &$data) { 966 | if (isset($columnKeys[$col])) { 967 | $data[$row][$columnKeys[$col]] = $val; 968 | } 969 | else { 970 | $data[$row][$col] = $val; 971 | } 972 | }, $columnKeys, $resultMode, $styleIdxInclude); 973 | 974 | if ($data && ($resultMode & Excel::KEYS_SWAP)) { 975 | $newData = []; 976 | $rowKeys = array_keys($data); 977 | $len = count($rowKeys); 978 | foreach (array_keys(reset($data)) as $colKey) { 979 | $rowValues = array_column($data, $colKey); 980 | if ($len - count($rowValues)) { 981 | $rowValues = array_pad($rowValues, $len, null); 982 | } 983 | $newData[$colKey] = array_combine($rowKeys, $rowValues); 984 | } 985 | return $newData; 986 | } 987 | 988 | return $data; 989 | } 990 | 991 | /** 992 | * Returns values, styles and other info of cells as array 993 | * 994 | * [ 995 | * 'v' => _value_, 996 | * 's' => _styles_, 997 | * 'f' => _formula_, 998 | * 't' => _type_, 999 | * 'o' => '_original_value_ 1000 | * ] 1001 | * 1002 | * @param array|bool|int|null $columnKeys 1003 | * @param int|null $resultMode 1004 | * 1005 | * @return array 1006 | */ 1007 | public function readRowsWithStyles($columnKeys = [], ?int $resultMode = null): array 1008 | { 1009 | $data = $this->readRows($columnKeys, $resultMode, true); 1010 | 1011 | foreach ($data as $row => $rowData) { 1012 | foreach ($rowData as $col => $cellData) { 1013 | if (isset($cellData['s'])) { 1014 | $data[$row][$col]['s'] = $this->excel->getCompleteStyleByIdx($cellData['s']); 1015 | } 1016 | } 1017 | } 1018 | 1019 | return $data; 1020 | } 1021 | 1022 | /** 1023 | * @return int 1024 | */ 1025 | public function firstRow(): int 1026 | { 1027 | if (!isset($this->area['first_row'])) { 1028 | $this->readFirstRow(); 1029 | } 1030 | 1031 | return $this->area['first_row']; 1032 | } 1033 | 1034 | /** 1035 | * @return string 1036 | */ 1037 | public function firstCol(): string 1038 | { 1039 | if (!isset($this->area['first_col'])) { 1040 | $this->readFirstRow(); 1041 | } 1042 | 1043 | return $this->area['first_col']; 1044 | } 1045 | 1046 | /** 1047 | * Returns values of cells of 1st row as array 1048 | * 1049 | * @param array|bool|int|null $columnKeys 1050 | * @param bool|null $styleIdxInclude 1051 | * 1052 | * @return array 1053 | */ 1054 | public function readFirstRow($columnKeys = [], ?bool $styleIdxInclude = null): array 1055 | { 1056 | $rowData = []; 1057 | $rowNum = -1; 1058 | $this->readCallback(static function($row, $col, $val) use (&$columnKeys, &$rowData, &$rowNum) { 1059 | if ($rowNum === -1) { 1060 | $rowNum = $row; 1061 | } 1062 | elseif ($rowNum !== $row) { 1063 | return true; 1064 | } 1065 | if (isset($columnKeys[$col])) { 1066 | $col = $rowData[$columnKeys[$col]]; 1067 | } 1068 | $rowData[$col] = $val; 1069 | 1070 | return null; 1071 | }, $columnKeys, null, $styleIdxInclude); 1072 | 1073 | return $rowData; 1074 | } 1075 | 1076 | /** 1077 | * @param array|bool|int|null $columnKeys 1078 | * 1079 | * @return array 1080 | */ 1081 | public function readFirstRowWithStyles($columnKeys = []): array 1082 | { 1083 | $rowData = $this->readFirstRow($columnKeys, true); 1084 | foreach ($rowData as $col => $cellData) { 1085 | if (isset($cellData['s'])) { 1086 | $rowData[$col]['s'] = $this->excel->getCompleteStyleByIdx($cellData['s']); 1087 | } 1088 | } 1089 | 1090 | return $rowData; 1091 | } 1092 | 1093 | /** 1094 | * Returns values and styles of cells of 1st row as array 1095 | * 1096 | * @param bool|null $styleIdxInclude 1097 | * 1098 | * @return array 1099 | */ 1100 | public function readFirstRowCells(?bool $styleIdxInclude = null): array 1101 | { 1102 | $rowData = []; 1103 | $rowNum = -1; 1104 | $this->readCallback(static function($row, $col, $val) use (&$columnKeys, &$rowData, &$rowNum) { 1105 | if ($rowNum === -1) { 1106 | $rowNum = $row; 1107 | } 1108 | elseif ($rowNum !== $row) { 1109 | return true; 1110 | } 1111 | $rowData[$col . $row] = $val; 1112 | 1113 | return null; 1114 | }, $columnKeys, null, $styleIdxInclude); 1115 | 1116 | return $rowData; 1117 | } 1118 | 1119 | /** 1120 | * Returns cell values as a two-dimensional array from default sheet [col][row] 1121 | * ['A' => [1 => _value_A1_], [2 => _value_A2_]], 1122 | * ['B' => [1 => _value_B1_], [2 => _value_B2_]] 1123 | * 1124 | * @param array|bool|int|null $columnKeys 1125 | * @param int|null $resultMode 1126 | * @param bool|null $styleIdxInclude 1127 | * 1128 | * @return array 1129 | */ 1130 | public function readColumns($columnKeys = null, ?int $resultMode = null, ?bool $styleIdxInclude = null): array 1131 | { 1132 | if (is_int($columnKeys) && $columnKeys > 1 && $resultMode === null) { 1133 | $resultMode = $columnKeys | Excel::KEYS_RELATIVE; 1134 | $columnKeys = $columnKeys & Excel::KEYS_FIRST_ROW; 1135 | } 1136 | else { 1137 | $resultMode = $resultMode | Excel::KEYS_RELATIVE; 1138 | } 1139 | 1140 | return $this->readRows($columnKeys, $resultMode | Excel::KEYS_SWAP); 1141 | } 1142 | 1143 | /** 1144 | * Returns values and styles of cells as array ['v' => _value_, 's' => _styles_] 1145 | * 1146 | * @param array|bool|int|null $columnKeys 1147 | * @param int|null $resultMode 1148 | * 1149 | * @return array 1150 | */ 1151 | public function readColumnsWithStyles($columnKeys = null, ?int $resultMode = null): array 1152 | { 1153 | $data = $this->readColumns($columnKeys, $resultMode, true); 1154 | 1155 | foreach ($data as $col => $colData) { 1156 | foreach ($colData as $row => $cellData) { 1157 | if (isset($cellData['s'])) { 1158 | $data[$col][$row]['s'] = $this->excel->getCompleteStyleByIdx($cellData['s']); 1159 | } 1160 | } 1161 | } 1162 | 1163 | return $data; 1164 | } 1165 | 1166 | /** 1167 | * Returns values and styles of cells as array 1168 | * 1169 | * @param bool|null $styleIdxInclude 1170 | * 1171 | * @return array 1172 | */ 1173 | public function readCells(?bool $styleIdxInclude = null): array 1174 | { 1175 | $data = []; 1176 | $this->readCallback(static function($row, $col, $val) use (&$data) { 1177 | $data[$col . $row] = $val; 1178 | }, [], null, $styleIdxInclude); 1179 | 1180 | return $data; 1181 | } 1182 | 1183 | /** 1184 | * Returns values and styles of cells as array: 1185 | * 'v' => _value_ 1186 | * 's' => _styles_ 1187 | * 'f' => _formula_ 1188 | * 't' => _type_ 1189 | * 'o' => _original_value_ 1190 | * 1191 | * @param $styleKey 1192 | * 1193 | * @return array 1194 | */ 1195 | public function readCellsWithStyles($styleKey = null): array 1196 | { 1197 | $data = $this->readCells(true); 1198 | foreach ($data as $cell => $cellData) { 1199 | if (isset($cellData['s'])) { 1200 | $style = $this->excel->getCompleteStyleByIdx($cellData['s']); 1201 | if ($styleKey && isset($style[$styleKey])) { 1202 | $data[$cell]['s'] = [$styleKey => $style[$styleKey]]; 1203 | } 1204 | else { 1205 | $data[$cell]['s'] = $style; 1206 | } 1207 | } 1208 | } 1209 | 1210 | return $data; 1211 | } 1212 | 1213 | /** 1214 | * Returns styles of cells as array 1215 | * 1216 | * @param bool|null $flat 1217 | * @param string|null $part 1218 | * 1219 | * @return array 1220 | */ 1221 | public function readCellStyles(?bool $flat = false, ?string $part = null): array 1222 | { 1223 | $cells = $this->readCells(true); 1224 | $result = []; 1225 | if ($part) { 1226 | $flat = false; 1227 | } 1228 | foreach ($cells as $cell => $cellData) { 1229 | if (isset($cellData['s'])) { 1230 | $style = $this->excel->getCompleteStyleByIdx($cellData['s'], $flat); 1231 | if ($cellData['t'] === 'date') { 1232 | //$style['format']['format-category'] = 'date'; 1233 | } 1234 | $result[$cell] = $part ? ($style[$part] ?? []) : $style; 1235 | } 1236 | else { 1237 | $result[$cell] = []; 1238 | } 1239 | } 1240 | 1241 | return $result; 1242 | } 1243 | 1244 | /** 1245 | * Reads cell values and passes them to a callback function 1246 | * 1247 | * @param callback $callback Callback function($row, $col, $value) 1248 | * @param array|bool|int|null $columnKeys 1249 | * @param int|null $resultMode 1250 | * @param bool|null $styleIdxInclude 1251 | */ 1252 | public function readCallback(callable $callback, $columnKeys = [], ?int $resultMode = null, ?bool $styleIdxInclude = null) 1253 | { 1254 | foreach ($this->nextRow($columnKeys, $resultMode, $styleIdxInclude) as $row => $rowData) { 1255 | if (isset($rowData['__cells'], $rowData['__row'])) { 1256 | $rowData = $rowData['__cells']; 1257 | } 1258 | foreach ($rowData as $col => $val) { 1259 | if (isset($this->area['col_keys']) && array_key_exists($col, $this->area['col_keys']) 1260 | || (!is_array($val) && $val !== null) || isset($val['v']) || isset($val['f']) || isset($val['s'])) { 1261 | $needBreak = $callback($row, $col, $val); 1262 | if (!isset($this->area['first_row'])) { 1263 | $this->area['first_row'] = $row; 1264 | $this->area['first_col'] = $col; 1265 | } 1266 | if ($needBreak) { 1267 | return; 1268 | } 1269 | } 1270 | } 1271 | } 1272 | } 1273 | 1274 | /** 1275 | * Read cell values row by row, returns either an array of values or an array of arrays 1276 | * 1277 | * nextRow(..., ...) : => [ => , => , ...] 1278 | * nextRow(..., ..., true) : => [ => ['v' => , 's' => ], => ['v' => , 's' => ], ...] 1279 | * 1280 | * @param array|bool|int|null $columnKeys 1281 | * @param int|null $resultMode 1282 | * @param bool|null $styleIdxInclude 1283 | * @param int|null $rowLimit 1284 | * 1285 | * @return \Generator|null 1286 | */ 1287 | public function nextRow($columnKeys = [], ?int $resultMode = null, ?bool $styleIdxInclude = null, ?int $rowLimit = 0): ?\Generator 1288 | { 1289 | // 1290 | // sometimes sheets doesn't contain this tag 1291 | $this->dimension(); 1292 | 1293 | if (!$columnKeys && is_int($resultMode) && ($resultMode & Excel::KEYS_FIRST_ROW)) { 1294 | $firstRowValues = $this->readFirstRow(); 1295 | $columnKeys = array_keys($firstRowValues); 1296 | } 1297 | $readArea = $this->area; 1298 | $rowTemplate = $readArea['col_keys']; 1299 | if (!empty($columnKeys) && is_array($columnKeys)) { 1300 | $firstRowKeys = is_int($resultMode) && ($resultMode & Excel::KEYS_FIRST_ROW); 1301 | $columnKeys = array_combine(array_map('strtoupper', array_keys($columnKeys)), array_values($columnKeys)); 1302 | } 1303 | elseif ($columnKeys === true) { 1304 | $firstRowKeys = true; 1305 | $columnKeys = []; 1306 | } 1307 | elseif ($resultMode & Excel::KEYS_FIRST_ROW) { 1308 | $firstRowKeys = true; 1309 | } 1310 | else { 1311 | $firstRowKeys = !empty($readArea['first_row_keys']); 1312 | } 1313 | 1314 | if ($columnKeys && ($resultMode & Excel::KEYS_FIRST_ROW)) { 1315 | foreach ($this->nextRow([], 0, null, 1) as $firstRowData) { 1316 | $columnKeys = array_merge($firstRowData, $columnKeys); 1317 | break; 1318 | } 1319 | } 1320 | $this->readRowNum = $this->countReadRows = 0; 1321 | 1322 | //$xmlReader = $this->getReader(); 1323 | //$xmlReader->openZip($this->pathInZip); 1324 | $xmlReader = $this->xmlReaderOpenZip($this->pathInZip); 1325 | 1326 | $rowData = $rowTemplate; 1327 | $rowNum = 0; 1328 | $rowOffset = $colOffset = null; 1329 | $row = -1; 1330 | $rowCnt = -1; 1331 | 1332 | if ($this->preReadFunc) { 1333 | ($this->preReadFunc)($xmlReader); 1334 | } 1335 | 1336 | if ($xmlReader->seekOpenTag('sheetData')) { 1337 | while ($xmlReader->read()) { 1338 | if ($rowLimit > 0 && $rowCnt >= $rowLimit) { 1339 | break; 1340 | } 1341 | if ($xmlReader->nodeType === \XMLReader::END_ELEMENT && $xmlReader->name === 'sheetData') { 1342 | break; 1343 | } 1344 | if ($this->readNodeFunc && isset($this->readNodeFunc[$xmlReader->name])) { 1345 | ($this->readNodeFunc[$xmlReader->name])($xmlReader->expand()); 1346 | } 1347 | 1348 | if ($xmlReader->nodeType === \XMLReader::END_ELEMENT && $xmlReader->name === 'row') { 1349 | //$this->countReadRows++; 1350 | if ($rowNum >= $readArea['row_min'] && $rowNum <= $readArea['row_max']) { 1351 | $this->readRowNum = $rowNum; 1352 | if ($rowCnt === 0 && $firstRowKeys) { 1353 | if (!$columnKeys) { 1354 | if ($styleIdxInclude) { 1355 | $columnKeys = array_combine(array_keys($rowData), array_column($rowData, 'v')); 1356 | } 1357 | else { 1358 | $columnKeys = $rowData; 1359 | } 1360 | $rowTemplate = array_fill_keys(array_keys($columnKeys), null); 1361 | } 1362 | } 1363 | else { 1364 | if ($resultMode & Excel::RESULT_MODE_ROW) { 1365 | $rowNode = $xmlReader->expand(); 1366 | $rowAttributes = []; 1367 | foreach ($rowNode->attributes as $key => $val) { 1368 | $rowAttributes[$key] = $val->value; 1369 | } 1370 | $rowData = [ 1371 | '__cells' => $rowData, 1372 | '__row' => $rowAttributes, 1373 | ]; 1374 | } 1375 | $row = $rowNum - $rowOffset; 1376 | yield $row => $rowData; 1377 | } 1378 | continue; 1379 | } 1380 | } 1381 | 1382 | if ($xmlReader->nodeType === \XMLReader::ELEMENT) { 1383 | if ($xmlReader->name === 'row') { // - tag row begins 1384 | $rowNum = (int)$xmlReader->getAttribute('r'); 1385 | 1386 | if ($rowNum > $readArea['row_max']) { 1387 | break; 1388 | } 1389 | if ($rowNum < $readArea['row_min']) { 1390 | continue; 1391 | } 1392 | $rowData = $rowTemplate; 1393 | 1394 | $rowCnt += 1; 1395 | if ($rowOffset === null) { 1396 | $rowOffset = 0; 1397 | if (is_int($resultMode) && $resultMode) { 1398 | if ($resultMode & Excel::KEYS_ROW_ZERO_BASED) { 1399 | $rowOffset = $rowNum + ($firstRowKeys ? 1 : 0); 1400 | } 1401 | elseif ($resultMode & Excel::KEYS_ROW_ONE_BASED) { 1402 | $rowOffset = $rowNum - 1 + ($firstRowKeys ? 1 : 0); 1403 | } 1404 | } 1405 | } 1406 | if ($xmlReader->isEmptyElement && ($resultMode & Excel::RESULT_MODE_ROW)) { 1407 | $rowNode = $xmlReader->expand(); 1408 | $rowAttributes = []; 1409 | foreach ($rowNode->attributes as $key => $val) { 1410 | $rowAttributes[$key] = $val->value; 1411 | } 1412 | $rowData = [ 1413 | '__cells' => $rowData, 1414 | '__row' => $rowAttributes, 1415 | ]; 1416 | $row = $rowNum - $rowOffset; 1417 | yield $row => $rowData; 1418 | } 1419 | } // - tag row end 1420 | 1421 | elseif ($xmlReader->name === 'c') { // - tag cell begins 1422 | $addr = $xmlReader->getAttribute('r'); 1423 | if ($addr && preg_match('/^([A-Za-z]+)(\d+)$/', $addr, $m)) { 1424 | // 1425 | if ($m[2] < $readArea['row_min'] || $m[2] > $readArea['row_max']) { 1426 | continue; 1427 | } 1428 | $colLetter = $m[1]; 1429 | $colNum = Excel::colNum($colLetter); 1430 | 1431 | if ($colNum >= $readArea['col_min'] && $colNum <= $readArea['col_max']) { 1432 | if ($colOffset === null) { 1433 | $colOffset = $colNum - 1; 1434 | if (is_int($resultMode) && ($resultMode & Excel::KEYS_COL_ZERO_BASED)) { 1435 | $colOffset += 1; 1436 | } 1437 | } 1438 | if ($resultMode) { 1439 | if (!($resultMode & (Excel::KEYS_COL_ZERO_BASED | Excel::KEYS_COL_ONE_BASED))) { 1440 | $col = $colLetter; 1441 | } 1442 | else { 1443 | $col = $colNum - $colOffset; 1444 | } 1445 | } 1446 | else { 1447 | $col = $colLetter; 1448 | } 1449 | $cell = $xmlReader->expand(); 1450 | if (is_array($columnKeys) && isset($columnKeys[$colLetter])) { 1451 | $col = $columnKeys[$colLetter]; 1452 | } 1453 | ///$value = $this->_cellValue($cell, $styleIdx, $formula, $dataType, $originalValue); 1454 | $value = $this->_cellValue($cell, $additionalData); 1455 | if ($styleIdxInclude) { 1456 | $rowData[$col] = $additionalData; 1457 | } 1458 | else { 1459 | if (is_string($value) && ($resultMode & Excel::TRIM_STRINGS)) { 1460 | $value = trim($value); 1461 | } 1462 | if (!($value === '' && ($resultMode & Excel::TREAT_EMPTY_STRING_AS_EMPTY_CELL))) { 1463 | $rowData[$col] = $value; 1464 | } 1465 | } 1466 | } 1467 | } 1468 | } // - tag cell end 1469 | } 1470 | } 1471 | } 1472 | 1473 | if ($this->postReadFunc) { 1474 | ($this->postReadFunc)($xmlReader); 1475 | } 1476 | 1477 | //$xmlReader->close(); 1478 | $this->xmlReaderClose($xmlReader);; 1479 | 1480 | return null; 1481 | } 1482 | 1483 | /** 1484 | * Reset read generator 1485 | * 1486 | * @param array|bool|int|null $columnKeys 1487 | * @param int|null $resultMode 1488 | * @param bool|null $styleIdxInclude 1489 | * @param int|null $rowLimit 1490 | * 1491 | * @return \Generator|null 1492 | */ 1493 | public function reset($columnKeys = [], ?int $resultMode = null, ?bool $styleIdxInclude = null, ?int $rowLimit = 0): ?\Generator 1494 | { 1495 | $this->generator = $this->nextRow($columnKeys, $resultMode, $styleIdxInclude, $rowLimit); 1496 | $this->countReadRows = 0; 1497 | 1498 | return $this->generator; 1499 | } 1500 | 1501 | /** 1502 | * Rewind read generator, alias of reset() 1503 | * 1504 | * @param array|bool|int|null $columnKeys 1505 | * @param int|null $resultMode 1506 | * @param bool|null $styleIdxInclude 1507 | * @param int|null $rowLimit 1508 | * 1509 | * @return \Generator|null 1510 | */ 1511 | public function rewind($columnKeys = [], ?int $resultMode = null, ?bool $styleIdxInclude = null, ?int $rowLimit = 0): ?\Generator 1512 | { 1513 | 1514 | return $this->reset($columnKeys = [], $resultMode, $styleIdxInclude, $rowLimit); 1515 | } 1516 | 1517 | /** 1518 | * @return mixed 1519 | */ 1520 | public function readNextRow() 1521 | { 1522 | if (!$this->generator) { 1523 | $this->reset(); 1524 | } 1525 | if ($this->countReadRows > 0) { 1526 | $this->generator->next(); 1527 | } 1528 | if ($result = $this->generator->current()) { 1529 | $this->countReadRows++; 1530 | } 1531 | 1532 | return $result; 1533 | } 1534 | 1535 | /** 1536 | * @return int 1537 | */ 1538 | public function getReadRowNum(): int 1539 | { 1540 | return $this->readRowNum; 1541 | } 1542 | 1543 | /** 1544 | * Returns all merged ranges 1545 | * 1546 | * @return array|null 1547 | */ 1548 | public function getMergedCells(): ?array 1549 | { 1550 | if ($this->mergedCells === null) { 1551 | $this->_readBottom(); 1552 | } 1553 | 1554 | return $this->mergedCells; 1555 | } 1556 | 1557 | /** 1558 | * Checks if a cell is merged 1559 | * 1560 | * @param string $cellAddress 1561 | * 1562 | * @return bool 1563 | */ 1564 | public function isMerged(string $cellAddress): bool 1565 | { 1566 | foreach ($this->getMergedCells() as $range) { 1567 | if (Helper::inRange($cellAddress, $range)) { 1568 | return true; 1569 | } 1570 | } 1571 | 1572 | return false; 1573 | } 1574 | 1575 | /** 1576 | * Returns merge range of specified cell 1577 | * 1578 | * @param string $cellAddress 1579 | * 1580 | * @return string|null 1581 | */ 1582 | public function mergedRange(string $cellAddress): ?string 1583 | { 1584 | foreach ($this->getMergedCells() as $range) { 1585 | if (Helper::inRange($cellAddress, $range)) { 1586 | return $range; 1587 | } 1588 | } 1589 | 1590 | return null; 1591 | } 1592 | 1593 | /** 1594 | * @return string|null 1595 | */ 1596 | protected function drawingFilename(): ?string 1597 | { 1598 | $findName = str_replace('/worksheets/sheet', '/drawings/drawing', $this->pathInZip); 1599 | 1600 | return in_array($findName, $this->excel->innerFileList(), true) ? $findName : null; 1601 | } 1602 | 1603 | /** 1604 | * @param string $cell 1605 | * @param string $fileName 1606 | * @param string|null $imageName 1607 | * 1608 | * @return void 1609 | */ 1610 | protected function addImage(string $cell, string $fileName, ?string $imageName = null, ?array $meta = []) 1611 | { 1612 | $this->images[$cell] = [ 1613 | 'image_name' => $imageName, 1614 | 'file_name' => $fileName, 1615 | 'meta' => $meta, 1616 | ]; 1617 | } 1618 | 1619 | /** 1620 | * @param $xmlName 1621 | * 1622 | * @return array 1623 | */ 1624 | protected function extractDrawingInfo($xmlName): array 1625 | { 1626 | $drawings = [ 1627 | 'xml' => $xmlName, 1628 | 'rel' => dirname($xmlName) . '/_rels/' . basename($xmlName) . '.rels', 1629 | ]; 1630 | $contents = file_get_contents('zip://' . $this->zipFilename . '#' . $xmlName); 1631 | $typeAnchors = []; 1632 | if (preg_match_all('#]*>(.*)]*>(.*)[^>]*>(.*)#siU', $contents, $anchors)) { 1639 | $typeAnchors['abs'] = $anchors[1]; 1640 | } 1641 | foreach ($typeAnchors as $type => $anchors) { 1642 | foreach ($anchors as $anchorStr) { 1643 | $picture = []; 1644 | if (preg_match('#(.*)#siU', $anchorStr, $pic)) { 1645 | if (preg_match('##siU', $pic[1], $m)) { 1649 | $picture['name'] = $m[2]; 1650 | } 1651 | } 1652 | if ($picture) { 1653 | if (preg_match('#]*>(.*)(.*)(.*)zipFilename . '#' . $drawings['rel']); 1673 | if (preg_match_all('#]+)>#siU', $contents, $rel)) { 1674 | foreach ($rel[1] as $str) { 1675 | if (preg_match('#Id="(\w+)#', $str, $m1) && preg_match('#Target="([^"]+)#', $str, $m2)) { 1676 | $rId = $m1[1]; 1677 | if (isset($drawings['media'][$rId])) { 1678 | $drawings['media'][$rId]['target'] = str_replace('../', 'xl/', $m2[1]); 1679 | } 1680 | } 1681 | } 1682 | } 1683 | } 1684 | 1685 | $result = [ 1686 | 'xml' => $drawings['xml'], 1687 | 'rel' => $drawings['rel'], 1688 | ]; 1689 | foreach ($drawings['media'] as $media) { 1690 | if (isset($media['target'])) { 1691 | $addr = $media['col'] . $media['row']; 1692 | if (!isset($media['name'])) { 1693 | $media['name'] = $addr; 1694 | } 1695 | $result['images'][$addr] = $media; 1696 | $result['rows'][$media['row']][] = $addr; 1697 | $this->addImage($addr, basename($media['target']), $media['name']); 1698 | } 1699 | } 1700 | 1701 | return $result; 1702 | } 1703 | 1704 | /** 1705 | * @param int $numImages 1706 | * 1707 | * @return void 1708 | */ 1709 | protected function extractRichValueImages(int $numImages) 1710 | { 1711 | //$xmlReader = $this->getReader(); 1712 | //$xmlReader->openZip($this->pathInZip); 1713 | $xmlReader = $this->xmlReaderOpenZip($this->pathInZip); 1714 | while ($xmlReader->read()) { 1715 | // seek 1716 | if ($xmlReader->name === 'sheetData') { 1717 | break; 1718 | } 1719 | } 1720 | $count = 0; 1721 | while ($xmlReader->read() && $numImages > $count) { 1722 | // loop until 1723 | if ($xmlReader->name === 'sheetData' && $xmlReader->nodeType === \XMLReader::END_ELEMENT) { 1724 | break; 1725 | } 1726 | if ($xmlReader->name === 'c' && $xmlReader->nodeType === \XMLReader::ELEMENT) { 1727 | $vm = (string)$xmlReader->getAttribute('vm'); 1728 | $cell = (string)$xmlReader->getAttribute('r'); 1729 | if ($vm && ($imageFile = $this->excel->metadataImage($vm))) { 1730 | $this->addImage($cell, basename($imageFile), null, ['r' => $cell, 'vm' => $vm]); 1731 | $count++; 1732 | } 1733 | } 1734 | } 1735 | //$xmlReader->close(); 1736 | $this->xmlReaderClose($xmlReader);; 1737 | } 1738 | 1739 | /** 1740 | * @return bool 1741 | */ 1742 | public function hasDrawings(): bool 1743 | { 1744 | return (bool)$this->drawingFilename(); 1745 | } 1746 | 1747 | /** 1748 | * Count images of the sheet 1749 | * 1750 | * @return int 1751 | */ 1752 | public function countImages(): int 1753 | { 1754 | if ($this->countImages === -1) { 1755 | $this->_countDrawingsImages(); 1756 | if ($cnt = $this->excel->countExtraImages()) { 1757 | $this->extractRichValueImages($cnt); 1758 | } 1759 | $this->countImages = count($this->images); 1760 | } 1761 | 1762 | return $this->countImages; 1763 | } 1764 | 1765 | /** 1766 | * Count images form drawings of the sheet 1767 | * 1768 | * @return int 1769 | */ 1770 | public function _countDrawingsImages(): int 1771 | { 1772 | $result = 0; 1773 | if ($this->hasDrawings()) { 1774 | if (!isset($this->props['drawings'])) { 1775 | if ($xmlName = $this->drawingFilename()) { 1776 | $this->props['drawings'] = $this->extractDrawingInfo($xmlName); 1777 | } 1778 | else { 1779 | $this->props['drawings'] = []; 1780 | } 1781 | } 1782 | if (!empty($this->props['drawings']['images'])) { 1783 | $result = count($this->props['drawings']['images']); 1784 | } 1785 | } 1786 | 1787 | return $result; 1788 | } 1789 | 1790 | /** 1791 | * @return array 1792 | */ 1793 | public function _getDrawingsImageFiles(): array 1794 | { 1795 | $result = []; 1796 | if ($this->_countDrawingsImages()) { 1797 | $result = array_column($this->props['drawings']['images'], 'target'); 1798 | } 1799 | 1800 | return $result; 1801 | } 1802 | 1803 | /** 1804 | * @return array 1805 | */ 1806 | public function getImageList(): array 1807 | { 1808 | $result = []; 1809 | if ($this->countImages()) { 1810 | foreach ($this->images as $cell => $image) { 1811 | $result[$cell]['image_name'] = $image['image_name']; 1812 | $result[$cell]['file_name'] = $image['file_name']; 1813 | } 1814 | } 1815 | 1816 | return $result; 1817 | } 1818 | 1819 | /** 1820 | * @param $row 1821 | * 1822 | * @return array 1823 | */ 1824 | public function getImageListByRow($row): array 1825 | { 1826 | $result = []; 1827 | if ($this->countImages()) { 1828 | if (isset($this->props['drawings']['rows'][$row])) { 1829 | foreach ($this->props['drawings']['rows'][$row] as $addr) { 1830 | $result[$addr] = [ 1831 | 'image_name' => $this->props['drawings']['images'][$addr]['name'], 1832 | 'file_name' => basename($this->props['drawings']['images'][$addr]['target']), 1833 | ]; 1834 | } 1835 | } 1836 | } 1837 | 1838 | return $result; 1839 | } 1840 | 1841 | /** 1842 | * Returns TRUE if the cell contains an image 1843 | * 1844 | * @param string $cell 1845 | * 1846 | * @return bool 1847 | */ 1848 | public function hasImage(string $cell): bool 1849 | { 1850 | if ($this->countImages()) { 1851 | return isset($this->images[strtoupper($cell)]); 1852 | } 1853 | 1854 | return false; 1855 | } 1856 | 1857 | /** 1858 | * Returns full path of an image from the cell (if exists) or null 1859 | * 1860 | * @param string $cell 1861 | * 1862 | * @return string|null 1863 | */ 1864 | public function imageEntryFullPath(string $cell): ?string 1865 | { 1866 | if ($this->countImages()) { 1867 | $cell = strtoupper($cell); 1868 | if (isset($this->props['drawings']['images'][$cell])) { 1869 | 1870 | return 'zip://' . $this->zipFilename . '#' . $this->props['drawings']['images'][$cell]['target']; 1871 | } 1872 | } 1873 | 1874 | return null; 1875 | } 1876 | 1877 | /** 1878 | * Returns the MIME type for an image from the cell as determined by using information from the magic.mime file 1879 | * Requires fileinfo extension 1880 | * 1881 | * @param string $cell 1882 | * 1883 | * @return string|null 1884 | */ 1885 | public function getImageMimeType(string $cell): ?string 1886 | { 1887 | if (function_exists('mime_content_type') && ($path = $this->imageEntryFullPath($cell))) { 1888 | return mime_content_type($path); 1889 | } 1890 | 1891 | return null; 1892 | } 1893 | 1894 | /** 1895 | * Returns the name for an image from the cell as it defines in XLSX 1896 | * 1897 | * @param string $cell 1898 | * 1899 | * @return string|null 1900 | */ 1901 | public function getImageName(string $cell): ?string 1902 | { 1903 | if ($this->countImages()) { 1904 | $cell = strtoupper($cell); 1905 | if (isset($this->props['drawings']['images'][$cell])) { 1906 | 1907 | return $this->props['drawings']['images'][$cell]['name']; 1908 | } 1909 | } 1910 | 1911 | return null; 1912 | } 1913 | 1914 | /** 1915 | * Returns an image from the cell as a blob (if exists) or null 1916 | * 1917 | * @param string $cell 1918 | * 1919 | * @return string|null 1920 | */ 1921 | public function getImageBlob(string $cell): ?string 1922 | { 1923 | if ($path = $this->imageEntryFullPath($cell)) { 1924 | return file_get_contents($path); 1925 | } 1926 | 1927 | return null; 1928 | } 1929 | 1930 | /** 1931 | * Writes an image from the cell to the specified filename 1932 | * 1933 | * @param string $cell 1934 | * @param string|null $filename 1935 | * 1936 | * @return string|null 1937 | */ 1938 | public function saveImage(string $cell, ?string $filename = null): ?string 1939 | { 1940 | if ($contents = $this->getImageBlob($cell)) { 1941 | if (!$filename) { 1942 | $filename = basename($this->props['drawings']['images'][strtoupper($cell)]['target']); 1943 | } 1944 | if (file_put_contents($filename, $contents)) { 1945 | return realpath($filename); 1946 | } 1947 | } 1948 | 1949 | return null; 1950 | } 1951 | 1952 | /** 1953 | * Writes an image from the cell to the specified directory 1954 | * 1955 | * @param string $cell 1956 | * @param string $dirname 1957 | * 1958 | * @return string|null 1959 | */ 1960 | public function saveImageTo(string $cell, string $dirname): ?string 1961 | { 1962 | $filename = basename($this->props['drawings']['images'][strtoupper($cell)]['target']); 1963 | 1964 | return $this->saveImage($cell, str_replace(['\\', '/'], DIRECTORY_SEPARATOR, $dirname) . DIRECTORY_SEPARATOR . $filename); 1965 | } 1966 | 1967 | /** 1968 | * Returns an array of data validation rules found in the sheet 1969 | * 1970 | * @return array 1976 | */ 1977 | public function getDataValidations(): array 1978 | { 1979 | if ($this->validations === null) { 1980 | $this->extractDataValidations(); 1981 | } 1982 | 1983 | return $this->validations; 1984 | } 1985 | 1986 | /** Extracts data validation rules from the sheet */ 1987 | public function extractDataValidations(): void 1988 | { 1989 | $validations = []; 1990 | //$xmlReader = $this->getReader(); 1991 | //$xmlReader->openZip($this->pathInZip); 1992 | $xmlReader = $this->xmlReaderOpenZip($this->pathInZip); 1993 | 1994 | while ($xmlReader->read()) { 1995 | if ($xmlReader->nodeType === \XMLReader::ELEMENT) { 1996 | // Standard data validation 1997 | if ($xmlReader->name === 'dataValidation') { 1998 | $validation = $this->parseDataValidation($xmlReader); 1999 | if ($validation) { 2000 | $validations[] = $validation; 2001 | } 2002 | } 2003 | 2004 | // Extended data validation 2005 | if ($xmlReader->name === 'x14:dataValidation') { 2006 | $validation = $this->parseExtendedDataValidation($xmlReader); 2007 | if ($validation) { 2008 | $validations[] = $validation; 2009 | } 2010 | } 2011 | } 2012 | } 2013 | 2014 | //$xmlReader->close(); 2015 | $this->xmlReaderClose($xmlReader);; 2016 | 2017 | $this->validations = $validations; 2018 | } 2019 | 2020 | /** 2021 | * Parse standard 2022 | * 2023 | * @param InterfaceXmlReader $xmlReader 2024 | * 2025 | * @return array{ 2026 | * type: string, 2027 | * sqref: string, 2028 | * formula1: ?string, 2029 | * formula2: ?string, 2030 | * } 2031 | */ 2032 | protected function parseDataValidation(InterfaceXmlReader $xmlReader): ?array 2033 | { 2034 | $type = $xmlReader->getAttribute('type'); 2035 | $sqref = $xmlReader->getAttribute('sqref'); 2036 | $formula1 = null; 2037 | $formula2 = null; 2038 | 2039 | // Check if it's a self-closing tag 2040 | if ($xmlReader->isEmptyElement) { 2041 | return [ 2042 | 'type' => $type, 2043 | 'sqref' => $sqref, 2044 | 'formula1' => $formula1, 2045 | 'formula2' => $formula2 2046 | ]; 2047 | } 2048 | 2049 | // Handle child nodes like formula1 and formula2 2050 | while ($xmlReader->read()) { 2051 | if ($xmlReader->nodeType === \XMLReader::ELEMENT && $xmlReader->name === 'formula1') { 2052 | $xmlReader->read(); 2053 | $formula1 = $xmlReader->value; 2054 | } elseif ($xmlReader->nodeType === \XMLReader::ELEMENT && $xmlReader->name === 'formula2') { 2055 | $xmlReader->read(); 2056 | $formula2 = $xmlReader->value; 2057 | } 2058 | if ($xmlReader->nodeType === \XMLReader::END_ELEMENT && $xmlReader->name === 'dataValidation') { 2059 | break; 2060 | } 2061 | } 2062 | 2063 | return [ 2064 | 'type' => $type, 2065 | 'sqref' => $sqref, 2066 | 'formula1' => $formula1, 2067 | 'formula2' => $formula2 2068 | ]; 2069 | } 2070 | 2071 | /** 2072 | * Parse extended 2073 | * 2074 | * @param InterfaceXmlReader $xmlReader 2075 | * 2076 | * @return array{ 2077 | * type: string, 2078 | * sqref: string, 2079 | * formula1: ?string, 2080 | * formula2: ?string, 2081 | * } 2082 | */ 2083 | protected function parseExtendedDataValidation(InterfaceXmlReader $xmlReader): array 2084 | { 2085 | $type = $xmlReader->getAttribute('type'); 2086 | $sqref = null; 2087 | $formula1 = null; 2088 | $formula2 = null; 2089 | 2090 | // Check if it's a self-closing tag 2091 | if ($xmlReader->isEmptyElement) { 2092 | return [ 2093 | 'type' => $type, 2094 | 'sqref' => $sqref, 2095 | 'formula1' => $formula1, 2096 | 'formula2' => $formula2 2097 | ]; 2098 | } 2099 | 2100 | // Parse the attributes within the tag 2101 | while ($xmlReader->read()) { 2102 | // Parse the sqref (cell range) 2103 | if ($xmlReader->nodeType === \XMLReader::ELEMENT && $xmlReader->name === 'xm:sqref') { 2104 | $xmlReader->read(); 2105 | $sqref = $xmlReader->value; 2106 | } 2107 | 2108 | // Capture formula1 and extract inner value 2109 | if ($xmlReader->nodeType === \XMLReader::ELEMENT && $xmlReader->name === 'x14:formula1') { 2110 | while ($xmlReader->read()) { 2111 | if ($xmlReader->nodeType === \XMLReader::ELEMENT && $xmlReader->name === 'xm:f') { 2112 | $xmlReader->read(); 2113 | $formula1 = $xmlReader->value; 2114 | break; 2115 | } 2116 | } 2117 | } 2118 | 2119 | // Capture formula2 and extract inner value 2120 | if ($xmlReader->nodeType === \XMLReader::ELEMENT && $xmlReader->name === 'x14:formula2') { 2121 | while ($xmlReader->read()) { 2122 | if ($xmlReader->nodeType === \XMLReader::ELEMENT && $xmlReader->name === 'xm:f') { 2123 | $xmlReader->read(); 2124 | $formula2 = $xmlReader->value; 2125 | break; 2126 | } 2127 | } 2128 | } 2129 | 2130 | // Break when reaching the end of 2131 | if ($xmlReader->nodeType === \XMLReader::END_ELEMENT && $xmlReader->name === 'x14:dataValidation') { 2132 | break; 2133 | } 2134 | } 2135 | 2136 | return [ 2137 | 'type' => $type, 2138 | 'sqref' => $sqref, 2139 | 'formula1' => $formula1, 2140 | 'formula2' => $formula2 2141 | ]; 2142 | } 2143 | 2144 | /** 2145 | * Returns an array of data validation rules found in the sheet 2146 | * 2147 | * @return array 2152 | */ 2153 | public function getConditionalFormatting(): array 2154 | { 2155 | if ($this->conditionals === null) { 2156 | $this->extractConditionalFormatting(); 2157 | } 2158 | 2159 | return $this->conditionals; 2160 | } 2161 | 2162 | /** Extracts conditional formatting rules from the sheet */ 2163 | public function extractConditionalFormatting(): void 2164 | { 2165 | $conditionals = []; 2166 | //$xmlReader = $this->getReader(); 2167 | //$xmlReader->openZip($this->pathInZip); 2168 | $xmlReader = $this->xmlReaderOpenZip($this->pathInZip); 2169 | 2170 | while ($xmlReader->read()) { 2171 | if ($xmlReader->nodeType === \XMLReader::ELEMENT && $xmlReader->name === 'conditionalFormatting') { 2172 | $conditional = $this->parseConditionalFormatting($xmlReader); 2173 | if ($conditional) { 2174 | $conditionals[] = $conditional; 2175 | } 2176 | } 2177 | } 2178 | 2179 | //$xmlReader->close(); 2180 | $this->xmlReaderClose($xmlReader);; 2181 | 2182 | $this->conditionals = $conditionals; 2183 | } 2184 | 2185 | /** 2186 | * Parse 2187 | * 2188 | * @param InterfaceXmlReader $xmlReader 2189 | * 2190 | * @return array{ 2191 | * type: string, 2192 | * sqref: string, 2193 | * attributes: [] 2194 | * } 2195 | */ 2196 | protected function parseConditionalFormatting(InterfaceXmlReader $xmlReader): ?array 2197 | { 2198 | $sqref = $xmlReader->getAttribute('sqref'); 2199 | $attributes = []; 2200 | 2201 | // Handle child nodes like formula1 and formula2 2202 | while ($xmlReader->read()) { 2203 | if ($xmlReader->nodeType === \XMLReader::ELEMENT && $xmlReader->name === 'cfRule') { 2204 | $node = $xmlReader->expand(); 2205 | foreach ($node->attributes as $key => $val) { 2206 | $attributes[$key] = $val->value; 2207 | } 2208 | } 2209 | if ($xmlReader->nodeType === \XMLReader::END_ELEMENT && $xmlReader->name === 'conditionalFormatting') { 2210 | break; 2211 | } 2212 | } 2213 | 2214 | return [ 2215 | 'type' => $attributes['type'] ?? null, 2216 | 'sqref' => $sqref, 2217 | 'attributes' => $attributes, 2218 | ]; 2219 | } 2220 | 2221 | public function setDefaultRowHeight(float $rowHeight): void 2222 | { 2223 | $this->defaultRowHeight = $rowHeight; 2224 | } 2225 | 2226 | /** 2227 | * Parses and retrieves column widths and row heights from the sheet XML. 2228 | * 2229 | * @return void 2230 | */ 2231 | protected function extractColumnWidthsAndRowHeights(): void 2232 | { 2233 | $this->colWidths = []; 2234 | $this->rowHeights = []; 2235 | 2236 | //$xmlReader = $this->getReader(); 2237 | //$xmlReader->openZip($this->pathInZip); 2238 | $xmlReader = $this->xmlReaderOpenZip($this->pathInZip); 2239 | 2240 | while ($xmlReader->read()) { 2241 | if ($xmlReader->nodeType === \XMLReader::ELEMENT) { 2242 | // Extract column width 2243 | if ($xmlReader->name === 'col') { 2244 | $min = (int)$xmlReader->getAttribute('min'); 2245 | $max = (int)$xmlReader->getAttribute('max'); 2246 | $width = (float)$xmlReader->getAttribute('width'); 2247 | 2248 | for ($i = $min; $i <= $max; $i++) { 2249 | $this->colWidths[$i] = $width; 2250 | } 2251 | } 2252 | // Extract row height 2253 | elseif ($xmlReader->name === 'row') { 2254 | $rowIndex = (int)$xmlReader->getAttribute('r'); 2255 | $height = $xmlReader->getAttribute('ht') ? (float)$xmlReader->getAttribute('ht') : $this->defaultRowHeight; 2256 | $this->rowHeights[$rowIndex] = $height; 2257 | } 2258 | } 2259 | } 2260 | 2261 | //$xmlReader->close(); 2262 | $this->xmlReaderClose($xmlReader);; 2263 | } 2264 | 2265 | /** 2266 | * Returns column width for a specific column number. 2267 | * 2268 | * @param int $colNumber 2269 | * @return float|null 2270 | */ 2271 | public function getColumnWidth(int $colNumber): ?float 2272 | { 2273 | if ($this->colWidths === null) { 2274 | $this->extractColumnWidthsAndRowHeights(); 2275 | } 2276 | return $this->colWidths[$colNumber] ?? null; 2277 | } 2278 | 2279 | /** 2280 | * Returns row height for a specific row number. 2281 | * 2282 | * @param int $rowNumber 2283 | * 2284 | * @return float|null 2285 | */ 2286 | public function getRowHeight(int $rowNumber): ?float 2287 | { 2288 | if ($this->rowHeights === null) { 2289 | $this->extractColumnWidthsAndRowHeights(); 2290 | } 2291 | return $this->rowHeights[$rowNumber] ?? null; 2292 | } 2293 | 2294 | /** 2295 | * Parses and retrieves frozen pane info from the sheet XML 2296 | * 2297 | * @return array|null 2298 | */ 2299 | public function getFreezePaneInfo(): ?array 2300 | { 2301 | //$xmlReader = $this->getReader(); 2302 | //$xmlReader->openZip($this->pathInZip); 2303 | $xmlReader = $this->xmlReaderOpenZip($this->pathInZip); 2304 | 2305 | $freezePane = null; 2306 | 2307 | while ($xmlReader->read()) { 2308 | if ($xmlReader->nodeType === \XMLReader::ELEMENT && $xmlReader->name === 'pane') { 2309 | $xSplit = (int)$xmlReader->getAttribute('xSplit'); 2310 | $ySplit = (int)$xmlReader->getAttribute('ySplit'); 2311 | $topLeftCell = $xmlReader->getAttribute('topLeftCell'); 2312 | 2313 | $freezePane = [ 2314 | 'xSplit' => $xSplit, 2315 | 'ySplit' => $ySplit, 2316 | 'topLeftCell' => $topLeftCell, 2317 | ]; 2318 | break; 2319 | } 2320 | } 2321 | //$xmlReader->close(); 2322 | $this->xmlReaderClose($xmlReader);; 2323 | 2324 | return $freezePane; 2325 | } 2326 | 2327 | /** 2328 | * Extracts the tab properties from the sheet XML 2329 | * 2330 | * @return void 2331 | */ 2332 | protected function _readTabProperties(): void 2333 | { 2334 | if ($this->tabProperties !== null) { 2335 | return; 2336 | } 2337 | 2338 | $this->tabProperties = [ 2339 | 'color' => null, 2340 | ]; 2341 | 2342 | //$xmlReader = $this->getReader(); 2343 | //$xmlReader->openZip($this->pathInZip); 2344 | $xmlReader = $this->xmlReaderOpenZip($this->pathInZip); 2345 | 2346 | while ($xmlReader->read()) { 2347 | if ($xmlReader->nodeType === \XMLReader::ELEMENT && $xmlReader->name === 'sheetPr') { 2348 | while ($xmlReader->read()) { 2349 | if ($xmlReader->nodeType === \XMLReader::ELEMENT && $xmlReader->name === 'tabColor') { 2350 | $this->tabProperties['color'] = [ 2351 | 'rgb' => $xmlReader->getAttribute('rgb'), 2352 | 'theme' => $xmlReader->getAttribute('theme'), 2353 | 'tint' => $xmlReader->getAttribute('tint'), 2354 | 'indexed' => $xmlReader->getAttribute('indexed'), 2355 | ]; 2356 | 2357 | $this->tabProperties['color'] = array_filter( 2358 | $this->tabProperties['color'], 2359 | static fn($value) => $value !== null 2360 | ); 2361 | break; 2362 | } 2363 | if ($xmlReader->nodeType === \XMLReader::END_ELEMENT && $xmlReader->name === 'sheetPr') { 2364 | break; 2365 | } 2366 | } 2367 | break; 2368 | } 2369 | } 2370 | 2371 | //$xmlReader->close(); 2372 | $this->xmlReaderClose($xmlReader); 2373 | } 2374 | 2375 | /** 2376 | * Returns the tab color info of the sheet 2377 | * Contains any of: rgb, theme, tint, indexed 2378 | * 2379 | * @return array|null 2380 | */ 2381 | public function getTabColorInfo(): ?array 2382 | { 2383 | if ($this->tabProperties === null) { 2384 | $this->_readTabProperties(); 2385 | } 2386 | 2387 | return $this->tabProperties['color'] ?? null; 2388 | } 2389 | 2390 | /** 2391 | * Alias of getTabColorConfig() 2392 | * 2393 | * @return array|null 2394 | */ 2395 | public function getTabColorConfiguration(): ?array 2396 | { 2397 | return $this->getTabColorInfo(); 2398 | } 2399 | } --------------------------------------------------------------------------------