diff --git a/src/EPPlus.DrawingRenderer.Tests/Chart/BarChartTests.cs b/src/EPPlus.DrawingRenderer.Tests/Chart/BarChartTests.cs index 6620be6406..cbecdc4035 100644 --- a/src/EPPlus.DrawingRenderer.Tests/Chart/BarChartTests.cs +++ b/src/EPPlus.DrawingRenderer.Tests/Chart/BarChartTests.cs @@ -42,5 +42,42 @@ public void GenerateSvgForBarCharts2() } } } + + [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 index 63b2cd8be4..6d6002687e 100644 --- a/src/EPPlus.DrawingRenderer.Tests/Chart/ColumnChartTests.cs +++ b/src/EPPlus.DrawingRenderer.Tests/Chart/ColumnChartTests.cs @@ -21,7 +21,7 @@ public void GenerateSvgForColumnCharts1() //var ix = 1; //var c = ws.Drawings[ix]; - //var svg = renderer.RenderDrawingToSvg(c); + //var svg = c.ToSvg(); //SaveTextFileToWorkbook($"svg\\ChartForSvg_ind{ix++}.svg", svg); var ix = 0; diff --git a/src/EPPlus.DrawingRenderer.Tests/Chart/ErrorbarsTests.cs b/src/EPPlus.DrawingRenderer.Tests/Chart/ErrorbarsTests.cs new file mode 100644 index 0000000000..a3a2e76f64 --- /dev/null +++ b/src/EPPlus.DrawingRenderer.Tests/Chart/ErrorbarsTests.cs @@ -0,0 +1,50 @@ +using OfficeOpenXml; +using OfficeOpenXml.Drawing.Chart; + +namespace EPPlus.Export.ImageRenderer.Tests.Chart +{ + [TestClass] + public class ErrorbarsTests : TestBase + { + [TestMethod] + public void GenerateSvgForErrorbars_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); + } + } + } + } +} diff --git a/src/EPPlus.DrawingRenderer.Tests/Chart/LineChartToSvgTests.cs b/src/EPPlus.DrawingRenderer.Tests/Chart/LineChartToSvgTests.cs index b7fe5272b2..2eb2da01c0 100644 --- a/src/EPPlus.DrawingRenderer.Tests/Chart/LineChartToSvgTests.cs +++ b/src/EPPlus.DrawingRenderer.Tests/Chart/LineChartToSvgTests.cs @@ -24,11 +24,11 @@ public void GenerateSvgForLineCharts_sheet1() //var svg = c.ToSvg(); //SaveTextFileToWorkbook($"svg\\ChartForSvg_ind{ix++}.svg", svg); - var ix = 0; - foreach (ExcelChart c in ws.Drawings) + for(int i = 0; i< ws.Drawings.Count; i++) { + var c = ws.Drawings[i]; var svg = c.ToSvg(); - SaveTextFileToWorkbook($"svg\\ChartForSvg{ix++}.svg", svg); + SaveTextFileToWorkbook($"svg\\ChartForSvg{i}.svg", svg); } } } @@ -128,64 +128,65 @@ public void GenerateSvgForLineCharts() } } } + [TestMethod] public void GenerateSuperScript() { ExcelPackage.License.SetNonCommercialOrganization("EPPlus Project"); - using (var p = OpenTemplatePackage("Superscript.xlsx")) + using (var p = OpenTemplatePackage("ChartForSvg_SecondaryAxis.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 svg = c.ToSvg(); + //SaveTextFileToWorkbook($"svg\\ChartForSvg_sheet2_{ix++}.svg", svg); var ix = 1; - foreach (var c in ws.Drawings) + foreach (ExcelChart c in ws.Drawings) { var svg = c.ToSvg(); - SaveTextFileToWorkbook($"svg\\ss{ix++}.svg", svg); + SaveTextFileToWorkbook($"svg\\ChartForSvg_SecAxis{ix++}.svg", svg); } } } - [TestMethod] - public void GenerateSvgForCharts_SecondaryAxis() + public void GenerateSvgForCharts_SecondaryAxis_sheet2() { ExcelPackage.License.SetNonCommercialOrganization("EPPlus Project"); using (var p = OpenTemplatePackage("ChartForSvg_SecondaryAxis.xlsx")) { - var ws = p.Workbook.Worksheets[0]; - //var ix = 3; + var ws = p.Workbook.Worksheets[1]; + //var ix = 2; //var c = ws.Drawings[ix]; //var svg = renderer.RenderDrawingToSvg(c); - //SaveTextFileToWorkbook($"svg\\ChartForSvg_sheet2_{ix++}.svg", svg); + //SaveTextFileToWorkbook($"svg\\ChartForSvg_Sheet2_SecAxis{ix++}.svg", svg); var ix = 1; foreach (ExcelChart c in ws.Drawings) { var svg = c.ToSvg(); - SaveTextFileToWorkbook($"svg\\ChartForSvg_SecAxis{ix++}.svg", svg); + SaveTextFileToWorkbook($"svg\\ChartForSvg_Sheet2_SecAxis{ix++}.svg", svg); } } } [TestMethod] - public void GenerateSvgForCharts_SecondaryAxis_sheet2() + public void GenerateSvgForCharts_SecondaryAxis_sheet3() { ExcelPackage.License.SetNonCommercialOrganization("EPPlus Project"); using (var p = OpenTemplatePackage("ChartForSvg_SecondaryAxis.xlsx")) { - var ws = p.Workbook.Worksheets[1]; - //var ix = 2; + var ws = p.Workbook.Worksheets[2]; + //var ix = 1; //var c = ws.Drawings[ix]; - //var svg = renderer.RenderDrawingToSvg(c); - //SaveTextFileToWorkbook($"svg\\ChartForSvg_Sheet2_SecAxis{ix++}.svg", svg); + //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_Sheet2_SecAxis{ix++}.svg", svg); + SaveTextFileToWorkbook($"svg\\ChartForSvg_Sheet3_SecAxis{ix++}.svg", svg); } } } + [TestMethod] public void GenerateSimplestChart() { @@ -280,5 +281,27 @@ public void GenerateSimpleLineChart() 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 index 9b73ea8922..693294b5c4 100644 --- a/src/EPPlus.DrawingRenderer.Tests/Chart/PieChartTests.cs +++ b/src/EPPlus.DrawingRenderer.Tests/Chart/PieChartTests.cs @@ -12,98 +12,20 @@ public class PieChartTests : TestBase { [TestMethod] - public void ReadAndGenerateExcelPieChartSvgs() + 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); - } - } - } - } - - - [TestMethod] - public void ReadAndGenerateExcelPieChartPointExplosion() - { - ExcelPackage.License.SetNonCommercialOrganization("EPPlus Project"); - using (var p = OpenTemplatePackage("PointExplosionStandAlone.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\\PointExplosionStandAlone\\s{i}_{ws.Name}_{c.Name}.svg", svg); - } - } - } - } - - [TestMethod] - public void BasicPieChart() - { - ExcelPackage.License.SetNonCommercialOrganization("EPPlus Project"); - using (var p = OpenTemplatePackage("BasicPieChart.xlsx")) - { - var ws = p.Workbook.Worksheets[0]; - - var ix = 0; - foreach (ExcelChart c in ws.Drawings) - { - var svg = c.ToSvg(); - SaveTextFileToWorkbook($"svg\\BasicPieChart{ix++}.svg", svg); - } - } - } - - [TestMethod] - public void Datalabels() - { - ExcelPackage.License.SetNonCommercialOrganization("EPPlus Project"); - using (var p = OpenTemplatePackage("PieChartDlblsOrig.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\\PieChartDlbls\\s{i}_{ws.Name}_{c.Name}.svg", svg); - } - } - } - } - - [TestMethod] - public void Datalabels2() - { - ExcelPackage.License.SetNonCommercialOrganization("EPPlus Project"); - using (var p = OpenTemplatePackage("PieChartDlblsInsideEndOnly.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\\PieChartDlbls\\s{i}_{ws.Name}_{c.Name}.svg", svg); + 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 index e9df670ed8..219689525b 100644 --- a/src/EPPlus.DrawingRenderer.Tests/Chart/TrendlineTests.cs +++ b/src/EPPlus.DrawingRenderer.Tests/Chart/TrendlineTests.cs @@ -13,9 +13,9 @@ public void GenerateSvgForTrendlines_Sheet1() using (var p = OpenTemplatePackage("Trendlines.xlsx")) { var ws = p.Workbook.Worksheets[0]; - //var ix = 2; + //var ix = 4; //var c = ws.Drawings[ix]; - //var svg = renderer.RenderDrawingToSvg(c); + //var svg = c.ToSvg(); //SaveTextFileToWorkbook($"svg\\Trendline_sheet1_ind{ix++}.svg", svg); var ix = 0; @@ -26,6 +26,28 @@ public void GenerateSvgForTrendlines_Sheet1() } } } + + [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() { @@ -36,7 +58,7 @@ public void GenerateSvgForTrendlines_Sheet2() //var ix = 3; //var c = ws.Drawings[ix]; - //var svg = renderer.RenderDrawingToSvg(c); + //var svg = c.ToSvg(); //SaveTextFileToWorkbook($"svg\\Trendline_sheet1_ind{ix++}.svg", svg); var ix = 0; diff --git a/src/EPPlus.DrawingRenderer.Tests/DrawingShapeRenderer/SvgStandAloneTests.cs b/src/EPPlus.DrawingRenderer.Tests/DrawingShapeRenderer/SvgStandAloneTests.cs index f4dbcfd416..df5dbb4697 100644 --- a/src/EPPlus.DrawingRenderer.Tests/DrawingShapeRenderer/SvgStandAloneTests.cs +++ b/src/EPPlus.DrawingRenderer.Tests/DrawingShapeRenderer/SvgStandAloneTests.cs @@ -1,76 +1,79 @@ using EPPlus.DrawingRenderer; using EPPlus.DrawingRenderer.RenderItems; -using EPPlus.DrawingRenderer.Svg; -using EPPlus.Export.ImageRenderer.RenderItems.Shared; -using EPPlus.Graphics; -using OfficeOpenXml.Drawing.Renderer.TextBox; -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; using EPPlus.DrawingRenderer.RenderItems.SvgItem; -using EPPlus.Fonts.OpenType.Integration.RichText; +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 { - [TestMethod] - public void SvgRectTest() + + private GroupRenderItem GenerateShapeRenderer() { - BoundingBox bounds = new BoundingBox(0,0,500,500); + BoundingBox bounds = new BoundingBox(0, 0, 500, 500); StringBuilder sb = new StringBuilder(); var svgShapeRenderer = new SvgShapeRenderer(bounds, sb); - var baseGroup = new GroupRenderItem(bounds); - var rectItem = new RectRenderItem(baseGroup.Bounds); + var baseGroup = new GroupRenderItem(bounds); - rectItem.Width = 250; - rectItem.Height = 250; - rectItem.FillColor = "darkblue"; + var background = new RectRenderItem(baseGroup.Bounds); - //var textBody = new RenderTextBody(baseGroup.Bounds, true); + background.Width = bounds.Width; + background.Height = bounds.Height; + background.FillColor = "aliceBlue"; - //textBody.Text = "Hello"; - //var para = new SvgParagraphRenderItem(textBody, textBody.Bounds); - - //var para2 = new DrawingParagraphRenderItem(textBody, textBody.Bounds); - //textBody.Paragraphs.Add + baseGroup.AddChildItem(background); + return baseGroup; + } - baseGroup.AddChildItem(rectItem); - //baseGroup.AddChildItem(textBody); + private GroupRenderItem GenerateGroupRenderItem() + { + BoundingBox bounds = new BoundingBox(0, 0, 500, 500); - List items = new List() { baseGroup }; + var baseGroup = new GroupRenderItem(bounds); - svgShapeRenderer.Render(items); + var background = new RectRenderItem(baseGroup.Bounds); - var svg = sb.ToString(); + background.Width = bounds.Width; + background.Height = bounds.Height; + background.FillColor = "aliceBlue"; - SaveTextFileToWorkbook("svg\\rectStandalone.svg", svg); + baseGroup.AddChildItem(background); + return baseGroup; } - [TestMethod] - public void SvgTextRun() + private void GenerateSvgFile(string fileName, BoundingBox bounds, params RenderItem[] items) { - BoundingBox bounds = new BoundingBox(0, 0, 500, 500); + StringBuilder sb = new StringBuilder(); var svgShapeRenderer = new SvgShapeRenderer(bounds, sb); + List renderItems = items.ToList(); + svgShapeRenderer.Render(renderItems); - var baseGroup = new GroupRenderItem(bounds); + var svg = sb.ToString(); - var background = new RectRenderItem(baseGroup.Bounds); + SaveTextFileToWorkbook($"svg\\{fileName}.svg", svg); + } - background.Width = bounds.Width; - background.Height = bounds.Height; - background.FillColor = "aliceBlue"; + [TestMethod] + public void SvgRectTest() + { + var baseGroup = GenerateShapeRenderer(); + GenerateSvgFile("rectStandAlone", baseGroup.Bounds, baseGroup); + } - baseGroup.AddChildItem(background); + [TestMethod] + public void SvgTextRun() + { + var baseGroup = GenerateShapeRenderer(); var rt = new RichTextFormatSimple(); rt.Text = "My text"; @@ -79,12 +82,30 @@ public void SvgTextRun() rt.Family = "Archivo Narrow"; rt.SubFamily = OfficeOpenXml.Interfaces.Fonts.FontSubFamily.Regular; rt.Size = 12f; - - //var paragraph = new SvgParagraphRenderItem() - var textRun = new SvgTextRunRenderItem(baseGroup.Bounds, rt, rt.Text); + 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); + + 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 }; @@ -93,49 +114,275 @@ public void SvgTextRun() var svg = sb.ToString(); - SaveTextFileToWorkbook("svg\\textRunStandAlone.svg", svg); + 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() { - BoundingBox bounds = new BoundingBox(0, 0, 500, 500); - StringBuilder sb = new StringBuilder(); - var svgShapeRenderer = new SvgShapeRenderer(bounds, sb); - + var baseGroup = GenerateGroupRenderItem(); + var textBody = GenerateTextBody(baseGroup); + GenerateSvgFile("standAloneTextBody", baseGroup.Bounds, baseGroup); + } - var baseGroup = new GroupRenderItem(bounds); + [TestMethod] + public void SvgTextBodyTestCenterAlignmentGenerated() + { + var baseGroup = GenerateGroupRenderItem(); - var background = new RectRenderItem(baseGroup.Bounds); + var textBody = GenerateTextBody(baseGroup); + textBody.Paragraphs[0].AddText(" a\r\n new day beckons"); + textBody.Paragraphs[0].HorizontalAlignment = RenderItems.Shared.TextAlignment.Center; - background.Width = bounds.Width; - background.Height = bounds.Height; - background.FillColor = "aliceBlue"; + textBody.Paragraphs[1].HorizontalAlignment = RenderItems.Shared.TextAlignment.Center; + textBody.Paragraphs[1].AddText("\r\n What fun, what fun!"); - baseGroup.Bounds.Width = bounds.Width; - baseGroup.Bounds.Height = bounds.Height; + //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(); - var textBody = new SvgTextBodyRenderItem(baseGroup.Bounds, true); - var paragraph = textBody.AddParagraph("Hello"); + 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 = textBody.AddParagraph(rtItem); + var para2 = textbox.TextBody.AddParagraph(rtItem); - baseGroup.AddChildItem(textBody); - baseGroup.AddChildItem(background); + textbox.Rectangle.FillColor = "#F9F6C4"; - List items = new List() { baseGroup }; - textBody.AppendRenderItems(items); + return textbox; + } - svgShapeRenderer.Render(items); + [TestMethod] + public void BasicTextBox() + { + var textbox = GenerateTextBox(out GroupRenderItem group); + textbox.AppendRenderItems(group.RenderItems); - var svg = sb.ToString(); + 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); - SaveTextFileToWorkbook("svg\\textBodyStandAlone.svg", svg); + 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 index 613f6b4ecc..b3596b206a 100644 --- a/src/EPPlus.DrawingRenderer.Tests/EPPlus.DrawingRenderer.Tests.csproj +++ b/src/EPPlus.DrawingRenderer.Tests/EPPlus.DrawingRenderer.Tests.csproj @@ -1,7 +1,7 @@ - net8.0 + net8.0;net462 latest enable enable diff --git a/src/EPPlus.DrawingRenderer.Tests/Shape/ShapeToSvgTests.cs b/src/EPPlus.DrawingRenderer.Tests/Shape/ShapeToSvgTests.cs index 48c1479b8e..0500a5ab0e 100644 --- a/src/EPPlus.DrawingRenderer.Tests/Shape/ShapeToSvgTests.cs +++ b/src/EPPlus.DrawingRenderer.Tests/Shape/ShapeToSvgTests.cs @@ -15,12 +15,33 @@ 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 Rect() { - ExcelPackage.License.SetNonCommercialOrganization("EPPlus Project"); + 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"; @@ -35,9 +56,9 @@ public void Rect() [TestMethod] public void AddShapeWithPatternFill() { - ExcelPackage.License.SetNonCommercialOrganization("EPPlus Project"); using (var p = OpenTemplatePackage("ShapeWithPattern.xlsx")) { + p.Workbook.UseFontEngine(DefaultFontEngine); var ws = p.Workbook.Worksheets[0]; var myShape = ws.Drawings[0].As.Shape; @@ -53,8 +74,7 @@ public void AddShapeWithPatternFill() [TestMethod] public void RoundRect() { - ExcelPackage.License.SetNonCommercialOrganization("EPPlus Project"); - using (var p = new ExcelPackage()) + using (var p = GetPackage()) { var ws = p.Workbook.Worksheets.Add("Sheet1"); var d = ws.Drawings.AddShape("Shape1", eShapeStyle.RoundRect); @@ -71,7 +91,7 @@ public void RoundRect() [TestMethod] public void Triangle() { - ExcelPackage.License.SetNonCommercialOrganization("EPPlus Project"); + using (var p = new ExcelPackage()) { var ws = p.Workbook.Worksheets.Add("Sheet1"); @@ -87,7 +107,6 @@ public void Triangle() [TestMethod] public void RightArrow() { - ExcelPackage.License.SetNonCommercialOrganization("EPPlus Project"); using (var p = new ExcelPackage()) { var ws = p.Workbook.Worksheets.Add("Sheet1"); @@ -101,7 +120,6 @@ public void RightArrow() [TestMethod] public void SmileyFace() { - ExcelPackage.License.SetNonCommercialOrganization("EPPlus Project"); using (var p = new ExcelPackage()) { var ws = p.Workbook.Worksheets.Add("Sheet1"); @@ -115,7 +133,6 @@ public void SmileyFace() [TestMethod] public void VerticalScroll() { - ExcelPackage.License.SetNonCommercialOrganization("EPPlus Project"); using (var p = new ExcelPackage()) { var ws = p.Workbook.Worksheets.Add("Sheet1"); @@ -129,7 +146,6 @@ public void VerticalScroll() [TestMethod] public void CloudCallout() { - ExcelPackage.License.SetNonCommercialOrganization("EPPlus Project"); using (var p = new ExcelPackage()) { var ws = p.Workbook.Worksheets.Add("Sheet1"); @@ -143,7 +159,6 @@ public void CloudCallout() [TestMethod] public void IrregularSeal2() { - ExcelPackage.License.SetNonCommercialOrganization("EPPlus Project"); using (var p = new ExcelPackage()) { var ws = p.Workbook.Worksheets.Add("Sheet1"); @@ -157,7 +172,6 @@ public void IrregularSeal2() [TestMethod] public void LightningBolt() { - ExcelPackage.License.SetNonCommercialOrganization("EPPlus Project"); using (var p = new ExcelPackage()) { var ws = p.Workbook.Worksheets.Add("Sheet1"); @@ -171,7 +185,6 @@ public void LightningBolt() [TestMethod] public void FlowChartMagneticTape() { - ExcelPackage.License.SetNonCommercialOrganization("EPPlus Project"); using (var p = new ExcelPackage()) { var ws = p.Workbook.Worksheets.Add("Sheet1"); @@ -185,7 +198,6 @@ public void FlowChartMagneticTape() [TestMethod] public void MathNotEqual() { - ExcelPackage.License.SetNonCommercialOrganization("EPPlus Project"); using (var p = new ExcelPackage()) { var ws = p.Workbook.Worksheets.Add("Sheet1"); @@ -199,7 +211,6 @@ public void MathNotEqual() [TestMethod] public void Sun() { - ExcelPackage.License.SetNonCommercialOrganization("EPPlus Project"); using (var p = new ExcelPackage()) { var ws = p.Workbook.Worksheets.Add("Sheet1"); @@ -215,7 +226,6 @@ public void Sun() [TestMethod] public void Ellipse() { - ExcelPackage.License.SetNonCommercialOrganization("EPPlus Project"); using (var p = new ExcelPackage()) { var ws = p.Workbook.Worksheets.Add("Sheet1"); @@ -231,8 +241,7 @@ public void Ellipse() [TestMethod] public void Heart() { - ExcelPackage.License.SetNonCommercialOrganization("EPPlus Project"); - using (var p = new ExcelPackage()) + using (var p = GetPackage()) { var ws = p.Workbook.Worksheets.Add("Sheet1"); var d = ws.Drawings.AddShape("Shape1", OfficeOpenXml.Drawing.eShapeStyle.Heart); @@ -247,7 +256,6 @@ public void Heart() [TestMethod] public void BevelRed() { - ExcelPackage.License.SetNonCommercialOrganization("EPPlus Project"); using (var p = new ExcelPackage()) { var ws = p.Workbook.Worksheets.Add("Sheet1"); @@ -263,7 +271,6 @@ public void BevelRed() [TestMethod] public void Bevel() { - ExcelPackage.License.SetNonCommercialOrganization("EPPlus Project"); using (var p = new ExcelPackage()) { var ws = p.Workbook.Worksheets.Add("Sheet1"); @@ -279,7 +286,6 @@ public void Bevel() [TestMethod] public void LeftBracket() { - ExcelPackage.License.SetNonCommercialOrganization("EPPlus Project"); using (var p = new ExcelPackage()) { var ws = p.Workbook.Worksheets.Add("Sheet1"); @@ -295,7 +301,6 @@ public void LeftBracket() [TestMethod] public void CalloutQuadArrow() { - ExcelPackage.License.SetNonCommercialOrganization("EPPlus Project"); using (var p = new ExcelPackage()) { var ws = p.Workbook.Worksheets.Add("Sheet1"); @@ -310,7 +315,6 @@ public void CalloutQuadArrow() [TestMethod] public void ActionButtonHome() { - ExcelPackage.License.SetNonCommercialOrganization("EPPlus Project"); using (var p = new ExcelPackage()) { var ws = p.Workbook.Worksheets.Add("Sheet1"); @@ -325,7 +329,6 @@ public void ActionButtonHome() [TestMethod] public void ActionButtonMovie() { - ExcelPackage.License.SetNonCommercialOrganization("EPPlus Project"); using (var p = new ExcelPackage()) { var ws = p.Workbook.Worksheets.Add("Sheet1"); @@ -340,7 +343,6 @@ public void ActionButtonMovie() [TestMethod] public void CustomPath() { - ExcelPackage.License.SetNonCommercialOrganization("EPPlus Project"); using (var p = OpenTemplatePackage(@"svg\CustPath.xlsx")) { var ws = p.Workbook.Worksheets[0]; @@ -364,7 +366,6 @@ public void CustomPath() [TestMethod] public void GenerateAllShapes() { - ExcelPackage.License.SetNonCommercialOrganization("EPPlus Project"); using (var p = new ExcelPackage()) { var ws = p.Workbook.Worksheets.Add("Shapes"); @@ -387,7 +388,6 @@ public void GenerateAllShapes() [TestMethod] public void TestShapes() { - ExcelPackage.License.SetNonCommercialOrganization("EPPlus Project"); using (var p = OpenTemplatePackage("margins.xlsx")) { var drawings = p.Workbook.Worksheets[0].Drawings; @@ -401,7 +401,6 @@ public void TestShapes() [TestMethod] public void GenerateSvgForGradientFilledShapes() { - ExcelPackage.License.SetNonCommercialOrganization("EPPlus Project"); using (var p = OpenTemplatePackage("GradientFillShapes.xlsx")) { var ws = p.Workbook.Worksheets[0]; @@ -422,7 +421,6 @@ public void GenerateSvgForGradientFilledShapes() [TestMethod] public void GenerateSvgForGradientRadialFilledShapes() { - ExcelPackage.License.SetNonCommercialOrganization("EPPlus Project"); using (var p = OpenTemplatePackage("GradiantRadial.xlsx")) { var ws = p.Workbook.Worksheets[0]; @@ -443,7 +441,6 @@ public void GenerateSvgForGradientRadialFilledShapes() [TestMethod] public void GenerateSvgForPatternFilledShapes() { - ExcelPackage.License.SetNonCommercialOrganization("EPPlus Project"); using (var p = OpenTemplatePackage("PatternFills.xlsx")) { var ws = p.Workbook.Worksheets[0]; @@ -460,7 +457,6 @@ public void GenerateSvgForPatternFilledShapes() [TestMethod] public void GenerateSvgForBlipFillShapes() { - ExcelPackage.License.SetNonCommercialOrganization("EPPlus Project"); using (var p = OpenTemplatePackage("BlipFills.xlsx")) { var ws = p.Workbook.Worksheets[0]; @@ -479,7 +475,6 @@ public void GenerateSvgForBlipFillShapes() [TestMethod] public void GenerateSvgForCircle() { - ExcelPackage.License.SetNonCommercialOrganization("EPPlus Project"); using (var p = OpenTemplatePackage("GradientRadialVerifyCircle.xlsx")) { var ws = p.Workbook.Worksheets[0]; @@ -497,10 +492,42 @@ public void GenerateSvgForCircle() } } + [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() { - ExcelPackage.License.SetNonCommercialOrganization("EPPlus Project"); using (var p = OpenTemplatePackage("SimpleChartRightAlign.xlsx")) { var c = p.Workbook.Worksheets[0].Drawings[0]; @@ -512,7 +539,6 @@ public void OpenRightAligned() [TestMethod] public void TestStyling() { - ExcelPackage.License.SetNonCommercialOrganization("EPPlus Project"); using (var p = OpenTemplatePackage("MyCellsAdvanced.xlsx")) { var ws = p.Workbook.Worksheets[0]; @@ -541,7 +567,6 @@ public void TestStyling() [TestMethod] public void GenerateShapeCenteredParagraph() { - ExcelPackage.License.SetNonCommercialOrganization("EPPlus Project"); using (var p = OpenPackage("ShapeTestCentered.xlsx",true)) { var sheet = p.Workbook.Worksheets.Add("ShapeSheet"); @@ -598,16 +623,34 @@ public void GenerateShapeCenteredParagraph() _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() { - ExcelPackage.License.SetNonCommercialOrganization("EPPlus Project"); using (var p = OpenPackage("ChartWithDifferentSizes.xlsx", true)) { var ws = p.Workbook.Worksheets.Add("Chart1"); @@ -637,20 +680,5 @@ public void CreateChartsWithDifferentSize() SaveAndCleanup(p); } } - - [TestMethod] - public void SuperAndSubScript() - { - ExcelPackage.License.SetNonCommercialOrganization("EPPlus Project"); - 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); - } - } } } diff --git a/src/EPPlus.DrawingRenderer.Tests/StyleTests.cs b/src/EPPlus.DrawingRenderer.Tests/StyleTests.cs index d98b49f066..28e3783fe1 100644 --- a/src/EPPlus.DrawingRenderer.Tests/StyleTests.cs +++ b/src/EPPlus.DrawingRenderer.Tests/StyleTests.cs @@ -1,5 +1,6 @@ using OfficeOpenXml; using System.Drawing; +using System.Linq; namespace EPPlus.Export.ImageRenderer.Tests { @@ -22,6 +23,22 @@ public void BaseThemeChartStyle() } } + [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 ExtractThemeStyleWorks() { @@ -51,18 +68,20 @@ public void ExtractThemeStyleWorks() //context.SetTranslator(fillTranslator); //context.AddDeclarations(styleClass); - simpleChart.StyleManager.ApplyStyles(); + //simpleChart.StyleManager.ApplyStyles(); //chartDefaultStyle.DataPointLine.FillReference.Color - simpleChart.Title.Font.Color = Color.CornflowerBlue; + //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.Border.Fill.Color = Color.Red; //simpleChart.StyleManager.Style.DataPointLine.Fill.Color = Color.Green; //simpleChart.StyleManager.Style.DataPoint.Fill.Color = Color.Yellow; @@ -70,7 +89,7 @@ public void ExtractThemeStyleWorks() simpleChart.StyleManager.ApplyStyles(); - simpleChart.Title.TextBody.Paragraphs[0].TextRuns[0].Fill.Color = Color.CornflowerBlue; + //simpleChart.Title.TextBody.Paragraphs[0].TextRuns[0].Fill.Color = Color.CornflowerBlue; var svg = simpleChart.ToSvg(); SaveTextFileToWorkbook($"svg\\MyLineIonThemeExcel2.svg", svg); @@ -87,6 +106,38 @@ public void ExtractThemeStyleWorks() } + [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() { diff --git a/src/EPPlus.DrawingRenderer.Tests/TestFontMeasurer.cs b/src/EPPlus.DrawingRenderer.Tests/TestFontMeasurer.cs index 80733eb230..a92705daff 100644 --- a/src/EPPlus.DrawingRenderer.Tests/TestFontMeasurer.cs +++ b/src/EPPlus.DrawingRenderer.Tests/TestFontMeasurer.cs @@ -4,12 +4,30 @@ 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() { @@ -20,7 +38,7 @@ public void CompareFontMeasurer3() Size = 72.0f, }; - var handler = new TextHandler(mf); + var handler = new TextHandler(SystemFolderEngine, mf); var testStr = "Hello there⁴₂"; @@ -56,7 +74,7 @@ public void TestWrapText() Style = MeasurementFontStyles.Regular }; - var handler = new TextHandler(mf); + var handler = new TextHandler(SystemFolderEngine, mf); var strings = handler.WrapText(testStr, MaxPixelWidth.PixelToPoint()); @@ -86,7 +104,7 @@ public void TestWrapTextLongContinous() Size = (float)fontSize, Style = MeasurementFontStyles.Regular }; - var handler = new TextHandler(mf); + var handler = new TextHandler(SystemFolderEngine, mf); var strings = handler.WrapText(testString, MaxPixelWidth.PixelToPoint()); @@ -139,7 +157,7 @@ public void SpaceCase() Style = MeasurementFontStyles.Regular, Size = 11.0f, }; - var layout = OpenTypeFonts.GetTextLayoutEngineForFont(mf); + var layout = SystemFolderEngine.GetTextLayoutEngineForFont(mf); var output = layout.WrapText(text, 11f, 39.4); var shaper = OpenTypeFonts.GetShaperForFont(mf); @@ -169,7 +187,7 @@ public void LoremIpsumTesting() Size = 11.0f, }; - var handler = new TextHandler(mf); + var handler = new TextHandler(SystemFolderEngine, mf); //float ptMax = 39.68503937007874015748031496063f; //float ptMax = 36.840393700787401574803149606299f - 1.5f; //float pixels = 53f; @@ -178,7 +196,8 @@ public void LoremIpsumTesting() ShapingOptions options = new ShapingOptions(); - var layout = OpenTypeFonts.GetTextLayoutEngineForFont(mf); + 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); @@ -231,7 +250,7 @@ public void WrapDifficultSpotSpace() Size = 11.0f, }; - var handler = new TextHandler(mf); + var handler = new TextHandler(SystemFolderEngine, mf); var wrappedStrings = handler.WrapText(text, pointWidth); @@ -252,7 +271,7 @@ public void LoremIpsum20Paragraphs() Size = 11.0f, }; - var handler = new TextHandler(mf); + var handler = new TextHandler(SystemFolderEngine, mf); double maxPixelWidth = 72d; @@ -260,7 +279,7 @@ public void LoremIpsum20Paragraphs() 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("\r\n"); + var savedStrings = SavedComparisonString.Split(new string[] { "\r\n" }, StringSplitOptions.None); List faultyStrings = new(); List excpectedStrings = new(); @@ -299,7 +318,7 @@ public void LoremIpsum20ParagraphsMultipleFragments() Size = 11.0f, }; - var handler = new TextHandler(mf); + var handler = new TextHandler(SystemFolderEngine, mf); double maxPixelWidth = 72d; @@ -311,7 +330,7 @@ public void LoremIpsum20ParagraphsMultipleFragments() var pointWidth = 54.085732283464566929133858267717f; var wrappedStrings = handler.WrapText(Lorem20Str, pointWidth); - var savedStrings = SavedComparisonString.Split("\r\n"); + var savedStrings = SavedComparisonString.Split(new string[] { "\r\n" }, StringSplitOptions.None); List differingStrings = new(); diff --git a/src/EPPlus.DrawingRenderer/Constants/PatternArrays.cs b/src/EPPlus.DrawingRenderer/Constants/PatternArrays.cs index a8ccf368b7..0992f78af8 100644 --- a/src/EPPlus.DrawingRenderer/Constants/PatternArrays.cs +++ b/src/EPPlus.DrawingRenderer/Constants/PatternArrays.cs @@ -15,7 +15,6 @@ namespace DrawingRenderer.Constants { internal static class PatternArrays { - internal static readonly short[][] Pct30 = new short[][] { new short[] { 0, 0, 0, 1 }, diff --git a/src/EPPlus.DrawingRenderer/EPPlus.DrawingRenderer.csproj b/src/EPPlus.DrawingRenderer/EPPlus.DrawingRenderer.csproj index 687c3fedc8..7643545c66 100644 --- a/src/EPPlus.DrawingRenderer/EPPlus.DrawingRenderer.csproj +++ b/src/EPPlus.DrawingRenderer/EPPlus.DrawingRenderer.csproj @@ -11,6 +11,8 @@ A spreadsheet library for .NET framework and .NET core latest + True + EPPlus.DrawingRenderer.snk 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 index a0bd9e0264..214a5fc828 100644 --- a/src/EPPlus.DrawingRenderer/RenderItems/GradientFill.cs +++ b/src/EPPlus.DrawingRenderer/RenderItems/GradientFill.cs @@ -83,6 +83,10 @@ public RenderGradientFill() 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(); diff --git a/src/EPPlus.DrawingRenderer/RenderItems/RenderItem.cs b/src/EPPlus.DrawingRenderer/RenderItems/RenderItem.cs index 9befe6cb01..bd46e6b0c0 100644 --- a/src/EPPlus.DrawingRenderer/RenderItems/RenderItem.cs +++ b/src/EPPlus.DrawingRenderer/RenderItems/RenderItem.cs @@ -230,7 +230,7 @@ public EllipseRenderItem(BoundingBox parent) : base(parent) { } - public override RenderItemType Type => RenderItemType.Rect; + public override RenderItemType Type => RenderItemType.Ellipse; public double Cx { get; set; } public double Cy { get; set; } public double Rx { get; set; } @@ -316,7 +316,7 @@ public LineRenderItem Clone() } public abstract class DrawingObject { - public abstract void AppendRenderItems(List renderItems); + public virtual void AppendRenderItems(List renderItems) { } } public abstract class RenderItem : RenderItemBase { diff --git a/src/EPPlus.DrawingRenderer/RenderItems/SvgItem/SvgParagraphRenderItem.cs b/src/EPPlus.DrawingRenderer/RenderItems/SvgItem/SvgParagraphRenderItem.cs index 72081685e5..06599819d6 100644 --- a/src/EPPlus.DrawingRenderer/RenderItems/SvgItem/SvgParagraphRenderItem.cs +++ b/src/EPPlus.DrawingRenderer/RenderItems/SvgItem/SvgParagraphRenderItem.cs @@ -1,4 +1,5 @@ -using EPPlus.Export.ImageRenderer.RenderItems.Shared; +using EPPlus.DrawingRenderer; +using EPPlus.Export.ImageRenderer.RenderItems.Shared; using EPPlus.Fonts.OpenType.Integration.DataHolders; using EPPlus.Graphics; @@ -7,11 +8,11 @@ namespace EPPlus.DrawingRenderer.RenderItems.SvgItem { public class SvgParagraphRenderItem : ParagraphRenderItem { - public SvgParagraphRenderItem(RenderTextBody body, BoundingBox parent, string text, bool setDefaultFont = true) : base(parent, body, text, setDefaultFont) + public SvgParagraphRenderItem(RenderContext renderContext, RenderTextBody body, BoundingBox parent, string text, bool setDefaultFont = true) : base(renderContext, parent, body, text, setDefaultFont) { ImportStyles(); } - public SvgParagraphRenderItem(RenderTextBody textBody, BoundingBox parent, IRichTextFormatSimple rtFormat): base(parent, textBody, rtFormat) + public SvgParagraphRenderItem(RenderContext renderContext, RenderTextBody textBody, BoundingBox parent, IRichTextFormatSimple rtFormat) : base(renderContext, parent, textBody, rtFormat) { ImportStyles(); } @@ -42,4 +43,4 @@ protected override TextRunRenderItem CreateTextRun(BoundingBox parent, string di 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 index 70ef9e714e..8716dae0ce 100644 --- a/src/EPPlus.DrawingRenderer/RenderItems/SvgItem/SvgTextBodyRenderItem.cs +++ b/src/EPPlus.DrawingRenderer/RenderItems/SvgItem/SvgTextBodyRenderItem.cs @@ -1,29 +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(BoundingBox parent, bool autoSize) : base(parent, autoSize) + public SvgTextBodyRenderItem(RenderContext renderContext, BoundingBox parent, bool autoSize) : base(renderContext, parent, autoSize) { } - public SvgTextBodyRenderItem(BoundingBox parent, double left, double top, double maxWidth, double maxHeight, bool clampedToParent = false, bool autoSize = false) : base(parent, left, top, maxWidth, maxHeight, clampedToParent, 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(this, parent, textIfEmpty); + return new SvgParagraphRenderItem(RenderContext, this, parent, textIfEmpty); } protected override ParagraphRenderItem CreateParagraph(BoundingBox parent, IRichTextFormatSimple richText) { - return new SvgParagraphRenderItem(this, parent, 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 index e681ac1eef..3971adac4c 100644 --- a/src/EPPlus.DrawingRenderer/RenderItems/SvgItem/SvgTextRunRenderItem.cs +++ b/src/EPPlus.DrawingRenderer/RenderItems/SvgItem/SvgTextRunRenderItem.cs @@ -19,12 +19,21 @@ public SvgTextRunRenderItem(BoundingBox parent, string text, int origRtIdx) : ba { } - public SvgTextRunRenderItem(BoundingBox parent, IFontFormatBase font, string displayText) : base(parent, font, displayText) + 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/Textbox/ParagraphRenderItem.cs b/src/EPPlus.DrawingRenderer/RenderItems/Textbox/ParagraphRenderItem.cs index 2211eeb9ab..3b54e73264 100644 --- a/src/EPPlus.DrawingRenderer/RenderItems/Textbox/ParagraphRenderItem.cs +++ b/src/EPPlus.DrawingRenderer/RenderItems/Textbox/ParagraphRenderItem.cs @@ -1,5 +1,6 @@ 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; @@ -82,7 +83,11 @@ public abstract class ParagraphRenderItem : RenderItem protected RichTextCollectionBase _textFragments = new RichTextCollectionBase(); public double ParagraphLineSpacing { get; protected set; } - public TextAlignment HorizontalAlignment { 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; @@ -110,11 +115,16 @@ protected bool LinespacingIsExact } } + protected RenderContext RenderContext { get; private set; } + + + protected TextLineSpacing _lsType; protected double? _centerAdjustment; - protected ParagraphRenderItem(BoundingBox parent, bool setFallbackDefaultFont = true) : base(parent) + protected ParagraphRenderItem(RenderContext renderContext, BoundingBox parent, bool setFallbackDefaultFont = true) : base(parent) { + RenderContext = renderContext; Bounds.Name = "Paragraph"; if (setFallbackDefaultFont) { @@ -124,25 +134,34 @@ protected ParagraphRenderItem(BoundingBox parent, bool setFallbackDefaultFont = } } - protected ParagraphRenderItem(BoundingBox parent, RenderTextBody textBody, bool setFallbackDefaultFont = true) : this(parent, setFallbackDefaultFont) + protected ParagraphRenderItem(RenderContext renderContext, BoundingBox parent, RenderTextBody textBody, bool setFallbackDefaultFont = true) + : this(renderContext, parent, setFallbackDefaultFont) { InitBasedOnParent(textBody); Bounds.Name = "Paragraph"; } - protected ParagraphRenderItem(BoundingBox parent, RenderTextBody textBody, string text, bool setFallbackDefaultFont = true) : this(parent, textBody, setFallbackDefaultFont) + protected ParagraphRenderItem(RenderContext renderContext, BoundingBox parent, RenderTextBody textBody, string text, bool setFallbackDefaultFont = true) + : this(renderContext, parent, textBody, setFallbackDefaultFont) { _lsMultiplier = 1d; ImportLinesAndTextRunsBase(text); } - protected ParagraphRenderItem(BoundingBox parent, RenderTextBody textBody, IRichTextFormatSimple rtFormat) : this(parent, textBody, false) + 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; @@ -170,13 +189,25 @@ void InitBasedOnParent(RenderTextBody 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(_textFragments); + _layoutSystem = new LayoutSystem(RenderContext.FontEngine, _textFragments); //if (fragments == null && _layoutSystem == null) //{ @@ -229,15 +260,7 @@ protected void ImportLinesAndTextRunsBase(string textIfEmpty) } AddDefaultTextFragment(textIfEmpty); - - Bounds.Left = GetAlignmentHorizontal(HorizontalAlignment); - if (HorizontalAlignment == TextAlignment.Center) - { - _centerAdjustment = GetAlignmentHorizontal(HorizontalAlignment); - } - WrapTextFragmentsAndGenerateTextRuns(); - } protected void WrapTextFragmentsAndGenerateTextRuns() @@ -262,7 +285,9 @@ protected void WrapTextFragmentsAndGenerateTextRuns() widthOfLargestLine = Lines.LargestWidthWithoutSpace; combinedHeight = Lines.GetHeightOfCollection(_lsMultiplier, lineSpacingResult); - SetHorizontalAlignment(widthOfLargestLine); + + Bounds.Width = widthOfLargestLine + RightMargin; + //SetHorizontalAlignment(widthOfLargestLine); int lineIdx = 0; foreach (var line in Lines) @@ -294,7 +319,6 @@ protected void WrapTextFragmentsAndGenerateTextRuns() lineIdx++; } } - Bounds.Width = widthOfLargestLine; Bounds.Height = combinedHeight; } @@ -318,11 +342,11 @@ protected double CalculatePrevWidthBasedOnAlignment(double lineDist) protected void SetHorizontalAlignment(double widthOfLargestLine) { - if (HorizontalAlignment == TextAlignment.Center && AutoSize && _centerAdjustment != null && TextIfEmptyIsNull) + 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 = _centerAdjustment.Value - (widthOfLargestLine / 2); + Bounds.Left = GetAlignmentHorizontal(HorizontalAlignment) - (widthOfLargestLine / 2); } else { @@ -332,15 +356,51 @@ protected void SetHorizontalAlignment(double widthOfLargestLine) } } + 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 index 204b101fbe..f487503c84 100644 --- a/src/EPPlus.DrawingRenderer/RenderItems/Textbox/RenderTextBody.cs +++ b/src/EPPlus.DrawingRenderer/RenderItems/Textbox/RenderTextBody.cs @@ -33,16 +33,18 @@ public enum TextAnchoringType public abstract class RenderTextBody : GroupRenderItem { - public RenderTextBody(BoundingBox parent, bool autoSize) + 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(BoundingBox parent, double left, double top, double maxWidth, double maxHeight, bool clampedToParent = false, bool autoSize=false) : this(parent, autoSize) + 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; @@ -52,6 +54,7 @@ public RenderTextBody(BoundingBox parent, double left, double top, double maxWid Bounds.Name = "Textbody"; } + protected RenderContext RenderContext { get; private set; } public List Paragraphs { get; set; } = new List(); public TextAnchoringType VerticalAlignment = TextAnchoringType.Top; @@ -105,6 +108,9 @@ public void AppendRenderItems(List renderItems) //TranslationOffset.Top = Bounds.Top; renderItems.Add(this); + + var titleItem = new TitleRenderItem("TextBody group"); + AddChildItem(titleItem); foreach (var item in Paragraphs) { AddChildItem(item); @@ -125,6 +131,70 @@ public ParagraphRenderItem AddParagraph(string text = null) 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}"; @@ -147,6 +217,7 @@ private void AdjustAndAddParagraph(ParagraphRenderItem paragraph) } } Paragraphs.Add(paragraph); + RecalculateParagraphs(); } private double GetTopToAddNextParagraphAt() @@ -165,7 +236,7 @@ private double GetTopToAddNextParagraphAt() /// Get the start of text space vertically /// /// - protected double GetAlignmentVertical() + public double GetAlignmentVertical() { double alignmentY = 0; @@ -177,10 +248,13 @@ protected double GetAlignmentVertical() //Center means center of a Shape's ENTIRE bounding box height. //Not center of the Inset GetRectangle case TextAnchoringType.Center: - alignmentY = (MaxHeight - Bounds.Height) / 2 + Bounds.Top; + if(AutoSize == false) + { + alignmentY = (Bounds.Height) / 2 - ContentBounds.Height; + } break; case TextAnchoringType.Bottom: - alignmentY = MaxHeight - Bounds.Height; + alignmentY = Bounds.Height - ContentBounds.Height; break; } diff --git a/src/EPPlus.DrawingRenderer/RenderItems/Textbox/RenderTextbox.cs b/src/EPPlus.DrawingRenderer/RenderItems/Textbox/RenderTextbox.cs index 2cbe96fe21..7939b54198 100644 --- a/src/EPPlus.DrawingRenderer/RenderItems/Textbox/RenderTextbox.cs +++ b/src/EPPlus.DrawingRenderer/RenderItems/Textbox/RenderTextbox.cs @@ -15,10 +15,12 @@ public RenderTextbox(BoundingBox parent, double left, double top, double width, public void Init(BoundingBox parent, double maxWidth, double maxHeight) { Parent = parent; - _rectangle = new RectRenderItem(Parent); - //TextBody = new TextBody(Rectangle.Bounds, true); + _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; + //TextBody.MaxHeight = maxHeight; } public RenderTextbox(BoundingBox parent, double maxWidth, double maxHeight) @@ -26,11 +28,13 @@ public RenderTextbox(BoundingBox parent, double maxWidth, double maxHeight) Init(parent, maxWidth, maxHeight); } - //Simplified input - public RenderTextbox(BoundingBox parent, BoundingBox maxBounds) - { - } - RectRenderItem _rectangle =null; + //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 @@ -39,83 +43,118 @@ public RectRenderItem Rectangle _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 virtual RenderTextBody TextBody {get;set;} public double Left { get { - return Rectangle.Bounds.Left; //TextBody.Bounds.Left - LeftMargin; + return _group.Bounds.Left; } set { - //TextBody.Bounds.Left = value + LeftMargin; - Rectangle.Bounds.Left = value; + _group.Bounds.Left = value; } } public double Top { get { - return Rectangle.Bounds.Top; //TextBody.Bounds.Top - TopMargin; + return _group.Bounds.Top; } set { - //TextBody.Bounds.Top = value + TopMargin; - Rectangle.Bounds.Top = value; + _group.Bounds.Top = value; } } public double Width { get { - return LeftMargin + (TextBody?.Bounds?.Width ?? 0D) + RightMargin; + 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; + 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; } } - internal double LeftMargin + public double LeftMargin { - get; set; + get { return _marginGroup.Left; } set { _marginGroup.Left = value; } } - internal double TopMargin + public double TopMargin { - get; set; + get { return _marginGroup.Top; } + set { _marginGroup.Top = value; } } - internal double RightMargin + public double RightMargin { get; set; } - internal double BottomMargin + public double BottomMargin { get; set; } - internal BoundingBox Parent { get; private set; } - internal double Rotation + + internal protected BoundingBox Parent { get; protected set; } + public double Rotation { get { - return Rectangle.Bounds.Rotation; + return _group.Rotation; } set { - Rectangle.Bounds.Rotation = value; + _group.Rotation = value; } } /// /// Gets the actual width of the rotated textbox. /// /// - internal double GetActualWidth() + public double GetActualWidth() { return Width * Math.Abs(Math.Cos(MathHelper.Radians(Rotation))) + Height * Math.Abs(Math.Sin(MathHelper.Radians(Rotation))); } @@ -123,7 +162,7 @@ internal double GetActualWidth() /// Gets the actual right position of the rotated textbox. /// /// - internal double GetActualRight() + public double GetActualRight() { return Left+GetActualWidth(); } @@ -131,7 +170,7 @@ internal double GetActualRight() /// Gets the actual height of the rotated textbox. /// /// - internal double GetActualHeight() + public double GetActualHeight() { return Width * Math.Abs(Math.Sin(MathHelper.Radians(Rotation))) + Height * Math.Abs(Math.Cos(MathHelper.Radians(Rotation))); } @@ -139,19 +178,72 @@ internal double GetActualHeight() /// Gets the actual right position of the rotated textbox. /// /// - internal double GetActualBottom() + public double GetActualBottom() { return Top + GetActualHeight(); } public override void AppendRenderItems(List renderItems) { - renderItems.Add(Rectangle); - TextBody.AppendRenderItems(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. /// - internal eTextAnchor TextAnchor + public eTextAnchor TextAnchor { get; set; diff --git a/src/EPPlus.DrawingRenderer/RenderItems/Textbox/RichTextFormatDrawing/RichTextFormatDrawing.cs b/src/EPPlus.DrawingRenderer/RenderItems/Textbox/RichTextFormatDrawing/RichTextFormatDrawing.cs index 07c5e253e9..cc4df26c5f 100644 --- a/src/EPPlus.DrawingRenderer/RenderItems/Textbox/RichTextFormatDrawing/RichTextFormatDrawing.cs +++ b/src/EPPlus.DrawingRenderer/RenderItems/Textbox/RichTextFormatDrawing/RichTextFormatDrawing.cs @@ -101,5 +101,10 @@ public RichTextFormatDrawing(string text, string fontFamily, float size, bool bo 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 index f9e0793632..0b04d4f66b 100644 --- a/src/EPPlus.DrawingRenderer/RenderItems/Textbox/TextRunRenderItem.cs +++ b/src/EPPlus.DrawingRenderer/RenderItems/Textbox/TextRunRenderItem.cs @@ -1,13 +1,14 @@ 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; -using EPPlusImageRenderer.Utils; namespace EPPlus.Export.ImageRenderer.RenderItems.Shared { @@ -126,7 +127,7 @@ public abstract class TextRunRenderItem : RenderItem protected internal bool _isItalic = false; protected internal bool _isBold = false; protected internal eDrawingUnderLineType _underLineType = eDrawingUnderLineType.None; - protected internal eDrawingStrikeType _strikeType; + protected internal eDrawingStrikeType _strikeType = eDrawingStrikeType.No; protected internal Color _underlineColor; protected internal double _baseline; @@ -153,6 +154,17 @@ 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) diff --git a/src/EPPlus.DrawingRenderer/Svg/IRender.cs b/src/EPPlus.DrawingRenderer/Svg/IRender.cs index b426d81e16..ca3a62b34b 100644 --- a/src/EPPlus.DrawingRenderer/Svg/IRender.cs +++ b/src/EPPlus.DrawingRenderer/Svg/IRender.cs @@ -111,7 +111,7 @@ private void WriteDefsForRenderItem(StringBuilder defSb, HashSet hs, ref var key = item.GradientFill.GetKey(); if (_defsCache.TryGetValue(key, out string? name) == false) { - name = WriteGradient($"Gradient{ix}", defSb, hs, item.GradientFill, item.FillColorSource, true); + name = WriteGradient($"Gradient{ix}", defSb, hs, item.GradientFill, item.FillColorSource); _defsCache[key] = name; } item.FillColor = $"Url(#{name})"; @@ -147,7 +147,7 @@ private void WriteDefsForRenderItem(StringBuilder defSb, HashSet hs, ref var key = item.BorderGradientFill.GetKey(); if (_defsCache.TryGetValue(key, out string? name) == false) { - name = WriteGradient($"StrokeGradient{ix}", defSb, hs, item.BorderGradientFill, item.BorderColorSource, true); + name = WriteGradient($"StrokeGradient{ix}", defSb, hs, item.BorderGradientFill, item.BorderColorSource); _defsCache[key] = name; } item.BorderColor = $"Url(#{name})"; @@ -249,11 +249,11 @@ private string WriteBlip(string namePrefix, StringBuilder defSb, HashSet defSb.Append($""); return name; } - private string WriteGradient(string namePrefix, StringBuilder defSb, HashSet hs, RenderGradientFill gradientFill, PathFillMode fillMode, bool userSpaceOnUse) + private string WriteGradient(string namePrefix, StringBuilder defSb, HashSet hs, RenderGradientFill gradientFill, PathFillMode fillMode) { //var gs = gradientFill.Settings; var name = $"{namePrefix}{fillMode}"; - var grUnits = userSpaceOnUse ? " gradientUnits=\"userSpaceOnUse\"" : ""; + var grUnits = gradientFill.UserSpaceOnUse ? " gradientUnits=\"userSpaceOnUse\"" : ""; if (gradientFill.ShadePath == ShadePath.Linear && hs.Contains(name) == false) { hs.Add(name); @@ -607,7 +607,7 @@ private string SetStretchTileProps(RenderBlipFill blipFill) 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.ToString(CultureInfo.InvariantCulture)}\" height=\"{height.ToString(CultureInfo.InvariantCulture)}\" "; + 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)) diff --git a/src/EPPlus.DrawingRenderer/Svg/SvgGroupRenderer.cs b/src/EPPlus.DrawingRenderer/Svg/SvgGroupRenderer.cs index d2216dd0f3..8f416409d6 100644 --- a/src/EPPlus.DrawingRenderer/Svg/SvgGroupRenderer.cs +++ b/src/EPPlus.DrawingRenderer/Svg/SvgGroupRenderer.cs @@ -20,7 +20,16 @@ public override void Render(GroupRenderItem item) { string combinedTransform = GetCombinedTransformString(item); - OutputStream.Append($""); + //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) { diff --git a/src/EPPlus.DrawingRenderer/Svg/SvgTextRunRenderer.cs b/src/EPPlus.DrawingRenderer/Svg/SvgTextRunRenderer.cs index 9078ceddc2..d1c9783ae8 100644 --- a/src/EPPlus.DrawingRenderer/Svg/SvgTextRunRenderer.cs +++ b/src/EPPlus.DrawingRenderer/Svg/SvgTextRunRenderer.cs @@ -1,4 +1,5 @@ using EPPlus.DrawingRenderer.RenderItems; +using EPPlus.DrawingRenderer.RenderItems.SvgItem; using EPPlus.Export.ImageRenderer.RenderItems; using EPPlus.Export.ImageRenderer.RenderItems.Shared; using EPPlus.Fonts.OpenType.Utils; @@ -18,6 +19,7 @@ public SvgTextRunRenderer(StringBuilder outputStream) : base(outputStream) { } + string GetFontStyleAttributes(TextRunRenderItem textRun) { string fontStyleAttributes = " "; @@ -166,11 +168,20 @@ public override void Render(TextRunRenderItem textRun) var textRunString = sb.ToString(); //Wrap in another tspan to apply underline color if neccesary - if(string.IsNullOrEmpty(UnderlineColorString) == false) + if (string.IsNullOrEmpty(UnderlineColorString) == false) { textRunString = string.Format(UnderlineColorString, textRunString); } + if (textRun is SvgTextRunRenderItem) + { + var tr = (SvgTextRunRenderItem)textRun; + if (tr.RenderTextNode) + { + textRunString = $"{textRunString}"; + } + } + OutputStream.Append(textRunString); UnderlineColorString = string.Empty; } diff --git a/src/EPPlus.Fonts.OpenType.Benchmarks/RichTextBenchmarks.cs b/src/EPPlus.Fonts.OpenType.Benchmarks/RichTextBenchmarks.cs index 3f4dc9003f..f1181423e7 100644 --- a/src/EPPlus.Fonts.OpenType.Benchmarks/RichTextBenchmarks.cs +++ b/src/EPPlus.Fonts.OpenType.Benchmarks/RichTextBenchmarks.cs @@ -77,7 +77,7 @@ public void Setup() font.FullName, font.SubFamily, font.GlyfTable.Glyphs.Count)); var shaper = new TextShaper(fontEngine, font); - _layoutEngine = new TextLayoutEngine(shaper); + _layoutEngine = new TextLayoutEngine(fontEngine, shaper); Console.WriteLine("\nPre-warming font cache (Regular, Bold, Italic)..."); PrewarmFontCache(); diff --git a/src/EPPlus.Fonts.OpenType.Tests/EPPlus.Fonts.OpenType.Tests.csproj b/src/EPPlus.Fonts.OpenType.Tests/EPPlus.Fonts.OpenType.Tests.csproj index 6823f6fbcd..5df411d299 100644 --- a/src/EPPlus.Fonts.OpenType.Tests/EPPlus.Fonts.OpenType.Tests.csproj +++ b/src/EPPlus.Fonts.OpenType.Tests/EPPlus.Fonts.OpenType.Tests.csproj @@ -56,6 +56,9 @@ PreserveNewest + + PreserveNewest + PreserveNewest @@ -65,6 +68,13 @@ PreserveNewest + + PreserveNewest + + + + + diff --git a/src/EPPlus.Fonts.OpenType.Tests/FallbackFonts/FontProviderTests.cs b/src/EPPlus.Fonts.OpenType.Tests/FallbackFonts/FontProviderTests.cs index 6f8f1fd93d..b713ae797c 100644 --- a/src/EPPlus.Fonts.OpenType.Tests/FallbackFonts/FontProviderTests.cs +++ b/src/EPPlus.Fonts.OpenType.Tests/FallbackFonts/FontProviderTests.cs @@ -79,6 +79,42 @@ public void DefaultFontProvider_MixedTextAndEmoji_ShouldUseMultipleFonts() Assert.AreNotEqual(_robotoFont, usedFonts[1], "Second font should be emoji fallback"); } + [TestMethod] + public void DefaultFontProvider_DefaultBehavior_DoesNotThrowOnFallback() + { + var engine = new OpenTypeFontEngine(cfg => + { + foreach (var folder in FontFolders) + cfg.FontDirectories.Add(folder); + cfg.SearchSystemDirectories = false; + cfg.SetScriptFallback(UnicodeScript.Latin, "Archivo Narrow"); + }); + + // Default behaviour: rendering trusts the fallback chain and does not throw, + // even when the requested font resolves only via the embedded fallback. + // (RequireExactFont defaults to false.) + + var shaper = engine.GetTextShaper("Archivo Narrow", FontSubFamily.Regular); + + var shaped = shaper.Shape("Hello There World"); + var usedFonts = shaper.GetUsedFonts().ToList(); + } + + [TestMethod] + public void DefaultFontProvider_EnsureLastFallbackThrowsWhenExactRequired() + { + var engine = new OpenTypeFontEngine(cfg => + { + foreach (var folder in FontFolders) + cfg.FontDirectories.Add(folder); + cfg.SearchSystemDirectories = false; + cfg.SetScriptFallback(UnicodeScript.Latin, "Archivo Narrow"); + }); + + engine.RequireExactFont = true; + Assert.ThrowsExactly(() => { engine.GetTextShaper("NonExistentFontFamily12345", FontSubFamily.Regular); }); + } + [TestMethod] public void TextShaper_SurrogatePair_ShouldMapToSingleGlyph() { diff --git a/src/EPPlus.Fonts.OpenType.Tests/FontResolver/DefaultFontResolverTests.cs b/src/EPPlus.Fonts.OpenType.Tests/FontResolver/DefaultFontResolverTests.cs index 3c57c2ce88..b568398623 100644 --- a/src/EPPlus.Fonts.OpenType.Tests/FontResolver/DefaultFontResolverTests.cs +++ b/src/EPPlus.Fonts.OpenType.Tests/FontResolver/DefaultFontResolverTests.cs @@ -218,6 +218,22 @@ public void ResolveFont_BuiltinChainExistsButNothingInstalled_FallsBackToArchivo #endregion + #region GetFontAvailability + + [TestMethod] + public void ResolveFont_ShouldResolveArchivoNarrowBold() + { + var scanner = new FakeFontScanner(); + var reader = new FakeFontFileReader(); + + var resolver = new DefaultFontResolver(scanner: scanner, fileReader: reader); + + var availability = resolver.GetFontAvailability("Archivo Narrow", FontSubFamily.Bold); + Assert.AreEqual(FontAvailability.Exact, availability); + } + + #endregion + #region Built-in chain integrity [TestMethod] diff --git a/src/EPPlus.Fonts.OpenType.Tests/Fonts/Roboto-ExtraLight.ttf b/src/EPPlus.Fonts.OpenType.Tests/Fonts/Roboto-ExtraLight.ttf new file mode 100644 index 0000000000..5e517b3baf Binary files /dev/null and b/src/EPPlus.Fonts.OpenType.Tests/Fonts/Roboto-ExtraLight.ttf differ diff --git a/src/EPPlus.Fonts.OpenType.Tests/Fonts/VariableFonts/ArchivoNarrow-VariableFont_wght.ttf b/src/EPPlus.Fonts.OpenType.Tests/Fonts/VariableFonts/ArchivoNarrow-VariableFont_wght.ttf new file mode 100644 index 0000000000..40f3c644f8 Binary files /dev/null and b/src/EPPlus.Fonts.OpenType.Tests/Fonts/VariableFonts/ArchivoNarrow-VariableFont_wght.ttf differ diff --git a/src/EPPlus.Fonts.OpenType.Tests/Integration/EnsureWrappingWithFreeFonts.cs b/src/EPPlus.Fonts.OpenType.Tests/Integration/EnsureWrappingWithFreeFonts.cs index f46748e66b..595896c5df 100644 --- a/src/EPPlus.Fonts.OpenType.Tests/Integration/EnsureWrappingWithFreeFonts.cs +++ b/src/EPPlus.Fonts.OpenType.Tests/Integration/EnsureWrappingWithFreeFonts.cs @@ -104,8 +104,6 @@ public void TestWrappingLoremIpsum20Paragraphs() cfg.SearchSystemDirectories = false; })).Value; - tEngine.LeastRequiredAvailability = FontAvailability.NotFound; - var tle = tEngine.GetTextLayoutEngineForFont(mf); var maxWidth = 54.1420d; diff --git a/src/EPPlus.Fonts.OpenType.Tests/Integration/LayoutSystemTests.cs b/src/EPPlus.Fonts.OpenType.Tests/Integration/LayoutSystemTests.cs index 68e01cc160..cfc24d12d4 100644 --- a/src/EPPlus.Fonts.OpenType.Tests/Integration/LayoutSystemTests.cs +++ b/src/EPPlus.Fonts.OpenType.Tests/Integration/LayoutSystemTests.cs @@ -45,7 +45,7 @@ public void TestParagraphs() fragments.Add(currentFrag); } - var paragraph = new LayoutSystem(fragments); + var paragraph = new LayoutSystem(SystemFontsEngine, fragments); var styleRuns = paragraph.GetTextOfAllTextRuns(); Assert.AreEqual(lstOfRichText[0], styleRuns[0]); @@ -84,7 +84,7 @@ public void TestLayoutSystemParagraphChars() new TextFragment() {Text = lstOfRichText[0], Font = font } }; - var layout = new LayoutSystem(fragments); + var layout = new LayoutSystem(SystemFontsEngine, fragments); Assert.AreEqual(3, layout.GetParagraphSeparatorCount()); } @@ -133,7 +133,7 @@ public void TestParagraphs_DifficultCase() var maxSizePoints = Math.Round(300d, 0, MidpointRounding.AwayFromZero).PixelToPoint(); - var paragraph = new LayoutSystem(fragments); + var paragraph = new LayoutSystem(SystemFontsEngine, fragments); var wrappedLines = paragraph.Wrap(225d); var line1 = wrappedLines[0]; @@ -167,7 +167,7 @@ public void EnsureCorrectTotalIndex() fragments.Add(currentFrag); } - var paragraph = new LayoutSystem(fragments); + var paragraph = new LayoutSystem(SystemFontsEngine, fragments); var wrappedLines = paragraph.Wrap(225d); Assert.AreEqual("StrikeGoudy size", wrappedLines[1].Text); @@ -203,12 +203,12 @@ public void EnsureRTCharIdxBecomesCorrectWhenBreaking() fragments.Add(currentFrag); } - var shaper = OpenTypeFonts.GetShaperForFont(font2); + var shaper = TestFolderEngine.GetShaperForFont(font2); //var shapes = shaper.ShapeLight("WithAbsolutelyNoSpacesAtAllJustToBeDifficult"); - var layout = new TextLayoutEngine(shaper); + var layout = new TextLayoutEngine(TestFolderEngine, shaper); var wrappedLines = layout.WrapRichTextLines(fragments, 225d); - var paragraph = new LayoutSystem(fragments); + var paragraph = new LayoutSystem(TestFolderEngine, fragments); var wrappedLines2 = paragraph.Wrap(225d); //var layout = OpenTypeFonts.GetTextLayoutEngineForFont(font, FontFolders); //var wrappedLines = layout.WrapRichTextLines(fragments, 225d); @@ -249,7 +249,7 @@ public void EnsureWrappingSimplePlainTextCorrectly() fragments.Add(currentFrag); } - var paragraph = new LayoutSystem(fragments); + var paragraph = new LayoutSystem(TestFolderEngine, fragments); var lines = paragraph.Wrap(92.976377953d); @@ -274,7 +274,7 @@ public void EnsureWrappingRichTextAndGettingLineSpacing() List rtLst = new List() { rt, rtSecond, rtThird}; - var paragraph = new LayoutSystem(rtLst); + var paragraph = new LayoutSystem(TestFolderEngine, rtLst); var lines = paragraph.Wrap(92.976377953d); @@ -313,7 +313,7 @@ public void TestGetSection() List rtLst = new List() { rt, rtSecond, rtThird }; - var paragraph = new LayoutSystem(rtLst); + var paragraph = new LayoutSystem(TestFolderEngine, rtLst); var fulltext = paragraph.GetTextOfAllTextRuns(); @@ -337,7 +337,7 @@ public void TestGetSectionWithIndividualChars() List rtLst = new List() { rt, rtSecond, rtThird, rtFourth }; - var pIndividual = new LayoutSystem(rtLst); + var pIndividual = new LayoutSystem(TestFolderEngine, rtLst); var joinedInput = string.Join("", txtLst.ToArray()); var joinedOutput = string.Join("", pIndividual.GetTextOfAllTextRuns().ToArray()); @@ -356,7 +356,7 @@ public void TestGetSectionMixed() var rtFourth = new RichTextFormatBase(txtLstMixed[3], "Archivo Narrow", 18f); var rtLstMixed = new List() { rt, rtSecond, rtThird, rtFourth }; - var pMixed = new LayoutSystem(rtLstMixed); + var pMixed = new LayoutSystem(TestFolderEngine, rtLstMixed); var InMix = string.Join("", txtLstMixed.ToArray()); var OutMix = string.Join("", pMixed.GetTextOfAllTextRuns().ToArray()); @@ -371,7 +371,7 @@ public void TestGetSectionNumber2() var rt = new RichTextFormatBase(txtLstMixed[0], "Roboto", 12f); var rtLstMixed = new List() { rt }; - var pMixed = new LayoutSystem(rtLstMixed); + var pMixed = new LayoutSystem(TestFolderEngine, rtLstMixed); var InMix = string.Join("", txtLstMixed.ToArray()); var OutMix = string.Join("", pMixed.GetTextOfAllTextRuns().ToArray()); @@ -382,33 +382,34 @@ public void TestGetSectionNumber2() } [TestMethod] - public void TestSimpleRichText2() + public void TestLayoutSystemMultipleParagraphs() { - //var rtCollection = new RichTextCollectionBase(); - //var someTextRt = rtCollection.Add("SomeText", true); - //var richRt = rtCollection.Add("rich"); - //var richerRt = rtCollection.Add("richer"); - //var richestRt = rtCollection.Add("richest"); - //var wealthyRt = rtCollection.Add("Wealthy"); + var paragraphEndSymbol = '\u2029'; - //richRt.Info.FontFamily = "Roboto"; - //richRt.Info.Italic = true; - - //richerRt.Info.FontFamily = "Roboto"; - //richerRt.Info.Size = 16; - //richerRt.Info.Italic = true; - //richerRt.Info.UnderlineType = (int)ExcelUnderLineType.Single; - - //richestRt.Info.FontFamily = "Oi"; - //richestRt.Info.FontColor = Color.BlueViolet; - //richestRt.Info.Bold = true; - //richestRt.Info.Italic = true; - //richerRt.Info.Size = 18; + List lstOfRichText = new() { $"Here comes lorem ipsum{paragraphEndSymbol} " + + $"Sed ut perspiciatis,{paragraphEndSymbol}", $"u{paragraphEndSymbol}n{paragraphEndSymbol}de{paragraphEndSymbol} omnis" }; + var font = new FontFormatBase() + { + Family = "Aptos Narrow", + Size = 11, + SubFamily = FontSubFamily.Bold + }; + var font2 = new FontFormatBase() + { + Family = "Aptos Narrow", + Size = 11, + SubFamily = FontSubFamily.Italic + }; - //someTextRt.FontData.Family = "Archivo Narrow"; + var fragments = new List() + { + new TextFragment() {Text = lstOfRichText[0], Font = font }, + new TextFragment() {Text = lstOfRichText[1], Font = font2 } + }; - //richRt + var layout = new LayoutSystem(SystemFontsEngine, fragments); + Assert.AreEqual(5, layout.GetParagraphSeparatorCount()); } } } diff --git a/src/EPPlus.Fonts.OpenType.Tests/Integration/TextLayoutEngineTests.cs b/src/EPPlus.Fonts.OpenType.Tests/Integration/TextLayoutEngineTests.cs index 62758d02ef..039e2a368f 100644 --- a/src/EPPlus.Fonts.OpenType.Tests/Integration/TextLayoutEngineTests.cs +++ b/src/EPPlus.Fonts.OpenType.Tests/Integration/TextLayoutEngineTests.cs @@ -23,7 +23,7 @@ public void WrapText_ShortText_NoWrapping() // Arrange var font = SystemFontsEngine.LoadFont("Calibri", FontSubFamily.Regular); var shaper = SystemFontsEngine.GetTextShaper("Calibri"); - var layout = new TextLayoutEngine(shaper); + var layout = new TextLayoutEngine(SystemFontsEngine, shaper); // Act var lines = layout.WrapText("Hello", 11f, 1000); @@ -40,7 +40,7 @@ public void WrapText_LongText_WrapsAtSpaces() // Arrange var font = SystemFontsEngine.LoadFont("Calibri", FontSubFamily.Regular); var shaper = SystemFontsEngine.GetTextShaper("Calibri"); - var layout = new TextLayoutEngine(shaper); + var layout = new TextLayoutEngine(SystemFontsEngine, shaper); // Act - narrow width forces wrapping var lines = layout.WrapText("Hello world test", 11f, 50); @@ -63,7 +63,7 @@ public void WrapText_WithLineBreaks_PreservesBreaks() // Arrange var font = SystemFontsEngine.LoadFont("Calibri", FontSubFamily.Regular); var shaper = new TextShaper(SystemFontsEngine, font); - var layout = new TextLayoutEngine(shaper); + var layout = new TextLayoutEngine(SystemFontsEngine, shaper); // Act var lines = layout.WrapText("Line 1\r\nLine 2\nLine 3", 11f, 1000); @@ -89,7 +89,7 @@ public void WrapText_TestWhenOnExactWrapPlusSpaces2() var maxWidthPoints = 54d; ITextShaper shaper = SystemFontsEngine.GetTextShaper("Aptos Narrow"); - using var layoutEngine = new TextLayoutEngine(shaper); + using var layoutEngine = new TextLayoutEngine(SystemFontsEngine, shaper); var wrappedLines = layoutEngine.WrapText( text, 11f, @@ -111,7 +111,7 @@ public void WrapText_TestWhenOnExactWrap() var comparison = new List() { "nulla", "efficitur", "commodo", "sit amet non", "lacus. Proin", "viverra enim" }; ITextShaper shaper = SystemFontsEngine.GetTextShaper("Aptos Narrow"); - using var layoutEngine = new TextLayoutEngine(shaper); + using var layoutEngine = new TextLayoutEngine(SystemFontsEngine, shaper); var wrappedLines = layoutEngine.WrapText( text, 11f, @@ -134,7 +134,7 @@ public void WrapText_TestFragments() var savedStrings = SavedComparisonString.Split("\r\n"); ITextShaper shaper = new TextShaper(SystemFontsEngine, font); - using var layoutEngine = new TextLayoutEngine(shaper); + using var layoutEngine = new TextLayoutEngine(SystemFontsEngine, shaper); var wrappedLines = layoutEngine.WrapText( Lorem20Str, 11f, @@ -174,7 +174,7 @@ public void WrapText_WithPreExistingWidth_AccountsForIt() // Arrange var font = SystemFontsEngine.LoadFont("Calibri", FontSubFamily.Regular); var shaper = SystemFontsEngine.GetTextShaper("Calibri"); - var layout = new TextLayoutEngine(shaper); + var layout = new TextLayoutEngine(SystemFontsEngine, shaper); // Measure "Hello " to get its width var testShaper = SystemFontsEngine.GetTextShaper("Calibri"); @@ -196,7 +196,7 @@ public void WrapText_EmptyString_ReturnsEmptyLine() // Arrange var font = SystemFontsEngine.LoadFont("Calibri", FontSubFamily.Regular); var shaper = SystemFontsEngine.GetTextShaper("Calibri"); - var layout = new TextLayoutEngine(shaper); + var layout = new TextLayoutEngine(SystemFontsEngine, shaper); // Act var lines = layout.WrapText("", 11f, 1000); @@ -212,7 +212,7 @@ public void WrapText_WithKerning_MeasuresCorrectly() // Arrange var font = TestFolderEngine.LoadFont("Roboto", FontSubFamily.Regular); var shaper = TestFolderEngine.GetTextShaper("Roboto"); - var layout = new TextLayoutEngine(shaper); + var layout = new TextLayoutEngine(TestFolderEngine, shaper); // Act - "AV" has kerning in Roboto var withKerning = layout.WrapText("AV", 11f, 1000, ShapingOptions.Default); @@ -244,7 +244,7 @@ public void MyVeryGoodRichTextWrapper() //Text containing emoji var inputText = "My long and 😝😱 bothersome 😝😱 text"; var shapedText = (ShapedText)shaper.Shape(inputText); - var layout = new TextLayoutEngine(shaper); + var layout = new TextLayoutEngine(SystemFontsEngine, shaper); var text = layout.WrapText(inputText, 12, 20); @@ -262,7 +262,7 @@ public void WrapRichText_SingleFragment_BehavesLikeSingleFont() // Arrange var font = SystemFontsEngine.LoadFont("Calibri", FontSubFamily.Regular); var shaper = SystemFontsEngine.GetTextShaper("Calibri"); - var layout = new TextLayoutEngine(shaper); + var layout = new TextLayoutEngine(SystemFontsEngine, shaper); var fragments = new List { @@ -290,7 +290,7 @@ public void WrapRichText_MultipleFragments_ConcatenatesCorrectly() // Arrange var font = SystemFontsEngine.LoadFont("Calibri", FontSubFamily.Regular); var shaper = SystemFontsEngine.GetTextShaper("Calibri"); - var layout = new TextLayoutEngine(shaper); + var layout = new TextLayoutEngine(SystemFontsEngine, shaper); var fragments = new List { @@ -321,7 +321,7 @@ public void WrapRichText_DifferentFonts_WrapsCorrectly() // Arrange var font = SystemFontsEngine.LoadFont("Calibri", FontSubFamily.Regular); var shaper = new TextShaper(SystemFontsEngine, font); - var layout = new TextLayoutEngine(shaper); + var layout = new TextLayoutEngine(SystemFontsEngine, shaper); var fragments = new List { @@ -392,8 +392,8 @@ public void WrapLongRichTextWord() var fragment = new TextFragment() { Text = longWord, Font = mFont }; var fragLst = new List() { fragment }; - ITextShaper shaper = OpenTypeFonts.GetShaperForFont(mFont); - using var layout = new TextLayoutEngine(shaper); + ITextShaper shaper = SystemFontsEngine.GetShaperForFont(mFont); + using var layout = new TextLayoutEngine(SystemFontsEngine, shaper); var wrappedLines = layout.WrapRichText(fragLst, 54); @@ -452,7 +452,7 @@ public void WrapRichTextDifficultCase() lap = sw.ElapsedMilliseconds; - var layout = new TextLayoutEngine(shaper); + var layout = new TextLayoutEngine(SystemFontsEngine, shaper); lap = sw.ElapsedMilliseconds; @@ -590,7 +590,7 @@ public void EnsureRichTextLineWrappingSameAsNonRichWhenNoWrap() var goudyFont = SystemFontsEngine.LoadFont(font22.Family, font22.SubFamily); ITextShaper shaper2 = new TextShaper(SystemFontsEngine, font); - using var layoutEngine = new TextLayoutEngine(shaper); + using var layoutEngine = new TextLayoutEngine(SystemFontsEngine, shaper); var wrappedLines = layoutEngine.WrapRichTextLines(comparatorFragments, 225d); Assert.AreEqual(pointsTotal, wrappedLines[0].Width); @@ -642,7 +642,7 @@ public void EnsureRichTextLineWrappingSameAsNonRichWhenNoWrapAndSpaceTrail() var goudyFont = SystemFontsEngine.LoadFont(font22.Family, font22.SubFamily); ITextShaper shaper2 = new TextShaper(SystemFontsEngine, font); - using var layoutEngine = new TextLayoutEngine(shaper); + using var layoutEngine = new TextLayoutEngine(SystemFontsEngine, shaper); var wrappedLines = layoutEngine.WrapRichTextLines(comparatorFragments, 225d); Assert.AreEqual(pointsTotal, wrappedLines[0].Width); @@ -703,7 +703,7 @@ public void EnsureLineFragmentsAreMeasuredCorrectlyWhenWrapping() var startFont = SystemFontsEngine.LoadFont(font2.FontFamily, GetFontSubType(font2.Style)); var shaper = new TextShaper(SystemFontsEngine, startFont); - var layout = new TextLayoutEngine(shaper); + var layout = new TextLayoutEngine(SystemFontsEngine, shaper); var wrappedLines = layout.WrapRichTextLines(fragments, maxSizePoints); @@ -765,7 +765,7 @@ public void TestParagraphs() GenerateTextFragments(lstOfRichText, fonts, ref fragments); - var paragraph = new LayoutSystem(fragments); + var paragraph = new LayoutSystem(SystemFontsEngine, fragments); var styleRuns = paragraph.GetTextOfAllTextRuns(); Assert.AreEqual(lstOfRichText[0], styleRuns[0]); @@ -804,7 +804,7 @@ public void TestLayoutSystemParagraphChars() new TextFragment() {Text = lstOfRichText[0], Font = font } }; - var layout = new LayoutSystem(fragments); + var layout = new LayoutSystem(SystemFontsEngine, fragments); Assert.AreEqual(3, layout.GetParagraphSeparatorCount()); } @@ -848,7 +848,7 @@ public void TestParagraphs_DifficultCase() var maxSizePoints = Math.Round(300d, 0, MidpointRounding.AwayFromZero).PixelToPoint(); - var paragraph = new LayoutSystem(fragments); + var paragraph = new LayoutSystem(SystemFontsEngine, fragments); var wrappedLines = paragraph.Wrap(225d); var line1 = wrappedLines[0]; @@ -877,7 +877,7 @@ public void EnsureCorrectTotalIndex() GenerateTextFragments(lstOfRichText, fonts, ref fragments); - var paragraph = new LayoutSystem(fragments); + var paragraph = new LayoutSystem(SystemFontsEngine, fragments); var wrappedLines = paragraph.Wrap(225d); Assert.AreEqual("StrikeGoudy size", wrappedLines[1].Text); @@ -908,9 +908,9 @@ public void EnsureRTCharIdxBecomesCorrectWhenBreaking() GenerateTextFragments(lstOfRichText, fonts, ref fragments); - var paragraph = new LayoutSystem(fragments); + var paragraph = new LayoutSystem(SystemFontsEngine, fragments); - var layout = OpenTypeFonts.GetTextLayoutEngineForFont(font); + var layout = SystemFontsEngine.GetTextLayoutEngineForFont(font); var wrappedLines = layout.WrapRichTextLines(fragments, 225d); Assert.AreEqual(5, wrappedLines[1].LineFragments[0].StartRtIdx); @@ -980,7 +980,7 @@ public void WrapRichTextDifficultCaseCompare() var startFont = SystemFontsEngine.LoadFont(font1.FontFamily, GetFontSubType(font1.Style)); var shaper = new TextShaper(SystemFontsEngine, startFont); - var layout = new TextLayoutEngine(shaper); + var layout = new TextLayoutEngine(SystemFontsEngine, shaper); var wrappedLines = layout.WrapRichTextLines(fragments, maxSizePoints); var measurer = SystemFontsEngine.GetTextLayoutEngineForFont(font1); @@ -1033,7 +1033,7 @@ public void WrapRichText_WordSpanningFragments_MeasuresCorrectly() // Arrange var font = SystemFontsEngine.LoadFont("Calibri", FontSubFamily.Regular); var shaper = new TextShaper(SystemFontsEngine, font); - var layout = new TextLayoutEngine(shaper); + var layout = new TextLayoutEngine(SystemFontsEngine, shaper); // "Hello" split across two fragments with different fonts var fragments = new List @@ -1066,7 +1066,7 @@ public void WrapRichText_WithLineBreaks_PreservesBreaks() // Arrange var font = SystemFontsEngine.LoadFont("Calibri", FontSubFamily.Regular); var shaper = new TextShaper(SystemFontsEngine, font); - var layout = new TextLayoutEngine(shaper); + var layout = new TextLayoutEngine(SystemFontsEngine, shaper); var fragments = new List { @@ -1100,7 +1100,7 @@ public void WrapRichText_EmptyFragments_HandlesGracefully() // Arrange var font = SystemFontsEngine.LoadFont("Calibri", FontSubFamily.Regular); var shaper = new TextShaper(SystemFontsEngine, font); - var layout = new TextLayoutEngine(shaper); + var layout = new TextLayoutEngine(SystemFontsEngine, shaper); var fragments = new List { @@ -1137,7 +1137,7 @@ public void WrapRichText_NullFragmentList_ReturnsEmptyLine() // Arrange var font = SystemFontsEngine.LoadFont("Calibri", FontSubFamily.Regular); var shaper = new TextShaper(SystemFontsEngine, font); - var layout = new TextLayoutEngine(shaper); + var layout = new TextLayoutEngine(SystemFontsEngine, shaper); // Act var lines = layout.WrapRichText(null, 1000); @@ -1200,7 +1200,7 @@ public void WrapText_Continous_Long_Word() var longWord = "pellentesquer"; ITextShaper shaper = new TextShaper(SystemFontsEngine, font); - using var layoutEngine = new TextLayoutEngine(shaper); + using var layoutEngine = new TextLayoutEngine(SystemFontsEngine, shaper); var wrappedLines = layoutEngine.WrapText( longWord, 11f, @@ -1220,7 +1220,7 @@ public void WrapRichText_MeasureCorrectly() // Arrange var font = SystemFontsEngine.LoadFont("Aptos Narrow", FontSubFamily.Regular); var shaper = new TextShaper(SystemFontsEngine,font); - var layout = new TextLayoutEngine(shaper); + var layout = new TextLayoutEngine(SystemFontsEngine, shaper); var fragments = new List { @@ -1257,7 +1257,7 @@ public void VerifyWrappingSingleChar() var maxWidthPt = 31.8125234375d; var gottenFont = SystemFontsEngine.LoadFont("Aptos Narrow", FontSubFamily.Regular); var shaper = new TextShaper(SystemFontsEngine, gottenFont); - var layout = new TextLayoutEngine(shaper); + var layout = new TextLayoutEngine(SystemFontsEngine, shaper); List fragments = new List() { new TextFragment() { Font = font1, Text = lstOfRichText[0] } }; diff --git a/src/EPPlus.Fonts.OpenType.Tests/Reading/TtfReadingTests.cs b/src/EPPlus.Fonts.OpenType.Tests/Reading/TtfReadingTests.cs index 0b7ff81efd..511dff8e7d 100644 --- a/src/EPPlus.Fonts.OpenType.Tests/Reading/TtfReadingTests.cs +++ b/src/EPPlus.Fonts.OpenType.Tests/Reading/TtfReadingTests.cs @@ -117,7 +117,7 @@ public void ReadSixFonts() OpenTypeFont? calibri = SystemFontsEngine.LoadFont("Calibri", FontSubFamily.Italic); OpenTypeFont? aptos = SystemFontsEngine.LoadFont("Aptos Narrow", FontSubFamily.Bold); OpenTypeFont? timesNewRoman = SystemFontsEngine.LoadFont("Times New Roman", FontSubFamily.Regular); - OpenTypeFont? SS3 = TestFolderEngine.LoadFont("Source Sans 3", FontSubFamily.Bold); + OpenTypeFont? SS3 = TestFolderEngine.LoadFont("Source Sans 3", FontSubFamily.Regular); Assert.IsNotNull(gothic); Assert.AreEqual("BIZ UDGothic Bold", gothic.FullName); @@ -242,7 +242,7 @@ public void TestWrapText() Style = MeasurementFontStyles.Regular }; - var fontMeasurer = OpenTypeFonts.GetTextLayoutEngineForFont(mf); + var fontMeasurer = SystemFontsEngine.GetTextLayoutEngineForFont(mf); var strings = fontMeasurer.WrapText(testStr, mf.Size, MaxPixelWidth); Assert.AreEqual("hello the", strings[0]); diff --git a/src/EPPlus.Fonts.OpenType.Tests/Subsetting/BasicSubsettingTests.cs b/src/EPPlus.Fonts.OpenType.Tests/Subsetting/BasicSubsettingTests.cs index 7055054c53..c75f92cad8 100644 --- a/src/EPPlus.Fonts.OpenType.Tests/Subsetting/BasicSubsettingTests.cs +++ b/src/EPPlus.Fonts.OpenType.Tests/Subsetting/BasicSubsettingTests.cs @@ -299,7 +299,8 @@ public void Subset_Ligatures_ShouldStillWork() [TestMethod] public void Subset_WithGposKerning_ShouldPreservePositioning() { - var font = OpenTypeFonts.LoadFont("Roboto Extra Light"); + var font = TestFolderEngine.LoadFont("Roboto Extra Light"); + //var font = OpenTypeFonts.LoadFont("Roboto Extra Light"); var chars = new[] { 'f', 'e', 'c', 'd', 'g', 'E', 'a', 'b', ' ' }; bool foundF_Original = font.CmapTable.TryGetGlyphId('f', out ushort fGlyphOrig); diff --git a/src/EPPlus.Fonts.OpenType.Tests/TextShaping/TextShaperTests.cs b/src/EPPlus.Fonts.OpenType.Tests/TextShaping/TextShaperTests.cs index 464f72f9b0..80382fce9d 100644 --- a/src/EPPlus.Fonts.OpenType.Tests/TextShaping/TextShaperTests.cs +++ b/src/EPPlus.Fonts.OpenType.Tests/TextShaping/TextShaperTests.cs @@ -685,8 +685,6 @@ public void Shape_PrecomposedVsDecomposed_SimilarWidth() [TestMethod] public void Shape_SourceSans3_SingleMark_PositionsCorrectly() { - TestFolderEngine.LeastRequiredAvailability = FontAvailability.NotFound; - // Arrange var shaper = TestFolderEngine.GetTextShaper("SourceSans3"); @@ -723,7 +721,6 @@ public void Shape_Cafe_HandlesDecomposed() cfg.SearchSystemDirectories = false; })).Value; - tEngine.LeastRequiredAvailability = FontAvailability.NotFound; // Arrange var shaper = tEngine.GetTextShaper("SourceSans3"); diff --git a/src/EPPlus.Fonts.OpenType.Tests/VariableFonts/VariableFontMatchingTests .cs b/src/EPPlus.Fonts.OpenType.Tests/VariableFonts/VariableFontMatchingTests .cs new file mode 100644 index 0000000000..dfcad40886 --- /dev/null +++ b/src/EPPlus.Fonts.OpenType.Tests/VariableFonts/VariableFontMatchingTests .cs @@ -0,0 +1,132 @@ +/************************************************************************************************* + 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 + ************************************************************************************************* + 06/25/2026 EPPlus Software AB Variable font matching tests + *************************************************************************************************/ +using EPPlus.Fonts.OpenType.Scanner; +using OfficeOpenXml.Interfaces.Fonts; +using System.Collections.Generic; +using System.IO; +using System.Linq; + +namespace EPPlus.Fonts.OpenType.Tests.VariableFonts +{ + /// + /// Verifies that the font scanner treats variable fonts as capable of delivering only + /// their default named instance. A variable font must not masquerade as an exact match + /// for a non-default subfamily. + /// + /// Regression context: a developer had Archivo Narrow installed as a wght-axis variable + /// web font. The scanner returned that file for a Bold request, the font library then read + /// the default (Regular) instance, and the developer wrote asserts against those Regular + /// values. On machines without the variable font installed, resolution fell back to the + /// embedded static Archivo Narrow Bold and the asserts failed — a non-deterministic, + /// machine-dependent test failure. + /// + /// These tests are deliberately written against FontScannerV2 directly (not through an + /// engine) so they exercise exactly the matching logic that changed, with no dependency on + /// system-installed fonts and no interference from the Archivo Narrow special-case in + /// DefaultFontResolver. + /// + [TestClass] + public class VariableFontMatchingTests : FontTestBase + { + public override TestContext? TestContext { get; set; } + + // Variable (wght-axis) build of Archivo Narrow. Its default instance is Regular (wght 400). + private const string VariableFontFamily = "Archivo Narrow"; + private const string VariableFontFileName = "ArchivoNarrow-VariableFont_wght.ttf"; + + /// + /// The isolated directory containing only the variable font. Pointing the scanner here + /// (with system directories disabled) guarantees the variable face is the only candidate, + /// which makes the disqualification observable as a null result. + /// + private static List VariableFontDirectories + { + get { return new List { Path.Combine(FontFolder, "VariableFonts") }; } + } + + private static string VariableFontPath + { + get { return Path.Combine(FontFolder, "VariableFonts", VariableFontFileName); } + } + + [TestMethod] + public void ScanSingleFace_VariableFont_SetsIsVariable() + { + // The fvar table must be detected purely from the table directory. + var face = FontScannerV2.GetFace(VariableFontPath); + + Assert.IsNotNull(face, "Expected the variable font to be scanned."); + Assert.IsTrue(face.IsVariable, + "A font containing an 'fvar' table must be flagged as variable."); + } + + [TestMethod] + public void FindBestMatch_VariableFont_DefaultStyle_IsExactMatch() + { + // The default instance of this variable font IS Regular, so a Regular request is a + // legitimate exact match. This guards against over-penalising variable fonts: they + // must still satisfy a request for their default subfamily. + var match = FontScannerV2.FindBestMatch( + VariableFontDirectories, + VariableFontFamily, + FontSubFamily.Regular, + searchSystemDirectories: false); + + Assert.IsNotNull(match, + "A variable font must still match a request for its default (Regular) subfamily."); + Assert.IsTrue(match.IsVariable, "The matched face is expected to be variable."); + Assert.AreEqual(FontSubFamily.Regular, match.Subfamily); + Assert.IsTrue(match.IsExactMatch, + "A variable font matching its default subfamily must be reported as an exact match."); + } + + [TestMethod] + public void FindBestMatch_VariableFont_NonDefaultStyle_IsDisqualified() + { + // Bold is NOT the default instance. Without variation interpolation the file cannot + // deliver Bold, so the variable face must be disqualified. With no other candidate in + // the isolated directory, the scanner returns null — and crucially never returns the + // Regular face flagged as an exact Bold match (the original bug). + var match = FontScannerV2.FindBestMatch( + VariableFontDirectories, + VariableFontFamily, + FontSubFamily.Bold, + searchSystemDirectories: false); + + Assert.IsNull(match, + "A variable font whose default instance is not Bold must not be returned as a " + + "match for a Bold request when it is the only candidate."); + } + + [TestMethod] + public void FindBestMatch_VariableFont_NonDefaultStyle_IsNotExactMatch() + { + // Belt-and-braces companion to the disqualification test, phrased as the property we + // actually care about: even if some future change let a variable face survive as a + // low-scoring candidate for a non-default style, it must never be flagged exact. + var match = FontScannerV2.FindBestMatch( + VariableFontDirectories, + VariableFontFamily, + FontSubFamily.BoldItalic, + searchSystemDirectories: false); + + // Current behaviour: disqualified → null. If that ever changes, the match must at + // least not be exact. + if (match != null) + { + Assert.IsFalse(match.IsExactMatch, + "A variable font must never be an exact match for a non-default subfamily."); + } + } + } +} \ No newline at end of file diff --git a/src/EPPlus.Fonts.OpenType/DefaultFontProvider.cs b/src/EPPlus.Fonts.OpenType/DefaultFontProvider.cs index 34e4a25924..3640404e4c 100644 --- a/src/EPPlus.Fonts.OpenType/DefaultFontProvider.cs +++ b/src/EPPlus.Fonts.OpenType/DefaultFontProvider.cs @@ -13,7 +13,9 @@ Date Author Change 05/20/2026 EPPlus Software AB Script-classified fallback via engine reference *************************************************************************************************/ using EPPlus.Fonts.OpenType.FontResolver; +using OfficeOpenXml.Interfaces.Drawing.Text; using OfficeOpenXml.Interfaces.Fonts; +using OfficeOpenXml.Interfaces.RichText; using System; using System.Collections.Generic; @@ -141,9 +143,27 @@ public IEnumerable GetAllFonts() // Internal helpers // ----------------------------------------------------------------------------------------- + /// + /// Resolves a shaper for a different font through this provider's engine. Used by the + /// layout engine to shape rich-text fragments that switch typeface, so the lookup goes + /// through the same engine that created this provider — not the global OpenTypeFonts + /// singleton. Kept internal: the engine dependency stays encapsulated here rather than + /// leaking onto IFontProvider. + /// + internal ITextShaper GetShaperForFont(IFontFormatBase font) + { + return _engine.GetShaperForFont(font); + } + + internal ITextShaper GetShaperForFont(MeasurementFont font) + { + return _engine.GetShaperForFont(font); + } + /// /// Tries to find the glyph in a lazy-loaded embedded fallback font (Noto Emoji / Math). /// + /// private bool TryGlyphInLazyFallback( LazyFallbackFont lazy, uint codePoint, diff --git a/src/EPPlus.Fonts.OpenType/FontResolver/DefaultFontResolver.cs b/src/EPPlus.Fonts.OpenType/FontResolver/DefaultFontResolver.cs index 3f9109c068..813b8bacad 100644 --- a/src/EPPlus.Fonts.OpenType/FontResolver/DefaultFontResolver.cs +++ b/src/EPPlus.Fonts.OpenType/FontResolver/DefaultFontResolver.cs @@ -56,6 +56,12 @@ public FontAvailability GetFontAvailability(string fontName, FontSubFamily subFa if (string.IsNullOrEmpty(fontName)) return FontAvailability.NotFound; + // special case for Archivo Narrow which is distributed as last-resort-font with EPPlus + if (string.Equals("archivo narrow", fontName, StringComparison.OrdinalIgnoreCase)) + { + return FontAvailability.Exact; + } + var face = _scanner.FindBestMatch( _fontDirectories, fontName, @@ -77,14 +83,20 @@ public FontAvailability GetFontAvailability(string fontName, FontSubFamily subFa public byte[] ResolveFont(string fontName, FontSubFamily subFamily) { - // 1. Try exact match first + // 1. special case for Archivo Narrow which is distributed as last-resort-font with EPPlus + if (string.Equals("archivo narrow", fontName, StringComparison.OrdinalIgnoreCase)) + { + return EmbeddedFonts.LoadArchivoNarrow(subFamily).RawData; + } + + // 2. Try exact match first var face = _scanner.FindBestMatch( _fontDirectories, fontName, subFamily, _searchSystemDirectories); if (face != null && face.IsExactMatch) return _fileReader.ReadFontBytes(face); - // 2. No exact match — try user-configured fallback chain + // 3. No exact match — try user-configured fallback chain if (_config != null) { var userFallbacks = _config.GetFallbacks(fontName); @@ -96,7 +108,7 @@ public byte[] ResolveFont(string fontName, FontSubFamily subFamily) } } - // 3. Try built-in fallback chain for known Office/system fonts. + // 4. Try built-in fallback chain for known Office/system fonts. // Runs after user config so user preferences win, but still provides a metric-aware // safety net for fonts the user hasn't configured. var builtinFallbacks = BuiltinFontFallbackChains.GetFallbacks(fontName); @@ -107,7 +119,7 @@ public byte[] ResolveFont(string fontName, FontSubFamily subFamily) return resolved; } - // 4. No match found — fall back to built-in Archivo Narrow. + // 5. No match found — fall back to built-in Archivo Narrow. // Only applies when using DefaultFontResolver (i.e. no custom resolver installed). return EmbeddedFonts.LoadArchivoNarrow(subFamily).RawData; } diff --git a/src/EPPlus.Fonts.OpenType/Integration/RichText/LayoutSystem.cs b/src/EPPlus.Fonts.OpenType/Integration/RichText/LayoutSystem.cs index 508f67c331..ae56ef5c7d 100644 --- a/src/EPPlus.Fonts.OpenType/Integration/RichText/LayoutSystem.cs +++ b/src/EPPlus.Fonts.OpenType/Integration/RichText/LayoutSystem.cs @@ -30,16 +30,21 @@ public class LayoutSystem TextLineCollection WrappedLineCollection; + private readonly OpenTypeFontEngine _engine; - public LayoutSystem(List preFragments): this(preFragments.Cast().ToList()) + public LayoutSystem(OpenTypeFontEngine engine, List preFragments) + : this(engine, preFragments.Cast().ToList()) { } - public LayoutSystem(IEnumerable preFragments) + public LayoutSystem(OpenTypeFontEngine engine, IEnumerable preFragments) { - InputFragments = new List(); + if (engine == null) + throw new ArgumentNullException("engine"); + _engine = engine; - foreach(var preFrag in preFragments) + InputFragments = new List(); + foreach (var preFrag in preFragments) { var frag = new TextFragmentBase(preFrag); InputFragments.Add(frag); @@ -47,8 +52,12 @@ public LayoutSystem(IEnumerable preFragments) InitializeLayout(); } - public LayoutSystem(IEnumerable fragments) + public LayoutSystem(OpenTypeFontEngine engine, IEnumerable fragments) { + if (engine == null) + throw new ArgumentNullException("engine"); + _engine = engine; + InputFragments = fragments.ToList(); InitializeLayout(); } @@ -186,7 +195,7 @@ void Shaping(bool shapeLight = true) foreach (var styleRun in StyleRuns) { var inputFrag = InputFragments[styleRun.FragmentIndex]; - var shaper = OpenTypeFonts.GetTextShaper(inputFrag.RichTextOptions.Family, inputFrag.RichTextOptions.SubFamily); + var shaper = _engine.GetTextShaper(inputFrag.RichTextOptions.Family, inputFrag.RichTextOptions.SubFamily); if (shapeLight) { @@ -219,7 +228,7 @@ void Shaping(bool shapeLight = true) var lastFragment = InputFragments[InputFragments.Count - 1]; var lastRun = StyleRuns[StyleRuns.Count - 1]; - var lastShaper = OpenTypeFonts.GetTextShaper(lastFragment.RichTextOptions.Family, lastFragment.RichTextOptions.SubFamily); + var lastShaper = _engine.GetTextShaper(lastFragment.RichTextOptions.Family, lastFragment.RichTextOptions.SubFamily); var lastShapedGlyphs = lastShaper.ShapeLight(lastRun.Text); double[] lastCharWidths = new double[lastRun.Length + 1]; lastShapedGlyphs.FillCharWidths((float)lastFragment.RichTextOptions.Size, lastCharWidths, lastRun.Length + 1); @@ -242,8 +251,8 @@ public TextLineCollection Wrap(double maxWidth) return new TextLineCollection(); } var inputRt = InputFragments[0]; - var shaper = OpenTypeFonts.GetTextShaper(inputRt.RichTextOptions.Family, inputRt.RichTextOptions.SubFamily); - var layoutEngine = new TextLayoutEngine(shaper); + var shaper = _engine.GetTextShaper(inputRt.RichTextOptions.Family, inputRt.RichTextOptions.SubFamily); + var layoutEngine = new TextLayoutEngine(_engine, shaper); var wrappedLines = layoutEngine.WrapRichTextRuns(StyleRuns, maxWidth); if(wrappedLines.Count > 1) diff --git a/src/EPPlus.Fonts.OpenType/Integration/TextHandler.cs b/src/EPPlus.Fonts.OpenType/Integration/TextHandler.cs index cec15d660c..2a5e44f0ea 100644 --- a/src/EPPlus.Fonts.OpenType/Integration/TextHandler.cs +++ b/src/EPPlus.Fonts.OpenType/Integration/TextHandler.cs @@ -11,10 +11,12 @@ public class TextHandler TextShaper _currentShaper; TextLayoutEngine _currentLayout; + OpenTypeFontEngine _fontEngine; - public TextHandler(MeasurementFont mf) + public TextHandler(OpenTypeFontEngine fontEngine, MeasurementFont mf) { CurrentFontSize = mf.Size; + _fontEngine = fontEngine; SetFont(mf); } @@ -26,8 +28,8 @@ public void SetFontSize(float fontSize) public void SetFont(MeasurementFont mf) { CurrentFontSize = mf.Size; - _currentShaper = (TextShaper)OpenTypeFonts.GetShaperForFont(mf); - _currentLayout = OpenTypeFonts.GetTextLayoutEngineForFont(mf); + _currentShaper = (TextShaper)_fontEngine.GetShaperForFont(mf); + _currentLayout = _fontEngine.GetTextLayoutEngineForFont(mf); } /// diff --git a/src/EPPlus.Fonts.OpenType/Integration/TextLayoutEngine.RichText.cs b/src/EPPlus.Fonts.OpenType/Integration/TextLayoutEngine.RichText.cs index 5a825ac6ad..944e558124 100644 --- a/src/EPPlus.Fonts.OpenType/Integration/TextLayoutEngine.RichText.cs +++ b/src/EPPlus.Fonts.OpenType/Integration/TextLayoutEngine.RichText.cs @@ -218,9 +218,6 @@ private void ProcessStyleRun( state.CharIdxRt = 0; state.CharIdxWithinOriginal = run.FullTextStart; - state.LineFrag = new LineFragment(state.CurrentFragmentIdx, lineBuilder.Length, state.CharIdxRt, state.CharIdxWithinOriginal); - state.LineFrag.SpaceWidth = run.SpaceWidth; - int i = 0; var len = run.Length; while (i < (len)) @@ -239,6 +236,13 @@ private void ProcessStyleRun( continue; } + //Must be done here rather than above the while loop since when a linebreak is the first char of a new linefragment it will produce empty internalLineFragments otherwise + if(i == 0) + { + state.LineFrag = new LineFragment(state.CurrentFragmentIdx, lineBuilder.Length, state.CharIdxRt, state.CharIdxWithinOriginal); + state.LineFrag.SpaceWidth = run.SpaceWidth; + } + state.CharIdxRt = i; var cWidth = run.GetCharWidthByIndex(i); diff --git a/src/EPPlus.Fonts.OpenType/Integration/TextLayoutEngine.cs b/src/EPPlus.Fonts.OpenType/Integration/TextLayoutEngine.cs index c1bb3178a6..5b0d08037d 100644 --- a/src/EPPlus.Fonts.OpenType/Integration/TextLayoutEngine.cs +++ b/src/EPPlus.Fonts.OpenType/Integration/TextLayoutEngine.cs @@ -35,6 +35,7 @@ namespace EPPlus.Fonts.OpenType.Integration public partial class TextLayoutEngine : IDisposable { private readonly ITextShaper _shaper; + private readonly OpenTypeFontEngine _engine; // Space width cache - avoids repeated Shape(" ") calls private readonly Dictionary _spaceWidthCache; @@ -61,6 +62,17 @@ public TextLayoutEngine(ITextShaper shaper) _spaceWidthCache = new Dictionary(); } + /// + /// Creates a TextLayoutEngine backed by a font engine, enabling per-fragment font resolution + /// for multi-font rich text. + /// + public TextLayoutEngine(OpenTypeFontEngine engine, ITextShaper shaper) + { + _engine = engine ?? throw new ArgumentNullException(nameof(engine)); + _shaper = shaper ?? throw new ArgumentNullException(nameof(shaper)); + _spaceWidthCache = new Dictionary(); + } + public double GetLineHeightInPoints(float fontSize) { return _shaper.GetLineHeightInPoints(fontSize); @@ -230,14 +242,15 @@ private double MeasureText(string text, float fontSize, ShapingOptions options) return shaped.GetWidthInPoints(fontSize); } - private ITextShaper GetShaperForFont(MeasurementFont font) - { - return OpenTypeFonts.GetShaperForFont(font); - } - private ITextShaper GetShaperForFont(IFontFormatBase font) { - return OpenTypeFonts.GetShaperForFont(font); + if (_engine != null) + { + return _engine.GetShaperForFont(font); + } + + // No engine available (single-font constructor): the only shaper we have is our own. + return _shaper; } diff --git a/src/EPPlus.Fonts.OpenType/OpenTypeFontEngine.cs b/src/EPPlus.Fonts.OpenType/OpenTypeFontEngine.cs index 511ccd1f5a..383263859a 100644 --- a/src/EPPlus.Fonts.OpenType/OpenTypeFontEngine.cs +++ b/src/EPPlus.Fonts.OpenType/OpenTypeFontEngine.cs @@ -105,11 +105,14 @@ public OpenTypeFontEngine(Action configure) // ----------------------------------------------------------------------------------------- // Public API // ----------------------------------------------------------------------------------------- - - public FontAvailability LeastRequiredAvailability { get; set; } = FontAvailability.NotFound; - bool RequireExactFoundFont { get { return LeastRequiredAvailability == FontAvailability.Exact; } } - bool RequireFamilyFont{ get { return RequireExactFoundFont || LeastRequiredAvailability == FontAvailability.FamilyOnly; } } - + + /// + /// When true, GetTextShaper throws if the requested font cannot be resolved to an exact match, + /// even though a fallback was found. Default is false: rendering trusts the fallback chain + /// (which always resolves to at least the embedded font) and never throws. Set to true only + /// for diagnostics or validation where a missing exact font should surface as an error. + /// + public bool RequireExactFont { get; set; } = false; //public FontAvailability FallBackAvailablility = FontAvailability.Exact; /// @@ -135,14 +138,14 @@ public TextShaper GetTextShaper(string fontName, FontSubFamily subFamily = FontS if (font == null) return null; - var availability = GetFontAvailability(fontName, subFamily); - if (RequireExactFoundFont && availability != FontAvailability.Exact) + if (RequireExactFont) { - throw new FileNotFoundException($"Could not find Font: {fontName} fallbacked to font:{font.GetEnglishFontFamilyName()}"); - } - else if(RequireFamilyFont && availability != FontAvailability.FamilyOnly && availability != FontAvailability.Exact) - { - throw new FileNotFoundException($"Could not find Font subfamily: {subFamily} fallbacked to familyOnly font:{font.GetEnglishFontFamilyName()} subfamily:{font.SubFamily}"); + var availability = GetFontAvailability(fontName, subFamily); + if (availability != FontAvailability.Exact) + { + throw new FileNotFoundException( + $"Could not find Font: {fontName} {subFamily}. Resolved via fallback to: {font.GetEnglishFontFamilyName()} {font.SubFamily}."); + } } shaper = new TextShaper(this, font); @@ -155,12 +158,12 @@ public TextShaper GetTextShaper(string fontName, FontSubFamily subFamily = FontS public TextLayoutEngine GetTextLayoutEngine(string fontName, FontSubFamily subFamily = FontSubFamily.Regular) { var shaper = GetTextShaper(fontName, subFamily); - return new TextLayoutEngine(shaper); + return new TextLayoutEngine(this, shaper); } public TextLayoutEngine GetTextLayoutEngineForFont(IFontFormatBase font) { var shaper = GetShaperForFont(font); - return new TextLayoutEngine(shaper); + return new TextLayoutEngine(this, shaper); } public ITextShaper GetShaperForFont(IFontFormatBase font) @@ -171,7 +174,7 @@ public ITextShaper GetShaperForFont(IFontFormatBase font) public TextLayoutEngine GetTextLayoutEngineForFont(MeasurementFont font) { var shaper = GetShaperForFont(font); - return new TextLayoutEngine(shaper); + return new TextLayoutEngine(this, shaper); } public ITextShaper GetShaperForFont(MeasurementFont font) diff --git a/src/EPPlus.Fonts.OpenType/Scanner/FontFaceInfo.cs b/src/EPPlus.Fonts.OpenType/Scanner/FontFaceInfo.cs index 825c9cfb28..864b597125 100644 --- a/src/EPPlus.Fonts.OpenType/Scanner/FontFaceInfo.cs +++ b/src/EPPlus.Fonts.OpenType/Scanner/FontFaceInfo.cs @@ -79,6 +79,14 @@ public class FontFaceInfo /// public bool IsExactMatch { get; internal set; } + /// + /// True if this face is a variable font (i.e. the file contains an 'fvar' table). + /// A variable font can only be relied upon to deliver its default named instance unless + /// the variation tables are interpolated, which this library does not yet do. Matching + /// therefore treats a variable face as capable of delivering only its default subfamily. + /// + public bool IsVariable { get; internal set; } + /// /// Table directory for this face. /// @@ -121,6 +129,7 @@ internal FontFaceInfo Clone() Subfamily = Subfamily, FsSelection = FsSelection, IsExactMatch = IsExactMatch, + IsVariable = IsVariable, // carry variable-font flag into per-query copy TableRecords = TableRecords, // shared by reference — never mutated post-scan }; } diff --git a/src/EPPlus.Fonts.OpenType/Scanner/FontScannerV2.cs b/src/EPPlus.Fonts.OpenType/Scanner/FontScannerV2.cs index 0b51030f88..fca2175447 100644 --- a/src/EPPlus.Fonts.OpenType/Scanner/FontScannerV2.cs +++ b/src/EPPlus.Fonts.OpenType/Scanner/FontScannerV2.cs @@ -65,7 +65,11 @@ public static FontFaceInfo FindBestMatch( // callers, and IsExactMatch is per-query state, not a property of the font on disk. // Mutating the cached instance creates a race condition between parallel callers. var result = bestMatch.Clone(); - result.IsExactMatch = bestScore >= 9_000; + // An exact match requires BOTH the family name and the requested style to match. + // Family-normalized (9_000) + exact style (2_000) = 11_000 is the lowest exact score. + // A face matching only the family but approximating the style (the +500/+1000 branches) + // must not count as exact, or the resolver returns e.g. a Regular face for a Bold request. + result.IsExactMatch = bestScore >= 11_000; return result; } @@ -93,8 +97,17 @@ private static int CalculateMatchScore(FontFaceInfo face, string requestedFamily requestedNormalized.IndexOf(faceFamilyNormalized, StringComparison.Ordinal) >= 0) score += 1_000; + bool styleMatches = face.Subfamily == requestedStyle; + + // A variable font is only trustworthy for its default instance. If the requested style is + // not the face's default subfamily, this face cannot deliver it without variation + // interpolation (not yet implemented), so it must not win — disqualify it outright. + // This is what makes a variable "Archivo Narrow Regular" stop masquerading as a Bold match. + if (face.IsVariable && !styleMatches) + return -1; + // Style matching - if (face.Subfamily == requestedStyle) + if (styleMatches) score += 2_000; else if (requestedStyle == FontSubFamily.Regular || face.Subfamily == FontSubFamily.Regular) score += 500; diff --git a/src/EPPlus.Fonts.OpenType/Scanner/FontScannerV2Core.cs b/src/EPPlus.Fonts.OpenType/Scanner/FontScannerV2Core.cs index 62a75df9da..f8237e49ee 100644 --- a/src/EPPlus.Fonts.OpenType/Scanner/FontScannerV2Core.cs +++ b/src/EPPlus.Fonts.OpenType/Scanner/FontScannerV2Core.cs @@ -91,6 +91,12 @@ internal static FontFaceInfo ScanSingleFace(string filePath, long offset) }; info.TableRecords[record.Tag.Value] = record; } + + // A font is "variable" if it carries a font variations table. We only need to know that the + // table exists — not parse it — to decide that this face cannot be trusted to deliver a + // non-default subfamily. No extra I/O: the table directory is already in memory. + info.IsVariable = info.TableRecords.ContainsKey("fvar"); + if (info.TableRecords.TryGetValue("OS/2", out TableRecord os2Rec)) { try diff --git a/src/EPPlus.Fonts.OpenType/Tables/Name/NameTable.cs b/src/EPPlus.Fonts.OpenType/Tables/Name/NameTable.cs index eeaaa4ccb0..f29a296ebf 100644 --- a/src/EPPlus.Fonts.OpenType/Tables/Name/NameTable.cs +++ b/src/EPPlus.Fonts.OpenType/Tables/Name/NameTable.cs @@ -153,12 +153,16 @@ private static Encoding GetEncodingForRecord(NameRecord record) // Fallback for unknown platforms return Encoding.UTF8; } - + private static readonly Dictionary _fallbackEncodings = new Dictionary(); // ADD GetWindowsEncoding() method: private static Encoding GetWindowsEncoding(ushort encodingId) { try { + if(_fallbackEncodings.TryGetValue(encodingId, out var cached)) + { + return cached; + } switch (encodingId) { case 0: @@ -204,6 +208,11 @@ private static Encoding GetWindowsEncoding(ushort encodingId) { // If encoding doesn't exist (e.g. in .NET Standard without System.Text.Encoding.CodePages) // Fallback to UTF-16BE - this is safe for most modern fonts + lock (_fallbackEncodings) + { + _fallbackEncodings.Add(encodingId, Encoding.BigEndianUnicode); + } + return Encoding.BigEndianUnicode; } catch (ArgumentException) diff --git a/src/EPPlus.Fonts.OpenType/TextShaping/TextShaper.cs b/src/EPPlus.Fonts.OpenType/TextShaping/TextShaper.cs index d135aa8331..3af30c604e 100644 --- a/src/EPPlus.Fonts.OpenType/TextShaping/TextShaper.cs +++ b/src/EPPlus.Fonts.OpenType/TextShaping/TextShaper.cs @@ -18,7 +18,9 @@ Date Author Change using EPPlus.Fonts.OpenType.TextShaping.Ligatures; using EPPlus.Fonts.OpenType.TextShaping.Positioning; using EPPlus.Fonts.OpenType.TextShaping.Substitutions; +using OfficeOpenXml.Interfaces.Drawing.Text; using OfficeOpenXml.Interfaces.Fonts; +using OfficeOpenXml.Interfaces.RichText; using System; using System.Collections.Generic; @@ -89,6 +91,30 @@ public TextShaper(IFontProvider fontProvider) _chainingContextualProcessor = new ChainingContextualProcessor(_primaryFont, _singleSubstitutionProcessor, _ligatureProcessor); } + /// + /// Resolves a shaper for a different font via this shaper's font provider. Returns null + /// when the provider is not engine-backed (e.g. a custom IFontProvider), in which case the + /// caller is expected to fall back. This lets a TextLayoutEngine shape multi-font rich text + /// through the engine that produced this shaper instead of the global singleton. + /// + internal ITextShaper GetShaperForFont(IFontFormatBase font) + { + var dfp = _fontProvider as DefaultFontProvider; + if (dfp != null) + return dfp.GetShaperForFont(font); + + return null; + } + + internal ITextShaper GetShaperForFont(MeasurementFont font) + { + var dfp = _fontProvider as DefaultFontProvider; + if (dfp != null) + return dfp.GetShaperForFont(font); + + return null; + } + #region Font Tracking API /// diff --git a/src/EPPlus/Drawing/Chart/ExcelChartAxisStandard.cs b/src/EPPlus/Drawing/Chart/ExcelChartAxisStandard.cs index 4ce29e17e2..7d4174febf 100644 --- a/src/EPPlus/Drawing/Chart/ExcelChartAxisStandard.cs +++ b/src/EPPlus/Drawing/Chart/ExcelChartAxisStandard.cs @@ -15,6 +15,7 @@ Date Author Change using OfficeOpenXml.FormulaParsing.Excel.Functions.Information; using OfficeOpenXml.FormulaParsing.Excel.Functions.MathFunctions; using OfficeOpenXml.FormulaParsing.Excel.Functions.RefAndLookup; +using OfficeOpenXml.FormulaParsing.Excel.Functions.Text; using OfficeOpenXml.Style.XmlAccess; using OfficeOpenXml.Utils.EnumUtils; using OfficeOpenXml.Utils.TypeConversion; @@ -158,6 +159,86 @@ internal set } } /// + /// Returns the actual position of the axis, taking into account the position and the label position. + /// For example, if the axis position is left and the label position is high, the actual position will be to the right of the plotarea. + /// + public eActualAxisPosition ActualAxisPosition + { + get + { + var ap = AxisPosition; + if (ap == eAxisPosition.Left && LabelPosition == eTickLabelPosition.High) + { + return eActualAxisPosition.Right; + } + else if (ap == eAxisPosition.Bottom) + { + if (LabelPosition == eTickLabelPosition.High) + { + return eActualAxisPosition.Top; + } + else + { + return eActualAxisPosition.Bottom; + } + } + else if (ap == eAxisPosition.Right) + { + if (LabelPosition == eTickLabelPosition.Low) + { + if (_chart.Axis.FirstOrDefault(x => x.AxisPosition == eAxisPosition.Left)?.LabelPosition != eTickLabelPosition.Low) + { + return eActualAxisPosition.Left; + } + else + { + return eActualAxisPosition.LeftSecond; + } + } + else + { + if (_chart.Axis.FirstOrDefault(x => x.AxisPosition == eAxisPosition.Left)?.LabelPosition != eTickLabelPosition.High) + { + return eActualAxisPosition.Right; + } + else + { + return eActualAxisPosition.RightSecond; + } + } + } + else if(ap==eAxisPosition.Top) + { + if (LabelPosition == eTickLabelPosition.Low) + { + if (_chart.Axis.FirstOrDefault(x => x.AxisPosition == eAxisPosition.Bottom)?.LabelPosition != eTickLabelPosition.Low) + { + return eActualAxisPosition.Bottom; + } + else + { + return eActualAxisPosition.BottomSecond; + } + } + else + { + if (_chart.Axis.FirstOrDefault(x => x.AxisPosition == eAxisPosition.Bottom)?.LabelPosition != eTickLabelPosition.High) + { + return eActualAxisPosition.Top; + } + else + { + return eActualAxisPosition.TopSecond; + } + } + } + else + { + return (eActualAxisPosition)ap; + } + } + } + /// /// Chart axis title /// public new ExcelChartTitleStandard Title diff --git a/src/EPPlus/Drawing/Chart/ExcelChartNumericSource.cs b/src/EPPlus/Drawing/Chart/ExcelChartNumericSource.cs index 07bfe3a79c..ba0a57cac9 100644 --- a/src/EPPlus/Drawing/Chart/ExcelChartNumericSource.cs +++ b/src/EPPlus/Drawing/Chart/ExcelChartNumericSource.cs @@ -13,6 +13,8 @@ Date Author Change using System.Xml; using System.Globalization; using System; +using System.Collections.Generic; +using OfficeOpenXml.Utils.TypeConversion; namespace OfficeOpenXml.Drawing.Chart { @@ -40,7 +42,7 @@ internal ExcelChartNumericSource(XmlNamespaceManager nameSpaceManager, XmlNode t case "numRef": _formatCode = GetXmlNodeString(_path + "/c:numRef/c:numCache/c:formatCode"); break; - } + } } } /// @@ -164,6 +166,61 @@ public string FormatCode _formatCode=value; } } + internal List GetValuesList(ExcelWorkbook wb) + { + var list = new List(); + var vs = ValuesSource; + if (_sourceElement?.LocalName=="numLit") + { + if (ValuesSource?.Length > 2) + { + var source = ValuesSource.Substring(1, ValuesSource.Length - 2).Split(','); + foreach(var s in source) + { + if(double.TryParse(s.Trim(), NumberStyles.Any, CultureInfo.InvariantCulture, out double d)) + { + list.Add(d); + } + else + { + list.Add(0); + } + } + } + } + else + { + var adr = GetXmlNodeString($"{_path}/c:numRef/c:f"); + if (!string.IsNullOrEmpty(adr)) + { + if (ExcelCellBase.IsValidAddress(adr)) + { + var address = new ExcelAddressBase(adr); + var ws = wb.Worksheets[address.WorkSheetName]; + if (ws != null) + { + var range = ws.Cells[address.Address]; + for (var r = 0; r < range.Start.Row; r++) + { + for (var c = 0; c < range.Start.Row; c++) + { + var v = range.Offset(r, c); + if (ConvertUtil.IsExcelNumeric(v.Value)) + { + list.Add(ConvertUtil.GetValueDouble(v.Value, false)); + } + else + { + list.Add(0D); + } + } + } + } + } + } + } + return list; + } } } diff --git a/src/EPPlus/Drawing/Chart/ExcelChartSerie.cs b/src/EPPlus/Drawing/Chart/ExcelChartSerie.cs index 27b397c37e..580c003d56 100644 --- a/src/EPPlus/Drawing/Chart/ExcelChartSerie.cs +++ b/src/EPPlus/Drawing/Chart/ExcelChartSerie.cs @@ -302,7 +302,23 @@ private string GetAddressValue(ExcelAddressBase address) } if (ws != null) { - return ws.Cells[HeaderAddress.Address].Offset(0, 0).Text; + if (HeaderAddress.IsSingleCell) + { + return ws.Cells[HeaderAddress.Address].Offset(0, 0).Text; + } + else + { + var sb = new StringBuilder(); + foreach (var cell in ws.Cells[HeaderAddress.Address]) + { + if (sb.Length != 0) + { + sb.Append(" "); + } + sb.Append(cell.TextMerged); + } + return sb.ToString(); + } } } return ExcelErrorValue.Values.Ref; diff --git a/src/EPPlus/Drawing/Chart/ExcelChartTitle.cs b/src/EPPlus/Drawing/Chart/ExcelChartTitle.cs index 1d41c5b709..055df57ece 100644 --- a/src/EPPlus/Drawing/Chart/ExcelChartTitle.cs +++ b/src/EPPlus/Drawing/Chart/ExcelChartTitle.cs @@ -148,11 +148,10 @@ public ExcelTextBody TextBody { if (_textBody == null) { - //var defBody = DefaultTextBody; - //var firstDefaultRunProperties = DefaultTextBody.Paragraphs.CreateOrGetDefaultRunProperties($"{_defTxBodyPath}/a:bodyPr/a:p/a:pPr/a:defRPr", TopNode); + var defBody = DefaultTextBody; + var firstDefaultRunProperties = DefaultTextBody.Paragraphs.CreateOrGetDefaultRunProperties($"{_defTxBodyPath}/a:p/a:pPr/a:defRPr", TopNode); _textBody = new ExcelTextBody(_chart, NameSpaceManager, TopNode, $"{_richTextPath}/a:bodyPr", SchemaNodeOrder); - //_textBody.Paragraphs.FirstDefaultRunProperties = firstDefaultRunProperties; - //_textBody.Paragraphs.FirstDefaultRunProperties = DefaultTextBody.Paragraphs.CreateOrGetDefaultRunProperties(); + _textBody.Paragraphs.FirstDefaultRunProperties = firstDefaultRunProperties; } return _textBody; } @@ -250,6 +249,12 @@ internal void CreateRichText() } var tb = TextBody; _richText = new ExcelParagraphCollection(tb, _chart, NameSpaceManager, TopNode, $"{_richTextPath}/a:p", SchemaNodeOrder, defFont, eTextAlignment.Center); + + //if(tb.Paragraphs.Count == 0) + //{ + // var para = _richText.Add(""); + // _richText.Remove(para); + //} } private ExcelChartStyleEntry GetStylePart() @@ -439,7 +444,18 @@ public override string Text var applyStyle = (RichText.Count == 0); RichText.Text = value; _font = null; - if (applyStyle) _chart.ApplyStyleOnPart(this, _chart.StyleManager?.Style?.Title, true); + if (applyStyle) + { + var defRprOld = GetNode($"{_defTxBodyPath}/a:p/a:pPr/a:defRPr"); + defRprOld.InnerXml = ""; + _chart.ApplyStyleOnPart(this, _chart.StyleManager?.Style?.Title, true); + + //Apply style on part does not update default path element yet replaces it + //Copy the new element to the old. + var defRpr = GetNode($"{_defTxBodyPath}/a:p/a:pPr/a:defRPr"); + var textSettingsDefRPr = GetNode($"{_richTextPath}/a:p/a:pPr/a:defRPr"); + CopyElement((XmlElement)defRpr, (XmlElement)textSettingsDefRPr); + } } } /// @@ -463,6 +479,7 @@ public string DisplayedText { combinedString += address.Text + separator; } + return combinedString; } return LinkedCell.Text; } diff --git a/src/EPPlus/Drawing/Chart/ExcelDrawingTextSettings.cs b/src/EPPlus/Drawing/Chart/ExcelDrawingTextSettings.cs index 9545be7711..18ef738b72 100644 --- a/src/EPPlus/Drawing/Chart/ExcelDrawingTextSettings.cs +++ b/src/EPPlus/Drawing/Chart/ExcelDrawingTextSettings.cs @@ -10,9 +10,8 @@ Date Author Change ************************************************************************************************* 01/27/2020 EPPlus Software AB Initial release EPPlus 5 *************************************************************************************************/ -using OfficeOpenXml.FormulaParsing.Excel.Functions.Finance; -using System.Xml; using OfficeOpenXml.Drawing.Style.Effect; +using System.Xml; namespace OfficeOpenXml.Drawing.Chart { /// diff --git a/src/EPPlus/Drawing/Chart/enums/eAxisPosition.cs b/src/EPPlus/Drawing/Chart/enums/eAxisPosition.cs index 0cbcd13295..823243bf3f 100644 --- a/src/EPPlus/Drawing/Chart/enums/eAxisPosition.cs +++ b/src/EPPlus/Drawing/Chart/enums/eAxisPosition.cs @@ -34,4 +34,39 @@ public enum eAxisPosition /// Top = 3 } + public enum eActualAxisPosition + { + /// + /// Left + /// + Left = 0, + /// + /// Bottom + /// + Bottom = 1, + /// + /// Right + /// + Right = 2, + /// + /// Top + /// + Top = 3, + /// + /// If there are two axis on the left side, this is the second axis left (most to the left) + /// + LeftSecond = 5, + /// + /// If there are two axis on the right side, this is the second axis right (most to the right) + /// + RightSecond = 7, + /// + /// If there are two axis on the top, this is the second top left (most to the top) + /// + TopSecond = 9, + /// + /// If there are two axis on the bottom, this is the second bottom left (most to the top) + /// + BottomSecond = 11 + } } \ No newline at end of file diff --git a/src/EPPlus/Drawing/Enums/eSchemeColor.cs b/src/EPPlus/Drawing/Enums/eSchemeColor.cs index fd51e80a13..adedcb5bff 100644 --- a/src/EPPlus/Drawing/Enums/eSchemeColor.cs +++ b/src/EPPlus/Drawing/Enums/eSchemeColor.cs @@ -66,6 +66,7 @@ public enum eSchemeColor /// FollowedHyperlink, /// + /// AKA "phClr" /// A color used in theme definitions which means to use the color of the style /// Style, diff --git a/src/EPPlus/Drawing/Renderer/Chart/Axis/CategoryAxisScaleCalculator.cs b/src/EPPlus/Drawing/Renderer/Chart/Axis/CategoryAxisScaleCalculator.cs index e0ca5c1358..bed313ba57 100644 --- a/src/EPPlus/Drawing/Renderer/Chart/Axis/CategoryAxisScaleCalculator.cs +++ b/src/EPPlus/Drawing/Renderer/Chart/Axis/CategoryAxisScaleCalculator.cs @@ -24,6 +24,7 @@ internal static AxisScale CalculateHorizontalAxisByWidth(ref List values var mf = ax.Font.GetMeasureFont(); List displayValues = GetUniqueValues(values).Select(x=>(object)x.ToString()).ToList(); var uniqeItems = displayValues.Count; + var res = tm.MeasureText(displayValues[0].ToString(), mf); //Get interval for maximum width with vertical text. diff --git a/src/EPPlus/Drawing/Renderer/Chart/ChartAxisRenderer.cs b/src/EPPlus/Drawing/Renderer/Chart/ChartAxisRenderer.cs index 7f3eb4c721..a8535bb3cb 100644 --- a/src/EPPlus/Drawing/Renderer/Chart/ChartAxisRenderer.cs +++ b/src/EPPlus/Drawing/Renderer/Chart/ChartAxisRenderer.cs @@ -36,6 +36,7 @@ Date Author Change using System; using System.Collections.Generic; using System.Linq; +using System.Security.AccessControl; namespace EPPlusImageRenderer.Svg { @@ -84,41 +85,29 @@ internal ChartAxisRenderer(ChartRenderer sc, ExcelChartAxisStandard ax) : base(s } else { - if (ax.AxisPosition == eAxisPosition.Left || ax.AxisPosition == eAxisPosition.Right) + var aav = ax.ActualAxisPosition; + if (ax.IsVertical) { - if (ax.AxisPosition == eAxisPosition.Left) + if (aav == eActualAxisPosition.Left || + aav == eActualAxisPosition.LeftSecond) { Rectangle.Width = GetTextWidest(ax) + LeftMargin; - var ll = 8D; - if (sc.Chart.HasLegend && sc.Chart.Legend.Position == eLegendPosition.Left) - { - ll = sc.Legend.Rectangle.Right + sc.Legend.RightMargin; - } - Rectangle.Left = Title == null ? ll : Title.Rectangle.Left + Title.TextBox.GetActualWidth(); } else { Rectangle.Width = GetTextWidest(ax) + RightMargin; - var lp = sc.ChartArea.Rectangle.Width - Rectangle.Width - 8D; - if (sc.Chart.HasLegend && sc.Chart.Legend.Position == eLegendPosition.Right) - { - lp = sc.Legend.Rectangle.Left + -Rectangle.Width; - } - Rectangle.Left = Title == null ? lp : Title.Rectangle.Left - Rectangle.Width - LeftMargin; } } else { Rectangle.Height = GetTextHeight(ax); - //TODO:Fix - Rectangle.Top = Title == null || ax.AxisPosition == eAxisPosition.Top ? sc.ChartArea.Rectangle.Height - 8 - Rectangle.Height : Title.Rectangle.Top - Rectangle.Height - 8; } } Rectangle.FillColor = "none"; Line = new LineRenderItem(Rectangle.Bounds); - Line.SetDrawingPropertiesBorder(ChartRenderer.Theme, ax.Border, sc.Chart.StyleManager.Style.Title.BorderReference.Color, ax.Border.Fill.Style != eFillStyle.NoFill, 1); + Line.SetDrawingPropertiesBorder(ChartRenderer.Theme, ax.Border, sc.Chart.StyleManager.Style?.Title.BorderReference.Color, ax.Border.Fill.Style != eFillStyle.NoFill, 1); if(Line.BorderWidth < 1) { Line.BorderWidth = 1; @@ -190,7 +179,7 @@ private double GetTextHeight(ExcelChartAxisStandard ax) private double GetTextWidest(ExcelChartAxisStandard ax) { var mf = ax.Font.GetMeasureFont(); - var shaper = OpenTypeFonts.GetShaperForFont(mf); + var shaper = RenderContext.FontEngine.GetShaperForFont(mf); var tm = new OpenTypeFontTextMeasurer(shaper); var widest = 0f; @@ -203,7 +192,7 @@ private double GetTextWidest(ExcelChartAxisStandard ax) widest = m.Width; } } - return widest.PointToPixel(); + return widest; } public List Values @@ -289,7 +278,7 @@ internal void AddTickmarksAndValues(List DefItems) if(Axis.HasMajorGridlines) { - MajorGridlinePositions = AddGridlines(MajorUnit, double.NaN, Axis.MajorGridlines, Chart.StyleManager.Style.GridlineMajor); + MajorGridlinePositions = AddGridlines(MajorUnit, double.NaN, Axis.MajorGridlines, Chart.StyleManager.Style?.GridlineMajor); } if ((Axis.HasMinorGridlines)) @@ -311,7 +300,7 @@ private List GetAxisValueTextBoxes() var mf = Axis.Font.GetMeasureFont(); - var shaper = OpenTypeFonts.GetShaperForFont(mf); + var shaper = RenderContext.FontEngine.GetShaperForFont(mf); var tm = new OpenTypeFontTextMeasurer(shaper); var axisStyle = GetAxisStyleEntry(); @@ -386,7 +375,7 @@ private List GetAxisValueTextBoxes() if (LabelOrientation == eTextOrientation.Diagonal) { x = ticMarkX - (height / 2) * cos; - if (Axis.AxisPosition == eAxisPosition.Bottom) + if (Axis.ActualAxisPosition == eActualAxisPosition.Bottom || Axis.ActualAxisPosition == eActualAxisPosition.BottomSecond) { y = ticMarkY + 4 + TopMargin - (height / 2 * cos); } @@ -398,7 +387,7 @@ private List GetAxisValueTextBoxes() else { x = ticMarkX - (height / 2); - if (Axis.AxisPosition == eAxisPosition.Bottom) + if (Axis.ActualAxisPosition == eActualAxisPosition.Bottom || Axis.ActualAxisPosition == eActualAxisPosition.BottomSecond) { y = ticMarkY + BottomMargin + 4; } @@ -413,7 +402,7 @@ private List GetAxisValueTextBoxes() if (LabelOrientation == eTextOrientation.Diagonal) { tb.Rotation = -45; - if(Axis.AxisPosition==eAxisPosition.Bottom) + if(Axis.ActualAxisPosition==eActualAxisPosition.Bottom || Axis.ActualAxisPosition == eActualAxisPosition.BottomSecond) { tb.TextAnchor = eTextAnchor.End; } @@ -422,7 +411,7 @@ private List GetAxisValueTextBoxes() else if (LabelOrientation == eTextOrientation.Vertical) { tb.Rotation = -90; - if (Axis.AxisPosition == eAxisPosition.Bottom) + if (Axis.ActualAxisPosition == eActualAxisPosition.Bottom || Axis.ActualAxisPosition == eActualAxisPosition.BottomSecond) { tb.TextAnchor = eTextAnchor.End; } @@ -437,10 +426,10 @@ private List GetAxisValueTextBoxes() p.HorizontalAlignment = eTextAlignment.Center; } - tb.TextBody.ImportParagraph(p, 0, v); + tb.ImportParagraph(p, 0, v); //tb.TextBody.Paragraphs[0].AddText(v, Axis.Font); - tb.Rectangle.SetDrawingPropertiesFill(ChartRenderer.Theme, Axis.Fill, axisStyle.FillReference.Color); + tb.Rectangle.SetDrawingPropertiesFill(ChartRenderer.Theme, Axis.Fill, axisStyle?.FillReference.Color); if(widest < tb.Width) { @@ -470,7 +459,7 @@ private List GetAxisValueTextBoxes() else if(LabelOrientation==eTextOrientation.Horizontal && IsCatAx()) //Only apples when labels are horizontally aligned { //Align the axis labels according to the label alignment setting. This is only relevant for horizontal axis, vertical axis are always right aligned. - var lblAlignment = (Axis as ExcelChartAxisStandard)?.LabelAlignment??OfficeOpenXml.eAxisLabelAlignment.Center; + var lblAlignment = (Axis as ExcelChartAxisStandard)?.LabelAlignment ?? OfficeOpenXml.eAxisLabelAlignment.Center; var majorWidth = Rectangle.Width / AxisValues.Count; if (Axis.CrossingAxis == null || Axis.CrossingAxis.CrossBetween == eCrossBetween.MidCat) { @@ -479,12 +468,13 @@ private List GetAxisValueTextBoxes() switch (lblAlignment) { case OfficeOpenXml.eAxisLabelAlignment.Left: - tb.Left -= tb.Width; + tb.Left -= (tb.Width + majorWidth) / 2; break; case OfficeOpenXml.eAxisLabelAlignment.Center: - tb.Left -= tb.Width / 2; + tb.Left -= tb.Width / 2; break; case OfficeOpenXml.eAxisLabelAlignment.Right: + tb.Left += (tb.Width + majorWidth) / 2; break; } } @@ -507,6 +497,17 @@ private List GetAxisValueTextBoxes() } } } + else if(LabelOrientation == eTextOrientation.Diagonal) + { + if (!(Axis.CrossingAxis == null || Axis.CrossingAxis.CrossBetween == eCrossBetween.MidCat)) + { + var majorWidth = Rectangle.Width / AxisValues.Count; + foreach (var tb in ret) + { + tb.Left += majorWidth / 2; + } + } + } return ret; } @@ -552,7 +553,7 @@ private double GetAxisItemLeft(int i, OfficeOpenXml.Interfaces.Drawing.Text.Text private double GetAxisItemTop(int i, OfficeOpenXml.Interfaces.Drawing.Text.TextMeasurement m) { - if (Axis.AxisPosition == eAxisPosition.Top) + if (Axis.ActualAxisPosition == eActualAxisPosition.Top || Axis.ActualAxisPosition == eActualAxisPosition.TopSecond) { switch (LabelOrientation) { @@ -563,7 +564,7 @@ private double GetAxisItemTop(int i, OfficeOpenXml.Interfaces.Drawing.Text.TextM return Rectangle.Bottom - m.Height - TopMargin; } } - else if (Axis.AxisPosition == eAxisPosition.Bottom) + else if (Axis.ActualAxisPosition == eActualAxisPosition.Bottom || Axis.ActualAxisPosition == eActualAxisPosition.BottomSecond) { switch(LabelOrientation) { @@ -662,27 +663,31 @@ private List AddTickmarks(double units, eTimeUnit? dateUnit, dou if (double.IsNaN(parentUnit) || (d % parentUnit != 0)) { double x1, y1, x2, y2; - switch (Axis.AxisPosition) + switch (Axis.ActualAxisPosition) { - case eAxisPosition.Left: + case eActualAxisPosition.Left: + case eActualAxisPosition.LeftSecond: y1 = (float)(Rectangle.Top + Rectangle.Height - ((d - min) / diff * Rectangle.Height)); y2 = y1; x1 = (float)Rectangle.Right - tickMarkWidthOutside; x2 = (float)Rectangle.Right + tickMarkWidthInside; break; - case eAxisPosition.Right: + case eActualAxisPosition.Right: + case eActualAxisPosition.RightSecond: y1 = (float)(Rectangle.Top + Rectangle.Height - ((d - min) / diff * Rectangle.Height)); y2 = y1; x1 = (float)Rectangle.Left - tickMarkWidthInside; x2 = (float)Rectangle.Left + tickMarkWidthOutside; break; - case eAxisPosition.Top: + case eActualAxisPosition.Top: + case eActualAxisPosition.TopSecond: x1 = (float)(Rectangle.Left + ((d - min) / diff * Rectangle.Width)); x2 = x1; y1 = (float)Rectangle.Bottom - tickMarkWidthOutside; y2 = (float)Rectangle.Bottom + tickMarkWidthInside; break; - case eAxisPosition.Bottom: + case eActualAxisPosition.Bottom: + case eActualAxisPosition.BottomSecond: x1 = (float)(Rectangle.Left + ((d - min) / diff * Rectangle.Width)); x2 = x1; y1 = (float)Rectangle.Top - tickMarkWidthInside; @@ -696,7 +701,7 @@ private List AddTickmarks(double units, eTimeUnit? dateUnit, dou tm.Y1 = y1; tm.X2 = x2; tm.Y2 = y2; - tm.SetDrawingPropertiesBorder(ChartRenderer.Theme, Axis.Border, axisStyle.BorderReference.Color, true); + tm.SetDrawingPropertiesBorder(ChartRenderer.Theme, Axis.Border, axisStyle?.BorderReference.Color, true); if(tm.BorderWidth<1) //Excel seems to have this as minimum width for tick marks, so we enforce it here to make sure they are visible. { tm.BorderWidth = 1; @@ -799,7 +804,7 @@ private List AddGridlines(double units, double parentUnit, ExcelDraw tm.Y1 = y1; tm.X2 = x2; tm.Y2 = y2; - tm.SetDrawingPropertiesBorder(ChartRenderer.Theme, lineItem, styleEntry.BorderReference.Color, true, lineItem.Width); + tm.SetDrawingPropertiesBorder(ChartRenderer.Theme, lineItem, styleEntry?.BorderReference.Color, true, lineItem.Width); tm.DefId = id; @@ -837,13 +842,13 @@ private ExcelChartStyleEntry GetAxisStyleEntry() switch (Axis.AxisType) { case eAxisType.Cat: - axisStyle = Chart.StyleManager.Style.CategoryAxis; + axisStyle = Chart.StyleManager.Style?.CategoryAxis; break; case eAxisType.Serie: - axisStyle = Chart.StyleManager.Style.SeriesAxis; + axisStyle = Chart.StyleManager.Style?.SeriesAxis; break; default: - axisStyle = Chart.StyleManager.Style.ValueAxis; + axisStyle = Chart.StyleManager.Style?.ValueAxis; break; } @@ -1096,6 +1101,21 @@ private void AdjustminMaxFromChartObjects(ExcelChartAxisStandard ax, ref double? } } } + if(drawer.SupportsErrorBars && drawer.ErrorBars!=null) + { + foreach(var v in drawer.ErrorBars.Values) + { + if (v[0] < min) + { + min = v[0]; + } + if (v[2] > max) + { + max = v[2]; + } + } + + } } } } diff --git a/src/EPPlus/Drawing/Renderer/Chart/ChartDrawingObject.cs b/src/EPPlus/Drawing/Renderer/Chart/ChartDrawingObject.cs index 2e73aeeb69..2c122aa9fd 100644 --- a/src/EPPlus/Drawing/Renderer/Chart/ChartDrawingObject.cs +++ b/src/EPPlus/Drawing/Renderer/Chart/ChartDrawingObject.cs @@ -18,7 +18,9 @@ Date Author Change using OfficeOpenXml.Drawing; using OfficeOpenXml.Drawing.Chart; using OfficeOpenXml.FormulaParsing.Excel.Functions.MathFunctions; +using OfficeOpenXml.Utils.TypeConversion; using System.Collections.Generic; +using System.Linq; namespace EPPlusImageRenderer.Svg { @@ -38,6 +40,8 @@ internal abstract class ChartDrawingObject : DrawingObject { internal ChartRenderer ChartRenderer; internal ExcelChart Chart => (ExcelChart)ChartRenderer.Drawing; + + internal RenderContext RenderContext => ChartRenderer.RenderContext; internal ChartDrawingObject(ChartRenderer chart) { ChartRenderer = chart; @@ -56,7 +60,7 @@ internal void SetMargins(ExcelTextBody tb) internal double RightMargin { get; set; } internal double TopMargin { get; set; } internal double BottomMargin { get; set; } - internal RectRenderItem Rectangle { get; set; } + internal virtual RectRenderItem Rectangle { get; set; } protected static RectRenderItem GetRectFromManualLayout(ChartRenderer sc, ExcelLayout layout, BoundingBox parent=null) { var bounds = parent ?? sc.Bounds; @@ -88,5 +92,26 @@ protected static RectRenderItem GetRectFromManualLayout(ChartRenderer sc, ExcelL rect.Height = bounds.Height * ml.GetHeight() / 100; return rect; } + /// + /// Get the X serie values. If the values are not numeric, return a serie with the index values (1,2,3,...). Trendline calculation requires numeric X values, but Excel allows non-numeric X values for trendlines, in which case it uses the index values as X for calculation. + /// + /// Input values + /// Output doubles + internal List GetXSerie(List xSerie) + { + var l = new List(); + for (int i = 0; i < xSerie.Count; i++) + { + if (ConvertUtil.IsExcelNumeric(xSerie[i])) + { + l.Add(ConvertUtil.GetValueDouble(xSerie[i])); + } + else + { + return xSerie.Select((x, index) => (double)(index + 1)).ToList(); + } + } + return l; + } } } diff --git a/src/EPPlus/Drawing/Renderer/Chart/ChartLegendRenderer.cs b/src/EPPlus/Drawing/Renderer/Chart/ChartLegendRenderer.cs index febf0e50b5..b0080d9990 100644 --- a/src/EPPlus/Drawing/Renderer/Chart/ChartLegendRenderer.cs +++ b/src/EPPlus/Drawing/Renderer/Chart/ChartLegendRenderer.cs @@ -11,13 +11,15 @@ Date Author Change 27/11/2025 EPPlus Software AB EPPlus 9 *************************************************************************************************/ using EPPlus.DrawingRenderer.RenderItems; -using EPPlus.Export.ImageRenderer.Svg; +using EPPlus.Export.Utils; using EPPlus.Fonts.OpenType; using EPPlus.Fonts.OpenType.Integration; using EPPlusImageRenderer.RenderItems; using OfficeOpenXml.Drawing; using OfficeOpenXml.Drawing.Chart; +using OfficeOpenXml.Drawing.Renderer.Chart; using OfficeOpenXml.Drawing.Renderer.TextBox; +using OfficeOpenXml.FormulaParsing.Excel.Functions.MathFunctions; using OfficeOpenXml.Interfaces.Drawing.Text; using OfficeOpenXml.Style; using System; @@ -42,7 +44,7 @@ internal class ChartLegendRenderer : ChartDrawingObject internal ChartLegendRenderer(ChartRenderer sc, bool isDataLabelLegend = false) : base(sc) { var mf = Chart.Font.GetMeasureFont(); - var shaper = OpenTypeFonts.GetShaperForFont(mf); + var shaper = RenderContext.FontEngine.GetShaperForFont(mf); var _ttMeasurer = new OpenTypeFontTextMeasurer(shaper); if (sc.Chart.HasLegend == false && isDataLabelLegend == false || sc.Chart.Series.Count == 0) @@ -54,17 +56,17 @@ internal ChartLegendRenderer(ChartRenderer sc, bool isDataLabelLegend = false) : LeftMargin = RightMargin = 3; //4px TopMargin = BottomMargin = 3; //4px - _marginItemsWidth = mf.Size; //We use the size of the font as margin between items. + _marginItemsWidth = mf.Size / 2; //We use half the size of the font as margin between items. switch (l.Position) { case eLegendPosition.Top: case eLegendPosition.Bottom: - _maxWidth = sc.ChartArea.Rectangle.Width * 0.8; + _maxWidth = sc.ChartArea.Rectangle.Width * 0.85; _maxHeight = sc.ChartArea.Rectangle.Height * 0.6; break; default: _maxWidth = sc.ChartArea.Rectangle.Width * 0.6; - _maxHeight = sc.ChartArea.Rectangle.Height * 0.8; + _maxHeight = sc.ChartArea.Rectangle.Height * 0.85; break; } double entryWidth, entryHeight; @@ -82,8 +84,8 @@ internal ChartLegendRenderer(ChartRenderer sc, bool isDataLabelLegend = false) : //Bounds.Height = Rectangle.Height; //Rectangle.Bounds.Left = Rectangle.Bounds.Top = 0; - Rectangle.SetDrawingPropertiesFill(sc.Theme, l.Fill, sc.Chart.StyleManager.Style.Title.FillReference.Color); - Rectangle.SetDrawingPropertiesBorder(sc.Theme, l.Border, sc.Chart.StyleManager.Style.Title.BorderReference.Color, l.Border.Fill.Style != eFillStyle.NoFill, 0.75); + Rectangle.SetDrawingPropertiesFill(sc.Theme, l.Fill, sc.Chart.StyleManager.Style?.Title.FillReference.Color); + Rectangle.SetDrawingPropertiesBorder(sc.Theme, l.Border, sc.Chart.StyleManager.Style?.Title.BorderReference.Color, l.Border.Fill.Style != eFillStyle.NoFill, 0.75); var pSls = SetLegendSeries(entryWidth, entryHeight); SetLegendTrendlines(entryWidth, entryHeight, pSls); @@ -101,11 +103,41 @@ private RectRenderItem GetLegendRectangleAndEntrySize(ExcelChartLegend l, out do //Find the widest and hightest legend entry, and calculate the total width and hight of the legend based on the orientation. foreach (var ct in Chart.PlotArea.ChartTypes) { - foreach (var s in ct.Series) + if (ct.GetType() == typeof(ExcelPieChart)) { - var text = s.GetHeaderText(index); - GetSerieSize(l, index, text, ref widest, ref highest); - index++; + //Pie chart cares only about first series + if (ct.Series[0].GetType() == typeof(ExcelPieChartSerie)) + { + var ps = (ExcelPieChartSerie)ct.Series[0]; + var catValues = DrawingExtensions.LoadSeriesValues(ct, ps.XSeries, ps.NumberLiteralsX, ps.StringLiteralsX); + + //Excel fallsback to index + 1 if no literals and no series + if (catValues == null) + { + catValues = new List(); + foreach (var dp in ps.DataPoints) + { + catValues.Add($"{dp.Index + 1}"); + } + } + for (int i = 0; i < catValues.Count; i++) + { + var text = catValues[i].ToString(); + GetSerieSize(l, index, text, ref widest, ref highest); + index++; + } + } + //Skip the rest + break; + } + else + { + foreach (var s in ct.Series) + { + var text = s.GetHeaderText(index); + GetSerieSize(l, index, text, ref widest, ref highest); + index++; + } } } @@ -126,7 +158,7 @@ private RectRenderItem GetLegendRectangleAndEntrySize(ExcelChartLegend l, out do index += trIndex; - var maxIconLength = GetMaxIconLenght(Chart, highest); + var maxIconLength = GetMaxIconLength(Chart, highest); entryWidth = maxIconLength + MarginIconText + widest; entryHeight = highest; @@ -150,7 +182,7 @@ private RectRenderItem GetLegendRectangleAndEntrySize(ExcelChartLegend l, out do { widestLine = width + RightMargin; } - width = RightMargin + widest; + width = RightMargin + entryWidth; } else { @@ -238,7 +270,7 @@ private void GetSerieSize(ExcelChartLegend l, int index, string text, ref double if (_ttMeasurer == null) { - _ttMeasurer = new OpenTypeFontTextMeasurer(OpenTypeFonts.GetShaperForFont(mf)); + _ttMeasurer = new OpenTypeFontTextMeasurer(RenderContext.FontEngine.GetShaperForFont(mf)); } var tm = _ttMeasurer.MeasureText(text, mf); @@ -249,18 +281,18 @@ private void GetSerieSize(ExcelChartLegend l, int index, string text, ref double widest = tm.Width; } - if (tm.Height > highest) + if (tm.Height > highest) { highest = tm.Height; } } - private double GetMaxIconLenght(ExcelChart ct, double heighestText) + private double GetMaxIconLength(ExcelChart ct, double heighestText) { var maxIconLength = 0D; foreach(var c in ct.PlotArea.ChartTypes) { - var il = GetIconLenght(c, heighestText); + var il = GetIconLength(c, heighestText); if (il > maxIconLength) { maxIconLength = il; @@ -268,24 +300,24 @@ private double GetMaxIconLenght(ExcelChart ct, double heighestText) } return maxIconLength; } - private double GetIconLenght(ExcelChart c, double heighestText) + private double GetIconLength(ExcelChart c, double highestText) { - return c.IsTypeLine() ? LineLength : Math.Max(MinBarLength, heighestText * 0.4); + return c.IsTypeLine() ? LineLength : Math.Max(MinBarLength, highestText * 0.4); } internal DrawingLegendSerie SetLegendSeries(double entryWidth, double entryHeight) { int index = 0; - DrawingLegendSerie pSls=null; + DrawingLegendSerie pSls = null; var pos = Chart.Legend.Position; - var maxIconLength = GetMaxIconLenght(Chart, entryHeight); + var maxIconLength = GetMaxIconLength(Chart, entryHeight); foreach (var ct in Chart.PlotArea.ChartTypes) { int ix, end; - if(ct.IsTypeBar()) + if (ct.IsTypeBar()) { - ix = ct.Series.Count-1; + ix = ct.Series.Count - 1; end = -1; } else @@ -294,7 +326,7 @@ internal DrawingLegendSerie SetLegendSeries(double entryWidth, double entryHeigh end = ct.Series.Count; } - while(ix != end) + while (ix != end) { var s = ct.Series[ix]; var sls = new DrawingLegendSerie(); @@ -318,7 +350,12 @@ internal DrawingLegendSerie SetLegendSeries(double entryWidth, double entryHeigh break; case eChartType.Pie: case eChartType.PieExploded: - SetPieLegend(ct, index, pSls, pos, s, sls, entryWidth, entryHeight, maxIconLength); + if (ix == 0) + { + SetPieLegend(ct, index, pSls, pos, s, sls, entryWidth, entryHeight, maxIconLength); + pSls = null; + sls = null; + } break; default: break; @@ -338,20 +375,57 @@ internal DrawingLegendSerie SetLegendSeries(double entryWidth, double entryHeigh break; } } - SeriesIcon.Add(sls); + if (sls != null) + { + SeriesIcon.Add(sls); + } + //SeriesIcon.Add(sls); pSls = sls; + //else + //{ + // pSls = null; + //} index++; - if(ix Rectangle.Bottom) + // { + // break; + // } + // } + // else + // { + // if (sls.Textbox.Bounds.Bottom > Rectangle.Height) + // { + // break; + // } + // } + // SeriesIcon.Add(sls); + // pSls = sls; + // index++; + // if(ix x.Index == entryIndex); var headerText = tl.GetName(serieIndex); @@ -432,55 +506,78 @@ private void SetTrendlineLegend(ExcelChart ct, int serieIndex, int entryIndex, D private void SetPieLegend(ExcelChart ct, int index, DrawingLegendSerie pSls, eLegendPosition pos, ExcelChartSerie s, DrawingLegendSerie sls, double entryWidth, double entryHeight, double maxIconLength) { var ps = (ExcelPieChartSerie)s; + pSls = null; + + //Pie chart only cares about series 0 + var series = ct.Series[0]; + var catSeries = series.XSeries; + var catValues = DrawingExtensions.LoadSeriesValues(ct, catSeries, series.NumberLiteralsX, series.StringLiteralsX); + //Excel fallsback to index + 1 if no literals and no series + if (catValues == null) + { + catValues = new List(); + foreach (var dp in ps.DataPoints) + { + catValues.Add($"{dp.Index + 1}"); + } + } - var si = GetLineSeriesIcon(ct, ps, pSls, entryWidth, entryHeight); - sls.SeriesIcon = si; + double lastWidth = 0; + double totalWidth = 0; - var tbLeft = si.X1 + maxIconLength + MarginIconText; - var tbTop = si.Y2 - entryHeight * 0.5; - var tbWidth = Rectangle.Bounds.Width - tbLeft; + for (int i = 0; i < catValues.Count; i++) + { + var tm = _seriesHeadersMeasure[index + i]; + //Step 1: Retrieve Icon - var tbHeight = entryHeight; - sls.Textbox = new DrawingTextbody(Chart, Rectangle.Bounds, tbLeft, tbTop, tbWidth, tbHeight, false, true); + var si = GetPieSeriesIcon(ct, ps, pSls, lastWidth, entryHeight, i); + sls = new DrawingLegendSerie(); + var tbLeft = si.Left + maxIconLength + MarginIconText; + var tbTop = si.Top - ((entryHeight) / 2); + + double tbWidth; + + if(i != catValues.Count -1) + { + tbWidth = Rectangle.Bounds.Width - tbLeft; + } + else + { + tbWidth = Rectangle.Bounds.Width; + } - //Cat values are the header text - //They create a rect marker for each slice + var tbHeight = tm.Height; + sls.Textbox = new DrawingTextBody(RenderContext, Chart, Rectangle.Bounds, tbLeft, tbTop, tbWidth, tbHeight, false, true); + //var para = sc.Chart.Legend.TextBody.Paragraphs.FirstOrDefault(); + sls.Textbox.ImportParagraph(Chart.Legend.TextBody.Paragraphs.FirstOrDefault(), 0, catValues[i].ToString()); + sls.SeriesIcon = si; + sls.Textbox.RecalculateParagraphs(); - //var entry = Chart.Legend.Entries.FirstOrDefault(x => x.Index == index); - //var catValues = LoadSeriesValues(serie.XSeries, serie.NumberLiteralsX, serie.StringLiteralsX); - //for (int i = 0; i< ps.NumberOfItems; i++) - //{ - - // var headerText = ps.XSeries - //} - //var headerText = s.GetHeaderText(index); - //if (entry == null || entry.Font.IsEmpty) - //{ - // sls.Textbox.ImportParagraph(sc.Chart.Legend.TextBody.Paragraphs.FirstOrDefault(), 0, headerText); - //} - //else - //{ - // //sls.Textbox.AddText(s.GetHeaderText(), entry.Font); - // sls.Textbox.ImportParagraph(entry.TextBody.Paragraphs.FirstOrDefault(), 0, headerText); - //} + tbWidth = sls.Textbox.Width; - //if (ps.DataPoints != null && ps.DataPoints.Count != null /*&& ps.Marker.Style != eMarkerStyle.None*/) - //{ - // var l = sls.SeriesIcon as SvgRenderLineItem; - // var x = l.X1 + (l.X2 - l.X1) / 2; - // var y = l.Y1; - - // //sls.MarkerIcon = LineMarkerHelper.GetMarkerItem(sc, ps, x, y, true); - // if ((ps.Marker.Style == eMarkerStyle.Plus || ps.Marker.Style == eMarkerStyle.X || ps.Marker.Style == eMarkerStyle.Star) && - // ps.Marker.Fill.IsEmpty == false) - // { - // sls.MarkerBackground = LineMarkerHelper.GetMarkerBackground(sc, ps, x, y, true); - // } - // else - // { - // sls.MarkerBackground = null; - // } - //} + lastWidth = tbWidth + si.Width; + + totalWidth += tbWidth + si.Width + maxIconLength + MarginIconText; + + var dp = ps.DataPoints[i]; + + sls.SeriesIcon.SetDrawingPropertiesFill(ChartRenderer.Theme, dp.Fill, ct.As.Chart.PieChart.StyleManager.Style?.DataPoint.FillReference.Color); + sls.SeriesIcon.SetDrawingPropertiesBorder(ChartRenderer.Theme, dp.Border, ct.As.Chart.PieChart.StyleManager.Style?.DataPoint.BorderReference.Color, true); + sls.SeriesIcon.SetDrawingPropertiesEffects(ChartRenderer.Theme, dp.Effect); + + SeriesIcon.Add(sls); + pSls = sls; + } + + foreach(var icon in SeriesIcon) + { + icon.SeriesIcon.Bounds.Top = icon.SeriesIcon.Bounds.Top - ((entryHeight) / 4); + } + + Rectangle.Bounds.Width = SeriesIcon.Last().Textbox.Bounds.GetGlobalBoundingbox().Right - SeriesIcon[0].SeriesIcon.Bounds.GlobalLeft + 4; + Rectangle.Bounds.Left = (ChartRenderer.Bounds.Width / 2) - (totalWidth/2); + pSls = null; + sls = null; } private void SetLineLegend(ExcelChart ct, int index, DrawingLegendSerie pSls, eLegendPosition pos, ExcelChartSerie s, DrawingLegendSerie sls, double entryWidth, double entryHeight, double maxIconLength) @@ -495,7 +592,7 @@ private void SetLineLegend(ExcelChart ct, int index, DrawingLegendSerie pSls, eL var tbWidth = Rectangle.Bounds.Width - tbLeft; var tbHeight = entryHeight; - sls.Textbox = new DrawingTextbody(Chart, Rectangle.Bounds, tbLeft, tbTop, tbWidth, tbHeight, false, true); + sls.Textbox = new DrawingTextBody(RenderContext, Chart, Rectangle.Bounds, tbLeft, tbTop, tbWidth, tbHeight, false, true); var entry = Chart.Legend.Entries.FirstOrDefault(x => x.Index == index); var headerText = s.GetHeaderText(index); @@ -541,7 +638,7 @@ private void SetBarLegend(ExcelChart ct, int index, DrawingLegendSerie pSls, eLe tbWidth = Rectangle.Bounds.Width - tbLeft; var tbHeight = tm.Height; - sls.Textbox = new DrawingTextbody(Chart, Rectangle.Bounds, tbLeft, tbTop, tbWidth, tbHeight, false, true); + sls.Textbox = new DrawingTextBody(RenderContext, Chart, Rectangle.Bounds, tbLeft, tbTop, tbWidth, tbHeight, false, true); //sls.Textbox.Bounds.Left = si.Bottom + MarginIconText; var entry = Chart.Legend.Entries.FirstOrDefault(x => x.Index == index); @@ -561,8 +658,8 @@ private void SetBarLegend(ExcelChart ct, int index, DrawingLegendSerie pSls, eLe private LineRenderItem GetLineSeriesIcon(ExcelChart ct, ExcelChartStandardSerie cStandardSerie, DrawingLegendSerie pSls, double entryWidth, double entryHeight) { var line = new LineRenderItem(Rectangle.Bounds); - line.SetDrawingPropertiesFill(ChartRenderer.Theme, cStandardSerie.Fill, Chart.StyleManager.Style.SeriesLine.FillReference.Color); - line.SetDrawingPropertiesBorder(ChartRenderer.Theme, cStandardSerie.Border, Chart.StyleManager.Style.SeriesLine.BorderReference.Color, cStandardSerie.Border.Fill.Style != eFillStyle.NoFill, 0.75); + line.SetDrawingPropertiesFill(ChartRenderer.Theme, cStandardSerie.Fill, Chart.StyleManager.Style?.SeriesLine.FillReference.Color); + line.SetDrawingPropertiesBorder(ChartRenderer.Theme, cStandardSerie.Border, Chart.StyleManager.Style?.SeriesLine.BorderReference.Color, cStandardSerie.Border.Fill.Style != eFillStyle.NoFill, 0.75); double iconTop = 0, iconLeft = 0; pSls?.GetIconTopLeft(out iconTop, out iconLeft); @@ -579,8 +676,8 @@ private LineRenderItem GetLineSeriesIcon(ExcelChart ct, ExcelChartStandardSerie private LineRenderItem GetTrendLineSeriesIcon(ExcelChart ct, ExcelChartTrendline tl, DrawingLegendSerie pSls, double entryWidth, double entryHeight) { var line = new LineRenderItem(Rectangle.Bounds); - line.SetDrawingPropertiesFill(ChartRenderer.Theme, tl.Fill, Chart.StyleManager.Style.Trendline.FillReference.Color); - line.SetDrawingPropertiesBorder(ChartRenderer.Theme, tl.Border, Chart.StyleManager.Style.Trendline.BorderReference.Color, tl.Border.Fill.Style != eFillStyle.NoFill, 0.75); + line.SetDrawingPropertiesFill(ChartRenderer.Theme, tl.Fill, Chart.StyleManager.Style?.Trendline.FillReference.Color); + line.SetDrawingPropertiesBorder(ChartRenderer.Theme, tl.Border, Chart.StyleManager.Style?.Trendline.BorderReference.Color, tl.Border.Fill.Style != eFillStyle.NoFill, 0.75); double iconTop = 0, iconLeft = 0; pSls?.GetIconTopLeft(out iconTop, out iconLeft); @@ -595,15 +692,46 @@ private LineRenderItem GetTrendLineSeriesIcon(ExcelChart ct, ExcelChartTrendline return line; } + private RectRenderItem GetPieSeriesIcon(ExcelChart ct, ExcelPieChartSerie pcS, DrawingLegendSerie pSls, double entryWidth, double entryHeight, int i) + { + var item = new RectRenderItem(Rectangle.Bounds); + var pt = pcS.DataPoints[i]; + + var iconHeight = GetIconLength(ct, entryHeight); + var icon = pSls?.SeriesIcon as RectRenderItem; + + GetItemPosition(pSls, entryWidth, entryHeight, icon?.Left ?? 0D, icon?.Top ?? 0D, out double x, out double y); + + item.LineCap = LineCap.Round; + item.Left = x; + if (pSls != null && (Chart.Legend.Position == eLegendPosition.Left || Chart.Legend.Position == eLegendPosition.Right)) + { + item.Top = y + (entryHeight - iconHeight) / 2; + } + else + { + item.Top = y; + } + //item.Top = y; + item.Width = iconHeight; + item.Height = iconHeight; + + item.SetDrawingPropertiesFill(ChartRenderer.Theme, pcS.Fill, Chart.StyleManager.Style?.SeriesLine.FillReference.Color); + item.SetDrawingPropertiesBorder(ChartRenderer.Theme, pcS.Border, Chart.StyleManager.Style?.SeriesLine.BorderReference.Color, pcS.Border.Fill.Style != eFillStyle.NoFill, 0.75); + + return item; + } + + private RectRenderItem GetBarSeriesIcon(ExcelChart ct, ExcelChartStandardSerie cStandardSerie, DrawingLegendSerie pSls, double entryWidth, double entryHeight) { var item = new RectRenderItem(Rectangle.Bounds); - var iconHeight = GetIconLenght(ct, entryHeight); + var iconHeight = GetIconLength(ct, entryHeight); //var icon = pSls?.SeriesIcon as RectRenderItem; double iconTop = 0, iconLeft = 0; pSls?.GetIconTopLeft(out iconTop, out iconLeft); - GetItemPosition(pSls, entryWidth, entryHeight, iconTop, iconTop + (iconHeight / 2), out double x, out double y); + GetItemPosition(pSls, entryWidth, entryHeight, iconLeft, iconTop + (iconHeight / 2), out double x, out double y); item.LineCap = LineCap.Round; item.Left = x; @@ -619,8 +747,8 @@ private RectRenderItem GetBarSeriesIcon(ExcelChart ct, ExcelChartStandardSerie c item.Width = iconHeight; item.Height = iconHeight; - item.SetDrawingPropertiesFill(ChartRenderer.Theme, cStandardSerie.Fill, Chart.StyleManager.Style.SeriesLine.FillReference.Color); - item.SetDrawingPropertiesBorder(ChartRenderer.Theme, cStandardSerie.Border, Chart.StyleManager.Style.SeriesLine.BorderReference.Color, cStandardSerie.Border.Fill.Style != eFillStyle.NoFill, 0.75); + item.SetDrawingPropertiesFill(ChartRenderer.Theme, cStandardSerie.Fill, Chart.StyleManager.Style?.SeriesLine.FillReference.Color); + item.SetDrawingPropertiesBorder(ChartRenderer.Theme, cStandardSerie.Border, Chart.StyleManager.Style?.SeriesLine.BorderReference.Color, cStandardSerie.Border.Fill.Style != eFillStyle.NoFill, 0.75); return item; } @@ -631,7 +759,7 @@ private double GetItemPosition(DrawingLegendSerie pSls, double entryWidth, doubl if (Chart.Legend.Position == eLegendPosition.Top || Chart.Legend.Position == eLegendPosition.Bottom) { - if (pSls != null && pSls.Textbox.Bounds.Right + entryWidth + RightMargin > _maxWidth) + if (pSls != null && iconLeft + entryWidth * 2 + _marginItemsWidth + RightMargin > _maxWidth) { topOffset += entryHeight * 1.25; x = LeftMargin; diff --git a/src/EPPlus/Drawing/Renderer/Chart/ChartPlotareaRenderer.cs b/src/EPPlus/Drawing/Renderer/Chart/ChartPlotareaRenderer.cs index ce905cfc0b..5903e8ed14 100644 --- a/src/EPPlus/Drawing/Renderer/Chart/ChartPlotareaRenderer.cs +++ b/src/EPPlus/Drawing/Renderer/Chart/ChartPlotareaRenderer.cs @@ -59,18 +59,19 @@ internal void SetPlotAreaRectangle() ChartRenderer.Legend.Rectangle.Top = Group.Top + rect.Height / 2 - ChartRenderer.Legend.Rectangle.Height / 2; } - rect.SetDrawingPropertiesFill(ChartRenderer.Theme, pa.Fill, ChartRenderer.Chart.StyleManager.Style.PlotArea.FillReference.Color); - rect.SetDrawingPropertiesBorder(ChartRenderer.Theme, pa.Border, ChartRenderer.Chart.StyleManager.Style.PlotArea.BorderReference.Color, pa.Border.Fill.Style != eFillStyle.NoFill, 0.75); + rect.SetDrawingPropertiesFill(ChartRenderer.Theme, pa.Fill, ChartRenderer.Chart.StyleManager.Style?.PlotArea.FillReference.Color, ChartRenderer.Theme.ColorScheme.Light1); + rect.SetDrawingPropertiesBorder(ChartRenderer.Theme, pa.Border, ChartRenderer.Chart.StyleManager.Style?.PlotArea.BorderReference.Color, pa.Border.Fill.Style != eFillStyle.NoFill, 0.75); Rectangle = rect; } private double GetPlotAreaHeight(RectRenderItem rect) { - var bottomAxis = GetAxisByPosition(eAxisPosition.Bottom); + var bottomAxis = GetAxisActualByPosition(eActualAxisPosition.Bottom); double vaHeight = 0; if (bottomAxis!=null) { - vaHeight = (bottomAxis.Rectangle?.Height ?? 0D) + (bottomAxis.Title?.TextBox?.GetActualHeight() ?? 0D); + var bottomSecondAxis = GetAxisActualByPosition(eActualAxisPosition.BottomSecond); + vaHeight = (bottomAxis.Rectangle?.Height ?? 0D) + (bottomAxis.Title?.TextBox?.GetActualHeight() ?? 0D) + (bottomSecondAxis?.Rectangle?.Height ?? 0D); } if (Chart.Legend?.Position == eLegendPosition.Bottom) { @@ -81,7 +82,8 @@ private double GetPlotAreaHeight(RectRenderItem rect) private double GetPlotAreaWidth(RectRenderItem rect) { - var rightAxis = GetAxisByPosition(eAxisPosition.Right); + var rightAxis = GetAxisActualByPosition(eActualAxisPosition.Right); + var rightSecondAxis = GetAxisActualByPosition(eActualAxisPosition.RightSecond); var lp = ChartRenderer.Chart.Legend?.Position; var right = ((lp == eLegendPosition.Right || lp == eLegendPosition.TopRight) && ChartRenderer.Legend != null ? ChartRenderer.Legend.Rectangle.Bounds.GlobalLeft - RightMargin : @@ -95,7 +97,7 @@ private double GetPlotAreaWidth(RectRenderItem rect) } else { - rightAxisWidth = (rightAxis.Title?.TextBox.GetActualWidth() ?? 0D) + (rightAxis.Rectangle?.Width ?? 0D); + rightAxisWidth = (rightAxis.Title?.TextBox.GetActualWidth() ?? 0D) + (rightAxis.Rectangle?.Width ?? 0D) + (rightSecondAxis?.Rectangle?.Width ?? 0D); } var width = right - rightAxisWidth - rect.GlobalLeft; @@ -127,10 +129,16 @@ private double GetPlotAreaLeft() left += ChartRenderer.Legend.Rectangle.Bounds.Width + ChartRenderer.Legend.RightMargin; } - var leftAxis = GetAxisByPosition(eAxisPosition.Left); - if (leftAxis != null) + var leftAxis = GetAxisActualByPosition(eActualAxisPosition.Left); + var leftSecondAxis = GetAxisActualByPosition(eActualAxisPosition.LeftSecond); + if (leftAxis == null) { - if(leftAxis.Title!=null) + leftAxis = GetAxisByPosition(eAxisPosition.Left); + left += leftAxis?.Title?.TextBox.Width ?? 0D; + } + else + { + if (leftAxis.Title!=null) { left += leftAxis.Title.TextBox.GetActualWidth(); } @@ -138,30 +146,52 @@ private double GetPlotAreaLeft() { left += leftAxis.Rectangle.Width + 1.5; } + if(leftSecondAxis!=null) + { + left += leftSecondAxis.Rectangle.Width; + } } return left; } private double GetPlotAreaTop() { double haHeight = 0; - var topAxis = GetAxisByPosition(eAxisPosition.Top); + var topAxis = GetAxisActualByPosition(eActualAxisPosition.Top); + var topSecondAxis = GetAxisActualByPosition(eActualAxisPosition.TopSecond); if (topAxis == null) { - //var bottomAxis = GetAxisByPosition(sc, eAxisPosition.Bottom); - //if (bottomAxis != null && sc.Chart.XAxis.LabelPosition == eTickLabelPosition.High) - //{ - // top += bottomAxis.Rectangle.Height; - //} - //return top; + //If the axis is not on the top, we should check if there is an axis that has the position on the top. If there is, we should reserve space for the title of the axis. This can happen when LabelPosition is set to Low and the axis is on the bottom, but the position of the axis is set to top. + topAxis = GetAxisByPosition(eAxisPosition.Top); + haHeight = topAxis?.Title?.Rectangle.Height ?? 0D; } else { - haHeight = (topAxis.Rectangle?.Height ?? 0D) + (topAxis.Title?.TextBox?.GetActualHeight() ?? 0D); + haHeight = (topAxis.Rectangle?.Height ?? 0D) + (topSecondAxis?.Rectangle?.Height ?? 0D) + (topAxis.Title?.TextBox?.GetActualHeight() ?? 0D); } return (Chart.Legend?.Position == eLegendPosition.Top ? ChartRenderer.Legend.Rectangle.Bounds.Bottom : ChartRenderer.Title?.Rectangle?.GlobalBottom ?? 0d) + haHeight + TopMargin; } + private ChartAxisRenderer GetAxisActualByPosition(eActualAxisPosition pos) + { + if (ChartRenderer.HorizontalAxis != null && ChartRenderer.HorizontalAxis.Axis.ActualAxisPosition == pos) + { + return ChartRenderer.HorizontalAxis; + } + else if (ChartRenderer.VerticalAxis != null && ChartRenderer.VerticalAxis.Axis.ActualAxisPosition == pos) + { + return ChartRenderer.VerticalAxis; + } + else if (ChartRenderer.SecondHorizontalAxis != null && ChartRenderer.SecondHorizontalAxis.Axis.ActualAxisPosition == pos) + { + return ChartRenderer.SecondHorizontalAxis; + } + else if (ChartRenderer.SecondVerticalAxis != null && ChartRenderer.SecondVerticalAxis.Axis.ActualAxisPosition == pos) + { + return ChartRenderer.SecondVerticalAxis; + } + return null; + } private ChartAxisRenderer GetAxisByPosition(eAxisPosition pos) { if (ChartRenderer.HorizontalAxis != null && ChartRenderer.HorizontalAxis.Axis.AxisPosition == pos) diff --git a/src/EPPlus/Drawing/Renderer/Chart/ChartTitleRenderer.cs b/src/EPPlus/Drawing/Renderer/Chart/ChartTitleRenderer.cs index e31fed1184..cd93d52462 100644 --- a/src/EPPlus/Drawing/Renderer/Chart/ChartTitleRenderer.cs +++ b/src/EPPlus/Drawing/Renderer/Chart/ChartTitleRenderer.cs @@ -102,8 +102,8 @@ internal ChartTitleRenderer(ChartRenderer sc, ExcelChartTitleStandard t, string } } - Rectangle.SetDrawingPropertiesFill(sc.Theme, t.Fill, sc.Chart.StyleManager.Style.Title.FillReference.Color); - Rectangle.SetDrawingPropertiesBorder(sc.Theme, t.Border, sc.Chart.StyleManager.Style.Title.BorderReference.Color, t.Border.Fill.Style != eFillStyle.NoFill, 0.75); + Rectangle.SetDrawingPropertiesFill(sc.Theme, t.Fill, sc.Chart.StyleManager.Style?.Title.FillReference.Color); + Rectangle.SetDrawingPropertiesBorder(sc.Theme, t.Border, sc.Chart.StyleManager.Style?.Title.BorderReference.Color, t.Border.Fill.Style != eFillStyle.NoFill, 0.75); } private void SetAxisTitleRect(ChartRenderer sc, ChartAxisRenderer axis) @@ -112,21 +112,29 @@ private void SetAxisTitleRect(ChartRenderer sc, ChartAxisRenderer axis) switch (axis.Axis.AxisPosition) { case eAxisPosition.Left: - Rectangle.Top = sc.GetPlotAreaTop(); - Rectangle.Left = sc.Chart.HasLegend && sc.Chart.Legend.Position == eLegendPosition.Left ? sc.Legend.Rectangle.Right + LeftMargin : margin; + TextBox.Top = sc.GetPlotAreaTop(); + TextBox.Left = sc.Chart.HasLegend && sc.Chart.Legend.Position == eLegendPosition.Left ? sc.Legend.Rectangle.Right + LeftMargin : margin; break; case eAxisPosition.Right: - Rectangle.Top = sc.GetPlotAreaTop(); - Rectangle.Left = sc.Chart.HasLegend && sc.Chart.Legend.Position == eLegendPosition.Right || sc.Chart.Legend.Position == eLegendPosition.TopRight ? sc.Legend.Rectangle.Left - Rectangle.Width - margin : sc.Bounds.Right - Rectangle.Width - margin; + TextBox.Top = sc.GetPlotAreaTop(); + TextBox.Left = sc.Chart.HasLegend && sc.Chart.Legend.Position == eLegendPosition.Right || sc.Chart.Legend.Position == eLegendPosition.TopRight ? sc.Legend.Rectangle.Left - Rectangle.Width - margin : sc.Bounds.Right - Rectangle.Width - margin; break; case eAxisPosition.Bottom: - Rectangle.Top = sc.ChartArea.Rectangle.Height - margin - Rectangle.Height; - Rectangle.Left = GetHorizontalLeft(sc); + TextBox.Top = sc.ChartArea.Rectangle.Height - margin - Rectangle.Height; + TextBox.Left = GetHorizontalLeft(sc); break; case eAxisPosition.Top: - Rectangle.Top = sc.Title != null && sc.Title._title.Layout.HasLayout==false ? sc.Title.Rectangle.Bottom+margin : margin; - Rectangle.Left = GetHorizontalLeft(sc); + TextBox.Top = sc.Title != null && sc.Title._title.Layout.HasLayout==false ? sc.Title.Rectangle.Bottom+margin : margin; + TextBox.Left = GetHorizontalLeft(sc); break; + //case eActualAxisPosition.BottomSecond: + // Rectangle.Top = sc.HorizontalAxis.Rectangle.Bottom; + // Rectangle.Left = GetHorizontalLeft(sc); + // break; + //case eActualAxisPosition.RightSecond: + // Rectangle.Top = sc.GetPlotAreaTop(); + // Rectangle.Left = sc.VerticalAxis.Rectangle.Right; + // break; } } @@ -183,7 +191,6 @@ private static string GetDefaultChartTitleText(ChartRenderer sc, ExcelChartTitle internal void InitTextBox(double maxWidth, double maxHeight) { TextBox = new DrawingTextBox(_svgChart.Drawing, _svgChart.ChartArea.Rectangle.Bounds, maxWidth, maxHeight); - Rectangle = TextBox.Rectangle; if (_title.Rotation != 0) { TextBox.Rotation = _title.Rotation; @@ -191,7 +198,7 @@ internal void InitTextBox(double maxWidth, double maxHeight) } if (_title.TextBody.Paragraphs.Count > 0) { - TextBox.ImportTextBody(_title.TextBody, true, ExcelHorizontalAlignment.Center); + TextBox.ImportTextBodyAndParagraphs(_title.TextBody, true, ExcelHorizontalAlignment.Center); } else { @@ -210,12 +217,16 @@ public DrawingTextBox TextBox { get; private set; } + internal override RectRenderItem Rectangle { get => TextBox.Rectangle; set => base.Rectangle = value; } public override void AppendRenderItems(List renderItems) { var p = _title.DefaultTextBody.Paragraphs.FirstOrDefault(); - TextBox.TextBody.FontColorString = "#" + p.DefaultRunProperties.Fill.Color.ToColorString(); - TextBox.Rectangle.SetDrawingPropertiesFill(_svgChart.Theme,_title.Fill, _svgChart.Chart.StyleManager.Style.Title.FillReference.Color); - TextBox.Rectangle.SetDrawingPropertiesBorder(_svgChart.Theme, _title.Border, _svgChart.Chart.StyleManager.Style.Title.BorderReference.Color, _title.Border.Fill.Style != eFillStyle.NoFill, 0.75); + if (p != null) + { + TextBox.TextBody.FontColorString = "#" + p.DefaultRunProperties.Fill.Color.ToColorString(); + TextBox.Rectangle.SetDrawingPropertiesFill(_svgChart.Theme, _title.Fill, _svgChart.Chart.StyleManager.Style?.Title.FillReference.Color); + TextBox.Rectangle.SetDrawingPropertiesBorder(_svgChart.Theme, _title.Border, _svgChart.Chart.StyleManager.Style?.Title.BorderReference.Color, _title.Border.Fill.Style != eFillStyle.NoFill, 0.75); + } TextBox.AppendRenderItems(renderItems); } diff --git a/src/EPPlus/Drawing/Renderer/Chart/ChartTypeDrawers/BarColumnChartTypeDrawer.cs b/src/EPPlus/Drawing/Renderer/Chart/ChartTypeDrawers/BarColumnChartTypeDrawer.cs index d91018e3b1..a2db076ba3 100644 --- a/src/EPPlus/Drawing/Renderer/Chart/ChartTypeDrawers/BarColumnChartTypeDrawer.cs +++ b/src/EPPlus/Drawing/Renderer/Chart/ChartTypeDrawers/BarColumnChartTypeDrawer.cs @@ -1,18 +1,15 @@ using EPPlus.DrawingRenderer.RenderItems; using EPPlus.Export.ImageRenderer.RenderItems.SvgItem; using EPPlus.Graphics; +using EPPlus.Graphics.Geometry; using EPPlusImageRenderer; using EPPlusImageRenderer.RenderItems; using EPPlusImageRenderer.Svg; -using OfficeOpenXml.DigitalSignatures; using OfficeOpenXml.Drawing.Chart; -using OfficeOpenXml.FormulaParsing.Excel.Functions.Finance; -using OfficeOpenXml.FormulaParsing.Excel.Functions.RefAndLookup; using OfficeOpenXml.Utils.TypeConversion; using System; using System.Collections.Generic; -using System.Linq; -using static OfficeOpenXml.ExcelErrorValue; + namespace EPPlus.Export.ImageRenderer.Svg.Chart { internal class BarColumnChartTypeDrawer : ChartTypeDrawer @@ -21,12 +18,14 @@ internal class BarColumnChartTypeDrawer : ChartTypeDrawer List serieDataLabels = new List(); List> dataPointsPerSerie = new List>(); internal override bool SupportsTrendlines => true; - + internal override bool SupportsErrorBars => true; internal BarColumnChartTypeDrawer(ChartRenderer svgChart, ExcelBarChart chartType) : base(svgChart, chartType) { _catValues = new List>(); _valValues = new List>(); + int serCounter = 0; + foreach (ExcelBarChartSerie serie in chartType.Series) { List valValue,catValue; @@ -35,13 +34,6 @@ internal BarColumnChartTypeDrawer(ChartRenderer svgChart, ExcelBarChart chartTyp _catValues.Add(catValue); _valValues.Add(valValue); - - - //if (serie.HasDataLabel) - //{ - // var datalabel = new ChartSerieDataLabelRenderer(svgChart, serie.DataLabel, svgChart.Bounds, serie, catValue, valValue, serCounter); - // serieDataLabels.Add(datalabel); - //} } if(chartType.IsTypeStacked()) @@ -54,6 +46,7 @@ internal BarColumnChartTypeDrawer(ChartRenderer svgChart, ExcelBarChart chartTyp } CreateTrendlines(chartType, _catValues, _valValues); + CreateErrorBars(chartType, _catValues, _valValues); } internal override void DrawSeries() @@ -71,13 +64,66 @@ internal override void DrawSeries() dataPointsPerSerie.Add(dataPoints); - //if (serie.HasDataLabel) - //{ - // for (int j = 0; j < dataPoints.Count; j++) - // { - // serieDataLabels[i].SetParentPoint(dataPoints[j], j); - // } - //} + int serCounter = 0; + + var isColumn = ((ExcelBarChart)_chartType).IsTypeColumn(); + + if (serie.HasDataLabel) + { + var datalabel = new ChartSerieDataLabelRenderer(ChartRenderer, serie.DataLabel, ChartRenderer.Bounds, serie, _catValues[i], _valValues[i], serCounter++); + serieDataLabels.Add(datalabel); + + for (int j = 0; j < dataPoints.Count; j++) + { + //Initialize transforms + Transform basePoint = new Transform(); + Transform endPoint = new Transform(); + basePoint.Parent = dataPoints[j]; + endPoint.Parent = dataPoints[j]; + + if (isColumn == true) + { + var middleRight = dataPoints[j].Left + (dataPoints[j].Width / 2); + + if (chartBaseY <= dataPoints[j].Top) + { + //We are a negative column + // ----- Base-Axis + // |_| Col + basePoint.Position = new Vector2(middleRight, dataPoints[j].Top); + endPoint.Position = new Vector2(middleRight, dataPoints[j].Bottom); + } + else + { + //We are a positive column + // _ + // | | Col + // ----- Base-Axis + basePoint.Position = new Vector2(middleRight, dataPoints[j].Bottom); + endPoint.Position = new Vector2(middleRight, dataPoints[j].Top); + } + + serieDataLabels[i].SetDimensions(j, basePoint, endPoint); + } + else + { + var middleHeight = dataPoints[j].Top + (dataPoints[j].Height / 2); + basePoint.Position = new Vector2(chartBaseY, middleHeight); + if (chartBaseY > dataPoints[j].Left) + { + endPoint.Position = new Vector2(chartBaseY - dataPoints[j].Width, middleHeight); + } + else + { + endPoint.Position = new Vector2(dataPoints[j].Left + dataPoints[j].Width, middleHeight); + } + + serieDataLabels[i].SetDimensions(j, basePoint, endPoint); + } + } + + serCounter++; + } } foreach (var tr in Trendlines) @@ -101,6 +147,8 @@ internal override void DrawSeries() } } + double chartBaseY = double.NaN; + private void AddBar(ExcelBarChart chartType, ExcelBarChartSerie serie, List> catSeries, List> valSeries, List dataPoints, int seriesCount, int position) { GetAxis(chartType, out var yAxis, out var xAxis); @@ -125,7 +173,10 @@ private void AddBar(ExcelBarChart chartType, ExcelBarChartSerie serie, List286 287 " + var gapWidth = chartType.GapWidth == int.MinValue ? 150 : chartType.GapWidth; + var gapPercent = gapWidth / 100D; // Gap width between bars/columns in percent var overlapPercent = chartType.Overlap / 100D; // Overlap between bars/columns in percent var slotWidth = yWidth / slotSize; var clusterWidth = slotWidth * 100 / (100 + chartType.GapWidth); @@ -133,18 +184,17 @@ private void AddBar(ExcelBarChart chartType, ExcelBarChartSerie serie, List _xSerie; + private ExcelChart _chartType; + + internal List Values { get; set; } = new List(); + public ChartErrorBarRenderer(ChartRenderer svgChart, ExcelChartErrorBars errorbars, List xSerie, List ySerie, ExcelChart chartType, int seriePos) : base(svgChart) + { + _chartType = chartType; + _errorbars = errorbars; + _xSerie = GetXSerie(xSerie); + _ySerie = ySerie.Select(y => ConvertUtil.GetValueDouble(y)).ToArray(); + //_useSecondaryAxis = chartType.UseSecondaryAxis; + //_serieCount = _chartType.Series.Count; + //_seriePos = seriePos; + int n = xSerie.Count; + + switch (errorbars.ValueType) + { + case eErrorValueType.StandardError: + case eErrorValueType.StandardDeviation: + if (n > 1) + { + double avg = _ySerie.Average(); + double sumSquaredDeviations = _ySerie.Sum(v => (v - avg) * (v - avg)); + double sampleStdDev = Math.Sqrt(sumSquaredDeviations / (n - 1)); // sample std dev (n-1) + for (int i=0;i < _xSerie.Count;i++) + { + if (i >= _ySerie.Length) break; + + if(errorbars.ValueType == eErrorValueType.StandardError) + { + double se = sampleStdDev / Math.Sqrt(n); + double y = _ySerie[i]; + Values.Add(new double[] { y - se, y, y + se }); + } + else + { + var mult = errorbars.Value ?? 1D; + Values.Add(new double[] { avg - sampleStdDev, avg, avg + sampleStdDev }); + } + } + } + break; + case eErrorValueType.Percentage: + var percent = (errorbars.Value ?? 0D) / 100D; + for (int i = 0; i < _xSerie.Count; i++) + { + double y = _ySerie[i]; + Values.Add(new double[] { y * (1 - percent), y, y * (1 + percent) }); + } + break; + case eErrorValueType.FixedValue: + var fixedValue = errorbars.Value ?? 0D; + for (int i = 0; i < _xSerie.Count; i++) + { + double y = _ySerie[i]; + Values.Add(new double[] { y - fixedValue, y, y + fixedValue }); + } + + break; + case eErrorValueType.Custom: + var minusList = errorbars.Minus.GetValuesList(_chartType.WorkSheet.Workbook); + var plusList = errorbars.Plus.GetValuesList(_chartType.WorkSheet.Workbook); + for (int i = 0; i < _xSerie.Count; i++) + { + double y = _ySerie[i]; + double minus = GetCustomValue(minusList, i); + double plus = GetCustomValue(plusList, i); + Values.Add(new double[] { y - minus, y, y + plus }); + } + + break; + } + } + public double GetCustomValue(List l, int i) + { + if(l.Count==0) + { + return l[0]; + } + else if(i GetErrorBarRenderItem(int index, ChartAxisRenderer xAxis, ChartAxisRenderer yAxis, double x, double y, double xPos, double yPos) + { + //var path = new PathRenderItem(ChartRenderer.Bounds); + var l = new List(); + double topValue=0, bottomValue=0; + if (_errorbars.BarType == eErrorBarType.Plus || _errorbars.BarType == eErrorBarType.Both) + { + topValue = Values[index][2]; + } + if(_errorbars.BarType == eErrorBarType.Minus || _errorbars.BarType == eErrorBarType.Both) + { + bottomValue = Values[index][0]; + } + + if (_errorbars.Direction == eErrorBarDirection.X) + { + var rightPos = xAxis.GetPositionInPlotarea(topValue); + var centerPos = yAxis.GetPositionInPlotarea(Values[index][1]); + var leftPos = xAxis.GetPositionInPlotarea(bottomValue); + + //Bottom line + //path.Commands.Add(new PathCommands(PathCommandType.Move, leftPos, yPos, centerPos, yPos)); + var bl = new LineRenderItem(ChartRenderer.Bounds) + { + X1 = leftPos, + Y1 = yPos, + X2 = centerPos, + Y2 = yPos + }; + //Top line + //path.Commands.Add(new PathCommands(PathCommandType.Move, centerPos, yPos, rightPos, yPos)); + var tl = new LineRenderItem(ChartRenderer.Bounds) + { + X1 = centerPos, + Y1 = yPos, + X2 = rightPos, + Y2 = yPos + }; + l.Add(bl); + l.Add(tl); + if (_errorbars.NoEndCap == false) + { + //Bottom cap + //path.Commands.Add(new PathCommands(PathCommandType.Move, leftPos, yPos - 3, leftPos, yPos + 3)); + var bc = new LineRenderItem(ChartRenderer.Bounds) + { + X1 = leftPos, + Y1 = yPos - 3, + X2 = leftPos, + Y2 = yPos + 3 + }; + //Top cap + //path.Commands.Add(new PathCommands(PathCommandType.Move, rightPos, yPos - 3, rightPos, yPos + 3)); + var tc = new LineRenderItem(ChartRenderer.Bounds) + { + X1 = rightPos, + Y1 = yPos - 3, + X2 = rightPos, + Y2 = yPos + 3 + }; + l.Add(bc); + l.Add(tc); + } + } + else + { + var bottomPos = yAxis.GetPositionInPlotarea(bottomValue); + var centerPos = yAxis.GetPositionInPlotarea(Values[index][1]); + var topPos = yAxis.GetPositionInPlotarea(topValue); + + //Bottom line + //path.Commands.Add(new PathCommands(PathCommandType.Move, xPos, bottomPos, xPos, centerPos)); + var bl = new LineRenderItem(ChartRenderer.Bounds) + { + X1 = xPos, + Y1 = bottomPos, + X2 = xPos, + Y2 = centerPos + }; + //Top line + //path.Commands.Add(new PathCommands(PathCommandType.Move, xPos, topPos, xPos, centerPos)); + var tl = new LineRenderItem(ChartRenderer.Bounds) + { + X1 = xPos, + Y1 = topPos, + X2 = xPos, + Y2 = centerPos + }; + l.Add(bl); + l.Add(tl); + if (_errorbars.NoEndCap == false) + { + //Bottom cap + //path.Commands.Add(new PathCommands(PathCommandType.Move, xPos-3, bottomPos, xPos+3, bottomPos)); + var bc = new LineRenderItem(ChartRenderer.Bounds) + { + X1 = xPos - 3, + Y1 = bottomPos, + X2 = xPos + 3, + Y2 = bottomPos + }; + + //Top cap + //path.Commands.Add(new PathCommands(PathCommandType.Move, xPos - 3, topPos, xPos + 3, topPos)); + var tc = new LineRenderItem(ChartRenderer.Bounds) + { + X1 = xPos - 3, + Y1 = topPos, + X2 = xPos + 3, + Y2 = topPos + }; + l.Add(bc); + l.Add(tc); + } + } + foreach (var ri in l) + { + if (_errorbars.Border.LineElement == null) + { + ri.SetDrawingPropertiesBorder(ChartRenderer.Theme, ChartRenderer.Chart.StyleManager.Style?.ErrorBar.Border, ChartRenderer.Chart.StyleManager.Style?.ErrorBar.BorderReference.Color, true, 0.75); + } + else + { + ri.SetDrawingPropertiesBorder(ChartRenderer.Theme, _errorbars.Border, ChartRenderer.Chart.StyleManager.Style?.ErrorBar.BorderReference.Color, _errorbars.Border.Fill.Style != eFillStyle.NoFill, 0.75); + } + ri.SetDrawingPropertiesEffects(ChartRenderer.Theme, _errorbars.Effect); + } + return l; + } + } +} diff --git a/src/EPPlus/Drawing/Renderer/Chart/ChartTypeDrawers/ChartTypeDrawer.cs b/src/EPPlus/Drawing/Renderer/Chart/ChartTypeDrawers/ChartTypeDrawer.cs index 4911da7fef..96b91846d8 100644 --- a/src/EPPlus/Drawing/Renderer/Chart/ChartTypeDrawers/ChartTypeDrawer.cs +++ b/src/EPPlus/Drawing/Renderer/Chart/ChartTypeDrawers/ChartTypeDrawer.cs @@ -7,6 +7,7 @@ using OfficeOpenXml; using OfficeOpenXml.Drawing.Chart; using OfficeOpenXml.Drawing.Chart.ChartEx; +using OfficeOpenXml.Drawing.Renderer.Chart.ChartTypeDrawers; using OfficeOpenXml.ExternalReferences; using OfficeOpenXml.FormulaParsing.Excel.Functions.Finance; using OfficeOpenXml.FormulaParsing.Excel.Functions.RefAndLookup; @@ -21,6 +22,7 @@ internal abstract class ChartTypeDrawer : ChartDrawingObject { internal protected ExcelChart _chartType; internal List Trendlines { get; } = new List(); + internal ChartErrorBarRenderer ErrorBars { get; private set; } internal virtual bool SupportsTrendlines { get { return false; } } internal virtual bool SupportsErrorBars { get { return false; } } internal virtual bool SupportsUpDownBars { get { return false; } } @@ -201,6 +203,22 @@ protected void CreateTrendlines(ExcelChart chartType, List> xValues serieIndex++; } } + protected void CreateErrorBars(ExcelChart chartType, List> xValues, List> yValues) + { + var serieIndex = 0; + if (!chartType.IsTypeLine()) return; + foreach (ExcelLineChartSerie serie in chartType.Series) + { + if (serie.HasErrorBars()) + { + var xSerie = xValues[serieIndex]; + var ySerie = yValues[serieIndex]; + ErrorBars = new ChartErrorBarRenderer(ChartRenderer, serie.ErrorBars, xSerie, ySerie, _chartType, serieIndex); + } + serieIndex++; + } + } + internal bool IsOnAxis(ExcelChartAxisStandard ax) { return _chartType.YAxis==ax || _chartType.XAxis==ax; diff --git a/src/EPPlus/Drawing/Renderer/Chart/ChartTypeDrawers/LineChartTypeDrawer.cs b/src/EPPlus/Drawing/Renderer/Chart/ChartTypeDrawers/LineChartTypeDrawer.cs index 6c6785f842..1c368a47d7 100644 --- a/src/EPPlus/Drawing/Renderer/Chart/ChartTypeDrawers/LineChartTypeDrawer.cs +++ b/src/EPPlus/Drawing/Renderer/Chart/ChartTypeDrawers/LineChartTypeDrawer.cs @@ -6,10 +6,12 @@ using EPPlusImageRenderer.Svg; using OfficeOpenXml.DigitalSignatures; using OfficeOpenXml.Drawing.Chart; +using OfficeOpenXml.Drawing.Renderer.Chart; using OfficeOpenXml.FormulaParsing.Excel.Functions.MathFunctions; using OfficeOpenXml.Utils.TypeConversion; using System; using System.Collections.Generic; +using System.Linq; namespace EPPlus.Export.ImageRenderer.Svg.Chart { @@ -20,6 +22,7 @@ internal class LineChartTypeDrawer : ChartTypeDrawer List serieDataLabels = new List(); List> dataPointsPerSerie = new List>(); internal override bool SupportsTrendlines => true; + internal override bool SupportsErrorBars => true; internal LineChartTypeDrawer(ChartRenderer svgChart, ExcelLineChart chartType) : base(svgChart, chartType) { var isStacked = chartType.IsTypeStacked(); @@ -31,12 +34,16 @@ internal LineChartTypeDrawer(ChartRenderer svgChart, ExcelLineChart chartType) : { var yValue = LoadSeriesValues(serie.Series, serie.NumberLiteralsY, serie.StringLiteralsY); var xValue = LoadSeriesValues(serie.XSeries, serie.NumberLiteralsX, serie.StringLiteralsX); - + if(xValue==null) + { + //No x-axis. Create serie from 1..y-items. + xValue = yValue.Select((x, index) => (object)(double)(index + 1)).ToList(); + } _xValues.Add(xValue); _yValues.Add(yValue); serCounter++; - } + } if (chartType.IsTypeStacked()) { @@ -48,6 +55,7 @@ internal LineChartTypeDrawer(ChartRenderer svgChart, ExcelLineChart chartType) : } CreateTrendlines(chartType, _xValues, _yValues); + CreateErrorBars(chartType, _xValues, _yValues); } internal override void DrawSeries() { @@ -134,7 +142,10 @@ private void AddLine(ExcelChart chartType, ExcelLineChartSerie serie, List(); var markerItems = new List(); + var errorBars = new List(); + var hasMarker = serie.HasMarker() && serie.Marker.Style != eMarkerStyle.None; + var hasErrorBars = serie.HasErrorBars(); for (var i = 0; i < yValues.Count; i++) { double x; @@ -162,8 +173,11 @@ private void AddLine(ExcelChart chartType, ExcelLineChartSerie serie, List renderItems) { renderItems.AddRange(ChartAreaRenderItems); diff --git a/src/EPPlus/Drawing/Renderer/Chart/ChartTypeDrawers/PieChartTypeDrawer.cs b/src/EPPlus/Drawing/Renderer/Chart/ChartTypeDrawers/PieChartTypeDrawer.cs index 4ac761a78b..9c4ec5975e 100644 --- a/src/EPPlus/Drawing/Renderer/Chart/ChartTypeDrawers/PieChartTypeDrawer.cs +++ b/src/EPPlus/Drawing/Renderer/Chart/ChartTypeDrawers/PieChartTypeDrawer.cs @@ -161,7 +161,7 @@ private void UpdateSlice(ExcelPieChart chartType, ExcelPieChartSerie serie, int var dataPoint = serie.DataPoints[position]; Slices[position].ImportPathData( - ChartRenderer.Plotarea.Rectangle.Bounds, Rectangle.Bounds, + ChartRenderer.Plotarea.Rectangle.Bounds, ChartRenderer.Bounds, _sliceScaleFactor, dataPoint.Explosion, _pieExplosionPercent, position); Slices[position].ImportStlyeInfo(dataPoint, chartType); @@ -170,16 +170,11 @@ private void UpdateSlice(ExcelPieChart chartType, ExcelPieChartSerie serie, int internal override void DrawSeries() { - _groupItem = new GroupRenderItem(ChartRenderer.Bounds); - _groupItem.TranslationOffset.Left = ChartRenderer.Plotarea.Rectangle.Left; - _groupItem.TranslationOffset.Top = ChartRenderer.Plotarea.Rectangle.Top; + _groupItem = new GroupRenderItem(ChartRenderer.Plotarea.Group.Bounds); Rectangle.Bounds.Name = "ChartDrawer"; - _groupItem.Bounds.Name = "OuterGroupChartDrawer"; - //_groupitem.bounds.parent = _groupitem.translationoffset; - var chartType = (ExcelPieChart)_chartType; //Read and set Starting angle offset as a rotation on the container @@ -189,6 +184,12 @@ internal override void DrawSeries() LoadSeriesValues(chartType); CalculateLocalCenterAndRadius(); + if (serieDataLabels.Count > 0) + { + serieDataLabels[0].rotation = angleOffset; + serieDataLabels[0].rotationPoint = _groupItem.RotationPoint; + } + InitializeSlices(); //How much to scale each slice due to pie explosion @@ -204,23 +205,23 @@ internal override void DrawSeries() _sliceScaleFactor += 0.02d; } - int count = 0; + int dataPointCount = 0; if (catValues != null) { if (valValues != null) { - count = Math.Min(catValues.Count, valValues.Count); + dataPointCount = Math.Max(catValues.Count, valValues.Count); } else { - count = catValues.Count; + dataPointCount = catValues.Count; } } else { if (valValues != null) { - count = valValues.Count; + dataPointCount = valValues.Count; } } @@ -234,18 +235,23 @@ internal override void DrawSeries() //Excel ignores series beyond the first for pie chart visualization if (i == 0) { - for (var j = 0; j < count; j++) + for (var j = 0; j < Slices.Count; j++) { //Update the initialized slice with path, style and group data - UpdateSlice(chartType, serie, count, j); + UpdateSlice(chartType, serie, dataPointCount, j); if (serie.HasDataLabel) { - var innerGroup = Slices[j].GetInnerGroupTransformOriginTranslated(); + var innerGroup = Slices[j].GetInnerGroupWithTransformOriginTranslated(); //Get the global position of the inner items (innerGroup the parent of itemGroup has already had its position set correctly) - var dlblBounds = new BoundingBox(innerGroup.X, innerGroup.Y, Rectangle.Bounds.Width, Rectangle.Bounds.Height); + var dlblBounds = new BoundingBox(innerGroup.LocalPosition.X, innerGroup.LocalPosition.Y, Rectangle.Bounds.Width, Rectangle.Bounds.Height); + + var ctrToMid = Slices[j].GetWholeVectorCenterToMid(); + var startPt = new Transform(); + startPt.Parent = innerGroup.Parent; + startPt.Position = innerGroup.Position + (ctrToMid * -1); - serieDataLabels[i].SetParentVector(dlblBounds, j, Slices[j].GetWholeVectorCenterToMid()); + serieDataLabels[i].SetDimensions(j, startPt, innerGroup); } } } @@ -263,8 +269,11 @@ internal override void DrawSeries() public override void AppendRenderItems(List renderItems) { - renderItems.AddRange(ChartAreaRenderItems); - SeriesRenderItems.ForEach(x => ChartRenderer.Plotarea.Group.AddChildItem(x)); + ChartRenderer.Plotarea.Group.AddChildItem(_groupItem); + if(SeriesRenderItems != null && SeriesRenderItems.Count > 0) + { + ChartRenderer.RenderItems.Add(SeriesRenderItems[0]); + } } } } diff --git a/src/EPPlus/Drawing/Renderer/Chart/ChartTypeDrawers/Trendlines/ChartTrendlineRenderer.cs b/src/EPPlus/Drawing/Renderer/Chart/ChartTypeDrawers/Trendlines/ChartTrendlineRenderer.cs index fa9ad5c115..86ee433cc9 100644 --- a/src/EPPlus/Drawing/Renderer/Chart/ChartTypeDrawers/Trendlines/ChartTrendlineRenderer.cs +++ b/src/EPPlus/Drawing/Renderer/Chart/ChartTypeDrawers/Trendlines/ChartTrendlineRenderer.cs @@ -12,6 +12,8 @@ Date Author Change *************************************************************************************************/ using EPPlus.DrawingRenderer; using EPPlus.DrawingRenderer.RenderItems; +using EPPlus.DrawingRenderer.RenderItems.Textbox; +using EPPlus.Export.ImageRenderer.RenderItems.Shared; using EPPlusImageRenderer; using EPPlusImageRenderer.RenderItems; using EPPlusImageRenderer.Svg; @@ -47,8 +49,8 @@ public ChartTrendlineRenderer(ChartRenderer svgChart, ExcelChartTrendline trendl { case eTrendLine.Linear: CalculateLinear(); - Coordinates.Add(new Coordinate(_xSerie[0], GetLinearValueAtPosition(_xSerie[0]))); - Coordinates.Add(new Coordinate(_xSerie[_xSerie.Count-1], GetLinearValueAtPosition(_xSerie[_xSerie.Count - 1]))); + Coordinates.Add(new Coordinate(0, GetLinearValueAtPosition(1))); + Coordinates.Add(new Coordinate(_xSerie.Count-1, GetLinearValueAtPosition(_xSerie.Count))); break; case eTrendLine.Exponential: CalculateExponential(); @@ -92,28 +94,6 @@ public ChartTrendlineRenderer(ChartRenderer svgChart, ExcelChartTrendline trendl } } - /// - /// Get the X serie values. If the values are not numeric, return a serie with the index values (1,2,3,...). Trendline calculation requires numeric X values, but Excel allows non-numeric X values for trendlines, in which case it uses the index values as X for calculation. - /// - /// Input values - /// Output doubles - private List GetXSerie(List xSerie) - { - var l=new List(); - for(int i=0;i (double)(index + 1)).ToList(); - } - } - return l; - } - private void CreateDatalabel() { if(_trendline.DisplayEquation==false && _trendline.DisplayRSquaredValue==false) @@ -157,7 +137,8 @@ private void CreateDatalabel() } DataLabel.TextBody.AutoSize = true; } - DataLabel.ImportTextBody(lbl.TextBody, true, OfficeOpenXml.Style.ExcelHorizontalAlignment.Center); + lbl.TextBody.Paragraphs[0].HorizontalAlignment = OfficeOpenXml.Drawing.eTextAlignment.Center; + DataLabel.ImportTextBodyAndParagraphs(lbl.TextBody, true, OfficeOpenXml.Style.ExcelHorizontalAlignment.Center); var labelText = ""; if (_trendline.DisplayEquation) { @@ -171,15 +152,16 @@ private void CreateDatalabel() } labelText += RSquare; } - lbl.TextBody.Paragraphs[0].HorizontalAlignment = OfficeOpenXml.Drawing.eTextAlignment.Center; - DataLabel.ImportParagraph(lbl.TextBody.Paragraphs[0], 0, labelText); + //lbl.TextBody.Paragraphs[0].HorizontalAlignment = OfficeOpenXml.Drawing.eTextAlignment.Center; + //DataLabel.ImportParagraph(lbl.TextBody.Paragraphs[0], 0, ""); + AddLblText(DataLabel, labelText); DataLabel.LeftMargin = DataLabel.RightMargin = 4; DataLabel.TopMargin = DataLabel.BottomMargin = 2; //Set datalabel position. - if(DataLabel.Left - (DataLabel.Width + 5) > ChartRenderer.Bounds.Right) + if(DataLabel.Left + 5 > ChartRenderer.Bounds.Right) { - DataLabel.Left = ChartRenderer.Bounds.Right - DataLabel.Width; + DataLabel.Left = ChartRenderer.Bounds.Right - DataLabel.Width - 5; } else { @@ -211,12 +193,41 @@ private void CreateDatalabel() DataLabel.Rectangle.SetDrawingPropertiesEffects(ChartRenderer.Theme, _trendline.Label.Effect); } + private void AddLblText(DrawingTextBox lbl, string labelText) + { + int pIx = 0; + var ix = labelText.IndexOf("|ss:"); + + while(ix>=0 && ix < labelText.Length) + { + lbl.TextBody.Paragraphs[0].AddText(labelText.Substring(pIx, ix - pIx)); + var endIx = labelText.IndexOf("|", ix + 4); + var rt = new RichTextFormatDrawing(lbl.TextBody.Paragraphs[0].DefaultParagraphFont) { SuperScript = true, Text = labelText.Substring(ix+4, endIx-ix-4)}; + lbl.TextBody.Paragraphs[0].AddRichText(rt); + pIx = endIx+1; + ix = labelText.IndexOf("|ss:", pIx); + } + + //Uncommenting this doubles the line in the case ix <0 + //Left as comment just in case there is some edge-cases + //if (ix < 0) + //{ + // lbl.TextBody.Paragraphs[0].AddText(labelText); + //} + if (pIx < labelText.Length) + { + lbl.TextBody.Paragraphs[0].AddText(labelText.Substring(pIx, labelText.Length - pIx)); + } + + //Should probably be a callback + lbl.TextBody.RecalculateParagraphs(); + lbl.TextBody.Top = lbl.TextBody.GetAlignmentVertical(); + } + private void CalculateLinear() { var n = _xSerie.Count; //var sumX = (double)n * (n + 1) / 2; - var sumX = _xSerie.Sum(x => x); - var sumX2 = _xSerie.Sum(x => x * x); var sumY = _ySerie.Sum(y => y); //var sumX2 = (double)n * (n + 1) * (2 * n + 1) / 6; var sumXY = 0D; @@ -224,9 +235,12 @@ private void CalculateLinear() double slope, intercept; if (double.IsNaN(_trendline.Intercept)) { + var sumX = (double)n * (n + 1) / 2; + var sumX2 = (double)n * (n + 1) * (2 * n + 1) / 6; for (int i = 0; i < _ySerie.Length; i++) { - sumXY += _ySerie[i] * (i + 1); + double x = _xSerie[i]; + sumXY += _ySerie[i] * (i+1); } //Slope @@ -236,6 +250,8 @@ private void CalculateLinear() } else { + var sumX = _xSerie.Sum(x => x); + var sumX2 = _xSerie.Sum(x => x * x); intercept = _trendline.Intercept; for (int i = 0; i < _ySerie.Length; i++) { @@ -303,8 +319,8 @@ private void CalculateExponential() var r2 = Math.Pow(Pearson.PearsonImpl(_ySerie.Cast(), GetExponentialSerie(slope, intercept)), 2); Coefficients = [slope, intercept]; - Formula = $"y={intercept:G5}|ss:e{slope:G3}|"; - RSquare = $"R²={r2:N4}"; + Formula = $"y = {intercept:G5}e|ss:{slope:G3}x| "; + RSquare = $"R² = {r2:N4} "; } private void CalculateLogarithmic() { @@ -330,8 +346,8 @@ private void CalculateLogarithmic() Coefficients = [slope, intercept]; var r2 = CalculateRSquared(x => slope * Math.Log(x) + intercept, _ySerie, _trendline.Intercept); - Formula = $"y={slope:G5}ln(x)+{intercept:G5}"; - RSquare = $"R²={r2:N4}"; + Formula = $"y = {slope:G5}ln(x) + {intercept:G5}"; + RSquare = $"R² = {r2:N4}"; } public void CalculatePolynomial() { @@ -454,9 +470,9 @@ public void CalculatePolynomial() Coefficients[i] = matrix[i, coeffCount]; } } - Formula = "y=" + GetPolynormFormula(); + Formula = "y = " + GetPolynormFormula(); var r2 = CalculateRSquared(x => PredictLinear(x), _ySerie, _trendline.Intercept); - RSquare = $"R²={r2:N4}"; + RSquare = $"R² = {r2:N4}"; } private void CalculatePower() @@ -477,10 +493,10 @@ private void CalculatePower() var intercept = Math.Pow(Math.E, (sumLnY - slope * sumLnX) / n); Coefficients = [slope, intercept]; - Formula = $"y={intercept:G5}x|ss:{slope:G3}|"; + Formula = $"y = {intercept:G5} x |ss:{slope:G5}|"; var ylogSerie = _ySerie.Select(y => Math.Log(y)).ToArray(); var r2 = CalculateRSquaredPearson(x => intercept * Math.Pow(x, slope), _ySerie); - RSquare = $"R²={r2:N4}"; + RSquare = $"R² = {r2:N4} "; } private void CalculateMoveingAverage() @@ -509,25 +525,25 @@ private string GetPolynormFormula() { if (Coefficients[i-1]>=0) { - sb.Insert(0, "+"); + sb.Insert(0, " + "); } else { - sb.Insert(0, "-"); + sb.Insert(0, " - "); } if(i < 2) { - sb.Insert(0, $"{Math.Abs(Coefficients[i]):G5}x"); + sb.Insert(0, $"{Math.Abs(Coefficients[i]):G5}x "); } else { - sb.Insert(0, $"{Math.Abs(Coefficients[i]):G5}x|ss:{i}|"); + sb.Insert(0, $"{Math.Abs(Coefficients[i]):G5}x|ss:{i}| "); } } if (Coefficients[Coefficients.Length - 1] < 0) { - sb.Insert(0, "-"); + sb.Insert(0, " - "); } return sb.ToString(); @@ -647,7 +663,7 @@ public override void AppendRenderItems(List renderItems) var pathItem = new PathRenderItem(ChartRenderer.Plotarea.Rectangle.Bounds); pathItem.Commands.Add(new EPPlusImageRenderer.PathCommands(PathCommandType.Move, RenderCoordinates)); pathItem.FillColor = "none"; - pathItem.SetDrawingPropertiesBorder(ChartRenderer.Theme, _trendline.Border, Chart.StyleManager.Style.Trendline.BorderReference.Color, true, _trendline.Border.Width); + pathItem.SetDrawingPropertiesBorder(ChartRenderer.Theme, _trendline.Border, Chart.StyleManager.Style?.Trendline.BorderReference.Color, true, _trendline.Border.Width); pathItem.SetDrawingPropertiesEffects(ChartRenderer.Theme, _trendline.Effect); renderItems.Add(pathItem); } diff --git a/src/EPPlus/Drawing/Renderer/Chart/ChartTypeDrawers/Trendlines/SvgTrendlineDataLabel.cs b/src/EPPlus/Drawing/Renderer/Chart/ChartTypeDrawers/Trendlines/SvgTrendlineDataLabel.cs deleted file mode 100644 index 2179533b59..0000000000 --- a/src/EPPlus/Drawing/Renderer/Chart/ChartTypeDrawers/Trendlines/SvgTrendlineDataLabel.cs +++ /dev/null @@ -1,10 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -namespace EPPlus.Export.ImageRenderer.Svg.Chart.ChartTypeDrawers.Trendlines -{ - internal class SvgTrendlineDataLabel - { - } -} diff --git a/src/EPPlus/Drawing/Renderer/Chart/DataLabels/ChartSerieDataLabelRenderer.cs b/src/EPPlus/Drawing/Renderer/Chart/DataLabels/ChartSerieDataLabelRenderer.cs index fd3dd52aa4..8b44fceca3 100644 --- a/src/EPPlus/Drawing/Renderer/Chart/DataLabels/ChartSerieDataLabelRenderer.cs +++ b/src/EPPlus/Drawing/Renderer/Chart/DataLabels/ChartSerieDataLabelRenderer.cs @@ -6,7 +6,9 @@ using EPPlusImageRenderer.Svg; using OfficeOpenXml.Drawing; using OfficeOpenXml.Drawing.Chart; +using OfficeOpenXml.Utils.TypeConversion; using System.Collections.Generic; +using System.Drawing; namespace EPPlus.Export.ImageRenderer.RenderItems.SvgItem { @@ -22,19 +24,22 @@ internal class ChartSerieDataLabelRenderer : ChartDrawingObject BoundingBox _defaultMargins; ExcelChartSerieDataLabel _dlblSerie; + internal double rotation = double.NaN; + internal Graphics.Point rotationPoint = null; + public ChartSerieDataLabelRenderer(ChartRenderer chart, ExcelChartSerieDataLabel dlblSerie, BoundingBox maxBounds, ExcelChartStandardSerie serie, List xValues, List yValues, int index) : base(chart) { _serieIndex = index; _dlblSerie = dlblSerie; - plotAreaBounds = chart.Plotarea.Rectangle.Bounds; + plotAreaBounds = chart.Plotarea.Group.Bounds; if (dlblSerie.TextBody.Paragraphs.Count != 0) { defaultParagraph = dlblSerie.TextBody.Paragraphs[0]; - dlblSerie.TextBody.GetInsetsInPoints(out double l, out double top, out double right, out double bottom); - _defaultMargins = new BoundingBox(l, top, right, bottom); } + dlblSerie.TextBody.GetInsetsInPoints(out double l, out double top, out double right, out double bottom); + _defaultMargins = new BoundingBox(l, top, right, bottom); if (dlblSerie.DataLabels.Count == 0 && serie.NumberOfItems > 0) { @@ -47,16 +52,30 @@ public ChartSerieDataLabelRenderer(ChartRenderer chart, ExcelChartSerieDataLabel } else { - //if (xValues != null) - //{ - for (int i = 0; i < dlblSerie.DataLabels.Count; i++) + int nextIndex = dlblSerie.DataLabels[0].Index; + var customIndex = 0; + for (int i = 0; i < serie.NumberOfItems; i++) + { + if (i == nextIndex) { - var dataLabel = dlblSerie.DataLabels[i]; + var dataLabel = dlblSerie.DataLabels[customIndex++]; + var individualIndex = dataLabel.Index; var yVal = yValues == null ? null : yValues[i]; var xVal = xValues == null ? null : xValues[i]; AddDatalabel(serie, dataLabel, xVal, yVal, maxBounds); + + if (customIndex < dlblSerie.DataLabels.Count) + { + nextIndex = dlblSerie.DataLabels[customIndex].Index; + } + } + else + { + var yVal = yValues == null ? null : yValues[i]; + var xVal = xValues == null ? null : xValues[i]; + AddDatalabel(serie, dlblSerie, xVal, yValues[i], maxBounds); } - //} + } } } @@ -102,50 +121,51 @@ private void AddDatalabel(ExcelChartStandardSerie serie, ExcelChartDataLabelStan dataLabels.Add(newDataLabel); } - BoundingBox _parentShapeBounds = null; - Vector2 _startToEndDir = Vector2.Zero; - /// - /// Datapoints can have different shapes. - /// Which gives different meaning to positions like'Center' and 'Inside' and 'Outside' - /// Therefore you have the option to provide the bounds of a shape and its endpoint - /// - /// - /// - /// - internal void SetParentShape(BoundingBox parentBounds, BoundingBox shapeEndPoint, int index) - { - _parentShapeBounds = parentBounds; - SetParentPoint(shapeEndPoint, index); - } - - internal void SetParentVector(BoundingBox parentPoint, int index, Vector2 startToEndDir) + internal void SetDimensions(int index, Transform basePoint, Transform endPoint) { - _startToEndDir = startToEndDir; - SetParentPoint(parentPoint, index); + if (dataLabels.Count > index) + { + dataLabels[index].SetShapeDimensions(basePoint, endPoint); + } } internal void SetParentPoint(BoundingBox parent, int index) { if (dataLabels.Count > index) { - dataLabels[index].SetParentPoint(parent, _parentShapeBounds, _startToEndDir); + dataLabels[index].SetParentPoint(parent); } - //dataLabels[index].SetParentPoint(parent); } public override void AppendRenderItems(List renderItems) { var plotAreaGroup = new GroupRenderItem(plotAreaBounds); - if(_dlblSerie.Fill.IsEmpty == false) + plotAreaGroup.Left = plotAreaBounds.Position.X; + plotAreaGroup.Top = plotAreaBounds.Position.Y; + + if(rotation != double.NaN) { + if(rotationPoint != null) + { + plotAreaGroup.RotationPoint = rotationPoint; + } + plotAreaGroup.Rotation = rotation; + } + + if (_dlblSerie.Fill.IsEmpty == false) + { + Rectangle.SetDrawingPropertiesFill(ChartRenderer.Theme, _dlblSerie.Fill, null); plotAreaGroup.SetDrawingPropertiesFill(ChartRenderer.Theme, _dlblSerie.Fill, null); - plotAreaGroup.GroupTransform += $" fill=\"{plotAreaGroup.FillColor}\""; } renderItems.Add(plotAreaGroup); for(int i = 0; i< dataLabels.Count; i++) { + if(rotation != double.NaN) + { + dataLabels[i].CounterRotation = -rotation; + } dataLabels[i].AppendRenderItems(plotAreaGroup.RenderItems); } } diff --git a/src/EPPlus/Drawing/Renderer/Chart/DataLabels/SvgDataLabelPoint.cs b/src/EPPlus/Drawing/Renderer/Chart/DataLabels/SvgDataLabelPoint.cs index db3db6cfea..f1bcda64f5 100644 --- a/src/EPPlus/Drawing/Renderer/Chart/DataLabels/SvgDataLabelPoint.cs +++ b/src/EPPlus/Drawing/Renderer/Chart/DataLabels/SvgDataLabelPoint.cs @@ -1,6 +1,5 @@ using EPPlus.DrawingRenderer; using EPPlus.DrawingRenderer.RenderItems; -using EPPlus.DrawingRenderer.ShapeDefinitions; using EPPlus.Graphics; using EPPlus.Graphics.Geometry; using EPPlusImageRenderer; @@ -28,6 +27,7 @@ internal class SvgDataLabelPoint : ChartDrawingObject Coordinate _manualLayoutOffset = new Coordinate (0, 0); PointLines _connectionPointLines; eLabelPosition _labelPosition; + internal double CounterRotation = double.NaN; //public SvgChartDataLabelStandard(DrawingChart chart, string dataLabelText) : base(chart) //{ @@ -104,23 +104,32 @@ internal void ImportDataLabel(ExcelChartStandardSerie serie, ExcelChartDataLabel var txtBox = new DrawingTextBox(Chart, Rectangle.Bounds, maxBounds.Width, maxBounds.Height); - txtBox.ImportTextBody(dataLabel.TextBody, false); + txtBox.ImportTextBodyAndParagraphs(dataLabel.TextBody, false); txtBox.TextBody.Bounds.Top = 0; txtBox.TextBody.AutoSize = true; if (txtBox.TextBody.Paragraphs.Count == 0) { - txtBox.TextBody.ImportParagraph(defaultParagraph, 0, finalString); + if(defaultParagraph == null) + { + txtBox.TextBody.AddParagraph(finalString); + } + else + { + txtBox.ImportParagraph(defaultParagraph, 0, finalString); + } //txtBox.TextBody.AddParagraph(0, finalString); } else if (txtBox.TextBody.Paragraphs.Count == 1) { - txtBox.TextBody.ImportParagraph(dataLabel.TextBody.Paragraphs[0], 0, finalString); + txtBox.ImportParagraph(dataLabel.TextBody.Paragraphs[0], 0, finalString); //Remove dummy paragraph added by ImportTextBody txtBox.TextBody.Paragraphs.RemoveAt(0); } + txtBox.TextBody.RecalculateParagraphs(); + if(txtBox.LeftMargin == 0) { txtBox.LeftMargin = defaultMargins.Left; @@ -147,6 +156,12 @@ internal void ImportDataLabel(ExcelChartStandardSerie serie, ExcelChartDataLabel { _txtBox.Rectangle.SetDrawingPropertiesFill(ChartRenderer.Theme, dataLabel.Fill, null); } + //else + //{ + // _txtBox.Rectangle.SetDrawingPropertiesFill(ChartRenderer.Theme, dataLabel.Fill, null); + // _txtBox.Rectangle.FillColor = "transparent"; + //} + if (dataLabel.Font.IsEmpty == false) { txtBox.TextBody.FontColorString = "#" + dataLabel.Font.Color.ToColorString(); @@ -167,12 +182,20 @@ internal void ImportDataLabel(ExcelChartStandardSerie serie, ExcelChartDataLabel { _hasManualLayout = true; var rect = GetRectFromManualLayout(ChartRenderer, individualLabel.Layout); - Rectangle = rect; - _manualLayoutOffset = new Coordinate(Rectangle.Left, Rectangle.Top); + Rectangle.Bounds.Left += rect.Left; + Rectangle.Bounds.Top += rect.Top; - Rectangle.Bounds.Left += _manualLayoutOffset.X; - Rectangle.Bounds.Top += _manualLayoutOffset.Y; + _manualLayoutOffset = new Coordinate(rect.Left, rect.Top); + + if (rect.Bounds.Width != 0) + { + Rectangle.Bounds.Width = rect.Bounds.Width; + } + if (rect.Bounds.Height != 0) + { + Rectangle.Bounds.Height = rect.Bounds.Height; + } if (dataLabel.ShowLeaderLines) { @@ -210,10 +233,6 @@ private int GetClosestConnectionPointCoordinateIndex(Coordinate originPoint) return smallestIndex; } - BoundingBox _parentShapeBounds = null; - - - private void SetPositionBasic(BoundingBox point, eLabelPosition basicPosition) { switch (basicPosition) @@ -223,68 +242,192 @@ private void SetPositionBasic(BoundingBox point, eLabelPosition basicPosition) // Bounds.Top = dataLabelCenter.Y; // break; case eLabelPosition.Left: - Rectangle.Bounds.Left -= _txtBox.Width + (point.Width / 2); + Rectangle.Bounds.Left -= (_txtBox.Width/2) + (point.Width / 2d); break; case eLabelPosition.Right: case eLabelPosition.BestFit: - Rectangle.Bounds.Left += _txtBox.Width / 2 + point.Width; + Rectangle.Bounds.Left += (_txtBox.Width / 2d) + point.Width; break; case eLabelPosition.Top: - Rectangle.Bounds.Top -= (point.Height + _txtBox.Height) / 2; + Rectangle.Bounds.Top -= (point.Height + _txtBox.Height) / 2d; break; case eLabelPosition.Bottom: - Rectangle.Bounds.Top += (point.Height + _txtBox.Height) / 2; + Rectangle.Bounds.Top += (point.Height + _txtBox.Height) / 2d; break; default: throw new InvalidOperationException($"The datalabel position entered in SetPositionBasic: '{basicPosition}' is not a basic position"); } } - internal void SetParentPoint(BoundingBox parentPoint, BoundingBox parentShape, Vector2 startToEndDir) + + RectRenderItem originPointRect; + RectRenderItem basePositionRect; + RectRenderItem endPositionRect; + RectRenderItem centerPositionRect; + + + private RectRenderItem GenerateDebugRenderItem(BoundingBox parent, string fillColor) { - Rectangle.Bounds.Parent = parentPoint; - _parentPoint = parentPoint; - _parentShapeBounds = parentShape; + var pointRect = new RectRenderItem(parent); + pointRect.Width = 10d; + pointRect.Height = 10d; + pointRect.FillColor = fillColor; + pointRect.Left = -5d; + pointRect.Top = -5d; + return pointRect; + } - var dataLabelCenter = new Vector2(Rectangle.Bounds.Left, Rectangle.Bounds.Top); - Vector2 startPointDirection = Vector2.Zero; - if ((startToEndDir.X == 0 && startToEndDir.Y == 0) == false) + private void CreateDebugPoints(Transform basePoint, Transform endPoint, Transform centerPoint) + { + originPointRect = GenerateDebugRenderItem(_parentPoint, "darkRed"); + basePositionRect = GenerateDebugRenderItem(_parentPoint, "darkGreen"); + basePositionRect.Left += basePoint.LocalPosition.X; + basePositionRect.Top += basePoint.LocalPosition.Y; + endPositionRect = GenerateDebugRenderItem(_parentPoint, "darkBlue"); + endPositionRect.Left += endPoint.LocalPosition.X; + endPositionRect.Top += endPoint.LocalPosition.Y; + endPositionRect.BorderWidth = 2d; + endPositionRect.BorderColor = "cyan"; + centerPositionRect = GenerateDebugRenderItem(_parentPoint, "Purple"); + centerPositionRect.Left += centerPoint.LocalPosition.X; + centerPositionRect.Top += centerPoint.LocalPosition.Y; + } + private void SetAdjustedTextBoxPosition(Vector2 direction, bool reverseDirection) + { + if(reverseDirection) + { + direction *= -1; + } + + //Ensure vector is normalized + var directionOnly = direction / direction.Length; + + //Get txtbox-size based vector + var txtBoxAdjustVector = new Vector2(_txtBox.Width / 2d, _txtBox.Height / 2d); + + //Apply translation to current position + Rectangle.Bounds.Position += directionOnly * txtBoxAdjustVector; + } + + private void SetInOut(Vector2 direction, Vector2 translation, bool reverseDirection) + { + //Translate to the translation point + Rectangle.Bounds.Position += translation; + SetAdjustedTextBoxPosition(direction, reverseDirection); + } + + /// + /// + /// + internal void SetShapeDimensions(Transform basePoint, Transform endPoint) + { + if(basePoint.Parent != endPoint.Parent) { - startPointDirection = startToEndDir / startToEndDir.Length; + throw new InvalidOperationException("basePoint and endPoint have different parents. " + + "Please ensure that they share the same parent"); } - //if (_parentShapeBounds != null) - //{ - // //shapeCenter - // dataLabelCenter = new Graphics.Math.Vector2((_parentShapeBounds.Width / 2)+parentShape.Left, (_parentShapeBounds.Height / 2)+parentShape.Top); - - // //Get directional vector (in local coords but does not matter since we make it directional) - // startPointDirection = dataLabelCenter - parentPoint.LocalPosition; - // //Divide by length to only get direction - // var startPointDirectionOnly = startPointDirection / startPointDirection.Length; - - // //var lenX = Math.Abs() - // //////Pythagoran theorem - // var len = Math.Sqrt(Math.Pow(startPointDirection.X, 2) + Math.Pow(startPointDirection.Y, 2)); - // startPointDirection = startPointDirectionOnly * len; - //} - //else - //{ - // dataLabelCenter = new Graphics.Math.Vector2(Bounds.Left, Bounds.Top); - //} - - switch (_labelPosition) - { + + //--- Set parent point --- + _parentPoint = new BoundingBox(); + _parentPoint.Parent = basePoint.Parent.Parent; + _parentPoint.Left = basePoint.Parent.LocalPosition.X; + _parentPoint.Top = basePoint.Parent.LocalPosition.Y; + Rectangle.Bounds.Parent = _parentPoint; + //--- + + //--- Calculate vectors and center point --- + var endVector = endPoint.LocalPosition; + var baseVector = basePoint.LocalPosition; + + var endToBaseVector = baseVector - endVector; + var centerVector = endToBaseVector * 0.5d; + + //Translate from end point towards base point by 50% to find the center point + Transform centerPoint = new Transform(endPoint.LocalPosition + centerVector, endPoint.LocalPosition + centerVector); + centerPoint.Parent = basePoint.Parent; + //--- + + //At this point our rectangle globally is centered on the top-left of the object. + //And endVector is the top center position. + + //--- Visualize positions for debugging purposes + CreateDebugPoints(basePoint, endPoint, centerPoint); + //--- + + switch (_labelPosition) + { case eLabelPosition.Center: + Rectangle.Bounds.Left += centerPoint.LocalPosition.X; + Rectangle.Bounds.Top += centerPoint.LocalPosition.Y; + break; + case eLabelPosition.Left: + SetPositionBasic(_parentPoint, _labelPosition); + break; + case eLabelPosition.Right: + SetPositionBasic(_parentPoint, _labelPosition); + break; + case eLabelPosition.Top: + SetPositionBasic(_parentPoint, _labelPosition); + break; + case eLabelPosition.Bottom: + SetPositionBasic(_parentPoint, _labelPosition); + break; + case eLabelPosition.InBase: + SetInOut(endToBaseVector, basePoint.LocalPosition, true); + break; + case eLabelPosition.InEnd: + SetInOut(endToBaseVector, endPoint.LocalPosition, false); + break; + case eLabelPosition.OutEnd: + SetInOut(endToBaseVector, endPoint.LocalPosition, true); + break; + //Only available in charts that include pie chart + case eLabelPosition.BestFit: + //Try to fit within object if possible. If not then as close to it as possible? - if ((startToEndDir.X == 0 && startToEndDir.Y == 0) == false) + //var labelVector = new Vector2(_txtBox.Width, _txtBox.Height); + bool CanFitWidth = _txtBox.Width < Math.Abs(endToBaseVector.X); + bool CanFitHeight = _txtBox.Height < Math.Abs(endToBaseVector.Y); + if (CanFitWidth && CanFitHeight) + { + //TODO: make input parameter in pie chart + bool canFitInCenter = false; + if (canFitInCenter) + { + Rectangle.Bounds.Left += centerPoint.LocalPosition.X; + Rectangle.Bounds.Top += centerPoint.LocalPosition.Y; + } + else + { + //Set inside End + SetInOut(endToBaseVector, endPoint.LocalPosition, false); + } + } + else { - //Half and invert - dataLabelCenter = ((startToEndDir*0.5d) * -1d); + //Set outside end + SetInOut(endToBaseVector, endPoint.LocalPosition, true); } - Rectangle.Bounds.Left += dataLabelCenter.X; - Rectangle.Bounds.Top += dataLabelCenter.Y; + break; + default: + throw new InvalidOperationException($"The datalabel position {_labelPosition} has not been implemented yet"); + } + } + + internal void SetParentPoint(BoundingBox parentPoint) + { + Rectangle.Bounds.Parent = parentPoint; + _parentPoint = parentPoint; + + var dataLabelCenter = new Vector2(Rectangle.Bounds.Left, Rectangle.Bounds.Top); + + switch (_labelPosition) + { + case eLabelPosition.Center: + Rectangle.Bounds.Left += Rectangle.Bounds.Left; + Rectangle.Bounds.Top += Rectangle.Bounds.Top; break; case eLabelPosition.Left: SetPositionBasic(parentPoint, _labelPosition); @@ -299,91 +442,6 @@ internal void SetParentPoint(BoundingBox parentPoint, BoundingBox parentShape, V case eLabelPosition.Bottom: SetPositionBasic(parentPoint, _labelPosition); break; - case eLabelPosition.InEnd: - //if (startPointDirection.X == 0 && startPointDirection.Y == 0) - //{ - // throw new InvalidOperationException("eLabelPosition.InEnd MUST have a direction." + - // "Cannot be within End if EndPoint is undefined."); - //} - ////if(parentShape == null) - ////{ - //// throw new InvalidOperationException("eLabelPosition.InEnd MUST have a parentShape"); - ////} - //startPointDirection = startPointDirection * -1; - //if (startPointDirection.X != 0) - //{ - // //If endPoint is to the right - // if (startPointDirection.X > 0) - // { - // //We must place to the left - // SetPositionBasic(parentPoint, eLabelPosition.Left); - // } - // //if endpoint is to the left - // else - // { - // //We must place to the right - // SetPositionBasic(parentPoint, eLabelPosition.Right); - // } - //} - - //if (startPointDirection.Y != 0) - //{ - // //If endpoint is below - // if (startPointDirection.Y > 0) - // { - // //We must place on Top - // SetPositionBasic(parentPoint, eLabelPosition.Top); - // } - // else - // { - // //We must place on Bottom - // SetPositionBasic(parentPoint, eLabelPosition.Bottom); - // } - //} - break; - case eLabelPosition.OutEnd: - //if (startPointDirection.X == 0 && startPointDirection.Y == 0) - //{ - // throw new InvalidOperationException("eLabelPosition.OutEnd MUST have a direction." + - // "Cannot be within End if EndPoint is undefined."); - //} - ////if (parentShape == null) - ////{ - //// throw new InvalidOperationException("eLabelPosition.OutEnd MUST have a parentShape"); - ////} - - //if (startPointDirection.X != 0) - //{ - // //If endPoint is to the left - // if (startPointDirection.X < 0) - // { - // //We must place to the left - // SetPositionBasic(parentPoint, eLabelPosition.Left); - // } - // //if endpoint is to the right - // else - // { - // //We must place to the right - // SetPositionBasic(parentPoint, eLabelPosition.Right); - // } - //} - - //if (startPointDirection.Y != 0) - //{ - // //If endpoint is on Top - // if (startPointDirection.Y < 0) - // { - // //We must place on Top - // SetPositionBasic(parentPoint, eLabelPosition.Top); - // } - // //If endpoint is on bottom - // else - // { - // //We must place on Bottom - // SetPositionBasic(parentPoint, eLabelPosition.Bottom); - // } - //} - break; default: throw new InvalidOperationException($"The datalabel position {_labelPosition} has not been implemented yet"); } @@ -458,24 +516,50 @@ private void AppendDebugBounds(List renderItems) public override void AppendRenderItems(List renderItems) { var parentPointGroup = new GroupRenderItem(_parentPoint); - renderItems.Add(parentPointGroup); + parentPointGroup.Left = _parentPoint.Left; + parentPointGroup.Top = _parentPoint.Top; var titleItemOrigin = new TitleRenderItem("DataLabel originpoint"); - renderItems.Add(titleItemOrigin); + parentPointGroup.AddChildItem(titleItemOrigin); + + if(originPointRect != null) + { + parentPointGroup.AddChildItem(originPointRect); + } + if(basePositionRect != null) + { + parentPointGroup.AddChildItem(basePositionRect); + } + if(endPositionRect != null) + { + parentPointGroup.AddChildItem(endPositionRect); + } + if(centerPositionRect != null) + { + parentPointGroup.AddChildItem(centerPositionRect); + } + + renderItems.Add(parentPointGroup); var group = new GroupRenderItem(Rectangle.Bounds); - parentPointGroup.RenderItems.Add(group); + group.Left = Rectangle.Bounds.Left; + group.Top = Rectangle.Bounds.Top; var titleItem = new TitleRenderItem("DataLabel size adjustment"); - parentPointGroup.RenderItems.Add(titleItem); + group.AddChildItem(titleItem); + + parentPointGroup.RenderItems.Add(group); + + group.RotationPoint = new Graphics.Point(_txtBox.Left + (_txtBox.Width / 2), _txtBox.Top + (_txtBox.Height / 2)); + group.Rotation = CounterRotation; - _txtBox.AppendRenderItems(parentPointGroup.RenderItems); + _txtBox.AppendRenderItems(group.RenderItems); if(_renderConnectionPointLines) { if (_connectionPointLines != null) { - _connectionPointLines.AppendRenderItems(parentPointGroup.RenderItems); + _connectionPointLines.AppendRenderItems(group.RenderItems); } } @@ -487,7 +571,9 @@ public override void AppendRenderItems(List renderItems) height = _txtBox.Height; } //Currently series icon always has a y1 y2 of 2 - var iconGrp = new GroupRenderItem(new BoundingBox(_seriesIcon.Bounds.Left, height / 2 - 2)); + var iconGrp = new GroupRenderItem(new BoundingBox(_seriesIcon.Bounds.Left, height / 2)); + iconGrp.Left = _seriesIcon.Bounds.Left; + iconGrp.Top = (height / 2) - 2; group.RenderItems.Add(iconGrp); iconGrp.RenderItems.Add(_seriesIcon); } diff --git a/src/EPPlus/Drawing/Renderer/DrawingLegendSerie.cs b/src/EPPlus/Drawing/Renderer/Chart/DrawingLegendSerie.cs similarity index 93% rename from src/EPPlus/Drawing/Renderer/DrawingLegendSerie.cs rename to src/EPPlus/Drawing/Renderer/Chart/DrawingLegendSerie.cs index 4809d8098b..f1aea585d8 100644 --- a/src/EPPlus/Drawing/Renderer/DrawingLegendSerie.cs +++ b/src/EPPlus/Drawing/Renderer/Chart/DrawingLegendSerie.cs @@ -18,9 +18,9 @@ Date Author Change namespace EPPlusImageRenderer.Svg { - internal class DrawingLegendSerie : SvgLegendSeriesIcon + internal class DrawingLegendSerie : DrawingLegendSeriesIcon { - internal DrawingTextbody Textbox { get; set; } + internal DrawingTextBody Textbox { get; set; } internal void GetIconTopLeft(out double top, out double left) { diff --git a/src/EPPlus/Drawing/Renderer/SvgLegendSeriesIcon.cs b/src/EPPlus/Drawing/Renderer/Chart/DrawingLegendSeriesIcon.cs similarity index 85% rename from src/EPPlus/Drawing/Renderer/SvgLegendSeriesIcon.cs rename to src/EPPlus/Drawing/Renderer/Chart/DrawingLegendSeriesIcon.cs index 39c0af1908..84ea79dfe7 100644 --- a/src/EPPlus/Drawing/Renderer/SvgLegendSeriesIcon.cs +++ b/src/EPPlus/Drawing/Renderer/Chart/DrawingLegendSeriesIcon.cs @@ -2,7 +2,7 @@ namespace EPPlusImageRenderer.Svg { - internal class SvgLegendSeriesIcon + internal class DrawingLegendSeriesIcon { internal RenderItem SeriesIcon { get; set; } internal RenderItem MarkerIcon { get; set; } diff --git a/src/EPPlus/Drawing/Renderer/LineMarkerHelper.cs b/src/EPPlus/Drawing/Renderer/Chart/LineMarkerHelper.cs similarity index 97% rename from src/EPPlus/Drawing/Renderer/LineMarkerHelper.cs rename to src/EPPlus/Drawing/Renderer/Chart/LineMarkerHelper.cs index 7a00fbe32c..b0452efee1 100644 --- a/src/EPPlus/Drawing/Renderer/LineMarkerHelper.cs +++ b/src/EPPlus/Drawing/Renderer/Chart/LineMarkerHelper.cs @@ -8,7 +8,7 @@ using System; using System.Collections.Generic; -namespace EPPlus.Export.ImageRenderer.Svg +namespace OfficeOpenXml.Drawing.Renderer.Chart { internal class LineMarkerHelper { @@ -127,7 +127,7 @@ internal static RenderItem GetMarkerItem(ChartRenderer sc, ExcelLineChartSerie l } else if (ls.Fill.IsEmpty) { - item?.SetDrawingPropertiesFillBasic(sc.Theme, ls.Border.Fill, sc.Chart.StyleManager.Style.DataPointMarker.FillReference.Color); + item?.SetDrawingPropertiesFillBasic(sc.Theme, ls.Border.Fill, sc.Chart.StyleManager.Style?.DataPointMarker.FillReference.Color, sc.Theme.ColorScheme.Accent1); } else { diff --git a/src/EPPlus/Drawing/Renderer/Chart/PieSliceRenderItem.cs b/src/EPPlus/Drawing/Renderer/Chart/PieSliceRenderItem.cs index 4570bd81ed..fb58373378 100644 --- a/src/EPPlus/Drawing/Renderer/Chart/PieSliceRenderItem.cs +++ b/src/EPPlus/Drawing/Renderer/Chart/PieSliceRenderItem.cs @@ -6,7 +6,6 @@ using EPPlusImageRenderer.RenderItems; using EPPlusImageRenderer.Svg; using OfficeOpenXml.Drawing.Chart; -using OfficeOpenXml.FormulaParsing.Excel.Functions.MathFunctions; using OfficeOpenXml.Utils.Drawing; using System; using System.Collections.Generic; @@ -58,11 +57,15 @@ internal Coordinate GetMidPointLocal() /// internal Coordinate GetEndPointLocal() { - return new Coordinate(_endPoint.Left, _endPoint.Top); + return new Coordinate(_endPoint.LocalPosition.X, _endPoint.LocalPosition.Y); } PathRenderItem _slicePath; + + + List DebugItems; PathRenderItem _debugBoundsPath; + RectRenderItem _debugCircleCenter; bool ExistWithinRange(double target, double min, double max) { @@ -151,24 +154,12 @@ void CalculateWidthHeight(double prevSliceDegrees) ExtremePoints = new BoundingBox(minX, minY, maxX - minX, maxY - minY); ExtremePoints.Parent = _innerGroup.Bounds; - //if(Degrees > 0) - //{ - - //} - //if(Degrees) - //double xMax = Math.Max(_startPoint.Left, _endPoint.Left); - //xMax = Math.Max(xMax, _midPoint.Left); - - //double yMax = Math.Max(_startPoint.Top, _endPoint.Top); - //yMax = Math.Max(yMax, _midPoint.Top); - //double yMax; - //double xMin; - //double yMin; } private double _sliceScaleFactor = 1d; private double _scaledRadius { get { return _radius * _sliceScaleFactor; } } + private void CalculateExplosionDir() { var transformOriginLocal = new Vector2(_innerGroup.TransformOrigin.X, _innerGroup.TransformOrigin.Y); @@ -182,11 +173,6 @@ private void CalculateExplosionDir() CtrToOuterMidDir = new Vector2(pieDirection.X, pieDirection.Y); } - //internal Graphics.Math.Vector2 GetVectorCtrToEnd() - //{ - // //_circleCenter + _sc - //} - public PieSliceRenderItem(ChartRenderer renderer, BoundingBox parent, Point circleCenter, double radius, double percentOfPie, double prevSliceDegrees) : base(renderer) { Rectangle.Bounds.Parent = parent; @@ -249,6 +235,7 @@ internal void ImportPathData(BoundingBox plotAreaBounds, BoundingBox globalAreaB _slicePath.Commands.Add(lineToStart); _slicePath.Commands.Add(arcCommand); + //Change to != -1 to activate debug items if (position == -1) { //Visualize all points @@ -266,39 +253,59 @@ internal void ImportPathData(BoundingBox plotAreaBounds, BoundingBox globalAreaB /// private void AddDebugLines(PathCommands moveCenter, BoundingBox bounds) { - //var lineToMidPoint = new PathCommands(PathCommandType.Line, _slicePath, _midPoint.Left, _midPoint.Top); - //var lineToEnd = new PathCommands(PathCommandType.Line, _slicePath, _endPoint.Left, _endPoint.Top); - //_slicePath.Commands.Add(moveCenter); - //_slicePath.Commands.Add(lineToMidPoint); - //_slicePath.Commands.Add(moveCenter); - //_slicePath.Commands.Add(lineToEnd); + DebugItems = new List(); + + //Render bounds for slice _debugBoundsPath = new PathRenderItem(bounds); _debugBoundsPath.BorderColor = "red"; _debugBoundsPath.FillColor = "transparent"; _debugBoundsPath.BorderWidth = 3; - var moveCenterDebug = new PathCommands(PathCommandType.Move, _circleCenter.Left, _circleCenter.Top); + var moveCenterDebug = new PathCommands(PathCommandType.Move, ExtremePoints.Left, ExtremePoints.Top); _debugBoundsPath.Commands.Add(moveCenterDebug); //Draw extremes/bounds - var lineToTopLeft = new PathCommands(PathCommandType.Line, ExtremePoints.Left, ExtremePoints.Top); + //var lineToTopLeft = new PathCommands(PathCommandType.Line, ExtremePoints.Left, ExtremePoints.Top); var lineToTopRight = new PathCommands(PathCommandType.Line, ExtremePoints.Right, ExtremePoints.Top); - //var sliceCenter = GetSliceShapeCenterLocal(); - //var lineToSliceCenter = new PathCommands(PathCommandType.Line, _debugBoundsPath, sliceCenter.Left, sliceCenter.Top); - var lineToBottomRight = new PathCommands(PathCommandType.Line, ExtremePoints.Right, ExtremePoints.Bottom); var lineToBottomLeft = new PathCommands(PathCommandType.Line, ExtremePoints.Left, ExtremePoints.Bottom); var end = new PathCommands(PathCommandType.End, ExtremePoints.Left, ExtremePoints.Top); - _debugBoundsPath.Commands.Add(lineToTopLeft); _debugBoundsPath.Commands.Add(lineToTopRight); - ////_debugBoundsPath.Commands.Add(lineToSliceCenter); _debugBoundsPath.Commands.Add(lineToBottomRight); _debugBoundsPath.Commands.Add(lineToBottomLeft); - //_debugBoundsPath.Commands.Add(end); + _debugBoundsPath.Commands.Add(end); + + DebugItems.Add(_debugBoundsPath); + + //Render green dot at circle center + _debugCircleCenter = GenerateDebugPoint(bounds, new Coordinate(_circleCenter.Left, _circleCenter.Top), "green"); + DebugItems.Add(_debugCircleCenter); + + //render dots at points + var pointColor = "yellow"; + + var startDebug = GenerateDebugPoint(bounds, GetStartPointPositionLocal(), pointColor); + var midDebug = GenerateDebugPoint(bounds, GetMidPointLocal(), pointColor); + var endDebug = GenerateDebugPoint(bounds, GetEndPointLocal(), pointColor); + + DebugItems.Add(startDebug); + DebugItems.Add(midDebug); + DebugItems.Add(endDebug); } + private RectRenderItem GenerateDebugPoint(BoundingBox parent, Coordinate point, string fillColor) + { + double l = -2.5d; + double t = -2.5d; + double w = 5d; + double h = 5d; + + return new RectRenderItem(parent) { Left = l + point.X, Top = t + point.Y, Width = w, Height = h, FillColor = fillColor }; + } + + internal void ImportStlyeInfo(ExcelChartDataPoint dp, ExcelPieChart chartType) { _slicePath.SetDrawingPropertiesFill(ChartRenderer.Theme, dp.Fill, chartType.StyleManager.Style.DataPoint.FillReference.Color); @@ -308,15 +315,21 @@ internal void ImportStlyeInfo(ExcelChartDataPoint dp, ExcelPieChart chartType) internal void AppendGroupItem(GroupRenderItem group) { + //Apply translation after all calculations are done + _innerGroup.Left += _innerGroup.TranslationOffset.Left; + _innerGroup.Top += _innerGroup.TranslationOffset.Top; + //The slice items post transform operations _innerItems.AddChildItem(_slicePath); //The bounds and translations of the slice _innerGroup.AddChildItem(_innerItems); - if (_debugBoundsPath != null) + if (DebugItems != null && DebugItems.Count > 0) { - //adding the debug lines - _innerGroup.AddChildItem(_debugBoundsPath); + foreach(var debugItem in DebugItems) + { + _innerGroup.AddChildItem(debugItem); + } } //The group containing all slices @@ -476,12 +489,6 @@ Coordinate GetFinalLocalTranslation(Vector2 LocalTranslationVector, Vector2 loca //The length is within the bounds. No binding neccesary. translationLeft = LocalTranslationVector.X; translationTop = LocalTranslationVector.Y; - - //translationLeft = Math.Min(translateX, maxTranslationXWorld); - //translationTop = Math.Min(translateY, maxTranslationY); - - //translationLeft = Math.Max(translationLeft, minTranslationXWorld); - //translationTop = Math.Max(translationTop, minTranslationYWorld); } return new Coordinate(translationLeft, translationTop); @@ -519,6 +526,24 @@ internal Coordinate GetOuterMidpointInGlobalCoords() return new Coordinate(_midPoint.Position.X, _midPoint.Position.Y); } + + internal Transform CopyOuterMidPoint() + { + var transform = new Transform(); + transform.Parent = _midPoint.Parent; + transform.Position = transform.Position + _midPoint.LocalPosition; + return transform; + } + + internal Transform CopyStartPoint() + { + var translationVector = GetLocalTranslationVector(100); + var transform = new Transform(); + transform.Parent = _midPoint.Parent; + transform.Position = transform.Position + _midPoint.LocalPosition; + return transform; + } + internal GroupRenderItem GetInnerItemGroup() { return _innerGroup; @@ -533,6 +558,18 @@ internal Coordinate GetInnerGroupTransformOriginTranslated() { return new Coordinate(_innerGroup.TransformOrigin.X + _innerGroup.TranslationOffset.Left, _innerGroup.TransformOrigin.Y + _innerGroup.TranslationOffset.Top); } + /// + /// Transform origin in local coordinates + /// Translated + /// + /// + internal Transform GetInnerGroupWithTransformOriginTranslated() + { + Transform transform = new Transform(); + transform.Parent = _innerGroup.Bounds.Parent; + transform.LocalPosition += new Vector2(_innerGroup.TransformOrigin.X + _innerGroup.TranslationOffset.Left, _innerGroup.TransformOrigin.Y + _innerGroup.TranslationOffset.Top); + return transform; + } public override void AppendRenderItems(List renderItems) { diff --git a/src/EPPlus/Drawing/Renderer/ChartRenderer.cs b/src/EPPlus/Drawing/Renderer/ChartRenderer.cs index e37f6fec9a..378b8d339e 100644 --- a/src/EPPlus/Drawing/Renderer/ChartRenderer.cs +++ b/src/EPPlus/Drawing/Renderer/ChartRenderer.cs @@ -84,15 +84,25 @@ private void SetAxisPositionsFromPlotarea() if (VerticalAxis != null) { PlaceVerticalAxis(VerticalAxis); + //Make sure the horizontal axis is moved up if the vertical axis has a negative minimum value, so that the 0 value is at the correct position. + if (VerticalAxis.Axis.TickLabelPosition == eTickLabelPosition.NextTo && HorizontalAxis.Axis.AxisType == eAxisType.Val && HorizontalAxis.Min < 0D) + { + Plotarea.Rectangle.Width += VerticalAxis.Rectangle.Width; + Plotarea.Group.Left = VerticalAxis.Rectangle.Left; + var newRight = Plotarea.Group.Left + HorizontalAxis.GetPositionInPlotarea(0D); + var rightDiff = newRight - VerticalAxis.Rectangle.Width; + VerticalAxis.Rectangle.Left = rightDiff; + VerticalAxis.Line.X1 = VerticalAxis.Line.X2 = newRight; + } VerticalAxis.AddTickmarksAndValues(DefItems); } if (HorizontalAxis != null && HorizontalAxis.Rectangle != null) { - PlaceHorizontalAxis(HorizontalAxis); + PlaceHorizontalAxis(HorizontalAxis, false); //Make sure the horizontal axis is moved up if the vertical axis has a negative minimum value, so that the 0 value is at the correct position. - if (VerticalAxis.Axis.AxisType == eAxisType.Val && VerticalAxis.Min < 0D) + if (HorizontalAxis.Axis.TickLabelPosition == eTickLabelPosition.NextTo && VerticalAxis.Axis.AxisType == eAxisType.Val && VerticalAxis.Min < 0D) { var newtop = VerticalAxis.GetPositionInPlotarea(0D) + Plotarea.Group.Top; var topDiff = HorizontalAxis.Rectangle.Top - newtop; @@ -112,12 +122,12 @@ private void SetAxisPositionsFromPlotarea() if (SecondHorizontalAxis != null && SecondHorizontalAxis.Rectangle != null) { - PlaceHorizontalAxis(SecondHorizontalAxis); + PlaceHorizontalAxis(SecondHorizontalAxis, true); SecondHorizontalAxis.AddTickmarksAndValues(DefItems); } } - private void PlaceHorizontalAxis(ChartAxisRenderer horizontalAxis) + private void PlaceHorizontalAxis(ChartAxisRenderer horizontalAxis, bool isSecondary) { if (horizontalAxis.Axis.Deleted == false) { @@ -125,60 +135,143 @@ private void PlaceHorizontalAxis(ChartAxisRenderer horizontalAxis) horizontalAxis.Rectangle.Left = Plotarea.Group.Left; horizontalAxis.Line.X1 = (float)horizontalAxis.Rectangle.Left; horizontalAxis.Line.X2 = (float)horizontalAxis.Rectangle.Right; - - if (horizontalAxis.Axis.AxisPosition == eAxisPosition.Bottom) + + var axisPos = horizontalAxis.Axis.ActualAxisPosition; + if (axisPos == eActualAxisPosition.Bottom) { horizontalAxis.Rectangle.Top = Plotarea.Group.Top + Plotarea.Rectangle.Height; horizontalAxis.Line.Y1 = horizontalAxis.Line.Y2 = (float)Plotarea.Group.Top + Plotarea.Rectangle.Height; } - else + else if(axisPos == eActualAxisPosition.BottomSecond) { - horizontalAxis.Rectangle.Top = Plotarea.Rectangle.Top - horizontalAxis.Rectangle.Height; + horizontalAxis.Rectangle.Top = Plotarea.Group.Top + Plotarea.Rectangle.Height + HorizontalAxis.Rectangle.Height; + horizontalAxis.Line.Y1 = horizontalAxis.Line.Y2 = horizontalAxis.Rectangle.Top; + } + else if(axisPos == eActualAxisPosition.Top) + { + horizontalAxis.Rectangle.Top = Plotarea.Group.Top - horizontalAxis.Rectangle.Height; horizontalAxis.Line.Y1 = horizontalAxis.Line.Y2 = (float)Plotarea.Group.Top; } + else + { + horizontalAxis.Rectangle.Top = Plotarea.Group.Top - horizontalAxis.Rectangle.Height - HorizontalAxis.Rectangle.Height; + horizontalAxis.Line.Y1 = horizontalAxis.Line.Y2 = horizontalAxis.Rectangle.Bottom; + } } if (horizontalAxis.Title != null) { - horizontalAxis.Title.Rectangle.Height = Bounds.Height / 4; - horizontalAxis.Title.Rectangle.Width = horizontalAxis.Rectangle?.Width ?? Plotarea.Rectangle.Width; - //horizontalAxis.Title.InitTextBox(); + PlaceHorizontalAxisTitle(horizontalAxis); + } + } + + private void PlaceHorizontalAxisTitle(ChartAxisRenderer horizontalAxis) + { + horizontalAxis.Title.Rectangle.Height = Bounds.Height / 4; + horizontalAxis.Title.Rectangle.Width = horizontalAxis.Rectangle?.Width ?? Plotarea.Rectangle.Width; + if (horizontalAxis.Axis.Deleted) + { + if (horizontalAxis.Axis.AxisPosition == eAxisPosition.Bottom) + { + horizontalAxis.Title.TextBox.Top = Plotarea.Group.Top + Plotarea.Rectangle.Height; + } + else + { + horizontalAxis.Title.TextBox.Top = Plotarea.Group.Top - horizontalAxis.Rectangle.Height; + } + } + else + { if (horizontalAxis.Axis.AxisPosition == eAxisPosition.Bottom) { - horizontalAxis.Title.TextBox.Top = horizontalAxis.Rectangle.Bottom; + if (SecondHorizontalAxis != null && SecondHorizontalAxis.Axis.ActualAxisPosition == eActualAxisPosition.BottomSecond) + { + horizontalAxis.Title.TextBox.Top = horizontalAxis.Rectangle.Bottom + SecondHorizontalAxis.Rectangle.Height; + } + else if (horizontalAxis.Axis.ActualAxisPosition == eActualAxisPosition.Bottom) + { + horizontalAxis.Title.TextBox.Top = horizontalAxis.Rectangle.Bottom; + } + else + { + if (Legend != null && Chart.Legend.Position == eLegendPosition.Top) + { + horizontalAxis.Title.TextBox.Top = Legend.Rectangle.Top - horizontalAxis.Title.Rectangle.Height; + } + else + { + horizontalAxis.Title.TextBox.Top = ChartArea.Rectangle.Bottom - horizontalAxis.Title.Rectangle.Height - ChartArea.BottomMargin; + } + } } else { - horizontalAxis.Title.TextBox.Top = horizontalAxis.Rectangle.Top - horizontalAxis.Title.TextBox.Height; + if (SecondHorizontalAxis.Axis.ActualAxisPosition == eActualAxisPosition.TopSecond) + { + horizontalAxis.Title.TextBox.Top = horizontalAxis.Rectangle.Top - SecondHorizontalAxis.Rectangle.Height - horizontalAxis.Title.TextBox.Height; + } + else if (horizontalAxis.Axis.ActualAxisPosition == eActualAxisPosition.Top) + { + horizontalAxis.Title.TextBox.Top = horizontalAxis.Rectangle.Top - horizontalAxis.Title.TextBox.Height; + } + else + { + if (Legend != null && Chart.Legend.Position == eLegendPosition.Top) + { + horizontalAxis.Title.TextBox.Top = Legend.Rectangle.Bottom; + } + else if (Title != null) + { + horizontalAxis.Title.TextBox.Top = Title.Rectangle.Bottom; + } + else + { + horizontalAxis.Title.TextBox.Top = ChartArea.TopMargin; + } + } } - horizontalAxis.Title.TextBox.Left = Plotarea.Group.Left + (Plotarea.Rectangle.Width / 2) - (horizontalAxis.Title.TextBox.Width / 2); } + horizontalAxis.Title.TextBox.Left = Plotarea.Group.Left + (Plotarea.Rectangle.Width / 2) - (horizontalAxis.Title.TextBox.Width / 2); } private void PlaceVerticalAxis(ChartAxisRenderer verticalAxis) { - if(verticalAxis.Axis.Deleted==false && verticalAxis.Rectangle != null) + if (verticalAxis.Axis.Deleted == false && verticalAxis.Rectangle != null) { verticalAxis.Rectangle.Top = Plotarea.Group.Top; verticalAxis.Rectangle.Height = Plotarea.Rectangle.Height; verticalAxis.Line.Y1 = (float)verticalAxis.Rectangle.Top; verticalAxis.Line.Y2 = (float)verticalAxis.Rectangle.Bottom; - if (verticalAxis.Axis.AxisPosition == eAxisPosition.Left) + var axisPos = verticalAxis.Axis.ActualAxisPosition; + + if (axisPos == eActualAxisPosition.Left) { verticalAxis.Rectangle.Left = Plotarea.Group.Left - verticalAxis.Rectangle.Width; verticalAxis.Line.X1 = verticalAxis.Line.X2 = (float)Plotarea.Group.Left; } + else if (axisPos == eActualAxisPosition.LeftSecond) + { + verticalAxis.Rectangle.Left = Plotarea.Group.Left - verticalAxis.Rectangle.Width - VerticalAxis.Rectangle.Width; + verticalAxis.Line.X1 = verticalAxis.Line.X2 = (float)Plotarea.Group.Left; + } + else if (axisPos == eActualAxisPosition.Right) + { + verticalAxis.Rectangle.Left = Plotarea.Group.Left + Plotarea.Rectangle.Width; + verticalAxis.Line.X1 = verticalAxis.Line.X2 = (float)Plotarea.Group.Left + Plotarea.Rectangle.Width; + } else { - verticalAxis.Rectangle.Left = Plotarea.Group.Left+Plotarea.Rectangle.Width; + verticalAxis.Rectangle.Left = Plotarea.Group.Left + Plotarea.Rectangle.Width + VerticalAxis.Rectangle.Width; verticalAxis.Line.X1 = verticalAxis.Line.X2 = (float)Plotarea.Group.Left + Plotarea.Rectangle.Width; } } + PlaceVerticalAxisTitle(verticalAxis); + } + + private void PlaceVerticalAxisTitle(ChartAxisRenderer verticalAxis) + { if (verticalAxis.Title != null) { - //verticalAxis.Title.Rectangle.Height = Plotarea.Rectangle.Height; - //verticalAxis.Title.Rectangle.Width = sc.Bounds.Width / 4; - //verticalAxis.Title.InitTextBox(); var sinRot = Math.Abs(Math.Sin(MathHelper.Radians(verticalAxis.Title.TextBox.Rotation))); var cosRot = Math.Abs(Math.Cos(MathHelper.Radians(verticalAxis.Title.TextBox.Rotation))); verticalAxis.Title.TextBox.Top = Plotarea.Group.Top + (Plotarea.Rectangle.Height / 2) + ((verticalAxis.Title.TextBox.Height * cosRot + verticalAxis.Title.TextBox.Width * sinRot) / 2); @@ -191,7 +284,7 @@ private void PlaceVerticalAxis(ChartAxisRenderer verticalAxis) } else { - verticalAxis.Title.TextBox.Left = verticalAxis.Rectangle.Left - verticalAxis.Title.TextBox.GetActualWidth() - 1.5; + verticalAxis.Title.TextBox.Left = Plotarea.Group.Left - verticalAxis.Rectangle.Width - verticalAxis.Title.TextBox.GetActualWidth() - 1.5; } } else @@ -213,9 +306,10 @@ private void SetChartArea() var item = new ChartAreaRenderer(this); item.Rectangle.Width = Bounds.Width; item.Rectangle.Height = Bounds.Height; - item.Rectangle.SetDrawingPropertiesFill(Theme, Chart.Fill, Chart.StyleManager.Style.ChartArea.FillReference.Color); - item.Rectangle.SetDrawingPropertiesBorder(Theme, Chart.Border, Chart.StyleManager.Style.ChartArea.BorderReference.Color, Chart.Border.Width > 0); + item.Rectangle.SetDrawingPropertiesFill(Theme, Chart.Fill, Chart.StyleManager.Style?.ChartArea.FillReference.Color, Theme.ColorScheme.Light1); + item.Rectangle.SetDrawingPropertiesBorder(Theme, Chart.Border, Chart.StyleManager.Style?.ChartArea.BorderReference.Color, Chart.Border.Width > 0); item.AppendRenderItems(RenderItems); + item.SetMargins(Chart.TextBody); ChartArea = item; } private ChartAxisRenderer GetAxis(bool vertical, int offset = 0) diff --git a/src/EPPlus/Drawing/Renderer/DrawingRenderer.cs b/src/EPPlus/Drawing/Renderer/DrawingRenderer.cs index 44eda3615e..9f1f5d2a33 100644 --- a/src/EPPlus/Drawing/Renderer/DrawingRenderer.cs +++ b/src/EPPlus/Drawing/Renderer/DrawingRenderer.cs @@ -11,6 +11,7 @@ Date Author Change 27/11/2025 EPPlus Software AB EPPlus 9 *************************************************************************************************/ +using EPPlus.DrawingRenderer; using EPPlus.DrawingRenderer.RenderItems; using EPPlus.Export.ImageRenderer.Utils; using EPPlus.Export.Utils; @@ -33,8 +34,9 @@ internal DrawingRenderer(ExcelDrawing drawing) var wb = drawing._drawings.Worksheet.Workbook; Theme = wb.ThemeManager.GetOrCreateTheme(); + RenderContext = wb.RenderContext; - var shaper = OpenTypeFonts.GetTextShaper(Theme.FontScheme.MajorFont[0].Typeface); + var shaper = RenderContext.FontEngine.GetTextShaper(Theme.FontScheme.MajorFont[0].Typeface); TextMeasurer = new OpenTypeFontTextMeasurer(shaper); } @@ -53,6 +55,7 @@ internal DrawingRenderer() public ExcelTheme Theme { get;} public ExcelWorkbook Workbook => Drawing._drawings.Worksheet.Workbook; internal ITextMeasurer TextMeasurer { get; } + internal RenderContext RenderContext { get; } public List RenderItems { get; } = new List(); internal BoundingBox Bounds = new BoundingBox(); } diff --git a/src/EPPlus/Drawing/Renderer/RenderItems/DrawingRenderItemExtentions.cs b/src/EPPlus/Drawing/Renderer/RenderItems/DrawingRenderItemExtentions.cs index b99f7e277d..7a5099505c 100644 --- a/src/EPPlus/Drawing/Renderer/RenderItems/DrawingRenderItemExtentions.cs +++ b/src/EPPlus/Drawing/Renderer/RenderItems/DrawingRenderItemExtentions.cs @@ -31,7 +31,7 @@ internal enum SvgFillType } internal static class DrawingRenderItemExtentions { - internal static void SetDrawingPropertiesFill(this RenderItem item, ExcelTheme theme, ExcelDrawingFill fill, ExcelDrawingColorManager color) + internal static void SetDrawingPropertiesFill(this RenderItem item, ExcelTheme theme, ExcelDrawingFill fill, ExcelDrawingColorManager color, ExcelDrawingThemeColorManager nullColor=null) { switch (fill.Style) { @@ -43,19 +43,19 @@ internal static void SetDrawingPropertiesFill(this RenderItem item, ExcelTheme t item.BlipFill = new DrawingRenderBlipFill(fill.BlipFill); break; default: - SetDrawingPropertiesFillBasic(item, theme, fill, color); + SetDrawingPropertiesFillBasic(item, theme, fill, color, nullColor ?? theme.ColorScheme.Accent1); break; } } - internal static void SetDrawingPropertiesFillBasic(this RenderItem item, ExcelTheme theme, ExcelDrawingFillBasic fill, ExcelDrawingColorManager color) + internal static void SetDrawingPropertiesFillBasic(this RenderItem item, ExcelTheme theme, ExcelDrawingFillBasic fill, ExcelDrawingColorManager color, ExcelDrawingThemeColorManager nullColor) { double? opacity = null; switch (fill.Style) { case eFillStyle.NoFill: - if (fill.IsEmpty) + if (fill.IsEmpty) //Do NOT remove. This if is required for Shapes { - item.FillColor = GetFillColor(theme, fill, color, item.FillColorSource, out opacity); + item.FillColor = GetFillColor(theme, fill, color, item.FillColorSource, out opacity, nullColor); } else { @@ -78,26 +78,36 @@ internal static void SetDrawingPropertiesFillBasic(this RenderItem item, ExcelTh internal static void SetDrawingPropertiesBorder(this RenderItem item, ExcelTheme theme, ExcelDrawingBorder border, ExcelChartStyleColorManager color, bool hasBorder, double defaultWidth = 1.5) { double? opacity = null; - switch (border.Fill.Style) + if (border == null) { - case eFillStyle.NoFill: - if (border.Fill.IsEmpty) - { + if (hasBorder) + { + item.BorderColor = GetFillColor(theme, null, color, item.BorderColorSource, out opacity, theme.ColorScheme.Dark1); + } + } + else + { + switch (border.Fill.Style) + { + case eFillStyle.NoFill: + if (border.Fill.IsEmpty) + { + item.BorderColor = GetFillColor(theme, border.Fill, color, item.BorderColorSource, out opacity); + } + else + { + item.BorderColor = "none"; + } + break; + case eFillStyle.SolidFill: item.BorderColor = GetFillColor(theme, border.Fill, color, item.BorderColorSource, out opacity); - } - else - { - item.BorderColor = "none"; - } - break; - case eFillStyle.SolidFill: - item.BorderColor = GetFillColor(theme, border.Fill, color, item.BorderColorSource, out opacity); - item.BorderGradientFill = null; - break; - case eFillStyle.GradientFill: - item.BorderGradientFill = new RenderGradientFill(); - item.BorderColor = null; - break; + item.BorderGradientFill = null; + break; + case eFillStyle.GradientFill: + item.BorderGradientFill = new RenderGradientFill(); + item.BorderColor = null; + break; + } } if (opacity.HasValue) @@ -107,12 +117,12 @@ internal static void SetDrawingPropertiesBorder(this RenderItem item, ExcelTheme if (hasBorder && item.BorderColorSource != PathFillMode.None) { - item.BorderWidth = border.Width == 0 ? defaultWidth : border.Width; - if (border.LineStyle.HasValue && border.LineStyle != eLineStyle.Solid) + item.BorderWidth = (border?.Width??0D) == 0D ? defaultWidth : border.Width; + if (border!=null && border.LineStyle.HasValue && border.LineStyle != eLineStyle.Solid) { - item.BorderDashArray = GetDashArray(border); + item.BorderDashArray = GetDashArray(border, item.BorderWidth.Value); } - if (border.CompoundLineStyle != eCompoundLineStyle.Single) + if (border != null && border.CompoundLineStyle != eCompoundLineStyle.Single) { item.CompoundLineStyle = (CompoundLineStyle)border.CompoundLineStyle; //TODO:Add support double compound borders. @@ -137,9 +147,9 @@ internal static void SetDrawingPropertiesEffects(this RenderItem item, ExcelThem } } - private static double[] GetDashArray(ExcelDrawingBorder border) + private static double[] GetDashArray(ExcelDrawingBorder border, double width) { - var lw = (int)Math.Round(border.Width * ExcelDrawing.EMU_PER_POINT / ExcelDrawing.EMU_PER_PIXEL); + var lw = (int)Math.Round(width * ExcelDrawing.EMU_PER_POINT / ExcelDrawing.EMU_PER_PIXEL); switch (border.LineStyle) { case eLineStyle.Dot: @@ -166,7 +176,7 @@ private static double[] GetDashArray(ExcelDrawingBorder border) return null; } - private static string GetFillColor(ExcelTheme theme, ExcelDrawingFillBasic fill, ExcelDrawingColorManager styleFillColor, PathFillMode fillColorSource, out double? opacity) + private static string GetFillColor(ExcelTheme theme, ExcelDrawingFillBasic fill, ExcelDrawingColorManager styleFillColor, PathFillMode fillColorSource, out double? opacity, ExcelDrawingThemeColorManager nullColor = null) { opacity = null; if (fillColorSource == PathFillMode.None) @@ -179,7 +189,7 @@ private static string GetFillColor(ExcelTheme theme, ExcelDrawingFillBasic fill, { if (styleFillColor == null) { - fc = tc.ColorConverter.GetThemeColor(theme.ColorScheme.Accent1); + fc = tc.ColorConverter.GetThemeColor(nullColor ?? theme.ColorScheme.Accent1); } else { @@ -188,7 +198,8 @@ private static string GetFillColor(ExcelTheme theme, ExcelDrawingFillBasic fill, } else if (fill.Style == eFillStyle.SolidFill) { - fc = tc.ColorConverter.GetThemeColor(theme, fill.SolidFill.Color); + //Send in styleFill as well since a solid fill can refer to style color + fc = tc.ColorConverter.GetThemeColor(theme, fill.SolidFill.Color, styleFillColor); } else { diff --git a/src/EPPlus/Drawing/Renderer/RenderItems/Fill/DrawingRenderGradientFill.cs b/src/EPPlus/Drawing/Renderer/RenderItems/Fill/DrawingRenderGradientFill.cs index 67e96a4044..c1ef784bf0 100644 --- a/src/EPPlus/Drawing/Renderer/RenderItems/Fill/DrawingRenderGradientFill.cs +++ b/src/EPPlus/Drawing/Renderer/RenderItems/Fill/DrawingRenderGradientFill.cs @@ -50,6 +50,11 @@ public DrawingRenderGradientFill(ExcelTheme theme, ExcelDrawingGradientFill grad { LinearSettings.Angle = gradientFill.LinearSettings.Angle; LinearSettings.Scaled = gradientFill.LinearSettings.Scaled; + UserSpaceOnUse = false; + } + else + { + UserSpaceOnUse = true; } } } diff --git a/src/EPPlus/Drawing/Renderer/RenderItems/Textbox/DrawingParagraphRenderItem.cs b/src/EPPlus/Drawing/Renderer/RenderItems/Textbox/DrawingParagraphRenderItem.cs index 6bc07e58c6..804abe2ef5 100644 --- a/src/EPPlus/Drawing/Renderer/RenderItems/Textbox/DrawingParagraphRenderItem.cs +++ b/src/EPPlus/Drawing/Renderer/RenderItems/Textbox/DrawingParagraphRenderItem.cs @@ -25,9 +25,10 @@ internal class DrawingParagraphRenderItem : ParagraphRenderItem /// /// /// - public DrawingParagraphRenderItem(DrawingTextbody textBody, BoundingBox parent) : base(parent, textBody) + public DrawingParagraphRenderItem(RenderContext renderContext, DrawingTextBody textBody, BoundingBox parent) + : base(renderContext, parent, textBody) { - ParagraphLineSpacing = GetParagraphLineSpacingInPoints(100, (TextShaper)OpenTypeFonts.GetShaperForFont(DefaultParagraphFont), DefaultParagraphFont.Size); + ParagraphLineSpacing = GetParagraphLineSpacingInPoints(100, (TextShaper)RenderContext.FontEngine.GetShaperForFont(DefaultParagraphFont), DefaultParagraphFont.Size); } /// @@ -36,7 +37,8 @@ public DrawingParagraphRenderItem(DrawingTextbody textBody, BoundingBox parent) /// /// /// - public DrawingParagraphRenderItem(DrawingTextbody textBody, BoundingBox parent, string text) : this(textBody, parent) + public DrawingParagraphRenderItem(RenderContext renderContext, DrawingTextBody textBody, BoundingBox parent, string text) + : this(renderContext, textBody, parent) { ImportLinesAndTextRunsDefault(text); } @@ -48,22 +50,18 @@ public DrawingParagraphRenderItem(DrawingTextbody textBody, BoundingBox parent, /// /// /// - public DrawingParagraphRenderItem(DrawingTextbody textBody, BoundingBox parent, ExcelDrawingParagraph p, string textIfEmpty = null) : base(parent, textBody, false) + public DrawingParagraphRenderItem(RenderContext renderContext, DrawingTextBody textBody, BoundingBox parent, ExcelDrawingParagraph p, string textIfEmpty = null) + : base(renderContext, parent, textBody, false) { IsFirstParagraph = p == p._paragraphs[0]; ImportStyleInfo(textBody, p); - + HorizontalAlignment = (TextAlignment)(int)p.HorizontalAlignment; ImportMarginAndIndent(p); - ImportAlignment(textBody.AutoSize, textBody.MaxWidth, parent.Width); - //---Initialize / calculate lines and runs--- - //measurer must be set before AddLinesAndRichText DefaultParagraphFont = new FontFormatBase(p.DefaultRunProperties.GetMeasureFont()); - //---Calculate linespacing--- ImportLineSpacing(p.LineSpacing.LineSpacingType, p.LineSpacing.Value); - //Import textruns or fallback text ImportLinesAndTextRuns(p, textIfEmpty); } @@ -132,7 +130,7 @@ void GenerateRichText(ExcelDrawingTextRunCollection runs/*, List } } - private void ImportStyleInfo(DrawingTextbody textBody, ExcelDrawingParagraph p) + private void ImportStyleInfo(DrawingTextBody textBody, ExcelDrawingParagraph p) { //If this paragraph has defaults of its own enter here if (p.DefaultRunProperties.Fill != null && p.DefaultRunProperties.Fill.IsEmpty == false) @@ -192,37 +190,37 @@ private void ImportMarginAndIndent(ExcelDrawingParagraph p) LeftMargin = LeftMargin.PixelToPoint(); RightMargin = RightMargin.PixelToPoint(); - HorizontalAlignment = (TextAlignment)p.HorizontalAlignment; + _alignment = (TextAlignment)p.HorizontalAlignment; LeftMargin = LeftMargin.PixelToPoint(); RightMargin = RightMargin.PixelToPoint(); } private void ImportAlignment(bool isAutoSize, double maxWidth, double parentWidth) { - if (isAutoSize == false) - { - Bounds.Left = 0; - Bounds.Width = ParentMaxWidth; + //if (isAutoSize == false) + //{ + //Bounds.Left = 0; + //Bounds.Width = ParentMaxWidth; - //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 - Bounds.Left = GetAlignmentHorizontal(TextAlignment.Left); - if (HorizontalAlignment == TextAlignment.Center) - { - //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 - _centerAdjustment = GetAlignmentHorizontal(HorizontalAlignment); - } - Bounds.Width = parentWidth - RightMargin - LeftMargin; - } + ////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 + //Bounds.Left = GetAlignmentHorizontal(TextAlignment.Left); + //if (HorizontalAlignment == TextAlignment.Center) + //{ + // //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 + // _centerAdjustment = GetAlignmentHorizontal(HorizontalAlignment); + //} + //Bounds.Width = parentWidth - RightMargin - LeftMargin; + //} } private void ImportLineSpacing(eDrawingTextLineSpacing lsType, double lineSpacingValue) { _lsType = (TextLineSpacing)lsType; - var shaper = (TextShaper)OpenTypeFonts.GetShaperForFont(DefaultParagraphFont); + var shaper = (TextShaper)RenderContext.FontEngine.GetShaperForFont(DefaultParagraphFont); ParagraphLineSpacing = GetParagraphLineSpacingInPoints( lineSpacingValue, diff --git a/src/EPPlus/Drawing/Renderer/RenderItems/Textbox/DrawingTextBody.cs b/src/EPPlus/Drawing/Renderer/RenderItems/Textbox/DrawingTextBody.cs index 151c7c2d44..3cbc496c86 100644 --- a/src/EPPlus/Drawing/Renderer/RenderItems/Textbox/DrawingTextBody.cs +++ b/src/EPPlus/Drawing/Renderer/RenderItems/Textbox/DrawingTextBody.cs @@ -1,4 +1,5 @@ -using EPPlus.DrawingRenderer.RenderItems; +using EPPlus.DrawingRenderer; +using EPPlus.DrawingRenderer.RenderItems; using EPPlus.DrawingRenderer.RenderItems.SvgItem; using EPPlus.Export.ImageRenderer.RenderItems.Shared; using EPPlus.Fonts.OpenType.Integration.DataHolders; @@ -20,20 +21,20 @@ namespace OfficeOpenXml.Drawing.Renderer.TextBox { - public class DrawingTextbody : RenderTextBody + public class DrawingTextBody : RenderTextBody { internal ExcelDrawing _drawing; internal ExcelTheme Theme { get; } - public DrawingTextbody(ExcelDrawing drawing, BoundingBox parent, bool autoSize, bool clampedToParent = false) : base(parent, autoSize) + public DrawingTextBody(RenderContext renderContext, ExcelDrawing drawing, BoundingBox parent, bool autoSize, bool clampedToParent = false) : base(renderContext, parent, autoSize) { _drawing = drawing; Theme = drawing._drawings.Worksheet.Workbook.ThemeManager.GetOrCreateTheme(); MaxWidth = parent.Width; MaxHeight = parent.Height; } - public DrawingTextbody(ExcelDrawing drawing, BoundingBox parent, double left, double top, double maxWidth, double maxHeight, bool clampedToParent = false, bool autoSize=false) : base(parent, autoSize) + public DrawingTextBody(RenderContext renderContext, ExcelDrawing drawing, BoundingBox parent, double left, double top, double maxWidth, double maxHeight, bool clampedToParent = false, bool autoSize=false) : base(renderContext, parent, autoSize) { _drawing = drawing; Theme = drawing._drawings.Worksheet.Workbook.ThemeManager.GetOrCreateTheme(); @@ -52,14 +53,7 @@ public void ImportParagraph(ExcelDrawingParagraph item, double startingY, string var paragraph = CreateParagraph(this, item, Bounds, text); paragraph.Bounds.Name = $"Container{Paragraphs.Count}"; - //if (startingY < 0) - //{ - // paragraph.Bounds.Top = GetAlignmentVertical(); - //} - //else - //{ - paragraph.Bounds.Top = startingY; - //} + paragraph.Bounds.Top = startingY; if (AutoSize) { @@ -78,12 +72,16 @@ public void ImportParagraph(ExcelDrawingParagraph item, double startingY, string } } Paragraphs.Add(paragraph); + RecalculateParagraphs(); } + //Horizontal alignment should technically be set directly in paragraph + //But as the text is not measured until a paragraph has been imported an autosized textbody + //does not know its maximum size until all its paragraphs has been imported + //thus after all have we need to adjust. + //The alternative would be to perform the performance heavy measurement twice. internal void SetHorizontalAlignmentPosition() { - //if (AutoSize) - //{ foreach (var p in Paragraphs) { switch (p.HorizontalAlignment) @@ -105,7 +103,25 @@ internal void SetHorizontalAlignmentPosition() break; } } - //} + } + + internal TextAlignment TranslateHorizontalPosition(ExcelHorizontalAlignment alignment) + { + switch (alignment) + { + case ExcelHorizontalAlignment.Left: + return TextAlignment.Left; + case ExcelHorizontalAlignment.Center: + return TextAlignment.Center; + case ExcelHorizontalAlignment.Right: + return TextAlignment.Right; + case ExcelHorizontalAlignment.Distributed: + case ExcelHorizontalAlignment.CenterContinuous: + case ExcelHorizontalAlignment.Justify: + case ExcelHorizontalAlignment.General: + default: + return TextAlignment.Left; //TODO: Set left for now as we do not support distributed spacing yet + } } internal virtual void ImportTextBodyAndParagraphs(ExcelTextBody body, ExcelHorizontalAlignment horizontalDefault = ExcelHorizontalAlignment.Left) @@ -117,6 +133,8 @@ internal virtual void ImportTextBodyAndParagraphs(ExcelTextBody body, ExcelHoriz double currentHeight = 0; double largestWidth = double.MinValue; + //var defaultAlignment = TranslateHorizontalPosition(horizontalDefault); + body.GetInsetsInPoints(out double left, out double top, out double right, out double bottom); if (AutoSize == false) @@ -128,24 +146,34 @@ internal virtual void ImportTextBodyAndParagraphs(ExcelTextBody body, ExcelHoriz MaxHeight = MaxHeight - top - bottom; MaxWidth = MaxWidth - left - right; + Height = MaxHeight; + Width = MaxWidth; } foreach (var paragraph in body.Paragraphs) { ImportParagraph(paragraph, currentHeight); var addedPara = Paragraphs.Last(); + //addedPara.HorizontalAlignment = defaultAlignment; + currentHeight = addedPara.Bounds.Bottom; largestWidth = Math.Max(largestWidth, addedPara.Bounds.Width); } - foreach (var paragraph in body.Paragraphs) + + if (Paragraphs != null && Paragraphs.Count() > 0 && AutoSize) { - SetHorizontalAlignmentPosition(); + Bounds.Height = currentHeight; } - if (Paragraphs != null && Paragraphs.Count() > 0) + //Ensure contentBounds are calculated and paragraphs don't overlap + RecalculateParagraphs(); + + //Alignment adjustment for e.g. ChartTitles one paragraph may be longer than another + //Therefore as paragraphs have no awareness of eachother we must compare and adjust + foreach (var paragraph in body.Paragraphs) { - Bounds.Height = currentHeight; + SetHorizontalAlignmentPosition(); } Bounds.Top = GetAlignmentVertical(); @@ -176,14 +204,14 @@ internal virtual void ImportTextBodyAndParagraphs(ExcelTextBody body, ExcelHoriz // renderItems.Add(new SvgEndGroupItem(DrawingRenderer, Bounds)); //} - internal DrawingParagraphRenderItem CreateParagraph(DrawingTextbody textBody, BoundingBox parent) + internal DrawingParagraphRenderItem CreateParagraph(DrawingTextBody textBody, BoundingBox parent) { - return new DrawingParagraphRenderItem(textBody, parent); + return new DrawingParagraphRenderItem(RenderContext, textBody, parent); } - internal DrawingParagraphRenderItem CreateParagraph(DrawingTextbody textBody, ExcelDrawingParagraph paragraph, BoundingBox parent, string textIfEmpty = null) + internal DrawingParagraphRenderItem CreateParagraph(DrawingTextBody textBody, ExcelDrawingParagraph paragraph, BoundingBox parent, string textIfEmpty = null) { - return new DrawingParagraphRenderItem(textBody, parent, paragraph, textIfEmpty); + return new DrawingParagraphRenderItem(RenderContext, textBody, parent, paragraph, textIfEmpty); } /// @@ -194,12 +222,12 @@ internal DrawingParagraphRenderItem CreateParagraph(DrawingTextbody textBody, Ex /// protected override ParagraphRenderItem CreateParagraph(BoundingBox parent, string textIfEmpty = "") { - return new DrawingParagraphRenderItem(this, parent, textIfEmpty); + return new DrawingParagraphRenderItem(RenderContext, this, parent, textIfEmpty); } protected override ParagraphRenderItem CreateParagraph(BoundingBox parent, IRichTextFormatSimple richText) { - var paragraph = new SvgParagraphRenderItem(this, parent, "", false); + var paragraph = new SvgParagraphRenderItem(RenderContext, this, parent, "", false); paragraph.AddRichText(richText); return paragraph; } diff --git a/src/EPPlus/Drawing/Renderer/RenderItems/Textbox/DrawingTextBox.cs b/src/EPPlus/Drawing/Renderer/RenderItems/Textbox/DrawingTextBox.cs index 9bdb7605d2..13a6eac205 100644 --- a/src/EPPlus/Drawing/Renderer/RenderItems/Textbox/DrawingTextBox.cs +++ b/src/EPPlus/Drawing/Renderer/RenderItems/Textbox/DrawingTextBox.cs @@ -22,11 +22,11 @@ internal DrawingTextBox(ExcelDrawing drawing, BoundingBox parent, double left, d private void Init(ExcelDrawing drawing, BoundingBox parent, double maxWidth, double maxHeight) { Parent = parent; - _drawing= drawing; - _rectangle = new RectRenderItem(parent); - TextBody = new DrawingTextbody(drawing, Rectangle.Bounds, true); + _drawing= drawing; + var renderContext = drawing._drawings.Worksheet.Workbook.RenderContext; + TextBody = new DrawingTextBody(renderContext, drawing, _marginGroup.Bounds, true); TextBody.MaxWidth = maxWidth; - TextBody.MaxHeight = maxHeight; + TextBody.MaxHeight = maxHeight; } internal DrawingTextBox(ExcelDrawing drawing, BoundingBox parent, double maxWidth, double maxHeight) : base(parent, maxWidth, maxHeight) @@ -34,185 +34,26 @@ internal DrawingTextBox(ExcelDrawing drawing, BoundingBox parent, double maxWidt Init(drawing, parent, maxWidth, maxHeight); } - ////Simplified input - //internal DrawingTextBox(BoundingBox parent, BoundingBox maxBounds) : this( - // parent, maxBounds.Left, maxBounds.Top, maxBounds.Width, maxBounds.Height, maxBounds.Width, maxBounds.Height) - //{ - //} - RectRenderItem _rectangle =null; - public RectRenderItem Rectangle - { - get - { - _rectangle.Bounds.Width = Width; - _rectangle.Bounds.Height = Height; - return _rectangle; - } - } - //internal override void AppendRenderItems(List renderItems) - //{ - // var rect = Rectangle; - - // SvgGroupItem groupItem; - // if (Rotation == 0) - // { - // groupItem = new SvgGroupItem(DrawingRenderer, new BoundingBox(Left, Top, Width, Height)); - // } - // else - // { - // groupItem = new SvgGroupItem(DrawingRenderer, new BoundingBox(Left, Top, Width, Height), Rotation); - // } - // groupItem.TextAnchor = TextAnchor.ToEnumString(); - // renderItems.Add(groupItem); - - // var textboxGroupItem = new SvgGroupItem(DrawingRenderer); - // renderItems.Add(textboxGroupItem); - - // var titleItem = new SvgTitleItem(DrawingRenderer, "TextBodySvg Rect"); - // //The rect shound encapse the text element, so we need to set the left depending on the text anchor. - // if (TextAnchor == eTextAnchor.Middle) - // { - // rect.Bounds.Left = -(rect.Bounds.Width / 2); - // } - // else if (TextAnchor == eTextAnchor.End) - // { - // rect.Bounds.Left = -rect.Bounds.Width; - // } - // else - // { - // rect.Bounds.Left = 0; - // } - // rect.Bounds.Top = 0; - // renderItems.Add(titleItem); - // renderItems.Add(rect); - - // renderItems.Add(new SvgEndGroupItem(DrawingRenderer, rect.Bounds)); - - // TextBody.Bounds.Left = LeftMargin; - // TextBody.Bounds.Top = TopMargin; - // TextBody.AppendRenderItems(renderItems); - // renderItems.Add(new SvgEndGroupItem(DrawingRenderer, rect.Bounds)); - //} - internal void AddText(string text = null) { TextBody.AddParagraph(text); } + DrawingTextBody _textBody; - public new DrawingTextbody TextBody {get;set;} - public double Left - { - get - { - return Rectangle.Bounds.Left; //TextBody.Bounds.Left - LeftMargin; - - } - set - { - //TextBody.Bounds.Left = value + LeftMargin; - Rectangle.Bounds.Left = value; - } - } - public double Top - { - get - { - return Rectangle.Bounds.Top; //TextBody.Bounds.Top - TopMargin; - } - set - { - //TextBody.Bounds.Top = value + TopMargin; - Rectangle.Bounds.Top = value; - } - } - public double Width - { - get - { - return LeftMargin + (TextBody?.Width ?? 0D) + RightMargin; - } - } - public double Height - { - get - { - return TopMargin + (TextBody?.Height ?? 0d) + BottomMargin; - } - } - internal double LeftMargin - { - get; set; - } - - internal double TopMargin + public DrawingTextBody GetTextBody() { - get; set; + return (DrawingTextBody)TextBody; } - internal double RightMargin + public void SetDrawingTextBody(DrawingTextBody tb) { - get; set; + TextBody = tb; } - internal double BottomMargin - { - get; set; - } - internal BoundingBox Parent { get; private set; } - internal double Rotation - { - get - { - return Rectangle.Bounds.Rotation; - } - set - { - Rectangle.Bounds.Rotation = value; - } - } - /// - /// Gets the actual width of the rotated textbox. - /// - /// - internal double GetActualWidth() - { - return Width * Math.Abs(Math.Cos(MathHelper.Radians(Rotation))) + Height * Math.Abs(Math.Sin(MathHelper.Radians(Rotation))); - } - /// - /// Gets the actual right position of the rotated textbox. - /// - /// - internal double GetActualRight() - { - return Left+GetActualWidth(); - } - /// - /// Gets the actual height of the rotated textbox. - /// - /// - internal double GetActualHeight() - { - return Width * Math.Abs(Math.Sin(MathHelper.Radians(Rotation))) + Height * Math.Abs(Math.Cos(MathHelper.Radians(Rotation))); - } - /// - /// Gets the actual right position of the rotated textbox. - /// - /// - internal double GetActualBottom() - { - return Top + GetActualHeight(); - } - /// - /// How the text is anchored. - /// - internal eTextAnchor TextAnchor - { - get; - set; - } + public override RenderTextBody TextBody { get { return _textBody; } set { _textBody = (DrawingTextBody)value; } } - internal void ImportTextBody(ExcelTextBody body, bool useDefaults = true, ExcelHorizontalAlignment horizontalDefault = ExcelHorizontalAlignment.Left) + internal void ImportTextBodyAndParagraphs(ExcelTextBody body, bool useDefaults = true, ExcelHorizontalAlignment horizontalDefault = ExcelHorizontalAlignment.Left) { double l, r, t, b; if (useDefaults) @@ -228,54 +69,12 @@ internal void ImportTextBody(ExcelTextBody body, bool useDefaults = true, ExcelH RightMargin = r; BottomMargin = b; - TextBody.ImportTextBodyAndParagraphs(body, horizontalDefault); - } - - public override void AppendRenderItems(List renderItems) - { - var rect = Rectangle; - - GroupRenderItem groupItem = groupItem = new GroupRenderItem((BoundingBox)rect.Bounds.Parent, Rotation); - groupItem.Bounds = new BoundingBox(Left, Top, Width, Height); - groupItem.TextAnchor = TextAnchor.ToEnumString(); - renderItems.Add(groupItem); - - rect.Top = 0; - rect.Left = 0; - - - var titleItem = new TitleRenderItem("TextBodySvg Rect"); - //The rect shound encapse the text element, so we need to set the left depending on the text anchor. - if(TextAnchor==eTextAnchor.Middle) - { - groupItem.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 - groupItem.Bounds.Left += -(rect.Bounds.Width * COS45); - groupItem.Bounds.Top += (rect.Bounds.Width * COS45); - } - else - { - groupItem.Bounds.Left += rect.Bounds.Height / 2; - groupItem.Bounds.Top += (rect.Bounds.Width); - } - } - groupItem.RenderItems.Add(titleItem); - //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. - groupItem.RenderItems.Add(rect); - - TextBody.Bounds.Left = LeftMargin ; - TextBody.Bounds.Top = TopMargin; - TextBody.AppendRenderItems(groupItem.RenderItems); + _textBody.ImportTextBodyAndParagraphs(body, horizontalDefault); } internal void ImportParagraph(ExcelDrawingParagraph item, double startingY, string text = null) { - TextBody.ImportParagraph(item, startingY, text); + _textBody.ImportParagraph(item, startingY, text); } //internal void AddText(double startingY, string text = null) diff --git a/src/EPPlus/Drawing/Renderer/ShapeRenderer.cs b/src/EPPlus/Drawing/Renderer/ShapeRenderer.cs index 78fbbac90e..e00d01666c 100644 --- a/src/EPPlus/Drawing/Renderer/ShapeRenderer.cs +++ b/src/EPPlus/Drawing/Renderer/ShapeRenderer.cs @@ -42,7 +42,7 @@ internal class ShapeRenderer : DrawingRenderer /// /// Textbox from memory /// - public DrawingTextbody TextBody{ get; internal set; } + public DrawingTextBody TextBody{ get; internal set; } //public ShapeRenderer(eShapeStyle style, double top, double left, double width, double height, eTextAutofit autofit) : base() //{ @@ -232,7 +232,7 @@ public string ViewBox return $"{(Bounds.Left).PointToPixelString()},{Bounds.Top.PointToPixelString()},{Bounds.Right.PointToPixelString()},{Bounds.Bottom.PointToPixelString()}"; } } - DrawingTextbody CreateTextBodyItem(ExcelTextBody bodyOrig) + DrawingTextBody CreateTextBodyItem(ExcelTextBody bodyOrig) { if (InsetTextBox == null) { @@ -263,7 +263,7 @@ DrawingTextbody CreateTextBodyItem(ExcelTextBody bodyOrig) var grp = new GroupRenderItem(MarginTextBox.Bounds); RenderItems.Add(grp); - var txtBodyItem = new DrawingTextbody(Drawing, MarginTextBox.Bounds, MarginTextBox.Left, MarginTextBox.Top, MarginTextBox.Width, MarginTextBox.Height); + var txtBodyItem = new DrawingTextBody(RenderContext, Drawing, MarginTextBox.Bounds, MarginTextBox.Left, MarginTextBox.Top, MarginTextBox.Width, MarginTextBox.Height); txtBodyItem.ImportTextBodyAndParagraphs(bodyOrig); txtBodyItem.AppendRenderItems(grp.RenderItems); diff --git a/src/EPPlus/Drawing/Renderer/SvgRange.cs b/src/EPPlus/Drawing/Renderer/SvgRange.cs deleted file mode 100644 index a7cc43b622..0000000000 --- a/src/EPPlus/Drawing/Renderer/SvgRange.cs +++ /dev/null @@ -1,89 +0,0 @@ -using EPPlusImageRenderer.RenderItems; -using OfficeOpenXml; -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; - -namespace EPPlus.Export.ImageRenderer.Svg -{ - //internal class SvgRange - //{ - // List renderItems = new List(); - // //List textBoxes = new List(); - - // internal SvgRange(ExcelRange range, double totalWidth, double totalHeight) - // { - // SvgRenderRectItem rangeBB = new SvgRenderRectItem(); - - // //To pixel multiplier - // float mult = 96f / 72f; - - // rangeBB.Width = (float)totalWidth; - // rangeBB.Height = (float)totalHeight * mult; - - // rangeBB.BorderColor = "yellow"; - - // rangeBB.BorderWidth = 1; - - // renderItems.Add(rangeBB); - - // float currentWidth = 0f; - - // for (int i = 0; i < range.Columns; i++) - // { - // float currentHeight = 0f; - // var currCol = range.Worksheet.GetColumn(range._fromCol + i); - // var colWidth = currCol == null ? range.Worksheet.DefaultColWidth : currCol.Width; - // colWidth = ExcelColumn.ColumnWidthToPixels(colWidth, range.Worksheet.Workbook.MaxFontWidth); - - // for (int j = 0; j < range.Rows; j++) - // { - // var cell = range.Offset(j, i); - - // var cellContent = range.Offset(j, i).TextForWidth; - // float heightAlt = (float)cell.Worksheet.GetRowHeight(cell._fromRow + j); - // float heightAltPixels = heightAlt * mult; - // float height = (float)cell.Worksheet.Rows[cell._fromRow].Height * mult; - - // SvgRenderRectItem cellBB = new SvgRenderRectItem(); - // cellBB.Left = currentWidth; - // cellBB.Top = currentHeight; - - // cellBB.Width = (float)colWidth; - // cellBB.Height = (float)height; - - // cellBB.BorderWidth = 1; - - // cellBB.BorderColor = "black"; - // cellBB.FillColor = "gray"; - - // renderItems.Add(cellBB); - - // var cellTextBox = new RenderTextbox(null, currentWidth, currentHeight, colWidth, height); - // cellTextBox.AddCellTextRun(cell); - - // var deltaHeight = (float)cellTextBox.Bounds.Height; - // cellBB.Height = cellBB.Height < deltaHeight ? deltaHeight : cellBB.Height; - // textBoxes.Add(cellTextBox); - - // currentHeight += height; - // } - // currentWidth += (float)colWidth; - // } - - // } - - // internal void Render(StringBuilder sb) - // { - // foreach (var item in renderItems) - // { - // item.Render(sb); - // } - // foreach (var item in textBoxes) - // { - // item.RenderTextRuns(sb); - // } - // } - //} -} \ No newline at end of file diff --git a/src/EPPlus/Drawing/Style/Text/ExcelDrawingParagraph.cs b/src/EPPlus/Drawing/Style/Text/ExcelDrawingParagraph.cs index b3410d654b..f482329dcd 100644 --- a/src/EPPlus/Drawing/Style/Text/ExcelDrawingParagraph.cs +++ b/src/EPPlus/Drawing/Style/Text/ExcelDrawingParagraph.cs @@ -36,6 +36,9 @@ public class ExcelDrawingParagraph : XmlHelper Action _initXml; internal IPictureRelationDocument _prd; internal ExcelDrawingParagraphCollection _paragraphs; + + //bool legacyDefaultRunPropertySetting = false; + internal ExcelDrawingParagraph(ExcelDrawingParagraphCollection paragraphs, IPictureRelationDocument prd, XmlNamespaceManager nameSpaceManager, XmlNode topNode, string[] schemaNodeOrder, Action initXml) : base(nameSpaceManager, topNode) { _paragraphs = paragraphs; @@ -43,6 +46,8 @@ internal ExcelDrawingParagraph(ExcelDrawingParagraphCollection paragraphs, IPict _initXml = initXml; _prd = prd; + + if (_paragraphs.FirstDefaultRunProperties == null) { DefaultRunProperties = new ExcelTextFontXml(prd, nameSpaceManager, topNode, "a:pPr/a:defRPr", schemaNodeOrder, initXml); @@ -50,6 +55,28 @@ internal ExcelDrawingParagraph(ExcelDrawingParagraphCollection paragraphs, IPict else { DefaultRunProperties = _paragraphs.FirstDefaultRunProperties; + //if(paragraphs.Count == 0) + //{ + // //The node must still be created + // var xmlFirstDefault = ((ExcelTextFontXml)paragraphs.FirstDefaultRunProperties).XmlHelper.TopNode.ParentNode; + // XmlNode paragraphProperties = topNode.SelectSingleNode("a:pPr", NameSpaceManager); + + // //Create paragraph properties if it does not already exist + // if (paragraphProperties == null) + // { + // paragraphProperties = CreateNode(topNode, "a:pPr", true); + // } + // //Create defRPr + // var textFont = new ExcelTextFontXml(prd, nameSpaceManager, topNode, "a:pPr/a:defRPr", schemaNodeOrder, initXml); + + // //Copy the first element and apply it to the paragraphProperties + // CopyElement((XmlElement)xmlFirstDefault, (XmlElement)paragraphProperties); + // DefaultRunProperties = textFont; + //} + //else + //{ + // DefaultRunProperties = _paragraphs.FirstDefaultRunProperties; + //} } var normalStyle = _prd.Package.Workbook.Styles.GetNormalStyle(); @@ -79,6 +106,24 @@ internal ExcelDrawingParagraph(ExcelDrawingParagraphCollection paragraphs, IPict } } } + else + { + //The node must still be created + var xmlFirstDefault = ((ExcelTextFontXml)paragraphs.FirstDefaultRunProperties).XmlHelper.TopNode.ParentNode; + XmlNode paragraphProperties = topNode.SelectSingleNode("a:pPr", NameSpaceManager); + + //Create paragraph properties if it does not already exist + if (paragraphProperties == null) + { + paragraphProperties = CreateNode(topNode, "a:pPr", true); + } + //Create defRPr + var textFont = new ExcelTextFontXml(prd, nameSpaceManager, topNode, "a:pPr/a:defRPr", schemaNodeOrder, initXml); + + //Copy the first element and apply it to the paragraphProperties + CopyElement((XmlElement)xmlFirstDefault, (XmlElement)paragraphProperties); + DefaultRunProperties = textFont; + } } else if (legacyDefaultRunPropertySetting && _paragraphs.FirstDefaultRunProperties != null) { diff --git a/src/EPPlus/Drawing/Style/Text/ExcelDrawingParagraphCollection.cs b/src/EPPlus/Drawing/Style/Text/ExcelDrawingParagraphCollection.cs index 0ec2e5ecc2..6348c8bfd2 100644 --- a/src/EPPlus/Drawing/Style/Text/ExcelDrawingParagraphCollection.cs +++ b/src/EPPlus/Drawing/Style/Text/ExcelDrawingParagraphCollection.cs @@ -47,7 +47,7 @@ internal ExcelDrawingParagraphCollection(IPictureRelationDocument prd, XmlNamesp { var paragraph = new ExcelDrawingParagraph(this, prd, NameSpaceManager, pn, schemaNodeOrder, initXml); _paragraphs.Add(paragraph); - //if(_paragraphs.Count == 1) + //if (_paragraphs.Count == 1 && FirstDefaultRunProperties == null) //{ // FirstDefaultRunProperties = paragraph.DefaultRunProperties; //} @@ -90,10 +90,10 @@ public ExcelDrawingParagraph Add(string text) } var p = new ExcelDrawingParagraph(this, _prd, NameSpaceManager, pn, SchemaNodeOrder, _initXml); + var tr = p.TextRuns.Add(text); _paragraphs.Add(p); - //_addCallback?.Invoke(tr); if (placeHolderNode != null) diff --git a/src/EPPlus/ExcelRangeBase.cs b/src/EPPlus/ExcelRangeBase.cs index 288464982d..d11c845020 100644 --- a/src/EPPlus/ExcelRangeBase.cs +++ b/src/EPPlus/ExcelRangeBase.cs @@ -19,6 +19,7 @@ Date Author Change using OfficeOpenXml.Export.HtmlExport.Interfaces; using OfficeOpenXml.FormulaParsing; using OfficeOpenXml.FormulaParsing.Excel.Functions; +using OfficeOpenXml.FormulaParsing.Excel.Functions.RefAndLookup; using OfficeOpenXml.FormulaParsing.LexicalAnalysis; using OfficeOpenXml.Sorting; using OfficeOpenXml.Style; @@ -867,7 +868,24 @@ public string Text } } } - + /// + /// Returns the Text for a merged cell. + /// + internal string TextMerged + { + get + { + if(Merge) + { + var ma = _worksheet.MergedCells[_fromRow, _fromCol]; + if(ma!=null) + { + return _worksheet.Cells[ma].Text; + } + } + return Text; + } + } /// /// Used to add/remove cell pictures in the range /// diff --git a/src/EPPlus/ExcelWorkbook.cs b/src/EPPlus/ExcelWorkbook.cs index 02b0c79d79..172f0aa6e6 100644 --- a/src/EPPlus/ExcelWorkbook.cs +++ b/src/EPPlus/ExcelWorkbook.cs @@ -10,42 +10,45 @@ Date Author Change ************************************************************************************************* 01/27/2020 EPPlus Software AB Initial release EPPlus 5 *************************************************************************************************/ -using System; -using System.Xml; -using System.IO; -using System.Collections.Generic; -using System.Text; -using System.Globalization; -using System.Linq; -using OfficeOpenXml.VBA; -using OfficeOpenXml.FormulaParsing; -using OfficeOpenXml.FormulaParsing.LexicalAnalysis; -using OfficeOpenXml.Packaging.Ionic.Zip; -using OfficeOpenXml.Drawing.Theme; +using EPPlus.DrawingRenderer; +using EPPlus.Fonts.OpenType; +using OfficeOpenXml.CellPictures; using OfficeOpenXml.Compatibility; +using OfficeOpenXml.Constants; using OfficeOpenXml.Core.CellStore; -using OfficeOpenXml.Drawing.Slicer; -using OfficeOpenXml.ThreadedComments; -using OfficeOpenXml.Table; -using OfficeOpenXml.Table.PivotTable; +using OfficeOpenXml.Data.Connection; +using OfficeOpenXml.Data.CustomXml; +using OfficeOpenXml.DigitalSignatures; using OfficeOpenXml.Drawing; -using OfficeOpenXml.Constants; -using OfficeOpenXml.ExternalReferences; -using OfficeOpenXml.Packaging; -using OfficeOpenXml.Export.HtmlExport.Interfaces; +using OfficeOpenXml.Drawing.Slicer; +using OfficeOpenXml.Drawing.Theme; using OfficeOpenXml.Export.HtmlExport.Exporters; +using OfficeOpenXml.Export.HtmlExport.Interfaces; +using OfficeOpenXml.ExternalReferences; +using OfficeOpenXml.FormulaParsing; +using OfficeOpenXml.FormulaParsing.LexicalAnalysis; +using OfficeOpenXml.Interfaces.Fonts; using OfficeOpenXml.Metadata; +using OfficeOpenXml.Packaging; +using OfficeOpenXml.Packaging.Ionic.Zip; using OfficeOpenXml.RichData; -using OfficeOpenXml.Style; -using OfficeOpenXml.CellPictures; using OfficeOpenXml.RichData.IndexRelations; -using OfficeOpenXml.DigitalSignatures; -using OfficeOpenXml.Utils.XML; -using OfficeOpenXml.Utils.TypeConversion; -using OfficeOpenXml.Utils.FileUtils; +using OfficeOpenXml.Style; +using OfficeOpenXml.Table; +using OfficeOpenXml.Table.PivotTable; +using OfficeOpenXml.ThreadedComments; using OfficeOpenXml.Utils.EnumUtils; -using OfficeOpenXml.Data.CustomXml; -using OfficeOpenXml.Data.Connection; +using OfficeOpenXml.Utils.FileUtils; +using OfficeOpenXml.Utils.TypeConversion; +using OfficeOpenXml.Utils.XML; +using OfficeOpenXml.VBA; +using System; +using System.Collections.Generic; +using System.Globalization; +using System.IO; +using System.Linq; +using System.Text; +using System.Xml; namespace OfficeOpenXml { @@ -1463,6 +1466,64 @@ public int? DefaultThemeVersion } } } + + RenderContext _renderContext = null; + + /// + /// Rendering-wide resources for this workbook (font engine, etc.), flowed down the drawing + /// render stack. Created lazily with a default font engine if none has been configured. + /// Internal: callers configure fonts via rather than touching + /// this directly. + /// + internal RenderContext RenderContext + { + get + { + if (_renderContext == null) + { + _renderContext = new RenderContext(() => new OpenTypeFontEngine()); + } + return _renderContext; + } + set + { + _renderContext = value; + } + } + + /// + /// Configures the fonts used when rendering drawings (charts, shapes) from this workbook to + /// image formats such as SVG. Use this to add font directories, control whether system font + /// directories are searched, or register fallback chains. + /// + /// A callback that configures the font settings for this workbook. + /// + /// The configuration is applied when this workbook first renders a drawing. Call this before + /// rendering. The font engine is per workbook configuring one workbook does not affect any + /// other workbook or any global state. + /// + public void ConfigureFonts(Action configure) + { + if (configure == null) + throw new ArgumentNullException("configure"); + + RenderContext = new RenderContext(() => new OpenTypeFontEngine(configure)); + } + + /// + /// Supplies a pre-built font engine for this workbook's rendering. Intended for advanced + /// scenarios and testing where a specific engine instance must be used. Per workbook never + /// global. + /// + /// The font engine to use for this workbook. + internal void UseFontEngine(OpenTypeFontEngine engine) + { + if (engine == null) + throw new ArgumentNullException("engine"); + + RenderContext = new RenderContext(() => engine); + } + bool _fullPrecision; /// /// If false, EPPlus will round cell values to the number of decimals as displayed in the cell by using the cells number format when calculating the workbook. @@ -2221,6 +2282,11 @@ public void Dispose() _formulaParser.Dispose(); _formulaParser = null; } + if (_renderContext != null) + { + _renderContext.Dispose(); + _renderContext = null; + } } /// diff --git a/src/EPPlus/Export/HtmlExport/Enums/eDrawingInclude.cs b/src/EPPlus/Export/HtmlExport/Enums/eDrawingInclude.cs new file mode 100644 index 0000000000..d092f66c83 --- /dev/null +++ b/src/EPPlus/Export/HtmlExport/Enums/eDrawingInclude.cs @@ -0,0 +1,33 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; + +namespace OfficeOpenXml.Export.HtmlExport +{ + /// + /// What drawings to include in html export + /// + [Flags] + public enum eDrawingInclude + { + /// + /// Include no drawings + /// + None = 0, + /// + /// Include Shapes + /// + Shapes = 2, + /// + /// Include Charts + /// + Charts = 4, + + //TODO: This is already handled by image enum. We may need restructure here + /// + /// Include Images ? + /// + Images = 8, + } +} diff --git a/src/EPPlus/Export/HtmlExport/Exporters/Internal/AbstractRangeExporter.cs b/src/EPPlus/Export/HtmlExport/Exporters/Internal/AbstractRangeExporter.cs index 61cc6bc4e5..ca8d63f8bf 100644 --- a/src/EPPlus/Export/HtmlExport/Exporters/Internal/AbstractRangeExporter.cs +++ b/src/EPPlus/Export/HtmlExport/Exporters/Internal/AbstractRangeExporter.cs @@ -11,6 +11,7 @@ Date Author Change 6/4/2022 EPPlus Software AB ExcelTable Html Export *************************************************************************************************/ using OfficeOpenXml.Drawing; +using OfficeOpenXml.Drawing.Chart; using OfficeOpenXml.FormulaParsing.Excel.Functions.Text; using OfficeOpenXml.Table; using OfficeOpenXml.Utils.String; @@ -27,6 +28,7 @@ public AbstractHtmlExporter() internal const string TableClass = "epplus-table"; internal List _rangePictures = null; + internal List _rangeDrawings = null; protected List _dataTypes = new List(); protected ExporterContext _exporterContext; @@ -45,13 +47,14 @@ protected void GetDataTypes(ExcelAddressBase adr, ExcelTable table) } } - internal void LoadRangeImages(List ranges) + internal void LoadRangeDrawings(List ranges) { if (_rangePictures != null) { return; } _rangePictures = new List(); + _rangeDrawings = new List(); //Render in-cell images. foreach (var worksheet in ranges.Select(x => x.Worksheet).Distinct()) { @@ -76,6 +79,44 @@ internal void LoadRangeImages(List ranges) ToColumnOff = toColOff }); } + else if(d is ExcelShape s) + { + s.GetFromBounds(out int fromRow, out int fromRowOff, out int fromCol, out int fromColOff); + s.GetToBounds(out int toRow, out int toRowOff, out int toCol, out int toColOff); + + _rangeDrawings.Add(new HtmlSvgDrawing() + { + WorksheetId = worksheet.PositionId, + Drawing = s, + FromRow = fromRow, + FromRowOff = fromRowOff, + FromColumn = fromCol, + FromColumnOff = fromColOff, + ToRow = toRow, + ToRowOff = toRowOff, + ToColumn = toCol, + ToColumnOff = toColOff + }); + } + else if(d is ExcelChart) + { + d.GetFromBounds(out int fromRow, out int fromRowOff, out int fromCol, out int fromColOff); + d.GetToBounds(out int toRow, out int toRowOff, out int toCol, out int toColOff); + + _rangeDrawings.Add(new HtmlSvgDrawing() + { + WorksheetId = worksheet.PositionId, + Drawing = d, + FromRow = fromRow, + FromRowOff = fromRowOff, + FromColumn = fromCol, + FromColumnOff = fromColOff, + ToRow = toRow, + ToRowOff = toRowOff, + ToColumn = toCol, + ToColumnOff = toColOff + }); + } } } } @@ -114,5 +155,18 @@ protected HtmlImage GetImage(int worksheetId, int row, int col) } return null; } + + protected HtmlSvgDrawing GetDrawing(int worksheetId, int row, int col) + { + if (_rangeDrawings == null) return null; + foreach (var d in _rangeDrawings) + { + if (d.FromRow == row - 1 && d.FromColumn == col - 1 && d.WorksheetId == worksheetId) + { + return d; + } + } + return null; + } } } diff --git a/src/EPPlus/Export/HtmlExport/Exporters/Internal/CssExporterBase.cs b/src/EPPlus/Export/HtmlExport/Exporters/Internal/CssExporterBase.cs index 789d89e7fe..22fb929ac2 100644 --- a/src/EPPlus/Export/HtmlExport/Exporters/Internal/CssExporterBase.cs +++ b/src/EPPlus/Export/HtmlExport/Exporters/Internal/CssExporterBase.cs @@ -139,7 +139,7 @@ protected void AddCssRulesToCollection(CssRangeRuleCollection cssTranslator, Htm if (Settings.Pictures.Include == ePictureInclude.Include) { - LoadRangeImages(_ranges._list); + LoadRangeDrawings(_ranges._list); foreach (var p in _rangePictures) { cssTranslator.AddPictureToCss(p); diff --git a/src/EPPlus/Export/HtmlExport/Exporters/Internal/HtmlExporterBaseInternal.cs b/src/EPPlus/Export/HtmlExport/Exporters/Internal/HtmlExporterBaseInternal.cs index e970079292..ea81ac73cd 100644 --- a/src/EPPlus/Export/HtmlExport/Exporters/Internal/HtmlExporterBaseInternal.cs +++ b/src/EPPlus/Export/HtmlExport/Exporters/Internal/HtmlExporterBaseInternal.cs @@ -50,7 +50,7 @@ public HtmlExporterBaseInternal(HtmlExportSettings settings, ExcelRangeBase rang } } - LoadRangeImages(_ranges._list); + LoadRangeDrawings(_ranges._list); } public HtmlExporterBaseInternal(HtmlExportSettings settings, EPPlusReadOnlyList ranges) @@ -59,7 +59,7 @@ public HtmlExporterBaseInternal(HtmlExportSettings settings, EPPlusReadOnlyList< Require.Argument(ranges).IsNotNull("ranges"); _ranges = ranges; //TODO: Fix support for all ranges - LoadRangeImages(_ranges._list); + LoadRangeDrawings(_ranges._list); } protected void SetColumnGroup(HTMLElement element, ExcelRangeBase _range, HtmlExportSettings settings, bool isMultiSheet) @@ -121,6 +121,7 @@ protected HTMLElement GetThead(ExcelRangeBase range, List headers = null ExcelWorksheet worksheet = range.Worksheet; HtmlImage image = null; + HtmlSvgDrawing drawing = null; foreach (var col in _columns) { if (InMergeCellSpan(row, col)) continue; @@ -153,7 +154,13 @@ protected HTMLElement GetThead(ExcelRangeBase range, List headers = null image = GetImage(cell.Worksheet.PositionId, cell._fromRow, cell._fromCol); } + if(Settings.Drawings.Include == (ePictureInclude.Include | ePictureInclude.IncludeInHtmlOnly)) + { + drawing = GetDrawing(cell.Worksheet.PositionId, cell._fromRow, cell._fromCol); + } + AddImage(contentElement, Settings, image, cell.Value); + AddDrawing(contentElement, Settings, drawing, cell.Value); if (headerRows > 0 || table != null) { @@ -224,6 +231,7 @@ protected HTMLElement GetTableBody(ExcelRangeBase range, int row, int endRow) var ws = range.Worksheet; HtmlImage image = null; + HtmlDrawing drawing = null; bool hasFooter = table != null && table.ShowTotal; while (row <= endRow) { @@ -274,6 +282,11 @@ protected HTMLElement GetTableBody(ExcelRangeBase range, int row, int endRow) image = GetImage(cell.Worksheet.PositionId, cell._fromRow, cell._fromCol); } + if (Settings.Drawings.Include == (ePictureInclude.Include | ePictureInclude.IncludeInHtmlOnly)) + { + drawing = GetDrawing(cell.Worksheet.PositionId, cell._fromRow, cell._fromCol); + } + if (cell.Hyperlink == null) { var addRowScope = table == null ? false : table.ShowFirstColumn && col == table.Address._fromCol || table.ShowLastColumn && col == table.Address._toCol; @@ -429,6 +442,35 @@ protected void AddImage(HTMLElement parent, HtmlExportSettings settings, HtmlIma } } + protected void AddDrawing(HTMLElement parent, HtmlExportSettings settings, HtmlSvgDrawing d, object value) + { + if (d != null) + { + var child = new HTMLElement(HtmlElements.Svg); + var name = d.Drawing.Name; + string drawingName = HtmlExportTableUtil.GetClassName(d.Drawing.Name, $"drawing{d.Drawing.Id}"); + child.AddAttribute("alt", d.Drawing.Name); + if (settings.Pictures.AddNameAsId) + { + child.AddAttribute("id", drawingName); + } + + if (settings.Drawings.Include == ePictureInclude.IncludeInHtmlOnly) + { + child.ElementName = "div"; + child.Content = d.Drawing.ToSvg(); + //var _encodedImage = ImageEncoder.EncodeImage(image, out type); + + //child.AddAttribute("src", $"data:{GetContentType(type.Value)};base64,{_encodedImage}"); + } + else + { + child.AddAttribute("class", $"{settings.StyleClassPrefix}drawing-{name} {settings.StyleClassPrefix}drawing-prop-{drawingName}"); + } + parent._childElements.Add(child); + } + } + protected List _columns = new List(); protected HtmlExportSettings Settings; protected readonly List _mergedCells = new List(); diff --git a/src/EPPlus/Export/HtmlExport/Exporters/Internal/HtmlTableExporterBase.cs b/src/EPPlus/Export/HtmlExport/Exporters/Internal/HtmlTableExporterBase.cs index 9648722f95..64633caec8 100644 --- a/src/EPPlus/Export/HtmlExport/Exporters/Internal/HtmlTableExporterBase.cs +++ b/src/EPPlus/Export/HtmlExport/Exporters/Internal/HtmlTableExporterBase.cs @@ -31,7 +31,7 @@ internal HtmlTableExporterBase _table = table; _tableExportSettings = settings; - LoadRangeImages(new List() { table.Range }); + LoadRangeDrawings(new List() { table.Range }); } protected readonly ExcelTable _table; diff --git a/src/EPPlus/Export/HtmlExport/HtmlDrawing.cs b/src/EPPlus/Export/HtmlExport/HtmlDrawing.cs new file mode 100644 index 0000000000..61836e5710 --- /dev/null +++ b/src/EPPlus/Export/HtmlExport/HtmlDrawing.cs @@ -0,0 +1,21 @@ +using OfficeOpenXml.Drawing; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; + +namespace OfficeOpenXml.Export.HtmlExport +{ + internal class HtmlDrawing + { + public int WorksheetId { get; set; } + public int FromRow { get; set; } + public int FromRowOff { get; set; } + public int ToRow { get; set; } + public int ToRowOff { get; set; } + public int FromColumn { get; set; } + public int FromColumnOff { get; set; } + public int ToColumn { get; set; } + public int ToColumnOff { get; set; } + } +} diff --git a/src/EPPlus/Export/HtmlExport/HtmlElements.cs b/src/EPPlus/Export/HtmlExport/HtmlElements.cs index 8f32db0ca0..9822084b40 100644 --- a/src/EPPlus/Export/HtmlExport/HtmlElements.cs +++ b/src/EPPlus/Export/HtmlExport/HtmlElements.cs @@ -46,5 +46,6 @@ internal static readonly HashSet NoIndentElements public const string Span = "span"; public const string ColGroup = "colgroup"; public const string Img = "img"; + public const string Svg = "svg"; } } diff --git a/src/EPPlus/Export/HtmlExport/HtmlImage.cs b/src/EPPlus/Export/HtmlExport/HtmlImage.cs index 4ee719a7a9..a5256cb624 100644 --- a/src/EPPlus/Export/HtmlExport/HtmlImage.cs +++ b/src/EPPlus/Export/HtmlExport/HtmlImage.cs @@ -14,17 +14,8 @@ Date Author Change namespace OfficeOpenXml.Export.HtmlExport { - internal class HtmlImage + internal class HtmlImage : HtmlDrawing { - public int WorksheetId { get; set; } public ExcelPicture Picture { get; set; } - public int FromRow { get; set; } - public int FromRowOff { get; set; } - public int ToRow { get; set; } - public int ToRowOff { get; set; } - public int FromColumn { get; set; } - public int FromColumnOff { get; set; } - public int ToColumn { get; set; } - public int ToColumnOff { get; set; } } } diff --git a/src/EPPlus/Export/HtmlExport/HtmlSvgDrawing.cs b/src/EPPlus/Export/HtmlExport/HtmlSvgDrawing.cs new file mode 100644 index 0000000000..19275c6b09 --- /dev/null +++ b/src/EPPlus/Export/HtmlExport/HtmlSvgDrawing.cs @@ -0,0 +1,13 @@ +using OfficeOpenXml.Drawing; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; + +namespace OfficeOpenXml.Export.HtmlExport +{ + internal class HtmlSvgDrawing : HtmlDrawing + { + public ExcelDrawing Drawing; + } +} diff --git a/src/EPPlus/Export/HtmlExport/Settings/HtmlDrawingSettings.cs b/src/EPPlus/Export/HtmlExport/Settings/HtmlDrawingSettings.cs new file mode 100644 index 0000000000..bab14da648 --- /dev/null +++ b/src/EPPlus/Export/HtmlExport/Settings/HtmlDrawingSettings.cs @@ -0,0 +1,28 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; + +namespace OfficeOpenXml.Export.HtmlExport +{ + public class HtmlDrawingSettings + { + internal HtmlDrawingSettings() + { + + } + + //Use picture for now. Possibly re-name + /// + /// If how drawings should be included in the html. Default is + /// + public ePictureInclude Include = ePictureInclude.Exclude; + + /// + /// Which type of drawing should be included + /// + public eDrawingInclude DrawTypeInclude = eDrawingInclude.None; + + + } +} diff --git a/src/EPPlus/Export/HtmlExport/Settings/HtmlExportSettings.cs b/src/EPPlus/Export/HtmlExport/Settings/HtmlExportSettings.cs index eeddc128af..5cde7470e7 100644 --- a/src/EPPlus/Export/HtmlExport/Settings/HtmlExportSettings.cs +++ b/src/EPPlus/Export/HtmlExport/Settings/HtmlExportSettings.cs @@ -136,6 +136,13 @@ public HtmlPictureSettings Pictures { get; } = new HtmlPictureSettings(); + /// + /// If and which Charts and/or Shapes will be included + /// + public HtmlDrawingSettings Drawings + { + get; + } = new HtmlDrawingSettings(); /// /// If set to true classes that identifies Excel table styling will be included in the html. Default value is true. diff --git a/src/EPPlus/Style/RichText/ExcelParagraphCollection.cs b/src/EPPlus/Style/RichText/ExcelParagraphCollection.cs index ef13fff0a6..0face72036 100644 --- a/src/EPPlus/Style/RichText/ExcelParagraphCollection.cs +++ b/src/EPPlus/Style/RichText/ExcelParagraphCollection.cs @@ -33,44 +33,19 @@ public class ExcelParagraphCollection : XmlHelper, IEnumerable private readonly float _defaultFontSize; private readonly ExcelTextFont _defaultFont; private readonly ExcelTextBody _textBody; - internal ExcelParagraphCollection(ExcelTextBody tb, ExcelDrawing drawing, XmlNamespaceManager ns, XmlNode topNode, string path, string[] schemaNodeOrder, float defaultFontSize =11, eTextAlignment defaultAlignment = eTextAlignment.Left) : + internal ExcelParagraphCollection(ExcelTextBody tb, ExcelDrawing drawing, XmlNamespaceManager ns, XmlNode topNode, string path, string[] schemaNodeOrder, float defaultFontSize = 11, eTextAlignment defaultAlignment = eTextAlignment.Left) : base(ns, topNode) { _drawing = drawing; _textBody = tb; _defaultFontSize = defaultFontSize; - AddSchemaNodeOrder(schemaNodeOrder, new string[] { "strRef","rich", "f", "strCache", "bodyPr", "lstStyle", "p", "ptCount","pt","pPr", "lnSpc", "spcBef", "spcAft", "buClrTx", "buClr", "buSzTx", "buSzPct", "buSzPts", "buFontTx", "buFont","buNone", "buAutoNum", "buChar","buBlip", "tabLst","defRPr", "r","br","fld" ,"endParaRPr" }); - - //if (topNode.SelectSingleNode(path, ns) == null) - //{ - // if (tb.Paragraphs.Count == 0) - // { - // var paragraphParent = path.Substring(0, path.LastIndexOf('/')); - // var tmpTop = TopNode; - // TopNode = TopNode.SelectNodes(paragraphParent, ns)[0]; - // var placeHolderNode = tb.Paragraphs.CreateAndReturnParagraphPlaceHolder(); - // TopNode = tmpTop; - // } - //} - - //var tfXml = new ExcelTextFontXml(drawing._drawings, ns, TopNode, path + "/a:pPr/a:defRPr", schemaNodeOrder); - ////if(tb.Paragraphs.Count == 0) - ////{ - //// if(tfXml._rootNode.SelectSingleNode("//a:p", tfXml.XmlHelper.NameSpaceManager) == null) - //// { - - //// var placeHolderNode = tb.Paragraphs.CreateAndReturnParagraphPlaceHolder(); - //// tfXml.XmlHelper.TopNode = placeHolderNode; - //// } - ////} - - //_defaultFont = tfXml; - + AddSchemaNodeOrder(schemaNodeOrder, new string[] { "strRef", "rich", "f", "strCache", "bodyPr", "lstStyle", "p", "ptCount", "pt", "pPr", "lnSpc", "spcBef", "spcAft", "buClrTx", "buClr", "buSzTx", "buSzPct", "buSzPts", "buFontTx", "buFont", "buNone", "buAutoNum", "buChar", "buBlip", "tabLst", "defRPr", "r", "br", "fld", "endParaRPr" }); _path = path; + foreach (var p in tb.Paragraphs) { p.defaultAlignment = defaultAlignment; - foreach(var tr in p.TextRuns) + foreach (var tr in p.TextRuns) { _list.Add(new ExcelParagraph(tr)); } @@ -152,7 +127,6 @@ public int Count public ExcelParagraph Add(string Text, bool NewParagraph=false) { ExcelDrawingParagraph p; - ExcelParagraph item; if (NewParagraph || _textBody.Paragraphs.Count==0) { _textBody.Paragraphs.Add(Text); @@ -239,7 +213,7 @@ public string Text { if (_textBody.Paragraphs.Count == 0) { - Add(value); + Add(value, true); } else { diff --git a/src/EPPlus/Utils/Rendering/DrawingExtensions.cs b/src/EPPlus/Utils/Rendering/DrawingExtensions.cs index 3da7ad5c4f..7d93cfe143 100644 --- a/src/EPPlus/Utils/Rendering/DrawingExtensions.cs +++ b/src/EPPlus/Utils/Rendering/DrawingExtensions.cs @@ -1,9 +1,14 @@ using EPPlus.DrawingRenderer.RenderItems; using EPPlus.Fonts.OpenType.Utils; using EPPlus.Graphics; +using OfficeOpenXml; using OfficeOpenXml.Drawing; +using OfficeOpenXml.Drawing.Chart; using OfficeOpenXml.Drawing.Style.Fill; using OfficeOpenXml.Drawing.Theme; +using OfficeOpenXml.ExternalReferences; +using System.Collections.Generic; +using System.Linq; namespace EPPlus.Export.Utils { @@ -19,6 +24,114 @@ internal static BoundingBox GetBoundingBox(this ExcelDrawing drawing) Height = drawing.GetPixelHeight().PixelToPoint() }; } + + internal static List LoadSeriesValues(ExcelChart chart, string serieAddressInput, double[] numLiterals, string[] strLiterals) + { + string serieAddress = serieAddressInput; + + //Some addresses are split and within parenthesis + if (serieAddressInput.StartsWith("(")) + { + serieAddress = serieAddressInput.Trim('(', ')'); + } + + List values = new List(); + if (numLiterals != null) + { + values.AddRange(numLiterals.Select(x => (object)x)); + } + else if (strLiterals != null) + { + values.AddRange(strLiterals.Select(x => (object)x)); + } + else + { + if (string.IsNullOrEmpty(serieAddress)) + { + return null; + } + var address = new ExcelAddressBase(serieAddress); + + if (address.Addresses != null && address.Addresses.Count > 1) + { + foreach (var splitAddress in address.Addresses) + { + FillValuesFromAddress(chart, splitAddress, ref values); + } + } + else + { + FillValuesFromAddress(chart, address, ref values); + } + } + return values; + } + + internal static void FillValuesFromAddress(ExcelChart Chart, ExcelAddressBase address, ref List values) + { + if (address.IsExternal) + { + var wb = Chart.WorkSheet.Workbook; + var extWb = wb.ExternalLinks[address.ExternalReferenceIndex - 1] as ExcelExternalWorkbook; + if (extWb != null) + { + var wsName = address.WorkSheetName; + if (extWb.Package == null) + { + var extWs = extWb.CachedWorksheets[wsName]; + FillExternalValues(extWs, address, ref values); + } + else + { + var ws = extWb.Package.Workbook.Worksheets[wsName]; + FillInternalValues(ws, address, ref values); + } + } + } + else + { + var wsName = address.WorkSheetName; + + if (string.IsNullOrEmpty(wsName)) + { + wsName = Chart.WorkSheet.Name; + } + + var ws = Chart.WorkSheet.Workbook.Worksheets[wsName]; + FillInternalValues(ws, address, ref values); + } + } + + internal static void FillExternalValues(ExcelExternalWorksheet extWs, ExcelAddressBase address, ref List values) + { + if (extWs != null) + { + for (int r = address.Start.Row; r <= address.End.Row; r++) + { + for (int c = address.Start.Column; c <= address.End.Column; c++) + { + values.Add(extWs.CellValues[r, c].Value); + } + } + } + } + + internal static void FillInternalValues(ExcelWorksheet ws, ExcelAddressBase address, ref List values) + { + + if (ws != null) + { + for (int r = address.Start.Row; r <= address.End.Row; r++) + { + for (int c = address.Start.Column; c <= address.End.Column; c++) + { + values.Add(ws.Cells[r, c].Value); + } + } + } + } + + internal static OffsetRectangle AsOffsetRectangle(this ExcelDrawingRectangle item) { return new OffsetRectangle @@ -33,7 +146,7 @@ internal static FillTile AsFillTile(this ExcelDrawingBlipFillTile fillTile) { return new FillTile { - Alignment = (RectangleAlignment)fillTile.Alignment, + Alignment = (RectangleAlignment?)fillTile.Alignment, FlipMode = (TileFlipMode)fillTile.FlipMode, HorizontalOffset = fillTile.HorizontalOffset, VerticalOffset = fillTile.VerticalOffset, diff --git a/src/EPPlus/Utils/TypeConversion/ColorConverter.cs b/src/EPPlus/Utils/TypeConversion/ColorConverter.cs index 76f21ce3a1..7f32f6db97 100644 --- a/src/EPPlus/Utils/TypeConversion/ColorConverter.cs +++ b/src/EPPlus/Utils/TypeConversion/ColorConverter.cs @@ -41,6 +41,27 @@ public static Color GetThemeColor(ExcelTheme theme, ExcelDrawingColorManager cm) } + public static Color GetThemeColor(ExcelTheme theme, ExcelDrawingColorManager cm, ExcelDrawingColorManager cmStyle) + { + if (cm != null && cm.ColorType == eDrawingColorType.Scheme) + { + ExcelDrawingThemeColorManager newCm; + if(cm.SchemeColor.Color == eSchemeColor.Style) + { + return GetThemeColor(theme, cmStyle); + } + else + { + newCm = theme.ColorScheme.GetColorByEnum(cm.SchemeColor.Color); + } + var nc = GetThemeColor(newCm); + return ApplyTransforms(nc, cm.Transforms); + } + var c = GetThemeColor(cm); + return ApplyTransforms(c, cm.Transforms); + + } + private static Color ApplyTransforms(Color c, ExcelColorTransformCollection transforms) { if (transforms==null || transforms.Count == 0) return c; diff --git a/src/EPPlusTest/Drawing/TextMeasuring/ReadMeasureTests.cs b/src/EPPlusTest/Drawing/TextMeasuring/ReadMeasureTests.cs index cb8cdb64c8..1eef5b295d 100644 --- a/src/EPPlusTest/Drawing/TextMeasuring/ReadMeasureTests.cs +++ b/src/EPPlusTest/Drawing/TextMeasuring/ReadMeasureTests.cs @@ -131,7 +131,8 @@ public void WrapMultipleFragments_LongPlusEndWord() fonts.Add(mf2); fonts.Add(mf2); - var txtMeasurer = OpenTypeFonts.GetTextLayoutEngineForFont(mf); + var engine = new OpenTypeFontEngine(x => x.SearchSystemDirectories = true); + var txtMeasurer = engine.GetTextLayoutEngineForFont(mf); var maxWidth = 114d; diff --git a/src/EPPlusTest/Export/HtmlExport/RangeExporterTests.cs b/src/EPPlusTest/Export/HtmlExport/RangeExporterTests.cs index 8efd80f6f1..6ddc5b8515 100644 --- a/src/EPPlusTest/Export/HtmlExport/RangeExporterTests.cs +++ b/src/EPPlusTest/Export/HtmlExport/RangeExporterTests.cs @@ -144,7 +144,6 @@ public async Task ShouldExportHtmlWithMergedCells() var resultAsync = await exporter.GetSinglePageAsync(); SaveAndCleanup(package); Assert.AreEqual(result, resultAsync); - } } [TestMethod] diff --git a/src/EPPlusTest/Export/HtmlExport/SvgShapeExportTests.cs b/src/EPPlusTest/Export/HtmlExport/SvgShapeExportTests.cs new file mode 100644 index 0000000000..d65926a4fa --- /dev/null +++ b/src/EPPlusTest/Export/HtmlExport/SvgShapeExportTests.cs @@ -0,0 +1,150 @@ +using Microsoft.VisualStudio.TestTools.UnitTesting; +using OfficeOpenXml.Drawing.Chart.Style; +using OfficeOpenXml.Export.HtmlExport; +using System; +using System.Collections.Generic; +using System.Drawing; +using System.IO; +using System.Linq; +using System.Text; + +namespace EPPlusTest.Export.HtmlExport +{ + [TestClass] + public class SvgShapeExportTests : TestBase + { + [TestMethod] + public void ExportBasicShapeWorksheet() + { + int[] values = { 5, 10, 15, 20 }; + using (var package = OpenPackage("HtmlBasicSvgShape.xlsx", true)) + { + var ws = package.Workbook.Worksheets.Add("ShapeWs"); + var rect = ws.Drawings.AddShape("SimpleRect", OfficeOpenXml.Drawing.eShapeStyle.Rect); + //rect.Fill.Color = System.Drawing.Color.AliceBlue; + //var rect2 = ws.Drawings.AddShape("SimpleRect2", OfficeOpenXml.Drawing.eShapeStyle.Rect); + //rect2.Fill.Color = System.Drawing.Color.BlanchedAlmond; + + for (int i = 0; i< values.Count()*2; i++) + { + if(i >= values.Count()) + { + ws.Cells[i+1, 1].Value = -values[i - values.Count()]; + } + else + { + ws.Cells[i+1, 1].Value = values[i]; + } + } + + var exporter = ws.Cells["A1:C20"].CreateHtmlExporter(); + + exporter.Settings.Drawings.Include = ePictureInclude.IncludeInHtmlOnly; + exporter.Settings.Drawings.DrawTypeInclude = eDrawingInclude.Shapes; + + var htmlPage = exporter.GetSinglePage(); + + var file = GetOutputFile("html", "svgRect.html"); + + SaveAndCleanup(package); + + File.WriteAllText(file.FullName, htmlPage); + } + } + + [TestMethod] + public void ExportBarChart() + { + int[] values = { 5, 10, 15, 20 }; + using (var package = OpenPackage("HtmlSvgColChart.xlsx", true)) + { + var ws = package.Workbook.Worksheets.Add("ShapeWs"); + + for (int i = 0; i < values.Count() * 2; i++) + { + if (i >= values.Count()) + { + ws.Cells[i + 1, 1].Value = -values[i - values.Count()]; + } + else + { + ws.Cells[i + 1, 1].Value = values[i]; + } + } + + var chart = ws.Drawings.AddBarChart("myColChart", OfficeOpenXml.Drawing.Chart.eBarChartType.ColumnClustered); + chart.Series.Add(ws.Cells["A1:A8"]); + + chart.StyleManager.SetChartStyle(ePresetChartStyleMultiSeries.BarChartStyle1); + + //chart.Style = OfficeOpenXml.Drawing.Chart.eChartStyle.Style1; + //var theme = ws.Workbook.ThemeManager.GetOrCreateTheme(); + //chart.StyleManager.SetChartStyle(0); + //chart.StyleManager.SetChartStyle(0); + //chart.StyleManager.ApplyStyles(); + //chart.Fill.Color = System.Drawing.Color.BlanchedAlmond; + //chart.Series[0].Fill.Color = System.Drawing.Color.LightCoral; + + var exporter = ws.Cells["A1:C20"].CreateHtmlExporter(); + + exporter.Settings.Drawings.Include = ePictureInclude.IncludeInHtmlOnly; + exporter.Settings.Drawings.DrawTypeInclude = eDrawingInclude.Charts; + + var htmlPage = exporter.GetSinglePage(); + + var file = GetOutputFile("html", "myColChart.html"); + var svgFile = GetOutputFile("html", "myColChartSvg.svg"); + + File.WriteAllText(file.FullName, htmlPage); + File.WriteAllText(svgFile.FullName, chart.ToSvg()); + + SaveAndCleanup(package); + } + } + + [TestMethod] + public void ExportBarChartWithCategories() + { + int[] values = { 5, 10, 15, 20 }; + using (var package = OpenPackage("HtmlSvgColChartCategories.xlsx", true)) + { + var ws = package.Workbook.Worksheets.Add("ShapeWs"); + + ws.Cells["A1"].Value = "Value"; + ws.Cells["B1"].Value = "Title"; + + for (int i = 0; i < values.Count() * 2; i++) + { + if (i >= values.Count()) + { + ws.Cells[i + 2, 1].Value = -values[i - values.Count()]; + } + else + { + ws.Cells[i + 2, 1].Value = values[i]; + } + } + + var chart = ws.Drawings.AddBarChart("myColChart", OfficeOpenXml.Drawing.Chart.eBarChartType.ColumnClustered); + chart.Series.Add(ws.Cells["A1:A10"]); + + chart.Fill.Color = System.Drawing.Color.BlanchedAlmond; + chart.Series[0].Fill.Color = System.Drawing.Color.LightCoral; + chart.SetPixelWidth(250); + + var exporter = ws.Cells["A1:C20"].CreateHtmlExporter(); + + exporter.Settings.Drawings.Include = ePictureInclude.IncludeInHtmlOnly; + exporter.Settings.Drawings.DrawTypeInclude = eDrawingInclude.Charts; + + var htmlPage = exporter.GetSinglePage(); + + var file = GetOutputFile("html", "colChartCats.html"); + + SaveAndCleanup(package); + + File.WriteAllText(file.FullName, htmlPage); + } + } + } +} diff --git a/src/EPPlusTest/Issues/FormulaCalculationIssues.cs b/src/EPPlusTest/Issues/FormulaCalculationIssues.cs index c29d104049..2544b23286 100644 --- a/src/EPPlusTest/Issues/FormulaCalculationIssues.cs +++ b/src/EPPlusTest/Issues/FormulaCalculationIssues.cs @@ -1626,7 +1626,6 @@ public void s1048() // Attach the logger before the calculation is performed. p.Workbook.FormulaParserManager.AttachLogger(logfile); - //ws.Cells["C56"].Calculate(new ExcelCalculationOption() { AllowCircularReferences = true }); p.Workbook.CalcMode = ExcelCalcMode.Manual; p.Workbook.FullCalcOnLoad = false; @@ -1650,5 +1649,4 @@ public void s1048() } } } -} - +} \ No newline at end of file