From 7b25d9778c2c5d8b038677ce3acbc9a33d6e18be Mon Sep 17 00:00:00 2001 From: Antony Liu Date: Sun, 18 May 2025 15:51:25 +0800 Subject: [PATCH 1/2] Standardize some more common chart axis properties. --- main/SS/UserModel/Charts/ChartAxis.cs | 7 +++ main/SS/UserModel/Charts/ChartAxisFactory.cs | 16 ++++-- .../XSSF/UserModel/Charts/XSSFCategoryAxis.cs | 10 ++++ ooxml/XSSF/UserModel/Charts/XSSFChartAxis.cs | 43 ++++++++-------- ooxml/XSSF/UserModel/Charts/XSSFDateAxis.cs | 40 +++++++++++---- ooxml/XSSF/UserModel/Charts/XSSFValueAxis.cs | 13 ++++- ooxml/XSSF/UserModel/XSSFChart.cs | 12 +++++ .../ooxml/XSSF/UserModel/TestXSSFDateAxis.cs | 50 +++++++++++++++++++ 8 files changed, 154 insertions(+), 37 deletions(-) create mode 100644 testcases/ooxml/XSSF/UserModel/TestXSSFDateAxis.cs diff --git a/main/SS/UserModel/Charts/ChartAxis.cs b/main/SS/UserModel/Charts/ChartAxis.cs index 0a7350d83..d115cfb18 100644 --- a/main/SS/UserModel/Charts/ChartAxis.cs +++ b/main/SS/UserModel/Charts/ChartAxis.cs @@ -106,6 +106,13 @@ public interface IChartAxis * @return minor tick mark. */ AxisTickMark MinorTickMark { get; set; } + + + /// + /// Use this to check before retrieving a number format, as calling {@link #getNumberFormat()} may create a default one if none exists. + /// + /// return true if a number format element is defined, false if not + bool HasNumberFormat(); } diff --git a/main/SS/UserModel/Charts/ChartAxisFactory.cs b/main/SS/UserModel/Charts/ChartAxisFactory.cs index 6cdd465f6..df303fd8c 100644 --- a/main/SS/UserModel/Charts/ChartAxisFactory.cs +++ b/main/SS/UserModel/Charts/ChartAxisFactory.cs @@ -22,16 +22,24 @@ namespace NPOI.SS.UserModel.Charts /// /// @author Roman Kashitsyn public interface IChartAxisFactory - { + { /// - /// returns new value axis + /// create new value axis at the end of the list at the specified chart position. /// /// /// IValueAxis CreateValueAxis(AxisPosition pos); - + /// + /// create new category axis at the end of the list at the specified chart position. + /// + /// + /// IChartAxis CreateCategoryAxis(AxisPosition pos); - + /// + /// create new date category axis at the end of the list at the specified chart position. + /// + /// + /// IChartAxis CreateDateAxis(AxisPosition pos); } diff --git a/ooxml/XSSF/UserModel/Charts/XSSFCategoryAxis.cs b/ooxml/XSSF/UserModel/Charts/XSSFCategoryAxis.cs index f44a77e9d..d01b4fc6f 100644 --- a/ooxml/XSSF/UserModel/Charts/XSSFCategoryAxis.cs +++ b/ooxml/XSSF/UserModel/Charts/XSSFCategoryAxis.cs @@ -88,6 +88,11 @@ public void SetAuto(CT_Boolean au) ctCatAx.auto = au; } + public override CT_ChartLines GetMajorGridLines() + { + return ctCatAx.majorGridlines; + } + private void createAxis(long id, AxisPosition pos) { ctCatAx = chart.GetCTChart().plotArea.AddNewCatAx(); @@ -109,6 +114,11 @@ private void createAxis(long id, AxisPosition pos) this.MajorTickMark=(AxisTickMark.Cross); this.MinorTickMark=(AxisTickMark.None); } + + public override bool HasNumberFormat() + { + return ctCatAx.IsSetNumFmt(); + } } } diff --git a/ooxml/XSSF/UserModel/Charts/XSSFChartAxis.cs b/ooxml/XSSF/UserModel/Charts/XSSFChartAxis.cs index 32448b39a..82eed5578 100644 --- a/ooxml/XSSF/UserModel/Charts/XSSFChartAxis.cs +++ b/ooxml/XSSF/UserModel/Charts/XSSFChartAxis.cs @@ -45,11 +45,11 @@ public AxisPosition Position { get { - return toAxisPosition(GetCTAxPos()); + return ToAxisPosition(GetCTAxPos()); } set { - GetCTAxPos().val = fromAxisPosition(value); + GetCTAxPos().val = FromAxisPosition(value); } } @@ -180,12 +180,12 @@ public AxisOrientation Orientation { get { - return toAxisOrientation(GetCTScaling().orientation); + return ToAxisOrientation(GetCTScaling().orientation); } set { CT_Scaling scaling = GetCTScaling(); - ST_Orientation stOrientation = fromAxisOrientation(value); + ST_Orientation stOrientation = FromAxisOrientation(value); if (scaling.IsSetOrientation()) { scaling.orientation.val = stOrientation; @@ -201,11 +201,11 @@ public AxisCrosses Crosses { get { - return toAxisCrosses(GetCTCrosses()); + return ToAxisCrosses(GetCTCrosses()); } set { - GetCTCrosses().val = fromAxisCrosses(value); + GetCTCrosses().val = FromAxisCrosses(value); } } public bool IsVisible @@ -220,27 +220,27 @@ public bool IsVisible } } - public AxisTickMark MajorTickMark + public virtual AxisTickMark MajorTickMark { get { - return toAxisTickMark(GetMajorCTTickMark()); + return ToAxisTickMark(GetMajorCTTickMark()); } set { - GetMajorCTTickMark().val = fromAxisTickMark(value); + GetMajorCTTickMark().val = FromAxisTickMark(value); } } - public AxisTickMark MinorTickMark + public virtual AxisTickMark MinorTickMark { get { - return toAxisTickMark(GetMinorCTTickMark()); + return ToAxisTickMark(GetMinorCTTickMark()); } set { - GetMinorCTTickMark().val = fromAxisTickMark(value); + GetMinorCTTickMark().val = FromAxisTickMark(value); } } @@ -251,8 +251,11 @@ public AxisTickMark MinorTickMark protected abstract CT_Boolean GetDelete(); protected abstract CT_TickMark GetMajorCTTickMark(); protected abstract CT_TickMark GetMinorCTTickMark(); + public abstract CT_ChartLines GetMajorGridLines(); - private static ST_Orientation fromAxisOrientation(AxisOrientation orientation) + public abstract bool HasNumberFormat(); + + private static ST_Orientation FromAxisOrientation(AxisOrientation orientation) { switch (orientation) { @@ -263,7 +266,7 @@ private static ST_Orientation fromAxisOrientation(AxisOrientation orientation) } } - private static AxisOrientation toAxisOrientation(CT_Orientation ctOrientation) + private static AxisOrientation ToAxisOrientation(CT_Orientation ctOrientation) { switch (ctOrientation.val) { @@ -274,7 +277,7 @@ private static AxisOrientation toAxisOrientation(CT_Orientation ctOrientation) } } - private static ST_Crosses fromAxisCrosses(AxisCrosses crosses) + private static ST_Crosses FromAxisCrosses(AxisCrosses crosses) { switch (crosses) { @@ -286,7 +289,7 @@ private static ST_Crosses fromAxisCrosses(AxisCrosses crosses) } } - private static AxisCrosses toAxisCrosses(CT_Crosses ctCrosses) + private static AxisCrosses ToAxisCrosses(CT_Crosses ctCrosses) { switch (ctCrosses.val) { @@ -298,7 +301,7 @@ private static AxisCrosses toAxisCrosses(CT_Crosses ctCrosses) } } - private static ST_AxPos fromAxisPosition(AxisPosition position) + private static ST_AxPos FromAxisPosition(AxisPosition position) { switch (position) { @@ -311,7 +314,7 @@ private static ST_AxPos fromAxisPosition(AxisPosition position) } } - private static AxisPosition toAxisPosition(CT_AxPos ctAxPos) + private static AxisPosition ToAxisPosition(CT_AxPos ctAxPos) { switch (ctAxPos.val) { @@ -322,7 +325,7 @@ private static AxisPosition toAxisPosition(CT_AxPos ctAxPos) default: return AxisPosition.Bottom; } } - private static ST_TickMark fromAxisTickMark(AxisTickMark tickMark) + private static ST_TickMark FromAxisTickMark(AxisTickMark tickMark) { switch (tickMark) { @@ -335,7 +338,7 @@ private static ST_TickMark fromAxisTickMark(AxisTickMark tickMark) } } - private static AxisTickMark toAxisTickMark(CT_TickMark ctTickMark) + private static AxisTickMark ToAxisTickMark(CT_TickMark ctTickMark) { switch (ctTickMark.val) { diff --git a/ooxml/XSSF/UserModel/Charts/XSSFDateAxis.cs b/ooxml/XSSF/UserModel/Charts/XSSFDateAxis.cs index 61e1081e2..5af8e6a51 100644 --- a/ooxml/XSSF/UserModel/Charts/XSSFDateAxis.cs +++ b/ooxml/XSSF/UserModel/Charts/XSSFDateAxis.cs @@ -1,4 +1,21 @@ -using NPOI.OpenXmlFormats.Dml.Chart; +/* ==================================================================== + Licensed to the Apache Software Foundation (ASF) under one or more + contributor license agreements. See the NOTICE file distributed with + this work for additional information regarding copyright ownership. + The ASF licenses this file to You under the Apache License, Version 2.0 + (the "License"); you may not use this file except in compliance with + the License. You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +==================================================================== */ + +using NPOI.OpenXmlFormats.Dml.Chart; using NPOI.SS.UserModel.Charts; using System; using System.Collections.Generic; @@ -14,7 +31,7 @@ public XSSFDateAxis(XSSFChart chart, long id, AxisPosition pos) : base(chart) { - createAxis(id, pos); + CreateAxis(id, pos); } public XSSFDateAxis(XSSFChart chart, CT_DateAx ctDateAx) @@ -84,7 +101,7 @@ protected override CT_TickMark GetMinorCTTickMark() return ctDateAx.minorTickMark; } - protected CT_ChartLines GetMajorGridLines() + public override CT_ChartLines GetMajorGridLines() { return ctDateAx.majorGridlines; } @@ -94,14 +111,10 @@ public override void CrossAxis(IChartAxis axis) ctDateAx.crossAx.val = (uint)axis.Id; } - public CT_TimeUnit GetBaseTimeUnit() + public CT_TimeUnit BaseTimeUnit { - return ctDateAx.baseTimeUnit; - } - - public void SetBaseTimeUnit(CT_TimeUnit unit) - { - ctDateAx.baseTimeUnit = unit; + get { return ctDateAx.baseTimeUnit; } + set { ctDateAx.baseTimeUnit = value;} } public void SetAuto(CT_Boolean au) @@ -109,7 +122,7 @@ public void SetAuto(CT_Boolean au) ctDateAx.auto = au; } - private void createAxis(long id, AxisPosition pos) + private void CreateAxis(long id, AxisPosition pos) { ctDateAx = chart.GetCTChart().plotArea.AddNewDateAx(); ctDateAx.AddNewAxId().val = (uint)id; @@ -130,5 +143,10 @@ private void createAxis(long id, AxisPosition pos) this.MajorTickMark = (AxisTickMark.Cross); this.MinorTickMark = (AxisTickMark.None); } + + public override bool HasNumberFormat() + { + return ctDateAx.IsSetNumFmt(); + } } } diff --git a/ooxml/XSSF/UserModel/Charts/XSSFValueAxis.cs b/ooxml/XSSF/UserModel/Charts/XSSFValueAxis.cs index c28a161d0..ad0ea59fc 100644 --- a/ooxml/XSSF/UserModel/Charts/XSSFValueAxis.cs +++ b/ooxml/XSSF/UserModel/Charts/XSSFValueAxis.cs @@ -55,7 +55,7 @@ public override long Id public void SetCrossBetween(AxisCrossBetween crossBetween) { - ctValAx.crossBetween.val= fromCrossBetween(crossBetween); + ctValAx.crossBetween.val= FromCrossBetween(crossBetween); } public AxisCrossBetween GetCrossBetween() @@ -106,6 +106,10 @@ protected override CT_Crosses GetCTCrosses() { return ctValAx.crosses; } + public override CT_ChartLines GetMajorGridLines() + { + return ctValAx.majorGridlines; + } public override void CrossAxis(IChartAxis axis) { @@ -135,7 +139,7 @@ private void CreateAxis(long id, AxisPosition pos) MinorTickMark=(AxisTickMark.None); } - private static ST_CrossBetween fromCrossBetween(AxisCrossBetween crossBetween) + private static ST_CrossBetween FromCrossBetween(AxisCrossBetween crossBetween) { switch (crossBetween) { @@ -156,5 +160,10 @@ private static AxisCrossBetween ToCrossBetween(ST_CrossBetween ctCrossBetween) throw new ArgumentException(); } } + + public override bool HasNumberFormat() + { + return ctValAx.IsSetNumFmt(); + } } } diff --git a/ooxml/XSSF/UserModel/XSSFChart.cs b/ooxml/XSSF/UserModel/XSSFChart.cs index eb43b5ade..e59886e49 100644 --- a/ooxml/XSSF/UserModel/XSSFChart.cs +++ b/ooxml/XSSF/UserModel/XSSFChart.cs @@ -383,6 +383,7 @@ private bool HasAxis() private void ParseAxis() { ParseCategoryAxis(); + ParseDateAxis(); ParseValueAxis(); } private void ParseCategoryAxis() @@ -394,6 +395,17 @@ private void ParseCategoryAxis() axis.Add(new XSSFCategoryAxis(this, catAx)); } } + + private void ParseDateAxis() + { + if (chart.plotArea.dateAx == null) + return; + foreach (CT_DateAx dateAx in chart.plotArea.dateAx) + { + axis.Add(new XSSFDateAxis(this, dateAx)); + } + } + private void ParseValueAxis() { if (chart.plotArea.valAx == null) diff --git a/testcases/ooxml/XSSF/UserModel/TestXSSFDateAxis.cs b/testcases/ooxml/XSSF/UserModel/TestXSSFDateAxis.cs new file mode 100644 index 000000000..4dd7f2fb1 --- /dev/null +++ b/testcases/ooxml/XSSF/UserModel/TestXSSFDateAxis.cs @@ -0,0 +1,50 @@ +/* ==================================================================== + Licensed to the Apache Software Foundation (ASF) under one or more + contributor license agreements. See the NOTICE file distributed with + this work for additional information regarding copyright ownership. + The ASF licenses this file to You under the Apache License, Version 2.0 + (the "License"); you may not use this file except in compliance with + the License. You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +==================================================================== */ + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using NPOI.XSSF.UserModel; +using NPOI.SS.UserModel.Charts; +using NPOI.XSSF.UserModel.Charts; +using NUnit.Framework; +using NUnit.Framework.Legacy; + +namespace TestCases.XSSF.UserModel +{ + [TestFixture] + internal class TestXSSFDateAxis + { + [Test] + public void TestAccessMethods() + { + XSSFWorkbook wb = new XSSFWorkbook(); + XSSFSheet sheet = wb.CreateSheet() as XSSFSheet; + XSSFDrawing Drawing = sheet.CreateDrawingPatriarch() as XSSFDrawing; + XSSFClientAnchor anchor = Drawing.CreateAnchor(0, 0, 0, 0, 1, 1, 10, 30) as XSSFClientAnchor; + XSSFChart chart = Drawing.CreateChart(anchor) as XSSFChart; + XSSFDateAxis axis = chart.ChartAxisFactory.CreateDateAxis(AxisPosition.Bottom) as XSSFDateAxis; + + axis.Crosses = AxisCrosses.AutoZero; + ClassicAssert.AreEqual(axis.Crosses, AxisCrosses.AutoZero); + + ClassicAssert.AreEqual(chart.GetAxis().Count, 1); + } + } +} \ No newline at end of file From cb4a7ea9aa0845a5eb4b1596e471b83573239844 Mon Sep 17 00:00:00 2001 From: Antony Liu Date: Sun, 18 May 2025 16:45:46 +0800 Subject: [PATCH 2/2] Bug 56557: Fix handling chart sheets --- ooxml/XSSF/Streaming/SXSSFWorkbook.cs | 3 ++- .../ooxml/XSSF/Streaming/TestSXSSFWorkbook.cs | 16 +++++++++++++++- testcases/ooxml/XSSF/XSSFTestDataSamples.cs | 8 +++++++- testcases/test-data/spreadsheet/56557.xlsx | Bin 0 -> 10483 bytes 4 files changed, 24 insertions(+), 3 deletions(-) create mode 100644 testcases/test-data/spreadsheet/56557.xlsx diff --git a/ooxml/XSSF/Streaming/SXSSFWorkbook.cs b/ooxml/XSSF/Streaming/SXSSFWorkbook.cs index 7e71323c6..61d5aa277 100644 --- a/ooxml/XSSF/Streaming/SXSSFWorkbook.cs +++ b/ooxml/XSSF/Streaming/SXSSFWorkbook.cs @@ -471,7 +471,8 @@ private void InjectData(FileInfo zipfile, Stream outStream, bool leaveOpen) zos.PutNextEntry(new ZipEntry(ze.Name)); var inputStream = zip.GetInputStream(ze); XSSFSheet xSheet = GetSheetFromZipEntryName(ze.Name); - if (xSheet != null) + // See bug 56557, we should not inject data into the special ChartSheets + if (xSheet != null && !(xSheet is XSSFChartSheet)) { SXSSFSheet sxSheet = GetSXSSFSheet(xSheet); var xis = sxSheet.GetWorksheetXMLInputStream(); diff --git a/testcases/ooxml/XSSF/Streaming/TestSXSSFWorkbook.cs b/testcases/ooxml/XSSF/Streaming/TestSXSSFWorkbook.cs index 3080e8aeb..addbfcecd 100644 --- a/testcases/ooxml/XSSF/Streaming/TestSXSSFWorkbook.cs +++ b/testcases/ooxml/XSSF/Streaming/TestSXSSFWorkbook.cs @@ -613,6 +613,20 @@ public void CreateFromReadOnlyWorkbook() ClassicAssert.AreEqual("Test Row 9", s.GetRow(9).GetCell(2).StringCellValue); } + [Test] + public void Test56557() + { + IWorkbook wb = XSSFTestDataSamples.OpenSampleWorkbook("56557.xlsx"); + + // Using streaming XSSFWorkbook makes the output file invalid + wb = new SXSSFWorkbook((XSSFWorkbook)wb); + + // Should not throw POIXMLException: java.io.IOException: Unable to parse xml bean when reading back + IWorkbook wbBack = XSSFTestDataSamples.WriteOutAndReadBack(wb); + ClassicAssert.IsNotNull(wbBack); + wbBack.Close(); + + wb.Close(); + } } - } \ No newline at end of file diff --git a/testcases/ooxml/XSSF/XSSFTestDataSamples.cs b/testcases/ooxml/XSSF/XSSFTestDataSamples.cs index 77554ba63..e36198a45 100644 --- a/testcases/ooxml/XSSF/XSSFTestDataSamples.cs +++ b/testcases/ooxml/XSSF/XSSFTestDataSamples.cs @@ -25,7 +25,9 @@ limitations under the License. using NPOI.HSSF; using TestCases.HSSF; using System.Diagnostics; -using NUnit.Framework;using NUnit.Framework.Legacy; +using NUnit.Framework; +using NUnit.Framework.Legacy; +using NPOI.XSSF.Streaming; namespace NPOI.XSSF { @@ -86,6 +88,10 @@ public static IWorkbook WriteOutAndReadBack(IWorkbook wb) sw2.Stop(); Debug.WriteLine("XSSFWorkbook parse time: " + sw2.ElapsedMilliseconds + "ms"); } + else if (wb is SXSSFWorkbook) + { + result = new SXSSFWorkbook(new XSSFWorkbook(is1)); + } else { throw new RuntimeException("Unexpected workbook type (" diff --git a/testcases/test-data/spreadsheet/56557.xlsx b/testcases/test-data/spreadsheet/56557.xlsx new file mode 100644 index 0000000000000000000000000000000000000000..7ca891cf5177b386b4f10f477dbcac901a64ffa4 GIT binary patch literal 10483 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 literal 0 HcmV?d00001