From 74a6bc47e034ce84c31f96c70f63394c515f5877 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jan=20K=C3=A4llman?= Date: Thu, 4 Jun 2026 13:49:21 +0200 Subject: [PATCH 01/82] Fixed some trendline issues --- .../Chart/ColumnChartTests.cs | 2 +- .../Chart/LineChartToSvgTests.cs | 20 +++++++------- .../Chart/TrendlineTests.cs | 6 ++--- .../Renderer/Chart/ChartAxisRenderer.cs | 2 +- .../Renderer/Chart/ChartLegendRenderer.cs | 4 +-- .../BarColumnChartTypeDrawer.cs | 9 ++++++- .../Trendlines/ChartTrendlineRenderer.cs | 15 ++++++----- src/EPPlus/Drawing/Renderer/ChartRenderer.cs | 26 +++++++++++++++++-- 8 files changed, 58 insertions(+), 26 deletions(-) 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/LineChartToSvgTests.cs b/src/EPPlus.DrawingRenderer.Tests/Chart/LineChartToSvgTests.cs index 52eab80bd2..fd4499de0d 100644 --- a/src/EPPlus.DrawingRenderer.Tests/Chart/LineChartToSvgTests.cs +++ b/src/EPPlus.DrawingRenderer.Tests/Chart/LineChartToSvgTests.cs @@ -19,17 +19,17 @@ public void GenerateSvgForLineCharts_sheet1() { var ws = p.Workbook.Worksheets[0]; - //var ix = 6; - //var c = ws.Drawings[ix]; - //var svg = c.ToSvg(); - //SaveTextFileToWorkbook($"svg\\ChartForSvg_ind{ix++}.svg", svg); + var ix = 6; + var c = ws.Drawings[ix]; + var svg = c.ToSvg(); + SaveTextFileToWorkbook($"svg\\ChartForSvg_ind{ix++}.svg", svg); - var ix = 0; - foreach (ExcelChart c in ws.Drawings) - { - var svg = c.ToSvg(); - SaveTextFileToWorkbook($"svg\\ChartForSvg{ix++}.svg", svg); - } + //var ix = 0; + //foreach (ExcelChart c in ws.Drawings) + //{ + // var svg = c.ToSvg(); + // SaveTextFileToWorkbook($"svg\\ChartForSvg{ix++}.svg", svg); + //} } } diff --git a/src/EPPlus.DrawingRenderer.Tests/Chart/TrendlineTests.cs b/src/EPPlus.DrawingRenderer.Tests/Chart/TrendlineTests.cs index e9df670ed8..842aee6627 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; @@ -36,7 +36,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/Drawing/Renderer/Chart/ChartAxisRenderer.cs b/src/EPPlus/Drawing/Renderer/Chart/ChartAxisRenderer.cs index 7f3eb4c721..8a7b34ef4a 100644 --- a/src/EPPlus/Drawing/Renderer/Chart/ChartAxisRenderer.cs +++ b/src/EPPlus/Drawing/Renderer/Chart/ChartAxisRenderer.cs @@ -203,7 +203,7 @@ private double GetTextWidest(ExcelChartAxisStandard ax) widest = m.Width; } } - return widest.PointToPixel(); + return widest; } public List Values diff --git a/src/EPPlus/Drawing/Renderer/Chart/ChartLegendRenderer.cs b/src/EPPlus/Drawing/Renderer/Chart/ChartLegendRenderer.cs index febf0e50b5..bfe4e19fc0 100644 --- a/src/EPPlus/Drawing/Renderer/Chart/ChartLegendRenderer.cs +++ b/src/EPPlus/Drawing/Renderer/Chart/ChartLegendRenderer.cs @@ -54,7 +54,7 @@ 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: @@ -603,7 +603,7 @@ private RectRenderItem GetBarSeriesIcon(ExcelChart ct, ExcelChartStandardSerie c 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; diff --git a/src/EPPlus/Drawing/Renderer/Chart/ChartTypeDrawers/BarColumnChartTypeDrawer.cs b/src/EPPlus/Drawing/Renderer/Chart/ChartTypeDrawers/BarColumnChartTypeDrawer.cs index d91018e3b1..8c7b661dcf 100644 --- a/src/EPPlus/Drawing/Renderer/Chart/ChartTypeDrawers/BarColumnChartTypeDrawer.cs +++ b/src/EPPlus/Drawing/Renderer/Chart/ChartTypeDrawers/BarColumnChartTypeDrawer.cs @@ -53,7 +53,14 @@ internal BarColumnChartTypeDrawer(ChartRenderer svgChart, ExcelBarChart chartTyp ExcelChartAxisStandard.CalculateStacked100(_valValues); } - CreateTrendlines(chartType, _catValues, _valValues); + //if(chartType.IsTypeBar()) + //{ + // CreateTrendlines(chartType, _valValues, _catValues); + //} + //else + //{ + CreateTrendlines(chartType, _catValues, _valValues); + //} } internal override void DrawSeries() diff --git a/src/EPPlus/Drawing/Renderer/Chart/ChartTypeDrawers/Trendlines/ChartTrendlineRenderer.cs b/src/EPPlus/Drawing/Renderer/Chart/ChartTypeDrawers/Trendlines/ChartTrendlineRenderer.cs index fa9ad5c115..e92028beae 100644 --- a/src/EPPlus/Drawing/Renderer/Chart/ChartTypeDrawers/Trendlines/ChartTrendlineRenderer.cs +++ b/src/EPPlus/Drawing/Renderer/Chart/ChartTypeDrawers/Trendlines/ChartTrendlineRenderer.cs @@ -47,8 +47,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(); @@ -177,7 +177,7 @@ private void CreateDatalabel() DataLabel.TopMargin = DataLabel.BottomMargin = 2; //Set datalabel position. - if(DataLabel.Left - (DataLabel.Width + 5) > ChartRenderer.Bounds.Right) + if(DataLabel.Left + (DataLabel.Width + 5) > ChartRenderer.Bounds.Right) { DataLabel.Left = ChartRenderer.Bounds.Right - DataLabel.Width; } @@ -215,8 +215,6 @@ 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 +222,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 +237,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++) { diff --git a/src/EPPlus/Drawing/Renderer/ChartRenderer.cs b/src/EPPlus/Drawing/Renderer/ChartRenderer.cs index e37f6fec9a..cc61266c15 100644 --- a/src/EPPlus/Drawing/Renderer/ChartRenderer.cs +++ b/src/EPPlus/Drawing/Renderer/ChartRenderer.cs @@ -84,6 +84,16 @@ 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); } @@ -92,7 +102,7 @@ private void SetAxisPositionsFromPlotarea() PlaceHorizontalAxis(HorizontalAxis); //Make sure the horizontal axis is moved up if the vertical axis has a negative minimum value, so that the 0 value is at the correct position. - if (VerticalAxis.Axis.AxisType == eAxisType.Val && VerticalAxis.Min < 0D) + 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; @@ -162,8 +172,20 @@ private void PlaceVerticalAxis(ChartAxisRenderer verticalAxis) 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.AxisPosition; + if(verticalAxis.Axis.TickLabelPosition==eTickLabelPosition.High) { + axisPos = axisPos == eAxisPosition.Left ? eAxisPosition.Left : eAxisPosition.Right; + } + //if(verticalAxis.Axis.TickLabelPosition==eTickLabelPosition.NextTo) + //{ + + // verticalAxis.Rectangle.Left = Plotarea.Group.Left - verticalAxis.Rectangle.Width; + // verticalAxis.Line.X1 = verticalAxis.Line.X2 = (float)Plotarea.Group.Left; + //} + //else if (axisPos == eAxisPosition.Left) + if (axisPos == eAxisPosition.Left) + { verticalAxis.Rectangle.Left = Plotarea.Group.Left - verticalAxis.Rectangle.Width; verticalAxis.Line.X1 = verticalAxis.Line.X2 = (float)Plotarea.Group.Left; } From e16b4c2306a8e86f2b60a3c6da730e4895f6e572 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jan=20K=C3=A4llman?= Date: Fri, 5 Jun 2026 14:36:19 +0200 Subject: [PATCH 02/82] Removed, renamed and fixed a few issues --- .../Chart/LineChartToSvgTests.cs | 24 ++++- .../RenderItems/GradientFill.cs | 4 + src/EPPlus.DrawingRenderer/Svg/IRender.cs | 8 +- .../Renderer/Chart/ChartLegendRenderer.cs | 2 +- .../Renderer/Chart/ChartTitleRenderer.cs | 9 +- .../ChartTypeDrawers/LineChartTypeDrawer.cs | 1 + .../{ => Chart}/DrawingLegendSerie.cs | 2 +- .../DrawingLegendSeriesIcon.cs} | 2 +- .../Renderer/{ => Chart}/LineMarkerHelper.cs | 2 +- src/EPPlus/Drawing/Renderer/ChartRenderer.cs | 29 ++++-- .../Fill/DrawingRenderGradientFill.cs | 5 ++ src/EPPlus/Drawing/Renderer/SvgRange.cs | 89 ------------------- 12 files changed, 69 insertions(+), 108 deletions(-) rename src/EPPlus/Drawing/Renderer/{ => Chart}/DrawingLegendSerie.cs (96%) rename src/EPPlus/Drawing/Renderer/{SvgLegendSeriesIcon.cs => Chart/DrawingLegendSeriesIcon.cs} (85%) rename src/EPPlus/Drawing/Renderer/{ => Chart}/LineMarkerHelper.cs (99%) delete mode 100644 src/EPPlus/Drawing/Renderer/SvgRange.cs diff --git a/src/EPPlus.DrawingRenderer.Tests/Chart/LineChartToSvgTests.cs b/src/EPPlus.DrawingRenderer.Tests/Chart/LineChartToSvgTests.cs index fd4499de0d..10df58204e 100644 --- a/src/EPPlus.DrawingRenderer.Tests/Chart/LineChartToSvgTests.cs +++ b/src/EPPlus.DrawingRenderer.Tests/Chart/LineChartToSvgTests.cs @@ -155,9 +155,9 @@ public void GenerateSvgForCharts_SecondaryAxis() using (var p = OpenTemplatePackage("ChartForSvg_SecondaryAxis.xlsx")) { var ws = p.Workbook.Worksheets[0]; - //var ix = 3; + //var ix = 1; //var c = ws.Drawings[ix]; - //var svg = renderer.RenderDrawingToSvg(c); + //var svg = c.ToSvg(); //SaveTextFileToWorkbook($"svg\\ChartForSvg_sheet2_{ix++}.svg", svg); var ix = 1; foreach (ExcelChart c in ws.Drawings) @@ -186,6 +186,26 @@ public void GenerateSvgForCharts_SecondaryAxis_sheet2() } } } + [TestMethod] + public void GenerateSvgForCharts_SecondaryAxis_sheet3() + { + ExcelPackage.License.SetNonCommercialOrganization("EPPlus Project"); + using (var p = OpenTemplatePackage("ChartForSvg_SecondaryAxis.xlsx")) + { + var ws = p.Workbook.Worksheets[2]; + //var ix = 1; + //var c = ws.Drawings[ix]; + //var svg = c.ToSvg(); + //SaveTextFileToWorkbook($"svg\\ChartForSvg_sheet2_{ix++}.svg", svg); + var ix = 1; + foreach (ExcelChart c in ws.Drawings) + { + var svg = c.ToSvg(); + SaveTextFileToWorkbook($"svg\\ChartForSvg_SecAxis{ix++}.svg", svg); + } + } + } + [TestMethod] public void GenerateSimplestChart() { 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/Svg/IRender.cs b/src/EPPlus.DrawingRenderer/Svg/IRender.cs index b426d81e16..88480d371e 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); diff --git a/src/EPPlus/Drawing/Renderer/Chart/ChartLegendRenderer.cs b/src/EPPlus/Drawing/Renderer/Chart/ChartLegendRenderer.cs index bfe4e19fc0..3919ead744 100644 --- a/src/EPPlus/Drawing/Renderer/Chart/ChartLegendRenderer.cs +++ b/src/EPPlus/Drawing/Renderer/Chart/ChartLegendRenderer.cs @@ -11,12 +11,12 @@ Date Author Change 27/11/2025 EPPlus Software AB EPPlus 9 *************************************************************************************************/ using EPPlus.DrawingRenderer.RenderItems; -using EPPlus.Export.ImageRenderer.Svg; 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.Interfaces.Drawing.Text; using OfficeOpenXml.Style; diff --git a/src/EPPlus/Drawing/Renderer/Chart/ChartTitleRenderer.cs b/src/EPPlus/Drawing/Renderer/Chart/ChartTitleRenderer.cs index e31fed1184..bee3ce50ef 100644 --- a/src/EPPlus/Drawing/Renderer/Chart/ChartTitleRenderer.cs +++ b/src/EPPlus/Drawing/Renderer/Chart/ChartTitleRenderer.cs @@ -120,7 +120,14 @@ private void SetAxisTitleRect(ChartRenderer sc, ChartAxisRenderer axis) Rectangle.Left = sc.Chart.HasLegend && sc.Chart.Legend.Position == eLegendPosition.Right || sc.Chart.Legend.Position == eLegendPosition.TopRight ? sc.Legend.Rectangle.Left - Rectangle.Width - margin : sc.Bounds.Right - Rectangle.Width - margin; break; case eAxisPosition.Bottom: - Rectangle.Top = sc.ChartArea.Rectangle.Height - margin - Rectangle.Height; + if(sc.HorizontalAxis!=null && sc.HorizontalAxis.Axis.HasTitle && sc.HorizontalAxis.Axis.AxisPosition==eAxisPosition.Bottom) + { + Rectangle.Top = sc.HorizontalAxis.Rectangle.Bottom + margin; + } + else + { + Rectangle.Top = sc.ChartArea.Rectangle.Height - margin - Rectangle.Height; + } Rectangle.Left = GetHorizontalLeft(sc); break; case eAxisPosition.Top: diff --git a/src/EPPlus/Drawing/Renderer/Chart/ChartTypeDrawers/LineChartTypeDrawer.cs b/src/EPPlus/Drawing/Renderer/Chart/ChartTypeDrawers/LineChartTypeDrawer.cs index 6c6785f842..89925263fe 100644 --- a/src/EPPlus/Drawing/Renderer/Chart/ChartTypeDrawers/LineChartTypeDrawer.cs +++ b/src/EPPlus/Drawing/Renderer/Chart/ChartTypeDrawers/LineChartTypeDrawer.cs @@ -6,6 +6,7 @@ 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; diff --git a/src/EPPlus/Drawing/Renderer/DrawingLegendSerie.cs b/src/EPPlus/Drawing/Renderer/Chart/DrawingLegendSerie.cs similarity index 96% rename from src/EPPlus/Drawing/Renderer/DrawingLegendSerie.cs rename to src/EPPlus/Drawing/Renderer/Chart/DrawingLegendSerie.cs index 4809d8098b..652f7bf936 100644 --- a/src/EPPlus/Drawing/Renderer/DrawingLegendSerie.cs +++ b/src/EPPlus/Drawing/Renderer/Chart/DrawingLegendSerie.cs @@ -18,7 +18,7 @@ Date Author Change namespace EPPlusImageRenderer.Svg { - internal class DrawingLegendSerie : SvgLegendSeriesIcon + internal class DrawingLegendSerie : DrawingLegendSeriesIcon { internal DrawingTextbody Textbox { get; set; } 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 99% rename from src/EPPlus/Drawing/Renderer/LineMarkerHelper.cs rename to src/EPPlus/Drawing/Renderer/Chart/LineMarkerHelper.cs index 7a00fbe32c..53d2015c52 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 { diff --git a/src/EPPlus/Drawing/Renderer/ChartRenderer.cs b/src/EPPlus/Drawing/Renderer/ChartRenderer.cs index cc61266c15..f20872b5ae 100644 --- a/src/EPPlus/Drawing/Renderer/ChartRenderer.cs +++ b/src/EPPlus/Drawing/Renderer/ChartRenderer.cs @@ -99,7 +99,7 @@ private void SetAxisPositionsFromPlotarea() 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 (HorizontalAxis.Axis.TickLabelPosition == eTickLabelPosition.NextTo && VerticalAxis.Axis.AxisType == eAxisType.Val && VerticalAxis.Min < 0D) @@ -122,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) { @@ -143,7 +143,7 @@ private void PlaceHorizontalAxis(ChartAxisRenderer horizontalAxis) } else { - horizontalAxis.Rectangle.Top = Plotarea.Rectangle.Top - horizontalAxis.Rectangle.Height; + horizontalAxis.Rectangle.Top = Plotarea.Group.Top - horizontalAxis.Rectangle.Height; horizontalAxis.Line.Y1 = horizontalAxis.Line.Y2 = (float)Plotarea.Group.Top; } } @@ -151,14 +151,27 @@ private void PlaceHorizontalAxis(ChartAxisRenderer horizontalAxis) { horizontalAxis.Title.Rectangle.Height = Bounds.Height / 4; horizontalAxis.Title.Rectangle.Width = horizontalAxis.Rectangle?.Width ?? Plotarea.Rectangle.Width; - //horizontalAxis.Title.InitTextBox(); - if (horizontalAxis.Axis.AxisPosition == eAxisPosition.Bottom) + if (horizontalAxis.Axis.Deleted) { - horizontalAxis.Title.TextBox.Top = horizontalAxis.Rectangle.Bottom; + 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 { - horizontalAxis.Title.TextBox.Top = horizontalAxis.Rectangle.Top - horizontalAxis.Title.TextBox.Height; + if (horizontalAxis.Axis.AxisPosition == eAxisPosition.Bottom) + { + horizontalAxis.Title.TextBox.Top = horizontalAxis.Rectangle.Bottom; + } + else + { + horizontalAxis.Title.TextBox.Top = horizontalAxis.Rectangle.Top - horizontalAxis.Title.TextBox.Height; + } } horizontalAxis.Title.TextBox.Left = Plotarea.Group.Left + (Plotarea.Rectangle.Width / 2) - (horizontalAxis.Title.TextBox.Width / 2); } 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/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 From 105f3f1194e30ae43d120b11c1619f5d434db6e3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jan=20K=C3=A4llman?= Date: Fri, 5 Jun 2026 16:17:13 +0200 Subject: [PATCH 03/82] Added new ActualPosition property for axis --- .../Drawing/Chart/ExcelChartAxisStandard.cs | 31 +++++++++++++++++++ 1 file changed, 31 insertions(+) diff --git a/src/EPPlus/Drawing/Chart/ExcelChartAxisStandard.cs b/src/EPPlus/Drawing/Chart/ExcelChartAxisStandard.cs index 4ce29e17e2..2f23144d13 100644 --- a/src/EPPlus/Drawing/Chart/ExcelChartAxisStandard.cs +++ b/src/EPPlus/Drawing/Chart/ExcelChartAxisStandard.cs @@ -158,6 +158,37 @@ 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 eAxisPosition ActualAxisPosition + { + get + { + var ap = AxisPosition; + if(ap==eAxisPosition.Left && LabelPosition==eTickLabelPosition.High) + { + return eAxisPosition.Right; + } + else if(ap==eAxisPosition.Right && LabelPosition==eTickLabelPosition.Low) + { + return eAxisPosition.Left; + } + else if(ap==eAxisPosition.Top && LabelPosition==eTickLabelPosition.Low) + { + return eAxisPosition.Bottom; + } + else if(ap==eAxisPosition.Bottom && LabelPosition==eTickLabelPosition.High) + { + return eAxisPosition.Top; + } + else + { + return ap; + } + } + } + /// /// Chart axis title /// public new ExcelChartTitleStandard Title From 181c1ee05c80e338ef16d4e93a6d018829966822 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ossian=20Edstr=C3=B6m?= Date: Mon, 8 Jun 2026 10:32:43 +0200 Subject: [PATCH 04/82] Re-implemented pie-chart and explosion --- .../ChartTypeDrawers/PieChartTypeDrawer.cs | 16 +++++---- .../Renderer/Chart/PieSliceRenderItem.cs | 35 ++++++++++++------- 2 files changed, 33 insertions(+), 18 deletions(-) diff --git a/src/EPPlus/Drawing/Renderer/Chart/ChartTypeDrawers/PieChartTypeDrawer.cs b/src/EPPlus/Drawing/Renderer/Chart/ChartTypeDrawers/PieChartTypeDrawer.cs index 4ac761a78b..bca35923c5 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,9 +170,12 @@ 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); + + //_groupItem.Left = ChartRenderer.Plotarea.Group.Left; + //_groupItem.Top = ChartRenderer.Plotarea.Group.Top; + + //_groupItem.TransformOrigin = new Coordinate(ChartRenderer.Plotarea.LeftMargin, ChartRenderer.Plotarea.TopMargin); Rectangle.Bounds.Name = "ChartDrawer"; @@ -263,8 +266,9 @@ 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); + //renderItems.AddRange(ChartAreaRenderItems); + //SeriesRenderItems.ForEach(x => _groupItem.AddChildItem(x)); } } } diff --git a/src/EPPlus/Drawing/Renderer/Chart/PieSliceRenderItem.cs b/src/EPPlus/Drawing/Renderer/Chart/PieSliceRenderItem.cs index 4570bd81ed..d06e699cb0 100644 --- a/src/EPPlus/Drawing/Renderer/Chart/PieSliceRenderItem.cs +++ b/src/EPPlus/Drawing/Renderer/Chart/PieSliceRenderItem.cs @@ -63,6 +63,7 @@ internal Coordinate GetEndPointLocal() PathRenderItem _slicePath; PathRenderItem _debugBoundsPath; + RectRenderItem _debugCircleCenter; bool ExistWithinRange(double target, double min, double max) { @@ -249,7 +250,7 @@ internal void ImportPathData(BoundingBox plotAreaBounds, BoundingBox globalAreaB _slicePath.Commands.Add(lineToStart); _slicePath.Commands.Add(arcCommand); - if (position == -1) + if (position != -1) { //Visualize all points AddDebugLines(moveCenter, plotAreaBounds); @@ -266,21 +267,16 @@ 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); + //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(); @@ -290,13 +286,23 @@ private void AddDebugLines(PathCommands moveCenter, BoundingBox bounds) 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(lineToTopLeft); _debugBoundsPath.Commands.Add(lineToTopRight); ////_debugBoundsPath.Commands.Add(lineToSliceCenter); _debugBoundsPath.Commands.Add(lineToBottomRight); _debugBoundsPath.Commands.Add(lineToBottomLeft); - //_debugBoundsPath.Commands.Add(end); - + //_debugBoundsPath.Commands.Add(lineToTopLeft); + _debugBoundsPath.Commands.Add(end); + + //Render green dot at circle center + _debugCircleCenter = new RectRenderItem(bounds); + _debugCircleCenter.Left = _circleCenter.Left; + _debugCircleCenter.Top = _circleCenter.Top; + _debugCircleCenter.Bounds.Left -= 2.5d; + _debugCircleCenter.Bounds.Top -= 2.5d; + _debugCircleCenter.Bounds.Width = 5d; + _debugCircleCenter.Bounds.Height = 5d; + _debugCircleCenter.FillColor = "green"; } internal void ImportStlyeInfo(ExcelChartDataPoint dp, ExcelPieChart chartType) @@ -308,6 +314,10 @@ 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 @@ -317,6 +327,7 @@ internal void AppendGroupItem(GroupRenderItem group) { //adding the debug lines _innerGroup.AddChildItem(_debugBoundsPath); + _innerGroup.AddChildItem(_debugCircleCenter); } //The group containing all slices From 65d52da34945626f40366a9258eb4cdd3bb916dc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jan=20K=C3=A4llman?= Date: Mon, 8 Jun 2026 10:53:29 +0200 Subject: [PATCH 05/82] Fixed trendline formulas to add superscript to textboxs --- .../RichTextFormatDrawing.cs | 7 +++ .../Drawing/Chart/ExcelChartAxisStandard.cs | 47 +++++++++++++++---- .../Drawing/Chart/enums/eAxisPosition.cs | 35 ++++++++++++++ .../Trendlines/ChartTrendlineRenderer.cs | 31 +++++++++++- .../Trendlines/SvgTrendlineDataLabel.cs | 10 ---- .../Issues/FormulaCalculationIssues.cs | 4 +- 6 files changed, 110 insertions(+), 24 deletions(-) delete mode 100644 src/EPPlus/Drawing/Renderer/Chart/ChartTypeDrawers/Trendlines/SvgTrendlineDataLabel.cs diff --git a/src/EPPlus.DrawingRenderer/RenderItems/Textbox/RichTextFormatDrawing/RichTextFormatDrawing.cs b/src/EPPlus.DrawingRenderer/RenderItems/Textbox/RichTextFormatDrawing/RichTextFormatDrawing.cs index 07c5e253e9..d33c87c048 100644 --- a/src/EPPlus.DrawingRenderer/RenderItems/Textbox/RichTextFormatDrawing/RichTextFormatDrawing.cs +++ b/src/EPPlus.DrawingRenderer/RenderItems/Textbox/RichTextFormatDrawing/RichTextFormatDrawing.cs @@ -101,5 +101,12 @@ public RichTextFormatDrawing(string text, string fontFamily, float size, bool bo StrikeType = eDrawingStrikeType.No; UnderlineType = eDrawingUnderLineType.None; } + + public RichTextFormatDrawing(FontFormatBase defaultParagraphFont) + { + Family = defaultParagraphFont.Family; + SubFamily = defaultParagraphFont.SubFamily; + Size = defaultParagraphFont.Size; + } } } diff --git a/src/EPPlus/Drawing/Chart/ExcelChartAxisStandard.cs b/src/EPPlus/Drawing/Chart/ExcelChartAxisStandard.cs index 2f23144d13..de5a081621 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; @@ -161,30 +162,58 @@ 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 eAxisPosition ActualAxisPosition + public eActualAxisPosition ActualAxisPosition { get { var ap = AxisPosition; - if(ap==eAxisPosition.Left && LabelPosition==eTickLabelPosition.High) + if(ap==eAxisPosition.Right && LabelPosition==eTickLabelPosition.Low) { - return eAxisPosition.Right; + if (_chart.Axis.FirstOrDefault(x => x.AxisPosition == eAxisPosition.Left)?.LabelPosition == eTickLabelPosition.Low) + { + return eActualAxisPosition.Right; + } + else + { + return eActualAxisPosition.RightSecond; + } } - else if(ap==eAxisPosition.Right && LabelPosition==eTickLabelPosition.Low) + else if(ap==eAxisPosition.Right && LabelPosition==eTickLabelPosition.High) { - return eAxisPosition.Left; + if(_chart.Axis.FirstOrDefault(x => x.AxisPosition == eAxisPosition.Left)?.LabelPosition == eTickLabelPosition.High) + { + return eActualAxisPosition.Left; + } + else + { + return eActualAxisPosition.LeftSecond; + } } else if(ap==eAxisPosition.Top && LabelPosition==eTickLabelPosition.Low) { - return eAxisPosition.Bottom; + if (_chart.Axis.FirstOrDefault(x => x.AxisPosition == eAxisPosition.Bottom)?.LabelPosition == eTickLabelPosition.Low) + { + return eActualAxisPosition.Bottom; + } + else + { + return eActualAxisPosition.BottomSecond; + } } else if(ap==eAxisPosition.Bottom && LabelPosition==eTickLabelPosition.High) - { - return eAxisPosition.Top; + { + if(_chart.Axis.FirstOrDefault(x => x.AxisPosition == eAxisPosition.Bottom)?.LabelPosition == eTickLabelPosition.High) + { + return eActualAxisPosition.Top; + } + else + { + return eActualAxisPosition.TopSecond; + } } else { - return ap; + return (eActualAxisPosition)ap; } } } 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/Renderer/Chart/ChartTypeDrawers/Trendlines/ChartTrendlineRenderer.cs b/src/EPPlus/Drawing/Renderer/Chart/ChartTypeDrawers/Trendlines/ChartTrendlineRenderer.cs index e92028beae..1c03130bff 100644 --- a/src/EPPlus/Drawing/Renderer/Chart/ChartTypeDrawers/Trendlines/ChartTrendlineRenderer.cs +++ b/src/EPPlus/Drawing/Renderer/Chart/ChartTypeDrawers/Trendlines/ChartTrendlineRenderer.cs @@ -12,6 +12,7 @@ Date Author Change *************************************************************************************************/ using EPPlus.DrawingRenderer; using EPPlus.DrawingRenderer.RenderItems; +using EPPlus.DrawingRenderer.RenderItems.Textbox; using EPPlusImageRenderer; using EPPlusImageRenderer.RenderItems; using EPPlusImageRenderer.Svg; @@ -172,7 +173,8 @@ private void CreateDatalabel() labelText += RSquare; } lbl.TextBody.Paragraphs[0].HorizontalAlignment = OfficeOpenXml.Drawing.eTextAlignment.Center; - DataLabel.ImportParagraph(lbl.TextBody.Paragraphs[0], 0, labelText); + DataLabel.ImportParagraph(lbl.TextBody.Paragraphs[0], 0, ""); + AddLblText(DataLabel, labelText); DataLabel.LeftMargin = DataLabel.RightMargin = 4; DataLabel.TopMargin = DataLabel.BottomMargin = 2; @@ -211,6 +213,31 @@ 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:"); + + if (ix < 0) + { + lbl.TextBody.Paragraphs[0].AddText(labelText); + } + 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); + } + + if(pIx < labelText.Length) + { + lbl.TextBody.Paragraphs[0].AddText(labelText.Substring(pIx, labelText.Length - pIx)); + } + } + private void CalculateLinear() { var n = _xSerie.Count; @@ -306,7 +333,7 @@ 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}|"; + Formula = $"y={intercept:G5}e|ss:{slope:G3}|"; RSquare = $"R²={r2:N4}"; } private void CalculateLogarithmic() 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/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 From 74cef878080c918e3beaa80ee3e9123d9137afc4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ossian=20Edstr=C3=B6m?= Date: Mon, 8 Jun 2026 12:33:42 +0200 Subject: [PATCH 06/82] Added debug properties to pie slices --- .../Renderer/Chart/PieSliceRenderItem.cs | 51 ++++++++++++++----- 1 file changed, 38 insertions(+), 13 deletions(-) diff --git a/src/EPPlus/Drawing/Renderer/Chart/PieSliceRenderItem.cs b/src/EPPlus/Drawing/Renderer/Chart/PieSliceRenderItem.cs index d06e699cb0..587180a8be 100644 --- a/src/EPPlus/Drawing/Renderer/Chart/PieSliceRenderItem.cs +++ b/src/EPPlus/Drawing/Renderer/Chart/PieSliceRenderItem.cs @@ -58,10 +58,13 @@ 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; @@ -267,6 +270,8 @@ internal void ImportPathData(BoundingBox plotAreaBounds, BoundingBox globalAreaB /// private void AddDebugLines(PathCommands moveCenter, BoundingBox bounds) { + DebugItems = new List(); + //Render bounds for slice _debugBoundsPath = new PathRenderItem(bounds); _debugBoundsPath.BorderColor = "red"; @@ -294,17 +299,36 @@ private void AddDebugLines(PathCommands moveCenter, BoundingBox bounds) //_debugBoundsPath.Commands.Add(lineToTopLeft); _debugBoundsPath.Commands.Add(end); + DebugItems.Add(_debugBoundsPath); + //Render green dot at circle center - _debugCircleCenter = new RectRenderItem(bounds); - _debugCircleCenter.Left = _circleCenter.Left; - _debugCircleCenter.Top = _circleCenter.Top; - _debugCircleCenter.Bounds.Left -= 2.5d; - _debugCircleCenter.Bounds.Top -= 2.5d; - _debugCircleCenter.Bounds.Width = 5d; - _debugCircleCenter.Bounds.Height = 5d; - _debugCircleCenter.FillColor = "green"; + _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); @@ -323,11 +347,12 @@ internal void AppendGroupItem(GroupRenderItem group) //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); - _innerGroup.AddChildItem(_debugCircleCenter); + foreach(var debugItem in DebugItems) + { + _innerGroup.AddChildItem(debugItem); + } } //The group containing all slices From 2d73a982e200b7f2cf86792278591b6296ea4010 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ossian=20Edstr=C3=B6m?= Date: Mon, 8 Jun 2026 12:40:39 +0200 Subject: [PATCH 07/82] Deactivated debug props + cleanup --- .../Renderer/Chart/PieSliceRenderItem.cs | 34 ++----------------- 1 file changed, 2 insertions(+), 32 deletions(-) diff --git a/src/EPPlus/Drawing/Renderer/Chart/PieSliceRenderItem.cs b/src/EPPlus/Drawing/Renderer/Chart/PieSliceRenderItem.cs index 587180a8be..037532da46 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; @@ -155,19 +154,6 @@ 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; @@ -186,11 +172,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; @@ -253,7 +234,8 @@ internal void ImportPathData(BoundingBox plotAreaBounds, BoundingBox globalAreaB _slicePath.Commands.Add(lineToStart); _slicePath.Commands.Add(arcCommand); - if (position != -1) + //Change to != -1 to activate debug items + if (position == -1) { //Visualize all points AddDebugLines(moveCenter, plotAreaBounds); @@ -284,19 +266,13 @@ private void AddDebugLines(PathCommands moveCenter, BoundingBox bounds) //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(lineToTopLeft); _debugBoundsPath.Commands.Add(end); DebugItems.Add(_debugBoundsPath); @@ -512,12 +488,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); From 2a887cd4474cb1dca4c1ab9df8ae8dc574530c96 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ossian=20Edstr=C3=B6m?= Date: Mon, 8 Jun 2026 12:52:58 +0200 Subject: [PATCH 08/82] Simplified constructor --- .../Textbox/RichTextFormatDrawing/RichTextFormatDrawing.cs | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/src/EPPlus.DrawingRenderer/RenderItems/Textbox/RichTextFormatDrawing/RichTextFormatDrawing.cs b/src/EPPlus.DrawingRenderer/RenderItems/Textbox/RichTextFormatDrawing/RichTextFormatDrawing.cs index d33c87c048..cc4df26c5f 100644 --- a/src/EPPlus.DrawingRenderer/RenderItems/Textbox/RichTextFormatDrawing/RichTextFormatDrawing.cs +++ b/src/EPPlus.DrawingRenderer/RenderItems/Textbox/RichTextFormatDrawing/RichTextFormatDrawing.cs @@ -104,9 +104,7 @@ public RichTextFormatDrawing(string text, string fontFamily, float size, bool bo public RichTextFormatDrawing(FontFormatBase defaultParagraphFont) { - Family = defaultParagraphFont.Family; - SubFamily = defaultParagraphFont.SubFamily; - Size = defaultParagraphFont.Size; + SetFont(defaultParagraphFont); } } } From a3b7262281be21a02669aaf9d25be66a16ecccb5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ossian=20Edstr=C3=B6m?= Date: Mon, 8 Jun 2026 15:59:39 +0200 Subject: [PATCH 09/82] Fixed most trendline sub/superscript issues + rect --- .../Chart/TrendlineTests.cs | 22 +++++++++ .../Textbox/ParagraphRenderItem.cs | 45 +++++++++++++++++++ .../RenderItems/Textbox/RenderTextBody.cs | 22 +++++++++ .../RenderItems/Textbox/TextRunRenderItem.cs | 16 ++++++- .../Textbox/DrawingParagraphRenderItem.cs | 2 +- .../RenderItems/Textbox/DrawingTextBox.cs | 12 ++++- 6 files changed, 114 insertions(+), 5 deletions(-) diff --git a/src/EPPlus.DrawingRenderer.Tests/Chart/TrendlineTests.cs b/src/EPPlus.DrawingRenderer.Tests/Chart/TrendlineTests.cs index 842aee6627..219689525b 100644 --- a/src/EPPlus.DrawingRenderer.Tests/Chart/TrendlineTests.cs +++ b/src/EPPlus.DrawingRenderer.Tests/Chart/TrendlineTests.cs @@ -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() { diff --git a/src/EPPlus.DrawingRenderer/RenderItems/Textbox/ParagraphRenderItem.cs b/src/EPPlus.DrawingRenderer/RenderItems/Textbox/ParagraphRenderItem.cs index 2211eeb9ab..77188f41c7 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; @@ -8,6 +9,9 @@ using EPPlus.Graphics; using OfficeOpenXml.Interfaces.Drawing.Text; using OfficeOpenXml.Interfaces.RichText; +using System.Drawing; +using System.Text; +using static System.Net.Mime.MediaTypeNames; namespace EPPlus.Export.ImageRenderer.RenderItems.Shared { @@ -143,6 +147,11 @@ protected ParagraphRenderItem(BoundingBox parent, RenderTextBody textBody, IRich AddRichText(rtFormat); } + protected ParagraphRenderItem(BoundingBox parent, RenderTextBody textBody, IRichTextFormatDrawing rtFormat) : this(parent, textBody, false) + { + AddRichText(rtFormat); + } + protected double GetAlignmentHorizontal(TextAlignment txAlignment) { double x = 0; @@ -332,15 +341,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..64561749c0 100644 --- a/src/EPPlus.DrawingRenderer/RenderItems/Textbox/RenderTextBody.cs +++ b/src/EPPlus.DrawingRenderer/RenderItems/Textbox/RenderTextBody.cs @@ -125,6 +125,28 @@ 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; + } + } + private void AdjustAndAddParagraph(ParagraphRenderItem paragraph) { paragraph.Bounds.Name = $"Container{Paragraphs.Count}"; 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/Drawing/Renderer/RenderItems/Textbox/DrawingParagraphRenderItem.cs b/src/EPPlus/Drawing/Renderer/RenderItems/Textbox/DrawingParagraphRenderItem.cs index 6bc07e58c6..7ebdb35ebf 100644 --- a/src/EPPlus/Drawing/Renderer/RenderItems/Textbox/DrawingParagraphRenderItem.cs +++ b/src/EPPlus/Drawing/Renderer/RenderItems/Textbox/DrawingParagraphRenderItem.cs @@ -55,7 +55,7 @@ public DrawingParagraphRenderItem(DrawingTextbody textBody, BoundingBox parent, 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()); diff --git a/src/EPPlus/Drawing/Renderer/RenderItems/Textbox/DrawingTextBox.cs b/src/EPPlus/Drawing/Renderer/RenderItems/Textbox/DrawingTextBox.cs index 9bdb7605d2..9e9812e88b 100644 --- a/src/EPPlus/Drawing/Renderer/RenderItems/Textbox/DrawingTextBox.cs +++ b/src/EPPlus/Drawing/Renderer/RenderItems/Textbox/DrawingTextBox.cs @@ -26,7 +26,7 @@ private void Init(ExcelDrawing drawing, BoundingBox parent, double maxWidth, dou _rectangle = new RectRenderItem(parent); TextBody = new DrawingTextbody(drawing, Rectangle.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) @@ -265,10 +265,18 @@ public override void AppendRenderItems(List renderItems) } } groupItem.RenderItems.Add(titleItem); + + if(TextBody.AutoSize) + { + TextBody.ApplyAutoSize(); + rect.Width = TextBody.Width + RightMargin; + rect.Height = TextBody.Height + BottomMargin; + } + //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.Left = LeftMargin; TextBody.Bounds.Top = TopMargin; TextBody.AppendRenderItems(groupItem.RenderItems); } From b83e3310a8e8655720efbc462893d2fe1facf14d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ossian=20Edstr=C3=B6m?= Date: Mon, 8 Jun 2026 16:22:28 +0200 Subject: [PATCH 10/82] Added margins to rect --- .../RenderItems/Textbox/ParagraphRenderItem.cs | 2 +- .../ChartTypeDrawers/Trendlines/ChartTrendlineRenderer.cs | 2 +- .../Drawing/Renderer/RenderItems/Textbox/DrawingTextBox.cs | 4 ++-- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/EPPlus.DrawingRenderer/RenderItems/Textbox/ParagraphRenderItem.cs b/src/EPPlus.DrawingRenderer/RenderItems/Textbox/ParagraphRenderItem.cs index 77188f41c7..7281accd8f 100644 --- a/src/EPPlus.DrawingRenderer/RenderItems/Textbox/ParagraphRenderItem.cs +++ b/src/EPPlus.DrawingRenderer/RenderItems/Textbox/ParagraphRenderItem.cs @@ -303,7 +303,7 @@ protected void WrapTextFragmentsAndGenerateTextRuns() lineIdx++; } } - Bounds.Width = widthOfLargestLine; + Bounds.Width = widthOfLargestLine + RightMargin; Bounds.Height = combinedHeight; } diff --git a/src/EPPlus/Drawing/Renderer/Chart/ChartTypeDrawers/Trendlines/ChartTrendlineRenderer.cs b/src/EPPlus/Drawing/Renderer/Chart/ChartTypeDrawers/Trendlines/ChartTrendlineRenderer.cs index 1c03130bff..465335ceb8 100644 --- a/src/EPPlus/Drawing/Renderer/Chart/ChartTypeDrawers/Trendlines/ChartTrendlineRenderer.cs +++ b/src/EPPlus/Drawing/Renderer/Chart/ChartTypeDrawers/Trendlines/ChartTrendlineRenderer.cs @@ -333,7 +333,7 @@ private void CalculateExponential() var r2 = Math.Pow(Pearson.PearsonImpl(_ySerie.Cast(), GetExponentialSerie(slope, intercept)), 2); Coefficients = [slope, intercept]; - Formula = $"y={intercept:G5}e|ss:{slope:G3}|"; + Formula = $"y={intercept:G5}e|ss:{slope:G3}x|"; RSquare = $"R²={r2:N4}"; } private void CalculateLogarithmic() diff --git a/src/EPPlus/Drawing/Renderer/RenderItems/Textbox/DrawingTextBox.cs b/src/EPPlus/Drawing/Renderer/RenderItems/Textbox/DrawingTextBox.cs index 9e9812e88b..78922fdf69 100644 --- a/src/EPPlus/Drawing/Renderer/RenderItems/Textbox/DrawingTextBox.cs +++ b/src/EPPlus/Drawing/Renderer/RenderItems/Textbox/DrawingTextBox.cs @@ -269,8 +269,8 @@ public override void AppendRenderItems(List renderItems) if(TextBody.AutoSize) { TextBody.ApplyAutoSize(); - rect.Width = TextBody.Width + RightMargin; - rect.Height = TextBody.Height + BottomMargin; + rect.Width = TextBody.Width + RightMargin + LeftMargin; + rect.Height = TextBody.Height + BottomMargin + RightMargin; } //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. From 344d7d58928c6a2cbcf05dd583aa6ce6822c8252 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ossian=20Edstr=C3=B6m?= Date: Mon, 8 Jun 2026 17:03:32 +0200 Subject: [PATCH 11/82] Half fix for duplication --- .../RenderItems/Textbox/ParagraphRenderItem.cs | 2 +- .../ChartTypeDrawers/Trendlines/ChartTrendlineRenderer.cs | 5 ++++- .../RenderItems/Textbox/DrawingParagraphRenderItem.cs | 6 +++--- .../Drawing/Renderer/RenderItems/Textbox/DrawingTextBox.cs | 3 +-- 4 files changed, 9 insertions(+), 7 deletions(-) diff --git a/src/EPPlus.DrawingRenderer/RenderItems/Textbox/ParagraphRenderItem.cs b/src/EPPlus.DrawingRenderer/RenderItems/Textbox/ParagraphRenderItem.cs index 7281accd8f..a0c642cafe 100644 --- a/src/EPPlus.DrawingRenderer/RenderItems/Textbox/ParagraphRenderItem.cs +++ b/src/EPPlus.DrawingRenderer/RenderItems/Textbox/ParagraphRenderItem.cs @@ -327,7 +327,7 @@ protected double CalculatePrevWidthBasedOnAlignment(double lineDist) protected void SetHorizontalAlignment(double widthOfLargestLine) { - if (HorizontalAlignment == TextAlignment.Center && AutoSize && _centerAdjustment != null && TextIfEmptyIsNull) + if (HorizontalAlignment == TextAlignment.Center && _centerAdjustment != null /*&& AutoSize && TextIfEmptyIsNull*/) { //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. diff --git a/src/EPPlus/Drawing/Renderer/Chart/ChartTypeDrawers/Trendlines/ChartTrendlineRenderer.cs b/src/EPPlus/Drawing/Renderer/Chart/ChartTypeDrawers/Trendlines/ChartTrendlineRenderer.cs index 465335ceb8..646eb4e8a7 100644 --- a/src/EPPlus/Drawing/Renderer/Chart/ChartTypeDrawers/Trendlines/ChartTrendlineRenderer.cs +++ b/src/EPPlus/Drawing/Renderer/Chart/ChartTypeDrawers/Trendlines/ChartTrendlineRenderer.cs @@ -13,6 +13,7 @@ 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; @@ -217,7 +218,9 @@ private void AddLblText(DrawingTextBox lbl, string labelText) { int pIx = 0; var ix = labelText.IndexOf("|ss:"); - + + //lbl.TextBody.Paragraphs[0].HorizontalAlignment = TextAlignment.Center; + if (ix < 0) { lbl.TextBody.Paragraphs[0].AddText(labelText); diff --git a/src/EPPlus/Drawing/Renderer/RenderItems/Textbox/DrawingParagraphRenderItem.cs b/src/EPPlus/Drawing/Renderer/RenderItems/Textbox/DrawingParagraphRenderItem.cs index 7ebdb35ebf..bc75b2f3cd 100644 --- a/src/EPPlus/Drawing/Renderer/RenderItems/Textbox/DrawingParagraphRenderItem.cs +++ b/src/EPPlus/Drawing/Renderer/RenderItems/Textbox/DrawingParagraphRenderItem.cs @@ -199,8 +199,8 @@ private void ImportMarginAndIndent(ExcelDrawingParagraph p) private void ImportAlignment(bool isAutoSize, double maxWidth, double parentWidth) { - if (isAutoSize == false) - { + //if (isAutoSize == false) + //{ Bounds.Left = 0; Bounds.Width = ParentMaxWidth; @@ -216,7 +216,7 @@ private void ImportAlignment(bool isAutoSize, double maxWidth, double parentWidt _centerAdjustment = GetAlignmentHorizontal(HorizontalAlignment); } Bounds.Width = parentWidth - RightMargin - LeftMargin; - } + //} } private void ImportLineSpacing(eDrawingTextLineSpacing lsType, double lineSpacingValue) diff --git a/src/EPPlus/Drawing/Renderer/RenderItems/Textbox/DrawingTextBox.cs b/src/EPPlus/Drawing/Renderer/RenderItems/Textbox/DrawingTextBox.cs index 78922fdf69..f8188c8486 100644 --- a/src/EPPlus/Drawing/Renderer/RenderItems/Textbox/DrawingTextBox.cs +++ b/src/EPPlus/Drawing/Renderer/RenderItems/Textbox/DrawingTextBox.cs @@ -99,8 +99,7 @@ internal void AddText(string text = null) TextBody.AddParagraph(text); } - - public new DrawingTextbody TextBody {get;set;} + public new DrawingTextbody TextBody { get { return (DrawingTextbody)base.TextBody; } set { base.TextBody = value; } } public double Left { get From 229d086783df99344718d0a0ab73781b56da32bb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ossian=20Edstr=C3=B6m?= Date: Tue, 9 Jun 2026 08:53:05 +0200 Subject: [PATCH 12/82] Fixed datalabel positions. Messily. --- .../Chart/DataLabels/ChartSerieDataLabelRenderer.cs | 6 ++++-- .../Renderer/Chart/DataLabels/SvgDataLabelPoint.cs | 9 +++++++-- 2 files changed, 11 insertions(+), 4 deletions(-) diff --git a/src/EPPlus/Drawing/Renderer/Chart/DataLabels/ChartSerieDataLabelRenderer.cs b/src/EPPlus/Drawing/Renderer/Chart/DataLabels/ChartSerieDataLabelRenderer.cs index fd3dd52aa4..da7a894eb7 100644 --- a/src/EPPlus/Drawing/Renderer/Chart/DataLabels/ChartSerieDataLabelRenderer.cs +++ b/src/EPPlus/Drawing/Renderer/Chart/DataLabels/ChartSerieDataLabelRenderer.cs @@ -26,7 +26,7 @@ public ChartSerieDataLabelRenderer(ChartRenderer chart, ExcelChartSerieDataLabel { _serieIndex = index; _dlblSerie = dlblSerie; - plotAreaBounds = chart.Plotarea.Rectangle.Bounds; + plotAreaBounds = chart.Plotarea.Group.Bounds; if (dlblSerie.TextBody.Paragraphs.Count != 0) { @@ -136,8 +136,10 @@ internal void SetParentPoint(BoundingBox parent, int index) public override void AppendRenderItems(List renderItems) { var plotAreaGroup = new GroupRenderItem(plotAreaBounds); + plotAreaGroup.Left = plotAreaBounds.Position.X; + plotAreaGroup.Top = plotAreaBounds.Position.Y; - if(_dlblSerie.Fill.IsEmpty == false) + if (_dlblSerie.Fill.IsEmpty == false) { plotAreaGroup.SetDrawingPropertiesFill(ChartRenderer.Theme, _dlblSerie.Fill, null); plotAreaGroup.GroupTransform += $" fill=\"{plotAreaGroup.FillColor}\""; diff --git a/src/EPPlus/Drawing/Renderer/Chart/DataLabels/SvgDataLabelPoint.cs b/src/EPPlus/Drawing/Renderer/Chart/DataLabels/SvgDataLabelPoint.cs index db3db6cfea..3428fb2542 100644 --- a/src/EPPlus/Drawing/Renderer/Chart/DataLabels/SvgDataLabelPoint.cs +++ b/src/EPPlus/Drawing/Renderer/Chart/DataLabels/SvgDataLabelPoint.cs @@ -245,6 +245,7 @@ internal void SetParentPoint(BoundingBox parentPoint, BoundingBox parentShape, V Rectangle.Bounds.Parent = parentPoint; _parentPoint = parentPoint; _parentShapeBounds = parentShape; + var dataLabelCenter = new Vector2(Rectangle.Bounds.Left, Rectangle.Bounds.Top); Vector2 startPointDirection = Vector2.Zero; @@ -458,24 +459,28 @@ private void AppendDebugBounds(List renderItems) public override void AppendRenderItems(List renderItems) { var parentPointGroup = new GroupRenderItem(_parentPoint); + parentPointGroup.Left = _parentPoint.Left; + parentPointGroup.Top = _parentPoint.Top; renderItems.Add(parentPointGroup); var titleItemOrigin = new TitleRenderItem("DataLabel originpoint"); renderItems.Add(titleItemOrigin); var group = new GroupRenderItem(Rectangle.Bounds); + group.Left = Rectangle.Bounds.Left; + group.Top = Rectangle.Bounds.Top; parentPointGroup.RenderItems.Add(group); var titleItem = new TitleRenderItem("DataLabel size adjustment"); parentPointGroup.RenderItems.Add(titleItem); - _txtBox.AppendRenderItems(parentPointGroup.RenderItems); + _txtBox.AppendRenderItems(group.RenderItems); if(_renderConnectionPointLines) { if (_connectionPointLines != null) { - _connectionPointLines.AppendRenderItems(parentPointGroup.RenderItems); + _connectionPointLines.AppendRenderItems(group.RenderItems); } } From 77b7382565aa0a8e8c9d2c5168b0de30d85b100b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ossian=20Edstr=C3=B6m?= Date: Tue, 9 Jun 2026 09:33:04 +0200 Subject: [PATCH 13/82] Fixed datalabels more. Started fixing centering trendline --- .../DataLabels/ChartSerieDataLabelRenderer.cs | 3 +- .../Chart/DataLabels/SvgDataLabelPoint.cs | 9 +-- .../RenderItems/Textbox/DrawingTextBody.cs | 62 +++++++++---------- .../RenderItems/Textbox/DrawingTextBox.cs | 8 +-- 4 files changed, 42 insertions(+), 40 deletions(-) diff --git a/src/EPPlus/Drawing/Renderer/Chart/DataLabels/ChartSerieDataLabelRenderer.cs b/src/EPPlus/Drawing/Renderer/Chart/DataLabels/ChartSerieDataLabelRenderer.cs index da7a894eb7..7078647db1 100644 --- a/src/EPPlus/Drawing/Renderer/Chart/DataLabels/ChartSerieDataLabelRenderer.cs +++ b/src/EPPlus/Drawing/Renderer/Chart/DataLabels/ChartSerieDataLabelRenderer.cs @@ -136,13 +136,14 @@ internal void SetParentPoint(BoundingBox parent, int index) public override void AppendRenderItems(List renderItems) { var plotAreaGroup = new GroupRenderItem(plotAreaBounds); + plotAreaGroup.Left = plotAreaBounds.Position.X; plotAreaGroup.Top = plotAreaBounds.Position.Y; if (_dlblSerie.Fill.IsEmpty == false) { plotAreaGroup.SetDrawingPropertiesFill(ChartRenderer.Theme, _dlblSerie.Fill, null); - plotAreaGroup.GroupTransform += $" fill=\"{plotAreaGroup.FillColor}\""; + plotAreaGroup.GroupTransform += $" fill=\"{plotAreaGroup.FillColor}\" name=\"Plot area group\""; } renderItems.Add(plotAreaGroup); diff --git a/src/EPPlus/Drawing/Renderer/Chart/DataLabels/SvgDataLabelPoint.cs b/src/EPPlus/Drawing/Renderer/Chart/DataLabels/SvgDataLabelPoint.cs index 3428fb2542..1b2ddb2df6 100644 --- a/src/EPPlus/Drawing/Renderer/Chart/DataLabels/SvgDataLabelPoint.cs +++ b/src/EPPlus/Drawing/Renderer/Chart/DataLabels/SvgDataLabelPoint.cs @@ -461,19 +461,20 @@ public override void AppendRenderItems(List renderItems) var parentPointGroup = new GroupRenderItem(_parentPoint); parentPointGroup.Left = _parentPoint.Left; parentPointGroup.Top = _parentPoint.Top; - renderItems.Add(parentPointGroup); var titleItemOrigin = new TitleRenderItem("DataLabel originpoint"); renderItems.Add(titleItemOrigin); + renderItems.Add(parentPointGroup); + + var titleItem = new TitleRenderItem("DataLabel size adjustment"); + parentPointGroup.RenderItems.Add(titleItem); + var group = new GroupRenderItem(Rectangle.Bounds); group.Left = Rectangle.Bounds.Left; group.Top = Rectangle.Bounds.Top; parentPointGroup.RenderItems.Add(group); - var titleItem = new TitleRenderItem("DataLabel size adjustment"); - parentPointGroup.RenderItems.Add(titleItem); - _txtBox.AppendRenderItems(group.RenderItems); if(_renderConnectionPointLines) diff --git a/src/EPPlus/Drawing/Renderer/RenderItems/Textbox/DrawingTextBody.cs b/src/EPPlus/Drawing/Renderer/RenderItems/Textbox/DrawingTextBody.cs index 151c7c2d44..29ca4672f5 100644 --- a/src/EPPlus/Drawing/Renderer/RenderItems/Textbox/DrawingTextBody.cs +++ b/src/EPPlus/Drawing/Renderer/RenderItems/Textbox/DrawingTextBody.cs @@ -80,33 +80,33 @@ public void ImportParagraph(ExcelDrawingParagraph item, double startingY, string Paragraphs.Add(paragraph); } - internal void SetHorizontalAlignmentPosition() - { - //if (AutoSize) - //{ - foreach (var p in Paragraphs) - { - switch (p.HorizontalAlignment) - { - case TextAlignment.Left: - p.Bounds.Left = 0; - break; - case TextAlignment.Center: - p.Bounds.Left = (Bounds.Width / 2) - (p.Bounds.Width / 2); - break; - case TextAlignment.Right: - p.Bounds.Left = Bounds.Right - p.Bounds.Width; - break; - case TextAlignment.Distributed: - case TextAlignment.Justified: - case TextAlignment.JustifiedLow: - case TextAlignment.ThaiDistributed: - p.Bounds.Left = 0; //TODO: Set left for now as we do not support distributed spacing yet - break; - } - } - //} - } + //internal void SetHorizontalAlignmentPosition() + //{ + // //if (AutoSize) + // //{ + // foreach (var p in Paragraphs) + // { + // switch (p.HorizontalAlignment) + // { + // case TextAlignment.Left: + // p.Bounds.Left = 0; + // break; + // case TextAlignment.Center: + // p.Bounds.Left = (Bounds.Width / 2) - (p.Bounds.Width / 2); + // break; + // case TextAlignment.Right: + // p.Bounds.Left = Bounds.Right - p.Bounds.Width; + // break; + // case TextAlignment.Distributed: + // case TextAlignment.Justified: + // case TextAlignment.JustifiedLow: + // case TextAlignment.ThaiDistributed: + // p.Bounds.Left = 0; //TODO: Set left for now as we do not support distributed spacing yet + // break; + // } + // } + // //} + //} internal virtual void ImportTextBodyAndParagraphs(ExcelTextBody body, ExcelHorizontalAlignment horizontalDefault = ExcelHorizontalAlignment.Left) { @@ -138,10 +138,10 @@ internal virtual void ImportTextBodyAndParagraphs(ExcelTextBody body, ExcelHoriz largestWidth = Math.Max(largestWidth, addedPara.Bounds.Width); } - foreach (var paragraph in body.Paragraphs) - { - SetHorizontalAlignmentPosition(); - } + //foreach (var paragraph in body.Paragraphs) + //{ + // SetHorizontalAlignmentPosition(); + //} if (Paragraphs != null && Paragraphs.Count() > 0) { diff --git a/src/EPPlus/Drawing/Renderer/RenderItems/Textbox/DrawingTextBox.cs b/src/EPPlus/Drawing/Renderer/RenderItems/Textbox/DrawingTextBox.cs index f8188c8486..5750582a03 100644 --- a/src/EPPlus/Drawing/Renderer/RenderItems/Textbox/DrawingTextBox.cs +++ b/src/EPPlus/Drawing/Renderer/RenderItems/Textbox/DrawingTextBox.cs @@ -265,18 +265,18 @@ public override void AppendRenderItems(List renderItems) } groupItem.RenderItems.Add(titleItem); - if(TextBody.AutoSize) + if (TextBody.AutoSize) { TextBody.ApplyAutoSize(); rect.Width = TextBody.Width + RightMargin + LeftMargin; rect.Height = TextBody.Height + BottomMargin + RightMargin; } + //TextBody.SetHorizontalAlignmentPosition(); + TextBody.Bounds.Left = LeftMargin; + TextBody.Bounds.Top = TopMargin; //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); } From 73c96255e61def33c1deed4176c23902f8e8715d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ossian=20Edstr=C3=B6m?= Date: Tue, 9 Jun 2026 10:47:42 +0200 Subject: [PATCH 14/82] Fixed DrawingTextBox duplicate properties --- .../Textbox/ParagraphRenderItem.cs | 4 +- .../RenderItems/Textbox/RenderTextbox.cs | 28 +-- .../Renderer/Chart/ChartAxisRenderer.cs | 2 +- .../Renderer/Chart/ChartLegendRenderer.cs | 8 +- .../Renderer/Chart/ChartTitleRenderer.cs | 2 +- .../Trendlines/ChartTrendlineRenderer.cs | 7 +- .../Chart/DataLabels/SvgDataLabelPoint.cs | 6 +- .../Renderer/Chart/DrawingLegendSerie.cs | 2 +- .../Textbox/DrawingParagraphRenderItem.cs | 8 +- .../RenderItems/Textbox/DrawingTextBody.cs | 10 +- .../RenderItems/Textbox/DrawingTextBox.cs | 205 +++++++++--------- src/EPPlus/Drawing/Renderer/ShapeRenderer.cs | 6 +- 12 files changed, 142 insertions(+), 146 deletions(-) diff --git a/src/EPPlus.DrawingRenderer/RenderItems/Textbox/ParagraphRenderItem.cs b/src/EPPlus.DrawingRenderer/RenderItems/Textbox/ParagraphRenderItem.cs index a0c642cafe..2c7a51047e 100644 --- a/src/EPPlus.DrawingRenderer/RenderItems/Textbox/ParagraphRenderItem.cs +++ b/src/EPPlus.DrawingRenderer/RenderItems/Textbox/ParagraphRenderItem.cs @@ -327,11 +327,11 @@ protected double CalculatePrevWidthBasedOnAlignment(double lineDist) protected void SetHorizontalAlignment(double widthOfLargestLine) { - if (HorizontalAlignment == TextAlignment.Center && _centerAdjustment != null /*&& AutoSize && 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 { diff --git a/src/EPPlus.DrawingRenderer/RenderItems/Textbox/RenderTextbox.cs b/src/EPPlus.DrawingRenderer/RenderItems/Textbox/RenderTextbox.cs index 2cbe96fe21..788be57ef9 100644 --- a/src/EPPlus.DrawingRenderer/RenderItems/Textbox/RenderTextbox.cs +++ b/src/EPPlus.DrawingRenderer/RenderItems/Textbox/RenderTextbox.cs @@ -30,7 +30,7 @@ public RenderTextbox(BoundingBox parent, double maxWidth, double maxHeight) public RenderTextbox(BoundingBox parent, BoundingBox maxBounds) { } - RectRenderItem _rectangle =null; + protected RectRenderItem _rectangle =null; public RectRenderItem Rectangle { get @@ -40,7 +40,8 @@ public RectRenderItem Rectangle return _rectangle; } } - public virtual RenderTextBody TextBody {get;set;} + + public virtual RenderTextBody TextBody { get; set; } public double Left { get @@ -80,27 +81,28 @@ public double Height return TopMargin + (TextBody?.Bounds.Height ?? 0d) + BottomMargin; } } - internal double LeftMargin + public double LeftMargin { get; set; } - internal double TopMargin + public double TopMargin { get; set; } - 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 { @@ -115,7 +117,7 @@ internal double Rotation /// 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 +125,7 @@ internal double GetActualWidth() /// Gets the actual right position of the rotated textbox. /// /// - internal double GetActualRight() + public double GetActualRight() { return Left+GetActualWidth(); } @@ -131,7 +133,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,7 +141,7 @@ internal double GetActualHeight() /// Gets the actual right position of the rotated textbox. /// /// - internal double GetActualBottom() + public double GetActualBottom() { return Top + GetActualHeight(); } @@ -151,7 +153,7 @@ public override void AppendRenderItems(List renderItems) /// /// How the text is anchored. /// - internal eTextAnchor TextAnchor + public eTextAnchor TextAnchor { get; set; diff --git a/src/EPPlus/Drawing/Renderer/Chart/ChartAxisRenderer.cs b/src/EPPlus/Drawing/Renderer/Chart/ChartAxisRenderer.cs index 8a7b34ef4a..652e89ec0b 100644 --- a/src/EPPlus/Drawing/Renderer/Chart/ChartAxisRenderer.cs +++ b/src/EPPlus/Drawing/Renderer/Chart/ChartAxisRenderer.cs @@ -437,7 +437,7 @@ 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); diff --git a/src/EPPlus/Drawing/Renderer/Chart/ChartLegendRenderer.cs b/src/EPPlus/Drawing/Renderer/Chart/ChartLegendRenderer.cs index 3919ead744..2789199c67 100644 --- a/src/EPPlus/Drawing/Renderer/Chart/ChartLegendRenderer.cs +++ b/src/EPPlus/Drawing/Renderer/Chart/ChartLegendRenderer.cs @@ -414,7 +414,7 @@ private void SetTrendlineLegend(ExcelChart ct, int serieIndex, int entryIndex, D 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(Chart, Rectangle.Bounds, tbLeft, tbTop, tbWidth, tbHeight, false, true); var entry = Chart.Legend.Entries.FirstOrDefault(x => x.Index == entryIndex); var headerText = tl.GetName(serieIndex); @@ -441,7 +441,7 @@ private void SetPieLegend(ExcelChart ct, int index, DrawingLegendSerie pSls, eLe 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(Chart, Rectangle.Bounds, tbLeft, tbTop, tbWidth, tbHeight, false, true); //Cat values are the header text //They create a rect marker for each slice @@ -495,7 +495,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(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 +541,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(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); diff --git a/src/EPPlus/Drawing/Renderer/Chart/ChartTitleRenderer.cs b/src/EPPlus/Drawing/Renderer/Chart/ChartTitleRenderer.cs index bee3ce50ef..fd138c21fb 100644 --- a/src/EPPlus/Drawing/Renderer/Chart/ChartTitleRenderer.cs +++ b/src/EPPlus/Drawing/Renderer/Chart/ChartTitleRenderer.cs @@ -198,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 { diff --git a/src/EPPlus/Drawing/Renderer/Chart/ChartTypeDrawers/Trendlines/ChartTrendlineRenderer.cs b/src/EPPlus/Drawing/Renderer/Chart/ChartTypeDrawers/Trendlines/ChartTrendlineRenderer.cs index 646eb4e8a7..5af479aa3f 100644 --- a/src/EPPlus/Drawing/Renderer/Chart/ChartTypeDrawers/Trendlines/ChartTrendlineRenderer.cs +++ b/src/EPPlus/Drawing/Renderer/Chart/ChartTypeDrawers/Trendlines/ChartTrendlineRenderer.cs @@ -159,7 +159,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) { @@ -173,8 +174,8 @@ private void CreateDatalabel() } labelText += RSquare; } - lbl.TextBody.Paragraphs[0].HorizontalAlignment = OfficeOpenXml.Drawing.eTextAlignment.Center; - DataLabel.ImportParagraph(lbl.TextBody.Paragraphs[0], 0, ""); + //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; diff --git a/src/EPPlus/Drawing/Renderer/Chart/DataLabels/SvgDataLabelPoint.cs b/src/EPPlus/Drawing/Renderer/Chart/DataLabels/SvgDataLabelPoint.cs index 1b2ddb2df6..fe439a7dac 100644 --- a/src/EPPlus/Drawing/Renderer/Chart/DataLabels/SvgDataLabelPoint.cs +++ b/src/EPPlus/Drawing/Renderer/Chart/DataLabels/SvgDataLabelPoint.cs @@ -104,19 +104,19 @@ 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); + 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); } diff --git a/src/EPPlus/Drawing/Renderer/Chart/DrawingLegendSerie.cs b/src/EPPlus/Drawing/Renderer/Chart/DrawingLegendSerie.cs index 652f7bf936..f1aea585d8 100644 --- a/src/EPPlus/Drawing/Renderer/Chart/DrawingLegendSerie.cs +++ b/src/EPPlus/Drawing/Renderer/Chart/DrawingLegendSerie.cs @@ -20,7 +20,7 @@ namespace EPPlusImageRenderer.Svg { 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/RenderItems/Textbox/DrawingParagraphRenderItem.cs b/src/EPPlus/Drawing/Renderer/RenderItems/Textbox/DrawingParagraphRenderItem.cs index bc75b2f3cd..318a65d4c3 100644 --- a/src/EPPlus/Drawing/Renderer/RenderItems/Textbox/DrawingParagraphRenderItem.cs +++ b/src/EPPlus/Drawing/Renderer/RenderItems/Textbox/DrawingParagraphRenderItem.cs @@ -25,7 +25,7 @@ internal class DrawingParagraphRenderItem : ParagraphRenderItem /// /// /// - public DrawingParagraphRenderItem(DrawingTextbody textBody, BoundingBox parent) : base(parent, textBody) + public DrawingParagraphRenderItem(DrawingTextBody textBody, BoundingBox parent) : base(parent, textBody) { ParagraphLineSpacing = GetParagraphLineSpacingInPoints(100, (TextShaper)OpenTypeFonts.GetShaperForFont(DefaultParagraphFont), DefaultParagraphFont.Size); } @@ -36,7 +36,7 @@ public DrawingParagraphRenderItem(DrawingTextbody textBody, BoundingBox parent) /// /// /// - public DrawingParagraphRenderItem(DrawingTextbody textBody, BoundingBox parent, string text) : this(textBody, parent) + public DrawingParagraphRenderItem(DrawingTextBody textBody, BoundingBox parent, string text) : this(textBody, parent) { ImportLinesAndTextRunsDefault(text); } @@ -48,7 +48,7 @@ public DrawingParagraphRenderItem(DrawingTextbody textBody, BoundingBox parent, /// /// /// - public DrawingParagraphRenderItem(DrawingTextbody textBody, BoundingBox parent, ExcelDrawingParagraph p, string textIfEmpty = null) : base(parent, textBody, false) + public DrawingParagraphRenderItem(DrawingTextBody textBody, BoundingBox parent, ExcelDrawingParagraph p, string textIfEmpty = null) : base(parent, textBody, false) { IsFirstParagraph = p == p._paragraphs[0]; ImportStyleInfo(textBody, p); @@ -132,7 +132,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) diff --git a/src/EPPlus/Drawing/Renderer/RenderItems/Textbox/DrawingTextBody.cs b/src/EPPlus/Drawing/Renderer/RenderItems/Textbox/DrawingTextBody.cs index 29ca4672f5..ecc7597480 100644 --- a/src/EPPlus/Drawing/Renderer/RenderItems/Textbox/DrawingTextBody.cs +++ b/src/EPPlus/Drawing/Renderer/RenderItems/Textbox/DrawingTextBody.cs @@ -20,20 +20,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(ExcelDrawing drawing, BoundingBox parent, bool autoSize, bool clampedToParent = false) : base(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(ExcelDrawing drawing, BoundingBox parent, double left, double top, double maxWidth, double maxHeight, bool clampedToParent = false, bool autoSize=false) : base(parent, autoSize) { _drawing = drawing; Theme = drawing._drawings.Worksheet.Workbook.ThemeManager.GetOrCreateTheme(); @@ -176,12 +176,12 @@ 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); } - 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); } diff --git a/src/EPPlus/Drawing/Renderer/RenderItems/Textbox/DrawingTextBox.cs b/src/EPPlus/Drawing/Renderer/RenderItems/Textbox/DrawingTextBox.cs index 5750582a03..ebb197fe70 100644 --- a/src/EPPlus/Drawing/Renderer/RenderItems/Textbox/DrawingTextBox.cs +++ b/src/EPPlus/Drawing/Renderer/RenderItems/Textbox/DrawingTextBox.cs @@ -24,7 +24,7 @@ private void Init(ExcelDrawing drawing, BoundingBox parent, double maxWidth, dou Parent = parent; _drawing= drawing; _rectangle = new RectRenderItem(parent); - TextBody = new DrawingTextbody(drawing, Rectangle.Bounds, true); + TextBody = new DrawingTextBody(drawing, Rectangle.Bounds, true); TextBody.MaxWidth = maxWidth; TextBody.MaxHeight = maxHeight; } @@ -39,16 +39,16 @@ internal DrawingTextBox(ExcelDrawing drawing, BoundingBox parent, double maxWidt // 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; - } - } + //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; @@ -99,119 +99,112 @@ internal void AddText(string text = null) TextBody.AddParagraph(text); } - public new DrawingTextbody TextBody { get { return (DrawingTextbody)base.TextBody; } set { base.TextBody = value; } } - public double Left - { - get - { - return Rectangle.Bounds.Left; //TextBody.Bounds.Left - LeftMargin; + DrawingTextBody _textBody; - } - 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 + public DrawingTextBody GetTextBody() { - get; set; + return (DrawingTextBody)TextBody; } - internal double TopMargin + public void SetDrawingTextBody(DrawingTextBody tb) { - get; set; + TextBody = tb; } - internal double RightMargin - { - get; set; - } + public override RenderTextBody TextBody { get { return _textBody; } set { _textBody = (DrawingTextBody)value; } } + //public double Left + //{ + // get + // { + // return Rectangle.Bounds.Left; //TextBody.Bounds.Left - LeftMargin; - internal double BottomMargin - { - get; set; - } - internal BoundingBox Parent { get; private set; } - internal double Rotation - { - get - { - return Rectangle.Bounds.Rotation; - } - set - { - Rectangle.Bounds.Rotation = value; - } - } + // } + // 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 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))); - } + //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(); - } + //internal double GetActualBottom() + //{ + // return Top + GetActualHeight(); + //} /// /// How the text is anchored. /// - internal eTextAnchor TextAnchor - { - get; - set; - } + //internal eTextAnchor TextAnchor + //{ + // get; + // set; + //} - 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) @@ -227,7 +220,7 @@ internal void ImportTextBody(ExcelTextBody body, bool useDefaults = true, ExcelH RightMargin = r; BottomMargin = b; - TextBody.ImportTextBodyAndParagraphs(body, horizontalDefault); + _textBody.ImportTextBodyAndParagraphs(body, horizontalDefault); } public override void AppendRenderItems(List renderItems) @@ -282,7 +275,7 @@ public override void AppendRenderItems(List renderItems) 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..03323b4a2c 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(Drawing, MarginTextBox.Bounds, MarginTextBox.Left, MarginTextBox.Top, MarginTextBox.Width, MarginTextBox.Height); txtBodyItem.ImportTextBodyAndParagraphs(bodyOrig); txtBodyItem.AppendRenderItems(grp.RenderItems); From 976e1ae155e44efe9029bb6e4fbd3b33c2513cbb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ossian=20Edstr=C3=B6m?= Date: Tue, 9 Jun 2026 12:01:30 +0200 Subject: [PATCH 15/82] Fixed Most of trendline centering --- .../Textbox/ParagraphRenderItem.cs | 15 ++++------ .../RenderItems/Textbox/RenderTextBody.cs | 4 +-- .../Trendlines/ChartTrendlineRenderer.cs | 6 ++-- .../Textbox/DrawingParagraphRenderItem.cs | 30 +++++++++---------- .../RenderItems/Textbox/DrawingTextBox.cs | 22 +++++++------- 5 files changed, 37 insertions(+), 40 deletions(-) diff --git a/src/EPPlus.DrawingRenderer/RenderItems/Textbox/ParagraphRenderItem.cs b/src/EPPlus.DrawingRenderer/RenderItems/Textbox/ParagraphRenderItem.cs index 2c7a51047e..0213751cb8 100644 --- a/src/EPPlus.DrawingRenderer/RenderItems/Textbox/ParagraphRenderItem.cs +++ b/src/EPPlus.DrawingRenderer/RenderItems/Textbox/ParagraphRenderItem.cs @@ -238,15 +238,7 @@ protected void ImportLinesAndTextRunsBase(string textIfEmpty) } AddDefaultTextFragment(textIfEmpty); - - Bounds.Left = GetAlignmentHorizontal(HorizontalAlignment); - if (HorizontalAlignment == TextAlignment.Center) - { - _centerAdjustment = GetAlignmentHorizontal(HorizontalAlignment); - } - WrapTextFragmentsAndGenerateTextRuns(); - } protected void WrapTextFragmentsAndGenerateTextRuns() @@ -271,7 +263,11 @@ protected void WrapTextFragmentsAndGenerateTextRuns() widthOfLargestLine = Lines.LargestWidthWithoutSpace; combinedHeight = Lines.GetHeightOfCollection(_lsMultiplier, lineSpacingResult); - SetHorizontalAlignment(widthOfLargestLine); + if(AutoSize) + { + Bounds.Width = widthOfLargestLine + RightMargin; + } + //SetHorizontalAlignment(widthOfLargestLine); int lineIdx = 0; foreach (var line in Lines) @@ -303,7 +299,6 @@ protected void WrapTextFragmentsAndGenerateTextRuns() lineIdx++; } } - Bounds.Width = widthOfLargestLine + RightMargin; Bounds.Height = combinedHeight; } diff --git a/src/EPPlus.DrawingRenderer/RenderItems/Textbox/RenderTextBody.cs b/src/EPPlus.DrawingRenderer/RenderItems/Textbox/RenderTextBody.cs index 64561749c0..2ab53c70bb 100644 --- a/src/EPPlus.DrawingRenderer/RenderItems/Textbox/RenderTextBody.cs +++ b/src/EPPlus.DrawingRenderer/RenderItems/Textbox/RenderTextBody.cs @@ -199,10 +199,10 @@ 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; + alignmentY = (Bounds.Height) / 2 + Bounds.Top; break; case TextAnchoringType.Bottom: - alignmentY = MaxHeight - Bounds.Height; + alignmentY = Bounds.Height; break; } diff --git a/src/EPPlus/Drawing/Renderer/Chart/ChartTypeDrawers/Trendlines/ChartTrendlineRenderer.cs b/src/EPPlus/Drawing/Renderer/Chart/ChartTypeDrawers/Trendlines/ChartTrendlineRenderer.cs index 5af479aa3f..a34fa7989f 100644 --- a/src/EPPlus/Drawing/Renderer/Chart/ChartTypeDrawers/Trendlines/ChartTrendlineRenderer.cs +++ b/src/EPPlus/Drawing/Renderer/Chart/ChartTypeDrawers/Trendlines/ChartTrendlineRenderer.cs @@ -490,7 +490,7 @@ public void CalculatePolynomial() } Formula = "y=" + GetPolynormFormula(); var r2 = CalculateRSquared(x => PredictLinear(x), _ySerie, _trendline.Intercept); - RSquare = $"R²={r2:N4}"; + RSquare = $"R² = {r2:N4}"; } private void CalculatePower() @@ -511,10 +511,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:G3}|"; 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() diff --git a/src/EPPlus/Drawing/Renderer/RenderItems/Textbox/DrawingParagraphRenderItem.cs b/src/EPPlus/Drawing/Renderer/RenderItems/Textbox/DrawingParagraphRenderItem.cs index 318a65d4c3..b830d43151 100644 --- a/src/EPPlus/Drawing/Renderer/RenderItems/Textbox/DrawingParagraphRenderItem.cs +++ b/src/EPPlus/Drawing/Renderer/RenderItems/Textbox/DrawingParagraphRenderItem.cs @@ -54,7 +54,7 @@ public DrawingParagraphRenderItem(DrawingTextBody textBody, BoundingBox parent, ImportStyleInfo(textBody, p); ImportMarginAndIndent(p); - ImportAlignment(textBody.AutoSize, textBody.MaxWidth, parent.Width); + //ImportAlignment(textBody.AutoSize, textBody.MaxWidth, parent.Width); //---Initialize / calculate lines and runs--- //measurer must be set before AddLinesAndRichText @@ -201,21 +201,21 @@ private void ImportAlignment(bool isAutoSize, double maxWidth, double parentWidt { //if (isAutoSize == false) //{ - Bounds.Left = 0; - Bounds.Width = ParentMaxWidth; + //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; //} } diff --git a/src/EPPlus/Drawing/Renderer/RenderItems/Textbox/DrawingTextBox.cs b/src/EPPlus/Drawing/Renderer/RenderItems/Textbox/DrawingTextBox.cs index ebb197fe70..450e15d58d 100644 --- a/src/EPPlus/Drawing/Renderer/RenderItems/Textbox/DrawingTextBox.cs +++ b/src/EPPlus/Drawing/Renderer/RenderItems/Textbox/DrawingTextBox.cs @@ -235,6 +235,14 @@ public override void AppendRenderItems(List renderItems) rect.Top = 0; rect.Left = 0; + if (TextBody.AutoSize) + { + TextBody.ApplyAutoSize(); + rect.Width = TextBody.Width + RightMargin + LeftMargin; + rect.Height = TextBody.Height + BottomMargin + TopMargin; + rect.Left = -LeftMargin; + rect.Top = -TopMargin; + } 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. @@ -257,16 +265,10 @@ public override void AppendRenderItems(List renderItems) } } groupItem.RenderItems.Add(titleItem); - - if (TextBody.AutoSize) - { - TextBody.ApplyAutoSize(); - rect.Width = TextBody.Width + RightMargin + LeftMargin; - rect.Height = TextBody.Height + BottomMargin + RightMargin; - } - //TextBody.SetHorizontalAlignmentPosition(); - TextBody.Bounds.Left = LeftMargin; - TextBody.Bounds.Top = TopMargin; + + ////TextBody.SetHorizontalAlignmentPosition(); + //TextBody.Bounds.Left = LeftMargin; + //TextBody.Bounds.Top = TopMargin; //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); From 5a394ce9d981e36ffe2b11a9c54edf9a78933aa3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ossian=20Edstr=C3=B6m?= Date: Tue, 9 Jun 2026 12:43:10 +0200 Subject: [PATCH 16/82] Added spacing in trendline strings --- .../Trendlines/ChartTrendlineRenderer.cs | 20 +++++++++---------- .../RenderItems/Textbox/DrawingTextBox.cs | 4 ++-- 2 files changed, 12 insertions(+), 12 deletions(-) diff --git a/src/EPPlus/Drawing/Renderer/Chart/ChartTypeDrawers/Trendlines/ChartTrendlineRenderer.cs b/src/EPPlus/Drawing/Renderer/Chart/ChartTypeDrawers/Trendlines/ChartTrendlineRenderer.cs index a34fa7989f..79ab7a93bb 100644 --- a/src/EPPlus/Drawing/Renderer/Chart/ChartTypeDrawers/Trendlines/ChartTrendlineRenderer.cs +++ b/src/EPPlus/Drawing/Renderer/Chart/ChartTypeDrawers/Trendlines/ChartTrendlineRenderer.cs @@ -337,8 +337,8 @@ private void CalculateExponential() var r2 = Math.Pow(Pearson.PearsonImpl(_ySerie.Cast(), GetExponentialSerie(slope, intercept)), 2); Coefficients = [slope, intercept]; - Formula = $"y={intercept:G5}e|ss:{slope:G3}x|"; - RSquare = $"R²={r2:N4}"; + Formula = $"y = {intercept:G5}e|ss:{slope:G3}x| "; + RSquare = $"R² = {r2:N4} "; } private void CalculateLogarithmic() { @@ -364,8 +364,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() { @@ -488,7 +488,7 @@ 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}"; } @@ -543,25 +543,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(); diff --git a/src/EPPlus/Drawing/Renderer/RenderItems/Textbox/DrawingTextBox.cs b/src/EPPlus/Drawing/Renderer/RenderItems/Textbox/DrawingTextBox.cs index 450e15d58d..de8c20bdf3 100644 --- a/src/EPPlus/Drawing/Renderer/RenderItems/Textbox/DrawingTextBox.cs +++ b/src/EPPlus/Drawing/Renderer/RenderItems/Textbox/DrawingTextBox.cs @@ -238,8 +238,8 @@ public override void AppendRenderItems(List renderItems) if (TextBody.AutoSize) { TextBody.ApplyAutoSize(); - rect.Width = TextBody.Width + RightMargin + LeftMargin; - rect.Height = TextBody.Height + BottomMargin + TopMargin; + rect.Width = TextBody.Width + RightMargin; + rect.Height = TextBody.Height + BottomMargin; rect.Left = -LeftMargin; rect.Top = -TopMargin; } From ab4198592ddbc36fcf6d423f88b26f861d89a70c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jan=20K=C3=A4llman?= Date: Tue, 9 Jun 2026 12:59:35 +0200 Subject: [PATCH 17/82] Fixed stacking axis --- .../Drawing/Chart/ExcelChartAxisStandard.cs | 69 +++++++++++------- .../Drawing/Chart/ExcelDrawingTextSettings.cs | 3 +- .../Renderer/Chart/ChartAxisRenderer.cs | 70 ++++++++++++------- .../Renderer/Chart/ChartPlotareaRenderer.cs | 56 +++++++++++---- .../Renderer/Chart/ChartTitleRenderer.cs | 17 ++--- src/EPPlus/Drawing/Renderer/ChartRenderer.cs | 49 ++++++++----- 6 files changed, 173 insertions(+), 91 deletions(-) diff --git a/src/EPPlus/Drawing/Chart/ExcelChartAxisStandard.cs b/src/EPPlus/Drawing/Chart/ExcelChartAxisStandard.cs index de5a081621..02be725181 100644 --- a/src/EPPlus/Drawing/Chart/ExcelChartAxisStandard.cs +++ b/src/EPPlus/Drawing/Chart/ExcelChartAxisStandard.cs @@ -167,48 +167,69 @@ public eActualAxisPosition ActualAxisPosition get { var ap = AxisPosition; - if(ap==eAxisPosition.Right && LabelPosition==eTickLabelPosition.Low) + if (ap == eAxisPosition.Left && LabelPosition == eTickLabelPosition.High) { - if (_chart.Axis.FirstOrDefault(x => x.AxisPosition == eAxisPosition.Left)?.LabelPosition == eTickLabelPosition.Low) + return eActualAxisPosition.Right; + } + else if (ap == eAxisPosition.Bottom) + { + if (LabelPosition == eTickLabelPosition.High) { - return eActualAxisPosition.Right; + return eActualAxisPosition.Top; } else { - return eActualAxisPosition.RightSecond; + return eActualAxisPosition.Bottom; } } - else if(ap==eAxisPosition.Right && LabelPosition==eTickLabelPosition.High) + else if (ap == eAxisPosition.Right) { - if(_chart.Axis.FirstOrDefault(x => x.AxisPosition == eAxisPosition.Left)?.LabelPosition == eTickLabelPosition.High) + if (LabelPosition == eTickLabelPosition.Low) { - return eActualAxisPosition.Left; + if (_chart.Axis.FirstOrDefault(x => x.AxisPosition == eAxisPosition.Left)?.LabelPosition == eTickLabelPosition.High) + { + return eActualAxisPosition.Left; + } + else + { + return eActualAxisPosition.LeftSecond; + } } - else + else { - return eActualAxisPosition.LeftSecond; + if (_chart.Axis.FirstOrDefault(x => x.AxisPosition == eAxisPosition.Left)?.LabelPosition == eTickLabelPosition.Low) + { + return eActualAxisPosition.Right; + } + else + { + return eActualAxisPosition.RightSecond; + } } } - else if(ap==eAxisPosition.Top && LabelPosition==eTickLabelPosition.Low) + else if(ap==eAxisPosition.Top) { - if (_chart.Axis.FirstOrDefault(x => x.AxisPosition == eAxisPosition.Bottom)?.LabelPosition == eTickLabelPosition.Low) + if (LabelPosition == eTickLabelPosition.Low) { - return eActualAxisPosition.Bottom; + if (_chart.Axis.FirstOrDefault(x => x.AxisPosition == eAxisPosition.Bottom)?.LabelPosition == eTickLabelPosition.High) + { + return eActualAxisPosition.Bottom; + } + else + { + return eActualAxisPosition.BottomSecond; + } } else { - return eActualAxisPosition.BottomSecond; - } - } - else if(ap==eAxisPosition.Bottom && LabelPosition==eTickLabelPosition.High) - { - if(_chart.Axis.FirstOrDefault(x => x.AxisPosition == eAxisPosition.Bottom)?.LabelPosition == eTickLabelPosition.High) - { - return eActualAxisPosition.Top; - } - else - { - return eActualAxisPosition.TopSecond; + if (_chart.Axis.FirstOrDefault(x => x.AxisPosition == eAxisPosition.Bottom)?.LabelPosition == eTickLabelPosition.Low) + { + return eActualAxisPosition.Top; + } + else + { + return eActualAxisPosition.TopSecond; + } } } else 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/Renderer/Chart/ChartAxisRenderer.cs b/src/EPPlus/Drawing/Renderer/Chart/ChartAxisRenderer.cs index 8a7b34ef4a..14e2d0be47 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,34 +85,55 @@ 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(); + //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; + //var lp = sc.ChartArea.Rectangle.Width - Rectangle.Width - 8D; + //if (aav == eActualAxisPosition.Right) + //{ + // if (sc.SecondVerticalAxis?.Axis?.ActualAxisPosition == eActualAxisPosition.RightSecond) + // { + // lp -= sc.SecondVerticalAxis.Rectangle.Width; + // } + // else + // { + // } + //} + //else + //{ + + //} + //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; + //var rb = 0D; + //if (aav == eActualAxisPosition.Bottom && sc.SecondHorizontalAxis?.Axis.ActualAxisPosition==eActualAxisPosition.BottomSecond) + //{ + // rb = sc.SecondHorizontalAxis.Rectangle.Height; + //} + + //Rectangle.Top = Title == null || ax.AxisPosition == eAxisPosition.Top ? sc.ChartArea.Rectangle.Height - 8 - Rectangle.Height : Title.Rectangle.Top - Rectangle.Height - 8; } } @@ -667,26 +689,26 @@ private List AddTickmarks(double units, eTimeUnit? dateUnit, dou case eAxisPosition.Left: y1 = (float)(Rectangle.Top + Rectangle.Height - ((d - min) / diff * Rectangle.Height)); y2 = y1; - x1 = (float)Rectangle.Right - tickMarkWidthOutside; - x2 = (float)Rectangle.Right + tickMarkWidthInside; + x1 = (float)ChartRenderer.Plotarea.Rectangle.Left - tickMarkWidthOutside; + x2 = (float)ChartRenderer.Plotarea.Rectangle.Left + tickMarkWidthInside; break; case eAxisPosition.Right: y1 = (float)(Rectangle.Top + Rectangle.Height - ((d - min) / diff * Rectangle.Height)); y2 = y1; - x1 = (float)Rectangle.Left - tickMarkWidthInside; - x2 = (float)Rectangle.Left + tickMarkWidthOutside; + x1 = (float)ChartRenderer.Plotarea.Rectangle.Right - tickMarkWidthInside; + x2 = (float)ChartRenderer.Plotarea.Rectangle.Right + tickMarkWidthOutside; break; case eAxisPosition.Top: x1 = (float)(Rectangle.Left + ((d - min) / diff * Rectangle.Width)); x2 = x1; - y1 = (float)Rectangle.Bottom - tickMarkWidthOutside; - y2 = (float)Rectangle.Bottom + tickMarkWidthInside; + y1 = (float)ChartRenderer.Plotarea.Rectangle.Top - tickMarkWidthOutside; + y2 = (float)ChartRenderer.Plotarea.Rectangle.Top + tickMarkWidthInside; break; case eAxisPosition.Bottom: x1 = (float)(Rectangle.Left + ((d - min) / diff * Rectangle.Width)); x2 = x1; - y1 = (float)Rectangle.Top - tickMarkWidthInside; - y2 = (float)Rectangle.Top + tickMarkWidthOutside; + y1 = (float)ChartRenderer.Plotarea.Rectangle.Top - tickMarkWidthInside; + y2 = (float)ChartRenderer.Plotarea.Rectangle.Top + tickMarkWidthOutside; break; default: throw new InvalidOperationException("Invalid axis position"); diff --git a/src/EPPlus/Drawing/Renderer/Chart/ChartPlotareaRenderer.cs b/src/EPPlus/Drawing/Renderer/Chart/ChartPlotareaRenderer.cs index ce905cfc0b..e6282569b8 100644 --- a/src/EPPlus/Drawing/Renderer/Chart/ChartPlotareaRenderer.cs +++ b/src/EPPlus/Drawing/Renderer/Chart/ChartPlotareaRenderer.cs @@ -66,11 +66,12 @@ internal void SetPlotAreaRectangle() 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.Bottom); + 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?.Rectangle.Width ?? 0D; + } + else + { + if (leftAxis.Title!=null) { left += leftAxis.Title.TextBox.GetActualWidth(); } @@ -144,24 +152,42 @@ private double GetPlotAreaLeft() 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 bee3ce50ef..a5a2ef7d33 100644 --- a/src/EPPlus/Drawing/Renderer/Chart/ChartTitleRenderer.cs +++ b/src/EPPlus/Drawing/Renderer/Chart/ChartTitleRenderer.cs @@ -120,20 +120,21 @@ private void SetAxisTitleRect(ChartRenderer sc, ChartAxisRenderer axis) Rectangle.Left = sc.Chart.HasLegend && sc.Chart.Legend.Position == eLegendPosition.Right || sc.Chart.Legend.Position == eLegendPosition.TopRight ? sc.Legend.Rectangle.Left - Rectangle.Width - margin : sc.Bounds.Right - Rectangle.Width - margin; break; case eAxisPosition.Bottom: - if(sc.HorizontalAxis!=null && sc.HorizontalAxis.Axis.HasTitle && sc.HorizontalAxis.Axis.AxisPosition==eAxisPosition.Bottom) - { - Rectangle.Top = sc.HorizontalAxis.Rectangle.Bottom + margin; - } - else - { - Rectangle.Top = sc.ChartArea.Rectangle.Height - margin - Rectangle.Height; - } + Rectangle.Top = sc.ChartArea.Rectangle.Height - margin - Rectangle.Height; Rectangle.Left = GetHorizontalLeft(sc); break; case eAxisPosition.Top: Rectangle.Top = sc.Title != null && sc.Title._title.Layout.HasLayout==false ? sc.Title.Rectangle.Bottom+margin : margin; Rectangle.Left = GetHorizontalLeft(sc); break; + //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; } } diff --git a/src/EPPlus/Drawing/Renderer/ChartRenderer.cs b/src/EPPlus/Drawing/Renderer/ChartRenderer.cs index f20872b5ae..fcc5e978c3 100644 --- a/src/EPPlus/Drawing/Renderer/ChartRenderer.cs +++ b/src/EPPlus/Drawing/Renderer/ChartRenderer.cs @@ -135,17 +135,28 @@ private void PlaceHorizontalAxis(ChartAxisRenderer horizontalAxis, bool isSecond 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.Group.Top + Plotarea.Rectangle.Height + HorizontalAxis.Rectangle.Height; + horizontalAxis.Line.Y1 = horizontalAxis.Line.Y2 = (float)Plotarea.Group.Top + Plotarea.Rectangle.Height; + } + 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 = (float)Plotarea.Group.Top; + } } if (horizontalAxis.Title != null) { @@ -185,26 +196,26 @@ private void PlaceVerticalAxis(ChartAxisRenderer verticalAxis) verticalAxis.Rectangle.Height = Plotarea.Rectangle.Height; verticalAxis.Line.Y1 = (float)verticalAxis.Rectangle.Top; verticalAxis.Line.Y2 = (float)verticalAxis.Rectangle.Bottom; - var axisPos = verticalAxis.Axis.AxisPosition; - if(verticalAxis.Axis.TickLabelPosition==eTickLabelPosition.High) - { - axisPos = axisPos == eAxisPosition.Left ? eAxisPosition.Left : eAxisPosition.Right; - } - //if(verticalAxis.Axis.TickLabelPosition==eTickLabelPosition.NextTo) - //{ - - // verticalAxis.Rectangle.Left = Plotarea.Group.Left - verticalAxis.Rectangle.Width; - // verticalAxis.Line.X1 = verticalAxis.Line.X2 = (float)Plotarea.Group.Left; - //} - //else if (axisPos == eAxisPosition.Left) - if (axisPos == 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; } } @@ -226,7 +237,8 @@ private void PlaceVerticalAxis(ChartAxisRenderer verticalAxis) } else { - verticalAxis.Title.TextBox.Left = verticalAxis.Rectangle.Left - verticalAxis.Title.TextBox.GetActualWidth() - 1.5; + //verticalAxis.Title.TextBox.Left = verticalAxis.Rectangle.Left - verticalAxis.Title.TextBox.GetActualWidth() - 1.5; + verticalAxis.Title.TextBox.Left = ChartArea.LeftMargin; } } else @@ -251,6 +263,7 @@ private void SetChartArea() 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.AppendRenderItems(RenderItems); + item.SetMargins(Chart.TextBody); ChartArea = item; } private ChartAxisRenderer GetAxis(bool vertical, int offset = 0) From 741941748558abcb0c17a3e88ea6d7ed37c94dd7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ossian=20Edstr=C3=B6m?= Date: Tue, 9 Jun 2026 15:17:26 +0200 Subject: [PATCH 18/82] Tweaked tests. Added recalculation to textbody --- .../SvgStandAloneTests.cs | 170 ++++++++++-------- .../SvgItem/SvgTextBodyRenderItem.cs | 1 + .../SvgItem/SvgTextRunRenderItem.cs | 11 +- .../Textbox/ParagraphRenderItem.cs | 9 +- .../RenderItems/Textbox/RenderTextBody.cs | 19 +- .../Svg/SvgTextRunRenderer.cs | 13 +- .../Integration/LayoutSystemTests.cs | 19 +- .../Textbox/DrawingParagraphRenderItem.cs | 2 +- 8 files changed, 163 insertions(+), 81 deletions(-) diff --git a/src/EPPlus.DrawingRenderer.Tests/DrawingShapeRenderer/SvgStandAloneTests.cs b/src/EPPlus.DrawingRenderer.Tests/DrawingShapeRenderer/SvgStandAloneTests.cs index f4dbcfd416..f10e764a70 100644 --- a/src/EPPlus.DrawingRenderer.Tests/DrawingShapeRenderer/SvgStandAloneTests.cs +++ b/src/EPPlus.DrawingRenderer.Tests/DrawingShapeRenderer/SvgStandAloneTests.cs @@ -1,76 +1,61 @@ 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.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 void GenerateSvgFile(string fileName, BoundingBox bounds, params RenderItem[] items) + { - List items = new List() { baseGroup }; + StringBuilder sb = new StringBuilder(); + var svgShapeRenderer = new SvgShapeRenderer(bounds, sb); - svgShapeRenderer.Render(items); + List renderItems = items.ToList(); + svgShapeRenderer.Render(renderItems); var svg = sb.ToString(); - SaveTextFileToWorkbook("svg\\rectStandalone.svg", svg); + SaveTextFileToWorkbook($"svg\\{fileName}.svg", svg); } [TestMethod] - public void SvgTextRun() + public void SvgRectTest() { - BoundingBox bounds = new BoundingBox(0, 0, 500, 500); - StringBuilder sb = new StringBuilder(); - var svgShapeRenderer = new SvgShapeRenderer(bounds, sb); - - - var baseGroup = new GroupRenderItem(bounds); - - var background = new RectRenderItem(baseGroup.Bounds); - - background.Width = bounds.Width; - background.Height = bounds.Height; - background.FillColor = "aliceBlue"; + 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,42 +64,45 @@ 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); - baseGroup.AddChildItem(textRun); - - - List items = new List() { baseGroup }; - svgShapeRenderer.Render(items); + var textRun = new SvgTextRunRenderItem(baseGroup.Bounds, rt, rt.Text, true); - var svg = sb.ToString(); + //Add size of text since svg renders text upwards from the start point. + textRun.YPosition = rt.Size; + baseGroup.AddChildItem(textRun); - SaveTextFileToWorkbook("svg\\textRunStandAlone.svg", svg); + GenerateSvgFile("textRunStandAlone", baseGroup.Bounds, baseGroup); } - [TestMethod] - public void SvgTextBodyTest() + private void GenerateTextBodyFile(string fileName, GroupRenderItem baseGroup, SvgTextBodyRenderItem textBody) { - BoundingBox bounds = new BoundingBox(0, 0, 500, 500); StringBuilder sb = new StringBuilder(); - var svgShapeRenderer = new SvgShapeRenderer(bounds, sb); - - - var baseGroup = new GroupRenderItem(bounds); + var svgShapeRenderer = new SvgShapeRenderer(baseGroup.Bounds, sb); var background = new RectRenderItem(baseGroup.Bounds); - background.Width = bounds.Width; - background.Height = bounds.Height; + background.Width = baseGroup.Bounds.Width; + background.Height = baseGroup.Bounds.Height; background.FillColor = "aliceBlue"; - baseGroup.Bounds.Width = bounds.Width; - baseGroup.Bounds.Height = bounds.Height; + baseGroup.AddChildItem(textBody); + baseGroup.AddChildItem(background); + + List items = new List() { baseGroup }; + textBody.AppendRenderItems(items); + svgShapeRenderer.Render(items); + + var svg = sb.ToString(); + + + SaveTextFileToWorkbook($"svg\\{fileName}.svg", svg); + } + + + private SvgTextBodyRenderItem GenerateTextBody(GroupRenderItem baseGroup) + { var textBody = new SvgTextBodyRenderItem(baseGroup.Bounds, true); var paragraph = textBody.AddParagraph("Hello"); @@ -124,18 +112,56 @@ public void SvgTextBodyTest() rtItem.FontColor = Color.DarkGreen; var para2 = textBody.AddParagraph(rtItem); - baseGroup.AddChildItem(textBody); - baseGroup.AddChildItem(background); + return textBody; + } - List items = new List() { baseGroup }; - textBody.AppendRenderItems(items); + [TestMethod] + public void SvgTextBodyTest() + { + BoundingBox bounds = new BoundingBox(0, 0, 500, 500); + var baseGroup = new GroupRenderItem(bounds); - svgShapeRenderer.Render(items); + baseGroup.Bounds.Width = bounds.Width; + baseGroup.Bounds.Height = bounds.Height; - var svg = sb.ToString(); + var textBody = GenerateTextBody(baseGroup); + + GenerateTextBodyFile("standAloneTextBody", baseGroup, textBody); + } + [TestMethod] + public void SvgTextBodyTestCenterAlignmentGenerated() + { + BoundingBox bounds = new BoundingBox(0, 0, 500, 500); + var baseGroup = new GroupRenderItem(bounds); + + baseGroup.Bounds.Width = bounds.Width; + baseGroup.Bounds.Height = bounds.Height; + + var textBody = GenerateTextBody(baseGroup); + textBody.Paragraphs[0].AddText(" a\r\n new day beckons"); + textBody.Paragraphs[0].HorizontalAlignment = RenderItems.Shared.TextAlignment.Center; + //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(); + + //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); + Assert.AreEqual(33.142333984375d, textBody.Paragraphs[0].Runs[1].Bounds.Left); + Assert.AreEqual(59.509033203125d, textBody.Paragraphs[0].Runs[2].Bounds.Left); + Assert.AreEqual(0d, textBody.Paragraphs[0].Runs[3].Bounds.Left); + + //Assert that the second paragraph has been moved correctly + Assert.AreEqual(26.85546875d, textBody.Paragraphs[1].Bounds.Top); + GenerateTextBodyFile("standAloneTextBodyAlignment", baseGroup, textBody); + } + + [TestMethod] + public void SvgTextBoxTest() + { - SaveTextFileToWorkbook("svg\\textBodyStandAlone.svg", svg); } } } diff --git a/src/EPPlus.DrawingRenderer/RenderItems/SvgItem/SvgTextBodyRenderItem.cs b/src/EPPlus.DrawingRenderer/RenderItems/SvgItem/SvgTextBodyRenderItem.cs index 70ef9e714e..7d5308eab5 100644 --- a/src/EPPlus.DrawingRenderer/RenderItems/SvgItem/SvgTextBodyRenderItem.cs +++ b/src/EPPlus.DrawingRenderer/RenderItems/SvgItem/SvgTextBodyRenderItem.cs @@ -1,6 +1,7 @@ using EPPlus.Export.ImageRenderer.RenderItems.Shared; using EPPlus.Fonts.OpenType.Integration.DataHolders; using EPPlus.Graphics; +using System.Drawing; namespace EPPlus.DrawingRenderer.RenderItems.SvgItem 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 0213751cb8..9fe3e2a1f8 100644 --- a/src/EPPlus.DrawingRenderer/RenderItems/Textbox/ParagraphRenderItem.cs +++ b/src/EPPlus.DrawingRenderer/RenderItems/Textbox/ParagraphRenderItem.cs @@ -9,9 +9,6 @@ using EPPlus.Graphics; using OfficeOpenXml.Interfaces.Drawing.Text; using OfficeOpenXml.Interfaces.RichText; -using System.Drawing; -using System.Text; -using static System.Net.Mime.MediaTypeNames; namespace EPPlus.Export.ImageRenderer.RenderItems.Shared { @@ -86,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; diff --git a/src/EPPlus.DrawingRenderer/RenderItems/Textbox/RenderTextBody.cs b/src/EPPlus.DrawingRenderer/RenderItems/Textbox/RenderTextBody.cs index 2ab53c70bb..92674bf306 100644 --- a/src/EPPlus.DrawingRenderer/RenderItems/Textbox/RenderTextBody.cs +++ b/src/EPPlus.DrawingRenderer/RenderItems/Textbox/RenderTextBody.cs @@ -104,11 +104,11 @@ public void AppendRenderItems(List renderItems) //TranslationOffset.Left = Bounds.Left; //TranslationOffset.Top = Bounds.Top; - renderItems.Add(this); foreach (var item in Paragraphs) { AddChildItem(item); } + renderItems.Add(this); } public ParagraphRenderItem AddParagraph(IRichTextFormatSimple rtFormat) @@ -147,6 +147,23 @@ public void ApplyAutoSize() } } + /// + /// If text is added to the first paragraph without using textbody e.g. Paragraphs[0].AddText() + /// Subsequent paragraphs must be updated + /// + public void RecalculateParagraphs() + { + double lastParagraphBottom = 0; + + foreach(var paragraph in Paragraphs) + { + paragraph.Bounds.Top = lastParagraphBottom; + lastParagraphBottom = paragraph.Bounds.Bottom; + } + + Bounds.Height = lastParagraphBottom; + } + private void AdjustAndAddParagraph(ParagraphRenderItem paragraph) { paragraph.Bounds.Name = $"Container{Paragraphs.Count}"; 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.Tests/Integration/LayoutSystemTests.cs b/src/EPPlus.Fonts.OpenType.Tests/Integration/LayoutSystemTests.cs index 68e01cc160..e5a4dc525b 100644 --- a/src/EPPlus.Fonts.OpenType.Tests/Integration/LayoutSystemTests.cs +++ b/src/EPPlus.Fonts.OpenType.Tests/Integration/LayoutSystemTests.cs @@ -382,8 +382,25 @@ public void TestGetSectionNumber2() } [TestMethod] - public void TestSimpleRichText2() + public void TestLayoutSystemMultipleParagraphs() { + var paragraphEndSymbol = '\u2029'; + + 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 fragments = new List() + { + new TextFragment() {Text = lstOfRichText[0], Font = font } + }; + + var layout = new LayoutSystem(fragments); + Assert.AreEqual(3, layout.GetParagraphSeparatorCount()); //var rtCollection = new RichTextCollectionBase(); //var someTextRt = rtCollection.Add("SomeText", true); //var richRt = rtCollection.Add("rich"); diff --git a/src/EPPlus/Drawing/Renderer/RenderItems/Textbox/DrawingParagraphRenderItem.cs b/src/EPPlus/Drawing/Renderer/RenderItems/Textbox/DrawingParagraphRenderItem.cs index b830d43151..61e1d725ae 100644 --- a/src/EPPlus/Drawing/Renderer/RenderItems/Textbox/DrawingParagraphRenderItem.cs +++ b/src/EPPlus/Drawing/Renderer/RenderItems/Textbox/DrawingParagraphRenderItem.cs @@ -192,7 +192,7 @@ 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(); } From 7d7113a9ec43246f532a88f7145e3339b06e77ec Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jan=20K=C3=A4llman?= Date: Tue, 9 Jun 2026 15:43:04 +0200 Subject: [PATCH 19/82] Fixed positioning of stacked axis --- .../RenderItems/Textbox/RenderTextbox.cs | 24 ++++++ .../Renderer/Chart/ChartAxisRenderer.cs | 42 +++++----- .../Renderer/Chart/ChartDrawingObject.cs | 2 +- .../Renderer/Chart/ChartPlotareaRenderer.cs | 2 +- .../Renderer/Chart/ChartTitleRenderer.cs | 2 +- src/EPPlus/Drawing/Renderer/ChartRenderer.cs | 84 ++++++++++++++----- 6 files changed, 115 insertions(+), 41 deletions(-) diff --git a/src/EPPlus.DrawingRenderer/RenderItems/Textbox/RenderTextbox.cs b/src/EPPlus.DrawingRenderer/RenderItems/Textbox/RenderTextbox.cs index 788be57ef9..2ac2c8b550 100644 --- a/src/EPPlus.DrawingRenderer/RenderItems/Textbox/RenderTextbox.cs +++ b/src/EPPlus.DrawingRenderer/RenderItems/Textbox/RenderTextbox.cs @@ -39,6 +39,10 @@ public RectRenderItem Rectangle _rectangle.Bounds.Height = Height; return _rectangle; } + set + { + _rectangle = value; + } } public virtual RenderTextBody TextBody { get; set; } @@ -74,6 +78,16 @@ public double Width return LeftMargin + (TextBody?.Bounds?.Width ?? 0D) + RightMargin; } } + 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 @@ -81,6 +95,16 @@ public double Height return TopMargin + (TextBody?.Bounds.Height ?? 0d) + BottomMargin; } } + public double HeightWithRotation + { + get + { + var radians = MathHelper.Radians(Rotation); + var sin = Math.Abs(Math.Sin(radians)); + var cos = Math.Abs(Math.Cos(radians)); + return Width * cos + Height * sin; + } + } public double LeftMargin { get; set; diff --git a/src/EPPlus/Drawing/Renderer/Chart/ChartAxisRenderer.cs b/src/EPPlus/Drawing/Renderer/Chart/ChartAxisRenderer.cs index bfe77dd36f..8eefdf9cf8 100644 --- a/src/EPPlus/Drawing/Renderer/Chart/ChartAxisRenderer.cs +++ b/src/EPPlus/Drawing/Renderer/Chart/ChartAxisRenderer.cs @@ -408,7 +408,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); } @@ -420,7 +420,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; } @@ -435,7 +435,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; } @@ -444,7 +444,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; } @@ -574,7 +574,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) { @@ -585,7 +585,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) { @@ -684,31 +684,35 @@ 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)ChartRenderer.Plotarea.Rectangle.Left - tickMarkWidthOutside; - x2 = (float)ChartRenderer.Plotarea.Rectangle.Left + tickMarkWidthInside; + 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)ChartRenderer.Plotarea.Rectangle.Right - tickMarkWidthInside; - x2 = (float)ChartRenderer.Plotarea.Rectangle.Right + tickMarkWidthOutside; + 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)ChartRenderer.Plotarea.Rectangle.Top - tickMarkWidthOutside; - y2 = (float)ChartRenderer.Plotarea.Rectangle.Top + tickMarkWidthInside; + 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)ChartRenderer.Plotarea.Rectangle.Top - tickMarkWidthInside; - y2 = (float)ChartRenderer.Plotarea.Rectangle.Top + tickMarkWidthOutside; + y1 = (float)Rectangle.Top - tickMarkWidthInside; + y2 = (float)Rectangle.Top + tickMarkWidthOutside; break; default: throw new InvalidOperationException("Invalid axis position"); diff --git a/src/EPPlus/Drawing/Renderer/Chart/ChartDrawingObject.cs b/src/EPPlus/Drawing/Renderer/Chart/ChartDrawingObject.cs index 2e73aeeb69..6b38e74c7a 100644 --- a/src/EPPlus/Drawing/Renderer/Chart/ChartDrawingObject.cs +++ b/src/EPPlus/Drawing/Renderer/Chart/ChartDrawingObject.cs @@ -56,7 +56,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; diff --git a/src/EPPlus/Drawing/Renderer/Chart/ChartPlotareaRenderer.cs b/src/EPPlus/Drawing/Renderer/Chart/ChartPlotareaRenderer.cs index e6282569b8..546b2c4ad2 100644 --- a/src/EPPlus/Drawing/Renderer/Chart/ChartPlotareaRenderer.cs +++ b/src/EPPlus/Drawing/Renderer/Chart/ChartPlotareaRenderer.cs @@ -134,7 +134,7 @@ private double GetPlotAreaLeft() if (leftAxis == null) { leftAxis = GetAxisByPosition(eAxisPosition.Left); - left += leftAxis?.Title?.Rectangle.Width ?? 0D; + left += leftAxis?.Title?.TextBox.Width ?? 0D; } else { diff --git a/src/EPPlus/Drawing/Renderer/Chart/ChartTitleRenderer.cs b/src/EPPlus/Drawing/Renderer/Chart/ChartTitleRenderer.cs index 6dd1513e2f..cbd4977a5c 100644 --- a/src/EPPlus/Drawing/Renderer/Chart/ChartTitleRenderer.cs +++ b/src/EPPlus/Drawing/Renderer/Chart/ChartTitleRenderer.cs @@ -191,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; @@ -218,6 +217,7 @@ 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(); diff --git a/src/EPPlus/Drawing/Renderer/ChartRenderer.cs b/src/EPPlus/Drawing/Renderer/ChartRenderer.cs index fcc5e978c3..9ca2534a2a 100644 --- a/src/EPPlus/Drawing/Renderer/ChartRenderer.cs +++ b/src/EPPlus/Drawing/Renderer/ChartRenderer.cs @@ -145,7 +145,7 @@ private void PlaceHorizontalAxis(ChartAxisRenderer horizontalAxis, bool isSecond else if(axisPos == eActualAxisPosition.BottomSecond) { horizontalAxis.Rectangle.Top = Plotarea.Group.Top + Plotarea.Rectangle.Height + HorizontalAxis.Rectangle.Height; - horizontalAxis.Line.Y1 = horizontalAxis.Line.Y2 = (float)Plotarea.Group.Top + Plotarea.Rectangle.Height; + horizontalAxis.Line.Y1 = horizontalAxis.Line.Y2 = horizontalAxis.Rectangle.Top; } else if(axisPos == eActualAxisPosition.Top) { @@ -155,42 +155,87 @@ private void PlaceHorizontalAxis(ChartAxisRenderer horizontalAxis, bool isSecond else { horizontalAxis.Rectangle.Top = Plotarea.Group.Top - horizontalAxis.Rectangle.Height - HorizontalAxis.Rectangle.Height; - horizontalAxis.Line.Y1 = horizontalAxis.Line.Y2 = (float)Plotarea.Group.Top; + 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; - if (horizontalAxis.Axis.Deleted) + 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) { - 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) + { + if (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 = Plotarea.Group.Top + Plotarea.Rectangle.Height; + horizontalAxis.Title.TextBox.Top = horizontalAxis.Rectangle.Bottom; } else { - horizontalAxis.Title.TextBox.Top = Plotarea.Group.Top - horizontalAxis.Rectangle.Height; + 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 { - if (horizontalAxis.Axis.AxisPosition == eAxisPosition.Bottom) + if (SecondHorizontalAxis.Axis.ActualAxisPosition == eActualAxisPosition.TopSecond) { - horizontalAxis.Title.TextBox.Top = horizontalAxis.Rectangle.Bottom; + horizontalAxis.Title.TextBox.Top = horizontalAxis.Rectangle.Top - SecondHorizontalAxis.Rectangle.Height - horizontalAxis.Title.TextBox.Height; } - else + 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; @@ -199,11 +244,11 @@ private void PlaceVerticalAxis(ChartAxisRenderer verticalAxis) 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) + 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; @@ -220,11 +265,13 @@ private void PlaceVerticalAxis(ChartAxisRenderer verticalAxis) } } + 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); @@ -237,7 +284,6 @@ private void PlaceVerticalAxis(ChartAxisRenderer verticalAxis) } else { - //verticalAxis.Title.TextBox.Left = verticalAxis.Rectangle.Left - verticalAxis.Title.TextBox.GetActualWidth() - 1.5; verticalAxis.Title.TextBox.Left = ChartArea.LeftMargin; } } From bd0f235cfd46497fbe398345217a88cd2aa1c246 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jan=20K=C3=A4llman?= Date: Tue, 9 Jun 2026 16:29:42 +0200 Subject: [PATCH 20/82] WIP:Axis positioning and sizeing --- .../Chart/LineChartToSvgTests.cs | 18 +++++++++--------- .../Renderer/Chart/ChartPlotareaRenderer.cs | 6 +++++- 2 files changed, 14 insertions(+), 10 deletions(-) diff --git a/src/EPPlus.DrawingRenderer.Tests/Chart/LineChartToSvgTests.cs b/src/EPPlus.DrawingRenderer.Tests/Chart/LineChartToSvgTests.cs index 10df58204e..092936ce7c 100644 --- a/src/EPPlus.DrawingRenderer.Tests/Chart/LineChartToSvgTests.cs +++ b/src/EPPlus.DrawingRenderer.Tests/Chart/LineChartToSvgTests.cs @@ -193,16 +193,16 @@ public void GenerateSvgForCharts_SecondaryAxis_sheet3() using (var p = OpenTemplatePackage("ChartForSvg_SecondaryAxis.xlsx")) { var ws = p.Workbook.Worksheets[2]; - //var ix = 1; - //var c = ws.Drawings[ix]; - //var svg = c.ToSvg(); - //SaveTextFileToWorkbook($"svg\\ChartForSvg_sheet2_{ix++}.svg", svg); var ix = 1; - foreach (ExcelChart c in ws.Drawings) - { - var svg = c.ToSvg(); - SaveTextFileToWorkbook($"svg\\ChartForSvg_SecAxis{ix++}.svg", svg); - } + var c = ws.Drawings[ix]; + var svg = c.ToSvg(); + SaveTextFileToWorkbook($"svg\\ChartForSvg_sheet3_{ix++}.svg", svg); + //var ix = 1; + //foreach (ExcelChart c in ws.Drawings) + //{ + // var svg = c.ToSvg(); + // SaveTextFileToWorkbook($"svg\\ChartForSvg_SecAxis{ix++}.svg", svg); + //} } } diff --git a/src/EPPlus/Drawing/Renderer/Chart/ChartPlotareaRenderer.cs b/src/EPPlus/Drawing/Renderer/Chart/ChartPlotareaRenderer.cs index 546b2c4ad2..d4d24f01af 100644 --- a/src/EPPlus/Drawing/Renderer/Chart/ChartPlotareaRenderer.cs +++ b/src/EPPlus/Drawing/Renderer/Chart/ChartPlotareaRenderer.cs @@ -146,6 +146,10 @@ private double GetPlotAreaLeft() { left += leftAxis.Rectangle.Width + 1.5; } + if(leftSecondAxis!=null) + { + left += leftSecondAxis.Rectangle.Width; + } } return left; } @@ -158,7 +162,7 @@ private double GetPlotAreaTop() { //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; + haHeight = topAxis?.Title?.Rectangle.Height ?? 0D; } else { From 45b069bde05c388ba04f82d198807775ffde5bd3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ossian=20Edstr=C3=B6m?= Date: Tue, 9 Jun 2026 16:49:03 +0200 Subject: [PATCH 21/82] Fixed line-ending issue --- .../SvgStandAloneTests.cs | 53 +++++++++++++++++-- .../Integration/TextLayoutEngine.RichText.cs | 10 ++-- 2 files changed, 56 insertions(+), 7 deletions(-) diff --git a/src/EPPlus.DrawingRenderer.Tests/DrawingShapeRenderer/SvgStandAloneTests.cs b/src/EPPlus.DrawingRenderer.Tests/DrawingShapeRenderer/SvgStandAloneTests.cs index f10e764a70..3485957741 100644 --- a/src/EPPlus.DrawingRenderer.Tests/DrawingShapeRenderer/SvgStandAloneTests.cs +++ b/src/EPPlus.DrawingRenderer.Tests/DrawingShapeRenderer/SvgStandAloneTests.cs @@ -141,21 +141,66 @@ public void SvgTextBodyTestCenterAlignmentGenerated() var textBody = GenerateTextBody(baseGroup); textBody.Paragraphs[0].AddText(" a\r\n new day beckons"); textBody.Paragraphs[0].HorizontalAlignment = RenderItems.Shared.TextAlignment.Center; + + textBody.Paragraphs[1].HorizontalAlignment = RenderItems.Shared.TextAlignment.Center; + textBody.Paragraphs[1].AddText("\r\n What fun, what fun!"); + //Text was added to the paragraph above the last paragraph //We must re-calculate where the next paragraph should be placed textBody.RecalculateParagraphs(); textBody.ApplyAutoSize(); + double delta = 0.001; + //new day beckons is the largest line in the centered paragraph[0] //Assert that the first line has been centered appropriately - Assert.AreEqual(9.890869140625d, textBody.Paragraphs[0].Runs[0].Bounds.Left); - Assert.AreEqual(33.142333984375d, textBody.Paragraphs[0].Runs[1].Bounds.Left); - Assert.AreEqual(59.509033203125d, textBody.Paragraphs[0].Runs[2].Bounds.Left); + 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(4.3760001659393311d, 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); - GenerateTextBodyFile("standAloneTextBodyAlignment", baseGroup, textBody); + GenerateTextBodyFile("textBodyAlignCenter", baseGroup, textBody); + } + + [TestMethod] + public void SvgTextBodyTestRightAlignmentGenerated() + { + BoundingBox bounds = new BoundingBox(0, 0, 500, 500); + var baseGroup = new GroupRenderItem(bounds); + + baseGroup.Bounds.Width = bounds.Width; + baseGroup.Bounds.Height = bounds.Height; + + 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(8.7520003318786621d, textBody.Paragraphs[1].Runs[0].Bounds.Left, delta); + Assert.AreEqual(0d, textBody.Paragraphs[1].Runs[1].Bounds.Left); + + GenerateTextBodyFile("textBodyAlignRight", baseGroup, textBody); } [TestMethod] 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); From a84dcf63872a3185fcd6199c729ec1676d52c307 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ossian=20Edstr=C3=B6m?= Date: Tue, 9 Jun 2026 17:15:58 +0200 Subject: [PATCH 22/82] Verified vertical alignment also functional --- .../SvgStandAloneTests.cs | 92 ++++++++++++++----- .../RenderItems/Textbox/RenderTextBody.cs | 6 +- 2 files changed, 73 insertions(+), 25 deletions(-) diff --git a/src/EPPlus.DrawingRenderer.Tests/DrawingShapeRenderer/SvgStandAloneTests.cs b/src/EPPlus.DrawingRenderer.Tests/DrawingShapeRenderer/SvgStandAloneTests.cs index 3485957741..9422b040fc 100644 --- a/src/EPPlus.DrawingRenderer.Tests/DrawingShapeRenderer/SvgStandAloneTests.cs +++ b/src/EPPlus.DrawingRenderer.Tests/DrawingShapeRenderer/SvgStandAloneTests.cs @@ -31,6 +31,22 @@ private GroupRenderItem GenerateShapeRenderer() return baseGroup; } + private GroupRenderItem GenerateGroupRenderItem() + { + BoundingBox bounds = new BoundingBox(0, 0, 500, 500); + + var baseGroup = new GroupRenderItem(bounds); + + var background = new RectRenderItem(baseGroup.Bounds); + + background.Width = bounds.Width; + background.Height = bounds.Height; + background.FillColor = "aliceBlue"; + + baseGroup.AddChildItem(background); + return baseGroup; + } + private void GenerateSvgFile(string fileName, BoundingBox bounds, params RenderItem[] items) { @@ -90,7 +106,6 @@ private void GenerateTextBodyFile(string fileName, GroupRenderItem baseGroup, Sv baseGroup.AddChildItem(background); List items = new List() { baseGroup }; - textBody.AppendRenderItems(items); svgShapeRenderer.Render(items); @@ -112,31 +127,26 @@ private SvgTextBodyRenderItem GenerateTextBody(GroupRenderItem baseGroup) 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); - var baseGroup = new GroupRenderItem(bounds); - - baseGroup.Bounds.Width = bounds.Width; - baseGroup.Bounds.Height = bounds.Height; - + var baseGroup = GenerateGroupRenderItem(); var textBody = GenerateTextBody(baseGroup); - - GenerateTextBodyFile("standAloneTextBody", baseGroup, textBody); + GenerateSvgFile("standAloneTextBody", baseGroup.Bounds, baseGroup); } [TestMethod] public void SvgTextBodyTestCenterAlignmentGenerated() { - BoundingBox bounds = new BoundingBox(0, 0, 500, 500); - var baseGroup = new GroupRenderItem(bounds); - - baseGroup.Bounds.Width = bounds.Width; - baseGroup.Bounds.Height = bounds.Height; + var baseGroup = GenerateGroupRenderItem(); var textBody = GenerateTextBody(baseGroup); textBody.Paragraphs[0].AddText(" a\r\n new day beckons"); @@ -164,17 +174,13 @@ public void SvgTextBodyTestCenterAlignmentGenerated() //Assert that the second paragraph has been moved correctly Assert.AreEqual(26.85546875d, textBody.Paragraphs[1].Bounds.Top); - GenerateTextBodyFile("textBodyAlignCenter", baseGroup, textBody); + GenerateSvgFile("textBodyAlignCenter", baseGroup.Bounds, baseGroup); } [TestMethod] public void SvgTextBodyTestRightAlignmentGenerated() { - BoundingBox bounds = new BoundingBox(0, 0, 500, 500); - var baseGroup = new GroupRenderItem(bounds); - - baseGroup.Bounds.Width = bounds.Width; - baseGroup.Bounds.Height = bounds.Height; + var baseGroup = GenerateGroupRenderItem(); var textBody = GenerateTextBody(baseGroup); textBody.Paragraphs[0].AddText(" a\r\n new day beckons"); @@ -200,13 +206,55 @@ public void SvgTextBodyTestRightAlignmentGenerated() Assert.AreEqual(8.7520003318786621d, textBody.Paragraphs[1].Runs[0].Bounds.Left, delta); Assert.AreEqual(0d, textBody.Paragraphs[1].Runs[1].Bounds.Left); - GenerateTextBodyFile("textBodyAlignRight", baseGroup, textBody); + 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.MaxHeight = 500; + + textBody.Bounds.Top = 0; + textBody.VerticalAlignment = TextAnchoringType.Center; + textBody.Bounds.Top = textBody.GetAlignmentVertical(); + + GenerateSvgFile("textBodyAlignVCenter", baseGroup.Bounds, baseGroup); } [TestMethod] - public void SvgTextBoxTest() + 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.MaxHeight = 500; + + textBody.Bounds.Top = 0; + textBody.VerticalAlignment = TextAnchoringType.Bottom; + textBody.Bounds.Top = textBody.GetAlignmentVertical(); + GenerateSvgFile("textBodyAlignVBottom", baseGroup.Bounds, baseGroup); } } } diff --git a/src/EPPlus.DrawingRenderer/RenderItems/Textbox/RenderTextBody.cs b/src/EPPlus.DrawingRenderer/RenderItems/Textbox/RenderTextBody.cs index 92674bf306..e93723715b 100644 --- a/src/EPPlus.DrawingRenderer/RenderItems/Textbox/RenderTextBody.cs +++ b/src/EPPlus.DrawingRenderer/RenderItems/Textbox/RenderTextBody.cs @@ -204,7 +204,7 @@ private double GetTopToAddNextParagraphAt() /// Get the start of text space vertically /// /// - protected double GetAlignmentVertical() + public double GetAlignmentVertical() { double alignmentY = 0; @@ -216,10 +216,10 @@ protected double GetAlignmentVertical() //Center means center of a Shape's ENTIRE bounding box height. //Not center of the Inset GetRectangle case TextAnchoringType.Center: - alignmentY = (Bounds.Height) / 2 + Bounds.Top; + alignmentY = (MaxHeight) / 2 - Bounds.Height; break; case TextAnchoringType.Bottom: - alignmentY = Bounds.Height; + alignmentY = MaxHeight - Bounds.Height; break; } From 388c06e02c66113663f04e5e82a82942853e553c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ossian=20Edstr=C3=B6m?= Date: Wed, 10 Jun 2026 11:27:04 +0200 Subject: [PATCH 23/82] Changed TextBox. GroupItem realtime. + margin grp --- .../SvgStandAloneTests.cs | 106 +++++++++ .../RenderItems/Textbox/RenderTextBody.cs | 5 +- .../RenderItems/Textbox/RenderTextbox.cs | 106 +++++++-- .../RenderItems/Textbox/DrawingTextBox.cs | 208 +----------------- 4 files changed, 198 insertions(+), 227 deletions(-) diff --git a/src/EPPlus.DrawingRenderer.Tests/DrawingShapeRenderer/SvgStandAloneTests.cs b/src/EPPlus.DrawingRenderer.Tests/DrawingShapeRenderer/SvgStandAloneTests.cs index 9422b040fc..ba014aa166 100644 --- a/src/EPPlus.DrawingRenderer.Tests/DrawingShapeRenderer/SvgStandAloneTests.cs +++ b/src/EPPlus.DrawingRenderer.Tests/DrawingShapeRenderer/SvgStandAloneTests.cs @@ -1,10 +1,14 @@ using EPPlus.DrawingRenderer; using EPPlus.DrawingRenderer.RenderItems; using EPPlus.DrawingRenderer.RenderItems.SvgItem; +using EPPlus.Export.ImageRenderer.RenderItems.SvgItem; using EPPlus.Fonts.OpenType.Integration.DataHolders; using EPPlus.Graphics; +using OfficeOpenXml.FormulaParsing.Excel.Functions.Engineering; +using OfficeOpenXml.FormulaParsing.Excel.Functions.MathFunctions; using System.Drawing; using System.Text; +using static OfficeOpenXml.Drawing.OleObject.Structures.OleObjectDataStructures; namespace EPPlus.Export.ImageRenderer.Tests.DrawingShapeRenderer { @@ -230,6 +234,10 @@ public void SvgTextBodyVerticalAlignmentGenerated() 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); } @@ -254,7 +262,105 @@ public void SvgTextBodyVerticalAlignmentBottomGenerated() 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); } + + + + [TestMethod] + public void BasicTextBox() + { + var group = GenerateGroupRenderItem(); + + var textbox = new RenderTextbox(group.Bounds, 500d, 500d); + textbox.TextBody = new SvgTextBodyRenderItem(group.Bounds, true); + var paragraph = textbox.TextBody.AddParagraph("Hello"); + + paragraph.AddText(" There"); + + var rtItem = new RichTextFormatSimple("Second paragraph", "Archivo Narrow", 16f, true); + rtItem.FontColor = Color.DarkGreen; + var para2 = textbox.TextBody.AddParagraph(rtItem); + + textbox.Rectangle.FillColor = "#F9F6C4"; + + textbox.AppendRenderItems(group.RenderItems); + + double delta = 0.001; + + Assert.AreEqual(107.95200681686401d, textbox.Width, delta); + Assert.AreEqual(34.979735851287842d, textbox.Height, delta); + + StringBuilder sb = new StringBuilder(); + var svgShapeRenderer = new SvgShapeRenderer(group.Bounds, sb); + + List renderItems = new List() { group }; + svgShapeRenderer.Render(renderItems); + + var svg = sb.ToString(); + //textbox.Rectangle.BorderColor = "green"; + //textbox.Rectangle.BorderWidth = 5; + + var fileName = "BasicTextbox"; + + SaveTextFileToWorkbook($"svg\\{fileName}.svg", svg); + + //var textBody = new (baseGroup.Bounds, true); + } + + + + [TestMethod] + public void TextBoxWithMargins() + { + var group = GenerateGroupRenderItem(); + + var textbox = new RenderTextbox(group.Bounds, 500d, 500d); + textbox.TextBody = new SvgTextBodyRenderItem(group.Bounds, true); + var paragraph = textbox.TextBody.AddParagraph("Hello"); + + paragraph.AddText(" There"); + + var rtItem = new RichTextFormatSimple("Second paragraph", "Archivo Narrow", 16f, true); + rtItem.FontColor = Color.DarkGreen; + var para2 = textbox.TextBody.AddParagraph(rtItem); + + textbox.Rectangle.FillColor = "#F9F6C4"; + + 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(117.95200681686401d, textbox.Width, delta); + Assert.AreEqual(44.979735851287842d, textbox.Height, delta); + + StringBuilder sb = new StringBuilder(); + var svgShapeRenderer = new SvgShapeRenderer(group.Bounds, sb); + + List renderItems = new List() { group }; + svgShapeRenderer.Render(renderItems); + + var svg = sb.ToString(); + var fileName = "MarginTextBox"; + + SaveTextFileToWorkbook($"svg\\{fileName}.svg", svg); + + //var textBody = new (baseGroup.Bounds, true); + } } } diff --git a/src/EPPlus.DrawingRenderer/RenderItems/Textbox/RenderTextBody.cs b/src/EPPlus.DrawingRenderer/RenderItems/Textbox/RenderTextBody.cs index e93723715b..83b5ccab0e 100644 --- a/src/EPPlus.DrawingRenderer/RenderItems/Textbox/RenderTextBody.cs +++ b/src/EPPlus.DrawingRenderer/RenderItems/Textbox/RenderTextBody.cs @@ -104,11 +104,14 @@ public void AppendRenderItems(List renderItems) //TranslationOffset.Left = Bounds.Left; //TranslationOffset.Top = Bounds.Top; + renderItems.Add(this); + + var titleItem = new TitleRenderItem("TextBody group"); + AddChildItem(titleItem); foreach (var item in Paragraphs) { AddChildItem(item); } - renderItems.Add(this); } public ParagraphRenderItem AddParagraph(IRichTextFormatSimple rtFormat) diff --git a/src/EPPlus.DrawingRenderer/RenderItems/Textbox/RenderTextbox.cs b/src/EPPlus.DrawingRenderer/RenderItems/Textbox/RenderTextbox.cs index 2ac2c8b550..469a39740b 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,10 +28,12 @@ public RenderTextbox(BoundingBox parent, double maxWidth, double maxHeight) Init(parent, maxWidth, maxHeight); } - //Simplified input - public RenderTextbox(BoundingBox parent, BoundingBox maxBounds) - { - } + //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 { @@ -45,30 +49,38 @@ public RectRenderItem Rectangle } } - public virtual RenderTextBody TextBody { get; set; } + RenderTextBody _textBody; + + public virtual RenderTextBody TextBody + { + get { return _textBody; } + set + { _textBody = value; + //Margins should affect textbody global position in real-time + _textBody.Bounds.Parent = _marginGroup.Bounds; + } + } public double Left { get { - return 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 @@ -107,12 +119,13 @@ public double HeightWithRotation } public double LeftMargin { - get; set; + get { return _marginGroup.Left; } set { _marginGroup.Left = value; } } public double TopMargin { - get; set; + get { return _marginGroup.Top; } + set { _marginGroup.Top = value; } } public double RightMargin @@ -130,11 +143,11 @@ public double Rotation { get { - return Rectangle.Bounds.Rotation; + return _group.Rotation; } set { - Rectangle.Bounds.Rotation = value; + _group.Rotation = value; } } /// @@ -171,8 +184,61 @@ public double GetActualBottom() } 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 I.e. a user changes textbody left and right, changing margin on the parent should not change the Local coordinates + //Therefore a group inbetween 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. diff --git a/src/EPPlus/Drawing/Renderer/RenderItems/Textbox/DrawingTextBox.cs b/src/EPPlus/Drawing/Renderer/RenderItems/Textbox/DrawingTextBox.cs index de8c20bdf3..ebbafe5446 100644 --- a/src/EPPlus/Drawing/Renderer/RenderItems/Textbox/DrawingTextBox.cs +++ b/src/EPPlus/Drawing/Renderer/RenderItems/Textbox/DrawingTextBox.cs @@ -22,9 +22,8 @@ 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; + TextBody = new DrawingTextBody(drawing, _marginGroup.Bounds, true); TextBody.MaxWidth = maxWidth; TextBody.MaxHeight = maxHeight; } @@ -34,66 +33,6 @@ 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); @@ -112,97 +51,6 @@ public void SetDrawingTextBody(DrawingTextBody tb) } public override RenderTextBody TextBody { get { return _textBody; } set { _textBody = (DrawingTextBody)value; } } - //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 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; - //} internal void ImportTextBodyAndParagraphs(ExcelTextBody body, bool useDefaults = true, ExcelHorizontalAlignment horizontalDefault = ExcelHorizontalAlignment.Left) { @@ -223,58 +71,6 @@ internal void ImportTextBodyAndParagraphs(ExcelTextBody body, bool useDefaults = _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; - - if (TextBody.AutoSize) - { - TextBody.ApplyAutoSize(); - rect.Width = TextBody.Width + RightMargin; - rect.Height = TextBody.Height + BottomMargin; - rect.Left = -LeftMargin; - rect.Top = -TopMargin; - } - - 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); - - ////TextBody.SetHorizontalAlignmentPosition(); - //TextBody.Bounds.Left = LeftMargin; - //TextBody.Bounds.Top = TopMargin; - - //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.AppendRenderItems(groupItem.RenderItems); - } - internal void ImportParagraph(ExcelDrawingParagraph item, double startingY, string text = null) { _textBody.ImportParagraph(item, startingY, text); From 34c04afdaea76eed5b8e1220169a2551b2b503b3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ossian=20Edstr=C3=B6m?= Date: Wed, 10 Jun 2026 11:35:23 +0200 Subject: [PATCH 24/82] Added tests for new textbox --- .../SvgStandAloneTests.cs | 79 +++++++++---------- 1 file changed, 38 insertions(+), 41 deletions(-) diff --git a/src/EPPlus.DrawingRenderer.Tests/DrawingShapeRenderer/SvgStandAloneTests.cs b/src/EPPlus.DrawingRenderer.Tests/DrawingShapeRenderer/SvgStandAloneTests.cs index ba014aa166..d4de4dfc23 100644 --- a/src/EPPlus.DrawingRenderer.Tests/DrawingShapeRenderer/SvgStandAloneTests.cs +++ b/src/EPPlus.DrawingRenderer.Tests/DrawingShapeRenderer/SvgStandAloneTests.cs @@ -268,12 +268,9 @@ public void SvgTextBodyVerticalAlignmentBottomGenerated() GenerateSvgFile("textBodyAlignVBottom", baseGroup.Bounds, baseGroup); } - - - [TestMethod] - public void BasicTextBox() + private RenderTextbox GenerateTextBox(out GroupRenderItem group) { - var group = GenerateGroupRenderItem(); + group = GenerateGroupRenderItem(); var textbox = new RenderTextbox(group.Bounds, 500d, 500d); textbox.TextBody = new SvgTextBodyRenderItem(group.Bounds, true); @@ -287,6 +284,13 @@ public void BasicTextBox() textbox.Rectangle.FillColor = "#F9F6C4"; + return textbox; + } + + [TestMethod] + public void BasicTextBox() + { + var textbox = GenerateTextBox(out GroupRenderItem group); textbox.AppendRenderItems(group.RenderItems); double delta = 0.001; @@ -294,21 +298,7 @@ public void BasicTextBox() Assert.AreEqual(107.95200681686401d, textbox.Width, delta); Assert.AreEqual(34.979735851287842d, textbox.Height, delta); - StringBuilder sb = new StringBuilder(); - var svgShapeRenderer = new SvgShapeRenderer(group.Bounds, sb); - - List renderItems = new List() { group }; - svgShapeRenderer.Render(renderItems); - - var svg = sb.ToString(); - //textbox.Rectangle.BorderColor = "green"; - //textbox.Rectangle.BorderWidth = 5; - - var fileName = "BasicTextbox"; - - SaveTextFileToWorkbook($"svg\\{fileName}.svg", svg); - - //var textBody = new (baseGroup.Bounds, true); + GenerateSvgFile("BasicTextBox", group.Bounds, group); } @@ -316,19 +306,7 @@ public void BasicTextBox() [TestMethod] public void TextBoxWithMargins() { - var group = GenerateGroupRenderItem(); - - var textbox = new RenderTextbox(group.Bounds, 500d, 500d); - textbox.TextBody = new SvgTextBodyRenderItem(group.Bounds, true); - var paragraph = textbox.TextBody.AddParagraph("Hello"); - - paragraph.AddText(" There"); - - var rtItem = new RichTextFormatSimple("Second paragraph", "Archivo Narrow", 16f, true); - rtItem.FontColor = Color.DarkGreen; - var para2 = textbox.TextBody.AddParagraph(rtItem); - - textbox.Rectangle.FillColor = "#F9F6C4"; + var textbox = GenerateTextBox(out GroupRenderItem group); double delta = 0.001; @@ -349,18 +327,37 @@ public void TextBoxWithMargins() Assert.AreEqual(117.95200681686401d, textbox.Width, delta); Assert.AreEqual(44.979735851287842d, textbox.Height, delta); - StringBuilder sb = new StringBuilder(); - var svgShapeRenderer = new SvgShapeRenderer(group.Bounds, sb); - List renderItems = new List() { group }; - svgShapeRenderer.Render(renderItems); + GenerateSvgFile("MarginTextBox", group.Bounds, group); + } - var svg = sb.ToString(); - var fileName = "MarginTextBox"; + [TestMethod] + public void TextBoxWithAllMargins() + { + var textbox = GenerateTextBox(out GroupRenderItem group); - SaveTextFileToWorkbook($"svg\\{fileName}.svg", svg); + double delta = 0.001; + + textbox.LeftMargin = 10d; + textbox.TopMargin = 10d; + textbox.RightMargin = 10d; + textbox.BottomMargin = 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(127.95200681686401d, textbox.Width, delta); + Assert.AreEqual(54.979735851287842d, textbox.Height, delta); - //var textBody = new (baseGroup.Bounds, true); + GenerateSvgFile("AllMarginsTextBox", group.Bounds, group); } } } From 29cd62cbdd95a11bf8cd53b3b7e96568c5ccd7f2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ossian=20Edstr=C3=B6m?= Date: Wed, 10 Jun 2026 15:17:14 +0200 Subject: [PATCH 25/82] Fixed alignment bug and in ChartTitle.DisplayedText --- .../Chart/LineChartToSvgTests.cs | 39 +++----- .../SvgStandAloneTests.cs | 43 ++++++--- .../Shape/ShapeToSvgTests.cs | 50 +++++++---- .../Textbox/ParagraphRenderItem.cs | 12 +++ .../RenderItems/Textbox/RenderTextbox.cs | 8 +- src/EPPlus/Drawing/Chart/ExcelChartTitle.cs | 1 + src/EPPlus/Drawing/Renderer/ChartRenderer.cs | 2 +- .../Textbox/DrawingParagraphRenderItem.cs | 2 +- .../RenderItems/Textbox/DrawingTextBody.cs | 88 ++++++++++++------- 9 files changed, 154 insertions(+), 91 deletions(-) diff --git a/src/EPPlus.DrawingRenderer.Tests/Chart/LineChartToSvgTests.cs b/src/EPPlus.DrawingRenderer.Tests/Chart/LineChartToSvgTests.cs index 092936ce7c..44f5074ad0 100644 --- a/src/EPPlus.DrawingRenderer.Tests/Chart/LineChartToSvgTests.cs +++ b/src/EPPlus.DrawingRenderer.Tests/Chart/LineChartToSvgTests.cs @@ -19,17 +19,17 @@ public void GenerateSvgForLineCharts_sheet1() { var ws = p.Workbook.Worksheets[0]; - var ix = 6; - var c = ws.Drawings[ix]; - var svg = c.ToSvg(); - SaveTextFileToWorkbook($"svg\\ChartForSvg_ind{ix++}.svg", svg); + //var ix = 6; + //var c = ws.Drawings[ix]; + //var svg = c.ToSvg(); + //SaveTextFileToWorkbook($"svg\\ChartForSvg_ind{ix++}.svg", svg); - //var ix = 0; - //foreach (ExcelChart c in ws.Drawings) - //{ - // var svg = c.ToSvg(); - // SaveTextFileToWorkbook($"svg\\ChartForSvg{ix++}.svg", svg); - //} + for(int i = 5; i< ws.Drawings.Count; i++) + { + var c = ws.Drawings[i]; + var svg = c.ToSvg(); + SaveTextFileToWorkbook($"svg\\ChartForSvg{i}.svg", svg); + } } } @@ -128,25 +128,6 @@ public void GenerateSvgForLineCharts() } } } - [TestMethod] - public void GenerateSvgForLineCharts2() - { - ExcelPackage.License.SetNonCommercialOrganization("EPPlus Project"); - 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 = 1; - foreach (var c in ws.Drawings) - { - var svg = c.ToSvg(); - SaveTextFileToWorkbook($"svg\\ss{ix++}.svg", svg); - } - } - } [TestMethod] public void GenerateSvgForCharts_SecondaryAxis() diff --git a/src/EPPlus.DrawingRenderer.Tests/DrawingShapeRenderer/SvgStandAloneTests.cs b/src/EPPlus.DrawingRenderer.Tests/DrawingShapeRenderer/SvgStandAloneTests.cs index d4de4dfc23..e8001dbaee 100644 --- a/src/EPPlus.DrawingRenderer.Tests/DrawingShapeRenderer/SvgStandAloneTests.cs +++ b/src/EPPlus.DrawingRenderer.Tests/DrawingShapeRenderer/SvgStandAloneTests.cs @@ -301,8 +301,6 @@ public void BasicTextBox() GenerateSvgFile("BasicTextBox", group.Bounds, group); } - - [TestMethod] public void TextBoxWithMargins() { @@ -345,19 +343,44 @@ public void TextBoxWithAllMargins() 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(127.95200681686401d, 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(142.952006816864d, textbox.Width, delta); + Assert.AreEqual(69.979735851287842d, textbox.Height, delta); + + GenerateSvgFile("TextAnchor_TextBox", group.Bounds, group); + } } } diff --git a/src/EPPlus.DrawingRenderer.Tests/Shape/ShapeToSvgTests.cs b/src/EPPlus.DrawingRenderer.Tests/Shape/ShapeToSvgTests.cs index 48c1479b8e..02f99f16ea 100644 --- a/src/EPPlus.DrawingRenderer.Tests/Shape/ShapeToSvgTests.cs +++ b/src/EPPlus.DrawingRenderer.Tests/Shape/ShapeToSvgTests.cs @@ -497,6 +497,41 @@ public void GenerateSvgForCircle() } } + [TestMethod] + public void SuperScriptShape() + { + ExcelPackage.License.SetNonCommercialOrganization("EPPlus Project"); + 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 = 1; + foreach (var c in ws.Drawings) + { + var svg = c.ToSvg(); + SaveTextFileToWorkbook($"svg\\ss{ix++}.svg", svg); + } + } + } + + [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); + } + } + [TestMethod] public void OpenRightAligned() { @@ -637,20 +672,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/RenderItems/Textbox/ParagraphRenderItem.cs b/src/EPPlus.DrawingRenderer/RenderItems/Textbox/ParagraphRenderItem.cs index 9fe3e2a1f8..66da7c508c 100644 --- a/src/EPPlus.DrawingRenderer/RenderItems/Textbox/ParagraphRenderItem.cs +++ b/src/EPPlus.DrawingRenderer/RenderItems/Textbox/ParagraphRenderItem.cs @@ -180,6 +180,18 @@ 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) diff --git a/src/EPPlus.DrawingRenderer/RenderItems/Textbox/RenderTextbox.cs b/src/EPPlus.DrawingRenderer/RenderItems/Textbox/RenderTextbox.cs index 469a39740b..7939b54198 100644 --- a/src/EPPlus.DrawingRenderer/RenderItems/Textbox/RenderTextbox.cs +++ b/src/EPPlus.DrawingRenderer/RenderItems/Textbox/RenderTextbox.cs @@ -87,7 +87,7 @@ public double Width { get { - return LeftMargin + (TextBody?.Bounds?.Width ?? 0D) + RightMargin; + return LeftMargin + (TextBody?.Bounds?.Width ?? 0D) + RightMargin + TextBody.Left; } } public double WidthRotated @@ -104,7 +104,7 @@ public double Height { get { - return TopMargin + (TextBody?.Bounds.Height ?? 0d) + BottomMargin; + return TopMargin + (TextBody?.Bounds.Height ?? 0d) + BottomMargin + TextBody.Top; } } public double HeightWithRotation @@ -229,8 +229,8 @@ public override void AppendRenderItems(List renderItems) _group.RenderItems.Add(rect); //The textbox should be in local-space. - //If I.e. a user changes textbody left and right, changing margin on the parent should not change the Local coordinates - //Therefore a group inbetween should hold the margins + //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; diff --git a/src/EPPlus/Drawing/Chart/ExcelChartTitle.cs b/src/EPPlus/Drawing/Chart/ExcelChartTitle.cs index 1d41c5b709..400f28e5b7 100644 --- a/src/EPPlus/Drawing/Chart/ExcelChartTitle.cs +++ b/src/EPPlus/Drawing/Chart/ExcelChartTitle.cs @@ -463,6 +463,7 @@ public string DisplayedText { combinedString += address.Text + separator; } + return combinedString; } return LinkedCell.Text; } diff --git a/src/EPPlus/Drawing/Renderer/ChartRenderer.cs b/src/EPPlus/Drawing/Renderer/ChartRenderer.cs index 9ca2534a2a..8970b02219 100644 --- a/src/EPPlus/Drawing/Renderer/ChartRenderer.cs +++ b/src/EPPlus/Drawing/Renderer/ChartRenderer.cs @@ -183,7 +183,7 @@ private void PlaceHorizontalAxisTitle(ChartAxisRenderer horizontalAxis) { if (horizontalAxis.Axis.AxisPosition == eAxisPosition.Bottom) { - if (SecondHorizontalAxis.Axis.ActualAxisPosition == eActualAxisPosition.BottomSecond) + if (SecondHorizontalAxis != null && SecondHorizontalAxis.Axis.ActualAxisPosition == eActualAxisPosition.BottomSecond) { horizontalAxis.Title.TextBox.Top = horizontalAxis.Rectangle.Bottom + SecondHorizontalAxis.Rectangle.Height; } diff --git a/src/EPPlus/Drawing/Renderer/RenderItems/Textbox/DrawingParagraphRenderItem.cs b/src/EPPlus/Drawing/Renderer/RenderItems/Textbox/DrawingParagraphRenderItem.cs index 61e1d725ae..eb29a9f132 100644 --- a/src/EPPlus/Drawing/Renderer/RenderItems/Textbox/DrawingParagraphRenderItem.cs +++ b/src/EPPlus/Drawing/Renderer/RenderItems/Textbox/DrawingParagraphRenderItem.cs @@ -52,7 +52,7 @@ public DrawingParagraphRenderItem(DrawingTextBody textBody, BoundingBox parent, { IsFirstParagraph = p == p._paragraphs[0]; ImportStyleInfo(textBody, p); - + HorizontalAlignment = (TextAlignment)(int)p.HorizontalAlignment; ImportMarginAndIndent(p); //ImportAlignment(textBody.AutoSize, textBody.MaxWidth, parent.Width); diff --git a/src/EPPlus/Drawing/Renderer/RenderItems/Textbox/DrawingTextBody.cs b/src/EPPlus/Drawing/Renderer/RenderItems/Textbox/DrawingTextBody.cs index ecc7597480..872e313d45 100644 --- a/src/EPPlus/Drawing/Renderer/RenderItems/Textbox/DrawingTextBody.cs +++ b/src/EPPlus/Drawing/Renderer/RenderItems/Textbox/DrawingTextBody.cs @@ -80,33 +80,52 @@ public void ImportParagraph(ExcelDrawingParagraph item, double startingY, string Paragraphs.Add(paragraph); } - //internal void SetHorizontalAlignmentPosition() - //{ - // //if (AutoSize) - // //{ - // foreach (var p in Paragraphs) - // { - // switch (p.HorizontalAlignment) - // { - // case TextAlignment.Left: - // p.Bounds.Left = 0; - // break; - // case TextAlignment.Center: - // p.Bounds.Left = (Bounds.Width / 2) - (p.Bounds.Width / 2); - // break; - // case TextAlignment.Right: - // p.Bounds.Left = Bounds.Right - p.Bounds.Width; - // break; - // case TextAlignment.Distributed: - // case TextAlignment.Justified: - // case TextAlignment.JustifiedLow: - // case TextAlignment.ThaiDistributed: - // p.Bounds.Left = 0; //TODO: Set left for now as we do not support distributed spacing yet - // break; - // } - // } - // //} - //} + internal void SetHorizontalAlignmentPosition() + { + if (AutoSize) + { + foreach (var p in Paragraphs) + { + switch (p.HorizontalAlignment) + { + case TextAlignment.Left: + p.Bounds.Left = 0; + break; + case TextAlignment.Center: + p.Bounds.Left = (Bounds.Width / 2) - (p.Bounds.Width / 2); + break; + case TextAlignment.Right: + p.Bounds.Left = Bounds.Right - p.Bounds.Width; + break; + case TextAlignment.Distributed: + case TextAlignment.Justified: + case TextAlignment.JustifiedLow: + case TextAlignment.ThaiDistributed: + p.Bounds.Left = 0; //TODO: Set left for now as we do not support distributed spacing yet + break; + } + } + } + } + + internal 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 +136,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) @@ -134,14 +155,19 @@ internal virtual void ImportTextBodyAndParagraphs(ExcelTextBody body, ExcelHoriz { ImportParagraph(paragraph, currentHeight); var addedPara = Paragraphs.Last(); + //addedPara.Bounds.Width = MaxWidth; + //addedPara.HorizontalAlignment = defaultAlignment; + currentHeight = addedPara.Bounds.Bottom; largestWidth = Math.Max(largestWidth, addedPara.Bounds.Width); } - //foreach (var paragraph in body.Paragraphs) - //{ - // SetHorizontalAlignmentPosition(); - //} + //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) + { + SetHorizontalAlignmentPosition(); + } if (Paragraphs != null && Paragraphs.Count() > 0) { From f8e83b687c029f5d71d658ab767ac8c2be89caa5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ossian=20Edstr=C3=B6m?= Date: Wed, 10 Jun 2026 16:22:11 +0200 Subject: [PATCH 26/82] Partial trendline fix --- .../RenderItems/Textbox/RenderTextBody.cs | 39 ++++++++++++++----- .../Renderer/Chart/ChartTitleRenderer.cs | 16 ++++---- .../Trendlines/ChartTrendlineRenderer.cs | 4 ++ .../RenderItems/Textbox/DrawingTextBody.cs | 4 ++ 4 files changed, 46 insertions(+), 17 deletions(-) diff --git a/src/EPPlus.DrawingRenderer/RenderItems/Textbox/RenderTextBody.cs b/src/EPPlus.DrawingRenderer/RenderItems/Textbox/RenderTextBody.cs index 83b5ccab0e..a0bcca68b3 100644 --- a/src/EPPlus.DrawingRenderer/RenderItems/Textbox/RenderTextBody.cs +++ b/src/EPPlus.DrawingRenderer/RenderItems/Textbox/RenderTextBody.cs @@ -156,17 +156,37 @@ public void ApplyAutoSize() /// public void RecalculateParagraphs() { - double lastParagraphBottom = 0; - - foreach(var paragraph in Paragraphs) + if(Paragraphs != null && Paragraphs.Count != 0) { - paragraph.Bounds.Top = lastParagraphBottom; - lastParagraphBottom = paragraph.Bounds.Bottom; - } + double lastParagraphBottom = 0; + + double smallestLeft = double.MaxValue; + double largestWidth = double.MinValue; + + ContentBounds.Top = Paragraphs[0].Bounds.Top; - Bounds.Height = lastParagraphBottom; + foreach (var paragraph in Paragraphs) + { + paragraph.Bounds.Top = lastParagraphBottom; + lastParagraphBottom = paragraph.Bounds.Bottom; + + smallestLeft = Math.Min(smallestLeft, paragraph.Bounds.Left); + largestWidth = paragraph.Bounds.Width; + } + + ContentBounds.Left = smallestLeft; + ContentBounds.Width = largestWidth; + ContentBounds.Top = Paragraphs[0].Bounds.Top; + + Bounds.Height = lastParagraphBottom; + } } + /// + /// The total bounds of all paragraphs without margins + /// + protected BoundingBox ContentBounds = new BoundingBox(); + private void AdjustAndAddParagraph(ParagraphRenderItem paragraph) { paragraph.Bounds.Name = $"Container{Paragraphs.Count}"; @@ -189,6 +209,7 @@ private void AdjustAndAddParagraph(ParagraphRenderItem paragraph) } } Paragraphs.Add(paragraph); + RecalculateParagraphs(); } private double GetTopToAddNextParagraphAt() @@ -219,10 +240,10 @@ public double GetAlignmentVertical() //Center means center of a Shape's ENTIRE bounding box height. //Not center of the Inset GetRectangle case TextAnchoringType.Center: - alignmentY = (MaxHeight) / 2 - Bounds.Height; + 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/Drawing/Renderer/Chart/ChartTitleRenderer.cs b/src/EPPlus/Drawing/Renderer/Chart/ChartTitleRenderer.cs index cbd4977a5c..d71eb3c3cd 100644 --- a/src/EPPlus/Drawing/Renderer/Chart/ChartTitleRenderer.cs +++ b/src/EPPlus/Drawing/Renderer/Chart/ChartTitleRenderer.cs @@ -112,20 +112,20 @@ 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; diff --git a/src/EPPlus/Drawing/Renderer/Chart/ChartTypeDrawers/Trendlines/ChartTrendlineRenderer.cs b/src/EPPlus/Drawing/Renderer/Chart/ChartTypeDrawers/Trendlines/ChartTrendlineRenderer.cs index 79ab7a93bb..5fa9cf92d5 100644 --- a/src/EPPlus/Drawing/Renderer/Chart/ChartTypeDrawers/Trendlines/ChartTrendlineRenderer.cs +++ b/src/EPPlus/Drawing/Renderer/Chart/ChartTypeDrawers/Trendlines/ChartTrendlineRenderer.cs @@ -240,6 +240,10 @@ private void AddLblText(DrawingTextBox lbl, string labelText) { 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() diff --git a/src/EPPlus/Drawing/Renderer/RenderItems/Textbox/DrawingTextBody.cs b/src/EPPlus/Drawing/Renderer/RenderItems/Textbox/DrawingTextBody.cs index 872e313d45..f82d3b0435 100644 --- a/src/EPPlus/Drawing/Renderer/RenderItems/Textbox/DrawingTextBody.cs +++ b/src/EPPlus/Drawing/Renderer/RenderItems/Textbox/DrawingTextBody.cs @@ -78,6 +78,7 @@ public void ImportParagraph(ExcelDrawingParagraph item, double startingY, string } } Paragraphs.Add(paragraph); + RecalculateParagraphs(); } internal void SetHorizontalAlignmentPosition() @@ -162,6 +163,9 @@ internal virtual void ImportTextBodyAndParagraphs(ExcelTextBody body, ExcelHoriz largestWidth = Math.Max(largestWidth, addedPara.Bounds.Width); } + //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) From 859353a185508518a09349652a7b1b9559303cbd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ossian=20Edstr=C3=B6m?= Date: Wed, 10 Jun 2026 16:46:01 +0200 Subject: [PATCH 27/82] Partial fix for trendline autosize alignment issue --- .../RenderItems/Textbox/RenderTextBody.cs | 20 +++++++++++++------ 1 file changed, 14 insertions(+), 6 deletions(-) diff --git a/src/EPPlus.DrawingRenderer/RenderItems/Textbox/RenderTextBody.cs b/src/EPPlus.DrawingRenderer/RenderItems/Textbox/RenderTextBody.cs index a0bcca68b3..3e0f046370 100644 --- a/src/EPPlus.DrawingRenderer/RenderItems/Textbox/RenderTextBody.cs +++ b/src/EPPlus.DrawingRenderer/RenderItems/Textbox/RenderTextBody.cs @@ -158,12 +158,11 @@ public void RecalculateParagraphs() { if(Paragraphs != null && Paragraphs.Count != 0) { - double lastParagraphBottom = 0; + double lastParagraphBottom = Paragraphs[0].Bounds.Top; double smallestLeft = double.MaxValue; double largestWidth = double.MinValue; - - ContentBounds.Top = Paragraphs[0].Bounds.Top; + double totalHeight = 0; foreach (var paragraph in Paragraphs) { @@ -172,13 +171,19 @@ public void RecalculateParagraphs() smallestLeft = Math.Min(smallestLeft, paragraph.Bounds.Left); largestWidth = paragraph.Bounds.Width; + totalHeight += paragraph.Bounds.Height; } + ContentBounds.Top = Paragraphs[0].Bounds.Top; ContentBounds.Left = smallestLeft; ContentBounds.Width = largestWidth; - ContentBounds.Top = Paragraphs[0].Bounds.Top; + ContentBounds.Height = totalHeight; - Bounds.Height = lastParagraphBottom; + if (AutoSize) + { + Bounds.Height = totalHeight; + Bounds.Width = ContentBounds.Width; + } } } @@ -240,7 +245,10 @@ public double GetAlignmentVertical() //Center means center of a Shape's ENTIRE bounding box height. //Not center of the Inset GetRectangle case TextAnchoringType.Center: - alignmentY = (Bounds.Height) / 2 - ContentBounds.Height; + if(AutoSize == false) + { + alignmentY = (Bounds.Height) / 2 - ContentBounds.Height; + } break; case TextAnchoringType.Bottom: alignmentY = Bounds.Height - ContentBounds.Height; From 03a5f5dea3565a8a0b2a2fc0a762da95090720b5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ossian=20Edstr=C3=B6m?= Date: Wed, 10 Jun 2026 16:57:42 +0200 Subject: [PATCH 28/82] Functional Trendlines fix --- .../DrawingShapeRenderer/SvgStandAloneTests.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/EPPlus.DrawingRenderer.Tests/DrawingShapeRenderer/SvgStandAloneTests.cs b/src/EPPlus.DrawingRenderer.Tests/DrawingShapeRenderer/SvgStandAloneTests.cs index e8001dbaee..bf24ef0fd7 100644 --- a/src/EPPlus.DrawingRenderer.Tests/DrawingShapeRenderer/SvgStandAloneTests.cs +++ b/src/EPPlus.DrawingRenderer.Tests/DrawingShapeRenderer/SvgStandAloneTests.cs @@ -228,7 +228,7 @@ public void SvgTextBodyVerticalAlignmentGenerated() textBody.ApplyAutoSize(); textBody.AutoSize = false; - textBody.MaxHeight = 500; + textBody.Height = 500; textBody.Bounds.Top = 0; textBody.VerticalAlignment = TextAnchoringType.Center; @@ -256,7 +256,7 @@ public void SvgTextBodyVerticalAlignmentBottomGenerated() textBody.ApplyAutoSize(); textBody.AutoSize = false; - textBody.MaxHeight = 500; + textBody.Height = 500; textBody.Bounds.Top = 0; textBody.VerticalAlignment = TextAnchoringType.Bottom; From 796e9c907664fc6da5b026b72bce505dfa636466 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ossian=20Edstr=C3=B6m?= Date: Thu, 11 Jun 2026 09:41:48 +0200 Subject: [PATCH 29/82] Fixed center-alignment for shapes --- .../Shape/ShapeToSvgTests.cs | 1 - .../Textbox/ParagraphRenderItem.cs | 6 +- .../RenderItems/Textbox/DrawingTextBody.cs | 55 ++++++++++--------- 3 files changed, 32 insertions(+), 30 deletions(-) diff --git a/src/EPPlus.DrawingRenderer.Tests/Shape/ShapeToSvgTests.cs b/src/EPPlus.DrawingRenderer.Tests/Shape/ShapeToSvgTests.cs index 02f99f16ea..ce95ac8260 100644 --- a/src/EPPlus.DrawingRenderer.Tests/Shape/ShapeToSvgTests.cs +++ b/src/EPPlus.DrawingRenderer.Tests/Shape/ShapeToSvgTests.cs @@ -633,7 +633,6 @@ public void GenerateShapeCenteredParagraph() _currentShape.GetSizeInPixels(out int testWidth, out int testHeight); - var svg = _currentShape.ToSvg(); SaveTextFileToWorkbook("svg\\centeredParagraph.svg", svg); SaveAndCleanup(p); diff --git a/src/EPPlus.DrawingRenderer/RenderItems/Textbox/ParagraphRenderItem.cs b/src/EPPlus.DrawingRenderer/RenderItems/Textbox/ParagraphRenderItem.cs index 66da7c508c..ad2c1494e2 100644 --- a/src/EPPlus.DrawingRenderer/RenderItems/Textbox/ParagraphRenderItem.cs +++ b/src/EPPlus.DrawingRenderer/RenderItems/Textbox/ParagraphRenderItem.cs @@ -276,10 +276,8 @@ protected void WrapTextFragmentsAndGenerateTextRuns() widthOfLargestLine = Lines.LargestWidthWithoutSpace; combinedHeight = Lines.GetHeightOfCollection(_lsMultiplier, lineSpacingResult); - if(AutoSize) - { - Bounds.Width = widthOfLargestLine + RightMargin; - } + + Bounds.Width = widthOfLargestLine + RightMargin; //SetHorizontalAlignment(widthOfLargestLine); int lineIdx = 0; diff --git a/src/EPPlus/Drawing/Renderer/RenderItems/Textbox/DrawingTextBody.cs b/src/EPPlus/Drawing/Renderer/RenderItems/Textbox/DrawingTextBody.cs index f82d3b0435..4a1d282266 100644 --- a/src/EPPlus/Drawing/Renderer/RenderItems/Textbox/DrawingTextBody.cs +++ b/src/EPPlus/Drawing/Renderer/RenderItems/Textbox/DrawingTextBody.cs @@ -81,30 +81,32 @@ public void ImportParagraph(ExcelDrawingParagraph item, double startingY, string 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) { - foreach (var p in Paragraphs) + switch (p.HorizontalAlignment) { - switch (p.HorizontalAlignment) - { - case TextAlignment.Left: - p.Bounds.Left = 0; - break; - case TextAlignment.Center: - p.Bounds.Left = (Bounds.Width / 2) - (p.Bounds.Width / 2); - break; - case TextAlignment.Right: - p.Bounds.Left = Bounds.Right - p.Bounds.Width; - break; - case TextAlignment.Distributed: - case TextAlignment.Justified: - case TextAlignment.JustifiedLow: - case TextAlignment.ThaiDistributed: - p.Bounds.Left = 0; //TODO: Set left for now as we do not support distributed spacing yet - break; - } + case TextAlignment.Left: + p.Bounds.Left = 0; + break; + case TextAlignment.Center: + p.Bounds.Left = (Bounds.Width / 2) - (p.Bounds.Width / 2); + break; + case TextAlignment.Right: + p.Bounds.Left = Bounds.Right - p.Bounds.Width; + break; + case TextAlignment.Distributed: + case TextAlignment.Justified: + case TextAlignment.JustifiedLow: + case TextAlignment.ThaiDistributed: + p.Bounds.Left = 0; //TODO: Set left for now as we do not support distributed spacing yet + break; } } } @@ -150,6 +152,8 @@ 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) @@ -163,6 +167,12 @@ internal virtual void ImportTextBodyAndParagraphs(ExcelTextBody body, ExcelHoriz largestWidth = Math.Max(largestWidth, addedPara.Bounds.Width); } + + if (Paragraphs != null && Paragraphs.Count() > 0 && AutoSize) + { + Bounds.Height = currentHeight; + } + //Ensure contentBounds are calculated and paragraphs don't overlap RecalculateParagraphs(); @@ -173,11 +183,6 @@ internal virtual void ImportTextBodyAndParagraphs(ExcelTextBody body, ExcelHoriz SetHorizontalAlignmentPosition(); } - if (Paragraphs != null && Paragraphs.Count() > 0) - { - Bounds.Height = currentHeight; - } - Bounds.Top = GetAlignmentVertical(); } From 1c09dd8d175a171fe06c77d76afa8a3a2a16c1f3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ossian=20Edstr=C3=B6m?= Date: Thu, 11 Jun 2026 10:21:52 +0200 Subject: [PATCH 30/82] Fixed sIcon location on datalabel --- .../Trendlines/ChartTrendlineRenderer.cs | 14 +++++++------- .../Renderer/Chart/DataLabels/SvgDataLabelPoint.cs | 4 +++- .../RenderItems/Textbox/DrawingTextBody.cs | 10 +--------- 3 files changed, 11 insertions(+), 17 deletions(-) diff --git a/src/EPPlus/Drawing/Renderer/Chart/ChartTypeDrawers/Trendlines/ChartTrendlineRenderer.cs b/src/EPPlus/Drawing/Renderer/Chart/ChartTypeDrawers/Trendlines/ChartTrendlineRenderer.cs index 5fa9cf92d5..9ef938bbc5 100644 --- a/src/EPPlus/Drawing/Renderer/Chart/ChartTypeDrawers/Trendlines/ChartTrendlineRenderer.cs +++ b/src/EPPlus/Drawing/Renderer/Chart/ChartTypeDrawers/Trendlines/ChartTrendlineRenderer.cs @@ -220,12 +220,6 @@ private void AddLblText(DrawingTextBox lbl, string labelText) int pIx = 0; var ix = labelText.IndexOf("|ss:"); - //lbl.TextBody.Paragraphs[0].HorizontalAlignment = TextAlignment.Center; - - if (ix < 0) - { - lbl.TextBody.Paragraphs[0].AddText(labelText); - } while(ix>=0 && ix < labelText.Length) { lbl.TextBody.Paragraphs[0].AddText(labelText.Substring(pIx, ix - pIx)); @@ -236,7 +230,13 @@ private void AddLblText(DrawingTextBox lbl, string labelText) ix = labelText.IndexOf("|ss:", pIx); } - if(pIx < labelText.Length) + //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)); } diff --git a/src/EPPlus/Drawing/Renderer/Chart/DataLabels/SvgDataLabelPoint.cs b/src/EPPlus/Drawing/Renderer/Chart/DataLabels/SvgDataLabelPoint.cs index fe439a7dac..afb1b0c122 100644 --- a/src/EPPlus/Drawing/Renderer/Chart/DataLabels/SvgDataLabelPoint.cs +++ b/src/EPPlus/Drawing/Renderer/Chart/DataLabels/SvgDataLabelPoint.cs @@ -493,7 +493,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/RenderItems/Textbox/DrawingTextBody.cs b/src/EPPlus/Drawing/Renderer/RenderItems/Textbox/DrawingTextBody.cs index 4a1d282266..a3d5851673 100644 --- a/src/EPPlus/Drawing/Renderer/RenderItems/Textbox/DrawingTextBody.cs +++ b/src/EPPlus/Drawing/Renderer/RenderItems/Textbox/DrawingTextBody.cs @@ -52,14 +52,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) { @@ -160,7 +153,6 @@ internal virtual void ImportTextBodyAndParagraphs(ExcelTextBody body, ExcelHoriz { ImportParagraph(paragraph, currentHeight); var addedPara = Paragraphs.Last(); - //addedPara.Bounds.Width = MaxWidth; //addedPara.HorizontalAlignment = defaultAlignment; currentHeight = addedPara.Bounds.Bottom; From 9cae12a6d09a8eb05a14ac0249ad6a14fd87c081 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jan=20K=C3=A4llman?= Date: Thu, 11 Jun 2026 11:16:07 +0200 Subject: [PATCH 31/82] Fixes axis position --- src/EPPlus/Drawing/Chart/ExcelChartAxisStandard.cs | 8 ++++---- .../Drawing/Renderer/Chart/ChartPlotareaRenderer.cs | 8 ++++---- src/EPPlus/Drawing/Renderer/ChartRenderer.cs | 4 ++-- 3 files changed, 10 insertions(+), 10 deletions(-) diff --git a/src/EPPlus/Drawing/Chart/ExcelChartAxisStandard.cs b/src/EPPlus/Drawing/Chart/ExcelChartAxisStandard.cs index 02be725181..7d4174febf 100644 --- a/src/EPPlus/Drawing/Chart/ExcelChartAxisStandard.cs +++ b/src/EPPlus/Drawing/Chart/ExcelChartAxisStandard.cs @@ -186,7 +186,7 @@ public eActualAxisPosition ActualAxisPosition { if (LabelPosition == eTickLabelPosition.Low) { - if (_chart.Axis.FirstOrDefault(x => x.AxisPosition == eAxisPosition.Left)?.LabelPosition == eTickLabelPosition.High) + if (_chart.Axis.FirstOrDefault(x => x.AxisPosition == eAxisPosition.Left)?.LabelPosition != eTickLabelPosition.Low) { return eActualAxisPosition.Left; } @@ -197,7 +197,7 @@ public eActualAxisPosition ActualAxisPosition } else { - if (_chart.Axis.FirstOrDefault(x => x.AxisPosition == eAxisPosition.Left)?.LabelPosition == eTickLabelPosition.Low) + if (_chart.Axis.FirstOrDefault(x => x.AxisPosition == eAxisPosition.Left)?.LabelPosition != eTickLabelPosition.High) { return eActualAxisPosition.Right; } @@ -211,7 +211,7 @@ public eActualAxisPosition ActualAxisPosition { if (LabelPosition == eTickLabelPosition.Low) { - if (_chart.Axis.FirstOrDefault(x => x.AxisPosition == eAxisPosition.Bottom)?.LabelPosition == eTickLabelPosition.High) + if (_chart.Axis.FirstOrDefault(x => x.AxisPosition == eAxisPosition.Bottom)?.LabelPosition != eTickLabelPosition.Low) { return eActualAxisPosition.Bottom; } @@ -222,7 +222,7 @@ public eActualAxisPosition ActualAxisPosition } else { - if (_chart.Axis.FirstOrDefault(x => x.AxisPosition == eAxisPosition.Bottom)?.LabelPosition == eTickLabelPosition.Low) + if (_chart.Axis.FirstOrDefault(x => x.AxisPosition == eAxisPosition.Bottom)?.LabelPosition != eTickLabelPosition.High) { return eActualAxisPosition.Top; } diff --git a/src/EPPlus/Drawing/Renderer/Chart/ChartPlotareaRenderer.cs b/src/EPPlus/Drawing/Renderer/Chart/ChartPlotareaRenderer.cs index d4d24f01af..22082fb41f 100644 --- a/src/EPPlus/Drawing/Renderer/Chart/ChartPlotareaRenderer.cs +++ b/src/EPPlus/Drawing/Renderer/Chart/ChartPlotareaRenderer.cs @@ -70,8 +70,8 @@ private double GetPlotAreaHeight(RectRenderItem rect) double vaHeight = 0; if (bottomAxis!=null) { - var bottomSecondAxis = GetAxisActualByPosition(eActualAxisPosition.Bottom); - vaHeight = (bottomAxis.Rectangle?.Height ?? 0D) + (bottomAxis.Title?.TextBox?.GetActualHeight() ?? 0D) + (bottomSecondAxis.Rectangle?.Height ?? 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) { @@ -97,7 +97,7 @@ private double GetPlotAreaWidth(RectRenderItem rect) } else { - rightAxisWidth = (rightAxis.Title?.TextBox.GetActualWidth() ?? 0D) + (rightAxis.Rectangle?.Width ?? 0D) + (rightSecondAxis.Rectangle?.Width ?? 0D); + rightAxisWidth = (rightAxis.Title?.TextBox.GetActualWidth() ?? 0D) + (rightAxis.Rectangle?.Width ?? 0D) + (rightSecondAxis?.Rectangle?.Width ?? 0D); } var width = right - rightAxisWidth - rect.GlobalLeft; @@ -166,7 +166,7 @@ private double GetPlotAreaTop() } else { - haHeight = (topAxis.Rectangle?.Height ?? 0D) + (topSecondAxis.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; diff --git a/src/EPPlus/Drawing/Renderer/ChartRenderer.cs b/src/EPPlus/Drawing/Renderer/ChartRenderer.cs index 8970b02219..2416c5ea73 100644 --- a/src/EPPlus/Drawing/Renderer/ChartRenderer.cs +++ b/src/EPPlus/Drawing/Renderer/ChartRenderer.cs @@ -99,7 +99,7 @@ private void SetAxisPositionsFromPlotarea() if (HorizontalAxis != null && HorizontalAxis.Rectangle != null) { - PlaceHorizontalAxis(HorizontalAxis, false); + 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 (HorizontalAxis.Axis.TickLabelPosition == eTickLabelPosition.NextTo && VerticalAxis.Axis.AxisType == eAxisType.Val && VerticalAxis.Min < 0D) @@ -284,7 +284,7 @@ private void PlaceVerticalAxisTitle(ChartAxisRenderer verticalAxis) } else { - verticalAxis.Title.TextBox.Left = ChartArea.LeftMargin; + verticalAxis.Title.TextBox.Left = Plotarea.Group.Left - verticalAxis.Rectangle.Width - verticalAxis.Title.TextBox.GetActualWidth() - 1.5; } } else From 7b1403f58cddf24a9caa9bccedbb11d4ceb050da Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ossian=20Edstr=C3=B6m?= Date: Thu, 11 Jun 2026 12:56:37 +0200 Subject: [PATCH 32/82] Fixed width of textbox --- .../RenderItems/Textbox/RenderTextBody.cs | 2 +- .../Renderer/Chart/DataLabels/SvgDataLabelPoint.cs | 13 +++++++++---- 2 files changed, 10 insertions(+), 5 deletions(-) diff --git a/src/EPPlus.DrawingRenderer/RenderItems/Textbox/RenderTextBody.cs b/src/EPPlus.DrawingRenderer/RenderItems/Textbox/RenderTextBody.cs index 3e0f046370..49c3534530 100644 --- a/src/EPPlus.DrawingRenderer/RenderItems/Textbox/RenderTextBody.cs +++ b/src/EPPlus.DrawingRenderer/RenderItems/Textbox/RenderTextBody.cs @@ -170,7 +170,7 @@ public void RecalculateParagraphs() lastParagraphBottom = paragraph.Bounds.Bottom; smallestLeft = Math.Min(smallestLeft, paragraph.Bounds.Left); - largestWidth = paragraph.Bounds.Width; + largestWidth = Math.Max(largestWidth, paragraph.Bounds.Width); totalHeight += paragraph.Bounds.Height; } diff --git a/src/EPPlus/Drawing/Renderer/Chart/DataLabels/SvgDataLabelPoint.cs b/src/EPPlus/Drawing/Renderer/Chart/DataLabels/SvgDataLabelPoint.cs index afb1b0c122..f8b11e96f3 100644 --- a/src/EPPlus/Drawing/Renderer/Chart/DataLabels/SvgDataLabelPoint.cs +++ b/src/EPPlus/Drawing/Renderer/Chart/DataLabels/SvgDataLabelPoint.cs @@ -143,10 +143,15 @@ internal void ImportDataLabel(ExcelChartStandardSerie serie, ExcelChartDataLabel _txtBox = txtBox; - if (dataLabel.Fill.IsEmpty == false) - { - _txtBox.Rectangle.SetDrawingPropertiesFill(ChartRenderer.Theme, dataLabel.Fill, null); - } + _txtBox.Rectangle.SetDrawingPropertiesFill(ChartRenderer.Theme, dataLabel.Fill, null); + //if (dataLabel.Fill.IsEmpty == false) + //{ + // _txtBox.Rectangle.SetDrawingPropertiesFill(ChartRenderer.Theme, dataLabel.Fill, null); + //} + //else + //{ + // _txtBox.Rectangle.SetDrawingPropertiesFill(ChartRenderer.Theme, dataLabel.Fill, null); + //} if (dataLabel.Font.IsEmpty == false) { txtBox.TextBody.FontColorString = "#" + dataLabel.Font.Color.ToColorString(); From 3ed60c455a9ccc648bd3f651a259aa5d1b22f027 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ossian=20Edstr=C3=B6m?= Date: Mon, 15 Jun 2026 10:50:16 +0200 Subject: [PATCH 33/82] Fixed multiple paragraph test --- .../Integration/LayoutSystemTests.cs | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/src/EPPlus.Fonts.OpenType.Tests/Integration/LayoutSystemTests.cs b/src/EPPlus.Fonts.OpenType.Tests/Integration/LayoutSystemTests.cs index e5a4dc525b..9e28aa3a12 100644 --- a/src/EPPlus.Fonts.OpenType.Tests/Integration/LayoutSystemTests.cs +++ b/src/EPPlus.Fonts.OpenType.Tests/Integration/LayoutSystemTests.cs @@ -394,13 +394,22 @@ public void TestLayoutSystemMultipleParagraphs() Size = 11, SubFamily = FontSubFamily.Bold }; + + var font2 = new FontFormatBase() + { + Family = "Aptos Narrow", + Size = 11, + SubFamily = FontSubFamily.Italic + }; + var fragments = new List() { - new TextFragment() {Text = lstOfRichText[0], Font = font } + new TextFragment() {Text = lstOfRichText[0], Font = font }, + new TextFragment() {Text = lstOfRichText[1], Font = font2 } }; var layout = new LayoutSystem(fragments); - Assert.AreEqual(3, layout.GetParagraphSeparatorCount()); + Assert.AreEqual(5, layout.GetParagraphSeparatorCount()); //var rtCollection = new RichTextCollectionBase(); //var someTextRt = rtCollection.Add("SomeText", true); //var richRt = rtCollection.Add("rich"); From dd28f16a9f446652fba14faee85fbdb258e730c3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ossian=20Edstr=C3=B6m?= Date: Mon, 15 Jun 2026 12:21:36 +0200 Subject: [PATCH 34/82] Temporary fix for exact font --- .../FallbackFonts/FontProviderTests.cs | 34 +++++++++++++++++++ .../Integration/LayoutSystemTests.cs | 25 -------------- .../OpenTypeFontEngine.cs | 18 ++++++++-- .../Fonts/FontAvailability.cs | 4 ++- 4 files changed, 52 insertions(+), 29 deletions(-) diff --git a/src/EPPlus.Fonts.OpenType.Tests/FallbackFonts/FontProviderTests.cs b/src/EPPlus.Fonts.OpenType.Tests/FallbackFonts/FontProviderTests.cs index 6f8f1fd93d..80c6c84dab 100644 --- a/src/EPPlus.Fonts.OpenType.Tests/FallbackFonts/FontProviderTests.cs +++ b/src/EPPlus.Fonts.OpenType.Tests/FallbackFonts/FontProviderTests.cs @@ -79,6 +79,40 @@ public void DefaultFontProvider_MixedTextAndEmoji_ShouldUseMultipleFonts() Assert.AreNotEqual(_robotoFont, usedFonts[1], "Second font should be emoji fallback"); } + [TestMethod] + public void DefaultFontProvider_EnsureLastFallbackDoesNotThrowOnExactAllowEmbed() + { + var engine = new OpenTypeFontEngine(cfg => + { + foreach (var folder in FontFolders) + cfg.FontDirectories.Add(folder); + cfg.SearchSystemDirectories = false; + cfg.SetScriptFallback(UnicodeScript.Latin, "Archivo Narrow"); + }); + + engine.LeastRequiredAvailability = FontAvailability.ExactAllowEmbed; + + var shaper = engine.GetTextShaper("Archivo Narrow", FontSubFamily.Regular); + + var shaped = shaper.Shape("Hello There World"); + var usedFonts = shaper.GetUsedFonts().ToList(); + } + + [TestMethod] + public void DefaultFontProvider_EnsureLastFallbackThrowOnExact() + { + var engine = new OpenTypeFontEngine(cfg => + { + foreach (var folder in FontFolders) + cfg.FontDirectories.Add(folder); + cfg.SearchSystemDirectories = false; + cfg.SetScriptFallback(UnicodeScript.Latin, "Archivo Narrow"); + }); + + engine.LeastRequiredAvailability = FontAvailability.Exact; + Assert.ThrowsExactly(() => { engine.GetTextShaper("Archivo Narrow", FontSubFamily.Regular); }); + } + [TestMethod] public void TextShaper_SurrogatePair_ShouldMapToSingleGlyph() { diff --git a/src/EPPlus.Fonts.OpenType.Tests/Integration/LayoutSystemTests.cs b/src/EPPlus.Fonts.OpenType.Tests/Integration/LayoutSystemTests.cs index 9e28aa3a12..0374c10028 100644 --- a/src/EPPlus.Fonts.OpenType.Tests/Integration/LayoutSystemTests.cs +++ b/src/EPPlus.Fonts.OpenType.Tests/Integration/LayoutSystemTests.cs @@ -410,31 +410,6 @@ public void TestLayoutSystemMultipleParagraphs() var layout = new LayoutSystem(fragments); Assert.AreEqual(5, layout.GetParagraphSeparatorCount()); - //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"); - - //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; - - - //someTextRt.FontData.Family = "Archivo Narrow"; - - //richRt } } } diff --git a/src/EPPlus.Fonts.OpenType/OpenTypeFontEngine.cs b/src/EPPlus.Fonts.OpenType/OpenTypeFontEngine.cs index 78d3136271..08c3584a8c 100644 --- a/src/EPPlus.Fonts.OpenType/OpenTypeFontEngine.cs +++ b/src/EPPlus.Fonts.OpenType/OpenTypeFontEngine.cs @@ -106,8 +106,8 @@ public OpenTypeFontEngine(Action configure) // Public API // ----------------------------------------------------------------------------------------- - public FontAvailability LeastRequiredAvailability { get; set; } = FontAvailability.Exact; - bool RequireExactFoundFont { get { return LeastRequiredAvailability == FontAvailability.Exact; } } + public FontAvailability LeastRequiredAvailability { get; set; } = FontAvailability.ExactAllowEmbed; + bool RequireExactFoundFont { get { return LeastRequiredAvailability == FontAvailability.ExactAllowEmbed || LeastRequiredAvailability == FontAvailability.Exact; } } bool RequireFamilyFont{ get { return RequireExactFoundFont || LeastRequiredAvailability == FontAvailability.FamilyOnly; } } //public FontAvailability FallBackAvailablility = FontAvailability.Exact; @@ -138,7 +138,19 @@ public TextShaper GetTextShaper(string fontName, FontSubFamily subFamily = FontS var availability = GetFontAvailability(fontName, subFamily); if (RequireExactFoundFont && availability != FontAvailability.Exact) { - throw new FileNotFoundException($"Could not find Font: {fontName} fallbacked to font:{font.GetEnglishFontFamilyName()}"); + var fullFontName = font.GetEnglishFontFamilyName(); + if(LeastRequiredAvailability == FontAvailability.ExactAllowEmbed) + { + //If we fallbacked to the correct embedded font despite not finding it in specified folders there's no reason to throw. + if (fullFontName.Contains(fontName) == false) + { + throw new FileNotFoundException($"Could not find Font: {fontName} fallbacked to font: {fullFontName}"); + } + } + else + { + throw new FileNotFoundException($"Could not find Font: {fontName} fallbacked to font: {fullFontName}"); + } } else if(RequireFamilyFont && availability != FontAvailability.FamilyOnly && availability != FontAvailability.Exact) { diff --git a/src/EPPlus.Interfaces/Fonts/FontAvailability.cs b/src/EPPlus.Interfaces/Fonts/FontAvailability.cs index 213f632459..be33bde183 100644 --- a/src/EPPlus.Interfaces/Fonts/FontAvailability.cs +++ b/src/EPPlus.Interfaces/Fonts/FontAvailability.cs @@ -26,6 +26,8 @@ public enum FontAvailability FamilyOnly, /// The exact font family and subfamily is available. - Exact + Exact, + /// Exact but does not throw if the embedded fallback font is asked for + ExactAllowEmbed } } \ No newline at end of file From 6531b1b845e06be25f4531e65b89fc496f349f03 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jan=20K=C3=A4llman?= Date: Mon, 15 Jun 2026 13:42:34 +0200 Subject: [PATCH 35/82] WIP:Added first test for errorbars --- .../Chart/ErrorbarsTests.cs | 30 +++++++++++++++++++ .../Tables/Name/NameTable.cs | 11 ++++++- src/EPPlus/Drawing/Chart/ExcelChartSerie.cs | 18 ++++++++++- .../Renderer/Chart/ChartLegendRenderer.cs | 6 ++-- src/EPPlus/ExcelRangeBase.cs | 20 ++++++++++++- 5 files changed, 79 insertions(+), 6 deletions(-) create mode 100644 src/EPPlus.DrawingRenderer.Tests/Chart/ErrorbarsTests.cs diff --git a/src/EPPlus.DrawingRenderer.Tests/Chart/ErrorbarsTests.cs b/src/EPPlus.DrawingRenderer.Tests/Chart/ErrorbarsTests.cs new file mode 100644 index 0000000000..db5a3b97bd --- /dev/null +++ b/src/EPPlus.DrawingRenderer.Tests/Chart/ErrorbarsTests.cs @@ -0,0 +1,30 @@ +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\\Trendline_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); + } + } + } + } +} 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/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/Renderer/Chart/ChartLegendRenderer.cs b/src/EPPlus/Drawing/Renderer/Chart/ChartLegendRenderer.cs index 2789199c67..fd51c489a0 100644 --- a/src/EPPlus/Drawing/Renderer/Chart/ChartLegendRenderer.cs +++ b/src/EPPlus/Drawing/Renderer/Chart/ChartLegendRenderer.cs @@ -59,12 +59,12 @@ internal ChartLegendRenderer(ChartRenderer sc, bool isDataLabelLegend = false) : { 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; @@ -150,7 +150,7 @@ private RectRenderItem GetLegendRectangleAndEntrySize(ExcelChartLegend l, out do { widestLine = width + RightMargin; } - width = RightMargin + widest; + width = RightMargin + entryWidth; } else { 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 /// From fc635bb0ad46a61cd30029b0cf89bfce7dbe9103 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jan=20K=C3=A4llman?= Date: Tue, 16 Jun 2026 08:00:47 +0200 Subject: [PATCH 36/82] Fixed some issues on chart axis and legends. --- .../Chart/ErrorbarsTests.cs | 2 +- src/EPPlus/Drawing/Renderer/Chart/ChartAxisRenderer.cs | 5 +++-- .../Drawing/Renderer/Chart/ChartLegendRenderer.cs | 10 +++++----- 3 files changed, 9 insertions(+), 8 deletions(-) diff --git a/src/EPPlus.DrawingRenderer.Tests/Chart/ErrorbarsTests.cs b/src/EPPlus.DrawingRenderer.Tests/Chart/ErrorbarsTests.cs index db5a3b97bd..5b67b2c388 100644 --- a/src/EPPlus.DrawingRenderer.Tests/Chart/ErrorbarsTests.cs +++ b/src/EPPlus.DrawingRenderer.Tests/Chart/ErrorbarsTests.cs @@ -16,7 +16,7 @@ public void GenerateSvgForErrorbars_Sheet1() //var ix = 4; //var c = ws.Drawings[ix]; //var svg = c.ToSvg(); - //SaveTextFileToWorkbook($"svg\\Trendline_sheet1_ind{ix++}.svg", svg); + //SaveTextFileToWorkbook($"svg\\Error_sheet1_ind{ix++}.svg", svg); var ix = 0; foreach (ExcelChart c in ws.Drawings) diff --git a/src/EPPlus/Drawing/Renderer/Chart/ChartAxisRenderer.cs b/src/EPPlus/Drawing/Renderer/Chart/ChartAxisRenderer.cs index 8eefdf9cf8..a99a7c4495 100644 --- a/src/EPPlus/Drawing/Renderer/Chart/ChartAxisRenderer.cs +++ b/src/EPPlus/Drawing/Renderer/Chart/ChartAxisRenderer.cs @@ -548,16 +548,17 @@ private double GetAxisItemLeft(int i, OfficeOpenXml.Interfaces.Drawing.Text.Text { if (IsCatAx()) { - double majorWidth; + double majorWidth, majorHalf=0D; if (Axis.CrossingAxis == null || Axis.CrossingAxis.CrossBetween == eCrossBetween.Between) { majorWidth = Rectangle.Width / AxisValues.Count; + majorHalf = majorWidth / 2; } else { majorWidth = Rectangle.Width / (AxisValues.Count - 1); } - var majorTickStartingPosition = Rectangle.Left + majorWidth * i; + var majorTickStartingPosition = Rectangle.Left + majorWidth * i + majorHalf; return majorTickStartingPosition; } else diff --git a/src/EPPlus/Drawing/Renderer/Chart/ChartLegendRenderer.cs b/src/EPPlus/Drawing/Renderer/Chart/ChartLegendRenderer.cs index fd51c489a0..a0db0fd7bf 100644 --- a/src/EPPlus/Drawing/Renderer/Chart/ChartLegendRenderer.cs +++ b/src/EPPlus/Drawing/Renderer/Chart/ChartLegendRenderer.cs @@ -326,10 +326,10 @@ internal DrawingLegendSerie SetLegendSeries(double entryWidth, double entryHeigh if (Chart.Legend.Position == eLegendPosition.Top || Chart.Legend.Position == eLegendPosition.Bottom) { - //if (sls.Textbox.Bounds.Bottom > Rectangle.Bottom) - //{ - // break; - //} + if (sls.Textbox.Bounds.Bottom > Rectangle.Bottom) + { + break; + } } else { @@ -631,7 +631,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; From ade4d16dd1b4b65f7b5fa98d8068186f1c20b843 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ossian=20Edstr=C3=B6m?= Date: Tue, 16 Jun 2026 09:36:49 +0200 Subject: [PATCH 37/82] Partial fix for datalabels --- .../Chart/DataLabels/SvgDataLabelPoint.cs | 34 ++++++++++++------- 1 file changed, 21 insertions(+), 13 deletions(-) diff --git a/src/EPPlus/Drawing/Renderer/Chart/DataLabels/SvgDataLabelPoint.cs b/src/EPPlus/Drawing/Renderer/Chart/DataLabels/SvgDataLabelPoint.cs index f8b11e96f3..f93b3766db 100644 --- a/src/EPPlus/Drawing/Renderer/Chart/DataLabels/SvgDataLabelPoint.cs +++ b/src/EPPlus/Drawing/Renderer/Chart/DataLabels/SvgDataLabelPoint.cs @@ -143,15 +143,15 @@ internal void ImportDataLabel(ExcelChartStandardSerie serie, ExcelChartDataLabel _txtBox = txtBox; - _txtBox.Rectangle.SetDrawingPropertiesFill(ChartRenderer.Theme, dataLabel.Fill, null); - //if (dataLabel.Fill.IsEmpty == false) - //{ - // _txtBox.Rectangle.SetDrawingPropertiesFill(ChartRenderer.Theme, dataLabel.Fill, null); - //} - //else - //{ - // _txtBox.Rectangle.SetDrawingPropertiesFill(ChartRenderer.Theme, dataLabel.Fill, null); - //} + if (dataLabel.Fill.IsEmpty == false) + { + _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(); @@ -172,12 +172,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; + + _manualLayoutOffset = new Coordinate(rect.Left, rect.Top); - Rectangle.Bounds.Left += _manualLayoutOffset.X; - Rectangle.Bounds.Top += _manualLayoutOffset.Y; + 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) { From da782617fd4f255a5277850c5619649f02bff72e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ossian=20Edstr=C3=B6m?= Date: Tue, 16 Jun 2026 10:23:45 +0200 Subject: [PATCH 38/82] Fixed background for datalabels --- .../Chart/LineChartToSvgTests.cs | 18 ++++++++--------- .../Svg/SvgGroupRenderer.cs | 9 ++++++++- .../DataLabels/ChartSerieDataLabelRenderer.cs | 5 ++++- .../Chart/DataLabels/SvgDataLabelPoint.cs | 20 ++++++++++--------- 4 files changed, 32 insertions(+), 20 deletions(-) diff --git a/src/EPPlus.DrawingRenderer.Tests/Chart/LineChartToSvgTests.cs b/src/EPPlus.DrawingRenderer.Tests/Chart/LineChartToSvgTests.cs index 44f5074ad0..17159bda53 100644 --- a/src/EPPlus.DrawingRenderer.Tests/Chart/LineChartToSvgTests.cs +++ b/src/EPPlus.DrawingRenderer.Tests/Chart/LineChartToSvgTests.cs @@ -174,16 +174,16 @@ public void GenerateSvgForCharts_SecondaryAxis_sheet3() using (var p = OpenTemplatePackage("ChartForSvg_SecondaryAxis.xlsx")) { var ws = p.Workbook.Worksheets[2]; - var ix = 1; - var c = ws.Drawings[ix]; - var svg = c.ToSvg(); - SaveTextFileToWorkbook($"svg\\ChartForSvg_sheet3_{ix++}.svg", svg); //var ix = 1; - //foreach (ExcelChart c in ws.Drawings) - //{ - // var svg = c.ToSvg(); - // SaveTextFileToWorkbook($"svg\\ChartForSvg_SecAxis{ix++}.svg", svg); - //} + //var c = ws.Drawings[ix]; + //var svg = c.ToSvg(); + //SaveTextFileToWorkbook($"svg\\ChartForSvg_sheet3_{ix++}.svg", svg); + var ix = 1; + foreach (ExcelChart c in ws.Drawings) + { + var svg = c.ToSvg(); + SaveTextFileToWorkbook($"svg\\ChartForSvg_Sheet3_SecAxis{ix++}.svg", svg); + } } } diff --git a/src/EPPlus.DrawingRenderer/Svg/SvgGroupRenderer.cs b/src/EPPlus.DrawingRenderer/Svg/SvgGroupRenderer.cs index d2216dd0f3..2ac6ac7a2a 100644 --- a/src/EPPlus.DrawingRenderer/Svg/SvgGroupRenderer.cs +++ b/src/EPPlus.DrawingRenderer/Svg/SvgGroupRenderer.cs @@ -20,7 +20,14 @@ public override void Render(GroupRenderItem item) { string combinedTransform = GetCombinedTransformString(item); - OutputStream.Append($""); + //Neccesary as fallback for e.g. DataLabels + string fillPropery = ""; + if (string.IsNullOrEmpty(item.FillColor) == false) + { + fillPropery = $" fill=\"{item.FillColor}\" "; + } + + OutputStream.Append($""); foreach (var childItem in item.RenderItems) { diff --git a/src/EPPlus/Drawing/Renderer/Chart/DataLabels/ChartSerieDataLabelRenderer.cs b/src/EPPlus/Drawing/Renderer/Chart/DataLabels/ChartSerieDataLabelRenderer.cs index 7078647db1..b2ef35241e 100644 --- a/src/EPPlus/Drawing/Renderer/Chart/DataLabels/ChartSerieDataLabelRenderer.cs +++ b/src/EPPlus/Drawing/Renderer/Chart/DataLabels/ChartSerieDataLabelRenderer.cs @@ -35,7 +35,6 @@ public ChartSerieDataLabelRenderer(ChartRenderer chart, ExcelChartSerieDataLabel _defaultMargins = new BoundingBox(l, top, right, bottom); } - if (dlblSerie.DataLabels.Count == 0 && serie.NumberOfItems > 0) { for (int i = 0; i < serie.NumberOfItems; i++) @@ -142,6 +141,10 @@ public override void AppendRenderItems(List renderItems) if (_dlblSerie.Fill.IsEmpty == false) { + //Rectangle.SetDrawingPropertiesFill(ChartRenderer.Theme, _dlblSerie.Fill, null); + //plotAreaGroup.AddChildItem(Rectangle); + Rectangle.SetDrawingPropertiesFill(ChartRenderer.Theme, _dlblSerie.Fill, null); + plotAreaGroup.SetDrawingPropertiesFill(ChartRenderer.Theme, _dlblSerie.Fill, null); plotAreaGroup.GroupTransform += $" fill=\"{plotAreaGroup.FillColor}\" name=\"Plot area group\""; } diff --git a/src/EPPlus/Drawing/Renderer/Chart/DataLabels/SvgDataLabelPoint.cs b/src/EPPlus/Drawing/Renderer/Chart/DataLabels/SvgDataLabelPoint.cs index f93b3766db..ad69e6df3b 100644 --- a/src/EPPlus/Drawing/Renderer/Chart/DataLabels/SvgDataLabelPoint.cs +++ b/src/EPPlus/Drawing/Renderer/Chart/DataLabels/SvgDataLabelPoint.cs @@ -147,11 +147,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"; - } + //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(); @@ -476,16 +477,17 @@ public override void AppendRenderItems(List renderItems) parentPointGroup.Top = _parentPoint.Top; var titleItemOrigin = new TitleRenderItem("DataLabel originpoint"); - renderItems.Add(titleItemOrigin); + parentPointGroup.AddChildItem(titleItemOrigin); renderItems.Add(parentPointGroup); - var titleItem = new TitleRenderItem("DataLabel size adjustment"); - parentPointGroup.RenderItems.Add(titleItem); - var group = new GroupRenderItem(Rectangle.Bounds); group.Left = Rectangle.Bounds.Left; group.Top = Rectangle.Bounds.Top; + + var titleItem = new TitleRenderItem("DataLabel size adjustment"); + group.AddChildItem(titleItem); + parentPointGroup.RenderItems.Add(group); _txtBox.AppendRenderItems(group.RenderItems); From d54fccffeb92af0d1ee106457ea9fe3b412b5f38 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ossian=20Edstr=C3=B6m?= Date: Tue, 16 Jun 2026 13:57:55 +0200 Subject: [PATCH 39/82] Transferred piechart legend special case --- .../Chart/PieChartTests.cs | 140 +++++++++- .../Svg/SvgGroupRenderer.cs | 2 +- .../Renderer/Chart/ChartLegendRenderer.cs | 242 +++++++++++++----- .../ChartTypeDrawers/PieChartTypeDrawer.cs | 6 + .../Chart/DataLabels/SvgDataLabelPoint.cs | 120 ++++----- .../Utils/Rendering/DrawingExtensions.cs | 113 ++++++++ 6 files changed, 475 insertions(+), 148 deletions(-) diff --git a/src/EPPlus.DrawingRenderer.Tests/Chart/PieChartTests.cs b/src/EPPlus.DrawingRenderer.Tests/Chart/PieChartTests.cs index 9b73ea8922..6f3e13eb75 100644 --- a/src/EPPlus.DrawingRenderer.Tests/Chart/PieChartTests.cs +++ b/src/EPPlus.DrawingRenderer.Tests/Chart/PieChartTests.cs @@ -19,15 +19,18 @@ public void ReadAndGenerateExcelPieChartSvgs() { 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); - } - } + var manySlices = ws.Drawings["ManySlices"]; + var test = manySlices.ToSvg(); + SaveTextFileToWorkbook($"svg\\PieChartSvgALL\\s{0}_{ws.Name}_{manySlices.Name}.svg", test); + //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); + // } + //} } } @@ -75,11 +78,16 @@ public void Datalabels() ExcelPackage.License.SetNonCommercialOrganization("EPPlus Project"); using (var p = OpenTemplatePackage("PieChartDlblsOrig.xlsx")) { - var ws = p.Workbook.Worksheets[0]; + //var ws = p.Workbook.Worksheets[0]; + + //var drawing = ws.Drawings["InsideEnd"]; + + //var svg = drawing.ToSvg(); + //SaveTextFileToWorkbook($"svg\\PieChartDlbls\\s{0}_{ws.Name}_{drawing.Name}.svg", svg); for (int i = 0; i < p.Workbook.Worksheets.Count; i++) { - ws = p.Workbook.Worksheets[i]; + var ws = p.Workbook.Worksheets[i]; foreach (ExcelChart c in ws.Drawings) { var svg = c.ToSvg(); @@ -108,5 +116,115 @@ public void Datalabels2() } } } + + [TestMethod] + public void ReadLegendIssue() + { + ExcelPackage.License.SetNonCommercialOrganization("EPPlus Project"); + using (var p = OpenTemplatePackage("PieChartSvgLegendIssue.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\\PieChartSvgLegendIssue\\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 renderer = new EPPlusImageRenderer.ImageRenderer(); + + // var pChart = ws.Drawings[0].As.Chart.PieChart; + + // var ser = pChart.Series[0]; + + // var legend = pChart.Legend; + // var entry = pChart.Legend.Entries; + // var pHeader = ser.Header; + // var pHeaderAddress = ser.HeaderAddress; + // var headerString = ser.GetHeaderString(); + + + // var ix = 0; + // foreach (ExcelChart c in ws.Drawings) + // { + // var svg = renderer.RenderDrawingToSvg(c); + // 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]; + // var renderer = new EPPlusImageRenderer.ImageRenderer(); + + // for (int i = 0; i < p.Workbook.Worksheets.Count; i++) + // { + // ws = p.Workbook.Worksheets[i]; + // foreach (ExcelChart c in ws.Drawings) + // { + // var svg = renderer.RenderDrawingToSvg(c); + // SaveTextFileToWorkbook($"svg\\PieChartDlbls\\s{i}_{ws.Name}_{c.Name}.svg", svg); + // } + // } + // } + //} + + [TestMethod] + public void Datalabels22() + { + ExcelPackage.License.SetNonCommercialOrganization("EPPlus Project"); + using (var p = OpenTemplatePackage("PieChartDlblsInside.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\\PieChartDlbls22\\s{i}_{ws.Name}_{c.Name}.svg", svg); + } + } + } + } + + + [TestMethod] + public void Datalabels3() + { + ExcelPackage.License.SetNonCommercialOrganization("EPPlus Project"); + using (var p = OpenTemplatePackage("PieChartDlblsOutside.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\\PieChartDlbls3\\s{i}_{ws.Name}_{c.Name}.svg", svg); + } + } + } + } } } diff --git a/src/EPPlus.DrawingRenderer/Svg/SvgGroupRenderer.cs b/src/EPPlus.DrawingRenderer/Svg/SvgGroupRenderer.cs index 2ac6ac7a2a..c2a66a0f69 100644 --- a/src/EPPlus.DrawingRenderer/Svg/SvgGroupRenderer.cs +++ b/src/EPPlus.DrawingRenderer/Svg/SvgGroupRenderer.cs @@ -22,7 +22,7 @@ public override void Render(GroupRenderItem item) //Neccesary as fallback for e.g. DataLabels string fillPropery = ""; - if (string.IsNullOrEmpty(item.FillColor) == false) + if (string.IsNullOrEmpty(item.FillColor) == false && item.FillColor != "none") { fillPropery = $" fill=\"{item.FillColor}\" "; } diff --git a/src/EPPlus/Drawing/Renderer/Chart/ChartLegendRenderer.cs b/src/EPPlus/Drawing/Renderer/Chart/ChartLegendRenderer.cs index a0db0fd7bf..2ced5030fa 100644 --- a/src/EPPlus/Drawing/Renderer/Chart/ChartLegendRenderer.cs +++ b/src/EPPlus/Drawing/Renderer/Chart/ChartLegendRenderer.cs @@ -11,6 +11,7 @@ Date Author Change 27/11/2025 EPPlus Software AB EPPlus 9 *************************************************************************************************/ using EPPlus.DrawingRenderer.RenderItems; +using EPPlus.Export.Utils; using EPPlus.Fonts.OpenType; using EPPlus.Fonts.OpenType.Integration; using EPPlusImageRenderer.RenderItems; @@ -18,6 +19,7 @@ Date Author Change 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; @@ -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; @@ -255,12 +287,12 @@ private void GetSerieSize(ExcelChartLegend l, int index, string text, ref double } } - 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; @@ -326,10 +363,10 @@ internal DrawingLegendSerie SetLegendSeries(double entryWidth, double entryHeigh if (Chart.Legend.Position == eLegendPosition.Top || Chart.Legend.Position == eLegendPosition.Bottom) { - if (sls.Textbox.Bounds.Bottom > Rectangle.Bottom) - { - break; - } + //if (sls.Textbox.Bounds.Bottom > Rectangle.Bottom) + //{ + // break; + //} } else { @@ -338,20 +375,57 @@ internal DrawingLegendSerie SetLegendSeries(double entryWidth, double entryHeigh break; } } + //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(); + foreach (var dp in ps.DataPoints) + { + catValues.Add($"{dp.Index + 1}"); + } + } - var si = GetLineSeriesIcon(ct, ps, pSls, entryWidth, entryHeight); - sls.SeriesIcon = si; + double lastWidth = entryWidth; + double totalWidth = entryWidth; - 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 - si.Height) / 2; + double tbWidth; - //Cat values are the header text - //They create a rect marker for each slice + tbWidth = Rectangle.Bounds.Width; - //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); - //} + var tbHeight = tm.Height; + sls.Textbox = new DrawingTextBody(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(); - //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; - // } - //} + tbWidth = sls.Textbox.Width; + + lastWidth = tbWidth + si.Width; + + totalWidth += tbWidth + si.Width; + + 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; + } + 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) @@ -595,10 +676,41 @@ 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); diff --git a/src/EPPlus/Drawing/Renderer/Chart/ChartTypeDrawers/PieChartTypeDrawer.cs b/src/EPPlus/Drawing/Renderer/Chart/ChartTypeDrawers/PieChartTypeDrawer.cs index bca35923c5..7511ca5f3e 100644 --- a/src/EPPlus/Drawing/Renderer/Chart/ChartTypeDrawers/PieChartTypeDrawer.cs +++ b/src/EPPlus/Drawing/Renderer/Chart/ChartTypeDrawers/PieChartTypeDrawer.cs @@ -267,6 +267,12 @@ internal override void DrawSeries() public override void AppendRenderItems(List renderItems) { ChartRenderer.Plotarea.Group.AddChildItem(_groupItem); + //ChartRenderer.Plotarea.Group.AddChildItem(SeriesRenderItems[0]); + if(SeriesRenderItems != null && SeriesRenderItems.Count > 0) + { + ChartRenderer.RenderItems.Add(SeriesRenderItems[0]); + } + //SeriesRenderItems.ForEach(x => ChartRenderer.Plotarea.Group.AddChildItem(x)); //renderItems.AddRange(ChartAreaRenderItems); //SeriesRenderItems.ForEach(x => _groupItem.AddChildItem(x)); } diff --git a/src/EPPlus/Drawing/Renderer/Chart/DataLabels/SvgDataLabelPoint.cs b/src/EPPlus/Drawing/Renderer/Chart/DataLabels/SvgDataLabelPoint.cs index ad69e6df3b..d626579139 100644 --- a/src/EPPlus/Drawing/Renderer/Chart/DataLabels/SvgDataLabelPoint.cs +++ b/src/EPPlus/Drawing/Renderer/Chart/DataLabels/SvgDataLabelPoint.cs @@ -9,6 +9,7 @@ using OfficeOpenXml.Drawing; using OfficeOpenXml.Drawing.Chart; using OfficeOpenXml.Drawing.Renderer.TextBox; +using OfficeOpenXml.FormulaParsing.Excel.Functions.MathFunctions; using OfficeOpenXml.Utils.EnumUtils; using System; using System.Collections.Generic; @@ -121,6 +122,8 @@ internal void ImportDataLabel(ExcelChartStandardSerie serie, ExcelChartDataLabel txtBox.TextBody.Paragraphs.RemoveAt(0); } + txtBox.TextBody.RecalculateParagraphs(); + if(txtBox.LeftMargin == 0) { txtBox.LeftMargin = defaultMargins.Left; @@ -259,7 +262,7 @@ internal void SetParentPoint(BoundingBox parentPoint, BoundingBox parentShape, V Rectangle.Bounds.Parent = parentPoint; _parentPoint = parentPoint; _parentShapeBounds = parentShape; - + var dataLabelCenter = new Vector2(Rectangle.Bounds.Left, Rectangle.Bounds.Top); Vector2 startPointDirection = Vector2.Zero; @@ -269,34 +272,34 @@ internal void SetParentPoint(BoundingBox parentPoint, BoundingBox parentShape, V startPointDirection = startToEndDir / startToEndDir.Length; } - //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) - { + //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) + { case eLabelPosition.Center: if ((startToEndDir.X == 0 && startToEndDir.Y == 0) == false) { //Half and invert - dataLabelCenter = ((startToEndDir*0.5d) * -1d); + dataLabelCenter = ((startToEndDir * 0.5d) * -1d); } Rectangle.Bounds.Left += dataLabelCenter.X; Rectangle.Bounds.Top += dataLabelCenter.Y; @@ -315,57 +318,32 @@ internal void SetParentPoint(BoundingBox parentPoint, BoundingBox parentShape, V 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); - // } - //} + if (startPointDirection.X == 0 && startPointDirection.Y == 0) + { + throw new InvalidOperationException("eLabelPosition.InEnd MUST have a direction." + + "Cannot be within End if EndPoint is undefined."); + } + var insidePos = startToEndDir * 0.15 * -1; + Rectangle.Bounds.Left += insidePos.X; + Rectangle.Bounds.Top += insidePos.Y; break; case eLabelPosition.OutEnd: - //if (startPointDirection.X == 0 && startPointDirection.Y == 0) + 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 (startPointDirection.X == 0 && startPointDirection.Y == 0) + { + throw new InvalidOperationException("eLabelPosition.OutEnd MUST have a direction." + + "Cannot be within End if EndPoint is undefined."); + } + Rectangle.Bounds.Left += startToEndDir.X * 0.15; + Rectangle.Bounds.Top += startToEndDir.Y * 0.15; + //if (parentShape == null) //{ - // throw new InvalidOperationException("eLabelPosition.OutEnd MUST have a direction." + - // "Cannot be within End if EndPoint is undefined."); + // throw new InvalidOperationException("eLabelPosition.OutEnd MUST have a parentShape"); //} - ////if (parentShape == null) - ////{ - //// throw new InvalidOperationException("eLabelPosition.OutEnd MUST have a parentShape"); - ////} //if (startPointDirection.X != 0) //{ diff --git a/src/EPPlus/Utils/Rendering/DrawingExtensions.cs b/src/EPPlus/Utils/Rendering/DrawingExtensions.cs index 3da7ad5c4f..d9242c41cb 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 From f40e61e848fb9b50bf319af4c020efcc480816dd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jan=20K=C3=A4llman?= Date: Tue, 16 Jun 2026 14:54:17 +0200 Subject: [PATCH 40/82] Fixed label position on horizontal axis --- .../Chart/LineChartToSvgTests.cs | 24 ++++++++++++++++++- .../Renderer/Chart/ChartAxisRenderer.cs | 15 ++++++++++-- src/EPPlus/Drawing/Renderer/ChartRenderer.cs | 6 ++--- 3 files changed, 39 insertions(+), 6 deletions(-) diff --git a/src/EPPlus.DrawingRenderer.Tests/Chart/LineChartToSvgTests.cs b/src/EPPlus.DrawingRenderer.Tests/Chart/LineChartToSvgTests.cs index 17159bda53..02f48468df 100644 --- a/src/EPPlus.DrawingRenderer.Tests/Chart/LineChartToSvgTests.cs +++ b/src/EPPlus.DrawingRenderer.Tests/Chart/LineChartToSvgTests.cs @@ -24,7 +24,7 @@ public void GenerateSvgForLineCharts_sheet1() //var svg = c.ToSvg(); //SaveTextFileToWorkbook($"svg\\ChartForSvg_ind{ix++}.svg", svg); - for(int i = 5; i< ws.Drawings.Count; i++) + for(int i = 0; i< ws.Drawings.Count; i++) { var c = ws.Drawings[i]; var svg = c.ToSvg(); @@ -281,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/Drawing/Renderer/Chart/ChartAxisRenderer.cs b/src/EPPlus/Drawing/Renderer/Chart/ChartAxisRenderer.cs index a99a7c4495..178a00e8a5 100644 --- a/src/EPPlus/Drawing/Renderer/Chart/ChartAxisRenderer.cs +++ b/src/EPPlus/Drawing/Renderer/Chart/ChartAxisRenderer.cs @@ -492,7 +492,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) { @@ -529,6 +529,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 +563,7 @@ private double GetAxisItemLeft(int i, OfficeOpenXml.Interfaces.Drawing.Text.Text if (Axis.CrossingAxis == null || Axis.CrossingAxis.CrossBetween == eCrossBetween.Between) { majorWidth = Rectangle.Width / AxisValues.Count; - majorHalf = majorWidth / 2; + //majorHalf = majorWidth / 2; } else { diff --git a/src/EPPlus/Drawing/Renderer/ChartRenderer.cs b/src/EPPlus/Drawing/Renderer/ChartRenderer.cs index 2416c5ea73..02bcd06172 100644 --- a/src/EPPlus/Drawing/Renderer/ChartRenderer.cs +++ b/src/EPPlus/Drawing/Renderer/ChartRenderer.cs @@ -99,7 +99,7 @@ private void SetAxisPositionsFromPlotarea() if (HorizontalAxis != null && HorizontalAxis.Rectangle != null) { - PlaceHorizontalAxis(HorizontalAxis, false); + 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 (HorizontalAxis.Axis.TickLabelPosition == eTickLabelPosition.NextTo && VerticalAxis.Axis.AxisType == eAxisType.Val && VerticalAxis.Min < 0D) @@ -306,8 +306,8 @@ 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); + 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; From dd18c5a2574c36ca13766efc7f9f1b0b4b1276e3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jan=20K=C3=A4llman?= Date: Tue, 16 Jun 2026 15:28:04 +0200 Subject: [PATCH 41/82] Fixes axis label alignment and label positioning --- .../Chart/LineChartToSvgTests.cs | 20 +++++++++---------- .../Renderer/Chart/ChartAxisRenderer.cs | 10 +++++----- 2 files changed, 15 insertions(+), 15 deletions(-) diff --git a/src/EPPlus.DrawingRenderer.Tests/Chart/LineChartToSvgTests.cs b/src/EPPlus.DrawingRenderer.Tests/Chart/LineChartToSvgTests.cs index 02f48468df..b0b3600277 100644 --- a/src/EPPlus.DrawingRenderer.Tests/Chart/LineChartToSvgTests.cs +++ b/src/EPPlus.DrawingRenderer.Tests/Chart/LineChartToSvgTests.cs @@ -289,17 +289,17 @@ public void GenerateSvgForLineCharts_AxisAlign_sheet1() { var ws = p.Workbook.Worksheets[0]; - var ix = 3; - var c = ws.Drawings[ix]; - var svg = c.ToSvg(); - SaveTextFileToWorkbook($"svg\\HorizontalAxisChartForSvg{ix++}.svg", svg); + //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); - //} + 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/Drawing/Renderer/Chart/ChartAxisRenderer.cs b/src/EPPlus/Drawing/Renderer/Chart/ChartAxisRenderer.cs index 178a00e8a5..7fa2ce3995 100644 --- a/src/EPPlus/Drawing/Renderer/Chart/ChartAxisRenderer.cs +++ b/src/EPPlus/Drawing/Renderer/Chart/ChartAxisRenderer.cs @@ -501,12 +501,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; } } @@ -559,17 +560,16 @@ private double GetAxisItemLeft(int i, OfficeOpenXml.Interfaces.Drawing.Text.Text { if (IsCatAx()) { - double majorWidth, majorHalf=0D; + double majorWidth; if (Axis.CrossingAxis == null || Axis.CrossingAxis.CrossBetween == eCrossBetween.Between) { majorWidth = Rectangle.Width / AxisValues.Count; - //majorHalf = majorWidth / 2; } else { majorWidth = Rectangle.Width / (AxisValues.Count - 1); } - var majorTickStartingPosition = Rectangle.Left + majorWidth * i + majorHalf; + var majorTickStartingPosition = Rectangle.Left + majorWidth * i; return majorTickStartingPosition; } else From 1e0beae6aac91c49e5681135e940ea8256539874 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ossian=20Edstr=C3=B6m?= Date: Tue, 16 Jun 2026 15:40:16 +0200 Subject: [PATCH 42/82] Fixed centering of piechart legend --- .../Chart/PieChartTests.cs | 71 +++---------------- .../Renderer/Chart/ChartLegendRenderer.cs | 42 +++++++---- 2 files changed, 38 insertions(+), 75 deletions(-) diff --git a/src/EPPlus.DrawingRenderer.Tests/Chart/PieChartTests.cs b/src/EPPlus.DrawingRenderer.Tests/Chart/PieChartTests.cs index 6f3e13eb75..bfe3fd61c3 100644 --- a/src/EPPlus.DrawingRenderer.Tests/Chart/PieChartTests.cs +++ b/src/EPPlus.DrawingRenderer.Tests/Chart/PieChartTests.cs @@ -19,18 +19,15 @@ public void ReadAndGenerateExcelPieChartSvgs() { var ws = p.Workbook.Worksheets[0]; - var manySlices = ws.Drawings["ManySlices"]; - var test = manySlices.ToSvg(); - SaveTextFileToWorkbook($"svg\\PieChartSvgALL\\s{0}_{ws.Name}_{manySlices.Name}.svg", test); - //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); - // } - //} + 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); + } + } } } @@ -137,56 +134,6 @@ public void ReadLegendIssue() } } - //[TestMethod] - //public void BasicPieChart() - //{ - // ExcelPackage.License.SetNonCommercialOrganization("EPPlus Project"); - // using (var p = OpenTemplatePackage("BasicPieChart.xlsx")) - // { - // var ws = p.Workbook.Worksheets[0]; - // var renderer = new EPPlusImageRenderer.ImageRenderer(); - - // var pChart = ws.Drawings[0].As.Chart.PieChart; - - // var ser = pChart.Series[0]; - - // var legend = pChart.Legend; - // var entry = pChart.Legend.Entries; - // var pHeader = ser.Header; - // var pHeaderAddress = ser.HeaderAddress; - // var headerString = ser.GetHeaderString(); - - - // var ix = 0; - // foreach (ExcelChart c in ws.Drawings) - // { - // var svg = renderer.RenderDrawingToSvg(c); - // 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]; - // var renderer = new EPPlusImageRenderer.ImageRenderer(); - - // for (int i = 0; i < p.Workbook.Worksheets.Count; i++) - // { - // ws = p.Workbook.Worksheets[i]; - // foreach (ExcelChart c in ws.Drawings) - // { - // var svg = renderer.RenderDrawingToSvg(c); - // SaveTextFileToWorkbook($"svg\\PieChartDlbls\\s{i}_{ws.Name}_{c.Name}.svg", svg); - // } - // } - // } - //} - [TestMethod] public void Datalabels22() { diff --git a/src/EPPlus/Drawing/Renderer/Chart/ChartLegendRenderer.cs b/src/EPPlus/Drawing/Renderer/Chart/ChartLegendRenderer.cs index 2ced5030fa..2b7b265941 100644 --- a/src/EPPlus/Drawing/Renderer/Chart/ChartLegendRenderer.cs +++ b/src/EPPlus/Drawing/Renderer/Chart/ChartLegendRenderer.cs @@ -281,7 +281,7 @@ 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; } @@ -353,8 +353,8 @@ internal DrawingLegendSerie SetLegendSeries(double entryWidth, double entryHeigh if (ix == 0) { SetPieLegend(ct, index, pSls, pos, s, sls, entryWidth, entryHeight, maxIconLength); - //pSls = null; - //sls = null; + pSls = null; + sls = null; } break; default: @@ -375,11 +375,11 @@ internal DrawingLegendSerie SetLegendSeries(double entryWidth, double entryHeigh break; } } - //if (sls != null) - //{ - // SeriesIcon.Add(sls); - //} - SeriesIcon.Add(sls); + if (sls != null) + { + SeriesIcon.Add(sls); + } + //SeriesIcon.Add(sls); pSls = sls; //else //{ @@ -522,8 +522,8 @@ private void SetPieLegend(ExcelChart ct, int index, DrawingLegendSerie pSls, eLe } } - double lastWidth = entryWidth; - double totalWidth = entryWidth; + double lastWidth = 0; + double totalWidth = 0; for (int i = 0; i < catValues.Count; i++) { @@ -533,10 +533,18 @@ private void SetPieLegend(ExcelChart ct, int index, DrawingLegendSerie pSls, eLe var si = GetPieSeriesIcon(ct, ps, pSls, lastWidth, entryHeight, i); sls = new DrawingLegendSerie(); var tbLeft = si.Left + maxIconLength + MarginIconText; - var tbTop = si.Top - (entryHeight - si.Height) / 2; + var tbTop = si.Top - ((entryHeight) / 2); + double tbWidth; - tbWidth = Rectangle.Bounds.Width; + if(i != catValues.Count -1) + { + tbWidth = Rectangle.Bounds.Width - tbLeft; + } + else + { + tbWidth = Rectangle.Bounds.Width; + } var tbHeight = tm.Height; sls.Textbox = new DrawingTextBody(Chart, Rectangle.Bounds, tbLeft, tbTop, tbWidth, tbHeight, false, true); @@ -549,7 +557,7 @@ private void SetPieLegend(ExcelChart ct, int index, DrawingLegendSerie pSls, eLe lastWidth = tbWidth + si.Width; - totalWidth += tbWidth + si.Width; + totalWidth += tbWidth + si.Width + maxIconLength + MarginIconText; var dp = ps.DataPoints[i]; @@ -560,6 +568,14 @@ private void SetPieLegend(ExcelChart ct, int index, DrawingLegendSerie pSls, eLe 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; } From 8df3dc4bcad676b56f0909c6432664d1ab64957b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ossian=20Edstr=C3=B6m?= Date: Tue, 16 Jun 2026 17:26:59 +0200 Subject: [PATCH 43/82] Start of datalabels for barcharts --- .../Chart/BarChartTests.cs | 16 +++++ .../BarColumnChartTypeDrawer.cs | 63 +++++++++++++------ 2 files changed, 60 insertions(+), 19 deletions(-) diff --git a/src/EPPlus.DrawingRenderer.Tests/Chart/BarChartTests.cs b/src/EPPlus.DrawingRenderer.Tests/Chart/BarChartTests.cs index 6620be6406..c1cff2f41d 100644 --- a/src/EPPlus.DrawingRenderer.Tests/Chart/BarChartTests.cs +++ b/src/EPPlus.DrawingRenderer.Tests/Chart/BarChartTests.cs @@ -42,5 +42,21 @@ public void GenerateSvgForBarCharts2() } } } + + [TestMethod] + public void DatalabelBarCharts() + { + ExcelPackage.License.SetNonCommercialOrganization("EPPlus Project"); + using (var p = OpenTemplatePackage("BarChartForSvg.xlsx")) + { + var ws = p.Workbook.Worksheets[0]; + + var chartWithDataLabels = ws.Drawings["Chart 4"]; + + var svg = chartWithDataLabels.ToSvg(); + + SaveTextFileToWorkbook($"svg\\DataLableBarChart_sheet1_{chartWithDataLabels.Name}.svg", svg); + } + } } } diff --git a/src/EPPlus/Drawing/Renderer/Chart/ChartTypeDrawers/BarColumnChartTypeDrawer.cs b/src/EPPlus/Drawing/Renderer/Chart/ChartTypeDrawers/BarColumnChartTypeDrawer.cs index 8c7b661dcf..fbe28c93e8 100644 --- a/src/EPPlus/Drawing/Renderer/Chart/ChartTypeDrawers/BarColumnChartTypeDrawer.cs +++ b/src/EPPlus/Drawing/Renderer/Chart/ChartTypeDrawers/BarColumnChartTypeDrawer.cs @@ -1,18 +1,16 @@ 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.FormulaParsing.Excel.Functions.Text; 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 @@ -27,6 +25,8 @@ internal BarColumnChartTypeDrawer(ChartRenderer svgChart, ExcelBarChart chartTyp _catValues = new List>(); _valValues = new List>(); + int serCounter = 0; + foreach (ExcelBarChartSerie serie in chartType.Series) { List valValue,catValue; @@ -35,13 +35,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()) @@ -78,13 +71,42 @@ 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; + + 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++) + { + + var middleHeight = dataPoints[j].Top + (dataPoints[j].Height / 2); + var middleRight = dataPoints[j].Left + (dataPoints[j].Width / 2); + + var furthestPoint = new Vector2(dataPoints[j].Right, middleHeight); + var startingPoint = new Vector2(dataPoints[j].Left, middleHeight); + + //var vectorRight = furthestPoint - startingPoint; + var vectorRight = Vector2.One; + + BoundingBox startingPointBound = new BoundingBox(dataPoints[j].Width, dataPoints[j].Height); + startingPointBound.Parent = dataPoints[j]; + startingPointBound.Top = middleHeight; + startingPointBound.Left = dataPoints[j].Width; + //var furthestRight = dataPoints[j].Right - dataPoints[j].Left; + + //var endPoint = new Vector2(furthestRight, middleHeight); + //var startPoint = new Vector2(0, middleHeight); + //var vectorRight = endPoint - startPoint; + //dataPoints[j].Width; + //var vectorRight = new Vector2(dataPoints[j].Right - dataPoints[j].Left, middleHeight); + serieDataLabels[i].SetParentVector(startingPointBound, j, vectorRight); + } + + serCounter++; + } } foreach (var tr in Trendlines) @@ -258,6 +280,9 @@ private void AddBar(ExcelBarChart chartType, ExcelBarChartSerie serie, List Date: Wed, 17 Jun 2026 10:14:10 +0200 Subject: [PATCH 44/82] Fixed out-end for bar-charts --- .../BarColumnChartTypeDrawer.cs | 8 +- .../Chart/DataLabels/SvgDataLabelPoint.cs | 113 +++++++++--------- 2 files changed, 65 insertions(+), 56 deletions(-) diff --git a/src/EPPlus/Drawing/Renderer/Chart/ChartTypeDrawers/BarColumnChartTypeDrawer.cs b/src/EPPlus/Drawing/Renderer/Chart/ChartTypeDrawers/BarColumnChartTypeDrawer.cs index fbe28c93e8..13ae0a6af6 100644 --- a/src/EPPlus/Drawing/Renderer/Chart/ChartTypeDrawers/BarColumnChartTypeDrawer.cs +++ b/src/EPPlus/Drawing/Renderer/Chart/ChartTypeDrawers/BarColumnChartTypeDrawer.cs @@ -88,13 +88,16 @@ internal override void DrawSeries() var furthestPoint = new Vector2(dataPoints[j].Right, middleHeight); var startingPoint = new Vector2(dataPoints[j].Left, middleHeight); - //var vectorRight = furthestPoint - startingPoint; - var vectorRight = Vector2.One; + var vectorRight = furthestPoint - startingPoint; BoundingBox startingPointBound = new BoundingBox(dataPoints[j].Width, dataPoints[j].Height); startingPointBound.Parent = dataPoints[j]; startingPointBound.Top = middleHeight; startingPointBound.Left = dataPoints[j].Width; + + startingPointBound.Width = 0; + startingPointBound.Height = 0; + //var furthestRight = dataPoints[j].Right - dataPoints[j].Left; //var endPoint = new Vector2(furthestRight, middleHeight); @@ -102,6 +105,7 @@ internal override void DrawSeries() //var vectorRight = endPoint - startPoint; //dataPoints[j].Width; //var vectorRight = new Vector2(dataPoints[j].Right - dataPoints[j].Left, middleHeight); + //serieDataLabels[i].SetParentPoint(startingPointBound, j); serieDataLabels[i].SetParentVector(startingPointBound, j, vectorRight); } diff --git a/src/EPPlus/Drawing/Renderer/Chart/DataLabels/SvgDataLabelPoint.cs b/src/EPPlus/Drawing/Renderer/Chart/DataLabels/SvgDataLabelPoint.cs index d626579139..6da9469998 100644 --- a/src/EPPlus/Drawing/Renderer/Chart/DataLabels/SvgDataLabelPoint.cs +++ b/src/EPPlus/Drawing/Renderer/Chart/DataLabels/SvgDataLabelPoint.cs @@ -240,17 +240,17 @@ 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 + (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"); @@ -272,6 +272,8 @@ internal void SetParentPoint(BoundingBox parentPoint, BoundingBox parentShape, V startPointDirection = startToEndDir / startToEndDir.Length; } + //Rectangle.Bounds.Left += 20; + //if (_parentShapeBounds != null) //{ // //shapeCenter @@ -318,64 +320,67 @@ internal void SetParentPoint(BoundingBox parentPoint, BoundingBox parentShape, V 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."); - } - var insidePos = startToEndDir * 0.15 * -1; - Rectangle.Bounds.Left += insidePos.X; - Rectangle.Bounds.Top += insidePos.Y; + //if (startPointDirection.X == 0 && startPointDirection.Y == 0) + //{ + // throw new InvalidOperationException("eLabelPosition.InEnd MUST have a direction." + + // "Cannot be within End if EndPoint is undefined."); + //} + //var insidePos = startToEndDir * 0.15 * -1; + //Rectangle.Bounds.Left += insidePos.X; + //Rectangle.Bounds.Top += insidePos.Y; 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 (startPointDirection.X == 0 && startPointDirection.Y == 0) - { - throw new InvalidOperationException("eLabelPosition.OutEnd MUST have a direction." + - "Cannot be within End if EndPoint is undefined."); - } - Rectangle.Bounds.Left += startToEndDir.X * 0.15; - Rectangle.Bounds.Top += startToEndDir.Y * 0.15; - //if (parentShape == null) + //if (startPointDirection.X == 0 && startPointDirection.Y == 0) //{ - // throw new InvalidOperationException("eLabelPosition.OutEnd MUST have a parentShape"); + // throw new InvalidOperationException("eLabelPosition.OutEnd MUST have a direction." + + // "Cannot be within End if EndPoint is undefined."); //} - - //if (startPointDirection.X != 0) + //if (startPointDirection.X == 0 && startPointDirection.Y == 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); - // } + // throw new InvalidOperationException("eLabelPosition.OutEnd MUST have a direction." + + // "Cannot be within End if EndPoint is undefined."); //} - - //if (startPointDirection.Y != 0) + //Rectangle.Bounds.Left += startToEndDir.X * 0.15; + //Rectangle.Bounds.Top += startToEndDir.Y * 0.15; + //if (parentShape == null) //{ - // //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); - // } + // 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); + } + } + + //Rectangle.Top = 0; + //Rectangle.Height = 0; + + 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"); From 2b6ff324dc7f5b31b231b72118247edb64233dc4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ossian=20Edstr=C3=B6m?= Date: Wed, 17 Jun 2026 10:38:15 +0200 Subject: [PATCH 45/82] Functional datalabels --- .../Chart/BarChartTests.cs | 2 +- .../Chart/DataLabels/SvgDataLabelPoint.cs | 62 +++++++++++++++++-- 2 files changed, 59 insertions(+), 5 deletions(-) diff --git a/src/EPPlus.DrawingRenderer.Tests/Chart/BarChartTests.cs b/src/EPPlus.DrawingRenderer.Tests/Chart/BarChartTests.cs index c1cff2f41d..aced5fdf3f 100644 --- a/src/EPPlus.DrawingRenderer.Tests/Chart/BarChartTests.cs +++ b/src/EPPlus.DrawingRenderer.Tests/Chart/BarChartTests.cs @@ -47,7 +47,7 @@ public void GenerateSvgForBarCharts2() public void DatalabelBarCharts() { ExcelPackage.License.SetNonCommercialOrganization("EPPlus Project"); - using (var p = OpenTemplatePackage("BarChartForSvg.xlsx")) + using (var p = OpenTemplatePackage("BarChartForSvgDatalabels.xlsx")) { var ws = p.Workbook.Worksheets[0]; diff --git a/src/EPPlus/Drawing/Renderer/Chart/DataLabels/SvgDataLabelPoint.cs b/src/EPPlus/Drawing/Renderer/Chart/DataLabels/SvgDataLabelPoint.cs index 6da9469998..a8b79be284 100644 --- a/src/EPPlus/Drawing/Renderer/Chart/DataLabels/SvgDataLabelPoint.cs +++ b/src/EPPlus/Drawing/Renderer/Chart/DataLabels/SvgDataLabelPoint.cs @@ -240,7 +240,7 @@ private void SetPositionBasic(BoundingBox point, eLabelPosition basicPosition) // Bounds.Top = dataLabelCenter.Y; // break; case eLabelPosition.Left: - Rectangle.Bounds.Left -= _txtBox.Width + (point.Width / 2d); + Rectangle.Bounds.Left -= (_txtBox.Width/2) + (point.Width / 2d); break; case eLabelPosition.Right: case eLabelPosition.BestFit: @@ -319,7 +319,64 @@ internal void SetParentPoint(BoundingBox parentPoint, BoundingBox parentShape, V case eLabelPosition.Bottom: SetPositionBasic(parentPoint, _labelPosition); break; + case eLabelPosition.InBase: + var endToStartVector = startToEndDir * -1; + //Rectangle.Left *= endToStartVector.X; + //Rectangle.Top *= endToStartVector.Y; + if (startPointDirection.X != 0) + { + Rectangle.Left += endToStartVector.X; + //If basePoint is to the left + if (startPointDirection.X < 0) + { + //We must place to the left + SetPositionBasic(new BoundingBox(0,0), eLabelPosition.Left); + } + //if basePoint is to the right + else + { + //We must place to the right + SetPositionBasic(new BoundingBox(0, 0), eLabelPosition.Right); + } + } + + if (startPointDirection.Y != 0) + { + Rectangle.Top += endToStartVector.Y; + } + break; case eLabelPosition.InEnd: + if (startPointDirection.X != 0) + { + //If endPoint is to the left + if (startPointDirection.X < 0) + { + //We must place to the right + SetPositionBasic(parentPoint, eLabelPosition.Right); + } + //if endpoint is to the right + else + { + //We must place to the left + SetPositionBasic(parentPoint, eLabelPosition.Left); + } + } + + if (startPointDirection.Y != 0) + { + //If endpoint is on Top + if (startPointDirection.Y < 0) + { + //We must place on bottom + SetPositionBasic(parentPoint, eLabelPosition.Bottom); + } + //If endpoint is on bottom + else + { + //We must place on top + SetPositionBasic(parentPoint, eLabelPosition.Top); + } + } //if (startPointDirection.X == 0 && startPointDirection.Y == 0) //{ // throw new InvalidOperationException("eLabelPosition.InEnd MUST have a direction." + @@ -363,9 +420,6 @@ internal void SetParentPoint(BoundingBox parentPoint, BoundingBox parentShape, V } } - //Rectangle.Top = 0; - //Rectangle.Height = 0; - if (startPointDirection.Y != 0) { //If endpoint is on Top From a84b1d5c9806b687689e81b8daa1245142c212a0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ossian=20Edstr=C3=B6m?= Date: Wed, 17 Jun 2026 11:02:34 +0200 Subject: [PATCH 46/82] Columnn and bar charts correct --- .../Chart/BarChartTests.cs | 11 ++-- .../BarColumnChartTypeDrawer.cs | 52 ++++++++++++------- .../Chart/DataLabels/SvgDataLabelPoint.cs | 12 +++++ 3 files changed, 50 insertions(+), 25 deletions(-) diff --git a/src/EPPlus.DrawingRenderer.Tests/Chart/BarChartTests.cs b/src/EPPlus.DrawingRenderer.Tests/Chart/BarChartTests.cs index aced5fdf3f..2996fde16c 100644 --- a/src/EPPlus.DrawingRenderer.Tests/Chart/BarChartTests.cs +++ b/src/EPPlus.DrawingRenderer.Tests/Chart/BarChartTests.cs @@ -51,11 +51,12 @@ public void DatalabelBarCharts() { var ws = p.Workbook.Worksheets[0]; - var chartWithDataLabels = ws.Drawings["Chart 4"]; - - var svg = chartWithDataLabels.ToSvg(); - - SaveTextFileToWorkbook($"svg\\DataLableBarChart_sheet1_{chartWithDataLabels.Name}.svg", svg); + var ix = 0; + foreach (ExcelChart c in ws.Drawings) + { + var svg = c.ToSvg(); + SaveTextFileToWorkbook($"svg\\BarChartDataLabels{ix++}.svg", svg); + } } } } diff --git a/src/EPPlus/Drawing/Renderer/Chart/ChartTypeDrawers/BarColumnChartTypeDrawer.cs b/src/EPPlus/Drawing/Renderer/Chart/ChartTypeDrawers/BarColumnChartTypeDrawer.cs index 13ae0a6af6..d853be1a98 100644 --- a/src/EPPlus/Drawing/Renderer/Chart/ChartTypeDrawers/BarColumnChartTypeDrawer.cs +++ b/src/EPPlus/Drawing/Renderer/Chart/ChartTypeDrawers/BarColumnChartTypeDrawer.cs @@ -5,6 +5,7 @@ using EPPlusImageRenderer; using EPPlusImageRenderer.RenderItems; using EPPlusImageRenderer.Svg; +using OfficeOpenXml.DigitalSignatures; using OfficeOpenXml.Drawing.Chart; using OfficeOpenXml.FormulaParsing.Excel.Functions.Text; using OfficeOpenXml.Utils.TypeConversion; @@ -73,6 +74,8 @@ internal override void DrawSeries() int serCounter = 0; + var isColumn = ((ExcelBarChart)_chartType).IsTypeColumn(); + if (serie.HasDataLabel) { @@ -81,32 +84,41 @@ internal override void DrawSeries() for (int j = 0; j < dataPoints.Count; j++) { - - var middleHeight = dataPoints[j].Top + (dataPoints[j].Height / 2); - var middleRight = dataPoints[j].Left + (dataPoints[j].Width / 2); - var furthestPoint = new Vector2(dataPoints[j].Right, middleHeight); - var startingPoint = new Vector2(dataPoints[j].Left, middleHeight); + if (isColumn == false) + { + var middleHeight = dataPoints[j].Top + (dataPoints[j].Height / 2); + + var furthestPoint = new Vector2(dataPoints[j].Right, middleHeight); + var startingPoint = new Vector2(dataPoints[j].Left, middleHeight); + + var vectorRight = furthestPoint - startingPoint; + + BoundingBox startingPointBound = new BoundingBox(0, 0); + startingPointBound.Parent = dataPoints[j]; + startingPointBound.Top = middleHeight; + startingPointBound.Left = dataPoints[j].Width; - var vectorRight = furthestPoint - startingPoint; + serieDataLabels[i].SetParentVector(startingPointBound, j, vectorRight); + } + else + { + var middleRight = dataPoints[j].Left + (dataPoints[j].Width / 2); - BoundingBox startingPointBound = new BoundingBox(dataPoints[j].Width, dataPoints[j].Height); - startingPointBound.Parent = dataPoints[j]; - startingPointBound.Top = middleHeight; - startingPointBound.Left = dataPoints[j].Width; + var furthestPoint = new Vector2(middleRight, dataPoints[j].Top); + var startingPoint = new Vector2(middleRight, dataPoints[j].Bottom); - startingPointBound.Width = 0; - startingPointBound.Height = 0; + var vectorRight = furthestPoint - startingPoint; - //var furthestRight = dataPoints[j].Right - dataPoints[j].Left; + BoundingBox startingPointBound = new BoundingBox(0, 0); + startingPointBound.Parent = dataPoints[j]; + startingPointBound.Top = dataPoints[j].Top; + startingPointBound.Left = middleRight; - //var endPoint = new Vector2(furthestRight, middleHeight); - //var startPoint = new Vector2(0, middleHeight); - //var vectorRight = endPoint - startPoint; - //dataPoints[j].Width; - //var vectorRight = new Vector2(dataPoints[j].Right - dataPoints[j].Left, middleHeight); - //serieDataLabels[i].SetParentPoint(startingPointBound, j); - serieDataLabels[i].SetParentVector(startingPointBound, j, vectorRight); + serieDataLabels[i].SetParentVector(startingPointBound, j, vectorRight); + + //serieDataLabels[i].SetParentVector(startingPointBound, j, vectorRight); + } } serCounter++; diff --git a/src/EPPlus/Drawing/Renderer/Chart/DataLabels/SvgDataLabelPoint.cs b/src/EPPlus/Drawing/Renderer/Chart/DataLabels/SvgDataLabelPoint.cs index a8b79be284..c139168d56 100644 --- a/src/EPPlus/Drawing/Renderer/Chart/DataLabels/SvgDataLabelPoint.cs +++ b/src/EPPlus/Drawing/Renderer/Chart/DataLabels/SvgDataLabelPoint.cs @@ -343,6 +343,18 @@ internal void SetParentPoint(BoundingBox parentPoint, BoundingBox parentShape, V if (startPointDirection.Y != 0) { Rectangle.Top += endToStartVector.Y; + //If endpoint is on Top + if (startPointDirection.Y < 0) + { + //We must place on bottom + SetPositionBasic(new BoundingBox(0, 0), eLabelPosition.Top); + } + //If endpoint is on bottom + else + { + //We must place on top + SetPositionBasic(new BoundingBox(0, 0), eLabelPosition.Bottom); + } } break; case eLabelPosition.InEnd: From f1ab07ffdd6a81bfe80219ea16d3caf7a71dd9d4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ossian=20Edstr=C3=B6m?= Date: Wed, 17 Jun 2026 13:37:27 +0200 Subject: [PATCH 47/82] fixed stacked column pos datalabels --- .../Chart/BarChartTests.cs | 8 +++-- .../BarColumnChartTypeDrawer.cs | 31 +++++++++---------- .../DataLabels/ChartSerieDataLabelRenderer.cs | 5 +-- .../Chart/DataLabels/SvgDataLabelPoint.cs | 15 ++++++++- 4 files changed, 37 insertions(+), 22 deletions(-) diff --git a/src/EPPlus.DrawingRenderer.Tests/Chart/BarChartTests.cs b/src/EPPlus.DrawingRenderer.Tests/Chart/BarChartTests.cs index 2996fde16c..3b72bcb1dc 100644 --- a/src/EPPlus.DrawingRenderer.Tests/Chart/BarChartTests.cs +++ b/src/EPPlus.DrawingRenderer.Tests/Chart/BarChartTests.cs @@ -50,11 +50,13 @@ public void DatalabelBarCharts() using (var p = OpenTemplatePackage("BarChartForSvgDatalabels.xlsx")) { var ws = p.Workbook.Worksheets[0]; - + var drawings = ws.Drawings; var ix = 0; - foreach (ExcelChart c in ws.Drawings) + //var svg = drawings[ix].ToSvg(); + //SaveTextFileToWorkbook($"svg\\BarChartDataLabels{ix++}.svg", svg); + for (int i = ix; i < drawings.Count; i++) { - var svg = c.ToSvg(); + var svg = drawings[i].ToSvg(); SaveTextFileToWorkbook($"svg\\BarChartDataLabels{ix++}.svg", svg); } } diff --git a/src/EPPlus/Drawing/Renderer/Chart/ChartTypeDrawers/BarColumnChartTypeDrawer.cs b/src/EPPlus/Drawing/Renderer/Chart/ChartTypeDrawers/BarColumnChartTypeDrawer.cs index d853be1a98..6a4945e4bd 100644 --- a/src/EPPlus/Drawing/Renderer/Chart/ChartTypeDrawers/BarColumnChartTypeDrawer.cs +++ b/src/EPPlus/Drawing/Renderer/Chart/ChartTypeDrawers/BarColumnChartTypeDrawer.cs @@ -78,46 +78,45 @@ internal override void DrawSeries() 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++) { - if (isColumn == false) + if (isColumn == true) { - var middleHeight = dataPoints[j].Top + (dataPoints[j].Height / 2); + var middleRight = dataPoints[j].Left + (dataPoints[j].Width / 2); - var furthestPoint = new Vector2(dataPoints[j].Right, middleHeight); - var startingPoint = new Vector2(dataPoints[j].Left, middleHeight); + var furthestPoint = new Vector2(middleRight, dataPoints[j].Top); + var startingPoint = new Vector2(middleRight, dataPoints[j].Bottom); - var vectorRight = furthestPoint - startingPoint; + var vectorTop = furthestPoint - startingPoint; + + vectorTop = new Vector2(0, vectorTop.Y); BoundingBox startingPointBound = new BoundingBox(0, 0); startingPointBound.Parent = dataPoints[j]; - startingPointBound.Top = middleHeight; - startingPointBound.Left = dataPoints[j].Width; + startingPointBound.Top = dataPoints[j].Top; + startingPointBound.Left = middleRight; - serieDataLabels[i].SetParentVector(startingPointBound, j, vectorRight); + serieDataLabels[i].SetParentVector(startingPointBound, j, vectorTop); } else { - var middleRight = dataPoints[j].Left + (dataPoints[j].Width / 2); + var middleHeight = dataPoints[j].Top + (dataPoints[j].Height / 2); - var furthestPoint = new Vector2(middleRight, dataPoints[j].Top); - var startingPoint = new Vector2(middleRight, dataPoints[j].Bottom); + var furthestPoint = new Vector2(dataPoints[j].Right, middleHeight); + var startingPoint = new Vector2(dataPoints[j].Left, middleHeight); var vectorRight = furthestPoint - startingPoint; BoundingBox startingPointBound = new BoundingBox(0, 0); startingPointBound.Parent = dataPoints[j]; - startingPointBound.Top = dataPoints[j].Top; - startingPointBound.Left = middleRight; + startingPointBound.Top = middleHeight; + startingPointBound.Left = dataPoints[j].Width; serieDataLabels[i].SetParentVector(startingPointBound, j, vectorRight); - - //serieDataLabels[i].SetParentVector(startingPointBound, j, vectorRight); } } diff --git a/src/EPPlus/Drawing/Renderer/Chart/DataLabels/ChartSerieDataLabelRenderer.cs b/src/EPPlus/Drawing/Renderer/Chart/DataLabels/ChartSerieDataLabelRenderer.cs index b2ef35241e..7c1d2d3219 100644 --- a/src/EPPlus/Drawing/Renderer/Chart/DataLabels/ChartSerieDataLabelRenderer.cs +++ b/src/EPPlus/Drawing/Renderer/Chart/DataLabels/ChartSerieDataLabelRenderer.cs @@ -31,10 +31,11 @@ public ChartSerieDataLabelRenderer(ChartRenderer chart, ExcelChartSerieDataLabel 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) { for (int i = 0; i < serie.NumberOfItems; i++) diff --git a/src/EPPlus/Drawing/Renderer/Chart/DataLabels/SvgDataLabelPoint.cs b/src/EPPlus/Drawing/Renderer/Chart/DataLabels/SvgDataLabelPoint.cs index c139168d56..99f4171a06 100644 --- a/src/EPPlus/Drawing/Renderer/Chart/DataLabels/SvgDataLabelPoint.cs +++ b/src/EPPlus/Drawing/Renderer/Chart/DataLabels/SvgDataLabelPoint.cs @@ -112,7 +112,14 @@ internal void ImportDataLabel(ExcelChartStandardSerie serie, ExcelChartDataLabel if (txtBox.TextBody.Paragraphs.Count == 0) { - txtBox.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) @@ -303,6 +310,12 @@ internal void SetParentPoint(BoundingBox parentPoint, BoundingBox parentShape, V //Half and invert dataLabelCenter = ((startToEndDir * 0.5d) * -1d); } + else if (startToEndDir.Y != 0) + { + //Half and invert + dataLabelCenter = ((startToEndDir * 0.5d) * -1d); + } + Rectangle.Bounds.Left += dataLabelCenter.X; Rectangle.Bounds.Top += dataLabelCenter.Y; break; From c5769423e013e717a2e2dee2bed76b22b0219bd9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ossian=20Edstr=C3=B6m?= Date: Wed, 17 Jun 2026 16:09:26 +0200 Subject: [PATCH 48/82] Start of better datalabel system+debug --- .../Chart/BarChartTests.cs | 2 +- .../BarColumnChartTypeDrawer.cs | 19 ++- .../DataLabels/ChartSerieDataLabelRenderer.cs | 8 ++ .../Chart/DataLabels/SvgDataLabelPoint.cs | 122 +++++++++++++++++- 4 files changed, 144 insertions(+), 7 deletions(-) diff --git a/src/EPPlus.DrawingRenderer.Tests/Chart/BarChartTests.cs b/src/EPPlus.DrawingRenderer.Tests/Chart/BarChartTests.cs index 3b72bcb1dc..5503370393 100644 --- a/src/EPPlus.DrawingRenderer.Tests/Chart/BarChartTests.cs +++ b/src/EPPlus.DrawingRenderer.Tests/Chart/BarChartTests.cs @@ -51,7 +51,7 @@ public void DatalabelBarCharts() { var ws = p.Workbook.Worksheets[0]; var drawings = ws.Drawings; - var ix = 0; + var ix = 2; //var svg = drawings[ix].ToSvg(); //SaveTextFileToWorkbook($"svg\\BarChartDataLabels{ix++}.svg", svg); for (int i = ix; i < drawings.Count; i++) diff --git a/src/EPPlus/Drawing/Renderer/Chart/ChartTypeDrawers/BarColumnChartTypeDrawer.cs b/src/EPPlus/Drawing/Renderer/Chart/ChartTypeDrawers/BarColumnChartTypeDrawer.cs index 6a4945e4bd..59adfc5398 100644 --- a/src/EPPlus/Drawing/Renderer/Chart/ChartTypeDrawers/BarColumnChartTypeDrawer.cs +++ b/src/EPPlus/Drawing/Renderer/Chart/ChartTypeDrawers/BarColumnChartTypeDrawer.cs @@ -95,12 +95,21 @@ internal override void DrawSeries() vectorTop = new Vector2(0, vectorTop.Y); - BoundingBox startingPointBound = new BoundingBox(0, 0); - startingPointBound.Parent = dataPoints[j]; - startingPointBound.Top = dataPoints[j].Top; - startingPointBound.Left = middleRight; + Transform basePoint = new Transform(); + Transform endPoint = new Transform(); + + basePoint.Parent = dataPoints[j]; + endPoint.Parent = dataPoints[j]; + + endPoint.Position = new Vector2(middleRight, endPoint.Position.Y); + basePoint.Position = new Vector2(middleRight, endPoint.Position.Y + dataPoints[j].Height); + //BoundingBox startingPointBound = new BoundingBox(0, 0); + //startingPointBound.Parent = dataPoints[j]; + //startingPointBound.Top = dataPoints[j].Top; + //startingPointBound.Left = middleRight; - serieDataLabels[i].SetParentVector(startingPointBound, j, vectorTop); + serieDataLabels[i].SetDimensions(j, basePoint, endPoint); + //serieDataLabels[i].SetParentVector(startingPointBound, j, vectorTop); } else { diff --git a/src/EPPlus/Drawing/Renderer/Chart/DataLabels/ChartSerieDataLabelRenderer.cs b/src/EPPlus/Drawing/Renderer/Chart/DataLabels/ChartSerieDataLabelRenderer.cs index 7c1d2d3219..196d0465c0 100644 --- a/src/EPPlus/Drawing/Renderer/Chart/DataLabels/ChartSerieDataLabelRenderer.cs +++ b/src/EPPlus/Drawing/Renderer/Chart/DataLabels/ChartSerieDataLabelRenderer.cs @@ -118,6 +118,14 @@ internal void SetParentShape(BoundingBox parentBounds, BoundingBox shapeEndPoint SetParentPoint(shapeEndPoint, index); } + internal void SetDimensions(int index, Transform basePoint, Transform endPoint) + { + if (dataLabels.Count > index) + { + dataLabels[index].SetShapeDimensions(basePoint, endPoint); + } + } + internal void SetParentVector(BoundingBox parentPoint, int index, Vector2 startToEndDir) { _startToEndDir = startToEndDir; diff --git a/src/EPPlus/Drawing/Renderer/Chart/DataLabels/SvgDataLabelPoint.cs b/src/EPPlus/Drawing/Renderer/Chart/DataLabels/SvgDataLabelPoint.cs index 99f4171a06..9ac37e01ad 100644 --- a/src/EPPlus/Drawing/Renderer/Chart/DataLabels/SvgDataLabelPoint.cs +++ b/src/EPPlus/Drawing/Renderer/Chart/DataLabels/SvgDataLabelPoint.cs @@ -13,6 +13,7 @@ using OfficeOpenXml.Utils.EnumUtils; using System; using System.Collections.Generic; +using System.Net; namespace EPPlus.Export.ImageRenderer.RenderItems.SvgItem { @@ -264,13 +265,115 @@ private void SetPositionBasic(BoundingBox point, eLabelPosition basicPosition) } } + + RectRenderItem originPointRect; + RectRenderItem basePositionRect; + RectRenderItem endPositionRect; + RectRenderItem centerPositionRect; + + + private RectRenderItem GenerateDebugRenderItem(BoundingBox parent, string fillColor) + { + var pointRect = new RectRenderItem(parent); + pointRect.Width = 10d; + pointRect.Height = 10d; + pointRect.FillColor = fillColor; + pointRect.Left = -5d; + pointRect.Top = -5d; + return pointRect; + } + + + private void CreateDebugPoints(Transform basePoint, Transform endPoint) + { + 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"; + + } + + /// + /// + /// + internal void SetShapeDimensions(Transform basePoint, Transform endPoint) + { + if(basePoint.Parent != endPoint.Parent) + { + throw new InvalidOperationException("basePoint and endPoint have different parents. " + + "Please ensure that they share the same parent"); + } + + + //--- 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); + centerPositionRect = GenerateDebugRenderItem(_parentPoint, "Purple"); + centerPositionRect.Left += centerPoint.LocalPosition.X; + centerPositionRect.Top += centerPoint.LocalPosition.Y; + //--- + + switch (_labelPosition) + { + case eLabelPosition.Center: + Rectangle.Bounds.Left += centerPoint.LocalPosition.X; + Rectangle.Bounds.Top += centerPoint.LocalPosition.Y; + break; + case eLabelPosition.Left: + break; + case eLabelPosition.Right: + case eLabelPosition.BestFit: + break; + case eLabelPosition.Top: + break; + case eLabelPosition.Bottom: + break; + case eLabelPosition.InBase: + break; + case eLabelPosition.InEnd: + break; + case eLabelPosition.OutEnd: + + + break; + default: + throw new InvalidOperationException($"The datalabel position {_labelPosition} has not been implemented yet"); + } + } + internal void SetParentPoint(BoundingBox parentPoint, BoundingBox parentShape, Vector2 startToEndDir) { Rectangle.Bounds.Parent = parentPoint; _parentPoint = parentPoint; _parentShapeBounds = parentShape; - var dataLabelCenter = new Vector2(Rectangle.Bounds.Left, Rectangle.Bounds.Top); Vector2 startPointDirection = Vector2.Zero; @@ -541,6 +644,23 @@ public override void AppendRenderItems(List renderItems) var titleItemOrigin = new TitleRenderItem("DataLabel originpoint"); 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); From f8afb27fd85575d690fd1db66b3964df532dec54 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jan=20K=C3=A4llman?= Date: Wed, 17 Jun 2026 16:16:07 +0200 Subject: [PATCH 49/82] Started Work on error bars --- .../Constants/PatternArrays.cs | 1 - .../Drawing/Chart/ExcelChartNumericSource.cs | 59 +++++++- .../Renderer/Chart/ChartAxisRenderer.cs | 16 +- .../Renderer/Chart/ChartDrawingObject.cs | 23 +++ .../Renderer/Chart/ChartLegendRenderer.cs | 24 +-- .../Renderer/Chart/ChartPlotareaRenderer.cs | 4 +- .../Renderer/Chart/ChartTitleRenderer.cs | 13 +- .../BarColumnChartTypeDrawer.cs | 14 +- .../ChartTypeDrawers/ChartErrorBarRenderer.cs | 139 ++++++++++++++++++ .../Chart/ChartTypeDrawers/ChartTypeDrawer.cs | 19 +++ .../ChartTypeDrawers/LineChartTypeDrawer.cs | 14 +- .../Trendlines/ChartTrendlineRenderer.cs | 24 +-- .../DrawingRenderItemExtentions.cs | 14 +- .../Export/HtmlExport/RangeExporterTests.cs | 1 - 14 files changed, 292 insertions(+), 73 deletions(-) create mode 100644 src/EPPlus/Drawing/Renderer/Chart/ChartTypeDrawers/ChartErrorBarRenderer.cs 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/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/Renderer/Chart/ChartAxisRenderer.cs b/src/EPPlus/Drawing/Renderer/Chart/ChartAxisRenderer.cs index 7fa2ce3995..d665ab1fef 100644 --- a/src/EPPlus/Drawing/Renderer/Chart/ChartAxisRenderer.cs +++ b/src/EPPlus/Drawing/Renderer/Chart/ChartAxisRenderer.cs @@ -140,7 +140,7 @@ internal ChartAxisRenderer(ChartRenderer sc, ExcelChartAxisStandard ax) : base(s 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; @@ -311,7 +311,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)) @@ -462,7 +462,7 @@ private List GetAxisValueTextBoxes() 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) { @@ -734,7 +734,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; @@ -837,7 +837,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; @@ -875,13 +875,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; } diff --git a/src/EPPlus/Drawing/Renderer/Chart/ChartDrawingObject.cs b/src/EPPlus/Drawing/Renderer/Chart/ChartDrawingObject.cs index 6b38e74c7a..bf5cec5c4c 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 { @@ -88,5 +90,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 2ced5030fa..42a955ba6d 100644 --- a/src/EPPlus/Drawing/Renderer/Chart/ChartLegendRenderer.cs +++ b/src/EPPlus/Drawing/Renderer/Chart/ChartLegendRenderer.cs @@ -84,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); @@ -553,8 +553,8 @@ private void SetPieLegend(ExcelChart ct, int index, DrawingLegendSerie pSls, eLe 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.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); @@ -642,8 +642,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); @@ -660,8 +660,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); @@ -700,8 +700,8 @@ private RectRenderItem GetPieSeriesIcon(ExcelChart ct, ExcelPieChartSerie pcS, D 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); + 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; } @@ -731,8 +731,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; } diff --git a/src/EPPlus/Drawing/Renderer/Chart/ChartPlotareaRenderer.cs b/src/EPPlus/Drawing/Renderer/Chart/ChartPlotareaRenderer.cs index 22082fb41f..e1abb1c709 100644 --- a/src/EPPlus/Drawing/Renderer/Chart/ChartPlotareaRenderer.cs +++ b/src/EPPlus/Drawing/Renderer/Chart/ChartPlotareaRenderer.cs @@ -59,8 +59,8 @@ 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); + rect.SetDrawingPropertiesBorder(ChartRenderer.Theme, pa.Border, ChartRenderer.Chart.StyleManager.Style?.PlotArea.BorderReference.Color, pa.Border.Fill.Style != eFillStyle.NoFill, 0.75); Rectangle = rect; } diff --git a/src/EPPlus/Drawing/Renderer/Chart/ChartTitleRenderer.cs b/src/EPPlus/Drawing/Renderer/Chart/ChartTitleRenderer.cs index d71eb3c3cd..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) @@ -221,9 +221,12 @@ public DrawingTextBox TextBox 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 8c7b661dcf..2281f2558f 100644 --- a/src/EPPlus/Drawing/Renderer/Chart/ChartTypeDrawers/BarColumnChartTypeDrawer.cs +++ b/src/EPPlus/Drawing/Renderer/Chart/ChartTypeDrawers/BarColumnChartTypeDrawer.cs @@ -53,14 +53,8 @@ internal BarColumnChartTypeDrawer(ChartRenderer svgChart, ExcelBarChart chartTyp ExcelChartAxisStandard.CalculateStacked100(_valValues); } - //if(chartType.IsTypeBar()) - //{ - // CreateTrendlines(chartType, _valValues, _catValues); - //} - //else - //{ - CreateTrendlines(chartType, _catValues, _valValues); - //} + CreateTrendlines(chartType, _catValues, _valValues); + CreateErrorBars(chartType, _catValues, _valValues); } internal override void DrawSeries() @@ -255,8 +249,8 @@ 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 y = _ySerie[i]; + Values.Add(new double[] { y - sampleStdDev, y + sampleStdDev }); + } + else + { + var mult = errorbars.Value ?? 1D; + sampleStdDev*= mult; + Values.Add(new double[] { avg - sampleStdDev, 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 + (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 + 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 + plus }); + } + + break; + } + } + public double GetCustomValue(List l, int i) + { + if(l.Count==0) + { + return l[0]; + } + else if(i renderItems) + { + throw new NotImplementedException(); + } + + internal RenderItem GetErrorBarRenderItem(int index, ChartAxisRenderer xAxis, ChartAxisRenderer yAxis, double x, double y) + { + var path = new PathRenderItem(ChartRenderer.Bounds); + double addTop=0, addBottom=0; + if (_errorbars.BarType == eErrorBarType.Plus || _errorbars.BarType == eErrorBarType.Both) + { + addTop = Values[index][1]; + } + if(_errorbars.BarType == eErrorBarType.Minus || _errorbars.BarType == eErrorBarType.Both) + { + addBottom = Values[index][0]; + } + + if(_errorbars.Direction == eErrorBarDirection.X) + { + path.Commands.Add(new PathCommands(PathCommandType.Move, xAxis.GetPositionInPlotarea(x - addBottom), yAxis.GetPositionInPlotarea(y))); + path.Commands.Add(new PathCommands(PathCommandType.Line, xAxis.GetPositionInPlotarea(x + addTop), yAxis.GetPositionInPlotarea(y))); + } + else + { + path.Commands.Add(new PathCommands(PathCommandType.Move, xAxis.GetPositionInPlotarea(x), yAxis.GetPositionInPlotarea(y - addBottom))); + path.Commands.Add(new PathCommands(PathCommandType.Line, xAxis.GetPositionInPlotarea(x), yAxis.GetPositionInPlotarea(y + addTop))); + } + return path; + } + } +} diff --git a/src/EPPlus/Drawing/Renderer/Chart/ChartTypeDrawers/ChartTypeDrawer.cs b/src/EPPlus/Drawing/Renderer/Chart/ChartTypeDrawers/ChartTypeDrawer.cs index 4911da7fef..7c454c747b 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,23 @@ 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 89925263fe..d0f7a42771 100644 --- a/src/EPPlus/Drawing/Renderer/Chart/ChartTypeDrawers/LineChartTypeDrawer.cs +++ b/src/EPPlus/Drawing/Renderer/Chart/ChartTypeDrawers/LineChartTypeDrawer.cs @@ -49,6 +49,7 @@ internal LineChartTypeDrawer(ChartRenderer svgChart, ExcelLineChart chartType) : } CreateTrendlines(chartType, _xValues, _yValues); + CreateErrorBars(chartType, _xValues, _yValues); } internal override void DrawSeries() { @@ -135,7 +136,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; @@ -163,8 +167,11 @@ private void AddLine(ExcelChart chartType, ExcelLineChartSerie serie, List renderItems) { renderItems.AddRange(ChartAreaRenderItems); diff --git a/src/EPPlus/Drawing/Renderer/Chart/ChartTypeDrawers/Trendlines/ChartTrendlineRenderer.cs b/src/EPPlus/Drawing/Renderer/Chart/ChartTypeDrawers/Trendlines/ChartTrendlineRenderer.cs index 9ef938bbc5..e3f400a22a 100644 --- a/src/EPPlus/Drawing/Renderer/Chart/ChartTypeDrawers/Trendlines/ChartTrendlineRenderer.cs +++ b/src/EPPlus/Drawing/Renderer/Chart/ChartTypeDrawers/Trendlines/ChartTrendlineRenderer.cs @@ -94,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) @@ -685,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/RenderItems/DrawingRenderItemExtentions.cs b/src/EPPlus/Drawing/Renderer/RenderItems/DrawingRenderItemExtentions.cs index b99f7e277d..b03d3dcc4f 100644 --- a/src/EPPlus/Drawing/Renderer/RenderItems/DrawingRenderItemExtentions.cs +++ b/src/EPPlus/Drawing/Renderer/RenderItems/DrawingRenderItemExtentions.cs @@ -53,14 +53,14 @@ internal static void SetDrawingPropertiesFillBasic(this RenderItem item, ExcelTh switch (fill.Style) { case eFillStyle.NoFill: - if (fill.IsEmpty) - { - item.FillColor = GetFillColor(theme, fill, color, item.FillColorSource, out opacity); - } - else - { + //if (fill.IsEmpty) //Removed for now. + //{ + // item.FillColor = GetFillColor(theme, fill, color, item.FillColorSource, out opacity); + //} + //else + //{ item.FillColor = "none"; - } + //} break; case eFillStyle.SolidFill: item.FillColor = GetFillColor(theme, fill, color, item.FillColorSource, out opacity); 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] From 8d11438f7c2df4d90ae6ee2645d3dcadc088696c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ossian=20Edstr=C3=B6m?= Date: Wed, 17 Jun 2026 16:26:02 +0200 Subject: [PATCH 50/82] New position functional for center and in-base --- .../Chart/DataLabels/SvgDataLabelPoint.cs | 38 +++++++++++++++++++ 1 file changed, 38 insertions(+) diff --git a/src/EPPlus/Drawing/Renderer/Chart/DataLabels/SvgDataLabelPoint.cs b/src/EPPlus/Drawing/Renderer/Chart/DataLabels/SvgDataLabelPoint.cs index 9ac37e01ad..4e819ef42a 100644 --- a/src/EPPlus/Drawing/Renderer/Chart/DataLabels/SvgDataLabelPoint.cs +++ b/src/EPPlus/Drawing/Renderer/Chart/DataLabels/SvgDataLabelPoint.cs @@ -356,6 +356,44 @@ internal void SetShapeDimensions(Transform basePoint, Transform endPoint) case eLabelPosition.Bottom: break; case eLabelPosition.InBase: + Rectangle.Left += basePoint.LocalPosition.X; + //Translate to the base point + Rectangle.Left += endToBaseVector.X; + Rectangle.Top += endToBaseVector.Y; + + //Move the textbox margins inside on left + if (endToBaseVector.X != 0) + { + //If basePoint is to the left + if (endToBaseVector.X > 0) + { + //We must place to the left + SetPositionBasic(new BoundingBox(0, 0), eLabelPosition.Left); + } + //if basePoint is to the right + else + { + //We must place to the right + SetPositionBasic(new BoundingBox(0, 0), eLabelPosition.Right); + } + } + + //Move the textbox margins inside on top + if (endToBaseVector.Y != 0) + { + //If endpoint is on Top + if (endToBaseVector.Y > 0) + { + //We must place on bottom + SetPositionBasic(new BoundingBox(0, 0), eLabelPosition.Top); + } + //If endpoint is on bottom + else + { + //We must place on top + SetPositionBasic(new BoundingBox(0, 0), eLabelPosition.Bottom); + } + } break; case eLabelPosition.InEnd: break; From 48bac1d3d945e7ecdbba52424522cd4f60761a05 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ossian=20Edstr=C3=B6m?= Date: Wed, 17 Jun 2026 16:34:25 +0200 Subject: [PATCH 51/82] Made labels work for inEnd and inBase again --- .../Chart/DataLabels/SvgDataLabelPoint.cs | 49 ++++++++++++++++++- 1 file changed, 47 insertions(+), 2 deletions(-) diff --git a/src/EPPlus/Drawing/Renderer/Chart/DataLabels/SvgDataLabelPoint.cs b/src/EPPlus/Drawing/Renderer/Chart/DataLabels/SvgDataLabelPoint.cs index 4e819ef42a..9c66df0592 100644 --- a/src/EPPlus/Drawing/Renderer/Chart/DataLabels/SvgDataLabelPoint.cs +++ b/src/EPPlus/Drawing/Renderer/Chart/DataLabels/SvgDataLabelPoint.cs @@ -333,6 +333,17 @@ internal void SetShapeDimensions(Transform basePoint, Transform endPoint) //At this point our rectangle globally is centered on the top-left of the object. //And endVector is the top center position. + //Centering is handled by the enum options below but if they are the same no change is made + if(basePoint.LocalPosition.X == endPoint.LocalPosition.X) + { + Rectangle.Left += endPoint.LocalPosition.X; + } + //We should not have to add this as we start at top left anyway + //if (basePoint.LocalPosition.Y == endPoint.LocalPosition.Y) + //{ + // Rectangle.Left += endPoint.LocalPosition.Y; + //} + //--- Visualize positions for debugging purposes CreateDebugPoints(basePoint, endPoint); centerPositionRect = GenerateDebugRenderItem(_parentPoint, "Purple"); @@ -343,7 +354,7 @@ internal void SetShapeDimensions(Transform basePoint, Transform endPoint) switch (_labelPosition) { case eLabelPosition.Center: - Rectangle.Bounds.Left += centerPoint.LocalPosition.X; + //Rectangle.Bounds.Left += centerPoint.LocalPosition.X; Rectangle.Bounds.Top += centerPoint.LocalPosition.Y; break; case eLabelPosition.Left: @@ -356,7 +367,6 @@ internal void SetShapeDimensions(Transform basePoint, Transform endPoint) case eLabelPosition.Bottom: break; case eLabelPosition.InBase: - Rectangle.Left += basePoint.LocalPosition.X; //Translate to the base point Rectangle.Left += endToBaseVector.X; Rectangle.Top += endToBaseVector.Y; @@ -396,6 +406,41 @@ internal void SetShapeDimensions(Transform basePoint, Transform endPoint) } break; case eLabelPosition.InEnd: + //Our base assumption is that we start at the endpoint. No change neccesary + + //Move the textbox margins inside on left + if (endToBaseVector.X != 0) + { + //If basePoint is to the left + if (endToBaseVector.X < 0) + { + //We must place to the left + SetPositionBasic(new BoundingBox(0, 0), eLabelPosition.Left); + } + //if basePoint is to the right + else + { + //We must place to the right + SetPositionBasic(new BoundingBox(0, 0), eLabelPosition.Right); + } + } + + //Move the textbox margins inside on top + if (endToBaseVector.Y != 0) + { + //If endpoint is on Top + if (endToBaseVector.Y < 0) + { + //We must place on bottom + SetPositionBasic(new BoundingBox(0, 0), eLabelPosition.Top); + } + //If endpoint is on bottom + else + { + //We must place on top + SetPositionBasic(new BoundingBox(0, 0), eLabelPosition.Bottom); + } + } break; case eLabelPosition.OutEnd: From 62b16c954b5fc27ddc8f3647b8e25f4592be52ce Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ossian=20Edstr=C3=B6m?= Date: Wed, 17 Jun 2026 16:38:46 +0200 Subject: [PATCH 52/82] Added outEnd --- .../Chart/BarChartTests.cs | 2 +- .../Chart/DataLabels/SvgDataLabelPoint.cs | 30 +++++++++++++++++++ 2 files changed, 31 insertions(+), 1 deletion(-) diff --git a/src/EPPlus.DrawingRenderer.Tests/Chart/BarChartTests.cs b/src/EPPlus.DrawingRenderer.Tests/Chart/BarChartTests.cs index 5503370393..3b72bcb1dc 100644 --- a/src/EPPlus.DrawingRenderer.Tests/Chart/BarChartTests.cs +++ b/src/EPPlus.DrawingRenderer.Tests/Chart/BarChartTests.cs @@ -51,7 +51,7 @@ public void DatalabelBarCharts() { var ws = p.Workbook.Worksheets[0]; var drawings = ws.Drawings; - var ix = 2; + var ix = 0; //var svg = drawings[ix].ToSvg(); //SaveTextFileToWorkbook($"svg\\BarChartDataLabels{ix++}.svg", svg); for (int i = ix; i < drawings.Count; i++) diff --git a/src/EPPlus/Drawing/Renderer/Chart/DataLabels/SvgDataLabelPoint.cs b/src/EPPlus/Drawing/Renderer/Chart/DataLabels/SvgDataLabelPoint.cs index 9c66df0592..8dedb7b06a 100644 --- a/src/EPPlus/Drawing/Renderer/Chart/DataLabels/SvgDataLabelPoint.cs +++ b/src/EPPlus/Drawing/Renderer/Chart/DataLabels/SvgDataLabelPoint.cs @@ -443,7 +443,37 @@ internal void SetShapeDimensions(Transform basePoint, Transform endPoint) } break; case eLabelPosition.OutEnd: + if (endToBaseVector.X != 0) + { + //If endPoint is to the left + if (endToBaseVector.X > 0) + { + //We must place to the left + SetPositionBasic(new BoundingBox(0, 0), eLabelPosition.Left); + } + //if endpoint is to the right + else + { + //We must place to the right + SetPositionBasic(new BoundingBox(0, 0), eLabelPosition.Right); + } + } + if (endToBaseVector.Y != 0) + { + //If endpoint is on Top + if (endToBaseVector.Y > 0) + { + //We must place on Top + SetPositionBasic(new BoundingBox(0, 0), eLabelPosition.Top); + } + //If endpoint is on bottom + else + { + //We must place on Bottom + SetPositionBasic(new BoundingBox(0, 0), eLabelPosition.Bottom); + } + } break; default: From 33374869d7c1c7af9ac541e4b1625d8b82c2bed1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ossian=20Edstr=C3=B6m?= Date: Wed, 17 Jun 2026 16:45:19 +0200 Subject: [PATCH 53/82] Simplified initilization in barChart --- .../BarColumnChartTypeDrawer.cs | 23 ++++--------------- 1 file changed, 5 insertions(+), 18 deletions(-) diff --git a/src/EPPlus/Drawing/Renderer/Chart/ChartTypeDrawers/BarColumnChartTypeDrawer.cs b/src/EPPlus/Drawing/Renderer/Chart/ChartTypeDrawers/BarColumnChartTypeDrawer.cs index 5d247675cd..286fab7936 100644 --- a/src/EPPlus/Drawing/Renderer/Chart/ChartTypeDrawers/BarColumnChartTypeDrawer.cs +++ b/src/EPPlus/Drawing/Renderer/Chart/ChartTypeDrawers/BarColumnChartTypeDrawer.cs @@ -77,33 +77,20 @@ internal override void DrawSeries() 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); - var furthestPoint = new Vector2(middleRight, dataPoints[j].Top); - var startingPoint = new Vector2(middleRight, dataPoints[j].Bottom); - - var vectorTop = furthestPoint - startingPoint; - - vectorTop = new Vector2(0, vectorTop.Y); - - Transform basePoint = new Transform(); - Transform endPoint = new Transform(); - - basePoint.Parent = dataPoints[j]; - endPoint.Parent = dataPoints[j]; - endPoint.Position = new Vector2(middleRight, endPoint.Position.Y); basePoint.Position = new Vector2(middleRight, endPoint.Position.Y + dataPoints[j].Height); - //BoundingBox startingPointBound = new BoundingBox(0, 0); - //startingPointBound.Parent = dataPoints[j]; - //startingPointBound.Top = dataPoints[j].Top; - //startingPointBound.Left = middleRight; serieDataLabels[i].SetDimensions(j, basePoint, endPoint); - //serieDataLabels[i].SetParentVector(startingPointBound, j, vectorTop); } else { From 7b47db43f2c3533f9aadab2d13b8681d7094c525 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ossian=20Edstr=C3=B6m?= Date: Wed, 17 Jun 2026 17:15:58 +0200 Subject: [PATCH 54/82] Made new system functional for bars AND columns --- .../BarColumnChartTypeDrawer.cs | 15 ++++--------- .../Chart/DataLabels/SvgDataLabelPoint.cs | 21 ++++++------------- 2 files changed, 10 insertions(+), 26 deletions(-) diff --git a/src/EPPlus/Drawing/Renderer/Chart/ChartTypeDrawers/BarColumnChartTypeDrawer.cs b/src/EPPlus/Drawing/Renderer/Chart/ChartTypeDrawers/BarColumnChartTypeDrawer.cs index 286fab7936..00f69d8c21 100644 --- a/src/EPPlus/Drawing/Renderer/Chart/ChartTypeDrawers/BarColumnChartTypeDrawer.cs +++ b/src/EPPlus/Drawing/Renderer/Chart/ChartTypeDrawers/BarColumnChartTypeDrawer.cs @@ -95,18 +95,11 @@ internal override void DrawSeries() else { var middleHeight = dataPoints[j].Top + (dataPoints[j].Height / 2); + + basePoint.Position = new Vector2(basePoint.Position.X, middleHeight); + endPoint.Position = new Vector2(basePoint.Position.X + dataPoints[j].Width, middleHeight); - var furthestPoint = new Vector2(dataPoints[j].Right, middleHeight); - var startingPoint = new Vector2(dataPoints[j].Left, middleHeight); - - var vectorRight = furthestPoint - startingPoint; - - BoundingBox startingPointBound = new BoundingBox(0, 0); - startingPointBound.Parent = dataPoints[j]; - startingPointBound.Top = middleHeight; - startingPointBound.Left = dataPoints[j].Width; - - serieDataLabels[i].SetParentVector(startingPointBound, j, vectorRight); + serieDataLabels[i].SetDimensions(j, basePoint, endPoint); } } diff --git a/src/EPPlus/Drawing/Renderer/Chart/DataLabels/SvgDataLabelPoint.cs b/src/EPPlus/Drawing/Renderer/Chart/DataLabels/SvgDataLabelPoint.cs index 8dedb7b06a..52b6e79476 100644 --- a/src/EPPlus/Drawing/Renderer/Chart/DataLabels/SvgDataLabelPoint.cs +++ b/src/EPPlus/Drawing/Renderer/Chart/DataLabels/SvgDataLabelPoint.cs @@ -333,17 +333,6 @@ internal void SetShapeDimensions(Transform basePoint, Transform endPoint) //At this point our rectangle globally is centered on the top-left of the object. //And endVector is the top center position. - //Centering is handled by the enum options below but if they are the same no change is made - if(basePoint.LocalPosition.X == endPoint.LocalPosition.X) - { - Rectangle.Left += endPoint.LocalPosition.X; - } - //We should not have to add this as we start at top left anyway - //if (basePoint.LocalPosition.Y == endPoint.LocalPosition.Y) - //{ - // Rectangle.Left += endPoint.LocalPosition.Y; - //} - //--- Visualize positions for debugging purposes CreateDebugPoints(basePoint, endPoint); centerPositionRect = GenerateDebugRenderItem(_parentPoint, "Purple"); @@ -354,7 +343,7 @@ internal void SetShapeDimensions(Transform basePoint, Transform endPoint) switch (_labelPosition) { case eLabelPosition.Center: - //Rectangle.Bounds.Left += centerPoint.LocalPosition.X; + Rectangle.Bounds.Left += centerPoint.LocalPosition.X; Rectangle.Bounds.Top += centerPoint.LocalPosition.Y; break; case eLabelPosition.Left: @@ -368,8 +357,7 @@ internal void SetShapeDimensions(Transform basePoint, Transform endPoint) break; case eLabelPosition.InBase: //Translate to the base point - Rectangle.Left += endToBaseVector.X; - Rectangle.Top += endToBaseVector.Y; + Rectangle.Bounds.Position += basePoint.LocalPosition; //Move the textbox margins inside on left if (endToBaseVector.X != 0) @@ -406,7 +394,8 @@ internal void SetShapeDimensions(Transform basePoint, Transform endPoint) } break; case eLabelPosition.InEnd: - //Our base assumption is that we start at the endpoint. No change neccesary + //Move to end point + Rectangle.Bounds.Position += endPoint.LocalPosition; //Move the textbox margins inside on left if (endToBaseVector.X != 0) @@ -443,6 +432,8 @@ internal void SetShapeDimensions(Transform basePoint, Transform endPoint) } break; case eLabelPosition.OutEnd: + //Move to end point + Rectangle.Bounds.Position += endPoint.LocalPosition; if (endToBaseVector.X != 0) { //If endPoint is to the left From 6bf43e0942c96731f26d5c8830e9f10ec595dca0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ossian=20Edstr=C3=B6m?= Date: Thu, 18 Jun 2026 10:13:06 +0200 Subject: [PATCH 55/82] Ensured mix of series and custom datalabels get read --- .../Chart/BarChartTests.cs | 22 ++++++++++++++++- .../Chart/Axis/CategoryAxisScaleCalculator.cs | 1 + .../DataLabels/ChartSerieDataLabelRenderer.cs | 24 +++++++++++++++---- 3 files changed, 41 insertions(+), 6 deletions(-) diff --git a/src/EPPlus.DrawingRenderer.Tests/Chart/BarChartTests.cs b/src/EPPlus.DrawingRenderer.Tests/Chart/BarChartTests.cs index 3b72bcb1dc..d6f41ec689 100644 --- a/src/EPPlus.DrawingRenderer.Tests/Chart/BarChartTests.cs +++ b/src/EPPlus.DrawingRenderer.Tests/Chart/BarChartTests.cs @@ -47,7 +47,7 @@ public void GenerateSvgForBarCharts2() public void DatalabelBarCharts() { ExcelPackage.License.SetNonCommercialOrganization("EPPlus Project"); - using (var p = OpenTemplatePackage("BarChartForSvgDatalabels.xlsx")) + using (var p = OpenTemplatePackage("BarChartForSvgDatalabels2.xlsx")) { var ws = p.Workbook.Worksheets[0]; var drawings = ws.Drawings; @@ -61,5 +61,25 @@ public void DatalabelBarCharts() } } } + + + [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; + //var svg = drawings[ix].ToSvg(); + //SaveTextFileToWorkbook($"svg\\BarChartDataLabels{ix++}.svg", svg); + for (int i = ix; i < drawings.Count; i++) + { + var svg = drawings[i].ToSvg(); + SaveTextFileToWorkbook($"svg\\NegativeLabels{ix++}.svg", svg); + } + } + } } } 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/DataLabels/ChartSerieDataLabelRenderer.cs b/src/EPPlus/Drawing/Renderer/Chart/DataLabels/ChartSerieDataLabelRenderer.cs index 196d0465c0..46c26ee6e7 100644 --- a/src/EPPlus/Drawing/Renderer/Chart/DataLabels/ChartSerieDataLabelRenderer.cs +++ b/src/EPPlus/Drawing/Renderer/Chart/DataLabels/ChartSerieDataLabelRenderer.cs @@ -47,16 +47,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); } - //} + } } } From d25f9db99870327b2c080a98850c8e37e7ae9164 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ossian=20Edstr=C3=B6m?= Date: Thu, 18 Jun 2026 10:36:39 +0200 Subject: [PATCH 56/82] Added support for negative columns + margin --- .../BarColumnChartTypeDrawer.cs | 35 ++++++++++++------- .../Chart/DataLabels/SvgDataLabelPoint.cs | 23 ++++++------ 2 files changed, 33 insertions(+), 25 deletions(-) diff --git a/src/EPPlus/Drawing/Renderer/Chart/ChartTypeDrawers/BarColumnChartTypeDrawer.cs b/src/EPPlus/Drawing/Renderer/Chart/ChartTypeDrawers/BarColumnChartTypeDrawer.cs index 00f69d8c21..2953ea65ca 100644 --- a/src/EPPlus/Drawing/Renderer/Chart/ChartTypeDrawers/BarColumnChartTypeDrawer.cs +++ b/src/EPPlus/Drawing/Renderer/Chart/ChartTypeDrawers/BarColumnChartTypeDrawer.cs @@ -86,16 +86,24 @@ internal override void DrawSeries() if (isColumn == true) { var middleRight = dataPoints[j].Left + (dataPoints[j].Width / 2); + basePoint.Position = new Vector2(middleRight, chartBaseY); - endPoint.Position = new Vector2(middleRight, endPoint.Position.Y); - basePoint.Position = new Vector2(middleRight, endPoint.Position.Y + dataPoints[j].Height); + if (chartBaseY > dataPoints[j].Top) + { + endPoint.Position = new Vector2(middleRight, chartBaseY - dataPoints[j].Height); + } + else + { + endPoint.Position = new Vector2(middleRight, chartBaseY + dataPoints[j].Height); + } serieDataLabels[i].SetDimensions(j, basePoint, endPoint); } else { var middleHeight = dataPoints[j].Top + (dataPoints[j].Height / 2); - + + basePoint.Position = new Vector2(basePoint.Position.X, middleHeight); endPoint.Position = new Vector2(basePoint.Position.X + dataPoints[j].Width, middleHeight); @@ -128,6 +136,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); @@ -160,18 +170,17 @@ private void AddBar(ExcelBarChart chartType, ExcelBarChartSerie serie, List @@ -334,10 +336,7 @@ internal void SetShapeDimensions(Transform basePoint, Transform endPoint) //And endVector is the top center position. //--- Visualize positions for debugging purposes - CreateDebugPoints(basePoint, endPoint); - centerPositionRect = GenerateDebugRenderItem(_parentPoint, "Purple"); - centerPositionRect.Left += centerPoint.LocalPosition.X; - centerPositionRect.Top += centerPoint.LocalPosition.Y; + //CreateDebugPoints(basePoint, endPoint, centerPoint); //--- switch (_labelPosition) @@ -382,14 +381,14 @@ internal void SetShapeDimensions(Transform basePoint, Transform endPoint) //If endpoint is on Top if (endToBaseVector.Y > 0) { - //We must place on bottom - SetPositionBasic(new BoundingBox(0, 0), eLabelPosition.Top); + //We must place on bottom and apply margin to height + SetPositionBasic(new BoundingBox(0, 0) { Height = 5d }, eLabelPosition.Top); } //If endpoint is on bottom else { - //We must place on top - SetPositionBasic(new BoundingBox(0, 0), eLabelPosition.Bottom); + //We must place on top and apply margin to height + SetPositionBasic(new BoundingBox(0, 0) { Height = 5d }, eLabelPosition.Bottom); } } break; @@ -421,13 +420,13 @@ internal void SetShapeDimensions(Transform basePoint, Transform endPoint) if (endToBaseVector.Y < 0) { //We must place on bottom - SetPositionBasic(new BoundingBox(0, 0), eLabelPosition.Top); + SetPositionBasic(new BoundingBox(0, 0) { Height = 5d }, eLabelPosition.Top); } //If endpoint is on bottom else { //We must place on top - SetPositionBasic(new BoundingBox(0, 0), eLabelPosition.Bottom); + SetPositionBasic(new BoundingBox(0, 0) { Height = 5d }, eLabelPosition.Bottom); } } break; From 6b38834a2a25e60e502054466a511a64068fb9b7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ossian=20Edstr=C3=B6m?= Date: Thu, 18 Jun 2026 13:02:48 +0200 Subject: [PATCH 57/82] Ensured works for bar charts as well --- .../Chart/BarChartTests.cs | 2 +- .../ChartTypeDrawers/BarColumnChartTypeDrawer.cs | 14 ++++++++++---- .../Chart/DataLabels/SvgDataLabelPoint.cs | 16 ++++++++-------- 3 files changed, 19 insertions(+), 13 deletions(-) diff --git a/src/EPPlus.DrawingRenderer.Tests/Chart/BarChartTests.cs b/src/EPPlus.DrawingRenderer.Tests/Chart/BarChartTests.cs index d6f41ec689..f09e6c0318 100644 --- a/src/EPPlus.DrawingRenderer.Tests/Chart/BarChartTests.cs +++ b/src/EPPlus.DrawingRenderer.Tests/Chart/BarChartTests.cs @@ -71,7 +71,7 @@ public void NegativeDatalabelBarCharts() { var ws = p.Workbook.Worksheets[0]; var drawings = ws.Drawings; - var ix = 0; + var ix = 1; //var svg = drawings[ix].ToSvg(); //SaveTextFileToWorkbook($"svg\\BarChartDataLabels{ix++}.svg", svg); for (int i = ix; i < drawings.Count; i++) diff --git a/src/EPPlus/Drawing/Renderer/Chart/ChartTypeDrawers/BarColumnChartTypeDrawer.cs b/src/EPPlus/Drawing/Renderer/Chart/ChartTypeDrawers/BarColumnChartTypeDrawer.cs index 2953ea65ca..93ef8c2017 100644 --- a/src/EPPlus/Drawing/Renderer/Chart/ChartTypeDrawers/BarColumnChartTypeDrawer.cs +++ b/src/EPPlus/Drawing/Renderer/Chart/ChartTypeDrawers/BarColumnChartTypeDrawer.cs @@ -102,10 +102,16 @@ internal override void DrawSeries() else { var middleHeight = dataPoints[j].Top + (dataPoints[j].Height / 2); - - - basePoint.Position = new Vector2(basePoint.Position.X, middleHeight); - endPoint.Position = new Vector2(basePoint.Position.X + dataPoints[j].Width, middleHeight); + 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); + } + //endPoint.Position = new Vector2(basePoint.Position.X + dataPoints[j].Width, middleHeight); serieDataLabels[i].SetDimensions(j, basePoint, endPoint); } diff --git a/src/EPPlus/Drawing/Renderer/Chart/DataLabels/SvgDataLabelPoint.cs b/src/EPPlus/Drawing/Renderer/Chart/DataLabels/SvgDataLabelPoint.cs index 6c83d3a61a..1493c0882e 100644 --- a/src/EPPlus/Drawing/Renderer/Chart/DataLabels/SvgDataLabelPoint.cs +++ b/src/EPPlus/Drawing/Renderer/Chart/DataLabels/SvgDataLabelPoint.cs @@ -365,13 +365,13 @@ internal void SetShapeDimensions(Transform basePoint, Transform endPoint) if (endToBaseVector.X > 0) { //We must place to the left - SetPositionBasic(new BoundingBox(0, 0), eLabelPosition.Left); + SetPositionBasic(new BoundingBox(0, 0) { Width = 5d }, eLabelPosition.Left); } //if basePoint is to the right else { //We must place to the right - SetPositionBasic(new BoundingBox(0, 0), eLabelPosition.Right); + SetPositionBasic(new BoundingBox(0, 0) { Width = 5d }, eLabelPosition.Right); } } @@ -403,13 +403,13 @@ internal void SetShapeDimensions(Transform basePoint, Transform endPoint) if (endToBaseVector.X < 0) { //We must place to the left - SetPositionBasic(new BoundingBox(0, 0), eLabelPosition.Left); + SetPositionBasic(new BoundingBox(0, 0) { Width = 5d }, eLabelPosition.Left); } //if basePoint is to the right else { //We must place to the right - SetPositionBasic(new BoundingBox(0, 0), eLabelPosition.Right); + SetPositionBasic(new BoundingBox(0, 0) { Width = 5d }, eLabelPosition.Right); } } @@ -439,13 +439,13 @@ internal void SetShapeDimensions(Transform basePoint, Transform endPoint) if (endToBaseVector.X > 0) { //We must place to the left - SetPositionBasic(new BoundingBox(0, 0), eLabelPosition.Left); + SetPositionBasic(new BoundingBox(0, 0) { Width = 5d }, eLabelPosition.Left); } //if endpoint is to the right else { //We must place to the right - SetPositionBasic(new BoundingBox(0, 0), eLabelPosition.Right); + SetPositionBasic(new BoundingBox(0, 0) { Width = 5d }, eLabelPosition.Right); } } @@ -455,13 +455,13 @@ internal void SetShapeDimensions(Transform basePoint, Transform endPoint) if (endToBaseVector.Y > 0) { //We must place on Top - SetPositionBasic(new BoundingBox(0, 0), eLabelPosition.Top); + SetPositionBasic(new BoundingBox(0, 0) { Height = 5d }, eLabelPosition.Top); } //If endpoint is on bottom else { //We must place on Bottom - SetPositionBasic(new BoundingBox(0, 0), eLabelPosition.Bottom); + SetPositionBasic(new BoundingBox(0, 0) { Height = 5d }, eLabelPosition.Bottom); } } From 9e3ce65908cabcad9525e3b9ad505fb1d7e40ab1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ossian=20Edstr=C3=B6m?= Date: Thu, 18 Jun 2026 13:34:25 +0200 Subject: [PATCH 58/82] Added simpler but somewhat unclear method --- .../Chart/DataLabels/SvgDataLabelPoint.cs | 157 +++++++----------- 1 file changed, 60 insertions(+), 97 deletions(-) diff --git a/src/EPPlus/Drawing/Renderer/Chart/DataLabels/SvgDataLabelPoint.cs b/src/EPPlus/Drawing/Renderer/Chart/DataLabels/SvgDataLabelPoint.cs index 1493c0882e..9eea73aa37 100644 --- a/src/EPPlus/Drawing/Renderer/Chart/DataLabels/SvgDataLabelPoint.cs +++ b/src/EPPlus/Drawing/Renderer/Chart/DataLabels/SvgDataLabelPoint.cs @@ -14,6 +14,7 @@ using System; using System.Collections.Generic; using System.Net; +using static OfficeOpenXml.FormulaParsing.Excel.Functions.MathFunctions.RoundingHelper; namespace EPPlus.Export.ImageRenderer.RenderItems.SvgItem { @@ -300,6 +301,53 @@ private void CreateDebugPoints(Transform basePoint, Transform endPoint, Transfor centerPositionRect.Top += centerPoint.LocalPosition.Y; } + private void SetBasicPositionBasedOnInput(double pointOnAxis, BoundingBox axisMargin, eLabelPosition positivePosition, eLabelPosition negativePosition) + { + if(pointOnAxis != 0) + { + //If point is positive + if (pointOnAxis > 0) + { + //We must place to the left + SetPositionBasic(axisMargin, positivePosition); + } + //if basePoint is to the left + else + { + //We must place to the right + SetPositionBasic(axisMargin, negativePosition); + } + } + } + + /// + /// Set the basic label position based on the direction of a vector + /// + /// Shows what direction is in or out + /// Set the label in the same direction as the vector (set to false for opposite) + private void SetBasicPositionForInOrOut(Vector2 direction, bool inDirection) + { + //Could be simplified by pos Array defined by isIn + if(inDirection) + { + //If direction is to the right (positive) + //We must set the label to the Right to stay outside the shape. and vice versa + SetBasicPositionBasedOnInput(direction.X, new BoundingBox(0, 0) { Width = 5d }, eLabelPosition.Right, eLabelPosition.Left); + //If direction is to the bottom (positive) + //We must set the label to the bottom to stay within the shape. and vice versa + SetBasicPositionBasedOnInput(direction.Y, new BoundingBox(0, 0) { Height = 5d }, eLabelPosition.Bottom, eLabelPosition.Top); + } + else + { + //If direction is to the right (positive) + //We must set the label to the left to stay within the shape. and vice versa + SetBasicPositionBasedOnInput(direction.X, new BoundingBox(0, 0) { Width = 5d }, eLabelPosition.Left, eLabelPosition.Right); + //If direction is to the downwards (positive) + //We must set the label to the top to stay within the shape. and vice versa + SetBasicPositionBasedOnInput(direction.Y, new BoundingBox(0, 0) { Height = 5d }, eLabelPosition.Top, eLabelPosition.Bottom); + } + } + /// /// /// @@ -346,124 +394,39 @@ internal void SetShapeDimensions(Transform basePoint, Transform endPoint) Rectangle.Bounds.Top += centerPoint.LocalPosition.Y; break; case eLabelPosition.Left: + SetPositionBasic(_parentPoint, _labelPosition); break; case eLabelPosition.Right: case eLabelPosition.BestFit: + SetPositionBasic(_parentPoint, _labelPosition); break; case eLabelPosition.Top: + SetPositionBasic(_parentPoint, _labelPosition); break; case eLabelPosition.Bottom: + SetPositionBasic(_parentPoint, _labelPosition); break; case eLabelPosition.InBase: //Translate to the base point Rectangle.Bounds.Position += basePoint.LocalPosition; - - //Move the textbox margins inside on left - if (endToBaseVector.X != 0) - { - //If basePoint is to the left - if (endToBaseVector.X > 0) - { - //We must place to the left - SetPositionBasic(new BoundingBox(0, 0) { Width = 5d }, eLabelPosition.Left); - } - //if basePoint is to the right - else - { - //We must place to the right - SetPositionBasic(new BoundingBox(0, 0) { Width = 5d }, eLabelPosition.Right); - } - } - - //Move the textbox margins inside on top - if (endToBaseVector.Y != 0) - { - //If endpoint is on Top - if (endToBaseVector.Y > 0) - { - //We must place on bottom and apply margin to height - SetPositionBasic(new BoundingBox(0, 0) { Height = 5d }, eLabelPosition.Top); - } - //If endpoint is on bottom - else - { - //We must place on top and apply margin to height - SetPositionBasic(new BoundingBox(0, 0) { Height = 5d }, eLabelPosition.Bottom); - } - } + //We want to stay inside the object so if the direction to end is Right we want to set the label to the Left + SetBasicPositionForInOrOut(endToBaseVector, false); break; case eLabelPosition.InEnd: //Move to end point Rectangle.Bounds.Position += endPoint.LocalPosition; - //Move the textbox margins inside on left - if (endToBaseVector.X != 0) - { - //If basePoint is to the left - if (endToBaseVector.X < 0) - { - //We must place to the left - SetPositionBasic(new BoundingBox(0, 0) { Width = 5d }, eLabelPosition.Left); - } - //if basePoint is to the right - else - { - //We must place to the right - SetPositionBasic(new BoundingBox(0, 0) { Width = 5d }, eLabelPosition.Right); - } - } - - //Move the textbox margins inside on top - if (endToBaseVector.Y != 0) - { - //If endpoint is on Top - if (endToBaseVector.Y < 0) - { - //We must place on bottom - SetPositionBasic(new BoundingBox(0, 0) { Height = 5d }, eLabelPosition.Top); - } - //If endpoint is on bottom - else - { - //We must place on top - SetPositionBasic(new BoundingBox(0, 0) { Height = 5d }, eLabelPosition.Bottom); - } - } + //We want to stay inside the object so if the direction to end is Right we want to set the label to the Right + //Since the only time endToBaseVector is Positive is when the end is further positive than the base + SetBasicPositionForInOrOut(endToBaseVector, true); break; case eLabelPosition.OutEnd: //Move to end point Rectangle.Bounds.Position += endPoint.LocalPosition; - if (endToBaseVector.X != 0) - { - //If endPoint is to the left - if (endToBaseVector.X > 0) - { - //We must place to the left - SetPositionBasic(new BoundingBox(0, 0) { Width = 5d }, eLabelPosition.Left); - } - //if endpoint is to the right - else - { - //We must place to the right - SetPositionBasic(new BoundingBox(0, 0) { Width = 5d }, eLabelPosition.Right); - } - } - if (endToBaseVector.Y != 0) - { - //If endpoint is on Top - if (endToBaseVector.Y > 0) - { - //We must place on Top - SetPositionBasic(new BoundingBox(0, 0) { Height = 5d }, eLabelPosition.Top); - } - //If endpoint is on bottom - else - { - //We must place on Bottom - SetPositionBasic(new BoundingBox(0, 0) { Height = 5d }, eLabelPosition.Bottom); - } - } + //We want to set the label outside the object + //So we do the opposite of InEnd + SetBasicPositionForInOrOut(endToBaseVector, false); break; default: From d5ea8ab5719be6fe21822915c2c5974e73d38b87 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ossian=20Edstr=C3=B6m?= Date: Thu, 18 Jun 2026 14:26:01 +0200 Subject: [PATCH 59/82] Attempting to use functionality in pie item --- .../ChartTypeDrawers/PieChartTypeDrawer.cs | 17 ++++++++-- .../Renderer/Chart/PieSliceRenderItem.cs | 31 +++++++++++++++++++ 2 files changed, 45 insertions(+), 3 deletions(-) diff --git a/src/EPPlus/Drawing/Renderer/Chart/ChartTypeDrawers/PieChartTypeDrawer.cs b/src/EPPlus/Drawing/Renderer/Chart/ChartTypeDrawers/PieChartTypeDrawer.cs index 7511ca5f3e..29a2b3d3e1 100644 --- a/src/EPPlus/Drawing/Renderer/Chart/ChartTypeDrawers/PieChartTypeDrawer.cs +++ b/src/EPPlus/Drawing/Renderer/Chart/ChartTypeDrawers/PieChartTypeDrawer.cs @@ -244,11 +244,22 @@ internal override void DrawSeries() 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); - serieDataLabels[i].SetParentVector(dlblBounds, j, Slices[j].GetWholeVectorCenterToMid()); + var ctrToMid = Slices[j].GetWholeVectorCenterToMid(); + var startPt = new Transform(); + startPt.Parent = innerGroup.Parent; + startPt.Position = innerGroup.Position + (ctrToMid * -1); + + serieDataLabels[i].SetDimensions(j, startPt, innerGroup); + + //var endPoint = Slices[j].CopyOuterMidPoint(); + //var startPoint = _circleCenter.Position + //serieDataLabels[i].SetDimensions(j, startPoint, endPoint); + + //serieDataLabels[i].SetParentVector(dlblBounds, j, Slices[j].GetWholeVectorCenterToMid()); } } } diff --git a/src/EPPlus/Drawing/Renderer/Chart/PieSliceRenderItem.cs b/src/EPPlus/Drawing/Renderer/Chart/PieSliceRenderItem.cs index 037532da46..fb58373378 100644 --- a/src/EPPlus/Drawing/Renderer/Chart/PieSliceRenderItem.cs +++ b/src/EPPlus/Drawing/Renderer/Chart/PieSliceRenderItem.cs @@ -159,6 +159,7 @@ void CalculateWidthHeight(double prevSliceDegrees) private double _sliceScaleFactor = 1d; private double _scaledRadius { get { return _radius * _sliceScaleFactor; } } + private void CalculateExplosionDir() { var transformOriginLocal = new Vector2(_innerGroup.TransformOrigin.X, _innerGroup.TransformOrigin.Y); @@ -525,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; @@ -539,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) { From f6db9124c5d9eeb528960716f8cdff04db34424b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ossian=20Edstr=C3=B6m?= Date: Thu, 18 Jun 2026 17:08:48 +0200 Subject: [PATCH 60/82] Fixed several pie chart datalabel bugs --- .../Chart/PieChartTests.cs | 4 +- .../Svg/SvgGroupRenderer.cs | 4 +- .../ChartTypeDrawers/PieChartTypeDrawer.cs | 18 ++- .../DataLabels/ChartSerieDataLabelRenderer.cs | 26 ++++- .../Chart/DataLabels/SvgDataLabelPoint.cs | 106 +++++++++++++++--- 5 files changed, 132 insertions(+), 26 deletions(-) diff --git a/src/EPPlus.DrawingRenderer.Tests/Chart/PieChartTests.cs b/src/EPPlus.DrawingRenderer.Tests/Chart/PieChartTests.cs index bfe3fd61c3..196ee89afc 100644 --- a/src/EPPlus.DrawingRenderer.Tests/Chart/PieChartTests.cs +++ b/src/EPPlus.DrawingRenderer.Tests/Chart/PieChartTests.cs @@ -12,12 +12,12 @@ public class PieChartTests : TestBase { [TestMethod] - public void ReadAndGenerateExcelPieChartSvgs() + public void ReadAndGenerateAll() { ExcelPackage.License.SetNonCommercialOrganization("EPPlus Project"); using (var p = OpenTemplatePackage("PieChartSvgALL.xlsx")) { - var ws = p.Workbook.Worksheets[0]; + var ws = p.Workbook.Worksheets[4]; for (int i = 0; i < p.Workbook.Worksheets.Count; i++) { diff --git a/src/EPPlus.DrawingRenderer/Svg/SvgGroupRenderer.cs b/src/EPPlus.DrawingRenderer/Svg/SvgGroupRenderer.cs index c2a66a0f69..8f416409d6 100644 --- a/src/EPPlus.DrawingRenderer/Svg/SvgGroupRenderer.cs +++ b/src/EPPlus.DrawingRenderer/Svg/SvgGroupRenderer.cs @@ -22,7 +22,9 @@ public override void Render(GroupRenderItem item) //Neccesary as fallback for e.g. DataLabels string fillPropery = ""; - if (string.IsNullOrEmpty(item.FillColor) == false && item.FillColor != "none") + + //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}\" "; } diff --git a/src/EPPlus/Drawing/Renderer/Chart/ChartTypeDrawers/PieChartTypeDrawer.cs b/src/EPPlus/Drawing/Renderer/Chart/ChartTypeDrawers/PieChartTypeDrawer.cs index 29a2b3d3e1..bc24cc1d55 100644 --- a/src/EPPlus/Drawing/Renderer/Chart/ChartTypeDrawers/PieChartTypeDrawer.cs +++ b/src/EPPlus/Drawing/Renderer/Chart/ChartTypeDrawers/PieChartTypeDrawer.cs @@ -192,6 +192,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 @@ -207,23 +213,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; } } @@ -237,10 +243,10 @@ 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) { diff --git a/src/EPPlus/Drawing/Renderer/Chart/DataLabels/ChartSerieDataLabelRenderer.cs b/src/EPPlus/Drawing/Renderer/Chart/DataLabels/ChartSerieDataLabelRenderer.cs index 46c26ee6e7..58f34a3a41 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,6 +24,9 @@ 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; @@ -162,19 +167,36 @@ public override void AppendRenderItems(List renderItems) 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.AddChildItem(Rectangle); Rectangle.SetDrawingPropertiesFill(ChartRenderer.Theme, _dlblSerie.Fill, null); - plotAreaGroup.SetDrawingPropertiesFill(ChartRenderer.Theme, _dlblSerie.Fill, null); - plotAreaGroup.GroupTransform += $" fill=\"{plotAreaGroup.FillColor}\" name=\"Plot area group\""; + //if (_dlblSerie.Fill.Color.ToArgb() == Color.Transparent.ToArgb()) + //{ + // plotAreaGroup.FillColor = "transparent"; + //} + + //plotAreaGroup.GroupTransform += $" fill=\"{plotAreaGroup.FillColor}\" name=\"Plot area group\""; } 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 9eea73aa37..6a4232f7b0 100644 --- a/src/EPPlus/Drawing/Renderer/Chart/DataLabels/SvgDataLabelPoint.cs +++ b/src/EPPlus/Drawing/Renderer/Chart/DataLabels/SvgDataLabelPoint.cs @@ -13,6 +13,7 @@ using OfficeOpenXml.Utils.EnumUtils; using System; using System.Collections.Generic; +using System.Drawing; using System.Net; using static OfficeOpenXml.FormulaParsing.Excel.Functions.MathFunctions.RoundingHelper; @@ -31,6 +32,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) //{ @@ -348,6 +350,29 @@ private void SetBasicPositionForInOrOut(Vector2 direction, bool inDirection) } } + private void SetAdjustedTextBoxPosition(Vector2 direction, bool reverseDirection) + { + if(reverseDirection) + { + direction *= -1; + } + + var txtBoxAdjustVector = new Vector2(_txtBox.Width / 2d, _txtBox.Height / 2d); + var percentualLength = txtBoxAdjustVector.Length / direction.Length; + Rectangle.Bounds.Position += direction * percentualLength; + } + + private void SetInOut(Vector2 direction, Vector2 translation, bool reverseDirection) + { + //Translate to the base point + Rectangle.Bounds.Position += translation; + SetAdjustedTextBoxPosition(direction, reverseDirection); + } + + //private void SetInEnd(Vector2 direction, Vector2 translation) + + //private void SetLeft(Transform centerPoint) + /// /// /// @@ -397,7 +422,6 @@ internal void SetShapeDimensions(Transform basePoint, Transform endPoint) SetPositionBasic(_parentPoint, _labelPosition); break; case eLabelPosition.Right: - case eLabelPosition.BestFit: SetPositionBasic(_parentPoint, _labelPosition); break; case eLabelPosition.Top: @@ -407,27 +431,76 @@ internal void SetShapeDimensions(Transform basePoint, Transform endPoint) SetPositionBasic(_parentPoint, _labelPosition); break; case eLabelPosition.InBase: - //Translate to the base point - Rectangle.Bounds.Position += basePoint.LocalPosition; - //We want to stay inside the object so if the direction to end is Right we want to set the label to the Left - SetBasicPositionForInOrOut(endToBaseVector, false); + SetInOut(endToBaseVector, basePoint.LocalPosition, true); + //SetInBase(endToBaseVector, basePoint.LocalPosition); + ////Translate to the base point + //Rectangle.Bounds.Position += basePoint.LocalPosition; + //SetAdjustedTextBoxPosition(endToBaseVector, true); + //var textPartToPoint = new Vector2(_txtBox.Width + 5, _txtBox.Height + 5) * -1; + //Rectangle.Bounds.Position += textPartToPoint; + //var reverseDir = new Vector2(direction.X + _txtBox.Width + 5, direction.Y + _txtBox.Height + 5) * -1; + + ////We want to stay inside the object so if the direction to end is Right we want to set the label to the Left + //SetBasicPositionForInOrOut(endToBaseVector, false); break; case eLabelPosition.InEnd: - //Move to end point - Rectangle.Bounds.Position += endPoint.LocalPosition; - - //We want to stay inside the object so if the direction to end is Right we want to set the label to the Right - //Since the only time endToBaseVector is Positive is when the end is further positive than the base - SetBasicPositionForInOrOut(endToBaseVector, true); + SetInOut(endToBaseVector, endPoint.LocalPosition, false); + ////Move to end point + //Rectangle.Bounds.Position += endPoint.LocalPosition; + + //SetAdjustedTextBoxPosition(endToBaseVector, false); + //var txtBoxAdjustVector = new Vector2(_txtBox.Width / 2d, _txtBox.Height / 2d); + //var percentualLength = txtBoxAdjustVector.Length / endToBaseVector.Length; + //Rectangle.Bounds.Position += endToBaseVector * percentualLength; + + //var textPartToPoint2 = new Vector2(1 - ((_txtBox.Width/2) / endPoint.LocalPosition.X), 1 - ((_txtBox.Height/2) / endPoint.LocalPosition.Y)); + //Rectangle.Bounds.Position *= textPartToPoint2; + ////We want to stay inside the object so if the direction to end is Right we want to set the label to the Right + ////Since the only time endToBaseVector is Positive is when the end is further positive than the base + //SetBasicPositionForInOrOut(endToBaseVector, true); break; case eLabelPosition.OutEnd: //Move to end point - Rectangle.Bounds.Position += endPoint.LocalPosition; + SetInOut(endToBaseVector, endPoint.LocalPosition, true); + //Rectangle.Bounds.Position += endPoint.LocalPosition; + //SetAdjustedTextBoxPosition(endToBaseVector, true); + //var txtBoxAdjustVector = new Vector2(_txtBox.Width / 2d, _txtBox.Height / 2d); + //var percentualLength = txtBoxAdjustVector.Length / endToBaseVector.Length; + //Rectangle.Bounds.Position += endToBaseVector * percentualLength; + //var textPartToPoint3 = new Vector2(_txtBox.Width + 5, _txtBox.Height + 5) * -1; + //Rectangle.Bounds.Position += textPartToPoint3; + ////We want to set the label outside the object + ////So we do the opposite of InEnd + //SetBasicPositionForInOrOut(endToBaseVector, false); - //We want to set the label outside the object - //So we do the opposite of InEnd - SetBasicPositionForInOrOut(endToBaseVector, false); + 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? + //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 + { + //Set outside end + SetInOut(endToBaseVector, endPoint.LocalPosition, true); + } break; default: throw new InvalidOperationException($"The datalabel position {_labelPosition} has not been implemented yet"); @@ -738,6 +811,9 @@ public override void AppendRenderItems(List renderItems) parentPointGroup.RenderItems.Add(group); + group.RotationPoint = new Graphics.Point(_txtBox.Left + (_txtBox.Width / 2), _txtBox.Top + (_txtBox.Height / 2)); + group.Rotation = CounterRotation; + _txtBox.AppendRenderItems(group.RenderItems); if(_renderConnectionPointLines) From c0770cd5ea41c90d646e7abc0ce877f7ec9bb5e1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ossian=20Edstr=C3=B6m?= Date: Mon, 22 Jun 2026 10:03:40 +0200 Subject: [PATCH 61/82] Fixed bugs and cleaned up datalabels --- .../Chart/BarChartTests.cs | 10 +- .../BarColumnChartTypeDrawer.cs | 17 +- .../DataLabels/ChartSerieDataLabelRenderer.cs | 33 +- .../Chart/DataLabels/SvgDataLabelPoint.cs | 283 +----------------- 4 files changed, 28 insertions(+), 315 deletions(-) diff --git a/src/EPPlus.DrawingRenderer.Tests/Chart/BarChartTests.cs b/src/EPPlus.DrawingRenderer.Tests/Chart/BarChartTests.cs index f09e6c0318..25c529bcfb 100644 --- a/src/EPPlus.DrawingRenderer.Tests/Chart/BarChartTests.cs +++ b/src/EPPlus.DrawingRenderer.Tests/Chart/BarChartTests.cs @@ -47,13 +47,12 @@ public void GenerateSvgForBarCharts2() public void DatalabelBarCharts() { ExcelPackage.License.SetNonCommercialOrganization("EPPlus Project"); - using (var p = OpenTemplatePackage("BarChartForSvgDatalabels2.xlsx")) + using (var p = OpenTemplatePackage("BarChartForSvgDatalabelsBasic.xlsx")) { var ws = p.Workbook.Worksheets[0]; var drawings = ws.Drawings; var ix = 0; - //var svg = drawings[ix].ToSvg(); - //SaveTextFileToWorkbook($"svg\\BarChartDataLabels{ix++}.svg", svg); + for (int i = ix; i < drawings.Count; i++) { var svg = drawings[i].ToSvg(); @@ -71,9 +70,8 @@ public void NegativeDatalabelBarCharts() { var ws = p.Workbook.Worksheets[0]; var drawings = ws.Drawings; - var ix = 1; - //var svg = drawings[ix].ToSvg(); - //SaveTextFileToWorkbook($"svg\\BarChartDataLabels{ix++}.svg", svg); + var ix = 0; + for (int i = ix; i < drawings.Count; i++) { var svg = drawings[i].ToSvg(); diff --git a/src/EPPlus/Drawing/Renderer/Chart/ChartTypeDrawers/BarColumnChartTypeDrawer.cs b/src/EPPlus/Drawing/Renderer/Chart/ChartTypeDrawers/BarColumnChartTypeDrawer.cs index 93ef8c2017..bc3fbe8a61 100644 --- a/src/EPPlus/Drawing/Renderer/Chart/ChartTypeDrawers/BarColumnChartTypeDrawer.cs +++ b/src/EPPlus/Drawing/Renderer/Chart/ChartTypeDrawers/BarColumnChartTypeDrawer.cs @@ -86,15 +86,23 @@ internal override void DrawSeries() if (isColumn == true) { var middleRight = dataPoints[j].Left + (dataPoints[j].Width / 2); - basePoint.Position = new Vector2(middleRight, chartBaseY); - if (chartBaseY > dataPoints[j].Top) + if (chartBaseY >= dataPoints[j].Top) { - endPoint.Position = new Vector2(middleRight, chartBaseY - dataPoints[j].Height); + //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 { - endPoint.Position = new Vector2(middleRight, chartBaseY + dataPoints[j].Height); + //We are a positive column + // _ + // | | Col + // ----- Base-Axis + basePoint.Position = new Vector2(middleRight, dataPoints[j].Top); + endPoint.Position = new Vector2(middleRight, dataPoints[j].Bottom); } serieDataLabels[i].SetDimensions(j, basePoint, endPoint); @@ -111,7 +119,6 @@ internal override void DrawSeries() { endPoint.Position = new Vector2(dataPoints[j].Left + dataPoints[j].Width, middleHeight); } - //endPoint.Position = new Vector2(basePoint.Position.X + dataPoints[j].Width, middleHeight); serieDataLabels[i].SetDimensions(j, basePoint, endPoint); } diff --git a/src/EPPlus/Drawing/Renderer/Chart/DataLabels/ChartSerieDataLabelRenderer.cs b/src/EPPlus/Drawing/Renderer/Chart/DataLabels/ChartSerieDataLabelRenderer.cs index 58f34a3a41..8b44fceca3 100644 --- a/src/EPPlus/Drawing/Renderer/Chart/DataLabels/ChartSerieDataLabelRenderer.cs +++ b/src/EPPlus/Drawing/Renderer/Chart/DataLabels/ChartSerieDataLabelRenderer.cs @@ -121,22 +121,6 @@ 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 SetDimensions(int index, Transform basePoint, Transform endPoint) { if (dataLabels.Count > index) @@ -145,19 +129,12 @@ internal void SetDimensions(int index, Transform basePoint, Transform endPoint) } } - internal void SetParentVector(BoundingBox parentPoint, int index, Vector2 startToEndDir) - { - _startToEndDir = startToEndDir; - SetParentPoint(parentPoint, index); - } - 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) @@ -178,16 +155,8 @@ public override void AppendRenderItems(List renderItems) if (_dlblSerie.Fill.IsEmpty == false) { - //Rectangle.SetDrawingPropertiesFill(ChartRenderer.Theme, _dlblSerie.Fill, null); - //plotAreaGroup.AddChildItem(Rectangle); Rectangle.SetDrawingPropertiesFill(ChartRenderer.Theme, _dlblSerie.Fill, null); plotAreaGroup.SetDrawingPropertiesFill(ChartRenderer.Theme, _dlblSerie.Fill, null); - //if (_dlblSerie.Fill.Color.ToArgb() == Color.Transparent.ToArgb()) - //{ - // plotAreaGroup.FillColor = "transparent"; - //} - - //plotAreaGroup.GroupTransform += $" fill=\"{plotAreaGroup.FillColor}\" name=\"Plot area group\""; } renderItems.Add(plotAreaGroup); diff --git a/src/EPPlus/Drawing/Renderer/Chart/DataLabels/SvgDataLabelPoint.cs b/src/EPPlus/Drawing/Renderer/Chart/DataLabels/SvgDataLabelPoint.cs index 6a4232f7b0..2222af6c02 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; @@ -9,13 +8,9 @@ using OfficeOpenXml.Drawing; using OfficeOpenXml.Drawing.Chart; using OfficeOpenXml.Drawing.Renderer.TextBox; -using OfficeOpenXml.FormulaParsing.Excel.Functions.MathFunctions; using OfficeOpenXml.Utils.EnumUtils; using System; using System.Collections.Generic; -using System.Drawing; -using System.Net; -using static OfficeOpenXml.FormulaParsing.Excel.Functions.MathFunctions.RoundingHelper; namespace EPPlus.Export.ImageRenderer.RenderItems.SvgItem { @@ -238,10 +233,6 @@ private int GetClosestConnectionPointCoordinateIndex(Coordinate originPoint) return smallestIndex; } - BoundingBox _parentShapeBounds = null; - - - private void SetPositionBasic(BoundingBox point, eLabelPosition basicPosition) { switch (basicPosition) @@ -302,54 +293,6 @@ private void CreateDebugPoints(Transform basePoint, Transform endPoint, Transfor centerPositionRect.Left += centerPoint.LocalPosition.X; centerPositionRect.Top += centerPoint.LocalPosition.Y; } - - private void SetBasicPositionBasedOnInput(double pointOnAxis, BoundingBox axisMargin, eLabelPosition positivePosition, eLabelPosition negativePosition) - { - if(pointOnAxis != 0) - { - //If point is positive - if (pointOnAxis > 0) - { - //We must place to the left - SetPositionBasic(axisMargin, positivePosition); - } - //if basePoint is to the left - else - { - //We must place to the right - SetPositionBasic(axisMargin, negativePosition); - } - } - } - - /// - /// Set the basic label position based on the direction of a vector - /// - /// Shows what direction is in or out - /// Set the label in the same direction as the vector (set to false for opposite) - private void SetBasicPositionForInOrOut(Vector2 direction, bool inDirection) - { - //Could be simplified by pos Array defined by isIn - if(inDirection) - { - //If direction is to the right (positive) - //We must set the label to the Right to stay outside the shape. and vice versa - SetBasicPositionBasedOnInput(direction.X, new BoundingBox(0, 0) { Width = 5d }, eLabelPosition.Right, eLabelPosition.Left); - //If direction is to the bottom (positive) - //We must set the label to the bottom to stay within the shape. and vice versa - SetBasicPositionBasedOnInput(direction.Y, new BoundingBox(0, 0) { Height = 5d }, eLabelPosition.Bottom, eLabelPosition.Top); - } - else - { - //If direction is to the right (positive) - //We must set the label to the left to stay within the shape. and vice versa - SetBasicPositionBasedOnInput(direction.X, new BoundingBox(0, 0) { Width = 5d }, eLabelPosition.Left, eLabelPosition.Right); - //If direction is to the downwards (positive) - //We must set the label to the top to stay within the shape. and vice versa - SetBasicPositionBasedOnInput(direction.Y, new BoundingBox(0, 0) { Height = 5d }, eLabelPosition.Top, eLabelPosition.Bottom); - } - } - private void SetAdjustedTextBoxPosition(Vector2 direction, bool reverseDirection) { if(reverseDirection) @@ -357,22 +300,23 @@ private void SetAdjustedTextBoxPosition(Vector2 direction, bool 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); - var percentualLength = txtBoxAdjustVector.Length / direction.Length; - Rectangle.Bounds.Position += direction * percentualLength; + + //Apply translation to current position + Rectangle.Bounds.Position += directionOnly * txtBoxAdjustVector; } private void SetInOut(Vector2 direction, Vector2 translation, bool reverseDirection) { - //Translate to the base point + //Translate to the translation point Rectangle.Bounds.Position += translation; SetAdjustedTextBoxPosition(direction, reverseDirection); } - //private void SetInEnd(Vector2 direction, Vector2 translation) - - //private void SetLeft(Transform centerPoint) - /// /// /// @@ -432,47 +376,12 @@ internal void SetShapeDimensions(Transform basePoint, Transform endPoint) break; case eLabelPosition.InBase: SetInOut(endToBaseVector, basePoint.LocalPosition, true); - //SetInBase(endToBaseVector, basePoint.LocalPosition); - ////Translate to the base point - //Rectangle.Bounds.Position += basePoint.LocalPosition; - //SetAdjustedTextBoxPosition(endToBaseVector, true); - //var textPartToPoint = new Vector2(_txtBox.Width + 5, _txtBox.Height + 5) * -1; - //Rectangle.Bounds.Position += textPartToPoint; - //var reverseDir = new Vector2(direction.X + _txtBox.Width + 5, direction.Y + _txtBox.Height + 5) * -1; - - ////We want to stay inside the object so if the direction to end is Right we want to set the label to the Left - //SetBasicPositionForInOrOut(endToBaseVector, false); break; case eLabelPosition.InEnd: SetInOut(endToBaseVector, endPoint.LocalPosition, false); - ////Move to end point - //Rectangle.Bounds.Position += endPoint.LocalPosition; - - //SetAdjustedTextBoxPosition(endToBaseVector, false); - //var txtBoxAdjustVector = new Vector2(_txtBox.Width / 2d, _txtBox.Height / 2d); - //var percentualLength = txtBoxAdjustVector.Length / endToBaseVector.Length; - //Rectangle.Bounds.Position += endToBaseVector * percentualLength; - - //var textPartToPoint2 = new Vector2(1 - ((_txtBox.Width/2) / endPoint.LocalPosition.X), 1 - ((_txtBox.Height/2) / endPoint.LocalPosition.Y)); - //Rectangle.Bounds.Position *= textPartToPoint2; - ////We want to stay inside the object so if the direction to end is Right we want to set the label to the Right - ////Since the only time endToBaseVector is Positive is when the end is further positive than the base - //SetBasicPositionForInOrOut(endToBaseVector, true); break; case eLabelPosition.OutEnd: - //Move to end point SetInOut(endToBaseVector, endPoint.LocalPosition, true); - //Rectangle.Bounds.Position += endPoint.LocalPosition; - //SetAdjustedTextBoxPosition(endToBaseVector, true); - //var txtBoxAdjustVector = new Vector2(_txtBox.Width / 2d, _txtBox.Height / 2d); - //var percentualLength = txtBoxAdjustVector.Length / endToBaseVector.Length; - //Rectangle.Bounds.Position += endToBaseVector * percentualLength; - //var textPartToPoint3 = new Vector2(_txtBox.Width + 5, _txtBox.Height + 5) * -1; - //Rectangle.Bounds.Position += textPartToPoint3; - ////We want to set the label outside the object - ////So we do the opposite of InEnd - //SetBasicPositionForInOrOut(endToBaseVector, false); - break; //Only available in charts that include pie chart case eLabelPosition.BestFit: @@ -507,59 +416,18 @@ internal void SetShapeDimensions(Transform basePoint, Transform endPoint) } } - internal void SetParentPoint(BoundingBox parentPoint, BoundingBox parentShape, Vector2 startToEndDir) + internal void SetParentPoint(BoundingBox parentPoint) { Rectangle.Bounds.Parent = parentPoint; _parentPoint = parentPoint; - _parentShapeBounds = parentShape; var dataLabelCenter = new Vector2(Rectangle.Bounds.Left, Rectangle.Bounds.Top); - Vector2 startPointDirection = Vector2.Zero; - - if ((startToEndDir.X == 0 && startToEndDir.Y == 0) == false) - { - startPointDirection = startToEndDir / startToEndDir.Length; - } - - //Rectangle.Bounds.Left += 20; - - //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) { case eLabelPosition.Center: - - if ((startToEndDir.X == 0 && startToEndDir.Y == 0) == false) - { - //Half and invert - dataLabelCenter = ((startToEndDir * 0.5d) * -1d); - } - else if (startToEndDir.Y != 0) - { - //Half and invert - dataLabelCenter = ((startToEndDir * 0.5d) * -1d); - } - - Rectangle.Bounds.Left += dataLabelCenter.X; - Rectangle.Bounds.Top += dataLabelCenter.Y; + Rectangle.Bounds.Left += Rectangle.Bounds.Left; + Rectangle.Bounds.Top += Rectangle.Bounds.Top; break; case eLabelPosition.Left: SetPositionBasic(parentPoint, _labelPosition); @@ -574,135 +442,6 @@ internal void SetParentPoint(BoundingBox parentPoint, BoundingBox parentShape, V case eLabelPosition.Bottom: SetPositionBasic(parentPoint, _labelPosition); break; - case eLabelPosition.InBase: - var endToStartVector = startToEndDir * -1; - //Rectangle.Left *= endToStartVector.X; - //Rectangle.Top *= endToStartVector.Y; - if (startPointDirection.X != 0) - { - Rectangle.Left += endToStartVector.X; - //If basePoint is to the left - if (startPointDirection.X < 0) - { - //We must place to the left - SetPositionBasic(new BoundingBox(0,0), eLabelPosition.Left); - } - //if basePoint is to the right - else - { - //We must place to the right - SetPositionBasic(new BoundingBox(0, 0), eLabelPosition.Right); - } - } - - if (startPointDirection.Y != 0) - { - Rectangle.Top += endToStartVector.Y; - //If endpoint is on Top - if (startPointDirection.Y < 0) - { - //We must place on bottom - SetPositionBasic(new BoundingBox(0, 0), eLabelPosition.Top); - } - //If endpoint is on bottom - else - { - //We must place on top - SetPositionBasic(new BoundingBox(0, 0), eLabelPosition.Bottom); - } - } - break; - case eLabelPosition.InEnd: - if (startPointDirection.X != 0) - { - //If endPoint is to the left - if (startPointDirection.X < 0) - { - //We must place to the right - SetPositionBasic(parentPoint, eLabelPosition.Right); - } - //if endpoint is to the right - else - { - //We must place to the left - SetPositionBasic(parentPoint, eLabelPosition.Left); - } - } - - if (startPointDirection.Y != 0) - { - //If endpoint is on Top - if (startPointDirection.Y < 0) - { - //We must place on bottom - SetPositionBasic(parentPoint, eLabelPosition.Bottom); - } - //If endpoint is on bottom - else - { - //We must place on top - SetPositionBasic(parentPoint, eLabelPosition.Top); - } - } - //if (startPointDirection.X == 0 && startPointDirection.Y == 0) - //{ - // throw new InvalidOperationException("eLabelPosition.InEnd MUST have a direction." + - // "Cannot be within End if EndPoint is undefined."); - //} - //var insidePos = startToEndDir * 0.15 * -1; - //Rectangle.Bounds.Left += insidePos.X; - //Rectangle.Bounds.Top += insidePos.Y; - 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 (startPointDirection.X == 0 && startPointDirection.Y == 0) - //{ - // throw new InvalidOperationException("eLabelPosition.OutEnd MUST have a direction." + - // "Cannot be within End if EndPoint is undefined."); - //} - //Rectangle.Bounds.Left += startToEndDir.X * 0.15; - //Rectangle.Bounds.Top += startToEndDir.Y * 0.15; - //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"); } From 3bb4be8db5ab6012b73ab52b247ce0f6b634a1b7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ossian=20Edstr=C3=B6m?= Date: Mon, 22 Jun 2026 10:26:20 +0200 Subject: [PATCH 62/82] Cleanup and added all pie datalabels to same file --- .../Chart/PieChartTests.cs | 147 +----------------- .../BarColumnChartTypeDrawer.cs | 2 - .../ChartTypeDrawers/PieChartTypeDrawer.cs | 18 --- 3 files changed, 2 insertions(+), 165 deletions(-) diff --git a/src/EPPlus.DrawingRenderer.Tests/Chart/PieChartTests.cs b/src/EPPlus.DrawingRenderer.Tests/Chart/PieChartTests.cs index 196ee89afc..693294b5c4 100644 --- a/src/EPPlus.DrawingRenderer.Tests/Chart/PieChartTests.cs +++ b/src/EPPlus.DrawingRenderer.Tests/Chart/PieChartTests.cs @@ -12,31 +12,10 @@ public class PieChartTests : TestBase { [TestMethod] - public void ReadAndGenerateAll() + public void ReadAndCreateSvgsAll() { ExcelPackage.License.SetNonCommercialOrganization("EPPlus Project"); using (var p = OpenTemplatePackage("PieChartSvgALL.xlsx")) - { - var ws = p.Workbook.Worksheets[4]; - - 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]; @@ -46,129 +25,7 @@ public void ReadAndGenerateExcelPieChartPointExplosion() 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]; - - //var drawing = ws.Drawings["InsideEnd"]; - - //var svg = drawing.ToSvg(); - //SaveTextFileToWorkbook($"svg\\PieChartDlbls\\s{0}_{ws.Name}_{drawing.Name}.svg", svg); - - for (int i = 0; i < p.Workbook.Worksheets.Count; i++) - { - var 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); - } - } - } - } - - [TestMethod] - public void ReadLegendIssue() - { - ExcelPackage.License.SetNonCommercialOrganization("EPPlus Project"); - using (var p = OpenTemplatePackage("PieChartSvgLegendIssue.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\\PieChartSvgLegendIssue\\s{i}_{ws.Name}_{c.Name}.svg", svg); - } - } - } - } - - [TestMethod] - public void Datalabels22() - { - ExcelPackage.License.SetNonCommercialOrganization("EPPlus Project"); - using (var p = OpenTemplatePackage("PieChartDlblsInside.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\\PieChartDlbls22\\s{i}_{ws.Name}_{c.Name}.svg", svg); - } - } - } - } - - - [TestMethod] - public void Datalabels3() - { - ExcelPackage.License.SetNonCommercialOrganization("EPPlus Project"); - using (var p = OpenTemplatePackage("PieChartDlblsOutside.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\\PieChartDlbls3\\s{i}_{ws.Name}_{c.Name}.svg", svg); + SaveTextFileToWorkbook($"svg\\PieChartSvgALL\\s{i}_{ws.Name}_{c.Name}.svg", svg); } } } diff --git a/src/EPPlus/Drawing/Renderer/Chart/ChartTypeDrawers/BarColumnChartTypeDrawer.cs b/src/EPPlus/Drawing/Renderer/Chart/ChartTypeDrawers/BarColumnChartTypeDrawer.cs index bc3fbe8a61..05a14f19fb 100644 --- a/src/EPPlus/Drawing/Renderer/Chart/ChartTypeDrawers/BarColumnChartTypeDrawer.cs +++ b/src/EPPlus/Drawing/Renderer/Chart/ChartTypeDrawers/BarColumnChartTypeDrawer.cs @@ -5,9 +5,7 @@ using EPPlusImageRenderer; using EPPlusImageRenderer.RenderItems; using EPPlusImageRenderer.Svg; -using OfficeOpenXml.DigitalSignatures; using OfficeOpenXml.Drawing.Chart; -using OfficeOpenXml.FormulaParsing.Excel.Functions.Text; using OfficeOpenXml.Utils.TypeConversion; using System; using System.Collections.Generic; diff --git a/src/EPPlus/Drawing/Renderer/Chart/ChartTypeDrawers/PieChartTypeDrawer.cs b/src/EPPlus/Drawing/Renderer/Chart/ChartTypeDrawers/PieChartTypeDrawer.cs index bc24cc1d55..9c4ec5975e 100644 --- a/src/EPPlus/Drawing/Renderer/Chart/ChartTypeDrawers/PieChartTypeDrawer.cs +++ b/src/EPPlus/Drawing/Renderer/Chart/ChartTypeDrawers/PieChartTypeDrawer.cs @@ -172,17 +172,9 @@ internal override void DrawSeries() { _groupItem = new GroupRenderItem(ChartRenderer.Plotarea.Group.Bounds); - //_groupItem.Left = ChartRenderer.Plotarea.Group.Left; - //_groupItem.Top = ChartRenderer.Plotarea.Group.Top; - - //_groupItem.TransformOrigin = new Coordinate(ChartRenderer.Plotarea.LeftMargin, ChartRenderer.Plotarea.TopMargin); - 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 @@ -260,12 +252,6 @@ internal override void DrawSeries() startPt.Position = innerGroup.Position + (ctrToMid * -1); serieDataLabels[i].SetDimensions(j, startPt, innerGroup); - - //var endPoint = Slices[j].CopyOuterMidPoint(); - //var startPoint = _circleCenter.Position - //serieDataLabels[i].SetDimensions(j, startPoint, endPoint); - - //serieDataLabels[i].SetParentVector(dlblBounds, j, Slices[j].GetWholeVectorCenterToMid()); } } } @@ -284,14 +270,10 @@ internal override void DrawSeries() public override void AppendRenderItems(List renderItems) { ChartRenderer.Plotarea.Group.AddChildItem(_groupItem); - //ChartRenderer.Plotarea.Group.AddChildItem(SeriesRenderItems[0]); if(SeriesRenderItems != null && SeriesRenderItems.Count > 0) { ChartRenderer.RenderItems.Add(SeriesRenderItems[0]); } - //SeriesRenderItems.ForEach(x => ChartRenderer.Plotarea.Group.AddChildItem(x)); - //renderItems.AddRange(ChartAreaRenderItems); - //SeriesRenderItems.ForEach(x => _groupItem.AddChildItem(x)); } } } From e3695a8e1d37481d2bb1e34bb531b0d62c424084 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ossian=20Edstr=C3=B6m?= Date: Mon, 22 Jun 2026 13:33:18 +0200 Subject: [PATCH 63/82] Start of adding svg to html export --- .../SvgStandAloneTests.cs | 3 -- .../HtmlExport/Enums/eDrawingInclude.cs | 33 +++++++++++++ .../Internal/AbstractRangeExporter.cs | 36 +++++++++++++- .../Exporters/Internal/CssExporterBase.cs | 2 +- .../Internal/HtmlExporterBaseInternal.cs | 47 ++++++++++++++++++- .../Internal/HtmlTableExporterBase.cs | 2 +- src/EPPlus/Export/HtmlExport/HtmlDrawing.cs | 21 +++++++++ src/EPPlus/Export/HtmlExport/HtmlElements.cs | 1 + src/EPPlus/Export/HtmlExport/HtmlImage.cs | 11 +---- .../Export/HtmlExport/HtmlSvgDrawing.cs | 13 +++++ .../Settings/HtmlDrawingSettings.cs | 28 +++++++++++ .../HtmlExport/Settings/HtmlExportSettings.cs | 7 +++ .../Export/HtmlExport/SvgShapeExportTests.cs | 33 +++++++++++++ 13 files changed, 219 insertions(+), 18 deletions(-) create mode 100644 src/EPPlus/Export/HtmlExport/Enums/eDrawingInclude.cs create mode 100644 src/EPPlus/Export/HtmlExport/HtmlDrawing.cs create mode 100644 src/EPPlus/Export/HtmlExport/HtmlSvgDrawing.cs create mode 100644 src/EPPlus/Export/HtmlExport/Settings/HtmlDrawingSettings.cs create mode 100644 src/EPPlusTest/Export/HtmlExport/SvgShapeExportTests.cs diff --git a/src/EPPlus.DrawingRenderer.Tests/DrawingShapeRenderer/SvgStandAloneTests.cs b/src/EPPlus.DrawingRenderer.Tests/DrawingShapeRenderer/SvgStandAloneTests.cs index bf24ef0fd7..6bdb9e74b8 100644 --- a/src/EPPlus.DrawingRenderer.Tests/DrawingShapeRenderer/SvgStandAloneTests.cs +++ b/src/EPPlus.DrawingRenderer.Tests/DrawingShapeRenderer/SvgStandAloneTests.cs @@ -4,11 +4,8 @@ using EPPlus.Export.ImageRenderer.RenderItems.SvgItem; using EPPlus.Fonts.OpenType.Integration.DataHolders; using EPPlus.Graphics; -using OfficeOpenXml.FormulaParsing.Excel.Functions.Engineering; -using OfficeOpenXml.FormulaParsing.Excel.Functions.MathFunctions; using System.Drawing; using System.Text; -using static OfficeOpenXml.Drawing.OleObject.Structures.OleObjectDataStructures; namespace EPPlus.Export.ImageRenderer.Tests.DrawingShapeRenderer { 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..151fbae2a8 100644 --- a/src/EPPlus/Export/HtmlExport/Exporters/Internal/AbstractRangeExporter.cs +++ b/src/EPPlus/Export/HtmlExport/Exporters/Internal/AbstractRangeExporter.cs @@ -27,6 +27,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 +46,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 +78,25 @@ internal void LoadRangeImages(List ranges) ToColumnOff = toColOff }); } + 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 + }); + } } } } @@ -114,5 +135,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..d2a9f086c4 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,36 @@ 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.As.Shape.ToSvg(); + //ePictureType? type; + //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/EPPlusTest/Export/HtmlExport/SvgShapeExportTests.cs b/src/EPPlusTest/Export/HtmlExport/SvgShapeExportTests.cs new file mode 100644 index 0000000000..751d75db01 --- /dev/null +++ b/src/EPPlusTest/Export/HtmlExport/SvgShapeExportTests.cs @@ -0,0 +1,33 @@ +using Microsoft.VisualStudio.TestTools.UnitTesting; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using OfficeOpenXml.Export.HtmlExport; + +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"); + ws.Drawings.AddShape("SimpleRect", OfficeOpenXml.Drawing.eShapeStyle.Rect); + + var exporter = ws.Cells["A1:A20"].CreateHtmlExporter(); + + exporter.Settings.Drawings.Include = ePictureInclude.IncludeInHtmlOnly; + exporter.Settings.Drawings.DrawTypeInclude = eDrawingInclude.Shapes; + + var htmlPage = exporter.GetSinglePage(); + + GetOutputFile("html", "svgRect"); + } + } + } +} From 64b8bbb7bd9a69de6570eb660c715e5df19a74b1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jan=20K=C3=A4llman?= Date: Mon, 22 Jun 2026 14:54:51 +0200 Subject: [PATCH 64/82] Render of Errorbars for line charts --- .../RenderItems/RenderItem.cs | 2 +- .../Renderer/Chart/ChartAxisRenderer.cs | 48 ++---- .../BarColumnChartTypeDrawer.cs | 2 +- .../ChartTypeDrawers/ChartErrorBarRenderer.cs | 146 +++++++++++++++--- .../Chart/ChartTypeDrawers/ChartTypeDrawer.cs | 3 +- .../ChartTypeDrawers/LineChartTypeDrawer.cs | 4 +- .../DrawingRenderItemExtentions.cs | 62 ++++---- 7 files changed, 180 insertions(+), 87 deletions(-) diff --git a/src/EPPlus.DrawingRenderer/RenderItems/RenderItem.cs b/src/EPPlus.DrawingRenderer/RenderItems/RenderItem.cs index 9befe6cb01..73babf0adb 100644 --- a/src/EPPlus.DrawingRenderer/RenderItems/RenderItem.cs +++ b/src/EPPlus.DrawingRenderer/RenderItems/RenderItem.cs @@ -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/Drawing/Renderer/Chart/ChartAxisRenderer.cs b/src/EPPlus/Drawing/Renderer/Chart/ChartAxisRenderer.cs index d665ab1fef..72a058173f 100644 --- a/src/EPPlus/Drawing/Renderer/Chart/ChartAxisRenderer.cs +++ b/src/EPPlus/Drawing/Renderer/Chart/ChartAxisRenderer.cs @@ -92,48 +92,15 @@ internal ChartAxisRenderer(ChartRenderer sc, ExcelChartAxisStandard ax) : base(s 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 (aav == eActualAxisPosition.Right) - //{ - // if (sc.SecondVerticalAxis?.Axis?.ActualAxisPosition == eActualAxisPosition.RightSecond) - // { - // lp -= sc.SecondVerticalAxis.Rectangle.Width; - // } - // else - // { - // } - //} - //else - //{ - - //} - //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); - //var rb = 0D; - //if (aav == eActualAxisPosition.Bottom && sc.SecondHorizontalAxis?.Axis.ActualAxisPosition==eActualAxisPosition.BottomSecond) - //{ - // rb = sc.SecondHorizontalAxis.Rectangle.Height; - //} - - //Rectangle.Top = Title == null || ax.AxisPosition == eAxisPosition.Top ? sc.ChartArea.Rectangle.Height - 8 - Rectangle.Height : Title.Rectangle.Top - Rectangle.Height - 8; } } @@ -1134,6 +1101,21 @@ private void AdjustminMaxFromChartObjects(ExcelChartAxisStandard ax, ref double? } } } + if(drawer.SupportsErrorBars) + { + 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/ChartTypeDrawers/BarColumnChartTypeDrawer.cs b/src/EPPlus/Drawing/Renderer/Chart/ChartTypeDrawers/BarColumnChartTypeDrawer.cs index 2953ea65ca..4f47ee4c90 100644 --- a/src/EPPlus/Drawing/Renderer/Chart/ChartTypeDrawers/BarColumnChartTypeDrawer.cs +++ b/src/EPPlus/Drawing/Renderer/Chart/ChartTypeDrawers/BarColumnChartTypeDrawer.cs @@ -20,7 +20,7 @@ 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>(); diff --git a/src/EPPlus/Drawing/Renderer/Chart/ChartTypeDrawers/ChartErrorBarRenderer.cs b/src/EPPlus/Drawing/Renderer/Chart/ChartTypeDrawers/ChartErrorBarRenderer.cs index 16e156e31d..25c666a616 100644 --- a/src/EPPlus/Drawing/Renderer/Chart/ChartTypeDrawers/ChartErrorBarRenderer.cs +++ b/src/EPPlus/Drawing/Renderer/Chart/ChartTypeDrawers/ChartErrorBarRenderer.cs @@ -4,6 +4,7 @@ using EPPlusImageRenderer.RenderItems; using EPPlusImageRenderer.Svg; using OfficeOpenXml.Drawing.Chart; +using OfficeOpenXml.FormulaParsing.Excel.Functions.MathFunctions; using OfficeOpenXml.Utils.TypeConversion; using System; using System.Collections.Generic; @@ -43,21 +44,20 @@ public ChartErrorBarRenderer(ChartRenderer svgChart, ExcelChartErrorBars errorba 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++) + 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 - sampleStdDev, y + sampleStdDev }); + Values.Add(new double[] { y - se, y, y + se }); } else { var mult = errorbars.Value ?? 1D; - sampleStdDev*= mult; - Values.Add(new double[] { avg - sampleStdDev, avg + sampleStdDev }); + Values.Add(new double[] { avg - sampleStdDev, avg, avg + sampleStdDev }); } } } @@ -67,7 +67,7 @@ public ChartErrorBarRenderer(ChartRenderer svgChart, ExcelChartErrorBars errorba for (int i = 0; i < _xSerie.Count; i++) { double y = _ySerie[i]; - Values.Add(new double[] { y * (1 - percent), y + (1 + percent) }); + Values.Add(new double[] { y * (1 - percent), y, y * (1 + percent) }); } break; case eErrorValueType.FixedValue: @@ -75,7 +75,7 @@ public ChartErrorBarRenderer(ChartRenderer svgChart, ExcelChartErrorBars errorba for (int i = 0; i < _xSerie.Count; i++) { double y = _ySerie[i]; - Values.Add(new double[] { y - fixedValue, y + fixedValue }); + Values.Add(new double[] { y - fixedValue, y, y + fixedValue }); } break; @@ -87,7 +87,7 @@ public ChartErrorBarRenderer(ChartRenderer svgChart, ExcelChartErrorBars errorba double y = _ySerie[i]; double minus = GetCustomValue(minusList, i); double plus = GetCustomValue(plusList, i); - Values.Add(new double[] { y - minus, y + plus }); + Values.Add(new double[] { y - minus, y, y + plus }); } break; @@ -105,35 +105,135 @@ public double GetCustomValue(List l, int i) } return 0D; } - public override void AppendRenderItems(List renderItems) - { - throw new NotImplementedException(); - } - internal RenderItem GetErrorBarRenderItem(int index, ChartAxisRenderer xAxis, ChartAxisRenderer yAxis, double x, double y) + internal List GetErrorBarRenderItem(int index, ChartAxisRenderer xAxis, ChartAxisRenderer yAxis, double x, double y, double xPos, double yPos) { - var path = new PathRenderItem(ChartRenderer.Bounds); - double addTop=0, addBottom=0; + //var path = new PathRenderItem(ChartRenderer.Bounds); + var l = new List(); + double topValue=0, bottomValue=0; if (_errorbars.BarType == eErrorBarType.Plus || _errorbars.BarType == eErrorBarType.Both) { - addTop = Values[index][1]; + topValue = Values[index][2]; } if(_errorbars.BarType == eErrorBarType.Minus || _errorbars.BarType == eErrorBarType.Both) { - addBottom = Values[index][0]; + bottomValue = Values[index][0]; } - if(_errorbars.Direction == eErrorBarDirection.X) + if (_errorbars.Direction == eErrorBarDirection.X) { - path.Commands.Add(new PathCommands(PathCommandType.Move, xAxis.GetPositionInPlotarea(x - addBottom), yAxis.GetPositionInPlotarea(y))); - path.Commands.Add(new PathCommands(PathCommandType.Line, xAxis.GetPositionInPlotarea(x + addTop), yAxis.GetPositionInPlotarea(y))); + 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 { - path.Commands.Add(new PathCommands(PathCommandType.Move, xAxis.GetPositionInPlotarea(x), yAxis.GetPositionInPlotarea(y - addBottom))); - path.Commands.Add(new PathCommands(PathCommandType.Line, xAxis.GetPositionInPlotarea(x), yAxis.GetPositionInPlotarea(y + addTop))); + 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 path; + return l; } } } diff --git a/src/EPPlus/Drawing/Renderer/Chart/ChartTypeDrawers/ChartTypeDrawer.cs b/src/EPPlus/Drawing/Renderer/Chart/ChartTypeDrawers/ChartTypeDrawer.cs index 7c454c747b..96b91846d8 100644 --- a/src/EPPlus/Drawing/Renderer/Chart/ChartTypeDrawers/ChartTypeDrawer.cs +++ b/src/EPPlus/Drawing/Renderer/Chart/ChartTypeDrawers/ChartTypeDrawer.cs @@ -213,8 +213,7 @@ protected void CreateErrorBars(ExcelChart chartType, List> xValues, { var xSerie = xValues[serieIndex]; var ySerie = yValues[serieIndex]; - ErrorBars = new ChartErrorBarRenderer(ChartRenderer, serie.ErrorBars, xSerie, ySerie, _chartType, serieIndex); - + ErrorBars = new ChartErrorBarRenderer(ChartRenderer, serie.ErrorBars, xSerie, ySerie, _chartType, serieIndex); } serieIndex++; } diff --git a/src/EPPlus/Drawing/Renderer/Chart/ChartTypeDrawers/LineChartTypeDrawer.cs b/src/EPPlus/Drawing/Renderer/Chart/ChartTypeDrawers/LineChartTypeDrawer.cs index d0f7a42771..4371e3883b 100644 --- a/src/EPPlus/Drawing/Renderer/Chart/ChartTypeDrawers/LineChartTypeDrawer.cs +++ b/src/EPPlus/Drawing/Renderer/Chart/ChartTypeDrawers/LineChartTypeDrawer.cs @@ -21,6 +21,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(); @@ -169,7 +170,7 @@ private void AddLine(ExcelChart chartType, ExcelLineChartSerie serie, List Date: Mon, 22 Jun 2026 14:57:02 +0200 Subject: [PATCH 65/82] Added html tests --- .../Internal/AbstractRangeExporter.cs | 22 +++- .../Internal/HtmlExporterBaseInternal.cs | 3 +- .../Export/HtmlExport/SvgShapeExportTests.cs | 112 +++++++++++++++++- 3 files changed, 131 insertions(+), 6 deletions(-) diff --git a/src/EPPlus/Export/HtmlExport/Exporters/Internal/AbstractRangeExporter.cs b/src/EPPlus/Export/HtmlExport/Exporters/Internal/AbstractRangeExporter.cs index 151fbae2a8..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; @@ -78,7 +79,7 @@ internal void LoadRangeDrawings(List ranges) ToColumnOff = toColOff }); } - if(d is ExcelShape s) + 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); @@ -97,6 +98,25 @@ internal void LoadRangeDrawings(List ranges) 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 + }); + } } } } diff --git a/src/EPPlus/Export/HtmlExport/Exporters/Internal/HtmlExporterBaseInternal.cs b/src/EPPlus/Export/HtmlExport/Exporters/Internal/HtmlExporterBaseInternal.cs index d2a9f086c4..ea81ac73cd 100644 --- a/src/EPPlus/Export/HtmlExport/Exporters/Internal/HtmlExporterBaseInternal.cs +++ b/src/EPPlus/Export/HtmlExport/Exporters/Internal/HtmlExporterBaseInternal.cs @@ -458,8 +458,7 @@ protected void AddDrawing(HTMLElement parent, HtmlExportSettings settings, HtmlS if (settings.Drawings.Include == ePictureInclude.IncludeInHtmlOnly) { child.ElementName = "div"; - child.Content = d.Drawing.As.Shape.ToSvg(); - //ePictureType? type; + child.Content = d.Drawing.ToSvg(); //var _encodedImage = ImageEncoder.EncodeImage(image, out type); //child.AddAttribute("src", $"data:{GetContentType(type.Value)};base64,{_encodedImage}"); diff --git a/src/EPPlusTest/Export/HtmlExport/SvgShapeExportTests.cs b/src/EPPlusTest/Export/HtmlExport/SvgShapeExportTests.cs index 751d75db01..f4b7585473 100644 --- a/src/EPPlusTest/Export/HtmlExport/SvgShapeExportTests.cs +++ b/src/EPPlusTest/Export/HtmlExport/SvgShapeExportTests.cs @@ -4,6 +4,8 @@ using System.Linq; using System.Text; using OfficeOpenXml.Export.HtmlExport; +using System.IO; +using System.Drawing; namespace EPPlusTest.Export.HtmlExport { @@ -17,16 +19,120 @@ public void ExportBasicShapeWorksheet() using (var package = OpenPackage("HtmlBasicSvgShape.xlsx", true)) { var ws = package.Workbook.Worksheets.Add("ShapeWs"); - ws.Drawings.AddShape("SimpleRect", OfficeOpenXml.Drawing.eShapeStyle.Rect); + 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; - var exporter = ws.Cells["A1:A20"].CreateHtmlExporter(); + 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(); - GetOutputFile("html", "svgRect"); + 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:A10"]); + + 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"); + + SaveAndCleanup(package); + + File.WriteAllText(file.FullName, htmlPage); + } + } + + [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; + + 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"); + + SaveAndCleanup(package); + + File.WriteAllText(file.FullName, htmlPage); } } } From 0df0bcdc8494f9c61f534dcd90dda3decbe910cd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jan=20K=C3=A4llman?= Date: Mon, 22 Jun 2026 15:00:51 +0200 Subject: [PATCH 66/82] Fixed Errorbars issue --- src/EPPlus/Drawing/Renderer/Chart/ChartAxisRenderer.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/EPPlus/Drawing/Renderer/Chart/ChartAxisRenderer.cs b/src/EPPlus/Drawing/Renderer/Chart/ChartAxisRenderer.cs index 72a058173f..434d9a25eb 100644 --- a/src/EPPlus/Drawing/Renderer/Chart/ChartAxisRenderer.cs +++ b/src/EPPlus/Drawing/Renderer/Chart/ChartAxisRenderer.cs @@ -1101,7 +1101,7 @@ private void AdjustminMaxFromChartObjects(ExcelChartAxisStandard ax, ref double? } } } - if(drawer.SupportsErrorBars) + if(drawer.SupportsErrorBars && drawer.ErrorBars!=null) { foreach(var v in drawer.ErrorBars.Values) { From 7c46aaad4c0405c803f46612f23f3499b8312d57 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ossian=20Edstr=C3=B6m?= Date: Mon, 22 Jun 2026 15:01:12 +0200 Subject: [PATCH 67/82] Added category test --- src/EPPlusTest/Export/HtmlExport/SvgShapeExportTests.cs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/EPPlusTest/Export/HtmlExport/SvgShapeExportTests.cs b/src/EPPlusTest/Export/HtmlExport/SvgShapeExportTests.cs index f4b7585473..da5a297bb1 100644 --- a/src/EPPlusTest/Export/HtmlExport/SvgShapeExportTests.cs +++ b/src/EPPlusTest/Export/HtmlExport/SvgShapeExportTests.cs @@ -120,6 +120,7 @@ public void ExportBarChartWithCategories() 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(); @@ -128,7 +129,7 @@ public void ExportBarChartWithCategories() var htmlPage = exporter.GetSinglePage(); - var file = GetOutputFile("html", "myColChart.html"); + var file = GetOutputFile("html", "colChartCats.html"); SaveAndCleanup(package); From 15fbf9d258c4ac6e1ccebd60c9d95c77abed1f51 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ossian=20Edstr=C3=B6m?= Date: Mon, 22 Jun 2026 15:23:10 +0200 Subject: [PATCH 68/82] Fixed gapwidth for barcharts --- .../Chart/ChartTypeDrawers/BarColumnChartTypeDrawer.cs | 5 ++++- .../Export/HtmlExport/SvgShapeExportTests.cs | 10 +++++----- 2 files changed, 9 insertions(+), 6 deletions(-) diff --git a/src/EPPlus/Drawing/Renderer/Chart/ChartTypeDrawers/BarColumnChartTypeDrawer.cs b/src/EPPlus/Drawing/Renderer/Chart/ChartTypeDrawers/BarColumnChartTypeDrawer.cs index f65c204106..14ca1d7c4a 100644 --- a/src/EPPlus/Drawing/Renderer/Chart/ChartTypeDrawers/BarColumnChartTypeDrawer.cs +++ b/src/EPPlus/Drawing/Renderer/Chart/ChartTypeDrawers/BarColumnChartTypeDrawer.cs @@ -173,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); diff --git a/src/EPPlusTest/Export/HtmlExport/SvgShapeExportTests.cs b/src/EPPlusTest/Export/HtmlExport/SvgShapeExportTests.cs index da5a297bb1..ed5b7c9658 100644 --- a/src/EPPlusTest/Export/HtmlExport/SvgShapeExportTests.cs +++ b/src/EPPlusTest/Export/HtmlExport/SvgShapeExportTests.cs @@ -20,9 +20,9 @@ public void ExportBasicShapeWorksheet() { 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; + //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++) { @@ -74,8 +74,8 @@ public void ExportBarChart() 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.Fill.Color = System.Drawing.Color.BlanchedAlmond; + //chart.Series[0].Fill.Color = System.Drawing.Color.LightCoral; var exporter = ws.Cells["A1:C20"].CreateHtmlExporter(); From 03272838c5bb3a90e0abde6bd5ef5b4042e4ef70 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jan=20K=C3=A4llman?= Date: Mon, 22 Jun 2026 15:58:15 +0200 Subject: [PATCH 69/82] Strong named EPPlus.DrawingRenderer and added tests for .NET 4.6.2. --- .../EPPlus.DrawingRenderer.Tests.csproj | 2 +- .../TestFontMeasurer.cs | 4 ++-- .../EPPlus.DrawingRenderer.csproj | 2 ++ .../EPPlus.DrawingRenderer.snk | Bin 0 -> 596 bytes .../Properties/AssemblyInfo.cs | 15 +++++++++++++++ 5 files changed, 20 insertions(+), 3 deletions(-) create mode 100644 src/EPPlus.DrawingRenderer/EPPlus.DrawingRenderer.snk create mode 100644 src/EPPlus.DrawingRenderer/Properties/AssemblyInfo.cs 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/TestFontMeasurer.cs b/src/EPPlus.DrawingRenderer.Tests/TestFontMeasurer.cs index 80733eb230..27a5333788 100644 --- a/src/EPPlus.DrawingRenderer.Tests/TestFontMeasurer.cs +++ b/src/EPPlus.DrawingRenderer.Tests/TestFontMeasurer.cs @@ -260,7 +260,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(); @@ -311,7 +311,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/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 0000000000000000000000000000000000000000..8a53d24382a044ae914e160a05e53f7625c1a966 GIT binary patch literal 596 zcmV-a0;~N80ssI2Bme+XQ$aES1ONa50097fJ;!cPIJOqGWGm$>^v-5Xe~SVO&SgET zT_sv5R7KB6X zMTR`jsp@jQW*CQI>D6$+V_IrGu{ugVqpw%>^aWdmnEG*b(AsDAvCnaYGTG`nnYqum zOvko&kWAVYnKHv;qnZxWh(Gb~UxU4C8FcCj%`V29I^4;=*^#j&(m=`9{c^`C7yp8r zLY%3)Gcl%Aj>z|GJCsM5M^V_B%kl{QaHoXgMX_N{G8Su93z?J>4I>3(;98{Sbk3=$@`I8=Z5ug14ff5 zQM2%4g!NZ{BG3m92wePhx_ap~FbSDsF2V_?jA4scaVuKP$5>$VH*j|XBE(7f zAKnLW>5C{matzN_&WLzP=I~ehq+NPw>2{_oHtiV!tlRI}YRy)dy@ Date: Mon, 22 Jun 2026 15:58:35 +0200 Subject: [PATCH 70/82] Added stylemanager styling --- .../RenderItems/DrawingRenderItemExtentions.cs | 14 +++++++------- .../Export/HtmlExport/SvgShapeExportTests.cs | 18 ++++++++++++------ 2 files changed, 19 insertions(+), 13 deletions(-) diff --git a/src/EPPlus/Drawing/Renderer/RenderItems/DrawingRenderItemExtentions.cs b/src/EPPlus/Drawing/Renderer/RenderItems/DrawingRenderItemExtentions.cs index 44e8a4806d..2654e873c2 100644 --- a/src/EPPlus/Drawing/Renderer/RenderItems/DrawingRenderItemExtentions.cs +++ b/src/EPPlus/Drawing/Renderer/RenderItems/DrawingRenderItemExtentions.cs @@ -53,14 +53,14 @@ internal static void SetDrawingPropertiesFillBasic(this RenderItem item, ExcelTh switch (fill.Style) { case eFillStyle.NoFill: - //if (fill.IsEmpty) //Removed for now. - //{ - // item.FillColor = GetFillColor(theme, fill, color, item.FillColorSource, out opacity); - //} - //else - //{ + if (fill.IsEmpty) //Removed for now. + { + item.FillColor = GetFillColor(theme, fill, color, item.FillColorSource, out opacity); + } + else + { item.FillColor = "none"; - //} + } break; case eFillStyle.SolidFill: item.FillColor = GetFillColor(theme, fill, color, item.FillColorSource, out opacity); diff --git a/src/EPPlusTest/Export/HtmlExport/SvgShapeExportTests.cs b/src/EPPlusTest/Export/HtmlExport/SvgShapeExportTests.cs index ed5b7c9658..7be5aab573 100644 --- a/src/EPPlusTest/Export/HtmlExport/SvgShapeExportTests.cs +++ b/src/EPPlusTest/Export/HtmlExport/SvgShapeExportTests.cs @@ -1,11 +1,12 @@ 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; -using OfficeOpenXml.Export.HtmlExport; -using System.IO; -using System.Drawing; namespace EPPlusTest.Export.HtmlExport { @@ -72,8 +73,11 @@ public void ExportBarChart() } var chart = ws.Drawings.AddBarChart("myColChart", OfficeOpenXml.Drawing.Chart.eBarChartType.ColumnClustered); - chart.Series.Add(ws.Cells["A1:A10"]); + chart.Series.Add(ws.Cells["A1:A8"]); + var theme = ws.Workbook.ThemeManager.GetOrCreateTheme(); + chart.StyleManager.SetChartStyle(ePresetChartStyle.BarChartStyle1, ePresetChartColors.MonochromaticPalette1); + //chart.StyleManager.SetChartStyle(ePresetChartStyle.ColumnChartStyle1, ePresetChartColors.ColorfulPalette1); //chart.Fill.Color = System.Drawing.Color.BlanchedAlmond; //chart.Series[0].Fill.Color = System.Drawing.Color.LightCoral; @@ -85,10 +89,12 @@ public void ExportBarChart() var htmlPage = exporter.GetSinglePage(); var file = GetOutputFile("html", "myColChart.html"); - - SaveAndCleanup(package); + var svgFile = GetOutputFile("html", "myColChartSvg.svg"); File.WriteAllText(file.FullName, htmlPage); + File.WriteAllText(svgFile.FullName, chart.ToSvg()); + + SaveAndCleanup(package); } } From 1922b10542abc5e4baf1bd27cd57ed1504c9bcd9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ossian=20Edstr=C3=B6m?= Date: Mon, 22 Jun 2026 17:04:15 +0200 Subject: [PATCH 71/82] Fixed ellipse items having rect type --- src/EPPlus.DrawingRenderer.Tests/StyleTests.cs | 16 ++++++++++++++++ .../RenderItems/RenderItem.cs | 2 +- .../RenderItems/DrawingRenderItemExtentions.cs | 2 +- .../Export/HtmlExport/SvgShapeExportTests.cs | 10 +++++++--- 4 files changed, 25 insertions(+), 5 deletions(-) diff --git a/src/EPPlus.DrawingRenderer.Tests/StyleTests.cs b/src/EPPlus.DrawingRenderer.Tests/StyleTests.cs index d98b49f066..07bdb81fd2 100644 --- a/src/EPPlus.DrawingRenderer.Tests/StyleTests.cs +++ b/src/EPPlus.DrawingRenderer.Tests/StyleTests.cs @@ -22,6 +22,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\\baseThemeChartStyle.svg", svg); + } + } + + [TestMethod] public void ExtractThemeStyleWorks() { diff --git a/src/EPPlus.DrawingRenderer/RenderItems/RenderItem.cs b/src/EPPlus.DrawingRenderer/RenderItems/RenderItem.cs index 73babf0adb..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; } diff --git a/src/EPPlus/Drawing/Renderer/RenderItems/DrawingRenderItemExtentions.cs b/src/EPPlus/Drawing/Renderer/RenderItems/DrawingRenderItemExtentions.cs index 2654e873c2..922d468322 100644 --- a/src/EPPlus/Drawing/Renderer/RenderItems/DrawingRenderItemExtentions.cs +++ b/src/EPPlus/Drawing/Renderer/RenderItems/DrawingRenderItemExtentions.cs @@ -53,7 +53,7 @@ internal static void SetDrawingPropertiesFillBasic(this RenderItem item, ExcelTh switch (fill.Style) { case eFillStyle.NoFill: - if (fill.IsEmpty) //Removed for now. + if (fill.IsEmpty) //Do NOT remove. This if is required for Shapes { item.FillColor = GetFillColor(theme, fill, color, item.FillColorSource, out opacity); } diff --git a/src/EPPlusTest/Export/HtmlExport/SvgShapeExportTests.cs b/src/EPPlusTest/Export/HtmlExport/SvgShapeExportTests.cs index 7be5aab573..d65926a4fa 100644 --- a/src/EPPlusTest/Export/HtmlExport/SvgShapeExportTests.cs +++ b/src/EPPlusTest/Export/HtmlExport/SvgShapeExportTests.cs @@ -75,9 +75,13 @@ public void ExportBarChart() var chart = ws.Drawings.AddBarChart("myColChart", OfficeOpenXml.Drawing.Chart.eBarChartType.ColumnClustered); chart.Series.Add(ws.Cells["A1:A8"]); - var theme = ws.Workbook.ThemeManager.GetOrCreateTheme(); - chart.StyleManager.SetChartStyle(ePresetChartStyle.BarChartStyle1, ePresetChartColors.MonochromaticPalette1); - //chart.StyleManager.SetChartStyle(ePresetChartStyle.ColumnChartStyle1, ePresetChartColors.ColorfulPalette1); + 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; From 8dcdd7b29a7cb4ddc892bc7f17ac5c0c69d94380 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jan=20K=C3=A4llman?= Date: Tue, 23 Jun 2026 08:25:45 +0200 Subject: [PATCH 72/82] Fixed trendline label positioning --- .../Chart/ChartTypeDrawers/LineChartTypeDrawer.cs | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/src/EPPlus/Drawing/Renderer/Chart/ChartTypeDrawers/LineChartTypeDrawer.cs b/src/EPPlus/Drawing/Renderer/Chart/ChartTypeDrawers/LineChartTypeDrawer.cs index 4371e3883b..1c368a47d7 100644 --- a/src/EPPlus/Drawing/Renderer/Chart/ChartTypeDrawers/LineChartTypeDrawer.cs +++ b/src/EPPlus/Drawing/Renderer/Chart/ChartTypeDrawers/LineChartTypeDrawer.cs @@ -11,6 +11,7 @@ using OfficeOpenXml.Utils.TypeConversion; using System; using System.Collections.Generic; +using System.Linq; namespace EPPlus.Export.ImageRenderer.Svg.Chart { @@ -33,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()) { From 5ee696adbc03e63e2a29f80f4af500508bae5618 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jan=20K=C3=A4llman?= Date: Tue, 23 Jun 2026 10:49:32 +0200 Subject: [PATCH 73/82] Fixed trendline label positioning. --- .../Chart/ErrorbarsTests.cs | 20 +++++++++++++++++++ .../Trendlines/ChartTrendlineRenderer.cs | 6 +++--- 2 files changed, 23 insertions(+), 3 deletions(-) diff --git a/src/EPPlus.DrawingRenderer.Tests/Chart/ErrorbarsTests.cs b/src/EPPlus.DrawingRenderer.Tests/Chart/ErrorbarsTests.cs index 5b67b2c388..a3a2e76f64 100644 --- a/src/EPPlus.DrawingRenderer.Tests/Chart/ErrorbarsTests.cs +++ b/src/EPPlus.DrawingRenderer.Tests/Chart/ErrorbarsTests.cs @@ -26,5 +26,25 @@ public void GenerateSvgForErrorbars_Sheet1() } } } + [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/Drawing/Renderer/Chart/ChartTypeDrawers/Trendlines/ChartTrendlineRenderer.cs b/src/EPPlus/Drawing/Renderer/Chart/ChartTypeDrawers/Trendlines/ChartTrendlineRenderer.cs index e3f400a22a..86ee433cc9 100644 --- a/src/EPPlus/Drawing/Renderer/Chart/ChartTypeDrawers/Trendlines/ChartTrendlineRenderer.cs +++ b/src/EPPlus/Drawing/Renderer/Chart/ChartTypeDrawers/Trendlines/ChartTrendlineRenderer.cs @@ -159,9 +159,9 @@ private void CreateDatalabel() 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 { @@ -493,7 +493,7 @@ 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} "; From 8f99a327a8d0e87fdf0b2b03c86e3778a2b964b9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ossian=20Edstr=C3=B6m?= Date: Tue, 23 Jun 2026 11:01:35 +0200 Subject: [PATCH 74/82] Fixed databar bug + styling border issue --- .../Chart/BarChartTests.cs | 2 +- .../StyleTests.cs | 34 ++++++++++++++++--- src/EPPlus/Drawing/Enums/eSchemeColor.cs | 1 + .../BarColumnChartTypeDrawer.cs | 6 ++-- .../Chart/DataLabels/SvgDataLabelPoint.cs | 2 +- .../DrawingRenderItemExtentions.cs | 3 +- .../Utils/TypeConversion/ColorConverter.cs | 21 ++++++++++++ 7 files changed, 59 insertions(+), 10 deletions(-) diff --git a/src/EPPlus.DrawingRenderer.Tests/Chart/BarChartTests.cs b/src/EPPlus.DrawingRenderer.Tests/Chart/BarChartTests.cs index 25c529bcfb..cbecdc4035 100644 --- a/src/EPPlus.DrawingRenderer.Tests/Chart/BarChartTests.cs +++ b/src/EPPlus.DrawingRenderer.Tests/Chart/BarChartTests.cs @@ -51,7 +51,7 @@ public void DatalabelBarCharts() { var ws = p.Workbook.Worksheets[0]; var drawings = ws.Drawings; - var ix = 0; + var ix = 1; for (int i = ix; i < drawings.Count; i++) { diff --git a/src/EPPlus.DrawingRenderer.Tests/StyleTests.cs b/src/EPPlus.DrawingRenderer.Tests/StyleTests.cs index 07bdb81fd2..8fd968a2f2 100644 --- a/src/EPPlus.DrawingRenderer.Tests/StyleTests.cs +++ b/src/EPPlus.DrawingRenderer.Tests/StyleTests.cs @@ -67,18 +67,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; @@ -86,7 +88,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); @@ -103,6 +105,30 @@ 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); + SaveAndCleanup(p); + } + + } + [TestMethod] public void TextRunIsStyledButNotTitleFont() { 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/ChartTypeDrawers/BarColumnChartTypeDrawer.cs b/src/EPPlus/Drawing/Renderer/Chart/ChartTypeDrawers/BarColumnChartTypeDrawer.cs index 14ca1d7c4a..a2db076ba3 100644 --- a/src/EPPlus/Drawing/Renderer/Chart/ChartTypeDrawers/BarColumnChartTypeDrawer.cs +++ b/src/EPPlus/Drawing/Renderer/Chart/ChartTypeDrawers/BarColumnChartTypeDrawer.cs @@ -85,7 +85,7 @@ internal override void DrawSeries() { var middleRight = dataPoints[j].Left + (dataPoints[j].Width / 2); - if (chartBaseY >= dataPoints[j].Top) + if (chartBaseY <= dataPoints[j].Top) { //We are a negative column // ----- Base-Axis @@ -99,8 +99,8 @@ internal override void DrawSeries() // _ // | | Col // ----- Base-Axis - basePoint.Position = new Vector2(middleRight, dataPoints[j].Top); - endPoint.Position = new Vector2(middleRight, dataPoints[j].Bottom); + basePoint.Position = new Vector2(middleRight, dataPoints[j].Bottom); + endPoint.Position = new Vector2(middleRight, dataPoints[j].Top); } serieDataLabels[i].SetDimensions(j, basePoint, endPoint); diff --git a/src/EPPlus/Drawing/Renderer/Chart/DataLabels/SvgDataLabelPoint.cs b/src/EPPlus/Drawing/Renderer/Chart/DataLabels/SvgDataLabelPoint.cs index 2222af6c02..f1bcda64f5 100644 --- a/src/EPPlus/Drawing/Renderer/Chart/DataLabels/SvgDataLabelPoint.cs +++ b/src/EPPlus/Drawing/Renderer/Chart/DataLabels/SvgDataLabelPoint.cs @@ -353,7 +353,7 @@ internal void SetShapeDimensions(Transform basePoint, Transform endPoint) //And endVector is the top center position. //--- Visualize positions for debugging purposes - //CreateDebugPoints(basePoint, endPoint, centerPoint); + CreateDebugPoints(basePoint, endPoint, centerPoint); //--- switch (_labelPosition) diff --git a/src/EPPlus/Drawing/Renderer/RenderItems/DrawingRenderItemExtentions.cs b/src/EPPlus/Drawing/Renderer/RenderItems/DrawingRenderItemExtentions.cs index 922d468322..0afa6e1965 100644 --- a/src/EPPlus/Drawing/Renderer/RenderItems/DrawingRenderItemExtentions.cs +++ b/src/EPPlus/Drawing/Renderer/RenderItems/DrawingRenderItemExtentions.cs @@ -198,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/Utils/TypeConversion/ColorConverter.cs b/src/EPPlus/Utils/TypeConversion/ColorConverter.cs index b25f4a8565..6b60f9d91a 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; From 2ecc4d139c0927c4232ae118703e998d80245f6e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jan=20K=C3=A4llman?= Date: Tue, 23 Jun 2026 12:49:30 +0200 Subject: [PATCH 75/82] Fixed null color for shapes and charts --- .../Drawing/Renderer/Chart/ChartPlotareaRenderer.cs | 2 +- src/EPPlus/Drawing/Renderer/Chart/LineMarkerHelper.cs | 2 +- src/EPPlus/Drawing/Renderer/ChartRenderer.cs | 2 +- .../Renderer/RenderItems/DrawingRenderItemExtentions.cs | 8 ++++---- 4 files changed, 7 insertions(+), 7 deletions(-) diff --git a/src/EPPlus/Drawing/Renderer/Chart/ChartPlotareaRenderer.cs b/src/EPPlus/Drawing/Renderer/Chart/ChartPlotareaRenderer.cs index e1abb1c709..5903e8ed14 100644 --- a/src/EPPlus/Drawing/Renderer/Chart/ChartPlotareaRenderer.cs +++ b/src/EPPlus/Drawing/Renderer/Chart/ChartPlotareaRenderer.cs @@ -59,7 +59,7 @@ 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.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; } diff --git a/src/EPPlus/Drawing/Renderer/Chart/LineMarkerHelper.cs b/src/EPPlus/Drawing/Renderer/Chart/LineMarkerHelper.cs index 53d2015c52..b0452efee1 100644 --- a/src/EPPlus/Drawing/Renderer/Chart/LineMarkerHelper.cs +++ b/src/EPPlus/Drawing/Renderer/Chart/LineMarkerHelper.cs @@ -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/ChartRenderer.cs b/src/EPPlus/Drawing/Renderer/ChartRenderer.cs index 02bcd06172..378b8d339e 100644 --- a/src/EPPlus/Drawing/Renderer/ChartRenderer.cs +++ b/src/EPPlus/Drawing/Renderer/ChartRenderer.cs @@ -306,7 +306,7 @@ 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.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); diff --git a/src/EPPlus/Drawing/Renderer/RenderItems/DrawingRenderItemExtentions.cs b/src/EPPlus/Drawing/Renderer/RenderItems/DrawingRenderItemExtentions.cs index 0afa6e1965..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,11 +43,11 @@ 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) @@ -55,7 +55,7 @@ internal static void SetDrawingPropertiesFillBasic(this RenderItem item, ExcelTh case eFillStyle.NoFill: 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 { From c2f3e88687df062756375f86e49fc8cd91ab0c2f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jan=20K=C3=A4llman?= Date: Tue, 23 Jun 2026 13:30:42 +0200 Subject: [PATCH 76/82] Fixed blip fills --- src/EPPlus.DrawingRenderer/Svg/IRender.cs | 2 +- src/EPPlus/Utils/Rendering/DrawingExtensions.cs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/EPPlus.DrawingRenderer/Svg/IRender.cs b/src/EPPlus.DrawingRenderer/Svg/IRender.cs index 88480d371e..ca3a62b34b 100644 --- a/src/EPPlus.DrawingRenderer/Svg/IRender.cs +++ b/src/EPPlus.DrawingRenderer/Svg/IRender.cs @@ -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/Utils/Rendering/DrawingExtensions.cs b/src/EPPlus/Utils/Rendering/DrawingExtensions.cs index d9242c41cb..7d93cfe143 100644 --- a/src/EPPlus/Utils/Rendering/DrawingExtensions.cs +++ b/src/EPPlus/Utils/Rendering/DrawingExtensions.cs @@ -146,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, From a60e584a3331eddc90ba5958ab2a4bdd74cdefb9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ossian=20Edstr=C3=B6m?= Date: Tue, 23 Jun 2026 18:46:07 +0200 Subject: [PATCH 77/82] Start of fix for title element --- .../Shape/ShapeToSvgTests.cs | 23 ++++++++++++- .../StyleTests.cs | 13 +++++--- src/EPPlus/Drawing/Chart/ExcelChartTitle.cs | 13 +++++--- .../Style/Text/ExcelDrawingParagraph.cs | 19 ++++++++++- .../Text/ExcelDrawingParagraphCollection.cs | 4 +-- .../RichText/ExcelParagraphCollection.cs | 32 ++++++++++++++++--- 6 files changed, 86 insertions(+), 18 deletions(-) diff --git a/src/EPPlus.DrawingRenderer.Tests/Shape/ShapeToSvgTests.cs b/src/EPPlus.DrawingRenderer.Tests/Shape/ShapeToSvgTests.cs index ce95ac8260..76f3640038 100644 --- a/src/EPPlus.DrawingRenderer.Tests/Shape/ShapeToSvgTests.cs +++ b/src/EPPlus.DrawingRenderer.Tests/Shape/ShapeToSvgTests.cs @@ -508,7 +508,7 @@ public void SuperScriptShape() //var c = ws.Drawings[ix]; //var svg = renderer.RenderDrawingToSvg(c); //SaveTextFileToWorkbook($"svg\\LineChartForSvg_Single{ix++}.svg", svg); - var ix = 1; + var ix = 0; foreach (var c in ws.Drawings) { var svg = c.ToSvg(); @@ -638,6 +638,27 @@ public void GenerateShapeCenteredParagraph() SaveAndCleanup(p); } } + + [TestMethod] + public void ChartAndShapeGreen() + { + ExcelPackage.License.SetNonCommercialOrganization("EPPlus Project"); + 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() { diff --git a/src/EPPlus.DrawingRenderer.Tests/StyleTests.cs b/src/EPPlus.DrawingRenderer.Tests/StyleTests.cs index 8fd968a2f2..57bb72132b 100644 --- a/src/EPPlus.DrawingRenderer.Tests/StyleTests.cs +++ b/src/EPPlus.DrawingRenderer.Tests/StyleTests.cs @@ -33,7 +33,7 @@ public void BaseThemeChartStyle2() var svg = c.ToSvg(); //var renderer = new EPPlusImageRenderer.ImageRenderer(); //var svg = renderer.RenderDrawingToSvg(c); - SaveTextFileToWorkbook($"svg\\baseThemeChartStyle.svg", svg); + SaveTextFileToWorkbook($"svg\\baseThemeChartStyle2.svg", svg); } } @@ -117,13 +117,16 @@ public void ExtractThemeStyleWorksDataLine() simpleChart.Title.Text = "Hello"; - var chartDefaultStyle = simpleChart.StyleManager.Style; - simpleChart.StyleManager.ApplyStyles(); + //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 svg = simpleChart.ToSvg(); + //SaveTextFileToWorkbook($"svg\\MyLineIonThemeExcelChangeOnlyTitle.svg", svg); + + var fontSize = simpleChart.Title.Font.Size; + SaveAndCleanup(p); } diff --git a/src/EPPlus/Drawing/Chart/ExcelChartTitle.cs b/src/EPPlus/Drawing/Chart/ExcelChartTitle.cs index 400f28e5b7..cf9759862b 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:bodyPr/a:p/a:pPr/a:defRPr", TopNode); _textBody = new ExcelTextBody(_chart, NameSpaceManager, TopNode, $"{_richTextPath}/a:bodyPr", SchemaNodeOrder); - //_textBody.Paragraphs.FirstDefaultRunProperties = firstDefaultRunProperties; - //_textBody.Paragraphs.FirstDefaultRunProperties = DefaultTextBody.Paragraphs.CreateOrGetDefaultRunProperties(); + _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() diff --git a/src/EPPlus/Drawing/Style/Text/ExcelDrawingParagraph.cs b/src/EPPlus/Drawing/Style/Text/ExcelDrawingParagraph.cs index b3410d654b..47d03d0567 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,13 +46,27 @@ 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); } else { - DefaultRunProperties = _paragraphs.FirstDefaultRunProperties; + if(paragraphs.Count == 0) + { + //The node must still be created + var xmlFirstDefault = ((ExcelTextFontXml)paragraphs.FirstDefaultRunProperties).XmlHelper; + var textFont = new ExcelTextFontXml(prd, nameSpaceManager, topNode, "a:pPr/a:defRPr", schemaNodeOrder, initXml); + var xmlNewNode = textFont.XmlHelper; + CopyElement((XmlElement)xmlFirstDefault.TopNode, (XmlElement)xmlNewNode.TopNode); + DefaultRunProperties = textFont; + } + else + { + DefaultRunProperties = _paragraphs.FirstDefaultRunProperties; + } } var normalStyle = _prd.Package.Workbook.Styles.GetNormalStyle(); 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/Style/RichText/ExcelParagraphCollection.cs b/src/EPPlus/Style/RichText/ExcelParagraphCollection.cs index ef13fff0a6..b0b6d31256 100644 --- a/src/EPPlus/Style/RichText/ExcelParagraphCollection.cs +++ b/src/EPPlus/Style/RichText/ExcelParagraphCollection.cs @@ -33,13 +33,13 @@ 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" }); + 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) //{ @@ -70,7 +70,7 @@ internal ExcelParagraphCollection(ExcelTextBody tb, ExcelDrawing drawing, XmlNa 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)); } @@ -95,7 +95,29 @@ private void AddParagraph(ExcelParagraphTextRunBase tr) return; } } - _list.Add(new ExcelParagraph(tr)); + if (_list.Count == 0) + { + var para = new ExcelParagraph(tr); + _list.Add(para); + + ////_textBody.Paragraphs[0].DefaultRunProperties = _textBody.Paragraphs.FirstDefaultRunProperties; + //var para = new ExcelParagraph(tr); + //_list.Add(para); + //var defRpr = tr.Paragraph._paragraphs.FirstDefaultRunProperties; + //var para = new ExcelParagraph(tr); + //var mf = defRpr.GetMeasureFont(); + //para.SetFromFont(mf.FontFamily, mf.Size, + // (mf.Style & MeasurementFontStyles.Bold) != 0, + // (mf.Style & MeasurementFontStyles.Italic) != 0, + // (mf.Style & MeasurementFontStyles.Underline) != 0, + // (mf.Style & MeasurementFontStyles.Strikeout) != 0); + //_list.Add(para); + } + else + { + _list.Add(new ExcelParagraph(tr)); + } + //_list.Add(new ExcelParagraph(tr)); } private void RemoveTextRun(ExcelParagraphTextRunBase textRun) { @@ -239,7 +261,7 @@ public string Text { if (_textBody.Paragraphs.Count == 0) { - Add(value); + Add(value, true); } else { From 08439a704675a7b4c5376837aa6d491617627929 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ossian=20Edstr=C3=B6m?= Date: Wed, 24 Jun 2026 09:47:24 +0200 Subject: [PATCH 78/82] Fixed title bug in epplus --- .../Style/Text/ExcelDrawingParagraph.cs | 15 ++++-- .../RichText/ExcelParagraphCollection.cs | 52 +------------------ 2 files changed, 14 insertions(+), 53 deletions(-) diff --git a/src/EPPlus/Drawing/Style/Text/ExcelDrawingParagraph.cs b/src/EPPlus/Drawing/Style/Text/ExcelDrawingParagraph.cs index 47d03d0567..ff8a9ad04b 100644 --- a/src/EPPlus/Drawing/Style/Text/ExcelDrawingParagraph.cs +++ b/src/EPPlus/Drawing/Style/Text/ExcelDrawingParagraph.cs @@ -57,10 +57,19 @@ internal ExcelDrawingParagraph(ExcelDrawingParagraphCollection paragraphs, IPict if(paragraphs.Count == 0) { //The node must still be created - var xmlFirstDefault = ((ExcelTextFontXml)paragraphs.FirstDefaultRunProperties).XmlHelper; + 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); - var xmlNewNode = textFont.XmlHelper; - CopyElement((XmlElement)xmlFirstDefault.TopNode, (XmlElement)xmlNewNode.TopNode); + + //Copy the first element and apply it to the paragraphProperties + CopyElement((XmlElement)xmlFirstDefault, (XmlElement)paragraphProperties); DefaultRunProperties = textFont; } else diff --git a/src/EPPlus/Style/RichText/ExcelParagraphCollection.cs b/src/EPPlus/Style/RichText/ExcelParagraphCollection.cs index b0b6d31256..0face72036 100644 --- a/src/EPPlus/Style/RichText/ExcelParagraphCollection.cs +++ b/src/EPPlus/Style/RichText/ExcelParagraphCollection.cs @@ -40,33 +40,8 @@ internal ExcelParagraphCollection(ExcelTextBody tb, ExcelDrawing drawing, XmlNam _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; - _path = path; + foreach (var p in tb.Paragraphs) { p.defaultAlignment = defaultAlignment; @@ -95,29 +70,7 @@ private void AddParagraph(ExcelParagraphTextRunBase tr) return; } } - if (_list.Count == 0) - { - var para = new ExcelParagraph(tr); - _list.Add(para); - - ////_textBody.Paragraphs[0].DefaultRunProperties = _textBody.Paragraphs.FirstDefaultRunProperties; - //var para = new ExcelParagraph(tr); - //_list.Add(para); - //var defRpr = tr.Paragraph._paragraphs.FirstDefaultRunProperties; - //var para = new ExcelParagraph(tr); - //var mf = defRpr.GetMeasureFont(); - //para.SetFromFont(mf.FontFamily, mf.Size, - // (mf.Style & MeasurementFontStyles.Bold) != 0, - // (mf.Style & MeasurementFontStyles.Italic) != 0, - // (mf.Style & MeasurementFontStyles.Underline) != 0, - // (mf.Style & MeasurementFontStyles.Strikeout) != 0); - //_list.Add(para); - } - else - { - _list.Add(new ExcelParagraph(tr)); - } - //_list.Add(new ExcelParagraph(tr)); + _list.Add(new ExcelParagraph(tr)); } private void RemoveTextRun(ExcelParagraphTextRunBase textRun) { @@ -174,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); From 396227bebf7e42bb0860bc590916b58e8e3ab272 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ossian=20Edstr=C3=B6m?= Date: Wed, 24 Jun 2026 11:33:15 +0200 Subject: [PATCH 79/82] Fixed one test-failure --- .../StyleTests.cs | 6 ++ src/EPPlus/Drawing/Chart/ExcelChartTitle.cs | 15 ++++- .../Style/Text/ExcelDrawingParagraph.cs | 59 ++++++++++++------- 3 files changed, 58 insertions(+), 22 deletions(-) diff --git a/src/EPPlus.DrawingRenderer.Tests/StyleTests.cs b/src/EPPlus.DrawingRenderer.Tests/StyleTests.cs index 57bb72132b..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 { @@ -127,6 +128,11 @@ public void ExtractThemeStyleWorksDataLine() 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); } diff --git a/src/EPPlus/Drawing/Chart/ExcelChartTitle.cs b/src/EPPlus/Drawing/Chart/ExcelChartTitle.cs index cf9759862b..055df57ece 100644 --- a/src/EPPlus/Drawing/Chart/ExcelChartTitle.cs +++ b/src/EPPlus/Drawing/Chart/ExcelChartTitle.cs @@ -149,7 +149,7 @@ 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 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; } @@ -444,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); + } } } /// diff --git a/src/EPPlus/Drawing/Style/Text/ExcelDrawingParagraph.cs b/src/EPPlus/Drawing/Style/Text/ExcelDrawingParagraph.cs index ff8a9ad04b..f482329dcd 100644 --- a/src/EPPlus/Drawing/Style/Text/ExcelDrawingParagraph.cs +++ b/src/EPPlus/Drawing/Style/Text/ExcelDrawingParagraph.cs @@ -54,28 +54,29 @@ internal ExcelDrawingParagraph(ExcelDrawingParagraphCollection paragraphs, IPict } else { - 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); + 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); + // //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; - } + // //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(); @@ -105,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) { From 3d451eb2bc1e331d77cc65fb1a8352610ae6baae Mon Sep 17 00:00:00 2001 From: swmal <897655+swmal@users.noreply.github.com> Date: Wed, 24 Jun 2026 15:45:13 +0200 Subject: [PATCH 80/82] Fixed failing tests for Archivo Narrow --- .../SvgStandAloneTests.cs | 14 ++++++------- .../FontResolver/DefaultFontResolverTests.cs | 16 +++++++++++++++ .../FontResolver/DefaultFontResolver.cs | 20 +++++++++++++++---- 3 files changed, 39 insertions(+), 11 deletions(-) diff --git a/src/EPPlus.DrawingRenderer.Tests/DrawingShapeRenderer/SvgStandAloneTests.cs b/src/EPPlus.DrawingRenderer.Tests/DrawingShapeRenderer/SvgStandAloneTests.cs index 6bdb9e74b8..a6b824b6de 100644 --- a/src/EPPlus.DrawingRenderer.Tests/DrawingShapeRenderer/SvgStandAloneTests.cs +++ b/src/EPPlus.DrawingRenderer.Tests/DrawingShapeRenderer/SvgStandAloneTests.cs @@ -170,7 +170,7 @@ public void SvgTextBodyTestCenterAlignmentGenerated() Assert.AreEqual(59.509033203125d, textBody.Paragraphs[0].Runs[2].Bounds.Left, delta); Assert.AreEqual(0d, textBody.Paragraphs[0].Runs[3].Bounds.Left); - Assert.AreEqual(4.3760001659393311d, textBody.Paragraphs[1].Runs[0].Bounds.Left, delta); + 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 @@ -204,7 +204,7 @@ public void SvgTextBodyTestRightAlignmentGenerated() Assert.AreEqual(69.39990234375d, textBody.Paragraphs[0].Runs[2].Bounds.Left, delta); Assert.AreEqual(0d, textBody.Paragraphs[0].Runs[3].Bounds.Left); - Assert.AreEqual(8.7520003318786621d, textBody.Paragraphs[1].Runs[0].Bounds.Left, delta); + 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); @@ -292,8 +292,8 @@ public void BasicTextBox() double delta = 0.001; - Assert.AreEqual(107.95200681686401d, textbox.Width, delta); - Assert.AreEqual(34.979735851287842d, textbox.Height, delta); + 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); } @@ -319,7 +319,7 @@ public void TextBoxWithMargins() Assert.AreEqual(10d, textbox.TextBody.Bounds.Position.Y); //Assert width and height changed by margins - Assert.AreEqual(117.95200681686401d, textbox.Width, delta); + Assert.AreEqual(125.2, textbox.Width, delta); Assert.AreEqual(44.979735851287842d, textbox.Height, delta); @@ -341,7 +341,7 @@ public void TextBoxWithAllMargins() textbox.AppendRenderItems(group.RenderItems); //Assert width and height changed by margins - Assert.AreEqual(127.95200681686401d, textbox.Width, delta); + Assert.AreEqual(135.2d, textbox.Width, delta); Assert.AreEqual(54.979735851287842d, textbox.Height, delta); GenerateSvgFile("AllMarginsTextBox", group.Bounds, group); @@ -374,7 +374,7 @@ public void TextBoxWithAllMarginsANDTextbodyChanged() textbox.AppendRenderItems(group.RenderItems); //Assert width and height changed by margins and textbody - Assert.AreEqual(142.952006816864d, textbox.Width, delta); + Assert.AreEqual(150.2d, textbox.Width, delta); Assert.AreEqual(69.979735851287842d, textbox.Height, delta); GenerateSvgFile("TextAnchor_TextBox", group.Bounds, group); diff --git a/src/EPPlus.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/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; } From 1d14074ce0fa1d1e8c6515eab95970662abccf12 Mon Sep 17 00:00:00 2001 From: swmal <897655+swmal@users.noreply.github.com> Date: Thu, 25 Jun 2026 16:50:08 +0200 Subject: [PATCH 81/82] Work in progress: removing calls to static methods to OpenTypeFonts during rendering. --- .../Textbox/ParagraphRenderItem.cs | 2 +- .../EPPlus.Fonts.OpenType.Tests.csproj | 10 ++ .../Fonts/Roboto-ExtraLight.ttf | Bin 0 -> 159200 bytes .../ArchivoNarrow-VariableFont_wght.ttf | Bin 0 -> 93092 bytes .../Integration/LayoutSystemTests.cs | 2 +- .../Reading/TtfReadingTests.cs | 2 +- .../Subsetting/BasicSubsettingTests.cs | 3 +- .../VariableFontMatchingTests .cs | 132 ++++++++++++++++++ .../DefaultFontProvider.cs | 20 +++ .../Integration/RichText/LayoutSystem.cs | 25 ++-- .../Integration/TextLayoutEngine.cs | 13 +- src/EPPlus.Fonts.OpenType/OpenTypeFonts.cs | 5 + .../Scanner/FontFaceInfo.cs | 9 ++ .../Scanner/FontScannerV2.cs | 17 ++- .../Scanner/FontScannerV2Core.cs | 6 + .../TextShaping/TextShaper.cs | 26 ++++ 16 files changed, 253 insertions(+), 19 deletions(-) create mode 100644 src/EPPlus.Fonts.OpenType.Tests/Fonts/Roboto-ExtraLight.ttf create mode 100644 src/EPPlus.Fonts.OpenType.Tests/Fonts/VariableFonts/ArchivoNarrow-VariableFont_wght.ttf create mode 100644 src/EPPlus.Fonts.OpenType.Tests/VariableFonts/VariableFontMatchingTests .cs diff --git a/src/EPPlus.DrawingRenderer/RenderItems/Textbox/ParagraphRenderItem.cs b/src/EPPlus.DrawingRenderer/RenderItems/Textbox/ParagraphRenderItem.cs index ad2c1494e2..6ac4496d1e 100644 --- a/src/EPPlus.DrawingRenderer/RenderItems/Textbox/ParagraphRenderItem.cs +++ b/src/EPPlus.DrawingRenderer/RenderItems/Textbox/ParagraphRenderItem.cs @@ -198,7 +198,7 @@ TextLineCollection WrapFragmentsToLines(List? fragments = nul { //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(OpenTypeFonts.GetDefaultEngine(), _textFragments); //if (fragments == null && _layoutSystem == null) //{ 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/Fonts/Roboto-ExtraLight.ttf b/src/EPPlus.Fonts.OpenType.Tests/Fonts/Roboto-ExtraLight.ttf new file mode 100644 index 0000000000000000000000000000000000000000..5e517b3baf2b0dd0f552d2c3cbf27d18e46ae410 GIT binary patch literal 159200 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 z zw$7y{1k#a!RP8mH=vvNvQ!1#(&ZEM}uvT!pR*KG8O48p3xADp+&>IE~ORzIURm01| zj!5Ai*^+<&!&;ZJ(bn#qk);Kdb*c4D3v1nk)}*4O6>Z*5d0p`ZXjj1)7Ypg)OnDFD zPST6Rbf0H-NJ5;#O_Z(DGb$nd-VGZU<0ntzC+kf=$xNWr3hAPqjXN_!;>7w@5m}60 z_~71v#T!95zcO9C#5^OMB3VwvuSuw-sKl)hBQjYw(m`aH(tQKW4gckQ!+)WB-Y@>- zjlXx}Pu}v_?gCM^?LJf zp1J-bC$2w(|G&!q{>m8{8RCnk55`Z;AKo$l(^IAo{J+irbj#0Ry@JF}rdzm7ktX5~ zl3Soj>@3TVbA#Lkgy4>*gd{;3q*MeY;C_-IH(YEagy7Z^loceoIV+5Z)?1IY?OJCu z*2etNmMOjBX)8V|)V8xscHY&Yc2;~YpK0!J+?CC})b6-DC*4kbSkBoo6^7uAHj6v< zl5Y&(>99Y~5ZdXuKcBbjV3~satJR&Xc(IW5NV+8cNk~vQ^TDzYWHOBGFcZ(@F$$Rj zpLXqIc$E-1^o-e8!`L$n(^4Y~j}MP0zjspn6YnHEh2Q(9^n0>3*S;mc$GK1b9%KH! zC8pn-M0FeCv3kioRW_d1Uhq@g+-z;Fq>3SUC~r!U7S2Dx#s5FSxs5-6C&9T~3B~%K z!r33C=!F+y_Asm<oM$x#X8$Z!}1b9qtY1V4)^qWau6#1%l!je2#+yn{O*0e3#oQ?v7>##cQ`-S`Ha7->OG&KwtFc4N|$txM+rr4)TOm0g!+T zI^~m83aQbBG!_E!f#hxSA8vR1Eb!$_mVKSEOc8$&JAUoHJ-d{qr4{7DJtj Lcn@ z{daQjp1HZJR3|Y_;R6rA5;9`NVpYIRq?cY}`>5i=h|wk--}!0Ua#TVnd_*;KPTVOq z)qzDI<>;xx(Hov2*VD_=Zg~cmsx1ShO*o3~_K%8tC1M+%@r?K-ABue?H|@Zxpl6G- z)0BQb^vW%)ARUJ@x=)w@3tH<0HVWQ-D_m`p}4UgZuUB60zrjj)Iq#@fvjb)ks8NLw#L_W)o3gamJd z6uTnkL|1{NM8I``8nGl#eX#TEbf)S))O%HMD?*-a<-#N$G8B1*5=QC9(ldNq{rH0J(xQsYqf1 zp3&*90IRF*7N-NhVp`m$_v7Vo1bkXF}8V{a!#Pu*?bL8FNagujWeKoRp zQZ?!guclHt_01vl&G&1y-vKxKXh2*i&gGZ^n;7QN*%ZYuH$K8;pj&P>X_Sju*8>&A zt?7_gY1Ysq4)*rAF^#hWgWnc)>r%u8-;m-(^b`QFCmW9IvXV?yE{lyHve`QBE8{2v zZX;V2X`CIyUKUQP)dZ+C+GyAZkga&J(C`D^i3ne?nljs|??mc;O0E2D-)Z9GF~1Y* znfJAvr|2n)gp`*k`oysQgz!R{3{z-%kbxJ(Lqa%}GrXNh5RVoNkTcF6jlm}6>?9<` zj>_48Op@Yu^G`{VqQLd-bj?U56Zp)exiDyKH*`{lGcBDMoYW9fwd73J3}1@R4d2HK zrcTOajies35~Pdg@xEnV)zS$6K!Vx>4s~n#8TWRqv1~69<|s zD!2Z_-QTQd11h)9avfWp$nx`FTl$f)%CnrX5O1r2XhQ`l7lZpiuKiuujd&s#OcJ{k z8=C;qUY21Z-DseP5t!h8L==Hw1x@x;n1l={gd%kknHuuac!QLx@WSrL#6Q0?JE?0+ zeckrHl$J9;?%(m(dn&RQo#^3eHg&{nHeDLu@&RrJ)6OkMXv2$S-d_gplD=PG~A=W38n@_4RMk=LYaaBNo}W;rjTi}+F>$?V)@yd zzc^MORyr#ucc?79Xw8$e!F11!s$X0Z-&`1)(7Usx>+V@8e#zRHcx{qb$(C0#HI;kb zT0Gc(bbfZ>(vuxM3trts-ax2;!3v}(n-ywa0+_v?wbmk(D@Gh z$v@Cjj2ljgmVkz^wG!YCC=?DnrAbl2GMzODGXs@DTtkUT$$o51XEMs!#oA831awMC zu}K6C6R8rbasjIXO^f+{YFd@I>3mrqk14C7$Ev(bSLJ8Zs(dW{loxCMDXhf>m;_RJ zMeYmmAwDI?Sy?I3*I3D;TbdV7Z2_%0lT05@C5|59dfcWKi~WN}jJ$0<79VA-U>{+Y z|3Xz3Zfb@XML3+qDiPln_S)UumSJpd1uMN1*en#RhyoN#GEwd(D3ifhZsm@$D>Te% zZTyUdYtlEIGt);oU*m7tNXDBS|656<@P?kmN{nG8NLCyAks(EBZYcP3qj0yGpK-gI zU--2An6ByUN~zwre+c!y?~NL8IU_UYrXn?a$apnK-o0(l;>0LTVlior4E`4dcfb@>i1!)2zD0W1#YwXd)#3UxxnBHTmN$xI} zpShOnN$!#>$>myfcl>|en_a||yZgt$F0-?5-uu4q`}X4LsSH4RXOz-pYNGBMP(n%8 zpa7>`OId4=d1^!%^L$#5(bh>zXgC#SPBsplT*i;j{p?_gZtttZlW)uw={f$f)RA)o zy$`LZa@ZC7US1{s zU+=SX+S0VlfZPabvlrmOCYzMS(T!Ho%@_bReZyrf@=CT`Z)uDw^fZWNr$63F9TnX8~7Cpm{*l4M=Ru_^XDU;$O)h zxkN+DSCj7@sf=6s$XMfSeS{=~Gl9stn7h~DT&&vmlOuJ{pKi;89A3Tg*|DXV&qLy$ z`1cs@TS)oZF3uJ-I$jA86wT>bK4i=^#d3KB9C( zW+#M06NcOr0GfN)B%g;t9aG&@nq{BlK~P^DZF zs2PCa+kq@g3E(%PH7SRGsSK1hZxcEnc-x>LdUkWHeg>JH24KZqZ4Y?4p@Kf`EFwf- z0UT++1zTt_c$&;!X*-0ZV~KoFApXZEo~5KWcQY-)Zez4oK~xkiJY z!s->kDw4s9$(VpuyaWXor3nhSi`p3M@5q?YDqO$gE{vt_Xt4i-aZCOwN5=49ut z!q1hKCG{f=$xTj&yK|8C@EyG9ltbRv|AQnC9(rR%_V|4*758t_Ry3U*7`eZ)^1(HI z@{3x-OM$d{ZRcyd+KwP;z4Np&M}6yA*gQGen7Eq13CxjZWn(Uvvso>2Fq2RxiuN~V z#F)kl{4cX==S9~%@=h?R*JJIX>gY0@d>kwj;mgsd3dN58;4mdEaI=e{b@Fa>K zSlY2dsV36{kYhfWn~E$nSp(tRoDVrHcI}|)#5+iTnW#pUh_vy`d{R-%k;m>(ggJ{;rAX-^wRMyQ?*G+z| z&5u=qDv#d&qasDIh~*6BgQD2Q8%d;fegdVyKAcwwHJtmGdYE zM=D|>i%Cb$slOs9NUDfB)|8KmsH(2Ljq?>z`w!JQSVmTCY+(ga&*~Cdv&ttcBM2Al zrE?GCIMv{x+M10TV`0?5+?LTCaYxan<;lhjpUE8E79DAg8(xuFwB@q+q444@_417> zl09{yZ^hdyNbB{^9Y2`K;RNeu+JCQ*G&T=$@|Y^aVRT!?qMdpH(6biA>8Ljm_}QNivW zbx%=9VO0H0ZEAO2ZiG287sP(9tZ9cfGNEN-NoC=nifCsZsatV<1=cz7)_xtAwHND* zxcx@~GBG6GEGlY)2wVUVGlk!eGJ$E~#C2*inS9Din*&&}o;o+NHgAnbwH4)9UTC#%Kb0@|O=Bd0bn4 zX$BMXK6+!oD|uOej$mUWI9n@RtRH-?)W*kM>*i}yc|Unjofzj6;>exW)b6h6*i)pb zxwtWYc}-rVInNVwVv*2rs&jitX_lLtXK_u$mZt3*!-q2shZB}8DXom3rR(Yyf5A^; z2fY#TkU=9}m-_nnU=K6Gl*avI)qJrwsv;nhbKdlwWQy~`l7Oc{|7BIb7C+-Y9on9r zzI{l1P9Uo0eJkR8)E)VPqr%ZrzQ}gdfpnM&D=n2==2}vwPK@;qaU#Djh;Pp?Ko!v7 zl7YhbdQFUypvx68OUNBBS?;|w+QG>wH`~4?yCq(Hc&7dcT~}2cTbCOBkDI$S^{s~s`wId{_T*jUqwqBLBplkzzpy8C?s331 zyWn8&C`&eXq?R#DIGy>O+25T(@|{zU9^_w_Y!WAFr#hNn# zNR%V8&PLP`0LBEqwlzI??8NuYZs}pbSEiK*q)i$Q^Gc(5@?VmbV z%W+<=%N)HqZqo6{@>8aZq;u*OnuX%e-NL16FcrIrkd+FNbl(E#l-Fembyt*=U<*SFPJ$-XL zJj-StTozjS;7l2~jxV^YoMD)+4ApI%oUFWPbcxZAz{MNZwC<`z{D zujDf0nGK#lv%n{u+4&<%H|*Q!ggIp0{!Ex**u>`G=xFIlBSN5E(jfi2=kRSg1<3QR zel)TW+;O>&^nPH z0B3x`kzi?(EzZp3IM;-jaDK?%S~9!dU6+8ycL8mfQi8B9TB?I>83fM-b-BTtzEPL^ z>F1sty|N}FbK>&w_IC%%>W{s+eB#|h-`Jj4H222D+-GxTBd<-Ie7hxX z)%gjCyqN(dYbuM^)!y$P)nRH_&dBd}XbNoEkFI96{1RB3YBab%gF7G>Z&;2)g27a(7Zv+1;y&pQu4_ukf+1w+B zXOp?&#@LSIw{8?W`jws>?YXx`)BD2o%u6e1{B)JDo(q(}3qPeTqCDhscy2f|8XRKx zE~AYJj&aU>AWPz=hn{5Yt$I}aNE;LHpIs_!vm~j`;tzh)-mGr+5BFXV2qtqYIS1~l zpaAcn`t)6Y<20Y(UKO_SR)8Hkhf|>i%nt`e*rs>kt>6(SvJvi8`J>;yL;mQ^H`yJW zOf9~}A7}8Vy<<3jT0v**{vIV#7%LDz{BivQe7*jGzi2f0AET}i))5At4nE>+=$zWvJYiHU-+G|JKy=xJ0UPOBI4(3@s+?OX``i6l)|+L zy{PpdJqIsrZZvD21*o@ZBO>Gc6TKC({?gTHO9JZq(kWb9v7V0<4(KSp#Q;ntTV${p z<9Z>+dPO0_^{pJezLngJ5CxHF8>T4S2aJ4P>onn1vaeOW7ATglp1r_WH%lt#nbG zEiMKIRG<(wv|+{%EvbZLNk<^O(g6&S*~r3JJt33Dj*cUZRmS=2{rQjd{_>0ezG$Ws zXA2AUwrrmdLx24{zZ2eH^GgDJ-o|~V5kOd-|Kgy-P1>@i#xh+K_nL1(b4$K&K~odp zcKP<#+$*vvSYaG|05_Qfd4$0e*M+qC>%r1>`Z*r}xMJ@(o$O=o$EZ{Qd`3kD`n(0L zfm#B+(51=CE0Ejk5ghD6Tl$*GdLS7NfNIiC^a-+j_-}g=sT87eY_6(`?J6nB2?fK>?!=t&5vlv%dHO&Z^+ATh~ys4oh)BAo|QFLQ9(bd&O?{tlTwn} zlate<&nYdZSIecBQ>7{Rus5YNvslHm6c_#pn2KQB%aYceWs5vRy%YmCz=J4=3J z90&Bq+?)~+$>B3bT%8Gz02s4QvbaY4*|KFMsOBu$OsvIO-b~zxwn0RulC9@ea#!HF z%@rAscm*li6o&)A;HJ-3TnFMnELMt+M$9Kn@L?g)29eqLe5r5dk{ z;hsJ03M@hFo=#lZA%6n*P_qpEmKxuhVfSPhujp{kPC7D|X}nUx?d3+;6&1aLwd%No z+;us%VN2119LJlXeXB8AkcG|DQ;1W|T{mnYExCe4ZZ3&3G;;T2r8@pET%Qn!Z{J1T zxjw`1gt**XIzFEOzx@~+-|7kmS72;PxhWT*l$%>=_&w)o_&q<7n`^*VFzO&_Cnpu- z66La2(AuI>5KFuR^DNL7&L-yT(s9-u}&7`}kuByXNfX~qizGCR`!(ko&xtR{^AyY(?q zd@?dF^5M_s?7E*mGU%7!KlSk2Sl=YKgZote3~L6^Ct-=W16MYP2g&o~QCxu!Q9Ot%<>EnZFS&{D0++yDv!##W072p=T(upfO$Ylw)*cCOtHN)-!Zm}z%!Q4(ZGH{DsDG}$m5q1T* zWxVHk?mB6~d``$@`X!j5PUgz>$r^Cwr0L4-CT^wd6t3J$uh88${FI#JP9e?!oZF5? z$Bs#yoB5Xz61-sN(>pWR#;04){q;gveCY9?Y_i+>(dn=_-;tBAvRya)kxUs!vy)6_p7b;Q$ISqB;IG>@RQi%Ht@e# zR?UxQ9tJ_Dvs>&J2rXFfjC<$DPvc%Hd8!rg*Apax-b+A=1oM}B<`A6oDi#z z=6_&Fcjm!Q0>T_Q=azFrYagjCtC+2*+o~ZWhK<}h!^T4XHA89Nu@G;|>hWam^6aZ! zZ5M`C&8n8&C!Wd66GeJ{c+5*C|JcZ9A)~91)T^+xL2ik)y^S!Ww5D@pp1ov;e zG6GtG!{QKYPueVi8X&D~g&`X}PjV!pum|l@sip=hlh}*rnc^Ag6da9yX(p&qR+qpj z#a|QOxpO5Y=jQ&UC3z=)*wy(XKFDzEjl27+aFv?Q`|6tRZ@&r8;J_ZC8MWFQDi1hfL~Elu_Pa4Q&%4RTORr#gya|YnIF8B@0zKc{wYU4hm#}bM_9x zkfVo(xjls-HNH`o!h47ZUJXF@f#^lcuRlz!w#Xi=_~*BO<$yg|Fb~% zck$)V0MM8CKNf#dDZV5={$*Lwms%d(LbAB+)>|+zyeZDGw9zd>7&AEh zSez1;l5x-;c--Z^%xbe3Ru5Yg(Xe+icQUtEqQI!66=q7c1DX4o8P6#Fp*CN8e?RK7 zW7~jNsAXr!du7wLk=iAN9U0JARsE;KASD9FqW#{%sX?>O)1k!Uyd0A&d%88oW= zE&~IiH526c;UiL3#R~C5DWli+l$PwiHk#U(BE$})cAXtlsmC6wHAHgn8zK`N>`H9y z3g&(^OV;f8(Oka0O{tw@+V~R_;uieM;5E6qH)(HD>0ChD!#f&51^J6@s*}>m)k3Ls zaTSJKtf+U*A$f|~RvnU&B4T1yY-1PTEg(3LxANedx8>yKZSG!CM=tKXSXOp%rxc0% zLEp*`glDj+zr@e$-wws)k=r5Tcd|Lrn5qbsvyZPYDmf}OoEBp446=EY(s zD84Zn*~|~cilH}}&Qg-(W`m4KoE`BYztc_sx*Jy`lAblE%UZ6kJK?@*@6XmWzH)?o zKI5kUqni*6yTebpj-1vL)qSUH3p-!l-uvRH;erh-n=i=XZ{OmF+3cjKFZ8Yw1m2cd zTJwS|6K4Piz$o(@bBaJusau_QmSYR$38bKm3oFy20i;u~vu+4orT=WWO?&Q1ih-ee zdfyl48+b0S;>eFTaz5oBln0gnxE#1W3`+YpaGQ1zSfsKvcXswhZw(vbO_Q6Yg9qA* z%yY>(!?F@6CVBJDl!OUy>q2zHhtOhjtx8wA@7<}6N4Gb}8E$Gn>9{o4o=|c4{SBY| ze*a&MIiGwL3n}9$1rW|)y~dbuw?*)5eS?NEg!mprFay$ZZy1`n(>pGf>3{@lbmYSm zAFS;C;IyHQYbtCw(b{^tjkbF1eR+E03mwO3ZvsI!0_*D*=m&nVz2+kK8Gend;OPAP z&Dh73!B6Re`)ZD%3+2c#p~SyqMOWUOrPsCHwS_^t6_o zo>VH=uP|u;7nz;NDq!t%hlm%6e#~x*7LfoehyYaHqomaoaT|9<%y3Uu zZ>S3OO|43dEJ^Xn7=83tGY|gth{n-B+B}&`3^I8P$g;k6cF7Em8 z<$J12HoW&)S3r7wnpbeiV38rPa9eK%_5%@BX1nDtflkma@iEvB`>^Q9P>%O-2(Yk- zi4;O3C<9i1uFYGC+J@jl_ z=arcfgDh!hY2&lvAByIZInVgn*Sq2~YL31$JaYYLP1(NJR`U+-;!|;{seE_VQt@-* z9nY*KD_Q@M6GAZg&F#;*t+HTpkYi$)V7m6pIABL$Y84R0NVoC6K{K)jVP z{PXSrRzzZP*}HzIQbd(>Z7NZ9RE4=Os@hOjcVfCeCZcN1;o9B{>$2fO?r+}Tk&w{7 zuX)9ho+QhZ<$HN?vU&Qw6Ws}mVgfQ;QbIGPUlYH2Ghxd2UGjC2V&&Hd z6jyP!(!{?BXu-?4u^shqF6eZtL`8R6=z#h+C0<7jeT)Y?(|{5%Jz?Li*LV@4$c$k_ zG*@MKB>FXtMzOk}K>w(l{z3+diTPz>W@orp`A@%XUF1L2KhLa-yi5%aOgj@uHF#W9 zN()O%rC@H(T1G&NW2}~yVMUW5e|Q4poXKsnwL%P3KYLN57XQBT@h^mz5Gnsd(f14P zhZ>lrFxKYVpPS2ZB3xxru=q4nYgc_@qPL^vP-%8{m=t@!0!i^}Vf2ABJZsokQ!Y1; zew?^qgCGIPbj}ASWC#=qXcbc$)XSSWd*ea|w>fjo!#zFsuPKjKT11zP-{0H&$hb-u z)v~*}?Lc=9T*kgRJqO#GcC|#A#i+&~>FIf3yewL28B@0AOi$0lx00S+XG?|*MvMnADP{g9!>;Z zN~OYo5DjxB16Q!gGA{KOGm3|_zKlF~Q-e=}myKPbdu~fy-lhh>EMH6anBbh{35^3s zOO?tzg=OZNYI$hSzG8D_u9;cdP#H`evUbuh*z>=HmL4bbK!pNfT4}F}@tzI{1ERTq z6zl#=sTp|%Y`t-bE~s@5x@7F|M+Mo%3jTXP8CFECQ|sz8Q)>#^gN}u(xj{rxoLCOc%584i}?19hF=U$d2$ggtP3d zSmLZ$L$^YKCXjP8GX;){q=U@Mh2ZH_hNkC*?=-x;l*@QX{{o+-e?geh+xzf;75`XN zBz6@QVLTLy;P(pajMz_gMObJAbA=p%3aE1kT`?+d((b*e*jNa|tcff^_iLn8!<2^M zxA0c_7kRb*ABntLp?{vcB|cnEMiWV-_z3a_M{Yrxq4S5OoPR;s`mLwn5qQvdJVlLX zI1`eDAG(Om=&)U&e~PzH!mm_3=U1evOx&3y{)p5FOT}673;G;Xhrz=TR}DI3OHUkP z(`?LOTn4C&4PYoikxm>sztCD25TKEpLi{tyX|5CWcYuk1k*B77vkJ`BtAV|bo}%wA~03?^V*c^Yyn)iLazKx+zG z%8SZw?3f#=;VT6sGatV0HDnw8p%irI?R4TJ+X>61^0F`u!!pf%rIrfG`9OT)BVzsi zca~_kL@tS2o~5On3zSQELg5S>HpECguaI-ZE03DtGJXrrmmSO1@KD(UD>C_wdQUI9#(z4PK}l6hs~P1}vmV0P$0DY|QsoB}@NB zepzxlLl>|vKDRAFo41J@HQapjj_JoEw;io@VdwdIxX+L7clJo9NWwNk4E(nB2Nb9q zl@+|@KVzC@3~c=|Dv#7y)ryvS%OR4hQJD<|MOY4b+dw<<* zF^evuZ;h5bYLcer?4)n4Ku(Q}kyY?RSkUs(Sh7rgZ_2ns?9ypeb7)?9F5yHUB9C#DzD{6GmwaHNQ0d~f<}{yIK014=O$A8l#?&;_zUKiG8rsRCm% zg@ALLDV%5ziCF4r_or{1T%BCdOc&IZ2EhUPRE5SDAX8C{gm_KIdvlN^TvJ?mgCwrH zX)j(MyryUp-+p72SSEf(T=c&d|3sYVkMM(4t5zAJp?o;&y#|G4^bOfV@3}cQI5=pw zf7KrvdUZ8@jZ33L5)H2$BBQDJud(^Ltme4I>L?;`RyI#ZZ;y|QiytHR*jSl`rFeIX zo7Z=AZkX=qoNg#os|t(MDvhw0c#VvRzx(<(B=zmV!M8<}6&O@hkK7vh`fKiQH*el- z8ssv+{#sZxGBTq7i(XI0Z{8GV^!l&Jgt%4PP*xEcku;PS5**?!Msb0@S@DaP1_+Ks zAb&3k4-SsXBXWY|`{c#NT2e63Ix*2ofBk46N2|>l$Sp0!4mTM7&+J3o0NFxs2eY z$q7rM(!Jh#d%d6#xJ~>I^fCGv?w4^H9)TOmr|+4XONv&-$CtSKrId=#KJtj6KoFEi z1p)lB9PEBC?~6=CyQ{pXr==r&F2+g~?Kp<0X=U6?6nUUq%ToB^uHI|-5V)lBZm3<+ zS(r6h1$#$adGw!Es-?s~t|HqfzpA|?t3NA#d2VcTQ}oAp(gv{S=XqDyhy7K~=vdD4 zQL^@X5dA{&(B!+j@|B-v9IH#S;CiS=f!L7%P zsTbJliYuJG5Us9(9l--dHRA<_9Eyh$X*BtHx%H|w)m4*O>dDIL3AHAoCMT;lBBCxk zyC$5xx2^wBY3ZSU=|z9AVoaqPt2kJ;My+0h$yMI|iXRnDAYKKUxs$hd5Q$M*1+81C znVVW7L7xCQW)v7fR>)IoH>+h-WyV5oI%phXK`ub)+ zK2t}vrMlZ+Adme7B#6a`cFlq8X1&DKRc&J2K5)+4pGN86eY*G8mt&6bt#>`rNa ztgrWchqiosW%X>ScDAN+ri{et)|7>4_VnZpl}ARF59O5%W@ZkS=mt|#2Dx=ATNPZK zK`zY2+&Uv<7vek9fXWf!2rN*rySn)J*r4Hq3>z2A0%Y-3V|3Mgc$IOTG^*Ex^7B># z?qdi0kyrO&Hg=XS0g- zb@h#LxlUYsiP+*AS^L09|AmhH-F*`qG`reswiHFlV zFK=&$_)tp=hdW0`!ome>SkQ!k;-0@VjPjJf&=Uk%;ZDK}RCY4hy>ees#^%nNT`=}t z?yK6dJRz}jf8*-ao%gOxOzquUU3_S5p|0*&Tl?<9xaLh-ZdBE+%I#OD4ivZU(S=ov z6(@I=McF2Y>iToF6P000maVHQ9nDD}(q=WLd17DSr9_?o6QI%#Pg**ytSxHq?(SU$x|S=G{SP!o6g9_+yEU3@a#^>oJ7#c8 z!4!ICwG{1cPwef1of0N6*jWI)HenC&TX{t>#Zl}Af&$?3gg~xMEAAy1g*~?hge9Y; zdnlay0O#$PqcbE0o2#Do>w=-j(o@dpp%HE-Aq&D207nA}B~(Xqk?ryA2b!A>b|xlt z9%yPl(2<}E){g6Rd#F5~=&%1NOh1Cm4zTFVo+1Q8*NX1s6AE=Ca`GacKrvrw zrWA-^A?IZn7g+(4vQFz$!w-r7kCQPn-iD$UPq&I=;&?Mkpi~QUB2Ns4R}B|ATf>)} zjs}wmklV107!?(pphu&^rOL)Zup>5t6|yuGWD1p;q?*f=We1KlTf~R+!W85r0R2^8@r0>Sx5y z`Afe5j!^hH8sTSq)r<$k5Scd*M_u>r#m`MH(*A8+IXR+ z=E6qaN&lyA9E9E76oV=3zreUu)3-+;jv0|nK@`ZOnI{tJgY+eEiCWG=pDxVNIl;-v7Y|JrHfI?u<1yDs(SVDx{5Q~%e8sKGg)27tBGfN+(_KY3znI*6Rkkf|+~3YGdWT z$km8#STt=@U@XOputGCaqdG`35cK^Bx>KJf2b+!zq!n*j<_r2uc6H3;=nnUY&uDp9 z@$qU>D9lCGZmd|gt=86BQ#hWpY%=H8^QcTBS}FnMzS}WE6yQel+@oYTrLca5g(;Ps zuk$*bQi<40WnJ1)ObwF#f2Se}Vw)s>Nm_E&UFz?3DEVtsZ-@V^7CJ^vy#OE#!Xz zNG(D{5DazJR<={trU_zmq0*SR9{^+YA|nY7Zg(;`x<5B}|EPFXOODPw)s)b6jC@p- zH(ppgS#;}pVXo$Qmv&uE6n3Nk_7_65k$&1e5v%|Or36$u9FQpg>)M5AESfDgXWivK zx_B*HG)QVv=brP!@C-VR;bs2nB4kAaHnU@u-5Qz`W(b#vq%ZP3**zXYI{||bHWrw85Xud86r)( z;JXaHzFJDdbLjV_SqYKs9qke5N~=0q%B{>5N+M0nNMq?95_xbEaRiQkrRD1lKSyz! zpOQbs8g9gKjcl=VhR;81`~mqf=F_4!G*h~i3px;r(H&*hkK0+9QEq_qPvLckESUrc z?q|Bj`??3uwd(S=wdmFr5*?qef3^QarRJXD>ap}3RL_&Wj6vxsQH}AXbDIVqg#1)=-afP$5jGr8New(K5XlwpNVJ2cvPIa6eR~Pw^ zrEsoEH7FjNU!S`Q9Rd_Yi5&bpq*)@*6<8U;D(TNxTJEx8g8RteZ``Vn(O}*Kw3-L4 zcI4lcQ;pXQ)<#5(m}O)Hn7hU)6mgcEuP+J>xl*Z+MP<+z8u8BbpdQv zoBsc(xqSG`FK_Ps@=LPl|C_3_m>B*tqpD`Gp+e$?hD#Q^tph!>rj}w!_>s_}6_kq_ z{R0us2D@|yAIt~cWz@3a_{;h;eEmD;dl2Z!_+t6|?ceZ=+yg zSWZd{d!}YMB-7AZQ;d$B&TU>UAQX9%3^Cjf3^AmiG%gcYrHfCKCYKa(75PXB+;_U>-1bmSaZWrC1DK16_d0g_0~YwZ*!|D znOqJ733@sLkdm-O1>9C7>Pw`IB%wX-kM&z2D%~Z+AkOX+y*nM8|K#i~ihm}0FL6KU zTRg`p!TG}A1u`keIPVk!{{!*o*BV~u{`%h;z^cMR@BfVl+%2uyTqPFpp|U-qBu&~B zEcTni+TF{%l$KLGRVn5P3sYjSR(gE-68&xNyx|e(EUdLZ&aQd1GPla9ut|9BD9_~j$#K7 zgM+oX1+P@t+1Z#)DLE@%ni<{QI{_7U=4bR)^H1te@FDu&q3>}?9GMe`;~1kssNYY* z46kQ$)*NEAd0M|z_*~cpS(RE)sn(CK@l)Pbp;-L8Q2W`O=T%FlTf(d?^^Ax3If!hGU)`9OyYcb< zz6+CTP2-j6P0uV}@$9Azml__rHH5ivY*yWX0Ie?sKfN=$uOODx(?;gyWle#Jl~cJ( zqI?qbVpF4x3<4^f6+MlDyLRYT_c={GvtFf5=sD5lwBy+(aRTrbUpe>1RQ~oir)hWI z-bcq(13%nNGK^dpkrqNDC=~UQ#LI$JjuG+7$T;N^2Zs=Q``FkRrNT?n%;^@eU0_P& zT`HriGo)#gHc6u=;66tuJ6bRUKfFG=?NCR@p|+@)Wveo@J9^Vq6PJbtFHL5qc5g4O z+@=#hAanX9WEa;RTy@`cQPK2$Rl#e|kLf(~JsWnsITN_@#l003dtY1`IP=Di2Cw{} zvFlmzJk>&y+-VHYArGU!5}td^KS$=T_nx>aII8-Lh?2kj6P> zRbBU{3@?;7;bg19u`K~h9@vGm1zU-Qm(#i>7T=^HrCfI!)XD-{nOO;BfszN6&Qb0jk=vFPKmx*Y;sae3LiG9z4e=}XH;WZo(sXve zdUlmK>15asB(y*9tK)4E#lG5Y@9wdyJla*VzCH#d__}_!b~Mk zhac;T?&0q4ZRsebX1}I|A6A7<8WNs?UML;UY1z2SQ6$QgWq{2guW|KTmMT#{36Bac-Yec9p_}$51BGbi*h>p&s;vxs&KA_!%*;j!hD0|C!Vo)! z(9!X%K;uFwh2^&C$RkD~n=ST}eF~GXElHFT-=(%Z3SEp$l_yIZCE#nKGp1GH;7T@q zs#`*ZV&?uJM;vCZ3~4-@R_##jKP%p+C66`lZ;z|Lc*3xpyPzptqZvF!#rv50^(9?L zE6G8&r%S~vh+dncng?`SR4#M(^0IJMDm|SnlxPReln_SUu`Veii}g_a?!M&hS|_=G zk}7v`ERq6L>L^7bw_Rs=L%b|h>GT>(?2@P7J~~|PrSYuW_p@W<4Xiy!FBf}AiHI^T za-0B}{3hF3Q)lu7m79~RtG$&aN}Af+D{O3R0c&R^+LYTG$uFkGFpkbo`X|Z$eyqk6 z^b!j$4mO%#Xit2yj{8uI{jvNa*_$AS$8uKsSTV7S?7pZtDMt2iRz@NCw1$IGluOJu zY~v!iX2WM1b98?krBtK>n#(JoBHpOqKy9Q*d&tOehKDPut1Q++`>n4|l1y@YhX~%O^ z!&+5Cvb%Jag)HM4qJ6SqQ9!{iC2BIundkyUW|9JuCW$KqHc9P#mQPC3zs=p&>GYSi z+9;tZ`WB4f(vhLG>@n~wyZQYCzhwIcDaL|_yl_W~q5A}-mZGm|mxw{fM;Jc-wQ!Bx zh&KEc^~Ari87jjpo&ahoQ2QL^DQ+}6+R;vlL`>d%ine~D{CJ+9u+w3pmb6*<>xTW@ zS*>BW&QakOncKR6FdF%Xi3}3Z!FTpxw|x+y3ymGV8QK9_NnEF#QD4EvjPeY+2FAJ| z;89g?zE>;4YFy0-H%x-j+BiTv*6NaCZEYRxm|K(PA&!&jGICtOrFM5<*+Hx>bk%)d+R9#3)xwka~$Icl+YNT-_9yM#x0;V z!fC#EkB^bqXz1x;0++N+7?*=jTE-v5I~u*<_$2yTbG!|+3YcgNc4odZAr@-15Ap|f zj-Z&D9WS?OwZ}(tDaE9t8van~66%h&)+AKCfyUO}j9TI>V-ML*(VDQJ(>TMnv|Jq? z1^}}8GkYeZ(~=}gH%;zx>jkbv%dOMh`s$8RVt)F73m_!Ffw4^xg3PFLu(!5$o+X^0L8Y6RUxE12D&^CEkd!+_#?kTA-3?T-+~%_JRrwmgRZbuxl@cl^>SMW zr9E_P*qu$jsS3KDZ&GQ==ffdaas01S}zC)y|j&*`YBhCptSGXw5 zp++ih_x_!SP8E|FmFV@#zTyK;BRFvg1 zZrgc`>T^bZk5%?US|WF(C2%88^I%Iy&pSj8R2Bjxp|lGeND?`r8v9cL&g3zr9xwiF z!^>Qz{!8nIlLNK8RwN{L?5Z6+QELtP>6MK=k3oK#c%*0ZDKC zpBc@}n^`{bNOjTdi;$jPm@TS#Xo8)N+h3qxI+f$3F4Ty13vgQ(Wn&4gP+=yK0ZFVh z36@mY_+Lz-oQwFjqgL*gJ#QSm)YpG$T~29I=fM{ETauEfWfVq24!R-dO))~0{ZA?) zazSz8o?&?7PfvosOc2~WT&CQZoxvzGQDxDe$o+zX;)4M>Q=|<;QbcCZL+D3-i=y{L z&B?@23toW+_k8o}#JCmvn&F@lPoA4Y!5^}ND=<986{MQc9x%kVa$S*jyet&c&Dk_s z+Mf)_Ibb%Qv)NEv7^pujpbhAmnUsB;d84>VPYA_l`qQrCp!bs?!#U~PqsiiLAVnx- zA>tKgJev8W7_s`BJ)kOpU?@L06S*}=ii*2==dQK4{CS|c8Ro_RCgO=-cMlL|Pn zM7#yu;SLX1SQHlhy}VqIJq9}KvS8MrAyO@Ig|*p~aqg678OeuH z%J`1i5y7RnSpVra8Q6Esf-*5sc*-YDnm&aWtZR6(QikYjTEUh+--fj^lrUpH^9j!e zC+6v%5cn9F)s-wOEUeb?J%-@LPJD{sS*`+Y&iQNb7KLoHY0~XAm5%}@P|=GjxFh7cRi87e)G9JNq89EYAR}qi$TCo5qh}LUCs5lT znG$N*fOrEGGXmJ;#NcWxTdcCTL>)#!sj!9hjK(XYaq=BRVs1~MIK^t5c>2HK1Pq$_ z4mYKQMBj2%%4NRbfz-`+hX*2p89dO*dCJbg%0i%`0(E^eQ2~4bkY33CC{bjT1qWMx zc7ebkzNVl3w$OlM4!UtiZlHCJA>Yw5$0!nQtuS*i3SjhC+a1UP$w;q1313QmU16#gp)VON=x%EHzGf(u*+G}?ih5ztu{WNY4nfCn3uy$9y? zJV-5)y#UfZvlQH=;SrhL?LW)Iz+2)erY>6Kzjf#GxM z{{G#29gB9nFtmOBTf2&~);~Sk6IL-%q8&^NAA6=JfBIbe_UcoU1*tI^-bn$F0GehnXL>pQr#>q1MZ5DxxoUmw1`TnV{4Twx{PftEFjp$Z29OJ4X;k%S zK^a;b=uTYx8Xu7^5o?f4pr9)*K&Ds-K7>z{}G`z+W@?s%xSv7ds^6kZL^h& zm6fa2rfdDTKIwmbvo&1Go>t3|ZUhRAOw4(|ge5#opwP_rZxQ6R&OxcPN0OA0aMHuJ z&;x*ADlmob^-yph8`Rv31~tEvdMXS%|C5`7JCmJ#BRgA+)gZO^KR7D@S4KB5=|N)SrNp*-s;cg3OKhbt_ath@^YX?uc*)Nn z{|aRAj++>#YY7$-gwsVkP{14H;R)dm`MFazOz&Ob4FV2)myew#=pHxh_-Q0xOO`1LArc#3aq2;%^ zU3dWC^-e6c^-;!x7HOt9SXeq11ii6%FewC;MN~@h%8XDF>%yFd==!#bK~4VJBJG%3 zHBnhv*)pvzn9}8KsVbakYUxSo$RZa*aza#f6$ROCiE(Xt@wJJzk+BUK`R(yB9i;`0 zRYgm4eKAkuzi?N0XHXwYXl52L%MqfGLkUD|ES)BsB|4j!qT#qJhFu&vlHMr(lI$xa z3Uws6_^`O0v()wNReyA1ZV__liZZvt6tr#NFe+cfw+7L^6Ejfz3Ca&>! zZAj5g2q@`9x zE?=J5nnpZH)==THp%nF~u5!8{wf`9Jr7k%-vJ!bGOPi7^h9in1yrLV@@fQSMyyolpuQO7KX7Xl2TeP+aMrDR+*=bgh0F5CxdP%<6Xu@kt1 zo->GBNv9gKRX{55)4PV@hgekbCNiG|ifV;`PNvb1U*%5W+`iHO#%rp;jCy z+uc=&-ZY#u=6F6UZ01rGM{ox$);Pfyj@H(QcI4e?AvqAdMekBe7O*NQ&dh86rXOhEP8(_b6(iiP~g$S!pNN3Ta9dYsC=u~{(`7_?}StxBzqtf;WHl@})24*8i{FeB_i za?w#saWc(fU%RY0R$F^)g-iBWeO%qaRW2F8=Ftq7g0;oPYsK@i z4n>)v5%!tWEy<&8Y2n(=l!k4~Y*NgN2UHQ-w8c@S_2IxCpTdlkWlOmhq>TZ3s9-Pn z0D3g`lxC?QvL{*@fe@+%Rl#ZaDj*~oa$KFIbZb*qE{CjK*6%`&N-vIloFl6NUbSt7 z%2t`tv9o5r1bFUnO{%prx_rEx)d0U(T2XIXnb&44`CYj2;ntUHAKX}EEciW@qaHb4 z*Ohy0du7qq%N?Az;kQz4EKv5BZr>K3lV1P_iiFN#8)Ocrh{XK3Asbe5lHwAOn1cD~y44AYRV0mwg>)68yayY)qP4s#2RTESrir{x0a zbWO80MhQ#JIcNYCasG~$WVGk9m0Pw|9QOUI|hb!9`6ot?E`H^po~ z!!$M_`8~*i_%fsaD5YaM<3sJ~!f}V>hhL1Jm{adediE6`kIxbzuf%$c~t85gO zX69CM81gwh0joelp0R$yLc;)E!TtJMLsseBjR^f;Q~5k&u@kpQ?rEAQo}~ZUIRDM4 zPCpMNBZTM*$lWWTq^SbB++AyU(eP@%ODg$?pWzoVT%BUk*MCqf5*Hi(PeXo-H3X&_ zU?gQ8HRg?=zI#MzdAQqh#Kx8dssdS97;GzA-%-JZnJ=l}#4MKxoT5(@#h-E{aO?F! z_1OI@kZv0_{L-${nZJs4TFub)E#vJEj3PT}$4jH5CqCaK8N?_>Hu;%@5fA9*jHobZ z=m>5@h%4pX|0O6ak$?8T`jtt~fAJ|xzQiAa0=pr9Vu{Mp(<8(Ujw+8J)XVmO&CnE+ z>6#oxtLlI#G6wLiqeHbbs6f0CNL0D?M}uWaY2!*!^o%5R&1s4c^ow6IZn<__#b2oUHMJw=FjwU#M(D=Px~q#SX^U>`U`G~f&%h429@3h0<<|uQx>for9S<{HW$zR0Y zScLE2DM};BxWU13uH1&g-1W7!)47_K)q`z6d93`n{9<}jLPAq|Mtx(@`|ry*{VT#{ z@kfe&W;6AIeazWe?uJf8_V!kEaG)vGMeq*~a5aiSkp7&5jiM0bjzDm3@n}+LUPNMg zQdnqOQb<8~Y-UPmXqxzcrz$VWo2V0&S+ZTj4 zxgRK0Y!pb{W-4=YUNOy4;f{^N+8EPe`~w4B{e#JSY?vM*6Hnh$dQN`f)5jnGbngvn zKN}E!&mT~n!bt9*^49ha<|=oKyGH{o+))OILV}K%gmES3w?AvxfY2QBzM=j4+wBIj zJ(VP7dL)#EE0Sun{2Q|5*FSycnNL@>3${KUegQU_jzMvLIq3@NxmdvoJl7i$PRLU* zvs+}Pa&mO^w6ao6VzAG1G|<=la0W>)2?)w83Np*+C|nX-9xK0?))*h(n3h&o z7u&)+279|LcD9If3`p>eO!gLNU4VLS4i!b80w(+x062^*ncQJi6n#vp`pB+~;hsJ0 z3auzg@9D&q9dcSLl2#O@SJe2{47-O`6s1>mxMwFFnN<{}S4y}&s3=OWsOUYQD;pqP z{T9`%Lg1BCy1Ur(LqRe*ZCmt?)Zg7m|EZbVLDG@$?(q&*B@$;$`cAIRTuxIu| z(^kEDdRSF|iE<)3n?C`cB497DjtCEzOEY2|7lJvRQXS?6K~B8zIN|W zdi9B)Zan+(db={)_7@HhKetsA-?rni_El$BI*GO1DUn}XomsH?sb5bA93y70u3YuP z7;1klzx2nMpux8uSd$f*^Y{X-wmrtJ9m!Meis zrx7@jbV9uS%nC^q!B$bo$lb6{lt0=%>F2}`MVQfT!h>~)ME$!ZWrKA1wzMd5Dw#)4uSN3%$mqpc1ov0gl8XHwRb*yga>1_qv-K$F=6{@To%nG)S;V~=b!XO5>pr@=DL;SH)sG&F zS@qg|W2)ts|1ul1>e}hy%+~XN0-jM}s~Ox{L4DD@fGBEX798VT61AeKaLrnA4>`qn z@sf*0Z{wSrfC0X+%lT5v0NnP@>v&(^`Gh@cYiH4`vNg!rK@YV0);xu{A z8QX_@&aKdnzPGu3e_NDWY+*FK>6!%c$L#K!prW;!io>0Q&kV#jOyot^S67FNP4s-& ziud!s#JqiEh?WQW_vT$}O_7N-r3c&>lczRSXSlhU>V7B6L(=@n6gZ9rUnLeC`T5AQ z!|zV?yxFs;{Mzy}zgV^UovlUOa`GEZz8KFBzqM`s>)UeU;xm`Tja^?qd!BYy$Pk|p z01jrcf_$o=shcoc(R~KRgoArIPf|5pNOJ0)xC;>y8m2P%?MRq+Y=>mP0_+dGgM zU4+>bicj!=V>n@Zl~9D3&t5Yn{r=5xsG3`;N6JFx%`W*aXiuE%lyIGU8ny~stRh5Z zZ*OPk=I7_^<>?vd>@1_qB`OX)7_5ac74R0kVvFXh0ZNR;@p~9E-1h3RWy{9Znwphe zb@oXi`4I^jiQy)JDK#WCO@1n)H8HU@BeSuIBz;3{!m7@S_sae$>%mDizeGT(z(dEt zhX1MU5fcb1Ez7CDK`|nnV6mJrIK#=Z7}cVs$;mNOE)fx~(aMDQ_#h9o)d^Z>?;%B= zf<01H!*)|wC|i#iEDa0XNKnLSnDZTPC@3NI$KPc)d23NXQ+vHH4NsssOup5v%|Sl7 zJ0_!PP8UscirKsa_W#huI+Jk)LV{c&pU>aIdyTpIe1?~~`K{+nA&T?X7b5Er&%g(F zA8K1TJt=ec@ky`@Nq`fDofb+vDZyrCR;d?^MnKaGNsRGR>xpDjKr0nWVOhi!dB5}A zWKR5w-F4-go8qGCH&%2XUJ(;H{_cjhQzPlBvC}R6=O;4bTed#XoU=3AIc-?{bJe}S zog<6ZJ~Ne3FnOx3?4HrQ;?)}yxvkfSN@gy%_dmBwTd?cH-Qu4&-Z(pw=I5L1<%{!i zdsr+Y)DvY7nco_^Gt9!&Y-cP$Fkf}+DJ`wC74Kz@o{c%pu~Rn&j_WLx8ikc=q6R!a zUA$jFn4qBGIwPsP(Th`O5O(74ZUz0@1!sDkectsG`u6}RoHb6lg6sG*{ksF7+py1j zJn7%b;L#C?_EK=uPt(6A;7j?Z{3QtD?qAct+a-A8^ZwiP@8yW8=H=H=+1=BCzX_Jc z{Tkz$aqY5cTCq=BC;E3NWXBi&jc>5OWfv*25K{2>9Q*v1I7U41cM$!XC}d}zLL9;q zD9s_0Wt;kC-t`?SsbZGD1D5lM8jZn8b+Qc#viA44@Id1v0W)&6SjW>>mX_w8-ad9QU)}`pTZrK~3Hrc%LL^Z998M^wleY)C*Csxel z7jAgCIke^B4Ujl_SsUhZZ2`=z{RF|q5>PWk3l0i0K#a`?L>avjOeRo!CJ&kl6Jn~y zwut|)x$gk4s>=31`;?nNNFzWf(jk-p0SPU%1VRfXiJ{j3f)E3SXu!fyZ3x(VNeqx6 zh9Wl585{PePSqJ@#xg#gv6s=&5t4J>Z>@FCzV{|L!~4GX|GxkC=FM<(fA_c7UVHDg z*IK(-mlRB?|NEGWtBM!yUNt^BKD*@R#l@B7xRHPU)w9;#xgaGjW9F5`v#(x|@q1^$ zDGZC&n_s(O`iRxLXUSuS1vkHTBXqWPvVQDv(iZ3pa=J6D7of8g%p+sq2PO{e8CRZ# z_n;C&xGs<`RU=8+S;gke1oTe5`*cY1S;)O5cf^`|mrr|mV!H{u=9b?%t^VsV|TvZ$`uYixtz-vb%O)eE3r9T=(U(tC5LYPCmEWqDGW= zPC6EqZjMMc%lZgrKb>v%_bMcq^CxXPe8K7?TPIE0dU(~UBU>luk63-{f(5s%8a{mW zEeq%0x@v^kJZ-~EHx(4z^wI|WKhjNi`oTHb@D-1o;mh&P_oYoo&V2<}GtDZhW|f*VS5(a@#0EM8i*4!w23RkPRLxv*vPfwQh$GUuv=8E&!D z{I{pltOr|fdE*x7Iq0v`Z+P{F)+-Ng!t>8~oO95+3v%+VP8^jza`<@| zL6=|zoq$9wv2H$^Mo>nNxbhM0VsH)M8AcG#HQEI@8(nV7UGb|c^A6{>&AoOu22g`F zyyPx_0I?Ogvt)Q-pQ0@h188ee9}J-Pu7BgAu3aZ}>AL99dTXfr^v`H`g0={G+o568 z80gN2pD-RZ>q$CJpvxgU+cADWpocKob3`(2N_iyOY}w7&|nse~Z}o!FJZE-pL7tP)Z%A7T+ zTh;|*C*fG&s;8#K#Kq(Y_p)D&fMQHyGujqLi3r?of_eIY;qd&V$71gA~u_FiiRDx zefeoB-oMV_I~9-H92OtHmI8k-ZxUbVkcZnGR^Nq-%xH(!aopzc6weG97^!g?8Mx4) ze}7zxL*@g~UR!j`jp#R&n88$sP7;`rrkCTSp!R2Ry+dC8{yb~m*l->eSPhW-6t+~?^DaSzrSz6f|~c;tU~od!@<0pm33h6 zrr+OLl((qv>znSs>Fc^h{?g$**?ao`qfC^xe;@uNtypz}U5@Y6u}yOrWurXVgcpv< zFa2D;<_^6e1(ybJ<_iDzw~0~^uTt`}do>$>(QWhj36hDwC@S7LKSOP4sFS7H`mzDD z@4a+l|KdAJ8^)PWZ`qwSYwLC9y`mGm;iB&mkE5J3o}orMx?D*EWelc}9OcnE0TpKM zDlliC-e*l~s1xO42mgPA%EC#fFU*Ubx%gLWjCh=>XpS8LZQEhl5RYE)Exs9uNc@Y< z!ITml3JTv=O8jZj_mk$X9IkdY9OOGnqu-)Y$5AY155%wbQ!%3vefGrU5o6@4kQ$Bn z(@H{qb7>HNHbzybrv;T@H9-x~T$RRe1(gi0(rB)h8dU>S9+h|wxGs!FSsGPhJOZxx zW46^DWS0^wGc5lSjoYj8R5s(<0XL};?#cjXj#E92&*>FP+vc&lMPfKUvmm2$31ShjI3>ARE2s{P^4e2 z6G08sT$RS_f=U5uLv)FbMwJ+cxvq}g6R_>Nt|avYmaM->=IvuP6@rF!nJ4kxPZ6jJ zW09cjNK|u7Mr~q9GU2N9E)rA>P=RFLw?JjYkYs`?@d{ZoQ?&yuEE(1F(->;|b6K() zHI&<4wGBnI{Wj!SPR8T|h1{4vYS0c@q#|3i?!ipLaQ*kSpn@HkLyari=y zsm7=Z>=2-lr`p6dLD85Ju1eKPQ0>5_+fuk_vRoP|qdEEppDUq3U`HI=+Ve)jH*!dRvb_xo1(ta zT$RRNL5%_Gj%cnoHLAwgW3(WyDL~yGjiPZWa+MfYfs6EC0N7rFg(VAh)wsQ?Bm82* zO$4sI5pH6DGshV}dWT>WY|EUSXuu}C8@36>?s)47dj_zRSSOdM3w)c1#oaLNiwg@`+_7Rn@|~OxCY<7piv>CqBmp6A_N%kEnxoYysMciy)0oO zi*Uea@_q%>Y3Ha$m3SS2BDu&SJf_>A7L)gL&hLSPGYYwCaMv5$QkcxyEl;7v=!i4XxM4s0wwBp!zjMeT5Q*tJ1hjP#HjJy$Tn}C8!$XE|Dt>DBU9k z^}I%v7#mspun2%@?Hha0FP;e*z|HgJe;a-tYS{^fjHU06V7j-^-n9J;ahNV#z8L>PwQf!%;RZ8KanuP$8oPt~hZG z04U7Wg?x$7-%w^x<|;E@R+GVHC7~^A1Ifg0><3Claao{QvM#?1P|OvRthuU<7gP>$ z(TrFWpoHrWnycEl10EEl&j!~fMj>wK;;ymrE)M=hyP3Kh>DjGuf1uT-BetL5PN#A> z)sAD5%LW&VZn~%Vj#KCG-YA;W+nW?`J*69DeYcCkWpiW0so7@t@#=$y)6K`3J^s|t z+S)N_z^tKZL((!^Cbr5RG-39LrB3;&>)GckA9JceeTPRddUxnCwJ4+K(6%kcCU%@r zFmUO7$iBo~W4(&JNe3efQ9ycjYNxEM#PNBF<-G>Qm5&@XsvR!4cM@YbG9z&*ze8a! za=5{9D{M`oWghHy#<_7^b)S}&lG>G*gXES;?4PB@pEM5Q&R8RB!sP4DUv$gNf{|-( zn^%5hSyIBBxVZMF`>*pW*Jh2oG{1CzdHe3klgsa3cEMdUbGJTmiRz2`4(S=LZ&vZk zX2vX>EiRj&#+1*?oPPJk4WDJL@6kMMt@{0hw28%|#;h7Wb?dCGA*J(%O}}Q&)Q$5p z8Xjw^5ZL#f82b;!%t9NBH&&tksb-;#RqrIF)&xDF7$fWnwh&^UE7V0is#Wh+5f(zY zCTOlotl~3QI=Fb6q?t{)$U+ELCH4id6;B73wh+P<(p-Dg>%uh%T-&1MI-v1?3TX8udb^vVYG~mttJb*$C~Jhm!fJqGzclm-xWvaTGfuF-#y;-R03~w} zjcA#J%8ctV(nu~$L3%_pS4@iLsy3cwx$M>$(Tk(G$nFYPwXsv`I0#%aqR~Pk7ui6O zYmd5-y;7T;{;knmq!>Vbgk3!sLv z{SmG*Z-vB*D;Qh%Dh3|kHkf&{|>nRK_#;N5w2=)IrlKt45+~Vc(?{h zedj!@xvISZ-Z#WWs2c-KWZ)V{91HO4h&B5!xEDGme5atidUuWL#4R)(oWMN6eia_* z))?ard{Y=AJ0>#iF$PO?9gotq9s3xs9KXnK1_Omm^#Bw5v(dJ9<1MqdoEOlxC;@+; zq2$fLBJ}?&W7>dqfV7i(7ugQf0T&6Wl0ZJ%kMR-vbl8_bt?7-1Zx0j>--YL|6YgX7 zTU>Sqq0rAZd6%M}S*a#vP5vJAvs(8t>s9t6^L<>B#-TsH8Q`9TzyEzqbI3N3ORX?Q zEB5zeG0qnT`aOW}UXIxc6ta$xt-?rGTS+#GMAikU6YebQdzOv4Als#0TgZk-&}j_Z z5tO;kou#lm0zEOCddvHmaf+TYi@;$N5MLl0_-?~9)9ByHF_0oC^%3-=Zjo%bvx#O- z6>s6202RfBw%MajH0(l~(XYD(zvjQ&4F`Pt_bKNLBr{TB_LMlkIRc5L0joqy6kkfT zL@_tnER%9)+FOTS>`z%Q%!PWqEA>rb+o1JQ3;jK3&wyT-4Y*07zn)F>f~-v+lq#}S zsj=*ftE6V({$z-3dP$jaqY=v}=uG<^k|zr1cB%~zF%GyW5Xy<>A`32D z)v!x6rp-a%(iU8}^0i#GYLTA-qNLLVBI68%NYdb}~c-!ktqoxbDx}G^iHfnW|tqRc`MPMc_KhRdhxE-=pI@IdK7BZ?}1A*xn(dNb? z*rU~)&l~2~@19{dij*u^^r-WB z!bNjCafOV^687ImfJyuBi`)tJYPQe9beVeE`xj(aS(s5%f_|~uorJd{X825$3oh_Z z5o##>6rBrRgp6?n7j(bb!)q&#s`2Th`}+e+wA!5z$p&0YFP3m)`$V;o)ROjuiELHI z``l+0#kF&_Y^*WTz0|U*oM)j?sHOIWhGJa{FP#EIM~KOOmwE|d8@W4>tPAFn9A_?Y za|LG{5*%d}j(gPsZm2swk`MZ*!`y=wkHvft%zcQsr<)~VHsdnn+8Nab>sze?`O&ZJ zoAE}LFB^Qh|577~i&hm@2PoWPY8HpmVWoV@RJKz^HE3_s4D>)CndCqzw=6l(8e~do zT|xDL52M!=ILG3=1(alBDAO2EW%flqb-szYIFlhaB&Nv^Kox;dJQRy#zlz3O`S3zSvs3bOEO z2eXApv%RLPiZjboagSw)clRT&g8f~*WB81(Uz{nsV$UeN>f_xv zd~{Xe+Gj7j?3uNH3}OxrpC-=E!rQ;~J@=k0Ej_sx5A5{3=7}Xsp18(c%@I_OVQpKC zN$fbClPieu_8_}*5-()h$gd1Cx@IT$NKY(R$)WOdI(D?}w%EK!E8da>qLtni_-FT& zVZpLDKJk{?p+i&7B&u~$L4JHDu9I1pzkSKz{PbxnNA231o1jx8Nv*R7Lg3<4R`%(a z)WhcT+ADJVFRLswk2XwSylqDJ(!=g+O#(mo2FTTi8e#^Zm2jmBd1}h#S;B6IT_~kN z-nwguM` z*!10jRNK6x_Qx@sP~LVQm8v!oDzi;Q-g&OK&Ti?Df3ZVQ>O8N3Py=z<)lQ$w`v96x zvUi8lO!N)9Da%{yt-=oFpZPn*AQp~lhSJn`z=zD7m2W{p#2R+{&^ch4!DZzd590wKjAXjX9zSaFG5A2D%AagqE+8UD3V{eD*Z91Iy6DCWP&O& zZez(z)fup`{&8iHZE~4kv1Ao$0hehdAb!(j@>n5%*dKRg7pFlNymrGb7G)d+1N-aS zf@)(Oc8~x?BX|*O40CIHWK`+xnqa*J_h-p^ORV*a#00$YAuq!5E;d=aIGa_LrYFlS zd(eQ{Saa}GsWi1VIc)A;d!`wc?+74`(75Kf>vm}cT2s*dLr^p_1yzX`WO=OgZ-T;G z&;e?XS|cdR5S9hKNVsA&stV^oNOPRwKR-axNJ$D23KpAYo8^Qtdvx?W8nuDg0%|9* z1!bkwPHcH}I|+u`NifwV#1oxq7YxZw7|Kv&vD|D_wn$sjO}}fR?vm+=-DpW|I|uj4 z;DneH($}H!npyPzOWF5y#=Lm+RWoL}_LF4EEUQ@W*9^`-oxZ+FV``1C)}*#oo4B@Q zL3wx*hTjV!iv}Ype$O#!rGaRrm4W^l<&fS4Rc+KsD-mjXG>SAQs6A?k9oA&2~$#rZQ3Mb$9D^y&>V~PLj=M9 z%Rs&H+m+7+fx2ECJkx8I?vv-xE{MoL9r_)N@WWsW@(uT^vXBt$mWZ@Kq=>G$4+TpmZCdVDa7ZJ(g_sKJ6t zMU1MgpKwtxB~;L_tX9D2(b5Iu@u^Th#BYclfjRu4iymqvShVYGjNe1OBS7s7Wgzp_ z=X@HqhuwFvk1gR*I|=I%$Gx!u?w5%Ca~ap~e2P*IyPr}o^?nELyF-2P`~Lp#J+B_W zhp$h+&k06CPvmF63iShSa4RmyD?;WK8Mc1N)*biy2IXK?#%a#D(LUdwBtzRg)-bLQ zX2&6r`UOTk>9Z|oHfw0dD7O9Hk7jIW1$6K{>->oOPw!JB2{#y+ylZg}5oXeUT-FwH zm?6>@KA%7drTjTff5r+vzDG_L-*aTopAp&n_z(DVKmJV7%~1Xf8{mJBH7@)fYh3a< z)P+9}3O>_%3cf#=KW~G+PQrqRsGs`3-@xBvXyG&GHTm;!{)~FtwfuQBe;$UyXp?8)xpH@lk!eU(!sQ zogRVO?-pAxGS_oHDwnx9!+$v7dXUUkl1y?$vS&bdJdNP;bxXz165GxPETe_tU_2<1Exs z##N|CR5rCj2>zyI5@0=eWx;s?`CjB_#;B zY0d?O6T5brv~csBnO83ve9`lpF4(iEVEgo8#RZePcAT>8>iP3_E59XSK4#w zi!t?U-aYUhYq#3?Gw&WSv8hV$9{7)zira`~K^vh?+_wB#EEmc4cP<6%q-g7El_`4^ z&~3t{;8#uTQ>1-sY@dkrJuc;OU23Gg;@xL#&wBCqjqO(>dnG%+c|=ZNJnzck&r|sG z0gT8OonJwtGJ97e`udW3BGuv-fjwwEuJ>VF|ASd(!uwRR3lBRXd(p{!18>E}l=Go= z+Vyx=C)T_9C)OV=Ej>^^a%B1bh!g9(=J)M8fA^44SK!3@?1D3%Sl^OIEyS5{wl0`e z@EH~!@7vSu4GY6{CvX1W@4trUXINQmZNHGwY5V{yz2u1dU5fT&)Y;?K8x zKbg<+%DsbM#~6;P<<_PhI`8vdqwdjNOl(>gh90!rO)JjqDQ95aYQ@>eyIZ6424uz932sEX#P=-jzjLfR24 zmM~YXm)WcZ;5siZ`vGY`3@i`p|4ipC#2(Q)x!9vz;}scu#HGjH4d}BbTgfL_kJq`g z?N4~5zrmlophvtK0*h+LWgWD$jnf=!w@y*2zwS9H7;C3#XPZK{6fzyO-uM2%xOUio zb%n;wcc;eUCSZ8o@FdXci|mWIoNew@+_Xfyx(3VN9rP_|xY#YQm$Ie#Gx3_G>VEGi z^k$ChYZy9%xY{kW?&Ws)+DG;C9wyXSXnRG_Z+C(#;)ENx-exYOb|Sb?V&s_@pw9DA z%_2}dQdn=JSgPO^TgabIIDBy>6=LJIcbQKs2( zc@Fu?M=jzxgi(49Y0OP+^wj^wsR8I)xX7aPWC?!m1frZlpc!C-k2~|JB(Q{B*3(wj z3$F*Vt=Hoge%}Ah{^%RAW3W%?Pw0bleVM+5GfqD5I2Zxm^u$(f9`236Ee&X^`&cIa z%zX@>(RWLX%b*GRU3QeOEN$qZ&eDb@>}$p!3pxGKh}(4^msTX7T3WG0O=aI(aADuO z7~a*=nD)SN-$uDdk!hhk)=XIMaGbu{`_j7CIRPB?Zk$6I)4-@gY=GaZOJ&=^yGF%! zl;LV;-U-qItF}jjF(6jqCUA*WC^L58loYHhp?({n#12tSg{uVTL1f1TeDsK#3LDi_ z*h+8@vBVFA3)hr=Gu|DG(ajn@^BF>kGoA+@tOMiDe6naWV$na*Os`V81z{2Gmeit{ zB*<#;_c1tmMBfvSio+foS^>>Ri}Erz_XPf(a}qfGj>{{@E)K%w09T`RdmZ=>#~{CC z{N2a>sAh8;jDw$WmBy`y*ZrD9@>5nDEwxhizvUqB5^gE>s=1}at1dI%<(4u#V9%Ov zDe+5apUooXDqF}qMhT;L*@%Bm?IcVk#;e>;az-iKBZccl&2{FJOu|OH%!I90jquqh zg}qGIlG!5W|61cXY&P{SSQ#Y!C4Tp zeC`KPv;HCb71Rp(DVg*u{Cp7R{12k${6qFg#`#nJjsPc?v<})7OIl|iRm(8atVHBm z*O8K_RJNm`H9(0St+Th|tQKajQ3xCE4N$_hHQ+MOGd^cO&$j5;HM=(fo1GCFAFu)U z6k9T!%mdvn-7>5OAz_~OY_Y#-cbFue7s>cs4ucByENY{ z<9_ce<|Fxd;%6MUZOj1SGkRhF56 zoaE(SQmdyvYE5ECZ^yCOras#%zmz&Wq3bXQCz7U#PS9py9ZL_mLf2uAvCBx%I%%xq zFkiu0@)i6$zSvAH-(2HqHA&02HnPp2W6Bgro6W_3<7QeuPV0z{*$bg)OZvct;G=Wm zHXmN(jRg^H_Mqmw5PZp+kCRCeZALo?!3WLrS;)vX>!RCCpN@=dGyi-f{}SgTBin4c zZZqn3yw6#FdBXjSPDPKIFgE;?++;a-=65bq)?YZuM)0rgTruew^hDAB%3``rx zJ#7QT>p!IV>g@g>=(3v^$~fjxOXl%x5IjGK}3AU--Y7w+pMUvbRm zTE3aM8Dc8)(XlyBPigu3g{Uuyd^3$Z(O$$y2j{}}M0EGM=9`6iGDFKZLCYsCu~YNS zGHzviBJzdp3HK8|In>9u3b?sK_m|X19eO6~xVQP(Y+K)D;kPyQ99o4YiP1Otd^`V= zdQj@4=5Xmj$7U;i;$40TpGSENw><^qi zf5zTo=t7``t5h>#5nMU`?~Paj#93xITOC>e8$nXtudO9 z(F(YZ;BQc(Iy)r zY=`V?ydMZR0;xOLt6=;+1Ya0C+JPIvnmuHH%6zJwv~_^n1Mlb=WIci%_lN9pj5BE~ zUPQUFLL`l?BkqyzQ;%1!r3ni_h5&!UyZqP)ezz8_V2iIjg!LFyD1mxR^+O) zKVbW8b_JKV&n#DHHc%p0oqZLb#ISmS>z<%zi(E9<3s((JZ_!+D4nSf+&-GHrr!-d$ zZb;@eoJ?@(voH_2?>WDrT?IbZA^T0S&E2_gY>8+~`?$SFvmLV6NgwKiG9%iuqx%W3 z>jr!257{+1e}=iE1^12h0beh7ob?d*4T@hecR2ChGP3pEVrfPEi2&CTbA4?Ll{?yR z^}TY|xH;ycvOpsebFKkO*y`+w(l>CRH{3UbtC{Ai!O49(y`^Xmyx!d|(J-x35*PMb zui&#VbYkqmpu7sNy}c^9DZuDu=BWFT^N%sc8%Mn5uwC{rgs{iG4Tu=%6gG#SVwHxv zRgq_fkU#tYdPLjBnTDa)N_)FiRwZla2dXttbI*W!5?mX39C8G0mRcA;#MQB#3Uif% zYbSI4BwQx09VS! zR8XD+y^^_194Ms{vv6~k-Z^5s^XxpD?bUR@j(?Z6_7Tk@P_>BupW`^3N-~W$=nu1S znucB+#U7n=Iv_O@*8=yI#PR!Iv~`xQIxM1>IrwVEt~j6)etVQICHPR?dGDJSj^6Na zg_~2T*6un~Sa@icyGre7sI$7Se00OSTS~KVE6Lpp?wC94)&=VCxD+nB+;-Va zF2DSLQY^|HkNTora3@1EqfIt$_KdMj$1Jz?6LVOYZcP_DQqAmc^I3yral=Q24If#h)GqZWtTFW)8s0H`Sv%^VC2rgBR|lZmSgu`ab{zH)n@)Ui z^|QXsrhD=*kat%q75@)d<<4}MiEbc09gh?cq*R8se5CJL&<<%Z8J zcau!D%*}8I;8kfi$(lkHYS>`zXc*Ga!Q9)h$o$xRtzj9L<*x}xmet($%aW@bf3~ds zYJ$pL>h4h26}u0(4-~5%?sa&0vkrZ6korx-Hw}MOpSW$!zGjl!X37-I=DceMmLz0g z*A6~YTTNzLhJFKEmg7C>UPyjSf@vDZRctV~Tw*@&9JE>>FT9<9hZ)Uo&MU;QGT>PO zkK40nUBKXy;{C)Xor(Aqk!X>3*|nkW^g?yz?`XZvK~S5A%bD~CbN9VO~cvI@*#^! zC)!DW*hRv)i&%daZ#CBX^FDx~xrysLTX5DvpRX=l;?92WR{z`nX*ZHw?B3cVRHzi0?S__4aYM2wyjx<%r;`K-@sNcmEzH>_~;Sx)w-XtPZHeua>(8y^`JwZ5jYxy|9KY`jlowrg3jksaZ4B9AVY)8=s=B5 zw&H}BM|X}uwhUi)$FL#@oyff&zX^0)>*Yn>IRf2HiNyUXz@e^@_j&+#RO&!`IU?$C zJ90ejxh)E$T&ok~U{<4W;N$&QX*fenee#g=i0G~r`~5n0ybritoZ~6bU!9^e5at0hEmUL-ze#2eT{R zKB4Qd!~K?Sp8!tA{UK~iNv2qcUKBp==@@Jg@?#M9t7n1{wBC~P+dgqCwA#o)PwDk zS!jQTQ*~so=~zE2kNa{r==gD7@6BTE)gXV%@oQVmO*($X++ZDYUIE*Dpb&?l!ytwr zjGmdgLBo%;hK1>neMB@I&z47PxDR?e?S~aETG{D;M%-xh&Iabg`VIHu_ZVll!*-U4 zQ8Cs1%sIw<3w&J88F7ms-x}r{BRKnM#`&XG%Qpw|)iB>;A2;tzIQJB?8(o~&X&=l) z$fwHPeP{{J$|V~{OWljxL&NPIM7ne)ku19Rtovu|m3tpL{m5xy<9ypF`Fp(k9{!G< z2zD9$U3vcB&!NBLw}zVT#NzL+|MvmzN&LMtPBUdW&G7esA*3Vz>CC?qJY)FpUkToQ zg8ojU#J+%d%mg35oq1SJoURFxoWP+r7TyTP=jrEq$hQ1cJ9S`7>n2 z=jR}AGJkgU_jH~rMCYjl?@Z>;r-ILXY6%ZG@Mk_dgi|&lI$`9?^B2bd$e;V8d^-CQ zqSN5`z60uI@@HrepXqy-KVvTj^!hqx^s_k)%sfAHB7>6$JZ?xk4-m?PHZUS0PdrDX zE~j%FCSo?WVEKrZF>+#In;1rt^0^_d>Gv#oBh8()S#<}!a zTspgoylI6>?~FY*Kk=EubGRneYr2qx9kCfqW6q{;TuvoZJ;BM^;m^`nNRn|}Mmv_I z8tYnAij|CPk1hqMCOsmuN1dTZ*d4eObY#EO$R5IHT1l^TJ{I3M?lDn41QA~m_LH5k z@58yJF~&H07rV1E%`szIkIl@?9@saoJTb2ICcL!pdiv{c;3O&K8Dukh_aV5Oa40|c zC{IxOuh`*Qj-J6=l>98kV0uc6khtWqJKf7@CRE;a#qAR&-EryMdyDhOufKif(tFm9 z8eVcQ-XtyCSDKl#zPfnYJzMh&)S~KDm)>7EV8ONX?s~4k96EYr$%F}IIiuE2pSpf# zzupDw^K;kc_vznj+S*A48)v5XD%vtX|I*y-i}SM=j%eTT4>wbx``b9vSY>avgZD%d z5k1G#-H~{oA~69^mC!-iq+jFQvw!Lt!6r#bDGmBx{YJ$-1<_AboPL*|H#Bh{K@r_Y zVD!TMDs8jl+9hr3-J?g}z7F48LJtIt<;zu~Ug^NgF5ROqeH`jgVgLD|bN9J||M1rN zMBe2OE%doHLvlGN)JLm#a=m6`e_;7xu^4~Rs#AdyKCVQLns+B zWfu{~Y>yG6F&9DIpWD}?@3r8*_Z+NrJ>KEtbE!DH*r0yTe{YFC|0YF!<_$7N&Br-{ z47G!g6LF3bZ=kRpxyc{Na;tzv3u6lczck;SXhob$gm2x~Yld1Oa)Q@Lc0l%i<*FlW z2kf@+rF9OlF{;8`C8#z{P;4FjePn!gmg2?EfQ5I$vBtSfSYymexrn^Zfh=^_apaEO zeQE{1Ck3J6O!pmRTu;G=<`M-Ar|Dccf z{VnK6Io1(K9Q%keS|T%)aWMwuE!JV{NIkqT+gR^??M!x_HIk4C>4F!w@XC6Zq#m(t zlg^1vqL(-6wjer@uz?;_O2&WTY9r(d@p=;d0spIl|K%^~FYE32SoK(PS}W7)-Mm#R zHOI!D!vl7F%;_8LgjjnmzILa#?AbDy|ty|PvJgwvOVn%N^0lbzqU>C%J-K#@6YrzG>a3jl05fTX##WZ&%o=ZyRL4!dmH!*WQezlGwH-teteE2pj9CSEO;p fb5BwFx*U&tEvs3%qITJ`+9=&kTUNV5e`ov;D~b(V literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..40f3c644f824e153ca3c0a8fa4a1193be8778543 GIT binary patch literal 93092 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

vsW&K$hhs(y@$oKV=ek~~~tEy>%8nW43 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

4HfsjsgI*Vcy1YicZw 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 literal 0 HcmV?d00001 diff --git a/src/EPPlus.Fonts.OpenType.Tests/Integration/LayoutSystemTests.cs b/src/EPPlus.Fonts.OpenType.Tests/Integration/LayoutSystemTests.cs index 0374c10028..8a84785b28 100644 --- a/src/EPPlus.Fonts.OpenType.Tests/Integration/LayoutSystemTests.cs +++ b/src/EPPlus.Fonts.OpenType.Tests/Integration/LayoutSystemTests.cs @@ -203,7 +203,7 @@ 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 wrappedLines = layout.WrapRichTextLines(fragments, 225d); diff --git a/src/EPPlus.Fonts.OpenType.Tests/Reading/TtfReadingTests.cs b/src/EPPlus.Fonts.OpenType.Tests/Reading/TtfReadingTests.cs index 0b7ff81efd..db571d118b 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); 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/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/Integration/RichText/LayoutSystem.cs b/src/EPPlus.Fonts.OpenType/Integration/RichText/LayoutSystem.cs index 6cabaf5c45..8bcd664061 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,7 +251,7 @@ public TextLineCollection Wrap(double maxWidth) return new TextLineCollection(); } var inputRt = InputFragments[0]; - var shaper = OpenTypeFonts.GetTextShaper(inputRt.RichTextOptions.Family, inputRt.RichTextOptions.SubFamily); + var shaper = _engine.GetTextShaper(inputRt.RichTextOptions.Family, inputRt.RichTextOptions.SubFamily); var layoutEngine = new TextLayoutEngine(shaper); var wrappedLines = layoutEngine.WrapRichTextRuns(StyleRuns, maxWidth); diff --git a/src/EPPlus.Fonts.OpenType/Integration/TextLayoutEngine.cs b/src/EPPlus.Fonts.OpenType/Integration/TextLayoutEngine.cs index c1bb3178a6..fe56ac7c56 100644 --- a/src/EPPlus.Fonts.OpenType/Integration/TextLayoutEngine.cs +++ b/src/EPPlus.Fonts.OpenType/Integration/TextLayoutEngine.cs @@ -230,13 +230,16 @@ 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) { + var ts = _shaper as TextShaper; + if (ts != null) + { + var resolved = ts.GetShaperForFont(font); + if (resolved != null) + return resolved; + } + return OpenTypeFonts.GetShaperForFont(font); } diff --git a/src/EPPlus.Fonts.OpenType/OpenTypeFonts.cs b/src/EPPlus.Fonts.OpenType/OpenTypeFonts.cs index 4a74778c1d..73c0219a01 100644 --- a/src/EPPlus.Fonts.OpenType/OpenTypeFonts.cs +++ b/src/EPPlus.Fonts.OpenType/OpenTypeFonts.cs @@ -172,5 +172,10 @@ internal static List GetLocationsCollection( { return OpenTypeFontEngine.GetLocationsCollection(fontDirectories, searchSystemDirectories); } + + public static OpenTypeFontEngine GetDefaultEngine() + { + return _default; + } } } \ No newline at end of file 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/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 /// From 80c92b56645162be88651803212250a29938e49b Mon Sep 17 00:00:00 2001 From: swmal <{ID}+username}@users.noreply.github.com> Date: Fri, 26 Jun 2026 16:14:02 +0200 Subject: [PATCH 82/82] Replace global font singleton with per-workbook RenderContext in render stack; split FontAvailability into result enum plus RequireExactFont policy --- .../SvgStandAloneTests.cs | 9 +- .../Shape/ShapeToSvgTests.cs | 64 ++++----- .../TestFontMeasurer.cs | 3 +- src/EPPlus.DrawingRenderer/RenderContext.cs | 51 ++++++++ .../SvgItem/SvgParagraphRenderItem.cs | 9 +- .../SvgItem/SvgTextBodyRenderItem.cs | 8 +- .../Textbox/ParagraphRenderItem.cs | 15 ++- .../RenderItems/Textbox/RenderTextBody.cs | 7 +- .../RichTextBenchmarks.cs | 2 +- .../FallbackFonts/FontProviderTests.cs | 12 +- .../EnsureWrappingWithFreeFonts.cs | 2 - .../Integration/LayoutSystemTests.cs | 26 ++-- .../Integration/TextLayoutEngineTests.cs | 66 +++++----- .../Reading/TtfReadingTests.cs | 2 +- .../TextShaping/TextShaperTests.cs | 3 - .../Integration/RichText/LayoutSystem.cs | 2 +- .../Integration/TextLayoutEngine.cs | 22 +++- .../OpenTypeFontEngine.cs | 41 +++--- .../Fonts/FontAvailability.cs | 4 +- .../Renderer/Chart/ChartAxisRenderer.cs | 4 +- .../Renderer/Chart/ChartDrawingObject.cs | 2 + .../Renderer/Chart/ChartLegendRenderer.cs | 10 +- .../Drawing/Renderer/DrawingRenderer.cs | 5 +- .../Textbox/DrawingParagraphRenderItem.cs | 20 ++- .../RenderItems/Textbox/DrawingTextBody.cs | 15 ++- .../RenderItems/Textbox/DrawingTextBox.cs | 3 +- src/EPPlus/Drawing/Renderer/ShapeRenderer.cs | 2 +- src/EPPlus/ExcelWorkbook.cs | 122 ++++++++++++++---- .../Drawing/TextMeasuring/ReadMeasureTests.cs | 3 +- 29 files changed, 328 insertions(+), 206 deletions(-) create mode 100644 src/EPPlus.DrawingRenderer/RenderContext.cs diff --git a/src/EPPlus.DrawingRenderer.Tests/DrawingShapeRenderer/SvgStandAloneTests.cs b/src/EPPlus.DrawingRenderer.Tests/DrawingShapeRenderer/SvgStandAloneTests.cs index a6b824b6de..df5dbb4697 100644 --- a/src/EPPlus.DrawingRenderer.Tests/DrawingShapeRenderer/SvgStandAloneTests.cs +++ b/src/EPPlus.DrawingRenderer.Tests/DrawingShapeRenderer/SvgStandAloneTests.cs @@ -2,6 +2,7 @@ using EPPlus.DrawingRenderer.RenderItems; using EPPlus.DrawingRenderer.RenderItems.SvgItem; using EPPlus.Export.ImageRenderer.RenderItems.SvgItem; +using EPPlus.Fonts.OpenType; using EPPlus.Fonts.OpenType.Integration.DataHolders; using EPPlus.Graphics; using System.Drawing; @@ -119,7 +120,9 @@ private void GenerateTextBodyFile(string fileName, GroupRenderItem baseGroup, Sv private SvgTextBodyRenderItem GenerateTextBody(GroupRenderItem baseGroup) { - var textBody = new SvgTextBodyRenderItem(baseGroup.Bounds, true); + 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"); @@ -270,7 +273,9 @@ private RenderTextbox GenerateTextBox(out GroupRenderItem group) group = GenerateGroupRenderItem(); var textbox = new RenderTextbox(group.Bounds, 500d, 500d); - textbox.TextBody = new SvgTextBodyRenderItem(group.Bounds, true); + 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"); diff --git a/src/EPPlus.DrawingRenderer.Tests/Shape/ShapeToSvgTests.cs b/src/EPPlus.DrawingRenderer.Tests/Shape/ShapeToSvgTests.cs index 76f3640038..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]; @@ -500,7 +495,6 @@ public void GenerateSvgForCircle() [TestMethod] public void SuperScriptShape() { - ExcelPackage.License.SetNonCommercialOrganization("EPPlus Project"); using (var p = OpenTemplatePackage("Superscript.xlsx")) { var ws = p.Workbook.Worksheets[0]; @@ -520,7 +514,6 @@ public void SuperScriptShape() [TestMethod] public void SuperAndSubScript() { - ExcelPackage.License.SetNonCommercialOrganization("EPPlus Project"); using (var p = OpenTemplatePackage("SuperAndSubScript.xlsx")) { var ws = p.Workbook.Worksheets[0]; @@ -535,7 +528,6 @@ public void SuperAndSubScript() [TestMethod] public void OpenRightAligned() { - ExcelPackage.License.SetNonCommercialOrganization("EPPlus Project"); using (var p = OpenTemplatePackage("SimpleChartRightAlign.xlsx")) { var c = p.Workbook.Worksheets[0].Drawings[0]; @@ -547,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]; @@ -576,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"); @@ -642,7 +632,6 @@ public void GenerateShapeCenteredParagraph() [TestMethod] public void ChartAndShapeGreen() { - ExcelPackage.License.SetNonCommercialOrganization("EPPlus Project"); using (var p = OpenTemplatePackage("ShapeAndChartTestGreen.xlsx")) { var ws = p.Workbook.Worksheets[0]; @@ -662,7 +651,6 @@ public void ChartAndShapeGreen() [TestMethod] public void CreateChartsWithDifferentSize() { - ExcelPackage.License.SetNonCommercialOrganization("EPPlus Project"); using (var p = OpenPackage("ChartWithDifferentSizes.xlsx", true)) { var ws = p.Workbook.Worksheets.Add("Chart1"); diff --git a/src/EPPlus.DrawingRenderer.Tests/TestFontMeasurer.cs b/src/EPPlus.DrawingRenderer.Tests/TestFontMeasurer.cs index 27a5333788..8eafec0698 100644 --- a/src/EPPlus.DrawingRenderer.Tests/TestFontMeasurer.cs +++ b/src/EPPlus.DrawingRenderer.Tests/TestFontMeasurer.cs @@ -178,7 +178,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); 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/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 7d5308eab5..8716dae0ce 100644 --- a/src/EPPlus.DrawingRenderer/RenderItems/SvgItem/SvgTextBodyRenderItem.cs +++ b/src/EPPlus.DrawingRenderer/RenderItems/SvgItem/SvgTextBodyRenderItem.cs @@ -8,23 +8,23 @@ 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/Textbox/ParagraphRenderItem.cs b/src/EPPlus.DrawingRenderer/RenderItems/Textbox/ParagraphRenderItem.cs index 6ac4496d1e..955924adbd 100644 --- a/src/EPPlus.DrawingRenderer/RenderItems/Textbox/ParagraphRenderItem.cs +++ b/src/EPPlus.DrawingRenderer/RenderItems/Textbox/ParagraphRenderItem.cs @@ -115,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) { @@ -129,26 +134,26 @@ 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(BoundingBox parent, RenderTextBody textBody, IRichTextFormatDrawing rtFormat) : this(parent, textBody, false) + protected ParagraphRenderItem(RenderContext renderContext, BoundingBox parent, RenderTextBody textBody, IRichTextFormatDrawing rtFormat) : this(renderContext, parent, textBody, false) { AddRichText(rtFormat); } diff --git a/src/EPPlus.DrawingRenderer/RenderItems/Textbox/RenderTextBody.cs b/src/EPPlus.DrawingRenderer/RenderItems/Textbox/RenderTextBody.cs index 49c3534530..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; 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/FallbackFonts/FontProviderTests.cs b/src/EPPlus.Fonts.OpenType.Tests/FallbackFonts/FontProviderTests.cs index 80c6c84dab..b713ae797c 100644 --- a/src/EPPlus.Fonts.OpenType.Tests/FallbackFonts/FontProviderTests.cs +++ b/src/EPPlus.Fonts.OpenType.Tests/FallbackFonts/FontProviderTests.cs @@ -80,7 +80,7 @@ public void DefaultFontProvider_MixedTextAndEmoji_ShouldUseMultipleFonts() } [TestMethod] - public void DefaultFontProvider_EnsureLastFallbackDoesNotThrowOnExactAllowEmbed() + public void DefaultFontProvider_DefaultBehavior_DoesNotThrowOnFallback() { var engine = new OpenTypeFontEngine(cfg => { @@ -90,7 +90,9 @@ public void DefaultFontProvider_EnsureLastFallbackDoesNotThrowOnExactAllowEmbed( cfg.SetScriptFallback(UnicodeScript.Latin, "Archivo Narrow"); }); - engine.LeastRequiredAvailability = FontAvailability.ExactAllowEmbed; + // 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); @@ -99,7 +101,7 @@ public void DefaultFontProvider_EnsureLastFallbackDoesNotThrowOnExactAllowEmbed( } [TestMethod] - public void DefaultFontProvider_EnsureLastFallbackThrowOnExact() + public void DefaultFontProvider_EnsureLastFallbackThrowsWhenExactRequired() { var engine = new OpenTypeFontEngine(cfg => { @@ -109,8 +111,8 @@ public void DefaultFontProvider_EnsureLastFallbackThrowOnExact() cfg.SetScriptFallback(UnicodeScript.Latin, "Archivo Narrow"); }); - engine.LeastRequiredAvailability = FontAvailability.Exact; - Assert.ThrowsExactly(() => { engine.GetTextShaper("Archivo Narrow", FontSubFamily.Regular); }); + engine.RequireExactFont = true; + Assert.ThrowsExactly(() => { engine.GetTextShaper("NonExistentFontFamily12345", FontSubFamily.Regular); }); } [TestMethod] 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 8a84785b28..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); @@ -205,10 +205,10 @@ public void EnsureRTCharIdxBecomesCorrectWhenBreaking() 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()); @@ -408,7 +408,7 @@ public void TestLayoutSystemMultipleParagraphs() new TextFragment() {Text = lstOfRichText[1], Font = font2 } }; - var layout = new LayoutSystem(fragments); + 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 db571d118b..511dff8e7d 100644 --- a/src/EPPlus.Fonts.OpenType.Tests/Reading/TtfReadingTests.cs +++ b/src/EPPlus.Fonts.OpenType.Tests/Reading/TtfReadingTests.cs @@ -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/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/Integration/RichText/LayoutSystem.cs b/src/EPPlus.Fonts.OpenType/Integration/RichText/LayoutSystem.cs index 8bcd664061..1dab196d93 100644 --- a/src/EPPlus.Fonts.OpenType/Integration/RichText/LayoutSystem.cs +++ b/src/EPPlus.Fonts.OpenType/Integration/RichText/LayoutSystem.cs @@ -252,7 +252,7 @@ public TextLineCollection Wrap(double maxWidth) } var inputRt = InputFragments[0]; var shaper = _engine.GetTextShaper(inputRt.RichTextOptions.Family, inputRt.RichTextOptions.SubFamily); - var layoutEngine = new TextLayoutEngine(shaper); + var layoutEngine = new TextLayoutEngine(_engine, shaper); var wrappedLines = layoutEngine.WrapRichTextRuns(StyleRuns, maxWidth); if(wrappedLines.Count > 1) diff --git a/src/EPPlus.Fonts.OpenType/Integration/TextLayoutEngine.cs b/src/EPPlus.Fonts.OpenType/Integration/TextLayoutEngine.cs index fe56ac7c56..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); @@ -232,15 +244,13 @@ private double MeasureText(string text, float fontSize, ShapingOptions options) private ITextShaper GetShaperForFont(IFontFormatBase font) { - var ts = _shaper as TextShaper; - if (ts != null) + if (_engine != null) { - var resolved = ts.GetShaperForFont(font); - if (resolved != null) - return resolved; + return _engine.GetShaperForFont(font); } - return OpenTypeFonts.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 08c3584a8c..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.ExactAllowEmbed; - bool RequireExactFoundFont { get { return LeastRequiredAvailability == FontAvailability.ExactAllowEmbed || 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,26 +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) { - var fullFontName = font.GetEnglishFontFamilyName(); - if(LeastRequiredAvailability == FontAvailability.ExactAllowEmbed) + var availability = GetFontAvailability(fontName, subFamily); + if (availability != FontAvailability.Exact) { - //If we fallbacked to the correct embedded font despite not finding it in specified folders there's no reason to throw. - if (fullFontName.Contains(fontName) == false) - { - throw new FileNotFoundException($"Could not find Font: {fontName} fallbacked to font: {fullFontName}"); - } + throw new FileNotFoundException( + $"Could not find Font: {fontName} {subFamily}. Resolved via fallback to: {font.GetEnglishFontFamilyName()} {font.SubFamily}."); } - else - { - throw new FileNotFoundException($"Could not find Font: {fontName} fallbacked to font: {fullFontName}"); - } - } - 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}"); } shaper = new TextShaper(this, font); @@ -167,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) @@ -183,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.Interfaces/Fonts/FontAvailability.cs b/src/EPPlus.Interfaces/Fonts/FontAvailability.cs index be33bde183..213f632459 100644 --- a/src/EPPlus.Interfaces/Fonts/FontAvailability.cs +++ b/src/EPPlus.Interfaces/Fonts/FontAvailability.cs @@ -26,8 +26,6 @@ public enum FontAvailability FamilyOnly, /// The exact font family and subfamily is available. - Exact, - /// Exact but does not throw if the embedded fallback font is asked for - ExactAllowEmbed + Exact } } \ No newline at end of file diff --git a/src/EPPlus/Drawing/Renderer/Chart/ChartAxisRenderer.cs b/src/EPPlus/Drawing/Renderer/Chart/ChartAxisRenderer.cs index 434d9a25eb..a8535bb3cb 100644 --- a/src/EPPlus/Drawing/Renderer/Chart/ChartAxisRenderer.cs +++ b/src/EPPlus/Drawing/Renderer/Chart/ChartAxisRenderer.cs @@ -179,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; @@ -300,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(); diff --git a/src/EPPlus/Drawing/Renderer/Chart/ChartDrawingObject.cs b/src/EPPlus/Drawing/Renderer/Chart/ChartDrawingObject.cs index bf5cec5c4c..2c122aa9fd 100644 --- a/src/EPPlus/Drawing/Renderer/Chart/ChartDrawingObject.cs +++ b/src/EPPlus/Drawing/Renderer/Chart/ChartDrawingObject.cs @@ -40,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; diff --git a/src/EPPlus/Drawing/Renderer/Chart/ChartLegendRenderer.cs b/src/EPPlus/Drawing/Renderer/Chart/ChartLegendRenderer.cs index 3cde9d7348..f4efe308ed 100644 --- a/src/EPPlus/Drawing/Renderer/Chart/ChartLegendRenderer.cs +++ b/src/EPPlus/Drawing/Renderer/Chart/ChartLegendRenderer.cs @@ -44,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) @@ -488,7 +488,7 @@ private void SetTrendlineLegend(ExcelChart ct, int serieIndex, int entryIndex, D 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 == entryIndex); var headerText = tl.GetName(serieIndex); @@ -547,7 +547,7 @@ private void SetPieLegend(ExcelChart ct, int index, DrawingLegendSerie pSls, eLe } 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); //var para = sc.Chart.Legend.TextBody.Paragraphs.FirstOrDefault(); sls.Textbox.ImportParagraph(Chart.Legend.TextBody.Paragraphs.FirstOrDefault(), 0, catValues[i].ToString()); sls.SeriesIcon = si; @@ -592,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); @@ -638,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); 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/Textbox/DrawingParagraphRenderItem.cs b/src/EPPlus/Drawing/Renderer/RenderItems/Textbox/DrawingParagraphRenderItem.cs index eb29a9f132..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); } @@ -222,7 +220,7 @@ private void ImportAlignment(bool isAutoSize, double maxWidth, double parentWidt 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 a3d5851673..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; @@ -26,14 +27,14 @@ public class DrawingTextBody : RenderTextBody 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(); @@ -205,12 +206,12 @@ internal virtual void ImportTextBodyAndParagraphs(ExcelTextBody body, ExcelHoriz 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) { - return new DrawingParagraphRenderItem(textBody, parent, paragraph, textIfEmpty); + return new DrawingParagraphRenderItem(RenderContext, textBody, parent, paragraph, textIfEmpty); } /// @@ -221,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 ebbafe5446..13a6eac205 100644 --- a/src/EPPlus/Drawing/Renderer/RenderItems/Textbox/DrawingTextBox.cs +++ b/src/EPPlus/Drawing/Renderer/RenderItems/Textbox/DrawingTextBox.cs @@ -23,7 +23,8 @@ private void Init(ExcelDrawing drawing, BoundingBox parent, double maxWidth, dou { Parent = parent; _drawing= drawing; - TextBody = new DrawingTextBody(drawing, _marginGroup.Bounds, true); + var renderContext = drawing._drawings.Worksheet.Workbook.RenderContext; + TextBody = new DrawingTextBody(renderContext, drawing, _marginGroup.Bounds, true); TextBody.MaxWidth = maxWidth; TextBody.MaxHeight = maxHeight; } diff --git a/src/EPPlus/Drawing/Renderer/ShapeRenderer.cs b/src/EPPlus/Drawing/Renderer/ShapeRenderer.cs index 03323b4a2c..e00d01666c 100644 --- a/src/EPPlus/Drawing/Renderer/ShapeRenderer.cs +++ b/src/EPPlus/Drawing/Renderer/ShapeRenderer.cs @@ -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/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/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;