Skip to content

Commit 297817f

Browse files
author
MarkBaker
committed
Merge branch 'master' into 2.0-Development
2 parents f918847 + 4d5a482 commit 297817f

File tree

13 files changed

+291
-50
lines changed

13 files changed

+291
-50
lines changed

docs/topics/reading-and-writing-to-file.md

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -436,7 +436,14 @@ You can read a .csv file using the following code:
436436

437437
```php
438438
$reader = new \PhpOffice\PhpSpreadsheet\Reader\Csv();
439-
$spreadsheet = $reader->load("sample.csv");
439+
$spreadsheet = $reader->load('sample.csv');
440+
```
441+
442+
You can also treat a string as if it were the contents of a CSV file as follows:
443+
444+
```php
445+
$reader = new \PhpOffice\PhpSpreadsheet\Reader\Csv();
446+
$spreadsheet = $reader->loadSpreadsheetFromString($data);
440447
```
441448

442449
#### Setting CSV options

samples/Basic/27_Images_Html_Pdf.php

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
<?php
2+
3+
use PhpOffice\PhpSpreadsheet\IOFactory;
4+
use PhpOffice\PhpSpreadsheet\Reader\Xlsx as XlsxReader;
5+
use PhpOffice\PhpSpreadsheet\Shared\File;
6+
use PhpOffice\PhpSpreadsheet\Worksheet\PageSetup;
7+
use PhpOffice\PhpSpreadsheet\Writer\Pdf\Mpdf;
8+
use PhpOffice\PhpSpreadsheet\Writer\Xlsx as XlsxWriter;
9+
10+
require __DIR__ . '/../Header.php';
11+
12+
// Read from Xls (.xls) template
13+
$helper->log('Load Xlsx template file');
14+
$reader = IOFactory::createReader('Xls');
15+
$initialSpreadsheet = $reader->load(__DIR__ . '/../templates/27template.xls');
16+
17+
$xlsxFile = File::temporaryFilename();
18+
$writer = new XlsxWriter($initialSpreadsheet);
19+
$helper->log('Save as Xlsx');
20+
$writer->save($xlsxFile);
21+
$initialSpreadsheet->disconnectWorksheets();
22+
$reader2 = new XlsxReader();
23+
$helper->log('Load Xlsx');
24+
$spreadsheet = $reader2->load($xlsxFile);
25+
26+
$helper->log('Hide grid lines');
27+
$spreadsheet->getActiveSheet()->setShowGridLines(false);
28+
29+
$helper->log('Set orientation to landscape');
30+
$spreadsheet->getActiveSheet()->getPageSetup()->setOrientation(PageSetup::ORIENTATION_LANDSCAPE);
31+
32+
$className = Mpdf::class;
33+
$helper->log("Write to PDF format using {$className}, and to Html");
34+
IOFactory::registerWriter('Pdf', $className);
35+
36+
// Save
37+
$helper->write($spreadsheet, __FILE__, ['Pdf', 'Html']);
38+
unlink($xlsxFile);
39+
$spreadsheet->disconnectWorksheets();

src/PhpSpreadsheet/Cell/AddressHelper.php

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -118,17 +118,17 @@ public static function convertToR1C1(
118118
throw new Exception('Invalid A1-format Cell Reference');
119119
}
120120

121-
$columnId = Coordinate::columnIndexFromString($cellReference['col_ref']);
122-
if ($cellReference['absolute_col'] === '$') {
121+
if ($cellReference['col'][0] === '$') {
123122
// Column must be absolute address
124123
$currentColumnNumber = null;
125124
}
125+
$columnId = Coordinate::columnIndexFromString(ltrim($cellReference['col'], '$'));
126126

127-
$rowId = (int) $cellReference['row_ref'];
128-
if ($cellReference['absolute_row'] === '$') {
127+
if ($cellReference['row'][0] === '$') {
129128
// Row must be absolute address
130129
$currentRowNumber = null;
131130
}
131+
$rowId = (int) ltrim($cellReference['row'], '$');
132132

133133
if ($currentRowNumber !== null) {
134134
if ($rowId === $currentRowNumber) {

src/PhpSpreadsheet/Cell/CellAddress.php

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -35,9 +35,7 @@ class CellAddress
3535
public function __construct(string $cellAddress, ?Worksheet $worksheet = null)
3636
{
3737
$this->cellAddress = str_replace('$', '', $cellAddress);
38-
[$this->columnName, $rowId] = Coordinate::coordinateFromString($cellAddress);
39-
$this->rowId = (int) $rowId;
40-
$this->columnId = Coordinate::columnIndexFromString($this->columnName);
38+
[$this->columnId, $this->rowId, $this->columnName] = Coordinate::indexesFromString($this->cellAddress);
4139
$this->worksheet = $worksheet;
4240
}
4341

src/PhpSpreadsheet/Cell/Coordinate.php

Lines changed: 24 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@
1313
*/
1414
abstract class Coordinate
1515
{
16-
public const A1_COORDINATE_REGEX = '/^(?<absolute_col>\$?)(?<col_ref>[A-Z]{1,3})(?<absolute_row>\$?)(?<row_ref>\d{1,7})$/i';
16+
public const A1_COORDINATE_REGEX = '/^(?<col>\$?[A-Z]{1,3})(?<row>\$?\d{1,7})$/i';
1717

1818
/**
1919
* Default range variable constant.
@@ -32,7 +32,7 @@ abstract class Coordinate
3232
public static function coordinateFromString($cellAddress)
3333
{
3434
if (preg_match(self::A1_COORDINATE_REGEX, $cellAddress, $matches)) {
35-
return [$matches['absolute_col'] . $matches['col_ref'], $matches['absolute_row'] . $matches['row_ref']];
35+
return [$matches['col'], $matches['row']];
3636
} elseif (self::coordinateIsRange($cellAddress)) {
3737
throw new Exception('Cell coordinate string can not be a range of cells');
3838
} elseif ($cellAddress == '') {
@@ -276,35 +276,42 @@ public static function columnIndexFromString($columnAddress)
276276
if (isset($indexCache[$columnAddress])) {
277277
return $indexCache[$columnAddress];
278278
}
279-
// It's surprising how costly the strtoupper() and ord() calls actually are, so we use a lookup array rather than use ord()
280-
// and make it case insensitive to get rid of the strtoupper() as well. Because it's a static, there's no significant
281-
// memory overhead either
279+
// It's surprising how costly the strtoupper() and ord() calls actually are, so we use a lookup array
280+
// rather than use ord() and make it case insensitive to get rid of the strtoupper() as well.
281+
// Because it's a static, there's no significant memory overhead either.
282282
static $columnLookup = [
283-
'A' => 1, 'B' => 2, 'C' => 3, 'D' => 4, 'E' => 5, 'F' => 6, 'G' => 7, 'H' => 8, 'I' => 9, 'J' => 10, 'K' => 11, 'L' => 12, 'M' => 13,
284-
'N' => 14, 'O' => 15, 'P' => 16, 'Q' => 17, 'R' => 18, 'S' => 19, 'T' => 20, 'U' => 21, 'V' => 22, 'W' => 23, 'X' => 24, 'Y' => 25, 'Z' => 26,
285-
'a' => 1, 'b' => 2, 'c' => 3, 'd' => 4, 'e' => 5, 'f' => 6, 'g' => 7, 'h' => 8, 'i' => 9, 'j' => 10, 'k' => 11, 'l' => 12, 'm' => 13,
286-
'n' => 14, 'o' => 15, 'p' => 16, 'q' => 17, 'r' => 18, 's' => 19, 't' => 20, 'u' => 21, 'v' => 22, 'w' => 23, 'x' => 24, 'y' => 25, 'z' => 26,
283+
'A' => 1, 'B' => 2, 'C' => 3, 'D' => 4, 'E' => 5, 'F' => 6, 'G' => 7, 'H' => 8, 'I' => 9, 'J' => 10,
284+
'K' => 11, 'L' => 12, 'M' => 13, 'N' => 14, 'O' => 15, 'P' => 16, 'Q' => 17, 'R' => 18, 'S' => 19,
285+
'T' => 20, 'U' => 21, 'V' => 22, 'W' => 23, 'X' => 24, 'Y' => 25, 'Z' => 26,
286+
'a' => 1, 'b' => 2, 'c' => 3, 'd' => 4, 'e' => 5, 'f' => 6, 'g' => 7, 'h' => 8, 'i' => 9, 'j' => 10,
287+
'k' => 11, 'l' => 12, 'm' => 13, 'n' => 14, 'o' => 15, 'p' => 16, 'q' => 17, 'r' => 18, 's' => 19,
288+
't' => 20, 'u' => 21, 'v' => 22, 'w' => 23, 'x' => 24, 'y' => 25, 'z' => 26,
287289
];
288290

289-
// We also use the language construct isset() rather than the more costly strlen() function to match the length of $columnAddress
290-
// for improved performance
291+
// We also use the language construct isset() rather than the more costly strlen() function to match the
292+
// length of $columnAddress for improved performance
291293
if (isset($columnAddress[0])) {
292294
if (!isset($columnAddress[1])) {
293295
$indexCache[$columnAddress] = $columnLookup[$columnAddress];
294296

295297
return $indexCache[$columnAddress];
296298
} elseif (!isset($columnAddress[2])) {
297-
$indexCache[$columnAddress] = $columnLookup[$columnAddress[0]] * 26 + $columnLookup[$columnAddress[1]];
299+
$indexCache[$columnAddress] = $columnLookup[$columnAddress[0]] * 26
300+
+ $columnLookup[$columnAddress[1]];
298301

299302
return $indexCache[$columnAddress];
300303
} elseif (!isset($columnAddress[3])) {
301-
$indexCache[$columnAddress] = $columnLookup[$columnAddress[0]] * 676 + $columnLookup[$columnAddress[1]] * 26 + $columnLookup[$columnAddress[2]];
304+
$indexCache[$columnAddress] = $columnLookup[$columnAddress[0]] * 676
305+
+ $columnLookup[$columnAddress[1]] * 26
306+
+ $columnLookup[$columnAddress[2]];
302307

303308
return $indexCache[$columnAddress];
304309
}
305310
}
306311

307-
throw new Exception('Column string index can not be ' . ((isset($columnAddress[0])) ? 'longer than 3 characters' : 'empty'));
312+
throw new Exception(
313+
'Column string index can not be ' . ((isset($columnAddress[0])) ? 'longer than 3 characters' : 'empty')
314+
);
308315
}
309316

310317
/**
@@ -317,14 +324,15 @@ public static function columnIndexFromString($columnAddress)
317324
public static function stringFromColumnIndex($columnIndex)
318325
{
319326
static $indexCache = [];
327+
static $lookupCache = ' ABCDEFGHIJKLMNOPQRSTUVWXYZ';
320328

321329
if (!isset($indexCache[$columnIndex])) {
322330
$indexValue = $columnIndex;
323-
$base26 = null;
331+
$base26 = '';
324332
do {
325333
$characterValue = ($indexValue % 26) ?: 26;
326334
$indexValue = ($indexValue - $characterValue) / 26;
327-
$base26 = chr($characterValue + 64) . ($base26 ?: '');
335+
$base26 = $lookupCache[$characterValue] . $base26;
328336
} while ($indexValue > 0);
329337
$indexCache[$columnIndex] = $base26;
330338
}

src/PhpSpreadsheet/Reader/Csv.php

Lines changed: 42 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -265,6 +265,18 @@ protected function loadSpreadsheetFromFile(string $filename): Spreadsheet
265265
return $this->loadIntoExisting($filename, $spreadsheet);
266266
}
267267

268+
/**
269+
* Loads Spreadsheet from string.
270+
*/
271+
public function loadSpreadsheetFromString(string $contents): Spreadsheet
272+
{
273+
// Create new Spreadsheet
274+
$spreadsheet = new Spreadsheet();
275+
276+
// Load into this instance
277+
return $this->loadStringOrFile('data://text/plain,' . urlencode($contents), $spreadsheet, true);
278+
}
279+
268280
private function openFileOrMemory(string $filename): void
269281
{
270282
// Open file
@@ -314,16 +326,43 @@ public function castFormattedNumberToNumeric(
314326
$this->preserveNumericFormatting = $preserveNumericFormatting;
315327
}
316328

329+
/**
330+
* Open data uri for reading.
331+
*/
332+
private function openDataUri(string $filename): void
333+
{
334+
$fileHandle = fopen($filename, 'rb');
335+
if ($fileHandle === false) {
336+
// @codeCoverageIgnoreStart
337+
throw new ReaderException('Could not open file ' . $filename . ' for reading.');
338+
// @codeCoverageIgnoreEnd
339+
}
340+
341+
$this->fileHandle = $fileHandle;
342+
}
343+
317344
/**
318345
* Loads PhpSpreadsheet from file into PhpSpreadsheet instance.
319346
*/
320347
public function loadIntoExisting(string $filename, Spreadsheet $spreadsheet): Spreadsheet
348+
{
349+
return $this->loadStringOrFile($filename, $spreadsheet, false);
350+
}
351+
352+
/**
353+
* Loads PhpSpreadsheet from file into PhpSpreadsheet instance.
354+
*/
355+
private function loadStringOrFile(string $filename, Spreadsheet $spreadsheet, bool $dataUri): Spreadsheet
321356
{
322357
// Deprecated in Php8.1
323358
$iniset = $this->setAutoDetect('1');
324359

325360
// Open file
326-
$this->openFileOrMemory($filename);
361+
if ($dataUri) {
362+
$this->openDataUri($filename);
363+
} else {
364+
$this->openFileOrMemory($filename);
365+
}
327366
$fileHandle = $this->fileHandle;
328367

329368
// Skip BOM, if any
@@ -395,8 +434,8 @@ private function convertBoolean(&$rowDatum, bool $preserveBooleanString): void
395434
} elseif (strcasecmp(Calculation::getFALSE(), $rowDatum) === 0 || strcasecmp('false', $rowDatum) === 0) {
396435
$rowDatum = false;
397436
}
398-
} elseif ($rowDatum === null) {
399-
$rowDatum = '';
437+
} else {
438+
$rowDatum = $rowDatum ?? '';
400439
}
401440
}
402441

src/PhpSpreadsheet/Worksheet/Drawing.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -106,7 +106,7 @@ public function getPath()
106106
*/
107107
public function setPath($path, $verifyFile = true, $zip = null)
108108
{
109-
if ($verifyFile) {
109+
if ($verifyFile && preg_match('~^data:image/[a-z]+;base64,~', $path) !== 1) {
110110
// Check if a URL has been passed. https://stackoverflow.com/a/2058596/1252979
111111
if (filter_var($path, FILTER_VALIDATE_URL)) {
112112
$this->path = $path;

src/PhpSpreadsheet/Worksheet/Validations.php

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -42,9 +42,8 @@ public static function validateCellAddress($cellAddress): string
4242
public static function validateCellOrCellRange($cellRange): string
4343
{
4444
if (is_string($cellRange) || is_numeric($cellRange)) {
45-
$cellRange = (string) $cellRange;
4645
// Convert a single column reference like 'A' to 'A:A'
47-
$cellRange = (string) preg_replace('/^([A-Z]+)$/', '${1}:${1}', $cellRange);
46+
$cellRange = (string) preg_replace('/^([A-Z]+)$/', '${1}:${1}', (string) $cellRange);
4847
// Convert a single row reference like '1' to '1:1'
4948
$cellRange = (string) preg_replace('/^(\d+)$/', '${1}:${1}', $cellRange);
5049
} elseif (is_object($cellRange) && $cellRange instanceof CellAddress) {
@@ -85,7 +84,7 @@ public static function validateCellRange($cellRange): string
8584
public static function definedNameToCoordinate(string $coordinate, Worksheet $worksheet): string
8685
{
8786
// Uppercase coordinate
88-
$testCoordinate = strtoupper($coordinate);
87+
$coordinate = strtoupper($coordinate);
8988
// Eliminate leading equal sign
9089
$testCoordinate = (string) preg_replace('/^=/', '', $coordinate);
9190
$defined = $worksheet->getParent()->getDefinedName($testCoordinate, $worksheet);

src/PhpSpreadsheet/Writer/Html.php

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -478,6 +478,7 @@ public function generateSheetData()
478478

479479
$html .= $endTag;
480480
}
481+
--$row;
481482
$html .= $this->extendRowsForChartsAndImages($sheet, $row);
482483

483484
// Write table footer
@@ -668,7 +669,7 @@ private function writeImageInCell(Worksheet $worksheet, $coordinates)
668669
$html .= PHP_EOL;
669670
$imageData = self::winFileToUrl($filename);
670671

671-
if ($this->embedImages && !$this->isPdf) {
672+
if (($this->embedImages && !$this->isPdf) || substr($imageData, 0, 6) === 'zip://') {
672673
$picture = @file_get_contents($filename);
673674
if ($picture !== false) {
674675
$imageDetails = getimagesize($filename);
@@ -692,7 +693,7 @@ private function writeImageInCell(Worksheet $worksheet, $coordinates)
692693
ob_end_clean(); // End the output buffer.
693694

694695
/** @phpstan-ignore-next-line */
695-
$dataUri = 'data:image/jpeg;base64,' . base64_encode($contents);
696+
$dataUri = 'data:image/png;base64,' . base64_encode($contents);
696697

697698
// Because of the nature of tables, width is more important than height.
698699
// max-width: 100% ensures that image doesnt overflow containing cell

tests/PhpSpreadsheetTests/Helper/SampleTest.php

Lines changed: 2 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -11,10 +11,8 @@ class SampleTest extends TestCase
1111
* @runInSeparateProcess
1212
* @preserveGlobalState disabled
1313
* @dataProvider providerSample
14-
*
15-
* @param mixed $sample
1614
*/
17-
public function testSample($sample): void
15+
public function testSample(string $sample): void
1816
{
1917
// Suppress output to console
2018
$this->setOutputCallback(function (): void {
@@ -32,18 +30,8 @@ public function providerSample(): array
3230
'Chart/32_Chart_read_write_HTML.php', // idem
3331
'Chart/35_Chart_render.php', // idem
3432
];
35-
// TCPDF and DomPDF libraries don't support PHP8 yet
36-
if (\PHP_VERSION_ID >= 80000) {
37-
$skipped = array_merge(
38-
$skipped,
39-
[
40-
'Pdf/21_Pdf_Domdf.php',
41-
'Pdf/21_Pdf_TCPDF.php',
42-
]
43-
);
44-
}
4533

46-
// Unfortunately some tests are too long be ran with code-coverage
34+
// Unfortunately some tests are too long to run with code-coverage
4735
// analysis on GitHub Actions, so we need to exclude them
4836
global $argv;
4937
if (in_array('--coverage-clover', $argv)) {
@@ -58,9 +46,6 @@ public function providerSample(): array
5846
$result = [];
5947
foreach ($helper->getSamples() as $samples) {
6048
foreach ($samples as $sample) {
61-
// if (array_pop(explode('/', $sample)) !== 'DGET.php') {
62-
// continue;
63-
// }
6449
if (!in_array($sample, $skipped)) {
6550
$file = 'samples/' . $sample;
6651
$result[$sample] = [$file];

0 commit comments

Comments
 (0)