From 656a7164e1a969437d02b7abeb3c3460ff5b07c0 Mon Sep 17 00:00:00 2001 From: oleibman <10341515+oleibman@users.noreply.github.com> Date: Thu, 9 Nov 2023 13:15:38 -0800 Subject: [PATCH] Address Some Chart Problems (#3771) * Address Some Chart Problems Fix #3767. The basic problem in that issue was user error, but it exposed a couple of problems in code which are being addressed. When a spreadsheet with charts is loaded without the includeCharts option and then saved, a corrupt spreadsheet is created. I believe this has been the case for quite some time. Nothing in the test suite covers this scenario. It is, in fact, a difficult thing to test, since the problem is exposed only when the file is opened through Excel. The specific problem is that a rels file generated by the output writer continues to refer to the drawing file which described the chart, but that file is not included (and not needed) in the output spreadsheet. The resolution is kludgey. The information that the file will not be needed is not available when the rels file is written. But, when it comes time to write the drawing file, it is known whether the rels file included it. So, if nothing else has caused the file to be generated, it is written out as a basically empty xml file after all. This solves the problem, but I will continue to search for a less kludgey solution. This solution is, at least, testable; if a different solution is applied later on, the test being introduced here is likely to break so a new one will be needed. When the provided spreadsheet is loaded with the includeCharts option and then saved, an error is exposed in processing the Chart Title caption when it doesn't exist. The change to Writer/Xlsx/Chart is simple and clearly justifiable. What is peculiar is that the error does not arise with release 1.29, but does arise with master. It is not at all clear to me what has changed since the release to expose the error - the code in question certainly hasn't changed. It is difficult to isolate changes because of the extensive number of changes following on the elimination of Php7.4 as a supported platform. The provided spreadsheet is unusual in at least two senses. When opened in Excel, it will show a clearly default value for the chart title, namely 'Chart Title'. I cannot find anything in the xml corresponding to that text. Since I have no idea why Excel is using that title, I will not try to duplicate its behavior, so that loading and saving the provided spreadsheet will omit the chart title. I will continue to investigate. The other sense in which it is unusual is that it includes some style files in the same directory as the chart. I doubt that PhpSpreadsheet looks at these. The styling after load and save seems to mostly match the original, although there is at least one color in the graph which does not match. I imagine it would be pretty complicated to formally support these files. * Unused Assignment --- src/PhpSpreadsheet/Writer/Xlsx.php | 3 + src/PhpSpreadsheet/Writer/Xlsx/Chart.php | 4 +- .../Reader/Xlsx/Issue3767Test.php | 88 ++++++++++++++++++ tests/data/Reader/XLSX/issue.3767.xlsx | Bin 0 -> 15637 bytes 4 files changed, 93 insertions(+), 2 deletions(-) create mode 100644 tests/PhpSpreadsheetTests/Reader/Xlsx/Issue3767Test.php create mode 100644 tests/data/Reader/XLSX/issue.3767.xlsx diff --git a/src/PhpSpreadsheet/Writer/Xlsx.php b/src/PhpSpreadsheet/Writer/Xlsx.php index 00c6752037..c18bc85862 100644 --- a/src/PhpSpreadsheet/Writer/Xlsx.php +++ b/src/PhpSpreadsheet/Writer/Xlsx.php @@ -410,6 +410,9 @@ public function save($filename, int $flags = 0): void } } } + if (isset($unparsedLoadedData['sheets'][$sheetCodeName]['drawingOriginalIds']) && !isset($zipContent['xl/drawings/drawing' . ($i + 1) . '.xml'])) { + $zipContent['xl/drawings/drawing' . ($i + 1) . '.xml'] = ''; + } // Add comment relationship parts $legacy = $unparsedLoadedData['sheets'][$this->spreadSheet->getSheet($i)->getCodeName()]['legacyDrawing'] ?? null; diff --git a/src/PhpSpreadsheet/Writer/Xlsx/Chart.php b/src/PhpSpreadsheet/Writer/Xlsx/Chart.php index 9bf65ea5ef..f27156507a 100644 --- a/src/PhpSpreadsheet/Writer/Xlsx/Chart.php +++ b/src/PhpSpreadsheet/Writer/Xlsx/Chart.php @@ -169,8 +169,8 @@ private function writeTitle(XMLWriter $objWriter, ?Title $title = null): void $objWriter->endElement(); $caption = $title->getCaption(); - if ((is_array($caption)) && (count($caption) > 0)) { - $caption = $caption[0]; + if (is_array($caption)) { + $caption = $caption[0] ?? ''; } $this->getParentWriter()->getWriterPartstringtable()->writeRichTextForCharts($objWriter, $caption, 'a'); diff --git a/tests/PhpSpreadsheetTests/Reader/Xlsx/Issue3767Test.php b/tests/PhpSpreadsheetTests/Reader/Xlsx/Issue3767Test.php new file mode 100644 index 0000000000..9f8c3266c9 --- /dev/null +++ b/tests/PhpSpreadsheetTests/Reader/Xlsx/Issue3767Test.php @@ -0,0 +1,88 @@ +tempfile !== '') { + unlink($this->tempfile); + $this->tempfile = ''; + } + } + + public function readCharts(XlsxReader $reader): void + { + $reader->setIncludeCharts(true); + } + + public function writeCharts(XlsxWriter $writer): void + { + $writer->setIncludeCharts(true); + } + + public function testReadWithoutCharts(): void + { + $reader = new XlsxReader(); + //$this->readCharts($reader); // Commented out - don't want to read charts. + $spreadsheet = $reader->load(self::$testbook); + $sheet = $spreadsheet->getActiveSheet(); + $charts = $sheet->getChartCollection(); + self::assertCount(0, $charts); + $this->tempfile = File::temporaryFileName(); + $writer = new XlsxWriter($spreadsheet); + $this->writeCharts($writer); + $writer->save($this->tempfile); + $spreadsheet->disconnectWorksheets(); + $file = 'zip://'; + $file .= $this->tempfile; + $file .= '#xl/worksheets/_rels/sheet1.xml.rels'; + $data = (string) file_get_contents($file); + // PhpSpreadsheet still generates this target even though charts aren't included + self::assertStringContainsString('Target="../drawings/drawing1.xml"', $data); + $file = 'zip://'; + $file .= $this->tempfile; + $file .= '#xl/drawings/drawing1.xml'; + $data = file_get_contents($file); + self::assertSame('', $data); // fake file because rels needs it + } + + public function testReadWithCharts(): void + { + $reader = new XlsxReader(); + $this->readCharts($reader); + $spreadsheet = $reader->load(self::$testbook); + $xsheet = $spreadsheet->getActiveSheet(); + $xcharts = $xsheet->getChartCollection(); + self::assertCount(1, $xcharts); + /** @var callable */ + $callableReader = [$this, 'readCharts']; + /** @var callable */ + $callableWriter = [$this, 'writeCharts']; + $reloadedSpreadsheet = $this->writeAndReload($spreadsheet, 'Xlsx', $callableReader, $callableWriter); + $spreadsheet->disconnectWorksheets(); + $sheet = $reloadedSpreadsheet->getActiveSheet(); + $charts = $xsheet->getChartCollection(); + self::assertCount(1, $charts); + // In Excel, a default title ('Chart Title') is shown. + // I can't find that anywhere in the Xml. + self::assertSame('', $charts[0]?->getTitle()?->getCaptionText()); + // Just test anything on the chart. + self::assertSame($sheet->getCell('B2')->getValue(), $charts[0]->getPlotArea()?->getPlotGroup()[0]->getPlotValues()[0]->getDataValues()[0]); + $reloadedSpreadsheet->disconnectWorksheets(); + } +} diff --git a/tests/data/Reader/XLSX/issue.3767.xlsx b/tests/data/Reader/XLSX/issue.3767.xlsx new file mode 100644 index 0000000000000000000000000000000000000000..bde09e6a665b9e85ad53552e84b8685461a2fd88 GIT binary patch literal 15637 zcmeHubz5A?wm0q&91^T?cXxO90Kwhe-6245cXxO9;OaONRu0-q zt~Q4Dnlvt!7JwWu5V9;FkhkCe-}S$k17-0eGQD(&!gpdn1b3(;{?b$@EH5snLSsJ6 z7c}lK_YovO-Pm^2Og_2&4CO}?rYm+3HkTxZaYdQJ~A+W83Z_4Q8~cPd5xltcP_xWVg54tu$qt|#{w6S@mY-;XQ` zaks};Gv?;Ec`+FvSRfBkLyo+T%5$02-+A-)KcO!O6@Cfm3Whl4f5 zwK4b+RO-NM_dP5S(CaHGklcT$bG}Z?~Ar!jMrp2^*ZtgAyNY9U&=5>=Hz5%f9s?InCY7-6o4lxRN=wM^cnF z6=h2fZ4ipgTnJSoPSB`+fJVv34npTn_ErBXt+uXrUjaNPpmb;Oq za0t&E!Y*|(os2eUuV=he;W21|fAjD`S4YVM26(IP1n+y^iT(B;B=-6}q={@O;QG46kVY&9eQslKAq(C0@O8q~%RXJ^(=h zyI9cvp(oDPcIJB4*5*I0-oJDP_|5daq5SW@$`qs}`{)qcVPAsjoKv09kd_>12~L!c z-$4!4k*|>8GkRTbU{SPaew>%21+@-xJsTTvx?@Jz0!6v$rY!o54CaPnamER5J$5w) z3Z-{ij_W51j)Hu0bXRuQ z+kO~HyN;W8=RU&iVwXzdmN0XB1UUeL2SxF`$z$wybCO$?&%QYbJPng~u3S=$T%Q^3 z3=Pa}yuIoF7MWV(Yq?xdARtI+AfR_|zjzawzY9y5vb=RJ8=@EQ;S1x(6<|BPlt#oY3@@t1alxZt_y4_kBGUqK^W@qxl*EH`X7haMI%?2rNbr&m0ZN4AdI*#sz}YIScQnnpoo#-jL*})%PPu!iL&)kJJEuRoY)^Zp*Ik{76ZGn zWI$%7OTsicxmyg4yV*dKxjo z%$qYM4%+cVre?FbItjZ%+rF+ytPoDwqb!!nZ?d$_anNFr9x3Jy%3qv*hgD-aonTv~ z$J(QhrHQ@cL0BGBAvSV@9KhA(ua7CBrNhOK3B8~+FEo=}#C%|bw@_(T(VI~YPPVE( z*{MN7&V+sds}~rJO7E(~DpXmna#hxiF!4AyK} zbR4FRnd9srr{Nn2$J9MHpS^hj6D6tw*4JiG_)iv-oQ|>EDv$3TNdm<`_?6v6-@JN{ z1Hj0(`FmN_)z4oUTKzcIV~mR^ObS<7pYzFA4zg(s!tu03#S;wqGuWqA;jwuys~MsF zCX)@xai1Ps_Q0>s5X0fN1)@|c@BHW|6;~h~v6Cx?tg3Wat<)Yww&&Kou^C^P;2O?I zbnKbe>06Yorx_&8ohObIEbzfj1mtLY8B)5;R+hf26)z~Rz>>b`p1m8+nCHWU>{tpw zVCXBjHAi+6_Y;lYCN@1FV&rw6X6lpK`BFYR-WT!0E+W>T?zn$AL4ION(PWq#{>qG2E0+ckp|k&f*>Lb3^> z%d}A5%e3T7S6906S}u4g*HKQQ7hbFB3dOB&y9OUCLT4q#Kf||8qdUYSgS#Dpf^?h> z6?79hitC7jrvp+W{piPyTr2Z4%Amb(hTJXuS_;XhsIQw$&#Z7#Z96u& zEh-)(JkxpVwAu!x*6(CrQ@5#wCJsZ#uUb)XjI}Eh7f2tqc1MDr9*W0@98-u3pGd=m zFBceI?b$${^#$ZrZ5W*xg#D#xy#7(2A>aJgZ*MctZ(BR+ANp)>qHAYppy*&{YGrKy z(*jsbbxVNKBSLJpZgK{!TB^JgmSE{uSt$*{b%SfwXHAltAMIOiWK}?18nf;AmiZpD z{RKhGgP#DSI=(SX1g4AT79q0S!+mXSRg&MLx{-&V?h=k@Vee4_rJ77FPX%FgTY~%u z0xALB7K|$WF3E3Kp&&Ph-uKiLXmfK;bB0Q}3L*bMPlU!jQNh3=^L>N24ikIs5_T6! z`J%~be(^4gZ9r4^gr2>retZI zN{9HuWBS^C=?p>=00N`rdZlD)_N4m+oIno@eKP12tEBw;WTD@}bevbNz3DMv@9dSq znb5eO4Jk^ZOzHwXMdjyXOL6q&O!EE;5`h6Kj--vGeX}fJLaKY^=s0n=IiX+Nl9&#< ziDI-mpnN`<01nM^iEma48ohPQK{D%^MgFF>#eK$^#tO-$` zXiET2aQv}UW?#_F^XD0r!Kpbh`fzkK&%lK2c3)IBSu5F#*6^BeKE=q@pu(Asog7Q%33KrG4tq4VNZ z>%^`;+pY~^*5@P{g_YyxWn-HHM?2z>jNn>wPiSoCof76LATmcA7d7$YAW-KelQ-y!C`vnSV0<48U8$D-$v}~POsHu8v!2U&0DmDqO9x}$X zGwtOH&)T2gCPK|Ce#^%pE0zrlOz@OrVursm5q!ZQv^JKAn0|H)rJ;}gQ;jgK!94

hlkPeum2L&NYwSO__WBO1A+tdOvczC^y^ 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