- {{^_disableToc}}
- {{>partials/toc}}
-
- {{/_disableToc}}
- {{#_disableToc}}
-
- {{/_disableToc}}
- {{#_disableAffix}}
-
- {{/_disableAffix}}
- {{^_disableAffix}}
-
- {{/_disableAffix}}
-
- {{!body}}
-
-
- {{^_disableAffix}}
- {{>partials/affix}}
- {{/_disableAffix}}
-
-
- {{^_disableFooter}}
- {{>partials/footer}}
- {{/_disableFooter}}
-
- {{>partials/scripts}}
-
-
diff --git a/docs/templates/epplus/partials/_logo.liquid b/docs/templates/epplus/partials/_logo.liquid
deleted file mode 100644
index f427336749..0000000000
--- a/docs/templates/epplus/partials/_logo.liquid
+++ /dev/null
@@ -1,8 +0,0 @@
-{% comment -%}Copyright (c) Microsoft. All rights reserved. Licensed under the MIT license. See LICENSE file in the project root for full license information.{% endcomment -%}
-
- {%- if _appLogoPath -%}
-
- {%- else -%}
-
- {%- endif -%}
-
diff --git a/docs/templates/epplus/partials/logo.tmpl.partial b/docs/templates/epplus/partials/logo.tmpl.partial
deleted file mode 100644
index 04386bfe0f..0000000000
--- a/docs/templates/epplus/partials/logo.tmpl.partial
+++ /dev/null
@@ -1,5 +0,0 @@
-{{!Copyright (c) Microsoft. All rights reserved. Licensed under the MIT license. See LICENSE file in the project root for full license information.}}
-
-
-
-
diff --git a/docs/templates/epplus/public/favicon.ico b/docs/templates/epplus/public/favicon.ico
new file mode 100644
index 0000000000..a3a799985c
Binary files /dev/null and b/docs/templates/epplus/public/favicon.ico differ
diff --git a/docs/templates/epplus/public/logo.png b/docs/templates/epplus/public/logo.png
new file mode 100644
index 0000000000..cb26262d85
Binary files /dev/null and b/docs/templates/epplus/public/logo.png differ
diff --git a/docs/templates/epplus/styles/main.css b/docs/templates/epplus/styles/main.css
new file mode 100644
index 0000000000..14591cb1d2
--- /dev/null
+++ b/docs/templates/epplus/styles/main.css
@@ -0,0 +1,14 @@
+/* Sidebar width */
+.sidenav {
+ width: 320px; /* default is around 260px, adjust to taste */
+}
+
+/* Push the main content over to match */
+.sidenav + .article {
+ margin-left: 320px;
+}
+
+/* Also adjust the inner toc container */
+#toc {
+ width: 320px;
+}
\ No newline at end of file
diff --git a/docs/templates/epplus/template.json b/docs/templates/epplus/template.json
new file mode 100644
index 0000000000..5783f59450
--- /dev/null
+++ b/docs/templates/epplus/template.json
@@ -0,0 +1,3 @@
+{
+ "metadata": "/templates/epplus/public"
+}
diff --git a/docs/toc.yml b/docs/toc.yml
index 5c0543f91b..59f8010471 100644
--- a/docs/toc.yml
+++ b/docs/toc.yml
@@ -2,4 +2,4 @@
href: articles/
- name: Api Documentation
href: api/
- homepage: index.md
+ homepage: api/index.md
diff --git a/src/EPPlus.Export.ImageRenderer.Test/Chart/BarChartTests.cs b/src/EPPlus.Export.ImageRenderer.Test/Chart/BarChartTests.cs
new file mode 100644
index 0000000000..53d5b0b20f
--- /dev/null
+++ b/src/EPPlus.Export.ImageRenderer.Test/Chart/BarChartTests.cs
@@ -0,0 +1,54 @@
+using OfficeOpenXml;
+using OfficeOpenXml.Drawing.Chart;
+
+namespace EPPlus.Export.ImageRenderer.Tests.Chart
+{
+ [TestClass]
+ public class BarChartTests : TestBase
+ {
+ [TestMethod]
+ public void GenerateSvgForBarCharts1()
+ {
+ ExcelPackage.License.SetNonCommercialOrganization("EPPlus Project");
+ using (var p = OpenTemplatePackage("BarChartForSvg.xlsx"))
+ {
+ var ws = p.Workbook.Worksheets[0];
+ var renderer = new EPPlusImageRenderer.ImageRenderer();
+
+ //var ix = 1;
+ //var c = ws.Drawings[ix];
+ //var svg = renderer.RenderDrawingToSvg(c);
+ //SaveTextFileToWorkbook($"svg\\ChartForSvg_ind{ix++}.svg", svg);
+
+ var ix = 0;
+ foreach (ExcelChart c in ws.Drawings)
+ {
+ var svg = renderer.RenderDrawingToSvg(c);
+ SaveTextFileToWorkbook($"svg\\BarChartForSvg_sheet1_{ix++}.svg", svg);
+ }
+ }
+ }
+ [TestMethod]
+ public void GenerateSvgForBarCharts2()
+ {
+ ExcelPackage.License.SetNonCommercialOrganization("EPPlus Project");
+ using (var p = OpenTemplatePackage("BarChartForSvg.xlsx"))
+ {
+ var ws = p.Workbook.Worksheets[1];
+ var renderer = new EPPlusImageRenderer.ImageRenderer();
+
+ //var ix = 2;
+ //var c = ws.Drawings[ix];
+ //var svg = renderer.RenderDrawingToSvg(c);
+ //SaveTextFileToWorkbook($"svg\\ChartForSvg_ind{ix++}.svg", svg);
+
+ var ix = 0;
+ foreach (ExcelChart c in ws.Drawings)
+ {
+ var svg = renderer.RenderDrawingToSvg(c);
+ SaveTextFileToWorkbook($"svg\\BarChartForSvg_sheet2_{ix++}.svg", svg);
+ }
+ }
+ }
+ }
+}
diff --git a/src/EPPlus.Export.ImageRenderer.Test/Chart/ColumnChartTests.cs b/src/EPPlus.Export.ImageRenderer.Test/Chart/ColumnChartTests.cs
new file mode 100644
index 0000000000..4a33b919ae
--- /dev/null
+++ b/src/EPPlus.Export.ImageRenderer.Test/Chart/ColumnChartTests.cs
@@ -0,0 +1,59 @@
+using OfficeOpenXml;
+using OfficeOpenXml.Drawing.Chart;
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+
+namespace EPPlus.Export.ImageRenderer.Tests.Chart
+{
+ [TestClass]
+ public class ColumnChartTests : TestBase
+ {
+ [TestMethod]
+ public void GenerateSvgForColumnCharts1()
+ {
+ ExcelPackage.License.SetNonCommercialOrganization("EPPlus Project");
+ using (var p = OpenTemplatePackage("ColumnChartForSvg.xlsx"))
+ {
+ var ws = p.Workbook.Worksheets[0];
+ var renderer = new EPPlusImageRenderer.ImageRenderer();
+
+ //var ix = 1;
+ //var c = ws.Drawings[ix];
+ //var svg = renderer.RenderDrawingToSvg(c);
+ //SaveTextFileToWorkbook($"svg\\ChartForSvg_ind{ix++}.svg", svg);
+
+ var ix = 0;
+ foreach (ExcelChart c in ws.Drawings)
+ {
+ var svg = renderer.RenderDrawingToSvg(c);
+ SaveTextFileToWorkbook($"svg\\ColumnChartForSvg_sheet1_{ix++}.svg", svg);
+ }
+ }
+ }
+ [TestMethod]
+ public void GenerateSvgForColumnCharts2()
+ {
+ ExcelPackage.License.SetNonCommercialOrganization("EPPlus Project");
+ using (var p = OpenTemplatePackage("ColumnChartForSvg.xlsx"))
+ {
+ var ws = p.Workbook.Worksheets[1];
+ var renderer = new EPPlusImageRenderer.ImageRenderer();
+
+ //var ix = 2;
+ //var c = ws.Drawings[ix];
+ //var svg = renderer.RenderDrawingToSvg(c);
+ //SaveTextFileToWorkbook($"svg\\ChartForSvg_ind{ix++}.svg", svg);
+
+ var ix = 0;
+ foreach (ExcelChart c in ws.Drawings)
+ {
+ var svg = renderer.RenderDrawingToSvg(c);
+ SaveTextFileToWorkbook($"svg\\ColumnChartForSvg_sheet2_{ix++}.svg", svg);
+ }
+ }
+ }
+ }
+}
diff --git a/src/EPPlus.Export.ImageRenderer.Test/Chart/LineChartToSvgTests.cs b/src/EPPlus.Export.ImageRenderer.Test/Chart/LineChartToSvgTests.cs
new file mode 100644
index 0000000000..811a568b3c
--- /dev/null
+++ b/src/EPPlus.Export.ImageRenderer.Test/Chart/LineChartToSvgTests.cs
@@ -0,0 +1,240 @@
+using OfficeOpenXml;
+using OfficeOpenXml.Drawing.Chart;
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+
+namespace EPPlus.Export.ImageRenderer.Tests.Chart
+{
+ [TestClass]
+ public class LineChartToSvgTests : TestBase
+ {
+ [TestMethod]
+ public void GenerateSvgForLineCharts_sheet1()
+ {
+ ExcelPackage.License.SetNonCommercialOrganization("EPPlus Project");
+ using (var p = OpenTemplatePackage("ChartForSvg.xlsx"))
+ {
+ var ws = p.Workbook.Worksheets[0];
+ var renderer = new EPPlusImageRenderer.ImageRenderer();
+
+ //var ix = 2;
+ //var c = ws.Drawings[ix];
+ //var svg = renderer.RenderDrawingToSvg(c);
+ //SaveTextFileToWorkbook($"svg\\ChartForSvg_ind{ix++}.svg", svg);
+
+ var ix = 0;
+ foreach (ExcelChart c in ws.Drawings)
+ {
+ var svg = renderer.RenderDrawingToSvg(c);
+ SaveTextFileToWorkbook($"svg\\ChartForSvg{ix++}.svg", svg);
+ }
+ }
+ }
+
+ [TestMethod]
+ public void GenerateSvgForLineCharts_sheet2()
+ {
+ ExcelPackage.License.SetNonCommercialOrganization("EPPlus Project");
+ using (var p = OpenTemplatePackage("ChartForSvg.xlsx"))
+ {
+ var ws = p.Workbook.Worksheets[1];
+ var renderer = new EPPlusImageRenderer.ImageRenderer();
+ //var ix = 1;
+ //var c = ws.Drawings[ix];
+ //var svg = renderer.RenderDrawingToSvg(c);
+ //SaveTextFileToWorkbook($"svg\\ChartForSvg_sheet2_{ix++}.svg", svg);
+ var ix = 1;
+ foreach (ExcelChart c in ws.Drawings)
+ {
+ var svg = renderer.RenderDrawingToSvg(c);
+ SaveTextFileToWorkbook($"svg\\ChartForSvg{ix++}.svg", svg);
+ }
+ }
+ }
+ [TestMethod]
+ public void GenerateSvgForLineCharts_sheet3()
+ {
+ ExcelPackage.License.SetNonCommercialOrganization("EPPlus Project");
+ using (var p = OpenTemplatePackage("ChartForSvg.xlsx"))
+ {
+ var ws = p.Workbook.Worksheets[2];
+ var renderer = new EPPlusImageRenderer.ImageRenderer();
+ //var ix = 1;
+ //var c = ws.Drawings[ix];
+ //var svg = renderer.RenderDrawingToSvg(c);
+ //SaveTextFileToWorkbook($"svg\\ChartForSvg_sheet2_{ix++}.svg", svg);
+ var ix = 1;
+ foreach (ExcelChart c in ws.Drawings)
+ {
+ var svg = renderer.RenderDrawingToSvg(c);
+ SaveTextFileToWorkbook($"svg\\ChartForSvg_Sheet3{ix++}.svg", svg);
+ }
+ }
+ }
+ [TestMethod]
+ public void GenerateSvgForLineCharts()
+ {
+ ExcelPackage.License.SetNonCommercialOrganization("EPPlus Project");
+ using (var p = OpenTemplatePackage("LineChartRenderTest.xlsx"))
+ {
+ var ws = p.Workbook.Worksheets[0];
+ var renderer = new EPPlusImageRenderer.ImageRenderer();
+ //var ix = 1;
+ //var c = ws.Drawings[ix];
+ //var svg = renderer.RenderDrawingToSvg(c);
+ //SaveTextFileToWorkbook($"svg\\LineChartForSvg_Single{ix++}.svg", svg);
+ var ix = 1;
+ foreach (ExcelChart c in ws.Drawings)
+ {
+ var svg = renderer.RenderDrawingToSvg(c);
+ SaveTextFileToWorkbook($"svg\\LineChartForSvg{ix++}.svg", svg);
+ }
+ }
+ }
+
+ [TestMethod]
+ public void GenerateSvgForCharts_SecondaryAxis()
+ {
+ ExcelPackage.License.SetNonCommercialOrganization("EPPlus Project");
+ using (var p = OpenTemplatePackage("ChartForSvg_SecondaryAxis.xlsx"))
+ {
+ var ws = p.Workbook.Worksheets[0];
+ var renderer = new EPPlusImageRenderer.ImageRenderer();
+ //var ix = 3;
+ //var c = ws.Drawings[ix];
+ //var svg = renderer.RenderDrawingToSvg(c);
+ //SaveTextFileToWorkbook($"svg\\ChartForSvg_sheet2_{ix++}.svg", svg);
+ var ix = 1;
+ foreach (ExcelChart c in ws.Drawings)
+ {
+ var svg = renderer.RenderDrawingToSvg(c);
+ SaveTextFileToWorkbook($"svg\\ChartForSvg_SecAxis{ix++}.svg", svg);
+ }
+ }
+ }
+ [TestMethod]
+ public void GenerateSvgForCharts_SecondaryAxis_sheet2()
+ {
+ ExcelPackage.License.SetNonCommercialOrganization("EPPlus Project");
+ using (var p = OpenTemplatePackage("ChartForSvg_SecondaryAxis.xlsx"))
+ {
+ var ws = p.Workbook.Worksheets[1];
+ var renderer = new EPPlusImageRenderer.ImageRenderer();
+ var ix = 2;
+ var c = ws.Drawings[ix];
+ var svg = renderer.RenderDrawingToSvg(c);
+ SaveTextFileToWorkbook($"svg\\ChartForSvg_sheet2_{ix++}.svg", svg);
+ //var ix = 1;
+ //foreach (ExcelChart c in ws.Drawings)
+ //{
+ // var svg = renderer.RenderDrawingToSvg(c);
+ // SaveTextFileToWorkbook($"svg\\ChartForSvg_Sheet2_SecAxis{ix++}.svg", svg);
+ //}
+ }
+ }
+ [TestMethod]
+ public void GenerateSimplestChart()
+ {
+ ExcelPackage.License.SetNonCommercialOrganization("EPPlus Project");
+ using (var p = OpenTemplatePackage("SimplestChart.xlsx"))
+ {
+ var c = p.Workbook.Worksheets[0].Drawings[0];
+
+ var renderer = new EPPlusImageRenderer.ImageRenderer();
+ var svg = renderer.RenderDrawingToSvg(c);
+ SaveTextFileToWorkbook($"svg\\SimplestChartTitle.svg", svg);
+ }
+ }
+
+
+ [TestMethod]
+ public void GenerateDataLabels()
+ {
+ ExcelPackage.License.SetNonCommercialOrganization("EPPlus Project");
+ using (var p = OpenTemplatePackage("datalabelsSvg.xlsx"))
+ {
+ var c = p.Workbook.Worksheets[0].Drawings[0];
+
+ var renderer = new EPPlusImageRenderer.ImageRenderer();
+ var svg = renderer.RenderDrawingToSvg(c);
+ SaveTextFileToWorkbook($"svg\\datalabelsAttempt.svg", svg);
+ }
+ }
+
+
+
+ [TestMethod]
+ public void GenerateDataLabelsTrueMost()
+ {
+ ExcelPackage.License.SetNonCommercialOrganization("EPPlus Project");
+ using (var p = OpenTemplatePackage("datalabelsSvgTrueMostWithFill.xlsx"))
+ {
+ var c = p.Workbook.Worksheets[0].Drawings[0];
+
+ var renderer = new EPPlusImageRenderer.ImageRenderer();
+ var svg = renderer.RenderDrawingToSvg(c);
+ SaveTextFileToWorkbook($"svg\\datalabelsSvgTrueMostWithFill.svg", svg);
+ }
+ }
+
+ [TestMethod]
+ public void GenerateDataLabelsTrueMostAndManualLayout()
+ {
+ ExcelPackage.License.SetNonCommercialOrganization("EPPlus Project");
+ using (var p = OpenTemplatePackage("datalabelsSvgTrueMostWithFillANDManual.xlsx"))
+ {
+ var c = p.Workbook.Worksheets[0].Drawings[0];
+
+ var renderer = new EPPlusImageRenderer.ImageRenderer();
+ var svg = renderer.RenderDrawingToSvg(c);
+ SaveTextFileToWorkbook($"svg\\datalabelsSvgTrueMostWithFillAndManual.svg", svg);
+ }
+ }
+
+ [TestMethod]
+ public void GenerateDatalabelsLeaderLines()
+ {
+ ExcelPackage.License.SetNonCommercialOrganization("EPPlus Project");
+ using (var p = OpenTemplatePackage("datalabelsSvgLeaderLinesAdjustedToBeSimilar.xlsx"))
+ {
+ var c = p.Workbook.Worksheets[0].Drawings[0];
+
+ var renderer = new EPPlusImageRenderer.ImageRenderer();
+ var svg = renderer.RenderDrawingToSvg(c);
+ SaveTextFileToWorkbook($"svg\\datalabelsSvgLeaderLines.svg", svg);
+ }
+ }
+
+
+ [TestMethod]
+ public void GenerateDatalabelsRightAlignedWithBg()
+ {
+ ExcelPackage.License.SetNonCommercialOrganization("EPPlus Project");
+ using (var p = OpenTemplatePackage("datalabelsSvgRightAlignedWithBg.xlsx"))
+ {
+ var c = p.Workbook.Worksheets[0].Drawings[0];
+
+ var renderer = new EPPlusImageRenderer.ImageRenderer();
+ var svg = renderer.RenderDrawingToSvg(c);
+ SaveTextFileToWorkbook($"svg\\datalabelsSvgLeaderLinesBg.svg", svg);
+ }
+ }
+
+ [TestMethod]
+ public void GenerateSimpleLineChart()
+ {
+ ExcelPackage.License.SetNonCommercialOrganization("EPPlus Project");
+ using (var p = OpenTemplatePackage("defChartLine3Points.xlsx"))
+ {
+ var c = p.Workbook.Worksheets[0].Drawings[0];
+
+ var renderer = new EPPlusImageRenderer.ImageRenderer();
+ var svg = renderer.RenderDrawingToSvg(c);
+ SaveTextFileToWorkbook($"svg\\defChartLine3Points.svg", svg);
+ }
+ }
+ }
+}
diff --git a/src/EPPlus.Export.ImageRenderer.Test/GroupItemNewTest.cs b/src/EPPlus.Export.ImageRenderer.Test/GroupItemNewTest.cs
new file mode 100644
index 0000000000..8dbdeef355
--- /dev/null
+++ b/src/EPPlus.Export.ImageRenderer.Test/GroupItemNewTest.cs
@@ -0,0 +1,142 @@
+using EPPlus.Export.ImageRenderer.RenderItems.SvgItem;
+using EPPlus.Export.ImageRenderer.Svg;
+using EPPlus.Graphics;
+using EPPlusImageRenderer.RenderItems;
+using System.Text;
+
+namespace EPPlus.Export.ImageRenderer.Tests
+{
+ [TestClass]
+ public class GroupItemNewTest : TestBase
+ {
+ [TestMethod]
+ public void TestGroupItemMovingTwoChildrenCorrectly()
+ {
+ var baseBB = new BoundingBox();
+
+ //96x96 px
+ baseBB.Width = 72;
+ baseBB.Height = 72;
+
+ var baseItem = new DrawingItemForTesting(baseBB);
+
+ var groupItem = new SvgGroupItemNew(baseItem, 9, 9);
+
+ SvgRenderRectItem rectItem = new SvgRenderRectItem(baseItem, groupItem.Bounds);
+
+ rectItem.FillColor = "red";
+ rectItem.FillOpacity = 0.2d;
+
+ rectItem.Width = 20;
+ rectItem.Height = 20;
+
+ groupItem.AddChildItem(rectItem);
+
+
+ SvgRenderRectItem siblingItem = new SvgRenderRectItem(baseItem, groupItem.Bounds);
+ siblingItem.FillColor = "blue";
+ siblingItem.FillOpacity = 0.2d;
+
+ siblingItem.Width = 20;
+ siblingItem.Height = 20;
+
+ siblingItem.Bounds.Left = 20;
+ siblingItem.Bounds.Top = 20;
+
+ groupItem.AddChildItem(siblingItem);
+
+ var worldCoordinatesRectBefore = rectItem.Bounds.GetWorldCoordinates();
+ Assert.AreEqual(9, worldCoordinatesRectBefore.X);
+ Assert.AreEqual(9, worldCoordinatesRectBefore.Y);
+
+ var worldCoordinatesSibBefore = siblingItem.Bounds.GetWorldCoordinates();
+ Assert.AreEqual(29, worldCoordinatesSibBefore.X);
+ Assert.AreEqual(29, worldCoordinatesSibBefore.Y);
+
+ groupItem.Position.Left = 18;
+ groupItem.Position.Top = 18;
+
+ var worldCoordinatesRect = rectItem.Bounds.GetWorldCoordinates();
+ Assert.AreEqual(18, worldCoordinatesRect.X);
+ Assert.AreEqual(18, worldCoordinatesRect.Y);
+
+ var worldCoordinatesSib = siblingItem.Bounds.GetWorldCoordinates();
+ Assert.AreEqual(38, worldCoordinatesSib.X);
+ Assert.AreEqual(38, worldCoordinatesSib.Y);
+
+ var sb = new StringBuilder();
+
+ baseItem.RenderItems.Add(groupItem);
+
+ baseItem.Render(sb);
+ var svgString = sb.ToString();
+
+ SaveTextFileToWorkbook($"svg\\StandAloneTestGroup.svg", svgString);
+ }
+
+ [TestMethod]
+ public void TestGroupItemRotatingTwoChildrenCorrectly()
+ {
+ var baseBB = new BoundingBox();
+
+ //96x96 px
+ baseBB.Width = 72;
+ baseBB.Height = 72;
+
+ var baseItem = new DrawingItemForTesting(baseBB);
+
+ BoundingBox parent = new BoundingBox();
+
+ var groupItem = new SvgGroupItemNew(baseItem, parent, 45);
+
+ groupItem.Position.Left = 10;
+ groupItem.Position.Top = 10;
+
+ SvgRenderRectItem rectItem = new SvgRenderRectItem(baseItem, groupItem.Bounds);
+
+ rectItem.FillColor = "red";
+ rectItem.FillOpacity = 0.2d;
+
+ rectItem.Width = 20;
+ rectItem.Height = 20;
+
+ groupItem.AddChildItem(rectItem);
+
+
+ SvgRenderRectItem siblingItem = new SvgRenderRectItem(baseItem, groupItem.Bounds);
+ siblingItem.FillColor = "blue";
+ siblingItem.FillOpacity = 0.2d;
+
+ siblingItem.Width = 20;
+ siblingItem.Height = 20;
+
+ siblingItem.Bounds.Left = 20;
+ siblingItem.Bounds.Top = 20;
+
+ groupItem.AddChildItem(siblingItem);
+
+ groupItem.SetRotationPointToCenterOfGroup();
+
+ SvgRenderRectItem centerOfGroupMarker = new SvgRenderRectItem(baseItem, baseItem.Bounds);
+ centerOfGroupMarker.FillColor = "green";
+ centerOfGroupMarker.FillOpacity = 0.8d;
+
+ centerOfGroupMarker.Width = 6;
+ centerOfGroupMarker.Height = 6;
+
+ centerOfGroupMarker.Left = 30 - (centerOfGroupMarker.Width / 2);
+ centerOfGroupMarker.Top = 30 - (centerOfGroupMarker.Height / 2);
+
+ baseItem.RenderItems.Add(centerOfGroupMarker);
+
+ var sb = new StringBuilder();
+
+ baseItem.RenderItems.Add(groupItem);
+
+ baseItem.Render(sb);
+ var svgString = sb.ToString();
+
+ SaveTextFileToWorkbook($"svg\\TestGroupRotated.svg", svgString);
+ }
+ }
+}
diff --git a/src/EPPlus.Export.ImageRenderer.Test/SvgPathTests.cs b/src/EPPlus.Export.ImageRenderer.Test/Shape/ShapeToSvgTests.cs
similarity index 92%
rename from src/EPPlus.Export.ImageRenderer.Test/SvgPathTests.cs
rename to src/EPPlus.Export.ImageRenderer.Test/Shape/ShapeToSvgTests.cs
index f95dbb6412..0521fbe93a 100644
--- a/src/EPPlus.Export.ImageRenderer.Test/SvgPathTests.cs
+++ b/src/EPPlus.Export.ImageRenderer.Test/Shape/ShapeToSvgTests.cs
@@ -1,8 +1,10 @@
using EPPlus.Fonts.OpenType;
+using EPPlus.Fonts.OpenType;
using OfficeOpenXml;
using OfficeOpenXml.Drawing;
using OfficeOpenXml.Drawing.Chart;
using OfficeOpenXml.Style;
+using OfficeOpenXml.Style;
using System.Diagnostics;
using System.Drawing;
using System.Reflection;
@@ -10,10 +12,10 @@
using System.Xml;
using TypeConv = OfficeOpenXml.Utils.TypeConversion;
-namespace TestProject1
+namespace EPPlus.Export.ImageRenderer.Tests.Shape
{
[TestClass]
- public sealed class SvgPathTests : TestBase
+ public sealed class ShapeToSvgTests : TestBase
{
[TestMethod]
public void Rect()
@@ -32,6 +34,25 @@ public void Rect()
SaveAndCleanup(p);
}
}
+
+ [TestMethod]
+ public void AddShapeWithPatternFill()
+ {
+ ExcelPackage.License.SetNonCommercialOrganization("EPPlus Project");
+ using (var p = OpenTemplatePackage("ShapeWithPattern.xlsx"))
+ {
+ var ws = p.Workbook.Worksheets[0];
+
+ var myShape = ws.Drawings[0].As.Shape;
+
+ var ir = new EPPlusImageRenderer.ImageRenderer();
+ var svg = ir.RenderDrawingToSvg(myShape);
+ SaveTextFileToWorkbook("ShapeWithPattern.svg", svg);
+ SaveAndCleanup(p);
+ }
+ }
+
+
[TestMethod]
public void RoundRect()
{
@@ -347,7 +368,7 @@ public void CustomPath()
var ws = p.Workbook.Worksheets[0];
//var d = ws.Drawings[0].As.Shape;
//Assert.AreEqual(1, d.CustomGeom.DrawingPaths.Count);
- //d.Textbox = "GetRectangle GetRectangle GetRectangle GetRectangle";
+ //d.Textbox = "GetGetRectangle GetGetRectangle GetGetRectangle GetGetRectangle";
//d.TextAlignment = OfficeOpenXml.Drawing.eTextAlignment.Left;
//d.TextAnchoring = OfficeOpenXml.Drawing.eTextAnchoringType.Bottom;
var renderer = new EPPlusImageRenderer.ImageRenderer();
@@ -504,26 +525,46 @@ public void GenerateSvgForCircle()
}
}
}
+
[TestMethod]
- public void GenerateSvgForCharts()
+ public void OpenRightAligned()
{
ExcelPackage.License.SetNonCommercialOrganization("EPPlus Project");
- using (var p = OpenTemplatePackage("ChartForSvg.xlsx"))
+ using (var p = OpenTemplatePackage("SimpleChartRightAlign.xlsx"))
{
- var ws = p.Workbook.Worksheets[0];
- var renderer = new EPPlusImageRenderer.ImageRenderer();
+ var c = p.Workbook.Worksheets[0].Drawings[0];
- var ix = 2;
- var c = ws.Drawings[ix];
+ var renderer = new EPPlusImageRenderer.ImageRenderer();
var svg = renderer.RenderDrawingToSvg(c);
- SaveTextFileToWorkbook($"svg\\ChartForSvg_ind{ix++}.svg", svg);
+ SaveTextFileToWorkbook($"svg\\SimplestChartRightAlign.svg", svg);
+ }
+ }
+ [TestMethod]
+ public void TestStyling()
+ {
+ ExcelPackage.License.SetNonCommercialOrganization("EPPlus Project");
+ using (var p = OpenTemplatePackage("MyCellsAdvanced.xlsx"))
+ {
+ var ws = p.Workbook.Worksheets[0];
+ var myCell = ws.Cells["B3"];
+ var myCellStyle = myCell.Style;
+ var fillStyle = myCell.Style.Fill;
+ var fontColor = myCell.Style.Font.Color;
+
+ var bgRgb = myCell.Style.Fill.BackgroundColor.Rgb;
+ var bgFillCol = myCell.Style.Fill.PatternColor.Rgb;
- //var ix = 1;
- //foreach (ExcelChart c in ws.Drawings)
- //{
- // var svg = renderer.RenderDrawingToSvg(c);
- // SaveTextFileToWorkbook($"svg\\ChartForSvg{ix++}.svg", svg);
- //}
+ var myOtherCell = ws.Cells["B2"];
+
+ var myShape = ws.Drawings[0].As.Shape;
+ var myRichtext = myShape.RichText;
+ var firstDefault = myShape.TextBody.Paragraphs.FirstDefaultRunProperties;
+
+ var firstPara = myShape.TextBody.Paragraphs[0];
+ var defRun = firstPara.DefaultRunProperties;
+
+ var secondPara = myShape.TextBody.Paragraphs[1];
+ var secondDefRun = secondPara.DefaultRunProperties;
}
}
@@ -549,7 +590,7 @@ public void GenerateShapeCenteredParagraph()
_currentShape.TextBody.RightInsert = 0;
_currentShape.TextBody.LeftInsert = 0;
- var para1 = _currentShape.TextBody.Paragraphs.Add("TextBox\r\na");
+ var para1 = _currentShape.TextBody.Paragraphs.Add("TextBodySvg\r\na");
//var test = _currentShape.TextBody.AnchorCenter;
para1.LeftMargin = 5;
@@ -590,49 +631,6 @@ public void GenerateShapeCenteredParagraph()
SaveAndCleanup(p);
}
}
-
- [TestMethod]
- public void GenerateSvgForLineCharts()
- {
- ExcelPackage.License.SetNonCommercialOrganization("EPPlus Project");
- using (var p = OpenTemplatePackage("LineChartRenderTest.xlsx"))
- {
- var ws = p.Workbook.Worksheets[0];
- var renderer = new EPPlusImageRenderer.ImageRenderer();
- //var ix = 1;
- //var c = ws.Drawings[ix];
- //var svg = renderer.RenderDrawingToSvg(c);
- //SaveTextFileToWorkbook($"svg\\LineChartForSvg_Single{ix++}.svg", svg);
- var ix = 1;
- foreach (ExcelChart c in ws.Drawings)
- {
- var svg = renderer.RenderDrawingToSvg(c);
- SaveTextFileToWorkbook($"svg\\LineChartForSvg{ix++}.svg", svg);
- }
- }
- }
-
- [TestMethod]
- public void GenerateSvgForCharts_sheet2()
- {
- ExcelPackage.License.SetNonCommercialOrganization("EPPlus Project");
- using (var p = OpenTemplatePackage("ChartForSvg.xlsx"))
- {
- var ws = p.Workbook.Worksheets[1];
- var renderer = new EPPlusImageRenderer.ImageRenderer();
- var ix = 1;
- var c = ws.Drawings[ix];
- var svg = renderer.RenderDrawingToSvg(c);
- SaveTextFileToWorkbook($"svg\\ChartForSvg_sheet2_{ix++}.svg", svg);
- //var ix = 1;
- //foreach (ExcelChart c in ws.Drawings)
- //{
- // var svg = renderer.RenderDrawingToSvg(c);
- // SaveTextFileToWorkbook($"svg\\ChartForSvg{ix++}.svg", svg);
- //}
- }
- }
-
[TestMethod]
public void CreateChartsWithDifferentSize()
{
@@ -666,6 +664,5 @@ public void CreateChartsWithDifferentSize()
SaveAndCleanup(p);
}
}
-
}
}
diff --git a/src/EPPlus.Export.ImageRenderer.Test/StyleTests.cs b/src/EPPlus.Export.ImageRenderer.Test/StyleTests.cs
new file mode 100644
index 0000000000..487249d24b
--- /dev/null
+++ b/src/EPPlus.Export.ImageRenderer.Test/StyleTests.cs
@@ -0,0 +1,160 @@
+using EPPlus.Export.ImageRenderer.Style;
+using EPPlusImageRenderer;
+using OfficeOpenXml;
+using OfficeOpenXml.Drawing;
+using OfficeOpenXml.Export.HtmlExport;
+using OfficeOpenXml.Export.HtmlExport.CssCollections;
+using OfficeOpenXml.Export.HtmlExport.Exporters.Internal;
+using OfficeOpenXml.Export.HtmlExport.Translators;
+using OfficeOpenXml.Export.HtmlExport.Writers;
+using OfficeOpenXml.Utils;
+using System;
+using System.Collections.Generic;
+using System.Drawing;
+using System.Linq;
+using System.Runtime;
+using System.Text;
+using System.Threading.Tasks;
+
+namespace EPPlus.Export.ImageRenderer.Tests
+{
+ [TestClass]
+ public class StyleTests : TestBase
+ {
+
+ [TestMethod]
+ public void BaseThemeChartStyle()
+ {
+ ExcelPackage.License.SetNonCommercialOrganization("EPPlus Project");
+
+ using (var p = OpenTemplatePackage("baseThemeChartStyle.xlsx"))
+ {
+ var c = p.Workbook.Worksheets[0].Drawings[0].As.Chart.LineChart;
+
+ var renderer = new EPPlusImageRenderer.ImageRenderer();
+ var svg = renderer.RenderDrawingToSvg(c);
+ SaveTextFileToWorkbook($"svg\\baseThemeChartStyle.svg", svg);
+ }
+ }
+
+ [TestMethod]
+ public void ExtractThemeStyleWorks()
+ {
+ ExcelPackage.License.SetNonCommercialOrganization("EPPlus Project");
+
+ using (var p = OpenTemplatePackage("MyLineIonThemeExcel.xlsx"))
+ {
+ var wbStyles = p.Workbook.Styles;
+ var simpleChart = p.Workbook.Worksheets[0].Drawings[0].As.Chart.LineChart;
+
+ simpleChart.Title.Text = "Hello";
+
+ //p.Workbook.ThemeManager.CurrentTheme.ColorScheme.Accent1.SetPresetColor(Color.DarkGoldenrod);
+
+ var chartDefaultStyle = simpleChart.StyleManager.Style;
+ //simpleChart.StyleManager.SetChartStyle(OfficeOpenXml.Drawing.Chart.Style.ePresetChartStyle.LineChartStyle2);
+ ////var fillStyle = simpleChart.StyleManager.Style.DataPointLine.Border.Fill;
+
+ //var svgFill = new SvgFill(fillStyle);
+ //var fillTranslator = new SvgFillTranslator(svgFill);
+
+ //var context = new TranslatorContext(new HtmlRangeExportSettings());
+
+ //var declarations = fillTranslator.GenerateDeclarationList(context);
+
+ //var styleClass = new CssRule("Style", 0);
+
+ //context.SetTranslator(fillTranslator);
+ //context.AddDeclarations(styleClass);
+ simpleChart.StyleManager.ApplyStyles();
+
+ //chartDefaultStyle.DataPointLine.FillReference.Color
+ simpleChart.Title.Font.Color = Color.CornflowerBlue;
+ //simpleChart.StyleManager.Style.Title.FontReference.Color.SetPresetColor(Color.CornflowerBlue);
+
+ //Highest order of styling if datapoint does not exist
+ //simpleChart.StyleManager.Style.DeleteAllNode("cs:dataPointLine");
+ simpleChart.StyleManager.Style.DataPointLine.BorderReference.Color.SetPresetColor(Color.Green);
+
+ //simpleChart.StyleManager.Style.DataPointLine.Border.Fill.DeleteNode(path);
+ simpleChart.StyleManager.Style.DataPointLine.Border.Fill.Color = Color.Red;
+ //simpleChart.StyleManager.Style.DataPointLine.Fill.Color = Color.Green;
+
+ //simpleChart.StyleManager.Style.DataPoint.Fill.Color = Color.Yellow;
+ //simpleChart.StyleManager.Style.DataPoint.Border.Fill.Color = Color.Magenta;
+
+ simpleChart.StyleManager.ApplyStyles();
+
+ simpleChart.Title.TextBody.Paragraphs[0].TextRuns[0].Fill.Color = Color.CornflowerBlue;
+
+ var renderer = new EPPlusImageRenderer.ImageRenderer();
+ var svg = renderer.RenderDrawingToSvg(simpleChart);
+ SaveTextFileToWorkbook($"svg\\MyLineIonThemeExcel2.svg", svg);
+
+ //var serLine = chartDefaultStyle.SeriesLine;
+ //var lineElement = serLine.Border.LineElement;
+ //var serLineFillRef = serLine.Border.Fill;
+ //var myFillRef = chartDefaultStyle.Title.FillReference;
+ //var myFill = chartDefaultStyle.Title.Fill;
+ //var myLine = chartDefaultStyle.Title.Border;
+
+ SaveAndCleanup(p);
+ }
+
+ }
+
+ [TestMethod]
+ public void TextRunIsStyledButNotTitleFont()
+ {
+ ExcelPackage.License.SetNonCommercialOrganization("EPPlus Project");
+
+ var chartName = "ChartLineDefaultDownUp.xlsx";
+ using (var p = OpenTemplatePackage(chartName))
+ {
+ var ws = p.Workbook.Worksheets[0];
+ var lChart = ws.Drawings[0].As.Chart.LineChart;
+
+ //lChart.Title.Font.Color = Color.DeepSkyBlue;
+
+ lChart.StyleManager.ApplyStyles();
+
+ SaveAndCleanup(p);
+ }
+
+ //using(var p= OpenPackage(chartName,false))
+ //{
+
+ //}
+ }
+
+ /////
+ ///// Exports an to a html string
+ /////
+ /////
A html table
+ //public string GetCssString()
+ //{
+ // using (var ms = EPPlusMemoryManager.GetStream())
+ // {
+ // RenderCss(ms);
+ // ms.Position = 0;
+ // using (var sr = new StreamReader(ms))
+ // {
+ // return sr.ReadToEnd();
+ // }
+ // }
+ //}
+ /////
+ ///// Exports the css part of the html export.
+ /////
+ /////
The stream to write the css to.
+ /////
+ //public void RenderCss(Stream stream)
+ //{
+ // var trueWriter = new CssWriter(stream);
+ // var cssRules = CreateRuleCollection(_settings);
+
+ // trueWriter.WriteAndClearFlush(cssRules, Settings.Minify);
+ //}
+ }
+}
+
diff --git a/src/EPPlus.Export.ImageRenderer.Test/TestBase.cs b/src/EPPlus.Export.ImageRenderer.Test/TestBase.cs
index 2e52c56a6c..227fafa276 100644
--- a/src/EPPlus.Export.ImageRenderer.Test/TestBase.cs
+++ b/src/EPPlus.Export.ImageRenderer.Test/TestBase.cs
@@ -382,7 +382,6 @@ protected static ExcelRangeBase LoadHierarkiTestData(ExcelWorksheet ws)
}
protected static void LoadGeoTestData(ExcelWorksheet ws)
{
-
var l = new List
{
new GeoData{ Country="Sweden", State = "Stockholm", Sales = 154 },
diff --git a/src/EPPlus.Export.ImageRenderer.Test/TextRenderTests.cs b/src/EPPlus.Export.ImageRenderer.Test/TextRenderTests.cs
index d21ae4cad8..7753bd27f9 100644
--- a/src/EPPlus.Export.ImageRenderer.Test/TextRenderTests.cs
+++ b/src/EPPlus.Export.ImageRenderer.Test/TextRenderTests.cs
@@ -95,7 +95,7 @@ List GetWrappedText(ExcelDrawingTextRunCollection runs, TextFrag
[TestMethod]
public void TextFragmentHandlesEndLines()
{
- string strWEndLines = "TextBox\r\na";
+ string strWEndLines = "TextBodySvg\r\na";
List inputFrags = new List() { strWEndLines };
var textFragments = new TextFragmentCollection(inputFrags);
@@ -220,20 +220,19 @@ public void VerifyTextRunBounds()
cube.SetPixelWidth(5000);
var svgShape = new SvgShape(cube);
-
- SvgTextBodyItem tbItem = svgShape.TextBox;
+ SvgTextBodyItem tbItem = svgShape.TextBodySvg;
var txtRun1Bounds = tbItem.Paragraphs[0].Runs[0].Bounds;
var pxWidth = txtRun1Bounds.Width.PointToPixel();
- Assert.AreEqual(43.835286458333336d, pxWidth);
+ Assert.AreEqual(43.835286458333336d, pxWidth, 0.2d);
var txtRuns2 = tbItem.Paragraphs[1].Runs;
- Assert.AreEqual(53.20963541666667d, txtRuns2[0].Bounds.Width.PointToPixel());
+ Assert.AreEqual(53.20963541666667d, txtRuns2[0].Bounds.Width.PointToPixel(),0.2);
var currentLineWidth = txtRuns2[0].Bounds.Width.PointToPixel();
- Assert.AreEqual(currentLineWidth, txtRuns2[1].Bounds.Left.PointToPixel());
- Assert.AreEqual(69.55924479166667d, txtRuns2[1].Bounds.Width.PointToPixel());
+ Assert.AreEqual(currentLineWidth, txtRuns2[1].Bounds.Left.PointToPixel(),0.2);
+ Assert.AreEqual(69.55924479166667d, txtRuns2[1].Bounds.Width.PointToPixel(),0.2);
currentLineWidth += txtRuns2[1].Bounds.Width.PointToPixel();
Assert.AreEqual(currentLineWidth, txtRuns2[2].Bounds.Left.PointToPixel(),0.0001);
@@ -252,84 +251,108 @@ public void VerifyTextRunBounds()
}
}
- [TestMethod]
- public void ConceptLines()
+ private ExcelShape GenerateTextShapeWithDifficultText(ExcelPackage p)
{
- var currentChar = 'a';
- var defaultFont = new MeasurementFont
- {
- FontFamily = "Aptos Narrow",
- Size = 11,
- Style = MeasurementFontStyles.Regular
- };
+ var myCulture = CultureInfo.CurrentCulture;
- List manyRichText = new List();
- List manyFonts = new List();
+ var ws = p.Workbook.Worksheets.Add("ShapeSheet");
- for (int i = 0; i< 20; i++)
- {
- manyRichText.Add(currentChar.ToString());
- manyFonts.Add(defaultFont);
- currentChar++;
- defaultFont.Size ++;
- }
+ var cube = ws.Drawings.AddShape("myCube", eShapeStyle.Cube);
+
+ cube.Fill.Style = eFillStyle.SolidFill;
+ cube.Fill.Color = System.Drawing.Color.BlueViolet;
+ cube.Font.Color = System.Drawing.Color.Goldenrod;
- var fragments = new TextFragmentCollection(manyRichText);
- var fontMeasurer = new FontMeasurerTrueType();
+ cube.TextBody.TopInsert = 0;
+ cube.TextBody.BottomInsert = 0;
+ cube.TextBody.RightInsert = 0;
+ cube.TextBody.LeftInsert = 0;
- var strings = fontMeasurer.WrapMultipleTextFragments(fragments, manyFonts, 30d.PixelToPoint());
+ var para1 = cube.TextBody.Paragraphs.Add("TextBodySvg\r\na");
- var outputLines = fragments.GetOutputLines();
+ var para2 = cube.TextBody.Paragraphs.Add("TextBox2");
+ para2.TextRuns[0].FontItalic = true;
+ para2.TextRuns[0].FontBold = true;
+ para2.TextRuns.Add("ra underline").FontUnderLine = eUnderLineType.Dash;
+ para2.TextRuns.Add("La Strike").FontStrike = eStrikeType.Single;
+ var tRun1 = para2.TextRuns.Add("Goudy size 16");
+ tRun1.SetFromFont("Goudy Stout", 16);
+ tRun1.Fill.Color = System.Drawing.Color.IndianRed;
+ var tRun2 = para2.TextRuns.Add("SvgSize 24");
+ tRun2.FontSize = 24;
- //var paragraph = new TextParagraph(fragments, manyFonts);
+ cube.TextBody.HorizontalTextOverflow = eTextHorizontalOverflow.Clip;
+ cube.TextBody.VerticalTextOverflow = eTextVerticalOverflow.Clip;
- //List manyRichText = new List() {"a","b","c","d","e","f","g" };
+ var aFont = cube.Font;
+ var paragraph0 = cube.TextBody.Paragraphs[0];
+
+ var autofit = cube.TextBody.TextAutofit;
+
+ cube.ChangeCellAnchor(eEditAs.Absolute);
+
+ cube.GetSizeInPixels(out int testWidth, out int testHeight);
+
+ cube.SetPixelHeight(400);
+ cube.SetPixelWidth(400);
+
+ return cube;
}
+
[TestMethod]
- public void ConceptTextRun()
+ public void VerifyVerticalAlignTop()
{
- string rt1 = "My richtext1\r\n of len";
- string rt2 = "gth beyond and then I am richtext2";
+ ExcelPackage.License.SetNonCommercialOrganization("EPPlus Project");
- string combined = rt1 + rt2;
+ using var p = new ExcelPackage();
+ var shape = GenerateTextShapeWithDifficultText(p);
- int charMax = 10;
+ shape.TextAnchoring = eTextAnchoringType.Top;
- int lineCharCount = 0;
+ var svgShape = new SvgShape(shape);
+ SvgTextBodyItem tbItem = svgShape.TextBodySvg;
- List lines = new List();
+ Assert.AreEqual(100, tbItem.Bounds.GlobalTop.PointToPixel());
+ }
- for (int i = 0; i < combined.Length; i++)
- {
- if(lineCharCount > charMax)
- {
- var currLine = combined.Substring(i - lineCharCount, lineCharCount);
- lines.Add(currLine);
- lineCharCount = 0;
- }
+ [TestMethod]
+ public void VerifyVerticalAlignCenter()
+ {
+ ExcelPackage.License.SetNonCommercialOrganization("EPPlus Project");
- if (combined[i] == '\r')
- {
- var currLine = combined.Substring(i - lineCharCount, lineCharCount);
- lines.Add(currLine);
- lineCharCount = 0;
- i++;
- continue;
- }
+ using var p = new ExcelPackage();
+ var shape = GenerateTextShapeWithDifficultText(p);
- lineCharCount++;
- }
+ shape.TextAnchoring = eTextAnchoringType.Center;
- var finalLine = combined.Substring(combined.Length - lineCharCount, lineCharCount);
- lines.Add(finalLine);
+ var svgShape = new SvgShape(shape);
+ SvgTextBodyItem tbItem = svgShape.TextBodySvg;
- foreach (string line in lines)
- {
- Debug.WriteLine(line);
- }
+ //Appears off by 1-2 px bc of border width
+ Assert.AreEqual(190d, tbItem.Bounds.GlobalTop.PointToPixel(),1.0);
+ }
+
+
+ [TestMethod]
+ public void VerifyVerticalAlignBottom()
+ {
+ ExcelPackage.License.SetNonCommercialOrganization("EPPlus Project");
+
+ using var p = new ExcelPackage();
+ var shape = GenerateTextShapeWithDifficultText(p);
+
+ shape.TextAnchoring = eTextAnchoringType.Bottom;
+
+ var svgShape = new SvgShape(shape);
+ SvgTextBodyItem tbItem = svgShape.TextBodySvg;
+
+ var ir = new EPPlusImageRenderer.ImageRenderer();
+ var svg = ir.RenderDrawingToSvg(shape);
+ //Appears off by ~2 px because of border width
+ Assert.AreEqual(278d, tbItem.Bounds.GlobalTop.PointToPixel(), 1.0);
}
}
}
diff --git a/src/EPPlus.Export.ImageRenderer.Test/TextboxTest.cs b/src/EPPlus.Export.ImageRenderer.Test/TextboxTest.cs
new file mode 100644
index 0000000000..757f95f480
--- /dev/null
+++ b/src/EPPlus.Export.ImageRenderer.Test/TextboxTest.cs
@@ -0,0 +1,65 @@
+using EPPlus.Export.ImageRenderer.RenderItems.SvgItem;
+using EPPlus.Export.ImageRenderer.Svg;
+using EPPlus.Graphics;
+using EPPlusImageRenderer.RenderItems;
+using OfficeOpenXml.FormulaParsing.Excel.Functions.MathFunctions;
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+
+namespace EPPlus.Export.ImageRenderer.Tests
+{
+ [TestClass]
+ public class TextboxTest : TestBase
+ {
+ [TestMethod]
+ public void TextBoxVerification()
+ {
+ var baseBB = new BoundingBox();
+
+ //96x96 px
+ baseBB.Width = 72;
+ baseBB.Height = 72;
+
+ var item = new DrawingItemForTesting(baseBB);
+
+ BoundingBox maxBounds = new BoundingBox();
+ maxBounds.Width = 36;
+ maxBounds.Height = 36;
+
+ var txtBox = new SvgTextBox(item, item.Bounds, maxBounds);
+ txtBox.AddText(0, "My new text which is fun");
+ txtBox.Rectangle.FillColor = "red";
+ txtBox.Rectangle.FillOpacity = 0.2d;
+
+ txtBox.Left = 5;
+ txtBox.Top = 5;
+
+ item.ExternalRenderItemsNoBounds.Add(txtBox);
+
+ var txtBoxNotMaxed = new SvgTextBox(item, item.Bounds, maxBounds);
+ txtBoxNotMaxed.Left = 42;
+ txtBoxNotMaxed.Top = 5;
+
+ txtBoxNotMaxed.AddText(0, "abc");
+ txtBoxNotMaxed.Rectangle.FillColor = "yellow";
+ txtBoxNotMaxed.Rectangle.FillOpacity = 0.2d;
+
+ item.ExternalRenderItemsNoBounds.Add(txtBoxNotMaxed);
+
+ //Assert.AreEqual(36d, txtBox.TextBody.Width);
+ Assert.AreEqual(37.5d, txtBox.Width, 0.5);
+ Assert.AreNotEqual(36d, txtBoxNotMaxed.Width);
+ Assert.AreEqual(16.29052734375d, txtBoxNotMaxed.Width);
+
+ var sb = new StringBuilder();
+
+ item.Render(sb);
+ var svgString = sb.ToString();
+
+ SaveTextFileToWorkbook($"svg\\StandAloneTextBox.svg", svgString);
+ }
+ }
+}
diff --git a/src/EPPlus.Export.ImageRenderer/DrawingBase.cs b/src/EPPlus.Export.ImageRenderer/DrawingBase.cs
index 4f1890a593..339b2ae6eb 100644
--- a/src/EPPlus.Export.ImageRenderer/DrawingBase.cs
+++ b/src/EPPlus.Export.ImageRenderer/DrawingBase.cs
@@ -13,11 +13,13 @@ Date Author Change
using EPPlus.Export.ImageRenderer;
using EPPlus.Export.ImageRenderer.Utils;
+using EPPlus.Fonts.OpenType;
using EPPlus.Graphics;
using EPPlusImageRenderer.RenderItems;
using OfficeOpenXml;
using OfficeOpenXml.Drawing;
using OfficeOpenXml.Drawing.Theme;
+using OfficeOpenXml.Export.HtmlExport;
using OfficeOpenXml.Interfaces.Drawing.Text;
using System.Collections.Generic;
using System.Text;
@@ -33,7 +35,20 @@ internal DrawingBase(ExcelDrawing drawing)
var wb = drawing._drawings.Worksheet.Workbook;
Theme = wb.ThemeManager.GetOrCreateTheme();
+ TextMeasurer = new FontMeasurerTrueType();
}
+
+
+ internal DrawingBase()
+ {
+ //Drawing = drawing;
+ //Bounds = drawing.GetBoundingBox();
+
+ //var wb = drawing._drawings.Worksheet.Workbook;
+ //Theme = wb.ThemeManager.GetOrCreateTheme();
+ }
+
+ internal readonly StyleCache _styleCache = new StyleCache();
public ExcelDrawing Drawing { get; }
public ExcelTheme Theme { get;}
public ExcelWorkbook Workbook => Drawing._drawings.Worksheet.Workbook;
diff --git a/src/EPPlus.Export.ImageRenderer/DrawingChart.cs b/src/EPPlus.Export.ImageRenderer/DrawingChart.cs
index 9f38da4193..9908c7db57 100644
--- a/src/EPPlus.Export.ImageRenderer/DrawingChart.cs
+++ b/src/EPPlus.Export.ImageRenderer/DrawingChart.cs
@@ -11,15 +11,7 @@ Date Author Change
27/11/2025 EPPlus Software AB EPPlus 9
*************************************************************************************************/
-using EPPlus.Export.ImageRenderer.Utils;
-using EPPlusImageRenderer.RenderItems;
using OfficeOpenXml.Drawing.Chart;
-using OfficeOpenXml.FormulaParsing.Excel.Functions.Finance;
-using OfficeOpenXml.FormulaParsing.Excel.Functions.MathFunctions;
-using OfficeOpenXml.Utils.TypeConversion;
-using System;
-using System.Collections.Generic;
-using System.Linq;
namespace EPPlusImageRenderer
{
diff --git a/src/EPPlus.Export.ImageRenderer/ImageRenderer.cs b/src/EPPlus.Export.ImageRenderer/ImageRenderer.cs
index 0642b6dc5c..20f27ba299 100644
--- a/src/EPPlus.Export.ImageRenderer/ImageRenderer.cs
+++ b/src/EPPlus.Export.ImageRenderer/ImageRenderer.cs
@@ -11,22 +11,25 @@ Date Author Change
27/11/2025 EPPlus Software AB EPPlus 9
*************************************************************************************************/
-using EPPlus.Export.ImageRenderer;
+using EPPlus.Export.ImageRenderer.RenderItems;
using EPPlus.Export.ImageRenderer.RenderItems.Shared;
using EPPlus.Export.ImageRenderer.RenderItems.SvgItem;
using EPPlus.Export.ImageRenderer.Svg;
+using EPPlus.Export.ImageRenderer.Svg.DefinitionUtils;
+using EPPlus.Export.ImageRenderer.Svg.DefinitionUtils.UtillNodes;
using EPPlus.Export.ImageRenderer.Svg.NodeAttributes;
using EPPlus.Export.ImageRenderer.Svg.Writer;
-using EPPlus.Fonts.OpenType;
+using EPPlus.Fonts.OpenType.Utils;
using EPPlus.Graphics;
+using EPPlusImageRenderer.RenderItems;
using EPPlusImageRenderer.Svg;
-using OfficeOpenXml;
using OfficeOpenXml.Drawing;
using OfficeOpenXml.Drawing.Chart;
-using OfficeOpenXml.Drawing.Theme;
-using OfficeOpenXml.FormulaParsing.Excel.Functions;
using OfficeOpenXml.Utils;
+using OfficeOpenXml.Utils.EnumUtils;
using System;
+using System.Collections.Generic;
+using System.Drawing;
using System.IO;
using System.Text;
@@ -53,6 +56,334 @@ public string RenderDrawingToSvg(ExcelDrawing drawing)
throw new NotImplementedException("Image rendering for drawing type not implemented.");
}
+ public enum RenderItemClasses
+ {
+ Rect,
+ TextBox,
+ Shape,
+ }
+
+ public Dictionary GetItemProperties(RenderItemClasses item)
+ {
+ switch (item)
+ {
+ case RenderItemClasses.Rect:
+ return new Dictionary { { "Top", 10d }, { "Left", 10d }, { "Width", 10d }, { "Height", 10d }, {"Opacity", 0.8 }, {"Fill", Color.Goldenrod } };
+ case RenderItemClasses.TextBox:
+ default:
+ throw new NotImplementedException("This class has not been implemented as an option yet");
+ }
+ }
+
+ ///
+ /// For Testing the specific renderItem class
+ ///
+ ///
+ ///
+ ///
+ ///
+ public string RenderIndividualClass(RenderItemClasses item, Dictionary itemProperties, double width, double height)
+ {
+ var svgCanvas = new DrawingItemForTesting(new BoundingBox(width.PixelToPoint(), height.PixelToPoint()));
+
+ var renderItem = GenerateFromClasses(item, svgCanvas, itemProperties);
+
+ svgCanvas.RenderItems.Add(renderItem);
+
+ var sb = new StringBuilder();
+
+ svgCanvas.Render(sb);
+
+ return sb.ToString();
+ }
+
+ public enum RenderPresets
+ {
+ ContainerMargins,
+ RotatingContainer,
+ PatternFill,
+ }
+
+ public string RenderTest(RenderPresets preset)
+ {
+ return GenerateFromPreset(preset);
+ }
+
+
+ private string rotatingContainer()
+ {
+ var baseBB = new BoundingBox();
+
+ //96x96 px
+ baseBB.Width = 72;
+ baseBB.Height = 72;
+
+ var baseItem = new DrawingItemForTesting(baseBB);
+
+ BoundingBox parent = new BoundingBox();
+
+ var groupItem = new SvgGroupItemNew(baseItem, parent, 45);
+
+ groupItem.Position.Left = 10;
+ groupItem.Position.Top = 10;
+
+ SvgRenderRectItem rectItem = new SvgRenderRectItem(baseItem, groupItem.Bounds);
+
+ rectItem.FillColor = "red";
+ rectItem.FillOpacity = 0.2d;
+
+ rectItem.Width = 20;
+ rectItem.Height = 20;
+
+ groupItem.AddChildItem(rectItem);
+
+
+ SvgRenderRectItem siblingItem = new SvgRenderRectItem(baseItem, groupItem.Bounds);
+ siblingItem.FillColor = "blue";
+ siblingItem.FillOpacity = 0.2d;
+
+ siblingItem.Width = 20;
+ siblingItem.Height = 20;
+
+ siblingItem.Bounds.Left = 20;
+ siblingItem.Bounds.Top = 20;
+
+ groupItem.AddChildItem(siblingItem);
+
+ groupItem.SetRotationPointToCenterOfGroup();
+
+ SvgRenderRectItem centerOfGroupMarker = new SvgRenderRectItem(baseItem, baseItem.Bounds);
+ centerOfGroupMarker.FillColor = "green";
+ centerOfGroupMarker.FillOpacity = 0.8d;
+
+ centerOfGroupMarker.Width = 6;
+ centerOfGroupMarker.Height = 6;
+
+ centerOfGroupMarker.Left = 30 - (centerOfGroupMarker.Width / 2);
+ centerOfGroupMarker.Top = 30 - (centerOfGroupMarker.Height / 2);
+
+ baseItem.RenderItems.Add(centerOfGroupMarker);
+
+ var sb = new StringBuilder();
+
+ baseItem.RenderItems.Add(groupItem);
+
+ baseItem.Render(sb);
+
+ return sb.ToString();
+ }
+
+ private string containerMargins()
+ {
+ var baseBB = new BoundingBox();
+
+ baseBB.Width = 400;
+ baseBB.Height = 400;
+
+ var baseItem = new DrawingItemForTesting(baseBB);
+
+ SvgRenderRectItem myBgItem = new SvgRenderRectItem(baseItem, baseItem.Bounds);
+ myBgItem.FillColor = "purple";
+ myBgItem.FillOpacity = 0.2d;
+
+ SvgRenderRectItem myInnerItem = new SvgRenderRectItem(baseItem, myBgItem.Bounds);
+
+ myInnerItem.FillColor = "green";
+ myInnerItem.FillOpacity = 0.8d;
+
+ myInnerItem.Width = 50;
+ myInnerItem.Height = 50;
+
+ var container = new SvgContainerItem(myInnerItem, myBgItem);
+
+ container.MarginLeft = 5;
+ container.MarginRight = 5;
+ container.MarginTop = 5;
+ container.MarginBottom = 5;
+
+ container.ApplyMargins();
+
+ baseItem.RenderItems.Add(container);
+
+ var sb = new StringBuilder();
+
+ baseItem.Render(sb);
+
+ return sb.ToString();
+ }
+
+ internal string pattern()
+ {
+ var baseBB = new BoundingBox();
+
+ baseBB.Width = 400;
+ baseBB.Height = 400;
+
+ var baseItem = new DrawingItemForTesting(baseBB);
+
+
+ var linePattern = new LinePattern(baseItem, "testLines", LinePatternType.Vertical);
+ linePattern.SetNumberOfLines(3);
+
+ var defItem = new DefinitionGroup(baseItem);
+ defItem.Items.Add(linePattern);
+
+ var rectItem = new SvgRenderRectItem(baseItem, baseItem.Bounds);
+
+ rectItem.FillColor = $"url(#{"testLines"})";
+
+ rectItem.Width = 200;
+ rectItem.Height = 200;
+
+ baseItem.RenderItems.Add(defItem);
+ baseItem.RenderItems.Add(rectItem);
+ var useItem = new SvgUseRefItem(baseItem,baseItem.Bounds,"testLines");
+
+ baseItem.RenderItems.Add(useItem);
+
+ var sb = new StringBuilder();
+
+ baseItem.Render(sb);
+
+ return sb.ToString();
+ }
+
+ internal string pattern2()
+ {
+ var baseBB = new BoundingBox();
+
+ baseBB.Width = 400;
+ baseBB.Height = 400;
+
+ var baseItem = new DrawingItemForTesting(baseBB);
+
+ string refId = "grid";
+
+ var defItem = new DefinitionGroup(baseItem);
+
+ var dynaGrid = new DynamicGridDefGroup(baseItem, refId, 7, 5);
+ defItem.Items.Add(dynaGrid);
+
+ baseItem.RenderItems.Add(defItem);
+
+ var useItem = new SvgUseRefItem(baseItem, baseItem.Bounds, refId);
+ useItem.Bounds.Width = 300;
+ useItem.Bounds.Height = 200;
+
+ baseItem.RenderItems.Add(useItem);
+
+ var sb = new StringBuilder();
+
+ baseItem.Render(sb);
+
+ return sb.ToString();
+ }
+
+ private string GenerateFromPreset(RenderPresets preset)
+ {
+ switch (preset)
+ {
+ case RenderPresets.ContainerMargins:
+ return containerMargins();
+ case RenderPresets.RotatingContainer:
+ return rotatingContainer();
+ case RenderPresets.PatternFill:
+ return pattern2();
+ }
+ return "";
+ }
+
+ private RenderItem GenerateRect(DrawingBase baseItem, Dictionary itemProperties)
+ {
+ var rectItem = new SvgRenderRectItem(baseItem, baseItem.Bounds);
+
+ if (itemProperties.ContainsKey("Top"))
+ {
+ rectItem.Top = (double)itemProperties["Top"];
+ }
+ if (itemProperties.ContainsKey("Left"))
+ {
+ rectItem.Left = (double)itemProperties["Left"];
+ }
+ if (itemProperties.ContainsKey("Width"))
+ {
+ rectItem.Width = (double)itemProperties["Width"];
+ }
+ if (itemProperties.ContainsKey("Height"))
+ {
+ rectItem.Height = (double)itemProperties["Height"];
+ }
+ if (itemProperties.ContainsKey("Opacity"))
+ {
+ rectItem.FillOpacity = (double)itemProperties["Opacity"];
+ }
+ if (itemProperties.ContainsKey("Fill"))
+ {
+ rectItem.FillColor = "#" + ((Color)itemProperties["Fill"]).ToColorString();
+ }
+
+ return rectItem;
+ }
+
+ private RenderItem GenerateFromClasses(RenderItemClasses preset, DrawingBase baseItem, Dictionary itemProperties)
+ {
+ switch (preset)
+ {
+ case RenderItemClasses.Rect:
+ return GenerateRect(baseItem, itemProperties);
+ case RenderItemClasses.TextBox:
+
+
+ default:
+ throw new NotImplementedException("This class has not been implemented as an option yet");
+ }
+ }
+
+ //public string RenderTestCanvas(double widthPixel, double heightPixel, Color bgColor)
+ //{
+
+ //}
+
+ //public string RenderBaseItemToSvg(DrawingBase drawing)
+ //{
+
+ //}
+
+ ////Attempt at ensuring features can be created/tested individually by the system
+ ////Without relying on having a whole workbook or epplus project.
+ ////Simply: Does our positioning, sizing and parent hierarchy logic work as expected or not.
+ //public string RenderIndependentCanvas(double widthPixel, double heightPixel, Color bgColor)
+ //{
+ // var widthPoint = widthPixel.PixelToPoint();
+ // var heightPoint = heightPixel.PixelToPoint();
+
+ // var CanvasBounds = new BoundingBox(widthPoint, heightPoint);
+
+ // var sb = new StringBuilder();
+ // var canvas = new SvgIndependentCanvas(CanvasBounds, bgColor);
+
+ // var rect = new SvgIndependentRect(canvas.Bounds, widthPoint/2, heightPoint/2);
+ // rect.FillColor = "red";
+ // rect.Left = 10;
+
+ // //BoundingBox boundsTextBox = new BoundingBox(rect.Bounds.Left, rect.Bounds.Top, rect.Bounds.Width, rect.Bounds.Height);
+ // //var independentTxtBox = new SvgIndependentTextBox(canvas, boundsTextBox);
+
+ // //var textBox = new svgin
+
+ // canvas.AddRenderItem(rect);
+
+ // canvas.Render(sb);
+ // return sb.ToString();
+ //}
+
+
+ //public string RenderTextBox(ExcelDrawing someDrawing, BoundingBox parent, double maxHeight, double maxWidth)
+ //{
+ // var sb = new StringBuilder();
+ // var svgTextBox = new SvgTextBox(someDrawing, parent, maxWidth, maxHeight);
+ //}
+
//public string RenderRangeToSvg(ExcelRange range)
//{
@@ -80,12 +411,28 @@ public string RenderDrawingToSvg(ExcelDrawing drawing)
// return sb.ToString();
//}
+ //public string RenderBox(string boxText)
+ //{
+ // string retStr = "";
+ // var container = new TextContainerBase(boxText);
+ // var element = GenerateSvg(container);
//public string RenderBox(string boxText)
//{
// string retStr = "";
// var container = new TextContainerBase(boxText);
// var element = GenerateSvg(container);
+ // using (var ms = EPPlusMemoryManager.GetStream())
+ // {
+ // SvgWriter writer = new SvgWriter(ms, Encoding.UTF8);
+ // writer.RenderSvgElement(element, true);
+ // ms.Position = 0;
+ // using (var sr = new StreamReader(ms))
+ // {
+ // retStr = sr.ReadToEnd();
+ // return retStr;
+ // }
+ // }
// using (var ms = EPPlusMemoryManager.GetStream())
// {
// SvgWriter writer = new SvgWriter(ms, Encoding.UTF8);
@@ -98,11 +445,12 @@ public string RenderDrawingToSvg(ExcelDrawing drawing)
// }
// }
+ // //writer.RenderSvgElement(element, true);
// //writer.RenderSvgElement(element, true);
// //StreamReader reader = new StreamReader(ms);
// //retStr = reader.ReadToEnd();
-
+
// ////SvgParagraph para = new SvgParagraph(container.GetContent(),);
// ////var doc = new SvgEpplusDocument();
// //return retStr;
@@ -178,25 +526,46 @@ internal SvgElement GetDefinitions(BoundingBox boundingBox, out string nameId, b
return def;
}
+ //internal SvgElement GenerateSvg(TextContainerBase container)
+ //{
+ // var fullString = container.GetContent();
//internal SvgElement GenerateSvg(TextContainerBase container)
//{
// var fullString = container.GetContent();
+ // var doc = new SvgEpplusDocument(500, 500);
// var doc = new SvgEpplusDocument(500, 500);
+ // var bg = new SvgElement("rect");
+ // bg.AddAttribute("width", "100%");
+ // bg.AddAttribute("height", "100%");
+ // bg.AddAttribute("fill", "red");
+ // bg.AddAttribute("opacity", "0.1");
// var bg = new SvgElement("rect");
// bg.AddAttribute("width", "100%");
// bg.AddAttribute("height", "100%");
// bg.AddAttribute("fill", "red");
// bg.AddAttribute("opacity", "0.1");
+ // var nameId = "boundingBox";
+ // var def = new SvgElement("defs");
+ // var clipPath = new SvgElement("clipPath");
+ // clipPath.AddAttribute("id", nameId);
// var nameId = "boundingBox";
// var def = new SvgElement("defs");
// var clipPath = new SvgElement("clipPath");
// clipPath.AddAttribute("id", nameId);
+ // def.AddChildElement(clipPath);
// def.AddChildElement(clipPath);
+ // var bb = new SvgElement("rect");
+ // bb.AddAttribute("x", container.Position.X);
+ // bb.AddAttribute("y", container.Position.Y);
+ // bb.AddAttribute("width", container.Width);
+ // bb.AddAttribute("height", container.Height);
+ // //bb.AddAttribute("fill", "blue");
+ // //bb.AddAttribute("opacity", "0.5");
// var bb = new SvgElement("rect");
// bb.AddAttribute("x", container.Position.X);
// bb.AddAttribute("y", container.Position.Y);
@@ -205,18 +574,33 @@ internal SvgElement GetDefinitions(BoundingBox boundingBox, out string nameId, b
// //bb.AddAttribute("fill", "blue");
// //bb.AddAttribute("opacity", "0.5");
+ // clipPath.AddChildElement(bb);
// clipPath.AddChildElement(bb);
+ // var fontSizePx = 16d;
// var fontSizePx = 16d;
+ // var renderElement = new SvgElement("text");
+ // renderElement.AddAttribute("x", container.Position.X);
+ // renderElement.AddAttribute("y", container.Position.Y + fontSizePx);
+ // renderElement.AddAttribute("_measurementFont-size", $"{fontSizePx}px");
+ // renderElement.AddAttribute("clip-path", $"url(#{nameId})");
// var renderElement = new SvgElement("text");
// renderElement.AddAttribute("x", container.Position.X);
// renderElement.AddAttribute("y", container.Position.Y + fontSizePx);
// renderElement.AddAttribute("_measurementFont-size", $"{fontSizePx}px");
// renderElement.AddAttribute("clip-path", $"url(#{nameId})");
+ // renderElement.Content = fullString;
// renderElement.Content = fullString;
+ // var bbVisual = new SvgElement("rect");
+ // bbVisual.AddAttribute("x", container.Position.X);
+ // bbVisual.AddAttribute("y", container.Position.Y);
+ // bbVisual.AddAttribute("width", container.Width);
+ // bbVisual.AddAttribute("height", container.Height);
+ // bbVisual.AddAttribute("fill", "blue");
+ // bbVisual.AddAttribute("opacity", "0.5");
// var bbVisual = new SvgElement("rect");
// bbVisual.AddAttribute("x", container.Position.X);
// bbVisual.AddAttribute("y", container.Position.Y);
@@ -225,14 +609,21 @@ internal SvgElement GetDefinitions(BoundingBox boundingBox, out string nameId, b
// bbVisual.AddAttribute("fill", "blue");
// bbVisual.AddAttribute("opacity", "0.5");
+ // doc.AddChildElement(def);
+ // doc.AddChildElement(bg);
+ // doc.AddChildElement(bbVisual);
+ // doc.AddChildElement(renderElement);
// doc.AddChildElement(def);
// doc.AddChildElement(bg);
// doc.AddChildElement(bbVisual);
// doc.AddChildElement(renderElement);
+ // doc.AddAttributes();
// doc.AddAttributes();
// return doc;
//}
+ // return doc;
+ //}
}
}
diff --git a/src/EPPlus.Export.ImageRenderer/RenderItems/GradientFill.cs b/src/EPPlus.Export.ImageRenderer/RenderItems/GradientFill.cs
index 1b9c06a2dc..b3adc22c44 100644
--- a/src/EPPlus.Export.ImageRenderer/RenderItems/GradientFill.cs
+++ b/src/EPPlus.Export.ImageRenderer/RenderItems/GradientFill.cs
@@ -14,7 +14,9 @@ Date Author Change
using OfficeOpenXml.Drawing.Style.Fill;
using OfficeOpenXml.Drawing.Theme;
using System.Collections.Generic;
+using System.Drawing;
using EPPlusColorConverter = OfficeOpenXml.Utils.TypeConversion.ColorConverter;
+using System;
namespace EPPlusImageRenderer.RenderItems
{
@@ -30,6 +32,16 @@ public DrawGradientFill(ExcelTheme theme, ExcelDrawingGradientFill gradientFill)
}
}
+
+ public DrawGradientFill(List colors, List stops)
+ {
+ for (int i = 0; i < stops.Count; i++)
+ {
+ var c = new GradientFillColor(stops[i], colors[i]);
+ Colors.Add(c);
+ }
+ }
+
public ExcelDrawingGradientFill Settings { get; set; }
public List Colors { get; set; } = new List();
}
diff --git a/src/EPPlus.Export.ImageRenderer/RenderItems/Interfaces/IBorder.cs b/src/EPPlus.Export.ImageRenderer/RenderItems/Interfaces/IBorder.cs
new file mode 100644
index 0000000000..4f70d4441a
--- /dev/null
+++ b/src/EPPlus.Export.ImageRenderer/RenderItems/Interfaces/IBorder.cs
@@ -0,0 +1,14 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+
+namespace EPPlus.Export.ImageRenderer.RenderItems.Interfaces
+{
+ public interface IBorder : IFill
+ {
+ double? BorderWidth { get; set; }
+ double[] BorderDashArray { get; set; }
+ double? BorderDashOffset { get; set; }
+ }
+}
diff --git a/src/EPPlus.Export.ImageRenderer/RenderItems/Interfaces/IFill.cs b/src/EPPlus.Export.ImageRenderer/RenderItems/Interfaces/IFill.cs
new file mode 100644
index 0000000000..7bdc1b33a6
--- /dev/null
+++ b/src/EPPlus.Export.ImageRenderer/RenderItems/Interfaces/IFill.cs
@@ -0,0 +1,15 @@
+using OfficeOpenXml.Drawing;
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+
+namespace EPPlus.Export.ImageRenderer.RenderItems.Interfaces
+{
+ public interface IFill
+ {
+ string Color { get; set; }
+ double? Opacity { get; set; }
+ PathFillMode FillMode { get; set; }
+ }
+}
diff --git a/src/EPPlus.Export.ImageRenderer/RenderItems/Interfaces/IRectItem.cs b/src/EPPlus.Export.ImageRenderer/RenderItems/Interfaces/IRectItem.cs
new file mode 100644
index 0000000000..ef091d7641
--- /dev/null
+++ b/src/EPPlus.Export.ImageRenderer/RenderItems/Interfaces/IRectItem.cs
@@ -0,0 +1,15 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+
+namespace EPPlus.Export.ImageRenderer.RenderItems.Interfaces
+{
+ internal interface IRectItem
+ {
+ double Top { get; set; }
+ double Left { get; set; }
+ double Height { get; set; }
+ double Width { get; set; }
+ }
+}
diff --git a/src/EPPlus.Export.ImageRenderer/RenderItems/Interfaces/IStylingInfo.cs b/src/EPPlus.Export.ImageRenderer/RenderItems/Interfaces/IStylingInfo.cs
new file mode 100644
index 0000000000..3a3c86eab8
--- /dev/null
+++ b/src/EPPlus.Export.ImageRenderer/RenderItems/Interfaces/IStylingInfo.cs
@@ -0,0 +1,25 @@
+using OfficeOpenXml.Drawing;
+using OfficeOpenXml.Drawing.Style.Effect;
+using OfficeOpenXml.Drawing.Style.Fill;
+
+namespace EPPlus.Export.ImageRenderer.RenderItems.Interfaces
+{
+ public interface StylingInfo
+ {
+ IBorder Border { get; set; }
+ IFill Fill { get; set; }
+ string FilterName { get; set; }
+ //public DrawGradientFill GradientFill { get; set; }
+ //SvgFillType FillType { get; set; }
+ //DrawGradientFill BorderGradientFill { get; set; }
+ ExcelDrawingPatternFill PatternFill { get; }
+ ExcelDrawingBlipFill BlipFill { get; }
+ int StrokeMiterLimit { get; set; }
+ eCompoundLineStyle CompoundLineStyle { get; set; }
+ eLineCap LineCap { get; set; }
+ //SvgLineJoin LineJoin { get; set; }
+ double? GlowRadius { get; }
+ string GlowColor { get; }
+ ExcelDrawingOuterShadowEffect OuterShadowEffect { get; }
+ }
+}
diff --git a/src/EPPlus.Export.ImageRenderer/RenderItems/Interfaces/IStylingInfoBase.cs b/src/EPPlus.Export.ImageRenderer/RenderItems/Interfaces/IStylingInfoBase.cs
new file mode 100644
index 0000000000..de110ec790
--- /dev/null
+++ b/src/EPPlus.Export.ImageRenderer/RenderItems/Interfaces/IStylingInfoBase.cs
@@ -0,0 +1,13 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+
+namespace EPPlus.Export.ImageRenderer.RenderItems.Interfaces
+{
+ internal interface IStylingInfoBase
+ {
+ string FillColor { get; set; }
+ string BorderColor { get; set; }
+ }
+}
diff --git a/src/EPPlus.Export.ImageRenderer/RenderItems/RenderItem.cs b/src/EPPlus.Export.ImageRenderer/RenderItems/RenderItem.cs
index a145fda4de..b343c69d66 100644
--- a/src/EPPlus.Export.ImageRenderer/RenderItems/RenderItem.cs
+++ b/src/EPPlus.Export.ImageRenderer/RenderItems/RenderItem.cs
@@ -59,6 +59,8 @@ internal virtual void GetBounds(out double il, out double it, out double ir, out
public ExcelDrawingBlipFill BlipFill { get; private set; }
public double? BorderWidth { get; set; }
public double[] BorderDashArray { get; set; }
+ public int StrokeMiterLimit { get; set; } = 4;
+ public eCompoundLineStyle CompoundLineStyle { get; set; } = eCompoundLineStyle.Single;
public double? BorderDashOffset { get; set; }
public eLineCap LineCap { get; set; } = eLineCap.Flat;
public SvgLineJoin LineJoin { get; set; } = SvgLineJoin.Miter;
@@ -67,7 +69,7 @@ internal virtual void GetBounds(out double il, out double it, out double ir, out
public PathFillMode BorderColorSource { get; set; } = PathFillMode.Norm;
public double? GlowRadius { get; private set; }
public string GlowColor { get; private set; }
-
+ public ExcelDrawingOuterShadowEffect OuterShadowEffect { get; private set; } = null;
protected void CloneBase(RenderItem item)
{
item.FillColor = FillColor;
@@ -82,6 +84,11 @@ protected void CloneBase(RenderItem item)
item.FillColorSource = FillColorSource;
}
+ internal void SetPatternFill()
+ {
+
+ }
+
internal virtual void SetDrawingPropertiesFill(ExcelDrawingFill fill, ExcelDrawingColorManager color)
{
switch (fill.Style)
@@ -100,12 +107,13 @@ internal virtual void SetDrawingPropertiesFill(ExcelDrawingFill fill, ExcelDrawi
}
internal virtual void SetDrawingPropertiesFill(ExcelDrawingFillBasic fill, ExcelDrawingColorManager color)
{
+ double? opacity=null;
switch (fill.Style)
{
case eFillStyle.NoFill:
if (fill.IsEmpty)
{
- FillColor = GetFillColor(fill, color, FillColorSource);
+ FillColor = GetFillColor(fill, color, FillColorSource, out opacity);
}
else
{
@@ -113,22 +121,27 @@ internal virtual void SetDrawingPropertiesFill(ExcelDrawingFillBasic fill, Excel
}
break;
case eFillStyle.SolidFill:
- FillColor = GetFillColor(fill, color, FillColorSource);
+ FillColor = GetFillColor(fill, color, FillColorSource, out opacity);
break;
case eFillStyle.GradientFill:
GradientFill = new DrawGradientFill(DrawingRenderer.Theme, fill.GradientFill);
FillColor = null;
break;
}
+ if (opacity.HasValue)
+ {
+ FillOpacity = opacity;
+ }
}
internal virtual void SetDrawingPropertiesBorder(ExcelDrawingBorder border, ExcelChartStyleColorManager color, bool hasBorder, double defaultWidth=1.5)
{
+ double? opacity = null;
switch (border.Fill.Style)
{
case eFillStyle.NoFill:
if(border.Fill.IsEmpty)
{
- BorderColor = GetFillColor(border.Fill, color, BorderColorSource);
+ BorderColor = GetFillColor(border.Fill, color, BorderColorSource, out opacity);
}
else
{
@@ -136,7 +149,7 @@ internal virtual void SetDrawingPropertiesBorder(ExcelDrawingBorder border, Exce
}
break;
case eFillStyle.SolidFill:
- BorderColor = GetFillColor(border.Fill, color, BorderColorSource);
+ BorderColor = GetFillColor(border.Fill, color, BorderColorSource, out opacity);
BorderGradientFill = null;
break;
case eFillStyle.GradientFill:
@@ -145,6 +158,11 @@ internal virtual void SetDrawingPropertiesBorder(ExcelDrawingBorder border, Exce
break;
}
+ if (opacity.HasValue)
+ {
+ BorderOpacity = opacity;
+ }
+
if (hasBorder && BorderColorSource != PathFillMode.None)
{
BorderWidth = border.Width == 0 ? defaultWidth : border.Width;
@@ -152,8 +170,9 @@ internal virtual void SetDrawingPropertiesBorder(ExcelDrawingBorder border, Exce
{
BorderDashArray = GetDashArray(border);
}
- if(border.CompoundLineStyle!=eCompundLineStyle.Single)
+ if(border.CompoundLineStyle!=eCompoundLineStyle.Single)
{
+ CompoundLineStyle = border.CompoundLineStyle;
//TODO:Add support double compound borders.
}
}
@@ -166,6 +185,10 @@ internal void SetDrawingPropertiesEffects(ExcelDrawingEffectStyle effect)
var gc = EPPlusColorConverter.GetThemeColor(DrawingRenderer.Theme, effect.Glow.Color);
GlowColor = "#" + gc.ToArgb().ToString("x8").Substring(2);
}
+ if(effect.HasOuterShadow)
+ {
+ OuterShadowEffect = effect.OuterShadow;
+ }
}
private double[] GetDashArray(ExcelDrawingBorder border)
@@ -197,8 +220,9 @@ private double[] GetDashArray(ExcelDrawingBorder border)
return null;
}
- private string GetFillColor(ExcelDrawingFillBasic fill, ExcelDrawingColorManager styleFillColor, PathFillMode fillColorSource)
+ private string GetFillColor(ExcelDrawingFillBasic fill, ExcelDrawingColorManager styleFillColor, PathFillMode fillColorSource, out double? opacity)
{
+ opacity = null;
if (fillColorSource == PathFillMode.None)
{
return "none";
@@ -226,14 +250,83 @@ private string GetFillColor(ExcelDrawingFillBasic fill, ExcelDrawingColorManager
}
fc = ColorUtils.GetAdjustedColor(fillColorSource, fc);
+ if(fc.A<255 && fc!=Color.Empty)
+ {
+ opacity = fc.A/255D;
+ }
return "#" + fc.ToArgb().ToString("x8").Substring(2);
}
- //internal void SetTheme(ExcelTheme theme)
- //{
- // theme = theme;
- //}
+ internal void GetOuterShadowColor(out string shadowColor, out double opacity)
+ {
+ if (OuterShadowEffect == null)
+ {
+ shadowColor = null;
+ opacity = 0;
+
+ }
+ else
+ {
+ var tc=EPPlusColorConverter.GetThemeColor(DrawingRenderer.Theme, OuterShadowEffect.Color);
+ if (tc.A < 255 && tc != Color.Empty)
+ {
+ opacity = tc.A / 255D;
+ }
+ else
+ {
+ opacity = 1;
+ }
+ shadowColor = "#" + tc.ToArgb().ToString("x8").Substring(2);
+ }
+ }
}
+
+ internal abstract class RenderItemIndependent : RenderItemBase
+ {
+ public string FillColor { get; set; }
+ public string FilterName { get; set; }
+ public SvgFillType FillType { get; set; }
+ public double? FillOpacity { get; set; }
+ public string BorderColor { get; set; }
+ public double? BorderWidth { get; set; }
+ public double[] BorderDashArray { get; set; }
+ public double? BorderDashOffset { get; set; }
+ public eLineCap LineCap { get; set; } = eLineCap.Flat;
+ public SvgLineJoin LineJoin { get; set; } = SvgLineJoin.Miter;
+ public double? BorderOpacity { get; set; }
+ public PathFillMode FillColorSource { get; set; } = PathFillMode.Norm;
+ public PathFillMode BorderColorSource { get; set; } = PathFillMode.Norm;
+ public double? GlowRadius { get; private set; }
+ public string GlowColor { get; private set; }
+
+ protected void CloneBase(RenderItem item)
+ {
+ item.FillColor = FillColor;
+ item.FillOpacity = FillOpacity;
+ item.BorderWidth = BorderWidth;
+ item.BorderColor = BorderColor;
+ item.BorderDashArray = BorderDashArray;
+ item.BorderDashOffset = BorderDashOffset;
+ item.BorderOpacity = BorderOpacity;
+ item.LineJoin = LineJoin;
+ item.LineCap = LineCap;
+ item.FillColorSource = FillColorSource;
+ }
+
+
+ internal string SetFillColor(Color color)
+ {
+ FillColor = GetAdjustedColor(color);
+ return FillColor;
+ }
+
+ private string GetAdjustedColor(Color color)
+ {
+ var fc = ColorUtils.GetAdjustedColor(FillColorSource, color);
+ return "#" + fc.ToArgb().ToString("x8").Substring(2);
+ }
+ }
+
///
/// Base class for any item rendered.
///
diff --git a/src/EPPlus.Export.ImageRenderer/RenderItems/RenderItemType.cs b/src/EPPlus.Export.ImageRenderer/RenderItems/RenderItemType.cs
index c770bfdc76..aa3412ab30 100644
--- a/src/EPPlus.Export.ImageRenderer/RenderItems/RenderItemType.cs
+++ b/src/EPPlus.Export.ImageRenderer/RenderItems/RenderItemType.cs
@@ -22,5 +22,7 @@ internal enum RenderItemType
Text = 5,
TSpan = 6,
Paragraph = 7,
+ CommentTitle = 8,
+ Reference = 9,
}
}
\ No newline at end of file
diff --git a/src/EPPlus.Export.ImageRenderer/RenderItems/Shared/ContainerItem.cs b/src/EPPlus.Export.ImageRenderer/RenderItems/Shared/ContainerItem.cs
new file mode 100644
index 0000000000..355a0df8bb
--- /dev/null
+++ b/src/EPPlus.Export.ImageRenderer/RenderItems/Shared/ContainerItem.cs
@@ -0,0 +1,141 @@
+using EPPlus.Graphics;
+using EPPlusImageRenderer;
+using EPPlusImageRenderer.RenderItems;
+using OfficeOpenXml.FormulaParsing.Excel.Functions.Text;
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+
+namespace EPPlus.Export.ImageRenderer.RenderItems.Shared
+{
+ internal abstract class ContainerItem : RenderItem
+ {
+ internal RenderItem InnerItem { get; private set; }
+ internal RenderItem OuterItem { get; private set; }
+
+ double _marginLeft;
+ double _marginTop;
+
+ internal double MarginLeft
+ {
+ get
+ {
+ return _marginLeft;
+ }
+ set
+ {
+ VerifyMarginInput(value);
+ _marginLeft = value;
+ //InnerItem.Bounds.Left = value;
+ }
+ }
+
+ internal double MarginTop
+ {
+ get
+ {
+ return _marginTop;
+ }
+ set
+ {
+ VerifyMarginInput(value);
+ _marginTop = value;
+ //InnerItem.Bounds.Top = value;
+ }
+ }
+
+ double _marginRight;
+
+ internal double MarginRight
+ {
+ get
+ {
+ return _marginRight;
+ }
+ set
+ {
+ VerifyMarginInput(value);
+ _marginRight = value;
+ //InnerItem.Bounds.Width = Bounds.Width - value;
+ }
+ }
+
+ double _marginBottom;
+
+ internal double MarginBottom
+ {
+ get
+ {
+ return _marginBottom;
+ }
+ set
+ {
+ VerifyMarginInput(value);
+ _marginBottom = value;
+ }
+ }
+
+ bool SizeToContents = true;
+ public ContainerItem(RenderItem innerItem, RenderItem outerItem) : base(innerItem.DrawingRenderer)
+ {
+ OuterItem = outerItem;
+ InnerItem = innerItem;
+
+ OuterItem.Bounds.Parent = Bounds;
+ InnerItem.Bounds.Parent = Bounds;
+ }
+
+ internal void ApplyMargins()
+ {
+ //Origin-Point: Set. Set in stone. All text moves from there
+ //Here it is Bounds.Top and Bounds.Left. Nothing in the content can change that
+
+ OuterItem.Bounds.Width = InnerItem.Bounds.Width + MarginRight + MarginLeft;
+ OuterItem.Bounds.Height = InnerItem.Bounds.Height + MarginBottom + MarginTop;
+
+ Bounds.Width = OuterItem.Bounds.Width;
+ Bounds.Height = OuterItem.Bounds.Height;
+ }
+
+ public override RenderItemType Type => RenderItemType.Group;
+
+ internal double GetInnerLeft()
+ {
+ return Bounds.Left + MarginLeft;
+ }
+
+ internal double GetInnerTop()
+ {
+ return Bounds.Top + MarginTop;
+ }
+
+ internal double GetInnerBottom()
+ {
+ return Bounds.Bottom - MarginBottom;
+ }
+
+ internal double GetInnerRight()
+ {
+ return Bounds.Right - MarginRight;
+ }
+
+ public double GetInnerWidth()
+ {
+ return GetInnerRight() - GetInnerLeft();
+ }
+
+ public double GetInnerHeight()
+ {
+ return GetInnerBottom() - GetInnerTop();
+ }
+
+ private void VerifyMarginInput(double value)
+ {
+ if (value < 0)
+ {
+ throw new ArgumentException("Margins cannot be set to a negative value! If you wish to set starting position, please set Left or Top for the ContainerItem");
+ }
+ }
+ }
+}
diff --git a/src/EPPlus.Export.ImageRenderer/RenderItems/Shared/FlexBaseContainer.cs b/src/EPPlus.Export.ImageRenderer/RenderItems/Shared/FlexBaseContainer.cs
new file mode 100644
index 0000000000..532679f706
--- /dev/null
+++ b/src/EPPlus.Export.ImageRenderer/RenderItems/Shared/FlexBaseContainer.cs
@@ -0,0 +1,18 @@
+using EPPlusImageRenderer.RenderItems;
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+
+namespace EPPlus.Export.ImageRenderer.RenderItems.Shared
+{
+ //internal class FlexBaseContainer : RenderItem
+ //{
+ // public override RenderItemType Type => throw new NotImplementedException();
+
+ // public override void Render(StringBuilder sb)
+ // {
+ // throw new NotImplementedException();
+ // }
+ //}
+}
diff --git a/src/EPPlus.Export.ImageRenderer/RenderItems/Shared/GroupItem.cs b/src/EPPlus.Export.ImageRenderer/RenderItems/Shared/GroupItem.cs
new file mode 100644
index 0000000000..99b17038a9
--- /dev/null
+++ b/src/EPPlus.Export.ImageRenderer/RenderItems/Shared/GroupItem.cs
@@ -0,0 +1,88 @@
+using EPPlus.Graphics;
+using EPPlusImageRenderer;
+using EPPlusImageRenderer.RenderItems;
+using System.Collections.Generic;
+
+namespace EPPlus.Export.ImageRenderer.RenderItems.Shared
+{
+ internal abstract class GroupItem : RenderItem
+ {
+ ///
+ /// In degrees
+ ///
+ internal double Rotation = double.NaN;
+ ///
+ /// In points
+ ///
+ internal Point Position = null;
+
+ Point _altRotationPoint = null;
+
+ internal Point RotationPoint
+ {
+ get
+ {
+ if (_altRotationPoint == null)
+ {
+ return Position;
+ }
+ return _altRotationPoint;
+ }
+ set
+ {
+ _altRotationPoint = value;
+ }
+ }
+
+
+ //Transform _rotationPoint;
+
+ ///
+ /// Items contained in this group
+ ///
+ internal protected List _childItems = new List();
+
+ public GroupItem(DrawingBase renderer) : base(renderer)
+ {
+ Bounds.Parent = Position;
+ //_rotationPoint = Bounds;
+ }
+
+ public GroupItem(DrawingBase renderer, double localXPos, double localYPos) : this(renderer)
+ {
+ Position = new Point(localXPos, localYPos);
+ }
+
+
+ public GroupItem(DrawingBase renderer, BoundingBox parent, double rotation, Transform rotationPoint = null) : this(renderer, 0, 0)
+ {
+ Position.Parent = parent;
+ Rotation = rotation;
+ if (rotationPoint != null)
+ {
+ RotationPoint = new Point(rotationPoint.LocalPosition.X, rotationPoint.LocalPosition.Y);
+ }
+ }
+
+ internal void SetRotationPointToCenterOfGroup(double rotation = double.NaN)
+ {
+ RotationPoint = new Point(Bounds.Width/2, Bounds.Height/2);
+
+ if (double.IsNaN(rotation) == false)
+ {
+ Rotation = rotation;
+ }
+ }
+
+ internal void AddChildItem(RenderItem item)
+ {
+ item.Bounds.Parent = Position;
+ _childItems.Add(item);
+
+ Bounds.Width = item.Bounds.Right > Bounds.Width ? item.Bounds.Right : Bounds.Width;
+ Bounds.Height = item.Bounds.Bottom > Bounds.Height ? item.Bounds.Bottom : Bounds.Height;
+ }
+
+ public override RenderItemType Type => RenderItemType.Group;
+ }
+}
diff --git a/src/EPPlus.Export.ImageRenderer/RenderItems/Shared/ParagraphItem.cs b/src/EPPlus.Export.ImageRenderer/RenderItems/Shared/ParagraphItem.cs
index 04ccbb0a0a..ebb2fb2f60 100644
--- a/src/EPPlus.Export.ImageRenderer/RenderItems/Shared/ParagraphItem.cs
+++ b/src/EPPlus.Export.ImageRenderer/RenderItems/Shared/ParagraphItem.cs
@@ -1,6 +1,8 @@
using EPPlus.Fonts.OpenType;
using EPPlus.Fonts.OpenType.Integration;
using EPPlus.Fonts.OpenType.TextShaping;
+using EPPlus.Fonts.OpenType.Integration;
+using EPPlus.Fonts.OpenType.TextShaping;
using EPPlus.Fonts.OpenType.TrueTypeMeasurer.DataHolders;
using EPPlus.Fonts.OpenType.Utils;
using EPPlus.Graphics;
@@ -25,7 +27,7 @@ internal abstract class ParagraphItem : RenderItem
{
ITextMeasurerWrap _measurer;
- double _leftMargin;
+ double _leftMargin;
double _rightMargin;
eDrawingTextLineSpacing _lsType;
@@ -40,32 +42,42 @@ internal abstract class ParagraphItem : RenderItem
internal protected MeasurementFont _paragraphFont;
internal TextBodyItem ParentTextBody { get; set; }
internal double ParagraphLineSpacing { get; private set; }
- internal eTextAlignment HorizontalAlignment { get; private set; } = eTextAlignment.Left;
+ internal eTextAlignment HorizontalAlignment { get; private set; }
internal List Runs { get; set; } = new List();
+ internal bool DisplayBounds { get; set; } = false;
+
+ private double? _centerAdjustment = null;
+
public ParagraphItem(TextBodyItem textBody, DrawingBase renderer, BoundingBox parent) : base(renderer, parent)
{
ParentTextBody = textBody;
Bounds.Name = "Paragraph";
var defaultFont = new MeasurementFont { FontFamily = "Aptos Narrow", Size = 11, Style = MeasurementFontStyles.Regular };
_paragraphFont = defaultFont;
+
+ _measurer = new FontMeasurerTrueType(defaultFont);
+ ParagraphLineSpacing = GetParagraphLineSpacingInPoints(100, _measurer);
}
- public ParagraphItem(TextBodyItem textBody, DrawingBase renderer, BoundingBox parent, ExcelDrawingParagraph p, string textIfEmpty=null) : base(renderer, parent)
+ public ParagraphItem(TextBodyItem textBody, DrawingBase renderer, BoundingBox parent, ExcelDrawingParagraph p, string textIfEmpty = null) : base(renderer, parent)
{
- ParentTextBody = textBody;
+ ParentTextBody = textBody;
IsFirstParagraph = p == p._paragraphs[0];
if (p.DefaultRunProperties.Fill != null && p.DefaultRunProperties.Fill.IsEmpty == false)
{
- if(IsFirstParagraph)
+ if (IsFirstParagraph)
{
- SetDrawingPropertiesFill(p.DefaultRunProperties.Fill, null);
+ if (p.DefaultRunProperties.Fill != null)
+ {
+ SetDrawingPropertiesFill(p.DefaultRunProperties.Fill, null);
+ }
}
else
{
//Drawingproperties has fallback to firstDefault but excel does not display it so we should not either.
- if(p.DefaultRunProperties != p._paragraphs.FirstDefaultRunProperties)
+ if (p.DefaultRunProperties != p._paragraphs.FirstDefaultRunProperties)
{
SetDrawingPropertiesFill(p.DefaultRunProperties.Fill, null);
}
@@ -79,6 +91,14 @@ public ParagraphItem(TextBodyItem textBody, DrawingBase renderer, BoundingBox pa
}
}
}
+ else
+ {
+ if (p._paragraphs.FirstDefaultRunProperties != null && p._paragraphs.FirstDefaultRunProperties.Fill != null && p._paragraphs.FirstDefaultRunProperties.Fill.IsEmpty == false)
+ {
+ var fill = p._paragraphs.FirstDefaultRunProperties.Fill;
+ SetDrawingPropertiesFill(fill, null);
+ }
+ }
//---Initialize Bounds / Margins-- -
Bounds.Name = "Paragraph";
@@ -90,16 +110,21 @@ public ParagraphItem(TextBodyItem textBody, DrawingBase renderer, BoundingBox pa
_leftMargin = _leftMargin.PixelToPoint();
_rightMargin = _rightMargin.PixelToPoint();
+ HorizontalAlignment = p.HorizontalAlignment;
+ _leftMargin = _leftMargin.PixelToPoint();
+ _rightMargin = _rightMargin.PixelToPoint();
+
HorizontalAlignment = p.HorizontalAlignment;
if (ParentTextBody.AutoSize == false)
{
- //Bounds.Width = ParentTextBody.Width;
- //var globBounds = Bounds.GetGlobalBoundingbox();
- //Bounds.Width = parent.Width;
- //Bounds.Left = GetAlignmentHorizontal(HorizontalAlignment);
- //Bounds.Width = parent.Width - _rightMargin - _leftMargin;
- Bounds.Width = parent.Width;
+ Bounds.Left = 0;
+ Bounds.Width = ParentTextBody.MaxWidth;
+
+ //Left is equal to left Paragraph margin
+ //Textbody or Textbox are assumed to handle shape/chart margins
+ //Paragraph handles only indentations/margins that is applied ON TOP of those margins
+ //Paragraph left is the exact position where the text itself starts on the left
if (HorizontalAlignment != eTextAlignment.Center)
{
Bounds.Left = GetAlignmentHorizontal(HorizontalAlignment);
@@ -109,6 +134,8 @@ public ParagraphItem(TextBodyItem textBody, DrawingBase renderer, BoundingBox pa
//Center is a bit strange the bounds really are the same as left or right aligned
//It doesn't truly matter as only left min and right max play a role
Bounds.Left = GetAlignmentHorizontal(eTextAlignment.Left);
+ _centerAdjustment = GetAlignmentHorizontal(HorizontalAlignment);
+
}
Bounds.Width = parent.Width - _rightMargin - _leftMargin;
}
@@ -136,8 +163,10 @@ private double GetParagraphLineSpacingInPoints(double spacingValue, ITextMeasure
if (IsFirstParagraph)
{
_lineSpacingAscendantOnly = spacingValue;
+ _lineSpacingAscendantOnly = spacingValue;
}
return spacingValue;
+ return spacingValue;
}
else
{
@@ -146,8 +175,10 @@ private double GetParagraphLineSpacingInPoints(double spacingValue, ITextMeasure
if (IsFirstParagraph)
{
_lineSpacingAscendantOnly = multiplier * fmExact.GetBaseLine();
+ _lineSpacingAscendantOnly = multiplier * fmExact.GetBaseLine();
}
return multiplier * fmExact.GetSingleLineSpacing();
+ return multiplier * fmExact.GetSingleLineSpacing();
}
}
@@ -160,6 +191,14 @@ internal protected TextRunItem AddRenderItemTextRun(ExcelParagraphTextRunBase or
return targetTxtRun;
}
+ public void AddText(string text, double prevWidth)
+ {
+ var container = CreateTextRun(_paragraphFont, Bounds, text);
+ Runs.Add(container);
+
+ container.Bounds.Name = $"Container{Runs.Count}";
+ container.Bounds.Left = prevWidth;
+ }
public void AddText(string text, ExcelTextFont font, bool isOld)
{
var measurer = new FontMeasurerTrueType();
@@ -173,7 +212,7 @@ public void AddText(string text, ExcelTextFont font, bool isOld)
}
- public void AddText(string text, ExcelTextFont font)
+ public void AddText(string text, ExcelTextFont font, double prevWidth)
{
var measurer = new FontMeasurerTrueType();
//var displayText = measurer.MeasureAndWrapTextLines(text, font.GetMeasureFont(), ParentTextBody.MaxWidth);
@@ -184,6 +223,18 @@ public void AddText(string text, ExcelTextFont font)
Runs.Add(container);
//Bounds.Width = container.Bounds.Width + 0.001; //TODO: fix for equal width issue
container.Bounds.Name = $"Container{Runs.Count}";
+ container.Bounds.Left = prevWidth;
+ }
+
+ void GenerateTextFragments(string text)
+ {
+ _newTextFragments = new List();
+
+ if (string.IsNullOrEmpty(text) == false)
+ {
+ var currentFrag = new TextFragment() { Text = text, Font = _paragraphFont};
+ _newTextFragments.Add(currentFrag);
+ }
}
///
@@ -202,20 +253,115 @@ void GenerateTextFragments(ExcelDrawingTextRunCollection runs)
var txtRun = runs[i];
var runFont = txtRun.GetMeasurementFont();
+ fonts.Add(runFont);
fonts.Add(runFont);
runContents.Add(txtRun.Text);
fontSizes.Add(runFont.Size);
}
- _textFragments = new TextFragmentCollection(runContents, fontSizes);
+ //_textFragments = new TextFragmentCollection(runContents, fontSizes);
_newTextFragments = new List();
for (int i = 0; i < runContents.Count(); i++)
{
- var currentFrag = new TextFragment() { Text = runContents[i], Font = fonts[i] };
- _newTextFragments.Add(currentFrag);
+ if (string.IsNullOrEmpty(runContents[i]) == false)
+ {
+ var currentFrag = new TextFragment() { Text = runContents[i], Font = fonts[i] };
+ _newTextFragments.Add(currentFrag);
+ }
+ }
+ }
+
+ internal void AddLinesAndTextRuns(string textIfEmpty)
+ {
+ GenerateTextFragments(textIfEmpty);
+ var lines = new List();
+
+ var measurer = new FontMeasurerTrueType();
+ var maxWidth = ParentTextBody.MaxWidth + 0.001; //TODO: fix for equal width issue;
+ lines = measurer.MeasureAndWrapTextLines_New(textIfEmpty, _paragraphFont, maxWidth);
+ bool lineSpacingIsExact = _lsMultiplier.HasValue == false;
+ double runLineSpacing = 0;
+ double greatestWidth = 0;
+ //In points
+ double lastDescent = 0;
+
+ if (lines != null && lines.Count != 0)
+ {
+ //This could be moved into a textLines collection class
+ //START
+ var idxOfLargestLine = 0;
+ double widthOfLargestLine = lines[0].Width;
+
+ for (int i = 1; i < lines.Count; i++)
+ {
+ if (lines[i].Width > widthOfLargestLine)
+ {
+ var ctrLineWidth = lines[i].GetWidthWithoutTrailingSpaces();
+ widthOfLargestLine = ctrLineWidth;
+ idxOfLargestLine = i;
+ }
+ }
+ //END
+
+ if (HorizontalAlignment == eTextAlignment.Center && ParentTextBody.AutoSize)
+ {
+ //Bounds of the paragraph should be bounds of the text itself.
+ //Therefore we must know the starting point to set accurate left and offset from left.
+ Bounds.Left = _centerAdjustment.Value - (widthOfLargestLine / 2);
+ }
+
+ foreach (var line in lines)
+ {
+ double prevWidth = 0;
+
+ if (HorizontalAlignment == eTextAlignment.Center)
+ {
+ var ctrLineWidth = line.GetWidthWithoutTrailingSpaces();
+ prevWidth = (widthOfLargestLine - ctrLineWidth) / 2;
+ }
+ else if (HorizontalAlignment == eTextAlignment.Right)
+ {
+ //Note that the actual bounds with the space will be outside max bounds.
+ //This appears to be how excel does it
+ var ctrLineWidth = line.GetWidthWithoutTrailingSpaces();
+ prevWidth = widthOfLargestLine - ctrLineWidth;
+ }
+
+ if (lineSpacingIsExact == false)
+ {
+ runLineSpacing += line.LargestAscent + lastDescent;
+ }
+ else
+ {
+ runLineSpacing += ParagraphLineSpacing;
+ }
+ if (line.GetWidthWithoutTrailingSpaces() > greatestWidth)
+ {
+ greatestWidth = line.GetWidthWithoutTrailingSpaces();
+ }
+
+ foreach (var lineFragment in line.LineFragments)
+ {
+ var displayText = line.GetLineFragmentText(lineFragment);
+
+ if (string.IsNullOrEmpty(textIfEmpty) == false)
+ {
+ AddText(displayText, prevWidth);
+ }
+
+ TextRunItem runItem = Runs.Last();
+ runItem.YPosition = runLineSpacing;
+
+ runItem.Bounds.Width = lineFragment.Width;
+ prevWidth += lineFragment.Width;
+ }
+ lastDescent = line.LargestDescent;
+ }
}
+ Bounds.Height = runLineSpacing + lastDescent;
+ Bounds.Width = greatestWidth;
}
private void AddLinesAndTextRuns(ExcelDrawingParagraph p, string textIfEmpty)
@@ -234,109 +380,119 @@ private void AddLinesAndTextRuns(ExcelDrawingParagraph p, string textIfEmpty)
{
var measurer = new FontMeasurerTrueType();
var maxWidth = ParentTextBody.MaxWidth + 0.001; //TODO: fix for equal width issue;
- lines = measurer.MeasureAndWrapTextLines(textIfEmpty, p.DefaultRunProperties.GetMeasureFont(), maxWidth);
+ lines = measurer.MeasureAndWrapTextLines_New(textIfEmpty, p.DefaultRunProperties.GetMeasureFont(), maxWidth);
+
+ //Bounds.Width = maxWidth;
+ if (HorizontalAlignment != eTextAlignment.Center)
+ {
+ Bounds.Left = GetAlignmentHorizontal(HorizontalAlignment);
+ }
+ else
+ {
+ Bounds.Left = GetAlignmentHorizontal(eTextAlignment.Left);
+ _centerAdjustment = GetAlignmentHorizontal(HorizontalAlignment);
+ }
}
else
{
- lines = WrapToSimpleTextLines(p, _textFragments);
+ lines = WrapToSimpleTextLines(p);
}
- foreach (var line in lines)
+
+ if (lines != null && lines.Count != 0)
{
- double prevWidth = 0;
+ //This could be moved into a textLines collection class
+ //START
+ var idxOfLargestLine = 0;
+ double widthOfLargestLine = lines[0].GetWidthWithoutTrailingSpaces();
- if (HorizontalAlignment == eTextAlignment.Center)
+ for (int i = 1; i < lines.Count; i++)
{
- //Center the line within context
- prevWidth = (Bounds.Width - line.Width)/2;
+ if (lines[i].Width > widthOfLargestLine)
+ {
+ var ctrLineWidth = lines[i].GetWidthWithoutTrailingSpaces();
+ widthOfLargestLine = ctrLineWidth;
+ idxOfLargestLine = i;
+ }
}
+ //END
-
- if (lineSpacingIsExact == false)
+ if (ParentTextBody.AutoSize)
{
- runLineSpacing += line.LargestAscent + lastDescent;
- }
- else
- {
- runLineSpacing += ParagraphLineSpacing;
- }
- if (line.Width > greatestWidth)
- {
- greatestWidth = line.Width;
+ //Bounds of the paragraph should be bounds of the text itself.
+ //Therefore we must know the starting point to set accurate left and offset from left.
+ Bounds.Left = 0;
}
- if(line.RtFragments.Count == 0 && line.LineFragments.Count > 0)
- {
- foreach (var lineFragment in line.LineFragments)
+ foreach (var line in lines)
{
- var displayText = line.GetLineFragmentText(lineFragment);
+ double prevWidth = 0;
- if (p.TextRuns.Count == 0 && string.IsNullOrEmpty(textIfEmpty) == false)
+ if (HorizontalAlignment == eTextAlignment.Center)
{
- AddText(displayText, p.DefaultRunProperties);
+ var ctrLineWidth = line.GetWidthWithoutTrailingSpaces();
+ //Calculate difference in widths and split to get offset between leftmost position and current line
+ prevWidth = (widthOfLargestLine - ctrLineWidth) / 2;
}
- else
+ else if (HorizontalAlignment == eTextAlignment.Right)
{
- AddRenderItemTextRun(p.TextRuns[lineFragment.RtFragIdx], displayText, prevWidth);
+ //Note that the actual bounds with the space will be outside max bounds.
+ //This appears to be how excel does it
+ var ctrLineWidth = line.GetWidthWithoutTrailingSpaces();
+ prevWidth = widthOfLargestLine - ctrLineWidth;
}
- TextRunItem runItem = Runs.Last();
- runItem.YPosition = runLineSpacing;
-
- runItem.Bounds.Width = lineFragment.Width;
- prevWidth += lineFragment.Width;
- }
- }
- else
- {
- foreach (var rtFragment in line.RtFragments)
- {
- var displayText = line.GetFragmentText(rtFragment);
-
- if (p.TextRuns.Count == 0 && string.IsNullOrEmpty(textIfEmpty) == false)
+ if (lineSpacingIsExact == false)
{
- AddText(displayText, p.DefaultRunProperties);
+ runLineSpacing += line.LargestAscent + lastDescent;
}
else
{
- AddRenderItemTextRun(p.TextRuns[rtFragment.Fragidx], displayText, prevWidth);
+ runLineSpacing += ParagraphLineSpacing;
+ }
+ if (line.GetWidthWithoutTrailingSpaces() > greatestWidth)
+ {
+ greatestWidth = line.GetWidthWithoutTrailingSpaces();
}
- TextRunItem runItem = Runs.Last();
- runItem.YPosition = runLineSpacing;
-
- runItem.Bounds.Width = rtFragment.Width;
- prevWidth += rtFragment.Width;
+ foreach (var lineFragment in line.LineFragments)
+ {
+ var displayText = line.GetLineFragmentText(lineFragment);
+
+ if (p.TextRuns.Count == 0 && string.IsNullOrEmpty(textIfEmpty) == false)
+ {
+ AddText(displayText, p.DefaultRunProperties, prevWidth);
+ }
+ else
+ {
+ AddRenderItemTextRun(p.TextRuns[lineFragment.RtFragIdx], displayText, prevWidth);
+ }
+
+ TextRunItem runItem = Runs.Last();
+ runItem.YPosition = runLineSpacing;
+
+ runItem.Bounds.Width = lineFragment.Width;
+ prevWidth += lineFragment.Width;
+ }
+ lastDescent = line.LargestDescent;
}
- }
-
- lastDescent = line.LargestDescent;
}
Bounds.Height = runLineSpacing + lastDescent;
Bounds.Width = greatestWidth;
+ Bounds.Width = greatestWidth;
}
- List WrapToSimpleTextLines(ExcelDrawingParagraph p, TextFragmentCollection fragments)
+ List WrapToSimpleTextLines(ExcelDrawingParagraph p)
{
var ttMeasurer = (FontMeasurerTrueType)_measurer;
- ttMeasurer.SetFont(_newTextFragments[0].Font);
- var maxWidthPoints = Math.Round(ParentTextBody.MaxWidth, 0, MidpointRounding.AwayFromZero);
- return ttMeasurer.WrapMultipleTextFragmentsToTextLines_New(_newTextFragments, maxWidthPoints);
- //List fonts = new List();
-
- //for (int i = 0; i < p.TextRuns.Count(); i++)
- //{
- // var txtRun = p.TextRuns[i];
- // var runFont = txtRun.GetMeasurementFont();
- // fonts.Add(runFont);
- //}
- //var layout = TextData.GetTextLayoutEngine(fonts[0]);
- //var shaper = new TextShaper( fonts[0]);
- //var layout = new TextLayoutEngine(shaper);
-
- //var wrappedLines = layout.WrapRichTextLines(fragments, maxSizePoints);
- //return ttMeasurer.WrapMultipleTextFragmentsToTextLines(fragments, fonts, maxWidthPoints);
+ if (_newTextFragments.Count > 0)
+ {
+ ttMeasurer.SetFont(_newTextFragments[0].Font);
+ var maxWidthPoints = Math.Round(ParentTextBody.MaxWidth, 0, MidpointRounding.AwayFromZero);
+ return ttMeasurer.WrapMultipleTextFragmentsToTextLines_New(_newTextFragments, maxWidthPoints);
+ }
+ return new List();
}
internal double GetAlignmentHorizontal(eTextAlignment txAlignment)
@@ -357,7 +513,7 @@ internal double GetAlignmentHorizontal(eTextAlignment txAlignment)
break;
}
- return TextUtils.RoundToWhole(x);
+ return x;
}
///
@@ -369,5 +525,6 @@ internal double GetAlignmentHorizontal(eTextAlignment txAlignment)
///
internal abstract TextRunItem CreateTextRun(ExcelParagraphTextRunBase run, BoundingBox parent, string displayText);
internal abstract TextRunItem CreateTextRun(string text, ExcelTextFont font, BoundingBox parent, string displayText);
+ internal abstract TextRunItem CreateTextRun(MeasurementFont font, BoundingBox parent, string displayText);
}
}
diff --git a/src/EPPlus.Export.ImageRenderer/RenderItems/Shared/TextBodyItem.cs b/src/EPPlus.Export.ImageRenderer/RenderItems/Shared/TextBodyItem.cs
index 9ce4f69e11..237800b594 100644
--- a/src/EPPlus.Export.ImageRenderer/RenderItems/Shared/TextBodyItem.cs
+++ b/src/EPPlus.Export.ImageRenderer/RenderItems/Shared/TextBodyItem.cs
@@ -60,8 +60,8 @@ public TextBodyItem(DrawingBase renderer, BoundingBox parent, double maxWidth, d
internal bool WrapText = true;
- private FontMeasurerTrueType _measurer = null;
internal string _text;
+
public void ImportParagraph(ExcelDrawingParagraph item, double startingY, string text=null)
{
var measureFont = item.DefaultRunProperties.GetMeasureFont();
@@ -89,13 +89,38 @@ public void ImportParagraph(ExcelDrawingParagraph item, double startingY, string
}
}
Paragraphs.Add(paragraph);
- SetHorizontalAlignmentPosition();
}
- private void SetHorizontalAlignmentPosition()
+ public void AddParagraph(double startingY, string text = null)
{
+ var paragraph = CreateParagraph(this, Bounds, text);
+ paragraph.Bounds.Name = $"Container{Paragraphs.Count}";
+ paragraph.Bounds.Top = startingY;
+ _text = text;
+
if (AutoSize)
{
+ if (Paragraphs.Count == 0)
+ {
+ Bounds.Height = paragraph.Bounds.Height;
+ }
+ else
+ {
+ Bounds.Height += paragraph.Bounds.Height;
+ }
+
+ if (Bounds.Width < paragraph.Bounds.Width || (Bounds.Width == MaxWidth && Paragraphs.Count == 0))
+ {
+ Bounds.Width = paragraph.Bounds.Width;
+ }
+ }
+ Paragraphs.Add(paragraph);
+ }
+
+ internal void SetHorizontalAlignmentPosition()
+ {
+ //if (AutoSize)
+ //{
foreach (var p in Paragraphs)
{
switch (p.HorizontalAlignment)
@@ -104,7 +129,7 @@ private void SetHorizontalAlignmentPosition()
p.Bounds.Left = 0;
break;
case eTextAlignment.Center:
- p.Bounds.Left = Bounds.Width / 2 - p.Bounds.Width / 2;
+ p.Bounds.Left = (Bounds.Width / 2) - (p.Bounds.Width / 2);
break;
case eTextAlignment.Right:
p.Bounds.Left = Bounds.Right - p.Bounds.Width;
@@ -117,61 +142,37 @@ private void SetHorizontalAlignmentPosition()
break;
}
}
- }
+ //}
}
- internal virtual void ImportTextBody(ExcelTextBody body)
+ internal virtual void ImportTextBody(ExcelTextBody body, ExcelHorizontalAlignment horizontalDefault = ExcelHorizontalAlignment.Left)
{
_text = null;
VerticalAlignment = body.Anchor;
+
//We already apply bounds top via the parent Transform
- double paragraphStartY = GetAlignmentVertical();
+ double paragraphStartY = 0;
+ double largestWidth = double.MinValue;
foreach (var paragraph in body.Paragraphs)
{
- //if (paragraph == body.Paragraphs[0])
- //{
- // //For the first line we always add ascent for Top-aligned but this should not be done for center vertical align
- // //However as paragraph and textRun should not have to deal with vertical align directly
- // //We wish to achieve the effect of not applying dy to the first textrun while not chaning anything
- // //thus: we change paragraphStartY to get around it. Should probably be solved in the textrun somehow
- // if (VerticalAlignment == eTextAnchoringType.Center && paragraph.TextRuns != null && paragraph.TextRuns.Count > 0)
- // {
- // var _measurer = paragraph._prd.Package.Settings.TextSettings.GenericTextMeasurerTrueType;
- // var spacingValue = GetParagraphAscendantSpacingInPixels(
- // paragraph.LineSpacing.LineSpacingType, paragraph.LineSpacing.Value, _measurer, out double multiplier);
-
- // if(multiplier == -1)
- // {
- // paragraphStartY -= spacingValue;
- // }
- // else
- // {
- // var runFont = paragraph.TextRuns[0].GetMeasurementFont();
- // var startFont = paragraph.DefaultRunProperties.GetMeasureFont();
- // _measurer.SetFont(runFont);
-
- // paragraphStartY -= multiplier * _measurer.GetBaseLine().PointToPixel(true);
-
- // //Reset measurer font
- // _measurer.SetFont(startFont);
- // }
-
-
- // //paragraph.TextRuns[0].GetMeasurementFont() *
- // ////var baseLineSize = paragraph.TextRuns[0].FontSize.PointToPixel();
- // ////paragraphStartY = paragraphStartY - baseLineSize;
- // }
- //}
-
ImportParagraph(paragraph, paragraphStartY);
var addedPara = Paragraphs.Last();
paragraphStartY = addedPara.Bounds.Bottom;
+ largestWidth = Math.Max(largestWidth, addedPara.Bounds.Width);
+ }
+
+ foreach (var paragraph in body.Paragraphs)
+ {
+ SetHorizontalAlignmentPosition();
}
+
if (Paragraphs != null && Paragraphs.Count() > 0)
{
Bounds.Height = paragraphStartY;
}
+
+ Bounds.Top = GetAlignmentVertical();
}
private double GetParagraphAscendantSpacingInPixels(eDrawingTextLineSpacing lineSpacingType, double spacingValue, ITextMeasurerWrap fmExact, out double multiplier)
{
@@ -187,6 +188,7 @@ private double GetParagraphAscendantSpacingInPixels(eDrawingTextLineSpacing line
}
}
+
//public void AddText(string text, FontMeasurerTrueType measurer)
//{
// if (Paragraphs.Count == 0)
@@ -216,10 +218,10 @@ private double GetParagraphAscendantSpacingInPixels(eDrawingTextLineSpacing line
// Paragraphs.Add(paragraph);
//}
- public void SetMeasurer(FontMeasurerTrueType fontMeasurer)
- {
- _measurer = fontMeasurer;
- }
+ //public void SetMeasurer(FontMeasurerTrueType fontMeasurer)
+ //{
+ // _measurer = fontMeasurer;
+ //}
double? _alignmentY = null;
@@ -235,18 +237,16 @@ private double GetAlignmentVertical()
switch (VerticalAlignment)
{
case eTextAnchoringType.Top:
- alignmentY = 0;
+ alignmentY = Bounds.Top;
break;
//Center means center of a Shape's ENTIRE bounding box height.
//Not center of the Inset GetRectangle
+ //Not center of the Inset GetRectangle
case eTextAnchoringType.Center:
- var globalHeight = (DrawingRenderer.Bounds.Height / 2) + Bounds.Top+2;
- var adjustedHeight = globalHeight - Bounds.Position.Y; //Global position.
-
- alignmentY = adjustedHeight;
+ alignmentY = (MaxHeight - Bounds.Height)/2 + Bounds.Top;
break;
case eTextAnchoringType.Bottom:
- alignmentY = Bounds.Height;
+ alignmentY = MaxHeight - Bounds.Height;
break;
}
@@ -262,6 +262,8 @@ private double GetAlignmentVertical()
///
internal abstract ParagraphItem CreateParagraph(TextBodyItem textBody, ExcelDrawingParagraph paragraph, BoundingBox parent, string textIfEmpty="");
+ internal abstract ParagraphItem CreateParagraph(TextBodyItem textBody, BoundingBox parent, string textIfEmpty = "");
+
///
/// Each file format defines its own paragraph
///
diff --git a/src/EPPlus.Export.ImageRenderer/RenderItems/Shared/TextRunItem.cs b/src/EPPlus.Export.ImageRenderer/RenderItems/Shared/TextRunItem.cs
index 0f8c8b8724..e4f0dd09fd 100644
--- a/src/EPPlus.Export.ImageRenderer/RenderItems/Shared/TextRunItem.cs
+++ b/src/EPPlus.Export.ImageRenderer/RenderItems/Shared/TextRunItem.cs
@@ -4,16 +4,12 @@
using EPPlusImageRenderer;
using EPPlusImageRenderer.RenderItems;
using OfficeOpenXml.Drawing;
-using OfficeOpenXml.Drawing.Theme;
-using OfficeOpenXml.FormulaParsing.Excel.Functions.MathFunctions;
using OfficeOpenXml.Interfaces.Drawing.Text;
using OfficeOpenXml.Style;
using OfficeOpenXml.Utils;
-using System;
using System.Collections.Generic;
using System.Drawing;
using System.Linq;
-using System.Text;
using System.Text.RegularExpressions;
namespace EPPlus.Export.ImageRenderer.RenderItems.Shared
{
@@ -26,18 +22,11 @@ internal abstract class TextRunItem : RenderItem
internal protected MeasurementFont _measurementFont;
internal protected bool _isFirstInParagraph;
- FontMeasurerTrueType _measurer;
- eTextAlignment _horizontalTextAlignment;
- MeasurementFontStyles _fontStyles;
+
internal double FontSizeInPixels { get; private set; }
public List Lines { get; private set; }
- ///
- /// Aka total Delta Y
- ///
- double _yEndPos;
-
protected internal bool _isItalic = false;
protected internal bool _isBold = false;
protected internal eUnderLineType _underLineType;
@@ -45,13 +34,45 @@ internal abstract class TextRunItem : RenderItem
protected internal Color _underlineColor;
internal double YPosition { get; set; }
- //internal double LineSpacingPerNewLine { get; set; }
- //internal double BaseLineSpacing { get; set; }
-
- //internal List YIncreasePerLine { get; private set; } = new List();
- //internal List PerLineWidth { get; private set; } = new List();
internal double ClippingHeight = double.NaN;
+ internal TextRunItem(DrawingBase renderer, BoundingBox parent, MeasurementFont font, string displayText) : base(renderer, parent)
+ {
+ _originalText = displayText;
+
+ Bounds.Name = "TextRun";
+ _currentText = displayText;
+
+ Lines = Regex.Split(_currentText, "\r\n|\r|\n").ToList();
+
+ _measurementFont = font;
+ _isFirstInParagraph = true;
+
+ FontSizeInPixels = ((double)_measurementFont.Size).PointToPixel(true);
+ Bounds.Height = _measurementFont.Size;
+ if (parent.Height < _measurementFont.Size)
+ {
+ parent.Height = _measurementFont.Size;
+ }
+
+ //To get clipping height we need to get the textbody bounds
+ if (parent != null && parent.Parent != null && parent.Parent.Parent != null)
+ {
+ ClippingHeight = parent.Parent.Parent.Position.Y + parent.Parent.Parent.Size.Y;
+ }
+ if (Lines.Count == 1)
+ {
+ //Bounds.Width = parent.Width;
+ GetBounds(out double il, out double it, out double ir, out double ib); //TODO: remove when calc works
+ }
+ else
+ {
+ //Measure text.
+ GetBounds(out double il, out double it, out double ir, out double ib); //TODO: remove when calc works
+ }
+ _underLineType = eUnderLineType.None;
+ }
+
internal TextRunItem(DrawingBase renderer, BoundingBox parent, string text, ExcelTextFont font, string displayText) : base(renderer, parent)
{
_originalText = text;
@@ -68,7 +89,7 @@ internal TextRunItem(DrawingBase renderer, BoundingBox parent, string text, Exce
_isFirstInParagraph = true;
- _fontStyles = _measurementFont.Style;
+ //_fontStyles = _measurementFont.Style;
FontSizeInPixels = ((double)_measurementFont.Size).PointToPixel(true);
Bounds.Height = _measurementFont.Size;
@@ -76,7 +97,7 @@ internal TextRunItem(DrawingBase renderer, BoundingBox parent, string text, Exce
{
parent.Height = _measurementFont.Size;
}
- _horizontalTextAlignment = eTextAlignment.Center;
+ //_horizontalTextAlignment = eTextAlignment.Center;
if (font.Fill.Style == eFillStyle.SolidFill)
{
@@ -122,12 +143,12 @@ internal TextRunItem(DrawingBase renderer, BoundingBox parent, ExcelParagraphTex
_measurementFont = run.GetMeasurementFont();
- _fontStyles = _measurementFont.Style;
+ //_fontStyles = _measurementFont.Style;
FontSizeInPixels = ((double)_measurementFont.Size).PointToPixel(true);
Bounds.Height = _measurementFont.Size;
- _horizontalTextAlignment = run.Paragraph.HorizontalAlignment;
+ //_horizontalTextAlignment = run.Paragraph.HorizontalAlignment;
if (run.Fill.IsEmpty == false && run.Fill.Style == eFillStyle.SolidFill)
{
diff --git a/src/EPPlus.Export.ImageRenderer/RenderItems/SvgGroupItem.cs b/src/EPPlus.Export.ImageRenderer/RenderItems/SvgGroupItem.cs
index ae92137b1b..0b6f047f72 100644
--- a/src/EPPlus.Export.ImageRenderer/RenderItems/SvgGroupItem.cs
+++ b/src/EPPlus.Export.ImageRenderer/RenderItems/SvgGroupItem.cs
@@ -37,8 +37,21 @@ internal class SvgGroupItem : RenderItem
{
public override RenderItemType Type => RenderItemType.Group;
+ public string TextAnchor { get; set; }
+
public string GroupTransform = "";
+ //internal SvgGroupItem(DrawingBase renderer, BoundingBox bounds)
+
+ internal SvgGroupItem(DrawingBase renderer) : base(renderer)
+ {
+ }
+
+ internal SvgGroupItem(DrawingBase renderer, double xPos, double yPos) : base(renderer)
+ {
+ GroupTransform = $"transform=\"translate({xPos.PointToPixelString()}, {yPos.PointToPixelString()})\"";
+ }
+
internal SvgGroupItem(DrawingBase renderer, BoundingBox bounds) : base(renderer)
{
Bounds = bounds;
@@ -58,16 +71,7 @@ internal SvgGroupItem(DrawingBase renderer, BoundingBox parent, double rotation)
}
if(rotation!=0)
{
- var cx = parent.Width / 2;
- var cy = parent.Height / 2;
- if (cx == 0 && cy == 0)
- {
- rot = $"rotate({rotation.ToString(CultureInfo.InvariantCulture)}))";
- }
- else
- {
- rot = $"rotate({rotation.ToString(CultureInfo.InvariantCulture)}, {cx.ToString(CultureInfo.InvariantCulture)}, {cy.ToString(CultureInfo.InvariantCulture)})";
- }
+ rot = $"rotate({rotation.ToString(CultureInfo.InvariantCulture)})";
}
if(string.IsNullOrEmpty(bounds)==false && string.IsNullOrEmpty(rot)==false)
@@ -86,14 +90,16 @@ internal SvgGroupItem(DrawingBase renderer, BoundingBox parent, double rotation)
public override void Render(StringBuilder sb)
{
- if(string.IsNullOrEmpty(GroupTransform))
+ sb.Append($"");
+ sb.Append($" {GroupTransform}");
}
- else
+ if (string.IsNullOrEmpty(TextAnchor) == false)
{
- sb.Append($"");
+ sb.Append($" text-anchor=\"{TextAnchor}\"");
}
+ sb.Append(">");
}
internal override void GetBounds(out double il, out double it, out double ir, out double ib)
diff --git a/src/EPPlus.Export.ImageRenderer/RenderItems/SvgItem/ConnectionPointsMiddle.cs b/src/EPPlus.Export.ImageRenderer/RenderItems/SvgItem/ConnectionPointsMiddle.cs
new file mode 100644
index 0000000000..e8150275f4
--- /dev/null
+++ b/src/EPPlus.Export.ImageRenderer/RenderItems/SvgItem/ConnectionPointsMiddle.cs
@@ -0,0 +1,37 @@
+using EPPlusImageRenderer;
+using OfficeOpenXml.FormulaParsing.Excel.Functions.Text;
+using System.Collections.Generic;
+
+
+namespace EPPlus.Export.ImageRenderer.RenderItems.SvgItem
+{
+ internal class ConnectionPointsMiddle
+ {
+ internal Coordinate Left;
+ internal Coordinate Top;
+ internal Coordinate Right;
+ internal Coordinate Bottom;
+
+ internal Dictionary Points = new Dictionary();
+
+ internal ConnectionPointsMiddle(double left, double top, double width, double height)
+ {
+ var middleWidth = width / 2;
+ var middleHeight = height / 2;
+
+ var middleX = left + middleWidth;
+ var middleY = top + middleHeight;
+
+ Left = new Coordinate(left, middleY);
+ Top = new Coordinate(middleX, top);
+
+ Right = new Coordinate(left + width, middleY);
+ Bottom = new Coordinate(left + middleX, top + height);
+
+ Points.Add(0, Left);
+ Points.Add(1, Top);
+ Points.Add(2, Right);
+ Points.Add(3, Bottom);
+ }
+ }
+}
diff --git a/src/EPPlus.Export.ImageRenderer/RenderItems/SvgItem/PointLines.cs b/src/EPPlus.Export.ImageRenderer/RenderItems/SvgItem/PointLines.cs
new file mode 100644
index 0000000000..79bd7dd591
--- /dev/null
+++ b/src/EPPlus.Export.ImageRenderer/RenderItems/SvgItem/PointLines.cs
@@ -0,0 +1,75 @@
+using EPPlus.Graphics;
+using EPPlusImageRenderer;
+using EPPlusImageRenderer.RenderItems;
+using EPPlusImageRenderer.Svg;
+using System;
+using System.Collections.Generic;
+using System.Drawing;
+using System.Linq;
+using System.Runtime.InteropServices;
+using System.Text;
+
+namespace EPPlus.Export.ImageRenderer.RenderItems.SvgItem
+{
+ internal class PointLines : DrawingObject
+ {
+ internal List RenderLines = new List();
+
+ internal ConnectionPointsMiddle ConnectionPoints;
+
+ private List ptColors = new List { "red", "green", "blue", "yellow" };
+
+ private BoundingBox parentBounds;
+
+ private PointLines(DrawingBase renderer) : base(renderer)
+ {
+ }
+
+ internal PointLines(DrawingBase renderer, BoundingBox parent, ConnectionPointsMiddle connectionPoints) : this(renderer)
+ {
+ parentBounds = parent;
+
+ Bounds = new BoundingBox();
+
+ Bounds.Parent = parent;
+ //Bounds.Left = parent.Left;
+ //Bounds.Top = parent.Top;
+ //Bounds.Width = parent.Width;
+ //Bounds.Height = parent.Height;
+ ConnectionPoints = connectionPoints;
+
+ UpdateLines();
+ }
+
+ internal void UpdateLines()
+ {
+ RenderLines.Clear();
+
+ for (int i = 0; i < ConnectionPoints.Points.Count; i++)
+ {
+ var cPoint = ConnectionPoints.Points[i];
+ var cPointLine = new SvgRenderLineItem(DrawingRenderer, Bounds);
+ cPointLine.X1 = 0;
+ cPointLine.Y1 = 0;
+ cPointLine.X2 = cPoint.X;
+ cPointLine.Y2 = cPoint.Y;
+
+ cPointLine.BorderWidth = 1;
+ cPointLine.BorderColor = ptColors[i];
+ RenderLines.Add(cPointLine);
+ }
+ }
+
+ internal override void AppendRenderItems(List renderItems)
+ {
+ SvgGroupItem gItem = new SvgGroupItem(DrawingRenderer, Bounds);
+ renderItems.Add(gItem);
+ foreach (var line in RenderLines)
+ {
+ renderItems.Add(line);
+ }
+ SvgEndGroupItem endGItem = new SvgEndGroupItem(DrawingRenderer, Bounds);
+ renderItems.Add(endGItem);
+ }
+ }
+}
diff --git a/src/EPPlus.Export.ImageRenderer/RenderItems/SvgItem/SvgChartDataLabelStandard.cs b/src/EPPlus.Export.ImageRenderer/RenderItems/SvgItem/SvgChartDataLabelStandard.cs
new file mode 100644
index 0000000000..c7e5bf0d44
--- /dev/null
+++ b/src/EPPlus.Export.ImageRenderer/RenderItems/SvgItem/SvgChartDataLabelStandard.cs
@@ -0,0 +1,348 @@
+using EPPlus.Graphics;
+using EPPlusImageRenderer;
+using EPPlusImageRenderer.RenderItems;
+using EPPlusImageRenderer.Svg;
+using OfficeOpenXml.Drawing;
+using OfficeOpenXml.Drawing.Chart;
+using OfficeOpenXml.Utils.EnumUtils;
+using System;
+using System.Collections.Generic;
+
+namespace EPPlus.Export.ImageRenderer.RenderItems.SvgItem
+{
+ internal class SvgChartDataLabelStandard : SvgChartObject
+ {
+ bool _hasManualLayout = false;
+ bool _hasLeaderLines = false;
+ bool _haveAdjustedForIcon = false;
+ bool _renderConnectionPointLines = false;
+
+ private SvgTextBox _txtBox;
+ BoundingBox _parentPoint;
+ List _leaderLines = new List();
+ Coordinate _manualLayoutOffset = new Coordinate (0, 0);
+ PointLines _connectionPointLines;
+ eLabelPosition _labelPosition;
+
+ //public SvgChartDataLabelStandard(DrawingChart chart, string dataLabelText) : base(chart)
+ //{
+ // var txtBox = new SvgTextBox(chart, chart.Bounds, chart.Bounds);
+ // txtBox.AddText(0, dataLabelText);
+ //}
+
+ //public SvgChartDataLabelStandard(DrawingChart chart, ExcelChartDataLabelStandard standard, SvgTextBox txtBox) : base(chart)
+ //{
+ // HasLegendKey = standard.ShowLegendKey;
+ // TxtBox = txtBox;
+ //}
+
+ public SvgChartDataLabelStandard(DrawingChart chart, ExcelChartDataLabelStandard standard) : base(chart)
+ {
+ _labelPosition = standard.Position;
+ }
+
+ RenderItem _seriesIcon = null;
+
+ internal void AddSeriesIcon(RenderItem seriesIcon)
+ {
+ var iconWidth = seriesIcon.Bounds.Width;
+ var iconHeight = seriesIcon.Bounds.Height;
+
+ _seriesIcon = seriesIcon;
+ _seriesIcon.Bounds.Parent = Bounds;
+
+ if (_haveAdjustedForIcon == false)
+ {
+ _txtBox.Left += iconWidth;
+ _seriesIcon.Bounds.Left -= 0.75d;
+ //It seems there is a hard-coded margin in excel of about 4.5pt (6px)
+ Bounds.Left += 4d + 2.25d;
+ LeftMargin -= 2.25d + 4d;
+ Bounds.Width += iconWidth + 2.25d;
+
+ _haveAdjustedForIcon = true;
+ }
+ }
+
+ internal void ImportDataLabel(SvgChart chart, ExcelChartStandardSerie serie, ExcelChartDataLabelStandard dataLabel, object xValue, object yValue, ExcelDrawingParagraph defaultParagraph, BoundingBox maxBounds, BoundingBox defaultMargins)
+ {
+ List dlblStrings = new List();
+
+ if (dataLabel.ShowSeriesName)
+ {
+ dlblStrings.Add(serie.GetHeaderString());
+ }
+ if (dataLabel.ShowCategory)
+ {
+ dlblStrings.Add(xValue.ToString());
+ }
+ if (dataLabel.ShowValue)
+ {
+ dlblStrings.Add(yValue.ToString());
+ }
+
+ var separator = string.IsNullOrEmpty(dataLabel.Separator) ? ", " : dataLabel.Separator;
+
+ string finalString = "";
+ for (int j = 0; j < dlblStrings.Count; j++)
+ {
+ finalString += dlblStrings[j];
+ if (j != dlblStrings.Count - 1)
+ {
+ finalString += separator;
+ }
+ }
+
+ var txtBox = new SvgTextBox(chart, Bounds, maxBounds);
+
+ txtBox.ImportTextBody(dataLabel.TextBody, false);
+
+ txtBox.TextBody.Bounds.Top = 0;
+ txtBox.TextBody.AutoSize = true;
+
+ if (txtBox.TextBody.Paragraphs.Count == 0)
+ {
+ txtBox.TextBody.ImportParagraph(defaultParagraph, 0, finalString);
+ //txtBox.TextBody.AddParagraph(0, finalString);
+ }
+ else if (txtBox.TextBody.Paragraphs.Count == 1)
+ {
+ txtBox.TextBody.ImportParagraph(dataLabel.TextBody.Paragraphs[0], 0, finalString);
+ //Remove dummy paragraph added by ImportTextBody
+ txtBox.TextBody.Paragraphs.RemoveAt(0);
+ }
+
+ if(txtBox.LeftMargin == 0)
+ {
+ txtBox.LeftMargin = defaultMargins.Left;
+ txtBox.RightMargin = defaultMargins.Width;
+ txtBox.TopMargin = defaultMargins.Top;
+ txtBox.BottomMargin = defaultMargins.Height;
+ }
+
+ //Txtbox is Broken. Workaround
+ txtBox.Left += txtBox.LeftMargin;
+ txtBox.Top += txtBox.TopMargin;
+
+ //Center the textbox at the origin point
+ Bounds.Left -= txtBox.Rectangle.Bounds.Width / 2;
+ Bounds.Top -= txtBox.Rectangle.Bounds.Height / 2;
+
+ //Set initial width and height to content
+ Bounds.Width = txtBox.Rectangle.Bounds.Width;
+ Bounds.Height = txtBox.Rectangle.Bounds.Height;
+
+ _txtBox = txtBox;
+
+ if (dataLabel.Fill.IsEmpty == false)
+ {
+ _txtBox.Rectangle.SetDrawingPropertiesFill(dataLabel.Fill, null);
+ }
+ if (dataLabel.Font.IsEmpty == false)
+ {
+ txtBox.TextBody.FontColorString = "#" + dataLabel.Font.Color.ToColorString();
+ }
+
+ _labelPosition = dataLabel.Position;
+
+ if (dataLabel is ExcelChartDataLabelItem)
+ {
+ var individualLabel = dataLabel as ExcelChartDataLabelItem;
+
+ if (individualLabel.Fill.IsEmpty == false)
+ {
+ _txtBox.Rectangle.FillColor = "#" + individualLabel.Fill.Color.ToColorString();
+ }
+
+ if (individualLabel.Layout != null && individualLabel.Layout.HasLayout)
+ {
+ _hasManualLayout = true;
+ var rect = GetRectFromManualLayout(chart, individualLabel.Layout);
+ Rectangle = rect;
+
+ _manualLayoutOffset = new Coordinate(Rectangle.Left, Rectangle.Top);
+
+ Bounds.Left += _manualLayoutOffset.X;
+ Bounds.Top += _manualLayoutOffset.Y;
+
+ if (dataLabel.ShowLeaderLines)
+ {
+ _hasLeaderLines = true;
+ }
+ }
+ }
+ }
+
+ private int GetClosestConnectionPointCoordinateIndex(Coordinate originPoint)
+ {
+ double smallestDist = double.MaxValue;
+ int i = 0;
+ int smallestIndex = 0;
+
+ foreach (var line in _connectionPointLines.RenderLines)
+ {
+ line.X1 = originPoint.X;
+ line.Y1 = originPoint.Y;
+
+ var w = Math.Abs(line.X2 - line.X1);
+ var h = Math.Abs(line.Y2 - line.Y1);
+
+ //Use pythagoran theorem to get diagonal distance
+ var totalDist = Math.Sqrt(Math.Pow(w, 2) + Math.Pow(h, 2));
+
+ if (totalDist < smallestDist)
+ {
+ smallestDist = totalDist;
+ smallestIndex = i;
+ }
+ i++;
+ }
+
+ return smallestIndex;
+ }
+
+ internal void SetParentPoint(BoundingBox parentPoint)
+ {
+ Bounds.Parent = parentPoint;
+ _parentPoint = parentPoint;
+
+ switch (_labelPosition)
+ {
+ case eLabelPosition.Center:
+ break;
+ case eLabelPosition.Left:
+ Bounds.Left -= _txtBox.Width + (parentPoint.Width / 2);
+ break;
+ case eLabelPosition.Right:
+ case eLabelPosition.BestFit:
+ Bounds.Left += _txtBox.Width / 2 + parentPoint.Width;
+ break;
+ case eLabelPosition.Top:
+ Bounds.Top -= (parentPoint.Height + _txtBox.Height) / 2;
+ break;
+ case eLabelPosition.Bottom:
+ Bounds.Top += (parentPoint.Height + _txtBox.Height) / 2;
+ break;
+ default:
+ throw new InvalidOperationException($"The datalabel position {_labelPosition} has not been implemented yet");
+ }
+
+ if (_hasManualLayout)
+ {
+ if (_hasLeaderLines)
+ {
+ //With origin in top left of current bounds get the connection points
+ var cPoints = new ConnectionPointsMiddle(0, 0, Bounds.Width, Bounds.Height);
+
+ //Ready to draw the lines so that we can visualize the distances to each point
+ _connectionPointLines = new PointLines(ChartRenderer, Bounds, cPoints);
+
+ //Adjust if there is a margin
+ _connectionPointLines.Bounds.Left += LeftMargin;
+ _connectionPointLines.UpdateLines();
+
+ //Get the offset between those points and the origin point
+ var offsetToParentPoint = new Coordinate(-(Bounds.Left + LeftMargin), -(Bounds.Top + TopMargin));
+
+ //Calculate closest point
+ var index = GetClosestConnectionPointCoordinateIndex(offsetToParentPoint);
+
+ _leaderLines.Clear();
+
+ double xOffset = 0;
+ if (index == 0 || index == 2)
+ {
+ //If Left or Right
+ //Add extra 7 px (5.25pt) line to the given side
+ var extraLine = new SvgRenderLineItem(ChartRenderer, ChartRenderer.Bounds);
+
+ xOffset += index == 0 ? -5.25d : 5.25d;
+
+ extraLine.X1 = _connectionPointLines.ConnectionPoints.Points[index].X + LeftMargin;
+ extraLine.Y1 = _connectionPointLines.ConnectionPoints.Points[index].Y;
+ extraLine.Y2 = _connectionPointLines.ConnectionPoints.Points[index].Y;
+ extraLine.X2 = extraLine.X1 + xOffset;
+
+ extraLine.BorderColor = "gray";
+ extraLine.BorderWidth = 0.5;
+
+ _leaderLines.Add(extraLine);
+ }
+ var mainLine = new SvgRenderLineItem(ChartRenderer, ChartRenderer.Bounds);
+ mainLine.X1 = _connectionPointLines.ConnectionPoints.Points[index].X + xOffset + LeftMargin;
+ mainLine.Y1 = _connectionPointLines.ConnectionPoints.Points[index].Y;
+ mainLine.X2 = offsetToParentPoint.X + LeftMargin;
+ mainLine.Y2 = offsetToParentPoint.Y;
+
+ mainLine.BorderColor = "gray";
+ mainLine.BorderWidth = 0.5;
+ _leaderLines.Add(mainLine);
+ }
+ }
+ }
+
+ private void AppendDebugBounds(List renderItems)
+ {
+ SvgRenderRectItem rect = new SvgRenderRectItem(ChartRenderer, Bounds);
+ rect.Bounds.Left = LeftMargin;
+ rect.Bounds.Top = 0;
+ rect.Bounds.Width = Bounds.Width;
+ rect.Bounds.Height = Bounds.Height;
+
+ rect.FillColor = "red";
+ rect.FillOpacity = 0.2;
+ renderItems.Add(rect);
+ }
+
+ internal override void AppendRenderItems(List renderItems)
+ {
+ var parentPointGroup = new SvgGroupItem(ChartRenderer, _parentPoint);
+ renderItems.Add(parentPointGroup);
+
+ var titleItemOrigin = new SvgTitleItem(DrawingRenderer, "DataLabel originpoint");
+ renderItems.Add(titleItemOrigin);
+
+ var group = new SvgGroupItem(ChartRenderer, Bounds);
+ renderItems.Add(group);
+
+ var titleItem = new SvgTitleItem(DrawingRenderer, "DataLabel size adjustment");
+ renderItems.Add(titleItem);
+
+ //AppendDebugBounds(renderItems);
+
+ _txtBox.AppendRenderItems(renderItems);
+
+ if(_renderConnectionPointLines)
+ {
+ if (_connectionPointLines != null)
+ {
+ _connectionPointLines.AppendRenderItems(renderItems);
+ }
+ }
+
+ if (_seriesIcon != null)
+ {
+ var height = Bounds.Height;
+ if (height == 0)
+ {
+ height = _txtBox.Height;
+ }
+ //Currently series icon always has a y1 y2 of 2
+ var iconGrp = new SvgGroupItem(ChartRenderer, _seriesIcon.Bounds.Left, height / 2 - 2);
+ renderItems.Add(iconGrp);
+ renderItems.Add(_seriesIcon);
+ renderItems.Add(new SvgEndGroupItem(DrawingRenderer, Bounds));
+ }
+
+ if (_leaderLines != null && _leaderLines.Count > 0)
+ {
+ foreach (var line in _leaderLines)
+ {
+ renderItems.Add(line);
+ }
+ }
+ renderItems.Add(new SvgEndGroupItem(DrawingRenderer, Bounds));
+ renderItems.Add(new SvgEndGroupItem(DrawingRenderer, Bounds));
+ }
+ }
+}
diff --git a/src/EPPlus.Export.ImageRenderer/RenderItems/SvgItem/SvgChartSerieDataLabel.cs b/src/EPPlus.Export.ImageRenderer/RenderItems/SvgItem/SvgChartSerieDataLabel.cs
new file mode 100644
index 0000000000..03b2eed398
--- /dev/null
+++ b/src/EPPlus.Export.ImageRenderer/RenderItems/SvgItem/SvgChartSerieDataLabel.cs
@@ -0,0 +1,131 @@
+using EPPlus.Graphics;
+using EPPlusImageRenderer.RenderItems;
+using EPPlusImageRenderer.Svg;
+using OfficeOpenXml.Drawing;
+using OfficeOpenXml.Drawing.Chart;
+using System.Collections.Generic;
+
+namespace EPPlus.Export.ImageRenderer.RenderItems.SvgItem
+{
+ internal class SvgChartSerieDataLabel : DrawingObjectNoBounds
+ {
+ //positioning is handled by parent item via these
+ internal List groupItems = new List();
+ private List dataLabels = new List();
+
+ private RenderItem seriesIcon = null;
+ private int _serieIndex = -1;
+ ExcelDrawingParagraph defaultParagraph;
+ BoundingBox plotAreaBounds;
+ BoundingBox _defaultMargins;
+ ExcelChartSerieDataLabel _dlblSerie;
+
+ public SvgChartSerieDataLabel(SvgChart chart, ExcelChartSerieDataLabel dlblSerie, BoundingBox maxBounds, ExcelChartStandardSerie serie, List xValues, List yValues, int index) : base(chart)
+ {
+ _serieIndex = index;
+ _dlblSerie = dlblSerie;
+ plotAreaBounds = chart.Plotarea.Rectangle.Bounds;
+
+ if (dlblSerie.TextBody.Paragraphs.Count != 0)
+ {
+ defaultParagraph = dlblSerie.TextBody.Paragraphs[0];
+ dlblSerie.TextBody.GetInsetsInPoints(out double l, out double top, out double right, out double bottom);
+ _defaultMargins = new BoundingBox(l, top, right, bottom);
+ }
+
+
+ if (dlblSerie.DataLabels.Count == 0 && serie.NumberOfItems > 0)
+ {
+ if (xValues != null)
+ {
+ for (int i = 0; i < serie.NumberOfItems; i++)
+ {
+ AddDatalabel(chart, serie, dlblSerie, xValues[i], yValues[i], maxBounds);
+ }
+ }
+ }
+ else
+ {
+ if (xValues != null)
+ {
+ for (int i = 0; i < dlblSerie.DataLabels.Count; i++)
+ {
+ var dataLabel = dlblSerie.DataLabels[i];
+
+ AddDatalabel(chart, serie, dataLabel, xValues[i], yValues[i], maxBounds);
+ }
+ }
+ }
+ }
+
+ private void CreateSeriesIcon(SvgChart chart, ExcelChartStandardSerie serie, BoundingBox maxBounds)
+ {
+ if (chart.Legend == null)
+ {
+ seriesIcon = chart.GetSeriesIcon(serie, _serieIndex, maxBounds);
+ }
+ else
+ {
+ var legendItem = chart.Legend;
+ var seriesIconOrig = (SvgRenderLineItem)legendItem.SeriesIcon[_serieIndex].SeriesIcon;
+ var clonedIcon = seriesIconOrig.Clone(chart);
+
+ clonedIcon.Y1 = 0;
+ clonedIcon.Y2 = 0;
+
+ seriesIcon = clonedIcon;
+ }
+ }
+
+ private RenderItem GetSeriesIcon(SvgChart chart, ExcelChartStandardSerie serie, BoundingBox maxBounds)
+ {
+ if(seriesIcon == null)
+ {
+ CreateSeriesIcon(chart, serie, maxBounds);
+ }
+
+ return seriesIcon;
+ }
+
+ private void AddDatalabel(SvgChart chart, ExcelChartStandardSerie serie, ExcelChartDataLabelStandard dataLabel, object xValue, object yValue, BoundingBox maxBounds)
+ {
+ var newDataLabel = new SvgChartDataLabelStandard(chart, dataLabel);
+ newDataLabel.ImportDataLabel(chart, serie, dataLabel, xValue, yValue, defaultParagraph, maxBounds, _defaultMargins);
+
+ if(dataLabel.ShowLegendKey)
+ {
+ newDataLabel.AddSeriesIcon(GetSeriesIcon(chart, serie, maxBounds));
+ }
+
+ dataLabels.Add(newDataLabel);
+ }
+
+ internal void SetParentPoint(BoundingBox parent, int index)
+ {
+ if (dataLabels.Count > index)
+ {
+ dataLabels[index].SetParentPoint(parent);
+ }
+ //dataLabels[index].SetParentPoint(parent);
+ }
+
+ internal override void AppendRenderItems(List renderItems)
+ {
+ var plotAreaGroup = new SvgGroupItem(DrawingRenderer, plotAreaBounds);
+
+ if(_dlblSerie.Fill.IsEmpty == false)
+ {
+ plotAreaGroup.SetDrawingPropertiesFill(_dlblSerie.Fill, null);
+
+ plotAreaGroup.GroupTransform += $" fill=\"{plotAreaGroup.FillColor}\"";
+ }
+
+ renderItems.Add(plotAreaGroup);
+ for(int i = 0; i< dataLabels.Count; i++)
+ {
+ dataLabels[i].AppendRenderItems(renderItems);
+ }
+ renderItems.Add(new SvgEndGroupItem(DrawingRenderer, plotAreaBounds));
+ }
+ }
+}
diff --git a/src/EPPlus.Export.ImageRenderer/RenderItems/SvgItem/SvgContainerItem.cs b/src/EPPlus.Export.ImageRenderer/RenderItems/SvgItem/SvgContainerItem.cs
new file mode 100644
index 0000000000..26d9928373
--- /dev/null
+++ b/src/EPPlus.Export.ImageRenderer/RenderItems/SvgItem/SvgContainerItem.cs
@@ -0,0 +1,36 @@
+using EPPlus.Export.ImageRenderer.RenderItems.Shared;
+using EPPlusImageRenderer.RenderItems;
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+
+namespace EPPlus.Export.ImageRenderer.RenderItems.SvgItem
+{
+ internal class SvgContainerItem : ContainerItem
+ {
+ public SvgContainerItem(RenderItem innerItem, RenderItem outerItem) : base(innerItem, outerItem)
+ {
+ }
+
+ public override void Render(StringBuilder sb)
+ {
+ var grpItem = new SvgGroupItem(DrawingRenderer, Bounds.Left, Bounds.Top);
+ grpItem.Render(sb);
+
+ OuterItem.Render(sb);
+
+ var grpItem2 = new SvgGroupItem(DrawingRenderer, MarginLeft, MarginTop);
+ grpItem2.Render(sb);
+
+ InnerItem.Render(sb);
+
+ var endGroupItem2 = new SvgEndGroupItem(DrawingRenderer, Bounds);
+ endGroupItem2.Render(sb);
+
+
+ var endGroupItem = new SvgEndGroupItem(DrawingRenderer, Bounds);
+ endGroupItem.Render(sb);
+ }
+ }
+}
diff --git a/src/EPPlus.Export.ImageRenderer/RenderItems/SvgItem/SvgGroupItemNew.cs b/src/EPPlus.Export.ImageRenderer/RenderItems/SvgItem/SvgGroupItemNew.cs
new file mode 100644
index 0000000000..2da802ed6c
--- /dev/null
+++ b/src/EPPlus.Export.ImageRenderer/RenderItems/SvgItem/SvgGroupItemNew.cs
@@ -0,0 +1,74 @@
+using EPPlus.Export.ImageRenderer.RenderItems.Shared;
+using EPPlus.Fonts.OpenType.Utils;
+using EPPlus.Graphics;
+using EPPlusImageRenderer;
+using System.Globalization;
+using System.Text;
+
+namespace EPPlus.Export.ImageRenderer.RenderItems.SvgItem
+{
+ internal class SvgGroupItemNew : GroupItem
+ {
+ const string transformTranslate = "translate({0}, {1}) ";
+ const string transformRotate = "rotate({0}) ";
+
+ public SvgGroupItemNew(DrawingBase renderer, double localXPos, double localYPos) : base(renderer, localXPos, localYPos)
+ {
+ }
+
+ public SvgGroupItemNew(DrawingBase renderer, BoundingBox parent, double rotation, Transform rotationPoint = null) : base(renderer, parent, rotation, rotationPoint)
+ {
+ }
+
+ public override void Render(StringBuilder sb)
+ {
+ string combinedTransform = GetCombinedTransformString();
+
+ if (string.IsNullOrEmpty(combinedTransform) == false)
+ {
+ sb.Append($"");
+ }
+ else
+ {
+ sb.Append($"");
+ }
+
+ foreach (var item in _childItems)
+ {
+ item.Render(sb);
+ }
+
+ sb.Append(" ");
+ }
+
+ string GetCombinedTransformString()
+ {
+ string positionStr = "";
+ string rotationStr = GetRotationStr();
+
+ if (Position != null)
+ {
+ positionStr = string.Format(transformTranslate, Position.Top.PointToPixelString(), Position.Left.PointToPixelString()) + " ";
+ }
+
+ return positionStr + rotationStr;
+ }
+
+ string GetRotationStr()
+ {
+ if (double.IsNaN(Rotation) == false)
+ {
+ string rot = Rotation.ToString(CultureInfo.InvariantCulture);
+
+ if(RotationPoint != null && RotationPoint != Position)
+ {
+ rot += $", {RotationPoint.Left.PointToPixelString()}, {RotationPoint.Top.PointToPixelString()}";
+ }
+
+ return string.Format(transformRotate, rot);
+ }
+
+ return string.Empty;
+ }
+ }
+}
diff --git a/src/EPPlus.Export.ImageRenderer/RenderItems/SvgItem/SvgParagraphItem.cs b/src/EPPlus.Export.ImageRenderer/RenderItems/SvgItem/SvgParagraphItem.cs
index acb10427ed..f4cd5d692a 100644
--- a/src/EPPlus.Export.ImageRenderer/RenderItems/SvgItem/SvgParagraphItem.cs
+++ b/src/EPPlus.Export.ImageRenderer/RenderItems/SvgItem/SvgParagraphItem.cs
@@ -5,6 +5,7 @@
using EPPlusImageRenderer.RenderItems;
using OfficeOpenXml.Drawing;
using OfficeOpenXml.Drawing.Theme;
+using OfficeOpenXml.Interfaces.Drawing.Text;
using OfficeOpenXml.Style;
using System.Globalization;
using System.Text;
@@ -19,6 +20,11 @@ public SvgParagraphItem(TextBodyItem textBody, DrawingBase renderer, BoundingBox
{
}
+ public SvgParagraphItem(TextBodyItem textBody, DrawingBase renderer, BoundingBox parent, string text) : base(textBody, renderer, parent)
+ {
+ AddLinesAndTextRuns(text);
+ }
+
public SvgParagraphItem(TextBodyItem textBody, DrawingBase renderer, BoundingBox parent, ExcelDrawingParagraph p, string textIfEmpty = null) : base(textBody, renderer, parent, p, textIfEmpty)
{
}
@@ -51,10 +57,27 @@ public override void Render(StringBuilder sb)
{
var fontSize = _paragraphFont.Size.PointToPixel().ToString(CultureInfo.InvariantCulture);
- sb.AppendLine($"");
+ sb.AppendLine($"");
sb.AppendLine("paragraph ");
+ if(DisplayBounds)
+ {
+ sb.AppendLine($"");
+ sb.AppendLine("Bounding-Box: Paragraph ");
+ SvgRenderRectItem visualBoundingBox = new SvgRenderRectItem(DrawingRenderer, ParentTextBody.Bounds);
+
+ //Left/Top handled by transform
+ visualBoundingBox.Bounds.Width = Bounds.Width;
+ visualBoundingBox.Bounds.Height = Bounds.Height;
+
+ visualBoundingBox.FillOpacity = 0.3;
+ visualBoundingBox.FillColor = "red";
+ visualBoundingBox.Render(sb);
+ sb.AppendLine($" ");
+
+ }
+
//var bb = new SvgRenderRectItem(DrawingRenderer, Bounds);
////The bb is affected by the Transform so set pos to zero
//if (IsFirstParagraph == false)
@@ -136,5 +159,10 @@ internal override TextRunItem CreateTextRun(string text, ExcelTextFont font, Bou
{
return new SvgTextRunItem(DrawingRenderer, parent, text, font, displayText);
}
+
+ internal override TextRunItem CreateTextRun(MeasurementFont font, BoundingBox parent, string displayText)
+ {
+ return new SvgTextRunItem(DrawingRenderer, parent, font, displayText);
+ }
}
}
diff --git a/src/EPPlus.Export.ImageRenderer/RenderItems/SvgItem/SvgTextBodyItem.cs b/src/EPPlus.Export.ImageRenderer/RenderItems/SvgItem/SvgTextBodyItem.cs
index 5310727cb3..605f8651b7 100644
--- a/src/EPPlus.Export.ImageRenderer/RenderItems/SvgItem/SvgTextBodyItem.cs
+++ b/src/EPPlus.Export.ImageRenderer/RenderItems/SvgItem/SvgTextBodyItem.cs
@@ -19,11 +19,10 @@ internal class SvgTextBodyItem : TextBodyItem
{
public SvgTextBodyItem(DrawingBase renderer, BoundingBox parent, bool autoSize, bool clampedToParent = false) : base(renderer, parent, autoSize)
{
- //Bounds.ClampedToParent = clampedToParent;
MaxWidth = parent.Width;
MaxHeight = parent.Height;
}
- public SvgTextBodyItem(DrawingBase renderer, BoundingBox parent, double left, double top, double maxWidth, double maxHeight, bool clampedToParent = false) : base(renderer, parent, false)
+ public SvgTextBodyItem(DrawingBase renderer, BoundingBox parent, double left, double top, double maxWidth, double maxHeight, bool clampedToParent = false, bool autoSize=false) : base(renderer, parent, autoSize)
{
Bounds.Left = left;
Bounds.Top = top;
@@ -31,8 +30,8 @@ public SvgTextBodyItem(DrawingBase renderer, BoundingBox parent, double left, do
Bounds.Height = maxHeight;
MaxWidth = maxWidth;
MaxHeight = maxHeight;
- //Bounds.ClampedToParent = clampedToParent;
}
+
internal override List Paragraphs { get; set; } = new List();
internal override void AppendRenderItems(List renderItems)
@@ -46,6 +45,12 @@ internal override void AppendRenderItems(List renderItems)
{
groupItem = new SvgGroupItem(DrawingRenderer, Bounds);
}
+
+ if (FontColorString != null)
+ {
+ groupItem.GroupTransform += $" fill=\"{FontColorString}\"";
+ }
+
renderItems.Add(groupItem);
foreach (SvgParagraphItem item in Paragraphs)
{
@@ -63,5 +68,10 @@ internal override ParagraphItem CreateParagraph(TextBodyItem textBody, ExcelDraw
{
return new SvgParagraphItem(this, DrawingRenderer, parent, paragraph, textIfEmpty);
}
+
+ internal override ParagraphItem CreateParagraph(TextBodyItem textBody, BoundingBox parent, string textIfEmpty = "")
+ {
+ return new SvgParagraphItem(this, DrawingRenderer, parent, textIfEmpty);
+ }
}
}
diff --git a/src/EPPlus.Export.ImageRenderer/RenderItems/SvgItem/SvgTextBox.cs b/src/EPPlus.Export.ImageRenderer/RenderItems/SvgItem/SvgTextBox.cs
index af3cfda0c5..071e63fcc3 100644
--- a/src/EPPlus.Export.ImageRenderer/RenderItems/SvgItem/SvgTextBox.cs
+++ b/src/EPPlus.Export.ImageRenderer/RenderItems/SvgItem/SvgTextBox.cs
@@ -5,11 +5,13 @@
using EPPlusImageRenderer.Svg;
using OfficeOpenXml.Drawing;
using OfficeOpenXml.FormulaParsing.Excel.Functions.Text;
+using OfficeOpenXml.Utils.EnumUtils;
+using OfficeOpenXml.Style;
using System;
using System.Collections.Generic;
-using System.Drawing;
-using System.Xml.Serialization;
-using static System.Net.Mime.MediaTypeNames;
+using System.Runtime.Serialization;
+using OfficeOpenXml.FormulaParsing.Excel.Functions.MathFunctions;
+
namespace EPPlus.Export.ImageRenderer.RenderItems.SvgItem
{
@@ -17,10 +19,9 @@ internal class SvgTextBox : DrawingObjectNoBounds
{
internal SvgTextBox(DrawingBase renderer, BoundingBox parent, double left, double top, double width, double height, double maxWidth = double.NaN, double maxHeight = double.NaN) : base(renderer)
{
+ Init(renderer, parent, maxWidth, maxHeight);
Left = left;
Top = top;
-
- Init(renderer, parent, maxWidth, maxHeight);
}
private void Init(DrawingBase renderer, BoundingBox parent, double maxWidth, double maxHeight)
@@ -29,7 +30,7 @@ private void Init(DrawingBase renderer, BoundingBox parent, double maxWidth, dou
_rectangle = new SvgRenderRectItem(DrawingRenderer, Parent);
TextBody = new SvgTextBodyItem(renderer, Rectangle.Bounds, true);
TextBody.MaxWidth = maxWidth;
- TextBody.MaxHeight = maxHeight;
+ TextBody.MaxHeight = maxHeight;
}
internal SvgTextBox(DrawingBase renderer, BoundingBox parent, double maxWidth, double maxHeight) : base(renderer)
@@ -53,8 +54,31 @@ public SvgRenderItem Rectangle
}
}
public SvgTextBodyItem TextBody {get;set;}
- public double Left { get; set; }
- public double Top { get; set; }
+ public double Left
+ {
+ get
+ {
+ return Rectangle.Bounds.Left; //TextBody.Bounds.Left - LeftMargin;
+
+ }
+ set
+ {
+ //TextBody.Bounds.Left = value + LeftMargin;
+ Rectangle.Bounds.Left = value;
+ }
+ }
+ public double Top
+ {
+ get
+ {
+ return Rectangle.Bounds.Top; //TextBody.Bounds.Top - TopMargin;
+ }
+ set
+ {
+ //TextBody.Bounds.Top = value + TopMargin;
+ Rectangle.Bounds.Top = value;
+ }
+ }
public double Width
{
get
@@ -100,11 +124,58 @@ internal double Rotation
Rectangle.Bounds.Rotation = value;
}
}
+ ///
+ /// Gets the actual width of the rotated textbox.
+ ///
+ ///
+ internal double GetActualWidth()
+ {
+ return Width * Math.Abs(Math.Cos(MathHelper.Radians(Rotation))) + Height * Math.Abs(Math.Sin(MathHelper.Radians(Rotation)));
+ }
+ ///
+ /// Gets the actual right position of the rotated textbox.
+ ///
+ ///
+ internal double GetActualRight()
+ {
+ return Left+GetActualWidth();
+ }
+ ///
+ /// Gets the actual height of the rotated textbox.
+ ///
+ ///
+ internal double GetActualHeight()
+ {
+ return Width * Math.Abs(Math.Sin(MathHelper.Radians(Rotation))) + Height * Math.Abs(Math.Cos(MathHelper.Radians(Rotation)));
+ }
+ ///
+ /// Gets the actual right position of the rotated textbox.
+ ///
+ ///
+ internal double GetActualBottom()
+ {
+ return Top + GetActualHeight();
+ }
+ ///
+ /// How the text is anchored.
+ ///
+ internal eTextAnchor TextAnchor
+ {
+ get;
+ set;
+ }
- internal void ImportTextBody(ExcelTextBody body)
+ internal void ImportTextBody(ExcelTextBody body, bool useDefaults = true, ExcelHorizontalAlignment horizontalDefault = ExcelHorizontalAlignment.Left)
{
double l, r, t, b;
- body.GetInsetsOrDefaults(out l, out t, out r, out b);
+ if (useDefaults)
+ {
+ body.GetInsetsOrDefaults(out l, out t, out r, out b);
+ }
+ else
+ {
+ body.GetInsetsInPoints(out l, out t, out r, out b);
+ }
LeftMargin = l;
TopMargin = t;
RightMargin = r;
@@ -116,6 +187,7 @@ internal void ImportTextBody(ExcelTextBody body)
internal override void AppendRenderItems(List renderItems)
{
var rect = Rectangle;
+
SvgGroupItem groupItem;
if (Rotation == 0)
{
@@ -125,8 +197,32 @@ internal override void AppendRenderItems(List renderItems)
{
groupItem = new SvgGroupItem(DrawingRenderer, new BoundingBox(Left, Top, Width, Height), Rotation);
}
+ groupItem.TextAnchor = TextAnchor.ToEnumString();
renderItems.Add(groupItem);
+
+ var textboxGroupItem = new SvgGroupItem(DrawingRenderer);
+ renderItems.Add(textboxGroupItem);
+
+ var titleItem = new SvgTitleItem(DrawingRenderer, "TextBodySvg Rect");
+ //The rect shound encapse the text element, so we need to set the left depending on the text anchor.
+ if(TextAnchor==eTextAnchor.Middle)
+ {
+ rect.Bounds.Left = -(rect.Bounds.Width / 2);
+ }
+ else if(TextAnchor==eTextAnchor.End)
+ {
+ rect.Bounds.Left = -rect.Bounds.Width;
+ }
+ else
+ {
+ rect.Bounds.Left = 0;
+ }
+ rect.Bounds.Top = 0;
+ renderItems.Add(titleItem);
renderItems.Add(rect);
+
+ renderItems.Add(new SvgEndGroupItem(DrawingRenderer, rect.Bounds));
+
TextBody.Bounds.Left = LeftMargin;
TextBody.Bounds.Top = TopMargin;
TextBody.AppendRenderItems(renderItems);
@@ -137,5 +233,10 @@ internal void ImportParagraph(ExcelDrawingParagraph item, double startingY, stri
{
TextBody.ImportParagraph(item, startingY, text);
}
+
+ internal void AddText(double startingY, string text = null)
+ {
+ TextBody.AddParagraph(startingY, text);
+ }
}
}
diff --git a/src/EPPlus.Export.ImageRenderer/RenderItems/SvgItem/SvgTextRunItem.cs b/src/EPPlus.Export.ImageRenderer/RenderItems/SvgItem/SvgTextRunItem.cs
index 73934f4979..b4c229a300 100644
--- a/src/EPPlus.Export.ImageRenderer/RenderItems/SvgItem/SvgTextRunItem.cs
+++ b/src/EPPlus.Export.ImageRenderer/RenderItems/SvgItem/SvgTextRunItem.cs
@@ -6,6 +6,7 @@
using EPPlusImageRenderer.Svg;
using OfficeOpenXml.Drawing;
using OfficeOpenXml.Drawing.Theme;
+using OfficeOpenXml.Interfaces.Drawing.Text;
using OfficeOpenXml.Style;
using System;
using System.Globalization;
@@ -15,6 +16,10 @@ namespace EPPlus.Export.ImageRenderer.RenderItems.SvgItem
{
internal class SvgTextRunItem : TextRunItem
{
+ public SvgTextRunItem(DrawingBase renderer, BoundingBox parent, MeasurementFont font, string displayText) : base(renderer, parent, font, displayText)
+ {
+ }
+
public SvgTextRunItem(DrawingBase renderer, BoundingBox parent, ExcelParagraphTextRunBase run, string displayText = "") : base(renderer,parent, run, displayText)
{
}
diff --git a/src/EPPlus.Export.ImageRenderer/RenderItems/SvgItem/SvgTitleItem.cs b/src/EPPlus.Export.ImageRenderer/RenderItems/SvgItem/SvgTitleItem.cs
new file mode 100644
index 0000000000..a053fbb56e
--- /dev/null
+++ b/src/EPPlus.Export.ImageRenderer/RenderItems/SvgItem/SvgTitleItem.cs
@@ -0,0 +1,26 @@
+using EPPlusImageRenderer;
+using EPPlusImageRenderer.RenderItems;
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+
+namespace EPPlus.Export.ImageRenderer.RenderItems.SvgItem
+{
+ internal class SvgTitleItem : RenderItem
+ {
+ internal string Title { get; private set; }
+
+ public SvgTitleItem(DrawingBase renderer, string titleName) : base(renderer)
+ {
+ Title = titleName;
+ }
+
+ public override RenderItemType Type => RenderItemType.CommentTitle;
+
+ public override void Render(StringBuilder sb)
+ {
+ sb.Append($"{Title} ");
+ }
+ }
+}
diff --git a/src/EPPlus.Export.ImageRenderer/RenderItems/SvgItem/eTextAnchor.cs b/src/EPPlus.Export.ImageRenderer/RenderItems/SvgItem/eTextAnchor.cs
new file mode 100644
index 0000000000..f1bead4f9c
--- /dev/null
+++ b/src/EPPlus.Export.ImageRenderer/RenderItems/SvgItem/eTextAnchor.cs
@@ -0,0 +1,21 @@
+namespace EPPlus.Export.ImageRenderer.RenderItems.SvgItem
+{
+ ///
+ /// Matches the enum for an svg text anchor of a text element.
+ ///
+ internal enum eTextAnchor
+ {
+ ///
+ /// Text is anchored to the start of the text element.
+ ///
+ Start,
+ ///
+ /// Text is anchored in the center of the text element.
+ ///
+ Middle,
+ ///
+ /// Text is anchored in the end of the text element.
+ ///
+ End
+ }
+}
\ No newline at end of file
diff --git a/src/EPPlus.Export.ImageRenderer/RenderItems/SvgRenderItem.cs b/src/EPPlus.Export.ImageRenderer/RenderItems/SvgRenderItem.cs
index 1c5b4a629e..4f81ec1094 100644
--- a/src/EPPlus.Export.ImageRenderer/RenderItems/SvgRenderItem.cs
+++ b/src/EPPlus.Export.ImageRenderer/RenderItems/SvgRenderItem.cs
@@ -15,9 +15,11 @@ Date Author Change
using EPPlusImageRenderer.Svg;
using OfficeOpenXml.Drawing;
using OfficeOpenXml.Drawing.Theme;
+using System;
using System.Globalization;
using System.Linq;
using System.Text;
+using System.Threading;
namespace EPPlusImageRenderer.RenderItems
{
internal enum SvgFillType
@@ -28,11 +30,24 @@ internal enum SvgFillType
}
internal abstract class SvgRenderItem : RenderItem
{
+ //Refrence string if this is part of a definition
+ internal string DefId = null;
+
internal SvgRenderItem(DrawingBase renderer, BoundingBox parent) : base(renderer, parent)
{
}
public override void Render(StringBuilder sb)
{
+ RenderBase(sb);
+ }
+
+ private void RenderBase(StringBuilder sb)
+ {
+ if (string.IsNullOrEmpty(DefId) == false)
+ {
+ sb.Append($"id=\"{DefId}\" ");
+ }
+
if (string.IsNullOrEmpty(FillColor) == false)
{
sb.Append($"fill=\"{FillColor}\" ");
@@ -46,7 +61,7 @@ public override void Render(StringBuilder sb)
{
sb.Append($"filter=\"{FilterName}\" ");
}
-
+
if (BorderWidth.HasValue)
{
if (string.IsNullOrEmpty(BorderColor) == false)
@@ -58,15 +73,55 @@ public override void Render(StringBuilder sb)
if (BorderDashArray != null)
{
- var BorderDashArrayStr = BorderDashArray.Select(x =>
+ var BorderDashArrayStr = BorderDashArray.Select(x =>
x.ToString(CultureInfo.InvariantCulture)).ToArray();
sb.Append($"stroke-dasharray=\"" + $"{string.Join(",", BorderDashArrayStr)}\" ");
}
+ if (BorderOpacity.HasValue)
+ {
+ sb.Append($" stroke-opacity=\"{(Math.Round(BorderOpacity.Value * 100)).ToString(CultureInfo.InvariantCulture)}%\" ");
+ }
}
- sb.Append($"stroke-miterlimit =\"8\" ");
+ sb.Append($"stroke-miterlimit =\"{StrokeMiterLimit}\" ");
}
+
internal abstract SvgRenderItem Clone(SvgShape svgDocument);
+ private protected void RenderCompoundItems(StringBuilder sb, double? borderWidth, string color, string filter)
+ {
+ var tmpBorderWidth = BorderWidth;
+ string tmpBorderColor = null;
+ BorderWidth = borderWidth ?? BorderWidth;
+ if (string.IsNullOrEmpty(color) == false)
+ {
+ tmpBorderColor = BorderColor;
+ BorderColor = color;
+ }
+
+ RenderBase(sb);
+ if (LineCap != eLineCap.Flat)
+ {
+ sb.AppendFormat(" stroke-linecap=\"{0}\"", LineCap == eLineCap.Round ? "round" : "square");
+ }
+ if (LineJoin != SvgLineJoin.Miter)
+ {
+ sb.AppendFormat(" stroke-linejoin=\"{0}\"", LineJoin);
+ }
+
+ if (string.IsNullOrEmpty(filter) == false)
+ {
+ sb.Append(" " + filter);
+ }
+
+ sb.AppendFormat("/>");
+
+ BorderWidth = tmpBorderWidth;
+ if (string.IsNullOrEmpty(color) == false)
+ {
+ BorderColor = tmpBorderColor;
+ }
+ }
+
}
}
\ No newline at end of file
diff --git a/src/EPPlus.Export.ImageRenderer/RenderItems/SvgRenderLineItem.cs b/src/EPPlus.Export.ImageRenderer/RenderItems/SvgRenderLineItem.cs
index fe588d7822..37298cc720 100644
--- a/src/EPPlus.Export.ImageRenderer/RenderItems/SvgRenderLineItem.cs
+++ b/src/EPPlus.Export.ImageRenderer/RenderItems/SvgRenderLineItem.cs
@@ -12,6 +12,7 @@ Date Author Change
*************************************************************************************************/
using EPPlus.Export.ImageRenderer.Utils;
using EPPlus.Fonts.OpenType.Utils;
+using EPPlus.Fonts.OpenType.Utils;
using EPPlus.Graphics;
using EPPlus.Graphics.Math;
using EPPlusImageRenderer.Svg;
@@ -24,6 +25,7 @@ namespace EPPlusImageRenderer.RenderItems
{
internal class SvgRenderLineItem : SvgRenderItem
{
+
public SvgRenderLineItem(DrawingBase renderer, BoundingBox parent) : base(renderer, parent)
{
@@ -92,17 +94,77 @@ private void UpdateBounds()
public override void Render(StringBuilder sb)
{
- sb.AppendFormat("");
+
+ RenderLineItem(sb, BorderWidth, "white", null);
+ RenderLineItem(sb, BorderWidth * (3D / 7D), "black", null);
+ sb.Append($" ");
+ break;
+ case eCompoundLineStyle.DoubleThickThin:
+ WriteThickThin(sb, "double-thick-thin-stroke-{0}", (BorderWidth ?? 1D) * 1D / 7D);
+ break;
+ case eCompoundLineStyle.DoubleThinThick:
+ WriteThickThin(sb, "double-thin-thick-stroke-{0}", ((BorderWidth ?? 1D) * 1D / 7D) * -1);
+ break;
+ case eCompoundLineStyle.TripleThinThickThin:
+ var guid= Guid.NewGuid().ToString();
+ var gapOffset = 5 * BorderWidth.Value / 16;
+ name = $"triple-stroke-{guid}";
+ sb.Append($"");
+ sb.Append($" ");
+ sb.Append($" ");
+ sb.Append($"");
+ RenderLineItem(sb, BorderWidth, "white", null);
+ RenderLineItem(sb, BorderWidth * (1D / 8D), "black", $"filter=\"url(#gap-left-{guid})\"");
+ RenderLineItem(sb, BorderWidth * (1D / 8D), "black", $"filter=\"url(#gap-right-{guid})\"");
+ sb.Append($" ");
+ break;
+ default:
+ RenderLineItem(sb, null, null, null);
+ break;
}
- sb.AppendFormat("/>");
+ }
+ private void WriteThickThin(StringBuilder sb, string name, double gapOffset)
+ {
+ var guid = Guid.NewGuid().ToString();
+ name = string.Format(name, guid);
+ string gapFilterName = $"f-gap-shift-{guid}";
+ sb.Append("");
+ sb.Append($" ");
+ sb.Append($"");
+ RenderLineItem(sb, BorderWidth, "white", null);
+ RenderLineItem(sb, BorderWidth * (1D / 4D), "black", $"filter=\"url(#{gapFilterName})\"");
+ sb.Append($" ");
+ }
+
+ internal string Suffix = "px";
+
+ private void RenderLineItem(StringBuilder sb, double? borderWidth, string color, string filter)
+ {
+ if(Suffix == "%")
+ {
+ sb.AppendFormat("");
+
+ RenderPathItem(sb, BorderWidth, "white", null);
+ RenderPathItem(sb, BorderWidth * (3D / 7D), "black", null);
+ sb.Append($" ");
+ break;
+ case eCompoundLineStyle.DoubleThickThin:
+ WriteThickThin(sb, (BorderWidth??1D) * 1D / 7D);
+ break;
+ case eCompoundLineStyle.DoubleThinThick:
+ WriteThickThin(sb, ((BorderWidth ?? 1D) * 1D / 7D) * -1);
+ break;
+ case eCompoundLineStyle.TripleThinThickThin:
+ var guid = Guid.NewGuid().ToString();
+ var gapOffset = 5 * BorderWidth.Value / 16;
+ name = $"triple-stroke-{guid}";
+ sb.Append($"");
+ sb.Append($" ");
+ sb.Append($" ");
+ sb.Append($"");
+ RenderPathItem(sb, BorderWidth, "white", null);
+ RenderPathItem(sb, BorderWidth * (1D / 8D), "black", $"filter=\"url(#gap-left-{guid})\"");
+ RenderPathItem(sb, BorderWidth * (1D / 8D), "black", $"filter=\"url(#gap-right-{guid})\"");
+ sb.Append($" ");
+ break;
+ }
+ }
+
+ private void WriteThickThin(StringBuilder sb, double gapOffset)
+ {
+ var guid = Guid.NewGuid().ToString();
+ var name = $"double-thick-thin-stroke-{guid}";
+ string gapFilterName = $"f-gap-shift-{guid}";
+ sb.Append("");
+ sb.Append($" ");
+ sb.Append($"");
+ RenderPathItem(sb, BorderWidth, "white", null);
+ RenderPathItem(sb, BorderWidth * (1 / 4D), "black", $"filter=\"url(#{gapFilterName})\"");
+ sb.Append($" ");
+ }
+
+ private void RenderPathItem(StringBuilder sb, double? borderWidth, string color, string filter)
{
var width = Bounds.Width.PointToPixel();
var height = Bounds.Height.PointToPixel();
@@ -45,8 +98,8 @@ public override void Render(StringBuilder sb)
Commands[i].Render(width, height, sb);
}
sb.Append("\" ");
- base.Render(sb);
- sb.Append("/>");
+ RenderCompoundItems(sb, borderWidth, color, filter);
+
}
internal override SvgRenderItem Clone(SvgShape svgDocument)
diff --git a/src/EPPlus.Export.ImageRenderer/RenderItems/SvgRenderRectItem.cs b/src/EPPlus.Export.ImageRenderer/RenderItems/SvgRenderRectItem.cs
index 5c2261dd4b..f3d0b0a9a0 100644
--- a/src/EPPlus.Export.ImageRenderer/RenderItems/SvgRenderRectItem.cs
+++ b/src/EPPlus.Export.ImageRenderer/RenderItems/SvgRenderRectItem.cs
@@ -19,6 +19,7 @@ Date Author Change
using EPPlus.Export.ImageRenderer.Utils;
using OfficeOpenXml.Drawing.Theme;
using EPPlus.Fonts.OpenType.Utils;
+using EPPlus.Fonts.OpenType.Utils;
namespace EPPlusImageRenderer.RenderItems
{
@@ -28,6 +29,7 @@ public SvgRenderRectItem(DrawingBase renderer, BoundingBox parent) : base(render
{
}
+
public double Left { get { return Bounds.Left; } set { Bounds.Left = value; } }
public double Top { get { return Bounds.Top; } set { Bounds.Top = value; } }
public double Width { get { return Bounds.Width; } set { Bounds.Width = value; } }
@@ -50,13 +52,26 @@ public override void Render(StringBuilder sb)
//groupItem.RenderEndGroup(sb);
}
+ internal string Suffix = "px";
+
internal void RenderRect(StringBuilder sb)
{
- sb.AppendFormat(" ");
}
diff --git a/src/EPPlus.Export.ImageRenderer/RenderItems/SvgUseRefItem.cs b/src/EPPlus.Export.ImageRenderer/RenderItems/SvgUseRefItem.cs
new file mode 100644
index 0000000000..d695f22428
--- /dev/null
+++ b/src/EPPlus.Export.ImageRenderer/RenderItems/SvgUseRefItem.cs
@@ -0,0 +1,61 @@
+using EPPlus.Fonts.OpenType.Utils;
+using EPPlus.Graphics;
+using EPPlusImageRenderer;
+using EPPlusImageRenderer.RenderItems;
+using System;
+using System.Collections.Generic;
+using System.Globalization;
+using System.Linq;
+using System.Text;
+
+namespace EPPlus.Export.ImageRenderer.RenderItems
+{
+ internal class SvgUseRefItem : RenderItem
+ {
+ internal string Href { get; private set; } = null;
+
+ internal double X
+ {
+ get
+ {
+ return Bounds.Left;
+ }
+ set
+ {
+ Bounds.Left = value;
+ }
+ }
+
+ internal double Y
+ {
+ get
+ {
+ return Bounds.Top;
+ }
+ set
+ {
+ Bounds.Top = value;
+ }
+ }
+
+ ///
+ /// Any bounds on this property are considered OFFSETS to the original object
+ /// Width and Height ONLY matter if this refers to an or element
+ ///
+ ///
+ internal SvgUseRefItem(DrawingBase renderer, BoundingBox parent, string idForHref) : base(renderer, parent)
+ {
+ Href = "#" + idForHref;
+ }
+
+ public override RenderItemType Type => RenderItemType.Reference;
+
+
+
+ public override void Render(StringBuilder sb)
+ {
+ string renderStr = $"";
+ sb.Append(renderStr);
+ }
+ }
+}
diff --git a/src/EPPlus.Export.ImageRenderer/Style/IFillBasic.cs b/src/EPPlus.Export.ImageRenderer/Style/IFillBasic.cs
new file mode 100644
index 0000000000..b80bb8fef9
--- /dev/null
+++ b/src/EPPlus.Export.ImageRenderer/Style/IFillBasic.cs
@@ -0,0 +1,21 @@
+using OfficeOpenXml.Drawing;
+using OfficeOpenXml.Drawing.Theme;
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+
+namespace EPPlus.Export.ImageRenderer.Style
+{
+ public interface IFillBasic
+ {
+ ///
+ /// Fill type of object
+ ///
+ eFillStyle Style { get; }
+
+ string GetBackgroundColor(ExcelTheme theme);
+ string GetGradientColor1(ExcelTheme theme);
+ string GetGradientColor2(ExcelTheme theme);
+ }
+}
diff --git a/src/EPPlus.Export.ImageRenderer/Style/IStyleExportDrawing.cs b/src/EPPlus.Export.ImageRenderer/Style/IStyleExportDrawing.cs
new file mode 100644
index 0000000000..8407997c2d
--- /dev/null
+++ b/src/EPPlus.Export.ImageRenderer/Style/IStyleExportDrawing.cs
@@ -0,0 +1,16 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+
+namespace EPPlus.Export.ImageRenderer.Style
+{
+ internal interface IStyleExportDrawing
+ {
+ string StyleKey { get; }
+
+ bool HasStyle { get; }
+
+ IFillBasic Fill { get; }
+ }
+}
diff --git a/src/EPPlus.Export.ImageRenderer/Style/SvgChartStyleTranslator.cs b/src/EPPlus.Export.ImageRenderer/Style/SvgChartStyleTranslator.cs
new file mode 100644
index 0000000000..34c760aa80
--- /dev/null
+++ b/src/EPPlus.Export.ImageRenderer/Style/SvgChartStyleTranslator.cs
@@ -0,0 +1,21 @@
+using OfficeOpenXml.Drawing.Chart.Style;
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+
+namespace EPPlus.Export.ImageRenderer.Style
+{
+ internal class SvgChartStyleTranslator : IStyleExportDrawing
+ {
+ public SvgChartStyleTranslator(ExcelChartStyleEntry chartStyle)
+ {
+ var fill = chartStyle.Fill;
+ }
+ public string StyleKey => throw new NotImplementedException();
+
+ public bool HasStyle => throw new NotImplementedException();
+
+ public IFillBasic Fill => throw new NotImplementedException();
+ }
+}
diff --git a/src/EPPlus.Export.ImageRenderer/Style/SvgFill.cs b/src/EPPlus.Export.ImageRenderer/Style/SvgFill.cs
new file mode 100644
index 0000000000..400566b210
--- /dev/null
+++ b/src/EPPlus.Export.ImageRenderer/Style/SvgFill.cs
@@ -0,0 +1,116 @@
+using OfficeOpenXml.Drawing;
+using OfficeOpenXml.Drawing.Theme;
+using OfficeOpenXml.Style.XmlAccess;
+using System;
+using System.Collections.Generic;
+using System.Drawing;
+using System.Globalization;
+using System.Linq;
+using System.Text;
+using OfficeOpenXml.Style;
+using OfficeOpenXml.Utils.EnumUtils;
+
+namespace EPPlus.Export.ImageRenderer.Style
+{
+ internal class SvgFill : IFillBasic
+ {
+ ExcelDrawingFillBasic _fill;
+
+ public SvgFill(ExcelDrawingFillBasic fill)
+ {
+ Style = fill.Style;
+ _fill = fill;
+ }
+
+ public SvgFill(ExcelDrawingFill fill)
+ {
+ Style = fill.Style;
+ _fill = fill;
+ }
+
+ public eFillStyle Style { get; set; }
+
+ public bool HasValue
+ {
+ get
+ {
+ return !_fill.IsEmpty;
+ }
+ }
+
+
+ public bool IsGradient
+ {
+ get
+ {
+ return Style == eFillStyle.GradientFill;
+ }
+ }
+
+ public string GetBackgroundColor(ExcelTheme theme)
+ {
+ return GetColor(_fill.Color, theme);
+ }
+
+ ///
+ /// Gets hexcode color for html as a string
+ ///
+ ///
+ ///
+ internal static string GetColor(Color c, ExcelTheme theme)
+ {
+ //Color ret;
+ //if (!string.IsNullOrEmpty(c.ToColorString()))
+ //{
+ // if (int.TryParse(c.Rgb, NumberStyles.HexNumber, null, out int hex))
+ // {
+ // ret = Color.FromArgb(hex);
+ // }
+ // else
+ // {
+ // ret = Color.Empty;
+ // }
+ //}
+ //else if (c.Theme.HasValue)
+ //{
+ // ret = Utils.TypeConversion.ColorConverter.GetThemeColor(theme, c.Theme.Value);
+ //}
+ //else if (c.Indexed >= 0)
+ //{
+ // ret = theme._wb.Styles.GetIndexedColor(c.Indexed);
+ //}
+ //else
+ //{
+ // //Automatic, set to black.
+ // if (c.Auto)
+ // {
+ // ret = Color.Black;
+ // }
+ // else if (c.Exists)
+ // {
+ // ret = Color.Empty;
+ // }
+ // else
+ // {
+ // return null;
+ // }
+ //}
+ //if (c.Tint != 0)
+ //{
+ // ret = Utils.TypeConversion.ColorConverter.ApplyTint(ret, Convert.ToDouble(c.Tint));
+ //}
+
+ return "#" + c.ToArgb().ToString("x8").Substring(2);
+ }
+
+ public string GetGradientColor1(ExcelTheme theme)
+ {
+ return GetColor(_fill.GradientFill.Colors.ToArray()[0].Color.GetColor(), theme);
+ }
+ public string GetGradientColor2(ExcelTheme theme)
+ {
+ return GetColor(_fill.GradientFill.Colors.ToArray()[1].Color.GetColor(), theme);
+ }
+
+ }
+}
diff --git a/src/EPPlus.Export.ImageRenderer/Style/SvgFillTranslator.cs b/src/EPPlus.Export.ImageRenderer/Style/SvgFillTranslator.cs
new file mode 100644
index 0000000000..634146cd33
--- /dev/null
+++ b/src/EPPlus.Export.ImageRenderer/Style/SvgFillTranslator.cs
@@ -0,0 +1,90 @@
+using OfficeOpenXml.Drawing;
+using OfficeOpenXml.Drawing.Theme;
+using OfficeOpenXml.Export.HtmlExport;
+using OfficeOpenXml.Export.HtmlExport.CssCollections;
+using OfficeOpenXml.Export.HtmlExport.Translators;
+using OfficeOpenXml.Style;
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+namespace EPPlus.Export.ImageRenderer.Style
+{
+ internal class SvgFillTranslator : TranslatorBase
+ {
+ ExcelTheme _theme;
+ IFillBasic _fill;
+
+ internal SvgFillTranslator(IFillBasic fill)
+ {
+ _fill = fill;
+ }
+
+ internal override List GenerateDeclarationList(TranslatorContext context)
+ {
+ _theme = context.Theme;
+
+ if (context.Exclude.Fill) return null;
+
+ if (_fill.Style == eFillStyle.GradientFill)
+ {
+ AddGradient();
+ }
+ else
+ {
+ if (_fill.Style == eFillStyle.PatternFill)
+ {
+ var bc = _fill.GetBackgroundColor(_theme) ?? "#0";
+ if (string.IsNullOrEmpty(bc) == false)
+ {
+ AddDeclaration("fill", bc);
+ }
+ }
+ else if (_fill.Style == eFillStyle.NoFill)
+ {
+ var fc = _fill.GetBackgroundColor(_theme);
+ if (string.IsNullOrEmpty(fc) == false)
+ {
+ AddDeclaration("fill", fc);
+ }
+ }
+ else if(_fill.Style == eFillStyle.BlipFill)
+ {
+ string bgColor = _fill.GetBackgroundColor(_theme) ?? "#0";
+ //string patternColor = _fill.GetPatternColor(_theme) ?? "#0";
+
+ var svg = PatternFills.GetPatternSvgConvertedOnly(ExcelFillStyle.Solid, bgColor, "");
+ AddDeclaration("background-repeat", "repeat");
+ //arguably some of the values should be its own declaration...Should still work though.
+ AddDeclaration("background", $"url(data:image/svg+xml;base64,{svg})");
+ }
+ }
+
+ return declarations;
+ }
+
+ private void AddGradient()
+ {
+ //AddDeclaration("linearGradient");
+
+ //GetLinearGradientNodeStop
+ //AddDeclaration("stop");
+ //var gradientDeclaration = declarations.LastOrDefault();
+
+ //if (_fill.Style == eFillStyle.GradientFill)
+ //{
+ // AddDeclaration("offset",)
+ //}
+ //else
+ //{
+ // gradientDeclaration.AddValues($"radial-gradient(ellipse {_fill.Right * 100}% {_fill.Bottom * 100}%");
+ //}
+
+ //gradientDeclaration.AddValues
+ // (
+ // $",{_fill.GetGradientColor1(_theme)} 0%",
+ // $",{_fill.GetGradientColor2(_theme)} 100%)"
+ // );
+ }
+ }
+}
diff --git a/src/EPPlus.Export.ImageRenderer/Svg/Chart/Axis/AxisOptions.cs b/src/EPPlus.Export.ImageRenderer/Svg/Chart/Axis/AxisOptions.cs
index 04abed0621..8ac618a85c 100644
--- a/src/EPPlus.Export.ImageRenderer/Svg/Chart/Axis/AxisOptions.cs
+++ b/src/EPPlus.Export.ImageRenderer/Svg/Chart/Axis/AxisOptions.cs
@@ -1,4 +1,6 @@
-using OfficeOpenXml.Drawing.Chart;
+using EPPlusImageRenderer.RenderItems;
+using OfficeOpenXml.Drawing.Chart;
+using System.Drawing;
internal class AxisOptions
{
@@ -9,4 +11,6 @@ internal class AxisOptions
public bool AddPadding { get; set; } = false;
public ExcelChartAxisStandard Axis { get; set; }
public bool IsStacked100 { get; set; }
+ public RenderItem ChartSize { get; set; }
+ public string NumberFormat { get; set; }
}
\ No newline at end of file
diff --git a/src/EPPlus.Export.ImageRenderer/Svg/Chart/Axis/AxisScale.cs b/src/EPPlus.Export.ImageRenderer/Svg/Chart/Axis/AxisScale.cs
index bec7c0a1c5..a19449ab8c 100644
--- a/src/EPPlus.Export.ImageRenderer/Svg/Chart/Axis/AxisScale.cs
+++ b/src/EPPlus.Export.ImageRenderer/Svg/Chart/Axis/AxisScale.cs
@@ -1,4 +1,5 @@
-using OfficeOpenXml.Drawing.Chart;
+using EPPlus.Export.ImageRenderer;
+using OfficeOpenXml.Drawing.Chart;
internal class AxisScale
{
@@ -9,4 +10,5 @@ internal class AxisScale
public int TickCount { get; set; }
public eTimeUnit? MajorDateUnit { get; set; }
public eTimeUnit? MinorDateUnit { get; set; }
+ public eTextOrientation TextOrientation { get; set; }
}
\ No newline at end of file
diff --git a/src/EPPlus.Export.ImageRenderer/Svg/Chart/Axis/CategoryAxisScaleCalculator.cs b/src/EPPlus.Export.ImageRenderer/Svg/Chart/Axis/CategoryAxisScaleCalculator.cs
new file mode 100644
index 0000000000..e7d8bcc1f4
--- /dev/null
+++ b/src/EPPlus.Export.ImageRenderer/Svg/Chart/Axis/CategoryAxisScaleCalculator.cs
@@ -0,0 +1,144 @@
+using EPPlusImageRenderer.Svg;
+using OfficeOpenXml;
+using OfficeOpenXml.DataValidation;
+using OfficeOpenXml.Drawing.Chart;
+using OfficeOpenXml.FormulaParsing.Excel.Functions.Information;
+using OfficeOpenXml.FormulaParsing.Excel.Functions.MathFunctions;
+using OfficeOpenXml.Interfaces.Drawing.Text;
+using OfficeOpenXml.Utils.DateUtils;
+using System;
+using System.Collections.Generic;
+using System.Drawing;
+using System.Linq;
+
+namespace EPPlus.Export.ImageRenderer.Svg.Chart.Util
+{
+ internal class CategoryAxisScaleCalculator
+ {
+
+ internal static AxisScale CalculateByWidth(ref List values, ITextMeasurer tm, AxisOptions options)
+ {
+ //var height = options.ChartSize.Bounds.Height;
+ var ax = options.Axis;
+ var plotAreaWidth = options.ChartSize.Bounds.Width;
+ var mf = ax.Font.GetMeasureFont();
+ var displayValues = GetUniqueValues(values).Select(x=>x.ToString()).ToList();
+ var res = tm.MeasureText(displayValues[0].ToString(), mf);
+
+ //Get interval for maximum width with vertical text.
+ var interval = GetMinUnitVerticalText(displayValues, res.Height, plotAreaWidth);
+
+
+ //Get max text width when using diagonal text
+ var width = mf.Size * Math.Sqrt(2);
+ var margin = mf.Size * 0.5;
+
+ if (FitAsVerticalDiagonalText(displayValues, interval, width, margin, plotAreaWidth)) //Check diagonal
+ {
+ if(FitAsHorizontalText(displayValues, interval, mf, tm, plotAreaWidth)) //Check horizontal
+ {
+ return new AxisScale()
+ {
+ MajorInterval = interval,
+ MinorInterval = 1,
+ Min = 1,
+ Max = displayValues.Count,
+ TextOrientation = eTextOrientation.Horizontal
+ };
+ }
+ else
+ {
+ return new AxisScale()
+ {
+ MajorInterval = interval,
+ MinorInterval = 1,
+ Min = 1,
+ Max = displayValues.Count,
+ TextOrientation = eTextOrientation.Diagonal
+ };
+ }
+ }
+ if(interval!=1)
+ {
+ var removeCount = interval - 1;
+ var c = (int)Math.Truncate(values.Count / (double)interval);
+ for(int i=0;i<=c;i++)
+ {
+ for(int j=0;j GetUniqueValues(List values)
+ {
+ var ret = new List();
+ var hs = new HashSet();
+ foreach(var v in values)
+ {
+ if(v is string[])
+ {
+ var s = (string[])v;
+ var key = s[0] + s[1];
+ if(hs.Add(key))
+ {
+ ret.Add(s[0]);
+ }
+ }
+ else
+ {
+ ret.Add(v);
+ }
+ }
+ return ret;
+ }
+
+ private static bool FitAsHorizontalText(List displayValues, int interval, MeasurementFont mf, ITextMeasurer tm, double plotAreaWidth)
+ {
+ var margin = mf.Size * 0.3;
+ var width = tm.MeasureText(displayValues[0], mf).Width + margin;
+ var pos = interval;
+ while (pos < displayValues.Count && width < plotAreaWidth)
+ {
+ width = tm.MeasureText(displayValues[pos], mf).Width + margin;
+ if(width > plotAreaWidth) return false;
+ pos += interval;
+ }
+ return width <= plotAreaWidth;
+ }
+
+ private static bool FitAsVerticalDiagonalText(List displayValues, int interval, double textWidth, double margin, double plotAreaWidth)
+ {
+ var items = Math.Truncate(displayValues.Count * 1D / interval);
+ return items * textWidth + (items - 1) * margin < plotAreaWidth;
+ }
+
+ private static int GetMinUnitVerticalText(List displayValues, double textHeight, double plotAreaWidth)
+ {
+ var interval = 1;
+ var margin = 0D;
+ var items = Math.Truncate((double)displayValues.Count / interval);
+ while (items * textHeight + (items - 1) * margin >= plotAreaWidth)
+ {
+ interval++;
+ items = Math.Truncate((double)displayValues.Count / interval);
+ }
+
+ return interval;
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/EPPlus.Export.ImageRenderer/Svg/Chart/Axis/DateAxisScaleCalculator.cs b/src/EPPlus.Export.ImageRenderer/Svg/Chart/Axis/DateAxisScaleCalculator.cs
index f49adda98c..429c184133 100644
--- a/src/EPPlus.Export.ImageRenderer/Svg/Chart/Axis/DateAxisScaleCalculator.cs
+++ b/src/EPPlus.Export.ImageRenderer/Svg/Chart/Axis/DateAxisScaleCalculator.cs
@@ -1,7 +1,12 @@
-using OfficeOpenXml.Drawing.Chart;
+using OfficeOpenXml;
+using OfficeOpenXml.Drawing.Chart;
+using OfficeOpenXml.FormulaParsing.Excel.Functions.Information;
+using OfficeOpenXml.FormulaParsing.Excel.Functions.MathFunctions;
+using OfficeOpenXml.Interfaces.Drawing.Text;
using OfficeOpenXml.Utils.DateUtils;
using System;
using System.Collections.Generic;
+using System.Drawing;
using System.Linq;
namespace EPPlus.Export.ImageRenderer.Svg.Chart.Util
@@ -15,99 +20,509 @@ internal class DateAxisScaleCalculator
///
internal static AxisScale Calculate(double dataMin, double dataMax, double chartHeightPixels, AxisOptions axisOptions = null)
{
+
+ ComputeAutoAxis(DateTime.FromOADate(dataMin), DateTime.FromOADate(dataMax), axisOptions.AddPadding);
var range = dataMax - dataMin;
// Calculate major majorUnit based on range
- CalculateMajorUnit(dataMin, dataMax, out double majorValue, out double minorUnit, out eTimeUnit majorUnit);
+ CalculateMajorUnit(axisOptions.LockedMin ?? dataMin, axisOptions.LockedMax ?? dataMax, axisOptions.AddPadding, out double majorValue, out double axisMin, out double axisMax, out double minorUnit, out eTimeUnit majorUnit);
+ if(axisOptions.LockedInterval.HasValue)
+ {
+ majorValue = axisOptions.LockedInterval.Value;
+ }
return new AxisScale
{
- Min = dataMin,
- Max = dataMax,
+ Min = axisMin,
+ Max = axisMax,
MajorInterval = majorValue,
MajorDateUnit = majorUnit,
MinorDateUnit = majorUnit
};
}
+ // Excel's date serial epoch: Dec 30, 1899
+ private static readonly DateTime ExcelEpoch = new DateTime(1899, 12, 30);
+
+ // Excel legacy leap year bug: dates after Feb 28, 1900 are offset by 1
+ private static readonly DateTime ExcelLeapBugThreshold = new DateTime(1900, 3, 1);
+ // Excel's "nice" interval ladder in days
+ private static readonly int[] NiceIntervals = { 1, 2, 5, 7, 14, 30, 91, 182, 365, 730 };
+ private const int TargetTicks = 11; // Excel typically targets 10–12 ticks
+ public class AxisResult
+ {
+ public DateTime AxisMin { get; set; }
+ public DateTime AxisMax { get; set; }
+ public int IntervalDays { get; set; }
+ public double AxisMinSerial { get; set; }
+ public double AxisMaxSerial { get; set; }
+ public int TickCount => (int)Math.Round((AxisMaxSerial - AxisMinSerial) / IntervalDays) + 1;
+
+ public override string ToString() =>
+ $"Axis Min : {AxisMin:yyyy-MM-dd} (serial {AxisMinSerial})\n" +
+ $"Axis Max : {AxisMax:yyyy-MM-dd} (serial {AxisMaxSerial})\n" +
+ $"Interval : {IntervalDays} day(s)\n" +
+ $"Ticks : {TickCount}";
+ }
+ ///
+ /// Converts a DateTime to an Excel serial number (days since Dec 30, 1899),
+ /// accounting for Excel's 1900 leap year bug.
+ ///
+ public static double ToExcelSerial(DateTime date)
+ {
+ double serial = (date - ExcelEpoch).TotalDays;
+ if (date >= ExcelLeapBugThreshold)
+ serial += 1; // Excel's legacy off-by-one
+ return serial;
+ }
+
+ ///
+ /// Converts an Excel serial number back to a DateTime.
+ ///
+ public static DateTime FromExcelSerial(double serial)
+ {
+ if (serial >= 61) // 61 = the phantom Feb 29, 1900 in Excel
+ serial -= 1;
+ return ExcelEpoch.AddDays(serial);
+ }
+ ///
+ /// Computes Excel-compatible auto axis min, max, and major unit for a date axis.
+ /// If the OOXML already contains explicit min/max values, use those directly instead.
+ ///
+ public static AxisResult ComputeAutoAxis(DateTime dataMin, DateTime dataMax, bool snapInterval)
+ {
+ double serialMin = ToExcelSerial(dataMin);
+ double serialMax = ToExcelSerial(dataMax);
+
+ // Step 1: pick a nice major unit
+ double rawInterval = (serialMax - serialMin) / 10.0;
+ int interval = NiceIntervals.FirstOrDefault(v => v >= rawInterval);
+ if (interval == 0)
+ interval = 730; // fallback for very large ranges
+
+ double axisMin = serialMin;
+ double axisMax = serialMax;
+ if (snapInterval)
+ {
+ // Step 2: snap outward to multiples of the interval
+ axisMin = Math.Floor(serialMin / interval) * interval;
+ axisMax = Math.Ceiling(serialMax / interval) * interval;
+ }
+ else
+ {
+ axisMin = serialMin;
+ axisMax = serialMax;
+ }
+ // Step 3: expand until we have enough ticks, then re-snap
+ while ((axisMax - axisMin) / interval < TargetTicks - 1)
+ {
+ axisMin -= interval;
+ axisMax += interval;
+ }
+
+ axisMin = Math.Floor(axisMin / interval) * interval;
+ axisMax = Math.Ceiling(axisMax / interval) * interval;
+
+ return new AxisResult
+ {
+ AxisMin = FromExcelSerial(axisMin),
+ AxisMax = FromExcelSerial(axisMax),
+ IntervalDays = interval,
+ AxisMinSerial = axisMin,
+ AxisMaxSerial = axisMax,
+ };
+ }
///
/// Calculate major majorUnit based on data range (in days)
///
- private static void CalculateMajorUnit(double min, double max,out double majorValue, out double minorUnits, out eTimeUnit majorUnit)
+ private static void CalculateMajorUnit(double min, double max, bool snapToInterval, out double majorValue, out double axisMin, out double axisMax, out double minorUnits, out eTimeUnit majorUnit)
{
var dtMin = DateTime.FromOADate(ExcelNormalizeOADate(min));
var dtMax = DateTime.FromOADate(ExcelNormalizeOADate(max));
var rangeDays = (dtMax - dtMin).TotalDays;
- // Target approximately 6 intervals
- const int targetIntervals = 6;
+ // Target approximately 10 intervals
+ const int targetIntervals = 9;
var roughUnit = rangeDays / targetIntervals;
-
+ double interval;
// Return nice round numbers based on the rough majorUnit
- if (rangeDays <= 1)
+ if (roughUnit < 1)
{
- majorValue = 1;
+ interval = majorValue = 1;
+ majorUnit = eTimeUnit.Days;
+ }
+ else if (roughUnit < 2)
+ {
+ interval = majorValue = 2;
+ majorUnit = eTimeUnit.Days;
+ }
+ else if (roughUnit < 5)
+ {
+ // Multiple days - round to nearest day
+ interval = majorValue = 5;
majorUnit = eTimeUnit.Days;
}
else if (roughUnit < 7)
{
// Multiple days - round to nearest day
- majorValue = Math.Max(1, Math.Round(roughUnit));
+ interval = majorValue = 7;
majorUnit = eTimeUnit.Days;
}
else if (roughUnit < 14)
{
- majorValue = 7;
+ interval = majorValue = 14;
majorUnit = eTimeUnit.Days;
}
else if (roughUnit < 21)
{
- majorValue = 14;
+ interval = majorValue = 21;
majorUnit = eTimeUnit.Days;
}
else if (roughUnit < 60)
{
majorValue = 1;
+ interval = 30;
majorUnit = eTimeUnit.Months;
}
else if (roughUnit < 120)
{
majorValue = 2;
+ interval = 60;
majorUnit = eTimeUnit.Months;
}
else if (roughUnit < 180)
{
majorValue = 3;
+ interval = 90;
majorUnit = eTimeUnit.Months;
}
else if (roughUnit < 365)
{
majorValue = 6;
+ interval = 180;
majorUnit = eTimeUnit.Months;
}
else if (roughUnit < 730)
{
majorValue = 1;
+ interval = 365;
majorUnit = eTimeUnit.Years;
}
else if (roughUnit < 1825)
{
// Multiple years
majorValue = Math.Ceiling(roughUnit / 365);
+ interval = 365 * majorValue;
majorUnit = eTimeUnit.Years;
}
else
{
// 5-year increments for very long ranges
majorValue = Math.Ceiling(roughUnit / 365 / 5) * 5;
+ interval = 365 * majorValue;
majorUnit = eTimeUnit.Years;
}
- minorUnits = 1;
+
+ if (snapToInterval)
+ {
+ axisMin = Math.Floor(min / interval) * interval;
+ axisMax = Math.Ceiling(max / interval) * interval;
+
+ if (axisMax == max)
+ {
+ axisMax += interval;
+ }
+ // Step 3: expand until we have enough ticks, then re-snap
+ while ((axisMax - axisMin) / interval < TargetTicks - 1 && ((min - axisMin) < interval * 2))
+ {
+ axisMin -= majorValue;
+ }
+ }
+ else
+ {
+ axisMin= min;
+ axisMax = max;
+ }
+
+ minorUnits = 1;
}
private static double ExcelNormalizeOADate(double value)
{
return Math.Min(Math.Max(value, 0), 2958465.99999999);
}
+
+ private static DateTime FloorToUnit(double value, eTimeUnit unit, int interval)
+ {
+ var date = DateTime.FromOADate(value);
+ switch (unit)
+ {
+ case eTimeUnit.Days:
+ if(interval % 7 != 0)
+ {
+ return date.Date.AddDays(-(((int)date.DayOfWeek + 6) % 7));
+ }
+ else
+ {
+ return date;
+ }
+ //return date.Date.AddDays(-(date.DayOfYear % interval));
+ case eTimeUnit.Months:
+ return new DateTime(date.Year, date.Month, 1)
+ .AddMonths(-((date.Month - 1) % interval));
+
+ case eTimeUnit.Years:
+ return new DateTime(date.Year - (date.Year % interval), 1, 1);
+ default:
+ return date;
+ };
+ }
+
+ private static DateTime CeilingToUnit(double value, eTimeUnit unit, int interval)
+ {
+ DateTime floor = FloorToUnit(value, unit, interval);
+
+ var date = DateTime.FromOADate(value);
+ if (floor == date) return date; // already on boundary
+
+ switch (unit)
+ {
+ case eTimeUnit.Days:
+ return floor.AddDays(interval);
+ case eTimeUnit.Months:
+ return floor.AddMonths(interval);
+ case eTimeUnit.Years:
+ return floor.AddYears(interval);
+ default:
+ return date;
+ };
+ }
+
+ internal static AxisScale CalculateByWidth(double min, double max, ITextMeasurer tm, AxisOptions options)
+ {
+ var ax = options.Axis;
+ var plotAreaWidth = options.ChartSize.Bounds.Width;
+ var mf = ax.Font.GetMeasureFont();
+ int interval;
+ eTimeUnit unit;
+ var minString = DateTime.FromOADate(min).ToString(options.NumberFormat);
+ var res = tm.MeasureText(minString, mf);
+ if (options.LockedInterval.HasValue)
+ {
+ interval = (int)options.LockedInterval.Value;
+ unit = options.LockedIntervalUnit ?? eTimeUnit.Days;
+ }
+ else
+ {
+ interval = 1;
+ unit = eTimeUnit.Days;
+ //Get interval for maximum width with vertical text.
+ while (FitAsVerticalDiagonalText(min, max, interval, unit, res.Height, res.Height * 0.3, plotAreaWidth) == false)
+ {
+ AddIntervall(ref interval, ref unit);
+ }
+ //Get max text width when using diagonal text
+ var width = mf.Size * Math.Sqrt(2);
+ var margin = mf.Size * 0.5;
+
+ if (FitAsVerticalDiagonalText(min, max, interval, unit, width, margin, plotAreaWidth)) //Check diagonal
+ {
+ if (FitAsHorizontalText(tm, options, min, max, interval, unit, res.Height, plotAreaWidth)) //Check horizontal
+ {
+ return new AxisScale()
+ {
+ MajorInterval = interval,
+ MinorInterval = 1,
+ MinorDateUnit = unit,
+ MajorDateUnit = unit,
+ Min = min,
+ Max = max,
+ TextOrientation = eTextOrientation.Horizontal
+ };
+ }
+ else
+ {
+ return new AxisScale()
+ {
+ MajorInterval = interval,
+ MinorInterval = 1,
+ MinorDateUnit = unit,
+ MajorDateUnit = unit,
+ Min = min,
+ Max = max,
+ TextOrientation = eTextOrientation.Diagonal
+ };
+ }
+ }
+
+ }
+
+ return new AxisScale()
+ {
+ MajorInterval = interval,
+ MinorInterval = 1,
+ MinorDateUnit = unit,
+ MajorDateUnit = unit,
+ Min = min,
+ Max = max,
+ TextOrientation = ax.TextBody.VerticalText==OfficeOpenXml.Drawing.eTextVerticalType.Horizontal ? eTextOrientation.Horizontal : eTextOrientation.Vertical
+ };
+ }
+
+ //private static bool FitAsDiagonalText(double min, double max, int interval, eTimeUnit unit, float size, double plotAreaWidth)
+ //{
+ // var margin = size * 0.5;
+ // var width = size * Math.Sqrt(2) + margin;
+
+ // if (unit == eTimeUnit.Days)
+ // {
+ // return ((max - min) / interval) * width < plotAreaWidth;
+ // }
+ // else if(unit == eTimeUnit.Months)
+ // {
+ // w
+ // }
+ //}
+
+ private static void AddIntervall(ref int interval, ref eTimeUnit unit)
+ {
+ if (unit == eTimeUnit.Days)
+ {
+ switch (interval)
+ {
+ case 1:
+ interval = 2;
+ break;
+ case 2:
+ interval = 7; //Week
+ break;
+ case 7:
+ interval = 1; //Month
+ unit = eTimeUnit.Months;
+ break;
+ default:
+ interval *= 10;
+ break;
+ }
+ }
+ else if (unit == eTimeUnit.Months)
+ {
+ unit = eTimeUnit.Years;
+ }
+ else if (unit == eTimeUnit.Years)
+ {
+ interval++;
+ }
+ }
+
+ private static bool FitAsHorizontalText(ITextMeasurer tm, AxisOptions options, double min, double max, int interval, eTimeUnit unit, float height, double width)
+ {
+ var minMargin = 2; //2 Points
+ var minDate = DateTime.FromOADate(min);
+ var maxDate = DateTime.FromOADate(max);
+ var date = minDate;
+ var horizontalWidth = 0D;
+ var nf = options.NumberFormat;
+ var mf = options.Axis.Font.GetMeasureFont();
+ var angMult = Math.Sin(MathHelper.Radians(45));
+ while (date < maxDate)
+ {
+ var textWidth = tm.MeasureText(date.ToString(), mf).Width;
+ horizontalWidth += textWidth + minMargin;
+ if(horizontalWidth > width)
+ {
+ return false;
+ }
+ if (unit == eTimeUnit.Days)
+ {
+ date = date.AddDays(interval);
+ }
+ else if(unit == eTimeUnit.Months)
+ {
+ date = minDate.AddDays(1).AddMonths(interval).AddDays(-1); //Extra adds to keep last day of month
+ }
+ else
+ {
+ date = minDate.AddDays(1);
+ }
+ }
+ return true;
+ }
+
+ private static bool FitAsVerticalDiagonalText(double min, double max, int interval,eTimeUnit unit, double textWidth, double margin, double plotAreaWidth)
+ {
+ if (unit == eTimeUnit.Days)
+ {
+ var items = ((max - min) / interval);
+ return items * textWidth + (items-1) * margin < plotAreaWidth;
+ }
+ else if(unit== eTimeUnit.Months)
+ {
+ var minDate = DateTime.FromOADate(min);
+ var maxDate = DateTime.FromOADate(max);
+ var date = minDate.AddDays(1).AddMonths(interval).AddDays(-1); //Handles last day of month.
+ var items = 1;
+ while(date <= maxDate)
+ {
+ items++;
+ if (items * textWidth + (items - 1) * margin > plotAreaWidth) return false;
+ date = date.AddDays(1).AddMonths(interval).AddDays(-1);
+ }
+ return items * textWidth + (items - 1) * margin < plotAreaWidth;
+ }
+ else
+ {
+ var minDate = DateTime.FromOADate(min);
+ var maxDate = DateTime.FromOADate(max);
+ var date = minDate.AddDays(1).AddYears(interval).AddDays(-1); //Handles leap year
+ var items = 1;
+ while (date <= maxDate)
+ {
+ items++;
+ if (items * textWidth + (items - 1) * margin > plotAreaWidth) return false;
+ date = date.AddDays(1).AddYears(interval).AddDays(-1);
+ }
+ return items * textWidth + (items - 1) * margin < plotAreaWidth;
+
+ }
+ }
+
+ internal static AxisScale CalculateByHeight(double min, double max, ITextMeasurer tm, AxisOptions options)
+ {
+ var ax = options.Axis;
+ var plotAreaHeight = options.ChartSize.Bounds.Height;
+ var mf = ax.Font.GetMeasureFont();
+ int interval;
+ eTimeUnit unit;
+ var minString = DateTime.FromOADate(min).ToString(options.NumberFormat);
+ var res = tm.MeasureText(minString, mf);
+ if (options.LockedInterval.HasValue)
+ {
+ interval = (int)options.LockedInterval.Value;
+ unit = options.LockedIntervalUnit ?? eTimeUnit.Days;
+ }
+ else
+ {
+ interval = 1;
+ unit = eTimeUnit.Days;
+ //Get interval for maximum width with vertical text.
+ while(FitAsVerticalDiagonalText(min, max, interval, unit, res.Height, res.Height * 0.3, plotAreaHeight) == false)
+ {
+ AddIntervall(ref interval, ref unit);
+ }
+ }
+
+ return new AxisScale()
+ {
+ MajorInterval = interval,
+ MinorInterval = 1,
+ MinorDateUnit = unit,
+ MajorDateUnit = unit,
+ Min = min,
+ Max = max,
+ TextOrientation = eTextOrientation.Horizontal
+ };
+
+ }
}
}
\ No newline at end of file
diff --git a/src/EPPlus.Export.ImageRenderer/Svg/Chart/Axis/ValueAxisScaleCalculator.cs b/src/EPPlus.Export.ImageRenderer/Svg/Chart/Axis/ValueAxisScaleCalculator.cs
index ead28dc099..ff739ae892 100644
--- a/src/EPPlus.Export.ImageRenderer/Svg/Chart/Axis/ValueAxisScaleCalculator.cs
+++ b/src/EPPlus.Export.ImageRenderer/Svg/Chart/Axis/ValueAxisScaleCalculator.cs
@@ -34,7 +34,7 @@ private static AxisScale GetNumberScale(ref double dataMin, ref double dataMax,
}
var isAllPositive = dataMin >= 0 && dataMax >= 0;
- var isAllNegativ = dataMin <= 0 && dataMax <= 0;
+ var isAllNegative = dataMin <= 0 && dataMax <= 0;
if(dataMin < 0 && dataMax > 0 && axisOptions.IsStacked100)
{
desiredTicks *= 2;
@@ -78,7 +78,7 @@ private static AxisScale GetNumberScale(ref double dataMin, ref double dataMax,
if (axisOptions.AddPadding)
{
dataMax = axisOptions.LockedMax.Value;
- if (isAllNegativ && dataMax > 0)
+ if (isAllNegative && dataMax > 0)
{
dataMax = 0;
}
@@ -105,6 +105,11 @@ private static AxisScale GetNumberScale(ref double dataMin, ref double dataMax,
dataMax = axisOptions.LockedMax.Value;
}
+ if (axisOptions.LockedMin.HasValue==false && dataMin / dataMax > 0.666666)
+ {
+ dataMin = 0;
+ }
+
double dataRange = dataMax - dataMin;
double axisMin;
@@ -121,10 +126,17 @@ private static AxisScale GetNumberScale(ref double dataMin, ref double dataMax,
}
else
{
- axisMin = dataMin - (dataRange * 0.1);
- if (axisOptions.IsStacked100 && axisMin < -1)
+ axisMin = dataMin - (dataRange * 0.05);
+ if (axisOptions.IsStacked100)
{
- axisMin = -1;
+ if (axisMin < -1)
+ {
+ axisMin = -1;
+ }
+ else if(axisMin>0)
+ {
+ axisMin = 0;
+ }
}
//Normalize to zero if all data is positive or negative
if (axisMin < 0 && isAllPositive)
@@ -139,13 +151,13 @@ private static AxisScale GetNumberScale(ref double dataMin, ref double dataMax,
}
else
{
- axisMax = dataMax + (dataRange * 0.1);
+ axisMax = dataMax + (dataRange * 0.05);
if (axisOptions.IsStacked100 && axisMax > 1)
{
axisMax = 1;
}
- if (axisMax > 0 && isAllNegativ)
+ if (axisMax > 0 && isAllNegative)
{
axisMax = 0;
}
@@ -160,11 +172,19 @@ private static AxisScale GetNumberScale(ref double dataMin, ref double dataMax,
}
else
{
- // Calculate interval
roughInterval = (dataMax - dataMin) / desiredTicks;
scaleInterval = GetScaleNumber(roughInterval, true);
+ if (dataMin / dataMax >= 0.666666666)
+ {
+ axisMin = 0;
+ }
+ else
+ {
+ axisMin = Math.Floor(dataMin / scaleInterval) * scaleInterval;
+ }
+
+ // Calculate interval
- axisMin = dataMin;
axisMax = dataMax;
}
diff --git a/src/EPPlus.Export.ImageRenderer/Svg/Chart/ChartTypeDrawers/BarColumnChartTypeDrawer.cs b/src/EPPlus.Export.ImageRenderer/Svg/Chart/ChartTypeDrawers/BarColumnChartTypeDrawer.cs
new file mode 100644
index 0000000000..7f33f9250f
--- /dev/null
+++ b/src/EPPlus.Export.ImageRenderer/Svg/Chart/ChartTypeDrawers/BarColumnChartTypeDrawer.cs
@@ -0,0 +1,346 @@
+using EPPlus.Export.ImageRenderer.RenderItems.SvgItem;
+using EPPlus.Graphics;
+using EPPlusImageRenderer.RenderItems;
+using EPPlusImageRenderer.Svg;
+using OfficeOpenXml.Drawing.Chart;
+using OfficeOpenXml.FormulaParsing.Excel.Functions.Finance;
+using OfficeOpenXml.FormulaParsing.Excel.Functions.RefAndLookup;
+using OfficeOpenXml.Utils.TypeConversion;
+using System;
+using System.Collections.Generic;
+using System.Linq;
+
+namespace EPPlus.Export.ImageRenderer.Svg.Chart
+{
+ internal class BarColumnChartTypeDrawer : ChartTypeDrawer
+ {
+ List serieDataLabels = new List();
+ List> dataPointsPerSerie = new List>();
+
+ internal BarColumnChartTypeDrawer(SvgChart svgChart, ExcelBarChart chartType) : base(svgChart, chartType)
+ {
+ var groupItem = new SvgGroupItem(ChartRenderer, _svgChart.Plotarea.Rectangle.Bounds);
+ RenderItems.Add(groupItem);
+ var catValues = new List>();
+ var valValues = new List>();
+ int serCounter = 0;
+
+ foreach (ExcelBarChartSerie serie in chartType.Series)
+ {
+ List valValue,catValue;
+ //if (chartType.IsTypeColumn())
+ //{
+ valValue = LoadSeriesValues(serie.Series, serie.NumberLiteralsY, serie.StringLiteralsY);
+ catValue = LoadSeriesValues(serie.XSeries, serie.NumberLiteralsX, serie.StringLiteralsX);
+ //}
+ //else
+ //{
+ // catValue = LoadSeriesValues(serie.Series, serie.NumberLiteralsY, serie.StringLiteralsY);
+ // valValue = LoadSeriesValues(serie.XSeries, serie.NumberLiteralsX, serie.StringLiteralsX);
+ //}
+
+ catValues.Add(catValue);
+ valValues.Add(valValue);
+
+ //if (serie.HasDataLabel)
+ //{
+ // var datalabel = new SvgChartSerieDataLabel(svgChart, serie.DataLabel, svgChart.Bounds, serie, catValue, valValue, serCounter);
+ // serieDataLabels.Add(datalabel);
+ //}
+ serCounter++;
+ }
+
+ if(chartType.IsTypeStacked())
+ {
+ SumSeries(valValues);
+ }
+ else if (chartType.IsTypePercentStacked())
+ {
+ ExcelChartAxisStandard.CalculateStacked100(valValues);
+ }
+
+ var count = Math.Min(catValues.Count, valValues.Count);
+ for (var i= 0; i < catValues.Count; i++)
+ {
+ var serie = (ExcelBarChartSerie)chartType.Series[i];
+
+ var dataPoints = new List();
+
+ //Add the bar or column.
+ AddBar(chartType, serie, catValues, valValues, dataPoints, count, i);
+
+ dataPointsPerSerie.Add(dataPoints);
+
+ //if (serie.HasDataLabel)
+ //{
+ // for (int j = 0; j < dataPoints.Count; j++)
+ // {
+ // serieDataLabels[i].SetParentPoint(dataPoints[j], j);
+ // }
+ //}
+ }
+ RenderItems.Add(new SvgEndGroupItem(ChartRenderer, null));
+
+ foreach (var dataLabel in serieDataLabels)
+ {
+ dataLabel.AppendRenderItems(RenderItems);
+ }
+ }
+ private void AddBar(ExcelBarChart chartType, ExcelBarChartSerie serie, List> catSeries, List> valSeries, List dataPoints, int seriesCount, int position)
+ {
+ GetAxis(chartType, out var yAxis, out var xAxis);
+ SvgChartAxis valAx, catAx;
+
+ var isColumn = chartType.IsTypeColumn();
+
+ if (isColumn)
+ {
+ catAx = xAxis;
+ valAx = yAxis;
+ }
+ else
+ {
+ catAx = yAxis;
+ valAx = xAxis;
+ }
+
+
+ var catValues = catSeries[position];
+ var valValues = valSeries[position];
+
+ var yWidth = (isColumn ? _svgChart.Plotarea.Rectangle.Width : _svgChart.Plotarea.Rectangle.Height);
+
+ var slotSize = valValues.Count;
+ var gapPercent = chartType.GapWidth / 100D; //Gap width between bars/columns in percent
+ var overlapPercent = chartType.Overlap / 100D; //Overlap between bars/columns in percent
+ var slotWidth = yWidth / slotSize;
+ var clusterWidth = slotWidth * 100 / (100 + chartType.GapWidth);
+ var step = 1 - overlapPercent;
+ var barWidth = slotWidth / (1 + (seriesCount - 1) * step + gapPercent);
+ var halfGap = (barWidth * gapPercent) / 2;
+
+ double yAxisStart;
+ if (yAxis.Axis.Crosses == eCrosses.AutoZero)
+ {
+ yAxisStart = valAx.GetPositionInPlotarea(valAx.Min <= 0 ? 0D : yAxis.Min, true);
+ }
+ else if (yAxis.Axis.Crosses == eCrosses.Min)
+ {
+ yAxisStart = valAx.GetPositionInPlotarea(valAx.Min, true);
+ }
+ else
+ {
+ yAxisStart = valAx.GetPositionInPlotarea(valAx.Max, true);
+ }
+
+ var isStacked = chartType.IsTypeStacked();
+ var isStacked100 = chartType.IsTypePercentStacked();
+ for (var i = 0; i < valValues.Count; i++)
+ {
+ double x;
+ if (catValues == null || catAx.Axis.AxisType == eAxisType.Cat)
+ {
+ x = (double)i;
+ }
+ else
+ {
+ x = ConvertUtil.GetValueDouble(catValues[i], false, true);
+ }
+
+ var y = ConvertUtil.GetValueDouble(valValues[i], false, true);
+
+ var xPos = catAx.GetPositionInPlotarea(x, true) + halfGap + position * barWidth * step;
+ var yPos = valAx.GetPositionInPlotarea(y);
+
+ var rect = new SvgRenderRectItem(ChartRenderer, _svgChart.Plotarea.Rectangle.Bounds);
+ if (isColumn)
+ {
+ rect.Left = xPos;
+ rect.Width = barWidth;
+ }
+ else
+ {
+ rect.Top = yWidth - xPos - barWidth;
+ rect.Height = barWidth;
+ }
+
+ if (position > 0 && (isStacked || isStacked100))
+ {
+ //Stacked
+ var pYValues = valSeries[position - 1];
+ var pAxisEnd = ConvertUtil.GetValueDouble(pYValues[i]);
+ var yPrevPos = yAxis.GetPositionInPlotarea(pAxisEnd, false);
+ if (isColumn)
+ {
+ if (y < 0)
+ {
+ rect.Top = yPrevPos;
+ rect.Height = yPos - yPrevPos;
+ }
+ else
+ {
+ rect.Top = yPos;
+ rect.Height = yPrevPos - yPos;
+ }
+ }
+ else
+ {
+ if (y < 0)
+ {
+ rect.Left = yPrevPos;
+ rect.Width = yPos - yPrevPos;
+ }
+ else
+ {
+ rect.Left = yPos;
+ rect.Width = yPrevPos - yPos;
+ }
+ }
+ }
+ else
+ {
+ if (isColumn)
+ {
+ if (y < 0)
+ {
+ rect.Top = yAxisStart;
+ rect.Height = yPos - yAxisStart;
+ }
+ else
+ {
+ rect.Top = yPos;
+ rect.Height = yAxisStart - yPos;
+ }
+ }
+ else
+ {
+ if (y < 0)
+ {
+ rect.Left = yPos;
+ rect.Width = yAxisStart - yPos;
+ }
+ else
+ {
+ rect.Left = yAxisStart;
+ rect.Width = yPos - yAxisStart;
+ }
+ }
+ }
+ rect.SetDrawingPropertiesFill(serie.Fill, chartType.StyleManager.Style.SeriesAxis.FillReference.Color);
+ rect.SetDrawingPropertiesBorder(serie.Border, chartType.StyleManager.Style.SeriesAxis.BorderReference.Color, true);
+ rect.SetDrawingPropertiesEffects(serie.Effect);
+ RenderItems.Add(rect);
+ }
+ }
+ private void AddColumn(ExcelBarChart chartType, ExcelBarChartSerie serie, List> xSeries, List> ySeries, List dataPoints, int seriesCount, int position)
+ {
+ SvgChartAxis yAxis, xAxis;
+ GetAxis(chartType, out yAxis, out xAxis);
+
+ var xValues = xSeries[position];
+ var yValues = ySeries[position];
+
+ var slotSize = yValues.Count;
+ var gapPercent = chartType.GapWidth / 100D; //Gap width between bars/columns in percent
+ var overlapPercent = chartType.Overlap / 100D; //Overlap between bars/columns in percent
+ var slotWidth = _svgChart.Plotarea.Rectangle.Width / slotSize;
+ var clusterWidth = slotWidth * 100 / (100 + chartType.GapWidth);
+ var step = 1 - overlapPercent;
+ double barWidth;
+ barWidth = slotWidth / (1 + (seriesCount - 1) * step + gapPercent);
+ var halfGap = (barWidth * gapPercent) / 2;
+
+ double yAxisStart;
+
+ if (yAxis.Axis.Crosses == eCrosses.AutoZero)
+ {
+ yAxisStart = yAxis.GetPositionInPlotarea(yAxis.Min <= 0 ? 0D : yAxis.Min, true);
+ }
+ else if (yAxis.Axis.Crosses == eCrosses.Min)
+ {
+ yAxisStart = yAxis.GetPositionInPlotarea(yAxis.Min, true);
+ }
+ else
+ {
+ yAxisStart = yAxis.GetPositionInPlotarea(yAxis.Max, true);
+ }
+
+ var isStacked = chartType.IsTypeStacked();
+ var isStacked100 = chartType.IsTypePercentStacked();
+ for (var i = 0; i < yValues.Count; i++)
+ {
+ double x;
+ if (xValues == null || xAxis.Axis.AxisType == eAxisType.Cat)
+ {
+ x = (double)i;
+ }
+ else
+ {
+ x = ConvertUtil.GetValueDouble(xValues[i], false, true);
+ }
+
+ var y = ConvertUtil.GetValueDouble(yValues[i], false, true);
+
+ var xPos = xAxis.GetPositionInPlotarea(x, true) + halfGap + position * barWidth * step;
+ var yPos = yAxis.GetPositionInPlotarea(y);
+
+ var rect = new SvgRenderRectItem(ChartRenderer, _svgChart.Plotarea.Rectangle.Bounds);
+ rect.Left = xPos;
+ rect.Width = barWidth;
+ if (position > 0 && (isStacked || isStacked100))
+ {
+ //Stacked
+ var pYValues = ySeries[position - 1];
+ var pAxisEnd = ConvertUtil.GetValueDouble(pYValues[i]);
+ var yPrevPos = yAxis.GetPositionInPlotarea(pAxisEnd, false);
+ if (y < 0)
+ {
+ rect.Top = yPrevPos;
+ rect.Height = yPos - yPrevPos;
+ }
+ else
+ {
+ rect.Top = yPos;
+ rect.Height = yPrevPos - yPos;
+ }
+ }
+ else
+ {
+ if (y < 0)
+ {
+ rect.Top = yAxisStart;
+ rect.Height = yPos - yAxisStart;
+ }
+ else
+ {
+ rect.Top = yPos;
+ rect.Height = yAxisStart - yPos;
+ }
+ }
+ rect.SetDrawingPropertiesFill(serie.Fill, chartType.StyleManager.Style.SeriesAxis.FillReference.Color);
+ rect.SetDrawingPropertiesBorder(serie.Border, chartType.StyleManager.Style.SeriesAxis.BorderReference.Color, true);
+ rect.SetDrawingPropertiesEffects(serie.Effect);
+ RenderItems.Add(rect);
+ }
+ }
+
+ private void GetAxis(ExcelBarChart chartType, out SvgChartAxis yAxis, out SvgChartAxis xAxis)
+ {
+ if (chartType.UseSecondaryAxis)
+ {
+ yAxis = _svgChart.SecondVerticalAxis;
+ xAxis = _svgChart.SecondHorizontalAxis;
+ if (xAxis.Axis.Deleted && xAxis.Values == null)
+ {
+ xAxis = _svgChart.HorizontalAxis;
+ }
+ }
+ else
+ {
+ yAxis = _svgChart.VerticalAxis;
+ xAxis = _svgChart.HorizontalAxis;
+ }
+ }
+ }
+
+}
diff --git a/src/EPPlus.Export.ImageRenderer/Svg/Chart/ChartTypeDrawers/ChartTypeDrawer.cs b/src/EPPlus.Export.ImageRenderer/Svg/Chart/ChartTypeDrawers/ChartTypeDrawer.cs
index 322f3a505e..9945080448 100644
--- a/src/EPPlus.Export.ImageRenderer/Svg/Chart/ChartTypeDrawers/ChartTypeDrawer.cs
+++ b/src/EPPlus.Export.ImageRenderer/Svg/Chart/ChartTypeDrawers/ChartTypeDrawer.cs
@@ -6,6 +6,7 @@
using OfficeOpenXml.Drawing.Chart.ChartEx;
using OfficeOpenXml.FormulaParsing.Excel.Functions.Finance;
using OfficeOpenXml.FormulaParsing.Excel.Functions.RefAndLookup;
+using OfficeOpenXml.Utils.TypeConversion;
using System;
using System.Collections.Generic;
using System.Linq;
@@ -71,7 +72,15 @@ internal static List Create(SvgChart svgChart)
case eChartType.LineStacked100:
case eChartType.LineMarkersStacked:
case eChartType.LineMarkersStacked100:
- drawers.Add(new LineChartTypeDrawer(svgChart, ct));
+ drawers.Add(new LineChartTypeDrawer(svgChart, (ExcelLineChart)ct));
+ break;
+ case eChartType.ColumnClustered:
+ case eChartType.ColumnStacked:
+ case eChartType.ColumnStacked100:
+ case eChartType.BarClustered:
+ case eChartType.BarStacked:
+ case eChartType.BarStacked100:
+ drawers.Add(new BarColumnChartTypeDrawer(svgChart, (ExcelBarChart)ct));
break;
default:
throw new NotImplementedException($"No Svg support for Chart type {ct} is implemented.");
@@ -79,6 +88,17 @@ internal static List Create(SvgChart svgChart)
}
return drawers;
}
+ internal void SumSeries(List> series)
+ {
+ for (var i = 1; i < series.Count; i++)
+ {
+ for (var j = 0; j < series[i].Count; j++)
+ {
+ series[i][j] = ConvertUtil.GetValueDouble(series[i][j]) + ConvertUtil.GetValueDouble(series[i - 1][j]);
+ }
+ }
+ }
+
internal override void AppendRenderItems(List renderItems)
{
renderItems.AddRange(RenderItems);
diff --git a/src/EPPlus.Export.ImageRenderer/Svg/Chart/ChartTypeDrawers/LineChartTypeDrawer.cs b/src/EPPlus.Export.ImageRenderer/Svg/Chart/ChartTypeDrawers/LineChartTypeDrawer.cs
index d71446e781..c18b43ae6b 100644
--- a/src/EPPlus.Export.ImageRenderer/Svg/Chart/ChartTypeDrawers/LineChartTypeDrawer.cs
+++ b/src/EPPlus.Export.ImageRenderer/Svg/Chart/ChartTypeDrawers/LineChartTypeDrawer.cs
@@ -1,38 +1,49 @@
-using EPPlusImageRenderer.RenderItems;
+using EPPlus.Export.ImageRenderer.RenderItems.SvgItem;
+using EPPlus.Graphics;
+using EPPlusImageRenderer.RenderItems;
using EPPlusImageRenderer.Svg;
using OfficeOpenXml.Drawing.Chart;
using OfficeOpenXml.Utils.TypeConversion;
-using System;
-using System.Collections;
using System.Collections.Generic;
-using System.Linq;
-using static OfficeOpenXml.ExcelErrorValue;
namespace EPPlus.Export.ImageRenderer.Svg.Chart
{
internal class LineChartTypeDrawer : ChartTypeDrawer
{
- internal LineChartTypeDrawer(SvgChart svgChart, ExcelChart chartType) : base(svgChart, chartType)
+ List serieDataLabels = new List();
+ List> dataPointsPerSerie = new List>();
+
+ internal LineChartTypeDrawer(SvgChart svgChart, ExcelLineChart chartType) : base(svgChart, chartType)
{
var groupItem = new SvgGroupItem(ChartRenderer, _svgChart.Plotarea.Rectangle.Bounds);
RenderItems.Add(groupItem);
- var isStacked = Chart.IsTypeStacked();
- var isPercentStacked = Chart.IsTypePercentStacked();
+ var isStacked = chartType.IsTypeStacked();
+ var isPercentStacked = chartType.IsTypePercentStacked();
var xValues = new List>();
var yValues = new List>();
+ int serCounter = 0;
+
foreach (ExcelLineChartSerie serie in chartType.Series)
{
var yValue = LoadSeriesValues(serie.Series, serie.NumberLiteralsY, serie.StringLiteralsY);
var xValue = LoadSeriesValues(serie.XSeries, serie.NumberLiteralsX, serie.StringLiteralsX);
+
xValues.Add(xValue);
yValues.Add(yValue);
+
+ if (serie.HasDataLabel)
+ {
+ var datalabel = new SvgChartSerieDataLabel(svgChart, serie.DataLabel, svgChart.Bounds, serie, xValue, yValue, serCounter);
+ serieDataLabels.Add(datalabel);
+ }
+ serCounter++;
}
-
- if (Chart.IsTypeStacked())
+
+ if (chartType.IsTypeStacked())
{
SumSeries(yValues);
}
- else if (Chart.IsTypePercentStacked())
+ else if (chartType.IsTypePercentStacked())
{
ExcelChartAxisStandard.CalculateStacked100(yValues);
}
@@ -42,10 +53,27 @@ internal LineChartTypeDrawer(SvgChart svgChart, ExcelChart chartType) : base(svg
var xSerie = xValues[i];
var ySerie = yValues[i];
var serie = (ExcelLineChartSerie)chartType.Series[i];
- AddLine(chartType, serie, xSerie, ySerie);
- }
+ var dataPoints = new List();
+
+ AddLine(chartType, serie, xSerie, ySerie, dataPoints);
+
+ dataPointsPerSerie.Add(dataPoints);
+
+ if (serie.HasDataLabel)
+ {
+ for (int j = 0; j < dataPoints.Count; j++)
+ {
+ serieDataLabels[i].SetParentPoint(dataPoints[j], j);
+ }
+ }
+ }
RenderItems.Add(new SvgEndGroupItem(ChartRenderer, null));
+
+ foreach (var dataLabel in serieDataLabels)
+ {
+ dataLabel.AppendRenderItems(RenderItems);
+ }
}
private void SumSeries(List> series)
{
@@ -57,17 +85,22 @@ private void SumSeries(List> series)
}
}
}
- private void AddLine(ExcelChart chartType, ExcelLineChartSerie serie, List xValues, List yValues)
- {
- var xAxis = _svgChart.HorizontalAxis;
- SvgChartAxis yAxis;
+ private void AddLine(ExcelChart chartType, ExcelLineChartSerie serie, List xValues, List yValues, List dataPoints)
+ {
+ SvgChartAxis yAxis, xAxis;
if (chartType.UseSecondaryAxis)
{
yAxis = _svgChart.SecondVerticalAxis;
+ xAxis = _svgChart.SecondHorizontalAxis;
+ if(xAxis.Axis.Deleted && xAxis.Values==null)
+ {
+ xAxis = _svgChart.HorizontalAxis;
+ }
}
else
{
yAxis = _svgChart.VerticalAxis;
+ xAxis = _svgChart.HorizontalAxis;
}
var linePath = new SvgRenderPathItem(ChartRenderer, _svgChart.Plotarea.Rectangle.Bounds);
var coords = new List();
@@ -76,7 +109,7 @@ private void AddLine(ExcelChart chartType, ExcelLineChartSerie serie, List TextBoxes
+ {
+ get;
+ set;
+ }=new List();
+
+ internal override void AppendRenderItems(List renderItems)
+ {
+ if (TextBoxes != null && TextBoxes.Count > 0)
+ {
+ foreach (var tb in TextBoxes)
+ {
+ tb.AppendRenderItems(renderItems);
+ }
+ }
+
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/EPPlus.Export.ImageRenderer/Svg/Chart/SvgChart.cs b/src/EPPlus.Export.ImageRenderer/Svg/Chart/SvgChart.cs
index 83528fdce4..32def0ffd6 100644
--- a/src/EPPlus.Export.ImageRenderer/Svg/Chart/SvgChart.cs
+++ b/src/EPPlus.Export.ImageRenderer/Svg/Chart/SvgChart.cs
@@ -10,11 +10,17 @@ Date Author Change
*************************************************************************************************
27/11/2025 EPPlus Software AB EPPlus 9
*************************************************************************************************/
+using EPPlus.Export.ImageRenderer.RenderItems.SvgItem;
using EPPlus.Export.ImageRenderer.Svg.Chart;
using EPPlus.Fonts.OpenType.Utils;
+using EPPlus.Graphics;
using EPPlusImageRenderer.RenderItems;
using EPPlusImageRenderer.Utils;
+using OfficeOpenXml.Drawing;
using OfficeOpenXml.Drawing.Chart;
+using OfficeOpenXml.Export.HtmlExport.Exporters.Internal;
+using OfficeOpenXml.FormulaParsing.Excel.Functions.MathFunctions;
+using OfficeOpenXml.Interfaces.Drawing.Text;
using System;
using System.Collections.Generic;
using System.Drawing;
@@ -27,6 +33,7 @@ internal class SvgChart : DrawingChart
{
public SvgChart(ExcelChart chart) : base(chart)
{
+
SetChartArea();
if(chart.HasTitle && chart.Series.Count > 0)
@@ -47,97 +54,166 @@ public SvgChart(ExcelChart chart) : base(chart)
Legend = null;
}
- VerticalAxis = new SvgChartAxis(this, (ExcelChartAxisStandard)chart.YAxis);
- HorizontalAxis = new SvgChartAxis(this, (ExcelChartAxisStandard)chart.XAxis);
- if (chart.Axis.Length > 2)
+ HorizontalAxis = GetAxis(false);
+ VerticalAxis = GetAxis(true);
+
+ if(chart.Axis.Length > 2)
{
- SecondVerticalAxis = new SvgChartAxis(this, (ExcelChartAxisStandard)chart.Axis[2]);
+ SecondVerticalAxis = GetAxis(true, 2);
+ SecondHorizontalAxis = GetAxis(false, 2);
}
Plotarea = new SvgChartPlotarea(this);
+ //As we need the plotarea dimensions to calculate the axis positions we need to set the axis positions after creating the plotarea.
SetAxisPositionsFromPlotarea(this);
- foreach (var ct in chart.PlotArea.ChartTypes)
+
+ Plotarea.ChartTypeDrawers = ChartTypeDrawer.Create(this);
+ }
+
+ private SvgChartAxis GetAxis(bool vertical, int offset=0)
+ {
+ var axis = (ExcelChartAxisStandard)Chart.Axis[offset];
+ if(axis.IsVertical==vertical)
+ {
+ return new SvgChartAxis(this, axis);
+ }
+ else if(Chart.Axis.Length > offset + 1)
{
- Plotarea.ChartTypeDrawers = ChartTypeDrawer.Create(this);
+ axis = (ExcelChartAxisStandard)Chart.Axis[offset + 1];
+ if(axis.IsVertical==vertical)
+ {
+ return new SvgChartAxis(this, axis);
+ }
}
+ return null;
}
private void SetAxisPositionsFromPlotarea(SvgChart sc)
{
if(VerticalAxis != null)
{
- if (VerticalAxis.Rectangle != null)
- {
- VerticalAxis.Rectangle.Top = Plotarea.Rectangle.Top;
- VerticalAxis.Rectangle.Height = Plotarea.Rectangle.Height;
- VerticalAxis.Rectangle.Left = Plotarea.Rectangle.Left - VerticalAxis.Rectangle.Width;
- VerticalAxis.Line.X1 = VerticalAxis.Line.X2 = (float)Plotarea.Rectangle.Left;
- VerticalAxis.Line.Y1 = (float)VerticalAxis.Rectangle.Top;
- VerticalAxis.Line.Y2 = (float)VerticalAxis.Rectangle.Bottom;
- }
+ PlaceVerticalAxis(sc, VerticalAxis);
+ VerticalAxis.AddTickmarksAndValues(DefItems);
+ }
- if(VerticalAxis.Title!=null)
+ if (HorizontalAxis!=null && HorizontalAxis.Rectangle != null)
+ {
+ PlaceHorizontalAxis(sc, HorizontalAxis);
+
+ //Make sure the horizontal axis is moved up if the vertical axis has a negative minimum value, so that the 0 value is at the correct position.
+ if (VerticalAxis.Axis.AxisType == eAxisType.Val && VerticalAxis.Min < 0D)
{
- VerticalAxis.Title.Rectangle.Height = Plotarea.Rectangle.Height;
- VerticalAxis.Title.Rectangle.Width = sc.Bounds.Width / 4;
- VerticalAxis.Title.InitTextBox();
- VerticalAxis.Title.TextBox.Top = Plotarea.Rectangle.Top + (Plotarea.Rectangle.Height / 2) - (VerticalAxis.Title.TextBox.Height / 2);
- if (VerticalAxis.Rectangle == null)
- {
- VerticalAxis.Title.TextBox.Left = sc.ChartArea.LeftMargin;
- }
- else
- {
- VerticalAxis.Title.TextBox.Left = VerticalAxis.Rectangle.Left - VerticalAxis.Title.TextBox.Width ;
- }
+ var newtop = VerticalAxis.GetPositionInPlotarea(0D) + sc.Plotarea.Rectangle.Top;
+ var topDiff = HorizontalAxis.Rectangle.Top - newtop;
+ HorizontalAxis.Rectangle.Top = newtop;
+ HorizontalAxis.Rectangle.Height += topDiff;
+ HorizontalAxis.Line.Y1 = HorizontalAxis.Line.Y2 = newtop;
}
-
- VerticalAxis.AddTickmarksAndValues();
+
+ HorizontalAxis.AddTickmarksAndValues(DefItems);
}
- if (HorizontalAxis!=null)
+ if (SecondVerticalAxis!=null)
{
- HorizontalAxis.Rectangle.Top = Plotarea.Rectangle.Bottom;
- HorizontalAxis.Rectangle.Width = Plotarea.Rectangle.Width;
- HorizontalAxis.Rectangle.Left = Plotarea.Rectangle.Left;
- HorizontalAxis.Line.Y1 = HorizontalAxis.Line.Y2 = (float)Plotarea.Rectangle.Bottom;
- HorizontalAxis.Line.X1 = (float)HorizontalAxis.Rectangle.Left;
- HorizontalAxis.Line.X2 = (float)HorizontalAxis.Rectangle.Right;
+ PlaceVerticalAxis(sc, SecondVerticalAxis);
+ SecondVerticalAxis.AddTickmarksAndValues(DefItems);
+ }
+
+ if (SecondHorizontalAxis != null && SecondHorizontalAxis.Rectangle != null)
+ {
+ PlaceHorizontalAxis(sc, SecondHorizontalAxis);
+ SecondHorizontalAxis.AddTickmarksAndValues(DefItems);
+ }
+ }
+
+ private void PlaceHorizontalAxis(SvgChart sc, SvgChartAxis horizontalAxis)
+ {
+ horizontalAxis.Rectangle.Width = Plotarea.Rectangle.Width;
+ horizontalAxis.Rectangle.Left = Plotarea.Rectangle.Left;
+ horizontalAxis.Line.X1 = (float)horizontalAxis.Rectangle.Left;
+ horizontalAxis.Line.X2 = (float)horizontalAxis.Rectangle.Right;
+
+ if (horizontalAxis.Axis.AxisPosition == eAxisPosition.Bottom)
+ {
+ horizontalAxis.Rectangle.Top = Plotarea.Rectangle.Bottom;
+ horizontalAxis.Line.Y1 = horizontalAxis.Line.Y2 = (float)Plotarea.Rectangle.Bottom;
+ }
+ else
+ {
+ horizontalAxis.Rectangle.Top = Plotarea.Rectangle.Top - horizontalAxis.Rectangle.Height;
+ horizontalAxis.Line.Y1 = horizontalAxis.Line.Y2 = (float)Plotarea.Rectangle.Top;
+ }
- if (HorizontalAxis.Title != null)
+ if (horizontalAxis.Title != null)
+ {
+ horizontalAxis.Title.Rectangle.Height = sc.Bounds.Height / 4;
+ horizontalAxis.Title.Rectangle.Width = horizontalAxis.Rectangle?.Width ?? sc.Plotarea.Rectangle.Width;
+ //horizontalAxis.Title.InitTextBox();
+ if (horizontalAxis.Axis.AxisPosition == eAxisPosition.Bottom)
+ {
+ horizontalAxis.Title.TextBox.Top = horizontalAxis.Rectangle.Bottom;
+ }
+ else
{
- HorizontalAxis.Title.Rectangle.Height = sc.Bounds.Height / 4;
- HorizontalAxis.Title.Rectangle.Width = HorizontalAxis.Rectangle?.Width ?? sc.Plotarea.Rectangle.Width;
- HorizontalAxis.Title.InitTextBox();
- HorizontalAxis.Title.TextBox.Left = Plotarea.Rectangle.Left + (Plotarea.Rectangle.Width / 2) - (HorizontalAxis.Title.TextBox.Width / 2);
- HorizontalAxis.Title.TextBox.Top = HorizontalAxis.Rectangle.Bottom;
+ horizontalAxis.Title.TextBox.Top = horizontalAxis.Rectangle.Top - horizontalAxis.Title.TextBox.Height;
}
- HorizontalAxis.AddTickmarksAndValues();
+ horizontalAxis.Title.TextBox.Left = Plotarea.Rectangle.Left + (Plotarea.Rectangle.Width / 2) - (horizontalAxis.Title.TextBox.Width / 2);
}
+ }
- if (SecondVerticalAxis!=null)
+ private void PlaceVerticalAxis(SvgChart sc, SvgChartAxis verticalAxis)
+ {
+ if (verticalAxis.Rectangle != null)
{
- if (SecondVerticalAxis.Rectangle != null)
+ verticalAxis.Rectangle.Top = Plotarea.Rectangle.Top;
+ verticalAxis.Rectangle.Height = Plotarea.Rectangle.Height;
+ verticalAxis.Line.Y1 = (float)verticalAxis.Rectangle.Top;
+ verticalAxis.Line.Y2 = (float)verticalAxis.Rectangle.Bottom;
+ if (verticalAxis.Axis.AxisPosition == eAxisPosition.Left)
{
- SecondVerticalAxis.Rectangle.Top = Plotarea.Rectangle.Top;
- SecondVerticalAxis.Rectangle.Height = Plotarea.Rectangle.Height;
- SecondVerticalAxis.Rectangle.Left = Plotarea.Rectangle.Left - SecondVerticalAxis.Rectangle.Width;
- SecondVerticalAxis.Line.X1 = SecondVerticalAxis.Line.X2 = (float)Plotarea.Rectangle.Left;
- SecondVerticalAxis.Line.Y1 = (float)SecondVerticalAxis.Rectangle.Top;
- SecondVerticalAxis.Line.Y2 = (float)SecondVerticalAxis.Rectangle.Bottom;
+ verticalAxis.Rectangle.Left = Plotarea.Rectangle.Left - verticalAxis.Rectangle.Width;
+ verticalAxis.Line.X1 = verticalAxis.Line.X2 = (float)Plotarea.Rectangle.Left;
}
-
- if (SecondVerticalAxis.Title != null)
+ else
{
- SecondVerticalAxis.Title.Rectangle.Height = SecondVerticalAxis.Rectangle.Height;
- SecondVerticalAxis.Title.Rectangle.Width = sc.Bounds.Width / 4;
- SecondVerticalAxis.Title.InitTextBox();
- SecondVerticalAxis.Title.TextBox.Top = Plotarea.Rectangle.Top + (Plotarea.Rectangle.Height / 2) - (SecondVerticalAxis.Title.TextBox.Height / 2);
- SecondVerticalAxis.Title.TextBox.Left = SecondVerticalAxis.Title.Rectangle.Left;
+ verticalAxis.Rectangle.Left = Plotarea.Rectangle.Right;
+ verticalAxis.Line.X1 = verticalAxis.Line.X2 = (float)Plotarea.Rectangle.Right;
}
+ }
- SecondVerticalAxis.AddTickmarksAndValues();
+ if (verticalAxis.Title != null)
+ {
+ //verticalAxis.Title.Rectangle.Height = Plotarea.Rectangle.Height;
+ //verticalAxis.Title.Rectangle.Width = sc.Bounds.Width / 4;
+ //verticalAxis.Title.InitTextBox();
+ var sinRot = Math.Abs(Math.Sin(MathHelper.Radians(verticalAxis.Title.TextBox.Rotation)));
+ var cosRot = Math.Abs(Math.Cos(MathHelper.Radians(verticalAxis.Title.TextBox.Rotation)));
+ verticalAxis.Title.TextBox.Top = Plotarea.Rectangle.Top + (Plotarea.Rectangle.Height / 2) + ((verticalAxis.Title.TextBox.Height * cosRot + verticalAxis.Title.TextBox.Width * sinRot) / 2);
+
+ if (verticalAxis.Axis.AxisPosition == eAxisPosition.Left)
+ {
+ if (verticalAxis.Rectangle == null)
+ {
+ verticalAxis.Title.TextBox.Left = sc.Plotarea.Rectangle.Left - verticalAxis.Title.TextBox.GetActualWidth() - 1.5;
+ }
+ else
+ {
+ verticalAxis.Title.TextBox.Left = verticalAxis.Rectangle.Left - verticalAxis.Title.TextBox.GetActualWidth() - 1.5;
+ }
+ }
+ else
+ {
+ if (verticalAxis.Rectangle == null)
+ {
+ verticalAxis.Title.TextBox.Left = Plotarea.Rectangle.Right;
+ }
+ else
+ {
+ verticalAxis.Title.TextBox.Left = verticalAxis.Rectangle.Right;
+ }
+ }
+ //verticalAxis.Title.TextBox.TextAnchor = eTextAnchor.Middle;
}
}
@@ -148,6 +224,7 @@ private void SetAxisPositionsFromPlotarea(SvgChart sc)
internal SvgChartAxis VerticalAxis { get; set; }
internal SvgChartAxis HorizontalAxis { get; set; }
internal SvgChartAxis SecondVerticalAxis { get; set; }
+ internal SvgChartAxis SecondHorizontalAxis { get; set; }
internal SvgChartTitle VerticalAxisTitle { get; set; }
internal SvgChartTitle HorizontalAxisTitle { get; set; }
internal SvgChartTitle SecondVerticalAxisTitle { get; set; }
@@ -173,6 +250,7 @@ public void Render(StringBuilder sb)
HorizontalAxis?.AppendRenderItems(RenderItems);
VerticalAxis?.AppendRenderItems(RenderItems);
+ SecondHorizontalAxis?.AppendRenderItems(RenderItems);
SecondVerticalAxis?.AppendRenderItems(RenderItems);
if (Plotarea != null)
@@ -182,8 +260,14 @@ public void Render(StringBuilder sb)
drawer.AppendRenderItems(RenderItems);
}
}
- Legend?.AppendRenderItems(RenderItems);
+
+ HorizontalAxis?.Textboxes?.AppendRenderItems(RenderItems);
+ VerticalAxis?.Textboxes?.AppendRenderItems(RenderItems);
+ SecondHorizontalAxis?.Textboxes?.AppendRenderItems(RenderItems);
+ SecondVerticalAxis?.Textboxes?.AppendRenderItems(RenderItems);
+
Title?.AppendRenderItems(RenderItems);
+ Legend?.AppendRenderItems(RenderItems);
sb.Append($"");
//Write defs used for gradient colors
@@ -215,5 +299,25 @@ internal double GetPlotAreaTop()
}
}
+
+ internal SvgRenderLineItem GetSeriesIcon(ExcelChartStandardSerie s, int index, BoundingBox parentItem)
+ {
+ const float MarginExtra = 1.5f;
+ const float LineLength = 21;
+
+ var item = new SvgRenderLineItem(this, parentItem);
+ item.SetDrawingPropertiesFill(s.Fill, this.Chart.StyleManager.Style.SeriesLine.FillReference.Color);
+ item.SetDrawingPropertiesBorder(s.Border, this.Chart.StyleManager.Style.SeriesLine.BorderReference.Color, s.Border.Fill.Style != eFillStyle.NoFill, 0.75);
+
+ float y = (float)parentItem.Top + MarginExtra;
+ float x = 0;
+ item.X1 = x;
+ item.Y1 = y;
+ item.X2 = x + LineLength;
+ item.Y2 = y;
+ item.LineCap = eLineCap.Round;
+
+ return item;
+ }
}
}
\ No newline at end of file
diff --git a/src/EPPlus.Export.ImageRenderer/Svg/Chart/SvgChartAxis.cs b/src/EPPlus.Export.ImageRenderer/Svg/Chart/SvgChartAxis.cs
index 9e22912a68..c5fb1274c4 100644
--- a/src/EPPlus.Export.ImageRenderer/Svg/Chart/SvgChartAxis.cs
+++ b/src/EPPlus.Export.ImageRenderer/Svg/Chart/SvgChartAxis.cs
@@ -10,17 +10,18 @@ Date Author Change
*************************************************************************************************
27/11/2025 EPPlus Software AB EPPlus 9
*************************************************************************************************/
-using EPPlus.Export.ImageRenderer.RenderItems.Shared;
+using EPPlus.Export.ImageRenderer;
+using EPPlus.Export.ImageRenderer.RenderItems;
using EPPlus.Export.ImageRenderer.RenderItems.SvgItem;
using EPPlus.Export.ImageRenderer.Svg.Chart.Util;
-using EPPlus.Fonts.OpenType;
-using EPPlus.Fonts.OpenType.Tables.Cmap;
using EPPlus.Fonts.OpenType.Utils;
using EPPlus.Graphics;
using EPPlusImageRenderer.RenderItems;
using OfficeOpenXml.Drawing;
using OfficeOpenXml.Drawing.Chart;
using OfficeOpenXml.Drawing.Chart.Style;
+using OfficeOpenXml.FormulaParsing.Excel.Functions.Logical;
+using OfficeOpenXml.FormulaParsing.Excel.Functions.MathFunctions;
using OfficeOpenXml.Style;
using OfficeOpenXml.Style.XmlAccess;
using OfficeOpenXml.Utils.String;
@@ -28,19 +29,19 @@ Date Author Change
using System;
using System.Collections.Generic;
using System.Linq;
-using System.Text;
namespace EPPlusImageRenderer.Svg
{
internal class SvgChartAxis : SvgChartObject, IDrawingChartAxis
{
+ private const double COS45 = 0.70710678118654757; //Constant for Math.Sin(Math.PI / 4) --45 degrees
internal SvgChartAxis(SvgChart sc, ExcelChartAxisStandard ax) : base(sc)
{
- SvgChart= sc;
+ SvgChart = sc;
Axis = ax;
SetMargins(ax.TextBody);
- if (sc.Chart.Series.Count == 0 || (ax.Deleted == true && ax.Title == null))
+ if (sc.Chart.Series.Count == 0)
{
return;
}
@@ -54,58 +55,63 @@ internal SvgChartAxis(SvgChart sc, ExcelChartAxisStandard ax) : base(sc)
Title = null;
}
- if (ax.Deleted == false || ax.HasMajorGridlines || ax.HasMinorGridlines)
+ Values = GetAxisValue(ax, sc.ChartArea.Rectangle, out double? min, out double? max, out double? majorUnit, out eTimeUnit? dateUnit, out eTextOrientation orientation);
+ AxisValues = GetAxisDisplayValues(ax, Values, min, max, majorUnit);
+
+ Min = min ?? 0D;
+ Max = max ?? (Values.Count > 0 ? ConvertUtil.GetValueDouble(Values[Values.Count - 1], false, true) : 0D);
+ MajorUnit = majorUnit ?? 1;
+ MinorUnit = ax.MinorUnit ?? GetAutoMinUnit(MajorUnit);
+ MajorDateUnit = dateUnit;
+ LabelOrientation = orientation;
+
+ if (ax.Deleted == false)
{
- Values = GetAxisValue(ax, Rectangle, out double? min, out double? max, out double? majorUnit);
- AxisValues = GetAxisDisplayValues(ax, Values, min, max, majorUnit);
-
- Min = min ?? 0D;
- Max = max ?? (Values.Count > 0 ? ConvertUtil.GetValueDouble(Values[Values.Count - 1], false, true) : 0D);
- MajorUnit = majorUnit ?? 1;
- MinorUnit = ax.MinorUnit ?? GetAutoMinUnit(MajorUnit);
- if (ax.Deleted == false)
+ if (ax.Layout.HasLayout)
{
- if (ax.Layout.HasLayout)
- {
- Rectangle = GetRectFromManualLayout(sc, ax.Layout);
- }
- else
+ Rectangle = GetRectFromManualLayout(sc, ax.Layout);
+ }
+ else
+ {
+ Rectangle = new SvgRenderRectItem(sc, sc.Bounds);
+ if (ax.AxisPosition == eAxisPosition.Left || ax.AxisPosition == eAxisPosition.Right)
{
- Rectangle = new SvgRenderRectItem(sc, sc.Bounds);
- if (ax.AxisPosition == eAxisPosition.Left || ax.AxisPosition == eAxisPosition.Right)
+ if (ax.AxisPosition == eAxisPosition.Left)
{
- if (ax.AxisPosition == eAxisPosition.Left)
- {
- Rectangle.Width = GetTextWidest(sc, ax) + LeftMargin;
- var ll = 8D;
- if (sc.Chart.Legend.Position == eLegendPosition.Left)
- {
- ll = sc.Legend.Rectangle.Right + sc.Legend.RightMargin;
- }
- Rectangle.Left = Title == null ? ll : Title.Rectangle.Right;
- }
- else
+ Rectangle.Width = GetTextWidest(sc, ax) + LeftMargin;
+ var ll = 8D;
+ if (sc.Chart.HasLegend && sc.Chart.Legend.Position == eLegendPosition.Left)
{
- Rectangle.Width = GetTextWidest(sc, ax) + RightMargin;
- var lp = sc.ChartArea.Rectangle.Width - Rectangle.Width - 8D;
- if (sc.Chart.Legend.Position == eLegendPosition.Right)
- {
- lp = sc.Legend.Rectangle.Left + -Rectangle.Width;
- }
- Rectangle.Left = Title == null ? lp : Title.Rectangle.Left - Rectangle.Width;
+ ll = sc.Legend.Rectangle.Right + sc.Legend.RightMargin;
}
+ Rectangle.Left = Title == null ? ll : Title.Rectangle.Left + Title.TextBox.GetActualWidth();
}
else
{
- Rectangle.Height = GetTextHeight(sc, ax);
- Rectangle.Top = Title == null || ax.AxisPosition == eAxisPosition.Top ? sc.ChartArea.Rectangle.Height - 8 - Rectangle.Height : Title.Rectangle.Top - Rectangle.Height - 8;
+ Rectangle.Width = GetTextWidest(sc, ax) + RightMargin;
+ var lp = sc.ChartArea.Rectangle.Width - Rectangle.Width - 8D;
+ if (sc.Chart.HasLegend && sc.Chart.Legend.Position == eLegendPosition.Right)
+ {
+ lp = sc.Legend.Rectangle.Left + -Rectangle.Width;
+ }
+ Rectangle.Left = Title == null ? lp : Title.Rectangle.Left - Rectangle.Width - LeftMargin;
}
}
+ else
+ {
+ Rectangle.Height = GetTextHeight(sc, ax);
+ //TODO:Fix
+ Rectangle.Top = Title == null || ax.AxisPosition == eAxisPosition.Top ? sc.ChartArea.Rectangle.Height - 8 - Rectangle.Height : Title.Rectangle.Top - Rectangle.Height - 8;
+ }
+ }
- Rectangle.FillColor = "none";
+ Rectangle.FillColor = "none";
- Line = new SvgRenderLineItem(sc, Rectangle.Bounds);
- Line.SetDrawingPropertiesBorder(ax.Border, sc.Chart.StyleManager.Style.Title.BorderReference.Color, ax.Border.Fill.Style != eFillStyle.NoFill, 0.75);
+ Line = new SvgRenderLineItem(sc, Rectangle.Bounds);
+ Line.SetDrawingPropertiesBorder(ax.Border, sc.Chart.StyleManager.Style.Title.BorderReference.Color, ax.Border.Fill.Style != eFillStyle.NoFill, 1);
+ if(Line.BorderWidth < 1)
+ {
+ Line.BorderWidth = 1;
}
}
}
@@ -117,6 +123,7 @@ private double GetAutoMinUnit(double majorUnit)
internal ExcelChartAxis Axis { get; }
internal SvgChart SvgChart { get; }
+ internal SvgRenderLineItem Line { get; set; }
private List GetAxisDisplayValues(ExcelChartAxisStandard ax, List values, double? min, double? max, double? majorUnit)
{
var displayValues = new List();
@@ -130,7 +137,7 @@ private List GetAxisDisplayValues(ExcelChartAxisStandard ax, List GetAxisDisplayValues(ExcelChartAxisStandard ax, List highest)
+ switch (LabelOrientation)
{
- highest = m.Height;
+ case eTextOrientation.Horizontal:
+ if (m.Height > highest)
+ {
+ highest = m.Height;
+ }
+ break;
+ case eTextOrientation.Diagonal:
+ var width = (m.Width + m.Height) * COS45;
+ if (width > highest)
+ {
+ highest = width;
+ }
+ break;
+ case eTextOrientation.Vertical:
+ if (m.Width > highest)
+ {
+ highest = m.Width;
+ }
+ break;
}
}
return highest.PointToPixel();
@@ -179,19 +204,16 @@ public List Values
public List MajorAxisPositions { get; private set; }
public List MinorAxisPositions { get; private set; }
- public List MajorGridlinePositions { get; private set; }
- public List MinorGridlinePositions { get; private set; }
- public List AxisValuesTextBoxes
- {
- get;
- private set;
- }
-
+ public List MajorGridlinePositions { get; private set; }
+ public List MinorGridlinePositions { get; private set; }
+ public SvgAxisTextBoxes Textboxes{get; private set;}
public SvgChartTitle Title { get; set; }
public double Min { get; set; }
public double Max { get; set; }
public double MajorUnit { get; set; }
public double MinorUnit { get; set; }
+ public eTimeUnit? MajorDateUnit { get; set; }
+ public eTextOrientation LabelOrientation { get; set; }
internal override void AppendRenderItems(List renderItems)
{
Title?.AppendRenderItems(renderItems);
@@ -229,38 +251,37 @@ internal override void AppendRenderItems(List renderItems)
renderItems.Add(tm);
}
}
-
- if (AxisValuesTextBoxes != null && AxisValuesTextBoxes.Count > 0)
- {
- foreach (var tb in AxisValuesTextBoxes)
- {
- tb.AppendRenderItems(renderItems);
- }
- }
+ //The axis text boxes is rendered later as they have a higher Z-order.
}
- internal void AddTickmarksAndValues()
- {
+ internal void AddTickmarksAndValues(List DefItems)
+ {
+ if (Axis.Deleted == true) return;
+
if (Axis.MajorTickMark != eAxisTickMark.None)
{
- MajorAxisPositions = AddTickmarks(MajorUnit,double.NaN, 4, Axis.MajorTickMark);
+ MajorAxisPositions = AddTickmarks(MajorUnit, MajorDateUnit, double.NaN, 4D.PixelToPoint(), Axis.MajorTickMark);
}
if (Axis.MinorTickMark != eAxisTickMark.None)
{
- MinorAxisPositions = AddTickmarks(MinorUnit, MajorUnit, 2, Axis.MinorTickMark);
+ MinorAxisPositions = AddTickmarks(MinorUnit, MajorDateUnit, MajorUnit, 2D.PixelToPoint(), Axis.MinorTickMark);
}
if(Axis.HasMajorGridlines)
{
MajorGridlinePositions = AddGridlines(MajorUnit, double.NaN, Axis.MajorGridlines, Chart.StyleManager.Style.GridlineMajor);
+ //DefItems.Add(MajorGridlinePositions[0]);
+ //MajorGridlinePositions.RemoveAt(0);
}
if ((Axis.HasMinorGridlines))
{
MinorGridlinePositions = AddGridlines(MinorUnit, MajorUnit, Axis.MinorGridlines, Chart.StyleManager.Style.GridlineMinor);
+ //DefItems.Add(MinorGridlinePositions[0]);
+ //MinorGridlinePositions.RemoveAt(0);
}
if (Axis.CrossBetween == eCrossBetween.MidCat)
@@ -271,18 +292,21 @@ internal void AddTickmarksAndValues()
{
}
- if (AxisValues != null && AxisValues.Count > 0 && Axis.Deleted==false)
+ if (AxisValues != null && AxisValues.Count > 0 && Axis.Deleted==false && Axis.LabelPosition != eTickLabelPosition.None)
{
- AxisValuesTextBoxes = GetAxisValueTextBoxes();
+ Textboxes = new SvgAxisTextBoxes(SvgChart);
+ Textboxes.TextBoxes = GetAxisValueTextBoxes();
}
}
private List GetAxisValueTextBoxes()
{
+ var ret = new List();
+ if (Axis.LabelPosition == eTickLabelPosition.None) return ret;
+
var tm = Chart.WorkSheet._package.Settings.TextSettings.GenericTextMeasurerTrueType;
var mf = Axis.Font.GetMeasureFont();
var axisStyle = GetAxisStyleEntry();
- var ret= new List();
double maxWidth, maxHeight;
if(Axis.AxisPosition==eAxisPosition.Left || Axis.AxisPosition == eAxisPosition.Right)
{
@@ -291,37 +315,178 @@ private List GetAxisValueTextBoxes()
}
else
{
- maxWidth = Rectangle.Width / AxisValues.Count;
- maxHeight = SvgChart.ChartArea.Bounds.Height / 3; //TODO: Check this value.
+ switch (LabelOrientation)
+ {
+ case eTextOrientation.Vertical:
+ maxWidth = SvgChart.ChartArea.Rectangle.Height / 3;
+ maxHeight = Rectangle.Width / AxisValues.Count; //TODO: Check this value.
+ break;
+ case eTextOrientation.Diagonal:
+ maxWidth = (Rectangle.Width + Rectangle.Height) / COS45;
+ maxHeight = SvgChart.ChartArea.Rectangle.Height / 3; //TODO: Check this value.
+ break;
+ default:
+ maxWidth = Rectangle.Width / AxisValues.Count;
+ maxHeight = SvgChart.ChartArea.Rectangle.Height / 3; //TODO: Check this value.
+ break;
+ }
}
+ double widest=0;
for (var i = 0; i < AxisValues.Count; i++)
{
var v = AxisValues[i];
var m = tm.MeasureText(v, mf);
- var x = GetAxisItemLeft(i, m);
- var y = GetAxisItemTop(i, m);
- //var tb = new TextBox(Chart, x, y, m.Width, m.Height);
- //var bounds = new BoundingBox();
- //bounds.Left = x;
- //bounds.Top = y;
- var width = m.Width.PointToPixel();
- var height = m.Height.PointToPixel();
+ var ticMarkX = GetAxisItemLeft(i, m);
+ var ticMarkY = GetAxisItemTop(i, m);
+ var width = m.Width;
+ var height = m.Height;
+ double x, y;
+ if(LabelOrientation==eTextOrientation.Horizontal)
+ {
+ if (Axis.AxisType == eAxisType.Cat)
+ {
+ x = ticMarkX;
+ y = ticMarkY;
+ }
+ else
+ {
+ if(Axis.IsVertical)
+ {
+ x = ticMarkX;
+ if (SvgChart.Chart.IsTypeBar())
+ {
+ y = ticMarkY;
+ }
+ else
+ {
+ y = ticMarkY - height / 2;
+ }
+ }
+ else
+ {
+ x = ticMarkX; // - width / 2;
+ y = ticMarkY;
+ }
+ }
+ }
+ else
+ {
+ var rot = LabelOrientation == eTextOrientation.Diagonal ? -45 : -90;
+ double cos = Math.Cos(MathHelper.Radians(rot));
+ double sin = Math.Sin(MathHelper.Radians(rot));
+
+ if (LabelOrientation == eTextOrientation.Diagonal)
+ {
+ x = ticMarkX - (height / 2) * cos;
+ if (Axis.AxisPosition == eAxisPosition.Bottom)
+ {
+ y = ticMarkY + 4 + TopMargin - (height / 2 * cos);
+ }
+ else //Top
+ {
+ y = ticMarkY - 4 - BottomMargin - (height/2 * cos);
+ }
+ }
+ else
+ {
+ x = ticMarkX - (height / 2);
+ if (Axis.AxisPosition == eAxisPosition.Bottom)
+ {
+ y = ticMarkY + BottomMargin + 4;
+ }
+ else //Top
+ {
+ y = ticMarkY - TopMargin - 4;
+ }
+ }
+ }
+
var tb = new SvgTextBox(SvgChart, Rectangle.Bounds, x, y, width, height, maxWidth, maxHeight);
+ if (LabelOrientation == eTextOrientation.Diagonal)
+ {
+ tb.Rotation = -45;
+ if(Axis.AxisPosition==eAxisPosition.Bottom)
+ {
+ tb.TextAnchor = eTextAnchor.End;
+ }
+ }
+
+ else if (LabelOrientation == eTextOrientation.Vertical)
+ {
+ tb.Rotation = -90;
+ if (Axis.AxisPosition == eAxisPosition.Bottom)
+ {
+ tb.TextAnchor = eTextAnchor.End;
+ }
+ }
var p = Axis.TextBody.Paragraphs.FirstOrDefault();
+
+ if (p.HorizontalAlignment != eTextAlignment.Center && Axis.AxisType!=eAxisType.Val && (Axis.AxisPosition == eAxisPosition.Bottom || Axis.AxisPosition == eAxisPosition.Top))
+ {
+ //Horizontal axises are always center aligned visually
+ //Should be broken out as input to ImportParagraph instead of changing the base item
+ p.HorizontalAlignment = eTextAlignment.Center;
+ }
+
tb.TextBody.ImportParagraph(p, 0, v);
//tb.TextBody.Paragraphs[0].AddText(v, Axis.Font);
tb.Rectangle.SetDrawingPropertiesFill(Axis.Fill, axisStyle.FillReference.Color);
+
+ if(widest < tb.Width)
+ {
+ widest = tb.Width;
+ }
ret.Add(tb);
}
+
+ if(Axis.IsVertical)
+ {
+ //If the axis is vertical, we need to adjust the left position of the textboxes to align them to the right and not have them overlap with the axis line.
+ if (Axis.AxisPosition == eAxisPosition.Left)
+ {
+ foreach (var tb in ret)
+ {
+ tb.Left += (widest - tb.Width);
+ }
+ }
+ else
+ {
+ foreach (var tb in ret)
+ {
+ tb.Left += LeftMargin;
+ }
+ }
+ }
+ else if(LabelOrientation==eTextOrientation.Horizontal && Axis.AxisType==eAxisType.Cat) //Only apples when labels are horizontally aligned
+ {
+ //Align the axis labels according to the label alignment setting. This is only relevant for horizontal axis, vertical axis are always right aligned.
+ var lblAlignment = (Axis as ExcelChartAxisStandard)?.LabelAlignment??OfficeOpenXml.eAxisLabelAlignment.Center;
+ var majorWidth = Rectangle.Width / AxisValues.Count;
+ foreach (var tb in ret)
+ {
+ switch (lblAlignment)
+ {
+ case OfficeOpenXml.eAxisLabelAlignment.Left:
+ break;
+ case OfficeOpenXml.eAxisLabelAlignment.Center:
+ tb.Left += majorWidth / 2 - tb.Width / 2;
+ break;
+ case OfficeOpenXml.eAxisLabelAlignment.Right:
+ tb.Left += majorWidth - tb.Width;
+ break;
+ }
+ }
+ }
+
return ret;
}
private double GetAxisItemLeft(int i, OfficeOpenXml.Interfaces.Drawing.Text.TextMeasurement m)
{
if (Axis.AxisPosition == eAxisPosition.Left)
- {
+ {
return Rectangle.Left;
}
else if (Axis.AxisPosition == eAxisPosition.Right)
@@ -331,16 +496,31 @@ private double GetAxisItemLeft(int i, OfficeOpenXml.Interfaces.Drawing.Text.Text
}
else
{
- if (Axis.AxisType == eAxisType.Cat)
+ if ((Axis.AxisType == eAxisType.Cat || Axis.IsVertical==false) && LabelOrientation==eTextOrientation.Horizontal)
{
+ //Between tickmarks
var majorWidth = Rectangle.Width / AxisValues.Count;
- return Rectangle.Left + majorWidth * i;
+ var majorTickStartingPosition = Rectangle.Left + majorWidth * i;
+ //var middleOfBounds = majorTickStartingPosition + (majorWidth / 2);
+ return majorTickStartingPosition;
}
else
{
- var majorWidth = Rectangle.Width / (AxisValues.Count - 1);
-
- return Rectangle.Left + majorWidth * i - m.Width.PointToPixel() / 2;
+ if(Axis.AxisType == eAxisType.Cat)
+ {
+ var majorWidth = Rectangle.Width / AxisValues.Count;
+ var majorTickStartingPosition = Rectangle.Left + majorWidth * i;
+ //var middleOfBounds = majorTickStartingPosition + (majorWidth / 2);
+ return majorTickStartingPosition;
+ }
+ else
+ {
+ var min = ConvertUtil.GetValueDouble(Values[0]);
+ var max = ConvertUtil.GetValueDouble(Values.Last());
+ var v = ConvertUtil.GetValueDouble(Values[i]);
+ var majorWidth = Rectangle.Width * (v - Min) / (Max - Min);
+ return Rectangle.Left + majorWidth;
+ }
}
}
}
@@ -349,64 +529,113 @@ private double GetAxisItemTop(int i, OfficeOpenXml.Interfaces.Drawing.Text.TextM
{
if (Axis.AxisPosition == eAxisPosition.Top)
{
- return Rectangle.Top - m.Height.PointToPixel() - TopMargin;
+ switch (LabelOrientation)
+ {
+ case eTextOrientation.Vertical:
+ case eTextOrientation.Diagonal:
+ return Rectangle.Bottom;
+ default:
+ return Rectangle.Bottom - m.Height - TopMargin;
+ }
}
else if (Axis.AxisPosition == eAxisPosition.Bottom)
{
- return Rectangle.Bottom - m.Height.PointToPixel() + BottomMargin;
+ switch(LabelOrientation)
+ {
+ case eTextOrientation.Vertical:
+ if (Axis.LabelPosition == eTickLabelPosition.Low)
+ {
+ return Rectangle.Bottom - m.Width;
+ }
+ else
+ {
+ return Rectangle.Top;
+ }
+ case eTextOrientation.Diagonal:
+ if (Axis.LabelPosition == eTickLabelPosition.Low)
+ {
+ return Rectangle.Bottom - (m.Width+m.Height)* COS45;
+ }
+ else
+ {
+ return Rectangle.Top;
+ }
+ default:
+ if (Axis.LabelPosition == eTickLabelPosition.Low)
+ {
+ return Rectangle.Bottom - m.Height;
+ }
+ else if (Axis.LabelPosition == eTickLabelPosition.NextTo)
+ {
+ return Rectangle.Top + BottomMargin;
+ }
+ else //TODO:Add support for hight.
+ {
+ return Rectangle.Top + BottomMargin;
+ }
+ }
}
else
{
- var majorHeight = Rectangle.Height / (AxisValues.Count-1);
- if (Axis.AxisType == eAxisType.Cat)
+ var majorHeight = Rectangle.Height / (AxisValues.Count);
+ if (Axis.AxisType == eAxisType.Cat || Axis.AxisType == eAxisType.Date)
{
- return Rectangle.Top + majorHeight * (AxisValues.Count - i - 1) + (majorHeight / 2) - m.Height.PointToPixel() / 2;
+ return Rectangle.Top + majorHeight * (AxisValues.Count - i - 1) + (majorHeight / 2) - m.Height / 2;
}
else
{
- return Rectangle.Top + majorHeight * (AxisValues.Count - i - 1) - m.Height.PointToPixel() / 2;
+ //return Rectangle.Top + majorHeight * (AxisValues.Count - i - 1) - m.Height / 2;
+ return Rectangle.Top + majorHeight * (AxisValues.Count - i - 1);
}
}
}
- private List AddTickmarks(double units, double parentUnit, float tickMarkWidth, eAxisTickMark type)
+ private List AddTickmarks(double units, eTimeUnit? dateUnit, double parentUnit, double tickMarkWidth, eAxisTickMark type)
{
var axisStyle = GetAxisStyleEntry();
var tms = new List();
- double min;
+ double min, max, addMinor=0D;
+ if(double.IsNaN(parentUnit)==false && parentUnit==units)
+ {
+ addMinor = parentUnit / 2;
+ }
+
if (Axis.AxisType == eAxisType.Cat)
{
- min = 0;
+ min = 1;
+ max = AxisValues.Count;
}
else
{
min = Min;
+ max = Max;
}
- float tickMarkWidthInside=0, tickMarkWidthOutside=0;
+
+ double tickMarkWidthInside=0, tickMarkWidthOutside=0;
if(type==eAxisTickMark.In || type==eAxisTickMark.Cross)
{
tickMarkWidthInside = tickMarkWidth;
}
- if(type==eAxisTickMark.Out|| type == eAxisTickMark.Cross)
+ if(type==eAxisTickMark.Out || type == eAxisTickMark.Cross)
{
tickMarkWidthOutside = tickMarkWidth;
}
-
- var diff = Max - min;
- for (double d = min; d <= Max; d += units)
+ var diff = max - min + 1;
+ double d = min + addMinor;
+ while (d <= max+1)
{
if (double.IsNaN(parentUnit) || (d % parentUnit != 0))
{
- float x1, y1, x2, y2;
+ double x1, y1, x2, y2;
switch (Axis.AxisPosition)
{
case eAxisPosition.Left:
y1 = (float)(Rectangle.Top + Rectangle.Height - ((d - min) / diff * Rectangle.Height));
y2 = y1;
- x1 = (float)Rectangle.Right - tickMarkWidthInside;
- x2 = (float)Rectangle.Right + tickMarkWidthOutside;
+ x1 = (float)Rectangle.Right - tickMarkWidthOutside;
+ x2 = (float)Rectangle.Right + tickMarkWidthInside;
break;
case eAxisPosition.Right:
y1 = (float)(Rectangle.Top + Rectangle.Height - ((d - min) / diff * Rectangle.Height));
@@ -417,8 +646,8 @@ private List AddTickmarks(double units, double parentUnit, fl
case eAxisPosition.Top:
x1 = (float)(Rectangle.Left + ((d - min) / diff * Rectangle.Width));
x2 = x1;
- y1 = (float)Rectangle.Bottom - tickMarkWidthInside;
- y2 = (float)Rectangle.Bottom + tickMarkWidthOutside;
+ y1 = (float)Rectangle.Bottom - tickMarkWidthOutside;
+ y2 = (float)Rectangle.Bottom + tickMarkWidthInside;
break;
case eAxisPosition.Bottom:
x1 = (float)(Rectangle.Left + ((d - min) / diff * Rectangle.Width));
@@ -435,16 +664,41 @@ private List AddTickmarks(double units, double parentUnit, fl
tm.X2 = x2;
tm.Y2 = y2;
tm.SetDrawingPropertiesBorder(Axis.Border, axisStyle.BorderReference.Color, true);
+ if(tm.BorderWidth<1) //Excel seems to have this as minimum width for tick marks, so we enforce it here to make sure they are visible.
+ {
+ tm.BorderWidth = 1;
+ }
tms.Add(tm);
}
+ switch (dateUnit)
+ {
+ case eTimeUnit.Years:
+ d = DateTime.FromOADate(d).AddYears((int)units).ToOADate();
+ break;
+ case eTimeUnit.Months:
+ if (units>=1D)
+ {
+ d = DateTime.FromOADate(d).AddMonths((int)units).ToOADate();
+ }
+ else
+ {
+ var dt = DateTime.FromOADate(d);
+ var days = DateTime.DaysInMonth(dt.Year, dt.Month) * units;
+ d += days;
+ }
+ break;
+ default:
+ d += units;
+ break;
+ }
}
return tms;
}
- private List AddGridlines(double units, double parentUnit, ExcelDrawingBorder lineItem, ExcelChartStyleEntry styleEntry)
+ private List AddGridlines(double units, double parentUnit, ExcelDrawingBorder lineItem, ExcelChartStyleEntry styleEntry)
{
var axisStyle = GetAxisStyleEntry();
- var tms = new List();
+ var tms = new List();
double min;
if (Axis.AxisType == eAxisType.Cat)
{
@@ -456,41 +710,108 @@ private List AddGridlines(double units, double parentUnit, Ex
}
var pa = SvgChart.Plotarea;
var diff = Max - min;
+
+ List points = new List();
+
for (double d = min; d <= Max; d += units)
{
if(d==min && Line!=null && Line.BorderWidth>0) continue;
if (double.IsNaN(parentUnit) || (d % parentUnit != 0))
{
- float x1, y1, x2, y2;
+ //float x1, y1, x2, y2;
switch (Axis.AxisPosition)
{
case eAxisPosition.Left:
case eAxisPosition.Right:
- y1 = (float)(pa.Rectangle.Top + pa.Rectangle.Height - ((d - min) / diff * pa.Rectangle.Height));
- y2 = y1;
- x1 = (float)pa.Rectangle.Right;
- x2 = (float)pa.Rectangle.Left;
+ points.Add(new Point(0f, (float)(pa.Rectangle.Top + pa.Rectangle.Height - ((d - min) / diff * pa.Rectangle.Height))));
+ //y1 = (float)(pa.Rectangle.Top + pa.Rectangle.Height - ((d - min) / diff * pa.Rectangle.Height));
+ //y2 = y1;
+ //x1 = (float)pa.Rectangle.Right;
+ //x2 = (float)pa.Rectangle.Left;
break;
case eAxisPosition.Top:
case eAxisPosition.Bottom:
- x1 = (float)(pa.Rectangle.Left + ((d - min) / diff * pa.Rectangle.Width));
- x2 = x1;
- y1 = (float)pa.Rectangle.Top;
- y2 = (float)pa.Rectangle.Bottom;
+ var xValue = (float)(pa.Rectangle.Left + ((d - min) / diff * pa.Rectangle.Width));
+ points.Add(new Point(xValue, 0f));
+ //x1 = (float)(pa.Rectangle.Left + ((d - min) / diff * pa.Rectangle.Width));
+ //x2 = x1;
+ //y1 = (float)pa.Rectangle.Top;
+ //y2 = (float)pa.Rectangle.Bottom;
break;
default:
throw new InvalidOperationException("Invalid axis position");
}
- var tm = new SvgRenderLineItem(SvgChart, SvgChart.Bounds);
- tm.X1 = x1;
- tm.Y1 = y1;
- tm.X2 = x2;
- tm.Y2 = y2;
- tm.SetDrawingPropertiesBorder(lineItem, styleEntry.BorderReference.Color, true, lineItem.Width);
- tms.Add(tm);
+ //var tm = new SvgRenderLineItem(SvgChart, SvgChart.Bounds);
+ //tm.X1 = x1;
+ //tm.Y1 = y1;
+ //tm.X2 = x2;
+ //tm.Y2 = y2;
+ //tm.SetDrawingPropertiesBorder(lineItem, styleEntry.BorderReference.Color, true, lineItem.Width);
+ //tms.Add(tm);
}
}
+
+ float x1, y1, x2, y2;
+
+ string id = "";
+
+ switch (Axis.AxisPosition)
+ {
+ case eAxisPosition.Left:
+ case eAxisPosition.Right:
+ id = "xGridLine";
+ y1 = (float)points.Last().Top;
+ y2 = y1;
+ x1 = (float)pa.Rectangle.Right;
+ x2 = (float)pa.Rectangle.Left;
+ break;
+ case eAxisPosition.Top:
+ case eAxisPosition.Bottom:
+ id = "yGridLine";
+ x1 = (float)points[0].Left;
+ x2 = x1;
+ y1 = (float)pa.Rectangle.Top;
+ y2 = (float)pa.Rectangle.Bottom;
+ break;
+ default:
+ throw new InvalidOperationException("Invalid axis position");
+ }
+
+ var tm = new SvgRenderLineItem(SvgChart, SvgChart.Bounds);
+ tm.X1 = x1;
+ tm.Y1 = y1;
+ tm.X2 = x2;
+ tm.Y2 = y2;
+ tm.SetDrawingPropertiesBorder(lineItem, styleEntry.BorderReference.Color, true, lineItem.Width);
+
+ tm.DefId = id;
+
+ tms.Add(tm);
+
+ var distX = (float)points.Last().Left - points[0].Left;
+ var distY = points[0].Top - (float)points.Last().Top;
+
+ var offsetX = distX / (points.Count-1);
+ var offsetY = distY / (points.Count-1);
+
+ for(int i = 0; i < points.Count; i++)
+ {
+ var refItem = new SvgUseRefItem(SvgChart, SvgChart.Bounds, id);
+ if(id == "xGridLine")
+ {
+ refItem.X = 0f;
+ refItem.Y = offsetY*i;
+ }
+ else if(id == "yGridLine")
+ {
+ refItem.X = offsetX*i;
+ refItem.Y = 0f;
+ }
+ tms.Add(refItem);
+ }
+
+
return tms;
}
@@ -513,19 +834,26 @@ private ExcelChartStyleEntry GetAxisStyleEntry()
return axisStyle;
}
- internal double GetPositionInPlotarea(double val)
+ internal double GetPositionInPlotarea(double val, bool startValue=false)
{
if (Axis.AxisPosition == eAxisPosition.Left || Axis.AxisPosition == eAxisPosition.Right)
{
if (Axis.AxisType == eAxisType.Cat)
{
var majorHeight = SvgChart.Plotarea.Rectangle.Height / Max;
- return (majorHeight * val + (majorHeight / 2));
+ if(startValue)
+ {
+ return majorHeight * val;
+ }
+ else
+ {
+ return majorHeight * val + (majorHeight / 2);
+ }
}
else
{
if (val < Min || val > Max) return double.NaN;
- var diff = Max - Min;
+ var diff = Max - Min + 1;
return (((Max-val) / diff * SvgChart.Plotarea.Rectangle.Height));
}
}
@@ -534,27 +862,58 @@ internal double GetPositionInPlotarea(double val)
if (Axis.AxisType == eAxisType.Cat)
{
var majorWidth = SvgChart.Plotarea.Rectangle.Width / Max;
- return (majorWidth * val + (majorWidth / 2));
+ if (startValue)
+ {
+ return majorWidth * val;
+ }
+ else
+ {
+ return majorWidth * val + (majorWidth / 2);
+ }
}
else
{
if (val < Min || val > Max) return double.NaN;
- var diff = Max - Min;
- return (((val-Min) / diff * SvgChart.Plotarea.Rectangle.Width));
+ var diff = Max - Min + 1;
+ return (((val - Min) / diff * SvgChart.Plotarea.Rectangle.Width));
}
}
}
- protected List GetAxisValue(ExcelChartAxisStandard ax, RenderItem rect, out double? min, out double? max, out double? majorUnit)
+ protected List GetAxisValue(ExcelChartAxisStandard ax, RenderItem rect, out double? min, out double? max, out double? majorUnit, out eTimeUnit? dateUnit, out eTextOrientation orientation)
{
var values = ax.GetAxisValues(out bool isCount);
+
+ var options = new AxisOptions
+ {
+ LockedMin = ax.MinValue,
+ LockedMax = ax.MaxValue,
+ LockedInterval = ax.MajorUnit,
+ LockedIntervalUnit = ax.MajorTimeUnit,
+ AddPadding = ax.AxisType==eAxisType.Val,//(ax.AxisPosition == eAxisPosition.Left || ax.AxisPosition == eAxisPosition.Right),
+ Axis = ax,
+ IsStacked100 = Chart.IsTypePercentStacked(),
+ ChartSize = rect
+ };
+
if (ax.AxisType == eAxisType.Cat &&
isCount == false)
{
- min = 0;
- max = values.Length;
- majorUnit = 1;
+ //min = 0;
+ //max = values.Count / Chart.Series.Count;
+ //majorUnit = 1;
+ //dateUnit = null;
+ //orientation = eTextOrientation.Horizontal;
+ var res = CategoryAxisScaleCalculator.CalculateByWidth(ref values, SvgChart.TextMeasurer, options);
+
+ min = res.Min;
+ max = res.Max;
+ majorUnit = res.MajorInterval;
+ dateUnit = null;
+ orientation = res.TextOrientation;
+
return values.ToList();
}
+
var l = new List();
min = double.MaxValue;
max = double.MinValue;
@@ -574,33 +933,43 @@ protected List GetAxisValue(ExcelChartAxisStandard ax, RenderItem rect,
max = d;
}
}
- var options = new AxisOptions
- {
- LockedMin = ax.MinValue,
- LockedMax = ax.MaxValue,
- LockedInterval = ax.MajorUnit,
- LockedIntervalUnit = ax.MajorTimeUnit,
- AddPadding = ax.AxisPosition == eAxisPosition.Left || ax.AxisPosition == eAxisPosition.Right,
- Axis = ax,
- IsStacked100 = Chart.IsTypePercentStacked()
- };
var length = ax.AxisPosition == eAxisPosition.Left || ax.AxisPosition == eAxisPosition.Right ? SvgChart.Bounds.Height : SvgChart.Bounds.Width; //Fix and use plotarea width/height.
if(isCount)
{
majorUnit = 1;
- for(int i=1;i<=max;i++)
+ dateUnit = null;
+ for (int i=1;i<=max;i++)
{
l.Add(i);
}
- return l;
+ var res = CategoryAxisScaleCalculator.CalculateByWidth(ref l, SvgChart.TextMeasurer, options);
+
+ min = res.Min;
+ max = res.Max;
+ majorUnit = res.MajorInterval;
+ dateUnit = null;
+ orientation = res.TextOrientation;
+
+ return l.ToList();
}
if (ax.IsDate)
{
- var res = DateAxisScaleCalculator.Calculate(min ?? 0, max ?? 0, length, options);
+ AxisScale res;
+ if (ax.IsVertical)
+ {
+ //res = DateAxisScaleCalculator.Calculate(min ?? 0, max ?? 0, length, options);
+ res = DateAxisScaleCalculator.CalculateByHeight(min ?? 0D, max ?? 0D, SvgChart.TextMeasurer, options);
+ }
+ else
+ {
+ res = DateAxisScaleCalculator.CalculateByWidth(min ?? 0D, max ?? 0D, SvgChart.TextMeasurer, options);
+ }
+ orientation = res.TextOrientation;
+ dateUnit = res.MajorDateUnit;
var dt = DateTime.FromOADate(res.Min);
var maxDt = DateTime.FromOADate(res.Max);
- while (dt < maxDt)
+ while (dt <= maxDt)
{
l.Add(dt);
switch(res.MajorDateUnit ?? eTimeUnit.Days)
@@ -632,6 +1001,8 @@ protected List GetAxisValue(ExcelChartAxisStandard ax, RenderItem rect,
min = res.Min;
max = res.Max;
majorUnit = res.MajorInterval;
+ dateUnit= null;
+ orientation = eTextOrientation.Horizontal;
}
return l;
diff --git a/src/EPPlus.Export.ImageRenderer/Svg/Chart/SvgChartLegend.cs b/src/EPPlus.Export.ImageRenderer/Svg/Chart/SvgChartLegend.cs
index 6fcb1342b3..67317d7c93 100644
--- a/src/EPPlus.Export.ImageRenderer/Svg/Chart/SvgChartLegend.cs
+++ b/src/EPPlus.Export.ImageRenderer/Svg/Chart/SvgChartLegend.cs
@@ -19,6 +19,7 @@ Date Author Change
using OfficeOpenXml.Drawing;
using OfficeOpenXml.Drawing.Chart;
using OfficeOpenXml.FormulaParsing.Excel.Functions.MathFunctions;
+using OfficeOpenXml.FormulaParsing.Excel.Functions.Text;
using OfficeOpenXml.Interfaces.Drawing.Text;
using OfficeOpenXml.Style;
using System;
@@ -31,15 +32,17 @@ namespace EPPlusImageRenderer.Svg
internal class SvgChartLegend : SvgChartObject
{
- List _seriesHeadersMeasure =new List();
+ List _seriesHeadersMeasure = new List();
ITextMeasurer _ttMeasurer;
- const int MarginExtra = 2;
- const int MiddleMargin = 10;
- const int LineLength = 30;
- internal SvgChartLegend(SvgChart sc) : base(sc)
+ const float MarginExtra = 1.5f;
+ const float MiddleMargin = 7.5f;
+ const float LineLength = 21;
+ const float MinBarLength = 4;
+ double _maxWidth, _maxHeight;
+ internal SvgChartLegend(SvgChart sc, bool isDataLabelLegend = false) : base(sc)
{
_ttMeasurer = sc.Chart.WorkSheet._package.Settings.TextSettings.GenericTextMeasurerTrueType;
- if (sc.Chart.HasLegend == false || sc.Chart.Series.Count == 0)
+ if (sc.Chart.HasLegend == false && isDataLabelLegend == false || sc.Chart.Series.Count == 0)
{
return;
}
@@ -47,14 +50,25 @@ internal SvgChartLegend(SvgChart sc) : base(sc)
LeftMargin = RightMargin = 3; //4px
TopMargin = BottomMargin = 3; //4px
-
- if (l.Layout.HasLayout)
+ switch (l.Position)
{
- Rectangle = GetRectFromManualLayout(sc, l.Layout);
+ case eLegendPosition.Top:
+ case eLegendPosition.Bottom:
+ _maxWidth = sc.ChartArea.Rectangle.Width * 0.8;
+ _maxHeight = sc.ChartArea.Rectangle.Height * 0.6;
+ break;
+ default:
+ _maxWidth = sc.ChartArea.Rectangle.Width * 0.6;
+ _maxHeight = sc.ChartArea.Rectangle.Height * 0.8;
+ break;
}
- else
+ double entryWidth, entryHeight;
+
+ Rectangle = GetLegendRectangleAndEntrySize(sc, l, out entryWidth, out entryHeight);
+
+ if (l.Layout.HasLayout) //Manual layout will override the position and size of legend, but not the entry size which is used for calculating the position of legend entries.
{
- Rectangle = GetLegendRectangle(sc, l);
+ Rectangle = GetRectFromManualLayout(sc, l.Layout);
}
Bounds.Left = Rectangle.Left;
@@ -66,35 +80,22 @@ internal SvgChartLegend(SvgChart sc) : base(sc)
Rectangle.SetDrawingPropertiesFill(l.Fill, sc.Chart.StyleManager.Style.Title.FillReference.Color);
Rectangle.SetDrawingPropertiesBorder(l.Border, sc.Chart.StyleManager.Style.Title.BorderReference.Color, l.Border.Fill.Style != eFillStyle.NoFill, 0.75);
- SetLegend(sc);
+ SetLegend(sc, entryWidth, entryHeight);
}
- private SvgRenderRectItem GetLegendRectangle(SvgChart sc, ExcelChartLegend l)
+ private SvgRenderRectItem GetLegendRectangleAndEntrySize(SvgChart sc, ExcelChartLegend l, out double entryWidth, out double entryHeight)
{
var rect = new SvgRenderRectItem(sc, sc.Bounds);
- bool isVertical;
- switch (l.Position)
- {
- case eLegendPosition.Top:
- case eLegendPosition.Bottom:
- isVertical = false;
- break;
- default:
- isVertical = true;
- break;
- }
var widest = 0d;
var highest = 0d;
- var textWidth = 0d;
- var height = TopMargin;
var index = 0;
+ //Find the widest and hightest legend entry, and calculate the total width and hight of the legend based on the orientation.
foreach (var ct in sc.Chart.PlotArea.ChartTypes)
{
foreach (var s in ct.Series)
{
- var text = s.GetHeaderText();
- if (string.IsNullOrEmpty(text)) text = $"Series{index + 1}";
+ var text = s.GetHeaderText(index);
var entry = l.Entries.FirstOrDefault(x => x.Index == index);
ExcelTextFont font;
if(entry==null || entry.Font.IsEmpty)
@@ -105,32 +106,68 @@ private SvgRenderRectItem GetLegendRectangle(SvgChart sc, ExcelChartLegend l)
{
font = entry.Font;
}
+
var tm = _ttMeasurer.MeasureText(text, font.GetMeasureFont());
_seriesHeadersMeasure.Add(tm);
+
if(tm.Width > widest)
{
widest = tm.Width;
}
- if (tm.Height > height)
+
+ if(tm.Height> highest)
{
highest = tm.Height;
}
- textWidth += tm.Width;
- height += tm.Height + MiddleMargin;
+
index++;
}
}
- height = height - MiddleMargin + BottomMargin; //remove last margin and add bottom margin
+ var iconLengh = GetIconLenght(sc, highest);
+ entryWidth = iconLengh + MarginExtra + widest;
+ entryHeight = highest;
+ //hight += BottomMargin; //remove last margin and add bottom margin
switch (l.Position)
{
case eLegendPosition.Top:
case eLegendPosition.Bottom:
- rect.Width = textWidth + LeftMargin + RightMargin + ((LineLength + MarginExtra) * index + (MiddleMargin*Math.Max(index-1,0))) ; // 28 is for the line length + 2px between line and text
- rect.Height = TopMargin + BottomMargin + highest + MarginExtra;
+ var fullLength = LeftMargin + entryWidth * index + MarginExtra*(index-1) + RightMargin;
+ if(fullLength > _maxWidth)
+ {
+ var height = TopMargin + highest;
+ var widestLine = 0D;
+ var width = LeftMargin + entryWidth;
+
+ for(int i = 0; i < index; i++)
+ {
+ if (width + entryWidth + RightMargin > _maxWidth)
+ {
+ height = height + highest;
+ if (width + RightMargin > widestLine)
+ {
+ widestLine = width + RightMargin;
+ }
+ width = RightMargin + widest;
+ }
+ else
+ {
+ width += entryWidth + MarginExtra;
+ }
+ }
+
+ height+= BottomMargin;
+ rect.Width = Math.Max(widestLine, width);
+ rect.Height = height + BottomMargin;
+ }
+ else
+ {
+ rect.Width = fullLength;
+ rect.Height = TopMargin + highest + BottomMargin;
+ }
rect.Left = (sc.ChartArea.Rectangle.Width - rect.Width) / 2;
if (l.Position == eLegendPosition.Top)
{
- rect.Top = sc.Title.Rectangle.Top+ sc.Title.Rectangle.Height + MiddleMargin;
+ rect.Top = sc.Title.Rectangle.Bottom + MiddleMargin;
}
else
{
@@ -140,12 +177,19 @@ private SvgRenderRectItem GetLegendRectangle(SvgChart sc, ExcelChartLegend l)
case eLegendPosition.Right:
case eLegendPosition.TopRight:
case eLegendPosition.Left:
- rect.Width = widest + LeftMargin + RightMargin + LineLength + 2; // 28 is for the line length + 2px between line and text
- rect.Height = height + BottomMargin;
+ rect.Width = LeftMargin + entryWidth + RightMargin;
+ rect.Height = TopMargin + (highest * index) + ((index - 1) * MarginExtra) + BottomMargin;
+
+ if (rect.Height > _maxHeight)
+ {
+ rect.Height = _maxHeight;
+ }
+
if (l.Position == eLegendPosition.Right ||
l.Position == eLegendPosition.TopRight)
{
rect.Left = sc.ChartArea.Rectangle.Width - rect.Width - TopMargin;
+ rect.Left = sc.ChartArea.Rectangle.Width - rect.Width - TopMargin;
}
else
{
@@ -154,7 +198,7 @@ private SvgRenderRectItem GetLegendRectangle(SvgChart sc, ExcelChartLegend l)
if (l.Position == eLegendPosition.Left ||
l.Position == eLegendPosition.Right)
{
- rect.Top = sc.ChartArea.Rectangle.Height / 2 + TopMargin + 2;
+ rect.Top = sc.ChartArea.Rectangle.Height / 2 - rect.Height / 2;
}
else
{
@@ -164,21 +208,21 @@ private SvgRenderRectItem GetLegendRectangle(SvgChart sc, ExcelChartLegend l)
}
else
{
- rect.Top = sc.Title.Rectangle.Height + 8 + 8; //Height+Margin Top and Bottom Title
+ rect.Top = sc.Title.Rectangle.Height + 8 + 8; //Height + Margin Top and Bottom Title
}
}
break;
}
- if (isVertical)
- {
- //var top = sc.Title.GetRectangle.Height+8+10;
- //var width = margin;
- }
return rect;
}
- internal void SetLegend(SvgChart sc)
+ private double GetIconLenght(SvgChart sc, double heighestText)
+ {
+ return sc.Chart.IsTypeLine() ? LineLength : Math.Max(MinBarLength, heighestText * 0.4);
+ }
+
+ internal void SetLegend(SvgChart sc, double entryWidth, double entryHeight)
{
int index = 0;
SvgLegendSerie pSls=null;
@@ -196,60 +240,34 @@ internal void SetLegend(SvgChart sc)
case eChartType.LineMarkersStacked100:
case eChartType.LineStacked:
case eChartType.LineStacked100:
- var ls=(ExcelLineChartSerie)s;
- var tm = _seriesHeadersMeasure[index];
- var si = GetSeriesIcon(sc, ls, index, tm, pSls);
- sls.SeriesIcon = si;
-
- var tbLeft = si.X2 + MarginExtra;
- var tbTop = si.Y2 - tm.Height * 0.75; //TODO:Should probably be font ascent
- double tbWidth;
- if (pos == eLegendPosition.Left || pos == eLegendPosition.Right)
- {
- tbWidth = Bounds.Width - tbLeft - RightMargin;
- }
- else
- {
- tbWidth = Bounds.Width - tbLeft - RightMargin;
- }
-
- var tbHeight = tm.Height;
- sls.Textbox = new SvgTextBodyItem(ChartRenderer, Bounds, tbLeft, tbTop, tbWidth, tbHeight);
- sls.Textbox.Bounds.Left = si.X2 + MarginExtra;
-
- var entry = Chart.Legend.Entries.FirstOrDefault(x => x.Index == index);
- var headerText = s.GetHeaderText();
- if (entry == null || entry.Font.IsEmpty)
- {
- //sls.Textbox.AddText(s.GetHeaderText(), sc.Chart.Legend.Font);
- sls.Textbox.ImportParagraph(sc.Chart.Legend.TextBody.Paragraphs.FirstOrDefault(), 0, headerText);
- }
- else
- {
- //sls.Textbox.AddText(s.GetHeaderText(), entry.Font);
- sls.Textbox.ImportParagraph(entry.TextBody.Paragraphs.FirstOrDefault(), 0, headerText);
- }
-
- if (ls.HasMarker() && ls.Marker.Style != eMarkerStyle.None)
- {
- var l = sls.SeriesIcon as SvgRenderLineItem;
- var x= l.X1 + (l.X2 - l.X1) / 2;
- var y = l.Y1;
- sls.MarkerIcon = LineMarkerHelper.GetMarkerItem(sc, ls, x, y, true);
- if((ls.Marker.Style == eMarkerStyle.Plus || ls.Marker.Style == eMarkerStyle.X || ls.Marker.Style == eMarkerStyle.Star) &&
- ls.Marker.Fill.IsEmpty == false)
- {
- sls.MarkerBackground = LineMarkerHelper.GetMarkerBackground(sc, ls, x, y, true);
- }
- else
- {
- sls.MarkerBackground = null;
- }
- }
+ SetLineLegend(sc, index, pSls, pos, s, sls, entryWidth, entryHeight);
+ break;
+ case eChartType.ColumnClustered:
+ case eChartType.ColumnStacked:
+ case eChartType.ColumnStacked100:
+ case eChartType.BarClustered:
+ case eChartType.BarStacked:
+ case eChartType.BarStacked100:
+ SetBarLegend(sc, index, pSls, pos, s, sls, entryWidth, entryHeight);
break;
default:
break;
}
+ if (sc.Chart.Legend.Position == eLegendPosition.Top ||
+ sc.Chart.Legend.Position == eLegendPosition.Bottom)
+ {
+ //if (sls.Textbox.Bounds.Bottom > Rectangle.Bottom)
+ //{
+ // break;
+ //}
+ }
+ else
+ {
+ if (sls.Textbox.Bounds.Bottom > Rectangle.Bottom)
+ {
+ break;
+ }
+ }
SeriesIcon.Add(sls);
pSls = sls;
index++;
@@ -257,53 +275,187 @@ internal void SetLegend(SvgChart sc)
}
}
- private SvgRenderLineItem GetSeriesIcon(SvgChart sc, ExcelLineChartSerie ls, int index, TextMeasurement tm, SvgLegendSerie pSls)
+ private void SetLineLegend(SvgChart sc, int index, SvgLegendSerie pSls, eLegendPosition pos, ExcelChartSerie s, SvgLegendSerie sls, double entryWidth, double entryHeight)
+ {
+ var ls = (ExcelLineChartSerie)s;
+ var tm = _seriesHeadersMeasure[index];
+ TextMeasurement prevTm = tm;
+ if (pSls != null)
+ {
+ prevTm = _seriesHeadersMeasure[index - 1];
+ }
+
+ var si = GetLineSeriesIcon(sc, ls, prevTm, tm, pSls, entryWidth, entryHeight);
+ sls.SeriesIcon = si;
+
+ var tbLeft = si.X2 + MarginExtra;
+ var tbTop = si.Y2 - tm.Height * 0.5; //TODO:Should probably be font ascent
+ double tbWidth;
+ if (pos == eLegendPosition.Left || pos == eLegendPosition.Right)
+ {
+ tbWidth = Bounds.Width - tbLeft - RightMargin;
+ }
+ else
+ {
+ tbWidth = Bounds.Width - tbLeft - RightMargin;
+ }
+
+ var tbHeight = tm.Height;
+ sls.Textbox = new SvgTextBodyItem(ChartRenderer, Bounds, tbLeft, tbTop, tbWidth, tbHeight, false, true);
+ //sls.Textbox.Bounds.Left = si.X2 + MarginExtra;
+
+ var entry = Chart.Legend.Entries.FirstOrDefault(x => x.Index == index);
+ var headerText = s.GetHeaderText(index);
+ if (entry == null || entry.Font.IsEmpty)
+ {
+ //sls.Textbox.AddText(s.GetHeaderText(), sc.Chart.Legend.Font);
+ sls.Textbox.ImportParagraph(sc.Chart.Legend.TextBody.Paragraphs.FirstOrDefault(), 0, headerText);
+ }
+ else
+ {
+ //sls.Textbox.AddText(s.GetHeaderText(), entry.Font);
+ sls.Textbox.ImportParagraph(entry.TextBody.Paragraphs.FirstOrDefault(), 0, headerText);
+ }
+
+ if (ls.HasMarker() && ls.Marker.Style != eMarkerStyle.None)
+ {
+ var l = sls.SeriesIcon as SvgRenderLineItem;
+ var x = l.X1 + (l.X2 - l.X1) / 2;
+ var y = l.Y1;
+ sls.MarkerIcon = LineMarkerHelper.GetMarkerItem(sc, ls, x, y, true);
+ if ((ls.Marker.Style == eMarkerStyle.Plus || ls.Marker.Style == eMarkerStyle.X || ls.Marker.Style == eMarkerStyle.Star) &&
+ ls.Marker.Fill.IsEmpty == false)
+ {
+ sls.MarkerBackground = LineMarkerHelper.GetMarkerBackground(sc, ls, x, y, true);
+ }
+ else
+ {
+ sls.MarkerBackground = null;
+ }
+ }
+ }
+
+ private void SetBarLegend(SvgChart sc, int index, SvgLegendSerie pSls, eLegendPosition pos, ExcelChartSerie s, SvgLegendSerie sls, double entryWidth, double entryHeight)
+ {
+ var bs = (ExcelBarChartSerie)s;
+ var tm = _seriesHeadersMeasure[index];
+ TextMeasurement prevTm = tm;
+ if (pSls != null)
+ {
+ prevTm = _seriesHeadersMeasure[index - 1];
+ }
+ var si = GetBarSeriesIcon(sc, bs, prevTm, tm, pSls, entryWidth, entryHeight);
+ sls.SeriesIcon = si;
+
+ var tbLeft = si.Right + MarginExtra;
+ var tbTop = si.Top - (tm.Height - si.Height) / 2;
+ double tbWidth;
+
+ tbWidth = Bounds.Width - tbLeft - RightMargin;
+
+ var tbHeight = tm.Height;
+ sls.Textbox = new SvgTextBodyItem(ChartRenderer, Bounds, tbLeft, tbTop, tbWidth, tbHeight, false, true);
+ //sls.Textbox.Bounds.Left = si.Bottom + MarginExtra;
+
+ var entry = Chart.Legend.Entries.FirstOrDefault(x => x.Index == index);
+ var headerText = s.GetHeaderText(index);
+ if (entry == null || entry.Font.IsEmpty)
+ {
+ //sls.Textbox.AddText(s.GetHeaderText(), sc.Chart.Legend.Font);
+ sls.Textbox.ImportParagraph(sc.Chart.Legend.TextBody.Paragraphs.FirstOrDefault(), 0, headerText);
+ }
+ else
+ {
+ //sls.Textbox.AddText(s.GetHeaderText(), entry.Font);
+ sls.Textbox.ImportParagraph(entry.TextBody.Paragraphs.FirstOrDefault(), 0, headerText);
+ }
+ }
+
+ private SvgRenderLineItem GetLineSeriesIcon(SvgChart sc, ExcelChartStandardSerie cStandardSerie, TextMeasurement pTm, TextMeasurement tm, SvgLegendSerie pSls, double entryWidth, double entryHeight)
{
- var item = new SvgRenderLineItem(sc, Rectangle.Bounds);
- item.SetDrawingPropertiesFill(ls.Fill, sc.Chart.StyleManager.Style.SeriesLine.FillReference.Color);
- item.SetDrawingPropertiesBorder(ls.Border, sc.Chart.StyleManager.Style.SeriesLine.BorderReference.Color, ls.Border.Fill.Style!=eFillStyle.NoFill, 0.75);
+ var line = new SvgRenderLineItem(sc, Rectangle.Bounds);
+ line.SetDrawingPropertiesFill(cStandardSerie.Fill, sc.Chart.StyleManager.Style.SeriesLine.FillReference.Color);
+ line.SetDrawingPropertiesBorder(cStandardSerie.Border, sc.Chart.StyleManager.Style.SeriesLine.BorderReference.Color, cStandardSerie.Border.Fill.Style != eFillStyle.NoFill, 0.75);
+ var icon = pSls?.SeriesIcon as SvgRenderLineItem;
+
+ GetItemPosition(sc, pTm, tm, pSls, entryWidth, entryHeight, icon?.X1 ?? 0D, icon?.Y1 ?? 0D, out double x, out double y);
+
+ line.X1 = x;
+ line.Y1 = y;
+ line.X2 = x + LineLength;
+ line.Y2 = y;
+ line.LineCap = eLineCap.Round;
+
+ return line;
+ }
+
+ private SvgRenderRectItem GetBarSeriesIcon(SvgChart sc, ExcelChartStandardSerie cStandardSerie, TextMeasurement pTm, TextMeasurement tm, SvgLegendSerie pSls, double entryWidth, double entryHeight)
+ {
+ var item = new SvgRenderRectItem(sc, Rectangle.Bounds);
+ item.SetDrawingPropertiesFill(cStandardSerie.Fill, sc.Chart.StyleManager.Style.SeriesLine.FillReference.Color);
+ item.SetDrawingPropertiesBorder(cStandardSerie.Border, sc.Chart.StyleManager.Style.SeriesLine.BorderReference.Color, cStandardSerie.Border.Fill.Style != eFillStyle.NoFill, 0.75);
+ var iconHeight = GetIconLenght(sc, tm.Height);
+ var icon = pSls?.SeriesIcon as SvgRenderRectItem;
+
+ GetItemPosition(sc, pTm, tm, pSls, entryWidth, entryHeight, icon?.Left ?? 0D, icon?.Top ?? 0D, out double x, out double y);
+ item.LineCap = eLineCap.Round;
+ item.Left = x;
+ item.Top = y;
+ item.Width = iconHeight;
+ item.Height = iconHeight;
+
+ return item;
+ }
+
+ private double GetItemPosition(SvgChart sc, TextMeasurement pTm, TextMeasurement tm, SvgLegendSerie pSls, double entryWidth, double entryHeight, double iconLeft, double iconTop, out double x, out double y)
+ {
+ var topOffset = 0D;
if (sc.Chart.Legend.Position == eLegendPosition.Top ||
sc.Chart.Legend.Position == eLegendPosition.Bottom)
{
- float y = (float)Rectangle.Top + (float)TopMargin + tm.Height / 2 + MarginExtra;
- float x = 0;
+ if (pSls != null && pSls.Textbox.Bounds.Right + entryWidth + RightMargin > _maxWidth)
+ {
+ topOffset += entryHeight + MarginExtra;
+ x = Rectangle.Left + LeftMargin;
+ }
+ else
+ {
+ if (pSls == null)
+ {
+ x = Rectangle.Left + (float)LeftMargin;
+ }
+ else
+ {
+ x = iconLeft + entryWidth + MarginExtra;
+ }
+ }
if (pSls == null)
{
- x = (float)Rectangle.Left + (float)LeftMargin + MarginExtra;
+ y = Rectangle.Top + TopMargin + tm.Height / 2;
}
else
{
- x = (float)pSls.Textbox.Bounds.Right + MiddleMargin;
+ y = iconTop + topOffset;
}
- item.X1 = x;
- item.Y1 = y;
- item.X2 = x + LineLength;
- item.Y2 = y;
- item.LineCap = eLineCap.Round;
+
}
else
{
- double y;
if (pSls == null)
{
- y = TopMargin + tm.Height / 2 + MarginExtra;
+ y = TopMargin + tm.Height / 2;
}
else
{
- var pTm = _seriesHeadersMeasure[index - 1];
- y = ((SvgRenderLineItem)pSls.SeriesIcon).Y1 + pTm.Height / 2 + tm.Height / 2 + MiddleMargin;
+ y = pSls.Textbox.Bounds.Bottom + MiddleMargin;
}
+ x = LeftMargin;
- item.X1 = (float)LeftMargin; //4
- item.Y1 = y;
- item.X2 = (float)LineLength;
- item.Y2 = y;
- item.LineCap = eLineCap.Round;
}
- return item;
+ return topOffset;
}
internal override void AppendRenderItems(List renderItems)
diff --git a/src/EPPlus.Export.ImageRenderer/Svg/Chart/SvgChartLegendIcon.cs b/src/EPPlus.Export.ImageRenderer/Svg/Chart/SvgChartLegendIcon.cs
new file mode 100644
index 0000000000..4069c8be82
--- /dev/null
+++ b/src/EPPlus.Export.ImageRenderer/Svg/Chart/SvgChartLegendIcon.cs
@@ -0,0 +1,117 @@
+using EPPlusImageRenderer.RenderItems;
+using EPPlusImageRenderer.Svg;
+using OfficeOpenXml.Drawing;
+using OfficeOpenXml.Drawing.Chart;
+using OfficeOpenXml.Interfaces.Drawing.Text;
+using System;
+using System.Collections.Generic;
+
+namespace EPPlus.Export.ImageRenderer.Svg.Chart
+{
+ internal class SvgChartLegendIcon : SvgRenderLineItem
+ {
+ const float MarginExtra = 1.5f;
+ const float MiddleMargin = 7.5f;
+ const float LineLength = 21;
+
+ public SvgChartLegendIcon(SvgChart sc, SvgRenderRectItem Rectangle, ExcelChartStandardSerie s, TextMeasurement tm, double topMargin, double leftMargin, TextMeasurement sHM, SvgLegendSerie pSls) : base(sc, Rectangle.Bounds)
+ {
+ SetDrawingPropertiesFill(s.Fill, sc.Chart.StyleManager.Style.SeriesLine.FillReference.Color);
+ SetDrawingPropertiesBorder(s.Border, sc.Chart.StyleManager.Style.SeriesLine.BorderReference.Color, s.Border.Fill.Style != eFillStyle.NoFill, 0.75);
+
+ if (sc.Chart.Legend.Position == eLegendPosition.Top ||
+ sc.Chart.Legend.Position == eLegendPosition.Bottom)
+ {
+ float y = (float)Rectangle.Top + (float)topMargin + tm.Height / 2 + MarginExtra;
+ float x = 0;
+ if (pSls == null)
+ {
+ x = (float)Rectangle.Left + (float)leftMargin;// + MarginExtra;
+ }
+ else
+ {
+ x = (float)pSls.Textbox.Bounds.Right + MiddleMargin;
+ }
+
+ X1 = x;
+ Y1 = y;
+ X2 = x + LineLength;
+ Y2 = y;
+ LineCap = eLineCap.Round;
+ }
+ else
+ {
+ double y;
+ if (pSls == null)
+ {
+ y = topMargin + tm.Height / 2 + MarginExtra;
+ }
+ else
+ {
+ var pTm = sHM;
+ y = ((SvgRenderLineItem)pSls.SeriesIcon).Y1 + pTm.Height / 2 + tm.Height / 2 + MiddleMargin;
+ }
+
+ X1 = (float)leftMargin; //4
+ Y1 = y;
+ X2 = (float)LineLength;
+ Y2 = y;
+ LineCap = eLineCap.Round;
+ }
+ }
+
+ //internal override void AppendRenderItems(List renderItems)
+ //{
+ // throw new NotImplementedException();
+ //}
+
+ //private SvgRenderLineItem GetLineSeriesIcon(SvgChart sc, ExcelChartStandardSerie s, TextMeasurement sHM, TextMeasurement tm, SvgLegendSerie pSls)
+ //{
+ // var item = new SvgRenderLineItem(sc, Rectangle.Bounds);
+ // item.SetDrawingPropertiesFill(s.Fill, sc.Chart.StyleManager.Style.SeriesLine.FillReference.Color);
+ // item.SetDrawingPropertiesBorder(s.Border, sc.Chart.StyleManager.Style.SeriesLine.BorderReference.Color, s.Border.Fill.Style != eFillStyle.NoFill, 0.75);
+
+ // if (sc.Chart.Legend.Position == eLegendPosition.Top ||
+ // sc.Chart.Legend.Position == eLegendPosition.Bottom)
+ // {
+ // float y = (float)Rectangle.Top + (float)TopMargin + tm.Height / 2 + MarginExtra;
+ // float x = 0;
+ // if (pSls == null)
+ // {
+ // x = (float)Rectangle.Left + (float)LeftMargin;// + MarginExtra;
+ // }
+ // else
+ // {
+ // x = (float)pSls.Textbox.Bounds.Right + MiddleMargin;
+ // }
+
+ // item.X1 = x;
+ // item.Y1 = y;
+ // item.X2 = x + LineLength;
+ // item.Y2 = y;
+ // item.LineCap = eLineCap.Round;
+ // }
+ // else
+ // {
+ // double y;
+ // if (pSls == null)
+ // {
+ // y = TopMargin + tm.Height / 2 + MarginExtra;
+ // }
+ // else
+ // {
+ // var pTm = sHM;
+ // y = ((SvgRenderLineItem)pSls.SeriesIcon).Y1 + pTm.Height / 2 + tm.Height / 2 + MiddleMargin;
+ // }
+
+ // item.X1 = (float)LeftMargin; //4
+ // item.Y1 = y;
+ // item.X2 = (float)LineLength;
+ // item.Y2 = y;
+ // item.LineCap = eLineCap.Round;
+ // }
+
+ // return item;
+ //}
+ }
+}
diff --git a/src/EPPlus.Export.ImageRenderer/Svg/Chart/SvgChartObject.cs b/src/EPPlus.Export.ImageRenderer/Svg/Chart/SvgChartObject.cs
index 52aab83468..93d59390d7 100644
--- a/src/EPPlus.Export.ImageRenderer/Svg/Chart/SvgChartObject.cs
+++ b/src/EPPlus.Export.ImageRenderer/Svg/Chart/SvgChartObject.cs
@@ -79,7 +79,6 @@ internal void SetMargins(ExcelTextBody tb)
internal double TopMargin { get; set; }
internal double BottomMargin { get; set; }
internal SvgRenderRectItem Rectangle { get; set; }
- internal SvgRenderLineItem Line { get; set; }
protected static SvgRenderRectItem GetRectFromManualLayout(SvgChart sc, ExcelLayout layout)
{
var rect = new SvgRenderRectItem(sc, sc.ChartArea.Bounds);
@@ -90,6 +89,7 @@ protected static SvgRenderRectItem GetRectFromManualLayout(SvgChart sc, ExcelLay
}
else
{
+ rect.Left = sc.Bounds.Width * (float)(ml.Left ?? 0D) / 100;
//TODO:Add factor from default position
}
//Width is always factor.
@@ -101,6 +101,7 @@ protected static SvgRenderRectItem GetRectFromManualLayout(SvgChart sc, ExcelLay
}
else
{
+ rect.Top = sc.Bounds.Height * (float)(ml.Top ?? 0D) / 100;
//TODO:Add factor from default position
}
//Height is always factor.
diff --git a/src/EPPlus.Export.ImageRenderer/Svg/Chart/SvgChartPlotarea.cs b/src/EPPlus.Export.ImageRenderer/Svg/Chart/SvgChartPlotarea.cs
index c70a230ac3..c4bb427f21 100644
--- a/src/EPPlus.Export.ImageRenderer/Svg/Chart/SvgChartPlotarea.cs
+++ b/src/EPPlus.Export.ImageRenderer/Svg/Chart/SvgChartPlotarea.cs
@@ -31,6 +31,7 @@ internal SvgRenderRectItem GetPlotAreaRectangle(SvgChart sc)
{
var pa = sc.Chart.PlotArea;
TopMargin = BottomMargin = LeftMargin = RightMargin = 10.5; //14px
+ TopMargin = BottomMargin = LeftMargin = RightMargin = 10.5; //14px
var rect = new SvgRenderRectItem(sc, sc.Bounds);
if (pa.Layout.HasLayout)
{
@@ -38,44 +39,117 @@ internal SvgRenderRectItem GetPlotAreaRectangle(SvgChart sc)
}
else
{
- var lp = sc.Chart.Legend?.Position;
- rect.Top = (lp==eLegendPosition.Top ? sc.Legend.Rectangle.GlobalBottom : sc.Title?.Rectangle?.GlobalBottom ?? 0d) + TopMargin;
- if(sc.HorizontalAxis!=null && sc.Chart.XAxis.LabelPosition==eTickLabelPosition.High)
- {
- rect.Top += sc.HorizontalAxis.Rectangle.Height;
- }
- rect.Left = lp == eLegendPosition.Left ? sc.Legend.Rectangle.GlobalRight + LeftMargin : LeftMargin;
- if(sc.VerticalAxis!=null)
- {
- rect.Left = sc.VerticalAxis.Rectangle?.GlobalRight ?? sc.VerticalAxis.Title.Rectangle.GlobalRight;
- }
+ rect.Top = GetPlotAreaTop(sc);
+ rect.Left = GetPlotAreaLeft(sc);
+ rect.Width = GetPlotAreaWidth(sc, rect);
+ rect.Height = GetPlotAreaHeight(sc, rect);
+ }
+
+ rect.SetDrawingPropertiesFill(pa.Fill, sc.Chart.StyleManager.Style.PlotArea.FillReference.Color);
+ rect.SetDrawingPropertiesBorder(pa.Border, sc.Chart.StyleManager.Style.PlotArea.BorderReference.Color, pa.Border.Fill.Style != eFillStyle.NoFill, 0.75);
+ return rect;
+ }
+
+ private double GetPlotAreaHeight(SvgChart sc, SvgRenderRectItem rect)
+ {
+ var bottomAxis = GetAxisByPosition(sc, eAxisPosition.Bottom);
+ double vaHeight = 0;
+ if (bottomAxis!=null)
+ {
+ vaHeight = (bottomAxis.Rectangle?.Height ?? 0D) + (bottomAxis.Title?.TextBox?.GetActualHeight() ?? 0D);
+ }
+ if (sc.Chart.Legend?.Position == eLegendPosition.Bottom)
+ {
+ vaHeight += sc.Legend.Rectangle.Height + sc.Legend.TopMargin;
+ }
+ return sc.Bounds.Height - rect.GlobalTop - vaHeight - BottomMargin;
+ }
+
+ private double GetPlotAreaWidth(SvgChart sc, SvgRenderRectItem rect)
+ {
+ var rightAxis = GetAxisByPosition(sc, eAxisPosition.Right);
+ var lp = sc.Chart.Legend?.Position;
+ var left = ((lp == eLegendPosition.Right || lp == eLegendPosition.TopRight) && sc.Legend != null ?
+ sc.Legend.Bounds.GlobalLeft - RightMargin :
+ sc.ChartArea.Rectangle.Width - RightMargin);
- rect.Width = (lp == eLegendPosition.Right || lp == eLegendPosition.TopRight ?
- sc.Legend.Bounds.GlobalLeft - RightMargin :
- sc.ChartArea.Rectangle.Width - RightMargin)
- - rect.GlobalLeft;
+ if (rightAxis == null)
+ {
+ return left - rect.GlobalLeft;
+ }
+ else
+ {
+ var rightWidth = (rightAxis.Title?.TextBox.GetActualWidth() ?? 0D) + (rightAxis.Rectangle?.Width ?? 0D);
+ return left - rightWidth - rect.Left;
+ }
+ }
+ private double GetPlotAreaLeft(SvgChart sc)
+ {
+ var left = LeftMargin;
+ if(sc.Chart.Legend?.Position == eLegendPosition.Left)
+ {
+ left += sc.Legend.Bounds.Width + sc.Legend.RightMargin;
+ }
- double vaHeight=0, vaTitleHeight=0;
- if(sc.HorizontalAxis != null)
+ var leftAxis = GetAxisByPosition(sc, eAxisPosition.Left);
+ if (leftAxis != null)
+ {
+ if(leftAxis.Title!=null)
{
- vaHeight = (sc.HorizontalAxis.Rectangle?.Height ?? 0D) + (sc.HorizontalAxis.Title?.Rectangle?.Height ?? 0D);
+ left += leftAxis.Title.TextBox.GetActualWidth();
}
- if(lp==eLegendPosition.Bottom)
+ if (leftAxis.Rectangle != null)
{
- vaHeight += sc.Legend.Rectangle.Height;
+ left += leftAxis.Rectangle.Width + 1.5;
}
- rect.Height = sc.Bounds.Height - rect.GlobalTop - vaHeight - vaTitleHeight - BottomMargin;
+ }
+ return left;
+ }
+ private double GetPlotAreaTop(SvgChart sc)
+ {
+ double haHeight = 0;
+ var topAxis = GetAxisByPosition(sc, eAxisPosition.Top);
+ if (topAxis == null)
+ {
+ //var bottomAxis = GetAxisByPosition(sc, eAxisPosition.Bottom);
+ //if (bottomAxis != null && sc.Chart.XAxis.LabelPosition == eTickLabelPosition.High)
+ //{
+ // top += bottomAxis.Rectangle.Height;
+ //}
+ //return top;
+ }
+ else
+ {
+ haHeight = (topAxis.Rectangle?.Height ?? 0D) + (topAxis.Title?.TextBox?.GetActualHeight() ?? 0D);
}
- rect.SetDrawingPropertiesFill(pa.Fill, sc.Chart.StyleManager.Style.PlotArea.FillReference.Color);
- rect.SetDrawingPropertiesBorder(pa.Border, sc.Chart.StyleManager.Style.PlotArea.BorderReference.Color, pa.Border.Fill.Style != eFillStyle.NoFill, 0.75);
- return rect;
+ return (sc.Chart.Legend?.Position == eLegendPosition.Top ? sc.Legend.Bounds.Bottom : sc.Title?.Rectangle?.GlobalBottom ?? 0d) + haHeight + TopMargin;
+ }
+
+ private SvgChartAxis GetAxisByPosition(SvgChart sc, eAxisPosition pos)
+ {
+ if (sc.HorizontalAxis != null && sc.HorizontalAxis.Axis.AxisPosition == pos)
+ {
+ return sc.HorizontalAxis;
+ }
+ else if (sc.VerticalAxis != null && sc.VerticalAxis.Axis.AxisPosition == pos)
+ {
+ return sc.VerticalAxis;
+ }
+ else if (sc.SecondHorizontalAxis != null && sc.SecondHorizontalAxis.Axis.AxisPosition == pos)
+ {
+ return sc.SecondHorizontalAxis;
+ }
+ else if (sc.SecondVerticalAxis != null && sc.SecondVerticalAxis.Axis.AxisPosition == pos)
+ {
+ return sc.SecondVerticalAxis;
+ }
+ return null;
}
internal override void AppendRenderItems(List renderItems)
{
renderItems.Add(Rectangle);
}
-
}
}
diff --git a/src/EPPlus.Export.ImageRenderer/Svg/Chart/SvgChartTitle.cs b/src/EPPlus.Export.ImageRenderer/Svg/Chart/SvgChartTitle.cs
index 8717399d6c..94ea36f721 100644
--- a/src/EPPlus.Export.ImageRenderer/Svg/Chart/SvgChartTitle.cs
+++ b/src/EPPlus.Export.ImageRenderer/Svg/Chart/SvgChartTitle.cs
@@ -14,12 +14,16 @@ Date Author Change
using EPPlus.Export.ImageRenderer.RenderItems.SvgItem;
using EPPlus.Fonts.OpenType.Utils;
using EPPlus.Graphics;
+using EPPlus.Fonts.OpenType.Utils;
+using EPPlus.Graphics;
using EPPlusImageRenderer.RenderItems;
using OfficeOpenXml;
using OfficeOpenXml.Drawing;
using OfficeOpenXml.Drawing.Chart;
using OfficeOpenXml.FormulaParsing.Excel.Functions.MathFunctions;
using OfficeOpenXml.FormulaParsing.Excel.Functions.Text;
+using OfficeOpenXml.Style;
+using OfficeOpenXml.Utils.EnumUtils;
using System;
using System.Collections.Generic;
using System.Globalization;
@@ -44,9 +48,12 @@ internal SvgChartTitle(SvgChart sc, ExcelChartTitleStandard t, string defaultTex
{
_svgChart = sc;
+
//These are hard coded margins for the title box.
LeftMargin = RightMargin = 3; //4px
TopMargin = BottomMargin = 1.5; //2px
+ LeftMargin = RightMargin = 3; //4px
+ TopMargin = BottomMargin = 1.5; //2px
var maxWidth = sc.Bounds.Width * 0.8;
var maxHeight = sc.Bounds.Height / 2D;
@@ -57,52 +64,43 @@ internal SvgChartTitle(SvgChart sc, ExcelChartTitleStandard t, string defaultTex
}
else
{
- if (string.IsNullOrEmpty(t.Text) == false)
+ if (string.IsNullOrEmpty(t.DisplayedText) == false)
{
- _titleText = t.Text;
+ _titleText = t.DisplayedText;
}
else
{
- _titleText = defaultText;
+ _titleText = t.Font.GetCapitalizedText(defaultText);
}
}
-
+
if (t.Layout.HasLayout) //Only for the main chart title, axis titles don't support manual layout in Excel.
{
- Rectangle = GetRectFromManualLayout(sc, t.Layout);
- var top = Rectangle.Top;
- var left = Rectangle.Left;
- Rectangle.Width = (float)maxWidth;
- Rectangle.Height = (float)maxHeight;
-
- InitTextBox();
- TextBox.Top = top;
- TextBox.Left = left;
+
+ InitTextBox(maxWidth, maxHeight);
+ var mr = GetRectFromManualLayout(sc, t.Layout);
+ TextBox.Top = mr.Top;
+ TextBox.Left = mr.Left;
}
else
{
- Rectangle = new SvgRenderRectItem(sc, sc.Bounds);
- if (axis==null)
+ if (axis == null)
{
- Rectangle.Width = (float)maxWidth;
- Rectangle.Height = (float)maxHeight;
-
- InitTextBox();
+ InitTextBox(maxWidth, maxHeight);
TextBox.Top = (float)6; //6 point for the chart title standard offset.
TextBox.Left = (float)(sc.Bounds.Width - TextBox.Width) / 2;
}
else
{
var isVertical = axis.Axis.IsVertical;
- Rectangle.Width = sc.Bounds.Width * (isVertical ? 0.2 : 0.8); //Max Width.
- Rectangle.Height = sc.Bounds.Height * (isVertical ? 0.8 : 0.2); //Max Height.
+ maxWidth = sc.Bounds.Width * (isVertical ? 0.2 : 0.8); //Max Width.
+ maxHeight = sc.Bounds.Height * (isVertical ? 0.8 : 0.2); //Max Height.
- InitTextBox();
- Rectangle = (SvgRenderRectItem)TextBox.Rectangle;
+ InitTextBox(maxWidth, maxHeight);
SetAxisTitleRect(sc, axis);
}
}
-
+ Bounds = Rectangle.Bounds;
Rectangle.SetDrawingPropertiesFill(t.Fill, sc.Chart.StyleManager.Style.Title.FillReference.Color);
Rectangle.SetDrawingPropertiesBorder(t.Border, sc.Chart.StyleManager.Style.Title.BorderReference.Color, t.Border.Fill.Style != eFillStyle.NoFill, 0.75);
}
@@ -114,12 +112,20 @@ private void SetAxisTitleRect(SvgChart sc, SvgChartAxis axis)
{
case eAxisPosition.Left:
Rectangle.Top = sc.GetPlotAreaTop();
- Rectangle.Left = sc.Chart.HasLegend && sc.Chart.Legend.Position == eLegendPosition.Left ? sc.Legend.Rectangle.Right : margin;
+ Rectangle.Left = sc.Chart.HasLegend && sc.Chart.Legend.Position == eLegendPosition.Left ? sc.Legend.Rectangle.Right + LeftMargin : margin;
+ break;
+ case eAxisPosition.Right:
+ Rectangle.Top = sc.GetPlotAreaTop();
+ Rectangle.Left = sc.Chart.HasLegend && sc.Chart.Legend.Position == eLegendPosition.Right || sc.Chart.Legend.Position == eLegendPosition.TopRight ? sc.Legend.Rectangle.Left - Rectangle.Width - margin : sc.Bounds.Right - Rectangle.Width - margin;
break;
case eAxisPosition.Bottom:
Rectangle.Top = sc.ChartArea.Rectangle.Height - margin - Rectangle.Height;
Rectangle.Left = GetHorizontalLeft(sc);
break;
+ case eAxisPosition.Top:
+ Rectangle.Top = sc.Title != null && sc.Title._title.Layout.HasLayout==false ? sc.Title.Rectangle.Bottom+margin : margin;
+ Rectangle.Left = GetHorizontalLeft(sc);
+ break;
}
}
@@ -142,9 +148,9 @@ private double GetHorizontalLeft(SvgChart sc)
private static string GetDefaultChartTitleText(SvgChart sc, ExcelChartTitleStandard t, string defaultText)
{
- if (string.IsNullOrEmpty(t.Text) == false)
+ if (string.IsNullOrEmpty(t.DisplayedText) == false)
{
- defaultText = t.Text;
+ defaultText = t.DisplayedText;
}
else if (sc.Chart.PlotArea.ChartTypes.Count == 1 && sc.Chart.Series.Count == 1)
{
@@ -161,7 +167,7 @@ private static string GetDefaultChartTitleText(SvgChart sc, ExcelChartTitleStand
}
else
{
- defaultText = s.GetHeaderText();
+ defaultText = s.GetHeaderText(0);
}
}
else
@@ -173,22 +179,22 @@ private static string GetDefaultChartTitleText(SvgChart sc, ExcelChartTitleStand
return defaultText;
}
- internal void InitTextBox()
+ internal void InitTextBox(double maxWidth, double maxHeight)
{
- TextBox = new SvgTextBox(_svgChart, _svgChart.ChartArea.Bounds, Rectangle.Width, Rectangle.Height);
+ TextBox = new SvgTextBox(_svgChart, _svgChart.ChartArea.Bounds, maxWidth, maxHeight);
if(_title.Rotation != 0)
{
TextBox.Rotation = _title.Rotation;
+ TextBox.Rotation = _title.Rotation;
}
if (_title.TextBody.Paragraphs.Count > 0)
{
- TextBox.ImportTextBody(_title.TextBody);
+ TextBox.ImportTextBody(_title.TextBody, true, ExcelHorizontalAlignment.Center);
}
else
{
- var text = string.IsNullOrEmpty(_title.Text) ? _titleText : _title.Text;
var p = _title.DefaultTextBody.Paragraphs.FirstOrDefault();
- TextBox.ImportParagraph(p, 0, text);
+ TextBox.ImportParagraph(p, 0, _titleText);
}
TextBox.LeftMargin = LeftMargin;
@@ -197,6 +203,13 @@ internal void InitTextBox()
TextBox.BottomMargin = BottomMargin;
TextBox.TextBody.VerticalAlignment = eTextAnchoringType.Top;
Rectangle = (SvgRenderRectItem)TextBox.Rectangle;
+
+ TextBox.LeftMargin = LeftMargin;
+ TextBox.RightMargin = RightMargin;
+ TextBox.TopMargin = TopMargin;
+ TextBox.BottomMargin = BottomMargin;
+ TextBox.TextBody.VerticalAlignment = eTextAnchoringType.Top;
+ Rectangle = (SvgRenderRectItem)TextBox.Rectangle;
}
public SvgTextBox TextBox
@@ -205,6 +218,8 @@ public SvgTextBox TextBox
}
internal override void AppendRenderItems(List renderItems)
{
+ var p = _title.DefaultTextBody.Paragraphs.FirstOrDefault();
+ TextBox.TextBody.FontColorString = "#" + p.DefaultRunProperties.Fill.Color.ToColorString();
TextBox.AppendRenderItems(renderItems);
TextBox.Rectangle.SetDrawingPropertiesFill(_title.Fill, _svgChart.Chart.StyleManager.Style.Title.FillReference.Color);
TextBox.Rectangle.SetDrawingPropertiesBorder(_title.Border, _svgChart.Chart.StyleManager.Style.Title.BorderReference.Color, _title.Border.Fill.Style != eFillStyle.NoFill, 0.75);
diff --git a/src/EPPlus.Export.ImageRenderer/Svg/Chart/eTextOrientation.cs b/src/EPPlus.Export.ImageRenderer/Svg/Chart/eTextOrientation.cs
new file mode 100644
index 0000000000..232bae630a
--- /dev/null
+++ b/src/EPPlus.Export.ImageRenderer/Svg/Chart/eTextOrientation.cs
@@ -0,0 +1,21 @@
+/*************************************************************************************************
+ Required Notice: Copyright (C) EPPlus Software AB.
+ This software is licensed under PolyForm Noncommercial License 1.0.0
+ and may only be used for noncommercial purposes
+ https://polyformproject.org/licenses/noncommercial/1.0.0/
+
+ A commercial license to use this software can be purchased at https://epplussoftware.com
+ *************************************************************************************************
+ Date Author Change
+ *************************************************************************************************
+ 27/11/2025 EPPlus Software AB EPPlus 9
+ *************************************************************************************************/
+namespace EPPlus.Export.ImageRenderer
+{
+ internal enum eTextOrientation
+ {
+ Horizontal,
+ Diagonal,
+ Vertical,
+ }
+}
\ No newline at end of file
diff --git a/src/EPPlus.Export.ImageRenderer/Svg/DefinitionUtils/DefinitionGroup.cs b/src/EPPlus.Export.ImageRenderer/Svg/DefinitionUtils/DefinitionGroup.cs
new file mode 100644
index 0000000000..e4301600f2
--- /dev/null
+++ b/src/EPPlus.Export.ImageRenderer/Svg/DefinitionUtils/DefinitionGroup.cs
@@ -0,0 +1,32 @@
+using EPPlusImageRenderer;
+using EPPlusImageRenderer.RenderItems;
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+
+namespace EPPlus.Export.ImageRenderer.Svg.DefinitionUtils
+{
+ internal class DefinitionGroup : RenderItem
+ {
+ internal List Items = new List();
+
+ public DefinitionGroup(DrawingBase renderer) : base(renderer)
+ {
+ }
+
+ public override RenderItemType Type => RenderItemType.Group;
+
+ public override void Render(StringBuilder sb)
+ {
+ sb.Append("");
+
+ foreach(var item in Items)
+ {
+ item.Render(sb);
+ }
+
+ sb.Append(" ");
+ }
+ }
+}
diff --git a/src/EPPlus.Export.ImageRenderer/Svg/DefinitionUtils/LinePattern.cs b/src/EPPlus.Export.ImageRenderer/Svg/DefinitionUtils/LinePattern.cs
new file mode 100644
index 0000000000..460ff8c80c
--- /dev/null
+++ b/src/EPPlus.Export.ImageRenderer/Svg/DefinitionUtils/LinePattern.cs
@@ -0,0 +1,76 @@
+using EPPlusImageRenderer;
+using EPPlusImageRenderer.RenderItems;
+using EPPlusImageRenderer.Svg;
+using OfficeOpenXml.Drawing;
+using OfficeOpenXml.Drawing.Style;
+using OfficeOpenXml.Utils;
+using OfficeOpenXml.Utils.EnumUtils;
+using System;
+using System.Collections.Generic;
+using System.Drawing;
+using System.Linq;
+using System.Text;
+
+namespace EPPlus.Export.ImageRenderer.Svg.DefinitionUtils
+{
+ enum LinePatternType
+ {
+ Vertical,
+ Horizontal
+ }
+
+ internal class LinePattern : PatternItem
+ {
+ LinePatternType type;
+
+ internal SvgRenderLineItem LineItem;
+
+ public LinePattern(DrawingBase baseRend, string id, LinePatternType linesType) : base(baseRend, id)
+ {
+ type = linesType;
+ LineItem = new SvgRenderLineItem(baseRend, Bounds);
+
+ LineItem.BorderWidth = 2;
+ LineItem.Suffix = "%";
+
+ LineItem.BorderColor = "#" + Color.DarkGoldenrod.ToColorString();
+
+ switch (type)
+ {
+ case LinePatternType.Vertical:
+ LineItem.X1 = 0; LineItem.X2 = 0; LineItem.Y1 = 0; LineItem.Y2 = 100;
+ break;
+ case LinePatternType.Horizontal:
+ LineItem.X1 = 0; LineItem.X2 = 100; LineItem.Y1 = 0; LineItem.Y2 = 0;
+ break;
+ }
+
+ SetNumberOfLines(6);
+ }
+
+ public override RenderItemType Type => RenderItemType.Reference;
+
+ ///
+ /// Sets number of lines via the width or height percent
+ ///
+ internal void SetNumberOfLines(int numberOfLines)
+ {
+ var percentOf = 100d / (double)numberOfLines;
+ switch (type)
+ {
+ case LinePatternType.Vertical:
+ widthPercent = percentOf;
+ break;
+ case LinePatternType.Horizontal:
+ heightPercent = percentOf;
+ break;
+ }
+ }
+
+ public override void Render(StringBuilder sb)
+ {
+ _items.Add(LineItem);
+ base.Render(sb);
+ }
+ }
+}
diff --git a/src/EPPlus.Export.ImageRenderer/Svg/DefinitionUtils/LinearGradient.cs b/src/EPPlus.Export.ImageRenderer/Svg/DefinitionUtils/LinearGradient.cs
new file mode 100644
index 0000000000..9c50b66e8a
--- /dev/null
+++ b/src/EPPlus.Export.ImageRenderer/Svg/DefinitionUtils/LinearGradient.cs
@@ -0,0 +1,70 @@
+using EPPlusImageRenderer;
+using EPPlusImageRenderer.RenderItems;
+using EPPlusImageRenderer.Utils;
+using OfficeOpenXml.Drawing;
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using EPPlus.Export.ImageRenderer.RenderItems;
+using EPPlus.Fonts.OpenType.Utils;
+using EPPlusImageRenderer.Constants;
+using OfficeOpenXml.Drawing.Style.Coloring;
+using OfficeOpenXml.Drawing.Style.Fill;
+using OfficeOpenXml.Drawing.Theme;
+using OfficeOpenXml.FormulaParsing.Excel.Functions.MathFunctions;
+using OfficeOpenXml.Utils;
+using System.Drawing;
+using System.Globalization;
+using TypeConv = OfficeOpenXml.Utils.TypeConversion;
+
+
+namespace EPPlus.Export.ImageRenderer.Svg.DefinitionUtils
+{
+ internal class LinearGradient : RenderItem
+ {
+ internal DrawGradientFill GradientFillExtra;
+ internal double Degrees;
+
+ string _id;
+ bool userSpaceOnUse = false;
+
+ public LinearGradient(DrawingBase renderer, string id) : base(renderer)
+ {
+ _id = id;
+ }
+
+ public override RenderItemType Type => RenderItemType.Group;
+
+ public override void Render(StringBuilder sb)
+ {
+ sb.Append($"");
+ SetStopColors(sb, GradientFillExtra, PathFillMode.Norm);
+ sb.Append(" ");
+ }
+
+ private string GetOpacity(ExcelDrawingColorManager c)
+ {
+ var opacityTransform = c.Transforms?.FirstOrDefault(x => x.Type == OfficeOpenXml.Drawing.Style.Coloring.eColorTransformType.Alpha);
+ if (opacityTransform == null) return "";
+
+ return $"stop-opacity=\"{opacityTransform.Value.ToString("0")}%\"";
+ }
+
+ private void SetStopColors(StringBuilder defSb, DrawGradientFill gradientFill, PathFillMode fillMode)
+ {
+ int ix = 0;
+
+ //Svg requires starting at 0 and moving towards 100% Excel sometimes starts at 100
+ //Sort to get around that
+ var sortedGradientColors = gradientFill.Colors.OrderBy(x => x.Position);
+
+ foreach (var c in sortedGradientColors)
+ {
+ var color = ColorUtils.GetAdjustedColor(fillMode, c.Color);
+ // TODO: check if ix should be increased...?
+ defSb.Append($" ");
+ }
+ }
+ }
+}
diff --git a/src/EPPlus.Export.ImageRenderer/Svg/DefinitionUtils/MaskGroup.cs b/src/EPPlus.Export.ImageRenderer/Svg/DefinitionUtils/MaskGroup.cs
new file mode 100644
index 0000000000..288095c0ea
--- /dev/null
+++ b/src/EPPlus.Export.ImageRenderer/Svg/DefinitionUtils/MaskGroup.cs
@@ -0,0 +1,40 @@
+using EPPlus.Fonts.OpenType.Utils;
+using EPPlusImageRenderer;
+using EPPlusImageRenderer.RenderItems;
+using System;
+using System.Collections.Generic;
+using System.Globalization;
+using System.Linq;
+using System.Text;
+
+namespace EPPlus.Export.ImageRenderer.Svg.DefinitionUtils
+{
+ internal class MaskGroup : RenderItem
+ {
+ protected string _id = null;
+
+ protected List _items = new List();
+
+ public MaskGroup(DrawingBase renderer, string id) : base(renderer)
+ {
+ _id = id;
+ }
+
+ public override RenderItemType Type => RenderItemType.Group;
+
+ public override void Render(StringBuilder sb)
+ {
+ sb.Append($"");
+
+ foreach (var item in _items)
+ {
+ item.Render(sb);
+ }
+
+ sb.Append(" ");
+ }
+ }
+}
diff --git a/src/EPPlus.Export.ImageRenderer/Svg/DefinitionUtils/PatternItem.cs b/src/EPPlus.Export.ImageRenderer/Svg/DefinitionUtils/PatternItem.cs
new file mode 100644
index 0000000000..a9a9c26931
--- /dev/null
+++ b/src/EPPlus.Export.ImageRenderer/Svg/DefinitionUtils/PatternItem.cs
@@ -0,0 +1,40 @@
+using EPPlus.Fonts.OpenType.Utils;
+using EPPlusImageRenderer;
+using EPPlusImageRenderer.RenderItems;
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using System.Globalization;
+
+namespace EPPlus.Export.ImageRenderer.Svg.DefinitionUtils
+{
+ internal class PatternItem : RenderItem
+ {
+ string _id = null;
+
+ protected List _items = new List();
+
+ public PatternItem(DrawingBase baseRend, string id) : base(baseRend)
+ {
+ _id = id;
+ }
+
+ protected double heightPercent = 100d;
+ protected double widthPercent = 100d;
+
+ public override RenderItemType Type => RenderItemType.Group;
+
+ public override void Render(StringBuilder sb)
+ {
+ sb.Append($"");
+
+ foreach (var item in _items)
+ {
+ item.Render(sb);
+ }
+
+ sb.Append(" ");
+ }
+ }
+}
diff --git a/src/EPPlus.Export.ImageRenderer/Svg/DefinitionUtils/SymbolGroup.cs b/src/EPPlus.Export.ImageRenderer/Svg/DefinitionUtils/SymbolGroup.cs
new file mode 100644
index 0000000000..da2ffff6c4
--- /dev/null
+++ b/src/EPPlus.Export.ImageRenderer/Svg/DefinitionUtils/SymbolGroup.cs
@@ -0,0 +1,48 @@
+using EPPlus.Fonts.OpenType.Utils;
+using EPPlusImageRenderer;
+using EPPlusImageRenderer.RenderItems;
+using System;
+using System.Collections.Generic;
+using System.Globalization;
+using System.Linq;
+using System.Text;
+
+namespace EPPlus.Export.ImageRenderer.Svg.DefinitionUtils
+{
+ internal class SymbolGroup : RenderItem
+ {
+ protected const string _urlRef = "url(#{0})";
+
+ protected string _id = null;
+
+ protected List _items = new List();
+
+ internal string Mask = null;
+
+ public SymbolGroup(DrawingBase renderer, string id) : base(renderer)
+ {
+ _id = id;
+ }
+
+ public override RenderItemType Type => RenderItemType.Group;
+
+ public override void Render(StringBuilder sb)
+ {
+ sb.Append($"");
+
+ foreach (var item in _items)
+ {
+ item.Render(sb);
+ }
+
+ sb.Append(" ");
+ }
+ }
+}
diff --git a/src/EPPlus.Export.ImageRenderer/Svg/DefinitionUtils/UtillNodes/DynamicGridDefGroup.cs b/src/EPPlus.Export.ImageRenderer/Svg/DefinitionUtils/UtillNodes/DynamicGridDefGroup.cs
new file mode 100644
index 0000000000..2321bc63fc
--- /dev/null
+++ b/src/EPPlus.Export.ImageRenderer/Svg/DefinitionUtils/UtillNodes/DynamicGridDefGroup.cs
@@ -0,0 +1,96 @@
+using EPPlus.Fonts.OpenType.Utils;
+using EPPlusImageRenderer;
+using EPPlusImageRenderer.RenderItems;
+using System;
+using System.Collections.Generic;
+using System.Drawing;
+using System.Globalization;
+using System.Linq;
+using System.Text;
+
+namespace EPPlus.Export.ImageRenderer.Svg.DefinitionUtils.UtillNodes
+{
+ internal class DynamicGridDefGroup : RenderItem
+ {
+ protected const string _urlRef = "\"url(#{0})\"";
+
+ internal string Id { get; private set; }
+
+ internal List Items = new List();
+
+ internal LinePattern LnPatternHorizontal { get; private set; }
+ internal LinePattern LnPatternVertical { get; private set; }
+
+ FadeOutMask maskItem;
+
+ LinearGradient fadeOutGradient;
+
+ DynamicGridItem gridItem;
+
+ internal string TopId { get; private set; }
+
+ public DynamicGridDefGroup(DrawingBase renderer, string id, int linesX, int linesY): base(renderer)
+ {
+ Id = id;
+
+ string lnGradId = id + "_lnGradFade";
+ string maskId = id + "_maskFade";
+ string horzId = id + "_lnHorizontal";
+ string verId = id + "_lnVertical";
+
+ fadeOutGradient = CreateFadeOutGradient(lnGradId);
+ Items.Add(fadeOutGradient);
+
+ maskItem = new FadeOutMask(renderer, maskId, string.Format("url(#{0})", lnGradId));
+
+ Items.Add(maskItem);
+
+ LnPatternHorizontal = new LinePattern(renderer, horzId, LinePatternType.Horizontal);
+ LnPatternVertical = new LinePattern(renderer, verId, LinePatternType.Vertical);
+
+ SetNumLines(linesX, linesY);
+
+ Items.Add(LnPatternHorizontal);
+ Items.Add(LnPatternVertical);
+
+ gridItem = new DynamicGridItem(renderer, id, maskId, horzId, verId);
+ Items.Add(gridItem);
+ }
+
+ internal void SetNumLines(int numLinesHorizontal, int numLinesVertical)
+ {
+ LnPatternHorizontal.SetNumberOfLines(numLinesHorizontal);
+ LnPatternVertical.SetNumberOfLines(numLinesVertical);
+ }
+
+ LinearGradient CreateFadeOutGradient(string id)
+ {
+ var colors = new List();
+ colors.Add(Color.Black);
+ colors.Add(Color.White);
+
+ var stops = new List();
+ stops.Add(0);
+ stops.Add(100);
+
+ fadeOutGradient = new LinearGradient(DrawingRenderer, id);
+ fadeOutGradient.GradientFillExtra = new DrawGradientFill(colors, stops);
+
+ return fadeOutGradient;
+ }
+
+ public override RenderItemType Type => RenderItemType.Group;
+
+ public override void Render(StringBuilder sb)
+ {
+ sb.Append($"");
+
+ foreach (var item in Items)
+ {
+ item.Render(sb);
+ }
+
+ sb.Append(" ");
+ }
+ }
+}
diff --git a/src/EPPlus.Export.ImageRenderer/Svg/DefinitionUtils/UtillNodes/DynamicGridItem.cs b/src/EPPlus.Export.ImageRenderer/Svg/DefinitionUtils/UtillNodes/DynamicGridItem.cs
new file mode 100644
index 0000000000..bfd4837d0d
--- /dev/null
+++ b/src/EPPlus.Export.ImageRenderer/Svg/DefinitionUtils/UtillNodes/DynamicGridItem.cs
@@ -0,0 +1,43 @@
+using EPPlus.Export.ImageRenderer.RenderItems;
+using EPPlus.Export.ImageRenderer.RenderItems.Interfaces;
+using EPPlusImageRenderer;
+using EPPlusImageRenderer.RenderItems;
+using OfficeOpenXml.Drawing.Style.Fill;
+using System;
+using System.Collections.Generic;
+using System.Drawing;
+using System.Linq;
+using System.Text;
+
+namespace EPPlus.Export.ImageRenderer.Svg.DefinitionUtils.UtillNodes
+{
+ internal class DynamicGridItem : SymbolGroup
+ {
+ SvgRenderRectItem HorizontalLines = null;
+ SvgRenderRectItem VerticalLines = null;
+
+ public DynamicGridItem(DrawingBase renderer, string id, string maskId, string linesHorizontalId, string linesVerticalId) : base(renderer, id)
+ {
+ Mask = string.Format(_urlRef, maskId);
+
+ HorizontalLines = IntitalizeRect();
+ HorizontalLines.FillColor = string.Format(_urlRef, linesHorizontalId);
+
+ VerticalLines = IntitalizeRect();
+ VerticalLines.FillColor = string.Format(_urlRef, linesVerticalId);
+
+ _items.Add(HorizontalLines);
+ _items.Add(VerticalLines);
+ }
+
+ SvgRenderRectItem IntitalizeRect()
+ {
+ var rect = new SvgRenderRectItem(DrawingRenderer, Bounds);
+ rect.Width = 100;
+ rect.Height = 100;
+ rect.Suffix = "%";
+
+ return rect;
+ }
+ }
+}
diff --git a/src/EPPlus.Export.ImageRenderer/Svg/DefinitionUtils/UtillNodes/FadeOutMask.cs b/src/EPPlus.Export.ImageRenderer/Svg/DefinitionUtils/UtillNodes/FadeOutMask.cs
new file mode 100644
index 0000000000..6ede276a63
--- /dev/null
+++ b/src/EPPlus.Export.ImageRenderer/Svg/DefinitionUtils/UtillNodes/FadeOutMask.cs
@@ -0,0 +1,26 @@
+using EPPlusImageRenderer;
+using EPPlusImageRenderer.RenderItems;
+using OfficeOpenXml.FormulaParsing.Excel.Functions.MathFunctions;
+using System;
+using System.Collections.Generic;
+using System.Drawing;
+using System.Linq;
+using System.Text;
+
+namespace EPPlus.Export.ImageRenderer.Svg.DefinitionUtils.UtillNodes
+{
+ internal class FadeOutMask : MaskGroup
+ {
+ public FadeOutMask(DrawingBase renderer, string id, string rectFillId) : base(renderer, id)
+ {
+ var rect = new SvgRenderRectItem(DrawingRenderer, Bounds);
+ rect.Width = 100;
+ rect.Height = 100;
+ rect.Suffix = "%";
+
+ rect.FillColor = rectFillId;
+
+ _items.Add(rect);
+ }
+ }
+}
diff --git a/src/EPPlus.Export.ImageRenderer/Svg/DrawingItemForTesting.cs b/src/EPPlus.Export.ImageRenderer/Svg/DrawingItemForTesting.cs
new file mode 100644
index 0000000000..4035e650b2
--- /dev/null
+++ b/src/EPPlus.Export.ImageRenderer/Svg/DrawingItemForTesting.cs
@@ -0,0 +1,58 @@
+using EPPlus.Fonts.OpenType.Utils;
+using EPPlus.Graphics;
+using EPPlusImageRenderer;
+using EPPlusImageRenderer.RenderItems;
+using EPPlusImageRenderer.Svg;
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+
+
+namespace EPPlus.Export.ImageRenderer.Svg
+{
+ internal class DrawingItemForTesting : DrawingBase
+ {
+ ///
+ /// List of items to be added to this basic container
+ ///
+ internal List ExternalRenderItems = new List();
+ internal List ExternalRenderItemsNoBounds = new List();
+
+ internal DrawingItemForTesting(BoundingBox bounds) : base()
+ {
+ Bounds = bounds;
+ SvgRenderRectItem bg = new SvgRenderRectItem(this, Bounds);
+
+ bg.Width = Bounds.Width;
+ bg.Height = Bounds.Height;
+ bg.FillColor = "blue";
+ bg.FillOpacity = 0.2d;
+
+ RenderItems.Add(bg);
+ }
+
+ public void Render(StringBuilder sb)
+ {
+ RenderItems.Add(new SvgGroupItem(this));
+ foreach(var item in ExternalRenderItemsNoBounds)
+ {
+ item.AppendRenderItems(RenderItems);
+ }
+ foreach(var item in ExternalRenderItems)
+ {
+ item.AppendRenderItems(RenderItems);
+ }
+ RenderItems.Add(new SvgEndGroupItem(this, Bounds));
+
+ sb.Append($"");
+
+ foreach (var item in RenderItems)
+ {
+ item.Render(sb);
+ }
+
+ sb.Append(" ");
+ }
+ }
+}
diff --git a/src/EPPlus.Export.ImageRenderer/Svg/SvgLegendSerie.cs b/src/EPPlus.Export.ImageRenderer/Svg/SvgLegendSerie.cs
index eb6d199b8e..ef933486e2 100644
--- a/src/EPPlus.Export.ImageRenderer/Svg/SvgLegendSerie.cs
+++ b/src/EPPlus.Export.ImageRenderer/Svg/SvgLegendSerie.cs
@@ -15,11 +15,8 @@ Date Author Change
namespace EPPlusImageRenderer.Svg
{
- internal class SvgLegendSerie
+ internal class SvgLegendSerie : SvgLegendSeriesIcon
{
- internal RenderItem SeriesIcon { get; set; }
- internal RenderItem MarkerIcon { get; set; }
- internal RenderItem MarkerBackground { get; set; }
- internal TextBodyItem Textbox { get; set;}
+ internal TextBodyItem Textbox { get; set; }
}
}
\ No newline at end of file
diff --git a/src/EPPlus.Export.ImageRenderer/Svg/SvgLegendSeriesIcon.cs b/src/EPPlus.Export.ImageRenderer/Svg/SvgLegendSeriesIcon.cs
new file mode 100644
index 0000000000..c361eb47f5
--- /dev/null
+++ b/src/EPPlus.Export.ImageRenderer/Svg/SvgLegendSeriesIcon.cs
@@ -0,0 +1,15 @@
+using EPPlusImageRenderer.RenderItems;
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+
+namespace EPPlusImageRenderer.Svg
+{
+ internal class SvgLegendSeriesIcon
+ {
+ internal RenderItem SeriesIcon { get; set; }
+ internal RenderItem MarkerIcon { get; set; }
+ internal RenderItem MarkerBackground { get; set; }
+ }
+}
diff --git a/src/EPPlus.Export.ImageRenderer/Svg/SvgShape.cs b/src/EPPlus.Export.ImageRenderer/Svg/SvgShape.cs
index f58b603da5..3eaec658ca 100644
--- a/src/EPPlus.Export.ImageRenderer/Svg/SvgShape.cs
+++ b/src/EPPlus.Export.ImageRenderer/Svg/SvgShape.cs
@@ -15,6 +15,7 @@ Date Author Change
using EPPlus.Export.ImageRenderer.Utils;
using EPPlus.Fonts.OpenType;
using EPPlus.Fonts.OpenType.Utils;
+using EPPlus.Fonts.OpenType.Utils;
using EPPlus.Graphics;
using EPPlusImageRenderer.RenderItems;
using EPPlusImageRenderer.ShapeDefinitions;
@@ -24,6 +25,8 @@ Date Author Change
using OfficeOpenXml.Drawing.Theme;
using OfficeOpenXml.FormulaParsing.Excel.Functions.Logical;
using OfficeOpenXml.FormulaParsing.Excel.Functions.Text;
+using OfficeOpenXml.FormulaParsing.Excel.Functions.Logical;
+using OfficeOpenXml.FormulaParsing.Excel.Functions.Text;
using OfficeOpenXml.Style;
using System;
using System.Collections.Generic;
@@ -41,10 +44,13 @@ internal class SvgShape : DrawingShape
/// Calculated shape textbox
///
SvgRenderRectItem InsetTextBox;
+
+ SvgRenderRectItem MarginTextBox;
+
///
/// Textbox from memory
///
- public SvgTextBodyItem TextBox { get; internal set; }
+ public SvgTextBodyItem TextBodySvg { get; internal set; }
public SvgShape(ExcelShape shape) : base(shape)
{
@@ -91,17 +97,23 @@ public SvgShape(ExcelShape shape) : base(shape)
InsetTextBox = new SvgRenderRectItem(this, Bounds);
InsetTextBox.Bounds.Left = (float)shapeDef.TextBoxRect.LeftValue.PixelToPoint();
InsetTextBox.Bounds.Top = (float)shapeDef.TextBoxRect.TopValue.PixelToPoint();
+ InsetTextBox.Bounds.Left = (float)shapeDef.TextBoxRect.LeftValue.PixelToPoint();
+ InsetTextBox.Bounds.Top = (float)shapeDef.TextBoxRect.TopValue.PixelToPoint();
InsetTextBox.FillOpacity = 0.3d;
if (shape.TextBody.TextAutofit != eTextAutofit.ShapeAutofit)
{
InsetTextBox.Width = ((double)((float)shapeDef.TextBoxRect.RightValue - (float)shapeDef.TextBoxRect.LeftValue)).PixelToPoint();
InsetTextBox.Height = ((double)((float)shapeDef.TextBoxRect.BottomValue - (float)shapeDef.TextBoxRect.TopValue)).PixelToPoint();
+ InsetTextBox.Width = ((double)((float)shapeDef.TextBoxRect.RightValue - (float)shapeDef.TextBoxRect.LeftValue)).PixelToPoint();
+ InsetTextBox.Height = ((double)((float)shapeDef.TextBoxRect.BottomValue - (float)shapeDef.TextBoxRect.TopValue)).PixelToPoint();
}
else
{
InsetTextBox.Width = (float)shapeDef.TextBoxRect.RightValue.PixelToPoint();
InsetTextBox.Height = (float)shapeDef.TextBoxRect.BottomValue.PixelToPoint();
+ InsetTextBox.Width = (float)shapeDef.TextBoxRect.RightValue.PixelToPoint();
+ InsetTextBox.Height = (float)shapeDef.TextBoxRect.BottomValue.PixelToPoint();
}
}
else
@@ -110,8 +122,8 @@ public SvgShape(ExcelShape shape) : base(shape)
}
InsetTextBox.FillOpacity = 0.3d;
- TextBox = CreateTextBodyItem(_shape.TextBody);
- TextBox.AppendRenderItems(RenderItems);
+
+ TextBodySvg = CreateTextBodyItem(_shape.TextBody);
}
}
}
@@ -216,14 +228,15 @@ public string ViewBox
public void Render(StringBuilder sb)
{
sb.Append($"");
+ sb.Append($"");
//Write defs used for gradient colors
var writer = new SvgDrawingWriter(this);
writer.WriteSvgDefs(sb, RenderItems);
+
//SvgGroupItem gItemTest = null;
- //RenderDebugTextBox(sb);
- foreach(var item in RenderItems)
+ foreach (var item in RenderItems)
{
item.Render(sb);
//if(item.Type == RenderItemType.Group && gItemTest == null)
@@ -236,6 +249,7 @@ public void Render(StringBuilder sb)
//}
}
//if (!string.IsNullOrEmpty(_shape.Text))t
+ //if (!string.IsNullOrEmpty(_shape.Text))t
//{
// //RenderText(sb);
//}
@@ -244,6 +258,8 @@ public void Render(StringBuilder sb)
//{
// gItemTest.RenderEndGroup(sb);
//}
+
+ RenderDebugTextBox(sb);
sb.AppendLine(" ");
}
@@ -258,20 +274,31 @@ SvgTextBodyItem CreateTextBodyItem(ExcelTextBody bodyOrig)
InsetTextBox.Width = width.PixelToPoint();
InsetTextBox.Height = height.PixelToPoint();
//InsetTextBox.Bounds.Parent = TextBox.Parent; //TODO:Check that textBody is correct.
+ InsetTextBox.Bounds.Left = x.PixelToPoint();
+ InsetTextBox.Bounds.Top = y.PixelToPoint();
+ InsetTextBox.Width = width.PixelToPoint();
+ InsetTextBox.Height = height.PixelToPoint();
+ //InsetTextBox.Bounds.Parent = TextBox.Parent; //TODO:Check that textBody is correct.
}
double l, r, t, b;
bodyOrig.GetInsetsOrDefaults(out l, out t, out r, out b);
- BoundingBox MarginsBB = new BoundingBox(InsetTextBox.Width - r, InsetTextBox.Height - b);
- MarginsBB.Parent = InsetTextBox.Bounds;
- MarginsBB.Left = l;
- MarginsBB.Top = t;
+ MarginTextBox = new SvgRenderRectItem(this, this.Bounds);
- var txtBodyItem = new SvgTextBodyItem(this, MarginsBB, 0, 0, MarginsBB.Width, MarginsBB.Height);
+ MarginTextBox.Top = t + InsetTextBox.Top;
+ MarginTextBox.Left = l + InsetTextBox.Left;
+ MarginTextBox.Width = InsetTextBox.Width - r - l;
+ MarginTextBox.Height = InsetTextBox.Height - b - t;
+ RenderItems.Add(new SvgGroupItem(this, MarginTextBox.Bounds));
+
+ var txtBodyItem = new SvgTextBodyItem(this, MarginTextBox.Bounds, 0, 0, MarginTextBox.Width, MarginTextBox.Height);
txtBodyItem.ImportTextBody(bodyOrig);
+ txtBodyItem.AppendRenderItems(RenderItems);
+
+ RenderItems.Add(new SvgEndGroupItem(this, Bounds));
//txtBodyItem.Width = InsetTextBox.Width;
return txtBodyItem;
@@ -285,9 +312,13 @@ SvgTextBodyItem CreateTextBodyItem(ExcelTextBody bodyOrig)
private void RenderDebugTextBox(StringBuilder sb)
{
+ InsetTextBox.FillOpacity = 0.3d;
InsetTextBox.FillColor = "green";
InsetTextBox.Render(sb);
+ MarginTextBox.FillColor = "red";
+ MarginTextBox.FillOpacity = 0.3;
+ MarginTextBox.Render(sb);
//InsetTextBox.GetBounds(out double l, out double t, out double r, out double b);
//var area = textBody.Bounds;
diff --git a/src/EPPlus.Export.ImageRenderer/Utils/SvgDrawingWriter.cs b/src/EPPlus.Export.ImageRenderer/Utils/SvgDrawingWriter.cs
index fb2ec97dbb..6780d8bbe5 100644
--- a/src/EPPlus.Export.ImageRenderer/Utils/SvgDrawingWriter.cs
+++ b/src/EPPlus.Export.ImageRenderer/Utils/SvgDrawingWriter.cs
@@ -10,6 +10,8 @@ Date Author Change
*************************************************************************************************
27/11/2025 EPPlus Software AB EPPlus 9
*************************************************************************************************/
+using EPPlus.Export.ImageRenderer.RenderItems;
+using EPPlus.Fonts.OpenType.Utils;
using EPPlusImageRenderer.Constants;
using EPPlusImageRenderer.RenderItems;
using OfficeOpenXml.Drawing;
@@ -44,17 +46,18 @@ internal void WriteSvgDefs(StringBuilder sb, List renderItems)
var defSb = new StringBuilder();
var hs = new HashSet();
var ix = 1;
+
foreach (RenderItem item in renderItems)
{
string filter = "";
if (item.GradientFill != null)
{
- string name = WriteGradient("Gradient", defSb, hs, item.GradientFill, item.FillColorSource, true);
+ string name = WriteGradient($"Gradient{ix}", defSb, hs, item.GradientFill, item.FillColorSource, true);
item.FillColor = $"Url(#{name})";
}
else if (item.PatternFill != null)
{
- string name = WritePattern($"Pattern{item.PatternFill.PatternType}", defSb, hs, item.PatternFill, item.FillColorSource);
+ string name = WritePattern($"Pattern{item.PatternFill.PatternType}{ix}", defSb, hs, item.PatternFill, item.FillColorSource);
item.FillColor = $"Url(#{name})";
}
else if (item.BlipFill != null)
@@ -69,7 +72,7 @@ internal void WriteSvgDefs(StringBuilder sb, List renderItems)
}
if (item.BorderGradientFill != null)
{
- string name = WriteGradient("StrokeGradient", defSb, hs, item.BorderGradientFill, item.BorderColorSource, true);
+ string name = WriteGradient($"StrokeGradient{ix}", defSb, hs, item.BorderGradientFill, item.BorderColorSource, true);
item.BorderColor = $"Url(#{name})";
}
if(item.GlowColor!=null)
@@ -86,10 +89,32 @@ internal void WriteSvgDefs(StringBuilder sb, List renderItems)
$"" +
$" ";
}
+ if(item.OuterShadowEffect != null)
+ {
+ if (string.IsNullOrEmpty(item.FilterName))
+ {
+ var filterName = GetFilterName(ix);
+ item.FilterName = $"Url(#{filterName})";
+ filter = $"";
+ }
+ item.GetOuterShadowColor(out string shadowColor, out double opacity);
+ var dx = Math.Round(item.OuterShadowEffect.Distance * Math.Cos(MathHelper.Radians(item.OuterShadowEffect.Direction ?? 0D)), 2);
+ var dy = Math.Round(item.OuterShadowEffect.Distance * Math.Sin(MathHelper.Radians(item.OuterShadowEffect.Direction ?? 0D)), 2);
+ var blurRadius = item.OuterShadowEffect.BlurRadius??0D / 2;
+ filter += $" ";
+ }
if(string.IsNullOrEmpty(filter)==false)
{
defSb.Append(filter+" ");
}
+
+ if(item is SvgRenderItem svgItem)
+ {
+ if(string.IsNullOrEmpty(svgItem.DefId) == false)
+ {
+ svgItem.Render(defSb);
+ }
+ }
ix++;
}
if (defSb.Length > 0)
@@ -98,6 +123,9 @@ internal void WriteSvgDefs(StringBuilder sb, List renderItems)
sb.Append(defSb);
sb.Append("");
}
+
+ //Remove all items that have already been rendered
+ renderItems.RemoveAll(x => x is SvgRenderItem svgX && string.IsNullOrEmpty(svgX.DefId) == false);
}
private static string GetFilterName(int ix)
@@ -416,7 +444,12 @@ private string GetScaling(DrawGradientFill gradientFill)
private void SetStopColors(StringBuilder defSb, DrawGradientFill gradientFill, PathFillMode fillMode)
{
int ix = 0;
- foreach (var c in gradientFill.Colors)
+
+ //Svg requires starting at 0 and moving towards 100% Excel sometimes starts at 100
+ //Sort to get around that
+ var sortedGradientColors = gradientFill.Colors.OrderBy(x => x.Position);
+
+ foreach (var c in sortedGradientColors)
{
var color = ColorUtils.GetAdjustedColor(fillMode, c.Color);
// TODO: check if ix should be increased...?
@@ -454,17 +487,17 @@ private string GetXy(double? angle)
x2 = 1D - Math.Sin(MathHelper.Radians(angle.Value - 180));
}
- return $" x1=\"{x1.ToString("0.00", CultureInfo.InvariantCulture)}\" x2=\"{x2.ToString("0.00", CultureInfo.InvariantCulture)}\" y1=\"{y1.ToString("0.00", CultureInfo.InvariantCulture)}\" y2=\"{y2.ToString("0.00", CultureInfo.InvariantCulture)}\"";
+ return $" x1=\"{(x1).ToString("0.00%", CultureInfo.InvariantCulture)}\" x2=\"{(x2).ToString("0.00%", CultureInfo.InvariantCulture)}\" y1=\"{y1.ToString("0.00%", CultureInfo.InvariantCulture)}\" y2=\"{y2.ToString("0.00%", CultureInfo.InvariantCulture)}\"";
}
return "";
}
private string GetOpacity(ExcelDrawingColorManager c)
{
- var opacetyTransform = c.Transforms?.FirstOrDefault(x => x.Type == OfficeOpenXml.Drawing.Style.Coloring.eColorTransformType.Alpha);
- if (opacetyTransform == null) return "";
+ var opacityTransform = c.Transforms?.FirstOrDefault(x => x.Type == OfficeOpenXml.Drawing.Style.Coloring.eColorTransformType.Alpha);
+ if (opacityTransform == null) return "";
- return $"stop-opacity=\"{opacetyTransform.Value.ToString("0")}%\"";
+ return $"stop-opacity=\"{opacityTransform.Value.ToString("0")}%\"";
}
private string SetStretchTileProps(ExcelDrawingBlipFill blipFill)
diff --git a/src/EPPlus.Fonts.OpenType.Tests/Integration/MeasurerComparisonTests.cs b/src/EPPlus.Fonts.OpenType.Tests/Integration/MeasurerComparisonTests.cs
index 714bbc6122..5b078c3eac 100644
--- a/src/EPPlus.Fonts.OpenType.Tests/Integration/MeasurerComparisonTests.cs
+++ b/src/EPPlus.Fonts.OpenType.Tests/Integration/MeasurerComparisonTests.cs
@@ -1,4 +1,4 @@
-//using EPPlus.Fonts.OpenType.Integration;
+//using EPPlus.Fonts.OpenType.Integration;
//using EPPlus.Fonts.OpenType.TextShaping;
//using EPPlus.Fonts.OpenType.TrueTypeMeasurer;
//using EPPlus.Fonts.OpenType.Utils;
diff --git a/src/EPPlus.Fonts.OpenType.Tests/Integration/TextLayoutEngineTests.cs b/src/EPPlus.Fonts.OpenType.Tests/Integration/TextLayoutEngineTests.cs
index 1256d95b2d..64110bc526 100644
--- a/src/EPPlus.Fonts.OpenType.Tests/Integration/TextLayoutEngineTests.cs
+++ b/src/EPPlus.Fonts.OpenType.Tests/Integration/TextLayoutEngineTests.cs
@@ -411,10 +411,189 @@ public void WrapRichTextDifficultCase()
Assert.AreEqual("16SvgSize 24", wrappedLines[4]);
}
+ [TestMethod]
+ public void MeasureBigGoudy()
+ {
+ List lstOfRichText = new() { "TextBox2ra underlineLa Strike", "Goudysize16SvgSize24" };
+
+ var regFont = new MeasurementFont()
+ {
+ FontFamily = "Aptos Narrow",
+ Size = 11,
+ Style = MeasurementFontStyles.Regular
+ };
+
+
+ var goudyFont = new MeasurementFont()
+ {
+ FontFamily = "Goudy Stout",
+ Size = 16,
+ Style = MeasurementFontStyles.Regular
+ };
+
+
+ List fonts = new() { regFont, goudyFont };
+
+ var startFont = TextData.GetFontData(regFont.FontFamily, GetFontSubType(regFont.Style));
+
+ var shaper = new TextShaper(startFont);
+ var layout = new TextLayoutEngine(shaper);
+
+ var maxSizeInPoints = 225d;
+
+ var fragments = new List();
+
+ for (int i = 0; i < lstOfRichText.Count(); i++)
+ {
+ var currentFrag = new TextFragment() { Text = lstOfRichText[i], Font = fonts[i] };
+ fragments.Add(currentFrag);
+ }
+
+ //var test = "TextBox2ra underlineLa StrikeGoudysize16SvgSize24";
+ ////var secondTest = "TextBox2ra underlineLa StrikeGoudy".ToArray();
+
+ var wrappedLines = layout.WrapRichTextLines(fragments, maxSizeInPoints);
+
+
+ var txtWidthsingle = shaper.MeasureTextInPixels("E", 16, 96, ShapingOptions.Full);
+ var txtWidth = shaper.MeasureTextInPixels("EEEEEEEEEE", 16, 96, ShapingOptions.Full);
+ var txtWidthAlt = shaper.MeasureTextInPixels("EEEEEEEEEE", 16, 96, ShapingOptions.Fast);
+
+ var txtWidthLowSize = shaper.MeasureTextInPixels("E", 11, 96, ShapingOptions.Fast);
+ var txtWidthHighSize= shaper.MeasureTextInPixels("E", 72, 96, ShapingOptions.Fast);
+ var txtWidthHighSize10 = shaper.MeasureTextInPixels("EEEEEEEEEE", 72, 96, ShapingOptions.Fast);
+
+ var pts16 = shaper.MeasureTextInPoints("E", 16);
+ var pts = shaper.MeasureTextInPoints("E", 72);
+ var pts2 = shaper.MeasureTextInPoints("E", 96);
+
+ var txtWidthMaxSizeSingle = shaper.MeasureTextInPixels("E", 96, 72, ShapingOptions.Full);
+ var txtWidthMaxSizeSingleFast = shaper.MeasureTextInPixels("E", 96, 72, ShapingOptions.Fast);
+
+ var txtWidthMaxSizeSingle96 = shaper.MeasureTextInPixels("E", 96, 96, ShapingOptions.Full);
+ var txtWidthMaxSizeSingleFast96 = shaper.MeasureTextInPixels("E", 96, 96, ShapingOptions.Fast);
+
+ //Assert.AreEqual(16, txtWidthLowSize);
+ //Assert.AreEqual(97, txtWidthHighSize);
+
+ //Assert.AreEqual(97, txtWidthHighSize);
+ //Assert.AreEqual(23, txtWidthsingle);
+ //Assert.AreEqual(249,txtWidth);
+
+ //var wrappedLines = layout.WrapRichTextLines(fragments, maxSizeInPoints);
+
+ ////E in goudy stout 16 should be equal to 22 px or 16.5 points
+ //Assert.AreEqual(16.5d, wrappedLines[0].LineFragments[0].Width);
+ //Assert.AreEqual(16.5d, wrappedLines[0].LineFragments[1].Width);
+ }
+
+ [TestMethod]
+ public void EnsureRichTextLineWrappingSameAsNonRichWhenNoWrap()
+ {
+ List comparatorLst = new() { "Strike", "Goudy size"};
+
+ var font = OpenTypeFonts.LoadFont("Aptos Narrow", FontSubFamily.Regular, FontFolders);
+ var shaper = new TextShaper(font);
+ var points1 = shaper.MeasureTextInPoints(comparatorLst[0], 11);
+
+ var font2 = OpenTypeFonts.LoadFont("Goudy Stout", FontSubFamily.Regular, FontFolders);
+ var otherShaper = new TextShaper(font2);
+
+ var points2 = otherShaper.MeasureTextInPoints(comparatorLst[1], 16);
+
+ var pointsTotal = points1 + points2;
+ Assert.AreEqual(202.8916f, pointsTotal);
+
+ var comparatorFragments = new List();
+
+ var font11= new MeasurementFont()
+ {
+ FontFamily = "Aptos Narrow",
+ Size = 11,
+ Style = MeasurementFontStyles.Strikeout
+ };
+
+ var font22 = new MeasurementFont()
+ {
+ FontFamily = "Goudy Stout",
+ Size = 16,
+ Style = MeasurementFontStyles.Regular
+ };
+
+ var frag1 = new TextFragment() { Font = font11, Text = comparatorLst[0] };
+ var frag2 = new TextFragment() { Font = font22, Text = comparatorLst[1] };
+ comparatorFragments.Add(frag1);
+ comparatorFragments.Add(frag2);
+
+ var startFont = OpenTypeFonts.LoadFont(font11.FontFamily, GetFontSubType(font11.Style), FontFolders);
+ var goudyFont = OpenTypeFonts.LoadFont(font22.FontFamily, GetFontSubType(font22.Style), FontFolders);
+
+ ITextShaper shaper2 = new TextShaper(font);
+ using var layoutEngine = new TextLayoutEngine(shaper);
+ var wrappedLines = layoutEngine.WrapRichTextLines(comparatorFragments, 225d);
+
+ Assert.AreEqual(pointsTotal, wrappedLines[0].Width);
+ Assert.AreEqual(points1, wrappedLines[0].LineFragments[0].Width);
+ Assert.AreEqual(points2, wrappedLines[0].LineFragments[1].Width);
+ }
+
+ [TestMethod]
+ public void EnsureRichTextLineWrappingSameAsNonRichWhenNoWrapAndSpaceTrail()
+ {
+ List comparatorLst = new() { "Strike", "Goudy size " };
+
+ var font = OpenTypeFonts.LoadFont("Aptos Narrow", FontSubFamily.Regular, FontFolders);
+ var shaper = new TextShaper(font);
+ var points1 = shaper.MeasureTextInPoints(comparatorLst[0], 11);
+
+ var font2 = OpenTypeFonts.LoadFont("Goudy Stout", FontSubFamily.Regular, FontFolders);
+ var otherShaper = new TextShaper(font2);
+
+ var points2 = otherShaper.MeasureTextInPoints(comparatorLst[1], 16);
+
+ var pointsTotal = points1 + points2;
+ Assert.AreEqual(210.8916f, pointsTotal);
+
+ var comparatorFragments = new List();
+
+ var font11 = new MeasurementFont()
+ {
+ FontFamily = "Aptos Narrow",
+ Size = 11,
+ Style = MeasurementFontStyles.Strikeout
+ };
+
+ var font22 = new MeasurementFont()
+ {
+ FontFamily = "Goudy Stout",
+ Size = 16,
+ Style = MeasurementFontStyles.Regular
+ };
+
+ var frag1 = new TextFragment() { Font = font11, Text = comparatorLst[0] };
+ var frag2 = new TextFragment() { Font = font22, Text = comparatorLst[1] };
+ comparatorFragments.Add(frag1);
+ comparatorFragments.Add(frag2);
+
+ var startFont = OpenTypeFonts.LoadFont(font11.FontFamily, GetFontSubType(font11.Style), FontFolders);
+ var goudyFont = OpenTypeFonts.LoadFont(font22.FontFamily, GetFontSubType(font22.Style), FontFolders);
+
+ ITextShaper shaper2 = new TextShaper(font);
+ using var layoutEngine = new TextLayoutEngine(shaper);
+ var wrappedLines = layoutEngine.WrapRichTextLines(comparatorFragments, 225d);
+
+ Assert.AreEqual(pointsTotal, wrappedLines[0].Width);
+ Assert.AreEqual(points1, wrappedLines[0].LineFragments[0].Width);
+ Assert.AreEqual(points2, wrappedLines[0].LineFragments[1].Width);
+ var noSpaceWidth = wrappedLines[0].GetWidthWithoutTrailingSpaces();
+ Assert.AreEqual(202.8916f, noSpaceWidth);
+ }
+
[TestMethod]
public void EnsureLineFragmentsAreMeasuredCorrectlyWhenWrapping()
{
List lstOfRichText = new() { "TextBox2", "ra underline", "La Strike", "Goudy size 16"};
+ List comparatorLst = new() { "Strike", "Goudy size"};
var font2 = new MeasurementFont()
{
FontFamily = "Aptos Narrow",
@@ -461,8 +640,8 @@ public void EnsureLineFragmentsAreMeasuredCorrectlyWhenWrapping()
var wrappedLines = layout.WrapRichTextLines(fragments, maxSizePoints);
-
Assert.AreEqual(12.55224609375d, wrappedLines[0].LineFragments[2].Width);
+ Assert.AreEqual(202.8916f, wrappedLines[1].GetWidthWithoutTrailingSpaces());
List smallestTextFragments = new List();
@@ -495,7 +674,7 @@ public void WrapRichTextDifficultCaseCompare()
FontFamily = "Aptos Narrow",
Size = 11,
Style = MeasurementFontStyles.Regular
- }; ;
+ };
var font2 = new MeasurementFont()
{
@@ -781,5 +960,55 @@ public void WrapText_Continous_Long_Word()
Assert.AreEqual("pellentesqu", wrappedLines[0]);
Assert.AreEqual("er", wrappedLines[1]);
}
+
+ [TestMethod]
+ public void WrapRichText_MeasureCorrectly()
+ {
+ // Arrange
+ var font = OpenTypeFonts.LoadFont("Aptos Narrow", FontSubFamily.Regular, FontFolders);
+ var shaper = new TextShaper(font);
+ var layout = new TextLayoutEngine(shaper);
+
+ var fragments = new List
+ {
+ new TextFragment
+ {
+ Text = "value, 1",
+ Font = new MeasurementFont { FontFamily = "Aptos Narrow", Size = 9 }
+ },
+ };
+
+ // Act
+ var lines = layout.WrapRichTextLines(fragments, 1000);
+
+ // Assert
+ Assert.AreEqual(1, lines.Count);
+ Assert.AreEqual(27.5712890625, lines[0].Width);
+ Assert.AreEqual("value, 1", lines[0].Text);
+ }
+
+ [TestMethod]
+ public void VerifyWrappingSingleChar()
+ {
+ List lstOfRichText = new() { "SE/DKK" };
+
+ var font1 = new MeasurementFont()
+ {
+ FontFamily = "Aptos Narrow",
+ Size = 11,
+ Style = MeasurementFontStyles.Regular
+ };
+
+ var maxWidthPt = 31.8125234375d;
+ var gottenFont = OpenTypeFonts.LoadFont("Aptos Narrow", FontSubFamily.Regular, FontFolders);
+ var shaper = new TextShaper(gottenFont);
+ var layout = new TextLayoutEngine(shaper);
+
+ List fragments = new List() { new TextFragment() { Font = font1, Text = lstOfRichText[0] } };
+
+ var wrappedLines = layout.WrapRichTextLines(fragments, maxWidthPt);
+
+ Assert.AreEqual(0, wrappedLines[1].LineFragments[0].StartIdx);
+ }
}
}
\ No newline at end of file
diff --git a/src/EPPlus.Fonts.OpenType/Integration/LineFragment.cs b/src/EPPlus.Fonts.OpenType/Integration/LineFragment.cs
index 3dbeeb5aec..26dbc439b0 100644
--- a/src/EPPlus.Fonts.OpenType/Integration/LineFragment.cs
+++ b/src/EPPlus.Fonts.OpenType/Integration/LineFragment.cs
@@ -14,6 +14,8 @@ public class LineFragment
public double Width { get; set; }
public int RtFragIdx { get; set; }
+ public double SpaceWidth { get; internal set; }
+
//public double AscentInPoints { get; private set; }
//public double DescentInPoints { get; private set; }
diff --git a/src/EPPlus.Fonts.OpenType/Integration/TextLayoutEngine.RichText.cs b/src/EPPlus.Fonts.OpenType/Integration/TextLayoutEngine.RichText.cs
index eddeff4448..dc9494e798 100644
--- a/src/EPPlus.Fonts.OpenType/Integration/TextLayoutEngine.RichText.cs
+++ b/src/EPPlus.Fonts.OpenType/Integration/TextLayoutEngine.RichText.cs
@@ -14,11 +14,13 @@ Date Author Change
02/23/2026 EPPlus Software AB Performance fix: Shape() → ShapeLight() in ProcessFragment
*************************************************************************************************/
using EPPlus.Fonts.OpenType.TrueTypeMeasurer.DataHolders;
+using EPPlus.Fonts.OpenType.TrueTypeMeasurer.DataHolders;
using EPPlus.Fonts.OpenType.Utilities;
using OfficeOpenXml.Interfaces.Fonts;
using System;
using System.Collections.Generic;
using System.Linq;
+using System.Linq;
using System.Text;
namespace EPPlus.Fonts.OpenType.Integration
@@ -55,7 +57,7 @@ public List WrapRichText(
ProcessFragment(fragment, maxWidthPoints, lineBuilder, state);
}
- FinalizeCurrentLine(lineBuilder, state.CurrentLineWidth, state.WordStart);
+ FinalizeCurrentLine(lineBuilder, state.CurrentLineWidth, state.WordStart, state.CurrentTextLine);
state.EndCurrentTextLine();
@@ -90,7 +92,7 @@ public List WrapRichTextLines(
ProcessFragment(fragment, maxWidthPoints, lineBuilder, state);
}
- FinalizeCurrentLine(lineBuilder, state.CurrentLineWidth, state.WordStart);
+ FinalizeCurrentLine(lineBuilder, state.CurrentLineWidth, state.WordStart, state.CurrentTextLine);
state.CurrentTextLine.Width = state.CurrentLineWidth;
state.CurrentTextLine.Text = lineBuilder.ToString();
state.EndCurrentTextLine();
@@ -142,7 +144,10 @@ private void ProcessFragment(
fragment.AscentPoints = shaper.GetAscentInPoints(fragment.Font.Size);
fragment.DescentPoints = shaper.GetDescentInPoints(fragment.Font.Size);
+ var spaceWidth = shaper.Shape(" ", options).GetWidthInPoints(fragment.Font.Size);
+
state.LineFrag = new LineFragment(state.CurrentFragmentIdx, lineBuilder.Length);
+ state.LineFrag.SpaceWidth = spaceWidth;
state.LineFrag.StartIdx = lineBuilder.Length;
state.LineFrag.RtFragIdx = state.CurrentFragmentIdx;
@@ -155,6 +160,7 @@ private void ProcessFragment(
{
HandleLineBreak(lineBuilder, state);
SkipLineBreakChars(fragment.Text, ref i);
+
state.CurrentLineWidth = 0;
state.CurrentWordWidth = 0;
state.WordStart = -1;
@@ -165,22 +171,23 @@ private void ProcessFragment(
state.CurrentLineWidth += charWidths[i];
state.CurrentWordWidth += charWidths[i];
state.LineFrag.Width += charWidths[i];
+
lineBuilder.Append(c);
if (c == ' ')
{
state.SetAndLogWordStartState(lineBuilder.Length - 1);
+ state.SetAndLogWordStartState(lineBuilder.Length - 1);
}
if (state.CurrentLineWidth > maxWidthPoints)
{
WrapCurrentLine(lineBuilder, state, maxWidthPoints, charWidths[i]);
}
-
i++;
}
- if (state.LineFrag.Width > 0)
+ if(state.LineFrag.Width > 0)
{
state.CurrentTextLine.LineFragments.Add(state.LineFrag);
}
@@ -240,15 +247,21 @@ private void SkipLineBreakChars(string text, ref int i)
}
i++;
}
-
private void WrapCurrentLine(StringBuilder lineBuilder, WrapStateRichText state, double maxWidthPoints, double advanceWidth)
{
int fragIdxAtBreak = state.CurrentFragmentIdx;
+ int adjustmentForLineBuilderLength = 0;
+
// Bounds check to prevent ArgumentOutOfRangeException
if (state.WordStart >= 0 && state.WordStart < lineBuilder.Length)
{
- string line = lineBuilder.ToString(0, state.WordStart).TrimEnd();
+ var lineStringWithTrail = lineBuilder.ToString(0, state.WordStart+1);//+1 was just added and should be here but everything else is sorta based on it being gone...
+ if (lineStringWithTrail[lineStringWithTrail.Length-1] == ' ')
+ {
+ state.CurrentTextLine.WasWrappedOnSpace = true;
+ }
+ string line = lineStringWithTrail.TrimEnd();
_lineListBuffer.Add(line);
lineBuilder.Remove(0, state.WordStart + 1);
@@ -282,27 +295,34 @@ private void WrapCurrentLine(StringBuilder lineBuilder, WrapStateRichText state,
if (lastChar != ' ')
{
lineBuilder.Append(lastChar);
+ //Since we appended we should remove it from line builder length when end current and initialize next happens
+ adjustmentForLineBuilderLength = 1;
//The char that made us move past maxWidth
//must be added to the new line
state.CurrentWordWidth = advanceWidth;
state.CurrentLineWidth = advanceWidth;
}
+ else
+ {
+ state.CurrentTextLine.WasWrappedOnSpace = true;
+ }
}
- state.EndCurrentTextLineAndIntializeNext(lineBuilder.Length);
+ state.EndCurrentTextLineAndIntializeNext(lineBuilder.Length - adjustmentForLineBuilderLength);
state.CurrentWordWidth = state.CurrentLineWidth;
state.WordStart = -1;
state.LineStart = -1;
}
- private void FinalizeCurrentLine(StringBuilder lineBuilder, double lineWidth, int lastSpaceIndex)
+ private void FinalizeCurrentLine(StringBuilder lineBuilder, double lineWidth, int lastSpaceIndex, TextLineSimple currentLine)
{
if (lineBuilder.Length > 0)
{
if (lineBuilder[lineBuilder.Length - 1] == ' ')
{
+ currentLine.WasWrappedOnSpace = true;
lineBuilder.Length--;
}
if (lineBuilder.Length > 0)
diff --git a/src/EPPlus.Fonts.OpenType/Integration/WrapStateRichText.cs b/src/EPPlus.Fonts.OpenType/Integration/WrapStateRichText.cs
index c80acfbb48..b4f605fa5f 100644
--- a/src/EPPlus.Fonts.OpenType/Integration/WrapStateRichText.cs
+++ b/src/EPPlus.Fonts.OpenType/Integration/WrapStateRichText.cs
@@ -31,7 +31,9 @@ internal void EndCurrentTextLineAndIntializeNext(int startIdxOfNewFragment)
if (_fragmentsForNextLine == null)
{
EndCurrentTextLine();
+ var spcWidthTemp = LineFrag.SpaceWidth;
LineFrag = new LineFragment(CurrentFragmentIdx, startIdxOfNewFragment);
+ LineFrag.SpaceWidth = spcWidthTemp;
}
else
{
diff --git a/src/EPPlus.Fonts.OpenType/TextShaping/TextShaper.cs b/src/EPPlus.Fonts.OpenType/TextShaping/TextShaper.cs
index f31f42da47..20ce300107 100644
--- a/src/EPPlus.Fonts.OpenType/TextShaping/TextShaper.cs
+++ b/src/EPPlus.Fonts.OpenType/TextShaping/TextShaper.cs
@@ -756,6 +756,8 @@ public float GetFontHeightInPoints(float fontSize)
///
/// Calculates the distance from the top of the font's bounding box to the baseline.
///
+ /// The font size, in points, for which to calculate the baseline position. Must be a positive value.
+ /// The distance, in points, from the top of the font's bounding box to the baseline for the given font size.
public float GetAscentInPoints(float fontSize)
{
var ascent = _primaryFont.Os2Table.UseTypoMetrics
diff --git a/src/EPPlus.Fonts.OpenType/TrueTypeMeasurer/DataHolders/TextLineSimple.cs b/src/EPPlus.Fonts.OpenType/TrueTypeMeasurer/DataHolders/TextLineSimple.cs
index 04d20b0579..f2c4982154 100644
--- a/src/EPPlus.Fonts.OpenType/TrueTypeMeasurer/DataHolders/TextLineSimple.cs
+++ b/src/EPPlus.Fonts.OpenType/TrueTypeMeasurer/DataHolders/TextLineSimple.cs
@@ -30,6 +30,40 @@ public class TextLineSimple
public double Width { get; internal set; }
+ public double lastFontSpaceWidth { get; internal set; }
+
+ internal bool WasWrappedOnSpace = false;
+
+ ///
+ /// In renderers like Excel the width of trailing spaces
+ /// MUST be resepected in some cases.
+ /// In others (e.g. Centering) the spaces width must be ignored
+ ///
+ public double GetWidthWithoutTrailingSpaces()
+ {
+ lastFontSpaceWidth = LineFragments.Last().SpaceWidth;
+
+ var trailingSpaceCount = 0;
+
+ for(int i = Text.Count()-1; i > 0; i--)
+ {
+ if(Text[i] != ' ')
+ {
+ break;
+ }
+
+ trailingSpaceCount++;
+ }
+
+ if(WasWrappedOnSpace)
+ {
+ trailingSpaceCount++;
+ }
+
+ var widthWithoutTrail = Width - lastFontSpaceWidth * (trailingSpaceCount);
+ return widthWithoutTrail;
+ }
+
public TextLineSimple()
{
}
diff --git a/src/EPPlus.Fonts.OpenType/TrueTypeMeasurer/FontMeasurerTrueType.cs b/src/EPPlus.Fonts.OpenType/TrueTypeMeasurer/FontMeasurerTrueType.cs
index 95ef510be8..e46d5b0f95 100644
--- a/src/EPPlus.Fonts.OpenType/TrueTypeMeasurer/FontMeasurerTrueType.cs
+++ b/src/EPPlus.Fonts.OpenType/TrueTypeMeasurer/FontMeasurerTrueType.cs
@@ -247,6 +247,28 @@ public List MeasureAndWrapTextLines(string text, MeasurementFont
return wrappedStrings;
}
+ public List MeasureAndWrapTextLines_New(string text, MeasurementFont font, double maxWidthPoints, double preExistingWidthPixels = 0)
+ {
+ List lines = null;
+ if (string.IsNullOrEmpty(text) == false)
+ {
+ var layout = TextData.GetTextLayoutEngine(font);
+ var txtFragment = new Integration.TextFragment()
+ {
+ Font = font,
+ Text = text
+ };
+
+ var list = new List() { txtFragment };
+ lines = layout.WrapRichTextLines(list, maxWidthPoints);
+ }
+
+ return lines;
+ //SetFont(font.Size, font.FontFamily);
+ //var wrappedStrings = TextData.MeasureAndWrapTextLines(text, FontSize, CurrentFont, maxWidthPoints, preExistingWidthPixels.PixelToPoint());
+ //return wrappedStrings;
+ }
+
public List MeasureAndWrapTextPoints(string text, MeasurementFont font, double MaxWidthInPoints)
{
SetFont(font.Size, font.FontFamily);
diff --git a/src/EPPlus.Fonts.OpenType/TrueTypeMeasurer/TextData.cs b/src/EPPlus.Fonts.OpenType/TrueTypeMeasurer/TextData.cs
index ba89d9eb2d..0f350315e5 100644
--- a/src/EPPlus.Fonts.OpenType/TrueTypeMeasurer/TextData.cs
+++ b/src/EPPlus.Fonts.OpenType/TrueTypeMeasurer/TextData.cs
@@ -23,6 +23,7 @@ Date Author Change
using System.Collections.Generic;
using System.Linq;
using System.Xml;
+using static System.Net.Mime.MediaTypeNames;
namespace EPPlus.Fonts.OpenType
diff --git a/src/EPPlus.Graphics/BoundingBoxContainer.cs b/src/EPPlus.Graphics/BoundingBoxContainer.cs
new file mode 100644
index 0000000000..8ab53281a1
--- /dev/null
+++ b/src/EPPlus.Graphics/BoundingBoxContainer.cs
@@ -0,0 +1,112 @@
+using OfficeOpenXml.Drawing;
+using OfficeOpenXml.FormulaParsing.Excel.Functions.Text;
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+
+namespace EPPlus.Graphics
+{
+ internal class BoundingBoxContainer : BoundingBox
+ {
+ internal double MarginLeft
+ {
+ get
+ {
+ return InnerBounds.Left;
+ }
+ set
+ {
+ InnerBounds.Left = value;
+ }
+ }
+
+ internal double MarginTop
+ {
+ get
+ {
+ return InnerBounds.Top;
+ }
+ set
+ {
+ InnerBounds.Top = value;
+ }
+ }
+
+ internal double MarginRight
+ {
+ get
+ {
+ return GetInnerRight();
+ }
+ set
+ {
+ InnerBounds.Width = Width - value;
+ }
+ }
+ internal double MarginBottom
+ {
+ get
+ {
+ return GetInnerBottom();
+ }
+ set
+ {
+ InnerBounds.Height = Height - value;
+ }
+ }
+
+ BoundingBox InnerBounds;
+
+ internal BoundingBoxContainer(BoundingBox innerBounds) : base()
+ {
+ InnerBounds = innerBounds;
+ }
+
+ //internal BoundingBoxContainer(double l, double t, double r, double b) : base(l, t, r, b)
+ //{
+ //}
+
+ internal double GetInnerLeft()
+ {
+ return Left + MarginLeft;
+ }
+
+ internal double GetInnerTop()
+ {
+ return Top + MarginTop;
+ }
+
+ internal double GetInnerBottom()
+ {
+ return Bottom - MarginBottom;
+ }
+
+ internal double GetInnerRight()
+ {
+ return Right - MarginRight;
+ }
+
+ //public BoundingBox GetInnerBounds()
+ //{
+ // var textArea = new BoundingBox();
+ // textArea.Left = GetInnerLeft();
+ // textArea.Top = GetInnerTop();
+
+ // textArea.Width = GetInnerWidth();
+ // textArea.Height = GetInnerHeight();
+
+ // return textArea;
+ //}
+
+ public double GetInnerWidth()
+ {
+ return GetInnerRight() - GetInnerLeft();
+ }
+
+ public double GetInnerHeight()
+ {
+ return GetInnerBottom() - GetInnerTop();
+ }
+ }
+}
diff --git a/src/EPPlus.Graphics/Point.cs b/src/EPPlus.Graphics/Point.cs
new file mode 100644
index 0000000000..b2380933f1
--- /dev/null
+++ b/src/EPPlus.Graphics/Point.cs
@@ -0,0 +1,40 @@
+using EPPlus.Graphics.Math;
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+
+namespace EPPlus.Graphics
+{
+ internal class Point : Transform
+ {
+ internal double Top
+ {
+ get { return LocalPosition.Y; }
+ set
+ {
+ LocalPosition = new Vector2(LocalPosition.X, value);
+ }
+ }
+
+ internal double Left
+ {
+ get { return LocalPosition.X; }
+ set
+ {
+ LocalPosition = new Vector2(value, LocalPosition.Y);
+ }
+ }
+
+ internal Point()
+ {
+
+ }
+
+ internal Point(double x, double y)
+ {
+ Left = x;
+ Top = y;
+ }
+ }
+}
diff --git a/src/EPPlus/BorderStyleBase.cs b/src/EPPlus/BorderStyleBase.cs
new file mode 100644
index 0000000000..9b0d577c6a
--- /dev/null
+++ b/src/EPPlus/BorderStyleBase.cs
@@ -0,0 +1,76 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+
+//namespace OfficeOpenXml
+//{
+// public enum BorderStyleBase
+// {
+// ///
+// /// No border style
+// ///
+// None,
+// ///
+// /// Hairline
+// ///
+// Hair,
+
+// ///
+// /// Dashed
+// ///
+// Dashed, //dash
+
+// ///
+// /// Dash Dot
+// ///
+// DashDot, //DashDot
+
+// ///
+// /// Dotted
+// ///
+// Dotted, //Dot
+
+// ///
+// /// Single line, medium thickness
+// ///
+// Medium, //solid
+
+
+
+
+// ///
+// /// Thin single line
+// ///
+// Thin,
+
+// ///
+// /// Dash Dot Dot
+// ///
+// DashDotDot,
+// ///
+// /// Dash Dot Dot, medium thickness
+// ///
+// MediumDashDotDot,
+// ///
+// /// Dashed, medium thickness
+// ///
+// MediumDashed,
+// ///
+// /// Dash Dot, medium thickness
+// ///
+// MediumDashDot,
+// ///
+// /// Single line, Thick
+// ///
+// Thick,
+// ///
+// /// Double line
+// ///
+// Double,
+// ///
+// /// Slanted Dash Dot
+// ///
+// SlantDashDot
+// }
+//}
diff --git a/src/EPPlus/CellPictures/CellPictureReferenceCache.cs b/src/EPPlus/CellPictures/CellPictureReferenceCache.cs
index 786477fa12..fb2fc19923 100644
--- a/src/EPPlus/CellPictures/CellPictureReferenceCache.cs
+++ b/src/EPPlus/CellPictures/CellPictureReferenceCache.cs
@@ -10,6 +10,7 @@ Date Author Change
*************************************************************************************************
11/11/2024 EPPlus Software AB Initial release EPPlus 8
*************************************************************************************************/
+using OfficeOpenXml.RichData;
using System;
using System.Collections.Generic;
@@ -25,6 +26,22 @@ private void OnLastReferenceRemoved(uint vmId)
LastReferenceRemoved?.Invoke(this, new LastReferenceRemovedEventArgs(vmId));
}
+ public static PictureCacheKey CreateKey(ExcelCellPicture picture)
+ {
+ if (picture.PictureType == ExcelCellPictureTypes.LocalImage)
+ {
+ return new LocalImageCacheKey(picture);
+ }
+ else if (picture.PictureType == ExcelCellPictureTypes.WebImage)
+ {
+ return new WebPictureCacheKey(picture);
+ }
+ else
+ {
+ throw new ArgumentException("Invalid pictureType: " + picture.PictureType.ToString());
+ }
+ }
+
public bool Contains(PictureCacheKey pictureCacheKey, out uint vmId)
{
vmId = uint.MaxValue;
@@ -34,6 +51,7 @@ public bool Contains(PictureCacheKey pictureCacheKey, out uint vmId)
{
vmId = _referenceCache[key].VmId;
}
+
return result;
}
@@ -43,6 +61,17 @@ public bool Contains(PictureCacheKey pictureCacheKey)
return _referenceCache.ContainsKey(key);
}
+ public int GetNumberOfReferences(PictureCacheKey pictureCacheKey)
+ {
+ int refNum = 0;
+ var key = pictureCacheKey.Key;
+ if (_referenceCache.ContainsKey(key))
+ {
+ refNum = _referenceCache[key].NumberOfReferences;
+ }
+ return refNum;
+ }
+
public void Add(PictureCacheKey pictureCacheKey, uint vmId)
{
var key = pictureCacheKey.Key;
diff --git a/src/EPPlus/CellPictures/CellPicturesManager.cs b/src/EPPlus/CellPictures/CellPicturesManager.cs
index e3c98ba9eb..9cc2a526d9 100644
--- a/src/EPPlus/CellPictures/CellPicturesManager.cs
+++ b/src/EPPlus/CellPictures/CellPicturesManager.cs
@@ -11,6 +11,7 @@ Date Author Change
11/11/2024 EPPlus Software AB Initial release EPPlus 8
*************************************************************************************************/
using OfficeOpenXml.Drawing;
+using OfficeOpenXml.FormulaParsing.Excel.Functions.DateAndTime;
using OfficeOpenXml.FormulaParsing.Excel.Functions.RefAndLookup;
using OfficeOpenXml.RichData;
using OfficeOpenXml.RichData.IndexRelations;
@@ -160,19 +161,51 @@ public void SetCellPicture(int row, int col, byte[] imageBytes, string altText,
var imageUri = UriHelper.ResolvePartUri(rdUri, imageInfo.Uri);
var cacheKey = new LocalImageCacheKey(imageUri, calcOrigin, altText);
+
+
if (_referenceCache.Contains(cacheKey, out uint cachedVmId))
{
- if(existingPic != null && existingPic.ImageUri.OriginalString != imageUri.OriginalString)
+ if(existingPic != null)
{
var cKey = GetCacheKey(existingPic);
- _referenceCache.RemoveReference(cKey, out int numberOfRefsLeft);
- if(numberOfRefsLeft <= 0)
+
+ //Happens if calculate has deleted the ref
+ if (_richDataStore.HasValueBeenDeleted(cachedVmId))
+ {
+ _referenceCache.RemoveReference(cKey, out int numRefs);
+ return;
+ }
+
+ if (existingPic.ImageUri.OriginalString != imageUri.OriginalString)
+ {
+ _referenceCache.RemoveReference(cKey, out int numberOfRefsLeft);
+ if (numberOfRefsLeft <= 0)
+ {
+ _richDataStore.DeleteRichData(row, col);
+ }
+ _pictureStore.RemoveReference(existingPic.ImageUri);
+ AddReferenceToPicture(row, col, cacheKey, cachedVmId);
+ }
+ else
{
- _richDataStore.DeleteRichData(row, col);
+ //There is no need to do anything.
+ //This method is attempting to set the picture to the exact same as what is already in the cell
+ return;
+ }
+ }
+ else
+ {
+ //Happens if calculate has deleted the ref
+ if (_richDataStore.HasValueBeenDeleted(cachedVmId))
+ {
+ _referenceCache.RemoveReference(cacheKey, out int numRefs);
+ return;
+ }
+ else
+ {
+ AddReferenceToPicture(row, col, cacheKey, cachedVmId);
}
- _pictureStore.RemoveReference(existingPic.ImageUri);
}
- AddReferenceToPicture(row, col, cacheKey, cachedVmId);
return;
}
@@ -216,9 +249,38 @@ public void SetWebPicture(int row, int col, Uri addressUri, byte[] imageBytes, s
var rdUri = new Uri(ExcelRichValueCollection.PART_URI_PATH, UriKind.Relative);
var imageUri = UriHelper.ResolvePartUri(rdUri, imageInfo.Uri);
+ var existingPic = GetCellPicture(row, col, StructureTypes.WebImage);
+ if (existingPic == null)
+ {
+ existingPic = GetCellPicture(row, col);
+ }
+
if (_referenceCache.Contains(cacheKey, out uint cachedVmId))
{
- AddReferenceToPicture(row, col, cacheKey, cachedVmId);
+ if (existingPic != null)
+ {
+ if(existingPic.ImageUri.OriginalString != imageUri.OriginalString)
+ {
+ var cKey = GetCacheKey(existingPic);
+ _referenceCache.RemoveReference(cKey, out int numberOfRefsLeft);
+ if (numberOfRefsLeft <= 0)
+ {
+ _richDataStore.DeleteRichData(row, col);
+ }
+ _pictureStore.RemoveReference(existingPic.ImageUri);
+ AddReferenceToPicture(row, col, cacheKey, cachedVmId);
+ }
+ else
+ {
+ //There is no need to do anything.
+ //This method is attempting to set the picture to the exact same as what is already in the cell
+ return;
+ }
+ }
+ else
+ {
+ AddReferenceToPicture(row, col, cacheKey, cachedVmId);
+ }
return;
}
@@ -230,7 +292,6 @@ public void SetWebPicture(int row, int col, Uri addressUri, byte[] imageBytes, s
}
else
{
- var existingPic = GetCellPicture(row, col);
if (existingPic != null)
{
if (existingPic.ImageUri.OriginalString == imageUri.OriginalString)
@@ -297,6 +358,11 @@ private void AddNewWebPicture(int row, int col, Uri imageUri, Uri addressUri, st
}
}
+ internal void ReadAndAddReference(PictureCacheKey key, uint vmId)
+ {
+ _referenceCache.Add(key, vmId);
+ }
+
private void AddReferenceToPicture(int row, int col, PictureCacheKey key, uint vmId)
{
var rv = _richDataStore.GetRichValue(vmId);
@@ -387,5 +453,36 @@ public void RemoveCellPicture(int row, int col)
_sheet.Cells[row, col].Value = null;
}
}
+
+
+ ///
+ /// Builds an for the specified cell during worksheet XML loading.
+ /// This method uses the value metadata index to resolve the corresponding rich value, constructs
+ /// a cell picture when the rich value represents image data, and registers the picture reference
+ /// for internal tracking.
+ ///
+ /// The row index of the cell.
+ /// The column index of the cell.
+ ///
+ /// The one-based metadata index read from the worksheet XML that identifies the rich value.
+ ///
+ /// The rich value associated with the cell.
+ ///
+ /// A constructed if the rich value contains image data;
+ /// otherwise, null .
+ ///
+
+ public ExcelCellPicture BuildCellPicture(int row, int col, uint valueMetadataIndex, ExcelRichValue fromRichValue)
+ {
+ fromRichValue?.SetStructure(_sheet.Workbook.RichData.Db);
+ var vmId = _sheet.Workbook.Metadata.Db.ValueMetadata.GetIdByIndex((int)valueMetadataIndex - 1);
+ var pic = GetExcelCellPictureByRichValue(fromRichValue, row, col, vmId);
+ if (pic == null) return null;
+
+ var cacheKey = CellPictureReferenceCache.CreateKey(pic);
+ _referenceCache.Add(cacheKey, vmId);
+
+ return pic;
+ }
}
}
diff --git a/src/EPPlus/CellPictures/LocalImageCacheKey.cs b/src/EPPlus/CellPictures/LocalImageCacheKey.cs
index f819576846..517fa14123 100644
--- a/src/EPPlus/CellPictures/LocalImageCacheKey.cs
+++ b/src/EPPlus/CellPictures/LocalImageCacheKey.cs
@@ -29,6 +29,12 @@ public LocalImageCacheKey(Uri imageUri, CalcOrigins calcOrigin, string altText)
this.calcOrigin = calcOrigin;
this.altText = altText;
}
+
+ public LocalImageCacheKey(ExcelCellPicture picture)
+ : this(picture.ImageUri, picture.CalcOrigin, picture.AltText)
+ {
+ }
+
protected override string Build()
{
var sb = new StringBuilder();
diff --git a/src/EPPlus/CellPictures/WebPictureCacheKey.cs b/src/EPPlus/CellPictures/WebPictureCacheKey.cs
index 33ab36309d..c331d412a1 100644
--- a/src/EPPlus/CellPictures/WebPictureCacheKey.cs
+++ b/src/EPPlus/CellPictures/WebPictureCacheKey.cs
@@ -37,6 +37,12 @@ public WebPictureCacheKey(Uri addressUri, string altText, CalcOrigins calcOrigin
this.width = width;
}
+ public WebPictureCacheKey(ExcelCellPicture pic)
+ : this(pic.ExternalAddress, pic.AltText, pic.CalcOrigin, pic.Sizing ?? WebImageSizing.FitToCellMaintainRatio, null)
+ {
+
+ }
+
protected override string Build()
{
var sb = new StringBuilder();
diff --git a/src/EPPlus/Constants/ExtLstUris.cs b/src/EPPlus/Constants/ExtLstUris.cs
index fd78aeda61..d000ca8ef1 100644
--- a/src/EPPlus/Constants/ExtLstUris.cs
+++ b/src/EPPlus/Constants/ExtLstUris.cs
@@ -56,8 +56,16 @@ internal class ExtLstUris
internal const string ConditionalFormattingUri = "{78C0D931-6437-407d-A8EE-F0AAD7539E65}";
internal const string ExtChildUri = "{B025F937-C7B1-47D3-B67F-A62EFF666E3E}";
- //Feature property bag refrence used for checkboxes is styling.
+ //Feature property bag refrence used for checkboxes is styling. (xfComplementXf)
+ ///
+ /// NOT FOR DXF
+ ///
internal const string FeaturePropertyBag = "{C7286773-470A-42A8-94C5-96B5CB345126}";
+ //dxfComplementExt
+ ///
+ /// FOR DXF
+ ///
+ internal const string FeaturePropertyBagDxf = "{0417FA29-78FA-4A13-93AC-8FF0FAFDF519}";
internal const string Connection2010Uri = "{DE250136-89BD-433C-8126-D09CA5730AF9}";
}
diff --git a/src/EPPlus/Core/ChangableDictionary.cs b/src/EPPlus/Core/ChangableDictionary.cs
index 7668b18cc3..260f7a4849 100644
--- a/src/EPPlus/Core/ChangableDictionary.cs
+++ b/src/EPPlus/Core/ChangableDictionary.cs
@@ -66,7 +66,7 @@ internal virtual void InsertAndShift(int fromPosition, int add)
pos = ~pos;
}
- if (pos + 1 >= _index[0].Length - 1)
+ if (_count >= _index[0].Length - 1)
{
Array.Resize(ref _index[0], _index[0].Length << 1);
Array.Resize(ref _index[1], _index[1].Length << 1);
diff --git a/src/EPPlus/Data/PowerQuery/ExcelPowerQueryMetaDataEntry.cs b/src/EPPlus/Data/PowerQuery/ExcelPowerQueryMetaDataEntry.cs
index c0872575c8..1ebf15ee75 100644
--- a/src/EPPlus/Data/PowerQuery/ExcelPowerQueryMetaDataEntry.cs
+++ b/src/EPPlus/Data/PowerQuery/ExcelPowerQueryMetaDataEntry.cs
@@ -153,11 +153,11 @@ public string GetValueAsText(CultureInfo culture)
}
if (Value is int i)
{
- return "l" + i.ToString();
+ return "l" + i.ToString(CultureInfo.InvariantCulture);
}
if (Value is DateTime dt)
{
- return "d" + dt.ToString("yyyy-MM-ddTHH:mm:ss.fffffffZ");
+ return "d" + dt.ToString("yyyy-MM-ddTHH:mm:ss.fffffffZ", CultureInfo.InvariantCulture);
}
if (Value is bool b)
{
@@ -165,11 +165,11 @@ public string GetValueAsText(CultureInfo culture)
}
if (Value is double d)
{
- return "f" + d.ToString();
+ return "f" + d.ToString(CultureInfo.InvariantCulture);
}
else
{
- return "s" + Value.ToString(); //You shoul never end up here.
+ return "s" + Value.ToString(); //You should never end up here.
}
}
///
diff --git a/src/EPPlus/Data/QueryTable/ExcelQueryTableCollection.cs b/src/EPPlus/Data/QueryTable/ExcelQueryTableCollection.cs
index 231e7ada8e..b3b07b3246 100644
--- a/src/EPPlus/Data/QueryTable/ExcelQueryTableCollection.cs
+++ b/src/EPPlus/Data/QueryTable/ExcelQueryTableCollection.cs
@@ -70,7 +70,7 @@ public ExcelQueryTable this[string name]
}
///
/// Adds a new query table to the collection. Worksheet level query tables are legacy objects and can only be used for older types of connection.
- /// For newer types of connections like PowerQuery, use instead.
+ /// For newer types of connections like PowerQuery, use instead.
///
/// The address
/// The name of the query table.
diff --git a/src/EPPlus/Drawing/Chart/ChartDataSource.cs b/src/EPPlus/Drawing/Chart/ChartDataSource.cs
new file mode 100644
index 0000000000..048c18e39a
--- /dev/null
+++ b/src/EPPlus/Drawing/Chart/ChartDataSource.cs
@@ -0,0 +1,497 @@
+using OfficeOpenXml.Core.CellStore;
+using OfficeOpenXml.FormulaParsing.Excel.Functions.RefAndLookup;
+using OfficeOpenXml.FormulaParsing.Excel.Functions.Text;
+using OfficeOpenXml.FormulaParsing.Utilities;
+using System;
+using System.Collections.Generic;
+using System.Data;
+using System.Globalization;
+using System.IO;
+using System.Linq;
+using System.Text;
+using System.Xml;
+
+namespace OfficeOpenXml.Drawing.Chart
+{
+ ///
+ /// Base-class for things like Series, XSeries, DataLabelsRange
+ /// This class and children should loosely represent "CT_AxDataSource" in dml-chart.xsd
+ ///
+ internal class ChartDataSource : XmlHelper
+ {
+ private readonly bool _isPivot;
+ ExcelWorksheet _ws;
+
+ private bool _isExt = false;
+
+ string _strRef = null;
+
+ string sourceTopPath;
+ string _StrRefParentPath = "{0}/{1}";
+ string _StrRefPath = "{0}/{1}/c:f";
+ string _xSeriesStrLitPath, _xSeriesNumLitPath;
+
+ internal string StrRefFormulaValue
+ {
+ get { return _strRef; }
+ }
+
+ internal bool RefIsValidAddress { private set; get; } = false;
+
+ private double[] _numberLiterals;
+ internal double[] NumberLiterals
+ {
+ get
+ {
+ if (string.IsNullOrEmpty(_xSeriesNumLitPath) == false && GetNode(_xSeriesNumLitPath) != null)
+ {
+ ReadNumLiterals(_xSeriesNumLitPath, out _numberLiterals);
+ return _numberLiterals;
+ }
+ return _numberLiterals;
+ }
+ set
+ {
+ _numberLiterals = value;
+ }
+ }
+
+ private string[] _stringLiterals;
+ internal string[] StringLiterals
+ {
+ get
+ {
+ if (string.IsNullOrEmpty(_xSeriesStrLitPath) == false && GetNode(_xSeriesStrLitPath) != null)
+ {
+ ReadStringLiterals(_xSeriesStrLitPath, out _stringLiterals);
+ return _stringLiterals;
+ }
+ return _stringLiterals;
+ }
+ set
+ {
+ _stringLiterals = value;
+ }
+ }
+
+ ///
+ /// Base-class for things like Series, XSeries, DataLabelsRange
+ /// This class and children should loosely represent "CT_AxDataSource" in dml-chart.xsd
+ ///
+ /// If this source is part of a pivotTable
+ /// The xml node
+ /// The namespace manager
+ /// The worksheet
+ /// The path to the top node of the series this source belongs to, e.g. "c:ser[1]"
+ internal ChartDataSource(bool isPivot, XmlNamespaceManager ns, XmlNode node, ExcelWorksheet ws, string seriesTopPath) : base(ns, node)
+ {
+ _isPivot = isPivot;
+ _ws = ws;
+ sourceTopPath = seriesTopPath;
+
+ var ep = string.Format(_StrRefParentPath, sourceTopPath, "c15:datalabelsRange");
+
+ if (ExistsNode(ep))
+ {
+ _isExt = true;
+ _StrRefPath = ep + "/c15:f";
+ _xSeriesStrLitPath = ep + "/c15:dlblRangeCache";
+ }
+ }
+
+ internal void SetStrRef(string strRef)
+ {
+ _strRef = strRef.Trim();
+ if (_strRef.StartsWith("=", StringComparison.OrdinalIgnoreCase)) _strRef = _strRef.Substring(1);
+
+ if (strRef.StartsWith("{", StringComparison.OrdinalIgnoreCase) && strRef.EndsWith("}", StringComparison.OrdinalIgnoreCase))
+ {
+ if(_isExt)
+ {
+ CreateNode(_StrRefPath, true);
+ SetXmlNodeString(_StrRefPath, _strRef);
+ }
+
+ GetLitValues(_strRef, out double[] numLit, out string[] strLit);
+ NumberLiterals = numLit;
+ StringLiterals = strLit;
+ SetLits(numLit, strLit, _xSeriesNumLitPath, _xSeriesStrLitPath);
+ }
+ else
+ {
+ NumberLiterals = null;
+ StringLiterals = null;
+ CreateNode(_StrRefPath, true);
+
+ if (ExcelCellBase.IsValidAddress(strRef))
+ {
+ RefIsValidAddress = true;
+ SetXmlNodeString(_StrRefPath, ExcelCellBase.GetFullAddress(_ws.Name, _strRef));
+ }
+ else
+ {
+ SetXmlNodeString(_StrRefPath, _strRef);
+ }
+ SetSourceFunction();
+ }
+ }
+
+ private void SetLits(double[] numLit, string[] strLit, string numLitPath, string strLitPath)
+ {
+ if (strLit != null)
+ {
+ XmlNode lit = CreateNode(strLitPath);
+ SetLitArray(lit, strLit);
+ }
+ else if (numLit != null)
+ {
+ XmlNode lit = CreateNode(numLitPath);
+ SetLitArray(lit, numLit);
+ }
+ }
+
+
+ private void ReadNumLiterals(string path, out double[] numberLiterals)
+ {
+ var childNodes = GetNode(path).ChildNodes;
+ numberLiterals = new double[childNodes.Count];
+ List numLits = new();
+
+ foreach (XmlNode node in childNodes)
+ {
+ if (node.NodeType == XmlNodeType.Element && node.LocalName == "pt")
+ {
+ if (double.TryParse(node.InnerText, NumberStyles.Any, CultureInfo.InvariantCulture, out double numLit) == false)
+ {
+ throw new InvalidDataException($"numberLiteral in xml node:'{node.Name}' in ws:'{_ws.Name}' with value:'{node.InnerText}' could not be parsed as double. Chart cannot be read.");
+ }
+ numLits.Add(numLit);
+ }
+ }
+ numberLiterals = numLits.ToArray();
+ }
+
+ private void SetLitArray(XmlNode lit, double[] numLit)
+ {
+ if (numLit.Length == 0) return;
+ var ci = CultureInfo.InvariantCulture;
+
+ //Remove previous child nodes
+ var previousPt = lit.SelectNodes("c:pt", NameSpaceManager);
+ if (previousPt != null)
+ {
+ for (int i = 0; i < previousPt.Count; i++)
+ {
+ lit.RemoveChild(previousPt[i]);
+ }
+ }
+
+ for (int i = 0; i < numLit.Length; i++)
+ {
+ var pt = lit.OwnerDocument.CreateElement("c", "pt", ExcelPackage.schemaChart);
+ pt.SetAttribute("idx", i.ToString(CultureInfo.InvariantCulture));
+ lit.AppendChild(pt);
+ pt.InnerXml = $"{((double)numLit[i]).ToString("R15", ci)} ";
+ }
+ AddCount(lit, numLit.Length);
+ }
+
+ private void SetLitArray(XmlNode lit, string[] strLit)
+ {
+ //Remove previous child nodes
+ var previousPt = lit.SelectNodes("c:pt", NameSpaceManager);
+ if (previousPt != null)
+ {
+ for (int i = 0; i < previousPt.Count; i++)
+ {
+ lit.RemoveChild(previousPt[i]);
+ }
+ }
+
+ for (int i = 0; i < strLit.Length; i++)
+ {
+ var pt = lit.OwnerDocument.CreateElement("c", "pt", ExcelPackage.schemaChart);
+ pt.SetAttribute("idx", i.ToString(CultureInfo.InvariantCulture));
+ lit.AppendChild(pt);
+ pt.InnerXml = $"{strLit[i]} ";
+ }
+ AddCount(lit, strLit.Length);
+ }
+ private void AddCount(XmlNode lit, int count)
+ {
+ var ct = (XmlElement)lit.SelectSingleNode("c:ptCount", NameSpaceManager);
+ if (ct == null)
+ {
+ ct = lit.OwnerDocument.CreateElement("c", "ptCount", ExcelPackage.schemaChart);
+ lit.InsertBefore(ct, lit.FirstChild);
+ }
+ ct.SetAttribute("val", count.ToString(CultureInfo.InvariantCulture));
+ }
+
+ private void ReadStringLiterals(string path, out string[] stringLiterals)
+ {
+ var parentNode = GetNode(path);
+ List strLits = new();
+
+ if (parentNode != null)
+ {
+ var childNodes = parentNode.ChildNodes;
+
+ foreach (XmlNode node in childNodes)
+ {
+ if (node.NodeType == XmlNodeType.Element && node.LocalName == "pt")
+ {
+ strLits.Add(node.InnerText);
+ }
+ }
+ }
+ stringLiterals = strLits.ToArray();
+ }
+
+ private void SetSourceFunction()
+ {
+ if (_StrRefPath.IndexOf("c:numRef", StringComparison.OrdinalIgnoreCase) > 0)
+ {
+ XmlNode cache = TopNode.SelectSingleNode(string.Format("{0}/c:numRef/c:numCache", sourceTopPath), NameSpaceManager);
+ if (cache != null)
+ {
+ cache.ParentNode.RemoveChild(cache);
+ }
+
+ XmlNode lit = TopNode.SelectSingleNode(_xSeriesNumLitPath, NameSpaceManager);
+ if (lit != null)
+ {
+ lit.ParentNode.RemoveChild(lit);
+ }
+ }
+ else
+ {
+ XmlNode cache = TopNode.SelectSingleNode(string.Format("{0}/c:strRef/c:strCache", sourceTopPath), NameSpaceManager);
+ if (cache != null)
+ {
+ cache.ParentNode.RemoveChild(cache);
+ }
+
+ XmlNode lit = TopNode.SelectSingleNode(_xSeriesStrLitPath, NameSpaceManager);
+ if (lit != null)
+ {
+ lit.ParentNode.RemoveChild(lit);
+ }
+
+ var extCacheStr = string.Format("{0}/c15:datalabelsRange/c15:dlblRangeCache", sourceTopPath);
+ XmlNode extCache = TopNode.SelectSingleNode(extCacheStr, NameSpaceManager);
+ if (extCache != null)
+ {
+ extCache.ParentNode.RemoveChild(extCache);
+ }
+ }
+ }
+
+
+ private void GetLitValues(string value, out double[] numberLiterals, out string[] stringLiterals)
+ {
+ value = value.Substring(1, value.Length - 2); //Remove outer {}
+ if (value[0] == '\"' || value[0] == '\'')
+ {
+ numberLiterals = null;
+ stringLiterals = SplitStringValue(value, value[0]);
+ }
+ else
+ {
+ stringLiterals = null;
+ var split = value.Split(',');
+ numberLiterals = new double[split.Length];
+
+ for (int i = 0; i < split.Length; i++)
+ {
+ if (double.TryParse(split[i], NumberStyles.Any, CultureInfo.InvariantCulture, out double d))
+ {
+ numberLiterals[i] = d;
+ }
+ }
+ }
+ }
+
+
+ private string[] SplitStringValue(string value, char textQualifier)
+ {
+ var sb = new StringBuilder();
+ bool insideStr = true;
+ var list = new List();
+ for (int i = 1; i < value.Length; i++)
+ {
+ if (insideStr)
+ {
+ if (value[i] == textQualifier)
+ {
+ insideStr = false;
+ }
+ else
+ {
+ sb.Append(value[i]);
+ }
+ }
+ else
+ {
+ if (value[i] == textQualifier)
+ {
+ insideStr = true;
+ if (sb.Length > 0)
+ {
+ sb.Append(value[i]);
+ }
+ }
+ else if (value[i] == ',')
+ {
+ list.Add(sb.ToString());
+ sb = new StringBuilder();
+ }
+ else
+ {
+ throw (new InvalidOperationException($"String array has an invalid format at position {i}"));
+ }
+ }
+ }
+ if (sb.Length > 0)
+ {
+ list.Add(sb.ToString());
+ }
+
+ return list.ToArray();
+ }
+
+ ///
+ /// Creates a num cach for a chart serie.
+ /// Please note that a serie can only have one column to have a cache.
+ ///
+ /// should be public later
+ internal void CreateCache(XmlNode seriesTopNode)
+ {
+ if (_isPivot) throw (new NotImplementedException("Cache for pivotcharts has not been implemented yet."));
+
+ if (!string.IsNullOrEmpty(StrRefFormulaValue))
+ {
+ var addr = new ExcelRangeBase(_ws, StrRefFormulaValue);
+ bool moreThanOneRow = addr.Rows > 1;
+ bool moreThanOneColumn = addr.Columns > 1;
+
+ if (moreThanOneColumn)
+ {
+ throw (new InvalidOperationException("A serie cannot be multiple columns. Please add one serie per column to create a cache"));
+ }
+
+ CreateCache(StrRefFormulaValue, seriesTopNode);
+ }
+ }
+ internal void CreateCache(string address, XmlNode node)
+ {
+ //var ws = _chart.WorkSheet;
+ var wb = _ws.Workbook;
+ var addr = new ExcelAddressBase(address);
+ if (addr.IsExternal)
+ {
+ var erIx = wb.ExternalLinks.GetExternalLink(addr._wb);
+ if (erIx >= 0 && wb.ExternalLinks[erIx].ExternalLinkType == ExternalReferences.eExternalLinkType.ExternalWorkbook)
+ {
+ var er = wb.ExternalLinks[erIx].As.ExternalWorkbook;
+ if (er.Package == null)
+ {
+ CreateCacheFromExternalCache(node, er, addr);
+ }
+ else
+ {
+ CreateCacheFromRange(node, er.Package.Workbook.Worksheets[addr.WorkSheetName]?.Cells[addr.LocalAddress]);
+ }
+ }
+ else
+ {
+ return;
+ }
+ }
+ else
+ {
+ var ws = string.IsNullOrEmpty(addr.WorkSheetName) ? _ws : _ws.Workbook.Worksheets[addr.WorkSheetName];
+ if (ws == null) //Worksheet does not exist, exit
+ {
+ return;
+ }
+ CreateCacheFromRange(node, ws.Cells[address]);
+ }
+
+ }
+
+ private void CreateCacheFromRange(XmlNode node, ExcelRangeBase range)
+ {
+ if (range == null) return;
+ lastCachedValues.Clear();
+ var startRow = range._fromRow;
+ var items = 0;
+ var cse = new CellStoreEnumerator(range.Worksheet._values, startRow, range._fromCol, range._toRow, range._toCol);
+ while (cse.Next())
+ {
+ var v = cse.Value._value;
+ if (v != null)
+ {
+ string xmlValue = "";
+ if (v.IsNumeric())
+ {
+ var d = Utils.TypeConversion.ConvertUtil.GetValueDouble(v);
+ xmlValue = Utils.TypeConversion.ConvertUtil.GetValueForXml(d, range.Worksheet.Workbook.Date1904);
+ }
+ else
+ {
+ xmlValue = string.Format(CultureInfo.InvariantCulture, v.ToString());
+ }
+
+ var ptNode = node.OwnerDocument.CreateElement("c", "pt", ExcelPackage.schemaChart);
+ node.AppendChild(ptNode);
+ ptNode.SetAttribute("idx", (cse.Row - startRow).ToString(CultureInfo.InvariantCulture));
+ lastCachedValues.Add(xmlValue);
+ ptNode.InnerXml = $"{xmlValue} ";
+ items++;
+ }
+ }
+
+ var countNode = node.SelectSingleNode("c:ptCount", NameSpaceManager) as XmlElement;
+ if (countNode != null)
+ {
+ countNode.SetAttribute("val", items.ToString(CultureInfo.InvariantCulture));
+ }
+ }
+ private void CreateCacheFromExternalCache(XmlNode node, ExternalReferences.ExcelExternalWorkbook er, ExcelAddressBase addr)
+ {
+ var ews = er.CachedWorksheets[addr.WorkSheetName];
+ if (ews == null) return;
+ var startRow = addr._fromRow;
+ var items = 0;
+ var cse = new CellStoreEnumerator(ews.CellValues._values, startRow, addr._fromCol, addr._toRow, addr._toCol);
+ while (cse.Next())
+ {
+ var v = cse.Value;
+ if (v != null)
+ {
+ var d = Utils.TypeConversion.ConvertUtil.GetValueDouble(v);
+ var ptNode = node.OwnerDocument.CreateElement("c", "pt", ExcelPackage.schemaChart);
+ node.AppendChild(ptNode);
+ ptNode.SetAttribute("idx", (cse.Row - startRow).ToString(CultureInfo.InvariantCulture));
+ var xmlValue = Utils.TypeConversion.ConvertUtil.GetValueForXml(d, er._wb.Date1904);
+ lastCachedValues.Add(xmlValue);
+ ptNode.InnerXml = $"{xmlValue} ";
+ items++;
+ }
+ }
+
+ var countNode = node.SelectSingleNode("c:ptCount", NameSpaceManager) as XmlElement;
+ if (countNode != null)
+ {
+ countNode.SetAttribute("val", items.ToString(CultureInfo.InvariantCulture));
+ }
+ }
+
+ private List lastCachedValues = new List();
+ internal List GetCachedValues()
+ {
+ return lastCachedValues;
+ }
+ }
+}
diff --git a/src/EPPlus/Drawing/Chart/ChartEx/ExcelChartExAxis.cs b/src/EPPlus/Drawing/Chart/ChartEx/ExcelChartExAxis.cs
index 9b84a6a6a7..d7be5cdad5 100644
--- a/src/EPPlus/Drawing/Chart/ChartEx/ExcelChartExAxis.cs
+++ b/src/EPPlus/Drawing/Chart/ChartEx/ExcelChartExAxis.cs
@@ -12,6 +12,7 @@ Date Author Change
*************************************************************************************************/
using OfficeOpenXml.Utils.EnumUtils;
using System;
+using System.Collections.Generic;
using System.Xml;
namespace OfficeOpenXml.Drawing.Chart.ChartEx
@@ -189,7 +190,7 @@ internal override ExcelChartTitle GetTitle()
return _title;
}
- internal override object[] GetAxisValues(out bool isCount)
+ internal override List GetAxisValues(out bool isCount)
{
throw new NotImplementedException();
}
diff --git a/src/EPPlus/Drawing/Chart/ExcelBarChart.cs b/src/EPPlus/Drawing/Chart/ExcelBarChart.cs
index 654fc52751..75f8d035ab 100644
--- a/src/EPPlus/Drawing/Chart/ExcelBarChart.cs
+++ b/src/EPPlus/Drawing/Chart/ExcelBarChart.cs
@@ -240,7 +240,7 @@ public int Overlap
{
get
{
- return _chartXmlHelper.GetXmlNodeInt(_overlapPath);
+ return _chartXmlHelper.GetXmlNodeInt(_overlapPath, 0);
}
set
{
diff --git a/src/EPPlus/Drawing/Chart/ExcelBarChartSerie.cs b/src/EPPlus/Drawing/Chart/ExcelBarChartSerie.cs
index feb80bdd6f..ce0cf52961 100644
--- a/src/EPPlus/Drawing/Chart/ExcelBarChartSerie.cs
+++ b/src/EPPlus/Drawing/Chart/ExcelBarChartSerie.cs
@@ -44,7 +44,7 @@ public ExcelChartSerieDataLabel DataLabel
{
if (_dataLabel == null)
{
- if (ExcelChartDataLabelStandard.ForbiddDataLabelPosition(_chart) == false)
+ if (ExcelChartDataLabelStandard.IsDataLabelPositionForbidden(_chart) == false)
{
_dataLabel = new ExcelChartSerieDataLabel(_chart, NameSpaceManager, TopNode, SchemaNodeOrder);
}
diff --git a/src/EPPlus/Drawing/Chart/ExcelChart.cs b/src/EPPlus/Drawing/Chart/ExcelChart.cs
index 806858a40c..8403e46a84 100644
--- a/src/EPPlus/Drawing/Chart/ExcelChart.cs
+++ b/src/EPPlus/Drawing/Chart/ExcelChart.cs
@@ -17,12 +17,14 @@ Date Author Change
using OfficeOpenXml.Drawing.Style.ThreeD;
using OfficeOpenXml.Packaging;
using OfficeOpenXml.Style;
+
using OfficeOpenXml.Table.PivotTable;
using OfficeOpenXml.Utils.FileUtils;
using System;
using System.Collections.Generic;
using System.Globalization;
using System.IO;
+using System.Linq;
using System.Text;
using System.Xml;
@@ -104,7 +106,7 @@ private bool HasPrimaryAxis()
return false;
}
internal abstract void AddAxis();
- bool _secondaryAxis = false;
+ bool? _secondaryAxis = null;
///
/// If true the charttype will use the secondary axis.
/// The chart must contain a least one other charttype that uses the primary axis.
@@ -113,7 +115,11 @@ public bool UseSecondaryAxis
{
get
{
- return _secondaryAxis;
+ if (_secondaryAxis.HasValue == false)
+ {
+ _secondaryAxis = Array.FindIndex(Axis, x => ((ExcelChartAxis)x).Id == YAxis.Id) > 1;
+ }
+ return _secondaryAxis.Value;
}
set
{
@@ -587,7 +593,7 @@ protected internal bool IsTypeStacked()
/// Returns true if the chart is of type clustered
///
/// True if the chart is of type clustered
- protected bool IsTypeClustered()
+ protected internal bool IsTypeClustered()
{
return ChartType == eChartType.BarClustered ||
ChartType == eChartType.BarClustered3D ||
@@ -601,6 +607,50 @@ protected bool IsTypeClustered()
ChartType == eChartType.PyramidColClustered;
}
///
+ /// Returns true if the chart is of type bar chart
+ ///
+ /// True if the chart is of type bar
+ protected internal bool IsTypeBar()
+ {
+ return ChartType == eChartType.BarClustered ||
+ ChartType == eChartType.BarStacked ||
+ ChartType == eChartType.BarStacked100 ||
+ ChartType == eChartType.BarClustered3D ||
+ ChartType == eChartType.BarStacked3D ||
+ ChartType == eChartType.BarStacked1003D ||
+ ChartType == eChartType.ConeBarClustered ||
+ ChartType == eChartType.ConeBarStacked ||
+ ChartType == eChartType.ConeBarStacked100 ||
+ ChartType == eChartType.CylinderBarClustered ||
+ ChartType == eChartType.CylinderBarStacked ||
+ ChartType == eChartType.CylinderBarStacked100 ||
+ ChartType == eChartType.PyramidBarClustered ||
+ ChartType == eChartType.PyramidBarStacked ||
+ ChartType == eChartType.PyramidBarStacked100;
+ }
+ ///
+ /// Returns true if the chart is of type column chart
+ ///
+ /// True if the chart is of type column
+ protected internal bool IsTypeColumn()
+ {
+ return ChartType == eChartType.ColumnClustered ||
+ ChartType == eChartType.ColumnStacked ||
+ ChartType == eChartType.ColumnStacked100 ||
+ ChartType == eChartType.ColumnClustered3D ||
+ ChartType == eChartType.ColumnStacked3D ||
+ ChartType == eChartType.ColumnStacked1003D ||
+ ChartType == eChartType.ConeColClustered ||
+ ChartType == eChartType.ConeColStacked ||
+ ChartType == eChartType.ConeColStacked100 ||
+ ChartType == eChartType.CylinderColClustered ||
+ ChartType == eChartType.CylinderColStacked ||
+ ChartType == eChartType.CylinderColStacked100 ||
+ ChartType == eChartType.PyramidColClustered ||
+ ChartType == eChartType.PyramidColStacked ||
+ ChartType == eChartType.PyramidColStacked100;
+ }
+ ///
/// Returns true if the chart is a pie or Doughnut chart
///
/// True if the chart is a pie or Doughnut chart
diff --git a/src/EPPlus/Drawing/Chart/ExcelChartAxis.cs b/src/EPPlus/Drawing/Chart/ExcelChartAxis.cs
index 1df76b94d4..264249edd5 100644
--- a/src/EPPlus/Drawing/Chart/ExcelChartAxis.cs
+++ b/src/EPPlus/Drawing/Chart/ExcelChartAxis.cs
@@ -18,6 +18,7 @@ Date Author Change
using OfficeOpenXml.Utils.EnumUtils;
using System;
using System.Reflection.Emit;
+using System.Collections.Generic;
namespace OfficeOpenXml.Drawing.Chart
{
@@ -240,7 +241,7 @@ public ExcelTextBody TextBody
{
//TODO: CreateTopNode of ExcelTextFontXML on init here somehow
//Note: txPR is a CT_Textbody node. The only difference between txPr and TextBody
- _textBody = new ExcelTextBody(_chart, NameSpaceManager, TopNode, $"{_nsPrefix}:txPr/a:bodyPr", SchemaNodeOrder/*, ((ExcelTextFontXml)Font).CreateTopNode*/);
+ _textBody = new ExcelTextBody(_chart, NameSpaceManager, TopNode, $"{_nsPrefix}:txPr/a:bodyPr", SchemaNodeOrder, ((ExcelTextFontXml)Font).CreateTopNode);
}
return _textBody;
}
@@ -599,6 +600,6 @@ void IStyleMandatoryProperties.SetMandatoryProperties()
CreatespPrNode($"{_nsPrefix}:spPr");
}
- internal abstract object[] GetAxisValues(out bool isCount);
+ internal abstract List GetAxisValues(out bool isCount);
}
}
diff --git a/src/EPPlus/Drawing/Chart/ExcelChartAxisStandard.cs b/src/EPPlus/Drawing/Chart/ExcelChartAxisStandard.cs
index 12793c96c0..611e96f8d4 100644
--- a/src/EPPlus/Drawing/Chart/ExcelChartAxisStandard.cs
+++ b/src/EPPlus/Drawing/Chart/ExcelChartAxisStandard.cs
@@ -21,6 +21,7 @@ Date Author Change
using System.Collections.Generic;
using System.Globalization;
using System.Linq;
+using System.Reflection.Emit;
using System.Xml;
using static OfficeOpenXml.Style.XmlAccess.ExcelNumberFormatXml;
namespace OfficeOpenXml.Drawing.Chart
@@ -387,6 +388,57 @@ public override eTickLabelPosition TickLabelPosition
SetXmlNodeString(_ticLblPos_Path, v);
}
}
+ const string _label_alignment_path = "c:lblAlgn/@val";
+ ///
+ /// Set the alingment for the axis labels within the major tickmarks.
+ ///
+ public eAxisLabelAlignment LabelAlignment
+ {
+ get
+ {
+ switch(GetXmlNodeString(_label_alignment_path))
+ {
+ case "l":
+ return eAxisLabelAlignment.Left;
+ case "r":
+ return eAxisLabelAlignment.Right;
+ default:
+ return eAxisLabelAlignment.Center;
+ }
+ }
+ set
+ {
+ string v;
+ switch (value)
+ {
+ case eAxisLabelAlignment.Left:
+ v = "l";
+ break;
+ case eAxisLabelAlignment.Right:
+ v = "r";
+ break;
+ default:
+ v = "ctr";
+ break;
+ }
+ SetXmlNodeString(_label_alignment_path, v);
+ }
+ }
+ const string _label_offset_path = "c:lblOffset/@val";
+ ///
+ /// Set the offset in whole percent between the labels and the axis.
+ ///
+ public int LabelOffset
+ {
+ get
+ {
+ return GetXmlNodeInt(_label_offset_path, 100);
+ }
+ set
+ {
+ SetXmlNodeInt(_label_offset_path, value, null, false);
+ }
+ }
const string _displayUnitPath = "c:dispUnits/c:builtInUnit/@val";
const string _custUnitPath = "c:dispUnits/c:custUnit/@val";
///
@@ -742,17 +794,20 @@ internal bool IsXAxis
return false;
}
}
- internal override object[] GetAxisValues(out bool isCount)
+ internal override List GetAxisValues(out bool isCount)
{
List> values;
GetSeriesValues(out isCount, out values);
var dl = values.SelectMany(x => x).Distinct().ToList();
- dl.Sort();
+ if (AxisType!=eAxisType.Cat)
+ {
+ dl.Sort();
+ }
if (Orientation == eAxisOrientation.MaxMin)
{
dl.Reverse();
}
- return dl.ToArray();
+ return dl;
}
internal void GetSeriesValues(out bool isCount, out List> values)
@@ -760,9 +815,10 @@ internal void GetSeriesValues(out bool isCount, out List> values)
values = new List>();
isCount = false;
List pl = new List();
+ int ix = 0;
foreach (var ct in _chart.PlotArea.ChartTypes)
{
- foreach (var serie in _chart.Series)
+ foreach (var serie in ct.Series)
{
var l = new List();
values.Add(l);
@@ -775,21 +831,22 @@ internal void GetSeriesValues(out bool isCount, out List> values)
}
else
{
- AddFromSerie(l, serie.XSeries, serie.NumberLiteralsX, serie.StringLiteralsX, true);
+ AddFromSerie(l, serie.XSeries, serie.NumberLiteralsX, serie.StringLiteralsX, true, new string[] { serie.HeaderAddress?.Address, serie.GetHeaderText(ix) });
}
}
else
{
- if (ct.YAxis == this)
+ if (ct.YAxis.Id == Id)
{
- AddFromSerie(l, serie.Series, serie.NumberLiteralsY, serie.StringLiteralsY, false, pl);
+ AddFromSerie(l, serie.Series, serie.NumberLiteralsY, serie.StringLiteralsY, false, null, pl);
}
- else if (ct.XAxis == this)
+ else if (ct.XAxis.Id == Id)
{
- AddFromSerie(l, serie.XSeries, serie.NumberLiteralsX, serie.StringLiteralsX, false);
+ AddFromSerie(l, serie.XSeries, serie.NumberLiteralsX, serie.StringLiteralsX, false, null);
}
}
pl = l;
+ ix++;
}
}
if (_chart.IsTypePercentStacked() && IsYAxis)
@@ -849,7 +906,7 @@ private void AddCountFromSeries(List l, string address, double[] numberL
}
}
- private void AddFromSerie(List list, string address, double[] numberLiterals, string[] stringLiterals, bool useText, List prevList=null)
+ private void AddFromSerie(List list, string address, double[] numberLiterals, string[] stringLiterals, bool useText, string[] headerInfo, List prevList=null)
{
var isStacked = _chart.IsTypeStacked() && prevList != null;
if (numberLiterals?.Length > 0)
@@ -879,14 +936,21 @@ private void AddFromSerie(List list, string address, double[] numberLite
{
var range = ws.Cells[a.Address];
var i = 0;
- for(var r=0;r ";
}
@@ -268,7 +268,7 @@ public ExcelTextBody TextBody
{
if (_textBody == null)
{
- _textBody = new ExcelTextBody(_chart, NameSpaceManager, TopNode, $"{_nsPrefix}:txPr/a:bodyPr"/*, SchemaNodeOrder, ((ExcelTextFontXml)Font).CreateTopNode*/);
+ _textBody = new ExcelTextBody(_chart, NameSpaceManager, TopNode, $"{NsPrefix}:txPr/a:bodyPr", SchemaNodeOrder, ((ExcelTextFontXml)Font).CreateTopNode);
}
return _textBody;
}
diff --git a/src/EPPlus/Drawing/Chart/ExcelChartDataLabelCollection.cs b/src/EPPlus/Drawing/Chart/ExcelChartDataLabelCollection.cs
index 8b2b2a0a28..613500be9c 100644
--- a/src/EPPlus/Drawing/Chart/ExcelChartDataLabelCollection.cs
+++ b/src/EPPlus/Drawing/Chart/ExcelChartDataLabelCollection.cs
@@ -13,7 +13,8 @@ Date Author Change
using System;
using System.Collections;
using System.Collections.Generic;
-using System.Reflection.Emit;
+using System.ComponentModel;
+using System.Data;
using System.Xml;
namespace OfficeOpenXml.Drawing.Chart
@@ -31,14 +32,25 @@ internal ExcelChartDataLabelCollection(ExcelChart chart, XmlNamespaceManager ns,
{
SchemaNodeOrder = schemaNodeOrder;
_list = new List();
- foreach (XmlNode dataLabelNode in TopNode.SelectNodes("c:dLbl", ns))
+ var existingDataLabelNodes = TopNode.SelectNodes("c:dLbl", ns);
+ foreach (XmlNode dataLabelNode in existingDataLabelNodes)
{
_list.Add(new ExcelChartDataLabelItem(chart, ns, dataLabelNode, "", schemaNodeOrder));
}
parentDatalabel = parent;
_chart = chart;
+
}
+
+ internal void InitializeDataLabelsXml()
+ {
+ if(_list.Count == 0)
+ {
+ var seriesNode = TopNode.ParentNode;
+ }
+ }
+
///
/// Adds a new chart label to the collection
///
@@ -72,7 +84,7 @@ private ExcelChartDataLabelItem CreateDataLabel(int idx)
dl.ShowLegendKey = parentDatalabel.ShowLegendKey;
dl.ShowLeaderLines = true;
dl.ShowValue = true;
- dl.Position = eLabelPosition.Center;
+ dl.Position = parentDatalabel.Position;
if (idx < _list.Count)
{
diff --git a/src/EPPlus/Drawing/Chart/ExcelChartDataLabelItem.cs b/src/EPPlus/Drawing/Chart/ExcelChartDataLabelItem.cs
index 8e0d06e5cb..c75c81844f 100644
--- a/src/EPPlus/Drawing/Chart/ExcelChartDataLabelItem.cs
+++ b/src/EPPlus/Drawing/Chart/ExcelChartDataLabelItem.cs
@@ -10,6 +10,10 @@ Date Author Change
*************************************************************************************************
01/27/2020 EPPlus Software AB Initial release EPPlus 5
*************************************************************************************************/
+using OfficeOpenXml.FormulaParsing.Excel.Functions.Information;
+using OfficeOpenXml.FormulaParsing.Excel.Functions.MathFunctions;
+using OfficeOpenXml.Style;
+using System.Collections.Generic;
using System.Globalization;
using System.Xml;
@@ -20,10 +24,12 @@ namespace OfficeOpenXml.Drawing.Chart
///
public class ExcelChartDataLabelItem : ExcelChartDataLabelStandard
{
+ string _fontPropertiesPath = "";
internal ExcelChartDataLabelItem(ExcelChart chart, XmlNamespaceManager ns, XmlNode node, string nodeName, string[] schemaNodeOrder)
: base(chart, ns, node, nodeName, schemaNodeOrder)
{
Layout = new ExcelLayout(NameSpaceManager, TopNode, $"c:layout","c:extLst/c:ext[1]/c15:layout", SchemaNodeOrder);
+ _fontPropertiesPath = $"{NsPrefix}:tx/{NsPrefix}:rich";
}
///
@@ -45,5 +51,7 @@ public int Index
SetXmlNodeString("c:idx/@val", value.ToString(CultureInfo.InvariantCulture));
}
}
+
+ internal string ValueFromSeries;
}
}
diff --git a/src/EPPlus/Drawing/Chart/ExcelChartDataLabelStandard.cs b/src/EPPlus/Drawing/Chart/ExcelChartDataLabelStandard.cs
index 743545c42b..3d8673c053 100644
--- a/src/EPPlus/Drawing/Chart/ExcelChartDataLabelStandard.cs
+++ b/src/EPPlus/Drawing/Chart/ExcelChartDataLabelStandard.cs
@@ -77,7 +77,10 @@ internal ExcelChartDataLabelStandard(ExcelChart chart, XmlNamespaceManager ns, X
const string positionPath = "c:dLblPos/@val";
///
/// Position of the labels
- /// Note: Only Center, InEnd and InBase are allowed for dataLabels on stacked columns
+ /// BE AWARE! For SERIES labels and all underlying labels:
+ /// Setting a position not available for this label in the Excel UI May cause a corrupt file.
+ /// Note: Only Center, InEnd and InBase are allowed for dataLabels on stacked columns
+ /// (Same applies to most BarCharts but they allow OutEnd)
///
public override eLabelPosition Position
{
@@ -87,14 +90,19 @@ public override eLabelPosition Position
}
set
{
- if (ForbiddDataLabelPosition(_chart))
+ if (IsDataLabelPositionForbidden(_chart))
{
throw new InvalidOperationException("Can't set data label position on a 3D-chart");
}
+ if(_chart.ChartType == eChartType.ColumnClustered && value == eLabelPosition.Top)
+ {
+ throw new InvalidOperationException($"DataLabelPosition: '{value}' is not allowed on chart of type: '{_chart.ChartType}' \n " +
+ $"because it would cause a corrupt file");
+ }
SetXmlNodeString(positionPath, GetPosText(value));
}
}
- internal static bool ForbiddDataLabelPosition(ExcelChart _chart)
+ internal static bool IsDataLabelPositionForbidden(ExcelChart _chart)
{
return _chart.IsType3D() && !_chart.IsTypePie() && _chart.ChartType != eChartType.Line3D
|| _chart.IsTypeDoughnut();
@@ -144,7 +152,7 @@ public override bool ShowSeriesName
SetXmlNodeString(showSerPath, value ? "1" : "0");
}
}
- const string showPerentPath = "c:showPercent/@val";
+ const string showPercentPath = "c:showPercent/@val";
///
/// Show percent values
///
@@ -152,11 +160,11 @@ public override bool ShowPercent
{
get
{
- return GetXmlNodeBool(showPerentPath);
+ return GetXmlNodeBool(showPercentPath);
}
set
{
- SetXmlNodeString(showPerentPath, value ? "1" : "0");
+ SetXmlNodeString(showPercentPath, value ? "1" : "0");
}
}
const string showLeaderLinesPath = "c:showLeaderLines/@val";
@@ -254,5 +262,30 @@ public override string Separator
}
}
}
+
+ internal void AddExtFieldTableEmpty()
+ {
+ CreateNode($"{extPath}/c15:dlblFieldTable");
+ }
+
+
+ internal bool ShowDatalabelsRange
+ {
+ get
+ {
+ return GetXmlNodeBool($"{extPath}/c15:showDataLabelsRange");
+ }
+ set
+ {
+ var rangePath = $"{extPath}/c15:showDataLabelsRange";
+ if(ExistsNode(rangePath) == false)
+ {
+ CreateNode(rangePath);
+ }
+ SetXmlNodeBool(rangePath+"/@val", value);
+ }
+ }
+
+ internal ExcelAddressBase SingleCellAddressFromSeries = null;
}
}
diff --git a/src/EPPlus/Drawing/Chart/ExcelChartLegend.cs b/src/EPPlus/Drawing/Chart/ExcelChartLegend.cs
index 2b22672552..40e1cf5a59 100644
--- a/src/EPPlus/Drawing/Chart/ExcelChartLegend.cs
+++ b/src/EPPlus/Drawing/Chart/ExcelChartLegend.cs
@@ -120,10 +120,13 @@ internal List LoadLegendEntries()
{
if (this is ExcelChartExLegend) return new List(); //Legend entries are not applicable for extended charts.
var entries = new List();
- var nodes = GetNodes("c:legendEntry");
- foreach(XmlNode n in nodes)
+ if (TopNode != null)
{
- entries.Add(new ExcelChartLegendEntry(NameSpaceManager, n, (ExcelChartStandard)_chart));
+ var nodes = GetNodes("c:legendEntry");
+ foreach (XmlNode n in nodes)
+ {
+ entries.Add(new ExcelChartLegendEntry(NameSpaceManager, n, (ExcelChartStandard)_chart));
+ }
}
return entries;
}
diff --git a/src/EPPlus/Drawing/Chart/ExcelChartLine.cs b/src/EPPlus/Drawing/Chart/ExcelChartLine.cs
index 6e4a8f27f9..ef674d4422 100644
--- a/src/EPPlus/Drawing/Chart/ExcelChartLine.cs
+++ b/src/EPPlus/Drawing/Chart/ExcelChartLine.cs
@@ -102,6 +102,17 @@ void IDrawingStyleBase.CreatespPr()
{
CreatespPrNode();
}
+
+ internal bool HasValue()
+ {
+ if(_threeD != null | _effect != null | _fill != null | _border != null)
+ {
+ return true;
+ }
+
+ return false;
+ }
+
///
/// Removes the item
///
diff --git a/src/EPPlus/Drawing/Chart/ExcelChartSerie.cs b/src/EPPlus/Drawing/Chart/ExcelChartSerie.cs
index 3d80b56a59..d4e740a1c5 100644
--- a/src/EPPlus/Drawing/Chart/ExcelChartSerie.cs
+++ b/src/EPPlus/Drawing/Chart/ExcelChartSerie.cs
@@ -10,17 +10,18 @@ Date Author Change
*************************************************************************************************
01/27/2020 EPPlus Software AB Initial release EPPlus 5
*************************************************************************************************/
-using System;
-using System.Collections.Generic;
-using System.Text;
-using System.Xml;
-using System.Linq;
using OfficeOpenXml.Core.CellStore;
-using System.Globalization;
using OfficeOpenXml.Drawing.Interfaces;
using OfficeOpenXml.Drawing.Style.Effect;
using OfficeOpenXml.Drawing.Style.ThreeD;
using OfficeOpenXml.FormulaParsing.Excel.Functions.RefAndLookup;
+using System;
+using System.Collections.Generic;
+using System.Globalization;
+using System.Linq;
+using System.Reflection;
+using System.Text;
+using System.Xml;
namespace OfficeOpenXml.Drawing.Chart
{
///
@@ -249,8 +250,9 @@ internal string ToFullAddress(string value)
}
}
- internal string GetHeaderText()
+ internal string GetHeaderText(int index)
{
+ var ret = "";
if(string.IsNullOrEmpty(Header) == false)
{
return Header;
@@ -268,9 +270,9 @@ internal string GetHeaderText()
{
return ws.Cells[HeaderAddress.Address].Offset(0, 0).Text;
}
- }
+ }
}
- return "";
+ return $"Series{index + 1}";
}
}
}
diff --git a/src/EPPlus/Drawing/Chart/ExcelChartSerieDataLabel.cs b/src/EPPlus/Drawing/Chart/ExcelChartSerieDataLabel.cs
index ce8b6c61e0..e34f5a268a 100644
--- a/src/EPPlus/Drawing/Chart/ExcelChartSerieDataLabel.cs
+++ b/src/EPPlus/Drawing/Chart/ExcelChartSerieDataLabel.cs
@@ -10,13 +10,12 @@ Date Author Change
*************************************************************************************************
01/27/2020 EPPlus Software AB Initial release EPPlus 5
*************************************************************************************************/
+using OfficeOpenXml.FormulaParsing.Excel.Functions.RefAndLookup;
using System;
-using System.Collections.Generic;
-using System.Text;
+using System.Data;
+using System.Linq;
+using System.Security.Cryptography.Xml;
using System.Xml;
-using OfficeOpenXml.Drawing.Interfaces;
-using OfficeOpenXml.Drawing.Style.Effect;
-using OfficeOpenXml.Style;
namespace OfficeOpenXml.Drawing.Chart
{
@@ -26,9 +25,21 @@ namespace OfficeOpenXml.Drawing.Chart
public sealed class ExcelChartSerieDataLabel : ExcelChartDataLabelStandard
{
internal ExcelChartSerieDataLabel(ExcelChart chart, XmlNamespaceManager ns, XmlNode node, string[] schemaNodeOrder)
- : base(chart, ns,node,"dLbls", schemaNodeOrder)
+ : base(chart, ns, node, "dLbls", schemaNodeOrder)
{
- Position = eLabelPosition.Center;
+ //Position = eLabelPosition.Center;
+ var parentSeries = GetParentSeries();
+
+ var strRef = parentSeries.GetDataLabelRange();
+ if (string.IsNullOrEmpty(strRef) == false)
+ {
+ SetValueSource(strRef);
+ //ValueFromCells = strRef;
+ //if (ExcelCellBase.IsValidAddress(strRef))
+ //{
+ // DataLabelRange = chart.WorkSheet.Cells[strRef];
+ //}
+ }
}
ExcelChartDataLabelCollection _dataLabels = null;
///
@@ -41,9 +52,126 @@ public ExcelChartDataLabelCollection DataLabels
if (_dataLabels == null)
{
_dataLabels = new ExcelChartDataLabelCollection(_chart, NameSpaceManager, TopNode, SchemaNodeOrder, this as ExcelChartDataLabelStandard);
+
+ //Fill datalabel addresses
+ if(DataLabelRange != null)
+ {
+ var address = DataLabelRange;
+ for(int i = 0; i< _dataLabels.Count(); i++)
+ {
+ if (address.Rows > address.Columns)
+ {
+ _dataLabels[i].SingleCellAddressFromSeries = address.TakeSingleCell(i, 0);
+ }
+ else
+ {
+ _dataLabels[i].SingleCellAddressFromSeries = address.TakeSingleCell(0, i);
+ }
+ }
+ }
}
return _dataLabels;
}
}
+
+ private string _valueFromCellsRange = null;
+
+ ///
+ /// Value From Cells in excel
+ /// This can be a range address or a list of values defined by a formula or within {}
+ /// e.g {"\one\",\"two\"}
+ /// This follows essentially the same rules as Serie or XSerie ranges in chart.AddSerie()
+ ///
+ public string ValueFromCellsRange {
+ get
+ {
+ return _valueFromCellsRange;
+ }
+ set
+ {
+ SetValueSource(value);
+ }
+ }
+
+ internal ExcelRangeBase DataLabelRange { get; private set; } = null;
+
+
+ ExcelChartStandardSerie GetParentSeries()
+ {
+ //TODO: The way we aquire the Series instance here is clumsy.
+ //Fix as part of datalabel refactor?
+ //Perhaps the series of a series label should be part of its constructor.
+ //Or use an eventhandler
+ //For a single case however that feels overkill.
+
+ //Has to get the series index:
+ var idxNode = (XmlElement)TopNode.ParentNode.SelectSingleNode($"{NsPrefix}:idx", NameSpaceManager);
+ var idxNodeValue = int.Parse(idxNode.GetAttribute("val"));
+ //Get the series this datalabel is on
+ return (ExcelChartStandardSerie)_chart.Series[idxNodeValue];
+ }
+
+ ///
+ /// Select range for Value From Cells
+ /// sets ValueFromCellsRange to the AddressAbsolute of the address
+ ///
+ public void SetValueFromCellsRange(ExcelAddressBase address)
+ {
+ ValueFromCellsRange = address.AddressAbsolute.ToString();
+ }
+
+ ///
+ /// Select range for Value From Cells
+ ///
+ /// must be a single; cell, row or column. Or alternatively a collection of literals
+ /// Thrown when input is not a cell, a row or a column
+ private void SetValueSource(string reference)
+ {
+ //TODO: Arguably this is just another series with a series cache.
+ //Same as Cat or Val except that it is added in Ext on the Serie node
+ //ShowValue property essentially changes the datalabels in the same way.
+ //This could be unified somehow so that all serie ranges; Cat, Val and DataLabelRange are handled the same way.
+ //The start of this is now being done in ChartDataSource.cs
+
+ var currentSeries = GetParentSeries();
+ //Set the ext data needed in the Series node
+ currentSeries.SetDataLabelRange(reference);
+
+ if(currentSeries.DataLabelRangeSource.RefIsValidAddress)
+ {
+ DataLabelRange = _chart.WorkSheet.Cells[reference];
+ }
+
+ //Create the Datalabels if they do not exist
+ if (DataLabels.Count < currentSeries.NumberOfItems)
+ {
+ for (int i = 0; i < currentSeries.NumberOfItems; i++)
+ {
+ ExcelChartDataLabelItem currentLabel;
+ if (DataLabels.Count - 1 < i)
+ {
+ currentLabel = DataLabels.Add(i);
+ }
+ else
+ {
+ currentLabel = DataLabels[i];
+ }
+ currentLabel.AddExtFieldTableEmpty();
+ currentLabel.ShowDatalabelsRange = true;
+
+ if (DataLabelRange != null)
+ {
+ if (DataLabelRange.Rows > DataLabelRange.Columns)
+ {
+ currentLabel.SingleCellAddressFromSeries = DataLabelRange.TakeSingleCell(i, 0);
+ }
+ else
+ {
+ currentLabel.SingleCellAddressFromSeries = DataLabelRange.TakeSingleCell(0, i);
+ }
+ }
+ }
+ }
+ }
}
-}
+}
\ No newline at end of file
diff --git a/src/EPPlus/Drawing/Chart/ExcelChartStandard.cs b/src/EPPlus/Drawing/Chart/ExcelChartStandard.cs
index ecae33d2f1..f7df55c8ec 100644
--- a/src/EPPlus/Drawing/Chart/ExcelChartStandard.cs
+++ b/src/EPPlus/Drawing/Chart/ExcelChartStandard.cs
@@ -129,7 +129,7 @@ private void Init(ExcelDrawings drawings, XmlNode chartNode)
{
_isChartEx = chartNode.NamespaceURI == ExcelPackage.schemaChartExMain;
_chartXmlHelper = XmlHelperFactory.Create(drawings.NameSpaceManager, chartNode);
- _chartXmlHelper.AddSchemaNodeOrder(new string[] { "date1904", "lang", "roundedCorners", "AlternateContent", "style", "clrMapOvr", "pivotSource", "protection", "chart", "ofPieType", "title", "autoTitleDeleted", "pivotFmts", "view3D", "floor", "sideWall", "backWall", "plotArea", "wireframe", "barDir", "grouping", "scatterStyle", "radarStyle", "varyColors", "ser", "dLbls", "bubbleScale", "showNegBubbles", "firstSliceAng", "holeSize", "dropLines", "hiLowLines", "upDownBars", "marker", "smooth", "shape", "legend", "plotVisOnly", "dispBlanksAs", "gapWidth", "upBars", "downBars", "showDLblsOverMax", "overlap", "bandFmts", "axId", "spPr", "txPr", "printSettings" }, ExcelDrawing._schemaNodeOrderSpPr);
+ _chartXmlHelper.AddSchemaNodeOrder(new string[] { "date1904", "lang", "roundedCorners", "AlternateContent", "style", "clrMapOvr", "pivotSource", "protection", "chart", "ofPieType", "title", "autoTitleDeleted", "pivotFmts", "view3D", "floor", "sideWall", "backWall", "plotArea", "wireframe", "barDir", "grouping", "scatterStyle", "radarStyle", "varyColors", "ser", "dLbls", "bubbleScale", "showNegBubbles", "firstSliceAng", "holeSize", "dropLines", "hiLowLines", "upDownBars", "marker", "smooth", "shape", "legend", "plotVisOnly", "dispBlanksAs", "gapWidth", "upBars", "downBars", "showDLblsOverMax", "overlap", "bandFmts", "axId", "tx", "layout", "overlay", "spPr", "txPr", "printSettings" }, ExcelDrawing._schemaNodeOrderSpPr);
WorkSheet = drawings.Worksheet;
}
#endregion
diff --git a/src/EPPlus/Drawing/Chart/ExcelChartStandardSerie.cs b/src/EPPlus/Drawing/Chart/ExcelChartStandardSerie.cs
index 06c03b88d7..3f578b5798 100644
--- a/src/EPPlus/Drawing/Chart/ExcelChartStandardSerie.cs
+++ b/src/EPPlus/Drawing/Chart/ExcelChartStandardSerie.cs
@@ -10,15 +10,20 @@ Date Author Change
*************************************************************************************************
05/15/2020 EPPlus Software AB EPPlus 5.2
*************************************************************************************************/
+using OfficeOpenXml.Core.CellStore;
+using OfficeOpenXml.FormulaParsing.Excel.Functions.RefAndLookup;
+using OfficeOpenXml.FormulaParsing.Excel.Functions.Text;
using System;
using System.Collections.Generic;
-using System.Text;
-using System.Xml;
-using System.Linq;
-using OfficeOpenXml.Core.CellStore;
using System.Globalization;
-using System.Runtime.CompilerServices;
using System.IO;
+using OfficeOpenXml.FormulaParsing.Utilities;
+using OfficeOpenXml.FormulaParsing.Excel.Functions.MathFunctions;
+using System.Linq;
+using System.Runtime.CompilerServices;
+using System.Text;
+using System.Xml;
+
namespace OfficeOpenXml.Drawing.Chart
{
///
@@ -28,26 +33,28 @@ public class ExcelChartStandardSerie : ExcelChartSerie
{
private readonly bool _isPivot;
-
double[] _NumberLiteralsY = null;
double[] _NumberLiteralsX = null;
string[] _StringLiteralsX = null;
string[] _StringLiteralsY = null;
+ const string extPath = "c:extLst/c:ext";
+ const string dlblRangePath = "c:extLst/c:ext/c15:datalabelsRange";
+
///
/// Literals for the Y serie, if the literal values are numeric
///
- public override double[] NumberLiteralsY
- {
- get
+ public override double[] NumberLiteralsY
+ {
+ get
{
- if(string.IsNullOrEmpty(_seriesNumLitPath) == false && GetNode(_seriesNumLitPath) != null)
+ if (string.IsNullOrEmpty(_seriesNumLitPath) == false && GetNode(_seriesNumLitPath) != null)
{
ReadNumLiterals(_seriesNumLitPath, out _NumberLiteralsY);
return _NumberLiteralsY;
}
return _NumberLiteralsY;
- }
+ }
protected set
{
_NumberLiteralsY = value;
@@ -56,7 +63,7 @@ protected set
///
/// Literals for the X serie, if the literal values are numeric
///
- public override double[] NumberLiteralsX
+ public override double[] NumberLiteralsX
{
get
{
@@ -112,6 +119,8 @@ protected set
}
}
+ internal ChartDataSource DataLabelRangeSource = null;
+
///
/// Default constructor
///
@@ -121,26 +130,27 @@ protected set
/// Is pivotchart
internal ExcelChartStandardSerie(ExcelChart chart, XmlNamespaceManager ns, XmlNode node, bool isPivot)
: base(chart, ns, node)
- {
- _chart = chart;
- _isPivot = isPivot;
- SchemaNodeOrder = new string[] { "idx", "order", "tx", "spPr", "marker", "invertIfNegative", "pictureOptions", "explosion", "dPt", "dLbls", "trendline","errBars", "cat", "val", "xVal", "yVal", "smooth","shape", "bubbleSize", "bubble3D", "numRef", "numLit", "strRef", "strLit", "formatCode", "ptCount", "pt" };
-
- if (_chart.ChartNode.LocalName=="scatterChart" ||
- _chart.ChartNode.LocalName.StartsWith("bubble", StringComparison.OrdinalIgnoreCase))
- {
- _seriesTopPath = "c:yVal";
- _xSeriesTopPath = "c:xVal";
- }
- else
- {
- _seriesTopPath = "c:val";
- _xSeriesTopPath = "c:cat";
- }
-
- _seriesPath = string.Format(_seriesPath, _seriesTopPath);
- _numCachePath = string.Format(_numCachePath, _seriesTopPath);
+ {
+ _chart = chart;
+ _isPivot = isPivot;
+ SchemaNodeOrder = new string[] { "idx", "order", "tx", "spPr", "marker", "invertIfNegative", "pictureOptions", "explosion", "dPt", "dLbls", "trendline", "errBars", "cat", "val", "xVal", "yVal", "smooth", "shape", "bubbleSize", "bubble3D", "numRef", "numLit", "strRef", "strLit", "formatCode", "ptCount", "pt" };
+ if (_chart.ChartNode.LocalName == "scatterChart" ||
+ _chart.ChartNode.LocalName.StartsWith("bubble", StringComparison.OrdinalIgnoreCase))
+ {
+ _seriesTopPath = "c:yVal";
+ _xSeriesTopPath = "c:xVal";
+ }
+ else
+ {
+ _seriesTopPath = "c:val";
+ _xSeriesTopPath = "c:cat";
+ }
+
+ _seriesPath = string.Format(_seriesPath, _seriesTopPath);
+ _numCachePath = string.Format(_numCachePath, _seriesTopPath);
+
+ //var XSeriesSource = new ChartDataSource(isPivot, ns, node, chart.WorkSheet);
var np = string.Format(_xSeriesParentPath, _xSeriesTopPath, isPivot ? "c:multiLvlStrRef" : "c:numRef");
var sp = string.Format(_xSeriesParentPath, _xSeriesTopPath, isPivot ? "c:multiLvlStrRef" : "c:strRef");
@@ -157,54 +167,147 @@ internal ExcelChartStandardSerie(ExcelChart chart, XmlNamespaceManager ns, XmlNo
_xSeriesStrLitPath = string.Format("{0}/c:strLit", _xSeriesTopPath);
_xSeriesNumLitPath = string.Format("{0}/c:numLit", _xSeriesTopPath);
- }
- internal override void SetID(string id)
- {
- SetXmlNodeString("c:idx/@val",id);
- SetXmlNodeString("c:order/@val", id);
- }
- const string headerPath="c:tx/c:v";
- ///
- /// Header for the serie.
- ///
- public override string Header
- {
- get
- {
+ }
+ internal override void SetID(string id)
+ {
+ SetXmlNodeString("c:idx/@val", id);
+ SetXmlNodeString("c:order/@val", id);
+ }
+
+ internal bool HasDataLabelRange()
+ {
+ return ExistsNode(dlblRangePath);
+ }
+
+ internal string GetDataLabelRange()
+ {
+ if(ExistsNode($"{dlblRangePath}/c15:f"))
+ {
+ return GetXmlNodeString($"{dlblRangePath}/c15:f");
+ }
+ return null;
+ }
+
+ internal void AddExtLstXml()
+ {
+ NameSpaceManager.AddNamespace("c15", ExcelPackage.schemaChart2012);
+ NameSpaceManager.AddNamespace("c16", ExcelPackage.schemaChart2014);
+
+ XmlElement ext15Node;
+
+ var c15Uri = "{02D57815-91ED-43cb-92C2-25804820EDAC}";
+
+ //Only add node if it doesn't already exist
+ if (ExistsNode(extPath + $"[@uri='{c15Uri}']") == false)
+ {
+ XmlElement el = (XmlElement)CreateNode($"{extPath}");
+ el.SetAttribute("xmlns:c15", ExcelPackage.schemaChart2012);
+ SetXmlNodeString($"{extPath}/@uri", $"{c15Uri}");
+ ext15Node = el;
+ }
+ else
+ {
+ ext15Node = (XmlElement)GetNode($"{extPath}");
+ }
+
+ //Only add node if it doesn't already exist
+ if (ExistsNode($"{extPath}[2]") == false)
+ {
+ XmlElement element = (XmlElement)CreateNode($"{extPath}", false, true);
+ element.SetAttribute("xmlns:c16", ExcelPackage.schemaChart2014);
+ SetXmlNodeString($"{extPath}[2]/@uri", "{C3380CC4-5D6E-409C-BE32-E72D297353CC}");
+ var _guidId = Guid.NewGuid();
+
+ var extNode2 = GetNode($"{extPath}[2]");
+ var uniqueIdNode = (XmlElement)CreateNode(extNode2, "c16:uniqueID");
+ uniqueIdNode.SetAttribute("val", $"{{{_guidId}}}");
+ }
+ }
+
+ internal string DataLabelSerie
+ {
+ get
+ {
+ if(DataLabelRangeSource == null)
+ {
+ AddExtLstXml();
+ var datalabelsRange = CreateNode(dlblRangePath);
+ DataLabelRangeSource = new ChartDataSource(_isPivot, NameSpaceManager, TopNode, _chart.WorkSheet, extPath);
+ if(GetDataLabelRange() != null)
+ {
+ DataLabelRangeSource.SetStrRef(GetDataLabelRange());
+ }
+ else
+ {
+ return null;
+ }
+ }
+
+ return DataLabelRangeSource.StrRefFormulaValue;
+ }
+ set
+ {
+ DataLabelRangeSource.SetStrRef(value);
+ }
+ }
+
+ internal string[] GetDataLabelLiterals()
+ {
+ if(string.IsNullOrEmpty(DataLabelSerie) == false)
+ {
+ return DataLabelRangeSource.StringLiterals;
+ }
+ return null;
+ }
+
+ internal void SetDataLabelRange(string strRef)
+ {
+ var dlblSer = DataLabelSerie;
+ DataLabelSerie = strRef;
+ }
+
+ const string headerPath = "c:tx/c:v";
+ ///
+ /// Header for the serie.
+ ///
+ public override string Header
+ {
+ get
+ {
return GetXmlNodeString(headerPath);
}
set
{
Cleartx();
- SetXmlNodeString(headerPath, value);
+ SetXmlNodeString(headerPath, value);
}
}
- private void Cleartx()
- {
- var n = TopNode.SelectSingleNode("c:tx", NameSpaceManager);
- if (n != null)
- {
- n.InnerXml = "";
- }
- }
- const string headerAddressPath = "c:tx/c:strRef/c:f";
+ private void Cleartx()
+ {
+ var n = TopNode.SelectSingleNode("c:tx", NameSpaceManager);
+ if (n != null)
+ {
+ n.InnerXml = "";
+ }
+ }
+ const string headerAddressPath = "c:tx/c:strRef/c:f";
///
- /// Header address for the serie.
- ///
- public override ExcelAddressBase HeaderAddress
- {
- get
- {
- string address = GetXmlNodeString(headerAddressPath);
- if (address == "")
- {
- return null;
- }
- else
- {
- return new ExcelAddressBase(address);
- }
+ /// Header address for the serie.
+ ///
+ public override ExcelAddressBase HeaderAddress
+ {
+ get
+ {
+ string address = GetXmlNodeString(headerAddressPath);
+ if (address == "")
+ {
+ return null;
+ }
+ else
+ {
+ return new ExcelAddressBase(address);
+ }
}
set
{
@@ -217,7 +320,30 @@ public override ExcelAddressBase HeaderAddress
SetXmlNodeString(headerAddressPath, ExcelCellBase.GetFullAddress(value.WorkSheetName, value.Address));
SetXmlNodeString("c:tx/c:strRef/c:strCache/c:ptCount/@val", "0");
}
- }
+ }
+
+ internal string GetHeaderString()
+ {
+ if(string.IsNullOrEmpty(Header))
+ {
+ if(HeaderAddress == null)
+ {
+ return null;
+ }
+
+ var ws = string.IsNullOrEmpty(HeaderAddress.WorkSheetName) ? _chart.WorkSheet : _chart.WorkSheet.Workbook.Worksheets[HeaderAddress.WorkSheetName];
+ if (ws == null) //Worksheet does not exist, exit
+ {
+ return null;
+ }
+ return ws.Cells[HeaderAddress.Address].Text;
+ }
+ else
+ {
+ return Header;
+ }
+ }
+
string _seriesTopPath;
string _seriesPath = "{0}/c:numRef/c:f";
string _numCachePath = "{0}/c:numRef/c:numCache";
@@ -227,18 +353,18 @@ public override ExcelAddressBase HeaderAddress
///
public override string Series
{
- get
- {
- return GetXmlNodeString(_seriesPath);
- }
- set
- {
+ get
+ {
+ return GetXmlNodeString(_seriesPath);
+ }
+ set
+ {
value = value.Trim();
if (value.StartsWith("=", StringComparison.OrdinalIgnoreCase)) value = value.Substring(1);
if (value.StartsWith("{", StringComparison.OrdinalIgnoreCase) && value.EndsWith("}", StringComparison.OrdinalIgnoreCase))
{
GetLitValues(value, out double[] numLit, out string[] strLit);
- if(strLit!=null)
+ if (strLit != null)
{
throw (new ArgumentException("Value series can't contain strings"));
}
@@ -255,22 +381,22 @@ public override string Series
}
- string _xSeries=null;
- string _xSeriesTopPath;
- string _xSeriesParentPath = "{0}/{1}";
- string _xSeriesPath = "{0}/{1}/c:f";
- string _xSeriesStrLitPath, _xSeriesNumLitPath;
+ string _xSeries = null;
+ string _xSeriesTopPath;
+ string _xSeriesParentPath = "{0}/{1}";
+ string _xSeriesPath = "{0}/{1}/c:f";
+ string _xSeriesStrLitPath, _xSeriesNumLitPath;
///
/// Set an address for the horisontal labels
///
- public override string XSeries
- {
- get
- {
- return GetXmlNodeString(_xSeriesPath);
- }
- set
- {
+ public override string XSeries
+ {
+ get
+ {
+ return GetXmlNodeString(_xSeriesPath);
+ }
+ set
+ {
_xSeries = value.Trim();
if (_xSeries.StartsWith("=", StringComparison.OrdinalIgnoreCase)) _xSeries = _xSeries.Substring(1);
if (value.StartsWith("{", StringComparison.OrdinalIgnoreCase) && value.EndsWith("}", StringComparison.OrdinalIgnoreCase))
@@ -285,7 +411,7 @@ public override string XSeries
NumberLiteralsX = null;
StringLiteralsX = null;
CreateNode(_xSeriesPath, true);
- if(ExcelCellBase.IsValidAddress(_xSeries))
+ if (ExcelCellBase.IsValidAddress(_xSeries))
{
SetXmlNodeString(_xSeriesPath, ExcelCellBase.GetFullAddress(_chart.WorkSheet.Name, _xSeries));
}
@@ -296,7 +422,7 @@ public override string XSeries
SetXSerieFunction();
}
}
- }
+ }
private void ReadNumLiterals(string path, out double[] numberLiterals)
{
@@ -306,9 +432,9 @@ private void ReadNumLiterals(string path, out double[] numberLiterals)
foreach (XmlNode node in childNodes)
{
- if(node.NodeType==XmlNodeType.Element && node.LocalName == "pt")
+ if (node.NodeType == XmlNodeType.Element && node.LocalName == "pt")
{
- if(double.TryParse(node.InnerText, NumberStyles.Any, CultureInfo.InvariantCulture, out double numLit) == false)
+ if (double.TryParse(node.InnerText, NumberStyles.Any, CultureInfo.InvariantCulture, out double numLit) == false)
{
throw new InvalidDataException($"numberLiteral in xml node:'{node.Name}' in chart:'{_chart.Name}' with value:'{node.InnerText}' could not be parsed as double. Chart cannot be read.");
}
@@ -323,7 +449,7 @@ private void ReadStringLiterals(string path, out string[] stringLiterals)
var parentNode = GetNode(path);
List strLits = new();
- if(parentNode != null)
+ if (parentNode != null)
{
var childNodes = parentNode.ChildNodes;
@@ -465,12 +591,12 @@ private void SetXSerieFunction()
}
private void SetLits(double[] numLit, string[] strLit, string numLitPath, string strLitPath)
{
- if(strLit!=null)
+ if (strLit != null)
{
XmlNode lit = CreateNode(strLitPath);
SetLitArray(lit, strLit);
}
- else if(numLit!=null)
+ else if (numLit != null)
{
XmlNode lit = CreateNode(numLitPath);
SetLitArray(lit, numLit);
@@ -506,7 +632,7 @@ private void SetLitArray(XmlNode lit, string[] strLit)
{
//Remove previous child nodes
var previousPt = lit.SelectNodes("c:pt", NameSpaceManager);
- if(previousPt != null)
+ if (previousPt != null)
{
for (int i = 0; i < previousPt.Count; i++)
{
@@ -535,9 +661,9 @@ private void AddCount(XmlNode lit, int count)
}
ExcelChartTrendlineCollection _trendLines = null;
- ///
- /// Access to the trendline collection
- ///
+ ///
+ /// Access to the trendline collection
+ ///
public override ExcelChartTrendlineCollection TrendLines
{
get
@@ -556,7 +682,7 @@ public override int NumberOfItems
{
get
{
- if(ExcelCellBase.IsValidAddress(Series))
+ if (ExcelCellBase.IsValidAddress(Series))
{
var a = new ExcelAddressBase(Series);
return a.Rows;
@@ -574,16 +700,16 @@ public override int NumberOfItems
///
public void CreateCache()
{
- if (_isPivot) throw(new NotImplementedException("Cache for pivotcharts has not been implemented yet."));
+ if (_isPivot) throw (new NotImplementedException("Cache for pivotcharts has not been implemented yet."));
if (!string.IsNullOrEmpty(Series))
{
- if(new ExcelRangeBase(_chart.WorkSheet, Series).Columns > 1)
+ if (new ExcelRangeBase(_chart.WorkSheet, Series).Columns > 1)
{
throw (new InvalidOperationException("A serie cannot be multiple columns. Please add one serie per column to create a cache"));
}
var node = GetTopNode(Series, _seriesTopPath);
-
+
CreateCache(Series, node);
}
@@ -598,8 +724,14 @@ public void CreateCache()
CreateCache(XSeries, node);
}
+
+ if(!string.IsNullOrEmpty(DataLabelSerie))
+ {
+ var node = GetTopNode(DataLabelSerie, extPath);
+ DataLabelRangeSource.CreateCache(node);
+ }
}
- private void CreateCache(string address, XmlNode node)
+ internal void CreateCache(string address, XmlNode node)
{
//var ws = _chart.WorkSheet;
var wb = _chart.WorkSheet.Workbook;
@@ -633,7 +765,7 @@ private void CreateCache(string address, XmlNode node)
}
CreateCacheFromRange(node, ws.Cells[address]);
}
-
+
}
private void CreateCacheFromRange(XmlNode node, ExcelRangeBase range)
@@ -641,17 +773,27 @@ private void CreateCacheFromRange(XmlNode node, ExcelRangeBase range)
if (range == null) return;
var startRow = range._fromRow;
var items = 0;
- var cse = new CellStoreEnumerator(range.Worksheet._values, startRow,range._fromCol, range._toRow, range._toCol);
+ var cse = new CellStoreEnumerator(range.Worksheet._values, startRow, range._fromCol, range._toRow, range._toCol);
while (cse.Next())
{
var v = cse.Value._value;
if (v != null)
{
- var d = Utils.TypeConversion.ConvertUtil.GetValueDouble(v);
+ string xmlValue = "";
+ if (v.IsNumeric())
+ {
+ var d = Utils.TypeConversion.ConvertUtil.GetValueDouble(v);
+ xmlValue = Utils.TypeConversion.ConvertUtil.GetValueForXml(d, range.Worksheet.Workbook.Date1904);
+ }
+ else
+ {
+ xmlValue = string.Format(CultureInfo.InvariantCulture, v.ToString());
+ }
+
var ptNode = node.OwnerDocument.CreateElement("c", "pt", ExcelPackage.schemaChart);
node.AppendChild(ptNode);
ptNode.SetAttribute("idx", (cse.Row - startRow).ToString(CultureInfo.InvariantCulture));
- ptNode.InnerXml = $"{Utils.TypeConversion.ConvertUtil.GetValueForXml(d, range.Worksheet.Workbook.Date1904)} ";
+ ptNode.InnerXml = $"{xmlValue} ";
items++;
}
}
@@ -700,10 +842,10 @@ private XmlNode GetTopNode(string address, string seriesTopPath)
if (addr.IsExternal)
{
var erIx = wb.ExternalLinks.GetExternalLink(addr._wb);
- if(erIx>=0)
+ if (erIx >= 0)
{
var er = wb.ExternalLinks[erIx].As.ExternalWorkbook;
- if(er.Package!=null)
+ if (er.Package != null)
{
var ws = er.Package.Workbook.Worksheets[addr.WorkSheetName];
var range = ws.Cells[addr.LocalAddress];
@@ -712,7 +854,7 @@ private XmlNode GetTopNode(string address, string seriesTopPath)
else
{
var ws = er.CachedWorksheets[addr.WorkSheetName];
- if(ws==null)
+ if (ws == null)
{
v = null;
}
@@ -728,7 +870,7 @@ private XmlNode GetTopNode(string address, string seriesTopPath)
v = null;
}
}
- else
+ else
{
ExcelWorksheet ws;
if (string.IsNullOrEmpty(addr.WorkSheetName))
@@ -752,22 +894,22 @@ private XmlNode GetTopNode(string address, string seriesTopPath)
string cachePath;
bool isNum;
- if(Utils.TypeConversion.ConvertUtil.IsNumericOrDate(v) || v is null)
+ if (Utils.TypeConversion.ConvertUtil.IsNumericOrDate(v) || v is null)
{
cachePath = string.Format("{0}/c:numRef/c:numCache", seriesTopPath);
isNum = true;
}
else
{
- cachePath=string.Format("{0}/c:strRef/c:strCache", seriesTopPath);
+ cachePath = string.Format("{0}/c:strRef/c:strCache", seriesTopPath);
isNum = false;
}
var node = CreateNode(cachePath);
if (node.HasChildNodes)
{
- if(isNum)
+ if (isNum)
{
- if(node.FirstChild.LocalName== "formatCode")
+ if (node.FirstChild.LocalName == "formatCode")
{
node.InnerXml = node.FirstChild.OuterXml;
}
@@ -778,7 +920,7 @@ private XmlNode GetTopNode(string address, string seriesTopPath)
}
else
{
- node.InnerXml = "";
+ node.InnerXml = "";
}
}
CreateNode($"{cachePath}/c:ptCount");
@@ -796,14 +938,14 @@ internal static XmlElement CreateSerieElement(ExcelChart chart)
//If the chart is added from a chart template, then use the chart templates series xml
if (chart._drawings._seriesTemplateXml != null)
{
- if(chart._drawings._seriesTemplateXml.Count != 0)
+ if (chart._drawings._seriesTemplateXml.Count != 0)
{
ser.InnerXml = chart._drawings._seriesTemplateXml[0];
return ser;
}
}
- int idx = FindIndex(chart._topChart??chart);
+ int idx = FindIndex(chart._topChart ?? chart);
ser.InnerXml = string.Format(" {2}{5}{0}{3}{4}", AddExplosion(chart.ChartType), idx, AddSpPrAndScatterPoint(chart.ChartType), AddAxisNodes(chart.ChartType), AddSmooth(chart.ChartType), AddMarker(chart.ChartType));
return ser;
}
diff --git a/src/EPPlus/Drawing/Chart/ExcelChartTitle.cs b/src/EPPlus/Drawing/Chart/ExcelChartTitle.cs
index 5cc46e61b5..1d41c5b709 100644
--- a/src/EPPlus/Drawing/Chart/ExcelChartTitle.cs
+++ b/src/EPPlus/Drawing/Chart/ExcelChartTitle.cs
@@ -18,6 +18,7 @@ Date Author Change
using OfficeOpenXml.Style;
using System;
using System.Collections.Generic;
+using System.Net.Security;
using System.Text;
using System.Xml;
@@ -56,7 +57,6 @@ internal ExcelChartTitle(ExcelChart chart, XmlNamespaceManager nameSpaceManager,
chart.ApplyStyleOnPart(this, chart.StyleManager?.Style?.Title, true);
}
}
-
}
private void CreateTopNode()
@@ -148,7 +148,11 @@ public ExcelTextBody TextBody
{
if (_textBody == null)
{
+ //var defBody = DefaultTextBody;
+ //var firstDefaultRunProperties = DefaultTextBody.Paragraphs.CreateOrGetDefaultRunProperties($"{_defTxBodyPath}/a:bodyPr/a:p/a:pPr/a:defRPr", TopNode);
_textBody = new ExcelTextBody(_chart, NameSpaceManager, TopNode, $"{_richTextPath}/a:bodyPr", SchemaNodeOrder);
+ //_textBody.Paragraphs.FirstDefaultRunProperties = firstDefaultRunProperties;
+ //_textBody.Paragraphs.FirstDefaultRunProperties = DefaultTextBody.Paragraphs.CreateOrGetDefaultRunProperties();
}
return _textBody;
}
@@ -245,7 +249,7 @@ internal void CreateRichText()
defFont = Convert.ToSingle(stylePart.DefaultTextRun.Size);
}
var tb = TextBody;
- _richText = new ExcelParagraphCollection(tb, _chart, NameSpaceManager, TopNode, $"{_richTextPath}/a:p", SchemaNodeOrder, defFont);
+ _richText = new ExcelParagraphCollection(tb, _chart, NameSpaceManager, TopNode, $"{_richTextPath}/a:p", SchemaNodeOrder, defFont, eTextAlignment.Center);
}
private ExcelChartStyleEntry GetStylePart()
@@ -340,7 +344,7 @@ public double Rotation
{
get
{
- var i=GetXmlNodeInt($"{_fontPropertiesPath}/a:bodyPr/@rot");
+ var i=GetXmlNodeInt($"{_fontPropertiesPath}/a:bodyPr/@rot", 0);
if (i < 0)
{
return 360 + (i / 60000);
@@ -382,6 +386,12 @@ void IStyleMandatoryProperties.SetMandatoryProperties()
if (Font.Kerning == 0) Font.Kerning = 12;
Font.Bold = Font.Bold; //Must be set
+ //Textbody cannot exist without a paragraph node
+ if(TextBody.Paragraphs.Count == 0)
+ {
+ TextBody.Paragraphs.Add("Title");
+ }
+
CreatespPrNode($"{_nsPrefix}:spPr");
}
}
@@ -406,6 +416,16 @@ public override string Text
}
else
{
+ if(LinkedCell.IsSingleCell == false)
+ {
+ string combinedString = "";
+ string separator = " ";
+ foreach(var address in LinkedCell)
+ {
+ combinedString += address.Text + separator;
+ }
+ return combinedString;
+ }
return LinkedCell.Text;
}
}
@@ -423,6 +443,44 @@ public override string Text
}
}
///
+ /// The text adjusted for the Capitalization property.
+ ///
+ public string DisplayedText
+ {
+ get
+ {
+ if (LinkedCell == null)
+ {
+ return RichText.DisplayedText;
+ }
+ else
+ {
+ if (LinkedCell.IsSingleCell == false)
+ {
+ string combinedString = "";
+ string separator = " ";
+ foreach (var address in LinkedCell)
+ {
+ combinedString += address.Text + separator;
+ }
+ }
+ return LinkedCell.Text;
+ }
+ }
+ set
+ {
+ if (RichText == null)
+ {
+ LinkedCell = null;
+ CreateRichText();
+ }
+ var applyStyle = (RichText.Count == 0);
+ RichText.Text = value;
+ _font = null;
+ if (applyStyle) _chart.ApplyStyleOnPart(this, _chart.StyleManager?.Style?.Title, true);
+ }
+ }
+ ///
/// A reference to a cell used as the title text
///
public ExcelRangeBase LinkedCell
diff --git a/src/EPPlus/Drawing/Chart/ExcelLineChartSerie.cs b/src/EPPlus/Drawing/Chart/ExcelLineChartSerie.cs
index f1e5f8ad67..d5f78979d6 100644
--- a/src/EPPlus/Drawing/Chart/ExcelLineChartSerie.cs
+++ b/src/EPPlus/Drawing/Chart/ExcelLineChartSerie.cs
@@ -10,13 +10,14 @@ Date Author Change
*************************************************************************************************
01/27/2020 EPPlus Software AB Initial release EPPlus 5
*************************************************************************************************/
+using OfficeOpenXml.Drawing.Interfaces;
using System;
using System.Collections.Generic;
+using System.Data;
+using System.Drawing;
using System.Globalization;
using System.Text;
using System.Xml;
-using System.Drawing;
-using OfficeOpenXml.Drawing.Interfaces;
namespace OfficeOpenXml.Drawing.Chart
{
diff --git a/src/EPPlus/Drawing/Chart/Style/ExcelChartStyleEntry.cs b/src/EPPlus/Drawing/Chart/Style/ExcelChartStyleEntry.cs
index 3de9cc2662..9e6bafd5be 100644
--- a/src/EPPlus/Drawing/Chart/Style/ExcelChartStyleEntry.cs
+++ b/src/EPPlus/Drawing/Chart/Style/ExcelChartStyleEntry.cs
@@ -27,7 +27,7 @@ namespace OfficeOpenXml.Drawing.Chart.Style
public class ExcelChartStyleEntry : XmlHelper
{
string _fillReferencePath = "{0}/{1}:fillRef";
- string _borderReferencePath = "{0}/{1}:lnRef ";
+ string _borderReferencePath = "{0}/{1}:lnRef";
string _effectReferencePath = "{0}/{1}:effectRef";
string _fontReferencePath = "{0}/{1}:fontRef";
diff --git a/src/EPPlus/Drawing/Chart/eAxisLabelAlignment.cs b/src/EPPlus/Drawing/Chart/eAxisLabelAlignment.cs
new file mode 100644
index 0000000000..7ecf91a743
--- /dev/null
+++ b/src/EPPlus/Drawing/Chart/eAxisLabelAlignment.cs
@@ -0,0 +1,33 @@
+/*************************************************************************************************
+ Required Notice: Copyright (C) EPPlus Software AB.
+ This software is licensed under PolyForm Noncommercial License 1.0.0
+ and may only be used for noncommercial purposes
+ https://polyformproject.org/licenses/noncommercial/1.0.0/
+
+ A commercial license to use this software can be purchased at https://epplussoftware.com
+ *************************************************************************************************
+ Date Author Change
+ *************************************************************************************************
+ 04/22/2020 EPPlus Software AB Added this class
+ *************************************************************************************************/
+namespace OfficeOpenXml
+{
+ ///
+ /// How the axis label should be alignted within the major tickmarks.
+ ///
+ public enum eAxisLabelAlignment
+ {
+ ///
+ /// The text shall be centered
+ ///
+ Left,
+ ///
+ /// The text shall be left justified.
+ ///
+ Center,
+ ///
+ /// The text shall be right justified.
+ ///
+ Right
+ }
+}
\ No newline at end of file
diff --git a/src/EPPlus/Drawing/Controls/ExcelControl.cs b/src/EPPlus/Drawing/Controls/ExcelControl.cs
index eb9d511051..79aae7407b 100644
--- a/src/EPPlus/Drawing/Controls/ExcelControl.cs
+++ b/src/EPPlus/Drawing/Controls/ExcelControl.cs
@@ -637,7 +637,7 @@ internal virtual void UpdateXml()
{
fill.Color = color;
}
- fill.Transparancy = (int)c.Fill.Opacity - 100;
+ fill.Transparency = (int)c.Fill.Opacity - 100;
}
}
}
diff --git a/src/EPPlus/Drawing/Enums/EnumTransl.cs b/src/EPPlus/Drawing/Enums/EnumTransl.cs
index d1c66ff1a5..6bc27dcb3c 100644
--- a/src/EPPlus/Drawing/Enums/EnumTransl.cs
+++ b/src/EPPlus/Drawing/Enums/EnumTransl.cs
@@ -94,34 +94,34 @@ internal static eLineCap ToLineCap(string text)
return eLineCap.Flat;
}
}
- internal static eCompundLineStyle ToLineCompound(string s)
+ internal static eCompoundLineStyle ToLineCompound(string s)
{
switch (s)
{
case "dbl":
- return eCompundLineStyle.Double;
- case "sng":
- return eCompundLineStyle.Single;
+ return eCompoundLineStyle.Double;
case "thickThin":
- return eCompundLineStyle.DoubleThickThin;
+ return eCompoundLineStyle.DoubleThickThin;
case "thinThick":
- return eCompundLineStyle.DoubleThinThick;
+ return eCompoundLineStyle.DoubleThinThick;
+ case "tri":
+ return eCompoundLineStyle.TripleThinThickThin;
default:
- return eCompundLineStyle.TripleThinThickThin;
+ return eCompoundLineStyle.Single;
}
}
- internal static string FromLineCompound(eCompundLineStyle v)
+ internal static string FromLineCompound(eCompoundLineStyle v)
{
switch (v)
{
- case eCompundLineStyle.Double:
+ case eCompoundLineStyle.Double:
return "dbl";
- case eCompundLineStyle.Single:
+ case eCompoundLineStyle.Single:
return "sng";
- case eCompundLineStyle.DoubleThickThin:
+ case eCompoundLineStyle.DoubleThickThin:
return "thickThin";
- case eCompundLineStyle.DoubleThinThick:
+ case eCompoundLineStyle.DoubleThinThick:
return "thinThick";
default:
return "tri";
diff --git a/src/EPPlus/Drawing/Enums/eCompundLineStyle.cs b/src/EPPlus/Drawing/Enums/eCompoundLineStyle.cs
similarity index 97%
rename from src/EPPlus/Drawing/Enums/eCompundLineStyle.cs
rename to src/EPPlus/Drawing/Enums/eCompoundLineStyle.cs
index 468ec03c2b..21d71d19cc 100644
--- a/src/EPPlus/Drawing/Enums/eCompundLineStyle.cs
+++ b/src/EPPlus/Drawing/Enums/eCompoundLineStyle.cs
@@ -15,7 +15,7 @@ namespace OfficeOpenXml.Drawing
///
/// The compound line type. Used for underlining text
///
- public enum eCompundLineStyle
+ public enum eCompoundLineStyle
{
///
/// Double lines with equal width
diff --git a/src/EPPlus/Drawing/ExcelDrawing.cs b/src/EPPlus/Drawing/ExcelDrawing.cs
index 0d632dbfd0..c7abe03133 100644
--- a/src/EPPlus/Drawing/ExcelDrawing.cs
+++ b/src/EPPlus/Drawing/ExcelDrawing.cs
@@ -2846,7 +2846,7 @@ private XmlNode CopyShape(ExcelChartStandard targetChart, bool isGroupShape = fa
private XmlNode CopyShape(ExcelWorksheet worksheet, bool isGroupShape = false, XmlNode groupDrawNode = null)
{
- var sourceShape = this as ExcelShape;
+ var sourceShape = this as ExcelShapeBase;
XmlNode drawNode = null;
if (isGroupShape && groupDrawNode != null)
{
@@ -2860,7 +2860,7 @@ private XmlNode CopyShape(ExcelWorksheet worksheet, bool isGroupShape = false, X
drawNode = worksheet.Drawings.CreateDocumentAndTopNode(CellAnchor, false);
drawNode.InnerXml = TopNode.InnerXml;
//Asign new id
- var targetShape = GetDrawing(worksheet._drawings, drawNode) as ExcelShape;
+ var targetShape = GetDrawing(worksheet._drawings, drawNode) as ExcelShapeBase;
targetShape.Id = ++worksheet.Drawings._nextDrawingId;
targetShape.Name = worksheet._drawings.GetUniqueDrawingName(sourceShape.Name);
}
diff --git a/src/EPPlus/Drawing/ExcelDrawingBorder.cs b/src/EPPlus/Drawing/ExcelDrawingBorder.cs
index fc3741bd84..f9c749bfed 100644
--- a/src/EPPlus/Drawing/ExcelDrawingBorder.cs
+++ b/src/EPPlus/Drawing/ExcelDrawingBorder.cs
@@ -110,7 +110,7 @@ private void InitSpPr()
///
/// The compound line type that is to be used for lines with text such as underlines
///
- public eCompundLineStyle CompoundLineStyle
+ public eCompoundLineStyle CompoundLineStyle
{
get
{
diff --git a/src/EPPlus/Drawing/ExcelDrawingFillBasic.cs b/src/EPPlus/Drawing/ExcelDrawingFillBasic.cs
index 23256b3958..da9f1d1a7b 100644
--- a/src/EPPlus/Drawing/ExcelDrawingFillBasic.cs
+++ b/src/EPPlus/Drawing/ExcelDrawingFillBasic.cs
@@ -303,10 +303,10 @@ public ExcelDrawingGradientFill GradientFill
}
}
///
- /// Transparancy in percent from a solid fill.
+ /// Transparency in percent from a solid fill.
/// This is the same as 100-Fill.Transform.Alpha
///
- public int Transparancy
+ public int Transparency
{
get
{
diff --git a/src/EPPlus/Drawing/ExcelImageBase.cs b/src/EPPlus/Drawing/ExcelImageBase.cs
index bb813d0a18..7abe2c57f1 100644
--- a/src/EPPlus/Drawing/ExcelImageBase.cs
+++ b/src/EPPlus/Drawing/ExcelImageBase.cs
@@ -182,6 +182,7 @@ internal void SetImageNoContainer(byte[] image, ePictureType pictureType)
{
ImageBytes = image;
}
+
using (var ms = EPPlusMemoryManager.GetStream(image))
{
var imageHandler = new GenericImageHandler();
diff --git a/src/EPPlus/Drawing/ImageHandling/GenericImageHandler.cs b/src/EPPlus/Drawing/ImageHandling/GenericImageHandler.cs
index 2951c07063..9f04ef981d 100644
--- a/src/EPPlus/Drawing/ImageHandling/GenericImageHandler.cs
+++ b/src/EPPlus/Drawing/ImageHandling/GenericImageHandler.cs
@@ -38,6 +38,7 @@ public HashSet SupportedTypes
public bool GetImageBounds(MemoryStream image, ePictureType type, out double width, out double height, out double horizontalResolution, out double verticalResolution)
{
if (image.Length == 0) throw new InvalidDataException("The Image has zero-length");
+
try
{
width = 0;
diff --git a/src/EPPlus/Drawing/ImageHandling/ImageReader.cs b/src/EPPlus/Drawing/ImageHandling/ImageReader.cs
index 0389431eb3..a764e55350 100644
--- a/src/EPPlus/Drawing/ImageHandling/ImageReader.cs
+++ b/src/EPPlus/Drawing/ImageHandling/ImageReader.cs
@@ -293,7 +293,6 @@ private static bool IsJpg(MemoryStream ms, ref double width, ref double height,
var precision = br.ReadByte(); //Bits
height = GetUInt16BigEndian(br);
width = GetUInt16BigEndian(br);
- br.Close();
return true;
case 0xFFD9:
return height != 0 && width != 0;
@@ -320,7 +319,6 @@ private static bool IsGif(MemoryStream ms, ref double width, ref double height)
{
width = br.ReadUInt16();
height = br.ReadUInt16();
- br.Close();
return true;
}
return false;
@@ -401,10 +399,8 @@ private static bool IsIcon(MemoryStream ms, ref double width, ref double height)
// IsPng(br, ref width, ref height, offset+fileSize);
//}
- br.Close();
return true;
}
- br.Close();
return false;
}
internal static bool IsIco(BinaryReader br)
@@ -734,10 +730,8 @@ private static bool IsPng(BinaryReader br, ref double width, ref double height,
verticalResolution /= M_TO_INCH;
}
- br.Close();
return true;
case "IEND":
- br.Close();
return width != 0 && height != 0;
default:
br.ReadBytes(length);
@@ -746,7 +740,6 @@ private static bool IsPng(BinaryReader br, ref double width, ref double height,
var crc = br.ReadInt32();
}
}
- br.Close();
return width != 0 && height != 0;
}
private static bool IsPng(BinaryReader br)
@@ -784,7 +777,6 @@ private static bool IsSvg(MemoryStream ms, ref double width, ref double height)
var w = reader.GetAttribute("width");
var h = reader.GetAttribute("height");
var vb = reader.GetAttribute("viewBox");
- reader.Close();
if (w == null || h == null)
{
if (vb == null)
diff --git a/src/EPPlus/Drawing/Style/Text/ExcelDrawingParagraph.cs b/src/EPPlus/Drawing/Style/Text/ExcelDrawingParagraph.cs
index fc298a44b8..fcf75f0316 100644
--- a/src/EPPlus/Drawing/Style/Text/ExcelDrawingParagraph.cs
+++ b/src/EPPlus/Drawing/Style/Text/ExcelDrawingParagraph.cs
@@ -56,7 +56,7 @@ internal ExcelDrawingParagraph(ExcelDrawingParagraphCollection paragraphs, IPict
//////Previously new paragraphs used the first DefaultRunProperties
//////Uncertain if we should keep this behaviour at least as an option. TODO: Decide if breaking change or legacy setting (or keep only previous paragraph's settings?)
- bool legacyDefaultRunPropertySetting = true;
+ bool legacyDefaultRunPropertySetting = false;
if (paragraphs.Count == 0)
{
@@ -127,6 +127,9 @@ public string Text
return sb.ToString();
}
}
+
+
+ internal eTextAlignment defaultAlignment = eTextAlignment.Left;
///
/// Horizontal Alignment
///
@@ -134,7 +137,7 @@ public eTextAlignment HorizontalAlignment
{
get
{
- return GetXmlNodeString("a:pPr/@algn").ToEnum(eTextAlignment.Left, new Dictionary
+ return GetXmlNodeString("a:pPr/@algn").ToEnum(defaultAlignment, new Dictionary
{
["r"] = eTextAlignment.Right,
["ctr"] = eTextAlignment.Center,
diff --git a/src/EPPlus/Drawing/Style/Text/ExcelParagraphTextRunBase.cs b/src/EPPlus/Drawing/Style/Text/ExcelParagraphTextRunBase.cs
index 63194313d3..4326858c78 100644
--- a/src/EPPlus/Drawing/Style/Text/ExcelParagraphTextRunBase.cs
+++ b/src/EPPlus/Drawing/Style/Text/ExcelParagraphTextRunBase.cs
@@ -409,9 +409,9 @@ public eTextCapsType Capitalization
get
{
var v = GetXmlNodeString(_capPath);
- if(v==null)
+ if(string.IsNullOrEmpty(v))
{
- return _dtr.Capitalization;
+ return _dtr?.Capitalization ?? eTextCapsType.None;
}
else
{
diff --git a/src/EPPlus/Drawing/Style/Text/ExcelTextBody.cs b/src/EPPlus/Drawing/Style/Text/ExcelTextBody.cs
index 25e4c70889..5ee1c7bb0b 100644
--- a/src/EPPlus/Drawing/Style/Text/ExcelTextBody.cs
+++ b/src/EPPlus/Drawing/Style/Text/ExcelTextBody.cs
@@ -36,19 +36,16 @@ public class ExcelTextBody : XmlHelper
///
///
///
- internal ExcelTextBody(IPictureRelationDocument pictureRelationDocument, XmlNamespaceManager ns, XmlNode topNode, string path, string[] schemaNodeOrder=null) :
+ ///
+ internal ExcelTextBody(IPictureRelationDocument pictureRelationDocument, XmlNamespaceManager ns, XmlNode topNode, string path, string[] schemaNodeOrder=null, Action initXml=null) :
base(ns, topNode)
{
_pictureRelationDocument = pictureRelationDocument;
_path = path;
- _initXml = null;
+ _initXml = initXml;
AddSchemaNodeOrder(schemaNodeOrder, new string[] { "ln", "noFill", "solidFill", "gradFill", "pattFill", "blipFill", "latin", "ea", "cs", "sym", "hlinkClick", "hlinkMouseOver", "rtl", "extLst", "highlight", "kumimoji", "lang", "altLang", "sz", "b", "i", "u", "strike", "kern", "cap", "spc", "normalizeH", "baseline", "noProof", "dirty", "err", "smtClean", "smtId", "bmk" });
}
-
- string ctTextBodyPath;
-
-
///
/// The anchoring position within the shape
///
@@ -431,7 +428,7 @@ internal void SetFromXml(XmlElement copyFromElement)
}
}
- //Excel default values for Top/Bottom and Right/Left in EMU
+ //Excel default values for Top/Bottom and Right/Left translated to points
//They are equivalent to 0.25cm and 0.13cm
internal const double DefaultTopBot = 45720d / ExcelDrawing.EMU_PER_POINT;
internal const double DefaultRightLeft = 91440d / ExcelDrawing.EMU_PER_POINT;
@@ -439,16 +436,24 @@ internal void SetFromXml(XmlElement copyFromElement)
///
/// Get Insets in points
///
- ///
- ///
- ///
- ///
- internal void GetInsetsOrDefaults(out double Left, out double Top, out double Right, out double Bottom)
+ ///
+ ///
+ ///
+ ///
+ internal void GetInsetsOrDefaults(out double left, out double top, out double right, out double bottom)
+ {
+ left = LeftInsert ?? DefaultRightLeft;
+ top = TopInsert ?? DefaultRightLeft;
+ right = RightInsert ?? DefaultTopBot;
+ bottom = BottomInsert ?? DefaultTopBot;
+ }
+
+ internal void GetInsetsInPoints(out double left, out double top, out double right, out double bottom)
{
- Left = LeftInsert ?? DefaultRightLeft;
- Top = TopInsert ?? DefaultRightLeft;
- Right = RightInsert ?? DefaultTopBot;
- Bottom = BottomInsert ?? DefaultTopBot;
+ left = (LeftInsert ?? 0);
+ top = (TopInsert ?? 0);
+ right = (RightInsert ?? 0);
+ bottom = (BottomInsert ?? 0);
}
ExcelDrawingParagraphCollection _paragraphs = null;
diff --git a/src/EPPlus/Drawing/Theme/ExcelThemeLine.cs b/src/EPPlus/Drawing/Theme/ExcelThemeLine.cs
index 843488b07e..8fa764b019 100644
--- a/src/EPPlus/Drawing/Theme/ExcelThemeLine.cs
+++ b/src/EPPlus/Drawing/Theme/ExcelThemeLine.cs
@@ -69,7 +69,7 @@ public eLineCap Cap
///
/// The compound line type to be used for the underline stroke
///
- public eCompundLineStyle CompoundLineStyle
+ public eCompoundLineStyle CompoundLineStyle
{
get
{
diff --git a/src/EPPlus/EPPlus.csproj b/src/EPPlus/EPPlus.csproj
index 1f3a9e97ae..4e50327dce 100644
--- a/src/EPPlus/EPPlus.csproj
+++ b/src/EPPlus/EPPlus.csproj
@@ -1,9 +1,9 @@

net8.0;net9.0;net10.0;netstandard2.1;netstandard2.0;net462;net35
- 8.4.2.0
- 8.4.2.0
- 8.4.2
+ 8.5.0.0
+ 8.5.0.0
+ 8.5.0
true
https://epplussoftware.com
EPPlus Software AB
@@ -18,7 +18,7 @@
readme.md
EPPlus Software AB
- EPPlus 8.4.2
+ EPPlus 8.5.0
IMPORTANT NOTICE!
From version 5 EPPlus changes the license model using a dual license, Polyform Non Commercial / Commercial license.
@@ -26,6 +26,10 @@
Commercial licenses can be purchased from https://epplussoftware.com
This applies to EPPlus version 5 and later. Earlier versions are still licensed LGPL.
+ ## Version 8.5.0
+ *
+ * Minor bug fixes. See https://epplussoftware.com/Developers/MinorFeaturesAndIssues
+
## Version 8.4.2
* Minor bug fixes. See https://epplussoftware.com/Developers/MinorFeaturesAndIssues
@@ -551,7 +555,8 @@
A list of fixed issues can be found here https://epplussoftware.com/docs/7.0/articles/fixedissues.html
Version history
- 8.4.2 20260204 Minor bug fixes. See https://epplussoftware.com/Developers/MinorFeaturesAndIssues
+ 8.5.0 20260306 Minor features and bug fixes. See https://epplussoftware.com/Developers/MinorFeaturesAndIssues
+ 8.4.2 20260204 Minor bug fixes.
8.4.1 20260112 Minor bug fixes.
8.4.0 20251212 Updated target frameworks. Minor bug fixes.
8.3.1 20251128 Minor bug fixes.
diff --git a/src/EPPlus/ExcelCalculationCacheSettings.cs b/src/EPPlus/ExcelCalculationCacheSettings.cs
new file mode 100644
index 0000000000..b527b26729
--- /dev/null
+++ b/src/EPPlus/ExcelCalculationCacheSettings.cs
@@ -0,0 +1,42 @@
+/*************************************************************************************************
+ Required Notice: Copyright (C) EPPlus Software AB.
+ This software is licensed under PolyForm Noncommercial License 1.0.0
+ and may only be used for noncommercial purposes
+ https://polyformproject.org/licenses/noncommercial/1.0.0/
+
+ A commercial license to use this software can be purchased at https://epplussoftware.com
+ *************************************************************************************************
+ Date Author Change
+ *************************************************************************************************
+ 02/10/2026 EPPlus Software AB Initial release
+ *************************************************************************************************/
+using OfficeOpenXml.FormulaParsing.Excel.Functions.MathFunctions;
+
+namespace OfficeOpenXml
+{
+ ///
+ /// Settings for formula calculation caching and optimization
+ ///
+ public class ExcelCalculationCacheSettings
+ {
+ ///
+ /// Maximum number of flattened ranges to cache for RangeCriteria functions (SUMIFS, AVERAGEIFS, COUNTIFS).
+ /// Default is 100. Set to 0 to disable flattened range caching.
+ ///
+ public int MaxFlattenedRanges { get; set; } = RangeCriteriaCache.DEFAULT_MAX_FLATTENED_RANGES;
+
+ ///
+ /// Maximum number of match indexes to cache for RangeCriteria functions (SUMIFS, AVERAGEIFS, COUNTIFS).
+ /// Default is 1000. Set to 0 to disable match index caching.
+ ///
+ public int MaxMatchIndexes { get; set; } = RangeCriteriaCache.DEFAULT_MAX_MATCH_INDEXES;
+
+ ///
+ /// Gets the default configuration for Excel calculation cache settings.
+ ///
+ /// Use this property to obtain a standard set of cache settings suitable for most
+ /// scenarios. The returned instance is immutable and can be shared safely across multiple components.
+ public static ExcelCalculationCacheSettings Default { get; } = new ExcelCalculationCacheSettings();
+
+ }
+}
diff --git a/src/EPPlus/ExcelPackage.cs b/src/EPPlus/ExcelPackage.cs
index b9fa1c9382..a6e323ec0c 100644
--- a/src/EPPlus/ExcelPackage.cs
+++ b/src/EPPlus/ExcelPackage.cs
@@ -953,6 +953,9 @@ public void Dispose()
public void Save()
{
CheckNotDisposed();
+#if !NET35
+ Workbook.ThrowIfCalculationCanceled();
+#endif
try
{
if (_stream is MemoryStream && _stream.Length > 0)
diff --git a/src/EPPlus/ExcelPackageSettings.cs b/src/EPPlus/ExcelPackageSettings.cs
index 9c1e569994..40022a6410 100644
--- a/src/EPPlus/ExcelPackageSettings.cs
+++ b/src/EPPlus/ExcelPackageSettings.cs
@@ -71,6 +71,23 @@ public ExcelImageSettings ImageSettings
return _imageSettings;
}
}
+
+ private ExcelCalculationCacheSettings _calculationCacheSettings = null;
+
+ ///
+ /// Cache settings for formula calculation optimization.
+ ///
+ public ExcelCalculationCacheSettings CalculationCacheSettings
+ {
+ get
+ {
+ if (_calculationCacheSettings == null)
+ {
+ _calculationCacheSettings = new ExcelCalculationCacheSettings();
+ }
+ return _calculationCacheSettings;
+ }
+ }
///
/// Any auto- or table- filters created will be applied on save.
/// In the case you want to handle this manually, set this property to false.
diff --git a/src/EPPlus/ExcelWorkbook.cs b/src/EPPlus/ExcelWorkbook.cs
index 276d191569..b285effb19 100644
--- a/src/EPPlus/ExcelWorkbook.cs
+++ b/src/EPPlus/ExcelWorkbook.cs
@@ -402,6 +402,37 @@ private void GetSharedStrings()
}
+ #region Calculation cancellation (poison flag)
+
+ #if !NET35
+ internal bool IsCalculationCanceled { get; private set; }
+
+ internal void MarkCalculationCanceled()
+ {
+ IsCalculationCanceled = true;
+ }
+
+ internal void ThrowIfCalculationCanceled()
+ {
+ if (IsCalculationCanceled)
+ {
+ throw new InvalidOperationException(
+ "This workbook has been left in an inconsistent state due to a canceled " +
+ "calculation. The workbook must be disposed and cannot be used for further " +
+ "operations. Reload the workbook from the source to continue.");
+ }
+ }
+
+ ///
+ /// Returns true if a calculation was canceled, leaving the workbook in an inconsistent state.
+ /// A workbook in this state must be disposed — saving or recalculating is not permitted.
+ ///
+ public bool IsCalculationInconsistent => IsCalculationCanceled;
+ #endif
+
+ #endregion
+
+
internal void GetDefinedNames()
{
XmlNodeList nl = WorkbookXml.SelectNodes("//d:definedNames/d:definedName", NameSpaceManager);
diff --git a/src/EPPlus/ExcelWorksheet.cs b/src/EPPlus/ExcelWorksheet.cs
index 1f87e7c527..f2ecc14dc0 100644
--- a/src/EPPlus/ExcelWorksheet.cs
+++ b/src/EPPlus/ExcelWorksheet.cs
@@ -15,9 +15,11 @@ Date Author Change
using OfficeOpenXml.Constants;
using OfficeOpenXml.Core;
using OfficeOpenXml.Core.CellStore;
+using OfficeOpenXml.Core.RangeQuadTree;
using OfficeOpenXml.Core.RichValues;
using OfficeOpenXml.Core.Worksheet;
using OfficeOpenXml.Core.Worksheet.XmlWriter;
+using OfficeOpenXml.Data.Connection.IOHandlers;
using OfficeOpenXml.Data.QueryTable;
using OfficeOpenXml.DataValidation;
using OfficeOpenXml.Drawing;
@@ -29,6 +31,7 @@ Date Author Change
using OfficeOpenXml.Packaging;
using OfficeOpenXml.Packaging.Ionic.Zip;
using OfficeOpenXml.RichData;
+using OfficeOpenXml.RichData.RichValues.WebImages;
using OfficeOpenXml.Sorting;
using OfficeOpenXml.Sparkline;
using OfficeOpenXml.Style;
@@ -48,9 +51,9 @@ Date Author Change
using System.Globalization;
using System.IO;
using System.Linq;
+using System.Security.Permissions;
using System.Text;
using System.Xml;
-using System.Security.Permissions;
namespace OfficeOpenXml
{
@@ -1625,7 +1628,7 @@ private void LoadCells(XmlReader xr)
style = 0;
SetStyleInner(address._fromRow, address._fromCol, style < 0 ? 0 : style);
}
- //Meta data. Meta data is only preserved by EPPlus at this point
+ //Cell metadata/Value metadata
var cm = xr.GetAttribute("cm");
var vm = xr.GetAttribute("vm");
if (cm != null || vm != null)
@@ -1666,36 +1669,14 @@ private void LoadCells(XmlReader xr)
}
else if (xr.LocalName == "v")
{
+
if (currentVm > 0)
{
- var rd = _richDataStore.GetRichValueByOneBasedIndex(Convert.ToInt32(currentVm));
- rd?.SetStructure(_package.Workbook.RichData.Db);
- if (rd != null && rd.Structure.StructureType == RichDataStructureTypes.LocalImage)
- {
- var rdLi = rd.As.LocalImage;
- var pic = new ExcelCellPicture(currentVm, rdLi.ImageUri, Workbook._package.PictureStore, ExcelCellPictureTypes.LocalImage)
- {
- CellAddress = new ExcelAddress(this.Name, row, col, row, col),
- AltText = rdLi.Text,
- CalcOrigin = rdLi.CalcOrigin ?? CalcOrigins.None
- };
- Workbook._package.PictureStore.AddImage(pic.GetImageBytes(), rdLi.ImageUri, null);
- SetValueInner(row, col, pic);
- while (!(xr.NodeType == XmlNodeType.EndElement && xr.LocalName == "c"))
- {
- xr.Read();
- }
- }
- else if (rd != null && rd.Structure.StructureType == RichDataStructureTypes.WebImage)
+ var richValue = _richDataStore.GetRichValueByOneBasedIndex(currentVm);
+ if (richValue != null && richValue.IsCellImage)
{
- var rdWi = rd.As.WebImage;
- var pic = new ExcelCellPicture(currentVm, rdWi.ImageUri, Workbook._package.PictureStore, ExcelCellPictureTypes.WebImage)
- {
- CellAddress = new ExcelAddress(this.Name, row, col, row, col),
- AltText = rdWi.Text,
- CalcOrigin = rdWi.CalcOrigin ?? CalcOrigins.None
- };
- Workbook._package.PictureStore.AddImage(pic.GetImageBytes(), rdWi.ImageUri, null);
+ var picManager = new CellPicturesManager(this);
+ var pic = picManager.BuildCellPicture(row, col, currentVm, richValue);
SetValueInner(row, col, pic);
while (!(xr.NodeType == XmlNodeType.EndElement && xr.LocalName == "c"))
{
diff --git a/src/EPPlus/Export/HtmlExport/CssCollections/CssChartRuleCollection.cs b/src/EPPlus/Export/HtmlExport/CssCollections/CssChartRuleCollection.cs
new file mode 100644
index 0000000000..453c7791e9
--- /dev/null
+++ b/src/EPPlus/Export/HtmlExport/CssCollections/CssChartRuleCollection.cs
@@ -0,0 +1,172 @@
+using OfficeOpenXml.Drawing;
+using OfficeOpenXml.Drawing.Chart;
+using OfficeOpenXml.Drawing.Chart.Style;
+using OfficeOpenXml.Drawing.Theme;
+using OfficeOpenXml.Export.HtmlExport.Exporters.Internal;
+using OfficeOpenXml.Export.HtmlExport.Settings;
+using OfficeOpenXml.Export.HtmlExport.StyleCollectors;
+using OfficeOpenXml.Export.HtmlExport.Translators;
+using OfficeOpenXml.Style;
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Runtime.InteropServices;
+using System.Text;
+
+namespace OfficeOpenXml.Export.HtmlExport.CssCollections
+{
+ internal class CssChartRuleCollection : CssRuleCollection
+ {
+ CssExportSettings _settings;
+ ExcelChart _chart;
+ ExcelTheme _theme;
+ TranslatorContext _context;
+
+ public CssChartRuleCollection(ExcelChart chart, CssExportSettings settings)
+ {
+ _chart = chart;
+ _settings = settings;
+
+ if (_chart.WorkSheet.Workbook.ThemeManager.CurrentTheme == null)
+ {
+ _chart.WorkSheet.Workbook.ThemeManager.CreateDefaultTheme();
+ }
+ _theme = _chart.WorkSheet.Workbook.ThemeManager.CurrentTheme;
+
+ _context = new TranslatorContext(settings);
+ _context.Theme = _theme;
+
+ //RuleCollection = new CssRuleCollection();
+
+ AddChartFillToCollection(chart, "epp-");
+ }
+
+ internal void AddChartFillToCollection(ExcelChart chart, string chartClassPreset)
+ {
+ var chartClass = $"{chartClassPreset}{HtmlExportTableUtil.GetClassName(chart.Name, $"chartstyle{chart.Id}")}";
+
+ var s = chart.StyleManager.Style;
+
+ if(s == null)
+ {
+ chart.StyleManager.ApplyStyles();
+ }
+
+ s = chart.StyleManager.Style;
+
+ if (s != null)
+ {
+
+ var chartAreaRef = chart.StyleManager.Style.ChartArea.FillReference.Color;
+
+ AddToCollection(chartClass, chart.Fill, chart.Border, "rect");
+
+
+ //var fallbackElementBorder = chart.StyleManager.Style.ChartArea.BorderReference.Color;
+
+ //AddToCollection(chartClass, chart.Border, "border");
+ }
+ }
+
+ internal void AddToCollection(string name, ExcelDrawingBorder element, string htmlElement)
+ {
+ //if (element) return; //Dont add empty elements
+
+ var s = element;
+
+ var styleClass = new CssRule($"{htmlElement}.{name}", int.MaxValue);
+
+ var translators = new List();
+
+ if (element != null && _context.Exclude.Fill == false)
+ {
+ //TODO: Ensure if gradients with more than 2 colors it is handled correctly.
+ translators.Add(new CssFillTranslator(new FillDrawingBasic(element.Fill)));
+ }
+ //if (s.Font != null && _context.Exclude.Font != eFontExclude.All)
+ //{
+ // translators.Add(new CssFontTranslator(new FontDxf(s.Font), null));
+ //}
+ //if (s.Border != null && _context.Exclude.Border != eBorderExclude.All)
+ //{
+ // translators.Add(new CssBorderTranslator(new BorderDxf(s.Border)));
+ //}
+
+ foreach (var translator in translators)
+ {
+ _context.SetTranslator(translator);
+ _context.AddDeclarations(styleClass);
+ }
+
+ AddRule(styleClass);
+ }
+
+ internal void AddToCollection(string name, ExcelDrawingFill element, ExcelDrawingBorder border, string htmlElement)
+ {
+ if (element.IsEmpty) return; //Dont add empty elements
+
+ var s = element;
+
+ var styleClass = new CssRule($"{htmlElement}.{name}", int.MaxValue);
+
+ var translators = new List();
+
+ if (element != null && _context.Exclude.Fill == false)
+ {
+ var fillGeneric = new FillDrawing(element);
+ var fillTranslator = new CssFillTranslator(fillGeneric, true);
+ //TODO: Ensure if gradients with more than 2 colors it is handled correctly.
+ translators.Add(fillTranslator);
+ }
+ //if (s.Font != null && _context.Exclude.Font != eFontExclude.All)
+ //{
+ // translators.Add(new CssFontTranslator(new FontDxf(s.Font), null));
+ //}
+ if (border != null && _context.Exclude.Border != eBorderExclude.All)
+ {
+
+ translators.Add(new CssStrokeTranslator(new BorderDrawing(border)));
+ }
+
+ foreach (var translator in translators)
+ {
+ _context.SetTranslator(translator);
+ _context.AddDeclarations(styleClass);
+ }
+
+ AddRule(styleClass);
+ }
+
+ internal void AddToCollection(string name, ExcelChartStyleItem element, string htmlElement)
+ {
+ if (element.HasValue() == false) return; //Dont add empty elements
+
+ var s = element;
+
+ var styleClass = new CssRule($"{htmlElement}.{name}", int.MaxValue);
+
+ var translators = new List();
+
+ if (s.Fill != null && _context.Exclude.Fill == false)
+ {
+ translators.Add(new CssFillTranslator(new FillDrawing(s.Fill)));
+ }
+ //if (s.Font != null && _context.Exclude.Font != eFontExclude.All)
+ //{
+ // translators.Add(new CssFontTranslator(new FontDxf(s.Font), null));
+ //}
+ //if (s.Border != null && _context.Exclude.Border != eBorderExclude.All)
+ //{
+ // translators.Add(new CssBorderTranslator(new BorderDxf(s.Border)));
+ //}
+
+ foreach (var translator in translators)
+ {
+ _context.SetTranslator(translator);
+ _context.AddDeclarations(styleClass);
+ }
+
+ AddRule(styleClass);
+ }
+ }
+}
diff --git a/src/EPPlus/Export/HtmlExport/Exporters/Internal/CssChartExporterSync.cs b/src/EPPlus/Export/HtmlExport/Exporters/Internal/CssChartExporterSync.cs
new file mode 100644
index 0000000000..4374a53e91
--- /dev/null
+++ b/src/EPPlus/Export/HtmlExport/Exporters/Internal/CssChartExporterSync.cs
@@ -0,0 +1,102 @@
+using OfficeOpenXml.ConditionalFormatting;
+using OfficeOpenXml.ConditionalFormatting.Contracts;
+using OfficeOpenXml.ConditionalFormatting.Rules;
+using OfficeOpenXml.Core;
+using OfficeOpenXml.Core.CellStore;
+using OfficeOpenXml.Core.RangeQuadTree;
+using OfficeOpenXml.Drawing.Chart;
+using OfficeOpenXml.Export.HtmlExport.CssCollections;
+using OfficeOpenXml.Export.HtmlExport.Determinator;
+using OfficeOpenXml.Export.HtmlExport.Settings;
+using OfficeOpenXml.Export.HtmlExport.StyleCollectors;
+using OfficeOpenXml.Export.HtmlExport.StyleCollectors.StyleContracts;
+using OfficeOpenXml.Export.HtmlExport.Translators;
+using OfficeOpenXml.Export.HtmlExport.Writers;
+using OfficeOpenXml.Style.XmlAccess;
+using OfficeOpenXml.Table;
+using OfficeOpenXml.Utils;
+using System;
+using System.Collections.Generic;
+using System.Data;
+using System.IO;
+using System.Linq;
+using System.Runtime.CompilerServices;
+
+namespace OfficeOpenXml.Export.HtmlExport.Exporters.Internal
+{
+ internal class CssChartExporterSync
+ {
+ CssExportSettings _settings;
+
+ ExcelChart _chart;
+
+ TranslatorContext _context;
+
+ public CssChartExporterSync(ExcelChart chart) : this(new CssChartExportSettings(), chart)
+ {
+
+ }
+
+ public CssChartExporterSync(CssChartExportSettings settings, ExcelChart chart)
+ {
+ _settings = settings;
+ Require.Argument(chart).IsNotNull("chart");
+ _chart = chart;
+ }
+ ///
+ /// Exports an to a html string
+ ///
+ /// A html table
+ public string GetCssString()
+ {
+ using (var ms = EPPlusMemoryManager.GetStream())
+ {
+ RenderCss(ms);
+ ms.Position = 0;
+ using (var sr = new StreamReader(ms))
+ {
+ return sr.ReadToEnd();
+ }
+ }
+ }
+ ///
+ /// Exports the css part of the html export.
+ ///
+ /// The stream to write the css to.
+ ///
+ public void RenderCss(Stream stream)
+ {
+ var trueWriter = new CssWriter(stream);
+
+ var cssCollection = new CssChartRuleCollection(_chart, _settings);
+
+ trueWriter.WriteAndClearFlush(cssCollection, false);
+ }
+
+ // ///
+ // /// Exports the css part of an to a html string
+ // ///
+ // /// A html table
+ // public void RenderCss(Stream stream)
+ // {
+ // var cssWriter = GetTableCssWriter(stream, _table, _tableSettings);
+ // if (cssWriter == null) { return; }
+
+ // var cssRules = CreateRuleCollection(_tableSettings);
+ // cssWriter.WriteAndClearFlush(cssRules, Settings.Minify);
+ // }
+
+ // protected CssRuleCollection CreateRuleCollection(CssExportSettings settings)
+ // {
+ // var cssTranslator = new CssChartRuleCollection(_chart, settings);
+
+ // _context = new TranslatorContext(settings);
+
+
+ // //AddCssRulesToCollection(cssTranslator, settings);
+
+ // //return cssTranslator.RuleCollection;
+ // }
+
+ }
+ }
diff --git a/src/EPPlus/Export/HtmlExport/Exporters/Internal/CssExporterBase.cs b/src/EPPlus/Export/HtmlExport/Exporters/Internal/CssExporterBase.cs
index e6bdbc9b67..789d89e7fe 100644
--- a/src/EPPlus/Export/HtmlExport/Exporters/Internal/CssExporterBase.cs
+++ b/src/EPPlus/Export/HtmlExport/Exporters/Internal/CssExporterBase.cs
@@ -11,10 +11,12 @@ Date Author Change
6/4/2022 EPPlus Software AB ExcelTable Html Export
*************************************************************************************************/
using OfficeOpenXml.ConditionalFormatting;
+using OfficeOpenXml.ConditionalFormatting.Contracts;
using OfficeOpenXml.ConditionalFormatting.Rules;
using OfficeOpenXml.Core;
using OfficeOpenXml.Core.CellStore;
using OfficeOpenXml.Core.RangeQuadTree;
+using OfficeOpenXml.Drawing.Chart;
using OfficeOpenXml.Export.HtmlExport.CssCollections;
using OfficeOpenXml.Export.HtmlExport.Determinator;
using OfficeOpenXml.Export.HtmlExport.Settings;
@@ -26,10 +28,9 @@ Date Author Change
using OfficeOpenXml.Utils;
using System;
using System.Collections.Generic;
+using System.Data;
using System.IO;
using System.Linq;
-using OfficeOpenXml.ConditionalFormatting.Contracts;
-using System.Data;
using System.Runtime.CompilerServices;
namespace OfficeOpenXml.Export.HtmlExport.Exporters.Internal
diff --git a/src/EPPlus/Export/HtmlExport/Settings/CssChartExportSettings.cs b/src/EPPlus/Export/HtmlExport/Settings/CssChartExportSettings.cs
new file mode 100644
index 0000000000..4d9ed35731
--- /dev/null
+++ b/src/EPPlus/Export/HtmlExport/Settings/CssChartExportSettings.cs
@@ -0,0 +1,14 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+
+namespace OfficeOpenXml.Export.HtmlExport.Settings
+{
+ internal class CssChartExportSettings : CssExportSettings
+ {
+ public CssChartExportSettings() : base()
+ {
+ }
+ }
+}
diff --git a/src/EPPlus/Export/HtmlExport/StyleCollectors/BorderDrawing.cs b/src/EPPlus/Export/HtmlExport/StyleCollectors/BorderDrawing.cs
new file mode 100644
index 0000000000..932ee0e0d1
--- /dev/null
+++ b/src/EPPlus/Export/HtmlExport/StyleCollectors/BorderDrawing.cs
@@ -0,0 +1,24 @@
+using OfficeOpenXml.Drawing;
+using OfficeOpenXml.Export.HtmlExport.StyleCollectors.StyleContracts;
+using OfficeOpenXml.Style;
+using OfficeOpenXml.Style.Dxf;
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+
+namespace OfficeOpenXml.Export.HtmlExport.StyleCollectors
+{
+ internal class BorderDrawing : IDrawingBorder
+ {
+ ExcelDrawingBorder _border;
+
+ internal BorderDrawing(ExcelDrawingBorder border)
+ {
+ _border = border;
+ Stroke = new FillDrawingBasic(border.Fill);
+ }
+
+ public IFill Stroke { get; private set; }
+ }
+}
diff --git a/src/EPPlus/Export/HtmlExport/StyleCollectors/BorderDrawingAlt.cs b/src/EPPlus/Export/HtmlExport/StyleCollectors/BorderDrawingAlt.cs
new file mode 100644
index 0000000000..90478306ab
--- /dev/null
+++ b/src/EPPlus/Export/HtmlExport/StyleCollectors/BorderDrawingAlt.cs
@@ -0,0 +1,10 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+namespace OfficeOpenXml.Export.HtmlExport.StyleCollectors
+{
+ internal class BorderDrawingAlt
+ {
+ }
+}
diff --git a/src/EPPlus/Export/HtmlExport/StyleCollectors/BorderItemDrawing.cs b/src/EPPlus/Export/HtmlExport/StyleCollectors/BorderItemDrawing.cs
new file mode 100644
index 0000000000..8e7ef14f7d
--- /dev/null
+++ b/src/EPPlus/Export/HtmlExport/StyleCollectors/BorderItemDrawing.cs
@@ -0,0 +1,23 @@
+using OfficeOpenXml.Drawing;
+using OfficeOpenXml.Export.HtmlExport.StyleCollectors.StyleContracts;
+using OfficeOpenXml.Style;
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+
+namespace OfficeOpenXml.Export.HtmlExport.StyleCollectors
+{
+ //internal class BorderItemDrawing : IBorderItem
+ //{
+ // public BorderItemDrawing(ExcelDrawingBorder border)
+ // {
+ // var fill = new FillDrawingBasic(border.Fill);
+ // //border.Fill.Color
+ // }
+
+ // ExcelBorderStyle IBorderItem.Style => throw new NotImplementedException();
+
+ // IStyleColor IBorderItem.Color => throw new NotImplementedException();
+ //}
+}
diff --git a/src/EPPlus/Export/HtmlExport/StyleCollectors/FillDrawing.cs b/src/EPPlus/Export/HtmlExport/StyleCollectors/FillDrawing.cs
new file mode 100644
index 0000000000..cfdd47818a
--- /dev/null
+++ b/src/EPPlus/Export/HtmlExport/StyleCollectors/FillDrawing.cs
@@ -0,0 +1,145 @@
+using OfficeOpenXml.Drawing;
+using OfficeOpenXml.Drawing.Theme;
+using OfficeOpenXml.Export.HtmlExport.StyleCollectors.StyleContracts;
+using OfficeOpenXml.Style;
+using OfficeOpenXml.Style.Dxf;
+using System;
+using System.Collections.Generic;
+using System.Drawing;
+using System.Linq;
+using System.Text;
+
+namespace OfficeOpenXml.Export.HtmlExport.StyleCollectors
+{
+ internal class FillDrawing : IFill
+ {
+ ExcelDrawingFill _fill;
+
+ internal FillDrawing(ExcelDrawingFill fill)
+ {
+ _fill = fill;
+ }
+
+ public bool IsGradient
+ {
+ get
+ {
+ return _fill.Style == eFillStyle.GradientFill;
+ }
+ }
+
+
+ public bool IsLinear
+ {
+ get
+ {
+ return _fill.GradientFill.ShadePath == eShadePath.Linear;
+ }
+ }
+
+
+ public double Degree
+ {
+ get
+ {
+ if (IsGradient)
+ {
+ if(IsLinear)
+ {
+ return _fill.GradientFill.LinearSettings.Angle;
+ }
+ }
+
+ return double.NaN;
+ }
+ }
+
+
+
+ public double Right
+ {
+ get
+ {
+ if (IsGradient)
+ {
+ if(IsLinear == false)
+ {
+ return _fill.GradientFill.TileRectangle.RightOffset;
+ }
+ }
+
+ return double.NaN;
+ }
+ }
+
+ public double Bottom
+ {
+ get
+ {
+ if (IsGradient)
+ {
+ if (IsLinear == false)
+ {
+ return _fill.GradientFill.TileRectangle.BottomOffset;
+ }
+ }
+
+ return double.NaN;
+ }
+ }
+
+ public string GetBackgroundColor(ExcelTheme theme)
+ {
+ return GetColor(_fill.Color, theme);
+ }
+
+ public string GetPatternColor(ExcelTheme theme)
+ {
+ return GetColor(_fill.PatternFill.ForegroundColor.GetColor(), theme);
+ }
+
+ public string GetGradientColor1(ExcelTheme theme)
+ {
+ return GetColor(_fill.GradientFill.Colors.ToArray()[0].Color.GetColor(), theme);
+ }
+ public string GetGradientColor2(ExcelTheme theme)
+ {
+ return GetColor(_fill.GradientFill.Colors.ToArray()[1].Color.GetColor(), theme);
+ }
+
+ public bool HasValue
+ {
+ get
+ {
+ return !_fill.IsEmpty;
+ }
+ }
+
+ ExcelFillStyle IFill.PatternType => ExcelFillStyle.Solid;
+
+ internal static string GetColor(Color c, ExcelTheme theme)
+ {
+ return "#" + c.ToArgb().ToString("x8").Substring(2);
+ }
+
+ string IFill.GetBackgroundColor(ExcelTheme theme)
+ {
+ return GetBackgroundColor(theme);
+ }
+
+ string IFill.GetPatternColor(ExcelTheme theme)
+ {
+ return GetPatternColor(theme);
+ }
+
+ string IFill.GetGradientColor1(ExcelTheme theme)
+ {
+ return GetGradientColor1(theme);
+ }
+
+ string IFill.GetGradientColor2(ExcelTheme theme)
+ {
+ return GetGradientColor2(theme);
+ }
+ }
+}
diff --git a/src/EPPlus/Export/HtmlExport/StyleCollectors/FillDrawingBasic.cs b/src/EPPlus/Export/HtmlExport/StyleCollectors/FillDrawingBasic.cs
new file mode 100644
index 0000000000..9841790876
--- /dev/null
+++ b/src/EPPlus/Export/HtmlExport/StyleCollectors/FillDrawingBasic.cs
@@ -0,0 +1,144 @@
+using OfficeOpenXml.Drawing;
+using OfficeOpenXml.Drawing.Theme;
+using OfficeOpenXml.Export.HtmlExport.StyleCollectors.StyleContracts;
+using OfficeOpenXml.Style;
+using System;
+using System.Collections.Generic;
+using System.Drawing;
+using System.Linq;
+using System.Text;
+
+namespace OfficeOpenXml.Export.HtmlExport.StyleCollectors
+{
+ internal class FillDrawingBasic : IFill
+ {
+ ExcelDrawingFillBasic _fill;
+
+ internal FillDrawingBasic(ExcelDrawingFillBasic fill)
+ {
+ _fill = fill;
+ }
+
+ public bool IsGradient
+ {
+ get
+ {
+ return _fill.Style == eFillStyle.GradientFill;
+ }
+ }
+
+
+ public bool IsLinear
+ {
+ get
+ {
+ return _fill.GradientFill.ShadePath == eShadePath.Linear;
+ }
+ }
+
+
+ public double Degree
+ {
+ get
+ {
+ if (IsGradient)
+ {
+ if (IsLinear)
+ {
+ return _fill.GradientFill.LinearSettings.Angle;
+ }
+ }
+
+ return double.NaN;
+ }
+ }
+
+
+
+ public double Right
+ {
+ get
+ {
+ if (IsGradient)
+ {
+ if (IsLinear == false)
+ {
+ return _fill.GradientFill.TileRectangle.RightOffset;
+ }
+ }
+
+ return double.NaN;
+ }
+ }
+
+ public double Bottom
+ {
+ get
+ {
+ if (IsGradient)
+ {
+ if (IsLinear == false)
+ {
+ return _fill.GradientFill.TileRectangle.BottomOffset;
+ }
+ }
+
+ return double.NaN;
+ }
+ }
+
+ public string GetBackgroundColor(ExcelTheme theme)
+ {
+ return GetColor(_fill.Color, theme);
+ }
+
+ public string GetPatternColor(ExcelTheme theme)
+ {
+ return GetColor(Color.Empty, theme);
+ }
+
+ public string GetGradientColor1(ExcelTheme theme)
+ {
+ return GetColor(_fill.GradientFill.Colors.ToArray()[0].Color.GetColor(), theme);
+ }
+ public string GetGradientColor2(ExcelTheme theme)
+ {
+ return GetColor(_fill.GradientFill.Colors.ToArray()[1].Color.GetColor(), theme);
+ }
+
+ public bool HasValue
+ {
+ get
+ {
+ return !_fill.IsEmpty;
+ }
+ }
+
+ public ExcelFillStyle PatternType => ExcelFillStyle.Solid;
+
+ internal static string GetColor(Color c, ExcelTheme theme)
+ {
+ return "#" + c.ToArgb().ToString("x8").Substring(2);
+ }
+
+ string IFill.GetBackgroundColor(ExcelTheme theme)
+ {
+ return GetBackgroundColor(theme);
+ }
+
+ string IFill.GetPatternColor(ExcelTheme theme)
+ {
+ return GetPatternColor(theme);
+ }
+
+ string IFill.GetGradientColor1(ExcelTheme theme)
+ {
+ return GetGradientColor1(theme);
+ }
+
+ string IFill.GetGradientColor2(ExcelTheme theme)
+ {
+ return GetGradientColor2(theme);
+ }
+ }
+}
diff --git a/src/EPPlus/Export/HtmlExport/StyleCollectors/StyleColorDrawing.cs b/src/EPPlus/Export/HtmlExport/StyleCollectors/StyleColorDrawing.cs
new file mode 100644
index 0000000000..68463da7e2
--- /dev/null
+++ b/src/EPPlus/Export/HtmlExport/StyleCollectors/StyleColorDrawing.cs
@@ -0,0 +1,43 @@
+using OfficeOpenXml.Drawing;
+using OfficeOpenXml.Drawing.Theme;
+using OfficeOpenXml.Export.HtmlExport.StyleCollectors.StyleContracts;
+using OfficeOpenXml.Style;
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+
+//namespace OfficeOpenXml.Export.HtmlExport.StyleCollectors
+//{
+// internal class StyleColorDrawing : IStyleColor
+// {
+ // public StyleColorDrawing()
+ // {
+ // }
+
+ // ExcelColor _color;
+
+ // //TODO: Is this correct?
+ // public bool Exists { get { return true; } }
+
+ // public bool Auto { get { return _color.Auto; } }
+
+ // public int Indexed { get { return _color.Indexed; } }
+
+ // public double Tint { get { return (double)_color.Tint; } }
+
+ // public eThemeSchemeColor? Theme { get { return _color.Theme; } }
+
+ // public string Rgb { get { return _color.Rgb; } }
+
+ // public bool AreColorEqual(IStyleColor color)
+ // {
+ // return StyleColorShared.AreColorEqual(this, color);
+ // }
+
+ // public string GetColor(ExcelTheme theme)
+ // {
+ // return StyleColorShared.GetColor(this, theme);
+ // }
+ //}
+//}
diff --git a/src/EPPlus/Export/HtmlExport/StyleCollectors/StyleContracts/IDrawingBorder.cs b/src/EPPlus/Export/HtmlExport/StyleCollectors/StyleContracts/IDrawingBorder.cs
new file mode 100644
index 0000000000..b3cbe1d495
--- /dev/null
+++ b/src/EPPlus/Export/HtmlExport/StyleCollectors/StyleContracts/IDrawingBorder.cs
@@ -0,0 +1,12 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+
+namespace OfficeOpenXml.Export.HtmlExport.StyleCollectors.StyleContracts
+{
+ public interface IDrawingBorder
+ {
+ IFill Stroke { get; }
+ }
+}
diff --git a/src/EPPlus/Export/HtmlExport/StyleCollectors/StyleDrawing.cs b/src/EPPlus/Export/HtmlExport/StyleCollectors/StyleDrawing.cs
new file mode 100644
index 0000000000..4cbc1dcf79
--- /dev/null
+++ b/src/EPPlus/Export/HtmlExport/StyleCollectors/StyleDrawing.cs
@@ -0,0 +1,72 @@
+using OfficeOpenXml.Drawing.Chart.Style;
+using OfficeOpenXml.Export.HtmlExport.StyleCollectors.StyleContracts;
+using OfficeOpenXml.Style.Dxf;
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+
+//namespace OfficeOpenXml.Export.HtmlExport.StyleCollectors
+//{
+// internal class StyleDrawing : IStyleExport
+// {
+// //ExcelChartStyleEntry _style;
+
+// //public bool HasStyle
+// //{
+// // get { return _style.HasFill; }
+// //}
+
+// //public string StyleKey { get; } = null; /*{ get { return _style.Id; } }*/
+
+// //public IFill Fill { get; } = null;
+// //public IFont Font { get; } = null;
+// //public IBorder Border { get; } = null;
+// //public INumberFormat NumberFormat { get; } = null;
+
+// ////Charts never have a checkbox. Break out of baseClass?
+// //bool IStyleExport.CheckBox => false;
+
+// //public int StyleId;
+
+// //public StyleDrawing(ExcelChartStyleEntry style, int styleId)
+// //{
+// // _style = style;
+// // StyleId = styleId;
+
+// // if (style.HasFill)
+// // {
+// // Fill = new FillDrawing(style.Fill);
+// // }
+// // if (style.HasTextBody)
+// // {
+// // //not implemented yet
+// // if (style.HasRichText)
+// // {
+// // if(style.HasTextRun)
+// // {
+// // //style.FontReference
+// // }
+// // }
+// // //Font = new FontDxf(style.Font);
+// // }
+// // if(style.FontReference != null)
+// // {
+// // //not implemented yet
+// // }
+// // if (style.HasBorder)
+// // {
+// // //style.Border.
+// // //var lineStyle = style.Border.LineStyle;
+// // //Border.Top.Style = Style.ExcelBorderStyle.
+// // //Border = new FillDrawingBasic(style.Border.Fill);
+// // }
+// // //if (style. != null && style.NumberFormat.HasValue)
+// // //{
+// // // NumberFormat = new NumberFormatDxf(style.NumberFormat, styleId);
+// // //}
+
+// // //CheckBox = false;
+// //}
+// }
+//}
diff --git a/src/EPPlus/Export/HtmlExport/Translators/CssFillTranslator.cs b/src/EPPlus/Export/HtmlExport/Translators/CssFillTranslator.cs
index 438a2a3c15..a9d5a945ea 100644
--- a/src/EPPlus/Export/HtmlExport/Translators/CssFillTranslator.cs
+++ b/src/EPPlus/Export/HtmlExport/Translators/CssFillTranslator.cs
@@ -24,10 +24,20 @@ internal class CssFillTranslator : TranslatorBase
{
ExcelTheme _theme;
IFill _fill;
+ bool _usePresentationName = false;
- internal CssFillTranslator(IFill fill)
+ string presentationName = "fill";
+
+ string bgName = "background-color";
+
+ internal CssFillTranslator(IFill fill, bool usePresentationName = false)
{
_fill = fill;
+ _usePresentationName = usePresentationName;
+ if(usePresentationName)
+ {
+ bgName = presentationName;
+ }
}
internal override List GenerateDeclarationList(TranslatorContext context)
@@ -47,7 +57,7 @@ internal override List GenerateDeclarationList(TranslatorContext co
var bc = _fill.GetBackgroundColor(_theme) ?? "#0";
if (string.IsNullOrEmpty(bc) == false)
{
- AddDeclaration("background-color", bc);
+ AddDeclaration(bgName, bc);
}
}
else if(_fill.PatternType == ExcelFillStyle.None)
@@ -55,7 +65,7 @@ internal override List GenerateDeclarationList(TranslatorContext co
var fc = _fill.GetPatternColor(_theme);
if (string.IsNullOrEmpty(fc) == false)
{
- AddDeclaration("background-color", fc);
+ AddDeclaration(bgName, fc);
}
}
else
diff --git a/src/EPPlus/Export/HtmlExport/Translators/CssStrokeTranslator.cs b/src/EPPlus/Export/HtmlExport/Translators/CssStrokeTranslator.cs
new file mode 100644
index 0000000000..8ea8396950
--- /dev/null
+++ b/src/EPPlus/Export/HtmlExport/Translators/CssStrokeTranslator.cs
@@ -0,0 +1,33 @@
+using OfficeOpenXml.Drawing.Theme;
+using OfficeOpenXml.Export.HtmlExport.CssCollections;
+using OfficeOpenXml.Export.HtmlExport.StyleCollectors.StyleContracts;
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+
+namespace OfficeOpenXml.Export.HtmlExport.Translators
+{
+ internal class CssStrokeTranslator : TranslatorBase
+ {
+ IDrawingBorder _drawingBorder;
+ ExcelTheme _theme;
+
+ public CssStrokeTranslator(IDrawingBorder drawingBorder)
+ {
+ _drawingBorder = drawingBorder;
+ }
+
+ internal override List GenerateDeclarationList(TranslatorContext context)
+ {
+ var borderExclude = context.Exclude.Border;
+ _theme = context.Theme;
+
+ if(_drawingBorder.Stroke.HasValue && _drawingBorder.Stroke.PatternType == Style.ExcelFillStyle.Solid)
+ {
+ AddDeclaration($"stroke", _drawingBorder.Stroke.GetBackgroundColor(_theme));
+ }
+ return declarations;
+ }
+ }
+}
diff --git a/src/EPPlus/Export/HtmlExport/Translators/TranslatorContext.cs b/src/EPPlus/Export/HtmlExport/Translators/TranslatorContext.cs
index 8145d29490..fceb1178d5 100644
--- a/src/EPPlus/Export/HtmlExport/Translators/TranslatorContext.cs
+++ b/src/EPPlus/Export/HtmlExport/Translators/TranslatorContext.cs
@@ -42,6 +42,11 @@ internal class TranslatorContext
internal HashSet AddedIcons = new HashSet();
+ public TranslatorContext(CssExportSettings settings)
+ {
+ Settings = settings;
+ Exclude = new CssExclude();
+ }
public TranslatorContext(HtmlRangeExportSettings settings)
{
diff --git a/src/EPPlus/FormulaParsing/CalculateExtensions.cs b/src/EPPlus/FormulaParsing/CalculateExtensions.cs
index 5d6e6b7801..6db4e071ac 100644
--- a/src/EPPlus/FormulaParsing/CalculateExtensions.cs
+++ b/src/EPPlus/FormulaParsing/CalculateExtensions.cs
@@ -62,6 +62,10 @@ public static void Calculate(this ExcelWorkbook workbook, ActionCalculation options
public static void Calculate(this ExcelWorkbook workbook, ExcelCalculationOption options)
{
+ #if !NET35
+ workbook.ThrowIfCalculationCanceled(); // Guard: prevent recalc on poisoned workbook
+ #endif
+
Init(workbook);
var filterInfo = new FilterInfo(workbook);
@@ -74,12 +78,25 @@ public static void Calculate(this ExcelWorkbook workbook, ExcelCalculationOption
}
//CalcChain(workbook, workbook.FormulaParser, dc, options);
- var dc=RpnFormulaExecution.Execute(workbook, options);
- if (workbook.FormulaParser.Logger != null)
+#if !NET35
+ try
{
- var msg = string.Format("Calculation done...number of cells parsed: {0}", dc.processedCells.Count);
- workbook.FormulaParser.Logger.Log(msg);
+#endif
+ var dc =RpnFormulaExecution.Execute(workbook, options);
+ dc._parsingContext.RangeCriteriaCache?.Clear();
+ if (workbook.FormulaParser.Logger != null)
+ {
+ var msg = string.Format("Calculation done...number of cells parsed: {0}", dc.processedCells.Count);
+ workbook.FormulaParser.Logger.Log(msg);
+ }
+#if !NET35
}
+ catch (OperationCanceledException)
+ {
+ workbook.MarkCalculationCanceled();
+ throw;
+ }
+#endif
}
internal static RpnOptimizedDependencyChain CalculateWithDC(this ExcelWorkbook workbook, Action configHandler)
{
@@ -157,7 +174,21 @@ public static void Calculate(this ExcelWorksheet worksheet, Action
@@ -193,13 +224,23 @@ public static void Calculate(this ExcelRangeBase range, ActionCalculation options
public static void Calculate(this ExcelRangeBase range, ExcelCalculationOption options)
{
- Init(range._workbook);
- //var parser = range._workbook.FormulaParser;
- //var filterInfo = new FilterInfo(range._workbook);
- //parser.InitNewCalc(filterInfo);
- //var dc = DependencyChainFactory.Create(range, options);
- //CalcChain(range._workbook, parser, dc, options);
- var dc = RpnFormulaExecution.Execute(range, options);
+#if !NET35
+ range._workbook.ThrowIfCalculationCanceled();
+ try
+ {
+#endif
+ Init(range._workbook);
+ var dc = RpnFormulaExecution.Execute(range, options);
+ // Clear RangeCriteriaCache after calculation completes
+ dc._parsingContext.RangeCriteriaCache?.Clear();
+#if !NET35
+ }
+ catch (OperationCanceledException)
+ {
+ range._workbook.MarkCalculationCanceled();
+ throw;
+ }
+#endif
}
///
@@ -230,7 +271,9 @@ public static object Calculate(this ExcelWorksheet worksheet, string Formula, Ex
//var filterInfo = new FilterInfo(worksheet.Workbook);
//parser.InitNewCalc(filterInfo);
if (Formula[0] == '=') Formula = Formula.Substring(1); //Remove any starting equal sign
- return RpnFormulaExecution.ExecuteFormula(worksheet.Workbook, Formula,new FormulaCellAddress(worksheet.IndexInList, -1, 0), options);
+ var result = RpnFormulaExecution.ExecuteFormula(worksheet.Workbook, Formula,new FormulaCellAddress(worksheet.IndexInList, -1, 0), options);
+ worksheet.Workbook.FormulaParser.ParsingContext?.RangeCriteriaCache?.Clear();
+ return result;
}
catch (Exception ex)
{
diff --git a/src/EPPlus/FormulaParsing/DependencyChain/ArrayFormulaOutput.cs b/src/EPPlus/FormulaParsing/DependencyChain/ArrayFormulaOutput.cs
index c44e548855..f8b83172ae 100644
--- a/src/EPPlus/FormulaParsing/DependencyChain/ArrayFormulaOutput.cs
+++ b/src/EPPlus/FormulaParsing/DependencyChain/ArrayFormulaOutput.cs
@@ -10,17 +10,18 @@ Date Author Change
*************************************************************************************************
05/14/2024 EPPlus Software AB Initial release EPPlus 7
*************************************************************************************************/
-using OfficeOpenXml.FormulaParsing.LexicalAnalysis;
using OfficeOpenXml.Core.CellStore;
-using System;
-using System.Collections.Generic;
-using System.Linq;
-using System.Text;
-using OfficeOpenXml.FormulaParsing.Excel.Functions.RefAndLookup;
using OfficeOpenXml.Filter;
+using OfficeOpenXml.FormulaParsing.Excel.Functions.RefAndLookup;
using OfficeOpenXml.FormulaParsing.FormulaExpressions;
+using OfficeOpenXml.FormulaParsing.LexicalAnalysis;
+using OfficeOpenXml.FormulaParsing.Ranges;
using OfficeOpenXml.Utils;
using OfficeOpenXml.Utils.TypeConversion;
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
namespace OfficeOpenXml.FormulaParsing
{
@@ -37,7 +38,14 @@ internal static void FillArrayFromRangeInfo(RpnFormula f, IRangeInfo array, Rang
var rows = sf.EndRow - sf.StartRow + 1;
var cols = sf.EndCol - sf.StartCol + 1;
var wsIx = ws.IndexInList;
- for (int r = 0; r < rows; r++)
+
+ // Determine physical boundary and default value for virtual rows
+ var virtualImr = array as InMemoryRange;
+ var hasVirtual = virtualImr != null && virtualImr.HasVirtualRows;
+ var physicalRowLimit = hasVirtual ? Math.Min(rows, virtualImr.PhysicalRows) : rows;
+
+ // Phase 1: Physical rows - read actual cell values via GetOffset
+ for (int r = 0; r < physicalRowLimit; r++)
{
for (int c = 0; c < cols; c++)
{
@@ -46,9 +54,8 @@ internal static void FillArrayFromRangeInfo(RpnFormula f, IRangeInfo array, Rang
if (r < nr && c < nc)
{
var val = array.GetOffset(r, c);
- if(ConvertUtil.IsNumeric(val) && val is double dbl && dbl == 0d)
+ if (ConvertUtil.IsNumeric(val) && val is double dbl && dbl == 0d)
{
- // avoid -0
val = 0d;
}
ws.SetValueInner(row, col, val ?? 0D);
@@ -58,7 +65,35 @@ internal static void FillArrayFromRangeInfo(RpnFormula f, IRangeInfo array, Rang
ws.SetValueInner(row, col, ErrorValues.NAError);
}
var id = ExcelCellBase.GetCellId(wsIx, row, col);
- depChain.processedCells.Add(id);
+ depChain.processedCells.Add(id);
+ }
+ }
+
+ // Phase 2: Virtual rows - write pre-computed default value directly
+ if (hasVirtual && physicalRowLimit < rows)
+ {
+ var defaultVal = virtualImr.VirtualDefaultValue ?? 0D;
+ if (ConvertUtil.IsNumeric(defaultVal) && defaultVal is double dblDefault && dblDefault == 0d)
+ {
+ defaultVal = 0d;
+ }
+ for (int r = physicalRowLimit; r < rows; r++)
+ {
+ for (int c = 0; c < cols; c++)
+ {
+ var row = sr + r;
+ var col = sc + c;
+ if (r < nr && c < nc)
+ {
+ ws.SetValueInner(row, col, defaultVal);
+ }
+ else
+ {
+ ws.SetValueInner(row, col, ErrorValues.NAError);
+ }
+ var id = ExcelCellBase.GetCellId(wsIx, row, col);
+ depChain.processedCells.Add(id);
+ }
}
}
@@ -74,7 +109,7 @@ private static bool HasSpill(ExcelWorksheet ws, int fIx, int startRow, int start
{
if (r == startRow && c == startColumn) continue;
object f = -1;
- if (fIx!=-1 && ws._formulas.Exists(r, c, ref f) && f != null)
+ if (fIx != -1 && ws._formulas.Exists(r, c, ref f) && f != null)
{
if (f is int intfIx && intfIx == fIx)
{
@@ -87,7 +122,7 @@ private static bool HasSpill(ExcelWorksheet ws, int fIx, int startRow, int start
else
{
var v = ws.GetValueInner(r, c);
- if(v!=null)
+ if (v != null)
{
rowOff = r - startRow;
colOff = c - startColumn;
@@ -98,7 +133,7 @@ private static bool HasSpill(ExcelWorksheet ws, int fIx, int startRow, int start
}
rowOff = colOff = 0;
return false;
- }
+ }
internal static SimpleAddress[] FillDynamicArrayFromRangeInfo(RpnFormula f, IRangeInfo array, RangeHashset rd, RpnOptimizedDependencyChain depChain)
{
var nr = array.Size.NumberOfRows;
@@ -107,13 +142,13 @@ internal static SimpleAddress[] FillDynamicArrayFromRangeInfo(RpnFormula f, IRan
var startRow = f._row;
var startCol = f._column;
var wsIx = ws.IndexInList;
-
+
f._flags |= FormulaFlags.IsDynamic;
var md = depChain._parsingContext.Package.Workbook.Metadata;
//d.GetDynamicArrayIndex(out int cm);
md.GetDynamicArrayId(out uint cm);
var metaData = f._ws._metadataStore.GetValue(startRow, startCol);
- metaData.cm= cm;
+ metaData.cm = cm;
f._ws._metadataStore.SetValue(f._row, f._column, metaData);
@@ -134,7 +169,7 @@ internal static SimpleAddress[] FillDynamicArrayFromRangeInfo(RpnFormula f, IRan
}
}
SimpleAddress[] dirtyRange;
- if(f._arrayIndex==-1)
+ if (f._arrayIndex == -1)
{
var endRow = startRow + array.Size.NumberOfRows - 1;
var endCol = f._column + array.Size.NumberOfCols - 1;
@@ -151,7 +186,7 @@ internal static SimpleAddress[] FillDynamicArrayFromRangeInfo(RpnFormula f, IRan
CleanupSharedFormulaValues(f, ws, sf, endRow, endCol);
sf.EndRow = endRow;
sf.EndCol = endCol;
-
+
}
FillArrayFromRangeInfo(f, array, rd, depChain);
return dirtyRange;
@@ -212,10 +247,10 @@ private static void CleanupSharedFormulaValues(RpnFormula f, ExcelWorksheet ws,
}
}
- private static SimpleAddress[] GetDirtyRange(int fromRow, int fromCol, int toRow, int toCol, int prevToRow=0, int prevToCol=0)
+ private static SimpleAddress[] GetDirtyRange(int fromRow, int fromCol, int toRow, int toCol, int prevToRow = 0, int prevToCol = 0)
{
- if(prevToRow == 0) prevToRow = fromRow;
- if(prevToCol == 0) prevToCol = fromCol;
+ if (prevToRow == 0) prevToRow = fromRow;
+ if (prevToCol == 0) prevToCol = fromCol;
if (prevToRow == toRow && prevToCol == toCol)
{
return new SimpleAddress[0];
@@ -226,15 +261,15 @@ private static SimpleAddress[] GetDirtyRange(int fromRow, int fromCol, int toRow
{
new SimpleAddress(fromRow, Math.Min(prevToCol+1, toCol+1), Math.Min(prevToRow, toRow), Math.Max(prevToCol, toCol)),
new SimpleAddress(Math.Min(prevToRow + 1, toRow + 1), fromCol, Math.Max(prevToRow, toRow), Math.Max(prevToCol, toCol))
- };
+ };
}
- else if(prevToRow != toRow)
+ else if (prevToRow != toRow)
{
- return new SimpleAddress[] { new SimpleAddress(Math.Min(prevToRow+1, toRow), fromCol, Math.Max(prevToRow, toRow), Math.Max(prevToCol,toCol)) };
- }
+ return new SimpleAddress[] { new SimpleAddress(Math.Min(prevToRow + 1, toRow), fromCol, Math.Max(prevToRow, toRow), Math.Max(prevToCol, toCol)) };
+ }
else
{
- return new SimpleAddress[] { new SimpleAddress(fromRow, Math.Min(prevToCol+1, toCol), Math.Max(prevToRow, toRow), Math.Max(prevToCol, toCol)) };
+ return new SimpleAddress[] { new SimpleAddress(fromRow, Math.Min(prevToCol + 1, toCol), Math.Max(prevToRow, toRow), Math.Max(prevToCol, toCol)) };
}
}
diff --git a/src/EPPlus/FormulaParsing/DependencyChain/RpnFormulaExecution.cs b/src/EPPlus/FormulaParsing/DependencyChain/RpnFormulaExecution.cs
index 2fb9c29c64..42e29e4c85 100644
--- a/src/EPPlus/FormulaParsing/DependencyChain/RpnFormulaExecution.cs
+++ b/src/EPPlus/FormulaParsing/DependencyChain/RpnFormulaExecution.cs
@@ -46,6 +46,9 @@ internal static RpnOptimizedDependencyChain Execute(ExcelWorkbook wb, ExcelCalcu
var depChain = new RpnOptimizedDependencyChain(wb, options);
foreach (var ws in wb.Worksheets)
{
+#if !NET35
+ options.CancellationToken.ThrowIfCancellationRequested();
+#endif
if (ws.IsChartSheet == false)
{
ExecuteChain(depChain, ws.Cells, options, true);
@@ -118,9 +121,15 @@ private static void ExecuteChain(RpnOptimizedDependencyChain depChain, ExcelRang
{
var ws = range.Worksheet;
RpnFormula f = null;
+#if !NET35
+ var ct = options.CancellationToken; // Cache locally — avoids property lookup in hot loop
+#endif
var fs = new CellStoreEnumerator(ws._formulas, range._fromRow, range._fromCol, range._toRow, range._toCol);
while (fs.Next())
{
+#if !NET35
+ ct.ThrowIfCancellationRequested(); // P0 – per cell
+#endif
if (fs.Value == null || fs.Value.ToString().Trim() == "") continue;
var id = ExcelCellBase.GetCellId(ws.IndexInList, fs.Row, fs.Column);
if (depChain.processedCells.Contains(id) == false)
@@ -132,6 +141,12 @@ private static void ExecuteChain(RpnOptimizedDependencyChain depChain, ExcelRang
CalculateFormulaChain(depChain, f, options, writeToCell);
}
}
+#if !NET35
+ catch (OperationCanceledException)
+ {
+ throw; // Must propagate — do not swallow
+ }
+#endif
catch (CircularReferenceException)
{
throw;
@@ -240,6 +255,12 @@ private static void ExecuteChain(RpnOptimizedDependencyChain depChain, ExcelName
ExecuteName(depChain, name, options, writeToCell);
}
}
+#if !NET35
+ catch (OperationCanceledException)
+ {
+ throw; // Must propagate
+ }
+#endif
catch (CircularReferenceException)
{
throw;
@@ -289,6 +310,12 @@ private static object ExecuteChain(RpnOptimizedDependencyChain depChain, ExcelWo
f.SetFormula(formula, depChain);
return CalculateFormulaChain(depChain, f, options, writeToCell).Result;
}
+#if !NET35
+ catch (OperationCanceledException)
+ {
+ throw; // Must propagate
+ }
+#endif
catch (CircularReferenceException)
{
throw;
@@ -309,6 +336,12 @@ private static object ExecuteChain(RpnOptimizedDependencyChain depChain, ExcelWo
f._row = -1;
return CalculateFormulaChain(depChain, f, options, writeToCell).Result;
}
+#if !NET35
+ catch (OperationCanceledException)
+ {
+ throw; // Must propagate
+ }
+#endif
catch (CircularReferenceException)
{
throw;
@@ -419,6 +452,9 @@ private static CompileResult CalculateFormulaChain(RpnOptimizedDependencyChain d
ExecuteFormula:
try
{
+#if !NET35
+ options.CancellationToken.ThrowIfCancellationRequested(); // P0 – per dependency step
+#endif
SetCurrentCell(depChain, f);
var ws = f._ws;
if (f._tokenIndex < f._tokens.Count)
@@ -568,6 +604,12 @@ private static CompileResult CalculateFormulaChain(RpnOptimizedDependencyChain d
goto ExecuteFormula;
}
+#if !NET35
+ catch (OperationCanceledException)
+ {
+ throw; // Must propagate
+ }
+#endif
catch (CircularReferenceException)
{
throw;
@@ -1562,7 +1604,7 @@ private static bool PreExecFunc(RpnOptimizedDependencyChain depChain, RpnFormula
{
args = CompileFunctionArguments(f, funcExp);
funcExp.Status = ExpressionStatus.CanCompile;
- return funcExp.SetArguments(args);
+ return funcExp.SetArguments(args, depChain._parsingContext);
}
else
{
@@ -1577,7 +1619,7 @@ private static bool PreExecFunc(RpnOptimizedDependencyChain depChain, RpnFormula
else
{
args = CompileFunctionArguments(f, funcExp);
- return funcExp.SetArguments(args);
+ return funcExp.SetArguments(args, depChain._parsingContext);
}
return false;
}
diff --git a/src/EPPlus/FormulaParsing/EpplusExcelDataProvider.cs b/src/EPPlus/FormulaParsing/EpplusExcelDataProvider.cs
index 922835df62..c51ca54c19 100644
--- a/src/EPPlus/FormulaParsing/EpplusExcelDataProvider.cs
+++ b/src/EPPlus/FormulaParsing/EpplusExcelDataProvider.cs
@@ -24,6 +24,7 @@ Date Author Change
using OfficeOpenXml.FormulaParsing.Ranges;
using System.Runtime.InteropServices;
using OfficeOpenXml.Utils.String;
+using OfficeOpenXml.Style;
namespace OfficeOpenXml.FormulaParsing
{
@@ -641,7 +642,21 @@ public override string GetFormat(object value, string format, out bool isValidFo
{
isValidFormat = true;
var ws = _currentWorksheet ?? _context.CurrentWorksheet;
- var arg = new NumberFormatToTextArgs(ws, _context.CurrentCell.Row, _context.CurrentCell.Column, value, ws.GetStyleInner(_context.CurrentCell.Row, _context.CurrentCell.Column));
+
+ var existingId = ExcelNumberFormat.GetFromBuildIdFromFormat(format);
+
+ NumberFormatToTextArgs arg;
+ if (existingId == int.MinValue)
+ {
+ //The format does not have a corresponding styleId
+ //Still allow the NumberFormatToTextHandler to see the format
+ arg = new NumberFormatToTextArgs(ws, _context.CurrentCell.Row, _context.CurrentCell.Column, value, format);
+ }
+ else
+ {
+ arg = new NumberFormatToTextArgs(ws, _context.CurrentCell.Row, _context.CurrentCell.Column, value, existingId);
+ }
+ arg.FromFormula = true;
return _workbook.NumberFormatToTextHandler(arg);
}
}
diff --git a/src/EPPlus/FormulaParsing/Excel/Functions/ExcelFunction.cs b/src/EPPlus/FormulaParsing/Excel/Functions/ExcelFunction.cs
index 7ce9e6d1af..e6de1e09db 100644
--- a/src/EPPlus/FormulaParsing/Excel/Functions/ExcelFunction.cs
+++ b/src/EPPlus/FormulaParsing/Excel/Functions/ExcelFunction.cs
@@ -122,8 +122,9 @@ internal CompileResult ExecuteInternal(IList arguments, Parsin
///
/// The function arguments that will be supplied to the execute method.
/// The index of the argument that should be adjusted.
+ /// The parsing context
/// A queue of addresses that will be calculated before calling the Execute function.
- public virtual void GetNewParameterAddress(IList args, int index, ref Queue addresses)
+ public virtual void GetNewParameterAddress(IList args, int index, ParsingContext ctx, ref Queue addresses)
{
}
diff --git a/src/EPPlus/FormulaParsing/Excel/Functions/MathFunctions/AverageIf.cs b/src/EPPlus/FormulaParsing/Excel/Functions/MathFunctions/AverageIf.cs
index 99f8fbb944..6fc9f8bdd1 100644
--- a/src/EPPlus/FormulaParsing/Excel/Functions/MathFunctions/AverageIf.cs
+++ b/src/EPPlus/FormulaParsing/Excel/Functions/MathFunctions/AverageIf.cs
@@ -42,14 +42,14 @@ public override void ConfigureArrayBehaviour(ArrayBehaviourConfig config)
{
config.SetArrayParameterIndexes(1);
}
- public override void GetNewParameterAddress(IList args, int index, ref Queue addresses)
+ public override void GetNewParameterAddress(IList args, int index, ParsingContext ctx, ref Queue addresses)
{
if (index == 2)
{
if (args[0].Result is IRangeInfo rangeInfo && args[2].Result is IRangeInfo valueRange)
{
var rv = new RangeOrValue { Range = rangeInfo };
- var mi = GetMatchIndexes(rv, args[1].Result, null);
+ var mi = GetMatchIndexes(rv, args[1].Result, ctx);
EnqueueMatchingAddresses(valueRange, mi, ref addresses);
}
}
diff --git a/src/EPPlus/FormulaParsing/Excel/Functions/MathFunctions/AverageIfs.cs b/src/EPPlus/FormulaParsing/Excel/Functions/MathFunctions/AverageIfs.cs
index 7ecd607ca4..50c63d6aec 100644
--- a/src/EPPlus/FormulaParsing/Excel/Functions/MathFunctions/AverageIfs.cs
+++ b/src/EPPlus/FormulaParsing/Excel/Functions/MathFunctions/AverageIfs.cs
@@ -50,19 +50,21 @@ internal class AverageIfs : RangeCriteriaFunction
}
return FunctionParameterInformation.AdjustParameterAddress;
}));
- public override void GetNewParameterAddress(IList args, int index, ref Queue addresses)
+
+ public override void GetNewParameterAddress(IList args, int index, ParsingContext ctx, ref Queue addresses)
{
if (index == 0 && args[0].Result is IRangeInfo valueRange)
{
- IEnumerable matchIndexes = GetMatchingIndicesFromArguments(1, args);
+ IEnumerable matchIndexes = GetMatchingIndicesFromArguments(1, args, ctx, convertNumericStrings: false);
EnqueueMatchingAddresses(valueRange, matchIndexes, ref addresses);
}
else if (args[index].Result is IRangeInfo criteriaRange)
{
- IEnumerable matchIndexes = GetMatchingIndicesFromArguments(1, args, index);
+ IEnumerable matchIndexes = GetMatchingIndicesFromArguments(1, args, ctx, index, convertNumericStrings: false);
EnqueueMatchingAddresses(criteriaRange, matchIndexes, ref addresses);
}
}
+
public override CompileResult Execute(IList arguments, ParsingContext context)
{
var valueRange = arguments[0].ValueAsRangeInfo;
@@ -103,19 +105,13 @@ public override CompileResult Execute(IList arguments, Parsing
}
private double GetAvgValue(ParsingContext context, IRangeInfo valueRange, List argRanges, List criteria, int row, int col, out ExcelErrorValue ev)
{
- IEnumerable matchIndexes = GetMatchIndexes(argRanges[0], GetCriteriaValue(criteria[0], row, col), context, false);
- var enumerable = matchIndexes as IList ?? matchIndexes.ToList();
- for (var ix = 1; ix < argRanges.Count && enumerable.Any(); ix++)
- {
- var indexes = GetMatchIndexes(argRanges[ix], GetCriteriaValue(criteria[ix], row, col), context, false);
- matchIndexes = matchIndexes.Intersect(indexes);
- }
- var sumRange = RangeFlattener.FlattenRangeObject(valueRange);
+ GetFilteredValueRange(context, valueRange, argRanges, criteria, row, col, out List matchIndexes, out List flattenedRange, convertNumericString: false);
+
KahanSum sum = 0d;
var count = 0;
foreach (var index in matchIndexes)
{
- var obj = sumRange[index];
+ var obj = flattenedRange[index];
if (obj is ExcelErrorValue e1)
{
ev = e1;
@@ -127,14 +123,15 @@ private double GetAvgValue(ParsingContext context, IRangeInfo valueRange, List args, int index, ref Queue addresses)
+ public override void GetNewParameterAddress(IList args, int index, ParsingContext ctx, ref Queue addresses)
{
if (args[index].Result is IRangeInfo criteriaRange)
{
- IEnumerable matchIndexes = GetMatchingIndicesFromArguments(0, args, index);
+ IEnumerable matchIndexes = GetMatchingIndicesFromArguments(0, args, ctx, index);
EnqueueMatchingAddresses(criteriaRange, matchIndexes, ref addresses);
}
}
@@ -76,16 +76,24 @@ public override CompileResult Execute(IList arguments, Parsing
}
}
+
private double GetCountValue(ParsingContext context, List argRanges, List criteria, int row, int col)
{
- IEnumerable matchIndexes = GetMatchIndexes(argRanges[0], GetCriteriaValue(criteria[0], row, col), context, false);
- var enumerable = matchIndexes as IList ?? matchIndexes.ToList();
- for (var ix = 1; ix < argRanges.Count && enumerable.Any(); ix++)
+ var matchIndexes = GetMatchIndexes(argRanges[0], GetCriteriaValue(criteria[0], row, col), context, false);
+
+ // Use HashSet.IntersectWith for multi-criteria (more efficient than LINQ Intersect)
+ if (argRanges.Count > 1)
{
- var indexes = GetMatchIndexes(argRanges[ix], GetCriteriaValue(criteria[ix], row, col), context, false);
- matchIndexes = matchIndexes.Intersect(indexes);
+ var hashSet = new HashSet(matchIndexes);
+ for (var ix = 1; ix < argRanges.Count && hashSet.Count > 0; ix++)
+ {
+ var indexes = GetMatchIndexes(argRanges[ix], GetCriteriaValue(criteria[ix], row, col), context, false);
+ hashSet.IntersectWith(indexes);
+ }
+ return (double)hashSet.Count;
}
- return (double)matchIndexes.Count();
+
+ return (double)matchIndexes.Count;
}
}
}
diff --git a/src/EPPlus/FormulaParsing/Excel/Functions/MathFunctions/RangeCriteriaCache.cs b/src/EPPlus/FormulaParsing/Excel/Functions/MathFunctions/RangeCriteriaCache.cs
new file mode 100644
index 0000000000..2728c40196
--- /dev/null
+++ b/src/EPPlus/FormulaParsing/Excel/Functions/MathFunctions/RangeCriteriaCache.cs
@@ -0,0 +1,244 @@
+/*************************************************************************************************
+ Required Notice: Copyright (C) EPPlus Software AB.
+ This software is licensed under PolyForm Noncommercial License 1.0.0
+ and may only be used for noncommercial purposes
+ https://polyformproject.org/licenses/noncommercial/1.0.0/
+
+ A commercial license to use this software can be purchased at https://epplussoftware.com
+ *************************************************************************************************
+ Date Author Change
+ *************************************************************************************************
+ 02/09/2026 EPPlus Software AB RangeCriteria performance optimization
+ *************************************************************************************************/
+using System.Collections.Generic;
+using OfficeOpenXml.FormulaParsing.LexicalAnalysis;
+
+namespace OfficeOpenXml.FormulaParsing.Excel.Functions.MathFunctions
+{
+ ///
+ /// Cache for RangeCriteria functions (SumIfs, AverageIfs, CountIfs) to improve performance
+ /// during calculation by avoiding repeated expensive operations.
+ /// Uses FIFO eviction policy to limit memory usage and integer keys for efficient lookups.
+ ///
+ internal class RangeCriteriaCache
+ {
+ internal const int DEFAULT_MAX_FLATTENED_RANGES = 100;
+ internal const int DEFAULT_MAX_MATCH_INDEXES = 1000;
+
+ private readonly ExcelPackage _package;
+ private readonly ExcelCalculationCacheSettings _settings;
+
+ private Dictionary> _flattenedRanges;
+ private Dictionary> _matchIndexes;
+ private HashSet _rangesWithFormulas;
+
+ private Queue _flattenedRangesOrder;
+ private Queue _matchIndexesOrder;
+
+ private Dictionary _keyToId;
+ private int _nextId = 1;
+
+ ///
+ /// Creates a new RangeCriteriaCache with specified limits
+ ///
+ /// The ExcelPackage where the cache operates.
+ public RangeCriteriaCache(ExcelPackage package)
+ {
+ _package = package;
+ if(package != null)
+ {
+ _settings = package.Settings.CalculationCacheSettings;
+ }
+ else
+ {
+ _settings = ExcelCalculationCacheSettings.Default;
+ }
+ }
+
+ ///
+ /// Gets a flattened range from cache, or null if not cached
+ ///
+ public List GetFlattenedRange(FormulaRangeAddress address)
+ {
+ if (_flattenedRanges == null || address == null) return null;
+
+ var key = CreateRangeKey(address);
+ if (_keyToId != null && _keyToId.TryGetValue(key, out var id))
+ {
+ if (_flattenedRanges.TryGetValue(id, out var cached))
+ {
+ return cached;
+ }
+ }
+ return null;
+ }
+
+ ///
+ /// Stores a flattened range in cache with FIFO eviction if cache is full
+ ///
+ public void SetFlattenedRange(FormulaRangeAddress address, List flattenedRange)
+ {
+ if (address == null || flattenedRange == null) return;
+
+ // Guard against disabled caching
+ var maxCapacity = _settings?.MaxFlattenedRanges ?? DEFAULT_MAX_FLATTENED_RANGES;
+ if (maxCapacity <= 0) return;
+
+ if (_flattenedRanges == null)
+ {
+ _flattenedRanges = new Dictionary>();
+ _flattenedRangesOrder = new Queue();
+ _keyToId = new Dictionary();
+ }
+
+ var key = CreateRangeKey(address);
+
+ if (!_keyToId.TryGetValue(key, out var id))
+ {
+ id = _nextId++;
+ _keyToId[key] = id;
+
+ // Evict entries until we're under the limit
+ // This handles both: cache full + capacity reduced scenarios
+ while (_flattenedRanges.Count >= maxCapacity)
+ {
+ var oldestId = _flattenedRangesOrder.Dequeue();
+ _flattenedRanges.Remove(oldestId);
+ }
+
+ _flattenedRangesOrder.Enqueue(id);
+ }
+
+ _flattenedRanges[id] = flattenedRange;
+ }
+
+ ///
+ /// Gets cached match indexes for a range+criteria combination.
+ /// Returns null if the range contains formulas (which might change during calculation).
+ ///
+ public List GetMatchIndexes(FormulaRangeAddress rangeAddress, object criteriaValue)
+ {
+ if (_matchIndexes == null || rangeAddress == null || criteriaValue == null) return null;
+
+ var rangeKey = CreateRangeKey(rangeAddress);
+ if (_keyToId != null && _keyToId.TryGetValue(rangeKey, out var rangeId))
+ {
+ // Don't use cache if this range contains formulas that might change
+ if (_rangesWithFormulas != null && _rangesWithFormulas.Contains(rangeId))
+ {
+ return null;
+ }
+ }
+
+ var key = CreateMatchIndexKey(rangeAddress, criteriaValue);
+ if (_keyToId != null && _keyToId.TryGetValue(key, out var id))
+ {
+ if (_matchIndexes.TryGetValue(id, out var cached))
+ {
+ return cached;
+ }
+ }
+ return null;
+ }
+
+ ///
+ /// Stores match indexes for a range+criteria combination with FIFO eviction if cache is full
+ ///
+ ///
+ /// Stores match indexes for a range+criteria combination with FIFO eviction if cache is full
+ ///
+ public void SetMatchIndexes(FormulaRangeAddress rangeAddress, object criteriaValue, List matchIndexes)
+ {
+ if (rangeAddress == null || criteriaValue == null || matchIndexes == null) return;
+
+ // Guard against disabled caching
+ var maxCapacity = _settings?.MaxMatchIndexes ?? DEFAULT_MAX_MATCH_INDEXES;
+ if (maxCapacity <= 0) return;
+
+ if (_keyToId == null)
+ {
+ _keyToId = new Dictionary();
+ }
+
+ // Track if this range has formulas
+ var rangeKey = CreateRangeKey(rangeAddress);
+ if (rangeAddress.HasFormulas(_package))
+ {
+ if (!_keyToId.TryGetValue(rangeKey, out var rangeId))
+ {
+ rangeId = _nextId++;
+ _keyToId[rangeKey] = rangeId;
+ }
+
+ if (_rangesWithFormulas == null)
+ {
+ _rangesWithFormulas = new HashSet();
+ }
+ _rangesWithFormulas.Add(rangeId);
+ // Don't cache matchIndexes for ranges with formulas
+ return;
+ }
+
+ if (_matchIndexes == null)
+ {
+ _matchIndexes = new Dictionary>();
+ _matchIndexesOrder = new Queue();
+ }
+
+ var key = CreateMatchIndexKey(rangeAddress, criteriaValue);
+
+ // Get or create ID for this key
+ if (!_keyToId.TryGetValue(key, out var id))
+ {
+ id = _nextId++;
+ _keyToId[key] = id;
+
+ // Evict entries until we're under the limit
+ // This handles both: cache full + capacity reduced scenarios
+ while (_matchIndexes.Count >= maxCapacity)
+ {
+ var oldestId = _matchIndexesOrder.Dequeue();
+ _matchIndexes.Remove(oldestId);
+ // Note: we keep the key in _keyToId to avoid ID reuse within same calculation
+ }
+
+ _matchIndexesOrder.Enqueue(id);
+ }
+
+ _matchIndexes[id] = matchIndexes;
+ }
+
+ ///
+ /// Clears all cached data. Should be called after calculation completes.
+ ///
+ public void Clear()
+ {
+ _flattenedRanges?.Clear();
+ _matchIndexes?.Clear();
+ _rangesWithFormulas?.Clear();
+ _flattenedRangesOrder?.Clear();
+ _matchIndexesOrder?.Clear();
+ _keyToId?.Clear();
+ _nextId = 1;
+ }
+
+ private string CreateRangeKey(FormulaRangeAddress address)
+ {
+ // Create a unique key for the range address
+ return $"{address.WorksheetIx}:{address.FromRow}:{address.FromCol}:{address.ToRow}:{address.ToCol}";
+ }
+
+ private string CreateMatchIndexKey(FormulaRangeAddress rangeAddress, object criteriaValue)
+ {
+ // Create a unique key combining range address and criteria value
+ var rangeKey = CreateRangeKey(rangeAddress);
+ var cv = criteriaValue;
+ if(criteriaValue is RangeOrValue rov)
+ {
+ cv = rov.Value != null ? rov.Value : rov?.Range?.Address.ToString() ?? "null";
+ }
+ var criteriaKey = cv?.ToString() ?? "null";
+ return $"{rangeKey}|{criteriaKey}";
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/EPPlus/FormulaParsing/Excel/Functions/MathFunctions/RangeCriteriaFunction.cs b/src/EPPlus/FormulaParsing/Excel/Functions/MathFunctions/RangeCriteriaFunction.cs
index 8c43cc6208..69014fcdf0 100644
--- a/src/EPPlus/FormulaParsing/Excel/Functions/MathFunctions/RangeCriteriaFunction.cs
+++ b/src/EPPlus/FormulaParsing/Excel/Functions/MathFunctions/RangeCriteriaFunction.cs
@@ -10,10 +10,7 @@ Date Author Change
*************************************************************************************************
01/27/2020 EPPlus Software AB Initial release EPPlus 5
*************************************************************************************************/
-using System;
-using System.Collections.Generic;
-using System.Linq;
-using System.Text;
+using OfficeOpenXml.FormulaParsing.Excel.Functions.RefAndLookup;
using OfficeOpenXml.FormulaParsing.ExcelUtilities;
using OfficeOpenXml.FormulaParsing.FormulaExpressions;
using OfficeOpenXml.FormulaParsing.LexicalAnalysis;
@@ -21,6 +18,10 @@ Date Author Change
using OfficeOpenXml.FormulaParsing.Utilities;
using OfficeOpenXml.Sorting.Internal;
using OfficeOpenXml.Utils.TypeConversion;
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
using Require = OfficeOpenXml.FormulaParsing.Utilities.Require;
@@ -45,7 +46,7 @@ protected static void GetArguments(ParsingContext context, IList GetMatchIndexes(RangeOrValue rangeOrValue, object searched, ParsingContext ctx, bool convertNumericString = true)
{
- var expressionEvaluator = new ExpressionEvaluator(ctx);
+ // Try to get from cache if we have a range with address
+ if (rangeOrValue.Range != null && rangeOrValue.Range.Address != null && searched != null)
+ {
+ var cached = ctx.RangeCriteriaCache?.GetMatchIndexes(rangeOrValue.Range.Address, searched);
+ if (cached != null)
+ {
+ return cached;
+ }
+ }
+
var result = new List();
var internalIndex = 0;
+
if (rangeOrValue.Range != null)
{
var rangeInfo = rangeOrValue.Range;
var address = rangeInfo.GetAddressDimensionAdjusted(0).Address;
+
for (var row = address.FromRow; row <= address.ToRow; row++)
{
for (var col = address.FromCol; col <= address.ToCol; col++)
@@ -165,6 +178,7 @@ protected List GetMatchIndexes(RangeOrValue rangeOrValue, object searched,
{
if (searched is RangeOrValue critRange)
{
+ // Handle range criteria (less common case) - use original Evaluate
if (critRange.Range != null)
{
foreach (var cell in critRange.Range)
@@ -175,7 +189,7 @@ protected List GetMatchIndexes(RangeOrValue rangeOrValue, object searched,
}
}
}
- else if(critRange.Value != null && Evaluate(candidate, critRange.Value, ctx, convertNumericString))
+ else if (critRange.Value != null && Evaluate(candidate, critRange.Value, ctx, convertNumericString))
{
result.Add(internalIndex);
}
@@ -190,23 +204,32 @@ protected List GetMatchIndexes(RangeOrValue rangeOrValue, object searched,
}
}
}
- else if (Evaluate(rangeOrValue.Value, searched, ctx, convertNumericString))
+ else if (searched != null && Evaluate(rangeOrValue.Value, searched, ctx, convertNumericString))
{
result.Add(internalIndex);
}
+
+ // Cache the result if we have a range with address
+ // Pass rangeHasFormulas flag so cache knows whether to store it
+ if (rangeOrValue.Range != null && rangeOrValue.Range.Address != null && searched != null)
+ {
+ ctx.RangeCriteriaCache?.SetMatchIndexes(rangeOrValue.Range.Address, searched, result);
+ }
+
return result;
}
+
protected static Queue EnqueueMatchingAddresses(IRangeInfo valueRange, IEnumerable matchIndexes, ref Queue addresses)
{
- if(addresses==null)
+ if (addresses == null)
{
- addresses=new Queue();
+ addresses = new Queue();
}
var pIx = int.MinValue;
var valueAddress = valueRange.GetAddressDimensionAdjusted(0);
var extRef = valueAddress.ExternalReferenceIx;
var wsIx = valueAddress.WorksheetIx;
- FormulaRangeAddress currentAddress=null;
+ FormulaRangeAddress currentAddress = null;
if (valueAddress.FromCol == valueAddress.ToCol)
{
var c = valueAddress.FromCol;
@@ -246,7 +269,7 @@ protected static Queue EnqueueMatchingAddresses(IRangeInfo
return addresses;
}
- protected IEnumerable GetMatchingIndicesFromArguments(int argStartIx, IList args, int maxIndex=31)
+ protected IEnumerable GetMatchingIndicesFromArguments(int argStartIx, IList args, ParsingContext ctx, int maxIndex = 31, bool convertNumericStrings = true)
{
//Return the addresses matching the criteria in the queue
var argRanges = new List();
@@ -273,16 +296,58 @@ protected IEnumerable GetMatchingIndicesFromArguments(int argStartIx, IList
criteria.Add(new RangeOrValue { Value = args[ix + 1].ResultValue });
}
}
- IEnumerable matchIndexes = GetMatchIndexes(argRanges[0], criteria[0], null);
+ IEnumerable matchIndexes = GetMatchIndexes(argRanges[0], criteria[0], ctx, convertNumericStrings);
var enumerable = matchIndexes as IList ?? matchIndexes.ToList();
for (var ix = 1; ix < argRanges.Count && enumerable.Any(); ix++)
{
- var indexes = GetMatchIndexes(argRanges[ix], criteria[ix], null);
+ var indexes = GetMatchIndexes(argRanges[ix], criteria[ix], ctx, convertNumericStrings);
matchIndexes = matchIndexes.Intersect(indexes);
}
return matchIndexes;
}
+ protected void GetFilteredValueRange(
+ ParsingContext context,
+ IRangeInfo valueRange,
+ List argRanges,
+ List criterias,
+ int row,
+ int col,
+ out List matchIndexes,
+ out List flattenedRange,
+ bool convertNumericString = true)
+ {
+ matchIndexes = GetMatchIndexes(argRanges[0], GetCriteriaValue(criterias[0], row, col), context, convertNumericString);
+
+ if (argRanges.Count > 1)
+ {
+ var hashSet = new HashSet(matchIndexes);
+ for (var ix = 1; ix < argRanges.Count && hashSet.Count > 0; ix++)
+ {
+ var indexes = GetMatchIndexes(argRanges[ix], GetCriteriaValue(criterias[ix], row, col), context, convertNumericString);
+ hashSet.IntersectWith(indexes);
+ }
+ matchIndexes = hashSet.ToList();
+ }
+
+ // Try to get flattened range from cache only if it doesn't have formulas
+ var valueAddress = valueRange.Address;
+ if (valueAddress != null && !valueAddress.HasFormulas(context.Package))
+ {
+ flattenedRange = context.RangeCriteriaCache?.GetFlattenedRange(valueAddress);
+ if (flattenedRange == null)
+ {
+ // Not in cache, flatten and cache it
+ flattenedRange = RangeFlattener.FlattenRangeObject(valueRange);
+ context.RangeCriteriaCache?.SetFlattenedRange(valueAddress, flattenedRange);
+ }
+ }
+ else
+ {
+ // Has formulas or no address - don't cache, always flatten fresh
+ flattenedRange = RangeFlattener.FlattenRangeObject(valueRange);
+ }
+ }
}
-}
+}
\ No newline at end of file
diff --git a/src/EPPlus/FormulaParsing/Excel/Functions/MathFunctions/SumIf.cs b/src/EPPlus/FormulaParsing/Excel/Functions/MathFunctions/SumIf.cs
index e2a9b4db75..773960b963 100644
--- a/src/EPPlus/FormulaParsing/Excel/Functions/MathFunctions/SumIf.cs
+++ b/src/EPPlus/FormulaParsing/Excel/Functions/MathFunctions/SumIf.cs
@@ -41,14 +41,14 @@ public override void ConfigureArrayBehaviour(ArrayBehaviourConfig config)
{
config.SetArrayParameterIndexes(1);
}
- public override void GetNewParameterAddress(IList args, int index, ref Queue addresses)
+ public override void GetNewParameterAddress(IList args, int index, ParsingContext ctx, ref Queue addresses)
{
if(index == 2)
{
if (args[0].Result is IRangeInfo criteriaRange && args[2].Result is IRangeInfo valueRange)
{
var rv = new RangeOrValue { Range = criteriaRange };
- var mi=GetMatchIndexes(rv, args[1].Result, null);
+ var mi=GetMatchIndexes(rv, args[1].Result, ctx);
addresses = EnqueueMatchingAddresses(valueRange, mi, ref addresses);
}
}
diff --git a/src/EPPlus/FormulaParsing/Excel/Functions/MathFunctions/SumIfs.cs b/src/EPPlus/FormulaParsing/Excel/Functions/MathFunctions/SumIfs.cs
index 57f208821b..2e10bde7d2 100644
--- a/src/EPPlus/FormulaParsing/Excel/Functions/MathFunctions/SumIfs.cs
+++ b/src/EPPlus/FormulaParsing/Excel/Functions/MathFunctions/SumIfs.cs
@@ -21,6 +21,7 @@ Date Author Change
using OfficeOpenXml.FormulaParsing.LexicalAnalysis;
using OfficeOpenXml.FormulaParsing.Ranges;
using OfficeOpenXml.Utils;
+using System.Diagnostics;
namespace OfficeOpenXml.FormulaParsing.Excel.Functions.MathFunctions
{
@@ -49,34 +50,36 @@ public override void ConfigureArrayBehaviour(ArrayBehaviourConfig config)
{
return FunctionParameterInformation.IgnoreErrorInPreExecute;
}
- else if(argumentIndex==1)
+ else if (argumentIndex == 1)
{
return FunctionParameterInformation.Normal;
}
return FunctionParameterInformation.AdjustParameterAddress;
}));
- public override void GetNewParameterAddress(IList args, int index, ref Queue addresses)
+
+ public override void GetNewParameterAddress(IList args, int index, ParsingContext ctx, ref Queue addresses)
{
if (index == 0 && args[0].Result is IRangeInfo valueRange)
{
- IEnumerable matchIndexes = GetMatchingIndicesFromArguments(1, args);
+ IEnumerable matchIndexes = GetMatchingIndicesFromArguments(1, args, ctx);
addresses = EnqueueMatchingAddresses(valueRange, matchIndexes, ref addresses);
}
- else if(args[index].Result is IRangeInfo criteriaRange)
+ else if (args[index].Result is IRangeInfo criteriaRange)
{
- IEnumerable matchIndexes = GetMatchingIndicesFromArguments(1, args, index);
+ IEnumerable matchIndexes = GetMatchingIndicesFromArguments(1, args, ctx, index);
addresses = EnqueueMatchingAddresses(criteriaRange, matchIndexes, ref addresses);
}
}
public override CompileResult Execute(IList arguments, ParsingContext context)
{
- var valueRange = arguments[0].ValueAsRangeInfo;
+ var valueRange = arguments[0].ValueAsRangeInfo;
GetArguments(context, arguments, out List argRanges, out List criteria, out int cols, out int rows, 1);
-
+
if (cols == 1 && rows == 1)
{
var result = GetSumValue(context, valueRange, argRanges, criteria, 0, 0, out ExcelErrorValue ev);
+
if (double.IsNaN(result) && ev != null)
{
return CreateResult(ev, DataType.ExcelError);
@@ -108,16 +111,12 @@ public override CompileResult Execute(IList arguments, Parsing
}
}
+
private double GetSumValue(ParsingContext context, IRangeInfo valueRange, List argRanges, List criterias, int row, int col, out ExcelErrorValue ev)
{
- IEnumerable matchIndexes = GetMatchIndexes(argRanges[0], GetCriteriaValue(criterias[0], row, col), context);
- var enumerable = matchIndexes as IList ?? matchIndexes.ToList();
- for (var ix = 1; ix < argRanges.Count && enumerable.Any(); ix++)
- {
- var indexes = GetMatchIndexes(argRanges[ix], GetCriteriaValue(criterias[ix], row, col), context);
- matchIndexes = matchIndexes.Intersect(indexes);
- }
- var sumRange = RangeFlattener.FlattenRangeObject(valueRange);
+
+ GetFilteredValueRange(context, valueRange, argRanges, criterias, row, col, out List matchIndexes, out List sumRange);
+
KahanSum result = 0d;
foreach (var index in matchIndexes)
{
diff --git a/src/EPPlus/FormulaParsing/Excel/Functions/RefAndLookup/HLookup.cs b/src/EPPlus/FormulaParsing/Excel/Functions/RefAndLookup/HLookup.cs
index 1ae2f96cee..82ee6f3018 100644
--- a/src/EPPlus/FormulaParsing/Excel/Functions/RefAndLookup/HLookup.cs
+++ b/src/EPPlus/FormulaParsing/Excel/Functions/RefAndLookup/HLookup.cs
@@ -83,7 +83,7 @@ public override CompileResult Execute(IList arguments, Parsing
}
return CompileResultFactory.Create(lookupRange.GetOffset(lookupIndex - 1, index));
}
- public override void GetNewParameterAddress(IList args, int index, ref Queue addresses)
+ public override void GetNewParameterAddress(IList args, int index, ParsingContext ctx, ref Queue addresses)
{
if (args.Count > 2)
{
diff --git a/src/EPPlus/FormulaParsing/Excel/Functions/RefAndLookup/LookupUtils/XlookupScanner.cs b/src/EPPlus/FormulaParsing/Excel/Functions/RefAndLookup/LookupUtils/XlookupScanner.cs
index 3e22613dfa..25f1a40600 100644
--- a/src/EPPlus/FormulaParsing/Excel/Functions/RefAndLookup/LookupUtils/XlookupScanner.cs
+++ b/src/EPPlus/FormulaParsing/Excel/Functions/RefAndLookup/LookupUtils/XlookupScanner.cs
@@ -150,16 +150,28 @@ private int FindIndexInternal()
return -1;
}
+
private int GetMaxItemsRow(IRangeInfo lookupRange)
{
+ var adjusted = lookupRange.GetAddressDimensionAdjusted(0);
+ if (adjusted != null)
+ {
+ return adjusted.ToRow - adjusted.FromRow + 1;
+ }
if (lookupRange.Address.ToRow > lookupRange.Dimension.ToRow)
{
return lookupRange.Dimension.ToRow - lookupRange.Address.FromRow + 1;
- }
+ }
return _lookupRange.Size.NumberOfRows;
}
+
private int GetMaxItemsColumns(IRangeInfo lookupRange)
{
+ var adjusted = lookupRange.GetAddressDimensionAdjusted(0);
+ if (adjusted != null)
+ {
+ return adjusted.ToCol - adjusted.FromCol + 1;
+ }
if (lookupRange.Address.ToCol > lookupRange.Dimension.ToCol)
{
return lookupRange.Dimension.ToCol - lookupRange.Address.FromCol + 1;
diff --git a/src/EPPlus/FormulaParsing/Excel/Functions/RefAndLookup/VLookup.cs b/src/EPPlus/FormulaParsing/Excel/Functions/RefAndLookup/VLookup.cs
index 465c5d9588..6f13a89f20 100644
--- a/src/EPPlus/FormulaParsing/Excel/Functions/RefAndLookup/VLookup.cs
+++ b/src/EPPlus/FormulaParsing/Excel/Functions/RefAndLookup/VLookup.cs
@@ -97,7 +97,7 @@ public override CompileResult Execute(IList arguments, Parsing
}
return CompileResultFactory.Create(lookupRange.GetOffset(index, lookupIndex - 1));
}
- public override void GetNewParameterAddress(IList args, int index, ref Queue addresses)
+ public override void GetNewParameterAddress(IList args, int index, ParsingContext ctx, ref Queue addresses)
{
if (args.Count > 2)
{
diff --git a/src/EPPlus/FormulaParsing/Excel/Operators/RangeOperationsOperator.cs b/src/EPPlus/FormulaParsing/Excel/Operators/RangeOperationsOperator.cs
index 4197fe5dbf..9fc72fe635 100644
--- a/src/EPPlus/FormulaParsing/Excel/Operators/RangeOperationsOperator.cs
+++ b/src/EPPlus/FormulaParsing/Excel/Operators/RangeOperationsOperator.cs
@@ -10,16 +10,11 @@ Date Author Change
*************************************************************************************************
05/30/2022 EPPlus Software AB EPPlus 6.1
*************************************************************************************************/
-using OfficeOpenXml.FormulaParsing.Excel.Functions.MathFunctions;
-using OfficeOpenXml.FormulaParsing.Excel.Functions.Text;
using OfficeOpenXml.FormulaParsing.FormulaExpressions;
using OfficeOpenXml.FormulaParsing.LexicalAnalysis;
using OfficeOpenXml.FormulaParsing.Ranges;
-using OfficeOpenXml.Utils;
using OfficeOpenXml.Utils.TypeConversion;
using System;
-using System.Globalization;
-using System.Linq;
namespace OfficeOpenXml.FormulaParsing.Excel.Operators
{
@@ -42,20 +37,32 @@ private static CompileResult ApplyOperator(CompileResult l, CompileResult r, Ope
internal static InMemoryRange Negate(IRangeInfo ri)
{
InMemoryRange imr;
- if(ri.IsInMemoryRange==false)
+ if (ri.IsInMemoryRange == false)
{
- imr = new InMemoryRange(ri.Size);
+ var physicalRows = RangeHelper.GetPhysicalRows(ri);
+ var logicalRows = ri.Size.NumberOfRows;
+ if (physicalRows < logicalRows)
+ {
+ imr = new InMemoryRange(
+ new RangeDefinition(physicalRows, ri.Size.NumberOfCols),
+ logicalRows);
+ }
+ else
+ {
+ imr = new InMemoryRange(ri.Size);
+ }
}
else
{
imr = (InMemoryRange)ri;
}
+ int rows = imr.PhysicalRows;
for (int c = 0; c < ri.Size.NumberOfCols; c++)
{
- for (int r = 0; r < ri.Size.NumberOfRows; r++)
+ for (int r = 0; r < rows; r++)
{
- var d = ConvertUtil.GetValueDouble(ri.GetOffset(r, c), true, true);
+ var d = ConvertUtil.GetValueDouble(ri.GetOffset(r, c), false, true);
if (double.IsNaN(d))
{
@@ -67,20 +74,62 @@ internal static InMemoryRange Negate(IRangeInfo ri)
}
}
}
+
+ if (imr.HasVirtualRows)
+ {
+ // Compute the negation of an empty cell: -(0) = 0
+ // Use the upstream default if one exists, otherwise null (= 0)
+ var srcDefault = imr.VirtualDefaultValue;
+ if (srcDefault == null)
+ {
+ srcDefault = 0d;
+ }
+ var d = ConvertUtil.GetValueDouble(srcDefault, false, false);
+ if (double.IsNaN(d))
+ {
+ imr.VirtualDefaultValue = ErrorValues.ValueError;
+ }
+ else
+ {
+ imr.VirtualDefaultValue = d == 0d ? 0d : -d;
+ }
+ }
+
return imr;
}
+
private static InMemoryRange CreateRange(IRangeInfo l, IRangeInfo r, FormulaRangeAddress address)
{
var width = Math.Max(l.Size.NumberOfCols, r.Size.NumberOfCols);
- var height = Math.Max(l.Size.NumberOfRows, r.Size.NumberOfRows);
- var rangeDef = new RangeDefinition(height, width);
- if(address != null)
+
+ int logicalHeight = Math.Max(l.Size.NumberOfRows, r.Size.NumberOfRows);
+ int physicalHeight = Math.Max(
+ RangeHelper.GetPhysicalRows(l),
+ RangeHelper.GetPhysicalRows(r));
+
+ if (physicalHeight >= logicalHeight)
+ {
+ // No virtual rows needed - existing behavior
+ var rangeDef = new RangeDefinition(logicalHeight, width);
+ if (address != null)
+ {
+ return new InMemoryRange(address, rangeDef);
+ }
+ else
+ {
+ return new InMemoryRange(rangeDef);
+ }
+ }
+
+ // Virtual range: small backing array, large logical size
+ var physicalDef = new RangeDefinition(physicalHeight, width);
+ if (address != null)
{
- return new InMemoryRange(address, rangeDef);
+ return new InMemoryRange(address, physicalDef, logicalHeight);
}
else
{
- return new InMemoryRange(rangeDef);
+ return new InMemoryRange(physicalDef, logicalHeight);
}
}
@@ -105,7 +154,7 @@ private static void SetValue(Operators op, InMemoryRange resultRange, int row, i
{
var res = ApplyOperator(leftVal, rightVal, op, out bool error, context);
var resultValue = res.ResultValue;
- if(!(resultValue is bool) && ConvertUtil.IsNumeric(resultValue) && res.ResultNumeric == 0d)
+ if (!(resultValue is bool) && ConvertUtil.IsNumeric(resultValue) && res.ResultNumeric == 0d)
{
// avoid -0 results.
resultValue = 0d;
@@ -115,7 +164,7 @@ private static void SetValue(Operators op, InMemoryRange resultRange, int row, i
private static bool ShouldUseSingleRow(RangeDefinition lSize, RangeDefinition rSize)
{
- if((lSize.NumberOfRows == 1 || rSize.NumberOfRows == 1) && lSize.NumberOfCols == rSize.NumberOfCols)
+ if ((lSize.NumberOfRows == 1 || rSize.NumberOfRows == 1) && lSize.NumberOfCols == rSize.NumberOfCols)
{
return true;
}
@@ -143,11 +192,11 @@ private static bool SingleRowSingleCol(RangeDefinition lSize, RangeDefinition rS
private static bool AddressIsNotAvailable(RangeDefinition lSize, RangeDefinition rSize, int row, int col)
{
- if(row >= lSize.NumberOfRows || row >=rSize.NumberOfRows)
+ if (row >= lSize.NumberOfRows || row >= rSize.NumberOfRows)
{
return true;
}
- else if(col >= lSize.NumberOfCols || col >= rSize.NumberOfCols)
+ else if (col >= lSize.NumberOfCols || col >= rSize.NumberOfCols)
{
return true;
}
@@ -193,22 +242,48 @@ private static object GetCellValue(IRangeInfo range, int rowOffset, int colOffse
catch
{
throw;
- }
+ }
+ }
+
+ private static void SetVirtualDefault(
+ InMemoryRange resultRange,
+ CompileResult nullLeft,
+ CompileResult nullRight,
+ Operators op,
+ ParsingContext context)
+ {
+ if (!resultRange.HasVirtualRows) return;
+
+ var res = ApplyOperator(nullLeft, nullRight, op, out bool error, context);
+ if (error)
+ {
+ resultRange.VirtualDefaultValue = ExcelErrorValue.Create(eErrorType.Value);
+ }
+ else
+ {
+ var resultValue = res.ResultValue;
+ if (!(resultValue is bool) && ConvertUtil.IsNumeric(resultValue) && res.ResultNumeric == 0d)
+ {
+ resultValue = 0d;
+ }
+ resultRange.VirtualDefaultValue = resultValue;
+ }
}
- public static InMemoryRange ApplySingleValueRight(CompileResult left, CompileResult right, Operators op, ParsingContext context)
+ public static InMemoryRange ApplySingleValueRight(
+ CompileResult left, CompileResult right, Operators op, ParsingContext context)
{
var lr = left.Result as IRangeInfo;
- if(lr == null && left.Result is FormulaRangeAddress fra)
+ if (lr == null && left.Result is FormulaRangeAddress fra)
{
lr = context.ExcelDataProvider.GetRange(fra);
}
- else if(left.Address != null && left.Result is not InMemoryRange)
+ else if (left.Address != null && left.Result is not InMemoryRange)
{
lr = context.ExcelDataProvider.GetRange(left.Address);
}
var resultRange = CreateRange(lr, InMemoryRange.Empty, lr.Address);
- for (var row = 0; row < resultRange.Size.NumberOfRows; row++)
+ for (var row = 0; row < resultRange.PhysicalRows; row++)
{
for (var col = 0; col < resultRange.Size.NumberOfCols; col++)
{
@@ -217,10 +292,16 @@ public static InMemoryRange ApplySingleValueRight(CompileResult left, CompileRes
SetValue(op, resultRange, row, col, lcr, right, context);
}
}
+
+ // Compute default for virtual rows: null op scalar
+ SetVirtualDefault(resultRange,
+ CompileResultFactory.Create(null), right, op, context);
+
return resultRange;
}
- public static InMemoryRange ApplySingleValueLeft(CompileResult left, CompileResult right, Operators op, ParsingContext context)
+ public static InMemoryRange ApplySingleValueLeft(
+ CompileResult left, CompileResult right, Operators op, ParsingContext context)
{
var rr = right.Result as IRangeInfo;
if (rr == null && right.Result is FormulaRangeAddress fra)
@@ -232,7 +313,7 @@ public static InMemoryRange ApplySingleValueLeft(CompileResult left, CompileResu
rr = context.ExcelDataProvider.GetRange(right.Address);
}
var resultRange = CreateRange(InMemoryRange.Empty, rr, rr.Address);
- for (var row = 0; row < resultRange.Size.NumberOfRows; row++)
+ for (var row = 0; row < resultRange.PhysicalRows; row++)
{
for (var col = 0; col < resultRange.Size.NumberOfCols; col++)
{
@@ -242,18 +323,83 @@ public static InMemoryRange ApplySingleValueLeft(CompileResult left, CompileResu
SetValue(op, resultRange, row, col, left, rcr, context);
}
}
+
+ // Compute default for virtual rows: scalar op null
+ SetVirtualDefault(resultRange,
+ left, CompileResultFactory.Create(null), op, context);
+
return resultRange;
}
+ private static void SetVirtualDefaultForRanges(
+ InMemoryRange resultRange,
+ IRangeInfo lr,
+ IRangeInfo rr,
+ bool shouldUseSingleRow,
+ bool shouldUseSingleCell,
+ bool singleRowSingleCol,
+ Operators op,
+ ParsingContext context)
+ {
+ if (!resultRange.HasVirtualRows) return;
+
+ CompileResult virtualLeft, virtualRight;
+ if (shouldUseSingleRow)
+ {
+ if (lr.Size.NumberOfRows == 1)
+ {
+ virtualLeft = CompileResultFactory.Create(GetCellValue(lr, 0, 0));
+ virtualRight = CompileResultFactory.Create(null);
+ }
+ else
+ {
+ virtualLeft = CompileResultFactory.Create(null);
+ virtualRight = CompileResultFactory.Create(GetCellValue(rr, 0, 0));
+ }
+ }
+ else if (shouldUseSingleCell)
+ {
+ if (lr.Size.NumberOfCols == 1 && lr.Size.NumberOfRows == 1)
+ {
+ virtualLeft = CompileResultFactory.Create(GetCellValue(lr, 0, 0));
+ virtualRight = CompileResultFactory.Create(null);
+ }
+ else
+ {
+ virtualLeft = CompileResultFactory.Create(null);
+ virtualRight = CompileResultFactory.Create(GetCellValue(rr, 0, 0));
+ }
+ }
+ else if (singleRowSingleCol)
+ {
+ if (lr.Size.NumberOfRows == 1)
+ {
+ virtualLeft = CompileResultFactory.Create(GetCellValue(lr, 0, 0));
+ virtualRight = CompileResultFactory.Create(null);
+ }
+ else
+ {
+ virtualLeft = CompileResultFactory.Create(null);
+ virtualRight = CompileResultFactory.Create(GetCellValue(rr, 0, 0));
+ }
+ }
+ else
+ {
+ virtualLeft = CompileResultFactory.Create(null);
+ virtualRight = CompileResultFactory.Create(null);
+ }
+ SetVirtualDefault(resultRange, virtualLeft, virtualRight, op, context);
+ }
+
private static InMemoryRange ApplyRanges(CompileResult left, CompileResult right, Operators op, ParsingContext context, FormulaRangeAddress intersectAddress)
{
var lr = left.Result as IRangeInfo;
var rr = right.Result as IRangeInfo;
- if(lr == null && left.Result is FormulaRangeAddress fral)
+ if (lr == null && left.Result is FormulaRangeAddress fral)
{
lr = new RangeInfo(fral);
}
- if(rr == null && right.Result is FormulaRangeAddress frar)
+ if (rr == null && right.Result is FormulaRangeAddress frar)
{
rr = new RangeInfo(frar);
}
@@ -263,7 +409,7 @@ private static InMemoryRange ApplyRanges(CompileResult left, CompileResult right
var shouldUseSingleRow = ShouldUseSingleRow(lr.Size, rr.Size);
var shouldUseSingleCell = ShouldUseSingleCell(lr.Size, rr.Size);
var singleRowSingleCol = SingleRowSingleCol(lr.Size, rr.Size);
- for (var row = 0; row < resultRange.Size.NumberOfRows; row++)
+ for (var row = 0; row < resultRange.PhysicalRows; row++)
{
for (var col = 0; col < resultRange.Size.NumberOfCols; col++)
{
@@ -340,7 +486,11 @@ private static InMemoryRange ApplyRanges(CompileResult left, CompileResult right
}
}
+ SetVirtualDefaultForRanges(resultRange, lr, rr,
+ shouldUseSingleRow, shouldUseSingleCell, singleRowSingleCol,
+ op, context);
+
return resultRange;
}
}
-}
+}
\ No newline at end of file
diff --git a/src/EPPlus/FormulaParsing/ExcelCalculationOption.cs b/src/EPPlus/FormulaParsing/ExcelCalculationOption.cs
index 25e14bebd3..646bfb6a48 100644
--- a/src/EPPlus/FormulaParsing/ExcelCalculationOption.cs
+++ b/src/EPPlus/FormulaParsing/ExcelCalculationOption.cs
@@ -15,6 +15,12 @@ Date Author Change
using System;
using System.Collections.Generic;
using System.IO;
+using System.Threading;
+using System.Threading.Tasks;
+#elif (!NET35)
+using System.Collections.Generic;
+using System.Threading;
+using System.Threading.Tasks;
#else
using System.Configuration;
using System.Collections.Generic;
@@ -115,5 +121,22 @@ public bool EnableUnicodeAwareStringOperations
{
get; set;
} = false;
+
+#if !NET35
+ ///
+ /// A cancellation token that can be used to cancel a running calculation.
+ /// When cancelled, an will be thrown
+ /// and the workbook will be left in an inconsistent, partially calculated state.
+ /// The workbook must be discarded after cancellation — saving or recalculating
+ /// a cancelled workbook is not permitted.
+ ///
+ ///
+ ///
+ /// using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(30));
+ /// workbook.Calculate(opt => opt.CancellationToken = cts.Token);
+ ///
+ ///
+ public CancellationToken CancellationToken { get; set; } = CancellationToken.None;
+#endif
}
}
diff --git a/src/EPPlus/FormulaParsing/ExcelUtilities/RangeFlattener.cs b/src/EPPlus/FormulaParsing/ExcelUtilities/RangeFlattener.cs
index 4ec22eec53..cca8f970c5 100644
--- a/src/EPPlus/FormulaParsing/ExcelUtilities/RangeFlattener.cs
+++ b/src/EPPlus/FormulaParsing/ExcelUtilities/RangeFlattener.cs
@@ -10,13 +10,10 @@ Date Author Change
*************************************************************************************************
21/06/2023 EPPlus Software AB Initial release EPPlus 7
*************************************************************************************************/
+using OfficeOpenXml.FormulaParsing.Ranges;
using OfficeOpenXml.Utils.TypeConversion;
-using OfficeOpenXml.FormulaParsing.Excel.Functions.RefAndLookup;
-using OfficeOpenXml.Utils;
using System;
using System.Collections.Generic;
-using System.Linq;
-using System.Text;
namespace OfficeOpenXml.FormulaParsing.ExcelUtilities
{
@@ -32,8 +29,9 @@ internal class RangeFlattener
public static List FlattenRange(IRangeInfo r1, bool addNullifEmpty = true)
{
var result = new List();
+ int rows = RangeHelper.GetPhysicalRows(r1);
- for (var row = 0; row < r1.Size.NumberOfRows; row++)
+ for (var row = 0; row < rows; row++)
{
for (var column = 0; column < r1.Size.NumberOfCols; column++)
{
@@ -67,7 +65,8 @@ public static List FlattenRangeObject(IRangeInfo r1)
return result;
}
///
- /// Produces two lists based on the supplied ranges. The lists will contain all data from positions where both ranges has numeric values.
+ /// Produces two lists based on the supplied ranges.
+ /// The lists will contain all data from positions where both ranges has numeric values.
///
/// range 1
/// range 2
@@ -122,4 +121,4 @@ public static void GetNumericPairLists(IRangeInfo r1, IRangeInfo r2, bool dataPo
}
}
}
-}
+}
\ No newline at end of file
diff --git a/src/EPPlus/FormulaParsing/FormulaExpressions/FunctionCompilers/CustomArrayBehaviourCompiler.cs b/src/EPPlus/FormulaParsing/FormulaExpressions/FunctionCompilers/CustomArrayBehaviourCompiler.cs
index 933300b724..42934d567c 100644
--- a/src/EPPlus/FormulaParsing/FormulaExpressions/FunctionCompilers/CustomArrayBehaviourCompiler.cs
+++ b/src/EPPlus/FormulaParsing/FormulaExpressions/FunctionCompilers/CustomArrayBehaviourCompiler.cs
@@ -40,18 +40,18 @@ internal CustomArrayBehaviourCompiler(ExcelFunction function, ParsingContext con
public override CompileResult Compile(IEnumerable children, ParsingContext context)
{
var args = new List();
-
+
if (!children.Any()) return new CompileResult(eErrorType.Value);
var rangeArgs = new Dictionary();
var otherArgs = new Dictionary();
- for(var ix = 0; ix < children.Count(); ix++)
+ for (var ix = 0; ix < children.Count(); ix++)
{
var cr = children.ElementAt(ix);
- if(cr.DataType == DataType.ExcelRange && Function.ArrayBehaviourConfig.CanBeArrayArg(ix) && (cr.Result is IRangeInfo || cr.Result is FormulaRangeAddress))
+ if (cr.DataType == DataType.ExcelRange && Function.ArrayBehaviourConfig.CanBeArrayArg(ix) && (cr.Result is IRangeInfo || cr.Result is FormulaRangeAddress))
{
var range = cr.Result as IRangeInfo;
- if(range == null && cr.Result is FormulaRangeAddress fra)
+ if (range == null && cr.Result is FormulaRangeAddress fra)
{
range = new RangeInfo(fra);
}
@@ -71,7 +71,7 @@ public override CompileResult Compile(IEnumerable children, Parsi
}
else
{
- if(cr.DataType == DataType.ExcelRange)
+ if (cr.DataType == DataType.ExcelRange)
{
cr = CompileResultFactory.Create(cr.Result);
}
@@ -79,55 +79,78 @@ public override CompileResult Compile(IEnumerable children, Parsi
}
}
- if(rangeArgs.Count == 0)
+ if (rangeArgs.Count == 0)
{
var defaultCompiler = new DefaultCompiler(Function);
return defaultCompiler.Compile(children, context);
}
short maxWidth = 0;
- var maxHeight = 0;
- foreach(var rangeArg in rangeArgs.Values)
+ var maxPhysicalHeight = 0;
+ var maxLogicalHeight = 0;
+ foreach (var rangeArg in rangeArgs.Values)
{
- if(rangeArg.Size.NumberOfCols > maxWidth)
+ if (rangeArg.Size.NumberOfCols > maxWidth)
{
maxWidth = rangeArg.Size.NumberOfCols;
}
- if(rangeArg.Size.NumberOfRows > maxHeight)
+
+ int physical = RangeHelper.GetPhysicalRows(rangeArg);
+ if (physical > maxPhysicalHeight)
+ {
+ maxPhysicalHeight = physical;
+ }
+ if (rangeArg.Size.NumberOfRows > maxLogicalHeight)
{
- maxHeight= rangeArg.Size.NumberOfRows;
+ maxLogicalHeight = rangeArg.Size.NumberOfRows;
}
}
- var resultRangeDef = new RangeDefinition(maxHeight, maxWidth);
InMemoryRange resultRange;
- if(rangeArgs.Count==1)
+ if (maxPhysicalHeight < maxLogicalHeight)
{
- resultRange = new InMemoryRange(rangeArgs.First().Value.Address, resultRangeDef);
+ var physicalDef = new RangeDefinition(maxPhysicalHeight, maxWidth);
+ if (rangeArgs.Count == 1)
+ {
+ resultRange = new InMemoryRange(rangeArgs.First().Value.Address, physicalDef, maxLogicalHeight);
+ }
+ else
+ {
+ resultRange = new InMemoryRange(physicalDef, maxLogicalHeight);
+ }
}
else
{
- resultRange = new InMemoryRange(resultRangeDef);
+ var rangeDef = new RangeDefinition(maxLogicalHeight, maxWidth);
+ if (rangeArgs.Count == 1)
+ {
+ resultRange = new InMemoryRange(rangeArgs.First().Value.Address, rangeDef);
+ }
+ else
+ {
+ resultRange = new InMemoryRange(rangeDef);
+ }
}
+
var nArgs = children.Count();
- for(var row = 0; row < resultRange.Size.NumberOfRows; row++)
+ for (var row = 0; row < resultRange.PhysicalRows; row++)
{
- for(var col = 0; col < resultRange.Size.NumberOfCols; col++)
+ for (var col = 0; col < resultRange.Size.NumberOfCols; col++)
{
bool isError = false;
var argList = new List();
- for(var argIx = 0; argIx < nArgs; argIx++)
+ for (var argIx = 0; argIx < nArgs; argIx++)
{
- if(rangeArgs.ContainsKey(argIx))
+ if (rangeArgs.ContainsKey(argIx))
{
var range = rangeArgs[argIx];
var r = row;
var c = col;
- if(range.Size.NumberOfCols == 1 && range.Size.NumberOfRows == resultRange.Size.NumberOfRows)
+ if (range.Size.NumberOfCols == 1 && range.Size.NumberOfRows == resultRange.Size.NumberOfRows)
{
c = 0;
}
- if(range.Size.NumberOfRows == 1 && range.Size.NumberOfCols == resultRange.Size.NumberOfCols)
+ if (range.Size.NumberOfRows == 1 && range.Size.NumberOfCols == resultRange.Size.NumberOfCols)
{
r = 0;
}
@@ -149,12 +172,12 @@ public override CompileResult Compile(IEnumerable children, Parsi
continue;
}
}
-
+
}
else
{
var arg = otherArgs[argIx];
- if(arg.DataType == DataType.LambdaCalculation)
+ if (arg.DataType == DataType.LambdaCalculation)
{
var calculator = arg.ResultValue as LambdaCalculator;
calculator.BeginCalculation();
@@ -180,4 +203,4 @@ public override CompileResult Compile(IEnumerable children, Parsi
}
}
}
-}
+}
\ No newline at end of file
diff --git a/src/EPPlus/FormulaParsing/FormulaExpressions/FunctionExpression.cs b/src/EPPlus/FormulaParsing/FormulaExpressions/FunctionExpression.cs
index 696dc409d6..03fb45959d 100644
--- a/src/EPPlus/FormulaParsing/FormulaExpressions/FunctionExpression.cs
+++ b/src/EPPlus/FormulaParsing/FormulaExpressions/FunctionExpression.cs
@@ -116,7 +116,7 @@ public override Expression Negate()
}
protected IList _args=null;
internal Queue _dependencyAddresses = null;
- internal bool SetArguments(IList argsResults)
+ internal bool SetArguments(IList argsResults, ParsingContext ctx)
{
_args = argsResults;
if (_function.ParametersInfo.HasNormalArguments == false)
@@ -126,7 +126,7 @@ internal bool SetArguments(IList argsResults)
var pi = _function.ParametersInfo.GetParameterInfo(i);
if (EnumUtil.HasFlag(pi, FunctionParameterInformation.AdjustParameterAddress) && argsResults[i].Address != null)
{
- _function.GetNewParameterAddress(argsResults, i, ref _dependencyAddresses);
+ _function.GetNewParameterAddress(argsResults, i, ctx, ref _dependencyAddresses);
}
}
return _dependencyAddresses != null;
diff --git a/src/EPPlus/FormulaParsing/FormulaExpressions/LambdaCalculator.cs b/src/EPPlus/FormulaParsing/FormulaExpressions/LambdaCalculator.cs
index 823125d7f1..a9eee92678 100644
--- a/src/EPPlus/FormulaParsing/FormulaExpressions/LambdaCalculator.cs
+++ b/src/EPPlus/FormulaParsing/FormulaExpressions/LambdaCalculator.cs
@@ -24,7 +24,7 @@ namespace OfficeOpenXml.FormulaParsing.FormulaExpressions
///
/// Stores the Lambda tokens and can be called multiple times with different parameters.
/// For each time the tokens are converted RPN tokens and runs through the calculation
- /// via the new method.
+ /// via the new method.
///
internal class LambdaCalculator
{
diff --git a/src/EPPlus/FormulaParsing/LexicalAnalysis/FormulaAddress.cs b/src/EPPlus/FormulaParsing/LexicalAnalysis/FormulaAddress.cs
index 2ec9fedb2f..ef5e769632 100644
--- a/src/EPPlus/FormulaParsing/LexicalAnalysis/FormulaAddress.cs
+++ b/src/EPPlus/FormulaParsing/LexicalAnalysis/FormulaAddress.cs
@@ -697,6 +697,69 @@ public bool IsSingleCell
return FromRow == ToRow && FromCol == ToCol;
}
}
+
+
+ private bool? _hasFormulas; // Nullable for lazy evaluation
+
+ ///
+ /// Returns true of the address contains formulas.
+ /// This is used to determine if we can use cached flattened ranges or if we need to evaluate
+ /// the formulas in the range to get the correct values. The result is cached for performance.
+ ///
+ /// The that contains the address
+ ///
+
+ public bool HasFormulas(ExcelPackage package)
+ {
+ if (_hasFormulas.HasValue) return _hasFormulas.Value;
+ if (package == null || WorksheetIx < 0)
+ {
+ _hasFormulas = false;
+ return false;
+ }
+ var ws = package.Workbook.GetWorksheetByIndexInList(WorksheetIx);
+ if (ws == null)
+ {
+ _hasFormulas = false;
+ return false;
+ }
+
+ // Clamp to worksheet dimension to avoid iterating 1M rows
+ // for full column references
+ var dim = ws.Dimension;
+ var fromRow = FromRow;
+ var toRow = ToRow;
+ var fromCol = FromCol;
+ var toCol = ToCol;
+ if (dim != null)
+ {
+ fromRow = Math.Max(fromRow, dim._fromRow);
+ toRow = Math.Min(toRow, dim._toRow);
+ fromCol = Math.Max(fromCol, dim._fromCol);
+ toCol = Math.Min(toCol, dim._toCol);
+ }
+ else
+ {
+ // No dimension means no data, hence no formulas
+ _hasFormulas = false;
+ return false;
+ }
+
+ for (var row = fromRow; row <= toRow; row++)
+ {
+ for (var col = fromCol; col <= toCol; col++)
+ {
+ if (ws._formulas.GetValue(row, col) != null)
+ {
+ _hasFormulas = true;
+ return true;
+ }
+ }
+ }
+ _hasFormulas = false;
+ return false;
+ }
+
///
/// Empty
///
diff --git a/src/EPPlus/FormulaParsing/ParsingContext.cs b/src/EPPlus/FormulaParsing/ParsingContext.cs
index 56cec80cee..18abf536ea 100644
--- a/src/EPPlus/FormulaParsing/ParsingContext.cs
+++ b/src/EPPlus/FormulaParsing/ParsingContext.cs
@@ -20,6 +20,8 @@ Date Author Change
using NvProvider = OfficeOpenXml.FormulaParsing.NameValueProvider;
using OfficeOpenXml.Utils.RemoteCalls;
using OfficeOpenXml.FormulaParsing.FormulaExpressions.VariableStorage;
+using OfficeOpenXml.FormulaParsing.Excel.Functions.MathFunctions;
+
#if (!NET35)
@@ -33,9 +35,11 @@ namespace OfficeOpenXml.FormulaParsing
///
public class ParsingContext
{
- private ParsingContext(ExcelPackage package) {
+ private ParsingContext(ExcelPackage package)
+ {
SubtotalAddresses = new HashSet();
Package = package;
+ ExpressionEvaluator = new ExpressionEvaluator(this);
}
internal FunctionCompilerFactory FunctionCompilerFactory
@@ -103,6 +107,13 @@ internal VariableStorageManager VariableStorage
}
}
+ ///
+ /// Cache for RangeCriteria functions to improve performance during calculation
+ ///
+ internal RangeCriteriaCache RangeCriteriaCache { get; set; }
+
+ internal ExpressionEvaluator ExpressionEvaluator { get; private set; }
+
///
/// Factory method.
///
@@ -112,10 +123,9 @@ public static ParsingContext Create(ExcelPackage package)
{
var context = new ParsingContext(package);
context.Configuration = ParsingConfiguration.Create();
- //context.Scopes = new ParsingScopes(context);
- //context.AddressCache = new ExcelAddressCache();
context.NameValueProvider = NvProvider.Empty;
context.FunctionCompilerFactory = new FunctionCompilerFactory(context.Configuration.FunctionRepository);
+ context.RangeCriteriaCache = new RangeCriteriaCache(package);
return context;
}
diff --git a/src/EPPlus/FormulaParsing/Ranges/InMemoryRange.cs b/src/EPPlus/FormulaParsing/Ranges/InMemoryRange.cs
index 1ad86b06e8..ce1166a4f4 100644
--- a/src/EPPlus/FormulaParsing/Ranges/InMemoryRange.cs
+++ b/src/EPPlus/FormulaParsing/Ranges/InMemoryRange.cs
@@ -25,6 +25,7 @@ namespace OfficeOpenXml.FormulaParsing.Ranges
[DebuggerDisplay("{Size}")]
public class InMemoryRange : IRangeInfo
{
+
///
/// The constructor
///
@@ -32,24 +33,67 @@ public class InMemoryRange : IRangeInfo
public InMemoryRange(RangeDefinition rangeDef)
{
_cells = new ICellInfo[rangeDef.NumberOfRows, rangeDef.NumberOfCols];
+ _physicalRows = rangeDef.NumberOfRows;
Size = rangeDef;
_address = new FormulaRangeAddress() { FromRow = 0, FromCol = 0, ToRow = rangeDef.NumberOfRows - 1, ToCol = rangeDef.NumberOfCols - 1 };
}
///
/// The constructor
///
- /// The worksheet address that should be used for this range. Will be used for implicit intersection.
+ /// The worksheet address that should be used for this range.
+ /// Will be used for implicit intersection.
/// Defines the size of the range
public InMemoryRange(FormulaRangeAddress address, RangeDefinition rangeDef)
{
- if (address?._context != null)
+ if (address != null && address._context != null)
{
_ws = address._context.Package.Workbook.GetWorksheetByIndexInList(address._context.CurrentCell.WorksheetIx);
}
_address = address;
_cells = new ICellInfo[rangeDef.NumberOfRows, rangeDef.NumberOfCols];
+ _physicalRows = rangeDef.NumberOfRows;
Size = rangeDef;
}
+
+ ///
+ /// Constructor for a virtual range with a small physical backing array but large logical size.
+ /// Rows beyond .NumberOfRows are virtual and return null.
+ ///
+ /// The worksheet address for implicit intersection.
+ /// Defines the physical (backed) size of the range.
+ /// The full logical row count (e.g. 1048576 for a full column).
+ internal InMemoryRange(FormulaRangeAddress address, RangeDefinition physicalDef, int logicalRows)
+ {
+ if (address != null && address._context != null)
+ {
+ _ws = address._context.Package.Workbook.GetWorksheetByIndexInList(address._context.CurrentCell.WorksheetIx);
+ }
+ _address = address;
+ _physicalRows = physicalDef.NumberOfRows;
+ _cells = new ICellInfo[physicalDef.NumberOfRows, physicalDef.NumberOfCols];
+ Size = new RangeDefinition(logicalRows, physicalDef.NumberOfCols);
+ }
+
+ ///
+ /// Constructor for a virtual range without an address.
+ /// Rows beyond .NumberOfRows are virtual and return null.
+ ///
+ /// Defines the physical (backed) size of the range.
+ /// The full logical row count.
+ internal InMemoryRange(RangeDefinition physicalDef, int logicalRows)
+ {
+ _physicalRows = physicalDef.NumberOfRows;
+ _cells = new ICellInfo[physicalDef.NumberOfRows, physicalDef.NumberOfCols];
+ Size = new RangeDefinition(logicalRows, physicalDef.NumberOfCols);
+ _address = new FormulaRangeAddress()
+ {
+ FromRow = 0,
+ FromCol = 0,
+ ToRow = logicalRows - 1,
+ ToCol = physicalDef.NumberOfCols - 1
+ };
+ }
+
///
/// Constructor
///
@@ -57,6 +101,7 @@ public InMemoryRange(FormulaRangeAddress address, RangeDefinition rangeDef)
public InMemoryRange(List> range)
{
Size = new RangeDefinition(range.Count, (short)range[0].Count);
+ _physicalRows = Size.NumberOfRows;
_cells = new ICellInfo[Size.NumberOfRows, Size.NumberOfCols];
for (int c = 0; c < Size.NumberOfCols; c++)
{
@@ -71,10 +116,12 @@ public InMemoryRange(List> range)
///
/// Constructor
///
- /// Another used as clone for this range. The address of the supplied range will not be copied.
+ /// Another used as clone for this range.
+ /// The address of the supplied range will not be copied.
public InMemoryRange(IRangeInfo ri)
{
Size = ri.Size;
+ _physicalRows = Size.NumberOfRows;
_cells = new ICellInfo[Size.NumberOfRows, Size.NumberOfCols];
for (int c = 0; c < Size.NumberOfCols; c++)
{
@@ -101,6 +148,8 @@ public InMemoryRange(int rows, short cols)
private readonly ICellInfo[,] _cells;
private int _colIx = -1;
private int _rowIndex = 0;
+ private readonly int _physicalRows;
+ private object _virtualDefaultValue; // null means "no default" (backward compat)
private static InMemoryRange _empty = new InMemoryRange(new RangeDefinition(0, 0));
@@ -109,6 +158,35 @@ public InMemoryRange(int rows, short cols)
///
public static InMemoryRange Empty => _empty;
+ ///
+ /// Number of rows backed by the physical cell array.
+ /// For non-virtual ranges this equals Size.NumberOfRows.
+ ///
+ internal int PhysicalRows
+ {
+ get { return _physicalRows; }
+ }
+
+
+ ///
+ /// The value returned for rows beyond PhysicalRows.
+ /// When null (default), virtual rows return null.
+ /// When set, virtual rows return this value instead.
+ ///
+ internal object VirtualDefaultValue
+ {
+ get { return _virtualDefaultValue; }
+ set { _virtualDefaultValue = value; }
+ }
+
+ ///
+ /// True if the range has virtual (unstored) rows beyond PhysicalRows.
+ ///
+ internal bool HasVirtualRows
+ {
+ get { return Size.NumberOfRows > _physicalRows; }
+ }
+
///
/// Sets the value for a cell.
///
@@ -117,6 +195,7 @@ public InMemoryRange(int rows, short cols)
/// The value to set
public void SetValue(int row, int col, object val)
{
+ if (row >= _physicalRows) return;
var c = new InMemoryCellInfo(val);
_cells[row, col] = c;
}
@@ -129,6 +208,7 @@ public void SetValue(int row, int col, object val)
/// The cell
public void SetCell(int row, int col, ICellInfo cell)
{
+ if (row >= _physicalRows) return;
_cells[row, col] = cell;
}
///
@@ -165,7 +245,7 @@ public void SetCell(int row, int col, ICellInfo cell)
public FormulaRangeAddress Dimension
{
get
- {
+ {
return _address;
}
}
@@ -190,7 +270,7 @@ object IEnumerator.Current
///
/// The addresses for the range, if more than one.
///
- public FormulaRangeAddress[] Addresses => [_address];
+ public FormulaRangeAddress[] Addresses => new FormulaRangeAddress[] { _address };
///
/// Dispose
@@ -226,12 +306,10 @@ public int GetNCells()
/// The value of the cell
public object GetOffset(int rowOffset, int colOffset)
{
+ if (rowOffset >= _physicalRows)
+ return _virtualDefaultValue; // was: return null;
var c = _cells[rowOffset, colOffset];
- if (c == null)
- {
- return null;
- }
- return c.Value;
+ return c == null ? null : c.Value;
}
///
@@ -242,23 +320,43 @@ public object GetOffset(int rowOffset, int colOffset)
/// The ending row offset from the top-left cell.
/// The ending column offset from the top-left cell
/// The value of the cell
- public IRangeInfo GetOffset(int rowOffsetStart, int colOffsetStart, int rowOffsetEnd, int colOffsetEnd)
+ public IRangeInfo GetOffset(int rowOffsetStart, int colOffsetStart,
+ int rowOffsetEnd, int colOffsetEnd)
{
- var nRows = Math.Abs(rowOffsetEnd - rowOffsetStart);
- var nCols = (short)Math.Abs(colOffsetEnd- colOffsetStart);
- nRows++;
- nCols++;
- var rangeDef = new RangeDefinition(nRows, nCols);
- var result = new InMemoryRange(rangeDef);
- var rowIx = 0;
- for(var row = rowOffsetStart; row <= rowOffsetEnd; row++)
+ var logicalRows = rowOffsetEnd - rowOffsetStart + 1;
+ var nCols = (short)(colOffsetEnd - colOffsetStart + 1);
+
+ if (rowOffsetStart >= _physicalRows)
+ {
+ var emptyRange = new InMemoryRange(new RangeDefinition(0, nCols), logicalRows);
+ emptyRange._virtualDefaultValue = _virtualDefaultValue; // propagate
+ return emptyRange;
+ }
+
+ var physicalEnd = Math.Min(rowOffsetEnd, _physicalRows - 1);
+ var physicalRows = physicalEnd - rowOffsetStart + 1;
+
+ InMemoryRange result;
+ if (physicalRows < logicalRows)
+ {
+ result = new InMemoryRange(new RangeDefinition(physicalRows, nCols), logicalRows);
+ result._virtualDefaultValue = _virtualDefaultValue; // propagate
+ }
+ else
+ {
+ result = new InMemoryRange(new RangeDefinition(physicalRows, nCols));
+ }
+
+ for (var row = rowOffsetStart; row <= physicalEnd; row++)
{
var colIx = 0;
for (var col = colOffsetStart; col <= colOffsetEnd; col++)
{
- result.SetValue(rowIx, colIx++, _cells[row, col].Value);
+ var cell = _cells[row, col];
+ var val = cell != null ? cell.Value : null;
+ result.SetValue(row - rowOffsetStart, colIx, val);
+ colIx++;
}
- rowIx++;
}
return result;
}
@@ -280,20 +378,12 @@ public bool IsHidden(int rowOffset, int colOffset)
///
public object GetValue(int row, int col)
{
- if (_address == null)
- {
- var c = _cells[row, col];
- if (c == null) return null;
- return c.Value;
-
-
- }
- else
- {
- var c = _cells[row-_address.FromRow, col-Address.FromCol];
- if (c == null) return null;
- return c.Value;
- }
+ int r = _address == null ? row : row - _address.FromRow;
+ if (r >= _physicalRows) return _virtualDefaultValue;
+ int c = _address == null ? col : col - _address.FromCol;
+ var cell = _cells[r, c];
+ if (cell == null) return null;
+ return cell.Value;
}
///
/// Get cell
@@ -303,6 +393,13 @@ public object GetValue(int row, int col)
///
public ICellInfo GetCell(int row, int col)
{
+ if (row >= _physicalRows)
+ {
+ // Return a virtual cell with the default value if set
+ if (_virtualDefaultValue != null)
+ return new InMemoryCellInfo(_virtualDefaultValue);
+ return null;
+ }
var c = _cells[row, col];
if (c == null) return null;
return c;
@@ -320,7 +417,7 @@ public bool MoveNext()
}
_colIx = 0;
_rowIndex++;
- if (_rowIndex >= Size.NumberOfRows) return false;
+ if (_rowIndex >= _physicalRows) return false;
return true;
}
///
@@ -341,15 +438,26 @@ IEnumerator IEnumerable.GetEnumerator()
internal static InMemoryRange CloneRange(IRangeInfo ri)
{
- var ret = new InMemoryRange(ri.Size);
- for(int r=0;r < ri.Size.NumberOfRows;r++)
+ var isVirtual = ri is InMemoryRange && ((InMemoryRange)ri).HasVirtualRows;
+ int physRows = isVirtual ? ((InMemoryRange)ri).PhysicalRows : ri.Size.NumberOfRows;
+
+ var ret = isVirtual
+ ? new InMemoryRange(new RangeDefinition(physRows, ri.Size.NumberOfCols),
+ ri.Size.NumberOfRows)
+ : new InMemoryRange(ri.Size);
+
+ if (isVirtual)
{
- for (int c = 0; c < ri.Size.NumberOfCols;c++)
+ ret._virtualDefaultValue = ((InMemoryRange)ri)._virtualDefaultValue;
+ }
+
+ for (int r = 0; r < physRows; r++)
+ {
+ for (int c = 0; c < ri.Size.NumberOfCols; c++)
{
- ret.SetValue(r,c,ri.GetOffset(r,c));
+ ret.SetValue(r, c, ri.GetOffset(r, c));
}
}
-
return ret;
}
@@ -357,7 +465,7 @@ internal static InMemoryRange GetFromArray(params object[] values)
{
var rows = values.GetUpperBound(0) + 1;
var ir = new InMemoryRange(rows, 1);
- for(int r=0;r < rows;r++)
+ for (int r = 0; r < rows; r++)
{
ir.SetValue(r, 0, values[r]);
}
@@ -365,12 +473,26 @@ internal static InMemoryRange GetFromArray(params object[] values)
}
///
- /// Get the address adjusted inside the dimension of the worksheet. Not applicable on InMemoryRange's, as no addresses us used.
+ /// Get the address adjusted inside the dimension of the worksheet.
+ /// Not applicable on InMemoryRange's, as no addresses us used.
///
/// Not applicable on InMemoryRange's.
/// The address.
public FormulaRangeAddress GetAddressDimensionAdjusted(int index)
{
+ if (index > 0) return null;
+
+ if (HasVirtualRows)
+ {
+ // Return address clamped to physical rows
+ return new FormulaRangeAddress()
+ {
+ FromRow = _address.FromRow,
+ FromCol = _address.FromCol,
+ ToRow = _address.FromRow + _physicalRows - 1,
+ ToCol = _address.ToCol
+ };
+ }
return _address;
}
@@ -378,7 +500,7 @@ internal IEnumerable SerializeToTokens()
{
var sb = new StringBuilder();
sb.Append("{");
- for (var row = 0; row < Size.NumberOfRows; row++)
+ for (var row = 0; row < _physicalRows; row++)
{
for (var col = 0; col < Size.NumberOfCols; col++)
{
@@ -389,7 +511,7 @@ internal IEnumerable SerializeToTokens()
sb.Append(",");
}
}
- if(row < Size.NumberOfRows - 1)
+ if (row < _physicalRows - 1)
{
sb.Append(";");
}
@@ -406,4 +528,4 @@ public IRangeInfo GetRangeInfoByValue()
return this;
}
}
-}
+}
\ No newline at end of file
diff --git a/src/EPPlus/FormulaParsing/Ranges/RangeHelper.cs b/src/EPPlus/FormulaParsing/Ranges/RangeHelper.cs
new file mode 100644
index 0000000000..d81bbf6b13
--- /dev/null
+++ b/src/EPPlus/FormulaParsing/Ranges/RangeHelper.cs
@@ -0,0 +1,50 @@
+/*************************************************************************************************
+ Required Notice: Copyright (C) EPPlus Software AB.
+ This software is licensed under PolyForm Noncommercial License 1.0.0
+ and may only be used for noncommercial purposes
+ https://polyformproject.org/licenses/noncommercial/1.0.0/
+
+ A commercial license to use this software can be purchased at https://epplussoftware.com
+ *************************************************************************************************
+ Date Author Change
+ *************************************************************************************************
+ 03/03/2026 EPPlus Software AB Virtual InMemoryRange support
+ *************************************************************************************************/
+
+namespace OfficeOpenXml.FormulaParsing.Ranges
+{
+ ///
+ /// Helper methods for working with range physical/logical size.
+ ///
+ internal static class RangeHelper
+ {
+ ///
+ /// Returns the effective number of data rows for a range.
+ /// For virtual InMemoryRanges: the physical row count.
+ /// For worksheet-backed ranges: constrained by worksheet dimension.
+ /// For other ranges: Size.NumberOfRows.
+ ///
+ internal static int GetPhysicalRows(IRangeInfo range)
+ {
+ if (range is InMemoryRange imr)
+ return imr.PhysicalRows;
+
+ if (!range.IsInMemoryRange && range.Worksheet?.Dimension != null)
+ {
+ var adjusted = range.GetAddressDimensionAdjusted(0);
+ if (adjusted != null)
+ {
+ // If the adjusted address is invalid (no column overlap),
+ // the range column has no data at all
+ if (adjusted.FromCol > adjusted.ToCol || adjusted.FromRow > adjusted.ToRow)
+ return 0;
+ var rangeFromRow = range.Address != null
+ ? range.Address.FromRow : adjusted.FromRow;
+ return adjusted.ToRow - rangeFromRow + 1;
+ }
+ }
+
+ return range.Size.NumberOfRows;
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/EPPlus/NumberFormatToTextArgs.cs b/src/EPPlus/NumberFormatToTextArgs.cs
index 01fcc949c6..88b87ec256 100644
--- a/src/EPPlus/NumberFormatToTextArgs.cs
+++ b/src/EPPlus/NumberFormatToTextArgs.cs
@@ -12,6 +12,7 @@ Date Author Change
*************************************************************************************************/
using OfficeOpenXml.FormulaParsing.Excel.Functions.Text;
using OfficeOpenXml.Style;
+using OfficeOpenXml.Style.Interfaces;
using OfficeOpenXml.Style.XmlAccess;
using OfficeOpenXml.Utils.String;
@@ -23,6 +24,32 @@ namespace OfficeOpenXml
public class NumberFormatToTextArgs
{
internal int _styleId;
+
+ ExcelNumberFormatWithoutId fallbackNumberFormat = null;
+
+ ///
+ /// If these args are provided from a formula
+ ///
+ public bool FromFormula = false;
+
+ ///
+ /// Constructor when numberformat is not built in
+ ///
+ ///
+ ///
+ ///
+ ///
+ ///
+ internal NumberFormatToTextArgs(ExcelWorksheet ws, int row, int column, object value, string numberFormat)
+ {
+ Worksheet = ws;
+ Row = row;
+ Column = column;
+ Value = value;
+ _styleId = -1;
+ fallbackNumberFormat = new ExcelNumberFormatWithoutId(numberFormat);
+ }
+
internal NumberFormatToTextArgs(ExcelWorksheet ws, int row, int column, object value, int styleId)
{
Worksheet = ws;
@@ -31,6 +58,7 @@ internal NumberFormatToTextArgs(ExcelWorksheet ws, int row, int column, object v
Value = value;
_styleId = styleId;
}
+
///
/// The worksheet of the cell.
///
@@ -46,11 +74,18 @@ internal NumberFormatToTextArgs(ExcelWorksheet ws, int row, int column, object v
///
/// The number format settings for the cell
///
- public ExcelNumberFormatXml NumberFormat
+ public IExcelNumberFormat NumberFormat
{
get
{
- return ValueToTextHandler.GetNumberFormat(_styleId, Worksheet.Workbook.Styles);
+ if(fallbackNumberFormat != null)
+ {
+ return fallbackNumberFormat;
+ }
+ else
+ {
+ return ValueToTextHandler.GetNumberFormat(_styleId, Worksheet.Workbook.Styles);
+ }
}
}
///
@@ -64,6 +99,13 @@ public string Text
{
get
{
+ if(fallbackNumberFormat != null)
+ {
+ var ft = new ExcelFormatTranslator(NumberFormat.Format, -1);
+ bool isValidFormat = false;
+ var frmt = ValueToTextHandler.FormatValue(Value, false, ft, null, out isValidFormat);
+ return frmt;
+ }
return ValueToTextHandler.GetFormattedText(Value, Worksheet.Workbook, _styleId, false);
}
}
diff --git a/src/EPPlus/RichData/RichDataStore.cs b/src/EPPlus/RichData/RichDataStore.cs
index 1f092cb202..e19f3375f2 100644
--- a/src/EPPlus/RichData/RichDataStore.cs
+++ b/src/EPPlus/RichData/RichDataStore.cs
@@ -74,6 +74,17 @@ internal ExcelRichValue GetRichValueByOneBasedIndex(int index)
return rv;
}
+ internal ExcelRichValue GetRichValueByOneBasedIndex(uint index)
+ {
+ return GetRichValueByOneBasedIndex((int)index);
+ }
+
+ internal bool HasValueBeenDeleted(uint vmId)
+ {
+ var valueMetaData = _metadata.Db.ValueMetadata.Get(vmId);
+ return valueMetaData.Deleted;
+ }
+
///
/// Gets a rich value by its value metadata index
///
diff --git a/src/EPPlus/RichData/RichValues/ExcelRichValue.cs b/src/EPPlus/RichData/RichValues/ExcelRichValue.cs
index abe8cac103..a15f127a83 100644
--- a/src/EPPlus/RichData/RichValues/ExcelRichValue.cs
+++ b/src/EPPlus/RichData/RichValues/ExcelRichValue.cs
@@ -55,6 +55,8 @@ public ExcelRichValue(RichDataDatabase richDatadb, IndexedSubsetCollection _structureType == RichDataStructureTypes.WebImage || _structureType == RichDataStructureTypes.LocalImage;
+
public IndexedSubsetCollection Values { get; private set; }
private Dictionary _relations = new Dictionary();
diff --git a/src/EPPlus/Style/Dxf/ExcelDxfStyle.cs b/src/EPPlus/Style/Dxf/ExcelDxfStyle.cs
index f74feaae82..64955bd68c 100644
--- a/src/EPPlus/Style/Dxf/ExcelDxfStyle.cs
+++ b/src/EPPlus/Style/Dxf/ExcelDxfStyle.cs
@@ -35,6 +35,7 @@ internal ExcelDxfStyle(XmlNamespaceManager nameSpaceManager, XmlNode topNode, Ex
Font.SetValuesFromXml(_helper);
Alignment.SetValuesFromXml(_helper);
Protection.SetValuesFromXml(_helper);
+ Checkbox = _helper.ExistsNode($"d:extLst/d:ext[@uri='{ExtLstUris.FeaturePropertyBagDxf}']/xfpb:DXFComplement");
}
}
///
@@ -122,7 +123,7 @@ internal override void CreateNodes(XmlHelper helper, string path)
var cmplNode = (XmlElement)helper.CreateNode("d:extLst/d:ext/xfpb:DXFComplement");
var extNode = (XmlElement)cmplNode.ParentNode;
extNode.SetAttribute("xmlns:xfpb", Schemas.schemaFeaturePropertyBag);
- extNode.SetAttribute("uri", ExtLstUris.FeaturePropertyBag);
+ extNode.SetAttribute("uri", ExtLstUris.FeaturePropertyBagDxf);
cmplNode.SetAttribute("i", "0");
_styles._hasDxfCheckbox = true;
}
diff --git a/src/EPPlus/Style/Dxf/ExcelDxfStyleConditionalFormatting.cs b/src/EPPlus/Style/Dxf/ExcelDxfStyleConditionalFormatting.cs
index ca15280b71..b92af96808 100644
--- a/src/EPPlus/Style/Dxf/ExcelDxfStyleConditionalFormatting.cs
+++ b/src/EPPlus/Style/Dxf/ExcelDxfStyleConditionalFormatting.cs
@@ -37,8 +37,6 @@ internal override string Id
{
get
{
-
-
return GetId() + ExcelDxfAlignment.GetEmptyId() + ExcelDxfProtection.GetEmptyId();
}
}
diff --git a/src/EPPlus/Style/ExcelTextFont.cs b/src/EPPlus/Style/ExcelTextFont.cs
index c171dcebc4..638979c01b 100644
--- a/src/EPPlus/Style/ExcelTextFont.cs
+++ b/src/EPPlus/Style/ExcelTextFont.cs
@@ -13,12 +13,14 @@ Date Author Change
using OfficeOpenXml.Drawing;
using OfficeOpenXml.Drawing.Interfaces;
using OfficeOpenXml.Drawing.Style.Font;
+using OfficeOpenXml.FormulaParsing.Excel.Functions.Text;
using OfficeOpenXml.Interfaces.Drawing.Text;
+using OfficeOpenXml.Utils;
using OfficeOpenXml.Utils.EnumUtils;
using System;
using System.Drawing;
+using System.Globalization;
using System.Linq;
-using OfficeOpenXml.Utils;
using System.Xml;
namespace OfficeOpenXml.Style
@@ -330,7 +332,7 @@ public override eTextCapsType Capitalization
{
get
{
- switch (_xml.GetXmlNodeString($"{_path}/@cap"))
+ switch (_xml.GetXmlNodeString($"@cap"))
{
case "all":
return eTextCapsType.All;
@@ -343,7 +345,7 @@ public override eTextCapsType Capitalization
set
{
CreateTopNode();
- _xml.SetXmlNodeString($"{_path}/@cap", value.ToEnumString());
+ _xml.SetXmlNodeString($"@cap", value.ToEnumString());
}
}
@@ -354,12 +356,12 @@ public override double Baseline
{
get
{
- return _xml.GetXmlNodeDouble($"{_path}/@baseline", 0);
+ return _xml.GetXmlNodeDouble($"@baseline", 0);
}
set
{
CreateTopNode();
- _xml.SetXmlNodePercentage($"{_path}/@baseline", value);
+ _xml.SetXmlNodePercentage($"@baseline", value);
}
}
@@ -785,6 +787,7 @@ internal MeasurementFont GetMeasureFont()
{
return FontUtil.GetMeasureFont(LatinFont, ComplexFont, EastAsianFont, Size, GetFontStyle(), _pictureRelationDocument.Package);
}
+
private MeasurementFontStyles GetFontStyle()
{
MeasurementFontStyles ret = MeasurementFontStyles.Regular;
@@ -802,6 +805,26 @@ private MeasurementFontStyles GetFontStyle()
}
return ret;
}
+ ///
+ /// Returns the text according to the capitalization settings.
+ ///
+ /// The text
+ /// If Capitalization is None, the original text is returned. If Capitalization is All, the text is converted to upper case. If Capitalization is Small, the text is converted to lower case.
+ internal string GetCapitalizedText(string text)
+ {
+ if (Capitalization == eTextCapsType.All)
+ {
+ return text.ToUpper(CultureInfo.InvariantCulture);
+ }
+ else if (Capitalization == eTextCapsType.Small)
+ {
+ return text.ToLower(CultureInfo.InvariantCulture);
+ }
+ else
+ {
+ return text;
+ }
+ }
internal abstract bool IsEmpty
{
diff --git a/src/EPPlus/Style/Interfaces/IExcelNumberFormat.cs b/src/EPPlus/Style/Interfaces/IExcelNumberFormat.cs
new file mode 100644
index 0000000000..764aa1cbdd
--- /dev/null
+++ b/src/EPPlus/Style/Interfaces/IExcelNumberFormat.cs
@@ -0,0 +1,26 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+
+namespace OfficeOpenXml.Style.Interfaces
+{
+ ///
+ /// An interface for number formats.
+ ///
+ public interface IExcelNumberFormat
+ {
+ ///
+ /// The numberformat string
+ ///
+ public string Format { get; }
+ ///
+ /// Number format Id
+ ///
+ public int NumFmtId { get; }
+ ///
+ /// If this numberformat is built in
+ ///
+ public bool BuildIn { get; }
+ }
+}
diff --git a/src/EPPlus/Style/RichText/ExcelParagraph.cs b/src/EPPlus/Style/RichText/ExcelParagraph.cs
index 4167b580f6..70138580bc 100644
--- a/src/EPPlus/Style/RichText/ExcelParagraph.cs
+++ b/src/EPPlus/Style/RichText/ExcelParagraph.cs
@@ -14,6 +14,7 @@ Date Author Change
using OfficeOpenXml.Drawing.Controls;
using OfficeOpenXml.Drawing.Interfaces;
using OfficeOpenXml.Utils.EnumUtils;
+using OfficeOpenXml.FormulaParsing.Excel.Functions.Text;
using System;
using System.Collections.Generic;
using System.Text;
@@ -31,6 +32,7 @@ internal ExcelParagraph(ExcelParagraphTextRunBase textRun) :
{
}
const string AligPath = "../../a:pPr/@algn";
+ const string FldPath = "../a:fld";
///
/// Text
///
@@ -68,14 +70,34 @@ public string Text
{
get
{
- return _textRun.Text;
+ return _textRun.Text;
}
set
{
_textRun.Text = value;
}
}
-
+ ///
+ /// Text, adjusted for the Capitalization property
+ ///
+ public string DisplayedText
+ {
+ get
+ {
+ switch (_textRun.Capitalization)
+ {
+
+ case eTextCapsType.All:
+ return _textRun.Text.ToUpper();
+ case eTextCapsType.Small:
+ return _textRun.Text.ToLower();
+ default:
+ return _textRun.Text;
+
+ }
+ }
+ }
+
///
/// If the paragraph is the first in the collection
///
diff --git a/src/EPPlus/Style/RichText/ExcelParagraphCollection.cs b/src/EPPlus/Style/RichText/ExcelParagraphCollection.cs
index e2b76807d8..da6cdbd3eb 100644
--- a/src/EPPlus/Style/RichText/ExcelParagraphCollection.cs
+++ b/src/EPPlus/Style/RichText/ExcelParagraphCollection.cs
@@ -10,15 +10,16 @@ Date Author Change
*************************************************************************************************
01/27/2020 EPPlus Software AB Initial release EPPlus 5
*************************************************************************************************/
+using OfficeOpenXml.Drawing;
using System;
using System.Collections.Generic;
-using System.Text;
-using System.Xml;
-using OfficeOpenXml.Drawing;
using System.Drawing;
-using System.Linq;
using System.Globalization;
using OfficeOpenXml.Interfaces.Drawing.Text;
+using System.IO;
+using System.Linq;
+using System.Text;
+using System.Xml;
namespace OfficeOpenXml.Style
{
///
@@ -32,7 +33,7 @@ public class ExcelParagraphCollection : XmlHelper, IEnumerable
private readonly float _defaultFontSize;
private readonly ExcelTextFont _defaultFont;
private readonly ExcelTextBody _textBody;
- internal ExcelParagraphCollection(ExcelTextBody tb, ExcelDrawing drawing, XmlNamespaceManager ns, XmlNode topNode, string path, string[] schemaNodeOrder, float defaultFontSize =11) :
+ internal ExcelParagraphCollection(ExcelTextBody tb, ExcelDrawing drawing, XmlNamespaceManager ns, XmlNode topNode, string path, string[] schemaNodeOrder, float defaultFontSize =11, eTextAlignment defaultAlignment = eTextAlignment.Left) :
base(ns, topNode)
{
_drawing = drawing;
@@ -66,8 +67,9 @@ internal ExcelParagraphCollection(ExcelTextBody tb, ExcelDrawing drawing, XmlNa
//_defaultFont = tfXml;
_path = path;
- foreach(var p in tb.Paragraphs)
+ foreach (var p in tb.Paragraphs)
{
+ p.defaultAlignment = defaultAlignment;
foreach(var tr in p.TextRuns)
{
_list.Add(new ExcelParagraph(tr));
@@ -256,6 +258,36 @@ public string Text
}
}
}
+ ///
+ /// The displayed test adjusted for capitalization.
+ ///
+ public string DisplayedText
+ {
+ get
+ {
+ StringBuilder sb = new StringBuilder();
+ foreach (var item in _list)
+ {
+ if (item.IsLastInParagraph)
+ {
+ sb.AppendLine(item.DisplayedText);
+ }
+ else
+ {
+ sb.Append(item.DisplayedText);
+ }
+ }
+
+ var ret = sb.ToString();
+ if (ret.EndsWith(Environment.NewLine))
+ {
+ //Remove last NewLine
+ return ret.Substring(0, ret.Length - Environment.NewLine.Length);
+ }
+
+ return ret;
+ }
+ }
#region IEnumerable Members
IEnumerator IEnumerable.GetEnumerator()
diff --git a/src/EPPlus/Style/XmlAccess/ExcelNumberFormatWithoutId.cs b/src/EPPlus/Style/XmlAccess/ExcelNumberFormatWithoutId.cs
new file mode 100644
index 0000000000..9b953ad1f6
--- /dev/null
+++ b/src/EPPlus/Style/XmlAccess/ExcelNumberFormatWithoutId.cs
@@ -0,0 +1,23 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using OfficeOpenXml.Style.Interfaces;
+
+namespace OfficeOpenXml.Style.XmlAccess
+{
+ internal class ExcelNumberFormatWithoutId : IExcelNumberFormat
+ {
+ internal ExcelNumberFormatWithoutId(string format)
+ {
+ Format = format;
+ NumFmtId = -1;
+ BuildIn = false;
+ }
+ public string Format { get; private set; }
+
+ public int NumFmtId { get; private set; }
+
+ public bool BuildIn { get; private set; }
+ }
+}
diff --git a/src/EPPlus/Style/XmlAccess/ExcelNumberFormatXml.cs b/src/EPPlus/Style/XmlAccess/ExcelNumberFormatXml.cs
index 7b6c4a6653..a0c0eb650e 100644
--- a/src/EPPlus/Style/XmlAccess/ExcelNumberFormatXml.cs
+++ b/src/EPPlus/Style/XmlAccess/ExcelNumberFormatXml.cs
@@ -10,22 +10,23 @@ Date Author Change
*************************************************************************************************
01/27/2020 EPPlus Software AB Initial release EPPlus 5
*************************************************************************************************/
+using OfficeOpenXml.Style.Interfaces;
+using OfficeOpenXml.Utils;
using System;
using System.Collections.Generic;
-using System.Text;
-using System.Xml;
using System.Globalization;
-using System.Text.RegularExpressions;
-using OfficeOpenXml.Utils;
-using System.Runtime.InteropServices;
using System.Linq;
+using System.Runtime.InteropServices;
+using System.Text;
+using System.Text.RegularExpressions;
+using System.Xml;
namespace OfficeOpenXml.Style.XmlAccess
{
///
/// Xml access class for number customFormats
///
- public sealed class ExcelNumberFormatXml : StyleXmlHelper
+ public sealed class ExcelNumberFormatXml : StyleXmlHelper, IExcelNumberFormat
{
internal ExcelNumberFormatXml(XmlNamespaceManager nameSpaceManager) : base(nameSpaceManager)
diff --git a/src/EPPlus/Table/PivotTable/Calculation/PivotTableCalculation.cs b/src/EPPlus/Table/PivotTable/Calculation/PivotTableCalculation.cs
index d1b4ff3f65..ee96c26d15 100644
--- a/src/EPPlus/Table/PivotTable/Calculation/PivotTableCalculation.cs
+++ b/src/EPPlus/Table/PivotTable/Calculation/PivotTableCalculation.cs
@@ -57,47 +57,50 @@ internal partial class PivotTableCalculation
};
internal static bool Calculate(ExcelPivotTable pivotTable, out List calculatedItems, out List>> keys)
{
- calculatedItems = new List();
- keys = new List>>();
+ calculatedItems = new List();
+ keys = new List>>();
var fieldIndex = pivotTable.RowColumnFieldIndicies;
- pivotTable.InitCalculation();
- int dfIx = 0;
- bool hasCalculatedField = false;
- foreach(var df in pivotTable.GetFieldsToCalculate())
+ pivotTable.InitCalculation();
+ int dfIx = 0;
+ bool hasCalculatedField = false;
+
+
+ foreach (var df in pivotTable.GetFieldsToCalculate())
{
- var dataFieldItems = new PivotCalculationStore();
- calculatedItems.Add(dataFieldItems);
- var keyDict = PivotTableCalculation.GetNewKeys();
- keys.Add(keyDict);
- if (string.IsNullOrEmpty(df.Field.Cache.Formula))
- {
- CalculateField(pivotTable, calculatedItems[calculatedItems.Count - 1], keys, df.Field.Cache, df.Function);
+ var dataFieldItems = new PivotCalculationStore();
+ calculatedItems.Add(dataFieldItems);
+ var keyDict = PivotTableCalculation.GetNewKeys();
+ keys.Add(keyDict);
+
+ if (string.IsNullOrEmpty(df.Field.Cache.Formula))
+ {
+ CalculateField(pivotTable, calculatedItems[calculatedItems.Count - 1], keys, df.Field.Cache, df.Function);
+
SetRowColumnsItemsToHashSets(pivotTable);
if (df.ShowDataAs.Value != eShowDataAs.Normal)
- {
- _calculateShowAs[df.ShowDataAs.Value].Calculate(df, fieldIndex, keys[dfIx], ref dataFieldItems);
- calculatedItems[dfIx] = dataFieldItems;
- }
- }
- else
- {
- hasCalculatedField=true;
- }
- dfIx++;
- }
+ {
+ _calculateShowAs[df.ShowDataAs.Value].Calculate(df, fieldIndex, keys[dfIx], ref dataFieldItems);
+ calculatedItems[dfIx] = dataFieldItems;
+ }
+ }
+ else
+ {
+ hasCalculatedField = true;
+ }
+ dfIx++;
+ }
- CalculateRowColumnSubtotals(pivotTable, keys);
+ CalculateRowColumnSubtotals(pivotTable, keys);
//Handle Calculated fields after the pivot table fields has been calculated.
- if (hasCalculatedField)
- {
- CalculateSourceFields(pivotTable);
- var ptCalc = new PivotTableColumnCalculation(pivotTable);
- ptCalc.CalculateFormulaFields(fieldIndex);
- }
-
- return true;
+ if (hasCalculatedField)
+ {
+ CalculateSourceFields(pivotTable);
+ var ptCalc = new PivotTableColumnCalculation(pivotTable);
+ ptCalc.CalculateFormulaFields(fieldIndex);
+ }
+ return true;
}
private static void CalculateRowColumnSubtotals(ExcelPivotTable pivotTable, List>> keys)
@@ -188,44 +191,98 @@ private static void SetRowColumnsItemsToHashSets(ExcelPivotTable pivotTable)
}
private static void CalculateSourceFields(ExcelPivotTable pivotTable)
- {
- var keys = new List>>();
- var calcFields = new Dictionary(StringComparer.InvariantCultureIgnoreCase);
- foreach(var field in pivotTable.Fields.Where(x=>string.IsNullOrEmpty(x.Cache.Formula)==false).Select(x=>x.Cache))
- {
- foreach(var token in field.FormulaTokens)
- {
- if(token.TokenType==TokenType.PivotField && calcFields.ContainsKey(token.Value)==false)
- {
- if(!GetSumCalcItems(pivotTable, token.Value, out PivotCalculationStore store))
- {
- var keyDict = PivotTableCalculation.GetNewKeys();
- keys.Add(keyDict);
- store = new PivotCalculationStore();
- CalculateField(pivotTable, store, keys, pivotTable.Fields[token.Value].Cache, DataFieldFunctions.Sum);
- }
- calcFields.Add(token.Value, store);
- }
- }
- }
- pivotTable.CalculatedFieldReferencedItems = calcFields;
- }
+ {
+ var keys = new List>>();
+ var calcFields = new Dictionary(
+ StringComparer.InvariantCultureIgnoreCase);
- private static bool GetSumCalcItems(ExcelPivotTable pivotTable, string fieldName, out PivotCalculationStore store)
- {
- foreach(var ds in pivotTable.DataFields)
- {
- if(ds.Field!=null && ds.Field.Name.Equals(fieldName, StringComparison.InvariantCultureIgnoreCase) && ds.Function==DataFieldFunctions.Sum && ds.ShowDataAsInternal == eShowDataAs.Normal)
- {
- store = pivotTable.CalculatedItems[ds.Index];
- return true;
- }
- }
- store = null;
- return false;
- }
+ // Find calculated cache fields via DataFields (not pivotTable.Fields)
+ var calcCacheFields = new List();
+ foreach (var df in pivotTable.DataFields)
+ {
+ if (!string.IsNullOrEmpty(df.Field.Cache.Formula))
+ {
+ if (!calcCacheFields.Contains(df.Field.Cache))
+ {
+ calcCacheFields.Add(df.Field.Cache);
+ }
+ }
+ }
+
+ // Also check transitive dependencies: calculated fields referencing
+ // other calculated fields
+ var cacheFieldsList = pivotTable.CacheDefinition._cacheReference.Fields;
+ for (int i = 0; i < calcCacheFields.Count; i++) // Count may grow during iteration
+ {
+ var cf = calcCacheFields[i];
+ foreach (var token in cf.FormulaTokens)
+ {
+ if (token.TokenType == TokenType.PivotField)
+ {
+ var refCf = cacheFieldsList.FirstOrDefault(
+ x => x.Name.Equals(token.Value, StringComparison.InvariantCultureIgnoreCase));
+ if (refCf != null && !string.IsNullOrEmpty(refCf.Formula)
+ && !calcCacheFields.Contains(refCf))
+ {
+ calcCacheFields.Add(refCf);
+ }
+ }
+ }
+ }
+
+ // Now resolve each referenced source field for all calculated fields
+ foreach (var cf in calcCacheFields)
+ {
+ foreach (var token in cf.FormulaTokens)
+ {
+ if (token.TokenType == TokenType.PivotField
+ && !calcFields.ContainsKey(token.Value))
+ {
+ if (!GetSumCalcItems(pivotTable, token.Value, out PivotCalculationStore store))
+ {
+ // Look up via cache fields, not pivotTable.Fields
+ var refCf = cacheFieldsList.FirstOrDefault(
+ x => x.Name.Equals(
+ token.Value, StringComparison.InvariantCultureIgnoreCase));
+ if (refCf != null && string.IsNullOrEmpty(refCf.Formula))
+ {
+ var keyDict = PivotTableCalculation.GetNewKeys();
+ keys.Add(keyDict);
+ store = new PivotCalculationStore();
+ CalculateField(pivotTable, store, keys, refCf, DataFieldFunctions.Sum);
+ }
+ else
+ {
+ // Referenced field is itself calculated or doesn't exist —
+ // it will be resolved during CalculateFormulaFields
+ store = new PivotCalculationStore();
+ }
+ }
+ calcFields.Add(token.Value, store);
+ }
+ }
+ }
+ pivotTable.CalculatedFieldReferencedItems = calcFields;
+ }
+
+ private static bool GetSumCalcItems(ExcelPivotTable pivotTable, string fieldName, out PivotCalculationStore store)
+ {
+ foreach (var ds in pivotTable.DataFields)
+ {
+ if (ds.Field != null &&
+ ds.Field.Name.Equals(fieldName, StringComparison.InvariantCultureIgnoreCase) &&
+ ds.Function == DataFieldFunctions.Sum &&
+ ds.ShowDataAsInternal == eShowDataAs.Normal)
+ {
+ store = pivotTable.CalculatedItems[pivotTable.DataFields.IndexOf(ds)];
+ return true;
+ }
+ }
+ store = null;
+ return false;
+ }
- private static void CalculateField(ExcelPivotTable pivotTable, PivotCalculationStore dataFieldItems, List>> keys, ExcelPivotTableCacheField cacheField, DataFieldFunctions function)
+ private static void CalculateField(ExcelPivotTable pivotTable, PivotCalculationStore dataFieldItems, List>> keys, ExcelPivotTableCacheField cacheField, DataFieldFunctions function)
{
var ci = pivotTable.CacheDefinition._cacheReference;
var recs = ci.Records;
diff --git a/src/EPPlus/Table/PivotTable/Calculation/PivotTableColumnCalculation.cs b/src/EPPlus/Table/PivotTable/Calculation/PivotTableColumnCalculation.cs
index 7648a559eb..3bb7baf447 100644
--- a/src/EPPlus/Table/PivotTable/Calculation/PivotTableColumnCalculation.cs
+++ b/src/EPPlus/Table/PivotTable/Calculation/PivotTableColumnCalculation.cs
@@ -10,20 +10,15 @@ Date Author Change
*************************************************************************************************
03/07/2024 EPPlus Software AB EPPlus 7.2
*************************************************************************************************/
-using OfficeOpenXml.DataValidation.Exceptions;
using OfficeOpenXml.FormulaParsing;
using OfficeOpenXml.FormulaParsing.Excel.Functions;
-using OfficeOpenXml.FormulaParsing.Excel.Functions.Finance;
-using OfficeOpenXml.FormulaParsing.Excel.Functions.MathFunctions;
using OfficeOpenXml.FormulaParsing.LexicalAnalysis;
-using OfficeOpenXml.LoadFunctions.ReflectionHelpers;
using OfficeOpenXml.Table.PivotTable.Calculation;
using OfficeOpenXml.Utils.TypeConversion;
using System;
using System.Collections.Generic;
using System.Globalization;
using System.Linq;
-using System.Net;
namespace OfficeOpenXml.Table.PivotTable
{
@@ -40,55 +35,140 @@ public PivotTableColumnCalculation(ExcelPivotTable tbl)
_fr = _tbl.WorkSheet.Workbook.FormulaParser.ParsingContext.Configuration.FunctionRepository;
_calcItems = tbl.CalculatedItems;
}
- internal void CalculateFormulaFields(List fieldIndex)
- {
- var calcOrder = GetCalcOrder();
- foreach(var i in calcOrder)
- {
- var f = _tbl.Fields[i];
- var tokens = f.Cache.FormulaTokens;
- var calcTokens = GetPivotFieldReferencesInFormula(f, tokens);
- PivotCalculationStore store;
- if(calcTokens.Any(x => x == null))
- {
- //Contains invalid field reference or functions not supported in PT.
- throw (new InvalidOperationException($"Pivot table {_tbl.Name} contains invalid column calculated formula : {f.Cache.Formula}. The formula contains an invalid field, an unsupported function or cell reference."));
+ internal void CalculateFormulaFields(List fieldIndex)
+ {
+ var calcOrder = GetCalcOrder();
+ var cacheFields = _tbl.CacheDefinition._cacheReference.Fields;
+
+ foreach (var cfIndex in calcOrder)
+ {
+ var cf = cacheFields[cfIndex];
+ var tokens = cf.FormulaTokens;
+
+ // Build pivot field references from tokens
+ // (mirrors original GetPivotFieldReferencesInFormula logic, but against cache)
+ var calcTokens = new List();
+ bool hasInvalid = false;
+ int ix = 0;
+ foreach (var t in tokens)
+ {
+ if (t.TokenType == TokenType.PivotField)
+ {
+ var refCf = cacheFields.FirstOrDefault(
+ x => x.Name.Equals(t.Value, StringComparison.InvariantCultureIgnoreCase));
+ if (refCf != null)
+ {
+ calcTokens.Add(new int[] { ix });
+ }
+ else
+ {
+ hasInvalid = true;
+ break;
+ }
+ }
+ else if (t.TokenType == TokenType.Array ||
+ t.TokenType == TokenType.CellAddress ||
+ t.TokenType == TokenType.FullColumnAddress ||
+ t.TokenType == TokenType.FullRowAddress ||
+ t.TokenType == TokenType.TableName ||
+ t.TokenType == TokenType.WorksheetName)
+ {
+ hasInvalid = true;
+ break;
+ }
+ else if (t.TokenType == TokenType.Function)
+ {
+ var func = _fr.GetFunction(t.Value);
+ if (func != null && func.IsAllowedInCalculatedPivotTableField == false)
+ {
+ hasInvalid = true;
+ break;
+ }
+ }
+ ix++;
+ }
+
+ PivotCalculationStore store;
+ if (hasInvalid)
+ {
+ throw new InvalidOperationException(
+ $"Pivot table {_tbl.Name} contains invalid column calculated formula : " +
+ $"{cf.Formula}. The formula contains an invalid field, an unsupported " +
+ "function or cell reference.");
}
else
- {
- store = CalculateField(f, tokens, calcTokens, fieldIndex);
+ {
+ store = new PivotCalculationStore();
+ var options = new ExcelCalculationOption();
+ var depChain = new RpnOptimizedDependencyChain(_tbl.WorkSheet.Workbook, options);
+ var ct = new List();
+ ct.AddRange(tokens.Select(x => new Token(x.Value, x.TokenType, x.IsNegated)));
+
+ // Collect ALL unique keys across all referenced source fields,
+ // so we don't miss keys that only exist in some fields.
+ var allKeys = new List();
+ var seenKeys = new HashSet();
+ foreach (var c in calcTokens)
+ {
+ var fieldName = tokens[c[0]].Value;
+ if (_tbl.CalculatedFieldReferencedItems.TryGetValue(fieldName, out var refStore))
+ {
+ foreach (var k in refStore.Index)
+ {
+ var keyStr = string.Join(",", k.Key.Select(x => x.ToString()).ToArray());
+ if (seenKeys.Add(keyStr))
+ {
+ allKeys.Add(k.Key);
+ }
+ }
+ }
+ }
+
+ // Calculate formula for each key combination
+ foreach (var key in allKeys)
+ {
+
+ foreach (var c in calcTokens)
+ {
+ var fieldName = tokens[c[0]].Value;
+ if (_tbl.CalculatedFieldReferencedItems.TryGetValue(fieldName, out var refStore)
+ && refStore.ContainsKey(key))
+ {
+ ct[c[0]] = GetTokenFromValue(refStore[key]);
+ }
+ else
+ {
+ // Key doesn't exist for this field — use 0 as default
+ // (Excel treats missing pivot values as 0 in calculated fields)
+ ct[c[0]] = new Token("0", TokenType.Decimal);
+ }
+ }
+ var cv = RpnFormulaExecution.ExecutePivotFieldFormula(depChain, ct, options);
+ store.Add(key, cv);
+ }
}
- if (f.IsDataField)
+
+ // Map back to DataField by matching the cache field.
+ // Multiple DataFields can reference the same calculated cache field
+ // (e.g. same field with different ShowDataAs), so don't break on first match.
+ bool stored = false;
+ for (int d = 0; d < _tbl.DataFields.Count; d++)
{
- var ix = _tbl.DataFields.IndexOf(f.DataField);
- _tbl.CalculatedItems[ix] = store;
+ var dfCache = _tbl.DataFields[d].Field.Cache;
+ if (dfCache == cf ||
+ dfCache.Name.Equals(cf.Name, StringComparison.InvariantCultureIgnoreCase))
+ {
+ _tbl.CalculatedItems[d] = store;
+ stored = true;
+ }
}
- else
+ if (!stored)
{
- _tbl.CalculatedFieldReferencedItems.Add(f.Name, store);
+ _tbl.CalculatedFieldReferencedItems[cf.Name] = store;
}
}
}
- private PivotCalculationStore CalculateField(ExcelPivotTableField f, IList tokens, List calcTokens, List fieldIndex)
- {
- var store = new PivotCalculationStore();
- var options = new ExcelCalculationOption();
- var depChain = new RpnOptimizedDependencyChain(_tbl.WorkSheet.Workbook, options);
- var ct = new List();
- ct.AddRange(tokens.Select(x => new Token(x.Value, x.TokenType, x.IsNegated)));
- foreach (var ci in _tbl.CalculatedFieldReferencedItems.First().Value.Index)
- {
- foreach (var c in calcTokens)
- {
- var v = _tbl.CalculatedFieldReferencedItems[tokens[c[0]].Value][ci.Key];
- ct[c[0]] = GetTokenFromValue(v);
- }
- var cv=RpnFormulaExecution.ExecutePivotFieldFormula(depChain, ct, options);
- store.Add(ci.Key, cv);
- }
- return store;
- }
private Token GetTokenFromValue(object v)
{
if(ConvertUtil.IsNumericOrDate(v))
@@ -116,88 +196,67 @@ private Token GetTokenFromValue(object v)
return new Token(v.ToString(),TokenType.String);
}
- private List GetPivotFieldReferencesInFormula(ExcelPivotTableField f, IList tokens)
- {
- var ret = new List();
- int ix = 0;
- foreach (var t in tokens)
- {
- if(t.TokenType==TokenType.PivotField)
- {
- var ff = _tbl.Fields[t.Value];
- if (ff == null)
- {
- ret.Add(null);
- return ret;
- }
- else
- {
- ret.Add([ix, ff.Index]);
- }
- }
- else if(
- t.TokenType == TokenType.Array ||
- t.TokenType == TokenType.CellAddress ||
- t.TokenType == TokenType.FullColumnAddress ||
- t.TokenType == TokenType.FullRowAddress ||
- t.TokenType == TokenType.TableName ||
- t.TokenType == TokenType.WorksheetName)
- {
- ret.Add(null);
- return ret;
- }
- else if(t.TokenType==TokenType.Function)
- {
- var function = _fr.GetFunction(t.Value);
- if(function.IsAllowedInCalculatedPivotTableField==false)
- {
- ret.Add(null);
- return ret;
- }
- }
- ix++;
- }
- return ret;
- }
+ private List GetCalcOrder()
+ {
+ var calcOrder = new List();
+ var cacheFields = _tbl.CacheDefinition._cacheReference.Fields;
- private List GetCalcOrder()
- {
- var calcOrder = new List();
- foreach (var f in _tbl.Fields.Where(x => string.IsNullOrEmpty(x.Cache.Formula) == false))
- {
- if (calcOrder.Contains(f.Index)) continue;
- ValidateNoCircularReference(f, calcOrder);
- }
- return calcOrder;
- }
+ // Collect cache field indices that are used as DataFields and have formulas
+ var relevantCfIndices = new HashSet();
+ foreach (var df in _tbl.DataFields)
+ {
+ if (!string.IsNullOrEmpty(df.Field.Cache.Formula))
+ {
+ var cfIndex = cacheFields.IndexOf(df.Field.Cache);
+ if (cfIndex >= 0)
+ {
+ relevantCfIndices.Add(cfIndex);
+ }
+ }
+ }
- private bool ValidateNoCircularReference(ExcelPivotTableField f, List calcOrder, Stack prevFields = null)
- {
- if (prevFields == null) prevFields = new Stack();
- var tokens = SourceCodeTokenizer.PivotFormula.Tokenize(f.Cache.Formula);
- foreach (var t in tokens)
- {
- if (t.TokenType == TokenType.PivotField)
- {
- var f2 = _tbl.Fields[t.Value];
- if (f2 != null && string.IsNullOrEmpty(f2.Cache.Formula)==false)
- {
- if (t.Value.Equals(f.Name, StringComparison.InvariantCultureIgnoreCase))
- {
- throw(new InvalidOperationException($"Circular reference in pivot table {_tbl.Name} Calculated Field {f.Name}"));
- }
- if(prevFields.Any(x=>x.Name.Equals(t.Value, StringComparison.InvariantCultureIgnoreCase)))
- {
- throw(new InvalidOperationException($"Circular reference in pivot table {_tbl.Name} Calculated Field {f.Name}"));
- }
+ foreach (var cfIndex in relevantCfIndices)
+ {
+ if (calcOrder.Contains(cfIndex)) continue;
+ ValidateNoCircularReferenceCacheField(
+ cacheFields[cfIndex], cfIndex, calcOrder, cacheFields);
+ }
+ return calcOrder;
+ }
- prevFields.Push(f);
- ValidateNoCircularReference(f2, calcOrder, prevFields);
- }
- }
- }
- calcOrder.Add(f.Index);
- return true;
- }
+ private bool ValidateNoCircularReferenceCacheField(
+ ExcelPivotTableCacheField cf,
+ int cfIndex,
+ List calcOrder,
+ List cacheFields,
+ Stack prevIndices = null)
+ {
+ if (prevIndices == null) prevIndices = new Stack();
+ var tokens = SourceCodeTokenizer.PivotFormula.Tokenize(cf.Formula);
+ foreach (var t in tokens)
+ {
+ if (t.TokenType == TokenType.PivotField)
+ {
+ var refIndex = cacheFields.FindIndex(
+ x => x.Name.Equals(t.Value, StringComparison.InvariantCultureIgnoreCase));
+ if (refIndex >= 0 && !string.IsNullOrEmpty(cacheFields[refIndex].Formula))
+ {
+ if (refIndex == cfIndex || prevIndices.Contains(refIndex))
+ {
+ throw new InvalidOperationException(
+ $"Circular reference in pivot table {_tbl.Name} Calculated Field {cf.Name}");
+ }
+ prevIndices.Push(cfIndex);
+ ValidateNoCircularReferenceCacheField(
+ cacheFields[refIndex], refIndex, calcOrder, cacheFields, prevIndices);
+ }
+ }
+ }
+ if (!calcOrder.Contains(cfIndex))
+ {
+ calcOrder.Add(cfIndex);
+ }
+ return true;
+ }
}
}
\ No newline at end of file
diff --git a/src/EPPlus/Table/PivotTable/ExcelPivotTable.cs b/src/EPPlus/Table/PivotTable/ExcelPivotTable.cs
index ab3a4b1d28..955e57540d 100644
--- a/src/EPPlus/Table/PivotTable/ExcelPivotTable.cs
+++ b/src/EPPlus/Table/PivotTable/ExcelPivotTable.cs
@@ -406,15 +406,18 @@ public bool IsCalculated
/// If the pivot cache should be refreshed from the source data, before calculating the pivot table.
public void Calculate(bool refreshCache = false)
{
- if(CacheDefinition.Connection!=null)
+ if (CacheDefinition.Connection != null)
{
return;
}
- if (refreshCache || CacheDefinition._cacheReference.Records == null || CacheDefinition._cacheReference.Records.RecordCount == 0)
+
+ if (refreshCache || CacheDefinition._cacheReference.Records == null || CacheDefinition._cacheReference.Records.RecordCount == 0)
{
CacheDefinition.Refresh();
}
+
PivotTableCalculation.Calculate(this, out CalculatedItems, out Keys);
+
IsCalculated = true;
}
///
@@ -452,17 +455,36 @@ public object GetPivotData(string dataFieldName, IList uniqueItems))
@@ -564,12 +584,19 @@ public object GetPivotData(string dataFieldName, IList fields, List<
//Update data field index
foreach (var df in pt.DataFields)
{
- var ix = movedFields.IndexOf(df.Index);
- if(ix<0)
+ // Check if the underlying field still exists in the updated fields list
+ // by matching field names (case-insensitive)
+ if (df.Field == null || !fields.Any(x => x.Name.Equals(df.Field.Name, StringComparison.InvariantCultureIgnoreCase)))
{
rmDfFields.Add(df);
}
- else if (df.Index != df.Field.Index)
+ else
{
- df.Index = df.Field.Index;
+ // Update the data field's index if the underlying field was moved
+ var newField = fields.FirstOrDefault(x => x.Name.Equals(df.Field.Name, StringComparison.InvariantCultureIgnoreCase));
+ if (newField != null && df.Index != newField.Index)
+ {
+ df.Index = newField.Index;
+ }
}
}
rmDfFields.ForEach(df => { df.Field.IsDataField = false; pt.DataFields.Remove(df); });
- if(pt.DataFields.Count==0)
+ if (pt.DataFields.Count == 0)
{
pt.DeleteNode("d:dataFields");
}
diff --git a/src/EPPlus/Utils/String/ValueToTextHandler.cs b/src/EPPlus/Utils/String/ValueToTextHandler.cs
index 6e4d47ed4a..f93ed755b8 100644
--- a/src/EPPlus/Utils/String/ValueToTextHandler.cs
+++ b/src/EPPlus/Utils/String/ValueToTextHandler.cs
@@ -263,7 +263,14 @@ private static string FormatNumberExcel(double d, string format, CultureInfo cul
}
else
{
- return d.ToString(format, cultureInfo);
+ try
+ {
+ return d.ToString(format, cultureInfo);
+ }
+ catch
+ {
+ return format;
+ }
}
}
diff --git a/src/EPPlus/Utils/TypeConversion/ColorConverter.cs b/src/EPPlus/Utils/TypeConversion/ColorConverter.cs
index dab444c20a..caf33cc0ab 100644
--- a/src/EPPlus/Utils/TypeConversion/ColorConverter.cs
+++ b/src/EPPlus/Utils/TypeConversion/ColorConverter.cs
@@ -30,15 +30,17 @@ public static Color GetThemeColor(ExcelTheme theme, ExcelDrawingColorManager cm)
if(cm!=null && cm.ColorType==eDrawingColorType.Scheme)
{
var newCm=theme.ColorScheme.GetColorByEnum(cm.SchemeColor.Color);
- var c = GetThemeColor(newCm);
- return ApplyTransforms(c, cm.Transforms);
+ var nc = GetThemeColor(newCm);
+ return ApplyTransforms(nc, cm.Transforms);
}
- return GetThemeColor(cm);
+ var c=GetThemeColor(cm);
+ return ApplyTransforms(c, cm.Transforms);
+
}
private static Color ApplyTransforms(Color c, ExcelColorTransformCollection transforms)
{
- if (transforms.Count == 0) return c;
+ if (transforms==null || transforms.Count == 0) return c;
var r = c.R;
var g = c.G;
@@ -73,6 +75,15 @@ private static Color ApplyTransforms(Color c, ExcelColorTransformCollection tran
case eColorTransformType.LumOff:
c = ApplyLumMod(c, 1, v);
break;
+ case eColorTransformType.Alpha:
+ c = Color.FromArgb((byte)Math.Round(255 * v), c.R, c.G, c.B);
+ break;
+ case eColorTransformType.AlphaMod:
+ c = Color.FromArgb((byte)Math.Round(c.A * v), c.R, c.G, c.B);
+ break;
+ case eColorTransformType.AlphaOff:
+ c = Color.FromArgb((byte)(c.A + v), c.R, c.G, c.B);
+ break;
}
}
return c;
diff --git a/src/EPPlus/XmlHelper.cs b/src/EPPlus/XmlHelper.cs
index f72c36ea2e..3602778db5 100644
--- a/src/EPPlus/XmlHelper.cs
+++ b/src/EPPlus/XmlHelper.cs
@@ -25,6 +25,7 @@ Date Author Change
using OfficeOpenXml.Utils.TypeConversion;
using OfficeOpenXml.Utils.EnumUtils;
using OfficeOpenXml.Drawing;
+using System.Xml.XPath;
namespace OfficeOpenXml
{
@@ -596,6 +597,38 @@ internal XmlNode GetNode(string path)
{
return TopNode.SelectSingleNode(path, NameSpaceManager);
}
+
+ ///
+ /// If Get Node doesn't work.
+ /// Simplified way of applying the default node prefix to empty args.
+ ///
+ ///
+ ///
+ internal XmlNode GetDefaultNode(string path)
+ {
+ var retNode = GetNode(path);
+
+ if (retNode == null)
+ {
+ var defaultPrefix = NameSpaceManager.LookupPrefix(NameSpaceManager.DefaultNamespace);
+
+ var splitArgs = path.Split('/');
+
+ for (int i = 0; i < splitArgs.Length; i++)
+ {
+ if (splitArgs[i].Contains(":") == false)
+ {
+ //No prefix add default prefix
+ splitArgs[i] = splitArgs[i].Insert(0, defaultPrefix + ":");
+ }
+ }
+ var alteredExp = string.Join("/", splitArgs.ToArray());
+ retNode = GetNode(alteredExp);
+ }
+
+ return retNode;
+ }
+
internal XmlNodeList GetNodes(string path)
{
return TopNode.SelectNodes(path, NameSpaceManager);
diff --git a/src/EPPlusTest/CompoundDocTests.cs b/src/EPPlusTest/CompoundDocTests.cs
index 03a8fef16b..0fe07c96c3 100644
--- a/src/EPPlusTest/CompoundDocTests.cs
+++ b/src/EPPlusTest/CompoundDocTests.cs
@@ -74,7 +74,7 @@ public void Read()
private void printitems(CompoundDocumentItem item)
{
- File.AppendAllText(@"c:\temp\items.txt", item.Name+ "\t");
+ File.AppendAllText(@"c:\temp\_items.txt", item.Name+ "\t");
foreach(var c in item.Children)
{
printitems(c);
diff --git a/src/EPPlusTest/Core/QuadTreeTest.cs b/src/EPPlusTest/Core/QuadTreeTest.cs
index 3292dc89e9..ae5a46ae4a 100644
--- a/src/EPPlusTest/Core/QuadTreeTest.cs
+++ b/src/EPPlusTest/Core/QuadTreeTest.cs
@@ -72,7 +72,7 @@ public void QuadLargeTest()
sw.Start();
var items = AddRangeItems(rows, cols, qt, 50, 50);
sw.Stop();
- Debug.WriteLine($"Added {items} items in {sw.ElapsedMilliseconds} ms");
+ Debug.WriteLine($"Added {items} _items in {sw.ElapsedMilliseconds} ms");
sw.Restart();
var r1 = new QuadRange(5000, 200, 10000, 300);
@@ -85,7 +85,7 @@ public void QuadLargeTest()
}
}
sw.Stop();
- Debug.WriteLine($"Queried {ir1.Count} items in {sw.ElapsedMilliseconds} ms");
+ Debug.WriteLine($"Queried {ir1.Count} _items in {sw.ElapsedMilliseconds} ms");
}
[TestMethod]
public void QuadTree_InsertRows_Inside()
diff --git a/src/EPPlusTest/Data/ConnectionTests.cs b/src/EPPlusTest/Data/ConnectionTests.cs
index 6fc6955989..12499eddb5 100644
--- a/src/EPPlusTest/Data/ConnectionTests.cs
+++ b/src/EPPlusTest/Data/ConnectionTests.cs
@@ -3,7 +3,9 @@
using OfficeOpenXml.Data.Connection;
using System;
using System.Data.Common;
+using System.Globalization;
using System.Text;
+using System.Threading;
using System.Xml;
namespace EPPlusTest.Data
@@ -193,10 +195,26 @@ public void ReadEPPWebPowerQuery()
SaveAndCleanup(p);
}
}
+
[TestMethod]
- public void AddPivotTableWithConnection()
+ public void GetValueAsText_DateTime_ShouldUseInvariantCulture()
{
+ var cc = Thread.CurrentThread.CurrentCulture;
+ try
+ {
+ Thread.CurrentThread.CurrentCulture = CultureInfo.GetCultureInfo("fi-FI");
+ var entry = new ExcelPowerQueryMetaDataEntry("FillLastUpdated",
+ new DateTime(2026, 2, 19, 8, 15, 36, DateTimeKind.Utc));
+ var result = entry.GetValueAsText(CultureInfo.GetCultureInfo("fi-FI"));
+ Assert.IsTrue(result.Contains(":"),
+ $"Expected colon as time separator but got: {result}");
+ }
+ finally
+ {
+ Thread.CurrentThread.CurrentCulture = cc;
+ }
}
+
[ClassCleanup]
public static void Cleanup()
{
diff --git a/src/EPPlusTest/Drawing/BorderTest.cs b/src/EPPlusTest/Drawing/BorderTest.cs
index 6dca521cde..600f4975fc 100644
--- a/src/EPPlusTest/Drawing/BorderTest.cs
+++ b/src/EPPlusTest/Drawing/BorderTest.cs
@@ -84,14 +84,14 @@ public void BorderWidthStyle()
shape.Border.Fill.Color = Color.Red;
shape.Border.Width = 12;
shape.Border.LineStyle = eLineStyle.Dot;
- shape.Border.CompoundLineStyle = eCompundLineStyle.TripleThinThickThin;
+ shape.Border.CompoundLineStyle = eCompoundLineStyle.TripleThinThickThin;
//Assert
Assert.AreEqual(eFillStyle.SolidFill, shape.Border.Fill.Style);
Assert.IsNotNull(shape.Border.Fill.SolidFill);
Assert.AreEqual(12, shape.Border.Width);
Assert.AreEqual(eLineStyle.Dot, shape.Border.LineStyle);
- Assert.AreEqual(eCompundLineStyle.TripleThinThickThin, shape.Border.CompoundLineStyle);
+ Assert.AreEqual(eCompoundLineStyle.TripleThinThickThin, shape.Border.CompoundLineStyle);
}
[TestMethod]
public void BorderAlignRoundJoin()
@@ -105,7 +105,7 @@ public void BorderAlignRoundJoin()
//Act
shape.Border.Fill.Color = Color.Red;
shape.Border.LineStyle = eLineStyle.LongDashDotDot;
- shape.Border.CompoundLineStyle = eCompundLineStyle.Double;
+ shape.Border.CompoundLineStyle = eCompoundLineStyle.Double;
shape.Border.Alignment = ePenAlignment.Inset;
shape.Border.LineCap = eLineCap.Square;
shape.Border.Join = eLineJoin.Round;
@@ -114,7 +114,7 @@ public void BorderAlignRoundJoin()
Assert.AreEqual(eFillStyle.SolidFill, shape.Border.Fill.Style);
Assert.IsNotNull(shape.Border.Fill.SolidFill);
Assert.AreEqual(eLineStyle.LongDashDotDot, shape.Border.LineStyle);
- Assert.AreEqual(eCompundLineStyle.Double, shape.Border.CompoundLineStyle);
+ Assert.AreEqual(eCompoundLineStyle.Double, shape.Border.CompoundLineStyle);
Assert.AreEqual(ePenAlignment.Inset, shape.Border.Alignment);
Assert.AreEqual(eLineJoin.Round, shape.Border.Join);
Assert.AreEqual(eLineCap.Square, shape.Border.LineCap);
@@ -131,7 +131,7 @@ public void BorderMitterJoin()
//Act
shape.Border.Fill.Color = Color.Red;
shape.Border.LineStyle = eLineStyle.LongDashDotDot;
- shape.Border.CompoundLineStyle = eCompundLineStyle.Double;
+ shape.Border.CompoundLineStyle = eCompoundLineStyle.Double;
shape.Border.LineCap = eLineCap.Flat;
shape.Border.Join = eLineJoin.Bevel;
shape.Border.MiterJoinLimit=10000; //Sets join to Miter
@@ -140,7 +140,7 @@ public void BorderMitterJoin()
Assert.AreEqual(eFillStyle.SolidFill, shape.Border.Fill.Style);
Assert.IsNotNull(shape.Border.Fill.SolidFill);
Assert.AreEqual(eLineStyle.LongDashDotDot, shape.Border.LineStyle);
- Assert.AreEqual(eCompundLineStyle.Double, shape.Border.CompoundLineStyle);
+ Assert.AreEqual(eCompoundLineStyle.Double, shape.Border.CompoundLineStyle);
Assert.AreEqual(eLineJoin.Miter, shape.Border.Join);
Assert.AreEqual(10000, shape.Border.MiterJoinLimit);
Assert.AreEqual(eLineCap.Flat, shape.Border.LineCap);
diff --git a/src/EPPlusTest/Drawing/Chart/ChartSeriesTest.cs b/src/EPPlusTest/Drawing/Chart/ChartSeriesTest.cs
index 353a22c52a..bd5edb1e9f 100644
--- a/src/EPPlusTest/Drawing/Chart/ChartSeriesTest.cs
+++ b/src/EPPlusTest/Drawing/Chart/ChartSeriesTest.cs
@@ -1,14 +1,22 @@
-using Microsoft.VisualStudio.TestTools.UnitTesting;
+using Castle.Components.DictionaryAdapter.Xml;
+using Microsoft.VisualStudio.TestTools.UnitTesting;
+using Newtonsoft.Json;
using OfficeOpenXml;
+using OfficeOpenXml.ConditionalFormatting;
using OfficeOpenXml.Drawing;
using OfficeOpenXml.Drawing.Chart;
using OfficeOpenXml.Drawing.Chart.ChartEx;
+using OfficeOpenXml.Drawing.Interfaces;
+using OfficeOpenXml.FormulaParsing.Utilities;
+using OfficeOpenXml.Style;
using System;
using System.Collections.Generic;
using System.Drawing;
using System.Linq;
+using System.Runtime.CompilerServices;
using System.Text;
using System.Threading.Tasks;
+using System.Xml;
namespace EPPlusTest.Drawing.Chart
{
@@ -241,5 +249,212 @@ public void SimpleChartDataLabels()
SaveAndCleanup(p);
}
}
+
+ [TestMethod]
+ //TODO: This test is one instance of a larger problem
+ //Many datalabels have different allowed positions depending on chart type
+ //Going against it will often create corrupt files.
+ //See microsoft offical documentation:
+ //"MS-OE376" page 659 2.1.1475 Part 4 Section 5.7.2.48, dLblPos (Data Label Position) for details.
+ public void TopIsDisallowedOnBarDataLabels()
+ {
+ using (var p = new ExcelPackage())
+ {
+ var ws = p.Workbook.Worksheets.Add("DataLabelSheet");
+
+ ws.Cells["A1"].Value = "Week";
+ ws.Cells["B1"].Value = "Income";
+
+ ws.Cells["A2:A10"].Formula = $"\"Week \"&(ROW()-1)";
+ ws.Cells["B2:B10"].Formula = $"(ROW()-1)*7";
+ ws.Calculate();
+
+ var chart = ws.Drawings.AddBarChart("columnChart", eBarChartType.ColumnClustered);
+ chart.Series.Add(ws.Cells["B2:B10"], ws.Cells["A2:A10"]);
+
+ var SeriesDataLabel = chart.Series[0].DataLabel;
+
+ Assert.Throws(() => SeriesDataLabel.Position = eLabelPosition.Top);
+ }
+ }
+
+ [TestMethod]
+ public void CreateFileWithDataLabelsManualAndGeneral()
+ {
+ using (var p = OpenPackage("dlblMissMatchTest.xlsx", true))
+ {
+ var ws = p.Workbook.Worksheets.Add("DataLabelSheet");
+
+ ws.Cells["A1"].Value = "Week";
+ ws.Cells["B1"].Value = "Income";
+
+ ws.Cells["A2:A10"].Formula = $"\"Week \"&(ROW()-1)";
+ ws.Cells["B2:B10"].Formula = $"(ROW()-1)*7";
+ ws.Cells["C2:C10"].Formula = $"\"Comment \"&(ROW()-1)";
+ ws.Calculate();
+
+ var chart = ws.Drawings.AddBarChart("columnChart", eBarChartType.ColumnClustered);
+
+ var barSerie = chart.Series.Add(ws.Cells["B2:B10"], ws.Cells["A2:A10"]);
+ var sDlbl = barSerie.DataLabel;
+
+ sDlbl.Separator = ",";
+ sDlbl.ShowValue = true;
+ sDlbl.ShowCategory = true;
+ sDlbl.Position = eLabelPosition.OutEnd;
+
+ sDlbl.SetValueFromCellsRange(ws.Cells["C2:C10"]);
+ Assert.AreEqual(ws.Cells["C2:C10"], barSerie.DataLabel.DataLabelRange);
+
+ Assert.AreEqual("C7", chart.Series[0].DataLabel.DataLabels[5].SingleCellAddressFromSeries.Address);
+ Assert.AreEqual("Comment 6", ws.Cells["C7"].Text);
+
+ SaveAndCleanup(p);
+ }
+
+ //Ensure data is read correctly after write
+ using (var p = OpenPackage("dlblMissMatchTest.xlsx"))
+ {
+ var ws = p.Workbook.Worksheets[0];
+ var chart = ws.Drawings[0].As.Chart.BarChart;
+
+ var barSerie = chart.Series[0];
+ Assert.AreEqual(ws.Cells["C2:C10"], barSerie.DataLabel.DataLabelRange);
+
+ Assert.AreEqual("C7", chart.Series[0].DataLabel.DataLabels[5].SingleCellAddressFromSeries.Address);
+ Assert.AreEqual("Comment 6", ws.Cells["C7"].Text);
+ }
+ }
+
+ [TestMethod]
+ public void ReadSimpleFile()
+ {
+ using (var package = OpenTemplatePackage("editedDataLabel.xlsx"))
+ {
+ var ws = package.Workbook.Worksheets[0];
+
+ var myChart = ws.Drawings[0].As.Chart.BarChart;
+
+ var lbl = myChart.Series[0].DataLabel.DataLabels[0];
+
+ var lblTxtBody = myChart.Series[0].DataLabel.DataLabels[0].TextBody;
+
+ SaveAndCleanup(package);
+ }
+ }
+
+ [TestMethod]
+ public void ReadFile()
+ {
+ using (var package = OpenTemplatePackage("S1008_NoComment.xlsx"))
+ {
+ var ws = package.Workbook.Worksheets[0];
+
+ var chart = ws.Drawings[0].As.Chart.LineChart;
+
+ chart.Series[0].DataLabel.Separator = " ";
+
+ //Select comment range
+ chart.Series[0].DataLabel.SetValueFromCellsRange(ws.Cells["E1:E53"]);
+
+ //Set the relevant labels to not show value
+ chart.Series[0].DataLabel.DataLabels[21].ShowValue = false;
+ chart.Series[0].DataLabel.DataLabels[26].ShowValue = false;
+
+ Assert.AreEqual("E22", chart.Series[0].DataLabel.DataLabels[21].SingleCellAddressFromSeries.Address);
+ Assert.AreEqual("First comment", ws.Cells["E22"].Text);
+
+ SaveAndCleanup(package);
+ }
+ }
+
+ [TestMethod]
+ public void TestAddCommentRangeToExistingFile()
+ {
+ using (var package = OpenTemplatePackage("S1008_NoComment.xlsx"))
+ {
+ var ws = package.Workbook.Worksheets[0];
+
+ var commentText = "Added Comment";
+
+ ws.Cells["E30"].Value = commentText;
+
+ var chart = ws.Drawings[0].As.Chart.LineChart;
+ chart.Series[0].DataLabel.Separator = " ";
+
+ //Select comment range
+ chart.Series[0].DataLabel.SetValueFromCellsRange(ws.Cells["E2:E53"]);
+
+ //Note that since we start on E2 the datalabel idx becomes 20 for row 22 etc.
+ var label1 = chart.Series[0].DataLabel.DataLabels[20];
+ var label2 = chart.Series[0].DataLabel.DataLabels[25];
+ var label3 = chart.Series[0].DataLabel.DataLabels[28];
+
+ //Set the relevant labels to not show value as we only want them to show comments
+ label1.ShowValue = false;
+ label2.ShowValue = false;
+ label3.ShowValue = false;
+
+ Assert.AreEqual("E30", chart.Series[0].DataLabel.DataLabels[28].SingleCellAddressFromSeries.Address);
+ Assert.AreEqual(commentText, ws.Cells["E30"].Text);
+
+ //XforSave is set soley on labels that are not truly neccesary
+
+ SaveAndCleanup(package);
+ }
+ }
+
+ [TestMethod]
+ public void DatalabelRangeLiterals()
+ {
+ string item1 = "one";
+ string item2 = "two";
+ string item3 = "three";
+
+ using (var p = OpenPackage("dlblRangeLiterals.xlsx", true))
+ {
+ var ws = p.Workbook.Worksheets.Add("DataLabelSheet");
+
+ ws.Cells["A1"].Value = "Week";
+ ws.Cells["B1"].Value = "Income";
+
+ ws.Cells["A2:A10"].Formula = $"\"Week \"&(ROW()-1)";
+ ws.Cells["B2:B10"].Formula = $"(ROW()-1)*7";
+ ws.Cells["C2:C10"].Formula = $"\"Comment \"&(ROW()-1)";
+ ws.Calculate();
+
+ var chart = ws.Drawings.AddBarChart("columnChart", eBarChartType.ColumnClustered);
+
+ var barSerie = chart.Series.Add(ws.Cells["B2:B10"], ws.Cells["A2:A10"]);
+ var sDlbl = barSerie.DataLabel;
+
+ sDlbl.ShowValue = true;
+ sDlbl.Position = eLabelPosition.OutEnd;
+
+ sDlbl.ValueFromCellsRange = $"{{\"{item1}\",\"{item2}\",\"{item3}\"}}";
+
+ var dlblLitterals = barSerie.GetDataLabelLiterals();
+
+ Assert.AreEqual(item1, dlblLitterals[0]);
+ Assert.AreEqual(item2, dlblLitterals[1]);
+ Assert.AreEqual(item3, dlblLitterals[2]);
+
+ SaveAndCleanup(p);
+ }
+
+ using (var p = OpenPackage("dlblRangeLiterals.xlsx"))
+ {
+ var ws = p.Workbook.Worksheets[0];
+ var chart = ws.Drawings[0].As.Chart.BarChart;
+
+ var barSerie = chart.Series[0];
+
+ var cache = barSerie.GetDataLabelLiterals();
+
+ Assert.AreEqual(item1, cache[0]);
+ Assert.AreEqual(item2, cache[1]);
+ Assert.AreEqual(item3, cache[2]);
+ }
+ }
}
}
diff --git a/src/EPPlusTest/Drawing/Chart/DataPointsTest.cs b/src/EPPlusTest/Drawing/Chart/DataPointsTest.cs
index 36b732c838..dd7685b1b2 100644
--- a/src/EPPlusTest/Drawing/Chart/DataPointsTest.cs
+++ b/src/EPPlusTest/Drawing/Chart/DataPointsTest.cs
@@ -93,7 +93,7 @@ public void BarChart()
point.Border.Fill.Style = eFillStyle.SolidFill;
point.Fill.Style = eFillStyle.SolidFill;
point.Fill.SolidFill.Color.SetRgbColor(Color.Yellow);
- point.Fill.Transparancy = 5;
+ point.Fill.Transparency = 5;
Assert.AreEqual(eColorTransformType.Alpha, point.Fill.SolidFill.Color.Transforms[0].Type);
Assert.AreEqual(95, point.Fill.SolidFill.Color.Transforms[0].Value);
chart.SetPosition(1, 0, 5, 0);
diff --git a/src/EPPlusTest/Drawing/Chart/Styling/StylingTest.cs b/src/EPPlusTest/Drawing/Chart/Styling/StylingTest.cs
index a538c8261b..6f51d844e4 100644
--- a/src/EPPlusTest/Drawing/Chart/Styling/StylingTest.cs
+++ b/src/EPPlusTest/Drawing/Chart/Styling/StylingTest.cs
@@ -140,7 +140,7 @@ public void StyleLineLoaded()
Assert.AreEqual(9, chart.StyleManager.Style.CategoryAxis.DefaultTextRun.Size);
Assert.AreEqual(eTextCapsType.None, chart.StyleManager.Style.CategoryAxis.DefaultTextRun.Capitalization);
Assert.AreEqual(ePenAlignment.Center, chart.StyleManager.Style.CategoryAxis.Border.Alignment);
- Assert.AreEqual(eCompundLineStyle.Single, chart.StyleManager.Style.CategoryAxis.Border.CompoundLineStyle);
+ Assert.AreEqual(eCompoundLineStyle.Single, chart.StyleManager.Style.CategoryAxis.Border.CompoundLineStyle);
Assert.AreEqual(eLineCap.Flat, chart.StyleManager.Style.CategoryAxis.Border.LineCap);
Assert.AreEqual(1.5, chart.StyleManager.Style.CategoryAxis.Border.Width);
Assert.AreEqual(eFillStyle.SolidFill, chart.StyleManager.Style.CategoryAxis.Border.Fill.Style);
diff --git a/src/EPPlusTest/Drawing/DrawingTest.cs b/src/EPPlusTest/Drawing/DrawingTest.cs
index f209f12848..9487d5c6de 100644
--- a/src/EPPlusTest/Drawing/DrawingTest.cs
+++ b/src/EPPlusTest/Drawing/DrawingTest.cs
@@ -95,7 +95,7 @@ public void Picture()
pic.Border.Fill.Color = Color.DarkCyan;
pic.Fill.Style = eFillStyle.SolidFill;
pic.Fill.Color = Color.White;
- pic.Fill.Transparancy = 50;
+ pic.Fill.Transparency = 50;
pic = ws.Drawings.AddPicture("Pic3", Resources.Test1);
pic.SetPosition(400, 200);
@@ -328,7 +328,7 @@ public void Scatter()
chrt.Title.Fill.Style = eFillStyle.SolidFill;
chrt.Title.Fill.Color = Color.LightBlue;
- chrt.Title.Fill.Transparancy = 50;
+ chrt.Title.Fill.Transparency = 50;
chrt.VaryColors = true;
ExcelScatterChartSerie ser = chrt.Series[0] as ExcelScatterChartSerie;
ser.DataLabel.Position = eLabelPosition.Center;
@@ -664,7 +664,7 @@ public void Drawings()
(ws.Drawings["shape5"] as ExcelShape).Fill.Style = eFillStyle.SolidFill;
(ws.Drawings["shape5"] as ExcelShape).Fill.Color = Color.Red;
- (ws.Drawings["shape5"] as ExcelShape).Fill.Transparancy = 50;
+ (ws.Drawings["shape5"] as ExcelShape).Fill.Transparency = 50;
(ws.Drawings["shape6"] as ExcelShape).Fill.Style = eFillStyle.NoFill;
(ws.Drawings["shape6"] as ExcelShape).Font.Fill.Color = Color.Black;
@@ -674,7 +674,7 @@ public void Drawings()
(ws.Drawings["shape7"] as ExcelShape).Fill.Color = Color.Gray;
(ws.Drawings["shape7"] as ExcelShape).Border.Fill.Style = eFillStyle.SolidFill;
(ws.Drawings["shape7"] as ExcelShape).Border.Fill.Color = Color.Black;
- (ws.Drawings["shape7"] as ExcelShape).Border.Fill.Transparancy = 43;
+ (ws.Drawings["shape7"] as ExcelShape).Border.Fill.Transparency = 43;
(ws.Drawings["shape7"] as ExcelShape).Border.LineCap = eLineCap.Round;
(ws.Drawings["shape7"] as ExcelShape).Border.LineStyle = eLineStyle.LongDash;
(ws.Drawings["shape7"] as ExcelShape).Font.UnderLineColor = Color.Blue;
diff --git a/src/EPPlusTest/Drawing/OLETests.cs b/src/EPPlusTest/Drawing/OLETests.cs
index 4c25fee4a4..72e09546fa 100644
--- a/src/EPPlusTest/Drawing/OLETests.cs
+++ b/src/EPPlusTest/Drawing/OLETests.cs
@@ -795,6 +795,6 @@ public void ExtractDataFromOleObjectTest()
var linkOleBytes = linkOle.GetEmbeddedObjectBytes();
Assert.IsNull(linkOleBytes);
}
-
+
}
}
\ No newline at end of file
diff --git a/src/EPPlusTest/Drawing/Style/FillReadTest.cs b/src/EPPlusTest/Drawing/Style/FillReadTest.cs
index 4d1a91f029..de82a90eaf 100644
--- a/src/EPPlusTest/Drawing/Style/FillReadTest.cs
+++ b/src/EPPlusTest/Drawing/Style/FillReadTest.cs
@@ -168,7 +168,7 @@ public void ReadSolidFill_ColorSystem()
public void ReadTransparancy()
{
//Setup
- var wsName = "Transparancy";
+ var wsName = "Transparency";
var expected = 45;
var ws = _pck.Workbook.Worksheets[wsName];
if (ws == null) Assert.Inconclusive($"{wsName} worksheet is missing");
@@ -176,7 +176,7 @@ public void ReadTransparancy()
//Assert
Assert.AreEqual(eFillStyle.SolidFill, shape.Fill.Style);
- Assert.AreEqual(expected, shape.Fill.Transparancy);
+ Assert.AreEqual(expected, shape.Fill.Transparency);
Assert.AreEqual(eColorTransformType.Alpha, shape.Fill.SolidFill.Color.Transforms[0].Type);
Assert.AreEqual(100 - expected, shape.Fill.SolidFill.Color.Transforms[0].Value);
}
@@ -192,7 +192,7 @@ public void ReadTransformAlpha()
//Assert
Assert.AreEqual(eFillStyle.SolidFill, shape.Fill.Style);
- Assert.AreEqual(100 - expected, shape.Fill.Transparancy);
+ Assert.AreEqual(100 - expected, shape.Fill.Transparency);
Assert.AreEqual(eColorTransformType.Alpha, shape.Fill.SolidFill.Color.Transforms[0].Type);
Assert.AreEqual(expected, shape.Fill.SolidFill.Color.Transforms[0].Value);
}
diff --git a/src/EPPlusTest/Drawing/Style/FillTest.cs b/src/EPPlusTest/Drawing/Style/FillTest.cs
index e5dd9c9f20..4bfdf6f04d 100644
--- a/src/EPPlusTest/Drawing/Style/FillTest.cs
+++ b/src/EPPlusTest/Drawing/Style/FillTest.cs
@@ -242,19 +242,19 @@ public void Transparancy()
{
//Setup
var expected = 45;
- var ws = _pck.Workbook.Worksheets.Add("Transparancy");
+ var ws = _pck.Workbook.Worksheets.Add("Transparency");
var shape = ws.Drawings.AddShape("Shape1", eShapeStyle.Rect);
shape.SetPosition(1, 0, 5, 0);
//Act
shape.Fill.Color = Color.Red;
- shape.Fill.Transparancy = expected;
+ shape.Fill.Transparency = expected;
//Assert
Assert.AreEqual(eFillStyle.SolidFill, shape.Fill.Style);
Assert.IsInstanceOfType(shape.Fill.SolidFill.Color.RgbColor, typeof(ExcelDrawingRgbColor));
- Assert.AreEqual(expected, shape.Fill.Transparancy);
+ Assert.AreEqual(expected, shape.Fill.Transparency);
Assert.AreEqual(100 - expected, shape.Fill.SolidFill.Color.Transforms[0].Value);
}
[TestMethod]
@@ -274,7 +274,7 @@ public void TransformAlpha()
//Assert
Assert.AreEqual(eFillStyle.SolidFill, shape.Fill.Style);
Assert.IsInstanceOfType(shape.Fill.SolidFill.Color.RgbColor, typeof(ExcelDrawingRgbColor));
- Assert.AreEqual(100 - expected, shape.Fill.Transparancy);
+ Assert.AreEqual(100 - expected, shape.Fill.Transparency);
Assert.AreEqual(eColorTransformType.Alpha, shape.Fill.SolidFill.Color.Transforms[0].Type);
Assert.AreEqual(expected, shape.Fill.SolidFill.Color.Transforms[0].Value);
}
diff --git a/src/EPPlusTest/Drawing/ThemeTest.cs b/src/EPPlusTest/Drawing/ThemeTest.cs
index a1bb671dad..55643c9b6e 100644
--- a/src/EPPlusTest/Drawing/ThemeTest.cs
+++ b/src/EPPlusTest/Drawing/ThemeTest.cs
@@ -236,7 +236,7 @@ public void LoadThmx_BordersScheme()
Assert.AreEqual(eLineStyle.Solid, currentTheme.FormatScheme.BorderStyle[0].Style);
Assert.AreEqual(ePenAlignment.Center, currentTheme.FormatScheme.BorderStyle[0].Alignment);
Assert.AreEqual(eLineCap.Flat, currentTheme.FormatScheme.BorderStyle[0].Cap);
- Assert.AreEqual(eCompundLineStyle.Single, currentTheme.FormatScheme.BorderStyle[0].CompoundLineStyle);
+ Assert.AreEqual(eCompoundLineStyle.Single, currentTheme.FormatScheme.BorderStyle[0].CompoundLineStyle);
Assert.IsNull(currentTheme.FormatScheme.BorderStyle[0].HeadEnd.Width);
Assert.IsNull(currentTheme.FormatScheme.BorderStyle[0].HeadEnd.Height);
Assert.IsNull(currentTheme.FormatScheme.BorderStyle[0].HeadEnd.Style);
diff --git a/src/EPPlusTest/Export/HtmlExport/CssRuleCollectionTests.cs b/src/EPPlusTest/Export/HtmlExport/CssRuleCollectionTests.cs
index 903460058e..508d2bb7b3 100644
--- a/src/EPPlusTest/Export/HtmlExport/CssRuleCollectionTests.cs
+++ b/src/EPPlusTest/Export/HtmlExport/CssRuleCollectionTests.cs
@@ -1,21 +1,23 @@
using Microsoft.VisualStudio.TestTools.UnitTesting;
-using System;
-using System.Collections.Generic;
-using System.Linq;
-using System.Text;
-using System.Threading.Tasks;
using OfficeOpenXml;
-using OfficeOpenXml.Export.HtmlExport.CssCollections;
+using OfficeOpenXml.Drawing.Chart;
using OfficeOpenXml.Export.HtmlExport;
+using OfficeOpenXml.Export.HtmlExport.CssCollections;
+using OfficeOpenXml.Export.HtmlExport.Exporters.Internal;
+using OfficeOpenXml.Export.HtmlExport.StyleCollectors;
using OfficeOpenXml.Export.HtmlExport.StyleCollectors.StyleContracts;
using OfficeOpenXml.Export.HtmlExport.Translators;
-using OfficeOpenXml.Export.HtmlExport.StyleCollectors;
+using System;
+using System.Collections.Generic;
using System.Drawing;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
namespace EPPlusTest.Export.HtmlExport
{
[TestClass]
- public class CssRuleCollectionTests
+ public class CssRuleCollectionTests : TestBase
{
[TestMethod]
public void ExportRangeWithNullBorderMergedCellsShouldNotThrow()
@@ -47,5 +49,73 @@ public void ExportRangeWithNullBorderMergedCellsShouldNotThrow()
var declarations = borderTranslator.GenerateDeclarationList(context);
}
}
+
+ [TestMethod]
+ public void ExportChartCssTest()
+ {
+ using (var package = new ExcelPackage())
+ {
+ var mySheet = package.Workbook.Worksheets.Add("chartSinglSheet");
+
+ mySheet.Cells["A1:C1"].Formula = "COLUMN()";
+
+ var lChart = mySheet.Drawings.AddLineChart("chart1", eLineChartType.Line);
+
+ mySheet.Calculate();
+
+ var address = mySheet.Cells["A1:C1"];
+
+ lChart.Series.Add(address, address);
+
+ lChart.StyleManager.SetChartStyle(OfficeOpenXml.Drawing.Chart.Style.ePresetChartStyle.LineChartStyle2);
+
+ lChart.Fill.Style = OfficeOpenXml.Drawing.eFillStyle.SolidFill;
+ lChart.Fill.Color = Color.Aquamarine;
+ lChart.Border.Fill.Color = Color.DarkCyan;
+
+ lChart.StyleManager.Style.ChartArea.Fill.Style = OfficeOpenXml.Drawing.eFillStyle.SolidFill;
+
+ //lChart.StyleManager.Style.ChartArea.Fill.Color = Color.DarkCyan;
+ //lChart.StyleManager.Style.ChartArea.Border.Fill.Color = Color.DarkSeaGreen;
+
+ var cssExporter = new CssChartExporterSync(lChart);
+
+ var css = cssExporter.GetCssString();
+
+ package.SaveAs("C:\\epplusTest\\Testoutput\\generatedLineChart.xlsx");
+ }
+ }
+
+ [TestMethod]
+ public void ChartTitleIssue()
+ {
+
+ using (var package = OpenTemplatePackage("ChartLineDefaultDownUp.xlsx"))
+ {
+ var mySheet = package.Workbook.Worksheets[0];
+
+ var lChart = mySheet.Drawings[0].As.Chart.LineChart;
+
+ lChart.StyleManager.ApplyStyles();
+
+ package.SaveAs("C:\\epplusTest\\Testoutput\\ChartLineDefaultApply.xlsx");
+ }
+
+ using (var package = OpenPackage("ChartLineDefaultApply.xlsx", false))
+ {
+ var mySheet = package.Workbook.Worksheets[0];
+
+ var lChart = mySheet.Drawings[0].As.Chart.LineChart;
+
+ lChart.Title.Text = "MyTitle";
+
+ lChart.StyleManager.ApplyStyles();
+
+ Assert.AreEqual("MyTitle", lChart.Title.TextBody.Paragraphs[0].Text);
+ Assert.AreEqual("MyTitle", lChart.Title.TextBody.Paragraphs[0].TextRuns[0].Text);
+
+ package.SaveAs("C:\\epplusTest\\Testoutput\\ChartLineDefaultApplyAndTitle.xlsx");
+ }
+ }
}
}
diff --git a/src/EPPlusTest/Export/HtmlExport/HtmlConditionalFormattingTest.cs b/src/EPPlusTest/Export/HtmlExport/HtmlConditionalFormattingTest.cs
index 7e05574101..9355f2b8c4 100644
--- a/src/EPPlusTest/Export/HtmlExport/HtmlConditionalFormattingTest.cs
+++ b/src/EPPlusTest/Export/HtmlExport/HtmlConditionalFormattingTest.cs
@@ -140,5 +140,36 @@ public void ExportingHtmlCFsWithThemeColor()
SaveAndCleanup(p);
}
}
+
+ [TestMethod]
+ public void ExportCss()
+ {
+ ExcelPackage.License.SetNonCommercialOrganization("EPPlus Project");
+
+ var chartName = "DifficultCssExportMb.xlsx";
+ using (var p = OpenTemplatePackage(chartName))
+ {
+ var ws = p.Workbook.Worksheets[0];
+
+ var range = ws.Cells["A1:G10"];
+
+ var html = range.CreateHtmlExporter();
+ var fs = GetOutputFile("", "cssExportDatabarMb.html");
+
+ var cssStr = html.GetCssString();
+ //var lChart = ws.Drawings[0].As.Chart.LineChart;
+
+ ////lChart.Title.Font.Color = Color.DeepSkyBlue;
+
+ //lChart.StyleManager.ApplyStyles();
+
+ SaveAndCleanup(p);
+ }
+
+ //using(var p= OpenPackage(chartName,false))
+ //{
+
+ //}
+ }
}
}
diff --git a/src/EPPlusTest/FormulaParsing/CancelCalculationTests.cs b/src/EPPlusTest/FormulaParsing/CancelCalculationTests.cs
new file mode 100644
index 0000000000..dbc690f252
--- /dev/null
+++ b/src/EPPlusTest/FormulaParsing/CancelCalculationTests.cs
@@ -0,0 +1,200 @@
+using Microsoft.VisualStudio.TestTools.UnitTesting;
+using OfficeOpenXml;
+using System;
+using System.Collections.Generic;
+using System.Diagnostics;
+using System.IO;
+using System.Linq;
+using System.Text;
+using System.Threading;
+
+namespace EPPlusTest.FormulaParsing
+{
+ [TestClass]
+ public class CancelCalculationTests
+ {
+ const int WaitTimeMs = 150;
+
+ [TestMethod]
+ public void CancelCalculation()
+ {
+ using var package = CreateHeavyChain(chainLength: 1500, sheetCount: 3);
+ using var cts = new CancellationTokenSource(TimeSpan.FromMilliseconds(WaitTimeMs));
+
+ var sw = Stopwatch.StartNew();
+ try
+ {
+ package.Workbook.Calculate(opt =>
+ {
+ opt.CancellationToken = cts.Token;
+ });
+ Assert.Fail("Expected OperationCanceledException was not thrown.");
+ }
+ catch (OperationCanceledException)
+ {
+ sw.Stop();
+ Debug.WriteLine($"Calculation cancelled after {sw.Elapsed.TotalSeconds:F2} seconds.");
+ Assert.IsTrue(sw.Elapsed.TotalSeconds < 10, "Cancellation took too long.");
+ Assert.IsTrue(package.Workbook.IsCalculationInconsistent);
+ }
+ }
+
+ [TestMethod]
+ public void Save_AfterCancelledCalculation_ThrowsInvalidOperationException()
+ {
+ using var package = CreateHeavyChain(chainLength: 1500, sheetCount: 3);
+ using var cts = new CancellationTokenSource(TimeSpan.FromMilliseconds(WaitTimeMs));
+ using var outputStream = new MemoryStream();
+
+ try
+ {
+ package.Workbook.Calculate(opt =>
+ {
+ opt.CancellationToken = cts.Token;
+ });
+ }
+ catch (OperationCanceledException) { /* expected */ }
+
+ Assert.ThrowsExactly(() =>
+ {
+ package.SaveAs(outputStream);
+ });
+ }
+
+ [TestMethod]
+ public void CancelCalculation_AlreadyCancelledToken_ThrowsImmediately()
+ {
+ using var package = CreateHeavyChain(chainLength: 1500, sheetCount: 3);
+ using var cts = new CancellationTokenSource();
+ cts.Cancel(); // Signal before calculate
+
+ Assert.ThrowsExactly(() =>
+ {
+ package.Workbook.Calculate(opt => opt.CancellationToken = cts.Token);
+ });
+ Assert.IsTrue(package.Workbook.IsCalculationInconsistent);
+ }
+
+ [TestMethod]
+ public void CancelCalculation_RecalculatePoisonedWorkbook_ThrowsInvalidOperationException()
+ {
+ using var package = CreateHeavyChain(chainLength: 1500, sheetCount: 3);
+ using var cts = new CancellationTokenSource(TimeSpan.FromMilliseconds(WaitTimeMs));
+
+ try
+ {
+ package.Workbook.Calculate(opt => opt.CancellationToken = cts.Token);
+ }
+ catch (OperationCanceledException) { /* expected */ }
+
+ Assert.ThrowsExactly(() =>
+ {
+ package.Workbook.Calculate(); // Should throw — workbook is poisoned
+ });
+ }
+
+ [TestMethod]
+ public void CancelCalculation_FromAnotherThread()
+ {
+ using var package = CreateHeavyChain(chainLength: 1500, sheetCount: 3);
+ using var cts = new CancellationTokenSource();
+ Exception caughtException = null;
+
+ var calcThread = new Thread(() =>
+ {
+ try
+ {
+ package.Workbook.Calculate(opt => opt.CancellationToken = cts.Token);
+ }
+ catch (OperationCanceledException ex)
+ {
+ caughtException = ex;
+ }
+ });
+
+ calcThread.Start();
+ Thread.Sleep(WaitTimeMs); // Let calculation run for a while
+ cts.Cancel(); // Cancel from this (main) thread
+ calcThread.Join(TimeSpan.FromSeconds(10)); // Wait for calc thread to finish
+
+ Assert.IsFalse(calcThread.IsAlive, "Calculation thread did not terminate.");
+ Assert.IsInstanceOfType(caughtException);
+ Assert.IsTrue(package.Workbook.IsCalculationInconsistent);
+ }
+
+ [TestMethod]
+ public void Calculate_WithToken_CompletesNormally_IsConsistent()
+ {
+ using var package = new ExcelPackage();
+ var ws = package.Workbook.Worksheets.Add("Sheet1");
+ ws.Cells["A1"].Formula = "1+1";
+
+ using var cts = new CancellationTokenSource();
+ package.Workbook.Calculate(opt => opt.CancellationToken = cts.Token);
+
+ Assert.AreEqual(2d, ws.Cells["A1"].Value);
+ Assert.IsFalse(package.Workbook.IsCalculationInconsistent);
+ }
+
+ [TestMethod]
+ public void CancelCalculation_WithHeavyNamedRanges()
+ {
+ using var package = new ExcelPackage();
+ var wb = package.Workbook;
+ var ws = wb.Worksheets.Add("Sheet1");
+
+ // Fill source data
+ for (int row = 1; row <= 1000; row++)
+ {
+ ws.Cells[row, 1].Value = row;
+ }
+
+ // Create named ranges with heavy formulas referencing each other
+ for (int i = 0; i < 800; i++)
+ {
+ wb.Names.Add($"HeavyName{i}", ws.Cells["A1:A500"]);
+ ws.Cells[1, i + 2].Formula = $"SUMPRODUCT(HeavyName{i},HeavyName{i})";
+ }
+
+ using var cts = new CancellationTokenSource(TimeSpan.FromMilliseconds(WaitTimeMs));
+
+ Assert.ThrowsExactly(() =>
+ {
+ wb.Calculate(opt => opt.CancellationToken = cts.Token);
+ });
+ Assert.IsTrue(wb.IsCalculationInconsistent);
+ }
+
+ [TestMethod]
+ public void CancelCalculation_WorksheetLevel()
+ {
+ using var package = CreateHeavyChain(chainLength: 1500, sheetCount: 1);
+ using var cts = new CancellationTokenSource(TimeSpan.FromMilliseconds(WaitTimeMs));
+
+ Assert.ThrowsExactly(() =>
+ {
+ package.Workbook.Worksheets[0].Calculate(opt => opt.CancellationToken = cts.Token);
+ });
+ Assert.IsTrue(package.Workbook.IsCalculationInconsistent);
+ }
+
+ public static ExcelPackage CreateHeavyChain(int chainLength = 1_000, int sheetCount = 3)
+ {
+ var package = new ExcelPackage();
+
+ for (int s = 1; s <= sheetCount; s++)
+ {
+ var ws = package.Workbook.Worksheets.Add($"Sheet{s}");
+ ws.Cells[1, 1].Value = 1;
+
+ for (int row = 2; row <= chainLength; row++)
+ {
+ // SUMPRODUCT over a growing range — O(N²) total work
+ ws.Cells[row, 1].Formula = $"SUMPRODUCT(A$1:A{row - 1})+1";
+ }
+ }
+
+ return package;
+ }
+ }
+}
diff --git a/src/EPPlusTest/FormulaParsing/EntireColumnTests.cs b/src/EPPlusTest/FormulaParsing/EntireColumnTests.cs
new file mode 100644
index 0000000000..8fb3cead80
--- /dev/null
+++ b/src/EPPlusTest/FormulaParsing/EntireColumnTests.cs
@@ -0,0 +1,233 @@
+using Microsoft.VisualStudio.TestTools.UnitTesting;
+using OfficeOpenXml;
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+
+namespace EPPlusTest.FormulaParsing
+{
+ [TestClass]
+ public class EntireColumnTests
+ {
+ private ExcelPackage _package;
+ private ExcelWorkbook _workbook;
+
+ [TestInitialize]
+ public void Initialize()
+ {
+ _package = new ExcelPackage();
+ _workbook = _package.Workbook;
+ }
+
+ [TestCleanup]
+ public void Cleanup()
+ {
+ _package.Dispose();
+ }
+
+ [TestMethod]
+ public void FullColumnRefPlusScalar_EmptyColumn_ShouldReturnOneInFirstAndLastRow()
+ {
+ // Arrange
+ var ws = _workbook.Worksheets.Add("Sheet1");
+ // Column B is entirely empty/blank
+ ws.Cells["C1"].Formula = "B:B+1";
+
+ // Act
+ ws.Calculate();
+
+ // Assert - first row of spill result
+ Assert.AreEqual(1d, ws.Cells["C1"].Value,
+ "First row should be 0 + 1 = 1");
+ var lastRow = ws.Dimension != null ? ws.Dimension.End.Row : 1;
+ Assert.AreEqual(ExcelPackage.MaxRows, ws.Dimension.End.Row,
+ "Spill should extend to the last row of the worksheet");
+ Assert.AreEqual(1d, ws.Cells["C" + lastRow].Value,
+ "Last row of spill should be 0 + 1 = 1");
+ }
+
+ [TestMethod]
+ public void FullColumnRefPlusScalar_WithData_PhysicalRowsCalculatedAndVirtualRowsGetDefault()
+ {
+ // Arrange - B has data in rows 1-3, formula in C1
+ var ws = _workbook.Worksheets.Add("Sheet1");
+ ws.Cells["B1"].Value = 10d;
+ ws.Cells["B2"].Value = 20d;
+ ws.Cells["B3"].Value = 30d;
+ ws.Cells["C1"].Formula = "B:B+5";
+
+ // Act
+ ws.Calculate();
+
+ // Assert - physical rows get their actual calculated values
+ Assert.AreEqual(15d, ws.Cells["C1"].Value);
+ Assert.AreEqual(25d, ws.Cells["C2"].Value);
+ Assert.AreEqual(35d, ws.Cells["C3"].Value);
+ // Virtual rows beyond data: empty + 5 = 5
+ Assert.AreEqual(5d, ws.Cells["C4"].Value,
+ "First virtual row should be 0 + 5 = 5");
+ Assert.AreEqual(5d, ws.Cells["C" + ExcelPackage.MaxRows].Value,
+ "Last row should be 0 + 5 = 5");
+ }
+
+ [TestMethod]
+ public void FullColumnRefEqualsString_VirtualRowsShouldBeFalse()
+ {
+ // Arrange - comparison operator: A:A="Hello"
+ // Virtual rows are empty, empty != "Hello" => FALSE
+ var ws = _workbook.Worksheets.Add("Sheet1");
+ ws.Cells["A1"].Value = "Hello";
+ ws.Cells["A2"].Value = "World";
+ ws.Cells["A3"].Value = "Hello";
+ ws.Cells["B1"].Formula = "A:A=\"Hello\"";
+
+ // Act
+ ws.Calculate();
+
+ // Assert - physical rows
+ Assert.AreEqual(true, ws.Cells["B1"].Value);
+ Assert.AreEqual(false, ws.Cells["B2"].Value);
+ Assert.AreEqual(true, ws.Cells["B3"].Value);
+ // Virtual rows: empty = "Hello" => FALSE
+ Assert.AreEqual(false, ws.Cells["B4"].Value,
+ "Virtual row: empty = \"Hello\" should be FALSE");
+ Assert.AreEqual(false, ws.Cells["B" + ExcelPackage.MaxRows].Value,
+ "Last row should also be FALSE");
+ }
+
+ [TestMethod]
+ public void TwoFullColumnRefsMultiplied_VirtualRowsShouldBeZero()
+ {
+ // Arrange - (A:A=x) * (B:B=y) pattern used in MATCH criteria
+ var ws = _workbook.Worksheets.Add("Sheet1");
+ ws.Cells["A1"].Value = "Alpha";
+ ws.Cells["A2"].Value = "Beta";
+ ws.Cells["B1"].Value = "X";
+ ws.Cells["B2"].Value = "Y";
+ ws.Cells["C1"].Value = 100d;
+ ws.Cells["C2"].Value = 200d;
+
+ // MATCH(1, (A:A="Alpha")*(B:B="X"), 0) should find row 1
+ ws.Cells["E1"].Formula = "MATCH(1,(A:A=\"Alpha\")*(B:B=\"X\"),0)";
+ // INDEX to retrieve the value
+ ws.Cells["F1"].Formula = "INDEX(C:C,MATCH(1,(A:A=\"Alpha\")*(B:B=\"X\"),0))";
+
+ // Act
+ ws.Calculate();
+
+ // Assert
+ Assert.AreEqual(1, ws.Cells["E1"].Value,
+ "MATCH should find row 1");
+ Assert.AreEqual(100d, ws.Cells["F1"].Value,
+ "INDEX should return 100 for Alpha+X");
+ }
+
+ [TestMethod]
+ public void CrossSheetFullColumnRef_IndexMatch_ShouldWork()
+ {
+ // Arrange - the original problem pattern from the design doc
+ var dataWs = _workbook.Worksheets.Add("Data");
+ dataWs.Cells["A1"].Value = "Item1";
+ dataWs.Cells["A2"].Value = "Item2";
+ dataWs.Cells["A3"].Value = "Item1";
+ dataWs.Cells["B1"].Value = "Day Shift";
+ dataWs.Cells["B2"].Value = "Day Shift";
+ dataWs.Cells["B3"].Value = "Night Shift";
+ dataWs.Cells["C1"].Value = 50d;
+ dataWs.Cells["C2"].Value = 75d;
+ dataWs.Cells["C3"].Value = 90d;
+
+ var ws = _workbook.Worksheets.Add("Formulas");
+ ws.Cells["A1"].Value = "Item1";
+ ws.Cells["B1"].Formula =
+ "IFERROR(INDEX(Data!$C:$C,MATCH(1,(Data!$A:$A=A1)*(Data!$B:$B=\"Day Shift\"),0)),\"-\")";
+ // Also test a lookup that should NOT match
+ ws.Cells["A2"].Value = "NoMatch";
+ ws.Cells["B2"].Formula =
+ "IFERROR(INDEX(Data!$C:$C,MATCH(1,(Data!$A:$A=A2)*(Data!$B:$B=\"Day Shift\"),0)),\"-\")";
+
+ // Act
+ _workbook.Calculate();
+
+ // Assert
+ Assert.AreEqual(50d, ws.Cells["B1"].Value,
+ "Should find Item1 + Day Shift => 50");
+ Assert.AreEqual("-", ws.Cells["B2"].Value,
+ "NoMatch should fall through to IFERROR => \"-\"");
+ }
+ [TestMethod]
+ public void ScalarDivideFullColumnRef_VirtualRowsShouldBeDivByZeroError()
+ {
+ // Arrange - 1/B:B where B has data in rows 1-2
+ // Virtual default: 1 / null = 1 / 0 = #DIV/0!
+ var ws = _workbook.Worksheets.Add("Sheet1");
+ ws.Cells["B1"].Value = 2d;
+ ws.Cells["B2"].Value = 4d;
+ ws.Cells["C1"].Formula = "1/B:B";
+
+ // Act
+ ws.Calculate();
+
+ // Assert - physical rows
+ Assert.AreEqual(0.5d, ws.Cells["C1"].Value);
+ Assert.AreEqual(0.25d, ws.Cells["C2"].Value);
+ // Virtual rows: 1 / 0 => #DIV/0!
+ var virtualVal = ws.Cells["C3"].Value;
+ Assert.IsInstanceOfType(virtualVal, typeof(ExcelErrorValue),
+ "Virtual row: 1/0 should produce an error");
+ Assert.AreEqual(eErrorType.Div0, ((ExcelErrorValue)virtualVal).Type,
+ "Error should be #DIV/0!");
+ // Last row should also be #DIV/0!
+ var lastVal = ws.Cells["C" + ExcelPackage.MaxRows].Value;
+ Assert.IsInstanceOfType(lastVal, typeof(ExcelErrorValue),
+ "Last row should also be #DIV/0!");
+ }
+
+ [TestMethod]
+ public void FullColumnRefConcat_VirtualRowsShouldConcatEmpty()
+ {
+ // Arrange - A:A&"!" via the Concat operator
+ // Virtual default: "" & "!" = "!"
+ var ws = _workbook.Worksheets.Add("Sheet1");
+ ws.Cells["A1"].Value = "Hello";
+ ws.Cells["A2"].Value = "World";
+ ws.Cells["B1"].Formula = "A:A&\"!\"";
+
+ // Act
+ ws.Calculate();
+
+ // Assert - physical rows
+ Assert.AreEqual("Hello!", ws.Cells["B1"].Value);
+ Assert.AreEqual("World!", ws.Cells["B2"].Value);
+ // Virtual rows: "" & "!" = "!"
+ Assert.AreEqual("!", ws.Cells["B3"].Value,
+ "Virtual row: empty & \"!\" should be \"!\"");
+ Assert.AreEqual("!", ws.Cells["B" + ExcelPackage.MaxRows].Value,
+ "Last row should also be \"!\"");
+ }
+
+ [TestMethod]
+ public void NegateFullColumnRef_VirtualRowsShouldBeZero()
+ {
+ // Arrange - negation: -A:A
+ // Virtual default: -(null) = -(0) = 0
+ var ws = _workbook.Worksheets.Add("Sheet1");
+ ws.Cells["A1"].Value = 5d;
+ ws.Cells["A2"].Value = -3d;
+ ws.Cells["B1"].Formula = "-A:A";
+
+ // Act
+ ws.Calculate();
+
+ // Assert - physical rows
+ Assert.AreEqual(-5d, ws.Cells["B1"].Value);
+ Assert.AreEqual(3d, ws.Cells["B2"].Value);
+ // Virtual rows: -(0) = 0
+ Assert.AreEqual(0d, ws.Cells["B3"].Value,
+ "Virtual row: -0 should be 0");
+ Assert.AreEqual(0d, ws.Cells["B" + ExcelPackage.MaxRows].Value,
+ "Last row should also be 0");
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/EPPlusTest/FormulaParsing/Excel/Functions/MathFunctions/AverageIfsTests.cs b/src/EPPlusTest/FormulaParsing/Excel/Functions/MathFunctions/AverageIfsTests.cs
index 281d3aa606..a3022fffad 100644
--- a/src/EPPlusTest/FormulaParsing/Excel/Functions/MathFunctions/AverageIfsTests.cs
+++ b/src/EPPlusTest/FormulaParsing/Excel/Functions/MathFunctions/AverageIfsTests.cs
@@ -17,15 +17,15 @@ public void AverageIfsShouldNotCountNumericStringsAsNumbers()
using (var package = new ExcelPackage())
{
var sheet = package.Workbook.Worksheets.Add("test");
- sheet.Cells[1, 1].Value = 3;
- sheet.Cells[2, 1].Value = 4;
- sheet.Cells[3, 1].Value = 5;
- sheet.Cells[1, 2].Value = 1;
- sheet.Cells[2, 2].Value = "2";
- sheet.Cells[3, 2].Value = 3;
- sheet.Cells[1, 3].Value = 2;
- sheet.Cells[2, 3].Value = 1;
- sheet.Cells[3, 3].Value = "4";
+ sheet.Cells["A1"].Value = 3;
+ sheet.Cells["A2"].Value = 4;
+ sheet.Cells["A3"].Value = 5;
+ sheet.Cells["B1"].Value = 1;
+ sheet.Cells["B2"].Value = "2";
+ sheet.Cells["B3"].Value = 3;
+ sheet.Cells["C1"].Value = 2;
+ sheet.Cells["C2"].Value = 1;
+ sheet.Cells["C3"].Value = "4";
sheet.Cells[4, 1].Formula = "AVERAGEIFS(A1:A3,B1:B3,\">0\",C1:C3,\">1\")";
sheet.Calculate();
diff --git a/src/EPPlusTest/FormulaParsing/Excel/Functions/MathFunctions/CriteriaRangeFunctionsTests.cs b/src/EPPlusTest/FormulaParsing/Excel/Functions/MathFunctions/CriteriaRangeFunctionsTests.cs
index 2df1f39665..95e8b40a57 100644
--- a/src/EPPlusTest/FormulaParsing/Excel/Functions/MathFunctions/CriteriaRangeFunctionsTests.cs
+++ b/src/EPPlusTest/FormulaParsing/Excel/Functions/MathFunctions/CriteriaRangeFunctionsTests.cs
@@ -36,5 +36,49 @@ public void SumAndAverageIfFunctionsShouldNotReturnCircularReferenceIfCriteriaIs
Assert.AreEqual(10.5D, ws.Cells["C7"].Value);
SaveAndCleanup(p);
}
+
+
+ [TestMethod]
+ public void AverageIfsShouldNotCacheWhenValueRangeHasFormulas()
+ {
+ using var p = new ExcelPackage();
+ var ws = p.Workbook.Worksheets.Add("Sheet1");
+ ws.Cells["A1"].Value = "Fruit";
+ ws.Cells["B1"].Value = "Employee";
+ ws.Cells["A2:A3"].Value = "Apples";
+ ws.Cells["A4:A5"].Value = "Artichokes";
+ ws.Cells["A6:A7"].Value = "Bananas";
+ ws.Cells["A8:A9"].Value = "Carrots";
+ ws.Cells["B2,B4,B8"].Value = "Mats";
+ ws.Cells["B3,B5,B9"].Value = "Jan";
+ ws.Cells["B6,B7"].Value = "Ossian"; // Both B6 and B7 are Ossian
+
+ // C2, C3 have values
+ ws.Cells["C2"].Value = 10D;
+ ws.Cells["C3"].Value = 11D;
+
+ // C4 and C5 have formulas that reference C2:C9 (circular reference scenario)
+ ws.Cells["C4"].Formula = "AVERAGEIFS(C2:C9,A2:A9,\"=B*\",B2:B9,\"Ossian\")";
+ ws.Cells["C5"].Formula = "AVERAGEIFS(C2:C9,A2:A9,\"=A*\",B2:B9,\"Mats\")";
+
+ ws.Cells["C6"].Value = 12D;
+ ws.Cells["C7"].Value = 13D;
+ ws.Cells["C8"].Value = 14D;
+ ws.Cells["C9"].Value = 15D;
+
+ ws.Calculate();
+
+ // C4 = AVERAGEIFS(C2:C9, A2:A9, "=B*", B2:B9, "Ossian")
+ // Matches: A6="Bananas" (B*), B6="Ossian" -> C6=12
+ // A7="Bananas" (B*), B7="Ossian" -> C7=13
+ // Average = (12+13)/2 = 12.5
+ Assert.AreEqual(12.5D, ws.Cells["C4"].Value, "C4 should be 12.5");
+
+ // C5 = AVERAGEIFS(C2:C9, A2:A9, "=A*", B2:B9, "Mats")
+ // Matches: A2="Apples" (A*), B2="Mats" -> C2=10
+ // A4="Artichokes" (A*), B4="Mats" -> C4=12.5 (calculated earlier!)
+ // Average = (10+12.5)/2 = 11.25
+ Assert.AreEqual(11.25D, ws.Cells["C5"].Value, "C5 should be 11.25 (including calculated C4 value)");
+ }
}
}
diff --git a/src/EPPlusTest/FormulaParsing/Excel/Functions/RangeCriteriaCacheTests.cs b/src/EPPlusTest/FormulaParsing/Excel/Functions/RangeCriteriaCacheTests.cs
new file mode 100644
index 0000000000..b28c5f0884
--- /dev/null
+++ b/src/EPPlusTest/FormulaParsing/Excel/Functions/RangeCriteriaCacheTests.cs
@@ -0,0 +1,234 @@
+using Microsoft.VisualStudio.TestTools.UnitTesting;
+using OfficeOpenXml;
+using OfficeOpenXml.FormulaParsing.Excel.Functions.MathFunctions;
+using OfficeOpenXml.FormulaParsing.LexicalAnalysis;
+using System.Collections.Generic;
+
+namespace EPPlusTest.FormulaParsing.Excel.Functions
+{
+ [TestClass]
+ public class RangeCriteriaCacheTests
+ {
+ private ExcelPackage _package;
+ private RangeCriteriaCache _cache;
+
+ [TestInitialize]
+ public void Setup()
+ {
+ _package = new ExcelPackage();
+ _package.Settings.CalculationCacheSettings.MaxFlattenedRanges = RangeCriteriaCache.DEFAULT_MAX_FLATTENED_RANGES;
+ _package.Settings.CalculationCacheSettings.MaxMatchIndexes = RangeCriteriaCache.DEFAULT_MAX_MATCH_INDEXES;
+ _package.Workbook.Worksheets.Add("Sheet1");
+ }
+
+ [TestCleanup]
+ public void Cleanup()
+ {
+ _package?.Dispose();
+ }
+
+ [TestMethod]
+ public void FlattenedRange_ShouldCacheAndRetrieve()
+ {
+ _cache = new RangeCriteriaCache(_package);
+ var address = new FormulaRangeAddress { WorksheetIx = 0, FromRow = 1, FromCol = 1, ToRow = 10, ToCol = 1 };
+ var data = new List { 1, 2, 3, 4, 5 };
+
+ _cache.SetFlattenedRange(address, data);
+ var retrieved = _cache.GetFlattenedRange(address);
+
+ Assert.IsNotNull(retrieved);
+ Assert.AreEqual(5, retrieved.Count);
+ Assert.AreEqual(1, retrieved[0]);
+ }
+
+ [TestMethod]
+ public void FlattenedRange_FIFO_ShouldEvictOldest()
+ {
+ _package.Settings.CalculationCacheSettings.MaxFlattenedRanges = 3;
+ _cache = new RangeCriteriaCache(_package);
+
+ var address1 = new FormulaRangeAddress { WorksheetIx = 0, FromRow = 1, FromCol = 1, ToRow = 10, ToCol = 1 };
+ var address2 = new FormulaRangeAddress { WorksheetIx = 0, FromRow = 11, FromCol = 1, ToRow = 20, ToCol = 1 };
+ var address3 = new FormulaRangeAddress { WorksheetIx = 0, FromRow = 21, FromCol = 1, ToRow = 30, ToCol = 1 };
+ var address4 = new FormulaRangeAddress { WorksheetIx = 0, FromRow = 31, FromCol = 1, ToRow = 40, ToCol = 1 };
+
+ var data1 = new List { 1 };
+ var data2 = new List { 2 };
+ var data3 = new List { 3 };
+ var data4 = new List { 4 };
+
+ _cache.SetFlattenedRange(address1, data1);
+ _cache.SetFlattenedRange(address2, data2);
+ _cache.SetFlattenedRange(address3, data3);
+
+ // Cache is now full (3/3)
+ Assert.IsNotNull(_cache.GetFlattenedRange(address1), "Address1 should be in cache");
+ Assert.IsNotNull(_cache.GetFlattenedRange(address2), "Address2 should be in cache");
+ Assert.IsNotNull(_cache.GetFlattenedRange(address3), "Address3 should be in cache");
+
+ // Adding 4th item should evict the oldest (address1)
+ _cache.SetFlattenedRange(address4, data4);
+
+ Assert.IsNull(_cache.GetFlattenedRange(address1), "Address1 should be evicted (oldest)");
+ Assert.IsNotNull(_cache.GetFlattenedRange(address2), "Address2 should still be in cache");
+ Assert.IsNotNull(_cache.GetFlattenedRange(address3), "Address3 should still be in cache");
+ Assert.IsNotNull(_cache.GetFlattenedRange(address4), "Address4 should be in cache");
+ }
+
+ [TestMethod]
+ public void MatchIndexes_ShouldCacheAndRetrieve()
+ {
+ _cache = new RangeCriteriaCache(_package);
+ var address = new FormulaRangeAddress { WorksheetIx = 0, FromRow = 1, FromCol = 1, ToRow = 10, ToCol = 1 };
+ var criteria = "test";
+ var indexes = new List { 1, 3, 5, 7 };
+
+ _cache.SetMatchIndexes(address, criteria, indexes);
+ var retrieved = _cache.GetMatchIndexes(address, criteria);
+
+ Assert.IsNotNull(retrieved);
+ Assert.AreEqual(4, retrieved.Count);
+ Assert.AreEqual(1, retrieved[0]);
+ Assert.AreEqual(7, retrieved[3]);
+ }
+
+ [TestMethod]
+ public void MatchIndexes_FIFO_ShouldEvictOldest()
+ {
+ _package.Settings.CalculationCacheSettings.MaxMatchIndexes = 3;
+ _cache = new RangeCriteriaCache(_package);
+
+ var address = new FormulaRangeAddress { WorksheetIx = 0, FromRow = 1, FromCol = 1, ToRow = 10, ToCol = 1 };
+ var criteria1 = "A";
+ var criteria2 = "B";
+ var criteria3 = "C";
+ var criteria4 = "D";
+
+ var indexes1 = new List { 1 };
+ var indexes2 = new List { 2 };
+ var indexes3 = new List { 3 };
+ var indexes4 = new List { 4 };
+
+ _cache.SetMatchIndexes(address, criteria1, indexes1);
+ _cache.SetMatchIndexes(address, criteria2, indexes2);
+ _cache.SetMatchIndexes(address, criteria3, indexes3);
+
+ // Cache is now full (3/3)
+ Assert.IsNotNull(_cache.GetMatchIndexes(address, criteria1), "Criteria1 should be in cache");
+ Assert.IsNotNull(_cache.GetMatchIndexes(address, criteria2), "Criteria2 should be in cache");
+ Assert.IsNotNull(_cache.GetMatchIndexes(address, criteria3), "Criteria3 should be in cache");
+
+ // Adding 4th item should evict the oldest (criteria1)
+ _cache.SetMatchIndexes(address, criteria4, indexes4);
+
+ Assert.IsNull(_cache.GetMatchIndexes(address, criteria1), "Criteria1 should be evicted (oldest)");
+ Assert.IsNotNull(_cache.GetMatchIndexes(address, criteria2), "Criteria2 should still be in cache");
+ Assert.IsNotNull(_cache.GetMatchIndexes(address, criteria3), "Criteria3 should still be in cache");
+ Assert.IsNotNull(_cache.GetMatchIndexes(address, criteria4), "Criteria4 should be in cache");
+ }
+
+ [TestMethod]
+ public void MatchIndexes_WithFormulas_ShouldNotCache()
+ {
+ var ws = _package.Workbook.Worksheets[0];
+ ws.Cells["A1"].Formula = "=1+1";
+ ws.Cells["A2"].Value = 2;
+
+ _cache = new RangeCriteriaCache(_package);
+ var address = new FormulaRangeAddress { WorksheetIx = 0, FromRow = 1, FromCol = 1, ToRow = 2, ToCol = 1 };
+ var criteria = "test";
+ var indexes = new List { 1 };
+
+ _cache.SetMatchIndexes(address, criteria, indexes);
+
+ // Should not cache because range has formulas
+ var retrieved = _cache.GetMatchIndexes(address, criteria);
+ Assert.IsNull(retrieved, "Should not cache ranges with formulas");
+ }
+
+ [TestMethod]
+ public void MatchIndexes_DifferentCriteria_SamRange_ShouldCacheSeparately()
+ {
+ _cache = new RangeCriteriaCache(_package);
+ var address = new FormulaRangeAddress { WorksheetIx = 0, FromRow = 1, FromCol = 1, ToRow = 10, ToCol = 1 };
+
+ var indexes1 = new List { 1, 2 };
+ var indexes2 = new List { 3, 4 };
+
+ _cache.SetMatchIndexes(address, "criteriaA", indexes1);
+ _cache.SetMatchIndexes(address, "criteriaB", indexes2);
+
+ var retrieved1 = _cache.GetMatchIndexes(address, "criteriaA");
+ var retrieved2 = _cache.GetMatchIndexes(address, "criteriaB");
+
+ Assert.IsNotNull(retrieved1);
+ Assert.IsNotNull(retrieved2);
+ Assert.AreEqual(2, retrieved1.Count);
+ Assert.AreEqual(2, retrieved2.Count);
+ Assert.AreEqual(1, retrieved1[0]);
+ Assert.AreEqual(3, retrieved2[0]);
+ }
+
+ [TestMethod]
+ public void Clear_ShouldRemoveAllCachedData()
+ {
+ _cache = new RangeCriteriaCache(_package);
+ var address = new FormulaRangeAddress { WorksheetIx = 0, FromRow = 1, FromCol = 1, ToRow = 10, ToCol = 1 };
+
+ _cache.SetFlattenedRange(address, new List { 1, 2, 3 });
+ _cache.SetMatchIndexes(address, "test", new List { 1, 2 });
+
+ Assert.IsNotNull(_cache.GetFlattenedRange(address));
+ Assert.IsNotNull(_cache.GetMatchIndexes(address, "test"));
+
+ _cache.Clear();
+
+ Assert.IsNull(_cache.GetFlattenedRange(address));
+ Assert.IsNull(_cache.GetMatchIndexes(address, "test"));
+ }
+
+ [TestMethod]
+ public void FlattenedRange_UpdateExisting_ShouldNotDuplicate()
+ {
+ _package.Settings.CalculationCacheSettings.MaxFlattenedRanges = 2;
+ _cache = new RangeCriteriaCache(_package);
+ var address = new FormulaRangeAddress { WorksheetIx = 0, FromRow = 1, FromCol = 1, ToRow = 10, ToCol = 1 };
+
+ var data1 = new List { 1, 2, 3 };
+ var data2 = new List { 4, 5, 6 };
+
+ // Add same address twice with different data
+ _cache.SetFlattenedRange(address, data1);
+ _cache.SetFlattenedRange(address, data2);
+
+ var retrieved = _cache.GetFlattenedRange(address);
+
+ // Should have updated value, not duplicated
+ Assert.IsNotNull(retrieved);
+ Assert.AreEqual(4, retrieved[0], "Should have updated data");
+ }
+
+ [TestMethod]
+ public void MatchIndexes_UpdateExisting_ShouldNotDuplicate()
+ {
+ _package.Settings.CalculationCacheSettings.MaxMatchIndexes = 2;
+ _cache = new RangeCriteriaCache(_package);
+ var address = new FormulaRangeAddress { WorksheetIx = 0, FromRow = 1, FromCol = 1, ToRow = 10, ToCol = 1 };
+ var criteria = "test";
+
+ var indexes1 = new List { 1, 2 };
+ var indexes2 = new List { 3, 4 };
+
+ // Add same address+criteria twice with different data
+ _cache.SetMatchIndexes(address, criteria, indexes1);
+ _cache.SetMatchIndexes(address, criteria, indexes2);
+
+ var retrieved = _cache.GetMatchIndexes(address, criteria);
+
+ // Should have updated value, not duplicated
+ Assert.IsNotNull(retrieved);
+ Assert.AreEqual(3, retrieved[0], "Should have updated indexes");
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/EPPlusTest/FormulaParsing/Excel/Functions/RefAndLookup/GetPivotData/GetPivotDataTests_CaluclatedFields2.cs b/src/EPPlusTest/FormulaParsing/Excel/Functions/RefAndLookup/GetPivotData/GetPivotDataTests_CaluclatedFields2.cs
new file mode 100644
index 0000000000..932ac57f85
--- /dev/null
+++ b/src/EPPlusTest/FormulaParsing/Excel/Functions/RefAndLookup/GetPivotData/GetPivotDataTests_CaluclatedFields2.cs
@@ -0,0 +1,335 @@
+using Microsoft.VisualStudio.TestTools.UnitTesting;
+using OfficeOpenXml;
+using OfficeOpenXml.Table.PivotTable;
+using OfficeOpenXml.Table.PivotTable.Calculation;
+using System.Collections.Generic;
+
+namespace EPPlus.Core.Tests.Table.PivotTable
+{
+ [TestClass]
+ public class GetPivotDataTests_CalculatedFields2
+ {
+ private ExcelPackage _package;
+ private ExcelWorksheet _dataWs;
+
+ [TestInitialize]
+ public void Initialize()
+ {
+ _package = new ExcelPackage();
+ _dataWs = _package.Workbook.Worksheets.Add("Data");
+
+ // Headers
+ _dataWs.Cells["A1"].Value = "Department";
+ _dataWs.Cells["B1"].Value = "Basic Pay";
+ _dataWs.Cells["C1"].Value = "Overtime";
+ _dataWs.Cells["D1"].Value = "Bonus";
+
+ // Sales department
+ _dataWs.Cells["A2"].Value = "Sales";
+ _dataWs.Cells["B2"].Value = 5000d;
+ _dataWs.Cells["C2"].Value = 500d;
+ _dataWs.Cells["D2"].Value = 1000d;
+
+ _dataWs.Cells["A3"].Value = "Sales";
+ _dataWs.Cells["B3"].Value = 6000d;
+ _dataWs.Cells["C3"].Value = 600d;
+ _dataWs.Cells["D3"].Value = 1200d;
+
+ // IT department
+ _dataWs.Cells["A4"].Value = "IT";
+ _dataWs.Cells["B4"].Value = 7000d;
+ _dataWs.Cells["C4"].Value = 300d;
+ _dataWs.Cells["D4"].Value = 800d;
+
+ _dataWs.Cells["A5"].Value = "IT";
+ _dataWs.Cells["B5"].Value = 7500d;
+ _dataWs.Cells["C5"].Value = 400d;
+ _dataWs.Cells["D5"].Value = 900d;
+ }
+
+ [TestCleanup]
+ public void Cleanup()
+ {
+ _package?.Dispose();
+ }
+
+ [TestMethod]
+ public void GetPivotData_CalculatedField_ShouldReturnCorrectValue()
+ {
+ // Arrange
+ var ws = _package.Workbook.Worksheets.Add("Pivot");
+ var pt = ws.PivotTables.Add(ws.Cells["A1"], _dataWs.Cells["A1:D5"], "TestPivot");
+
+ pt.RowFields.Add(pt.Fields["Department"]);
+
+ pt.Fields.AddCalculatedField("Total Comp", "'Basic Pay'+'Overtime'+'Bonus'");
+ var df = pt.DataFields.Add(pt.Fields["Total Comp"]);
+ df.Function = DataFieldFunctions.Sum;
+ df.Name = "Sum of Total Comp";
+
+ // Act
+ pt.Calculate(refreshCache: true);
+
+ var criteria = new List
+ {
+ new PivotDataFieldItemSelection("Department", "Sales")
+ };
+
+ var result = pt.GetPivotData("Sum of Total Comp", criteria);
+
+ // Assert
+ Assert.IsNotNull(result, "Result should not be null");
+ Assert.IsFalse(result is ExcelErrorValue,
+ $"GetPivotData should not return #REF! error, got: {result}");
+
+ // Sales: (5000+500+1000) + (6000+600+1200) = 14300
+ Assert.AreEqual(14300d, (double)result, 0.001,
+ "Calculated field value for Sales should be 14300");
+ }
+
+ [TestMethod]
+ public void GetPivotData_CalculatedField_GrandTotal_ShouldWork()
+ {
+ // Arrange
+ var ws = _package.Workbook.Worksheets.Add("Pivot");
+ var pt = ws.PivotTables.Add(ws.Cells["A1"], _dataWs.Cells["A1:D5"], "TestPivot");
+
+ pt.RowFields.Add(pt.Fields["Department"]);
+ pt.Fields.AddCalculatedField("All Pay", "'Basic Pay'+'Overtime'+'Bonus'");
+ var df = pt.DataFields.Add(pt.Fields["All Pay"]);
+ df.Function = DataFieldFunctions.Sum;
+ df.Name = "Sum of All Pay";
+
+ // Act
+ pt.Calculate(refreshCache: true);
+ var grandTotal = pt.GetPivotData("Sum of All Pay");
+
+ // Assert
+ Assert.IsNotNull(grandTotal);
+ Assert.IsFalse(grandTotal is ExcelErrorValue,
+ string.Format("Grand total should not return error, got: {0}", grandTotal));
+
+ // Total: Sales(14300) + IT(16900) = 31200
+ Assert.AreEqual(31200d, (double)grandTotal, 0.001);
+ }
+
+ [TestMethod]
+ public void GetPivotData_MultipleCalculatedFields_ShouldWorkIndependently()
+ {
+ // Arrange
+ var ws = _package.Workbook.Worksheets.Add("Pivot");
+ var pt = ws.PivotTables.Add(ws.Cells["A1"], _dataWs.Cells["A1:D5"], "TestPivot");
+
+ pt.RowFields.Add(pt.Fields["Department"]);
+
+ pt.Fields.AddCalculatedField("Base Plus OT", "'Basic Pay'+'Overtime'");
+ pt.Fields.AddCalculatedField("Total Comp", "'Basic Pay'+'Overtime'+'Bonus'");
+
+ var df1 = pt.DataFields.Add(pt.Fields["Base Plus OT"]);
+ df1.Function = DataFieldFunctions.Sum;
+ df1.Name = "Sum of Base Plus OT";
+
+ var df2 = pt.DataFields.Add(pt.Fields["Total Comp"]);
+ df2.Function = DataFieldFunctions.Sum;
+ df2.Name = "Sum of Total Comp";
+
+ // Act
+ pt.Calculate(refreshCache: true);
+
+ var criteriaIT = new List
+ {
+ new PivotDataFieldItemSelection("Department", "IT")
+ };
+
+ var result1 = pt.GetPivotData("Sum of Base Plus OT", criteriaIT);
+ var result2 = pt.GetPivotData("Sum of Total Comp", criteriaIT);
+
+ // Assert
+ Assert.IsFalse(result1 is ExcelErrorValue);
+ Assert.IsFalse(result2 is ExcelErrorValue);
+
+ // IT Base + OT: (7000+300) + (7500+400) = 15200
+ Assert.AreEqual(15200d, (double)result1, 0.001);
+
+ // IT Total: (7000+300+800) + (7500+400+900) = 16900
+ Assert.AreEqual(16900d, (double)result2, 0.001);
+ }
+
+ [TestMethod]
+ public void GetPivotData_CalculatedField_CalculatedItems_ShouldBePopulated()
+ {
+ // Arrange
+ var ws = _package.Workbook.Worksheets.Add("Pivot");
+ var pt = ws.PivotTables.Add(ws.Cells["A1"], _dataWs.Cells["A1:D5"], "TestPivot");
+
+ pt.RowFields.Add(pt.Fields["Department"]);
+ pt.Fields.AddCalculatedField("Sum Total", "'Basic Pay'+'Overtime'");
+ var df = pt.DataFields.Add(pt.Fields["Sum Total"]);
+ df.Function = DataFieldFunctions.Sum;
+ df.Name = "Sum of Sum Total";
+
+ // Act
+ pt.Calculate(refreshCache: true);
+
+ // Assert
+ Assert.IsTrue(pt.IsCalculated, "Pivot table should be marked as calculated");
+ Assert.IsNotNull(pt.CalculatedItems, "CalculatedItems should not be null");
+
+ var dfIndex = pt.DataFields.IndexOf(df);
+ Assert.IsTrue(dfIndex >= 0, "DataField should be in collection");
+ Assert.IsTrue(dfIndex < pt.CalculatedItems.Count,
+ "Should have CalculatedItems entry for this index");
+
+ var store = pt.CalculatedItems[dfIndex];
+ Assert.IsNotNull(store, "Store should not be null");
+ Assert.IsTrue(store.Count > 0,
+ "CalculatedItems store should be populated (this was empty before the fix)");
+ }
+
+ [TestMethod]
+ public void GetPivotData_CalculatedFieldMixedWithRegular_ShouldWork()
+ {
+ // Arrange
+ var ws = _package.Workbook.Worksheets.Add("Pivot");
+ var pt = ws.PivotTables.Add(ws.Cells["A1"], _dataWs.Cells["A1:D5"], "TestPivot");
+
+ pt.RowFields.Add(pt.Fields["Department"]);
+
+ // Add regular data field first
+ var dfBasic = pt.DataFields.Add(pt.Fields["Basic Pay"]);
+ dfBasic.Function = DataFieldFunctions.Sum;
+ dfBasic.Name = "Sum of Basic Pay";
+
+ // Add calculated field
+ pt.Fields.AddCalculatedField("Total", "'Basic Pay'+'Overtime'+'Bonus'");
+ var dfCalc = pt.DataFields.Add(pt.Fields["Total"]);
+ dfCalc.Function = DataFieldFunctions.Sum;
+ dfCalc.Name = "Sum of Total";
+
+ // Act
+ pt.Calculate(refreshCache: true);
+
+ var criteriaSales = new List
+ {
+ new PivotDataFieldItemSelection("Department", "Sales")
+ };
+
+ var basicResult = pt.GetPivotData("Sum of Basic Pay", criteriaSales);
+ var calcResult = pt.GetPivotData("Sum of Total", criteriaSales);
+
+ // Assert
+ Assert.IsFalse(basicResult is ExcelErrorValue, "Regular field should work");
+ Assert.IsFalse(calcResult is ExcelErrorValue, "Calculated field should work");
+
+ Assert.AreEqual(11000d, (double)basicResult, 0.001); // 5000 + 6000
+ Assert.AreEqual(14300d, (double)calcResult, 0.001);
+ }
+
+ [TestMethod]
+ public void GetPivotData_CalculatedFieldWithComplexFormula_ShouldWork()
+ {
+ // Arrange
+ var ws = _package.Workbook.Worksheets.Add("Pivot");
+ var pt = ws.PivotTables.Add(ws.Cells["A1"], _dataWs.Cells["A1:D5"], "TestPivot");
+
+ pt.RowFields.Add(pt.Fields["Department"]);
+
+ // Complex formula with multiple operations
+ pt.Fields.AddCalculatedField("Complex Calc",
+ "'Basic Pay' * 2 + 'Overtime' - 'Bonus'");
+ var df = pt.DataFields.Add(pt.Fields["Complex Calc"]);
+ df.Function = DataFieldFunctions.Sum;
+ df.Name = "Sum of Complex Calc";
+
+ // Act
+ pt.Calculate(refreshCache: true);
+
+ var criteriaIT = new List
+ {
+ new PivotDataFieldItemSelection("Department", "IT")
+ };
+
+ var result = pt.GetPivotData("Sum of Complex Calc", criteriaIT);
+
+ // Assert
+ Assert.IsFalse(result is ExcelErrorValue);
+
+ // IT: (7000*2+300-800) + (7500*2+400-900)
+ // = (14000+300-800) + (15000+400-900)
+ // = 13500 + 14500 = 28000
+ Assert.AreEqual(28000d, (double)result, 0.001);
+ }
+
+ [TestMethod]
+ public void GetPivotData_CalculatedFieldOnlyWithFormula_NoRegularFields()
+ {
+ // Arrange
+ var ws = _package.Workbook.Worksheets.Add("Pivot");
+ var pt = ws.PivotTables.Add(ws.Cells["A1"], _dataWs.Cells["A1:D5"], "TestPivot");
+
+ pt.RowFields.Add(pt.Fields["Department"]);
+
+ // Only add calculated field, no regular data fields
+ pt.Fields.AddCalculatedField("Only Calc", "'Basic Pay'+'Overtime'");
+ var df = pt.DataFields.Add(pt.Fields["Only Calc"]);
+ df.Function = DataFieldFunctions.Sum;
+ df.Name = "Sum of Only Calc";
+
+ // Act
+ pt.Calculate(refreshCache: true);
+
+ var criteriaIT = new List
+ {
+ new PivotDataFieldItemSelection("Department", "IT")
+ };
+
+ var result = pt.GetPivotData("Sum of Only Calc", criteriaIT);
+
+ // Assert
+ Assert.IsNotNull(result);
+ Assert.IsFalse(result is ExcelErrorValue,
+ "Should work even when pivot has only calculated fields");
+
+ // IT: (7000+300) + (7500+400) = 15200
+ Assert.AreEqual(15200d, (double)result, 0.001);
+ }
+
+ [TestMethod]
+ public void GetPivotData_CalculatedField_DifferentDepartments()
+ {
+ // Arrange
+ var ws = _package.Workbook.Worksheets.Add("Pivot");
+ var pt = ws.PivotTables.Add(ws.Cells["A1"], _dataWs.Cells["A1:D5"], "TestPivot");
+
+ pt.RowFields.Add(pt.Fields["Department"]);
+ pt.Fields.AddCalculatedField("Total Pay", "'Basic Pay'+'Overtime'+'Bonus'");
+ var df = pt.DataFields.Add(pt.Fields["Total Pay"]);
+ df.Function = DataFieldFunctions.Sum;
+ df.Name = "Sum of Total Pay";
+
+ // Act
+ pt.Calculate(refreshCache: true);
+
+ var criteriaSales = new List
+ {
+ new PivotDataFieldItemSelection("Department", "Sales")
+ };
+
+ var criteriaIT = new List
+ {
+ new PivotDataFieldItemSelection("Department", "IT")
+ };
+
+ var salesResult = pt.GetPivotData("Sum of Total Pay", criteriaSales);
+ var itResult = pt.GetPivotData("Sum of Total Pay", criteriaIT);
+
+ // Assert
+ Assert.IsFalse(salesResult is ExcelErrorValue);
+ Assert.IsFalse(itResult is ExcelErrorValue);
+
+ // Sales: 14300, IT: 16900
+ Assert.AreEqual(14300d, (double)salesResult, 0.001);
+ Assert.AreEqual(16900d, (double)itResult, 0.001);
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/EPPlusTest/FormulaParsing/Excel/OperatorsTests/RangeOperationsOperatorTests.cs b/src/EPPlusTest/FormulaParsing/Excel/OperatorsTests/RangeOperationsOperatorTests.cs
index 740c5dc98a..9deb102102 100644
--- a/src/EPPlusTest/FormulaParsing/Excel/OperatorsTests/RangeOperationsOperatorTests.cs
+++ b/src/EPPlusTest/FormulaParsing/Excel/OperatorsTests/RangeOperationsOperatorTests.cs
@@ -529,5 +529,18 @@ public void ShouldCalculateRangesDoubleWithExpOperator()
Assert.AreEqual(1000d, range.GetValue(0, 0));
Assert.AreEqual(32d, range.GetValue(1, 0));
}
+
+ [TestMethod]
+ public void ShouldHandleDoubleMinus()
+ {
+ using var package = new ExcelPackage();
+ var sheet = package.Workbook.Worksheets.Add("Test");
+ sheet.Cells["A1"].Formula = "TRUE()";
+ sheet.Cells["A2"].Formula = "FALSE()";
+ sheet.Cells["B1"].Formula = "--A1:A2";
+ sheet.Calculate();
+ Assert.AreEqual(1d, sheet.Cells["B1"].Value);
+ Assert.AreEqual(0d, sheet.Cells["B2"].Value);
+ }
}
}
diff --git a/src/EPPlusTest/InCellImages/InCellImagesCacheTests.cs b/src/EPPlusTest/InCellImages/InCellImagesCacheTests.cs
index 1d8568ada8..dd0309019d 100644
--- a/src/EPPlusTest/InCellImages/InCellImagesCacheTests.cs
+++ b/src/EPPlusTest/InCellImages/InCellImagesCacheTests.cs
@@ -1,10 +1,12 @@
-using Microsoft.VisualStudio.TestTools.UnitTesting;
+using EPPlusTest.Properties;
+using Microsoft.VisualStudio.TestTools.UnitTesting;
+using OfficeOpenXml;
using System;
using System.Collections.Generic;
+using System.IO;
using System.Linq;
using System.Text;
-using OfficeOpenXml;
-using EPPlusTest.Properties;
+using OfficeOpenXml.CellPictures;
namespace EPPlusTest.InCellImages
{
@@ -56,6 +58,59 @@ public void ShouldNotRemoveRichDataWhenMoreReferencesExists()
SaveWorkbook("InCellPicturesReuseCache3.xlsx", package);
}
+ [TestMethod]
+ public void ShouldNotRemoveRichDataWhenMoreReferencesExistsWhenReading()
+ {
+ var ms = new MemoryStream();
+
+ using (var package = new ExcelPackage())
+ {
+ var sheet = package.Workbook.Worksheets.Add("Sheet 1");
+ sheet.Cells["A1"].Picture.Set(Resources.Png2ByteArray);
+ sheet.Cells["A2"].Picture.Set(Resources.Png2ByteArray);
+ var vm1 = sheet._metadataStore.GetValue(1, 1);
+ var vm2 = sheet._metadataStore.GetValue(2, 1);
+ Assert.AreEqual(vm1, vm2);
+ Assert.AreEqual(1, package.Workbook.RichData.Db.Values.Count());
+ package.SaveAs(ms);
+ }
+ ms.Position = 0;
+ ms.Seek(0, SeekOrigin.Begin);
+
+ using (var package = new ExcelPackage(ms))
+ {
+ var ws = package.Workbook.Worksheets[0];
+
+ var pic = ws.Cells[1, 1].Value as ExcelCellPicture;
+
+ //Verify that the picture has been read into refs correctly
+ PictureCacheKey key = null;
+ if (pic != null)
+ {
+ if (pic.PictureType == ExcelCellPictureTypes.LocalImage)
+ {
+ key = new LocalImageCacheKey(pic.ImageUri, pic.CalcOrigin, pic.AltText);
+ }
+ Assert.IsTrue(ws.Workbook.CellPictureReferenceCache.Contains(key));
+ var numberReferencesLeft = ws.Workbook.CellPictureReferenceCache.GetNumberOfReferences(key);
+ Assert.AreEqual(2, numberReferencesLeft);
+ }
+
+ ws.Cells["A1"].Picture.Remove();
+ Assert.IsNull(ws.Cells["A1"].Value);
+ var vm3 = ws._metadataStore.GetValue(1, 1);
+ Assert.AreEqual(0u, vm3.vm);
+ Assert.AreEqual(1, package.Workbook.RichData.Db.Values.Count());
+
+ //Verify that the picture ref has been removed
+ Assert.IsTrue(ws.Workbook.CellPictureReferenceCache.Contains(key));
+ var numberOfRefs = ws.Workbook.CellPictureReferenceCache.GetNumberOfReferences(key);
+ Assert.AreEqual(1, numberOfRefs);
+
+ SaveWorkbook("InCellPicturesReuseCache3_OnRead.xlsx", package);
+ }
+ }
+
[TestMethod]
public void ShouldRemoveRichDataWhenLastReferenceRemoved()
{
@@ -78,5 +133,34 @@ public void ShouldRemoveRichDataWhenLastReferenceRemoved()
Assert.AreEqual(0, package.Workbook.RichData.Db.Values.Count, "RichDataValue still exists");
SaveWorkbook("InCellPicturesReuseCache4.xlsx", package);
}
+
+ [TestMethod]
+ public void ReCalculateInCellImageShouldWork()
+ {
+ using (var p = OpenPackage("ReacalculateInCellImage.xlsx", true))
+ {
+ var ws = p.Workbook.Worksheets.Add("Sheet");
+
+ ws.Cells["A1"].Picture.Set(Resources.Png2ByteArray);
+ var pic = ws.Cells[1, 1].Value as ExcelCellPicture;
+
+ ws.Cells["B1"].Formula = "A1";
+
+ ws.Calculate();
+
+ //Verify that the picture has added to refs correctly
+ PictureCacheKey key = new LocalImageCacheKey(pic.ImageUri, pic.CalcOrigin, pic.AltText);
+ var numRefs = ws.Workbook.CellPictureReferenceCache.GetNumberOfReferences(key);
+ Assert.AreEqual(1, numRefs);
+
+ ws.Calculate();
+
+ PictureCacheKey key2 = new LocalImageCacheKey(pic.ImageUri, pic.CalcOrigin, pic.AltText);
+ var numRefs2 = ws.Workbook.CellPictureReferenceCache.GetNumberOfReferences(key);
+ Assert.AreEqual(1, numRefs2);
+
+ SaveAndCleanup(p);
+ }
+ }
}
}
diff --git a/src/EPPlusTest/InCellImages/WebImagesTests.cs b/src/EPPlusTest/InCellImages/WebImagesTests.cs
index 17d4297d6a..16ec858bc6 100644
--- a/src/EPPlusTest/InCellImages/WebImagesTests.cs
+++ b/src/EPPlusTest/InCellImages/WebImagesTests.cs
@@ -2,8 +2,10 @@
using Microsoft.VisualStudio.TestTools.UnitTesting;
using OfficeOpenXml;
using OfficeOpenXml.CellPictures;
+using OfficeOpenXml.RichData.RichValues.WebImages;
using System;
using System.Collections.Generic;
+using System.IO;
using System.Linq;
using System.Text;
@@ -49,5 +51,157 @@ public void WebImagesShouldBeRemovedWhenOverwritten()
SaveWorkbook("WebImages_Removed.xlsx", p);
}
+
+ [TestMethod]
+ public void DoublePictDelete()
+ {
+ using (var package = OpenTemplatePackage("DoublePictInCellWeb.xlsx"))
+ {
+ var ws = package.Workbook.Worksheets[0];
+
+ //ws.Calculate();
+
+ ws.Cells["B3"].Picture.Remove();
+
+ SaveAndCleanup(package);
+ }
+ }
+
+ [TestMethod]
+ public void ShouldNotRemoveRichDataWhenMoreReferencesExistsWhenReading()
+ {
+ var ms = new MemoryStream();
+
+ using (var p = new ExcelPackage())
+ {
+ var sheet = p.Workbook.Worksheets.Add("Sheet");
+ sheet.Cells["A1"].Formula = "IMAGE(\"https://epplussoftware.com/img/EPPlus-logo-full.png\")";
+ sheet.Cells["A2"].Formula = "IMAGE(\"https://epplussoftware.com/img/EPPlus-logo-full.png\")";
+ sheet.Calculate();
+ Assert.IsTrue(sheet.Cells["A1"].Picture.Exists);
+ Assert.IsTrue(sheet.Cells["A2"].Picture.Exists);
+ Assert.AreEqual(ExcelCellPictureTypes.WebImage, sheet.Cells["A1"].Picture.Get().PictureType);
+ Assert.AreEqual(ExcelCellPictureTypes.WebImage, sheet.Cells["A2"].Picture.Get().PictureType);
+ p.SaveAs(ms);
+ }
+ ms.Position = 0;
+ ms.Seek(0, SeekOrigin.Begin);
+
+ using (var package = new ExcelPackage(ms))
+ {
+ var ws = package.Workbook.Worksheets[0];
+
+ var pic = ws.Cells[1, 1].Value as ExcelCellPicture;
+
+ //Verify that the picture has been read into refs correctly
+ PictureCacheKey key = null;
+ if (pic != null)
+ {
+
+ key = new WebPictureCacheKey(pic.ExternalAddress, pic.AltText, pic.CalcOrigin, pic.Sizing ?? WebImageSizing.FitToCellMaintainRatio, null, null);
+
+ Assert.IsTrue(ws.Workbook.CellPictureReferenceCache.Contains(key));
+ var numberReferencesLeft = ws.Workbook.CellPictureReferenceCache.GetNumberOfReferences(key);
+ Assert.AreEqual(2, numberReferencesLeft);
+ }
+
+ ws.Cells["A1"].Picture.Remove();
+ Assert.IsNull(ws.Cells["A1"].Value);
+ var vm3 = ws._metadataStore.GetValue(1, 1);
+ Assert.AreEqual(0u, vm3.vm);
+ Assert.AreEqual(1, package.Workbook.RichData.Db.Values.Count());
+
+ //Verify that the picture ref has been removed
+ Assert.IsTrue(ws.Workbook.CellPictureReferenceCache.Contains(key));
+ var numberOfRefs = ws.Workbook.CellPictureReferenceCache.GetNumberOfReferences(key);
+ Assert.AreEqual(1, numberOfRefs);
+
+ SaveWorkbook("InCellPicturesReuseCache3_OnRead.xlsx", package);
+ }
+ }
+
+ [TestMethod]
+ public void ReCalculateWebImagesShouldWork()
+ {
+ using (var p = OpenPackage("ReacalculateWebImage.xlsx", true))
+ {
+ var ws = p.Workbook.Worksheets.Add("Sheet");
+ ws.Cells["A1"].Formula = "IMAGE(\"https://epplussoftware.com/img/EPPlus-logo-full.png\")";
+ ws.Calculate();
+ Assert.IsTrue(ws.Cells["A1"].Picture.Exists);
+ Assert.AreEqual(ExcelCellPictureTypes.WebImage, ws.Cells["A1"].Picture.Get().PictureType);
+
+ var pic = ws.Cells[1, 1].Value as ExcelCellPicture;
+
+ //Verify that the picture has added to refs correctly
+ PictureCacheKey key = new WebPictureCacheKey(pic.ExternalAddress, pic.AltText, pic.CalcOrigin, pic.Sizing ?? WebImageSizing.FitToCellMaintainRatio, null, null);
+ var numRefs = ws.Workbook.CellPictureReferenceCache.GetNumberOfReferences(key);
+ Assert.AreEqual(1, numRefs);
+
+ ws.Calculate();
+
+ PictureCacheKey key2 = new WebPictureCacheKey(pic.ExternalAddress, pic.AltText, pic.CalcOrigin, pic.Sizing ?? WebImageSizing.FitToCellMaintainRatio, null, null);
+ var numRefs2 = ws.Workbook.CellPictureReferenceCache.GetNumberOfReferences(key);
+ Assert.AreEqual(1, numRefs2);
+
+ SaveAndCleanup(p);
+ }
+ }
+
+ [TestMethod]
+ public void ShouldNotRemoveRichDataWhenMoreReferencesExistsWhenReadingWithCalculate()
+ {
+ var ms = new MemoryStream();
+
+ using (var p = new ExcelPackage())
+ {
+ var sheet = p.Workbook.Worksheets.Add("Sheet");
+ sheet.Cells["A1"].Formula = "IMAGE(\"https://epplussoftware.com/img/EPPlus-logo-full.png\")";
+ sheet.Cells["A2"].Formula = "IMAGE(\"https://epplussoftware.com/img/EPPlus-logo-full.png\")";
+ sheet.Calculate();
+ Assert.IsTrue(sheet.Cells["A1"].Picture.Exists);
+ Assert.IsTrue(sheet.Cells["A2"].Picture.Exists);
+ Assert.AreEqual(ExcelCellPictureTypes.WebImage, sheet.Cells["A1"].Picture.Get().PictureType);
+ Assert.AreEqual(ExcelCellPictureTypes.WebImage, sheet.Cells["A2"].Picture.Get().PictureType);
+ p.SaveAs(ms);
+ }
+ ms.Position = 0;
+ ms.Seek(0, SeekOrigin.Begin);
+
+ using (var package = new ExcelPackage(ms))
+ {
+ var ws = package.Workbook.Worksheets[0];
+
+ //This causes issues as the NumberOfReferences is doubled while there's actually the same amount left
+ ws.Calculate();
+
+ var pic = ws.Cells[1, 1].Value as ExcelCellPicture;
+
+ //Verify that the picture has been read into refs correctly
+ PictureCacheKey key = null;
+ if (pic != null)
+ {
+
+ key = new WebPictureCacheKey(pic.ExternalAddress, pic.AltText, pic.CalcOrigin, pic.Sizing ?? WebImageSizing.FitToCellMaintainRatio, null, null);
+
+ Assert.IsTrue(ws.Workbook.CellPictureReferenceCache.Contains(key));
+ var numberReferencesLeft = ws.Workbook.CellPictureReferenceCache.GetNumberOfReferences(key);
+ Assert.AreEqual(2, numberReferencesLeft);
+ }
+
+ ws.Cells["A1"].Picture.Remove();
+ Assert.IsNull(ws.Cells["A1"].Value);
+ var vm3 = ws._metadataStore.GetValue(1, 1);
+ Assert.AreEqual(0u, vm3.vm);
+ Assert.AreEqual(1, package.Workbook.RichData.Db.Values.Count());
+
+ //Verify that the picture ref has been removed
+ Assert.IsTrue(ws.Workbook.CellPictureReferenceCache.Contains(key));
+ var numberOfRefs = ws.Workbook.CellPictureReferenceCache.GetNumberOfReferences(key);
+ Assert.AreEqual(1, numberOfRefs);
+
+ SaveWorkbook("InCellPicturesReuseCache3_WebImageOnRead.xlsx", package);
+ }
+ }
}
}
diff --git a/src/EPPlusTest/Issues/ChartIssues.cs b/src/EPPlusTest/Issues/ChartIssues.cs
index f537b666c3..73ffc622eb 100644
--- a/src/EPPlusTest/Issues/ChartIssues.cs
+++ b/src/EPPlusTest/Issues/ChartIssues.cs
@@ -117,7 +117,7 @@ public void s598()
chart.Legend.TextSettings.Fill.Style = OfficeOpenXml.Drawing.eFillStyle.SolidFill;
chart.Legend.TextSettings.Fill.SolidFill.Color.SetRgbColor(System.Drawing.Color.Black);
- chart.Legend.TextSettings.Fill.Transparancy = 0;
+ chart.Legend.TextSettings.Fill.Transparency = 0;
chart.Legend.TextSettings.Effect.SetPresetReflection(OfficeOpenXml.Drawing.ePresetExcelReflectionType.FullTouching);
SaveAndCleanup(p);
diff --git a/src/EPPlusTest/Issues/ConditionalFormattingIssues.cs b/src/EPPlusTest/Issues/ConditionalFormattingIssues.cs
index 1c6e8aa05e..75caf82d1d 100644
--- a/src/EPPlusTest/Issues/ConditionalFormattingIssues.cs
+++ b/src/EPPlusTest/Issues/ConditionalFormattingIssues.cs
@@ -1,6 +1,8 @@
using Microsoft.VisualStudio.TestTools.UnitTesting;
using OfficeOpenXml;
+using OfficeOpenXml.Compatibility.System.Drawing;
using OfficeOpenXml.ConditionalFormatting;
+using OfficeOpenXml.ConditionalFormatting.Contracts;
using OfficeOpenXml.Style;
using System.Globalization;
using System.Threading;
@@ -75,5 +77,33 @@ public void Test1_Input_ExpectedOutput()
SaveAndCleanup(package);
Thread.CurrentThread.CurrentCulture = currentCulture;
}
+ [TestMethod]
+ public void s1025()
+ {
+ using var package = OpenTemplatePackage("s1025.xlsx"); //attached template, ExampleWB.xlsx
+ ExcelWorksheet ws = package.Workbook.Worksheets[0];
+
+ IExcelConditionalFormattingBetween condGreen = ws.ConditionalFormatting.AddBetween(ws.Cells[5, 2, 25, 2]);
+ condGreen.Style.Fill.PatternType = OfficeOpenXml.Style.ExcelFillStyle.Solid;
+ condGreen.Style.Fill.BackgroundColor.Color = ColorTranslator.FromHtml("#A9D08E");
+ condGreen.Formula = ws.Cells[6, 8].FullAddressAbsolute.ToString();
+ condGreen.Formula2 = ws.Cells[6, 9].FullAddressAbsolute.ToString();
+ condGreen.Priority = 104;
+
+ IExcelConditionalFormattingBetween condYellow = ws.ConditionalFormatting.AddBetween(ws.Cells[5, 2, 25, 2]);
+ condYellow.Style.Fill.PatternType = OfficeOpenXml.Style.ExcelFillStyle.Solid;
+ condYellow.Style.Fill.BackgroundColor.Color = ColorTranslator.FromHtml("#FFE699");
+ condYellow.Formula = ws.Cells[7, 8].FullAddressAbsolute.ToString();
+ condYellow.Formula2 = ws.Cells[7, 9].FullAddressAbsolute.ToString();
+ condYellow.Priority = 105;
+
+ IExcelConditionalFormattingGreaterThan condRed = ws.ConditionalFormatting.AddGreaterThan(ws.Cells[5, 2, 25, 2]);
+ condRed.Style.Fill.PatternType = OfficeOpenXml.Style.ExcelFillStyle.Solid;
+ condRed.Style.Fill.BackgroundColor.Color = ColorTranslator.FromHtml("#FF7979");
+ condRed.Formula = ws.Cells[9, 8].FullAddressAbsolute.ToString();
+ condRed.Priority = 106;
+
+ SaveAndCleanup(package);
+ }
}
}
diff --git a/src/EPPlusTest/Issues/DefinedNameIssues.cs b/src/EPPlusTest/Issues/DefinedNameIssues.cs
index d180ac253d..2ac08c50fd 100644
--- a/src/EPPlusTest/Issues/DefinedNameIssues.cs
+++ b/src/EPPlusTest/Issues/DefinedNameIssues.cs
@@ -232,7 +232,7 @@ static void RunTest(string name, Func<(ExcelPackage pkg, ExcelWorksheet ws1, Exc
try { fromWs1 = ctx.ws1.Calculate("'Sheet2'!C1"); }
catch (Exception ex) { fromWs1 = $"EXCEPTION: {ex.GetType().Name}: {ex.Message}"; }
return $"ws2.Calculate(\"'Sheet2'!C1\") => {fromWs2}\nws1.Calculate(\"'Sheet2'!C1\") => {fromWs1}";
- Assert.AreEqual(fromWs1, 10);
+ //Assert.AreEqual(fromWs1, 10);
});
RunTest("Sanity: removing sheet-scoped name fixes formula-string eval", ctx =>
diff --git a/src/EPPlusTest/Issues/DrawingIssues.cs b/src/EPPlusTest/Issues/DrawingIssues.cs
index e413317fd2..5f8d9ba27e 100644
--- a/src/EPPlusTest/Issues/DrawingIssues.cs
+++ b/src/EPPlusTest/Issues/DrawingIssues.cs
@@ -187,6 +187,36 @@ public void EnsureWhiteSpaceIsPreservedInShapes()
Assert.AreEqual(" ", retText);
}
+ [TestMethod]
+ public void i2278()
+ {
+ using (var package = OpenTemplatePackage("i2278.xlsx"))
+ {
+ using var target = new ExcelPackage();
+ var targetSheet = target.Workbook.Worksheets.Add("Sheet1");
+
+ var worksheet = package.Workbook.Worksheets[0];
+
+ foreach (var drawing in worksheet.Drawings)
+ {
+ drawing.Copy(targetSheet, drawing.From.Row, drawing.From.Column, drawing.From.RowOff, drawing.From.ColumnOff);
+ }
+
+ SaveAndCleanup(package);
+ }
+ }
+ [TestMethod]
+ public void i2303()
+ {
+ using (var package = OpenTemplatePackage("i2303.xlsx"))
+ {
+ var sheet = package.Workbook.Worksheets.First();
+ var drawing = sheet.Drawings.First();
+ var image = drawing.As.Picture.Image;
+
+ SaveAndCleanup(package);
+ }
+ }
}
}
diff --git a/src/EPPlusTest/Issues/FormulaCalculationIssues.cs b/src/EPPlusTest/Issues/FormulaCalculationIssues.cs
index 3747cf7a10..07b9bb3a81 100644
--- a/src/EPPlusTest/Issues/FormulaCalculationIssues.cs
+++ b/src/EPPlusTest/Issues/FormulaCalculationIssues.cs
@@ -3,6 +3,7 @@
using OfficeOpenXml.FormulaParsing;
using OfficeOpenXml.FormulaParsing.Excel.Functions.MathFunctions;
using OfficeOpenXml.FormulaParsing.LexicalAnalysis;
+using OfficeOpenXml.FormulaParsing.Logging;
using OfficeOpenXml.Sorting;
using System;
using System.Collections.Generic;
diff --git a/src/EPPlusTest/Issues/PackageIssues.cs b/src/EPPlusTest/Issues/PackageIssues.cs
index 0a8a4f644d..8bb6d73d8c 100644
--- a/src/EPPlusTest/Issues/PackageIssues.cs
+++ b/src/EPPlusTest/Issues/PackageIssues.cs
@@ -1,6 +1,12 @@
using Microsoft.VisualStudio.TestTools.UnitTesting;
using OfficeOpenXml;
+using System;
+using System.ComponentModel;
using System.Drawing;
+using System.Globalization;
+using System.IO;
+using System.Linq;
+using System.Threading;
namespace EPPlusTest.Issues
{
diff --git a/src/EPPlusTest/Issues/RangeTextIssues.cs b/src/EPPlusTest/Issues/RangeTextIssues.cs
index 9db0536dc6..027abf620f 100644
--- a/src/EPPlusTest/Issues/RangeTextIssues.cs
+++ b/src/EPPlusTest/Issues/RangeTextIssues.cs
@@ -65,5 +65,26 @@ public void i1964()
Assert.AreEqual("2021-12-31", cell);
}
}
+ [TestMethod]
+ public void i2276()
+ {
+ using (var package = OpenTemplatePackage("CurrencyTest2.xlsx"))
+ {
+ SwitchToCulture("nl-NL");
+ ExcelWorkbook workbook = package.Workbook;
+ ExcelWorksheet worksheet = workbook.Worksheets[0];
+
+ Assert.AreEqual("€ 12 347", worksheet.Cells["A1"].Text);
+ Assert.AreEqual("[$€-2]\\ #,##0", worksheet.Cells["A1"].Style.Numberformat.Format);
+ Assert.AreEqual("€ 12 346,78", worksheet.Cells["D1"].Text);
+ Assert.AreEqual("[$€-2]\\ #,##0.00", worksheet.Cells["D1"].Style.Numberformat.Format);
+
+ Assert.AreEqual("€ 12.347", worksheet.Cells["A2"].Text);
+ Assert.AreEqual("\"€\"\\ #,##0", worksheet.Cells["A2"].Style.Numberformat.Format);
+ Assert.AreEqual("€ 12.346,78", worksheet.Cells["D2"].Text);
+ Assert.AreEqual("\"€\"\\ #,##0.00", worksheet.Cells["D2"].Style.Numberformat.Format);
+ SwitchBackToCurrentCulture();
+ }
+ }
}
}
diff --git a/src/EPPlusTest/Issues/StylingIssues.cs b/src/EPPlusTest/Issues/StylingIssues.cs
index 612630f3db..1082c31630 100644
--- a/src/EPPlusTest/Issues/StylingIssues.cs
+++ b/src/EPPlusTest/Issues/StylingIssues.cs
@@ -8,6 +8,8 @@
using System.IO;
using System.Globalization;
using OfficeOpenXml.Style;
+using OfficeOpenXml.FormulaParsing;
+
namespace EPPlusTest
{
[TestClass]
@@ -413,6 +415,95 @@ public void i1839()
Assert.AreEqual(288, p.Workbook.Worksheets[0].Cells["E31"].StyleID);
SaveWorkbook("i1839-saved.xlsx", p);
}
+ [TestMethod]
+ public void s1007()
+ {
+ using var p = OpenPackage("s1007.xlsx");
+ var worksheet = p.Workbook.Worksheets.Add("Sheet1");
+ worksheet.Cells["A1"].Style.Numberformat.Format = "#.##0;-#.##0;X";
+ worksheet.Cells["A1"].Value = 0;
+ Assert.AreEqual("X", worksheet.Cells["A1"].Text);
+ //SaveAndCleanup(p);
+ }
+
+
+ [TestMethod]
+ public void s1005()
+ {
+ SwitchToCulture("de-DE");
+
+ //Set specific built-in formats
+ if (!ExcelPackageSettings.CultureSpecificBuildInNumberFormats.ContainsKey("de-DE"))
+ {
+ ExcelPackageSettings.CultureSpecificBuildInNumberFormats.Add("de-DE",
+ new Dictionary
+ {
+ {14,"dd.MM.yyyy"},
+ {15,"dd. MMM yy"},
+ {16,"dd. MMM"},
+ {17,"MMM yy"},
+ {18,"hh:mm AM/PM" },
+ {22,"dd.MM.yyyy hh:mm"},
+ {37,"#,##0;-#,##0"},
+ {38,"#,##0;[Rot]-#,##0"},
+ {39,"#,##0.00;-#,##0.00"},
+ {40,"#,##0.00;[Rot]-#,##0.00"},
+ {47,"mm:ss,f"}
+ });
+ }
+
+ using (var p = OpenTemplatePackage("s1005.xlsx"))
+ {
+ //AND use a Custom Number Format
+ //This caused issues in the Text.cs file when we ran Calculate
+ p.Workbook.NumberFormatToTextHandler = CustomNumberFormatToTextExample;
+
+ var ws1 = p.Workbook.Worksheets[1];
+
+ var origText = ws1.Cells["D8"].Text;
+
+ //Verify cell contents match when test was written
+ Assert.IsTrue(origText.Contains(".8000"));
+ Assert.IsTrue(origText.Contains("(26895559.000)"));
+ Assert.IsTrue(origText.Contains("69928453.64000"));
+
+ ws1.Cells["D8"].Calculate();
+ var cellRich = ws1.Cells["D8"].RichText.Text;
+
+ //Verify formatting has changed appropriately for calculated string
+ Assert.IsTrue(cellRich.Contains("0,80"));
+ Assert.IsTrue(cellRich.Contains("(26.895.559,00)"));
+ Assert.IsTrue(cellRich.Contains("69.928.453,64"));
+
+ SaveAndCleanup(p);
+ }
+
+ SwitchBackToCurrentCulture();
+ }
+
+ private static string CustomNumberFormatToTextExample(NumberFormatToTextArgs options)
+ {
+ var result = options.Text;
+
+ switch (options.Value)
+ {
+ case DateTime dt:
+ {
+ switch (options.NumberFormat.Format)
+ {
+ case "dd. mmm yy":
+ result = dt.ToString("dd. mmm yy"); //Return your own formatted text. Example
+ break;
+ default:
+ result = dt.ToString(options.NumberFormat.Format);
+ break;
+ }
+ break;
+ }
+ }
+
+ return result;
+ }
public class TestData
{
diff --git a/src/EPPlusTest/Issues/WorksheetIssues.cs b/src/EPPlusTest/Issues/WorksheetIssues.cs
index ae73c6771b..ccb91d9f36 100644
--- a/src/EPPlusTest/Issues/WorksheetIssues.cs
+++ b/src/EPPlusTest/Issues/WorksheetIssues.cs
@@ -4,6 +4,7 @@
using OfficeOpenXml.Drawing;
using OfficeOpenXml.Drawing.Chart;
using OfficeOpenXml.FormulaParsing;
+using OfficeOpenXml.FormulaParsing.Excel.Functions.Information;
using OfficeOpenXml.FormulaParsing.Excel.Functions.Logical;
using OfficeOpenXml.FormulaParsing.Excel.Functions.MathFunctions;
using OfficeOpenXml.RichData;
@@ -1121,5 +1122,22 @@ public void i2258()
var d3 = ws.Cells["D3"].Text;
Assert.AreEqual("Negative 4000,000", d3);
}
+
+ [TestMethod]
+ public void InsertAndShift_ShouldNotThrow_WhenArrayIsFull()
+ {
+ using var package = new ExcelPackage();
+ var sheet = package.Workbook.Worksheets.Add("Sheet1");
+
+ // Fill 7 columns with formatting to trigger the boundary condition
+ for (int col = 1; col <= 7; col++)
+ {
+ sheet.Column(col).Width = 15;
+ }
+
+ // These two inserts should not throw ArgumentException
+ sheet.InsertColumn(3, 1);
+ sheet.InsertColumn(5, 1);
+ }
}
}
diff --git a/src/EPPlusTest/VBA/VBATests.cs b/src/EPPlusTest/VBA/VBATests.cs
index 423cfcb2dd..659d711f41 100644
--- a/src/EPPlusTest/VBA/VBATests.cs
+++ b/src/EPPlusTest/VBA/VBATests.cs
@@ -1,5 +1,7 @@
using Microsoft.VisualStudio.TestTools.UnitTesting;
using OfficeOpenXml;
+using OfficeOpenXml.Constants;
+using OfficeOpenXml.Drawing;
using OfficeOpenXml.Utils.VBA;
using OfficeOpenXml.VBA;
using OfficeOpenXml.VBA.ContentHash;
@@ -12,6 +14,8 @@
using System.Security.Cryptography.X509Certificates;
using System.Text;
using System.Threading.Tasks;
+using System.Xml;
+using System.Xml.XPath;
namespace EPPlusTest.VBA
{
@@ -300,5 +304,91 @@ public void VbaModuleNameShouldAllowSpace()
var module = package.Workbook.VbaProject.Modules.AddModule("My BubbleChartModule");
module.Code = sb.ToString();
}
+
+
+ [TestMethod]
+ public void ReadAndSaveTemplateDxfIdsCheckbox()
+ {
+ using (var package = OpenTemplatePackage("i2273.xlsx"))
+ {
+ var ws = package.Workbook.Worksheets[0];
+ SaveAndCleanup(package);
+ }
+
+ using (var package = OpenPackage("i2273.xlsx"))
+ {
+ var ws = package.Workbook.Worksheets[0];
+
+ var tbl = ws.Tables[0];
+ var cols = tbl.Columns;
+
+ //Verify style bools
+ Assert.IsTrue(cols[0].DataStyle.Checkbox);
+ Assert.IsTrue(cols[1].DataStyle.Checkbox);
+
+ var topNode = cols[0].DataStyle._helper.TopNode;
+ cols[0].DataStyle._helper.GetDefaultNode("extLst/ext");
+
+ //ws.Workbook.Styles
+ var extNode = (XmlElement)cols[0].DataStyle._helper.GetDefaultNode($"extLst/ext");
+ var extNodeCol2 = (XmlElement)cols[0].DataStyle._helper.GetDefaultNode($"extLst/ext");
+
+ //Verify dxf property bag
+ Assert.IsTrue(extNode.GetAttribute("uri") == ExtLstUris.FeaturePropertyBagDxf);
+ Assert.IsTrue(extNodeCol2.GetAttribute("uri") == ExtLstUris.FeaturePropertyBagDxf);
+
+ SaveAndCleanup(package);
+ }
+ }
+
+ [TestMethod]
+ public void EnsureEpplusReadsXlsmDxfIdsForTablesCorrectly()
+ {
+ var fileName = "MyVBACheckboxes";
+ var fileEnding = ".xlsm";
+
+ using (var package = OpenPackage(fileName + fileEnding, true))
+ {
+ var ws = package.Workbook.Worksheets.Add("TableCheckboxes");
+ package.Workbook.CreateVBAProject();
+
+ var tbl = ws.Tables.Add(ws.Cells["B2:C3"], "Table1");
+ tbl.ShowHeader = true;
+ tbl.DataStyle.Checkbox = true;
+ tbl.Columns[0].DataStyle.Checkbox = true;
+ tbl.Columns[1].DataStyle.Checkbox = true;
+
+ ws.Cells["B3:C3"].Value = false;
+
+ SaveAndCleanup(package);
+ }
+
+ using (var package = OpenPackage(fileName + fileEnding))
+ {
+ var ws = package.Workbook.Worksheets[0];
+
+ var tbl = ws.Tables[0];
+ var cols = tbl.Columns;
+
+ //Verify style bools
+ Assert.IsTrue(tbl.DataStyle.Checkbox);
+ Assert.IsTrue(cols[0].DataStyle.Checkbox);
+ Assert.IsTrue(cols[1].DataStyle.Checkbox);
+
+ var topNode = cols[0].DataStyle._helper.TopNode;
+ cols[0].DataStyle._helper.GetDefaultNode("extLst/ext");
+
+ //ws.Workbook.Styles
+ var extNode = (XmlElement)cols[0].DataStyle._helper.GetDefaultNode($"extLst/ext");
+ var extNodeCol2 = (XmlElement)cols[0].DataStyle._helper.GetDefaultNode($"extLst/ext");
+
+ //Verify dxf property bag
+ Assert.IsTrue(extNode.GetAttribute("uri") == ExtLstUris.FeaturePropertyBagDxf);
+ Assert.IsTrue(extNodeCol2.GetAttribute("uri") == ExtLstUris.FeaturePropertyBagDxf);
+
+ var outFile = GetOutputFile("", fileName + "_Resaved" + fileEnding);
+ package.SaveAs(outFile);
+ }
+ }
}
}