From 924ec23eb07d39fc5e97acfd02e716c6112fa4ca Mon Sep 17 00:00:00 2001 From: MarkBaker Date: Mon, 23 May 2022 12:12:05 +0200 Subject: [PATCH 1/4] Don't load invalid Print Area, because this will corrupt the internal print area definition, and break on Writing --- CHANGELOG.md | 2 +- src/PhpSpreadsheet/Reader/Xlsx.php | 9 +++++++-- 2 files changed, 8 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index a53df5cf..8f9bc6b4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -32,7 +32,7 @@ and this project adheres to [Semantic Versioning](https://semver.org). - Xls Reader resolving absolute named ranges to relative ranges [Issue #2826](https://github.com/PHPOffice/PhpSpreadsheet/issues/2826) [PR #2827](https://github.com/PHPOffice/PhpSpreadsheet/pull/2827) - Null value handling in the Excel Math/Trig PRODUCT() function [Issue #2833](https://github.com/PHPOffice/PhpSpreadsheet/issues/2833) [PR #2834](https://github.com/PHPOffice/PhpSpreadsheet/pull/2834) - +- Invalid Print Area defined in Xlsx corrupts internal storage of print area [Issue #2848](https://github.com/PHPOffice/PhpSpreadsheet/issues/2848) [PR #2849](https://github.com/PHPOffice/PhpSpreadsheet/pull/2849) ## 1.23.0 - 2022-04-24 diff --git a/src/PhpSpreadsheet/Reader/Xlsx.php b/src/PhpSpreadsheet/Reader/Xlsx.php index ecae5ad7..8eadb39c 100644 --- a/src/PhpSpreadsheet/Reader/Xlsx.php +++ b/src/PhpSpreadsheet/Reader/Xlsx.php @@ -1530,13 +1530,18 @@ class Xlsx extends BaseReader $rangeSets = preg_split("/('?(?:.*?)'?(?:![A-Z0-9]+:[A-Z0-9]+)),?/", $extractedRange, -1, PREG_SPLIT_NO_EMPTY | PREG_SPLIT_DELIM_CAPTURE); $newRangeSets = []; foreach ($rangeSets as $rangeSet) { - [$sheetName, $rangeSet] = Worksheet::extractSheetTitle($rangeSet, true); + [, $rangeSet] = Worksheet::extractSheetTitle($rangeSet, true); + if (empty($rangeSet)) { + continue; + } if (strpos($rangeSet, ':') === false) { $rangeSet = $rangeSet . ':' . $rangeSet; } $newRangeSets[] = str_replace('$', '', $rangeSet); } - $docSheet->getPageSetup()->setPrintArea(implode(',', $newRangeSets)); + if (count($newRangeSets) > 0) { + $docSheet->getPageSetup()->setPrintArea(implode(',', $newRangeSets)); + } break; default: From 2e5ebea1101c0a9ba61b5bee3d8a1010e49e6975 Mon Sep 17 00:00:00 2001 From: MarkBaker Date: Mon, 23 May 2022 13:38:08 +0200 Subject: [PATCH 2/4] Add support for writing Worksheet Visibility for Ods --- CHANGELOG.md | 2 + src/PhpSpreadsheet/Writer/Ods/Cell/Style.php | 22 ++++++++++ src/PhpSpreadsheet/Writer/Ods/Content.php | 3 ++ .../Writer/Ods/ContentTest.php | 23 ++++++++++ tests/data/Writer/Ods/content-empty.xml | 5 ++- .../Writer/Ods/content-hidden-worksheet.xml | 42 +++++++++++++++++++ tests/data/Writer/Ods/content-with-data.xml | 10 ++++- 7 files changed, 104 insertions(+), 3 deletions(-) create mode 100644 tests/data/Writer/Ods/content-hidden-worksheet.xml diff --git a/CHANGELOG.md b/CHANGELOG.md index a53df5cf..fd1a6eb5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -14,6 +14,8 @@ and this project adheres to [Semantic Versioning](https://semver.org). Note that a ChartSheet is still only written as a normal Worksheet containing a single chart, not as an actual ChartSheet. +- Added Worksheet visibility in Ods Writer [PR #2850](https://github.com/PHPOffice/PhpSpreadsheet/pull/2850) + ### Changed - Memory and speed improvements, particularly for the Cell Collection, and the Writers. diff --git a/src/PhpSpreadsheet/Writer/Ods/Cell/Style.php b/src/PhpSpreadsheet/Writer/Ods/Cell/Style.php index 66194468..1bf2c463 100644 --- a/src/PhpSpreadsheet/Writer/Ods/Cell/Style.php +++ b/src/PhpSpreadsheet/Writer/Ods/Cell/Style.php @@ -10,12 +10,14 @@ use PhpOffice\PhpSpreadsheet\Style\Font; use PhpOffice\PhpSpreadsheet\Style\Style as CellStyle; use PhpOffice\PhpSpreadsheet\Worksheet\ColumnDimension; use PhpOffice\PhpSpreadsheet\Worksheet\RowDimension; +use PhpOffice\PhpSpreadsheet\Worksheet\Worksheet; class Style { public const CELL_STYLE_PREFIX = 'ce'; public const COLUMN_STYLE_PREFIX = 'co'; public const ROW_STYLE_PREFIX = 'ro'; + public const TABLE_STYLE_PREFIX = 'ta'; private $writer; @@ -221,6 +223,26 @@ class Style $this->writer->endElement(); // Close style:style } + public function writeTableStyle(Worksheet $worksheet, int $sheetId): void + { + $this->writer->startElement('style:style'); + $this->writer->writeAttribute('style:family', 'table'); + $this->writer->writeAttribute( + 'style:name', + sprintf('%s%d', self::TABLE_STYLE_PREFIX, $sheetId) + ); + + $this->writer->startElement('style:table-properties'); + + $this->writer->writeAttribute( + 'table:display', + $worksheet->getSheetState() === Worksheet::SHEETSTATE_VISIBLE ? 'true' : 'false' + ); + + $this->writer->endElement(); // Close style:table-properties + $this->writer->endElement(); // Close style:style + } + public function write(CellStyle $style): void { $this->writer->startElement('style:style'); diff --git a/src/PhpSpreadsheet/Writer/Ods/Content.php b/src/PhpSpreadsheet/Writer/Ods/Content.php index a8da1019..00ab0643 100644 --- a/src/PhpSpreadsheet/Writer/Ods/Content.php +++ b/src/PhpSpreadsheet/Writer/Ods/Content.php @@ -123,6 +123,7 @@ class Content extends WriterPart for ($sheetIndex = 0; $sheetIndex < $sheetCount; ++$sheetIndex) { $objWriter->startElement('table:table'); $objWriter->writeAttribute('table:name', $spreadsheet->getSheet($sheetIndex)->getTitle()); + $objWriter->writeAttribute('table:style-name', Style::TABLE_STYLE_PREFIX . (string) ($sheetIndex + 1)); $objWriter->writeElement('office:forms'); foreach ($spreadsheet->getSheet($sheetIndex)->getColumnDimensions() as $columnDimension) { $objWriter->startElement('table:table-column'); @@ -289,6 +290,8 @@ class Content extends WriterPart $sheetCount = $spreadsheet->getSheetCount(); for ($i = 0; $i < $sheetCount; ++$i) { $worksheet = $spreadsheet->getSheet($i); + $styleWriter->writeTableStyle($worksheet, $i + 1); + $worksheet->calculateColumnWidths(); foreach ($worksheet->getColumnDimensions() as $columnDimension) { if ($columnDimension->getWidth() !== -1.0) { diff --git a/tests/PhpSpreadsheetTests/Writer/Ods/ContentTest.php b/tests/PhpSpreadsheetTests/Writer/Ods/ContentTest.php index 917a0410..81c244a7 100644 --- a/tests/PhpSpreadsheetTests/Writer/Ods/ContentTest.php +++ b/tests/PhpSpreadsheetTests/Writer/Ods/ContentTest.php @@ -10,6 +10,7 @@ use PhpOffice\PhpSpreadsheet\Style\Color; use PhpOffice\PhpSpreadsheet\Style\Fill; use PhpOffice\PhpSpreadsheet\Style\Font; use PhpOffice\PhpSpreadsheet\Style\NumberFormat; +use PhpOffice\PhpSpreadsheet\Worksheet\Worksheet; use PhpOffice\PhpSpreadsheet\Writer\Ods; use PhpOffice\PhpSpreadsheet\Writer\Ods\Content; use PHPUnit\Framework\TestCase; @@ -106,4 +107,26 @@ class ContentTest extends TestCase self::assertXmlStringEqualsXmlFile($this->samplesPath . '/content-with-data.xml', $xml); } + + public function testWriteWithHiddenWorksheet(): void + { + $workbook = new Spreadsheet(); + + // Worksheet 1 + $worksheet1 = $workbook->getActiveSheet(); + $worksheet1->setCellValue('A1', 1); + + // Worksheet 2 + $worksheet2 = $workbook->createSheet(); + $worksheet2->setTitle('New Worksheet'); + $worksheet2->setCellValue('A1', 2); + + $worksheet2->setSheetState(Worksheet::SHEETSTATE_HIDDEN); + + // Write + $content = new Content(new Ods($workbook)); + $xml = $content->write(); + + self::assertXmlStringEqualsXmlFile($this->samplesPath . '/content-hidden-worksheet.xml', $xml); + } } diff --git a/tests/data/Writer/Ods/content-empty.xml b/tests/data/Writer/Ods/content-empty.xml index 867cfa3e..84f4c239 100644 --- a/tests/data/Writer/Ods/content-empty.xml +++ b/tests/data/Writer/Ods/content-empty.xml @@ -3,6 +3,9 @@ + + + @@ -12,7 +15,7 @@ - + diff --git a/tests/data/Writer/Ods/content-hidden-worksheet.xml b/tests/data/Writer/Ods/content-hidden-worksheet.xml new file mode 100644 index 00000000..88a53257 --- /dev/null +++ b/tests/data/Writer/Ods/content-hidden-worksheet.xml @@ -0,0 +1,42 @@ + + + + + + + + + + + + + + + + + + + + + + + + + 1 + + + + + + + + + 2 + + + + + + + + \ No newline at end of file diff --git a/tests/data/Writer/Ods/content-with-data.xml b/tests/data/Writer/Ods/content-with-data.xml index fff47f65..12140fa9 100644 --- a/tests/data/Writer/Ods/content-with-data.xml +++ b/tests/data/Writer/Ods/content-with-data.xml @@ -3,6 +3,12 @@ + + + + + + @@ -62,7 +68,7 @@ - + @@ -107,7 +113,7 @@ - + From 3fae29d6134f42b9b555e6b7ccf17a6eed370532 Mon Sep 17 00:00:00 2001 From: MarkBaker Date: Mon, 23 May 2022 15:30:45 +0200 Subject: [PATCH 3/4] Add support for reading Worksheet Visibility for Ods --- CHANGELOG.md | 2 + phpstan-baseline.neon | 25 -------- src/PhpSpreadsheet/Reader/Ods.php | 1 + .../Reader/Ods/PageSettings.php | 47 +++++++++++++++ .../Reader/Ods/HiddenWorksheetTest.php | 57 ++++++++++++++++++ tests/data/Reader/Ods/HiddenSheet.ods | Bin 0 -> 2821 bytes 6 files changed, 107 insertions(+), 25 deletions(-) create mode 100644 tests/PhpSpreadsheetTests/Reader/Ods/HiddenWorksheetTest.php create mode 100644 tests/data/Reader/Ods/HiddenSheet.ods diff --git a/CHANGELOG.md b/CHANGELOG.md index a53df5cf..779f435f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -14,6 +14,8 @@ and this project adheres to [Semantic Versioning](https://semver.org). Note that a ChartSheet is still only written as a normal Worksheet containing a single chart, not as an actual ChartSheet. +- Added Worksheet visibility in Ods Reader [PR #2851](https://github.com/PHPOffice/PhpSpreadsheet/pull/2851) + ### Changed - Memory and speed improvements, particularly for the Cell Collection, and the Writers. diff --git a/phpstan-baseline.neon b/phpstan-baseline.neon index eabedcc8..ad2777a0 100644 --- a/phpstan-baseline.neon +++ b/phpstan-baseline.neon @@ -1945,36 +1945,11 @@ parameters: count: 6 path: src/PhpSpreadsheet/Reader/Ods/PageSettings.php - - - message: "#^Property PhpOffice\\\\PhpSpreadsheet\\\\Reader\\\\Ods\\\\PageSettings\\:\\:\\$masterPrintStylesCrossReference has no type specified\\.$#" - count: 1 - path: src/PhpSpreadsheet/Reader/Ods/PageSettings.php - - - - message: "#^Property PhpOffice\\\\PhpSpreadsheet\\\\Reader\\\\Ods\\\\PageSettings\\:\\:\\$masterStylesCrossReference has no type specified\\.$#" - count: 1 - path: src/PhpSpreadsheet/Reader/Ods/PageSettings.php - - - - message: "#^Property PhpOffice\\\\PhpSpreadsheet\\\\Reader\\\\Ods\\\\PageSettings\\:\\:\\$officeNs has no type specified\\.$#" - count: 1 - path: src/PhpSpreadsheet/Reader/Ods/PageSettings.php - - message: "#^Property PhpOffice\\\\PhpSpreadsheet\\\\Reader\\\\Ods\\\\PageSettings\\:\\:\\$pageLayoutStyles has no type specified\\.$#" count: 1 path: src/PhpSpreadsheet/Reader/Ods/PageSettings.php - - - message: "#^Property PhpOffice\\\\PhpSpreadsheet\\\\Reader\\\\Ods\\\\PageSettings\\:\\:\\$stylesFo has no type specified\\.$#" - count: 1 - path: src/PhpSpreadsheet/Reader/Ods/PageSettings.php - - - - message: "#^Property PhpOffice\\\\PhpSpreadsheet\\\\Reader\\\\Ods\\\\PageSettings\\:\\:\\$stylesNs has no type specified\\.$#" - count: 1 - path: src/PhpSpreadsheet/Reader/Ods/PageSettings.php - - message: "#^Method PhpOffice\\\\PhpSpreadsheet\\\\Reader\\\\Ods\\\\Properties\\:\\:load\\(\\) has parameter \\$namespacesMeta with no type specified\\.$#" count: 1 diff --git a/src/PhpSpreadsheet/Reader/Ods.php b/src/PhpSpreadsheet/Reader/Ods.php index 27c58edb..7e776ab7 100644 --- a/src/PhpSpreadsheet/Reader/Ods.php +++ b/src/PhpSpreadsheet/Reader/Ods.php @@ -588,6 +588,7 @@ class Ods extends BaseReader break; } } + $pageSettings->setVisibilityForWorksheet($spreadsheet->getActiveSheet(), $worksheetStyleName); $pageSettings->setPrintSettingsForWorksheet($spreadsheet->getActiveSheet(), $worksheetStyleName); ++$worksheetID; } diff --git a/src/PhpSpreadsheet/Reader/Ods/PageSettings.php b/src/PhpSpreadsheet/Reader/Ods/PageSettings.php index 8d24fd0c..4d2fd992 100644 --- a/src/PhpSpreadsheet/Reader/Ods/PageSettings.php +++ b/src/PhpSpreadsheet/Reader/Ods/PageSettings.php @@ -8,16 +8,41 @@ use PhpOffice\PhpSpreadsheet\Worksheet\Worksheet; class PageSettings { + /** + * @var string + */ private $officeNs; + /** + * @var string + */ private $stylesNs; + /** + * @var string + */ private $stylesFo; + /** + * @var string + */ + private $tableNs; + + /** + * @var string[] + */ + private $tableStylesCrossReference = []; + private $pageLayoutStyles = []; + /** + * @var string[] + */ private $masterStylesCrossReference = []; + /** + * @var string[] + */ private $masterPrintStylesCrossReference = []; public function __construct(DOMDocument $styleDom) @@ -32,6 +57,7 @@ class PageSettings $this->officeNs = $styleDom->lookupNamespaceUri('office'); $this->stylesNs = $styleDom->lookupNamespaceUri('style'); $this->stylesFo = $styleDom->lookupNamespaceUri('fo'); + $this->tableNs = $styleDom->lookupNamespaceUri('table'); } private function readPageSettingStyles(DOMDocument $styleDom): void @@ -98,12 +124,33 @@ class PageSettings foreach ($styleXReferences as $styleXreferenceSet) { $styleXRefName = $styleXreferenceSet->getAttributeNS($this->stylesNs, 'name'); $stylePageLayoutName = $styleXreferenceSet->getAttributeNS($this->stylesNs, 'master-page-name'); + $styleFamilyName = $styleXreferenceSet->getAttributeNS($this->stylesNs, 'family'); + if (!empty($styleFamilyName) && $styleFamilyName === 'table') { + $styleVisibility = 'true'; + foreach ($styleXreferenceSet->getElementsByTagNameNS($this->stylesNs, 'table-properties') as $tableProperties) { + $styleVisibility = $tableProperties->getAttributeNS($this->tableNs, 'display'); + } + $this->tableStylesCrossReference[$styleXRefName] = $styleVisibility; + } if (!empty($stylePageLayoutName)) { $this->masterStylesCrossReference[$styleXRefName] = $stylePageLayoutName; } } } + public function setVisibilityForWorksheet(Worksheet $worksheet, string $styleName): void + { + if (!array_key_exists($styleName, $this->tableStylesCrossReference)) { + return; + } + + $worksheet->setSheetState( + $this->tableStylesCrossReference[$styleName] === 'false' + ? Worksheet::SHEETSTATE_HIDDEN + : Worksheet::SHEETSTATE_VISIBLE + ); + } + public function setPrintSettingsForWorksheet(Worksheet $worksheet, string $styleName): void { if (!array_key_exists($styleName, $this->masterStylesCrossReference)) { diff --git a/tests/PhpSpreadsheetTests/Reader/Ods/HiddenWorksheetTest.php b/tests/PhpSpreadsheetTests/Reader/Ods/HiddenWorksheetTest.php new file mode 100644 index 00000000..9edaa9b7 --- /dev/null +++ b/tests/PhpSpreadsheetTests/Reader/Ods/HiddenWorksheetTest.php @@ -0,0 +1,57 @@ +spreadsheet = $reader->load($filename); + } + + public function testPageSetup(): void + { + $assertions = $this->worksheetAssertions(); + + foreach ($this->spreadsheet->getAllSheets() as $worksheet) { + if (!array_key_exists($worksheet->getTitle(), $assertions)) { + continue; + } + + $sheetAssertions = $assertions[$worksheet->getTitle()]; + foreach ($sheetAssertions as $test => $expectedResult) { + $testMethodName = 'get' . ucfirst($test); + $actualResult = $worksheet->getSheetState(); + self::assertSame( + $expectedResult, + $actualResult, + "Failed asserting sheet state {$expectedResult} for Worksheet '{$worksheet->getTitle()}' {$test}" + ); + } + } + } + + private function worksheetAssertions(): array + { + return [ + 'Sheet1' => [ + 'sheetState' => Worksheet::SHEETSTATE_VISIBLE, + ], + 'Sheet2' => [ + 'sheetState' => Worksheet::SHEETSTATE_HIDDEN, + ], + ]; + } +} diff --git a/tests/data/Reader/Ods/HiddenSheet.ods b/tests/data/Reader/Ods/HiddenSheet.ods new file mode 100644 index 0000000000000000000000000000000000000000..9b1d01a69cfd9833776b874d4ecb1d669a344d79 GIT binary patch literal 2821 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 Date: Mon, 23 May 2022 09:21:52 -0700 Subject: [PATCH 4/4] Eliminate Corruption in Surface Chart Samples (#2846) * Eliminate Corruption in Surface Chart Samples Fix #2763 (bubble charts were fixed earlier). * Improvement in Testing for Numeric Values The original proposal was to test for numeric values by checking whether the format was NUMERIC or DATE. This is pretty inflexible, and left open a wide gap concerning what to do with GENERAL. Changed to allow the user to set a `numeric` property at the same time as `axisNumberProperties`, eliminating these concerns. User can set property when chart is created (new test member Charts32CatAxValAxTest), or can set it when spreadsheet with chart is read (new method testCatAxValAx in Charts32XmlTest). Sample 33_Chart_create_scatter2 creates a slightly wider chart when `numeric` is set to true than it does when unset, but chart is otherwise the same. Corrected a bug which occured twice in Chart. Methods getChartAxisX and getChartAxisY created a new Axis object if the chart pointer was null, but neglected to attach the new object to the chart. Thus, subsequent calls to the methods return different objects. Code is changed to attach the new Axis to the Chart. --- phpstan-baseline.neon | 20 -- samples/Chart/33_Chart_create_scatter2.php | 8 +- src/PhpSpreadsheet/Chart/Axis.php | 21 ++- src/PhpSpreadsheet/Chart/Chart.php | 66 ++++++- src/PhpSpreadsheet/Chart/Properties.php | 1 + src/PhpSpreadsheet/Reader/Xlsx/Chart.php | 20 ++ src/PhpSpreadsheet/Writer/Xlsx/Chart.php | 111 ++++++++++-- .../Writer/Xlsx/Charts32CatAxValAxTest.php | 171 ++++++++++++++++++ .../Writer/Xlsx/Charts32XmlTest.php | 58 ++++++ 9 files changed, 434 insertions(+), 42 deletions(-) create mode 100644 tests/PhpSpreadsheetTests/Writer/Xlsx/Charts32CatAxValAxTest.php diff --git a/phpstan-baseline.neon b/phpstan-baseline.neon index ad2777a0..f031bfe6 100644 --- a/phpstan-baseline.neon +++ b/phpstan-baseline.neon @@ -4775,26 +4775,6 @@ parameters: count: 2 path: src/PhpSpreadsheet/Writer/Xlsx/Chart.php - - - message: "#^Parameter \\#3 \\$id1 of method PhpOffice\\\\PhpSpreadsheet\\\\Writer\\\\Xlsx\\\\Chart\\:\\:writeCategoryAxis\\(\\) expects string, int\\|string given\\.$#" - count: 1 - path: src/PhpSpreadsheet/Writer/Xlsx/Chart.php - - - - message: "#^Parameter \\#4 \\$id1 of method PhpOffice\\\\PhpSpreadsheet\\\\Writer\\\\Xlsx\\\\Chart\\:\\:writeValueAxis\\(\\) expects string, int\\|string given\\.$#" - count: 2 - path: src/PhpSpreadsheet/Writer/Xlsx/Chart.php - - - - message: "#^Parameter \\#4 \\$id2 of method PhpOffice\\\\PhpSpreadsheet\\\\Writer\\\\Xlsx\\\\Chart\\:\\:writeCategoryAxis\\(\\) expects string, int\\|string given\\.$#" - count: 1 - path: src/PhpSpreadsheet/Writer/Xlsx/Chart.php - - - - message: "#^Parameter \\#5 \\$id2 of method PhpOffice\\\\PhpSpreadsheet\\\\Writer\\\\Xlsx\\\\Chart\\:\\:writeValueAxis\\(\\) expects string, int\\|string given\\.$#" - count: 2 - path: src/PhpSpreadsheet/Writer/Xlsx/Chart.php - - message: "#^Parameter \\#6 \\$yAxis of method PhpOffice\\\\PhpSpreadsheet\\\\Writer\\\\Xlsx\\\\Chart\\:\\:writeCategoryAxis\\(\\) expects PhpOffice\\\\PhpSpreadsheet\\\\Chart\\\\Axis, PhpOffice\\\\PhpSpreadsheet\\\\Chart\\\\Axis\\|null given\\.$#" count: 1 diff --git a/samples/Chart/33_Chart_create_scatter2.php b/samples/Chart/33_Chart_create_scatter2.php index 59d6bd3b..1d6e331c 100644 --- a/samples/Chart/33_Chart_create_scatter2.php +++ b/samples/Chart/33_Chart_create_scatter2.php @@ -25,7 +25,7 @@ $worksheet->fromArray( ['=DATEVALUE("2021-01-10")', 30.2, 32.2, 0.2], ] ); -$worksheet->getStyle('A2:A5')->getNumberFormat()->setFormatCode('yyyy-mm-dd'); +$worksheet->getStyle('A2:A5')->getNumberFormat()->setFormatCode(Properties::FORMAT_CODE_DATE_ISO8601); $worksheet->getColumnDimension('A')->setAutoSize(true); $worksheet->setSelectedCells('A1'); @@ -67,7 +67,7 @@ $dataSeriesValues = [ // Added so that Xaxis shows dates instead of Excel-equivalent-year1900-numbers $xAxis = new Axis(); //$xAxis->setAxisNumberProperties(Properties::FORMAT_CODE_DATE ); -$xAxis->setAxisNumberProperties('yyyy-mm-dd'); +$xAxis->setAxisNumberProperties(Properties::FORMAT_CODE_DATE_ISO8601, true); // Build the dataseries $series = new DataSeries( @@ -78,7 +78,7 @@ $series = new DataSeries( $xAxisTickValues, // plotCategory $dataSeriesValues, // plotValues null, // plotDirection - null, // smooth line + false, // smooth line //DataSeries::STYLE_LINEMARKER // plotStyle DataSeries::STYLE_MARKER // plotStyle ); @@ -107,7 +107,7 @@ $chart = new Chart( // Set the position where the chart should appear in the worksheet $chart->setTopLeftPosition('A7'); -$chart->setBottomRightPosition('N20'); +$chart->setBottomRightPosition('P20'); // Add the chart to the worksheet $worksheet->addChart($chart); diff --git a/src/PhpSpreadsheet/Chart/Axis.php b/src/PhpSpreadsheet/Chart/Axis.php index eeed326d..089ebdb9 100644 --- a/src/PhpSpreadsheet/Chart/Axis.php +++ b/src/PhpSpreadsheet/Chart/Axis.php @@ -18,6 +18,7 @@ class Axis extends Properties private $axisNumber = [ 'format' => self::FORMAT_CODE_GENERAL, 'source_linked' => 1, + 'numeric' => null, ]; /** @@ -131,15 +132,26 @@ class Axis extends Properties 'size' => null, ]; + private const NUMERIC_FORMAT = [ + Properties::FORMAT_CODE_NUMBER, + Properties::FORMAT_CODE_DATE, + ]; + /** * Get Series Data Type. * * @param mixed $format_code */ - public function setAxisNumberProperties($format_code): void + public function setAxisNumberProperties($format_code, ?bool $numeric = null): void { - $this->axisNumber['format'] = (string) $format_code; + $format = (string) $format_code; + $this->axisNumber['format'] = $format; $this->axisNumber['source_linked'] = 0; + if (is_bool($numeric)) { + $this->axisNumber['numeric'] = $numeric; + } elseif (in_array($format, self::NUMERIC_FORMAT, true)) { + $this->axisNumber['numeric'] = true; + } } /** @@ -162,6 +174,11 @@ class Axis extends Properties return (string) $this->axisNumber['source_linked']; } + public function getAxisIsNumericFormat(): bool + { + return (bool) $this->axisNumber['numeric']; + } + /** * Set Axis Options Properties. * diff --git a/src/PhpSpreadsheet/Chart/Chart.php b/src/PhpSpreadsheet/Chart/Chart.php index 2efec5bb..80d3d5f3 100644 --- a/src/PhpSpreadsheet/Chart/Chart.php +++ b/src/PhpSpreadsheet/Chart/Chart.php @@ -140,6 +140,18 @@ class Chart */ private $bottomRightYOffset = 10; + /** @var ?int */ + private $rotX; + + /** @var ?int */ + private $rotY; + + /** @var ?int */ + private $rAngAx; + + /** @var ?int */ + private $perspective; + /** * Create a new Chart. * @@ -351,8 +363,9 @@ class Chart if ($this->yAxis !== null) { return $this->yAxis; } + $this->yAxis = new Axis(); - return new Axis(); + return $this->yAxis; } /** @@ -365,8 +378,9 @@ class Chart if ($this->xAxis !== null) { return $this->xAxis; } + $this->xAxis = new Axis(); - return new Axis(); + return $this->xAxis; } /** @@ -681,4 +695,52 @@ class Chart return $renderer->render($outputDestination); } + + public function getRotX(): ?int + { + return $this->rotX; + } + + public function setRotX(?int $rotX): self + { + $this->rotX = $rotX; + + return $this; + } + + public function getRotY(): ?int + { + return $this->rotY; + } + + public function setRotY(?int $rotY): self + { + $this->rotY = $rotY; + + return $this; + } + + public function getRAngAx(): ?int + { + return $this->rAngAx; + } + + public function setRAngAx(?int $rAngAx): self + { + $this->rAngAx = $rAngAx; + + return $this; + } + + public function getPerspective(): ?int + { + return $this->perspective; + } + + public function setPerspective(?int $perspective): self + { + $this->perspective = $perspective; + + return $this; + } } diff --git a/src/PhpSpreadsheet/Chart/Properties.php b/src/PhpSpreadsheet/Chart/Properties.php index ef22fb52..9f1a5ce0 100644 --- a/src/PhpSpreadsheet/Chart/Properties.php +++ b/src/PhpSpreadsheet/Chart/Properties.php @@ -37,6 +37,7 @@ abstract class Properties const FORMAT_CODE_CURRENCY = '$#,##0.00'; const FORMAT_CODE_ACCOUNTING = '_($* #,##0.00_);_($* (#,##0.00);_($* "-"??_);_(@_)'; const FORMAT_CODE_DATE = 'm/d/yyyy'; + const FORMAT_CODE_DATE_ISO8601 = 'yyyy-mm-dd'; const FORMAT_CODE_TIME = '[$-F400]h:mm:ss AM/PM'; const FORMAT_CODE_PERCENTAGE = '0.00%'; const FORMAT_CODE_FRACTION = '# ?/?'; diff --git a/src/PhpSpreadsheet/Reader/Xlsx/Chart.php b/src/PhpSpreadsheet/Reader/Xlsx/Chart.php index 4480c6ff..e3498e20 100644 --- a/src/PhpSpreadsheet/Reader/Xlsx/Chart.php +++ b/src/PhpSpreadsheet/Reader/Xlsx/Chart.php @@ -55,12 +55,20 @@ class Chart $XaxisLabel = $YaxisLabel = $legend = $title = null; $dispBlanksAs = $plotVisOnly = null; $plotArea = null; + $rotX = $rotY = $rAngAx = $perspective = null; foreach ($chartElementsC as $chartElementKey => $chartElement) { switch ($chartElementKey) { case 'chart': foreach ($chartElement as $chartDetailsKey => $chartDetails) { $chartDetailsC = $chartDetails->children($namespacesChartMeta['c']); switch ($chartDetailsKey) { + case 'view3D': + $rotX = self::getAttribute($chartDetails->rotX, 'val', 'integer'); + $rotY = self::getAttribute($chartDetails->rotY, 'val', 'integer'); + $rAngAx = self::getAttribute($chartDetails->rAngAx, 'val', 'integer'); + $perspective = self::getAttribute($chartDetails->perspective, 'val', 'integer'); + + break; case 'plotArea': $plotAreaLayout = $XaxisLabel = $YaxisLabel = null; $plotSeries = $plotAttributes = []; @@ -220,6 +228,18 @@ class Chart } } $chart = new \PhpOffice\PhpSpreadsheet\Chart\Chart($chartName, $title, $legend, $plotArea, $plotVisOnly, $dispBlanksAs, $XaxisLabel, $YaxisLabel); + if (is_int($rotX)) { + $chart->setRotX($rotX); + } + if (is_int($rotY)) { + $chart->setRotY($rotY); + } + if (is_int($rAngAx)) { + $chart->setRAngAx($rAngAx); + } + if (is_int($perspective)) { + $chart->setPerspective($perspective); + } return $chart; } diff --git a/src/PhpSpreadsheet/Writer/Xlsx/Chart.php b/src/PhpSpreadsheet/Writer/Xlsx/Chart.php index 0f65ae3b..77cbb872 100644 --- a/src/PhpSpreadsheet/Writer/Xlsx/Chart.php +++ b/src/PhpSpreadsheet/Writer/Xlsx/Chart.php @@ -9,7 +9,6 @@ use PhpOffice\PhpSpreadsheet\Chart\GridLines; use PhpOffice\PhpSpreadsheet\Chart\Layout; use PhpOffice\PhpSpreadsheet\Chart\Legend; use PhpOffice\PhpSpreadsheet\Chart\PlotArea; -use PhpOffice\PhpSpreadsheet\Chart\Properties; use PhpOffice\PhpSpreadsheet\Chart\Title; use PhpOffice\PhpSpreadsheet\Shared\XMLWriter; use PhpOffice\PhpSpreadsheet\Writer\Exception as WriterException; @@ -75,6 +74,33 @@ class Chart extends WriterPart $objWriter->writeAttribute('val', 0); $objWriter->endElement(); + $objWriter->startElement('c:view3D'); + $rotX = $chart->getRotX(); + if (is_int($rotX)) { + $objWriter->startElement('c:rotX'); + $objWriter->writeAttribute('val', "$rotX"); + $objWriter->endElement(); + } + $rotY = $chart->getRotY(); + if (is_int($rotY)) { + $objWriter->startElement('c:rotY'); + $objWriter->writeAttribute('val', "$rotY"); + $objWriter->endElement(); + } + $rAngAx = $chart->getRAngAx(); + if (is_int($rAngAx)) { + $objWriter->startElement('c:rAngAx'); + $objWriter->writeAttribute('val', "$rAngAx"); + $objWriter->endElement(); + } + $perspective = $chart->getPerspective(); + if (is_int($perspective)) { + $objWriter->startElement('c:perspective'); + $objWriter->writeAttribute('val', "$perspective"); + $objWriter->endElement(); + } + $objWriter->endElement(); // view3D + $this->writePlotArea($objWriter, $chart->getPlotArea(), $chart->getXAxisLabel(), $chart->getYAxisLabel(), $chart->getChartAxisX(), $chart->getChartAxisY(), $chart->getMajorGridlines(), $chart->getMinorGridlines()); $this->writeLegend($objWriter, $chart->getLegend()); @@ -200,7 +226,7 @@ class Chart extends WriterPart return; } - $id1 = $id2 = 0; + $id1 = $id2 = $id3 = '0'; $this->seriesIndex = 0; $objWriter->startElement('c:plotArea'); @@ -230,6 +256,10 @@ class Chart extends WriterPart $objWriter->startElement('c:scatterStyle'); $objWriter->writeAttribute('val', $plotStyle); $objWriter->endElement(); + } elseif ($groupType === DataSeries::TYPE_SURFACECHART_3D || $groupType === DataSeries::TYPE_SURFACECHART) { + $objWriter->startElement('c:wireframe'); + $objWriter->writeAttribute('val', $plotStyle ? '1' : '0'); + $objWriter->endElement(); } $this->writePlotGroup($plotGroup, $chartType, $objWriter, $catIsMultiLevelSeries, $valIsMultiLevelSeries, $plotGroupingType); @@ -280,9 +310,10 @@ class Chart extends WriterPart $objWriter->endElement(); } - // Generate 2 unique numbers to use for axId values - $id1 = '75091328'; - $id2 = '75089408'; + // Generate 3 unique numbers to use for axId values + $id1 = '110438656'; + $id2 = '110444544'; + $id3 = '110365312'; // used in Surface Chart if (($chartType !== DataSeries::TYPE_PIECHART) && ($chartType !== DataSeries::TYPE_PIECHART_3D) && ($chartType !== DataSeries::TYPE_DONUTCHART)) { $objWriter->startElement('c:axId'); @@ -291,6 +322,11 @@ class Chart extends WriterPart $objWriter->startElement('c:axId'); $objWriter->writeAttribute('val', $id2); $objWriter->endElement(); + if ($chartType === DataSeries::TYPE_SURFACECHART_3D || $chartType === DataSeries::TYPE_SURFACECHART) { + $objWriter->startElement('c:axId'); + $objWriter->writeAttribute('val', $id3); + $objWriter->endElement(); + } } else { $objWriter->startElement('c:firstSliceAng'); $objWriter->writeAttribute('val', 0); @@ -314,6 +350,9 @@ class Chart extends WriterPart } $this->writeValueAxis($objWriter, $yAxisLabel, $chartType, $id1, $id2, $valIsMultiLevelSeries, $yAxis, $majorGridlines, $minorGridlines); + if ($chartType === DataSeries::TYPE_SURFACECHART_3D || $chartType === DataSeries::TYPE_SURFACECHART) { + $this->writeSerAxis($objWriter, $id2, $id3); + } } $objWriter->endElement(); @@ -375,17 +414,13 @@ class Chart extends WriterPart { // N.B. writeCategoryAxis may be invoked with the last parameter($yAxis) using $xAxis for ScatterChart, etc // In that case, xAxis is NOT a category. - $AxisFormat = $yAxis->getAxisNumberFormat(); - if ( - $AxisFormat === Properties::FORMAT_CODE_DATE - || $AxisFormat == Properties::FORMAT_CODE_NUMBER - ) { + if ($yAxis->getAxisIsNumericFormat()) { $objWriter->startElement('c:valAx'); } else { $objWriter->startElement('c:catAx'); } - if ($id1 > 0) { + if ($id1 !== '0') { $objWriter->startElement('c:axId'); $objWriter->writeAttribute('val', $id1); $objWriter->endElement(); @@ -459,7 +494,7 @@ class Chart extends WriterPart $objWriter->writeAttribute('val', $yAxis->getAxisOptionsProperty('axis_labels')); $objWriter->endElement(); - if ($id2 > 0) { + if ($id2 !== '0') { $objWriter->startElement('c:crossAx'); $objWriter->writeAttribute('val', $id2); $objWriter->endElement(); @@ -501,7 +536,7 @@ class Chart extends WriterPart { $objWriter->startElement('c:valAx'); - if ($id2 > 0) { + if ($id2 !== '0') { $objWriter->startElement('c:axId'); $objWriter->writeAttribute('val', $id2); $objWriter->endElement(); @@ -926,7 +961,7 @@ class Chart extends WriterPart $objWriter->endElement(); //effectList $objWriter->endElement(); //end spPr - if ($id1 > 0) { + if ($id1 !== '0') { $objWriter->startElement('c:crossAx'); $objWriter->writeAttribute('val', $id2); $objWriter->endElement(); @@ -969,6 +1004,54 @@ class Chart extends WriterPart $objWriter->endElement(); } + /** + * Write Ser Axis, for Surface chart. + */ + private function writeSerAxis(XMLWriter $objWriter, string $id2, string $id3): void + { + $objWriter->startElement('c:serAx'); + + $objWriter->startElement('c:axId'); + $objWriter->writeAttribute('val', $id3); + $objWriter->endElement(); // axId + + $objWriter->startElement('c:scaling'); + $objWriter->startElement('c:orientation'); + $objWriter->writeAttribute('val', 'minMax'); + $objWriter->endElement(); // orientation + $objWriter->endElement(); // scaling + + $objWriter->startElement('c:delete'); + $objWriter->writeAttribute('val', '0'); + $objWriter->endElement(); // delete + + $objWriter->startElement('c:axPos'); + $objWriter->writeAttribute('val', 'b'); + $objWriter->endElement(); // axPos + + $objWriter->startElement('c:majorTickMark'); + $objWriter->writeAttribute('val', 'out'); + $objWriter->endElement(); // majorTickMark + + $objWriter->startElement('c:minorTickMark'); + $objWriter->writeAttribute('val', 'none'); + $objWriter->endElement(); // minorTickMark + + $objWriter->startElement('c:tickLblPos'); + $objWriter->writeAttribute('val', 'nextTo'); + $objWriter->endElement(); // tickLblPos + + $objWriter->startElement('c:crossAx'); + $objWriter->writeAttribute('val', $id2); + $objWriter->endElement(); // crossAx + + $objWriter->startElement('c:crosses'); + $objWriter->writeAttribute('val', 'autoZero'); + $objWriter->endElement(); // crosses + + $objWriter->endElement(); //serAx + } + /** * Get the data series type(s) for a chart plot series. * diff --git a/tests/PhpSpreadsheetTests/Writer/Xlsx/Charts32CatAxValAxTest.php b/tests/PhpSpreadsheetTests/Writer/Xlsx/Charts32CatAxValAxTest.php new file mode 100644 index 00000000..af33baa1 --- /dev/null +++ b/tests/PhpSpreadsheetTests/Writer/Xlsx/Charts32CatAxValAxTest.php @@ -0,0 +1,171 @@ +outputFileName !== '') { + unlink($this->outputFileName); + $this->outputFileName = ''; + } + } + + /** + * @dataProvider providerCatAxValAx + */ + public function test1CatAx1ValAx(?bool $numeric): void + { + $spreadsheet = new Spreadsheet(); + $worksheet = $spreadsheet->getActiveSheet(); + // changed data to simulate a trend chart - Xaxis are dates; Yaxis are 3 meausurements from each date + $worksheet->fromArray( + [ + ['', 'metric1', 'metric2', 'metric3'], + ['=DATEVALUE("2021-01-01")', 12.1, 15.1, 21.1], + ['=DATEVALUE("2021-01-04")', 56.2, 73.2, 86.2], + ['=DATEVALUE("2021-01-07")', 52.2, 61.2, 69.2], + ['=DATEVALUE("2021-01-10")', 30.2, 32.2, 0.2], + ] + ); + $worksheet->getStyle('A2:A5')->getNumberFormat()->setFormatCode(Properties::FORMAT_CODE_DATE_ISO8601); + $worksheet->getColumnDimension('A')->setAutoSize(true); + $worksheet->setSelectedCells('A1'); + + // Set the Labels for each data series we want to plot + // Datatype + // Cell reference for data + // Format Code + // Number of datapoints in series + // Data values + // Data Marker + $dataSeriesLabels = [ + new DataSeriesValues(DataSeriesValues::DATASERIES_TYPE_STRING, 'Worksheet!$B$1', null, 1), // was 2010 + new DataSeriesValues(DataSeriesValues::DATASERIES_TYPE_STRING, 'Worksheet!$C$1', null, 1), // was 2011 + new DataSeriesValues(DataSeriesValues::DATASERIES_TYPE_STRING, 'Worksheet!$D$1', null, 1), // was 2012 + ]; + // Set the X-Axis Labels + // changed from STRING to NUMBER + // added 2 additional x-axis values associated with each of the 3 metrics + // added FORMATE_CODE_NUMBER + $xAxisTickValues = [ + //new DataSeriesValues(DataSeriesValues::DATASERIES_TYPE_STRING, 'Worksheet!$A$2:$A$5', null, 4), // Q1 to Q4 + new DataSeriesValues(DataSeriesValues::DATASERIES_TYPE_NUMBER, 'Worksheet!$A$2:$A$5', Properties::FORMAT_CODE_DATE, 4), + new DataSeriesValues(DataSeriesValues::DATASERIES_TYPE_NUMBER, 'Worksheet!$A$2:$A$5', Properties::FORMAT_CODE_DATE, 4), + new DataSeriesValues(DataSeriesValues::DATASERIES_TYPE_NUMBER, 'Worksheet!$A$2:$A$5', Properties::FORMAT_CODE_DATE, 4), + ]; + // Set the Data values for each data series we want to plot + // Datatype + // Cell reference for data + // Format Code + // Number of datapoints in series + // Data values + // Data Marker + // added FORMAT_CODE_NUMBER + $dataSeriesValues = [ + new DataSeriesValues(DataSeriesValues::DATASERIES_TYPE_NUMBER, 'Worksheet!$B$2:$B$5', Properties::FORMAT_CODE_NUMBER, 4), + new DataSeriesValues(DataSeriesValues::DATASERIES_TYPE_NUMBER, 'Worksheet!$C$2:$C$5', Properties::FORMAT_CODE_NUMBER, 4), + new DataSeriesValues(DataSeriesValues::DATASERIES_TYPE_NUMBER, 'Worksheet!$D$2:$D$5', Properties::FORMAT_CODE_NUMBER, 4), + ]; + // Added so that Xaxis shows dates instead of Excel-equivalent-year1900-numbers + $xAxis = new Axis(); + //$xAxis->setAxisNumberProperties(Properties::FORMAT_CODE_DATE ); + if (is_bool($numeric)) { + $xAxis->setAxisNumberProperties(Properties::FORMAT_CODE_DATE_ISO8601, $numeric); + } else { + $xAxis->setAxisNumberProperties(Properties::FORMAT_CODE_DATE_ISO8601); + } + + // Build the dataseries + $series = new DataSeries( + DataSeries::TYPE_SCATTERCHART, // plotType + null, // plotGrouping (Scatter charts don't have any grouping) + range(0, count($dataSeriesValues) - 1), // plotOrder + $dataSeriesLabels, // plotLabel + $xAxisTickValues, // plotCategory + $dataSeriesValues, // plotValues + null, // plotDirection + false, // smooth line + //DataSeries::STYLE_LINEMARKER // plotStyle + DataSeries::STYLE_MARKER // plotStyle + ); + + // Set the series in the plot area + $plotArea = new PlotArea(null, [$series]); + // Set the chart legend + $legend = new ChartLegend(ChartLegend::POSITION_TOPRIGHT, null, false); + + $title = new Title('Test Scatter Trend Chart'); + $yAxisLabel = new Title('Value ($k)'); + + // Create the chart + $chart = new Chart( + 'chart1', // name + $title, // title + $legend, // legend + $plotArea, // plotArea + true, // plotVisibleOnly + DataSeries::EMPTY_AS_GAP, // displayBlanksAs + null, // xAxisLabel + $yAxisLabel, // yAxisLabel + // added xAxis for correct date display + $xAxis, // xAxis + ); + + // Set the position where the chart should appear in the worksheet + $chart->setTopLeftPosition('A7'); + $chart->setBottomRightPosition('P20'); + // Add the chart to the worksheet + $worksheet->addChart($chart); + + $writer = new XlsxWriter($spreadsheet); + $writer->setIncludeCharts(true); + $this->outputFileName = File::temporaryFilename(); + $writer->save($this->outputFileName); + $spreadsheet->disconnectWorksheets(); + + $file = 'zip://'; + $file .= $this->outputFileName; + $file .= '#xl/charts/chart1.xml'; + $data = file_get_contents($file); + // confirm that file contains expected tags + if ($data === false) { + self::fail('Unable to read file'); + } elseif ($numeric === true) { + self::assertSame(0, substr_count($data, '')); } } + + /** + * @dataProvider providerCatAxValAx + */ + public function testCatAxValAx(?bool $numeric): void + { + $file = self::DIRECTORY . '32readwriteScatterChart1.xlsx'; + $reader = new XlsxReader(); + $reader->setIncludeCharts(true); + $spreadsheet = $reader->load($file); + $sheet = $spreadsheet->getActiveSheet(); + $charts = $sheet->getChartCollection(); + self::assertCount(1, $charts); + $chart = $charts[0]; + self::assertNotNull($chart); + $xAxis = $chart->getChartAxisX(); + self::assertSame(Properties::FORMAT_CODE_GENERAL, $xAxis->getAxisNumberFormat()); + if (is_bool($numeric)) { + $xAxis->setAxisNumberProperties(Properties::FORMAT_CODE_GENERAL, true); + } + $yAxis = $chart->getChartAxisY(); + self::assertSame(Properties::FORMAT_CODE_GENERAL, $yAxis->getAxisNumberFormat()); + if (is_bool($numeric)) { + $xAxis->setAxisNumberProperties(Properties::FORMAT_CODE_GENERAL, $numeric); + $yAxis->setAxisNumberProperties(Properties::FORMAT_CODE_GENERAL, $numeric); + } + + $writer = new XlsxWriter($spreadsheet); + $writer->setIncludeCharts(true); + $this->outputFileName = File::temporaryFilename(); + $writer->save($this->outputFileName); + $spreadsheet->disconnectWorksheets(); + + $file = 'zip://'; + $file .= $this->outputFileName; + $file .= '#xl/charts/chart2.xml'; + $data = file_get_contents($file); + // confirm that file contains expected tags + if ($data === false) { + self::fail('Unable to read file'); + } elseif ($numeric === true) { + self::assertSame(0, substr_count($data, '')); + self::assertSame(2, substr_count($data, '')); + } else { + self::assertSame(1, substr_count($data, '')); + self::assertSame(1, substr_count($data, '')); + } + } + + public function providerCatAxValAx(): array + { + return [ + [true], + [false], + [null], + ]; + } }