diff --git a/.github/workflows/Build-Release.yml b/.github/workflows/Build-Release.yml
index 233a1bac58..ce662d09df 100644
--- a/.github/workflows/Build-Release.yml
+++ b/.github/workflows/Build-Release.yml
@@ -34,6 +34,26 @@ jobs:
- name: Restore dependencies
run: dotnet restore ./src/EPPlus.sln
+
+ # --- SBOM ---
+ - name: Install CycloneDX
+ run: dotnet tool install --global CycloneDX
+ - name: Read version from csproj
+ id: read_version
+ run: |
+ $version = ([xml](Get-Content ./src/EPPlus/EPPlus.csproj)).Project.PropertyGroup.Version | Where-Object { $_ } | Select-Object -First 1
+ echo "VERSION=$version" >> $env:GITHUB_ENV
+ shell: pwsh
+ - name: Generate SBOM
+ run: dotnet CycloneDX ./src/EPPlus/EPPlus.csproj -o ./sbom -F Json -st Library -sv ${{ env.VERSION }} -fn epplus-${{ env.VERSION }}.sbom.json -imp ./src/EPPlus/sbom-metadata-template.xml
+ - name: Generate SHA-256 checksum for SBOM
+ run: |
+ $sbomFile = "./sbom/epplus-${{ env.VERSION }}.sbom.json"
+ $hash = (Get-FileHash -Path $sbomFile -Algorithm SHA256).Hash.ToLower()
+ "$hash epplus-${{ env.VERSION }}.sbom.json" | Out-File -FilePath "./sbom/epplus-${{ env.VERSION }}.sbom.json.sha256" -Encoding utf8NoBOM
+ shell: pwsh
+ # --- SBOM ---
+
- name: Build
run: dotnet build ./src/EPPlus.sln --no-restore --configuration Release
- name: Test
diff --git a/docs/articles/breakingchanges.md b/docs/articles/breakingchanges.md
index d6abb22c67..330ff3421f 100644
--- a/docs/articles/breakingchanges.md
+++ b/docs/articles/breakingchanges.md
@@ -216,8 +216,19 @@ Renaming worksheet's will now change the formula correctly to include single quo
### 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.
+### 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.
+
### 8.5.5
* `ws.Cells["A1"].RichText` no longer sets cells with `null` to `string.empty`
* .RichText no longer sets the cell or contents to be RichText automatically.
- This is instead done when properties such as; `.Text`, `.Add` or `.Insert` are set on the .RichText property.
\ No newline at end of file
+ This is instead done when properties such as; `.Text`, `.Add` or `.Insert` are set on the .RichText property.
+The misspelled enum eCompundLineStyle has been renamed eCompoundLineStyle.
+
+
+### 9.0.0
+The misspelled enum eCompundLineStyle has been renamed eCompoundLineStyle.
+The misspelled property on drawingFill `Transparancy` has been renamed to `Transparency`
+The `Richtext.Baseline` property now always return that value in whole percent.
\ No newline at end of file
diff --git a/docs/articles/fixedissues.md b/docs/articles/fixedissues.md
index 72b47d5c0b..f763512770 100644
--- a/docs/articles/fixedissues.md
+++ b/docs/articles/fixedissues.md
@@ -1,4 +1,6 @@
# Features / Fixed issues - EPPlus 8
+## Version 9.0.0
+* Added 'Layout' property to 'ExcelChartTrendlineLabel' class.
## Version 8.6 1
### Features
diff --git a/src/.runsettings b/src/.runsettings
index 74acb790f1..2c84e809bf 100644
--- a/src/.runsettings
+++ b/src/.runsettings
@@ -16,5 +16,6 @@
False
False
False
+ true
\ No newline at end of file
diff --git a/src/Directory.Packages.props b/src/Directory.Packages.props
index 564eca7234..bacdaf92bd 100644
--- a/src/Directory.Packages.props
+++ b/src/Directory.Packages.props
@@ -3,9 +3,10 @@
true
+
-
+
@@ -17,9 +18,9 @@
-
-
-
+
+
+
\ No newline at end of file
diff --git a/src/EPPlus.DrawingRenderer.Tests/Chart/BarChartTests.cs b/src/EPPlus.DrawingRenderer.Tests/Chart/BarChartTests.cs
new file mode 100644
index 0000000000..c786c38b92
--- /dev/null
+++ b/src/EPPlus.DrawingRenderer.Tests/Chart/BarChartTests.cs
@@ -0,0 +1,83 @@
+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 ix = 1;
+ //var c = ws.Drawings[ix];
+ //var svg = c.ToSvg();
+ //SaveTextFileToWorkbook($"svg\\ChartForSvg_ind{ix++}.svg", svg);
+
+ var ix = 0;
+ foreach (ExcelChart c in ws.Drawings)
+ {
+ var svg = c.ToSvg(x => { x.Size.Width = 100; x.Size.Height = 100; });
+ 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 ix = 0;
+ foreach (ExcelChart c in ws.Drawings)
+ {
+ var svg = c.ToSvg();
+ SaveTextFileToWorkbook($"svg\\BarChartForSvg_sheet2_{ix++}.svg", svg);
+ }
+ }
+ }
+
+ [TestMethod]
+ public void DatalabelBarCharts()
+ {
+ ExcelPackage.License.SetNonCommercialOrganization("EPPlus Project");
+ using (var p = OpenTemplatePackage("BarChartForSvgDatalabelsBasic.xlsx"))
+ {
+ var ws = p.Workbook.Worksheets[0];
+ var drawings = ws.Drawings;
+ var ix = 1;
+
+ for (int i = ix; i < drawings.Count; i++)
+ {
+ var svg = drawings[i].ToSvg();
+ SaveTextFileToWorkbook($"svg\\BarChartDataLabels{ix++}.svg", svg);
+ }
+ }
+ }
+
+
+ [TestMethod]
+ public void NegativeDatalabelBarCharts()
+ {
+ ExcelPackage.License.SetNonCommercialOrganization("EPPlus Project");
+ using (var p = OpenTemplatePackage("negativeDatalabels.xlsx"))
+ {
+ var ws = p.Workbook.Worksheets[0];
+ var drawings = ws.Drawings;
+ var ix = 0;
+
+ for (int i = ix; i < drawings.Count; i++)
+ {
+ var svg = drawings[i].ToSvg();
+ SaveTextFileToWorkbook($"svg\\NegativeLabels{ix++}.svg", svg);
+ }
+ }
+ }
+ }
+}
diff --git a/src/EPPlus.DrawingRenderer.Tests/Chart/ColumnChartTests.cs b/src/EPPlus.DrawingRenderer.Tests/Chart/ColumnChartTests.cs
new file mode 100644
index 0000000000..6d6002687e
--- /dev/null
+++ b/src/EPPlus.DrawingRenderer.Tests/Chart/ColumnChartTests.cs
@@ -0,0 +1,57 @@
+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 ix = 1;
+ //var c = ws.Drawings[ix];
+ //var svg = c.ToSvg();
+ //SaveTextFileToWorkbook($"svg\\ChartForSvg_ind{ix++}.svg", svg);
+
+ var ix = 0;
+ foreach (ExcelChart c in ws.Drawings)
+ {
+ var svg = c.ToSvg();
+ 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 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 = c.ToSvg();
+ SaveTextFileToWorkbook($"svg\\ColumnChartForSvg_sheet2_{ix++}.svg", svg);
+ }
+ }
+ }
+ }
+}
diff --git a/src/EPPlus.DrawingRenderer.Tests/Chart/ComboChartTests.cs b/src/EPPlus.DrawingRenderer.Tests/Chart/ComboChartTests.cs
new file mode 100644
index 0000000000..37b69fe8f7
--- /dev/null
+++ b/src/EPPlus.DrawingRenderer.Tests/Chart/ComboChartTests.cs
@@ -0,0 +1,31 @@
+using OfficeOpenXml;
+using OfficeOpenXml.Drawing.Chart;
+
+namespace EPPlus.Export.ImageRenderer.Tests.Chart
+{
+ [TestClass]
+ public class ComboChartToSvgTests : TestBase
+ {
+ [TestMethod]
+ public void GenerateSvgForComboCharts_sheet1()
+ {
+ ExcelPackage.License.SetNonCommercialOrganization("EPPlus Project");
+ using (var p = OpenTemplatePackage("ComboChart.xlsx"))
+ {
+ var ws = p.Workbook.Worksheets[0];
+
+ //var ix = 1;
+ //var c = ws.Drawings[ix];
+ //var svg = renderer.RenderDrawingToSvg(c);
+ //SaveTextFileToWorkbook($"svg\\ComboChart_Sheet1_{ix++}.svg", svg);
+
+ var ix = 0;
+ foreach (ExcelChart c in ws.Drawings)
+ {
+ var svg = c.ToSvg();
+ SaveTextFileToWorkbook($"svg\\ComboChart_sheet1_{ix++}.svg", svg);
+ }
+ }
+ }
+ }
+}
diff --git a/src/EPPlus.DrawingRenderer.Tests/Chart/ErrorbarsTests.cs b/src/EPPlus.DrawingRenderer.Tests/Chart/ErrorbarsTests.cs
new file mode 100644
index 0000000000..3b2ea9db41
--- /dev/null
+++ b/src/EPPlus.DrawingRenderer.Tests/Chart/ErrorbarsTests.cs
@@ -0,0 +1,70 @@
+using OfficeOpenXml;
+using OfficeOpenXml.Drawing.Chart;
+
+namespace EPPlus.Export.ImageRenderer.Tests.Chart
+{
+ [TestClass]
+ public class ErrorbarsTests : TestBase
+ {
+ [TestMethod]
+ public void GenerateSvgForErrorbars_Line_Sheet1()
+ {
+ ExcelPackage.License.SetNonCommercialOrganization("EPPlus Project");
+ using (var p = OpenTemplatePackage("Errorbars.xlsx"))
+ {
+ var ws = p.Workbook.Worksheets[0];
+ //var ix = 4;
+ //var c = ws.Drawings[ix];
+ //var svg = c.ToSvg();
+ //SaveTextFileToWorkbook($"svg\\Error_sheet1_ind{ix++}.svg", svg);
+
+ var ix = 0;
+ foreach (ExcelChart c in ws.Drawings)
+ {
+ var svg = c.ToSvg();
+ SaveTextFileToWorkbook($"svg\\Errorbar_sheet1_{ix++}.svg", svg);
+ }
+ }
+ }
+ [TestMethod]
+ public void GenerateSvgForErrorbars_Column_Sheet2()
+ {
+ ExcelPackage.License.SetNonCommercialOrganization("EPPlus Project");
+ using (var p = OpenTemplatePackage("Errorbars.xlsx"))
+ {
+ var ws = p.Workbook.Worksheets[1];
+ //var ix = 4;
+ //var c = ws.Drawings[ix];
+ //var svg = c.ToSvg();
+ //SaveTextFileToWorkbook($"svg\\Error_sheet1_ind{ix++}.svg", svg);
+
+ var ix = 0;
+ foreach (ExcelChart c in ws.Drawings)
+ {
+ var svg = c.ToSvg();
+ SaveTextFileToWorkbook($"svg\\Errorbar_sheet2_{ix++}.svg", svg);
+ }
+ }
+ }
+ [TestMethod]
+ public void GenerateSvgForErrorbars_Bar_Sheet3()
+ {
+ ExcelPackage.License.SetNonCommercialOrganization("EPPlus Project");
+ using (var p = OpenTemplatePackage("Errorbars.xlsx"))
+ {
+ var ws = p.Workbook.Worksheets[2];
+ //var ix = 4;
+ //var c = ws.Drawings[ix];
+ //var svg = c.ToSvg();
+ //SaveTextFileToWorkbook($"svg\\Error_sheet1_ind{ix++}.svg", svg);
+
+ var ix = 0;
+ foreach (ExcelChart c in ws.Drawings)
+ {
+ var svg = c.ToSvg();
+ SaveTextFileToWorkbook($"svg\\Errorbar_sheet3_{ix++}.svg", svg);
+ }
+ }
+ }
+ }
+}
diff --git a/src/EPPlus.DrawingRenderer.Tests/Chart/LineChartToSvgTests.cs b/src/EPPlus.DrawingRenderer.Tests/Chart/LineChartToSvgTests.cs
new file mode 100644
index 0000000000..5ea7ab57f6
--- /dev/null
+++ b/src/EPPlus.DrawingRenderer.Tests/Chart/LineChartToSvgTests.cs
@@ -0,0 +1,307 @@
+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 ix = 6;
+ var c = ws.Drawings[ix];
+ var svg = c.ToSvg();
+ SaveTextFileToWorkbook($"svg\\ChartForSvg_ind{ix++}.svg", svg);
+
+ //for (int i = 0; i < ws.Drawings.Count; i++)
+ //{
+ // var c = ws.Drawings[i];
+ // var svg = c.ToSvg();
+ // SaveTextFileToWorkbook($"svg\\ChartForSvg{i}.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 ix = 1;
+ //var c = ws.Drawings[ix];
+ //var svg = c.ToSvg();
+ //SaveTextFileToWorkbook($"svg\\ChartForSvg_sheet2_{ix++}.svg", svg);
+ var ix = 1;
+ foreach (ExcelChart c in ws.Drawings)
+ {
+ var svg = c.ToSvg();
+ SaveTextFileToWorkbook($"svg\\ChartForSvg_sheet2_{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 ix = 0;
+ //var c = ws.Drawings[ix];
+ //var svg = c.ToSvg();
+ //SaveTextFileToWorkbook($"svg\\chartforsvg_sheet3_{ix++}.svg", svg);
+ var ix = 1;
+ foreach (ExcelChart c in ws.Drawings)
+ {
+ var svg = c.ToSvg();
+ SaveTextFileToWorkbook($"svg\\ChartForSvg_Sheet3{ix++}.svg", svg);
+ }
+ }
+ }
+ [TestMethod]
+ public void GenerateSvgForComboCharts_sheet4()
+ {
+ ExcelPackage.License.SetNonCommercialOrganization("EPPlus Project");
+ using (var p = OpenTemplatePackage("ChartForSvg.xlsx"))
+ {
+ var ws = p.Workbook.Worksheets[3];
+ var ix = 1;
+ var c = ws.Drawings[ix];
+ var svg = c.ToSvg();
+ SaveTextFileToWorkbook($"svg\\ChartForSvg_sheet2_{ix++}.svg", svg);
+ //var ix = 1;
+ //foreach (ExcelChart c in ws.Drawings)
+ //{
+ // var svg = c.ToSvg();
+ // SaveTextFileToWorkbook($"svg\\ChartForSvg_Combo_Sheet4{ix++}.svg", svg);
+ //}
+ }
+ }
+ [TestMethod]
+ public void GenerateSvgForTrendlineCharts_sheet5()
+ {
+ ExcelPackage.License.SetNonCommercialOrganization("EPPlus Project");
+ using (var p = OpenTemplatePackage("ChartForSvg.xlsx"))
+ {
+ var ws = p.Workbook.Worksheets[4];
+ //var ix = 4;
+ //var c = ws.Drawings[ix];
+ //var svg = renderer.RenderDrawingToSvg(c);
+ //SaveTextFileToWorkbook($"svg\\Trendline_sheet5_{ix++}.svg", svg);
+ var ix = 1;
+ foreach (ExcelChart c in ws.Drawings)
+ {
+ var svg = c.ToSvg();
+ SaveTextFileToWorkbook($"svg\\Trendline_Sheet5{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 ix = 1;
+ //var c = ws.Drawings[ix];
+ //var svg = c.ToSvg();
+ //SaveTextFileToWorkbook($"svg\\LineChartForSvg_Single{ix++}.svg", svg);
+ var ix = 1;
+ foreach (ExcelChart c in ws.Drawings)
+ {
+ var svg = c.ToSvg();
+ SaveTextFileToWorkbook($"svg\\LineChartForSvg{ix++}.svg", svg);
+ }
+ }
+ }
+
+ [TestMethod]
+ public void GenerateSuperScript()
+ {
+ ExcelPackage.License.SetNonCommercialOrganization("EPPlus Project");
+ using (var p = OpenTemplatePackage("ChartForSvg_SecondaryAxis.xlsx"))
+ {
+ var ws = p.Workbook.Worksheets[0];
+ //var ix = 1;
+ //var c = ws.Drawings[ix];
+ //var svg = c.ToSvg();
+ //SaveTextFileToWorkbook($"svg\\ChartForSvg_sheet2_{ix++}.svg", svg);
+ var ix = 1;
+ foreach (ExcelChart c in ws.Drawings)
+ {
+ var svg = c.ToSvg();
+ 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 ix = 2;
+ //var c = ws.Drawings[ix];
+ //var svg = renderer.RenderDrawingToSvg(c);
+ //SaveTextFileToWorkbook($"svg\\ChartForSvg_Sheet2_SecAxis{ix++}.svg", svg);
+ var ix = 1;
+ foreach (ExcelChart c in ws.Drawings)
+ {
+ var svg = c.ToSvg();
+ SaveTextFileToWorkbook($"svg\\ChartForSvg_Sheet2_SecAxis{ix++}.svg", svg);
+ }
+ }
+ }
+ [TestMethod]
+ public void GenerateSvgForCharts_SecondaryAxis_sheet3()
+ {
+ ExcelPackage.License.SetNonCommercialOrganization("EPPlus Project");
+ using (var p = OpenTemplatePackage("ChartForSvg_SecondaryAxis.xlsx"))
+ {
+ var ws = p.Workbook.Worksheets[2];
+ //var ix = 1;
+ //var c = ws.Drawings[ix];
+ //var svg = c.ToSvg();
+ //SaveTextFileToWorkbook($"svg\\ChartForSvg_sheet3_{ix++}.svg", svg);
+ var ix = 1;
+ foreach (ExcelChart c in ws.Drawings)
+ {
+ var svg = c.ToSvg();
+ SaveTextFileToWorkbook($"svg\\ChartForSvg_Sheet3_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 svg = c.ToSvg();
+ 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 svg = c.ToSvg();
+ 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 svg = c.ToSvg();
+ 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 svg = c.ToSvg();
+ 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 svg = c.ToSvg();
+ 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 svg = c.ToSvg();
+ 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 svg = c.ToSvg();
+ SaveTextFileToWorkbook($"svg\\defChartLine3Points.svg", svg);
+ }
+ }
+ [TestMethod]
+ public void GenerateSvgForLineCharts_AxisAlign_sheet1()
+ {
+ ExcelPackage.License.SetNonCommercialOrganization("EPPlus Project");
+ using (var p = OpenTemplatePackage("HorizontalAxisAlign.xlsx"))
+ {
+ var ws = p.Workbook.Worksheets[0];
+
+ //var ix = 3;
+ //var c = ws.Drawings[ix];
+ //var svg = c.ToSvg();
+ //SaveTextFileToWorkbook($"svg\\HorizontalAxisChartForSvg{ix++}.svg", svg);
+
+ for (int i = 0; i < ws.Drawings.Count; i++)
+ {
+ var c = ws.Drawings[i];
+ var svg = c.ToSvg();
+ SaveTextFileToWorkbook($"svg\\HorizontalAxisChartForSvg{i}.svg", svg);
+ }
+ }
+ }
+
+ }
+}
diff --git a/src/EPPlus.DrawingRenderer.Tests/Chart/PieChartTests.cs b/src/EPPlus.DrawingRenderer.Tests/Chart/PieChartTests.cs
new file mode 100644
index 0000000000..693294b5c4
--- /dev/null
+++ b/src/EPPlus.DrawingRenderer.Tests/Chart/PieChartTests.cs
@@ -0,0 +1,34 @@
+using OfficeOpenXml;
+using OfficeOpenXml.Drawing.Chart;
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+
+namespace EPPlus.Export.ImageRenderer.Tests.Chart
+{
+ [TestClass]
+ public class PieChartTests : TestBase
+ {
+
+ [TestMethod]
+ public void ReadAndCreateSvgsAll()
+ {
+ ExcelPackage.License.SetNonCommercialOrganization("EPPlus Project");
+ using (var p = OpenTemplatePackage("PieChartSvgALL.xlsx"))
+ {
+ var ws = p.Workbook.Worksheets[0];
+
+ for (int i = 0; i < p.Workbook.Worksheets.Count; i++)
+ {
+ ws = p.Workbook.Worksheets[i];
+ foreach (ExcelChart c in ws.Drawings)
+ {
+ var svg = c.ToSvg();
+ SaveTextFileToWorkbook($"svg\\PieChartSvgALL\\s{i}_{ws.Name}_{c.Name}.svg", svg);
+ }
+ }
+ }
+ }
+ }
+}
diff --git a/src/EPPlus.DrawingRenderer.Tests/Chart/TrendlineTests.cs b/src/EPPlus.DrawingRenderer.Tests/Chart/TrendlineTests.cs
new file mode 100644
index 0000000000..219689525b
--- /dev/null
+++ b/src/EPPlus.DrawingRenderer.Tests/Chart/TrendlineTests.cs
@@ -0,0 +1,73 @@
+using OfficeOpenXml;
+using OfficeOpenXml.Drawing.Chart;
+
+namespace EPPlus.Export.ImageRenderer.Tests.Chart
+{
+ [TestClass]
+ public class TrendlineTests : TestBase
+ {
+ [TestMethod]
+ public void GenerateSvgForTrendlines_Sheet1()
+ {
+ ExcelPackage.License.SetNonCommercialOrganization("EPPlus Project");
+ using (var p = OpenTemplatePackage("Trendlines.xlsx"))
+ {
+ var ws = p.Workbook.Worksheets[0];
+ //var ix = 4;
+ //var c = ws.Drawings[ix];
+ //var svg = c.ToSvg();
+ //SaveTextFileToWorkbook($"svg\\Trendline_sheet1_ind{ix++}.svg", svg);
+
+ var ix = 0;
+ foreach (ExcelChart c in ws.Drawings)
+ {
+ var svg = c.ToSvg();
+ SaveTextFileToWorkbook($"svg\\Trendline_sheet1_{ix++}.svg", svg);
+ }
+ }
+ }
+
+ [TestMethod]
+ public void TrendlineAlt()
+ {
+ ExcelPackage.License.SetNonCommercialOrganization("EPPlus Project");
+ using (var p = OpenTemplatePackage("TrendlineAlt.xlsx"))
+ {
+ var ws = p.Workbook.Worksheets[0];
+ //var ix = 4;
+ //var c = ws.Drawings[ix];
+ //var svg = c.ToSvg();
+ //SaveTextFileToWorkbook($"svg\\Trendline_sheet1_ind{ix++}.svg", svg);
+
+ var ix = 0;
+ foreach (ExcelChart c in ws.Drawings)
+ {
+ var svg = c.ToSvg();
+ SaveTextFileToWorkbook($"svg\\TrendlineAlt_sheet1_{ix++}.svg", svg);
+ }
+ }
+ }
+
+ [TestMethod]
+ public void GenerateSvgForTrendlines_Sheet2()
+ {
+ ExcelPackage.License.SetNonCommercialOrganization("EPPlus Project");
+ using (var p = OpenTemplatePackage("Trendlines.xlsx"))
+ {
+ var ws = p.Workbook.Worksheets[1];
+
+ //var ix = 3;
+ //var c = ws.Drawings[ix];
+ //var svg = c.ToSvg();
+ //SaveTextFileToWorkbook($"svg\\Trendline_sheet1_ind{ix++}.svg", svg);
+
+ var ix = 0;
+ foreach (ExcelChart c in ws.Drawings)
+ {
+ var svg = c.ToSvg();
+ SaveTextFileToWorkbook($"svg\\Trendline_sheet2_{ix++}.svg", svg);
+ }
+ }
+ }
+ }
+}
diff --git a/src/EPPlus.DrawingRenderer.Tests/DrawingShapeRenderer/SvgStandAloneTests.cs b/src/EPPlus.DrawingRenderer.Tests/DrawingShapeRenderer/SvgStandAloneTests.cs
new file mode 100644
index 0000000000..e2ce78fce8
--- /dev/null
+++ b/src/EPPlus.DrawingRenderer.Tests/DrawingShapeRenderer/SvgStandAloneTests.cs
@@ -0,0 +1,389 @@
+using EPPlus.DrawingRenderer;
+using EPPlus.DrawingRenderer.RenderItems;
+using EPPlus.DrawingRenderer.RenderItems.SvgItem;
+using EPPlus.DrawingRenderer.Svg;
+using EPPlus.Export.ImageRenderer.RenderItems.SvgItem;
+using EPPlus.Fonts.OpenType;
+using EPPlus.Fonts.OpenType.Integration.DataHolders;
+using EPPlus.Graphics;
+using System.Drawing;
+using System.Text;
+
+namespace EPPlus.Export.ImageRenderer.Tests.DrawingShapeRenderer
+{
+ [TestClass]
+ public class SvgStandAloneTests : TestBase
+ {
+
+ private GroupRenderItem GenerateShapeRenderer()
+ {
+ BoundingBox bounds = new BoundingBox(0, 0, 500, 500);
+ StringBuilder sb = new StringBuilder();
+ var svgShapeRenderer = new SvgShapeRenderer(bounds, sb, new SvgRenderOptions());
+
+
+ var baseGroup = new GroupRenderItem(bounds);
+
+ var background = new RectRenderItem(baseGroup.Bounds);
+
+ background.Width = bounds.Width;
+ background.Height = bounds.Height;
+ background.FillColor = "aliceBlue";
+
+ baseGroup.AddChildItem(background);
+ return baseGroup;
+ }
+
+ private GroupRenderItem GenerateGroupRenderItem()
+ {
+ BoundingBox bounds = new BoundingBox(0, 0, 500, 500);
+
+ var baseGroup = new GroupRenderItem(bounds);
+
+ var background = new RectRenderItem(baseGroup.Bounds);
+
+ background.Width = bounds.Width;
+ background.Height = bounds.Height;
+ background.FillColor = "aliceBlue";
+
+ baseGroup.AddChildItem(background);
+ return baseGroup;
+ }
+
+ private void GenerateSvgFile(string fileName, BoundingBox bounds, params RenderItem[] items)
+ {
+
+ StringBuilder sb = new StringBuilder();
+ var svgShapeRenderer = new SvgShapeRenderer(bounds, sb, new SvgRenderOptions());
+
+ List renderItems = items.ToList();
+ svgShapeRenderer.Render(renderItems);
+
+ var svg = sb.ToString();
+
+ SaveTextFileToWorkbook($"svg\\{fileName}.svg", svg);
+ }
+
+ [TestMethod]
+ public void SvgRectTest()
+ {
+ var baseGroup = GenerateShapeRenderer();
+ GenerateSvgFile("rectStandAlone", baseGroup.Bounds, baseGroup);
+ }
+
+ [TestMethod]
+ public void SvgTextRun()
+ {
+ var baseGroup = GenerateShapeRenderer();
+
+ var rt = new RichTextFormatSimple();
+ rt.Text = "My text";
+ rt.UnderlineType = 1;
+ rt.FontColor = System.Drawing.Color.Black;
+ rt.Family = "Archivo Narrow";
+ rt.SubFamily = OfficeOpenXml.Interfaces.Fonts.FontSubFamily.Regular;
+ rt.Size = 12f;
+
+ var textRun = new SvgTextRunRenderItem(baseGroup.Bounds, rt, rt.Text, true);
+
+ //Add size of text since svg renders text upwards from the start point.
+ textRun.YPosition = rt.Size;
+
+ baseGroup.AddChildItem(textRun);
+
+ GenerateSvgFile("textRunStandAlone", baseGroup.Bounds, baseGroup);
+ }
+
+ private void GenerateTextBodyFile(string fileName, GroupRenderItem baseGroup, SvgTextBodyRenderItem textBody)
+ {
+ StringBuilder sb = new StringBuilder();
+ var svgShapeRenderer = new SvgShapeRenderer(baseGroup.Bounds, sb, new SvgRenderOptions());
+
+ var background = new RectRenderItem(baseGroup.Bounds);
+
+ background.Width = baseGroup.Bounds.Width;
+ background.Height = baseGroup.Bounds.Height;
+ background.FillColor = "aliceBlue";
+
+ baseGroup.AddChildItem(textBody);
+ baseGroup.AddChildItem(background);
+
+ List items = new List() { baseGroup };
+
+ svgShapeRenderer.Render(items);
+
+ var svg = sb.ToString();
+
+
+ SaveTextFileToWorkbook($"svg\\{fileName}.svg", svg);
+ }
+
+
+ private SvgTextBodyRenderItem GenerateTextBody(GroupRenderItem baseGroup)
+ {
+ var engine = new OpenTypeFontEngine(x => x.SearchSystemDirectories = true);
+ var renderContext = new RenderContext(() => engine);
+ var textBody = new SvgTextBodyRenderItem(renderContext, baseGroup.Bounds, true);
+ var paragraph = textBody.AddParagraph("Hello");
+
+ paragraph.AddText(" There");
+
+ var rtItem = new RichTextFormatSimple("Second paragraph", "Archivo Narrow", 16f, true);
+ rtItem.FontColor = Color.DarkGreen;
+ var para2 = textBody.AddParagraph(rtItem);
+
+ textBody.AddChildItem(paragraph);
+ textBody.AddChildItem(para2);
+
+ baseGroup.AddChildItem(textBody);
+
+ return textBody;
+ }
+
+ [TestMethod]
+ public void SvgTextBodyTest()
+ {
+ var baseGroup = GenerateGroupRenderItem();
+ var textBody = GenerateTextBody(baseGroup);
+ GenerateSvgFile("standAloneTextBody", baseGroup.Bounds, baseGroup);
+ }
+
+ [TestMethod]
+ public void SvgTextBodyTestCenterAlignmentGenerated()
+ {
+ var baseGroup = GenerateGroupRenderItem();
+
+ var textBody = GenerateTextBody(baseGroup);
+ textBody.Paragraphs[0].AddText(" a\r\n new day beckons");
+ textBody.Paragraphs[0].HorizontalAlignment = RenderItems.Shared.TextAlignment.Center;
+
+ textBody.Paragraphs[1].HorizontalAlignment = RenderItems.Shared.TextAlignment.Center;
+ textBody.Paragraphs[1].AddText("\r\n What fun, what fun!");
+
+ //Text was added to the paragraph above the last paragraph
+ //We must re-calculate where the next paragraph should be placed
+ textBody.RecalculateParagraphs();
+ textBody.ApplyAutoSize();
+
+ double delta = 0.001;
+
+ //new day beckons is the largest line in the centered paragraph[0]
+ //Assert that the first line has been centered appropriately
+ Assert.AreEqual(9.890869140625d, textBody.Paragraphs[0].Runs[0].Bounds.Left, delta);
+ Assert.AreEqual(33.142333984375d, textBody.Paragraphs[0].Runs[1].Bounds.Left, delta);
+ Assert.AreEqual(59.509033203125d, textBody.Paragraphs[0].Runs[2].Bounds.Left, delta);
+ Assert.AreEqual(0d, textBody.Paragraphs[0].Runs[3].Bounds.Left);
+
+ Assert.AreEqual(5.08, textBody.Paragraphs[1].Runs[0].Bounds.Left, delta);
+ Assert.AreEqual(0d, textBody.Paragraphs[1].Runs[1].Bounds.Left);
+
+ //Assert that the second paragraph has been moved correctly
+ Assert.AreEqual(26.85546875d, textBody.Paragraphs[1].Bounds.Top);
+ GenerateSvgFile("textBodyAlignCenter", baseGroup.Bounds, baseGroup);
+ }
+
+ [TestMethod]
+ public void SvgTextBodyTestRightAlignmentGenerated()
+ {
+ var baseGroup = GenerateGroupRenderItem();
+
+ var textBody = GenerateTextBody(baseGroup);
+ textBody.Paragraphs[0].AddText(" a\r\n new day beckons");
+ textBody.Paragraphs[0].HorizontalAlignment = RenderItems.Shared.TextAlignment.Right;
+
+
+ textBody.Paragraphs[1].HorizontalAlignment = RenderItems.Shared.TextAlignment.Right;
+ textBody.Paragraphs[1].AddText("\r\n What fun, what fun!");
+
+ //Text was added to the paragraph above the last paragraph
+ //We must re-calculate where the next paragraph should be placed
+ textBody.RecalculateParagraphs();
+ textBody.ApplyAutoSize();
+
+ double delta = 0.001;
+
+ //Assert that the first line has been aligned correctly
+ Assert.AreEqual(19.78173828125d, textBody.Paragraphs[0].Runs[0].Bounds.Left, delta);
+ Assert.AreEqual(43.033203125d, textBody.Paragraphs[0].Runs[1].Bounds.Left, delta);
+ Assert.AreEqual(69.39990234375d, textBody.Paragraphs[0].Runs[2].Bounds.Left, delta);
+ Assert.AreEqual(0d, textBody.Paragraphs[0].Runs[3].Bounds.Left);
+
+ Assert.AreEqual(10.16d, textBody.Paragraphs[1].Runs[0].Bounds.Left, delta);
+ Assert.AreEqual(0d, textBody.Paragraphs[1].Runs[1].Bounds.Left);
+
+ GenerateSvgFile("textBodyAlignRight", baseGroup.Bounds, baseGroup);
+ }
+
+ [TestMethod]
+ public void SvgTextBodyVerticalAlignmentGenerated()
+ {
+ var baseGroup = GenerateGroupRenderItem();
+
+ var textBody = GenerateTextBody(baseGroup);
+ textBody.Paragraphs[0].AddText(" a\r\n new day beckons");
+ textBody.Paragraphs[1].AddText("\r\n What fun, what fun!");
+
+ //Text was added to the paragraph above the last paragraph
+ //We must re-calculate where the next paragraph should be placed
+ textBody.RecalculateParagraphs();
+ textBody.ApplyAutoSize();
+
+ textBody.AutoSize = false;
+ textBody.Height = 500;
+
+ textBody.Bounds.Top = 0;
+ textBody.VerticalAlignment = TextAnchoringType.Center;
+ textBody.Bounds.Top = textBody.GetAlignmentVertical();
+
+ double delta = 0.001;
+
+ Assert.AreEqual(180.04052829742432d, textBody.Bounds.Top, delta);
+
+ GenerateSvgFile("textBodyAlignVCenter", baseGroup.Bounds, baseGroup);
+ }
+
+ [TestMethod]
+ public void SvgTextBodyVerticalAlignmentBottomGenerated()
+ {
+ var baseGroup = GenerateGroupRenderItem();
+
+ var textBody = GenerateTextBody(baseGroup);
+ textBody.Paragraphs[0].AddText(" a\r\n new day beckons");
+ textBody.Paragraphs[1].AddText("\r\n What fun, what fun!");
+
+ //Text was added to the paragraph above the last paragraph
+ //We must re-calculate where the next paragraph should be placed
+ textBody.RecalculateParagraphs();
+ textBody.ApplyAutoSize();
+
+ textBody.AutoSize = false;
+ textBody.Height = 500;
+
+ textBody.Bounds.Top = 0;
+ textBody.VerticalAlignment = TextAnchoringType.Bottom;
+ textBody.Bounds.Top = textBody.GetAlignmentVertical();
+
+ double delta = 0.001;
+ Assert.AreEqual(430.04052829742432d, textBody.Bounds.Top, delta);
+
+ GenerateSvgFile("textBodyAlignVBottom", baseGroup.Bounds, baseGroup);
+ }
+
+ private RenderTextbox GenerateTextBox(out GroupRenderItem group)
+ {
+ group = GenerateGroupRenderItem();
+
+ var textbox = new RenderTextbox(group.Bounds, 500d, 500d);
+ var engine = new OpenTypeFontEngine(x => x.SearchSystemDirectories = true);
+ var rc = new RenderContext(() => engine);
+ textbox.TextBody = new SvgTextBodyRenderItem(rc, group.Bounds, true);
+ var paragraph = textbox.TextBody.AddParagraph("Hello");
+
+ paragraph.AddText(" There");
+
+ var rtItem = new RichTextFormatSimple("Second paragraph", "Archivo Narrow", 16f, true);
+ rtItem.FontColor = Color.DarkGreen;
+ var para2 = textbox.TextBody.AddParagraph(rtItem);
+
+ textbox.Rectangle.FillColor = "#F9F6C4";
+
+ return textbox;
+ }
+
+ [TestMethod]
+ public void BasicTextBox()
+ {
+ var textbox = GenerateTextBox(out GroupRenderItem group);
+ textbox.AppendRenderItems(group.RenderItems);
+
+ double delta = 0.001;
+
+ Assert.AreEqual(115.2d, textbox.Width, delta, $"textbox.Width was {textbox.Width}, not 107.952 as expected");
+ Assert.AreEqual(34.979735851287842d, textbox.Height, delta, $"textbox.Height was {textbox.Height}, not 34.978 as expected");
+
+ GenerateSvgFile("BasicTextBox", group.Bounds, group);
+ }
+
+ [TestMethod]
+ public void TextBoxWithMargins()
+ {
+ var textbox = GenerateTextBox(out GroupRenderItem group);
+
+ double delta = 0.001;
+
+ textbox.LeftMargin = 10d;
+ textbox.TopMargin = 10d;
+
+ textbox.AppendRenderItems(group.RenderItems);
+
+ //Assert local position unchanged
+ Assert.AreEqual(0d, textbox.TextBody.Left);
+ Assert.AreEqual(0d, textbox.TextBody.Top);
+
+ //Assert global position changed
+ Assert.AreEqual(10d, textbox.TextBody.Bounds.Position.X);
+ Assert.AreEqual(10d, textbox.TextBody.Bounds.Position.Y);
+
+ //Assert width and height changed by margins
+ Assert.AreEqual(125.2, textbox.Width, delta);
+ Assert.AreEqual(44.979735851287842d, textbox.Height, delta);
+
+
+ GenerateSvgFile("MarginTextBox", group.Bounds, group);
+ }
+
+ [TestMethod]
+ public void TextBoxWithAllMargins()
+ {
+ var textbox = GenerateTextBox(out GroupRenderItem group);
+
+ double delta = 0.001;
+
+ textbox.LeftMargin = 10d;
+ textbox.TopMargin = 10d;
+ textbox.RightMargin = 10d;
+ textbox.BottomMargin = 10d;
+
+ textbox.AppendRenderItems(group.RenderItems);
+
+ //Assert width and height changed by margins
+ Assert.AreEqual(135.2d, textbox.Width, delta);
+ Assert.AreEqual(54.979735851287842d, textbox.Height, delta);
+
+ GenerateSvgFile("AllMarginsTextBox", group.Bounds, group);
+ }
+
+
+ ///
+ /// TODO: Discuss. Should it really work like this?
+ /// There IS an argument to be made that margin should BE textbody position
+ /// At the same time then positioning in accordance with vertical aligment then becomes difficult
+ /// And might affect the margin
+ ///
+ [TestMethod]
+ public void TextBoxWithAllMarginsANDTextbodyChanged()
+ {
+ var textbox = GenerateTextBox(out GroupRenderItem group);
+
+ double delta = 0.001;
+
+ textbox.TextBody.AutoSize = true;
+
+ textbox.TextBody.Left = 15d;
+ textbox.TextBody.Top = 15d;
+
+ textbox.LeftMargin = 10d;
+ textbox.TopMargin = 10d;
+ textbox.RightMargin = 10d;
+ textbox.BottomMargin = 10d;
+
+ textbox.AppendRenderItems(group.RenderItems);
+
+ //Assert width and height changed by margins and textbody
+ Assert.AreEqual(150.2d, textbox.Width, delta);
+ Assert.AreEqual(69.979735851287842d, textbox.Height, delta);
+
+ GenerateSvgFile("TextAnchor_TextBox", group.Bounds, group);
+ }
+ }
+}
diff --git a/src/EPPlus.DrawingRenderer.Tests/EPPlus.DrawingRenderer.Tests.csproj b/src/EPPlus.DrawingRenderer.Tests/EPPlus.DrawingRenderer.Tests.csproj
new file mode 100644
index 0000000000..20604a2971
--- /dev/null
+++ b/src/EPPlus.DrawingRenderer.Tests/EPPlus.DrawingRenderer.Tests.csproj
@@ -0,0 +1,37 @@
+
+
+
+ net8.0;net462
+ latest
+ enable
+ enable
+ True
+ EPPlus.DrawingRenderer.Tests.snk
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ PreserveNewest
+
+
+
+
diff --git a/src/EPPlus.DrawingRenderer.Tests/EPPlus.DrawingRenderer.Tests.snk b/src/EPPlus.DrawingRenderer.Tests/EPPlus.DrawingRenderer.Tests.snk
new file mode 100644
index 0000000000..bee7301e73
Binary files /dev/null and b/src/EPPlus.DrawingRenderer.Tests/EPPlus.DrawingRenderer.Tests.snk differ
diff --git a/src/EPPlus.DrawingRenderer.Tests/GroupItemNewTest.cs b/src/EPPlus.DrawingRenderer.Tests/GroupItemNewTest.cs
new file mode 100644
index 0000000000..757b5c14cd
--- /dev/null
+++ b/src/EPPlus.DrawingRenderer.Tests/GroupItemNewTest.cs
@@ -0,0 +1,378 @@
+//using EPPlus.Export.ImageRenderer.ChartAreaRenderItems.SvgItem;
+//using EPPlus.Export.ImageRenderer.Svg;
+//using EPPlus.Graphics;
+//using EPPlusImageRenderer.ChartAreaRenderItems;
+//using System.Text;
+
+//namespace EPPlus.Export.ImageRenderer.Tests
+//{
+// [TestClass]
+// public class GroupItemNewTest : TestBase
+// {
+// //private void InitalizeTransformGroup()
+// //{
+// // var baseBB = new BoundingBox();
+
+// // //96x96 px
+// // baseBB.Width = 72;
+// // baseBB.Height = 72;
+
+// // var baseItem = new DrawingItemForTesting(baseBB);
+
+// // var groupItem = new SvgTransformGroup(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);
+// //}
+
+
+// [TestMethod]
+// public void TransformGroupMoveCorrect()
+// {
+// var baseBB = new BoundingBox();
+
+// //96x96 px
+// baseBB.Width = 72;
+// baseBB.Height = 72;
+
+// var baseItem = new DrawingItemForTesting(baseBB);
+
+// var holderBB = new BoundingBox();
+// holderBB.Parent = baseItem.Bounds;
+
+// var groupItem = new SvgTransformGroup(baseItem, 9, 9);
+// groupItem.Bounds.Parent = holderBB;
+
+// 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.Bounds.Left = 18;
+// groupItem.Bounds.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);
+
+// groupItem.Scale = new EPPlusImageRenderer.Coordinate(0.5d, 0.5d);
+
+// groupItem.Bounds.Left = 0;
+// groupItem.Bounds.Top = 0;
+
+// //groupItem.Bounds.Left = 0;
+// //groupItem.Bounds.Top = 0;
+
+// rectItem.Bounds.Left = 18;
+// rectItem.Bounds.Top = 18;
+
+// var unchangedVector = rectItem.Bounds.GetWorldCoordinates();
+
+// ////Graphics.Math.Matrix3x3 scaleHalfMatrix
+// var worldCoordinatesRectAfterScale = rectItem.Bounds.Position;
+// Assert.AreEqual(9, worldCoordinatesRectAfterScale.X);
+// Assert.AreEqual(9, worldCoordinatesRectAfterScale.Y);
+
+// var worldCoordinatesSibAfterScale = siblingItem.Bounds.GetWorldCoordinates();
+// Assert.AreEqual(10, worldCoordinatesSibAfterScale.X);
+// Assert.AreEqual(10, worldCoordinatesSibAfterScale.Y);
+
+
+// var sb = new StringBuilder();
+
+// baseItem.ChartAreaRenderItems.Add(groupItem);
+
+// baseItem.Render(sb);
+// var svgString = sb.ToString();
+
+// SaveTextFileToWorkbook($"svg\\StandAloneTranslateGroup.svg", svgString);
+// }
+
+
+// [TestMethod]
+// public void GroupInGroupMoveCorrect2()
+// {
+// var baseBB = new BoundingBox();
+
+// //96x96 px
+// baseBB.Width = 72;
+// baseBB.Height = 72;
+
+// var baseItem = new DrawingItemForTesting(baseBB);
+
+// var groupItem = new SvgGroupItemNew(baseItem, 5, 15);
+
+// SvgRenderRectItem rectItem = new SvgRenderRectItem(baseItem, groupItem.Bounds);
+
+// groupItem.AddChildItem(rectItem);
+
+// rectItem.FillColor = "red";
+// rectItem.FillOpacity = 0.2d;
+
+// rectItem.Width = 20;
+// rectItem.Height = 20;
+
+// groupItem.TranslationOffset.Left = 5;
+// groupItem.TranslationOffset.Top = 6;
+
+// var leftGlobal = groupItem.TranslationOffset.Position.X;
+// var topGlobal = groupItem.TranslationOffset.Position.Y;
+// var leftGlobalUnder = groupItem.Bounds.Position.X;
+// var topGlobalUnder = groupItem.Bounds.Position.Y;
+
+// Assert.AreEqual(10, leftGlobalUnder);
+// Assert.AreEqual(21, topGlobalUnder);
+// }
+
+// [TestMethod]
+// public void GroupInGroupMoveCorrect()
+// {
+// var baseBB = new BoundingBox();
+
+// //96x96 px
+// baseBB.Width = 72;
+// baseBB.Height = 72;
+
+// var baseItem = new DrawingItemForTesting(baseBB);
+
+// var groupItem = new SvgGroupItemNew(baseItem, 9, 9);
+
+// var subItem = new SvgGroupItemNew(baseItem, 9, 9);
+// subItem.TranslationOffset.Parent = groupItem.TranslationOffset;
+// groupItem.AddChildItem(subItem);
+
+// SvgRenderRectItem rectItem = new SvgRenderRectItem(baseItem, groupItem.Bounds);
+
+// rectItem.FillColor = "red";
+// rectItem.FillOpacity = 0.2d;
+
+// rectItem.Width = 20;
+// rectItem.Height = 20;
+
+// subItem.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;
+
+// subItem.AddChildItem(siblingItem);
+
+// var worldCoordinatesRectBefore = rectItem.Bounds.GetWorldCoordinates();
+// Assert.AreEqual(18, worldCoordinatesRectBefore.X);
+// Assert.AreEqual(18, worldCoordinatesRectBefore.Y);
+
+// var worldCoordinatesSibBefore = siblingItem.Bounds.GetWorldCoordinates();
+// Assert.AreEqual(38, worldCoordinatesSibBefore.X);
+// Assert.AreEqual(38, worldCoordinatesSibBefore.Y);
+
+// subItem.TranslationOffset.Left = 9;
+// subItem.TranslationOffset.Top = 9;
+
+// var worldCoordinatesRect = rectItem.Bounds.GetWorldCoordinates();
+// Assert.AreEqual(27, worldCoordinatesRect.X);
+// Assert.AreEqual(27, worldCoordinatesRect.Y);
+
+// var worldCoordinatesSib = siblingItem.Bounds.GetWorldCoordinates();
+// Assert.AreEqual(47, worldCoordinatesSib.X);
+// Assert.AreEqual(47, worldCoordinatesSib.Y);
+
+// var sb = new StringBuilder();
+
+// baseItem.ChartAreaRenderItems.Add(groupItem);
+
+// baseItem.Render(sb);
+// var svgString = sb.ToString();
+
+// SaveTextFileToWorkbook($"svg\\subItemInSubItem.svg", svgString);
+// }
+
+// [TestMethod]
+// public void GrpPosTranslateChildren()
+// {
+// 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.TranslationOffset.Left = 9;
+// groupItem.TranslationOffset.Top = 9;
+
+// 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 gLeft = groupItem.Bounds.Position.X;
+// var gTop = groupItem.Bounds.Position.Y;
+
+// var sb = new StringBuilder();
+
+// baseItem.ChartAreaRenderItems.Add(groupItem);
+
+// baseItem.Render(sb);
+// var svgString = sb.ToString();
+
+// SaveTextFileToWorkbook($"svg\\StandAloneTestGroup.svg", svgString);
+// }
+
+// [TestMethod]
+// public void RotateTwoChildren()
+// {
+// 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.TranslationOffset.Left = 10;
+// groupItem.TranslationOffset.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.ChartAreaRenderItems.Add(centerOfGroupMarker);
+
+// var sb = new StringBuilder();
+
+// baseItem.ChartAreaRenderItems.Add(groupItem);
+
+// baseItem.Render(sb);
+// var svgString = sb.ToString();
+
+// SaveTextFileToWorkbook($"svg\\TestGroupRotated.svg", svgString);
+// }
+// }
+//}
diff --git a/src/EPPlus.DrawingRenderer.Tests/MSTestSettings.cs b/src/EPPlus.DrawingRenderer.Tests/MSTestSettings.cs
new file mode 100644
index 0000000000..aaf278c844
--- /dev/null
+++ b/src/EPPlus.DrawingRenderer.Tests/MSTestSettings.cs
@@ -0,0 +1 @@
+[assembly: Parallelize(Scope = ExecutionScope.MethodLevel)]
diff --git a/src/EPPlus.DrawingRenderer.Tests/PresetShapeDefinitionTests.cs b/src/EPPlus.DrawingRenderer.Tests/PresetShapeDefinitionTests.cs
new file mode 100644
index 0000000000..190b8d847e
--- /dev/null
+++ b/src/EPPlus.DrawingRenderer.Tests/PresetShapeDefinitionTests.cs
@@ -0,0 +1,78 @@
+using EPPlus.DrawingRenderer;
+using EPPlus.DrawingRenderer.ShapeDefinitions;
+using OfficeOpenXml;
+using OfficeOpenXml.Drawing;
+
+namespace TestProject1
+{
+ [TestClass]
+ public sealed class PresetShapeDefinitionTests
+ {
+ [TestMethod]
+ public async Task LoadPreset()
+ {
+ var psd = PresetShapeDefinitions.ShapeDefinitions;
+ Assert.AreEqual(187, psd.Count);
+
+ ExcelPackage.License.SetNonCommercialOrganization("EPPlus Project");
+ using (var p = new ExcelPackage())
+ {
+ var ws = p.Workbook.Worksheets.Add("Sheet1");
+ var shape = ws.Drawings.AddShape("Rect1", OfficeOpenXml.Drawing.eShapeStyle.Rect);
+
+ PresetShapeDefinitions.ShapeDefinitions[(ShapeStyle)shape.Style].Calculate(shape._width, shape._height, shape.TextBody.TextAutofit == eTextAutofit.ShapeAutofit, null, null);
+ await p.SaveAsAsync("c:\\temp\\rect.xlsx");
+ }
+ }
+
+ [TestMethod]
+ public void MathMultiplyTest()
+ {
+ ExcelPackage.License.SetNonCommercialPersonal("EPPLUS");
+ var shDef = PresetShapeDefinitions.ShapeDefinitions[ShapeStyle.MathMultiply];
+
+ using (var p = new ExcelPackage())
+ {
+ var ws = p.Workbook.Worksheets.Add("ws");
+ var shape = ws.Drawings.AddShape("mMult", eShapeStyle.MathMultiply);
+ shDef.Calculate(shape._width, shape._height, shape.TextBody.TextAutofit == eTextAutofit.ShapeAutofit, null, null);
+ }
+ }
+
+ [TestMethod]
+ public void CurvedDownArrow()
+ {
+ ExcelPackage.License.SetNonCommercialPersonal("EPPLUS");
+ var shDef = PresetShapeDefinitions.ShapeDefinitions[ShapeStyle.CurvedDownArrow];
+
+ using (var p = new ExcelPackage())
+ {
+ var ws = p.Workbook.Worksheets.Add("ws");
+ var shape = ws.Drawings.AddShape("mMult", eShapeStyle.CurvedDownArrow);
+ shape.SetSize(100, 100);
+
+ shDef.Calculate(shape._width, shape._height, shape.TextBody.TextAutofit == eTextAutofit.ShapeAutofit, null, null);
+
+
+ //shDef._calculatedValues
+
+ }
+ }
+ [TestMethod]
+ public void BlockArc()
+ {
+ ExcelPackage.License.SetNonCommercialPersonal("EPPLUS");
+ var shDef = PresetShapeDefinitions.ShapeDefinitions[ShapeStyle.BlockArc];
+
+ using (var p = new ExcelPackage())
+ {
+ var ws = p.Workbook.Worksheets.Add("ws");
+ var shape = ws.Drawings.AddShape("mMult", eShapeStyle.BlockArc);
+ shape.SetSize(100, 100);
+
+ shDef.Calculate(shape._width, shape._height, shape.TextBody.TextAutofit == eTextAutofit.ShapeAutofit, null, null);
+ //shDef._calculatedValues
+ }
+ }
+ }
+}
diff --git a/src/EPPlus.DrawingRenderer.Tests/Shape/ShapeToSvgTests.cs b/src/EPPlus.DrawingRenderer.Tests/Shape/ShapeToSvgTests.cs
new file mode 100644
index 0000000000..d56766ae27
--- /dev/null
+++ b/src/EPPlus.DrawingRenderer.Tests/Shape/ShapeToSvgTests.cs
@@ -0,0 +1,720 @@
+using EPPlus.Fonts.OpenType;
+using OfficeOpenXml;
+using OfficeOpenXml.Drawing;
+using OfficeOpenXml.Drawing.Chart;
+using OfficeOpenXml.Style;
+using System.Diagnostics;
+using System.Drawing;
+using System.Reflection;
+using System.Text;
+using System.Xml;
+using TypeConv = OfficeOpenXml.Utils.TypeConversion;
+
+namespace EPPlus.Export.ImageRenderer.Tests.Shape
+{
+ [TestClass]
+ public sealed class ShapeToSvgTests : TestBase
+ {
+
+ [TestInitialize]
+ public void Initialize()
+ {
+ ExcelPackage.License.SetNonCommercialOrganization("EPPlus Project");
+ }
+
+ private OpenTypeFontEngine DefaultFontEngine
+ {
+ get { return new OpenTypeFontEngine(x => x.SearchSystemDirectories = true); }
+ }
+
+ private ExcelPackage GetPackage()
+ {
+ var p = new ExcelPackage();
+ p.Workbook.UseFontEngine(DefaultFontEngine);
+ return p;
+ }
+
+ [TestMethod]
+ public void GroupFill()
+ {
+ ExcelPackage.License.SetNonCommercialOrganization("EPPlus Project");
+ using (var p = OpenTemplatePackage("GroupFill.xlsx"))
+ {
+ var ws = p.Workbook.Worksheets[0];
+ var drawings = ws.Drawings;
+
+ int ix = 0;
+
+ foreach (var drawing in drawings)
+ {
+ if (drawing is ExcelGroupShape)
+ {
+ var gShape = (ExcelGroupShape)drawing;
+ var gDrawings = gShape.Drawings;
+ foreach (var gDrawing in gDrawings)
+ {
+ SaveTextFileToWorkbook($"svg\\GroupFill{ix++}.svg", gDrawing.ToSvg());
+ }
+ }
+ else
+ {
+ var shapeCast = drawing.As.Shape;
+ var filltype = shapeCast.Fill.Style;
+
+ var svg = drawing.ToSvg();
+ SaveTextFileToWorkbook($"svg\\GroupFill{ix++}.svg", svg);
+ }
+ }
+ SaveAndCleanup(p);
+ }
+ }
+
+
+
+ [TestMethod]
+ public void Rect()
+ {
+
+ using (var p = OpenPackage("svg/rect.xlsx", true))
+ {
+ p.Workbook.UseFontEngine(DefaultFontEngine);
+ var ws = p.Workbook.Worksheets.Add("Sheet1");
+ var d = ws.Drawings.AddShape("Shape1", OfficeOpenXml.Drawing.eShapeStyle.Rect);
+ d.Text = "Rectangle Rectangle Rectangle Rectangle";
+ d.TextAlignment = OfficeOpenXml.Drawing.eTextAlignment.Left;
+ d.TextAnchoring = OfficeOpenXml.Drawing.eTextAnchoringType.Bottom;
+ var svg = d.ToSvg();
+ SaveTextFileToWorkbook("svg\\rect.svg", svg);
+ SaveAndCleanup(p);
+ }
+ }
+
+ [TestMethod]
+ public void AddShapeWithPatternFill()
+ {
+ using (var p = OpenTemplatePackage("ShapeWithPattern.xlsx"))
+ {
+ p.Workbook.UseFontEngine(DefaultFontEngine);
+ var ws = p.Workbook.Worksheets[0];
+
+ var myShape = ws.Drawings[0].As.Shape;
+
+ var svg = myShape.ToSvg();
+
+ SaveTextFileToWorkbook("svg\\ShapeWithPattern.svg", svg);
+ SaveAndCleanup(p);
+ }
+ }
+
+
+ [TestMethod]
+ public void RoundRect()
+ {
+ using (var p = GetPackage())
+ {
+ var ws = p.Workbook.Worksheets.Add("Sheet1");
+ var d = ws.Drawings.AddShape("Shape1", eShapeStyle.RoundRect);
+
+ d.TextAlignment = OfficeOpenXml.Drawing.eTextAlignment.Left;
+ d.TextAnchoring = OfficeOpenXml.Drawing.eTextAnchoringType.Bottom;
+ d.Text = "Rectangle Rectangle Rectangle Rectangle";
+
+ var svg = d.ToSvg();
+ SaveTextFileToWorkbook("svg\\Roundrect.svg", svg);
+ SaveWorkbook("svgRoundRectdrawing.xlsx", p);
+ }
+ }
+ [TestMethod]
+ public void Triangle()
+ {
+
+ using (var p = new ExcelPackage())
+ {
+ var ws = p.Workbook.Worksheets.Add("Sheet1");
+ var d = ws.Drawings.AddShape("Shape1", OfficeOpenXml.Drawing.eShapeStyle.Triangle);
+ d.Text = "Test";
+ d.TextAlignment = OfficeOpenXml.Drawing.eTextAlignment.Center;
+ d.TextAnchoring = OfficeOpenXml.Drawing.eTextAnchoringType.Center;
+ var svg = d.ToSvg();
+ SaveTextFileToWorkbook("svg\\Triangle.svg", svg);
+ SaveWorkbook("svgTriangleDrawing.xlsx", p);
+ }
+ }
+ [TestMethod]
+ public void RightArrow()
+ {
+ using (var p = new ExcelPackage())
+ {
+ var ws = p.Workbook.Worksheets.Add("Sheet1");
+ var d = ws.Drawings.AddShape("Shape1", OfficeOpenXml.Drawing.eShapeStyle.RightArrow);
+ d.SetSize(100, 100);
+ var svg = d.ToSvg();
+ SaveTextFileToWorkbook("svg\\RightArrow.svg", svg);
+ SaveWorkbook("svgRightArrowDrawing.xlsx", p);
+ }
+ }
+ [TestMethod]
+ public void SmileyFace()
+ {
+ using (var p = new ExcelPackage())
+ {
+ var ws = p.Workbook.Worksheets.Add("Sheet1");
+ var d = ws.Drawings.AddShape("Shape1", OfficeOpenXml.Drawing.eShapeStyle.SmileyFace);
+ d.SetSize(800, 800);
+ var svg = d.ToSvg();
+ SaveTextFileToWorkbook("svg\\SmileyFace.svg", svg);
+ SaveWorkbook("svgSmileyFace.xlsx", p);
+ }
+ }
+ [TestMethod]
+ public void VerticalScroll()
+ {
+ using (var p = new ExcelPackage())
+ {
+ var ws = p.Workbook.Worksheets.Add("Sheet1");
+ var d = ws.Drawings.AddShape("Shape1", OfficeOpenXml.Drawing.eShapeStyle.VerticalScroll);
+ d.SetSize(800, 800);
+ var svg = d.ToSvg();
+ SaveTextFileToWorkbook("svg\\VerticalScroll.svg", svg);
+ SaveWorkbook("svgVerticalScroll.xlsx", p);
+ }
+ }
+ [TestMethod]
+ public void CloudCallout()
+ {
+ using (var p = new ExcelPackage())
+ {
+ var ws = p.Workbook.Worksheets.Add("Sheet1");
+ var d = ws.Drawings.AddShape("Shape1", OfficeOpenXml.Drawing.eShapeStyle.CloudCallout);
+ d.SetSize(800, 800);
+ var svg = d.ToSvg();
+ SaveTextFileToWorkbook("svg\\CloudCallout.svg", svg);
+ SaveWorkbook("svgCloudCallout.xlsx", p);
+ }
+ }
+ [TestMethod]
+ public void IrregularSeal2()
+ {
+ using (var p = new ExcelPackage())
+ {
+ var ws = p.Workbook.Worksheets.Add("Sheet1");
+ var d = ws.Drawings.AddShape("Shape1", OfficeOpenXml.Drawing.eShapeStyle.IrregularSeal2);
+ d.SetSize(800, 800);
+ var svg = d.ToSvg();
+ SaveTextFileToWorkbook("svg\\IrregularSeal2.svg", svg);
+ SaveWorkbook("IrregularSeal2.xlsx", p);
+ }
+ }
+ [TestMethod]
+ public void LightningBolt()
+ {
+ using (var p = new ExcelPackage())
+ {
+ var ws = p.Workbook.Worksheets.Add("Sheet1");
+ var d = ws.Drawings.AddShape("Shape1", OfficeOpenXml.Drawing.eShapeStyle.LightningBolt);
+ d.SetSize(800, 800);
+ var svg = d.ToSvg();
+ SaveTextFileToWorkbook("svg\\LightningBolt.svg", svg);
+ SaveWorkbook("svgLightningBolt.xlsx", p);
+ }
+ }
+ [TestMethod]
+ public void FlowChartMagneticTape()
+ {
+ using (var p = new ExcelPackage())
+ {
+ var ws = p.Workbook.Worksheets.Add("Sheet1");
+ var d = ws.Drawings.AddShape("Shape1", OfficeOpenXml.Drawing.eShapeStyle.FlowChartMagneticTape);
+ d.SetSize(800, 800);
+ var svg = d.ToSvg();
+ SaveTextFileToWorkbook("svg\\FlowChartMagneticTape.svg", svg);
+ SaveWorkbook("svgFlowChartMagneticTape.xlsx", p);
+ }
+ }
+ [TestMethod]
+ public void MathNotEqual()
+ {
+ using (var p = new ExcelPackage())
+ {
+ var ws = p.Workbook.Worksheets.Add("Sheet1");
+ var d = ws.Drawings.AddShape("Shape1", OfficeOpenXml.Drawing.eShapeStyle.MathNotEqual);
+ d.SetSize(800, 800);
+ var svg = d.ToSvg();
+ SaveTextFileToWorkbook("svg\\MathNotEqual.svg", svg);
+ SaveWorkbook("svgMathNotEqual.xlsx", p);
+ }
+ }
+ [TestMethod]
+ public void Sun()
+ {
+ using (var p = new ExcelPackage())
+ {
+ var ws = p.Workbook.Worksheets.Add("Sheet1");
+ var d = ws.Drawings.AddShape("Shape1", OfficeOpenXml.Drawing.eShapeStyle.Sun);
+
+ d.SetSize(800, 800);
+ d.Fill.Color = Color.Orange;
+ var svg = d.ToSvg();
+ SaveTextFileToWorkbook("svg\\Sun.svg", svg);
+ SaveWorkbook("svgSun.xlsx", p);
+ }
+ }
+ [TestMethod]
+ public void Ellipse()
+ {
+ using (var p = new ExcelPackage())
+ {
+ var ws = p.Workbook.Worksheets.Add("Sheet1");
+ var d = ws.Drawings.AddShape("Shape1", OfficeOpenXml.Drawing.eShapeStyle.Ellipse);
+
+ d.SetSize(800, 800);
+ d.Fill.Color = Color.Orange;
+ var svg = d.ToSvg();
+ SaveTextFileToWorkbook("svg\\Ellipse.svg", svg);
+ SaveWorkbook("svgEllipse.xlsx", p);
+ }
+ }
+ [TestMethod]
+ public void Heart()
+ {
+ using (var p = GetPackage())
+ {
+ var ws = p.Workbook.Worksheets.Add("Sheet1");
+ var d = ws.Drawings.AddShape("Shape1", OfficeOpenXml.Drawing.eShapeStyle.Heart);
+
+ d.SetSize(800, 800);
+ d.Fill.Color = Color.Red;
+ var svg = d.ToSvg();
+ SaveTextFileToWorkbook("svg\\Heart.svg", svg);
+ SaveWorkbook("svgHeart.xlsx", p);
+ }
+ }
+ [TestMethod]
+ public void BevelRed()
+ {
+ using (var p = new ExcelPackage())
+ {
+ var ws = p.Workbook.Worksheets.Add("Sheet1");
+ var d = ws.Drawings.AddShape("Shape1", OfficeOpenXml.Drawing.eShapeStyle.Bevel);
+
+ d.SetSize(804, 804);
+ d.Fill.Color = Color.Red;
+ var svg = d.ToSvg();
+ SaveTextFileToWorkbook("svg\\bevelred.svg", svg);
+ SaveWorkbook("svgBevelred.xlsx", p);
+ }
+ }
+ [TestMethod]
+ public void Bevel()
+ {
+ using (var p = new ExcelPackage())
+ {
+ var ws = p.Workbook.Worksheets.Add("Sheet1");
+ var d = ws.Drawings.AddShape("Shape1", OfficeOpenXml.Drawing.eShapeStyle.Bevel);
+
+ d.SetSize(804, 804);
+ var svg = d.ToSvg();
+ SaveTextFileToWorkbook("svg\\bevel.svg", svg);
+ SaveWorkbook("svgBevel.xlsx", p);
+ }
+ }
+
+ [TestMethod]
+ public void LeftBracket()
+ {
+ using (var p = new ExcelPackage())
+ {
+ var ws = p.Workbook.Worksheets.Add("Sheet1");
+ var d = ws.Drawings.AddShape("Shape1", eShapeStyle.LeftBracket);
+
+ d.SetSize(804, 804);
+ d.Fill.Style = eFillStyle.NoFill;
+ var svg = d.ToSvg();
+ SaveTextFileToWorkbook("svg\\LeftBracket.svg", svg);
+ SaveWorkbook("LeftBracket.xlsx", p);
+ }
+ }
+ [TestMethod]
+ public void CalloutQuadArrow()
+ {
+ using (var p = new ExcelPackage())
+ {
+ var ws = p.Workbook.Worksheets.Add("Sheet1");
+ var d = ws.Drawings.AddShape("Shape1", eShapeStyle.QuadArrowCallout);
+
+ d.SetSize(804, 200);
+ var svg = d.ToSvg();
+ SaveTextFileToWorkbook("svg\\QuadArrowCallout.svg", svg);
+ SaveWorkbook("QuadArrowCallout.xlsx", p);
+ }
+ }
+ [TestMethod]
+ public void ActionButtonHome()
+ {
+ using (var p = new ExcelPackage())
+ {
+ var ws = p.Workbook.Worksheets.Add("Sheet1");
+ var d = ws.Drawings.AddShape("Shape1", eShapeStyle.ActionButtonHome);
+
+ d.SetSize(804, 804);
+ var svg = d.ToSvg();
+ SaveTextFileToWorkbook($"svg\\ActionButtonHome.svg", svg);
+ SaveWorkbook("ActionButtonHome.xlsx", p);
+ }
+ }
+ [TestMethod]
+ public void ActionButtonMovie()
+ {
+ using (var p = new ExcelPackage())
+ {
+ var ws = p.Workbook.Worksheets.Add("Sheet1");
+ var d = ws.Drawings.AddShape("Shape1", eShapeStyle.ActionButtonMovie);
+
+ d.SetSize(804, 804);
+ var svg = d.ToSvg();
+ SaveTextFileToWorkbook($"svg\\ActionButtonMovie.svg", svg);
+ SaveWorkbook("ActionButtonMovie.xlsx", p);
+ }
+ }
+ [TestMethod]
+ public void CustomPath()
+ {
+ using (var p = OpenTemplatePackage(@"svg\CustPath.xlsx"))
+ {
+ var ws = p.Workbook.Worksheets[0];
+ //var d = ws.Drawings[0].As.Shape;
+ //Assert.AreEqual(1, d.CustomGeom.DrawingPaths.Count);
+ //d.Textbox = "GetGetRectangle GetGetRectangle GetGetRectangle GetGetRectangle";
+ //d.TextAlignment = OfficeOpenXml.Drawing.TextAlignment.Left;
+ //d.TextAnchoring = OfficeOpenXml.Drawing.eTextAnchoringType.Bottom;
+ //var svg = renderer.RenderDrawingToSvg(d);
+ //SaveTextFileToWorkbook("CustomDrawing1.svg", svg);
+
+ var d = ws.Drawings[0].As.Shape;
+ var svg = d.ToSvg();
+ SaveTextFileToWorkbook($"svg\\CustomDrawing2.svg", svg);
+
+ //SaveWorkbook("svgdrawing.xlsx");
+ }
+ }
+
+
+ [TestMethod]
+ public void GenerateAllShapes()
+ {
+ using (var p = new ExcelPackage())
+ {
+ var ws = p.Workbook.Worksheets.Add("Shapes");
+ int y = 100, i = 1;
+ foreach (eShapeStyle style in Enum.GetValues(typeof(eShapeStyle)))
+ {
+ if (style == eShapeStyle.CustomShape) continue;
+ var shape = ws.Drawings.AddShape(style.ToString(), style);
+ shape.Text = style.ToString();
+ Assert.AreEqual(eDrawingType.Shape, shape.DrawingType);
+ shape.SetPosition(y, 100);
+ shape.SetSize(600, 600);
+ y += 700;
+ i++;
+ }
+ SaveWorkbook("shapes.xlsx", p);
+ }
+ }
+
+ [TestMethod]
+ public void TestShapes()
+ {
+ using (var p = OpenTemplatePackage("margins.xlsx"))
+ {
+ var drawings = p.Workbook.Worksheets[0].Drawings;
+ var textbody = drawings[0].As.Shape.TextBody;
+
+ var insertCM = textbody.LeftInsert.Value * 0.0352777778;
+
+ Assert.AreEqual(0.3d, insertCM, 0.00001);
+ }
+ }
+ [TestMethod]
+ public void GenerateSvgForGradientFilledShapes()
+ {
+ using (var p = OpenTemplatePackage("GradientFillShapes.xlsx"))
+ {
+ var ws = p.Workbook.Worksheets[0];
+ int ix = 1;
+
+ //var d = ws.Drawings[7];
+ //var svg = renderer.RenderDrawingToSvg(d);
+ //File.WriteAllText($"c:\\temp\\{d.Name}-{ix}.svg", svg);
+
+ foreach (var d in ws.Drawings)
+ {
+ var svg = d.ToSvg();
+ SaveTextFileToWorkbook($"svg\\{d.Name}-{ix}.svg", svg);
+ ix++;
+ }
+ }
+ }
+ [TestMethod]
+ public void GenerateSvgForGradientRadialFilledShapes()
+ {
+ using (var p = OpenTemplatePackage("GradiantRadial.xlsx"))
+ {
+ var ws = p.Workbook.Worksheets[0];
+ //var d = ws.Drawings[5];
+ //var svg = renderer.RenderDrawingToSvg(d);
+ //File.WriteAllText($"c:\\temp\\{d.Name}-{5}.svg", svg);
+
+ int ix = 1;
+
+ foreach (var d in ws.Drawings)
+ {
+ var svg = d.ToSvg();
+ SaveTextFileToWorkbook($"svg\\{d.Name}-{ix}.svg", svg);
+ ix++;
+ }
+ }
+ }
+ [TestMethod]
+ public void GenerateSvgForPatternFilledShapes()
+ {
+ using (var p = OpenTemplatePackage("PatternFills.xlsx"))
+ {
+ var ws = p.Workbook.Worksheets[0];
+ int ix = 1;
+
+ foreach (ExcelShape d in ws.Drawings)
+ {
+ var svg = d.ToSvg();
+ SaveTextFileToWorkbook($"svg\\Pattern-{d.Fill.PatternFill.PatternType}-{ix}.svg", svg);
+ ix++;
+ }
+ }
+ }
+ [TestMethod]
+ public void GenerateSvgForBlipFillShapes()
+ {
+ using (var p = OpenTemplatePackage("BlipFills.xlsx"))
+ {
+ var ws = p.Workbook.Worksheets[0];
+ int ix = 1;
+
+ foreach (ExcelShape d in ws.Drawings)
+ {
+ var svg = d.ToSvg();
+ SaveSvg($"Blip{ix}.svg", svg);
+ ix++;
+ }
+ }
+ }
+
+
+ [TestMethod]
+ public void GenerateSvgForCircle()
+ {
+ using (var p = OpenTemplatePackage("GradientRadialVerifyCircle.xlsx"))
+ {
+ var ws = p.Workbook.Worksheets[0];
+ //var svg = renderer.RenderDrawingToSvg(ws.Drawings[1]);
+ //File.WriteAllText($"c:\\temp\\ChartForSvg{1}.svg", svg);
+
+ int ix = 1;
+
+ foreach (ExcelShape d in ws.Drawings)
+ {
+ var svg = d.ToSvg();
+ SaveTextFileToWorkbook($"svg\\DrawForSvg{ix}.svg", svg);
+ ix++;
+ }
+ }
+ }
+
+ [TestMethod]
+ public void SuperScriptShape()
+ {
+ using (var p = OpenTemplatePackage("Superscript.xlsx"))
+ {
+ var ws = p.Workbook.Worksheets[0];
+ //var ix = 1;
+ //var c = ws.Drawings[ix];
+ //var svg = renderer.RenderDrawingToSvg(c);
+ //SaveTextFileToWorkbook($"svg\\LineChartForSvg_Single{ix++}.svg", svg);
+ var ix = 0;
+ foreach (var c in ws.Drawings)
+ {
+ var svg = c.ToSvg();
+ SaveTextFileToWorkbook($"svg\\ss{ix++}.svg", svg);
+ }
+ }
+ }
+
+ [TestMethod]
+ public void SuperAndSubScript()
+ {
+ using (var p = OpenTemplatePackage("SuperAndSubScript.xlsx"))
+ {
+ var ws = p.Workbook.Worksheets[0];
+
+ var currShape = ws.Drawings[0];
+
+ var svg = currShape.ToSvg();
+ SaveTextFileToWorkbook("svg\\SuperAndSubScript.svg", svg);
+ }
+ }
+
+ [TestMethod]
+ public void OpenRightAligned()
+ {
+ using (var p = OpenTemplatePackage("SimpleChartRightAlign.xlsx"))
+ {
+ var c = p.Workbook.Worksheets[0].Drawings[0];
+
+ var svg = c.ToSvg();
+ SaveTextFileToWorkbook($"svg\\SimplestChartRightAlign.svg", svg);
+ }
+ }
+ [TestMethod]
+ public void TestStyling()
+ {
+ 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 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;
+ }
+ }
+
+ [TestMethod]
+ public void GenerateShapeCenteredParagraph()
+ {
+ using (var p = OpenPackage("ShapeTestCentered.xlsx",true))
+ {
+ var sheet = p.Workbook.Worksheets.Add("ShapeSheet");
+
+ var _currentShape = sheet.Drawings.AddShape("CubeTest", eShapeStyle.Cube);
+
+ _currentShape.SetPixelWidth(300d);
+ _currentShape.SetPixelHeight(300d);
+
+ _currentShape.Fill.Style = eFillStyle.SolidFill;
+ _currentShape.Fill.Color = System.Drawing.Color.BlueViolet;
+ _currentShape.Font.Color = System.Drawing.Color.Goldenrod;
+
+ _currentShape.TextBody.TopInsert = 0;
+ _currentShape.TextBody.BottomInsert = 0;
+ _currentShape.TextBody.RightInsert = 0;
+ _currentShape.TextBody.LeftInsert = 0;
+
+ var para1 = _currentShape.TextBody.Paragraphs.Add("TextBodySvg\r\na");
+ //var test = _currentShape.TextBody.AnchorCenter;
+
+ para1.LeftMargin = 5;
+ _currentShape.TextBody.TopInsert = 10;
+
+ var para2 = _currentShape.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);
+
+ _currentShape.TextBody.Paragraphs[0].HorizontalAlignment = eTextAlignment.Center;
+
+ _currentShape.TextAnchoring = eTextAnchoringType.Top;
+
+ //var smiley = "\ud83d\ude03";
+
+ tRun1.Fill.Color = System.Drawing.Color.IndianRed;
+ var tRun2 = para2.TextRuns.Add("SvgSize 24");
+ tRun2.FontSize = 24;
+
+ //_currentShape.TextAnchoring = eTextAnchoringType.Center;
+
+ _currentShape.TextBody.HorizontalTextOverflow = eTextHorizontalOverflow.Clip;
+ _currentShape.TextBody.VerticalTextOverflow = eTextVerticalOverflow.Clip;
+
+
+ //SetFillColor(_currentShape.Fill, txtFillColor.Text);
+ //SetFillColor(_currentShape.Border.Fill, txtBorderColor.Text);
+
+ var aFont = _currentShape.Font;
+ var paragraph0 = _currentShape.TextBody.Paragraphs[0];
+
+ _currentShape.GetSizeInPixels(out int testWidth, out int testHeight);
+
+ var svg = _currentShape.ToSvg();
+ SaveTextFileToWorkbook("svg\\centeredParagraph.svg", svg);
+ SaveAndCleanup(p);
+ }
+ }
+
+ [TestMethod]
+ public void ChartAndShapeGreen()
+ {
+ using (var p = OpenTemplatePackage("ShapeAndChartTestGreen.xlsx"))
+ {
+ var ws = p.Workbook.Worksheets[0];
+ //var ix = 1;
+ //var c = ws.Drawings[ix];
+ //var svg = renderer.RenderDrawingToSvg(c);
+ //SaveTextFileToWorkbook($"svg\\LineChartForSvg_Single{ix++}.svg", svg);
+ var ix = 0;
+ foreach (var c in ws.Drawings)
+ {
+ var svg = c.ToSvg();
+ SaveTextFileToWorkbook($"svg\\TestGreen{ix++}.svg", svg);
+ }
+ }
+ }
+
+ [TestMethod]
+ public void CreateChartsWithDifferentSize()
+ {
+ using (var p = OpenPackage("ChartWithDifferentSizes.xlsx", true))
+ {
+ var ws = p.Workbook.Worksheets.Add("Chart1");
+
+ LoadItemData(ws);
+ var chart1 = ws.Drawings.AddChart("chart1", eChartType.Line);
+ chart1.Title.Text = "This is a very long title that should be wrapped into multiple lines.";
+ chart1.Title.Font.Size = 32;
+ chart1.Series.Add(ws.Cells["O2:O11"], ws.Cells["N2:N10"]);
+ chart1.SetPosition(2, 0, 1, 0);
+ chart1.SetSize(400, 400);
+
+ var chart2 = ws.Drawings.AddChart("chart2", eChartType.ColumnClustered);
+ chart2.Title.Text = "This is a very long title that should be wrapped into multiple lines.";
+ chart2.Title.Font.Size = 32;
+ chart2.Series.Add(ws.Cells["O2:O11"], ws.Cells["N2:N10"]);
+ chart2.SetPosition(25, 0, 1, 0);
+ chart2.SetSize(400, 800);
+
+ var chart3 = ws.Drawings.AddChart("chart3", eChartType.BarClustered);
+ chart3.Title.Text = "This is a very long title that should be wrapped into multiple lines.";
+ chart3.Title.Font.Size = 32;
+ chart3.Series.Add(ws.Cells["O2:O11"], ws.Cells["N2:N10"]);
+ chart3.SetPosition(25, 0, 8, 0);
+ chart3.SetSize(800, 400);
+
+ SaveAndCleanup(p);
+ }
+ }
+ }
+}
diff --git a/src/EPPlus.DrawingRenderer.Tests/StyleTests.cs b/src/EPPlus.DrawingRenderer.Tests/StyleTests.cs
new file mode 100644
index 0000000000..89b24c08fd
--- /dev/null
+++ b/src/EPPlus.DrawingRenderer.Tests/StyleTests.cs
@@ -0,0 +1,244 @@
+using OfficeOpenXml;
+using System.Drawing;
+using System.Linq;
+
+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 svg = c.ToSvg();
+ //var renderer = new EPPlusImageRenderer.ImageRenderer();
+ //var svg = renderer.RenderDrawingToSvg(c);
+ SaveTextFileToWorkbook($"svg\\baseThemeChartStyle.svg", svg);
+ }
+ }
+
+ [TestMethod]
+ public void BaseThemeChartStyle2()
+ {
+ ExcelPackage.License.SetNonCommercialOrganization("EPPlus Project");
+
+ using (var p = OpenTemplatePackage("baseThemeChartStyle2.xlsx"))
+ {
+ var c = p.Workbook.Worksheets[0].Drawings[0].As.Chart.LineChart;
+ var svg = c.ToSvg();
+ //var renderer = new EPPlusImageRenderer.ImageRenderer();
+ //var svg = renderer.RenderDrawingToSvg(c);
+ SaveTextFileToWorkbook($"svg\\baseThemeChartStyle2.svg", svg);
+ }
+ }
+ [TestMethod]
+ public void ChangeSeriesTest()
+ {
+ 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.StyleManager.ApplyStyles();
+
+ p.Workbook.Worksheets[0].Cells["A3"].Value = 15;
+ p.Workbook.Worksheets[0].Cells["B3"].Value = 2;
+ p.Workbook.Worksheets[0].Cells["C3"].Value = 25;
+
+ var range = p.Workbook.Worksheets[0].Cells["A3:C3"];
+
+ var series = simpleChart.Series.Add(range);
+ series.Header = "MySeries";
+
+ simpleChart.Title.Text = "Hello";
+
+ //simpleChart.StyleManager.ApplyStyles();
+ //var chartDefaultStyle = simpleChart.StyleManager.Style;
+
+ //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.ApplyStyles();
+
+ var svg = simpleChart.ToSvg();
+ SaveTextFileToWorkbook($"svg\\ChangeSeriesStyleTest.svg", svg);
+ p.SaveAs(GetOutputFile("", "ChangeSeriesStyleTest.xlsx"));
+ //SaveAndCleanup(p);
+ }
+ }
+
+ [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;
+
+ //p.Workbook.Worksheets[0].Cells["A3"].Value = 15;
+ //p.Workbook.Worksheets[0].Cells["B3"].Value = 2;
+ //p.Workbook.Worksheets[0].Cells["C3"].Value = 25;
+
+ //var range = p.Workbook.Worksheets[0].Cells["A3:C3"];
+
+ //var series = simpleChart.Series.Add(range);
+ //series.Header = "MySeries";
+
+ 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);
+
+ //
+ //var currBorderFill = simpleChart.StyleManager.Style.DataPointLine.Border.Fill;
+ //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 svg = simpleChart.ToSvg();
+ 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 ExtractThemeStyleWorksDataLine()
+ {
+ 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";
+
+ //var chartDefaultStyle = simpleChart.StyleManager.Style;
+ //simpleChart.StyleManager.ApplyStyles();
+
+ //simpleChart.Title.TextBody.Paragraphs[0].TextRuns[0].Fill.Color = Color.CornflowerBlue;
+
+ //var svg = simpleChart.ToSvg();
+ //SaveTextFileToWorkbook($"svg\\MyLineIonThemeExcelChangeOnlyTitle.svg", svg);
+
+ var fontSize = simpleChart.Title.Font.Size;
+
+ Assert.AreEqual(simpleChart.Title.Font.Size, 14d);
+ var paragraphPropeties = simpleChart.Title.GetNode("c:txPr/a:p/a:pPr");
+ var paragraphPropertiesRich = simpleChart.Title.GetNode("c:tx/c:rich/a:p/a:pPr");
+ Assert.AreEqual(paragraphPropeties.InnerXml, paragraphPropertiesRich.InnerXml);
+
+ 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.DrawingRenderer.Tests/TestBase.cs b/src/EPPlus.DrawingRenderer.Tests/TestBase.cs
new file mode 100644
index 0000000000..227fafa276
--- /dev/null
+++ b/src/EPPlus.DrawingRenderer.Tests/TestBase.cs
@@ -0,0 +1,570 @@
+/*******************************************************************************
+ * You may amend and distribute as you like, but don't remove this header!
+ *
+ * Required Notice: Copyright (C) EPPlus Software AB.
+ * https://epplussoftware.com
+ *
+ * This library is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+
+ * This library is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
+ * See the GNU Lesser General Public License for more details.
+ *
+ * The GNU Lesser General Public License can be viewed at http://www.opensource.org/licenses/lgpl-license.php
+ * If you unfamiliar with this license or have questions about it, here is an http://www.gnu.org/licenses/gpl-faq.html
+ *
+ * All code and executables are provided "" as is "" with no warranty either express or implied.
+ * The author accepts no liability for any damage or loss of business that this product may cause.
+ *
+ * Code change notes:
+ *
+ Date Author Change
+ *******************************************************************************
+ 01/27/2020 EPPlus Software AB Initial release EPPlus 5
+ *******************************************************************************/
+using System;
+using Microsoft.VisualStudio.TestTools.UnitTesting;
+using OfficeOpenXml;
+using System.IO;
+using System.Reflection;
+using OfficeOpenXml.Drawing;
+using System.Threading.Tasks;
+using System.Collections.Generic;
+using System.Globalization;
+using System.Threading;
+
+[TestClass]
+public abstract class TestBase
+{
+ protected TestBase()
+ {
+ }
+
+ private static void GetEnvironment(string key, ref string value, bool isPath)
+ {
+ var v = Environment.GetEnvironmentVariable(key, EnvironmentVariableTarget.Machine);
+ if (string.IsNullOrEmpty(v))
+ {
+ v = Environment.GetEnvironmentVariable(key, EnvironmentVariableTarget.User);
+ if (string.IsNullOrEmpty(v))
+ {
+ v = Environment.GetEnvironmentVariable(key, EnvironmentVariableTarget.Process);
+ }
+ }
+ if (!string.IsNullOrEmpty(v))
+ {
+ if (isPath && !v.EndsWith("\\")) v += "\\";
+ value = v;
+ }
+ }
+
+ private class SalesData
+ {
+ public string Continent { get; set; }
+ public string Country { get; set; }
+ public string State { get; set; }
+ public double Sales { get; set; }
+
+ }
+ private class GeoData
+ {
+ public string Country { get; set; }
+ public string State { get; set; }
+ public double Sales { get; set; }
+ }
+ //protected static FileInfo _file;
+ protected static string _clipartPath = "";
+ protected static string _worksheetPath = @"c:\epplusTest\Testoutput\";
+ protected static string _testInputPath = AppContext.BaseDirectory + "\\workbooks\\";
+ protected static string _testInputPathOptional = @"c:\epplusTest\workbooks\"; //Team shared workbooks for tests
+ protected static string _testInputLocalPathOptional = @"c:\epplusTest\workbooks\"; //Local workboks for tests
+ protected static string _imagePath = @"c:\epplusTest\images\";
+ ///
+ ///Gets or sets the test context which provides
+ ///information about and functionality for the current test run.
+ ///
+ public TestContext TestContext { get; set; }
+ static bool _isInitialized = false;
+ public static void InitBase()
+ {
+ _isInitialized = true;
+ //Gets the paths from environment variables if set. Otherwise uses the default paths as specified in the declaration.
+ GetEnvironment("EPPlusTestOutputPath", ref _worksheetPath, true);
+ GetEnvironment("EPPlusTestTemplateLocalPath", ref _testInputLocalPathOptional, true);
+ GetEnvironment("EPPlusTestTemplateSharedPath", ref _testInputPathOptional, true);
+ GetEnvironment("EPPlusTestImagePath", ref _imagePath, true);
+
+
+ _clipartPath = Path.Combine(Path.GetTempPath(), @"EPPlus clipart");
+ if (!Directory.Exists(_clipartPath))
+ {
+ Directory.CreateDirectory(_clipartPath);
+ }
+ //if(Environment.GetEnvironmentVariable("EPPlusTestInputPath")!=null)
+ //{
+ // _testInputPathOptional = Environment.GetEnvironmentVariable("EPPlusTestInputPath");
+ //}
+ var asm = Assembly.GetExecutingAssembly();
+ var validExtensions = new[]
+ {
+ ".gif", ".wmf"
+ };
+
+ foreach (var name in asm.GetManifestResourceNames())
+ {
+ foreach (var ext in validExtensions)
+ {
+ if (name.EndsWith(ext, StringComparison.OrdinalIgnoreCase))
+ {
+ string fileName = name.Replace("EPPlusTest.Resources.", "");
+ using (var stream = asm.GetManifestResourceStream(name))
+ using (var file = File.Create(Path.Combine(_clipartPath, fileName)))
+ {
+ stream.CopyTo(file);
+ }
+ break;
+ }
+ }
+ }
+
+ var di = new DirectoryInfo(_worksheetPath);
+ _worksheetPath = di.FullName + "\\";
+ }
+
+ ///
+ /// Saves and disposes a package
+ ///
+ ///
+ ///
+ protected static void SaveAndCleanup(ExcelPackage pck, bool dispose = true)
+ {
+ if (pck.Workbook.Worksheets.Count > 0)
+ {
+ pck.Save();
+ }
+ if (dispose) pck.Dispose();
+ }
+ protected static void SaveTextFileToWorkbook(string fileName, string content)
+ {
+ var file = EnsurePathExists(_worksheetPath + fileName);
+ File.WriteAllText(file, content);
+ }
+ protected void SaveSvg(string fileName, string svg)
+ {
+ var file = EnsurePathExists(_imagePath + fileName);
+ File.WriteAllText(_imagePath + fileName, svg);
+ }
+
+
+ private static string EnsurePathExists(string fileName)
+ {
+ var file = new FileInfo(fileName);
+ if (string.IsNullOrEmpty(file.DirectoryName)==false && Directory.Exists(file.DirectoryName) == false)
+ {
+ Directory.CreateDirectory(file.DirectoryName);
+ }
+
+ return file.FullName;
+ }
+
+ protected static bool ExistsPackage(string name)
+ {
+ var fi = new FileInfo(_worksheetPath + name);
+ return fi.Exists;
+ }
+ protected static void AssertIfNotExists(string name)
+ {
+ if (!ExistsPackage(name))
+ {
+ Assert.Inconclusive($"{_worksheetPath}{name} workbook is missing");
+ }
+ }
+ protected static ExcelPackage OpenPackage(string name, bool delete = false)
+ {
+ CreateWorksheetPathIfNotExists();
+ var file = new FileInfo(_worksheetPath + name);
+ if (delete && file.Exists)
+ {
+ file.Delete();
+ }
+ return new ExcelPackage(file);
+ }
+ protected static async Task OpenPackageAsync(string name, bool delete = false, string password = null)
+ {
+ CreateWorksheetPathIfNotExists();
+ var file = new FileInfo(_worksheetPath + name);
+ if (delete && file.Exists)
+ {
+ file.Delete();
+ }
+ var p = new ExcelPackage();
+ if (password == null)
+ {
+ await p.LoadAsync(file).ConfigureAwait(false);
+ }
+ else
+ {
+ await p.LoadAsync(file, password).ConfigureAwait(false);
+ }
+ return p;
+ }
+
+ static void CreateWorksheetPathIfNotExists()
+ {
+ CreatePathIfNotExists(_worksheetPath);
+ }
+ protected static void CreatePathIfNotExists(string path)
+ {
+ if (!Directory.Exists(path))
+ {
+ Directory.CreateDirectory(path);
+ }
+ }
+ protected static ExcelPackage OpenTemplatePackage(string name)
+ {
+ var t = new FileInfo(_testInputPath + name);
+ if (t.Exists)
+ {
+ var file = new FileInfo(_worksheetPath + name);
+ return new ExcelPackage(file, t);
+ }
+ else
+ {
+ t = new FileInfo(_testInputPathOptional + name);
+ if (t.Exists)
+ {
+ var file = new FileInfo(_worksheetPath + name);
+ return new ExcelPackage(file, t);
+ }
+ t = new FileInfo(_worksheetPath + name);
+ if (t.Exists)
+ {
+ return new ExcelPackage(t);
+ }
+ Assert.Inconclusive($"Template {name} does not exist in path {_testInputPath}");
+ }
+ return null;
+ }
+ protected static FileInfo GetTemplateFile(string name)
+ {
+ var t = new FileInfo(_testInputPath + name);
+ if (t.Exists)
+ {
+ return new FileInfo(_worksheetPath + name);
+ }
+ else
+ {
+ t = new FileInfo(_testInputPathOptional + name);
+ if (t.Exists)
+ {
+ return t;
+ }
+ t = new FileInfo(_worksheetPath + name);
+ if (t.Exists)
+ {
+ return t;
+ }
+
+ Assert.Inconclusive($"Template File {name} does not exist in path {_testInputPath} or {_worksheetPath}");
+ }
+ return null;
+ }
+ protected static FileInfo GetOutputFile(string subPath, string fileName)
+ {
+ var path = _worksheetPath + subPath;
+ if (Directory.Exists(path) == false)
+ {
+ Directory.CreateDirectory(path);
+ }
+ if (path.EndsWith("\\") == false) path += "\\";
+
+ return new FileInfo(path + fileName);
+ }
+ internal void IsNullRange(ExcelRange address)
+ {
+ for (int row = address._fromRow; row <= address._toRow; row++)
+ {
+ for (int col = address._fromCol; col <= address._toCol; col++)
+ {
+ Assert.IsNull(address._worksheet.Cells[row, col].Value);
+ }
+ }
+ }
+ protected static void SaveWorkbook(string name, ExcelPackage pck)
+ {
+ if (pck.Workbook.Worksheets.Count == 0) return;
+ var fi = new FileInfo(_worksheetPath + name);
+ if (fi.Exists)
+ {
+ //fi.Delete();
+ }
+ pck.SaveAs(fi);
+ }
+ protected static readonly DateTime _loadDataStartDate = new DateTime(2022, 11, 1);
+ ///
+ /// Loads 4 columns of {date, numeric, string, numeric}
+ ///
+ /// The worksheet
+ /// Number of items
+ /// The start column
+ /// The start row
+ /// Add a column with hyperlinks
+ /// Adds a TimeSpan column. Requires add hyperlink to be true
+ protected static void LoadTestdata(ExcelWorksheet ws, int noItems = 100, int startColumn = 1, int startRow = 1, bool addHyperlinkColumn = false, bool addTimeSpan = false, DateTime? startDate = null)
+ {
+ ws.SetValue(1, startColumn, "Date");
+ ws.SetValue(1, startColumn + 1, "NumValue");
+ ws.SetValue(1, startColumn + 2, "StrValue");
+ ws.SetValue(1, startColumn + 3, "NumFormattedValue");
+ if (addHyperlinkColumn)
+ {
+ ws.SetValue(1, startColumn + 4, "HyperLink");
+ }
+ if (addTimeSpan)
+ {
+ ws.SetValue(1, startColumn + 5, "TimeSpan");
+ }
+
+ DateTime dt = startDate ?? _loadDataStartDate;
+ int row = 1;
+ for (int i = 1; i < noItems; i++)
+ {
+ row = startRow + i;
+ ws.SetValue(row, startColumn, dt);
+ ws.SetValue(row, startColumn + 1, row);
+ ws.SetValue(row, startColumn + 2, $"Value {row}");
+ ws.SetValue(row, startColumn + 3, row * 33);
+ if (addHyperlinkColumn)
+ {
+ ws.Cells[row, startColumn + 4].SetHyperlink(new Uri("https://epplussoftware.com"));
+ if (addTimeSpan)
+ {
+ ws.SetValue(row, startColumn + 5, new TimeSpan(0, 1, i % 60, 0, 0));
+ }
+ }
+
+ dt = dt.AddDays(1);
+ }
+ ws.Cells[startRow, startColumn, row, startColumn].Style.Numberformat.Format = "yyyy-MM-dd";
+ if (addTimeSpan)
+ {
+ ws.Cells[startRow, startColumn + 5, row, startColumn + 5].Style.Numberformat.Format = "hh:mm:ss";
+ }
+ ws.Cells.AutoFitColumns();
+ }
+ protected static ExcelRangeBase LoadHierarkiTestData(ExcelWorksheet ws)
+ {
+ var l = new List
+ {
+ new SalesData{ Continent="Europe", Country="Sweden", State = "Stockholm", Sales = 154 },
+ new SalesData{ Continent="Asia", Country="Vietnam", State = "Ho Chi Minh", Sales= 88 },
+ new SalesData{ Continent="Europe", Country="Sweden", State = "Västerås", Sales = 33 },
+ new SalesData{ Continent="Asia", Country="Japan", State = "Tokyo", Sales= 534 },
+ new SalesData{ Continent="Europe", Country="Germany", State = "Frankfurt", Sales = 109 },
+ new SalesData{ Continent="Asia", Country="Vietnam", State = "Hanoi", Sales= 322 },
+ new SalesData{ Continent="Asia", Country="Japan", State = "Osaka", Sales= 88 },
+ new SalesData{ Continent="North America", Country="Canada", State = "Vancover", Sales= 99 },
+ new SalesData{ Continent="Asia", Country="China", State = "Peking", Sales= 205 },
+ new SalesData{ Continent="North America", Country="Canada", State = "Toronto", Sales= 138 },
+ new SalesData{ Continent="Europe", Country="France", State = "Lyon", Sales = 185 },
+ new SalesData{ Continent="North America", Country="USA", State = "Boston", Sales= 155 },
+ new SalesData{ Continent="Europe", Country="France", State = "Paris", Sales = 127 },
+ new SalesData{ Continent="North America", Country="USA", State = "New York", Sales= 330 },
+ new SalesData{ Continent="Europe", Country="Germany", State = "Berlin", Sales = 210 },
+ new SalesData{ Continent="North America", Country="USA", State = "San Fransisco", Sales= 411 },
+ };
+
+ return ws.Cells["A1"].LoadFromCollection(l, true, OfficeOpenXml.Table.TableStyles.Medium12);
+ }
+ protected static void LoadGeoTestData(ExcelWorksheet ws)
+ {
+ var l = new List
+ {
+ new GeoData{ Country="Sweden", State = "Stockholm", Sales = 154 },
+ new GeoData{ Country="Sweden", State = "Jämtland", Sales = 55 },
+ new GeoData{ Country="Sweden", State = "Västerbotten", Sales = 44},
+ new GeoData{ Country="Sweden", State = "Dalarna", Sales = 33 },
+ new GeoData{ Country="Sweden", State = "Uppsala", Sales = 22 },
+ new GeoData{ Country="Sweden", State = "Skåne", Sales = 47 },
+ new GeoData{ Country="Sweden", State = "Halland", Sales = 88 },
+ new GeoData{ Country="Sweden", State = "Norrbotten", Sales = 99 },
+ new GeoData{ Country="Sweden", State = "Västra Götaland", Sales = 120 },
+ new GeoData{ Country="Sweden", State = "Södermanland", Sales = 57 },
+ };
+
+ ws.Cells["A1"].LoadFromCollection(l, true, OfficeOpenXml.Table.TableStyles.Medium12);
+ }
+ protected static ExcelRangeBase LoadItemData(ExcelWorksheet ws)
+ {
+ ws.Cells["K1"].Value = "Item";
+ ws.Cells["L1"].Value = "Category";
+ ws.Cells["M1"].Value = "Stock";
+ ws.Cells["N1"].Value = "Price";
+ ws.Cells["O1"].Value = "Date for grouping";
+
+ ws.Cells["K2"].Value = "Crowbar";
+ ws.Cells["L2"].Value = "Hardware";
+ ws.Cells["M2"].Value = 12;
+ ws.Cells["N2"].Value = 85.2;
+ ws.Cells["O2"].Value = new DateTime(2010, 1, 31);
+
+ ws.Cells["K3"].Value = "Crowbar";
+ ws.Cells["L3"].Value = "Hardware";
+ ws.Cells["M3"].Value = 15;
+ ws.Cells["N3"].Value = 12.2;
+ ws.Cells["O3"].Value = new DateTime(2010, 2, 28);
+
+ ws.Cells["K4"].Value = "Hammer";
+ ws.Cells["L4"].Value = "Hardware";
+ ws.Cells["M4"].Value = 550;
+ ws.Cells["N4"].Value = 72.7;
+ ws.Cells["O4"].Value = new DateTime(2010, 3, 31);
+
+ ws.Cells["K5"].Value = "Hammer";
+ ws.Cells["L5"].Value = "Hardware";
+ ws.Cells["M5"].Value = 120;
+ ws.Cells["N5"].Value = 11.3;
+ ws.Cells["O5"].Value = new DateTime(2010, 4, 30);
+
+ ws.Cells["K6"].Value = "Crowbar";
+ ws.Cells["L6"].Value = "Hardware";
+ ws.Cells["M6"].Value = 120;
+ ws.Cells["N6"].Value = 173.2;
+ ws.Cells["O6"].Value = new DateTime(2010, 5, 31);
+
+ ws.Cells["K7"].Value = "Hammer";
+ ws.Cells["L7"].Value = "Hardware";
+ ws.Cells["M7"].Value = 1;
+ ws.Cells["N7"].Value = 4.2;
+ ws.Cells["O7"].Value = new DateTime(2010, 6, 30);
+
+ ws.Cells["K8"].Value = "Saw";
+ ws.Cells["L8"].Value = "Hardware";
+ ws.Cells["M8"].Value = 4;
+ ws.Cells["N8"].Value = 33.12;
+ ws.Cells["O8"].Value = new DateTime(2010, 6, 28);
+
+ ws.Cells["K9"].Value = "Screwdriver";
+ ws.Cells["L9"].Value = "Hardware";
+ ws.Cells["M9"].Value = 1200;
+ ws.Cells["N9"].Value = 45.2;
+ ws.Cells["O9"].Value = new DateTime(2010, 8, 31);
+
+ ws.Cells["K10"].Value = "Apple";
+ ws.Cells["L10"].Value = "Groceries";
+ ws.Cells["M10"].Value = 807;
+ ws.Cells["N10"].Value = 1.2;
+ ws.Cells["O10"].Value = new DateTime(2010, 9, 30);
+
+ ws.Cells["K11"].Value = "Butter";
+ ws.Cells["L11"].Value = "Groceries";
+ ws.Cells["M11"].Value = 52;
+ ws.Cells["N11"].Value = 7.2;
+ ws.Cells["O11"].Value = new DateTime(2010, 10, 31);
+ ws.Cells["O2:O11"].Style.Numberformat.Format = "yyyy-MM-dd";
+ return ws.Cells["K1:O11"];
+ }
+
+ protected static void SetDateValues(ExcelWorksheet _ws, int noItems = 100)
+ {
+ /* Set dates in numeric column */
+ _ws.SetValue(50, 2, new DateTime(2018, 12, 15));
+ _ws.SetValue(51, 2, new DateTime(2018, 12, 16));
+ _ws.SetValue(52, 2, new DateTime(2018, 12, 17));
+ _ws.Cells[50, 2, 52, 2].Style.Numberformat.Format = "yyyy-MM-dd";
+
+ _ws.Cells[1, 1, noItems, 1].Style.Numberformat.Format = "yyyy-MM-dd";
+ _ws.Cells[2, 4, noItems, 4].Style.Numberformat.Format = "#,##0.00";
+ }
+ protected int GetRowFromDate(DateTime date, DateTime? initStartDate = null)
+ {
+ var startDate = initStartDate ?? _loadDataStartDate; //new DateTime(DateTime.Today.Year-1, 11, 1);
+ if (startDate > date)
+ return 2;
+ else
+ return (date - startDate).Days + 2;
+ }
+ protected static ExcelWorksheet TryGetWorksheet(ExcelPackage pck, string worksheetName)
+ {
+ var ws = pck.Workbook.Worksheets[worksheetName];
+ if (ws == null) Assert.Inconclusive($"{worksheetName} worksheet is missing");
+ return ws;
+ }
+ protected static ExcelShape TryGetShape(ExcelPackage pck, string wsName)
+ {
+ var ws = pck.Workbook.Worksheets[wsName];
+ if (ws == null) Assert.Inconclusive($"{wsName} worksheet is missing");
+ var shape = (ExcelShape)ws.Drawings[0];
+ return shape;
+ }
+ protected static FileInfo GetResourceFile(string fileName)
+ {
+ string path = AppContext.BaseDirectory;
+ while (!Directory.Exists(path + "\\Resources") && path.Length > 4)
+ {
+ path = new DirectoryInfo(path + "\\..").FullName;
+ }
+ if (path.Length > 4)
+ {
+ return new FileInfo(path + "\\Resources\\" + fileName);
+ }
+ else
+ {
+ return null;
+ }
+ }
+ protected void AssertIsNull(ExcelRangeBase range)
+ {
+ foreach (var r in range)
+ {
+ Assert.IsNotNull(r.Value);
+ }
+ }
+ protected void AssertNoChange(ExcelRangeBase range)
+ {
+ foreach (var r in range)
+ {
+ Assert.AreEqual(r.Address, r.Value);
+ }
+ }
+
+ protected static void SetValues(ExcelWorksheet ws, int rowcols)
+ {
+ for (int r = 1; r <= rowcols; r++)
+ {
+ for (int c = 1; c <= rowcols; c++)
+ {
+ ws.Cells[r, c].Value = ExcelCellBase.GetAddress(r, c);
+ }
+ }
+ }
+ protected static MemoryStream GetImageMemoryStream(string imageFile)
+ {
+ imageFile = _imagePath + imageFile;
+ if (File.Exists(imageFile))
+ {
+ return new MemoryStream(File.ReadAllBytes(imageFile));
+ }
+ Assert.Inconclusive($"Image file {imageFile} does not exist");
+ return null;
+ }
+ CultureInfo _savedCurrentCulture = null;
+ protected void SwitchToCulture(string cultureCode = "en-US")
+ {
+ _savedCurrentCulture = Thread.CurrentThread.CurrentCulture;
+ Thread.CurrentThread.CurrentCulture = new CultureInfo(cultureCode);
+ }
+ protected void SwitchBackToCurrentCulture()
+ {
+ if (_savedCurrentCulture == null)
+ {
+ throw new InvalidOperationException("Current Culture is not saved. Please use method SwitchToCulture() before using this method.");
+ }
+ Thread.CurrentThread.CurrentCulture = _savedCurrentCulture;
+
+ }
+}
diff --git a/src/EPPlus.DrawingRenderer.Tests/TestFontMeasurer.cs b/src/EPPlus.DrawingRenderer.Tests/TestFontMeasurer.cs
new file mode 100644
index 0000000000..a92705daff
--- /dev/null
+++ b/src/EPPlus.DrawingRenderer.Tests/TestFontMeasurer.cs
@@ -0,0 +1,361 @@
+using EPPlus.Fonts.OpenType;
+using EPPlus.Fonts.OpenType.Integration;
+using EPPlus.Fonts.OpenType.TextShaping;
+using EPPlus.Fonts.OpenType.Utils;
+using OfficeOpenXml.Interfaces.Drawing.Text;
+using OfficeOpenXml.Interfaces.Fonts;
+using System.Drawing;
+
+namespace TestProject1
+{
+ [TestClass]
+ public class TestFontMeasurer
+ {
+ [TestInitialize]
+ public void Setup()
+ {
+ _systemFolderEngine = new OpenTypeFontEngine(x => x.SearchSystemDirectories = true);
+ }
+
+ [TestCleanup]
+ public void Cleanup()
+ {
+ SystemFolderEngine.Dispose();
+ _systemFolderEngine = null;
+ }
+
+ private OpenTypeFontEngine? _systemFolderEngine;
+
+ private OpenTypeFontEngine SystemFolderEngine => _systemFolderEngine ?? new OpenTypeFontEngine(x => x.SearchSystemDirectories = true);
+
+ [TestMethod]
+ public void CompareFontMeasurer3()
+ {
+ var mf = new MeasurementFont()
+ {
+ FontFamily = "Aptos Narrow",
+ Style = MeasurementFontStyles.Regular,
+ Size = 72.0f,
+ };
+
+ var handler = new TextHandler(SystemFolderEngine, mf);
+
+ var testStr = "Hello there⁴₂";
+
+ var exactWidth = handler.MeasureTextInPixels(testStr);
+ var exactHeight = handler.GetLineHeightInPoints().PointToPixel();
+
+ //Ascent+Descent
+ var lineSpacing = handler.GetLineHeightInPoints().PointToPixel();
+
+ //Distance between text baseline and top of box AKA Ascent
+ var getBaseLine = handler.GetAscentInPoints().PointToPixel();
+
+ var approxHeight = (handler.GetAscentInPoints() - handler.GetDescentInPoints()).PointToPixel();
+
+ var wholePixelWidth = TextUtils.RoundToWhole(exactWidth);
+ var wholePixelHeight = TextUtils.RoundToWhole(getBaseLine);
+
+ Assert.AreEqual(90d, wholePixelHeight, 0.1);
+ }
+
+ [TestMethod]
+ public void TestWrapText()
+ {
+ string fontName = "Aptos Narrow";
+ string testStr = "hello the most";
+ double fontSize = 11.0d;
+ double MaxPixelWidth = 54d;
+
+ MeasurementFont mf = new MeasurementFont()
+ {
+ FontFamily = fontName,
+ Size = (float)fontSize,
+ Style = MeasurementFontStyles.Regular
+ };
+
+ var handler = new TextHandler(SystemFolderEngine, mf);
+
+ var strings = handler.WrapText(testStr, MaxPixelWidth.PixelToPoint());
+
+ Assert.AreEqual("hello the", strings[0]);
+ Assert.AreEqual("most", strings[1]);
+ }
+
+ [TestMethod]
+ public void TestWrapTextLongContinous()
+ {
+ string testString = "Hello World! a b c d e f g h i j k l m n o p q r s t u v w x y z \r\n" +
+ "A B C D E F G H I J K L M N O P Q R S T U V W X Y Z Sooooo " +
+ "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA" +
+ "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA " +
+ "BBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB" +
+ "BBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBCCCCCC" +
+ "CCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCC" +
+ "CCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCC" +
+ "CCCCCCCCCCCCCCCCCCCCCCCCCC";
+ double fontSize = 11.0d;
+ //double MaxPixelWidth = 750d;
+ double MaxPixelWidth = 750d;
+ var fontName = "Aptos Narrow";
+ MeasurementFont mf = new MeasurementFont()
+ {
+ FontFamily = fontName,
+ Size = (float)fontSize,
+ Style = MeasurementFontStyles.Regular
+ };
+ var handler = new TextHandler(SystemFolderEngine, mf);
+
+ var strings = handler.WrapText(testString, MaxPixelWidth.PixelToPoint());
+
+ Assert.AreEqual("Hello World! a b c d e f g h i j k l m n o p q r s t u v w x y z ", strings[0]);
+ Assert.AreEqual("A B C D E F G H I J K L M N O P Q R S T U V W X Y Z Sooooo", strings[1]);
+ Assert.AreEqual("AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA", strings[2]);
+ Assert.AreEqual("AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA", strings[3]);
+ Assert.AreEqual("BBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB", strings[4]);
+ Assert.AreEqual("BBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBCCCCCC", strings[5]);
+
+ //Note: We wrap differently from Excel. We assume the kerning is applied correctly which means one extra 'C' fits in these two rows
+ //And is therefore not part of the last one.
+ Assert.AreEqual("CCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCC", strings[6]); //In excel one less 'C'
+ Assert.AreEqual("CCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCC", strings[7]); //In excel one less 'C'
+ Assert.AreEqual("CCCCCCCCCCCCCCCCCCCCCCCC", strings[8]);//In excel two more 'C' chars
+
+
+ mf.Size = 12f;
+
+ handler.SetFontSize(mf.Size);
+
+ var strings12Size = handler.WrapText(testString, MaxPixelWidth.PixelToPoint());
+ Assert.AreEqual("Hello World! a b c d e f g h i j k l m n o p q r s t u v w x y z ", strings12Size[0]);
+ Assert.AreEqual("A B C D E F G H I J K L M N O P Q R S T U V W X Y Z Sooooo", strings12Size[1]);
+ var actual = "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA";
+ var render = "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA";
+ Assert.AreEqual("AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA", strings12Size[2]);
+ Assert.AreEqual("AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA", strings12Size[3]);
+
+ Assert.AreEqual("BBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB", strings12Size[4]);
+ Assert.AreEqual("BBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB", strings12Size[5]);
+ Assert.AreEqual("BBBBBBBBBCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCC", strings12Size[6]);
+ Assert.AreEqual("CCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCC", strings12Size[7]);
+ Assert.AreEqual("CCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCC", strings12Size[8]);
+ }
+
+ [TestMethod]
+ public void SpaceCase2()
+ {
+ var text = "fermentum nec rhoncus et, vulputate";
+ }
+
+ [TestMethod]
+ public void SpaceCase()
+ {
+ var text = "Praesent ut auctor urna.";
+ var mf = new MeasurementFont()
+ {
+ FontFamily = "Aptos Narrow",
+ Style = MeasurementFontStyles.Regular,
+ Size = 11.0f,
+ };
+ var layout = SystemFolderEngine.GetTextLayoutEngineForFont(mf);
+ var output = layout.WrapText(text, 11f, 39.4);
+
+ var shaper = OpenTypeFonts.GetShaperForFont(mf);
+ var shapes2 = shaper.ShapeLight("nec rhoncus");
+ var width= shapes2.GetWidthInPoints(11f);
+ //var shaper = OpenTypeFonts.GetShaperForFont(mf);
+ //var shapes2 = shaper.ShapeLight("ut auctor");
+ //var width2 = shapes2.GetWidthInPoints(11f);
+
+ //var width3 = shaper.ShapeLight("ut").GetWidthInPoints(11.0f);
+
+ //Assert.AreEqual(8.84619140625, width3);
+ Assert.AreEqual("Praesent", output[0]);
+ Assert.AreEqual("ut auctor", output[1]);
+ Assert.AreEqual("urna.", output[2]);
+ }
+
+ [TestMethod]
+ public void LoremIpsumTesting()
+ {
+ var text = "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nulla pulvinar interdum imperdiet. Praesent ut auctor urna. Phasellus sollicitudin quam vitae est convallis";
+
+ var mf = new MeasurementFont()
+ {
+ FontFamily = "Aptos Narrow",
+ Style = MeasurementFontStyles.Regular,
+ Size = 11.0f,
+ };
+
+ var handler = new TextHandler(SystemFolderEngine, mf);
+ //float ptMax = 39.68503937007874015748031496063f;
+ //float ptMax = 36.840393700787401574803149606299f - 1.5f;
+ //float pixels = 53f;
+ //float pixels = 46f;
+ //var ptMax = pixels.PointToPixel();
+
+ ShapingOptions options = new ShapingOptions();
+
+ var engine = new OpenTypeFontEngine(x => x.SearchSystemDirectories = true);
+ var layout = engine.GetTextLayoutEngineForFont(mf);
+
+ //var wrappedStrings = layout.WrapRichText(new List() { text }, new List() { mf }, 39.4f);
+
+ var shaper = OpenTypeFonts.GetShaperForFont(mf);
+
+ var wrappedStrings = layout.WrapText(text, 11f, 39.4);
+
+ //var shapes1 = shaper.ShapeLight("imperdie");
+ //var shapes2 = shaper.ShapeLight("ut auctor");
+ //var width = shapes1.GetWidthInPoints(11f);
+ //var width2 = shapes2.GetWidthInPoints(11f);
+ var curIndex = 0;
+
+ Assert.AreEqual("Lorem", wrappedStrings[curIndex++]);
+ Assert.AreEqual("ipsum", wrappedStrings[curIndex++]);
+ Assert.AreEqual("dolor sit", wrappedStrings[curIndex++]);
+ Assert.AreEqual("amet,", wrappedStrings[curIndex++]);
+ Assert.AreEqual("consect", wrappedStrings[curIndex++]);
+ Assert.AreEqual("etur", wrappedStrings[curIndex++]);
+ Assert.AreEqual("adipisci", wrappedStrings[curIndex++]);
+ Assert.AreEqual("ng elit.", wrappedStrings[curIndex++]);
+ Assert.AreEqual("Nulla", wrappedStrings[curIndex++]);
+ Assert.AreEqual("pulvinar", wrappedStrings[curIndex++]);
+ Assert.AreEqual("interdu", wrappedStrings[curIndex++]);
+ Assert.AreEqual("m", wrappedStrings[curIndex++]);
+ Assert.AreEqual("imperdie", wrappedStrings[curIndex++]);
+ Assert.AreEqual("t.", wrappedStrings[curIndex++]);
+ Assert.AreEqual("Praesent", wrappedStrings[curIndex++]);
+ Assert.AreEqual("ut auctor", wrappedStrings[curIndex++]);
+ Assert.AreEqual("urna.", wrappedStrings[curIndex++]);
+ Assert.AreEqual("Phasellu", wrappedStrings[curIndex++]);
+ Assert.AreEqual("s", wrappedStrings[curIndex++]);
+ Assert.AreEqual("sollicitu", wrappedStrings[curIndex++]);
+ Assert.AreEqual("din", wrappedStrings[curIndex++]);
+ Assert.AreEqual("quam", wrappedStrings[curIndex++]);
+ Assert.AreEqual("vitae est", wrappedStrings[curIndex++]);
+ Assert.AreEqual("convallis", wrappedStrings[curIndex++]);
+ }
+
+ [TestMethod]
+ public void WrapDifficultSpotSpace()
+ {
+ var text = "facilisis tellus. Morbi ";
+ var pointWidth = 54.081732283464566929133858267717f;
+
+ var mf = new MeasurementFont()
+ {
+ FontFamily = "Aptos Narrow",
+ Style = MeasurementFontStyles.Regular,
+ Size = 11.0f,
+ };
+
+ var handler = new TextHandler(SystemFolderEngine, mf);
+
+ var wrappedStrings = handler.WrapText(text, pointWidth);
+
+ Assert.AreEqual("facilisis", wrappedStrings[0]);
+ Assert.AreEqual("tellus.", wrappedStrings[1]);//Difficult part
+ Assert.AreEqual("Morbi ", wrappedStrings[2]);
+ }
+
+ [TestMethod]
+ public void LoremIpsum20Paragraphs()
+ {
+ var Lorem20Str = "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nulla pulvinar interdum imperdiet. Praesent ut auctor urna. Phasellus sollicitudin quam vitae est convallis, eu mattis lorem efficitur. Mauris nulla libero, tincidunt id ipsum non, lobortis tristique mauris. Donec ut enim sed enim fermentum molestie vel quis odio. Morbi a fermentum massa, sit amet ultrices est. Aenean ante mi, fermentum nec rhoncus et, vulputate vel sapien. Donec tempus, leo quis luctus rhoncus, augue odio pharetra libero, ac blandit urna turpis sed diam. Vivamus augue purus, eleifend et justo facilisis, imperdiet rhoncus sem. Quisque accumsan pellentesque elit, eget finibus massa accumsan in. Fusce eu accumsan enim. Cras pulvinar enim vel tellus lacinia, consectetur euismod tortor consectetur. Praesent tincidunt pretium eros, ac auctor magna luctus sed. Ut porta lectus quam, non ornare mauris lacinia sit amet. Nullam egestas dolor quis magna porttitor, ac iaculis nisi hendrerit. Proin at mollis lacus, in porttitor nunc. Aliquam erat volutpat. Sed vel egestas risus, at aliquam arcu. Vestibulum quis lobortis nulla. Etiam pellentesque auctor nulla, eget tincidunt felis rhoncus id. Sed metus ante, efficitur id dui eu, fermentum mollis odio. Phasellus ullamcorper iaculis augue vel consequat. Etiam fringilla euismod interdum. Ut molestie massa id fringilla lobortis. Vestibulum malesuada, ante vel mattis ultrices, sem ante molestie augue, non tristique dui mi non nibh. Maecenas dictum, sem eget convallis rhoncus, lacus enim porta neque, in posuere dui ex a sapien. Nam lacus nibh, posuere sed elit eget, condimentum facilisis ligula. Cras consectetur lacus ullamcorper velit aliquet bibendum eget vel nulla. Aenean varius ac erat quis ullamcorper. Donec laoreet arcu a lorem volutpat faucibus. Vivamus vehicula leo ut erat luctus scelerisque. Morbi posuere ex et magna egestas facilisis. Fusce scelerisque volutpat erat bibendum hendrerit. Nam blandit mi ut metus pulvinar, vel tempus lacus euismod. Quisque imperdiet sit amet sapien sed ultricies. Phasellus sodales, ipsum vitae tincidunt facilisis, nulla ligula faucibus felis, eget vehicula ante lacus eu lorem. Integer congue diam ac viverra tristique. Curabitur tristique dolor quis quam pretium, et scelerisque quam dictum. Maecenas vitae sodales ligula. Pellentesque maximus diam vel porta convallis. Ut aliquam eros quis porta pellentesque. Fusce in ex ut mi egestas cursus. Aliquam erat volutpat. Cras laoreet condimentum laoreet. Sed eget facilisis tellus. Morbi viverra odio sed odio placerat mollis. Duis turpis metus, dignissim varius urna quis, viverra dignissim dui. Vivamus viverra at nisi quis convallis. Suspendisse fringilla risus et ante sollicitudin, sed eleifend sem placerat. Proin pretium blandit arcu, eget rhoncus risus hendrerit at. Interdum et malesuada fames ac ante ipsum primis in faucibus. Phasellus vulputate efficitur maximus. Cras blandit nulla eu nisi auctor tempus. Sed pretium lacus ac magna vestibulum, aliquam faucibus orci luctus. Mauris enim lorem, varius ut ante quis, varius viverra lectus. Fusce blandit nibh vel feugiat efficitur. Donec maximus id justo ac mollis. Vestibulum ante ipsum primis in faucibus orci luctus et ultrices posuere cubilia curae; Nulla placerat lectus et purus dictum, id congue nisi euismod. Maecenas euismod fermentum diam, sit amet gravida magna suscipit a. Quisque consectetur arcu eu nunc sodales scelerisque. Nulla non tincidunt nulla. Pellentesque ut tortor vel enim convallis malesuada. Aliquam ultricies bibendum ultrices. Mauris rutrum ac nisl vel luctus. Donec quis nibh vitae orci ultricies gravida. Aliquam vitae velit porttitor lorem bibendum fringilla volutpat a eros. Curabitur at commodo tortor. Etiam ultricies, neque et iaculis euismod, diam ligula luctus mi, vitae lobortis felis lorem eu nulla. Sed a semper ex. Interdum et malesuada fames ac ante ipsum primis in faucibus. Nulla mauris elit, pulvinar ac tortor et, luctus hendrerit nisl. In egestas auctor urna vitae laoreet. Praesent bibendum egestas convallis. Proin non suscipit tellus. Nullam at nibh in urna laoreet sodales non vel tellus. Donec in enim dui. Phasellus quis quam tincidunt, pellentesque lorem ac, scelerisque neque. Integer nec tempus urna. Donec elit massa, eleifend eu sapien sit amet, mollis pellentesque est. Nullam tristique tellus iaculis arcu consectetur pretium. Sed venenatis convallis scelerisque. Suspendisse varius urna sit amet purus accumsan, id ultricies erat efficitur. Cras non ipsum eget nulla efficitur commodo sit amet non lacus. Proin viverra enim sit amet enim tempus ullamcorper. Class aptent taciti sociosqu ad litora torquent per conubia nostra, per inceptos himenaeos. Duis ac massa interdum, gravida ex egestas, finibus purus. Nunc consectetur commodo lacus, ac convallis quam lobortis eu. Sed convallis tempor commodo. Nulla sed convallis mauris. Donec venenatis nisi est, ac ullamcorper mi pretium quis. Donec vitae eros at ipsum interdum scelerisque nec vitae nisi. Sed vestibulum erat ac bibendum dapibus. Morbi nec elit id quam tristique cursus id sed sem. Praesent non ante enim. Pellentesque habitant morbi tristique senectus et netus et malesuada fames ac turpis egestas. Praesent non mauris dui. Aliquam rhoncus mattis ante sed venenatis. Vivamus vehicula sed sapien sed dictum. In aliquet, urna efficitur tincidunt lobortis, nibh justo tristique purus, sed volutpat risus magna et libero.Suspendisse lectus justo, varius eget arcu et, semper laoreet erat. Quisque eget lacus ornare, pellentesque erat sit amet, vulputate felis. Duis luctus, massa a pellentesque mollis, massa elit convallis mi, vel bibendum ex ex eu purus. Suspendisse vel fermentum urna, ac commodo enim. Mauris tincidunt cursus elit, a volutpat libero commodo et. Etiam dapibus libero venenatis tellus lobortis, vel lacinia elit faucibus. Maecenas semper sed quam quis finibus. Integer efficitur, libero imperdiet sollicitudin commodo, elit arcu vulputate est, eget finibus mi urna sit amet magna. Cras ullamcorper consequat ornare. Fusce convallis nunc vel risus cursus, at maximus ligula cursus. Pellentesque vulputate risus libero, eget cursus nibh sodales sed. Donec accumsan sem et massa semper, id dignissim velit vehicula.Cras cursus ipsum ac erat vehicula, nec iaculis purus dictum. Quisque lacinia elit vitae leo dictum, vel dignissim velit dapibus. Aenean sem nisi, faucibus interdum justo eu, euismod porttitor ex. Morbi et lectus lectus. Duis neque felis, suscipit at scelerisque eu, scelerisque id orci. Curabitur et placerat ipsum. Proin gravida sapien nisl, et varius ipsum mollis nec. Quisque dignissim consectetur feugiat. Aenean eros purus, laoreet interdum rutrum at, aliquet sit amet lectus. Donec gravida lorem ut tincidunt laoreet. Donec consequat viverra ligula, in accumsan mi bibendum scelerisque. Quisque ac risus justo. Morbi magna arcu, egestas nec luctus commodo, cursus eget nunc. Vivamus euismod lorem ex, et maximus felis hendrerit eget. Nullam ullamcorper euismod ligula, et iaculis ligula ultricies a. Fusce aliquam, enim vel fermentum ultrices, elit quam semper erat, vitae semper velit augue non magna.Quisque maximus semper arcu, id pellentesque est tempus a. Phasellus lacus elit, auctor sit amet lacinia a, dapibus vitae velit. Phasellus ut pharetra justo, ut ultricies erat. Sed molestie sapien vel interdum lobortis. Nulla facilisi. Vestibulum ante ipsum primis in faucibus orci luctus et ultrices posuere cubilia curae; Nulla nec mauris quis nisi vulputate gravida quis nec velit.Nam et congue ipsum. Nulla vel elit non dolor mollis aliquet vel at magna. Pellentesque nec facilisis elit. In vulputate quis sem porta suscipit. Nullam sed ex ornare nibh suscipit mattis quis non lacus. Mauris vel ex urna. Vivamus ultricies sapien sit amet sapien vehicula gravida. Donec feugiat volutpat quam. Vestibulum auctor dictum nisl, id hendrerit metus ullamcorper sed. Nulla maximus lacus vel mollis maximus. Nulla laoreet placerat quam eu viverra. Etiam feugiat accumsan nisl a condimentum. Sed ultricies ante ante, ac auctor ligula gravida nec. Praesent a neque dignissim, sagittis felis sit amet, condimentum turpis. Fusce at leo vel est blandit malesuada. Pellentesque et neque non metus pellentesque imperdiet. Praesent pellentesque lacinia lorem, et tristique tellus efficitur id. Suspendisse aliquet ultricies justo vitae interdum. Cras tristique viverra quam, eget gravida mi fermentum imperdiet. Sed imperdiet vitae purus ut volutpat. Nulla lacinia elit in fermentum consectetur. Phasellus commodo ut nisl sit amet sagittis. Duis ac ornare orci. Vivamus vel enim posuere, pharetra ex vel, elementum est. Vestibulum commodo luctus metus eget maximus. Suspendisse a nulla a odio eleifend faucibus. Suspendisse semper lacus non porttitor aliquet. Cras ac scelerisque magna, et pulvinar justo. Integer cursus pulvinar fringilla. Mauris imperdiet nibh sit amet tempor laoreet. Morbi tincidunt tortor ex, sit amet maximus purus tristique quis. Quisque sed hendrerit velit. Mauris mattis nibh ut eros luctus, eget mattis massa auctor. Phasellus eu neque at augue gravida sagittis nec non tortor. Etiam porttitor sem sodales mi ullamcorper gravida. In in dictum orci. In vitae vestibulum quam. Cras augue eros, tincidunt ac elit posuere, sollicitudin efficitur lectus. Praesent quis sodales nisl. Proin sit amet molestie est. In commodo mauris vel mauris efficitur, nec mollis mauris sagittis. Cras ligula nibh, egestas sit amet eros in, lacinia tristique magna. Cras risus libero, lacinia eget libero vitae, maximus aliquet nibh. Mauris id sodales purus, vitae dictum lectus. Cras consectetur ligula velit, tempus pulvinar lacus porttitor vitae. Phasellus eget tellus ipsum. Donec interdum laoreet elit non vestibulum. Cras sed urna ullamcorper, aliquam erat eget, porta orci. Vestibulum eget congue nulla. Sed sem tortor, euismod at rutrum id, sagittis a nunc. Duis in nibh facilisis, dignissim purus ut, hendrerit magna. Sed semper ligula id massa elementum, non malesuada velit egestas. Nullam dictum, mi nec euismod sagittis, ligula leo ullamcorper dolor, quis faucibus odio metus eget magna. Ut gravida metus non metus bibendum bibendum. In sagittis eleifend aliquet. Interdum et malesuada fames ac ante ipsum primis in faucibus. Nam mollis sagittis felis, in faucibus tortor pretium vel. Nam nec enim metus. Donec in augue arcu. Proin non lobortis purus, sit amet lacinia elit. Suspendisse quis eros condimentum, blandit justo sit amet, lobortis nisl. Suspendisse maximus massa sed urna tempor ornare. Nunc malesuada purus odio, eu luctus lectus auctor nec. Morbi auctor pellentesque auctor. Sed ullamcorper, ex vitae aliquam vulputate, est diam feugiat mi, id porttitor lectus orci ac leo. Donec sit amet velit pulvinar, venenatis turpis ut, interdum ligula. Interdum et malesuada fames ac ante ipsum primis in faucibus. Vestibulum eu lacus urna. Maecenas sem nulla, accumsan eu ultricies sed, tempor vel magna. Cras aliquet sollicitudin sapien ac pulvinar. Praesent ac sodales mi. Integer vitae mauris massa. Maecenas iaculis orci et faucibus interdum. Nunc nec maximus felis, sed finibus quam. Pellentesque felis massa, vestibulum in tellus vitae, congue tincidunt justo. Nunc vitae enim malesuada, bibendum ante nec, varius tellus. Praesent vitae nisi id quam auctor lacinia at non quam. Nam nec ligula sit amet felis auctor sagittis. Nunc in risus eu urna varius laoreet quis sit amet felis. Morbi varius tempor orci, eu vestibulum nunc vestibulum ac. Nunc vehicula velit eleifend consequat porta. Suspendisse maximus dapibus orci, in vulputate massa pretium ac. Quisque malesuada aliquet aliquet.";
+
+ var mf = new MeasurementFont()
+ {
+ FontFamily = "Aptos Narrow",
+ Style = MeasurementFontStyles.Regular,
+ Size = 11.0f,
+ };
+
+ var handler = new TextHandler(SystemFolderEngine, mf);
+
+ double maxPixelWidth = 72d;
+
+ var wrappedStrings = handler.WrapText(Lorem20Str, maxPixelWidth.PixelToPoint());
+
+ const string SavedComparisonString = "Lorem\r\nipsum dolor\r\nsit amet,\r\nconsectetur\r\nadipiscing\r\nelit. Nulla\r\npulvinar\r\ninterdum\r\nimperdiet.\r\nPraesent ut\r\nauctor urna.\r\nPhasellus\r\nsollicitudin\r\nquam vitae\r\nest\r\nconvallis,\r\neu mattis\r\nlorem\r\nefficitur.\r\nMauris nulla\r\nlibero,\r\ntincidunt id\r\nipsum non,\r\nlobortis\r\ntristique\r\nmauris.\r\nDonec ut\r\nenim sed\r\nenim\r\nfermentum\r\nmolestie vel\r\nquis odio.\r\nMorbi a\r\nfermentum\r\nmassa, sit\r\namet\r\nultrices est.\r\nAenean\r\nante mi,\r\nfermentum\r\nnec\r\nrhoncus et,\r\nvulputate\r\nvel sapien.\r\nDonec\r\ntempus, leo\r\nquis luctus\r\nrhoncus,\r\naugue odio\r\npharetra\r\nlibero, ac\r\nblandit urna\r\nturpis sed\r\ndiam.\r\nVivamus\r\naugue\r\npurus,\r\neleifend et\r\njusto\r\nfacilisis,\r\nimperdiet\r\nrhoncus\r\nsem.\r\nQuisque\r\naccumsan\r\npellentesqu\r\ne elit, eget\r\nfinibus\r\nmassa\r\naccumsan\r\nin. Fusce eu\r\naccumsan\r\nenim. Cras\r\npulvinar\r\nenim vel\r\ntellus\r\nlacinia,\r\nconsectetur\r\neuismod\r\ntortor\r\nconsectetur\r\n. Praesent\r\ntincidunt\r\npretium\r\neros, ac\r\nauctor\r\nmagna\r\nluctus sed.\r\nUt porta\r\nlectus\r\nquam, non\r\nornare\r\nmauris\r\nlacinia sit\r\namet.\r\nNullam\r\negestas\r\ndolor quis\r\nmagna\r\nporttitor, ac\r\niaculis nisi\r\nhendrerit.\r\nProin at\r\nmollis\r\nlacus, in\r\nporttitor\r\nnunc.\r\nAliquam\r\nerat\r\nvolutpat.\r\nSed vel\r\negestas\r\nrisus, at\r\naliquam\r\narcu.\r\nVestibulum\r\nquis\r\nlobortis\r\nnulla. Etiam\r\npellentesqu\r\ne auctor\r\nnulla, eget\r\ntincidunt\r\nfelis\r\nrhoncus id.\r\nSed metus\r\nante,\r\nefficitur id\r\ndui eu,\r\nfermentum\r\nmollis odio.\r\nPhasellus\r\nullamcorper\r\niaculis\r\naugue vel\r\nconsequat.\r\nEtiam\r\nfringilla\r\neuismod\r\ninterdum.\r\nUt molestie\r\nmassa id\r\nfringilla\r\nlobortis.\r\nVestibulum\r\nmalesuada,\r\nante vel\r\nmattis\r\nultrices,\r\nsem ante\r\nmolestie\r\naugue, non\r\ntristique dui\r\nmi non\r\nnibh.\r\nMaecenas\r\ndictum,\r\nsem eget\r\nconvallis\r\nrhoncus,\r\nlacus enim\r\nporta\r\nneque, in\r\nposuere dui\r\nex a sapien.\r\nNam lacus\r\nnibh,\r\nposuere sed\r\nelit eget,\r\ncondimentu\r\nm facilisis\r\nligula. Cras\r\nconsectetur\r\nlacus\r\nullamcorper\r\nvelit aliquet\r\nbibendum\r\neget vel\r\nnulla.\r\nAenean\r\nvarius ac\r\nerat quis\r\nullamcorper\r\n. Donec\r\nlaoreet arcu\r\na lorem\r\nvolutpat\r\nfaucibus.\r\nVivamus\r\nvehicula leo\r\nut erat\r\nluctus\r\nscelerisque.\r\nMorbi\r\nposuere ex\r\net magna\r\negestas\r\nfacilisis.\r\nFusce\r\nscelerisque\r\nvolutpat\r\nerat\r\nbibendum\r\nhendrerit.\r\nNam blandit\r\nmi ut metus\r\npulvinar, vel\r\ntempus\r\nlacus\r\neuismod.\r\nQuisque\r\nimperdiet\r\nsit amet\r\nsapien sed\r\nultricies.\r\nPhasellus\r\nsodales,\r\nipsum vitae\r\ntincidunt\r\nfacilisis,\r\nnulla ligula\r\nfaucibus\r\nfelis, eget\r\nvehicula\r\nante lacus\r\neu lorem.\r\nInteger\r\ncongue\r\ndiam ac\r\nviverra\r\ntristique.\r\nCurabitur\r\ntristique\r\ndolor quis\r\nquam\r\npretium, et\r\nscelerisque\r\nquam\r\ndictum.\r\nMaecenas\r\nvitae\r\nsodales\r\nligula.\r\nPellentesqu\r\ne maximus\r\ndiam vel\r\nporta\r\nconvallis. Ut\r\naliquam\r\neros quis\r\nporta\r\npellentesqu\r\ne. Fusce in\r\nex ut mi\r\negestas\r\ncursus.\r\nAliquam\r\nerat\r\nvolutpat.\r\nCras laoreet\r\ncondimentu\r\nm laoreet.\r\nSed eget\r\nfacilisis\r\ntellus.\r\nMorbi\r\nviverra odio\r\nsed odio\r\nplacerat\r\nmollis. Duis\r\nturpis\r\nmetus,\r\ndignissim\r\nvarius urna\r\nquis, viverra\r\ndignissim\r\ndui.\r\nVivamus\r\nviverra at\r\nnisi quis\r\nconvallis.\r\nSuspendiss\r\ne fringilla\r\nrisus et ante\r\nsollicitudin,\r\nsed eleifend\r\nsem\r\nplacerat.\r\nProin\r\npretium\r\nblandit\r\narcu, eget\r\nrhoncus\r\nrisus\r\nhendrerit at.\r\nInterdum et\r\nmalesuada\r\nfames ac\r\nante ipsum\r\nprimis in\r\nfaucibus.\r\nPhasellus\r\nvulputate\r\nefficitur\r\nmaximus.\r\nCras blandit\r\nnulla eu nisi\r\nauctor\r\ntempus.\r\nSed pretium\r\nlacus ac\r\nmagna\r\nvestibulum,\r\naliquam\r\nfaucibus\r\norci luctus.\r\nMauris enim\r\nlorem,\r\nvarius ut\r\nante quis,\r\nvarius\r\nviverra\r\nlectus.\r\nFusce\r\nblandit nibh\r\nvel feugiat\r\nefficitur.\r\nDonec\r\nmaximus id\r\njusto ac\r\nmollis.\r\nVestibulum\r\nante ipsum\r\nprimis in\r\nfaucibus\r\norci luctus\r\net ultrices\r\nposuere\r\ncubilia\r\ncurae; Nulla\r\nplacerat\r\nlectus et\r\npurus\r\ndictum, id\r\ncongue nisi\r\neuismod.\r\nMaecenas\r\neuismod\r\nfermentum\r\ndiam, sit\r\namet\r\ngravida\r\nmagna\r\nsuscipit a.\r\nQuisque\r\nconsectetur\r\narcu eu\r\nnunc\r\nsodales\r\nscelerisque.\r\nNulla non\r\ntincidunt\r\nnulla.\r\nPellentesqu\r\ne ut tortor\r\nvel enim\r\nconvallis\r\nmalesuada.\r\nAliquam\r\nultricies\r\nbibendum\r\nultrices.\r\nMauris\r\nrutrum ac\r\nnisl vel\r\nluctus.\r\nDonec quis\r\nnibh vitae\r\norci ultricies\r\ngravida.\r\nAliquam\r\nvitae velit\r\nporttitor\r\nlorem\r\nbibendum\r\nfringilla\r\nvolutpat a\r\neros.\r\nCurabitur at\r\ncommodo\r\ntortor. Etiam\r\nultricies,\r\nneque et\r\niaculis\r\neuismod,\r\ndiam ligula\r\nluctus mi,\r\nvitae\r\nlobortis felis\r\nlorem eu\r\nnulla. Sed a\r\nsemper ex.\r\nInterdum et\r\nmalesuada\r\nfames ac\r\nante ipsum\r\nprimis in\r\nfaucibus.\r\nNulla\r\nmauris elit,\r\npulvinar ac\r\ntortor et,\r\nluctus\r\nhendrerit\r\nnisl. In\r\negestas\r\nauctor urna\r\nvitae\r\nlaoreet.\r\nPraesent\r\nbibendum\r\negestas\r\nconvallis.\r\nProin non\r\nsuscipit\r\ntellus.\r\nNullam at\r\nnibh in urna\r\nlaoreet\r\nsodales non\r\nvel tellus.\r\nDonec in\r\nenim dui.\r\nPhasellus\r\nquis quam\r\ntincidunt,\r\npellentesqu\r\ne lorem ac,\r\nscelerisque\r\nneque.\r\nInteger nec\r\ntempus\r\nurna. Donec\r\nelit massa,\r\neleifend eu\r\nsapien sit\r\namet,\r\nmollis\r\npellentesqu\r\ne est.\r\nNullam\r\ntristique\r\ntellus\r\niaculis arcu\r\nconsectetur\r\npretium.\r\nSed\r\nvenenatis\r\nconvallis\r\nscelerisque.\r\nSuspendiss\r\ne varius\r\nurna sit\r\namet purus\r\naccumsan,\r\nid ultricies\r\nerat\r\nefficitur.\r\nCras non\r\nipsum eget\r\nnulla\r\nefficitur\r\ncommodo\r\nsit amet non\r\nlacus. Proin\r\nviverra enim\r\nsit amet\r\nenim\r\ntempus\r\nullamcorper\r\n. Class\r\naptent taciti\r\nsociosqu ad\r\nlitora\r\ntorquent per\r\nconubia\r\nnostra, per\r\ninceptos\r\nhimenaeos.\r\nDuis ac\r\nmassa\r\ninterdum,\r\ngravida ex\r\negestas,\r\nfinibus\r\npurus. Nunc\r\nconsectetur\r\ncommodo\r\nlacus, ac\r\nconvallis\r\nquam\r\nlobortis eu.\r\nSed\r\nconvallis\r\ntempor\r\ncommodo.\r\nNulla sed\r\nconvallis\r\nmauris.\r\nDonec\r\nvenenatis\r\nnisi est, ac\r\nullamcorper\r\nmi pretium\r\nquis. Donec\r\nvitae eros at\r\nipsum\r\ninterdum\r\nscelerisque\r\nnec vitae\r\nnisi. Sed\r\nvestibulum\r\nerat ac\r\nbibendum\r\ndapibus.\r\nMorbi nec\r\nelit id quam\r\ntristique\r\ncursus id\r\nsed sem.\r\nPraesent\r\nnon ante\r\nenim.\r\nPellentesqu\r\ne habitant\r\nmorbi\r\ntristique\r\nsenectus et\r\nnetus et\r\nmalesuada\r\nfames ac\r\nturpis\r\negestas.\r\nPraesent\r\nnon mauris\r\ndui.\r\nAliquam\r\nrhoncus\r\nmattis ante\r\nsed\r\nvenenatis.\r\nVivamus\r\nvehicula\r\nsed sapien\r\nsed dictum.\r\nIn aliquet,\r\nurna\r\nefficitur\r\ntincidunt\r\nlobortis,\r\nnibh justo\r\ntristique\r\npurus, sed\r\nvolutpat\r\nrisus magna\r\net\r\nlibero.Susp\r\nendisse\r\nlectus justo,\r\nvarius eget\r\narcu et,\r\nsemper\r\nlaoreet erat.\r\nQuisque\r\neget lacus\r\nornare,\r\npellentesqu\r\ne erat sit\r\namet,\r\nvulputate\r\nfelis. Duis\r\nluctus,\r\nmassa a\r\npellentesqu\r\ne mollis,\r\nmassa elit\r\nconvallis\r\nmi, vel\r\nbibendum\r\nex ex eu\r\npurus.\r\nSuspendiss\r\ne vel\r\nfermentum\r\nurna, ac\r\ncommodo\r\nenim.\r\nMauris\r\ntincidunt\r\ncursus elit,\r\na volutpat\r\nlibero\r\ncommodo\r\net. Etiam\r\ndapibus\r\nlibero\r\nvenenatis\r\ntellus\r\nlobortis, vel\r\nlacinia elit\r\nfaucibus.\r\nMaecenas\r\nsemper sed\r\nquam quis\r\nfinibus.\r\nInteger\r\nefficitur,\r\nlibero\r\nimperdiet\r\nsollicitudin\r\ncommodo,\r\nelit arcu\r\nvulputate\r\nest, eget\r\nfinibus mi\r\nurna sit\r\namet\r\nmagna.\r\nCras\r\nullamcorper\r\nconsequat\r\nornare.\r\nFusce\r\nconvallis\r\nnunc vel\r\nrisus\r\ncursus, at\r\nmaximus\r\nligula\r\ncursus.\r\nPellentesqu\r\ne vulputate\r\nrisus libero,\r\neget cursus\r\nnibh\r\nsodales\r\nsed. Donec\r\naccumsan\r\nsem et\r\nmassa\r\nsemper, id\r\ndignissim\r\nvelit\r\nvehicula.Cr\r\nas cursus\r\nipsum ac\r\nerat\r\nvehicula,\r\nnec iaculis\r\npurus\r\ndictum.\r\nQuisque\r\nlacinia elit\r\nvitae leo\r\ndictum, vel\r\ndignissim\r\nvelit\r\ndapibus.\r\nAenean sem\r\nnisi,\r\nfaucibus\r\ninterdum\r\njusto eu,\r\neuismod\r\nporttitor ex.\r\nMorbi et\r\nlectus\r\nlectus. Duis\r\nneque felis,\r\nsuscipit at\r\nscelerisque\r\neu,\r\nscelerisque\r\nid orci.\r\nCurabitur et\r\nplacerat\r\nipsum.\r\nProin\r\ngravida\r\nsapien nisl,\r\net varius\r\nipsum\r\nmollis nec.\r\nQuisque\r\ndignissim\r\nconsectetur\r\nfeugiat.\r\nAenean\r\neros purus,\r\nlaoreet\r\ninterdum\r\nrutrum at,\r\naliquet sit\r\namet\r\nlectus.\r\nDonec\r\ngravida\r\nlorem ut\r\ntincidunt\r\nlaoreet.\r\nDonec\r\nconsequat\r\nviverra\r\nligula, in\r\naccumsan\r\nmi\r\nbibendum\r\nscelerisque.\r\nQuisque ac\r\nrisus justo.\r\nMorbi\r\nmagna\r\narcu,\r\negestas nec\r\nluctus\r\ncommodo,\r\ncursus eget\r\nnunc.\r\nVivamus\r\neuismod\r\nlorem ex, et\r\nmaximus\r\nfelis\r\nhendrerit\r\neget.\r\nNullam\r\nullamcorper\r\neuismod\r\nligula, et\r\niaculis\r\nligula\r\nultricies a.\r\nFusce\r\naliquam,\r\nenim vel\r\nfermentum\r\nultrices, elit\r\nquam\r\nsemper\r\nerat, vitae\r\nsemper velit\r\naugue non\r\nmagna.Quis\r\nque\r\nmaximus\r\nsemper\r\narcu, id\r\npellentesqu\r\ne est\r\ntempus a.\r\nPhasellus\r\nlacus elit,\r\nauctor sit\r\namet lacinia\r\na, dapibus\r\nvitae velit.\r\nPhasellus ut\r\npharetra\r\njusto, ut\r\nultricies\r\nerat. Sed\r\nmolestie\r\nsapien vel\r\ninterdum\r\nlobortis.\r\nNulla\r\nfacilisi.\r\nVestibulum\r\nante ipsum\r\nprimis in\r\nfaucibus\r\norci luctus\r\net ultrices\r\nposuere\r\ncubilia\r\ncurae; Nulla\r\nnec mauris\r\nquis nisi\r\nvulputate\r\ngravida quis\r\nnec\r\nvelit.Nam et\r\ncongue\r\nipsum.\r\nNulla vel elit\r\nnon dolor\r\nmollis\r\naliquet vel\r\nat magna.\r\nPellentesqu\r\ne nec\r\nfacilisis elit.\r\nIn vulputate\r\nquis sem\r\nporta\r\nsuscipit.\r\nNullam sed\r\nex ornare\r\nnibh\r\nsuscipit\r\nmattis quis\r\nnon lacus.\r\nMauris vel\r\nex urna.\r\nVivamus\r\nultricies\r\nsapien sit\r\namet sapien\r\nvehicula\r\ngravida.\r\nDonec\r\nfeugiat\r\nvolutpat\r\nquam.\r\nVestibulum\r\nauctor\r\ndictum nisl,\r\nid hendrerit\r\nmetus\r\nullamcorper\r\nsed. Nulla\r\nmaximus\r\nlacus vel\r\nmollis\r\nmaximus.\r\nNulla\r\nlaoreet\r\nplacerat\r\nquam eu\r\nviverra.\r\nEtiam\r\nfeugiat\r\naccumsan\r\nnisl a\r\ncondimentu\r\nm. Sed\r\nultricies\r\nante ante,\r\nac auctor\r\nligula\r\ngravida nec.\r\nPraesent a\r\nneque\r\ndignissim,\r\nsagittis felis\r\nsit amet,\r\ncondimentu\r\nm turpis.\r\nFusce at leo\r\nvel est\r\nblandit\r\nmalesuada.\r\nPellentesqu\r\ne et neque\r\nnon metus\r\npellentesqu\r\ne imperdiet.\r\nPraesent\r\npellentesqu\r\ne lacinia\r\nlorem, et\r\ntristique\r\ntellus\r\nefficitur id.\r\nSuspendiss\r\ne aliquet\r\nultricies\r\njusto vitae\r\ninterdum.\r\nCras\r\ntristique\r\nviverra\r\nquam, eget\r\ngravida mi\r\nfermentum\r\nimperdiet.\r\nSed\r\nimperdiet\r\nvitae purus\r\nut volutpat.\r\nNulla\r\nlacinia elit\r\nin\r\nfermentum\r\nconsectetur\r\n. Phasellus\r\ncommodo\r\nut nisl sit\r\namet\r\nsagittis.\r\nDuis ac\r\nornare orci.\r\nVivamus vel\r\nenim\r\nposuere,\r\npharetra ex\r\nvel,\r\nelementum\r\nest.\r\nVestibulum\r\ncommodo\r\nluctus\r\nmetus eget\r\nmaximus.\r\nSuspendiss\r\ne a nulla a\r\nodio\r\neleifend\r\nfaucibus.\r\nSuspendiss\r\ne semper\r\nlacus non\r\nporttitor\r\naliquet.\r\nCras ac\r\nscelerisque\r\nmagna, et\r\npulvinar\r\njusto.\r\nInteger\r\ncursus\r\npulvinar\r\nfringilla.\r\nMauris\r\nimperdiet\r\nnibh sit\r\namet\r\ntempor\r\nlaoreet.\r\nMorbi\r\ntincidunt\r\ntortor ex, sit\r\namet\r\nmaximus\r\npurus\r\ntristique\r\nquis.\r\nQuisque\r\nsed\r\nhendrerit\r\nvelit. Mauris\r\nmattis nibh\r\nut eros\r\nluctus, eget\r\nmattis\r\nmassa\r\nauctor.\r\nPhasellus\r\neu neque at\r\naugue\r\ngravida\r\nsagittis nec\r\nnon tortor.\r\nEtiam\r\nporttitor\r\nsem\r\nsodales mi\r\nullamcorper\r\ngravida. In\r\nin dictum\r\norci. In vitae\r\nvestibulum\r\nquam. Cras\r\naugue eros,\r\ntincidunt ac\r\nelit posuere,\r\nsollicitudin\r\nefficitur\r\nlectus.\r\nPraesent\r\nquis\r\nsodales\r\nnisl. Proin\r\nsit amet\r\nmolestie\r\nest. In\r\ncommodo\r\nmauris vel\r\nmauris\r\nefficitur,\r\nnec mollis\r\nmauris\r\nsagittis.\r\nCras ligula\r\nnibh,\r\negestas sit\r\namet eros\r\nin, lacinia\r\ntristique\r\nmagna.\r\nCras risus\r\nlibero,\r\nlacinia eget\r\nlibero vitae,\r\nmaximus\r\naliquet\r\nnibh. Mauris\r\nid sodales\r\npurus, vitae\r\ndictum\r\nlectus. Cras\r\nconsectetur\r\nligula velit,\r\ntempus\r\npulvinar\r\nlacus\r\nporttitor\r\nvitae.\r\nPhasellus\r\neget tellus\r\nipsum.\r\nDonec\r\ninterdum\r\nlaoreet elit\r\nnon\r\nvestibulum.\r\nCras sed\r\nurna\r\nullamcorper\r\n, aliquam\r\nerat eget,\r\nporta orci.\r\nVestibulum\r\neget congue\r\nnulla. Sed\r\nsem tortor,\r\neuismod at\r\nrutrum id,\r\nsagittis a\r\nnunc. Duis\r\nin nibh\r\nfacilisis,\r\ndignissim\r\npurus ut,\r\nhendrerit\r\nmagna. Sed\r\nsemper\r\nligula id\r\nmassa\r\nelementum,\r\nnon\r\nmalesuada\r\nvelit\r\negestas.\r\nNullam\r\ndictum, mi\r\nnec\r\neuismod\r\nsagittis,\r\nligula leo\r\nullamcorper\r\ndolor, quis\r\nfaucibus\r\nodio metus\r\neget magna.\r\nUt gravida\r\nmetus non\r\nmetus\r\nbibendum\r\nbibendum.\r\nIn sagittis\r\neleifend\r\naliquet.\r\nInterdum et\r\nmalesuada\r\nfames ac\r\nante ipsum\r\nprimis in\r\nfaucibus.\r\nNam mollis\r\nsagittis\r\nfelis, in\r\nfaucibus\r\ntortor\r\npretium vel.\r\nNam nec\r\nenim\r\nmetus.\r\nDonec in\r\naugue arcu.\r\nProin non\r\nlobortis\r\npurus, sit\r\namet lacinia\r\nelit.\r\nSuspendiss\r\ne quis eros\r\ncondimentu\r\nm, blandit\r\njusto sit\r\namet,\r\nlobortis nisl.\r\nSuspendiss\r\ne maximus\r\nmassa sed\r\nurna tempor\r\nornare.\r\nNunc\r\nmalesuada\r\npurus odio,\r\neu luctus\r\nlectus\r\nauctor nec.\r\nMorbi\r\nauctor\r\npellentesqu\r\ne auctor.\r\nSed\r\nullamcorper\r\n, ex vitae\r\naliquam\r\nvulputate,\r\nest diam\r\nfeugiat mi,\r\nid porttitor\r\nlectus orci\r\nac leo.\r\nDonec sit\r\namet velit\r\npulvinar,\r\nvenenatis\r\nturpis ut,\r\ninterdum\r\nligula.\r\nInterdum et\r\nmalesuada\r\nfames ac\r\nante ipsum\r\nprimis in\r\nfaucibus.\r\nVestibulum\r\neu lacus\r\nurna.\r\nMaecenas\r\nsem nulla,\r\naccumsan\r\neu ultricies\r\nsed, tempor\r\nvel magna.\r\nCras aliquet\r\nsollicitudin\r\nsapien ac\r\npulvinar.\r\nPraesent ac\r\nsodales mi.\r\nInteger vitae\r\nmauris\r\nmassa.\r\nMaecenas\r\niaculis orci\r\net faucibus\r\ninterdum.\r\nNunc nec\r\nmaximus\r\nfelis, sed\r\nfinibus\r\nquam.\r\nPellentesqu\r\ne felis\r\nmassa,\r\nvestibulum\r\nin tellus\r\nvitae,\r\ncongue\r\ntincidunt\r\njusto. Nunc\r\nvitae enim\r\nmalesuada,\r\nbibendum\r\nante nec,\r\nvarius\r\ntellus.\r\nPraesent\r\nvitae nisi id\r\nquam\r\nauctor\r\nlacinia at\r\nnon quam.\r\nNam nec\r\nligula sit\r\namet felis\r\nauctor\r\nsagittis.\r\nNunc in\r\nrisus eu\r\nurna varius\r\nlaoreet quis\r\nsit amet\r\nfelis. Morbi\r\nvarius\r\ntempor orci,\r\neu\r\nvestibulum\r\nnunc\r\nvestibulum\r\nac. Nunc\r\nvehicula\r\nvelit\r\neleifend\r\nconsequat\r\nporta.\r\nSuspendiss\r\ne maximus\r\ndapibus\r\norci, in\r\nvulputate\r\nmassa\r\npretium ac.\r\nQuisque\r\nmalesuada\r\naliquet\r\naliquet.";
+
+ var savedStrings = SavedComparisonString.Split(new string[] { "\r\n" }, StringSplitOptions.None);
+
+ List faultyStrings = new();
+ List excpectedStrings = new();
+ List indiciesOfDifferingString = new();
+
+ for (int i = 0; i < savedStrings.Count(); i++)
+ {
+ if(savedStrings[i] != wrappedStrings[i])
+ {
+ indiciesOfDifferingString.Add(i);
+ faultyStrings.Add(wrappedStrings[i]);
+ excpectedStrings.Add(savedStrings[i]);
+ }
+ }
+
+ if(indiciesOfDifferingString.Count != 0)
+ {
+ //The start of indicies diverging
+ Assert.IsNull(indiciesOfDifferingString[0]);
+ Assert.AreEqual(faultyStrings[0], excpectedStrings[0]);
+ }
+
+ Assert.AreEqual(0, faultyStrings.Count);
+ }
+
+ [TestMethod]
+ public void LoremIpsum20ParagraphsMultipleFragments()
+ {
+ var Lorem20Str = "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nulla pulvinar interdum imperdiet. Praesent ut auctor urna. Phasellus sollicitudin quam vitae est convallis, eu mattis lorem efficitur. Mauris nulla libero, tincidunt id ipsum non, lobortis tristique mauris. Donec ut enim sed enim fermentum molestie vel quis odio. Morbi a fermentum massa, sit amet ultrices est. Aenean ante mi, fermentum nec rhoncus et, vulputate vel sapien. Donec tempus, leo quis luctus rhoncus, augue odio pharetra libero, ac blandit urna turpis sed diam. Vivamus augue purus, eleifend et justo facilisis, imperdiet rhoncus sem. Quisque accumsan pellentesque elit, eget finibus massa accumsan in. Fusce eu accumsan enim. Cras pulvinar enim vel tellus lacinia, consectetur euismod tortor consectetur. Praesent tincidunt pretium eros, ac auctor magna luctus sed. Ut porta lectus quam, non ornare mauris lacinia sit amet. Nullam egestas dolor quis magna porttitor, ac iaculis nisi hendrerit. Proin at mollis lacus, in porttitor nunc. Aliquam erat volutpat. Sed vel egestas risus, at aliquam arcu. Vestibulum quis lobortis nulla. Etiam pellentesque auctor nulla, eget tincidunt felis rhoncus id. Sed metus ante, efficitur id dui eu, fermentum mollis odio. Phasellus ullamcorper iaculis augue vel consequat. Etiam fringilla euismod interdum. Ut molestie massa id fringilla lobortis. Vestibulum malesuada, ante vel mattis ultrices, sem ante molestie augue, non tristique dui mi non nibh. Maecenas dictum, sem eget convallis rhoncus, lacus enim porta neque, in posuere dui ex a sapien. Nam lacus nibh, posuere sed elit eget, condimentum facilisis ligula. Cras consectetur lacus ullamcorper velit aliquet bibendum eget vel nulla. Aenean varius ac erat quis ullamcorper. Donec laoreet arcu a lorem volutpat faucibus. Vivamus vehicula leo ut erat luctus scelerisque. Morbi posuere ex et magna egestas facilisis. Fusce scelerisque volutpat erat bibendum hendrerit. Nam blandit mi ut metus pulvinar, vel tempus lacus euismod. Quisque imperdiet sit amet sapien sed ultricies. Phasellus sodales, ipsum vitae tincidunt facilisis, nulla ligula faucibus felis, eget vehicula ante lacus eu lorem. Integer congue diam ac viverra tristique. Curabitur tristique dolor quis quam pretium, et scelerisque quam dictum. Maecenas vitae sodales ligula. Pellentesque maximus diam vel porta convallis. Ut aliquam eros quis porta pellentesque. Fusce in ex ut mi egestas cursus. Aliquam erat volutpat. Cras laoreet condimentum laoreet. Sed eget facilisis tellus. Morbi viverra odio sed odio placerat mollis. Duis turpis metus, dignissim varius urna quis, viverra dignissim dui. Vivamus viverra at nisi quis convallis. Suspendisse fringilla risus et ante sollicitudin, sed eleifend sem placerat. Proin pretium blandit arcu, eget rhoncus risus hendrerit at. Interdum et malesuada fames ac ante ipsum primis in faucibus. Phasellus vulputate efficitur maximus. Cras blandit nulla eu nisi auctor tempus. Sed pretium lacus ac magna vestibulum, aliquam faucibus orci luctus. Mauris enim lorem, varius ut ante quis, varius viverra lectus. Fusce blandit nibh vel feugiat efficitur. Donec maximus id justo ac mollis. Vestibulum ante ipsum primis in faucibus orci luctus et ultrices posuere cubilia curae; Nulla placerat lectus et purus dictum, id congue nisi euismod. Maecenas euismod fermentum diam, sit amet gravida magna suscipit a. Quisque consectetur arcu eu nunc sodales scelerisque. Nulla non tincidunt nulla. Pellentesque ut tortor vel enim convallis malesuada. Aliquam ultricies bibendum ultrices. Mauris rutrum ac nisl vel luctus. Donec quis nibh vitae orci ultricies gravida. Aliquam vitae velit porttitor lorem bibendum fringilla volutpat a eros. Curabitur at commodo tortor. Etiam ultricies, neque et iaculis euismod, diam ligula luctus mi, vitae lobortis felis lorem eu nulla. Sed a semper ex. Interdum et malesuada fames ac ante ipsum primis in faucibus. Nulla mauris elit, pulvinar ac tortor et, luctus hendrerit nisl. In egestas auctor urna vitae laoreet. Praesent bibendum egestas convallis. Proin non suscipit tellus. Nullam at nibh in urna laoreet sodales non vel tellus. Donec in enim dui. Phasellus quis quam tincidunt, pellentesque lorem ac, scelerisque neque. Integer nec tempus urna. Donec elit massa, eleifend eu sapien sit amet, mollis pellentesque est. Nullam tristique tellus iaculis arcu consectetur pretium. Sed venenatis convallis scelerisque. Suspendisse varius urna sit amet purus accumsan, id ultricies erat efficitur. Cras non ipsum eget nulla efficitur commodo sit amet non lacus. Proin viverra enim sit amet enim tempus ullamcorper. Class aptent taciti sociosqu ad litora torquent per conubia nostra, per inceptos himenaeos. Duis ac massa interdum, gravida ex egestas, finibus purus. Nunc consectetur commodo lacus, ac convallis quam lobortis eu. Sed convallis tempor commodo. Nulla sed convallis mauris. Donec venenatis nisi est, ac ullamcorper mi pretium quis. Donec vitae eros at ipsum interdum scelerisque nec vitae nisi. Sed vestibulum erat ac bibendum dapibus. Morbi nec elit id quam tristique cursus id sed sem. Praesent non ante enim. Pellentesque habitant morbi tristique senectus et netus et malesuada fames ac turpis egestas. Praesent non mauris dui. Aliquam rhoncus mattis ante sed venenatis. Vivamus vehicula sed sapien sed dictum. In aliquet, urna efficitur tincidunt lobortis, nibh justo tristique purus, sed volutpat risus magna et libero.Suspendisse lectus justo, varius eget arcu et, semper laoreet erat. Quisque eget lacus ornare, pellentesque erat sit amet, vulputate felis. Duis luctus, massa a pellentesque mollis, massa elit convallis mi, vel bibendum ex ex eu purus. Suspendisse vel fermentum urna, ac commodo enim. Mauris tincidunt cursus elit, a volutpat libero commodo et. Etiam dapibus libero venenatis tellus lobortis, vel lacinia elit faucibus. Maecenas semper sed quam quis finibus. Integer efficitur, libero imperdiet sollicitudin commodo, elit arcu vulputate est, eget finibus mi urna sit amet magna. Cras ullamcorper consequat ornare. Fusce convallis nunc vel risus cursus, at maximus ligula cursus. Pellentesque vulputate risus libero, eget cursus nibh sodales sed. Donec accumsan sem et massa semper, id dignissim velit vehicula.Cras cursus ipsum ac erat vehicula, nec iaculis purus dictum. Quisque lacinia elit vitae leo dictum, vel dignissim velit dapibus. Aenean sem nisi, faucibus interdum justo eu, euismod porttitor ex. Morbi et lectus lectus. Duis neque felis, suscipit at scelerisque eu, scelerisque id orci. Curabitur et placerat ipsum. Proin gravida sapien nisl, et varius ipsum mollis nec. Quisque dignissim consectetur feugiat. Aenean eros purus, laoreet interdum rutrum at, aliquet sit amet lectus. Donec gravida lorem ut tincidunt laoreet. Donec consequat viverra ligula, in accumsan mi bibendum scelerisque. Quisque ac risus justo. Morbi magna arcu, egestas nec luctus commodo, cursus eget nunc. Vivamus euismod lorem ex, et maximus felis hendrerit eget. Nullam ullamcorper euismod ligula, et iaculis ligula ultricies a. Fusce aliquam, enim vel fermentum ultrices, elit quam semper erat, vitae semper velit augue non magna.Quisque maximus semper arcu, id pellentesque est tempus a. Phasellus lacus elit, auctor sit amet lacinia a, dapibus vitae velit. Phasellus ut pharetra justo, ut ultricies erat. Sed molestie sapien vel interdum lobortis. Nulla facilisi. Vestibulum ante ipsum primis in faucibus orci luctus et ultrices posuere cubilia curae; Nulla nec mauris quis nisi vulputate gravida quis nec velit.Nam et congue ipsum. Nulla vel elit non dolor mollis aliquet vel at magna. Pellentesque nec facilisis elit. In vulputate quis sem porta suscipit. Nullam sed ex ornare nibh suscipit mattis quis non lacus. Mauris vel ex urna. Vivamus ultricies sapien sit amet sapien vehicula gravida. Donec feugiat volutpat quam. Vestibulum auctor dictum nisl, id hendrerit metus ullamcorper sed. Nulla maximus lacus vel mollis maximus. Nulla laoreet placerat quam eu viverra. Etiam feugiat accumsan nisl a condimentum. Sed ultricies ante ante, ac auctor ligula gravida nec. Praesent a neque dignissim, sagittis felis sit amet, condimentum turpis. Fusce at leo vel est blandit malesuada. Pellentesque et neque non metus pellentesque imperdiet. Praesent pellentesque lacinia lorem, et tristique tellus efficitur id. Suspendisse aliquet ultricies justo vitae interdum. Cras tristique viverra quam, eget gravida mi fermentum imperdiet. Sed imperdiet vitae purus ut volutpat. Nulla lacinia elit in fermentum consectetur. Phasellus commodo ut nisl sit amet sagittis. Duis ac ornare orci. Vivamus vel enim posuere, pharetra ex vel, elementum est. Vestibulum commodo luctus metus eget maximus. Suspendisse a nulla a odio eleifend faucibus. Suspendisse semper lacus non porttitor aliquet. Cras ac scelerisque magna, et pulvinar justo. Integer cursus pulvinar fringilla. Mauris imperdiet nibh sit amet tempor laoreet. Morbi tincidunt tortor ex, sit amet maximus purus tristique quis. Quisque sed hendrerit velit. Mauris mattis nibh ut eros luctus, eget mattis massa auctor. Phasellus eu neque at augue gravida sagittis nec non tortor. Etiam porttitor sem sodales mi ullamcorper gravida. In in dictum orci. In vitae vestibulum quam. Cras augue eros, tincidunt ac elit posuere, sollicitudin efficitur lectus. Praesent quis sodales nisl. Proin sit amet molestie est. In commodo mauris vel mauris efficitur, nec mollis mauris sagittis. Cras ligula nibh, egestas sit amet eros in, lacinia tristique magna. Cras risus libero, lacinia eget libero vitae, maximus aliquet nibh. Mauris id sodales purus, vitae dictum lectus. Cras consectetur ligula velit, tempus pulvinar lacus porttitor vitae. Phasellus eget tellus ipsum. Donec interdum laoreet elit non vestibulum. Cras sed urna ullamcorper, aliquam erat eget, porta orci. Vestibulum eget congue nulla. Sed sem tortor, euismod at rutrum id, sagittis a nunc. Duis in nibh facilisis, dignissim purus ut, hendrerit magna. Sed semper ligula id massa elementum, non malesuada velit egestas. Nullam dictum, mi nec euismod sagittis, ligula leo ullamcorper dolor, quis faucibus odio metus eget magna. Ut gravida metus non metus bibendum bibendum. In sagittis eleifend aliquet. Interdum et malesuada fames ac ante ipsum primis in faucibus. Nam mollis sagittis felis, in faucibus tortor pretium vel. Nam nec enim metus. Donec in augue arcu. Proin non lobortis purus, sit amet lacinia elit. Suspendisse quis eros condimentum, blandit justo sit amet, lobortis nisl. Suspendisse maximus massa sed urna tempor ornare. Nunc malesuada purus odio, eu luctus lectus auctor nec. Morbi auctor pellentesque auctor. Sed ullamcorper, ex vitae aliquam vulputate, est diam feugiat mi, id porttitor lectus orci ac leo. Donec sit amet velit pulvinar, venenatis turpis ut, interdum ligula. Interdum et malesuada fames ac ante ipsum primis in faucibus. Vestibulum eu lacus urna. Maecenas sem nulla, accumsan eu ultricies sed, tempor vel magna. Cras aliquet sollicitudin sapien ac pulvinar. Praesent ac sodales mi. Integer vitae mauris massa. Maecenas iaculis orci et faucibus interdum. Nunc nec maximus felis, sed finibus quam. Pellentesque felis massa, vestibulum in tellus vitae, congue tincidunt justo. Nunc vitae enim malesuada, bibendum ante nec, varius tellus. Praesent vitae nisi id quam auctor lacinia at non quam. Nam nec ligula sit amet felis auctor sagittis. Nunc in risus eu urna varius laoreet quis sit amet felis. Morbi varius tempor orci, eu vestibulum nunc vestibulum ac. Nunc vehicula velit eleifend consequat porta. Suspendisse maximus dapibus orci, in vulputate massa pretium ac. Quisque malesuada aliquet aliquet.";
+ const string SavedComparisonString = "Lorem\r\nipsum dolor\r\nsit amet,\r\nconsectetur\r\nadipiscing\r\nelit. Nulla\r\npulvinar\r\ninterdum\r\nimperdiet.\r\nPraesent ut\r\nauctor urna.\r\nPhasellus\r\nsollicitudin\r\nquam vitae\r\nest\r\nconvallis,\r\neu mattis\r\nlorem\r\nefficitur.\r\nMauris nulla\r\nlibero,\r\ntincidunt id\r\nipsum non,\r\nlobortis\r\ntristique\r\nmauris.\r\nDonec ut\r\nenim sed\r\nenim\r\nfermentum\r\nmolestie vel\r\nquis odio.\r\nMorbi a\r\nfermentum\r\nmassa, sit\r\namet\r\nultrices est.\r\nAenean\r\nante mi,\r\nfermentum\r\nnec rhoncus\r\net,\r\nvulputate\r\nvel sapien.\r\nDonec\r\ntempus, leo\r\nquis luctus\r\nrhoncus,\r\naugue odio\r\npharetra\r\nlibero, ac\r\nblandit urna\r\nturpis sed\r\ndiam.\r\nVivamus\r\naugue\r\npurus,\r\neleifend et\r\njusto\r\nfacilisis,\r\nimperdiet\r\nrhoncus\r\nsem.\r\nQuisque\r\naccumsan\r\npellentesqu\r\ne elit, eget\r\nfinibus\r\nmassa\r\naccumsan\r\nin. Fusce eu\r\naccumsan\r\nenim. Cras\r\npulvinar\r\nenim vel\r\ntellus\r\nlacinia,\r\nconsectetur\r\neuismod\r\ntortor\r\nconsectetur\r\n. Praesent\r\ntincidunt\r\npretium\r\neros, ac\r\nauctor\r\nmagna\r\nluctus sed.\r\nUt porta\r\nlectus\r\nquam, non\r\nornare\r\nmauris\r\nlacinia sit\r\namet.\r\nNullam\r\negestas\r\ndolor quis\r\nmagna\r\nporttitor, ac\r\niaculis nisi\r\nhendrerit.\r\nProin at\r\nmollis\r\nlacus, in\r\nporttitor\r\nnunc.\r\nAliquam\r\nerat\r\nvolutpat.\r\nSed vel\r\negestas\r\nrisus, at\r\naliquam\r\narcu.\r\nVestibulum\r\nquis\r\nlobortis\r\nnulla. Etiam\r\npellentesqu\r\ne auctor\r\nnulla, eget\r\ntincidunt\r\nfelis\r\nrhoncus id.\r\nSed metus\r\nante,\r\nefficitur id\r\ndui eu,\r\nfermentum\r\nmollis odio.\r\nPhasellus\r\nullamcorper\r\niaculis\r\naugue vel\r\nconsequat.\r\nEtiam\r\nfringilla\r\neuismod\r\ninterdum.\r\nUt molestie\r\nmassa id\r\nfringilla\r\nlobortis.\r\nVestibulum\r\nmalesuada,\r\nante vel\r\nmattis\r\nultrices,\r\nsem ante\r\nmolestie\r\naugue, non\r\ntristique dui\r\nmi non nibh.\r\nMaecenas\r\ndictum,\r\nsem eget\r\nconvallis\r\nrhoncus,\r\nlacus enim\r\nporta\r\nneque, in\r\nposuere dui\r\nex a sapien.\r\nNam lacus\r\nnibh,\r\nposuere sed\r\nelit eget,\r\ncondimentu\r\nm facilisis\r\nligula. Cras\r\nconsectetur\r\nlacus\r\nullamcorper\r\nvelit aliquet\r\nbibendum\r\neget vel\r\nnulla.\r\nAenean\r\nvarius ac\r\nerat quis\r\nullamcorper\r\n. Donec\r\nlaoreet arcu\r\na lorem\r\nvolutpat\r\nfaucibus.\r\nVivamus\r\nvehicula leo\r\nut erat\r\nluctus\r\nscelerisque.\r\nMorbi\r\nposuere ex\r\net magna\r\negestas\r\nfacilisis.\r\nFusce\r\nscelerisque\r\nvolutpat\r\nerat\r\nbibendum\r\nhendrerit.\r\nNam blandit\r\nmi ut metus\r\npulvinar, vel\r\ntempus\r\nlacus\r\neuismod.\r\nQuisque\r\nimperdiet\r\nsit amet\r\nsapien sed\r\nultricies.\r\nPhasellus\r\nsodales,\r\nipsum vitae\r\ntincidunt\r\nfacilisis,\r\nnulla ligula\r\nfaucibus\r\nfelis, eget\r\nvehicula\r\nante lacus\r\neu lorem.\r\nInteger\r\ncongue\r\ndiam ac\r\nviverra\r\ntristique.\r\nCurabitur\r\ntristique\r\ndolor quis\r\nquam\r\npretium, et\r\nscelerisque\r\nquam\r\ndictum.\r\nMaecenas\r\nvitae\r\nsodales\r\nligula.\r\nPellentesqu\r\ne maximus\r\ndiam vel\r\nporta\r\nconvallis. Ut\r\naliquam\r\neros quis\r\nporta\r\npellentesqu\r\ne. Fusce in\r\nex ut mi\r\negestas\r\ncursus.\r\nAliquam\r\nerat\r\nvolutpat.\r\nCras laoreet\r\ncondimentu\r\nm laoreet.\r\nSed eget\r\nfacilisis\r\ntellus.\r\nMorbi\r\nviverra odio\r\nsed odio\r\nplacerat\r\nmollis. Duis\r\nturpis\r\nmetus,\r\ndignissim\r\nvarius urna\r\nquis, viverra\r\ndignissim\r\ndui.\r\nVivamus\r\nviverra at\r\nnisi quis\r\nconvallis.\r\nSuspendiss\r\ne fringilla\r\nrisus et ante\r\nsollicitudin,\r\nsed eleifend\r\nsem\r\nplacerat.\r\nProin\r\npretium\r\nblandit\r\narcu, eget\r\nrhoncus\r\nrisus\r\nhendrerit at.\r\nInterdum et\r\nmalesuada\r\nfames ac\r\nante ipsum\r\nprimis in\r\nfaucibus.\r\nPhasellus\r\nvulputate\r\nefficitur\r\nmaximus.\r\nCras blandit\r\nnulla eu nisi\r\nauctor\r\ntempus.\r\nSed pretium\r\nlacus ac\r\nmagna\r\nvestibulum,\r\naliquam\r\nfaucibus\r\norci luctus.\r\nMauris enim\r\nlorem,\r\nvarius ut\r\nante quis,\r\nvarius\r\nviverra\r\nlectus.\r\nFusce\r\nblandit nibh\r\nvel feugiat\r\nefficitur.\r\nDonec\r\nmaximus id\r\njusto ac\r\nmollis.\r\nVestibulum\r\nante ipsum\r\nprimis in\r\nfaucibus\r\norci luctus\r\net ultrices\r\nposuere\r\ncubilia\r\ncurae; Nulla\r\nplacerat\r\nlectus et\r\npurus\r\ndictum, id\r\ncongue nisi\r\neuismod.\r\nMaecenas\r\neuismod\r\nfermentum\r\ndiam, sit\r\namet\r\ngravida\r\nmagna\r\nsuscipit a.\r\nQuisque\r\nconsectetur\r\narcu eu\r\nnunc\r\nsodales\r\nscelerisque.\r\nNulla non\r\ntincidunt\r\nnulla.\r\nPellentesqu\r\ne ut tortor\r\nvel enim\r\nconvallis\r\nmalesuada.\r\nAliquam\r\nultricies\r\nbibendum\r\nultrices.\r\nMauris\r\nrutrum ac\r\nnisl vel\r\nluctus.\r\nDonec quis\r\nnibh vitae\r\norci ultricies\r\ngravida.\r\nAliquam\r\nvitae velit\r\nporttitor\r\nlorem\r\nbibendum\r\nfringilla\r\nvolutpat a\r\neros.\r\nCurabitur at\r\ncommodo\r\ntortor. Etiam\r\nultricies,\r\nneque et\r\niaculis\r\neuismod,\r\ndiam ligula\r\nluctus mi,\r\nvitae\r\nlobortis felis\r\nlorem eu\r\nnulla. Sed a\r\nsemper ex.\r\nInterdum et\r\nmalesuada\r\nfames ac\r\nante ipsum\r\nprimis in\r\nfaucibus.\r\nNulla\r\nmauris elit,\r\npulvinar ac\r\ntortor et,\r\nluctus\r\nhendrerit\r\nnisl. In\r\negestas\r\nauctor urna\r\nvitae\r\nlaoreet.\r\nPraesent\r\nbibendum\r\negestas\r\nconvallis.\r\nProin non\r\nsuscipit\r\ntellus.\r\nNullam at\r\nnibh in urna\r\nlaoreet\r\nsodales non\r\nvel tellus.\r\nDonec in\r\nenim dui.\r\nPhasellus\r\nquis quam\r\ntincidunt,\r\npellentesqu\r\ne lorem ac,\r\nscelerisque\r\nneque.\r\nInteger nec\r\ntempus\r\nurna. Donec\r\nelit massa,\r\neleifend eu\r\nsapien sit\r\namet,\r\nmollis\r\npellentesqu\r\ne est.\r\nNullam\r\ntristique\r\ntellus\r\niaculis arcu\r\nconsectetur\r\npretium.\r\nSed\r\nvenenatis\r\nconvallis\r\nscelerisque.\r\nSuspendiss\r\ne varius\r\nurna sit\r\namet purus\r\naccumsan,\r\nid ultricies\r\nerat\r\nefficitur.\r\nCras non\r\nipsum eget\r\nnulla\r\nefficitur\r\ncommodo\r\nsit amet non\r\nlacus. Proin\r\nviverra enim\r\nsit amet\r\nenim\r\ntempus\r\nullamcorper\r\n. Class\r\naptent taciti\r\nsociosqu ad\r\nlitora\r\ntorquent per\r\nconubia\r\nnostra, per\r\ninceptos\r\nhimenaeos.\r\nDuis ac\r\nmassa\r\ninterdum,\r\ngravida ex\r\negestas,\r\nfinibus\r\npurus. Nunc\r\nconsectetur\r\ncommodo\r\nlacus, ac\r\nconvallis\r\nquam\r\nlobortis eu.\r\nSed\r\nconvallis\r\ntempor\r\ncommodo.\r\nNulla sed\r\nconvallis\r\nmauris.\r\nDonec\r\nvenenatis\r\nnisi est, ac\r\nullamcorper\r\nmi pretium\r\nquis. Donec\r\nvitae eros at\r\nipsum\r\ninterdum\r\nscelerisque\r\nnec vitae\r\nnisi. Sed\r\nvestibulum\r\nerat ac\r\nbibendum\r\ndapibus.\r\nMorbi nec\r\nelit id quam\r\ntristique\r\ncursus id\r\nsed sem.\r\nPraesent\r\nnon ante\r\nenim.\r\nPellentesqu\r\ne habitant\r\nmorbi\r\ntristique\r\nsenectus et\r\nnetus et\r\nmalesuada\r\nfames ac\r\nturpis\r\negestas.\r\nPraesent\r\nnon mauris\r\ndui.\r\nAliquam\r\nrhoncus\r\nmattis ante\r\nsed\r\nvenenatis.\r\nVivamus\r\nvehicula\r\nsed sapien\r\nsed dictum.\r\nIn aliquet,\r\nurna\r\nefficitur\r\ntincidunt\r\nlobortis,\r\nnibh justo\r\ntristique\r\npurus, sed\r\nvolutpat\r\nrisus magna\r\net\r\nlibero.Susp\r\nendisse\r\nlectus justo,\r\nvarius eget\r\narcu et,\r\nsemper\r\nlaoreet erat.\r\nQuisque\r\neget lacus\r\nornare,\r\npellentesqu\r\ne erat sit\r\namet,\r\nvulputate\r\nfelis. Duis\r\nluctus,\r\nmassa a\r\npellentesqu\r\ne mollis,\r\nmassa elit\r\nconvallis\r\nmi, vel\r\nbibendum\r\nex ex eu\r\npurus.\r\nSuspendiss\r\ne vel\r\nfermentum\r\nurna, ac\r\ncommodo\r\nenim.\r\nMauris\r\ntincidunt\r\ncursus elit,\r\na volutpat\r\nlibero\r\ncommodo\r\net. Etiam\r\ndapibus\r\nlibero\r\nvenenatis\r\ntellus\r\nlobortis, vel\r\nlacinia elit\r\nfaucibus.\r\nMaecenas\r\nsemper sed\r\nquam quis\r\nfinibus.\r\nInteger\r\nefficitur,\r\nlibero\r\nimperdiet\r\nsollicitudin\r\ncommodo,\r\nelit arcu\r\nvulputate\r\nest, eget\r\nfinibus mi\r\nurna sit\r\namet\r\nmagna.\r\nCras\r\nullamcorper\r\nconsequat\r\nornare.\r\nFusce\r\nconvallis\r\nnunc vel\r\nrisus\r\ncursus, at\r\nmaximus\r\nligula\r\ncursus.\r\nPellentesqu\r\ne vulputate\r\nrisus libero,\r\neget cursus\r\nnibh\r\nsodales\r\nsed. Donec\r\naccumsan\r\nsem et\r\nmassa\r\nsemper, id\r\ndignissim\r\nvelit\r\nvehicula.Cr\r\nas cursus\r\nipsum ac\r\nerat\r\nvehicula,\r\nnec iaculis\r\npurus\r\ndictum.\r\nQuisque\r\nlacinia elit\r\nvitae leo\r\ndictum, vel\r\ndignissim\r\nvelit\r\ndapibus.\r\nAenean sem\r\nnisi,\r\nfaucibus\r\ninterdum\r\njusto eu,\r\neuismod\r\nporttitor ex.\r\nMorbi et\r\nlectus\r\nlectus. Duis\r\nneque felis,\r\nsuscipit at\r\nscelerisque\r\neu,\r\nscelerisque\r\nid orci.\r\nCurabitur et\r\nplacerat\r\nipsum.\r\nProin\r\ngravida\r\nsapien nisl,\r\net varius\r\nipsum\r\nmollis nec.\r\nQuisque\r\ndignissim\r\nconsectetur\r\nfeugiat.\r\nAenean\r\neros purus,\r\nlaoreet\r\ninterdum\r\nrutrum at,\r\naliquet sit\r\namet\r\nlectus.\r\nDonec\r\ngravida\r\nlorem ut\r\ntincidunt\r\nlaoreet.\r\nDonec\r\nconsequat\r\nviverra\r\nligula, in\r\naccumsan\r\nmi\r\nbibendum\r\nscelerisque.\r\nQuisque ac\r\nrisus justo.\r\nMorbi\r\nmagna\r\narcu,\r\negestas nec\r\nluctus\r\ncommodo,\r\ncursus eget\r\nnunc.\r\nVivamus\r\neuismod\r\nlorem ex, et\r\nmaximus\r\nfelis\r\nhendrerit\r\neget.\r\nNullam\r\nullamcorper\r\neuismod\r\nligula, et\r\niaculis\r\nligula\r\nultricies a.\r\nFusce\r\naliquam,\r\nenim vel\r\nfermentum\r\nultrices, elit\r\nquam\r\nsemper\r\nerat, vitae\r\nsemper velit\r\naugue non\r\nmagna.Quis\r\nque\r\nmaximus\r\nsemper\r\narcu, id\r\npellentesqu\r\ne est\r\ntempus a.\r\nPhasellus\r\nlacus elit,\r\nauctor sit\r\namet lacinia\r\na, dapibus\r\nvitae velit.\r\nPhasellus ut\r\npharetra\r\njusto, ut\r\nultricies\r\nerat. Sed\r\nmolestie\r\nsapien vel\r\ninterdum\r\nlobortis.\r\nNulla\r\nfacilisi.\r\nVestibulum\r\nante ipsum\r\nprimis in\r\nfaucibus\r\norci luctus\r\net ultrices\r\nposuere\r\ncubilia\r\ncurae; Nulla\r\nnec mauris\r\nquis nisi\r\nvulputate\r\ngravida quis\r\nnec\r\nvelit.Nam et\r\ncongue\r\nipsum.\r\nNulla vel elit\r\nnon dolor\r\nmollis\r\naliquet vel\r\nat magna.\r\nPellentesqu\r\ne nec\r\nfacilisis elit.\r\nIn vulputate\r\nquis sem\r\nporta\r\nsuscipit.\r\nNullam sed\r\nex ornare\r\nnibh\r\nsuscipit\r\nmattis quis\r\nnon lacus.\r\nMauris vel\r\nex urna.\r\nVivamus\r\nultricies\r\nsapien sit\r\namet sapien\r\nvehicula\r\ngravida.\r\nDonec\r\nfeugiat\r\nvolutpat\r\nquam.\r\nVestibulum\r\nauctor\r\ndictum nisl,\r\nid hendrerit\r\nmetus\r\nullamcorper\r\nsed. Nulla\r\nmaximus\r\nlacus vel\r\nmollis\r\nmaximus.\r\nNulla\r\nlaoreet\r\nplacerat\r\nquam eu\r\nviverra.\r\nEtiam\r\nfeugiat\r\naccumsan\r\nnisl a\r\ncondimentu\r\nm. Sed\r\nultricies\r\nante ante,\r\nac auctor\r\nligula\r\ngravida nec.\r\nPraesent a\r\nneque\r\ndignissim,\r\nsagittis felis\r\nsit amet,\r\ncondimentu\r\nm turpis.\r\nFusce at leo\r\nvel est\r\nblandit\r\nmalesuada.\r\nPellentesqu\r\ne et neque\r\nnon metus\r\npellentesqu\r\ne imperdiet.\r\nPraesent\r\npellentesqu\r\ne lacinia\r\nlorem, et\r\ntristique\r\ntellus\r\nefficitur id.\r\nSuspendiss\r\ne aliquet\r\nultricies\r\njusto vitae\r\ninterdum.\r\nCras\r\ntristique\r\nviverra\r\nquam, eget\r\ngravida mi\r\nfermentum\r\nimperdiet.\r\nSed\r\nimperdiet\r\nvitae purus\r\nut volutpat.\r\nNulla\r\nlacinia elit\r\nin\r\nfermentum\r\nconsectetur\r\n. Phasellus\r\ncommodo\r\nut nisl sit\r\namet\r\nsagittis.\r\nDuis ac\r\nornare orci.\r\nVivamus vel\r\nenim\r\nposuere,\r\npharetra ex\r\nvel,\r\nelementum\r\nest.\r\nVestibulum\r\ncommodo\r\nluctus\r\nmetus eget\r\nmaximus.\r\nSuspendiss\r\ne a nulla a\r\nodio\r\neleifend\r\nfaucibus.\r\nSuspendiss\r\ne semper\r\nlacus non\r\nporttitor\r\naliquet.\r\nCras ac\r\nscelerisque\r\nmagna, et\r\npulvinar\r\njusto.\r\nInteger\r\ncursus\r\npulvinar\r\nfringilla.\r\nMauris\r\nimperdiet\r\nnibh sit\r\namet\r\ntempor\r\nlaoreet.\r\nMorbi\r\ntincidunt\r\ntortor ex, sit\r\namet\r\nmaximus\r\npurus\r\ntristique\r\nquis.\r\nQuisque\r\nsed\r\nhendrerit\r\nvelit. Mauris\r\nmattis nibh\r\nut eros\r\nluctus, eget\r\nmattis\r\nmassa\r\nauctor.\r\nPhasellus\r\neu neque at\r\naugue\r\ngravida\r\nsagittis nec\r\nnon tortor.\r\nEtiam\r\nporttitor\r\nsem\r\nsodales mi\r\nullamcorper\r\ngravida. In\r\nin dictum\r\norci. In vitae\r\nvestibulum\r\nquam. Cras\r\naugue eros,\r\ntincidunt ac\r\nelit posuere,\r\nsollicitudin\r\nefficitur\r\nlectus.\r\nPraesent\r\nquis\r\nsodales\r\nnisl. Proin\r\nsit amet\r\nmolestie\r\nest. In\r\ncommodo\r\nmauris vel\r\nmauris\r\nefficitur,\r\nnec mollis\r\nmauris\r\nsagittis.\r\nCras ligula\r\nnibh,\r\negestas sit\r\namet eros\r\nin, lacinia\r\ntristique\r\nmagna.\r\nCras risus\r\nlibero,\r\nlacinia eget\r\nlibero vitae,\r\nmaximus\r\naliquet\r\nnibh. Mauris\r\nid sodales\r\npurus, vitae\r\ndictum\r\nlectus. Cras\r\nconsectetur\r\nligula velit,\r\ntempus\r\npulvinar\r\nlacus\r\nporttitor\r\nvitae.\r\nPhasellus\r\neget tellus\r\nipsum.\r\nDonec\r\ninterdum\r\nlaoreet elit\r\nnon\r\nvestibulum.\r\nCras sed\r\nurna\r\nullamcorper\r\n, aliquam\r\nerat eget,\r\nporta orci.\r\nVestibulum\r\neget congue\r\nnulla. Sed\r\nsem tortor,\r\neuismod at\r\nrutrum id,\r\nsagittis a\r\nnunc. Duis\r\nin nibh\r\nfacilisis,\r\ndignissim\r\npurus ut,\r\nhendrerit\r\nmagna. Sed\r\nsemper\r\nligula id\r\nmassa\r\nelementum,\r\nnon\r\nmalesuada\r\nvelit\r\negestas.\r\nNullam\r\ndictum, mi\r\nnec\r\neuismod\r\nsagittis,\r\nligula leo\r\nullamcorper\r\ndolor, quis\r\nfaucibus\r\nodio metus\r\neget magna.\r\nUt gravida\r\nmetus non\r\nmetus\r\nbibendum\r\nbibendum.\r\nIn sagittis\r\neleifend\r\naliquet.\r\nInterdum et\r\nmalesuada\r\nfames ac\r\nante ipsum\r\nprimis in\r\nfaucibus.\r\nNam mollis\r\nsagittis\r\nfelis, in\r\nfaucibus\r\ntortor\r\npretium vel.\r\nNam nec\r\nenim\r\nmetus.\r\nDonec in\r\naugue arcu.\r\nProin non\r\nlobortis\r\npurus, sit\r\namet lacinia\r\nelit.\r\nSuspendiss\r\ne quis eros\r\ncondimentu\r\nm, blandit\r\njusto sit\r\namet,\r\nlobortis nisl.\r\nSuspendiss\r\ne maximus\r\nmassa sed\r\nurna tempor\r\nornare.\r\nNunc\r\nmalesuada\r\npurus odio,\r\neu luctus\r\nlectus\r\nauctor nec.\r\nMorbi\r\nauctor\r\npellentesqu\r\ne auctor.\r\nSed\r\nullamcorper\r\n, ex vitae\r\naliquam\r\nvulputate,\r\nest diam\r\nfeugiat mi,\r\nid porttitor\r\nlectus orci\r\nac leo.\r\nDonec sit\r\namet velit\r\npulvinar,\r\nvenenatis\r\nturpis ut,\r\ninterdum\r\nligula.\r\nInterdum et\r\nmalesuada\r\nfames ac\r\nante ipsum\r\nprimis in\r\nfaucibus.\r\nVestibulum\r\neu lacus\r\nurna.\r\nMaecenas\r\nsem nulla,\r\naccumsan\r\neu ultricies\r\nsed, tempor\r\nvel magna.\r\nCras aliquet\r\nsollicitudin\r\nsapien ac\r\npulvinar.\r\nPraesent ac\r\nsodales mi.\r\nInteger vitae\r\nmauris\r\nmassa.\r\nMaecenas\r\niaculis orci\r\net faucibus\r\ninterdum.\r\nNunc nec\r\nmaximus\r\nfelis, sed\r\nfinibus\r\nquam.\r\nPellentesqu\r\ne felis\r\nmassa,\r\nvestibulum\r\nin tellus\r\nvitae,\r\ncongue\r\ntincidunt\r\njusto. Nunc\r\nvitae enim\r\nmalesuada,\r\nbibendum\r\nante nec,\r\nvarius\r\ntellus.\r\nPraesent\r\nvitae nisi id\r\nquam\r\nauctor\r\nlacinia at\r\nnon quam.\r\nNam nec\r\nligula sit\r\namet felis\r\nauctor\r\nsagittis.\r\nNunc in\r\nrisus eu\r\nurna varius\r\nlaoreet quis\r\nsit amet\r\nfelis. Morbi\r\nvarius\r\ntempor orci,\r\neu\r\nvestibulum\r\nnunc\r\nvestibulum\r\nac. Nunc\r\nvehicula\r\nvelit\r\neleifend\r\nconsequat\r\nporta.\r\nSuspendiss\r\ne maximus\r\ndapibus\r\norci, in\r\nvulputate\r\nmassa\r\npretium ac.\r\nQuisque\r\nmalesuada\r\naliquet\r\naliquet.";
+
+ var mf = new MeasurementFont()
+ {
+ FontFamily = "Aptos Narrow",
+ Style = MeasurementFontStyles.Regular,
+ Size = 11.0f,
+ };
+
+ var handler = new TextHandler(SystemFolderEngine, mf);
+
+ double maxPixelWidth = 72d;
+
+ List fragments = new List() { Lorem20Str };
+ List fonts = new List() { mf };
+
+ //TextFragmentCollectionSimple collection = new TextFragmentCollectionSimple(fonts, fragments);
+
+ var pointWidth = 54.085732283464566929133858267717f;
+
+ var wrappedStrings = handler.WrapText(Lorem20Str, pointWidth);
+ var savedStrings = SavedComparisonString.Split(new string[] { "\r\n" }, StringSplitOptions.None);
+
+ List differingStrings = new();
+
+ if (wrappedStrings.Count() > savedStrings.Count())
+ differingStrings = savedStrings.Except(wrappedStrings).ToList();
+ else
+ differingStrings = wrappedStrings.Except(savedStrings).ToList();
+
+ ////INTENTIONAL COMMENT for easier debugging later.
+ ////It is sometimes easier to compare files directly when debugging rather than looking through "differingStrings"
+ //bool writeFiles = true;
+
+ //if (/*differingStrings.Count() > 0*/ writeFiles)
+ ////{
+ File.WriteAllText("C:\\temp\\LoremIpsum20_NEW.txt", string.Join("\r\n", wrappedStrings.ToArray()));
+ File.WriteAllText("C:\\temp\\LoremIpsum20_OLD.txt", SavedComparisonString);
+
+ var currStr = File.ReadAllText("C:\\temp\\LoremIpsum20_NEW.txt");
+
+ var res = SavedComparisonString.CompareTo(currStr);
+
+ Assert.AreEqual(SavedComparisonString, currStr);
+ ////}
+
+ Assert.AreEqual(0, differingStrings.Count());
+ }
+ }
+}
diff --git a/src/EPPlus.DrawingRenderer.Tests/TextRenderTests.cs b/src/EPPlus.DrawingRenderer.Tests/TextRenderTests.cs
new file mode 100644
index 0000000000..d04da9f575
--- /dev/null
+++ b/src/EPPlus.DrawingRenderer.Tests/TextRenderTests.cs
@@ -0,0 +1,390 @@
+//using EPPlus.Export.ImageRenderer.ChartAreaRenderItems.SvgItem;
+//using EPPlus.Fonts.OpenType;
+//using EPPlus.Fonts.OpenType.Integration;
+//using EPPlus.Fonts.OpenType.Utils;
+//using EPPlus.Graphics;
+//using EPPlusImageRenderer;
+//using EPPlusImageRenderer.Svg;
+//using OfficeOpenXml;
+//using OfficeOpenXml.Drawing;
+//using OfficeOpenXml.FormulaParsing.Excel.Functions.MathFunctions;
+//using OfficeOpenXml.Interfaces.Drawing.Text;
+//using OfficeOpenXml.Style;
+//using System.Diagnostics;
+//using System.Globalization;
+//using System.Runtime.InteropServices;
+//using System.Text;
+
+//namespace EPPlus.Export.ImageRenderer.Tests
+//{
+// [TestClass]
+// public sealed class TextRenderTests : TestBase
+// {
+// private void SetFillColor(ExcelDrawingFillBasic fill, string color)
+// {
+// if (color.StartsWith("#"))
+// {
+// fill.Style = eFillStyle.SolidFill;
+// var r = int.Parse(color.Substring(1, 2), NumberStyles.HexNumber);
+// var g = int.Parse(color.Substring(3, 2), NumberStyles.HexNumber);
+// var b = int.Parse(color.Substring(5, 2), NumberStyles.HexNumber);
+// var c = System.Drawing.Color.FromArgb(0xFF, (byte)r, (byte)g, (byte)b);
+// fill.SolidFill.Color.SetRgbColor(c);
+// }
+// else if (string.IsNullOrWhiteSpace(color) == false)
+// {
+// fill.Style = eFillStyle.SolidFill;
+// try
+// {
+// var c = System.Drawing.Color.FromName(color);
+// if (c.IsEmpty)
+// {
+// var sc = Enum.Parse(color);
+// fill.SolidFill.Color.SetSchemeColor(sc);
+// }
+// else
+// {
+// fill.SolidFill.Color.SetPresetColor(c);
+// }
+// }
+// catch
+// {
+// var sc = Enum.Parse(color);
+// fill.SolidFill.Color.SetSchemeColor(sc);
+// }
+// }
+// else
+// {
+// fill.Style = eFillStyle.NoFill;
+// }
+// }
+
+// //TextFragmentCollectionSimple GenerateTextFragments(ExcelDrawingTextRunCollection runs)
+// //{
+// // List runContents = new List();
+// // List fontSizes = new List();
+// // List fonts = new List();
+
+// // for (int i = 0; i < runs.Count(); i++)
+// // {
+// // var txtRun = runs[i];
+// // var runFont = txtRun.GetMeasurementFont();
+
+// // fonts.Add(runFont);
+
+// // runContents.Add(txtRun.Text);
+// // fontSizes.Add(runFont.Size);
+// // }
+
+
+// // return new TextFragmentCollectionSimple(fonts, runContents);
+// //}
+
+// //List GetWrappedText(ExcelDrawingTextRunCollection runs, TextFragmentCollectionSimple fragments)
+// //{
+
+// // List fonts = new List();
+
+// // for (int i = 0; i < runs.Count(); i++)
+// // {
+// // var txtRun = runs[i];
+// // var runFont = txtRun.GetMeasurementFont();
+// // fonts.Add(runFont);
+// // }
+
+// // var maxSizePoints = Math.Round(300d, 0, MidpointRounding.AwayFromZero).PixelToPoint();
+// // TextHandler handler = new TextHandler(fonts[0]);
+
+// // return handler.WrapMultipleTextFragmentsToTextLines(fragments, fonts, maxSizePoints);
+// //}
+
+// [TestMethod]
+// public void MeasureWrappedWidths()
+// {
+// List lstOfRichText = new() { /*"RenderTextbox\r\na",*/ "TextBox2", "ra underline", "La Strike", "Goudy size 16", "SvgSize 24" };
+
+// //var font1 = new MeasurementFont()
+// //{
+// // FontFamily = "Aptos Narrow",
+// // Size = 11,
+// // Style = MeasurementFontStyles.Regular
+// //}; ;
+
+// var font2 = new MeasurementFont()
+// {
+// FontFamily = "Aptos Narrow",
+// Size = 11,
+// Style = MeasurementFontStyles.Italic | MeasurementFontStyles.Bold
+// };
+
+// var font3 = new MeasurementFont()
+// {
+// FontFamily = "Aptos Narrow",
+// Size = 11,
+// Style = MeasurementFontStyles.Underline
+// };
+
+// var font4 = new MeasurementFont()
+// {
+// FontFamily = "Aptos Narrow",
+// Size = 11,
+// Style = MeasurementFontStyles.Strikeout
+// };
+
+// var font5 = new MeasurementFont()
+// {
+// FontFamily = "Goudy Stout",
+// Size = 16,
+// Style = MeasurementFontStyles.Regular
+// };
+
+
+// var font6 = new MeasurementFont()
+// {
+// FontFamily = "Aptos Narrow",
+// Size = 24,
+// Style = MeasurementFontStyles.Regular
+// };
+
+// List fonts = new() { /*font1,*/ font2, font3, font4, font5, font6};
+
+// var maxSizePoints = Math.Round(300d, 0, MidpointRounding.AwayFromZero).PixelToPoint();
+// var ttMeasurer = OpenTypeFonts.GetTextLayoutEngineForFont(font2);
+
+// var textFragments = new TextFragmentCollectionSimple(fonts, lstOfRichText);
+
+// var wrappedLines = ttMeasurer.WrapRichTextLines(textFragments, maxSizePoints);
+
+// //Assert.AreEqual(wrappedLines[0].r)
+
+
+// //Line 1 45 px 34.5pt
+// //Line 2 6px 4.5 pt
+// //Line 3 137 px 102.75 pt //result: 104.6328125 pt width "whole
+// //Line 4 270 px 202.5 pt
+// //Line 5 169 px 126.75 pt
+// }
+
+// [TestMethod]
+// public void VerifyTextRunBounds()
+// {
+// ExcelPackage.License.SetNonCommercialOrganization("EPPlus Project");
+
+// using (var p = new ExcelPackage())
+// {
+// var ws = p.Workbook.Worksheets.Add("Sheet1");
+// var cube = ws.Drawings.AddShape("Cube1", OfficeOpenXml.Drawing.eShapeStyle.Cube);
+
+// cube.Font.Color = System.Drawing.Color.Goldenrod;
+
+// cube.TextBody.TopInsert = 0;
+// cube.TextBody.BottomInsert = 0;
+// cube.TextBody.RightInsert = 0;
+// cube.TextBody.LeftInsert = 0;
+
+// var para1 = cube.TextBody.Paragraphs.Add("TextBox\r\na");
+
+// para1.LeftMargin = 5;
+// cube.TextBody.TopInsert = 10;
+
+// 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;
+
+// cube.TextAlignment = eTextAlignment.Left;
+// cube.TextAnchoring = eTextAnchoringType.Top;
+
+// cube.TextBody.HorizontalTextOverflow = eTextHorizontalOverflow.Clip;
+// cube.TextBody.VerticalTextOverflow = eTextVerticalOverflow.Clip;
+
+// SetFillColor(cube.Fill, "#156082");
+// SetFillColor(cube.Border.Fill, "#042433");
+
+// var aFont = cube.Font;
+// var paragraph0 = cube.TextBody.Paragraphs[0];
+
+// var autofit = cube.TextBody.TextAutofit;
+
+// cube.ChangeCellAnchor(eEditAs.Absolute);
+
+// cube.SetPixelWidth(5000);
+// var svgShape = new SvgShape(cube);
+// SvgTextBodyItem tbItem = svgShape.TextBodySvg;
+
+// var txtRun1Bounds = tbItem.Paragraphs[0].Runs[0].Bounds;
+// var pxWidth = txtRun1Bounds.Width.PointToPixel();
+// Assert.AreEqual(43.835286458333336d, pxWidth, 0.2d);
+
+// var txtRuns2 = tbItem.Paragraphs[1].Runs;
+
+// 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(),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);
+// Assert.AreEqual(49.89388020833334, txtRuns2[2].Bounds.Width.PointToPixel(), 0.0001);
+// currentLineWidth += txtRuns2[2].Bounds.Width.PointToPixel();
+
+// Assert.AreEqual(21.333333333333332, txtRuns2[3].Bounds.Height.PointToPixel());
+// Assert.AreEqual(currentLineWidth, txtRuns2[3].Bounds.Left.PointToPixel(), 0.0001);
+// Assert.AreEqual(283.21875d, txtRuns2[3].Bounds.Width.PointToPixel(), 0.0001);
+// currentLineWidth += txtRuns2[3].Bounds.Width.PointToPixel();
+
+// Assert.AreEqual(32, txtRuns2[4].Bounds.Height.PointToPixel(), 0.0001);
+// Assert.AreEqual(currentLineWidth, txtRuns2[4].Bounds.Left.PointToPixel(), 0.0001);
+// Assert.AreEqual(134.20312500000006d, txtRuns2[4].Bounds.Width.PointToPixel(), 0.0001);
+// currentLineWidth += txtRuns2[4].Bounds.Width.PointToPixel();
+// }
+// }
+
+// private ExcelShape GenerateTextShapeWithDifficultText(ExcelPackage p)
+// {
+// var myCulture = CultureInfo.CurrentCulture;
+
+// var ws = p.Workbook.Worksheets.Add("ShapeSheet");
+
+// 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;
+
+// cube.TextBody.TopInsert = 0;
+// cube.TextBody.BottomInsert = 0;
+// cube.TextBody.RightInsert = 0;
+// cube.TextBody.LeftInsert = 0;
+
+// var para1 = cube.TextBody.Paragraphs.Add("TextBodySvg\r\na");
+
+// 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;
+
+// cube.TextBody.HorizontalTextOverflow = eTextHorizontalOverflow.Clip;
+// cube.TextBody.VerticalTextOverflow = eTextVerticalOverflow.Clip;
+
+// 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 VerifyVerticalAlignTop()
+// {
+// ExcelPackage.License.SetNonCommercialOrganization("EPPlus Project");
+
+// using var p = new ExcelPackage();
+// var shape = GenerateTextShapeWithDifficultText(p);
+
+// shape.TextAnchoring = eTextAnchoringType.Top;
+
+// var svgShape = new SvgShape(shape);
+// SvgTextBodyItem tbItem = svgShape.TextBodySvg;
+
+// Assert.AreEqual(100, tbItem.Bounds.GlobalTop.PointToPixel());
+// }
+
+// [TestMethod]
+// public void VerifyVerticalAlignCenter()
+// {
+// ExcelPackage.License.SetNonCommercialOrganization("EPPlus Project");
+
+// using var p = new ExcelPackage();
+// var shape = GenerateTextShapeWithDifficultText(p);
+
+// shape.TextAnchoring = eTextAnchoringType.Center;
+
+// var svgShape = new SvgShape(shape);
+// SvgTextBodyItem tbItem = svgShape.TextBodySvg;
+
+
+// //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);
+// SaveTextFileToWorkbook("bottom.svg", svg);
+// SaveWorkbook("vaBottom.xlsx", p);
+// //Appears off by ~2 px because of border width
+// Assert.AreEqual(278d, tbItem.Bounds.GlobalTop.PointToPixel(), 1.0);
+// }
+
+// [TestMethod]
+// public void AddTextPosition()
+// {
+// ExcelPackage.License.SetNonCommercialOrganization("EPPlus Project");
+
+// using var p = new ExcelPackage();
+// var shape = GenerateTextShapeWithDifficultText(p);
+
+// shape.TextAnchoring = eTextAnchoringType.Center;
+
+// var svgShape = new SvgShape(shape);
+// SvgTextBodyItem tbItem = svgShape.TextBodySvg;
+
+
+// var sb = new StringBuilder();
+// var lastpara = tbItem.Paragraphs.Last();
+
+// MeasurementFont font = new MeasurementFont
+// {
+// FontFamily = "Aptos Narrow",
+// Size = 11,
+// Style = MeasurementFontStyles.Bold
+// };
+
+// //var trItem = new SvgTextRunItem(svgShape, lastpara.Bounds, font, "MyText");
+// //lastpara.Runs.Add(trItem);
+// lastpara.AddOwnText(new TextFragment() {Text = "my new text", Font = font });
+// svgShape.Render(sb);
+
+// var str = sb.ToString();
+
+// SaveTextFileToWorkbook("InsertedTextTest.svg", str);
+// }
+// }
+//}
diff --git a/src/EPPlus.DrawingRenderer.Tests/TextboxTest.cs b/src/EPPlus.DrawingRenderer.Tests/TextboxTest.cs
new file mode 100644
index 0000000000..daa8beb25e
--- /dev/null
+++ b/src/EPPlus.DrawingRenderer.Tests/TextboxTest.cs
@@ -0,0 +1,74 @@
+//using EPPlus.Export.ImageRenderer.ChartAreaRenderItems.SvgItem;
+//using EPPlus.Export.ImageRenderer.Svg;
+//using EPPlus.Graphics;
+//using EPPlusImageRenderer.ChartAreaRenderItems;
+//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);
+
+// var sb = new StringBuilder();
+
+// item.Render(sb);
+// var svgString = sb.ToString();
+
+// //before we assumed we consider the space widths
+// var widthWithSpace = txtBox.TextBody.Paragraphs[0].SpaceWidthsPerLine[0] + txtBox.Width;
+
+
+// //Assert.AreEqual(36d, txtBox.TextBody.Width);
+// Assert.AreEqual(37.5d, widthWithSpace, 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.DrawingRenderer/AdjustmentType.cs b/src/EPPlus.DrawingRenderer/AdjustmentType.cs
new file mode 100644
index 0000000000..0c1912a58b
--- /dev/null
+++ b/src/EPPlus.DrawingRenderer/AdjustmentType.cs
@@ -0,0 +1,23 @@
+/*************************************************************************************************
+ 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.DrawingRenderer
+{
+ internal enum AdjustmentType
+ {
+ AdjustToWidthHeight = 0,
+ LockToAdjustMin = 1,
+ LockToAdjustMax = 2,
+ Adjust = 255,
+ }
+}
\ No newline at end of file
diff --git a/src/EPPlus.DrawingRenderer/ChartObjectMargins.cs b/src/EPPlus.DrawingRenderer/ChartObjectMargins.cs
new file mode 100644
index 0000000000..8a1c0cca47
--- /dev/null
+++ b/src/EPPlus.DrawingRenderer/ChartObjectMargins.cs
@@ -0,0 +1,8 @@
+namespace EPPlus.DrawingRenderer
+{
+ internal static class ChartObjectMargins
+ {
+ static int TitleToPlotarea = 14;
+ static int LegendToPlotarea = 14;
+ }
+}
diff --git a/src/EPPlus.DrawingRenderer/Constants/Constants.cs b/src/EPPlus.DrawingRenderer/Constants/Constants.cs
new file mode 100644
index 0000000000..a29202b7e4
--- /dev/null
+++ b/src/EPPlus.DrawingRenderer/Constants/Constants.cs
@@ -0,0 +1,38 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+
+namespace EPPlus.DrawingRenderer
+{
+ internal class Constants
+ {
+ internal const float STANDARD_DPI = 96;
+ ///
+ /// The ratio between EMU and Pixels
+ ///
+ public const int EMU_PER_PIXEL = 9525;
+ ///
+ /// The ratio between EMU and Points
+ ///
+ public const int EMU_PER_POINT = 12700;
+ ///
+ /// The ratio between EMU and centimeters
+ ///
+ public const int EMU_PER_CM = 360000;
+ ///
+ /// The ratio between EMU and millimeters
+ ///
+ public const int EMU_PER_MM = 3600000;
+ ///
+ /// The ratio between EMU and US Inches
+ ///
+ public const int EMU_PER_US_INCH = 914400;
+ ///
+ /// The ratio between EMU and pica
+ ///
+ public const int EMU_PER_PICA = EMU_PER_US_INCH / 6;
+
+ }
+}
diff --git a/src/EPPlus.DrawingRenderer/Constants/PatternArrays.cs b/src/EPPlus.DrawingRenderer/Constants/PatternArrays.cs
new file mode 100644
index 0000000000..0992f78af8
--- /dev/null
+++ b/src/EPPlus.DrawingRenderer/Constants/PatternArrays.cs
@@ -0,0 +1,402 @@
+/*************************************************************************************************
+ 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 DrawingRenderer.Constants
+{
+ internal static class PatternArrays
+ {
+ internal static readonly short[][] Pct30 = new short[][]
+ {
+ new short[] { 0, 0, 0, 1 },
+ new short[] { 1, 0, 1, 0 },
+ new short[] { 0, 1, 0, 0 },
+ new short[] { 1, 0, 1, 0 }
+ };
+
+ internal static readonly short[][] Pct40 = new short[][]
+ {
+ new short[] { 0, 0, 0, 1, 0, 1, 0, 1 },
+ new short[] { 1, 0, 1, 0, 1, 0, 1, 0 },
+ new short[] { 0, 1, 0, 1, 0, 1, 0, 1 },
+ new short[] { 1, 0, 1, 0, 1, 0, 1, 0 },
+ new short[] { 0, 1, 0, 1, 0, 0, 0, 1 },
+ new short[] { 1, 0, 1, 0, 1, 0, 1, 0 },
+ new short[] { 0, 1, 0, 1, 0, 1, 0, 1 },
+ new short[] { 1, 0, 1, 0, 1, 0, 1, 0 }
+ };
+
+ internal static readonly short[][] Pct50 = new short[][]
+ {
+ new short[] { 1, 1, 1, 0 },
+ new short[] { 0, 1, 0, 1 },
+ new short[] { 1, 0, 1, 1 },
+ new short[] { 0, 1, 0, 1 }
+ };
+
+ internal static readonly short[][] Pct60 = new short[][]
+ {
+ new short[] { 1, 1, 0, 1 },
+ new short[] { 0, 1, 1, 1 }
+ };
+
+ internal static readonly short[][] Pct70 = new short[][]
+ {
+ new short[] { 1, 1, 0, 1 },
+ new short[] { 1, 1, 1, 1 },
+ new short[] { 0, 1, 1, 1 },
+ new short[] { 1, 1, 1, 1 }
+ };
+
+
+ internal static readonly short[][] LtHorz = new short[][] {
+ new short[] { 1 },
+ new short[] { 0 },
+ new short[] { 0 },
+ new short[] { 0 }
+ };
+
+ internal static readonly short[][] LtVert = new short[][] {
+ new short[] { 1, 0, 0, 0 }
+ };
+
+ internal static readonly short[][] LtUpDiag = new short[][] {
+ new short[] { 0, 0, 0, 1 },
+ new short[] { 0, 0, 1, 0 },
+ new short[] { 0, 1, 0, 0 },
+ new short[] { 1, 0, 0, 0 }
+ };
+
+ internal static readonly short[][] LtDnDiag = new short[][] {
+ new short[] { 1, 0, 0, 0 },
+ new short[] { 0, 1, 0, 0 },
+ new short[] { 0, 0, 1, 0 },
+ new short[] { 0, 0, 0, 1 }
+ };
+
+ internal static readonly short[][] DkVert = new short[][] {
+ new short[] { 1, 1, 0, 0 }
+ };
+
+ internal static readonly short[][] DkHorz = new short[][] {
+ new short[] { 1 },
+ new short[] { 1 },
+ new short[] { 0 },
+ new short[] { 0 }
+ };
+
+
+ internal static readonly short[][] DkUpDiag = new short[][] {
+ new short[] { 1, 0, 0, 1 },
+ new short[] { 0, 0, 1, 1 },
+ new short[] { 0, 1, 1, 0 },
+ new short[] { 1, 1, 0, 0 }
+ };
+
+ internal static readonly short[][] DkDnDiag = new short[][] {
+ new short[] { 1, 0, 0, 1 },
+ new short[] { 1, 1, 0, 0 },
+ new short[] { 0, 1, 1, 0 },
+ new short[] { 0, 0, 1, 1 }
+ };
+
+ internal static readonly short[][] WdUpDiag = new short[][] {
+ new short[] { 1, 1, 0, 0, 0, 0, 0, 0 },
+ new short[] { 1, 0, 0, 0, 0, 0, 0, 1 },
+ new short[] { 0, 0, 0, 0, 0, 0, 1, 1 },
+ new short[] { 0, 0, 0, 0, 0, 1, 1, 0 },
+ new short[] { 0, 0, 0, 0, 1, 1, 0, 0 },
+ new short[] { 0, 0, 0, 1, 1, 0, 0, 0 },
+ new short[] { 0, 0, 1, 1, 0, 0, 0, 0 },
+ new short[] { 0, 1, 1, 0, 0, 0, 0, 0 }
+ };
+
+ internal static readonly short[][] WdDnDiag = new short[][] {
+ new short[] { 1, 0, 0, 0, 0, 0, 0, 1 },
+ new short[] { 1, 1, 0, 0, 0, 0, 0, 0 },
+ new short[] { 0, 1, 1, 0, 0, 0, 0, 0 },
+ new short[] { 0, 0, 1, 1, 0, 0, 0, 0 },
+ new short[] { 0, 0, 0, 1, 1, 0, 0, 0 },
+ new short[] { 0, 0, 0, 0, 1, 1, 0, 0 },
+ new short[] { 0, 0, 0, 0, 0, 1, 1, 0 },
+ new short[] { 0, 0, 0, 0, 0, 0, 1, 1 }
+ };
+
+ internal static readonly short[][] NarVert = new short[][] {
+ new short[] { 1, 0 }
+ };
+
+ internal static readonly short[][] NarHorz = new short[][] {
+ new short[] { 1 },
+ new short[] { 0 }
+ };
+
+
+ internal static readonly short[][] Vert = new short[][] {
+ new short[] { 1, 0, 0, 0, 0, 0, 0, 0 }
+ };
+
+ internal static readonly short[][] Horz = new short[][] {
+ new short[] { 1 },
+ new short[] { 0 },
+ new short[] { 0 },
+ new short[] { 0 },
+ new short[] { 0 },
+ new short[] { 0 },
+ new short[] { 0 },
+ new short[] { 0 }
+ };
+
+ internal static readonly short[][] DashDnDiag = new short[][] {
+ new short[] { 1, 0, 0, 0, 1, 0, 0, 0 },
+ new short[] { 0, 1, 0, 0, 0, 1, 0, 0 },
+ new short[] { 0, 0, 1, 0, 0, 0, 1, 0 },
+ new short[] { 0, 0, 0, 1, 0, 0, 0, 1 },
+ new short[] { 0, 0, 0, 0, 0, 0, 0, 0 },
+ new short[] { 0, 0, 0, 0, 0, 0, 0, 0 },
+ new short[] { 0, 0, 0, 0, 0, 0, 0, 0 },
+ new short[] { 0, 0, 0, 0, 0, 0, 0, 0 }
+ };
+
+ internal static readonly short[][] DashUpDiag = new short[][] {
+ new short[] { 0, 1, 0, 0, 0, 1, 0, 0 },
+ new short[] { 1, 0, 0, 0, 1, 0, 0, 0 },
+ new short[] { 0, 0, 0, 0, 0, 0, 0, 0 },
+ new short[] { 0, 0, 0, 0, 0, 0, 0, 0 },
+ new short[] { 0, 0, 0, 0, 0, 0, 0, 0 },
+ new short[] { 0, 0, 0, 0, 0, 0, 0, 0 },
+ new short[] { 0, 0, 0, 1, 0, 0, 0, 1 },
+ new short[] { 0, 0, 1, 0, 0, 0, 1, 0 }
+ };
+
+ internal static readonly short[][] DashHorz = new short[][] {
+ new short[] { 0, 0, 0, 0, 1, 1, 1, 1 },
+ new short[] { 0, 0, 0, 0, 0, 0, 0, 0 },
+ new short[] { 0, 0, 0, 0, 0, 0, 0, 0 },
+ new short[] { 0, 0, 0, 0, 0, 0, 0, 0 },
+ new short[] { 0, 0, 0, 0, 1, 1, 1, 1 },
+ new short[] { 0, 0, 0, 0, 0, 0, 0, 0 },
+ new short[] { 0, 0, 0, 0, 0, 0, 0, 0 },
+ new short[] { 0, 0, 0, 0, 0, 0, 0, 0 }
+ };
+
+ internal static readonly short[][] DashVert = new short[][] {
+ new short[] { 0, 0, 0, 0, 1, 0, 0, 0 },
+ new short[] { 0, 0, 0, 0, 1, 0, 0, 0 },
+ new short[] { 0, 0, 0, 0, 1, 0, 0, 0 },
+ new short[] { 0, 0, 0, 0, 1, 0, 0, 0 },
+ new short[] { 1, 0, 0, 0, 0, 0, 0, 0 },
+ new short[] { 1, 0, 0, 0, 0, 0, 0, 0 },
+ new short[] { 1, 0, 0, 0, 0, 0, 0, 0 },
+ new short[] { 1, 0, 0, 0, 0, 0, 0, 0 }
+ };
+
+
+ internal static readonly short[][] SmConfetti = new short[][] {
+ new short[] { 0, 0, 0, 1, 0, 0, 0, 0 },
+ new short[] { 0, 0, 0, 0, 0, 0, 0, 1 },
+ new short[] { 0, 0, 1, 0, 0, 0, 0, 0 },
+ new short[] { 0, 0, 0, 0, 0, 1, 0, 0 },
+ new short[] { 1, 0, 0, 0, 0, 0, 0, 0 },
+ new short[] { 0, 0, 0, 0, 1, 0, 0, 0 },
+ new short[] { 0, 1, 0, 0, 0, 0, 0, 0 },
+ new short[] { 0, 0, 0, 0, 0, 0, 1, 0 }
+ };
+
+ internal static readonly short[][] LgConfetti = new short[][] {
+ new short[] { 0, 0, 0, 0, 0, 0, 1, 1 },
+ new short[] { 0, 0, 0, 1, 1, 0, 1, 1 },
+ new short[] { 1, 1, 0, 1, 1, 0, 0, 0 },
+ new short[] { 1, 1, 0, 0, 0, 0, 0, 0 },
+ new short[] { 0, 0, 0, 0, 1, 1, 0, 0 },
+ new short[] { 1, 0, 0, 0, 1, 1, 0, 1 },
+ new short[] { 1, 0, 1, 1, 0, 0, 0, 1 },
+ new short[] { 0, 0, 1, 1, 0, 0, 0, 0 }
+ };
+
+ internal static readonly short[][] ZigZag = new short[][] {
+ new short[] { 1, 0, 0, 0, 0, 0, 0, 1 },
+ new short[] { 0, 1, 0, 0, 0, 0, 1, 0 },
+ new short[] { 0, 0, 1, 0, 0, 1, 0, 0 },
+ new short[] { 0, 0, 0, 1, 1, 0, 0, 0 }
+ };
+
+ internal static readonly short[][] Wave = new short[][] {
+ new short[] { 0, 0, 1, 0, 0, 1, 0, 1 },
+ new short[] { 1, 1, 0, 0, 0, 0, 0, 0 },
+ new short[] { 0, 0, 0, 0, 0, 0, 0, 0 },
+ new short[] { 0, 0, 0, 1, 1, 0, 0, 0 }
+ };
+
+ internal static readonly short[][] DiagBrick = new short[][] {
+ new short[] { 0, 0, 1, 0, 0, 1, 0, 0 },
+ new short[] { 0, 1, 0, 0, 0, 0, 1, 0 },
+ new short[] { 1, 0, 0, 0, 0, 0, 0, 1 },
+ new short[] { 0, 0, 0, 0, 0, 0, 0, 1 },
+ new short[] { 0, 0, 0, 0, 0, 0, 1, 0 },
+ new short[] { 0, 0, 0, 0, 0, 1, 0, 0 },
+ new short[] { 0, 0, 0, 0, 1, 0, 0, 0 },
+ new short[] { 0, 0, 0, 1, 1, 0, 0, 0 }
+ };
+
+ internal static readonly short[][] HorzBrick = new short[][] {
+ new short[] { 1, 0, 0, 0, 0, 0, 0, 1 },
+ new short[] { 1, 0, 0, 0, 0, 0, 0, 1 },
+ new short[] { 1, 0, 0, 0, 0, 0, 0, 1 },
+ new short[] { 1, 1, 1, 1, 1, 1, 1, 1 }
+ };
+
+ internal static readonly short[][] Weave = new short[][] {
+ new short[] { 1, 0, 0, 0, 1, 0, 0, 0 },
+ new short[] { 0, 1, 0, 1, 0, 1, 0, 0 },
+ new short[] { 0, 0, 1, 0, 0, 0, 1, 0 },
+ new short[] { 0, 1, 0, 0, 0, 1, 0, 1 },
+ new short[] { 1, 0, 0, 0, 1, 0, 0, 0 },
+ new short[] { 0, 0, 0, 1, 0, 1, 0, 0 },
+ new short[] { 0, 0, 1, 0, 0, 0, 1, 0 },
+ new short[] { 0, 1, 0, 1, 0, 0, 0, 1 }
+ };
+
+ internal static readonly short[][] Plaid = new short[][] {
+ new short[] { 1, 0, 1, 0, 1, 0, 1, 0 },
+ new short[] { 0, 1, 0, 1, 0, 1, 0, 1 },
+ new short[] { 1, 1, 1, 1, 0, 0, 0, 0 },
+ new short[] { 1, 1, 1, 1, 0, 0, 0, 0 },
+ new short[] { 1, 1, 1, 1, 0, 0, 0, 0 },
+ new short[] { 1, 1, 1, 1, 0, 0, 0, 0 },
+ new short[] { 1, 0, 1, 0, 1, 0, 1, 0 },
+ new short[] { 0, 1, 0, 1, 0, 1, 0, 1 }
+ };
+
+
+ internal static readonly short[][] Divot = new short[][] {
+ new short[] { 0, 0, 0, 0, 0, 0, 0, 1 },
+ new short[] { 1, 0, 0, 0, 0, 0, 0, 0 },
+ new short[] { 0, 0, 0, 0, 0, 0, 0, 0 },
+ new short[] { 0, 0, 0, 1, 0, 0, 0, 0 },
+ new short[] { 0, 0, 0, 0, 1, 0, 0, 0 },
+ new short[] { 0, 0, 0, 1, 0, 0, 0, 0 },
+ new short[] { 0, 0, 0, 0, 0, 0, 0, 0 },
+ new short[] { 1, 0, 0, 0, 0, 0, 0, 0 }
+ };
+
+ internal static readonly short[][] DotGrid = new short[][] {
+ new short[] { 1, 0, 1, 0, 1, 0, 1, 0 },
+ new short[] { 0, 0, 0, 0, 0, 0, 0, 0 },
+ new short[] { 1, 0, 0, 0, 0, 0, 0, 1 },
+ new short[] { 0, 0, 0, 0, 0, 0, 0, 0 },
+ new short[] { 1, 0, 0, 0, 0, 0, 0, 1 },
+ new short[] { 0, 0, 0, 0, 0, 0, 0, 0 },
+ new short[] { 1, 0, 0, 0, 0, 0, 0, 1 },
+ new short[] { 0, 0, 0, 0, 0, 0, 0, 0 }
+ };
+
+ internal static readonly short[][] DotDmnd = new short[][] {
+ new short[] { 1, 0, 0, 0, 0, 0, 0, 0 },
+ new short[] { 0, 0, 0, 0, 0, 0, 0, 0 },
+ new short[] { 0, 0, 1, 0, 0, 0, 1, 0 },
+ new short[] { 0, 0, 0, 0, 0, 0, 0, 0 },
+ new short[] { 0, 0, 0, 0, 1, 0, 0, 0 },
+ new short[] { 0, 0, 0, 0, 0, 0, 0, 0 },
+ new short[] { 0, 0, 1, 0, 0, 0, 1, 0 },
+ new short[] { 0, 0, 0, 0, 0, 0, 0, 0 }
+ };
+
+ internal static readonly short[][] Shingle = new short[][] {
+ new short[] { 0, 0, 1, 1, 0, 0, 0, 0 },
+ new short[] { 0, 0, 0, 0, 1, 1, 0, 0 },
+ new short[] { 0, 0, 0, 0, 0, 0, 1, 0 },
+ new short[] { 0, 0, 0, 0, 0, 0, 0, 1 },
+ new short[] { 0, 0, 0, 0, 0, 0, 0, 1 },
+ new short[] { 0, 0, 0, 0, 0, 0, 1, 1 },
+ new short[] { 1, 0, 0, 0, 0, 1, 0, 0 },
+ new short[] { 0, 1, 0, 0, 1, 0, 0, 0 }
+ };
+
+ internal static readonly short[][] Trellis = new short[][] {
+ new short[] { 0, 1, 1, 0 },
+ new short[] { 1, 1, 1, 1 },
+ new short[] { 1, 0, 0, 1 },
+ new short[] { 1, 1, 1, 1 }
+ };
+
+ internal static readonly short[][] Sphere = new short[][] {
+ new short[] { 1, 0, 0, 1, 1, 0, 0, 0 },
+ new short[] { 1, 1, 1, 1, 1, 0, 0, 0 },
+ new short[] { 1, 1, 1, 1, 1, 0, 0, 0 },
+ new short[] { 0, 1, 1, 1, 0, 1, 1, 1 },
+ new short[] { 1, 0, 0, 0, 1, 0, 0, 1 },
+ new short[] { 1, 0, 0, 0, 1, 1, 1, 1 },
+ new short[] { 1, 0, 0, 0, 1, 1, 1, 1 },
+ new short[] { 0, 1, 1, 1, 0, 1, 1, 1 }
+ };
+
+ internal static readonly short[][] SmGrid = new short[][] {
+ new short[] { 1, 1, 1, 1 },
+ new short[] { 1, 0, 0, 0 },
+ new short[] { 1, 0, 0 },
+ new short[] { 1, 0, 0, 0 }
+ };
+
+ internal static readonly short[][] LgGrid = new short[][] {
+ new short[] { 1, 1, 1, 1, 1, 1, 1, 1 },
+ new short[] { 1, 0, 0, 0, 0, 0, 0, 0 },
+ new short[] { 1, 0, 0, 0, 0, 0, 0, 0 },
+ new short[] { 1, 0, 0, 0, 0, 0, 0, 0 },
+ new short[] { 1, 0, 0, 0, 0, 0, 0, 0 },
+ new short[] { 1, 0, 0, 0, 0, 0, 0, 0 },
+ new short[] { 1, 0, 0, 0, 0, 0, 0, 0 },
+ new short[] { 1, 0, 0, 0, 0, 0, 0, 0 }
+ };
+
+ internal static readonly short[][] SmCheck = new short[][] {
+ new short[] { 1, 0, 0, 1 },
+ new short[] { 1, 0, 0, 1 },
+ new short[] { 0, 1, 1, 0 },
+ new short[] { 0, 1, 1, 0 }
+ };
+
+ internal static readonly short[][] LgCheck = new short[][] {
+ new short[] { 1, 1, 0, 0, 0, 0, 1, 1 },
+ new short[] { 1, 1, 0, 0, 0, 0, 1, 1 },
+ new short[] { 0, 0, 1, 1, 1, 1, 0, 0 },
+ new short[] { 0, 0, 1, 1, 1, 1, 0, 0 },
+ new short[] { 0, 0, 1, 1, 1, 1, 0, 0 },
+ new short[] { 0, 0, 1, 1, 1, 1, 0, 0 },
+ new short[] { 1, 1, 0, 0, 0, 0, 1, 1 },
+ new short[] { 1, 1, 0, 0, 0, 0, 1, 1 }
+ };
+
+ internal static readonly short[][] OpenDmnd = new short[][] {
+ new short[] { 0, 1, 0, 0, 0, 1, 0, 0 },
+ new short[] { 1, 0, 0, 0, 0, 0, 1, 0 },
+ new short[] { 0, 0, 0, 0, 0, 0, 0, 1 },
+ new short[] { 1, 0, 0, 0, 0, 0, 1, 0 },
+ new short[] { 0, 1, 0, 0, 0, 1, 0, 0 },
+ new short[] { 0, 0, 1, 0, 1, 0, 0, 0 },
+ new short[] { 0, 0, 0, 1, 0, 0, 0, 0 },
+ new short[] { 0, 0, 1, 0, 1, 0, 0, 0 }
+ };
+
+ internal static readonly short[][] SolidDmnd = new short[][] {
+ new short[] { 0, 1, 1, 1, 1, 1, 0, 0 },
+ new short[] { 0, 0, 1, 1, 1, 0, 0, 0 },
+ new short[] { 0, 0, 0, 1, 0, 0, 0, 0 },
+ new short[] { 0, 0, 0, 0, 0, 0, 0, 0 },
+ new short[] { 0, 0, 0, 1, 0, 0, 0, 0 },
+ new short[] { 0, 0, 1, 1, 1, 0, 0, 0 },
+ new short[] { 0, 1, 1, 1, 1, 1, 0, 0 },
+ new short[] { 1, 1, 1, 1, 1, 1, 1, 0 }
+ };
+
+ }
+}
diff --git a/src/EPPlus.DrawingRenderer/Coordinate.cs b/src/EPPlus.DrawingRenderer/Coordinate.cs
new file mode 100644
index 0000000000..7aa56ae67d
--- /dev/null
+++ b/src/EPPlus.DrawingRenderer/Coordinate.cs
@@ -0,0 +1,26 @@
+/*************************************************************************************************
+ 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.DrawingRenderer
+{
+ public class Coordinate
+ {
+ public Coordinate(double x, double y)
+ {
+ X = x;
+ Y = y;
+ }
+ public double X { get; set; }
+ public double Y { get; set; }
+ }
+}
\ No newline at end of file
diff --git a/src/EPPlus.DrawingRenderer/EPPlus.DrawingRenderer.csproj b/src/EPPlus.DrawingRenderer/EPPlus.DrawingRenderer.csproj
new file mode 100644
index 0000000000..7643545c66
--- /dev/null
+++ b/src/EPPlus.DrawingRenderer/EPPlus.DrawingRenderer.csproj
@@ -0,0 +1,41 @@
+
+
+ net8.0;net9.0;net10.0;netstandard2.1;netstandard2.0;net462
+ 9.0.0.0
+ 9.0.0.0
+ 9.0.0.0
+ enable
+ enable
+ EPPlus Software AB
+ EPPlus
+ A spreadsheet library for .NET framework and .NET core
+
+ latest
+ True
+ EPPlus.DrawingRenderer.snk
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ PreserveNewest
+
+
+
+
+
+
+
+
diff --git a/src/EPPlus.DrawingRenderer/EPPlus.DrawingRenderer.snk b/src/EPPlus.DrawingRenderer/EPPlus.DrawingRenderer.snk
new file mode 100644
index 0000000000..8a53d24382
Binary files /dev/null and b/src/EPPlus.DrawingRenderer/EPPlus.DrawingRenderer.snk differ
diff --git a/src/EPPlus.DrawingRenderer/Properties/AssemblyInfo.cs b/src/EPPlus.DrawingRenderer/Properties/AssemblyInfo.cs
new file mode 100644
index 0000000000..5ee4e11884
--- /dev/null
+++ b/src/EPPlus.DrawingRenderer/Properties/AssemblyInfo.cs
@@ -0,0 +1,15 @@
+/*************************************************************************************************
+ 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
+ *************************************************************************************************
+ 01/27/2020 EPPlus Software AB Initial release EPPlus 5
+ *************************************************************************************************/
+using System.Security;
+
+[assembly: AllowPartiallyTrustedCallers]
diff --git a/src/EPPlus.DrawingRenderer/RenderContext.cs b/src/EPPlus.DrawingRenderer/RenderContext.cs
new file mode 100644
index 0000000000..fa2ebd031f
--- /dev/null
+++ b/src/EPPlus.DrawingRenderer/RenderContext.cs
@@ -0,0 +1,51 @@
+using EPPlus.Fonts.OpenType;
+
+namespace EPPlus.DrawingRenderer
+{
+ ///
+ /// Carries rendering-wide resources down the drawing render stack (DrawingRenderer and
+ /// below), independent of output format (SVG, PDF). Owned by the workbook, created once
+ /// per workbook. The font engine is lazy-loaded on first use so constructing the context
+ /// is cheap; the expensive engine (and its font cache) is only built when something is
+ /// actually rendered.
+ ///
+ public class RenderContext : IDisposable
+ {
+ private readonly object _lock = new object();
+ private readonly Func _engineFactory;
+ private OpenTypeFontEngine? _fontEngine;
+
+ public RenderContext(Func engineFactory)
+ {
+ if (engineFactory == null)
+ throw new ArgumentNullException("engineFactory");
+ _engineFactory = engineFactory;
+ }
+
+ public OpenTypeFontEngine FontEngine
+ {
+ get
+ {
+ if (_fontEngine == null)
+ {
+ lock (_lock)
+ {
+ if (_fontEngine == null)
+ _fontEngine = _engineFactory();
+ }
+ }
+ return _fontEngine;
+ }
+ }
+
+
+ public void Dispose()
+ {
+ if (_fontEngine != null)
+ {
+ try { _fontEngine.Dispose(); } catch { /* best effort */ }
+ _fontEngine = null;
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/EPPlus.DrawingRenderer/RenderItems/GradientFill.cs b/src/EPPlus.DrawingRenderer/RenderItems/GradientFill.cs
new file mode 100644
index 0000000000..214a5fc828
--- /dev/null
+++ b/src/EPPlus.DrawingRenderer/RenderItems/GradientFill.cs
@@ -0,0 +1,115 @@
+/*************************************************************************************************
+ 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
+ *************************************************************************************************/
+using EPPlus.Export.ImageRenderer.RenderItems;
+
+using System.Text;
+
+namespace EPPlus.DrawingRenderer.RenderItems
+{
+ ///
+ /// The path for a gradiant color
+ ///
+ public enum ShadePath
+ {
+ ///
+ /// The gradient folows a linear path
+ ///
+ Linear,
+ ///
+ /// The gradient follows a circular path
+ ///
+ Circle,
+ ///
+ /// The gradient follows a rectangular path
+ ///
+ Rectangle,
+ ///
+ /// The gradient follows the shape
+ ///
+ Shape
+ }
+ public class OffsetRectangle : RenderStyle
+ {
+ ///
+ /// Top offset in percentage
+ ///
+ public double TopOffset { get; set; }
+ ///
+ /// Bottom offset in percentage
+ ///
+ public double BottomOffset { get; set; }
+ ///
+ /// Left offset in percentage
+ ///
+ public double LeftOffset { get; set; }
+ ///
+ /// Right offset in percentage
+ ///
+ public double RightOffset { get; set; }
+
+ public override string GetKey()
+ {
+ return $"{TopOffset} {BottomOffset} {LeftOffset} {RightOffset}";
+ }
+ }
+ public class RenderLinearGradientSettings : RenderStyle
+ {
+ public double Angle { get; set; }
+ public bool Scaled { get; set; }
+
+ public override string GetKey()
+ {
+ return $"{Angle} {Scaled}";
+ }
+ }
+ public class RenderGradientFill : RenderStyle
+ {
+ public RenderGradientFill()
+ {
+
+ }
+ public List Colors { get; set; } = new List();
+ public ShadePath ShadePath { get; set; }
+ public OffsetRectangle FocusPoint { get; set; } = new OffsetRectangle();
+ public OffsetRectangle TileRectangle { get; set; } = new OffsetRectangle();
+ public RenderLinearGradientSettings LinearSettings { get; private set; } = new RenderLinearGradientSettings();
+ ///
+ /// If the gradient should use the user space as coordinate system or the bounding box of the item. This is only used for gradient fills and is ignored for other fill types. If true, the gradient will use the user space as coordinate system, if false, the gradient will use the bounding box of the item as coordinate system.
+ ///
+ public bool UserSpaceOnUse { get; set; }
+ public override string GetKey()
+ {
+ var sb = new StringBuilder();
+ foreach(var c in Colors)
+ {
+ sb.Append(c.Color.ToArgb());
+ sb.Append(' ');
+ sb.Append(c.Position);
+ }
+ sb.Append(ShadePath);
+
+ sb.Append(' ');
+
+ sb.Append(FocusPoint.GetKey());
+ sb.Append(' ');
+ sb.Append(LinearSettings.GetKey());
+
+ return sb.ToString();
+ }
+ }
+
+ public abstract class RenderStyle
+ {
+ public abstract string GetKey();
+ }
+}
\ No newline at end of file
diff --git a/src/EPPlus.DrawingRenderer/RenderItems/GradientFillColor.cs b/src/EPPlus.DrawingRenderer/RenderItems/GradientFillColor.cs
new file mode 100644
index 0000000000..413f511b71
--- /dev/null
+++ b/src/EPPlus.DrawingRenderer/RenderItems/GradientFillColor.cs
@@ -0,0 +1,30 @@
+/*************************************************************************************************
+ 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
+ *************************************************************************************************/
+using System.Drawing;
+
+namespace EPPlus.Export.ImageRenderer.RenderItems
+{
+ public class GradientFillColor
+ {
+ public GradientFillColor(double position, Color color)
+ {
+ Position = position;
+ Color = color;
+ }
+
+ public double Position { get; private set; }
+
+ public Color Color { get; private set; }
+ public double Opacity { get; set; }
+ }
+}
diff --git a/src/EPPlus.DrawingRenderer/RenderItems/Interfaces/IBorder.cs b/src/EPPlus.DrawingRenderer/RenderItems/Interfaces/IBorder.cs
new file mode 100644
index 0000000000..4f70d4441a
--- /dev/null
+++ b/src/EPPlus.DrawingRenderer/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.DrawingRenderer/RenderItems/Interfaces/IFill.cs b/src/EPPlus.DrawingRenderer/RenderItems/Interfaces/IFill.cs
new file mode 100644
index 0000000000..f2a1dabed6
--- /dev/null
+++ b/src/EPPlus.DrawingRenderer/RenderItems/Interfaces/IFill.cs
@@ -0,0 +1,10 @@
+using EPPlus.DrawingRenderer;
+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.DrawingRenderer/RenderItems/Interfaces/IRectItem.cs b/src/EPPlus.DrawingRenderer/RenderItems/Interfaces/IRectItem.cs
new file mode 100644
index 0000000000..ef091d7641
--- /dev/null
+++ b/src/EPPlus.DrawingRenderer/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.DrawingRenderer/RenderItems/Interfaces/IRender.cs b/src/EPPlus.DrawingRenderer/RenderItems/Interfaces/IRender.cs
new file mode 100644
index 0000000000..326c9b37aa
--- /dev/null
+++ b/src/EPPlus.DrawingRenderer/RenderItems/Interfaces/IRender.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
+ *************************************************************************************************/
+using System.Text;
+
+namespace EPPlusImageRenderer
+{
+ internal interface IRender
+ {
+ void Render(T renderer);
+ }
+}
\ No newline at end of file
diff --git a/src/EPPlus.DrawingRenderer/RenderItems/Interfaces/IStylingInfoBase.cs b/src/EPPlus.DrawingRenderer/RenderItems/Interfaces/IStylingInfoBase.cs
new file mode 100644
index 0000000000..de110ec790
--- /dev/null
+++ b/src/EPPlus.DrawingRenderer/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.DrawingRenderer/RenderItems/PathCommandType.cs b/src/EPPlus.DrawingRenderer/RenderItems/PathCommandType.cs
new file mode 100644
index 0000000000..9e3e62a4c2
--- /dev/null
+++ b/src/EPPlus.DrawingRenderer/RenderItems/PathCommandType.cs
@@ -0,0 +1,27 @@
+/*************************************************************************************************
+ 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 EPPlusImageRenderer.RenderItems
+{
+ public enum PathCommandType : byte
+ {
+ Move = 0,
+ Line = 1,
+ HorizontalLine = 2,
+ VerticalLine = 3,
+ CubicBézier = 4,
+ QuadraticBézier = 5,
+ Arc = 6,
+ End = 0xFF
+ }
+
+}
\ No newline at end of file
diff --git a/src/EPPlus.DrawingRenderer/RenderItems/PathCommands.cs b/src/EPPlus.DrawingRenderer/RenderItems/PathCommands.cs
new file mode 100644
index 0000000000..224d0de5af
--- /dev/null
+++ b/src/EPPlus.DrawingRenderer/RenderItems/PathCommands.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
+ *************************************************************************************************
+ 27/11/2025 EPPlus Software AB EPPlus 9
+ *************************************************************************************************/
+
+using EPPlus.Fonts.OpenType.Utils;
+using EPPlusImageRenderer.RenderItems;
+using OfficeOpenXml;
+using System;
+using System.Globalization;
+using System.Text;
+
+namespace EPPlusImageRenderer
+{
+ public class PathCommands
+ {
+ public PathCommands(PathCommandType type, params double[] coordinates)
+ {
+ Type = type;
+ Coordinates = coordinates;
+ }
+ //public SvgRenderItem RenderItem{ get; set;}
+ public PathCommandType Type { get; }
+ public double[] Coordinates { get; set; }
+ public PathCommands Clone()
+ {
+ return new PathCommands(Type)
+ {
+ Coordinates = (double[])Coordinates.Clone(),
+ };
+ }
+ }
+
+}
\ No newline at end of file
diff --git a/src/EPPlus.DrawingRenderer/RenderItems/PresetShapeRenderItem.cs b/src/EPPlus.DrawingRenderer/RenderItems/PresetShapeRenderItem.cs
new file mode 100644
index 0000000000..277ab004ab
--- /dev/null
+++ b/src/EPPlus.DrawingRenderer/RenderItems/PresetShapeRenderItem.cs
@@ -0,0 +1,17 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+
+namespace EPPlus.DrawingRenderer.RenderItems
+{
+ //internal class PresetShapeRenderItem : RenderItem
+ //{
+ // internal PresetShapeRenderItem(eShapeStyle style, double top, double left, double width, double height, eTextAutofit autofit)
+ // {
+
+ // }
+ // public override RenderItemType Type => RenderItemType.Group;
+ //}
+}
diff --git a/src/EPPlus.DrawingRenderer/RenderItems/RenderBlipFill.cs b/src/EPPlus.DrawingRenderer/RenderItems/RenderBlipFill.cs
new file mode 100644
index 0000000000..5a299f9d4f
--- /dev/null
+++ b/src/EPPlus.DrawingRenderer/RenderItems/RenderBlipFill.cs
@@ -0,0 +1,130 @@
+/*************************************************************************************************
+ 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
+ *************************************************************************************************/
+using EPPlus.Graphics;
+using System.Security.Cryptography;
+
+namespace EPPlus.DrawingRenderer.RenderItems
+{
+ ///
+ /// Describes how to position two rectangles relative to each other
+ ///
+ public enum RectangleAlignment
+ {
+ ///
+ /// Bottom
+ ///
+ Bottom,
+ ///
+ /// Bottom Left
+ ///
+ BottomLeft,
+ ///
+ /// Bottom Right
+ ///
+ BottomRight,
+ ///
+ /// Center
+ ///
+ Center,
+ ///
+ /// Left
+ ///
+ Left,
+ ///
+ /// Right
+ ///
+ Right,
+ ///
+ /// Top
+ ///
+ Top,
+ ///
+ /// TopLeft
+ ///
+ TopLeft,
+ ///
+ /// TopRight
+ ///
+ TopRight
+ }
+ public enum TileFlipMode
+ {
+ ///
+ /// Tiles are not flipped
+ ///
+ None,
+ ///
+ /// Tiles are flipped horizontally.
+ ///
+ X,
+ ///
+ /// Tiles are flipped horizontally and Vertically
+ ///
+ XY,
+ ///
+ /// Tiles are flipped vertically.
+ ///
+ Y
+ }
+ public class FillTile : RenderStyle
+ {
+ ///
+ /// The direction(s) in which to flip the image.
+ ///
+ public TileFlipMode? FlipMode { get; set; }
+ ///
+ /// Where to align the first tile with respect to the shape.
+ ///
+ public RectangleAlignment? Alignment { get; set; }
+ ///
+ /// The ratio for horizontally scale
+ ///
+ public double HorizontalRatio { get; set; }
+ ///
+ /// The ratio for vertically scale
+ ///
+ public double VerticalRatio { get; set; }
+ ///
+ /// The horizontal offset after alignment
+ ///
+ public double HorizontalOffset { get; set; }
+ ///
+ /// The vertical offset after alignment
+ ///
+ public double VerticalOffset { get; set; }
+
+ public override string GetKey()
+ {
+ return $"{FlipMode} {Alignment} {HorizontalRatio} {VerticalRatio} {HorizontalOffset} {VerticalOffset}";
+ }
+ }
+ public class RenderBlipFill : RenderStyle
+ {
+ internal static SHA256 sha = SHA256.Create();
+ public BoundingBox ImageBounds { get; set; } = new BoundingBox();
+ public string ContentType { get; set; }
+ public byte[] ImageBytes { get; set; }
+ ///
+ /// The image should be stretched to fill the target.
+ ///
+ public bool Stretch { get; set; } = false;
+ public OffsetRectangle StretchOffset{ get; set; }
+ public FillTile Tile{ get;set;}
+
+ public override string GetKey()
+ {
+ var imageHash = Convert.ToBase64String(sha.ComputeHash(ImageBytes));
+ return $"{ContentType} {imageHash} {Stretch} {StretchOffset.GetKey()} {Tile.GetKey()}";
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/EPPlus.DrawingRenderer/RenderItems/RenderItem.cs b/src/EPPlus.DrawingRenderer/RenderItems/RenderItem.cs
new file mode 100644
index 0000000000..bd46e6b0c0
--- /dev/null
+++ b/src/EPPlus.DrawingRenderer/RenderItems/RenderItem.cs
@@ -0,0 +1,425 @@
+/*************************************************************************************************
+ 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
+ *************************************************************************************************/
+using EPPlus.Fonts.OpenType.Utils;
+using EPPlus.Graphics;
+using EPPlus.Graphics.Geometry;
+using EPPlusImageRenderer;
+using EPPlusImageRenderer.RenderItems;
+using System.Drawing;
+
+namespace EPPlus.DrawingRenderer.RenderItems
+{
+ public enum FillType
+ {
+ SolidFill,
+ GradientFill,
+ PatternFill
+ }
+ ///
+ /// The compound line type. Used for underlining text
+ ///
+ public enum CompoundLineStyle
+ {
+ ///
+ /// Double lines with equal width
+ ///
+ Double,
+ ///
+ /// Single line normal width
+ ///
+ Single,
+ ///
+ /// Double lines, one thick, one thin
+ ///
+ DoubleThickThin,
+ ///
+ /// Double lines, one thin, one thick
+ ///
+ DoubleThinThick,
+ ///
+ /// Three lines, thin, thick, thin
+ ///
+ TripleThinThickThin
+ }
+ public enum LineCap
+ {
+ ///
+ /// A flat line cap
+ ///
+ Flat, //flat
+ ///
+ /// A round line cap
+ ///
+ Round,
+ ///
+ /// A Square line cap
+ ///
+ Square
+ }
+
+ public enum LineJoin
+ {
+ Arcs,
+ Bevel,
+ Miter,
+ MiterClip,
+ Round
+ }
+ public class UseReferenceRenderItem : RenderItem
+ {
+ public UseReferenceRenderItem(BoundingBox parent, string hRef) : base(parent)
+ {
+ Href = hRef;
+ }
+ public string Href { get; private set; }
+
+ public override RenderItemType Type => RenderItemType.UseReference;
+
+ public double X
+ {
+ get
+ {
+ return Bounds.Left;
+ }
+ set
+ {
+ Bounds.Left = value;
+ }
+ }
+ public double Y
+ {
+ get
+ {
+ return Bounds.Top;
+ }
+ set
+ {
+ Bounds.Top = value;
+ }
+ }
+ }
+ public class RectRenderItem : RenderItem
+ {
+ public RectRenderItem(BoundingBox parent) : base(parent)
+ {
+
+ }
+ public override RenderItemType Type => RenderItemType.Rect;
+ 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; } }
+ public double Height { get { return Bounds.Height; } set { Bounds.Height = value; } }
+ public double Right { get { return Bounds.Left + Width; } }
+ public double Bottom { get { return Bounds.Top + Height; } }
+ public double GlobalLeft => Bounds.GlobalLeft;
+ public double GlobalTop => Bounds.GlobalTop;
+ public double GlobalRight => Bounds.GlobalLeft + Width;
+ public double GlobalBottom => Bounds.GlobalTop + Height;
+ }
+ public class GroupRenderItem : RenderItem
+ {
+ public GroupRenderItem(BoundingBox parent) : base(parent)
+ {
+ }
+ public GroupRenderItem(BoundingBox parent, double rotation) : base(parent)
+ {
+ Rotation = rotation;
+ }
+
+ public GroupRenderItem() : base()
+ {
+ Bounds.Parent = TranslationOffset;
+ }
+
+ public GroupRenderItem(double localXPos, double localYPos) : this()
+ {
+ TranslationOffset = new Graphics.Point(localXPos, localYPos);
+ }
+
+
+ public GroupRenderItem(BoundingBox parent, double rotation, Transform rotationPoint = null) : this(0, 0)
+ {
+ TranslationOffset.Parent = parent;
+ Rotation = rotation;
+ if (rotationPoint != null)
+ {
+ RotationPoint = new Graphics.Point(rotationPoint.LocalPosition.X, rotationPoint.LocalPosition.Y);
+ }
+ }
+ ///
+ /// The top position of the group. This is the position of the group relative to its parent. The child items are positioned relative to this position.
+ ///
+ public double Top { get { return Bounds.Top; } set { Bounds.Top = value; } }
+ ///
+ /// The left position of the group. This is the position of the group relative to its parent. The child items are positioned relative to this position.
+ ///
+ public double Left { get { return Bounds.Left; } set { Bounds.Left = value; } }
+ public override RenderItemType Type => RenderItemType.Group;
+ public string TextAnchor { get; set; }
+ public double Rotation { get; set; }
+ public string GroupTransform = "";
+ public List RenderItems { get; } = new List();
+
+ Graphics.Point _altRotationPoint = null;
+ ///
+ /// The translated position of this item in points
+ /// Also the parent position of the group item
+ /// (This may seem strange but it ensures the the translation is seen
+ /// immediately in the global position of GroupItem without affecting local position)
+ ///
+ public Graphics.Point TranslationOffset = new Graphics.Point(0, 0);
+ public Graphics.Point RotationPoint
+ {
+ get
+ {
+ if (_altRotationPoint == null)
+ {
+ return TranslationOffset;
+ }
+ return _altRotationPoint;
+ }
+ set
+ {
+ _altRotationPoint = value;
+ }
+ }
+
+ public Coordinate Scale = null;
+
+ internal void SetRotationPointToCenterOfGroup(double rotation = double.NaN)
+ {
+ RotationPoint = new Graphics.Point(Bounds.Width / 2, Bounds.Height / 2);
+
+ if (double.IsNaN(rotation) == false)
+ {
+ Rotation = rotation;
+ }
+ }
+
+ public void AddChildItem(RenderItem item)
+ {
+ item.Bounds.Parent = TranslationOffset;
+ RenderItems.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 class PathRenderItem : RenderItem
+ {
+ public override RenderItemType Type => RenderItemType.Path;
+ public PathRenderItem(BoundingBox parent) : base(parent)
+ {
+
+ }
+ public List Commands { get; } = new List();
+ }
+ public class EllipseRenderItem : RenderItem
+ {
+ public EllipseRenderItem(BoundingBox parent) : base(parent)
+ {
+
+ }
+ public override RenderItemType Type => RenderItemType.Ellipse;
+ public double Cx { get; set; }
+ public double Cy { get; set; }
+ public double Rx { get; set; }
+ public double Ry { get; set; }
+ }
+ public class LineRenderItem : RenderItem
+ {
+ public LineRenderItem(BoundingBox parent) : base(parent)
+ {
+
+ }
+ double _x1, _y1, _x2, _y2;
+ public double X1
+ {
+ get
+ {
+ return _x1;
+ }
+ set
+ {
+ _x1 = value;
+ UpdateBounds();
+ }
+ }
+ public double Y1
+ {
+ get
+ {
+ return _y1;
+ }
+ set
+ {
+ _y1 = value;
+ UpdateBounds();
+ }
+ }
+ public double X2
+ {
+ get
+ {
+ return _x2;
+ }
+ set
+ {
+ _x2 = value;
+ UpdateBounds();
+ }
+ }
+ public double Y2
+ {
+ get
+ {
+ return _y2;
+ }
+ set
+ {
+ _y2 = value;
+ UpdateBounds();
+ }
+ }
+ private void UpdateBounds()
+ {
+ var px = Math.Min(X1, X2);
+ var py = Math.Min(Y1, Y2);
+ var sizeX = Math.Abs(X2 - X1);
+ var sizeY = Math.Abs(Y2 - Y1);
+
+ Bounds.Position = new Vector2(px, py);
+ Bounds.Size = new Vector2(sizeX, sizeY);
+ }
+ public LineRenderItem Clone()
+ {
+ var clone = new LineRenderItem((BoundingBox)Bounds.Parent);
+ CloneBase(clone);
+ clone._x1 = X1;
+ clone._y1 = Y1;
+ clone._x2 = X2;
+ clone._y2 = Y2;
+ clone.UpdateBounds();
+ return clone;
+ }
+ public override RenderItemType Type => RenderItemType.Line;
+ }
+ public abstract class DrawingObject
+ {
+ public virtual void AppendRenderItems(List renderItems) { }
+ }
+ public abstract class RenderItem : RenderItemBase
+ {
+ protected RenderItem()
+ {
+ }
+ protected RenderItem(BoundingBox parent)
+ {
+ Bounds.Parent = parent;
+ }
+ //internal abstract void GetBounds(out double il, out double it, out double ir, out double ib);
+ public virtual void GetBounds(out double il, out double it, out double ir, out double ib)
+ {
+ il = Bounds.Left;
+ it = Bounds.Top;
+ ir = Bounds.Right;
+ ib = Bounds.Bottom;
+ }
+ public string DefId { get; set; }
+ //internal bool IsEndOfGroup { get; set; } = false;
+ public string FillColor { get; set; }
+ public string FilterName { get; set; }
+ public RenderGradientFill GradientFill { get; set; }
+ public FillType FillType { get; set; }
+ public double? FillOpacity { get; set; }
+ public string BorderColor { get; set; }
+ public RenderGradientFill BorderGradientFill { get; set; }
+ public RenderPatternFill PatternFill { get; set; }
+ public RenderBlipFill BlipFill { get; set; }
+ public double? BorderWidth { get; set; }
+ public double[] BorderDashArray { get; set; }
+ public int? StrokeMiterLimit { get; set; }
+ public CompoundLineStyle CompoundLineStyle { get; set; } = CompoundLineStyle.Single;
+ public double? BorderDashOffset { get; set; }
+ public LineCap LineCap { get; set; } = LineCap.Flat;
+ public LineJoin LineJoin { get; set; } = LineJoin.Miter;
+ public double? BorderOpacity { get; set; }
+ public PathFillMode FillColorSource { get; set; } = PathFillMode.Norm;
+ public PathFillMode BorderColorSource { get; set; } = PathFillMode.Norm;
+ public double? GlowRadius { get; set; }
+ public string GlowColor { get; set; }
+ public RenderShadowEffect OuterShadowEffect { get; set; }
+
+ ///
+ /// The origin point for any transform actions in svg.
+ /// Normally/Default 0,0
+ ///
+ public Coordinate TransformOrigin { get; set; } = null;
+
+ 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 void GetOuterShadowColor(out string shadowColor, out double opacity)
+ {
+ if (OuterShadowEffect == null)
+ {
+ shadowColor = null;
+ opacity = 0;
+
+ }
+ else
+ {
+ var tc = OuterShadowEffect.OuterShadowEffectColor;
+ if (tc.A < 255 && tc != Color.Empty)
+ {
+ opacity = tc.A / 255D;
+ }
+ else
+ {
+ opacity = 1;
+ }
+ shadowColor = "#" + tc.ToArgb().ToString("x8").Substring(2);
+ }
+ }
+
+ internal string GetFilterKey()
+ {
+ return $"{GlowColor} {GlowRadius} { OuterShadowEffect?.GetKey()}";
+ }
+ }
+ ///
+ /// Base class for any item rendered.
+ ///
+ public abstract class RenderItemBase
+ {
+ public BoundingBox Bounds = new BoundingBox();
+ public abstract RenderItemType Type { get; }
+ public virtual void GetBounds(out double il, out double it, out double ir, out double ib)
+ {
+ il = Bounds.Left;
+ it = Bounds.Top;
+ ir = Bounds.Right;
+ ib = Bounds.Bottom;
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/EPPlus.DrawingRenderer/RenderItems/RenderPatternFill.cs b/src/EPPlus.DrawingRenderer/RenderItems/RenderPatternFill.cs
new file mode 100644
index 0000000000..57b9dd2755
--- /dev/null
+++ b/src/EPPlus.DrawingRenderer/RenderItems/RenderPatternFill.cs
@@ -0,0 +1,250 @@
+/*************************************************************************************************
+ 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
+ *************************************************************************************************/
+using EPPlus.DrawingRenderer.RenderItems;
+using System.Drawing;
+
+namespace EPPlus.DrawingRenderer.RenderItems
+{
+ ///
+ /// Pattern styles for drawing fills
+ ///
+ public enum FillPatternStyle
+ {
+ ///
+ /// 5 Percent
+ ///
+ Pct5,
+ ///
+ /// 10 Percent
+ ///
+ Pct10,
+ ///
+ /// 20 Percent
+ ///
+ Pct20,
+ ///
+ /// 25 Percent
+ ///
+ Pct25,
+ ///
+ /// 30 Percent
+ ///
+ Pct30,
+ ///
+ /// 40 Percent
+ ///
+ Pct40,
+ ///
+ /// 50 Percent
+ ///
+ Pct50,
+ ///
+ /// 60 Percent
+ ///
+ Pct60,
+ ///
+ /// 70 Percent
+ ///
+ Pct70,
+ ///
+ /// 75 Percent
+ ///
+ Pct75,
+ ///
+ /// 80 Percent
+ ///
+ Pct80,
+ ///
+ /// 90 Percent
+ ///
+ Pct90,
+ ///
+ /// Horizontal
+ ///
+ Horz,
+ ///
+ /// Vertical
+ ///
+ Vert,
+ ///
+ /// Light Horizontal
+ ///
+ LtHorz,
+ ///
+ /// Light Vertical
+ ///
+ LtVert,
+ ///
+ /// Dark Horizontal
+ ///
+ DkHorz,
+ ///
+ /// Dark Vertical
+ ///
+ DkVert,
+ ///
+ /// Narrow Horizontal
+ ///
+ NarHorz,
+ ///
+ /// Narrow Vertical
+ ///
+ NarVert,
+ ///
+ /// Dashed Horizontal
+ ///
+ DashHorz,
+ ///
+ /// Dashed Vertical
+ ///
+ DashVert,
+ ///
+ /// Cross
+ ///
+ Cross,
+ ///
+ /// Downward Diagonal
+ ///
+ DnDiag,
+ ///
+ /// Upward Diagonal
+ ///
+ UpDiag,
+ ///
+ /// Light Downward Diagonal
+ ///
+ LtDnDiag,
+ ///
+ /// Light Upward Diagonal
+ ///
+ LtUpDiag,
+ ///
+ /// Dark Downward Diagonal
+ ///
+ DkDnDiag,
+ ///
+ /// Dark Upward Diagonal
+ ///
+ DkUpDiag,
+ ///
+ /// Wide Downward Diagonal
+ ///
+ WdDnDiag,
+ ///
+ /// Wide Upward Diagonal
+ ///
+ WdUpDiag,
+ ///
+ /// Dashed Downward Diagonal
+ ///
+ DashDnDiag,
+ ///
+ /// Dashed Upward DIagonal
+ ///
+ DashUpDiag,
+ ///
+ /// Diagonal Cross
+ ///
+ DiagCross,
+ ///
+ /// Small Checker Board
+ ///
+ SmCheck,
+ ///
+ /// Large Checker Board
+ ///
+ LgCheck,
+ ///
+ /// Small Grid
+ ///
+ SmGrid,
+ ///
+ /// Large Grid
+ ///
+ LgGrid,
+ ///
+ /// Dotted Grid
+ ///
+ DotGrid,
+ ///
+ /// Small Confetti
+ ///
+ SmConfetti,
+ ///
+ /// Large Confetti
+ ///
+ LgConfetti,
+ ///
+ /// Horizontal Brick
+ ///
+ HorzBrick,
+ ///
+ /// Diagonal Brick
+ ///
+ DiagBrick,
+ ///
+ /// Solid Diamond
+ ///
+ SolidDmnd,
+ ///
+ /// Open Diamond
+ ///
+ OpenDmnd,
+ ///
+ /// Dotted Diamond
+ ///
+ DotDmnd,
+ ///
+ /// Plaid
+ ///
+ Plaid,
+ ///
+ /// Sphere
+ ///
+ Sphere,
+ ///
+ /// Weave
+ ///
+ Weave,
+ ///
+ /// Divot
+ ///
+ Divot,
+ ///
+ /// Shingle
+ ///
+ Shingle,
+ ///
+ /// Wave
+ ///
+ Wave,
+ ///
+ /// Trellis
+ ///
+ Trellis,
+ ///
+ /// Zig Zag
+ ///
+ ZigZag
+ }
+}
+public class RenderPatternFill : RenderStyle
+{
+ public FillPatternStyle PatternType { get; set; }
+ public Color ForegroundColor { get; set; }
+ public Color BackgroundColor { get; set; }
+ public override string GetKey()
+ {
+ return $"{PatternType}|{ForegroundColor.ToArgb()} {BackgroundColor.ToArgb()}";
+ }
+}
\ No newline at end of file
diff --git a/src/EPPlus.DrawingRenderer/RenderItems/RenderShadowEffect.cs b/src/EPPlus.DrawingRenderer/RenderItems/RenderShadowEffect.cs
new file mode 100644
index 0000000000..b21b8b2c15
--- /dev/null
+++ b/src/EPPlus.DrawingRenderer/RenderItems/RenderShadowEffect.cs
@@ -0,0 +1,29 @@
+/*************************************************************************************************
+ 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
+ *************************************************************************************************/
+using System.Drawing;
+
+namespace EPPlus.DrawingRenderer.RenderItems
+{
+ public class RenderShadowEffect : RenderStyle
+ {
+ public Color OuterShadowEffectColor { get; set; }
+ public double Distance { get; set; }
+ public double? BlurRadius { get; set; }
+ public double? Direction { get; set; }
+
+ public override string GetKey()
+ {
+ return $"{OuterShadowEffectColor.ToArgb()} {Distance} {BlurRadius} {Direction}";
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/EPPlus.DrawingRenderer/RenderItems/Shared/GroupItem.cs b/src/EPPlus.DrawingRenderer/RenderItems/Shared/GroupItem.cs
new file mode 100644
index 0000000000..1c86273a2b
--- /dev/null
+++ b/src/EPPlus.DrawingRenderer/RenderItems/Shared/GroupItem.cs
@@ -0,0 +1,102 @@
+using EPPlus.Graphics;
+using EPPlusImageRenderer;
+using EPPlusImageRenderer.RenderItems;
+using System.Collections.Generic;
+
+namespace EPPlus.DrawingRenderer.RenderItems
+{
+ internal abstract class GroupItem : RenderItem
+ {
+ ///
+ /// In degrees
+ ///
+ internal double Rotation = double.NaN;
+ ///
+ /// The translated position of this item in points
+ /// Also the parent position of the group item
+ /// (This may seem strange but it ensures the the translation is seen
+ /// immediately in the global position of GroupRenderItemBaseNew without affecting local position)
+ ///
+ internal Point TranslationOffset = new Point(0,0);
+
+ Point _altRotationPoint = null;
+
+ internal Point RotationPoint
+ {
+ get
+ {
+ if (_altRotationPoint == null)
+ {
+ return TranslationOffset;
+ }
+ return _altRotationPoint;
+ }
+ set
+ {
+ _altRotationPoint = value;
+ }
+ }
+
+ internal Coordinate Scale = null;
+
+
+ //Transform _rotationPoint;
+
+ ///
+ /// Items contained in this group
+ ///
+ internal protected List _childItems = new List();
+
+ public GroupItem(DrawingRenderer renderer) : base(renderer)
+ {
+ Bounds.Parent = TranslationOffset;
+ //_rotationPoint = Bounds;
+ }
+
+ public GroupItem(DrawingRenderer renderer, double localXPos, double localYPos) : this(renderer)
+ {
+ Bounds.Left = localXPos;
+ Bounds.Top = localYPos;
+ }
+
+
+ public GroupItem(DrawingBase renderer, BoundingBox parent, double rotation, Transform rotationPoint = null) : this(renderer, 0, 0)
+ {
+ TranslationOffset.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)
+ {
+ if (item is GroupItem)
+ {
+ var subGroup = (GroupItem)item;
+ subGroup.TranslationOffset.Parent = Bounds;
+ }
+ else
+ {
+ item.Bounds.Parent = Bounds;
+ }
+ _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.DrawingRenderer/RenderItems/Shared/InnerGroup.cs b/src/EPPlus.DrawingRenderer/RenderItems/Shared/InnerGroup.cs
new file mode 100644
index 0000000000..ea31127a57
--- /dev/null
+++ b/src/EPPlus.DrawingRenderer/RenderItems/Shared/InnerGroup.cs
@@ -0,0 +1,31 @@
+using EPPlus.Graphics;
+using EPPlusImageRenderer;
+using EPPlusImageRenderer.RenderItems;
+using System.Collections.Generic;
+
+namespace EPPlus.DrawingRenderer.RenderItems
+{
+ internal abstract class InnerGroup : RenderItem
+ {
+ ///
+ /// Items contained in this group
+ ///
+ internal protected List _childItems = new List();
+
+ public InnerGroup(DrawingRenderer renderer) : base(renderer)
+ {
+ }
+
+ internal void AddChildItem(RenderItem item)
+ {
+ item.Bounds.Parent = Bounds;
+
+ _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.DrawingRenderer/RenderItems/Shared/ParagraphItem.cs b/src/EPPlus.DrawingRenderer/RenderItems/Shared/ParagraphItem.cs
new file mode 100644
index 0000000000..a932176a67
--- /dev/null
+++ b/src/EPPlus.DrawingRenderer/RenderItems/Shared/ParagraphItem.cs
@@ -0,0 +1,581 @@
+using EPPlus.Fonts.OpenType;
+using EPPlus.Fonts.OpenType.Integration;
+using EPPlus.Fonts.OpenType.TextShaping;
+using EPPlus.Fonts.OpenType.Utils;
+using EPPlus.Graphics;
+using EPPlusImageRenderer.Utils;
+using OfficeOpenXml.Interfaces.Drawing.Text;
+
+namespace EPPlus.DrawingRenderer.RenderItems
+{
+ public enum DrawingTextLineSpacing
+ {
+ ///
+ /// Single line spacing
+ ///
+ Single,
+ ///
+ /// 1.5 lines
+ ///
+ OneAndAHalf,
+ ///
+ /// Double line spacing
+ ///
+ Double,
+ ///
+ /// Exact point spacing
+ ///
+ Exactly,
+ ///
+ /// Multiple line spacing
+ ///
+ Multiple
+ }
+ ///
+ /// Text alignment
+ ///
+ public enum TextAlignment
+ {
+ ///
+ /// Left alignment
+ ///
+ Left,
+ ///
+ /// Center alignment
+ ///
+ Center,
+ ///
+ /// Right alignment
+ ///
+ Right,
+ ///
+ /// Distributes the text words across an entire text line
+ ///
+ Distributed,
+ ///
+ /// Align text so that it is justified across the whole line.
+ ///
+ Justified,
+ ///
+ /// Aligns the text with an adjusted kashida length for Arabic text
+ ///
+ JustifiedLow,
+ ///
+ /// Distributes Thai text specially, specially, because each character is treated as a word
+ ///
+ ThaiDistributed
+ }
+
+ internal abstract class ParagraphItem : RenderItem
+ {
+ TextLayoutEngine _layout;
+
+ double _leftMargin;
+ double _rightMargin;
+
+ DrawingTextLineSpacing _lsType;
+ double _lineSpacingAscendantOnly;
+ double? _lsMultiplier = null;
+ internal bool IsFirstParagraph { get; private set; }
+ List _paragraphLines = new List();
+ protected List _textRunDisplayText = new List();
+
+ List _newTextFragments;
+ int _manualFragmentsStartIndex = -1;
+ List _manualFragments;
+ internal protected MeasurementFont _paragraphFont;
+ internal TextBodyItem ParentTextBody { get; set; }
+ internal double ParagraphLineSpacing { get; private set; }
+ internal TextAlignment HorizontalAlignment { get; private set; }
+ internal List Runs { get; set; } = new List();
+
+ internal bool DisplayBounds { get; set; } = false;
+
+ private List _lines;
+
+ //Start temp workaround vars
+ string _textIfEmpty = null;
+ ExcelDrawingParagraph _p = null;
+ //end temp workaround vars
+
+ private double? _centerAdjustment = null;
+
+ internal List SpaceWidthsPerLine = new List();
+
+ bool LinespacingIsExact
+ {
+ get
+ {
+ return _lsMultiplier.HasValue == false;
+ }
+ }
+
+ 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;
+
+ _layout = OpenTypeFonts.GetTextLayoutEngineForFont(defaultFont);
+ ParagraphLineSpacing = GetParagraphLineSpacingInPoints(100, (TextShaper)OpenTypeFonts.GetShaperForFont(defaultFont), defaultFont.Size);
+ }
+
+ public ParagraphItem(TextBodyItem textBody, DrawingBase renderer, BoundingBox parent, ExcelDrawingParagraph p, string textIfEmpty = null) : base(renderer, parent)
+ {
+ ParentTextBody = textBody;
+ IsFirstParagraph = p == p._paragraphs[0];
+
+ if (p.DefaultRunProperties.Fill != null && p.DefaultRunProperties.Fill.IsEmpty == false)
+ {
+ if (IsFirstParagraph)
+ {
+ 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)
+ {
+ SetDrawingPropertiesFill(p.DefaultRunProperties.Fill, null);
+ }
+ else
+ {
+ var fc = EPPlusColorConverter.GetThemeColor(DrawingRenderer.Theme.ColorScheme.Light1);
+ fc = ColorUtils.GetAdjustedColor(PathFillMode.Norm, fc);
+ FillColor = "#" + fc.ToArgb().ToString("x8").Substring(2);
+ //Use shape fill somehow
+ //Maybe use a name property for fallback theme accent1 color?
+ }
+ }
+ }
+ 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";
+
+ var indent = 48 * p.IndentLevel;
+ _leftMargin = p.LeftMargin + p.Indent + indent;
+ _rightMargin = p.RightMargin;
+
+ _leftMargin = _leftMargin.PixelToPoint();
+ _rightMargin = _rightMargin.PixelToPoint();
+
+ HorizontalAlignment = p.HorizontalAlignment;
+ _leftMargin = _leftMargin.PixelToPoint();
+ _rightMargin = _rightMargin.PixelToPoint();
+
+ HorizontalAlignment = p.HorizontalAlignment;
+
+ if (ParentTextBody.AutoSize == false)
+ {
+ 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 != TextAlignment.Center)
+ {
+ Bounds.Left = GetAlignmentHorizontal(HorizontalAlignment);
+ }
+ else
+ {
+ //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(TextAlignment.Left);
+ _centerAdjustment = GetAlignmentHorizontal(HorizontalAlignment);
+
+ }
+ Bounds.Width = parent.Width - _rightMargin - _leftMargin;
+ }
+
+ //---Initialize / calculate lines and runs---
+ //measurer must be set before AddLinesAndRichText
+ _paragraphFont = p.DefaultRunProperties.GetMeasureFont();
+
+ //---Get measurer---
+ _layout = OpenTypeFonts.GetTextLayoutEngineForFont(_paragraphFont);
+
+ //---Calculate linespacing---
+ int numLines = _paragraphLines.Count;
+ _lsType = p.LineSpacing.LineSpacingType;
+ ParagraphLineSpacing = GetParagraphLineSpacingInPoints(p.LineSpacing.Value,
+ (TextShaper) OpenTypeFonts.GetShaperForFont(_paragraphFont),
+ _paragraphFont.Size);
+
+
+ ImportLinesAndTextRuns(p, textIfEmpty);
+ }
+
+ private double GetParagraphLineSpacingInPoints(double spacingValue, TextShaper fmExact, float fontSize)
+ {
+ if (_lsType == DrawingTextLineSpacing.Exactly)
+ {
+ if (IsFirstParagraph)
+ {
+ _lineSpacingAscendantOnly = spacingValue;
+ }
+ return spacingValue;
+ }
+ else
+ {
+ var multiplier = (spacingValue / 100);
+ _lsMultiplier = multiplier;
+ if (IsFirstParagraph)
+ {
+ _lineSpacingAscendantOnly = multiplier * fmExact.GetAscentInPoints(fontSize);
+ }
+ return multiplier * fmExact.GetLineHeightInPoints(fontSize);
+ }
+ }
+
+ //public void AddOwnText(string text)
+ //{
+ // var fragment = new TextFragment();
+ // fragment.Text = text;
+ // fragment.Font = ParagraphFont;
+ // _manualFragments.Add(fragment);
+
+ // //if(_newTextFragments == null)
+ // //{
+ // // //This should probably never happen
+ // // throw new InvalidOperationException("Must GENERATE textfragments first in the constructor");
+ // // //GenerateTextFragments(text);
+ // //}
+ // //else
+ // //{
+ // // var fragment = new TextFragment();
+ // // fragment.Text = text;
+ // // fragment.Font = ParagraphFont;
+ // // //_newTextFragments.Add(fragment);
+ // // _manualFragments.Add(fragment);
+ // //}
+
+ // //Redo whole thing for now.
+ // //Import and wrapping really should be completely seperated but can't refactor all of it yet
+ // //AddTextLinesAndSpacing(_p, _textIfEmpty);
+ //}
+
+ public void AddOwnText(TextFragment fragment)
+ {
+ //_manualFragments.Add(fragment);
+
+ if (_newTextFragments == null)
+ {
+ //This should probably never happen
+ throw new InvalidOperationException("Must GENERATE textfragments first in the constructor");
+ //GenerateTextFragments(text);
+ }
+ else
+ {
+ if(_manualFragmentsStartIndex == -1)
+ {
+ _manualFragmentsStartIndex = _newTextFragments.Count;
+ }
+ //_newTextFragments.Add(fragment);
+ _newTextFragments.Add(fragment);
+ }
+
+
+ //Redo whole thing for now.
+ //Import and wrapping really should be completely seperated but can't refactor all of it yet
+ AddTextLinesAndSpacing(_p, _textIfEmpty);
+ }
+
+
+ internal protected TextRunItem AddRenderItemTextRun(ExcelParagraphTextRunBase origTxtRun, string displayText)
+ {
+ var targetTxtRun = CreateTextRun(origTxtRun, Bounds, displayText);
+
+ Runs.Add(targetTxtRun);
+ return targetTxtRun;
+ }
+
+ private void AddText(string text, MeasurementFont font)
+ {
+ var container = CreateTextRun(font, Bounds, text);
+ Runs.Add(container);
+
+ container.Bounds.Name = $"Container{Runs.Count}";
+ }
+
+ private void AddText(string text, ExcelTextFont font)
+ {
+ var mf = font.GetMeasureFont();
+ var measurer = OpenTypeFonts.GetTextLayoutEngineForFont(mf);
+
+ var container = CreateTextRun(text, font, Bounds, text);
+ Runs.Add(container);
+ //Bounds.Width = container.Bounds.Width + 0.001; //TODO: fix for equal width issue
+ container.Bounds.Name = $"Container{Runs.Count}";
+ }
+
+ void GenerateTextFragments(string text)
+ {
+ _newTextFragments = new List();
+
+ if (string.IsNullOrEmpty(text) == false)
+ {
+ var currentFrag = new TextFragment() { Text = text, Font = _paragraphFont};
+ _newTextFragments.Add(currentFrag);
+ }
+ }
+
+ ///
+ /// Log linebreak positions and sizes of the runs
+ /// So that we can easily know what textfragment is on what line and what size it has later
+ ///
+ ///
+ void GenerateTextFragments(ExcelDrawingTextRunCollection runs/*, string textIfEmpty*/)
+ {
+ List runContents = new List();
+ List fonts = new List();
+
+ for (int i = 0; i < runs.Count(); i++)
+ {
+ var txtRun = runs[i];
+ var runFont = txtRun.GetMeasurementFont();
+
+ fonts.Add(runFont);
+ runContents.Add(txtRun.Text);
+ }
+
+ _newTextFragments = new List();
+
+ for (int i = 0; i < runContents.Count(); i++)
+ {
+ if (string.IsNullOrEmpty(runContents[i]) == false)
+ {
+ var currentFrag = new TextFragment() { Text = runContents[i], Font = fonts[i] };
+ _newTextFragments.Add(currentFrag);
+ }
+ }
+ }
+
+ internal void ImportLinesAndTextRunsDefault(string textIfEmpty)
+ {
+ GenerateTextFragments(textIfEmpty);
+
+ if (HorizontalAlignment != TextAlignment.Center)
+ {
+ Bounds.Left = GetAlignmentHorizontal(HorizontalAlignment);
+ }
+ else
+ {
+ Bounds.Left = GetAlignmentHorizontal(TextAlignment.Left);
+ _centerAdjustment = GetAlignmentHorizontal(HorizontalAlignment);
+ }
+
+ AddTextLinesAndSpacing(null, textIfEmpty);
+ }
+ private void ImportLinesAndTextRuns(ExcelDrawingParagraph p, string textIfEmpty)
+ {
+ if (p.TextRuns.Count == 0 && string.IsNullOrEmpty(textIfEmpty) == false)
+ {
+ ImportLinesAndTextRunsDefault(textIfEmpty);
+ }
+ else
+ {
+ //Log line positions and run sizes
+ GenerateTextFragments(p.TextRuns);
+ AddTextLinesAndSpacing(p, textIfEmpty);
+ }
+ }
+
+ private void AddTextLinesAndSpacing(ExcelDrawingParagraph p, string textIfEmpty)
+ {
+ //Temp workaround
+ if (_p == null)
+ {
+ _p = p;
+ }
+ if (_textIfEmpty == null)
+ {
+ _textIfEmpty = textIfEmpty;
+ }
+
+ _lines = WrapFragmentsToLines();
+
+ //In points
+ double lastDescent = 0;
+ double lineTop = 0;
+ double greatestWidth = 0;
+
+ if (_lines != null && _lines.Count != 0)
+ {
+ //This could be moved into a textLines collection class
+ //START
+ var idxOfLargestLine = 0;
+ double widthOfLargestLine = _lines[0].GetWidthWithoutTrailingSpaces();
+
+ for (int i = 1; i < _lines.Count; i++)
+ {
+ if (_lines[i].Width > widthOfLargestLine)
+ {
+ var ctrLineWidth = _lines[i].GetWidthWithoutTrailingSpaces();
+ SpaceWidthsPerLine.Add(_lines[i].lastFontSpaceWidth);
+ widthOfLargestLine = ctrLineWidth;
+ idxOfLargestLine = i;
+ }
+ }
+ //END
+
+
+ if (HorizontalAlignment == TextAlignment.Center && ParentTextBody.AutoSize && _centerAdjustment != null && string.IsNullOrEmpty(textIfEmpty))
+ {
+ //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);
+ }
+ else
+ {
+ Bounds.Left = 0;
+ }
+ //if (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 = 0;
+ //}
+
+ foreach (var line in _lines)
+ {
+ double prevWidth = 0;
+
+ if (HorizontalAlignment == TextAlignment.Center)
+ {
+ var ctrLineWidth = line.GetWidthWithoutTrailingSpaces();
+ //Calculate difference in widths and split to get offset between leftmost position and current line
+ prevWidth = (widthOfLargestLine - ctrLineWidth) / 2;
+ }
+ else if (HorizontalAlignment == TextAlignment.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)
+ {
+ lineTop += line.LargestAscent + lastDescent;
+ }
+ else
+ {
+ lineTop += ParagraphLineSpacing;
+ }
+ if (line.GetWidthWithoutTrailingSpaces() > greatestWidth)
+ {
+ greatestWidth = line.GetWidthWithoutTrailingSpaces();
+ }
+
+ foreach (var lineFragment in line.LineFragments)
+ {
+ var displayText = lineFragment.Text;
+
+
+ if (p != null && p.TextRuns.Count == 0 && string.IsNullOrEmpty(textIfEmpty) == false)
+ {
+ //Import fallback text with paragraph settings
+ AddText(displayText, p.DefaultRunProperties);
+ }
+ else if (p != null && p.TextRuns.Count != 0)
+ {
+ var rtIdx = _newTextFragments.IndexOf(lineFragment.OriginalTextFragment);
+ if (rtIdx > p.TextRuns.Count - 1)
+ {
+ AddText(displayText, _newTextFragments[rtIdx].Font);
+ }
+ else
+ {
+ //Import Paragraph text run
+ var idx = _newTextFragments.IndexOf(lineFragment.OriginalTextFragment);
+ AddRenderItemTextRun(p.TextRuns[idx], displayText);
+ }
+ }
+ else
+ {
+ //Import fallback with default settings from constructor
+ AddText(displayText, _paragraphFont);
+ }
+ TextRunItem runItem = Runs.Last();
+ runItem.Bounds.Left = prevWidth;
+ runItem.YPosition = lineTop;
+
+ runItem.Bounds.Width = lineFragment.Width;
+ prevWidth += lineFragment.Width;
+ }
+ lastDescent = line.LargestDescent;
+ }
+ }
+ Bounds.Height = lineTop + lastDescent;
+ Bounds.Width = greatestWidth;
+ }
+
+ List WrapFragmentsToLines(List fragments = null)
+ {
+ if(fragments == null )
+ {
+ fragments = _newTextFragments;
+ }
+
+ if (fragments.Count > 0)
+ {
+ if(_layout == null)
+ {
+ _layout = OpenTypeFonts.GetTextLayoutEngineForFont((fragments[0].Font));
+ }
+
+ var maxWidthPoints = Math.Round(ParentTextBody.MaxWidth, 0, MidpointRounding.AwayFromZero);
+
+ _lines = _layout.WrapRichTextLines(fragments, maxWidthPoints);
+ return _lines;
+ }
+ return new List();
+ }
+
+ internal double GetAlignmentHorizontal(TextAlignment txAlignment)
+ {
+ var area = Bounds;
+ double x = 0;
+ switch (txAlignment)
+ {
+ case TextAlignment.Left:
+ default:
+ x = area.Left + _leftMargin;
+ break;
+ case TextAlignment.Center:
+ x = (area.Right / 2) + _leftMargin - _rightMargin;
+ break;
+ case TextAlignment.Right:
+ x = area.Right - _rightMargin;
+ break;
+ }
+
+ return x;
+ }
+
+ ///
+ /// Type of textrun defined by child type
+ ///
+ ///
+ ///
+ ///
+ ///
+ 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.DrawingRenderer/RenderItems/Shared/TextBodyItem.cs b/src/EPPlus.DrawingRenderer/RenderItems/Shared/TextBodyItem.cs
new file mode 100644
index 0000000000..eb91d8feba
--- /dev/null
+++ b/src/EPPlus.DrawingRenderer/RenderItems/Shared/TextBodyItem.cs
@@ -0,0 +1,274 @@
+using EPPlus.Fonts.OpenType;
+using EPPlus.Fonts.OpenType.Utils;
+using EPPlus.Graphics;
+using EPPlusImageRenderer;
+using EPPlusImageRenderer.RenderItems;
+using EPPlusImageRenderer.Svg;
+using OfficeOpenXml.Drawing;
+using OfficeOpenXml.Drawing.Theme;
+using OfficeOpenXml.Interfaces.Drawing.Text;
+using OfficeOpenXml.Interfaces.Fonts;
+using OfficeOpenXml.Style;
+using OfficeOpenXml.Utils;
+using System;
+using System.Collections.Generic;
+using System.Linq;
+
+
+namespace EPPlus.DrawingRenderer.RenderItems
+{
+ ///
+ /// Margin left = X
+ /// Margin right = Y
+ ///
+ internal abstract class TextBodyItem : DrawingObject
+ {
+ public TextBodyItem(DrawingBase renderer, BoundingBox parent, bool autoSize) : base(renderer, parent)
+ {
+ Bounds.Name = "TxtBody";
+ Bounds.Parent = parent;
+ AutoSize = autoSize;
+ }
+ public TextBodyItem(DrawingBase renderer, BoundingBox parent, double maxWidth, double maxHeight) : base(renderer, parent)
+ {
+ Bounds.Name = "TxtBody";
+ Bounds.Parent = parent;
+ MaxWidth = maxWidth;
+ MaxHeight = maxHeight;
+ AutoSize = false;
+ }
+ internal bool AutoSize { get; set; }
+ internal double MaxWidth { get; set; }
+ internal double MaxHeight { get; set; }
+
+ ///
+ /// Shorthand for Bounds.Width
+ ///
+ internal double Width { get { return Bounds.Width; } set { Bounds.Width = value; } }
+
+ ///
+ /// Shorthand for Bounds.Height
+ ///
+ internal double Height { get { return Bounds.Height; } set { Bounds.Height = value; } }
+
+ internal string FontColorString { get; set; }
+
+ internal eTextAnchoringType VerticalAlignment = eTextAnchoringType.Top;
+
+ internal abstract List Paragraphs { get; set; }
+
+ public bool AllowOverflow;
+
+ internal bool WrapText = true;
+
+ internal string _text;
+
+ public void ImportParagraph(ExcelDrawingParagraph item, double startingY, string text=null)
+ {
+ var measureFont = item.DefaultRunProperties.GetMeasureFont();
+ bool isFirst = Paragraphs.Count == 0;
+
+ var paragraph = CreateParagraph(this, item, 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);
+ }
+
+ 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)
+ {
+ case TextAlignment.Left:
+ p.Bounds.Left = 0;
+ break;
+ case TextAlignment.Center:
+ p.Bounds.Left = (Bounds.Width / 2) - (p.Bounds.Width / 2);
+ break;
+ case TextAlignment.Right:
+ p.Bounds.Left = Bounds.Right - p.Bounds.Width;
+ break;
+ case TextAlignment.Distributed:
+ case TextAlignment.Justified:
+ case TextAlignment.JustifiedLow:
+ case TextAlignment.ThaiDistributed:
+ p.Bounds.Left = 0; //TODO: Set left for now as we do not support distributed spacing yet
+ break;
+ }
+ }
+ //}
+ }
+
+ 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 currentHeight = 0;
+ double largestWidth = double.MinValue;
+
+ foreach (var paragraph in body.Paragraphs)
+ {
+ ImportParagraph(paragraph, currentHeight);
+ var addedPara = Paragraphs.Last();
+ currentHeight = 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 = currentHeight;
+ }
+
+ Bounds.Top = GetAlignmentVertical();
+ }
+
+ private double GetParagraphAscendantSpacingInPixels(DrawingTextLineSpacing lineSpacingType, double spacingValue, ITextShaper fmExact, float fontSize, out double multiplier)
+ {
+ if (lineSpacingType == DrawingTextLineSpacing.Exactly)
+ {
+ multiplier = -1;
+ return spacingValue.PointToPixel();
+ }
+ else
+ {
+ multiplier = (spacingValue / 100);
+ return multiplier * fmExact.GetAscentInPoints(fontSize).PointToPixel();
+ }
+ }
+
+
+ //public void AddText(string text, FontMeasurerTrueType measurer)
+ //{
+ // if (Paragraphs.Count == 0)
+ // {
+ // AddParagraph(text, measurer);
+ // }
+ // else
+ // {
+ // Paragraphs.Last().AddText(text, measurer);
+ // }
+ //}
+ //internal void AddText(string text, ExcelTextFont font)
+ //{
+ // //Document Top position for the paragraph text based on vertical alignment
+ // var posY = GetAlignmentVertical();
+ // //var vertAlignAttribute = GetVerticalAlignAttribute(posY);
+
+ // //var measurer = font.PictureRelationDocument.Package.Settings.TextSettings.GenericTextMeasurerTrueType;
+ // //Limit bounding area with the space taken by previous paragraphs
+ // //Note that this is ONLY identical to PosY if the vertical alignment is top
+
+ // //The first run in the first paragraph must apply different line-spacing
+ // //var svgParagraph = new SvgParagraph(text, font, area, vertAlignAttribute, posY);
+ // var paragraph = CreateParagraph(Bounds);
+ // paragraph.AddText(text, font);
+ // paragraph.FillColor = font.Fill.Color.To6CharHexString();
+
+ // Paragraphs.Add(paragraph);
+ //}
+ //public void SetMeasurer(FontMeasurerTrueType fontMeasurer)
+ //{
+ // _measurer = fontMeasurer;
+ //}
+
+ double? _alignmentY = null;
+
+ ///
+ /// Get the start of text space vertically
+ ///
+ ///
+ ///
+ private double GetAlignmentVertical()
+ {
+ double alignmentY = 0;
+
+ switch (VerticalAlignment)
+ {
+ case eTextAnchoringType.Top:
+ alignmentY = Bounds.Top;
+ break;
+ //Center means center of a Shape's ENTIRE bounding box height.
+ //Not center of the Inset GetRectangle
+ case eTextAnchoringType.Center:
+ alignmentY = (MaxHeight - Bounds.Height)/2 + Bounds.Top;
+ break;
+ case eTextAnchoringType.Bottom:
+ alignmentY = MaxHeight - Bounds.Height;
+ break;
+ }
+
+ _alignmentY = alignmentY;
+
+ return _alignmentY.Value;
+ }
+
+
+ ///
+ /// Each file format defines its own paragraph
+ ///
+ ///
+ 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
+ ///
+ ///
+ internal abstract ParagraphItem CreateParagraph(TextBodyItem textBody, BoundingBox parent);
+ }
+}
diff --git a/src/EPPlus.DrawingRenderer/RenderItems/Shared/TextRunCollection.cs b/src/EPPlus.DrawingRenderer/RenderItems/Shared/TextRunCollection.cs
new file mode 100644
index 0000000000..e59e69e474
--- /dev/null
+++ b/src/EPPlus.DrawingRenderer/RenderItems/Shared/TextRunCollection.cs
@@ -0,0 +1,50 @@
+using OfficeOpenXml.Drawing.Chart;
+using System;
+using System.Collections;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+
+namespace EPPlus.DrawingRenderer.RenderItems
+{
+ internal class TextRunCollection : IEnumerable
+ {
+ private List _textRunItems;
+
+ public void Add(TextRunItem item)
+ {
+ _textRunItems.Add(item);
+ }
+
+ ///
+ /// Number of items in the collection
+ ///
+ public int Count
+ {
+ get
+ {
+ return _textRunItems.Count;
+ }
+ }
+ IEnumerator IEnumerable.GetEnumerator()
+ {
+ return _textRunItems.GetEnumerator();
+ }
+ IEnumerator IEnumerable.GetEnumerator()
+ {
+ return _textRunItems.GetEnumerator();
+ }
+ ///
+ /// Returns a textrun at position
+ ///
+ /// The position of the chart. 0-base
+ ///
+ public TextRunItem this[int PositionID]
+ {
+ get
+ {
+ return (_textRunItems[PositionID]);
+ }
+ }
+ }
+}
diff --git a/src/EPPlus.DrawingRenderer/RenderItems/Shared/TextRunItem.cs b/src/EPPlus.DrawingRenderer/RenderItems/Shared/TextRunItem.cs
new file mode 100644
index 0000000000..9670527633
--- /dev/null
+++ b/src/EPPlus.DrawingRenderer/RenderItems/Shared/TextRunItem.cs
@@ -0,0 +1,205 @@
+using EPPlus.DrawingRenderer;
+using EPPlus.Fonts.OpenType;
+using EPPlus.Fonts.OpenType.Utils;
+using EPPlus.Graphics;
+using EPPlusImageRenderer;
+using EPPlusImageRenderer.RenderItems;
+using OfficeOpenXml.Drawing;
+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.RegularExpressions;
+using static System.Net.Mime.MediaTypeNames;
+
+namespace EPPlus.DrawingRenderer.RenderItems
+{
+ internal abstract class TextRunItem : RenderItem
+ {
+ public override RenderItemType Type => RenderItemType.Text;
+
+ internal readonly string _originalText;
+ protected string _currentText;
+
+ internal protected MeasurementFont _measurementFont;
+ internal protected bool _isFirstInParagraph;
+
+ internal double FontSizeInPixels { get; private set; }
+
+ public List Lines { get; private set; }
+
+ protected internal bool _isItalic = false;
+ protected internal bool _isBold = false;
+ protected internal eUnderLineType _underLineType;
+ protected internal eStrikeType _strikeType;
+ protected internal Color _underlineColor;
+ protected internal double _baseline;
+
+ internal double YPosition { get; set; }
+ 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;
+
+ Bounds.Name = "TextRun";
+ _currentText = string.IsNullOrEmpty(displayText) ? _originalText : displayText;
+
+ Lines = Regex.Split(_currentText, "\r\n|\r|\n").ToList();
+
+ //_measurer = new FontMeasurerTrueType();
+
+ _measurementFont = font.GetMeasureFont();
+ //_measurer.SetFont(_measurementFont);
+
+ _isFirstInParagraph = true;
+
+ //_fontStyles = _measurementFont.Style;
+
+ _baseline = font.Baseline;
+ if (_baseline != 0)
+ {
+ _measurementFont.Size *= (float)(1 - (Math.Abs(_baseline) / 100));
+ }
+ FontSizeInPixels = ((double)_measurementFont.Size).PointToPixel(true);
+
+ Bounds.Height = _measurementFont.Size;
+ if (parent.Height < _measurementFont.Size)
+ {
+ parent.Height = _measurementFont.Size;
+ }
+ //_horizontalTextAlignment = TextAlignment.Center;
+
+ if (font.Fill.Style == eFillStyle.SolidFill)
+ {
+ FillColor = "#" + font.Fill.Color.To6CharHexString();
+ }
+
+ //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
+ }
+ _isItalic = font.Italic;
+ _isBold = font.Bold;
+ _underLineType = font.UnderLine;
+ _underlineColor = font.UnderLineColor;
+ _strikeType = font.Strike;
+ }
+
+ ///
+ /// If the run has been wrapped more line-breaks may have been added in displayText
+ ///
+ ///
+ ///
+ ///
+ internal TextRunItem(DrawingBase renderer, BoundingBox parent, ExcelParagraphTextRunBase run, string displayText = "") : base(renderer, parent)
+ {
+ _originalText = run.Text;
+
+ Bounds.Name = "TextRun";
+ _currentText = string.IsNullOrEmpty(displayText) ? _originalText : displayText;
+
+ Lines = Regex.Split(_currentText, "\r\n|\r|\n").ToList();
+
+ _measurementFont = run.GetMeasurementFont();
+
+
+ //_fontStyles = _measurementFont.Style;
+
+ _baseline = run.Baseline;
+ FontSizeInPixels = ((double)_measurementFont.Size).PointToPixel(true);
+
+ Bounds.Height = _measurementFont.Size;
+
+ //_horizontalTextAlignment = run.Paragraph.HorizontalAlignment;
+
+ if (run.Fill.IsEmpty == false && run.Fill.Style == eFillStyle.SolidFill)
+ {
+ FillColor = "#" + run.Fill.Color.To6CharHexString();
+ }
+
+ //To get clipping height we need to get the textbody bounds
+ if( parent!= null && parent.Parent != null && parent.Parent.Parent != null)
+ {
+ ClippingHeight = ((BoundingBox)parent.Parent.Parent).Bottom;
+ }
+
+ if (run.Fill.Style == eFillStyle.SolidFill)
+ {
+ FillColor = "#" + run.Fill.Color.To6CharHexString();
+ }
+
+ _isItalic = run.FontItalic;
+ _isBold = run.FontBold;
+ _underLineType = run.FontUnderLine;
+ _underlineColor = run.UnderLineColor;
+ _strikeType = run.FontStrike;
+ }
+
+ ///
+ /// Calculates right/bottom
+ ///
+ ///
+ ///
+ ///
+ ///
+ internal override void GetBounds(out double il, out double it, out double ir, out double ib)
+ {
+ il = Bounds.Left;
+ it = Bounds.Top;
+ ir = Bounds.Right;
+ ib = Bounds.Bottom;
+ }
+ }
+}
diff --git a/src/EPPlus.DrawingRenderer/RenderItems/Shared/TransformGroup.cs b/src/EPPlus.DrawingRenderer/RenderItems/Shared/TransformGroup.cs
new file mode 100644
index 0000000000..d5b002ac66
--- /dev/null
+++ b/src/EPPlus.DrawingRenderer/RenderItems/Shared/TransformGroup.cs
@@ -0,0 +1,100 @@
+using EPPlus.Graphics;
+using EPPlusImageRenderer;
+using EPPlusImageRenderer.RenderItems;
+using System.Collections.Generic;
+
+namespace EPPlus.DrawingRenderer.RenderItems
+{
+ internal abstract class TransformGroup : RenderItem
+ {
+ protected InnerGroup _innerGroup;
+
+ Point PositionAfterTransform;
+
+ ///
+ /// In degrees
+ ///
+ internal double Rotation = double.NaN;
+
+ BoundingBox _altRotationPoint = null;
+
+ internal BoundingBox RotationPoint
+ {
+ get
+ {
+ if (_altRotationPoint == null)
+ {
+ return Bounds;
+ }
+ return _altRotationPoint;
+ }
+ set
+ {
+ _altRotationPoint = value;
+ }
+ }
+ Coordinate _scale = new Coordinate(1,1);
+
+ internal Coordinate Scale
+ {
+ get
+ {
+ return _scale;
+ }
+ set
+ {
+ Bounds.Parent.Scale = new Graphics.Math.Vector2(value.X, value.Y);
+ _scale = value;
+ }
+ }
+
+ public TransformGroup(DrawingBase renderer) : base(renderer)
+ {
+ _innerGroup = CreateInnerGroup();
+ _innerGroup.Bounds.Parent = Bounds;
+ }
+
+ public TransformGroup(DrawingBase renderer, double localXPos, double localYPos) : this(renderer)
+ {
+ Bounds.Left = localXPos;
+ Bounds.Top = localYPos;
+ }
+
+
+ public TransformGroup(DrawingBase renderer, BoundingBox parent, double rotation, Transform rotationPoint = null) : this(renderer, 0, 0)
+ {
+ Bounds.Parent = parent;
+ Rotation = rotation;
+ if (rotationPoint != null)
+ {
+ RotationPoint = new BoundingBox(rotationPoint.LocalPosition.X, rotationPoint.LocalPosition.Y);
+ }
+ }
+
+ internal void SetRotationPointToCenterOfGroup(double rotation = double.NaN)
+ {
+ RotationPoint = new BoundingBox(Bounds.Width / 2, Bounds.Height / 2);
+
+ if (double.IsNaN(rotation) == false)
+ {
+ Rotation = rotation;
+ }
+ }
+
+ ///
+ /// Adds child item to the group under this group
+ ///
+ ///
+ internal void AddChildItem(RenderItem item)
+ {
+ _innerGroup.AddChildItem(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;
+ }
+
+ internal abstract InnerGroup CreateInnerGroup();
+
+ public override RenderItemType Type => RenderItemType.Group;
+ }
+}
diff --git a/src/EPPlus.DrawingRenderer/RenderItems/SvgDrawingCommands.cs b/src/EPPlus.DrawingRenderer/RenderItems/SvgDrawingCommands.cs
new file mode 100644
index 0000000000..b5087795db
--- /dev/null
+++ b/src/EPPlus.DrawingRenderer/RenderItems/SvgDrawingCommands.cs
@@ -0,0 +1,181 @@
+/*************************************************************************************************
+ 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
+ *************************************************************************************************/
+//using OfficeOpenXml.Drawing;
+//using OfficeOpenXml.Utils.TypeConversion;
+//using System;
+//using System.Reflection;
+//using System.Textbox.Json;
+//using System.Windows;
+//using System.Xml;
+//namespace EPPlusImageRenderer.RenderItems
+//{
+// public static class SvgDrawingCommands
+// {
+// public static void LoadShapes()
+// {
+// Shapes.Clear();
+// var br = new BinaryReader(new FileStream("c:\\temp\\Shapedrawing.bin", FileMode.Open));
+// while (br.BaseStream.Position < br.BaseStream.Length)
+// {
+// var shapeStyle = (eShapeStyle)br.ReadByte();
+// var list = new List();
+// var adj = ShapeAdjustments.ContainsKey(shapeStyle) ? ShapeAdjustments[shapeStyle] : null;
+// Shapes.Add(shapeStyle, list);
+// var ix = 0;
+// do
+// {
+// var recType = br.ReadByte();
+// if (recType == 0) break;
+// SvgAdjustmentPoint adjPoint = null;
+// if (adj != null && adj.ContainsKey(ix & 0x1FF))
+// {
+// adjPoint = adj[ix];
+// }
+
+// switch (recType)
+// {
+// case 1: //rect
+// list.Add(ReadRect(br));
+// break;
+// case 2: //path
+// list.Add(ReadPath(br));
+// break;
+// }
+// ix++;
+// }
+// while (br.BaseStream.Position < br.BaseStream.Length);
+// }
+// }
+
+// private static SvgRenderPathItem ReadPath(BinaryReader br)
+// {
+// var fill = br.ReadByte();
+// var stroke = br.ReadByte();
+// var commandCount = br.ReadByte();
+// var item = new SvgRenderPathItem();
+// for (var i = 0; i < commandCount; i++)
+// {
+// var ct = (PathCommandType)br.ReadByte();
+// var itemCount = br.ReadInt16();
+// var l = new List();
+// for (int j = 0; j < itemCount; j++)
+// {
+// l.Add(br.ReadSingle());
+// }
+// var cmd = new PathCommands(ct, item, l.ToArray());
+// //if (adj != null && (adj.Commands == null || adj.Commands.Any(x => x.Index == i)))
+// //{
+// // cmd.AdjustmentPoint = adj;
+// // if (adj.Commands != null)
+// // {
+// // cmd.CommandIndex = adj.Commands.FindIndex(x => x.Index == i);
+// // }
+// //}
+// item.Commands.Add(cmd);
+// }
+
+// item.FillColorSource = (PathFillMode)fill;
+// item.BorderColorSource = (PathFillMode)stroke;
+
+// return item;
+// }
+
+// private static SvgRenderRectItem ReadRect(BinaryReader br)
+// {
+// var fill = br.ReadByte();
+// var stroke = br.ReadByte();
+// var x = br.ReadSingle();
+// var y = br.ReadSingle();
+// var width = br.ReadSingle();
+// var height = br.ReadSingle();
+
+// return new SvgRenderRectItem() { Left = x, Top = y, Width = width, Height = height, FillColorSource = (PathFillMode)fill, BorderColorSource = (PathFillMode)stroke };
+// }
+// public static string SerializeShapeAdjustments()
+// {
+// var s = JsonSerializer.Serialize(ShapeAdjustments);
+// return new StringReader(s).ReadToEnd();
+// }
+// public static void DeSerializeShapeAdjustments(string json)
+// {
+// ShapeAdjustments.Clear();
+// LoadShapes();
+// //MessageBox.Show("Json Loaded");
+// }
+
+// public static void LoadAdjustments()
+// {
+// var path = new FileInfo(Assembly.GetExecutingAssembly().FullName).DirectoryName;
+// ShapeAdjustments = new Dictionary>();
+// foreach (var f in Directory.GetFiles(path + "\\Adjustments\\", "*.xml"))
+// {
+// ReadAdjustmentXml(f, ShapeAdjustments);
+// }
+// }
+
+// private static void ReadAdjustmentXml(string f, Dictionary> shapeAdjustments)
+// {
+// var xml = new XmlDocument();
+// xml.Load(f);
+
+// var style = (eShapeStyle)Enum.Parse(typeof(eShapeStyle), xml.SelectSingleNode("adjust/@type").Value);
+// var adjs = new Dictionary();
+// foreach (XmlElement objNode in xml.SelectNodes("adjust/objects/object"))
+// {
+// var adj = new SvgAdjustmentPoint();
+// adj.ItemIndex = int.Parse(objNode.GetAttribute("ix"));
+// adj.AdjustmentType = (AdjustmentType)Enum.Parse(typeof(AdjustmentType), objNode.GetAttribute("adjustmentType"));
+// foreach (XmlElement cmdNode in objNode.SelectNodes("commands/command"))
+// {
+// var ix = int.Parse(cmdNode.GetAttribute("ix"));
+// var cmd = new SvgCommand(ix);
+// if (adj.Commands == null) adj.Commands = new List();
+// adj.Commands.Add(cmd);
+// foreach (XmlElement ptNode in cmdNode.SelectNodes("pt"))
+// {
+// var ptIx = short.Parse(ptNode.GetAttribute("ix"));
+// var coordinate = new SvgCoordinate(ptIx);
+// if (string.IsNullOrEmpty(ptNode.GetAttribute("adjustPointName"))==false)
+// {
+// coordinate.PointName = ptNode.GetAttribute("adjustPointName");
+// }
+// else
+// {
+// coordinate.PointName = "";
+// }
+
+// if (ConvertUtil.TryParseIntString(ptNode.GetAttribute("origin"), out int r))
+// {
+// coordinate.Origin = (short)r;
+// }
+// else
+// {
+// coordinate.Origin = 0;
+// }
+// if (Enum.TryParse(ptNode.GetAttribute("type"), true, out var result))
+// {
+// coordinate.BulletType = result;
+// }
+// cmd.Coordinates.Add(ptIx, coordinate);
+// }
+// }
+// adjs.Add(adj.ItemIndex, adj);
+// }
+// shapeAdjustments.Add(style, adjs);
+// }
+
+// internal static Dictionary> Shapes { get; } = new Dictionary>();
+// internal static Dictionary> ShapeAdjustments { get; set; } = new Dictionary>();
+// }
+
+//}
\ No newline at end of file
diff --git a/src/EPPlus.DrawingRenderer/RenderItems/SvgExtensions.cs b/src/EPPlus.DrawingRenderer/RenderItems/SvgExtensions.cs
new file mode 100644
index 0000000000..93c3ac7124
--- /dev/null
+++ b/src/EPPlus.DrawingRenderer/RenderItems/SvgExtensions.cs
@@ -0,0 +1,45 @@
+/*************************************************************************************************
+ 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
+ *************************************************************************************************/
+using System;
+
+namespace EPPlusImageRenderer.RenderItems
+{
+ public static class SvgExtensions
+ {
+ public static char AsCommandChar(this PathCommandType type)
+ {
+ switch (type)
+ {
+ case PathCommandType.Move:
+ return 'M';
+ case PathCommandType.Line:
+ return 'L';
+ case PathCommandType.HorizontalLine:
+ return 'H';
+ case PathCommandType.VerticalLine:
+ return 'V';
+ case PathCommandType.CubicBézier:
+ return 'C';
+ case PathCommandType.QuadraticBézier:
+ return 'Q';
+ case PathCommandType.Arc:
+ return 'A';
+ case PathCommandType.End:
+ return 'Z';
+ default:
+ throw new NotImplementedException("SVG path type not implemented");
+ }
+ }
+ }
+
+}
\ No newline at end of file
diff --git a/src/EPPlus.DrawingRenderer/RenderItems/SvgItem/ConnectionPointsMiddle.cs b/src/EPPlus.DrawingRenderer/RenderItems/SvgItem/ConnectionPointsMiddle.cs
new file mode 100644
index 0000000000..f519ec5d3a
--- /dev/null
+++ b/src/EPPlus.DrawingRenderer/RenderItems/SvgItem/ConnectionPointsMiddle.cs
@@ -0,0 +1,34 @@
+using EPPlus.DrawingRenderer;
+
+namespace EPPlus.Export.ImageRenderer.RenderItems.SvgItem
+{
+ public class ConnectionPointsMiddle
+ {
+ public Coordinate Left;
+ public Coordinate Top;
+ public Coordinate Right;
+ public Coordinate Bottom;
+
+ public Dictionary Points = new Dictionary();
+
+ public 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.DrawingRenderer/RenderItems/SvgItem/SvgParagraphRenderItem.cs b/src/EPPlus.DrawingRenderer/RenderItems/SvgItem/SvgParagraphRenderItem.cs
new file mode 100644
index 0000000000..06599819d6
--- /dev/null
+++ b/src/EPPlus.DrawingRenderer/RenderItems/SvgItem/SvgParagraphRenderItem.cs
@@ -0,0 +1,46 @@
+using EPPlus.DrawingRenderer;
+using EPPlus.Export.ImageRenderer.RenderItems.Shared;
+using EPPlus.Fonts.OpenType.Integration.DataHolders;
+using EPPlus.Graphics;
+
+
+namespace EPPlus.DrawingRenderer.RenderItems.SvgItem
+{
+ public class SvgParagraphRenderItem : ParagraphRenderItem
+ {
+ public SvgParagraphRenderItem(RenderContext renderContext, RenderTextBody body, BoundingBox parent, string text, bool setDefaultFont = true) : base(renderContext, parent, body, text, setDefaultFont)
+ {
+ ImportStyles();
+ }
+ public SvgParagraphRenderItem(RenderContext renderContext, RenderTextBody textBody, BoundingBox parent, IRichTextFormatSimple rtFormat) : base(renderContext, parent, textBody, rtFormat)
+ {
+ ImportStyles();
+ }
+
+ private void ImportStyles()
+ {
+ //Import RichText data to each run
+ foreach (var run in Runs)
+ {
+ var textRun = (SvgTextRunRenderItem)run;
+ var rtOptions = _layoutSystem.InputFragments[run.OriginalRtIdx].RichTextOptions;
+ if (_layoutSystem.InputFragments.Count != 0 && run.OriginalRtIdx != -1 && rtOptions is IRichTextFormatSimple)
+ {
+ textRun.ImportRichTextData((IRichTextFormatSimple)rtOptions);
+ }
+ else
+ {
+ //If not use the default for the whole paragraph (potentially user specified)
+ run.ImportFontData(DefaultParagraphFont);
+ }
+ }
+ }
+
+ public override RenderItemType Type => RenderItemType.Paragraph;
+
+ protected override TextRunRenderItem CreateTextRun(BoundingBox parent, string displayText, int origRtIdx)
+ {
+ return new SvgTextRunRenderItem(parent, displayText, origRtIdx);
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/EPPlus.DrawingRenderer/RenderItems/SvgItem/SvgTextBodyRenderItem.cs b/src/EPPlus.DrawingRenderer/RenderItems/SvgItem/SvgTextBodyRenderItem.cs
new file mode 100644
index 0000000000..8716dae0ce
--- /dev/null
+++ b/src/EPPlus.DrawingRenderer/RenderItems/SvgItem/SvgTextBodyRenderItem.cs
@@ -0,0 +1,30 @@
+using EPPlus.Export.ImageRenderer.RenderItems.Shared;
+using EPPlus.Fonts.OpenType.Integration.DataHolders;
+using EPPlus.Graphics;
+using System.Drawing;
+
+
+namespace EPPlus.DrawingRenderer.RenderItems.SvgItem
+{
+ public class SvgTextBodyRenderItem : RenderTextBody
+ {
+ public SvgTextBodyRenderItem(RenderContext renderContext, BoundingBox parent, bool autoSize) : base(renderContext, parent, autoSize)
+ {
+ }
+
+ public SvgTextBodyRenderItem(RenderContext renderContext, BoundingBox parent, double left, double top, double maxWidth, double maxHeight, bool clampedToParent = false, bool autoSize = false) : base(renderContext, parent, left, top, maxWidth, maxHeight, clampedToParent, autoSize)
+ {
+
+ }
+
+ protected override ParagraphRenderItem CreateParagraph(BoundingBox parent, string textIfEmpty = "")
+ {
+ return new SvgParagraphRenderItem(RenderContext, this, parent, textIfEmpty);
+ }
+
+ protected override ParagraphRenderItem CreateParagraph(BoundingBox parent, IRichTextFormatSimple richText)
+ {
+ return new SvgParagraphRenderItem(RenderContext, this, parent, richText);
+ }
+ }
+}
diff --git a/src/EPPlus.DrawingRenderer/RenderItems/SvgItem/SvgTextRunRenderItem.cs b/src/EPPlus.DrawingRenderer/RenderItems/SvgItem/SvgTextRunRenderItem.cs
new file mode 100644
index 0000000000..3971adac4c
--- /dev/null
+++ b/src/EPPlus.DrawingRenderer/RenderItems/SvgItem/SvgTextRunRenderItem.cs
@@ -0,0 +1,39 @@
+using EPPlus.Export.ImageRenderer.RenderItems.Shared;
+using EPPlus.Graphics;
+using OfficeOpenXml.Interfaces.RichText;
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+
+namespace EPPlus.DrawingRenderer.RenderItems.SvgItem
+{
+ public class SvgTextRunRenderItem : TextRunRenderItem
+ {
+ public SvgTextRunRenderItem(BoundingBox parent) : base(parent)
+ {
+ }
+
+ public SvgTextRunRenderItem(BoundingBox parent, string text, int origRtIdx) : base(parent, text, origRtIdx)
+ {
+ }
+
+ public SvgTextRunRenderItem(BoundingBox parent, IFontFormatBase font, string displayText, bool renderTextNode = false) : base(parent, font, displayText)
+ {
+ RenderTextNode = renderTextNode;
+ }
+
+ public SvgTextRunRenderItem(BoundingBox parent, string text, IFontFormatBase font, string displayText) : base(parent, text, font, displayText)
+ {
+ }
+
+
+ ///
+ /// If set to true will render its own parent Text Node
+ /// Will not work properly within paragraphs
+ ///
+ internal bool RenderTextNode { get; private set; } = false;
+
+ }
+}
diff --git a/src/EPPlus.DrawingRenderer/RenderItems/SvgItem/eTextAnchor.cs b/src/EPPlus.DrawingRenderer/RenderItems/SvgItem/eTextAnchor.cs
new file mode 100644
index 0000000000..762d32e686
--- /dev/null
+++ b/src/EPPlus.DrawingRenderer/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.
+ ///
+ public 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.DrawingRenderer/RenderItems/SvgLineJoin.cs b/src/EPPlus.DrawingRenderer/RenderItems/SvgLineJoin.cs
new file mode 100644
index 0000000000..9b1e1afc04
--- /dev/null
+++ b/src/EPPlus.DrawingRenderer/RenderItems/SvgLineJoin.cs
@@ -0,0 +1,24 @@
+/*************************************************************************************************
+ 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 EPPlusImageRenderer.RenderItems
+{
+ internal enum SvgLineJoin
+ {
+ Arcs,
+ Bevel,
+ Miter,
+ MiterClip,
+ Round
+ }
+
+}
\ No newline at end of file
diff --git a/src/EPPlus.DrawingRenderer/RenderItems/Textbox/ParagraphRenderItem.cs b/src/EPPlus.DrawingRenderer/RenderItems/Textbox/ParagraphRenderItem.cs
new file mode 100644
index 0000000000..3b54e73264
--- /dev/null
+++ b/src/EPPlus.DrawingRenderer/RenderItems/Textbox/ParagraphRenderItem.cs
@@ -0,0 +1,408 @@
+using EPPlus.DrawingRenderer;
+using EPPlus.DrawingRenderer.RenderItems;
+using EPPlus.DrawingRenderer.RenderItems.Textbox;
+using EPPlus.Fonts.OpenType;
+using EPPlus.Fonts.OpenType.Integration;
+using EPPlus.Fonts.OpenType.Integration.DataHolders;
+using EPPlus.Fonts.OpenType.Integration.RichText;
+using EPPlus.Fonts.OpenType.TextShaping;
+using EPPlus.Graphics;
+using OfficeOpenXml.Interfaces.Drawing.Text;
+using OfficeOpenXml.Interfaces.RichText;
+
+namespace EPPlus.Export.ImageRenderer.RenderItems.Shared
+{
+ ///
+ /// Text alignment
+ ///
+ public enum TextAlignment
+ {
+ ///
+ /// Left alignment
+ ///
+ Left,
+ ///
+ /// Center alignment
+ ///
+ Center,
+ ///
+ /// Right alignment
+ ///
+ Right,
+ ///
+ /// Distributes the text words across an entire text line
+ ///
+ Distributed,
+ ///
+ /// Align text so that it is justified across the whole line.
+ ///
+ Justified,
+ ///
+ /// Aligns the text with an adjusted kashida length for Arabic text
+ ///
+ JustifiedLow,
+ ///
+ /// Distributes Thai text specially, specially, because each character is treated as a word
+ ///
+ ThaiDistributed
+ }
+
+ public enum TextLineSpacing
+ {
+ ///
+ /// Single line spacing
+ ///
+ Single,
+ ///
+ /// 1.5 lines
+ ///
+ OneAndAHalf,
+ ///
+ /// Double line spacing
+ ///
+ Double,
+ ///
+ /// Exact point spacing
+ ///
+ Exactly,
+ ///
+ /// Multiple line spacing
+ ///
+ Multiple
+ }
+
+ public abstract class ParagraphRenderItem : RenderItem
+ {
+ protected double LeftMargin { get; set; }
+ protected double RightMargin { get; set; }
+ protected double LineSpacingAscendantOnly { get; set; }
+ protected bool IsFirstParagraph { get; set; }
+
+ protected LayoutSystem _layoutSystem;
+
+ protected RichTextCollectionBase _textFragments = new RichTextCollectionBase();
+
+ public double ParagraphLineSpacing { get; protected set; }
+
+ protected TextAlignment _alignment;
+
+ //After setting alignment we must re-calculate the rows
+ public TextAlignment HorizontalAlignment { get { return _alignment; } set { _alignment = value; WrapTextFragmentsAndGenerateTextRuns(); } }
+ public List Runs { get; set; } = new List();
+ public TextLineCollection Lines { get; protected set; }
+ public bool DisplayBounds { get; set; } = false;
+
+ public override RenderItemType Type => RenderItemType.Paragraph;
+
+ public bool AutoSize = false;
+
+ public FontFormatBase DefaultParagraphFont;
+
+ protected double ParentMaxWidth;
+ protected double ParentMaxHeight;
+
+ protected RenderTextBody ParentTextBody { get; set; }
+
+ protected double? _lsMultiplier = null;
+
+ protected bool TextIfEmptyIsNull { get; set; }
+
+ protected bool LinespacingIsExact
+ {
+ get
+ {
+ return _lsMultiplier.HasValue == false;
+ }
+ }
+
+ protected RenderContext RenderContext { get; private set; }
+
+
+
+ protected TextLineSpacing _lsType;
+ protected double? _centerAdjustment;
+
+ protected ParagraphRenderItem(RenderContext renderContext, BoundingBox parent, bool setFallbackDefaultFont = true) : base(parent)
+ {
+ RenderContext = renderContext;
+ Bounds.Name = "Paragraph";
+ if (setFallbackDefaultFont)
+ {
+ var defaultFont = new MeasurementFont { FontFamily = "Aptos Narrow", Size = 11, Style = MeasurementFontStyles.Regular };
+ DefaultParagraphFont = new FontFormatBase(defaultFont);
+ FillColor = "black";
+ }
+ }
+
+ protected ParagraphRenderItem(RenderContext renderContext, BoundingBox parent, RenderTextBody textBody, bool setFallbackDefaultFont = true)
+ : this(renderContext, parent, setFallbackDefaultFont)
+ {
+ InitBasedOnParent(textBody);
+ Bounds.Name = "Paragraph";
+ }
+
+ protected ParagraphRenderItem(RenderContext renderContext, BoundingBox parent, RenderTextBody textBody, string text, bool setFallbackDefaultFont = true)
+ : this(renderContext, parent, textBody, setFallbackDefaultFont)
+ {
+ _lsMultiplier = 1d;
+ ImportLinesAndTextRunsBase(text);
+ }
+
+ protected ParagraphRenderItem(RenderContext renderContext, BoundingBox parent, RenderTextBody textBody, IRichTextFormatSimple rtFormat)
+ : this(renderContext, parent, textBody, false)
+ {
+ _lsMultiplier = 1d;
+ DefaultParagraphFont = new FontFormatBase(rtFormat.Family, rtFormat.SubFamily, rtFormat.Size);
+ AddRichText(rtFormat);
+ }
+
+ protected ParagraphRenderItem(RenderContext renderContext, BoundingBox parent, RenderTextBody textBody, IRichTextFormatDrawing rtFormat)
+ : this(renderContext, parent, textBody, false)
+ {
+ AddRichText(rtFormat);
+ }
+
+ protected double GetAlignmentHorizontal(TextAlignment txAlignment)
+ {
+ double x = 0;
+ switch (txAlignment)
+ {
+ case TextAlignment.Left:
+ default:
+ x = Bounds.Left + LeftMargin;
+ break;
+ case TextAlignment.Center:
+ x = (Bounds.Right / 2) + LeftMargin - RightMargin;
+ break;
+ case TextAlignment.Right:
+ x = Bounds.Right - RightMargin;
+ break;
+ }
+
+ return x;
+ }
+
+
+ void InitBasedOnParent(RenderTextBody textBody)
+ {
+ ParentTextBody = textBody;
+ ParentMaxWidth = textBody.MaxWidth;
+ ParentMaxHeight = textBody.MaxHeight;
+ AutoSize = textBody.AutoSize;
+
+ if (AutoSize == false)
+ {
+ Bounds.Width = textBody.Width;
+ Bounds.Height = textBody.Height;
+ }
+ else
+ {
+ //Set to max until measured
+ Bounds.Width = ParentMaxWidth;
+ Bounds.Height = ParentMaxHeight;
+ }
+ }
+
+ TextLineCollection WrapFragmentsToLines(List? fragments = null)
+ {
+ //This is highly innefficent. Really, LayoutSystem should be
+ //Holding the fragments from the start/wrapping should only be done when textFragments are fully complete
+ _layoutSystem = new LayoutSystem(RenderContext.FontEngine, _textFragments);
+
+ //if (fragments == null && _layoutSystem == null)
+ //{
+ // _layoutSystem = new LayoutSystem(_textFragments);
+ //}
+
+ double maxWidthInPoints;
+ if(AutoSize)
+ {
+ maxWidthInPoints = Math.Round(ParentMaxWidth - RightMargin - LeftMargin, 0, MidpointRounding.AwayFromZero);
+ }
+ else
+ {
+ maxWidthInPoints = Bounds.Width;
+ }
+ return _layoutSystem.Wrap(maxWidthInPoints);
+ }
+
+ private void AddRichTextBase(IRichTextFormatSimple rt)
+ {
+ if (_textFragments == null)
+ {
+ _textFragments = new RichTextCollectionBase();
+ }
+
+ if (string.IsNullOrEmpty(rt.Text) == false)
+ {
+ _textFragments.Add(rt);
+ }
+ }
+
+ protected void AddDefaultTextFragment(string text)
+ {
+ var defaults = new RichTextFormatSimple();
+ defaults.Text = text;
+ defaults.SetFont(DefaultParagraphFont);
+
+ AddRichTextBase(defaults);
+ }
+
+ protected void ImportLinesAndTextRunsBase(string textIfEmpty)
+ {
+ if(string.IsNullOrEmpty(textIfEmpty))
+ {
+ TextIfEmptyIsNull = true;
+ }
+ else
+ {
+ TextIfEmptyIsNull = false;
+ }
+
+ AddDefaultTextFragment(textIfEmpty);
+ WrapTextFragmentsAndGenerateTextRuns();
+ }
+
+ protected void WrapTextFragmentsAndGenerateTextRuns()
+ {
+ Lines = WrapFragmentsToLines();
+ Runs.Clear();
+
+ //In points
+ double widthOfLargestLine = 0;
+ //Set to 0 then grow to size of content after wrap/measure
+ //This as an empty paragraph should have no real size
+ double combinedHeight = 0;
+
+ //has value if there is linespacing otherwise isNaN
+ //Don't do this on the actual property as a paragraph can have a fallback linespacing without it being applied
+ //(e.g. paragraph linespacing is set in the ooxml but the paragraph contains no textruns)
+ //We should not change the 'ParagraphLineSpacing' variable directly here
+ double lineSpacingResult = LinespacingIsExact ? ParagraphLineSpacing : double.NaN;
+
+ if (Lines != null && Lines.Count != 0)
+ {
+ widthOfLargestLine = Lines.LargestWidthWithoutSpace;
+ combinedHeight = Lines.GetHeightOfCollection(_lsMultiplier, lineSpacingResult);
+
+
+ Bounds.Width = widthOfLargestLine + RightMargin;
+ //SetHorizontalAlignment(widthOfLargestLine);
+
+ int lineIdx = 0;
+ foreach (var line in Lines)
+ {
+ double lineDist = widthOfLargestLine - line.GetWidthWithoutTrailingSpaces();
+ double prevWidth = CalculatePrevWidthBasedOnAlignment(lineDist);
+
+ foreach (var lineFragment in line.LineFragments)
+ {
+ var displayText = lineFragment.Text;
+
+ int rtIdx = -1;
+ if (_layoutSystem != null && lineFragment.OriginalTextFragment != null && _layoutSystem.InputFragments.Count > 0)
+ {
+ rtIdx = _layoutSystem.InputFragments.IndexOf(lineFragment.OriginalTextFragment);
+ }
+
+ var run = CreateTextRun(Bounds, displayText, rtIdx);
+ //Potentially we could import styling here instead but that leads to multiple issues.
+ //We may need to move it back here for auto-size reasons
+
+ run.YPosition = Lines.GetBaseLinePosition(lineIdx, lineSpacingResult);
+ run.Bounds.Left = prevWidth;
+ run.Bounds.Width = lineFragment.Width;
+ prevWidth += lineFragment.Width;
+
+ Runs.Add(run);
+ }
+ lineIdx++;
+ }
+ }
+ Bounds.Height = combinedHeight;
+ }
+
+ protected double CalculatePrevWidthBasedOnAlignment(double lineDist)
+ {
+ double prevWidth = 0;
+ if (HorizontalAlignment == TextAlignment.Center)
+ {
+ //Calculate difference in widths and split to get offset between leftmost position and current line
+ prevWidth = lineDist / 2;
+ }
+ else if (HorizontalAlignment == TextAlignment.Right)
+ {
+ //Note that the actual bounds with the space will be outside max bounds.
+ //This appears to be how excel does it
+ prevWidth = lineDist;
+ }
+
+ return prevWidth;
+ }
+
+ protected void SetHorizontalAlignment(double widthOfLargestLine)
+ {
+ if (HorizontalAlignment == TextAlignment.Center)
+ {
+ //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 = GetAlignmentHorizontal(HorizontalAlignment) - (widthOfLargestLine / 2);
+ }
+ else
+ {
+ //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;
+ }
+ }
+
+ void ImportStyles()
+ {
+ foreach (var run in Runs)
+ {
+ var rt = _layoutSystem.InputFragments[run.OriginalRtIdx].RichTextOptions;
+ if(rt is RichTextFormatDrawing)
+ {
+ //Import drawing data
+ run.ImportRichTextData((RichTextFormatDrawing)rt);
+ }
+ else if(rt is IRichTextFormatSimple)
+ {
+ //Import basic/cell data
+ run.ImportRichTextData((IRichTextFormatSimple)rt);
+ }
+ else
+ {
+ //Import only essential font data
+ run.ImportFontData((IFontFormatBase)rt);
+ }
+ }
+ }
+
+ public void AddRichText(IRichTextFormatSimple richText)
+ {
+ //TODO: Fix superScript/subScript should apply baseLine changes appropriately
+
+ AddRichTextBase(richText);
+ WrapTextFragmentsAndGenerateTextRuns();
+ }
+
+ public void AddRichText(IRichTextFormatDrawing richText)
+ {
+ //adjust size in accordance with baseline
+ richText.Size = richText.Baseline == 0 ? richText.Size : (float)(richText.Size * (1 - (Math.Abs(richText.Baseline) / 100)));
+
+ AddRichTextBase(richText);
+ WrapTextFragmentsAndGenerateTextRuns();
+ ImportStyles();
+ }
+
+ public void AddText(string text)
+ {
+ ImportLinesAndTextRunsBase(text);
+ ImportStyles();
+ }
+
+ protected abstract TextRunRenderItem CreateTextRun(BoundingBox parent, string displayText, int origRtIdx);
+ }
+}
diff --git a/src/EPPlus.DrawingRenderer/RenderItems/Textbox/RenderTextBody.cs b/src/EPPlus.DrawingRenderer/RenderItems/Textbox/RenderTextBody.cs
new file mode 100644
index 0000000000..f487503c84
--- /dev/null
+++ b/src/EPPlus.DrawingRenderer/RenderItems/Textbox/RenderTextBody.cs
@@ -0,0 +1,268 @@
+using EPPlus.Export.ImageRenderer.RenderItems.Shared;
+using EPPlus.Fonts.OpenType.Integration.DataHolders;
+using EPPlus.Graphics;
+
+namespace EPPlus.DrawingRenderer.RenderItems
+{
+ ///
+ /// Text anchoring
+ ///
+ public enum TextAnchoringType
+ {
+ ///
+ /// Anchor the text to the bottom
+ ///
+ Bottom,
+ ///
+ /// Anchor the text to the center
+ ///
+ Center,
+ ///
+ /// Anchor the text so that it is distributed vertically.
+ ///
+ Distributed,
+ ///
+ /// Anchor the text so that it is justified vertically.
+ ///
+ Justify,
+ ///
+ /// Anchor the text to the top
+ ///
+ Top
+ }
+
+ public abstract class RenderTextBody : GroupRenderItem
+ {
+ public RenderTextBody(RenderContext renderContext, BoundingBox parent, bool autoSize)
+ {
+ RenderContext = renderContext;
+ Bounds.Parent = parent;
+ AutoSize = autoSize;
+ MaxWidth = parent.Width;
+ MaxHeight = parent.Height;
+ Bounds.Name = "Textbody";
+ }
+ public RenderTextBody(RenderContext renderContext, BoundingBox parent, double left, double top, double maxWidth, double maxHeight, bool clampedToParent = false, bool autoSize=false) : this(renderContext, parent, autoSize)
+ {
+ RenderContext = renderContext;
+ Bounds.Left = left;
+ Bounds.Top = top;
+ Bounds.Width = maxWidth;
+ Bounds.Height = maxHeight;
+ MaxWidth = maxWidth;
+ MaxHeight = maxHeight;
+ Bounds.Name = "Textbody";
+ }
+
+ protected RenderContext RenderContext { get; private set; }
+ public List Paragraphs { get; set; } = new List();
+
+ public TextAnchoringType VerticalAlignment = TextAnchoringType.Top;
+ public string Text { get; set; }
+ public double MaxWidth { get; set; }
+ public double MaxHeight { get; set; }
+ ///
+ /// Shorthand for Bounds.Width
+ ///
+ public double Width { get { return Bounds.Width; } set { Bounds.Width = value; } }
+
+ ///
+ /// Shorthand for Bounds.Height
+ ///
+ public double Height { get { return Bounds.Height; } set { Bounds.Height = value; } }
+
+ public bool AutoSize { get; set; }
+ public double TopMargin { get; set; }
+ public double BottomMargin { get; set; }
+ public double RightMargin { get; set; }
+ public double LeftMargin { get; set; }
+ public string FontColorString { get; set; }
+
+
+ public void AppendRenderItems(List renderItems)
+ {
+ //foreach(var item in Paragraphs)
+ //{
+ // AddChildItem(item);
+ //}
+ //GroupRenderItem groupItem;
+ //if (Bounds.Parent.Rotation == 0) //If the parent is rotated, we should not apply rotation again. This is usually when the parent is a textbox.
+ //{
+ // groupItem = new GroupRenderItem(Bounds, Bounds.Rotation);
+ //}
+ //else
+ //{
+ // groupItem = new GroupRenderItem(Bounds);
+ //}
+
+ //if (FontColorString != null)
+ //{
+ // groupItem.GroupTransform += $" fill=\"{FontColorString}\"";
+ //}
+ //renderItems.Add(groupItem);
+
+ //Set bounds position to be translation
+ //Posibly remove translationOffset and make it always be bounds?
+ //But then we will have an inaccurate bounding box if a child object has negative position.
+ //TranslationOffset.Left = Bounds.Left;
+ //TranslationOffset.Top = Bounds.Top;
+
+ renderItems.Add(this);
+
+ var titleItem = new TitleRenderItem("TextBody group");
+ AddChildItem(titleItem);
+ foreach (var item in Paragraphs)
+ {
+ AddChildItem(item);
+ }
+ }
+
+ public ParagraphRenderItem AddParagraph(IRichTextFormatSimple rtFormat)
+ {
+ var paragraph = CreateParagraph(Bounds, rtFormat);
+ AdjustAndAddParagraph(paragraph);
+ return paragraph;
+ }
+
+ public ParagraphRenderItem AddParagraph(string text = null)
+ {
+ var paragraph = CreateParagraph(Bounds, text);
+ AdjustAndAddParagraph(paragraph);
+ return paragraph;
+ }
+
+ public void ApplyAutoSize()
+ {
+ if (AutoSize)
+ {
+ var currentHeight = 0d;
+ var currentWidth = 0d;
+
+ foreach(var paragraph in Paragraphs)
+ {
+ currentHeight += paragraph.Bounds.Height;
+
+ if (currentWidth < paragraph.Bounds.Width || currentWidth == MaxWidth)
+ {
+ currentWidth = paragraph.Bounds.Width;
+ }
+ }
+
+ Bounds.Width = currentWidth;
+ Bounds.Height = currentHeight;
+ }
+ }
+
+ ///
+ /// If text is added to the first paragraph without using textbody e.g. Paragraphs[0].AddText()
+ /// Subsequent paragraphs must be updated
+ ///
+ public void RecalculateParagraphs()
+ {
+ if(Paragraphs != null && Paragraphs.Count != 0)
+ {
+ double lastParagraphBottom = Paragraphs[0].Bounds.Top;
+
+ double smallestLeft = double.MaxValue;
+ double largestWidth = double.MinValue;
+ double totalHeight = 0;
+
+ foreach (var paragraph in Paragraphs)
+ {
+ paragraph.Bounds.Top = lastParagraphBottom;
+ lastParagraphBottom = paragraph.Bounds.Bottom;
+
+ smallestLeft = Math.Min(smallestLeft, paragraph.Bounds.Left);
+ largestWidth = Math.Max(largestWidth, paragraph.Bounds.Width);
+ totalHeight += paragraph.Bounds.Height;
+ }
+
+ ContentBounds.Top = Paragraphs[0].Bounds.Top;
+ ContentBounds.Left = smallestLeft;
+ ContentBounds.Width = largestWidth;
+ ContentBounds.Height = totalHeight;
+
+ if (AutoSize)
+ {
+ Bounds.Height = totalHeight;
+ Bounds.Width = ContentBounds.Width;
+ }
+ }
+ }
+
+ ///
+ /// The total bounds of all paragraphs without margins
+ ///
+ protected BoundingBox ContentBounds = new BoundingBox();
+
+ private void AdjustAndAddParagraph(ParagraphRenderItem paragraph)
+ {
+ paragraph.Bounds.Name = $"Container{Paragraphs.Count}";
+ paragraph.Bounds.Top = GetTopToAddNextParagraphAt();
+
+ 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);
+ RecalculateParagraphs();
+ }
+
+ private double GetTopToAddNextParagraphAt()
+ {
+ double paragraphTop = 0;
+
+ if (Paragraphs.Count != 0)
+ {
+ paragraphTop = Paragraphs.Last().Bounds.Bottom;
+ }
+ return paragraphTop;
+ }
+
+
+ ///
+ /// Get the start of text space vertically
+ ///
+ ///
+ public double GetAlignmentVertical()
+ {
+ double alignmentY = 0;
+
+ switch (VerticalAlignment)
+ {
+ case TextAnchoringType.Top:
+ alignmentY = Bounds.Top;
+ break;
+ //Center means center of a Shape's ENTIRE bounding box height.
+ //Not center of the Inset GetRectangle
+ case TextAnchoringType.Center:
+ if(AutoSize == false)
+ {
+ alignmentY = (Bounds.Height) / 2 - ContentBounds.Height;
+ }
+ break;
+ case TextAnchoringType.Bottom:
+ alignmentY = Bounds.Height - ContentBounds.Height;
+ break;
+ }
+
+ return alignmentY;
+ }
+
+ protected abstract ParagraphRenderItem CreateParagraph(BoundingBox parent, string textIfEmpty = "");
+
+ protected abstract ParagraphRenderItem CreateParagraph(BoundingBox parent, IRichTextFormatSimple richText);
+ }
+}
diff --git a/src/EPPlus.DrawingRenderer/RenderItems/Textbox/RenderTextRunCollection.cs b/src/EPPlus.DrawingRenderer/RenderItems/Textbox/RenderTextRunCollection.cs
new file mode 100644
index 0000000000..09e9e3e3ce
--- /dev/null
+++ b/src/EPPlus.DrawingRenderer/RenderItems/Textbox/RenderTextRunCollection.cs
@@ -0,0 +1,45 @@
+using System.Collections;
+
+namespace EPPlus.Export.ImageRenderer.RenderItems.Shared
+{
+ internal class RenderTextRunCollection : IEnumerable
+ {
+ private List _textRunItems;
+
+ public void Add(TextRunRenderItem item)
+ {
+ _textRunItems.Add(item);
+ }
+
+ ///
+ /// Number of items in the collection
+ ///
+ public int Count
+ {
+ get
+ {
+ return _textRunItems.Count;
+ }
+ }
+ IEnumerator IEnumerable.GetEnumerator()
+ {
+ return _textRunItems.GetEnumerator();
+ }
+ IEnumerator IEnumerable.GetEnumerator()
+ {
+ return _textRunItems.GetEnumerator();
+ }
+ ///
+ /// Returns a textrun at position
+ ///
+ /// The position of the chart. 0-base
+ ///
+ public TextRunRenderItem this[int PositionID]
+ {
+ get
+ {
+ return (_textRunItems[PositionID]);
+ }
+ }
+ }
+}
diff --git a/src/EPPlus.DrawingRenderer/RenderItems/Textbox/RenderTextbox.cs b/src/EPPlus.DrawingRenderer/RenderItems/Textbox/RenderTextbox.cs
new file mode 100644
index 0000000000..7939b54198
--- /dev/null
+++ b/src/EPPlus.DrawingRenderer/RenderItems/Textbox/RenderTextbox.cs
@@ -0,0 +1,252 @@
+using EPPlus.DrawingRenderer.RenderItems;
+using EPPlus.DrawingRenderer.Utils;
+using EPPlus.Graphics;
+namespace EPPlus.Export.ImageRenderer.RenderItems.SvgItem
+{
+ public class RenderTextbox : DrawingObject
+ {
+ public RenderTextbox(BoundingBox parent, double left, double top, double width, double height, double maxWidth = double.NaN, double maxHeight = double.NaN)
+ {
+ Init(parent, maxWidth, maxHeight);
+ Left = left;
+ Top = top;
+ }
+
+ public void Init(BoundingBox parent, double maxWidth, double maxHeight)
+ {
+ Parent = parent;
+ _group = new GroupRenderItem(Parent);
+ _rectangle = new RectRenderItem(_group.Bounds);
+ _marginGroup = new GroupRenderItem(_group.Bounds);
+ //TextBody = new RenderTextBody(Rectangle.Bounds, true);
+ //TextBody.MaxWidth = maxWidth;
+ //TextBody.MaxHeight = maxHeight;
+ }
+
+ public RenderTextbox(BoundingBox parent, double maxWidth, double maxHeight)
+ {
+ Init(parent, maxWidth, maxHeight);
+ }
+
+ //The origin point of the entire textbox itself (its outermost left and top point)
+ protected GroupRenderItem _group;
+ //The origin point of the textbody after applied margins
+ protected GroupRenderItem _marginGroup;
+
+
+ protected RectRenderItem _rectangle =null;
+ public RectRenderItem Rectangle
+ {
+ get
+ {
+ _rectangle.Bounds.Width = Width;
+ _rectangle.Bounds.Height = Height;
+ return _rectangle;
+ }
+ set
+ {
+ _rectangle = value;
+ }
+ }
+
+ RenderTextBody _textBody;
+
+ public virtual RenderTextBody TextBody
+ {
+ get { return _textBody; }
+ set
+ { _textBody = value;
+ //Margins should affect textbody global position in real-time
+ _textBody.Bounds.Parent = _marginGroup.Bounds;
+ }
+ }
+ public double Left
+ {
+ get
+ {
+ return _group.Bounds.Left;
+
+ }
+ set
+ {
+ _group.Bounds.Left = value;
+ }
+ }
+ public double Top
+ {
+ get
+ {
+ return _group.Bounds.Top;
+ }
+ set
+ {
+ _group.Bounds.Top = value;
+ }
+ }
+ public double Width
+ {
+ get
+ {
+ return LeftMargin + (TextBody?.Bounds?.Width ?? 0D) + RightMargin + TextBody.Left;
+ }
+ }
+ public double WidthRotated
+ {
+ get
+ {
+ var radians = MathHelper.Radians(Rotation);
+ var sin = Math.Abs(Math.Sin(radians));
+ var cos = Math.Abs(Math.Cos(radians));
+ return Width * sin + Height * cos;
+ }
+ }
+ public double Height
+ {
+ get
+ {
+ return TopMargin + (TextBody?.Bounds.Height ?? 0d) + BottomMargin + TextBody.Top;
+ }
+ }
+ public double HeightWithRotation
+ {
+ get
+ {
+ var radians = MathHelper.Radians(Rotation);
+ var sin = Math.Abs(Math.Sin(radians));
+ var cos = Math.Abs(Math.Cos(radians));
+ return Width * cos + Height * sin;
+ }
+ }
+ public double LeftMargin
+ {
+ get { return _marginGroup.Left; } set { _marginGroup.Left = value; }
+ }
+
+ public double TopMargin
+ {
+ get { return _marginGroup.Top; }
+ set { _marginGroup.Top = value; }
+ }
+
+ public double RightMargin
+ {
+ get; set;
+ }
+
+ public double BottomMargin
+ {
+ get; set;
+ }
+
+ internal protected BoundingBox Parent { get; protected set; }
+ public double Rotation
+ {
+ get
+ {
+ return _group.Rotation;
+ }
+ set
+ {
+ _group.Rotation = value;
+ }
+ }
+ ///
+ /// Gets the actual width of the rotated textbox.
+ ///
+ ///
+ public 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.
+ ///
+ ///
+ public double GetActualRight()
+ {
+ return Left+GetActualWidth();
+ }
+ ///
+ /// Gets the actual height of the rotated textbox.
+ ///
+ ///
+ public 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.
+ ///
+ ///
+ public double GetActualBottom()
+ {
+ return Top + GetActualHeight();
+ }
+ public override void AppendRenderItems(List renderItems)
+ {
+ var rect = Rectangle;
+
+ //As the rect item is inside the group, we set the left and right to the group and top and left on the rect to 0.
+ _group.Bounds.Left = Left;
+ _group.Bounds.Top = Top;
+ _group.Bounds.Width = Width;
+ _group.Bounds.Height = Height;
+
+ _group.TextAnchor = TextAnchor.ToEnumString();
+ renderItems.Add(_group);
+ rect.Top = 0;
+ rect.Left = 0;
+
+ if (TextBody.AutoSize)
+ {
+ TextBody.ApplyAutoSize();
+ }
+
+ rect.Width = Width;
+ rect.Height = Height;
+
+ var titleItem = new TitleRenderItem("TextBox group");
+ _group.RenderItems.Add(titleItem);
+ //The rect shound encapse the text element, so we need to set the left depending on the text anchor.
+ if (TextAnchor == eTextAnchor.Middle)
+ {
+ _group.Bounds.Left += -(rect.Bounds.Width / 2);
+ }
+ else if (TextAnchor == eTextAnchor.End)
+ {
+ if (Math.Abs(Rotation) == 45)
+ {
+ const double COS45 = 0.70710678118654757; //Constant for Math.Sin(Math.PI / 4) --45 degrees
+ _group.Bounds.Left += -(rect.Bounds.Width * COS45);
+ _group.Bounds.Top += (rect.Bounds.Width * COS45);
+ }
+ else
+ {
+ _group.Bounds.Left += rect.Bounds.Height / 2;
+ _group.Bounds.Top += (rect.Bounds.Width);
+ }
+ }
+ _group.RenderItems.Add(rect);
+
+ //The textbox should be in local-space.
+ //If e.g. a user changes textbody left and right, changing margin on the parent should not change the Local coordinates
+ //Therefore a group in-between should hold the margins
+ _marginGroup.Left = LeftMargin;
+ _marginGroup.Top = TopMargin;
+
+ var marginTitleItem = new TitleRenderItem("TextBox Margin Group");
+ _marginGroup.AddChildItem(marginTitleItem);
+
+ _group.AddChildItem(_marginGroup);
+ TextBody.AppendRenderItems(_marginGroup.RenderItems);
+ }
+ ///
+ /// How the text is anchored.
+ ///
+ public eTextAnchor TextAnchor
+ {
+ get;
+ set;
+ }
+ }
+}
diff --git a/src/EPPlus.DrawingRenderer/RenderItems/Textbox/RichTextFormatDrawing/IRichTextFormatDrawing.cs b/src/EPPlus.DrawingRenderer/RenderItems/Textbox/RichTextFormatDrawing/IRichTextFormatDrawing.cs
new file mode 100644
index 0000000000..548a9ab0ec
--- /dev/null
+++ b/src/EPPlus.DrawingRenderer/RenderItems/Textbox/RichTextFormatDrawing/IRichTextFormatDrawing.cs
@@ -0,0 +1,36 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using EPPlus.Fonts.OpenType.Integration.DataHolders;
+using EPPlus.Export.ImageRenderer.RenderItems.Shared;
+using System.Drawing;
+
+namespace EPPlus.DrawingRenderer.RenderItems.Textbox
+{
+ ///
+ /// TODO: Move this to interfaces. Only here in order to not break existing references in PDF
+ /// (This should be moved when IRichTextFormatSimple is moved)
+ ///
+ /// Rich text data for drawings
+ ///
+ public interface IRichTextFormatDrawing : IRichTextFormatSimple
+ {
+ public new eDrawingStrikeType StrikeType { get; set; } /*{ get { return (DrawingStrikeType)StrikeType; } set { StrikeType = (int)value; } }*/
+ public new eDrawingUnderLineType UnderlineType { get; set; }
+ Color? HighLightColor { get; set; }
+ ///
+ /// The spacing between characters within a text run.
+ ///
+ double Spacing { get; set; }
+
+ ///
+ /// +Superscript or -Subscript offset in percent
+ /// (default 30% Super and -25% subscript)
+ ///
+ public double Baseline { get; set; }
+
+ //TODO: Advanced fills/Textoutline
+ //TODO: Effects once implemented in Epplus
+ }
+}
diff --git a/src/EPPlus.DrawingRenderer/RenderItems/Textbox/RichTextFormatDrawing/RichTextFormatDrawing.cs b/src/EPPlus.DrawingRenderer/RenderItems/Textbox/RichTextFormatDrawing/RichTextFormatDrawing.cs
new file mode 100644
index 0000000000..cc4df26c5f
--- /dev/null
+++ b/src/EPPlus.DrawingRenderer/RenderItems/Textbox/RichTextFormatDrawing/RichTextFormatDrawing.cs
@@ -0,0 +1,110 @@
+using System;
+using System.Collections.Generic;
+using System.Drawing;
+using System.Linq;
+using System.Text;
+using EPPlus.Export.ImageRenderer.RenderItems.Shared;
+using EPPlus.Fonts.OpenType.Integration.DataHolders;
+
+namespace EPPlus.DrawingRenderer.RenderItems.Textbox
+{
+ ///
+ /// Default richText data class for drawings
+ ///
+ public class RichTextFormatDrawing : RichTextFormatSimple, IRichTextFormatDrawing
+ {
+ public new eDrawingStrikeType StrikeType { get => (eDrawingStrikeType)base.StrikeType; set => base.StrikeType = (int)value; }
+ public new eDrawingUnderLineType UnderlineType { get => (eDrawingUnderLineType)base.UnderlineType; set => base.UnderlineType = (int)value; }
+
+ public Color? HighLightColor { get; set; }
+ public double Spacing { get; set; } = 0d;
+
+ double _baseLine = 0d;
+
+ private bool _subScript = false;
+ private bool _superScript = false;
+
+ ///
+ /// +Superscript or -Subscript offset in percent
+ /// (default 30% Super and -25% subscript)
+ ///
+ public double Baseline
+ {
+ get
+ {
+ return _baseLine;
+ }
+ set
+ {
+ if (value > 0d)
+ {
+ _superScript = true;
+ _subScript = false;
+ }
+ else if (value < 0d)
+ {
+ _superScript = false;
+ _subScript = true;
+ }
+ else
+ {
+ //When offset is 0 it is neither a sub or super script
+ _superScript = false;
+ _subScript = false;
+ }
+
+ _baseLine = value;
+ }
+ }
+ public new bool SubScript
+ {
+ get
+ {
+ return _subScript;
+ }
+ set
+ {
+ if (value == true)
+ {
+ _superScript = false;
+ Baseline = -25d;
+ }
+ _subScript = value;
+ }
+ }
+
+ public new bool SuperScript
+ {
+ get
+ {
+ return _superScript;
+ }
+ set
+ {
+ if (value == true)
+ {
+ _subScript = false;
+ Baseline = 30d;
+ }
+ _superScript = value;
+ }
+ }
+
+ public RichTextFormatDrawing() : base()
+ {
+ StrikeType = eDrawingStrikeType.No;
+ UnderlineType = eDrawingUnderLineType.None;
+ }
+
+ public RichTextFormatDrawing(string text, string fontFamily, float size, bool bold = false, bool italic = false) : base(text, fontFamily, size, bold, italic)
+ {
+ StrikeType = eDrawingStrikeType.No;
+ UnderlineType = eDrawingUnderLineType.None;
+ }
+
+ public RichTextFormatDrawing(FontFormatBase defaultParagraphFont)
+ {
+ SetFont(defaultParagraphFont);
+ }
+ }
+}
diff --git a/src/EPPlus.DrawingRenderer/RenderItems/Textbox/TextRunRenderItem.cs b/src/EPPlus.DrawingRenderer/RenderItems/Textbox/TextRunRenderItem.cs
new file mode 100644
index 0000000000..0b04d4f66b
--- /dev/null
+++ b/src/EPPlus.DrawingRenderer/RenderItems/Textbox/TextRunRenderItem.cs
@@ -0,0 +1,227 @@
+using EPPlus.DrawingRenderer;
+using EPPlus.DrawingRenderer.RenderItems;
+using EPPlus.DrawingRenderer.RenderItems.Textbox;
+using EPPlus.Fonts.OpenType.Integration.DataHolders;
+using EPPlus.Fonts.OpenType.Utils;
+using EPPlus.Graphics;
+using EPPlusImageRenderer.Utils;
+using OfficeOpenXml.Interfaces.Drawing.Text;
+using OfficeOpenXml.Interfaces.RichText;
+using System.Drawing;
+using System.Text.RegularExpressions;
+
+namespace EPPlus.Export.ImageRenderer.RenderItems.Shared
+{
+ ///
+ /// Linestyle
+ ///
+ public enum eDrawingUnderLineType
+ {
+ ///
+ /// Dashed
+ ///
+ Dash,
+ ///
+ /// Dashed, Thicker
+ ///
+ DashHeavy,
+ ///
+ /// Dashed Long
+ ///
+ DashLong,
+ ///
+ /// Long Dashed, Thicker
+ ///
+ DashLongHeavy,
+ ///
+ /// Double lines with normal thickness
+ ///
+ Double,
+ ///
+ /// Dot Dash
+ ///
+ DotDash,
+ ///
+ /// Dot Dash, Thicker
+ ///
+ DotDashHeavy,
+ ///
+ /// Dot Dot Dash
+ ///
+ DotDotDash,
+ ///
+ /// Dot Dot Dash, Thicker
+ ///
+ DotDotDashHeavy,
+ ///
+ /// Dotted
+ ///
+ Dotted,
+ ///
+ /// Dotted, Thicker
+ ///
+ DottedHeavy,
+ ///
+ /// Single line, Thicker
+ ///
+ Heavy,
+ ///
+ /// No underline
+ ///
+ None,
+ ///
+ /// Single line
+ ///
+ Single,
+ ///
+ /// A single wavy line
+ ///
+ Wavy,
+ ///
+ /// A double wavy line
+ ///
+ WavyDbl,
+ ///
+ /// A single wavy line, Thicker
+ ///
+ WavyHeavy,
+ ///
+ /// Underline just the words and not the spaces between them
+ ///
+ Words
+ }
+ ///
+ /// BulletType of font strike
+ ///
+ public enum eDrawingStrikeType
+ {
+ ///
+ /// Double-lined font strike
+ ///
+ Double,
+ ///
+ /// No font strike
+ ///
+ No,
+ ///
+ /// Single-lined font strike
+ ///
+ Single
+ }
+ public abstract class TextRunRenderItem : RenderItem
+ {
+ public override RenderItemType Type => RenderItemType.TextRun;
+
+ public int OriginalRtIdx { get; private set; } = -1;
+
+ protected string _originalText;
+ public string _currentText { get; protected set; }
+
+ public IFontFormatBase _measurementFont { get; internal protected set; }
+ protected bool _isFirstInParagraph;
+
+ public double FontSizeInPixels { get; protected set; }
+
+ public List Lines { get; set; }
+
+ protected internal bool _isItalic = false;
+ protected internal bool _isBold = false;
+ protected internal eDrawingUnderLineType _underLineType = eDrawingUnderLineType.None;
+ protected internal eDrawingStrikeType _strikeType = eDrawingStrikeType.No;
+ protected internal Color _underlineColor;
+ protected internal double _baseline;
+
+ public double YPosition { get; set; }
+ public double ClippingHeight { get; protected set; } = double.NaN;
+ public TextRunRenderItem(BoundingBox parent) : base(parent)
+ {
+ Bounds.Name = "TextRun";
+ }
+
+ public TextRunRenderItem(BoundingBox parent, string text, int origRtIdx) : base(parent)
+ {
+ Bounds.Name = "TextRun";
+ _currentText = text;
+ OriginalRtIdx = origRtIdx;
+ }
+
+ public void ImportFontData(IFontFormatBase font)
+ {
+ InitializeBase(font);
+ }
+
+ public void ImportRichTextData(IRichTextFormatSimple rt)
+ {
+ InitializeBase(rt);
+ FillColor = "#" + rt.FontColor.To6CharHexStringImage();
+ _underLineType = (int)rt.UnderlineType == -1 ? eDrawingUnderLineType.None : (eDrawingUnderLineType)rt.UnderlineType;
+ _underlineColor = rt.UnderlineColor;
+ }
+
+ public void ImportRichTextData(IRichTextFormatDrawing rt)
+ {
+ InitializeBase(rt);
+ FillColor = "#" + rt.FontColor.To6CharHexStringImage();
+ _baseline = rt.Baseline;
+ _strikeType = (int)rt.StrikeType == -1 ? eDrawingStrikeType.No : rt.StrikeType;
+ _underLineType = (int)rt.UnderlineType == -1 ? eDrawingUnderLineType.None : rt.UnderlineType;
+ }
+
+ internal protected void InitializeBase(IFontFormatBase font)
+ {
+ //Should be ascent-only?
+ Bounds.Height = font.Size;
+ FontSizeInPixels = ((double)font.Size).PointToPixel(true);
+ _measurementFont = font;
+ }
+
+ ///
+ /// Initialization for the two lower constructors
+ /// Not a Initialize() method since compiler warns of un-initalized variables if you do.
+ ///
+ ///
+ ///
+ ///
+ ///
+ private TextRunRenderItem(BoundingBox parent, string origText, string currentText, IFontFormatBase font) : base(parent)
+ {
+ Bounds.Name = "TextRun";
+
+ //possibly no longer neccesary
+ _originalText = origText;
+
+ _currentText = currentText;
+
+ //Should be ascent-only?
+ Bounds.Height = font.Size;
+
+ //Possibly no longer neccesary
+ Lines = Regex.Split(_currentText, "\r\n|\r|\n").ToList();
+
+ FontSizeInPixels = ((double)font.Size).PointToPixel(true);
+
+ _measurementFont = font;
+ _isFirstInParagraph = true;
+ }
+
+ public TextRunRenderItem(BoundingBox parent, IFontFormatBase font, string displayText)
+ : this(parent, displayText, displayText, font)
+ {
+ ////Dash is default but we know there is no underline in our input here
+ //_underLineType = UnderLineType.None;
+ }
+ public TextRunRenderItem(BoundingBox parent, string text, IFontFormatBase font, string displayText)
+ : this(parent, text, string.IsNullOrEmpty(displayText) ? text : displayText, font)
+ {
+ }
+
+ internal protected void CalculateClippingHeightFromTextBodyParent()
+ {
+ //To get clipping height we need to get the textbody bounds
+ if (Bounds.Parent != null && Bounds.Parent.Parent != null && Bounds.Parent.Parent.Parent != null)
+ {
+ ClippingHeight = Bounds.Parent.Parent.Parent.Position.Y + Bounds.Parent.Parent.Parent.Size.Y;
+ }
+ }
+ }
+}
diff --git a/src/EPPlus.DrawingRenderer/RenderItems/TitleItem.cs b/src/EPPlus.DrawingRenderer/RenderItems/TitleItem.cs
new file mode 100644
index 0000000000..d634505ae1
--- /dev/null
+++ b/src/EPPlus.DrawingRenderer/RenderItems/TitleItem.cs
@@ -0,0 +1,15 @@
+namespace EPPlus.DrawingRenderer.RenderItems
+{
+ public class TitleRenderItem : RenderItem
+ {
+ public string Title { get; private set; }
+
+ public TitleRenderItem(string titleName) : base()
+ {
+ Title = titleName;
+ }
+
+ public override RenderItemType Type => RenderItemType.CommentTitle;
+
+ }
+}
\ No newline at end of file
diff --git a/src/EPPlus.DrawingRenderer/ShapeDefinitions/ArcTo.cs b/src/EPPlus.DrawingRenderer/ShapeDefinitions/ArcTo.cs
new file mode 100644
index 0000000000..a102737084
--- /dev/null
+++ b/src/EPPlus.DrawingRenderer/ShapeDefinitions/ArcTo.cs
@@ -0,0 +1,128 @@
+using System.Xml;
+
+namespace EPPlus.DrawingRenderer.ShapeDefinitions
+{
+ public class ArcTo : PathsBase
+ {
+ public ArcTo(XmlReader xr)
+ {
+ if (long.TryParse(xr.GetAttribute("hR"), out var hrv))
+ {
+ HeightRadius = hrv;
+ }
+ else
+ {
+ HeightRadiusName = xr.GetAttribute("hR");
+ }
+
+ if (long.TryParse(xr.GetAttribute("wR"), out var wrv))
+ {
+ WidthRadius = wrv;
+ }
+ else
+ {
+ WidthRadiusName = xr.GetAttribute("wR");
+ }
+
+ if (long.TryParse(xr.GetAttribute("swAng"), out var swAng))
+ {
+ SwingAngle = swAng;
+ }
+ else
+ {
+ SwingAngleName = xr.GetAttribute("swAng");
+ }
+
+ if (long.TryParse(xr.GetAttribute("stAng"), out var stAng))
+ {
+ StartAngle = stAng;
+ }
+ else
+ {
+ StartAngleName = xr.GetAttribute("stAng");
+ }
+ }
+ public ArcTo(XmlElement e)
+ {
+ if (long.TryParse(e.GetAttribute("hR"), out var hrv))
+ {
+ HeightRadius = hrv;
+ }
+ else
+ {
+ HeightRadiusName = e.GetAttribute("hR");
+ }
+
+ if (long.TryParse(e.GetAttribute("wR"), out var wrv))
+ {
+ WidthRadius = wrv;
+ }
+ else
+ {
+ WidthRadiusName = e.GetAttribute("wR");
+ }
+
+ if (long.TryParse(e.GetAttribute("swAng"), out var swAng))
+ {
+ SwingAngle = swAng;
+ }
+ else
+ {
+ SwingAngleName = e.GetAttribute("swAng");
+ }
+
+ if (long.TryParse(e.GetAttribute("stAng"), out var stAng))
+ {
+ StartAngle = stAng;
+ }
+ else
+ {
+ StartAngleName = e.GetAttribute("stAng");
+ }
+ }
+ public override PathDrawingType Type => PathDrawingType.ArcTo;
+ public double? HeightRadius { get; set; }
+ public double? StartAngle { get; set; }
+ public double? SwingAngle { get; set; }
+ public double? WidthRadius { get; set; }
+ public string HeightRadiusName { get; set; }
+ public string StartAngleName { get; set; }
+ public string SwingAngleName { get; set; }
+ public string WidthRadiusName { get; set; }
+ private ArcTo()
+ {
+
+ }
+ internal override PathsBase Clone()
+ {
+ return new ArcTo()
+ {
+ HeightRadius = HeightRadius,
+ StartAngle = StartAngle,
+ SwingAngle = SwingAngle,
+ WidthRadius = WidthRadius,
+ HeightRadiusName = HeightRadiusName,
+ StartAngleName = StartAngleName,
+ SwingAngleName = SwingAngleName,
+ WidthRadiusName = WidthRadiusName
+ };
+ }
+ double _endX, _endY;
+ public void SetEndCoordinates(double x, double y)
+ {
+ _endX = x;
+ _endY = y;
+ }
+ public override void TranslateCoordiantesToPointsAndDegrees(double coordinateRatio, double angleRatio)
+ {
+ HeightRadius /= coordinateRatio;
+ WidthRadius /= coordinateRatio;
+ StartAngle /= angleRatio;
+ SwingAngle /= angleRatio;
+ _endX = _endX / coordinateRatio;
+ _endY = _endY / coordinateRatio;
+ }
+ public override double EndX => _endX;
+ public override double EndY => _endY;
+ }
+}
diff --git a/src/EPPlus.DrawingRenderer/ShapeDefinitions/ClosePath.cs b/src/EPPlus.DrawingRenderer/ShapeDefinitions/ClosePath.cs
new file mode 100644
index 0000000000..9f7c21ed29
--- /dev/null
+++ b/src/EPPlus.DrawingRenderer/ShapeDefinitions/ClosePath.cs
@@ -0,0 +1,20 @@
+namespace EPPlus.DrawingRenderer.ShapeDefinitions
+{
+ public class ClosePath : PathsBase
+ {
+ public ClosePath()
+ {
+
+ }
+ public override PathDrawingType Type => PathDrawingType.Close;
+ internal override PathsBase Clone()
+ {
+ return new ClosePath();
+ }
+ public override double EndX => double.MinValue;
+ public override double EndY => double.MinValue;
+ public override void TranslateCoordiantesToPointsAndDegrees(double coordinateRatio, double angleRatio)
+ {
+ }
+ }
+}
diff --git a/src/EPPlus.DrawingRenderer/ShapeDefinitions/CubicBezerTo.cs b/src/EPPlus.DrawingRenderer/ShapeDefinitions/CubicBezerTo.cs
new file mode 100644
index 0000000000..704c9c9511
--- /dev/null
+++ b/src/EPPlus.DrawingRenderer/ShapeDefinitions/CubicBezerTo.cs
@@ -0,0 +1,26 @@
+using System.Xml;
+
+namespace EPPlus.DrawingRenderer.ShapeDefinitions
+{
+ public class CubicBezerTo : PathWithCoordinates
+ {
+ public CubicBezerTo(CubicBezerTo clone) : base(clone)
+ {
+
+ }
+ public CubicBezerTo(XmlReader xr) : base(xr)
+ {
+
+ }
+ public CubicBezerTo(XmlElement e) : base(e)
+ {
+
+ }
+
+ public override PathDrawingType Type => PathDrawingType.CubicBezierTo;
+ internal override PathsBase Clone()
+ {
+ return new CubicBezerTo(this);
+ }
+ }
+}
diff --git a/src/EPPlus.DrawingRenderer/ShapeDefinitions/DrawCoordinate.cs b/src/EPPlus.DrawingRenderer/ShapeDefinitions/DrawCoordinate.cs
new file mode 100644
index 0000000000..813524a10c
--- /dev/null
+++ b/src/EPPlus.DrawingRenderer/ShapeDefinitions/DrawCoordinate.cs
@@ -0,0 +1,40 @@
+namespace EPPlus.DrawingRenderer.ShapeDefinitions
+{
+ public class DrawCoordinate
+ {
+ public DrawCoordinate(DrawCoordinate c)
+ {
+ X = c.X;
+ Y = c.Y;
+ XName = c.XName;
+ YName = c.YName;
+ }
+
+ public DrawCoordinate(object x, object y)
+ {
+ if (x is long xl)
+ {
+ X = xl;
+ }
+ else
+ {
+ XName = x.ToString();
+ X = null;
+ }
+ if (y is long yl)
+ {
+ Y = yl;
+ }
+ else
+ {
+ YName = y.ToString();
+ Y = null;
+ }
+
+ }
+ public double? X { get; set; }
+ public double? Y { get; set; }
+ public string XName { get; set; }
+ public string YName { get; set; }
+ }
+}
diff --git a/src/EPPlus.DrawingRenderer/ShapeDefinitions/DrawingPath.cs b/src/EPPlus.DrawingRenderer/ShapeDefinitions/DrawingPath.cs
new file mode 100644
index 0000000000..e9c3e47845
--- /dev/null
+++ b/src/EPPlus.DrawingRenderer/ShapeDefinitions/DrawingPath.cs
@@ -0,0 +1,117 @@
+using EPPlus.DrawingRenderer.Utils;
+using System;
+using System.Collections.Generic;
+using System.Text;
+using System.Xml;
+
+namespace EPPlus.DrawingRenderer.ShapeDefinitions
+{
+ public class DrawingPath
+ {
+ public DrawingPath(DrawingPath clone)
+ {
+ Width = clone.Width;
+ Height = clone.Height;
+ Fill = clone.Fill;
+ Stroke = clone.Stroke;
+ ExtrusionOk = clone.ExtrusionOk;
+ foreach (var p in clone.Paths)
+ {
+ Paths.Add(p.Clone());
+ }
+ }
+ public DrawingPath(XmlReader xr)
+ {
+ Width = ConvertUtil.GetValueLongNull(xr.GetAttribute("w"));
+ Height = ConvertUtil.GetValueLongNull(xr.GetAttribute("h"));
+ Fill = GetFill(xr.GetAttribute("fill"));
+ Stroke = ConvertUtil.ToBooleanString(xr.GetAttribute("stroke"), true);
+ ExtrusionOk = ConvertUtil.ToBooleanString(xr.GetAttribute("extrusionOk"), false);
+ while (xr.Read())
+ {
+ if (xr.NodeType == XmlNodeType.Element)
+ {
+ switch (xr.LocalName)
+ {
+ case "moveTo":
+ Paths.Add(new MoveTo(xr));
+ break;
+ case "lnTo":
+ Paths.Add(new LineTo(xr));
+ break;
+ case "cubicBezTo":
+ Paths.Add(new CubicBezerTo(xr));
+ break;
+ case "quadBezTo":
+ Paths.Add(new QuadBezerTo(xr));
+ break;
+ case "arcTo":
+ Paths.Add(new ArcTo(xr));
+ break;
+ case "close":
+ Paths.Add(new ClosePath());
+ break;
+ }
+ }
+ else if (xr.LocalName == "path" && xr.NodeType == XmlNodeType.EndElement)
+ {
+ break;
+ }
+ }
+ }
+
+ public DrawingPath(XmlElement topNode, XmlNamespaceManager nsm)
+ {
+ Width = int.Parse(topNode.GetAttribute("w"));
+ Height = int.Parse(topNode.GetAttribute("h"));
+ Fill = GetFill(topNode.GetAttribute("fill"));
+ Stroke = ConvertUtil.ToBooleanString(topNode.GetAttribute("stroke"), true);
+ ExtrusionOk = ConvertUtil.ToBooleanString(topNode.GetAttribute("extrusionOk"), true);
+ foreach (var child in topNode.ChildNodes)
+ {
+ if (child is XmlElement e)
+ {
+ switch (e.LocalName)
+ {
+ case "moveTo":
+ Paths.Add(new MoveTo(e));
+ break;
+ case "lnTo":
+ Paths.Add(new LineTo(e));
+ break;
+ case "cubicBezTo":
+ Paths.Add(new CubicBezerTo(e));
+ break;
+ case "quadBezTo":
+ Paths.Add(new CubicBezerTo(e));
+ break;
+ case "arcTo":
+ Paths.Add(new ArcTo(e));
+ break;
+ case "close":
+ Paths.Add(new ClosePath());
+ break;
+ }
+ }
+ }
+ }
+
+ private PathFillMode GetFill(string s)
+ {
+ if (string.IsNullOrEmpty(s) == false)
+ {
+ return (PathFillMode)Enum.Parse(typeof(PathFillMode), s, true);
+ }
+ return PathFillMode.Norm;
+ }
+
+ public DrawingPath Clone() => new DrawingPath(this);
+
+ public bool Stroke { get; set; }
+ public bool ExtrusionOk { get; set; }
+ public PathFillMode Fill { get; set; }
+ public double? Width { get; set; }
+ public double? Height { get; set; }
+ public List Paths { get; set; } = new List();
+ }
+}
diff --git a/src/EPPlus.DrawingRenderer/ShapeDefinitions/LineTo.cs b/src/EPPlus.DrawingRenderer/ShapeDefinitions/LineTo.cs
new file mode 100644
index 0000000000..d8ed12dcfa
--- /dev/null
+++ b/src/EPPlus.DrawingRenderer/ShapeDefinitions/LineTo.cs
@@ -0,0 +1,27 @@
+using System.Xml;
+
+namespace EPPlus.DrawingRenderer.ShapeDefinitions
+{
+ public class LineTo : PathWithCoordinates
+ {
+ public LineTo(LineTo clone) : base(clone)
+ {
+
+ }
+ public LineTo(XmlReader xr) : base(xr)
+ {
+
+ }
+
+ public LineTo(XmlElement e) : base(e)
+ {
+
+ }
+ public override PathDrawingType Type => PathDrawingType.LineTo;
+ public DrawCoordinate Coordinate { get; set; }
+ internal override PathsBase Clone()
+ {
+ return new LineTo(this);
+ }
+ }
+}
diff --git a/src/EPPlus.DrawingRenderer/ShapeDefinitions/MoveTo.cs b/src/EPPlus.DrawingRenderer/ShapeDefinitions/MoveTo.cs
new file mode 100644
index 0000000000..035229333c
--- /dev/null
+++ b/src/EPPlus.DrawingRenderer/ShapeDefinitions/MoveTo.cs
@@ -0,0 +1,25 @@
+using System.Xml;
+
+namespace EPPlus.DrawingRenderer.ShapeDefinitions
+{
+ public class MoveTo : PathWithCoordinates
+ {
+ public MoveTo(MoveTo clone) : base(clone)
+ {
+
+ }
+ public MoveTo(XmlElement e) : base(e)
+ {
+ }
+ public MoveTo(XmlReader xr) : base(xr)
+ {
+ }
+ public override PathDrawingType Type => PathDrawingType.MoveTo;
+ public DrawCoordinate Coordinate { get; set; }
+
+ internal override PathsBase Clone()
+ {
+ return new MoveTo(this);
+ }
+ }
+}
diff --git a/src/EPPlus.DrawingRenderer/ShapeDefinitions/PathWithCoordinates.cs b/src/EPPlus.DrawingRenderer/ShapeDefinitions/PathWithCoordinates.cs
new file mode 100644
index 0000000000..3e114aa706
--- /dev/null
+++ b/src/EPPlus.DrawingRenderer/ShapeDefinitions/PathWithCoordinates.cs
@@ -0,0 +1,64 @@
+using EPPlus.DrawingRenderer.Utils;
+using System.Globalization;
+using System.Xml;
+
+namespace EPPlus.DrawingRenderer.ShapeDefinitions
+{
+ public abstract class PathWithCoordinates : PathsBase
+ {
+ protected PathWithCoordinates(XmlElement e)
+ {
+ foreach (var cn in e.ChildNodes)
+ {
+ if (cn is XmlElement ce && ce.LocalName == "pt")
+ {
+ Coordinates.Add(new DrawCoordinate(GetNameOrNumber(ce.GetAttribute("x")), GetNameOrNumber(ce.GetAttribute("y"))));
+ }
+ }
+ }
+
+ private object GetNameOrNumber(string s)
+ {
+ if (long.TryParse(s, NumberStyles.Number, CultureInfo.InvariantCulture, out var l))
+ {
+ return l;
+ }
+ return s;
+ }
+
+ protected PathWithCoordinates(XmlReader xr)
+ {
+ var name = xr.LocalName;
+ while (xr.Read())
+ {
+ if (xr.LocalName == "pt" && xr.NodeType == XmlNodeType.Element)
+ {
+ Coordinates.Add(new DrawCoordinate(GetNameOrNumber(xr.GetAttribute("x")), GetNameOrNumber(xr.GetAttribute("y"))));
+ }
+ else if (xr.IsEndElementWithName(name))
+ {
+ break;
+ }
+ }
+ }
+
+ protected PathWithCoordinates(PathWithCoordinates clone)
+ {
+ foreach (var c in clone.Coordinates)
+ {
+ Coordinates.Add(new DrawCoordinate(c));
+ }
+ }
+ public List Coordinates { get; set; } = new List();
+ public override void TranslateCoordiantesToPointsAndDegrees(double coordinateRatio, double angleRatio)
+ {
+ foreach (var c in Coordinates)
+ {
+ c.X /= coordinateRatio;
+ c.Y /= coordinateRatio;
+ }
+ }
+ public override double EndX => Coordinates.Count > 0D ? Coordinates[Coordinates.Count - 1].X.Value : 0D;
+ public override double EndY => Coordinates.Count > 0D ? Coordinates[Coordinates.Count - 1].Y.Value : 0D;
+ }
+}
diff --git a/src/EPPlus.DrawingRenderer/ShapeDefinitions/PathsBase.cs b/src/EPPlus.DrawingRenderer/ShapeDefinitions/PathsBase.cs
new file mode 100644
index 0000000000..f7b591cd6f
--- /dev/null
+++ b/src/EPPlus.DrawingRenderer/ShapeDefinitions/PathsBase.cs
@@ -0,0 +1,12 @@
+namespace EPPlus.DrawingRenderer.ShapeDefinitions
+{
+ public abstract class PathsBase
+ {
+ public abstract PathDrawingType Type { get; }
+
+ internal abstract PathsBase Clone();
+ public abstract double EndX { get; }
+ public abstract double EndY { get; }
+ public abstract void TranslateCoordiantesToPointsAndDegrees(double coordinateRatio, double angleRatio);
+ }
+}
diff --git a/src/EPPlus.DrawingRenderer/ShapeDefinitions/PresetShapeDefinition.Sync.cs b/src/EPPlus.DrawingRenderer/ShapeDefinitions/PresetShapeDefinition.Sync.cs
new file mode 100644
index 0000000000..5449c06b88
--- /dev/null
+++ b/src/EPPlus.DrawingRenderer/ShapeDefinitions/PresetShapeDefinition.Sync.cs
@@ -0,0 +1,253 @@
+/*************************************************************************************************
+ 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
+ *************************************************************************************************/
+using System;
+using System.Collections.Generic;
+using System.IO;
+using System.Xml;
+using EPPlus.DrawingRenderer;
+using EPPlus.DrawingRenderer.ShapeDefinitions;
+using EPPlus.DrawingRenderer.Utils;
+
+namespace EPPlus.DrawingRenderer.ShapeDefinitions
+{
+ public partial class PresetShapeDefinitions
+ {
+ public static void LoadPresetShapeDefinitionFromXml()
+ {
+ var xmlFile = Directory.GetCurrentDirectory() + "\\resource\\presetShapeDefinitions.xml";
+
+ try
+ {
+ var ms = new MemoryStream(File.ReadAllBytes(xmlFile));
+#if NET35
+ var xr = XmlReader.Create(ms, new XmlReaderSettings()
+ {
+ ProhibitDtd=true,
+ IgnoreWhitespace = true
+ });
+#else
+ var xr = XmlReader.Create(ms, new XmlReaderSettings()
+ {
+ DtdProcessing = DtdProcessing.Prohibit,
+ IgnoreWhitespace = true,
+ Async = true
+ });
+#endif
+
+ while (xr.Read())
+ {
+ if (xr.NodeType == XmlNodeType.Element)
+ {
+ if (xr.LocalName != "presetShapeDefinitons")
+ {
+ var item = LoadPresetShapeDefinition(xr);
+ _shapeDefinitions.Add(item.Style, item);
+ }
+ }
+ }
+ }
+ catch (Exception ex)
+ {
+ throw (new IOException("Cannot preset shape definitions file:presetShapeDefinitions.xml", ex));
+ }
+ }
+
+ public static bool LoadPresetShapeDefinitionFromXmlTriangle()
+ {
+ _shapeDefinitions = new Dictionary();
+
+ var xmlFile = Directory.GetCurrentDirectory() + "\\resource\\triangleOnly.xml";
+ var fs = new FileStream(xmlFile, FileMode.Open, FileAccess.Read);
+#if NET35
+ var xr = XmlReader.Create(fs, new XmlReaderSettings()
+ {
+ ProhibitDtd=true,
+ IgnoreWhitespace = true
+ });
+#else
+ var xr = XmlReader.Create(fs, new XmlReaderSettings()
+ {
+ DtdProcessing = DtdProcessing.Prohibit,
+ IgnoreWhitespace = true,
+ Async = true
+ });
+#endif
+ while (xr.Read())
+ {
+ if (xr.NodeType == XmlNodeType.Element)
+ {
+ if (xr.LocalName == "triangle" && xr.NodeType != XmlNodeType.EndElement)
+ {
+ var item = LoadPresetShapeDefinition(xr);
+ _shapeDefinitions.Add(item.Style, item);
+ }
+ }
+ }
+ return true;
+ }
+
+ private static ShapeDefinition LoadPresetShapeDefinition(XmlReader xr)
+ {
+ var style = xr.LocalName.ToEnum();
+ if (!style.HasValue) throw new InvalidOperationException();
+ var psd = new ShapeDefinition
+ {
+ Style = style.Value,
+ };
+ LoadFromXml(psd, xr);
+ return psd;
+ }
+
+ private static void LoadFromXml(ShapeDefinition psd, XmlReader xr)
+ {
+ while (xr.Read())
+ {
+ if (xr.NodeType == XmlNodeType.Element)
+ {
+ switch (xr.LocalName)
+ {
+ case "avLst":
+ psd.ShapeAdjustValues = LoadShapeGuides(xr);
+ break;
+ case "gdLst":
+ psd.ShapeGuides = LoadShapeGuides(xr);
+ break;
+ case "ahLst":
+ psd.ShapeAdjustHandles = LoadAdjustHandle(xr);
+ break;
+ case "cxnLst":
+ psd.ShapeConnectionSite = LoadConnectionLst(xr);
+ break;
+ case "rect":
+ psd.TextBoxRect = LoadRect(xr);
+ break;
+ case "pathLst":
+ psd.ShapePaths = LoadShapePaths(xr);
+ break;
+ }
+ }
+ else if (xr.NodeType == XmlNodeType.EndElement && xr.LocalName.Equals(psd.Style.ToString(), StringComparison.InvariantCultureIgnoreCase))
+ {
+ return;
+ }
+ }
+ }
+
+ private static TextBoxRect LoadRect(XmlReader xr)
+ {
+ // < rect l = "l" t = "y1" r = "x4" b = "b" xmlns = "http://schemas.openxmlformats.org/drawingml/2006/main" />
+ var rect = new TextBoxRect()
+ {
+ TopName = xr.GetAttribute("t"),
+ BottomName = xr.GetAttribute("b"),
+ LeftName = xr.GetAttribute("l"),
+ RightName = xr.GetAttribute("r")
+ };
+
+ return rect;
+ }
+
+ private static List LoadConnectionLst(XmlReader xr)
+ {
+ var shapeConnectionSite = new List();
+
+ while (xr.Read() && (xr.NodeType != XmlNodeType.EndElement && xr.LocalName != "cxnLst"))
+ {
+ var newConnection = new ShapeConnectionSite();
+
+ var attrStr = xr.GetAttribute("ang");
+
+ newConnection.Angle = xr.GetAttribute("ang");
+ xr.Read();
+
+ newConnection.PositionCoordinate = new ShapePositionCoordinate() { X = xr.GetAttribute("x"), Y = xr.GetAttribute("y") };
+
+ shapeConnectionSite.Add(newConnection);
+ xr.Read();
+
+ if (xr.LocalName == "cxnLst" && xr.NodeType == XmlNodeType.EndElement)
+ {
+ break;
+ }
+ }
+
+ return shapeConnectionSite;
+ }
+
+ private static List LoadAdjustHandle(XmlReader xr)
+ {
+ var l = new List();
+ var name = xr.LocalName;
+ while (xr.Read())
+ {
+ if (xr.NodeType == XmlNodeType.Element)
+ {
+ switch (xr.LocalName)
+ {
+ case "ahXY":
+ l.Add(ShapeAdjustHandleBase.CreateXy(xr));
+ break;
+ case "ahPolar":
+ l.Add(ShapeAdjustHandleBase.CreatePolar(xr));
+ break;
+ case "pos":
+ l[l.Count - 1].PositionCoordinate = new ShapePositionCoordinate() { X = xr.GetAttribute("x"), Y = xr.GetAttribute("y") };
+ break;
+ }
+ }
+ else if (xr.NodeType == XmlNodeType.EndElement && xr.LocalName == name)
+ {
+ break;
+ }
+ }
+ return l;
+ }
+
+ private static List LoadShapeGuides(XmlReader xr)
+ {
+ var l = new List();
+ var name = xr.LocalName;
+ while (xr.Read())
+ {
+ if (xr.NodeType == XmlNodeType.Element && xr.LocalName == "gd")
+ {
+ l.Add(new ShapeGuide() { Name = xr.GetAttribute("name"), Formula = xr.GetAttribute("fmla") });
+ }
+
+ if (xr.NodeType == XmlNodeType.EndElement && xr.LocalName == name)
+ {
+ break;
+ }
+ }
+ return l;
+ }
+
+
+ private static List LoadShapePaths(XmlReader xr)
+ {
+ var list = new List();
+ while (xr.Read())
+ {
+ if (xr.LocalName == "path" && xr.NodeType == XmlNodeType.Element)
+ {
+ list.Add(new DrawingPath(xr));
+ }
+ else if (xr.IsEndElementWithName("pathLst"))
+ {
+ break;
+ }
+ }
+ return list;
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/EPPlus.DrawingRenderer/ShapeDefinitions/PresetShapeDefiniton.Async.cs b/src/EPPlus.DrawingRenderer/ShapeDefinitions/PresetShapeDefiniton.Async.cs
new file mode 100644
index 0000000000..a131a20e1a
--- /dev/null
+++ b/src/EPPlus.DrawingRenderer/ShapeDefinitions/PresetShapeDefiniton.Async.cs
@@ -0,0 +1,256 @@
+/*************************************************************************************************
+ 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
+ *************************************************************************************************/
+using EPPlus.DrawingRenderer;
+using EPPlus.DrawingRenderer.ShapeDefinitions;
+using System;
+using System.Collections.Generic;
+using System.IO;
+
+#if !NET35
+//using System.Reflection.Metadata.Ecma335;
+using System.Threading.Tasks;
+//using System.Windows.Markup;
+#endif
+using System.Xml;
+
+namespace EPPlus.DrawingRenderer.ShapeDefinitions
+{
+ public partial class PresetShapeDefinitions
+ {
+ static object _syncRoot=new object();
+ static Dictionary _shapeDefinitions=null;
+ public static Dictionary ShapeDefinitions
+ {
+ get
+ {
+ lock (_syncRoot)
+ {
+ if (_shapeDefinitions == null)
+ {
+ _shapeDefinitions = new Dictionary();
+#if NET35
+ LoadPresetShapeDefinitionFromXml();
+#else
+ Task.Run(() => LoadPresetShapeDefinitionFromXmlAsync()).Wait();
+#endif
+
+ }
+ return _shapeDefinitions;
+ }
+ }
+ }
+
+#if !NET35
+ public static async Task LoadPresetShapeDefinitionFromXmlAsync()
+ {
+ var xmlFile = Directory.GetCurrentDirectory() + "\\resource\\presetShapeDefinitions.xml";
+
+ try
+ {
+ var ms=new MemoryStream(File.ReadAllBytes(xmlFile));
+ var xr = XmlReader.Create(ms, new XmlReaderSettings()
+ {
+ DtdProcessing = DtdProcessing.Prohibit,
+ IgnoreWhitespace = true,
+ Async = true
+ });
+
+ while (await xr.ReadAsync())
+ {
+ if (xr.NodeType == XmlNodeType.Element)
+ {
+ if (xr.LocalName != "presetShapeDefinitons")
+ {
+ var item = await LoadPresetShapeDefinitionAsync(xr);
+ _shapeDefinitions.Add(item.Style, item);
+ }
+ }
+ }
+ }
+ catch(Exception ex)
+ {
+ throw (new IOException("Cannot preset shape definitions file:presetShapeDefinitions.xml", ex));
+ }
+ }
+
+ public static async Task LoadPresetShapeDefinitionFromXmlAsyncTriangle()
+ {
+ _shapeDefinitions = new Dictionary();
+
+ var xmlFile = Directory.GetCurrentDirectory() + "\\resource\\triangleOnly.xml";
+ var fs = new FileStream(xmlFile, FileMode.Open, FileAccess.Read);
+ var xr = XmlReader.Create(fs, new XmlReaderSettings()
+ {
+ DtdProcessing = DtdProcessing.Prohibit,
+ IgnoreWhitespace = true,
+ Async = true
+ });
+ while (await xr.ReadAsync())
+ {
+ if (xr.NodeType == XmlNodeType.Element)
+ {
+ if (xr.LocalName == "triangle" && xr.NodeType != XmlNodeType.EndElement)
+ {
+ var item = await LoadPresetShapeDefinitionAsync(xr);
+ _shapeDefinitions.Add(item.Style, item);
+ }
+ }
+ }
+ return true;
+ }
+
+ private static async Task LoadPresetShapeDefinitionAsync(XmlReader xr)
+ {
+ if (Enum.TryParse(xr.LocalName, true, out var style))
+ {
+ var psd = new ShapeDefinition()
+ {
+ Style = style
+ };
+ await LoadFromXmlAsync(psd, xr);
+ return psd;
+ }
+ throw new InvalidOperationException();
+ }
+ private static async Task LoadFromXmlAsync(ShapeDefinition psd, XmlReader xr)
+ {
+ while (await xr.ReadAsync())
+ {
+ if (xr.NodeType == XmlNodeType.Element)
+ {
+ switch (xr.LocalName)
+ {
+ case "avLst":
+ psd.ShapeAdjustValues = await LoadShapeGuidesAsync(xr);
+ break;
+ case "gdLst":
+ psd.ShapeGuides = await LoadShapeGuidesAsync(xr);
+ break;
+ case "ahLst":
+ psd.ShapeAdjustHandles = await LoadAdjustHandleAsync(xr);
+ break;
+ case "cxnLst":
+ psd.ShapeConnectionSite = await LoadConnectionLstAsync(xr);
+ break;
+ case "rect":
+ psd.TextBoxRect = new TextBoxRect() { TopName = xr.GetAttribute("t"), BottomName = xr.GetAttribute("b"), LeftName = xr.GetAttribute("l"), RightName = xr.GetAttribute("r") };
+ break;
+ case "pathLst":
+ psd.ShapePaths = LoadShapePaths(xr);
+ break;
+ }
+ }
+ else if (xr.NodeType == XmlNodeType.EndElement && xr.LocalName.Equals(psd.Style.ToString(), StringComparison.InvariantCultureIgnoreCase))
+ {
+ return;
+ }
+ }
+ }
+
+ private static async Task LoadRectAsync(XmlReader xr)
+ {
+ TextBoxRect rect;
+ while (await xr.ReadAsync())
+ {
+ if (xr.NodeType == XmlNodeType.Element && xr.LocalName == "rect")
+ {
+ rect = new TextBoxRect()
+ {
+ TopName = xr.GetAttribute("t"),
+ BottomName = xr.GetAttribute("b"),
+ LeftName = xr.GetAttribute("l"),
+ RightName = xr.GetAttribute("r")
+ };
+ return rect;
+ }
+ }
+ return null;
+ }
+
+ private static async Task> LoadConnectionLstAsync(XmlReader xr)
+ {
+ var shapeConnectionSite = new List();
+
+ while (await xr.ReadAsync() && (xr.NodeType != XmlNodeType.EndElement && xr.LocalName != "cxnLst"))
+ {
+ var newConnection = new ShapeConnectionSite();
+
+ var attrStr = xr.GetAttribute("ang");
+
+ newConnection.Angle = xr.GetAttribute("ang");
+ await xr.ReadAsync();
+
+ newConnection.PositionCoordinate = new ShapePositionCoordinate() { X = xr.GetAttribute("x"), Y = xr.GetAttribute("y") };
+
+ shapeConnectionSite.Add(newConnection);
+ await xr.ReadAsync();
+
+ if (xr.LocalName == "cxnLst" && xr.NodeType == XmlNodeType.EndElement)
+ {
+ break;
+ }
+ }
+
+ return shapeConnectionSite;
+ }
+
+ private static async Task> LoadAdjustHandleAsync(XmlReader xr)
+ {
+ var l = new List();
+ var name = xr.LocalName;
+ while (await xr.ReadAsync())
+ {
+ if (xr.NodeType == XmlNodeType.Element)
+ {
+ switch (xr.LocalName)
+ {
+ case "ahXY":
+ l.Add(ShapeAdjustHandleBase.CreateXy(xr));
+ break;
+ case "ahPolar":
+ l.Add(ShapeAdjustHandleBase.CreatePolar(xr));
+ break;
+ case "pos":
+ l[l.Count - 1].PositionCoordinate = new ShapePositionCoordinate() { X = xr.GetAttribute("x"), Y = xr.GetAttribute("y") };
+ break;
+ }
+ }
+ else if (xr.NodeType == XmlNodeType.EndElement && xr.LocalName == name)
+ {
+ break;
+ }
+ }
+ return l;
+ }
+
+ private static async Task> LoadShapeGuidesAsync(XmlReader xr)
+ {
+ var l = new List();
+ var name = xr.LocalName;
+ while (await xr.ReadAsync())
+ {
+ if (xr.NodeType == XmlNodeType.Element && xr.LocalName == "gd")
+ {
+ l.Add(new ShapeGuide() { Name = xr.GetAttribute("name"), Formula = xr.GetAttribute("fmla") });
+ }
+
+ if (xr.NodeType == XmlNodeType.EndElement && xr.LocalName == name)
+ {
+ break;
+ }
+ }
+ return l;
+ }
+#endif
+ }
+}
\ No newline at end of file
diff --git a/src/EPPlus.DrawingRenderer/ShapeDefinitions/QuadBezerTo.cs b/src/EPPlus.DrawingRenderer/ShapeDefinitions/QuadBezerTo.cs
new file mode 100644
index 0000000000..3a4f6156c2
--- /dev/null
+++ b/src/EPPlus.DrawingRenderer/ShapeDefinitions/QuadBezerTo.cs
@@ -0,0 +1,26 @@
+using System.Xml;
+
+namespace EPPlus.DrawingRenderer.ShapeDefinitions
+{
+ public class QuadBezerTo : PathWithCoordinates
+ {
+ public QuadBezerTo(QuadBezerTo clone) : base(clone)
+ {
+
+ }
+ public QuadBezerTo(XmlReader xr) : base(xr)
+ {
+
+ }
+ public QuadBezerTo(XmlElement e) : base(e)
+ {
+
+ }
+ public override PathDrawingType Type => PathDrawingType.QuadBezierTo;
+ internal override PathsBase Clone()
+ {
+ return new QuadBezerTo(this);
+ }
+
+ }
+}
diff --git a/src/EPPlus.DrawingRenderer/ShapeDefinitions/ShapeAdjustHandle.cs b/src/EPPlus.DrawingRenderer/ShapeDefinitions/ShapeAdjustHandle.cs
new file mode 100644
index 0000000000..8287241b75
--- /dev/null
+++ b/src/EPPlus.DrawingRenderer/ShapeDefinitions/ShapeAdjustHandle.cs
@@ -0,0 +1,49 @@
+/*************************************************************************************************
+ 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
+ *************************************************************************************************/
+using System.Xml;
+
+namespace EPPlus.DrawingRenderer.ShapeDefinitions
+{
+
+ public abstract class ShapeAdjustHandleBase
+ {
+ public abstract ShapeAdjustHandleType AhType { get; }
+ public ShapePositionCoordinate PositionCoordinate { get; set; }
+ public static ShapeAdjustHandleXY CreateXy(XmlReader xr)
+ {
+ return new ShapeAdjustHandleXY()
+ {
+ HorizontalAdjustmentGuide = xr.GetAttribute("gdRefX"),
+ VerticalAdjustmentGuide = xr.GetAttribute("gdRefY"),
+ MinimumHorizontalAdjustment = xr.GetAttribute("minX"),
+ MaximumHorizontalAdjustment = xr.GetAttribute("minY"),
+ MinimumVerticalAdjustment = xr.GetAttribute("maxX"),
+ MaximumVerticalAdjustment = xr.GetAttribute("maxY"),
+ };
+ }
+ public static ShapeAdjustHandlePolar CreatePolar(XmlReader xr)
+ {
+ return new ShapeAdjustHandlePolar()
+ {
+ AngleAdjustmentGuide = xr.GetAttribute("gdRefAng"),
+ RadialAdjustmentGuide = xr.GetAttribute("gdRefR"),
+ MinimumAngleAdjustment = xr.GetAttribute("minAng"),
+ MinimumRadialAdjustment = xr.GetAttribute("minR"),
+ MaximumAngleAdjustment = xr.GetAttribute("maxAng"),
+ MaximumRadialAdjustment = xr.GetAttribute("maxR"),
+ };
+ }
+
+ public abstract ShapeAdjustHandleBase Clone();
+ }
+}
diff --git a/src/EPPlus.DrawingRenderer/ShapeDefinitions/ShapeAdjustHandlePolar.cs b/src/EPPlus.DrawingRenderer/ShapeDefinitions/ShapeAdjustHandlePolar.cs
new file mode 100644
index 0000000000..93503e73a4
--- /dev/null
+++ b/src/EPPlus.DrawingRenderer/ShapeDefinitions/ShapeAdjustHandlePolar.cs
@@ -0,0 +1,39 @@
+/*************************************************************************************************
+ 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.DrawingRenderer.ShapeDefinitions
+{
+ public class ShapeAdjustHandlePolar : ShapeAdjustHandleBase
+ {
+ public override ShapeAdjustHandleType AhType => ShapeAdjustHandleType.Polar;
+ public string AngleAdjustmentGuide { get; set; }
+ public string RadialAdjustmentGuide { get; set; }
+ public object MaximumAngleAdjustment { get; set; }
+ public object MaximumRadialAdjustment { get; set; }
+ public object MinimumAngleAdjustment { get; set; }
+ public object MinimumRadialAdjustment { get; set; }
+ public override ShapeAdjustHandleBase Clone()
+ {
+ return new ShapeAdjustHandlePolar()
+ {
+ AngleAdjustmentGuide = AngleAdjustmentGuide,
+ RadialAdjustmentGuide = RadialAdjustmentGuide,
+ MaximumAngleAdjustment = MaximumAngleAdjustment,
+ MinimumAngleAdjustment = MinimumAngleAdjustment,
+ MaximumRadialAdjustment = MaximumRadialAdjustment,
+ MinimumRadialAdjustment = MinimumRadialAdjustment,
+ };
+ }
+
+ }
+}
diff --git a/src/EPPlus.DrawingRenderer/ShapeDefinitions/ShapeAdjustHandleType.cs b/src/EPPlus.DrawingRenderer/ShapeDefinitions/ShapeAdjustHandleType.cs
new file mode 100644
index 0000000000..d01eeeeff6
--- /dev/null
+++ b/src/EPPlus.DrawingRenderer/ShapeDefinitions/ShapeAdjustHandleType.cs
@@ -0,0 +1,20 @@
+/*************************************************************************************************
+ 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.DrawingRenderer.ShapeDefinitions
+{
+ public enum ShapeAdjustHandleType
+ {
+ XY,
+ Polar,
+ }
+}
diff --git a/src/EPPlus.DrawingRenderer/ShapeDefinitions/ShapeAdjustHandleXY.cs b/src/EPPlus.DrawingRenderer/ShapeDefinitions/ShapeAdjustHandleXY.cs
new file mode 100644
index 0000000000..3bfb9e6928
--- /dev/null
+++ b/src/EPPlus.DrawingRenderer/ShapeDefinitions/ShapeAdjustHandleXY.cs
@@ -0,0 +1,65 @@
+/*************************************************************************************************
+ 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.DrawingRenderer.ShapeDefinitions
+{
+ public class ShapeAdjustHandleXY : ShapeAdjustHandleBase
+ {
+ public override ShapeAdjustHandleType AhType => ShapeAdjustHandleType.XY;
+
+ ///
+ ///Specifies the name of the guide that is updated with the adjustment x position from this adjust handle.
+ ///The possible values for this attribute are defined by the ST_GeomGuideName simple type (§20.1.10.28).
+ ///
+ public string HorizontalAdjustmentGuide { get; set; }
+ ///
+ /// Specifies the name of the guide that is updated with the adjustment y position from this adjust handle.
+ ///
+ public string VerticalAdjustmentGuide { get; set; }
+ ///
+ /// Specifies the minimum horizontal position that is allowed for this adjustment handle.
+ /// If this attribute is omitted, then it is assumed that this adjust handle cannot move in the x direction.
+ /// That is the maxX and minX are equal.
+ ///
+ public object MinimumHorizontalAdjustment{ get; set; }
+ ///
+ /// Specifies the maximum horizontal position that is allowed for this adjustment handle.
+ /// If this attribute is omitted, then it is assumed that this adjust handle cannot move in the x direction.
+ /// That is the maxX and minX are equal.
+ ///
+ public object MaximumHorizontalAdjustment { get; set; }
+ ///
+ /// Specifies the minimum vertical position that is allowed for this adjustment handle.
+ /// If this attribute is omitted, then it is assumed that this adjust handle cannot move in the y direction.That is the maxY and minY are equal.
+ ///
+ public object MinimumVerticalAdjustment { get; set; }
+ ///
+ /// Specifies the minimum vertical position that is allowed for this adjustment handle.
+ /// If this attribute is omitted, then it is assumed that this adjust handle cannot move in the y direction.
+ /// That is the maxY and minY are equal.
+ ///
+ public object MaximumVerticalAdjustment { get; set; }
+ public override ShapeAdjustHandleBase Clone()
+ {
+ return new ShapeAdjustHandleXY()
+ {
+ HorizontalAdjustmentGuide = HorizontalAdjustmentGuide,
+ VerticalAdjustmentGuide = VerticalAdjustmentGuide,
+ MinimumHorizontalAdjustment = MinimumHorizontalAdjustment,
+ MaximumHorizontalAdjustment = MaximumHorizontalAdjustment,
+ MinimumVerticalAdjustment = MinimumVerticalAdjustment,
+ MaximumVerticalAdjustment = MaximumVerticalAdjustment,
+ };
+ }
+ }
+}
diff --git a/src/EPPlus.DrawingRenderer/ShapeDefinitions/ShapeConnectionSite.cs b/src/EPPlus.DrawingRenderer/ShapeDefinitions/ShapeConnectionSite.cs
new file mode 100644
index 0000000000..f5a0d3b403
--- /dev/null
+++ b/src/EPPlus.DrawingRenderer/ShapeDefinitions/ShapeConnectionSite.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.DrawingRenderer.ShapeDefinitions
+{
+ public class ShapeConnectionSite
+ {
+ public object Angle { get; set; }
+ public ShapePositionCoordinate PositionCoordinate { get; set; }
+ }
+}
\ No newline at end of file
diff --git a/src/EPPlus.DrawingRenderer/ShapeDefinitions/ShapeDefinitionBase.cs b/src/EPPlus.DrawingRenderer/ShapeDefinitions/ShapeDefinitionBase.cs
new file mode 100644
index 0000000000..8e9885c35a
--- /dev/null
+++ b/src/EPPlus.DrawingRenderer/ShapeDefinitions/ShapeDefinitionBase.cs
@@ -0,0 +1,512 @@
+/*************************************************************************************************
+ 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
+ *************************************************************************************************/
+using EPPlus.DrawingRenderer.Utils;
+using System.Diagnostics;
+
+namespace EPPlus.DrawingRenderer.ShapeDefinitions
+{
+ [DebuggerDisplay("{Style}")]
+ public class ShapeDefinition
+ {
+ public Dictionary _calculatedValues = new Dictionary();
+
+ public ShapeDefinition()
+ {
+
+ }
+ ///
+ /// Clone constructor
+ ///
+ /// The original to clone from
+ protected ShapeDefinition(ShapeDefinition original)
+ {
+ Style=original.Style;
+ if (original.ShapeAdjustValues != null)
+ {
+ ShapeAdjustValues = new List();
+ foreach (var av in original.ShapeAdjustValues)
+ {
+ ShapeAdjustValues.Add(av.Clone());
+ }
+ }
+ if (original.ShapeGuides != null)
+ {
+ ShapeGuides = new List();
+ foreach (var g in original.ShapeGuides)
+ {
+ ShapeGuides.Add(g.Clone());
+ }
+ }
+
+ if (original.ShapeAdjustHandles!=null)
+ {
+ ShapeAdjustHandles = new List();
+ foreach (var ah in original.ShapeAdjustHandles)
+ {
+ ShapeAdjustHandles.Add(ah.Clone());
+ }
+ }
+
+ TextBoxRect = original.TextBoxRect?.Clone();
+
+ ShapePaths = new List();
+ foreach (var p in original.ShapePaths)
+ {
+ ShapePaths.Add(p.Clone());
+ }
+ }
+
+
+ public ShapeStyle Style { get; set; }
+ ///
+ /// avLst
+ ///
+ public List ShapeAdjustValues { get; set; }
+ ///
+ /// gdLst
+ ///
+ public List ShapeGuides { get; set; }
+ ///
+ /// ahLst
+ ///
+ public List ShapeAdjustHandles { get; set; }
+ //cxnLst
+ public List ShapeConnectionSite { get; set; }
+ //rect
+ ///
+ /// The rectangle for the text inside the shape.
+ ///
+ public TextBoxRect TextBoxRect { get; set; }
+ //pathLst
+ ///
+ /// Paths to draw the shape
+ ///
+ public List ShapePaths { get; set; }
+
+
+ //private object GetValueOfNameOrCalculateValue(object value)
+ //{
+ // if (value is string s && _calculatedValues.ContainsKey(s))
+ // {
+ // return _calculatedValues[s];
+ // }
+ // return value;
+ //}
+ public void Calculate(double width, double height, bool textAutofit, List customAdjPtsNames, List customAdjPts)
+ {
+ InitCalculatedValues(width, height);
+
+ if (ShapeAdjustValues != null)
+ {
+ //if (shape.HasCustomAdjustmentPoints())
+ //{
+ // var names = shape.GetAdjustmentPointsNames();
+ // var l = shape.GetAdjustmentPointsList();
+ if (customAdjPts != null && customAdjPtsNames != null)
+ {
+
+ for (int i = 0; i < ShapeAdjustValues.Count; i++)
+ {
+ _calculatedValues.Add(customAdjPtsNames[i], Convert.ToDouble(customAdjPts[i]));
+ }
+ }
+ else
+ {
+ foreach (var ap in ShapeAdjustValues)
+ {
+ if (string.IsNullOrEmpty(ap.Formula) == false)
+ {
+ ap.CalculatedValue = CalculateFormula(ap.Formula);
+ _calculatedValues.Add(ap.Name, ap.CalculatedValue);
+ }
+ }
+ }
+ }
+ if (ShapeGuides != null)
+ {
+ foreach (var g in ShapeGuides)
+ {
+ g.CalculatedValue = CalculateFormula(g.Formula);
+
+ if (g.CalculatedValue == double.MaxValue || g.CalculatedValue == double.PositiveInfinity || g.CalculatedValue == double.NegativeInfinity)
+ {
+ throw new Exception($"Double overflowed during calculation of {g.Name}");
+ }
+
+ if (_calculatedValues.ContainsKey(g.Name))
+ {
+ _calculatedValues[g.Name] = g.CalculatedValue;
+ }
+ else
+ {
+ _calculatedValues.Add(g.Name, g.CalculatedValue);
+ }
+ }
+ }
+ if (ShapeAdjustHandles != null)
+ {
+ foreach (var h in ShapeAdjustHandles)
+ {
+ switch (h.AhType)
+ {
+ case ShapeAdjustHandleType.XY:
+ var xyH = (ShapeAdjustHandleXY)h;
+ xyH.MinimumVerticalAdjustment = GetValueOfNameOrCalculateValue(xyH.MinimumVerticalAdjustment);
+ xyH.MaximumVerticalAdjustment = GetValueOfNameOrCalculateValue(xyH.MaximumVerticalAdjustment);
+ xyH.MinimumHorizontalAdjustment = GetValueOfNameOrCalculateValue(xyH.MinimumHorizontalAdjustment);
+ xyH.MaximumHorizontalAdjustment = GetValueOfNameOrCalculateValue(xyH.MaximumHorizontalAdjustment);
+ break;
+ case ShapeAdjustHandleType.Polar:
+ var polarH = (ShapeAdjustHandlePolar)h;
+ polarH.MinimumAngleAdjustment = GetValueOfNameOrCalculateValue(polarH.MinimumAngleAdjustment);
+ polarH.MaximumAngleAdjustment = GetValueOfNameOrCalculateValue(polarH.MaximumAngleAdjustment);
+ polarH.MinimumRadialAdjustment = GetValueOfNameOrCalculateValue(polarH.MinimumRadialAdjustment);
+ polarH.MaximumRadialAdjustment = GetValueOfNameOrCalculateValue(polarH.MaximumRadialAdjustment);
+ break;
+ }
+ }
+ }
+
+ double overrideWidthRatio = 1;
+ double overrideHeightRatio = 1;
+
+ if (TextBoxRect != null)
+ {
+ //if (textAutofit != eTextAutofit.ShapeAutofit)
+ if (textAutofit == false)
+ {
+ TextBoxRect.LeftValue = GetValue(TextBoxRect.LeftName) / Constants.EMU_PER_PIXEL;
+ TextBoxRect.RightValue = GetValue(TextBoxRect.RightName) / Constants.EMU_PER_PIXEL;
+ TextBoxRect.TopValue = GetValue(TextBoxRect.TopName) / Constants.EMU_PER_PIXEL;
+ TextBoxRect.BottomValue = GetValue(TextBoxRect.BottomName) / Constants.EMU_PER_PIXEL;
+ }
+ else
+ {
+ //var txt = shape.Text;
+
+ //List txtContainers = new List();
+
+ ////TODO: This needs to actually iterate through the individual paragraphs/txtRuns when differing fonts/font sizes exist
+ ////foreach(var paragraph in shape.TextBodyItem.Paragraphs)
+ ////{
+ //// foreach(var txtRun in paragraph.TextRuns)
+ //// {
+ //// var newContainer = new TextContainer(txt, txtRun.GetMeasureFont(), true);
+ //// txtContainers.Add(newContainer);
+ //// }
+ ////}
+ //var newContainer = new TextContainer(txt, shape.TextBodyItem.Paragraphs.FirstDefaultRunProperties.GetMeasureFont(), true);
+
+ //var cW = newContainer.Width;
+ //var cH = newContainer.Height;
+
+ //var expectedWidth = (GetValue(TextBoxRect.RightName) - GetValue(TextBoxRect.LeftName)) / (double)ExcelDrawing.EMU_PER_PIXEL;
+ //var expectedHeight = (GetValue(TextBoxRect.BottomName) - GetValue(TextBoxRect.TopName)) / (double)ExcelDrawing.EMU_PER_PIXEL;
+
+ //overrideWidthRatio = cW / expectedWidth;
+ //overrideHeightRatio = cH / expectedHeight;
+
+ //TextBoxRect.RightValue = cW;
+ //TextBoxRect.BottomValue = cH;
+
+ //TextBoxRect.LeftValue = GetValue(TextBoxRect.LeftName) / (double)ExcelDrawing.EMU_PER_PIXEL * overrideWidthRatio;
+ //TextBoxRect.TopValue = GetValue(TextBoxRect.TopName) / (double)ExcelDrawing.EMU_PER_PIXEL * overrideHeightRatio;
+ }
+ }
+
+ if (ShapePaths.Count > 0)
+ {
+ foreach (var item in ShapePaths)
+ {
+ var shapeWidth = (double)width * Constants.EMU_PER_PIXEL;
+ var shapeHeight = (double)height * Constants.EMU_PER_PIXEL;
+
+ var widthRatio = item.Width.HasValue ? (double)shapeWidth / (double)item.Width : 1D;
+ var heightRatio = item.Height.HasValue ? (double)shapeWidth / (double)item.Height : 1D;
+
+ widthRatio *= overrideWidthRatio;
+ heightRatio *= overrideHeightRatio;
+
+ item.Width = shapeWidth;
+ item.Height = shapeHeight;
+
+ foreach (var p in item.Paths)
+ {
+ switch (p.Type)
+ {
+ case PathDrawingType.Close:
+ break;
+ case PathDrawingType.ArcTo:
+ var arc = (ArcTo)p;
+ if (string.IsNullOrEmpty(arc.WidthRadiusName) == false)
+ {
+ arc.WidthRadius = _calculatedValues[arc.WidthRadiusName] * overrideWidthRatio;
+ }
+ else
+ {
+ arc.WidthRadius = (double)((arc.WidthRadius ?? 0D) * widthRatio);
+ }
+ if (string.IsNullOrEmpty(arc.HeightRadiusName) == false)
+ {
+ arc.HeightRadius = _calculatedValues[arc.HeightRadiusName] * overrideHeightRatio;
+ }
+ else
+ {
+ arc.HeightRadius = (double)((arc.HeightRadius ?? 0D) * heightRatio);
+ }
+ if (string.IsNullOrEmpty(arc.StartAngleName) == false) arc.StartAngle = _calculatedValues[arc.StartAngleName];
+ if (string.IsNullOrEmpty(arc.SwingAngleName) == false) arc.SwingAngle = _calculatedValues[arc.SwingAngleName];
+ break;
+ default:
+ var pb = (PathWithCoordinates)p;
+ for (var i = 0; i < pb.Coordinates.Count; i++)
+ {
+ var c = pb.Coordinates[i];
+ if (string.IsNullOrEmpty(c.XName) == false)
+ {
+ c.X = _calculatedValues[c.XName] * overrideWidthRatio;
+ }
+ else
+ {
+ c.X = (double)((c.X ?? 0D) * widthRatio);
+ }
+
+ if (string.IsNullOrEmpty(c.YName) == false)
+ {
+ c.Y = _calculatedValues[c.YName] * overrideHeightRatio;
+ }
+ else
+ {
+ c.Y = (double)((c.Y ?? 0D) * heightRatio);
+ }
+ }
+ break;
+ }
+ }
+ }
+ }
+ }
+ private object GetValueOfNameOrCalculateValue(object value)
+ {
+ if (value is string s && _calculatedValues.ContainsKey(s))
+ {
+ return _calculatedValues[s];
+ }
+ return value;
+ }
+
+ private void InitCalculatedValues(double width, double height)
+ {
+
+ var adjustedHeight = height;
+ var adjustedWidth = width;
+
+ //Convert shape bounds from pixels to EMUs
+ var h = (double)(adjustedHeight * Constants.EMU_PER_PIXEL);
+ var w = (double)(adjustedWidth * Constants.EMU_PER_PIXEL);
+
+ //Longest side
+ var ls = Math.Max(h, w);
+ //Shortest side
+ var ss = Math.Min(h, w);
+
+ _calculatedValues = new Dictionary()
+ {
+ {"t", 0d },
+ {"l", 0d },
+ {"w", w },
+ {"r", w },
+ {"h", h },
+ {"b", h },
+ {"hc", w/2d },
+ {"vc", h/2d },
+ {"ls", ls },
+ {"ss", ss },
+ {"3cd4", 16200000.0d},
+ {"3cd8", 8100000.0d},
+ {"5cd8", 13500000.0d},
+ {"7cd8", 18900000.0d},
+ {"cd2", 10800000.0d},
+ {"cd4", 5400000.0d},
+ {"cd8", 2700000.0d},
+ {"hd2", h/2d},
+ {"hd3", h/3d},
+ {"hd4", h/4d},
+ {"hd5", h/5d},
+ {"hd6", h/6d},
+ {"hd8", h/8d},
+ {"wd2", w/2d},
+ {"wd3", w/3d},
+ {"wd4", w/4d},
+ {"wd5", w/5d},
+ {"wd6", w/6d},
+ {"wd8", w/8d},
+ {"wd10", w/10d},
+ {"wd12", w/12d},
+ {"wd16", w/16d},
+ {"wd32", w/32d},
+ {"ssd2", ss/2d },
+ {"ssd4", ss/4d },
+ {"ssd6", ss/6d },
+ {"ssd8", ss/8d },
+ {"ssd16", ss/16d },
+ {"ssd32", ss/32d },
+ };
+ }
+
+ internal double CalculateFormula(string formula)
+ {
+ var tokens = formula.Split(new char[] { ' ' }, StringSplitOptions.RemoveEmptyEntries);
+ var t = tokens[0];
+ switch (t)
+ {
+ case "val":
+ var val = GetValue(tokens[1]);
+ return GetValue(tokens[1]);
+ case "*/":
+ var divBy = GetValue(tokens[3]);
+ if (divBy == 0) return 0;
+ var val1 = GetValue(tokens[1]);
+ var val2 = GetValue(tokens[2]);
+
+ return (val1 * val2) / (double)divBy;
+ case "+-":
+ return (GetValue(tokens[1]) + GetValue(tokens[2])) - GetValue(tokens[3]);
+ case "+/":
+ divBy = GetValue(tokens[3]);
+ if (divBy == 0) return 0;
+ return (GetValue(tokens[1]) + GetValue(tokens[2])) / (double)divBy;
+ case "?:":
+ return GetValue(tokens[1]) > 0 ? GetValue(tokens[2]) : GetValue(tokens[3]);
+ case "sqrt":
+ return Math.Sqrt(Math.Abs(GetValue(tokens[1])));
+ case "abs":
+ return Math.Abs(GetValue(tokens[1]));
+ case "min":
+ return Math.Min(GetValue(tokens[1]), GetValue(tokens[2]));
+ case "max":
+ return Math.Max(GetValue(tokens[1]), GetValue(tokens[2]));
+ case "mod":
+ return Math.Sqrt(Math.Pow((double)GetValue(tokens[1]), 2d) + Math.Pow((double)GetValue(tokens[2]), 2) + Math.Pow((double)GetValue(tokens[3]), 2));
+ case "pin":
+ //if (y < x), then x = value of this guide else if (y > z), then z
+ double x = GetValue(tokens[1]);
+ double y = GetValue(tokens[2]);
+ double z = GetValue(tokens[3]);
+
+ return (y < x ? x : y > z ? z : y);
+ case "sin":
+ var angleSin = GetValue(tokens[2]) / 60000D;
+ if (angleSin == 0d || angleSin == 180d)
+ {
+ return 0;
+ }
+ var radAngleSin = Math.Sin(MathHelper.Radians(angleSin));
+ return (GetValue(tokens[1]) * radAngleSin);
+ case "cos":
+ var angleCos = GetValue(tokens[2]) / 60000D;
+
+ if (angleCos == 90d || angleCos == 270d)
+ {
+ return 0;
+ }
+ var radAngleCos = Math.Cos(MathHelper.Radians(angleCos));
+ return (GetValue(tokens[1]) * radAngleCos);
+ case "tan":
+ var angleTan = GetValue(tokens[2]) / 60000D;
+
+ if (angleTan == 0d || angleTan == 90d)
+ {
+ //Tan technically undefined
+ return 0;
+ }
+
+ return (GetValue(tokens[1]) * Math.Tan(MathHelper.Radians(angleTan)));
+ case "at2":
+ x = GetValue(tokens[1]);
+ y = GetValue(tokens[2]);
+ double angleRad = Math.Atan2(y, x); // radians
+ double angleDeg = angleRad * (180d / Math.PI); // degrees
+
+ while (angleDeg < -360)
+ {
+ angleDeg += 360; // normalize to [0, 360)
+ }
+ while (angleDeg > 360)
+ {
+ angleDeg -= 360;
+ }
+
+ return (angleDeg * 60000D);
+ case "cat2":
+ x = GetValue(tokens[1]);
+ y = GetValue(tokens[2]);
+ z = GetValue(tokens[3]);
+
+ double angleRadCatOrig = Math.Atan2(z, y); // radians
+ double angleDegCat2Orig = angleRadCatOrig * (180.0 / Math.PI); // degrees
+
+ if (angleDegCat2Orig == 90d || angleDegCat2Orig == 270d)
+ {
+ return 0;
+ }
+
+ var dist = (x * Math.Cos(angleRadCatOrig));
+ return dist;
+ case "sat2":
+ x = GetValue(tokens[1]);
+ y = GetValue(tokens[2]);
+ z = GetValue(tokens[3]);
+
+ double angleRadSatOrig = Math.Atan2(z, y); // radians
+ double angleDegSat2Orig = angleRadSatOrig * (180.0 / Math.PI); // degrees
+
+ if (angleDegSat2Orig == 0d || angleDegSat2Orig == 180d)
+ {
+ return 0;
+ }
+
+ var sat2Rad = (x * Math.Sin(angleRadSatOrig));
+
+ return sat2Rad;
+ default:
+ if (_calculatedValues.TryGetValue(t, out var v))
+ {
+ return v;
+ }
+ throw new InvalidOperationException($"Unknown function or variable {{{t}}}");
+ }
+
+ }
+ private double GetValue(string v)
+ {
+ if (double.TryParse(v, out var l))
+ {
+ return l;
+ }
+ else
+ {
+ if (_calculatedValues.TryGetValue(v, out var cv))
+ {
+ return cv;
+ }
+ throw new InvalidOperationException($"Unknown variable {{{v}}}");
+ }
+ }
+
+ public ShapeDefinition Clone()
+ {
+ return new ShapeDefinition(this);
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/EPPlus.DrawingRenderer/ShapeDefinitions/ShapeGuide.cs b/src/EPPlus.DrawingRenderer/ShapeDefinitions/ShapeGuide.cs
new file mode 100644
index 0000000000..8fb8940ac6
--- /dev/null
+++ b/src/EPPlus.DrawingRenderer/ShapeDefinitions/ShapeGuide.cs
@@ -0,0 +1,37 @@
+/*************************************************************************************************
+ 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
+ *************************************************************************************************/
+using System.Diagnostics;
+
+namespace EPPlus.DrawingRenderer.ShapeDefinitions
+{
+ ///
+ /// 20.1.9.11 Ecma part 1
+ /// 20.1.10.76 - Preset Textbox Shape Types
+ ///
+ [DebuggerDisplay("{Name}-{Formula}={CalculatedValue}")]
+ public class ShapeGuide
+ {
+ public string Name { get; set; }
+ public string Formula { get; set; }
+ public double CalculatedValue
+ {
+ get;
+ set;
+ }
+
+ public ShapeGuide Clone()
+ {
+ return new ShapeGuide() { Name=Name, Formula=Formula, CalculatedValue=CalculatedValue};
+ }
+ }
+}
diff --git a/src/EPPlus.DrawingRenderer/ShapeDefinitions/ShapePositionCoordinate.cs b/src/EPPlus.DrawingRenderer/ShapeDefinitions/ShapePositionCoordinate.cs
new file mode 100644
index 0000000000..a04cda38dd
--- /dev/null
+++ b/src/EPPlus.DrawingRenderer/ShapeDefinitions/ShapePositionCoordinate.cs
@@ -0,0 +1,20 @@
+/*************************************************************************************************
+ 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.DrawingRenderer.ShapeDefinitions
+{
+ public class ShapePositionCoordinate
+ {
+ public object X { get; set; }
+ public object Y { get; set; }
+ }
+}
diff --git a/src/EPPlus.DrawingRenderer/ShapeDefinitions/TextBoxRect.cs b/src/EPPlus.DrawingRenderer/ShapeDefinitions/TextBoxRect.cs
new file mode 100644
index 0000000000..b7dd100bba
--- /dev/null
+++ b/src/EPPlus.DrawingRenderer/ShapeDefinitions/TextBoxRect.cs
@@ -0,0 +1,38 @@
+/*************************************************************************************************
+ 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.DrawingRenderer.ShapeDefinitions
+{
+ public class TextBoxRect
+ {
+ public string LeftName { get; set; }
+ public string RightName { get; set; }
+ public string TopName { get; set; }
+ public string BottomName { get; set; }
+
+ public double LeftValue { get; set; }
+ public double RightValue { get; set; }
+ public double TopValue { get; set; }
+ public double BottomValue { get; set; }
+
+ public TextBoxRect Clone()
+ {
+ return new TextBoxRect()
+ {
+ LeftName = LeftName,
+ RightName = RightName,
+ TopName = TopName,
+ BottomName = BottomName
+ };
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/EPPlus.DrawingRenderer/Svg/IRender.cs b/src/EPPlus.DrawingRenderer/Svg/IRender.cs
new file mode 100644
index 0000000000..84a3603bd9
--- /dev/null
+++ b/src/EPPlus.DrawingRenderer/Svg/IRender.cs
@@ -0,0 +1,748 @@
+using DrawingRenderer.Constants;
+using EPPlus.DrawingRenderer.RenderItems;
+using EPPlus.DrawingRenderer.Svg;
+using EPPlus.DrawingRenderer.Utils;
+using EPPlus.Export.ImageRenderer.RenderItems.Shared;
+using EPPlus.Fonts.OpenType.Utils;
+using EPPlus.Graphics;
+using EPPlusImageRenderer;
+using EPPlusImageRenderer.RenderItems;
+using EPPlusImageRenderer.Utils;
+using OfficeOpenXml.Utils;
+using System;
+using System.Collections.Generic;
+using System.Drawing;
+using System.Globalization;
+using System.IO;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+
+namespace EPPlus.DrawingRenderer
+{
+ public class SvgShapeRenderer : IShapeRenderer
+ {
+ public SvgShapeRenderer(BoundingBox bounds, StringBuilder outputStream, SvgRenderOptions options)
+ {
+ BasicShapesRenderer = new SvgBasicShapesRenderer(outputStream);
+
+ //Override size.
+ if (options.Size.Width.HasValue)
+ {
+ bounds.Width = options.Size.WidthPixels;
+ }
+ if (options.Size.Height.HasValue)
+ {
+ bounds.Height = options.Size.HeightPixels;
+ }
+
+ Bounds = bounds;
+ OutputStream = outputStream;
+ }
+ public IBasicIShapesRenderer BasicShapesRenderer { get; }
+
+ public StringBuilder OutputStream { get; }
+
+ public BoundingBox Bounds { get; }
+ public string ViewBox { get; set; }
+ public bool Render(List items)
+ {
+ OutputStream.Clear();
+ OutputStream.Append($"");
+ return true;
+ }
+
+ public bool PreRender(List items)
+ {
+ var defSb = new StringBuilder();
+ var hs = new HashSet();
+ var ix = 1;
+
+ foreach (RenderItem item in items)
+ {
+ if(item is GroupRenderItem group)
+ {
+ foreach(var child in group.RenderItems)
+ {
+ WriteDefsForRenderItem(defSb, hs, ref ix, child);
+ }
+ }
+ else
+ {
+ WriteDefsForRenderItem(defSb, hs,ref ix, item);
+ }
+ }
+
+ if (defSb.Length > 0)
+ {
+ OutputStream.Append("");
+ OutputStream.Append(defSb);
+ OutputStream.Append("");
+ }
+
+ ////Remove all items that have already been rendered
+ //renderItems.RemoveAll(x => x is SvgRenderItem svgX && string.IsNullOrEmpty(svgX.DefId) == false);
+ return true;
+ }
+ Dictionary _defsCache = new Dictionary();
+ private void WriteDefsForRenderItem(StringBuilder defSb, HashSet hs, ref int ix, RenderItem item)
+ {
+ string filter = "";
+ if (item.GradientFill != null)
+ {
+ var key = item.GradientFill.GetKey();
+ if (_defsCache.TryGetValue(key, out string? name) == false)
+ {
+ name = WriteGradient($"Gradient{ix}", defSb, hs, item.GradientFill, item.FillColorSource);
+ _defsCache[key] = name;
+ }
+ item.FillColor = $"Url(#{name})";
+ }
+ else if (item.PatternFill != null)
+ {
+ var key = item.PatternFill.GetKey();
+ if (_defsCache.TryGetValue(key, out string? name) == false)
+ {
+ name = WritePattern($"Pattern{item.PatternFill.PatternType}{ix}", defSb, hs, item.PatternFill, item.FillColorSource);
+ _defsCache[key] = name;
+ }
+ item.FillColor = $"Url(#{name})";
+ }
+ else if (item.BlipFill != null)
+ {
+ var key = item.BlipFill.GetKey();
+
+ if (_defsCache.TryGetValue(key, out string? name) == false)
+ {
+ if (item.FillColorSource != PathFillMode.Norm)
+ {
+ item.FilterName = GetFilterName(ix);
+ }
+
+ name = WriteBlip("Blip", defSb, hs, item, ref filter);
+ _defsCache[key] = name;
+ }
+ item.FillColor = $"Url(#{name})";
+ }
+ if (item.BorderGradientFill != null)
+ {
+ var key = item.BorderGradientFill.GetKey();
+ if (_defsCache.TryGetValue(key, out string? name) == false)
+ {
+ name = WriteGradient($"StrokeGradient{ix}", defSb, hs, item.BorderGradientFill, item.BorderColorSource);
+ _defsCache[key] = name;
+ }
+ item.BorderColor = $"Url(#{name})";
+ }
+ if (item.GlowColor != null)
+ {
+ var key = item.GetFilterKey();
+ if (_defsCache.TryGetValue(key, out string? name) == false)
+ {
+ if (string.IsNullOrEmpty(item.FilterName))
+ {
+ name = GetFilterName(ix);
+ item.FilterName = $"Url(#{name})";
+ filter = $"";
+ filter += $"" +
+ $"" +
+ $"" +
+ $"";
+
+ _defsCache[key] = name;
+ }
+ }
+ else
+ {
+ item.FilterName = name;
+ }
+ }
+ if (item.OuterShadowEffect != null)
+ {
+ if (string.IsNullOrEmpty(item.FilterName))
+ {
+ var key = item.GetFilterKey();
+ if (_defsCache.TryGetValue(key, out string? name) == false)
+ {
+ var filterName = GetFilterName(ix);
+ item.FilterName = $"Url(#{filterName})";
+ filter = $"";
+ _defsCache[key] = filterName;
+ }
+ else
+ {
+ item.FilterName = $"Url(#{name})";
+ }
+ }
+ if (string.IsNullOrEmpty(filter) == false)
+ {
+ 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 + "");
+ }
+ ix++;
+ }
+
+ private static string GetFilterName(int ix)
+ {
+ return $"item{ix}Filter";
+ }
+
+ private string WriteBlip(string namePrefix, StringBuilder defSb, HashSet hs, RenderItem item, ref string filter)
+ {
+ //, item.BlipFill, item.FillColorSource
+ var name = $"{namePrefix}";
+ var fillMode = item.FillColorSource;
+ if (fillMode != PathFillMode.Norm)
+ {
+ if (hs.Contains(item.FilterName) == false)
+ {
+ switch (fillMode)
+ {
+ case PathFillMode.Lighten:
+ filter = $"";
+ break;
+ case PathFillMode.LightenLess:
+ filter = $"";
+ break;
+ case PathFillMode.DarkenLess:
+ filter = $"";
+ break;
+ case PathFillMode.Darken:
+ filter = $"";
+ break;
+ }
+ }
+ hs.Add(item.FilterName);
+ }
+
+ if (hs.Contains(name)) return name;
+ hs.Add(name);
+
+ defSb.Append($"");
+ defSb.Append($"");
+ defSb.Append($"");
+ return name;
+ }
+ private string WriteGradient(string namePrefix, StringBuilder defSb, HashSet hs, RenderGradientFill gradientFill, PathFillMode fillMode)
+ {
+ //var gs = gradientFill.Settings;
+ var name = $"{namePrefix}{fillMode}";
+ var grUnits = gradientFill.UserSpaceOnUse ? " gradientUnits=\"userSpaceOnUse\"" : "";
+ if (gradientFill.ShadePath == ShadePath.Linear && hs.Contains(name) == false)
+ {
+ hs.Add(name);
+ var xy = GetXy(gradientFill.LinearSettings?.Angle);
+ defSb.Append($"");
+ SetStopColors(defSb, gradientFill, fillMode);
+ defSb.Append("");
+ }
+ else if (hs.Contains(name) == false)
+ {
+ hs.Add(name);
+ defSb.Append($"");
+ SetStopColors(defSb, gradientFill, fillMode);
+ defSb.Append($"");
+ }
+
+ return name;
+ }
+ private string WritePattern(string namePrefix, StringBuilder defSb, HashSet hs, RenderPatternFill patternFill, PathFillMode fillMode)
+ {
+ var name = $"{namePrefix}{fillMode}";
+ var afc = ColorUtils.GetAdjustedColor(fillMode, patternFill.ForegroundColor);
+ var abc = ColorUtils.GetAdjustedColor(fillMode, patternFill.BackgroundColor);
+ switch (patternFill.PatternType)
+ {
+ case FillPatternStyle.Pct5:
+ SetPatternHalf(defSb, name, afc, abc, 10, 10);
+ break;
+ case FillPatternStyle.Pct10:
+ SetPatternHalf(defSb, name, afc, abc, 10, 5);
+ break;
+ case FillPatternStyle.Pct20:
+ SetPatternHalf(defSb, name, afc, abc, 4, 4);
+ break;
+ case FillPatternStyle.Pct25:
+ SetPatternHalf(defSb, name, afc, abc, 4, 2);
+ break;
+ case FillPatternStyle.Pct30:
+ SetPatternArray(defSb, name, afc, abc, PatternArrays.Pct30);
+ break;
+ case FillPatternStyle.Pct40:
+ SetPatternArray(defSb, name, afc, abc, PatternArrays.Pct40);
+ break;
+ case FillPatternStyle.Pct50:
+ SetPatternArray(defSb, name, afc, abc, PatternArrays.Pct50);
+ break;
+ case FillPatternStyle.Pct60:
+ SetPatternArray(defSb, name, afc, abc, PatternArrays.Pct60);
+ break;
+ case FillPatternStyle.Pct70:
+ SetPatternArray(defSb, name, afc, abc, PatternArrays.Pct70);
+ break;
+ case FillPatternStyle.Pct75:
+ SetPatternHalf(defSb, name, abc, afc, 4, 2);
+ break;
+ case FillPatternStyle.Pct80:
+ SetPatternHalf(defSb, name, abc, afc, 4, 4);
+ break;
+ case FillPatternStyle.Pct90:
+ SetPatternHalf(defSb, name, abc, afc, 10, 5);
+ break;
+ case FillPatternStyle.LtHorz:
+ SetPatternArray(defSb, name, afc, abc, PatternArrays.LtHorz);
+ break;
+ case FillPatternStyle.LtVert:
+ SetPatternArray(defSb, name, afc, abc, PatternArrays.LtVert);
+ break;
+ case FillPatternStyle.LtUpDiag:
+ SetPatternArray(defSb, name, afc, abc, PatternArrays.LtUpDiag);
+ break;
+ case FillPatternStyle.LtDnDiag:
+ SetPatternArray(defSb, name, afc, abc, PatternArrays.LtDnDiag);
+ break;
+ case FillPatternStyle.DkVert:
+ SetPatternArray(defSb, name, afc, abc, PatternArrays.DkVert);
+ break;
+ case FillPatternStyle.DkHorz:
+ SetPatternArray(defSb, name, afc, abc, PatternArrays.DkHorz);
+ break;
+ case FillPatternStyle.DkUpDiag:
+ SetPatternArray(defSb, name, afc, abc, PatternArrays.DkUpDiag);
+ break;
+ case FillPatternStyle.DkDnDiag:
+ SetPatternArray(defSb, name, afc, abc, PatternArrays.DkDnDiag);
+ break;
+ case FillPatternStyle.WdUpDiag:
+ SetPatternArray(defSb, name, afc, abc, PatternArrays.WdUpDiag);
+ break;
+ case FillPatternStyle.WdDnDiag:
+ SetPatternArray(defSb, name, afc, abc, PatternArrays.WdDnDiag);
+ break;
+ case FillPatternStyle.NarVert:
+ SetPatternArray(defSb, name, afc, abc, PatternArrays.NarVert);
+ break;
+ case FillPatternStyle.NarHorz:
+ SetPatternArray(defSb, name, afc, abc, PatternArrays.NarHorz);
+ break;
+ case FillPatternStyle.Vert:
+ SetPatternArray(defSb, name, afc, abc, PatternArrays.Vert);
+ break;
+ case FillPatternStyle.Horz:
+ SetPatternArray(defSb, name, afc, abc, PatternArrays.Horz);
+ break;
+ case FillPatternStyle.DashDnDiag:
+ SetPatternArray(defSb, name, afc, abc, PatternArrays.DashDnDiag);
+ break;
+ case FillPatternStyle.DashUpDiag:
+ SetPatternArray(defSb, name, afc, abc, PatternArrays.DashUpDiag);
+ break;
+ case FillPatternStyle.DashHorz:
+ SetPatternArray(defSb, name, afc, abc, PatternArrays.DashHorz);
+ break;
+ case FillPatternStyle.DashVert:
+ SetPatternArray(defSb, name, afc, abc, PatternArrays.DashVert);
+ break;
+ case FillPatternStyle.SmConfetti:
+ SetPatternArray(defSb, name, afc, abc, PatternArrays.SmConfetti);
+ break;
+ case FillPatternStyle.LgConfetti:
+ SetPatternArray(defSb, name, afc, abc, PatternArrays.LgConfetti);
+ break;
+ case FillPatternStyle.ZigZag:
+ SetPatternArray(defSb, name, afc, abc, PatternArrays.ZigZag);
+ break;
+ case FillPatternStyle.Wave:
+ SetPatternArray(defSb, name, afc, abc, PatternArrays.Wave);
+ break;
+ case FillPatternStyle.DiagBrick:
+ SetPatternArray(defSb, name, afc, abc, PatternArrays.DiagBrick);
+ break;
+ case FillPatternStyle.HorzBrick:
+ SetPatternArray(defSb, name, afc, abc, PatternArrays.HorzBrick);
+ break;
+ case FillPatternStyle.Weave:
+ SetPatternArray(defSb, name, afc, abc, PatternArrays.Weave);
+ break;
+ case FillPatternStyle.Plaid:
+ SetPatternArray(defSb, name, afc, abc, PatternArrays.Plaid);
+ break;
+ case FillPatternStyle.Divot:
+ SetPatternArray(defSb, name, afc, abc, PatternArrays.Divot);
+ break;
+ case FillPatternStyle.DotGrid:
+ SetPatternArray(defSb, name, afc, abc, PatternArrays.DotGrid);
+ break;
+ case FillPatternStyle.DotDmnd:
+ SetPatternArray(defSb, name, afc, abc, PatternArrays.DotDmnd);
+ break;
+ case FillPatternStyle.Shingle:
+ SetPatternArray(defSb, name, afc, abc, PatternArrays.Shingle);
+ break;
+ case FillPatternStyle.Trellis:
+ SetPatternArray(defSb, name, afc, abc, PatternArrays.Trellis);
+ break;
+ case FillPatternStyle.Sphere:
+ SetPatternArray(defSb, name, afc, abc, PatternArrays.Sphere);
+ break;
+ case FillPatternStyle.SmGrid:
+ SetPatternArray(defSb, name, afc, abc, PatternArrays.SmGrid);
+ break;
+ case FillPatternStyle.LgGrid:
+ SetPatternArray(defSb, name, afc, abc, PatternArrays.LgGrid);
+ break;
+ case FillPatternStyle.SmCheck:
+ SetPatternArray(defSb, name, afc, abc, PatternArrays.SmCheck);
+ break;
+ case FillPatternStyle.LgCheck:
+ SetPatternArray(defSb, name, afc, abc, PatternArrays.LgCheck);
+ break;
+ case FillPatternStyle.OpenDmnd:
+ SetPatternArray(defSb, name, afc, abc, PatternArrays.OpenDmnd);
+ break;
+ case FillPatternStyle.SolidDmnd:
+ SetPatternArray(defSb, name, afc, abc, PatternArrays.SolidDmnd);
+ break;
+ default:
+ break;
+ }
+ return name;
+ }
+ ///
+ /// Sets the pattern to the size with one point top-left and on point middle (x/y).
+ ///
+ ///
+ ///
+ ///
+ ///
+ ///
+ ///
+ private static void SetPatternHalf(StringBuilder defSb, string name, Color afc, Color abc, int width, int height)
+ {
+ defSb.Append($"");
+
+
+ defSb.Append($"");
+ defSb.Append($"");
+ defSb.Append($"");
+ defSb.Append($"");
+ }
+
+ private static object WriteFillColor(Color c)
+ {
+ double opacity = -1;
+ if (c.A < 255 && c != Color.Empty)
+ {
+ opacity = c.A / 255D * 100;
+ }
+ if (opacity == -1)
+ {
+ return $"fill=\"#{c.To6CharHexString()}\"";
+ }
+ else
+ {
+ return $"fill=\"#{c.To6CharHexString()}\" fill-opacity=\"{opacity}%\"";
+ }
+ }
+
+ private static void SetPatternArray(StringBuilder defSb, string name, Color afc, Color abc, short[][] pathArray)
+ {
+ var height = pathArray.GetLength(0);
+ var width = pathArray[0].GetLength(0);
+ defSb.Append($"");
+ defSb.Append($"");
+ for (int y = 0; y < height; y++)
+ {
+ for (int x = 0; x < width; x++)
+ {
+ if (pathArray[y][x] == 1)
+ {
+ defSb.Append($"");
+ }
+ }
+ }
+ defSb.Append($"");
+ }
+
+ private string GetScaling(RenderGradientFill gradientFill)
+ {
+ var sb = new StringBuilder();
+ var t = gradientFill.FocusPoint.TopOffset / 100;
+ var b = gradientFill.FocusPoint.BottomOffset / 100;
+ var l = gradientFill.FocusPoint.LeftOffset / 100;
+ var r = gradientFill.FocusPoint.RightOffset / 100;
+
+ var tt = gradientFill.TileRectangle.TopOffset / 100;
+ var tb = gradientFill.TileRectangle.BottomOffset / 100;
+ var tl = gradientFill.TileRectangle.LeftOffset / 100;
+ var tr = gradientFill.TileRectangle.RightOffset / 100;
+
+ var dy = Math.Abs(t - b);
+ var dx = Math.Abs(l - r);
+ //var scaleTo = Math.Min(dy, dx);
+ //var mult = 0.5 + (scaleTo / 2);
+ var cx = Bounds.Width * l;
+ var cy = Bounds.Height * t;
+ //var radX = Math.Abs((t - b )) * _svgDrawing.FontSize.Item1;
+ //var radY = Math.Abs((l - r)) * _svgDrawing.FontSize.Item2;
+ var rx = Bounds.Width * 0.5 * (Math.Abs(t - tb) + Math.Abs(b - tt));
+ var ry = Bounds.Height * 0.5 * (Math.Abs(l - tr) + Math.Abs(r - tl));
+
+ var rad = Math.Sqrt(rx * rx + ry * ry);
+
+ sb.Append($"cx=\"{cx.ToString(CultureInfo.InvariantCulture)}\" cy=\"{cy.ToString(CultureInfo.InvariantCulture)}\" ");
+
+ //if(tt-tb != 0)
+ //{
+ // sb.Append($"fy=\"{fy.ToString(CultureInfo.InvariantCulture)}\" ");
+ //}
+ if (dy > 0 && dy < 1)
+ {
+ var fy = cy * (1 + dy);
+ sb.Append($"fy=\"{fy.ToString(CultureInfo.InvariantCulture)}\" ");
+ }
+
+ if (dx > 0 && dx < 1)
+ {
+ var fx = cx * (1 + dx);
+ sb.Append($"fx=\"{fx.ToString(CultureInfo.InvariantCulture)}\" ");
+ }
+ sb.Append($"r=\"{rad.ToString(CultureInfo.InvariantCulture)}\" ");
+ sb.Append($"gradientUnits=\"userSpaceOnUse\" spreadMethod=\"pad\" gradientTransform=\"matrix(1 0 0 1 1 1)\" ");
+
+ return sb.ToString();
+ }
+
+ private void SetStopColors(StringBuilder defSb, RenderGradientFill gradientFill, PathFillMode fillMode)
+ {
+ //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...?
+ var op = c.Opacity;
+ defSb.Append($"");
+ }
+ }
+
+ private string GetXy(double? angle)
+ {
+ if (angle.HasValue && angle != 0)
+ {
+ var x1 = 0D;
+ var x2 = 0D;
+ var y1 = 0D;
+ var y2 = 0D;
+ angle %= 360;
+ if (angle <= 90)
+ {
+ x2 = 1D - Math.Sin(MathHelper.Radians(angle.Value));
+ y2 = Math.Sin(MathHelper.Radians(angle.Value));
+ }
+ else if (angle <= 180)
+ {
+ y2 = Math.Sin(MathHelper.Radians(angle.Value));
+ x1 = 1D - Math.Sin(MathHelper.Radians(angle.Value));
+ }
+ else if (angle <= 270)
+ {
+ y1 = Math.Sin(MathHelper.Radians(angle.Value - 180));
+ x1 = 1D - Math.Sin(MathHelper.Radians(angle.Value - 180));
+ }
+ else
+ {
+ y1 = Math.Sin(MathHelper.Radians(angle.Value - 180));
+ 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 "";
+ }
+
+ private string GetOpacity(double opacity)
+ {
+ if (opacity > 0 && opacity < 1)
+ {
+ return $"stop-opacity=\"{opacity.ToString("0")}%\"";
+ }
+ return "";
+ }
+
+ private string SetStretchTileProps(RenderBlipFill blipFill)
+ {
+ if (blipFill.Stretch)
+ {
+ var x = Bounds.Width * blipFill.StretchOffset.LeftOffset / 100;
+ var y = Bounds.Height * blipFill.StretchOffset.TopOffset / 100;
+ var width = Bounds.Width - x - Bounds.Width * blipFill.StretchOffset.RightOffset / 100;
+ var height = Bounds.Height - x - Bounds.Height * blipFill.StretchOffset.BottomOffset / 100;
+ return $" preserveAspectRatio=\"none\" x=\"{x.ToString(CultureInfo.InvariantCulture)}\" y=\"{y.ToString(CultureInfo.InvariantCulture)}\" width=\"{width.PointToPixelString()}\" height=\"{height.PointToPixelString()}\" ";
+ }
+ else if (!(blipFill.Tile.HorizontalOffset == 0 && blipFill.Tile.VerticalOffset == 0 &&
+ blipFill.Tile.HorizontalRatio == 100 && blipFill.Tile.VerticalRatio == 100 && blipFill.Tile.FlipMode == TileFlipMode.None))
+ {
+ var flip = "";
+ switch (blipFill.Tile.FlipMode)
+ {
+ case TileFlipMode.X:
+ flip = $" transform=\"translate({Bounds.Width.ToString(CultureInfo.InvariantCulture)}, 0) scale(-1, 1)\"";
+ break;
+ case TileFlipMode.Y:
+ flip = $" transform=\"translate(0, {Bounds.Height.ToString(CultureInfo.InvariantCulture)}) scale(1, -1)\"";
+ break;
+ case TileFlipMode.XY:
+ flip = $" transform=\"translate({Bounds.Width.ToString(CultureInfo.InvariantCulture)}, {Bounds.Height.ToString(CultureInfo.InvariantCulture)}) scale(-1, -1)\"";
+ break;
+ }
+ return $"{flip}";
+ }
+
+ return "";
+ }
+
+ private object GetImageAsHref(RenderBlipFill blipFill)
+ {
+ return $"data:{blipFill.ContentType};base64," + Convert.ToBase64String(blipFill.ImageBytes);
+ }
+ }
+
+ public interface IShapeRenderer
+ {
+ IBasicIShapesRenderer BasicShapesRenderer { get; }
+ T OutputStream { get; }
+ bool PreRender(List items);
+ BoundingBox Bounds { get; }
+ string ViewBox { get; set; }
+ bool Render(List items);
+ }
+
+ public class SvgBasicShapesRenderer : IBasicIShapesRenderer
+ {
+ public SvgBasicShapesRenderer(StringBuilder outputStream)
+ {
+ LineRenderer = new SvgLineRenderer(outputStream);
+ RectangleRenderer = new SvgRectRenderer(outputStream);
+ EllipseRenderer = new SvgEllipseRenderer(outputStream);
+ PathRenderer = new SvgPathRenderer(outputStream);
+ ParagraphRenderer = new SvgParagraphRenderer(this, outputStream);
+ GroupRenderer = new SvgGroupRenderer(this, outputStream);
+ TextRunRenderer = new SvgTextRunRenderer(outputStream);
+ TitleRenderer = new SvgTitleRenderer(outputStream);
+ UseReferenceRenderer = new SvgUseReferenceRenderer(outputStream);
+
+ // ImageRenderer = new SvgImageRenderer(outputStream);
+ }
+ public BaseRenderer GroupRenderer { get; }
+ public BaseRenderer RectangleRenderer { get; }
+ public BaseRenderer EllipseRenderer { get; }
+ public BaseRenderer PathRenderer { get; }
+ //public BaseRenderer ImageRenderer { get; }
+ public BaseRenderer LineRenderer { get; }
+ public BaseRenderer TitleRenderer { get; }
+ public BaseRenderer ParagraphRenderer { get; }
+ public BaseRenderer TextRunRenderer { get; }
+ public BaseRenderer UseReferenceRenderer { get; }
+
+ public void Render(RenderItem item)
+ {
+ switch(item.Type)
+ {
+ case RenderItemType.Group:
+ GroupRenderer.Render((GroupRenderItem)item);
+ break;
+ case RenderItemType.Line:
+ LineRenderer.Render((LineRenderItem)item);
+ break;
+ case RenderItemType.Rect:
+ RectangleRenderer.Render((RectRenderItem)item);
+ break;
+ case RenderItemType.Ellipse:
+ EllipseRenderer.Render((EllipseRenderItem)item);
+ break;
+ case RenderItemType.Path:
+ PathRenderer.Render((PathRenderItem)item);
+ break;
+ case RenderItemType.Text:
+ throw new NotImplementedException();
+ break;
+ case RenderItemType.CommentTitle:
+ TitleRenderer.Render((TitleRenderItem)item);
+ break;
+ case RenderItemType.Paragraph:
+ ParagraphRenderer.Render((ParagraphRenderItem)item);
+ break;
+ case RenderItemType.UseReference:
+ UseReferenceRenderer.Render((UseReferenceRenderItem)item);
+ break;
+ case RenderItemType.TextRun:
+ TextRunRenderer.Render((TextRunRenderItem)item);
+ break;
+ }
+ }
+ }
+ public interface IBasicIShapesRenderer
+ {
+ public BaseRenderer GroupRenderer { get; }
+ public BaseRenderer RectangleRenderer { get; }
+ public BaseRenderer EllipseRenderer { get; }
+ public BaseRenderer PathRenderer { get; }
+ //public BaseRenderer ImageRenderer { get; }
+ public BaseRenderer LineRenderer { get; }
+ public BaseRenderer TitleRenderer { get; }
+ public BaseRenderer ParagraphRenderer { get; }
+ public BaseRenderer UseReferenceRenderer { get; }
+
+ public void Render(RenderItem item);
+ }
+ public abstract class BaseRenderer
+ {
+ protected BaseRenderer(T outputStream)
+ {
+ OutputStream = outputStream;
+ }
+ public T OutputStream { get; }
+ public abstract void Render(T2 item);
+ }
+}
diff --git a/src/EPPlus.DrawingRenderer/Svg/Options/SvgRenderOptions.cs b/src/EPPlus.DrawingRenderer/Svg/Options/SvgRenderOptions.cs
new file mode 100644
index 0000000000..ea42b7c955
--- /dev/null
+++ b/src/EPPlus.DrawingRenderer/Svg/Options/SvgRenderOptions.cs
@@ -0,0 +1,23 @@
+/*************************************************************************************************
+ 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
+ *************************************************************************************************
+ 01/27/2020 EPPlus Software AB Initial release EPPlus 5
+ *************************************************************************************************/
+namespace EPPlus.DrawingRenderer.Svg
+{
+ ///
+ /// Options for rendering drawings to svg.
+ ///
+ public class SvgRenderOptions
+ {
+ public SvgSize Size { get; } = new SvgSize();
+ }
+
+}
\ No newline at end of file
diff --git a/src/EPPlus.DrawingRenderer/Svg/Options/SvgSize.cs b/src/EPPlus.DrawingRenderer/Svg/Options/SvgSize.cs
new file mode 100644
index 0000000000..ba651d948c
--- /dev/null
+++ b/src/EPPlus.DrawingRenderer/Svg/Options/SvgSize.cs
@@ -0,0 +1,70 @@
+/*************************************************************************************************
+ 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
+ *************************************************************************************************
+ 01/27/2020 EPPlus Software AB Initial release EPPlus 5
+ *************************************************************************************************/
+
+using EPPlus.Fonts.OpenType.Utils;
+
+namespace EPPlus.DrawingRenderer.Svg
+{
+ public class SvgSize
+ {
+ ///
+ /// Overrides the width for ouput the svg image.
+ ///
+ public double? Width { get; set; } = null;
+ ///
+ /// Overrides the height of the ouput svg drawing.
+ ///
+ public double? Height { get; set; } = null;
+ public SvgSizeUnit Unit { get; set; } = SvgSizeUnit.Pixels;
+ public double WidthPixels
+ {
+ get
+ {
+ return GetPixels(Width ?? 0D, Unit);
+ }
+ }
+ public double HeightPixels
+ {
+ get
+ {
+ return GetPixels(Height ?? 0D, Unit);
+ }
+ }
+
+ internal static double GetPixels(double width, SvgSizeUnit unit)
+ {
+ switch(unit)
+ {
+ case SvgSizeUnit.Points:
+ return width.PointToPixel();
+ case SvgSizeUnit.Inches:
+ return width * 96;
+ case SvgSizeUnit.Centimeters:
+ return width * 96 / 2.54;
+ case SvgSizeUnit.Millimeters:
+ return width * 96 / 25.4;
+ default: //Pixels
+ return width;
+ }
+ }
+ }
+}
+
+ public enum SvgSizeUnit
+ {
+ Pixels=0,
+ Points=1,
+ Millimeters, // mm
+ Centimeters, // cm
+ Inches // in
+}
diff --git a/src/EPPlus.DrawingRenderer/Svg/SvgBaseRenderer.cs b/src/EPPlus.DrawingRenderer/Svg/SvgBaseRenderer.cs
new file mode 100644
index 0000000000..3bf5da9712
--- /dev/null
+++ b/src/EPPlus.DrawingRenderer/Svg/SvgBaseRenderer.cs
@@ -0,0 +1,120 @@
+using EPPlus.DrawingRenderer.RenderItems;
+using System.Globalization;
+using System.Text;
+
+namespace EPPlus.DrawingRenderer.Svg
+{
+ public abstract class SvgBaseRenderer : BaseRenderer where T : RenderItem
+ {
+ protected SvgBaseRenderer(StringBuilder outputStream) : base(outputStream)
+ {
+
+ }
+
+ ///
+ /// Used if you wish to render base to a different string builder first
+ ///
+ ///
+ ///
+ protected void RenderBaseToSpecified(T item, StringBuilder sb)
+ {
+ if (item.Bounds.Name != null)
+ {
+ sb.Append($" id=\"{item.Bounds.Name}\" ");
+ }
+
+ if (string.IsNullOrEmpty(item.DefId) == false)
+ {
+ sb.Append($"id=\"{item.DefId}\" ");
+ }
+
+ if (string.IsNullOrEmpty(item.FillColor) == false)
+ {
+ sb.Append($"fill=\"{item.FillColor}\" ");
+ }
+ //If fill is null it may in e.g. Rect still get the color black which can have an opacity
+ if (item.FillOpacity != null && item.FillOpacity != 1)
+ {
+ sb.Append($"opacity=\"{item.FillOpacity.Value.ToString(CultureInfo.InvariantCulture)}\" ");
+ }
+ if (string.IsNullOrEmpty(item.FilterName) == false)
+ {
+ sb.Append($"filter=\"{item.FilterName}\" ");
+ }
+
+ if (item.BorderWidth.HasValue)
+ {
+ if (string.IsNullOrEmpty(item.BorderColor) == false)
+ {
+ sb.Append($"stroke=\"{item.BorderColor}\" ");
+ }
+ var v = item.BorderWidth.Value * Constants.EMU_PER_POINT / Constants.EMU_PER_PIXEL;
+ sb.Append($"stroke-width=\"{v.ToString(CultureInfo.InvariantCulture)}\" ");
+
+ if (item.BorderDashArray != null)
+ {
+ var BorderDashArrayStr = item.BorderDashArray.Select(x =>
+ x.ToString(CultureInfo.InvariantCulture)).ToArray();
+
+ sb.Append($"stroke-dasharray=\"" + $"{string.Join(",", BorderDashArrayStr)}\" ");
+ }
+ if (item.BorderOpacity.HasValue)
+ {
+ sb.Append($" stroke-opacity=\"{(Math.Round(item.BorderOpacity.Value * 100)).ToString(CultureInfo.InvariantCulture)}%\" ");
+ }
+ }
+
+ if (item.TransformOrigin != null)
+ {
+ sb.Append($" transform-origin=\"{item.TransformOrigin.X.ToString(CultureInfo.InvariantCulture)} {item.TransformOrigin.Y.ToString(CultureInfo.InvariantCulture)}\" ");
+ }
+
+ if (item.StrokeMiterLimit.HasValue)
+ {
+ sb.Append($"stroke-miterlimit =\"{item.StrokeMiterLimit}\" ");
+ }
+ }
+
+ protected void RenderBase(T item)
+ {
+ var sb = OutputStream;
+ RenderBaseToSpecified(item, sb);
+ }
+ protected void RenderCompoundItems(T li, double? borderWidth, string color, string filter)
+ {
+ var tmpBorderWidth = li.BorderWidth;
+ string tmpBorderColor = null;
+ li.BorderWidth = borderWidth ?? li.BorderWidth;
+ if (string.IsNullOrEmpty(color) == false)
+ {
+ tmpBorderColor = li.BorderColor;
+ li.BorderColor = color;
+ }
+
+ RenderBase(li);
+ var sb = OutputStream;
+ if (li.LineCap != LineCap.Flat)
+ {
+ sb.AppendFormat(" stroke-linecap=\"{0}\"", li.LineCap == LineCap.Round ? "round" : "square");
+ }
+ if (li.LineJoin != LineJoin.Miter)
+ {
+ sb.AppendFormat(" stroke-linejoin=\"{0}\"", li.LineJoin);
+ }
+
+ if (string.IsNullOrEmpty(filter) == false)
+ {
+ sb.Append(" " + filter);
+ }
+
+ sb.AppendFormat("/>");
+
+ li.BorderWidth = tmpBorderWidth;
+ if (string.IsNullOrEmpty(color) == false)
+ {
+ li.BorderColor = tmpBorderColor;
+ }
+ }
+
+ }
+}
diff --git a/src/EPPlus.DrawingRenderer/Svg/SvgEllipseRenderer.cs b/src/EPPlus.DrawingRenderer/Svg/SvgEllipseRenderer.cs
new file mode 100644
index 0000000000..ca9697120b
--- /dev/null
+++ b/src/EPPlus.DrawingRenderer/Svg/SvgEllipseRenderer.cs
@@ -0,0 +1,26 @@
+using EPPlus.DrawingRenderer.RenderItems;
+using EPPlus.Fonts.OpenType.Utils;
+using System.Text;
+
+namespace EPPlus.DrawingRenderer.Svg
+{
+ public class SvgEllipseRenderer : SvgBaseRenderer
+ {
+ public SvgEllipseRenderer(StringBuilder outputStream) : base(outputStream)
+ {
+ }
+ public override void Render(EllipseRenderItem item)
+ {
+ OutputStream.AppendFormat("");
+ }
+
+ }
+}
diff --git a/src/EPPlus.DrawingRenderer/Svg/SvgGroupRenderer.cs b/src/EPPlus.DrawingRenderer/Svg/SvgGroupRenderer.cs
new file mode 100644
index 0000000000..8f416409d6
--- /dev/null
+++ b/src/EPPlus.DrawingRenderer/Svg/SvgGroupRenderer.cs
@@ -0,0 +1,99 @@
+using EPPlus.DrawingRenderer.RenderItems;
+using EPPlus.Fonts.OpenType.Utils;
+using System.Globalization;
+using System.Text;
+
+namespace EPPlus.DrawingRenderer.Svg
+{
+ public class SvgGroupRenderer : SvgBaseRenderer
+ {
+ const string transformRotate = "rotate({0})";
+ const string transformScale = "scale({0}, {1})";
+
+ IBasicIShapesRenderer _shapeRenderer;
+ public SvgGroupRenderer(IBasicIShapesRenderer shapeRenderer, StringBuilder outputStream) : base(outputStream)
+ {
+ _shapeRenderer = shapeRenderer;
+ }
+ internal string Suffix = "px";
+ public override void Render(GroupRenderItem item)
+ {
+ string combinedTransform = GetCombinedTransformString(item);
+
+ //Neccesary as fallback for e.g. DataLabels
+ string fillPropery = "";
+
+ //We may need special handling for fill-color 'none' as it is sometimes transparent and sometimes un-applied.
+ if (string.IsNullOrEmpty(item.FillColor) == false)
+ {
+ fillPropery = $" fill=\"{item.FillColor}\" ";
+ }
+
+ OutputStream.Append($"");
+
+ foreach (var childItem in item.RenderItems)
+ {
+ _shapeRenderer.Render(childItem);
+ }
+
+ OutputStream.Append("");
+ }
+ string GetCombinedTransformString(GroupRenderItem item)
+ {
+ string positionStr = "";
+ string rotationStr = GetRotationStr(item);
+ string scalingStr = GetScalingStr(item);
+
+
+ //if (item.TranslationOffset != null && (item.TranslationOffset.Left == 0 && item.TranslationOffset.Top == 0) == false)
+ //{
+ // positionStr = string.Format(transformTranslate, item.TranslationOffset.Left.PointToPixelString(), item.TranslationOffset.Top.PointToPixelString()) + " ";
+ //}
+
+ positionStr = string.Format("translate({0}, {1})", item.Bounds.Left.PointToPixelString(), item.Bounds.Top.PointToPixelString()) + " ";
+
+ return positionStr + rotationStr + scalingStr;
+ }
+ string GetTransformOrigin(GroupRenderItem item)
+ {
+ string tOrigin = string.Empty;
+
+ if (item.TransformOrigin != null && (item.TransformOrigin.X == 0 && item.TransformOrigin.Y == 0) == false)
+ {
+ tOrigin = $"transform-origin=\"{item.TransformOrigin.X.PointToPixelString()} {item.TransformOrigin.Y.PointToPixelString()}\"";
+ }
+ return tOrigin;
+ }
+
+
+ string GetScalingStr(GroupRenderItem item)
+ {
+ var scaleStr = string.Empty;
+
+ if (item.Scale != null)
+ {
+ scaleStr = string.Format(transformScale, item.Scale.X.ToString(CultureInfo.InvariantCulture), item.Scale.Y.ToString(CultureInfo.InvariantCulture)) + " ";
+ }
+
+ return scaleStr;
+ }
+
+ string GetRotationStr(GroupRenderItem item)
+ {
+ if (double.IsNaN(item.Rotation) == false && item.Rotation!=0)
+ {
+ string rot = item.Rotation.ToString(CultureInfo.InvariantCulture);
+
+ if (item.RotationPoint != null && item.RotationPoint != item.TranslationOffset)
+ {
+ rot += $", {item.RotationPoint.Left.PointToPixelString()}, {item.RotationPoint.Top.PointToPixelString()}" + " ";
+ }
+
+ return string.Format(transformRotate, rot);
+ }
+
+ return string.Empty;
+ }
+
+ }
+}
diff --git a/src/EPPlus.DrawingRenderer/Svg/SvgLineRenderer.cs b/src/EPPlus.DrawingRenderer/Svg/SvgLineRenderer.cs
new file mode 100644
index 0000000000..edca4f7704
--- /dev/null
+++ b/src/EPPlus.DrawingRenderer/Svg/SvgLineRenderer.cs
@@ -0,0 +1,92 @@
+using EPPlus.DrawingRenderer.RenderItems;
+using EPPlus.Fonts.OpenType.Utils;
+using System.Globalization;
+using System.Text;
+
+namespace EPPlus.DrawingRenderer.Svg
+{
+ public class SvgLineRenderer : SvgBaseRenderer
+ {
+ public SvgLineRenderer(StringBuilder outputStream) : base(outputStream)
+ {
+ }
+ public override void Render(LineRenderItem item)
+ {
+ var li = (LineRenderItem)item;
+ StringBuilder sb = OutputStream;
+ //Draw transparent lines to create the compond line effect, as SVG does not support compound lines natively
+ switch (li.CompoundLineStyle)
+ {
+ case CompoundLineStyle.Double:
+ li.LineCap = LineCap.Flat;
+ var name = $"double-stroke-{Guid.NewGuid().ToString()}";
+ sb.Append($"");
+
+ RenderLineItem(li, li.BorderWidth, "white", null);
+ RenderLineItem(li, li.BorderWidth * (3D / 7D), "black", null);
+ sb.Append($"");
+ break;
+ case CompoundLineStyle.DoubleThickThin:
+ WriteThickThin(li, "double-thick-thin-stroke-{0}", (li.BorderWidth ?? 1D) * 1D / 7D);
+ break;
+ case CompoundLineStyle.DoubleThinThick:
+ WriteThickThin(li, "double-thin-thick-stroke-{0}", ((li.BorderWidth ?? 1D) * 1D / 7D) * -1);
+ break;
+ case CompoundLineStyle.TripleThinThickThin:
+ var guid = Guid.NewGuid().ToString();
+ var gapOffset = 5 * li.BorderWidth.Value / 16;
+ name = $"triple-stroke-{guid}";
+ sb.Append($"");
+ sb.Append($"");
+ sb.Append($"");
+ sb.Append($"");
+ RenderLineItem(li, li.BorderWidth, "white", null);
+ RenderLineItem(li, li.BorderWidth * (1D / 8D), "black", $"filter=\"url(#gap-left-{guid})\"");
+ RenderLineItem(li, li.BorderWidth * (1D / 8D), "black", $"filter=\"url(#gap-right-{guid})\"");
+ sb.Append($"");
+ break;
+ default:
+ RenderLineItem(li, null, null, null);
+ break;
+ }
+ }
+ private void WriteThickThin(LineRenderItem li, string name, double gapOffset)
+ {
+ var sb = OutputStream;
+ var guid = Guid.NewGuid().ToString();
+ name = string.Format(name, guid);
+ string gapFilterName = $"f-gap-shift-{guid}";
+ sb.Append("");
+ sb.Append($"");
+ sb.Append($"");
+ RenderLineItem(li, li.BorderWidth, "white", null);
+ RenderLineItem(li, li.BorderWidth * (1D / 4D), "black", $"filter=\"url(#{gapFilterName})\"");
+ sb.Append($"");
+ }
+ internal string Suffix = "px";
+
+ private void RenderLineItem(LineRenderItem li, double? borderWidth, string color, string filter)
+ {
+ var sb = OutputStream;
+ if (Suffix == "%")
+ {
+
+ sb.AppendFormat("
+ {
+ IBasicIShapesRenderer _shapeRenderer;
+ public SvgParagraphRenderer(IBasicIShapesRenderer shapeRenderer, StringBuilder outputStream) : base(outputStream)
+ {
+ _shapeRenderer = shapeRenderer;
+ }
+ internal string Suffix = "px";
+ public override void Render(ParagraphRenderItem item)
+ {
+ var sb = OutputStream;
+ var fontSize = item.DefaultParagraphFont.Size.PointToPixel().ToString(CultureInfo.InvariantCulture);
+
+ 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 = item.Bounds.Width;
+ // visualBoundingBox.Bounds.Height = itemBounds.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)
+ //{
+ // bb.Y = 0;
+ //}
+ //bb.X = 0;
+
+ //bb.Width = Bounds.Width;
+ //bb.Height = Bounds.Height;
+ //bb.FillColor = FillColor;
+ //bb.FillOpacity = 0.3;
+ //bb.Render(sb);
+
+ //Render text run debug boxes as we cannot place the rects after or inside the text element
+
+ //if (Runs.Count > 0)
+ //{
+ // //double lastWidth = 0;
+
+ // var bbLines = new SvgRenderRectItem();
+
+ // foreach (var textRun in Runs)
+ // {
+ // ////render txtRun debug
+ // bbLines.X = textRun.Bounds.Left;
+ // bbLines.Width = textRun.Bounds.Width;
+ // if (IsFirstParagraph == false)
+ // {
+ // bbLines.Y = -textRun.FontSizeInPixels.PixelToPoint();
+ // }
+
+ // //Render each line debug
+ // bbLines.FillColor = "purple";
+ // for (int i = 0; i < textRun.Lines.Count; i++)
+ // {
+ // if (i > 0)
+ // {
+ // bbLines.Y += textRun.YIncreasePerLine[i];
+ // //lastWidth = 0;
+ // bbLines.X = 0;
+ // }
+ // else
+ // {
+ // bbLines.X = textRun.Bounds.Left;
+ // }
+
+ // bbLines.Height = textRun.FontSizeInPixels;
+ // bbLines.Width = textRun.PerLineWidth[i];
+ // bbLines.RenderRect(sb);
+ // }
+ // }
+ //}
+
+ sb.Append("");
+
+ if (item.Runs != null && item.Runs.Count > 0)
+ {
+ foreach (var textRun in item.Runs)
+ {
+ _shapeRenderer.Render(textRun);
+ }
+ }
+
+ sb.AppendLine("");
+ sb.AppendLine("");
+ }
+
+ //string GetFontStyleAttributes()
+ //{
+ // string fontStyleAttributes = " ";
+
+ // if (_isItalic)
+ // {
+ // fontStyleAttributes += "font-style=\"italic\" ";
+ // }
+ // if (_isBold)
+ // {
+ // fontStyleAttributes += "font-weight=\"bold\" ";
+ // }
+ // if (_underLineType != eUnderLineType.None | _strikeType != eStrikeType.No)
+ // {
+
+ // fontStyleAttributes += "text-decoration=\" ";
+ // if (_underLineType != eUnderLineType.None)
+ // {
+ // switch (_underLineType)
+ // {
+ // case eUnderLineType.Single:
+ // fontStyleAttributes += "underline";
+ // break;
+ // //These are all css only apparently
+ // //case eUnderLineType.Double:
+ // // fontStyleAttributes += "double";
+ // // break;
+ // //case eUnderLineType.Dotted:
+ // // fontStyleAttributes += "dotted";
+ // // break;
+ // //case eUnderLineType.Dash:
+ // // fontStyleAttributes += "dashed";
+ // // break;
+ // //case eUnderLineType.Wavy:
+ // // fontStyleAttributes += "wavy";
+ // // break;
+ // default:
+ // fontStyleAttributes += "underline";
+ // break;
+ // //throw new NotImplementedException("Not implemented yet");
+ // }
+ // }
+
+ // if (_strikeType == eStrikeType.Single)
+ // {
+ // //Has to check if Both underline and strike
+ // if (_underLineType != eUnderLineType.None)
+ // {
+ // fontStyleAttributes += ",";
+ // }
+ // fontStyleAttributes += "line-through";
+ // }
+
+ // fontStyleAttributes += "\" ";
+ // }
+
+ // return fontStyleAttributes;
+ //}
+ }
+}
diff --git a/src/EPPlus.DrawingRenderer/Svg/SvgPathRenderer.cs b/src/EPPlus.DrawingRenderer/Svg/SvgPathRenderer.cs
new file mode 100644
index 0000000000..9fe9acde34
--- /dev/null
+++ b/src/EPPlus.DrawingRenderer/Svg/SvgPathRenderer.cs
@@ -0,0 +1,99 @@
+using EPPlus.DrawingRenderer.RenderItems;
+using EPPlus.Fonts.OpenType.Utils;
+using EPPlusImageRenderer;
+using EPPlusImageRenderer.RenderItems;
+using System.Globalization;
+using System.Text;
+
+namespace EPPlus.DrawingRenderer.Svg
+{
+ public class SvgPathRenderer : SvgBaseRenderer
+ {
+ public SvgPathRenderer(StringBuilder outputStream) : base(outputStream)
+ {
+ }
+ public override void Render(PathRenderItem path)
+ {
+ StringBuilder sb = OutputStream;
+ //Draw transparent lines to create the compond line effect, as SVG does not support compound lines natively
+ switch (path.CompoundLineStyle)
+ {
+ case CompoundLineStyle.Single:
+ RenderPathItem(path, null, null, null);
+ break;
+ case CompoundLineStyle.Double:
+ var name = $"double-stroke-{Guid.NewGuid().ToString()}";
+ sb.Append($"");
+
+ RenderPathItem(path, path.BorderWidth, "white", null);
+ RenderPathItem(path, path.BorderWidth * (3D / 7D), "black", null);
+ sb.Append($"");
+ break;
+ case CompoundLineStyle.DoubleThickThin:
+ WriteThickThin(path, (path.BorderWidth ?? 1D) * 1D / 7D);
+ break;
+ case CompoundLineStyle.DoubleThinThick:
+ WriteThickThin(path, ((path.BorderWidth ?? 1D) * 1D / 7D) * -1);
+ break;
+ case CompoundLineStyle.TripleThinThickThin:
+ var guid = Guid.NewGuid().ToString();
+ var gapOffset = 5 * (path.BorderWidth??1D) / 16;
+ name = $"triple-stroke-{guid}";
+ sb.Append($"");
+ sb.Append($"");
+ sb.Append($"");
+ sb.Append($"");
+ RenderPathItem(path, path.BorderWidth, "white", null);
+ RenderPathItem(path, path.BorderWidth * (1D / 8D), "black", $"filter=\"url(#gap-left-{guid})\"");
+ RenderPathItem(path, path.BorderWidth * (1D / 8D), "black", $"filter=\"url(#gap-right-{guid})\"");
+ sb.Append($"");
+ break;
+ }
+ }
+ private void RenderPathItem(PathRenderItem path, double? borderWidth, string color, string filter)
+ {
+ OutputStream.Append($" 0)
+ {
+ OutputStream.Remove(OutputStream.Length - 1, 1);
+ }
+ }
+
+ private void WriteThickThin(PathRenderItem path, double gapOffset)
+ {
+ var guid = Guid.NewGuid().ToString();
+ var name = $"double-thick-thin-stroke-{guid}";
+ string gapFilterName = $"f-gap-shift-{guid}";
+ OutputStream.Append("");
+ OutputStream.Append($"");
+ OutputStream.Append($"");
+ RenderPathItem(path, path.BorderWidth, "white", null);
+ RenderPathItem(path, path.BorderWidth * (1 / 4D), "black", $"filter=\"url(#{gapFilterName})\"");
+ OutputStream.Append($"");
+ }
+ }
+}
diff --git a/src/EPPlus.DrawingRenderer/Svg/SvgRectRenderer.cs b/src/EPPlus.DrawingRenderer/Svg/SvgRectRenderer.cs
new file mode 100644
index 0000000000..9ca3b3ff3c
--- /dev/null
+++ b/src/EPPlus.DrawingRenderer/Svg/SvgRectRenderer.cs
@@ -0,0 +1,49 @@
+using EPPlus.DrawingRenderer.RenderItems;
+using EPPlus.Fonts.OpenType.Utils;
+using System.Globalization;
+using System.Text;
+
+namespace EPPlus.DrawingRenderer.Svg
+{
+ public class SvgRectRenderer : SvgBaseRenderer
+ {
+ public SvgRectRenderer(StringBuilder outputStream) : base(outputStream)
+ {
+
+ }
+ internal string Suffix = "px";
+ public override void Render(RectRenderItem item)
+ {
+ if (Suffix == "%")
+ {
+ OutputStream.AppendFormat("");
+ }
+ }
+ public class SvgUseReferenceRenderer : SvgBaseRenderer
+ {
+ public SvgUseReferenceRenderer(StringBuilder outputStream) : base(outputStream)
+ {
+ }
+ internal string Suffix = "px";
+ public override void Render(UseReferenceRenderItem item)
+ {
+ string renderStr = $"