diff --git a/appveyor8.yml b/appveyor8.yml index b264d4fa3c..561865cce5 100644 --- a/appveyor8.yml +++ b/appveyor8.yml @@ -1,4 +1,4 @@ -version: 8.4.2.{build} +version: 8.5.0.{build} branches: only: - develop8 @@ -10,15 +10,15 @@ install: & $env:temp\dotnet-install.ps1 -Architecture x64 -Version '10.0.100' -InstallDir "$env:ProgramFiles\dotnet" init: - ps: >- - Update-AppveyorBuild -Version "8.4.2.$env:appveyor_build_number-$(Get-Date -format yyyyMMdd)-$env:appveyor_repo_branch" + Update-AppveyorBuild -Version "8.5.0.$env:appveyor_build_number-$(Get-Date -format yyyyMMdd)-$env:appveyor_repo_branch" - Write-Host "8.4.2.$env:appveyor_build_number-$(Get-Date -format yyyyMMdd)-$env:appveyor_repo_branch" + Write-Host "8.5.0.$env:appveyor_build_number-$(Get-Date -format yyyyMMdd)-$env:appveyor_repo_branch" dotnet_csproj: patch: true file: '**\*.csproj' version: '{version}' - assembly_version: 8.4.2.{build} - file_version: 8.4.2.{build} + assembly_version: 8.5.0.{build} + file_version: 8.5.0.{build} nuget: project_feed: true before_build: diff --git a/docs/api/index.md b/docs/api/index.md new file mode 100644 index 0000000000..4cb7408d04 --- /dev/null +++ b/docs/api/index.md @@ -0,0 +1,5 @@ +# EPPlus API Documentation + +This section contains the full API reference for EPPlus, generated from the XML comments in the source code. + +Browse the namespaces in the sidebar to explore the available classes and members. \ No newline at end of file diff --git a/docs/articles/breakingchanges.md b/docs/articles/breakingchanges.md index 89a45fcf1c..78c7fcfdfa 100644 --- a/docs/articles/breakingchanges.md +++ b/docs/articles/breakingchanges.md @@ -215,4 +215,10 @@ Renaming worksheet's will now change the formula correctly to include single quo `ExcelColor.Tint` from `decimal` to `double`. ### 8.0.2 -* Removed base class from ExcelVmlDrawingPosition and with that the Load, UpdateXml methods and the RowOff and ColOff properties as they were duplicates. \ No newline at end of file +* Removed base class from ExcelVmlDrawingPosition and with that the Load, UpdateXml methods and the RowOff and ColOff properties as they were duplicates. +### 8.5.0 +* Setting dataLabelPosition.Top on BarCharts corrupted the excel file when saved. Trying to set this now throws an error instead. +* NumberFormatToTextArgs.NumberFormat now returns the interface IExcelNumberFormat rather than the ExcelNumberFormatXml class. All public variables remain the same and it can be safely cast to ´ExcelNumberFormatXml´ as long as ´Package.Workbook.NumberFormatToTextHandler´ is null. + +### 9.0.0 +The misspelled enum eCompundLineStyle has been renamed eCompoundLineStyle. \ No newline at end of file diff --git a/docs/articles/fixedissues.md b/docs/articles/fixedissues.md index 40ab5376a2..568447f268 100644 --- a/docs/articles/fixedissues.md +++ b/docs/articles/fixedissues.md @@ -1,4 +1,24 @@ # Features / Fixed issues - EPPlus 8 +## Version 8.5.0 +### Minor Features +* Added ´CancellationToken´ to Calculate - see [Cancelling a calculation](https://github.com/EPPlusSoftware/EPPlus/wiki/Cancelling-a-calculation). +* Added property ´ValueFromCellsRange´ and the ´SetValueFromCellsRange´ method to data labels on chart series. +* Improved performance for SUMIFS, AVERAGEIFS, COUNTIFS. +* Improved performance for formula calculation with full column references (e.g. A:A, $B:$B). +### Fixed issues +* Deleting pictures in cells, caused a corrupt workbook in rare cases. +* Fixed several formatting issues when reading and writing tables with checkboxes in them. +* Fixed several issues when using a custom ExcelPackage.Workbook.NumberFormatToTextHandler: +* Fixed unary minus coercion on dynamic array results. +* Fixed off-by-one in ´InsertAndShift´ resize condition. This caused an ´ArgumentException´ when calling ´InsertColumn´ on xlsx files where the number of defined columns was at the array boundary. +* Fixed culture-specific DateTime in PowerQuery metadata. +* Calculated field computation in pivot tables did not work correctly in some cases. +* Copying a Connection shape from one workbook to another caused an unhandled exception. +* Having the wrong content type/extension on an image in the workbook package, caused an unhandled exception on load. +Breaking Change: +* Setting dataLabelPosition.Top on BarCharts corrupted the excel file when saved. Trying to set this now throws an error instead. +* NumberFormatToTextArgs.NumberFormat now returns the interface IExcelNumberFormat rather than the ExcelNumberFormatXml class. All public variables remain the same and it can be safely cast to ´ExcelNumberFormatXml´ as long as ´Package.Workbook.NumberFormatToTextHandler´ is null. + ## Version 8.4.2 * Fixed an issue where the ExcelRange.Text property could format negative values with double minus signs in rare cases. * Fixed a performance issue when using FollowDependencyChain = false in the Calculate method. With this option, EPPlus no longer calculates dependent cells or dynamic array formula spills. diff --git a/docs/docfx.json b/docs/docfx.json index 46ca254817..db1ed279ce 100644 --- a/docs/docfx.json +++ b/docs/docfx.json @@ -64,8 +64,10 @@ "default", "templates/epplus" ], - "globalMetadata":{ - "_disableContribution":true + "globalMetadata": { + "_disableContribution": true, + "_appLogoPath": "images/logo.png", + "_appFaviconPath": "images/favicon.ico" }, "postProcessors": [], "markdownEngineName": "markdig", diff --git a/docs/templates/epplus/favicon.ico b/docs/images/favicon.ico similarity index 100% rename from docs/templates/epplus/favicon.ico rename to docs/images/favicon.ico diff --git a/docs/templates/epplus/logo.png b/docs/images/logo.png similarity index 100% rename from docs/templates/epplus/logo.png rename to docs/images/logo.png diff --git a/docs/index.md b/docs/index.md index 8f4bceaa2c..8c026daf7e 100644 --- a/docs/index.md +++ b/docs/index.md @@ -4,4 +4,4 @@ EPPlus is a .NET Framework/.NET Core library for managing Office Open XML spread The API documentation provided is generated from the XML comments in the EPPlus source code. It is updated when we releases new versions of the library. ### Breaking changes -See this list of [breaking changes](articles/breakingchanges.html) in version 5. +See this list of [breaking changes](articles/breakingchanges.md) in version 5. diff --git a/docs/templates/epplus/layout/_master.tmpl b/docs/templates/epplus/layout/_master.tmpl deleted file mode 100644 index 92346f7b36..0000000000 --- a/docs/templates/epplus/layout/_master.tmpl +++ /dev/null @@ -1,55 +0,0 @@ -{{!Copyright (c) Microsoft. All rights reserved. Licensed under the MIT license. See LICENSE file in the project root for full license information.}} -{{!include(/^styles/.*/)}} -{{!include(/^fonts/.*/)}} -{{!include(favicon.ico)}} -{{!include(logo.png)}} -{{!include(search-stopwords.json)}} - - - - {{>partials/head}} - -
-
- {{^_disableNavbar}} - {{>partials/navbar}} - {{/_disableNavbar}} - {{^_disableBreadcrumb}} - {{>partials/breadcrumb}} - {{/_disableBreadcrumb}} -
- {{#_enableSearch}} -
- {{>partials/searchResults}} -
- {{/_enableSearch}} -
- {{^_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 -%} - {{_appName}} - {%- else -%} - {{_appName}} - {%- 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.}} - - - {{_appName}} - 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); + } + } } }