From 85b9022f5bcb44c0ad85fb1fa48130cc5ce63f92 Mon Sep 17 00:00:00 2001 From: MarkBaker Date: Wed, 2 Jun 2021 18:44:28 +0200 Subject: [PATCH 01/20] Updates to documentation concerning Value Binders Fix a couple of checks on characters at positions within a string before checking the length of the string when the offset may not exist --- docs/topics/accessing-cells.md | 22 ++++++++++++++----- .../Cell/DefaultValueBinder.php | 4 ++-- 2 files changed, 19 insertions(+), 7 deletions(-) diff --git a/docs/topics/accessing-cells.md b/docs/topics/accessing-cells.md index a777afc1..7330a719 100644 --- a/docs/topics/accessing-cells.md +++ b/docs/topics/accessing-cells.md @@ -110,6 +110,11 @@ values beginning with `=` will be converted to a formula. Strings that aren't numeric, or that don't begin with a leading `=` will be treated as genuine string values. +Note that a numeric string that begins with a leading zero (that isn't +immediately followed by a decimal separator) will not be converted to a +numeric, so values like phone numbers (e.g. `01615991375``will remain as +strings). + This "conversion" is handled by a cell "value binder", and you can write custom "value binders" to change the behaviour of these "conversions". The standard PhpSpreadsheet package also provides an "advanced value @@ -138,8 +143,10 @@ Formats handled by the advanced value binder include: - When strings contain a newline character (`\n`), then the cell styling is set to wrap. -You can read more about value binders later in this section of the -documentation. +Basically, it attempts to mimic the behaviour of the MS Excel GUI. + +You can read more about value binders [later in this section of the +documentation](#using-value-binders-to-facilitate-data-entry). ### Setting a formula in a Cell @@ -551,8 +558,13 @@ $spreadsheet->getActiveSheet()->setCellValue('A5', 'Date/time value:'); $spreadsheet->getActiveSheet()->setCellValue('B5', '21 December 1983'); ``` -**Creating your own value binder is easy.** When advanced value binding -is required, you can implement the -`\PhpOffice\PhpSpreadsheet\Cell\IValueBinder` interface or extend the +Alternatively, a `\PhpOffice\PhpSpreadsheet\Cell\StringValueBinder` class is available +if you want to preserve all string content as strings. This might be appropriate if you +were loading a file containing values that could be interpreted as numbers (e.g. numbers +with leading zeroes such as phone numbers), but that should be retained as strings. + +**Creating your own value binder is relatively straightforward.** When more specialised +value binding is required, you can implement the +`\PhpOffice\PhpSpreadsheet\Cell\IValueBinder` interface or extend the existing `\PhpOffice\PhpSpreadsheet\Cell\DefaultValueBinder` or `\PhpOffice\PhpSpreadsheet\Cell\AdvancedValueBinder` classes. diff --git a/src/PhpSpreadsheet/Cell/DefaultValueBinder.php b/src/PhpSpreadsheet/Cell/DefaultValueBinder.php index 6fae5e76..bfb8bfb2 100644 --- a/src/PhpSpreadsheet/Cell/DefaultValueBinder.php +++ b/src/PhpSpreadsheet/Cell/DefaultValueBinder.php @@ -57,11 +57,11 @@ class DefaultValueBinder implements IValueBinder return DataType::TYPE_STRING; } elseif ($value instanceof RichText) { return DataType::TYPE_INLINE; - } elseif (is_string($value) && $value[0] === '=' && strlen($value) > 1) { + } elseif (is_string($value) && strlen($value) > 1 && $value[0] === '=') { return DataType::TYPE_FORMULA; } elseif (preg_match('/^[\+\-]?(\d+\\.?\d*|\d*\\.?\d+)([Ee][\-\+]?[0-2]?\d{1,3})?$/', $value)) { $tValue = ltrim($value, '+-'); - if (is_string($value) && $tValue[0] === '0' && strlen($tValue) > 1 && $tValue[1] !== '.') { + if (is_string($value) && strlen($tValue) > 1 && $tValue[0] === '0' && $tValue[1] !== '.') { return DataType::TYPE_STRING; } elseif ((strpos($value, '.') === false) && ($value > PHP_INT_MAX)) { return DataType::TYPE_STRING; From 89eac0feed805bdaf2c5cbced29578fd451b91a0 Mon Sep 17 00:00:00 2001 From: MarkBaker Date: Wed, 2 Jun 2021 18:53:22 +0200 Subject: [PATCH 02/20] Further documentation update with reference to phone numbers with a leading sign --- docs/topics/accessing-cells.md | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/docs/topics/accessing-cells.md b/docs/topics/accessing-cells.md index 7330a719..65538b9b 100644 --- a/docs/topics/accessing-cells.md +++ b/docs/topics/accessing-cells.md @@ -561,7 +561,9 @@ $spreadsheet->getActiveSheet()->setCellValue('B5', '21 December 1983'); Alternatively, a `\PhpOffice\PhpSpreadsheet\Cell\StringValueBinder` class is available if you want to preserve all string content as strings. This might be appropriate if you were loading a file containing values that could be interpreted as numbers (e.g. numbers -with leading zeroes such as phone numbers), but that should be retained as strings. +with leading sign such as international phone numbers like `+441615579382`), but that +should be retained as strings (non-international phone numbers with leading zeroes are +already maintained as strings). **Creating your own value binder is relatively straightforward.** When more specialised value binding is required, you can implement the From 3297f503c2854a3556088856da0912e2e620c053 Mon Sep 17 00:00:00 2001 From: MarkBaker Date: Thu, 3 Jun 2021 14:10:28 +0200 Subject: [PATCH 03/20] Addtional unit tests for vlue binders --- .../Cell/DefaultValueBinder.php | 1 + .../Cell/AdvancedValueBinderTest.php | 19 +++++++++++++++++++ tests/data/Cell/DefaultValueBinder.php | 4 ++++ 3 files changed, 24 insertions(+) diff --git a/src/PhpSpreadsheet/Cell/DefaultValueBinder.php b/src/PhpSpreadsheet/Cell/DefaultValueBinder.php index bfb8bfb2..4f2cdf78 100644 --- a/src/PhpSpreadsheet/Cell/DefaultValueBinder.php +++ b/src/PhpSpreadsheet/Cell/DefaultValueBinder.php @@ -26,6 +26,7 @@ class DefaultValueBinder implements IValueBinder if ($value instanceof DateTimeInterface) { $value = $value->format('Y-m-d H:i:s'); } elseif (!($value instanceof RichText)) { + // Attempt to cast any unexpected objects to string $value = (string) $value; } } diff --git a/tests/PhpSpreadsheetTests/Cell/AdvancedValueBinderTest.php b/tests/PhpSpreadsheetTests/Cell/AdvancedValueBinderTest.php index 7b4bbb42..eeb8ccdb 100644 --- a/tests/PhpSpreadsheetTests/Cell/AdvancedValueBinderTest.php +++ b/tests/PhpSpreadsheetTests/Cell/AdvancedValueBinderTest.php @@ -9,6 +9,7 @@ use PhpOffice\PhpSpreadsheet\Collection\Cells; use PhpOffice\PhpSpreadsheet\Shared\StringHelper; use PhpOffice\PhpSpreadsheet\Style\NumberFormat; use PhpOffice\PhpSpreadsheet\Worksheet\Worksheet; +use PHPUnit\Framework\MockObject\MockObject; use PHPUnit\Framework\TestCase; class AdvancedValueBinderTest extends TestCase @@ -42,6 +43,23 @@ class AdvancedValueBinderTest extends TestCase StringHelper::setThousandsSeparator($this->thousandsSeparator); } + public function testNullValue() + { + /** @var Cell&MockObject $cellStub */ + $cellStub = $this->getMockBuilder(Cell::class) + ->disableOriginalConstructor() + ->getMock(); + + // Configure the stub. + $cellStub->expects(self::once()) + ->method('setValueExplicit') + ->with(null, DataType::TYPE_NULL) + ->willReturn(true); + + $binder = new AdvancedValueBinder(); + $binder->bindValue($cellStub, null); + } + /** * @dataProvider currencyProvider * @@ -105,6 +123,7 @@ class AdvancedValueBinderTest extends TestCase ['€2.020,20', 2020.2, $currencyEURO, '.', ',', '€'], ['€ 2.020,20', 2020.2, $currencyEURO, '.', ',', '€'], ['€2,020.22', 2020.22, $currencyEURO, ',', '.', '€'], + ['$10.11', 10.11, $currencyUSD, ',', '.', '€'], ]; } diff --git a/tests/data/Cell/DefaultValueBinder.php b/tests/data/Cell/DefaultValueBinder.php index 1f0c40e0..35b1f4d9 100644 --- a/tests/data/Cell/DefaultValueBinder.php +++ b/tests/data/Cell/DefaultValueBinder.php @@ -77,4 +77,8 @@ return [ 's', '123456\n', ], + 'Numeric that exceeds PHP MAX_INT Size' => [ + 's', + '1234567890123459012345689012345690', + ] ]; From 642fc7dee738249d131194c0b012b64c03b400e3 Mon Sep 17 00:00:00 2001 From: MarkBaker Date: Thu, 3 Jun 2021 14:25:28 +0200 Subject: [PATCH 04/20] PHPCS and PHStan appeasement --- tests/PhpSpreadsheetTests/Cell/AdvancedValueBinderTest.php | 2 +- tests/data/Cell/DefaultValueBinder.php | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/PhpSpreadsheetTests/Cell/AdvancedValueBinderTest.php b/tests/PhpSpreadsheetTests/Cell/AdvancedValueBinderTest.php index eeb8ccdb..3fa8b69b 100644 --- a/tests/PhpSpreadsheetTests/Cell/AdvancedValueBinderTest.php +++ b/tests/PhpSpreadsheetTests/Cell/AdvancedValueBinderTest.php @@ -43,7 +43,7 @@ class AdvancedValueBinderTest extends TestCase StringHelper::setThousandsSeparator($this->thousandsSeparator); } - public function testNullValue() + public function testNullValue(): void { /** @var Cell&MockObject $cellStub */ $cellStub = $this->getMockBuilder(Cell::class) diff --git a/tests/data/Cell/DefaultValueBinder.php b/tests/data/Cell/DefaultValueBinder.php index 35b1f4d9..f5dafc7b 100644 --- a/tests/data/Cell/DefaultValueBinder.php +++ b/tests/data/Cell/DefaultValueBinder.php @@ -80,5 +80,5 @@ return [ 'Numeric that exceeds PHP MAX_INT Size' => [ 's', '1234567890123459012345689012345690', - ] + ], ]; From 8e41445fbd75b22a278b75b7b074987372a9d902 Mon Sep 17 00:00:00 2001 From: MarkBaker Date: Thu, 3 Jun 2021 19:11:31 +0200 Subject: [PATCH 05/20] Allow more control over what non-string datatypes are converted to strings in the StringValueBinder --- src/PhpSpreadsheet/Cell/StringValueBinder.php | 78 +++++- .../Cell/StringValueBinderTest.php | 225 ++++++++++++++++++ 2 files changed, 301 insertions(+), 2 deletions(-) create mode 100644 tests/PhpSpreadsheetTests/Cell/StringValueBinderTest.php diff --git a/src/PhpSpreadsheet/Cell/StringValueBinder.php b/src/PhpSpreadsheet/Cell/StringValueBinder.php index 346d0253..1c4e579c 100644 --- a/src/PhpSpreadsheet/Cell/StringValueBinder.php +++ b/src/PhpSpreadsheet/Cell/StringValueBinder.php @@ -2,10 +2,58 @@ namespace PhpOffice\PhpSpreadsheet\Cell; +use DateTimeInterface; +use PhpOffice\PhpSpreadsheet\RichText\RichText; use PhpOffice\PhpSpreadsheet\Shared\StringHelper; class StringValueBinder implements IValueBinder { + protected $convertNull = true; + + protected $convertBoolean = true; + + protected $convertNumeric = true; + + protected $convertFormula = true; + + public function setNullConversion(bool $suppressConversion = false): self + { + $this->convertNull = $suppressConversion; + + return $this; + } + + public function setBooleanConversion(bool $suppressConversion = false): self + { + $this->convertBoolean = $suppressConversion; + + return $this; + } + + public function setNumericConversion(bool $suppressConversion = false): self + { + $this->convertNumeric = $suppressConversion; + + return $this; + } + + public function setFormulaConversion(bool $suppressConversion = false): self + { + $this->convertFormula = $suppressConversion; + + return $this; + } + + public function setConversionForAllValueTypes(bool $suppressConversion = false): self + { + $this->convertNull = $suppressConversion; + $this->convertBoolean = $suppressConversion; + $this->convertNumeric = $suppressConversion; + $this->convertFormula = $suppressConversion; + + return $this; + } + /** * Bind value to a cell. * @@ -21,9 +69,35 @@ class StringValueBinder implements IValueBinder $value = StringHelper::sanitizeUTF8($value); } - $cell->setValueExplicit((string) $value, DataType::TYPE_STRING); + if (is_object($value)) { + // Handle any objects that might be injected + if ($value instanceof DateTimeInterface) { + $value = $value->format('Y-m-d H:i:s'); + } elseif ($value instanceof RichText) { + $cell->setValueExplicit((string) $value, DataType::TYPE_INLINE); + + return true; + } else { + // Attempt to cast any unexpected objects to string + $value = (string) $value; + } + } + + if ($value === null && $this->convertNull === false) { + $cell->setValueExplicit($value, DataType::TYPE_NULL); + } elseif (is_bool($value) && $this->convertBoolean === false) { + $cell->setValueExplicit($value, DataType::TYPE_BOOL); + } elseif ((is_int($value) || is_float($value)) && $this->convertNumeric === false) { + $cell->setValueExplicit($value, DataType::TYPE_NUMERIC); + } elseif (is_string($value) && strlen($value) > 1 && $value[0] === '=' && $this->convertFormula === false) { + $cell->setValueExplicit($value, DataType::TYPE_FORMULA); + } else { + if (is_string($value) && strlen($value) > 1 && $value[0] === '=') { + $cell->getStyle()->setQuotePrefix(true); + } + $cell->setValueExplicit((string) $value, DataType::TYPE_STRING); + } - // Done! return true; } } diff --git a/tests/PhpSpreadsheetTests/Cell/StringValueBinderTest.php b/tests/PhpSpreadsheetTests/Cell/StringValueBinderTest.php new file mode 100644 index 00000000..0f9b2f39 --- /dev/null +++ b/tests/PhpSpreadsheetTests/Cell/StringValueBinderTest.php @@ -0,0 +1,225 @@ +getMockBuilder(Style::class) + ->disableOriginalConstructor() + ->getMock(); + + /** @var Cell&MockObject $cellStub */ + $cellStub = $this->getMockBuilder(Cell::class) + ->disableOriginalConstructor() + ->getMock(); + + // Configure the stub. + $cellStub->expects(self::once()) + ->method('setValueExplicit') + ->with($expectedValue, $expectedDataType) + ->willReturn(true); + $cellStub->expects($quotePrefix ? self::once() : self::never()) + ->method('getStyle') + ->willReturn($styleStub); + + return $cellStub; + } + + /** + * @dataProvider providerDataValuesDefault + */ + public function testStringValueBinderDefaultBehaviour( + $value, + $expectedValue, + string $expectedDataType, + bool $quotePrefix = false + ): void { + $cellStub = $this->createCellStub($expectedValue, $expectedDataType, $quotePrefix); + + $binder = new StringValueBinder(); + $binder->bindValue($cellStub, $value); + } + + public function providerDataValuesDefault(): array + { + return [ + [null, '', DataType::TYPE_STRING], + [true, '1', DataType::TYPE_STRING], + [false, '', DataType::TYPE_STRING], + ['', '', DataType::TYPE_STRING], + ['123', '123', DataType::TYPE_STRING], + ['123.456', '123.456', DataType::TYPE_STRING], + ['0.123', '0.123', DataType::TYPE_STRING], + ['.123', '.123', DataType::TYPE_STRING], + ['-0.123', '-0.123', DataType::TYPE_STRING], + ['-.123', '-.123', DataType::TYPE_STRING], + ['1.23e-4', '1.23e-4', DataType::TYPE_STRING], + ['ABC', 'ABC', DataType::TYPE_STRING], + ['=SUM(A1:C3)', '=SUM(A1:C3)', DataType::TYPE_STRING, true], + [123, '123', DataType::TYPE_STRING], + [123.456, '123.456', DataType::TYPE_STRING], + [0.123, '0.123', DataType::TYPE_STRING], + [.123, '0.123', DataType::TYPE_STRING], + [-0.123, '-0.123', DataType::TYPE_STRING], + [-.123, '-0.123', DataType::TYPE_STRING], + [1.23e-4, '0.000123', DataType::TYPE_STRING], + [1.23e-24, '1.23E-24', DataType::TYPE_STRING], + [new DateTime('2021-06-01 00:00:00', new DateTimeZone('UTC')), '2021-06-01 00:00:00', DataType::TYPE_STRING], + ]; + } + + /** + * @dataProvider providerDataValuesSuppressNullConversion + */ + public function testStringValueBinderSuppressNullConversion( + $value, + $expectedValue, + string $expectedDataType, + bool $quotePrefix = false + ): void { + $cellStub = $this->createCellStub($expectedValue, $expectedDataType, $quotePrefix); + + $binder = new StringValueBinder(); + $binder->setNullConversion(false); + $binder->bindValue($cellStub, $value); + } + + public function providerDataValuesSuppressNullConversion(): array + { + return [ + [null, null, DataType::TYPE_NULL], + ]; + } + + /** + * @dataProvider providerDataValuesSuppressBooleanConversion + */ + public function testStringValueBinderSuppressBooleanConversion( + $value, + $expectedValue, + string $expectedDataType, + bool $quotePrefix = false + ): void { + $cellStub = $this->createCellStub($expectedValue, $expectedDataType, $quotePrefix); + + $binder = new StringValueBinder(); + $binder->setBooleanConversion(false); + $binder->bindValue($cellStub, $value); + } + + public function providerDataValuesSuppressBooleanConversion(): array + { + return [ + [true, true, DataType::TYPE_BOOL], + [false, false, DataType::TYPE_BOOL], + ]; + } + + /** + * @dataProvider providerDataValuesSuppressNumericConversion + */ + public function testStringValueBinderSuppressNumericConversion( + $value, + $expectedValue, + string $expectedDataType, + bool $quotePrefix = false + ): void { + $cellStub = $this->createCellStub($expectedValue, $expectedDataType, $quotePrefix); + + $binder = new StringValueBinder(); + $binder->setNumericConversion(false); + $binder->bindValue($cellStub, $value); + } + + public function providerDataValuesSuppressNumericConversion(): array + { + return [ + [123, 123, DataType::TYPE_NUMERIC], + [123.456, 123.456, DataType::TYPE_NUMERIC], + [0.123, 0.123, DataType::TYPE_NUMERIC], + [.123, 0.123, DataType::TYPE_NUMERIC], + [-0.123, -0.123, DataType::TYPE_NUMERIC], + [-.123, -0.123, DataType::TYPE_NUMERIC], + [1.23e-4, 0.000123, DataType::TYPE_NUMERIC], + [1.23e-24, 1.23E-24, DataType::TYPE_NUMERIC], + ]; + } + + /** + * @dataProvider providerDataValuesSuppressFormulaConversion + */ + public function testStringValueBinderSuppressFormulaConversion( + $value, + $expectedValue, + string $expectedDataType, + bool $quotePrefix = false + ): void { + $cellStub = $this->createCellStub($expectedValue, $expectedDataType, $quotePrefix); + + $binder = new StringValueBinder(); + $binder->setFormulaConversion(false); + $binder->bindValue($cellStub, $value); + } + + public function providerDataValuesSuppressFormulaConversion(): array + { + return [ + ['=SUM(A1:C3)', '=SUM(A1:C3)', DataType::TYPE_FORMULA, false], + ]; + } + + /** + * @dataProvider providerDataValuesSuppressAllConversion + */ + public function testStringValueBinderSuppressAllConversion( + $value, + $expectedValue, + string $expectedDataType, + bool $quotePrefix = false + ): void { + $cellStub = $this->createCellStub($expectedValue, $expectedDataType, $quotePrefix); + + $binder = new StringValueBinder(); + $binder->setConversionForAllValueTypes(false); + $binder->bindValue($cellStub, $value); + } + + public function providerDataValuesSuppressAllConversion(): array + { + return [ + [null, null, DataType::TYPE_NULL], + [true, true, DataType::TYPE_BOOL], + [false, false, DataType::TYPE_BOOL], + ['', '', DataType::TYPE_STRING], + ['123', '123', DataType::TYPE_STRING], + ['123.456', '123.456', DataType::TYPE_STRING], + ['0.123', '0.123', DataType::TYPE_STRING], + ['.123', '.123', DataType::TYPE_STRING], + ['-0.123', '-0.123', DataType::TYPE_STRING], + ['-.123', '-.123', DataType::TYPE_STRING], + ['1.23e-4', '1.23e-4', DataType::TYPE_STRING], + ['ABC', 'ABC', DataType::TYPE_STRING], + ['=SUM(A1:C3)', '=SUM(A1:C3)', DataType::TYPE_FORMULA, false], + [123, 123, DataType::TYPE_NUMERIC], + [123.456, 123.456, DataType::TYPE_NUMERIC], + [0.123, 0.123, DataType::TYPE_NUMERIC], + [.123, 0.123, DataType::TYPE_NUMERIC], + [-0.123, -0.123, DataType::TYPE_NUMERIC], + [-.123, -0.123, DataType::TYPE_NUMERIC], + [1.23e-4, 0.000123, DataType::TYPE_NUMERIC], + [1.23e-24, 1.23E-24, DataType::TYPE_NUMERIC], + ]; + } +} From f135da0b1112351ec42c548a5e08e059f82f35ad Mon Sep 17 00:00:00 2001 From: MarkBaker Date: Thu, 3 Jun 2021 19:23:58 +0200 Subject: [PATCH 06/20] phpstan appeasement --- src/PhpSpreadsheet/Cell/StringValueBinder.php | 12 +++++++++++ .../Cell/StringValueBinderTest.php | 21 +++++++++++++++++++ 2 files changed, 33 insertions(+) diff --git a/src/PhpSpreadsheet/Cell/StringValueBinder.php b/src/PhpSpreadsheet/Cell/StringValueBinder.php index 1c4e579c..a005f1a7 100644 --- a/src/PhpSpreadsheet/Cell/StringValueBinder.php +++ b/src/PhpSpreadsheet/Cell/StringValueBinder.php @@ -8,12 +8,24 @@ use PhpOffice\PhpSpreadsheet\Shared\StringHelper; class StringValueBinder implements IValueBinder { + /** + * @var bool $convertNull + */ protected $convertNull = true; + /** + * @var bool $convertBoolean + */ protected $convertBoolean = true; + /** + * @var bool $convertNumeric + */ protected $convertNumeric = true; + /** + * @var bool $convertFormula + */ protected $convertFormula = true; public function setNullConversion(bool $suppressConversion = false): self diff --git a/tests/PhpSpreadsheetTests/Cell/StringValueBinderTest.php b/tests/PhpSpreadsheetTests/Cell/StringValueBinderTest.php index 0f9b2f39..2455a4e9 100644 --- a/tests/PhpSpreadsheetTests/Cell/StringValueBinderTest.php +++ b/tests/PhpSpreadsheetTests/Cell/StringValueBinderTest.php @@ -13,6 +13,9 @@ use PHPUnit\Framework\TestCase; class StringValueBinderTest extends TestCase { + /** + * @param mixed $expectedValue + */ protected function createCellStub($expectedValue, string $expectedDataType, bool $quotePrefix = false): Cell { /** @var Style&MockObject $styleStub */ @@ -39,6 +42,9 @@ class StringValueBinderTest extends TestCase /** * @dataProvider providerDataValuesDefault + * + * @param mixed $value + * @param mixed $expectedValue */ public function testStringValueBinderDefaultBehaviour( $value, @@ -82,6 +88,9 @@ class StringValueBinderTest extends TestCase /** * @dataProvider providerDataValuesSuppressNullConversion + * + * @param mixed $value + * @param mixed $expectedValue */ public function testStringValueBinderSuppressNullConversion( $value, @@ -105,6 +114,9 @@ class StringValueBinderTest extends TestCase /** * @dataProvider providerDataValuesSuppressBooleanConversion + * + * @param mixed $value + * @param mixed $expectedValue */ public function testStringValueBinderSuppressBooleanConversion( $value, @@ -129,6 +141,9 @@ class StringValueBinderTest extends TestCase /** * @dataProvider providerDataValuesSuppressNumericConversion + * + * @param mixed $value + * @param mixed $expectedValue */ public function testStringValueBinderSuppressNumericConversion( $value, @@ -159,6 +174,9 @@ class StringValueBinderTest extends TestCase /** * @dataProvider providerDataValuesSuppressFormulaConversion + * + * @param mixed $value + * @param mixed $expectedValue */ public function testStringValueBinderSuppressFormulaConversion( $value, @@ -182,6 +200,9 @@ class StringValueBinderTest extends TestCase /** * @dataProvider providerDataValuesSuppressAllConversion + * + * @param mixed $value + * @param mixed $expectedValue */ public function testStringValueBinderSuppressAllConversion( $value, From 883115a079f29f52240e17bbf623f3caf778e76c Mon Sep 17 00:00:00 2001 From: MarkBaker Date: Thu, 3 Jun 2021 19:31:22 +0200 Subject: [PATCH 07/20] phpstan appeasement --- src/PhpSpreadsheet/Cell/StringValueBinder.php | 8 ++++---- tests/PhpSpreadsheetTests/Cell/StringValueBinderTest.php | 4 +++- 2 files changed, 7 insertions(+), 5 deletions(-) diff --git a/src/PhpSpreadsheet/Cell/StringValueBinder.php b/src/PhpSpreadsheet/Cell/StringValueBinder.php index a005f1a7..1b607692 100644 --- a/src/PhpSpreadsheet/Cell/StringValueBinder.php +++ b/src/PhpSpreadsheet/Cell/StringValueBinder.php @@ -9,22 +9,22 @@ use PhpOffice\PhpSpreadsheet\Shared\StringHelper; class StringValueBinder implements IValueBinder { /** - * @var bool $convertNull + * @var bool */ protected $convertNull = true; /** - * @var bool $convertBoolean + * @var bool */ protected $convertBoolean = true; /** - * @var bool $convertNumeric + * @var bool */ protected $convertNumeric = true; /** - * @var bool $convertFormula + * @var bool */ protected $convertFormula = true; diff --git a/tests/PhpSpreadsheetTests/Cell/StringValueBinderTest.php b/tests/PhpSpreadsheetTests/Cell/StringValueBinderTest.php index 2455a4e9..d772241f 100644 --- a/tests/PhpSpreadsheetTests/Cell/StringValueBinderTest.php +++ b/tests/PhpSpreadsheetTests/Cell/StringValueBinderTest.php @@ -15,8 +15,10 @@ class StringValueBinderTest extends TestCase { /** * @param mixed $expectedValue + * + * @return Cell&MockObject */ - protected function createCellStub($expectedValue, string $expectedDataType, bool $quotePrefix = false): Cell + protected function createCellStub($expectedValue, string $expectedDataType, bool $quotePrefix = false): MockObject { /** @var Style&MockObject $styleStub */ $styleStub = $this->getMockBuilder(Style::class) From 8cea3a94dfe2063c588505015c885d03bd430128 Mon Sep 17 00:00:00 2001 From: MarkBaker Date: Thu, 3 Jun 2021 20:03:53 +0200 Subject: [PATCH 08/20] Unit test for RichText object --- src/PhpSpreadsheet/Cell/StringValueBinder.php | 5 +---- .../Cell/StringValueBinderTest.php | 14 ++++++++++++++ 2 files changed, 15 insertions(+), 4 deletions(-) diff --git a/src/PhpSpreadsheet/Cell/StringValueBinder.php b/src/PhpSpreadsheet/Cell/StringValueBinder.php index 1b607692..e253e7be 100644 --- a/src/PhpSpreadsheet/Cell/StringValueBinder.php +++ b/src/PhpSpreadsheet/Cell/StringValueBinder.php @@ -86,12 +86,9 @@ class StringValueBinder implements IValueBinder if ($value instanceof DateTimeInterface) { $value = $value->format('Y-m-d H:i:s'); } elseif ($value instanceof RichText) { - $cell->setValueExplicit((string) $value, DataType::TYPE_INLINE); + $cell->setValueExplicit((string)$value, DataType::TYPE_INLINE); return true; - } else { - // Attempt to cast any unexpected objects to string - $value = (string) $value; } } diff --git a/tests/PhpSpreadsheetTests/Cell/StringValueBinderTest.php b/tests/PhpSpreadsheetTests/Cell/StringValueBinderTest.php index d772241f..1a8533aa 100644 --- a/tests/PhpSpreadsheetTests/Cell/StringValueBinderTest.php +++ b/tests/PhpSpreadsheetTests/Cell/StringValueBinderTest.php @@ -6,7 +6,9 @@ use DateTime; use DateTimeZone; use PhpOffice\PhpSpreadsheet\Cell\Cell; use PhpOffice\PhpSpreadsheet\Cell\DataType; +use PhpOffice\PhpSpreadsheet\Cell\DefaultValueBinder; use PhpOffice\PhpSpreadsheet\Cell\StringValueBinder; +use PhpOffice\PhpSpreadsheet\RichText\RichText; use PhpOffice\PhpSpreadsheet\Style\Style; use PHPUnit\Framework\MockObject\MockObject; use PHPUnit\Framework\TestCase; @@ -245,4 +247,16 @@ class StringValueBinderTest extends TestCase [1.23e-24, 1.23E-24, DataType::TYPE_NUMERIC], ]; } + + public function testStringValueBinderForRichTextObject(): void + { + $objRichText = new RichText(); + $objRichText->createText('Hello World'); + + $cellStub = $this->createCellStub($objRichText, DataType::TYPE_INLINE); + + $binder = new StringValueBinder(); + $binder->setConversionForAllValueTypes(false); + $binder->bindValue($cellStub, $objRichText); + } } From da9fbd6c8d80e8b25ff7481c5ae85adb1ff63bfa Mon Sep 17 00:00:00 2001 From: MarkBaker Date: Thu, 3 Jun 2021 20:12:26 +0200 Subject: [PATCH 09/20] PHPCS appeasement again --- src/PhpSpreadsheet/Cell/StringValueBinder.php | 2 +- tests/PhpSpreadsheetTests/Cell/StringValueBinderTest.php | 1 - 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/src/PhpSpreadsheet/Cell/StringValueBinder.php b/src/PhpSpreadsheet/Cell/StringValueBinder.php index e253e7be..6c25bda0 100644 --- a/src/PhpSpreadsheet/Cell/StringValueBinder.php +++ b/src/PhpSpreadsheet/Cell/StringValueBinder.php @@ -86,7 +86,7 @@ class StringValueBinder implements IValueBinder if ($value instanceof DateTimeInterface) { $value = $value->format('Y-m-d H:i:s'); } elseif ($value instanceof RichText) { - $cell->setValueExplicit((string)$value, DataType::TYPE_INLINE); + $cell->setValueExplicit($value, DataType::TYPE_INLINE); return true; } diff --git a/tests/PhpSpreadsheetTests/Cell/StringValueBinderTest.php b/tests/PhpSpreadsheetTests/Cell/StringValueBinderTest.php index 1a8533aa..411af353 100644 --- a/tests/PhpSpreadsheetTests/Cell/StringValueBinderTest.php +++ b/tests/PhpSpreadsheetTests/Cell/StringValueBinderTest.php @@ -6,7 +6,6 @@ use DateTime; use DateTimeZone; use PhpOffice\PhpSpreadsheet\Cell\Cell; use PhpOffice\PhpSpreadsheet\Cell\DataType; -use PhpOffice\PhpSpreadsheet\Cell\DefaultValueBinder; use PhpOffice\PhpSpreadsheet\Cell\StringValueBinder; use PhpOffice\PhpSpreadsheet\RichText\RichText; use PhpOffice\PhpSpreadsheet\Style\Style; From af85f888bea752d6d7e5ff88d5cbe34fafd82bf5 Mon Sep 17 00:00:00 2001 From: MarkBaker Date: Thu, 3 Jun 2021 20:26:57 +0200 Subject: [PATCH 10/20] Now it's Scrutinizer's turn --- src/PhpSpreadsheet/Cell/StringValueBinder.php | 33 +++++++++++-------- 1 file changed, 20 insertions(+), 13 deletions(-) diff --git a/src/PhpSpreadsheet/Cell/StringValueBinder.php b/src/PhpSpreadsheet/Cell/StringValueBinder.php index 6c25bda0..c635db2c 100644 --- a/src/PhpSpreadsheet/Cell/StringValueBinder.php +++ b/src/PhpSpreadsheet/Cell/StringValueBinder.php @@ -71,27 +71,18 @@ class StringValueBinder implements IValueBinder * * @param Cell $cell Cell to bind value to * @param mixed $value Value to bind in cell - * - * @return bool */ public function bindValue(Cell $cell, $value) { + if (is_object($value)) { + return $this->bindObjectValue($cell, $value); + } + // sanitize UTF-8 strings if (is_string($value)) { $value = StringHelper::sanitizeUTF8($value); } - if (is_object($value)) { - // Handle any objects that might be injected - if ($value instanceof DateTimeInterface) { - $value = $value->format('Y-m-d H:i:s'); - } elseif ($value instanceof RichText) { - $cell->setValueExplicit($value, DataType::TYPE_INLINE); - - return true; - } - } - if ($value === null && $this->convertNull === false) { $cell->setValueExplicit($value, DataType::TYPE_NULL); } elseif (is_bool($value) && $this->convertBoolean === false) { @@ -109,4 +100,20 @@ class StringValueBinder implements IValueBinder return true; } + + protected function bindObjectValue(Cell $cell, $value): bool + { + // Handle any objects that might be injected + if ($value instanceof DateTimeInterface) { + $value = $value->format('Y-m-d H:i:s'); + } elseif ($value instanceof RichText) { + $cell->setValueExplicit($value, DataType::TYPE_INLINE); + + return true; + } + + $cell->setValueExplicit((string) $value, DataType::TYPE_STRING); + + return true; + } } From 504ed9a87c3bdf1bf87b589e306aab7bc5f34882 Mon Sep 17 00:00:00 2001 From: MarkBaker Date: Thu, 3 Jun 2021 20:53:02 +0200 Subject: [PATCH 11/20] Ok! Let's try again with phpstan now --- src/PhpSpreadsheet/Cell/StringValueBinder.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/PhpSpreadsheet/Cell/StringValueBinder.php b/src/PhpSpreadsheet/Cell/StringValueBinder.php index c635db2c..5b6e3db1 100644 --- a/src/PhpSpreadsheet/Cell/StringValueBinder.php +++ b/src/PhpSpreadsheet/Cell/StringValueBinder.php @@ -101,7 +101,7 @@ class StringValueBinder implements IValueBinder return true; } - protected function bindObjectValue(Cell $cell, $value): bool + protected function bindObjectValue(Cell $cell, object $value): bool { // Handle any objects that might be injected if ($value instanceof DateTimeInterface) { From 488701b748d19626a20bab47c4255c6ff984f9fd Mon Sep 17 00:00:00 2001 From: MarkBaker Date: Thu, 3 Jun 2021 21:32:35 +0200 Subject: [PATCH 12/20] Update documentation with details of changes to the StringValueBinder --- CHANGELOG.md | 2 +- docs/topics/accessing-cells.md | 14 +++++++++++++- 2 files changed, 14 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 5e9eec11..ba74ff7a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,7 +9,7 @@ and this project adheres to [Semantic Versioning](https://semver.org). ### Added -- Nothing. +- More flexibility in the StringValueBinder to determine what datatypes should be treated as strings [PR #2138](https://github.com/PHPOffice/PhpSpreadsheet/pull/2138) ### Changed diff --git a/docs/topics/accessing-cells.md b/docs/topics/accessing-cells.md index 65538b9b..346e5858 100644 --- a/docs/topics/accessing-cells.md +++ b/docs/topics/accessing-cells.md @@ -559,12 +559,24 @@ $spreadsheet->getActiveSheet()->setCellValue('B5', '21 December 1983'); ``` Alternatively, a `\PhpOffice\PhpSpreadsheet\Cell\StringValueBinder` class is available -if you want to preserve all string content as strings. This might be appropriate if you +if you want to preserve all content as strings. This might be appropriate if you were loading a file containing values that could be interpreted as numbers (e.g. numbers with leading sign such as international phone numbers like `+441615579382`), but that should be retained as strings (non-international phone numbers with leading zeroes are already maintained as strings). +By default, the StringValueBinder will cast any datatype passed to it into a string. However, there are a number of settings which allow you to specify that certain datatypes shouldn't be cast to strings, but left "as is": + +```php +// Set value binder +$stringValueBinder = new \PhpOffice\PhpSpreadsheet\Cell\StringValueBinder(); +$stringValueBinder->setNumericConversion(false) + ->setBooleanConversion(false) + ->setNullConversion(false) + ->setFormulaConversion(false); +\PhpOffice\PhpSpreadsheet\Cell\Cell::setValueBinder( $stringValueBinder ); +``` + **Creating your own value binder is relatively straightforward.** When more specialised value binding is required, you can implement the `\PhpOffice\PhpSpreadsheet\Cell\IValueBinder` interface or extend the existing From 19724e32175cd1b7a5c9d4205eb6b4129d35a6ad Mon Sep 17 00:00:00 2001 From: Mark Baker Date: Fri, 4 Jun 2021 13:45:32 +0200 Subject: [PATCH 13/20] Reader writer flags (#2136) * Use of passing flags with Readers to identify whether speacial features such as loading charts should be enabled; no need to instantiate a reader and manually enable it before loading any more. This is in preparation for supporting new "boolean" Reaer/Writer features, such as pivot tables * Use of passing flags with Writers to identify whether speacial features such as loading charts should be enabled; no need to instantiate a writer and manually enable it before loading any more. * Update documentation with details of changes to the StringValueBinder --- CHANGELOG.md | 2 + docs/topics/reading-and-writing-to-file.md | 70 +++++++++++++++++-- phpstan-baseline.neon | 5 -- src/PhpSpreadsheet/IOFactory.php | 8 +-- src/PhpSpreadsheet/Reader/BaseReader.php | 7 ++ src/PhpSpreadsheet/Reader/Csv.php | 6 +- src/PhpSpreadsheet/Reader/Gnumeric.php | 6 +- src/PhpSpreadsheet/Reader/Html.php | 6 +- src/PhpSpreadsheet/Reader/IReader.php | 6 +- src/PhpSpreadsheet/Reader/Ods.php | 6 +- src/PhpSpreadsheet/Reader/Slk.php | 6 +- src/PhpSpreadsheet/Reader/Xls.php | 6 +- src/PhpSpreadsheet/Reader/Xlsx.php | 5 +- src/PhpSpreadsheet/Reader/Xml.php | 8 +-- src/PhpSpreadsheet/Writer/BaseWriter.php | 9 ++- src/PhpSpreadsheet/Writer/Csv.php | 4 +- src/PhpSpreadsheet/Writer/Html.php | 4 +- src/PhpSpreadsheet/Writer/IWriter.php | 4 +- src/PhpSpreadsheet/Writer/Ods.php | 4 +- src/PhpSpreadsheet/Writer/Pdf/Dompdf.php | 2 +- src/PhpSpreadsheet/Writer/Pdf/Mpdf.php | 2 +- src/PhpSpreadsheet/Writer/Pdf/Tcpdf.php | 2 +- src/PhpSpreadsheet/Writer/Xls.php | 4 +- src/PhpSpreadsheet/Writer/Xlsx.php | 4 +- .../Reader/SheetsXlsxChartTest.php | 5 +- 25 files changed, 136 insertions(+), 55 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index ba74ff7a..82289a0b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,8 @@ and this project adheres to [Semantic Versioning](https://semver.org). ### Added +- Support for passing flags in the Reader `load()` and Writer `save()`methods, and through the IOFactory, to set behaviours. [PR #2136](https://github.com/PHPOffice/PhpSpreadsheet/pull/2136) + - See [documentation](https://phpspreadsheet.readthedocs.io/en/latest/topics/reading-and-writing-to-file/) for details - More flexibility in the StringValueBinder to determine what datatypes should be treated as strings [PR #2138](https://github.com/PHPOffice/PhpSpreadsheet/pull/2138) ### Changed diff --git a/docs/topics/reading-and-writing-to-file.md b/docs/topics/reading-and-writing-to-file.md index e5c2afd9..cb1e0e77 100644 --- a/docs/topics/reading-and-writing-to-file.md +++ b/docs/topics/reading-and-writing-to-file.md @@ -1,8 +1,7 @@ # Reading and writing to file As you already know from the [architecture](./architecture.md#readers-and-writers), -reading and writing to a -persisted storage is not possible using the base PhpSpreadsheet classes. +reading and writing to a persisted storage is not possible using the base PhpSpreadsheet classes. For this purpose, PhpSpreadsheet provides readers and writers, which are implementations of `\PhpOffice\PhpSpreadsheet\Reader\IReader` and `\PhpOffice\PhpSpreadsheet\Writer\IWriter`. @@ -892,8 +891,7 @@ class My_Custom_TCPDF_Writer extends \PhpOffice\PhpSpreadsheet\Writer\Pdf\Tcpdf #### Writing a spreadsheet -Once you have identified the Renderer that you wish to use for PDF -generation, you can write a .pdf file using the following code: +Once you have identified the Renderer that you wish to use for PDF generation, you can write a .pdf file using the following code: ```php $writer = new \PhpOffice\PhpSpreadsheet\Writer\Pdf\Mpdf($spreadsheet); @@ -905,8 +903,7 @@ first worksheet by default. #### Write all worksheets -PDF files can contain one or more worksheets. If you want to write all -sheets into a single PDF file, use the following code: +PDF files can contain one or more worksheets. If you want to write all sheets into a single PDF file, use the following code: ```php $writer->writeAllSheets(); @@ -1020,3 +1017,64 @@ $spreadhseet = $reader->loadFromString($secondHtmlString, $spreadsheet); $writer = \PhpOffice\PhpSpreadsheet\IOFactory::createWriter($spreadsheet, 'Xls'); $writer->save('write.xls'); ``` + +## Reader/Writer Flags + +Some Readers and Writers support special "Feature Flags" that need to be explicitly enabled. +An example of this is Charts in a spreadsheet. By default, when you load a spreadsheet that contains Charts, the charts will not be loaded. If all you want to do is read the data in the spreadsheet, then loading charts is an overhead for both speed of loading and memory usage. +However, there are times when you may want to load any charts in the spreadsheet as well as the data. To do so, you need to tell the Reader explicitly to include Charts. + +```php +$reader = \PhpOffice\PhpSpreadsheet\IOFactory::createReaderForFile("05featuredemo.xlsx"); +$reader->setIncludeCharts(true); +$reader->load("spreadsheetWithCharts.xlsx"); +``` +Alternatively, you can specify this in the call to load the spreadsheet: +```php +$reader = \PhpOffice\PhpSpreadsheet\IOFactory::createReaderForFile("spreadsheetWithCharts.xlsx"); +$reader->load("spreadsheetWithCharts.xlsx", $reader::LOAD_WITH_CHARTS); +``` + +If you wish to use the IOFactory `load()` method rather than instantiating a specific Reader, then you can still pass these flags. + +```php +$spreadsheet = \PhpOffice\PhpSpreadsheet\IOFactory::load("spreadsheetWithCharts.xlsx", \PhpOffice\PhpSpreadsheet\Reader\IReader::LOAD_WITH_CHARTS); +``` + +Likewise, when saving a file using a Writer, loaded charts wil not be saved unless you explicitly tell the Writer to include them: + +```php +$writer = IOFactory::createWriter($spreadsheet, 'Xlsx'); +$writer->setIncludeCharts(true); +$writer->save('mySavedFileWithCharts.xlsx'); +``` + +As with the `load()` method, you can also pass flags in the `save()` method: +```php +$writer = IOFactory::createWriter($spreadsheet, 'Xlsx'); +$writer->save('mySavedFileWithCharts.xlsx', \PhpOffice\PhpSpreadsheet\Writer\IWriter::SAVE_WITH_CHARTS); +``` + +Currently, the only special "Feature Flag" that is supported in this way is the inclusion of Charts, and only for certain formats. + +Readers | LOAD_WITH_CHARTS | +---------|------------------| +Xlsx | YES | +Xls | NO | +Xml | NO | +Ods | NO | +Gnumeric | NO | +Html | N/A | +Slk | N/A | +Csv | N/A | + + +Writers | SAVE_WITH_CHARTS | +--------|------------------| +Xlsx | YES | +Xls | NO | +Ods | NO | +Html | YES | +Pdf | YES | +Csv | N/A | + diff --git a/phpstan-baseline.neon b/phpstan-baseline.neon index 4ee0a9ef..4d4f7193 100644 --- a/phpstan-baseline.neon +++ b/phpstan-baseline.neon @@ -6245,11 +6245,6 @@ parameters: count: 1 path: src/PhpSpreadsheet/Writer/Html.php - - - message: "#^Method PhpOffice\\\\PhpSpreadsheet\\\\Writer\\\\IWriter\\:\\:save\\(\\) has no return typehint specified\\.$#" - count: 1 - path: src/PhpSpreadsheet/Writer/IWriter.php - - message: "#^Negated boolean expression is always false\\.$#" count: 1 diff --git a/src/PhpSpreadsheet/IOFactory.php b/src/PhpSpreadsheet/IOFactory.php index 06006edc..6ed1fd82 100644 --- a/src/PhpSpreadsheet/IOFactory.php +++ b/src/PhpSpreadsheet/IOFactory.php @@ -75,15 +75,15 @@ abstract class IOFactory /** * Loads Spreadsheet from file using automatic Reader\IReader resolution. * - * @param string $pFilename The name of the spreadsheet file + * @param string $filename The name of the spreadsheet file * * @return Spreadsheet */ - public static function load($pFilename) + public static function load(string $filename, int $flags = 0) { - $reader = self::createReaderForFile($pFilename); + $reader = self::createReaderForFile($filename); - return $reader->load($pFilename); + return $reader->load($filename, $flags); } /** diff --git a/src/PhpSpreadsheet/Reader/BaseReader.php b/src/PhpSpreadsheet/Reader/BaseReader.php index 80348132..1f18484f 100644 --- a/src/PhpSpreadsheet/Reader/BaseReader.php +++ b/src/PhpSpreadsheet/Reader/BaseReader.php @@ -137,6 +137,13 @@ abstract class BaseReader implements IReader return $this->securityScanner; } + protected function processFlags(int $flags): void + { + if (((bool) ($flags & self::LOAD_WITH_CHARTS)) === true) { + $this->setIncludeCharts(true); + } + } + /** * Open file for reading. * diff --git a/src/PhpSpreadsheet/Reader/Csv.php b/src/PhpSpreadsheet/Reader/Csv.php index b7bc0d49..f06de635 100644 --- a/src/PhpSpreadsheet/Reader/Csv.php +++ b/src/PhpSpreadsheet/Reader/Csv.php @@ -236,12 +236,12 @@ class Csv extends BaseReader /** * Loads Spreadsheet from file. * - * @param string $pFilename - * * @return Spreadsheet */ - public function load($pFilename) + public function load(string $pFilename, int $flags = 0) { + $this->processFlags($flags); + // Create new Spreadsheet $spreadsheet = new Spreadsheet(); diff --git a/src/PhpSpreadsheet/Reader/Gnumeric.php b/src/PhpSpreadsheet/Reader/Gnumeric.php index 85bae6f8..64c27366 100644 --- a/src/PhpSpreadsheet/Reader/Gnumeric.php +++ b/src/PhpSpreadsheet/Reader/Gnumeric.php @@ -235,12 +235,12 @@ class Gnumeric extends BaseReader /** * Loads Spreadsheet from file. * - * @param string $pFilename - * * @return Spreadsheet */ - public function load($pFilename) + public function load(string $pFilename, int $flags = 0) { + $this->processFlags($flags); + // Create new Spreadsheet $spreadsheet = new Spreadsheet(); $spreadsheet->removeSheetByIndex(0); diff --git a/src/PhpSpreadsheet/Reader/Html.php b/src/PhpSpreadsheet/Reader/Html.php index b7faac87..6e6155c2 100644 --- a/src/PhpSpreadsheet/Reader/Html.php +++ b/src/PhpSpreadsheet/Reader/Html.php @@ -204,12 +204,12 @@ class Html extends BaseReader /** * Loads Spreadsheet from file. * - * @param string $pFilename - * * @return Spreadsheet */ - public function load($pFilename) + public function load(string $pFilename, int $flags = 0) { + $this->processFlags($flags); + // Create new Spreadsheet $spreadsheet = new Spreadsheet(); diff --git a/src/PhpSpreadsheet/Reader/IReader.php b/src/PhpSpreadsheet/Reader/IReader.php index a8bd3606..6a57f4a0 100644 --- a/src/PhpSpreadsheet/Reader/IReader.php +++ b/src/PhpSpreadsheet/Reader/IReader.php @@ -4,6 +4,8 @@ namespace PhpOffice\PhpSpreadsheet\Reader; interface IReader { + public const LOAD_WITH_CHARTS = 1; + /** * IReader constructor. */ @@ -125,9 +127,7 @@ interface IReader /** * Loads PhpSpreadsheet from file. * - * @param string $pFilename - * * @return \PhpOffice\PhpSpreadsheet\Spreadsheet */ - public function load($pFilename); + public function load(string $pFilename, int $flags = 0); } diff --git a/src/PhpSpreadsheet/Reader/Ods.php b/src/PhpSpreadsheet/Reader/Ods.php index 760aa10d..b39fffed 100644 --- a/src/PhpSpreadsheet/Reader/Ods.php +++ b/src/PhpSpreadsheet/Reader/Ods.php @@ -231,12 +231,12 @@ class Ods extends BaseReader /** * Loads PhpSpreadsheet from file. * - * @param string $pFilename - * * @return Spreadsheet */ - public function load($pFilename) + public function load(string $pFilename, int $flags = 0) { + $this->processFlags($flags); + // Create new Spreadsheet $spreadsheet = new Spreadsheet(); diff --git a/src/PhpSpreadsheet/Reader/Slk.php b/src/PhpSpreadsheet/Reader/Slk.php index c7b6fc82..b58fdcba 100644 --- a/src/PhpSpreadsheet/Reader/Slk.php +++ b/src/PhpSpreadsheet/Reader/Slk.php @@ -197,12 +197,12 @@ class Slk extends BaseReader /** * Loads PhpSpreadsheet from file. * - * @param string $pFilename - * * @return Spreadsheet */ - public function load($pFilename) + public function load(string $pFilename, int $flags = 0) { + $this->processFlags($flags); + // Create new Spreadsheet $spreadsheet = new Spreadsheet(); diff --git a/src/PhpSpreadsheet/Reader/Xls.php b/src/PhpSpreadsheet/Reader/Xls.php index bfdd5583..a8c097f7 100644 --- a/src/PhpSpreadsheet/Reader/Xls.php +++ b/src/PhpSpreadsheet/Reader/Xls.php @@ -622,12 +622,12 @@ class Xls extends BaseReader /** * Loads PhpSpreadsheet from file. * - * @param string $pFilename - * * @return Spreadsheet */ - public function load($pFilename) + public function load(string $pFilename, int $flags = 0) { + $this->processFlags($flags); + // Read the OLE file $this->loadOLE($pFilename); diff --git a/src/PhpSpreadsheet/Reader/Xlsx.php b/src/PhpSpreadsheet/Reader/Xlsx.php index 82e4e82d..8adb3bc8 100644 --- a/src/PhpSpreadsheet/Reader/Xlsx.php +++ b/src/PhpSpreadsheet/Reader/Xlsx.php @@ -310,13 +310,12 @@ class Xlsx extends BaseReader /** * Loads Spreadsheet from file. * - * @param string $pFilename - * * @return Spreadsheet */ - public function load($pFilename) + public function load(string $pFilename, int $flags = 0) { File::assertFile($pFilename); + $this->processFlags($flags); // Initialisations $excel = new Spreadsheet(); diff --git a/src/PhpSpreadsheet/Reader/Xml.php b/src/PhpSpreadsheet/Reader/Xml.php index 4ef4efe7..f63bc798 100644 --- a/src/PhpSpreadsheet/Reader/Xml.php +++ b/src/PhpSpreadsheet/Reader/Xml.php @@ -236,18 +236,18 @@ class Xml extends BaseReader /** * Loads Spreadsheet from file. * - * @param string $filename - * * @return Spreadsheet */ - public function load($filename) + public function load(string $pFilename, int $flags = 0) { + $this->processFlags($flags); + // Create new Spreadsheet $spreadsheet = new Spreadsheet(); $spreadsheet->removeSheetByIndex(0); // Load into this instance - return $this->loadIntoExisting($filename, $spreadsheet); + return $this->loadIntoExisting($pFilename, $spreadsheet); } /** diff --git a/src/PhpSpreadsheet/Writer/BaseWriter.php b/src/PhpSpreadsheet/Writer/BaseWriter.php index afda5c43..3b03cfc3 100644 --- a/src/PhpSpreadsheet/Writer/BaseWriter.php +++ b/src/PhpSpreadsheet/Writer/BaseWriter.php @@ -6,7 +6,7 @@ abstract class BaseWriter implements IWriter { /** * Write charts that are defined in the workbook? - * Identifies whether the Writer should write definitions for any charts that exist in the PhpSpreadsheet object;. + * Identifies whether the Writer should write definitions for any charts that exist in the PhpSpreadsheet object. * * @var bool */ @@ -94,6 +94,13 @@ abstract class BaseWriter implements IWriter return $this->diskCachingDirectory; } + protected function processFlags(int $flags): void + { + if (((bool) ($flags & self::SAVE_WITH_CHARTS)) === true) { + $this->setIncludeCharts(true); + } + } + /** * Open file handle. * diff --git a/src/PhpSpreadsheet/Writer/Csv.php b/src/PhpSpreadsheet/Writer/Csv.php index 188a83a8..79e8e5f1 100644 --- a/src/PhpSpreadsheet/Writer/Csv.php +++ b/src/PhpSpreadsheet/Writer/Csv.php @@ -86,8 +86,10 @@ class Csv extends BaseWriter * * @param resource|string $pFilename */ - public function save($pFilename): void + public function save($pFilename, int $flags = 0): void { + $this->processFlags($flags); + // Fetch sheet $sheet = $this->spreadsheet->getSheet($this->sheetIndex); diff --git a/src/PhpSpreadsheet/Writer/Html.php b/src/PhpSpreadsheet/Writer/Html.php index b694d4ef..76c5e722 100644 --- a/src/PhpSpreadsheet/Writer/Html.php +++ b/src/PhpSpreadsheet/Writer/Html.php @@ -153,8 +153,10 @@ class Html extends BaseWriter * * @param resource|string $pFilename */ - public function save($pFilename): void + public function save($pFilename, int $flags = 0): void { + $this->processFlags($flags); + // Open file $this->openFileHandle($pFilename); diff --git a/src/PhpSpreadsheet/Writer/IWriter.php b/src/PhpSpreadsheet/Writer/IWriter.php index 5129d655..a0361f74 100644 --- a/src/PhpSpreadsheet/Writer/IWriter.php +++ b/src/PhpSpreadsheet/Writer/IWriter.php @@ -6,6 +6,8 @@ use PhpOffice\PhpSpreadsheet\Spreadsheet; interface IWriter { + public const SAVE_WITH_CHARTS = 1; + /** * IWriter constructor. */ @@ -59,7 +61,7 @@ interface IWriter * * @param resource|string $pFilename Name of the file to save */ - public function save($pFilename); + public function save($pFilename, int $flags = 0): void; /** * Get use disk caching where possible? diff --git a/src/PhpSpreadsheet/Writer/Ods.php b/src/PhpSpreadsheet/Writer/Ods.php index f07ade9a..141a4119 100644 --- a/src/PhpSpreadsheet/Writer/Ods.php +++ b/src/PhpSpreadsheet/Writer/Ods.php @@ -115,12 +115,14 @@ class Ods extends BaseWriter * * @param resource|string $pFilename */ - public function save($pFilename): void + public function save($pFilename, int $flags = 0): void { if (!$this->spreadSheet) { throw new WriterException('PhpSpreadsheet object unassigned.'); } + $this->processFlags($flags); + // garbage collect $this->spreadSheet->garbageCollect(); diff --git a/src/PhpSpreadsheet/Writer/Pdf/Dompdf.php b/src/PhpSpreadsheet/Writer/Pdf/Dompdf.php index 87e8eeb5..313b34e8 100644 --- a/src/PhpSpreadsheet/Writer/Pdf/Dompdf.php +++ b/src/PhpSpreadsheet/Writer/Pdf/Dompdf.php @@ -22,7 +22,7 @@ class Dompdf extends Pdf * * @param string $pFilename Name of the file to save as */ - public function save($pFilename): void + public function save($pFilename, int $flags = 0): void { $fileHandle = parent::prepareForSave($pFilename); diff --git a/src/PhpSpreadsheet/Writer/Pdf/Mpdf.php b/src/PhpSpreadsheet/Writer/Pdf/Mpdf.php index 56ac6930..0db164b6 100644 --- a/src/PhpSpreadsheet/Writer/Pdf/Mpdf.php +++ b/src/PhpSpreadsheet/Writer/Pdf/Mpdf.php @@ -24,7 +24,7 @@ class Mpdf extends Pdf * * @param string $pFilename Name of the file to save as */ - public function save($pFilename): void + public function save($pFilename, int $flags = 0): void { $fileHandle = parent::prepareForSave($pFilename); diff --git a/src/PhpSpreadsheet/Writer/Pdf/Tcpdf.php b/src/PhpSpreadsheet/Writer/Pdf/Tcpdf.php index 56e917e3..76f983a0 100644 --- a/src/PhpSpreadsheet/Writer/Pdf/Tcpdf.php +++ b/src/PhpSpreadsheet/Writer/Pdf/Tcpdf.php @@ -38,7 +38,7 @@ class Tcpdf extends Pdf * * @param string $pFilename Name of the file to save as */ - public function save($pFilename): void + public function save($pFilename, int $flags = 0): void { $fileHandle = parent::prepareForSave($pFilename); diff --git a/src/PhpSpreadsheet/Writer/Xls.php b/src/PhpSpreadsheet/Writer/Xls.php index 624dc414..fa782755 100644 --- a/src/PhpSpreadsheet/Writer/Xls.php +++ b/src/PhpSpreadsheet/Writer/Xls.php @@ -119,8 +119,10 @@ class Xls extends BaseWriter * * @param resource|string $pFilename */ - public function save($pFilename): void + public function save($pFilename, int $flags = 0): void { + $this->processFlags($flags); + // garbage collect $this->spreadsheet->garbageCollect(); diff --git a/src/PhpSpreadsheet/Writer/Xlsx.php b/src/PhpSpreadsheet/Writer/Xlsx.php index 27ceb559..6f43989f 100644 --- a/src/PhpSpreadsheet/Writer/Xlsx.php +++ b/src/PhpSpreadsheet/Writer/Xlsx.php @@ -286,8 +286,10 @@ class Xlsx extends BaseWriter * * @param resource|string $pFilename */ - public function save($pFilename): void + public function save($pFilename, int $flags = 0): void { + $this->processFlags($flags); + // garbage collect $this->pathNames = []; $this->spreadSheet->garbageCollect(); diff --git a/tests/PhpSpreadsheetTests/Reader/SheetsXlsxChartTest.php b/tests/PhpSpreadsheetTests/Reader/SheetsXlsxChartTest.php index c4dc328d..9c478c4a 100644 --- a/tests/PhpSpreadsheetTests/Reader/SheetsXlsxChartTest.php +++ b/tests/PhpSpreadsheetTests/Reader/SheetsXlsxChartTest.php @@ -4,6 +4,7 @@ namespace PhpOffice\PhpSpreadsheetTests\Reader; use PhpOffice\PhpSpreadsheet\Chart\DataSeries; use PhpOffice\PhpSpreadsheet\IOFactory; +use PhpOffice\PhpSpreadsheet\Reader\IReader; use PHPUnit\Framework\TestCase; class SheetsXlsxChartTest extends TestCase @@ -11,8 +12,8 @@ class SheetsXlsxChartTest extends TestCase public function testLoadSheetsXlsxChart(): void { $filename = 'tests/data/Reader/XLSX/sheetsChartsTest.xlsx'; - $reader = IOFactory::createReader('Xlsx')->setIncludeCharts(true); - $spreadsheet = $reader->load($filename); + $reader = IOFactory::createReader('Xlsx'); + $spreadsheet = $reader->load($filename, IReader::LOAD_WITH_CHARTS); $worksheet = $spreadsheet->getActiveSheet(); $charts = $worksheet->getChartCollection(); From a340240a3fd3e793011e8a16da52e2f3fd3c42e4 Mon Sep 17 00:00:00 2001 From: oleibman Date: Sat, 5 Jun 2021 06:14:23 -0700 Subject: [PATCH 14/20] PHP8.1 Deprecation Passing Null to String Function (#2137) For each of the files in this PR, one or more statements can pass a null to string functions like strlower. This is deprecated in PHP8.1, and, when deprecated messages are enabled, causes many tests to error out. In every case, use coercion to pass null string rather than null. --- .../Calculation/Database/DatabaseAbstract.php | 2 +- src/PhpSpreadsheet/Calculation/DateTimeExcel/DateValue.php | 2 +- src/PhpSpreadsheet/Calculation/DateTimeExcel/TimeValue.php | 2 +- src/PhpSpreadsheet/Calculation/TextData/Extract.php | 6 +++--- src/PhpSpreadsheet/Calculation/TextData/Text.php | 2 +- src/PhpSpreadsheet/Reader/Html.php | 2 +- src/PhpSpreadsheet/Reader/Xml/PageSettings.php | 2 +- src/PhpSpreadsheet/Shared/StringHelper.php | 2 +- src/PhpSpreadsheet/Writer/Xlsx/Worksheet.php | 2 +- tests/PhpSpreadsheetTests/Calculation/FunctionsTest.php | 2 +- 10 files changed, 12 insertions(+), 12 deletions(-) diff --git a/src/PhpSpreadsheet/Calculation/Database/DatabaseAbstract.php b/src/PhpSpreadsheet/Calculation/Database/DatabaseAbstract.php index 2c9bb2de..b2b0e669 100644 --- a/src/PhpSpreadsheet/Calculation/Database/DatabaseAbstract.php +++ b/src/PhpSpreadsheet/Calculation/Database/DatabaseAbstract.php @@ -27,7 +27,7 @@ abstract class DatabaseAbstract */ protected static function fieldExtract(array $database, $field): ?int { - $field = strtoupper(Functions::flattenSingleValue($field)); + $field = strtoupper(Functions::flattenSingleValue($field ?? '')); if ($field === '') { return null; } diff --git a/src/PhpSpreadsheet/Calculation/DateTimeExcel/DateValue.php b/src/PhpSpreadsheet/Calculation/DateTimeExcel/DateValue.php index de767fca..d8e88a21 100644 --- a/src/PhpSpreadsheet/Calculation/DateTimeExcel/DateValue.php +++ b/src/PhpSpreadsheet/Calculation/DateTimeExcel/DateValue.php @@ -37,7 +37,7 @@ class DateValue { $dti = new DateTimeImmutable(); $baseYear = SharedDateHelper::getExcelCalendar(); - $dateValue = trim(Functions::flattenSingleValue($dateValue), '"'); + $dateValue = trim(Functions::flattenSingleValue($dateValue ?? ''), '"'); // Strip any ordinals because they're allowed in Excel (English only) $dateValue = preg_replace('/(\d)(st|nd|rd|th)([ -\/])/Ui', '$1$3', $dateValue) ?? ''; // Convert separators (/ . or space) to hyphens (should also handle dot used for ordinals in some countries, e.g. Denmark, Germany) diff --git a/src/PhpSpreadsheet/Calculation/DateTimeExcel/TimeValue.php b/src/PhpSpreadsheet/Calculation/DateTimeExcel/TimeValue.php index 73b7ba91..2a294344 100644 --- a/src/PhpSpreadsheet/Calculation/DateTimeExcel/TimeValue.php +++ b/src/PhpSpreadsheet/Calculation/DateTimeExcel/TimeValue.php @@ -31,7 +31,7 @@ class TimeValue */ public static function fromString($timeValue) { - $timeValue = trim(Functions::flattenSingleValue($timeValue), '"'); + $timeValue = trim(Functions::flattenSingleValue($timeValue ?? ''), '"'); $timeValue = str_replace(['/', '.'], '-', $timeValue); $arraySplit = preg_split('/[\/:\-\s]/', $timeValue) ?: []; diff --git a/src/PhpSpreadsheet/Calculation/TextData/Extract.php b/src/PhpSpreadsheet/Calculation/TextData/Extract.php index 2f994858..015fabfb 100644 --- a/src/PhpSpreadsheet/Calculation/TextData/Extract.php +++ b/src/PhpSpreadsheet/Calculation/TextData/Extract.php @@ -26,7 +26,7 @@ class Extract $value = ($value) ? Calculation::getTRUE() : Calculation::getFALSE(); } - return mb_substr($value, 0, $chars, 'UTF-8'); + return mb_substr($value ?? '', 0, $chars, 'UTF-8'); } /** @@ -50,7 +50,7 @@ class Extract $value = ($value) ? Calculation::getTRUE() : Calculation::getFALSE(); } - return mb_substr($value, --$start, $chars, 'UTF-8'); + return mb_substr($value ?? '', --$start, $chars, 'UTF-8'); } /** @@ -72,6 +72,6 @@ class Extract $value = ($value) ? Calculation::getTRUE() : Calculation::getFALSE(); } - return mb_substr($value, mb_strlen($value, 'UTF-8') - $chars, $chars, 'UTF-8'); + return mb_substr($value ?? '', mb_strlen($value ?? '', 'UTF-8') - $chars, $chars, 'UTF-8'); } } diff --git a/src/PhpSpreadsheet/Calculation/TextData/Text.php b/src/PhpSpreadsheet/Calculation/TextData/Text.php index 6e408891..f3da4bb3 100644 --- a/src/PhpSpreadsheet/Calculation/TextData/Text.php +++ b/src/PhpSpreadsheet/Calculation/TextData/Text.php @@ -20,7 +20,7 @@ class Text $value = ($value) ? Calculation::getTRUE() : Calculation::getFALSE(); } - return mb_strlen($value, 'UTF-8'); + return mb_strlen($value ?? '', 'UTF-8'); } /** diff --git a/src/PhpSpreadsheet/Reader/Html.php b/src/PhpSpreadsheet/Reader/Html.php index 6e6155c2..ced65e97 100644 --- a/src/PhpSpreadsheet/Reader/Html.php +++ b/src/PhpSpreadsheet/Reader/Html.php @@ -914,7 +914,7 @@ class Html extends BaseReader */ public function getStyleColor($value) { - if (strpos($value, '#') === 0) { + if (strpos($value ?? '', '#') === 0) { return substr($value, 1); } diff --git a/src/PhpSpreadsheet/Reader/Xml/PageSettings.php b/src/PhpSpreadsheet/Reader/Xml/PageSettings.php index e56ac331..62e1e385 100644 --- a/src/PhpSpreadsheet/Reader/Xml/PageSettings.php +++ b/src/PhpSpreadsheet/Reader/Xml/PageSettings.php @@ -115,7 +115,7 @@ class PageSettings private function setLayout(stdClass $printDefaults, SimpleXMLElement $pageSetupAttributes): void { - $printDefaults->orientation = (string) strtolower($pageSetupAttributes->Orientation) ?: PageSetup::ORIENTATION_PORTRAIT; + $printDefaults->orientation = (string) strtolower($pageSetupAttributes->Orientation ?? '') ?: PageSetup::ORIENTATION_PORTRAIT; $printDefaults->horizontalCentered = (bool) $pageSetupAttributes->CenterHorizontal ?: false; $printDefaults->verticalCentered = (bool) $pageSetupAttributes->CenterVertical ?: false; } diff --git a/src/PhpSpreadsheet/Shared/StringHelper.php b/src/PhpSpreadsheet/Shared/StringHelper.php index e85ce55d..0e8eb8a5 100644 --- a/src/PhpSpreadsheet/Shared/StringHelper.php +++ b/src/PhpSpreadsheet/Shared/StringHelper.php @@ -502,7 +502,7 @@ class StringHelper */ public static function strToLower($pValue) { - return mb_convert_case($pValue, MB_CASE_LOWER, 'UTF-8'); + return mb_convert_case($pValue ?? '', MB_CASE_LOWER, 'UTF-8'); } /** diff --git a/src/PhpSpreadsheet/Writer/Xlsx/Worksheet.php b/src/PhpSpreadsheet/Writer/Xlsx/Worksheet.php index 38dead4f..493fd07e 100644 --- a/src/PhpSpreadsheet/Writer/Xlsx/Worksheet.php +++ b/src/PhpSpreadsheet/Writer/Xlsx/Worksheet.php @@ -1256,7 +1256,7 @@ class Worksheet extends WriterPart $objWriter, $this->getParentWriter()->getOffice2003Compatibility() === false, 'v', - ($this->getParentWriter()->getPreCalculateFormulas() && !is_array($calculatedValue) && substr($calculatedValue, 0, 1) !== '#') + ($this->getParentWriter()->getPreCalculateFormulas() && !is_array($calculatedValue) && substr($calculatedValue ?? '', 0, 1) !== '#') ? StringHelper::formatNumber($calculatedValue) : '0' ); } diff --git a/tests/PhpSpreadsheetTests/Calculation/FunctionsTest.php b/tests/PhpSpreadsheetTests/Calculation/FunctionsTest.php index 4e8335f1..ea629183 100644 --- a/tests/PhpSpreadsheetTests/Calculation/FunctionsTest.php +++ b/tests/PhpSpreadsheetTests/Calculation/FunctionsTest.php @@ -341,7 +341,7 @@ class FunctionsTest extends TestCase ->disableOriginalConstructor() ->getMock(); $remoteCell->method('isFormula') - ->willReturn(substr($value, 0, 1) == '='); + ->willReturn(substr($value ?? '', 0, 1) == '='); $remoteSheet = $this->getMockBuilder(Worksheet::class) ->disableOriginalConstructor() From a911e9bb7bf6c83d91bab1035246ab56fd1a310b Mon Sep 17 00:00:00 2001 From: Mark Baker Date: Thu, 10 Jun 2021 08:49:53 +0200 Subject: [PATCH 15/20] Calculation engine empty arguments (#2143) * Initia work on differentiating between empty arguments and null arguments passed to Excel functions Previously we always passed a null value for an empty argument (i.e. where there was an argument separator in the function call without an argument.... PHP doesn't support empty arguments, so we needed to provide some value but then it wasn't possible to differentiate between a genuine null argument (either a literal null, or a null cell value) and the null that we were passing to represent an empty argument value. This change evaluates empty arguments within the calculation engine, and instead of passing a null, it reads the signature of the required Excel function, and passes the default value for that argument; so now a null argument really does mean a null value argument. * If the Excel function implementation doesn't accept any arguments; or once we reach a variadic argument, or try to pass more arguments than the method supports in its signature, then there's no point in checking for defaults, and to do so will lead to PHP errors, so break out of the default replacement loop --- .../Calculation/Calculation.php | 79 ++++++++++++++++--- tests/data/Calculation/DateTime/WEEKNUM.php | 2 + 2 files changed, 72 insertions(+), 9 deletions(-) diff --git a/src/PhpSpreadsheet/Calculation/Calculation.php b/src/PhpSpreadsheet/Calculation/Calculation.php index d03b3623..eb111437 100644 --- a/src/PhpSpreadsheet/Calculation/Calculation.php +++ b/src/PhpSpreadsheet/Calculation/Calculation.php @@ -12,7 +12,9 @@ use PhpOffice\PhpSpreadsheet\ReferenceHelper; use PhpOffice\PhpSpreadsheet\Shared; use PhpOffice\PhpSpreadsheet\Spreadsheet; use PhpOffice\PhpSpreadsheet\Worksheet\Worksheet; +use ReflectionClassConstant; use ReflectionMethod; +use ReflectionParameter; class Calculation { @@ -4108,7 +4110,7 @@ class Calculation // If we've a comma when we're expecting an operand, then what we actually have is a null operand; // so push a null onto the stack if (($expectingOperand) || (!$expectingOperator)) { - $output[] = ['type' => 'NULL Value', 'value' => self::$excelConstants['NULL'], 'reference' => null]; + $output[] = ['type' => 'Empty Argument', 'value' => self::$excelConstants['NULL'], 'reference' => null]; } // make sure there was a function $d = $stack->last(2); @@ -4293,7 +4295,7 @@ class Calculation ++$index; } elseif ($opCharacter == ')') { // miscellaneous error checking if ($expectingOperand) { - $output[] = ['type' => 'NULL Value', 'value' => self::$excelConstants['NULL'], 'reference' => null]; + $output[] = ['type' => 'Empty Argument', 'value' => self::$excelConstants['NULL'], 'reference' => null]; $expectingOperand = false; $expectingOperator = true; } else { @@ -4773,7 +4775,7 @@ class Calculation $functionName = $matches[1]; $argCount = $stack->pop(); $argCount = $argCount['value']; - if ($functionName != 'MKMATRIX') { + if ($functionName !== 'MKMATRIX') { $this->debugLog->writeDebugLog('Evaluating Function ', self::localeFunc($functionName), '() with ', (($argCount == 0) ? 'no' : $argCount), ' argument', (($argCount == 1) ? '' : 's')); } if ((isset(self::$phpSpreadsheetFunctions[$functionName])) || (isset(self::$controlFunctions[$functionName]))) { // function @@ -4789,8 +4791,10 @@ class Calculation $passByReference = isset(self::$controlFunctions[$functionName]['passByReference']); $passCellReference = isset(self::$controlFunctions[$functionName]['passCellReference']); } + // get the arguments for this function $args = $argArrayVals = []; + $emptyArguments = []; for ($i = 0; $i < $argCount; ++$i) { $arg = $stack->pop(); $a = $argCount - $i - 1; @@ -4801,18 +4805,19 @@ class Calculation ) { if ($arg['reference'] === null) { $args[] = $cellID; - if ($functionName != 'MKMATRIX') { + if ($functionName !== 'MKMATRIX') { $argArrayVals[] = $this->showValue($cellID); } } else { $args[] = $arg['reference']; - if ($functionName != 'MKMATRIX') { + if ($functionName !== 'MKMATRIX') { $argArrayVals[] = $this->showValue($arg['reference']); } } } else { + $emptyArguments[] = ($arg['type'] === 'Empty Argument'); $args[] = self::unwrapResult($arg['value']); - if ($functionName != 'MKMATRIX') { + if ($functionName !== 'MKMATRIX') { $argArrayVals[] = $this->showValue($arg['value']); } } @@ -4820,13 +4825,18 @@ class Calculation // Reverse the order of the arguments krsort($args); + krsort($emptyArguments); + + if ($argCount > 0) { + $args = $this->addDefaultArgumentValues($functionCall, $args, $emptyArguments); + } if (($passByReference) && ($argCount == 0)) { $args[] = $cellID; $argArrayVals[] = $this->showValue($cellID); } - if ($functionName != 'MKMATRIX') { + if ($functionName !== 'MKMATRIX') { if ($this->debugLog->getWriteDebugLog()) { krsort($argArrayVals); $this->debugLog->writeDebugLog('Evaluating ', self::localeFunc($functionName), '( ', implode(self::$localeArgumentSeparator . ' ', Functions::flattenArray($argArrayVals)), ' )'); @@ -4845,7 +4855,7 @@ class Calculation $result = call_user_func_array($functionCall, $args); - if ($functionName != 'MKMATRIX') { + if ($functionName !== 'MKMATRIX') { $this->debugLog->writeDebugLog('Evaluation Result for ', self::localeFunc($functionName), '() function call is ', $this->showTypeDetails($result)); } $stack->push('Value', self::wrapResult($result)); @@ -4863,7 +4873,7 @@ class Calculation } $this->debugLog->writeDebugLog('Evaluating Constant ', $excelConstant, ' as ', $this->showTypeDetails(self::$excelConstants[$excelConstant])); } elseif ((is_numeric($token)) || ($token === null) || (is_bool($token)) || ($token == '') || ($token[0] == self::FORMULA_STRING_QUOTE) || ($token[0] == '#')) { - $stack->push('Value', $token); + $stack->push($tokenData['type'], $token, $tokenData['reference']); if (isset($storeKey)) { $branchStore[$storeKey] = $token; } @@ -5386,6 +5396,57 @@ class Calculation return $returnValue; } + private function addDefaultArgumentValues(array $functionCall, array $args, array $emptyArguments): array + { + $reflector = new ReflectionMethod(implode('::', $functionCall)); + $methodArguments = $reflector->getParameters(); + + if (count($methodArguments) > 0) { + // Apply any defaults for empty argument values + foreach ($emptyArguments as $argumentId => $isArgumentEmpty) { + if ($isArgumentEmpty === true) { + $reflectedArgumentId = count($args) - $argumentId - 1; + if ( + !array_key_exists($reflectedArgumentId, $methodArguments) || + $methodArguments[$reflectedArgumentId]->isVariadic() + ) { + break; + } + + $args[$argumentId] = $this->getArgumentDefaultValue($methodArguments[$reflectedArgumentId]); + } + } + } + + return $args; + } + + /** + * @return null|mixed + */ + private function getArgumentDefaultValue(ReflectionParameter $methodArgument) + { + $defaultValue = null; + + if ($methodArgument->isDefaultValueAvailable()) { + $defaultValue = $methodArgument->getDefaultValue(); + if ($methodArgument->isDefaultValueConstant()) { + $constantName = $methodArgument->getDefaultValueConstantName() ?? ''; + // read constant value + if (strpos($constantName, '::') !== false) { + [$className, $constantName] = explode('::', $constantName); + $constantReflector = new ReflectionClassConstant($className, $constantName); + + return $constantReflector->getValue(); + } + + return constant($constantName); + } + } + + return $defaultValue; + } + /** * Add cell reference if needed while making sure that it is the last argument. * diff --git a/tests/data/Calculation/DateTime/WEEKNUM.php b/tests/data/Calculation/DateTime/WEEKNUM.php index 08de5738..065da89d 100644 --- a/tests/data/Calculation/DateTime/WEEKNUM.php +++ b/tests/data/Calculation/DateTime/WEEKNUM.php @@ -77,4 +77,6 @@ return [ ['exception', ''], [48, 'B1'], [0, 'Q15'], + [52, '"21-Dec-2000", '], + [52, '"21-Dec-2000", null'], ]; From 05466e99ce2ec19750ba9373116bfbc07e69ae55 Mon Sep 17 00:00:00 2001 From: Mark Baker Date: Fri, 11 Jun 2021 17:29:49 +0200 Subject: [PATCH 16/20] Html import dimension conversions (#2152) Allows basic column width conversion when importing from Html that includes UoM... while not overly-sophisticated in converting units to MS Excel's column width units, it should allow import without errors Also provides a general conversion helper class, and allows column width getters/setters to specify a UoM for easier usage --- CHANGELOG.md | 5 +- docs/topics/recipes.md | 49 +++++++++ phpstan-baseline.neon | 22 +--- src/PhpSpreadsheet/Helper/Dimension.php | 103 ++++++++++++++++++ src/PhpSpreadsheet/Reader/Html.php | 9 +- src/PhpSpreadsheet/Shared/Drawing.php | 56 +++++----- src/PhpSpreadsheet/Shared/Font.php | 2 +- src/PhpSpreadsheet/Shared/Xls.php | 8 +- .../Worksheet/ColumnDimension.php | 22 +++- src/PhpSpreadsheet/Worksheet/RowDimension.php | 20 +++- .../Helper/DimensionTest.php | 72 ++++++++++++ .../Reader/Html/HtmlTest.php | 20 +++- .../Shared/DrawingTest.php | 80 ++++++++++++++ .../Worksheet/ColumnDimensionTest.php | 10 ++ .../Worksheet/RowDimensionTest.php | 55 ++++++++++ 15 files changed, 464 insertions(+), 69 deletions(-) create mode 100644 src/PhpSpreadsheet/Helper/Dimension.php create mode 100644 tests/PhpSpreadsheetTests/Helper/DimensionTest.php create mode 100644 tests/PhpSpreadsheetTests/Shared/DrawingTest.php create mode 100644 tests/PhpSpreadsheetTests/Worksheet/RowDimensionTest.php diff --git a/CHANGELOG.md b/CHANGELOG.md index 82289a0b..4825193a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,6 +12,8 @@ and this project adheres to [Semantic Versioning](https://semver.org). - Support for passing flags in the Reader `load()` and Writer `save()`methods, and through the IOFactory, to set behaviours. [PR #2136](https://github.com/PHPOffice/PhpSpreadsheet/pull/2136) - See [documentation](https://phpspreadsheet.readthedocs.io/en/latest/topics/reading-and-writing-to-file/) for details - More flexibility in the StringValueBinder to determine what datatypes should be treated as strings [PR #2138](https://github.com/PHPOffice/PhpSpreadsheet/pull/2138) +- Helper class for conversion between css size Units of measure (`px`, `pt`, `pc`, `in`, `cm`, `mm`). [PR #2152](https://github.com/PHPOffice/PhpSpreadsheet/issues/2145) +- Allow Row height and Column Width to be set using different units of measure (`px`, `pt`, `pc`, `in`, `cm`, `mm`), rather than only in points or MS Excel column width units. [PR #2152](https://github.com/PHPOffice/PhpSpreadsheet/issues/2145) ### Changed @@ -27,7 +29,8 @@ and this project adheres to [Semantic Versioning](https://semver.org). ### Fixed -- Nothing. +- Column width and Row height styles in the Html Reader when the value includes a unit of measure. [Issue #2145](https://github.com/PHPOffice/PhpSpreadsheet/issues/2145). + ## 1.18.0 - 2021-05-31 diff --git a/docs/topics/recipes.md b/docs/topics/recipes.md index 8854e55e..471d1dda 100644 --- a/docs/topics/recipes.md +++ b/docs/topics/recipes.md @@ -1122,6 +1122,16 @@ A column's width can be set using the following code: $spreadsheet->getActiveSheet()->getColumnDimension('D')->setWidth(12); ``` +If you want to set a column width using a different unit of measure, +then you can do so by telling PhpSpreadsheet what UoM the width value +that you are setting is measured in. +Valid units are `pt` (points), `px` (pixels), `pc` (pica), `in` (inches), +`cm` (centimeters) and `mm` (millimeters). + +```php +$spreadsheet->getActiveSheet()->getColumnDimension('D')->setWidth(120, 'pt'); +``` + If you want PhpSpreadsheet to perform an automatic width calculation, use the following code. PhpSpreadsheet will approximate the column with to the width of the widest column value. @@ -1207,6 +1217,16 @@ Excel measures row height in points, where 1 pt is 1/72 of an inch (or about 0.35mm). The default value is 12.75 pts; and the permitted range of values is between 0 and 409 pts, where 0 pts is a hidden row. +If you want to set a row height using a different unit of measure, +then you can do so by telling PhpSpreadsheet what UoM the height value +that you are setting is measured in. +Valid units are `pt` (points), `px` (pixels), `pc` (pica), `in` (inches), +`cm` (centimeters) and `mm` (millimeters). + +```php +$spreadsheet->getActiveSheet()->getRowDimension('10')->setRowHeight(100, 'pt'); +``` + ## Show/hide a row To set a worksheet''s row visibility, you can use the following code. @@ -1560,6 +1580,20 @@ Default column width can be set using the following code: $spreadsheet->getActiveSheet()->getDefaultColumnDimension()->setWidth(12); ``` +Excel measures column width in its own proprietary units, based on the number +of characters that will be displayed in the default font. + +If you want to set the default column width using a different unit of measure, +then you can do so by telling PhpSpreadsheet what UoM the width value +that you are setting is measured in. +Valid units are `pt` (points), `px` (pixels), `pc` (pica), `in` (inches), +`cm` (centimeters) and `mm` (millimeters). + +```php +$spreadsheet->getActiveSheet()->getDefaultColumnDimension()->setWidth(400, 'pt'); +``` +and PhpSpreadsheet will handle the internal conversion. + ## Setting the default row height Default row height can be set using the following code: @@ -1568,6 +1602,21 @@ Default row height can be set using the following code: $spreadsheet->getActiveSheet()->getDefaultRowDimension()->setRowHeight(15); ``` +Excel measures row height in points, where 1 pt is 1/72 of an inch (or +about 0.35mm). The default value is 12.75 pts; and the permitted range +of values is between 0 and 409 pts, where 0 pts is a hidden row. + +If you want to set a row height using a different unit of measure, +then you can do so by telling PhpSpreadsheet what UoM the height value +that you are setting is measured in. +Valid units are `pt` (points), `px` (pixels), `pc` (pica), `in` (inches), +`cm` (centimeters) and `mm` (millimeters). + +```php +$spreadsheet->getActiveSheet()->getDefaultRowDimension()->setRowHeight(100, 'pt'); +``` + + ## Add a GD drawing to a worksheet There might be a situation where you want to generate an in-memory image diff --git a/phpstan-baseline.neon b/phpstan-baseline.neon index 4d4f7193..d41411de 100644 --- a/phpstan-baseline.neon +++ b/phpstan-baseline.neon @@ -2487,7 +2487,7 @@ parameters: - message: "#^Parameter \\#3 \\$subject of function str_replace expects array\\|string, string\\|null given\\.$#" - count: 4 + count: 2 path: src/PhpSpreadsheet/Reader/Html.php - @@ -4100,11 +4100,6 @@ parameters: count: 1 path: src/PhpSpreadsheet/Shared/Date.php - - - message: "#^Method PhpOffice\\\\PhpSpreadsheet\\\\Shared\\\\Drawing\\:\\:pixelsToCellDimension\\(\\) should return int but returns float\\|int\\.$#" - count: 1 - path: src/PhpSpreadsheet/Shared/Drawing.php - - message: "#^Parameter \\#1 \\$fp of function fread expects resource, resource\\|false given\\.$#" count: 2 @@ -4240,11 +4235,6 @@ parameters: count: 1 path: src/PhpSpreadsheet/Shared/Font.php - - - message: "#^Parameter \\#1 \\$pValue of static method PhpOffice\\\\PhpSpreadsheet\\\\Shared\\\\Drawing\\:\\:pixelsToCellDimension\\(\\) expects int, float\\|int given\\.$#" - count: 1 - path: src/PhpSpreadsheet/Shared/Font.php - - message: "#^Parameter \\#2 \\$pDefaultFont of static method PhpOffice\\\\PhpSpreadsheet\\\\Shared\\\\Drawing\\:\\:pixelsToCellDimension\\(\\) expects PhpOffice\\\\PhpSpreadsheet\\\\Style\\\\Font, PhpOffice\\\\PhpSpreadsheet\\\\Style\\\\Font\\|null given\\.$#" count: 1 @@ -4860,16 +4850,6 @@ parameters: count: 1 path: src/PhpSpreadsheet/Shared/XMLWriter.php - - - message: "#^Parameter \\#1 \\$pValue of static method PhpOffice\\\\PhpSpreadsheet\\\\Shared\\\\Drawing\\:\\:pointsToPixels\\(\\) expects int, float given\\.$#" - count: 1 - path: src/PhpSpreadsheet/Shared/Xls.php - - - - message: "#^Parameter \\#1 \\$fontSizeInPoints of static method PhpOffice\\\\PhpSpreadsheet\\\\Shared\\\\Font\\:\\:fontSizeToPixels\\(\\) expects int, float given\\.$#" - count: 1 - path: src/PhpSpreadsheet/Shared/Xls.php - - message: "#^Property PhpOffice\\\\PhpSpreadsheet\\\\Spreadsheet\\:\\:\\$workbookViewVisibilityValues has no typehint specified\\.$#" count: 1 diff --git a/src/PhpSpreadsheet/Helper/Dimension.php b/src/PhpSpreadsheet/Helper/Dimension.php new file mode 100644 index 00000000..136ffd7f --- /dev/null +++ b/src/PhpSpreadsheet/Helper/Dimension.php @@ -0,0 +1,103 @@ + 96.0 / 2.54, + self::UOM_MILLIMETERS => 96.0 / 25.4, + self::UOM_INCHES => 96.0, + self::UOM_PIXELS => 1.0, + self::UOM_POINTS => 96.0 / 72, + self::UOM_PICA => 96.0 * 12 / 72, + ]; + + /** + * Based on a standard column width of 8.54 units in MS Excel. + */ + const RELATIVE_UNITS = [ + 'em' => 10.0 / 8.54, + 'ex' => 10.0 / 8.54, + 'ch' => 10.0 / 8.54, + 'rem' => 10.0 / 8.54, + 'vw' => 8.54, + 'vh' => 8.54, + 'vmin' => 8.54, + 'vmax' => 8.54, + '%' => 8.54 / 100, + ]; + + /** + * @var float|int If this is a width, then size is measured in pixels (if is set) + * or in Excel's default column width units if $unit is null. + * If this is a height, then size is measured in pixels () + * or in points () if $unit is null. + */ + protected $size; + + /** + * @var null|string + */ + protected $unit; + + public function __construct(string $dimension) + { + [$size, $unit] = sscanf($dimension, '%[1234567890.]%s'); + $unit = strtolower(trim($unit)); + + // If a UoM is specified, then convert the size to pixels for internal storage + if (isset(self::ABSOLUTE_UNITS[$unit])) { + $size *= self::ABSOLUTE_UNITS[$unit]; + $this->unit = self::UOM_PIXELS; + } elseif (isset(self::RELATIVE_UNITS[$unit])) { + $size *= self::RELATIVE_UNITS[$unit]; + $size = round($size, 4); + } + + $this->size = $size; + } + + public function width(): float + { + return (float) ($this->unit === null) + ? $this->size + : round(Drawing::pixelsToCellDimension((int) $this->size, new Font(false)), 4); + } + + public function height(): float + { + return (float) ($this->unit === null) + ? $this->size + : $this->toUnit(self::UOM_POINTS); + } + + public function toUnit(string $unitOfMeasure): float + { + $unitOfMeasure = strtolower($unitOfMeasure); + if (!array_key_exists($unitOfMeasure, self::ABSOLUTE_UNITS)) { + throw new Exception("{$unitOfMeasure} is not a vaid unit of measure"); + } + + $size = $this->size; + if ($this->unit === null) { + $size = Drawing::cellDimensionToPixels($size, new Font(false)); + } + + return $size / self::ABSOLUTE_UNITS[$unitOfMeasure]; + } +} diff --git a/src/PhpSpreadsheet/Reader/Html.php b/src/PhpSpreadsheet/Reader/Html.php index ced65e97..fe2ea018 100644 --- a/src/PhpSpreadsheet/Reader/Html.php +++ b/src/PhpSpreadsheet/Reader/Html.php @@ -7,6 +7,7 @@ use DOMElement; use DOMNode; use DOMText; use PhpOffice\PhpSpreadsheet\Cell\Coordinate; +use PhpOffice\PhpSpreadsheet\Helper\Dimension as CssDimension; use PhpOffice\PhpSpreadsheet\Reader\Security\XmlScanner; use PhpOffice\PhpSpreadsheet\Spreadsheet; use PhpOffice\PhpSpreadsheet\Style\Border; @@ -527,14 +528,14 @@ class Html extends BaseReader private function processDomElementWidth(Worksheet $sheet, string $column, array $attributeArray): void { if (isset($attributeArray['width'])) { - $sheet->getColumnDimension($column)->setWidth($attributeArray['width']); + $sheet->getColumnDimension($column)->setWidth((new CssDimension($attributeArray['width']))->width()); } } private function processDomElementHeight(Worksheet $sheet, int $row, array $attributeArray): void { if (isset($attributeArray['height'])) { - $sheet->getRowDimension($row)->setRowHeight($attributeArray['height']); + $sheet->getRowDimension($row)->setRowHeight((new CssDimension($attributeArray['height']))->height()); } } @@ -878,14 +879,14 @@ class Html extends BaseReader case 'width': $sheet->getColumnDimension($column)->setWidth( - (float) str_replace(['px', 'pt'], '', $styleValue) + (new CssDimension($styleValue ?? ''))->width() ); break; case 'height': $sheet->getRowDimension($row)->setRowHeight( - (float) str_replace(['px', 'pt'], '', $styleValue) + (new CssDimension($styleValue ?? ''))->height() ); break; diff --git a/src/PhpSpreadsheet/Shared/Drawing.php b/src/PhpSpreadsheet/Shared/Drawing.php index ebb87ed1..08982d84 100644 --- a/src/PhpSpreadsheet/Shared/Drawing.php +++ b/src/PhpSpreadsheet/Shared/Drawing.php @@ -9,26 +9,26 @@ class Drawing /** * Convert pixels to EMU. * - * @param int $pValue Value in pixels + * @param int $pixelValue Value in pixels * * @return int Value in EMU */ - public static function pixelsToEMU($pValue) + public static function pixelsToEMU($pixelValue) { - return $pValue * 9525; + return $pixelValue * 9525; } /** * Convert EMU to pixels. * - * @param int $pValue Value in EMU + * @param int $emuValue Value in EMU * * @return int Value in pixels */ - public static function EMUToPixels($pValue) + public static function EMUToPixels($emuValue) { - if ($pValue != 0) { - return (int) round($pValue / 9525); + if ($emuValue != 0) { + return (int) round($emuValue / 9525); } return 0; @@ -39,12 +39,12 @@ class Drawing * By inspection of a real Excel file using Calibri 11, one finds 1000px ~ 142.85546875 * This gives a conversion factor of 7. Also, we assume that pixels and font size are proportional. * - * @param int $pValue Value in pixels + * @param int $pixelValue Value in pixels * @param \PhpOffice\PhpSpreadsheet\Style\Font $pDefaultFont Default font of the workbook * - * @return int Value in cell dimension + * @return float|int Value in cell dimension */ - public static function pixelsToCellDimension($pValue, \PhpOffice\PhpSpreadsheet\Style\Font $pDefaultFont) + public static function pixelsToCellDimension($pixelValue, \PhpOffice\PhpSpreadsheet\Style\Font $pDefaultFont) { // Font name and size $name = $pDefaultFont->getName(); @@ -52,25 +52,25 @@ class Drawing if (isset(Font::$defaultColumnWidths[$name][$size])) { // Exact width can be determined - $colWidth = $pValue * Font::$defaultColumnWidths[$name][$size]['width'] / Font::$defaultColumnWidths[$name][$size]['px']; - } else { - // We don't have data for this particular font and size, use approximation by - // extrapolating from Calibri 11 - $colWidth = $pValue * 11 * Font::$defaultColumnWidths['Calibri'][11]['width'] / Font::$defaultColumnWidths['Calibri'][11]['px'] / $size; + return $pixelValue * Font::$defaultColumnWidths[$name][$size]['width'] + / Font::$defaultColumnWidths[$name][$size]['px']; } - return $colWidth; + // We don't have data for this particular font and size, use approximation by + // extrapolating from Calibri 11 + return $pixelValue * 11 * Font::$defaultColumnWidths['Calibri'][11]['width'] + / Font::$defaultColumnWidths['Calibri'][11]['px'] / $size; } /** * Convert column width from (intrinsic) Excel units to pixels. * - * @param float $pValue Value in cell dimension + * @param float $cellWidth Value in cell dimension * @param \PhpOffice\PhpSpreadsheet\Style\Font $pDefaultFont Default font of the workbook * * @return int Value in pixels */ - public static function cellDimensionToPixels($pValue, \PhpOffice\PhpSpreadsheet\Style\Font $pDefaultFont) + public static function cellDimensionToPixels($cellWidth, \PhpOffice\PhpSpreadsheet\Style\Font $pDefaultFont) { // Font name and size $name = $pDefaultFont->getName(); @@ -78,11 +78,13 @@ class Drawing if (isset(Font::$defaultColumnWidths[$name][$size])) { // Exact width can be determined - $colWidth = $pValue * Font::$defaultColumnWidths[$name][$size]['px'] / Font::$defaultColumnWidths[$name][$size]['width']; + $colWidth = $cellWidth * Font::$defaultColumnWidths[$name][$size]['px'] + / Font::$defaultColumnWidths[$name][$size]['width']; } else { // We don't have data for this particular font and size, use approximation by // extrapolating from Calibri 11 - $colWidth = $pValue * $size * Font::$defaultColumnWidths['Calibri'][11]['px'] / Font::$defaultColumnWidths['Calibri'][11]['width'] / 11; + $colWidth = $cellWidth * $size * Font::$defaultColumnWidths['Calibri'][11]['px'] + / Font::$defaultColumnWidths['Calibri'][11]['width'] / 11; } // Round pixels to closest integer @@ -94,26 +96,26 @@ class Drawing /** * Convert pixels to points. * - * @param int $pValue Value in pixels + * @param int $pixelValue Value in pixels * * @return float Value in points */ - public static function pixelsToPoints($pValue) + public static function pixelsToPoints($pixelValue) { - return $pValue * 0.75; + return $pixelValue * 0.75; } /** * Convert points to pixels. * - * @param int $pValue Value in points + * @param int $pointValue Value in points * * @return int Value in pixels */ - public static function pointsToPixels($pValue) + public static function pointsToPixels($pointValue) { - if ($pValue != 0) { - return (int) ceil($pValue / 0.75); + if ($pointValue != 0) { + return (int) ceil($pointValue / 0.75); } return 0; diff --git a/src/PhpSpreadsheet/Shared/Font.php b/src/PhpSpreadsheet/Shared/Font.php index 00629e69..a0817448 100644 --- a/src/PhpSpreadsheet/Shared/Font.php +++ b/src/PhpSpreadsheet/Shared/Font.php @@ -265,7 +265,7 @@ class Font } // Convert from pixel width to column width - $columnWidth = Drawing::pixelsToCellDimension($columnWidth, $defaultFont); + $columnWidth = Drawing::pixelsToCellDimension((int) $columnWidth, $defaultFont); // Return return (int) round($columnWidth, 6); diff --git a/src/PhpSpreadsheet/Shared/Xls.php b/src/PhpSpreadsheet/Shared/Xls.php index 26035ec6..cd2482b0 100644 --- a/src/PhpSpreadsheet/Shared/Xls.php +++ b/src/PhpSpreadsheet/Shared/Xls.php @@ -3,6 +3,7 @@ namespace PhpOffice\PhpSpreadsheet\Shared; use PhpOffice\PhpSpreadsheet\Cell\Coordinate; +use PhpOffice\PhpSpreadsheet\Helper\Dimension; use PhpOffice\PhpSpreadsheet\Worksheet\Worksheet; class Xls @@ -76,12 +77,11 @@ class Xls } elseif ($sheet->getDefaultRowDimension()->getRowHeight() != -1) { // then we have a default row dimension with explicit height $defaultRowDimension = $sheet->getDefaultRowDimension(); - $rowHeight = $defaultRowDimension->getRowHeight(); - $pixelRowHeight = Drawing::pointsToPixels($rowHeight); + $pixelRowHeight = $defaultRowDimension->getRowHeight(Dimension::UOM_PIXELS); } else { // we don't even have any default row dimension. Height depends on default font $pointRowHeight = Font::getDefaultRowHeightByFont($font); - $pixelRowHeight = Font::fontSizeToPixels($pointRowHeight); + $pixelRowHeight = Font::fontSizeToPixels((int) $pointRowHeight); } // now find the effective row height in pixels @@ -91,7 +91,7 @@ class Xls $effectivePixelRowHeight = $pixelRowHeight; } - return $effectivePixelRowHeight; + return (int) $effectivePixelRowHeight; } /** diff --git a/src/PhpSpreadsheet/Worksheet/ColumnDimension.php b/src/PhpSpreadsheet/Worksheet/ColumnDimension.php index 12b1efdf..4ea30c8f 100644 --- a/src/PhpSpreadsheet/Worksheet/ColumnDimension.php +++ b/src/PhpSpreadsheet/Worksheet/ColumnDimension.php @@ -2,6 +2,8 @@ namespace PhpOffice\PhpSpreadsheet\Worksheet; +use PhpOffice\PhpSpreadsheet\Helper\Dimension as CssDimension; + class ColumnDimension extends Dimension { /** @@ -63,20 +65,32 @@ class ColumnDimension extends Dimension /** * Get Width. + * + * Each unit of column width is equal to the width of one character in the default font size. + * By default, this will be the return value; but this method also accepts a unit of measure argument and will + * return the value converted to the specified UoM using an approximation method. */ - public function getWidth(): float + public function getWidth(?string $unitOfMeasure = null): float { - return $this->width; + return ($unitOfMeasure === null || $this->width < 0) + ? $this->width + : (new CssDimension((string) $this->width))->toUnit($unitOfMeasure); } /** * Set Width. * + * Each unit of column width is equal to the width of one character in the default font size. + * By default, this will be the unit of measure for the passed value; but this method accepts a unit of measure + * argument, and will convert the value from the specified UoM using an approximation method. + * * @return $this */ - public function setWidth(float $width) + public function setWidth(float $width, ?string $unitOfMeasure = null) { - $this->width = $width; + $this->width = ($unitOfMeasure === null || $width < 0) + ? $width + : (new CssDimension("{$width}{$unitOfMeasure}"))->width(); return $this; } diff --git a/src/PhpSpreadsheet/Worksheet/RowDimension.php b/src/PhpSpreadsheet/Worksheet/RowDimension.php index d86dd80f..006157b2 100644 --- a/src/PhpSpreadsheet/Worksheet/RowDimension.php +++ b/src/PhpSpreadsheet/Worksheet/RowDimension.php @@ -2,6 +2,8 @@ namespace PhpOffice\PhpSpreadsheet\Worksheet; +use PhpOffice\PhpSpreadsheet\Helper\Dimension as CssDimension; + class RowDimension extends Dimension { /** @@ -63,24 +65,32 @@ class RowDimension extends Dimension /** * Get Row Height. + * By default, this will be in points; but this method accepts a unit of measure + * argument, and will convert the value to the specified UoM. * * @return float */ - public function getRowHeight() + public function getRowHeight(?string $unitOfMeasure = null) { - return $this->height; + return ($unitOfMeasure === null || $this->height < 0) + ? $this->height + : (new CssDimension($this->height . CssDimension::UOM_POINTS))->toUnit($unitOfMeasure); } /** * Set Row Height. * - * @param float $height + * @param float $height in points + * By default, this will be the passed argument value; but this method accepts a unit of measure + * argument, and will convert the passed argument value to points from the specified UoM * * @return $this */ - public function setRowHeight($height) + public function setRowHeight($height, ?string $unitOfMeasure = null) { - $this->height = $height; + $this->height = ($unitOfMeasure === null || $height < 0) + ? $height + : (new CssDimension("{$height}{$unitOfMeasure}"))->height(); return $this; } diff --git a/tests/PhpSpreadsheetTests/Helper/DimensionTest.php b/tests/PhpSpreadsheetTests/Helper/DimensionTest.php new file mode 100644 index 00000000..87bce380 --- /dev/null +++ b/tests/PhpSpreadsheetTests/Helper/DimensionTest.php @@ -0,0 +1,72 @@ +width(); + self::assertSame($expectedResult, $result); + } + + /** + * @dataProvider providerConvertUoM + */ + public function testConvertDimension(float $expectedResult, string $dimension, string $unitOfMeasure): void + { + $result = (new Dimension($dimension))->toUnit($unitOfMeasure); + self::assertSame($expectedResult, $result); + } + + public function testConvertDimensionInvalidUoM(): void + { + $this->expectException(Exception::class); + $this->expectExceptionMessage('pikachu is not a vaid unit of measure'); + (new Dimension('999'))->toUnit('pikachu'); + } + + public function providerCellWidth(): array + { + return [ + [12.0, '12'], + [2.2852, '12pt'], + [4.5703, '24 pt'], + [5.1416, '36px'], + [5.7129, '2.5pc'], + [13.7109, '2.54cm'], + [13.7109, '25.4mm'], + [13.7109, '1in'], + [4.27, '50%'], + [3.7471, '3.2em'], + [2.3419, '2ch'], + [4.6838, '4ex'], + [14.0515, '12rem'], + ]; + } + + public function providerConvertUoM(): array + { + return [ + [60, '8.54', Dimension::UOM_PIXELS], + [100, '100px', Dimension::UOM_PIXELS], + [150, '200px', Dimension::UOM_POINTS], + [45, '8.54', Dimension::UOM_POINTS], + [12.5, '200px', Dimension::UOM_PICA], + [3.75, '8.54', Dimension::UOM_PICA], + [3.125, '300px', Dimension::UOM_INCHES], + [0.625, '8.54', Dimension::UOM_INCHES], + [7.9375, '300px', Dimension::UOM_CENTIMETERS], + [1.5875, '8.54', Dimension::UOM_CENTIMETERS], + [79.375, '300px', Dimension::UOM_MILLIMETERS], + [15.875, '8.54', Dimension::UOM_MILLIMETERS], + ]; + } +} diff --git a/tests/PhpSpreadsheetTests/Reader/Html/HtmlTest.php b/tests/PhpSpreadsheetTests/Reader/Html/HtmlTest.php index 7acc5527..38a9bc9e 100644 --- a/tests/PhpSpreadsheetTests/Reader/Html/HtmlTest.php +++ b/tests/PhpSpreadsheetTests/Reader/Html/HtmlTest.php @@ -136,6 +136,7 @@ class HtmlTest extends TestCase 50px 100px + 50px '; $filename = HtmlHelper::createHtml($html); @@ -143,10 +144,16 @@ class HtmlTest extends TestCase $firstSheet = $spreadsheet->getSheet(0); $dimension = $firstSheet->getColumnDimension('A'); + self::assertNotNull($dimension); self::assertEquals(50, $dimension->getWidth()); $dimension = $firstSheet->getColumnDimension('B'); - self::assertEquals(100, $dimension->getWidth()); + self::assertNotNull($dimension); + self::assertEquals(100, $dimension->getWidth('px')); + + $dimension = $firstSheet->getColumnDimension('C'); + self::assertNotNull($dimension); + self::assertEquals(50, $dimension->getWidth('px')); } public function testCanApplyInlineHeight(): void @@ -158,16 +165,25 @@ class HtmlTest extends TestCase 2 + + 1 + '; $filename = HtmlHelper::createHtml($html); $spreadsheet = HtmlHelper::loadHtmlIntoSpreadsheet($filename, true); $firstSheet = $spreadsheet->getSheet(0); $dimension = $firstSheet->getRowDimension(1); + self::assertNotNull($dimension); self::assertEquals(50, $dimension->getRowHeight()); $dimension = $firstSheet->getRowDimension(2); - self::assertEquals(100, $dimension->getRowHeight()); + self::assertNotNull($dimension); + self::assertEquals(100, $dimension->getRowHeight('px')); + + $dimension = $firstSheet->getRowDimension(3); + self::assertNotNull($dimension); + self::assertEquals(50, $dimension->getRowHeight('px')); } public function testCanApplyAlignment(): void diff --git a/tests/PhpSpreadsheetTests/Shared/DrawingTest.php b/tests/PhpSpreadsheetTests/Shared/DrawingTest.php new file mode 100644 index 00000000..dc0ff631 --- /dev/null +++ b/tests/PhpSpreadsheetTests/Shared/DrawingTest.php @@ -0,0 +1,80 @@ +setName($fontName); + $font->setSize($fontSize); + + $result = Drawing::pixelsToCellDimension($pixelSize, $font); + self::assertSame($expectedResult, $result); + } + + /** + * @dataProvider providerCellDimensionToPixels + */ + public function testCellDimensionToPixels( + int $expectedResult, + int $cellSize, + string $fontName, + int $fontSize + ): void { + $font = new Font(); + $font->setName($fontName); + $font->setSize($fontSize); + + $result = Drawing::cellDimensionToPixels($cellSize, $font); + self::assertSame($expectedResult, $result); + } + + public function providerPixelsToCellDimension(): array + { + return [ + [19.9951171875, 100, 'Arial', 7], + [14.2822265625, 100, 'Arial', 9], + [14.2822265625, 100, 'Arial', 11], + [13.092041015625, 100, 'Arial', 12], // approximation by extrapolating from Calibri 11 + [19.9951171875, 100, 'Calibri', 7], + [16.664341517857142, 100, 'Calibri', 9], + [14.2822265625, 100, 'Calibri', 11], + [13.092041015625, 100, 'Calibri', 12], // approximation by extrapolating from Calibri 11 + [19.9951171875, 100, 'Verdana', 7], + [12.5, 100, 'Verdana', 9], + [13.092041015625, 100, 'Verdana', 12], // approximation by extrapolating from Calibri 11 + [17.4560546875, 100, 'Wingdings', 9], // approximation by extrapolating from Calibri 11 + ]; + } + + public function providerCellDimensionToPixels(): array + { + return [ + [500, 100, 'Arial', 7], + [700, 100, 'Arial', 9], + [700, 100, 'Arial', 11], + [764, 100, 'Arial', 12], // approximation by extrapolating from Calibri 11 + [500, 100, 'Calibri', 7], + [600, 100, 'Calibri', 9], + [700, 100, 'Calibri', 11], + [764, 100, 'Calibri', 12], // approximation by extrapolating from Calibri 11 + [500, 100, 'Verdana', 7], + [800, 100, 'Verdana', 9], + [764, 100, 'Verdana', 12], // approximation by extrapolating from Calibri 11 + [573, 100, 'Wingdings', 9], // approximation by extrapolating from Calibri 11 + ]; + } +} diff --git a/tests/PhpSpreadsheetTests/Worksheet/ColumnDimensionTest.php b/tests/PhpSpreadsheetTests/Worksheet/ColumnDimensionTest.php index 8062a24c..1994fa81 100644 --- a/tests/PhpSpreadsheetTests/Worksheet/ColumnDimensionTest.php +++ b/tests/PhpSpreadsheetTests/Worksheet/ColumnDimensionTest.php @@ -2,6 +2,7 @@ namespace PhpOffice\PhpSpreadsheetTests\Worksheet; +use PhpOffice\PhpSpreadsheet\Helper\Dimension; use PhpOffice\PhpSpreadsheet\Worksheet\ColumnDimension; use PHPUnit\Framework\TestCase; @@ -29,9 +30,18 @@ class ColumnDimensionTest extends TestCase { $expected = 1.2; $columnDimension = new ColumnDimension(); + $columnDimension->setWidth($expected); $result = $columnDimension->getWidth(); self::assertSame($expected, $result); + + $expectedPx = 32.0; + $expectedPt = 24.0; + $columnDimension->setWidth($expectedPx, Dimension::UOM_PIXELS); + $resultPx = $columnDimension->getWidth(Dimension::UOM_PIXELS); + self::assertSame($expectedPx, $resultPx); + $resultPt = $columnDimension->getWidth(Dimension::UOM_POINTS); + self::assertSame($expectedPt, $resultPt); } public function testGetAndSetAutoSize(): void diff --git a/tests/PhpSpreadsheetTests/Worksheet/RowDimensionTest.php b/tests/PhpSpreadsheetTests/Worksheet/RowDimensionTest.php new file mode 100644 index 00000000..a5b5a04d --- /dev/null +++ b/tests/PhpSpreadsheetTests/Worksheet/RowDimensionTest.php @@ -0,0 +1,55 @@ +getRowIndex(); + self::assertEquals($expected, $result); + } + + public function testGetAndSetColumnIndex(): void + { + $expected = 2; + $rowDimension = new RowDimension(); + $rowDimension->setRowIndex($expected); + $result = $rowDimension->getRowIndex(); + self::assertSame($expected, $result); + } + + public function testGetAndSetHeight(): void + { + $expected = 1.2; + $columnDimension = new RowDimension(); + + $columnDimension->setRowHeight($expected); + $result = $columnDimension->getRowHeight(); + self::assertSame($expected, $result); + + $expectedPx = 32.0; + $expectedPt = 24.0; + $columnDimension->setRowHeight($expectedPx, Dimension::UOM_PIXELS); + $resultPx = $columnDimension->getRowHeight(Dimension::UOM_PIXELS); + self::assertSame($expectedPx, $resultPx); + $resultPt = $columnDimension->getRowHeight(Dimension::UOM_POINTS); + self::assertSame($expectedPt, $resultPt); + } + + public function testRowZeroHeight(): void + { + $expected = true; + $rowDimension = new RowDimension(); + $rowDimension->setZeroHeight($expected); + $result = $rowDimension->getZeroHeight(); + self::assertTrue($result); + } +} From 9c2ce22505d6aea47dda520976d9982bc2b0c028 Mon Sep 17 00:00:00 2001 From: Mark Baker Date: Fri, 11 Jun 2021 22:00:26 +0200 Subject: [PATCH 17/20] This should fix png files with transparency in the Xls reader (#2155) * This should fix png files with transparency in the Xls reader --- phpstan-baseline.neon | 5 --- src/PhpSpreadsheet/Reader/Xls.php | 53 +++++++++++++++++-------------- 2 files changed, 29 insertions(+), 29 deletions(-) diff --git a/phpstan-baseline.neon b/phpstan-baseline.neon index d41411de..7976a3ad 100644 --- a/phpstan-baseline.neon +++ b/phpstan-baseline.neon @@ -2775,11 +2775,6 @@ parameters: count: 1 path: src/PhpSpreadsheet/Reader/Xls.php - - - message: "#^Parameter \\#1 \\$value of method PhpOffice\\\\PhpSpreadsheet\\\\Worksheet\\\\MemoryDrawing\\:\\:setImageResource\\(\\) expects GdImage\\|resource, resource\\|false given\\.$#" - count: 1 - path: src/PhpSpreadsheet/Reader/Xls.php - - message: "#^Parameter \\#1 \\$pValue of method PhpOffice\\\\PhpSpreadsheet\\\\Worksheet\\\\BaseDrawing\\:\\:setOffsetX\\(\\) expects int, float\\|int given\\.$#" count: 1 diff --git a/src/PhpSpreadsheet/Reader/Xls.php b/src/PhpSpreadsheet/Reader/Xls.php index a8c097f7..1077e980 100644 --- a/src/PhpSpreadsheet/Reader/Xls.php +++ b/src/PhpSpreadsheet/Reader/Xls.php @@ -17,6 +17,7 @@ use PhpOffice\PhpSpreadsheet\Shared\File; use PhpOffice\PhpSpreadsheet\Shared\OLE; use PhpOffice\PhpSpreadsheet\Shared\OLERead; use PhpOffice\PhpSpreadsheet\Shared\StringHelper; +use PhpOffice\PhpSpreadsheet\Shared\Xls as SharedXls; use PhpOffice\PhpSpreadsheet\Spreadsheet; use PhpOffice\PhpSpreadsheet\Style\Alignment; use PhpOffice\PhpSpreadsheet\Style\Borders; @@ -1102,12 +1103,12 @@ class Xls extends BaseReader $endOffsetX = $spContainer->getEndOffsetX(); $endOffsetY = $spContainer->getEndOffsetY(); - $width = \PhpOffice\PhpSpreadsheet\Shared\Xls::getDistanceX($this->phpSheet, $startColumn, $startOffsetX, $endColumn, $endOffsetX); - $height = \PhpOffice\PhpSpreadsheet\Shared\Xls::getDistanceY($this->phpSheet, $startRow, $startOffsetY, $endRow, $endOffsetY); + $width = SharedXls::getDistanceX($this->phpSheet, $startColumn, $startOffsetX, $endColumn, $endOffsetX); + $height = SharedXls::getDistanceY($this->phpSheet, $startRow, $startOffsetY, $endRow, $endOffsetY); // calculate offsetX and offsetY of the shape - $offsetX = $startOffsetX * \PhpOffice\PhpSpreadsheet\Shared\Xls::sizeCol($this->phpSheet, $startColumn) / 1024; - $offsetY = $startOffsetY * \PhpOffice\PhpSpreadsheet\Shared\Xls::sizeRow($this->phpSheet, $startRow) / 256; + $offsetX = $startOffsetX * SharedXls::sizeCol($this->phpSheet, $startColumn) / 1024; + $offsetY = $startOffsetY * SharedXls::sizeRow($this->phpSheet, $startRow) / 256; switch ($obj['otObjType']) { case 0x19: @@ -1143,31 +1144,35 @@ class Xls extends BaseReader // need check because some blip types are not supported by Escher reader such as EMF if ($blip = $BSE->getBlip()) { $ih = imagecreatefromstring($blip->getData()); - $drawing = new MemoryDrawing(); - $drawing->setImageResource($ih); + if ($ih !== false) { + $drawing = new MemoryDrawing(); + $drawing->setImageResource($ih); - // width, height, offsetX, offsetY - $drawing->setResizeProportional(false); - $drawing->setWidth($width); - $drawing->setHeight($height); - $drawing->setOffsetX($offsetX); - $drawing->setOffsetY($offsetY); + // width, height, offsetX, offsetY + $drawing->setResizeProportional(false); + $drawing->setWidth($width); + $drawing->setHeight($height); + $drawing->setOffsetX($offsetX); + $drawing->setOffsetY($offsetY); - switch ($blipType) { - case BSE::BLIPTYPE_JPEG: - $drawing->setRenderingFunction(MemoryDrawing::RENDERING_JPEG); - $drawing->setMimeType(MemoryDrawing::MIMETYPE_JPEG); + switch ($blipType) { + case BSE::BLIPTYPE_JPEG: + $drawing->setRenderingFunction(MemoryDrawing::RENDERING_JPEG); + $drawing->setMimeType(MemoryDrawing::MIMETYPE_JPEG); - break; - case BSE::BLIPTYPE_PNG: - $drawing->setRenderingFunction(MemoryDrawing::RENDERING_PNG); - $drawing->setMimeType(MemoryDrawing::MIMETYPE_PNG); + break; + case BSE::BLIPTYPE_PNG: + imagealphablending($ih, false); + imagesavealpha($ih, true); + $drawing->setRenderingFunction(MemoryDrawing::RENDERING_PNG); + $drawing->setMimeType(MemoryDrawing::MIMETYPE_PNG); - break; + break; + } + + $drawing->setWorksheet($this->phpSheet); + $drawing->setCoordinates($spContainer->getStartCoordinates()); } - - $drawing->setWorksheet($this->phpSheet); - $drawing->setCoordinates($spContainer->getStartCoordinates()); } } From b98b9c761c0a90f70ebdc533620e11b52329012c Mon Sep 17 00:00:00 2001 From: oleibman Date: Fri, 11 Jun 2021 13:29:44 -0700 Subject: [PATCH 18/20] Improve Identification of Samples in Coverage Report (#2153) The Phpunit coverage report currently contains bullet items like `PhpOffice\PhpSpreadsheetTests\Helper\SampleTest\testSample with data set "49"`. This extremely simple change takes advantage of Phpunit's ability to accept an array with keys which are either strings or integers, by using the sample filenames as the array keys rather than sequential but otherwise meaningless integers (e.g. `49` in the earlier cited item). The bullet item will now read `PhpOffice\PhpSpreadsheetTests\Helper\SampleTest\testSample with data set "Basic/38_Clone_worksheet.php"`. --- tests/PhpSpreadsheetTests/Helper/SampleTest.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/PhpSpreadsheetTests/Helper/SampleTest.php b/tests/PhpSpreadsheetTests/Helper/SampleTest.php index 0817f1d5..bdcb2a6a 100644 --- a/tests/PhpSpreadsheetTests/Helper/SampleTest.php +++ b/tests/PhpSpreadsheetTests/Helper/SampleTest.php @@ -63,7 +63,7 @@ class SampleTest extends TestCase // } if (!in_array($sample, $skipped)) { $file = 'samples/' . $sample; - $result[] = [$file]; + $result[$sample] = [$file]; } } } From 74b02fb31c9b3b72f7e1785ccfa19937bc1bea9f Mon Sep 17 00:00:00 2001 From: Mark Baker Date: Sun, 13 Jun 2021 21:46:49 +0200 Subject: [PATCH 19/20] Fix for the BIFF-8 Xls colour mappings in the Reader (#2156) * Fix for the BIFF-8 Xls colour mappings in the Reader * Unit test for reading colours, writing hen rereading and ensuring that the RGB values have not changed --- src/PhpSpreadsheet/Reader/Xls/Color/BIFF8.php | 112 +++++++++--------- .../Reader/Xls/ColourTest.php | 42 +++++++ tests/data/Reader/XLS/Colours.xls | Bin 0 -> 31232 bytes 3 files changed, 98 insertions(+), 56 deletions(-) create mode 100644 tests/PhpSpreadsheetTests/Reader/Xls/ColourTest.php create mode 100644 tests/data/Reader/XLS/Colours.xls diff --git a/src/PhpSpreadsheet/Reader/Xls/Color/BIFF8.php b/src/PhpSpreadsheet/Reader/Xls/Color/BIFF8.php index 5d8b5ab8..5c109fb0 100644 --- a/src/PhpSpreadsheet/Reader/Xls/Color/BIFF8.php +++ b/src/PhpSpreadsheet/Reader/Xls/Color/BIFF8.php @@ -5,62 +5,62 @@ namespace PhpOffice\PhpSpreadsheet\Reader\Xls\Color; class BIFF8 { protected static $map = [ - '000000' => 0x08, - 'FFFFFF' => 0x09, - 'FF0000' => 0x0A, - '00FF00' => 0x0B, - '0000FF' => 0x0C, - 'FFFF00' => 0x0D, - 'FF00FF' => 0x0E, - '00FFFF' => 0x0F, - '800000' => 0x10, - '008000' => 0x11, - '000080' => 0x12, - '808000' => 0x13, - '800080' => 0x14, - '008080' => 0x15, - 'C0C0C0' => 0x16, - '808080' => 0x17, - '9999FF' => 0x18, - '993366' => 0x19, - 'FFFFCC' => 0x1A, - 'CCFFFF' => 0x1B, - '660066' => 0x1C, - 'FF8080' => 0x1D, - '0066CC' => 0x1E, - 'CCCCFF' => 0x1F, - // '000080' => 0x20, - // 'FF00FF' => 0x21, - // 'FFFF00' => 0x22, - // '00FFFF' => 0x23, - // '800080' => 0x24, - // '800000' => 0x25, - // '008080' => 0x26, - // '0000FF' => 0x27, - '00CCFF' => 0x28, - // 'CCFFFF' => 0x29, - 'CCFFCC' => 0x2A, - 'FFFF99' => 0x2B, - '99CCFF' => 0x2C, - 'FF99CC' => 0x2D, - 'CC99FF' => 0x2E, - 'FFCC99' => 0x2F, - '3366FF' => 0x30, - '33CCCC' => 0x31, - '99CC00' => 0x32, - 'FFCC00' => 0x33, - 'FF9900' => 0x34, - 'FF6600' => 0x35, - '666699' => 0x36, - '969696' => 0x37, - '003366' => 0x38, - '339966' => 0x39, - '003300' => 0x3A, - '333300' => 0x3B, - '993300' => 0x3C, - // '993366' => 0x3D, - '333399' => 0x3E, - '333333' => 0x3F, + 0x08 => '000000', + 0x09 => 'FFFFFF', + 0x0A => 'FF0000', + 0x0B => '00FF00', + 0x0C => '0000FF', + 0x0D => 'FFFF00', + 0x0E => 'FF00FF', + 0x0F => '00FFFF', + 0x10 => '800000', + 0x11 => '008000', + 0x12 => '000080', + 0x13 => '808000', + 0x14 => '800080', + 0x15 => '008080', + 0x16 => 'C0C0C0', + 0x17 => '808080', + 0x18 => '9999FF', + 0x19 => '993366', + 0x1A => 'FFFFCC', + 0x1B => 'CCFFFF', + 0x1C => '660066', + 0x1D => 'FF8080', + 0x1E => '0066CC', + 0x1F => 'CCCCFF', + 0x20 => '000080', + 0x21 => 'FF00FF', + 0x22 => 'FFFF00', + 0x23 => '00FFFF', + 0x24 => '800080', + 0x25 => '800000', + 0x26 => '008080', + 0x27 => '0000FF', + 0x28 => '00CCFF', + 0x29 => 'CCFFFF', + 0x2A => 'CCFFCC', + 0x2B => 'FFFF99', + 0x2C => '99CCFF', + 0x2D => 'FF99CC', + 0x2E => 'CC99FF', + 0x2F => 'FFCC99', + 0x30 => '3366FF', + 0x31 => '33CCCC', + 0x32 => '99CC00', + 0x33 => 'FFCC00', + 0x34 => 'FF9900', + 0x35 => 'FF6600', + 0x36 => '666699', + 0x37 => '969696', + 0x38 => '003366', + 0x39 => '339966', + 0x3A => '003300', + 0x3B => '333300', + 0x3C => '993300', + 0x3D => '993366', + 0x3E => '333399', + 0x3F => '333333', ]; /** diff --git a/tests/PhpSpreadsheetTests/Reader/Xls/ColourTest.php b/tests/PhpSpreadsheetTests/Reader/Xls/ColourTest.php new file mode 100644 index 00000000..d17e7e16 --- /dev/null +++ b/tests/PhpSpreadsheetTests/Reader/Xls/ColourTest.php @@ -0,0 +1,42 @@ +spreadsheet = $reader->load($filename); + } + + public function testColours(): void + { + $colours = []; + + $worksheet = $this->spreadsheet->getActiveSheet(); + for ($row = 1; $row <= 7; ++$row) { + for ($column = 'A'; $column !== 'J'; ++$column) { + $cellAddress = "{$column}{$row}"; + $colours[$cellAddress] = $worksheet->getStyle($cellAddress)->getFill()->getStartColor()->getRGB(); + } + } + + $newSpreadsheet = $this->writeAndReload($this->spreadsheet, 'Xls'); + $newWorksheet = $newSpreadsheet->getActiveSheet(); + foreach ($colours as $cellAddress => $expectedColourValue) { + $actualColourValue = $newWorksheet->getStyle($cellAddress)->getFill()->getStartColor()->getRGB(); + self::assertSame($expectedColourValue, $actualColourValue); + } + } +} diff --git a/tests/data/Reader/XLS/Colours.xls b/tests/data/Reader/XLS/Colours.xls new file mode 100644 index 0000000000000000000000000000000000000000..c3aa05926d95098e98ef370ad0e654d131631d31 GIT binary patch literal 31232 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 b7B;29q9T-m{tAm`@^txc)E{57RPz5Xi%pt7 literal 0 HcmV?d00001 From 66cd68daeab53e921c65d77db4629a76cc233133 Mon Sep 17 00:00:00 2001 From: MarkBaker Date: Sun, 13 Jun 2021 22:41:10 +0200 Subject: [PATCH 20/20] Update Change log --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 4825193a..ba42e6bd 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -29,6 +29,7 @@ and this project adheres to [Semantic Versioning](https://semver.org). ### Fixed +- Xls Reader changing grey background to black in Excel template [Issue #2147](Changing grey background to black in Excel template) [PR #2156](https://github.com/PHPOffice/PhpSpreadsheet/pull/2156) - Column width and Row height styles in the Html Reader when the value includes a unit of measure. [Issue #2145](https://github.com/PHPOffice/PhpSpreadsheet/issues/2145).