From 0fdf13c531e87119b285ec2eac0fd88a5f1da65c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jan=20K=C3=A4llman?= Date: Wed, 21 Jan 2026 14:13:53 +0100 Subject: [PATCH 001/151] WIP:Refactor --- .../TestTextContainer.cs | 30 +- .../DrawingBase.cs | 20 +- .../DrawingChart.cs | 149 +-- .../DrawingShape.cs | 2 +- .../ImageRenderer.cs | 72 +- .../RenderItems/RenderItem.cs | 25 +- .../RenderItems/RenderItemType.cs | 3 +- .../RenderItems/Shared/DrawingBaseItem.cs | 27 - .../RenderItems/Shared/ParagraphContainer.cs | 38 +- .../RenderItems/Shared/ShapeItem.cs | 401 ------- .../RenderItems/Shared/TextBody.cs | 99 +- .../RenderItems/Shared/TextRunItem.cs | 4 +- .../RenderItems/SvgGroupItem.cs | 5 +- .../RenderItems/SvgItem/SvgParagraphItem.cs | 21 +- .../RenderItems/SvgItem/SvgTextBodyItem.cs | 9 +- .../RenderItems/SvgRenderItem.cs | 7 +- .../RenderItems/SvgRenderPathItem.cs | 6 +- .../RenderItems/SvgRenderRectItem.cs | 16 +- .../Svg/Chart/ChartTypeDrawer.cs | 11 +- .../Svg/SvgChart.cs | 6 +- .../Svg/SvgChartAxis.cs | 150 ++- .../Svg/SvgChartLegend.cs | 6 +- .../Svg/SvgChartObject.cs | 17 +- .../Svg/SvgChartPlotarea.cs | 2 +- .../Svg/SvgChartTitle.cs | 29 +- .../Svg/SvgParagraph.cs | 741 ++++++------ .../Svg/SvgShape.cs | 16 +- .../Svg/SvgTextRun.cs | 1018 ++++++++--------- .../Text/FontWrapContainer.cs | 4 +- .../Text/TextBox.cs | 601 +++++----- .../Utils/DrawingExtensions.cs | 19 + .../Utils/SvgDrawingWriter.cs | 26 +- src/EPPlus.Graphics.Tests/GrahpicsTests.cs | 44 +- src/EPPlus.Graphics/BoundingBox.cs | 30 +- src/EPPlus.Graphics/Rect.cs | 6 +- src/EPPlus/Drawing/ExcelShape.cs | 1 + .../Drawing/Style/Text/ExcelTextBody.cs | 24 +- 37 files changed, 1602 insertions(+), 2083 deletions(-) delete mode 100644 src/EPPlus.Export.ImageRenderer/RenderItems/Shared/DrawingBaseItem.cs delete mode 100644 src/EPPlus.Export.ImageRenderer/RenderItems/Shared/ShapeItem.cs create mode 100644 src/EPPlus.Export.ImageRenderer/Utils/DrawingExtensions.cs diff --git a/src/EPPlus.Export.ImageRenderer.Test/TestTextContainer.cs b/src/EPPlus.Export.ImageRenderer.Test/TestTextContainer.cs index 88d0bf761..bffe8c8af 100644 --- a/src/EPPlus.Export.ImageRenderer.Test/TestTextContainer.cs +++ b/src/EPPlus.Export.ImageRenderer.Test/TestTextContainer.cs @@ -107,26 +107,26 @@ public void TestNonStandardFontSizesMultiLineLargeFontGoudyStout() Assert.AreEqual(expectedHeight, Math.Round(container.Height, 0)); } - [TestMethod] - public void EnsureTextBodyAddsRunsCorrectly() - { - BoundingBox shapeRect = new BoundingBox(); + //[TestMethod] + //public void EnsureTextBodyAddsRunsCorrectly() + //{ + // BoundingBox shapeRect = new BoundingBox(); - shapeRect.Width = 20; - shapeRect.Height = 10; + // shapeRect.Width = 20; + // shapeRect.Height = 10; - FontMeasurerTrueType measurer = new FontMeasurerTrueType(12, "Aptos Narrow", FontSubFamily.Regular); - var body = new SvgTextBodyItem(shapeRect); + // FontMeasurerTrueType measurer = new FontMeasurerTrueType(12, "Aptos Narrow", FontSubFamily.Regular); + // var body = new SvgTextBodyItem(shapeRect); - body.Bounds.transform.Name = "TxtBody"; + // body.Bounds.Transform.Name = "TxtBody"; - body.AddText("A new Paragraph", measurer); - body.AddText("Second paragraph", measurer); + // body.AddText("A new Paragraph", measurer); + // body.AddText("Second paragraph", measurer); - Assert.AreEqual(2, body.Paragraphs[0].Bounds.transform.ChildObjects.Count); - Assert.AreEqual(body.Bounds.transform.ChildObjects[0], body.Paragraphs[0].Bounds.transform); + // Assert.AreEqual(2, body.Paragraphs[0].Bounds.Transform.ChildObjects.Count); + // Assert.AreEqual(body.Bounds.Transform.ChildObjects[0], body.Paragraphs[0].Bounds.Transform); - Assert.AreEqual(shapeRect.transform, body.Bounds.transform.Parent); - } + // Assert.AreEqual(shapeRect.Transform, body.Bounds.Transform.Parent); + //} } } diff --git a/src/EPPlus.Export.ImageRenderer/DrawingBase.cs b/src/EPPlus.Export.ImageRenderer/DrawingBase.cs index ecc349a32..38110344d 100644 --- a/src/EPPlus.Export.ImageRenderer/DrawingBase.cs +++ b/src/EPPlus.Export.ImageRenderer/DrawingBase.cs @@ -12,6 +12,7 @@ Date Author Change *************************************************************************************************/ using EPPlus.Export.ImageRenderer; +using EPPlus.Export.ImageRenderer.Utils; using EPPlus.Graphics; using EPPlusImageRenderer.RenderItems; using OfficeOpenXml; @@ -25,24 +26,25 @@ namespace EPPlusImageRenderer { internal abstract class DrawingBase { - protected ExcelWorkbook _wb; + //protected ExcelWorkbook _wb; protected ExcelDrawing _drawing; protected ExcelTheme _theme; internal DrawingBase(ExcelDrawing drawing) { - drawing.GetSizeInPixels(out int width, out int height); + //drawing.GetSizeInPixels(out int width, out int height); Drawing = drawing; - Size = new DrawingSize(width, height); - TextMeasurer = drawing._drawings._package.Settings.TextSettings.PrimaryTextMeasurer; + //Bounds = new DrawingSize(width, height); + Bounds = drawing.GetBoundingBox(); + //TextMeasurer = drawing._drawings._package.Settings.TextSettings.PrimaryTextMeasurer; - _wb = drawing._drawings.Worksheet.Workbook; - _theme = _wb.ThemeManager.GetOrCreateTheme(); + var wb = drawing._drawings.Worksheet.Workbook; + _theme = wb.ThemeManager.GetOrCreateTheme(); } public ExcelDrawing Drawing { get; } - internal ITextMeasurer TextMeasurer { get; } - public List RenderItems { get; } = new List(); - public DrawingSize Size { get; internal set; } + //internal ITextMeasurer TextMeasurer { get; } + public List RenderItems { get; } = new List(); + //public DrawingSize Bounds { get; internal set; } internal BoundingBox Bounds = new BoundingBox(); } } \ No newline at end of file diff --git a/src/EPPlus.Export.ImageRenderer/DrawingChart.cs b/src/EPPlus.Export.ImageRenderer/DrawingChart.cs index c6ecec652..065cd43b3 100644 --- a/src/EPPlus.Export.ImageRenderer/DrawingChart.cs +++ b/src/EPPlus.Export.ImageRenderer/DrawingChart.cs @@ -11,6 +11,7 @@ Date Author Change 27/11/2025 EPPlus Software AB EPPlus 9 *************************************************************************************************/ +using EPPlus.Export.ImageRenderer.Utils; using EPPlusImageRenderer.RenderItems; using OfficeOpenXml.Drawing.Chart; using OfficeOpenXml.FormulaParsing.Excel.Functions.Finance; @@ -29,153 +30,5 @@ public DrawingChart(ExcelChart chart) : base(chart) Chart = chart; } public ExcelChart Chart { get; set; } - - protected List GetAxisValue(ExcelChartAxisStandard ax, RenderItem rect, out double? min, out double? max, out double? majorUnit) - { - var values = ax.GetAxisValues(out bool isCount); - if(ax.AxisType == eAxisType.Cat && - isCount == false) - { - min = 0; - max = values.Length; - majorUnit = 1; - return values.ToList(); - } - var l = new List(); - min = double.MaxValue; - max = double.MinValue; - foreach (var v in values) - { - var d = ConvertUtil.GetValueDouble(v, false, true); - if(double.IsNaN(d)) - { - d = 0; - } - if(min>d) - { - min = d; - } - if(max 0.091) - { - min = 0; - } - } - } - - if(isCount) - { - majorUnit = 1; - } - else - { - if (ax.MaxValue.HasValue) - { - max = ax.MaxValue; - majorUnit = ax.MajorUnit ?? GetAutoUnit(min.Value, max.Value); - if (ax.MinValue.HasValue == false) - { - var newMin = max - majorUnit; - while (newMin > min) - { - newMin -= majorUnit.Value; - } - min = newMin; - } - } - else - { - majorUnit = ax.MajorUnit ?? GetAutoUnit(min.Value, max.Value); - if (isCount == false) - { - var diff = max.Value - min.Value; - var newMax = min.Value + majorUnit; - while ((newMax - min) < (diff * 1.05)) - { - newMax += majorUnit.Value; - } - max = newMax; - } - if(min != 0 && max-min<9) - { - min -= 2; - } - } - var newUnit = majorUnit; - while (newUnit >= 2 && (max - min) / newUnit > maxMajorTickmarks) - { - newUnit /= 2; - } - } - } - - private double GetAutoUnit(double min, double max) - { - var diff = max - min; - if (diff < 8) - { - return 1; - } - else - { - var rawMajorUnit = diff; - var exponent = Math.Floor(Math.Log10(rawMajorUnit)); - var fraction = rawMajorUnit / (Math.Pow(10, exponent)); - double unit; - if (fraction <= 1) - { - unit = 1D; - } - else if (fraction <= 2) - { - unit = 2; - } - else if (fraction <= 2.5) - { - unit = 2.5; - } - else if (fraction <= 5) - { - unit = 5; - } - else - { - unit = 10; - } - - var axMax = unit * Math.Pow(10, exponent); - var axMin = Math.Floor(min / axMax) * axMax; - axMax = Math.Ceiling(max / axMax) * axMax; - return axMax / 10; - } - } } } \ No newline at end of file diff --git a/src/EPPlus.Export.ImageRenderer/DrawingShape.cs b/src/EPPlus.Export.ImageRenderer/DrawingShape.cs index 7ee42114e..42d4fec46 100644 --- a/src/EPPlus.Export.ImageRenderer/DrawingShape.cs +++ b/src/EPPlus.Export.ImageRenderer/DrawingShape.cs @@ -21,7 +21,7 @@ Date Author Change namespace EPPlusImageRenderer { - internal abstract class DrawingShape : DrawingBaseItem + internal abstract class DrawingShape : DrawingBase { protected ExcelShape _shape; protected DrawingShape(ExcelShape shape) : base(shape) diff --git a/src/EPPlus.Export.ImageRenderer/ImageRenderer.cs b/src/EPPlus.Export.ImageRenderer/ImageRenderer.cs index 3c1771372..1b7cfb150 100644 --- a/src/EPPlus.Export.ImageRenderer/ImageRenderer.cs +++ b/src/EPPlus.Export.ImageRenderer/ImageRenderer.cs @@ -41,14 +41,14 @@ public string RenderDrawingToSvg(ExcelDrawing drawing) if (drawing is ExcelShape shape) { var svg = new SvgShape(shape); - svg.Size = new DrawingSize(width, height); + //svg.Bounds = new DrawingSize(width, height); svg.Render(sb); return sb.ToString(); } else if(drawing is ExcelChart chart) { var svg = new SvgChart(chart); - svg.Size = new DrawingSize(width, height); + //svg.Bounds = new DrawingSize(width, height); svg.Render(sb); return sb.ToString(); } @@ -130,62 +130,6 @@ public string RenderTextBody(ExcelTextBody body, double shapeWidth, double shape return sb.ToString(); } - public string RenderTextBody(string txtBody) - { - BoundingBox worldBounds = new BoundingBox(); - worldBounds.Width = 400; - worldBounds.Height = 400; - - worldBounds.transform.Name = "World Bounds"; - - BoundingBox shapeRect = new BoundingBox(); - - shapeRect.Parent = worldBounds; - - shapeRect.Width = 200; - shapeRect.Height = 200; - - shapeRect.X = 20; - shapeRect.Y = 20; - - shapeRect.transform.Name = "Shape"; - - FontMeasurerTrueType measurer = new FontMeasurerTrueType(11, "Aptos Narrow", FontSubFamily.Regular); - var body = new SvgTextBodyItem(shapeRect); - - body.Bounds.transform.Name = "TxtBody"; - - body.Bounds.X = 20; - body.Bounds.Y = 20; - - body.Bounds.Width = 100; - body.Bounds.Height = 100; - - body.AddText(txtBody, measurer); - - var para1 = body.Paragraphs[0]; - - para1.AddText("Extra Text", measurer); - para1.Runs[1].X = 10; - para1.Runs[1].Y = 20; - - para1.Bounds.Width = 120; - para1.Bounds.Height = 100; - - //body.AddParagraph("Paragraph2 text", measurer); - //var para2 = body.Paragraphs[1]; - - //para2.Bounds.Y = 40; - - //para2.AddText("Para2 Run2", measurer); - //para2.Runs[1].X = 5; - //para2.Runs[1].Y = 20; - - var svgBody = GenerateSvgTextBody(body, (int)worldBounds.Width, (int)worldBounds.Height); - - return RenderSvgElement(svgBody); - } - internal string RenderSvgElement(SvgElement element) { string retStr = string.Empty; @@ -360,8 +304,8 @@ internal SvgElement GenerateSvg(TextContainerBase container) def.AddChildElement(clipPath); var bb = new SvgElement("rect"); - bb.AddAttribute("x", container.transform.Position.X); - bb.AddAttribute("y", container.transform.Position.Y); + bb.AddAttribute("x", container.Transform.Position.X); + bb.AddAttribute("y", container.Transform.Position.Y); bb.AddAttribute("width", container.Width); bb.AddAttribute("height", container.Height); //bb.AddAttribute("fill", "blue"); @@ -372,16 +316,16 @@ internal SvgElement GenerateSvg(TextContainerBase container) var fontSizePx = 16d; var renderElement = new SvgElement("text"); - renderElement.AddAttribute("x", container.transform.Position.X); - renderElement.AddAttribute("y", container.transform.Position.Y + fontSizePx); + renderElement.AddAttribute("x", container.Transform.Position.X); + renderElement.AddAttribute("y", container.Transform.Position.Y + fontSizePx); renderElement.AddAttribute("_measurementFont-size", $"{fontSizePx}px"); renderElement.AddAttribute("clip-path", $"url(#{nameId})"); renderElement.Content = fullString; var bbVisual = new SvgElement("rect"); - bbVisual.AddAttribute("x", container.transform.Position.X); - bbVisual.AddAttribute("y", container.transform.Position.Y); + bbVisual.AddAttribute("x", container.Transform.Position.X); + bbVisual.AddAttribute("y", container.Transform.Position.Y); bbVisual.AddAttribute("width", container.Width); bbVisual.AddAttribute("height", container.Height); bbVisual.AddAttribute("fill", "blue"); diff --git a/src/EPPlus.Export.ImageRenderer/RenderItems/RenderItem.cs b/src/EPPlus.Export.ImageRenderer/RenderItems/RenderItem.cs index bbbf0f1d5..fca007a2c 100644 --- a/src/EPPlus.Export.ImageRenderer/RenderItems/RenderItem.cs +++ b/src/EPPlus.Export.ImageRenderer/RenderItems/RenderItem.cs @@ -31,15 +31,21 @@ internal abstract class RenderItem : RenderItemBase { protected ExcelDrawing _drawing; protected ExcelTheme _theme; - protected RenderItem() + internal RenderItem(BoundingBox parent) { - + Bounds.Parent = parent; + //_drawing = drawing; + //_theme = drawing._drawings.Worksheet.Workbook.ThemeManager.GetOrCreateTheme(); } - internal RenderItem(ExcelDrawing drawing) + //internal abstract void GetBounds(out double il, out double it, out double ir, out double ib); + internal virtual void GetBounds(out double il, out double it, out double ir, out double ib) { - _drawing = drawing; - _theme = drawing._drawings.Worksheet.Workbook.ThemeManager.GetOrCreateTheme(); + il = Bounds.Left; + it = Bounds.Top; + ir = Bounds.Right; + ib = Bounds.Bottom; } + internal bool IsEndOfGroup { get; set; } = false; public string FillColor { get; set; } public string FilterName { get; set; } @@ -209,20 +215,13 @@ private string GetFillColor(ExcelDrawingFillBasic fill, ExcelDrawingColorManager fc = ColorUtils.GetAdjustedColor(fillColorSource, fc); return "#" + fc.ToArgb().ToString("x8").Substring(2); } - - internal BoundingBox Bounds = new BoundingBox(); - internal abstract void GetBounds(out double il, out double it, out double ir, out double ib); - - internal void SetTheme(ExcelTheme theme) - { - _theme = theme; - } } /// /// Base class for any item rendered. /// internal abstract class RenderItemBase { + internal BoundingBox Bounds = new BoundingBox(); public abstract RenderItemType Type { get; } public abstract void Render(StringBuilder sb); } diff --git a/src/EPPlus.Export.ImageRenderer/RenderItems/RenderItemType.cs b/src/EPPlus.Export.ImageRenderer/RenderItems/RenderItemType.cs index e3e3b1895..c770bfdc7 100644 --- a/src/EPPlus.Export.ImageRenderer/RenderItems/RenderItemType.cs +++ b/src/EPPlus.Export.ImageRenderer/RenderItems/RenderItemType.cs @@ -20,6 +20,7 @@ internal enum RenderItemType Line = 3, Ellipse = 4, Text = 5, - TSpan = 6 + TSpan = 6, + Paragraph = 7, } } \ No newline at end of file diff --git a/src/EPPlus.Export.ImageRenderer/RenderItems/Shared/DrawingBaseItem.cs b/src/EPPlus.Export.ImageRenderer/RenderItems/Shared/DrawingBaseItem.cs deleted file mode 100644 index 55b9d6fdd..000000000 --- a/src/EPPlus.Export.ImageRenderer/RenderItems/Shared/DrawingBaseItem.cs +++ /dev/null @@ -1,27 +0,0 @@ -using EPPlusImageRenderer; -using EPPlusImageRenderer.RenderItems; -using OfficeOpenXml.Drawing; -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Text.RegularExpressions; - -namespace EPPlus.Export.ImageRenderer.RenderItems.Shared -{ - internal abstract class DrawingBaseItem : DrawingBase - { - public DrawingBaseItem(ExcelShapeBase drawing) : base(drawing) - { - ImportEpplusDrawing(drawing); - } - - internal void ImportEpplusDrawing(ExcelShapeBase drawing) - { - Bounds.Left = drawing.GetPixelLeft(); - Bounds.Top = drawing.GetPixelTop(); - Bounds.Width = drawing.GetPixelWidth(); - Bounds.Height = drawing.GetPixelHeight(); - } - } -} diff --git a/src/EPPlus.Export.ImageRenderer/RenderItems/Shared/ParagraphContainer.cs b/src/EPPlus.Export.ImageRenderer/RenderItems/Shared/ParagraphContainer.cs index 02e9f05f3..66ad8fc3a 100644 --- a/src/EPPlus.Export.ImageRenderer/RenderItems/Shared/ParagraphContainer.cs +++ b/src/EPPlus.Export.ImageRenderer/RenderItems/Shared/ParagraphContainer.cs @@ -15,7 +15,7 @@ namespace EPPlus.Export.ImageRenderer.RenderItems.Shared { - internal abstract class ParagraphContainer : SvgRenderItem + internal abstract class ParagraphContainer : RenderItem { ITextMeasurerWrap _measurer; @@ -28,9 +28,6 @@ internal abstract class ParagraphContainer : SvgRenderItem double _lineSpacingAscendantOnly; double? _lsMultiplier = null; internal bool IsFirstParagraph { get; private set; } - - public override RenderItemType Type => RenderItemType.Text; - List _paragraphLines = new List(); protected List _textRunDisplayText = new List(); @@ -39,23 +36,17 @@ internal abstract class ParagraphContainer : SvgRenderItem internal List _textRunItems; internal protected MeasurementFont _paragraphFont; + //internal BoundingBox Bounds = new BoundingBox(); - public ParagraphContainer() : base() + public ParagraphContainer(BoundingBox parent) : base(parent) { - + Bounds.Transform.Name = "Paragraph"; } - public ParagraphContainer(BoundingBox parent) - { - Bounds.transform.Name = "Paragraph"; - Bounds.Parent = parent; - } - - public ParagraphContainer(ExcelDrawingParagraph p, BoundingBox parent) : base() + public ParagraphContainer(ExcelDrawingParagraph p, BoundingBox parent) : base(parent) { //---Initialize Bounds/Margins--- - Bounds.transform.Name = "Paragraph"; - Bounds.Parent = parent; + Bounds.Transform.Name = "Paragraph"; var indent = 48 * p.IndentLevel; _leftMargin = p.LeftMargin + p.Indent + indent; @@ -147,7 +138,7 @@ public void AddText(string text, FontMeasurerTrueType measurer) Runs.Add(container); - container.transform.Name = $"Container{Runs.Count}"; + container.Transform.Name = $"Container{Runs.Count}"; container.SetContent(text); } @@ -342,13 +333,14 @@ internal double GetAlignmentHorizontal(eTextAlignment txAlignment) return TextUtils.RoundToWhole(x); } - internal override void GetBounds(out double il, out double it, out double ir, out double ib) - { - il = Bounds.Left + _leftMargin; - it = Bounds.Top; - ir = Bounds.Right - _rightMargin; - ib = Bounds.Bottom; - } + //internal override void GetBounds(out double il, out double it, out double ir, out double ib) + //{ + // il = Bounds.Left + _leftMargin; + // it = Bounds.Top; + // ir = Bounds.Right - _rightMargin; + // ib = Bounds.Bottom; + //} + /// /// Type of textrun defined by child type diff --git a/src/EPPlus.Export.ImageRenderer/RenderItems/Shared/ShapeItem.cs b/src/EPPlus.Export.ImageRenderer/RenderItems/Shared/ShapeItem.cs deleted file mode 100644 index ffd51f1c9..000000000 --- a/src/EPPlus.Export.ImageRenderer/RenderItems/Shared/ShapeItem.cs +++ /dev/null @@ -1,401 +0,0 @@ -using EPPlus.Export.ImageRenderer.RenderItems.SvgItem; -using EPPlus.Export.ImageRenderer.Text; -using EPPlus.Fonts.OpenType; -using EPPlusImageRenderer; -using EPPlusImageRenderer.RenderItems; -using EPPlusImageRenderer.ShapeDefinitions; -using EPPlusImageRenderer.Text; -using OfficeOpenXml.Drawing; -using OfficeOpenXml.Style; -using System; -using System.Collections.Generic; -using System.Globalization; -using System.Linq; -using System.Text; -using TypeConv = OfficeOpenXml.Utils.TypeConversion; - -namespace EPPlus.Export.ImageRenderer.RenderItems.Shared -{ - /// - /// Base shapeItem class for file-specific classes to inherit from - /// - internal abstract class ShapeItem : DrawingShape - { - TextBody _textBox; - - internal ShapeItem(ExcelShape shape) : base(shape) - { - var style = shape.Style; - - if (style == eShapeStyle.CustomShape) - { - foreach (var path in shape.CustomGeom.DrawingPaths) - { - AddFromPaths(path); - } - } - else - { - var shapeDef = PresetShapeDefinitions.ShapeDefinitions[style].Clone(); - shapeDef.Calculate(shape); - - //RenderItems.Add(new SvgRenderPathItem(shape)); - RenderItems.Add(new SvgGroupItem(shape.Rotation, shape.GetPixelWidth()/2, shape.GetPixelHeight()/2)); - - //Draw Filled path's - foreach (var path in shapeDef.ShapePaths) - { - if (path.Fill != PathFillMode.None) - { - AddFromPaths(path, true, false); - } - } - - //Draw border path's - foreach (var path in shapeDef.ShapePaths) - { - if (path.Stroke) - { - AddFromPaths(path, false, true); - } - } - - //Position text - if (_shape.Text != null) - { - GenerateTextItems(shapeDef); - } - } - } - - private void LoadTextBox() - { - _textBox.VerticalAlignment = _shape.TextAnchoring; - _textBox.WrapText = _shape.TextBody.WrapText != eTextWrappingType.None; - var fontMeasurer = (FontMeasurerTrueType)_shape._drawings._package.Settings.TextSettings.GenericTextMeasurerTrueType; - - //Make width the doc width if meant to overflow - if (_shape.TextBody.HorizontalTextOverflow == eTextHorizontalOverflow.Overflow) - { - _textBox.Width = Size.Width; - } - - string color = "#" + GetFontColor(); - _textBox.FontColorString = color; - - var totalText = _shape.Text; - List> charAdvanceWidths = new List>(); - var txtRunIndicies = LineFormatter.GetTextRunIndiciesAndWidths(_shape.TextBody, out charAdvanceWidths); - - _textBox.ImportTextBody(_shape.TextBody); - - ////Paragraph level begins - //foreach (var paragraph in _shape.TextBody.Paragraphs) - //{ - - // //textBox.ImportParagraph(paragraph); - //} - } - - protected void AddFromPaths(DrawingPath path, bool drawFill = true, bool drawBorder = true) - { - var pi = new SvgRenderPathItem(_shape); - var coordinates = new List(); - PathCommands cmd = null; - PathsBase pCmd = null; - double cx = 0, cy = 0; - foreach (var p in path.Paths) - { - switch (p.Type) - { - case PathDrawingType.MoveTo: - AddCmd(pi, path, coordinates, ref cmd, pCmd, p, PathCommandType.Move); - break; - case PathDrawingType.LineTo: - AddCmd(pi, path, coordinates, ref cmd, pCmd, p, PathCommandType.Line); - break; - case PathDrawingType.CubicBezierTo: - AddCmd(pi, path, coordinates, ref cmd, pCmd, p, PathCommandType.CubicBézier); - break; - case PathDrawingType.QuadBezierTo: - AddCmd(pi, path, coordinates, ref cmd, pCmd, p, PathCommandType.QuadraticBézier); - break; - case PathDrawingType.ArcTo: - SetCmdCoordinats(cmd, p, coordinates); - AddArc(pi, path, coordinates, pCmd, out cx, out cy, p); - cmd = null; - break; - case PathDrawingType.Close: - if (pi.Commands[pi.Commands.Count - 1].Type != PathCommandType.Arc) - { - pi.Commands[pi.Commands.Count - 1].Coordinates = coordinates.ToArray(); - coordinates.Clear(); - } - pi.Commands.Add(new PathCommands(PathCommandType.End, pi)); - cmd = null; - break; - } - pCmd = p; - } - if (coordinates.Count > 0) - { - pi.Commands[pi.Commands.Count - 1].Coordinates = coordinates.ToArray(); - } - if (drawFill) - { - pi.FillColorSource = path.Fill; - pi.SetDrawingPropertiesFill(_shape.Fill, _shape.ThemeStyles.FillReference.Color); - } - else - { - pi.FillColorSource = PathFillMode.None; - pi.FillColor = "none"; - } - - if (drawBorder) - { - pi.BorderColorSource = path.Stroke ? PathFillMode.Norm : PathFillMode.None; - pi.SetDrawingPropertiesBorder(_shape.Border, _shape.ThemeStyles.BorderReference.Color, path.Stroke); - } - else - { - pi.BorderColorSource = PathFillMode.None; - pi.BorderColor = "none"; - } - - RenderItems.Add(pi); - } - public string ViewBox - { - get - { - double l = 0, t = 0, r = 1, b = 1; - foreach (var item in RenderItems) - { - item.GetBounds(out var il, out var it, out var ir, out var ib); - if (il < l) - { - l = il; - } - if (it < t) - { - t = it; - } - if (ir > r) - { - r = ir; - } - if (ib > b) - { - b = ib; - } - } - return $"{(l * Size.Width).ToString(CultureInfo.InvariantCulture)},{(t * Size.Height).ToString(CultureInfo.InvariantCulture)},{((Math.Abs(l) + r) * Size.Width).ToString(CultureInfo.InvariantCulture)},{((Math.Abs(t) + b) * Size.Height).ToString(CultureInfo.InvariantCulture)}"; - } - } - - private string GetFontColor() - { - string color; - - if (_shape.Font.Fill.Style == eFillStyle.SolidFill) - { - var c = TypeConv.ColorConverter.GetThemeColor(_shape.Font.Fill.SolidFill.Color); - color = ((uint)c.ToArgb()).ToString("x").Substring(2, 6); - } - else - { - var c = TypeConv.ColorConverter.GetThemeColor(_wb.ThemeManager.CurrentTheme, _shape.ThemeStyles.FontReference.Color); - color = ((uint)c.ToArgb()).ToString("x").Substring(2, 6); - } - - return color; - } - - private void GetFontNameAndSize(ExcelFont nsFont, out string fontName, out double fontSize) - { - fontName = string.IsNullOrEmpty(_shape.Font.LatinFont) ? _shape.Font.ComplexFont : _shape.Font.LatinFont; - - fontSize = _shape.Font.Size; - if (string.IsNullOrEmpty(fontName)) fontName = nsFont?.Name ?? _theme.FontScheme.MajorFont.First().Typeface; - if (fontSize <= 0 && nsFont != null) fontSize = nsFont.Size; - } - - private void RenderText(StringBuilder sb) - { - //RenderDebugTextBox(sb); - _textBox.Render(sb); - } - - //private void RenderDebugTextBox(StringBuilder sb) - //{ - // _renderTextBox.FillColor = "green"; - // _renderTextBox.Render(sb); - - // var area = textBox.GetTextArea(); - - // _renderTextBox.X = (float)area.Left; - // _renderTextBox.Y = (float)area.Top; - // _renderTextBox.Width = (float)area.Width; - // _renderTextBox.Height = (float)area.Height; - // _renderTextBox.FillColor = "blue"; - // _renderTextBox.Render(sb); - //} - - private void GetShapeInnerBound(out double x, out double y, out double width, out double height) - { - double currentX = 0, currentY = 0, xe, ye; - x = y = 0; - width = xe = Size.Width; - height = ye = Size.Height; - foreach (var ri in RenderItems) - { - switch (ri.Type) - { - case RenderItemType.Rect: - var rectItem = (SvgRenderRectItem)ri; - x = rectItem.X; - y = rectItem.Y; - width = rectItem.Width; - height = rectItem.Height; - break; - case RenderItemType.Path: - var pathItem = (SvgRenderPathItem)ri; - foreach (var cmd in pathItem.Commands) - { - var cmdCoordinates = new List(); - for (int i = 0; i < cmd.Coordinates.Length; i++) - { - switch (cmd.Type) - { - case PathCommandType.Move: - if (i == 0) - { - currentX = cmd.Coordinates[i]; - currentY = cmd.Coordinates[++i]; - } - else - { - HandleLine(ref currentX, ref currentY, ref xe, ref ye, cmd, cmdCoordinates, ref i); - } - break; - case PathCommandType.VerticalLine: - HandleVertical(y, ref currentY, ref xe, cmd.Coordinates[i]); - break; - case PathCommandType.HorizontalLine: - HandleHorizontal(x, ref currentX, ref xe, cmd.Coordinates[i]); - break; - case PathCommandType.Line: - HandleLine(ref currentX, ref currentY, ref xe, ref ye, cmd, cmdCoordinates, ref i); - break; - case PathCommandType.CubicBézier: - if (currentX > x) - { - x = currentX; - } - if (currentY > y) - { - y = currentY; - } - i += 4; - break; - - } - } - } - break; - } - } - if (xe != double.MinValue) - { - width = xe - x; - } - if (ye != double.MinValue) - { - height = ye - y; - } - } - - private static void HandleLine(ref double currentX, ref double currentY, ref double xe, ref double ye, PathCommands cmd, List cmdCoordinates, ref int i) - { - xe = cmd.Coordinates[i]; - ye = cmd.Coordinates[++i]; - if (xe == currentX || ye == currentY) - { - if (cmdCoordinates.Count == 0) - { - cmdCoordinates.Add(new Coordinate(currentX, currentY)); - } - cmdCoordinates.Add(new Coordinate(xe, ye)); - } - else - { - var w = Math.Abs(xe - currentX); - var h = Math.Abs(ye - currentY); - cmdCoordinates.Add(new Coordinate((Math.Min(xe, currentX) + w) / 2, (Math.Min(ye, currentY) + h) / 2)); - } - currentX = xe; - currentY = ye; - } - - - private static void HandleVertical(double y, ref double currentY, ref double ye, double yec) - { - if (currentY < y || currentY == double.MinValue) - { - currentY = y; - } - if (ye > yec || ye == double.MinValue) - { - ye = yec; - } - } - private static void HandleHorizontal(double x, ref double currentX, ref double xe, double xec) - { - if (currentX < x || currentX == double.MinValue) - { - currentX = x; - } - if (xe > xec || xe == double.MinValue) - { - xe = xec; - } - } - - private void GenerateTextItems(ShapeDefinition shapeDef) - { - if (shapeDef.TextBoxRect != null) - { - if (_shape.TextBody.TextAutofit != eTextAutofit.ShapeAutofit) - { - var rectItem = new SvgRenderRectItem(_shape); - - rectItem.X = (float)shapeDef.TextBoxRect.LeftValue; - rectItem.Y = (float)shapeDef.TextBoxRect.TopValue; - rectItem.Width = (float)shapeDef.TextBoxRect.RightValue - rectItem.X; - rectItem.Height = (float)shapeDef.TextBoxRect.BottomValue - rectItem.Y; - rectItem.FillOpacity = 0.3d; - //_renderTextBox = rectItem; - } - else - { - var rectItem = new SvgRenderRectItem(_shape); - - rectItem.X = (float)shapeDef.TextBoxRect.LeftValue; - rectItem.Y = (float)shapeDef.TextBoxRect.TopValue; - rectItem.Width = (float)shapeDef.TextBoxRect.RightValue; - rectItem.Height = (float)shapeDef.TextBoxRect.BottomValue; - rectItem.FillOpacity = 0.3d; - //_renderTextBox = rectItem; - } - } - else - { - //_renderTextBox = null; - } - - //textBox = GetTextBox(); - LoadTextBox(); - } - } -} diff --git a/src/EPPlus.Export.ImageRenderer/RenderItems/Shared/TextBody.cs b/src/EPPlus.Export.ImageRenderer/RenderItems/Shared/TextBody.cs index ee031af9c..7d58bc716 100644 --- a/src/EPPlus.Export.ImageRenderer/RenderItems/Shared/TextBody.cs +++ b/src/EPPlus.Export.ImageRenderer/RenderItems/Shared/TextBody.cs @@ -1,15 +1,13 @@ -using EPPlus.Export.ImageRenderer.Text; -using EPPlus.Fonts.OpenType; +using EPPlus.Fonts.OpenType; using EPPlus.Fonts.OpenType.Utils; using EPPlus.Graphics; using EPPlusImageRenderer.RenderItems; -using EPPlusImageRenderer.Svg; using OfficeOpenXml.Drawing; -using OfficeOpenXml.FormulaParsing.Excel.Functions.MathFunctions; +using OfficeOpenXml.Style; +using OfficeOpenXml.Utils; using System; using System.Collections.Generic; using System.Linq; -using System.Text; namespace EPPlus.Export.ImageRenderer.RenderItems.Shared @@ -18,8 +16,9 @@ namespace EPPlus.Export.ImageRenderer.RenderItems.Shared /// Margin left = X /// Margin right = Y /// - internal abstract class TextBody: SvgRenderItem + internal abstract class TextBody : RenderItemBase { + //internal BoundingBox Bounds = new BoundingBox(); /// /// Shorthand for Bounds.Width /// @@ -42,9 +41,9 @@ internal abstract class TextBody: SvgRenderItem private FontMeasurerTrueType _measurer = null; - public TextBody(BoundingBox parent) : base() + public TextBody(BoundingBox parent) { - Bounds.transform.Name = "TxtBody"; + Bounds.Transform.Name = "TxtBody"; Bounds.Parent = parent; @@ -52,25 +51,25 @@ public TextBody(BoundingBox parent) : base() Bounds.Height = parent.Height; } - public void AddParagraph(string text, FontMeasurerTrueType measurer) - { - if(_measurer == null && measurer != null) - { - _measurer = measurer; - } + //public void AddParagraph(string text, FontMeasurerTrueType measurer) + //{ + // if(_measurer == null && measurer != null) + // { + // _measurer = measurer; + // } - if (_measurer != null) - { - var paragraph = CreateParagraph(Bounds); - Paragraphs.Add(paragraph); + // if (_measurer != null) + // { + // var paragraph = CreateParagraph(Bounds); + // Paragraphs.Add(paragraph); - paragraph.AddText(text, measurer); + // //paragraph.AddText(text, measurer); - paragraph.Bounds.transform.Name = $"Container{Paragraphs.Count}"; - return; - } - throw new NullReferenceException($"The FontMeasurer: {_measurer} object is null. Use SetMeasurer before adding text"); - } + // paragraph.Bounds.Transform.Name = $"Container{Paragraphs.Count}"; + // return; + // } + // throw new NullReferenceException($"The FontMeasurer: {_measurer} object is null. Use SetMeasurer before adding text"); + //} public void ImportParagraph(ExcelDrawingParagraph item, double startingY) { @@ -78,14 +77,13 @@ public void ImportParagraph(ExcelDrawingParagraph item, double startingY) bool isFirst = Paragraphs.Count == 0; var paragraph = CreateParagraph(item, Bounds); - paragraph.Bounds.transform.Name = $"Container{Paragraphs.Count}"; + paragraph.Bounds.Transform.Name = $"Container{Paragraphs.Count}"; paragraph.Bounds.Top = startingY; Paragraphs.Add(paragraph); //TODO; Fix this. This is a strange workaround - paragraph.SetTheme(item._prd.Package.Workbook.ThemeManager.GetOrCreateTheme()); paragraph.SetDrawingPropertiesFill(item.DefaultRunProperties.Fill, null); } @@ -99,7 +97,7 @@ public void ImportTextBody(ExcelTextBody body) RightMargin = r.PointToPixel(); BottomMargin = b.PointToPixel(); - //We already apply bounds top via the parent transform + //We already apply bounds top via the parent Transform double paragraphStartY = GetAlignmentVertical(); foreach (var paragraph in body.Paragraphs) @@ -120,18 +118,41 @@ public string GetContent() //string.Join(Paragraphs[}]) } - public void AddText(string text, FontMeasurerTrueType measurer) + //public void AddText(string text, FontMeasurerTrueType measurer) + //{ + // if (Paragraphs.Count == 0) + // { + // AddParagraph(text, measurer); + // } + // else + // { + // Paragraphs.Last().AddText(text, measurer); + // } + //} + internal void AddText(string text, ExcelTextFont font) { - if (Paragraphs.Count <= 0) - { - AddParagraph(text, measurer); - } - else - { - Paragraphs.Last().AddText(text, measurer); - } - } + var measureFont = font.GetMeasureFont(); + + //Document Top position for the paragraph text based on vertical alignment + var posY = GetAlignmentVertical(); + //var vertAlignAttribute = GetVerticalAlignAttribute(posY); + + + + //var measurer = font.PictureRelationDocument.Package.Settings.TextSettings.GenericTextMeasurerTrueType; + var measurer = new FontMeasurerTrueType(); + var m = measurer.MeasureText(text, measureFont); + //Limit bounding area with the space taken by previous paragraphs + //Note that this is ONLY identical to PosY if the vertical alignment is top + //The first run in the first paragraph must apply different line-spacing + //var svgParagraph = new SvgParagraph(text, font, area, vertAlignAttribute, posY); + var paragraph = CreateParagraph(Bounds); + + paragraph.FillColor = font.Fill.Color.To6CharHexString(); + + Paragraphs.Add(paragraph); + } public void SetMeasurer(FontMeasurerTrueType fontMeasurer) { _measurer = fontMeasurer; @@ -193,10 +214,6 @@ private double GetAlignmentVertical() return _alignmentY.Value; } - internal override void GetBounds(out double il, out double it, out double ir, out double ib) - { - il = Bounds.Left; it = Bounds.Top; ir = Bounds.Right; ib = Bounds.Bottom; - } /// /// Each file format defines its own paragraph diff --git a/src/EPPlus.Export.ImageRenderer/RenderItems/Shared/TextRunItem.cs b/src/EPPlus.Export.ImageRenderer/RenderItems/Shared/TextRunItem.cs index 79bf7eb80..8d99d6ec3 100644 --- a/src/EPPlus.Export.ImageRenderer/RenderItems/Shared/TextRunItem.cs +++ b/src/EPPlus.Export.ImageRenderer/RenderItems/Shared/TextRunItem.cs @@ -59,9 +59,9 @@ internal abstract class TextRunItem : SvgRenderItem /// /// /// - internal TextRunItem(ExcelParagraphTextRunBase run, BoundingBox parent = null, string displayText = "") + internal TextRunItem(ExcelParagraphTextRunBase run, BoundingBox parent = null, string displayText = "") : base(parent) { - Bounds.transform.Name = "TextRun"; + Bounds.Transform.Name = "TextRun"; _originalText = run.Text; _currentText = string.IsNullOrEmpty(displayText) ? _originalText : displayText; diff --git a/src/EPPlus.Export.ImageRenderer/RenderItems/SvgGroupItem.cs b/src/EPPlus.Export.ImageRenderer/RenderItems/SvgGroupItem.cs index 1c97301b4..b8372bf53 100644 --- a/src/EPPlus.Export.ImageRenderer/RenderItems/SvgGroupItem.cs +++ b/src/EPPlus.Export.ImageRenderer/RenderItems/SvgGroupItem.cs @@ -10,6 +10,7 @@ Date Author Change ************************************************************************************************* 27/11/2025 EPPlus Software AB EPPlus 9 *************************************************************************************************/ +using EPPlus.Graphics; using EPPlusImageRenderer.Svg; using System.Globalization; using System.Text; @@ -22,11 +23,11 @@ internal class SvgGroupItem : SvgRenderItem public string GroupTransform = ""; - internal SvgGroupItem() : base() + internal SvgGroupItem(BoundingBox parent) : base(parent) { } - internal SvgGroupItem(double rotation, double cx, double cy) : base() + internal SvgGroupItem(BoundingBox parent, double rotation, double cx, double cy) : base(parent) { if(rotation!=0) { diff --git a/src/EPPlus.Export.ImageRenderer/RenderItems/SvgItem/SvgParagraphItem.cs b/src/EPPlus.Export.ImageRenderer/RenderItems/SvgItem/SvgParagraphItem.cs index 2c8b812ae..7928b9b8b 100644 --- a/src/EPPlus.Export.ImageRenderer/RenderItems/SvgItem/SvgParagraphItem.cs +++ b/src/EPPlus.Export.ImageRenderer/RenderItems/SvgItem/SvgParagraphItem.cs @@ -1,23 +1,16 @@ using EPPlus.Export.ImageRenderer.RenderItems.Shared; -using EPPlus.Export.ImageRenderer.Svg.NodeAttributes; using EPPlus.Fonts.OpenType.Utils; using EPPlus.Graphics; using EPPlusImageRenderer.RenderItems; -using EPPlusImageRenderer.Svg; using OfficeOpenXml.Drawing; -using OfficeOpenXml.FormulaParsing.Excel.Functions.Statistical; -using OfficeOpenXml.FormulaParsing.Excel.Functions.Text; using System.Globalization; -using System.Linq; using System.Text; namespace EPPlus.Export.ImageRenderer.RenderItems.SvgItem { internal class SvgParagraphItem : ParagraphContainer { - public SvgParagraphItem() - { - } + public override RenderItemType Type => RenderItemType.Paragraph; public SvgParagraphItem(BoundingBox parent) : base(parent) { @@ -83,8 +76,8 @@ public override void Render(StringBuilder sb) sb.AppendLine("paragraph "); - var bb = new SvgRenderRectItem(); - //The bb is affected by the transform so set pos to zero + var bb = new SvgRenderRectItem(Bounds); + //The bb is affected by the Transform so set pos to zero if(IsFirstParagraph == false) { bb.Y = 0; @@ -138,7 +131,7 @@ public override void Render(StringBuilder sb) //} sb.Append(""); sb.AppendLine(""); } - - internal override SvgRenderItem Clone(SvgShape svgDocument) - { - throw new System.NotImplementedException(); - } - internal override TextRunItem CreateTextRun(ExcelParagraphTextRunBase run, BoundingBox parent, string displayText) { return new SvgTextRunItem(run, parent, displayText); diff --git a/src/EPPlus.Export.ImageRenderer/RenderItems/SvgItem/SvgTextBodyItem.cs b/src/EPPlus.Export.ImageRenderer/RenderItems/SvgItem/SvgTextBodyItem.cs index c0342d316..0c326d463 100644 --- a/src/EPPlus.Export.ImageRenderer/RenderItems/SvgItem/SvgTextBodyItem.cs +++ b/src/EPPlus.Export.ImageRenderer/RenderItems/SvgItem/SvgTextBodyItem.cs @@ -20,11 +20,11 @@ public SvgTextBodyItem(BoundingBox parent) : base(parent) public override void Render(StringBuilder sb) { sb.AppendLine($""); sb.AppendLine($"txtBody"); - var bb = new SvgRenderRectItem(); + var bb = new SvgRenderRectItem(Bounds); bb.X = 0; bb.Width = Width; bb.Height = Height; @@ -39,11 +39,6 @@ public override void Render(StringBuilder sb) sb.AppendLine(""); } - internal override SvgRenderItem Clone(SvgShape svgDocument) - { - throw new NotImplementedException(); - } - internal override ParagraphContainer CreateParagraph(BoundingBox parent) { return new SvgParagraphItem(parent); diff --git a/src/EPPlus.Export.ImageRenderer/RenderItems/SvgRenderItem.cs b/src/EPPlus.Export.ImageRenderer/RenderItems/SvgRenderItem.cs index 0448ad988..69a8b3f12 100644 --- a/src/EPPlus.Export.ImageRenderer/RenderItems/SvgRenderItem.cs +++ b/src/EPPlus.Export.ImageRenderer/RenderItems/SvgRenderItem.cs @@ -10,6 +10,7 @@ Date Author Change ************************************************************************************************* 27/11/2025 EPPlus Software AB EPPlus 9 *************************************************************************************************/ +using EPPlus.Graphics; using EPPlusImageRenderer.Svg; using OfficeOpenXml.Drawing; using System.Globalization; @@ -25,11 +26,7 @@ internal enum SvgFillType } internal abstract class SvgRenderItem : RenderItem { - protected SvgRenderItem() : base() - { - } - - internal SvgRenderItem(ExcelDrawing drawing) : base(drawing) + internal SvgRenderItem(BoundingBox parent) : base(parent) { } public override void Render(StringBuilder sb) diff --git a/src/EPPlus.Export.ImageRenderer/RenderItems/SvgRenderPathItem.cs b/src/EPPlus.Export.ImageRenderer/RenderItems/SvgRenderPathItem.cs index 4a9f53e71..b4729c6b6 100644 --- a/src/EPPlus.Export.ImageRenderer/RenderItems/SvgRenderPathItem.cs +++ b/src/EPPlus.Export.ImageRenderer/RenderItems/SvgRenderPathItem.cs @@ -10,6 +10,8 @@ Date Author Change ************************************************************************************************* 27/11/2025 EPPlus Software AB EPPlus 9 *************************************************************************************************/ +using EPPlus.Export.ImageRenderer.Utils; +using EPPlus.Graphics; using EPPlusImageRenderer.Svg; using OfficeOpenXml.Drawing; using System.Collections.Generic; @@ -19,7 +21,7 @@ namespace EPPlusImageRenderer.RenderItems { internal class SvgRenderPathItem : SvgRenderItem { - public SvgRenderPathItem(ExcelDrawing drawing) : base(drawing) + public SvgRenderPathItem(BoundingBox parent) : base(parent) { } @@ -42,7 +44,7 @@ public override void Render(StringBuilder sb) internal override SvgRenderItem Clone(SvgShape svgDocument) { - var clone = new SvgRenderPathItem(_drawing); + var clone = new SvgRenderPathItem(_drawing.GetBoundingBox()); clone._theme = _theme; CloneBase(clone); clone.Commands = CloneCommands(Commands); diff --git a/src/EPPlus.Export.ImageRenderer/RenderItems/SvgRenderRectItem.cs b/src/EPPlus.Export.ImageRenderer/RenderItems/SvgRenderRectItem.cs index 40bac32e4..f41925968 100644 --- a/src/EPPlus.Export.ImageRenderer/RenderItems/SvgRenderRectItem.cs +++ b/src/EPPlus.Export.ImageRenderer/RenderItems/SvgRenderRectItem.cs @@ -17,17 +17,17 @@ Date Author Change using System.Globalization; using System.Text; using EPPlus.Graphics; +using EPPlus.Export.ImageRenderer.Utils; namespace EPPlusImageRenderer.RenderItems { internal class SvgRenderRectItem : SvgRenderItem { - public SvgRenderRectItem() : base() + public SvgRenderRectItem(BoundingBox parent) : base(parent) { } - - public SvgRenderRectItem(ExcelDrawing drawing) : base(drawing) + public SvgRenderRectItem(ExcelDrawing drawing) : base(drawing.GetBoundingBox()) { } @@ -45,7 +45,7 @@ public SvgRenderRectItem(ExcelDrawing drawing) : base(drawing) public override void Render(StringBuilder sb) { - var groupItem = new SvgGroupItem(); + var groupItem = new SvgGroupItem(Bounds); groupItem.Render(sb); RenderRect(sb); @@ -70,10 +70,10 @@ internal override SvgRenderItem Clone(SvgShape svgDocument) clone._theme = _theme; CloneBase(clone); - clone.X = X * svgDocument.Size.Width; - clone.Y = Y * svgDocument.Size.Height; - clone.Width = svgDocument.Size.Width * Width; - clone.Height = svgDocument.Size.Height * Height; + clone.X = X * svgDocument.Bounds.Width; + clone.Y = Y * svgDocument.Bounds.Height; + clone.Width = svgDocument.Bounds.Width * Width; + clone.Height = svgDocument.Bounds.Height * Height; return clone; } diff --git a/src/EPPlus.Export.ImageRenderer/Svg/Chart/ChartTypeDrawer.cs b/src/EPPlus.Export.ImageRenderer/Svg/Chart/ChartTypeDrawer.cs index 1e9b71b05..722c02dca 100644 --- a/src/EPPlus.Export.ImageRenderer/Svg/Chart/ChartTypeDrawer.cs +++ b/src/EPPlus.Export.ImageRenderer/Svg/Chart/ChartTypeDrawer.cs @@ -1,4 +1,5 @@ -using EPPlusImageRenderer.RenderItems; +using EPPlus.Export.ImageRenderer.Utils; +using EPPlusImageRenderer.RenderItems; using EPPlusImageRenderer.Svg; using OfficeOpenXml; using OfficeOpenXml.Drawing.Chart; @@ -102,7 +103,7 @@ private void AddLine(ExcelChart chartType, ExcelLineChartSerie serie, List(); var markerItems = new List(); @@ -124,8 +125,8 @@ private void AddLine(ExcelChart chartType, ExcelLineChartSerie serie, List 0); RenderItems.Add(item); @@ -162,7 +162,7 @@ public void Render(StringBuilder sb) Legend?.AppendRenderItems(RenderItems); Title?.AppendRenderItems(RenderItems); - sb.Append($""); + sb.Append($""); //Write defs used for gradient colors var writer = new SvgDrawingWriter(this); writer.WriteSvgDefs(sb, RenderItems); diff --git a/src/EPPlus.Export.ImageRenderer/Svg/SvgChartAxis.cs b/src/EPPlus.Export.ImageRenderer/Svg/SvgChartAxis.cs index d4fe4bd68..6a7d3c868 100644 --- a/src/EPPlus.Export.ImageRenderer/Svg/SvgChartAxis.cs +++ b/src/EPPlus.Export.ImageRenderer/Svg/SvgChartAxis.cs @@ -27,6 +27,7 @@ Date Author Change using OfficeOpenXml.Utils.TypeConversion; using System; using System.Collections.Generic; +using System.Linq; using System.Text; namespace EPPlusImageRenderer.Svg @@ -191,7 +192,7 @@ public List AxisValuesTextBoxes public double Max { get; set; } public double MajorUnit { get; set; } public double MinorUnit { get; set; } - internal override void AppendRenderItems(List renderItems) + internal override void AppendRenderItems(List renderItems) { Title?.AppendRenderItems(renderItems); //Title?.Render(sb); @@ -535,6 +536,153 @@ internal double GetPositionInPlotarea(object val) } } } + protected List GetAxisValue(ExcelChartAxisStandard ax, RenderItem rect, out double? min, out double? max, out double? majorUnit) + { + var values = ax.GetAxisValues(out bool isCount); + if (ax.AxisType == eAxisType.Cat && + isCount == false) + { + min = 0; + max = values.Length; + majorUnit = 1; + return values.ToList(); + } + var l = new List(); + min = double.MaxValue; + max = double.MinValue; + foreach (var v in values) + { + var d = ConvertUtil.GetValueDouble(v, false, true); + if (double.IsNaN(d)) + { + d = 0; + } + if (min > d) + { + min = d; + } + if (max < d) + { + max = d; + } + } + var maxMajorTickmarks = 10; //TODO: Calculate based on rect size + GetAutoMinMaxValue(ax, maxMajorTickmarks, isCount, ref min, ref max, out majorUnit); + for (var v = min; v <= max; v += majorUnit) + { + l.Add(v); + } + return l; + } + + private void GetAutoMinMaxValue(ExcelChartAxisStandard ax, int maxMajorTickmarks, bool isCount, ref double? min, ref double? max, out double? majorUnit) + { + if (ax.MinValue.HasValue) + { + min = ax.MinValue; + } + else + { + if (isCount) + { + min = 1; + } + else + { + var diffFromZero = (max - min) / max; + if (diffFromZero > 0.091) + { + min = 0; + } + } + } + + if (isCount) + { + majorUnit = 1; + } + else + { + if (ax.MaxValue.HasValue) + { + max = ax.MaxValue; + majorUnit = ax.MajorUnit ?? GetAutoUnit(min.Value, max.Value); + if (ax.MinValue.HasValue == false) + { + var newMin = max - majorUnit; + while (newMin > min) + { + newMin -= majorUnit.Value; + } + min = newMin; + } + } + else + { + majorUnit = ax.MajorUnit ?? GetAutoUnit(min.Value, max.Value); + if (isCount == false) + { + var diff = max.Value - min.Value; + var newMax = min.Value + majorUnit; + while ((newMax - min) < (diff * 1.05)) + { + newMax += majorUnit.Value; + } + max = newMax; + } + if (min != 0 && max - min < 9) + { + min -= 2; + } + } + var newUnit = majorUnit; + while (newUnit >= 2 && (max - min) / newUnit > maxMajorTickmarks) + { + newUnit /= 2; + } + } + } + + private double GetAutoUnit(double min, double max) + { + var diff = max - min; + if (diff < 8) + { + return 1; + } + else + { + var rawMajorUnit = diff; + var exponent = Math.Floor(Math.Log10(rawMajorUnit)); + var fraction = rawMajorUnit / (Math.Pow(10, exponent)); + double unit; + if (fraction <= 1) + { + unit = 1D; + } + else if (fraction <= 2) + { + unit = 2; + } + else if (fraction <= 2.5) + { + unit = 2.5; + } + else if (fraction <= 5) + { + unit = 5; + } + else + { + unit = 10; + } + + var axMax = unit * Math.Pow(10, exponent); + var axMin = Math.Floor(min / axMax) * axMax; + axMax = Math.Ceiling(max / axMax) * axMax; + return axMax / 10; + } + } } } \ No newline at end of file diff --git a/src/EPPlus.Export.ImageRenderer/Svg/SvgChartLegend.cs b/src/EPPlus.Export.ImageRenderer/Svg/SvgChartLegend.cs index c2be35784..34f298688 100644 --- a/src/EPPlus.Export.ImageRenderer/Svg/SvgChartLegend.cs +++ b/src/EPPlus.Export.ImageRenderer/Svg/SvgChartLegend.cs @@ -10,6 +10,8 @@ Date Author Change ************************************************************************************************* 27/11/2025 EPPlus Software AB EPPlus 9 *************************************************************************************************/ +using EPPlus.Export.ImageRenderer.RenderItems.Shared; +using EPPlus.Export.ImageRenderer.RenderItems.SvgItem; using EPPlus.Export.ImageRenderer.Svg; using EPPlusImageRenderer.RenderItems; using EPPlusImageRenderer.Text; @@ -192,7 +194,7 @@ internal void SetLegend(SvgChart sc) var tm = _seriesHeadersMeasure[index]; var si = GetSeriesIcon(sc, ls, index, tm, pSls); sls.SeriesIcon = si; - sls.Textbox = new TextBox(sc.Chart, si.X2 + MarginExtra, (si.Y1 - (tm.Height * 0.75)), tm.Width, tm.Height); + sls.Textbox = new SvgTextBodyItem(Rectangle.Bounds); var entry = Chart.Legend.Entries.FirstOrDefault(x => x.Index == index); if (entry == null || entry.Font.IsEmpty) { @@ -298,6 +300,6 @@ internal class SvgLegendSerie internal RenderItem SeriesIcon { get; set; } internal RenderItem MarkerIcon { get; set; } internal RenderItem MarkerBackground { get; set; } - internal TextBox Textbox { get; set;} + internal TextBody Textbox { get; set;} } } \ No newline at end of file diff --git a/src/EPPlus.Export.ImageRenderer/Svg/SvgChartObject.cs b/src/EPPlus.Export.ImageRenderer/Svg/SvgChartObject.cs index 1ac487a0b..716f6d797 100644 --- a/src/EPPlus.Export.ImageRenderer/Svg/SvgChartObject.cs +++ b/src/EPPlus.Export.ImageRenderer/Svg/SvgChartObject.cs @@ -18,10 +18,12 @@ Date Author Change namespace EPPlusImageRenderer.Svg { - internal abstract class SvgChartObject : DrawingChart + internal abstract class SvgChartObject { - internal SvgChartObject(ExcelChart chart) : base(chart) + internal ExcelChart Chart { get; } + internal SvgChartObject(ExcelChart chart) { + Chart= chart; } internal void SetMargins(ExcelTextBody tb) { @@ -38,33 +40,34 @@ internal void SetMargins(ExcelTextBody tb) internal SvgRenderRectItem Rectangle { get; set; } internal SvgRenderLineItem Line { get; set; } public string Text { get; set; } - internal abstract void AppendRenderItems(List renderItems); protected static SvgRenderRectItem GetRectFromManualLayout(SvgChart sc, ExcelLayout layout) { var rect = new SvgRenderRectItem(sc.Chart); var ml = layout.ManualLayout; if (ml.LeftMode == eLayoutMode.Edge) { - rect.Left = sc.Size.Width * (float)(layout.ManualLayout.Left ?? 0D) / 100; + rect.Left = sc.Bounds.Width * (float)(layout.ManualLayout.Left ?? 0D) / 100; } else { //TODO:Add factor from default position } //Width is always factor. - rect.Width = sc.Size.Width * ml.GetWidth() / 100; + rect.Width = sc.Bounds.Width * ml.GetWidth() / 100; if (ml.LeftMode == eLayoutMode.Edge) { - rect.Top = sc.Size.Height * (float)(layout.ManualLayout.Top ?? 0D) / 100; + rect.Top = sc.Bounds.Height * (float)(layout.ManualLayout.Top ?? 0D) / 100; } else { //TODO:Add factor from default position } //Height is always factor. - rect.Height = sc.Size.Height * ml.GetHeight() / 100; + rect.Height = sc.Bounds.Height * ml.GetHeight() / 100; return rect; } + internal abstract void AppendRenderItems(List renderItems); + } } diff --git a/src/EPPlus.Export.ImageRenderer/Svg/SvgChartPlotarea.cs b/src/EPPlus.Export.ImageRenderer/Svg/SvgChartPlotarea.cs index 9d0056694..21b35cdd5 100644 --- a/src/EPPlus.Export.ImageRenderer/Svg/SvgChartPlotarea.cs +++ b/src/EPPlus.Export.ImageRenderer/Svg/SvgChartPlotarea.cs @@ -64,7 +64,7 @@ internal SvgRenderRectItem GetPlotAreaRectangle(SvgChart sc) { vaHeight += sc.Legend.Rectangle.Height; } - rect.Height = sc.Size.Height - rect.Top - vaHeight - vaTitleHeight - BottomMargin; + rect.Height = sc.Bounds.Height - rect.Top - vaHeight - vaTitleHeight - BottomMargin; } rect.SetDrawingPropertiesFill(pa.Fill, sc.Chart.StyleManager.Style.PlotArea.FillReference.Color); diff --git a/src/EPPlus.Export.ImageRenderer/Svg/SvgChartTitle.cs b/src/EPPlus.Export.ImageRenderer/Svg/SvgChartTitle.cs index 2b69de285..d138c6bed 100644 --- a/src/EPPlus.Export.ImageRenderer/Svg/SvgChartTitle.cs +++ b/src/EPPlus.Export.ImageRenderer/Svg/SvgChartTitle.cs @@ -10,6 +10,8 @@ Date Author Change ************************************************************************************************* 27/11/2025 EPPlus Software AB EPPlus 9 *************************************************************************************************/ +using EPPlus.Export.ImageRenderer.RenderItems.Shared; +using EPPlus.Export.ImageRenderer.RenderItems.SvgItem; using EPPlusImageRenderer.RenderItems; using EPPlusImageRenderer.Text; using OfficeOpenXml; @@ -41,8 +43,8 @@ internal SvgChartTitle(SvgChart sc, ExcelChartTitleStandard t, string defaultTex LeftMargin = RightMargin = 4; TopMargin = BottomMargin = 2; - var maxWidth = sc.Size.Width * 0.8; - var maxHeight = sc.Size.Height / 2D; + var maxWidth = sc.Bounds.Width * 0.8; + var maxHeight = sc.Bounds.Height / 2D; _title = t; if (axis==null) { @@ -81,7 +83,7 @@ internal SvgChartTitle(SvgChart sc, ExcelChartTitleStandard t, string defaultTex if (axis==null) { Rectangle.Top = (float)8; //8 pixels for the chart title standard offset - Rectangle.Left = (float)(sc.Size.Width - rect.Width) / 2; + Rectangle.Left = (float)(sc.Bounds.Width - rect.Width) / 2; Rectangle.Height = (float)rect.Height; Rectangle.Width = (float)rect.Width; InitTextBox(); @@ -166,21 +168,22 @@ private static string GetDefaultChartTitleText(SvgChart sc, ExcelChartTitleStand internal void InitTextBox() { - TextBox = new TextBox(Chart, Rectangle.Left, Rectangle.Top , Rectangle.Width, Rectangle.Height); - TextBox.Bounds.MarginLeft = LeftMargin; - TextBox.Bounds.MarginRight = RightMargin; - TextBox.Bounds.MarginTop = TopMargin; - TextBox.Bounds.MarginBottom = BottomMargin; + //TextBox = new TextBody(Chart, Rectangle.Left, Rectangle.Top , Rectangle.Width, Rectangle.Height); + TextBox = new SvgTextBodyItem(Rectangle.Bounds); + TextBox.LeftMargin = LeftMargin; + TextBox.RightMargin = RightMargin; + TextBox.TopMargin = TopMargin; + TextBox.BottomMargin = BottomMargin; TextBox.VerticalAlignment = eTextAnchoringType.Top; if(_title.Rotation != 0) { - TextBox.Rotation = _title.Rotation; + TextBox.Bounds.Transform.Rotation = _title.Rotation; } if (_title.TextBody.Paragraphs.Count > 0) { foreach (var p in _title.TextBody.Paragraphs) { - TextBox.ImportParagraph(p); + TextBox.ImportParagraph(p, 0); } } else @@ -189,16 +192,16 @@ internal void InitTextBox() } } - public TextBox TextBox + public TextBody TextBox { get; private set; } - internal override void AppendRenderItems(List renderItems) + internal override void AppendRenderItems(List renderItems) { SvgGroupItem groupItem; if (TextBox.Rotation == 0) { - groupItem = new SvgGroupItem(); + groupItem = new SvgGroupItem(Rectangle.Bounds); } else { diff --git a/src/EPPlus.Export.ImageRenderer/Svg/SvgParagraph.cs b/src/EPPlus.Export.ImageRenderer/Svg/SvgParagraph.cs index 944de1dfc..d75c96e9e 100644 --- a/src/EPPlus.Export.ImageRenderer/Svg/SvgParagraph.cs +++ b/src/EPPlus.Export.ImageRenderer/Svg/SvgParagraph.cs @@ -24,381 +24,380 @@ Date Author Change using System.Xml.Schema; namespace EPPlusImageRenderer.Svg -{ - internal class SvgParagraph : SvgRenderItem - { - int numLines = 0; - - public override void Render(StringBuilder sb) - { - sb.Append(""); - - if (TextRuns.Count > 0) - { - foreach (var textRun in TextRuns) - { - textRun.Render(sb); - } - } - - sb.Append(""); - } - - public override RenderItemType Type => RenderItemType.Text; - - internal override SvgRenderItem Clone(SvgShape svgDocument) - { - throw new NotImplementedException(); - } - - internal override void GetBounds(out double il, out double it, out double ir, out double ib) - { - il = ParagraphArea.Left + LeftMargin; - it = ParagraphArea.Top; - ir = ParagraphArea.Right - RightMargin; - ib = ParagraphArea.Bottom; - } - - string vertAlignAttribute = ""; - protected List TextRuns = new List(); - - //ExcelDrawingParagraph Paragraph; - double RightMargin; - double LeftMargin; - - eTextAlignment HorizontalAlignment; - - RectBase ParagraphArea; - - /// - /// Left position - /// - protected double XPos; - - /// - /// Line-Spacing in pixels - /// - protected double LineSpacing; - protected double LineSpacingAscendantOnly; - - MeasurementFont _measurementFont; - - bool IsFirstParagraph = false; - - ITextMeasurerWrap fmtt; - - double? lnMultiplier = null; - double paragraphHeight; - private string text; - private ExcelTextFont font; - private RectBase area; - private double posY; - /// - /// First paragraph must use different linespacing - /// - /// paragraph data. Ideally only used in constructor as datasource - /// - /// - /// - /// - /// - public SvgParagraph(ExcelDrawingParagraph p, RectBase paragraphArea, string VertAlign, double yPosition, bool isFirstParagraph = false) - { - fmtt = p._prd.Package.Settings.TextSettings.GenericTextMeasurerTrueType; - - _measurementFont = p.DefaultRunProperties.GetMeasureFont(); - fmtt.SetFont(_measurementFont); - vertAlignAttribute = VertAlign; - - //Seperated out in case of some final render item - //needing to adjust without changing the original values - RightMargin = p.RightMargin; - - //p.Indent - //0.5 inches per indent level 48 pixels - - var indent = 48 * p.IndentLevel; - LeftMargin = p.LeftMargin + p.Indent + indent; - - ParagraphArea = paragraphArea; - HorizontalAlignment = p.HorizontalAlignment; - - //Must be set before linespacing - IsFirstParagraph = isFirstParagraph; - - - XPos = GetAlignmentHorizontal(HorizontalAlignment); - - GetBounds(out double l, out double t, out double r, out double b); - var textMaxWidth = r - l; - - foreach (var run in p.TextRuns) - { - AddTextRun(run, paragraphArea.Bottom, yPosition); - } +{ //internal class SvgParagraph : SvgRenderItem + //{ + // int numLines = 0; + + // public override void Render(StringBuilder sb) + // { + // sb.Append(""); + + // if (TextRuns.Count > 0) + // { + // foreach (var textRun in TextRuns) + // { + // textRun.Render(sb); + // } + // } + + // sb.Append(""); + // } + + // public override RenderItemType Type => RenderItemType.Text; + + // internal override SvgRenderItem Clone(SvgShape svgDocument) + // { + // throw new NotImplementedException(); + // } + + // internal override void GetBounds(out double il, out double it, out double ir, out double ib) + // { + // il = ParagraphArea.Left + LeftMargin; + // it = ParagraphArea.Top; + // ir = ParagraphArea.Right - RightMargin; + // ib = ParagraphArea.Bottom; + // } + + // string vertAlignAttribute = ""; + // protected List TextRuns = new List(); + + // //ExcelDrawingParagraph Paragraph; + // double RightMargin; + // double LeftMargin; + + // eTextAlignment HorizontalAlignment; + + // RectBase ParagraphArea; + + // /// + // /// Left position + // /// + // protected double XPos; + + // /// + // /// Line-Spacing in pixels + // /// + // protected double LineSpacing; + // protected double LineSpacingAscendantOnly; + + // MeasurementFont _measurementFont; + + // bool IsFirstParagraph = false; + + // ITextMeasurerWrap fmtt; + + // double? lnMultiplier = null; + // double paragraphHeight; + // private string text; + // private ExcelTextFont font; + // private RectBase area; + // private double posY; + // /// + // /// First paragraph must use different linespacing + // /// + // /// paragraph data. Ideally only used in constructor as datasource + // /// + // /// + // /// + // /// + // /// + // public SvgParagraph(ExcelDrawingParagraph p, RectBase paragraphArea, string VertAlign, double yPosition, bool isFirstParagraph = false) + // { + // fmtt = p._prd.Package.Settings.TextSettings.GenericTextMeasurerTrueType; + + // _measurementFont = p.DefaultRunProperties.GetMeasureFont(); + // fmtt.SetFont(_measurementFont); + // vertAlignAttribute = VertAlign; + + // //Seperated out in case of some final render item + // //needing to adjust without changing the original values + // RightMargin = p.RightMargin; + + // //p.Indent + // //0.5 inches per indent level 48 pixels + + // var indent = 48 * p.IndentLevel; + // LeftMargin = p.LeftMargin + p.Indent + indent; + + // ParagraphArea = paragraphArea; + // HorizontalAlignment = p.HorizontalAlignment; + + // //Must be set before linespacing + // IsFirstParagraph = isFirstParagraph; + + + // XPos = GetAlignmentHorizontal(HorizontalAlignment); + + // GetBounds(out double l, out double t, out double r, out double b); + // var textMaxWidth = r - l; + + // foreach (var run in p.TextRuns) + // { + // AddTextRun(run, paragraphArea.Bottom, yPosition); + // } - if (p._paragraphs.WrapText == eTextWrappingType.Square) - { - List textFragments = new List(); - List fonts = new List(); - - foreach (var txtRun in p.TextRuns) - { - textFragments.Add(txtRun.Text); - fonts.Add(txtRun.GetMeasurementFont()); - } - - var trueTypeMeasurer = (FontMeasurerTrueType)fmtt; - var maxWidthPoints = textMaxWidth.PixelToPoint(); - var svgLines = trueTypeMeasurer.WrapMultipleTextFragments(textFragments, fonts, maxWidthPoints); - - numLines = svgLines.Count; - - List txtRunStrings = new List(); - List txtRunStartIndicies = new List(); - List txtRunEndIndicies = new List(); - - int lastIndex = 0; - for (int i = 0; i < TextRuns.Count; i++) - { - var txtString = TextRuns[i].originalText; - txtRunStrings.Add(txtString); - var indexOfRun = p.Text.IndexOf(txtString, lastIndex); - txtRunStartIndicies.Add(indexOfRun); - txtRunEndIndicies.Add(indexOfRun + txtString.Length); - lastIndex = indexOfRun; - } - - List lineIndicies = new List(); - lastIndex = 0; + // if (p._paragraphs.WrapText == eTextWrappingType.Square) + // { + // List textFragments = new List(); + // List fonts = new List(); + + // foreach (var txtRun in p.TextRuns) + // { + // textFragments.Add(txtRun.Text); + // fonts.Add(txtRun.GetMeasurementFont()); + // } + + // var trueTypeMeasurer = (FontMeasurerTrueType)fmtt; + // var maxWidthPoints = textMaxWidth.PixelToPoint(); + // var svgLines = trueTypeMeasurer.WrapMultipleTextFragments(textFragments, fonts, maxWidthPoints); + + // numLines = svgLines.Count; + + // List txtRunStrings = new List(); + // List txtRunStartIndicies = new List(); + // List txtRunEndIndicies = new List(); + + // int lastIndex = 0; + // for (int i = 0; i < TextRuns.Count; i++) + // { + // var txtString = TextRuns[i].originalText; + // txtRunStrings.Add(txtString); + // var indexOfRun = p.Text.IndexOf(txtString, lastIndex); + // txtRunStartIndicies.Add(indexOfRun); + // txtRunEndIndicies.Add(indexOfRun + txtString.Length); + // lastIndex = indexOfRun; + // } + + // List lineIndicies = new List(); + // lastIndex = 0; - //Last line should be handled by paragraph handling - for (int i = 0; i< svgLines.Count(); i++) - { - var txtString = svgLines[i]; - txtRunStrings.Add(txtString); - var startIndex = p.Text.IndexOf(txtString, lastIndex); - lastIndex = startIndex + txtString.Length; - lineIndicies.Add(startIndex + txtString.Length); - } - - for (int i = 0; i < lineIndicies.Count; i++) - { - var lnBreakPosition = lineIndicies[i]; - for (int j = 0; j < txtRunEndIndicies.Count; j++) - { - var start = txtRunStartIndicies[j]; - var end = txtRunEndIndicies[j]; - - bool containsBreak = (start <= lnBreakPosition && lnBreakPosition < end); - if (containsBreak) - { - var localLnBreakPosition = lnBreakPosition - start; - TextRuns[j].InsertLineBreak(localLnBreakPosition); - break; - } - } - } - SetParagraphLineSpacingInPixels(p, fmtt); - } - else - { - numLines = p.Text.Split(new string[] { Environment.NewLine }, StringSplitOptions.None).Count(); - } - } + // //Last line should be handled by paragraph handling + // for (int i = 0; i< svgLines.Count(); i++) + // { + // var txtString = svgLines[i]; + // txtRunStrings.Add(txtString); + // var startIndex = p.Text.IndexOf(txtString, lastIndex); + // lastIndex = startIndex + txtString.Length; + // lineIndicies.Add(startIndex + txtString.Length); + // } + + // for (int i = 0; i < lineIndicies.Count; i++) + // { + // var lnBreakPosition = lineIndicies[i]; + // for (int j = 0; j < txtRunEndIndicies.Count; j++) + // { + // var start = txtRunStartIndicies[j]; + // var end = txtRunEndIndicies[j]; + + // bool containsBreak = (start <= lnBreakPosition && lnBreakPosition < end); + // if (containsBreak) + // { + // var localLnBreakPosition = lnBreakPosition - start; + // TextRuns[j].InsertLineBreak(localLnBreakPosition); + // break; + // } + // } + // } + // SetParagraphLineSpacingInPixels(p, fmtt); + // } + // else + // { + // numLines = p.Text.Split(new string[] { Environment.NewLine }, StringSplitOptions.None).Count(); + // } + // } - /// - /// First paragraph must use different linespacing - /// - /// paragraph data. Ideally only used in constructor as datasource - /// - /// - /// - /// - /// - public SvgParagraph(string text, ExcelTextFont font, RectBase paragraphArea, string VertAlign, double yPosition) - { - fmtt = font.PictureRelationDocument.Package.Settings.TextSettings.GenericTextMeasurerTrueType; - - _measurementFont = font.GetMeasureFont(); - fmtt.SetFont(_measurementFont); - vertAlignAttribute = VertAlign; + // /// + // /// First paragraph must use different linespacing + // /// + // /// paragraph data. Ideally only used in constructor as datasource + // /// + // /// + // /// + // /// + // /// + // public SvgParagraph(string text, ExcelTextFont font, RectBase paragraphArea, string VertAlign, double yPosition) + // { + // fmtt = font.PictureRelationDocument.Package.Settings.TextSettings.GenericTextMeasurerTrueType; + + // _measurementFont = font.GetMeasureFont(); + // fmtt.SetFont(_measurementFont); + // vertAlignAttribute = VertAlign; - //p.Indent - //0.5 inches per indent level 48 pixels - - ParagraphArea = paragraphArea; - HorizontalAlignment = eTextAlignment.Left; - - LineSpacingAscendantOnly = fmtt.GetBaseLine().PointToPixel(); - LineSpacing = fmtt.GetSingleLineSpacing().PointToPixel(); - - //Must be set before linespacing - IsFirstParagraph = true; - - XPos = GetAlignmentHorizontal(HorizontalAlignment); - - //GetBounds(out double l, out double t, out double r, out double b); - //var textMaxWidth = r - l; - - var tr = new SvgTextRun(text, font, LineSpacing, paragraphArea.Bottom, XPos, yPosition, LineSpacingAscendantOnly); - TextRuns.Add(tr); - } - - private string GetHorizontalAlignmentAttribute(double indentX) - { - string ret = ""; - var xStr = indentX.ToString(CultureInfo.InvariantCulture); - - switch (HorizontalAlignment) - { - default: - case eTextAlignment.Left: - ret = $"text-anchor=\"start\" x=\"{xStr}\" "; - break; - case eTextAlignment.Center: - ret = $"text-anchor=\"middle\" x=\"{xStr}\" "; - break; - case eTextAlignment.Right: - - ret = $"text-anchor=\"end\" x=\"{xStr}\" "; - break; - } - - return ret; - } - - private void SetParagraphLineSpacingInPixels(ExcelDrawingParagraph p, ITextMeasurerWrap fmExact) - { - if (p.LineSpacing.LineSpacingType == eDrawingTextLineSpacing.Exactly) - { - if (IsFirstParagraph) - { - LineSpacingAscendantOnly = p.LineSpacing.Value.PointToPixel(); - } - var lineSpacing= p.LineSpacing.Value.PointToPixel(); - var lines = 1; - foreach (var tr in TextRuns) - { - var lc = tr.GetLineCount(); - if(lc>1) - { - lines += lc - 1; - } - } - LineSpacing = lineSpacing * lines; - } - else - { - var multiplier = (p.LineSpacing.Value / 100); - lnMultiplier = multiplier; - if (IsFirstParagraph) - { - LineSpacingAscendantOnly = multiplier * fmExact.GetBaseLine().PointToPixel(); - } - if (TextRuns.Count == 0) - { - LineSpacing = multiplier * fmExact.GetSingleLineSpacing().PointToPixel(); - } - else - { - var lineSpacing=0D; - var currentMaxLineSpacing = 0D; - foreach(var tr in TextRuns) - { - if(currentMaxLineSpacing < tr.LineSpacing) - { - currentMaxLineSpacing = tr.LineSpacing; - } - var lc = tr.GetLineCount(); - if (lc > 1) - { - lineSpacing += currentMaxLineSpacing; - currentMaxLineSpacing = tr.LineSpacing; - if(lc>2) - { - lineSpacing += (lc - 2) * tr.LineSpacing; - } - } - } - LineSpacing = multiplier * (lineSpacing + currentMaxLineSpacing); - } - } - } - - internal double GetAlignmentHorizontal(eTextAlignment txAlignment) - { - var area = ParagraphArea; - double x = 0; - switch (txAlignment) - { - case eTextAlignment.Left: - default: - x = area.Left + LeftMargin; - break; - case eTextAlignment.Center: - x = (area.Right / 2) + LeftMargin - RightMargin; - break; - case eTextAlignment.Right: - x = area.Right - RightMargin; - break; - } - - return TextUtils.RoundToWhole(x); - } - - internal void AddTextRun(ExcelParagraphTextRunBase txtRun, double clippingHeight, double yPosition) - { - GetBounds(out double l, out double t, out double r, out double b); - var textMaxWidth = r - l; - - SvgTextRun textRun; - - //if (TextRuns.Count == 0 && IsFirstParagraph == true) - //{ - // textRun = new SvgTextRun(txtRun, textMaxWidth, clippingHeight, XPos, yPosition); - //} - //else - //{ - textRun = new SvgTextRun(txtRun, textMaxWidth, clippingHeight, XPos, yPosition); - - //If there are multiple sizes/multiple fonts with multiple sizes - //if (lnType != eDrawingTextLineSpacing.Exactly && txtRun.FontSize != _measurementFont.Size) - if(lnMultiplier.HasValue) - { - textRun.AdjustLineSpacing(lnMultiplier.Value); - } - //} - - TextRuns.Add(textRun); - } - - internal double GetBottomYPosition() - { - //double bottomY = 0; - //if (IsFirstParagraph) - //{ - // bottomY = LineSpacingAscendantOnly + LineSpacing * (numLines - 1); - //} - //else - //{ - // var bottomY = LineSpacing; - //} - return ParagraphArea.Top + LineSpacing; - } - - internal void CalculateTextWrapping(double maxWidth, MeasurementFont mFont, string fullParagraphText) - { - List NewContentLines = new List(); - fmtt.SetFont(mFont); - var textWidth = fmtt.MeasureText(fullParagraphText, mFont); - } - } + // //p.Indent + // //0.5 inches per indent level 48 pixels + + // ParagraphArea = paragraphArea; + // HorizontalAlignment = eTextAlignment.Left; + + // LineSpacingAscendantOnly = fmtt.GetBaseLine().PointToPixel(); + // LineSpacing = fmtt.GetSingleLineSpacing().PointToPixel(); + + // //Must be set before linespacing + // IsFirstParagraph = true; + + // XPos = GetAlignmentHorizontal(HorizontalAlignment); + + // //GetBounds(out double l, out double t, out double r, out double b); + // //var textMaxWidth = r - l; + + // var tr = new SvgTextRun(text, font, LineSpacing, paragraphArea.Bottom, XPos, yPosition, LineSpacingAscendantOnly); + // TextRuns.Add(tr); + // } + + // private string GetHorizontalAlignmentAttribute(double indentX) + // { + // string ret = ""; + // var xStr = indentX.ToString(CultureInfo.InvariantCulture); + + // switch (HorizontalAlignment) + // { + // default: + // case eTextAlignment.Left: + // ret = $"text-anchor=\"start\" x=\"{xStr}\" "; + // break; + // case eTextAlignment.Center: + // ret = $"text-anchor=\"middle\" x=\"{xStr}\" "; + // break; + // case eTextAlignment.Right: + + // ret = $"text-anchor=\"end\" x=\"{xStr}\" "; + // break; + // } + + // return ret; + // } + + // private void SetParagraphLineSpacingInPixels(ExcelDrawingParagraph p, ITextMeasurerWrap fmExact) + // { + // if (p.LineSpacing.LineSpacingType == eDrawingTextLineSpacing.Exactly) + // { + // if (IsFirstParagraph) + // { + // LineSpacingAscendantOnly = p.LineSpacing.Value.PointToPixel(); + // } + // var lineSpacing= p.LineSpacing.Value.PointToPixel(); + // var lines = 1; + // foreach (var tr in TextRuns) + // { + // var lc = tr.GetLineCount(); + // if(lc>1) + // { + // lines += lc - 1; + // } + // } + // LineSpacing = lineSpacing * lines; + // } + // else + // { + // var multiplier = (p.LineSpacing.Value / 100); + // lnMultiplier = multiplier; + // if (IsFirstParagraph) + // { + // LineSpacingAscendantOnly = multiplier * fmExact.GetBaseLine().PointToPixel(); + // } + // if (TextRuns.Count == 0) + // { + // LineSpacing = multiplier * fmExact.GetSingleLineSpacing().PointToPixel(); + // } + // else + // { + // var lineSpacing=0D; + // var currentMaxLineSpacing = 0D; + // foreach(var tr in TextRuns) + // { + // if(currentMaxLineSpacing < tr.LineSpacing) + // { + // currentMaxLineSpacing = tr.LineSpacing; + // } + // var lc = tr.GetLineCount(); + // if (lc > 1) + // { + // lineSpacing += currentMaxLineSpacing; + // currentMaxLineSpacing = tr.LineSpacing; + // if(lc>2) + // { + // lineSpacing += (lc - 2) * tr.LineSpacing; + // } + // } + // } + // LineSpacing = multiplier * (lineSpacing + currentMaxLineSpacing); + // } + // } + // } + + // internal double GetAlignmentHorizontal(eTextAlignment txAlignment) + // { + // var area = ParagraphArea; + // double x = 0; + // switch (txAlignment) + // { + // case eTextAlignment.Left: + // default: + // x = area.Left + LeftMargin; + // break; + // case eTextAlignment.Center: + // x = (area.Right / 2) + LeftMargin - RightMargin; + // break; + // case eTextAlignment.Right: + // x = area.Right - RightMargin; + // break; + // } + + // return TextUtils.RoundToWhole(x); + // } + + // internal void AddTextRun(ExcelParagraphTextRunBase txtRun, double clippingHeight, double yPosition) + // { + // GetBounds(out double l, out double t, out double r, out double b); + // var textMaxWidth = r - l; + + // SvgTextRun textRun; + + // //if (TextRuns.Count == 0 && IsFirstParagraph == true) + // //{ + // // textRun = new SvgTextRun(txtRun, textMaxWidth, clippingHeight, XPos, yPosition); + // //} + // //else + // //{ + // textRun = new SvgTextRun(txtRun, textMaxWidth, clippingHeight, XPos, yPosition); + + // //If there are multiple sizes/multiple fonts with multiple sizes + // //if (lnType != eDrawingTextLineSpacing.Exactly && txtRun.FontSize != _measurementFont.Bounds) + // if(lnMultiplier.HasValue) + // { + // textRun.AdjustLineSpacing(lnMultiplier.Value); + // } + // //} + + // TextRuns.Add(textRun); + // } + + // internal double GetBottomYPosition() + // { + // //double bottomY = 0; + // //if (IsFirstParagraph) + // //{ + // // bottomY = LineSpacingAscendantOnly + LineSpacing * (numLines - 1); + // //} + // //else + // //{ + // // var bottomY = LineSpacing; + // //} + // return ParagraphArea.Top + LineSpacing; + // } + + // internal void CalculateTextWrapping(double maxWidth, MeasurementFont mFont, string fullParagraphText) + // { + // List NewContentLines = new List(); + // fmtt.SetFont(mFont); + // var textWidth = fmtt.MeasureText(fullParagraphText, mFont); + // } + //} } \ No newline at end of file diff --git a/src/EPPlus.Export.ImageRenderer/Svg/SvgShape.cs b/src/EPPlus.Export.ImageRenderer/Svg/SvgShape.cs index db40a5ef6..a8db5b213 100644 --- a/src/EPPlus.Export.ImageRenderer/Svg/SvgShape.cs +++ b/src/EPPlus.Export.ImageRenderer/Svg/SvgShape.cs @@ -29,6 +29,7 @@ Date Author Change using EPPlus.Export.ImageRenderer.RenderItems.Shared; using EPPlus.Graphics; using EPPlus.Export.ImageRenderer.RenderItems.SvgItem; +using EPPlus.Export.ImageRenderer.Utils; namespace EPPlusImageRenderer.Svg { @@ -53,7 +54,7 @@ public SvgShape(ExcelShape shape) : base(shape) var shapeDef = PresetShapeDefinitions.ShapeDefinitions[style].Clone(); shapeDef.Calculate(shape); - RenderItems.Add(new SvgGroupItem(shape.Rotation, 0, 0)); + RenderItems.Add(new SvgGroupItem(shape.GetBoundingBox(), shape.Rotation, 0, 0)); //Draw Filled path's foreach (var path in shapeDef.ShapePaths) @@ -106,7 +107,7 @@ public SvgShape(ExcelShape shape) : base(shape) protected void AddFromPaths(DrawingPath path, bool drawFill = true, bool drawBorder = true) { - var pi = new SvgRenderPathItem(_shape); + var pi = new SvgRenderPathItem(_shape.GetBoundingBox()); var coordinates = new List(); PathCommands cmd = null; PathsBase pCmd = null; @@ -197,13 +198,13 @@ public string ViewBox b = ib; } } - return $"{(l * Size.Width).ToString(CultureInfo.InvariantCulture)},{(t * Size.Height).ToString(CultureInfo.InvariantCulture)},{((Math.Abs(l) + r) * Size.Width).ToString(CultureInfo.InvariantCulture)},{((Math.Abs(t) + b) * Size.Height).ToString(CultureInfo.InvariantCulture)}"; + return $"{(l * Bounds.Width).ToString(CultureInfo.InvariantCulture)},{(t * Bounds.Height).ToString(CultureInfo.InvariantCulture)},{((Math.Abs(l) + r) * Bounds.Width).ToString(CultureInfo.InvariantCulture)},{((Math.Abs(t) + b) * Bounds.Height).ToString(CultureInfo.InvariantCulture)}"; } } public void Render(StringBuilder sb) { - sb.Append($""); + sb.Append($""); //Write defs used for gradient colors var writer = new SvgDrawingWriter(this); @@ -239,7 +240,7 @@ SvgTextBodyItem CreateTextBodyItem() if (insetTextBox == null) { GetShapeInnerBound(out double x, out double y, out double width, out double height); - insetTextBox = new SvgRenderRectItem(); + insetTextBox = new SvgRenderRectItem(Bounds); insetTextBox.Bounds.Left = x; insetTextBox.Bounds.Top = y; insetTextBox.Width = width; @@ -288,8 +289,8 @@ private void GetShapeInnerBound(out double x, out double y, out double width, ou { double currentX = 0, currentY = 0, xe, ye; x = y = 0; - width = xe = Size.Width; - height = ye = Size.Height; + width = xe = Bounds.Width; + height = ye = Bounds.Height; foreach (var ri in RenderItems) { switch (ri.Type) @@ -403,5 +404,6 @@ private static void HandleHorizontal(double x, ref double currentX, ref double x xe = xec; } } + } } \ No newline at end of file diff --git a/src/EPPlus.Export.ImageRenderer/Svg/SvgTextRun.cs b/src/EPPlus.Export.ImageRenderer/Svg/SvgTextRun.cs index 649d2ea06..e83da3b7f 100644 --- a/src/EPPlus.Export.ImageRenderer/Svg/SvgTextRun.cs +++ b/src/EPPlus.Export.ImageRenderer/Svg/SvgTextRun.cs @@ -26,515 +26,515 @@ Date Author Change namespace EPPlusImageRenderer.Svg { - internal class SvgTextRun : SvgRenderItem - { - internal double LineSpacing { get; set; } - double _yEndPos; - double ClippingHeight = Double.NaN; - double fontSizeInPixels; - - internal RectBase BoundingBox = new RectBase(); - internal Coordinate origin = new Coordinate(0, 0); - List Lines; - - MeasurementFont measurementFont; - FontMeasurerTrueType fmExact; - - //Unnecesary?? - double TextLengthInPixels; - double LineSpacingAscendantOnly; - - string horizontalAttribute; - internal readonly string originalText; - private string currentText; - - double _xPosition; - - string fontStyleAttributes; - - bool isFirstInParagraph; - - /// - /// constructor. enter linespacing in pixels - /// - /// - /// - internal SvgTextRun(ExcelParagraphTextRunBase textRun, double textMaxX, double textMaxY, double xPosition, double yPosition) : base() - { - originalText = textRun.Text; - Lines = SplitIntoLines(originalText); - currentText = originalText; - - measurementFont = textRun.GetMeasureFont(); - - isFirstInParagraph = textRun.IsFirstInParagraph; - - if(textRun.Fill.Style==eFillStyle.SolidFill) - { - FillColor = "#"+textRun.Fill.Color.To6CharHexString(); - } - - fmExact = new FontMeasurerTrueType(measurementFont); - - //Used for the first line - LineSpacingAscendantOnly = fmExact.GetBaseLine().PointToPixel(); - - //LineSpacing = lineSpacing; - LineSpacing = fmExact.GetSingleLineSpacing().PointToPixel(); - - - _xPosition = xPosition; - _yEndPos = yPosition; - - fontSizeInPixels = ((double)measurementFont.Size).PointToPixel(true); - - ClippingHeight = textMaxY; - if (textRun.Paragraph._paragraphs.WrapText == eTextWrappingType.Square) - { - CalculateTextWrapping(textMaxX); - } - - if (textRun.FontItalic) - { - fontStyleAttributes += " font-style=\"italic\" "; - } - if(textRun.FontBold) - { - fontStyleAttributes += "font-weight=\"bold\" "; - } - if(textRun.FontUnderLine != eUnderLineType.None | textRun.FontStrike != eStrikeType.No) - { - - fontStyleAttributes += "text-decoration=\""; - if (textRun.FontUnderLine != eUnderLineType.None) - { - switch (textRun.FontUnderLine) - { - case eUnderLineType.Single: - fontStyleAttributes += "underline"; - break; - //These are all css only apparently - //case eUnderLineType.Double: - // fontStyleAttributes += "double"; - // break; - //case eUnderLineType.Dotted: - // fontStyleAttributes += "dotted"; - // break; - //case eUnderLineType.Dash: - // fontStyleAttributes += "dashed"; - // break; - //case eUnderLineType.Wavy: - // fontStyleAttributes += "wavy"; - // break; - default: - fontStyleAttributes += "underline"; - break; - //throw new NotImplementedException("Not implemented yet"); - } - } + //internal class SvgTextRun : SvgRenderItem + //{ + // internal double LineSpacing { get; set; } + // double _yEndPos; + // double ClippingHeight = Double.NaN; + // double fontSizeInPixels; + + // internal RectBase BoundingBox = new RectBase(); + // internal Coordinate origin = new Coordinate(0, 0); + // List Lines; + + // MeasurementFont measurementFont; + // FontMeasurerTrueType fmExact; + + // //Unnecesary?? + // double TextLengthInPixels; + // double LineSpacingAscendantOnly; + + // string horizontalAttribute; + // internal readonly string originalText; + // private string currentText; + + // double _xPosition; + + // string fontStyleAttributes; + + // bool isFirstInParagraph; + + // /// + // /// constructor. enter linespacing in pixels + // /// + // /// + // /// + // internal SvgTextRun(ExcelParagraphTextRunBase textRun, double textMaxX, double textMaxY, double xPosition, double yPosition) : base() + // { + // originalText = textRun.Text; + // Lines = SplitIntoLines(originalText); + // currentText = originalText; + + // measurementFont = textRun.GetMeasureFont(); + + // isFirstInParagraph = textRun.IsFirstInParagraph; + + // if(textRun.Fill.Style==eFillStyle.SolidFill) + // { + // FillColor = "#"+textRun.Fill.Color.To6CharHexString(); + // } + + // fmExact = new FontMeasurerTrueType(measurementFont); + + // //Used for the first line + // LineSpacingAscendantOnly = fmExact.GetBaseLine().PointToPixel(); + + // //LineSpacing = lineSpacing; + // LineSpacing = fmExact.GetSingleLineSpacing().PointToPixel(); + + + // _xPosition = xPosition; + // _yEndPos = yPosition; + + // fontSizeInPixels = ((double)measurementFont.Bounds).PointToPixel(true); + + // ClippingHeight = textMaxY; + // if (textRun.Paragraph._paragraphs.WrapText == eTextWrappingType.Square) + // { + // CalculateTextWrapping(textMaxX); + // } + + // if (textRun.FontItalic) + // { + // fontStyleAttributes += " font-style=\"italic\" "; + // } + // if(textRun.FontBold) + // { + // fontStyleAttributes += "font-weight=\"bold\" "; + // } + // if(textRun.FontUnderLine != eUnderLineType.None | textRun.FontStrike != eStrikeType.No) + // { + + // fontStyleAttributes += "text-decoration=\""; + // if (textRun.FontUnderLine != eUnderLineType.None) + // { + // switch (textRun.FontUnderLine) + // { + // case eUnderLineType.Single: + // fontStyleAttributes += "underline"; + // break; + // //These are all css only apparently + // //case eUnderLineType.Double: + // // fontStyleAttributes += "double"; + // // break; + // //case eUnderLineType.Dotted: + // // fontStyleAttributes += "dotted"; + // // break; + // //case eUnderLineType.Dash: + // // fontStyleAttributes += "dashed"; + // // break; + // //case eUnderLineType.Wavy: + // // fontStyleAttributes += "wavy"; + // // break; + // default: + // fontStyleAttributes += "underline"; + // break; + // //throw new NotImplementedException("Not implemented yet"); + // } + // } - if(textRun.FontStrike == eStrikeType.Single) - { - if(textRun.FontUnderLine != eUnderLineType.None) - { - fontStyleAttributes += ","; - } - fontStyleAttributes += "line-through"; - } - - fontStyleAttributes += "\" "; - } - } - /// - /// A item with a font property and no rich text. - /// - /// The text - /// The font - /// - /// - /// - /// - /// - /// - internal SvgTextRun(string text, ExcelTextFont font, double lineSpacing, double textMaxY, double xPosition, double yPosition, double baselineLineSpacing = double.NaN) : base() - { - originalText = text; - currentText = text; - isFirstInParagraph = true; - Lines = SplitIntoLines(originalText); - - measurementFont = font.GetMeasureFont(); - - - if (font.Fill.Style == eFillStyle.SolidFill) - { - FillColor = "#" + font.Fill.Color.To6CharHexString(); - } - - fmExact = new FontMeasurerTrueType(measurementFont); - - //horizontalTextAlignment = font..Paragraph.HorizontalAlignment; - - LineSpacing = lineSpacing; - - //Used for the first line - LineSpacingAscendantOnly = baselineLineSpacing; - - _xPosition = xPosition; - _yEndPos = yPosition; - - //origin.Left = xPosition; - //origin.Top = yPosition; - - fontSizeInPixels = ((double)measurementFont.Size).PointToPixel(true); - - //ClippingHeight = textMaxY; - //CalculateTextWrapping(textMaxX); - - if (font.Italic) - { - fontStyleAttributes += " _measurementFont-style=\"italic\" "; - } - if (font.Bold) - { - fontStyleAttributes += "_measurementFont-weight=\"bold\" "; - } - if (font.UnderLine != eUnderLineType.None | font.Strike != eStrikeType.No) - { - - fontStyleAttributes += "text-decoration=\""; - if (font.UnderLine != eUnderLineType.None) - { - switch (font.UnderLine) - { - case eUnderLineType.Single: - fontStyleAttributes += "underline"; - break; - //These are all css only apparently - //case eUnderLineType.Double: - // fontStyleAttributes += "double"; - // break; - //case eUnderLineType.Dotted: - // fontStyleAttributes += "dotted"; - // break; - //case eUnderLineType.Dash: - // fontStyleAttributes += "dashed"; - // break; - //case eUnderLineType.Wavy: - // fontStyleAttributes += "wavy"; - // break; - default: - fontStyleAttributes += "underline"; - break; - //throw new NotImplementedException("Not implemented yet"); - } - } - - if (font.Strike == eStrikeType.Single) - { - if (font.UnderLine != eUnderLineType.None) - { - fontStyleAttributes += ","; - } - fontStyleAttributes += "line-through"; - } - - fontStyleAttributes += "\" "; - } - } - - //internal SvgTextRun(ExcelRichText textRun, double lineSpacing, double textMaxX, double textMaxY, double xPosition, double yPosition, MeasurementFont mf, ExcelHorizontalAlignment horAlign, double baselineLineSpacing = double.NaN) : base() - //{ - // originalText = textRun.Textbox; - // Lines = SplitIntoLines(originalText); - - // fmExact = new FontMeasurerTrueType(mf); - // measurementFont = mf; - - // horizontalTextAlignment = (eTextAlignment)horAlign; - - // LineSpacing = fmExact.GetSingleLineSpacing().PointToPixel(true); - - // //Used for the first line - // LineSpacingAscendantOnly = fmExact.GetBaseLine().PointToPixel(true); - - // _xPosition = xPosition; - // _yPosition = yPosition; - - // fontSizeInPixels = ((double)mf.Size).PointToPixel(true); - - // ClippingHeight = textMaxY; - // //No idea how to get this property now since we have no access to textbody/there may not be a direct text body as this might be a cell - // bool wrapText = true; - // if (wrapText) - // { - // CalculateTextWrapping(textMaxX); - // } - - // if (textRun.FontItalic) - // { - // fontStyleAttributes += " font-style=\"italic\" "; - // } - // if(textRun.FontBold) - // { - // fontStyleAttributes += "font-weight=\"bold\" "; - // } - // if(textRun.FontUnderLine != eUnderLineType.None | textRun.FontStrike != eStrikeType.No) - // { - - // fontStyleAttributes += "text-decoration=\""; - // if (textRun.FontUnderLine != eUnderLineType.None) - // { - // switch (textRun.FontUnderLine) - // { - // case eUnderLineType.Single: - // fontStyleAttributes += "underline"; - // break; - // //These are all css only apparently - // //case eUnderLineType.Double: - // // fontStyleAttributes += "double"; - // // break; - // //case eUnderLineType.Dotted: - // // fontStyleAttributes += "dotted"; - // // break; - // //case eUnderLineType.Dash: - // // fontStyleAttributes += "dashed"; - // // break; - // //case eUnderLineType.Wavy: - // // fontStyleAttributes += "wavy"; - // // break; - // default: - // fontStyleAttributes += "underline"; - // break; - // //throw new NotImplementedException("Not implemented yet"); - // } - // } - - // if(textRun.FontStrike == eStrikeType.Single) - // { - // if(textRun.FontUnderLine != eUnderLineType.None) - // { - // fontStyleAttributes += ","; - // } - // fontStyleAttributes += "line-through"; - // } - - // fontStyleAttributes += "\" "; - // } - //} - - internal SvgTextRun(ExcelRichText textRun, double lineSpacing, double textMaxX, double textMaxY, double xPosition, double yPosition, MeasurementFont mf, ExcelHorizontalAlignment horAlign, double baselineLineSpacing = double.NaN) : base() - { - originalText = textRun.Text; - Lines = SplitIntoLines(originalText); - - fmExact = new FontMeasurerTrueType(mf); - measurementFont = mf; - - LineSpacing = fmExact.GetSingleLineSpacing().PointToPixel(true); - - //Used for the first line - LineSpacingAscendantOnly = fmExact.GetBaseLine().PointToPixel(true); - - _xPosition = xPosition; - _yEndPos = yPosition; - - fontSizeInPixels = ((double)mf.Size).PointToPixel(true); - - ClippingHeight = textMaxY; - //No idea how to get this property now since we have no access to textbody - bool wrapText = true; - if (wrapText) - { - CalculateTextWrapping(textMaxX); - } - } - - internal void AdjustLineSpacing(double lineMultiplier) - { - LineSpacing = lineMultiplier * fmExact.GetSingleLineSpacing().PointToPixel(true); - } - - public RectBase textArea; - - public override RenderItemType Type => RenderItemType.TSpan; - - public override void Render(StringBuilder sb) - { - string finalString = ""; - bool useBaselineSpacing = double.IsNaN(LineSpacingAscendantOnly) == false; - Lines = SplitIntoLines(currentText); - - foreach (var line in Lines) - { - finalString += $"= ClippingHeight) - { - visibility = "display=\"none\""; - } - - var yIncreaseString = yIncrease.ToString(CultureInfo.InvariantCulture); - var xString = $"x =\"{(_xPosition).ToString(CultureInfo.InvariantCulture)}\" "; - var dyString = $"dy =\"{yIncreaseString}px\" "; - finalString += xString; - finalString += dyString; - } - - finalString += $"{visibility} " + $"{fontStyleAttributes} "; - if (measurementFont != null) - { - finalString += $" font-family=\"{measurementFont.FontFamily}," - + $"{measurementFont.FontFamily}_MSFontService,sans-serif\" " - + $"font-size=\"{fontSizeInPixels.ToString(CultureInfo.InvariantCulture)}px\" "; - } - sb.Append(finalString); - //Get color etc. - base.Render(sb); - finalString = ""; - - finalString += ">"; - finalString += line; - finalString += ""; - } - - sb.Append(finalString); - //throw new NotImplementedException(); - } - - internal override SvgRenderItem Clone(SvgShape svgDocument) - { - throw new NotImplementedException(); - } - - private int GetNumberOfLines() - { - if (originalText != null) - { - if(currentText == null) - { - currentText = originalText; - } + // if(textRun.FontStrike == eStrikeType.Single) + // { + // if(textRun.FontUnderLine != eUnderLineType.None) + // { + // fontStyleAttributes += ","; + // } + // fontStyleAttributes += "line-through"; + // } + + // fontStyleAttributes += "\" "; + // } + // } + // /// + // /// A item with a font property and no rich text. + // /// + // /// The text + // /// The font + // /// + // /// + // /// + // /// + // /// + // /// + // internal SvgTextRun(string text, ExcelTextFont font, double lineSpacing, double textMaxY, double xPosition, double yPosition, double baselineLineSpacing = double.NaN) : base() + // { + // originalText = text; + // currentText = text; + // isFirstInParagraph = true; + // Lines = SplitIntoLines(originalText); + + // measurementFont = font.GetMeasureFont(); + + + // if (font.Fill.Style == eFillStyle.SolidFill) + // { + // FillColor = "#" + font.Fill.Color.To6CharHexString(); + // } + + // fmExact = new FontMeasurerTrueType(measurementFont); + + // //horizontalTextAlignment = font..Paragraph.HorizontalAlignment; + + // LineSpacing = lineSpacing; + + // //Used for the first line + // LineSpacingAscendantOnly = baselineLineSpacing; + + // _xPosition = xPosition; + // _yEndPos = yPosition; + + // //origin.Left = xPosition; + // //origin.Top = yPosition; + + // fontSizeInPixels = ((double)measurementFont.Bounds).PointToPixel(true); + + // //ClippingHeight = textMaxY; + // //CalculateTextWrapping(textMaxX); + + // if (font.Italic) + // { + // fontStyleAttributes += " _measurementFont-style=\"italic\" "; + // } + // if (font.Bold) + // { + // fontStyleAttributes += "_measurementFont-weight=\"bold\" "; + // } + // if (font.UnderLine != eUnderLineType.None | font.Strike != eStrikeType.No) + // { + + // fontStyleAttributes += "text-decoration=\""; + // if (font.UnderLine != eUnderLineType.None) + // { + // switch (font.UnderLine) + // { + // case eUnderLineType.Single: + // fontStyleAttributes += "underline"; + // break; + // //These are all css only apparently + // //case eUnderLineType.Double: + // // fontStyleAttributes += "double"; + // // break; + // //case eUnderLineType.Dotted: + // // fontStyleAttributes += "dotted"; + // // break; + // //case eUnderLineType.Dash: + // // fontStyleAttributes += "dashed"; + // // break; + // //case eUnderLineType.Wavy: + // // fontStyleAttributes += "wavy"; + // // break; + // default: + // fontStyleAttributes += "underline"; + // break; + // //throw new NotImplementedException("Not implemented yet"); + // } + // } + + // if (font.Strike == eStrikeType.Single) + // { + // if (font.UnderLine != eUnderLineType.None) + // { + // fontStyleAttributes += ","; + // } + // fontStyleAttributes += "line-through"; + // } + + // fontStyleAttributes += "\" "; + // } + // } + + // //internal SvgTextRun(ExcelRichText textRun, double lineSpacing, double textMaxX, double textMaxY, double xPosition, double yPosition, MeasurementFont mf, ExcelHorizontalAlignment horAlign, double baselineLineSpacing = double.NaN) : base() + // //{ + // // originalText = textRun.Textbox; + // // Lines = SplitIntoLines(originalText); + + // // fmExact = new FontMeasurerTrueType(mf); + // // measurementFont = mf; + + // // horizontalTextAlignment = (eTextAlignment)horAlign; + + // // LineSpacing = fmExact.GetSingleLineSpacing().PointToPixel(true); + + // // //Used for the first line + // // LineSpacingAscendantOnly = fmExact.GetBaseLine().PointToPixel(true); + + // // _xPosition = xPosition; + // // _yPosition = yPosition; + + // // fontSizeInPixels = ((double)mf.Bounds).PointToPixel(true); + + // // ClippingHeight = textMaxY; + // // //No idea how to get this property now since we have no access to textbody/there may not be a direct text body as this might be a cell + // // bool wrapText = true; + // // if (wrapText) + // // { + // // CalculateTextWrapping(textMaxX); + // // } + + // // if (textRun.FontItalic) + // // { + // // fontStyleAttributes += " font-style=\"italic\" "; + // // } + // // if(textRun.FontBold) + // // { + // // fontStyleAttributes += "font-weight=\"bold\" "; + // // } + // // if(textRun.FontUnderLine != eUnderLineType.None | textRun.FontStrike != eStrikeType.No) + // // { + + // // fontStyleAttributes += "text-decoration=\""; + // // if (textRun.FontUnderLine != eUnderLineType.None) + // // { + // // switch (textRun.FontUnderLine) + // // { + // // case eUnderLineType.Single: + // // fontStyleAttributes += "underline"; + // // break; + // // //These are all css only apparently + // // //case eUnderLineType.Double: + // // // fontStyleAttributes += "double"; + // // // break; + // // //case eUnderLineType.Dotted: + // // // fontStyleAttributes += "dotted"; + // // // break; + // // //case eUnderLineType.Dash: + // // // fontStyleAttributes += "dashed"; + // // // break; + // // //case eUnderLineType.Wavy: + // // // fontStyleAttributes += "wavy"; + // // // break; + // // default: + // // fontStyleAttributes += "underline"; + // // break; + // // //throw new NotImplementedException("Not implemented yet"); + // // } + // // } + + // // if(textRun.FontStrike == eStrikeType.Single) + // // { + // // if(textRun.FontUnderLine != eUnderLineType.None) + // // { + // // fontStyleAttributes += ","; + // // } + // // fontStyleAttributes += "line-through"; + // // } + + // // fontStyleAttributes += "\" "; + // // } + // //} + + // internal SvgTextRun(ExcelRichText textRun, double lineSpacing, double textMaxX, double textMaxY, double xPosition, double yPosition, MeasurementFont mf, ExcelHorizontalAlignment horAlign, double baselineLineSpacing = double.NaN) : base() + // { + // originalText = textRun.Text; + // Lines = SplitIntoLines(originalText); + + // fmExact = new FontMeasurerTrueType(mf); + // measurementFont = mf; + + // LineSpacing = fmExact.GetSingleLineSpacing().PointToPixel(true); + + // //Used for the first line + // LineSpacingAscendantOnly = fmExact.GetBaseLine().PointToPixel(true); + + // _xPosition = xPosition; + // _yEndPos = yPosition; + + // fontSizeInPixels = ((double)mf.Bounds).PointToPixel(true); + + // ClippingHeight = textMaxY; + // //No idea how to get this property now since we have no access to textbody + // bool wrapText = true; + // if (wrapText) + // { + // CalculateTextWrapping(textMaxX); + // } + // } + + // internal void AdjustLineSpacing(double lineMultiplier) + // { + // LineSpacing = lineMultiplier * fmExact.GetSingleLineSpacing().PointToPixel(true); + // } + + // public RectBase textArea; + + // public override RenderItemType Type => RenderItemType.TSpan; + + // public override void Render(StringBuilder sb) + // { + // string finalString = ""; + // bool useBaselineSpacing = double.IsNaN(LineSpacingAscendantOnly) == false; + // Lines = SplitIntoLines(currentText); + + // foreach (var line in Lines) + // { + // finalString += $"= ClippingHeight) + // { + // visibility = "display=\"none\""; + // } + + // var yIncreaseString = yIncrease.ToString(CultureInfo.InvariantCulture); + // var xString = $"x =\"{(_xPosition).ToString(CultureInfo.InvariantCulture)}\" "; + // var dyString = $"dy =\"{yIncreaseString}px\" "; + // finalString += xString; + // finalString += dyString; + // } + + // finalString += $"{visibility} " + $"{fontStyleAttributes} "; + // if (measurementFont != null) + // { + // finalString += $" font-family=\"{measurementFont.FontFamily}," + // + $"{measurementFont.FontFamily}_MSFontService,sans-serif\" " + // + $"font-size=\"{fontSizeInPixels.ToString(CultureInfo.InvariantCulture)}px\" "; + // } + // sb.Append(finalString); + // //Get color etc. + // base.Render(sb); + // finalString = ""; + + // finalString += ">"; + // finalString += line; + // finalString += ""; + // } + + // sb.Append(finalString); + // //throw new NotImplementedException(); + // } + + // internal override SvgRenderItem Clone(SvgShape svgDocument) + // { + // throw new NotImplementedException(); + // } + + // private int GetNumberOfLines() + // { + // if (originalText != null) + // { + // if(currentText == null) + // { + // currentText = originalText; + // } - SplitIntoLines(currentText); - return Lines.Count; - } - else - { - return 0; - } - } - - internal int GetLineCount() - { - if (Lines == null) return 0; - - var count = 0; - foreach (var l in Lines) - { - count=Regex.Matches(l, Environment.NewLine).Count+1; - } - return count; - } - - internal List SplitIntoLines(string text) - { - return (text ?? "").Split(new string[] { Environment.NewLine }, StringSplitOptions.None).ToList(); - } - - internal override void GetBounds(out double il, out double it, out double ir, out double ib) - { - il = origin.X; - it = origin.Y; - - ir = CalculateRightPositionInPixels(); - ib = CalculateBottomPositionInPixels(); - - BoundingBox.Left = il; - BoundingBox.Top = it; - BoundingBox.Right = ir; - BoundingBox.Bottom = ib; - } - - internal double CalculateRightPositionInPixels() - { - var numLines = GetNumberOfLines(); - double retPos = origin.X; - - if (numLines <= 0 || string.IsNullOrEmpty(originalText)) - { - TextLengthInPixels = 0; - return retPos; - } - - double longestWidth = -1; - - for (int i = 0; i < numLines; i++) - { - var text = Lines[i]; - var width = CalculateTextWidth(Lines[i]); - - if (width > longestWidth) - { - longestWidth = width; - } - } - - TextLengthInPixels = longestWidth; - - retPos += longestWidth; - - return retPos; - } - internal double CalculateBottomPositionInPixels() - { - var lineSize = ((double)measurementFont.Size).PointToPixel(true) + origin.Y; - var bottomPosition = lineSize * GetNumberOfLines(); - return bottomPosition; - } - - internal double CalculateTextWidth(string targetString) - { - var textMesurer = new FontMeasurerTrueType(measurementFont); - textMesurer.MeasureWrappedTextCells = true; - var width = textMesurer.MeasureTextWidth(targetString); - - return width; - } - - internal void CalculateTextWrapping(double maxWidth) - { - List NewContentLines = new List(); - var textMesurer = new FontMeasurerTrueType(measurementFont); - var newLines = textMesurer.MeasureAndWrapText(originalText, measurementFont, maxWidth); - Lines = newLines; - } - - internal void InsertLineBreak(int insertPosition) - { - while(insertPosition < currentText.Length && char.IsWhiteSpace(currentText[insertPosition])) - { - currentText=currentText.Remove(insertPosition, 1); - } - currentText = currentText.Insert(insertPosition, Environment.NewLine); - //if(Lines.Count==1) - //{ - // Lines.Clear(); - // Lines.Add(currentText.Substring(0, insertPosition)); - // Lines.Add(currentText.Substring(insertPosition + Environment.NewLine.Length, currentText.Length-(insertPosition + Environment.NewLine.Length))); - //} - } - } + // SplitIntoLines(currentText); + // return Lines.Count; + // } + // else + // { + // return 0; + // } + // } + + // internal int GetLineCount() + // { + // if (Lines == null) return 0; + + // var count = 0; + // foreach (var l in Lines) + // { + // count=Regex.Matches(l, Environment.NewLine).Count+1; + // } + // return count; + // } + + // internal List SplitIntoLines(string text) + // { + // return (text ?? "").Split(new string[] { Environment.NewLine }, StringSplitOptions.None).ToList(); + // } + + // internal override void GetBounds(out double il, out double it, out double ir, out double ib) + // { + // il = origin.X; + // it = origin.Y; + + // ir = CalculateRightPositionInPixels(); + // ib = CalculateBottomPositionInPixels(); + + // BoundingBox.Left = il; + // BoundingBox.Top = it; + // BoundingBox.Right = ir; + // BoundingBox.Bottom = ib; + // } + + // internal double CalculateRightPositionInPixels() + // { + // var numLines = GetNumberOfLines(); + // double retPos = origin.X; + + // if (numLines <= 0 || string.IsNullOrEmpty(originalText)) + // { + // TextLengthInPixels = 0; + // return retPos; + // } + + // double longestWidth = -1; + + // for (int i = 0; i < numLines; i++) + // { + // var text = Lines[i]; + // var width = CalculateTextWidth(Lines[i]); + + // if (width > longestWidth) + // { + // longestWidth = width; + // } + // } + + // TextLengthInPixels = longestWidth; + + // retPos += longestWidth; + + // return retPos; + // } + // internal double CalculateBottomPositionInPixels() + // { + // var lineSize = ((double)measurementFont.Bounds).PointToPixel(true) + origin.Y; + // var bottomPosition = lineSize * GetNumberOfLines(); + // return bottomPosition; + // } + + // internal double CalculateTextWidth(string targetString) + // { + // var textMesurer = new FontMeasurerTrueType(measurementFont); + // textMesurer.MeasureWrappedTextCells = true; + // var width = textMesurer.MeasureTextWidth(targetString); + + // return width; + // } + + // internal void CalculateTextWrapping(double maxWidth) + // { + // List NewContentLines = new List(); + // var textMesurer = new FontMeasurerTrueType(measurementFont); + // var newLines = textMesurer.MeasureAndWrapText(originalText, measurementFont, maxWidth); + // Lines = newLines; + // } + + // internal void InsertLineBreak(int insertPosition) + // { + // while(insertPosition < currentText.Length && char.IsWhiteSpace(currentText[insertPosition])) + // { + // currentText=currentText.Remove(insertPosition, 1); + // } + // currentText = currentText.Insert(insertPosition, Environment.NewLine); + // //if(Lines.Count==1) + // //{ + // // Lines.Clear(); + // // Lines.Add(currentText.Substring(0, insertPosition)); + // // Lines.Add(currentText.Substring(insertPosition + Environment.NewLine.Length, currentText.Length-(insertPosition + Environment.NewLine.Length))); + // //} + // } + //} } \ No newline at end of file diff --git a/src/EPPlus.Export.ImageRenderer/Text/FontWrapContainer.cs b/src/EPPlus.Export.ImageRenderer/Text/FontWrapContainer.cs index 6c35ae111..a5060beb0 100644 --- a/src/EPPlus.Export.ImageRenderer/Text/FontWrapContainer.cs +++ b/src/EPPlus.Export.ImageRenderer/Text/FontWrapContainer.cs @@ -52,7 +52,7 @@ public FontWrapContainer(FontMeasurerTrueType txtMeasurer, string content, bool private void Initialize(FontMeasurerTrueType txtMeasurer) { _measurer = txtMeasurer; - //transform = parent.transform; + //Transform = parent.Transform; //if (parent != null) //{ // SetParent(parent); @@ -62,7 +62,7 @@ private void Initialize(FontMeasurerTrueType txtMeasurer) //void SetParent(Rect parent) //{ // Parent = parent; - // transform.Parent = parent.transform; + // Transform.Parent = parent.Transform; //} private void SplitContentToLines() diff --git a/src/EPPlus.Export.ImageRenderer/Text/TextBox.cs b/src/EPPlus.Export.ImageRenderer/Text/TextBox.cs index a423fe720..4cbd10e16 100644 --- a/src/EPPlus.Export.ImageRenderer/Text/TextBox.cs +++ b/src/EPPlus.Export.ImageRenderer/Text/TextBox.cs @@ -22,316 +22,317 @@ Date Author Change using System.Text; using EPPlus.Graphics; using System.Linq; +using EPPlus.Export.ImageRenderer.Utils; namespace EPPlusImageRenderer.Text { - internal class TextBox : RenderItem - { - internal RectMargins Bounds = new RectMargins(); + //internal class TextBox : RenderItem + //{ + // internal RectMargins Bounds = new RectMargins(); - internal eTextAnchoringType VerticalAlignment=eTextAnchoringType.Top; + // internal eTextAnchoringType VerticalAlignment=eTextAnchoringType.Top; - internal double ClippingHeight; + // internal double ClippingHeight; - internal bool WrapText = true; + // internal bool WrapText = true; - internal double Width - { - get; - set; - } + // internal double Width + // { + // get; + // set; + // } - internal double Height - { - get; - set; - } + // internal double Height + // { + // get; + // set; + // } - public override RenderItemType Type => RenderItemType.Rect; - - /// - /// Top of the paragraph bounding box - /// - double paragraphStartPosY = 0; - private OfficeOpenXml.Interfaces.Drawing.Text.MeasurementFont mFontTextRun = null; + // public override RenderItemType Type => RenderItemType.Rect; + + // /// + // /// Top of the paragraph bounding box + // /// + // double paragraphStartPosY = 0; + // private OfficeOpenXml.Interfaces.Drawing.Text.MeasurementFont mFontTextRun = null; - List Paragraphs = new List(); - List CellTextRuns = new List(); + // List Paragraphs = new List(); + // List CellTextRuns = new List(); - internal string fontColor; + // internal string fontColor; - /// - /// The input parameters are assumed to be in pixels - /// - /// - /// - /// - /// - internal TextBox(ExcelDrawing drawing, double l, double t, double width, double height) : base(drawing) - { - Bounds.Left = l; Bounds.Top = t; Width = width; Height = height; - - Bounds.Right = Bounds.Left + Width; - Bounds.Bottom = Bounds.Top + Height; - - //Initalize margins to 0 - Bounds.MarginLeft = Bounds.MarginRight = Bounds.MarginTop = Bounds.MarginBottom = 0; - - paragraphStartPosY = Bounds.Top; - - ClippingHeight = Bounds.GetInnerBottom(); - } - - /// - /// It is pressumed that txBody has units in EMUs and SvgRenderRectItem in pixels - /// - /// - /// - internal TextBox(ExcelTextBody txBody, SvgRenderRectItem textBoxRender) - { - Bounds.Left = textBoxRender.Left; - Bounds.Top = textBoxRender.Top; - - //var transform = Bounds.transform; - - //var pos = transform.Position; - - //pos.Y = 2; - - //transform.LocalPosition.X = textBoxRender.X; - //transform.LocalPosition.Y = textBoxRender.Y; - - Width = textBoxRender.Width; - Height = textBoxRender.Height; - - Bounds.Right = Bounds.Left + Width; - Bounds.Bottom = Bounds.Top + Height; - - txBody.GetInsetsOrDefaults(out double l, out double r, out double t, out double b); - - //Ensure margins are in pixels - Bounds.MarginLeft = l.PointToPixel(); - Bounds.MarginRight = r.PointToPixel(); - Bounds.MarginTop = t.PointToPixel(); - Bounds.MarginBottom = b.PointToPixel(); - - paragraphStartPosY = Bounds.Top + Bounds.MarginTop; - - ClippingHeight = Bounds.GetInnerBottom(); - } - - public TextBox() - { - } - - double? _alignmentY = null; - - /// - /// Get the start of text space vertically - /// - /// - /// - private double GetAlignmentVertical() - { - double alignmentY = 0; - double y = paragraphStartPosY; - var height = y - Bounds.Bottom; - - switch (VerticalAlignment) - { - case eTextAnchoringType.Top: - alignmentY = y; - break; - case eTextAnchoringType.Center: - var adjustedHeight = y + (height / 2); - - alignmentY = adjustedHeight + Bounds.MarginTop; - break; - case eTextAnchoringType.Bottom: - alignmentY = height - Bounds.MarginBottom; - break; - } - - _alignmentY = alignmentY; - - return _alignmentY.Value; - } - - private string GetVerticalAlignAttribute(double textOrigY) - { - string ret = "dominant-baseline="; - - switch (VerticalAlignment) - { - case eTextAnchoringType.Top: - ret += $"\"text-top\" "; - break; - case eTextAnchoringType.Center: - ret += $"\"middle\" "; - break; - case eTextAnchoringType.Bottom: - ret += $"\"text-bottom\" "; - break; - default: - return ""; - } - - var yStrValue = textOrigY.ToString(CultureInfo.InvariantCulture); - ret += $" y=\"{yStrValue}\""; - - return ret; - } - - public RectBase GetTextArea() - { - return Bounds.GetInnerRect(); - } - - internal SvgParagraph ImportParagraph(ExcelDrawingParagraph item) - { - var measureFont = item.GetMeasurementFont(); - - //Document Top position for the paragraph text based on vertical alignment - var posY = GetAlignmentVertical(); - var vertAlignAttribute = GetVerticalAlignAttribute(posY); - - var area = GetTextArea(); - - //Limit bounding area with the space taken by previous paragraphs - //Note that this is ONLY identical to PosY if the vertical alignment is top - area.Top = paragraphStartPosY; - - //The first run in the first paragraph must apply different line-spacing - bool isFirst = Paragraphs.Count == 0; - var svgParagraph = new SvgParagraph(item, area, vertAlignAttribute, posY, isFirst); - - //Set starting position for next paragraph - paragraphStartPosY = svgParagraph.GetBottomYPosition(); - - svgParagraph.FillColor = string.IsNullOrEmpty(fontColor) ? item.DefaultRunProperties.Fill.Color.Name : fontColor; - - //Above we've calculated heights from empty paragraphs - //But below we do not add them for rendering (As they would not have any visible effect anyway) - if (item.TextRuns.Count != 0) - { - Paragraphs.Add(svgParagraph); - } - - return svgParagraph; - } - - internal void RemoveParagraph(SvgParagraph item) - { - if(Paragraphs.Contains(item)) - { - Paragraphs.Remove(item); - } - } - internal override void GetBounds(out double il, out double it, out double ir, out double ib) - { - il = Bounds.GetInnerLeft(); - ir = Bounds.GetInnerRight(); - it = Bounds.GetInnerTop(); - ib = Bounds.GetInnerBottom(); - } - - public override void Render(StringBuilder sb) - { - RenderParagraphs(sb); - } - - internal void RenderParagraphs(StringBuilder sb) - { - //TODO: add textbody property stuff here - foreach (var paragraph in Paragraphs) - { - paragraph.Render(sb); - } - } - internal void RenderTextRuns(StringBuilder sb) - { - var groupItem = new SvgGroupItem(); - groupItem.Render(sb); - - var innerTop = Bounds.GetInnerRect().Top; - var fontFamilyAttr = $"_measurementFont-family=\"{mFontTextRun.FontFamily},{mFontTextRun.FontFamily}_MSFontService,sans-serif\" "; - sb.Append($""); - //TODO: add textbody property stuff here - foreach (var textRun in CellTextRuns) - { - textRun.Render(sb); - } - sb.Append(""); - groupItem.RenderEndGroup(sb); - } - - internal void AddCellTextRun(ExcelRangeBase cell) - { - var fontStyle = cell.Style.Font; - - //Line spacing in points and left margin in pixels - double lineSpacing = (0.205d * fontStyle.Size + 1); - Bounds.MarginLeft = lineSpacing; - Bounds.MarginRight = lineSpacing; - - mFontTextRun = fontStyle.GetMeasureFont(); - - var lineSpacingPixels = (lineSpacing / 72d) * 92d; - - var posY = GetAlignmentVertical(); - var vertAlignAttribute = GetVerticalAlignAttribute(posY); - - var area = GetTextArea(); - var horizontalAlign = cell.Style.HorizontalAlignment; - - - foreach (var rt in cell.RichText) - { - var textrun = new SvgTextRun(rt, lineSpacingPixels, - Bounds.GetInnerRight(), Bounds.GetInnerBottom(), - Bounds.GetInnerLeft(), Bounds.GetInnerTop(), mFontTextRun, horizontalAlign); - - CellTextRuns.Add(textrun); - } - - //var lines = CellTextRuns.Last().GetLineCount(); - - //if(lines * ) - - //if (botPos > Bounds.Height) - //{ - // //Negative values increases bounds - // Bounds.Bottom = Bounds.Top + botPos; - //} - } - public string Text { get; set; } - public double Rotation { get; internal set; } - - internal void AddText(string text, OfficeOpenXml.Style.ExcelTextFont font) - { - var measureFont = font.GetMeasureFont(); - - var area = GetTextArea(); - paragraphStartPosY = area.Top; - //Document Top position for the paragraph text based on vertical alignment - var posY = GetAlignmentVertical(); - var vertAlignAttribute = GetVerticalAlignAttribute(posY); - - - - var measurer = font.PictureRelationDocument.Package.Settings.TextSettings.GenericTextMeasurerTrueType; - var m = measurer.MeasureText(text, measureFont); - //Limit bounding area with the space taken by previous paragraphs - //Note that this is ONLY identical to PosY if the vertical alignment is top - - //The first run in the first paragraph must apply different line-spacing - var svgParagraph = new SvgParagraph(text, font, area, vertAlignAttribute, posY); - - svgParagraph.FillColor = font.Fill.Color.To6CharHexString(); - - paragraphStartPosY = svgParagraph.GetBottomYPosition(); - - Paragraphs.Add(svgParagraph); - } - } + // /// + // /// The input parameters are assumed to be in pixels + // /// + // /// + // /// + // /// + // /// + // internal TextBox(ExcelDrawing drawing, double l, double t, double width, double height) : base(drawing.GetBoundingBox()) + // { + // Bounds.Left = l; Bounds.Top = t; Width = width; Height = height; + + // Bounds.Right = Bounds.Left + Width; + // Bounds.Bottom = Bounds.Top + Height; + + // //Initalize margins to 0 + // Bounds.MarginLeft = Bounds.MarginRight = Bounds.MarginTop = Bounds.MarginBottom = 0; + + // paragraphStartPosY = Bounds.Top; + + // ClippingHeight = Bounds.GetInnerBottom(); + // } + + // ///// + // ///// It is pressumed that txBody has units in EMUs and SvgRenderRectItem in pixels + // ///// + // ///// + // ///// + // //internal TextBox(ExcelTextBody txBody, SvgRenderRectItem textBoxRender) + // //{ + // // Bounds.Left = textBoxRender.Left; + // // Bounds.Top = textBoxRender.Top; + + // // //var Transform = Bounds.Transform; + + // // //var pos = Transform.Position; + + // // //pos.Y = 2; + + // // //Transform.LocalPosition.X = textBoxRender.X; + // // //Transform.LocalPosition.Y = textBoxRender.Y; + + // // Width = textBoxRender.Width; + // // Height = textBoxRender.Height; + + // // Bounds.Right = Bounds.Left + Width; + // // Bounds.Bottom = Bounds.Top + Height; + + // // txBody.GetInsetsOrDefaults(out double l, out double r, out double t, out double b); + + // // //Ensure margins are in pixels + // // Bounds.MarginLeft = l.PointToPixel(); + // // Bounds.MarginRight = r.PointToPixel(); + // // Bounds.MarginTop = t.PointToPixel(); + // // Bounds.MarginBottom = b.PointToPixel(); + + // // paragraphStartPosY = Bounds.Top + Bounds.MarginTop; + + // // ClippingHeight = Bounds.GetInnerBottom(); + // //} + + // //public TextBox() + // //{ + // //} + + // double? _alignmentY = null; + + // /// + // /// Get the start of text space vertically + // /// + // /// + // /// + // private double GetAlignmentVertical() + // { + // double alignmentY = 0; + // double y = paragraphStartPosY; + // var height = y - Bounds.Bottom; + + // switch (VerticalAlignment) + // { + // case eTextAnchoringType.Top: + // alignmentY = y; + // break; + // case eTextAnchoringType.Center: + // var adjustedHeight = y + (height / 2); + + // alignmentY = adjustedHeight + Bounds.MarginTop; + // break; + // case eTextAnchoringType.Bottom: + // alignmentY = height - Bounds.MarginBottom; + // break; + // } + + // _alignmentY = alignmentY; + + // return _alignmentY.Value; + // } + + // private string GetVerticalAlignAttribute(double textOrigY) + // { + // string ret = "dominant-baseline="; + + // switch (VerticalAlignment) + // { + // case eTextAnchoringType.Top: + // ret += $"\"text-top\" "; + // break; + // case eTextAnchoringType.Center: + // ret += $"\"middle\" "; + // break; + // case eTextAnchoringType.Bottom: + // ret += $"\"text-bottom\" "; + // break; + // default: + // return ""; + // } + + // var yStrValue = textOrigY.ToString(CultureInfo.InvariantCulture); + // ret += $" y=\"{yStrValue}\""; + + // return ret; + // } + + // public RectBase GetTextArea() + // { + // return Bounds.GetInnerRect(); + // } + + // internal SvgParagraph ImportParagraph(ExcelDrawingParagraph item) + // { + // var measureFont = item.GetMeasurementFont(); + + // //Document Top position for the paragraph text based on vertical alignment + // var posY = GetAlignmentVertical(); + // var vertAlignAttribute = GetVerticalAlignAttribute(posY); + + // var area = GetTextArea(); + + // //Limit bounding area with the space taken by previous paragraphs + // //Note that this is ONLY identical to PosY if the vertical alignment is top + // area.Top = paragraphStartPosY; + + // //The first run in the first paragraph must apply different line-spacing + // bool isFirst = Paragraphs.Count == 0; + // var svgParagraph = new SvgParagraph(item, area, vertAlignAttribute, posY, isFirst); + + // //Set starting position for next paragraph + // paragraphStartPosY = svgParagraph.GetBottomYPosition(); + + // svgParagraph.FillColor = string.IsNullOrEmpty(fontColor) ? item.DefaultRunProperties.Fill.Color.Name : fontColor; + + // //Above we've calculated heights from empty paragraphs + // //But below we do not add them for rendering (As they would not have any visible effect anyway) + // if (item.TextRuns.Count != 0) + // { + // Paragraphs.Add(svgParagraph); + // } + + // return svgParagraph; + // } + + // internal void RemoveParagraph(SvgParagraph item) + // { + // if(Paragraphs.Contains(item)) + // { + // Paragraphs.Remove(item); + // } + // } + // internal override void GetBounds(out double il, out double it, out double ir, out double ib) + // { + // il = Bounds.GetInnerLeft(); + // ir = Bounds.GetInnerRight(); + // it = Bounds.GetInnerTop(); + // ib = Bounds.GetInnerBottom(); + // } + + // public override void Render(StringBuilder sb) + // { + // RenderParagraphs(sb); + // } + + // internal void RenderParagraphs(StringBuilder sb) + // { + // //TODO: add textbody property stuff here + // foreach (var paragraph in Paragraphs) + // { + // paragraph.Render(sb); + // } + // } + // internal void RenderTextRuns(StringBuilder sb) + // { + // var groupItem = new SvgGroupItem(); + // groupItem.Render(sb); + + // var innerTop = Bounds.GetInnerRect().Top; + // var fontFamilyAttr = $"_measurementFont-family=\"{mFontTextRun.FontFamily},{mFontTextRun.FontFamily}_MSFontService,sans-serif\" "; + // sb.Append($""); + // //TODO: add textbody property stuff here + // foreach (var textRun in CellTextRuns) + // { + // textRun.Render(sb); + // } + // sb.Append(""); + // groupItem.RenderEndGroup(sb); + // } + + // internal void AddCellTextRun(ExcelRangeBase cell) + // { + // var fontStyle = cell.Style.Font; + + // //Line spacing in points and left margin in pixels + // double lineSpacing = (0.205d * fontStyle.Bounds + 1); + // Bounds.MarginLeft = lineSpacing; + // Bounds.MarginRight = lineSpacing; + + // mFontTextRun = fontStyle.GetMeasureFont(); + + // var lineSpacingPixels = (lineSpacing / 72d) * 92d; + + // var posY = GetAlignmentVertical(); + // var vertAlignAttribute = GetVerticalAlignAttribute(posY); + + // var area = GetTextArea(); + // var horizontalAlign = cell.Style.HorizontalAlignment; + + + // foreach (var rt in cell.RichText) + // { + // var textrun = new SvgTextRun(rt, lineSpacingPixels, + // Bounds.GetInnerRight(), Bounds.GetInnerBottom(), + // Bounds.GetInnerLeft(), Bounds.GetInnerTop(), mFontTextRun, horizontalAlign); + + // CellTextRuns.Add(textrun); + // } + + // //var lines = CellTextRuns.Last().GetLineCount(); + + // //if(lines * ) + + // //if (botPos > Bounds.Height) + // //{ + // // //Negative values increases bounds + // // Bounds.Bottom = Bounds.Top + botPos; + // //} + // } + // public string Text { get; set; } + // public double Rotation { get; internal set; } + + //internal void AddText(string text, OfficeOpenXml.Style.ExcelTextFont font) + //{ + // var measureFont = font.GetMeasureFont(); + + // var area = GetTextArea(); + // paragraphStartPosY = area.Top; + // //Document Top position for the paragraph text based on vertical alignment + // var posY = GetAlignmentVertical(); + // var vertAlignAttribute = GetVerticalAlignAttribute(posY); + + + + // var measurer = font.PictureRelationDocument.Package.Settings.TextSettings.GenericTextMeasurerTrueType; + // var m = measurer.MeasureText(text, measureFont); + // //Limit bounding area with the space taken by previous paragraphs + // //Note that this is ONLY identical to PosY if the vertical alignment is top + + // //The first run in the first paragraph must apply different line-spacing + // var svgParagraph = new SvgParagraph(text, font, area, vertAlignAttribute, posY); + + // svgParagraph.FillColor = font.Fill.Color.To6CharHexString(); + + // paragraphStartPosY = svgParagraph.GetBottomYPosition(); + + // Paragraphs.Add(svgParagraph); + //} + //} } diff --git a/src/EPPlus.Export.ImageRenderer/Utils/DrawingExtensions.cs b/src/EPPlus.Export.ImageRenderer/Utils/DrawingExtensions.cs new file mode 100644 index 000000000..d50d3a6d7 --- /dev/null +++ b/src/EPPlus.Export.ImageRenderer/Utils/DrawingExtensions.cs @@ -0,0 +1,19 @@ +using EPPlus.Graphics; +using OfficeOpenXml.Drawing; + +namespace EPPlus.Export.ImageRenderer.Utils +{ + internal static class DrawingExtensions + { + internal static BoundingBox GetBoundingBox(this ExcelDrawing drawing) + { + return new BoundingBox() + { + Left = drawing.GetPixelLeft(), + Top = drawing.GetPixelTop(), + Width = drawing.GetPixelWidth(), + Height = drawing.GetPixelHeight() + }; + } + } +} diff --git a/src/EPPlus.Export.ImageRenderer/Utils/SvgDrawingWriter.cs b/src/EPPlus.Export.ImageRenderer/Utils/SvgDrawingWriter.cs index 62de1a7c2..3bd34c23d 100644 --- a/src/EPPlus.Export.ImageRenderer/Utils/SvgDrawingWriter.cs +++ b/src/EPPlus.Export.ImageRenderer/Utils/SvgDrawingWriter.cs @@ -39,11 +39,11 @@ public SvgDrawingWriter(DrawingBase svgDrawing) var wb = svgDrawing.Drawing._drawings.Worksheet.Workbook; _theme = wb.ThemeManager.GetOrCreateTheme(); } - internal void WriteSvgDefs(StringBuilder sb, List renderItems) + internal void WriteSvgDefs(StringBuilder sb, List renderItems) { var defSb = new StringBuilder(); var hs = new HashSet(); - foreach (var item in renderItems) + foreach (RenderItem item in renderItems) { if (item.GradientFill != null) { @@ -353,12 +353,12 @@ private string GetScaling(DrawGradientFill gradientFill) var dx = Math.Abs(l - r); //var scaleTo = Math.Min(dy, dx); //var mult = 0.5 + (scaleTo / 2); - var cx = _svgDrawing.Size.Width * l; - var cy = _svgDrawing.Size.Height * t; + var cx = _svgDrawing.Bounds.Width * l; + var cy = _svgDrawing.Bounds.Height * t; //var radX = Math.Abs((t - b )) * _svgDrawing.FontSize.Item1; //var radY = Math.Abs((l - r)) * _svgDrawing.FontSize.Item2; - var rx = _svgDrawing.Size.Width * 0.5 * (Math.Abs(t - tb) + Math.Abs(b - tt)); - var ry = _svgDrawing.Size.Height * 0.5 * (Math.Abs(l - tr) + Math.Abs(r - tl)); + var rx = _svgDrawing.Bounds.Width * 0.5 * (Math.Abs(t - tb) + Math.Abs(b - tt)); + var ry = _svgDrawing.Bounds.Height * 0.5 * (Math.Abs(l - tr) + Math.Abs(r - tl)); var rad = Math.Sqrt(rx * rx + ry * ry); @@ -443,10 +443,10 @@ private string SetStretchTileProps(ExcelDrawingBlipFill blipFill) { if (blipFill.Stretch) { - var x = _svgDrawing.Size.Width * blipFill.StretchOffset.LeftOffset / 100; - var y = _svgDrawing.Size.Height * blipFill.StretchOffset.TopOffset / 100; - var width = _svgDrawing.Size.Width - x - _svgDrawing.Size.Width * blipFill.StretchOffset.RightOffset / 100; - var height = _svgDrawing.Size.Height - x - _svgDrawing.Size.Height * blipFill.StretchOffset.BottomOffset / 100; + var x = _svgDrawing.Bounds.Width * blipFill.StretchOffset.LeftOffset / 100; + var y = _svgDrawing.Bounds.Height * blipFill.StretchOffset.TopOffset / 100; + var width = _svgDrawing.Bounds.Width - x - _svgDrawing.Bounds.Width * blipFill.StretchOffset.RightOffset / 100; + var height = _svgDrawing.Bounds.Height - x - _svgDrawing.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)}\" "; } else if (!(blipFill.Tile.HorizontalOffset == 0 && blipFill.Tile.VerticalOffset == 0 && @@ -456,13 +456,13 @@ private string SetStretchTileProps(ExcelDrawingBlipFill blipFill) switch (blipFill.Tile.FlipMode) { case eTileFlipMode.X: - flip = $" transform=\"translate({_svgDrawing.Size.Width.ToString(CultureInfo.InvariantCulture)}, 0) scale(-1, 1)\""; + flip = $" transform=\"translate({_svgDrawing.Bounds.Width.ToString(CultureInfo.InvariantCulture)}, 0) scale(-1, 1)\""; break; case eTileFlipMode.Y: - flip = $" transform=\"translate(0, {_svgDrawing.Size.Height.ToString(CultureInfo.InvariantCulture)}) scale(1, -1)\""; + flip = $" transform=\"translate(0, {_svgDrawing.Bounds.Height.ToString(CultureInfo.InvariantCulture)}) scale(1, -1)\""; break; case eTileFlipMode.XY: - flip = $" transform=\"translate({_svgDrawing.Size.Width.ToString(CultureInfo.InvariantCulture)}, {_svgDrawing.Size.Height.ToString(CultureInfo.InvariantCulture)}) scale(-1, -1)\""; + flip = $" transform=\"translate({_svgDrawing.Bounds.Width.ToString(CultureInfo.InvariantCulture)}, {_svgDrawing.Bounds.Height.ToString(CultureInfo.InvariantCulture)}) scale(-1, -1)\""; break; } return $"{flip}"; diff --git a/src/EPPlus.Graphics.Tests/GrahpicsTests.cs b/src/EPPlus.Graphics.Tests/GrahpicsTests.cs index fcb8dbd50..7a69ff5cb 100644 --- a/src/EPPlus.Graphics.Tests/GrahpicsTests.cs +++ b/src/EPPlus.Graphics.Tests/GrahpicsTests.cs @@ -137,52 +137,52 @@ public void BoundingBoxes() { BoundingBox Shape = new BoundingBox(); - Shape.transform.Position = new Vector2(2,2); - Shape.transform.Size = Vector2.One; - Shape.transform.Name = "ShapeTransform"; + Shape.Transform.Position = new Vector2(2,2); + Shape.Transform.Size = Vector2.One; + Shape.Transform.Name = "ShapeTransform"; Shape.Width = 10; Shape.Height = 10; BoundingBox TextBody = new BoundingBox(); - TextBody.transform.Parent = Shape.transform; - TextBody.transform.Name = "TextBodyTransform"; + TextBody.Transform.Parent = Shape.Transform; + TextBody.Transform.Name = "TextBodyTransform"; TextBody.Width = 20; TextBody.Height = 20; - TextBody.transform.LocalPosition = new(10, 11); + TextBody.Transform.LocalPosition = new(10, 11); - Assert.AreEqual(10, TextBody.transform.LocalPosition.X); - Assert.AreEqual(11, TextBody.transform.LocalPosition.Y); + Assert.AreEqual(10, TextBody.Transform.LocalPosition.X); + Assert.AreEqual(11, TextBody.Transform.LocalPosition.Y); BoundingBox Paragraph1 = new BoundingBox(); - Paragraph1.transform.Name = "Paragraph1Transform"; - Paragraph1.transform.Parent = TextBody.transform; + Paragraph1.Transform.Name = "Paragraph1Transform"; + Paragraph1.Transform.Parent = TextBody.Transform; Paragraph1.Width = 5; Paragraph1.Height = 5; - Paragraph1.transform.LocalPosition = new Vector2(5, 8); + Paragraph1.Transform.LocalPosition = new Vector2(5, 8); - Assert.AreEqual(10, TextBody.transform.LocalPosition.X); - Assert.AreEqual(11, TextBody.transform.LocalPosition.Y); + Assert.AreEqual(10, TextBody.Transform.LocalPosition.X); + Assert.AreEqual(11, TextBody.Transform.LocalPosition.Y); - Assert.AreEqual(2, Shape.transform.LocalPosition.X); - Assert.AreEqual(2, Shape.transform.LocalPosition.Y); + Assert.AreEqual(2, Shape.Transform.LocalPosition.X); + Assert.AreEqual(2, Shape.Transform.LocalPosition.Y); - Assert.AreEqual(10, Shape.transform.ChildObjects[0].LocalPosition.X); - Assert.AreEqual(11, Shape.transform.ChildObjects[0].LocalPosition.Y); + Assert.AreEqual(10, Shape.Transform.ChildObjects[0].LocalPosition.X); + Assert.AreEqual(11, Shape.Transform.ChildObjects[0].LocalPosition.Y); - Assert.AreEqual(5, Paragraph1.transform.LocalPosition.X); - Assert.AreEqual(8, Paragraph1.transform.LocalPosition.Y); + Assert.AreEqual(5, Paragraph1.Transform.LocalPosition.X); + Assert.AreEqual(8, Paragraph1.Transform.LocalPosition.Y); - Assert.AreEqual(Paragraph1.transform.Position.X, 17); - Assert.AreEqual(Paragraph1.transform.Position.Y, 21); + Assert.AreEqual(Paragraph1.Transform.Position.X, 17); + Assert.AreEqual(Paragraph1.Transform.Position.Y, 21); - var str = Shape.transform.ToHierarchyString(); + var str = Shape.Transform.ToHierarchyString(); } } } diff --git a/src/EPPlus.Graphics/BoundingBox.cs b/src/EPPlus.Graphics/BoundingBox.cs index 1db0b28cc..30f286d53 100644 --- a/src/EPPlus.Graphics/BoundingBox.cs +++ b/src/EPPlus.Graphics/BoundingBox.cs @@ -7,17 +7,17 @@ namespace EPPlus.Graphics { internal class BoundingBox : Rect { - internal Transform transform; + internal Transform Transform { get; } private BoundingBox _parent = null; - internal BoundingBox Parent { get { return _parent; } set { _parent = value; transform.Parent = value.transform; } } + internal BoundingBox Parent { get { return _parent; } set { _parent = value; Transform.Parent = value.Transform; } } bool ClampedToParent = false; internal BoundingBox() : base() { - transform = new Transform(); + Transform = new Transform(); } internal BoundingBox(double width, double height) : this() @@ -40,14 +40,14 @@ internal BoundingBox(double left, double top, double right, double bottom) : thi /// internal override double Top { - get { return transform.LocalPosition.Y; } + get { return Transform.LocalPosition.Y; } set { var tmpHeight = Height != 0 ? Height : 0; - var currentPosition = transform.LocalPosition; + var currentPosition = Transform.LocalPosition; currentPosition.Y = value; - transform.LocalPosition = currentPosition; + Transform.LocalPosition = currentPosition; //Recalculate bottom position correctly if (tmpHeight != 0) @@ -61,14 +61,14 @@ internal override double Top /// internal override double Left { - get { return transform.LocalPosition.X; } + get { return Transform.LocalPosition.X; } set { var tmpWidth = Width != 0 ? Width : 0; - var currentPosition = transform.LocalPosition; + var currentPosition = Transform.LocalPosition; currentPosition.X = value; - transform.LocalPosition = currentPosition; + Transform.LocalPosition = currentPosition; //Recalculate Right position correctly if (tmpWidth != 0) @@ -116,7 +116,7 @@ private void SetRight(double value) { if(ClampedToParent) { - if(transform.Parent != null) + if(Transform.Parent != null) { var newValue = System.Math.Min(value, Parent.Right); base.Right = newValue; @@ -129,7 +129,7 @@ private void SetBottom(double value) { if (ClampedToParent) { - if (transform.Parent != null) + if (Transform.Parent != null) { var newValue = System.Math.Min(value, Parent.Bottom); base.Bottom = newValue; @@ -138,7 +138,7 @@ private void SetBottom(double value) base.Bottom = value; } - //Quick-access to underlying transform + //Quick-access to underlying Transform /// /// Local position X @@ -148,12 +148,12 @@ private void SetBottom(double value) /// /// Local position Y - /// Y-position from parent transform position + /// Y-position from parent Transform position /// internal override double Y { get { return Top; } set { Top = value; } } //Gets global position x and y - internal double GlobalX { get { return transform.Position.X; } } - internal double GlobalY { get { return transform.Position.Y; } } + internal double GlobalX { get { return Transform.Position.X; } } + internal double GlobalY { get { return Transform.Position.Y; } } } } diff --git a/src/EPPlus.Graphics/Rect.cs b/src/EPPlus.Graphics/Rect.cs index f36964a18..e9984d18c 100644 --- a/src/EPPlus.Graphics/Rect.cs +++ b/src/EPPlus.Graphics/Rect.cs @@ -14,7 +14,7 @@ Date Author Change namespace EPPlus.Graphics { - //REname and move class? Inherit from transform? what do? + //REname and move class? Inherit from Transform? what do? internal class Rect { internal Rect() @@ -88,13 +88,13 @@ internal virtual double Height /// /// Local position X - /// X-position from parent transform position + /// X-position from parent Transform position /// internal virtual double X { get { return Left; } set { Left = value; } } /// /// Local position Y - /// Y-position from parent transform position + /// Y-position from parent Transform position /// internal virtual double Y { get { return Top; } set { Top = value; } } } diff --git a/src/EPPlus/Drawing/ExcelShape.cs b/src/EPPlus/Drawing/ExcelShape.cs index d616ec809..74292e2b1 100644 --- a/src/EPPlus/Drawing/ExcelShape.cs +++ b/src/EPPlus/Drawing/ExcelShape.cs @@ -90,5 +90,6 @@ internal override void DeleteMe() } base.DeleteMe(); } + } } diff --git a/src/EPPlus/Drawing/Style/Text/ExcelTextBody.cs b/src/EPPlus/Drawing/Style/Text/ExcelTextBody.cs index d44a618e1..25e4c7088 100644 --- a/src/EPPlus/Drawing/Style/Text/ExcelTextBody.cs +++ b/src/EPPlus/Drawing/Style/Text/ExcelTextBody.cs @@ -26,7 +26,7 @@ public class ExcelTextBody : XmlHelper { private readonly string _path; private readonly Action _initXml; - private readonly IPictureRelationDocument _pictureRelationDocument; + internal readonly IPictureRelationDocument _pictureRelationDocument; /// /// Represents the TextBody AND the TextbodyProperties node (?) @@ -42,28 +42,6 @@ internal ExcelTextBody(IPictureRelationDocument pictureRelationDocument, XmlName _pictureRelationDocument = pictureRelationDocument; _path = path; - //var bodyProperties = topNode.SelectSingleNode(path, ns); - - //if (bodyProperties != null && bodyProperties.ParentNode != null) - //{ - // //If the path exists, the path leads to the tbPR node. - // //The parent of the tbPR node is the CT_TextBody body node. - // //We want to operate directly on the CT_TextBody node rather than having topNode be the parent of - // //CT_Textbody as the parent node can be very different between exCharts and shapes for example - - // var ctTextBody = bodyProperties.ParentNode; - // ctTextBodyPath = "../"; - - // //var indexOfFirstNode = path.IndexOf("/"); - // //_path = path.Substring(indexOfFirstNode + 1, path.Length - indexOfFirstNode - 1); - // //TopNode = ctTextBody; - //} - //else - //{ - // ctTextBodyPath = _path; - //} - - _initXml = null; AddSchemaNodeOrder(schemaNodeOrder, new string[] { "ln", "noFill", "solidFill", "gradFill", "pattFill", "blipFill", "latin", "ea", "cs", "sym", "hlinkClick", "hlinkMouseOver", "rtl", "extLst", "highlight", "kumimoji", "lang", "altLang", "sz", "b", "i", "u", "strike", "kern", "cap", "spc", "normalizeH", "baseline", "noProof", "dirty", "err", "smtClean", "smtId", "bmk" }); } From 504b2a9ce4556b077a0f22b90973a8dfbaf1c6e9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jan=20K=C3=A4llman?= Date: Wed, 21 Jan 2026 14:21:39 +0100 Subject: [PATCH 002/151] WIP:Fixed errors --- src/EPPlus.Export.ImageRenderer/DrawingBase.cs | 2 +- .../RenderItems/Shared/TextBody.cs | 4 ++-- src/EPPlus.Export.ImageRenderer/Svg/SvgChartAxis.cs | 2 +- src/EPPlus.Export.ImageRenderer/Svg/SvgChartObject.cs | 2 +- src/EPPlus.Export.ImageRenderer/Svg/SvgChartTitle.cs | 2 +- src/EPPlus.Export.ImageRenderer/Utils/SvgDrawingWriter.cs | 2 +- 6 files changed, 7 insertions(+), 7 deletions(-) diff --git a/src/EPPlus.Export.ImageRenderer/DrawingBase.cs b/src/EPPlus.Export.ImageRenderer/DrawingBase.cs index 38110344d..14f226d65 100644 --- a/src/EPPlus.Export.ImageRenderer/DrawingBase.cs +++ b/src/EPPlus.Export.ImageRenderer/DrawingBase.cs @@ -43,7 +43,7 @@ internal DrawingBase(ExcelDrawing drawing) } public ExcelDrawing Drawing { get; } //internal ITextMeasurer TextMeasurer { get; } - public List RenderItems { get; } = new List(); + public List RenderItems { get; } = new List(); //public DrawingSize Bounds { get; internal set; } internal BoundingBox Bounds = new BoundingBox(); } diff --git a/src/EPPlus.Export.ImageRenderer/RenderItems/Shared/TextBody.cs b/src/EPPlus.Export.ImageRenderer/RenderItems/Shared/TextBody.cs index 7d58bc716..df2a1ca42 100644 --- a/src/EPPlus.Export.ImageRenderer/RenderItems/Shared/TextBody.cs +++ b/src/EPPlus.Export.ImageRenderer/RenderItems/Shared/TextBody.cs @@ -16,7 +16,7 @@ namespace EPPlus.Export.ImageRenderer.RenderItems.Shared /// Margin left = X /// Margin right = Y /// - internal abstract class TextBody : RenderItemBase + internal abstract class TextBody : RenderItem { //internal BoundingBox Bounds = new BoundingBox(); /// @@ -41,7 +41,7 @@ internal abstract class TextBody : RenderItemBase private FontMeasurerTrueType _measurer = null; - public TextBody(BoundingBox parent) + public TextBody(BoundingBox parent) : base(parent) { Bounds.Transform.Name = "TxtBody"; diff --git a/src/EPPlus.Export.ImageRenderer/Svg/SvgChartAxis.cs b/src/EPPlus.Export.ImageRenderer/Svg/SvgChartAxis.cs index 6a7d3c868..36ce36452 100644 --- a/src/EPPlus.Export.ImageRenderer/Svg/SvgChartAxis.cs +++ b/src/EPPlus.Export.ImageRenderer/Svg/SvgChartAxis.cs @@ -192,7 +192,7 @@ public List AxisValuesTextBoxes public double Max { get; set; } public double MajorUnit { get; set; } public double MinorUnit { get; set; } - internal override void AppendRenderItems(List renderItems) + internal override void AppendRenderItems(List renderItems) { Title?.AppendRenderItems(renderItems); //Title?.Render(sb); diff --git a/src/EPPlus.Export.ImageRenderer/Svg/SvgChartObject.cs b/src/EPPlus.Export.ImageRenderer/Svg/SvgChartObject.cs index 716f6d797..bf664d30e 100644 --- a/src/EPPlus.Export.ImageRenderer/Svg/SvgChartObject.cs +++ b/src/EPPlus.Export.ImageRenderer/Svg/SvgChartObject.cs @@ -67,7 +67,7 @@ protected static SvgRenderRectItem GetRectFromManualLayout(SvgChart sc, ExcelLay rect.Height = sc.Bounds.Height * ml.GetHeight() / 100; return rect; } - internal abstract void AppendRenderItems(List renderItems); + internal abstract void AppendRenderItems(List renderItems); } } diff --git a/src/EPPlus.Export.ImageRenderer/Svg/SvgChartTitle.cs b/src/EPPlus.Export.ImageRenderer/Svg/SvgChartTitle.cs index d138c6bed..fc9fbc9c4 100644 --- a/src/EPPlus.Export.ImageRenderer/Svg/SvgChartTitle.cs +++ b/src/EPPlus.Export.ImageRenderer/Svg/SvgChartTitle.cs @@ -196,7 +196,7 @@ public TextBody TextBox { get; private set; } - internal override void AppendRenderItems(List renderItems) + internal override void AppendRenderItems(List renderItems) { SvgGroupItem groupItem; if (TextBox.Rotation == 0) diff --git a/src/EPPlus.Export.ImageRenderer/Utils/SvgDrawingWriter.cs b/src/EPPlus.Export.ImageRenderer/Utils/SvgDrawingWriter.cs index 3bd34c23d..e28372257 100644 --- a/src/EPPlus.Export.ImageRenderer/Utils/SvgDrawingWriter.cs +++ b/src/EPPlus.Export.ImageRenderer/Utils/SvgDrawingWriter.cs @@ -39,7 +39,7 @@ public SvgDrawingWriter(DrawingBase svgDrawing) var wb = svgDrawing.Drawing._drawings.Worksheet.Workbook; _theme = wb.ThemeManager.GetOrCreateTheme(); } - internal void WriteSvgDefs(StringBuilder sb, List renderItems) + internal void WriteSvgDefs(StringBuilder sb, List renderItems) { var defSb = new StringBuilder(); var hs = new HashSet(); From 0686c3d583726e62ff5f8d499c01bb18c4678e79 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jan=20K=C3=A4llman?= Date: Wed, 21 Jan 2026 14:29:28 +0100 Subject: [PATCH 003/151] WIP:Fixed more errors --- .../ImageRenderer.cs | 50 ++++---- .../RenderItems/SvgGroupItem.cs | 5 +- .../RenderItems/SvgRenderEllipseItem.cs | 3 +- .../RenderItems/SvgRenderLineItem.cs | 3 +- .../Svg/LineMarkerHelper.cs | 9 +- .../Svg/SvgChartTitle.cs | 4 +- .../Svg/SvgRange.cs | 118 +++++++++--------- .../Svg/SvgShape.cs | 2 +- 8 files changed, 100 insertions(+), 94 deletions(-) diff --git a/src/EPPlus.Export.ImageRenderer/ImageRenderer.cs b/src/EPPlus.Export.ImageRenderer/ImageRenderer.cs index 1b7cfb150..aca39d2b6 100644 --- a/src/EPPlus.Export.ImageRenderer/ImageRenderer.cs +++ b/src/EPPlus.Export.ImageRenderer/ImageRenderer.cs @@ -55,31 +55,31 @@ public string RenderDrawingToSvg(ExcelDrawing drawing) throw new NotImplementedException("Image rendering for drawing type not implemented."); } - public string RenderRangeToSvg(ExcelRange range) - { - var ws = range.Worksheet; - - //ws.Workbook.Styles.CellXfs. - - double totalWidth = 0; - foreach (var col in range.EntireColumn) - { - totalWidth += ExcelColumn.ColumnWidthToPixels(col.Width, range.Worksheet.Workbook.MaxFontWidth); - } - double totalHeight = 0; - foreach (var row in range.EntireRow) - { - totalHeight = row.Height; - } - - var sRange = new SvgRange(range, totalWidth, totalHeight); - - var sb = new StringBuilder(); - sb.Append($""); - sRange.Render(sb); - sb.Append($""); - return sb.ToString(); - } + //public string RenderRangeToSvg(ExcelRange range) + //{ + // var ws = range.Worksheet; + + // //ws.Workbook.Styles.CellXfs. + + // double totalWidth = 0; + // foreach (var col in range.EntireColumn) + // { + // totalWidth += ExcelColumn.ColumnWidthToPixels(col.Width, range.Worksheet.Workbook.MaxFontWidth); + // } + // double totalHeight = 0; + // foreach (var row in range.EntireRow) + // { + // totalHeight = row.Height; + // } + + // var sRange = new SvgRange(range, totalWidth, totalHeight); + + // var sb = new StringBuilder(); + // sb.Append($""); + // sRange.Render(sb); + // sb.Append($""); + // return sb.ToString(); + //} public string RenderBox(string boxText) { diff --git a/src/EPPlus.Export.ImageRenderer/RenderItems/SvgGroupItem.cs b/src/EPPlus.Export.ImageRenderer/RenderItems/SvgGroupItem.cs index b8372bf53..eff03e71c 100644 --- a/src/EPPlus.Export.ImageRenderer/RenderItems/SvgGroupItem.cs +++ b/src/EPPlus.Export.ImageRenderer/RenderItems/SvgGroupItem.cs @@ -12,6 +12,7 @@ Date Author Change *************************************************************************************************/ using EPPlus.Graphics; using EPPlusImageRenderer.Svg; +using System.Drawing; using System.Globalization; using System.Text; @@ -27,10 +28,12 @@ internal SvgGroupItem(BoundingBox parent) : base(parent) { } - internal SvgGroupItem(BoundingBox parent, double rotation, double cx, double cy) : base(parent) + internal SvgGroupItem(BoundingBox parent, double rotation) : base(parent) { if(rotation!=0) { + var cx = parent.Width / 2 + parent.Left; + var cy = parent.Height / 2 + parent.Top; if (cx == 0 && cy == 0) { GroupTransform = $"transform=\"rotate({rotation}))\""; diff --git a/src/EPPlus.Export.ImageRenderer/RenderItems/SvgRenderEllipseItem.cs b/src/EPPlus.Export.ImageRenderer/RenderItems/SvgRenderEllipseItem.cs index 7cceae2a7..6d7ee9e7c 100644 --- a/src/EPPlus.Export.ImageRenderer/RenderItems/SvgRenderEllipseItem.cs +++ b/src/EPPlus.Export.ImageRenderer/RenderItems/SvgRenderEllipseItem.cs @@ -10,6 +10,7 @@ Date Author Change ************************************************************************************************* 27/11/2025 EPPlus Software AB EPPlus 9 *************************************************************************************************/ +using EPPlus.Export.ImageRenderer.Utils; using EPPlusImageRenderer.Svg; using OfficeOpenXml.Drawing; using System.Globalization; @@ -18,7 +19,7 @@ namespace EPPlusImageRenderer.RenderItems { internal class SvgRenderEllipseItem : SvgRenderItem { - public SvgRenderEllipseItem(ExcelDrawing drawing) : base(drawing) + public SvgRenderEllipseItem(ExcelDrawing drawing) : base(drawing.GetBoundingBox()) { } diff --git a/src/EPPlus.Export.ImageRenderer/RenderItems/SvgRenderLineItem.cs b/src/EPPlus.Export.ImageRenderer/RenderItems/SvgRenderLineItem.cs index 660ec843f..76114d705 100644 --- a/src/EPPlus.Export.ImageRenderer/RenderItems/SvgRenderLineItem.cs +++ b/src/EPPlus.Export.ImageRenderer/RenderItems/SvgRenderLineItem.cs @@ -10,6 +10,7 @@ Date Author Change ************************************************************************************************* 27/11/2025 EPPlus Software AB EPPlus 9 *************************************************************************************************/ +using EPPlus.Export.ImageRenderer.Utils; using EPPlusImageRenderer.Svg; using OfficeOpenXml.Drawing; using System.Globalization; @@ -18,7 +19,7 @@ namespace EPPlusImageRenderer.RenderItems { internal class SvgRenderLineItem : SvgRenderItem { - public SvgRenderLineItem(ExcelDrawing drawing) : base(drawing) + public SvgRenderLineItem(ExcelDrawing drawing) : base(drawing.GetBoundingBox()) { } diff --git a/src/EPPlus.Export.ImageRenderer/Svg/LineMarkerHelper.cs b/src/EPPlus.Export.ImageRenderer/Svg/LineMarkerHelper.cs index 12f736783..ad9923ea0 100644 --- a/src/EPPlus.Export.ImageRenderer/Svg/LineMarkerHelper.cs +++ b/src/EPPlus.Export.ImageRenderer/Svg/LineMarkerHelper.cs @@ -1,4 +1,5 @@ -using EPPlusImageRenderer; +using EPPlus.Export.ImageRenderer.Utils; +using EPPlusImageRenderer; using EPPlusImageRenderer.RenderItems; using EPPlusImageRenderer.Svg; using OfficeOpenXml.Drawing; @@ -35,7 +36,7 @@ internal static RenderItem GetMarkerItem(SvgChart sc, ExcelLineChartSerie ls, fl }; break; case eMarkerStyle.Triangle: - item = new SvgRenderPathItem(sc.Drawing) + item = new SvgRenderPathItem(sc.Drawing.GetBoundingBox()) { Commands = new List() }; @@ -44,7 +45,7 @@ internal static RenderItem GetMarkerItem(SvgChart sc, ExcelLineChartSerie ls, fl ((SvgRenderPathItem)item).Commands.Add(new PathCommands(PathCommandType.End, item)); break; case eMarkerStyle.Diamond: - item = new SvgRenderPathItem(sc.Drawing) + item = new SvgRenderPathItem(sc.Drawing.GetBoundingBox()) { Commands = new List() }; @@ -95,7 +96,7 @@ internal static RenderItem GetMarkerItem(SvgChart sc, ExcelLineChartSerie ls, fl case eMarkerStyle.Plus: case eMarkerStyle.Star: case eMarkerStyle.X: - var pathItem = new SvgRenderPathItem(sc.Drawing) + var pathItem = new SvgRenderPathItem(sc.Drawing.GetBoundingBox()) { Commands = new List() }; diff --git a/src/EPPlus.Export.ImageRenderer/Svg/SvgChartTitle.cs b/src/EPPlus.Export.ImageRenderer/Svg/SvgChartTitle.cs index fc9fbc9c4..c5f195b81 100644 --- a/src/EPPlus.Export.ImageRenderer/Svg/SvgChartTitle.cs +++ b/src/EPPlus.Export.ImageRenderer/Svg/SvgChartTitle.cs @@ -199,13 +199,13 @@ public TextBody TextBox internal override void AppendRenderItems(List renderItems) { SvgGroupItem groupItem; - if (TextBox.Rotation == 0) + if (TextBox.Bounds.Transform.Rotation == 0) { groupItem = new SvgGroupItem(Rectangle.Bounds); } else { - groupItem = new SvgGroupItem(TextBox.Rotation, Rectangle.Width / 2 + Rectangle.Left, Rectangle.Height / 2 + Rectangle.Top); + groupItem = new SvgGroupItem(Rectangle.Bounds, TextBox.Bounds.Transform.Rotation); } renderItems.Add(groupItem); diff --git a/src/EPPlus.Export.ImageRenderer/Svg/SvgRange.cs b/src/EPPlus.Export.ImageRenderer/Svg/SvgRange.cs index 4e0c748b1..9d0da8dc6 100644 --- a/src/EPPlus.Export.ImageRenderer/Svg/SvgRange.cs +++ b/src/EPPlus.Export.ImageRenderer/Svg/SvgRange.cs @@ -8,83 +8,83 @@ namespace EPPlus.Export.ImageRenderer.Svg { - internal class SvgRange - { - List renderItems = new List(); - List textBoxes = new List(); + //internal class SvgRange + //{ + // List renderItems = new List(); + // //List textBoxes = new List(); - internal SvgRange(ExcelRange range, double totalWidth, double totalHeight) - { - SvgRenderRectItem rangeBB = new SvgRenderRectItem(); + // internal SvgRange(ExcelRange range, double totalWidth, double totalHeight) + // { + // SvgRenderRectItem rangeBB = new SvgRenderRectItem(); - //To pixel multiplier - float mult = 96f / 72f; + // //To pixel multiplier + // float mult = 96f / 72f; - rangeBB.Width = (float)totalWidth; - rangeBB.Height = (float)totalHeight * mult; + // rangeBB.Width = (float)totalWidth; + // rangeBB.Height = (float)totalHeight * mult; - rangeBB.BorderColor = "yellow"; + // rangeBB.BorderColor = "yellow"; - rangeBB.BorderWidth = 1; + // rangeBB.BorderWidth = 1; - renderItems.Add(rangeBB); + // renderItems.Add(rangeBB); - float currentWidth = 0f; + // 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 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); + // 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; + // 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; + // SvgRenderRectItem cellBB = new SvgRenderRectItem(); + // cellBB.Left = currentWidth; + // cellBB.Top = currentHeight; - cellBB.Width = (float)colWidth; - cellBB.Height = (float)height; + // cellBB.Width = (float)colWidth; + // cellBB.Height = (float)height; - cellBB.BorderWidth = 1; + // cellBB.BorderWidth = 1; - cellBB.BorderColor = "black"; - cellBB.FillColor = "gray"; + // cellBB.BorderColor = "black"; + // cellBB.FillColor = "gray"; - renderItems.Add(cellBB); + // renderItems.Add(cellBB); - var cellTextBox = new TextBox(null, currentWidth, currentHeight, colWidth, height); - cellTextBox.AddCellTextRun(cell); + // var cellTextBox = new TextBox(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); + // var deltaHeight = (float)cellTextBox.Bounds.Height; + // cellBB.Height = cellBB.Height < deltaHeight ? deltaHeight : cellBB.Height; + // textBoxes.Add(cellTextBox); - currentHeight += height; - } - currentWidth += (float)colWidth; - } + // 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); - } - } - } + // internal void Render(StringBuilder sb) + // { + // foreach (var item in renderItems) + // { + // item.Render(sb); + // } + // foreach (var item in textBoxes) + // { + // item.RenderTextRuns(sb); + // } + // } + //} } \ No newline at end of file diff --git a/src/EPPlus.Export.ImageRenderer/Svg/SvgShape.cs b/src/EPPlus.Export.ImageRenderer/Svg/SvgShape.cs index a8db5b213..52fbdaa96 100644 --- a/src/EPPlus.Export.ImageRenderer/Svg/SvgShape.cs +++ b/src/EPPlus.Export.ImageRenderer/Svg/SvgShape.cs @@ -54,7 +54,7 @@ public SvgShape(ExcelShape shape) : base(shape) var shapeDef = PresetShapeDefinitions.ShapeDefinitions[style].Clone(); shapeDef.Calculate(shape); - RenderItems.Add(new SvgGroupItem(shape.GetBoundingBox(), shape.Rotation, 0, 0)); + RenderItems.Add(new SvgGroupItem(shape.GetBoundingBox(), shape.Rotation)); //Draw Filled path's foreach (var path in shapeDef.ShapePaths) From e1b72c22c3bb6134f86c9121298091b1d07a8872 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ossian=20Edstr=C3=B6m?= Date: Wed, 21 Jan 2026 15:25:33 +0100 Subject: [PATCH 004/151] Fixed broken shape drawing (bounds were not set) --- .../RenderItems/RenderItem.cs | 9 ++-- .../RenderItems/Shared/TextBody.cs | 4 ++ .../SvgItem/SvgParagraphCollection.cs | 41 ------------------- .../RenderItems/SvgItem/SvgParagraphItem.cs | 26 ++++++------ .../RenderItems/SvgItem/SvgTextBodyItem.cs | 14 +++---- .../RenderItems/SvgRenderPathItem.cs | 7 +++- 6 files changed, 34 insertions(+), 67 deletions(-) delete mode 100644 src/EPPlus.Export.ImageRenderer/RenderItems/SvgItem/SvgParagraphCollection.cs diff --git a/src/EPPlus.Export.ImageRenderer/RenderItems/RenderItem.cs b/src/EPPlus.Export.ImageRenderer/RenderItems/RenderItem.cs index fca007a2c..1df840667 100644 --- a/src/EPPlus.Export.ImageRenderer/RenderItems/RenderItem.cs +++ b/src/EPPlus.Export.ImageRenderer/RenderItems/RenderItem.cs @@ -10,8 +10,6 @@ Date Author Change ************************************************************************************************* 27/11/2025 EPPlus Software AB EPPlus 9 *************************************************************************************************/ -using EPPlus.Export.ImageRenderer.Svg.NodeAttributes; -using EPPlus.Export.ImageRenderer.Svg.Writer; using EPPlus.Graphics; using EPPlusImageRenderer.Utils; using OfficeOpenXml.Drawing; @@ -19,10 +17,8 @@ Date Author Change using OfficeOpenXml.Drawing.Style.Coloring; using OfficeOpenXml.Drawing.Style.Fill; using OfficeOpenXml.Drawing.Theme; -using OfficeOpenXml.Utils; using System; using System.Drawing; -using System.IO; using System.Text; using EPPlusColorConverter = OfficeOpenXml.Utils.TypeConversion.ColorConverter; namespace EPPlusImageRenderer.RenderItems @@ -215,6 +211,11 @@ private string GetFillColor(ExcelDrawingFillBasic fill, ExcelDrawingColorManager fc = ColorUtils.GetAdjustedColor(fillColorSource, fc); return "#" + fc.ToArgb().ToString("x8").Substring(2); } + + internal void SetTheme(ExcelTheme theme) + { + _theme = theme; + } } /// /// Base class for any item rendered. diff --git a/src/EPPlus.Export.ImageRenderer/RenderItems/Shared/TextBody.cs b/src/EPPlus.Export.ImageRenderer/RenderItems/Shared/TextBody.cs index df2a1ca42..82562a624 100644 --- a/src/EPPlus.Export.ImageRenderer/RenderItems/Shared/TextBody.cs +++ b/src/EPPlus.Export.ImageRenderer/RenderItems/Shared/TextBody.cs @@ -84,6 +84,7 @@ public void ImportParagraph(ExcelDrawingParagraph item, double startingY) Paragraphs.Add(paragraph); //TODO; Fix this. This is a strange workaround + paragraph.SetTheme(item._prd.Package.Workbook.ThemeManager.GetOrCreateTheme()); paragraph.SetDrawingPropertiesFill(item.DefaultRunProperties.Fill, null); } @@ -100,6 +101,9 @@ public void ImportTextBody(ExcelTextBody body) //We already apply bounds top via the parent Transform double paragraphStartY = GetAlignmentVertical(); + //TODO; Fix this. This is a strange workaround + SetTheme(body._pictureRelationDocument.Package.Workbook.ThemeManager.GetOrCreateTheme()); + foreach (var paragraph in body.Paragraphs) { ImportParagraph(paragraph, paragraphStartY); diff --git a/src/EPPlus.Export.ImageRenderer/RenderItems/SvgItem/SvgParagraphCollection.cs b/src/EPPlus.Export.ImageRenderer/RenderItems/SvgItem/SvgParagraphCollection.cs deleted file mode 100644 index 7a50767f2..000000000 --- a/src/EPPlus.Export.ImageRenderer/RenderItems/SvgItem/SvgParagraphCollection.cs +++ /dev/null @@ -1,41 +0,0 @@ -using EPPlus.Export.ImageRenderer.RenderItems.Shared; -using System; -using System.Collections; -using System.Collections.Generic; -using System.Linq; -using System.Text; - -namespace EPPlus.Export.ImageRenderer.RenderItems.SvgItem -{ - //internal class SvgParagraphCollection : ParagraphContainerCollection - //{ - // List realList = new List(); - - // protected internal override List _list - // { - // get - // { - // return realList; - // } - // set - // { - // realList = value; - // } - // } - - // public override void Render(StringBuilder sb) - // { - // throw new NotImplementedException(); - // } - - // //IEnumerator IEnumerable.GetEnumerator() - // //{ - // // return _list.GetEnumerator(); - // //} - - // //IEnumerator IEnumerable.GetEnumerator() - // //{ - // // return _list.GetEnumerator(); - // //} - //} -} diff --git a/src/EPPlus.Export.ImageRenderer/RenderItems/SvgItem/SvgParagraphItem.cs b/src/EPPlus.Export.ImageRenderer/RenderItems/SvgItem/SvgParagraphItem.cs index 7928b9b8b..bba742ba2 100644 --- a/src/EPPlus.Export.ImageRenderer/RenderItems/SvgItem/SvgParagraphItem.cs +++ b/src/EPPlus.Export.ImageRenderer/RenderItems/SvgItem/SvgParagraphItem.cs @@ -76,19 +76,19 @@ public override void Render(StringBuilder sb) sb.AppendLine("paragraph "); - var bb = new SvgRenderRectItem(Bounds); - //The bb is affected by the Transform so set pos to zero - if(IsFirstParagraph == false) - { - bb.Y = 0; - } - bb.X = 0; + //var bb = new SvgRenderRectItem(Bounds); + ////The bb is affected by the Transform so set pos to zero + //if(IsFirstParagraph == false) + //{ + // bb.Y = 0; + //} + //bb.X = 0; - bb.Width = Bounds.Width; - bb.Height = Bounds.Height; - bb.FillColor = "gold"; - bb.FillOpacity = 0.3; - bb.Render(sb); + //bb.Width = Bounds.Width; + //bb.Height = Bounds.Height; + //bb.FillColor = "gold"; + //bb.FillOpacity = 0.3; + //bb.Render(sb); //Render text run debug boxes as we cannot place the rects after or inside the text element @@ -131,7 +131,7 @@ public override void Render(StringBuilder sb) //} sb.Append(""); sb.AppendLine($"txtBody"); - var bb = new SvgRenderRectItem(Bounds); - bb.X = 0; - bb.Width = Width; - bb.Height = Height; - bb.FillColor = "red"; - bb.FillOpacity = 0.5; - bb.Render(sb); + //var bb = new SvgRenderRectItem(Bounds); + //bb.X = 0; + //bb.Width = Width; + //bb.Height = Height; + //bb.FillColor = "red"; + //bb.FillOpacity = 0.5; + //bb.Render(sb); foreach (var item in Paragraphs) { diff --git a/src/EPPlus.Export.ImageRenderer/RenderItems/SvgRenderPathItem.cs b/src/EPPlus.Export.ImageRenderer/RenderItems/SvgRenderPathItem.cs index b4729c6b6..31f1884dc 100644 --- a/src/EPPlus.Export.ImageRenderer/RenderItems/SvgRenderPathItem.cs +++ b/src/EPPlus.Export.ImageRenderer/RenderItems/SvgRenderPathItem.cs @@ -23,7 +23,8 @@ internal class SvgRenderPathItem : SvgRenderItem { public SvgRenderPathItem(BoundingBox parent) : base(parent) { - + Bounds.Width = parent.Width; + Bounds.Height = parent.Height; } public override RenderItemType Type { get => RenderItemType.Path; } public List Commands { get; set; } = new List(); @@ -31,7 +32,9 @@ public SvgRenderPathItem(BoundingBox parent) : base(parent) public override void Render(StringBuilder sb) { - _drawing.GetSizeInPixels(out int width, out int height); + int width = (int)Bounds.Width; + int height = (int)Bounds.Height; + sb.Append($" Date: Wed, 21 Jan 2026 15:57:55 +0100 Subject: [PATCH 005/151] Fixes issue #2254 --- src/EPPlus/Data/CustomXml/ExcelCustomXml.cs | 19 ++++++++++--------- .../CustomXml/ExcelCustomXmlCollection.cs | 4 ++++ .../PowerQuery/ExcelPowerQuerySettings.cs | 7 ++++--- src/EPPlus/Style/ExcelTextFont.cs | 2 +- src/EPPlusTest/Issues/TableIssues.cs | 11 ++++++++++- 5 files changed, 29 insertions(+), 14 deletions(-) diff --git a/src/EPPlus/Data/CustomXml/ExcelCustomXml.cs b/src/EPPlus/Data/CustomXml/ExcelCustomXml.cs index 49ab17f2e..8dd721fd0 100644 --- a/src/EPPlus/Data/CustomXml/ExcelCustomXml.cs +++ b/src/EPPlus/Data/CustomXml/ExcelCustomXml.cs @@ -107,24 +107,25 @@ internal void Save(ExcelPackage pck) var nsm = CreateNsm(); _xmlHelper = XmlHelperFactory.Create(nsm, PropertiesXml.DocumentElement.SelectSingleNode("ds:schemaRefs", nsm)); } - _xmlHelper.TopNode.InnerXml = ""; - foreach (var schemaRef in SchemasReferences) + if (_xmlHelper != null) { - XmlElement schemaRefNode = (XmlElement)_xmlHelper.CreateNode("ds:schemaRef"); - schemaRefNode.SetAttribute("uri", CreateNsm().LookupNamespace("ds"), schemaRef); - _xmlHelper.TopNode.AppendChild(schemaRefNode); + _xmlHelper.TopNode.InnerXml = ""; + foreach (var schemaRef in SchemasReferences) + { + XmlElement schemaRefNode = (XmlElement)_xmlHelper.CreateNode("ds:schemaRef"); + schemaRefNode.SetAttribute("uri", CreateNsm().LookupNamespace("ds"), schemaRef); + _xmlHelper.TopNode.AppendChild(schemaRefNode); + } } - - var xmlSettings = new XmlWriterSettings() ; + var xmlSettings = new XmlWriterSettings(); var stream = Part.GetStream(FileMode.Create, FileAccess.Write); var xmlWriter = XmlWriter.Create(stream, xmlSettings); CustomXml.Save(xmlWriter); stream = PropertiesPart.GetStream(FileMode.Create, FileAccess.Write); - xmlWriter = XmlWriter.Create(stream, xmlSettings); + xmlWriter = XmlWriter.Create(stream, xmlSettings); PropertiesXml.Save(xmlWriter); - } } } \ No newline at end of file diff --git a/src/EPPlus/Data/CustomXml/ExcelCustomXmlCollection.cs b/src/EPPlus/Data/CustomXml/ExcelCustomXmlCollection.cs index f42ec0f35..4bfdf6f3a 100644 --- a/src/EPPlus/Data/CustomXml/ExcelCustomXmlCollection.cs +++ b/src/EPPlus/Data/CustomXml/ExcelCustomXmlCollection.cs @@ -63,6 +63,10 @@ internal void Add(ExcelCustomXml customXml) { _list.Add(customXml); } + internal bool Contains(ExcelCustomXml customXml) + { + return _list.Contains(customXml); + } internal void Remove(ExcelCustomXml customXml) { _package.ZipPackage.DeletePart(customXml.Part.Uri); diff --git a/src/EPPlus/Data/PowerQuery/ExcelPowerQuerySettings.cs b/src/EPPlus/Data/PowerQuery/ExcelPowerQuerySettings.cs index ecf61cf8e..738c883ef 100644 --- a/src/EPPlus/Data/PowerQuery/ExcelPowerQuerySettings.cs +++ b/src/EPPlus/Data/PowerQuery/ExcelPowerQuerySettings.cs @@ -270,10 +270,11 @@ internal void Save(ExcelCustomXmlCollection customXml) } cx.CustomXml.LoadXml($"{Convert.ToBase64String(retMs.ToArray())}"); - customXml.Add(cx); + if (customXml.Contains(cx) == false) + { + customXml.Add(cx); + } } - - private byte[] GetMetaDataBytes() { var bw = new BinaryWriter(new MemoryStream()); diff --git a/src/EPPlus/Style/ExcelTextFont.cs b/src/EPPlus/Style/ExcelTextFont.cs index 9d55ee9f9..2cea39dfb 100644 --- a/src/EPPlus/Style/ExcelTextFont.cs +++ b/src/EPPlus/Style/ExcelTextFont.cs @@ -310,7 +310,7 @@ public void SetFromFont(string name, float size, bool bold = false, bool italic if (bold) Bold = bold; if (italic) Italic = italic; if (underline) UnderLine = eUnderLineType.Single; - if (strikeout) Strike = eStrikeType.Single; + if (strikeout) Strike = eStrikeType.Single; } } } diff --git a/src/EPPlusTest/Issues/TableIssues.cs b/src/EPPlusTest/Issues/TableIssues.cs index bfa5ee40b..8c3bb3ff2 100644 --- a/src/EPPlusTest/Issues/TableIssues.cs +++ b/src/EPPlusTest/Issues/TableIssues.cs @@ -410,5 +410,14 @@ public void RemoveAndReAddCalculatedColumnFormula() Assert.AreEqual("ROW()-1", sheet1.Cells["A2"].Formula); } } + [TestMethod] + public void s997() + { + using (var package = OpenTemplatePackage("s997.xlsx")) + { + var t = package.Workbook.Worksheets.First().Tables; + SaveAndCleanup(package); + } + } + } } -} From edc55c21004524b77ef5ac8ca0ff32ed624bb3a3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ossian=20Edstr=C3=B6m?= Date: Wed, 21 Jan 2026 16:09:04 +0100 Subject: [PATCH 006/151] Ugly quick-fix shape theme colors --- .../RenderItems/Shared/IRenderItem.cs | 23 ------------------ .../RenderItems/Shared/ITextRunItem.cs | 24 ------------------- .../RenderItems/Shared/InsetTextbox.cs | 22 ----------------- .../RenderItems/Shared/ParagraphContainer.cs | 7 ++++++ .../Shared/ParagraphContainerCollection.cs | 24 ------------------- .../RenderItems/Shared/TextBody.cs | 8 ++----- .../RenderItems/Shared/TextRunItem.cs | 2 +- .../RenderItems/SvgItem/SvgParagraphItem.cs | 1 + .../RenderItems/SvgItem/SvgTextRunItem.cs | 7 +----- .../RenderItems/SvgRenderItem.cs | 1 + 10 files changed, 13 insertions(+), 106 deletions(-) delete mode 100644 src/EPPlus.Export.ImageRenderer/RenderItems/Shared/IRenderItem.cs delete mode 100644 src/EPPlus.Export.ImageRenderer/RenderItems/Shared/ITextRunItem.cs delete mode 100644 src/EPPlus.Export.ImageRenderer/RenderItems/Shared/InsetTextbox.cs delete mode 100644 src/EPPlus.Export.ImageRenderer/RenderItems/Shared/ParagraphContainerCollection.cs diff --git a/src/EPPlus.Export.ImageRenderer/RenderItems/Shared/IRenderItem.cs b/src/EPPlus.Export.ImageRenderer/RenderItems/Shared/IRenderItem.cs deleted file mode 100644 index 71b8a2a21..000000000 --- a/src/EPPlus.Export.ImageRenderer/RenderItems/Shared/IRenderItem.cs +++ /dev/null @@ -1,23 +0,0 @@ -/************************************************************************************************* - Required Notice: Copyright (C) EPPlus Software AB. - This software is licensed under PolyForm Noncommercial License 1.0.0 - and may only be used for noncommercial purposes - https://polyformproject.org/licenses/noncommercial/1.0.0/ - - A commercial license to use this software can be purchased at https://epplussoftware.com - ************************************************************************************************* - Date Author Change - ************************************************************************************************* - 27/11/2025 EPPlus Software AB EPPlus 9 - *************************************************************************************************/ -using OfficeOpenXml.Drawing; -using System.Text; - -namespace EPPlusImageRenderer.RenderItems.Shared -{ - internal interface IRenderItem - { - void Render(StringBuilder sb); - RectBase GetBounds(); - } -} diff --git a/src/EPPlus.Export.ImageRenderer/RenderItems/Shared/ITextRunItem.cs b/src/EPPlus.Export.ImageRenderer/RenderItems/Shared/ITextRunItem.cs deleted file mode 100644 index be9f4ce23..000000000 --- a/src/EPPlus.Export.ImageRenderer/RenderItems/Shared/ITextRunItem.cs +++ /dev/null @@ -1,24 +0,0 @@ -/************************************************************************************************* - Required Notice: Copyright (C) EPPlus Software AB. - This software is licensed under PolyForm Noncommercial License 1.0.0 - and may only be used for noncommercial purposes - https://polyformproject.org/licenses/noncommercial/1.0.0/ - - A commercial license to use this software can be purchased at https://epplussoftware.com - ************************************************************************************************* - Date Author Change - ************************************************************************************************* - 27/11/2025 EPPlus Software AB EPPlus 9 - *************************************************************************************************/ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; - -namespace EPPlusImageRenderer.RenderItems.Shared -{ - internal interface ITextRunItem - { - - } -} diff --git a/src/EPPlus.Export.ImageRenderer/RenderItems/Shared/InsetTextbox.cs b/src/EPPlus.Export.ImageRenderer/RenderItems/Shared/InsetTextbox.cs deleted file mode 100644 index 2a5f58438..000000000 --- a/src/EPPlus.Export.ImageRenderer/RenderItems/Shared/InsetTextbox.cs +++ /dev/null @@ -1,22 +0,0 @@ -using EPPlus.Graphics; -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; - -namespace EPPlus.Export.ImageRenderer.RenderItems.Shared -{ - ///// - ///// Calculated textbox position from a shape - ///// That contains the textbody - ///// - //internal class InsetTextbox: BoundingBox - //{ - // TextBody textBody; - - // //internal InsetTextbox(double l, double t, double width, double height) - // //{ - // // textBody = new TextBody(this); - // //} - //} -} diff --git a/src/EPPlus.Export.ImageRenderer/RenderItems/Shared/ParagraphContainer.cs b/src/EPPlus.Export.ImageRenderer/RenderItems/Shared/ParagraphContainer.cs index 66ad8fc3a..2cb240525 100644 --- a/src/EPPlus.Export.ImageRenderer/RenderItems/Shared/ParagraphContainer.cs +++ b/src/EPPlus.Export.ImageRenderer/RenderItems/Shared/ParagraphContainer.cs @@ -45,6 +45,10 @@ public ParagraphContainer(BoundingBox parent) : base(parent) public ParagraphContainer(ExcelDrawingParagraph p, BoundingBox parent) : base(parent) { + //TODO; Fix this. This is a strange workaround + SetTheme(p._prd.Package.Workbook.ThemeManager.GetOrCreateTheme()); + SetDrawingPropertiesFill(p.DefaultRunProperties.Fill, null); + //---Initialize Bounds/Margins--- Bounds.Transform.Name = "Paragraph"; @@ -128,6 +132,9 @@ internal protected void AddRenderItemTextRun(ExcelParagraphTextRunBase origTxtRu targetTxtRun.GetBounds(out double l, out double t, out double r, out double b); + targetTxtRun.SetTheme(_theme); + targetTxtRun.SetDrawingPropertiesFill(origTxtRun.Fill, null); + _textRunItems.Add(targetTxtRun); } diff --git a/src/EPPlus.Export.ImageRenderer/RenderItems/Shared/ParagraphContainerCollection.cs b/src/EPPlus.Export.ImageRenderer/RenderItems/Shared/ParagraphContainerCollection.cs deleted file mode 100644 index b19747ed0..000000000 --- a/src/EPPlus.Export.ImageRenderer/RenderItems/Shared/ParagraphContainerCollection.cs +++ /dev/null @@ -1,24 +0,0 @@ -using EPPlus.Export.ImageRenderer.RenderItems.SvgItem; -using System; -using System.Collections; -using System.Collections.Generic; -using System.Linq; -using System.Text; - -namespace EPPlus.Export.ImageRenderer.RenderItems.Shared -{ - //internal abstract class ParagraphContainerCollection : ParagraphContainer, IEnumerable - //{ - // internal abstract protected List _list { get; set; } - - // public IEnumerator GetEnumerator() - // { - // return _list.GetEnumerator(); - // } - - // IEnumerator IEnumerable.GetEnumerator() - // { - // return _list.GetEnumerator(); - // } - //} -} diff --git a/src/EPPlus.Export.ImageRenderer/RenderItems/Shared/TextBody.cs b/src/EPPlus.Export.ImageRenderer/RenderItems/Shared/TextBody.cs index 82562a624..d6b13d095 100644 --- a/src/EPPlus.Export.ImageRenderer/RenderItems/Shared/TextBody.cs +++ b/src/EPPlus.Export.ImageRenderer/RenderItems/Shared/TextBody.cs @@ -82,10 +82,6 @@ public void ImportParagraph(ExcelDrawingParagraph item, double startingY) paragraph.Bounds.Top = startingY; Paragraphs.Add(paragraph); - - //TODO; Fix this. This is a strange workaround - paragraph.SetTheme(item._prd.Package.Workbook.ThemeManager.GetOrCreateTheme()); - paragraph.SetDrawingPropertiesFill(item.DefaultRunProperties.Fill, null); } public void ImportTextBody(ExcelTextBody body) @@ -101,8 +97,8 @@ public void ImportTextBody(ExcelTextBody body) //We already apply bounds top via the parent Transform double paragraphStartY = GetAlignmentVertical(); - //TODO; Fix this. This is a strange workaround - SetTheme(body._pictureRelationDocument.Package.Workbook.ThemeManager.GetOrCreateTheme()); + ////TODO; Fix this. This is a strange workaround + //SetTheme(body._pictureRelationDocument.Package.Workbook.ThemeManager.GetOrCreateTheme()); foreach (var paragraph in body.Paragraphs) { diff --git a/src/EPPlus.Export.ImageRenderer/RenderItems/Shared/TextRunItem.cs b/src/EPPlus.Export.ImageRenderer/RenderItems/Shared/TextRunItem.cs index 8d99d6ec3..5dd60da6a 100644 --- a/src/EPPlus.Export.ImageRenderer/RenderItems/Shared/TextRunItem.cs +++ b/src/EPPlus.Export.ImageRenderer/RenderItems/Shared/TextRunItem.cs @@ -15,7 +15,7 @@ using System.Text.RegularExpressions; namespace EPPlus.Export.ImageRenderer.RenderItems.Shared { - internal abstract class TextRunItem : SvgRenderItem + internal abstract class TextRunItem : RenderItem { public override RenderItemType Type => RenderItemType.Text; diff --git a/src/EPPlus.Export.ImageRenderer/RenderItems/SvgItem/SvgParagraphItem.cs b/src/EPPlus.Export.ImageRenderer/RenderItems/SvgItem/SvgParagraphItem.cs index bba742ba2..d97be21bc 100644 --- a/src/EPPlus.Export.ImageRenderer/RenderItems/SvgItem/SvgParagraphItem.cs +++ b/src/EPPlus.Export.ImageRenderer/RenderItems/SvgItem/SvgParagraphItem.cs @@ -131,6 +131,7 @@ public override void Render(StringBuilder sb) //} sb.Append(" Date: Wed, 21 Jan 2026 16:33:33 +0100 Subject: [PATCH 007/151] Added missing file --- .../RenderItems/SvgBaseRenderer.cs | 49 +++++++++++++++++++ 1 file changed, 49 insertions(+) create mode 100644 src/EPPlus.Export.ImageRenderer/RenderItems/SvgBaseRenderer.cs diff --git a/src/EPPlus.Export.ImageRenderer/RenderItems/SvgBaseRenderer.cs b/src/EPPlus.Export.ImageRenderer/RenderItems/SvgBaseRenderer.cs new file mode 100644 index 000000000..645718edd --- /dev/null +++ b/src/EPPlus.Export.ImageRenderer/RenderItems/SvgBaseRenderer.cs @@ -0,0 +1,49 @@ +using EPPlusImageRenderer.RenderItems; +using OfficeOpenXml.Drawing; +using System; +using System.Collections.Generic; +using System.Globalization; +using System.Linq; +using System.Text; + +namespace EPPlus.Export.ImageRenderer.RenderItems +{ + internal static class SvgBaseRenderer + { + public static void BaseRender(StringBuilder sb, RenderItem item) + { + if (string.IsNullOrEmpty(item.FillColor) == false) + { + sb.Append($"fill=\"{item.FillColor}\" "); + if (item.FillOpacity != null && item.FillOpacity != 1) + { + sb.Append($"opacity=\"{item.FillOpacity.Value.ToString(CultureInfo.InvariantCulture)}\" "); + } + } + if (string.IsNullOrEmpty(item.FilterName) == false) + { + sb.Append($"filter=\"{item.FilterName}\" "); + } + + if (item.BorderWidth.HasValue) + { + if (string.IsNullOrEmpty(item.BorderColor) == false) + { + sb.Append($"stroke=\"{item.BorderColor}\" "); + } + var v = item.BorderWidth.Value * ExcelDrawing.EMU_PER_POINT / ExcelDrawing.EMU_PER_PIXEL; + sb.Append($"stroke-width=\"{v.ToString(CultureInfo.InvariantCulture)}\" "); + + if (item.BorderDashArray != null) + { + var BorderDashArrayStr = item.BorderDashArray.Select(x => + x.ToString(CultureInfo.InvariantCulture)).ToArray(); + + sb.Append($"stroke-dasharray=\"" + $"{string.Join(",", BorderDashArrayStr)}\" "); + } + } + + sb.Append($"stroke-miterlimit =\"8\" "); + } + } +} From ffc48b1edbd9117d08268be32268eb8689b132ac Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ossian=20Edstr=C3=B6m?= Date: Wed, 21 Jan 2026 16:57:38 +0100 Subject: [PATCH 008/151] Added theme to base renderItem constructor. Added paragraph default font --- .../TestFontMeasurer.cs | 12 +-------- .../TextRenderTests.cs | 2 +- .../ImageRenderer.cs | 4 ++- .../RenderItems/RenderItem.cs | 3 ++- .../RenderItems/Shared/ParagraphContainer.cs | 9 ++++--- .../RenderItems/Shared/TextBody.cs | 5 ++-- .../RenderItems/Shared/TextRunItem.cs | 2 +- .../RenderItems/SvgGroupItem.cs | 5 ++-- .../RenderItems/SvgItem/SvgParagraphItem.cs | 5 ++-- .../RenderItems/SvgItem/SvgTextBodyItem.cs | 5 ++-- .../RenderItems/SvgRenderEllipseItem.cs | 6 ++--- .../RenderItems/SvgRenderItem.cs | 3 ++- .../RenderItems/SvgRenderLineItem.cs | 5 ++-- .../RenderItems/SvgRenderPathItem.cs | 5 ++-- .../RenderItems/SvgRenderRectItem.cs | 7 ++--- .../Svg/Chart/ChartTypeDrawer.cs | 2 +- .../Svg/LineMarkerHelper.cs | 8 +++--- .../Svg/SvgChartAxis.cs | 8 +++--- .../Svg/SvgChartLegend.cs | 4 +-- .../Svg/SvgChartTitle.cs | 6 ++--- .../Svg/SvgShape.cs | 27 ++++++++++--------- 21 files changed, 68 insertions(+), 65 deletions(-) diff --git a/src/EPPlus.Export.ImageRenderer.Test/TestFontMeasurer.cs b/src/EPPlus.Export.ImageRenderer.Test/TestFontMeasurer.cs index c468900c6..4f4ab8728 100644 --- a/src/EPPlus.Export.ImageRenderer.Test/TestFontMeasurer.cs +++ b/src/EPPlus.Export.ImageRenderer.Test/TestFontMeasurer.cs @@ -1,16 +1,6 @@ -using EPPlusImageRenderer.RenderItems; -using EPPlusImageRenderer.RenderItems.Shared; -using EPPlusImageRenderer.Utils; -using OfficeOpenXml; -using OfficeOpenXml.Interfaces.Drawing.Text; -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; +using OfficeOpenXml.Interfaces.Drawing.Text; using EPPlus.Fonts.OpenType; using EPPlus.Fonts.OpenType.Utils; -using System.Security.Cryptography; using EPPlus.Fonts.OpenType.TrueTypeMeasurer.DataHolders; namespace TestProject1 diff --git a/src/EPPlus.Export.ImageRenderer.Test/TextRenderTests.cs b/src/EPPlus.Export.ImageRenderer.Test/TextRenderTests.cs index f664d8fa4..ba5430334 100644 --- a/src/EPPlus.Export.ImageRenderer.Test/TextRenderTests.cs +++ b/src/EPPlus.Export.ImageRenderer.Test/TextRenderTests.cs @@ -105,7 +105,7 @@ public void VerifyTextRunBounds() parentBB.Width = testWidth; parentBB.Height = testHeight; - SvgTextBodyItem tbItem = new SvgTextBodyItem(parentBB); + SvgTextBodyItem tbItem = new SvgTextBodyItem(parentBB, p.Workbook.ThemeManager.GetOrCreateTheme()); tbItem.ImportTextBody(cube.TextBody); diff --git a/src/EPPlus.Export.ImageRenderer/ImageRenderer.cs b/src/EPPlus.Export.ImageRenderer/ImageRenderer.cs index aca39d2b6..1b7a377be 100644 --- a/src/EPPlus.Export.ImageRenderer/ImageRenderer.cs +++ b/src/EPPlus.Export.ImageRenderer/ImageRenderer.cs @@ -24,6 +24,7 @@ Date Author Change using OfficeOpenXml; using OfficeOpenXml.Drawing; using OfficeOpenXml.Drawing.Chart; +using OfficeOpenXml.Drawing.Theme; using OfficeOpenXml.FormulaParsing.Excel.Functions; using OfficeOpenXml.Utils; using System; @@ -121,7 +122,8 @@ public string RenderTextBody(ExcelTextBody body, double shapeWidth, double shape doc.Render(sb); FontMeasurerTrueType measurer = new FontMeasurerTrueType(11, "Aptos Narrow", FontSubFamily.Regular); - var svgBody = new SvgTextBodyItem(worldBounds); + + var svgBody = new SvgTextBodyItem(worldBounds, null); svgBody.ImportTextBody(body); svgBody.Render(sb); diff --git a/src/EPPlus.Export.ImageRenderer/RenderItems/RenderItem.cs b/src/EPPlus.Export.ImageRenderer/RenderItems/RenderItem.cs index 1df840667..f1ecfcff5 100644 --- a/src/EPPlus.Export.ImageRenderer/RenderItems/RenderItem.cs +++ b/src/EPPlus.Export.ImageRenderer/RenderItems/RenderItem.cs @@ -27,9 +27,10 @@ internal abstract class RenderItem : RenderItemBase { protected ExcelDrawing _drawing; protected ExcelTheme _theme; - internal RenderItem(BoundingBox parent) + internal RenderItem(BoundingBox parent, ExcelTheme theme) { Bounds.Parent = parent; + _theme = theme; //_drawing = drawing; //_theme = drawing._drawings.Worksheet.Workbook.ThemeManager.GetOrCreateTheme(); } diff --git a/src/EPPlus.Export.ImageRenderer/RenderItems/Shared/ParagraphContainer.cs b/src/EPPlus.Export.ImageRenderer/RenderItems/Shared/ParagraphContainer.cs index 2cb240525..6e8d9badc 100644 --- a/src/EPPlus.Export.ImageRenderer/RenderItems/Shared/ParagraphContainer.cs +++ b/src/EPPlus.Export.ImageRenderer/RenderItems/Shared/ParagraphContainer.cs @@ -5,6 +5,7 @@ using EPPlus.Graphics; using EPPlusImageRenderer.RenderItems; using OfficeOpenXml.Drawing; +using OfficeOpenXml.Drawing.Theme; using OfficeOpenXml.Interfaces.Drawing.Text; using OfficeOpenXml.Style; using System; @@ -38,15 +39,15 @@ internal abstract class ParagraphContainer : RenderItem internal protected MeasurementFont _paragraphFont; //internal BoundingBox Bounds = new BoundingBox(); - public ParagraphContainer(BoundingBox parent) : base(parent) + public ParagraphContainer(BoundingBox parent, ExcelTheme theme) : base(parent, theme) { Bounds.Transform.Name = "Paragraph"; + var defaultFont = new MeasurementFont { FontFamily = "Aptos Narrow", Size = 11, Style = MeasurementFontStyles.Regular }; + _paragraphFont = defaultFont; } - public ParagraphContainer(ExcelDrawingParagraph p, BoundingBox parent) : base(parent) + public ParagraphContainer(ExcelDrawingParagraph p, BoundingBox parent) : base(parent, p._prd.Package.Workbook.ThemeManager.GetOrCreateTheme()) { - //TODO; Fix this. This is a strange workaround - SetTheme(p._prd.Package.Workbook.ThemeManager.GetOrCreateTheme()); SetDrawingPropertiesFill(p.DefaultRunProperties.Fill, null); //---Initialize Bounds/Margins--- diff --git a/src/EPPlus.Export.ImageRenderer/RenderItems/Shared/TextBody.cs b/src/EPPlus.Export.ImageRenderer/RenderItems/Shared/TextBody.cs index d6b13d095..572446097 100644 --- a/src/EPPlus.Export.ImageRenderer/RenderItems/Shared/TextBody.cs +++ b/src/EPPlus.Export.ImageRenderer/RenderItems/Shared/TextBody.cs @@ -3,6 +3,7 @@ using EPPlus.Graphics; using EPPlusImageRenderer.RenderItems; using OfficeOpenXml.Drawing; +using OfficeOpenXml.Drawing.Theme; using OfficeOpenXml.Style; using OfficeOpenXml.Utils; using System; @@ -41,7 +42,7 @@ internal abstract class TextBody : RenderItem private FontMeasurerTrueType _measurer = null; - public TextBody(BoundingBox parent) : base(parent) + public TextBody(BoundingBox parent, ExcelTheme theme) : base(parent, theme) { Bounds.Transform.Name = "TxtBody"; @@ -181,7 +182,7 @@ public void SetMeasurer(FontMeasurerTrueType fontMeasurer) /// internal double BottomMargin { get { return _bottomMargin; } set { _bottomMargin = value; Bounds.Height = Bounds.Parent.Height - value; } } - public override RenderItemType Type => throw new NotImplementedException(); + public override RenderItemType Type => RenderItemType.Text; double? _alignmentY = null; diff --git a/src/EPPlus.Export.ImageRenderer/RenderItems/Shared/TextRunItem.cs b/src/EPPlus.Export.ImageRenderer/RenderItems/Shared/TextRunItem.cs index 5dd60da6a..a4b0534ed 100644 --- a/src/EPPlus.Export.ImageRenderer/RenderItems/Shared/TextRunItem.cs +++ b/src/EPPlus.Export.ImageRenderer/RenderItems/Shared/TextRunItem.cs @@ -59,7 +59,7 @@ internal abstract class TextRunItem : RenderItem /// /// /// - internal TextRunItem(ExcelParagraphTextRunBase run, BoundingBox parent = null, string displayText = "") : base(parent) + internal TextRunItem(ExcelParagraphTextRunBase run, BoundingBox parent = null, string displayText = "") : base(parent, run.Paragraph._prd.Package.Workbook.ThemeManager.GetOrCreateTheme()) { Bounds.Transform.Name = "TextRun"; diff --git a/src/EPPlus.Export.ImageRenderer/RenderItems/SvgGroupItem.cs b/src/EPPlus.Export.ImageRenderer/RenderItems/SvgGroupItem.cs index eff03e71c..3b8cb8c4f 100644 --- a/src/EPPlus.Export.ImageRenderer/RenderItems/SvgGroupItem.cs +++ b/src/EPPlus.Export.ImageRenderer/RenderItems/SvgGroupItem.cs @@ -12,6 +12,7 @@ Date Author Change *************************************************************************************************/ using EPPlus.Graphics; using EPPlusImageRenderer.Svg; +using OfficeOpenXml.Drawing.Theme; using System.Drawing; using System.Globalization; using System.Text; @@ -24,11 +25,11 @@ internal class SvgGroupItem : SvgRenderItem public string GroupTransform = ""; - internal SvgGroupItem(BoundingBox parent) : base(parent) + internal SvgGroupItem(BoundingBox parent, ExcelTheme theme) : base(parent, theme) { } - internal SvgGroupItem(BoundingBox parent, double rotation) : base(parent) + internal SvgGroupItem(BoundingBox parent, double rotation, ExcelTheme theme) : base(parent, theme) { if(rotation!=0) { diff --git a/src/EPPlus.Export.ImageRenderer/RenderItems/SvgItem/SvgParagraphItem.cs b/src/EPPlus.Export.ImageRenderer/RenderItems/SvgItem/SvgParagraphItem.cs index d97be21bc..9321b5fb2 100644 --- a/src/EPPlus.Export.ImageRenderer/RenderItems/SvgItem/SvgParagraphItem.cs +++ b/src/EPPlus.Export.ImageRenderer/RenderItems/SvgItem/SvgParagraphItem.cs @@ -3,6 +3,7 @@ using EPPlus.Graphics; using EPPlusImageRenderer.RenderItems; using OfficeOpenXml.Drawing; +using OfficeOpenXml.Drawing.Theme; using System.Globalization; using System.Text; @@ -12,7 +13,7 @@ internal class SvgParagraphItem : ParagraphContainer { public override RenderItemType Type => RenderItemType.Paragraph; - public SvgParagraphItem(BoundingBox parent) : base(parent) + public SvgParagraphItem(BoundingBox parent, ExcelTheme theme) : base(parent, theme) { } @@ -138,7 +139,7 @@ public override void Render(StringBuilder sb) $"font-family=\"{_paragraphFont.FontFamily},{_paragraphFont.FontFamily}_MSFontService,sans-serif\" " + $"font-size=\"{fontSize}px\" >"); - if (_textRunItems.Count > 0) + if (_textRunItems != null && _textRunItems.Count > 0) { foreach (var textRun in _textRunItems) { diff --git a/src/EPPlus.Export.ImageRenderer/RenderItems/SvgItem/SvgTextBodyItem.cs b/src/EPPlus.Export.ImageRenderer/RenderItems/SvgItem/SvgTextBodyItem.cs index d07c51ca3..785fb439a 100644 --- a/src/EPPlus.Export.ImageRenderer/RenderItems/SvgItem/SvgTextBodyItem.cs +++ b/src/EPPlus.Export.ImageRenderer/RenderItems/SvgItem/SvgTextBodyItem.cs @@ -3,6 +3,7 @@ using EPPlusImageRenderer.RenderItems; using EPPlusImageRenderer.Svg; using OfficeOpenXml.Drawing; +using OfficeOpenXml.Drawing.Theme; using System; using System.Collections.Generic; using System.Text; @@ -11,7 +12,7 @@ namespace EPPlus.Export.ImageRenderer.RenderItems.SvgItem { internal class SvgTextBodyItem : TextBody { - public SvgTextBodyItem(BoundingBox parent) : base(parent) + public SvgTextBodyItem(BoundingBox parent, ExcelTheme theme) : base(parent, theme) { } @@ -41,7 +42,7 @@ public override void Render(StringBuilder sb) internal override ParagraphContainer CreateParagraph(BoundingBox parent) { - return new SvgParagraphItem(parent); + return new SvgParagraphItem(parent, _theme); } internal override ParagraphContainer CreateParagraph(ExcelDrawingParagraph paragraph, BoundingBox parent) diff --git a/src/EPPlus.Export.ImageRenderer/RenderItems/SvgRenderEllipseItem.cs b/src/EPPlus.Export.ImageRenderer/RenderItems/SvgRenderEllipseItem.cs index 6d7ee9e7c..bec2aaf81 100644 --- a/src/EPPlus.Export.ImageRenderer/RenderItems/SvgRenderEllipseItem.cs +++ b/src/EPPlus.Export.ImageRenderer/RenderItems/SvgRenderEllipseItem.cs @@ -13,13 +13,14 @@ Date Author Change using EPPlus.Export.ImageRenderer.Utils; using EPPlusImageRenderer.Svg; using OfficeOpenXml.Drawing; +using OfficeOpenXml.Drawing.Theme; using System.Globalization; using System.Text; namespace EPPlusImageRenderer.RenderItems { internal class SvgRenderEllipseItem : SvgRenderItem { - public SvgRenderEllipseItem(ExcelDrawing drawing) : base(drawing.GetBoundingBox()) + public SvgRenderEllipseItem(ExcelDrawing drawing, ExcelTheme theme) : base(drawing.GetBoundingBox(), theme) { } @@ -45,8 +46,7 @@ public override void Render(StringBuilder sb) internal override SvgRenderItem Clone(SvgShape svgDocument) { - var clone = new SvgRenderEllipseItem(_drawing); - clone._theme = _theme; + var clone = new SvgRenderEllipseItem(_drawing, _theme); CloneBase(clone); //if (AdjustmentPoints != null && AdjustmentPoints.Commands == null && AdjustmentPoints.AdjustmentType == AdjustmentType.AdjustToWidthHeight) //{ diff --git a/src/EPPlus.Export.ImageRenderer/RenderItems/SvgRenderItem.cs b/src/EPPlus.Export.ImageRenderer/RenderItems/SvgRenderItem.cs index 16a7e04e4..8e4820d11 100644 --- a/src/EPPlus.Export.ImageRenderer/RenderItems/SvgRenderItem.cs +++ b/src/EPPlus.Export.ImageRenderer/RenderItems/SvgRenderItem.cs @@ -14,6 +14,7 @@ Date Author Change using EPPlus.Graphics; using EPPlusImageRenderer.Svg; using OfficeOpenXml.Drawing; +using OfficeOpenXml.Drawing.Theme; using System.Globalization; using System.Linq; using System.Text; @@ -27,7 +28,7 @@ internal enum SvgFillType } internal abstract class SvgRenderItem : RenderItem { - internal SvgRenderItem(BoundingBox parent) : base(parent) + internal SvgRenderItem(BoundingBox parent, ExcelTheme theme) : base(parent, theme) { } public override void Render(StringBuilder sb) diff --git a/src/EPPlus.Export.ImageRenderer/RenderItems/SvgRenderLineItem.cs b/src/EPPlus.Export.ImageRenderer/RenderItems/SvgRenderLineItem.cs index 76114d705..1d82aa3a6 100644 --- a/src/EPPlus.Export.ImageRenderer/RenderItems/SvgRenderLineItem.cs +++ b/src/EPPlus.Export.ImageRenderer/RenderItems/SvgRenderLineItem.cs @@ -13,13 +13,14 @@ Date Author Change using EPPlus.Export.ImageRenderer.Utils; using EPPlusImageRenderer.Svg; using OfficeOpenXml.Drawing; +using OfficeOpenXml.Drawing.Theme; using System.Globalization; using System.Text; namespace EPPlusImageRenderer.RenderItems { internal class SvgRenderLineItem : SvgRenderItem { - public SvgRenderLineItem(ExcelDrawing drawing) : base(drawing.GetBoundingBox()) + public SvgRenderLineItem(ExcelDrawing drawing, ExcelTheme theme) : base(drawing.GetBoundingBox(), theme) { } @@ -46,7 +47,7 @@ public override void Render(StringBuilder sb) internal override SvgRenderItem Clone(SvgShape svgDocument) { - var clone = new SvgRenderLineItem(_drawing); + var clone = new SvgRenderLineItem(_drawing, _theme); clone._theme = _theme; CloneBase(clone); //if (adjustmentpoints != null && adjustmentpoints.commands == null && adjustmentpoints.adjustmenttype == adjustmenttype.adjusttowidthheight) diff --git a/src/EPPlus.Export.ImageRenderer/RenderItems/SvgRenderPathItem.cs b/src/EPPlus.Export.ImageRenderer/RenderItems/SvgRenderPathItem.cs index 31f1884dc..692b6b055 100644 --- a/src/EPPlus.Export.ImageRenderer/RenderItems/SvgRenderPathItem.cs +++ b/src/EPPlus.Export.ImageRenderer/RenderItems/SvgRenderPathItem.cs @@ -14,6 +14,7 @@ Date Author Change using EPPlus.Graphics; using EPPlusImageRenderer.Svg; using OfficeOpenXml.Drawing; +using OfficeOpenXml.Drawing.Theme; using System.Collections.Generic; using System.Text; @@ -21,7 +22,7 @@ namespace EPPlusImageRenderer.RenderItems { internal class SvgRenderPathItem : SvgRenderItem { - public SvgRenderPathItem(BoundingBox parent) : base(parent) + public SvgRenderPathItem(BoundingBox parent, ExcelTheme theme) : base(parent, theme) { Bounds.Width = parent.Width; Bounds.Height = parent.Height; @@ -47,7 +48,7 @@ public override void Render(StringBuilder sb) internal override SvgRenderItem Clone(SvgShape svgDocument) { - var clone = new SvgRenderPathItem(_drawing.GetBoundingBox()); + var clone = new SvgRenderPathItem(_drawing.GetBoundingBox(), _theme); clone._theme = _theme; CloneBase(clone); clone.Commands = CloneCommands(Commands); diff --git a/src/EPPlus.Export.ImageRenderer/RenderItems/SvgRenderRectItem.cs b/src/EPPlus.Export.ImageRenderer/RenderItems/SvgRenderRectItem.cs index f41925968..e409def55 100644 --- a/src/EPPlus.Export.ImageRenderer/RenderItems/SvgRenderRectItem.cs +++ b/src/EPPlus.Export.ImageRenderer/RenderItems/SvgRenderRectItem.cs @@ -18,16 +18,17 @@ Date Author Change using System.Text; using EPPlus.Graphics; using EPPlus.Export.ImageRenderer.Utils; +using OfficeOpenXml.Drawing.Theme; namespace EPPlusImageRenderer.RenderItems { internal class SvgRenderRectItem : SvgRenderItem { - public SvgRenderRectItem(BoundingBox parent) : base(parent) + public SvgRenderRectItem(BoundingBox parent, ExcelTheme theme) : base(parent, theme) { } - public SvgRenderRectItem(ExcelDrawing drawing) : base(drawing.GetBoundingBox()) + public SvgRenderRectItem(ExcelDrawing drawing) : base(drawing.GetBoundingBox(), drawing._drawings._package.Workbook.ThemeManager.GetOrCreateTheme()) { } @@ -45,7 +46,7 @@ public SvgRenderRectItem(ExcelDrawing drawing) : base(drawing.GetBoundingBox()) public override void Render(StringBuilder sb) { - var groupItem = new SvgGroupItem(Bounds); + var groupItem = new SvgGroupItem(Bounds, _theme); groupItem.Render(sb); RenderRect(sb); diff --git a/src/EPPlus.Export.ImageRenderer/Svg/Chart/ChartTypeDrawer.cs b/src/EPPlus.Export.ImageRenderer/Svg/Chart/ChartTypeDrawer.cs index 722c02dca..e4a3f0059 100644 --- a/src/EPPlus.Export.ImageRenderer/Svg/Chart/ChartTypeDrawer.cs +++ b/src/EPPlus.Export.ImageRenderer/Svg/Chart/ChartTypeDrawer.cs @@ -103,7 +103,7 @@ private void AddLine(ExcelChart chartType, ExcelLineChartSerie serie, List(); var markerItems = new List(); diff --git a/src/EPPlus.Export.ImageRenderer/Svg/LineMarkerHelper.cs b/src/EPPlus.Export.ImageRenderer/Svg/LineMarkerHelper.cs index ad9923ea0..417feec2b 100644 --- a/src/EPPlus.Export.ImageRenderer/Svg/LineMarkerHelper.cs +++ b/src/EPPlus.Export.ImageRenderer/Svg/LineMarkerHelper.cs @@ -27,7 +27,7 @@ internal static RenderItem GetMarkerItem(SvgChart sc, ExcelLineChartSerie ls, fl switch (m.Style) { case eMarkerStyle.Circle: - item = new SvgRenderEllipseItem(sc.Drawing) + item = new SvgRenderEllipseItem(sc.Drawing, sc.Drawing._drawings._package.Workbook.ThemeManager.GetOrCreateTheme()) { Rx = halfSize, Ry = halfSize, @@ -36,7 +36,7 @@ internal static RenderItem GetMarkerItem(SvgChart sc, ExcelLineChartSerie ls, fl }; break; case eMarkerStyle.Triangle: - item = new SvgRenderPathItem(sc.Drawing.GetBoundingBox()) + item = new SvgRenderPathItem(sc.Drawing.GetBoundingBox(), sc.Drawing._drawings._package.Workbook.ThemeManager.GetOrCreateTheme()) { Commands = new List() }; @@ -45,7 +45,7 @@ internal static RenderItem GetMarkerItem(SvgChart sc, ExcelLineChartSerie ls, fl ((SvgRenderPathItem)item).Commands.Add(new PathCommands(PathCommandType.End, item)); break; case eMarkerStyle.Diamond: - item = new SvgRenderPathItem(sc.Drawing.GetBoundingBox()) + item = new SvgRenderPathItem(sc.Drawing.GetBoundingBox(), sc.Drawing._drawings._package.Workbook.ThemeManager.GetOrCreateTheme()) { Commands = new List() }; @@ -96,7 +96,7 @@ internal static RenderItem GetMarkerItem(SvgChart sc, ExcelLineChartSerie ls, fl case eMarkerStyle.Plus: case eMarkerStyle.Star: case eMarkerStyle.X: - var pathItem = new SvgRenderPathItem(sc.Drawing.GetBoundingBox()) + var pathItem = new SvgRenderPathItem(sc.Drawing.GetBoundingBox(), sc.Drawing._drawings._package.Workbook.ThemeManager.GetOrCreateTheme()) { Commands = new List() }; diff --git a/src/EPPlus.Export.ImageRenderer/Svg/SvgChartAxis.cs b/src/EPPlus.Export.ImageRenderer/Svg/SvgChartAxis.cs index 36ce36452..6bcd17971 100644 --- a/src/EPPlus.Export.ImageRenderer/Svg/SvgChartAxis.cs +++ b/src/EPPlus.Export.ImageRenderer/Svg/SvgChartAxis.cs @@ -104,7 +104,7 @@ internal SvgChartAxis(SvgChart sc, ExcelChartAxisStandard ax) : base(sc.Chart) Rectangle.FillColor = "none"; - Line = new SvgRenderLineItem(sc.Chart); + Line = new SvgRenderLineItem(sc.Chart, sc.Drawing._drawings._package.Workbook.ThemeManager.GetOrCreateTheme()); Line.SetDrawingPropertiesBorder(ax.Border, sc.Chart.StyleManager.Style.Title.BorderReference.Color, ax.Border.Fill.Style != eFillStyle.NoFill, 0.75); } } @@ -295,7 +295,7 @@ private List GetAxisValueTextBoxes() bounds.Top = y; bounds.Width = m.Width.PointToPixel(); bounds.Height = m.Height.PointToPixel(); - var tb = new SvgTextBodyItem(bounds); + var tb = new SvgTextBodyItem(bounds, Chart.WorkSheet._package.Workbook.ThemeManager.GetOrCreateTheme()); tb.ImportTextBody(Axis.TextBody); tb.Paragraphs[0].AddText(v, (FontMeasurerTrueType)tm); tb.SetDrawingPropertiesFill(Axis.Fill, axisStyle.FillReference.Color); @@ -415,7 +415,7 @@ private List AddTickmarks(double units, double parentUnit, fl default: throw new InvalidOperationException("Invalid axis position"); } - var tm = new SvgRenderLineItem(Chart); + var tm = new SvgRenderLineItem(Chart, Chart._drawings._package.Workbook.ThemeManager.GetOrCreateTheme()); tm.X1 = x1; tm.Y1 = y1; tm.X2 = x2; @@ -468,7 +468,7 @@ private List AddGridlines(double units, double parentUnit, Ex throw new InvalidOperationException("Invalid axis position"); } - var tm = new SvgRenderLineItem(Chart); + var tm = new SvgRenderLineItem(Chart, Chart._drawings._package.Workbook.ThemeManager.GetOrCreateTheme()); tm.X1 = x1; tm.Y1 = y1; tm.X2 = x2; diff --git a/src/EPPlus.Export.ImageRenderer/Svg/SvgChartLegend.cs b/src/EPPlus.Export.ImageRenderer/Svg/SvgChartLegend.cs index 34f298688..d766a7fb4 100644 --- a/src/EPPlus.Export.ImageRenderer/Svg/SvgChartLegend.cs +++ b/src/EPPlus.Export.ImageRenderer/Svg/SvgChartLegend.cs @@ -194,7 +194,7 @@ internal void SetLegend(SvgChart sc) var tm = _seriesHeadersMeasure[index]; var si = GetSeriesIcon(sc, ls, index, tm, pSls); sls.SeriesIcon = si; - sls.Textbox = new SvgTextBodyItem(Rectangle.Bounds); + sls.Textbox = new SvgTextBodyItem(Rectangle.Bounds, Chart.WorkSheet._package.Workbook.ThemeManager.GetOrCreateTheme()); var entry = Chart.Legend.Entries.FirstOrDefault(x => x.Index == index); if (entry == null || entry.Font.IsEmpty) { @@ -233,7 +233,7 @@ internal void SetLegend(SvgChart sc) private SvgRenderLineItem GetSeriesIcon(SvgChart sc, ExcelLineChartSerie ls, int index, TextMeasurement tm, SvgLegendSerie pSls) { - var item = new SvgRenderLineItem(sc.Chart); + var item = new SvgRenderLineItem(sc.Chart, sc.Chart._drawings._package.Workbook.ThemeManager.GetOrCreateTheme()); item.SetDrawingPropertiesFill(ls.Fill, sc.Chart.StyleManager.Style.SeriesLine.FillReference.Color); item.SetDrawingPropertiesBorder(ls.Border, sc.Chart.StyleManager.Style.SeriesLine.BorderReference.Color, ls.Border.Fill.Style!=eFillStyle.NoFill, 0.75); diff --git a/src/EPPlus.Export.ImageRenderer/Svg/SvgChartTitle.cs b/src/EPPlus.Export.ImageRenderer/Svg/SvgChartTitle.cs index c5f195b81..de3b856ea 100644 --- a/src/EPPlus.Export.ImageRenderer/Svg/SvgChartTitle.cs +++ b/src/EPPlus.Export.ImageRenderer/Svg/SvgChartTitle.cs @@ -169,7 +169,7 @@ private static string GetDefaultChartTitleText(SvgChart sc, ExcelChartTitleStand internal void InitTextBox() { //TextBox = new TextBody(Chart, Rectangle.Left, Rectangle.Top , Rectangle.Width, Rectangle.Height); - TextBox = new SvgTextBodyItem(Rectangle.Bounds); + TextBox = new SvgTextBodyItem(Rectangle.Bounds, Chart.WorkSheet._package.Workbook.ThemeManager.GetOrCreateTheme()); TextBox.LeftMargin = LeftMargin; TextBox.RightMargin = RightMargin; TextBox.TopMargin = TopMargin; @@ -201,11 +201,11 @@ internal override void AppendRenderItems(List renderItems) SvgGroupItem groupItem; if (TextBox.Bounds.Transform.Rotation == 0) { - groupItem = new SvgGroupItem(Rectangle.Bounds); + groupItem = new SvgGroupItem(Rectangle.Bounds, Chart.WorkSheet.Workbook.ThemeManager.GetOrCreateTheme()); } else { - groupItem = new SvgGroupItem(Rectangle.Bounds, TextBox.Bounds.Transform.Rotation); + groupItem = new SvgGroupItem(Rectangle.Bounds, TextBox.Bounds.Transform.Rotation, Chart.WorkSheet.Workbook.ThemeManager.GetOrCreateTheme()); } renderItems.Add(groupItem); diff --git a/src/EPPlus.Export.ImageRenderer/Svg/SvgShape.cs b/src/EPPlus.Export.ImageRenderer/Svg/SvgShape.cs index 52fbdaa96..18f842ebc 100644 --- a/src/EPPlus.Export.ImageRenderer/Svg/SvgShape.cs +++ b/src/EPPlus.Export.ImageRenderer/Svg/SvgShape.cs @@ -10,6 +10,12 @@ Date Author Change ************************************************************************************************* 27/11/2025 EPPlus Software AB EPPlus 9 *************************************************************************************************/ +using EPPlus.Export.ImageRenderer.RenderItems.Shared; +using EPPlus.Export.ImageRenderer.RenderItems.SvgItem; +using EPPlus.Export.ImageRenderer.Text; +using EPPlus.Export.ImageRenderer.Utils; +using EPPlus.Fonts.OpenType; +using EPPlus.Graphics; using EPPlusImageRenderer.RenderItems; using EPPlusImageRenderer.ShapeDefinitions; using EPPlusImageRenderer.Text; @@ -18,18 +24,13 @@ Date Author Change using OfficeOpenXml.Drawing; using OfficeOpenXml.Drawing.Theme; using OfficeOpenXml.Style; +using System; +using System.Collections.Generic; using System.Globalization; +using System.Linq; +using System.Runtime.InteropServices; using System.Text; using TypeConv = OfficeOpenXml.Utils.TypeConversion; -using EPPlus.Fonts.OpenType; -using System.Collections.Generic; -using System; -using System.Linq; -using EPPlus.Export.ImageRenderer.Text; -using EPPlus.Export.ImageRenderer.RenderItems.Shared; -using EPPlus.Graphics; -using EPPlus.Export.ImageRenderer.RenderItems.SvgItem; -using EPPlus.Export.ImageRenderer.Utils; namespace EPPlusImageRenderer.Svg { @@ -54,7 +55,7 @@ public SvgShape(ExcelShape shape) : base(shape) var shapeDef = PresetShapeDefinitions.ShapeDefinitions[style].Clone(); shapeDef.Calculate(shape); - RenderItems.Add(new SvgGroupItem(shape.GetBoundingBox(), shape.Rotation)); + RenderItems.Add(new SvgGroupItem(shape.GetBoundingBox(), shape.Rotation, shape._drawings._package.Workbook.ThemeManager.GetOrCreateTheme())); //Draw Filled path's foreach (var path in shapeDef.ShapePaths) @@ -107,7 +108,7 @@ public SvgShape(ExcelShape shape) : base(shape) protected void AddFromPaths(DrawingPath path, bool drawFill = true, bool drawBorder = true) { - var pi = new SvgRenderPathItem(_shape.GetBoundingBox()); + var pi = new SvgRenderPathItem(_shape.GetBoundingBox(), _theme); var coordinates = new List(); PathCommands cmd = null; PathsBase pCmd = null; @@ -240,14 +241,14 @@ SvgTextBodyItem CreateTextBodyItem() if (insetTextBox == null) { GetShapeInnerBound(out double x, out double y, out double width, out double height); - insetTextBox = new SvgRenderRectItem(Bounds); + insetTextBox = new SvgRenderRectItem(Bounds, _theme); insetTextBox.Bounds.Left = x; insetTextBox.Bounds.Top = y; insetTextBox.Width = width; insetTextBox.Height = height; insetTextBox.Bounds.Parent = textBody.Bounds; //TODO:Check that textBody is correct. } - var txtBodyItem = new SvgTextBodyItem(insetTextBox.Bounds); + var txtBodyItem = new SvgTextBodyItem(insetTextBox.Bounds, _theme); return txtBodyItem; } From a1c2a9721ae6a3e548b6d81d0c780ce95fd4da17 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jan=20K=C3=A4llman?= Date: Thu, 22 Jan 2026 11:12:12 +0100 Subject: [PATCH 009/151] WIP:Fixes for text handling in new implementation --- .../SvgPathTests.cs | 20 ++-- .../TextRenderTests.cs | 8 +- .../ImageRenderer.cs | 108 ------------------ .../RenderItems/Shared/ParagraphContainer.cs | 98 +++------------- .../RenderItems/Shared/TextBody.cs | 11 +- .../RenderItems/Shared/TextRunItem.cs | 50 +++++++- .../RenderItems/SvgItem/SvgParagraphItem.cs | 13 ++- .../RenderItems/SvgItem/SvgTextRunItem.cs | 13 ++- .../Svg/SvgChartAxis.cs | 2 +- 9 files changed, 98 insertions(+), 225 deletions(-) diff --git a/src/EPPlus.Export.ImageRenderer.Test/SvgPathTests.cs b/src/EPPlus.Export.ImageRenderer.Test/SvgPathTests.cs index 48f83276f..0d48a1e94 100644 --- a/src/EPPlus.Export.ImageRenderer.Test/SvgPathTests.cs +++ b/src/EPPlus.Export.ImageRenderer.Test/SvgPathTests.cs @@ -511,16 +511,16 @@ public void GenerateSvgForCharts() { var ws = p.Workbook.Worksheets[0]; var renderer = new EPPlusImageRenderer.ImageRenderer(); - //var ix = 5; - //var c = ws.Drawings[ix]; - //var svg = renderer.RenderDrawingToSvg(c); - //SaveTextFileToWorkbook($"svg\\ChartForSvg{ix++}.svg", svg); - var ix = 1; - foreach (ExcelChart c in ws.Drawings) - { - var svg = renderer.RenderDrawingToSvg(c); - SaveTextFileToWorkbook($"svg\\ChartForSvg{ix++}.svg", svg); - } + var ix = 0; + var c = ws.Drawings[ix]; + var svg = renderer.RenderDrawingToSvg(c); + SaveTextFileToWorkbook($"svg\\ChartForSvg{ix++}.svg", svg); + //var ix = 1; + //foreach (ExcelChart c in ws.Drawings) + //{ + // var svg = renderer.RenderDrawingToSvg(c); + // SaveTextFileToWorkbook($"svg\\ChartForSvg{ix++}.svg", svg); + //} } } [TestMethod] diff --git a/src/EPPlus.Export.ImageRenderer.Test/TextRenderTests.cs b/src/EPPlus.Export.ImageRenderer.Test/TextRenderTests.cs index ba5430334..66893d999 100644 --- a/src/EPPlus.Export.ImageRenderer.Test/TextRenderTests.cs +++ b/src/EPPlus.Export.ImageRenderer.Test/TextRenderTests.cs @@ -109,16 +109,16 @@ public void VerifyTextRunBounds() tbItem.ImportTextBody(cube.TextBody); - var txtRun1Bounds = tbItem.Paragraphs[0]._textRunItems[0].Bounds; + var txtRun1Bounds = tbItem.Paragraphs[0].Runs[0].Bounds; Assert.AreEqual(43.835286458333336d, txtRun1Bounds.Width); - var widthLine2 = tbItem.Paragraphs[0]._textRunItems[0].PerLineWidth[1]; - var topYLine2 = tbItem.Paragraphs[0]._textRunItems[0].YIncreasePerLine[1]; + var widthLine2 = tbItem.Paragraphs[0].Runs[0].PerLineWidth[1]; + var topYLine2 = tbItem.Paragraphs[0].Runs[0].YIncreasePerLine[1]; Assert.AreEqual(7.147135416666667d, widthLine2); Assert.AreEqual(17.903645833333336d, topYLine2); - var txtRuns2 = tbItem.Paragraphs[1]._textRunItems; + var txtRuns2 = tbItem.Paragraphs[1].Runs; Assert.AreEqual(53.20963541666667d, txtRuns2[0].Bounds.Width); var currentLineWidth = txtRuns2[0].Bounds.Width; diff --git a/src/EPPlus.Export.ImageRenderer/ImageRenderer.cs b/src/EPPlus.Export.ImageRenderer/ImageRenderer.cs index 1b7a377be..3ab656fc7 100644 --- a/src/EPPlus.Export.ImageRenderer/ImageRenderer.cs +++ b/src/EPPlus.Export.ImageRenderer/ImageRenderer.cs @@ -42,14 +42,12 @@ public string RenderDrawingToSvg(ExcelDrawing drawing) if (drawing is ExcelShape shape) { var svg = new SvgShape(shape); - //svg.Bounds = new DrawingSize(width, height); svg.Render(sb); return sb.ToString(); } else if(drawing is ExcelChart chart) { var svg = new SvgChart(chart); - //svg.Bounds = new DrawingSize(width, height); svg.Render(sb); return sb.ToString(); } @@ -149,112 +147,6 @@ internal string RenderSvgElement(SvgElement element) } } - internal SvgElement GenerateSvgTextBody(SvgTextBodyItem body, int width, int height) - { - var fullString = body.GetContent(); - - var doc = new SvgEpplusDocument(width, height); - - //Represents world bounds/svg node - var bg = new SvgElement("rect"); - bg.AddAttribute("width", "100%"); - bg.AddAttribute("height", "100%"); - bg.AddAttribute("fill", "red"); - bg.AddAttribute("opacity", "0.1"); - - body.AllowOverflow = false; - - var svgDefs = GetDefinitions(body.Bounds, out string nameId, body.AllowOverflow); - - var fontSizePx = 16d; - - doc.AddChildElement(svgDefs); - doc.AddChildElement(bg); - - var shapeRectBB = body.Bounds.Parent; - - var shapeRoot = new SvgElement("g"); - shapeRoot.AddAttribute("transform", $"translate({shapeRectBB.GlobalX},{shapeRectBB.GlobalY})"); - - doc.AddChildElement(shapeRoot); - - var shapeTitle = new SvgElement("title"); - shapeTitle.Content = "Shape Group"; - shapeRoot.AddChildElement(shapeTitle); - - var shapeVisual = new SvgElement("rect"); - shapeVisual.AddAttribute("width", $"{shapeRectBB.Width}px"); - shapeVisual.AddAttribute("height", $"{shapeRectBB.Height}px"); - shapeVisual.AddAttribute("fill", "yellow"); - shapeVisual.AddAttribute("opacity", "0.2"); - - shapeRoot.AddChildElement(shapeVisual); - - var textBodyGroup = new SvgElement("g"); - textBodyGroup.AddAttribute("transform", $"translate({body.Bounds.X},{body.Bounds.Y})"); - - shapeRoot.AddChildElement(textBodyGroup); - - var txtBodyTitle = new SvgElement("title"); - txtBodyTitle.Content = "txtBody"; - textBodyGroup.AddChildElement(txtBodyTitle); - - - var txBodyVisual = new SvgElement("use"); - txBodyVisual.AddAttribute("href", "#defaultRect"); - txBodyVisual.AddAttribute("fill", "green"); - txBodyVisual.AddAttribute("opacity", "0.5"); - - textBodyGroup.AddChildElement(txBodyVisual); - - int paragraphCount = 1; - - foreach(var paragraph in body.Paragraphs) - { - var paragraphGroup = new SvgElement("g"); - paragraphGroup.AddAttribute("transform", $"translate({paragraph.Bounds.X},{paragraph.Bounds.Y})"); - - textBodyGroup.AddChildElement(paragraphGroup); - - var paragraphTitle = new SvgElement("title"); - paragraphTitle.Content = "Paragraph " + paragraphCount.ToString(); - paragraphGroup.AddChildElement(paragraphTitle); - - var paragraphElement = new SvgElement("text"); - paragraphElement.AddAttribute("y", fontSizePx); - paragraphElement.AddAttribute("font-size", $"{fontSizePx}px"); - paragraphElement.AddAttribute("clip-path", $"url(#{nameId})"); - - paragraphGroup.AddChildElement(paragraphElement); - - foreach (var run in paragraph.Runs) - { - var bbVisual = new SvgElement("rect"); - bbVisual.AddAttribute("x", run.X); - bbVisual.AddAttribute("y", run.Y); - bbVisual.AddAttribute("width", run.Width); - bbVisual.AddAttribute("height", run.Height); - bbVisual.AddAttribute("fill", "blue"); - bbVisual.AddAttribute("opacity", "0.5"); - - paragraphGroup.AddChildElement(bbVisual); - - var runElement = new SvgElement("tspan"); - runElement.AddAttribute("x", run.X); - runElement.AddAttribute("y", run.Y + fontSizePx); - runElement.AddAttribute("font-size", $"{fontSizePx}px"); - - runElement.Content = run.GetContent(); - paragraphElement.AddChildElement(runElement); - } - - paragraphCount++; - } - - doc.AddAttributes(); - - return doc; - } internal SvgElement GetDefinitions(BoundingBox boundingBox, out string nameId, bool AllowOverflow = false) { diff --git a/src/EPPlus.Export.ImageRenderer/RenderItems/Shared/ParagraphContainer.cs b/src/EPPlus.Export.ImageRenderer/RenderItems/Shared/ParagraphContainer.cs index 6e8d9badc..b3df42b7f 100644 --- a/src/EPPlus.Export.ImageRenderer/RenderItems/Shared/ParagraphContainer.cs +++ b/src/EPPlus.Export.ImageRenderer/RenderItems/Shared/ParagraphContainer.cs @@ -32,9 +32,8 @@ internal abstract class ParagraphContainer : RenderItem List _paragraphLines = new List(); protected List _textRunDisplayText = new List(); - internal List Runs = new List(); TextFragmentCollection _textFragments; - internal List _textRunItems; + internal List Runs { get; set; } = new List(); internal protected MeasurementFont _paragraphFont; //internal BoundingBox Bounds = new BoundingBox(); @@ -113,7 +112,7 @@ internal protected void AddRenderItemTextRun(ExcelParagraphTextRunBase origTxtRu var targetTxtRun = CreateTextRun(origTxtRun, Bounds, displayText); targetTxtRun.LineSpacingPerNewLine = ParagraphLineSpacing; - if (_textRunItems.Count == 0 && IsFirstParagraph == true) + if (Runs.Count == 0 && IsFirstParagraph == true) { targetTxtRun.BaseLineSpacing = _lineSpacingAscendantOnly; } @@ -136,31 +135,18 @@ internal protected void AddRenderItemTextRun(ExcelParagraphTextRunBase origTxtRu targetTxtRun.SetTheme(_theme); targetTxtRun.SetDrawingPropertiesFill(origTxtRun.Fill, null); - _textRunItems.Add(targetTxtRun); + Runs.Add(targetTxtRun); } - public void AddText(string text, FontMeasurerTrueType measurer) + public void AddText(string text, ExcelTextFont font) { - var container = new FontWrapContainer(measurer); - container.Parent = Bounds; + var measurer = new FontMeasurerTrueType(); + var displayText = measurer.MeasureAndWrapText(text, font.GetMeasureFont(), Bounds.Width); + var container = CreateTextRun(text, font, Bounds, string.Concat(displayText, new string[] { "\r\n"})); Runs.Add(container); - container.Transform.Name = $"Container{Runs.Count}"; - - container.SetContent(text); - } - - public string GetContent() - { - StringBuilder sb = new StringBuilder(); - - foreach (var item in Runs) - { - sb.Append(item.GetContent()); - } - - return sb.ToString(); + container.Bounds.Transform.Name = $"Container{Runs.Count}"; } /// @@ -208,64 +194,11 @@ private void AddLinesAndTextRuns(ExcelDrawingParagraph p) //Calculate line breaks and/or wrapping to know how the text should be displayed CalculateDisplayText(p, _textFragments); - //var largestSizes = _textFragments.GetLargestFontSizesOfEachLine(); - - _textRunItems = new List(); - string currentLine = _paragraphLines[0]; - int currentLineIdx = 0; - double currentPosition = 0; double widthOfCurrentLine = 0; double largestFontSizeCurrentLine = 0; int idxLargestFontSize = 0; - ////WE CAN NOW OPERATE PER LINE COUNTING CHARS AS THEY HAVE THE SAME BASIS. - ////THEREFORE WE CAN MOVE PER FRAGMENT OR CHAR AND KNOW WHEN WE ARE IN A NEW LINE MORE EASILY - ////WE HAVE IT MAPPED I JUST NEED TO ITERATE CORRECTLY - - ////---Add Actual textruns--- - //for (int i = 0; i < p.TextRuns.Count; i++) - //{ - // if (p.TextRuns[i].FontSize > largestFontSizeCurrentLine) - // { - // largestFontSizeCurrentLine = p.TextRuns[i].FontSize; - // idxLargestFontSize = i; - // } - - // if(_textRunDisplayText[i].Length + currentPosition > currentLine.Length) - // { - // AddRenderItemTextRun(p.TextRuns[i], _textRunDisplayText[i], widthOfCurrentLine); - - // var lastAdded = _textRunItems.Last(); - // var posLineIsBrokenAt = currentLine.Length - currentPosition; - // currentPosition = posLineIsBrokenAt; - - // //Since the linebreaks are not included in display text anymore this is innaccurate. - // widthOfCurrentLine = _textRunItems.Last().PerLineWidth.Last(); - - // Bounds.Height += _textRunItems[idxLargestFontSize].Bounds.Bottom; - - // //if (_textRunItems.Count == 0 && IsFirstParagraph == true || lastAdded.YIncreasePerLine.Count() > 1) - // //{ - // // Bounds.Height += txtRunBottom; - // //} - - // //currently the only font of the new line - // largestFontSizeCurrentLine = p.TextRuns[i].FontSize; - // idxLargestFontSize = i; - - // currentLineIdx += lastAdded.Lines.Count-1; - // currentLine = _paragraphLines[currentLineIdx]; - // } - // else - // { - // AddRenderItemTextRun(p.TextRuns[i], _textRunDisplayText[i], widthOfCurrentLine); - // var lastAdded = _textRunItems.Last(); - // widthOfCurrentLine += _textRunItems.Last().PerLineWidth.Last(); - - // currentPosition += _textRunDisplayText[i].Length; - // } - //} for (int i = 0; i < p.TextRuns.Count; i++) { if (p.TextRuns[i].FontSize > largestFontSizeCurrentLine) @@ -275,25 +208,25 @@ private void AddLinesAndTextRuns(ExcelDrawingParagraph p) } AddRenderItemTextRun(p.TextRuns[i], _textRunDisplayText[i], widthOfCurrentLine); - var lastAdded = _textRunItems.Last(); + var lastAdded = Runs.Last(); //We are on a new line if (lastAdded.YIncreasePerLine.Count > 1) { - _textRunItems[idxLargestFontSize].GetBounds(out double l, out double t, out double r, out double b); - Bounds.Height += _textRunItems[idxLargestFontSize].Bounds.Height; - widthOfCurrentLine = _textRunItems.Last().PerLineWidth.Last(); + Runs[idxLargestFontSize].GetBounds(out double l, out double t, out double r, out double b); + Bounds.Height += Runs[idxLargestFontSize].Bounds.Height; + widthOfCurrentLine = Runs.Last().PerLineWidth.Last(); idxLargestFontSize = i; largestFontSizeCurrentLine = p.TextRuns[i].FontSize; } else { - widthOfCurrentLine += _textRunItems.Last().PerLineWidth.Last(); + widthOfCurrentLine += Runs.Last().PerLineWidth.Last(); if(i == p.TextRuns.Count -1) { - _textRunItems[idxLargestFontSize].GetBounds(out double l, out double t, out double r, out double b); - Bounds.Height += _textRunItems[idxLargestFontSize].Bounds.Height; + Runs[idxLargestFontSize].GetBounds(out double l, out double t, out double r, out double b); + Bounds.Height += Runs[idxLargestFontSize].Bounds.Height; } } } @@ -358,5 +291,6 @@ internal double GetAlignmentHorizontal(eTextAlignment txAlignment) /// /// internal abstract TextRunItem CreateTextRun(ExcelParagraphTextRunBase run, BoundingBox parent, string displayText); + internal abstract TextRunItem CreateTextRun(string text, ExcelTextFont font, BoundingBox parent, string displayText); } } diff --git a/src/EPPlus.Export.ImageRenderer/RenderItems/Shared/TextBody.cs b/src/EPPlus.Export.ImageRenderer/RenderItems/Shared/TextBody.cs index 572446097..4935361c1 100644 --- a/src/EPPlus.Export.ImageRenderer/RenderItems/Shared/TextBody.cs +++ b/src/EPPlus.Export.ImageRenderer/RenderItems/Shared/TextBody.cs @@ -113,11 +113,6 @@ public void ImportTextBody(ExcelTextBody body) } } - public string GetContent() - { - return Paragraphs[0].GetContent(); - //string.Join(Paragraphs[}]) - } //public void AddText(string text, FontMeasurerTrueType measurer) //{ @@ -132,8 +127,6 @@ public string GetContent() //} internal void AddText(string text, ExcelTextFont font) { - var measureFont = font.GetMeasureFont(); - //Document Top position for the paragraph text based on vertical alignment var posY = GetAlignmentVertical(); //var vertAlignAttribute = GetVerticalAlignAttribute(posY); @@ -141,15 +134,13 @@ internal void AddText(string text, ExcelTextFont font) //var measurer = font.PictureRelationDocument.Package.Settings.TextSettings.GenericTextMeasurerTrueType; - var measurer = new FontMeasurerTrueType(); - var m = measurer.MeasureText(text, measureFont); //Limit bounding area with the space taken by previous paragraphs //Note that this is ONLY identical to PosY if the vertical alignment is top //The first run in the first paragraph must apply different line-spacing //var svgParagraph = new SvgParagraph(text, font, area, vertAlignAttribute, posY); var paragraph = CreateParagraph(Bounds); - + paragraph.AddText(text, font); paragraph.FillColor = font.Fill.Color.To6CharHexString(); Paragraphs.Add(paragraph); diff --git a/src/EPPlus.Export.ImageRenderer/RenderItems/Shared/TextRunItem.cs b/src/EPPlus.Export.ImageRenderer/RenderItems/Shared/TextRunItem.cs index a4b0534ed..cea062c80 100644 --- a/src/EPPlus.Export.ImageRenderer/RenderItems/Shared/TextRunItem.cs +++ b/src/EPPlus.Export.ImageRenderer/RenderItems/Shared/TextRunItem.cs @@ -3,6 +3,7 @@ using EPPlus.Graphics; using EPPlusImageRenderer.RenderItems; using OfficeOpenXml.Drawing; +using OfficeOpenXml.Drawing.Theme; using OfficeOpenXml.FormulaParsing.Excel.Functions.MathFunctions; using OfficeOpenXml.Interfaces.Drawing.Text; using OfficeOpenXml.Style; @@ -53,6 +54,51 @@ internal abstract class TextRunItem : RenderItem internal List PerLineWidth { get; private set; } = new List(); internal double ClippingHeight = double.NaN; + internal TextRunItem(ExcelTheme theme, BoundingBox parent, string text, ExcelTextFont font, string displayText) : base(parent, theme) + { + _originalText = text; + + Bounds.Transform.Name = "TextRun"; + _currentText = string.IsNullOrEmpty(displayText) ? _originalText : displayText; + + Lines = Regex.Split(_currentText, "\r\n|\r|\n").ToList(); + + _measurer = new FontMeasurerTrueType(); + + _measurementFont = font.GetMeasureFont(); + _measurer.SetFont(_measurementFont); + + _isFirstInParagraph = true; + + if (parent != null) + { + Bounds.Parent = parent; + } + + _fontStyles = _measurementFont.Style; + + FontSizeInPixels = ((double)_measurementFont.Size).PointToPixel(true); + + _horizontalTextAlignment = eTextAlignment.Center; + + if (font.Fill.Style == eFillStyle.SolidFill) + { + FillColor = "#" + font.Fill.Color.To6CharHexString(); + } + + //To get clipping height we need to get the textbody bounds + if (parent != null && parent.Parent != null && parent.Parent.Parent != null) + { + ClippingHeight = parent.Parent.Parent.Bottom; + } + + _isItalic = font.Italic; + _isBold = font.Bold; + _underLineType = font.UnderLine; + _underlineColor = font.UnderLineColor; + _strikeType = font.Strike; + } + /// /// If the run has been wrapped more line-breaks may have been added in displayText /// @@ -61,9 +107,9 @@ internal abstract class TextRunItem : RenderItem /// internal TextRunItem(ExcelParagraphTextRunBase run, BoundingBox parent = null, string displayText = "") : base(parent, run.Paragraph._prd.Package.Workbook.ThemeManager.GetOrCreateTheme()) { - Bounds.Transform.Name = "TextRun"; - _originalText = run.Text; + + Bounds.Transform.Name = "TextRun"; _currentText = string.IsNullOrEmpty(displayText) ? _originalText : displayText; Lines = Regex.Split(_currentText, "\r\n|\r|\n").ToList(); diff --git a/src/EPPlus.Export.ImageRenderer/RenderItems/SvgItem/SvgParagraphItem.cs b/src/EPPlus.Export.ImageRenderer/RenderItems/SvgItem/SvgParagraphItem.cs index 9321b5fb2..ec5dcb00d 100644 --- a/src/EPPlus.Export.ImageRenderer/RenderItems/SvgItem/SvgParagraphItem.cs +++ b/src/EPPlus.Export.ImageRenderer/RenderItems/SvgItem/SvgParagraphItem.cs @@ -4,6 +4,7 @@ using EPPlusImageRenderer.RenderItems; using OfficeOpenXml.Drawing; using OfficeOpenXml.Drawing.Theme; +using OfficeOpenXml.Style; using System.Globalization; using System.Text; @@ -93,13 +94,13 @@ public override void Render(StringBuilder sb) //Render text run debug boxes as we cannot place the rects after or inside the text element - //if (_textRunItems.Count > 0) + //if (Runs.Count > 0) //{ // //double lastWidth = 0; // var bbLines = new SvgRenderRectItem(); - // foreach (var textRun in _textRunItems) + // foreach (var textRun in Runs) // { // ////render txtRun debug // bbLines.X = textRun.Bounds.Left; @@ -139,9 +140,9 @@ public override void Render(StringBuilder sb) $"font-family=\"{_paragraphFont.FontFamily},{_paragraphFont.FontFamily}_MSFontService,sans-serif\" " + $"font-size=\"{fontSize}px\" >"); - if (_textRunItems != null && _textRunItems.Count > 0) + if (Runs != null && Runs.Count > 0) { - foreach (var textRun in _textRunItems) + foreach (var textRun in Runs) { textRun.Render(sb); } @@ -154,5 +155,9 @@ internal override TextRunItem CreateTextRun(ExcelParagraphTextRunBase run, Bound { return new SvgTextRunItem(run, parent, displayText); } + internal override TextRunItem CreateTextRun(string text, ExcelTextFont font, BoundingBox parent, string displayText) + { + return new SvgTextRunItem(_theme, parent, text, font, displayText); + } } } diff --git a/src/EPPlus.Export.ImageRenderer/RenderItems/SvgItem/SvgTextRunItem.cs b/src/EPPlus.Export.ImageRenderer/RenderItems/SvgItem/SvgTextRunItem.cs index 439a34e32..13930af8a 100644 --- a/src/EPPlus.Export.ImageRenderer/RenderItems/SvgItem/SvgTextRunItem.cs +++ b/src/EPPlus.Export.ImageRenderer/RenderItems/SvgItem/SvgTextRunItem.cs @@ -1,12 +1,13 @@ using EPPlus.Export.ImageRenderer.RenderItems.Shared; using EPPlus.Graphics; +using EPPlusImageRenderer.RenderItems; +using EPPlusImageRenderer.Svg; using OfficeOpenXml.Drawing; +using OfficeOpenXml.Drawing.Theme; +using OfficeOpenXml.Style; using System; using System.Globalization; using System.Text; -using OfficeOpenXml.Style; -using EPPlusImageRenderer.RenderItems; -using EPPlusImageRenderer.Svg; namespace EPPlus.Export.ImageRenderer.RenderItems.SvgItem { @@ -16,6 +17,10 @@ public SvgTextRunItem(ExcelParagraphTextRunBase run, BoundingBox parent = null, { } + public SvgTextRunItem(ExcelTheme theme, BoundingBox parent, string text, ExcelTextFont font, string displayText) : base(theme, parent, text, font, displayText) + { + + } string GetFontStyleAttributes() { string fontStyleAttributes = ""; @@ -91,7 +96,7 @@ public override void Render(StringBuilder sb) //Textrun may continue on same line or start a new line //Refer to pre-calculated list - if (YIncreasePerLine[i] != 0) + if (YIncreasePerLine.Count > 0 && YIncreasePerLine[i] != 0) { var yIncrease = Fonts.OpenType.Utils.TextUtils.RoundToWhole(YIncreasePerLine[i]); diff --git a/src/EPPlus.Export.ImageRenderer/Svg/SvgChartAxis.cs b/src/EPPlus.Export.ImageRenderer/Svg/SvgChartAxis.cs index 6bcd17971..5bbabbc24 100644 --- a/src/EPPlus.Export.ImageRenderer/Svg/SvgChartAxis.cs +++ b/src/EPPlus.Export.ImageRenderer/Svg/SvgChartAxis.cs @@ -297,7 +297,7 @@ private List GetAxisValueTextBoxes() bounds.Height = m.Height.PointToPixel(); var tb = new SvgTextBodyItem(bounds, Chart.WorkSheet._package.Workbook.ThemeManager.GetOrCreateTheme()); tb.ImportTextBody(Axis.TextBody); - tb.Paragraphs[0].AddText(v, (FontMeasurerTrueType)tm); + tb.Paragraphs[0].AddText(v, Axis.Font); tb.SetDrawingPropertiesFill(Axis.Fill, axisStyle.FillReference.Color); ret.Add(tb); } From 30e2245eb8306d65db66d2ea4332a6294c474028 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ossian=20Edstr=C3=B6m?= Date: Thu, 22 Jan 2026 12:28:50 +0100 Subject: [PATCH 010/151] added corrected fillcolors and paragraph spacing --- .../RenderItems/Shared/ParagraphContainer.cs | 77 ++++++------------- .../RenderItems/Shared/TextBody.cs | 9 +-- .../RenderItems/Shared/TextRunItem.cs | 2 +- src/EPPlusTest/WorkSheetTests.cs | 30 ++++++++ 4 files changed, 57 insertions(+), 61 deletions(-) diff --git a/src/EPPlus.Export.ImageRenderer/RenderItems/Shared/ParagraphContainer.cs b/src/EPPlus.Export.ImageRenderer/RenderItems/Shared/ParagraphContainer.cs index 6e8d9badc..ae9b0993d 100644 --- a/src/EPPlus.Export.ImageRenderer/RenderItems/Shared/ParagraphContainer.cs +++ b/src/EPPlus.Export.ImageRenderer/RenderItems/Shared/ParagraphContainer.cs @@ -48,9 +48,31 @@ public ParagraphContainer(BoundingBox parent, ExcelTheme theme) : base(parent, t public ParagraphContainer(ExcelDrawingParagraph p, BoundingBox parent) : base(parent, p._prd.Package.Workbook.ThemeManager.GetOrCreateTheme()) { - SetDrawingPropertiesFill(p.DefaultRunProperties.Fill, null); + IsFirstParagraph = p == p._paragraphs[0]; + - //---Initialize Bounds/Margins--- + if (p.DefaultRunProperties.Fill != null && p.DefaultRunProperties.Fill.IsEmpty == false) + { + if(IsFirstParagraph) + { + SetDrawingPropertiesFill(p.DefaultRunProperties.Fill, null); + } + else + { + //Drawingproperties has fallback to firstDefault but excel does not display it so we should not either. + if(p.DefaultRunProperties != p._paragraphs.FirstDefaultRunProperties) + { + SetDrawingPropertiesFill(p.DefaultRunProperties.Fill, null); + } + else + { + //Use shape fill somehow + //Maybe use a name property for fallback theme accent1 color? + } + } + } + + //---Initialize Bounds / Margins-- - Bounds.Transform.Name = "Paragraph"; var indent = 48 * p.IndentLevel; @@ -66,7 +88,6 @@ public ParagraphContainer(ExcelDrawingParagraph p, BoundingBox parent) : base(pa _measurer = p._prd.Package.Settings.TextSettings.GenericTextMeasurerTrueType; //---Calculate linespacing--- - IsFirstParagraph = p == p._paragraphs[0]; int numLines = _paragraphLines.Count; _lsType = p.LineSpacing.LineSpacingType; ParagraphLineSpacing = GetParagraphLineSpacingInPixels(p.LineSpacing.Value, _measurer); @@ -134,7 +155,6 @@ internal protected void AddRenderItemTextRun(ExcelParagraphTextRunBase origTxtRu targetTxtRun.GetBounds(out double l, out double t, out double r, out double b); targetTxtRun.SetTheme(_theme); - targetTxtRun.SetDrawingPropertiesFill(origTxtRun.Fill, null); _textRunItems.Add(targetTxtRun); } @@ -213,59 +233,10 @@ private void AddLinesAndTextRuns(ExcelDrawingParagraph p) _textRunItems = new List(); string currentLine = _paragraphLines[0]; - int currentLineIdx = 0; - double currentPosition = 0; double widthOfCurrentLine = 0; double largestFontSizeCurrentLine = 0; int idxLargestFontSize = 0; - ////WE CAN NOW OPERATE PER LINE COUNTING CHARS AS THEY HAVE THE SAME BASIS. - ////THEREFORE WE CAN MOVE PER FRAGMENT OR CHAR AND KNOW WHEN WE ARE IN A NEW LINE MORE EASILY - ////WE HAVE IT MAPPED I JUST NEED TO ITERATE CORRECTLY - - ////---Add Actual textruns--- - //for (int i = 0; i < p.TextRuns.Count; i++) - //{ - // if (p.TextRuns[i].FontSize > largestFontSizeCurrentLine) - // { - // largestFontSizeCurrentLine = p.TextRuns[i].FontSize; - // idxLargestFontSize = i; - // } - - // if(_textRunDisplayText[i].Length + currentPosition > currentLine.Length) - // { - // AddRenderItemTextRun(p.TextRuns[i], _textRunDisplayText[i], widthOfCurrentLine); - - // var lastAdded = _textRunItems.Last(); - // var posLineIsBrokenAt = currentLine.Length - currentPosition; - // currentPosition = posLineIsBrokenAt; - - // //Since the linebreaks are not included in display text anymore this is innaccurate. - // widthOfCurrentLine = _textRunItems.Last().PerLineWidth.Last(); - - // Bounds.Height += _textRunItems[idxLargestFontSize].Bounds.Bottom; - - // //if (_textRunItems.Count == 0 && IsFirstParagraph == true || lastAdded.YIncreasePerLine.Count() > 1) - // //{ - // // Bounds.Height += txtRunBottom; - // //} - - // //currently the only font of the new line - // largestFontSizeCurrentLine = p.TextRuns[i].FontSize; - // idxLargestFontSize = i; - - // currentLineIdx += lastAdded.Lines.Count-1; - // currentLine = _paragraphLines[currentLineIdx]; - // } - // else - // { - // AddRenderItemTextRun(p.TextRuns[i], _textRunDisplayText[i], widthOfCurrentLine); - // var lastAdded = _textRunItems.Last(); - // widthOfCurrentLine += _textRunItems.Last().PerLineWidth.Last(); - - // currentPosition += _textRunDisplayText[i].Length; - // } - //} for (int i = 0; i < p.TextRuns.Count; i++) { if (p.TextRuns[i].FontSize > largestFontSizeCurrentLine) diff --git a/src/EPPlus.Export.ImageRenderer/RenderItems/Shared/TextBody.cs b/src/EPPlus.Export.ImageRenderer/RenderItems/Shared/TextBody.cs index 572446097..114928baa 100644 --- a/src/EPPlus.Export.ImageRenderer/RenderItems/Shared/TextBody.cs +++ b/src/EPPlus.Export.ImageRenderer/RenderItems/Shared/TextBody.cs @@ -98,18 +98,15 @@ public void ImportTextBody(ExcelTextBody body) //We already apply bounds top via the parent Transform double paragraphStartY = GetAlignmentVertical(); - ////TODO; Fix this. This is a strange workaround - //SetTheme(body._pictureRelationDocument.Package.Workbook.ThemeManager.GetOrCreateTheme()); - foreach (var paragraph in body.Paragraphs) { ImportParagraph(paragraph, paragraphStartY); var addedPara = Paragraphs.Last(); - paragraphStartY = addedPara.Bounds.Bottom + addedPara.ParagraphLineSpacing; + paragraphStartY = addedPara.Bounds.Bottom; } if (Paragraphs != null && Paragraphs.Count() > 0) { - Bounds.Height = paragraphStartY - Paragraphs.Last().ParagraphLineSpacing; + Bounds.Height = paragraphStartY; } } @@ -138,8 +135,6 @@ internal void AddText(string text, ExcelTextFont font) var posY = GetAlignmentVertical(); //var vertAlignAttribute = GetVerticalAlignAttribute(posY); - - //var measurer = font.PictureRelationDocument.Package.Settings.TextSettings.GenericTextMeasurerTrueType; var measurer = new FontMeasurerTrueType(); var m = measurer.MeasureText(text, measureFont); diff --git a/src/EPPlus.Export.ImageRenderer/RenderItems/Shared/TextRunItem.cs b/src/EPPlus.Export.ImageRenderer/RenderItems/Shared/TextRunItem.cs index a4b0534ed..f1e1cd4b2 100644 --- a/src/EPPlus.Export.ImageRenderer/RenderItems/Shared/TextRunItem.cs +++ b/src/EPPlus.Export.ImageRenderer/RenderItems/Shared/TextRunItem.cs @@ -87,7 +87,7 @@ internal TextRunItem(ExcelParagraphTextRunBase run, BoundingBox parent = null, s _horizontalTextAlignment = run.Paragraph.HorizontalAlignment; - if (run.Fill.Style == eFillStyle.SolidFill) + if (run.Fill.IsEmpty == false && run.Fill.Style == eFillStyle.SolidFill) { FillColor = "#" + run.Fill.Color.To6CharHexString(); } diff --git a/src/EPPlusTest/WorkSheetTests.cs b/src/EPPlusTest/WorkSheetTests.cs index 85026eed8..f61a59fa0 100644 --- a/src/EPPlusTest/WorkSheetTests.cs +++ b/src/EPPlusTest/WorkSheetTests.cs @@ -2748,5 +2748,35 @@ internal ExcelRangeBase GenerateRowsFromTemplate(ExcelWorksheet wks, ExcelRangeB return wks.Cells[$"{originalTemplateRow}:{originalTemplateRow + insertCount}"]; } + + [TestMethod] + public void testHeaders() + { + using(ExcelPackage p = OpenPackage("somePackage.xlsx",true)) + { + var Styles = p.Workbook.Styles; + + p.Workbook.Worksheets.Add("mt"); + + Stream fs = File.Create(("C:\\epplusTest\\Testoutput\\" + "somePackage.xlsx").Replace("file://", "")); + p.SaveAs(fs); + fs.Close(); + //Styles.Add("MMDDYYYY", new XLSStyle() { NumberFormat = "m/d/yyyy" }); + //Styles.Add("YYYYMMDD HHMMSS", new XLSStyle() { NumberFormat = "yyyy-mm-dd hh:mm:ss" }); + //Styles.Add("YYYYMMDD HHMM", new XLSStyle() { NumberFormat = "yyyy-mm-dd hh:mm" }); + //Styles.Add("Currency", new XLSStyle() { NumberFormat = "$#,##0.00;-$#,##0.00;;@" }); + //Styles.Add("Currency2", new XLSStyle() { NumberFormat = "$#,##0.00;-$#,##0.00;$#,##0.00;@" }); + //Styles.Add("BlankIfZero", new XLSStyle() { NumberFormat = "#.00;-#.00;;@" }); + //Styles.Add("BlankIfZeroInt", new XLSStyle() { NumberFormat = "#;-#;;@" }); + //Styles.Add("BlankIfZeroPercent", new XLSStyle() { NumberFormat = "#.00%;-#.00%;;@" }); + //Styles.Add("Header", new XLSStyle() { Font = new XLSFont() { Size = 14, Bold = true } }); + //Styles.Add("Bold", new XLSStyle() { Font = new XLSFont() { Bold = true } }); + //Styles.Add("HeaderRowLabel", new XLSStyle() { Font = new XLSFont() { Bold = true }, Alignment = new XLSCellAlignment() { Horizontal = ExcelHorizontalAlignment.Right } }); + //Styles.Add("RowLabel", new XLSStyle() { Alignment = new XLSCellAlignment() { Horizontal = ExcelHorizontalAlignment.Right }, Font = new XLSFont() { Bold = true } }); + //Styles.Add("RowLabelCentered", new XLSStyle() { Font = new XLSFont() { Bold = true }, Alignment = new XLSCellAlignment() { Horizontal = ExcelHorizontalAlignment.Center } }); + //Styles.Add("HeaderCentered", new XLSStyle() { Font = new XLSFont() { Bold = true }, Alignment = new XLSCellAlignment() { Horizontal = ExcelHorizontalAlignment.Center } }); + //Styles.Add("Underlined", new XLSStyle() { Font = new XLSFont() { Underline = true } }); + } + } } } From 158f1da0db67bf5a235f0db500cbd7342adaca63 Mon Sep 17 00:00:00 2001 From: OssianEPPlus <122265629+OssianEPPlus@users.noreply.github.com> Date: Thu, 22 Jan 2026 16:51:33 +0100 Subject: [PATCH 011/151] Potential solve if string.Concat was the issue (#2256) * Potential solve if string.Concat was the issue * Added space * Fixed first case --- src/EPPlus/ExcelWorksheet.cs | 13 ++++++++++++- src/EPPlusTest/Issues/WorksheetIssues.cs | 20 ++++++++++++++++++++ 2 files changed, 32 insertions(+), 1 deletion(-) diff --git a/src/EPPlus/ExcelWorksheet.cs b/src/EPPlus/ExcelWorksheet.cs index 47ef355dc..46697d24e 100644 --- a/src/EPPlus/ExcelWorksheet.cs +++ b/src/EPPlus/ExcelWorksheet.cs @@ -2878,7 +2878,18 @@ private void EnsureIgnorablesAreCorrect() } else { - var ignorablesConcatenated = string.Concat(mcIgnorables); + string ignorablesConcatenated = ""; + for (int i = 0; i < mcIgnorables.Count; i++) + { + if(i == 0) + { + ignorablesConcatenated += $"{mcIgnorables[i]}"; + } + else + { + ignorablesConcatenated += $" {mcIgnorables[i]}"; + } + } WorksheetXml.DocumentElement.SetAttributeNode("Ignorable", ExcelPackage.schemaMarkupCompatibility); WorksheetXml.DocumentElement.SetAttribute("Ignorable", ExcelPackage.schemaMarkupCompatibility, ignorablesConcatenated); diff --git a/src/EPPlusTest/Issues/WorksheetIssues.cs b/src/EPPlusTest/Issues/WorksheetIssues.cs index 89615d2a2..fb4b12423 100644 --- a/src/EPPlusTest/Issues/WorksheetIssues.cs +++ b/src/EPPlusTest/Issues/WorksheetIssues.cs @@ -1059,5 +1059,25 @@ public void i2240() SaveAndCleanup(package); } } + [TestMethod] + public void testRepro() + { + SwitchToCulture(""); + using (ExcelPackage p = OpenTemplatePackage("reproTimesheets - Copy.xlsx")) + { + var Styles = p.Workbook.Styles; + + p.Workbook.Worksheets[0].Cells["A10"].Style.Font.Bold = true; + p.Workbook.Worksheets[0].Cells["A10"].Value = "Debugging"; + + p.Workbook.Worksheets.Add("SomeSheet"); + p.Workbook.Worksheets[1].Cells["A10"].Value = "Debugging2"; + + Stream fs = File.Create(("C:\\epplusTest\\Testoutput\\" + "reproTimesheets.xlsx").Replace("file://", "")); + p.SaveAs(fs); + fs.Close(); + } + SwitchBackToCurrentCulture(); + } } } From a13994f6cd19f4e25f41fec286ed97eb74c2eb25 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jan=20K=C3=A4llman?= Date: Fri, 23 Jan 2026 13:28:50 +0100 Subject: [PATCH 012/151] Changed AddText to use DefaultParagraph instead. --- .../TestTextContainer.cs | 2 +- .../TextRenderTests.cs | 4 ++- .../DrawingBase.cs | 14 +++----- .../DrawingChart.cs | 6 ++-- .../ImageRenderer.cs | 30 ++++++++--------- .../RenderItems/RenderItem.cs | 27 +++++++-------- .../RenderItems/Shared/ParagraphContainer.cs | 18 +++++----- .../RenderItems/Shared/TextBody.cs | 33 ++++--------------- .../RenderItems/Shared/TextRunItem.cs | 21 +++++------- .../RenderItems/SvgGroupItem.cs | 4 +-- .../RenderItems/SvgItem/SvgParagraphItem.cs | 33 ++++++++++--------- .../RenderItems/SvgItem/SvgTextBodyItem.cs | 10 +++--- .../RenderItems/SvgItem/SvgTextRunItem.cs | 6 ++-- .../RenderItems/SvgRenderEllipseItem.cs | 5 +-- .../RenderItems/SvgRenderItem.cs | 2 +- .../RenderItems/SvgRenderLineItem.cs | 6 ++-- .../RenderItems/SvgRenderPathItem.cs | 5 ++- .../RenderItems/SvgRenderRectItem.cs | 13 ++++---- .../Svg/Chart/ChartTypeDrawer.cs | 4 +-- .../Svg/DrawingContext.cs | 17 ++++++++++ .../Svg/LineMarkerHelper.cs | 16 ++++----- .../Svg/SvgChart.cs | 3 +- .../Svg/SvgChartAxis.cs | 12 +++---- .../Svg/SvgChartLegend.cs | 15 +++++---- .../Svg/SvgChartObject.cs | 9 ++--- .../Svg/SvgChartPlotarea.cs | 4 +-- .../Svg/SvgChartTitle.cs | 27 ++++++++------- .../Svg/SvgShape.cs | 10 +++--- .../Text/FontWrapContainer.cs | 4 +-- src/EPPlus/Drawing/Chart/ExcelChartTitle.cs | 19 ++++++++++- src/EPPlus/Style/ExcelTextFont.cs | 1 - 31 files changed, 197 insertions(+), 183 deletions(-) create mode 100644 src/EPPlus.Export.ImageRenderer/Svg/DrawingContext.cs diff --git a/src/EPPlus.Export.ImageRenderer.Test/TestTextContainer.cs b/src/EPPlus.Export.ImageRenderer.Test/TestTextContainer.cs index bffe8c8af..c3dfe4634 100644 --- a/src/EPPlus.Export.ImageRenderer.Test/TestTextContainer.cs +++ b/src/EPPlus.Export.ImageRenderer.Test/TestTextContainer.cs @@ -126,7 +126,7 @@ public void TestNonStandardFontSizesMultiLineLargeFontGoudyStout() // Assert.AreEqual(2, body.Paragraphs[0].Bounds.Transform.ChildObjects.Count); // Assert.AreEqual(body.Bounds.Transform.ChildObjects[0], body.Paragraphs[0].Bounds.Transform); - // Assert.AreEqual(shapeRect.Transform, body.Bounds.Transform.Parent); + // Assert.AreEqual(shapeRect.Transform, body.Bounds.Transform.TopDrawingHandler); //} } } diff --git a/src/EPPlus.Export.ImageRenderer.Test/TextRenderTests.cs b/src/EPPlus.Export.ImageRenderer.Test/TextRenderTests.cs index 66893d999..ffc75492d 100644 --- a/src/EPPlus.Export.ImageRenderer.Test/TextRenderTests.cs +++ b/src/EPPlus.Export.ImageRenderer.Test/TextRenderTests.cs @@ -6,6 +6,7 @@ using EPPlus.Export.ImageRenderer.RenderItems.SvgItem; using EPPlus.Graphics; using System.Diagnostics; +using EPPlusImageRenderer.Svg; namespace EPPlus.Export.ImageRenderer.Tests { @@ -105,7 +106,8 @@ public void VerifyTextRunBounds() parentBB.Width = testWidth; parentBB.Height = testHeight; - SvgTextBodyItem tbItem = new SvgTextBodyItem(parentBB, p.Workbook.ThemeManager.GetOrCreateTheme()); + var svgShape = new SvgShape(cube); + SvgTextBodyItem tbItem = new SvgTextBodyItem(svgShape, parentBB); tbItem.ImportTextBody(cube.TextBody); diff --git a/src/EPPlus.Export.ImageRenderer/DrawingBase.cs b/src/EPPlus.Export.ImageRenderer/DrawingBase.cs index 14f226d65..4f1890a59 100644 --- a/src/EPPlus.Export.ImageRenderer/DrawingBase.cs +++ b/src/EPPlus.Export.ImageRenderer/DrawingBase.cs @@ -26,25 +26,19 @@ namespace EPPlusImageRenderer { internal abstract class DrawingBase { - //protected ExcelWorkbook _wb; - protected ExcelDrawing _drawing; - protected ExcelTheme _theme; - internal DrawingBase(ExcelDrawing drawing) { - //drawing.GetSizeInPixels(out int width, out int height); Drawing = drawing; - //Bounds = new DrawingSize(width, height); Bounds = drawing.GetBoundingBox(); - //TextMeasurer = drawing._drawings._package.Settings.TextSettings.PrimaryTextMeasurer; var wb = drawing._drawings.Worksheet.Workbook; - _theme = wb.ThemeManager.GetOrCreateTheme(); + Theme = wb.ThemeManager.GetOrCreateTheme(); } public ExcelDrawing Drawing { get; } - //internal ITextMeasurer TextMeasurer { get; } + public ExcelTheme Theme { get;} + public ExcelWorkbook Workbook => Drawing._drawings.Worksheet.Workbook; + internal ITextMeasurer TextMeasurer { get; } public List RenderItems { get; } = new List(); - //public DrawingSize Bounds { get; internal set; } internal BoundingBox Bounds = new BoundingBox(); } } \ No newline at end of file diff --git a/src/EPPlus.Export.ImageRenderer/DrawingChart.cs b/src/EPPlus.Export.ImageRenderer/DrawingChart.cs index 065cd43b3..9f38da419 100644 --- a/src/EPPlus.Export.ImageRenderer/DrawingChart.cs +++ b/src/EPPlus.Export.ImageRenderer/DrawingChart.cs @@ -27,8 +27,10 @@ internal abstract class DrawingChart : DrawingBase { public DrawingChart(ExcelChart chart) : base(chart) { - Chart = chart; } - public ExcelChart Chart { get; set; } + public ExcelChart Chart + { + get =>(ExcelChart)Drawing; + } } } \ No newline at end of file diff --git a/src/EPPlus.Export.ImageRenderer/ImageRenderer.cs b/src/EPPlus.Export.ImageRenderer/ImageRenderer.cs index 3ab656fc7..1e330a6f4 100644 --- a/src/EPPlus.Export.ImageRenderer/ImageRenderer.cs +++ b/src/EPPlus.Export.ImageRenderer/ImageRenderer.cs @@ -108,27 +108,27 @@ public string RenderBox(string boxText) //return retStr; } - public string RenderTextBody(ExcelTextBody body, double shapeWidth, double shapeHeight) - { - var sb = new StringBuilder(); + //public string RenderTextBody(ExcelTextBody body, double shapeWidth, double shapeHeight) + //{ + // var sb = new StringBuilder(); - BoundingBox worldBounds = new BoundingBox(); - worldBounds.Width = shapeWidth; - worldBounds.Height = shapeHeight; + // BoundingBox worldBounds = new BoundingBox(); + // worldBounds.Width = shapeWidth; + // worldBounds.Height = shapeHeight; - var doc = new SvgEpplusDocument((int)worldBounds.Width, (int)worldBounds.Height); - doc.Render(sb); + // var doc = new SvgEpplusDocument((int)worldBounds.Width, (int)worldBounds.Height); + // doc.Render(sb); - FontMeasurerTrueType measurer = new FontMeasurerTrueType(11, "Aptos Narrow", FontSubFamily.Regular); + // FontMeasurerTrueType measurer = new FontMeasurerTrueType(11, "Aptos Narrow", FontSubFamily.Regular); - var svgBody = new SvgTextBodyItem(worldBounds, null); - svgBody.ImportTextBody(body); + // var svgBody = new SvgTextBodyItem(doc, worldBounds, null); + // svgBody.ImportTextBody(body); - svgBody.Render(sb); - sb.AppendLine(""); + // svgBody.Render(sb); + // sb.AppendLine(""); - return sb.ToString(); - } + // return sb.ToString(); + //} internal string RenderSvgElement(SvgElement element) { diff --git a/src/EPPlus.Export.ImageRenderer/RenderItems/RenderItem.cs b/src/EPPlus.Export.ImageRenderer/RenderItems/RenderItem.cs index f1ecfcff5..cb5c028c4 100644 --- a/src/EPPlus.Export.ImageRenderer/RenderItems/RenderItem.cs +++ b/src/EPPlus.Export.ImageRenderer/RenderItems/RenderItem.cs @@ -25,14 +25,11 @@ namespace EPPlusImageRenderer.RenderItems { internal abstract class RenderItem : RenderItemBase { - protected ExcelDrawing _drawing; - protected ExcelTheme _theme; - internal RenderItem(BoundingBox parent, ExcelTheme theme) + internal protected DrawingBase DrawingRenderer { get; } + internal RenderItem(DrawingBase renderer, BoundingBox parent) { Bounds.Parent = parent; - _theme = theme; - //_drawing = drawing; - //_theme = drawing._drawings.Worksheet.Workbook.ThemeManager.GetOrCreateTheme(); + DrawingRenderer = renderer; } //internal abstract void GetBounds(out double il, out double it, out double ir, out double ib); internal virtual void GetBounds(out double il, out double it, out double ir, out double ib) @@ -110,7 +107,7 @@ internal virtual void SetDrawingPropertiesFill(ExcelDrawingFillBasic fill, Excel FillColor = GetFillColor(fill, color, FillColorSource); break; case eFillStyle.GradientFill: - GradientFill = new DrawGradientFill(_theme, fill.GradientFill); + GradientFill = new DrawGradientFill(DrawingRenderer.Theme, fill.GradientFill); FillColor = null; break; } @@ -134,7 +131,7 @@ internal virtual void SetDrawingPropertiesBorder(ExcelDrawingBorder border, Exce BorderGradientFill = null; break; case eFillStyle.GradientFill: - BorderGradientFill = new DrawGradientFill(_theme, border.Fill.GradientFill); + BorderGradientFill = new DrawGradientFill(DrawingRenderer.Theme, border.Fill.GradientFill); BorderColor = null; break; } @@ -193,16 +190,16 @@ private string GetFillColor(ExcelDrawingFillBasic fill, ExcelDrawingColorManager { if (styleFillColor == null) { - fc = EPPlusColorConverter.GetThemeColor(_theme.ColorScheme.Accent1); + fc = EPPlusColorConverter.GetThemeColor(DrawingRenderer.Theme.ColorScheme.Accent1); } else { - fc = EPPlusColorConverter.GetThemeColor(_theme, styleFillColor); + fc = EPPlusColorConverter.GetThemeColor(DrawingRenderer.Theme, styleFillColor); } } else if (fill.Style == eFillStyle.SolidFill) { - fc = EPPlusColorConverter.GetThemeColor(_theme, fill.SolidFill.Color); + fc = EPPlusColorConverter.GetThemeColor(DrawingRenderer.Theme, fill.SolidFill.Color); } else { @@ -213,10 +210,10 @@ private string GetFillColor(ExcelDrawingFillBasic fill, ExcelDrawingColorManager return "#" + fc.ToArgb().ToString("x8").Substring(2); } - internal void SetTheme(ExcelTheme theme) - { - _theme = theme; - } + //internal void SetTheme(ExcelTheme theme) + //{ + // _theme = theme; + //} } /// /// Base class for any item rendered. diff --git a/src/EPPlus.Export.ImageRenderer/RenderItems/Shared/ParagraphContainer.cs b/src/EPPlus.Export.ImageRenderer/RenderItems/Shared/ParagraphContainer.cs index b3df42b7f..e1cc0fa5f 100644 --- a/src/EPPlus.Export.ImageRenderer/RenderItems/Shared/ParagraphContainer.cs +++ b/src/EPPlus.Export.ImageRenderer/RenderItems/Shared/ParagraphContainer.cs @@ -3,6 +3,7 @@ using EPPlus.Fonts.OpenType.TrueTypeMeasurer.DataHolders; using EPPlus.Fonts.OpenType.Utils; using EPPlus.Graphics; +using EPPlusImageRenderer; using EPPlusImageRenderer.RenderItems; using OfficeOpenXml.Drawing; using OfficeOpenXml.Drawing.Theme; @@ -25,7 +26,6 @@ internal abstract class ParagraphContainer : RenderItem protected eTextAlignment _hAlign; eDrawingTextLineSpacing _lsType; - internal double ParagraphLineSpacing { get; private set; } double _lineSpacingAscendantOnly; double? _lsMultiplier = null; internal bool IsFirstParagraph { get; private set; } @@ -33,19 +33,19 @@ internal abstract class ParagraphContainer : RenderItem protected List _textRunDisplayText = new List(); TextFragmentCollection _textFragments; - internal List Runs { get; set; } = new List(); - internal protected MeasurementFont _paragraphFont; - //internal BoundingBox Bounds = new BoundingBox(); - public ParagraphContainer(BoundingBox parent, ExcelTheme theme) : base(parent, theme) + internal double ParagraphLineSpacing { get; private set; } + internal List Runs { get; set; } = new List(); + + public ParagraphContainer(DrawingBase renderer, BoundingBox parent) : base(renderer, parent) { Bounds.Transform.Name = "Paragraph"; var defaultFont = new MeasurementFont { FontFamily = "Aptos Narrow", Size = 11, Style = MeasurementFontStyles.Regular }; _paragraphFont = defaultFont; } - public ParagraphContainer(ExcelDrawingParagraph p, BoundingBox parent) : base(parent, p._prd.Package.Workbook.ThemeManager.GetOrCreateTheme()) + public ParagraphContainer(DrawingBase renderer, BoundingBox parent, ExcelDrawingParagraph p) : base(renderer, parent) { SetDrawingPropertiesFill(p.DefaultRunProperties.Fill, null); @@ -100,6 +100,7 @@ private double GetParagraphLineSpacingInPixels(double spacingValue, ITextMeasure } } + /// /// DisplayString is the text altered for display with respect to bounds etc. /// Containing line breaks appropriate for the given container @@ -132,7 +133,6 @@ internal protected void AddRenderItemTextRun(ExcelParagraphTextRunBase origTxtRu targetTxtRun.GetBounds(out double l, out double t, out double r, out double b); - targetTxtRun.SetTheme(_theme); targetTxtRun.SetDrawingPropertiesFill(origTxtRun.Fill, null); Runs.Add(targetTxtRun); @@ -141,8 +141,8 @@ internal protected void AddRenderItemTextRun(ExcelParagraphTextRunBase origTxtRu public void AddText(string text, ExcelTextFont font) { var measurer = new FontMeasurerTrueType(); - var displayText = measurer.MeasureAndWrapText(text, font.GetMeasureFont(), Bounds.Width); - var container = CreateTextRun(text, font, Bounds, string.Concat(displayText, new string[] { "\r\n"})); + var displayText = measurer.MeasureAndWrapText(text, font.GetMeasureFont(), Bounds.Parent.Width); + var container = CreateTextRun(text, font, Bounds, string.Join("\r\n", displayText.ToArray())); Runs.Add(container); diff --git a/src/EPPlus.Export.ImageRenderer/RenderItems/Shared/TextBody.cs b/src/EPPlus.Export.ImageRenderer/RenderItems/Shared/TextBody.cs index 4935361c1..484ce1b6d 100644 --- a/src/EPPlus.Export.ImageRenderer/RenderItems/Shared/TextBody.cs +++ b/src/EPPlus.Export.ImageRenderer/RenderItems/Shared/TextBody.cs @@ -1,6 +1,7 @@ using EPPlus.Fonts.OpenType; using EPPlus.Fonts.OpenType.Utils; using EPPlus.Graphics; +using EPPlusImageRenderer; using EPPlusImageRenderer.RenderItems; using OfficeOpenXml.Drawing; using OfficeOpenXml.Drawing.Theme; @@ -41,8 +42,8 @@ internal abstract class TextBody : RenderItem internal bool WrapText = true; private FontMeasurerTrueType _measurer = null; - - public TextBody(BoundingBox parent, ExcelTheme theme) : base(parent, theme) + internal string _text; + public TextBody(DrawingBase renderer, BoundingBox parent) : base(renderer, parent) { Bounds.Transform.Name = "TxtBody"; @@ -52,27 +53,7 @@ public TextBody(BoundingBox parent, ExcelTheme theme) : base(parent, theme) Bounds.Height = parent.Height; } - //public void AddParagraph(string text, FontMeasurerTrueType measurer) - //{ - // if(_measurer == null && measurer != null) - // { - // _measurer = measurer; - // } - - // if (_measurer != null) - // { - // var paragraph = CreateParagraph(Bounds); - // Paragraphs.Add(paragraph); - - // //paragraph.AddText(text, measurer); - - // paragraph.Bounds.Transform.Name = $"Container{Paragraphs.Count}"; - // return; - // } - // throw new NullReferenceException($"The FontMeasurer: {_measurer} object is null. Use SetMeasurer before adding text"); - //} - - public void ImportParagraph(ExcelDrawingParagraph item, double startingY) + public void ImportParagraph(ExcelDrawingParagraph item, double startingY, string text=null) { var measureFont = item.DefaultRunProperties.GetMeasureFont(); bool isFirst = Paragraphs.Count == 0; @@ -81,7 +62,7 @@ public void ImportParagraph(ExcelDrawingParagraph item, double startingY) paragraph.Bounds.Transform.Name = $"Container{Paragraphs.Count}"; paragraph.Bounds.Top = startingY; - + _text = text; Paragraphs.Add(paragraph); } @@ -95,12 +76,10 @@ public void ImportTextBody(ExcelTextBody body) RightMargin = r.PointToPixel(); BottomMargin = b.PointToPixel(); + _text = null; //We already apply bounds top via the parent Transform double paragraphStartY = GetAlignmentVertical(); - ////TODO; Fix this. This is a strange workaround - //SetTheme(body._pictureRelationDocument.Package.Workbook.ThemeManager.GetOrCreateTheme()); - foreach (var paragraph in body.Paragraphs) { ImportParagraph(paragraph, paragraphStartY); diff --git a/src/EPPlus.Export.ImageRenderer/RenderItems/Shared/TextRunItem.cs b/src/EPPlus.Export.ImageRenderer/RenderItems/Shared/TextRunItem.cs index cea062c80..eae24d2c1 100644 --- a/src/EPPlus.Export.ImageRenderer/RenderItems/Shared/TextRunItem.cs +++ b/src/EPPlus.Export.ImageRenderer/RenderItems/Shared/TextRunItem.cs @@ -1,6 +1,7 @@ using EPPlus.Fonts.OpenType; using EPPlus.Fonts.OpenType.Utils; using EPPlus.Graphics; +using EPPlusImageRenderer; using EPPlusImageRenderer.RenderItems; using OfficeOpenXml.Drawing; using OfficeOpenXml.Drawing.Theme; @@ -54,7 +55,7 @@ internal abstract class TextRunItem : RenderItem internal List PerLineWidth { get; private set; } = new List(); internal double ClippingHeight = double.NaN; - internal TextRunItem(ExcelTheme theme, BoundingBox parent, string text, ExcelTextFont font, string displayText) : base(parent, theme) + internal TextRunItem(DrawingBase renderer, BoundingBox parent, string text, ExcelTextFont font, string displayText) : base(renderer, parent) { _originalText = text; @@ -70,15 +71,14 @@ internal TextRunItem(ExcelTheme theme, BoundingBox parent, string text, ExcelTex _isFirstInParagraph = true; - if (parent != null) - { - Bounds.Parent = parent; - } - _fontStyles = _measurementFont.Style; FontSizeInPixels = ((double)_measurementFont.Size).PointToPixel(true); - + Bounds.Height = FontSizeInPixels; + if (parent.Height < FontSizeInPixels) + { + parent.Height = FontSizeInPixels; + } _horizontalTextAlignment = eTextAlignment.Center; if (font.Fill.Style == eFillStyle.SolidFill) @@ -105,7 +105,7 @@ internal TextRunItem(ExcelTheme theme, BoundingBox parent, string text, ExcelTex /// /// /// - internal TextRunItem(ExcelParagraphTextRunBase run, BoundingBox parent = null, string displayText = "") : base(parent, run.Paragraph._prd.Package.Workbook.ThemeManager.GetOrCreateTheme()) + internal TextRunItem(DrawingBase renderer, BoundingBox parent, ExcelParagraphTextRunBase run, string displayText = "") : base(renderer, parent) { _originalText = run.Text; @@ -122,11 +122,6 @@ internal TextRunItem(ExcelParagraphTextRunBase run, BoundingBox parent = null, s _isFirstInParagraph = run.IsFirstInParagraph; - if (parent != null) - { - Bounds.Parent = parent; - } - _fontStyles = _measurementFont.Style; FontSizeInPixels = ((double)_measurementFont.Size).PointToPixel(true); diff --git a/src/EPPlus.Export.ImageRenderer/RenderItems/SvgGroupItem.cs b/src/EPPlus.Export.ImageRenderer/RenderItems/SvgGroupItem.cs index 3b8cb8c4f..d9ec234ca 100644 --- a/src/EPPlus.Export.ImageRenderer/RenderItems/SvgGroupItem.cs +++ b/src/EPPlus.Export.ImageRenderer/RenderItems/SvgGroupItem.cs @@ -25,11 +25,11 @@ internal class SvgGroupItem : SvgRenderItem public string GroupTransform = ""; - internal SvgGroupItem(BoundingBox parent, ExcelTheme theme) : base(parent, theme) + internal SvgGroupItem(DrawingBase renderer, BoundingBox parent) : base(renderer, parent) { } - internal SvgGroupItem(BoundingBox parent, double rotation, ExcelTheme theme) : base(parent, theme) + internal SvgGroupItem(DrawingBase renderer, BoundingBox parent, double rotation) : base(renderer, parent) { if(rotation!=0) { diff --git a/src/EPPlus.Export.ImageRenderer/RenderItems/SvgItem/SvgParagraphItem.cs b/src/EPPlus.Export.ImageRenderer/RenderItems/SvgItem/SvgParagraphItem.cs index ec5dcb00d..af497a6b7 100644 --- a/src/EPPlus.Export.ImageRenderer/RenderItems/SvgItem/SvgParagraphItem.cs +++ b/src/EPPlus.Export.ImageRenderer/RenderItems/SvgItem/SvgParagraphItem.cs @@ -1,6 +1,7 @@ using EPPlus.Export.ImageRenderer.RenderItems.Shared; using EPPlus.Fonts.OpenType.Utils; using EPPlus.Graphics; +using EPPlusImageRenderer; using EPPlusImageRenderer.RenderItems; using OfficeOpenXml.Drawing; using OfficeOpenXml.Drawing.Theme; @@ -14,11 +15,11 @@ internal class SvgParagraphItem : ParagraphContainer { public override RenderItemType Type => RenderItemType.Paragraph; - public SvgParagraphItem(BoundingBox parent, ExcelTheme theme) : base(parent, theme) + public SvgParagraphItem(DrawingBase renderer, BoundingBox parent) : base(renderer, parent) { } - public SvgParagraphItem(ExcelDrawingParagraph p, BoundingBox parent) : base(p, parent) + public SvgParagraphItem(DrawingBase renderer, BoundingBox parent, ExcelDrawingParagraph p) : base(renderer, parent, p) { } @@ -78,19 +79,19 @@ public override void Render(StringBuilder sb) sb.AppendLine("paragraph "); - //var bb = new SvgRenderRectItem(Bounds); - ////The bb is affected by the Transform so set pos to zero - //if(IsFirstParagraph == false) - //{ - // bb.Y = 0; - //} - //bb.X = 0; + var bb = new SvgRenderRectItem(DrawingRenderer, Bounds); + //The bb is affected by the Transform so set pos to zero + if (IsFirstParagraph == false) + { + bb.Y = 0; + } + bb.X = 0; - //bb.Width = Bounds.Width; - //bb.Height = Bounds.Height; - //bb.FillColor = "gold"; - //bb.FillOpacity = 0.3; - //bb.Render(sb); + bb.Width = Bounds.Width; + bb.Height = Bounds.Height; + bb.FillColor = FillColor; + bb.FillOpacity = 0.3; + bb.Render(sb); //Render text run debug boxes as we cannot place the rects after or inside the text element @@ -153,11 +154,11 @@ public override void Render(StringBuilder sb) } internal override TextRunItem CreateTextRun(ExcelParagraphTextRunBase run, BoundingBox parent, string displayText) { - return new SvgTextRunItem(run, parent, displayText); + return new SvgTextRunItem(DrawingRenderer, parent, run, displayText); } internal override TextRunItem CreateTextRun(string text, ExcelTextFont font, BoundingBox parent, string displayText) { - return new SvgTextRunItem(_theme, parent, text, font, displayText); + return new SvgTextRunItem(DrawingRenderer, parent, text, font, displayText); } } } diff --git a/src/EPPlus.Export.ImageRenderer/RenderItems/SvgItem/SvgTextBodyItem.cs b/src/EPPlus.Export.ImageRenderer/RenderItems/SvgItem/SvgTextBodyItem.cs index 785fb439a..895b7d5c6 100644 --- a/src/EPPlus.Export.ImageRenderer/RenderItems/SvgItem/SvgTextBodyItem.cs +++ b/src/EPPlus.Export.ImageRenderer/RenderItems/SvgItem/SvgTextBodyItem.cs @@ -1,18 +1,20 @@ using EPPlus.Export.ImageRenderer.RenderItems.Shared; using EPPlus.Graphics; +using EPPlusImageRenderer; using EPPlusImageRenderer.RenderItems; using EPPlusImageRenderer.Svg; using OfficeOpenXml.Drawing; using OfficeOpenXml.Drawing.Theme; using System; using System.Collections.Generic; +using System.Globalization; using System.Text; namespace EPPlus.Export.ImageRenderer.RenderItems.SvgItem { internal class SvgTextBodyItem : TextBody { - public SvgTextBodyItem(BoundingBox parent, ExcelTheme theme) : base(parent, theme) + public SvgTextBodyItem(DrawingBase renderer, BoundingBox parent) : base(renderer, parent) { } @@ -20,7 +22,7 @@ public SvgTextBodyItem(BoundingBox parent, ExcelTheme theme) : base(parent, them public override void Render(StringBuilder sb) { - sb.AppendLine($""); sb.AppendLine($"txtBody"); @@ -42,12 +44,12 @@ public override void Render(StringBuilder sb) internal override ParagraphContainer CreateParagraph(BoundingBox parent) { - return new SvgParagraphItem(parent, _theme); + return new SvgParagraphItem(DrawingRenderer, parent); } internal override ParagraphContainer CreateParagraph(ExcelDrawingParagraph paragraph, BoundingBox parent) { - return new SvgParagraphItem(paragraph, parent); + return new SvgParagraphItem(DrawingRenderer, parent, paragraph); } } } diff --git a/src/EPPlus.Export.ImageRenderer/RenderItems/SvgItem/SvgTextRunItem.cs b/src/EPPlus.Export.ImageRenderer/RenderItems/SvgItem/SvgTextRunItem.cs index 13930af8a..5d0d7d262 100644 --- a/src/EPPlus.Export.ImageRenderer/RenderItems/SvgItem/SvgTextRunItem.cs +++ b/src/EPPlus.Export.ImageRenderer/RenderItems/SvgItem/SvgTextRunItem.cs @@ -1,5 +1,6 @@ using EPPlus.Export.ImageRenderer.RenderItems.Shared; using EPPlus.Graphics; +using EPPlusImageRenderer; using EPPlusImageRenderer.RenderItems; using EPPlusImageRenderer.Svg; using OfficeOpenXml.Drawing; @@ -13,11 +14,11 @@ namespace EPPlus.Export.ImageRenderer.RenderItems.SvgItem { internal class SvgTextRunItem : TextRunItem { - public SvgTextRunItem(ExcelParagraphTextRunBase run, BoundingBox parent = null, string displayText = "") : base(run, parent, displayText) + public SvgTextRunItem(DrawingBase renderer, BoundingBox parent, ExcelParagraphTextRunBase run, string displayText = "") : base(renderer,parent, run, displayText) { } - public SvgTextRunItem(ExcelTheme theme, BoundingBox parent, string text, ExcelTextFont font, string displayText) : base(theme, parent, text, font, displayText) + public SvgTextRunItem(DrawingBase renderer, BoundingBox parent, string text, ExcelTextFont font, string displayText) : base(renderer, parent, text, font, displayText) { } @@ -82,6 +83,7 @@ string GetFontStyleAttributes() public override void Render(StringBuilder sb) { + CalculateLineSpacing(); string finalString = ""; var xString = $"x =\"{(Bounds.X).ToString(CultureInfo.InvariantCulture)}\" "; diff --git a/src/EPPlus.Export.ImageRenderer/RenderItems/SvgRenderEllipseItem.cs b/src/EPPlus.Export.ImageRenderer/RenderItems/SvgRenderEllipseItem.cs index bec2aaf81..f9caab6d5 100644 --- a/src/EPPlus.Export.ImageRenderer/RenderItems/SvgRenderEllipseItem.cs +++ b/src/EPPlus.Export.ImageRenderer/RenderItems/SvgRenderEllipseItem.cs @@ -11,6 +11,7 @@ Date Author Change 27/11/2025 EPPlus Software AB EPPlus 9 *************************************************************************************************/ using EPPlus.Export.ImageRenderer.Utils; +using EPPlus.Graphics; using EPPlusImageRenderer.Svg; using OfficeOpenXml.Drawing; using OfficeOpenXml.Drawing.Theme; @@ -20,7 +21,7 @@ namespace EPPlusImageRenderer.RenderItems { internal class SvgRenderEllipseItem : SvgRenderItem { - public SvgRenderEllipseItem(ExcelDrawing drawing, ExcelTheme theme) : base(drawing.GetBoundingBox(), theme) + public SvgRenderEllipseItem(DrawingBase renderer, BoundingBox parent) : base(renderer, parent) { } @@ -46,7 +47,7 @@ public override void Render(StringBuilder sb) internal override SvgRenderItem Clone(SvgShape svgDocument) { - var clone = new SvgRenderEllipseItem(_drawing, _theme); + var clone = new SvgRenderEllipseItem(svgDocument, svgDocument.Bounds); CloneBase(clone); //if (AdjustmentPoints != null && AdjustmentPoints.Commands == null && AdjustmentPoints.AdjustmentType == AdjustmentType.AdjustToWidthHeight) //{ diff --git a/src/EPPlus.Export.ImageRenderer/RenderItems/SvgRenderItem.cs b/src/EPPlus.Export.ImageRenderer/RenderItems/SvgRenderItem.cs index 8e4820d11..b44983ca9 100644 --- a/src/EPPlus.Export.ImageRenderer/RenderItems/SvgRenderItem.cs +++ b/src/EPPlus.Export.ImageRenderer/RenderItems/SvgRenderItem.cs @@ -28,7 +28,7 @@ internal enum SvgFillType } internal abstract class SvgRenderItem : RenderItem { - internal SvgRenderItem(BoundingBox parent, ExcelTheme theme) : base(parent, theme) + internal SvgRenderItem(DrawingBase renderer, BoundingBox parent) : base(renderer, parent) { } public override void Render(StringBuilder sb) diff --git a/src/EPPlus.Export.ImageRenderer/RenderItems/SvgRenderLineItem.cs b/src/EPPlus.Export.ImageRenderer/RenderItems/SvgRenderLineItem.cs index 1d82aa3a6..7163514da 100644 --- a/src/EPPlus.Export.ImageRenderer/RenderItems/SvgRenderLineItem.cs +++ b/src/EPPlus.Export.ImageRenderer/RenderItems/SvgRenderLineItem.cs @@ -11,6 +11,7 @@ Date Author Change 27/11/2025 EPPlus Software AB EPPlus 9 *************************************************************************************************/ using EPPlus.Export.ImageRenderer.Utils; +using EPPlus.Graphics; using EPPlusImageRenderer.Svg; using OfficeOpenXml.Drawing; using OfficeOpenXml.Drawing.Theme; @@ -20,7 +21,7 @@ namespace EPPlusImageRenderer.RenderItems { internal class SvgRenderLineItem : SvgRenderItem { - public SvgRenderLineItem(ExcelDrawing drawing, ExcelTheme theme) : base(drawing.GetBoundingBox(), theme) + public SvgRenderLineItem(DrawingBase renderer, BoundingBox parent) : base(renderer, parent) { } @@ -47,8 +48,7 @@ public override void Render(StringBuilder sb) internal override SvgRenderItem Clone(SvgShape svgDocument) { - var clone = new SvgRenderLineItem(_drawing, _theme); - clone._theme = _theme; + var clone = new SvgRenderLineItem(svgDocument, svgDocument.Bounds); CloneBase(clone); //if (adjustmentpoints != null && adjustmentpoints.commands == null && adjustmentpoints.adjustmenttype == adjustmenttype.adjusttowidthheight) //{ diff --git a/src/EPPlus.Export.ImageRenderer/RenderItems/SvgRenderPathItem.cs b/src/EPPlus.Export.ImageRenderer/RenderItems/SvgRenderPathItem.cs index 692b6b055..a30e4e14d 100644 --- a/src/EPPlus.Export.ImageRenderer/RenderItems/SvgRenderPathItem.cs +++ b/src/EPPlus.Export.ImageRenderer/RenderItems/SvgRenderPathItem.cs @@ -22,7 +22,7 @@ namespace EPPlusImageRenderer.RenderItems { internal class SvgRenderPathItem : SvgRenderItem { - public SvgRenderPathItem(BoundingBox parent, ExcelTheme theme) : base(parent, theme) + public SvgRenderPathItem(DrawingBase renderer, BoundingBox parent) : base(renderer, parent) { Bounds.Width = parent.Width; Bounds.Height = parent.Height; @@ -48,8 +48,7 @@ public override void Render(StringBuilder sb) internal override SvgRenderItem Clone(SvgShape svgDocument) { - var clone = new SvgRenderPathItem(_drawing.GetBoundingBox(), _theme); - clone._theme = _theme; + var clone = new SvgRenderPathItem(svgDocument, svgDocument.Bounds); CloneBase(clone); clone.Commands = CloneCommands(Commands); return clone; diff --git a/src/EPPlus.Export.ImageRenderer/RenderItems/SvgRenderRectItem.cs b/src/EPPlus.Export.ImageRenderer/RenderItems/SvgRenderRectItem.cs index e409def55..4078eb6c9 100644 --- a/src/EPPlus.Export.ImageRenderer/RenderItems/SvgRenderRectItem.cs +++ b/src/EPPlus.Export.ImageRenderer/RenderItems/SvgRenderRectItem.cs @@ -24,14 +24,14 @@ namespace EPPlusImageRenderer.RenderItems { internal class SvgRenderRectItem : SvgRenderItem { - public SvgRenderRectItem(BoundingBox parent, ExcelTheme theme) : base(parent, theme) + public SvgRenderRectItem(DrawingBase renderer, BoundingBox parent) : base(renderer, parent) { } - public SvgRenderRectItem(ExcelDrawing drawing) : base(drawing.GetBoundingBox(), drawing._drawings._package.Workbook.ThemeManager.GetOrCreateTheme()) - { + //public SvgRenderRectItem(ExcelDrawing drawing) : base(drawing.GetBoundingBox()) + //{ - } + //} public double X { get { return Bounds.Left; } set { Bounds.Left = value; } } public double Y { get { return Bounds.Top; } set { Bounds.Top = value; } } @@ -46,7 +46,7 @@ public SvgRenderRectItem(ExcelDrawing drawing) : base(drawing.GetBoundingBox(), public override void Render(StringBuilder sb) { - var groupItem = new SvgGroupItem(Bounds, _theme); + var groupItem = new SvgGroupItem(DrawingRenderer, Bounds); groupItem.Render(sb); RenderRect(sb); @@ -67,8 +67,7 @@ internal void RenderRect(StringBuilder sb) internal override SvgRenderItem Clone(SvgShape svgDocument) { - var clone = new SvgRenderRectItem(_drawing); - clone._theme = _theme; + var clone = new SvgRenderRectItem(svgDocument, svgDocument.Bounds); CloneBase(clone); clone.X = X * svgDocument.Bounds.Width; diff --git a/src/EPPlus.Export.ImageRenderer/Svg/Chart/ChartTypeDrawer.cs b/src/EPPlus.Export.ImageRenderer/Svg/Chart/ChartTypeDrawer.cs index e4a3f0059..32dbefa16 100644 --- a/src/EPPlus.Export.ImageRenderer/Svg/Chart/ChartTypeDrawer.cs +++ b/src/EPPlus.Export.ImageRenderer/Svg/Chart/ChartTypeDrawer.cs @@ -16,7 +16,7 @@ internal abstract class ChartTypeDrawer : SvgChartObject { protected SvgChart _svgChart; protected ExcelChart _chartType; - private ChartTypeDrawer(SvgChart svgChart, ExcelChart chartType) : base(svgChart.Chart) + private ChartTypeDrawer(SvgChart svgChart, ExcelChart chartType) : base(svgChart) { _svgChart = svgChart; _chartType = chartType; @@ -103,7 +103,7 @@ private void AddLine(ExcelChart chartType, ExcelLineChartSerie serie, List(); var markerItems = new List(); diff --git a/src/EPPlus.Export.ImageRenderer/Svg/DrawingContext.cs b/src/EPPlus.Export.ImageRenderer/Svg/DrawingContext.cs new file mode 100644 index 000000000..fd2dacd78 --- /dev/null +++ b/src/EPPlus.Export.ImageRenderer/Svg/DrawingContext.cs @@ -0,0 +1,17 @@ +using OfficeOpenXml.Drawing; +using OfficeOpenXml.Drawing.Theme; + +namespace EPPlus.Export.ImageRenderer.Svg +{ + internal class DrawingContext + { + public DrawingContext(T topDrawingHandler,ExcelDrawing drawing) + { + TopDrawingHandler = topDrawingHandler; + Drawing = drawing; + } + public T TopDrawingHandler { get; set; } + public ExcelDrawing Drawing { get; set; } + public ExcelTheme Theme { get; set; } + } +} diff --git a/src/EPPlus.Export.ImageRenderer/Svg/LineMarkerHelper.cs b/src/EPPlus.Export.ImageRenderer/Svg/LineMarkerHelper.cs index 417feec2b..94ed4d92d 100644 --- a/src/EPPlus.Export.ImageRenderer/Svg/LineMarkerHelper.cs +++ b/src/EPPlus.Export.ImageRenderer/Svg/LineMarkerHelper.cs @@ -27,7 +27,7 @@ internal static RenderItem GetMarkerItem(SvgChart sc, ExcelLineChartSerie ls, fl switch (m.Style) { case eMarkerStyle.Circle: - item = new SvgRenderEllipseItem(sc.Drawing, sc.Drawing._drawings._package.Workbook.ThemeManager.GetOrCreateTheme()) + item = new SvgRenderEllipseItem(sc, sc.Bounds) { Rx = halfSize, Ry = halfSize, @@ -36,7 +36,7 @@ internal static RenderItem GetMarkerItem(SvgChart sc, ExcelLineChartSerie ls, fl }; break; case eMarkerStyle.Triangle: - item = new SvgRenderPathItem(sc.Drawing.GetBoundingBox(), sc.Drawing._drawings._package.Workbook.ThemeManager.GetOrCreateTheme()) + item = new SvgRenderPathItem(sc, sc.Bounds) { Commands = new List() }; @@ -45,7 +45,7 @@ internal static RenderItem GetMarkerItem(SvgChart sc, ExcelLineChartSerie ls, fl ((SvgRenderPathItem)item).Commands.Add(new PathCommands(PathCommandType.End, item)); break; case eMarkerStyle.Diamond: - item = new SvgRenderPathItem(sc.Drawing.GetBoundingBox(), sc.Drawing._drawings._package.Workbook.ThemeManager.GetOrCreateTheme()) + item = new SvgRenderPathItem(sc, sc.Drawing.GetBoundingBox()) { Commands = new List() }; @@ -64,7 +64,7 @@ internal static RenderItem GetMarkerItem(SvgChart sc, ExcelLineChartSerie ls, fl { if(m.Style == eMarkerStyle.Dot) { - item = new SvgRenderRectItem(sc.Drawing) + item = new SvgRenderRectItem(sc, sc.Bounds) { Left = x, Top = y - size / 8, @@ -74,7 +74,7 @@ internal static RenderItem GetMarkerItem(SvgChart sc, ExcelLineChartSerie ls, fl } else //Dash { - item = new SvgRenderRectItem(sc.Drawing) + item = new SvgRenderRectItem(sc, sc.Bounds) { Left = x - size / 2, Top = y - size / 8, @@ -85,7 +85,7 @@ internal static RenderItem GetMarkerItem(SvgChart sc, ExcelLineChartSerie ls, fl } break; case eMarkerStyle.Square: - item = new SvgRenderRectItem(sc.Drawing) + item = new SvgRenderRectItem(sc, sc.Bounds) { Left = x - size / 2, Top = y - size / 2, @@ -96,7 +96,7 @@ internal static RenderItem GetMarkerItem(SvgChart sc, ExcelLineChartSerie ls, fl case eMarkerStyle.Plus: case eMarkerStyle.Star: case eMarkerStyle.X: - var pathItem = new SvgRenderPathItem(sc.Drawing.GetBoundingBox(), sc.Drawing._drawings._package.Workbook.ThemeManager.GetOrCreateTheme()) + var pathItem = new SvgRenderPathItem(sc, sc.Bounds) { Commands = new List() }; @@ -165,7 +165,7 @@ internal static RenderItem GetMarkerBackground(SvgChart sc, ExcelLineChartSerie float maxSize = isLegend ? 7f : float.MaxValue; var size = m.Size > maxSize ? maxSize : m.Size; //var line = sls.SeriesIcon as SvgRenderLineItem; - item = new SvgRenderRectItem(sc.Drawing) + item = new SvgRenderRectItem(sc, sc.Bounds) { Left = x - (size / 2),// line.X1 + (line.X2 - line.X1 - size) / 2, Top = y - (size / 2), diff --git a/src/EPPlus.Export.ImageRenderer/Svg/SvgChart.cs b/src/EPPlus.Export.ImageRenderer/Svg/SvgChart.cs index b4691e31e..e684ebac6 100644 --- a/src/EPPlus.Export.ImageRenderer/Svg/SvgChart.cs +++ b/src/EPPlus.Export.ImageRenderer/Svg/SvgChart.cs @@ -26,7 +26,6 @@ internal class SvgChart : DrawingChart { public SvgChart(ExcelChart chart) : base(chart) { - Chart = chart; SetChartArea(); if(chart.HasTitle && chart.Series.Count > 0) @@ -133,7 +132,7 @@ private void SetAxisPositionsFromPlotarea(SvgChart sc) private void SetChartArea() { - var item = new SvgRenderRectItem(Chart); + var item = new SvgRenderRectItem(this, Bounds); item.Width = Bounds.Width; item.Height = Bounds.Height; item.SetDrawingPropertiesFill(Chart.Fill, Chart.StyleManager.Style.ChartArea.FillReference.Color); diff --git a/src/EPPlus.Export.ImageRenderer/Svg/SvgChartAxis.cs b/src/EPPlus.Export.ImageRenderer/Svg/SvgChartAxis.cs index 5bbabbc24..b5c16b4b6 100644 --- a/src/EPPlus.Export.ImageRenderer/Svg/SvgChartAxis.cs +++ b/src/EPPlus.Export.ImageRenderer/Svg/SvgChartAxis.cs @@ -34,7 +34,7 @@ namespace EPPlusImageRenderer.Svg { internal class SvgChartAxis : SvgChartObject, IDrawingChartAxis { - internal SvgChartAxis(SvgChart sc, ExcelChartAxisStandard ax) : base(sc.Chart) + internal SvgChartAxis(SvgChart sc, ExcelChartAxisStandard ax) : base(sc) { SvgChart= sc; Axis = ax; @@ -71,7 +71,7 @@ internal SvgChartAxis(SvgChart sc, ExcelChartAxisStandard ax) : base(sc.Chart) } else { - Rectangle = new SvgRenderRectItem(sc.Chart); + Rectangle = new SvgRenderRectItem(sc, sc.Bounds); if (ax.AxisPosition == eAxisPosition.Left || ax.AxisPosition == eAxisPosition.Right) { if (ax.AxisPosition == eAxisPosition.Left) @@ -104,7 +104,7 @@ internal SvgChartAxis(SvgChart sc, ExcelChartAxisStandard ax) : base(sc.Chart) Rectangle.FillColor = "none"; - Line = new SvgRenderLineItem(sc.Chart, sc.Drawing._drawings._package.Workbook.ThemeManager.GetOrCreateTheme()); + Line = new SvgRenderLineItem(sc, Rectangle.Bounds); Line.SetDrawingPropertiesBorder(ax.Border, sc.Chart.StyleManager.Style.Title.BorderReference.Color, ax.Border.Fill.Style != eFillStyle.NoFill, 0.75); } } @@ -295,7 +295,7 @@ private List GetAxisValueTextBoxes() bounds.Top = y; bounds.Width = m.Width.PointToPixel(); bounds.Height = m.Height.PointToPixel(); - var tb = new SvgTextBodyItem(bounds, Chart.WorkSheet._package.Workbook.ThemeManager.GetOrCreateTheme()); + var tb = new SvgTextBodyItem(SvgChart, bounds); tb.ImportTextBody(Axis.TextBody); tb.Paragraphs[0].AddText(v, Axis.Font); tb.SetDrawingPropertiesFill(Axis.Fill, axisStyle.FillReference.Color); @@ -415,7 +415,7 @@ private List AddTickmarks(double units, double parentUnit, fl default: throw new InvalidOperationException("Invalid axis position"); } - var tm = new SvgRenderLineItem(Chart, Chart._drawings._package.Workbook.ThemeManager.GetOrCreateTheme()); + var tm = new SvgRenderLineItem(SvgChart, SvgChart.Bounds); tm.X1 = x1; tm.Y1 = y1; tm.X2 = x2; @@ -468,7 +468,7 @@ private List AddGridlines(double units, double parentUnit, Ex throw new InvalidOperationException("Invalid axis position"); } - var tm = new SvgRenderLineItem(Chart, Chart._drawings._package.Workbook.ThemeManager.GetOrCreateTheme()); + var tm = new SvgRenderLineItem(SvgChart, SvgChart.Bounds); tm.X1 = x1; tm.Y1 = y1; tm.X2 = x2; diff --git a/src/EPPlus.Export.ImageRenderer/Svg/SvgChartLegend.cs b/src/EPPlus.Export.ImageRenderer/Svg/SvgChartLegend.cs index d766a7fb4..7f995696a 100644 --- a/src/EPPlus.Export.ImageRenderer/Svg/SvgChartLegend.cs +++ b/src/EPPlus.Export.ImageRenderer/Svg/SvgChartLegend.cs @@ -37,7 +37,7 @@ internal class SvgChartLegend : SvgChartObject const int MarginExtra = 2; const int MiddleMargin = 10; const int LineLength = 32; - internal SvgChartLegend(SvgChart sc) : base(sc.Chart) + internal SvgChartLegend(SvgChart sc) : base(sc) { _ttMeasurer = sc.Chart.WorkSheet._package.Settings.TextSettings.GenericTextMeasurerTrueType; if (sc.Chart.HasLegend == false || sc.Chart.Series.Count == 0) @@ -66,7 +66,7 @@ internal SvgChartLegend(SvgChart sc) : base(sc.Chart) private SvgRenderRectItem GetLegendRectangle(SvgChart sc, ExcelChartLegend l) { - var rect = new SvgRenderRectItem(Chart); + var rect = new SvgRenderRectItem(sc, sc.Bounds); bool isVertical; switch (l.Position) { @@ -194,15 +194,18 @@ internal void SetLegend(SvgChart sc) var tm = _seriesHeadersMeasure[index]; var si = GetSeriesIcon(sc, ls, index, tm, pSls); sls.SeriesIcon = si; - sls.Textbox = new SvgTextBodyItem(Rectangle.Bounds, Chart.WorkSheet._package.Workbook.ThemeManager.GetOrCreateTheme()); + sls.Textbox = new SvgTextBodyItem(ChartRenderer, Rectangle.Bounds); var entry = Chart.Legend.Entries.FirstOrDefault(x => x.Index == index); + var headerText = s.GetHeaderText(); if (entry == null || entry.Font.IsEmpty) { - sls.Textbox.AddText(s.GetHeaderText(), sc.Chart.Legend.Font); + //sls.Textbox.AddText(s.GetHeaderText(), sc.Chart.Legend.Font); + sls.Textbox.ImportParagraph(sc.Chart.Legend.TextBody.Paragraphs.FirstOrDefault(), 0, headerText); } else { - sls.Textbox.AddText(s.GetHeaderText(), entry.Font); + //sls.Textbox.AddText(s.GetHeaderText(), entry.Font); + sls.Textbox.ImportParagraph(sc.Chart.Legend.TextBody.Paragraphs.FirstOrDefault(), 0, headerText); } if (ls.HasMarker() && ls.Marker.Style != eMarkerStyle.None) { @@ -233,7 +236,7 @@ internal void SetLegend(SvgChart sc) private SvgRenderLineItem GetSeriesIcon(SvgChart sc, ExcelLineChartSerie ls, int index, TextMeasurement tm, SvgLegendSerie pSls) { - var item = new SvgRenderLineItem(sc.Chart, sc.Chart._drawings._package.Workbook.ThemeManager.GetOrCreateTheme()); + var item = new SvgRenderLineItem(sc, sc.Bounds); item.SetDrawingPropertiesFill(ls.Fill, sc.Chart.StyleManager.Style.SeriesLine.FillReference.Color); item.SetDrawingPropertiesBorder(ls.Border, sc.Chart.StyleManager.Style.SeriesLine.BorderReference.Color, ls.Border.Fill.Style!=eFillStyle.NoFill, 0.75); diff --git a/src/EPPlus.Export.ImageRenderer/Svg/SvgChartObject.cs b/src/EPPlus.Export.ImageRenderer/Svg/SvgChartObject.cs index bf664d30e..ee32cbd81 100644 --- a/src/EPPlus.Export.ImageRenderer/Svg/SvgChartObject.cs +++ b/src/EPPlus.Export.ImageRenderer/Svg/SvgChartObject.cs @@ -20,10 +20,11 @@ namespace EPPlusImageRenderer.Svg { internal abstract class SvgChartObject { - internal ExcelChart Chart { get; } - internal SvgChartObject(ExcelChart chart) + internal DrawingChart ChartRenderer; + internal ExcelChart Chart => (ExcelChart)ChartRenderer.Drawing; + internal SvgChartObject(DrawingChart chart) { - Chart= chart; + ChartRenderer = chart; } internal void SetMargins(ExcelTextBody tb) { @@ -42,7 +43,7 @@ internal void SetMargins(ExcelTextBody tb) public string Text { get; set; } protected static SvgRenderRectItem GetRectFromManualLayout(SvgChart sc, ExcelLayout layout) { - var rect = new SvgRenderRectItem(sc.Chart); + var rect = new SvgRenderRectItem(sc, sc.Bounds); var ml = layout.ManualLayout; if (ml.LeftMode == eLayoutMode.Edge) { diff --git a/src/EPPlus.Export.ImageRenderer/Svg/SvgChartPlotarea.cs b/src/EPPlus.Export.ImageRenderer/Svg/SvgChartPlotarea.cs index 21b35cdd5..25fb39098 100644 --- a/src/EPPlus.Export.ImageRenderer/Svg/SvgChartPlotarea.cs +++ b/src/EPPlus.Export.ImageRenderer/Svg/SvgChartPlotarea.cs @@ -20,7 +20,7 @@ namespace EPPlusImageRenderer.Svg { internal class SvgChartPlotarea : SvgChartObject { - public SvgChartPlotarea(SvgChart sc) : base(sc.Chart) + public SvgChartPlotarea(SvgChart sc) : base(sc) { SvgChart = sc; Rectangle = GetPlotAreaRectangle(sc); @@ -31,7 +31,7 @@ internal SvgRenderRectItem GetPlotAreaRectangle(SvgChart sc) { var pa = sc.Chart.PlotArea; TopMargin = BottomMargin = LeftMargin = RightMargin = 14; - var rect = new SvgRenderRectItem(sc.Chart); + var rect = new SvgRenderRectItem(sc, sc.Bounds); if (pa.Layout.HasLayout) { rect = GetRectFromManualLayout(sc, pa.Layout); diff --git a/src/EPPlus.Export.ImageRenderer/Svg/SvgChartTitle.cs b/src/EPPlus.Export.ImageRenderer/Svg/SvgChartTitle.cs index de3b856ea..dddccd946 100644 --- a/src/EPPlus.Export.ImageRenderer/Svg/SvgChartTitle.cs +++ b/src/EPPlus.Export.ImageRenderer/Svg/SvgChartTitle.cs @@ -22,6 +22,7 @@ Date Author Change using System; using System.Collections.Generic; using System.Globalization; +using System.Linq; using System.Text; namespace EPPlusImageRenderer.Svg @@ -30,6 +31,7 @@ internal class SvgChartTitle : SvgChartObject { ExcelChartTitleStandard _title; string _titleText; + SvgChart _svgChart; /// /// /// @@ -37,8 +39,9 @@ internal class SvgChartTitle : SvgChartObject /// /// /// If null, this is the main chart title. - internal SvgChartTitle(SvgChart sc, ExcelChartTitleStandard t, string defaultText, SvgChartAxis axis=null) : base(sc.Chart) + internal SvgChartTitle(SvgChart sc, ExcelChartTitleStandard t, string defaultText, SvgChartAxis axis=null) : base(sc) { + _svgChart = sc; //These are hard coded margins for the title box. LeftMargin = RightMargin = 4; TopMargin = BottomMargin = 2; @@ -79,7 +82,7 @@ internal SvgChartTitle(SvgChart sc, ExcelChartTitleStandard t, string defaultTex } else { - Rectangle = new SvgRenderRectItem(sc.Chart); + Rectangle = new SvgRenderRectItem(sc, sc.Bounds); if (axis==null) { Rectangle.Top = (float)8; //8 pixels for the chart title standard offset @@ -168,12 +171,11 @@ private static string GetDefaultChartTitleText(SvgChart sc, ExcelChartTitleStand internal void InitTextBox() { - //TextBox = new TextBody(Chart, Rectangle.Left, Rectangle.Top , Rectangle.Width, Rectangle.Height); - TextBox = new SvgTextBodyItem(Rectangle.Bounds, Chart.WorkSheet._package.Workbook.ThemeManager.GetOrCreateTheme()); - TextBox.LeftMargin = LeftMargin; - TextBox.RightMargin = RightMargin; - TextBox.TopMargin = TopMargin; - TextBox.BottomMargin = BottomMargin; + TextBox = new SvgTextBodyItem(_svgChart, Rectangle.Bounds); + TextBox.Bounds.Left = LeftMargin; + TextBox.Bounds.Width -= LeftMargin + RightMargin; + TextBox.Bounds.Top = TopMargin; + TextBox.Bounds.Height -= TopMargin + BottomMargin; TextBox.VerticalAlignment = eTextAnchoringType.Top; if(_title.Rotation != 0) { @@ -188,7 +190,10 @@ internal void InitTextBox() } else { - TextBox.AddText(string.IsNullOrEmpty(_title.Text) ? _titleText : _title.Text, _title.Font); + //TextBox.AddText(string.IsNullOrEmpty(_title.Text) ? _titleText : _title.Text, _title.Font); + var text = string.IsNullOrEmpty(_title.Text) ? _titleText : _title.Text; + var p = _title.DefaultTextBody.Paragraphs.FirstOrDefault(); + TextBox.ImportParagraph(p, 0, text); } } @@ -201,11 +206,11 @@ internal override void AppendRenderItems(List renderItems) SvgGroupItem groupItem; if (TextBox.Bounds.Transform.Rotation == 0) { - groupItem = new SvgGroupItem(Rectangle.Bounds, Chart.WorkSheet.Workbook.ThemeManager.GetOrCreateTheme()); + groupItem = new SvgGroupItem(_svgChart, Rectangle.Bounds); } else { - groupItem = new SvgGroupItem(Rectangle.Bounds, TextBox.Bounds.Transform.Rotation, Chart.WorkSheet.Workbook.ThemeManager.GetOrCreateTheme()); + groupItem = new SvgGroupItem(_svgChart, Rectangle.Bounds, TextBox.Bounds.Transform.Rotation); } renderItems.Add(groupItem); diff --git a/src/EPPlus.Export.ImageRenderer/Svg/SvgShape.cs b/src/EPPlus.Export.ImageRenderer/Svg/SvgShape.cs index 18f842ebc..6608d4364 100644 --- a/src/EPPlus.Export.ImageRenderer/Svg/SvgShape.cs +++ b/src/EPPlus.Export.ImageRenderer/Svg/SvgShape.cs @@ -55,7 +55,7 @@ public SvgShape(ExcelShape shape) : base(shape) var shapeDef = PresetShapeDefinitions.ShapeDefinitions[style].Clone(); shapeDef.Calculate(shape); - RenderItems.Add(new SvgGroupItem(shape.GetBoundingBox(), shape.Rotation, shape._drawings._package.Workbook.ThemeManager.GetOrCreateTheme())); + RenderItems.Add(new SvgGroupItem(this, shape.GetBoundingBox(), shape.Rotation)); //Draw Filled path's foreach (var path in shapeDef.ShapePaths) @@ -79,7 +79,7 @@ public SvgShape(ExcelShape shape) : base(shape) { if (shapeDef.TextBoxRect != null) { - insetTextBox = new SvgRenderRectItem(_shape); + insetTextBox = new SvgRenderRectItem(this, Bounds); insetTextBox.Bounds.Left = (float)shapeDef.TextBoxRect.LeftValue; insetTextBox.Bounds.Top = (float)shapeDef.TextBoxRect.TopValue; insetTextBox.FillOpacity = 0.3d; @@ -108,7 +108,7 @@ public SvgShape(ExcelShape shape) : base(shape) protected void AddFromPaths(DrawingPath path, bool drawFill = true, bool drawBorder = true) { - var pi = new SvgRenderPathItem(_shape.GetBoundingBox(), _theme); + var pi = new SvgRenderPathItem(this, _shape.GetBoundingBox()); var coordinates = new List(); PathCommands cmd = null; PathsBase pCmd = null; @@ -241,14 +241,14 @@ SvgTextBodyItem CreateTextBodyItem() if (insetTextBox == null) { GetShapeInnerBound(out double x, out double y, out double width, out double height); - insetTextBox = new SvgRenderRectItem(Bounds, _theme); + insetTextBox = new SvgRenderRectItem(this, Bounds); insetTextBox.Bounds.Left = x; insetTextBox.Bounds.Top = y; insetTextBox.Width = width; insetTextBox.Height = height; insetTextBox.Bounds.Parent = textBody.Bounds; //TODO:Check that textBody is correct. } - var txtBodyItem = new SvgTextBodyItem(insetTextBox.Bounds, _theme); + var txtBodyItem = new SvgTextBodyItem(this, insetTextBox.Bounds); return txtBodyItem; } diff --git a/src/EPPlus.Export.ImageRenderer/Text/FontWrapContainer.cs b/src/EPPlus.Export.ImageRenderer/Text/FontWrapContainer.cs index a5060beb0..4b903bf78 100644 --- a/src/EPPlus.Export.ImageRenderer/Text/FontWrapContainer.cs +++ b/src/EPPlus.Export.ImageRenderer/Text/FontWrapContainer.cs @@ -61,8 +61,8 @@ private void Initialize(FontMeasurerTrueType txtMeasurer) //void SetParent(Rect parent) //{ - // Parent = parent; - // Transform.Parent = parent.Transform; + // TopDrawingHandler = parent; + // Transform.TopDrawingHandler = parent.Transform; //} private void SplitContentToLines() diff --git a/src/EPPlus/Drawing/Chart/ExcelChartTitle.cs b/src/EPPlus/Drawing/Chart/ExcelChartTitle.cs index 96b065ebc..5cc46e61b 100644 --- a/src/EPPlus/Drawing/Chart/ExcelChartTitle.cs +++ b/src/EPPlus/Drawing/Chart/ExcelChartTitle.cs @@ -32,6 +32,7 @@ public abstract class ExcelChartTitle : XmlHelper, IDrawingStyle, IStyleMandator internal string _nsPrefix = ""; internal string _fontPropertiesPath = ""; internal string _richTextPath = ""; + internal string _defTxBodyPath = ""; internal ExcelChartTitle(ExcelChart chart, XmlNamespaceManager nameSpaceManager, XmlNode node, string nsPrefix) : base(nameSpaceManager, node) { @@ -39,6 +40,7 @@ internal ExcelChartTitle(ExcelChart chart, XmlNamespaceManager nameSpaceManager, _nsPrefix = nsPrefix; _fontPropertiesPath = $"{_nsPrefix}:txPr"; _richTextPath = $"{_nsPrefix}:tx/{_nsPrefix}:rich"; + _defTxBodyPath = $"{_nsPrefix}:txPr"; if (chart._isChartEx) { AddSchemaNodeOrder(new string[] { "layout", "tx", "strRef", "rich", "bodyPr", "lstStyle", "layout", "p", "overlay", "spPr", "txPr" }, ExcelDrawing._schemaNodeOrderSpPr); @@ -151,7 +153,22 @@ public ExcelTextBody TextBody return _textBody; } } - ExcelDrawingTextSettings _textSettings = null; + ExcelTextBody _defaultTextBody = null; + /// + /// Access to default text body properties + /// + public ExcelTextBody DefaultTextBody + { + get + { + if (_defaultTextBody == null) + { + _defaultTextBody = new ExcelTextBody(_chart, NameSpaceManager, TopNode, $"{_defTxBodyPath}/a:bodyPr", SchemaNodeOrder); + } + return _defaultTextBody; + } + } + ExcelDrawingTextSettings _textSettings = null; /// /// Text settings like fills, text outlines and effects /// diff --git a/src/EPPlus/Style/ExcelTextFont.cs b/src/EPPlus/Style/ExcelTextFont.cs index 8349c4a57..c171dcebc 100644 --- a/src/EPPlus/Style/ExcelTextFont.cs +++ b/src/EPPlus/Style/ExcelTextFont.cs @@ -52,7 +52,6 @@ internal ExcelTextFontXml(IPictureRelationDocument pictureRelationDocument, XmlN } _path = path; } - internal void TriggerCreateTopNodeOnTextSet() { CreateTopNode(); From 31c9164ab7ed84fb18661f53ab82a9130bc47c93 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ossian=20Edstr=C3=B6m?= Date: Fri, 23 Jan 2026 13:48:42 +0100 Subject: [PATCH 013/151] Tried to fix center align. WIP --- .../RenderItems/Shared/TextBody.cs | 4 +- src/EPPlus/ExcelWorksheet.cs | 2 + src/EPPlusTest/WorkSheetTests.cs | 138 +++++++++++++++++- 3 files changed, 141 insertions(+), 3 deletions(-) diff --git a/src/EPPlus.Export.ImageRenderer/RenderItems/Shared/TextBody.cs b/src/EPPlus.Export.ImageRenderer/RenderItems/Shared/TextBody.cs index ae217a513..82145671e 100644 --- a/src/EPPlus.Export.ImageRenderer/RenderItems/Shared/TextBody.cs +++ b/src/EPPlus.Export.ImageRenderer/RenderItems/Shared/TextBody.cs @@ -95,6 +95,7 @@ public void ImportTextBody(ExcelTextBody body) RightMargin = r.PointToPixel(); BottomMargin = b.PointToPixel(); + VerticalAlignment = body.Anchor; //We already apply bounds top via the parent Transform double paragraphStartY = GetAlignmentVertical(); @@ -110,7 +111,6 @@ public void ImportTextBody(ExcelTextBody body) } } - //public void AddText(string text, FontMeasurerTrueType measurer) //{ // if (Paragraphs.Count == 0) @@ -187,7 +187,7 @@ private double GetAlignmentVertical() alignmentY = 0; break; case eTextAnchoringType.Center: - var adjustedHeight = (Bounds.Height / 2); + var adjustedHeight = ((Bounds.Height - Bounds.Parent.Top) / 2); alignmentY = adjustedHeight; break; diff --git a/src/EPPlus/ExcelWorksheet.cs b/src/EPPlus/ExcelWorksheet.cs index 4b8833a5f..8ead7a05f 100644 --- a/src/EPPlus/ExcelWorksheet.cs +++ b/src/EPPlus/ExcelWorksheet.cs @@ -2876,6 +2876,8 @@ private void EnsureIgnorablesAreCorrect() } else { + var someString = string.Concat(mcIgnorables, new string[]{"\r\n"}); + var ignorablesConcatenated = string.Concat(mcIgnorables); WorksheetXml.DocumentElement.SetAttributeNode("Ignorable", ExcelPackage.schemaMarkupCompatibility); diff --git a/src/EPPlusTest/WorkSheetTests.cs b/src/EPPlusTest/WorkSheetTests.cs index f61a59fa0..a180a24c2 100644 --- a/src/EPPlusTest/WorkSheetTests.cs +++ b/src/EPPlusTest/WorkSheetTests.cs @@ -2749,14 +2749,54 @@ internal ExcelRangeBase GenerateRowsFromTemplate(ExcelWorksheet wks, ExcelRangeB return wks.Cells[$"{originalTemplateRow}:{originalTemplateRow + insertCount}"]; } + [TestMethod] + public void testRepro() + { + SwitchToCulture(""); + using (ExcelPackage p = OpenTemplatePackage("reproTimesheets - Copy.xlsx")) + { + var Styles = p.Workbook.Styles; + + p.Workbook.Worksheets[0].Cells["A10"].Style.Font.Bold = true; + p.Workbook.Worksheets[0].Cells["A10"].Value = "Debugging"; + + p.Workbook.Worksheets.Add("SomeSheet"); + p.Workbook.Worksheets[1].Cells["A10"].Value = "Debugging2"; + //p.Workbook.Worksheets.Add("mt"); + //p.Workbook.Worksheets.Add("mt2"); + + Stream fs = File.Create(("C:\\epplusTest\\Testoutput\\" + "reproTimesheets.xlsx").Replace("file://", "")); + p.SaveAs(fs); + fs.Close(); + //Styles.Add("MMDDYYYY", new XLSStyle() { NumberFormat = "m/d/yyyy" }); + //Styles.Add("YYYYMMDD HHMMSS", new XLSStyle() { NumberFormat = "yyyy-mm-dd hh:mm:ss" }); + //Styles.Add("YYYYMMDD HHMM", new XLSStyle() { NumberFormat = "yyyy-mm-dd hh:mm" }); + //Styles.Add("Currency", new XLSStyle() { NumberFormat = "$#,##0.00;-$#,##0.00;;@" }); + //Styles.Add("Currency2", new XLSStyle() { NumberFormat = "$#,##0.00;-$#,##0.00;$#,##0.00;@" }); + //Styles.Add("BlankIfZero", new XLSStyle() { NumberFormat = "#.00;-#.00;;@" }); + //Styles.Add("BlankIfZeroInt", new XLSStyle() { NumberFormat = "#;-#;;@" }); + //Styles.Add("BlankIfZeroPercent", new XLSStyle() { NumberFormat = "#.00%;-#.00%;;@" }); + //Styles.Add("Header", new XLSStyle() { Font = new XLSFont() { Size = 14, Bold = true } }); + //Styles.Add("Bold", new XLSStyle() { Font = new XLSFont() { Bold = true } }); + //Styles.Add("HeaderRowLabel", new XLSStyle() { Font = new XLSFont() { Bold = true }, Alignment = new XLSCellAlignment() { Horizontal = ExcelHorizontalAlignment.Right } }); + //Styles.Add("RowLabel", new XLSStyle() { Alignment = new XLSCellAlignment() { Horizontal = ExcelHorizontalAlignment.Right }, Font = new XLSFont() { Bold = true } }); + //Styles.Add("RowLabelCentered", new XLSStyle() { Font = new XLSFont() { Bold = true }, Alignment = new XLSCellAlignment() { Horizontal = ExcelHorizontalAlignment.Center } }); + //Styles.Add("HeaderCentered", new XLSStyle() { Font = new XLSFont() { Bold = true }, Alignment = new XLSCellAlignment() { Horizontal = ExcelHorizontalAlignment.Center } }); + //Styles.Add("Underlined", new XLSStyle() { Font = new XLSFont() { Underline = true } }); + } + SwitchBackToCurrentCulture(); + } + [TestMethod] public void testHeaders() { - using(ExcelPackage p = OpenPackage("somePackage.xlsx",true)) + SwitchToCulture("sv-SE"); + using (ExcelPackage p = OpenPackage("somePackage.xlsx",true)) { var Styles = p.Workbook.Styles; p.Workbook.Worksheets.Add("mt"); + p.Workbook.Worksheets.Add("mt2"); Stream fs = File.Create(("C:\\epplusTest\\Testoutput\\" + "somePackage.xlsx").Replace("file://", "")); p.SaveAs(fs); @@ -2777,6 +2817,102 @@ public void testHeaders() //Styles.Add("HeaderCentered", new XLSStyle() { Font = new XLSFont() { Bold = true }, Alignment = new XLSCellAlignment() { Horizontal = ExcelHorizontalAlignment.Center } }); //Styles.Add("Underlined", new XLSStyle() { Font = new XLSFont() { Underline = true } }); } + SwitchBackToCurrentCulture(); + } + + [TestMethod] + public void TestEmptyIgnorables() + { + SwitchToCulture("sv-SE"); + ExcelPackage.License.SetNonCommercialPersonal("Jan"); + + using (ExcelPackage p = OpenTemplatePackage("emptyIgnorableNamespaces.xlsx")) + { + var Styles = p.Workbook.Styles; + + List mcIgnorables = new(); + + mcIgnorables.Add("xr "); + mcIgnorables.Add(string.Empty); + mcIgnorables.Add("xr2 "); + mcIgnorables.Add(null); + mcIgnorables.Add("xr5 "); + + var result = string.Concat(mcIgnorables); + + + //var result = string.Concat(mcIgnorables, new string[] {"\r\n"}); + + //p.Workbook.Worksheets.Add("mt"); + + Stream fs = File.Create(("C:\\epplusTest\\Testoutput\\" + "emptyIgnorableNamespaces.xlsx").Replace("file://", "")); + p.SaveAs(fs); + fs.Close(); + //Styles.Add("MMDDYYYY", new XLSStyle() { NumberFormat = "m/d/yyyy" }); + //Styles.Add("YYYYMMDD HHMMSS", new XLSStyle() { NumberFormat = "yyyy-mm-dd hh:mm:ss" }); + //Styles.Add("YYYYMMDD HHMM", new XLSStyle() { NumberFormat = "yyyy-mm-dd hh:mm" }); + //Styles.Add("Currency", new XLSStyle() { NumberFormat = "$#,##0.00;-$#,##0.00;;@" }); + //Styles.Add("Currency2", new XLSStyle() { NumberFormat = "$#,##0.00;-$#,##0.00;$#,##0.00;@" }); + //Styles.Add("BlankIfZero", new XLSStyle() { NumberFormat = "#.00;-#.00;;@" }); + //Styles.Add("BlankIfZeroInt", new XLSStyle() { NumberFormat = "#;-#;;@" }); + //Styles.Add("BlankIfZeroPercent", new XLSStyle() { NumberFormat = "#.00%;-#.00%;;@" }); + //Styles.Add("Header", new XLSStyle() { Font = new XLSFont() { Size = 14, Bold = true } }); + //Styles.Add("Bold", new XLSStyle() { Font = new XLSFont() { Bold = true } }); + //Styles.Add("HeaderRowLabel", new XLSStyle() { Font = new XLSFont() { Bold = true }, Alignment = new XLSCellAlignment() { Horizontal = ExcelHorizontalAlignment.Right } }); + //Styles.Add("RowLabel", new XLSStyle() { Alignment = new XLSCellAlignment() { Horizontal = ExcelHorizontalAlignment.Right }, Font = new XLSFont() { Bold = true } }); + //Styles.Add("RowLabelCentered", new XLSStyle() { Font = new XLSFont() { Bold = true }, Alignment = new XLSCellAlignment() { Horizontal = ExcelHorizontalAlignment.Center } }); + //Styles.Add("HeaderCentered", new XLSStyle() { Font = new XLSFont() { Bold = true }, Alignment = new XLSCellAlignment() { Horizontal = ExcelHorizontalAlignment.Center } }); + //Styles.Add("Underlined", new XLSStyle() { Font = new XLSFont() { Underline = true } }); + } + + SwitchBackToCurrentCulture(); } + + [TestMethod] + public void strangeness() + { + var aCollection = new HashSet { "a", "b" }; + var bCollection = new HashSet { "a", "b", "c" }; + var result = aCollection.Select(item => + { + if (bCollection.Contains(item)) + { + bCollection.Remove(item); + } + + return item; + }).Concat(bCollection); + + var res = result.ToString(); + } + + [TestMethod] + public void readWrite() + { + using (ExcelPackage p = OpenTemplatePackage("test998.xlsx")) + { + var ws = p.Workbook.Worksheets[0]; + ws.Cells["A1"].Value = "SomeValue"; + + Stream fs = File.Create(("C:\\epplusTest\\Testoutput\\" + "test998.xlsx").Replace("file://", "")); + p.SaveAs(fs); + fs.Close(); + } + } + //[TestMethod] + //public void strangenessAllMyOwn() + //{ + // const string schemaMarkupCompatibility = @"http://schemas.openxmlformats.org/markup-compatibility/2006"; + + // if (string.IsNullOrEmpty(existingIgnorables) == false) + // { + // var namespaces = existingIgnorables.Split(' '); + // //var ignorablesConcatenated = string.Concat(namespaces); + // if (namespaces.Any(x => x == "xr") == false) + // { + // WorksheetXml.DocumentElement.SetAttribute("Ignorable", ExcelPackage.schemaMarkupCompatibility, existingIgnorables + " xr"); + // } + // } + //} } } From 0361ee34c31e69bee8ce25dbc3a9257dd02b98f7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jan=20K=C3=A4llman?= Date: Fri, 23 Jan 2026 14:03:43 +0100 Subject: [PATCH 014/151] Wip:text fixes --- .../RenderItems/Shared/TextBody.cs | 35 ++++++++++--------- 1 file changed, 19 insertions(+), 16 deletions(-) diff --git a/src/EPPlus.Export.ImageRenderer/RenderItems/Shared/TextBody.cs b/src/EPPlus.Export.ImageRenderer/RenderItems/Shared/TextBody.cs index 2b827b78a..e43f5660d 100644 --- a/src/EPPlus.Export.ImageRenderer/RenderItems/Shared/TextBody.cs +++ b/src/EPPlus.Export.ImageRenderer/RenderItems/Shared/TextBody.cs @@ -60,7 +60,10 @@ public void ImportParagraph(ExcelDrawingParagraph item, double startingY, string var paragraph = CreateParagraph(item, Bounds); paragraph.Bounds.Transform.Name = $"Container{Paragraphs.Count}"; - + if (string.IsNullOrEmpty(text) == false) + { + paragraph.AddText(text, item.DefaultRunProperties); + } paragraph.Bounds.Top = startingY; _text = text; Paragraphs.Add(paragraph); @@ -104,24 +107,24 @@ public void ImportTextBody(ExcelTextBody body) // Paragraphs.Last().AddText(text, measurer); // } //} - internal void AddText(string text, ExcelTextFont font) - { - //Document Top position for the paragraph text based on vertical alignment - var posY = GetAlignmentVertical(); - //var vertAlignAttribute = GetVerticalAlignAttribute(posY); + //internal void AddText(string text, ExcelTextFont font) + //{ + // //Document Top position for the paragraph text based on vertical alignment + // var posY = GetAlignmentVertical(); + // //var vertAlignAttribute = GetVerticalAlignAttribute(posY); - //var measurer = font.PictureRelationDocument.Package.Settings.TextSettings.GenericTextMeasurerTrueType; - //Limit bounding area with the space taken by previous paragraphs - //Note that this is ONLY identical to PosY if the vertical alignment is top + // //var measurer = font.PictureRelationDocument.Package.Settings.TextSettings.GenericTextMeasurerTrueType; + // //Limit bounding area with the space taken by previous paragraphs + // //Note that this is ONLY identical to PosY if the vertical alignment is top - //The first run in the first paragraph must apply different line-spacing - //var svgParagraph = new SvgParagraph(text, font, area, vertAlignAttribute, posY); - var paragraph = CreateParagraph(Bounds); - paragraph.AddText(text, font); - paragraph.FillColor = font.Fill.Color.To6CharHexString(); + // //The first run in the first paragraph must apply different line-spacing + // //var svgParagraph = new SvgParagraph(text, font, area, vertAlignAttribute, posY); + // var paragraph = CreateParagraph(Bounds); + // paragraph.AddText(text, font); + // paragraph.FillColor = font.Fill.Color.To6CharHexString(); - Paragraphs.Add(paragraph); - } + // Paragraphs.Add(paragraph); + //} public void SetMeasurer(FontMeasurerTrueType fontMeasurer) { _measurer = fontMeasurer; From 26bd11c5f0f01d225c9ce721a0dc3652c8f71641 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ossian=20Edstr=C3=B6m?= Date: Fri, 23 Jan 2026 16:34:28 +0100 Subject: [PATCH 015/151] Still two pixels off. Likely Border size --- .../RenderItems/Shared/ParagraphContainer.cs | 1 - .../RenderItems/Shared/TextBody.cs | 55 ++++++++++++++++++- .../RenderItems/SvgItem/SvgTextBodyItem.cs | 2 +- .../RenderItems/SvgRenderItem.cs | 9 +-- .../Style/Text/ExcelParagraphTextRunBase.cs | 2 +- 5 files changed, 61 insertions(+), 8 deletions(-) diff --git a/src/EPPlus.Export.ImageRenderer/RenderItems/Shared/ParagraphContainer.cs b/src/EPPlus.Export.ImageRenderer/RenderItems/Shared/ParagraphContainer.cs index 2d79ac305..a422c2844 100644 --- a/src/EPPlus.Export.ImageRenderer/RenderItems/Shared/ParagraphContainer.cs +++ b/src/EPPlus.Export.ImageRenderer/RenderItems/Shared/ParagraphContainer.cs @@ -49,7 +49,6 @@ public ParagraphContainer(DrawingBase renderer, BoundingBox parent, ExcelDrawing { IsFirstParagraph = p == p._paragraphs[0]; - if (p.DefaultRunProperties.Fill != null && p.DefaultRunProperties.Fill.IsEmpty == false) { if(IsFirstParagraph) diff --git a/src/EPPlus.Export.ImageRenderer/RenderItems/Shared/TextBody.cs b/src/EPPlus.Export.ImageRenderer/RenderItems/Shared/TextBody.cs index 1a82a8f28..16a85c3bf 100644 --- a/src/EPPlus.Export.ImageRenderer/RenderItems/Shared/TextBody.cs +++ b/src/EPPlus.Export.ImageRenderer/RenderItems/Shared/TextBody.cs @@ -5,6 +5,7 @@ using EPPlusImageRenderer.RenderItems; using OfficeOpenXml.Drawing; using OfficeOpenXml.Drawing.Theme; +using OfficeOpenXml.Interfaces.Drawing.Text; using OfficeOpenXml.Style; using OfficeOpenXml.Utils; using System; @@ -83,6 +84,41 @@ public void ImportTextBody(ExcelTextBody body) foreach (var paragraph in body.Paragraphs) { + if (paragraph == body.Paragraphs[0]) + { + //For the first line we always add ascent for Top-aligned but this should not be done for center vertical align + //However as paragraph and textRun should not have to deal with vertical align directly + //We wish to achieve the effect of not applying dy to the first textrun while not chaning anything + //thus: we change paragraphStartY to get around it. Should probably be solved in the textrun somehow + if (VerticalAlignment == eTextAnchoringType.Center && paragraph.TextRuns != null && paragraph.TextRuns.Count > 0) + { + var _measurer = paragraph._prd.Package.Settings.TextSettings.GenericTextMeasurerTrueType; + var spacingValue = GetParagraphAscendantSpacingInPixels( + paragraph.LineSpacing.LineSpacingType, paragraph.LineSpacing.Value, _measurer, out double multiplier); + + if(multiplier == -1) + { + paragraphStartY -= spacingValue; + } + else + { + var runFont = paragraph.TextRuns[0].GetMeasurementFont(); + var startFont = paragraph.DefaultRunProperties.GetMeasureFont(); + _measurer.SetFont(runFont); + + paragraphStartY -= multiplier * _measurer.GetBaseLine().PointToPixel(true); + + //Reset measurer font + _measurer.SetFont(startFont); + } + + + //paragraph.TextRuns[0].GetMeasurementFont() * + ////var baseLineSize = paragraph.TextRuns[0].FontSize.PointToPixel(); + ////paragraphStartY = paragraphStartY - baseLineSize; + } + } + ImportParagraph(paragraph, paragraphStartY); var addedPara = Paragraphs.Last(); paragraphStartY = addedPara.Bounds.Bottom; @@ -93,6 +129,20 @@ public void ImportTextBody(ExcelTextBody body) } } + private double GetParagraphAscendantSpacingInPixels(eDrawingTextLineSpacing lineSpacingType, double spacingValue, ITextMeasurerWrap fmExact, out double multiplier) + { + if (lineSpacingType == eDrawingTextLineSpacing.Exactly) + { + multiplier = -1; + return spacingValue.PointToPixel(); + } + else + { + multiplier = (spacingValue / 100); + return multiplier * fmExact.GetBaseLine().PointToPixel(); + } + } + //public void AddText(string text, FontMeasurerTrueType measurer) //{ // if (Paragraphs.Count == 0) @@ -168,8 +218,11 @@ private double GetAlignmentVertical() case eTextAnchoringType.Top: alignmentY = 0; break; + //Center means center of a Shape's ENTIRE bounding box height. + //Not center of the Inset Rectangle case eTextAnchoringType.Center: - var adjustedHeight = ((Bounds.Height - Bounds.Parent.Top) / 2); + var globalHeight = (DrawingRenderer.Bounds.Height / 2) + Bounds.Top+2; + var adjustedHeight = globalHeight - Bounds.GlobalY; alignmentY = adjustedHeight; break; diff --git a/src/EPPlus.Export.ImageRenderer/RenderItems/SvgItem/SvgTextBodyItem.cs b/src/EPPlus.Export.ImageRenderer/RenderItems/SvgItem/SvgTextBodyItem.cs index 895b7d5c6..eb23332ac 100644 --- a/src/EPPlus.Export.ImageRenderer/RenderItems/SvgItem/SvgTextBodyItem.cs +++ b/src/EPPlus.Export.ImageRenderer/RenderItems/SvgItem/SvgTextBodyItem.cs @@ -35,7 +35,7 @@ public override void Render(StringBuilder sb) //bb.FillOpacity = 0.5; //bb.Render(sb); - foreach (var item in Paragraphs) + foreach (SvgParagraphItem item in Paragraphs) { item.Render(sb); } diff --git a/src/EPPlus.Export.ImageRenderer/RenderItems/SvgRenderItem.cs b/src/EPPlus.Export.ImageRenderer/RenderItems/SvgRenderItem.cs index b44983ca9..1c5b4a629 100644 --- a/src/EPPlus.Export.ImageRenderer/RenderItems/SvgRenderItem.cs +++ b/src/EPPlus.Export.ImageRenderer/RenderItems/SvgRenderItem.cs @@ -36,10 +36,11 @@ public override void Render(StringBuilder sb) if (string.IsNullOrEmpty(FillColor) == false) { sb.Append($"fill=\"{FillColor}\" "); - if (FillOpacity != null && FillOpacity != 1) - { - sb.Append($"opacity=\"{FillOpacity.Value.ToString(CultureInfo.InvariantCulture)}\" "); - } + } + //If fill is null it may in e.g. Rect still get the color black which can have an opacity + if (FillOpacity != null && FillOpacity != 1) + { + sb.Append($"opacity=\"{FillOpacity.Value.ToString(CultureInfo.InvariantCulture)}\" "); } if (string.IsNullOrEmpty(FilterName) == false) { diff --git a/src/EPPlus/Drawing/Style/Text/ExcelParagraphTextRunBase.cs b/src/EPPlus/Drawing/Style/Text/ExcelParagraphTextRunBase.cs index 10e4fa6d9..63194313d 100644 --- a/src/EPPlus/Drawing/Style/Text/ExcelParagraphTextRunBase.cs +++ b/src/EPPlus/Drawing/Style/Text/ExcelParagraphTextRunBase.cs @@ -361,7 +361,7 @@ public float FontSize var v = GetXmlNodeDoubleNull(_sizePath); if (v == null) { - if(_dtr == null) + if(_dtr == null || _dtr.Size == int.MinValue) { return _prd.Package.Workbook.Styles.GetNormalStyle().Style.Font.Size; } From c676d447aff661b6ada0b195f40acc936a6ad268 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jan=20K=C3=A4llman?= Date: Mon, 26 Jan 2026 16:37:23 +0100 Subject: [PATCH 016/151] WIP:Fixed bounding boxes to tranform to inherit from transform --- .../TestTextContainer.cs | 2 +- .../ImageRenderer.cs | 12 +- .../RenderItems/RenderItem.cs | 7 +- ...ParagraphContainer.cs => ParagraphItem.cs} | 14 +- .../Shared/{TextBody.cs => TextBodyItem.cs} | 53 ++---- .../RenderItems/Shared/TextRunItem.cs | 19 ++- .../RenderItems/SvgGroupItem.cs | 59 +++++-- .../RenderItems/SvgItem/SvgParagraphItem.cs | 4 +- .../RenderItems/SvgItem/SvgTextBodyItem.cs | 39 ++--- .../RenderItems/SvgItem/SvgTextBoxItem.cs | 66 ++++++++ .../RenderItems/SvgItem/SvgTextRunItem.cs | 4 +- .../RenderItems/SvgRenderRectItem.cs | 6 +- .../ShapeDefinitions/ShapeDefinition.cs | 12 +- .../Svg/SvgChart.cs | 44 ++--- .../Svg/SvgChartAxis.cs | 23 +-- .../Svg/SvgChartLegend.cs | 5 +- .../Svg/SvgChartObject.cs | 21 ++- .../Svg/SvgChartTitle.cs | 52 +++--- .../Svg/SvgShape.cs | 47 +++--- .../Text/FontWrapContainer.cs | 2 +- .../Text/TextContainerBase.cs | 4 +- .../Utils/DrawingExtensions.cs | 4 +- src/EPPlus.Graphics.Tests/GrahpicsTests.cs | 44 ++--- src/EPPlus.Graphics/BoundingBox.cs | 156 +++++++----------- .../Drawing/TextMeasuring/ReadMeasureTests.cs | 2 +- 25 files changed, 381 insertions(+), 320 deletions(-) rename src/EPPlus.Export.ImageRenderer/RenderItems/Shared/{ParagraphContainer.cs => ParagraphItem.cs} (95%) rename src/EPPlus.Export.ImageRenderer/RenderItems/Shared/{TextBody.cs => TextBodyItem.cs} (72%) create mode 100644 src/EPPlus.Export.ImageRenderer/RenderItems/SvgItem/SvgTextBoxItem.cs diff --git a/src/EPPlus.Export.ImageRenderer.Test/TestTextContainer.cs b/src/EPPlus.Export.ImageRenderer.Test/TestTextContainer.cs index c3dfe4634..03f7fa1f6 100644 --- a/src/EPPlus.Export.ImageRenderer.Test/TestTextContainer.cs +++ b/src/EPPlus.Export.ImageRenderer.Test/TestTextContainer.cs @@ -118,7 +118,7 @@ public void TestNonStandardFontSizesMultiLineLargeFontGoudyStout() // FontMeasurerTrueType measurer = new FontMeasurerTrueType(12, "Aptos Narrow", FontSubFamily.Regular); // var body = new SvgTextBodyItem(shapeRect); - // body.Bounds.Transform.Name = "TxtBody"; + // body.Bounds.Name = "TxtBody"; // body.AddText("A new Paragraph", measurer); // body.AddText("Second paragraph", measurer); diff --git a/src/EPPlus.Export.ImageRenderer/ImageRenderer.cs b/src/EPPlus.Export.ImageRenderer/ImageRenderer.cs index 1e330a6f4..f6221cb93 100644 --- a/src/EPPlus.Export.ImageRenderer/ImageRenderer.cs +++ b/src/EPPlus.Export.ImageRenderer/ImageRenderer.cs @@ -198,8 +198,8 @@ internal SvgElement GenerateSvg(TextContainerBase container) def.AddChildElement(clipPath); var bb = new SvgElement("rect"); - bb.AddAttribute("x", container.Transform.Position.X); - bb.AddAttribute("y", container.Transform.Position.Y); + bb.AddAttribute("x", container.Position.X); + bb.AddAttribute("y", container.Position.Y); bb.AddAttribute("width", container.Width); bb.AddAttribute("height", container.Height); //bb.AddAttribute("fill", "blue"); @@ -210,16 +210,16 @@ internal SvgElement GenerateSvg(TextContainerBase container) var fontSizePx = 16d; var renderElement = new SvgElement("text"); - renderElement.AddAttribute("x", container.Transform.Position.X); - renderElement.AddAttribute("y", container.Transform.Position.Y + fontSizePx); + renderElement.AddAttribute("x", container.Position.X); + renderElement.AddAttribute("y", container.Position.Y + fontSizePx); renderElement.AddAttribute("_measurementFont-size", $"{fontSizePx}px"); renderElement.AddAttribute("clip-path", $"url(#{nameId})"); renderElement.Content = fullString; var bbVisual = new SvgElement("rect"); - bbVisual.AddAttribute("x", container.Transform.Position.X); - bbVisual.AddAttribute("y", container.Transform.Position.Y); + bbVisual.AddAttribute("x", container.Position.X); + bbVisual.AddAttribute("y", container.Position.Y); bbVisual.AddAttribute("width", container.Width); bbVisual.AddAttribute("height", container.Height); bbVisual.AddAttribute("fill", "blue"); diff --git a/src/EPPlus.Export.ImageRenderer/RenderItems/RenderItem.cs b/src/EPPlus.Export.ImageRenderer/RenderItems/RenderItem.cs index cb5c028c4..a2caf4536 100644 --- a/src/EPPlus.Export.ImageRenderer/RenderItems/RenderItem.cs +++ b/src/EPPlus.Export.ImageRenderer/RenderItems/RenderItem.cs @@ -26,6 +26,11 @@ namespace EPPlusImageRenderer.RenderItems internal abstract class RenderItem : RenderItemBase { internal protected DrawingBase DrawingRenderer { get; } + internal RenderItem(DrawingBase renderer) + { + DrawingRenderer = renderer; + } + internal RenderItem(DrawingBase renderer, BoundingBox parent) { Bounds.Parent = parent; @@ -40,7 +45,7 @@ internal virtual void GetBounds(out double il, out double it, out double ir, out ib = Bounds.Bottom; } - internal bool IsEndOfGroup { get; set; } = false; + //internal bool IsEndOfGroup { get; set; } = false; public string FillColor { get; set; } public string FilterName { get; set; } public DrawGradientFill GradientFill { get; set; } diff --git a/src/EPPlus.Export.ImageRenderer/RenderItems/Shared/ParagraphContainer.cs b/src/EPPlus.Export.ImageRenderer/RenderItems/Shared/ParagraphItem.cs similarity index 95% rename from src/EPPlus.Export.ImageRenderer/RenderItems/Shared/ParagraphContainer.cs rename to src/EPPlus.Export.ImageRenderer/RenderItems/Shared/ParagraphItem.cs index 2d79ac305..25e2e0dd3 100644 --- a/src/EPPlus.Export.ImageRenderer/RenderItems/Shared/ParagraphContainer.cs +++ b/src/EPPlus.Export.ImageRenderer/RenderItems/Shared/ParagraphItem.cs @@ -17,7 +17,7 @@ namespace EPPlus.Export.ImageRenderer.RenderItems.Shared { - internal abstract class ParagraphContainer : RenderItem + internal abstract class ParagraphItem : RenderItem { ITextMeasurerWrap _measurer; @@ -38,14 +38,14 @@ internal abstract class ParagraphContainer : RenderItem internal double ParagraphLineSpacing { get; private set; } internal List Runs { get; set; } = new List(); - public ParagraphContainer(DrawingBase renderer, BoundingBox parent) : base(renderer, parent) + public ParagraphItem(DrawingBase renderer, BoundingBox parent) : base(renderer, parent) { - Bounds.Transform.Name = "Paragraph"; + Bounds.Name = "Paragraph"; var defaultFont = new MeasurementFont { FontFamily = "Aptos Narrow", Size = 11, Style = MeasurementFontStyles.Regular }; _paragraphFont = defaultFont; } - public ParagraphContainer(DrawingBase renderer, BoundingBox parent, ExcelDrawingParagraph p) : base(renderer, parent) + public ParagraphItem(DrawingBase renderer, BoundingBox parent, ExcelDrawingParagraph p) : base(renderer, parent) { IsFirstParagraph = p == p._paragraphs[0]; @@ -72,7 +72,7 @@ public ParagraphContainer(DrawingBase renderer, BoundingBox parent, ExcelDrawing } //---Initialize Bounds / Margins-- - - Bounds.Transform.Name = "Paragraph"; + Bounds.Name = "Paragraph"; var indent = 48 * p.IndentLevel; _leftMargin = p.LeftMargin + p.Indent + indent; @@ -161,12 +161,12 @@ internal protected void AddRenderItemTextRun(ExcelParagraphTextRunBase origTxtRu public void AddText(string text, ExcelTextFont font) { var measurer = new FontMeasurerTrueType(); - var displayText = measurer.MeasureAndWrapText(text, font.GetMeasureFont(), Bounds.Parent.Width); + var displayText = measurer.MeasureAndWrapText(text, font.GetMeasureFont(), Bounds.Parent.Size.X); var container = CreateTextRun(text, font, Bounds, string.Join("\r\n", displayText.ToArray())); Runs.Add(container); - container.Bounds.Transform.Name = $"Container{Runs.Count}"; + container.Bounds.Name = $"Container{Runs.Count}"; } /// diff --git a/src/EPPlus.Export.ImageRenderer/RenderItems/Shared/TextBody.cs b/src/EPPlus.Export.ImageRenderer/RenderItems/Shared/TextBodyItem.cs similarity index 72% rename from src/EPPlus.Export.ImageRenderer/RenderItems/Shared/TextBody.cs rename to src/EPPlus.Export.ImageRenderer/RenderItems/Shared/TextBodyItem.cs index b181eeb57..05e007639 100644 --- a/src/EPPlus.Export.ImageRenderer/RenderItems/Shared/TextBody.cs +++ b/src/EPPlus.Export.ImageRenderer/RenderItems/Shared/TextBodyItem.cs @@ -3,6 +3,7 @@ using EPPlus.Graphics; using EPPlusImageRenderer; using EPPlusImageRenderer.RenderItems; +using EPPlusImageRenderer.Svg; using OfficeOpenXml.Drawing; using OfficeOpenXml.Drawing.Theme; using OfficeOpenXml.Style; @@ -18,9 +19,8 @@ namespace EPPlus.Export.ImageRenderer.RenderItems.Shared /// Margin left = X /// Margin right = Y /// - internal abstract class TextBody : RenderItem + internal abstract class TextBodyItem : DrawingObject { - //internal BoundingBox Bounds = new BoundingBox(); /// /// Shorthand for Bounds.Width /// @@ -35,7 +35,7 @@ internal abstract class TextBody : RenderItem internal eTextAnchoringType VerticalAlignment = eTextAnchoringType.Top; - internal abstract List Paragraphs { get; set; } + internal abstract List Paragraphs { get; set; } public bool AllowOverflow; @@ -43,9 +43,9 @@ internal abstract class TextBody : RenderItem private FontMeasurerTrueType _measurer = null; internal string _text; - public TextBody(DrawingBase renderer, BoundingBox parent) : base(renderer, parent) + public TextBodyItem(DrawingBase renderer, BoundingBox parent) : base(renderer, parent) { - Bounds.Transform.Name = "TxtBody"; + Bounds.Name = "TxtBody"; Bounds.Parent = parent; @@ -59,7 +59,7 @@ public void ImportParagraph(ExcelDrawingParagraph item, double startingY, string bool isFirst = Paragraphs.Count == 0; var paragraph = CreateParagraph(item, Bounds); - paragraph.Bounds.Transform.Name = $"Container{Paragraphs.Count}"; + paragraph.Bounds.Name = $"Container{Paragraphs.Count}"; if (string.IsNullOrEmpty(text) == false) { paragraph.AddText(text, item.DefaultRunProperties); @@ -69,16 +69,8 @@ public void ImportParagraph(ExcelDrawingParagraph item, double startingY, string Paragraphs.Add(paragraph); } - public void ImportTextBody(ExcelTextBody body) + internal virtual void ImportTextBody(ExcelTextBody body) { - double l, r, t, b; - body.GetInsetsOrDefaults(out l, out t, out r, out b); - - LeftMargin = l.PointToPixel(); - TopMargin = t.PointToPixel(); - RightMargin = r.PointToPixel(); - BottomMargin = b.PointToPixel(); - _text = null; VerticalAlignment = body.Anchor; //We already apply bounds top via the parent Transform @@ -130,31 +122,6 @@ public void SetMeasurer(FontMeasurerTrueType fontMeasurer) _measurer = fontMeasurer; } - /// - /// Same as X or Left - /// - internal double LeftMargin { get { return Bounds.Left; } set { Bounds.Left = value; } } - - /// - /// Same as Y or Top - /// - internal double TopMargin { get { return Bounds.Top; } set { Bounds.Top = value; } } - - double _rightMargin = 0; - - /// - /// Setting this property also changes the width of the textbody - /// - internal double RightMargin { get { return _rightMargin; } set { _rightMargin = value; Bounds.Width = Bounds.Parent.Width - value; } } - - double _bottomMargin = 0; - /// - /// Setting this property also changes the height of the textbody - /// - internal double BottomMargin { get { return _bottomMargin; } set { _bottomMargin = value; Bounds.Height = Bounds.Parent.Height - value; } } - - public override RenderItemType Type => RenderItemType.Text; - double? _alignmentY = null; /// @@ -172,7 +139,7 @@ private double GetAlignmentVertical() alignmentY = 0; break; case eTextAnchoringType.Center: - var adjustedHeight = ((Bounds.Height - Bounds.Parent.Top) / 2); + var adjustedHeight = ((Bounds.Height - Bounds.Parent.LocalPosition.Y) / 2); alignmentY = adjustedHeight; break; @@ -191,12 +158,12 @@ private double GetAlignmentVertical() /// Each file format defines its own paragraph /// /// - internal abstract ParagraphContainer CreateParagraph(ExcelDrawingParagraph paragraph, BoundingBox parent); + internal abstract ParagraphItem CreateParagraph(ExcelDrawingParagraph paragraph, BoundingBox parent); /// /// Each file format defines its own paragraph /// /// - internal abstract ParagraphContainer CreateParagraph(BoundingBox parent); + internal abstract ParagraphItem CreateParagraph(BoundingBox parent); } } diff --git a/src/EPPlus.Export.ImageRenderer/RenderItems/Shared/TextRunItem.cs b/src/EPPlus.Export.ImageRenderer/RenderItems/Shared/TextRunItem.cs index e1ba70cb1..290d3db6f 100644 --- a/src/EPPlus.Export.ImageRenderer/RenderItems/Shared/TextRunItem.cs +++ b/src/EPPlus.Export.ImageRenderer/RenderItems/Shared/TextRunItem.cs @@ -59,7 +59,7 @@ internal TextRunItem(DrawingBase renderer, BoundingBox parent, string text, Exce { _originalText = text; - Bounds.Transform.Name = "TextRun"; + Bounds.Name = "TextRun"; _currentText = string.IsNullOrEmpty(displayText) ? _originalText : displayText; Lines = Regex.Split(_currentText, "\r\n|\r|\n").ToList(); @@ -89,9 +89,16 @@ internal TextRunItem(DrawingBase renderer, BoundingBox parent, string text, Exce //To get clipping height we need to get the textbody bounds if (parent != null && parent.Parent != null && parent.Parent.Parent != null) { - ClippingHeight = parent.Parent.Parent.Bottom; + ClippingHeight = parent.Parent.Parent.LocalPosition.Y + parent.Parent.Parent.Size.Y; + } + if(Lines.Count==1) + { + Bounds.Width = parent.Width; + } + else + { + //Measure text. } - _isItalic = font.Italic; _isBold = font.Bold; _underLineType = font.UnderLine; @@ -109,7 +116,7 @@ internal TextRunItem(DrawingBase renderer, BoundingBox parent, ExcelParagraphTex { _originalText = run.Text; - Bounds.Transform.Name = "TextRun"; + Bounds.Name = "TextRun"; _currentText = string.IsNullOrEmpty(displayText) ? _originalText : displayText; Lines = Regex.Split(_currentText, "\r\n|\r|\n").ToList(); @@ -136,7 +143,7 @@ internal TextRunItem(DrawingBase renderer, BoundingBox parent, ExcelParagraphTex //To get clipping height we need to get the textbody bounds if( parent!= null && parent.Parent != null && parent.Parent.Parent != null) { - ClippingHeight = parent.Parent.Parent.Bottom; + ClippingHeight = ((BoundingBox)parent.Parent.Parent).Bottom; } _isItalic = run.FontItalic; @@ -227,7 +234,7 @@ internal override void GetBounds(out double il, out double it, out double ir, ou //Caculate bounds right ir = CalculateRightPositionInPixels(); - Bounds.Right = ir; + Bounds.Width = ir-Bounds.Left; } internal double CalculateRightPositionInPixels() diff --git a/src/EPPlus.Export.ImageRenderer/RenderItems/SvgGroupItem.cs b/src/EPPlus.Export.ImageRenderer/RenderItems/SvgGroupItem.cs index d9ec234ca..c02d9f86a 100644 --- a/src/EPPlus.Export.ImageRenderer/RenderItems/SvgGroupItem.cs +++ b/src/EPPlus.Export.ImageRenderer/RenderItems/SvgGroupItem.cs @@ -13,37 +13,73 @@ Date Author Change using EPPlus.Graphics; using EPPlusImageRenderer.Svg; using OfficeOpenXml.Drawing.Theme; +using OfficeOpenXml.FormulaParsing.Excel.Functions.MathFunctions; using System.Drawing; using System.Globalization; using System.Text; namespace EPPlusImageRenderer.RenderItems { - internal class SvgGroupItem : SvgRenderItem + internal class SvgEndGroupItem : RenderItem + { + internal SvgEndGroupItem(DrawingBase renderer, BoundingBox parent) : base(renderer, parent) + { + + } + public override RenderItemType Type => throw new System.NotImplementedException(); + public override void Render(StringBuilder sb) + { + sb.Append(""); + } + } + internal class SvgGroupItem : RenderItem { public override RenderItemType Type => RenderItemType.Group; public string GroupTransform = ""; - internal SvgGroupItem(DrawingBase renderer, BoundingBox parent) : base(renderer, parent) + internal SvgGroupItem(DrawingBase renderer, BoundingBox bounds) : base(renderer) { - + Bounds = bounds; + if (Bounds.Left != 0 || Bounds.Top != 0) + { + GroupTransform = $"tranform=\"translate({Bounds.Left.ToString(CultureInfo.InvariantCulture)}, {Bounds.Top.ToString(CultureInfo.InvariantCulture)})\""; + } } internal SvgGroupItem(DrawingBase renderer, BoundingBox parent, double rotation) : base(renderer, parent) { + var bounds = ""; + var rot = ""; + if(Bounds.Left!=0 || Bounds.Top!=0) + { + bounds = $"translate({Bounds.Left.ToString(CultureInfo.InvariantCulture)}, {Bounds.Top.ToString(CultureInfo.InvariantCulture)})"; + } if(rotation!=0) { var cx = parent.Width / 2 + parent.Left; var cy = parent.Height / 2 + parent.Top; if (cx == 0 && cy == 0) { - GroupTransform = $"transform=\"rotate({rotation}))\""; + rot = $"rotate({rotation.ToString(CultureInfo.InvariantCulture)}))"; } else { - GroupTransform = $"transform=\"rotate({rotation}, {cx.ToString(CultureInfo.InvariantCulture)}, {cy.ToString(CultureInfo.InvariantCulture)})\""; + rot = $"rotate({rotation.ToString(CultureInfo.InvariantCulture)}, {cx.ToString(CultureInfo.InvariantCulture)}, {cy.ToString(CultureInfo.InvariantCulture)})"; } } + + if(string.IsNullOrEmpty(bounds)==false && string.IsNullOrEmpty(rot)==false) + { + GroupTransform = $"transform=\"{bounds} {rot}\""; + } + else if(string.IsNullOrEmpty(bounds)==false) + { + GroupTransform = $"transform=\"{bounds}\""; + } + else if (string.IsNullOrEmpty(rot) == false) + { + GroupTransform = $"transform=\"{rot}\""; + } } public override void Render(StringBuilder sb) @@ -57,16 +93,11 @@ public override void Render(StringBuilder sb) sb.Append($""); } } - internal void RenderEndGroup(StringBuilder sb) - { - sb.Append($""); - } - - internal override SvgRenderItem Clone(SvgShape svgDocument) - { - return this.Clone(svgDocument); - } + //internal override SvgRenderItem Clone(SvgShape svgDocument) + //{ + // return this.Clone(svgDocument); + //} internal override void GetBounds(out double il, out double it, out double ir, out double ib) { diff --git a/src/EPPlus.Export.ImageRenderer/RenderItems/SvgItem/SvgParagraphItem.cs b/src/EPPlus.Export.ImageRenderer/RenderItems/SvgItem/SvgParagraphItem.cs index af497a6b7..bbb87016e 100644 --- a/src/EPPlus.Export.ImageRenderer/RenderItems/SvgItem/SvgParagraphItem.cs +++ b/src/EPPlus.Export.ImageRenderer/RenderItems/SvgItem/SvgParagraphItem.cs @@ -11,7 +11,7 @@ namespace EPPlus.Export.ImageRenderer.RenderItems.SvgItem { - internal class SvgParagraphItem : ParagraphContainer + internal class SvgParagraphItem : ParagraphItem { public override RenderItemType Type => RenderItemType.Paragraph; @@ -75,7 +75,7 @@ public override void Render(StringBuilder sb) { var fontSize = _paragraphFont.Size.PointToPixel().ToString(CultureInfo.InvariantCulture); - sb.AppendLine($""); + sb.AppendLine($""); sb.AppendLine("paragraph "); diff --git a/src/EPPlus.Export.ImageRenderer/RenderItems/SvgItem/SvgTextBodyItem.cs b/src/EPPlus.Export.ImageRenderer/RenderItems/SvgItem/SvgTextBodyItem.cs index 895b7d5c6..e62be4563 100644 --- a/src/EPPlus.Export.ImageRenderer/RenderItems/SvgItem/SvgTextBodyItem.cs +++ b/src/EPPlus.Export.ImageRenderer/RenderItems/SvgItem/SvgTextBodyItem.cs @@ -7,47 +7,44 @@ using OfficeOpenXml.Drawing.Theme; using System; using System.Collections.Generic; +using System.Drawing; using System.Globalization; +using System.Linq; using System.Text; namespace EPPlus.Export.ImageRenderer.RenderItems.SvgItem { - internal class SvgTextBodyItem : TextBody + internal class SvgTextBodyItem : TextBodyItem { - public SvgTextBodyItem(DrawingBase renderer, BoundingBox parent) : base(renderer, parent) + public SvgTextBodyItem(DrawingBase renderer, BoundingBox parent, bool clampedToParent = false) : base(renderer, parent) { + Bounds.ClampedToParent = clampedToParent; } - internal override List Paragraphs { get; set; } = new List(); + internal override List Paragraphs { get; set; } = new List(); - public override void Render(StringBuilder sb) + internal override void AppendRenderItems(List renderItems) { - sb.AppendLine($""); - sb.AppendLine($"txtBody"); - - //var bb = new SvgRenderRectItem(Bounds); - //bb.X = 0; - //bb.Width = Width; - //bb.Height = Height; - //bb.FillColor = "red"; - //bb.FillOpacity = 0.5; - //bb.Render(sb); - + //sb.AppendLine($""); + //sb.AppendLine($"txtBody"); + + var groupItem = new SvgGroupItem(DrawingRenderer, Bounds, Bounds.Rotation); + renderItems.Add(groupItem); foreach (var item in Paragraphs) { - item.Render(sb); + renderItems.Add(item); } - sb.AppendLine(""); + renderItems.Add(new SvgEndGroupItem(DrawingRenderer, Bounds)); } - internal override ParagraphContainer CreateParagraph(BoundingBox parent) + internal override ParagraphItem CreateParagraph(BoundingBox parent) { return new SvgParagraphItem(DrawingRenderer, parent); } - internal override ParagraphContainer CreateParagraph(ExcelDrawingParagraph paragraph, BoundingBox parent) + internal override ParagraphItem CreateParagraph(ExcelDrawingParagraph paragraph, BoundingBox parent) { return new SvgParagraphItem(DrawingRenderer, parent, paragraph); } diff --git a/src/EPPlus.Export.ImageRenderer/RenderItems/SvgItem/SvgTextBoxItem.cs b/src/EPPlus.Export.ImageRenderer/RenderItems/SvgItem/SvgTextBoxItem.cs new file mode 100644 index 000000000..f3f8d49a9 --- /dev/null +++ b/src/EPPlus.Export.ImageRenderer/RenderItems/SvgItem/SvgTextBoxItem.cs @@ -0,0 +1,66 @@ +using EPPlus.Fonts.OpenType.Utils; +using EPPlus.Graphics; +using EPPlusImageRenderer; +using EPPlusImageRenderer.RenderItems; +using EPPlusImageRenderer.Svg; +using OfficeOpenXml.Drawing; +using OfficeOpenXml.FormulaParsing.Excel.Functions.Text; +using System.Collections.Generic; +using System.Xml.Serialization; + +namespace EPPlus.Export.ImageRenderer.RenderItems.SvgItem +{ + internal class SvgTextBoxItem : DrawingObject + { + internal SvgTextBoxItem(DrawingBase renderer, BoundingBox parent, double left, double top, double maxWidth, double maxHeight) : base(renderer, parent) + { + Bounds.Left = left; + Bounds.Top = top; + Bounds.Width = maxWidth; + Bounds.Height = maxHeight; + TextBody = new SvgTextBodyItem(renderer, Bounds, true); + Bounds.Width = TextBody.Width; + Bounds.Height = TextBody.Height; + + Rectangle = new SvgRenderRectItem(renderer, parent); + Rectangle.Bounds = Bounds; + } + public RenderItem Rectangle { get; set; } + public SvgTextBodyItem TextBody {get;set;} + internal double LeftMargin { get { return TextBody.Bounds.Left; } set { TextBody.Bounds.Left = value; } } + + internal double TopMargin { get { return TextBody.Bounds.Top; } set { TextBody.Bounds.Top = value; } } + + internal double RightMargin { get { return Bounds.Width - TextBody.Bounds.Left - TextBody.Bounds.Width ; } set { TextBody.Bounds.Width = Bounds.Width - TextBody.Bounds.Left - value; } } + internal double BottomMargin { get { return Bounds.Height - TextBody.Bounds.Top - TextBody.Bounds.Height; } set { TextBody.Bounds.Height = Bounds.Height - TextBody.Bounds.Top - value; } } + internal void ImportTextBody(ExcelTextBody body) + { + double l, r, t, b; + body.GetInsetsOrDefaults(out l, out t, out r, out b); + LeftMargin = l.PointToPixel(); + TopMargin = t.PointToPixel(); + RightMargin = r.PointToPixel(); + BottomMargin = b.PointToPixel(); + + TextBody.ImportTextBody(body); + } + + + internal override void AppendRenderItems(List renderItems) + { + SvgGroupItem groupItem; + if (Bounds.Rotation == 0) + { + groupItem = new SvgGroupItem(DrawingRenderer, Bounds); + } + else + { + groupItem = new SvgGroupItem(DrawingRenderer, Bounds, Bounds.Rotation); + } + renderItems.Add(groupItem); + renderItems.Add(Rectangle); + TextBody.AppendRenderItems(renderItems); + renderItems.Add(new SvgEndGroupItem(DrawingRenderer, Bounds)); + } + } +} diff --git a/src/EPPlus.Export.ImageRenderer/RenderItems/SvgItem/SvgTextRunItem.cs b/src/EPPlus.Export.ImageRenderer/RenderItems/SvgItem/SvgTextRunItem.cs index 5d0d7d262..e2e81d959 100644 --- a/src/EPPlus.Export.ImageRenderer/RenderItems/SvgItem/SvgTextRunItem.cs +++ b/src/EPPlus.Export.ImageRenderer/RenderItems/SvgItem/SvgTextRunItem.cs @@ -85,9 +85,9 @@ public override void Render(StringBuilder sb) { CalculateLineSpacing(); string finalString = ""; - var xString = $"x =\"{(Bounds.X).ToString(CultureInfo.InvariantCulture)}\" "; + var xString = $"x =\"{(Bounds.Left).ToString(CultureInfo.InvariantCulture)}\" "; - var currentYEndPos = Bounds.GlobalY; + var currentYEndPos = Bounds.Position.Y; // Global position Y for (int i = 0; i < Lines.Count; i++) { diff --git a/src/EPPlus.Export.ImageRenderer/RenderItems/SvgRenderRectItem.cs b/src/EPPlus.Export.ImageRenderer/RenderItems/SvgRenderRectItem.cs index 4078eb6c9..bfedf9249 100644 --- a/src/EPPlus.Export.ImageRenderer/RenderItems/SvgRenderRectItem.cs +++ b/src/EPPlus.Export.ImageRenderer/RenderItems/SvgRenderRectItem.cs @@ -46,12 +46,12 @@ public SvgRenderRectItem(DrawingBase renderer, BoundingBox parent) : base(render public override void Render(StringBuilder sb) { - var groupItem = new SvgGroupItem(DrawingRenderer, Bounds); - groupItem.Render(sb); + //var groupItem = new SvgGroupItem(DrawingRenderer, Bounds); + //groupItem.Render(sb); RenderRect(sb); - groupItem.RenderEndGroup(sb); + //groupItem.RenderEndGroup(sb); } internal void RenderRect(StringBuilder sb) diff --git a/src/EPPlus.Export.ImageRenderer/ShapeDefinitions/ShapeDefinition.cs b/src/EPPlus.Export.ImageRenderer/ShapeDefinitions/ShapeDefinition.cs index 10aef933a..a7f200384 100644 --- a/src/EPPlus.Export.ImageRenderer/ShapeDefinitions/ShapeDefinition.cs +++ b/src/EPPlus.Export.ImageRenderer/ShapeDefinitions/ShapeDefinition.cs @@ -191,7 +191,7 @@ public void Calculate(ExcelShape shape) //List txtContainers = new List(); ////TODO: This needs to actually iterate through the individual paragraphs/txtRuns when differing fonts/font sizes exist - ////foreach(var paragraph in shape.TextBody.Paragraphs) + ////foreach(var paragraph in shape.TextBodyItem.Paragraphs) ////{ //// foreach(var txtRun in paragraph.TextRuns) //// { @@ -199,7 +199,7 @@ public void Calculate(ExcelShape shape) //// txtContainers.Add(newContainer); //// } ////} - //var newContainer = new TextContainer(txt, shape.TextBody.Paragraphs.FirstDefaultRunProperties.GetMeasureFont(), true); + //var newContainer = new TextContainer(txt, shape.TextBodyItem.Paragraphs.FirstDefaultRunProperties.GetMeasureFont(), true); //var cW = newContainer.Width; //var cH = newContainer.Height; @@ -317,12 +317,12 @@ private void InitCalculatedValues(ExcelShape shape) //var hOld = adjustedHeight; //var wOld = adjustedWidth; - //if (shape.TextBody.TextAutofit == eTextAutofit.ShapeAutofit) + //if (shape.TextBodyItem.TextAutofit == eTextAutofit.ShapeAutofit) //{ // var txt = shape.Textbox; - // var newContainer = new TextContainer(txt, shape.TextBody.Paragraphs.FirstDefaultRunProperties.GetMeasureFont(), true); + // var newContainer = new TextContainer(txt, shape.TextBodyItem.Paragraphs.FirstDefaultRunProperties.GetMeasureFont(), true); // adjustedHeight = newContainer.Height; // adjustedWidth = newContainer.Width; @@ -348,9 +348,9 @@ private void InitCalculatedValues(ExcelShape shape) //double largestHeight = -1; //double largestWidth = -1; - //if(shape.TextBody.TextAutofit == eTextAutofit.ShapeAutofit) + //if(shape.TextBodyItem.TextAutofit == eTextAutofit.ShapeAutofit) //{ - // foreach( var paragraph in shape.TextBody.Paragraphs) + // foreach( var paragraph in shape.TextBodyItem.Paragraphs) // { // foreach(var txtRun in paragraph.TextRuns) // { diff --git a/src/EPPlus.Export.ImageRenderer/Svg/SvgChart.cs b/src/EPPlus.Export.ImageRenderer/Svg/SvgChart.cs index e684ebac6..faa347547 100644 --- a/src/EPPlus.Export.ImageRenderer/Svg/SvgChart.cs +++ b/src/EPPlus.Export.ImageRenderer/Svg/SvgChart.cs @@ -149,16 +149,16 @@ public void Render(StringBuilder sb) { Plotarea?.AppendRenderItems(RenderItems); - HorizontalAxis?.AppendRenderItems(RenderItems); - VerticalAxis?.AppendRenderItems(RenderItems); - SecondVerticalAxis?.AppendRenderItems(RenderItems); + //HorizontalAxis?.AppendRenderItems(RenderItems); + //VerticalAxis?.AppendRenderItems(RenderItems); + //SecondVerticalAxis?.AppendRenderItems(RenderItems); - foreach (var drawer in Plotarea?.ChartTypeDrawers) - { - drawer.AppendRenderItems(RenderItems); - } + //foreach (var drawer in Plotarea?.ChartTypeDrawers) + //{ + // drawer.AppendRenderItems(RenderItems); + //} - Legend?.AppendRenderItems(RenderItems); + //Legend?.AppendRenderItems(RenderItems); Title?.AppendRenderItems(RenderItems); sb.Append($""); @@ -166,24 +166,24 @@ public void Render(StringBuilder sb) var writer = new SvgDrawingWriter(this); writer.WriteSvgDefs(sb, RenderItems); - SvgGroupItem gItemTest = null; + //SvgGroupItem gItemTest = null; foreach (var item in RenderItems) { item.Render(sb); - if (item.Type == RenderItemType.Group && gItemTest == null) - { - gItemTest = (SvgGroupItem)item; - } - if(item.IsEndOfGroup && gItemTest != null) - { - gItemTest.RenderEndGroup(sb); - gItemTest = null; - } - } - if(gItemTest != null) - { - gItemTest.RenderEndGroup(sb); + //if (item.Type == RenderItemType.Group && gItemTest == null) + //{ + // gItemTest = (SvgGroupItem)item; + //} + //if(item.IsEndOfGroup && gItemTest != null) + //{ + // gItemTest.RenderEndGroup(sb); + // gItemTest = null; + //} } + //if(gItemTest != null) + //{ + // gItemTest.RenderEndGroup(sb); + //} sb.Append(""); } diff --git a/src/EPPlus.Export.ImageRenderer/Svg/SvgChartAxis.cs b/src/EPPlus.Export.ImageRenderer/Svg/SvgChartAxis.cs index b5c16b4b6..0ebcf0d68 100644 --- a/src/EPPlus.Export.ImageRenderer/Svg/SvgChartAxis.cs +++ b/src/EPPlus.Export.ImageRenderer/Svg/SvgChartAxis.cs @@ -181,7 +181,7 @@ public List Values public List MinorAxisPositions { get; private set; } public List MajorGridlinePositions { get; private set; } public List MinorGridlinePositions { get; private set; } - public List AxisValuesTextBoxes + public List AxisValuesTextBoxes { get; private set; @@ -234,7 +234,8 @@ internal override void AppendRenderItems(List renderItems) { foreach (var tb in AxisValuesTextBoxes) { - renderItems.Add(tb); + //renderItems.Add(); + tb.AppendRenderItems(renderItems); } } @@ -277,12 +278,12 @@ internal void AddTickmarksAndValues() } } - private List GetAxisValueTextBoxes() + private List GetAxisValueTextBoxes() { var tm = Chart.WorkSheet._package.Settings.TextSettings.GenericTextMeasurerTrueType; var mf = Axis.Font.GetMeasureFont(); var axisStyle = GetAxisStyleEntry(); - var ret= new List(); + var ret= new List(); for (var i=0;i < AxisValues.Count;i++) { var v = AxisValues[i]; @@ -295,10 +296,10 @@ private List GetAxisValueTextBoxes() bounds.Top = y; bounds.Width = m.Width.PointToPixel(); bounds.Height = m.Height.PointToPixel(); - var tb = new SvgTextBodyItem(SvgChart, bounds); + var tb = new SvgTextBoxItem(SvgChart, bounds, x, y, bounds.Width, bounds.Height); tb.ImportTextBody(Axis.TextBody); - tb.Paragraphs[0].AddText(v, Axis.Font); - tb.SetDrawingPropertiesFill(Axis.Fill, axisStyle.FillReference.Color); + tb.TextBody.Paragraphs[0].AddText(v, Axis.Font); + //tb.SetDrawingPropertiesFill(Axis.Fill, axisStyle.FillReference.Color); ret.Add(tb); } return ret; @@ -508,14 +509,14 @@ internal double GetPositionInPlotarea(object val) //var index = Values.IndexOf(val); //if (index < 0) return -1; var majorHeight = SvgChart.Plotarea.Rectangle.Height / Max; - return (SvgChart.Plotarea.Rectangle.Top + majorHeight * (int)val + (majorHeight / 2)); + return (/*SvgChart.Plotarea.Rectangle.Top +*/ majorHeight * (int)val + (majorHeight / 2)); } else { var dVal = ConvertUtil.GetValueDouble(val, false, true); if (dVal < Min || dVal > Max) return double.NaN; var diff = Max - Min; - return (SvgChart.Plotarea.Rectangle.Top + ((Max-dVal) / diff * SvgChart.Plotarea.Rectangle.Height)); + return (/*SvgChart.Plotarea.Rectangle.Top + */((Max-dVal) / diff * SvgChart.Plotarea.Rectangle.Height)); } } else @@ -525,14 +526,14 @@ internal double GetPositionInPlotarea(object val) //var index = Values[(int)val]; //if (index < 0) return -1; var majorWidth = SvgChart.Plotarea.Rectangle.Width / Max; - return (SvgChart.Plotarea.Rectangle.Left + majorWidth * (int)val + (majorWidth / 2)); + return (/*SvgChart.Plotarea.Rectangle.Left + */majorWidth * (int)val + (majorWidth / 2)); } else { var dVal = ConvertUtil.GetValueDouble(val, false, true); if (dVal < Min || dVal > Max) return double.NaN; var diff = Max - Min; - return (SvgChart.Plotarea.Rectangle.Left + ((Max - dVal) / diff * SvgChart.Plotarea.Rectangle.Width)); + return (/*SvgChart.Plotarea.Rectangle.Left + */((Max - dVal) / diff * SvgChart.Plotarea.Rectangle.Width)); } } } diff --git a/src/EPPlus.Export.ImageRenderer/Svg/SvgChartLegend.cs b/src/EPPlus.Export.ImageRenderer/Svg/SvgChartLegend.cs index 7f995696a..f1fc40b0c 100644 --- a/src/EPPlus.Export.ImageRenderer/Svg/SvgChartLegend.cs +++ b/src/EPPlus.Export.ImageRenderer/Svg/SvgChartLegend.cs @@ -291,7 +291,8 @@ internal override void AppendRenderItems(List renderItems) renderItems.Add(s.SeriesIcon); if(s.MarkerBackground != null) renderItems.Add(s.MarkerBackground); if (s.MarkerIcon != null) renderItems.Add(s.MarkerIcon); - renderItems.Add(s.Textbox); + //renderItems.Add(s.Textbox); + s.Textbox.AppendRenderItems(renderItems); } } @@ -303,6 +304,6 @@ internal class SvgLegendSerie internal RenderItem SeriesIcon { get; set; } internal RenderItem MarkerIcon { get; set; } internal RenderItem MarkerBackground { get; set; } - internal TextBody Textbox { get; set;} + internal TextBodyItem Textbox { get; set;} } } \ No newline at end of file diff --git a/src/EPPlus.Export.ImageRenderer/Svg/SvgChartObject.cs b/src/EPPlus.Export.ImageRenderer/Svg/SvgChartObject.cs index ee32cbd81..ae0997f38 100644 --- a/src/EPPlus.Export.ImageRenderer/Svg/SvgChartObject.cs +++ b/src/EPPlus.Export.ImageRenderer/Svg/SvgChartObject.cs @@ -11,18 +11,32 @@ Date Author Change 27/11/2025 EPPlus Software AB EPPlus 9 *************************************************************************************************/ using EPPlus.Fonts.OpenType.Utils; +using EPPlus.Graphics; using EPPlusImageRenderer.RenderItems; using OfficeOpenXml.Drawing; using OfficeOpenXml.Drawing.Chart; +using OfficeOpenXml.FormulaParsing.Excel.Functions.MathFunctions; using System.Collections.Generic; namespace EPPlusImageRenderer.Svg { - internal abstract class SvgChartObject + internal abstract class DrawingObject + { + internal protected DrawingBase DrawingRenderer { get; } + internal BoundingBox Bounds { get; set; } + + protected DrawingObject(DrawingBase renderer, BoundingBox parent) + { + DrawingRenderer = renderer; + Bounds = new BoundingBox() { Parent=parent}; + } + internal abstract void AppendRenderItems(List renderItems); + } + internal abstract class SvgChartObject : DrawingObject { internal DrawingChart ChartRenderer; internal ExcelChart Chart => (ExcelChart)ChartRenderer.Drawing; - internal SvgChartObject(DrawingChart chart) + internal SvgChartObject(DrawingChart chart) : base(chart, chart.Bounds) { ChartRenderer = chart; } @@ -40,7 +54,6 @@ internal void SetMargins(ExcelTextBody tb) internal double BottomMargin { get; set; } internal SvgRenderRectItem Rectangle { get; set; } internal SvgRenderLineItem Line { get; set; } - public string Text { get; set; } protected static SvgRenderRectItem GetRectFromManualLayout(SvgChart sc, ExcelLayout layout) { var rect = new SvgRenderRectItem(sc, sc.Bounds); @@ -68,7 +81,5 @@ protected static SvgRenderRectItem GetRectFromManualLayout(SvgChart sc, ExcelLay rect.Height = sc.Bounds.Height * ml.GetHeight() / 100; return rect; } - internal abstract void AppendRenderItems(List renderItems); - } } diff --git a/src/EPPlus.Export.ImageRenderer/Svg/SvgChartTitle.cs b/src/EPPlus.Export.ImageRenderer/Svg/SvgChartTitle.cs index dddccd946..3b8a009fa 100644 --- a/src/EPPlus.Export.ImageRenderer/Svg/SvgChartTitle.cs +++ b/src/EPPlus.Export.ImageRenderer/Svg/SvgChartTitle.cs @@ -84,7 +84,7 @@ internal SvgChartTitle(SvgChart sc, ExcelChartTitleStandard t, string defaultTex { Rectangle = new SvgRenderRectItem(sc, sc.Bounds); if (axis==null) - { + { Rectangle.Top = (float)8; //8 pixels for the chart title standard offset Rectangle.Left = (float)(sc.Bounds.Width - rect.Width) / 2; Rectangle.Height = (float)rect.Height; @@ -171,21 +171,21 @@ private static string GetDefaultChartTitleText(SvgChart sc, ExcelChartTitleStand internal void InitTextBox() { - TextBox = new SvgTextBodyItem(_svgChart, Rectangle.Bounds); - TextBox.Bounds.Left = LeftMargin; - TextBox.Bounds.Width -= LeftMargin + RightMargin; - TextBox.Bounds.Top = TopMargin; - TextBox.Bounds.Height -= TopMargin + BottomMargin; - TextBox.VerticalAlignment = eTextAnchoringType.Top; + TextBox = new SvgTextBoxItem(_svgChart, _svgChart.Bounds, Rectangle.Left, Rectangle.Top, Rectangle.Width, Rectangle.Height); + TextBox.LeftMargin = LeftMargin; + TextBox.RightMargin = RightMargin; + TextBox.TopMargin = TopMargin; + TextBox.BottomMargin = BottomMargin; + TextBox.TextBody.VerticalAlignment = eTextAnchoringType.Top; if(_title.Rotation != 0) { - TextBox.Bounds.Transform.Rotation = _title.Rotation; + TextBox.Bounds.Rotation = _title.Rotation; } if (_title.TextBody.Paragraphs.Count > 0) { foreach (var p in _title.TextBody.Paragraphs) { - TextBox.ImportParagraph(p, 0); + TextBox.TextBody.ImportParagraph(p, 0); } } else @@ -193,31 +193,33 @@ internal void InitTextBox() //TextBox.AddText(string.IsNullOrEmpty(_title.Text) ? _titleText : _title.Text, _title.Font); var text = string.IsNullOrEmpty(_title.Text) ? _titleText : _title.Text; var p = _title.DefaultTextBody.Paragraphs.FirstOrDefault(); - TextBox.ImportParagraph(p, 0, text); + TextBox.TextBody.ImportParagraph(p, 0, text); } } - public TextBody TextBox + public SvgTextBoxItem TextBox { get; private set; } internal override void AppendRenderItems(List renderItems) { - SvgGroupItem groupItem; - if (TextBox.Bounds.Transform.Rotation == 0) - { - groupItem = new SvgGroupItem(_svgChart, Rectangle.Bounds); - } - else - { - groupItem = new SvgGroupItem(_svgChart, Rectangle.Bounds, TextBox.Bounds.Transform.Rotation); - } + //SvgGroupItem groupItem; + //if (TextBox.Bounds.Rotation == 0) + //{ + // groupItem = new SvgGroupItem(_svgChart, Rectangle.Bounds); + //} + //else + //{ + // groupItem = new SvgGroupItem(_svgChart, Rectangle.Bounds, TextBox.Bounds.Rotation); + //} - renderItems.Add(groupItem); - renderItems.Add(Rectangle); - renderItems.Add(TextBox); - TextBox.IsEndOfGroup = true; + //renderItems.Add(groupItem); + //renderItems.Add(Rectangle); + TextBox.AppendRenderItems(renderItems); + TextBox.Rectangle.SetDrawingPropertiesFill(_title.Fill, _svgChart.Chart.StyleManager.Style.Title.FillReference.Color); + TextBox.Rectangle.SetDrawingPropertiesBorder(_title.Border, _svgChart.Chart.StyleManager.Style.Title.BorderReference.Color, _title.Border.Fill.Style != eFillStyle.NoFill, 0.75); + //renderItems.Add(new SvgEndGroupItem(DrawingRenderer, Bounds)); } - } + } } \ No newline at end of file diff --git a/src/EPPlus.Export.ImageRenderer/Svg/SvgShape.cs b/src/EPPlus.Export.ImageRenderer/Svg/SvgShape.cs index 6608d4364..add00bf66 100644 --- a/src/EPPlus.Export.ImageRenderer/Svg/SvgShape.cs +++ b/src/EPPlus.Export.ImageRenderer/Svg/SvgShape.cs @@ -102,6 +102,7 @@ public SvgShape(ExcelShape shape) : base(shape) textBody = CreateTextBodyItem(); textBody.ImportTextBody(_shape.TextBody); + textBody.AppendRenderItems(RenderItems); } } } @@ -211,28 +212,28 @@ public void Render(StringBuilder sb) var writer = new SvgDrawingWriter(this); writer.WriteSvgDefs(sb, RenderItems); - SvgGroupItem gItemTest = null; + //SvgGroupItem gItemTest = null; foreach(var item in RenderItems) { item.Render(sb); - if(item.Type == RenderItemType.Group && gItemTest == null) - { - gItemTest = (SvgGroupItem)item; - } - if (item.IsEndOfGroup && gItemTest != null) - { - gItemTest.RenderEndGroup(sb); - } - } - if (!string.IsNullOrEmpty(_shape.Text)) - { - RenderText(sb); - } - - if (gItemTest != null) - { - gItemTest.RenderEndGroup(sb); + //if(item.Type == RenderItemType.Group && gItemTest == null) + //{ + // gItemTest = (SvgGroupItem)item; + //} + //if (item.IsEndOfGroup && gItemTest != null) + //{ + // gItemTest.RenderEndGroup(sb); + //} } + //if (!string.IsNullOrEmpty(_shape.Text)) + //{ + // //RenderText(sb); + //} + + //if (gItemTest != null) + //{ + // gItemTest.RenderEndGroup(sb); + //} sb.AppendLine(""); } @@ -253,11 +254,11 @@ SvgTextBodyItem CreateTextBodyItem() return txtBodyItem; } - private void RenderText(StringBuilder sb) - { - //RenderDebugTextBox(sb); - textBody.Render(sb); - } + //private void RenderText(StringBuilder sb) + //{ + // //RenderDebugTextBox(sb); + // textBody.Render(sb); + //} private void RenderDebugTextBox(StringBuilder sb) { diff --git a/src/EPPlus.Export.ImageRenderer/Text/FontWrapContainer.cs b/src/EPPlus.Export.ImageRenderer/Text/FontWrapContainer.cs index 4b903bf78..3a2ddbe38 100644 --- a/src/EPPlus.Export.ImageRenderer/Text/FontWrapContainer.cs +++ b/src/EPPlus.Export.ImageRenderer/Text/FontWrapContainer.cs @@ -30,7 +30,7 @@ public double MaxWidthPixels { if(Parent != null) { - return Parent.Width; + return Parent.Size.X; } else { diff --git a/src/EPPlus.Export.ImageRenderer/Text/TextContainerBase.cs b/src/EPPlus.Export.ImageRenderer/Text/TextContainerBase.cs index 95f7e1fca..a812a6244 100644 --- a/src/EPPlus.Export.ImageRenderer/Text/TextContainerBase.cs +++ b/src/EPPlus.Export.ImageRenderer/Text/TextContainerBase.cs @@ -22,7 +22,7 @@ public TextContainerBase(bool initDefaults = true) { //Right and Bottom Pixel defaults for a Cell in excel at 96 PPI //(15pts height, 8.43pts width) - Left = 0; Top = 0; Right = 64; Bottom = 20d; + Left = 0; Top = 0; Width = 64; Height = 20d; SetContent("Some Text"); } } @@ -33,7 +33,7 @@ public TextContainerBase(string content, bool initDefaults = true) { //Right and Bottom Pixel defaults for a Cell in excel at 96 PPI //(15pts height, 8.43pts width) - Left = 0; Top = 0; Right = 64; Bottom = 20d; + Left = 0; Top = 0; Width = 64; Height = 20d; } SetContent(content); diff --git a/src/EPPlus.Export.ImageRenderer/Utils/DrawingExtensions.cs b/src/EPPlus.Export.ImageRenderer/Utils/DrawingExtensions.cs index d50d3a6d7..e702ba482 100644 --- a/src/EPPlus.Export.ImageRenderer/Utils/DrawingExtensions.cs +++ b/src/EPPlus.Export.ImageRenderer/Utils/DrawingExtensions.cs @@ -9,8 +9,8 @@ internal static BoundingBox GetBoundingBox(this ExcelDrawing drawing) { return new BoundingBox() { - Left = drawing.GetPixelLeft(), - Top = drawing.GetPixelTop(), + Left = 0, + Top = 0, Width = drawing.GetPixelWidth(), Height = drawing.GetPixelHeight() }; diff --git a/src/EPPlus.Graphics.Tests/GrahpicsTests.cs b/src/EPPlus.Graphics.Tests/GrahpicsTests.cs index 7a69ff5cb..65a4e57d7 100644 --- a/src/EPPlus.Graphics.Tests/GrahpicsTests.cs +++ b/src/EPPlus.Graphics.Tests/GrahpicsTests.cs @@ -137,52 +137,52 @@ public void BoundingBoxes() { BoundingBox Shape = new BoundingBox(); - Shape.Transform.Position = new Vector2(2,2); - Shape.Transform.Size = Vector2.One; - Shape.Transform.Name = "ShapeTransform"; + Shape.Position = new Vector2(2,2); + Shape.Size = Vector2.One; + Shape.Name = "ShapeTransform"; Shape.Width = 10; Shape.Height = 10; BoundingBox TextBody = new BoundingBox(); - TextBody.Transform.Parent = Shape.Transform; - TextBody.Transform.Name = "TextBodyTransform"; + TextBody.Parent = Shape; + TextBody.Name = "TextBodyTransform"; TextBody.Width = 20; TextBody.Height = 20; - TextBody.Transform.LocalPosition = new(10, 11); + TextBody.LocalPosition = new(10, 11); - Assert.AreEqual(10, TextBody.Transform.LocalPosition.X); - Assert.AreEqual(11, TextBody.Transform.LocalPosition.Y); + Assert.AreEqual(10, TextBody.LocalPosition.X); + Assert.AreEqual(11, TextBody.LocalPosition.Y); BoundingBox Paragraph1 = new BoundingBox(); - Paragraph1.Transform.Name = "Paragraph1Transform"; - Paragraph1.Transform.Parent = TextBody.Transform; + Paragraph1.Name = "Paragraph1Transform"; + Paragraph1.Parent = TextBody; Paragraph1.Width = 5; Paragraph1.Height = 5; - Paragraph1.Transform.LocalPosition = new Vector2(5, 8); + Paragraph1.LocalPosition = new Vector2(5, 8); - Assert.AreEqual(10, TextBody.Transform.LocalPosition.X); - Assert.AreEqual(11, TextBody.Transform.LocalPosition.Y); + Assert.AreEqual(10, TextBody.LocalPosition.X); + Assert.AreEqual(11, TextBody.LocalPosition.Y); - Assert.AreEqual(2, Shape.Transform.LocalPosition.X); - Assert.AreEqual(2, Shape.Transform.LocalPosition.Y); + Assert.AreEqual(2, Shape.LocalPosition.X); + Assert.AreEqual(2, Shape.LocalPosition.Y); - Assert.AreEqual(10, Shape.Transform.ChildObjects[0].LocalPosition.X); - Assert.AreEqual(11, Shape.Transform.ChildObjects[0].LocalPosition.Y); + Assert.AreEqual(10, Shape.ChildObjects[0].LocalPosition.X); + Assert.AreEqual(11, Shape.ChildObjects[0].LocalPosition.Y); - Assert.AreEqual(5, Paragraph1.Transform.LocalPosition.X); - Assert.AreEqual(8, Paragraph1.Transform.LocalPosition.Y); + Assert.AreEqual(5, Paragraph1.LocalPosition.X); + Assert.AreEqual(8, Paragraph1.LocalPosition.Y); - Assert.AreEqual(Paragraph1.Transform.Position.X, 17); - Assert.AreEqual(Paragraph1.Transform.Position.Y, 21); + Assert.AreEqual(Paragraph1.Position.X, 17); + Assert.AreEqual(Paragraph1.Position.Y, 21); - var str = Shape.Transform.ToHierarchyString(); + var str = Shape.ToHierarchyString(); } } } diff --git a/src/EPPlus.Graphics/BoundingBox.cs b/src/EPPlus.Graphics/BoundingBox.cs index 30f286d53..bc94157ba 100644 --- a/src/EPPlus.Graphics/BoundingBox.cs +++ b/src/EPPlus.Graphics/BoundingBox.cs @@ -1,159 +1,131 @@ -using System; +using EPPlus.Graphics.Math; +using System; using System.Collections.Generic; using System.Linq; using System.Text; namespace EPPlus.Graphics { - internal class BoundingBox : Rect + internal class BoundingBox : Transform { - internal Transform Transform { get; } + //internal Transform Transform { get; } - private BoundingBox _parent = null; + //private BoundingBox _parent = null; - internal BoundingBox Parent { get { return _parent; } set { _parent = value; Transform.Parent = value.Transform; } } + //internal BoundingBox Parent { get { return base.; } set { _parent = value; Transform.Parent = value.Transform; } } - bool ClampedToParent = false; + internal bool ClampedToParent { get; set; } = false; internal BoundingBox() : base() { - Transform = new Transform(); + //Transform = new Transform(); } - internal BoundingBox(double width, double height) : this() + internal BoundingBox(double width, double height) : base(0, 0, width, height) { - Left = 0; - Top = 0; - Right = width; - Bottom = height; + //Left = 0; + //Top = 0; + //Right = width; + //Bottom = height; } - internal BoundingBox(double left, double top, double right, double bottom) : this() + internal BoundingBox(double left, double top, double right, double bottom) : base(left,top, right-left, bottom-top) { - Left = left; - Top = top; - Right = right; - Bottom = bottom; + //Left = left; + //Top = top; + //Right = right; + //Bottom = bottom; } /// /// Y pos (min) /// - internal override double Top + internal double Top { - get { return Transform.LocalPosition.Y; } + get { return LocalPosition.Y; } set { - var tmpHeight = Height != 0 ? Height : 0; - - var currentPosition = Transform.LocalPosition; - currentPosition.Y = value; - Transform.LocalPosition = currentPosition; - - //Recalculate bottom position correctly - if (tmpHeight != 0) - { - Height = tmpHeight; - } + LocalPosition = new Vector2(LocalPosition.X, value); + //var tmpHeight = Height != 0 ? Height : 0; + + //var currentPosition = Transform.LocalPosition; + //currentPosition.Y = value; + //Transform.LocalPosition = currentPosition; + + ////Recalculate bottom position correctly + ////if (tmpHeight != 0) + ////{ + // //Height = tmpHeight; + // Bottom = Top + tmpHeight; + ////} } } /// /// X pos (min) /// - internal override double Left + internal double Left { - get { return Transform.LocalPosition.X; } + get { return LocalPosition.X; } set { - var tmpWidth = Width != 0 ? Width : 0; + //var tmpWidth = Width != 0 ? Width : 0; - var currentPosition = Transform.LocalPosition; - currentPosition.X = value; - Transform.LocalPosition = currentPosition; + //var currentPosition = Transform.LocalPosition; + //currentPosition.X = value; + //Transform.LocalPosition = currentPosition; //Recalculate Right position correctly - if (tmpWidth != 0) - { - Width = tmpWidth; - } + //if (tmpWidth != 0) + //{ + //Right = Left + tmpWidth; + //Width = tmpWidth; + //} + LocalPosition = new Vector2(value, LocalPosition.Y); } } /// /// If @ClampedToParent is true will not set value beyond parent /// - internal override double Bottom { get => base.Bottom; set => SetBottom(value); } + internal double Bottom + { + get + { + return LocalPosition.Y + Size.Y; + } + } /// /// If @ClampedToParent is true will not set value beyond parent /// - internal override double Right { get => base.Right; set => SetRight(value); } - - internal override double Width { + internal double Right + { get { - return Right - Left; + return LocalPosition.X + Size.X; } - set - { - Right = Left + value; - } - } - - internal override double Height + internal double Width { get { - return Bottom - Top; + return Size.X; } set { - Bottom = Top + value; - } + Size=new Vector2(value,Size.Y); + } } - private void SetRight(double value) + internal double Height { - if(ClampedToParent) + get { - if(Transform.Parent != null) - { - var newValue = System.Math.Min(value, Parent.Right); - base.Right = newValue; - } + return Size.Y; } - base.Right = value; - } - - private void SetBottom(double value) - { - if (ClampedToParent) + set { - if (Transform.Parent != null) - { - var newValue = System.Math.Min(value, Parent.Bottom); - base.Bottom = newValue; - } + Size = new Vector2(Size.X, value); } - base.Bottom = value; } - - //Quick-access to underlying Transform - - /// - /// Local position X - /// X-position from parent transform position - /// - internal override double X { get { return Left; } set { Left = value; } } - - /// - /// Local position Y - /// Y-position from parent Transform position - /// - internal override double Y { get { return Top; } set { Top = value; } } - - //Gets global position x and y - internal double GlobalX { get { return Transform.Position.X; } } - internal double GlobalY { get { return Transform.Position.Y; } } } } diff --git a/src/EPPlusTest/Drawing/TextMeasuring/ReadMeasureTests.cs b/src/EPPlusTest/Drawing/TextMeasuring/ReadMeasureTests.cs index 25957b3df..53b6df1a8 100644 --- a/src/EPPlusTest/Drawing/TextMeasuring/ReadMeasureTests.cs +++ b/src/EPPlusTest/Drawing/TextMeasuring/ReadMeasureTests.cs @@ -30,7 +30,7 @@ public void ReadShape() var width = theShape.Size.Width / 9525d; var height = theShape.Size.Height / 9525d; - //var height = theShape.TextBody.Paragraphs.GetSizeInPixels(theShape.GetPixelWidth(), theShape.GetPixelHeight(), theShape.Text, theShape.Font); + //var height = theShape.TextBodyItem.Paragraphs.GetSizeInPixels(theShape.GetPixelWidth(), theShape.GetPixelHeight(), theShape.Text, theShape.Font); } } [TestMethod] From 70a180e7e7a7d2b1a8d4620d07d42761d48bc0ed Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jan=20K=C3=A4llman?= Date: Mon, 26 Jan 2026 16:41:25 +0100 Subject: [PATCH 017/151] Merge fix --- .../RenderItems/Shared/TextBodyItem.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/EPPlus.Export.ImageRenderer/RenderItems/Shared/TextBodyItem.cs b/src/EPPlus.Export.ImageRenderer/RenderItems/Shared/TextBodyItem.cs index 65ce2d76e..55972fefa 100644 --- a/src/EPPlus.Export.ImageRenderer/RenderItems/Shared/TextBodyItem.cs +++ b/src/EPPlus.Export.ImageRenderer/RenderItems/Shared/TextBodyItem.cs @@ -192,7 +192,7 @@ private double GetAlignmentVertical() //Not center of the Inset Rectangle case eTextAnchoringType.Center: var globalHeight = (DrawingRenderer.Bounds.Height / 2) + Bounds.Top+2; - var adjustedHeight = globalHeight - Bounds.GlobalY; + var adjustedHeight = globalHeight - Bounds.Position.Y; //Global position. alignmentY = adjustedHeight; break; From c4733332ad77c4bbff31390f626e2e669a268cad Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ossian=20Edstr=C3=B6m?= Date: Mon, 26 Jan 2026 17:07:26 +0100 Subject: [PATCH 018/151] Fixed minor spelling mistake --- src/EPPlus.Export.ImageRenderer/RenderItems/SvgGroupItem.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/EPPlus.Export.ImageRenderer/RenderItems/SvgGroupItem.cs b/src/EPPlus.Export.ImageRenderer/RenderItems/SvgGroupItem.cs index c02d9f86a..7fd77f9fa 100644 --- a/src/EPPlus.Export.ImageRenderer/RenderItems/SvgGroupItem.cs +++ b/src/EPPlus.Export.ImageRenderer/RenderItems/SvgGroupItem.cs @@ -43,7 +43,7 @@ internal SvgGroupItem(DrawingBase renderer, BoundingBox bounds) : base(renderer) Bounds = bounds; if (Bounds.Left != 0 || Bounds.Top != 0) { - GroupTransform = $"tranform=\"translate({Bounds.Left.ToString(CultureInfo.InvariantCulture)}, {Bounds.Top.ToString(CultureInfo.InvariantCulture)})\""; + GroupTransform = $"transform=\"translate({Bounds.Left.ToString(CultureInfo.InvariantCulture)}, {Bounds.Top.ToString(CultureInfo.InvariantCulture)})\""; } } internal SvgGroupItem(DrawingBase renderer, BoundingBox parent, double rotation) : base(renderer, parent) From c1440a2f77d71f83059ee219ce456fa845cd02f7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ossian=20Edstr=C3=B6m?= Date: Mon, 26 Jan 2026 17:21:13 +0100 Subject: [PATCH 019/151] Adjusted rectangle to not be doubled --- src/EPPlus.Export.ImageRenderer/RenderItems/SvgGroupItem.cs | 3 ++- .../RenderItems/SvgItem/SvgTextBoxItem.cs | 4 ++++ 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/src/EPPlus.Export.ImageRenderer/RenderItems/SvgGroupItem.cs b/src/EPPlus.Export.ImageRenderer/RenderItems/SvgGroupItem.cs index 7fd77f9fa..63a5d0d7c 100644 --- a/src/EPPlus.Export.ImageRenderer/RenderItems/SvgGroupItem.cs +++ b/src/EPPlus.Export.ImageRenderer/RenderItems/SvgGroupItem.cs @@ -50,7 +50,8 @@ internal SvgGroupItem(DrawingBase renderer, BoundingBox parent, double rotation) { var bounds = ""; var rot = ""; - if(Bounds.Left!=0 || Bounds.Top!=0) + Bounds = parent; + if (Bounds.Left!=0 || Bounds.Top!=0) { bounds = $"translate({Bounds.Left.ToString(CultureInfo.InvariantCulture)}, {Bounds.Top.ToString(CultureInfo.InvariantCulture)})"; } diff --git a/src/EPPlus.Export.ImageRenderer/RenderItems/SvgItem/SvgTextBoxItem.cs b/src/EPPlus.Export.ImageRenderer/RenderItems/SvgItem/SvgTextBoxItem.cs index f3f8d49a9..cfb0e1a26 100644 --- a/src/EPPlus.Export.ImageRenderer/RenderItems/SvgItem/SvgTextBoxItem.cs +++ b/src/EPPlus.Export.ImageRenderer/RenderItems/SvgItem/SvgTextBoxItem.cs @@ -6,6 +6,7 @@ using OfficeOpenXml.Drawing; using OfficeOpenXml.FormulaParsing.Excel.Functions.Text; using System.Collections.Generic; +using System.Drawing; using System.Xml.Serialization; namespace EPPlus.Export.ImageRenderer.RenderItems.SvgItem @@ -58,6 +59,9 @@ internal override void AppendRenderItems(List renderItems) groupItem = new SvgGroupItem(DrawingRenderer, Bounds, Bounds.Rotation); } renderItems.Add(groupItem); + //handled by group now + Rectangle.Bounds.Top = 0; + Rectangle.Bounds.Left = 0; renderItems.Add(Rectangle); TextBody.AppendRenderItems(renderItems); renderItems.Add(new SvgEndGroupItem(DrawingRenderer, Bounds)); From 3e2e267fb130e1e71e3293993a6d6ce6497efda5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jan=20K=C3=A4llman?= Date: Tue, 27 Jan 2026 12:50:18 +0100 Subject: [PATCH 020/151] WIP:Fixes for the Svg Textbox in differnet chart objects. --- .../RenderItems/Shared/ParagraphItem.cs | 59 +++++++++++-------- .../RenderItems/Shared/TextBodyItem.cs | 8 +-- .../RenderItems/Shared/TextRunItem.cs | 2 +- .../RenderItems/SvgItem/SvgParagraphItem.cs | 26 ++++---- .../RenderItems/SvgItem/SvgTextBodyItem.cs | 4 +- .../RenderItems/SvgItem/SvgTextBoxItem.cs | 1 + .../Svg/SvgChart.cs | 30 +++------- .../Svg/SvgChartAxis.cs | 21 +++---- .../Svg/SvgChartObject.cs | 2 +- .../Svg/SvgChartTitle.cs | 14 +---- .../TrueTypeMeasurer/TextData.cs | 4 +- src/EPPlus.Fonts.OpenType/Utils/TextUtils.cs | 4 +- 12 files changed, 77 insertions(+), 98 deletions(-) diff --git a/src/EPPlus.Export.ImageRenderer/RenderItems/Shared/ParagraphItem.cs b/src/EPPlus.Export.ImageRenderer/RenderItems/Shared/ParagraphItem.cs index eb982a603..51c355985 100644 --- a/src/EPPlus.Export.ImageRenderer/RenderItems/Shared/ParagraphItem.cs +++ b/src/EPPlus.Export.ImageRenderer/RenderItems/Shared/ParagraphItem.cs @@ -45,7 +45,7 @@ public ParagraphItem(DrawingBase renderer, BoundingBox parent) : base(renderer, _paragraphFont = defaultFont; } - public ParagraphItem(DrawingBase renderer, BoundingBox parent, ExcelDrawingParagraph p) : base(renderer, parent) + public ParagraphItem(DrawingBase renderer, BoundingBox parent, ExcelDrawingParagraph p, string textIfEmpty=null) : base(renderer, parent) { IsFirstParagraph = p == p._paragraphs[0]; @@ -95,7 +95,7 @@ public ParagraphItem(DrawingBase renderer, BoundingBox parent, ExcelDrawingParag _paragraphFont = p.DefaultRunProperties.GetMeasureFont(); _measurer.SetFont(_paragraphFont); - AddLinesAndTextRuns(p); + AddLinesAndTextRuns(p, textIfEmpty); } private double GetParagraphLineSpacingInPixels(double spacingValue, ITextMeasurerWrap fmExact) @@ -162,7 +162,8 @@ public void AddText(string text, ExcelTextFont font) var measurer = new FontMeasurerTrueType(); var displayText = measurer.MeasureAndWrapText(text, font.GetMeasureFont(), Bounds.Parent.Size.X); var container = CreateTextRun(text, font, Bounds, string.Join("\r\n", displayText.ToArray())); - + container.BaseLineSpacing = _lineSpacingAscendantOnly; + container.LineSpacingPerNewLine = _lsMultiplier.Value * _measurer.GetSingleLineSpacing().PointToPixel(true); Runs.Add(container); container.Bounds.Name = $"Container{Runs.Count}"; @@ -206,7 +207,7 @@ List GetWrappedText(ExcelDrawingTextRunCollection runs, TextFragmentColl return ttMeasurer.WrapMultipleTextFragments(fragments, fonts, maxSizePoints); } - private void AddLinesAndTextRuns(ExcelDrawingParagraph p) + private void AddLinesAndTextRuns(ExcelDrawingParagraph p, string textIfEmpty) { //Log line positions and run sizes GenerateTextFragments(p.TextRuns); @@ -217,35 +218,41 @@ private void AddLinesAndTextRuns(ExcelDrawingParagraph p) double widthOfCurrentLine = 0; double largestFontSizeCurrentLine = 0; int idxLargestFontSize = 0; - - for (int i = 0; i < p.TextRuns.Count; i++) + if (p.TextRuns.Count == 0 && string.IsNullOrEmpty(textIfEmpty) == false) + { + AddText(textIfEmpty, p.DefaultRunProperties); + } + else { - if (p.TextRuns[i].FontSize > largestFontSizeCurrentLine) + for (int i = 0; i < p.TextRuns.Count; i++) { - largestFontSizeCurrentLine = p.TextRuns[i].FontSize; - idxLargestFontSize = i; - } + if (p.TextRuns[i].FontSize > largestFontSizeCurrentLine) + { + largestFontSizeCurrentLine = p.TextRuns[i].FontSize; + idxLargestFontSize = i; + } - AddRenderItemTextRun(p.TextRuns[i], _textRunDisplayText[i], widthOfCurrentLine); - var lastAdded = Runs.Last(); + AddRenderItemTextRun(p.TextRuns[i], _textRunDisplayText[i], widthOfCurrentLine); + var lastAdded = Runs.Last(); - //We are on a new line - if (lastAdded.YIncreasePerLine.Count > 1) - { - Runs[idxLargestFontSize].GetBounds(out double l, out double t, out double r, out double b); - Bounds.Height += Runs[idxLargestFontSize].Bounds.Height; - widthOfCurrentLine = Runs.Last().PerLineWidth.Last(); - - idxLargestFontSize = i; - largestFontSizeCurrentLine = p.TextRuns[i].FontSize; - } - else - { - widthOfCurrentLine += Runs.Last().PerLineWidth.Last(); - if(i == p.TextRuns.Count -1) + //We are on a new line + if (lastAdded.YIncreasePerLine.Count > 1) { Runs[idxLargestFontSize].GetBounds(out double l, out double t, out double r, out double b); Bounds.Height += Runs[idxLargestFontSize].Bounds.Height; + widthOfCurrentLine = Runs.Last().PerLineWidth.Last(); + + idxLargestFontSize = i; + largestFontSizeCurrentLine = p.TextRuns[i].FontSize; + } + else + { + widthOfCurrentLine += Runs.Last().PerLineWidth.Last(); + if (i == p.TextRuns.Count - 1) + { + Runs[idxLargestFontSize].GetBounds(out double l, out double t, out double r, out double b); + Bounds.Height += Runs[idxLargestFontSize].Bounds.Height; + } } } } diff --git a/src/EPPlus.Export.ImageRenderer/RenderItems/Shared/TextBodyItem.cs b/src/EPPlus.Export.ImageRenderer/RenderItems/Shared/TextBodyItem.cs index 55972fefa..1b1333fd4 100644 --- a/src/EPPlus.Export.ImageRenderer/RenderItems/Shared/TextBodyItem.cs +++ b/src/EPPlus.Export.ImageRenderer/RenderItems/Shared/TextBodyItem.cs @@ -59,12 +59,8 @@ public void ImportParagraph(ExcelDrawingParagraph item, double startingY, string var measureFont = item.DefaultRunProperties.GetMeasureFont(); bool isFirst = Paragraphs.Count == 0; - var paragraph = CreateParagraph(item, Bounds); + var paragraph = CreateParagraph(item, Bounds, text); paragraph.Bounds.Name = $"Container{Paragraphs.Count}"; - if (string.IsNullOrEmpty(text) == false) - { - paragraph.AddText(text, item.DefaultRunProperties); - } paragraph.Bounds.Top = startingY; _text = text; Paragraphs.Add(paragraph); @@ -211,7 +207,7 @@ private double GetAlignmentVertical() /// Each file format defines its own paragraph /// /// - internal abstract ParagraphItem CreateParagraph(ExcelDrawingParagraph paragraph, BoundingBox parent); + internal abstract ParagraphItem CreateParagraph(ExcelDrawingParagraph paragraph, BoundingBox parent, string textIfEmpty=""); /// /// Each file format defines its own paragraph diff --git a/src/EPPlus.Export.ImageRenderer/RenderItems/Shared/TextRunItem.cs b/src/EPPlus.Export.ImageRenderer/RenderItems/Shared/TextRunItem.cs index 290d3db6f..8eac44d2d 100644 --- a/src/EPPlus.Export.ImageRenderer/RenderItems/Shared/TextRunItem.cs +++ b/src/EPPlus.Export.ImageRenderer/RenderItems/Shared/TextRunItem.cs @@ -89,7 +89,7 @@ internal TextRunItem(DrawingBase renderer, BoundingBox parent, string text, Exce //To get clipping height we need to get the textbody bounds if (parent != null && parent.Parent != null && parent.Parent.Parent != null) { - ClippingHeight = parent.Parent.Parent.LocalPosition.Y + parent.Parent.Parent.Size.Y; + ClippingHeight = parent.Parent.Parent.Position.Y + parent.Parent.Parent.Size.Y; } if(Lines.Count==1) { diff --git a/src/EPPlus.Export.ImageRenderer/RenderItems/SvgItem/SvgParagraphItem.cs b/src/EPPlus.Export.ImageRenderer/RenderItems/SvgItem/SvgParagraphItem.cs index bbb87016e..933ea4a86 100644 --- a/src/EPPlus.Export.ImageRenderer/RenderItems/SvgItem/SvgParagraphItem.cs +++ b/src/EPPlus.Export.ImageRenderer/RenderItems/SvgItem/SvgParagraphItem.cs @@ -19,7 +19,7 @@ public SvgParagraphItem(DrawingBase renderer, BoundingBox parent) : base(rendere { } - public SvgParagraphItem(DrawingBase renderer, BoundingBox parent, ExcelDrawingParagraph p) : base(renderer, parent, p) + public SvgParagraphItem(DrawingBase renderer, BoundingBox parent, ExcelDrawingParagraph p, string textIfEmpty = null) : base(renderer, parent, p, textIfEmpty) { } @@ -79,19 +79,19 @@ public override void Render(StringBuilder sb) sb.AppendLine("paragraph "); - var bb = new SvgRenderRectItem(DrawingRenderer, Bounds); - //The bb is affected by the Transform so set pos to zero - if (IsFirstParagraph == false) - { - bb.Y = 0; - } - bb.X = 0; + //var bb = new SvgRenderRectItem(DrawingRenderer, Bounds); + ////The bb is affected by the Transform so set pos to zero + //if (IsFirstParagraph == false) + //{ + // bb.Y = 0; + //} + //bb.X = 0; - bb.Width = Bounds.Width; - bb.Height = Bounds.Height; - bb.FillColor = FillColor; - bb.FillOpacity = 0.3; - bb.Render(sb); + //bb.Width = Bounds.Width; + //bb.Height = Bounds.Height; + //bb.FillColor = FillColor; + //bb.FillOpacity = 0.3; + //bb.Render(sb); //Render text run debug boxes as we cannot place the rects after or inside the text element diff --git a/src/EPPlus.Export.ImageRenderer/RenderItems/SvgItem/SvgTextBodyItem.cs b/src/EPPlus.Export.ImageRenderer/RenderItems/SvgItem/SvgTextBodyItem.cs index 760cf0cdb..614c069c5 100644 --- a/src/EPPlus.Export.ImageRenderer/RenderItems/SvgItem/SvgTextBodyItem.cs +++ b/src/EPPlus.Export.ImageRenderer/RenderItems/SvgItem/SvgTextBodyItem.cs @@ -44,9 +44,9 @@ internal override ParagraphItem CreateParagraph(BoundingBox parent) return new SvgParagraphItem(DrawingRenderer, parent); } - internal override ParagraphItem CreateParagraph(ExcelDrawingParagraph paragraph, BoundingBox parent) + internal override ParagraphItem CreateParagraph(ExcelDrawingParagraph paragraph, BoundingBox parent, string textIfEmpty = null) { - return new SvgParagraphItem(DrawingRenderer, parent, paragraph); + return new SvgParagraphItem(DrawingRenderer, parent, paragraph, textIfEmpty); } } } diff --git a/src/EPPlus.Export.ImageRenderer/RenderItems/SvgItem/SvgTextBoxItem.cs b/src/EPPlus.Export.ImageRenderer/RenderItems/SvgItem/SvgTextBoxItem.cs index cfb0e1a26..3b4dfdf7f 100644 --- a/src/EPPlus.Export.ImageRenderer/RenderItems/SvgItem/SvgTextBoxItem.cs +++ b/src/EPPlus.Export.ImageRenderer/RenderItems/SvgItem/SvgTextBoxItem.cs @@ -19,6 +19,7 @@ internal SvgTextBoxItem(DrawingBase renderer, BoundingBox parent, double left, d Bounds.Top = top; Bounds.Width = maxWidth; Bounds.Height = maxHeight; + Bounds.Name = "TextBox"; TextBody = new SvgTextBodyItem(renderer, Bounds, true); Bounds.Width = TextBody.Width; Bounds.Height = TextBody.Height; diff --git a/src/EPPlus.Export.ImageRenderer/Svg/SvgChart.cs b/src/EPPlus.Export.ImageRenderer/Svg/SvgChart.cs index faa347547..9e21f4197 100644 --- a/src/EPPlus.Export.ImageRenderer/Svg/SvgChart.cs +++ b/src/EPPlus.Export.ImageRenderer/Svg/SvgChart.cs @@ -149,16 +149,16 @@ public void Render(StringBuilder sb) { Plotarea?.AppendRenderItems(RenderItems); - //HorizontalAxis?.AppendRenderItems(RenderItems); - //VerticalAxis?.AppendRenderItems(RenderItems); - //SecondVerticalAxis?.AppendRenderItems(RenderItems); + HorizontalAxis?.AppendRenderItems(RenderItems); + VerticalAxis?.AppendRenderItems(RenderItems); + SecondVerticalAxis?.AppendRenderItems(RenderItems); - //foreach (var drawer in Plotarea?.ChartTypeDrawers) - //{ - // drawer.AppendRenderItems(RenderItems); - //} + foreach (var drawer in Plotarea?.ChartTypeDrawers) + { + drawer.AppendRenderItems(RenderItems); + } - //Legend?.AppendRenderItems(RenderItems); + Legend?.AppendRenderItems(RenderItems); Title?.AppendRenderItems(RenderItems); sb.Append($""); @@ -166,24 +166,10 @@ public void Render(StringBuilder sb) var writer = new SvgDrawingWriter(this); writer.WriteSvgDefs(sb, RenderItems); - //SvgGroupItem gItemTest = null; foreach (var item in RenderItems) { item.Render(sb); - //if (item.Type == RenderItemType.Group && gItemTest == null) - //{ - // gItemTest = (SvgGroupItem)item; - //} - //if(item.IsEndOfGroup && gItemTest != null) - //{ - // gItemTest.RenderEndGroup(sb); - // gItemTest = null; - //} } - //if(gItemTest != null) - //{ - // gItemTest.RenderEndGroup(sb); - //} sb.Append(""); } diff --git a/src/EPPlus.Export.ImageRenderer/Svg/SvgChartAxis.cs b/src/EPPlus.Export.ImageRenderer/Svg/SvgChartAxis.cs index 0ebcf0d68..8e20fe3e0 100644 --- a/src/EPPlus.Export.ImageRenderer/Svg/SvgChartAxis.cs +++ b/src/EPPlus.Export.ImageRenderer/Svg/SvgChartAxis.cs @@ -234,7 +234,6 @@ internal override void AppendRenderItems(List renderItems) { foreach (var tb in AxisValuesTextBoxes) { - //renderItems.Add(); tb.AppendRenderItems(renderItems); } } @@ -291,15 +290,17 @@ private List GetAxisValueTextBoxes() var x = GetAxisItemLeft(i, m); var y = GetAxisItemTop(i, m); //var tb = new TextBox(Chart, x, y, m.Width, m.Height); - var bounds = new BoundingBox(); - bounds.Left = x; - bounds.Top = y; - bounds.Width = m.Width.PointToPixel(); - bounds.Height = m.Height.PointToPixel(); - var tb = new SvgTextBoxItem(SvgChart, bounds, x, y, bounds.Width, bounds.Height); - tb.ImportTextBody(Axis.TextBody); - tb.TextBody.Paragraphs[0].AddText(v, Axis.Font); - //tb.SetDrawingPropertiesFill(Axis.Fill, axisStyle.FillReference.Color); + //var bounds = new BoundingBox(); + //bounds.Left = x; + //bounds.Top = y; + var width = m.Width.PointToPixel(); + var height = m.Height.PointToPixel(); + var tb = new SvgTextBoxItem(SvgChart, Rectangle.Bounds, x, y, width, height); + var p = Axis.TextBody.Paragraphs.FirstOrDefault(); + tb.TextBody.ImportParagraph(p, 0, v); + + //tb.TextBody.Paragraphs[0].AddText(v, Axis.Font); + tb.Rectangle.SetDrawingPropertiesFill(Axis.Fill, axisStyle.FillReference.Color); ret.Add(tb); } return ret; diff --git a/src/EPPlus.Export.ImageRenderer/Svg/SvgChartObject.cs b/src/EPPlus.Export.ImageRenderer/Svg/SvgChartObject.cs index ae0997f38..bd4e57fac 100644 --- a/src/EPPlus.Export.ImageRenderer/Svg/SvgChartObject.cs +++ b/src/EPPlus.Export.ImageRenderer/Svg/SvgChartObject.cs @@ -56,7 +56,7 @@ internal void SetMargins(ExcelTextBody tb) internal SvgRenderLineItem Line { get; set; } protected static SvgRenderRectItem GetRectFromManualLayout(SvgChart sc, ExcelLayout layout) { - var rect = new SvgRenderRectItem(sc, sc.Bounds); + var rect = new SvgRenderRectItem(sc, sc.ChartArea.Bounds); var ml = layout.ManualLayout; if (ml.LeftMode == eLayoutMode.Edge) { diff --git a/src/EPPlus.Export.ImageRenderer/Svg/SvgChartTitle.cs b/src/EPPlus.Export.ImageRenderer/Svg/SvgChartTitle.cs index 3b8a009fa..e99965458 100644 --- a/src/EPPlus.Export.ImageRenderer/Svg/SvgChartTitle.cs +++ b/src/EPPlus.Export.ImageRenderer/Svg/SvgChartTitle.cs @@ -171,7 +171,7 @@ private static string GetDefaultChartTitleText(SvgChart sc, ExcelChartTitleStand internal void InitTextBox() { - TextBox = new SvgTextBoxItem(_svgChart, _svgChart.Bounds, Rectangle.Left, Rectangle.Top, Rectangle.Width, Rectangle.Height); + TextBox = new SvgTextBoxItem(_svgChart, _svgChart.ChartArea.Bounds, Rectangle.Left, Rectangle.Top, Rectangle.Width, Rectangle.Height); TextBox.LeftMargin = LeftMargin; TextBox.RightMargin = RightMargin; TextBox.TopMargin = TopMargin; @@ -203,22 +203,10 @@ public SvgTextBoxItem TextBox } internal override void AppendRenderItems(List renderItems) { - //SvgGroupItem groupItem; - //if (TextBox.Bounds.Rotation == 0) - //{ - // groupItem = new SvgGroupItem(_svgChart, Rectangle.Bounds); - //} - //else - //{ - // groupItem = new SvgGroupItem(_svgChart, Rectangle.Bounds, TextBox.Bounds.Rotation); - //} - //renderItems.Add(groupItem); - //renderItems.Add(Rectangle); TextBox.AppendRenderItems(renderItems); TextBox.Rectangle.SetDrawingPropertiesFill(_title.Fill, _svgChart.Chart.StyleManager.Style.Title.FillReference.Color); TextBox.Rectangle.SetDrawingPropertiesBorder(_title.Border, _svgChart.Chart.StyleManager.Style.Title.BorderReference.Color, _title.Border.Fill.Style != eFillStyle.NoFill, 0.75); - //renderItems.Add(new SvgEndGroupItem(DrawingRenderer, Bounds)); } } diff --git a/src/EPPlus.Fonts.OpenType/TrueTypeMeasurer/TextData.cs b/src/EPPlus.Fonts.OpenType/TrueTypeMeasurer/TextData.cs index 548eda54f..dcd115a87 100644 --- a/src/EPPlus.Fonts.OpenType/TrueTypeMeasurer/TextData.cs +++ b/src/EPPlus.Fonts.OpenType/TrueTypeMeasurer/TextData.cs @@ -384,7 +384,7 @@ private static void WrapAtCharPos(string line, int charPos, ref int nextLineStar private static void MeasureAndWrapLine(string line, OpenTypeFont font, ref int lineWidth, ref int wordWidth, GlyphMappings glyphMappings, ushort? lastGlyphIndex, double maxWidth, List wrappedStrings, bool applyKerning = true) { int nextLineStartIndex = 0; - + for (int i = 0; i < line.Length; i++) { char c = line[i]; @@ -426,7 +426,7 @@ internal static List MeasureAndWrapText(string text, double fontSize, Op var inputMaxWidth = maxWidth; //Convert maxWidth from points to font design units - maxWidth = (maxWidth * (double)fontData.HeadTable.UnitsPerEm) / fontSize; + maxWidth = Math.Round((maxWidth * (double)fontData.HeadTable.UnitsPerEm) / fontSize,0,MidpointRounding.AwayFromZero); var glyphMappings = fontData.CmapTable.GetPreferredSubtable().GetGlyphMappings(); diff --git a/src/EPPlus.Fonts.OpenType/Utils/TextUtils.cs b/src/EPPlus.Fonts.OpenType/Utils/TextUtils.cs index 5420faadf..49297919b 100644 --- a/src/EPPlus.Fonts.OpenType/Utils/TextUtils.cs +++ b/src/EPPlus.Fonts.OpenType/Utils/TextUtils.cs @@ -27,8 +27,8 @@ public static float PointToPixel(this float pointSize) public static double PixelToPoint(this double pixelSize) { - var roundedSize = (double)RoundToWhole(pixelSize); - return roundedSize / 96 * 72; + //var roundedSize = (double)RoundToWhole(pixelSize); + return pixelSize / 96 * 72; } public static double EmuToPoint(this double emuNumber) From 598f287d1030371f7647c2b0e91f16509413ed5f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ossian=20Edstr=C3=B6m?= Date: Tue, 27 Jan 2026 14:03:55 +0100 Subject: [PATCH 021/151] fixed endgroup for shape + minor misspelling --- .../TextRenderTests.cs | 52 ++++++++- .../Constants/PatternArrays.cs | 2 +- .../RenderItems/Shared/ParagraphItem.cs | 18 ++- .../RenderItems/SvgItem/SvgTextBoxItem.cs | 10 +- .../Svg/SvgShape.cs | 60 +++++----- .../Utils/SvgDrawingWriter.cs | 2 +- .../DataHolders/TextFragmentCollection.cs | 108 ++++++++++++++++++ .../TrueTypeMeasurer/DataHolders/TextLine.cs | 6 +- .../DataHolders/TextParagraph.cs | 2 +- .../TrueTypeMeasurer/TextData.cs | 7 +- 10 files changed, 227 insertions(+), 40 deletions(-) diff --git a/src/EPPlus.Export.ImageRenderer.Test/TextRenderTests.cs b/src/EPPlus.Export.ImageRenderer.Test/TextRenderTests.cs index ffc75492d..98aab5157 100644 --- a/src/EPPlus.Export.ImageRenderer.Test/TextRenderTests.cs +++ b/src/EPPlus.Export.ImageRenderer.Test/TextRenderTests.cs @@ -1,12 +1,17 @@ -using OfficeOpenXml; +using EPPlus.Export.ImageRenderer.RenderItems.SvgItem; +using EPPlus.Fonts.OpenType; +using EPPlus.Fonts.OpenType.TrueTypeMeasurer; +using EPPlus.Fonts.OpenType.TrueTypeMeasurer.DataHolders; +using EPPlus.Fonts.OpenType.Utils; +using EPPlus.Graphics; +using EPPlusImageRenderer; +using EPPlusImageRenderer.Svg; +using OfficeOpenXml; using OfficeOpenXml.Drawing; +using OfficeOpenXml.Interfaces.Drawing.Text; using OfficeOpenXml.Style; -using System.Globalization; -using EPPlusImageRenderer; -using EPPlus.Export.ImageRenderer.RenderItems.SvgItem; -using EPPlus.Graphics; using System.Diagnostics; -using EPPlusImageRenderer.Svg; +using System.Globalization; namespace EPPlus.Export.ImageRenderer.Tests { @@ -145,6 +150,41 @@ public void VerifyTextRunBounds() } } + [TestMethod] + public void ConceptLines() + { + var currentChar = 'a'; + var defaultFont = new MeasurementFont + { + FontFamily = "Aptos Narrow", + Size = 11, + Style = MeasurementFontStyles.Regular + }; + + List manyRichText = new List(); + List manyFonts = new List(); + + for (int i = 0; i< 20; i++) + { + manyRichText.Add(currentChar.ToString()); + manyFonts.Add(defaultFont); + currentChar++; + defaultFont.Size ++; + } + + var fragments = new TextFragmentCollection(manyRichText); + var fontMeasurer = new FontMeasurerTrueType(); + + var strings = fontMeasurer.WrapMultipleTextFragments(fragments, manyFonts, 30d.PixelToPoint()); + + var outputLines = fragments.GetOutputLines(); + + + //var paragraph = new TextParagraph(fragments, manyFonts); + + //List manyRichText = new List() {"a","b","c","d","e","f","g" }; + } + [TestMethod] public void ConceptTextRun() { diff --git a/src/EPPlus.Export.ImageRenderer/Constants/PatternArrays.cs b/src/EPPlus.Export.ImageRenderer/Constants/PatternArrays.cs index fbddde46a..673c9a095 100644 --- a/src/EPPlus.Export.ImageRenderer/Constants/PatternArrays.cs +++ b/src/EPPlus.Export.ImageRenderer/Constants/PatternArrays.cs @@ -330,7 +330,7 @@ internal static class PatternArrays new short[] { 1, 1, 1, 1 } }; - internal static readonly short[][] Shpere = new short[][] { + internal static readonly short[][] Sphere = new short[][] { new short[] { 1, 0, 0, 1, 1, 0, 0, 0 }, new short[] { 1, 1, 1, 1, 1, 0, 0, 0 }, new short[] { 1, 1, 1, 1, 1, 0, 0, 0 }, diff --git a/src/EPPlus.Export.ImageRenderer/RenderItems/Shared/ParagraphItem.cs b/src/EPPlus.Export.ImageRenderer/RenderItems/Shared/ParagraphItem.cs index eb982a603..3fc1ec3bc 100644 --- a/src/EPPlus.Export.ImageRenderer/RenderItems/Shared/ParagraphItem.cs +++ b/src/EPPlus.Export.ImageRenderer/RenderItems/Shared/ParagraphItem.cs @@ -217,6 +217,7 @@ private void AddLinesAndTextRuns(ExcelDrawingParagraph p) double widthOfCurrentLine = 0; double largestFontSizeCurrentLine = 0; int idxLargestFontSize = 0; + int firstRunInLine = 0; for (int i = 0; i < p.TextRuns.Count; i++) { @@ -229,18 +230,33 @@ private void AddLinesAndTextRuns(ExcelDrawingParagraph p) AddRenderItemTextRun(p.TextRuns[i], _textRunDisplayText[i], widthOfCurrentLine); var lastAdded = Runs.Last(); - //We are on a new line if (lastAdded.YIncreasePerLine.Count > 1) { + //We are on a new line + + //Ensure bounds of the largest font size have been calculated Runs[idxLargestFontSize].GetBounds(out double l, out double t, out double r, out double b); + //Add its height to the bounds height as smaller fonts are irrelevant for height increase Bounds.Height += Runs[idxLargestFontSize].Bounds.Height; + + //if(firstRunInLine != 0) + //{ + //} + + ////Excel in addition adds the height to the first y-value as well + //Runs[i].YIncreasePerLine[1] = Runs[idxLargestFontSize].Bounds.Height; + //Runs[firstRunInLine] + widthOfCurrentLine = Runs.Last().PerLineWidth.Last(); idxLargestFontSize = i; largestFontSizeCurrentLine = p.TextRuns[i].FontSize; + + firstRunInLine = i; } else { + //We are continuing on the current line widthOfCurrentLine += Runs.Last().PerLineWidth.Last(); if(i == p.TextRuns.Count -1) { diff --git a/src/EPPlus.Export.ImageRenderer/RenderItems/SvgItem/SvgTextBoxItem.cs b/src/EPPlus.Export.ImageRenderer/RenderItems/SvgItem/SvgTextBoxItem.cs index cfb0e1a26..3c1ed9c3f 100644 --- a/src/EPPlus.Export.ImageRenderer/RenderItems/SvgItem/SvgTextBoxItem.cs +++ b/src/EPPlus.Export.ImageRenderer/RenderItems/SvgItem/SvgTextBoxItem.cs @@ -26,7 +26,14 @@ internal SvgTextBoxItem(DrawingBase renderer, BoundingBox parent, double left, d Rectangle = new SvgRenderRectItem(renderer, parent); Rectangle.Bounds = Bounds; } - public RenderItem Rectangle { get; set; } + + //Simplified input + internal SvgTextBoxItem(DrawingBase renderer, BoundingBox parent, BoundingBox maxBounds) : this( + renderer, parent, maxBounds.Left, maxBounds.Top, maxBounds.Width, maxBounds.Height) + { + } + + public SvgRenderItem Rectangle { get; set; } public SvgTextBodyItem TextBody {get;set;} internal double LeftMargin { get { return TextBody.Bounds.Left; } set { TextBody.Bounds.Left = value; } } @@ -62,6 +69,7 @@ internal override void AppendRenderItems(List renderItems) //handled by group now Rectangle.Bounds.Top = 0; Rectangle.Bounds.Left = 0; + Rectangle.FillOpacity = 0.1; renderItems.Add(Rectangle); TextBody.AppendRenderItems(renderItems); renderItems.Add(new SvgEndGroupItem(DrawingRenderer, Bounds)); diff --git a/src/EPPlus.Export.ImageRenderer/Svg/SvgShape.cs b/src/EPPlus.Export.ImageRenderer/Svg/SvgShape.cs index add00bf66..f44828a2b 100644 --- a/src/EPPlus.Export.ImageRenderer/Svg/SvgShape.cs +++ b/src/EPPlus.Export.ImageRenderer/Svg/SvgShape.cs @@ -36,8 +36,14 @@ namespace EPPlusImageRenderer.Svg { internal class SvgShape : DrawingShape { - SvgRenderRectItem insetTextBox; - SvgTextBodyItem textBody; + /// + /// Calculated shape textbox + /// + SvgRenderRectItem InsetTextBox; + /// + /// Textbox from memory + /// + SvgTextBoxItem TextBox; public SvgShape(ExcelShape shape) : base(shape) { @@ -75,34 +81,36 @@ public SvgShape(ExcelShape shape) : base(shape) } } + RenderItems.Add(new SvgEndGroupItem(this, Bounds)); + if (_shape.Text != null) { if (shapeDef.TextBoxRect != null) { - insetTextBox = new SvgRenderRectItem(this, Bounds); - insetTextBox.Bounds.Left = (float)shapeDef.TextBoxRect.LeftValue; - insetTextBox.Bounds.Top = (float)shapeDef.TextBoxRect.TopValue; - insetTextBox.FillOpacity = 0.3d; + InsetTextBox = new SvgRenderRectItem(this, Bounds); + InsetTextBox.Bounds.Left = (float)shapeDef.TextBoxRect.LeftValue; + InsetTextBox.Bounds.Top = (float)shapeDef.TextBoxRect.TopValue; + InsetTextBox.FillOpacity = 0.3d; if (shape.TextBody.TextAutofit != eTextAutofit.ShapeAutofit) { - insetTextBox.Width = (float)shapeDef.TextBoxRect.RightValue - (float)shapeDef.TextBoxRect.LeftValue; - insetTextBox.Height = (float)shapeDef.TextBoxRect.BottomValue - (float)shapeDef.TextBoxRect.TopValue; + InsetTextBox.Width = (float)shapeDef.TextBoxRect.RightValue - (float)shapeDef.TextBoxRect.LeftValue; + InsetTextBox.Height = (float)shapeDef.TextBoxRect.BottomValue - (float)shapeDef.TextBoxRect.TopValue; } else { - insetTextBox.Width = (float)shapeDef.TextBoxRect.RightValue; - insetTextBox.Height = (float)shapeDef.TextBoxRect.BottomValue; + InsetTextBox.Width = (float)shapeDef.TextBoxRect.RightValue; + InsetTextBox.Height = (float)shapeDef.TextBoxRect.BottomValue; } } else { - insetTextBox = null; + InsetTextBox = null; } - textBody = CreateTextBodyItem(); - textBody.ImportTextBody(_shape.TextBody); - textBody.AppendRenderItems(RenderItems); + TextBox = CreateTextBodyItem(); + TextBox.ImportTextBody(_shape.TextBody); + TextBox.AppendRenderItems(RenderItems); } } } @@ -237,19 +245,19 @@ public void Render(StringBuilder sb) sb.AppendLine(""); } - SvgTextBodyItem CreateTextBodyItem() + SvgTextBoxItem CreateTextBodyItem() { - if (insetTextBox == null) + if (InsetTextBox == null) { GetShapeInnerBound(out double x, out double y, out double width, out double height); - insetTextBox = new SvgRenderRectItem(this, Bounds); - insetTextBox.Bounds.Left = x; - insetTextBox.Bounds.Top = y; - insetTextBox.Width = width; - insetTextBox.Height = height; - insetTextBox.Bounds.Parent = textBody.Bounds; //TODO:Check that textBody is correct. + InsetTextBox = new SvgRenderRectItem(this, Bounds); + InsetTextBox.Bounds.Left = x; + InsetTextBox.Bounds.Top = y; + InsetTextBox.Width = width; + InsetTextBox.Height = height; + InsetTextBox.Bounds.Parent = TextBox.Bounds; //TODO:Check that textBody is correct. } - var txtBodyItem = new SvgTextBodyItem(this, insetTextBox.Bounds); + var txtBodyItem = new SvgTextBoxItem(this, Bounds, InsetTextBox.Bounds); return txtBodyItem; } @@ -262,10 +270,10 @@ SvgTextBodyItem CreateTextBodyItem() private void RenderDebugTextBox(StringBuilder sb) { - insetTextBox.FillColor = "green"; - insetTextBox.Render(sb); + InsetTextBox.FillColor = "green"; + InsetTextBox.Render(sb); - insetTextBox.GetBounds(out double l, out double t, out double r, out double b); + InsetTextBox.GetBounds(out double l, out double t, out double r, out double b); //var area = textBody.Bounds; diff --git a/src/EPPlus.Export.ImageRenderer/Utils/SvgDrawingWriter.cs b/src/EPPlus.Export.ImageRenderer/Utils/SvgDrawingWriter.cs index e28372257..1cc06e6ff 100644 --- a/src/EPPlus.Export.ImageRenderer/Utils/SvgDrawingWriter.cs +++ b/src/EPPlus.Export.ImageRenderer/Utils/SvgDrawingWriter.cs @@ -275,7 +275,7 @@ private string WritePattern(string namePrefix, StringBuilder defSb, HashSet _fragmentItems = new List(); List _lines = new List(); List outputFragments = new List(); + List outputStrings = new List(); /// /// Added linewidths in points @@ -157,6 +158,113 @@ public List GetFragmentsWithFinalLineBreaks() return outputFragments; } + public List GetOutputLines() + { + var fragments = GetFragmentsWithFinalLineBreaks(); + + string outputAllText = ""; + + //Create final all text + for (int i = 0; i < fragments.Count(); i++) + { + outputAllText += fragments[i]; + } + + var newLineIndiciesOutput = GetFirstCharPositionOfNewLines(outputAllText); + List outputTextLines = new List(); + + //Save minor information about each char so each char knows its line/fragment + int charCount = 0; + int lineIndex = 0; + + List currFragments = new(); + int lineStartCharIndex = 0; + int RtInternalStartIndex = 0; + + //For each fragment + for (int i = 0; i < fragments.Count; i++) + { + var textFragment = fragments[i]; + + //For each char in current fragment + for (int j = 0; j < textFragment.Length; j++) + { + if (lineIndex <= newLineIndiciesOutput.Count - 1 && + charCount >= newLineIndiciesOutput[lineIndex]) + { + //We have reached a new line + var text = outputAllText.Substring(lineStartCharIndex, charCount - lineStartCharIndex); + var trimmedText = text.Trim(['\r', '\n']); + + var line = new TextLine() + { + richTextIndicies = currFragments, + content = trimmedText, + startIndex = lineStartCharIndex, + rtContentStartIndexPerRt = new List(j), + lastRtInternalIndex = j, + startRtInternalIndex = RtInternalStartIndex + }; + + lineStartCharIndex = newLineIndiciesOutput[lineIndex]; + outputTextLines.Add(line); + currFragments.Clear(); + RtInternalStartIndex = j; + lineIndex++; + } + + //var info = new CharInfo(charCount, i, lineIndex); + //CharLookup.Add(charCount, info); + charCount++; + } + currFragments.Add(i); + } + + //Add the last line + var lastText = outputAllText.Substring(lineStartCharIndex, charCount - lineStartCharIndex); + var lastTrimmedText = lastText.Trim(['\r', '\n']); + + var lastLine = new TextLine() + { + richTextIndicies = currFragments, + content = lastTrimmedText, + startIndex = lineStartCharIndex, + lastRtInternalIndex = fragments.Last().Length, + startRtInternalIndex = RtInternalStartIndex + }; + + outputTextLines.Add(lastLine); + currFragments.Clear(); + + return outputTextLines; + } + + //public void GetTextFragmentOutputStartIndiciesOnFinalLines(List finalOutputLines) + //{ + // var lineBreakStrings = GetFragmentsWithFinalLineBreaks(); + // //var fragmentsWithoutLineBreaks = GetFragmentsWithoutLineBreaks(); + // string outputAllText = ""; + + // //List> eachFragmentForEachLine = new List>(); + + // for(int i = 0; i< finalOutputLines.Count(); i++) + // { + // var line = new TextLine(); + // line.content = finalOutputLines[i]; + + // } + + // //Create final all text + // for(int i = 0; i< lineBreakStrings.Count(); i++) + // { + + // ////var length = lineBreakStrings[i].Length; + // //outputAllText += lineBreakStrings[i]; + // } + + + //} + public List GetFragmentsWithoutLineBreaks() { ////var newFragments = TextFragments.ToArray(); diff --git a/src/EPPlus.Fonts.OpenType/TrueTypeMeasurer/DataHolders/TextLine.cs b/src/EPPlus.Fonts.OpenType/TrueTypeMeasurer/DataHolders/TextLine.cs index 29f1870f3..f1aab663b 100644 --- a/src/EPPlus.Fonts.OpenType/TrueTypeMeasurer/DataHolders/TextLine.cs +++ b/src/EPPlus.Fonts.OpenType/TrueTypeMeasurer/DataHolders/TextLine.cs @@ -5,10 +5,14 @@ namespace EPPlus.Fonts.OpenType.TrueTypeMeasurer { - internal class TextLine + public class TextLine { internal List richTextIndicies; internal string content; internal int startIndex; + internal List rtContentStartIndexPerRt; + internal int lastRtInternalIndex; + internal int startRtInternalIndex; + } } diff --git a/src/EPPlus.Fonts.OpenType/TrueTypeMeasurer/DataHolders/TextParagraph.cs b/src/EPPlus.Fonts.OpenType/TrueTypeMeasurer/DataHolders/TextParagraph.cs index 5750326e6..2b7b81170 100644 --- a/src/EPPlus.Fonts.OpenType/TrueTypeMeasurer/DataHolders/TextParagraph.cs +++ b/src/EPPlus.Fonts.OpenType/TrueTypeMeasurer/DataHolders/TextParagraph.cs @@ -10,7 +10,7 @@ namespace EPPlus.Fonts.OpenType.TrueTypeMeasurer { - internal class TextParagraph + public class TextParagraph { //prevent creating multiple OpenTypeFonts via cache/indexing internal Dictionary FontIndexDict = new(); diff --git a/src/EPPlus.Fonts.OpenType/TrueTypeMeasurer/TextData.cs b/src/EPPlus.Fonts.OpenType/TrueTypeMeasurer/TextData.cs index 548eda54f..9eecb6fd4 100644 --- a/src/EPPlus.Fonts.OpenType/TrueTypeMeasurer/TextData.cs +++ b/src/EPPlus.Fonts.OpenType/TrueTypeMeasurer/TextData.cs @@ -718,6 +718,11 @@ internal static void ConvertDesignUnits(OpenTypeFont origFont, double origSize, wordWidth = Convert.ToInt16(wordWidthInPoints * factorTarget); } + internal static List WrapRichText(TextParagraph paragraph, double maxWidthPoints) + { + + } + internal static List WrapMultipleTextFragments(TextParagraph paragraph, double maxWidthPoints) { //Initialize variables @@ -743,8 +748,6 @@ internal static List WrapMultipleTextFragments(TextParagraph paragraph, var allText = fragments.AllText; var newLineIndicies = fragments.AllTextNewLineIndicies; - var len = allText.Length-7; - //Iterate through All fragments as one concatenated text string for (int i = 0; i < allText.Length; i++) { From c943e1afd08db49ab236309129a2052648183933 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ossian=20Edstr=C3=B6m?= Date: Tue, 27 Jan 2026 14:09:02 +0100 Subject: [PATCH 022/151] removed unimplemented function --- src/EPPlus.Fonts.OpenType/TrueTypeMeasurer/TextData.cs | 5 ----- 1 file changed, 5 deletions(-) diff --git a/src/EPPlus.Fonts.OpenType/TrueTypeMeasurer/TextData.cs b/src/EPPlus.Fonts.OpenType/TrueTypeMeasurer/TextData.cs index 623f313af..e4b810320 100644 --- a/src/EPPlus.Fonts.OpenType/TrueTypeMeasurer/TextData.cs +++ b/src/EPPlus.Fonts.OpenType/TrueTypeMeasurer/TextData.cs @@ -718,11 +718,6 @@ internal static void ConvertDesignUnits(OpenTypeFont origFont, double origSize, wordWidth = Convert.ToInt16(wordWidthInPoints * factorTarget); } - internal static List WrapRichText(TextParagraph paragraph, double maxWidthPoints) - { - - } - internal static List WrapMultipleTextFragments(TextParagraph paragraph, double maxWidthPoints) { //Initialize variables From 2b097f0152a825b1d58a1d872b922b18889a0166 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ossian=20Edstr=C3=B6m?= Date: Tue, 27 Jan 2026 17:03:55 +0100 Subject: [PATCH 023/151] Measurer from > to >= to adjust for round. + test debug --- .../TestFontMeasurer.cs | 41 ++++++++++--------- .../TrueTypeMeasurer/TextData.cs | 7 ++-- 2 files changed, 24 insertions(+), 24 deletions(-) diff --git a/src/EPPlus.Export.ImageRenderer.Test/TestFontMeasurer.cs b/src/EPPlus.Export.ImageRenderer.Test/TestFontMeasurer.cs index 4f4ab8728..465fc0ad4 100644 --- a/src/EPPlus.Export.ImageRenderer.Test/TestFontMeasurer.cs +++ b/src/EPPlus.Export.ImageRenderer.Test/TestFontMeasurer.cs @@ -164,27 +164,28 @@ public void LoremIpsum20Paragraphs() var savedStrings = SavedComparisonString.Split("\r\n"); - List differingStrings = new(); - - if (wrappedStrings.Count() > savedStrings.Count()) - differingStrings = savedStrings.Except(wrappedStrings).ToList(); - else - differingStrings = wrappedStrings.Except(savedStrings).ToList(); - - //INTENTIONAL COMMENT for easier debugging later. - //It is sometimes easier to compare files directly when debugging rather than looking through "differingStrings" - //bool writeFiles = true; - - //if (/*differingStrings.Count() > 0*/ writeFiles) - //{ - // File.WriteAllText("C:\\temp\\LoremIpsum20_NEW.txt", string.Join("\r\n", wrappedStrings.ToArray())); - // File.WriteAllText("C:\\temp\\LoremIpsum20_OLD.txt", SavedComparisonString); - - // var currStr = File.ReadAllText("C:\\temp\\LoremIpsum20_NEW.txt"); - // Assert.AreEqual(SavedComparisonString, currStr); - //} + List faultyStrings = new(); + List excpectedStrings = new(); + List indiciesOfDifferingString = new(); + + for (int i = 0; i < savedStrings.Count(); i++) + { + if(savedStrings[i] != wrappedStrings[i]) + { + indiciesOfDifferingString.Add(i); + faultyStrings.Add(wrappedStrings[i]); + excpectedStrings.Add(savedStrings[i]); + } + } + + if(indiciesOfDifferingString.Count != 0) + { + //The start of indicies diverging + Assert.IsNull(indiciesOfDifferingString[0]); + Assert.AreEqual(faultyStrings[0], excpectedStrings[0]); + } - Assert.AreEqual(0, differingStrings.Count()); + Assert.AreEqual(0, faultyStrings.Count); } [TestMethod] public void LoremIpsum20ParagraphsMultipleFragments() diff --git a/src/EPPlus.Fonts.OpenType/TrueTypeMeasurer/TextData.cs b/src/EPPlus.Fonts.OpenType/TrueTypeMeasurer/TextData.cs index e4b810320..1e929abdf 100644 --- a/src/EPPlus.Fonts.OpenType/TrueTypeMeasurer/TextData.cs +++ b/src/EPPlus.Fonts.OpenType/TrueTypeMeasurer/TextData.cs @@ -390,7 +390,7 @@ private static void MeasureAndWrapLine(string line, OpenTypeFont font, ref int l char c = line[i]; var advanceWidth = CalculateAdvanceWidth(c, glyphMappings, font, ref lastGlyphIndex, ref lineWidth, ref wordWidth, ref applyKerning); - if (lineWidth > maxWidth) + if (lineWidth >= maxWidth) { WrapAtCharPos(line, i, ref nextLineStartIndex, ref lineWidth, ref wordWidth, advanceWidth, wrappedStrings); } @@ -417,7 +417,6 @@ private static void MeasureAndWrapLine(string line, OpenTypeFont font, ref int l internal static List MeasureAndWrapText(string text, double fontSize, OpenTypeFont fontData, double maxWidth, double preExistingLineWidth = 0, double preExistingWordWidth = 0) { // Initialize: - int totalAdvanceWidth = 0; ushort? lastGlyphIndex = 0; @@ -426,7 +425,7 @@ internal static List MeasureAndWrapText(string text, double fontSize, Op var inputMaxWidth = maxWidth; //Convert maxWidth from points to font design units - maxWidth = Math.Round((maxWidth * (double)fontData.HeadTable.UnitsPerEm) / fontSize,0,MidpointRounding.AwayFromZero); + var maxWidthInDesignUnits = Math.Round(((inputMaxWidth * (double)fontData.HeadTable.UnitsPerEm) / fontSize), 0, MidpointRounding.AwayFromZero); var glyphMappings = fontData.CmapTable.GetPreferredSubtable().GetGlyphMappings(); @@ -446,7 +445,7 @@ internal static List MeasureAndWrapText(string text, double fontSize, Op // Execute: - MeasureAndWrapLines(text, ref totalAdvanceWidth, ref wordWidth, fontData, glyphMappings, lastGlyphIndex, maxWidth, wrappedStrings); + MeasureAndWrapLines(text, ref totalAdvanceWidth, ref wordWidth, fontData, glyphMappings, lastGlyphIndex, maxWidthInDesignUnits, wrappedStrings); return wrappedStrings; } From d1066851ad4e86469e1525fe43a877b51f9088ca Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jan=20K=C3=A4llman?= Date: Wed, 28 Jan 2026 13:16:16 +0100 Subject: [PATCH 024/151] WIP:Fixes for new positioning system --- .../SvgPathTests.cs | 2 +- .../RenderItems/Shared/TextBodyItem.cs | 4 +- .../RenderItems/SvgGroupItem.cs | 4 +- .../RenderItems/SvgItem/SvgParagraphItem.cs | 2 +- .../RenderItems/SvgItem/SvgTextBodyItem.cs | 26 ++++-- .../RenderItems/SvgItem/SvgTextBoxItem.cs | 1 - .../RenderItems/SvgRenderEllipseItem.cs | 8 +- .../RenderItems/SvgRenderLineItem.cs | 81 ++++++++++++++----- .../RenderItems/SvgRenderRectItem.cs | 24 +++--- .../Svg/Chart/ChartTypeDrawer.cs | 6 +- .../Svg/LineMarkerHelper.cs | 18 ++--- .../Svg/SvgChartAxis.cs | 22 +++-- .../Svg/SvgChartLegend.cs | 41 ++++++---- .../Svg/SvgChartPlotarea.cs | 12 +-- .../Svg/SvgChartTitle.cs | 9 ++- .../Svg/SvgLegendSerie.cs | 25 ++++++ src/EPPlus.Graphics/BoundingBox.cs | 29 ++++--- 17 files changed, 198 insertions(+), 116 deletions(-) create mode 100644 src/EPPlus.Export.ImageRenderer/Svg/SvgLegendSerie.cs diff --git a/src/EPPlus.Export.ImageRenderer.Test/SvgPathTests.cs b/src/EPPlus.Export.ImageRenderer.Test/SvgPathTests.cs index 0d48a1e94..65eab343e 100644 --- a/src/EPPlus.Export.ImageRenderer.Test/SvgPathTests.cs +++ b/src/EPPlus.Export.ImageRenderer.Test/SvgPathTests.cs @@ -511,7 +511,7 @@ public void GenerateSvgForCharts() { var ws = p.Workbook.Worksheets[0]; var renderer = new EPPlusImageRenderer.ImageRenderer(); - var ix = 0; + var ix = 2; var c = ws.Drawings[ix]; var svg = renderer.RenderDrawingToSvg(c); SaveTextFileToWorkbook($"svg\\ChartForSvg{ix++}.svg", svg); diff --git a/src/EPPlus.Export.ImageRenderer/RenderItems/Shared/TextBodyItem.cs b/src/EPPlus.Export.ImageRenderer/RenderItems/Shared/TextBodyItem.cs index 1b1333fd4..8d99ea434 100644 --- a/src/EPPlus.Export.ImageRenderer/RenderItems/Shared/TextBodyItem.cs +++ b/src/EPPlus.Export.ImageRenderer/RenderItems/Shared/TextBodyItem.cs @@ -50,8 +50,8 @@ public TextBodyItem(DrawingBase renderer, BoundingBox parent) : base(renderer, Bounds.Parent = parent; - Bounds.Width = parent.Width; - Bounds.Height = parent.Height; + //Bounds.Width = parent.Width; + //Bounds.Height = parent.Height; } public void ImportParagraph(ExcelDrawingParagraph item, double startingY, string text=null) diff --git a/src/EPPlus.Export.ImageRenderer/RenderItems/SvgGroupItem.cs b/src/EPPlus.Export.ImageRenderer/RenderItems/SvgGroupItem.cs index 63a5d0d7c..779a91a15 100644 --- a/src/EPPlus.Export.ImageRenderer/RenderItems/SvgGroupItem.cs +++ b/src/EPPlus.Export.ImageRenderer/RenderItems/SvgGroupItem.cs @@ -57,8 +57,8 @@ internal SvgGroupItem(DrawingBase renderer, BoundingBox parent, double rotation) } if(rotation!=0) { - var cx = parent.Width / 2 + parent.Left; - var cy = parent.Height / 2 + parent.Top; + var cx = parent.Width / 2; + var cy = parent.Height / 2; if (cx == 0 && cy == 0) { rot = $"rotate({rotation.ToString(CultureInfo.InvariantCulture)}))"; diff --git a/src/EPPlus.Export.ImageRenderer/RenderItems/SvgItem/SvgParagraphItem.cs b/src/EPPlus.Export.ImageRenderer/RenderItems/SvgItem/SvgParagraphItem.cs index 933ea4a86..c834e9993 100644 --- a/src/EPPlus.Export.ImageRenderer/RenderItems/SvgItem/SvgParagraphItem.cs +++ b/src/EPPlus.Export.ImageRenderer/RenderItems/SvgItem/SvgParagraphItem.cs @@ -75,7 +75,7 @@ public override void Render(StringBuilder sb) { var fontSize = _paragraphFont.Size.PointToPixel().ToString(CultureInfo.InvariantCulture); - sb.AppendLine($""); + sb.AppendLine($""); sb.AppendLine("paragraph "); diff --git a/src/EPPlus.Export.ImageRenderer/RenderItems/SvgItem/SvgTextBodyItem.cs b/src/EPPlus.Export.ImageRenderer/RenderItems/SvgItem/SvgTextBodyItem.cs index 614c069c5..248775248 100644 --- a/src/EPPlus.Export.ImageRenderer/RenderItems/SvgItem/SvgTextBodyItem.cs +++ b/src/EPPlus.Export.ImageRenderer/RenderItems/SvgItem/SvgTextBodyItem.cs @@ -7,6 +7,7 @@ using OfficeOpenXml.Drawing.Theme; using System; using System.Collections.Generic; +using System.ComponentModel; using System.Drawing; using System.Globalization; using System.Linq; @@ -19,18 +20,31 @@ internal class SvgTextBodyItem : TextBodyItem public SvgTextBodyItem(DrawingBase renderer, BoundingBox parent, bool clampedToParent = false) : base(renderer, parent) { Bounds.ClampedToParent = clampedToParent; + Bounds.Width = parent.Width; + Bounds.Height = parent.Height; + } + public SvgTextBodyItem(DrawingBase renderer, BoundingBox parent, double left, double top, double maxWidth, double maxHeight, bool clampedToParent = false) : base(renderer, parent) + { + Bounds.Left = left; + Bounds.Top = top; + Bounds.Width = maxWidth; + Bounds.Height = maxHeight; + Bounds.ClampedToParent = clampedToParent; } internal override List Paragraphs { get; set; } = new List(); internal override void AppendRenderItems(List renderItems) { - //sb.AppendLine($""); - //sb.AppendLine($"txtBody"); - - var groupItem = new SvgGroupItem(DrawingRenderer, Bounds, Bounds.Rotation); + SvgGroupItem groupItem; + if (Bounds.Parent.Rotation == 0) //If the parent is rotated, we should not apply rotation again. This is usually when the parent is a textbox. + { + groupItem = new SvgGroupItem(DrawingRenderer, Bounds, Bounds.Rotation); + } + else + { + groupItem = new SvgGroupItem(DrawingRenderer, Bounds); + } renderItems.Add(groupItem); foreach (SvgParagraphItem item in Paragraphs) { diff --git a/src/EPPlus.Export.ImageRenderer/RenderItems/SvgItem/SvgTextBoxItem.cs b/src/EPPlus.Export.ImageRenderer/RenderItems/SvgItem/SvgTextBoxItem.cs index eb59bb0f0..25bf4f8b2 100644 --- a/src/EPPlus.Export.ImageRenderer/RenderItems/SvgItem/SvgTextBoxItem.cs +++ b/src/EPPlus.Export.ImageRenderer/RenderItems/SvgItem/SvgTextBoxItem.cs @@ -70,7 +70,6 @@ internal override void AppendRenderItems(List renderItems) //handled by group now Rectangle.Bounds.Top = 0; Rectangle.Bounds.Left = 0; - Rectangle.FillOpacity = 0.1; renderItems.Add(Rectangle); TextBody.AppendRenderItems(renderItems); renderItems.Add(new SvgEndGroupItem(DrawingRenderer, Bounds)); diff --git a/src/EPPlus.Export.ImageRenderer/RenderItems/SvgRenderEllipseItem.cs b/src/EPPlus.Export.ImageRenderer/RenderItems/SvgRenderEllipseItem.cs index f9caab6d5..b9f613971 100644 --- a/src/EPPlus.Export.ImageRenderer/RenderItems/SvgRenderEllipseItem.cs +++ b/src/EPPlus.Export.ImageRenderer/RenderItems/SvgRenderEllipseItem.cs @@ -25,10 +25,10 @@ public SvgRenderEllipseItem(DrawingBase renderer, BoundingBox parent) : base(ren { } - public float Cx { get; set; } - public float Cy { get; set; } - public float Rx { get; set; } - public float Ry { get; set; } + public double Cx { get; set; } + public double Cy { get; set; } + public double Rx { get; set; } + public double Ry { get; set; } public override RenderItemType Type => RenderItemType.Rect; diff --git a/src/EPPlus.Export.ImageRenderer/RenderItems/SvgRenderLineItem.cs b/src/EPPlus.Export.ImageRenderer/RenderItems/SvgRenderLineItem.cs index 7163514da..7f83460ca 100644 --- a/src/EPPlus.Export.ImageRenderer/RenderItems/SvgRenderLineItem.cs +++ b/src/EPPlus.Export.ImageRenderer/RenderItems/SvgRenderLineItem.cs @@ -12,9 +12,11 @@ Date Author Change *************************************************************************************************/ using EPPlus.Export.ImageRenderer.Utils; using EPPlus.Graphics; +using EPPlus.Graphics.Math; using EPPlusImageRenderer.Svg; using OfficeOpenXml.Drawing; using OfficeOpenXml.Drawing.Theme; +using System; using System.Globalization; using System.Text; namespace EPPlusImageRenderer.RenderItems @@ -25,10 +27,66 @@ public SvgRenderLineItem(DrawingBase renderer, BoundingBox parent) : base(render { } - public float X1 { get; set; } - public float Y1 { get; set; } - public float X2 { get; set; } - public float Y2 { get; set; } + double _x1, _y1, _x2, _y2; + public double X1 + { + get + { + return _x1; + } + set + { + _x1 = value; + UpdateBounds(); + } + } + public double Y1 + { + get + { + return _y1; + } + set + { + _y1 = value; + UpdateBounds(); + } + } + public double X2 + { + get + { + return _x2; + } + set + { + _x2 = value; + UpdateBounds(); + } + } + public double Y2 + { + get + { + return _y2; + } + set + { + _y2 = value; + UpdateBounds(); + } + } + private void UpdateBounds() + { + var px = Math.Min(X1, X2); + var py = Math.Min(Y1, Y2); + var sizeX = Math.Abs(X2 - X1); + var sizeY = Math.Abs(Y2 - Y1); + + Bounds.Position = new Vector2(px, py); + Bounds.Size = new Vector2(sizeX, sizeY); + } + public override RenderItemType Type => RenderItemType.Line; public override void Render(StringBuilder sb) @@ -50,21 +108,6 @@ internal override SvgRenderItem Clone(SvgShape svgDocument) { var clone = new SvgRenderLineItem(svgDocument, svgDocument.Bounds); CloneBase(clone); - //if (adjustmentpoints != null && adjustmentpoints.commands == null && adjustmentpoints.adjustmenttype == adjustmenttype.adjusttowidthheight) - //{ - // var wh = math.min(width, height); - // clone.x = x * svgdocument.size.item1; - // clone.y = y * svgdocument.size.item2; - // clone.width = svgdocument.size.item1 * wh; - // clone.height = svgdocument.size.item2 * wh; - //} - //else - //{ - // clone.x = x * svgdocument.size.item1; - // clone.y = y * svgdocument.size.item2; - // clone.width = svgdocument.size.item1 * width; - // clone.height = svgdocument.size.item2 * height; - //} return clone; } internal override void GetBounds(out double il, out double it, out double ir, out double ib) diff --git a/src/EPPlus.Export.ImageRenderer/RenderItems/SvgRenderRectItem.cs b/src/EPPlus.Export.ImageRenderer/RenderItems/SvgRenderRectItem.cs index bfedf9249..39f2bd434 100644 --- a/src/EPPlus.Export.ImageRenderer/RenderItems/SvgRenderRectItem.cs +++ b/src/EPPlus.Export.ImageRenderer/RenderItems/SvgRenderRectItem.cs @@ -28,20 +28,16 @@ public SvgRenderRectItem(DrawingBase renderer, BoundingBox parent) : base(render { } - //public SvgRenderRectItem(ExcelDrawing drawing) : base(drawing.GetBoundingBox()) - //{ - - //} - - public double X { get { return Bounds.Left; } set { Bounds.Left = value; } } - public double Y { get { return Bounds.Top; } set { Bounds.Top = value; } } public double Left { get { return Bounds.Left; } set { Bounds.Left = value; } } public double Top { get { return Bounds.Top; } set { Bounds.Top = value; } } public double Width { get { return Bounds.Width; } set { Bounds.Width = value; } } public double Height { get { return Bounds.Height; } set { Bounds.Height = value; } } public double Right { get { return Bounds.Left + Width; } } public double Bottom { get { return Bounds.Top + Height; } } - + public double GlobalLeft => Bounds.GlobalLeft; + public double GlobalTop => Bounds.GlobalTop; + public double GlobalRight => Bounds.GlobalLeft + Width; + public double GlobalBottom => Bounds.GlobalTop + Height; public override RenderItemType Type => RenderItemType.Rect; public override void Render(StringBuilder sb) @@ -57,8 +53,8 @@ public override void Render(StringBuilder sb) internal void RenderRect(StringBuilder sb) { sb.AppendFormat(" xValues, List yValues) @@ -103,7 +106,7 @@ private void AddLine(ExcelChart chartType, ExcelLineChartSerie serie, List(); var markerItems = new List(); @@ -147,7 +150,6 @@ private void AddLine(ExcelChart chartType, ExcelLineChartSerie serie, List renderItems) diff --git a/src/EPPlus.Export.ImageRenderer/Svg/LineMarkerHelper.cs b/src/EPPlus.Export.ImageRenderer/Svg/LineMarkerHelper.cs index 94ed4d92d..1c4738169 100644 --- a/src/EPPlus.Export.ImageRenderer/Svg/LineMarkerHelper.cs +++ b/src/EPPlus.Export.ImageRenderer/Svg/LineMarkerHelper.cs @@ -4,26 +4,24 @@ using EPPlusImageRenderer.Svg; using OfficeOpenXml.Drawing; using OfficeOpenXml.Drawing.Chart; +using System; using System.Collections.Generic; namespace EPPlus.Export.ImageRenderer.Svg { internal class LineMarkerHelper { - internal static RenderItem GetMarkerItem(SvgChart sc, ExcelLineChartSerie ls, float x, float y, bool isLegend) + internal static RenderItem GetMarkerItem(SvgChart sc, ExcelLineChartSerie ls, double x, double y, bool isLegend) { SvgRenderItem item; var m = ls.Marker; float maxSize = isLegend ? 7f : float.MaxValue; var size = m.Size > maxSize ? maxSize : m.Size; - var halfSize = (float)size / 2; - //var line = sls.SeriesIcon as SvgRenderLineItem; - //var x = line.X1 + (line.X2 - line.X1) / 2; - var xPath = x / (float)sc.ChartArea.Width; - //var y = (float)line.Y1; - var yPath = y / (float)sc.ChartArea.Height; - var halfY = (float)halfSize / sc.ChartArea.Height; - var halfX = (float)halfSize / sc.ChartArea.Width; + var halfSize = size / 2; + var xPath = x / sc.ChartArea.Width; + var yPath = y / sc.ChartArea.Height; + var halfY = halfSize / sc.ChartArea.Height; + var halfX = halfSize / sc.ChartArea.Width; switch (m.Style) { case eMarkerStyle.Circle: @@ -158,7 +156,7 @@ internal static RenderItem GetMarkerItem(SvgChart sc, ExcelLineChartSerie ls, fl } return item; } - internal static RenderItem GetMarkerBackground(SvgChart sc, ExcelLineChartSerie ls, float x, float y, bool isLegend) + internal static RenderItem GetMarkerBackground(SvgChart sc, ExcelLineChartSerie ls, double x, double y, bool isLegend) { SvgRenderItem item; var m = ls.Marker; diff --git a/src/EPPlus.Export.ImageRenderer/Svg/SvgChartAxis.cs b/src/EPPlus.Export.ImageRenderer/Svg/SvgChartAxis.cs index 8e20fe3e0..7adcd30a0 100644 --- a/src/EPPlus.Export.ImageRenderer/Svg/SvgChartAxis.cs +++ b/src/EPPlus.Export.ImageRenderer/Svg/SvgChartAxis.cs @@ -82,7 +82,7 @@ internal SvgChartAxis(SvgChart sc, ExcelChartAxisStandard ax) : base(sc) { ll = sc.Legend.Rectangle.Right + sc.Legend.RightMargin; } - Rectangle.X = Title == null ? ll : Title.Rectangle.Right; + Rectangle.Left = Title == null ? ll : Title.Rectangle.Right; } else { @@ -90,15 +90,15 @@ internal SvgChartAxis(SvgChart sc, ExcelChartAxisStandard ax) : base(sc) var lp = sc.ChartArea.Width - Rectangle.Width - 8D; if (sc.Chart.Legend.Position == eLegendPosition.Right) { - lp = sc.Legend.Rectangle.X + -Rectangle.Width; + lp = sc.Legend.Rectangle.Left + -Rectangle.Width; } - Rectangle.X = Title == null ? lp : Title.Rectangle.X - Rectangle.Width; + Rectangle.Left = Title == null ? lp : Title.Rectangle.Left - Rectangle.Width; } } else { Rectangle.Height = GetTextHeight(sc, ax); - Rectangle.Y = Title == null || ax.AxisPosition == eAxisPosition.Top ? sc.ChartArea.Height - 8 - Rectangle.Height : Title.Rectangle.Y - Rectangle.Height - 8; + Rectangle.Top = Title == null || ax.AxisPosition == eAxisPosition.Top ? sc.ChartArea.Height - 8 - Rectangle.Height : Title.Rectangle.Top - Rectangle.Height - 8; } } @@ -337,11 +337,11 @@ private double GetAxisItemTop(int i, OfficeOpenXml.Interfaces.Drawing.Text.TextM { if (Axis.AxisPosition == eAxisPosition.Top) { - return Rectangle.Top - m.Height.PointToPixel(); + return Rectangle.Top - m.Height.PointToPixel() - TopMargin; } else if (Axis.AxisPosition == eAxisPosition.Bottom) { - return Rectangle.Bottom - m.Height.PointToPixel(); + return Rectangle.Bottom - m.Height.PointToPixel() + BottomMargin; } else { @@ -507,27 +507,23 @@ internal double GetPositionInPlotarea(object val) { if (Axis.AxisType == eAxisType.Cat) { - //var index = Values.IndexOf(val); - //if (index < 0) return -1; var majorHeight = SvgChart.Plotarea.Rectangle.Height / Max; - return (/*SvgChart.Plotarea.Rectangle.Top +*/ majorHeight * (int)val + (majorHeight / 2)); + return (majorHeight * (int)val + (majorHeight / 2)); } else { var dVal = ConvertUtil.GetValueDouble(val, false, true); if (dVal < Min || dVal > Max) return double.NaN; var diff = Max - Min; - return (/*SvgChart.Plotarea.Rectangle.Top + */((Max-dVal) / diff * SvgChart.Plotarea.Rectangle.Height)); + return (((Max-dVal) / diff * SvgChart.Plotarea.Rectangle.Height)); } } else { if (Axis.AxisType == eAxisType.Cat) { - //var index = Values[(int)val]; - //if (index < 0) return -1; var majorWidth = SvgChart.Plotarea.Rectangle.Width / Max; - return (/*SvgChart.Plotarea.Rectangle.Left + */majorWidth * (int)val + (majorWidth / 2)); + return (majorWidth * (int)val + (majorWidth / 2)); } else { diff --git a/src/EPPlus.Export.ImageRenderer/Svg/SvgChartLegend.cs b/src/EPPlus.Export.ImageRenderer/Svg/SvgChartLegend.cs index f1fc40b0c..1677748ca 100644 --- a/src/EPPlus.Export.ImageRenderer/Svg/SvgChartLegend.cs +++ b/src/EPPlus.Export.ImageRenderer/Svg/SvgChartLegend.cs @@ -36,7 +36,7 @@ internal class SvgChartLegend : SvgChartObject ITextMeasurer _ttMeasurer; const int MarginExtra = 2; const int MiddleMargin = 10; - const int LineLength = 32; + const int LineLength = 30; internal SvgChartLegend(SvgChart sc) : base(sc) { _ttMeasurer = sc.Chart.WorkSheet._package.Settings.TextSettings.GenericTextMeasurerTrueType; @@ -57,6 +57,11 @@ internal SvgChartLegend(SvgChart sc) : base(sc) { Rectangle = GetLegendRectangle(sc, l); } + Bounds.Left = Rectangle.Left; + Bounds.Top = Rectangle.Top; + Bounds.Width = Rectangle.Width; + Bounds.Height = Rectangle.Height; + Rectangle.Bounds.Left = Rectangle.Bounds.Top = 0; Rectangle.SetDrawingPropertiesFill(l.Fill, sc.Chart.StyleManager.Style.Title.FillReference.Color); Rectangle.SetDrawingPropertiesBorder(l.Border, sc.Chart.StyleManager.Style.Title.BorderReference.Color, l.Border.Fill.Style != eFillStyle.NoFill, 0.75); @@ -194,7 +199,14 @@ internal void SetLegend(SvgChart sc) var tm = _seriesHeadersMeasure[index]; var si = GetSeriesIcon(sc, ls, index, tm, pSls); sls.SeriesIcon = si; - sls.Textbox = new SvgTextBodyItem(ChartRenderer, Rectangle.Bounds); + + var tbLeft = si.X2 + MarginExtra; + var tbTop = si.Y2 - tm.Height * 0.75; //TODO:Should probably be font ascent + var tbWidth = Bounds.Width - tbLeft - RightMargin; + var tbHeight = tm.Height; + sls.Textbox = new SvgTextBodyItem(ChartRenderer, Bounds, tbLeft, tbTop, tbWidth, tbHeight); + sls.Textbox.Bounds.Left = si.X2 + MarginExtra; + var entry = Chart.Legend.Entries.FirstOrDefault(x => x.Index == index); var headerText = s.GetHeaderText(); if (entry == null || entry.Font.IsEmpty) @@ -205,15 +217,16 @@ internal void SetLegend(SvgChart sc) else { //sls.Textbox.AddText(s.GetHeaderText(), entry.Font); - sls.Textbox.ImportParagraph(sc.Chart.Legend.TextBody.Paragraphs.FirstOrDefault(), 0, headerText); + sls.Textbox.ImportParagraph(entry.TextBody.Paragraphs.FirstOrDefault(), 0, headerText); } + if (ls.HasMarker() && ls.Marker.Style != eMarkerStyle.None) { var l = sls.SeriesIcon as SvgRenderLineItem; var x= l.X1 + (l.X2 - l.X1) / 2; var y = l.Y1; sls.MarkerIcon = LineMarkerHelper.GetMarkerItem(sc, ls, x, y, true); - if((ls.Marker.Style==eMarkerStyle.Plus || ls.Marker.Style == eMarkerStyle.X || ls.Marker.Style == eMarkerStyle.Star) && + if((ls.Marker.Style == eMarkerStyle.Plus || ls.Marker.Style == eMarkerStyle.X || ls.Marker.Style == eMarkerStyle.Star) && ls.Marker.Fill.IsEmpty == false) { sls.MarkerBackground = LineMarkerHelper.GetMarkerBackground(sc, ls, x, y, true); @@ -236,7 +249,7 @@ internal void SetLegend(SvgChart sc) private SvgRenderLineItem GetSeriesIcon(SvgChart sc, ExcelLineChartSerie ls, int index, TextMeasurement tm, SvgLegendSerie pSls) { - var item = new SvgRenderLineItem(sc, sc.Bounds); + var item = new SvgRenderLineItem(sc, Rectangle.Bounds); item.SetDrawingPropertiesFill(ls.Fill, sc.Chart.StyleManager.Style.SeriesLine.FillReference.Color); item.SetDrawingPropertiesBorder(ls.Border, sc.Chart.StyleManager.Style.SeriesLine.BorderReference.Color, ls.Border.Fill.Style!=eFillStyle.NoFill, 0.75); @@ -262,10 +275,10 @@ private SvgRenderLineItem GetSeriesIcon(SvgChart sc, ExcelLineChartSerie ls, int } else { - float y; + double y; if (pSls == null) { - y = (float)Rectangle.Top + (float)TopMargin + tm.Height / 2 + MarginExtra; + y = TopMargin + tm.Height / 2 + MarginExtra; } else { @@ -273,9 +286,9 @@ private SvgRenderLineItem GetSeriesIcon(SvgChart sc, ExcelLineChartSerie ls, int y = ((SvgRenderLineItem)pSls.SeriesIcon).Y1 + pTm.Height / 2 + tm.Height / 2 + MiddleMargin; } - item.X1 = (float)Rectangle.Left + MarginExtra; //4 + item.X1 = (float)LeftMargin; //4 item.Y1 = y; - item.X2 = (float)Rectangle.Left + LineLength; + item.X2 = (float)LineLength; item.Y2 = y; item.LineCap = eLineCap.Round; } @@ -285,6 +298,8 @@ private SvgRenderLineItem GetSeriesIcon(SvgChart sc, ExcelLineChartSerie ls, int internal override void AppendRenderItems(List renderItems) { + var groupItem = new SvgGroupItem(ChartRenderer, Bounds); + renderItems.Add(groupItem); renderItems.Add(Rectangle); foreach(var s in SeriesIcon) { @@ -294,16 +309,10 @@ internal override void AppendRenderItems(List renderItems) //renderItems.Add(s.Textbox); s.Textbox.AppendRenderItems(renderItems); } + renderItems.Add(new SvgEndGroupItem(ChartRenderer, null)); } public List SeriesIcon { get; } = new List(); } - internal class SvgLegendSerie - { - internal RenderItem SeriesIcon { get; set; } - internal RenderItem MarkerIcon { get; set; } - internal RenderItem MarkerBackground { get; set; } - internal TextBodyItem Textbox { get; set;} - } } \ No newline at end of file diff --git a/src/EPPlus.Export.ImageRenderer/Svg/SvgChartPlotarea.cs b/src/EPPlus.Export.ImageRenderer/Svg/SvgChartPlotarea.cs index 25fb39098..295f00ce2 100644 --- a/src/EPPlus.Export.ImageRenderer/Svg/SvgChartPlotarea.cs +++ b/src/EPPlus.Export.ImageRenderer/Svg/SvgChartPlotarea.cs @@ -39,21 +39,21 @@ internal SvgRenderRectItem GetPlotAreaRectangle(SvgChart sc) else { var lp = sc.Chart.Legend?.Position; - rect.Top = (lp==eLegendPosition.Top ? sc.Legend.Rectangle.Bottom : sc.Title?.Rectangle?.Bottom ?? 0d) + TopMargin; + rect.Top = (lp==eLegendPosition.Top ? sc.Legend.Rectangle.GlobalBottom : sc.Title?.Rectangle?.GlobalBottom ?? 0d) + TopMargin; if(sc.HorizontalAxis!=null && sc.Chart.XAxis.LabelPosition==eTickLabelPosition.High) { rect.Top += sc.HorizontalAxis.Rectangle.Height; } - rect.Left = lp == eLegendPosition.Left ? sc.Legend.Rectangle.Right + LeftMargin : LeftMargin; + rect.Left = lp == eLegendPosition.Left ? sc.Legend.Rectangle.GlobalRight + LeftMargin : LeftMargin; if(sc.VerticalAxis!=null) { - rect.Left = sc.VerticalAxis.Rectangle?.Right ?? sc.VerticalAxis.Title.Rectangle.Right; + rect.Left = sc.VerticalAxis.Rectangle?.GlobalRight ?? sc.VerticalAxis.Title.Rectangle.GlobalRight; } rect.Width = (lp == eLegendPosition.Right || lp == eLegendPosition.TopRight ? - sc.Legend.Rectangle.Left - RightMargin : + sc.Legend.Bounds.GlobalLeft - RightMargin : sc.ChartArea.Width - RightMargin) - - rect.Left; + - rect.GlobalLeft; double vaHeight=0, vaTitleHeight=0; if(sc.HorizontalAxis != null) @@ -64,7 +64,7 @@ internal SvgRenderRectItem GetPlotAreaRectangle(SvgChart sc) { vaHeight += sc.Legend.Rectangle.Height; } - rect.Height = sc.Bounds.Height - rect.Top - vaHeight - vaTitleHeight - BottomMargin; + rect.Height = sc.Bounds.Height - rect.GlobalTop - vaHeight - vaTitleHeight - BottomMargin; } rect.SetDrawingPropertiesFill(pa.Fill, sc.Chart.StyleManager.Style.PlotArea.FillReference.Color); diff --git a/src/EPPlus.Export.ImageRenderer/Svg/SvgChartTitle.cs b/src/EPPlus.Export.ImageRenderer/Svg/SvgChartTitle.cs index e99965458..092f3f85b 100644 --- a/src/EPPlus.Export.ImageRenderer/Svg/SvgChartTitle.cs +++ b/src/EPPlus.Export.ImageRenderer/Svg/SvgChartTitle.cs @@ -183,10 +183,11 @@ internal void InitTextBox() } if (_title.TextBody.Paragraphs.Count > 0) { - foreach (var p in _title.TextBody.Paragraphs) - { - TextBox.TextBody.ImportParagraph(p, 0); - } + //foreach (var p in _title.TextBody.Paragraphs) + //{ + // TextBox.TextBody.ImportParagraph(p, 0); + //} + TextBox.TextBody.ImportTextBody(_title.TextBody); } else { diff --git a/src/EPPlus.Export.ImageRenderer/Svg/SvgLegendSerie.cs b/src/EPPlus.Export.ImageRenderer/Svg/SvgLegendSerie.cs new file mode 100644 index 000000000..eb6d199b8 --- /dev/null +++ b/src/EPPlus.Export.ImageRenderer/Svg/SvgLegendSerie.cs @@ -0,0 +1,25 @@ +/************************************************************************************************* + Required Notice: Copyright (C) EPPlus Software AB. + This software is licensed under PolyForm Noncommercial License 1.0.0 + and may only be used for noncommercial purposes + https://polyformproject.org/licenses/noncommercial/1.0.0/ + + A commercial license to use this software can be purchased at https://epplussoftware.com + ************************************************************************************************* + Date Author Change + ************************************************************************************************* + 27/11/2025 EPPlus Software AB EPPlus 9 + *************************************************************************************************/ +using EPPlus.Export.ImageRenderer.RenderItems.Shared; +using EPPlusImageRenderer.RenderItems; + +namespace EPPlusImageRenderer.Svg +{ + internal class SvgLegendSerie + { + internal RenderItem SeriesIcon { get; set; } + internal RenderItem MarkerIcon { get; set; } + internal RenderItem MarkerBackground { get; set; } + internal TextBodyItem Textbox { get; set;} + } +} \ No newline at end of file diff --git a/src/EPPlus.Graphics/BoundingBox.cs b/src/EPPlus.Graphics/BoundingBox.cs index bc94157ba..585d0d861 100644 --- a/src/EPPlus.Graphics/BoundingBox.cs +++ b/src/EPPlus.Graphics/BoundingBox.cs @@ -8,32 +8,17 @@ namespace EPPlus.Graphics { internal class BoundingBox : Transform { - //internal Transform Transform { get; } - - //private BoundingBox _parent = null; - - //internal BoundingBox Parent { get { return base.; } set { _parent = value; Transform.Parent = value.Transform; } } - internal bool ClampedToParent { get; set; } = false; internal BoundingBox() : base() { - //Transform = new Transform(); } internal BoundingBox(double width, double height) : base(0, 0, width, height) { - //Left = 0; - //Top = 0; - //Right = width; - //Bottom = height; } internal BoundingBox(double left, double top, double right, double bottom) : base(left,top, right-left, bottom-top) { - //Left = left; - //Top = top; - //Right = right; - //Bottom = bottom; } /// @@ -127,5 +112,19 @@ internal double Height Size = new Vector2(Size.X, value); } } + internal double GlobalLeft + { + get + { + return Position.X; + } + } + internal double GlobalTop + { + get + { + return Position.Y; + } + } } } From 719766abd2c6c9cc8e6983c4177f800b95b20e90 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ossian=20Edstr=C3=B6m?= Date: Wed, 28 Jan 2026 17:34:59 +0100 Subject: [PATCH 025/151] Major re-write of line handling --- .../RenderItems/Shared/ParagraphItem.cs | 179 +++++++++++++++--- .../RenderItems/Shared/TextRunItem.cs | 136 ++++++++----- .../DataHolders/TextFragment.cs | 4 + .../DataHolders/TextFragmentCollection.cs | 77 ++++++-- .../TrueTypeMeasurer/TextData.cs | 99 +++++++++- 5 files changed, 391 insertions(+), 104 deletions(-) diff --git a/src/EPPlus.Export.ImageRenderer/RenderItems/Shared/ParagraphItem.cs b/src/EPPlus.Export.ImageRenderer/RenderItems/Shared/ParagraphItem.cs index 51c355985..7be94f9cc 100644 --- a/src/EPPlus.Export.ImageRenderer/RenderItems/Shared/ParagraphItem.cs +++ b/src/EPPlus.Export.ImageRenderer/RenderItems/Shared/ParagraphItem.cs @@ -132,27 +132,33 @@ internal protected void AddRenderItemTextRun(ExcelParagraphTextRunBase origTxtRu //Create object of type var targetTxtRun = CreateTextRun(origTxtRun, Bounds, displayText); targetTxtRun.LineSpacingPerNewLine = ParagraphLineSpacing; + targetTxtRun.Bounds.Left = startingX; + //if (Runs.Count == 0) + //{ + // targetTxtRun.BaseLineSpacing = _lineSpacingAscendantOnly; + //} - if (Runs.Count == 0 && IsFirstParagraph == true) - { - targetTxtRun.BaseLineSpacing = _lineSpacingAscendantOnly; - } + ////If there are multiple sizes/multiple fonts with multiple sizes + //if (_lsMultiplier.HasValue) + //{ + // var runFont = origTxtRun.GetMeasurementFont(); + // _measurer.SetFont(runFont); + // targetTxtRun.LineSpacingPerNewLine = _lsMultiplier.Value * _measurer.GetSingleLineSpacing().PointToPixel(true); + // targetTxtRun.BaseLineSpacing = _lsMultiplier.Value * _measurer.GetBaseLine().PointToPixel(true); + // //Reset measurer font + // _measurer.SetFont(_paragraphFont); + //} - //If there are multiple sizes/multiple fonts with multiple sizes - if (_lsMultiplier.HasValue) - { - var runFont = origTxtRun.GetMeasurementFont(); - _measurer.SetFont(runFont); - targetTxtRun.LineSpacingPerNewLine = _lsMultiplier.Value * _measurer.GetSingleLineSpacing().PointToPixel(true); - targetTxtRun.BaseLineSpacing = _lsMultiplier.Value * _measurer.GetBaseLine().PointToPixel(true); - //Reset measurer font - _measurer.SetFont(_paragraphFont); - } + //targetTxtRun.Bounds.Left = startingX; + //targetTxtRun.GetBounds(out double l, out double t, out double r, out double b); - targetTxtRun.Bounds.Left = startingX; + //for (int i = 1; i < targetTxtRun.Lines.Count; i++) + //{ - targetTxtRun.GetBounds(out double l, out double t, out double r, out double b); + //} + //targetTxtRun.SetPerLineWidths(_textFragments.GetFragmentWidths(fragIdx)); + //lineIdxAfter = currentLineIdx + targetTxtRun.Lines.Count - 1; Runs.Add(targetTxtRun); } @@ -215,9 +221,22 @@ private void AddLinesAndTextRuns(ExcelDrawingParagraph p, string textIfEmpty) CalculateDisplayText(p, _textFragments); string currentLine = _paragraphLines[0]; + int currentLineIdx = 0; + double widthOfCurrentLine = 0; double largestFontSizeCurrentLine = 0; int idxLargestFontSize = 0; + int firstRunInLineIdx = 0; + + //var lineSizes = _textFragments.GetLargestFontSizesOfEachLine(); + //var currentLineSize = lineSizes[currentLineIdx]; + double lineSpacing = 0; + if(_lsMultiplier.HasValue == false) + { + //linespacing is exact + lineSpacing = ParagraphLineSpacing; + } + if (p.TextRuns.Count == 0 && string.IsNullOrEmpty(textIfEmpty) == false) { AddText(textIfEmpty, p.DefaultRunProperties); @@ -226,35 +245,133 @@ private void AddLinesAndTextRuns(ExcelDrawingParagraph p, string textIfEmpty) { for (int i = 0; i < p.TextRuns.Count; i++) { - if (p.TextRuns[i].FontSize > largestFontSizeCurrentLine) - { - largestFontSizeCurrentLine = p.TextRuns[i].FontSize; - idxLargestFontSize = i; - } - AddRenderItemTextRun(p.TextRuns[i], _textRunDisplayText[i], widthOfCurrentLine); var lastAdded = Runs.Last(); - //We are on a new line - if (lastAdded.YIncreasePerLine.Count > 1) + lastAdded.SetPerLineWidths(_textFragments.GetFragmentWidths(i)); + + if (lastAdded.Lines.Count > 1) { - Runs[idxLargestFontSize].GetBounds(out double l, out double t, out double r, out double b); - Bounds.Height += Runs[idxLargestFontSize].Bounds.Height; - widthOfCurrentLine = Runs.Last().PerLineWidth.Last(); + //Just in case. Should always be empty here + lastAdded.YIncreasePerLine.Clear(); - idxLargestFontSize = i; - largestFontSizeCurrentLine = p.TextRuns[i].FontSize; + //We are on a new line + for (int j = 0; j < lastAdded.Lines.Count; j++) + { + currentLine = _paragraphLines[currentLineIdx]; + + if (_lsMultiplier.HasValue) + { + //Add ascent to descent (Add ascent to Nothing for the first run) + lineSpacing += _textFragments.GetAscent(currentLineIdx).PointToPixel() * _lsMultiplier.Value; + } + + lastAdded.AddLineSpacing(lineSpacing); + + if (_lsMultiplier.HasValue) + { + //Set linespacing to descent + lineSpacing = _textFragments.GetDescent(currentLineIdx).PointToPixel(); + } + currentLineIdx++; + } + + widthOfCurrentLine = Runs.Last().PerLineWidth.Last(); } else { widthOfCurrentLine += Runs.Last().PerLineWidth.Last(); + //If we are on the last run if (i == p.TextRuns.Count - 1) { - Runs[idxLargestFontSize].GetBounds(out double l, out double t, out double r, out double b); - Bounds.Height += Runs[idxLargestFontSize].Bounds.Height; + if (_lsMultiplier.HasValue) + { + //Add ascent to descent (Add ascent to Nothing for the first run) + lineSpacing += _textFragments.GetAscent(currentLineIdx).PointToPixel() * _lsMultiplier.Value; + } + lastAdded.AddLineSpacing(lineSpacing); + //Runs[idxLargestFontSize].GetBounds(out double l, out double t, out double r, out double b); + //Bounds.Height += Runs[idxLargestFontSize].Bounds.Height; } } + ////if(p.TextRuns[i].IsFirstInParagraph) + ////{ + //// lastAdded.BaseLineSpacing = _textFragments.GetAscent(currentLineIdx) * ; + ////} + ////if (Runs.Count == 0) + ////{ + //// targetTxtRun.BaseLineSpacing = _lineSpacingAscendantOnly; + ////} + + //////If there are multiple sizes/multiple fonts with multiple sizes + ////if (_lsMultiplier.HasValue) + ////{ + //// var runFont = origTxtRun.GetMeasurementFont(); + //// _measurer.SetFont(runFont); + //// targetTxtRun.LineSpacingPerNewLine = _lsMultiplier.Value * _measurer.GetSingleLineSpacing().PointToPixel(true); + //// targetTxtRun.BaseLineSpacing = _lsMultiplier.Value * _measurer.GetBaseLine().PointToPixel(true); + //// //Reset measurer font + //// _measurer.SetFont(_paragraphFont); + ////} + + ////targetTxtRun.Bounds.Left = startingX; + ////targetTxtRun.GetBounds(out double l, out double t, out double r, out double b); + + ////for (int i = 1; i < targetTxtRun.Lines.Count; i++) + ////{ + + ////} + ////targetTxtRun.SetPerLineWidths(_textFragments.GetFragmentWidths(fragIdx)); + + ////lineIdxAfter = currentLineIdx + targetTxtRun.Lines.Count - 1; + + //if (lastAdded.YIncreasePerLine.Count > 1) + //{ + // //We are on a new line + + // //Ensure bounds of the largest font size have been calculated + // //Runs[idxLargestFontSize].GetBounds(out double l, out double t, out double r, out double b); + + + // ////Add its height to the bounds height as smaller fonts are irrelevant for height increase + // //Bounds.Height += Runs[idxLargestFontSize].Bounds.Height; + + // //if (firstRunInLineIdx != 0) + // //{ + // // //Excel in addition adds the height to the first y-value as well + // // Runs[firstRunInLineIdx].YIncreasePerLine[1] = Runs[idxLargestFontSize].BaseLineSpacing; + // //} + + // widthOfCurrentLine = Runs.Last().PerLineWidth.Last(); + + // idxLargestFontSize = i; + // firstRunInLineIdx = i; + + // descentLastLine = _textFragments.GetDescent(currentLineIdx); + // //Update + // currentLineIdx++; + // currentLine = _paragraphLines[currentLineIdx]; + // ascentCurrentLine = _textFragments.GetAscent(currentLineIdx); + // //currentLineSize = lineSizes[currentLineIdx]; + //} + //else + //{ + // widthOfCurrentLine += Runs.Last().PerLineWidth.Last(); + // if (i == p.TextRuns.Count - 1) + // { + // Runs[idxLargestFontSize].GetBounds(out double l, out double t, out double r, out double b); + // Bounds.Height += Runs[idxLargestFontSize].Bounds.Height; + // } + //} } + + //var lastAdded = Runs.Last(); + ////Apply correct linespacing to last line as well + //if (firstRunInLineIdx != 0) + //{ + // //Excel in addition adds the height to the first y-value as well + // Runs[firstRunInLineIdx].YIncreasePerLine[1] = Runs[idxLargestFontSize].BaseLineSpacing; + //} } } diff --git a/src/EPPlus.Export.ImageRenderer/RenderItems/Shared/TextRunItem.cs b/src/EPPlus.Export.ImageRenderer/RenderItems/Shared/TextRunItem.cs index 8eac44d2d..df78015f6 100644 --- a/src/EPPlus.Export.ImageRenderer/RenderItems/Shared/TextRunItem.cs +++ b/src/EPPlus.Export.ImageRenderer/RenderItems/Shared/TextRunItem.cs @@ -37,10 +37,6 @@ internal abstract class TextRunItem : RenderItem /// Aka total Delta Y /// double _yEndPos; - double BaselineSpacing; - - bool _wrapText; - double _maxWidthPixels = double.NaN; protected internal bool _isItalic = false; protected internal bool _isBold = false; @@ -155,44 +151,46 @@ internal TextRunItem(DrawingBase renderer, BoundingBox parent, ExcelParagraphTex internal double CalculateLineSpacing() { - //---Calculation of total y-height--- + ////---Calculation of total y-height--- - //If already did this - if(YIncreasePerLine.Count > 0) + ////If already did this + if (YIncreasePerLine.Count > 0) { return Bounds.Height; } - bool lineIsFirstInParagraph = _isFirstInParagraph; - - foreach (var line in Lines) - { - //Despite new textrun it could still be on the same line as previous textrun - //Therefore only do line increase if we are first in paragraph or if we are not Lines[0]. - //This as line == Lines[0] && isFirstInParagraph == false means we are continuing on the same line as previous textRun - //This is important if for example we have rich text where two letters on the same line has different colors. - if (line != Lines[0] || lineIsFirstInParagraph) - { - var yIncrease = lineIsFirstInParagraph ? BaseLineSpacing : LineSpacingPerNewLine; - lineIsFirstInParagraph = false; - - //yIncrease = Fonts.OpenType.Utils.TextUtils.RoundToWhole(yIncrease); - - YIncreasePerLine.Add(yIncrease); - - _yEndPos += yIncrease; - //if (Double.IsNaN(ClippingHeight) == false && _yEndPos >= ClippingHeight) - //{ - // bool displayLine = false - //} - } - else - { - YIncreasePerLine.Add(0); - } - } - - if(_yEndPos == 0) + //bool lineIsFirstInParagraph = _isFirstInParagraph; + + //foreach (var line in Lines) + //{ + // //Despite new textrun it could still be on the same line as previous textrun + // //Therefore only do line increase if we are first in paragraph or if we are not Lines[0]. + // //This as line == Lines[0] && isFirstInParagraph == false means we are continuing on the same line as previous textRun + // //This is important if for example we have rich text where two letters on the same line has different colors. + // if (line != Lines[0] || lineIsFirstInParagraph) + // { + // var yIncrease = lineIsFirstInParagraph ? BaseLineSpacing : LineSpacingPerNewLine; + // lineIsFirstInParagraph = false; + + // //yIncrease = Fonts.OpenType.Utils.TextUtils.RoundToWhole(yIncrease); + + // YIncreasePerLine.Add(yIncrease); + + // _yEndPos += yIncrease; + // //if (Double.IsNaN(ClippingHeight) == false && _yEndPos >= ClippingHeight) + // //{ + // // bool displayLine = false + // //} + // } + // else + // { + // YIncreasePerLine.Add(0); + // } + //} + + //for (int i = 0; i < YIncreasePerLine) + + if (_yEndPos == 0) { Bounds.Height = FontSizeInPixels; } @@ -202,6 +200,7 @@ internal double CalculateLineSpacing() } return _yEndPos; + } internal List GetLines(string text) @@ -227,14 +226,16 @@ internal override void GetBounds(out double il, out double it, out double ir, ou { il = Bounds.Left; it = Bounds.Top; - - //Sets bounds bottom correctly - CalculateLineSpacing(); + //ib = Bounds.Bottom; + ir = Bounds.Right; ib = Bounds.Bottom; - - //Caculate bounds right - ir = CalculateRightPositionInPixels(); - Bounds.Width = ir-Bounds.Left; + ////Sets bounds bottom correctly + //CalculateLineSpacing(); + //ib = Bounds.Bottom; + + ////Caculate bounds right + //ir = CalculateRightPositionInPixels(); + //Bounds.Width = ir-Bounds.Left; } internal double CalculateRightPositionInPixels() @@ -287,8 +288,49 @@ internal double CalculateTextWidth(string targetString) return width; } - // origin.Left = x; origin.Top = y; - // origin.Left = x; - // origin.Top = y; + + internal void SetPerLineWidths(List widthsInPoints) + { + var newList = new List(); + for (int i = 0; i < widthsInPoints.Count; i++) + { + newList.Add(widthsInPoints[i].PointToPixel()); + } + + PerLineWidth.Clear(); + PerLineWidth = newList; + } + + internal void AddLineSpacing(double lineSpacing) + { + YIncreasePerLine.Add(lineSpacing); + _yEndPos += lineSpacing; + Bounds.Height = _yEndPos; + //bool lineIsFirstInParagraph = _isFirstInParagraph; + + //for(int i = 0; i< Lines.Count(); i++) + //{ + // if (Lines[i] != Lines[0] || lineIsFirstInParagraph) + // { + // var yIncrease = lineIsFirstInParagraph ? ascent : ascent + descent; + // YIncreasePerLine.Add() + + // lineIsFirstInParagraph = false; + // } + //} + + //for (int i = 0; i < YIncreasePerLine) + + // if (_yEndPos == 0) + // { + // Bounds.Height = FontSizeInPixels; + // } + // else + // { + // Bounds.Height = _yEndPos; + // } + + //return _yEndPos; + } } } diff --git a/src/EPPlus.Fonts.OpenType/TrueTypeMeasurer/DataHolders/TextFragment.cs b/src/EPPlus.Fonts.OpenType/TrueTypeMeasurer/DataHolders/TextFragment.cs index 37f6e5d39..943d44fd2 100644 --- a/src/EPPlus.Fonts.OpenType/TrueTypeMeasurer/DataHolders/TextFragment.cs +++ b/src/EPPlus.Fonts.OpenType/TrueTypeMeasurer/DataHolders/TextFragment.cs @@ -16,7 +16,11 @@ internal class TextFragment internal List IsPartOfLines; internal string ThisFragment; + internal List PointWidthPerOutputLineInFragment = new(); + internal List PointYIncreasePerLine = new(); + List positionsToBreakAt = new(); + internal float FontSize { get; set; } = float.NaN; internal TextFragment(string thisFragment, int startPos) diff --git a/src/EPPlus.Fonts.OpenType/TrueTypeMeasurer/DataHolders/TextFragmentCollection.cs b/src/EPPlus.Fonts.OpenType/TrueTypeMeasurer/DataHolders/TextFragmentCollection.cs index 33fdbeeab..b2613f926 100644 --- a/src/EPPlus.Fonts.OpenType/TrueTypeMeasurer/DataHolders/TextFragmentCollection.cs +++ b/src/EPPlus.Fonts.OpenType/TrueTypeMeasurer/DataHolders/TextFragmentCollection.cs @@ -17,7 +17,10 @@ public class TextFragmentCollection List _fragmentItems = new List(); List _lines = new List(); List outputFragments = new List(); - List outputStrings = new List(); + //List outputStrings = new List(); + internal List LargestFontSizePerLine { get; private set; } = new(); + internal List AscentPerLine { get; private set; } = new(); + internal List DescentPerLine { get; private set; } = new(); /// /// Added linewidths in points @@ -137,6 +140,21 @@ private List GetFirstCharPositionOfNewLines(string stringsCombined) return positions; } + internal void AddLargestFontSizePerLine(double fontSize) + { + LargestFontSizePerLine.Add(fontSize); + } + + internal void AddAscentPerLine(double Ascent) + { + AscentPerLine.Add(Ascent); + } + + internal void AddDescentPerLine(double Descent) + { + DescentPerLine.Add(Descent); + } + internal void AddWrappingIndex(int index) { IndiciesToWrapAt.Add(index); @@ -291,25 +309,46 @@ internal void AddLineWidth(double width) /// /// /// - public List GetLargestFontSizesOfEachLine() + public List GetLargestFontSizesOfEachLine() { - List largestFontSizes = new List(); - foreach(var line in _lines) - { - float largest = float.MinValue; - foreach(var idx in line.richTextIndicies) - { - if (float.IsNaN(_fragmentItems[idx].FontSize) == false && _fragmentItems[idx].FontSize > largest) - { - largest = _fragmentItems[idx].FontSize; - } - } - if (largest != float.MinValue) - { - largestFontSizes.Add(largest); - } - } - return largestFontSizes; + //List largestFontSizes = new List(); + //foreach(var line in _lines) + //{ + // float largest = float.MinValue; + // foreach(var idx in line.richTextIndicies) + // { + // if (float.IsNaN(_fragmentItems[idx].FontSize) == false && _fragmentItems[idx].FontSize > largest) + // { + // largest = _fragmentItems[idx].FontSize; + // } + // } + // if (largest != float.MinValue) + // { + // largestFontSizes.Add(largest); + // } + //} + //return largestFontSizes; + return LargestFontSizePerLine; + } + + public double GetAscent(int lineIdx) + { + return AscentPerLine[lineIdx]; + } + + public double GetDescent(int lineIdx) + { + return DescentPerLine[lineIdx]; + } + + internal void AddFragmentWidth(int fragmentIdx, double width) + { + _fragmentItems[fragmentIdx].PointWidthPerOutputLineInFragment.Add(width); + } + + public List GetFragmentWidths(int fragmentIdx) + { + return _fragmentItems[fragmentIdx].PointWidthPerOutputLineInFragment; } public List GetLinesFragmentIsPartOf(int fragmentIndex) diff --git a/src/EPPlus.Fonts.OpenType/TrueTypeMeasurer/TextData.cs b/src/EPPlus.Fonts.OpenType/TrueTypeMeasurer/TextData.cs index 1e929abdf..e4a99d28b 100644 --- a/src/EPPlus.Fonts.OpenType/TrueTypeMeasurer/TextData.cs +++ b/src/EPPlus.Fonts.OpenType/TrueTypeMeasurer/TextData.cs @@ -13,6 +13,7 @@ Date Author Change using EPPlus.Fonts.OpenType.Tables.Cmap.Mappings; using EPPlus.Fonts.OpenType.Tables.Kern; using EPPlus.Fonts.OpenType.TrueTypeMeasurer; +using EPPlus.Fonts.OpenType.TrueTypeMeasurer.DataHolders; using OfficeOpenXml.Interfaces.Drawing.Text; using System; using System.Collections.Generic; @@ -302,7 +303,10 @@ internal static double MeasureAscent(OpenTypeFont font, double fontSize) internal static double MeasureDescent(OpenTypeFont font, double fontSize) { - return font.Os2Table.usWinDescent * (fontSize / font.HeadTable.UnitsPerEm); + //TypoDescender is Short WinDescent ushort so we use long so both can be assigned same variable + long descentBase = font.Os2Table.UseTypoMetrics ? -(long)font.Os2Table.sTypoDescender : (long)font.Os2Table.usWinDescent; + + return descentBase * (fontSize / font.HeadTable.UnitsPerEm); } /// @@ -717,6 +721,28 @@ internal static void ConvertDesignUnits(OpenTypeFont origFont, double origSize, wordWidth = Convert.ToInt16(wordWidthInPoints * factorTarget); } + private static void LogFragmentWidth(TextFragmentCollection collection, int UPM, double fontSize, int lineWidth, int prevFragmentWidths, int fragmentIdx) + { + var currentFragmentLineWidth = lineWidth - prevFragmentWidths; + var factorOrig = fontSize / ((double)UPM); + var lineWidthInPoints = currentFragmentLineWidth * factorOrig; + + collection.AddFragmentWidth(fragmentIdx, lineWidthInPoints); + } + + private static void LogLineData(TextParagraph paragraph, OpenTypeFont currentFont, OpenTypeFont largestFont, double CurrentLineLargestSize, int lineWidth, int fragmentIdx) + { + //Log line width + paragraph.Fragments.AddLineWidth(lineWidth / currentFont.HeadTable.UnitsPerEm * paragraph.FontSizes[fragmentIdx]); + + //Log line height + paragraph.Fragments.AddLargestFontSizePerLine(CurrentLineLargestSize); + var lineAscent = GetBaseLine(largestFont, CurrentLineLargestSize); + paragraph.Fragments.AddAscentPerLine(lineAscent); + var lineDescent = MeasureDescent(largestFont, CurrentLineLargestSize); + paragraph.Fragments.AddDescentPerLine(lineDescent); + } + internal static List WrapMultipleTextFragments(TextParagraph paragraph, double maxWidthPoints) { //Initialize variables @@ -738,25 +764,60 @@ internal static List WrapMultipleTextFragments(TextParagraph paragraph, //Does not take new text-strings into account int currentLineIndex = 0; - var fragments = paragraph.Fragments; - var allText = fragments.AllText; - var newLineIndicies = fragments.AllTextNewLineIndicies; + //---Logger variables--- + var fragmentCollection = paragraph.Fragments; + var allText = fragmentCollection.AllText; + var newLineIndicies = fragmentCollection.AllTextNewLineIndicies; + double CurrentLineLargestFontSize = 0; + OpenTypeFont largestFontCurrentLine = paragraph.FontIndexDict[0]; + + var prevFragmentIdx = 0; + var prevFragWidths = 0; + ////Technically currentLineOfFragmentWidth + //int fragmentWidth = 0; + ////Technically lastLINEofFragmentWidth + ////lastFragmentWidth could be a different fragment OR current fragment on a previous line + ////Whichever the previous measured piece of text was + //int lastFragmentWidth = 0; + //---Logger Variables end--- //Iterate through All fragments as one concatenated text string for (int i = 0; i < allText.Length; i++) { //Get char info stored previously in the textParagraph class - var charInfo = fragments.CharLookup[i]; + var charInfo = fragmentCollection.CharLookup[i]; var lineIdx = charInfo.Line; var fragmentIdx = charInfo.Fragment; + //Happens at the end of a fragment/start of a new fragment + if(fragmentIdx != prevFragmentIdx) + { + LogFragmentWidth(paragraph.Fragments, currentFont.HeadTable.UnitsPerEm, fontSize, lineWidth, prevFragWidths, prevFragmentIdx); + + //Since there is no gurantee any wrapping has occured + //But we are moving on to the next fragment we need to know how much of the + //linewidth is not part of the current fragment + //Since there could be multiple fragments before line break add to current prevfragwidth + prevFragWidths += (lineWidth - prevFragWidths); + } + //If we hit a pre-existing line break. Reset line and wordwidths var indexExists = newLineIndicies.Count() > currentLineIndex; if(indexExists) { if (i >= newLineIndicies[currentLineIndex]) { - paragraph.Fragments.AddLineWidth(lineWidth / currentFont.HeadTable.UnitsPerEm * paragraph.FontSizes[fragmentIdx]); + //Log current line data + LogLineData(paragraph, currentFont, largestFontCurrentLine, CurrentLineLargestFontSize, lineWidth, fragmentIdx); + //Log current fragment width + LogFragmentWidth(paragraph.Fragments, currentFont.HeadTable.UnitsPerEm, fontSize, lineWidth, prevFragWidths, fragmentIdx); + //We are on the same fragment but there's been a line break + //Therefore the previous fragment width is 0 as it is within this fragment + prevFragWidths = 0; + + //Since we are on a new line the current largest font-size for this line is the current size + CurrentLineLargestFontSize = paragraph.FontSizes[fragmentIdx]; + largestFontCurrentLine = currentFont; var addedLine = leftOverLine.Trim(['\r', '\n']); wrappedStrings.Add(addedLine); @@ -777,12 +838,22 @@ internal static List WrapMultipleTextFragments(TextParagraph paragraph, ref maxWidth, ref lineWidth, ref wordWidth); //Font/fragment change currentFont = paragraph.FontIndexDict[fragmentIdx]; + if (paragraph.FontSizes[fragmentIdx] > CurrentLineLargestFontSize) + { + largestFontCurrentLine = currentFont; + CurrentLineLargestFontSize = paragraph.FontSizes[fragmentIdx]; + } } } else { currentFont = paragraph.FontIndexDict[fragmentIdx]; maxWidth = (maxWidthPoints * (double)currentFont.HeadTable.UnitsPerEm) / paragraph.FontSizes[fragmentIdx]; + if (paragraph.FontSizes[fragmentIdx] > CurrentLineLargestFontSize) + { + largestFontCurrentLine = currentFont; + CurrentLineLargestFontSize = paragraph.FontSizes[fragmentIdx]; + } } fontSize = paragraph.FontSizes[fragmentIdx]; @@ -797,8 +868,14 @@ internal static List WrapMultipleTextFragments(TextParagraph paragraph, WrapAtCharPos(allText, i, ref prevLineEndIndex, ref lineWidth, ref wordWidth, advanceWidth, wrappedStrings); //Log where wrapping occured in order to keep track of fragment/run/richtext + + //Can't be part of Log function as there could be pre-existing line-breaks paragraph.Fragments.AddWrappingIndex(prevLineEndIndex); - paragraph.Fragments.AddLineWidth(lineWidth / currentFont.HeadTable.UnitsPerEm * paragraph.FontSizes[fragmentIdx]); + //Log line data + LogLineData(paragraph, currentFont, largestFontCurrentLine, CurrentLineLargestFontSize, lineWidth, fragmentIdx); + //Log Fragment widths + LogFragmentWidth(paragraph.Fragments, currentFont.HeadTable.UnitsPerEm, fontSize, lineWidth, prevFragWidths, fragmentIdx); + prevFragWidths = 0; //Since we're using the AllText, need to handle leftover line differently if (i < prevLineEndIndex) @@ -820,14 +897,22 @@ internal static List WrapMultipleTextFragments(TextParagraph paragraph, leftOverLine = allText.Substring(prevLineEndIndex, i - prevLineEndIndex); leftOverLine += allText.Substring(i, 1); } + //Since we are on a new line the current largest font-size for this line is the current size + CurrentLineLargestFontSize = paragraph.FontSizes[fragmentIdx]; + largestFontCurrentLine = currentFont; } else { //Add the current char to current unwrapped line leftOverLine += allText.Substring(i, 1); } + + prevFragmentIdx = fragmentIdx; } + LogLineData(paragraph, currentFont, largestFontCurrentLine, CurrentLineLargestFontSize, lineWidth, paragraph.Fragments.TextFragments.Count()-1); + LogFragmentWidth(paragraph.Fragments, currentFont.HeadTable.UnitsPerEm, fontSize, lineWidth, prevFragWidths, paragraph.Fragments.TextFragments.Count() - 1); + wrappedStrings.Add(leftOverLine); return wrappedStrings; From 1b72f821b7e3fd866801a015ba5e7c18ecae3ab7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jan=20K=C3=A4llman?= Date: Thu, 29 Jan 2026 13:50:35 +0100 Subject: [PATCH 026/151] WIP:Textbox fixes+powerquery metadata --- .../SvgPathTests.cs | 23 ++++++- .../RenderItems/Shared/ParagraphItem.cs | 11 ++-- .../RenderItems/Shared/TextBodyItem.cs | 32 ++++++---- .../RenderItems/SvgItem/SvgParagraphItem.cs | 28 +------- .../RenderItems/SvgItem/SvgTextBodyItem.cs | 17 +++-- .../RenderItems/SvgItem/SvgTextBoxItem.cs | 16 +++-- .../Svg/SvgChartAxis.cs | 20 ++++-- .../Svg/SvgChartLegend.cs | 11 +++- .../Data/PowerQuery/ExcelMetadataItem.cs | 64 +++++++++++++++++-- 9 files changed, 155 insertions(+), 67 deletions(-) diff --git a/src/EPPlus.Export.ImageRenderer.Test/SvgPathTests.cs b/src/EPPlus.Export.ImageRenderer.Test/SvgPathTests.cs index 65eab343e..4d7811054 100644 --- a/src/EPPlus.Export.ImageRenderer.Test/SvgPathTests.cs +++ b/src/EPPlus.Export.ImageRenderer.Test/SvgPathTests.cs @@ -511,7 +511,7 @@ public void GenerateSvgForCharts() { var ws = p.Workbook.Worksheets[0]; var renderer = new EPPlusImageRenderer.ImageRenderer(); - var ix = 2; + var ix = 1; var c = ws.Drawings[ix]; var svg = renderer.RenderDrawingToSvg(c); SaveTextFileToWorkbook($"svg\\ChartForSvg{ix++}.svg", svg); @@ -521,8 +521,29 @@ public void GenerateSvgForCharts() // var svg = renderer.RenderDrawingToSvg(c); // SaveTextFileToWorkbook($"svg\\ChartForSvg{ix++}.svg", svg); //} + } + } + [TestMethod] + public void GenerateSvgForLineCharts() + { + ExcelPackage.License.SetNonCommercialOrganization("EPPlus Project"); + using (var p = OpenTemplatePackage("LineChartRenderTest.xlsx")) + { + var ws = p.Workbook.Worksheets[0]; + var renderer = new EPPlusImageRenderer.ImageRenderer(); + //var ix = 1; + //var c = ws.Drawings[ix]; + //var svg = renderer.RenderDrawingToSvg(c); + //SaveTextFileToWorkbook($"svg\\ChartForSvg{ix++}.svg", svg); + var ix = 1; + foreach (ExcelChart c in ws.Drawings) + { + var svg = renderer.RenderDrawingToSvg(c); + SaveTextFileToWorkbook($"svg\\LineChartForSvg{ix++}.svg", svg); + } } } + [TestMethod] public void GenerateSvgForCharts_sheet2() { diff --git a/src/EPPlus.Export.ImageRenderer/RenderItems/Shared/ParagraphItem.cs b/src/EPPlus.Export.ImageRenderer/RenderItems/Shared/ParagraphItem.cs index 51c355985..47d4d1974 100644 --- a/src/EPPlus.Export.ImageRenderer/RenderItems/Shared/ParagraphItem.cs +++ b/src/EPPlus.Export.ImageRenderer/RenderItems/Shared/ParagraphItem.cs @@ -34,19 +34,21 @@ internal abstract class ParagraphItem : RenderItem TextFragmentCollection _textFragments; internal protected MeasurementFont _paragraphFont; - + internal TextBodyItem ParentTextBody { get; set; } internal double ParagraphLineSpacing { get; private set; } internal List Runs { get; set; } = new List(); - public ParagraphItem(DrawingBase renderer, BoundingBox parent) : base(renderer, parent) + public ParagraphItem(TextBodyItem textBody, DrawingBase renderer, BoundingBox parent) : base(renderer, parent) { + ParentTextBody = textBody; Bounds.Name = "Paragraph"; var defaultFont = new MeasurementFont { FontFamily = "Aptos Narrow", Size = 11, Style = MeasurementFontStyles.Regular }; _paragraphFont = defaultFont; } - public ParagraphItem(DrawingBase renderer, BoundingBox parent, ExcelDrawingParagraph p, string textIfEmpty=null) : base(renderer, parent) + public ParagraphItem(TextBodyItem textBody, DrawingBase renderer, BoundingBox parent, ExcelDrawingParagraph p, string textIfEmpty=null) : base(renderer, parent) { + ParentTextBody = textBody; IsFirstParagraph = p == p._paragraphs[0]; if (p.DefaultRunProperties.Fill != null && p.DefaultRunProperties.Fill.IsEmpty == false) @@ -160,7 +162,7 @@ internal protected void AddRenderItemTextRun(ExcelParagraphTextRunBase origTxtRu public void AddText(string text, ExcelTextFont font) { var measurer = new FontMeasurerTrueType(); - var displayText = measurer.MeasureAndWrapText(text, font.GetMeasureFont(), Bounds.Parent.Size.X); + var displayText = measurer.MeasureAndWrapText(text, font.GetMeasureFont(), ParentTextBody.MaxWidth); var container = CreateTextRun(text, font, Bounds, string.Join("\r\n", displayText.ToArray())); container.BaseLineSpacing = _lineSpacingAscendantOnly; container.LineSpacingPerNewLine = _lsMultiplier.Value * _measurer.GetSingleLineSpacing().PointToPixel(true); @@ -254,6 +256,7 @@ private void AddLinesAndTextRuns(ExcelDrawingParagraph p, string textIfEmpty) Bounds.Height += Runs[idxLargestFontSize].Bounds.Height; } } + Bounds.Width = widthOfCurrentLine; } } } diff --git a/src/EPPlus.Export.ImageRenderer/RenderItems/Shared/TextBodyItem.cs b/src/EPPlus.Export.ImageRenderer/RenderItems/Shared/TextBodyItem.cs index 8d99ea434..62c38ce53 100644 --- a/src/EPPlus.Export.ImageRenderer/RenderItems/Shared/TextBodyItem.cs +++ b/src/EPPlus.Export.ImageRenderer/RenderItems/Shared/TextBodyItem.cs @@ -22,6 +22,22 @@ namespace EPPlus.Export.ImageRenderer.RenderItems.Shared /// internal abstract class TextBodyItem : DrawingObject { + public TextBodyItem(DrawingBase renderer, BoundingBox parent) : base(renderer, parent) + { + Bounds.Name = "TxtBody"; + + Bounds.Parent = parent; + } + public TextBodyItem(DrawingBase renderer, BoundingBox parent, double maxWidth, double maxHeight) : base(renderer, parent) + { + Bounds.Name = "TxtBody"; + Bounds.Parent = parent; + MaxWidth = maxWidth; + MaxHeight = maxHeight; + } + internal double MaxWidth { get; set; } + internal double MaxHeight { get; set; } + /// /// Shorthand for Bounds.Width /// @@ -44,22 +60,12 @@ internal abstract class TextBodyItem : DrawingObject private FontMeasurerTrueType _measurer = null; internal string _text; - public TextBodyItem(DrawingBase renderer, BoundingBox parent) : base(renderer, parent) - { - Bounds.Name = "TxtBody"; - - Bounds.Parent = parent; - - //Bounds.Width = parent.Width; - //Bounds.Height = parent.Height; - } - public void ImportParagraph(ExcelDrawingParagraph item, double startingY, string text=null) { var measureFont = item.DefaultRunProperties.GetMeasureFont(); bool isFirst = Paragraphs.Count == 0; - var paragraph = CreateParagraph(item, Bounds, text); + var paragraph = CreateParagraph(this, item, Bounds, text); paragraph.Bounds.Name = $"Container{Paragraphs.Count}"; paragraph.Bounds.Top = startingY; _text = text; @@ -207,12 +213,12 @@ private double GetAlignmentVertical() /// Each file format defines its own paragraph /// /// - internal abstract ParagraphItem CreateParagraph(ExcelDrawingParagraph paragraph, BoundingBox parent, string textIfEmpty=""); + internal abstract ParagraphItem CreateParagraph(TextBodyItem textBody, ExcelDrawingParagraph paragraph, BoundingBox parent, string textIfEmpty=""); /// /// Each file format defines its own paragraph /// /// - internal abstract ParagraphItem CreateParagraph(BoundingBox parent); + internal abstract ParagraphItem CreateParagraph(TextBodyItem textBody, BoundingBox parent); } } diff --git a/src/EPPlus.Export.ImageRenderer/RenderItems/SvgItem/SvgParagraphItem.cs b/src/EPPlus.Export.ImageRenderer/RenderItems/SvgItem/SvgParagraphItem.cs index c834e9993..f6cc49f90 100644 --- a/src/EPPlus.Export.ImageRenderer/RenderItems/SvgItem/SvgParagraphItem.cs +++ b/src/EPPlus.Export.ImageRenderer/RenderItems/SvgItem/SvgParagraphItem.cs @@ -15,37 +15,13 @@ internal class SvgParagraphItem : ParagraphItem { public override RenderItemType Type => RenderItemType.Paragraph; - public SvgParagraphItem(DrawingBase renderer, BoundingBox parent) : base(renderer, parent) + public SvgParagraphItem(TextBodyItem textBody, DrawingBase renderer, BoundingBox parent) : base(textBody, renderer, parent) { } - public SvgParagraphItem(DrawingBase renderer, BoundingBox parent, ExcelDrawingParagraph p, string textIfEmpty = null) : base(renderer, parent, p, textIfEmpty) + public SvgParagraphItem(TextBodyItem textBody, DrawingBase renderer, BoundingBox parent, ExcelDrawingParagraph p, string textIfEmpty = null) : base(textBody, renderer, parent, p, textIfEmpty) { } - - private string GetHorizontalAlignmentAttribute(double indentX) - { - string ret = ""; - var xStr = indentX.ToString(CultureInfo.InvariantCulture); - - switch (_hAlign) - { - default: - case eTextAlignment.Left: - ret = $"text-anchor=\"start\" x=\"{xStr}\" "; - break; - case eTextAlignment.Center: - ret = $"text-anchor=\"middle\" x=\"{xStr}\" "; - break; - case eTextAlignment.Right: - - ret = $"text-anchor=\"end\" x=\"{xStr}\" "; - break; - } - - return ret; - } - //private string GetVerticalAlignAttribute(double textOrigY) //{ // string ret = "dominant-baseline="; diff --git a/src/EPPlus.Export.ImageRenderer/RenderItems/SvgItem/SvgTextBodyItem.cs b/src/EPPlus.Export.ImageRenderer/RenderItems/SvgItem/SvgTextBodyItem.cs index 248775248..e6ae234a8 100644 --- a/src/EPPlus.Export.ImageRenderer/RenderItems/SvgItem/SvgTextBodyItem.cs +++ b/src/EPPlus.Export.ImageRenderer/RenderItems/SvgItem/SvgTextBodyItem.cs @@ -20,8 +20,10 @@ internal class SvgTextBodyItem : TextBodyItem public SvgTextBodyItem(DrawingBase renderer, BoundingBox parent, bool clampedToParent = false) : base(renderer, parent) { Bounds.ClampedToParent = clampedToParent; - Bounds.Width = parent.Width; - Bounds.Height = parent.Height; + MaxWidth = parent.Width; + MaxHeight = parent.Height; + //Bounds.Width = MaxWidth; + //Bounds.Height = MaxHeight; } public SvgTextBodyItem(DrawingBase renderer, BoundingBox parent, double left, double top, double maxWidth, double maxHeight, bool clampedToParent = false) : base(renderer, parent) { @@ -29,9 +31,10 @@ public SvgTextBodyItem(DrawingBase renderer, BoundingBox parent, double left, do Bounds.Top = top; Bounds.Width = maxWidth; Bounds.Height = maxHeight; + MaxWidth = maxWidth; + MaxHeight = maxHeight; Bounds.ClampedToParent = clampedToParent; } - internal override List Paragraphs { get; set; } = new List(); internal override void AppendRenderItems(List renderItems) @@ -53,14 +56,14 @@ internal override void AppendRenderItems(List renderItems) renderItems.Add(new SvgEndGroupItem(DrawingRenderer, Bounds)); } - internal override ParagraphItem CreateParagraph(BoundingBox parent) + internal override ParagraphItem CreateParagraph(TextBodyItem textBody, BoundingBox parent) { - return new SvgParagraphItem(DrawingRenderer, parent); + return new SvgParagraphItem(this, DrawingRenderer, parent); } - internal override ParagraphItem CreateParagraph(ExcelDrawingParagraph paragraph, BoundingBox parent, string textIfEmpty = null) + internal override ParagraphItem CreateParagraph(TextBodyItem textBody, ExcelDrawingParagraph paragraph, BoundingBox parent, string textIfEmpty = null) { - return new SvgParagraphItem(DrawingRenderer, parent, paragraph, textIfEmpty); + return new SvgParagraphItem(this, DrawingRenderer, parent, paragraph, textIfEmpty); } } } diff --git a/src/EPPlus.Export.ImageRenderer/RenderItems/SvgItem/SvgTextBoxItem.cs b/src/EPPlus.Export.ImageRenderer/RenderItems/SvgItem/SvgTextBoxItem.cs index 25bf4f8b2..ddca27d1e 100644 --- a/src/EPPlus.Export.ImageRenderer/RenderItems/SvgItem/SvgTextBoxItem.cs +++ b/src/EPPlus.Export.ImageRenderer/RenderItems/SvgItem/SvgTextBoxItem.cs @@ -13,20 +13,26 @@ namespace EPPlus.Export.ImageRenderer.RenderItems.SvgItem { internal class SvgTextBoxItem : DrawingObject { - internal SvgTextBoxItem(DrawingBase renderer, BoundingBox parent, double left, double top, double maxWidth, double maxHeight) : base(renderer, parent) + internal SvgTextBoxItem(DrawingBase renderer, BoundingBox parent, double left, double top, double width, double height, double maxWidth = double.NaN, double maxHeight = double.NaN) : base(renderer, parent) { Bounds.Left = left; Bounds.Top = top; - Bounds.Width = maxWidth; - Bounds.Height = maxHeight; + Bounds.Width = width; + Bounds.Height = height; Bounds.Name = "TextBox"; + TextBody = new SvgTextBodyItem(renderer, Bounds, true); - Bounds.Width = TextBody.Width; - Bounds.Height = TextBody.Height; + TextBody.MaxWidth = maxWidth; + TextBody.MaxHeight = maxHeight; + //Bounds.Width = TextBody.Width; + //Bounds.Height = TextBody.Height; Rectangle = new SvgRenderRectItem(renderer, parent); Rectangle.Bounds = Bounds; } + internal SvgTextBoxItem(DrawingBase renderer, BoundingBox parent, double maxWidth, double maxHeight) : base(renderer, parent) + { + } //Simplified input internal SvgTextBoxItem(DrawingBase renderer, BoundingBox parent, BoundingBox maxBounds) : this( diff --git a/src/EPPlus.Export.ImageRenderer/Svg/SvgChartAxis.cs b/src/EPPlus.Export.ImageRenderer/Svg/SvgChartAxis.cs index 7adcd30a0..2f7405fd1 100644 --- a/src/EPPlus.Export.ImageRenderer/Svg/SvgChartAxis.cs +++ b/src/EPPlus.Export.ImageRenderer/Svg/SvgChartAxis.cs @@ -283,7 +283,18 @@ private List GetAxisValueTextBoxes() var mf = Axis.Font.GetMeasureFont(); var axisStyle = GetAxisStyleEntry(); var ret= new List(); - for (var i=0;i < AxisValues.Count;i++) + double maxWidth, maxHeight; + if(Axis.AxisPosition==eAxisPosition.Left || Axis.AxisPosition == eAxisPosition.Right) + { + maxWidth = SvgChart.ChartArea.Bounds.Width / 3; //TODO: Check this value. + maxHeight = Rectangle.Height / AxisValues.Count; + } + else + { + maxWidth = Rectangle.Width / AxisValues.Count; + maxHeight = SvgChart.ChartArea.Bounds.Height / 3; //TODO: Check this value. + } + for (var i = 0; i < AxisValues.Count; i++) { var v = AxisValues[i]; var m = tm.MeasureText(v, mf); @@ -294,8 +305,9 @@ private List GetAxisValueTextBoxes() //bounds.Left = x; //bounds.Top = y; var width = m.Width.PointToPixel(); - var height = m.Height.PointToPixel(); - var tb = new SvgTextBoxItem(SvgChart, Rectangle.Bounds, x, y, width, height); + var height = m.Height.PointToPixel(); + var tb = new SvgTextBoxItem(SvgChart, Rectangle.Bounds, x, y, width, height, maxWidth, maxHeight); + var p = Axis.TextBody.Paragraphs.FirstOrDefault(); tb.TextBody.ImportParagraph(p, 0, v); @@ -309,7 +321,7 @@ private List GetAxisValueTextBoxes() private double GetAxisItemLeft(int i, OfficeOpenXml.Interfaces.Drawing.Text.TextMeasurement m) { if (Axis.AxisPosition == eAxisPosition.Left) - { + { return Rectangle.Left; } else if (Axis.AxisPosition == eAxisPosition.Right) diff --git a/src/EPPlus.Export.ImageRenderer/Svg/SvgChartLegend.cs b/src/EPPlus.Export.ImageRenderer/Svg/SvgChartLegend.cs index 1677748ca..ab9ad5089 100644 --- a/src/EPPlus.Export.ImageRenderer/Svg/SvgChartLegend.cs +++ b/src/EPPlus.Export.ImageRenderer/Svg/SvgChartLegend.cs @@ -182,6 +182,7 @@ internal void SetLegend(SvgChart sc) { int index = 0; SvgLegendSerie pSls=null; + var pos = Chart.Legend.Position; foreach (var ct in sc.Chart.PlotArea.ChartTypes) { foreach (var s in ct.Series) @@ -202,7 +203,15 @@ internal void SetLegend(SvgChart sc) var tbLeft = si.X2 + MarginExtra; var tbTop = si.Y2 - tm.Height * 0.75; //TODO:Should probably be font ascent - var tbWidth = Bounds.Width - tbLeft - RightMargin; + double tbWidth; + if (pos == eLegendPosition.Left || pos == eLegendPosition.Right) + { + tbWidth = Bounds.Width - tbLeft - RightMargin; + } + else + { + tbWidth = Bounds.Width - tbLeft - RightMargin; + } var tbHeight = tm.Height; sls.Textbox = new SvgTextBodyItem(ChartRenderer, Bounds, tbLeft, tbTop, tbWidth, tbHeight); sls.Textbox.Bounds.Left = si.X2 + MarginExtra; diff --git a/src/EPPlus/Data/PowerQuery/ExcelMetadataItem.cs b/src/EPPlus/Data/PowerQuery/ExcelMetadataItem.cs index fb1ebfc25..f80dc3a8b 100644 --- a/src/EPPlus/Data/PowerQuery/ExcelMetadataItem.cs +++ b/src/EPPlus/Data/PowerQuery/ExcelMetadataItem.cs @@ -33,27 +33,79 @@ internal ExcelPowerQueryMetadataItem(XmlNamespaceManager nsm, XmlNode topNode, C var type = node.GetAttribute("Type"); var sv = node.GetAttribute("Value"); object value; - switch(sv[0]) + var prefix = sv[0]; + var s = sv.Substring(1); + switch (sv[0]) { case 's': case 'S': - value = sv.Substring(1); + value = s; break; case 'l': case 'L': - value = int.Parse(sv.Substring(1), culture); + if(long.TryParse(s, NumberStyles.Integer, CultureInfo.InvariantCulture, out long l)) + { + value = l; + } + else + { + value = s; + } break; case 'b': case 'B': - value = bool.Parse(sv.Substring(1)); + if(bool.TryParse(s, out bool b)) + { + value = b; + } + else + { + value = s; + } break; case 'd': case 'D': - value = DateTime.Parse(sv.Substring(1), culture); + if(DateTime.TryParse(s, CultureInfo.InvariantCulture, DateTimeStyles.AssumeUniversal, out DateTime dt)) + { + value = dt; + } + else + { + value = s; + } break; case 'f': case 'F': - value = double.Parse(sv.Substring(1), culture); + if(double.TryParse(s, NumberStyles.Any, CultureInfo.InvariantCulture, out double f)) + { + value = f; + } + else + { + value = s; + } + break; + case 'c': + case 'C': +#if (Core) + if(Guid.TryParse(s, CultureInfo.InvariantCulture, out Guid guid)) + { + value = guid; + } + else + { + value = s; + } +#else + try + { + value = new Guid(s); + } + catch + { + value = s; + } +#endif break; default: throw new InvalidOperationException($"Invalid or no data type on Power Query meta data entry with name {type}"); From de227140cafe709f6f737129739807b92ae4c061 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jan=20K=C3=A4llman?= Date: Thu, 29 Jan 2026 14:53:37 +0100 Subject: [PATCH 027/151] Fixes issue #2261 --- .../Data/PowerQuery/ExcelMetadataItem.cs | 72 ++++++++++++++++--- 1 file changed, 63 insertions(+), 9 deletions(-) diff --git a/src/EPPlus/Data/PowerQuery/ExcelMetadataItem.cs b/src/EPPlus/Data/PowerQuery/ExcelMetadataItem.cs index fb1ebfc25..369794c27 100644 --- a/src/EPPlus/Data/PowerQuery/ExcelMetadataItem.cs +++ b/src/EPPlus/Data/PowerQuery/ExcelMetadataItem.cs @@ -13,6 +13,7 @@ Date Author Change using System; using System.Collections.Generic; using System.Globalization; +using System.Security.Cryptography; using System.Xml; namespace OfficeOpenXml.Data.Connection @@ -33,30 +34,83 @@ internal ExcelPowerQueryMetadataItem(XmlNamespaceManager nsm, XmlNode topNode, C var type = node.GetAttribute("Type"); var sv = node.GetAttribute("Value"); object value; - switch(sv[0]) + var prefix = sv[0]; + var s = sv.Substring(1); + switch (prefix) { case 's': case 'S': - value = sv.Substring(1); + value = s; break; case 'l': case 'L': - value = int.Parse(sv.Substring(1), culture); + if (long.TryParse(s, NumberStyles.Integer, CultureInfo.InvariantCulture, out long l)) + { + value = l; + } + else + { + value = s; + } break; case 'b': case 'B': - value = bool.Parse(sv.Substring(1)); + if (bool.TryParse(s, out bool b)) + { + value = b; + } + else + { + value = s; + } break; case 'd': case 'D': - value = DateTime.Parse(sv.Substring(1), culture); + if (DateTime.TryParse(s, CultureInfo.InvariantCulture, DateTimeStyles.AssumeUniversal, out DateTime dt)) + { + value = dt; + } + else + { + value = s; + } break; case 'f': case 'F': - value = double.Parse(sv.Substring(1), culture); + if (double.TryParse(s, NumberStyles.Any, CultureInfo.InvariantCulture, out double f)) + { + value = f; + } + else + { + value = s; + } + break; + case 'c': + case 'C': +#if (NET8_0_OR_GREATER) + if (Guid.TryParse(s, CultureInfo.InvariantCulture, out Guid guid)) + { + value = guid; + } + else + { + value = s; + } +#else + try + { + value = new Guid(s); + } + catch + { + value = s; + } +#endif break; default: - throw new InvalidOperationException($"Invalid or no data type on Power Query meta data entry with name {type}"); + value = sv; + break; } Entries.Add(new ExcelPowerQueryMetaDataEntry(type, value, true, false)); } @@ -72,9 +126,9 @@ internal ExcelPowerQueryMetadataItem(XmlNamespaceManager nsm, XmlNode topNode, C /// /// A collection of metadata entries. /// - public List Entries + public List Entries { - get; + get; } = new List(); } } \ No newline at end of file From 8b9930df4e1f531fb84816e40fbd3dcbb3feb525 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jan=20K=C3=A4llman?= Date: Thu, 29 Jan 2026 15:55:04 +0100 Subject: [PATCH 028/151] WIP:Textbox --- src/EPPlus.Export.ImageRenderer.Test/SvgPathTests.cs | 4 ++-- .../RenderItems/Shared/ParagraphItem.cs | 3 ++- .../RenderItems/Shared/TextBodyItem.cs | 8 ++++++++ .../RenderItems/Shared/TextRunItem.cs | 4 +++- src/EPPlus.Export.ImageRenderer/Svg/SvgChartLegend.cs | 1 + src/EPPlus/Data/PowerQuery/ExcelMetadataItem.cs | 2 +- 6 files changed, 17 insertions(+), 5 deletions(-) diff --git a/src/EPPlus.Export.ImageRenderer.Test/SvgPathTests.cs b/src/EPPlus.Export.ImageRenderer.Test/SvgPathTests.cs index 4d7811054..e3f272d5d 100644 --- a/src/EPPlus.Export.ImageRenderer.Test/SvgPathTests.cs +++ b/src/EPPlus.Export.ImageRenderer.Test/SvgPathTests.cs @@ -511,7 +511,7 @@ public void GenerateSvgForCharts() { var ws = p.Workbook.Worksheets[0]; var renderer = new EPPlusImageRenderer.ImageRenderer(); - var ix = 1; + var ix = 0; var c = ws.Drawings[ix]; var svg = renderer.RenderDrawingToSvg(c); SaveTextFileToWorkbook($"svg\\ChartForSvg{ix++}.svg", svg); @@ -521,7 +521,7 @@ public void GenerateSvgForCharts() // var svg = renderer.RenderDrawingToSvg(c); // SaveTextFileToWorkbook($"svg\\ChartForSvg{ix++}.svg", svg); //} - } + } } [TestMethod] public void GenerateSvgForLineCharts() diff --git a/src/EPPlus.Export.ImageRenderer/RenderItems/Shared/ParagraphItem.cs b/src/EPPlus.Export.ImageRenderer/RenderItems/Shared/ParagraphItem.cs index 47d4d1974..45fb318a1 100644 --- a/src/EPPlus.Export.ImageRenderer/RenderItems/Shared/ParagraphItem.cs +++ b/src/EPPlus.Export.ImageRenderer/RenderItems/Shared/ParagraphItem.cs @@ -167,7 +167,7 @@ public void AddText(string text, ExcelTextFont font) container.BaseLineSpacing = _lineSpacingAscendantOnly; container.LineSpacingPerNewLine = _lsMultiplier.Value * _measurer.GetSingleLineSpacing().PointToPixel(true); Runs.Add(container); - + Bounds.Width = container.Bounds.Width + 0.001; //TODO: fix for equal width issue container.Bounds.Name = $"Container{Runs.Count}"; } @@ -223,6 +223,7 @@ private void AddLinesAndTextRuns(ExcelDrawingParagraph p, string textIfEmpty) if (p.TextRuns.Count == 0 && string.IsNullOrEmpty(textIfEmpty) == false) { AddText(textIfEmpty, p.DefaultRunProperties); + Bounds.Width = Runs.Sum(x=>x.Bounds.Width); } else { diff --git a/src/EPPlus.Export.ImageRenderer/RenderItems/Shared/TextBodyItem.cs b/src/EPPlus.Export.ImageRenderer/RenderItems/Shared/TextBodyItem.cs index 62c38ce53..8dd655fee 100644 --- a/src/EPPlus.Export.ImageRenderer/RenderItems/Shared/TextBodyItem.cs +++ b/src/EPPlus.Export.ImageRenderer/RenderItems/Shared/TextBodyItem.cs @@ -69,6 +69,14 @@ public void ImportParagraph(ExcelDrawingParagraph item, double startingY, string paragraph.Bounds.Name = $"Container{Paragraphs.Count}"; paragraph.Bounds.Top = startingY; _text = text; + if(Paragraphs.Count == 0) + { + Bounds.Width = paragraph.Bounds.Width; + } + else + { + Bounds.Width += paragraph.Bounds.Width; + } Paragraphs.Add(paragraph); } diff --git a/src/EPPlus.Export.ImageRenderer/RenderItems/Shared/TextRunItem.cs b/src/EPPlus.Export.ImageRenderer/RenderItems/Shared/TextRunItem.cs index 8eac44d2d..46497dd18 100644 --- a/src/EPPlus.Export.ImageRenderer/RenderItems/Shared/TextRunItem.cs +++ b/src/EPPlus.Export.ImageRenderer/RenderItems/Shared/TextRunItem.cs @@ -93,11 +93,13 @@ internal TextRunItem(DrawingBase renderer, BoundingBox parent, string text, Exce } if(Lines.Count==1) { - Bounds.Width = parent.Width; + //Bounds.Width = parent.Width; + GetBounds(out double il, out double it, out double ir, out double ib); //TODO: remove when calc works } else { //Measure text. + GetBounds(out double il, out double it, out double ir, out double ib); //TODO: remove when calc works } _isItalic = font.Italic; _isBold = font.Bold; diff --git a/src/EPPlus.Export.ImageRenderer/Svg/SvgChartLegend.cs b/src/EPPlus.Export.ImageRenderer/Svg/SvgChartLegend.cs index ab9ad5089..71565d3e5 100644 --- a/src/EPPlus.Export.ImageRenderer/Svg/SvgChartLegend.cs +++ b/src/EPPlus.Export.ImageRenderer/Svg/SvgChartLegend.cs @@ -212,6 +212,7 @@ internal void SetLegend(SvgChart sc) { tbWidth = Bounds.Width - tbLeft - RightMargin; } + var tbHeight = tm.Height; sls.Textbox = new SvgTextBodyItem(ChartRenderer, Bounds, tbLeft, tbTop, tbWidth, tbHeight); sls.Textbox.Bounds.Left = si.X2 + MarginExtra; diff --git a/src/EPPlus/Data/PowerQuery/ExcelMetadataItem.cs b/src/EPPlus/Data/PowerQuery/ExcelMetadataItem.cs index f80dc3a8b..d98fabfff 100644 --- a/src/EPPlus/Data/PowerQuery/ExcelMetadataItem.cs +++ b/src/EPPlus/Data/PowerQuery/ExcelMetadataItem.cs @@ -87,7 +87,7 @@ internal ExcelPowerQueryMetadataItem(XmlNamespaceManager nsm, XmlNode topNode, C break; case 'c': case 'C': -#if (Core) +#if (NET8_0_OR_GREATER) if(Guid.TryParse(s, CultureInfo.InvariantCulture, out Guid guid)) { value = guid; From 5a14d9f2781af0fc7186747ebcf077fa0ed901ac Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ossian=20Edstr=C3=B6m?= Date: Thu, 29 Jan 2026 16:44:02 +0100 Subject: [PATCH 029/151] Broken messy solution --- .../TextRenderTests.cs | 52 ++++ .../RenderItems/Shared/ParagraphItem.cs | 32 +- .../RenderItems/SvgItem/SvgTextRunItem.cs | 1 - .../DataHolders/RichTextFragmentSimple.cs | 22 ++ .../DataHolders/TextFragmentCollection.cs | 28 ++ .../TrueTypeMeasurer/DataHolders/TextLine.cs | 1 - .../DataHolders/TextLineSimple.cs | 32 ++ .../TrueTypeMeasurer/FontMeasurerTrueType.cs | 8 + .../TrueTypeMeasurer/TextData.cs | 290 ++++++++++++++++++ 9 files changed, 463 insertions(+), 3 deletions(-) create mode 100644 src/EPPlus.Fonts.OpenType/TrueTypeMeasurer/DataHolders/RichTextFragmentSimple.cs create mode 100644 src/EPPlus.Fonts.OpenType/TrueTypeMeasurer/DataHolders/TextLineSimple.cs diff --git a/src/EPPlus.Export.ImageRenderer.Test/TextRenderTests.cs b/src/EPPlus.Export.ImageRenderer.Test/TextRenderTests.cs index 98aab5157..6d840b542 100644 --- a/src/EPPlus.Export.ImageRenderer.Test/TextRenderTests.cs +++ b/src/EPPlus.Export.ImageRenderer.Test/TextRenderTests.cs @@ -8,10 +8,12 @@ using EPPlusImageRenderer.Svg; using OfficeOpenXml; using OfficeOpenXml.Drawing; +using OfficeOpenXml.FormulaParsing.Excel.Functions.MathFunctions; using OfficeOpenXml.Interfaces.Drawing.Text; using OfficeOpenXml.Style; using System.Diagnostics; using System.Globalization; +using System.Text; namespace EPPlus.Export.ImageRenderer.Tests { @@ -57,6 +59,38 @@ private void SetFillColor(ExcelDrawingFillBasic fill, string color) } } + TextFragmentCollection GenerateTextFragments(ExcelDrawingTextRunCollection runs) + { + List runContents = new List(); + List fontSizes = new List(); + + for (int i = 0; i < runs.Count(); i++) + { + var txtRun = runs[i]; + var runFont = txtRun.GetMeasurementFont(); + + runContents.Add(txtRun.Text); + fontSizes.Add(runFont.Size); + } + + return new TextFragmentCollection(runContents, fontSizes); + } + + List GetWrappedText(ExcelDrawingTextRunCollection runs, TextFragmentCollection fragments) + { + FontMeasurerTrueType ttMeasurer = new(); + List fonts = new List(); + + for (int i = 0; i < runs.Count(); i++) + { + var txtRun = runs[i]; + var runFont = txtRun.GetMeasurementFont(); + fonts.Add(runFont); + } + + var maxSizePoints = Math.Round(300d, 0, MidpointRounding.AwayFromZero).PixelToPoint(); + return ttMeasurer.WrapMultipleTextFragmentsToTextLines(fragments, fonts, maxSizePoints); + } [TestMethod] public void VerifyTextRunBounds() @@ -114,6 +148,24 @@ public void VerifyTextRunBounds() var svgShape = new SvgShape(cube); SvgTextBodyItem tbItem = new SvgTextBodyItem(svgShape, parentBB); + //Verify new method works at all + for(int i = 1; i< cube.TextBody.Paragraphs.Count; i++) + { + var fragments = GenerateTextFragments(cube.TextBody.Paragraphs[i].TextRuns); + + var wrappedText = GetWrappedText(cube.TextBody.Paragraphs[i].TextRuns, fragments); + if(i == 0) + { + Assert.AreEqual("TextBox", wrappedText[0].Text); + Assert.AreEqual("a", wrappedText[1].Text); + } + if(i == 1) + { + Assert.AreEqual("TextBox2ra underlineLa", wrappedText[0].Text); + Assert.AreEqual("StrikeGoudy size", wrappedText[1].Text); + Assert.AreEqual("16SvgSize 24", wrappedText[2].Text); + } + } tbItem.ImportTextBody(cube.TextBody); var txtRun1Bounds = tbItem.Paragraphs[0].Runs[0].Bounds; diff --git a/src/EPPlus.Export.ImageRenderer/RenderItems/Shared/ParagraphItem.cs b/src/EPPlus.Export.ImageRenderer/RenderItems/Shared/ParagraphItem.cs index 7be94f9cc..d0bae9101 100644 --- a/src/EPPlus.Export.ImageRenderer/RenderItems/Shared/ParagraphItem.cs +++ b/src/EPPlus.Export.ImageRenderer/RenderItems/Shared/ParagraphItem.cs @@ -212,7 +212,24 @@ List GetWrappedText(ExcelDrawingTextRunCollection runs, TextFragmentColl var maxSizePoints = Math.Round(Bounds.Width, 0, MidpointRounding.AwayFromZero).PixelToPoint(); return ttMeasurer.WrapMultipleTextFragments(fragments, fonts, maxSizePoints); } + public List Lines { get; set; } + private void AddLinesAndTextRuns2(ExcelDrawingParagraph p, string textIfEmpty) + { + //var line = new ParagraphLine(); + foreach (var r in p.TextRuns) + { + foreach(var text in r.SplitIntoLines()) + { + var line = new ParagraphLine(); + //var textWidth = GetWidth(text, r); + //if(line.Width+textWidth > maxWidth) + //{ + + //} + } + } + } private void AddLinesAndTextRuns(ExcelDrawingParagraph p, string textIfEmpty) { //Log line positions and run sizes @@ -273,7 +290,14 @@ private void AddLinesAndTextRuns(ExcelDrawingParagraph p, string textIfEmpty) //Set linespacing to descent lineSpacing = _textFragments.GetDescent(currentLineIdx).PointToPixel(); } - currentLineIdx++; + + //Last line in added lines we will continue on if there are more textruns + //Therefore the index will be added to after the next line-break or at the end + if (j < lastAdded.Lines.Count - 1) + { + Bounds.Height += lastAdded.Bounds.Height; + currentLineIdx++; + } } widthOfCurrentLine = Runs.Last().PerLineWidth.Last(); @@ -281,15 +305,18 @@ private void AddLinesAndTextRuns(ExcelDrawingParagraph p, string textIfEmpty) else { widthOfCurrentLine += Runs.Last().PerLineWidth.Last(); + //If we are on the last run if (i == p.TextRuns.Count - 1) { if (_lsMultiplier.HasValue) { + //currentLineIdx++; //Add ascent to descent (Add ascent to Nothing for the first run) lineSpacing += _textFragments.GetAscent(currentLineIdx).PointToPixel() * _lsMultiplier.Value; } lastAdded.AddLineSpacing(lineSpacing); + Bounds.Height += lastAdded.Bounds.Height; //Runs[idxLargestFontSize].GetBounds(out double l, out double t, out double r, out double b); //Bounds.Height += Runs[idxLargestFontSize].Bounds.Height; } @@ -394,6 +421,9 @@ private void CalculateDisplayText(ExcelDrawingParagraph p, TextFragmentCollectio } //Gets each actual text run, with linebreak symbols _textRunDisplayText = fragments.GetFragmentsWithFinalLineBreaks(); + //var test = fragments.GetFragmentsWithoutLineBreaks(); + //var + //var lineToRunMapping = } internal double GetAlignmentHorizontal(eTextAlignment txAlignment) diff --git a/src/EPPlus.Export.ImageRenderer/RenderItems/SvgItem/SvgTextRunItem.cs b/src/EPPlus.Export.ImageRenderer/RenderItems/SvgItem/SvgTextRunItem.cs index e2e81d959..f9b78b305 100644 --- a/src/EPPlus.Export.ImageRenderer/RenderItems/SvgItem/SvgTextRunItem.cs +++ b/src/EPPlus.Export.ImageRenderer/RenderItems/SvgItem/SvgTextRunItem.cs @@ -83,7 +83,6 @@ string GetFontStyleAttributes() public override void Render(StringBuilder sb) { - CalculateLineSpacing(); string finalString = ""; var xString = $"x =\"{(Bounds.Left).ToString(CultureInfo.InvariantCulture)}\" "; diff --git a/src/EPPlus.Fonts.OpenType/TrueTypeMeasurer/DataHolders/RichTextFragmentSimple.cs b/src/EPPlus.Fonts.OpenType/TrueTypeMeasurer/DataHolders/RichTextFragmentSimple.cs new file mode 100644 index 000000000..f9d738f3b --- /dev/null +++ b/src/EPPlus.Fonts.OpenType/TrueTypeMeasurer/DataHolders/RichTextFragmentSimple.cs @@ -0,0 +1,22 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; + +namespace EPPlus.Fonts.OpenType.TrueTypeMeasurer.DataHolders +{ + public class RichTextFragmentSimple + { + internal int Fragidx { get; set; } + + internal int OverallParagraphStartCharIdx { get; set; } + //Char length + internal int charStarIdxWithinCurrentLine { get; set; } + //Width in points + internal double Width { get; set; } + //Font-size in points + internal double FontSize { get; set; } + + public RichTextFragmentSimple() { } + } +} diff --git a/src/EPPlus.Fonts.OpenType/TrueTypeMeasurer/DataHolders/TextFragmentCollection.cs b/src/EPPlus.Fonts.OpenType/TrueTypeMeasurer/DataHolders/TextFragmentCollection.cs index b2613f926..29a7c91d1 100644 --- a/src/EPPlus.Fonts.OpenType/TrueTypeMeasurer/DataHolders/TextFragmentCollection.cs +++ b/src/EPPlus.Fonts.OpenType/TrueTypeMeasurer/DataHolders/TextFragmentCollection.cs @@ -28,6 +28,9 @@ public class TextFragmentCollection internal List lineWidths { get; private set; } = new List(); public List IndiciesToWrapAt {get; internal set; } + + List TextLines = new(); + public TextFragmentCollection(List textFragments) { TextFragments = textFragments; @@ -283,6 +286,31 @@ public List GetOutputLines() //} + //public List GetSimpleTextLines(List wrappedLines) + //{ + // //The wrapped lines + // var lines = wrappedLines; + // //The richText data in a form that is one to one in chars + // var frags = GetFragmentsWithoutLineBreaks(); + + // int fragIdx = 0; + // int charIdx = 0; + // int lastFragLength = 0; + + // var currentFragment = frags[fragIdx]; + + // for (int i = 0; i< wrappedLines.Count(); i++) + // { + // var textLineSimple = new TextLineSimple(); + + // for (int j = 0; j < wrappedLines[i].Length; j++) + // { + // textLineSimple + // charIdx++; + // } + // } + //} + public List GetFragmentsWithoutLineBreaks() { ////var newFragments = TextFragments.ToArray(); diff --git a/src/EPPlus.Fonts.OpenType/TrueTypeMeasurer/DataHolders/TextLine.cs b/src/EPPlus.Fonts.OpenType/TrueTypeMeasurer/DataHolders/TextLine.cs index f1aab663b..df4dc2644 100644 --- a/src/EPPlus.Fonts.OpenType/TrueTypeMeasurer/DataHolders/TextLine.cs +++ b/src/EPPlus.Fonts.OpenType/TrueTypeMeasurer/DataHolders/TextLine.cs @@ -13,6 +13,5 @@ public class TextLine internal List rtContentStartIndexPerRt; internal int lastRtInternalIndex; internal int startRtInternalIndex; - } } diff --git a/src/EPPlus.Fonts.OpenType/TrueTypeMeasurer/DataHolders/TextLineSimple.cs b/src/EPPlus.Fonts.OpenType/TrueTypeMeasurer/DataHolders/TextLineSimple.cs new file mode 100644 index 000000000..648963ef6 --- /dev/null +++ b/src/EPPlus.Fonts.OpenType/TrueTypeMeasurer/DataHolders/TextLineSimple.cs @@ -0,0 +1,32 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; + +namespace EPPlus.Fonts.OpenType.TrueTypeMeasurer.DataHolders +{ + public class TextLineSimple + { + internal List RtFragments = new(); + + public string Text { get; internal set; } + internal double LargestFontSize { get; set; } + internal double LargestAscent { get; set; } + internal double LargestDescent { get; set; } + internal double Width { get; set; } + + public TextLineSimple() + { + + } + + public TextLineSimple(List rtFragments, string text, double largestFontSize, double largestAscent, double largestDescent) + { + RtFragments = rtFragments; + Text = text; + LargestFontSize = largestFontSize; + LargestAscent = largestAscent; + LargestDescent = largestDescent; + } + } +} diff --git a/src/EPPlus.Fonts.OpenType/TrueTypeMeasurer/FontMeasurerTrueType.cs b/src/EPPlus.Fonts.OpenType/TrueTypeMeasurer/FontMeasurerTrueType.cs index 09845b36b..7dd616732 100644 --- a/src/EPPlus.Fonts.OpenType/TrueTypeMeasurer/FontMeasurerTrueType.cs +++ b/src/EPPlus.Fonts.OpenType/TrueTypeMeasurer/FontMeasurerTrueType.cs @@ -316,5 +316,13 @@ public List WrapMultipleTextFragments(TextFragmentCollection fragments, //Wrap the fragments return TextData.WrapMultipleTextFragments(paragraph, maxWidthPoints); } + + public List WrapMultipleTextFragmentsToTextLines(TextFragmentCollection fragments, List fonts, double maxWidthPoints) + { + TextParagraph paragraph = new TextParagraph(fragments, fonts); + + //Wrap the fragments + return TextData.WrapMultipleTextFragmentsToTextLines(paragraph, maxWidthPoints); + } } } diff --git a/src/EPPlus.Fonts.OpenType/TrueTypeMeasurer/TextData.cs b/src/EPPlus.Fonts.OpenType/TrueTypeMeasurer/TextData.cs index e4a99d28b..f48c2439f 100644 --- a/src/EPPlus.Fonts.OpenType/TrueTypeMeasurer/TextData.cs +++ b/src/EPPlus.Fonts.OpenType/TrueTypeMeasurer/TextData.cs @@ -730,6 +730,15 @@ private static void LogFragmentWidth(TextFragmentCollection collection, int UPM, collection.AddFragmentWidth(fragmentIdx, lineWidthInPoints); } + private static void AddWidthToFragment(RichTextFragmentSimple simpleFragment, int UPM, double fontSize, int lineWidth, int prevFragmentWidths, int fragmentIdx) + { + var currentFragmentLineWidth = lineWidth - prevFragmentWidths; + var factorOrig = fontSize / ((double)UPM); + var lineWidthInPoints = currentFragmentLineWidth * factorOrig; + + simpleFragment.Width = lineWidthInPoints; + } + private static void LogLineData(TextParagraph paragraph, OpenTypeFont currentFont, OpenTypeFont largestFont, double CurrentLineLargestSize, int lineWidth, int fragmentIdx) { //Log line width @@ -743,6 +752,14 @@ private static void LogLineData(TextParagraph paragraph, OpenTypeFont currentFon paragraph.Fragments.AddDescentPerLine(lineDescent); } + private static void AddDataToSimpleLine(TextLineSimple line, TextParagraph paragraph, OpenTypeFont currentFont, OpenTypeFont largestFont, double CurrentLineLargestSize, int lineWidth, int fragmentIdx) + { + line.Width = (double)lineWidth / (double)currentFont.HeadTable.UnitsPerEm * paragraph.FontSizes[fragmentIdx]; + line.LargestFontSize = CurrentLineLargestSize; + line.LargestAscent = GetBaseLine(largestFont, CurrentLineLargestSize); + line.LargestDescent = MeasureDescent(largestFont, CurrentLineLargestSize); + } + internal static List WrapMultipleTextFragments(TextParagraph paragraph, double maxWidthPoints) { //Initialize variables @@ -792,6 +809,7 @@ internal static List WrapMultipleTextFragments(TextParagraph paragraph, //Happens at the end of a fragment/start of a new fragment if(fragmentIdx != prevFragmentIdx) { + LogFragmentWidth(paragraph.Fragments, currentFont.HeadTable.UnitsPerEm, fontSize, lineWidth, prevFragWidths, prevFragmentIdx); //Since there is no gurantee any wrapping has occured @@ -917,6 +935,278 @@ internal static List WrapMultipleTextFragments(TextParagraph paragraph, return wrappedStrings; } + + internal static List WrapMultipleTextFragmentsToTextLines(TextParagraph paragraph, double maxWidthPoints) + { + //Initialize variables + List outputTextLines = new List(); + List currentLineRtFragments = new List(); + + var currentRtFragment = new RichTextFragmentSimple(); + currentRtFragment.Fragidx = 0; + currentRtFragment.FontSize = paragraph.FontSizes[0]; + + TextLineSimple currentTextLine = new(); + + ushort? lastGlyphIndex = null; + bool applyKerning = false; + OpenTypeFont currentFont = null; + + int lineWidth = 0; + int wordWidth = 0; + int prevLineEndIndex = 0; + + string leftOverLine = ""; + + double maxWidth = maxWidthPoints; + double fontSize = 0; + + //In the AllText string. + //Does not take new text-strings into account + int currentLineIndex = 0; + + int prevLineBreakIndex = 0; + + //---Logger variables--- + var fragmentCollection = paragraph.Fragments; + var allText = fragmentCollection.AllText; + var newLineIndicies = fragmentCollection.AllTextNewLineIndicies; + double CurrentLineLargestFontSize = 0; + OpenTypeFont largestFontCurrentLine = paragraph.FontIndexDict[0]; + + var prevFragmentIdx = 0; + var prevFragWidthsThisLine = 0; + ////Technically currentLineOfFragmentWidth + //int fragmentWidth = 0; + ////Technically lastLINEofFragmentWidth + ////lastFragmentWidth could be a different fragment OR current fragment on a previous line + ////Whichever the previous measured piece of text was + //int lastFragmentWidth = 0; + //---Logger Variables end--- + + //Iterate through All fragments as one concatenated text string + for (int i = 0; i < allText.Length; i++) + { + //Get char info stored previously in the textParagraph class + var charInfo = fragmentCollection.CharLookup[i]; + var lineIdx = charInfo.Line; + var fragmentIdx = charInfo.Fragment; + + //Happens at the end of a fragment/start of a new fragment + if (fragmentIdx != prevFragmentIdx) + { + AddWidthToFragment(currentRtFragment, currentFont.HeadTable.UnitsPerEm, fontSize, lineWidth, prevFragWidthsThisLine, prevFragmentIdx); + + var charLenCurrentLine = i - prevLineEndIndex; + currentRtFragment.charStarIdxWithinCurrentLine = charLenCurrentLine; + + currentLineRtFragments.Add(currentRtFragment); + + currentRtFragment = new RichTextFragmentSimple(); + currentRtFragment.Fragidx = fragmentIdx; + currentRtFragment.OverallParagraphStartCharIdx = i; + currentRtFragment.FontSize = paragraph.FontSizes[fragmentIdx]; + //Since there is no gurantee any wrapping has occured + //But we are moving on to the next fragment we need to know how much of the + //linewidth is not part of the current fragment + //Since there could be multiple fragments before line break add to current prevfragwidth + prevFragWidthsThisLine = lineWidth; + } + + //If we hit a pre-existing line break. Reset line and wordwidths + var indexExists = newLineIndicies.Count() > currentLineIndex; + if (indexExists) + { + if (i >= newLineIndicies[currentLineIndex]) + { + AddDataToSimpleLine(currentTextLine, paragraph, currentFont, largestFontCurrentLine, CurrentLineLargestFontSize, lineWidth, fragmentIdx); + //Log current fragment width + AddWidthToFragment(currentRtFragment, currentFont.HeadTable.UnitsPerEm, fontSize, lineWidth, prevFragWidthsThisLine, fragmentIdx); + //We are on the same fragment but there's been a line break + //Therefore the previous fragment width is 0 as it is within this fragment + prevFragWidthsThisLine = 0; + + //Since we are on a new line the current largest font-size for this line is the current size + CurrentLineLargestFontSize = paragraph.FontSizes[fragmentIdx]; + largestFontCurrentLine = currentFont; + + prevLineBreakIndex = i; + var addedLine = leftOverLine.Trim(['\r', '\n']); + for (int j = 0; j < currentLineRtFragments.Count(); j++) + { + //Always true here? + if (currentLineRtFragments[j].OverallParagraphStartCharIdx < prevLineBreakIndex) + { + currentLineRtFragments[j].charStarIdxWithinCurrentLine = prevLineBreakIndex - currentLineRtFragments[j].OverallParagraphStartCharIdx; + currentTextLine.RtFragments.Add(currentLineRtFragments[j]); + } + } + ////In this case it goes all the way to the end + //currentRtFragment.charStarIdxWithinCurrentLine = addedLine.Length-1; + //currentTextLine.RtFragments.Add(currentRtFragment); + currentTextLine.Text = addedLine; + outputTextLines.Add(currentTextLine); + + + //var overallStartIdx = currentRtFragment.OverallParagraphStartCharIdx; + currentTextLine = new TextLineSimple(); + //currentRtFragment = new RichTextFragmentSimple(); + //currentRtFragment.Fragidx = fragmentIdx; + //currentRtFragment.FontSize = paragraph.FontSizes[fragmentIdx]; + //currentRtFragment.charStarIdxWithinCurrentLine = 0; + //currentRtFragment.OverallParagraphStartCharIdx = overallStartIdx; + //currentLineRtFragments.Clear(); + //currentLineRtFragments.Add(currentRtFragment); + + lineWidth = 0; + wordWidth = 0; + leftOverLine = ""; + //prevLineEndIndex = i; + currentLineIndex++; + } + } + + //If this char has a different font, do the neccesary conversions + if (currentFont != null) + { + if (currentFont != paragraph.FontIndexDict[fragmentIdx] | fontSize != paragraph.FontSizes[fragmentIdx]) + { + ConvertDesignUnits(currentFont, fontSize, paragraph.FontIndexDict[fragmentIdx], paragraph.FontSizes[fragmentIdx], + ref maxWidth, ref lineWidth, ref wordWidth); + //Font/fragment change + currentFont = paragraph.FontIndexDict[fragmentIdx]; + if (paragraph.FontSizes[fragmentIdx] > CurrentLineLargestFontSize) + { + largestFontCurrentLine = currentFont; + CurrentLineLargestFontSize = paragraph.FontSizes[fragmentIdx]; + } + } + } + else + { + currentFont = paragraph.FontIndexDict[fragmentIdx]; + maxWidth = (maxWidthPoints * (double)currentFont.HeadTable.UnitsPerEm) / paragraph.FontSizes[fragmentIdx]; + if (paragraph.FontSizes[fragmentIdx] > CurrentLineLargestFontSize) + { + largestFontCurrentLine = currentFont; + CurrentLineLargestFontSize = paragraph.FontSizes[fragmentIdx]; + } + } + + fontSize = paragraph.FontSizes[fragmentIdx]; + var glyphMapping = paragraph.GlyphMappings[fragmentIdx]; + + char c = allText[i]; + var advanceWidth = CalculateAdvanceWidth(c, glyphMapping, currentFont, ref lastGlyphIndex, ref lineWidth, ref wordWidth, ref applyKerning); + + //Perform the actual wrapping + if (lineWidth > maxWidth) + { + List tempSingleReturnHolder = new(); + var prevLineWidth = lineWidth; + WrapAtCharPos(allText, i, ref prevLineEndIndex, ref lineWidth, ref wordWidth, advanceWidth, tempSingleReturnHolder); + + //Log where wrapping occured in order to keep track of fragment/run/richtext + + //Can't be part of Log function as there could be pre-existing line-breaks + paragraph.Fragments.AddWrappingIndex(prevLineEndIndex); + + AddDataToSimpleLine(currentTextLine, paragraph, currentFont, largestFontCurrentLine, CurrentLineLargestFontSize, prevLineWidth, fragmentIdx); + //Log current fragment width + AddWidthToFragment(currentRtFragment, currentFont.HeadTable.UnitsPerEm, fontSize, prevLineWidth, prevFragWidthsThisLine, fragmentIdx); + + + int newRtStartIdx = 0; + currentLineRtFragments.Add(currentRtFragment); + + for (int j = 0; j< currentLineRtFragments.Count(); j++) + { + if(currentLineRtFragments[j].OverallParagraphStartCharIdx < prevLineEndIndex) + { + currentLineRtFragments[j].charStarIdxWithinCurrentLine = currentLineRtFragments[j].OverallParagraphStartCharIdx - prevLineBreakIndex; + currentTextLine.RtFragments.Add(currentLineRtFragments[j]); + newRtStartIdx = j; + } + } + + currentLineRtFragments.RemoveRange(0, newRtStartIdx); + for(int j = 0; j < currentLineRtFragments.Count(); j++) + { + currentLineRtFragments[j].charStarIdxWithinCurrentLine = 0; + } + + //currentRtFragment.charStarIdxWithinCurrentLine = tempSingleReturnHolder[0].Length - 1; + //currentTextLine.RtFragments.Add(currentRtFragment); + + currentTextLine.Text = tempSingleReturnHolder[0]; + outputTextLines.Add(currentTextLine); + + prevFragWidthsThisLine = 0; + currentTextLine = new TextLineSimple(); + + //var overallStartIdx = currentRtFragment.OverallParagraphStartCharIdx; + //currentTextLine = new TextLineSimple(); + //currentRtFragment = new RichTextFragmentSimple(); + //currentRtFragment.Fragidx = fragmentIdx; + //currentRtFragment.FontSize = paragraph.FontSizes[fragmentIdx]; + //currentRtFragment.charStarIdxWithinCurrentLine = 0; + //currentRtFragment.OverallParagraphStartCharIdx = overallStartIdx; + //currentLineRtFragments.Clear(); + //currentLineRtFragments.Add(currentRtFragment); + + prevLineBreakIndex = prevLineEndIndex; + + //Since we're using the AllText, need to handle leftover line differently + if (i < prevLineEndIndex) + { + if (lineWidth != 0) + { + leftOverLine = allText.Substring(prevLineEndIndex, prevLineEndIndex - i); + //Since we've moved one beyond the last + i = prevLineEndIndex; + } + else + { + leftOverLine = ""; + } + } + else + { + //Special case for only 1 or 0 chars in leftover line + leftOverLine = allText.Substring(prevLineEndIndex, i - prevLineEndIndex); + leftOverLine += allText.Substring(i, 1); + } + //Since we are on a new line the current largest font-size for this line is the current size + CurrentLineLargestFontSize = paragraph.FontSizes[fragmentIdx]; + largestFontCurrentLine = currentFont; + } + else + { + //Add the current char to current unwrapped line + leftOverLine += allText.Substring(i, 1); + } + + prevFragmentIdx = fragmentIdx; + } + + AddDataToSimpleLine(currentTextLine, paragraph, currentFont, largestFontCurrentLine, CurrentLineLargestFontSize, lineWidth, paragraph.Fragments.TextFragments.Count() - 1); + //Log current fragment width + AddWidthToFragment(currentRtFragment, currentFont.HeadTable.UnitsPerEm, fontSize, lineWidth, prevFragWidthsThisLine, paragraph.Fragments.TextFragments.Count() - 1); + + for (int j = 0; j < currentLineRtFragments.Count(); j++) + { + if (currentLineRtFragments[j].OverallParagraphStartCharIdx < prevLineBreakIndex) + { + currentLineRtFragments[j].charStarIdxWithinCurrentLine = prevLineBreakIndex - currentLineRtFragments[j].OverallParagraphStartCharIdx; + currentTextLine.RtFragments.Add(currentLineRtFragments[j]); + } + } + currentTextLine.RtFragments.Add(currentRtFragment); + currentTextLine.Text = leftOverLine; + outputTextLines.Add(currentTextLine); + + return outputTextLines; + } } } From c246dbc08bf2419e0f662c550f64fe062a9219f5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jan=20K=C3=A4llman?= Date: Fri, 30 Jan 2026 14:47:32 +0100 Subject: [PATCH 030/151] Added more checks on the ExcelCustomXml.Save method - #2254 (#2264) --- src/EPPlus/Data/CustomXml/ExcelCustomXml.cs | 23 +++++++++++++------ .../PowerQuery/ExcelPowerQuerySettings.cs | 2 +- src/EPPlusTest/Issues/PackageIssues.cs | 8 +++++++ 3 files changed, 25 insertions(+), 8 deletions(-) diff --git a/src/EPPlus/Data/CustomXml/ExcelCustomXml.cs b/src/EPPlus/Data/CustomXml/ExcelCustomXml.cs index 8dd721fd0..52cabada6 100644 --- a/src/EPPlus/Data/CustomXml/ExcelCustomXml.cs +++ b/src/EPPlus/Data/CustomXml/ExcelCustomXml.cs @@ -44,6 +44,8 @@ internal XmlDocument PropertiesXml } internal ExcelCustomXml() { + SchemasReferences = [Schemas.schemaDataMashup]; + CustomXml = new XmlDocument(); } internal ExcelCustomXml(ZipPackagePart part) { @@ -107,6 +109,7 @@ internal void Save(ExcelPackage pck) var nsm = CreateNsm(); _xmlHelper = XmlHelperFactory.Create(nsm, PropertiesXml.DocumentElement.SelectSingleNode("ds:schemaRefs", nsm)); } + if (_xmlHelper != null) { _xmlHelper.TopNode.InnerXml = ""; @@ -117,15 +120,21 @@ internal void Save(ExcelPackage pck) _xmlHelper.TopNode.AppendChild(schemaRefNode); } } - var xmlSettings = new XmlWriterSettings(); - var stream = Part.GetStream(FileMode.Create, FileAccess.Write); - var xmlWriter = XmlWriter.Create(stream, xmlSettings); - CustomXml.Save(xmlWriter); + var xmlSettings = new XmlWriterSettings(); - stream = PropertiesPart.GetStream(FileMode.Create, FileAccess.Write); - xmlWriter = XmlWriter.Create(stream, xmlSettings); - PropertiesXml.Save(xmlWriter); + if (Part != null) + { + var stream = Part.GetStream(FileMode.Create, FileAccess.Write); + var xmlWriter = XmlWriter.Create(stream, xmlSettings); + CustomXml.Save(xmlWriter); + } + if (PropertiesPart != null) + { + var stream = PropertiesPart.GetStream(FileMode.Create, FileAccess.Write); + var xmlWriter = XmlWriter.Create(stream, xmlSettings); + PropertiesXml.Save(xmlWriter); + } } } } \ No newline at end of file diff --git a/src/EPPlus/Data/PowerQuery/ExcelPowerQuerySettings.cs b/src/EPPlus/Data/PowerQuery/ExcelPowerQuerySettings.cs index 738c883ef..2f6b7054d 100644 --- a/src/EPPlus/Data/PowerQuery/ExcelPowerQuerySettings.cs +++ b/src/EPPlus/Data/PowerQuery/ExcelPowerQuerySettings.cs @@ -266,7 +266,7 @@ internal void Save(ExcelCustomXmlCollection customXml) if (cx == null) { - cx = new ExcelCustomXml() { SchemasReferences = { Schemas.schemaDataMashup}, CustomXml = new XmlDocument() }; + cx = new ExcelCustomXml(); } cx.CustomXml.LoadXml($"{Convert.ToBase64String(retMs.ToArray())}"); diff --git a/src/EPPlusTest/Issues/PackageIssues.cs b/src/EPPlusTest/Issues/PackageIssues.cs index 70c507b13..4aa0d722d 100644 --- a/src/EPPlusTest/Issues/PackageIssues.cs +++ b/src/EPPlusTest/Issues/PackageIssues.cs @@ -153,5 +153,13 @@ public void i2235() sheet.Calculate(o => o.EnableUnicodeAwareStringOperations = true); SaveAndCleanup(package); } + [TestMethod] + public void XmlIssue() + { + using var package = OpenTemplatePackage("test external connection.xlsx"); + package.Workbook.CalculateAllPivotTables(); + + SaveAndCleanup(package); + } } } \ No newline at end of file From 83e52ca02d0a286edaae83741c104c3079ca40d6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ossian=20Edstr=C3=B6m?= Date: Fri, 30 Jan 2026 15:24:08 +0100 Subject: [PATCH 031/151] Fixed the major issues. width still wrong after 1st line --- .../DataHolders/RichTextFragmentSimple.cs | 12 +- .../TrueTypeMeasurer/TextData.cs | 173 +++++++++++++----- 2 files changed, 137 insertions(+), 48 deletions(-) diff --git a/src/EPPlus.Fonts.OpenType/TrueTypeMeasurer/DataHolders/RichTextFragmentSimple.cs b/src/EPPlus.Fonts.OpenType/TrueTypeMeasurer/DataHolders/RichTextFragmentSimple.cs index f9d738f3b..3af5cbacc 100644 --- a/src/EPPlus.Fonts.OpenType/TrueTypeMeasurer/DataHolders/RichTextFragmentSimple.cs +++ b/src/EPPlus.Fonts.OpenType/TrueTypeMeasurer/DataHolders/RichTextFragmentSimple.cs @@ -14,9 +14,17 @@ public class RichTextFragmentSimple internal int charStarIdxWithinCurrentLine { get; set; } //Width in points internal double Width { get; set; } - //Font-size in points - internal double FontSize { get; set; } public RichTextFragmentSimple() { } + + internal RichTextFragmentSimple Clone() + { + var fragment = new RichTextFragmentSimple(); + fragment.Width = Width; + fragment.OverallParagraphStartCharIdx = OverallParagraphStartCharIdx; + fragment.charStarIdxWithinCurrentLine = charStarIdxWithinCurrentLine; + fragment.Fragidx = Fragidx; + return fragment; + } } } diff --git a/src/EPPlus.Fonts.OpenType/TrueTypeMeasurer/TextData.cs b/src/EPPlus.Fonts.OpenType/TrueTypeMeasurer/TextData.cs index f48c2439f..1ef9f6b25 100644 --- a/src/EPPlus.Fonts.OpenType/TrueTypeMeasurer/TextData.cs +++ b/src/EPPlus.Fonts.OpenType/TrueTypeMeasurer/TextData.cs @@ -944,7 +944,6 @@ internal static List WrapMultipleTextFragmentsToTextLines(TextPa var currentRtFragment = new RichTextFragmentSimple(); currentRtFragment.Fragidx = 0; - currentRtFragment.FontSize = paragraph.FontSizes[0]; TextLineSimple currentTextLine = new(); @@ -975,7 +974,7 @@ internal static List WrapMultipleTextFragmentsToTextLines(TextPa OpenTypeFont largestFontCurrentLine = paragraph.FontIndexDict[0]; var prevFragmentIdx = 0; - var prevFragWidthsThisLine = 0; + var prevFragWidths = 0; ////Technically currentLineOfFragmentWidth //int fragmentWidth = 0; ////Technically lastLINEofFragmentWidth @@ -995,22 +994,20 @@ internal static List WrapMultipleTextFragmentsToTextLines(TextPa //Happens at the end of a fragment/start of a new fragment if (fragmentIdx != prevFragmentIdx) { - AddWidthToFragment(currentRtFragment, currentFont.HeadTable.UnitsPerEm, fontSize, lineWidth, prevFragWidthsThisLine, prevFragmentIdx); - - var charLenCurrentLine = i - prevLineEndIndex; - currentRtFragment.charStarIdxWithinCurrentLine = charLenCurrentLine; - + double leftoverWidth = 0; + if(currentRtFragment.Width != 0) + { + leftoverWidth = currentRtFragment.Width; + } + AddWidthToFragment(currentRtFragment, currentFont.HeadTable.UnitsPerEm, fontSize, lineWidth, prevFragWidths, prevFragmentIdx); + currentRtFragment.Width += leftoverWidth; currentLineRtFragments.Add(currentRtFragment); currentRtFragment = new RichTextFragmentSimple(); currentRtFragment.Fragidx = fragmentIdx; currentRtFragment.OverallParagraphStartCharIdx = i; - currentRtFragment.FontSize = paragraph.FontSizes[fragmentIdx]; - //Since there is no gurantee any wrapping has occured - //But we are moving on to the next fragment we need to know how much of the - //linewidth is not part of the current fragment - //Since there could be multiple fragments before line break add to current prevfragwidth - prevFragWidthsThisLine = lineWidth; + + prevFragWidths = lineWidth; } //If we hit a pre-existing line break. Reset line and wordwidths @@ -1021,10 +1018,12 @@ internal static List WrapMultipleTextFragmentsToTextLines(TextPa { AddDataToSimpleLine(currentTextLine, paragraph, currentFont, largestFontCurrentLine, CurrentLineLargestFontSize, lineWidth, fragmentIdx); //Log current fragment width - AddWidthToFragment(currentRtFragment, currentFont.HeadTable.UnitsPerEm, fontSize, lineWidth, prevFragWidthsThisLine, fragmentIdx); + AddWidthToFragment(currentRtFragment, currentFont.HeadTable.UnitsPerEm, fontSize, lineWidth, prevFragWidths, fragmentIdx); + currentLineRtFragments.Add(currentRtFragment); + //We are on the same fragment but there's been a line break //Therefore the previous fragment width is 0 as it is within this fragment - prevFragWidthsThisLine = 0; + prevFragWidths = 0; //Since we are on a new line the current largest font-size for this line is the current size CurrentLineLargestFontSize = paragraph.FontSizes[fragmentIdx]; @@ -1104,55 +1103,128 @@ internal static List WrapMultipleTextFragmentsToTextLines(TextPa { List tempSingleReturnHolder = new(); var prevLineWidth = lineWidth; - WrapAtCharPos(allText, i, ref prevLineEndIndex, ref lineWidth, ref wordWidth, advanceWidth, tempSingleReturnHolder); + //Add currentfragment to the list + + double leftoverWidth = 0; + if(currentRtFragment.Width != 0) + { + leftoverWidth = currentRtFragment.Width; + //leftover from previously + } + AddWidthToFragment(currentRtFragment, currentFont.HeadTable.UnitsPerEm, fontSize, prevLineWidth, prevFragWidths, fragmentIdx); + currentRtFragment.Width += leftoverWidth; + currentLineRtFragments.Add(currentRtFragment); + + WrapAtCharPos(allText, i, ref prevLineEndIndex, ref lineWidth, ref wordWidth, advanceWidth, tempSingleReturnHolder); + prevLineWidth -= lineWidth; //Log where wrapping occured in order to keep track of fragment/run/richtext //Can't be part of Log function as there could be pre-existing line-breaks paragraph.Fragments.AddWrappingIndex(prevLineEndIndex); - AddDataToSimpleLine(currentTextLine, paragraph, currentFont, largestFontCurrentLine, CurrentLineLargestFontSize, prevLineWidth, fragmentIdx); - //Log current fragment width - AddWidthToFragment(currentRtFragment, currentFont.HeadTable.UnitsPerEm, fontSize, prevLineWidth, prevFragWidthsThisLine, fragmentIdx); + var charInfoAtBreak = fragmentCollection.CharLookup[prevLineEndIndex]; + var fragIdxAtBreak = charInfoAtBreak.Fragment; + //Largest font might be wrong here as we may linebreak at a different font than current font if we break at the start of a word + //and not current i + largestFontCurrentLine = null; + CurrentLineLargestFontSize = 0; - int newRtStartIdx = 0; - currentLineRtFragments.Add(currentRtFragment); + for (int j = currentLineRtFragments[0].Fragidx; j < fragIdxAtBreak + 1; j++) + { + if (paragraph.FontSizes[j] > CurrentLineLargestFontSize) + { + largestFontCurrentLine = paragraph.FontIndexDict[j]; + CurrentLineLargestFontSize = paragraph.FontSizes[j]; + } + } - for (int j = 0; j< currentLineRtFragments.Count(); j++) + AddDataToSimpleLine(currentTextLine, paragraph, currentFont, largestFontCurrentLine, CurrentLineLargestFontSize, prevLineWidth, fragmentIdx); + + //Largest font is not neccesarily current font as word wrap might have wrapped another font between + largestFontCurrentLine = null; + CurrentLineLargestFontSize = 0; + + for (int j = fragIdxAtBreak; j < fragmentIdx + 1; j++) { - if(currentLineRtFragments[j].OverallParagraphStartCharIdx < prevLineEndIndex) + if (paragraph.FontSizes[j] > CurrentLineLargestFontSize) { - currentLineRtFragments[j].charStarIdxWithinCurrentLine = currentLineRtFragments[j].OverallParagraphStartCharIdx - prevLineBreakIndex; - currentTextLine.RtFragments.Add(currentLineRtFragments[j]); - newRtStartIdx = j; + largestFontCurrentLine = paragraph.FontIndexDict[j]; + CurrentLineLargestFontSize = paragraph.FontSizes[j]; } } - currentLineRtFragments.RemoveRange(0, newRtStartIdx); - for(int j = 0; j < currentLineRtFragments.Count(); j++) + //Because of word wrap we now need to verify if the fragments we have actually exist in the + //line we are currently wrapping and what their widths are + double prevFragWidthsAtBreakInPoints = 0; + int newStartIdxRt = 0; + double originalWidthOfFragment = 0; + + for (int j = 0; j < currentLineRtFragments.Count(); j++) { - currentLineRtFragments[j].charStarIdxWithinCurrentLine = 0; + //Recalculate startIdx. + //Word wrap might have made it inaccurate + currentLineRtFragments[j].charStarIdxWithinCurrentLine = Math.Max(0, currentLineRtFragments[j].OverallParagraphStartCharIdx - prevLineBreakIndex); + + if (currentLineRtFragments[j].Fragidx < fragIdxAtBreak) + { + //Store variables for final fragment + prevFragWidthsAtBreakInPoints += currentLineRtFragments[j].Width; + + //Add the rtFragments that actually exist within the wrapped line + currentTextLine.RtFragments.Add(currentLineRtFragments[j].Clone()); + } + else if(currentLineRtFragments[j].Fragidx == fragIdxAtBreak) + { + //Handle 'splitting' the rtFragment + newStartIdxRt = j; + originalWidthOfFragment = currentLineRtFragments[j].Width; + + var widthOfFragmentAtBreak = currentTextLine.Width - prevFragWidthsAtBreakInPoints -leftoverWidth; + + currentLineRtFragments[j].Width = widthOfFragmentAtBreak; + currentTextLine.RtFragments.Add(currentLineRtFragments[j].Clone()); + + //Calculate the new width of the fragment in the resulting line + currentLineRtFragments[j].Width = originalWidthOfFragment - widthOfFragmentAtBreak; + currentLineRtFragments[j].charStarIdxWithinCurrentLine = 0; + + //Ensure charStartIdxWithinCurrentLine is accurate for the next line + prevLineBreakIndex = prevLineEndIndex; + } + else + { + //We are past the atBreak part. + //All rtFragments that make it here is part of the next/new currentLine + if(currentLineRtFragments[j].OverallParagraphStartCharIdx < prevLineEndIndex) + { + var doSome = "Thing?"; + } + + break; + } } - //currentRtFragment.charStarIdxWithinCurrentLine = tempSingleReturnHolder[0].Length - 1; - //currentTextLine.RtFragments.Add(currentRtFragment); + //Remove those that are now irrelevant + for(int j = 0; j < newStartIdxRt; j++) + { + currentLineRtFragments.RemoveAt(0); + } + //Remove the current fragment as we have not found its end yet + currentLineRtFragments.Remove(currentRtFragment); + //Equal to new line width for when the current fragment ends + prevFragWidths = lineWidth; + currentTextLine.Text = tempSingleReturnHolder[0]; outputTextLines.Add(currentTextLine); - prevFragWidthsThisLine = 0; currentTextLine = new TextLineSimple(); - //var overallStartIdx = currentRtFragment.OverallParagraphStartCharIdx; - //currentTextLine = new TextLineSimple(); - //currentRtFragment = new RichTextFragmentSimple(); - //currentRtFragment.Fragidx = fragmentIdx; - //currentRtFragment.FontSize = paragraph.FontSizes[fragmentIdx]; + + currentRtFragment = currentRtFragment.Clone(); //currentRtFragment.charStarIdxWithinCurrentLine = 0; - //currentRtFragment.OverallParagraphStartCharIdx = overallStartIdx; - //currentLineRtFragments.Clear(); - //currentLineRtFragments.Add(currentRtFragment); prevLineBreakIndex = prevLineEndIndex; @@ -1172,13 +1244,14 @@ internal static List WrapMultipleTextFragmentsToTextLines(TextPa } else { + //Since we are on a new line the current largest font-size for this line is the current size + CurrentLineLargestFontSize = paragraph.FontSizes[fragmentIdx]; + largestFontCurrentLine = currentFont; + //Special case for only 1 or 0 chars in leftover line leftOverLine = allText.Substring(prevLineEndIndex, i - prevLineEndIndex); leftOverLine += allText.Substring(i, 1); } - //Since we are on a new line the current largest font-size for this line is the current size - CurrentLineLargestFontSize = paragraph.FontSizes[fragmentIdx]; - largestFontCurrentLine = currentFont; } else { @@ -1190,14 +1263,22 @@ internal static List WrapMultipleTextFragmentsToTextLines(TextPa } AddDataToSimpleLine(currentTextLine, paragraph, currentFont, largestFontCurrentLine, CurrentLineLargestFontSize, lineWidth, paragraph.Fragments.TextFragments.Count() - 1); - //Log current fragment width - AddWidthToFragment(currentRtFragment, currentFont.HeadTable.UnitsPerEm, fontSize, lineWidth, prevFragWidthsThisLine, paragraph.Fragments.TextFragments.Count() - 1); + + double lastLeftoverWidth = 0; + if (currentRtFragment.Width != 0) + { + lastLeftoverWidth = currentRtFragment.Width; + } + AddWidthToFragment(currentRtFragment, currentFont.HeadTable.UnitsPerEm, fontSize, lineWidth, prevFragWidths, currentRtFragment.Fragidx); + currentRtFragment.Width += lastLeftoverWidth; + currentRtFragment.charStarIdxWithinCurrentLine = Math.Max(0, currentRtFragment.OverallParagraphStartCharIdx - prevLineBreakIndex); + currentLineRtFragments.Add(currentRtFragment); for (int j = 0; j < currentLineRtFragments.Count(); j++) { if (currentLineRtFragments[j].OverallParagraphStartCharIdx < prevLineBreakIndex) { - currentLineRtFragments[j].charStarIdxWithinCurrentLine = prevLineBreakIndex - currentLineRtFragments[j].OverallParagraphStartCharIdx; + currentLineRtFragments[j].charStarIdxWithinCurrentLine = Math.Max(0, currentLineRtFragments[j].OverallParagraphStartCharIdx - prevLineBreakIndex); currentTextLine.RtFragments.Add(currentLineRtFragments[j]); } } From a6aadb60e93b2b206a9a89131e0c32737fdf9dca Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jan=20K=C3=A4llman?= Date: Mon, 2 Feb 2026 08:01:35 +0100 Subject: [PATCH 032/151] WIP:Work on auto value axis --- .../RenderItems/Shared/ParagraphItem.cs | 2 +- .../Svg/Chart/ChartTypeDrawer.cs | 160 ---------------- .../Chart/ChartTypeDrawers/ChartTypeDrawer.cs | 88 +++++++++ .../ChartTypeDrawers/LineChartTypeDrawer.cs | 82 ++++++++ .../Svg/{ => Chart}/SvgChart.cs | 0 .../Svg/{ => Chart}/SvgChartAxis.cs | 18 +- .../Svg/{ => Chart}/SvgChartLegend.cs | 0 .../Svg/{ => Chart}/SvgChartObject.cs | 0 .../Svg/{ => Chart}/SvgChartPlotarea.cs | 0 .../Svg/{ => Chart}/SvgChartTitle.cs | 0 .../Chart/Util/ValueAxisScaleCalculator.cs | 175 ++++++++++++++++++ .../Drawing/Chart/ExcelChartAxisStandard.cs | 7 +- 12 files changed, 367 insertions(+), 165 deletions(-) delete mode 100644 src/EPPlus.Export.ImageRenderer/Svg/Chart/ChartTypeDrawer.cs create mode 100644 src/EPPlus.Export.ImageRenderer/Svg/Chart/ChartTypeDrawers/ChartTypeDrawer.cs create mode 100644 src/EPPlus.Export.ImageRenderer/Svg/Chart/ChartTypeDrawers/LineChartTypeDrawer.cs rename src/EPPlus.Export.ImageRenderer/Svg/{ => Chart}/SvgChart.cs (100%) rename src/EPPlus.Export.ImageRenderer/Svg/{ => Chart}/SvgChartAxis.cs (97%) rename src/EPPlus.Export.ImageRenderer/Svg/{ => Chart}/SvgChartLegend.cs (100%) rename src/EPPlus.Export.ImageRenderer/Svg/{ => Chart}/SvgChartObject.cs (100%) rename src/EPPlus.Export.ImageRenderer/Svg/{ => Chart}/SvgChartPlotarea.cs (100%) rename src/EPPlus.Export.ImageRenderer/Svg/{ => Chart}/SvgChartTitle.cs (100%) create mode 100644 src/EPPlus.Export.ImageRenderer/Svg/Chart/Util/ValueAxisScaleCalculator.cs diff --git a/src/EPPlus.Export.ImageRenderer/RenderItems/Shared/ParagraphItem.cs b/src/EPPlus.Export.ImageRenderer/RenderItems/Shared/ParagraphItem.cs index 45fb318a1..20e51986b 100644 --- a/src/EPPlus.Export.ImageRenderer/RenderItems/Shared/ParagraphItem.cs +++ b/src/EPPlus.Export.ImageRenderer/RenderItems/Shared/ParagraphItem.cs @@ -250,7 +250,7 @@ private void AddLinesAndTextRuns(ExcelDrawingParagraph p, string textIfEmpty) } else { - widthOfCurrentLine += Runs.Last().PerLineWidth.Last(); + widthOfCurrentLine += Runs.Last().PerLineWidth.LastOrDefault(); if (i == p.TextRuns.Count - 1) { Runs[idxLargestFontSize].GetBounds(out double l, out double t, out double r, out double b); diff --git a/src/EPPlus.Export.ImageRenderer/Svg/Chart/ChartTypeDrawer.cs b/src/EPPlus.Export.ImageRenderer/Svg/Chart/ChartTypeDrawer.cs deleted file mode 100644 index 97d541e5f..000000000 --- a/src/EPPlus.Export.ImageRenderer/Svg/Chart/ChartTypeDrawer.cs +++ /dev/null @@ -1,160 +0,0 @@ -using EPPlus.Export.ImageRenderer.Utils; -using EPPlusImageRenderer.RenderItems; -using EPPlusImageRenderer.Svg; -using OfficeOpenXml; -using OfficeOpenXml.Drawing.Chart; -using OfficeOpenXml.Drawing.Chart.ChartEx; -using OfficeOpenXml.FormulaParsing.Excel.Functions.Finance; -using OfficeOpenXml.FormulaParsing.Excel.Functions.RefAndLookup; -using System; -using System.Collections.Generic; -using System.Linq; - -namespace EPPlus.Export.ImageRenderer.Svg.Chart -{ - internal abstract class ChartTypeDrawer : SvgChartObject - { - protected SvgChart _svgChart; - protected ExcelChart _chartType; - private ChartTypeDrawer(SvgChart svgChart, ExcelChart chartType) : base(svgChart) - { - _svgChart = svgChart; - _chartType = chartType; - } - protected List LoadSeriesValues(string serieAddress, double[] numLiterals, string[] strLiterals) - { - 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); - var wsName = address.WorkSheetName; - if (string.IsNullOrEmpty(wsName)) - { - wsName = Chart.WorkSheet.Name; - } - if (Chart.WorkSheet.Workbook.Worksheets[wsName] != 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(Chart.WorkSheet.Workbook.Worksheets[wsName].Cells[r, c].Value); - } - } - } - } - return values; - } - public List RenderItems { get; } = new List(); - internal static List Create(SvgChart svgChart) - { - var drawers = new List(); - foreach (var ct in svgChart.Chart.PlotArea.ChartTypes) - { - switch (ct.ChartType) - { - case eChartType.Line: - case eChartType.LineMarkers: - case eChartType.LineStacked: - case eChartType.LineStacked100: - case eChartType.LineMarkersStacked: - case eChartType.LineMarkersStacked100: - drawers.Add(new LineChartTypeDrawer(svgChart, ct)); - break; - default: - throw new NotImplementedException($"No Svg support for Chart type {ct} is implemented."); - } - } - return drawers; - } - internal class LineChartTypeDrawer : ChartTypeDrawer - { - internal LineChartTypeDrawer(SvgChart svgChart, ExcelChart chartType) : base(svgChart, chartType) - { - var groupItem = new SvgGroupItem(ChartRenderer, _svgChart.Plotarea.Rectangle.Bounds); - RenderItems.Add(groupItem); - foreach (ExcelLineChartSerie serie in chartType.Series) - { - var yValues = LoadSeriesValues(serie.Series, serie.NumberLiteralsY, serie.StringLiteralsY); - var xValues = LoadSeriesValues(serie.XSeries, serie.NumberLiteralsX, serie.StringLiteralsX); - AddLine(chartType, serie, xValues, yValues); - } - RenderItems.Add(new SvgEndGroupItem(ChartRenderer, null)); - } - - private void AddLine(ExcelChart chartType, ExcelLineChartSerie serie, List xValues, List yValues) - { - var xAxis= _svgChart.HorizontalAxis; - SvgChartAxis yAxis; - if (chartType.UseSecondaryAxis) - { - yAxis = _svgChart.SecondVerticalAxis; - } - else - { - yAxis = _svgChart.VerticalAxis; - } - var linePath = new SvgRenderPathItem(ChartRenderer, _svgChart.Plotarea.Rectangle.Bounds); - var coords = new List(); - var markerItems = new List(); - - for (var i = 0; i < yValues.Count; i++) - { - object x; - if (xValues == null) - { - x = i + 1; - } - else - { - x = xValues[i]; - } - - var y = yValues[i]; - var xPos = xAxis.GetPositionInPlotarea(i); - var yPos = yAxis.GetPositionInPlotarea(y); - - if(double.IsNaN(yPos)==false) - { - coords.Add(xPos / _svgChart.Plotarea.Rectangle.Bounds.Width); - coords.Add(yPos / _svgChart.Plotarea.Rectangle.Bounds.Height); - } - if (serie.HasMarker() && serie.Marker.Style != eMarkerStyle.None) - { - float mx = (float)xPos; - float my = (float)yPos; - var ls = LineMarkerHelper.GetMarkerItem(_svgChart, serie, mx, my, false); - if ((serie.Marker.Style == eMarkerStyle.Plus || serie.Marker.Style == eMarkerStyle.X || serie.Marker.Style == eMarkerStyle.Star) && - serie.Marker.Fill.IsEmpty == false) - { - markerItems.Add(LineMarkerHelper.GetMarkerBackground(_svgChart, serie, mx, my, false)); - } - markerItems.Add(ls); - } - } - - linePath.Commands.Add(new EPPlusImageRenderer.PathCommands(PathCommandType.Move, linePath, coords.ToArray())); - linePath.SetDrawingPropertiesBorder(serie.Border, chartType.StyleManager.Style.SeriesLine.BorderReference.Color, true); - linePath.FillColor = "none"; // No fill for line - RenderItems.Add(linePath); - RenderItems.AddRange(markerItems); - } - } - internal override void AppendRenderItems(List renderItems) - { - renderItems.AddRange(RenderItems); - } - } -} diff --git a/src/EPPlus.Export.ImageRenderer/Svg/Chart/ChartTypeDrawers/ChartTypeDrawer.cs b/src/EPPlus.Export.ImageRenderer/Svg/Chart/ChartTypeDrawers/ChartTypeDrawer.cs new file mode 100644 index 000000000..322f3a505 --- /dev/null +++ b/src/EPPlus.Export.ImageRenderer/Svg/Chart/ChartTypeDrawers/ChartTypeDrawer.cs @@ -0,0 +1,88 @@ +using EPPlus.Export.ImageRenderer.Utils; +using EPPlusImageRenderer.RenderItems; +using EPPlusImageRenderer.Svg; +using OfficeOpenXml; +using OfficeOpenXml.Drawing.Chart; +using OfficeOpenXml.Drawing.Chart.ChartEx; +using OfficeOpenXml.FormulaParsing.Excel.Functions.Finance; +using OfficeOpenXml.FormulaParsing.Excel.Functions.RefAndLookup; +using System; +using System.Collections.Generic; +using System.Linq; + +namespace EPPlus.Export.ImageRenderer.Svg.Chart +{ + internal abstract class ChartTypeDrawer : SvgChartObject + { + protected SvgChart _svgChart; + protected ExcelChart _chartType; + internal ChartTypeDrawer(SvgChart svgChart, ExcelChart chartType) : base(svgChart) + { + _svgChart = svgChart; + _chartType = chartType; + } + protected List LoadSeriesValues(string serieAddress, double[] numLiterals, string[] strLiterals) + { + 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); + var wsName = address.WorkSheetName; + if (string.IsNullOrEmpty(wsName)) + { + wsName = Chart.WorkSheet.Name; + } + if (Chart.WorkSheet.Workbook.Worksheets[wsName] != 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(Chart.WorkSheet.Workbook.Worksheets[wsName].Cells[r, c].Value); + } + } + } + } + return values; + } + public List RenderItems { get; } = new List(); + internal static List Create(SvgChart svgChart) + { + var drawers = new List(); + foreach (var ct in svgChart.Chart.PlotArea.ChartTypes) + { + switch (ct.ChartType) + { + case eChartType.Line: + case eChartType.LineMarkers: + case eChartType.LineStacked: + case eChartType.LineStacked100: + case eChartType.LineMarkersStacked: + case eChartType.LineMarkersStacked100: + drawers.Add(new LineChartTypeDrawer(svgChart, ct)); + break; + default: + throw new NotImplementedException($"No Svg support for Chart type {ct} is implemented."); + } + } + return drawers; + } + internal override void AppendRenderItems(List renderItems) + { + renderItems.AddRange(RenderItems); + } + } + +} diff --git a/src/EPPlus.Export.ImageRenderer/Svg/Chart/ChartTypeDrawers/LineChartTypeDrawer.cs b/src/EPPlus.Export.ImageRenderer/Svg/Chart/ChartTypeDrawers/LineChartTypeDrawer.cs new file mode 100644 index 000000000..241b65957 --- /dev/null +++ b/src/EPPlus.Export.ImageRenderer/Svg/Chart/ChartTypeDrawers/LineChartTypeDrawer.cs @@ -0,0 +1,82 @@ +using EPPlusImageRenderer.RenderItems; +using EPPlusImageRenderer.Svg; +using OfficeOpenXml.Drawing.Chart; +using System.Collections.Generic; + +namespace EPPlus.Export.ImageRenderer.Svg.Chart +{ + internal class LineChartTypeDrawer : ChartTypeDrawer + { + internal LineChartTypeDrawer(SvgChart svgChart, ExcelChart chartType) : base(svgChart, chartType) + { + var groupItem = new SvgGroupItem(ChartRenderer, _svgChart.Plotarea.Rectangle.Bounds); + RenderItems.Add(groupItem); + foreach (ExcelLineChartSerie serie in chartType.Series) + { + var yValues = LoadSeriesValues(serie.Series, serie.NumberLiteralsY, serie.StringLiteralsY); + var xValues = LoadSeriesValues(serie.XSeries, serie.NumberLiteralsX, serie.StringLiteralsX); + AddLine(chartType, serie, xValues, yValues); + } + RenderItems.Add(new SvgEndGroupItem(ChartRenderer, null)); + } + + private void AddLine(ExcelChart chartType, ExcelLineChartSerie serie, List xValues, List yValues) + { + var xAxis = _svgChart.HorizontalAxis; + SvgChartAxis yAxis; + if (chartType.UseSecondaryAxis) + { + yAxis = _svgChart.SecondVerticalAxis; + } + else + { + yAxis = _svgChart.VerticalAxis; + } + var linePath = new SvgRenderPathItem(ChartRenderer, _svgChart.Plotarea.Rectangle.Bounds); + var coords = new List(); + var markerItems = new List(); + + for (var i = 0; i < yValues.Count; i++) + { + object x; + if (xValues == null) + { + x = i + 1; + } + else + { + x = xValues[i]; + } + + var y = yValues[i]; + var xPos = xAxis.GetPositionInPlotarea(x); + var yPos = yAxis.GetPositionInPlotarea(y); + + if (double.IsNaN(yPos) == false) + { + coords.Add(xPos / _svgChart.Plotarea.Rectangle.Bounds.Width); + coords.Add(yPos / _svgChart.Plotarea.Rectangle.Bounds.Height); + } + if (serie.HasMarker() && serie.Marker.Style != eMarkerStyle.None) + { + float mx = (float)xPos; + float my = (float)yPos; + var ls = LineMarkerHelper.GetMarkerItem(_svgChart, serie, mx, my, false); + if ((serie.Marker.Style == eMarkerStyle.Plus || serie.Marker.Style == eMarkerStyle.X || serie.Marker.Style == eMarkerStyle.Star) && + serie.Marker.Fill.IsEmpty == false) + { + markerItems.Add(LineMarkerHelper.GetMarkerBackground(_svgChart, serie, mx, my, false)); + } + markerItems.Add(ls); + } + } + + linePath.Commands.Add(new EPPlusImageRenderer.PathCommands(PathCommandType.Move, linePath, coords.ToArray())); + linePath.SetDrawingPropertiesBorder(serie.Border, chartType.StyleManager.Style.SeriesLine.BorderReference.Color, true); + linePath.FillColor = "none"; // No fill for line + RenderItems.Add(linePath); + RenderItems.AddRange(markerItems); + } + } + +} diff --git a/src/EPPlus.Export.ImageRenderer/Svg/SvgChart.cs b/src/EPPlus.Export.ImageRenderer/Svg/Chart/SvgChart.cs similarity index 100% rename from src/EPPlus.Export.ImageRenderer/Svg/SvgChart.cs rename to src/EPPlus.Export.ImageRenderer/Svg/Chart/SvgChart.cs diff --git a/src/EPPlus.Export.ImageRenderer/Svg/SvgChartAxis.cs b/src/EPPlus.Export.ImageRenderer/Svg/Chart/SvgChartAxis.cs similarity index 97% rename from src/EPPlus.Export.ImageRenderer/Svg/SvgChartAxis.cs rename to src/EPPlus.Export.ImageRenderer/Svg/Chart/SvgChartAxis.cs index a6c44b961..b14c818d9 100644 --- a/src/EPPlus.Export.ImageRenderer/Svg/SvgChartAxis.cs +++ b/src/EPPlus.Export.ImageRenderer/Svg/Chart/SvgChartAxis.cs @@ -576,12 +576,24 @@ protected List GetAxisValue(ExcelChartAxisStandard ax, RenderItem rect, max = d; } } - var maxMajorTickmarks = 10; //TODO: Calculate based on rect size - GetAutoMinMaxValue(ax, maxMajorTickmarks, isCount, ref min, ref max, out majorUnit); - for (var v = min; v <= max; v += majorUnit) + var options = new ValueAxisScaleCalculator.AxisOptions + { + LockedMin = ax.MinValue, + LockedMax = ax.MaxValue, + LockedInterval = ax.MajorUnit, + AddPadding = ax.AxisPosition == eAxisPosition.Left || ax.AxisPosition == eAxisPosition.Right + }; + var length = ax.AxisPosition == eAxisPosition.Left || ax.AxisPosition == eAxisPosition.Right ? SvgChart.Bounds.Height : SvgChart.Bounds.Width; //Fix and use plotarea width/height. + var res = ValueAxisScaleCalculator.Calculate(min ?? 0, max ?? 0, length, options); + for ( var v = res.Min; v <= res.Max; v += res.Interval) { l.Add(v); } + + min = res.Min; + max = res.Max; + majorUnit = res.Interval; + return l; } diff --git a/src/EPPlus.Export.ImageRenderer/Svg/SvgChartLegend.cs b/src/EPPlus.Export.ImageRenderer/Svg/Chart/SvgChartLegend.cs similarity index 100% rename from src/EPPlus.Export.ImageRenderer/Svg/SvgChartLegend.cs rename to src/EPPlus.Export.ImageRenderer/Svg/Chart/SvgChartLegend.cs diff --git a/src/EPPlus.Export.ImageRenderer/Svg/SvgChartObject.cs b/src/EPPlus.Export.ImageRenderer/Svg/Chart/SvgChartObject.cs similarity index 100% rename from src/EPPlus.Export.ImageRenderer/Svg/SvgChartObject.cs rename to src/EPPlus.Export.ImageRenderer/Svg/Chart/SvgChartObject.cs diff --git a/src/EPPlus.Export.ImageRenderer/Svg/SvgChartPlotarea.cs b/src/EPPlus.Export.ImageRenderer/Svg/Chart/SvgChartPlotarea.cs similarity index 100% rename from src/EPPlus.Export.ImageRenderer/Svg/SvgChartPlotarea.cs rename to src/EPPlus.Export.ImageRenderer/Svg/Chart/SvgChartPlotarea.cs diff --git a/src/EPPlus.Export.ImageRenderer/Svg/SvgChartTitle.cs b/src/EPPlus.Export.ImageRenderer/Svg/Chart/SvgChartTitle.cs similarity index 100% rename from src/EPPlus.Export.ImageRenderer/Svg/SvgChartTitle.cs rename to src/EPPlus.Export.ImageRenderer/Svg/Chart/SvgChartTitle.cs diff --git a/src/EPPlus.Export.ImageRenderer/Svg/Chart/Util/ValueAxisScaleCalculator.cs b/src/EPPlus.Export.ImageRenderer/Svg/Chart/Util/ValueAxisScaleCalculator.cs new file mode 100644 index 000000000..2a6cb67e4 --- /dev/null +++ b/src/EPPlus.Export.ImageRenderer/Svg/Chart/Util/ValueAxisScaleCalculator.cs @@ -0,0 +1,175 @@ +using System; +using System.Collections.Generic; + +internal class ValueAxisScaleCalculator +{ + internal class AxisOptions + { + public double? LockedMin { get; set; } + public double? LockedMax { get; set; } + public double? LockedInterval { get; set; } + public bool AddPadding { get; set; } = false; + } + internal class AxisScale + { + public double Min { get; set; } + public double Max { get; set; } + public double Interval { get; set; } + public int TickCount { get; set; } + } + + internal static AxisScale Calculate(double dataMin, double dataMax, double chartHeightPixels, AxisOptions axisOptions = null) + { + int desiredTicks = chartHeightPixels < 200 ? 4 : + chartHeightPixels < 400 ? 5 : 6; + + if (axisOptions == null) + { + axisOptions = new AxisOptions(); + } + + // Handle equal data series. + if (dataMin == dataMax) + { + if (dataMin < 0) + { + dataMax = 0; + } + else + { + dataMin = 0; + } + } + + var isAllPositive = dataMin > 0 && dataMax > 0; + var isAllNegativ = dataMin < 0 && dataMax < 0; + + double interval; + if (axisOptions.LockedInterval.HasValue) + { + interval = axisOptions.LockedInterval.Value; + + // Calculate min and max based on locked interval + if (!axisOptions.LockedMin.HasValue) + { + if (axisOptions.AddPadding) + { + dataMin = Math.Floor(dataMin * 0.1 / interval) * interval; + if(isAllPositive && dataMin < 0 ) + { + dataMin = 0; + } + } + } + else + { + dataMin = axisOptions.LockedMin.Value; + } + + if (!axisOptions.LockedMax.HasValue) + { + dataMax = Math.Ceiling(dataMax * 0.1 / interval) * interval; + } + else + { + if (axisOptions.AddPadding) + { + dataMax = axisOptions.LockedMax.Value; + if (isAllNegativ && dataMax > 0) + { + dataMax = 0; + } + } + } + + int tickCount = (int)Math.Round((dataMax - dataMin) / Math.Min(desiredTicks, interval)) + 1; + return new AxisScale + { + Min = dataMin, + Max = dataMax, + Interval = interval, + TickCount = tickCount + }; + } + else + { + if (axisOptions.LockedMin.HasValue) + { + dataMin = axisOptions.LockedMin.Value; + } + if (axisOptions.LockedMax.HasValue) + { + dataMax = axisOptions.LockedMax.Value; + } + + double dataRange = dataMax - dataMin; + + // Add padding (10%) + double paddedMin = dataMin - (dataRange * 0.1); + double paddedMax = dataMax + (dataRange * 0.1); + + //Normalize to zero if all data is positive or negative + if (paddedMin < 0 && isAllPositive) + { + paddedMin = 0; + } + if (paddedMax > 0 && isAllNegativ) + { + paddedMax = 0; + } + + // Calculate nice interval + double roughInterval = (paddedMax - paddedMin) / desiredTicks; + double scaleInterval = GetScaleNumber(roughInterval, true); + + // Calculate false min and max + double axisMin = Math.Floor(paddedMin / scaleInterval) * scaleInterval; + double axisMax = Math.Ceiling(paddedMax / scaleInterval) * scaleInterval; + + // Calculate actual tick count + int tickCount = (int)Math.Round((axisMax - axisMin) / scaleInterval) + 1; + return new AxisScale + { + Min = axisOptions.LockedMin ?? axisMin, + Max = axisOptions.LockedMax ?? axisMax, + Interval = scaleInterval, + TickCount = tickCount + }; + } + } + + private static double GetScaleNumber(double value, bool round) + { + double exponent = Math.Floor(Math.Log10(value)); + double fraction = value / Math.Pow(10, exponent); + double scaleFraction; + + if (round) + { + if (fraction < 1.5) scaleFraction = 1; + else if (fraction < 3) scaleFraction = 2; + else if (fraction < 7) scaleFraction = 5; + else scaleFraction = 10; + } + else + { + if (fraction <= 1) scaleFraction = 1; + else if (fraction <= 2) scaleFraction = 2; + else if (fraction <= 5) scaleFraction = 5; + else scaleFraction = 10; + } + + return scaleFraction * Math.Pow(10, exponent); + } + + public static List GetTickValues(AxisScale scale) + { + var ticks = new List(); + for (int i = 0; i < scale.TickCount; i++) + { + double tickValue = scale.Min + (i * scale.Interval); + ticks.Add(Math.Round(tickValue, 10)); // Round to avoid floating point errors + } + return ticks; + } +} \ No newline at end of file diff --git a/src/EPPlus/Drawing/Chart/ExcelChartAxisStandard.cs b/src/EPPlus/Drawing/Chart/ExcelChartAxisStandard.cs index 839473f6d..91e83a84d 100644 --- a/src/EPPlus/Drawing/Chart/ExcelChartAxisStandard.cs +++ b/src/EPPlus/Drawing/Chart/ExcelChartAxisStandard.cs @@ -712,10 +712,15 @@ internal override object[] GetAxisValues(out bool isCount) } else { - if ((Index == 1 && !ct.UseSecondaryAxis) || (Index == 2 && ct.UseSecondaryAxis)) + //if ((Index == 1 && !ct.UseSecondaryAxis) || (Index == 2 && ct.UseSecondaryAxis)) + if (ct.YAxis == this) { AddFromSerie(hs, serie.Series, serie.NumberLiteralsY, serie.StringLiteralsY, false); } + else if(ct.XAxis == this) + { + AddFromSerie(hs, serie.XSeries, serie.NumberLiteralsX, serie.StringLiteralsX, false); + } } } } From 2d1089a19ea3e538539cf9509ef60eb2aaae2138 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jan=20K=C3=A4llman?= Date: Mon, 2 Feb 2026 13:50:04 +0100 Subject: [PATCH 033/151] Fixes issue #2265 (#2267) --- src/EPPlus.sln | 4 ++-- .../DependencyChain/RpnFormulaExecution.cs | 21 ++++++++++--------- 2 files changed, 13 insertions(+), 12 deletions(-) diff --git a/src/EPPlus.sln b/src/EPPlus.sln index 15c8f4703..707a70612 100644 --- a/src/EPPlus.sln +++ b/src/EPPlus.sln @@ -1,7 +1,7 @@  Microsoft Visual Studio Solution File, Format Version 12.00 -# Visual Studio Version 17 -VisualStudioVersion = 17.0.31912.275 +# Visual Studio Version 18 +VisualStudioVersion = 18.0.11217.181 d18.0 MinimumVisualStudioVersion = 10.0.40219.1 Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "EPPlus", "EPPlus\EPPlus.csproj", "{219F673E-6115-4858-9E07-E33D24E795FE}" EndProject diff --git a/src/EPPlus/FormulaParsing/DependencyChain/RpnFormulaExecution.cs b/src/EPPlus/FormulaParsing/DependencyChain/RpnFormulaExecution.cs index 4c81515e0..2fb9c29c6 100644 --- a/src/EPPlus/FormulaParsing/DependencyChain/RpnFormulaExecution.cs +++ b/src/EPPlus/FormulaParsing/DependencyChain/RpnFormulaExecution.cs @@ -410,7 +410,7 @@ private static CompileResult CalculateFormulaChain(RpnOptimizedDependencyChain d RangeHashset rd = AddOrGetRDFromWsIx(depChain, f._ws == null ? -1 : f._ws.IndexInList); object v = null; bool hasLogger = depChain._parsingContext.Parser.Logger != null; - var followChain = options.FollowDependencyChain && depChainPos==-1; + var followChain = options.FollowDependencyChain; if (depChainPos == -1) { rd?.Merge(f._row, f._column); @@ -423,7 +423,7 @@ private static CompileResult CalculateFormulaChain(RpnOptimizedDependencyChain d var ws = f._ws; if (f._tokenIndex < f._tokens.Count) { - addresses = ExecuteNextToken(depChain, f, followChain || depChain._formulaStack.Count>0); + addresses = ExecuteNextToken(depChain, f, followChain); if (f._tokenIndex < f._tokens.Count) { if (addresses == null && f._expressions.ContainsKey(f._tokenIndex) && f._expressions[f._tokenIndex].ExpressionType == ExpressionType.NameValue) @@ -807,7 +807,7 @@ private static void SetValueToWorkbook(RpnOptimizedDependencyChain depChain, Rpn } private static void RecalculateDirtyCells(SimpleAddress[] dirtyRange, RpnOptimizedDependencyChain depChain, RangeHashset rd, ExcelCalculationOption options) { - if(depChain._isInRecalculateDirtyCells) return; //EPPlus will not recalculate dirty cells while recalculating other dirty cells to avoid stack overflow. + if(depChain._isInRecalculateDirtyCells || options.FollowDependencyChain==false) return; //EPPlus will not recalculate dirty cells while recalculating other dirty cells to avoid stack overflow. var dirtyCells = dirtyRange.ToList(); if (depChain.FormulaRangeReferences.ContainsKey(depChain._parsingContext.CurrentWorksheet.IndexInList)) { @@ -1055,6 +1055,7 @@ private static FormulaRangeAddress[] ExecuteNextToken(RpnOptimizedDependencyChai { FormulaRangeAddress[] addresses; var s = f._expressionStack; + bool IsInRecalculateDirtyCells = depChain._isInRecalculateDirtyCells; while (f._tokenIndex < f._tokens.Count) { if (f.HasLambdaToken(f._tokenIndex)) @@ -1150,7 +1151,7 @@ private static FormulaRangeAddress[] ExecuteNextToken(RpnOptimizedDependencyChai f.LambdaSettings.LambdaArgsAdded.Push(++nLambdaArgsAdded); } } - if (localReturnAddress && returnAddresses && (f._funcStack.Count == 0 || ShouldIgnoreAddress(f._funcStack.Peek()) == false)) + if (localReturnAddress && returnAddresses && (f._funcStack.Count == 0 || ShouldIgnoreAddress(f._funcStack.Peek()) == false) && IsInRecalculateDirtyCells == false) { if(f._tokenIndex + 1 < f._tokens.Count) { @@ -1178,12 +1179,12 @@ private static FormulaRangeAddress[] ExecuteNextToken(RpnOptimizedDependencyChai var nameAddress = ne.GetAddress(); if (nameAddress == null) { - if (returnAddresses && string.IsNullOrEmpty(ne._name?.Formula) == false) + if (returnAddresses && string.IsNullOrEmpty(ne._name?.Formula) == false && IsInRecalculateDirtyCells == false) { return null; } } - else if (returnAddresses && (f._funcStack.Count == 0 || ShouldIgnoreAddress(f._funcStack.Peek()) == false)) + else if (returnAddresses && (f._funcStack.Count == 0 || ShouldIgnoreAddress(f._funcStack.Peek()) == false) && IsInRecalculateDirtyCells == false) { if (IsSingleAddress(f)) { @@ -1252,7 +1253,7 @@ private static FormulaRangeAddress[] ExecuteNextToken(RpnOptimizedDependencyChai f.IgnoreCaching = true; } - if (PreExecFunc(depChain, f, funcExp)) + if (PreExecFunc(depChain, f, funcExp) && returnAddresses) { f._currentFunction = funcExp; f._tokenIndex--; //We should stay on this token when we continue on this formula. @@ -1264,7 +1265,7 @@ private static FormulaRangeAddress[] ExecuteNextToken(RpnOptimizedDependencyChai else { funcExp = f._currentFunction; - if (funcExp._dependencyAddresses.Count > 0) + if (funcExp._dependencyAddresses.Count > 0 && returnAddresses) { f._tokenIndex--; //We should stay on this token when we continue on this formula. var a = funcExp._dependencyAddresses.ToArray(); @@ -1290,7 +1291,7 @@ private static FormulaRangeAddress[] ExecuteNextToken(RpnOptimizedDependencyChai f.LambdaSettings.LambdaStackNumbers.Push(s.Count); f.LambdaSettings.NumberOfLambdaVariables.Push(clc.NumberOfVariables); } - if (r.Address != null) + if (r.Address != null && returnAddresses) { if ((f._funcStack.Count == 0 || ShouldIgnoreAddress(f._funcStack.Peek()) == false) && r.Address != null) { @@ -1382,7 +1383,7 @@ private static FormulaRangeAddress[] ExecuteNextToken(RpnOptimizedDependencyChai } f._tokenIndex++; - if (f._tokenIndex == f._tokens.Count) + if (f._tokenIndex == f._tokens.Count && returnAddresses) { if (s.Count > 0 && s.Peek().Status == ExpressionStatus.IsAddress) { From 8c3fe47f0189a2ee462d79471e26c02da3920395 Mon Sep 17 00:00:00 2001 From: AdrianEPPlus <162118292+AdrianEPPlus@users.noreply.github.com> Date: Mon, 2 Feb 2026 14:14:26 +0100 Subject: [PATCH 034/151] fixed checking for negative sign when using custom number format. (#2259) --- src/EPPlus/ExcelRangeBase.cs | 1 - src/EPPlus/Utils/String/ValueToTextHandler.cs | 7 ++++++- src/EPPlusTest/Issues/WorksheetIssues.cs | 12 ++++++++++++ 3 files changed, 18 insertions(+), 2 deletions(-) diff --git a/src/EPPlus/ExcelRangeBase.cs b/src/EPPlus/ExcelRangeBase.cs index 1a3a6312c..a681380e3 100644 --- a/src/EPPlus/ExcelRangeBase.cs +++ b/src/EPPlus/ExcelRangeBase.cs @@ -816,7 +816,6 @@ public string Text { get { - object value; if (IsSingleCell || IsName) { diff --git a/src/EPPlus/Utils/String/ValueToTextHandler.cs b/src/EPPlus/Utils/String/ValueToTextHandler.cs index 05a335c0c..3325a47d7 100644 --- a/src/EPPlus/Utils/String/ValueToTextHandler.cs +++ b/src/EPPlus/Utils/String/ValueToTextHandler.cs @@ -231,7 +231,12 @@ private static string FormatNumber(double d, string format, CultureInfo cultureI private static string CheckAndRemoveNegativeSign(string format, string s, string ns) { - if ((s.StartsWith($"{ns}{ns}") || s.StartsWith($"{ns}-")) && (format.StartsWith(ns) || format.StartsWith("-"))) + //This regex pattern ^(\[[^\]]+\]|[\\'\""\*_])* removes: + // \[[^\]]+\] : This part removes Anything inside square brackets + // [\\'\""\*_] : This part removes Backslashes, quotes, asterisks and underlines + string trimmedFormat = System.Text.RegularExpressions.Regex.Replace(format, @"^(\[[^\]]+\]|[\\'\""\*_])*", ""); + bool formatStartsWithNegative = trimmedFormat.StartsWith(ns) || trimmedFormat.StartsWith("-"); + if ((s.StartsWith($"{ns}{ns}") || s.StartsWith($"{ns}-")) && formatStartsWithNegative) { return s.Remove(1, 1); } diff --git a/src/EPPlusTest/Issues/WorksheetIssues.cs b/src/EPPlusTest/Issues/WorksheetIssues.cs index fb4b12423..9ab79e640 100644 --- a/src/EPPlusTest/Issues/WorksheetIssues.cs +++ b/src/EPPlusTest/Issues/WorksheetIssues.cs @@ -1079,5 +1079,17 @@ public void testRepro() } SwitchBackToCurrentCulture(); } + + [TestMethod] + public void i2258() + { + var p = OpenTemplatePackage("repro2.xlsx"); + var ws = p.Workbook.Worksheets.First(); + var a1 = ws.Cells["A1"].Text; + var b1 = ws.Cells["B1"].Text; + Assert.AreEqual("-/- 1 000", a1); + Assert.AreEqual("-/- 2 000", b1); + } + } } From 728ec9eef9e27da555cdccc1caa0d4166a7a62ad Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ossian=20Edstr=C3=B6m?= Date: Tue, 3 Feb 2026 13:40:15 +0100 Subject: [PATCH 035/151] Functional non-bugged return of textLines --- .../TextRenderTests.cs | 77 ++++++++++ .../TextFragmentCollectionTests.cs | 134 ++++++++++++++++++ .../TrueTypeMeasurer/FontMeasurerTrueType.cs | 9 +- .../TrueTypeMeasurer/TextData.cs | 105 +++++++++----- 4 files changed, 278 insertions(+), 47 deletions(-) diff --git a/src/EPPlus.Export.ImageRenderer.Test/TextRenderTests.cs b/src/EPPlus.Export.ImageRenderer.Test/TextRenderTests.cs index 6d840b542..00ecf0ad7 100644 --- a/src/EPPlus.Export.ImageRenderer.Test/TextRenderTests.cs +++ b/src/EPPlus.Export.ImageRenderer.Test/TextRenderTests.cs @@ -92,6 +92,83 @@ List GetWrappedText(ExcelDrawingTextRunCollection runs, TextFrag return ttMeasurer.WrapMultipleTextFragmentsToTextLines(fragments, fonts, maxSizePoints); } + [TestMethod] + public void TextFragmentHandlesEndLines() + { + string strWEndLines = "TextBox\r\na"; + + List inputFrags = new List() { strWEndLines }; + var textFragments = new TextFragmentCollection(inputFrags); + + } + + [TestMethod] + public void MeasureWrappedWidths() + { + List lstOfRichText = new() { /*"TextBox\r\na",*/ "TextBox2", "ra underline", "La Strike", "Goudy size 16", "SvgSize 24" }; + + //var font1 = new MeasurementFont() + //{ + // FontFamily = "Aptos Narrow", + // Size = 11, + // Style = MeasurementFontStyles.Regular + //}; ; + + var font2 = new MeasurementFont() + { + FontFamily = "Aptos Narrow", + Size = 11, + Style = MeasurementFontStyles.Italic | MeasurementFontStyles.Bold + }; + + var font3 = new MeasurementFont() + { + FontFamily = "Aptos Narrow", + Size = 11, + Style = MeasurementFontStyles.Underline + }; + + var font4 = new MeasurementFont() + { + FontFamily = "Aptos Narrow", + Size = 11, + Style = MeasurementFontStyles.Strikeout + }; + + var font5 = new MeasurementFont() + { + FontFamily = "Goudy Stout", + Size = 16, + Style = MeasurementFontStyles.Regular + }; + + + var font6 = new MeasurementFont() + { + FontFamily = "Aptos Narrow", + Size = 24, + Style = MeasurementFontStyles.Regular + }; + + List fonts = new() { /*font1,*/ font2, font3, font4, font5, font6}; + + var maxSizePoints = Math.Round(300d, 0, MidpointRounding.AwayFromZero).PixelToPoint(); + var ttMeasurer = new FontMeasurerTrueType(font2); + + var textFragments = new TextFragmentCollection(lstOfRichText); + + var wrappedLines = ttMeasurer.WrapMultipleTextFragmentsToTextLines(textFragments, fonts, maxSizePoints); + + Assert.AreEqual(wrappedLines[0].r) + + + //Line 1 45 px 34.5pt + //Line 2 6px 4.5 pt + //Line 3 137 px 102.75 pt //result: 104.6328125 pt width "whole + //Line 4 270 px 202.5 pt + //Line 5 169 px 126.75 pt + } + [TestMethod] public void VerifyTextRunBounds() { diff --git a/src/EPPlus.Fonts.OpenType.Tests/TextFragmentCollectionTests.cs b/src/EPPlus.Fonts.OpenType.Tests/TextFragmentCollectionTests.cs index af30eb521..50372ea90 100644 --- a/src/EPPlus.Fonts.OpenType.Tests/TextFragmentCollectionTests.cs +++ b/src/EPPlus.Fonts.OpenType.Tests/TextFragmentCollectionTests.cs @@ -1,5 +1,6 @@ using EPPlus.Fonts.OpenType.TrueTypeMeasurer; using EPPlus.Fonts.OpenType.TrueTypeMeasurer.DataHolders; +using EPPlus.Fonts.OpenType.Utils; using Microsoft.VisualStudio.TestPlatform.CrossPlatEngine.Adapter; using OfficeOpenXml; using OfficeOpenXml.Interfaces.Drawing.Text; @@ -33,5 +34,138 @@ public void EnsureTextFragmentsAndWrapperWorkCorrectlyForLongParagraphs() Assert.AreEqual("e f g h i j k l m n o p q", outputLines[1]); Assert.AreEqual("r s t u v w x y z ", outputLines[2]); } + + //TODO: DOUBLE-CHECK BOLD+ITALIC for narrow later it seems innaccurate + [TestMethod] + public void MeasureBold() + { + var font2 = new MeasurementFont() + { + FontFamily = "Aptos Narrow", + Size = 11, + Style = MeasurementFontStyles.Bold + }; + string test = "TextBox2"; + + var maxSizePoints = Math.Round(300d, 0, MidpointRounding.AwayFromZero).PixelToPoint(); + var ttMeasurer = new FontMeasurerTrueType(font2); + + ttMeasurer.SetFont(font2); + + var widthInPoints = ttMeasurer.MeasureTextWidth(test); + var inPixels = Math.Round(widthInPoints.PointToPixel(),0,MidpointRounding.AwayFromZero); + + Assert.AreEqual(54, inPixels); + } + + [TestMethod] + public void MeasureGoudy() + { + var font5 = new MeasurementFont() + { + FontFamily = "Goudy Stout", + Size = 16, + Style = MeasurementFontStyles.Regular + }; + + var text = "Goudy size"; + + var ttMeasurer = new FontMeasurerTrueType(font5); + + var widthInPoints = ttMeasurer.MeasureTextWidth(text); + var inPixels = Math.Round(widthInPoints.PointToPixel(), 0, MidpointRounding.AwayFromZero); + + Assert.AreEqual(237, inPixels); + } + + [TestMethod] + public void MeasureWrappedWidths() + { + List lstOfRichText = new() { "TextBox2", "ra underline", "La Strike", "Goudy size 16", "SvgSize 24" }; + + var font2 = new MeasurementFont() + { + FontFamily = "Aptos Narrow", + Size = 11, + Style = MeasurementFontStyles.Bold + }; + + var font3 = new MeasurementFont() + { + FontFamily = "Aptos Narrow", + Size = 11, + Style = MeasurementFontStyles.Underline + }; + + var font4 = new MeasurementFont() + { + FontFamily = "Aptos Narrow", + Size = 11, + Style = MeasurementFontStyles.Strikeout + }; + + var font5 = new MeasurementFont() + { + FontFamily = "Goudy Stout", + Size = 16, + Style = MeasurementFontStyles.Regular + }; + + + var font6 = new MeasurementFont() + { + FontFamily = "Aptos Narrow", + Size = 24, + Style = MeasurementFontStyles.Regular + }; + + List fonts = new() {font2, font3, font4, font5, font6 }; + + var maxSizePoints = Math.Round(300d, 0, MidpointRounding.AwayFromZero).PixelToPoint(); + var ttMeasurer = new FontMeasurerTrueType(font2); + + var textFragments = new TextFragmentCollection(lstOfRichText); + + var wrappedLines = ttMeasurer.WrapMultipleTextFragmentsToTextLines(textFragments, fonts, maxSizePoints); + + var pixels1 = Math.Round(wrappedLines[0].RtFragments[0].Width.PointToPixel(), 0, MidpointRounding.AwayFromZero); + var pixels2 = Math.Round(wrappedLines[0].RtFragments[1].Width.PointToPixel(), 0, MidpointRounding.AwayFromZero); + var pixels3 = Math.Round(wrappedLines[0].RtFragments[2].Width.PointToPixel(), 0, MidpointRounding.AwayFromZero); + var pixelsWholeLine = Math.Round(wrappedLines[0].Width.PointToPixel(),0, MidpointRounding.AwayFromZero); + + //~54 px + Assert.AreEqual(54, pixels1); + //~70 px aka 51.75pt + Assert.AreEqual(70, pixels2); + //~16-17 px This line contains a space at the end + Assert.AreEqual(17, pixels3); + + //Total Width: ~140 + Assert.AreEqual(140d, pixelsWholeLine); + + var pixels21 = Math.Round(wrappedLines[1].RtFragments[0].Width.PointToPixel(), 0, MidpointRounding.AwayFromZero); + var pixels22 = Math.Round(wrappedLines[1].RtFragments[1].Width.PointToPixel(), 0, MidpointRounding.AwayFromZero); + var pixelsWholeLine2 = Math.Round(wrappedLines[1].Width.PointToPixel(), 0, MidpointRounding.AwayFromZero); + //~34 px + Assert.AreEqual(33, pixels21); + // This line contains a space at the end + Assert.AreEqual(248, pixels22); + + Assert.AreEqual(281, pixelsWholeLine2); + + var pixels31 = Math.Round(wrappedLines[2].RtFragments[0].Width.PointToPixel(), 0, MidpointRounding.AwayFromZero); + var pixels32 = Math.Round(wrappedLines[2].RtFragments[1].Width.PointToPixel(), 0, MidpointRounding.AwayFromZero); + var pixelsWholeLine3 = Math.Round(wrappedLines[2].Width.PointToPixel(), 0, MidpointRounding.AwayFromZero); + Assert.AreEqual(35, pixels31); + //This line does NOT contain a space at the end + Assert.AreEqual(134, pixels32); + + + Assert.AreEqual(169, pixelsWholeLine3); + + //Line 1 137 px 102.75 pt //result: 104.6328125 pt width "whole + //Line 2 270 px 202.5 pt + //Line 3 169 px 126.75 pt + } } } diff --git a/src/EPPlus.Fonts.OpenType/TrueTypeMeasurer/FontMeasurerTrueType.cs b/src/EPPlus.Fonts.OpenType/TrueTypeMeasurer/FontMeasurerTrueType.cs index 7dd616732..6d0d307a3 100644 --- a/src/EPPlus.Fonts.OpenType/TrueTypeMeasurer/FontMeasurerTrueType.cs +++ b/src/EPPlus.Fonts.OpenType/TrueTypeMeasurer/FontMeasurerTrueType.cs @@ -81,14 +81,7 @@ public FontMeasurerTrueType(double fontSize, string fontName, FontSubFamily font public FontMeasurerTrueType(MeasurementFont mFont) { - CurrentFontName = string.IsNullOrEmpty(mFont.FontFamily) ? "" : mFont.FontFamily; - - SetFont(mFont.Size, mFont.FontFamily); - - if (CurrentFont == null) - { - CurrentFont = _defaultFont; - } + SetFont(mFont); } diff --git a/src/EPPlus.Fonts.OpenType/TrueTypeMeasurer/TextData.cs b/src/EPPlus.Fonts.OpenType/TrueTypeMeasurer/TextData.cs index 1ef9f6b25..9750c0763 100644 --- a/src/EPPlus.Fonts.OpenType/TrueTypeMeasurer/TextData.cs +++ b/src/EPPlus.Fonts.OpenType/TrueTypeMeasurer/TextData.cs @@ -10,6 +10,7 @@ Date Author Change ************************************************************************************************* 10/07/2025 EPPlus Software AB EPPlus.Fonts.OpenType 1.0 *************************************************************************************************/ +using EPPlus.Fonts.OpenType.Tables.Cmap; using EPPlus.Fonts.OpenType.Tables.Cmap.Mappings; using EPPlus.Fonts.OpenType.Tables.Kern; using EPPlus.Fonts.OpenType.TrueTypeMeasurer; @@ -373,14 +374,19 @@ private static int CalculateAdvanceWidth(char c, GlyphMappings mappings, OpenTyp return advanceWidth; } - private static void WrapAtCharPos(string line, int charPos, ref int nextLineStartIndex, ref int lineWidth, ref int wordWidth, int advanceWidth, List wrappedStrings) + private static void WrapAtCharPos(string line, int charPos, ref int nextLineStartIndex, ref int lineWidth, ref int wordWidth, int advanceWidth, List wrappedStrings, int spaceWidth, out int previousLineWidthPostWrap) { - var wrappedString = ExtractWrappedSubstring(line, charPos, ref nextLineStartIndex, out TotalAdvanceMode advanceMode); - wrappedStrings.Add(wrappedString); + var prevLineWidth = lineWidth; + var wrappedString = ExtractWrappedSubstring(line, charPos, ref nextLineStartIndex, out TotalAdvanceMode advanceMode, out bool trimmedSpace); + wrappedStrings.Add(wrappedString); //Using enum to make it one Input parameter in WrapString instead of all 3 //this as they're not actually used in there lineWidth = GetAdvanceWidthFromMode(advanceWidth, wordWidth, advanceMode); + + //var trimmedSpaceWidth = trimmedSpace ? spaceWidth : 0; + + previousLineWidthPostWrap = prevLineWidth - lineWidth /*- trimmedSpaceWidth*/; //New line means both totals are equal wordWidth = lineWidth; } @@ -388,7 +394,7 @@ private static void WrapAtCharPos(string line, int charPos, ref int nextLineStar private static void MeasureAndWrapLine(string line, OpenTypeFont font, ref int lineWidth, ref int wordWidth, GlyphMappings glyphMappings, ushort? lastGlyphIndex, double maxWidth, List wrappedStrings, bool applyKerning = true) { int nextLineStartIndex = 0; - + var spaceWidth = CalcGlyphWidth(glyphMappings, ' ', font, ref lastGlyphIndex, ref applyKerning); for (int i = 0; i < line.Length; i++) { char c = line[i]; @@ -396,7 +402,7 @@ private static void MeasureAndWrapLine(string line, OpenTypeFont font, ref int l if (lineWidth >= maxWidth) { - WrapAtCharPos(line, i, ref nextLineStartIndex, ref lineWidth, ref wordWidth, advanceWidth, wrappedStrings); + WrapAtCharPos(line, i, ref nextLineStartIndex, ref lineWidth, ref wordWidth, advanceWidth, wrappedStrings, spaceWidth, out int prevLineWidth); } } @@ -620,10 +626,11 @@ enum TotalAdvanceMode /// The starting char index of old and then the new line in orgLine /// Informs calling method what the advance of the next line should be set to /// - private static string ExtractWrappedSubstring(string orgLine, int cIdx, ref int startLineIdx, out TotalAdvanceMode mode) + private static string ExtractWrappedSubstring(string orgLine, int cIdx, ref int startLineIdx, out TotalAdvanceMode mode, out bool trimmedSpace) { //Result string string wrappedString = string.Empty; + trimmedSpace = false; var prevStartIdx = startLineIdx; char c = orgLine[cIdx]; @@ -641,8 +648,14 @@ private static string ExtractWrappedSubstring(string orgLine, int cIdx, ref int { var stringOverMax = splitLines.Last(); var startIndex = txt.Length - stringOverMax.Length; + var spacedString = txt.Remove(startIndex, stringOverMax.Length); + + if(spacedString.EndsWith(" ")) + { + trimmedSpace = true; + } //Remove the overflowing characters - var spacedString = txt.Remove(startIndex, stringOverMax.Length).TrimEnd(' '); + var trimmedString = spacedString.TrimEnd(' '); wrappedString = spacedString; @@ -657,6 +670,7 @@ private static string ExtractWrappedSubstring(string orgLine, int cIdx, ref int //Therefore we do not add its width and the index of the next line starts at the next character. if (c == ' ') { + trimmedSpace = true; //The current char has crossed the max //Therefore remove it from the text to be added. wrappedString = txt.Substring(0, txt.Length); @@ -694,6 +708,20 @@ private static int GetAdvanceWidthFromMode(int widthChar, int widthWord, TotalAd $"And does not exist within the enum: '{typeof(TotalAdvanceMode)}' "); } + internal static int ConvertDesignUnits(OpenTypeFont origFont, double origSize, OpenTypeFont targetFont, double targetSize, int width) + { + //Potential future optimization: Check if units perEm are equal if they are (most fonts are) + //Should be able to only apply a factor of origSize/targetSize + var factorOrig = origSize / ((double)origFont.HeadTable.UnitsPerEm); + + var widthInPoints = width * factorOrig; + + var factorTarget = ((double)targetFont.HeadTable.UnitsPerEm) / targetSize; + + width = Convert.ToInt16(widthInPoints * factorTarget); + return width; + } + /// /// Converts the design units of one font to the design units of another font /// @@ -754,7 +782,8 @@ private static void LogLineData(TextParagraph paragraph, OpenTypeFont currentFon private static void AddDataToSimpleLine(TextLineSimple line, TextParagraph paragraph, OpenTypeFont currentFont, OpenTypeFont largestFont, double CurrentLineLargestSize, int lineWidth, int fragmentIdx) { - line.Width = (double)lineWidth / (double)currentFont.HeadTable.UnitsPerEm * paragraph.FontSizes[fragmentIdx]; + var factorOrig = paragraph.FontSizes[fragmentIdx] / (double)currentFont.HeadTable.UnitsPerEm; + line.Width = (double)lineWidth * factorOrig; line.LargestFontSize = CurrentLineLargestSize; line.LargestAscent = GetBaseLine(largestFont, CurrentLineLargestSize); line.LargestDescent = MeasureDescent(largestFont, CurrentLineLargestSize); @@ -798,6 +827,8 @@ internal static List WrapMultipleTextFragments(TextParagraph paragraph, //int lastFragmentWidth = 0; //---Logger Variables end--- + int spaceWidth = 0; + //Iterate through All fragments as one concatenated text string for (int i = 0; i < allText.Length; i++) { @@ -861,6 +892,7 @@ internal static List WrapMultipleTextFragments(TextParagraph paragraph, largestFontCurrentLine = currentFont; CurrentLineLargestFontSize = paragraph.FontSizes[fragmentIdx]; } + spaceWidth = CalcGlyphWidth(paragraph.GlyphMappings[fragmentIdx], ' ', currentFont, ref lastGlyphIndex, ref applyKerning); } } else @@ -872,6 +904,7 @@ internal static List WrapMultipleTextFragments(TextParagraph paragraph, largestFontCurrentLine = currentFont; CurrentLineLargestFontSize = paragraph.FontSizes[fragmentIdx]; } + spaceWidth = CalcGlyphWidth(paragraph.GlyphMappings[fragmentIdx], ' ', currentFont, ref lastGlyphIndex, ref applyKerning); } fontSize = paragraph.FontSizes[fragmentIdx]; @@ -883,7 +916,7 @@ internal static List WrapMultipleTextFragments(TextParagraph paragraph, //Perform the actual wrapping if (lineWidth > maxWidth) { - WrapAtCharPos(allText, i, ref prevLineEndIndex, ref lineWidth, ref wordWidth, advanceWidth, wrappedStrings); + WrapAtCharPos(allText, i, ref prevLineEndIndex, ref lineWidth, ref wordWidth, advanceWidth, wrappedStrings, spaceWidth, out int previousLineWidth); //Log where wrapping occured in order to keep track of fragment/run/richtext @@ -983,6 +1016,8 @@ internal static List WrapMultipleTextFragmentsToTextLines(TextPa //int lastFragmentWidth = 0; //---Logger Variables end--- + int spaceWidth = 0; + //Iterate through All fragments as one concatenated text string for (int i = 0; i < allText.Length; i++) { @@ -994,20 +1029,14 @@ internal static List WrapMultipleTextFragmentsToTextLines(TextPa //Happens at the end of a fragment/start of a new fragment if (fragmentIdx != prevFragmentIdx) { - double leftoverWidth = 0; - if(currentRtFragment.Width != 0) - { - leftoverWidth = currentRtFragment.Width; - } AddWidthToFragment(currentRtFragment, currentFont.HeadTable.UnitsPerEm, fontSize, lineWidth, prevFragWidths, prevFragmentIdx); - currentRtFragment.Width += leftoverWidth; currentLineRtFragments.Add(currentRtFragment); currentRtFragment = new RichTextFragmentSimple(); currentRtFragment.Fragidx = fragmentIdx; currentRtFragment.OverallParagraphStartCharIdx = i; - prevFragWidths = lineWidth; + prevFragWidths = ConvertDesignUnits(currentFont, fontSize, paragraph.FontIndexDict[fragmentIdx], paragraph.FontSizes[fragmentIdx], lineWidth); } //If we hit a pre-existing line break. Reset line and wordwidths @@ -1079,6 +1108,7 @@ internal static List WrapMultipleTextFragmentsToTextLines(TextPa largestFontCurrentLine = currentFont; CurrentLineLargestFontSize = paragraph.FontSizes[fragmentIdx]; } + spaceWidth = CalcGlyphWidth(paragraph.GlyphMappings[fragmentIdx], ' ', currentFont, ref lastGlyphIndex, ref applyKerning); } } else @@ -1090,6 +1120,7 @@ internal static List WrapMultipleTextFragmentsToTextLines(TextPa largestFontCurrentLine = currentFont; CurrentLineLargestFontSize = paragraph.FontSizes[fragmentIdx]; } + spaceWidth = CalcGlyphWidth(paragraph.GlyphMappings[fragmentIdx], ' ', currentFont, ref lastGlyphIndex, ref applyKerning); } fontSize = paragraph.FontSizes[fragmentIdx]; @@ -1102,22 +1133,13 @@ internal static List WrapMultipleTextFragmentsToTextLines(TextPa if (lineWidth > maxWidth) { List tempSingleReturnHolder = new(); - var prevLineWidth = lineWidth; //Add currentfragment to the list - - double leftoverWidth = 0; - if(currentRtFragment.Width != 0) - { - leftoverWidth = currentRtFragment.Width; - //leftover from previously - } - AddWidthToFragment(currentRtFragment, currentFont.HeadTable.UnitsPerEm, fontSize, prevLineWidth, prevFragWidths, fragmentIdx); - currentRtFragment.Width += leftoverWidth; + AddWidthToFragment(currentRtFragment, currentFont.HeadTable.UnitsPerEm, fontSize, lineWidth, prevFragWidths, fragmentIdx); currentLineRtFragments.Add(currentRtFragment); - WrapAtCharPos(allText, i, ref prevLineEndIndex, ref lineWidth, ref wordWidth, advanceWidth, tempSingleReturnHolder); - prevLineWidth -= lineWidth; + WrapAtCharPos(allText, i, ref prevLineEndIndex, ref lineWidth, ref wordWidth, advanceWidth, tempSingleReturnHolder, spaceWidth, out int prevLineWidth); + //lineWidth does not include the width of any skipped spaces at this point causing inaccuracies //Log where wrapping occured in order to keep track of fragment/run/richtext //Can't be part of Log function as there could be pre-existing line-breaks @@ -1181,13 +1203,14 @@ internal static List WrapMultipleTextFragmentsToTextLines(TextPa newStartIdxRt = j; originalWidthOfFragment = currentLineRtFragments[j].Width; - var widthOfFragmentAtBreak = currentTextLine.Width - prevFragWidthsAtBreakInPoints -leftoverWidth; + var widthOfFragmentAtBreak = currentTextLine.Width - prevFragWidthsAtBreakInPoints; currentLineRtFragments[j].Width = widthOfFragmentAtBreak; currentTextLine.RtFragments.Add(currentLineRtFragments[j].Clone()); //Calculate the new width of the fragment in the resulting line currentLineRtFragments[j].Width = originalWidthOfFragment - widthOfFragmentAtBreak; + currentLineRtFragments[j].charStarIdxWithinCurrentLine = 0; //Ensure charStartIdxWithinCurrentLine is accurate for the next line @@ -1206,17 +1229,25 @@ internal static List WrapMultipleTextFragmentsToTextLines(TextPa } } + if(currentLineRtFragments[newStartIdxRt].Fragidx == fragmentIdx) + { + prevFragWidths = 0; + } + else + { + //Remove the part of current fragment that has already been added in font-design units + var factorTarget = ((double)paragraph.FontIndexDict[fragmentIdx].HeadTable.UnitsPerEm) / paragraph.FontSizes[fragmentIdx]; + prevFragWidths = Convert.ToInt16((currentLineRtFragments[newStartIdxRt].Width * factorTarget)); + } + //Remove those that are now irrelevant - for(int j = 0; j < newStartIdxRt; j++) + for (int j = 0; j < newStartIdxRt; j++) { currentLineRtFragments.RemoveAt(0); } //Remove the current fragment as we have not found its end yet currentLineRtFragments.Remove(currentRtFragment); - //Equal to new line width for when the current fragment ends - prevFragWidths = lineWidth; - currentTextLine.Text = tempSingleReturnHolder[0]; outputTextLines.Add(currentTextLine); @@ -1264,13 +1295,9 @@ internal static List WrapMultipleTextFragmentsToTextLines(TextPa AddDataToSimpleLine(currentTextLine, paragraph, currentFont, largestFontCurrentLine, CurrentLineLargestFontSize, lineWidth, paragraph.Fragments.TextFragments.Count() - 1); - double lastLeftoverWidth = 0; - if (currentRtFragment.Width != 0) - { - lastLeftoverWidth = currentRtFragment.Width; - } + AddWidthToFragment(currentRtFragment, currentFont.HeadTable.UnitsPerEm, fontSize, lineWidth, prevFragWidths, currentRtFragment.Fragidx); - currentRtFragment.Width += lastLeftoverWidth; + currentRtFragment.charStarIdxWithinCurrentLine = Math.Max(0, currentRtFragment.OverallParagraphStartCharIdx - prevLineBreakIndex); currentLineRtFragments.Add(currentRtFragment); From 4302a47b23ccdacbd4fa09a5dc5dd22f392e1306 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jan=20K=C3=A4llman?= Date: Tue, 3 Feb 2026 14:58:35 +0100 Subject: [PATCH 036/151] WIP:Added support for stacked line charts --- .../Properties/AssemblyInfo.cs | 2 +- .../Svg/Chart/Axis/AxisOptions.cs | 11 ++ .../Svg/Chart/Axis/AxisScale.cs | 12 ++ .../Svg/Chart/Axis/DateAxisScaleCalculator.cs | 113 +++++++++++++++ .../ValueAxisScaleCalculator.cs | 107 ++++++++------ .../ChartTypeDrawers/LineChartTypeDrawer.cs | 31 +++- .../Svg/Chart/SvgChartAxis.cs | 52 +++++-- .../Drawing/Chart/ExcelChartAxisStandard.cs | 132 +++++++++++++----- src/EPPlus/Utils/DateUtils/DateTimeUtil.cs | 4 + src/EPPlusTest/Properties/AssemblyInfo.cs | 2 +- 10 files changed, 373 insertions(+), 93 deletions(-) create mode 100644 src/EPPlus.Export.ImageRenderer/Svg/Chart/Axis/AxisOptions.cs create mode 100644 src/EPPlus.Export.ImageRenderer/Svg/Chart/Axis/AxisScale.cs create mode 100644 src/EPPlus.Export.ImageRenderer/Svg/Chart/Axis/DateAxisScaleCalculator.cs rename src/EPPlus.Export.ImageRenderer/Svg/Chart/{Util => Axis}/ValueAxisScaleCalculator.cs (58%) diff --git a/src/EPPlus.Export.ImageRenderer/Properties/AssemblyInfo.cs b/src/EPPlus.Export.ImageRenderer/Properties/AssemblyInfo.cs index 8b5a78323..27ba35f44 100644 --- a/src/EPPlus.Export.ImageRenderer/Properties/AssemblyInfo.cs +++ b/src/EPPlus.Export.ImageRenderer/Properties/AssemblyInfo.cs @@ -26,7 +26,7 @@ Date Author Change // Version information for an assembly consists of the following four values: // -// Major Version +// MajorInterval Version // Minor Version // Build Number // Revision diff --git a/src/EPPlus.Export.ImageRenderer/Svg/Chart/Axis/AxisOptions.cs b/src/EPPlus.Export.ImageRenderer/Svg/Chart/Axis/AxisOptions.cs new file mode 100644 index 000000000..a9e2a1f54 --- /dev/null +++ b/src/EPPlus.Export.ImageRenderer/Svg/Chart/Axis/AxisOptions.cs @@ -0,0 +1,11 @@ +using OfficeOpenXml.Drawing.Chart; + +internal class AxisOptions +{ + public double? LockedMin { get; set; } + public double? LockedMax { get; set; } + public double? LockedInterval { get; set; } + public eTimeUnit? LockedIntervalUnit { get; set; } + public bool AddPadding { get; set; } = false; + public ExcelChartAxisStandard Axis { get; internal set; } +} \ No newline at end of file diff --git a/src/EPPlus.Export.ImageRenderer/Svg/Chart/Axis/AxisScale.cs b/src/EPPlus.Export.ImageRenderer/Svg/Chart/Axis/AxisScale.cs new file mode 100644 index 000000000..bec7c0a1c --- /dev/null +++ b/src/EPPlus.Export.ImageRenderer/Svg/Chart/Axis/AxisScale.cs @@ -0,0 +1,12 @@ +using OfficeOpenXml.Drawing.Chart; + +internal class AxisScale +{ + public double Min { get; set; } + public double Max { get; set; } + public double MajorInterval { get; set; } + public double MinorInterval { get; set; } + public int TickCount { get; set; } + public eTimeUnit? MajorDateUnit { get; set; } + public eTimeUnit? MinorDateUnit { get; set; } +} \ No newline at end of file diff --git a/src/EPPlus.Export.ImageRenderer/Svg/Chart/Axis/DateAxisScaleCalculator.cs b/src/EPPlus.Export.ImageRenderer/Svg/Chart/Axis/DateAxisScaleCalculator.cs new file mode 100644 index 000000000..f49adda98 --- /dev/null +++ b/src/EPPlus.Export.ImageRenderer/Svg/Chart/Axis/DateAxisScaleCalculator.cs @@ -0,0 +1,113 @@ +using OfficeOpenXml.Drawing.Chart; +using OfficeOpenXml.Utils.DateUtils; +using System; +using System.Collections.Generic; +using System.Linq; + +namespace EPPlus.Export.ImageRenderer.Svg.Chart.Util +{ + internal class DateAxisScaleCalculator + { + + + /// + /// Calculate automatic axis settings for date data + /// + internal static AxisScale Calculate(double dataMin, double dataMax, double chartHeightPixels, AxisOptions axisOptions = null) + { + var range = dataMax - dataMin; + + // Calculate major majorUnit based on range + CalculateMajorUnit(dataMin, dataMax, out double majorValue, out double minorUnit, out eTimeUnit majorUnit); + + return new AxisScale + { + Min = dataMin, + Max = dataMax, + MajorInterval = majorValue, + MajorDateUnit = majorUnit, + MinorDateUnit = majorUnit + }; + } + + /// + /// Calculate major majorUnit based on data range (in days) + /// + private static void CalculateMajorUnit(double min, double max,out double majorValue, out double minorUnits, out eTimeUnit majorUnit) + { + var dtMin = DateTime.FromOADate(ExcelNormalizeOADate(min)); + var dtMax = DateTime.FromOADate(ExcelNormalizeOADate(max)); + + var rangeDays = (dtMax - dtMin).TotalDays; + // Target approximately 6 intervals + const int targetIntervals = 6; + var roughUnit = rangeDays / targetIntervals; + + // Return nice round numbers based on the rough majorUnit + if (rangeDays <= 1) + { + majorValue = 1; + majorUnit = eTimeUnit.Days; + } + else if (roughUnit < 7) + { + // Multiple days - round to nearest day + majorValue = Math.Max(1, Math.Round(roughUnit)); + majorUnit = eTimeUnit.Days; + } + else if (roughUnit < 14) + { + majorValue = 7; + majorUnit = eTimeUnit.Days; + } + else if (roughUnit < 21) + { + majorValue = 14; + majorUnit = eTimeUnit.Days; + } + else if (roughUnit < 60) + { + majorValue = 1; + majorUnit = eTimeUnit.Months; + } + else if (roughUnit < 120) + { + majorValue = 2; + majorUnit = eTimeUnit.Months; + } + else if (roughUnit < 180) + { + majorValue = 3; + majorUnit = eTimeUnit.Months; + } + else if (roughUnit < 365) + { + majorValue = 6; + majorUnit = eTimeUnit.Months; + } + else if (roughUnit < 730) + { + majorValue = 1; + majorUnit = eTimeUnit.Years; + } + else if (roughUnit < 1825) + { + // Multiple years + majorValue = Math.Ceiling(roughUnit / 365); + majorUnit = eTimeUnit.Years; + } + else + { + // 5-year increments for very long ranges + majorValue = Math.Ceiling(roughUnit / 365 / 5) * 5; + majorUnit = eTimeUnit.Years; + } + minorUnits = 1; + } + + private static double ExcelNormalizeOADate(double value) + { + return Math.Min(Math.Max(value, 0), 2958465.99999999); + } + } +} \ No newline at end of file diff --git a/src/EPPlus.Export.ImageRenderer/Svg/Chart/Util/ValueAxisScaleCalculator.cs b/src/EPPlus.Export.ImageRenderer/Svg/Chart/Axis/ValueAxisScaleCalculator.cs similarity index 58% rename from src/EPPlus.Export.ImageRenderer/Svg/Chart/Util/ValueAxisScaleCalculator.cs rename to src/EPPlus.Export.ImageRenderer/Svg/Chart/Axis/ValueAxisScaleCalculator.cs index 2a6cb67e4..cfb04f3ad 100644 --- a/src/EPPlus.Export.ImageRenderer/Svg/Chart/Util/ValueAxisScaleCalculator.cs +++ b/src/EPPlus.Export.ImageRenderer/Svg/Chart/Axis/ValueAxisScaleCalculator.cs @@ -1,30 +1,22 @@ -using System; +using Microsoft.VisualBasic; +using OfficeOpenXml.Drawing.Chart; +using System; using System.Collections.Generic; internal class ValueAxisScaleCalculator { - internal class AxisOptions - { - public double? LockedMin { get; set; } - public double? LockedMax { get; set; } - public double? LockedInterval { get; set; } - public bool AddPadding { get; set; } = false; - } - internal class AxisScale + internal static AxisScale Calculate(double dataMin, double dataMax, double chartHeightPixels, AxisOptions axisOptions = null) { - public double Min { get; set; } - public double Max { get; set; } - public double Interval { get; set; } - public int TickCount { get; set; } + return GetNumberScale(ref dataMin, ref dataMax, chartHeightPixels, ref axisOptions); } - internal static AxisScale Calculate(double dataMin, double dataMax, double chartHeightPixels, AxisOptions axisOptions = null) + private static AxisScale GetNumberScale(ref double dataMin, ref double dataMax, double chartHeightPixels, ref AxisOptions axisOptions) { - int desiredTicks = chartHeightPixels < 200 ? 4 : - chartHeightPixels < 400 ? 5 : 6; + var desiredTicks = chartHeightPixels < 200 ? 4 : + chartHeightPixels < 400 ? 5 : 6; if (axisOptions == null) - { + { axisOptions = new AxisOptions(); } @@ -55,7 +47,7 @@ internal static AxisScale Calculate(double dataMin, double dataMax, double chart if (axisOptions.AddPadding) { dataMin = Math.Floor(dataMin * 0.1 / interval) * interval; - if(isAllPositive && dataMin < 0 ) + if (isAllPositive && dataMin < 0) { dataMin = 0; } @@ -69,7 +61,7 @@ internal static AxisScale Calculate(double dataMin, double dataMax, double chart if (!axisOptions.LockedMax.HasValue) { dataMax = Math.Ceiling(dataMax * 0.1 / interval) * interval; - } + } else { if (axisOptions.AddPadding) @@ -83,11 +75,11 @@ internal static AxisScale Calculate(double dataMin, double dataMax, double chart } int tickCount = (int)Math.Round((dataMax - dataMin) / Math.Min(desiredTicks, interval)) + 1; - return new AxisScale + return new AxisScale { Min = dataMin, Max = dataMax, - Interval = interval, + MajorInterval = interval, TickCount = tickCount }; } @@ -104,36 +96,67 @@ internal static AxisScale Calculate(double dataMin, double dataMax, double chart double dataRange = dataMax - dataMin; - // Add padding (10%) - double paddedMin = dataMin - (dataRange * 0.1); - double paddedMax = dataMax + (dataRange * 0.1); + double axisMin; + double axisMax; + double roughInterval; + double scaleInterval; - //Normalize to zero if all data is positive or negative - if (paddedMin < 0 && isAllPositive) + if (axisOptions.AddPadding) { - paddedMin = 0; + // Add padding (10%) + if (axisOptions.LockedMin.HasValue) + { + axisMin = axisOptions.LockedMin.Value; + } + else + { + axisMin = dataMin - (dataRange * 0.1); + //Normalize to zero if all data is positive or negative + if (axisMin < 0 && isAllPositive) + { + axisMin = 0; + } + } + + if (axisOptions.LockedMax.HasValue) + { + axisMax = axisOptions.LockedMax.Value; + } + else + { + axisMax = dataMax + (dataRange * 0.1); + + if (axisMax > 0 && isAllNegativ) + { + axisMax = 0; + } + } + + // Calculate interval + roughInterval = (axisMax - axisMin) / desiredTicks; + scaleInterval = GetScaleNumber(roughInterval, true); + + if (!axisOptions.LockedMin.HasValue) axisMin = Math.Floor(axisMin / scaleInterval) * scaleInterval; + if (!axisOptions.LockedMax.HasValue) axisMax = Math.Ceiling(axisMax / scaleInterval) * scaleInterval; } - if (paddedMax > 0 && isAllNegativ) + else { - paddedMax = 0; - } + // Calculate interval + roughInterval = (dataMax - dataMin) / desiredTicks; + scaleInterval = GetScaleNumber(roughInterval, true); - // Calculate nice interval - double roughInterval = (paddedMax - paddedMin) / desiredTicks; - double scaleInterval = GetScaleNumber(roughInterval, true); - - // Calculate false min and max - double axisMin = Math.Floor(paddedMin / scaleInterval) * scaleInterval; - double axisMax = Math.Ceiling(paddedMax / scaleInterval) * scaleInterval; + axisMin = dataMin; + axisMax = dataMax; + } - // Calculate actual tick count - int tickCount = (int)Math.Round((axisMax - axisMin) / scaleInterval) + 1; + var tickCount = (int)Math.Round((axisMax - axisMin) / scaleInterval) + 1; return new AxisScale { Min = axisOptions.LockedMin ?? axisMin, Max = axisOptions.LockedMax ?? axisMax, - Interval = scaleInterval, - TickCount = tickCount + MajorInterval = scaleInterval, + MinorInterval = scaleInterval / 5, + TickCount = tickCount, }; } } @@ -167,7 +190,7 @@ public static List GetTickValues(AxisScale scale) var ticks = new List(); for (int i = 0; i < scale.TickCount; i++) { - double tickValue = scale.Min + (i * scale.Interval); + double tickValue = scale.Min + (i * scale.MajorInterval); ticks.Add(Math.Round(tickValue, 10)); // Round to avoid floating point errors } return ticks; diff --git a/src/EPPlus.Export.ImageRenderer/Svg/Chart/ChartTypeDrawers/LineChartTypeDrawer.cs b/src/EPPlus.Export.ImageRenderer/Svg/Chart/ChartTypeDrawers/LineChartTypeDrawer.cs index 241b65957..1f5fef66a 100644 --- a/src/EPPlus.Export.ImageRenderer/Svg/Chart/ChartTypeDrawers/LineChartTypeDrawer.cs +++ b/src/EPPlus.Export.ImageRenderer/Svg/Chart/ChartTypeDrawers/LineChartTypeDrawer.cs @@ -1,7 +1,11 @@ using EPPlusImageRenderer.RenderItems; using EPPlusImageRenderer.Svg; using OfficeOpenXml.Drawing.Chart; +using OfficeOpenXml.Utils.TypeConversion; +using System; +using System.Collections; using System.Collections.Generic; +using System.Linq; namespace EPPlus.Export.ImageRenderer.Svg.Chart { @@ -11,15 +15,38 @@ internal LineChartTypeDrawer(SvgChart svgChart, ExcelChart chartType) : base(svg { var groupItem = new SvgGroupItem(ChartRenderer, _svgChart.Plotarea.Rectangle.Bounds); RenderItems.Add(groupItem); + var isStacked = Chart.IsTypeStacked(); + List pXValues = null, pYValues = null; foreach (ExcelLineChartSerie serie in chartType.Series) { var yValues = LoadSeriesValues(serie.Series, serie.NumberLiteralsY, serie.StringLiteralsY); var xValues = LoadSeriesValues(serie.XSeries, serie.NumberLiteralsX, serie.StringLiteralsX); - AddLine(chartType, serie, xValues, yValues); + if(isStacked) + { + AddLine(chartType, serie, xValues, SumLists(yValues, pYValues)); + } + else + { + AddLine(chartType, serie, xValues, yValues); + } + + if (isStacked) + { + pYValues = yValues; + } } RenderItems.Add(new SvgEndGroupItem(ChartRenderer, null)); } - + private List SumLists(List l1, List l2) + { + if (l2 == null) return l1; + var sumList = new List(l1.Count); + for (int i = 0; i < Math.Min(l1.Count, l2.Count); i++) + { + sumList.Add(ConvertUtil.GetValueDouble(l1[i]) + ConvertUtil.GetValueDouble(l2[i])); + } + return sumList; + } private void AddLine(ExcelChart chartType, ExcelLineChartSerie serie, List xValues, List yValues) { var xAxis = _svgChart.HorizontalAxis; diff --git a/src/EPPlus.Export.ImageRenderer/Svg/Chart/SvgChartAxis.cs b/src/EPPlus.Export.ImageRenderer/Svg/Chart/SvgChartAxis.cs index b14c818d9..2950b79a2 100644 --- a/src/EPPlus.Export.ImageRenderer/Svg/Chart/SvgChartAxis.cs +++ b/src/EPPlus.Export.ImageRenderer/Svg/Chart/SvgChartAxis.cs @@ -12,6 +12,7 @@ Date Author Change *************************************************************************************************/ using EPPlus.Export.ImageRenderer.RenderItems.Shared; using EPPlus.Export.ImageRenderer.RenderItems.SvgItem; +using EPPlus.Export.ImageRenderer.Svg.Chart.Util; using EPPlus.Fonts.OpenType; using EPPlus.Fonts.OpenType.Tables.Cmap; using EPPlus.Fonts.OpenType.Utils; @@ -542,7 +543,7 @@ internal double GetPositionInPlotarea(object val) var dVal = ConvertUtil.GetValueDouble(val, false, true); if (dVal < Min || dVal > Max) return double.NaN; var diff = Max - Min; - return (/*SvgChart.Plotarea.Rectangle.Left + */((Max - dVal) / diff * SvgChart.Plotarea.Rectangle.Width)); + return (((dVal-Min) / diff * SvgChart.Plotarea.Rectangle.Width)); } } } @@ -576,24 +577,55 @@ protected List GetAxisValue(ExcelChartAxisStandard ax, RenderItem rect, max = d; } } - var options = new ValueAxisScaleCalculator.AxisOptions + var options = new AxisOptions { LockedMin = ax.MinValue, LockedMax = ax.MaxValue, LockedInterval = ax.MajorUnit, - AddPadding = ax.AxisPosition == eAxisPosition.Left || ax.AxisPosition == eAxisPosition.Right + LockedIntervalUnit = ax.MajorTimeUnit, + AddPadding = ax.AxisPosition == eAxisPosition.Left || ax.AxisPosition == eAxisPosition.Right, + Axis = ax }; var length = ax.AxisPosition == eAxisPosition.Left || ax.AxisPosition == eAxisPosition.Right ? SvgChart.Bounds.Height : SvgChart.Bounds.Width; //Fix and use plotarea width/height. - var res = ValueAxisScaleCalculator.Calculate(min ?? 0, max ?? 0, length, options); - for ( var v = res.Min; v <= res.Max; v += res.Interval) + if (ax.IsDate) { - l.Add(v); + var res = DateAxisScaleCalculator.Calculate(min ?? 0, max ?? 0, length, options); + var dt = DateTime.FromOADate(res.Min); + var maxDt = DateTime.FromOADate(res.Max); + while (dt < maxDt) + { + l.Add(dt); + switch(res.MajorDateUnit ?? eTimeUnit.Days) + { + case eTimeUnit.Years: + dt = dt.AddYears((int)res.MajorInterval); + break; + case eTimeUnit.Months: + dt = dt.AddMonths((int)res.MajorInterval); + break; + case eTimeUnit.Days: + dt = dt.AddDays((int)res.MajorInterval); + break; + } + } + + min = res.Min; + max = res.Max; + majorUnit = res.MajorInterval; + } + else + { + var res = ValueAxisScaleCalculator.Calculate(min ?? 0, max ?? 0, length, options); + for (var v = res.Min; v <= res.Max; v += res.MajorInterval) + { + l.Add(v); + } + + min = res.Min; + max = res.Max; + majorUnit = res.MajorInterval; } - min = res.Min; - max = res.Max; - majorUnit = res.Interval; - return l; } diff --git a/src/EPPlus/Drawing/Chart/ExcelChartAxisStandard.cs b/src/EPPlus/Drawing/Chart/ExcelChartAxisStandard.cs index 91e83a84d..7059b3cf0 100644 --- a/src/EPPlus/Drawing/Chart/ExcelChartAxisStandard.cs +++ b/src/EPPlus/Drawing/Chart/ExcelChartAxisStandard.cs @@ -10,14 +10,19 @@ Date Author Change ************************************************************************************************* 04/22/2020 EPPlus Software AB Added this class *************************************************************************************************/ +using OfficeOpenXml.FormulaParsing.Excel.Functions.Finance; +using OfficeOpenXml.FormulaParsing.Excel.Functions.Information; using OfficeOpenXml.FormulaParsing.Excel.Functions.RefAndLookup; +using OfficeOpenXml.Style.XmlAccess; using OfficeOpenXml.Utils.EnumUtils; +using OfficeOpenXml.Utils.TypeConversion; using System; using System.Collections; using System.Collections.Generic; using System.Globalization; using System.Linq; using System.Xml; +using static OfficeOpenXml.Style.XmlAccess.ExcelNumberFormatXml; namespace OfficeOpenXml.Drawing.Chart { /// @@ -53,7 +58,7 @@ internal ExcelChartAxisStandard(ExcelChart chart, XmlNamespaceManager nameSpaceM { AddSchemaNodeOrder(new string[] { "axId", "scaling", "delete", "axPos", "majorGridlines", "minorGridlines", "title", "numFmt", "majorTickMark", "minorTickMark", "tickLblPos", "spPr", "txPr", "crossAx", "crosses", "crossesAt", "crossBetween", "auto", "lblOffset", "baseTimeUnit", "majorUnit", "majorTimeUnit", "minorUnit", "minorTimeUnit", "tickLblSkip", "tickMarkSkip", "dispUnits", "noMultiLvlLbl", "logBase", "orientation", "max", "min" }, ExcelDrawing._schemaNodeOrderSpPr); - + Index = index; } internal override string Id @@ -68,7 +73,7 @@ internal override string Id /// /// Get or Sets the major tick marks for the axis. /// - public override eAxisTickMark MajorTickMark + public override eAxisTickMark MajorTickMark { get { @@ -592,7 +597,7 @@ public override double? LogBase { if (value == null) { - DeleteNode(_logbasePath,true); + DeleteNode(_logbasePath, true); } else { @@ -689,25 +694,47 @@ public ExcelLayout Layout /// The index for the axis. /// public int Index { get; private set; } + internal bool IsDate + { + get + { + if (AxisType == eAxisType.Date) + { + return true; + } + else if (AxisType != eAxisType.Cat) + { + var nf = new ExcelFormatTranslator(Format, 0); + if (nf.DataType == eFormatType.DateTime) + { + return true; + } + } + return false; + } + } internal override object[] GetAxisValues(out bool isCount) - { - var hs = new HashSet(); + { + var values = new List>(); isCount = false; + List pl = new List(); foreach (var ct in _chart.PlotArea.ChartTypes) { foreach (var serie in _chart.Series) { - if(AxisType==eAxisType.Cat) + var l = new List(); + values.Add(l); + if (AxisType == eAxisType.Cat) { - if(string.IsNullOrEmpty(serie.XSeries) && (serie.NumberLiteralsX == null || serie.NumberLiteralsX.Length==0) && (serie.StringLiteralsX==null || serie.StringLiteralsX?.Length==0)) + if (string.IsNullOrEmpty(serie.XSeries) && (serie.NumberLiteralsX == null || serie.NumberLiteralsX.Length == 0) && (serie.StringLiteralsX == null || serie.StringLiteralsX?.Length == 0)) { - AddCountFromSeries(hs, serie.Series, serie.NumberLiteralsY, serie.StringLiteralsY); + AddCountFromSeries(l, serie.Series, serie.NumberLiteralsY, serie.StringLiteralsY); isCount = true; } else { - AddFromSerie(hs, serie.XSeries, serie.NumberLiteralsX, serie.StringLiteralsX, true); + AddFromSerie(l, serie.XSeries, serie.NumberLiteralsX, serie.StringLiteralsX, true); } } else @@ -715,30 +742,31 @@ internal override object[] GetAxisValues(out bool isCount) //if ((Index == 1 && !ct.UseSecondaryAxis) || (Index == 2 && ct.UseSecondaryAxis)) if (ct.YAxis == this) { - AddFromSerie(hs, serie.Series, serie.NumberLiteralsY, serie.StringLiteralsY, false); + AddFromSerie(l, serie.Series, serie.NumberLiteralsY, serie.StringLiteralsY, false, pl); } - else if(ct.XAxis == this) + else if (ct.XAxis == this) { - AddFromSerie(hs, serie.XSeries, serie.NumberLiteralsX, serie.StringLiteralsX, false); - } + AddFromSerie(l, serie.XSeries, serie.NumberLiteralsX, serie.StringLiteralsX, false); + } } - } + pl = l; + } } - var values = hs.ToList(); - values.Sort(); - if(Orientation==eAxisOrientation.MaxMin) + var dl = values.SelectMany(x => x).Distinct().ToList(); + dl.Sort(); + if (Orientation == eAxisOrientation.MaxMin) { - values.Reverse(); + dl.Reverse(); } - return values.ToArray(); + return dl.ToArray(); } - private void AddCountFromSeries(HashSet hs, string address, double[] numberLiterals, string[] stringLiterals) + private void AddCountFromSeries(List l, string address, double[] numberLiterals, string[] stringLiterals) { - hs.Add(1); + l.Add(1); if (numberLiterals?.Length > 0) { - hs.Add(numberLiterals.Length); + l.Add(numberLiterals.Length); //for(int i=1;i<= numberLiterals?.Length;i++) //{ // hs.Add(i); @@ -746,7 +774,7 @@ private void AddCountFromSeries(HashSet hs, string address, double[] num } else if (stringLiterals?.Length > 0) { - hs.Add(stringLiterals.Length); + l.Add(stringLiterals.Length); //for (int i = 1; i <= stringLiterals?.Length; i++) //{ // hs.Add(i); @@ -755,7 +783,7 @@ private void AddCountFromSeries(HashSet hs, string address, double[] num else { var a = new ExcelAddressBase(address); - hs.Add(Math.Max(a.Rows, a.Columns)); + l.Add(Math.Max(a.Rows, a.Columns)); //var ws = _chart.WorkSheet.Workbook.Worksheets[a.WorkSheetName]; //int i = 1; //if (ws != null) @@ -768,41 +796,71 @@ private void AddCountFromSeries(HashSet hs, string address, double[] num } } - private void AddFromSerie(HashSet hs, string address, double[] numberLiterals, string[] stringLiterals, bool useText) + private void AddFromSerie(List list, string address, double[] numberLiterals, string[] stringLiterals, bool useText, List prevList=null) { + var isStacked = _chart.IsTypeStacked() && prevList != null; if (numberLiterals?.Length > 0) - { - foreach (var n in numberLiterals) + { + for (int i = 0; i < numberLiterals.Length; i++) { - hs.Add(n); + var n = numberLiterals[i]; + if (isStacked) + { + n += GetSerieValue(prevList, i); + } + list.Add(n); } } else if (stringLiterals?.Length > 0) { foreach (var s in stringLiterals) { - hs.Add(s); + list.Add(s); } } else { var a = new ExcelAddressBase(address); var ws = _chart.WorkSheet.Workbook.Worksheets[a.WorkSheetName]; - if(ws != null) + if (ws != null) { - foreach(var c in ws.Cells[a.Address]) + var range = ws.Cells[a.Address]; + var i = 0; + for(var r=0;r prevList, int i) + { + if (prevList.Count > i && ConvertUtil.IsExcelNumeric(prevList[i])) + { + var v=ConvertUtil.GetValueDouble(prevList[i], true, true); + if (double.IsNaN(v)) return 0d; + return v; + } + return 0d; + } } } diff --git a/src/EPPlus/Utils/DateUtils/DateTimeUtil.cs b/src/EPPlus/Utils/DateUtils/DateTimeUtil.cs index 26444a5e3..7028fe1ab 100644 --- a/src/EPPlus/Utils/DateUtils/DateTimeUtil.cs +++ b/src/EPPlus/Utils/DateUtils/DateTimeUtil.cs @@ -35,5 +35,9 @@ internal static void GetWeekDates(DateTime date, out DateTime startDate, out Dat startDate = date; endDate = date.AddDays(6); } + internal static bool IsValidOADate(double date) + { + return date >= -657435 && date < 2958466; + } } } diff --git a/src/EPPlusTest/Properties/AssemblyInfo.cs b/src/EPPlusTest/Properties/AssemblyInfo.cs index cd6ac2673..c0ce1dced 100644 --- a/src/EPPlusTest/Properties/AssemblyInfo.cs +++ b/src/EPPlusTest/Properties/AssemblyInfo.cs @@ -53,7 +53,7 @@ Date Author Change //// Version information for an assembly consists of the following four values: //// -//// Major Version +//// MajorInterval Version //// Minor Version //// Build Number //// Revision From fecba8559a60f123d5d93e980810c86ba3efbcdc Mon Sep 17 00:00:00 2001 From: AdrianEPPlus <162118292+AdrianEPPlus@users.noreply.github.com> Date: Wed, 4 Feb 2026 07:45:57 +0100 Subject: [PATCH 037/151] =?UTF-8?q?Removed=20check=20for=20double=20negati?= =?UTF-8?q?ve=20sign=20in=20custom=20format.=20Replaced=20by=20=E2=80=A6?= =?UTF-8?q?=20(#2270)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Removed check for double negative sign in custom format. Replaced by Math.Abs when formatting the string. * Added more tests * added comment * Reverted to using regexp * Added some additional checks for custom format. --- src/EPPlus/Utils/String/ValueToTextHandler.cs | 15 ++++++++---- src/EPPlusTest/Issues/WorksheetIssues.cs | 23 ++++++++++++++++++- 2 files changed, 32 insertions(+), 6 deletions(-) diff --git a/src/EPPlus/Utils/String/ValueToTextHandler.cs b/src/EPPlus/Utils/String/ValueToTextHandler.cs index 3325a47d7..6e4d47ed4 100644 --- a/src/EPPlus/Utils/String/ValueToTextHandler.cs +++ b/src/EPPlus/Utils/String/ValueToTextHandler.cs @@ -76,7 +76,7 @@ internal static string FormatValue(object v, bool forWidthCalc, ExcelFormatTrans if (v == null) v = 0; var f = nf.GetFormatPart(v); isValidFormat = f.IsValid; - if(isValidFormat == false) + if (isValidFormat == false) { return null; } @@ -117,7 +117,7 @@ internal static string FormatValue(object v, bool forWidthCalc, ExcelFormatTrans } else { - return FormatNumber(d, format, overrideCultureInfo ?? nf.Culture); + return FormatNumber(d, format, overrideCultureInfo ?? nf.Culture, nf.Formats.Count > 1); } } else @@ -215,13 +215,13 @@ internal static string FormatValue(object v, bool forWidthCalc, ExcelFormatTrans return v.ToString(); } - private static string FormatNumber(double d, string format, CultureInfo cultureInfo) + private static string FormatNumber(double d, string format, CultureInfo cultureInfo, bool isNegativeFormat) { var s = FormatNumberExcel(d, format, cultureInfo); var ns = cultureInfo?.NumberFormat?.NegativeSign ?? "-"; if (string.IsNullOrEmpty(s) == false && d < 0) { - return CheckAndRemoveNegativeSign(format, s, ns); + return CheckAndRemoveNegativeSign(format, s, ns, isNegativeFormat); } else { @@ -229,8 +229,9 @@ private static string FormatNumber(double d, string format, CultureInfo cultureI } } - private static string CheckAndRemoveNegativeSign(string format, string s, string ns) + private static string CheckAndRemoveNegativeSign(string format, string s, string ns, bool isNegativeFormat) { + //We should not use regexp, but it will do for now. //This regex pattern ^(\[[^\]]+\]|[\\'\""\*_])* removes: // \[[^\]]+\] : This part removes Anything inside square brackets // [\\'\""\*_] : This part removes Backslashes, quotes, asterisks and underlines @@ -244,6 +245,10 @@ private static string CheckAndRemoveNegativeSign(string format, string s, string { return s.Substring(1); } + else if ((s.StartsWith($"{ns}") || s.StartsWith($"-")) && !formatStartsWithNegative && isNegativeFormat) + { + return s.Remove(0, 1); + } else { return s; diff --git a/src/EPPlusTest/Issues/WorksheetIssues.cs b/src/EPPlusTest/Issues/WorksheetIssues.cs index 9ab79e640..a7cd11ea6 100644 --- a/src/EPPlusTest/Issues/WorksheetIssues.cs +++ b/src/EPPlusTest/Issues/WorksheetIssues.cs @@ -6,6 +6,7 @@ using OfficeOpenXml.FormulaParsing; using OfficeOpenXml.FormulaParsing.Excel.Functions.Logical; using OfficeOpenXml.FormulaParsing.Excel.Functions.MathFunctions; +using OfficeOpenXml.RichData; using OfficeOpenXml.SystemDrawing.Image; using OfficeOpenXml.SystemDrawing.Text; using System; @@ -1085,10 +1086,30 @@ public void i2258() { var p = OpenTemplatePackage("repro2.xlsx"); var ws = p.Workbook.Worksheets.First(); + var a1 = ws.Cells["A1"].Text; - var b1 = ws.Cells["B1"].Text; Assert.AreEqual("-/- 1 000", a1); + + var b1 = ws.Cells["B1"].Text; Assert.AreEqual("-/- 2 000", b1); + + var c1 = ws.Cells["C1"].Text; + Assert.AreEqual("3 000,00", c1); + + var d1 = ws.Cells["D1"].Text; + Assert.AreEqual("(4000,000)", d1); + + var a3 = ws.Cells["A3"].Text; + Assert.AreEqual("1000,000", a3); + + var b3 = ws.Cells["B3"].Text; + Assert.AreEqual("(2000,000)", b3); + + var c3 = ws.Cells["C3"].Text; + Assert.AreEqual("-3000,000", c3); + + var d3 = ws.Cells["D3"].Text; + Assert.AreEqual("Negative 4000,000", d3); } } From d9a169c3214e2c28ab3398ad47255d9e39160c73 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jan=20K=C3=A4llman?= Date: Wed, 4 Feb 2026 08:46:23 +0100 Subject: [PATCH 038/151] EPPlus version 8.4.2 --- appveyor8.yml | 10 +++++----- docs/articles/fixedissues.md | 7 +++++++ src/EPPlus/EPPlus.csproj | 18 +++++++++++------- 3 files changed, 23 insertions(+), 12 deletions(-) diff --git a/appveyor8.yml b/appveyor8.yml index b2483da3d..b264d4fa3 100644 --- a/appveyor8.yml +++ b/appveyor8.yml @@ -1,4 +1,4 @@ -version: 8.4.1.{build} +version: 8.4.2.{build} branches: only: - develop8 @@ -10,15 +10,15 @@ install: & $env:temp\dotnet-install.ps1 -Architecture x64 -Version '10.0.100' -InstallDir "$env:ProgramFiles\dotnet" init: - ps: >- - Update-AppveyorBuild -Version "8.4.1.$env:appveyor_build_number-$(Get-Date -format yyyyMMdd)-$env:appveyor_repo_branch" + Update-AppveyorBuild -Version "8.4.2.$env:appveyor_build_number-$(Get-Date -format yyyyMMdd)-$env:appveyor_repo_branch" - Write-Host "8.4.1.$env:appveyor_build_number-$(Get-Date -format yyyyMMdd)-$env:appveyor_repo_branch" + Write-Host "8.4.2.$env:appveyor_build_number-$(Get-Date -format yyyyMMdd)-$env:appveyor_repo_branch" dotnet_csproj: patch: true file: '**\*.csproj' version: '{version}' - assembly_version: 8.4.1.{build} - file_version: 8.4.1.{build} + assembly_version: 8.4.2.{build} + file_version: 8.4.2.{build} nuget: project_feed: true before_build: diff --git a/docs/articles/fixedissues.md b/docs/articles/fixedissues.md index 74ec7c6e4..40ab5376a 100644 --- a/docs/articles/fixedissues.md +++ b/docs/articles/fixedissues.md @@ -1,4 +1,11 @@ # Features / Fixed issues - EPPlus 8 +## Version 8.4.2 +* Fixed an issue where the ExcelRange.Text property could format negative values with double minus signs in rare cases. +* Fixed a performance issue when using FollowDependencyChain = false in the Calculate method. With this option, EPPlus no longer calculates dependent cells or dynamic array formula spills. +* Fixed an issue where the ExcelPowerQueryMetadataItem class parsed dates using the supplied culture instead of the invariant culture, which could cause an unhandled exception. +* Fixed an issue where a workbook could become corrupt when EPPlus adjusted the XML ignorable namespace list. +* Fixed an unhandled exception that occurred when saving a workbook containing a customXml property file without a schemaRefs file. + ## Version 8.4.1 * Fixes several issues related to expanding (spilling) and contracting dynamic array formulas. * Changing cell dependencies were not taken into account when recalculating dirty cells from dynamic array formulas. diff --git a/src/EPPlus/EPPlus.csproj b/src/EPPlus/EPPlus.csproj index d95dbc30c..dc1f13801 100644 --- a/src/EPPlus/EPPlus.csproj +++ b/src/EPPlus/EPPlus.csproj @@ -1,9 +1,9 @@  net8.0;net9.0;net10.0;netstandard2.1;netstandard2.0;net462;net35 - 8.4.1.0 - 8.4.1.0 - 8.4.1 + 8.4.2.0 + 8.4.2.0 + 8.4.2 true https://epplussoftware.com EPPlus Software AB @@ -18,7 +18,7 @@ readme.md EPPlus Software AB - EPPlus 8.4.1 + EPPlus 8.4.2 IMPORTANT NOTICE! From version 5 EPPlus changes the license model using a dual license, Polyform Non Commercial / Commercial license. @@ -26,9 +26,12 @@ Commercial licenses can be purchased from https://epplussoftware.com This applies to EPPlus version 5 and later. Earlier versions are still licensed LGPL. - ## Version 8.4.1 + ## Version 8.4.2 * Minor bug fixes. See https://epplussoftware.com/Developers/MinorFeaturesAndIssues - + + ## Version 8.4.1 + * Minor bug fixes. + ## Version 8.4.0 * Added target framework .NET 10. Minor bug fixes. @@ -548,10 +551,11 @@ A list of fixed issues can be found here https://epplussoftware.com/docs/7.0/articles/fixedissues.html Version history + 8.4.2 20260204 Minor bug fixes. See https://epplussoftware.com/Developers/MinorFeaturesAndIssues 8.4.1 20260112 Minor bug fixes. 8.4.0 20251212 Updated target frameworks. Minor bug fixes. 8.3.1 20251128 Minor bug fixes. - 8.3.0 20251120 Connections and query tables support. Minor features and bug fixes. See https://epplussoftware.com/Developers/MinorFeaturesAndIssues + 8.3.0 20251120 Connections and query tables support. Minor features and bug fixes. 8.2.1 20251012 Minor bug fixes. 8.2.0 20250924 LAMBDA functions support. Minor bug fixes. 8.1.1 20250909 Minor bug fixes. From aa2a7d019e1eb981fb452f3d953258ac045ffdf2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ossian=20Edstr=C3=B6m?= Date: Wed, 4 Feb 2026 14:20:22 +0100 Subject: [PATCH 039/151] Added functional implementation of new system --- .../TextRenderTests.cs | 46 +-- .../RenderItems/RenderItem.cs | 2 +- .../RenderItems/Shared/ParagraphItem.cs | 248 +++++++++----- .../RenderItems/Shared/TextBodyItem.cs | 68 ++-- .../RenderItems/Shared/TextRunItem.cs | 310 +++++++++--------- .../RenderItems/SvgItem/SvgParagraphItem.cs | 2 +- .../RenderItems/SvgItem/SvgTextBoxItem.cs | 6 +- .../RenderItems/SvgItem/SvgTextRunItem.cs | 131 +++++--- .../Svg/SvgShape.cs | 2 +- .../TextFragmentCollectionTests.cs | 119 ++++++- .../DataHolders/RichTextFragmentSimple.cs | 8 +- .../DataHolders/TextLineSimple.cs | 49 ++- .../TrueTypeMeasurer/TextData.cs | 93 ++++-- 13 files changed, 685 insertions(+), 399 deletions(-) diff --git a/src/EPPlus.Export.ImageRenderer.Test/TextRenderTests.cs b/src/EPPlus.Export.ImageRenderer.Test/TextRenderTests.cs index 00ecf0ad7..34a5d626a 100644 --- a/src/EPPlus.Export.ImageRenderer.Test/TextRenderTests.cs +++ b/src/EPPlus.Export.ImageRenderer.Test/TextRenderTests.cs @@ -159,7 +159,7 @@ public void MeasureWrappedWidths() var wrappedLines = ttMeasurer.WrapMultipleTextFragmentsToTextLines(textFragments, fonts, maxSizePoints); - Assert.AreEqual(wrappedLines[0].r) + //Assert.AreEqual(wrappedLines[0].r) //Line 1 45 px 34.5pt @@ -225,34 +225,34 @@ public void VerifyTextRunBounds() var svgShape = new SvgShape(cube); SvgTextBodyItem tbItem = new SvgTextBodyItem(svgShape, parentBB); - //Verify new method works at all - for(int i = 1; i< cube.TextBody.Paragraphs.Count; i++) - { - var fragments = GenerateTextFragments(cube.TextBody.Paragraphs[i].TextRuns); - - var wrappedText = GetWrappedText(cube.TextBody.Paragraphs[i].TextRuns, fragments); - if(i == 0) - { - Assert.AreEqual("TextBox", wrappedText[0].Text); - Assert.AreEqual("a", wrappedText[1].Text); - } - if(i == 1) - { - Assert.AreEqual("TextBox2ra underlineLa", wrappedText[0].Text); - Assert.AreEqual("StrikeGoudy size", wrappedText[1].Text); - Assert.AreEqual("16SvgSize 24", wrappedText[2].Text); - } - } + ////Verify new method works at all + //for(int i = 1; i< cube.TextBody.Paragraphs.Count; i++) + //{ + // var fragments = GenerateTextFragments(cube.TextBody.Paragraphs[i].TextRuns); + + // var wrappedText = GetWrappedText(cube.TextBody.Paragraphs[i].TextRuns, fragments); + // if(i == 0) + // { + // Assert.AreEqual("TextBox", wrappedText[0].Text); + // Assert.AreEqual("a", wrappedText[1].Text); + // } + // if(i == 1) + // { + // Assert.AreEqual("TextBox2ra underlineLa", wrappedText[0].Text); + // Assert.AreEqual("StrikeGoudy size", wrappedText[1].Text); + // Assert.AreEqual("16SvgSize 24", wrappedText[2].Text); + // } + //} tbItem.ImportTextBody(cube.TextBody); var txtRun1Bounds = tbItem.Paragraphs[0].Runs[0].Bounds; Assert.AreEqual(43.835286458333336d, txtRun1Bounds.Width); - var widthLine2 = tbItem.Paragraphs[0].Runs[0].PerLineWidth[1]; - var topYLine2 = tbItem.Paragraphs[0].Runs[0].YIncreasePerLine[1]; - Assert.AreEqual(7.147135416666667d, widthLine2); - Assert.AreEqual(17.903645833333336d, topYLine2); + //var widthLine2 = tbItem.Paragraphs[0].Runs[0].PerLineWidth[1]; + //var topYLine2 = tbItem.Paragraphs[0].Runs[0].YIncreasePerLine[1]; + //Assert.AreEqual(7.147135416666667d, widthLine2); + //Assert.AreEqual(17.903645833333336d, topYLine2); var txtRuns2 = tbItem.Paragraphs[1].Runs; diff --git a/src/EPPlus.Export.ImageRenderer/RenderItems/RenderItem.cs b/src/EPPlus.Export.ImageRenderer/RenderItems/RenderItem.cs index a2caf4536..76eb2b67c 100644 --- a/src/EPPlus.Export.ImageRenderer/RenderItems/RenderItem.cs +++ b/src/EPPlus.Export.ImageRenderer/RenderItems/RenderItem.cs @@ -217,7 +217,7 @@ private string GetFillColor(ExcelDrawingFillBasic fill, ExcelDrawingColorManager //internal void SetTheme(ExcelTheme theme) //{ - // _theme = theme; + // theme = theme; //} } /// diff --git a/src/EPPlus.Export.ImageRenderer/RenderItems/Shared/ParagraphItem.cs b/src/EPPlus.Export.ImageRenderer/RenderItems/Shared/ParagraphItem.cs index 22d0bfc0b..4dd5c7d60 100644 --- a/src/EPPlus.Export.ImageRenderer/RenderItems/Shared/ParagraphItem.cs +++ b/src/EPPlus.Export.ImageRenderer/RenderItems/Shared/ParagraphItem.cs @@ -5,6 +5,7 @@ using EPPlus.Graphics; using EPPlusImageRenderer; using EPPlusImageRenderer.RenderItems; +using EPPlusImageRenderer.Utils; using OfficeOpenXml.Drawing; using OfficeOpenXml.Drawing.Theme; using OfficeOpenXml.Interfaces.Drawing.Text; @@ -14,6 +15,7 @@ using System.Linq; using System.Text; using System.Text.RegularExpressions; +using EPPlusColorConverter = OfficeOpenXml.Utils.TypeConversion.ColorConverter; namespace EPPlus.Export.ImageRenderer.RenderItems.Shared { @@ -66,6 +68,9 @@ public ParagraphItem(TextBodyItem textBody, DrawingBase renderer, BoundingBox pa } else { + var fc = EPPlusColorConverter.GetThemeColor(DrawingRenderer.Theme.ColorScheme.Light1); + fc = ColorUtils.GetAdjustedColor(PathFillMode.Norm, fc); + FillColor = "#" + fc.ToArgb().ToString("x8").Substring(2); //Use shape fill somehow //Maybe use a name property for fallback theme accent1 color? } @@ -122,7 +127,15 @@ private double GetParagraphLineSpacingInPixels(double spacingValue, ITextMeasure } } + internal protected TextRunItem AddRenderItemTextRun(ExcelParagraphTextRunBase origTxtRun, string displayText, double startingX, double lineSpacing) + { + var targetTxtRun = CreateTextRun(origTxtRun, Bounds, displayText); + targetTxtRun.lineSpacing = lineSpacing; + targetTxtRun.Bounds.Left = startingX; + Runs.Add(targetTxtRun); + return targetTxtRun; + } /// /// DisplayString is the text altered for display with respect to bounds etc. /// Containing line breaks appropriate for the given container @@ -133,7 +146,7 @@ internal protected void AddRenderItemTextRun(ExcelParagraphTextRunBase origTxtRu { //Create object of type var targetTxtRun = CreateTextRun(origTxtRun, Bounds, displayText); - targetTxtRun.LineSpacingPerNewLine = ParagraphLineSpacing; + targetTxtRun.lineSpacing = ParagraphLineSpacing; targetTxtRun.Bounds.Left = startingX; //if (Runs.Count == 0) //{ @@ -170,8 +183,8 @@ public void AddText(string text, ExcelTextFont font) var measurer = new FontMeasurerTrueType(); var displayText = measurer.MeasureAndWrapText(text, font.GetMeasureFont(), ParentTextBody.MaxWidth); var container = CreateTextRun(text, font, Bounds, string.Join("\r\n", displayText.ToArray())); - container.BaseLineSpacing = _lineSpacingAscendantOnly; - container.LineSpacingPerNewLine = _lsMultiplier.Value * _measurer.GetSingleLineSpacing().PointToPixel(true); + //container.BaseLineSpacing = _lineSpacingAscendantOnly; + //container.LineSpacingPerNewLine = _lsMultiplier.Value * _measurer.GetSingleLineSpacing().PointToPixel(true); Runs.Add(container); Bounds.Width = container.Bounds.Width + 0.001; //TODO: fix for equal width issue container.Bounds.Name = $"Container{Runs.Count}"; @@ -199,6 +212,22 @@ void GenerateTextFragments(ExcelDrawingTextRunCollection runs) _textFragments = new TextFragmentCollection(runContents, fontSizes); } + List GetWrappedTextLines(ExcelDrawingTextRunCollection runs, TextFragmentCollection fragments) + { + var ttMeasurer = (FontMeasurerTrueType)_measurer; + List fonts = new List(); + + for (int i = 0; i < runs.Count(); i++) + { + var txtRun = runs[i]; + var runFont = txtRun.GetMeasurementFont(); + fonts.Add(runFont); + } + + var maxSizePoints = Math.Round(Bounds.Width, 0, MidpointRounding.AwayFromZero).PixelToPoint(); + return ttMeasurer.WrapMultipleTextFragmentsToTextLines(fragments, fonts, maxSizePoints); + } + List GetWrappedText(ExcelDrawingTextRunCollection runs, TextFragmentCollection fragments) { var ttMeasurer = (FontMeasurerTrueType)_measurer; @@ -236,25 +265,61 @@ private void AddLinesAndTextRuns(ExcelDrawingParagraph p, string textIfEmpty) { //Log line positions and run sizes GenerateTextFragments(p.TextRuns); - //Calculate line breaks and/or wrapping to know how the text should be displayed - CalculateDisplayText(p, _textFragments); - - string currentLine = _paragraphLines[0]; - int currentLineIdx = 0; + ////Calculate line breaks and/or wrapping to know how the text should be displayed + //CalculateDisplayText(p, _textFragments); - double widthOfCurrentLine = 0; - double largestFontSizeCurrentLine = 0; - int idxLargestFontSize = 0; - int firstRunInLineIdx = 0; + var lines = WrapToSimpleTextLines(p, _textFragments); + //In points + double lastDescent = 0; + bool lineSpacingIsExact = _lsMultiplier.HasValue == false; + double runLineSpacing = 0; + double greatestWidth = 0; - //var lineSizes = _textFragments.GetLargestFontSizesOfEachLine(); - //var currentLineSize = lineSizes[currentLineIdx]; - double lineSpacing = 0; - if(_lsMultiplier.HasValue == false) + foreach (var line in lines) { - //linespacing is exact - lineSpacing = ParagraphLineSpacing; + double prevWidth = 0; + + if(lineSpacingIsExact == false) + { + runLineSpacing += line.LargestAscent + lastDescent; + } + else + { + runLineSpacing += ParagraphLineSpacing; + } + if(line.Width > greatestWidth) + { + greatestWidth = line.Width; + } + + foreach (var rtFragment in line.RtFragments) + { + var displayText = line.GetFragmentText(rtFragment); + var runItem = AddRenderItemTextRun(p.TextRuns[rtFragment.Fragidx], displayText, prevWidth, runLineSpacing); + runItem.Bounds.Width = rtFragment.Width; + prevWidth += rtFragment.Width; + } + + lastDescent = line.LargestDescent; } + Bounds.Height = runLineSpacing + lastDescent; + //Bounds.Width = greatestWidth; + //string currentLine = _paragraphLines[0]; + //int currentLineIdx = 0; + + //double widthOfCurrentLine = 0; + //double largestFontSizeCurrentLine = 0; + //int idxLargestFontSize = 0; + //int firstRunInLineIdx = 0; + + ////var lineSizes = _textFragments.GetLargestFontSizesOfEachLine(); + ////var currentLineSize = lineSizes[currentLineIdx]; + //double lineSpacing = 0; + //if(_lsMultiplier.HasValue == false) + //{ + // //linespacing is exact + // lineSpacing = ParagraphLineSpacing; + //} if (p.TextRuns.Count == 0 && string.IsNullOrEmpty(textIfEmpty) == false) { @@ -263,72 +328,77 @@ private void AddLinesAndTextRuns(ExcelDrawingParagraph p, string textIfEmpty) } else { - for (int i = 0; i < p.TextRuns.Count; i++) - { - AddRenderItemTextRun(p.TextRuns[i], _textRunDisplayText[i], widthOfCurrentLine); - var lastAdded = Runs.Last(); - - lastAdded.SetPerLineWidths(_textFragments.GetFragmentWidths(i)); - - if (lastAdded.Lines.Count > 1) - { - //Just in case. Should always be empty here - lastAdded.YIncreasePerLine.Clear(); - - //We are on a new line - for (int j = 0; j < lastAdded.Lines.Count; j++) - { - currentLine = _paragraphLines[currentLineIdx]; - - if (_lsMultiplier.HasValue) - { - //Add ascent to descent (Add ascent to Nothing for the first run) - lineSpacing += _textFragments.GetAscent(currentLineIdx).PointToPixel() * _lsMultiplier.Value; - } - - lastAdded.AddLineSpacing(lineSpacing); - - if (_lsMultiplier.HasValue) - { - //Set linespacing to descent - lineSpacing = _textFragments.GetDescent(currentLineIdx).PointToPixel(); - } - - //Last line in added lines we will continue on if there are more textruns - //Therefore the index will be added to after the next line-break or at the end - if (j < lastAdded.Lines.Count - 1) - { - Bounds.Height += lastAdded.Bounds.Height; - currentLineIdx++; - } - } - - widthOfCurrentLine = Runs.Last().PerLineWidth.Last(); - } - else - { - widthOfCurrentLine += Runs.Last().PerLineWidth.Last(); - - //If we are on the last run - if (i == p.TextRuns.Count - 1) - { - if (_lsMultiplier.HasValue) - { - //currentLineIdx++; - //Add ascent to descent (Add ascent to Nothing for the first run) - lineSpacing += _textFragments.GetAscent(currentLineIdx).PointToPixel() * _lsMultiplier.Value; - } - lastAdded.AddLineSpacing(lineSpacing); - Bounds.Height += lastAdded.Bounds.Height; - //Runs[idxLargestFontSize].GetBounds(out double l, out double t, out double r, out double b); - //Bounds.Height += Runs[idxLargestFontSize].Bounds.Height; - } - } - Bounds.Width = widthOfCurrentLine; - } + //for (int i = 0; i < p.TextRuns.Count; i++) + //{ + // AddRenderItemTextRun(p.TextRuns[i], _textRunDisplayText[i], widthOfCurrentLine); + // var lastAdded = Runs.Last(); + + // lastAdded.SetPerLineWidths(_textFragments.GetFragmentWidths(i)); + + // if (lastAdded.Lines.Count > 1) + // { + // //Just in case. Should always be empty here + // lastAdded.YIncreasePerLine.Clear(); + + // //We are on a new line + // for (int j = 0; j < lastAdded.Lines.Count; j++) + // { + // currentLine = _paragraphLines[currentLineIdx]; + + // if (_lsMultiplier.HasValue) + // { + // //Add ascent to descent (Add ascent to Nothing for the first run) + // lineSpacing += _textFragments.GetAscent(currentLineIdx).PointToPixel() * _lsMultiplier.Value; + // } + + // lastAdded.AddLineSpacing(lineSpacing); + + // if (_lsMultiplier.HasValue) + // { + // //Set linespacing to descent + // lineSpacing = _textFragments.GetDescent(currentLineIdx).PointToPixel(); + // } + + // //Last line in added lines we will continue on if there are more textruns + // //Therefore the index will be added to after the next line-break or at the end + // if (j < lastAdded.Lines.Count - 1) + // { + // Bounds.Height += lastAdded.Bounds.Height; + // currentLineIdx++; + // } + // } + + // widthOfCurrentLine = Runs.Last().PerLineWidth.Last(); + // } + // else + // { + // widthOfCurrentLine += Runs.Last().PerLineWidth.Last(); + + // //If we are on the last run + // if (i == p.TextRuns.Count - 1) + // { + // if (_lsMultiplier.HasValue) + // { + // //currentLineIdx++; + // //Add ascent to descent (Add ascent to Nothing for the first run) + // lineSpacing += _textFragments.GetAscent(currentLineIdx).PointToPixel() * _lsMultiplier.Value; + // } + // lastAdded.AddLineSpacing(lineSpacing); + // Bounds.Height += lastAdded.Bounds.Height; + // //Runs[idxLargestFontSize].GetBounds(out double l, out double t, out double r, out double b); + // //Bounds.Height += Runs[idxLargestFontSize].Bounds.Height; + // } + // } + // Bounds.Width = widthOfCurrentLine; + //} } } + private void CalculateTextLines(ExcelDrawingParagraph p, TextFragmentCollection fragments) + { + WrapToSimpleTextLines(p, fragments); + } + /// /// Use textfragments to calculate wrapping/line-breaks /// @@ -353,6 +423,22 @@ private void CalculateDisplayText(ExcelDrawingParagraph p, TextFragmentCollectio //var lineToRunMapping = } + List WrapToSimpleTextLines(ExcelDrawingParagraph p, TextFragmentCollection fragments) + { + var ttMeasurer = (FontMeasurerTrueType)_measurer; + List fonts = new List(); + + for (int i = 0; i < p.TextRuns.Count(); i++) + { + var txtRun = p.TextRuns[i]; + var runFont = txtRun.GetMeasurementFont(); + fonts.Add(runFont); + } + + var maxWidthPoints = Math.Round(Bounds.Width, 0, MidpointRounding.AwayFromZero).PixelToPoint(); + return ttMeasurer.WrapMultipleTextFragmentsToTextLines(fragments, fonts, maxWidthPoints); + } + internal double GetAlignmentHorizontal(eTextAlignment txAlignment) { var area = Bounds; diff --git a/src/EPPlus.Export.ImageRenderer/RenderItems/Shared/TextBodyItem.cs b/src/EPPlus.Export.ImageRenderer/RenderItems/Shared/TextBodyItem.cs index 8dd655fee..db40afda8 100644 --- a/src/EPPlus.Export.ImageRenderer/RenderItems/Shared/TextBodyItem.cs +++ b/src/EPPlus.Export.ImageRenderer/RenderItems/Shared/TextBodyItem.cs @@ -89,40 +89,40 @@ internal virtual void ImportTextBody(ExcelTextBody body) foreach (var paragraph in body.Paragraphs) { - if (paragraph == body.Paragraphs[0]) - { - //For the first line we always add ascent for Top-aligned but this should not be done for center vertical align - //However as paragraph and textRun should not have to deal with vertical align directly - //We wish to achieve the effect of not applying dy to the first textrun while not chaning anything - //thus: we change paragraphStartY to get around it. Should probably be solved in the textrun somehow - if (VerticalAlignment == eTextAnchoringType.Center && paragraph.TextRuns != null && paragraph.TextRuns.Count > 0) - { - var _measurer = paragraph._prd.Package.Settings.TextSettings.GenericTextMeasurerTrueType; - var spacingValue = GetParagraphAscendantSpacingInPixels( - paragraph.LineSpacing.LineSpacingType, paragraph.LineSpacing.Value, _measurer, out double multiplier); - - if(multiplier == -1) - { - paragraphStartY -= spacingValue; - } - else - { - var runFont = paragraph.TextRuns[0].GetMeasurementFont(); - var startFont = paragraph.DefaultRunProperties.GetMeasureFont(); - _measurer.SetFont(runFont); - - paragraphStartY -= multiplier * _measurer.GetBaseLine().PointToPixel(true); - - //Reset measurer font - _measurer.SetFont(startFont); - } - - - //paragraph.TextRuns[0].GetMeasurementFont() * - ////var baseLineSize = paragraph.TextRuns[0].FontSize.PointToPixel(); - ////paragraphStartY = paragraphStartY - baseLineSize; - } - } + //if (paragraph == body.Paragraphs[0]) + //{ + // //For the first line we always add ascent for Top-aligned but this should not be done for center vertical align + // //However as paragraph and textRun should not have to deal with vertical align directly + // //We wish to achieve the effect of not applying dy to the first textrun while not chaning anything + // //thus: we change paragraphStartY to get around it. Should probably be solved in the textrun somehow + // if (VerticalAlignment == eTextAnchoringType.Center && paragraph.TextRuns != null && paragraph.TextRuns.Count > 0) + // { + // var _measurer = paragraph._prd.Package.Settings.TextSettings.GenericTextMeasurerTrueType; + // var spacingValue = GetParagraphAscendantSpacingInPixels( + // paragraph.LineSpacing.LineSpacingType, paragraph.LineSpacing.Value, _measurer, out double multiplier); + + // if(multiplier == -1) + // { + // paragraphStartY -= spacingValue; + // } + // else + // { + // var runFont = paragraph.TextRuns[0].GetMeasurementFont(); + // var startFont = paragraph.DefaultRunProperties.GetMeasureFont(); + // _measurer.SetFont(runFont); + + // paragraphStartY -= multiplier * _measurer.GetBaseLine().PointToPixel(true); + + // //Reset measurer font + // _measurer.SetFont(startFont); + // } + + + // //paragraph.TextRuns[0].GetMeasurementFont() * + // ////var baseLineSize = paragraph.TextRuns[0].FontSize.PointToPixel(); + // ////paragraphStartY = paragraphStartY - baseLineSize; + // } + //} ImportParagraph(paragraph, paragraphStartY); var addedPara = Paragraphs.Last(); diff --git a/src/EPPlus.Export.ImageRenderer/RenderItems/Shared/TextRunItem.cs b/src/EPPlus.Export.ImageRenderer/RenderItems/Shared/TextRunItem.cs index 4db81ad78..5ff296117 100644 --- a/src/EPPlus.Export.ImageRenderer/RenderItems/Shared/TextRunItem.cs +++ b/src/EPPlus.Export.ImageRenderer/RenderItems/Shared/TextRunItem.cs @@ -44,11 +44,12 @@ internal abstract class TextRunItem : RenderItem protected internal eStrikeType _strikeType; protected internal Color _underlineColor; - internal double LineSpacingPerNewLine { get; set; } - internal double BaseLineSpacing { get; set; } + internal double lineSpacing { get; set; } + //internal double LineSpacingPerNewLine { get; set; } + //internal double BaseLineSpacing { get; set; } - internal List YIncreasePerLine { get; private set; } = new List(); - internal List PerLineWidth { get; private set; } = new List(); + //internal List YIncreasePerLine { get; private set; } = new List(); + //internal List PerLineWidth { get; private set; } = new List(); internal double ClippingHeight = double.NaN; internal TextRunItem(DrawingBase renderer, BoundingBox parent, string text, ExcelTextFont font, string displayText) : base(renderer, parent) @@ -119,17 +120,12 @@ internal TextRunItem(DrawingBase renderer, BoundingBox parent, ExcelParagraphTex Lines = Regex.Split(_currentText, "\r\n|\r|\n").ToList(); - var measurer = run.Paragraph._prd.Package.Settings.TextSettings.GenericTextMeasurerTrueType; - _measurer = (FontMeasurerTrueType)measurer; - _measurementFont = run.GetMeasurementFont(); - _measurer.SetFont(_measurementFont); - - _isFirstInParagraph = run.IsFirstInParagraph; _fontStyles = _measurementFont.Style; FontSizeInPixels = ((double)_measurementFont.Size).PointToPixel(true); + Bounds.Height = FontSizeInPixels; _horizontalTextAlignment = run.Paragraph.HorizontalAlignment; @@ -151,59 +147,59 @@ internal TextRunItem(DrawingBase renderer, BoundingBox parent, ExcelParagraphTex _strikeType = run.FontStrike; } - internal double CalculateLineSpacing() - { - ////---Calculation of total y-height--- - - ////If already did this - if (YIncreasePerLine.Count > 0) - { - return Bounds.Height; - } - - //bool lineIsFirstInParagraph = _isFirstInParagraph; - - //foreach (var line in Lines) - //{ - // //Despite new textrun it could still be on the same line as previous textrun - // //Therefore only do line increase if we are first in paragraph or if we are not Lines[0]. - // //This as line == Lines[0] && isFirstInParagraph == false means we are continuing on the same line as previous textRun - // //This is important if for example we have rich text where two letters on the same line has different colors. - // if (line != Lines[0] || lineIsFirstInParagraph) - // { - // var yIncrease = lineIsFirstInParagraph ? BaseLineSpacing : LineSpacingPerNewLine; - // lineIsFirstInParagraph = false; - - // //yIncrease = Fonts.OpenType.Utils.TextUtils.RoundToWhole(yIncrease); - - // YIncreasePerLine.Add(yIncrease); - - // _yEndPos += yIncrease; - // //if (Double.IsNaN(ClippingHeight) == false && _yEndPos >= ClippingHeight) - // //{ - // // bool displayLine = false - // //} - // } - // else - // { - // YIncreasePerLine.Add(0); - // } - //} - - //for (int i = 0; i < YIncreasePerLine) - - if (_yEndPos == 0) - { - Bounds.Height = FontSizeInPixels; - } - else - { - Bounds.Height = _yEndPos; - } - - return _yEndPos; - - } + //internal double CalculateLineSpacing() + //{ + // ////---Calculation of total y-height--- + + // ////If already did this + // if (YIncreasePerLine.Count > 0) + // { + // return Bounds.Height; + // } + + // //bool lineIsFirstInParagraph = _isFirstInParagraph; + + // //foreach (var line in Lines) + // //{ + // // //Despite new textrun it could still be on the same line as previous textrun + // // //Therefore only do line increase if we are first in paragraph or if we are not Lines[0]. + // // //This as line == Lines[0] && isFirstInParagraph == false means we are continuing on the same line as previous textRun + // // //This is important if for example we have rich text where two letters on the same line has different colors. + // // if (line != Lines[0] || lineIsFirstInParagraph) + // // { + // // var yIncrease = lineIsFirstInParagraph ? BaseLineSpacing : LineSpacingPerNewLine; + // // lineIsFirstInParagraph = false; + + // // //yIncrease = Fonts.OpenType.Utils.TextUtils.RoundToWhole(yIncrease); + + // // YIncreasePerLine.Add(yIncrease); + + // // _yEndPos += yIncrease; + // // //if (Double.IsNaN(ClippingHeight) == false && _yEndPos >= ClippingHeight) + // // //{ + // // // bool displayLine = false + // // //} + // // } + // // else + // // { + // // YIncreasePerLine.Add(0); + // // } + // //} + + // //for (int i = 0; i < YIncreasePerLine) + + // if (_yEndPos == 0) + // { + // Bounds.Height = FontSizeInPixels; + // } + // else + // { + // Bounds.Height = _yEndPos; + // } + + // return _yEndPos; + + //} internal List GetLines(string text) { @@ -240,99 +236,99 @@ internal override void GetBounds(out double il, out double it, out double ir, ou //Bounds.Width = ir-Bounds.Left; } - internal double CalculateRightPositionInPixels() - { - var numLines = GetNumberOfLines(); - double retPos = Bounds.Left; - - double textLengthPixels = 0; - - if (numLines <= 0 || string.IsNullOrEmpty(_originalText)) - { - textLengthPixels = 0; - return retPos; - } - - double longestWidth = -1; - - for (int i = 0; i < numLines; i++) - { - var text = Lines[i]; - var width = CalculateTextWidth(Lines[i]).PointToPixel(); - PerLineWidth.Add(width); - - if (width > longestWidth) - { - longestWidth = width; - } - } - - textLengthPixels = longestWidth; - - retPos += longestWidth; - - //Calculates right - Bounds.Width = textLengthPixels; - - return Bounds.Right; - } - - internal double CalculateBottomPositionInPixels() - { - return Bounds.Height; - } - - internal double CalculateTextWidth(string targetString) - { - _measurer.SetFont(_measurementFont); - _measurer.MeasureWrappedTextCells = true; - var width = _measurer.MeasureTextWidth(targetString); - - return width; - } - - internal void SetPerLineWidths(List widthsInPoints) - { - var newList = new List(); - for (int i = 0; i < widthsInPoints.Count; i++) - { - newList.Add(widthsInPoints[i].PointToPixel()); - } - - PerLineWidth.Clear(); - PerLineWidth = newList; - } - - internal void AddLineSpacing(double lineSpacing) - { - YIncreasePerLine.Add(lineSpacing); - _yEndPos += lineSpacing; - Bounds.Height = _yEndPos; - //bool lineIsFirstInParagraph = _isFirstInParagraph; - - //for(int i = 0; i< Lines.Count(); i++) - //{ - // if (Lines[i] != Lines[0] || lineIsFirstInParagraph) - // { - // var yIncrease = lineIsFirstInParagraph ? ascent : ascent + descent; - // YIncreasePerLine.Add() - - // lineIsFirstInParagraph = false; - // } - //} - - //for (int i = 0; i < YIncreasePerLine) - - // if (_yEndPos == 0) - // { - // Bounds.Height = FontSizeInPixels; - // } - // else - // { - // Bounds.Height = _yEndPos; - // } - - //return _yEndPos; - } + //internal double CalculateRightPositionInPixels() + //{ + // var numLines = GetNumberOfLines(); + // double retPos = Bounds.Left; + + // double textLengthPixels = 0; + + // if (numLines <= 0 || string.IsNullOrEmpty(_originalText)) + // { + // textLengthPixels = 0; + // return retPos; + // } + + // double longestWidth = -1; + + // for (int i = 0; i < numLines; i++) + // { + // var text = Lines[i]; + // var width = CalculateTextWidth(Lines[i]).PointToPixel(); + // PerLineWidth.Add(width); + + // if (width > longestWidth) + // { + // longestWidth = width; + // } + // } + + // textLengthPixels = longestWidth; + + // retPos += longestWidth; + + // //Calculates right + // Bounds.Width = textLengthPixels; + + // return Bounds.Right; + //} + + //internal double CalculateBottomPositionInPixels() + //{ + // return Bounds.Height; + //} + + //internal double CalculateTextWidth(string targetString) + //{ + // _measurer.SetFont(_measurementFont); + // _measurer.MeasureWrappedTextCells = true; + // var width = _measurer.MeasureTextWidth(targetString); + + // return width; + //} + + //internal void SetPerLineWidths(List widthsInPoints) + //{ + // var newList = new List(); + // for (int i = 0; i < widthsInPoints.Count; i++) + // { + // newList.Add(widthsInPoints[i].PointToPixel()); + // } + + // PerLineWidth.Clear(); + // PerLineWidth = newList; + //} + + //internal void AddLineSpacing(double lineSpacing) + //{ + // YIncreasePerLine.Add(lineSpacing); + // _yEndPos += lineSpacing; + // Bounds.Height = _yEndPos; + // //bool lineIsFirstInParagraph = _isFirstInParagraph; + + // //for(int i = 0; i< Lines.Count(); i++) + // //{ + // // if (Lines[i] != Lines[0] || lineIsFirstInParagraph) + // // { + // // var yIncrease = lineIsFirstInParagraph ? ascent : ascent + descent; + // // YIncreasePerLine.Add() + + // // lineIsFirstInParagraph = false; + // // } + // //} + + // //for (int i = 0; i < YIncreasePerLine) + + // // if (_yEndPos == 0) + // // { + // // Bounds.Height = FontSizeInPixels; + // // } + // // else + // // { + // // Bounds.Height = _yEndPos; + // // } + + // //return _yEndPos; + //} } } diff --git a/src/EPPlus.Export.ImageRenderer/RenderItems/SvgItem/SvgParagraphItem.cs b/src/EPPlus.Export.ImageRenderer/RenderItems/SvgItem/SvgParagraphItem.cs index f6cc49f90..b7f4c962e 100644 --- a/src/EPPlus.Export.ImageRenderer/RenderItems/SvgItem/SvgParagraphItem.cs +++ b/src/EPPlus.Export.ImageRenderer/RenderItems/SvgItem/SvgParagraphItem.cs @@ -51,7 +51,7 @@ public override void Render(StringBuilder sb) { var fontSize = _paragraphFont.Size.PointToPixel().ToString(CultureInfo.InvariantCulture); - sb.AppendLine($""); + sb.AppendLine($""); sb.AppendLine("paragraph "); diff --git a/src/EPPlus.Export.ImageRenderer/RenderItems/SvgItem/SvgTextBoxItem.cs b/src/EPPlus.Export.ImageRenderer/RenderItems/SvgItem/SvgTextBoxItem.cs index ddca27d1e..dd3608e5a 100644 --- a/src/EPPlus.Export.ImageRenderer/RenderItems/SvgItem/SvgTextBoxItem.cs +++ b/src/EPPlus.Export.ImageRenderer/RenderItems/SvgItem/SvgTextBoxItem.cs @@ -74,9 +74,9 @@ internal override void AppendRenderItems(List renderItems) } renderItems.Add(groupItem); //handled by group now - Rectangle.Bounds.Top = 0; - Rectangle.Bounds.Left = 0; - renderItems.Add(Rectangle); + //Rectangle.Bounds.Top = 0; + //Rectangle.Bounds.Left = 0; + //renderItems.Add(Rectangle); TextBody.AppendRenderItems(renderItems); renderItems.Add(new SvgEndGroupItem(DrawingRenderer, Bounds)); } diff --git a/src/EPPlus.Export.ImageRenderer/RenderItems/SvgItem/SvgTextRunItem.cs b/src/EPPlus.Export.ImageRenderer/RenderItems/SvgItem/SvgTextRunItem.cs index f9b78b305..6af09af6f 100644 --- a/src/EPPlus.Export.ImageRenderer/RenderItems/SvgItem/SvgTextRunItem.cs +++ b/src/EPPlus.Export.ImageRenderer/RenderItems/SvgItem/SvgTextRunItem.cs @@ -1,4 +1,5 @@ using EPPlus.Export.ImageRenderer.RenderItems.Shared; +using EPPlus.Fonts.OpenType.Utils; using EPPlus.Graphics; using EPPlusImageRenderer; using EPPlusImageRenderer.RenderItems; @@ -24,7 +25,7 @@ public SvgTextRunItem(DrawingBase renderer, BoundingBox parent, string text, Exc } string GetFontStyleAttributes() { - string fontStyleAttributes = ""; + string fontStyleAttributes = " "; if (_isItalic) { @@ -37,7 +38,7 @@ string GetFontStyleAttributes() if (_underLineType != eUnderLineType.None | _strikeType != eStrikeType.No) { - fontStyleAttributes += "text-decoration=\""; + fontStyleAttributes += "text-decoration=\" "; if (_underLineType != eUnderLineType.None) { switch (_underLineType) @@ -84,60 +85,92 @@ string GetFontStyleAttributes() public override void Render(StringBuilder sb) { string finalString = ""; - var xString = $"x =\"{(Bounds.Left).ToString(CultureInfo.InvariantCulture)}\" "; + var xString = $"x =\"{(Bounds.Left.PointToPixel()).ToString(CultureInfo.InvariantCulture)}\" "; var currentYEndPos = Bounds.Position.Y; // Global position Y + finalString += $" 0 && YIncreasePerLine[i] != 0) - { - var yIncrease = Fonts.OpenType.Utils.TextUtils.RoundToWhole(YIncreasePerLine[i]); - - currentYEndPos += yIncrease; - if (double.IsNaN(ClippingHeight) == false && currentYEndPos >= ClippingHeight) - { - visibility = "display=\"none\""; - } - - var yIncreaseString = yIncrease.ToString(CultureInfo.InvariantCulture); - var dyString = $"dy=\"{yIncreaseString}px\" "; - finalString += dyString; - finalString += "x=\"0\" "; - } - else - { - finalString += xString; - } - - finalString += $"{visibility} " + $"{GetFontStyleAttributes()} "; - - if (_measurementFont != null) - { - finalString += $"font-family=\"{_measurementFont.FontFamily}," - + $"{_measurementFont.FontFamily}_MSFontService,sans-serif\" " - + $"font-size=\"{FontSizeInPixels.ToString(CultureInfo.InvariantCulture)}px\" "; - } + finalString += xString; + var yString = $" y=\"{lineSpacing.PointToPixel()}px\" "; + finalString += yString; - sb.Append(finalString); + currentYEndPos += lineSpacing; - //Get color etc. - //Renders up until this point - SvgBaseRenderer.BaseRender(sb, this); - //Since final string has been written in base.render erase it. - finalString = ""; + if (double.IsNaN(ClippingHeight) == false && currentYEndPos >= ClippingHeight) + { + visibility = " display=\"none\""; + } + finalString += visibility; + finalString += $"{GetFontStyleAttributes()}"; - finalString += ">"; - finalString += line; - finalString += ""; + if (_measurementFont != null) + { + finalString += $"font-family=\"{_measurementFont.FontFamily}," + + $"{_measurementFont.FontFamily}_MSFontService,sans-serif\" " + + $"font-size=\"{FontSizeInPixels.ToString(CultureInfo.InvariantCulture)}px\" "; } + + sb.Append(finalString); + //Get color etc. + //Renders up until this point + SvgBaseRenderer.BaseRender(sb, this); + //Since final string has been written in base.render erase it. + finalString = ""; + + finalString += ">"; + finalString += _currentText; + finalString += ""; + //for (int i = 0; i < Lines.Count; i++) + //{ + // var line = Lines[i]; + + // finalString += $" 0 && YIncreasePerLine[i] != 0) + // { + // var yIncrease = Fonts.OpenType.Utils.TextUtils.RoundToWhole(YIncreasePerLine[i]); + + // currentYEndPos += yIncrease; + // if (double.IsNaN(ClippingHeight) == false && currentYEndPos >= ClippingHeight) + // { + // visibility = "display=\"none\""; + // } + + // var yIncreaseString = yIncrease.ToString(CultureInfo.InvariantCulture); + // var dyString = $"dy=\"{yIncreaseString}px\" "; + // finalString += dyString; + // finalString += "x=\"0\" "; + // } + // else + // { + // finalString += xString; + // } + + // finalString += $"{visibility} " + $"{GetFontStyleAttributes()} "; + + // if (_measurementFont != null) + // { + // finalString += $"font-family=\"{_measurementFont.FontFamily}," + // + $"{_measurementFont.FontFamily}_MSFontService,sans-serif\" " + // + $"font-size=\"{FontSizeInPixels.ToString(CultureInfo.InvariantCulture)}px\" "; + // } + + // sb.Append(finalString); + + // //Get color etc. + // //Renders up until this point + // SvgBaseRenderer.BaseRender(sb, this); + // //Since final string has been written in base.render erase it. + // finalString = ""; + + // finalString += ">"; + // finalString += line; + // finalString += ""; + //} sb.Append(finalString); } } diff --git a/src/EPPlus.Export.ImageRenderer/Svg/SvgShape.cs b/src/EPPlus.Export.ImageRenderer/Svg/SvgShape.cs index f44828a2b..cb5b8bc4d 100644 --- a/src/EPPlus.Export.ImageRenderer/Svg/SvgShape.cs +++ b/src/EPPlus.Export.ImageRenderer/Svg/SvgShape.cs @@ -208,7 +208,7 @@ public string ViewBox b = ib; } } - return $"{(l * Bounds.Width).ToString(CultureInfo.InvariantCulture)},{(t * Bounds.Height).ToString(CultureInfo.InvariantCulture)},{((Math.Abs(l) + r) * Bounds.Width).ToString(CultureInfo.InvariantCulture)},{((Math.Abs(t) + b) * Bounds.Height).ToString(CultureInfo.InvariantCulture)}"; + return $"{(Bounds.Left).ToString(CultureInfo.InvariantCulture)},{Bounds.Top.ToString(CultureInfo.InvariantCulture)},{Bounds.Right.ToString(CultureInfo.InvariantCulture)},{Bounds.Bottom.ToString(CultureInfo.InvariantCulture)}"; } } diff --git a/src/EPPlus.Fonts.OpenType.Tests/TextFragmentCollectionTests.cs b/src/EPPlus.Fonts.OpenType.Tests/TextFragmentCollectionTests.cs index 50372ea90..a35ef6132 100644 --- a/src/EPPlus.Fonts.OpenType.Tests/TextFragmentCollectionTests.cs +++ b/src/EPPlus.Fonts.OpenType.Tests/TextFragmentCollectionTests.cs @@ -78,6 +78,121 @@ public void MeasureGoudy() Assert.AreEqual(237, inPixels); } + [TestMethod] + public void MeasureWrappedWidthsWithInternalLineBreaks() + { + List lstOfRichText = new() { "TextBox\r\na\r\n", "TextBox2", "ra underline", "La Strike", "Goudy size 16", "SvgSize 24" }; + + var font1 = new MeasurementFont() + { + FontFamily = "Aptos Narrow", + Size = 11, + Style = MeasurementFontStyles.Regular + }; ; + + var font2 = new MeasurementFont() + { + FontFamily = "Aptos Narrow", + Size = 11, + Style = MeasurementFontStyles.Bold + }; + + var font3 = new MeasurementFont() + { + FontFamily = "Aptos Narrow", + Size = 11, + Style = MeasurementFontStyles.Underline + }; + + var font4 = new MeasurementFont() + { + FontFamily = "Aptos Narrow", + Size = 11, + Style = MeasurementFontStyles.Strikeout + }; + + var font5 = new MeasurementFont() + { + FontFamily = "Goudy Stout", + Size = 16, + Style = MeasurementFontStyles.Regular + }; + + + var font6 = new MeasurementFont() + { + FontFamily = "Aptos Narrow", + Size = 24, + Style = MeasurementFontStyles.Regular + }; + + List fonts = new() { font1, font2, font3, font4, font5, font6 }; + + var maxSizePoints = Math.Round(300d, 0, MidpointRounding.AwayFromZero).PixelToPoint(); + var ttMeasurer = new FontMeasurerTrueType(font1); + + var textFragments = new TextFragmentCollection(lstOfRichText); + + var wrappedLines = ttMeasurer.WrapMultipleTextFragmentsToTextLines(textFragments, fonts, maxSizePoints); + + var line1 = wrappedLines[0]; + + var pixels11 = Math.Round(line1.RtFragments[0].Width.PointToPixel(), 0, MidpointRounding.AwayFromZero); + var pixelsWholeline1 = Math.Round(line1.Width.PointToPixel(), 0, MidpointRounding.AwayFromZero); + + Assert.AreEqual(44, pixels11); + Assert.AreEqual(pixels11, pixelsWholeline1); + + var line2 = wrappedLines[1]; + + var pixels21 = Math.Round(line2.RtFragments[0].Width.PointToPixel(), 0, MidpointRounding.AwayFromZero); + var pixelsWholeline2 = Math.Round(line2.Width.PointToPixel(), 0, MidpointRounding.AwayFromZero); + + Assert.AreEqual(7, pixels21); + Assert.AreEqual(pixels21, pixelsWholeline2); + + var line3 = wrappedLines[2]; + + var pixels31 = Math.Round(line3.RtFragments[0].Width.PointToPixel(), 0, MidpointRounding.AwayFromZero); + var pixels32 = Math.Round(line3.RtFragments[1].Width.PointToPixel(), 0, MidpointRounding.AwayFromZero); + var pixels33 = Math.Round(line3.RtFragments[2].Width.PointToPixel(), 0, MidpointRounding.AwayFromZero); + var pixelsWholeLine3 = Math.Round(line3.Width.PointToPixel(), 0, MidpointRounding.AwayFromZero); + + //~54 px + Assert.AreEqual(54, pixels31); + //~70 px aka 51.75pt + Assert.AreEqual(70, pixels32); + //~16-17 px This line contains a space at the end + Assert.AreEqual(17, pixels33); + + //Total Width: ~140 + Assert.AreEqual(140d, pixelsWholeLine3); + + var line4 = wrappedLines[3]; + + var pixels41 = Math.Round(line4.RtFragments[0].Width.PointToPixel(), 0, MidpointRounding.AwayFromZero); + var pixels42 = Math.Round(line4.RtFragments[1].Width.PointToPixel(), 0, MidpointRounding.AwayFromZero); + var pixelsWholeLine4 = Math.Round(line4.Width.PointToPixel(), 0, MidpointRounding.AwayFromZero); + //~34 px + Assert.AreEqual(33, pixels41); + // This line contains a space at the end + Assert.AreEqual(248, pixels42); + + Assert.AreEqual(281, pixelsWholeLine4); + + var line5 = wrappedLines[4]; + + var pixels51 = Math.Round(line5.RtFragments[0].Width.PointToPixel(), 0, MidpointRounding.AwayFromZero); + var pixels52 = Math.Round(line5.RtFragments[1].Width.PointToPixel(), 0, MidpointRounding.AwayFromZero); + var pixelsWholeLine5 = Math.Round(line5.Width.PointToPixel(), 0, MidpointRounding.AwayFromZero); + Assert.AreEqual(35, pixels51); + //This line does NOT contain a space at the end + Assert.AreEqual(134, pixels52); + + + Assert.AreEqual(169, pixelsWholeLine5); + } + [TestMethod] public void MeasureWrappedWidths() { @@ -162,10 +277,6 @@ public void MeasureWrappedWidths() Assert.AreEqual(169, pixelsWholeLine3); - - //Line 1 137 px 102.75 pt //result: 104.6328125 pt width "whole - //Line 2 270 px 202.5 pt - //Line 3 169 px 126.75 pt } } } diff --git a/src/EPPlus.Fonts.OpenType/TrueTypeMeasurer/DataHolders/RichTextFragmentSimple.cs b/src/EPPlus.Fonts.OpenType/TrueTypeMeasurer/DataHolders/RichTextFragmentSimple.cs index 3af5cbacc..16181dce3 100644 --- a/src/EPPlus.Fonts.OpenType/TrueTypeMeasurer/DataHolders/RichTextFragmentSimple.cs +++ b/src/EPPlus.Fonts.OpenType/TrueTypeMeasurer/DataHolders/RichTextFragmentSimple.cs @@ -7,13 +7,15 @@ namespace EPPlus.Fonts.OpenType.TrueTypeMeasurer.DataHolders { public class RichTextFragmentSimple { - internal int Fragidx { get; set; } + public int Fragidx { get; internal set; } internal int OverallParagraphStartCharIdx { get; set; } //Char length internal int charStarIdxWithinCurrentLine { get; set; } - //Width in points - internal double Width { get; set; } + /// + /// Width in points + /// + public double Width { get; internal set; } public RichTextFragmentSimple() { } diff --git a/src/EPPlus.Fonts.OpenType/TrueTypeMeasurer/DataHolders/TextLineSimple.cs b/src/EPPlus.Fonts.OpenType/TrueTypeMeasurer/DataHolders/TextLineSimple.cs index 648963ef6..1dcb6bf00 100644 --- a/src/EPPlus.Fonts.OpenType/TrueTypeMeasurer/DataHolders/TextLineSimple.cs +++ b/src/EPPlus.Fonts.OpenType/TrueTypeMeasurer/DataHolders/TextLineSimple.cs @@ -7,17 +7,28 @@ namespace EPPlus.Fonts.OpenType.TrueTypeMeasurer.DataHolders { public class TextLineSimple { - internal List RtFragments = new(); + public List RtFragments { get; private set; } = new List(); public string Text { get; internal set; } - internal double LargestFontSize { get; set; } - internal double LargestAscent { get; set; } - internal double LargestDescent { get; set; } - internal double Width { get; set; } + /// + /// The largest font size within this line + /// + public double LargestFontSize { get; internal set; } + /// + /// The largest ascent in points within this line + /// (Two differing fonts with the same size can have different ascents) + /// + public double LargestAscent { get; internal set; } + /// + /// The largest descent in points within this line + /// (Two differing fonts with the same size can have different descents) + /// + public double LargestDescent { get; internal set; } + + public double Width { get; internal set; } public TextLineSimple() { - } public TextLineSimple(List rtFragments, string text, double largestFontSize, double largestAscent, double largestDescent) @@ -28,5 +39,31 @@ public TextLineSimple(List rtFragments, string text, dou LargestAscent = largestAscent; LargestDescent = largestDescent; } + + /// + /// Get the text of one of the fragments in this line + /// + /// + /// + public string GetFragmentText(RichTextFragmentSimple rtFragment) + { + if(RtFragments.Contains(rtFragment) == false) + { + throw new InvalidOperationException($"GetFragmentText failed. Cannot retrieve {rtFragment} since it is not part of this textLine: {this}"); + } + + var startIdx = rtFragment.charStarIdxWithinCurrentLine; + + var idxInLst = RtFragments.FindIndex(x => x == rtFragment); + if(idxInLst == RtFragments.Count -1) + { + return Text.Substring(startIdx, Text.Length - startIdx); + } + else + { + var endIdx = RtFragments[idxInLst + 1].charStarIdxWithinCurrentLine; + return Text.Substring(startIdx, endIdx - startIdx); + } + } } } diff --git a/src/EPPlus.Fonts.OpenType/TrueTypeMeasurer/TextData.cs b/src/EPPlus.Fonts.OpenType/TrueTypeMeasurer/TextData.cs index 9750c0763..f0713bdbb 100644 --- a/src/EPPlus.Fonts.OpenType/TrueTypeMeasurer/TextData.cs +++ b/src/EPPlus.Fonts.OpenType/TrueTypeMeasurer/TextData.cs @@ -657,7 +657,7 @@ private static string ExtractWrappedSubstring(string orgLine, int cIdx, ref int //Remove the overflowing characters var trimmedString = spacedString.TrimEnd(' '); - wrappedString = spacedString; + wrappedString = trimmedString; //The start index of the first character in the overflow (After space) startLineIdx += startIndex; @@ -840,7 +840,6 @@ internal static List WrapMultipleTextFragments(TextParagraph paragraph, //Happens at the end of a fragment/start of a new fragment if(fragmentIdx != prevFragmentIdx) { - LogFragmentWidth(paragraph.Fragments, currentFont.HeadTable.UnitsPerEm, fontSize, lineWidth, prevFragWidths, prevFragmentIdx); //Since there is no gurantee any wrapping has occured @@ -911,6 +910,10 @@ internal static List WrapMultipleTextFragments(TextParagraph paragraph, var glyphMapping = paragraph.GlyphMappings[fragmentIdx]; char c = allText[i]; + if (c == '\r' || c == '\n') + { + continue; + } var advanceWidth = CalculateAdvanceWidth(c, glyphMapping, currentFont, ref lastGlyphIndex, ref lineWidth, ref wordWidth, ref applyKerning); //Perform the actual wrapping @@ -961,10 +964,16 @@ internal static List WrapMultipleTextFragments(TextParagraph paragraph, prevFragmentIdx = fragmentIdx; } - LogLineData(paragraph, currentFont, largestFontCurrentLine, CurrentLineLargestFontSize, lineWidth, paragraph.Fragments.TextFragments.Count()-1); - LogFragmentWidth(paragraph.Fragments, currentFont.HeadTable.UnitsPerEm, fontSize, lineWidth, prevFragWidths, paragraph.Fragments.TextFragments.Count() - 1); + if(lineWidth > 0) + { + LogLineData(paragraph, currentFont, largestFontCurrentLine, CurrentLineLargestFontSize, lineWidth, paragraph.Fragments.TextFragments.Count() - 1); + LogFragmentWidth(paragraph.Fragments, currentFont.HeadTable.UnitsPerEm, fontSize, lineWidth, prevFragWidths, paragraph.Fragments.TextFragments.Count() - 1); + } - wrappedStrings.Add(leftOverLine); + if(string.IsNullOrEmpty(leftOverLine) == false) + { + wrappedStrings.Add(leftOverLine); + } return wrappedStrings; } @@ -1026,28 +1035,15 @@ internal static List WrapMultipleTextFragmentsToTextLines(TextPa var lineIdx = charInfo.Line; var fragmentIdx = charInfo.Fragment; - //Happens at the end of a fragment/start of a new fragment - if (fragmentIdx != prevFragmentIdx) - { - AddWidthToFragment(currentRtFragment, currentFont.HeadTable.UnitsPerEm, fontSize, lineWidth, prevFragWidths, prevFragmentIdx); - currentLineRtFragments.Add(currentRtFragment); - - currentRtFragment = new RichTextFragmentSimple(); - currentRtFragment.Fragidx = fragmentIdx; - currentRtFragment.OverallParagraphStartCharIdx = i; - - prevFragWidths = ConvertDesignUnits(currentFont, fontSize, paragraph.FontIndexDict[fragmentIdx], paragraph.FontSizes[fragmentIdx], lineWidth); - } - //If we hit a pre-existing line break. Reset line and wordwidths var indexExists = newLineIndicies.Count() > currentLineIndex; if (indexExists) { if (i >= newLineIndicies[currentLineIndex]) { - AddDataToSimpleLine(currentTextLine, paragraph, currentFont, largestFontCurrentLine, CurrentLineLargestFontSize, lineWidth, fragmentIdx); + AddDataToSimpleLine(currentTextLine, paragraph, currentFont, largestFontCurrentLine, CurrentLineLargestFontSize, lineWidth, prevFragmentIdx); //Log current fragment width - AddWidthToFragment(currentRtFragment, currentFont.HeadTable.UnitsPerEm, fontSize, lineWidth, prevFragWidths, fragmentIdx); + AddWidthToFragment(currentRtFragment, currentFont.HeadTable.UnitsPerEm, fontSize, lineWidth, prevFragWidths, prevFragmentIdx); currentLineRtFragments.Add(currentRtFragment); //We are on the same fragment but there's been a line break @@ -1055,45 +1051,65 @@ internal static List WrapMultipleTextFragmentsToTextLines(TextPa prevFragWidths = 0; //Since we are on a new line the current largest font-size for this line is the current size - CurrentLineLargestFontSize = paragraph.FontSizes[fragmentIdx]; + CurrentLineLargestFontSize = paragraph.FontSizes[prevFragmentIdx]; largestFontCurrentLine = currentFont; - prevLineBreakIndex = i; - var addedLine = leftOverLine.Trim(['\r', '\n']); for (int j = 0; j < currentLineRtFragments.Count(); j++) { - //Always true here? - if (currentLineRtFragments[j].OverallParagraphStartCharIdx < prevLineBreakIndex) - { - currentLineRtFragments[j].charStarIdxWithinCurrentLine = prevLineBreakIndex - currentLineRtFragments[j].OverallParagraphStartCharIdx; - currentTextLine.RtFragments.Add(currentLineRtFragments[j]); - } + currentLineRtFragments[j].charStarIdxWithinCurrentLine = prevLineBreakIndex - currentLineRtFragments[j].OverallParagraphStartCharIdx; + currentTextLine.RtFragments.Add(currentLineRtFragments[j]); } + + //no fragments but the current one should be applicable to next line in this case + currentLineRtFragments.Clear(); + + //prevLineBreakIndex = i; + var addedLine = leftOverLine.Trim(['\r', '\n']); ////In this case it goes all the way to the end //currentRtFragment.charStarIdxWithinCurrentLine = addedLine.Length-1; //currentTextLine.RtFragments.Add(currentRtFragment); currentTextLine.Text = addedLine; outputTextLines.Add(currentTextLine); + prevFragWidths = 0; - //var overallStartIdx = currentRtFragment.OverallParagraphStartCharIdx; + var overallStartIdx = currentRtFragment.OverallParagraphStartCharIdx; currentTextLine = new TextLineSimple(); - //currentRtFragment = new RichTextFragmentSimple(); - //currentRtFragment.Fragidx = fragmentIdx; - //currentRtFragment.FontSize = paragraph.FontSizes[fragmentIdx]; - //currentRtFragment.charStarIdxWithinCurrentLine = 0; - //currentRtFragment.OverallParagraphStartCharIdx = overallStartIdx; + currentRtFragment = new RichTextFragmentSimple(); + currentRtFragment.Fragidx = fragmentIdx; + currentRtFragment.charStarIdxWithinCurrentLine = 0; + currentRtFragment.OverallParagraphStartCharIdx = overallStartIdx; //currentLineRtFragments.Clear(); //currentLineRtFragments.Add(currentRtFragment); lineWidth = 0; wordWidth = 0; leftOverLine = ""; - //prevLineEndIndex = i; + prevLineEndIndex = i; currentLineIndex++; } } + //Happens at the end of a fragment/start of a new fragment + if (fragmentIdx != prevFragmentIdx) + { + if(lineWidth != 0) + { + AddWidthToFragment(currentRtFragment, currentFont.HeadTable.UnitsPerEm, fontSize, lineWidth, prevFragWidths, prevFragmentIdx); + currentLineRtFragments.Add(currentRtFragment); + } + else + { + string test = "should only happen after \r\n"; + } + + currentRtFragment = new RichTextFragmentSimple(); + currentRtFragment.Fragidx = fragmentIdx; + currentRtFragment.OverallParagraphStartCharIdx = i; + + prevFragWidths = ConvertDesignUnits(currentFont, fontSize, paragraph.FontIndexDict[fragmentIdx], paragraph.FontSizes[fragmentIdx], lineWidth); + } + //If this char has a different font, do the neccesary conversions if (currentFont != null) { @@ -1127,6 +1143,11 @@ internal static List WrapMultipleTextFragmentsToTextLines(TextPa var glyphMapping = paragraph.GlyphMappings[fragmentIdx]; char c = allText[i]; + if (c == '\r' || c == '\n') + { + continue; + } + var advanceWidth = CalculateAdvanceWidth(c, glyphMapping, currentFont, ref lastGlyphIndex, ref lineWidth, ref wordWidth, ref applyKerning); //Perform the actual wrapping From 0cd3b1074f92c06b235ba4ac4a3bbeceaf62393f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ossian=20Edstr=C3=B6m?= Date: Wed, 4 Feb 2026 15:21:52 +0100 Subject: [PATCH 040/151] Cleanup and starting to add to addtext --- .../RenderItems/Shared/ParagraphItem.cs | 295 +++++------------- .../RenderItems/Shared/TextRunItem.cs | 4 +- .../TrueTypeMeasurer/FontMeasurerTrueType.cs | 7 + .../TrueTypeMeasurer/TextData.cs | 127 +++++++- 4 files changed, 191 insertions(+), 242 deletions(-) diff --git a/src/EPPlus.Export.ImageRenderer/RenderItems/Shared/ParagraphItem.cs b/src/EPPlus.Export.ImageRenderer/RenderItems/Shared/ParagraphItem.cs index 4dd5c7d60..0a8e2d40e 100644 --- a/src/EPPlus.Export.ImageRenderer/RenderItems/Shared/ParagraphItem.cs +++ b/src/EPPlus.Export.ImageRenderer/RenderItems/Shared/ParagraphItem.cs @@ -8,6 +8,7 @@ using EPPlusImageRenderer.Utils; using OfficeOpenXml.Drawing; using OfficeOpenXml.Drawing.Theme; +using OfficeOpenXml.FormulaParsing.Excel.Functions.Text; using OfficeOpenXml.Interfaces.Drawing.Text; using OfficeOpenXml.Style; using System; @@ -15,6 +16,7 @@ using System.Linq; using System.Text; using System.Text.RegularExpressions; +using static System.Net.Mime.MediaTypeNames; using EPPlusColorConverter = OfficeOpenXml.Utils.TypeConversion.ColorConverter; namespace EPPlus.Export.ImageRenderer.RenderItems.Shared @@ -136,57 +138,17 @@ internal protected TextRunItem AddRenderItemTextRun(ExcelParagraphTextRunBase or Runs.Add(targetTxtRun); return targetTxtRun; } - /// - /// DisplayString is the text altered for display with respect to bounds etc. - /// Containing line breaks appropriate for the given container - /// - /// - /// - internal protected void AddRenderItemTextRun(ExcelParagraphTextRunBase origTxtRun, string displayText, double startingX) - { - //Create object of type - var targetTxtRun = CreateTextRun(origTxtRun, Bounds, displayText); - targetTxtRun.lineSpacing = ParagraphLineSpacing; - targetTxtRun.Bounds.Left = startingX; - //if (Runs.Count == 0) - //{ - // targetTxtRun.BaseLineSpacing = _lineSpacingAscendantOnly; - //} - - ////If there are multiple sizes/multiple fonts with multiple sizes - //if (_lsMultiplier.HasValue) - //{ - // var runFont = origTxtRun.GetMeasurementFont(); - // _measurer.SetFont(runFont); - // targetTxtRun.LineSpacingPerNewLine = _lsMultiplier.Value * _measurer.GetSingleLineSpacing().PointToPixel(true); - // targetTxtRun.BaseLineSpacing = _lsMultiplier.Value * _measurer.GetBaseLine().PointToPixel(true); - // //Reset measurer font - // _measurer.SetFont(_paragraphFont); - //} - - //targetTxtRun.Bounds.Left = startingX; - //targetTxtRun.GetBounds(out double l, out double t, out double r, out double b); - - //for (int i = 1; i < targetTxtRun.Lines.Count; i++) - //{ - - //} - //targetTxtRun.SetPerLineWidths(_textFragments.GetFragmentWidths(fragIdx)); - - //lineIdxAfter = currentLineIdx + targetTxtRun.Lines.Count - 1; - - Runs.Add(targetTxtRun); - } public void AddText(string text, ExcelTextFont font) { var measurer = new FontMeasurerTrueType(); - var displayText = measurer.MeasureAndWrapText(text, font.GetMeasureFont(), ParentTextBody.MaxWidth); - var container = CreateTextRun(text, font, Bounds, string.Join("\r\n", displayText.ToArray())); + //var displayText = measurer.MeasureAndWrapTextLines(text, font.GetMeasureFont(), ParentTextBody.MaxWidth); + + var container = CreateTextRun(text, font, Bounds, text); //container.BaseLineSpacing = _lineSpacingAscendantOnly; //container.LineSpacingPerNewLine = _lsMultiplier.Value * _measurer.GetSingleLineSpacing().PointToPixel(true); Runs.Add(container); - Bounds.Width = container.Bounds.Width + 0.001; //TODO: fix for equal width issue + //Bounds.Width = container.Bounds.Width + 0.001; //TODO: fix for equal width issue container.Bounds.Name = $"Container{Runs.Count}"; } @@ -212,74 +174,34 @@ void GenerateTextFragments(ExcelDrawingTextRunCollection runs) _textFragments = new TextFragmentCollection(runContents, fontSizes); } - List GetWrappedTextLines(ExcelDrawingTextRunCollection runs, TextFragmentCollection fragments) - { - var ttMeasurer = (FontMeasurerTrueType)_measurer; - List fonts = new List(); - - for (int i = 0; i < runs.Count(); i++) - { - var txtRun = runs[i]; - var runFont = txtRun.GetMeasurementFont(); - fonts.Add(runFont); - } - - var maxSizePoints = Math.Round(Bounds.Width, 0, MidpointRounding.AwayFromZero).PixelToPoint(); - return ttMeasurer.WrapMultipleTextFragmentsToTextLines(fragments, fonts, maxSizePoints); - } - - List GetWrappedText(ExcelDrawingTextRunCollection runs, TextFragmentCollection fragments) - { - var ttMeasurer = (FontMeasurerTrueType)_measurer; - List fonts = new List(); - - for (int i = 0; i < runs.Count(); i++) - { - var txtRun = runs[i]; - var runFont = txtRun.GetMeasurementFont(); - fonts.Add(runFont); - } - - var maxSizePoints = Math.Round(Bounds.Width, 0, MidpointRounding.AwayFromZero).PixelToPoint(); - return ttMeasurer.WrapMultipleTextFragments(fragments, fonts, maxSizePoints); - } - public List Lines { get; set; } - private void AddLinesAndTextRuns2(ExcelDrawingParagraph p, string textIfEmpty) - { - //var line = new ParagraphLine(); - foreach (var r in p.TextRuns) - { - foreach(var text in r.SplitIntoLines()) - { - var line = new ParagraphLine(); - - //var textWidth = GetWidth(text, r); - //if(line.Width+textWidth > maxWidth) - //{ - - //} - } - } - } private void AddLinesAndTextRuns(ExcelDrawingParagraph p, string textIfEmpty) { //Log line positions and run sizes GenerateTextFragments(p.TextRuns); - ////Calculate line breaks and/or wrapping to know how the text should be displayed - //CalculateDisplayText(p, _textFragments); - var lines = WrapToSimpleTextLines(p, _textFragments); + var lines = new List(); //In points double lastDescent = 0; bool lineSpacingIsExact = _lsMultiplier.HasValue == false; double runLineSpacing = 0; double greatestWidth = 0; + if (p.TextRuns.Count == 0 && string.IsNullOrEmpty(textIfEmpty) == false) + { + var measurer = new FontMeasurerTrueType(); + var maxWidth = ParentTextBody.MaxWidth + 0.001; //TODO: fix for equal width issue; + lines = measurer.MeasureAndWrapTextLines(textIfEmpty, p.DefaultRunProperties.GetMeasureFont(), maxWidth); + } + else + { + lines = WrapToSimpleTextLines(p, _textFragments); + } + foreach (var line in lines) { double prevWidth = 0; - - if(lineSpacingIsExact == false) + + if (lineSpacingIsExact == false) { runLineSpacing += line.LargestAscent + lastDescent; } @@ -287,7 +209,7 @@ private void AddLinesAndTextRuns(ExcelDrawingParagraph p, string textIfEmpty) { runLineSpacing += ParagraphLineSpacing; } - if(line.Width > greatestWidth) + if (line.Width > greatestWidth) { greatestWidth = line.Width; } @@ -295,7 +217,18 @@ private void AddLinesAndTextRuns(ExcelDrawingParagraph p, string textIfEmpty) foreach (var rtFragment in line.RtFragments) { var displayText = line.GetFragmentText(rtFragment); - var runItem = AddRenderItemTextRun(p.TextRuns[rtFragment.Fragidx], displayText, prevWidth, runLineSpacing); + + if(p.TextRuns.Count == 0 && string.IsNullOrEmpty(textIfEmpty) == false) + { + AddText(displayText, p.DefaultRunProperties); + } + else + { + AddRenderItemTextRun(p.TextRuns[rtFragment.Fragidx], displayText, prevWidth, runLineSpacing); + } + + TextRunItem runItem = Runs.Last(); + runItem.Bounds.Width = rtFragment.Width; prevWidth += rtFragment.Width; } @@ -303,124 +236,45 @@ private void AddLinesAndTextRuns(ExcelDrawingParagraph p, string textIfEmpty) lastDescent = line.LargestDescent; } Bounds.Height = runLineSpacing + lastDescent; - //Bounds.Width = greatestWidth; - //string currentLine = _paragraphLines[0]; - //int currentLineIdx = 0; - - //double widthOfCurrentLine = 0; - //double largestFontSizeCurrentLine = 0; - //int idxLargestFontSize = 0; - //int firstRunInLineIdx = 0; - - ////var lineSizes = _textFragments.GetLargestFontSizesOfEachLine(); - ////var currentLineSize = lineSizes[currentLineIdx]; - //double lineSpacing = 0; - //if(_lsMultiplier.HasValue == false) + //Bounds.Width = Runs.Sum(x => x.Bounds.Width); + + //if (p.TextRuns.Count == 0 && string.IsNullOrEmpty(textIfEmpty) == false) //{ - // //linespacing is exact - // lineSpacing = ParagraphLineSpacing; + // AddText(textIfEmpty, p.DefaultRunProperties); + // Bounds.Width = Runs.Sum(x => x.Bounds.Width); + //} + //else + //{ + // lines = WrapToSimpleTextLines(p, _textFragments); + // foreach (var line in lines) + // { + // double prevWidth = 0; + + // if (lineSpacingIsExact == false) + // { + // runLineSpacing += line.LargestAscent + lastDescent; + // } + // else + // { + // runLineSpacing += ParagraphLineSpacing; + // } + // if (line.Width > greatestWidth) + // { + // greatestWidth = line.Width; + // } + + // foreach (var rtFragment in line.RtFragments) + // { + // var displayText = line.GetFragmentText(rtFragment); + // var runItem = AddRenderItemTextRun(p.TextRuns[rtFragment.Fragidx], displayText, prevWidth, runLineSpacing); + // runItem.Bounds.Width = rtFragment.Width; + // prevWidth += rtFragment.Width; + // } + + // lastDescent = line.LargestDescent; + // } + // Bounds.Height = runLineSpacing + lastDescent; //} - - if (p.TextRuns.Count == 0 && string.IsNullOrEmpty(textIfEmpty) == false) - { - AddText(textIfEmpty, p.DefaultRunProperties); - Bounds.Width = Runs.Sum(x=>x.Bounds.Width); - } - else - { - //for (int i = 0; i < p.TextRuns.Count; i++) - //{ - // AddRenderItemTextRun(p.TextRuns[i], _textRunDisplayText[i], widthOfCurrentLine); - // var lastAdded = Runs.Last(); - - // lastAdded.SetPerLineWidths(_textFragments.GetFragmentWidths(i)); - - // if (lastAdded.Lines.Count > 1) - // { - // //Just in case. Should always be empty here - // lastAdded.YIncreasePerLine.Clear(); - - // //We are on a new line - // for (int j = 0; j < lastAdded.Lines.Count; j++) - // { - // currentLine = _paragraphLines[currentLineIdx]; - - // if (_lsMultiplier.HasValue) - // { - // //Add ascent to descent (Add ascent to Nothing for the first run) - // lineSpacing += _textFragments.GetAscent(currentLineIdx).PointToPixel() * _lsMultiplier.Value; - // } - - // lastAdded.AddLineSpacing(lineSpacing); - - // if (_lsMultiplier.HasValue) - // { - // //Set linespacing to descent - // lineSpacing = _textFragments.GetDescent(currentLineIdx).PointToPixel(); - // } - - // //Last line in added lines we will continue on if there are more textruns - // //Therefore the index will be added to after the next line-break or at the end - // if (j < lastAdded.Lines.Count - 1) - // { - // Bounds.Height += lastAdded.Bounds.Height; - // currentLineIdx++; - // } - // } - - // widthOfCurrentLine = Runs.Last().PerLineWidth.Last(); - // } - // else - // { - // widthOfCurrentLine += Runs.Last().PerLineWidth.Last(); - - // //If we are on the last run - // if (i == p.TextRuns.Count - 1) - // { - // if (_lsMultiplier.HasValue) - // { - // //currentLineIdx++; - // //Add ascent to descent (Add ascent to Nothing for the first run) - // lineSpacing += _textFragments.GetAscent(currentLineIdx).PointToPixel() * _lsMultiplier.Value; - // } - // lastAdded.AddLineSpacing(lineSpacing); - // Bounds.Height += lastAdded.Bounds.Height; - // //Runs[idxLargestFontSize].GetBounds(out double l, out double t, out double r, out double b); - // //Bounds.Height += Runs[idxLargestFontSize].Bounds.Height; - // } - // } - // Bounds.Width = widthOfCurrentLine; - //} - } - } - - private void CalculateTextLines(ExcelDrawingParagraph p, TextFragmentCollection fragments) - { - WrapToSimpleTextLines(p, fragments); - } - - /// - /// Use textfragments to calculate wrapping/line-breaks - /// - /// - private void CalculateDisplayText(ExcelDrawingParagraph p, TextFragmentCollection fragments) - { - //Gets the individual lines free of any line breaks - if (p._paragraphs.WrapText != eTextWrappingType.None) - { - _paragraphLines = GetWrappedText(p.TextRuns, fragments); - } - else - { - //Using Regex to avoid empty lines in windows. - //Which the alternative "paragraph.Text.Split(new [] '\r' '\n')" would result in. - _paragraphLines = Regex.Split(p.Text, "\r\n|\r|\n").ToList(); - } - //Gets each actual text run, with linebreak symbols - _textRunDisplayText = fragments.GetFragmentsWithFinalLineBreaks(); - //var test = fragments.GetFragmentsWithoutLineBreaks(); - //var - //var lineToRunMapping = } List WrapToSimpleTextLines(ExcelDrawingParagraph p, TextFragmentCollection fragments) @@ -460,15 +314,6 @@ internal double GetAlignmentHorizontal(eTextAlignment txAlignment) return TextUtils.RoundToWhole(x); } - //internal override void GetBounds(out double il, out double it, out double ir, out double ib) - //{ - // il = Bounds.Left + _leftMargin; - // it = Bounds.Top; - // ir = Bounds.Right - _rightMargin; - // ib = Bounds.Bottom; - //} - - /// /// Type of textrun defined by child type /// diff --git a/src/EPPlus.Export.ImageRenderer/RenderItems/Shared/TextRunItem.cs b/src/EPPlus.Export.ImageRenderer/RenderItems/Shared/TextRunItem.cs index 5ff296117..977b89055 100644 --- a/src/EPPlus.Export.ImageRenderer/RenderItems/Shared/TextRunItem.cs +++ b/src/EPPlus.Export.ImageRenderer/RenderItems/Shared/TextRunItem.cs @@ -61,10 +61,10 @@ internal TextRunItem(DrawingBase renderer, BoundingBox parent, string text, Exce Lines = Regex.Split(_currentText, "\r\n|\r|\n").ToList(); - _measurer = new FontMeasurerTrueType(); + //_measurer = new FontMeasurerTrueType(); _measurementFont = font.GetMeasureFont(); - _measurer.SetFont(_measurementFont); + //_measurer.SetFont(_measurementFont); _isFirstInParagraph = true; diff --git a/src/EPPlus.Fonts.OpenType/TrueTypeMeasurer/FontMeasurerTrueType.cs b/src/EPPlus.Fonts.OpenType/TrueTypeMeasurer/FontMeasurerTrueType.cs index 6d0d307a3..9ccb8793d 100644 --- a/src/EPPlus.Fonts.OpenType/TrueTypeMeasurer/FontMeasurerTrueType.cs +++ b/src/EPPlus.Fonts.OpenType/TrueTypeMeasurer/FontMeasurerTrueType.cs @@ -239,6 +239,13 @@ public List MeasureAndWrapText(string text, MeasurementFont font, double return wrappedStrings; } + public List MeasureAndWrapTextLines(string text, MeasurementFont font, double MaxWidthInPixels, double preExistingWidthPixels = 0) + { + SetFont(font.Size, font.FontFamily); + var wrappedStrings = TextData.MeasureAndWrapTextLines(text, FontSize, CurrentFont, MaxWidthInPixels.PixelToPoint(), preExistingWidthPixels.PixelToPoint()); + return wrappedStrings; + } + public List MeasureAndWrapTextPoints(string text, MeasurementFont font, double MaxWidthInPoints) { SetFont(font.Size, font.FontFamily); diff --git a/src/EPPlus.Fonts.OpenType/TrueTypeMeasurer/TextData.cs b/src/EPPlus.Fonts.OpenType/TrueTypeMeasurer/TextData.cs index f0713bdbb..2ab9da671 100644 --- a/src/EPPlus.Fonts.OpenType/TrueTypeMeasurer/TextData.cs +++ b/src/EPPlus.Fonts.OpenType/TrueTypeMeasurer/TextData.cs @@ -344,6 +344,7 @@ private static int CalcGlyphWidth(GlyphMappings glyphMappings, char c, OpenTypeF return advanceWidth; } + private static void MeasureAndWrapLines(string text, ref int totalAdvanceWidth, ref int totalWordWidth, OpenTypeFont fontData, GlyphMappings glyphMappings, ushort? lastGlyphIndex, double maxWidth, List wrappedStrings, bool applyKerning = true) { //Handle line-endings @@ -364,6 +365,26 @@ private static void MeasureAndWrapLines(string text, ref int totalAdvanceWidth, } } + private static void MeasureAndWrapTextLines(string text, ref int totalAdvanceWidth, ref int totalWordWidth, OpenTypeFont fontData, GlyphMappings glyphMappings, ushort? lastGlyphIndex, double maxWidth, List wrappedStrings, bool applyKerning = true) + { + //Handle line-endings + var splitStrings = text.Split([Environment.NewLine], StringSplitOptions.None); + + if (splitStrings.Length != 0) + { + //Avoid using kerning for first char/line + MeasureAndWrapTextLine(splitStrings[0], fontData, ref totalAdvanceWidth, ref totalWordWidth, glyphMappings, lastGlyphIndex, maxWidth, wrappedStrings, false); + + if (splitStrings.Length > 1) + { + for (int i = 1; i < splitStrings.Count(); i++) + { + MeasureAndWrapTextLine(splitStrings[i], fontData, ref totalAdvanceWidth, ref totalWordWidth, glyphMappings, lastGlyphIndex, maxWidth, wrappedStrings); + } + } + } + } + private static int CalculateAdvanceWidth(char c, GlyphMappings mappings, OpenTypeFont font, ref ushort? lastGlyphIndex, ref int lineWidth, ref int wordWidth, ref bool applyKerning) { int advanceWidth = CalcGlyphWidth(mappings, c, font, ref lastGlyphIndex, ref applyKerning); @@ -391,6 +412,38 @@ private static void WrapAtCharPos(string line, int charPos, ref int nextLineStar wordWidth = lineWidth; } + private static void MeasureAndWrapTextLine(string line, OpenTypeFont font, ref int lineWidth, ref int wordWidth, GlyphMappings glyphMappings, ushort? lastGlyphIndex, double maxWidth, List wrappedStrings, bool applyKerning = true) + { + int nextLineStartIndex = 0; + var spaceWidth = CalcGlyphWidth(glyphMappings, ' ', font, ref lastGlyphIndex, ref applyKerning); + var actualLine = new TextLineSimple(); + for (int i = 0; i < line.Length; i++) + { + char c = line[i]; + var advanceWidth = CalculateAdvanceWidth(c, glyphMappings, font, ref lastGlyphIndex, ref lineWidth, ref wordWidth, ref applyKerning); + + if (lineWidth >= maxWidth) + { + List tmpLst = new List(); + WrapAtCharPos(line, i, ref nextLineStartIndex, ref lineWidth, ref wordWidth, advanceWidth, tmpLst, spaceWidth, out int prevLineWidth); + + actualLine.Text = line; + actualLine.Width = prevLineWidth; + } + } + + var leftover = new TextLineSimple(); + var remainingLine = line.Substring(nextLineStartIndex); + leftover.Text = remainingLine; + leftover.Width = maxWidth; + + wrappedStrings.Add(leftover); + + //Has to be done After instead of before for loop. + //For the case that we enter with an existing line width + lineWidth = 0; + } + private static void MeasureAndWrapLine(string line, OpenTypeFont font, ref int lineWidth, ref int wordWidth, GlyphMappings glyphMappings, ushort? lastGlyphIndex, double maxWidth, List wrappedStrings, bool applyKerning = true) { int nextLineStartIndex = 0; @@ -454,13 +507,69 @@ internal static List MeasureAndWrapText(string text, double fontSize, Op } // Execute: - MeasureAndWrapLines(text, ref totalAdvanceWidth, ref wordWidth, fontData, glyphMappings, lastGlyphIndex, maxWidthInDesignUnits, wrappedStrings); return wrappedStrings; } + /// + /// Measures the text and breaks it into smaller strings so that none exceed the MaxWidth + /// + /// + /// + /// + /// + /// point size previously calculated width of starting line + /// + /// + internal static List MeasureAndWrapTextLines(string text, double fontSize, OpenTypeFont fontData, double maxWidth, double preExistingLineWidth = 0, double preExistingWordWidth = 0) + { + // Initialize: + int totalAdvanceWidth = 0; + ushort? lastGlyphIndex = 0; + + //Initalise collection to return + List wrappedStrings = new List(); + + var inputMaxWidth = maxWidth; + //Convert maxWidth from points to font design units + var maxWidthInDesignUnits = Math.Round(((inputMaxWidth * (double)fontData.HeadTable.UnitsPerEm) / fontSize), 0, MidpointRounding.AwayFromZero); + + var glyphMappings = fontData.CmapTable.GetPreferredSubtable().GetGlyphMappings(); + + int wordWidth = 0; + + //If the starting line width is not zero. + //Happens e.g. if other chars on the starting line have been measured with a different font. + if (preExistingLineWidth != 0) + { + //Convert from points to current fonts font design units + totalAdvanceWidth = Convert.ToInt16((preExistingLineWidth * (double)fontData.HeadTable.UnitsPerEm) / fontSize); + } + if (preExistingWordWidth != 0) + { + wordWidth = Convert.ToInt16((preExistingWordWidth * (double)fontData.HeadTable.UnitsPerEm) / fontSize); + } + + // Execute: + MeasureAndWrapTextLines(text, ref totalAdvanceWidth, ref wordWidth, fontData, glyphMappings, lastGlyphIndex, maxWidthInDesignUnits, wrappedStrings); + + var designUnitsToPoints = fontSize / (double)fontData.HeadTable.UnitsPerEm; + + //Translate widths back to points + for (int i = 0; i< wrappedStrings.Count; i++) + { + wrappedStrings[i].Width *= designUnitsToPoints; + wrappedStrings[i].LargestFontSize = fontSize; + wrappedStrings[i].LargestAscent = GetBaseLine(fontData, fontSize); + wrappedStrings[i].LargestDescent = MeasureDescent(fontData, fontSize); + } + + return wrappedStrings; + } + + /// /// If wrap text is true returns the largest width before newLine characters @@ -819,12 +928,6 @@ internal static List WrapMultipleTextFragments(TextParagraph paragraph, var prevFragmentIdx = 0; var prevFragWidths = 0; - ////Technically currentLineOfFragmentWidth - //int fragmentWidth = 0; - ////Technically lastLINEofFragmentWidth - ////lastFragmentWidth could be a different fragment OR current fragment on a previous line - ////Whichever the previous measured piece of text was - //int lastFragmentWidth = 0; //---Logger Variables end--- int spaceWidth = 0; @@ -1008,7 +1111,7 @@ internal static List WrapMultipleTextFragmentsToTextLines(TextPa int prevLineBreakIndex = 0; - //---Logger variables--- + //---RichTextFragment variables--- var fragmentCollection = paragraph.Fragments; var allText = fragmentCollection.AllText; var newLineIndicies = fragmentCollection.AllTextNewLineIndicies; @@ -1017,13 +1120,7 @@ internal static List WrapMultipleTextFragmentsToTextLines(TextPa var prevFragmentIdx = 0; var prevFragWidths = 0; - ////Technically currentLineOfFragmentWidth - //int fragmentWidth = 0; - ////Technically lastLINEofFragmentWidth - ////lastFragmentWidth could be a different fragment OR current fragment on a previous line - ////Whichever the previous measured piece of text was - //int lastFragmentWidth = 0; - //---Logger Variables end--- + //---RichTextFragment Variables end--- int spaceWidth = 0; From 96945c5e677e768c320de1bc96ed710cda37d890 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ossian=20Edstr=C3=B6m?= Date: Wed, 4 Feb 2026 16:31:13 +0100 Subject: [PATCH 041/151] Made it work for charts/regular text --- .../TextRenderTests.cs | 29 +++++-------------- .../RenderItems/Shared/ParagraphItem.cs | 19 ++++++++++-- .../RenderItems/Shared/TextRunItem.cs | 2 +- .../RenderItems/SvgItem/SvgTextBoxItem.cs | 6 ++-- .../RenderItems/SvgItem/SvgTextRunItem.cs | 4 +-- .../Svg/Chart/SvgChartTitle.cs | 2 +- .../Svg/SvgShape.cs | 2 +- .../DataHolders/RichTextFragmentSimple.cs | 8 ++++- .../DataHolders/TextLineSimple.cs | 5 ++++ .../TrueTypeMeasurer/TextData.cs | 13 ++++++++- 10 files changed, 56 insertions(+), 34 deletions(-) diff --git a/src/EPPlus.Export.ImageRenderer.Test/TextRenderTests.cs b/src/EPPlus.Export.ImageRenderer.Test/TextRenderTests.cs index 34a5d626a..088d9137d 100644 --- a/src/EPPlus.Export.ImageRenderer.Test/TextRenderTests.cs +++ b/src/EPPlus.Export.ImageRenderer.Test/TextRenderTests.cs @@ -218,32 +218,19 @@ public void VerifyTextRunBounds() cube.GetSizeInPixels(out int testWidth, out int testHeight); + //cube.SetPixelWidth(300); + //cube.SetPixelHeight(300); + var parentBB = new BoundingBox(); parentBB.Width = testWidth; parentBB.Height = testHeight; var svgShape = new SvgShape(cube); - SvgTextBodyItem tbItem = new SvgTextBodyItem(svgShape, parentBB); - - ////Verify new method works at all - //for(int i = 1; i< cube.TextBody.Paragraphs.Count; i++) - //{ - // var fragments = GenerateTextFragments(cube.TextBody.Paragraphs[i].TextRuns); - - // var wrappedText = GetWrappedText(cube.TextBody.Paragraphs[i].TextRuns, fragments); - // if(i == 0) - // { - // Assert.AreEqual("TextBox", wrappedText[0].Text); - // Assert.AreEqual("a", wrappedText[1].Text); - // } - // if(i == 1) - // { - // Assert.AreEqual("TextBox2ra underlineLa", wrappedText[0].Text); - // Assert.AreEqual("StrikeGoudy size", wrappedText[1].Text); - // Assert.AreEqual("16SvgSize 24", wrappedText[2].Text); - // } - //} - tbItem.ImportTextBody(cube.TextBody); + + SvgTextBodyItem tbItem = svgShape.TextBox.TextBody; + //SvgTextBodyItem tbItem = new SvgTextBodyItem(svgShape, parentBB); + + //tbItem.ImportTextBody(cube.TextBody); var txtRun1Bounds = tbItem.Paragraphs[0].Runs[0].Bounds; diff --git a/src/EPPlus.Export.ImageRenderer/RenderItems/Shared/ParagraphItem.cs b/src/EPPlus.Export.ImageRenderer/RenderItems/Shared/ParagraphItem.cs index 0a8e2d40e..8dadf61b3 100644 --- a/src/EPPlus.Export.ImageRenderer/RenderItems/Shared/ParagraphItem.cs +++ b/src/EPPlus.Export.ImageRenderer/RenderItems/Shared/ParagraphItem.cs @@ -129,16 +129,28 @@ private double GetParagraphLineSpacingInPixels(double spacingValue, ITextMeasure } } - internal protected TextRunItem AddRenderItemTextRun(ExcelParagraphTextRunBase origTxtRun, string displayText, double startingX, double lineSpacing) + internal protected TextRunItem AddRenderItemTextRun(ExcelParagraphTextRunBase origTxtRun, string displayText, double startingX) { var targetTxtRun = CreateTextRun(origTxtRun, Bounds, displayText); - targetTxtRun.lineSpacing = lineSpacing; targetTxtRun.Bounds.Left = startingX; Runs.Add(targetTxtRun); return targetTxtRun; } + public void AddText(string text, ExcelTextFont font, bool isOld) + { + var measurer = new FontMeasurerTrueType(); + var displayText = measurer.MeasureAndWrapText(text, font.GetMeasureFont(), ParentTextBody.MaxWidth); + var container = CreateTextRun(text, font, Bounds, string.Join("\r\n", displayText.ToArray())); + //container.BaseLineSpacing = _lineSpacingAscendantOnly; + //container.LineSpacingPerNewLine = _lsMultiplier.Value * _measurer.GetSingleLineSpacing().PointToPixel(true); + Runs.Add(container); + Bounds.Width = container.Bounds.Width + 0.001; //TODO: fix for equal width issue + container.Bounds.Name = $"Container{Runs.Count}"; + } + + public void AddText(string text, ExcelTextFont font) { var measurer = new FontMeasurerTrueType(); @@ -224,10 +236,11 @@ private void AddLinesAndTextRuns(ExcelDrawingParagraph p, string textIfEmpty) } else { - AddRenderItemTextRun(p.TextRuns[rtFragment.Fragidx], displayText, prevWidth, runLineSpacing); + AddRenderItemTextRun(p.TextRuns[rtFragment.Fragidx], displayText, prevWidth); } TextRunItem runItem = Runs.Last(); + runItem.YPosition = runLineSpacing; runItem.Bounds.Width = rtFragment.Width; prevWidth += rtFragment.Width; diff --git a/src/EPPlus.Export.ImageRenderer/RenderItems/Shared/TextRunItem.cs b/src/EPPlus.Export.ImageRenderer/RenderItems/Shared/TextRunItem.cs index 977b89055..61bfd517c 100644 --- a/src/EPPlus.Export.ImageRenderer/RenderItems/Shared/TextRunItem.cs +++ b/src/EPPlus.Export.ImageRenderer/RenderItems/Shared/TextRunItem.cs @@ -44,7 +44,7 @@ internal abstract class TextRunItem : RenderItem protected internal eStrikeType _strikeType; protected internal Color _underlineColor; - internal double lineSpacing { get; set; } + internal double YPosition { get; set; } //internal double LineSpacingPerNewLine { get; set; } //internal double BaseLineSpacing { get; set; } diff --git a/src/EPPlus.Export.ImageRenderer/RenderItems/SvgItem/SvgTextBoxItem.cs b/src/EPPlus.Export.ImageRenderer/RenderItems/SvgItem/SvgTextBoxItem.cs index dd3608e5a..ddca27d1e 100644 --- a/src/EPPlus.Export.ImageRenderer/RenderItems/SvgItem/SvgTextBoxItem.cs +++ b/src/EPPlus.Export.ImageRenderer/RenderItems/SvgItem/SvgTextBoxItem.cs @@ -74,9 +74,9 @@ internal override void AppendRenderItems(List renderItems) } renderItems.Add(groupItem); //handled by group now - //Rectangle.Bounds.Top = 0; - //Rectangle.Bounds.Left = 0; - //renderItems.Add(Rectangle); + Rectangle.Bounds.Top = 0; + Rectangle.Bounds.Left = 0; + renderItems.Add(Rectangle); TextBody.AppendRenderItems(renderItems); renderItems.Add(new SvgEndGroupItem(DrawingRenderer, Bounds)); } diff --git a/src/EPPlus.Export.ImageRenderer/RenderItems/SvgItem/SvgTextRunItem.cs b/src/EPPlus.Export.ImageRenderer/RenderItems/SvgItem/SvgTextRunItem.cs index 6af09af6f..cb8c98de0 100644 --- a/src/EPPlus.Export.ImageRenderer/RenderItems/SvgItem/SvgTextRunItem.cs +++ b/src/EPPlus.Export.ImageRenderer/RenderItems/SvgItem/SvgTextRunItem.cs @@ -92,10 +92,10 @@ public override void Render(StringBuilder sb) string visibility = ""; finalString += xString; - var yString = $" y=\"{lineSpacing.PointToPixel()}px\" "; + var yString = $" y=\"{YPosition.PointToPixel()}px\" "; finalString += yString; - currentYEndPos += lineSpacing; + currentYEndPos += YPosition; if (double.IsNaN(ClippingHeight) == false && currentYEndPos >= ClippingHeight) { diff --git a/src/EPPlus.Export.ImageRenderer/Svg/Chart/SvgChartTitle.cs b/src/EPPlus.Export.ImageRenderer/Svg/Chart/SvgChartTitle.cs index 092f3f85b..1f1308d00 100644 --- a/src/EPPlus.Export.ImageRenderer/Svg/Chart/SvgChartTitle.cs +++ b/src/EPPlus.Export.ImageRenderer/Svg/Chart/SvgChartTitle.cs @@ -171,7 +171,7 @@ private static string GetDefaultChartTitleText(SvgChart sc, ExcelChartTitleStand internal void InitTextBox() { - TextBox = new SvgTextBoxItem(_svgChart, _svgChart.ChartArea.Bounds, Rectangle.Left, Rectangle.Top, Rectangle.Width, Rectangle.Height); + TextBox = new SvgTextBoxItem(_svgChart, _svgChart.ChartArea.Bounds, Rectangle.Left, Rectangle.Top, Rectangle.Width, Rectangle.Height, Rectangle.Width, Rectangle.Height); TextBox.LeftMargin = LeftMargin; TextBox.RightMargin = RightMargin; TextBox.TopMargin = TopMargin; diff --git a/src/EPPlus.Export.ImageRenderer/Svg/SvgShape.cs b/src/EPPlus.Export.ImageRenderer/Svg/SvgShape.cs index cb5b8bc4d..3ace69702 100644 --- a/src/EPPlus.Export.ImageRenderer/Svg/SvgShape.cs +++ b/src/EPPlus.Export.ImageRenderer/Svg/SvgShape.cs @@ -43,7 +43,7 @@ internal class SvgShape : DrawingShape /// /// Textbox from memory /// - SvgTextBoxItem TextBox; + public SvgTextBoxItem TextBox { get; internal set; } public SvgShape(ExcelShape shape) : base(shape) { diff --git a/src/EPPlus.Fonts.OpenType/TrueTypeMeasurer/DataHolders/RichTextFragmentSimple.cs b/src/EPPlus.Fonts.OpenType/TrueTypeMeasurer/DataHolders/RichTextFragmentSimple.cs index 16181dce3..654434d6a 100644 --- a/src/EPPlus.Fonts.OpenType/TrueTypeMeasurer/DataHolders/RichTextFragmentSimple.cs +++ b/src/EPPlus.Fonts.OpenType/TrueTypeMeasurer/DataHolders/RichTextFragmentSimple.cs @@ -17,7 +17,13 @@ public class RichTextFragmentSimple /// public double Width { get; internal set; } - public RichTextFragmentSimple() { } + public RichTextFragmentSimple() + { + Width = 0; + Fragidx = 0; + OverallParagraphStartCharIdx = 0; + charStarIdxWithinCurrentLine = 0; + } internal RichTextFragmentSimple Clone() { diff --git a/src/EPPlus.Fonts.OpenType/TrueTypeMeasurer/DataHolders/TextLineSimple.cs b/src/EPPlus.Fonts.OpenType/TrueTypeMeasurer/DataHolders/TextLineSimple.cs index 1dcb6bf00..bda52fe14 100644 --- a/src/EPPlus.Fonts.OpenType/TrueTypeMeasurer/DataHolders/TextLineSimple.cs +++ b/src/EPPlus.Fonts.OpenType/TrueTypeMeasurer/DataHolders/TextLineSimple.cs @@ -52,6 +52,11 @@ public string GetFragmentText(RichTextFragmentSimple rtFragment) throw new InvalidOperationException($"GetFragmentText failed. Cannot retrieve {rtFragment} since it is not part of this textLine: {this}"); } + if(string.IsNullOrEmpty(Text)) + { + return Text; + } + var startIdx = rtFragment.charStarIdxWithinCurrentLine; var idxInLst = RtFragments.FindIndex(x => x == rtFragment); diff --git a/src/EPPlus.Fonts.OpenType/TrueTypeMeasurer/TextData.cs b/src/EPPlus.Fonts.OpenType/TrueTypeMeasurer/TextData.cs index 2ab9da671..b4967b800 100644 --- a/src/EPPlus.Fonts.OpenType/TrueTypeMeasurer/TextData.cs +++ b/src/EPPlus.Fonts.OpenType/TrueTypeMeasurer/TextData.cs @@ -435,7 +435,7 @@ private static void MeasureAndWrapTextLine(string line, OpenTypeFont font, ref i var leftover = new TextLineSimple(); var remainingLine = line.Substring(nextLineStartIndex); leftover.Text = remainingLine; - leftover.Width = maxWidth; + leftover.Width = lineWidth; wrappedStrings.Add(leftover); @@ -564,6 +564,12 @@ internal static List MeasureAndWrapTextLines(string text, double wrappedStrings[i].LargestFontSize = fontSize; wrappedStrings[i].LargestAscent = GetBaseLine(fontData, fontSize); wrappedStrings[i].LargestDescent = MeasureDescent(fontData, fontSize); + + //Add "dummy" richtext item + var rtItem = new RichTextFragmentSimple(); + rtItem.Width = wrappedStrings[i].Width; + + wrappedStrings[i].RtFragments.Add(rtItem); } return wrappedStrings; @@ -1083,6 +1089,11 @@ internal static List WrapMultipleTextFragments(TextParagraph paragraph, internal static List WrapMultipleTextFragmentsToTextLines(TextParagraph paragraph, double maxWidthPoints) { + if(string.IsNullOrEmpty(paragraph.Fragments.AllText)) + { + return new List(); + } + //Initialize variables List outputTextLines = new List(); List currentLineRtFragments = new List(); From d9d9183102f60bb588d97a7dfc44d9bed5695141 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jan=20K=C3=A4llman?= Date: Thu, 5 Feb 2026 10:10:39 +0100 Subject: [PATCH 042/151] WIP:Added glow effect for chart objects. --- .../SvgPathTests.cs | 24 +++--- .../RenderItems/RenderItem.cs | 14 ++++ .../RenderItems/SvgBaseRenderer.cs | 1 - .../RenderItems/SvgRenderPathItem.cs | 3 +- .../Svg/Chart/Axis/AxisOptions.cs | 3 +- .../Chart/Axis/ValueAxisScaleCalculator.cs | 21 ++++- .../ChartTypeDrawers/LineChartTypeDrawer.cs | 61 ++++++++------ .../Svg/Chart/SvgChartAxis.cs | 22 ++--- .../Utils/SvgDrawingWriter.cs | 56 +++++++++---- .../Drawing/Chart/ExcelChartAxisStandard.cs | 84 +++++++++++++++++-- src/EPPlusTest/Issues/WorksheetIssues.cs | 1 - 11 files changed, 216 insertions(+), 74 deletions(-) diff --git a/src/EPPlus.Export.ImageRenderer.Test/SvgPathTests.cs b/src/EPPlus.Export.ImageRenderer.Test/SvgPathTests.cs index e2bd8ad66..c60f4eedb 100644 --- a/src/EPPlus.Export.ImageRenderer.Test/SvgPathTests.cs +++ b/src/EPPlus.Export.ImageRenderer.Test/SvgPathTests.cs @@ -511,20 +511,20 @@ public void GenerateSvgForCharts() { var ws = p.Workbook.Worksheets[0]; var renderer = new EPPlusImageRenderer.ImageRenderer(); - var ix = 0; - var c = ws.Drawings[ix]; - var svg = renderer.RenderDrawingToSvg(c); - SaveTextFileToWorkbook($"svg\\ChartForSvg_ind{ix++}.svg", svg); - //var ix = 1; - //foreach (ExcelChart c in ws.Drawings) - //{ - // var svg = renderer.RenderDrawingToSvg(c); - // SaveTextFileToWorkbook($"svg\\ChartForSvg{ix++}.svg", svg); - //} + //var ix = 0; + //var c = ws.Drawings[ix]; + //var svg = renderer.RenderDrawingToSvg(c); + //SaveTextFileToWorkbook($"svg\\ChartForSvg_ind{ix++}.svg", svg); + var ix = 1; + foreach (ExcelChart c in ws.Drawings) + { + var svg = renderer.RenderDrawingToSvg(c); + SaveTextFileToWorkbook($"svg\\ChartForSvg{ix++}.svg", svg); + } } } [TestMethod] - public void GenerateSvgForLineCharts() + public void GenerateSvgForLineCharts() { ExcelPackage.License.SetNonCommercialOrganization("EPPlus Project"); using (var p = OpenTemplatePackage("LineChartRenderTest.xlsx")) @@ -534,7 +534,7 @@ public void GenerateSvgForLineCharts() //var ix = 1; //var c = ws.Drawings[ix]; //var svg = renderer.RenderDrawingToSvg(c); - //SaveTextFileToWorkbook($"svg\\ChartForSvg{ix++}.svg", svg); + //SaveTextFileToWorkbook($"svg\\LineChartForSvg_Single{ix++}.svg", svg); var ix = 1; foreach (ExcelChart c in ws.Drawings) { diff --git a/src/EPPlus.Export.ImageRenderer/RenderItems/RenderItem.cs b/src/EPPlus.Export.ImageRenderer/RenderItems/RenderItem.cs index a2caf4536..c2cc6dc63 100644 --- a/src/EPPlus.Export.ImageRenderer/RenderItems/RenderItem.cs +++ b/src/EPPlus.Export.ImageRenderer/RenderItems/RenderItem.cs @@ -15,8 +15,10 @@ Date Author Change using OfficeOpenXml.Drawing; using OfficeOpenXml.Drawing.Chart.Style; using OfficeOpenXml.Drawing.Style.Coloring; +using OfficeOpenXml.Drawing.Style.Effect; using OfficeOpenXml.Drawing.Style.Fill; using OfficeOpenXml.Drawing.Theme; +using OfficeOpenXml.Style; using System; using System.Drawing; using System.Text; @@ -63,6 +65,8 @@ internal virtual void GetBounds(out double il, out double it, out double ir, out public double? BorderOpacity { get; set; } public PathFillMode FillColorSource { get; set; } = PathFillMode.Norm; public PathFillMode BorderColorSource { get; set; } = PathFillMode.Norm; + public double? GlowRadius { get; private set; } + public string GlowColor { get; private set; } protected void CloneBase(RenderItem item) { @@ -154,6 +158,16 @@ internal virtual void SetDrawingPropertiesBorder(ExcelDrawingBorder border, Exce } } } + internal void SetDrawingPropertiesEffects(ExcelDrawingEffectStyle effect) + { + if (effect.HasGlow) + { + GlowRadius = effect.Glow.Radius; + var gc = EPPlusColorConverter.GetThemeColor(DrawingRenderer.Theme, effect.Glow.Color); + GlowColor = "#" + gc.ToArgb().ToString("x8").Substring(2); + } + } + private double[] GetDashArray(ExcelDrawingBorder border) { var lw = (int)Math.Round(border.Width * ExcelDrawing.EMU_PER_POINT / ExcelDrawing.EMU_PER_PIXEL); diff --git a/src/EPPlus.Export.ImageRenderer/RenderItems/SvgBaseRenderer.cs b/src/EPPlus.Export.ImageRenderer/RenderItems/SvgBaseRenderer.cs index 645718edd..3cf77c09a 100644 --- a/src/EPPlus.Export.ImageRenderer/RenderItems/SvgBaseRenderer.cs +++ b/src/EPPlus.Export.ImageRenderer/RenderItems/SvgBaseRenderer.cs @@ -42,7 +42,6 @@ public static void BaseRender(StringBuilder sb, RenderItem item) sb.Append($"stroke-dasharray=\"" + $"{string.Join(",", BorderDashArrayStr)}\" "); } } - sb.Append($"stroke-miterlimit =\"8\" "); } } diff --git a/src/EPPlus.Export.ImageRenderer/RenderItems/SvgRenderPathItem.cs b/src/EPPlus.Export.ImageRenderer/RenderItems/SvgRenderPathItem.cs index a30e4e14d..df3e6448e 100644 --- a/src/EPPlus.Export.ImageRenderer/RenderItems/SvgRenderPathItem.cs +++ b/src/EPPlus.Export.ImageRenderer/RenderItems/SvgRenderPathItem.cs @@ -14,7 +14,9 @@ Date Author Change using EPPlus.Graphics; using EPPlusImageRenderer.Svg; using OfficeOpenXml.Drawing; +using OfficeOpenXml.Drawing.Style.Effect; using OfficeOpenXml.Drawing.Theme; +using System; using System.Collections.Generic; using System.Text; @@ -98,5 +100,4 @@ internal override void GetBounds(out double il, out double it, out double ir, ou } } } - } \ No newline at end of file diff --git a/src/EPPlus.Export.ImageRenderer/Svg/Chart/Axis/AxisOptions.cs b/src/EPPlus.Export.ImageRenderer/Svg/Chart/Axis/AxisOptions.cs index a9e2a1f54..04abed062 100644 --- a/src/EPPlus.Export.ImageRenderer/Svg/Chart/Axis/AxisOptions.cs +++ b/src/EPPlus.Export.ImageRenderer/Svg/Chart/Axis/AxisOptions.cs @@ -7,5 +7,6 @@ internal class AxisOptions public double? LockedInterval { get; set; } public eTimeUnit? LockedIntervalUnit { get; set; } public bool AddPadding { get; set; } = false; - public ExcelChartAxisStandard Axis { get; internal set; } + public ExcelChartAxisStandard Axis { get; set; } + public bool IsStacked100 { get; set; } } \ No newline at end of file diff --git a/src/EPPlus.Export.ImageRenderer/Svg/Chart/Axis/ValueAxisScaleCalculator.cs b/src/EPPlus.Export.ImageRenderer/Svg/Chart/Axis/ValueAxisScaleCalculator.cs index cfb04f3ad..8fd6f39f8 100644 --- a/src/EPPlus.Export.ImageRenderer/Svg/Chart/Axis/ValueAxisScaleCalculator.cs +++ b/src/EPPlus.Export.ImageRenderer/Svg/Chart/Axis/ValueAxisScaleCalculator.cs @@ -35,7 +35,10 @@ private static AxisScale GetNumberScale(ref double dataMin, ref double dataMax, var isAllPositive = dataMin > 0 && dataMax > 0; var isAllNegativ = dataMin < 0 && dataMax < 0; - + if(dataMin < 0 && dataMax > 0 && axisOptions.IsStacked100) + { + desiredTicks *= 2; + } double interval; if (axisOptions.LockedInterval.HasValue) { @@ -47,6 +50,10 @@ private static AxisScale GetNumberScale(ref double dataMin, ref double dataMax, if (axisOptions.AddPadding) { dataMin = Math.Floor(dataMin * 0.1 / interval) * interval; + if(axisOptions.IsStacked100 && dataMin < -1) + { + dataMin = -1; + } if (isAllPositive && dataMin < 0) { dataMin = 0; @@ -61,6 +68,10 @@ private static AxisScale GetNumberScale(ref double dataMin, ref double dataMax, if (!axisOptions.LockedMax.HasValue) { dataMax = Math.Ceiling(dataMax * 0.1 / interval) * interval; + if (axisOptions.IsStacked100 && dataMin > 1) + { + dataMax = 1; + } } else { @@ -111,6 +122,10 @@ private static AxisScale GetNumberScale(ref double dataMin, ref double dataMax, else { axisMin = dataMin - (dataRange * 0.1); + if (axisOptions.IsStacked100 && axisMin < -1) + { + axisMin = -1; + } //Normalize to zero if all data is positive or negative if (axisMin < 0 && isAllPositive) { @@ -125,6 +140,10 @@ private static AxisScale GetNumberScale(ref double dataMin, ref double dataMax, else { axisMax = dataMax + (dataRange * 0.1); + if (axisOptions.IsStacked100 && axisMax > 1) + { + axisMax = 1; + } if (axisMax > 0 && isAllNegativ) { diff --git a/src/EPPlus.Export.ImageRenderer/Svg/Chart/ChartTypeDrawers/LineChartTypeDrawer.cs b/src/EPPlus.Export.ImageRenderer/Svg/Chart/ChartTypeDrawers/LineChartTypeDrawer.cs index 1f5fef66a..be422752e 100644 --- a/src/EPPlus.Export.ImageRenderer/Svg/Chart/ChartTypeDrawers/LineChartTypeDrawer.cs +++ b/src/EPPlus.Export.ImageRenderer/Svg/Chart/ChartTypeDrawers/LineChartTypeDrawer.cs @@ -6,6 +6,7 @@ using System.Collections; using System.Collections.Generic; using System.Linq; +using static OfficeOpenXml.ExcelErrorValue; namespace EPPlus.Export.ImageRenderer.Svg.Chart { @@ -16,36 +17,45 @@ internal LineChartTypeDrawer(SvgChart svgChart, ExcelChart chartType) : base(svg var groupItem = new SvgGroupItem(ChartRenderer, _svgChart.Plotarea.Rectangle.Bounds); RenderItems.Add(groupItem); var isStacked = Chart.IsTypeStacked(); - List pXValues = null, pYValues = null; + var isPercentStacked = Chart.IsTypePercentStacked(); + var xValues = new List>(); + var yValues = new List>(); foreach (ExcelLineChartSerie serie in chartType.Series) { - var yValues = LoadSeriesValues(serie.Series, serie.NumberLiteralsY, serie.StringLiteralsY); - var xValues = LoadSeriesValues(serie.XSeries, serie.NumberLiteralsX, serie.StringLiteralsX); - if(isStacked) - { - AddLine(chartType, serie, xValues, SumLists(yValues, pYValues)); - } - else - { - AddLine(chartType, serie, xValues, yValues); - } + var yValue = LoadSeriesValues(serie.Series, serie.NumberLiteralsY, serie.StringLiteralsY); + var xValue = LoadSeriesValues(serie.XSeries, serie.NumberLiteralsX, serie.StringLiteralsX); + xValues.Add(xValue); + yValues.Add(yValue); + } + + if (Chart.IsTypeStacked()) + { + SumSeries(yValues); + } + else if (Chart.IsTypePercentStacked()) + { + ExcelChartAxisStandard.CalculateStacked100(yValues); + } - if (isStacked) - { - pYValues = yValues; - } + for(var i= 0;i < xValues.Count;i++) + { + var xSerie = xValues[i]; + var ySerie = yValues[i]; + var serie = (ExcelLineChartSerie)chartType.Series[i]; + AddLine(chartType, serie, xSerie, ySerie); } + RenderItems.Add(new SvgEndGroupItem(ChartRenderer, null)); } - private List SumLists(List l1, List l2) + private void SumSeries(List> series) { - if (l2 == null) return l1; - var sumList = new List(l1.Count); - for (int i = 0; i < Math.Min(l1.Count, l2.Count); i++) + for(var i=1;i < series.Count;i++) { - sumList.Add(ConvertUtil.GetValueDouble(l1[i]) + ConvertUtil.GetValueDouble(l2[i])); + for(var j=0;j < series[i].Count;j++) + { + series[i][j] = ConvertUtil.GetValueDouble(series[i][j]) + ConvertUtil.GetValueDouble(series[i-1][j]); + } } - return sumList; } private void AddLine(ExcelChart chartType, ExcelLineChartSerie serie, List xValues, List yValues) { @@ -65,17 +75,17 @@ private void AddLine(ExcelChart chartType, ExcelLineChartSerie serie, List 0 ? ConvertUtil.GetValueDouble(Values[Values.Count - 1], false, true) : 0D); - MajorUnit = majorUnit??1; + MajorUnit = majorUnit ?? 1; MinorUnit = ax.MinorUnit ?? GetAutoMinUnit(MajorUnit); if (ax.Deleted == false) { @@ -514,21 +514,20 @@ private ExcelChartStyleEntry GetAxisStyleEntry() return axisStyle; } - internal double GetPositionInPlotarea(object val) + internal double GetPositionInPlotarea(double val) { if (Axis.AxisPosition == eAxisPosition.Left || Axis.AxisPosition == eAxisPosition.Right) { if (Axis.AxisType == eAxisType.Cat) { var majorHeight = SvgChart.Plotarea.Rectangle.Height / Max; - return (majorHeight * (int)val + (majorHeight / 2)); + return (majorHeight * val + (majorHeight / 2)); } else { - var dVal = ConvertUtil.GetValueDouble(val, false, true); - if (dVal < Min || dVal > Max) return double.NaN; + if (val < Min || val > Max) return double.NaN; var diff = Max - Min; - return (((Max-dVal) / diff * SvgChart.Plotarea.Rectangle.Height)); + return (((Max-val) / diff * SvgChart.Plotarea.Rectangle.Height)); } } else @@ -536,14 +535,13 @@ internal double GetPositionInPlotarea(object val) if (Axis.AxisType == eAxisType.Cat) { var majorWidth = SvgChart.Plotarea.Rectangle.Width / Max; - return (majorWidth * (int)val + (majorWidth / 2)); + return (majorWidth * val + (majorWidth / 2)); } else { - var dVal = ConvertUtil.GetValueDouble(val, false, true); - if (dVal < Min || dVal > Max) return double.NaN; + if (val < Min || val > Max) return double.NaN; var diff = Max - Min; - return (((dVal-Min) / diff * SvgChart.Plotarea.Rectangle.Width)); + return (((val-Min) / diff * SvgChart.Plotarea.Rectangle.Width)); } } } @@ -584,8 +582,10 @@ protected List GetAxisValue(ExcelChartAxisStandard ax, RenderItem rect, LockedInterval = ax.MajorUnit, LockedIntervalUnit = ax.MajorTimeUnit, AddPadding = ax.AxisPosition == eAxisPosition.Left || ax.AxisPosition == eAxisPosition.Right, - Axis = ax + Axis = ax, + IsStacked100 = Chart.IsTypePercentStacked() }; + var length = ax.AxisPosition == eAxisPosition.Left || ax.AxisPosition == eAxisPosition.Right ? SvgChart.Bounds.Height : SvgChart.Bounds.Width; //Fix and use plotarea width/height. if (ax.IsDate) { diff --git a/src/EPPlus.Export.ImageRenderer/Utils/SvgDrawingWriter.cs b/src/EPPlus.Export.ImageRenderer/Utils/SvgDrawingWriter.cs index 1cc06e6ff..fb2ec97db 100644 --- a/src/EPPlus.Export.ImageRenderer/Utils/SvgDrawingWriter.cs +++ b/src/EPPlus.Export.ImageRenderer/Utils/SvgDrawingWriter.cs @@ -43,8 +43,10 @@ internal void WriteSvgDefs(StringBuilder sb, List renderItems) { var defSb = new StringBuilder(); var hs = new HashSet(); + var ix = 1; foreach (RenderItem item in renderItems) { + string filter = ""; if (item.GradientFill != null) { string name = WriteGradient("Gradient", defSb, hs, item.GradientFill, item.FillColorSource, true); @@ -57,19 +59,38 @@ internal void WriteSvgDefs(StringBuilder sb, List renderItems) } else if (item.BlipFill != null) { - string name = WriteBlip("Blip", defSb, hs, item.BlipFill, item.FillColorSource); - item.FillColor = $"Url(#{name})"; if (item.FillColorSource != PathFillMode.Norm) { - item.FilterName = $"Url(#{item.FillColorSource}Filter)"; + item.FilterName = GetFilterName(ix); } + + string name = WriteBlip("Blip", defSb, hs, item, ref filter); + item.FillColor = $"Url(#{name})"; } if (item.BorderGradientFill != null) { string name = WriteGradient("StrokeGradient", defSb, hs, item.BorderGradientFill, item.BorderColorSource, true); item.BorderColor = $"Url(#{name})"; } - + if(item.GlowColor!=null) + { + if(string.IsNullOrEmpty(item.FilterName)) + { + var filterName = GetFilterName(ix); + item.FilterName = $"Url(#{filterName})"; + filter = $""; + } + + filter += $"" + + $"" + + $"" + + $""; + } + if(string.IsNullOrEmpty(filter)==false) + { + defSb.Append(filter+""); + } + ix++; } if (defSb.Length > 0) { @@ -78,38 +99,45 @@ internal void WriteSvgDefs(StringBuilder sb, List renderItems) sb.Append(""); } } - private string WriteBlip(string namePrefix, StringBuilder defSb, HashSet hs, ExcelDrawingBlipFill blipFill, PathFillMode fillMode) + + private static string GetFilterName(int ix) + { + return $"item{ix}Filter"; + } + + private string WriteBlip(string namePrefix, StringBuilder defSb, HashSet hs, RenderItem item, ref string filter) { + //, item.BlipFill, item.FillColorSource var name = $"{namePrefix}"; + var fillMode = item.FillColorSource; if (fillMode != PathFillMode.Norm) { - var filterName = $"{fillMode}Filter"; - if (hs.Contains(filterName) == false) + if (hs.Contains(item.FilterName) == false) { switch (fillMode) { case PathFillMode.Lighten: - defSb.Append($""); + filter = $""; break; case PathFillMode.LightenLess: - defSb.Append($""); + filter = $""; break; case PathFillMode.DarkenLess: - defSb.Append($""); + filter = $""; break; case PathFillMode.Darken: - defSb.Append($""); + filter = $""; break; } } - hs.Add(filterName); + hs.Add(item.FilterName); } if (hs.Contains(name)) return name; hs.Add(name); - defSb.Append($""); - defSb.Append($""); + defSb.Append($""); + defSb.Append($""); defSb.Append($""); return name; } diff --git a/src/EPPlus/Drawing/Chart/ExcelChartAxisStandard.cs b/src/EPPlus/Drawing/Chart/ExcelChartAxisStandard.cs index 7059b3cf0..943dad056 100644 --- a/src/EPPlus/Drawing/Chart/ExcelChartAxisStandard.cs +++ b/src/EPPlus/Drawing/Chart/ExcelChartAxisStandard.cs @@ -714,9 +714,50 @@ internal bool IsDate } } + internal bool IsYAxis + { + get + { + foreach(var ct in _chart.PlotArea.ChartTypes) + { + if (ct.YAxis == this) + { + return true; + } + } + return false; + } + } + internal bool IsXAxis + { + get + { + foreach (var ct in _chart.PlotArea.ChartTypes) + { + if (ct.XAxis == this) + { + return true; + } + } + return false; + } + } internal override object[] GetAxisValues(out bool isCount) { - var values = new List>(); + List> values; + GetSeriesValues(out isCount, out values); + var dl = values.SelectMany(x => x).Distinct().ToList(); + dl.Sort(); + if (Orientation == eAxisOrientation.MaxMin) + { + dl.Reverse(); + } + return dl.ToArray(); + } + + internal void GetSeriesValues(out bool isCount, out List> values) + { + values = new List>(); isCount = false; List pl = new List(); foreach (var ct in _chart.PlotArea.ChartTypes) @@ -739,7 +780,6 @@ internal override object[] GetAxisValues(out bool isCount) } else { - //if ((Index == 1 && !ct.UseSecondaryAxis) || (Index == 2 && ct.UseSecondaryAxis)) if (ct.YAxis == this) { AddFromSerie(l, serie.Series, serie.NumberLiteralsY, serie.StringLiteralsY, false, pl); @@ -752,15 +792,45 @@ internal override object[] GetAxisValues(out bool isCount) pl = l; } } - var dl = values.SelectMany(x => x).Distinct().ToList(); - dl.Sort(); - if (Orientation == eAxisOrientation.MaxMin) + if (_chart.IsTypePercentStacked() && IsYAxis) { - dl.Reverse(); + CalculateStacked100(values); } - return dl.ToArray(); } + internal static void CalculateStacked100(List> values) + { + for (int i = 0; i < values[0].Count; i++) + { + double rowAbsSum = 0; + for (int k = 0; k < values.Count; k++) + { + var v = values[k][i]; + if (ConvertUtil.IsExcelNumeric(v)) + { + rowAbsSum += Math.Abs(ConvertUtil.GetValueDouble(v, true, true)); + } + } + var pv = 0D; + for (int j=0;j< values.Count;j++) + { + var val = values[j][i]; + if (ConvertUtil.IsExcelNumeric(val)) + { + double d = pv + ConvertUtil.GetValueDouble(val, true, true); + if (rowAbsSum != 0) + { + values[j][i] = d / rowAbsSum; + } + else + { + values[j][i] = 0; + } + pv = d; + } + } + } + } private void AddCountFromSeries(List l, string address, double[] numberLiterals, string[] stringLiterals) { l.Add(1); diff --git a/src/EPPlusTest/Issues/WorksheetIssues.cs b/src/EPPlusTest/Issues/WorksheetIssues.cs index 446a64326..ae73c6771 100644 --- a/src/EPPlusTest/Issues/WorksheetIssues.cs +++ b/src/EPPlusTest/Issues/WorksheetIssues.cs @@ -1121,6 +1121,5 @@ public void i2258() var d3 = ws.Cells["D3"].Text; Assert.AreEqual("Negative 4000,000", d3); } - } } From 501c8ee4af0a8ec0d214dd46e4bce93c8c0ca5c9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ossian=20Edstr=C3=B6m?= Date: Thu, 5 Feb 2026 11:50:29 +0100 Subject: [PATCH 043/151] Fixed test and a few minor bugs --- .../TextRenderTests.cs | 54 +++++++------------ .../RenderItems/Shared/ParagraphItem.cs | 39 -------------- .../RenderItems/Shared/TextRunItem.cs | 4 +- .../RenderItems/SvgItem/SvgParagraphItem.cs | 2 +- .../RenderItems/SvgItem/SvgTextBoxItem.cs | 1 - .../RenderItems/SvgItem/SvgTextRunItem.cs | 2 +- .../TrueTypeMeasurer/TextData.cs | 8 +-- 7 files changed, 25 insertions(+), 85 deletions(-) diff --git a/src/EPPlus.Export.ImageRenderer.Test/TextRenderTests.cs b/src/EPPlus.Export.ImageRenderer.Test/TextRenderTests.cs index 088d9137d..622e3974d 100644 --- a/src/EPPlus.Export.ImageRenderer.Test/TextRenderTests.cs +++ b/src/EPPlus.Export.ImageRenderer.Test/TextRenderTests.cs @@ -216,53 +216,39 @@ public void VerifyTextRunBounds() var autofit = cube.TextBody.TextAutofit; - cube.GetSizeInPixels(out int testWidth, out int testHeight); - - //cube.SetPixelWidth(300); - //cube.SetPixelHeight(300); - - var parentBB = new BoundingBox(); - parentBB.Width = testWidth; - parentBB.Height = testHeight; + cube.ChangeCellAnchor(eEditAs.Absolute); + cube.SetPixelWidth(5000); var svgShape = new SvgShape(cube); SvgTextBodyItem tbItem = svgShape.TextBox.TextBody; - //SvgTextBodyItem tbItem = new SvgTextBodyItem(svgShape, parentBB); - - //tbItem.ImportTextBody(cube.TextBody); var txtRun1Bounds = tbItem.Paragraphs[0].Runs[0].Bounds; - - Assert.AreEqual(43.835286458333336d, txtRun1Bounds.Width); - - //var widthLine2 = tbItem.Paragraphs[0].Runs[0].PerLineWidth[1]; - //var topYLine2 = tbItem.Paragraphs[0].Runs[0].YIncreasePerLine[1]; - //Assert.AreEqual(7.147135416666667d, widthLine2); - //Assert.AreEqual(17.903645833333336d, topYLine2); + var pxWidth = txtRun1Bounds.Width.PointToPixel(); + Assert.AreEqual(43.835286458333336d, pxWidth); var txtRuns2 = tbItem.Paragraphs[1].Runs; - Assert.AreEqual(53.20963541666667d, txtRuns2[0].Bounds.Width); - var currentLineWidth = txtRuns2[0].Bounds.Width; + Assert.AreEqual(53.20963541666667d, txtRuns2[0].Bounds.Width.PointToPixel()); + var currentLineWidth = txtRuns2[0].Bounds.Width.PointToPixel(); - Assert.AreEqual(currentLineWidth, txtRuns2[1].Bounds.Left); - Assert.AreEqual(69.55924479166667d, txtRuns2[1].Bounds.Width); - currentLineWidth += txtRuns2[1].Bounds.Width; + Assert.AreEqual(currentLineWidth, txtRuns2[1].Bounds.Left.PointToPixel()); + Assert.AreEqual(69.55924479166667d, txtRuns2[1].Bounds.Width.PointToPixel()); + currentLineWidth += txtRuns2[1].Bounds.Width.PointToPixel(); - Assert.AreEqual(currentLineWidth, txtRuns2[2].Bounds.Left); - Assert.AreEqual(49.89388020833334, txtRuns2[2].Bounds.Width); - currentLineWidth += txtRuns2[2].Bounds.Width; + Assert.AreEqual(currentLineWidth, txtRuns2[2].Bounds.Left.PointToPixel(),0.0001); + Assert.AreEqual(49.89388020833334, txtRuns2[2].Bounds.Width.PointToPixel(), 0.0001); + currentLineWidth += txtRuns2[2].Bounds.Width.PointToPixel(); - Assert.AreEqual(21.333333333333332, txtRuns2[3].Bounds.Height); - Assert.AreEqual(currentLineWidth, txtRuns2[3].Bounds.Left); - Assert.AreEqual(283.21875d, txtRuns2[3].Bounds.Width); - currentLineWidth += txtRuns2[3].Bounds.Width; + Assert.AreEqual(21.333333333333332, txtRuns2[3].Bounds.Height.PointToPixel()); + Assert.AreEqual(currentLineWidth, txtRuns2[3].Bounds.Left.PointToPixel(), 0.0001); + Assert.AreEqual(283.21875d, txtRuns2[3].Bounds.Width.PointToPixel(), 0.0001); + currentLineWidth += txtRuns2[3].Bounds.Width.PointToPixel(); - Assert.AreEqual(32, txtRuns2[4].Bounds.Height); - Assert.AreEqual(currentLineWidth, txtRuns2[4].Bounds.Left); - Assert.AreEqual(134.20312500000006d, txtRuns2[4].Bounds.Width); - currentLineWidth += txtRuns2[4].Bounds.Width; + Assert.AreEqual(32, txtRuns2[4].Bounds.Height.PointToPixel(), 0.0001); + Assert.AreEqual(currentLineWidth, txtRuns2[4].Bounds.Left.PointToPixel(), 0.0001); + Assert.AreEqual(134.20312500000006d, txtRuns2[4].Bounds.Width.PointToPixel(), 0.0001); + currentLineWidth += txtRuns2[4].Bounds.Width.PointToPixel(); } } diff --git a/src/EPPlus.Export.ImageRenderer/RenderItems/Shared/ParagraphItem.cs b/src/EPPlus.Export.ImageRenderer/RenderItems/Shared/ParagraphItem.cs index 8dadf61b3..c2cea7a45 100644 --- a/src/EPPlus.Export.ImageRenderer/RenderItems/Shared/ParagraphItem.cs +++ b/src/EPPlus.Export.ImageRenderer/RenderItems/Shared/ParagraphItem.cs @@ -249,45 +249,6 @@ private void AddLinesAndTextRuns(ExcelDrawingParagraph p, string textIfEmpty) lastDescent = line.LargestDescent; } Bounds.Height = runLineSpacing + lastDescent; - //Bounds.Width = Runs.Sum(x => x.Bounds.Width); - - //if (p.TextRuns.Count == 0 && string.IsNullOrEmpty(textIfEmpty) == false) - //{ - // AddText(textIfEmpty, p.DefaultRunProperties); - // Bounds.Width = Runs.Sum(x => x.Bounds.Width); - //} - //else - //{ - // lines = WrapToSimpleTextLines(p, _textFragments); - // foreach (var line in lines) - // { - // double prevWidth = 0; - - // if (lineSpacingIsExact == false) - // { - // runLineSpacing += line.LargestAscent + lastDescent; - // } - // else - // { - // runLineSpacing += ParagraphLineSpacing; - // } - // if (line.Width > greatestWidth) - // { - // greatestWidth = line.Width; - // } - - // foreach (var rtFragment in line.RtFragments) - // { - // var displayText = line.GetFragmentText(rtFragment); - // var runItem = AddRenderItemTextRun(p.TextRuns[rtFragment.Fragidx], displayText, prevWidth, runLineSpacing); - // runItem.Bounds.Width = rtFragment.Width; - // prevWidth += rtFragment.Width; - // } - - // lastDescent = line.LargestDescent; - // } - // Bounds.Height = runLineSpacing + lastDescent; - //} } List WrapToSimpleTextLines(ExcelDrawingParagraph p, TextFragmentCollection fragments) diff --git a/src/EPPlus.Export.ImageRenderer/RenderItems/Shared/TextRunItem.cs b/src/EPPlus.Export.ImageRenderer/RenderItems/Shared/TextRunItem.cs index 61bfd517c..a6dd0c1c0 100644 --- a/src/EPPlus.Export.ImageRenderer/RenderItems/Shared/TextRunItem.cs +++ b/src/EPPlus.Export.ImageRenderer/RenderItems/Shared/TextRunItem.cs @@ -125,9 +125,7 @@ internal TextRunItem(DrawingBase renderer, BoundingBox parent, ExcelParagraphTex _fontStyles = _measurementFont.Style; FontSizeInPixels = ((double)_measurementFont.Size).PointToPixel(true); - Bounds.Height = FontSizeInPixels; - - _horizontalTextAlignment = run.Paragraph.HorizontalAlignment; + Bounds.Height = _measurementFont.Size; if (run.Fill.IsEmpty == false && run.Fill.Style == eFillStyle.SolidFill) { diff --git a/src/EPPlus.Export.ImageRenderer/RenderItems/SvgItem/SvgParagraphItem.cs b/src/EPPlus.Export.ImageRenderer/RenderItems/SvgItem/SvgParagraphItem.cs index b7f4c962e..2c80a87ab 100644 --- a/src/EPPlus.Export.ImageRenderer/RenderItems/SvgItem/SvgParagraphItem.cs +++ b/src/EPPlus.Export.ImageRenderer/RenderItems/SvgItem/SvgParagraphItem.cs @@ -115,7 +115,7 @@ public override void Render(StringBuilder sb) sb.Append(/*$"{GetHorizontalAlignmentAttribute(Bounds.X)} y=\"{Bounds.Y}\" " +*/ $"font-family=\"{_paragraphFont.FontFamily},{_paragraphFont.FontFamily}_MSFontService,sans-serif\" " + - $"font-size=\"{fontSize}px\" >"); + $"font-size=\"{fontSize.ToString(CultureInfo.InvariantCulture)}px\" >"); if (Runs != null && Runs.Count > 0) { diff --git a/src/EPPlus.Export.ImageRenderer/RenderItems/SvgItem/SvgTextBoxItem.cs b/src/EPPlus.Export.ImageRenderer/RenderItems/SvgItem/SvgTextBoxItem.cs index ddca27d1e..fc4bf1982 100644 --- a/src/EPPlus.Export.ImageRenderer/RenderItems/SvgItem/SvgTextBoxItem.cs +++ b/src/EPPlus.Export.ImageRenderer/RenderItems/SvgItem/SvgTextBoxItem.cs @@ -60,7 +60,6 @@ internal void ImportTextBody(ExcelTextBody body) TextBody.ImportTextBody(body); } - internal override void AppendRenderItems(List renderItems) { SvgGroupItem groupItem; diff --git a/src/EPPlus.Export.ImageRenderer/RenderItems/SvgItem/SvgTextRunItem.cs b/src/EPPlus.Export.ImageRenderer/RenderItems/SvgItem/SvgTextRunItem.cs index cb8c98de0..cb23c717b 100644 --- a/src/EPPlus.Export.ImageRenderer/RenderItems/SvgItem/SvgTextRunItem.cs +++ b/src/EPPlus.Export.ImageRenderer/RenderItems/SvgItem/SvgTextRunItem.cs @@ -92,7 +92,7 @@ public override void Render(StringBuilder sb) string visibility = ""; finalString += xString; - var yString = $" y=\"{YPosition.PointToPixel()}px\" "; + var yString = $" y=\"{YPosition.PointToPixel().ToString(CultureInfo.InvariantCulture)}px\" "; finalString += yString; currentYEndPos += YPosition; diff --git a/src/EPPlus.Fonts.OpenType/TrueTypeMeasurer/TextData.cs b/src/EPPlus.Fonts.OpenType/TrueTypeMeasurer/TextData.cs index b4967b800..ecece0334 100644 --- a/src/EPPlus.Fonts.OpenType/TrueTypeMeasurer/TextData.cs +++ b/src/EPPlus.Fonts.OpenType/TrueTypeMeasurer/TextData.cs @@ -1432,13 +1432,9 @@ internal static List WrapMultipleTextFragmentsToTextLines(TextPa for (int j = 0; j < currentLineRtFragments.Count(); j++) { - if (currentLineRtFragments[j].OverallParagraphStartCharIdx < prevLineBreakIndex) - { - currentLineRtFragments[j].charStarIdxWithinCurrentLine = Math.Max(0, currentLineRtFragments[j].OverallParagraphStartCharIdx - prevLineBreakIndex); - currentTextLine.RtFragments.Add(currentLineRtFragments[j]); - } + currentLineRtFragments[j].charStarIdxWithinCurrentLine = Math.Max(0, currentLineRtFragments[j].OverallParagraphStartCharIdx - prevLineBreakIndex); + currentTextLine.RtFragments.Add(currentLineRtFragments[j]); } - currentTextLine.RtFragments.Add(currentRtFragment); currentTextLine.Text = leftOverLine; outputTextLines.Add(currentTextLine); From c6de9180c04b1ebdef156cac88cffa74f1e5844f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ossian=20Edstr=C3=B6m?= Date: Thu, 5 Feb 2026 12:58:43 +0100 Subject: [PATCH 044/151] fixed constructor RunItem now point height --- .../RenderItems/Shared/TextRunItem.cs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/EPPlus.Export.ImageRenderer/RenderItems/Shared/TextRunItem.cs b/src/EPPlus.Export.ImageRenderer/RenderItems/Shared/TextRunItem.cs index a6dd0c1c0..7e35a5ae2 100644 --- a/src/EPPlus.Export.ImageRenderer/RenderItems/Shared/TextRunItem.cs +++ b/src/EPPlus.Export.ImageRenderer/RenderItems/Shared/TextRunItem.cs @@ -71,10 +71,10 @@ internal TextRunItem(DrawingBase renderer, BoundingBox parent, string text, Exce _fontStyles = _measurementFont.Style; FontSizeInPixels = ((double)_measurementFont.Size).PointToPixel(true); - Bounds.Height = FontSizeInPixels; - if (parent.Height < FontSizeInPixels) + Bounds.Height = _measurementFont.Size; + if (parent.Height < _measurementFont.Size) { - parent.Height = FontSizeInPixels; + parent.Height = _measurementFont.Size; } _horizontalTextAlignment = eTextAlignment.Center; From f21dec01c562cb80af9a7834c06b8217ba024bf8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jan=20K=C3=A4llman?= Date: Fri, 6 Feb 2026 10:16:52 +0100 Subject: [PATCH 045/151] WIP:Changed to unit for bounds to use points everyware. Changed svg render to output pixels. --- .../SvgPathTests.cs | 20 +-- .../PathCommands.cs | 76 +---------- .../RenderItems/Shared/ParagraphItem.cs | 42 +----- .../RenderItems/Shared/TextBodyItem.cs | 10 +- .../RenderItems/SvgGroupItem.cs | 10 +- .../RenderItems/SvgItem/SvgParagraphItem.cs | 2 +- .../RenderItems/SvgItem/SvgTextBodyItem.cs | 2 - .../RenderItems/SvgItem/SvgTextBoxItem.cs | 92 +++++++++++-- .../RenderItems/SvgItem/SvgTextRunItem.cs | 4 +- .../RenderItems/SvgRenderEllipseItem.cs | 11 +- .../RenderItems/SvgRenderLineItem.cs | 9 +- .../RenderItems/SvgRenderPathItem.cs | 5 +- .../RenderItems/SvgRenderRectItem.cs | 9 +- .../Chart/Axis/ValueAxisScaleCalculator.cs | 4 +- .../Svg/Chart/SvgChartAxis.cs | 127 ++---------------- .../Svg/Chart/SvgChartObject.cs | 2 +- .../Svg/Chart/SvgChartTitle.cs | 63 +++++---- .../Svg/SvgShape.cs | 2 +- src/EPPlus.Fonts.OpenType/Utils/TextUtils.cs | 6 + .../Drawing/Chart/ExcelChartAxisStandard.cs | 17 --- 20 files changed, 182 insertions(+), 331 deletions(-) diff --git a/src/EPPlus.Export.ImageRenderer.Test/SvgPathTests.cs b/src/EPPlus.Export.ImageRenderer.Test/SvgPathTests.cs index c60f4eedb..fd2824687 100644 --- a/src/EPPlus.Export.ImageRenderer.Test/SvgPathTests.cs +++ b/src/EPPlus.Export.ImageRenderer.Test/SvgPathTests.cs @@ -511,20 +511,20 @@ public void GenerateSvgForCharts() { var ws = p.Workbook.Worksheets[0]; var renderer = new EPPlusImageRenderer.ImageRenderer(); - //var ix = 0; - //var c = ws.Drawings[ix]; - //var svg = renderer.RenderDrawingToSvg(c); - //SaveTextFileToWorkbook($"svg\\ChartForSvg_ind{ix++}.svg", svg); var ix = 1; - foreach (ExcelChart c in ws.Drawings) - { - var svg = renderer.RenderDrawingToSvg(c); - SaveTextFileToWorkbook($"svg\\ChartForSvg{ix++}.svg", svg); - } + var c = ws.Drawings[ix]; + var svg = renderer.RenderDrawingToSvg(c); + SaveTextFileToWorkbook($"svg\\ChartForSvg_ind{ix++}.svg", svg); + //var ix = 1; + //foreach (ExcelChart c in ws.Drawings) + //{ + // var svg = renderer.RenderDrawingToSvg(c); + // SaveTextFileToWorkbook($"svg\\ChartForSvg{ix++}.svg", svg); + //} } } [TestMethod] - public void GenerateSvgForLineCharts() + public void GenerateSvgForLineCharts() { ExcelPackage.License.SetNonCommercialOrganization("EPPlus Project"); using (var p = OpenTemplatePackage("LineChartRenderTest.xlsx")) diff --git a/src/EPPlus.Export.ImageRenderer/PathCommands.cs b/src/EPPlus.Export.ImageRenderer/PathCommands.cs index ed2c6a1e2..b8fb0753e 100644 --- a/src/EPPlus.Export.ImageRenderer/PathCommands.cs +++ b/src/EPPlus.Export.ImageRenderer/PathCommands.cs @@ -32,7 +32,7 @@ public PathCommands(PathCommandType type, SvgRenderItem item, params double[] co public SvgAdjustmentPoint AdjustmentPoint { get; set; } public int CommandIndex { get; set; } - public void Render(int width, int height, StringBuilder sb) + public void Render(double width, double height, StringBuilder sb) { sb.Append(Type.AsCommandChar()); for (int i = 0; i < Coordinates.Length; i++) @@ -92,80 +92,6 @@ private double AdjustWithPoint(int width, int height, int i, double x) } return x; } - - private double AdjustPointHalf(int width, int height, int i, double x, bool minus) - { - if (i % 2 == 0) - { - if (width > height) - { - x *= Math.Max(width, height); - if (minus) - { - x -= (Math.Abs(width - height) / 2); - } - else - { - x += (Math.Abs(width - height) / 2); - } - } - else - { - x *= width; - } - } - else - { - if (height > width) - { - x *= Math.Max(width, height); - if (minus) - { - x -= (Math.Abs(width - height) / 2); - } - else - { - x += (Math.Abs(width - height) / 2); - } - } - else - { - x *= height; - } - } - - return x; - } - - private static double AdjustToWidthHight(int width, int height, int i, double x) - { - if (i % 2 == 0) - { - if (width > height) - { - x *= Math.Min(width, height); - x += (Math.Abs(width - height) / 2); - } - else - { - x *= width; - } - } - else - { - if (height > width) - { - x *= Math.Min(width, height); - x += (Math.Abs(width - height) / 2); - } - else - { - x *= height; - } - } - - return x; - } internal PathCommands Clone() { return new PathCommands(Type, RenderItem) diff --git a/src/EPPlus.Export.ImageRenderer/RenderItems/Shared/ParagraphItem.cs b/src/EPPlus.Export.ImageRenderer/RenderItems/Shared/ParagraphItem.cs index 8dadf61b3..176c8becb 100644 --- a/src/EPPlus.Export.ImageRenderer/RenderItems/Shared/ParagraphItem.cs +++ b/src/EPPlus.Export.ImageRenderer/RenderItems/Shared/ParagraphItem.cs @@ -249,45 +249,7 @@ private void AddLinesAndTextRuns(ExcelDrawingParagraph p, string textIfEmpty) lastDescent = line.LargestDescent; } Bounds.Height = runLineSpacing + lastDescent; - //Bounds.Width = Runs.Sum(x => x.Bounds.Width); - - //if (p.TextRuns.Count == 0 && string.IsNullOrEmpty(textIfEmpty) == false) - //{ - // AddText(textIfEmpty, p.DefaultRunProperties); - // Bounds.Width = Runs.Sum(x => x.Bounds.Width); - //} - //else - //{ - // lines = WrapToSimpleTextLines(p, _textFragments); - // foreach (var line in lines) - // { - // double prevWidth = 0; - - // if (lineSpacingIsExact == false) - // { - // runLineSpacing += line.LargestAscent + lastDescent; - // } - // else - // { - // runLineSpacing += ParagraphLineSpacing; - // } - // if (line.Width > greatestWidth) - // { - // greatestWidth = line.Width; - // } - - // foreach (var rtFragment in line.RtFragments) - // { - // var displayText = line.GetFragmentText(rtFragment); - // var runItem = AddRenderItemTextRun(p.TextRuns[rtFragment.Fragidx], displayText, prevWidth, runLineSpacing); - // runItem.Bounds.Width = rtFragment.Width; - // prevWidth += rtFragment.Width; - // } - - // lastDescent = line.LargestDescent; - // } - // Bounds.Height = runLineSpacing + lastDescent; - //} + Bounds.Width = greatestWidth; } List WrapToSimpleTextLines(ExcelDrawingParagraph p, TextFragmentCollection fragments) @@ -302,7 +264,7 @@ List WrapToSimpleTextLines(ExcelDrawingParagraph p, TextFragment fonts.Add(runFont); } - var maxWidthPoints = Math.Round(Bounds.Width, 0, MidpointRounding.AwayFromZero).PixelToPoint(); + var maxWidthPoints = Math.Round(ParentTextBody.MaxWidth, 0, MidpointRounding.AwayFromZero).PixelToPoint(); return ttMeasurer.WrapMultipleTextFragmentsToTextLines(fragments, fonts, maxWidthPoints); } diff --git a/src/EPPlus.Export.ImageRenderer/RenderItems/Shared/TextBodyItem.cs b/src/EPPlus.Export.ImageRenderer/RenderItems/Shared/TextBodyItem.cs index db40afda8..169b927ab 100644 --- a/src/EPPlus.Export.ImageRenderer/RenderItems/Shared/TextBodyItem.cs +++ b/src/EPPlus.Export.ImageRenderer/RenderItems/Shared/TextBodyItem.cs @@ -71,11 +71,16 @@ public void ImportParagraph(ExcelDrawingParagraph item, double startingY, string _text = text; if(Paragraphs.Count == 0) { - Bounds.Width = paragraph.Bounds.Width; + Bounds.Height = paragraph.Bounds.Height; } else { - Bounds.Width += paragraph.Bounds.Width; + Bounds.Height += paragraph.Bounds.Height; + } + + if(Bounds.Width < paragraph.Bounds.Width) + { + Bounds.Width = paragraph.Bounds.Width; } Paragraphs.Add(paragraph); } @@ -133,7 +138,6 @@ internal virtual void ImportTextBody(ExcelTextBody body) Bounds.Height = paragraphStartY; } } - private double GetParagraphAscendantSpacingInPixels(eDrawingTextLineSpacing lineSpacingType, double spacingValue, ITextMeasurerWrap fmExact, out double multiplier) { if (lineSpacingType == eDrawingTextLineSpacing.Exactly) diff --git a/src/EPPlus.Export.ImageRenderer/RenderItems/SvgGroupItem.cs b/src/EPPlus.Export.ImageRenderer/RenderItems/SvgGroupItem.cs index 779a91a15..ae92137b1 100644 --- a/src/EPPlus.Export.ImageRenderer/RenderItems/SvgGroupItem.cs +++ b/src/EPPlus.Export.ImageRenderer/RenderItems/SvgGroupItem.cs @@ -10,6 +10,7 @@ Date Author Change ************************************************************************************************* 27/11/2025 EPPlus Software AB EPPlus 9 *************************************************************************************************/ +using EPPlus.Fonts.OpenType.Utils; using EPPlus.Graphics; using EPPlusImageRenderer.Svg; using OfficeOpenXml.Drawing.Theme; @@ -43,7 +44,7 @@ internal SvgGroupItem(DrawingBase renderer, BoundingBox bounds) : base(renderer) Bounds = bounds; if (Bounds.Left != 0 || Bounds.Top != 0) { - GroupTransform = $"transform=\"translate({Bounds.Left.ToString(CultureInfo.InvariantCulture)}, {Bounds.Top.ToString(CultureInfo.InvariantCulture)})\""; + GroupTransform = $"transform=\"translate({Bounds.Left.PointToPixelString()}, {Bounds.Top.PointToPixelString()})\""; } } internal SvgGroupItem(DrawingBase renderer, BoundingBox parent, double rotation) : base(renderer, parent) @@ -53,7 +54,7 @@ internal SvgGroupItem(DrawingBase renderer, BoundingBox parent, double rotation) Bounds = parent; if (Bounds.Left!=0 || Bounds.Top!=0) { - bounds = $"translate({Bounds.Left.ToString(CultureInfo.InvariantCulture)}, {Bounds.Top.ToString(CultureInfo.InvariantCulture)})"; + bounds = $"translate({Bounds.Left.PointToPixelString()}, {Bounds.Top.PointToPixelString()})"; } if(rotation!=0) { @@ -95,11 +96,6 @@ public override void Render(StringBuilder sb) } } - //internal override SvgRenderItem Clone(SvgShape svgDocument) - //{ - // return this.Clone(svgDocument); - //} - internal override void GetBounds(out double il, out double it, out double ir, out double ib) { il = 0; diff --git a/src/EPPlus.Export.ImageRenderer/RenderItems/SvgItem/SvgParagraphItem.cs b/src/EPPlus.Export.ImageRenderer/RenderItems/SvgItem/SvgParagraphItem.cs index b7f4c962e..42da8974b 100644 --- a/src/EPPlus.Export.ImageRenderer/RenderItems/SvgItem/SvgParagraphItem.cs +++ b/src/EPPlus.Export.ImageRenderer/RenderItems/SvgItem/SvgParagraphItem.cs @@ -51,7 +51,7 @@ public override void Render(StringBuilder sb) { var fontSize = _paragraphFont.Size.PointToPixel().ToString(CultureInfo.InvariantCulture); - sb.AppendLine($""); + sb.AppendLine($""); sb.AppendLine("paragraph "); diff --git a/src/EPPlus.Export.ImageRenderer/RenderItems/SvgItem/SvgTextBodyItem.cs b/src/EPPlus.Export.ImageRenderer/RenderItems/SvgItem/SvgTextBodyItem.cs index e6ae234a8..b66c5507b 100644 --- a/src/EPPlus.Export.ImageRenderer/RenderItems/SvgItem/SvgTextBodyItem.cs +++ b/src/EPPlus.Export.ImageRenderer/RenderItems/SvgItem/SvgTextBodyItem.cs @@ -22,8 +22,6 @@ public SvgTextBodyItem(DrawingBase renderer, BoundingBox parent, bool clampedToP Bounds.ClampedToParent = clampedToParent; MaxWidth = parent.Width; MaxHeight = parent.Height; - //Bounds.Width = MaxWidth; - //Bounds.Height = MaxHeight; } public SvgTextBodyItem(DrawingBase renderer, BoundingBox parent, double left, double top, double maxWidth, double maxHeight, bool clampedToParent = false) : base(renderer, parent) { diff --git a/src/EPPlus.Export.ImageRenderer/RenderItems/SvgItem/SvgTextBoxItem.cs b/src/EPPlus.Export.ImageRenderer/RenderItems/SvgItem/SvgTextBoxItem.cs index ddca27d1e..9fba79663 100644 --- a/src/EPPlus.Export.ImageRenderer/RenderItems/SvgItem/SvgTextBoxItem.cs +++ b/src/EPPlus.Export.ImageRenderer/RenderItems/SvgItem/SvgTextBoxItem.cs @@ -5,9 +5,11 @@ using EPPlusImageRenderer.Svg; using OfficeOpenXml.Drawing; using OfficeOpenXml.FormulaParsing.Excel.Functions.Text; +using System; using System.Collections.Generic; using System.Drawing; using System.Xml.Serialization; +using static System.Net.Mime.MediaTypeNames; namespace EPPlus.Export.ImageRenderer.RenderItems.SvgItem { @@ -19,19 +21,24 @@ internal SvgTextBoxItem(DrawingBase renderer, BoundingBox parent, double left, d Bounds.Top = top; Bounds.Width = width; Bounds.Height = height; + Init(renderer, parent, maxWidth, maxHeight); + } + + private void Init(DrawingBase renderer, BoundingBox parent, double maxWidth, double maxHeight) + { Bounds.Name = "TextBox"; TextBody = new SvgTextBodyItem(renderer, Bounds, true); TextBody.MaxWidth = maxWidth; TextBody.MaxHeight = maxHeight; - //Bounds.Width = TextBody.Width; - //Bounds.Height = TextBody.Height; Rectangle = new SvgRenderRectItem(renderer, parent); Rectangle.Bounds = Bounds; } + internal SvgTextBoxItem(DrawingBase renderer, BoundingBox parent, double maxWidth, double maxHeight) : base(renderer, parent) { + Init(renderer, parent, maxWidth, maxHeight); } //Simplified input @@ -39,16 +46,65 @@ internal SvgTextBoxItem(DrawingBase renderer, BoundingBox parent, BoundingBox ma renderer, parent, maxBounds.Left, maxBounds.Top, maxBounds.Width, maxBounds.Height) { } - + public SvgRenderItem Rectangle { get; set; } public SvgTextBodyItem TextBody {get;set;} - internal double LeftMargin { get { return TextBody.Bounds.Left; } set { TextBody.Bounds.Left = value; } } + double _leftMargin; + internal double LeftMargin + { + get + { + return _leftMargin; + } + set + { + Bounds.Width += (value - _leftMargin); + _leftMargin = value; + } + } - internal double TopMargin { get { return TextBody.Bounds.Top; } set { TextBody.Bounds.Top = value; } } + double _topMargin; + internal double TopMargin + { + get + { + return _topMargin; + } + set + { + Bounds.Height += (value - _topMargin); + _topMargin = value; + } + } - internal double RightMargin { get { return Bounds.Width - TextBody.Bounds.Left - TextBody.Bounds.Width ; } set { TextBody.Bounds.Width = Bounds.Width - TextBody.Bounds.Left - value; } } - internal double BottomMargin { get { return Bounds.Height - TextBody.Bounds.Top - TextBody.Bounds.Height; } set { TextBody.Bounds.Height = Bounds.Height - TextBody.Bounds.Top - value; } } - internal void ImportTextBody(ExcelTextBody body) + double _rightMargin; + internal double RightMargin + { + get + { + return _rightMargin; + } + set + { + Bounds.Width += (value - _rightMargin); + _rightMargin = value; + } + } + double _bottomMargin; + internal double BottomMargin + { + get + { + return _bottomMargin; + } + set + { + Bounds.Height += (value - _bottomMargin); + _bottomMargin = value; + } + } + + internal void ImportTextBody(ExcelTextBody body, bool autoSize) { double l, r, t, b; body.GetInsetsOrDefaults(out l, out t, out r, out b); @@ -58,6 +114,12 @@ internal void ImportTextBody(ExcelTextBody body) BottomMargin = b.PointToPixel(); TextBody.ImportTextBody(body); + + if (autoSize) + { + Bounds.Width = TextBody.Bounds.Width + LeftMargin + RightMargin; + Bounds.Height = TextBody.Bounds.Height + TopMargin + BottomMargin; + } } @@ -80,5 +142,19 @@ internal override void AppendRenderItems(List renderItems) TextBody.AppendRenderItems(renderItems); renderItems.Add(new SvgEndGroupItem(DrawingRenderer, Bounds)); } + + internal void ImportParagraph(ExcelDrawingParagraph item, double startingY, string text = null) + { + TextBody.ImportParagraph(item, startingY, text); + Bounds.Width = TextBody.Width; + Bounds.Height = TextBody.Height; + } + + internal void ImportTextBody(ExcelTextBody textBody) + { + TextBody.ImportTextBody(textBody); + Bounds.Width = TextBody.Width; + Bounds.Height = TextBody.Height; + } } } diff --git a/src/EPPlus.Export.ImageRenderer/RenderItems/SvgItem/SvgTextRunItem.cs b/src/EPPlus.Export.ImageRenderer/RenderItems/SvgItem/SvgTextRunItem.cs index cb8c98de0..868082836 100644 --- a/src/EPPlus.Export.ImageRenderer/RenderItems/SvgItem/SvgTextRunItem.cs +++ b/src/EPPlus.Export.ImageRenderer/RenderItems/SvgItem/SvgTextRunItem.cs @@ -85,14 +85,14 @@ string GetFontStyleAttributes() public override void Render(StringBuilder sb) { string finalString = ""; - var xString = $"x =\"{(Bounds.Left.PointToPixel()).ToString(CultureInfo.InvariantCulture)}\" "; + var xString = $"x =\"{(Bounds.Left.PointToPixelString())}\" "; var currentYEndPos = Bounds.Position.Y; // Global position Y finalString += $""); } diff --git a/src/EPPlus.Export.ImageRenderer/Svg/Chart/Axis/ValueAxisScaleCalculator.cs b/src/EPPlus.Export.ImageRenderer/Svg/Chart/Axis/ValueAxisScaleCalculator.cs index 8fd6f39f8..ead28dc09 100644 --- a/src/EPPlus.Export.ImageRenderer/Svg/Chart/Axis/ValueAxisScaleCalculator.cs +++ b/src/EPPlus.Export.ImageRenderer/Svg/Chart/Axis/ValueAxisScaleCalculator.cs @@ -33,8 +33,8 @@ private static AxisScale GetNumberScale(ref double dataMin, ref double dataMax, } } - var isAllPositive = dataMin > 0 && dataMax > 0; - var isAllNegativ = dataMin < 0 && dataMax < 0; + var isAllPositive = dataMin >= 0 && dataMax >= 0; + var isAllNegativ = dataMin <= 0 && dataMax <= 0; if(dataMin < 0 && dataMax > 0 && axisOptions.IsStacked100) { desiredTicks *= 2; diff --git a/src/EPPlus.Export.ImageRenderer/Svg/Chart/SvgChartAxis.cs b/src/EPPlus.Export.ImageRenderer/Svg/Chart/SvgChartAxis.cs index 501d9db3a..1d4a9aebe 100644 --- a/src/EPPlus.Export.ImageRenderer/Svg/Chart/SvgChartAxis.cs +++ b/src/EPPlus.Export.ImageRenderer/Svg/Chart/SvgChartAxis.cs @@ -587,6 +587,15 @@ protected List GetAxisValue(ExcelChartAxisStandard ax, RenderItem rect, }; var length = ax.AxisPosition == eAxisPosition.Left || ax.AxisPosition == eAxisPosition.Right ? SvgChart.Bounds.Height : SvgChart.Bounds.Width; //Fix and use plotarea width/height. + if(isCount) + { + majorUnit = 1; + for(int i=1;i<=max;i++) + { + l.Add(i); + } + return l; + } if (ax.IsDate) { var res = DateAxisScaleCalculator.Calculate(min ?? 0, max ?? 0, length, options); @@ -628,123 +637,5 @@ protected List GetAxisValue(ExcelChartAxisStandard ax, RenderItem rect, return l; } - - private void GetAutoMinMaxValue(ExcelChartAxisStandard ax, int maxMajorTickmarks, bool isCount, ref double? min, ref double? max, out double? majorUnit) - { - if (ax.MinValue.HasValue) - { - min = ax.MinValue; - } - else - { - if (isCount) - { - min = 1; - } - else - { - var diffFromZero = (max - min) / max; - if (diffFromZero > 0.091 && min > 0D) - { - min = 0; - } - } - } - - if (isCount) - { - majorUnit = 1; - } - else - { - if (ax.MaxValue.HasValue) - { - max = ax.MaxValue; - majorUnit = ax.MajorUnit ?? GetAutoUnit(min.Value, max.Value); - if (ax.MinValue.HasValue == false) - { - var newMin = max - majorUnit; - while (newMin > min) - { - newMin -= majorUnit.Value; - } - min = newMin; - } - } - else - { - majorUnit = ax.MajorUnit ?? GetAutoUnit(min.Value, max.Value); - if (isCount == false) - { - var diff = max.Value - min.Value; - var newMax = min.Value + majorUnit; - while ((newMax - min) < (diff * 1.05)) - { - newMax += majorUnit.Value; - } - max = newMax; - } - if (min != 0 && max - min < 9) - { - min -= 2; - } - } - var newUnit = majorUnit; - while (newUnit >= 2 && (max - min) / newUnit > maxMajorTickmarks) - { - newUnit /= 2; - } - } - } - - private double GetAutoUnit(double min, double max) - { - //if (diff < 8) - //{ - // return 1; - //} - //else - //{ - if (min < 0) - { - var diff = max - min; - return 0; - } - else - { - var diff = max - min; - var rawMajorUnit = diff; - var exponent = Math.Floor(Math.Log10(rawMajorUnit)); - var fraction = rawMajorUnit / (Math.Pow(10, exponent)); - double unit; - if (fraction <= 1) - { - unit = 1D; - } - else if (fraction <= 2) - { - unit = 2; - } - else if (fraction <= 2.5) - { - unit = 2.5; - } - else if (fraction <= 5) - { - unit = 5; - } - else - { - unit = 10; - } - - var axMax = unit * Math.Pow(10, exponent); - var axMin = Math.Floor(min / axMax) * axMax; - axMax = Math.Ceiling(max / axMax) * axMax; - return axMax / 10; - } - //} - } - } } \ No newline at end of file diff --git a/src/EPPlus.Export.ImageRenderer/Svg/Chart/SvgChartObject.cs b/src/EPPlus.Export.ImageRenderer/Svg/Chart/SvgChartObject.cs index bd4e57fac..11b143e96 100644 --- a/src/EPPlus.Export.ImageRenderer/Svg/Chart/SvgChartObject.cs +++ b/src/EPPlus.Export.ImageRenderer/Svg/Chart/SvgChartObject.cs @@ -23,7 +23,7 @@ namespace EPPlusImageRenderer.Svg internal abstract class DrawingObject { internal protected DrawingBase DrawingRenderer { get; } - internal BoundingBox Bounds { get; set; } + internal virtual BoundingBox Bounds { get; set; } protected DrawingObject(DrawingBase renderer, BoundingBox parent) { diff --git a/src/EPPlus.Export.ImageRenderer/Svg/Chart/SvgChartTitle.cs b/src/EPPlus.Export.ImageRenderer/Svg/Chart/SvgChartTitle.cs index 1f1308d00..442cb6f4c 100644 --- a/src/EPPlus.Export.ImageRenderer/Svg/Chart/SvgChartTitle.cs +++ b/src/EPPlus.Export.ImageRenderer/Svg/Chart/SvgChartTitle.cs @@ -12,6 +12,8 @@ Date Author Change *************************************************************************************************/ using EPPlus.Export.ImageRenderer.RenderItems.Shared; using EPPlus.Export.ImageRenderer.RenderItems.SvgItem; +using EPPlus.Fonts.OpenType.Utils; +using EPPlus.Graphics; using EPPlusImageRenderer.RenderItems; using EPPlusImageRenderer.Text; using OfficeOpenXml; @@ -65,35 +67,44 @@ internal SvgChartTitle(SvgChart sc, ExcelChartTitleStandard t, string defaultTex } } - var rect = t.TextBody.Paragraphs.GetSizeInPixels(maxWidth, maxHeight, _titleText, t.Font, LeftMargin, TopMargin, t.Rotation); + //var rect = t.TextBody.Paragraphs.GetSizeInPixels(maxWidth, maxHeight, _titleText, t.Font, LeftMargin, TopMargin, t.Rotation); if (t.Layout.HasLayout) { Rectangle = GetRectFromManualLayout(sc, t.Layout); - if (double.IsNaN(Rectangle.Width)) - { - Rectangle.Width = (float)(rect.Width); - } - if (double.IsNaN(Rectangle.Height)) - { - Rectangle.Height = (float)(rect.Height); - } + Rectangle.Width = (float)maxWidth; + Rectangle.Height = (float)maxHeight; + //if (double.IsNaN(Rectangle.Width)) + //{ + // Rectangle.Width = (float)(rect.Width); + //} + //if (double.IsNaN(Rectangle.Height)) + //{ + // Rectangle.Height = (float)(rect.Height); + //} InitTextBox(); + TextBox.Bounds.Top = Rectangle.Top; + TextBox.Bounds.Left = Rectangle.Left; + Rectangle.Width = TextBox.Bounds.Width; + Rectangle.Height = TextBox.Bounds.Height; } else { Rectangle = new SvgRenderRectItem(sc, sc.Bounds); if (axis==null) - { - Rectangle.Top = (float)8; //8 pixels for the chart title standard offset - Rectangle.Left = (float)(sc.Bounds.Width - rect.Width) / 2; - Rectangle.Height = (float)rect.Height; - Rectangle.Width = (float)rect.Width; + { + Rectangle.Width = (float)maxWidth; + Rectangle.Height = (float)maxHeight; + InitTextBox(); + TextBox.Bounds.Top = (float)6; //6 point for the chart title standard offset + TextBox.Bounds.Left = (float)(sc.Bounds.Width - TextBox.Bounds.Width) / 2; + Rectangle.Height = (float)TextBox.Bounds.Height; + Rectangle.Width = (float)TextBox.Bounds.Width; } else { - SetAxisTitleRect(sc, axis, rect); + //SetAxisTitleRect(sc, axis); } } @@ -171,31 +182,26 @@ private static string GetDefaultChartTitleText(SvgChart sc, ExcelChartTitleStand internal void InitTextBox() { - TextBox = new SvgTextBoxItem(_svgChart, _svgChart.ChartArea.Bounds, Rectangle.Left, Rectangle.Top, Rectangle.Width, Rectangle.Height, Rectangle.Width, Rectangle.Height); - TextBox.LeftMargin = LeftMargin; - TextBox.RightMargin = RightMargin; - TextBox.TopMargin = TopMargin; - TextBox.BottomMargin = BottomMargin; - TextBox.TextBody.VerticalAlignment = eTextAnchoringType.Top; + TextBox = new SvgTextBoxItem(_svgChart, _svgChart.ChartArea.Bounds, Rectangle.Width, Rectangle.Height); if(_title.Rotation != 0) { TextBox.Bounds.Rotation = _title.Rotation; } if (_title.TextBody.Paragraphs.Count > 0) { - //foreach (var p in _title.TextBody.Paragraphs) - //{ - // TextBox.TextBody.ImportParagraph(p, 0); - //} - TextBox.TextBody.ImportTextBody(_title.TextBody); + TextBox.ImportTextBody(_title.TextBody); } else { - //TextBox.AddText(string.IsNullOrEmpty(_title.Text) ? _titleText : _title.Text, _title.Font); var text = string.IsNullOrEmpty(_title.Text) ? _titleText : _title.Text; var p = _title.DefaultTextBody.Paragraphs.FirstOrDefault(); - TextBox.TextBody.ImportParagraph(p, 0, text); + TextBox.ImportParagraph(p, 0, text); } + TextBox.LeftMargin = LeftMargin; + TextBox.RightMargin = RightMargin; + TextBox.TopMargin = TopMargin; + TextBox.BottomMargin = BottomMargin; + TextBox.TextBody.VerticalAlignment = eTextAnchoringType.Top; } public SvgTextBoxItem TextBox @@ -204,7 +210,6 @@ public SvgTextBoxItem TextBox } internal override void AppendRenderItems(List renderItems) { - TextBox.AppendRenderItems(renderItems); TextBox.Rectangle.SetDrawingPropertiesFill(_title.Fill, _svgChart.Chart.StyleManager.Style.Title.FillReference.Color); TextBox.Rectangle.SetDrawingPropertiesBorder(_title.Border, _svgChart.Chart.StyleManager.Style.Title.BorderReference.Color, _title.Border.Fill.Style != eFillStyle.NoFill, 0.75); diff --git a/src/EPPlus.Export.ImageRenderer/Svg/SvgShape.cs b/src/EPPlus.Export.ImageRenderer/Svg/SvgShape.cs index 3ace69702..f3f0fcc1d 100644 --- a/src/EPPlus.Export.ImageRenderer/Svg/SvgShape.cs +++ b/src/EPPlus.Export.ImageRenderer/Svg/SvgShape.cs @@ -109,7 +109,7 @@ public SvgShape(ExcelShape shape) : base(shape) } TextBox = CreateTextBodyItem(); - TextBox.ImportTextBody(_shape.TextBody); + TextBox.ImportTextBody(_shape.TextBody, true); TextBox.AppendRenderItems(RenderItems); } } diff --git a/src/EPPlus.Fonts.OpenType/Utils/TextUtils.cs b/src/EPPlus.Fonts.OpenType/Utils/TextUtils.cs index 49297919b..d3120ca5a 100644 --- a/src/EPPlus.Fonts.OpenType/Utils/TextUtils.cs +++ b/src/EPPlus.Fonts.OpenType/Utils/TextUtils.cs @@ -1,5 +1,6 @@ using OfficeOpenXml.Drawing; using System; +using System.Globalization; namespace EPPlus.Fonts.OpenType.Utils { @@ -13,6 +14,11 @@ public static double PointToPixel(this double pointSize) //1 inch is 72 pts. "Inches * dots/inch = dots" aka Pixels return pointSize / 72 * 96; } + public static string PointToPixelString(this double pointSize) + { + //1 inch is 72 pts. "Inches * dots/inch = dots" aka Pixels + return (pointSize / 72 * 96).ToString(CultureInfo.InvariantCulture); + } public static double PointToPixel(this double pointSize, bool isFonts) { diff --git a/src/EPPlus/Drawing/Chart/ExcelChartAxisStandard.cs b/src/EPPlus/Drawing/Chart/ExcelChartAxisStandard.cs index 943dad056..12793c96c 100644 --- a/src/EPPlus/Drawing/Chart/ExcelChartAxisStandard.cs +++ b/src/EPPlus/Drawing/Chart/ExcelChartAxisStandard.cs @@ -837,32 +837,15 @@ private void AddCountFromSeries(List l, string address, double[] numberL if (numberLiterals?.Length > 0) { l.Add(numberLiterals.Length); - //for(int i=1;i<= numberLiterals?.Length;i++) - //{ - // hs.Add(i); - //} } else if (stringLiterals?.Length > 0) { l.Add(stringLiterals.Length); - //for (int i = 1; i <= stringLiterals?.Length; i++) - //{ - // hs.Add(i); - //} } else { var a = new ExcelAddressBase(address); l.Add(Math.Max(a.Rows, a.Columns)); - //var ws = _chart.WorkSheet.Workbook.Worksheets[a.WorkSheetName]; - //int i = 1; - //if (ws != null) - //{ - // foreach (var c in ws.Cells[a.Address]) - // { - // hs.Add(i++); - // } - //} } } From 75fc718a073f9e7537f6899f32492613569343aa Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jan=20K=C3=A4llman?= Date: Fri, 6 Feb 2026 13:46:24 +0100 Subject: [PATCH 046/151] WIP:Changed to Points in bound and Pixels in render. --- src/EPPlus.Export.ImageRenderer/ImageRenderer.cs | 1 - .../RenderItems/Shared/ParagraphItem.cs | 8 +++++--- .../RenderItems/Shared/TextBodyItem.cs | 6 ++++-- .../RenderItems/SvgItem/SvgTextBodyItem.cs | 4 ++-- src/EPPlus.Export.ImageRenderer/Svg/Chart/SvgChart.cs | 3 ++- .../Utils/DrawingExtensions.cs | 7 ++++--- 6 files changed, 17 insertions(+), 12 deletions(-) diff --git a/src/EPPlus.Export.ImageRenderer/ImageRenderer.cs b/src/EPPlus.Export.ImageRenderer/ImageRenderer.cs index f6221cb93..0c933a8f0 100644 --- a/src/EPPlus.Export.ImageRenderer/ImageRenderer.cs +++ b/src/EPPlus.Export.ImageRenderer/ImageRenderer.cs @@ -37,7 +37,6 @@ public class ImageRenderer { public string RenderDrawingToSvg(ExcelDrawing drawing) { - drawing.GetSizeInPixels(out int width, out int height); var sb = new StringBuilder(); if (drawing is ExcelShape shape) { diff --git a/src/EPPlus.Export.ImageRenderer/RenderItems/Shared/ParagraphItem.cs b/src/EPPlus.Export.ImageRenderer/RenderItems/Shared/ParagraphItem.cs index 176c8becb..895b69af7 100644 --- a/src/EPPlus.Export.ImageRenderer/RenderItems/Shared/ParagraphItem.cs +++ b/src/EPPlus.Export.ImageRenderer/RenderItems/Shared/ParagraphItem.cs @@ -88,9 +88,11 @@ public ParagraphItem(TextBodyItem textBody, DrawingBase renderer, BoundingBox pa _hAlign = p.HorizontalAlignment; - Bounds.Left = GetAlignmentHorizontal(_hAlign); - Bounds.Width = parent.Width - p.RightMargin - p.LeftMargin; - + if (ParentTextBody.AutoSize == false) + { + Bounds.Left = GetAlignmentHorizontal(_hAlign); + Bounds.Width = parent.Width - p.RightMargin - p.LeftMargin; + } //---Get measurer--- _measurer = p._prd.Package.Settings.TextSettings.GenericTextMeasurerTrueType; diff --git a/src/EPPlus.Export.ImageRenderer/RenderItems/Shared/TextBodyItem.cs b/src/EPPlus.Export.ImageRenderer/RenderItems/Shared/TextBodyItem.cs index 169b927ab..c887a5ce7 100644 --- a/src/EPPlus.Export.ImageRenderer/RenderItems/Shared/TextBodyItem.cs +++ b/src/EPPlus.Export.ImageRenderer/RenderItems/Shared/TextBodyItem.cs @@ -22,11 +22,11 @@ namespace EPPlus.Export.ImageRenderer.RenderItems.Shared /// internal abstract class TextBodyItem : DrawingObject { - public TextBodyItem(DrawingBase renderer, BoundingBox parent) : base(renderer, parent) + public TextBodyItem(DrawingBase renderer, BoundingBox parent, bool autoSize) : base(renderer, parent) { Bounds.Name = "TxtBody"; - Bounds.Parent = parent; + AutoSize = autoSize; } public TextBodyItem(DrawingBase renderer, BoundingBox parent, double maxWidth, double maxHeight) : base(renderer, parent) { @@ -34,7 +34,9 @@ public TextBodyItem(DrawingBase renderer, BoundingBox parent, double maxWidth, d Bounds.Parent = parent; MaxWidth = maxWidth; MaxHeight = maxHeight; + AutoSize = false; } + internal bool AutoSize { get; set; } internal double MaxWidth { get; set; } internal double MaxHeight { get; set; } diff --git a/src/EPPlus.Export.ImageRenderer/RenderItems/SvgItem/SvgTextBodyItem.cs b/src/EPPlus.Export.ImageRenderer/RenderItems/SvgItem/SvgTextBodyItem.cs index b66c5507b..49a324135 100644 --- a/src/EPPlus.Export.ImageRenderer/RenderItems/SvgItem/SvgTextBodyItem.cs +++ b/src/EPPlus.Export.ImageRenderer/RenderItems/SvgItem/SvgTextBodyItem.cs @@ -17,13 +17,13 @@ namespace EPPlus.Export.ImageRenderer.RenderItems.SvgItem { internal class SvgTextBodyItem : TextBodyItem { - public SvgTextBodyItem(DrawingBase renderer, BoundingBox parent, bool clampedToParent = false) : base(renderer, parent) + public SvgTextBodyItem(DrawingBase renderer, BoundingBox parent, bool autoSize, bool clampedToParent = false) : base(renderer, parent, autoSize) { Bounds.ClampedToParent = clampedToParent; MaxWidth = parent.Width; MaxHeight = parent.Height; } - public SvgTextBodyItem(DrawingBase renderer, BoundingBox parent, double left, double top, double maxWidth, double maxHeight, bool clampedToParent = false) : base(renderer, parent) + public SvgTextBodyItem(DrawingBase renderer, BoundingBox parent, double left, double top, double maxWidth, double maxHeight, bool clampedToParent = false) : base(renderer, parent, false) { Bounds.Left = left; Bounds.Top = top; diff --git a/src/EPPlus.Export.ImageRenderer/Svg/Chart/SvgChart.cs b/src/EPPlus.Export.ImageRenderer/Svg/Chart/SvgChart.cs index 9e21f4197..7ec7c32e8 100644 --- a/src/EPPlus.Export.ImageRenderer/Svg/Chart/SvgChart.cs +++ b/src/EPPlus.Export.ImageRenderer/Svg/Chart/SvgChart.cs @@ -11,6 +11,7 @@ Date Author Change 27/11/2025 EPPlus Software AB EPPlus 9 *************************************************************************************************/ using EPPlus.Export.ImageRenderer.Svg.Chart; +using EPPlus.Fonts.OpenType.Utils; using EPPlusImageRenderer.RenderItems; using EPPlusImageRenderer.Utils; using OfficeOpenXml.Drawing.Chart; @@ -161,7 +162,7 @@ public void Render(StringBuilder sb) Legend?.AppendRenderItems(RenderItems); Title?.AppendRenderItems(RenderItems); - sb.Append($""); + sb.Append($""); //Write defs used for gradient colors var writer = new SvgDrawingWriter(this); writer.WriteSvgDefs(sb, RenderItems); diff --git a/src/EPPlus.Export.ImageRenderer/Utils/DrawingExtensions.cs b/src/EPPlus.Export.ImageRenderer/Utils/DrawingExtensions.cs index e702ba482..ac62618b8 100644 --- a/src/EPPlus.Export.ImageRenderer/Utils/DrawingExtensions.cs +++ b/src/EPPlus.Export.ImageRenderer/Utils/DrawingExtensions.cs @@ -1,4 +1,5 @@ -using EPPlus.Graphics; +using EPPlus.Fonts.OpenType.Utils; +using EPPlus.Graphics; using OfficeOpenXml.Drawing; namespace EPPlus.Export.ImageRenderer.Utils @@ -11,8 +12,8 @@ internal static BoundingBox GetBoundingBox(this ExcelDrawing drawing) { Left = 0, Top = 0, - Width = drawing.GetPixelWidth(), - Height = drawing.GetPixelHeight() + Width = drawing.GetPixelWidth().PixelToPoint(), + Height = drawing.GetPixelHeight().PixelToPoint() }; } } From ac453364418ee2b6c3c66f378ad7c722525ab79f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ossian=20Edstr=C3=B6m?= Date: Fri, 6 Feb 2026 14:06:03 +0100 Subject: [PATCH 047/151] Switched additional classes to point from pixel --- .../RenderItems/Shared/ParagraphItem.cs | 17 +- .../RenderItems/Shared/TextRunItem.cs | 177 +----------------- .../RenderItems/SvgItem/SvgTextBoxItem.cs | 8 +- .../RenderItems/SvgItem/SvgTextRunItem.cs | 51 +---- .../Svg/SvgShape.cs | 26 +-- .../Utils/DrawingExtensions.cs | 7 +- .../TrueTypeMeasurer/FontMeasurerTrueType.cs | 4 +- 7 files changed, 39 insertions(+), 251 deletions(-) diff --git a/src/EPPlus.Export.ImageRenderer/RenderItems/Shared/ParagraphItem.cs b/src/EPPlus.Export.ImageRenderer/RenderItems/Shared/ParagraphItem.cs index 176c8becb..66dfc4da3 100644 --- a/src/EPPlus.Export.ImageRenderer/RenderItems/Shared/ParagraphItem.cs +++ b/src/EPPlus.Export.ImageRenderer/RenderItems/Shared/ParagraphItem.cs @@ -86,10 +86,13 @@ public ParagraphItem(TextBodyItem textBody, DrawingBase renderer, BoundingBox pa _leftMargin = p.LeftMargin + p.Indent + indent; _rightMargin = p.RightMargin; + _leftMargin = _leftMargin.PixelToPoint(); + _rightMargin = _rightMargin.PixelToPoint(); + _hAlign = p.HorizontalAlignment; Bounds.Left = GetAlignmentHorizontal(_hAlign); - Bounds.Width = parent.Width - p.RightMargin - p.LeftMargin; + Bounds.Width = parent.Width - _rightMargin - _leftMargin; //---Get measurer--- _measurer = p._prd.Package.Settings.TextSettings.GenericTextMeasurerTrueType; @@ -97,7 +100,7 @@ public ParagraphItem(TextBodyItem textBody, DrawingBase renderer, BoundingBox pa //---Calculate linespacing--- int numLines = _paragraphLines.Count; _lsType = p.LineSpacing.LineSpacingType; - ParagraphLineSpacing = GetParagraphLineSpacingInPixels(p.LineSpacing.Value, _measurer); + ParagraphLineSpacing = GetParagraphLineSpacingInPoints(p.LineSpacing.Value, _measurer); //---Initialize / calculate lines and runs--- //measurer must be set before AddLinesAndRichText @@ -107,15 +110,15 @@ public ParagraphItem(TextBodyItem textBody, DrawingBase renderer, BoundingBox pa AddLinesAndTextRuns(p, textIfEmpty); } - private double GetParagraphLineSpacingInPixels(double spacingValue, ITextMeasurerWrap fmExact) + private double GetParagraphLineSpacingInPoints(double spacingValue, ITextMeasurerWrap fmExact) { if (_lsType == eDrawingTextLineSpacing.Exactly) { if (IsFirstParagraph) { - _lineSpacingAscendantOnly = spacingValue.PointToPixel(); + _lineSpacingAscendantOnly = spacingValue; } - return spacingValue.PointToPixel(); + return spacingValue; } else { @@ -123,9 +126,9 @@ private double GetParagraphLineSpacingInPixels(double spacingValue, ITextMeasure _lsMultiplier = multiplier; if (IsFirstParagraph) { - _lineSpacingAscendantOnly = multiplier * fmExact.GetBaseLine().PointToPixel(); + _lineSpacingAscendantOnly = multiplier * fmExact.GetBaseLine(); } - return multiplier * fmExact.GetSingleLineSpacing().PointToPixel(); + return multiplier * fmExact.GetSingleLineSpacing(); } } diff --git a/src/EPPlus.Export.ImageRenderer/RenderItems/Shared/TextRunItem.cs b/src/EPPlus.Export.ImageRenderer/RenderItems/Shared/TextRunItem.cs index 61bfd517c..0f8c8b872 100644 --- a/src/EPPlus.Export.ImageRenderer/RenderItems/Shared/TextRunItem.cs +++ b/src/EPPlus.Export.ImageRenderer/RenderItems/Shared/TextRunItem.cs @@ -71,10 +71,10 @@ internal TextRunItem(DrawingBase renderer, BoundingBox parent, string text, Exce _fontStyles = _measurementFont.Style; FontSizeInPixels = ((double)_measurementFont.Size).PointToPixel(true); - Bounds.Height = FontSizeInPixels; - if (parent.Height < FontSizeInPixels) + Bounds.Height = _measurementFont.Size; + if (parent.Height < _measurementFont.Size) { - parent.Height = FontSizeInPixels; + parent.Height = _measurementFont.Size; } _horizontalTextAlignment = eTextAlignment.Center; @@ -125,7 +125,7 @@ internal TextRunItem(DrawingBase renderer, BoundingBox parent, ExcelParagraphTex _fontStyles = _measurementFont.Style; FontSizeInPixels = ((double)_measurementFont.Size).PointToPixel(true); - Bounds.Height = FontSizeInPixels; + Bounds.Height = _measurementFont.Size; _horizontalTextAlignment = run.Paragraph.HorizontalAlignment; @@ -147,72 +147,6 @@ internal TextRunItem(DrawingBase renderer, BoundingBox parent, ExcelParagraphTex _strikeType = run.FontStrike; } - //internal double CalculateLineSpacing() - //{ - // ////---Calculation of total y-height--- - - // ////If already did this - // if (YIncreasePerLine.Count > 0) - // { - // return Bounds.Height; - // } - - // //bool lineIsFirstInParagraph = _isFirstInParagraph; - - // //foreach (var line in Lines) - // //{ - // // //Despite new textrun it could still be on the same line as previous textrun - // // //Therefore only do line increase if we are first in paragraph or if we are not Lines[0]. - // // //This as line == Lines[0] && isFirstInParagraph == false means we are continuing on the same line as previous textRun - // // //This is important if for example we have rich text where two letters on the same line has different colors. - // // if (line != Lines[0] || lineIsFirstInParagraph) - // // { - // // var yIncrease = lineIsFirstInParagraph ? BaseLineSpacing : LineSpacingPerNewLine; - // // lineIsFirstInParagraph = false; - - // // //yIncrease = Fonts.OpenType.Utils.TextUtils.RoundToWhole(yIncrease); - - // // YIncreasePerLine.Add(yIncrease); - - // // _yEndPos += yIncrease; - // // //if (Double.IsNaN(ClippingHeight) == false && _yEndPos >= ClippingHeight) - // // //{ - // // // bool displayLine = false - // // //} - // // } - // // else - // // { - // // YIncreasePerLine.Add(0); - // // } - // //} - - // //for (int i = 0; i < YIncreasePerLine) - - // if (_yEndPos == 0) - // { - // Bounds.Height = FontSizeInPixels; - // } - // else - // { - // Bounds.Height = _yEndPos; - // } - - // return _yEndPos; - - //} - - internal List GetLines(string text) - { - //var inputWidth = _wrapText ? _maxWidthPixels : double.NaN; - //Lines = TextWrapper.GetLines(text, _measurer, inputWidth); - return text.Split(new string[] { Environment.NewLine }, StringSplitOptions.None).ToList(); - } - - private int GetNumberOfLines() - { - return Lines.Count(); - } - /// /// Calculates right/bottom /// @@ -224,111 +158,8 @@ internal override void GetBounds(out double il, out double it, out double ir, ou { il = Bounds.Left; it = Bounds.Top; - //ib = Bounds.Bottom; ir = Bounds.Right; ib = Bounds.Bottom; - ////Sets bounds bottom correctly - //CalculateLineSpacing(); - //ib = Bounds.Bottom; - - ////Caculate bounds right - //ir = CalculateRightPositionInPixels(); - //Bounds.Width = ir-Bounds.Left; } - - //internal double CalculateRightPositionInPixels() - //{ - // var numLines = GetNumberOfLines(); - // double retPos = Bounds.Left; - - // double textLengthPixels = 0; - - // if (numLines <= 0 || string.IsNullOrEmpty(_originalText)) - // { - // textLengthPixels = 0; - // return retPos; - // } - - // double longestWidth = -1; - - // for (int i = 0; i < numLines; i++) - // { - // var text = Lines[i]; - // var width = CalculateTextWidth(Lines[i]).PointToPixel(); - // PerLineWidth.Add(width); - - // if (width > longestWidth) - // { - // longestWidth = width; - // } - // } - - // textLengthPixels = longestWidth; - - // retPos += longestWidth; - - // //Calculates right - // Bounds.Width = textLengthPixels; - - // return Bounds.Right; - //} - - //internal double CalculateBottomPositionInPixels() - //{ - // return Bounds.Height; - //} - - //internal double CalculateTextWidth(string targetString) - //{ - // _measurer.SetFont(_measurementFont); - // _measurer.MeasureWrappedTextCells = true; - // var width = _measurer.MeasureTextWidth(targetString); - - // return width; - //} - - //internal void SetPerLineWidths(List widthsInPoints) - //{ - // var newList = new List(); - // for (int i = 0; i < widthsInPoints.Count; i++) - // { - // newList.Add(widthsInPoints[i].PointToPixel()); - // } - - // PerLineWidth.Clear(); - // PerLineWidth = newList; - //} - - //internal void AddLineSpacing(double lineSpacing) - //{ - // YIncreasePerLine.Add(lineSpacing); - // _yEndPos += lineSpacing; - // Bounds.Height = _yEndPos; - // //bool lineIsFirstInParagraph = _isFirstInParagraph; - - // //for(int i = 0; i< Lines.Count(); i++) - // //{ - // // if (Lines[i] != Lines[0] || lineIsFirstInParagraph) - // // { - // // var yIncrease = lineIsFirstInParagraph ? ascent : ascent + descent; - // // YIncreasePerLine.Add() - - // // lineIsFirstInParagraph = false; - // // } - // //} - - // //for (int i = 0; i < YIncreasePerLine) - - // // if (_yEndPos == 0) - // // { - // // Bounds.Height = FontSizeInPixels; - // // } - // // else - // // { - // // Bounds.Height = _yEndPos; - // // } - - // //return _yEndPos; - //} } } diff --git a/src/EPPlus.Export.ImageRenderer/RenderItems/SvgItem/SvgTextBoxItem.cs b/src/EPPlus.Export.ImageRenderer/RenderItems/SvgItem/SvgTextBoxItem.cs index 9fba79663..2222cb3c8 100644 --- a/src/EPPlus.Export.ImageRenderer/RenderItems/SvgItem/SvgTextBoxItem.cs +++ b/src/EPPlus.Export.ImageRenderer/RenderItems/SvgItem/SvgTextBoxItem.cs @@ -108,10 +108,10 @@ internal void ImportTextBody(ExcelTextBody body, bool autoSize) { double l, r, t, b; body.GetInsetsOrDefaults(out l, out t, out r, out b); - LeftMargin = l.PointToPixel(); - TopMargin = t.PointToPixel(); - RightMargin = r.PointToPixel(); - BottomMargin = b.PointToPixel(); + LeftMargin = l; + TopMargin = t; + RightMargin = r; + BottomMargin = b; TextBody.ImportTextBody(body); diff --git a/src/EPPlus.Export.ImageRenderer/RenderItems/SvgItem/SvgTextRunItem.cs b/src/EPPlus.Export.ImageRenderer/RenderItems/SvgItem/SvgTextRunItem.cs index 868082836..2a080ad5d 100644 --- a/src/EPPlus.Export.ImageRenderer/RenderItems/SvgItem/SvgTextRunItem.cs +++ b/src/EPPlus.Export.ImageRenderer/RenderItems/SvgItem/SvgTextRunItem.cs @@ -121,56 +121,7 @@ public override void Render(StringBuilder sb) finalString += ">"; finalString += _currentText; finalString += ""; - //for (int i = 0; i < Lines.Count; i++) - //{ - // var line = Lines[i]; - - // finalString += $" 0 && YIncreasePerLine[i] != 0) - // { - // var yIncrease = Fonts.OpenType.Utils.TextUtils.RoundToWhole(YIncreasePerLine[i]); - - // currentYEndPos += yIncrease; - // if (double.IsNaN(ClippingHeight) == false && currentYEndPos >= ClippingHeight) - // { - // visibility = "display=\"none\""; - // } - - // var yIncreaseString = yIncrease.ToString(CultureInfo.InvariantCulture); - // var dyString = $"dy=\"{yIncreaseString}px\" "; - // finalString += dyString; - // finalString += "x=\"0\" "; - // } - // else - // { - // finalString += xString; - // } - - // finalString += $"{visibility} " + $"{GetFontStyleAttributes()} "; - - // if (_measurementFont != null) - // { - // finalString += $"font-family=\"{_measurementFont.FontFamily}," - // + $"{_measurementFont.FontFamily}_MSFontService,sans-serif\" " - // + $"font-size=\"{FontSizeInPixels.ToString(CultureInfo.InvariantCulture)}px\" "; - // } - - // sb.Append(finalString); - - // //Get color etc. - // //Renders up until this point - // SvgBaseRenderer.BaseRender(sb, this); - // //Since final string has been written in base.render erase it. - // finalString = ""; - - // finalString += ">"; - // finalString += line; - // finalString += ""; - //} + sb.Append(finalString); } } diff --git a/src/EPPlus.Export.ImageRenderer/Svg/SvgShape.cs b/src/EPPlus.Export.ImageRenderer/Svg/SvgShape.cs index f3f0fcc1d..90d2b7ae6 100644 --- a/src/EPPlus.Export.ImageRenderer/Svg/SvgShape.cs +++ b/src/EPPlus.Export.ImageRenderer/Svg/SvgShape.cs @@ -15,6 +15,7 @@ Date Author Change using EPPlus.Export.ImageRenderer.Text; using EPPlus.Export.ImageRenderer.Utils; using EPPlus.Fonts.OpenType; +using EPPlus.Fonts.OpenType.Utils; using EPPlus.Graphics; using EPPlusImageRenderer.RenderItems; using EPPlusImageRenderer.ShapeDefinitions; @@ -23,6 +24,7 @@ Date Author Change using OfficeOpenXml; using OfficeOpenXml.Drawing; using OfficeOpenXml.Drawing.Theme; +using OfficeOpenXml.FormulaParsing.Excel.Functions.Logical; using OfficeOpenXml.Style; using System; using System.Collections.Generic; @@ -88,19 +90,19 @@ public SvgShape(ExcelShape shape) : base(shape) if (shapeDef.TextBoxRect != null) { InsetTextBox = new SvgRenderRectItem(this, Bounds); - InsetTextBox.Bounds.Left = (float)shapeDef.TextBoxRect.LeftValue; - InsetTextBox.Bounds.Top = (float)shapeDef.TextBoxRect.TopValue; + InsetTextBox.Bounds.Left = (float)shapeDef.TextBoxRect.LeftValue.PixelToPoint(); + InsetTextBox.Bounds.Top = (float)shapeDef.TextBoxRect.TopValue.PixelToPoint(); InsetTextBox.FillOpacity = 0.3d; if (shape.TextBody.TextAutofit != eTextAutofit.ShapeAutofit) { - InsetTextBox.Width = (float)shapeDef.TextBoxRect.RightValue - (float)shapeDef.TextBoxRect.LeftValue; - InsetTextBox.Height = (float)shapeDef.TextBoxRect.BottomValue - (float)shapeDef.TextBoxRect.TopValue; + InsetTextBox.Width = (float)shapeDef.TextBoxRect.RightValue - (float)shapeDef.TextBoxRect.LeftValue.PixelToPoint(); + InsetTextBox.Height = (float)shapeDef.TextBoxRect.BottomValue - (float)shapeDef.TextBoxRect.TopValue.PixelToPoint(); } else { - InsetTextBox.Width = (float)shapeDef.TextBoxRect.RightValue; - InsetTextBox.Height = (float)shapeDef.TextBoxRect.BottomValue; + InsetTextBox.Width = (float)shapeDef.TextBoxRect.RightValue.PixelToPoint(); + InsetTextBox.Height = (float)shapeDef.TextBoxRect.BottomValue.PixelToPoint(); } } else @@ -109,7 +111,7 @@ public SvgShape(ExcelShape shape) : base(shape) } TextBox = CreateTextBodyItem(); - TextBox.ImportTextBody(_shape.TextBody, true); + TextBox.ImportTextBody(_shape.TextBody, false); TextBox.AppendRenderItems(RenderItems); } } @@ -214,7 +216,7 @@ public string ViewBox public void Render(StringBuilder sb) { - sb.Append($""); + sb.Append($""); //Write defs used for gradient colors var writer = new SvgDrawingWriter(this); @@ -251,10 +253,10 @@ SvgTextBoxItem CreateTextBodyItem() { GetShapeInnerBound(out double x, out double y, out double width, out double height); InsetTextBox = new SvgRenderRectItem(this, Bounds); - InsetTextBox.Bounds.Left = x; - InsetTextBox.Bounds.Top = y; - InsetTextBox.Width = width; - InsetTextBox.Height = height; + InsetTextBox.Bounds.Left = x.PixelToPoint(); + InsetTextBox.Bounds.Top = y.PixelToPoint(); + InsetTextBox.Width = width.PixelToPoint(); + InsetTextBox.Height = height.PixelToPoint(); InsetTextBox.Bounds.Parent = TextBox.Bounds; //TODO:Check that textBody is correct. } var txtBodyItem = new SvgTextBoxItem(this, Bounds, InsetTextBox.Bounds); diff --git a/src/EPPlus.Export.ImageRenderer/Utils/DrawingExtensions.cs b/src/EPPlus.Export.ImageRenderer/Utils/DrawingExtensions.cs index e702ba482..ac62618b8 100644 --- a/src/EPPlus.Export.ImageRenderer/Utils/DrawingExtensions.cs +++ b/src/EPPlus.Export.ImageRenderer/Utils/DrawingExtensions.cs @@ -1,4 +1,5 @@ -using EPPlus.Graphics; +using EPPlus.Fonts.OpenType.Utils; +using EPPlus.Graphics; using OfficeOpenXml.Drawing; namespace EPPlus.Export.ImageRenderer.Utils @@ -11,8 +12,8 @@ internal static BoundingBox GetBoundingBox(this ExcelDrawing drawing) { Left = 0, Top = 0, - Width = drawing.GetPixelWidth(), - Height = drawing.GetPixelHeight() + Width = drawing.GetPixelWidth().PixelToPoint(), + Height = drawing.GetPixelHeight().PixelToPoint() }; } } diff --git a/src/EPPlus.Fonts.OpenType/TrueTypeMeasurer/FontMeasurerTrueType.cs b/src/EPPlus.Fonts.OpenType/TrueTypeMeasurer/FontMeasurerTrueType.cs index 9ccb8793d..d28557930 100644 --- a/src/EPPlus.Fonts.OpenType/TrueTypeMeasurer/FontMeasurerTrueType.cs +++ b/src/EPPlus.Fonts.OpenType/TrueTypeMeasurer/FontMeasurerTrueType.cs @@ -239,10 +239,10 @@ public List MeasureAndWrapText(string text, MeasurementFont font, double return wrappedStrings; } - public List MeasureAndWrapTextLines(string text, MeasurementFont font, double MaxWidthInPixels, double preExistingWidthPixels = 0) + public List MeasureAndWrapTextLines(string text, MeasurementFont font, double maxWidthPoints, double preExistingWidthPixels = 0) { SetFont(font.Size, font.FontFamily); - var wrappedStrings = TextData.MeasureAndWrapTextLines(text, FontSize, CurrentFont, MaxWidthInPixels.PixelToPoint(), preExistingWidthPixels.PixelToPoint()); + var wrappedStrings = TextData.MeasureAndWrapTextLines(text, FontSize, CurrentFont, maxWidthPoints, preExistingWidthPixels.PixelToPoint()); return wrappedStrings; } From 724e946d92c277c695ef421104bbf493df8d4c17 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ossian=20Edstr=C3=B6m?= Date: Mon, 9 Feb 2026 11:25:08 +0100 Subject: [PATCH 048/151] Fixed rename merge issue --- src/EPPlus.Fonts.OpenType/Integration/TextLayoutEngine.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/EPPlus.Fonts.OpenType/Integration/TextLayoutEngine.cs b/src/EPPlus.Fonts.OpenType/Integration/TextLayoutEngine.cs index 9569b3a30..cc48abd73 100644 --- a/src/EPPlus.Fonts.OpenType/Integration/TextLayoutEngine.cs +++ b/src/EPPlus.Fonts.OpenType/Integration/TextLayoutEngine.cs @@ -173,7 +173,7 @@ private List WrapParagraph( var charWidths = CalculateCharacterWidths(text, fontSize, options); - var state = new WrapState(startingWidthPoints, GetCachedSpaceWidth(fontSize, options)); + var state = new WrapStateText(startingWidthPoints, GetCachedSpaceWidth(fontSize, options)); PrepareLineBuilder(text.Length); From f10f5b3276c9d390de2656a41f98b1b71f074be9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jan=20K=C3=A4llman?= Date: Mon, 9 Feb 2026 12:51:00 +0100 Subject: [PATCH 049/151] Merge --- .../Svg/Chart/SvgChart.cs | 6 ++++-- .../Svg/Chart/SvgChartTitle.cs | 20 ++++--------------- .../TrueTypeMeasurer/TextData.cs | 2 +- 3 files changed, 9 insertions(+), 19 deletions(-) diff --git a/src/EPPlus.Export.ImageRenderer/Svg/Chart/SvgChart.cs b/src/EPPlus.Export.ImageRenderer/Svg/Chart/SvgChart.cs index 7ec7c32e8..3e4fc4c1e 100644 --- a/src/EPPlus.Export.ImageRenderer/Svg/Chart/SvgChart.cs +++ b/src/EPPlus.Export.ImageRenderer/Svg/Chart/SvgChart.cs @@ -79,8 +79,9 @@ private void SetAxisPositionsFromPlotarea(SvgChart sc) if(VerticalAxis.Title!=null) { - VerticalAxis.Title.Rectangle.Top = Plotarea.Rectangle.Top + (Plotarea.Rectangle.Height / 2) - (VerticalAxis.Title.Rectangle.Height / 2); VerticalAxis.Title.InitTextBox(); + VerticalAxis.Title.TextBox.Bounds.Top = Plotarea.Rectangle.Top + (Plotarea.Rectangle.Height / 2) - (VerticalAxis.Title.TextBox.Bounds.Height / 2); + VerticalAxis.Title.TextBox.Bounds.Left = VerticalAxis.Title.Rectangle.Left; } VerticalAxis.AddTickmarksAndValues(); @@ -97,8 +98,9 @@ private void SetAxisPositionsFromPlotarea(SvgChart sc) if (HorizontalAxis.Title != null) { - HorizontalAxis.Title.Rectangle.Left = Plotarea.Rectangle.Left + (Plotarea.Rectangle.Width / 2) - (HorizontalAxis.Title.Rectangle.Width / 2); HorizontalAxis.Title.InitTextBox(); + HorizontalAxis.Title.TextBox.Bounds.Left = Plotarea.Rectangle.Left + (Plotarea.Rectangle.Width / 2) - (HorizontalAxis.Title.TextBox.Bounds.Width / 2); + HorizontalAxis.Title.TextBox.Bounds.Top = HorizontalAxis.Title.Rectangle.Top; } HorizontalAxis.AddTickmarksAndValues(); } diff --git a/src/EPPlus.Export.ImageRenderer/Svg/Chart/SvgChartTitle.cs b/src/EPPlus.Export.ImageRenderer/Svg/Chart/SvgChartTitle.cs index 442cb6f4c..b66862f97 100644 --- a/src/EPPlus.Export.ImageRenderer/Svg/Chart/SvgChartTitle.cs +++ b/src/EPPlus.Export.ImageRenderer/Svg/Chart/SvgChartTitle.cs @@ -66,22 +66,12 @@ internal SvgChartTitle(SvgChart sc, ExcelChartTitleStandard t, string defaultTex _titleText = defaultText; } } - - //var rect = t.TextBody.Paragraphs.GetSizeInPixels(maxWidth, maxHeight, _titleText, t.Font, LeftMargin, TopMargin, t.Rotation); - + if (t.Layout.HasLayout) { Rectangle = GetRectFromManualLayout(sc, t.Layout); Rectangle.Width = (float)maxWidth; Rectangle.Height = (float)maxHeight; - //if (double.IsNaN(Rectangle.Width)) - //{ - // Rectangle.Width = (float)(rect.Width); - //} - //if (double.IsNaN(Rectangle.Height)) - //{ - // Rectangle.Height = (float)(rect.Height); - //} InitTextBox(); TextBox.Bounds.Top = Rectangle.Top; TextBox.Bounds.Left = Rectangle.Left; @@ -104,7 +94,7 @@ internal SvgChartTitle(SvgChart sc, ExcelChartTitleStandard t, string defaultTex } else { - //SetAxisTitleRect(sc, axis); + SetAxisTitleRect(sc, axis); } } @@ -112,7 +102,7 @@ internal SvgChartTitle(SvgChart sc, ExcelChartTitleStandard t, string defaultTex Rectangle.SetDrawingPropertiesBorder(t.Border, sc.Chart.StyleManager.Style.Title.BorderReference.Color, t.Border.Fill.Style != eFillStyle.NoFill, 0.75); } - private void SetAxisTitleRect(SvgChart sc, SvgChartAxis axis, RectBase rect) + private void SetAxisTitleRect(SvgChart sc, SvgChartAxis axis) { var margin = 8F; switch (axis.Axis.AxisPosition) @@ -122,12 +112,10 @@ private void SetAxisTitleRect(SvgChart sc, SvgChartAxis axis, RectBase rect) Rectangle.Left = sc.Chart.HasLegend && sc.Chart.Legend.Position == eLegendPosition.Left ? sc.Legend.Rectangle.Right : margin; break; case eAxisPosition.Bottom: - Rectangle.Top = sc.ChartArea.Height - margin - rect.Height; + Rectangle.Top = sc.ChartArea.Height - margin - Rectangle.Height; Rectangle.Left = GetHorizontalLeft(sc); break; } - Rectangle.Width = rect.Width; - Rectangle.Height = rect.Height; } private double GetHorizontalLeft(SvgChart sc) diff --git a/src/EPPlus.Fonts.OpenType/TrueTypeMeasurer/TextData.cs b/src/EPPlus.Fonts.OpenType/TrueTypeMeasurer/TextData.cs index ecece0334..f2cf526b3 100644 --- a/src/EPPlus.Fonts.OpenType/TrueTypeMeasurer/TextData.cs +++ b/src/EPPlus.Fonts.OpenType/TrueTypeMeasurer/TextData.cs @@ -1430,7 +1430,7 @@ internal static List WrapMultipleTextFragmentsToTextLines(TextPa currentRtFragment.charStarIdxWithinCurrentLine = Math.Max(0, currentRtFragment.OverallParagraphStartCharIdx - prevLineBreakIndex); currentLineRtFragments.Add(currentRtFragment); - for (int j = 0; j < currentLineRtFragments.Count(); j++) + for (int j = 0; j < currentLineRtFragments.Count; j++) { currentLineRtFragments[j].charStarIdxWithinCurrentLine = Math.Max(0, currentLineRtFragments[j].OverallParagraphStartCharIdx - prevLineBreakIndex); currentTextLine.RtFragments.Add(currentLineRtFragments[j]); From 22f2342f53417609c3f8d2bc816af2d4271f5fa9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ossian=20Edstr=C3=B6m?= Date: Mon, 9 Feb 2026 14:18:39 +0100 Subject: [PATCH 050/151] Fixed calculation error for fonts of same size --- .../RenderItems/SvgItem/SvgTextBodyItem.cs | 4 ++-- .../RenderItems/SvgItem/SvgTextRunItem.cs | 2 +- .../TrueTypeMeasurer/TextData.cs | 19 +++++++++++++------ src/EPPlus.Graphics/BoundingBox.cs | 6 ++---- 4 files changed, 18 insertions(+), 13 deletions(-) diff --git a/src/EPPlus.Export.ImageRenderer/RenderItems/SvgItem/SvgTextBodyItem.cs b/src/EPPlus.Export.ImageRenderer/RenderItems/SvgItem/SvgTextBodyItem.cs index 49a324135..5310727cb 100644 --- a/src/EPPlus.Export.ImageRenderer/RenderItems/SvgItem/SvgTextBodyItem.cs +++ b/src/EPPlus.Export.ImageRenderer/RenderItems/SvgItem/SvgTextBodyItem.cs @@ -19,7 +19,7 @@ internal class SvgTextBodyItem : TextBodyItem { public SvgTextBodyItem(DrawingBase renderer, BoundingBox parent, bool autoSize, bool clampedToParent = false) : base(renderer, parent, autoSize) { - Bounds.ClampedToParent = clampedToParent; + //Bounds.ClampedToParent = clampedToParent; MaxWidth = parent.Width; MaxHeight = parent.Height; } @@ -31,7 +31,7 @@ public SvgTextBodyItem(DrawingBase renderer, BoundingBox parent, double left, do Bounds.Height = maxHeight; MaxWidth = maxWidth; MaxHeight = maxHeight; - Bounds.ClampedToParent = clampedToParent; + //Bounds.ClampedToParent = clampedToParent; } internal override List Paragraphs { get; set; } = new List(); diff --git a/src/EPPlus.Export.ImageRenderer/RenderItems/SvgItem/SvgTextRunItem.cs b/src/EPPlus.Export.ImageRenderer/RenderItems/SvgItem/SvgTextRunItem.cs index 2a080ad5d..73934f497 100644 --- a/src/EPPlus.Export.ImageRenderer/RenderItems/SvgItem/SvgTextRunItem.cs +++ b/src/EPPlus.Export.ImageRenderer/RenderItems/SvgItem/SvgTextRunItem.cs @@ -99,7 +99,7 @@ public override void Render(StringBuilder sb) if (double.IsNaN(ClippingHeight) == false && currentYEndPos >= ClippingHeight) { - visibility = " display=\"none\""; + //visibility = " display=\"none\""; } finalString += visibility; finalString += $"{GetFontStyleAttributes()}"; diff --git a/src/EPPlus.Fonts.OpenType/TrueTypeMeasurer/TextData.cs b/src/EPPlus.Fonts.OpenType/TrueTypeMeasurer/TextData.cs index ecece0334..94f3e2e49 100644 --- a/src/EPPlus.Fonts.OpenType/TrueTypeMeasurer/TextData.cs +++ b/src/EPPlus.Fonts.OpenType/TrueTypeMeasurer/TextData.cs @@ -1227,9 +1227,12 @@ internal static List WrapMultipleTextFragmentsToTextLines(TextPa ref maxWidth, ref lineWidth, ref wordWidth); //Font/fragment change currentFont = paragraph.FontIndexDict[fragmentIdx]; - if (paragraph.FontSizes[fragmentIdx] > CurrentLineLargestFontSize) + if (paragraph.FontSizes[fragmentIdx] >= CurrentLineLargestFontSize) { - largestFontCurrentLine = currentFont; + if(GetSingleLineSpacing(currentFont, paragraph.FontSizes[fragmentIdx]) > GetSingleLineSpacing(largestFontCurrentLine, CurrentLineLargestFontSize)) + { + largestFontCurrentLine = currentFont; + } CurrentLineLargestFontSize = paragraph.FontSizes[fragmentIdx]; } spaceWidth = CalcGlyphWidth(paragraph.GlyphMappings[fragmentIdx], ' ', currentFont, ref lastGlyphIndex, ref applyKerning); @@ -1286,7 +1289,10 @@ internal static List WrapMultipleTextFragmentsToTextLines(TextPa { if (paragraph.FontSizes[j] > CurrentLineLargestFontSize) { - largestFontCurrentLine = paragraph.FontIndexDict[j]; + if (GetSingleLineSpacing(paragraph.FontIndexDict[j], paragraph.FontSizes[j]) > GetSingleLineSpacing(largestFontCurrentLine, CurrentLineLargestFontSize)) + { + largestFontCurrentLine = paragraph.FontIndexDict[j]; + } CurrentLineLargestFontSize = paragraph.FontSizes[j]; } } @@ -1301,7 +1307,10 @@ internal static List WrapMultipleTextFragmentsToTextLines(TextPa { if (paragraph.FontSizes[j] > CurrentLineLargestFontSize) { - largestFontCurrentLine = paragraph.FontIndexDict[j]; + if (GetSingleLineSpacing(paragraph.FontIndexDict[j], paragraph.FontSizes[j]) > GetSingleLineSpacing(largestFontCurrentLine, CurrentLineLargestFontSize)) + { + largestFontCurrentLine = paragraph.FontIndexDict[j]; + } CurrentLineLargestFontSize = paragraph.FontSizes[j]; } } @@ -1423,8 +1432,6 @@ internal static List WrapMultipleTextFragmentsToTextLines(TextPa } AddDataToSimpleLine(currentTextLine, paragraph, currentFont, largestFontCurrentLine, CurrentLineLargestFontSize, lineWidth, paragraph.Fragments.TextFragments.Count() - 1); - - AddWidthToFragment(currentRtFragment, currentFont.HeadTable.UnitsPerEm, fontSize, lineWidth, prevFragWidths, currentRtFragment.Fragidx); currentRtFragment.charStarIdxWithinCurrentLine = Math.Max(0, currentRtFragment.OverallParagraphStartCharIdx - prevLineBreakIndex); diff --git a/src/EPPlus.Graphics/BoundingBox.cs b/src/EPPlus.Graphics/BoundingBox.cs index 585d0d861..eef4bfdc4 100644 --- a/src/EPPlus.Graphics/BoundingBox.cs +++ b/src/EPPlus.Graphics/BoundingBox.cs @@ -8,8 +8,6 @@ namespace EPPlus.Graphics { internal class BoundingBox : Transform { - internal bool ClampedToParent { get; set; } = false; - internal BoundingBox() : base() { } @@ -89,7 +87,7 @@ internal double Right return LocalPosition.X + Size.X; } } - internal double Width + internal virtual double Width { get { @@ -101,7 +99,7 @@ internal double Width } } - internal double Height + internal virtual double Height { get { From 396134ca823b9722882cfa3303344489b025f3d9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ossian=20Edstr=C3=B6m?= Date: Mon, 9 Feb 2026 15:02:32 +0100 Subject: [PATCH 051/151] Removed commented out classes --- .../RenderItems/SvgItem/SvgTextBoxItem.cs | 1 - .../Svg/SvgParagraph.cs | 403 ------------- .../Svg/SvgTextRun.cs | 540 ------------------ .../Text/TextBox.cs | 338 ----------- .../Text/TextField.cs | 6 - 5 files changed, 1288 deletions(-) delete mode 100644 src/EPPlus.Export.ImageRenderer/Svg/SvgParagraph.cs delete mode 100644 src/EPPlus.Export.ImageRenderer/Svg/SvgTextRun.cs delete mode 100644 src/EPPlus.Export.ImageRenderer/Text/TextBox.cs delete mode 100644 src/EPPlus.Export.ImageRenderer/Text/TextField.cs diff --git a/src/EPPlus.Export.ImageRenderer/RenderItems/SvgItem/SvgTextBoxItem.cs b/src/EPPlus.Export.ImageRenderer/RenderItems/SvgItem/SvgTextBoxItem.cs index 722435e7a..3d16e9d78 100644 --- a/src/EPPlus.Export.ImageRenderer/RenderItems/SvgItem/SvgTextBoxItem.cs +++ b/src/EPPlus.Export.ImageRenderer/RenderItems/SvgItem/SvgTextBoxItem.cs @@ -9,7 +9,6 @@ using System.Collections.Generic; using System.Drawing; using System.Xml.Serialization; -using static System.Net.Mime.MediaTypeNames; namespace EPPlus.Export.ImageRenderer.RenderItems.SvgItem { diff --git a/src/EPPlus.Export.ImageRenderer/Svg/SvgParagraph.cs b/src/EPPlus.Export.ImageRenderer/Svg/SvgParagraph.cs deleted file mode 100644 index d75c96e9e..000000000 --- a/src/EPPlus.Export.ImageRenderer/Svg/SvgParagraph.cs +++ /dev/null @@ -1,403 +0,0 @@ -/************************************************************************************************* - Required Notice: Copyright (C) EPPlus Software AB. - This software is licensed under PolyForm Noncommercial License 1.0.0 - and may only be used for noncommercial purposes - https://polyformproject.org/licenses/noncommercial/1.0.0/ - - A commercial license to use this software can be purchased at https://epplussoftware.com - ************************************************************************************************* - Date Author Change - ************************************************************************************************* - 27/11/2025 EPPlus Software AB EPPlus 9 - *************************************************************************************************/ -using EPPlus.Fonts.OpenType; -using EPPlus.Fonts.OpenType.Utils; -using EPPlusImageRenderer.RenderItems; -using OfficeOpenXml.Drawing; -using OfficeOpenXml.Interfaces.Drawing.Text; -using OfficeOpenXml.Style; -using System; -using System.Collections.Generic; -using System.Globalization; -using System.Linq; -using System.Text; -using System.Xml.Schema; - -namespace EPPlusImageRenderer.Svg -{ //internal class SvgParagraph : SvgRenderItem - //{ - // int numLines = 0; - - // public override void Render(StringBuilder sb) - // { - // sb.Append(""); - - // if (TextRuns.Count > 0) - // { - // foreach (var textRun in TextRuns) - // { - // textRun.Render(sb); - // } - // } - - // sb.Append(""); - // } - - // public override RenderItemType Type => RenderItemType.Text; - - // internal override SvgRenderItem Clone(SvgShape svgDocument) - // { - // throw new NotImplementedException(); - // } - - // internal override void GetBounds(out double il, out double it, out double ir, out double ib) - // { - // il = ParagraphArea.Left + LeftMargin; - // it = ParagraphArea.Top; - // ir = ParagraphArea.Right - RightMargin; - // ib = ParagraphArea.Bottom; - // } - - // string vertAlignAttribute = ""; - // protected List TextRuns = new List(); - - // //ExcelDrawingParagraph Paragraph; - // double RightMargin; - // double LeftMargin; - - // eTextAlignment HorizontalAlignment; - - // RectBase ParagraphArea; - - // /// - // /// Left position - // /// - // protected double XPos; - - // /// - // /// Line-Spacing in pixels - // /// - // protected double LineSpacing; - // protected double LineSpacingAscendantOnly; - - // MeasurementFont _measurementFont; - - // bool IsFirstParagraph = false; - - // ITextMeasurerWrap fmtt; - - // double? lnMultiplier = null; - // double paragraphHeight; - // private string text; - // private ExcelTextFont font; - // private RectBase area; - // private double posY; - // /// - // /// First paragraph must use different linespacing - // /// - // /// paragraph data. Ideally only used in constructor as datasource - // /// - // /// - // /// - // /// - // /// - // public SvgParagraph(ExcelDrawingParagraph p, RectBase paragraphArea, string VertAlign, double yPosition, bool isFirstParagraph = false) - // { - // fmtt = p._prd.Package.Settings.TextSettings.GenericTextMeasurerTrueType; - - // _measurementFont = p.DefaultRunProperties.GetMeasureFont(); - // fmtt.SetFont(_measurementFont); - // vertAlignAttribute = VertAlign; - - // //Seperated out in case of some final render item - // //needing to adjust without changing the original values - // RightMargin = p.RightMargin; - - // //p.Indent - // //0.5 inches per indent level 48 pixels - - // var indent = 48 * p.IndentLevel; - // LeftMargin = p.LeftMargin + p.Indent + indent; - - // ParagraphArea = paragraphArea; - // HorizontalAlignment = p.HorizontalAlignment; - - // //Must be set before linespacing - // IsFirstParagraph = isFirstParagraph; - - - // XPos = GetAlignmentHorizontal(HorizontalAlignment); - - // GetBounds(out double l, out double t, out double r, out double b); - // var textMaxWidth = r - l; - - // foreach (var run in p.TextRuns) - // { - // AddTextRun(run, paragraphArea.Bottom, yPosition); - // } - - // if (p._paragraphs.WrapText == eTextWrappingType.Square) - // { - // List textFragments = new List(); - // List fonts = new List(); - - // foreach (var txtRun in p.TextRuns) - // { - // textFragments.Add(txtRun.Text); - // fonts.Add(txtRun.GetMeasurementFont()); - // } - - // var trueTypeMeasurer = (FontMeasurerTrueType)fmtt; - // var maxWidthPoints = textMaxWidth.PixelToPoint(); - // var svgLines = trueTypeMeasurer.WrapMultipleTextFragments(textFragments, fonts, maxWidthPoints); - - // numLines = svgLines.Count; - - // List txtRunStrings = new List(); - // List txtRunStartIndicies = new List(); - // List txtRunEndIndicies = new List(); - - // int lastIndex = 0; - // for (int i = 0; i < TextRuns.Count; i++) - // { - // var txtString = TextRuns[i].originalText; - // txtRunStrings.Add(txtString); - // var indexOfRun = p.Text.IndexOf(txtString, lastIndex); - // txtRunStartIndicies.Add(indexOfRun); - // txtRunEndIndicies.Add(indexOfRun + txtString.Length); - // lastIndex = indexOfRun; - // } - - // List lineIndicies = new List(); - // lastIndex = 0; - - // //Last line should be handled by paragraph handling - // for (int i = 0; i< svgLines.Count(); i++) - // { - // var txtString = svgLines[i]; - // txtRunStrings.Add(txtString); - // var startIndex = p.Text.IndexOf(txtString, lastIndex); - // lastIndex = startIndex + txtString.Length; - // lineIndicies.Add(startIndex + txtString.Length); - // } - - // for (int i = 0; i < lineIndicies.Count; i++) - // { - // var lnBreakPosition = lineIndicies[i]; - // for (int j = 0; j < txtRunEndIndicies.Count; j++) - // { - // var start = txtRunStartIndicies[j]; - // var end = txtRunEndIndicies[j]; - - // bool containsBreak = (start <= lnBreakPosition && lnBreakPosition < end); - // if (containsBreak) - // { - // var localLnBreakPosition = lnBreakPosition - start; - // TextRuns[j].InsertLineBreak(localLnBreakPosition); - // break; - // } - // } - // } - // SetParagraphLineSpacingInPixels(p, fmtt); - // } - // else - // { - // numLines = p.Text.Split(new string[] { Environment.NewLine }, StringSplitOptions.None).Count(); - // } - // } - - - - // /// - // /// First paragraph must use different linespacing - // /// - // /// paragraph data. Ideally only used in constructor as datasource - // /// - // /// - // /// - // /// - // /// - // public SvgParagraph(string text, ExcelTextFont font, RectBase paragraphArea, string VertAlign, double yPosition) - // { - // fmtt = font.PictureRelationDocument.Package.Settings.TextSettings.GenericTextMeasurerTrueType; - - // _measurementFont = font.GetMeasureFont(); - // fmtt.SetFont(_measurementFont); - // vertAlignAttribute = VertAlign; - - // //p.Indent - // //0.5 inches per indent level 48 pixels - - // ParagraphArea = paragraphArea; - // HorizontalAlignment = eTextAlignment.Left; - - // LineSpacingAscendantOnly = fmtt.GetBaseLine().PointToPixel(); - // LineSpacing = fmtt.GetSingleLineSpacing().PointToPixel(); - - // //Must be set before linespacing - // IsFirstParagraph = true; - - // XPos = GetAlignmentHorizontal(HorizontalAlignment); - - // //GetBounds(out double l, out double t, out double r, out double b); - // //var textMaxWidth = r - l; - - // var tr = new SvgTextRun(text, font, LineSpacing, paragraphArea.Bottom, XPos, yPosition, LineSpacingAscendantOnly); - // TextRuns.Add(tr); - // } - - // private string GetHorizontalAlignmentAttribute(double indentX) - // { - // string ret = ""; - // var xStr = indentX.ToString(CultureInfo.InvariantCulture); - - // switch (HorizontalAlignment) - // { - // default: - // case eTextAlignment.Left: - // ret = $"text-anchor=\"start\" x=\"{xStr}\" "; - // break; - // case eTextAlignment.Center: - // ret = $"text-anchor=\"middle\" x=\"{xStr}\" "; - // break; - // case eTextAlignment.Right: - - // ret = $"text-anchor=\"end\" x=\"{xStr}\" "; - // break; - // } - - // return ret; - // } - - // private void SetParagraphLineSpacingInPixels(ExcelDrawingParagraph p, ITextMeasurerWrap fmExact) - // { - // if (p.LineSpacing.LineSpacingType == eDrawingTextLineSpacing.Exactly) - // { - // if (IsFirstParagraph) - // { - // LineSpacingAscendantOnly = p.LineSpacing.Value.PointToPixel(); - // } - // var lineSpacing= p.LineSpacing.Value.PointToPixel(); - // var lines = 1; - // foreach (var tr in TextRuns) - // { - // var lc = tr.GetLineCount(); - // if(lc>1) - // { - // lines += lc - 1; - // } - // } - // LineSpacing = lineSpacing * lines; - // } - // else - // { - // var multiplier = (p.LineSpacing.Value / 100); - // lnMultiplier = multiplier; - // if (IsFirstParagraph) - // { - // LineSpacingAscendantOnly = multiplier * fmExact.GetBaseLine().PointToPixel(); - // } - // if (TextRuns.Count == 0) - // { - // LineSpacing = multiplier * fmExact.GetSingleLineSpacing().PointToPixel(); - // } - // else - // { - // var lineSpacing=0D; - // var currentMaxLineSpacing = 0D; - // foreach(var tr in TextRuns) - // { - // if(currentMaxLineSpacing < tr.LineSpacing) - // { - // currentMaxLineSpacing = tr.LineSpacing; - // } - // var lc = tr.GetLineCount(); - // if (lc > 1) - // { - // lineSpacing += currentMaxLineSpacing; - // currentMaxLineSpacing = tr.LineSpacing; - // if(lc>2) - // { - // lineSpacing += (lc - 2) * tr.LineSpacing; - // } - // } - // } - // LineSpacing = multiplier * (lineSpacing + currentMaxLineSpacing); - // } - // } - // } - - // internal double GetAlignmentHorizontal(eTextAlignment txAlignment) - // { - // var area = ParagraphArea; - // double x = 0; - // switch (txAlignment) - // { - // case eTextAlignment.Left: - // default: - // x = area.Left + LeftMargin; - // break; - // case eTextAlignment.Center: - // x = (area.Right / 2) + LeftMargin - RightMargin; - // break; - // case eTextAlignment.Right: - // x = area.Right - RightMargin; - // break; - // } - - // return TextUtils.RoundToWhole(x); - // } - - // internal void AddTextRun(ExcelParagraphTextRunBase txtRun, double clippingHeight, double yPosition) - // { - // GetBounds(out double l, out double t, out double r, out double b); - // var textMaxWidth = r - l; - - // SvgTextRun textRun; - - // //if (TextRuns.Count == 0 && IsFirstParagraph == true) - // //{ - // // textRun = new SvgTextRun(txtRun, textMaxWidth, clippingHeight, XPos, yPosition); - // //} - // //else - // //{ - // textRun = new SvgTextRun(txtRun, textMaxWidth, clippingHeight, XPos, yPosition); - - // //If there are multiple sizes/multiple fonts with multiple sizes - // //if (lnType != eDrawingTextLineSpacing.Exactly && txtRun.FontSize != _measurementFont.Bounds) - // if(lnMultiplier.HasValue) - // { - // textRun.AdjustLineSpacing(lnMultiplier.Value); - // } - // //} - - // TextRuns.Add(textRun); - // } - - // internal double GetBottomYPosition() - // { - // //double bottomY = 0; - // //if (IsFirstParagraph) - // //{ - // // bottomY = LineSpacingAscendantOnly + LineSpacing * (numLines - 1); - // //} - // //else - // //{ - // // var bottomY = LineSpacing; - // //} - // return ParagraphArea.Top + LineSpacing; - // } - - // internal void CalculateTextWrapping(double maxWidth, MeasurementFont mFont, string fullParagraphText) - // { - // List NewContentLines = new List(); - // fmtt.SetFont(mFont); - // var textWidth = fmtt.MeasureText(fullParagraphText, mFont); - // } - //} -} \ No newline at end of file diff --git a/src/EPPlus.Export.ImageRenderer/Svg/SvgTextRun.cs b/src/EPPlus.Export.ImageRenderer/Svg/SvgTextRun.cs deleted file mode 100644 index e83da3b7f..000000000 --- a/src/EPPlus.Export.ImageRenderer/Svg/SvgTextRun.cs +++ /dev/null @@ -1,540 +0,0 @@ -/************************************************************************************************* - Required Notice: Copyright (C) EPPlus Software AB. - This software is licensed under PolyForm Noncommercial License 1.0.0 - and may only be used for noncommercial purposes - https://polyformproject.org/licenses/noncommercial/1.0.0/ - - A commercial license to use this software can be purchased at https://epplussoftware.com - ************************************************************************************************* - Date Author Change - ************************************************************************************************* - 27/11/2025 EPPlus Software AB EPPlus 9 - *************************************************************************************************/ -using EPPlus.Fonts.OpenType; -using EPPlus.Fonts.OpenType.Utils; -using EPPlusImageRenderer.RenderItems; -using OfficeOpenXml.Drawing; -using OfficeOpenXml.Interfaces.Drawing.Text; -using System; -using System.Collections.Generic; -using System.Globalization; -using System.Linq; -using System.Text; -using OfficeOpenXml.Style; -using OfficeOpenXml.Utils; -using System.Text.RegularExpressions; - -namespace EPPlusImageRenderer.Svg -{ - //internal class SvgTextRun : SvgRenderItem - //{ - // internal double LineSpacing { get; set; } - // double _yEndPos; - // double ClippingHeight = Double.NaN; - // double fontSizeInPixels; - - // internal RectBase BoundingBox = new RectBase(); - // internal Coordinate origin = new Coordinate(0, 0); - // List Lines; - - // MeasurementFont measurementFont; - // FontMeasurerTrueType fmExact; - - // //Unnecesary?? - // double TextLengthInPixels; - // double LineSpacingAscendantOnly; - - // string horizontalAttribute; - // internal readonly string originalText; - // private string currentText; - - // double _xPosition; - - // string fontStyleAttributes; - - // bool isFirstInParagraph; - - // /// - // /// constructor. enter linespacing in pixels - // /// - // /// - // /// - // internal SvgTextRun(ExcelParagraphTextRunBase textRun, double textMaxX, double textMaxY, double xPosition, double yPosition) : base() - // { - // originalText = textRun.Text; - // Lines = SplitIntoLines(originalText); - // currentText = originalText; - - // measurementFont = textRun.GetMeasureFont(); - - // isFirstInParagraph = textRun.IsFirstInParagraph; - - // if(textRun.Fill.Style==eFillStyle.SolidFill) - // { - // FillColor = "#"+textRun.Fill.Color.To6CharHexString(); - // } - - // fmExact = new FontMeasurerTrueType(measurementFont); - - // //Used for the first line - // LineSpacingAscendantOnly = fmExact.GetBaseLine().PointToPixel(); - - // //LineSpacing = lineSpacing; - // LineSpacing = fmExact.GetSingleLineSpacing().PointToPixel(); - - - // _xPosition = xPosition; - // _yEndPos = yPosition; - - // fontSizeInPixels = ((double)measurementFont.Bounds).PointToPixel(true); - - // ClippingHeight = textMaxY; - // if (textRun.Paragraph._paragraphs.WrapText == eTextWrappingType.Square) - // { - // CalculateTextWrapping(textMaxX); - // } - - // if (textRun.FontItalic) - // { - // fontStyleAttributes += " font-style=\"italic\" "; - // } - // if(textRun.FontBold) - // { - // fontStyleAttributes += "font-weight=\"bold\" "; - // } - // if(textRun.FontUnderLine != eUnderLineType.None | textRun.FontStrike != eStrikeType.No) - // { - - // fontStyleAttributes += "text-decoration=\""; - // if (textRun.FontUnderLine != eUnderLineType.None) - // { - // switch (textRun.FontUnderLine) - // { - // case eUnderLineType.Single: - // fontStyleAttributes += "underline"; - // break; - // //These are all css only apparently - // //case eUnderLineType.Double: - // // fontStyleAttributes += "double"; - // // break; - // //case eUnderLineType.Dotted: - // // fontStyleAttributes += "dotted"; - // // break; - // //case eUnderLineType.Dash: - // // fontStyleAttributes += "dashed"; - // // break; - // //case eUnderLineType.Wavy: - // // fontStyleAttributes += "wavy"; - // // break; - // default: - // fontStyleAttributes += "underline"; - // break; - // //throw new NotImplementedException("Not implemented yet"); - // } - // } - - // if(textRun.FontStrike == eStrikeType.Single) - // { - // if(textRun.FontUnderLine != eUnderLineType.None) - // { - // fontStyleAttributes += ","; - // } - // fontStyleAttributes += "line-through"; - // } - - // fontStyleAttributes += "\" "; - // } - // } - // /// - // /// A item with a font property and no rich text. - // /// - // /// The text - // /// The font - // /// - // /// - // /// - // /// - // /// - // /// - // internal SvgTextRun(string text, ExcelTextFont font, double lineSpacing, double textMaxY, double xPosition, double yPosition, double baselineLineSpacing = double.NaN) : base() - // { - // originalText = text; - // currentText = text; - // isFirstInParagraph = true; - // Lines = SplitIntoLines(originalText); - - // measurementFont = font.GetMeasureFont(); - - - // if (font.Fill.Style == eFillStyle.SolidFill) - // { - // FillColor = "#" + font.Fill.Color.To6CharHexString(); - // } - - // fmExact = new FontMeasurerTrueType(measurementFont); - - // //horizontalTextAlignment = font..Paragraph.HorizontalAlignment; - - // LineSpacing = lineSpacing; - - // //Used for the first line - // LineSpacingAscendantOnly = baselineLineSpacing; - - // _xPosition = xPosition; - // _yEndPos = yPosition; - - // //origin.Left = xPosition; - // //origin.Top = yPosition; - - // fontSizeInPixels = ((double)measurementFont.Bounds).PointToPixel(true); - - // //ClippingHeight = textMaxY; - // //CalculateTextWrapping(textMaxX); - - // if (font.Italic) - // { - // fontStyleAttributes += " _measurementFont-style=\"italic\" "; - // } - // if (font.Bold) - // { - // fontStyleAttributes += "_measurementFont-weight=\"bold\" "; - // } - // if (font.UnderLine != eUnderLineType.None | font.Strike != eStrikeType.No) - // { - - // fontStyleAttributes += "text-decoration=\""; - // if (font.UnderLine != eUnderLineType.None) - // { - // switch (font.UnderLine) - // { - // case eUnderLineType.Single: - // fontStyleAttributes += "underline"; - // break; - // //These are all css only apparently - // //case eUnderLineType.Double: - // // fontStyleAttributes += "double"; - // // break; - // //case eUnderLineType.Dotted: - // // fontStyleAttributes += "dotted"; - // // break; - // //case eUnderLineType.Dash: - // // fontStyleAttributes += "dashed"; - // // break; - // //case eUnderLineType.Wavy: - // // fontStyleAttributes += "wavy"; - // // break; - // default: - // fontStyleAttributes += "underline"; - // break; - // //throw new NotImplementedException("Not implemented yet"); - // } - // } - - // if (font.Strike == eStrikeType.Single) - // { - // if (font.UnderLine != eUnderLineType.None) - // { - // fontStyleAttributes += ","; - // } - // fontStyleAttributes += "line-through"; - // } - - // fontStyleAttributes += "\" "; - // } - // } - - // //internal SvgTextRun(ExcelRichText textRun, double lineSpacing, double textMaxX, double textMaxY, double xPosition, double yPosition, MeasurementFont mf, ExcelHorizontalAlignment horAlign, double baselineLineSpacing = double.NaN) : base() - // //{ - // // originalText = textRun.Textbox; - // // Lines = SplitIntoLines(originalText); - - // // fmExact = new FontMeasurerTrueType(mf); - // // measurementFont = mf; - - // // horizontalTextAlignment = (eTextAlignment)horAlign; - - // // LineSpacing = fmExact.GetSingleLineSpacing().PointToPixel(true); - - // // //Used for the first line - // // LineSpacingAscendantOnly = fmExact.GetBaseLine().PointToPixel(true); - - // // _xPosition = xPosition; - // // _yPosition = yPosition; - - // // fontSizeInPixels = ((double)mf.Bounds).PointToPixel(true); - - // // ClippingHeight = textMaxY; - // // //No idea how to get this property now since we have no access to textbody/there may not be a direct text body as this might be a cell - // // bool wrapText = true; - // // if (wrapText) - // // { - // // CalculateTextWrapping(textMaxX); - // // } - - // // if (textRun.FontItalic) - // // { - // // fontStyleAttributes += " font-style=\"italic\" "; - // // } - // // if(textRun.FontBold) - // // { - // // fontStyleAttributes += "font-weight=\"bold\" "; - // // } - // // if(textRun.FontUnderLine != eUnderLineType.None | textRun.FontStrike != eStrikeType.No) - // // { - - // // fontStyleAttributes += "text-decoration=\""; - // // if (textRun.FontUnderLine != eUnderLineType.None) - // // { - // // switch (textRun.FontUnderLine) - // // { - // // case eUnderLineType.Single: - // // fontStyleAttributes += "underline"; - // // break; - // // //These are all css only apparently - // // //case eUnderLineType.Double: - // // // fontStyleAttributes += "double"; - // // // break; - // // //case eUnderLineType.Dotted: - // // // fontStyleAttributes += "dotted"; - // // // break; - // // //case eUnderLineType.Dash: - // // // fontStyleAttributes += "dashed"; - // // // break; - // // //case eUnderLineType.Wavy: - // // // fontStyleAttributes += "wavy"; - // // // break; - // // default: - // // fontStyleAttributes += "underline"; - // // break; - // // //throw new NotImplementedException("Not implemented yet"); - // // } - // // } - - // // if(textRun.FontStrike == eStrikeType.Single) - // // { - // // if(textRun.FontUnderLine != eUnderLineType.None) - // // { - // // fontStyleAttributes += ","; - // // } - // // fontStyleAttributes += "line-through"; - // // } - - // // fontStyleAttributes += "\" "; - // // } - // //} - - // internal SvgTextRun(ExcelRichText textRun, double lineSpacing, double textMaxX, double textMaxY, double xPosition, double yPosition, MeasurementFont mf, ExcelHorizontalAlignment horAlign, double baselineLineSpacing = double.NaN) : base() - // { - // originalText = textRun.Text; - // Lines = SplitIntoLines(originalText); - - // fmExact = new FontMeasurerTrueType(mf); - // measurementFont = mf; - - // LineSpacing = fmExact.GetSingleLineSpacing().PointToPixel(true); - - // //Used for the first line - // LineSpacingAscendantOnly = fmExact.GetBaseLine().PointToPixel(true); - - // _xPosition = xPosition; - // _yEndPos = yPosition; - - // fontSizeInPixels = ((double)mf.Bounds).PointToPixel(true); - - // ClippingHeight = textMaxY; - // //No idea how to get this property now since we have no access to textbody - // bool wrapText = true; - // if (wrapText) - // { - // CalculateTextWrapping(textMaxX); - // } - // } - - // internal void AdjustLineSpacing(double lineMultiplier) - // { - // LineSpacing = lineMultiplier * fmExact.GetSingleLineSpacing().PointToPixel(true); - // } - - // public RectBase textArea; - - // public override RenderItemType Type => RenderItemType.TSpan; - - // public override void Render(StringBuilder sb) - // { - // string finalString = ""; - // bool useBaselineSpacing = double.IsNaN(LineSpacingAscendantOnly) == false; - // Lines = SplitIntoLines(currentText); - - // foreach (var line in Lines) - // { - // finalString += $"= ClippingHeight) - // { - // visibility = "display=\"none\""; - // } - - // var yIncreaseString = yIncrease.ToString(CultureInfo.InvariantCulture); - // var xString = $"x =\"{(_xPosition).ToString(CultureInfo.InvariantCulture)}\" "; - // var dyString = $"dy =\"{yIncreaseString}px\" "; - // finalString += xString; - // finalString += dyString; - // } - - // finalString += $"{visibility} " + $"{fontStyleAttributes} "; - // if (measurementFont != null) - // { - // finalString += $" font-family=\"{measurementFont.FontFamily}," - // + $"{measurementFont.FontFamily}_MSFontService,sans-serif\" " - // + $"font-size=\"{fontSizeInPixels.ToString(CultureInfo.InvariantCulture)}px\" "; - // } - // sb.Append(finalString); - // //Get color etc. - // base.Render(sb); - // finalString = ""; - - // finalString += ">"; - // finalString += line; - // finalString += ""; - // } - - // sb.Append(finalString); - // //throw new NotImplementedException(); - // } - - // internal override SvgRenderItem Clone(SvgShape svgDocument) - // { - // throw new NotImplementedException(); - // } - - // private int GetNumberOfLines() - // { - // if (originalText != null) - // { - // if(currentText == null) - // { - // currentText = originalText; - // } - - // SplitIntoLines(currentText); - // return Lines.Count; - // } - // else - // { - // return 0; - // } - // } - - // internal int GetLineCount() - // { - // if (Lines == null) return 0; - - // var count = 0; - // foreach (var l in Lines) - // { - // count=Regex.Matches(l, Environment.NewLine).Count+1; - // } - // return count; - // } - - // internal List SplitIntoLines(string text) - // { - // return (text ?? "").Split(new string[] { Environment.NewLine }, StringSplitOptions.None).ToList(); - // } - - // internal override void GetBounds(out double il, out double it, out double ir, out double ib) - // { - // il = origin.X; - // it = origin.Y; - - // ir = CalculateRightPositionInPixels(); - // ib = CalculateBottomPositionInPixels(); - - // BoundingBox.Left = il; - // BoundingBox.Top = it; - // BoundingBox.Right = ir; - // BoundingBox.Bottom = ib; - // } - - // internal double CalculateRightPositionInPixels() - // { - // var numLines = GetNumberOfLines(); - // double retPos = origin.X; - - // if (numLines <= 0 || string.IsNullOrEmpty(originalText)) - // { - // TextLengthInPixels = 0; - // return retPos; - // } - - // double longestWidth = -1; - - // for (int i = 0; i < numLines; i++) - // { - // var text = Lines[i]; - // var width = CalculateTextWidth(Lines[i]); - - // if (width > longestWidth) - // { - // longestWidth = width; - // } - // } - - // TextLengthInPixels = longestWidth; - - // retPos += longestWidth; - - // return retPos; - // } - // internal double CalculateBottomPositionInPixels() - // { - // var lineSize = ((double)measurementFont.Bounds).PointToPixel(true) + origin.Y; - // var bottomPosition = lineSize * GetNumberOfLines(); - // return bottomPosition; - // } - - // internal double CalculateTextWidth(string targetString) - // { - // var textMesurer = new FontMeasurerTrueType(measurementFont); - // textMesurer.MeasureWrappedTextCells = true; - // var width = textMesurer.MeasureTextWidth(targetString); - - // return width; - // } - - // internal void CalculateTextWrapping(double maxWidth) - // { - // List NewContentLines = new List(); - // var textMesurer = new FontMeasurerTrueType(measurementFont); - // var newLines = textMesurer.MeasureAndWrapText(originalText, measurementFont, maxWidth); - // Lines = newLines; - // } - - // internal void InsertLineBreak(int insertPosition) - // { - // while(insertPosition < currentText.Length && char.IsWhiteSpace(currentText[insertPosition])) - // { - // currentText=currentText.Remove(insertPosition, 1); - // } - // currentText = currentText.Insert(insertPosition, Environment.NewLine); - // //if(Lines.Count==1) - // //{ - // // Lines.Clear(); - // // Lines.Add(currentText.Substring(0, insertPosition)); - // // Lines.Add(currentText.Substring(insertPosition + Environment.NewLine.Length, currentText.Length-(insertPosition + Environment.NewLine.Length))); - // //} - // } - //} -} \ No newline at end of file diff --git a/src/EPPlus.Export.ImageRenderer/Text/TextBox.cs b/src/EPPlus.Export.ImageRenderer/Text/TextBox.cs deleted file mode 100644 index 4cbd10e16..000000000 --- a/src/EPPlus.Export.ImageRenderer/Text/TextBox.cs +++ /dev/null @@ -1,338 +0,0 @@ -/************************************************************************************************* - Required Notice: Copyright (C) EPPlus Software AB. - This software is licensed under PolyForm Noncommercial License 1.0.0 - and may only be used for noncommercial purposes - https://polyformproject.org/licenses/noncommercial/1.0.0/ - - A commercial license to use this software can be purchased at https://epplussoftware.com - ************************************************************************************************* - Date Author Change - ************************************************************************************************* - 27/11/2025 EPPlus Software AB EPPlus 9 - *************************************************************************************************/ -using EPPlus.Fonts.OpenType.Utils; -using EPPlusImageRenderer.RenderItems; -using EPPlusImageRenderer.Svg; -using OfficeOpenXml; -using OfficeOpenXml.Drawing; -using OfficeOpenXml.Utils; -using System; -using System.Collections.Generic; -using System.Globalization; -using System.Text; -using EPPlus.Graphics; -using System.Linq; -using EPPlus.Export.ImageRenderer.Utils; - -namespace EPPlusImageRenderer.Text -{ - //internal class TextBox : RenderItem - //{ - // internal RectMargins Bounds = new RectMargins(); - - // internal eTextAnchoringType VerticalAlignment=eTextAnchoringType.Top; - - // internal double ClippingHeight; - - // internal bool WrapText = true; - - // internal double Width - // { - // get; - // set; - // } - - // internal double Height - // { - // get; - // set; - // } - - // public override RenderItemType Type => RenderItemType.Rect; - - // /// - // /// Top of the paragraph bounding box - // /// - // double paragraphStartPosY = 0; - // private OfficeOpenXml.Interfaces.Drawing.Text.MeasurementFont mFontTextRun = null; - - // List Paragraphs = new List(); - // List CellTextRuns = new List(); - - // internal string fontColor; - - // /// - // /// The input parameters are assumed to be in pixels - // /// - // /// - // /// - // /// - // /// - // internal TextBox(ExcelDrawing drawing, double l, double t, double width, double height) : base(drawing.GetBoundingBox()) - // { - // Bounds.Left = l; Bounds.Top = t; Width = width; Height = height; - - // Bounds.Right = Bounds.Left + Width; - // Bounds.Bottom = Bounds.Top + Height; - - // //Initalize margins to 0 - // Bounds.MarginLeft = Bounds.MarginRight = Bounds.MarginTop = Bounds.MarginBottom = 0; - - // paragraphStartPosY = Bounds.Top; - - // ClippingHeight = Bounds.GetInnerBottom(); - // } - - // ///// - // ///// It is pressumed that txBody has units in EMUs and SvgRenderRectItem in pixels - // ///// - // ///// - // ///// - // //internal TextBox(ExcelTextBody txBody, SvgRenderRectItem textBoxRender) - // //{ - // // Bounds.Left = textBoxRender.Left; - // // Bounds.Top = textBoxRender.Top; - - // // //var Transform = Bounds.Transform; - - // // //var pos = Transform.Position; - - // // //pos.Y = 2; - - // // //Transform.LocalPosition.X = textBoxRender.X; - // // //Transform.LocalPosition.Y = textBoxRender.Y; - - // // Width = textBoxRender.Width; - // // Height = textBoxRender.Height; - - // // Bounds.Right = Bounds.Left + Width; - // // Bounds.Bottom = Bounds.Top + Height; - - // // txBody.GetInsetsOrDefaults(out double l, out double r, out double t, out double b); - - // // //Ensure margins are in pixels - // // Bounds.MarginLeft = l.PointToPixel(); - // // Bounds.MarginRight = r.PointToPixel(); - // // Bounds.MarginTop = t.PointToPixel(); - // // Bounds.MarginBottom = b.PointToPixel(); - - // // paragraphStartPosY = Bounds.Top + Bounds.MarginTop; - - // // ClippingHeight = Bounds.GetInnerBottom(); - // //} - - // //public TextBox() - // //{ - // //} - - // double? _alignmentY = null; - - // /// - // /// Get the start of text space vertically - // /// - // /// - // /// - // private double GetAlignmentVertical() - // { - // double alignmentY = 0; - // double y = paragraphStartPosY; - // var height = y - Bounds.Bottom; - - // switch (VerticalAlignment) - // { - // case eTextAnchoringType.Top: - // alignmentY = y; - // break; - // case eTextAnchoringType.Center: - // var adjustedHeight = y + (height / 2); - - // alignmentY = adjustedHeight + Bounds.MarginTop; - // break; - // case eTextAnchoringType.Bottom: - // alignmentY = height - Bounds.MarginBottom; - // break; - // } - - // _alignmentY = alignmentY; - - // return _alignmentY.Value; - // } - - // private string GetVerticalAlignAttribute(double textOrigY) - // { - // string ret = "dominant-baseline="; - - // switch (VerticalAlignment) - // { - // case eTextAnchoringType.Top: - // ret += $"\"text-top\" "; - // break; - // case eTextAnchoringType.Center: - // ret += $"\"middle\" "; - // break; - // case eTextAnchoringType.Bottom: - // ret += $"\"text-bottom\" "; - // break; - // default: - // return ""; - // } - - // var yStrValue = textOrigY.ToString(CultureInfo.InvariantCulture); - // ret += $" y=\"{yStrValue}\""; - - // return ret; - // } - - // public RectBase GetTextArea() - // { - // return Bounds.GetInnerRect(); - // } - - // internal SvgParagraph ImportParagraph(ExcelDrawingParagraph item) - // { - // var measureFont = item.GetMeasurementFont(); - - // //Document Top position for the paragraph text based on vertical alignment - // var posY = GetAlignmentVertical(); - // var vertAlignAttribute = GetVerticalAlignAttribute(posY); - - // var area = GetTextArea(); - - // //Limit bounding area with the space taken by previous paragraphs - // //Note that this is ONLY identical to PosY if the vertical alignment is top - // area.Top = paragraphStartPosY; - - // //The first run in the first paragraph must apply different line-spacing - // bool isFirst = Paragraphs.Count == 0; - // var svgParagraph = new SvgParagraph(item, area, vertAlignAttribute, posY, isFirst); - - // //Set starting position for next paragraph - // paragraphStartPosY = svgParagraph.GetBottomYPosition(); - - // svgParagraph.FillColor = string.IsNullOrEmpty(fontColor) ? item.DefaultRunProperties.Fill.Color.Name : fontColor; - - // //Above we've calculated heights from empty paragraphs - // //But below we do not add them for rendering (As they would not have any visible effect anyway) - // if (item.TextRuns.Count != 0) - // { - // Paragraphs.Add(svgParagraph); - // } - - // return svgParagraph; - // } - - // internal void RemoveParagraph(SvgParagraph item) - // { - // if(Paragraphs.Contains(item)) - // { - // Paragraphs.Remove(item); - // } - // } - // internal override void GetBounds(out double il, out double it, out double ir, out double ib) - // { - // il = Bounds.GetInnerLeft(); - // ir = Bounds.GetInnerRight(); - // it = Bounds.GetInnerTop(); - // ib = Bounds.GetInnerBottom(); - // } - - // public override void Render(StringBuilder sb) - // { - // RenderParagraphs(sb); - // } - - // internal void RenderParagraphs(StringBuilder sb) - // { - // //TODO: add textbody property stuff here - // foreach (var paragraph in Paragraphs) - // { - // paragraph.Render(sb); - // } - // } - // internal void RenderTextRuns(StringBuilder sb) - // { - // var groupItem = new SvgGroupItem(); - // groupItem.Render(sb); - - // var innerTop = Bounds.GetInnerRect().Top; - // var fontFamilyAttr = $"_measurementFont-family=\"{mFontTextRun.FontFamily},{mFontTextRun.FontFamily}_MSFontService,sans-serif\" "; - // sb.Append($""); - // //TODO: add textbody property stuff here - // foreach (var textRun in CellTextRuns) - // { - // textRun.Render(sb); - // } - // sb.Append(""); - // groupItem.RenderEndGroup(sb); - // } - - // internal void AddCellTextRun(ExcelRangeBase cell) - // { - // var fontStyle = cell.Style.Font; - - // //Line spacing in points and left margin in pixels - // double lineSpacing = (0.205d * fontStyle.Bounds + 1); - // Bounds.MarginLeft = lineSpacing; - // Bounds.MarginRight = lineSpacing; - - // mFontTextRun = fontStyle.GetMeasureFont(); - - // var lineSpacingPixels = (lineSpacing / 72d) * 92d; - - // var posY = GetAlignmentVertical(); - // var vertAlignAttribute = GetVerticalAlignAttribute(posY); - - // var area = GetTextArea(); - // var horizontalAlign = cell.Style.HorizontalAlignment; - - - // foreach (var rt in cell.RichText) - // { - // var textrun = new SvgTextRun(rt, lineSpacingPixels, - // Bounds.GetInnerRight(), Bounds.GetInnerBottom(), - // Bounds.GetInnerLeft(), Bounds.GetInnerTop(), mFontTextRun, horizontalAlign); - - // CellTextRuns.Add(textrun); - // } - - // //var lines = CellTextRuns.Last().GetLineCount(); - - // //if(lines * ) - - // //if (botPos > Bounds.Height) - // //{ - // // //Negative values increases bounds - // // Bounds.Bottom = Bounds.Top + botPos; - // //} - // } - // public string Text { get; set; } - // public double Rotation { get; internal set; } - - //internal void AddText(string text, OfficeOpenXml.Style.ExcelTextFont font) - //{ - // var measureFont = font.GetMeasureFont(); - - // var area = GetTextArea(); - // paragraphStartPosY = area.Top; - // //Document Top position for the paragraph text based on vertical alignment - // var posY = GetAlignmentVertical(); - // var vertAlignAttribute = GetVerticalAlignAttribute(posY); - - - - // var measurer = font.PictureRelationDocument.Package.Settings.TextSettings.GenericTextMeasurerTrueType; - // var m = measurer.MeasureText(text, measureFont); - // //Limit bounding area with the space taken by previous paragraphs - // //Note that this is ONLY identical to PosY if the vertical alignment is top - - // //The first run in the first paragraph must apply different line-spacing - // var svgParagraph = new SvgParagraph(text, font, area, vertAlignAttribute, posY); - - // svgParagraph.FillColor = font.Fill.Color.To6CharHexString(); - - // paragraphStartPosY = svgParagraph.GetBottomYPosition(); - - // Paragraphs.Add(svgParagraph); - //} - //} -} diff --git a/src/EPPlus.Export.ImageRenderer/Text/TextField.cs b/src/EPPlus.Export.ImageRenderer/Text/TextField.cs deleted file mode 100644 index f3b4e1c52..000000000 --- a/src/EPPlus.Export.ImageRenderer/Text/TextField.cs +++ /dev/null @@ -1,6 +0,0 @@ -namespace EPPlusImageRenderer.Text -{ - internal class TextField - { - } -} From d4fbee27fdb7321814a39ffd5b0f2baf7810e9ba Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ossian=20Edstr=C3=B6m?= Date: Mon, 9 Feb 2026 15:10:46 +0100 Subject: [PATCH 052/151] Removed old unused TestContainer debug concept --- .../TestTextContainer.cs | 132 ----------------- .../ImageRenderer.cs | 133 +++++++++-------- .../RenderItems/Shared/ParagraphItem.cs | 3 +- .../RenderItems/SvgRenderRectItem.cs | 1 - .../ShapeDefinitions/ShapeDefinition.cs | 1 - .../Svg/Chart/SvgChartAxis.cs | 1 - .../Svg/Chart/SvgChartLegend.cs | 1 - .../Svg/Chart/SvgChartTitle.cs | 1 - .../Svg/SvgRange.cs | 1 - .../Svg/SvgShape.cs | 2 - .../Text/EpplusTextRun.cs | 66 --------- .../Text/FontWrapContainer.cs | 95 ------------ .../Text/LineFormatter.cs | 104 ------------- .../Text/TextContainer.cs | 140 ------------------ .../Text/TextContainerBase.cs | 54 ------- .../Text/TextWrapper.cs | 54 ------- 16 files changed, 67 insertions(+), 722 deletions(-) delete mode 100644 src/EPPlus.Export.ImageRenderer.Test/TestTextContainer.cs delete mode 100644 src/EPPlus.Export.ImageRenderer/Text/EpplusTextRun.cs delete mode 100644 src/EPPlus.Export.ImageRenderer/Text/FontWrapContainer.cs delete mode 100644 src/EPPlus.Export.ImageRenderer/Text/LineFormatter.cs delete mode 100644 src/EPPlus.Export.ImageRenderer/Text/TextContainer.cs delete mode 100644 src/EPPlus.Export.ImageRenderer/Text/TextContainerBase.cs delete mode 100644 src/EPPlus.Export.ImageRenderer/Text/TextWrapper.cs diff --git a/src/EPPlus.Export.ImageRenderer.Test/TestTextContainer.cs b/src/EPPlus.Export.ImageRenderer.Test/TestTextContainer.cs deleted file mode 100644 index 03f7fa1f6..000000000 --- a/src/EPPlus.Export.ImageRenderer.Test/TestTextContainer.cs +++ /dev/null @@ -1,132 +0,0 @@ -using EPPlus.Export.ImageRenderer.RenderItems.Shared; -using EPPlus.Export.ImageRenderer.RenderItems.SvgItem; -using EPPlus.Export.ImageRenderer.Text; -using EPPlus.Fonts.OpenType; -using EPPlus.Graphics; -using OfficeOpenXml; -using OfficeOpenXml.Drawing; -using OfficeOpenXml.Interfaces.Drawing.Text; -using OfficeOpenXml.Style; -using System; -using System.Collections.Generic; -using System.Globalization; -using System.Linq; -using System.Text; -using EPPlusImageRenderer; - -namespace EPPlus.Export.ImageRenderer.Tests -{ - [TestClass] - public class TestTextContainer - { - [TestMethod] - public void TestDynamicSizeSingleLine() - { - string content = "TextBox"; - var expectedWidth = 48d; - var expectedHeight = 18d; - - var mf = new MeasurementFont() { FontFamily = "Aptos", Size = 11, Style = MeasurementFontStyles.Regular }; - - var container = new TextContainer(content, mf, true); - - Assert.AreEqual(expectedWidth, Math.Round(container.Width, 0)); - Assert.AreEqual(expectedHeight, Math.Round(container.Height, 0)); - } - - [TestMethod] - public void TestDynamicSizeMultipleLines() - { - string content = "TextBox\r\na very long line2\r\nline3"; - var expectedWidth = 93d; - var expectedHeight = 54d; - - //0.57 inches - //0.98 inches - - var mf = new MeasurementFont() { FontFamily = "Aptos Narrow", Size = 11, Style = MeasurementFontStyles.Regular }; - - var container = new TextContainer(content, mf, true); - - Assert.AreEqual(expectedWidth, Math.Round(container.Width, 0)); - Assert.AreEqual(expectedHeight, Math.Round(container.Height, 0)); - } - - [TestMethod] - public void TestNonStandardFontSizesMultiLine() - { - string content = "TextBox\r\na very long line2\r\nline3"; - var expectedWidth = 203d; - var expectedHeight = 117d; - - var mf = new MeasurementFont() { FontFamily = "Aptos Narrow", Size = 24, Style = MeasurementFontStyles.Regular }; - - var container = new TextContainer(content, mf, true, true); - //0,056640625 * font size width correction for pixels (minimum) - Assert.AreEqual(expectedWidth, Math.Round(container.Width, 0)); - Assert.AreEqual(expectedHeight, Math.Round(container.Height, 0)); - } - - [TestMethod] - public void TestNonStandardFontSizesMultiLineLargeFont() - { - string content = "TextBox\r\na very long line2\r\nline3"; - var expectedWidth = 813d; - var expectedHeight = 469d; - - var mf = new MeasurementFont() { FontFamily = "Aptos Narrow", Size = 96, Style = MeasurementFontStyles.Regular }; - - var container = new TextContainer(content, mf, true, true); - //0,072265625 * font size width correction for pixels - //0,0442708333333333 * font size height correction for pixels - Assert.AreEqual(expectedWidth, Math.Round(container.Width, 0)); - Assert.AreEqual(expectedHeight, Math.Round(container.Height, 0)); - } - - [TestMethod] - public void TestNonStandardFontSizesMultiLineLargeFontGoudyStout() - { - string content = "TextBox\r\na very long line2\r\nline3"; - //This is the actual height in excel. - //var expectedHeight = 533d; - //It is unclear why as the Max possible glyph height in pixels for each line is - //175.125 pixels. *3 = 525.375 - //Best guess is it adds 4 pixels per row for potential "outline" - - //This is the glyph yMax and YMin - var expectedHeight = 525d; - //Excel has 2332d but they add some kind of internal buffer/minX spacing for lineEnding - var expectedWidth = 2309d; - - var mf = new MeasurementFont() { FontFamily = "Goudy Stout", Size = 96, Style = MeasurementFontStyles.Regular }; - - var container = new TextContainer(content, mf, true, true); - //0,072265625 * font size width correction for pixels - //0,0442708333333333 * font size height correction for pixels - Assert.AreEqual(expectedWidth, Math.Round(container.Width, 0)); - Assert.AreEqual(expectedHeight, Math.Round(container.Height, 0)); - } - - //[TestMethod] - //public void EnsureTextBodyAddsRunsCorrectly() - //{ - // BoundingBox shapeRect = new BoundingBox(); - - // shapeRect.Width = 20; - // shapeRect.Height = 10; - - // FontMeasurerTrueType measurer = new FontMeasurerTrueType(12, "Aptos Narrow", FontSubFamily.Regular); - // var body = new SvgTextBodyItem(shapeRect); - - // body.Bounds.Name = "TxtBody"; - - // body.AddText("A new Paragraph", measurer); - // body.AddText("Second paragraph", measurer); - - // Assert.AreEqual(2, body.Paragraphs[0].Bounds.Transform.ChildObjects.Count); - // Assert.AreEqual(body.Bounds.Transform.ChildObjects[0], body.Paragraphs[0].Bounds.Transform); - - // Assert.AreEqual(shapeRect.Transform, body.Bounds.Transform.TopDrawingHandler); - //} - } -} diff --git a/src/EPPlus.Export.ImageRenderer/ImageRenderer.cs b/src/EPPlus.Export.ImageRenderer/ImageRenderer.cs index 0c933a8f0..9ad5dbfca 100644 --- a/src/EPPlus.Export.ImageRenderer/ImageRenderer.cs +++ b/src/EPPlus.Export.ImageRenderer/ImageRenderer.cs @@ -17,7 +17,6 @@ Date Author Change using EPPlus.Export.ImageRenderer.Svg; using EPPlus.Export.ImageRenderer.Svg.NodeAttributes; using EPPlus.Export.ImageRenderer.Svg.Writer; -using EPPlus.Export.ImageRenderer.Text; using EPPlus.Fonts.OpenType; using EPPlus.Graphics; using EPPlusImageRenderer.Svg; @@ -79,33 +78,33 @@ public string RenderDrawingToSvg(ExcelDrawing drawing) // return sb.ToString(); //} - public string RenderBox(string boxText) - { - string retStr = ""; - var container = new TextContainerBase(boxText); - var element = GenerateSvg(container); + //public string RenderBox(string boxText) + //{ + // string retStr = ""; + // var container = new TextContainerBase(boxText); + // var element = GenerateSvg(container); - using (var ms = EPPlusMemoryManager.GetStream()) - { - SvgWriter writer = new SvgWriter(ms, Encoding.UTF8); - writer.RenderSvgElement(element, true); - ms.Position = 0; - using (var sr = new StreamReader(ms)) - { - retStr = sr.ReadToEnd(); - return retStr; - } - } + // using (var ms = EPPlusMemoryManager.GetStream()) + // { + // SvgWriter writer = new SvgWriter(ms, Encoding.UTF8); + // writer.RenderSvgElement(element, true); + // ms.Position = 0; + // using (var sr = new StreamReader(ms)) + // { + // retStr = sr.ReadToEnd(); + // return retStr; + // } + // } - //writer.RenderSvgElement(element, true); + // //writer.RenderSvgElement(element, true); - //StreamReader reader = new StreamReader(ms); - //retStr = reader.ReadToEnd(); + // //StreamReader reader = new StreamReader(ms); + // //retStr = reader.ReadToEnd(); - ////SvgParagraph para = new SvgParagraph(container.GetContent(),); - ////var doc = new SvgEpplusDocument(); - //return retStr; - } + // ////SvgParagraph para = new SvgParagraph(container.GetContent(),); + // ////var doc = new SvgEpplusDocument(); + // //return retStr; + //} //public string RenderTextBody(ExcelTextBody body, double shapeWidth, double shapeHeight) //{ @@ -177,61 +176,61 @@ internal SvgElement GetDefinitions(BoundingBox boundingBox, out string nameId, b return def; } - internal SvgElement GenerateSvg(TextContainerBase container) - { - var fullString = container.GetContent(); + //internal SvgElement GenerateSvg(TextContainerBase container) + //{ + // var fullString = container.GetContent(); - var doc = new SvgEpplusDocument(500, 500); + // var doc = new SvgEpplusDocument(500, 500); - var bg = new SvgElement("rect"); - bg.AddAttribute("width", "100%"); - bg.AddAttribute("height", "100%"); - bg.AddAttribute("fill", "red"); - bg.AddAttribute("opacity", "0.1"); + // var bg = new SvgElement("rect"); + // bg.AddAttribute("width", "100%"); + // bg.AddAttribute("height", "100%"); + // bg.AddAttribute("fill", "red"); + // bg.AddAttribute("opacity", "0.1"); - var nameId = "boundingBox"; - var def = new SvgElement("defs"); - var clipPath = new SvgElement("clipPath"); - clipPath.AddAttribute("id", nameId); + // var nameId = "boundingBox"; + // var def = new SvgElement("defs"); + // var clipPath = new SvgElement("clipPath"); + // clipPath.AddAttribute("id", nameId); - def.AddChildElement(clipPath); + // def.AddChildElement(clipPath); - var bb = new SvgElement("rect"); - bb.AddAttribute("x", container.Position.X); - bb.AddAttribute("y", container.Position.Y); - bb.AddAttribute("width", container.Width); - bb.AddAttribute("height", container.Height); - //bb.AddAttribute("fill", "blue"); - //bb.AddAttribute("opacity", "0.5"); + // var bb = new SvgElement("rect"); + // bb.AddAttribute("x", container.Position.X); + // bb.AddAttribute("y", container.Position.Y); + // bb.AddAttribute("width", container.Width); + // bb.AddAttribute("height", container.Height); + // //bb.AddAttribute("fill", "blue"); + // //bb.AddAttribute("opacity", "0.5"); - clipPath.AddChildElement(bb); + // clipPath.AddChildElement(bb); - var fontSizePx = 16d; + // var fontSizePx = 16d; - var renderElement = new SvgElement("text"); - renderElement.AddAttribute("x", container.Position.X); - renderElement.AddAttribute("y", container.Position.Y + fontSizePx); - renderElement.AddAttribute("_measurementFont-size", $"{fontSizePx}px"); - renderElement.AddAttribute("clip-path", $"url(#{nameId})"); + // var renderElement = new SvgElement("text"); + // renderElement.AddAttribute("x", container.Position.X); + // renderElement.AddAttribute("y", container.Position.Y + fontSizePx); + // renderElement.AddAttribute("_measurementFont-size", $"{fontSizePx}px"); + // renderElement.AddAttribute("clip-path", $"url(#{nameId})"); - renderElement.Content = fullString; + // renderElement.Content = fullString; - var bbVisual = new SvgElement("rect"); - bbVisual.AddAttribute("x", container.Position.X); - bbVisual.AddAttribute("y", container.Position.Y); - bbVisual.AddAttribute("width", container.Width); - bbVisual.AddAttribute("height", container.Height); - bbVisual.AddAttribute("fill", "blue"); - bbVisual.AddAttribute("opacity", "0.5"); + // var bbVisual = new SvgElement("rect"); + // bbVisual.AddAttribute("x", container.Position.X); + // bbVisual.AddAttribute("y", container.Position.Y); + // bbVisual.AddAttribute("width", container.Width); + // bbVisual.AddAttribute("height", container.Height); + // bbVisual.AddAttribute("fill", "blue"); + // bbVisual.AddAttribute("opacity", "0.5"); - doc.AddChildElement(def); - doc.AddChildElement(bg); - doc.AddChildElement(bbVisual); - doc.AddChildElement(renderElement); + // doc.AddChildElement(def); + // doc.AddChildElement(bg); + // doc.AddChildElement(bbVisual); + // doc.AddChildElement(renderElement); - doc.AddAttributes(); + // doc.AddAttributes(); - return doc; - } + // return doc; + //} } } diff --git a/src/EPPlus.Export.ImageRenderer/RenderItems/Shared/ParagraphItem.cs b/src/EPPlus.Export.ImageRenderer/RenderItems/Shared/ParagraphItem.cs index 9ca3ab6cd..9817f46e0 100644 --- a/src/EPPlus.Export.ImageRenderer/RenderItems/Shared/ParagraphItem.cs +++ b/src/EPPlus.Export.ImageRenderer/RenderItems/Shared/ParagraphItem.cs @@ -1,5 +1,4 @@ -using EPPlus.Export.ImageRenderer.Text; -using EPPlus.Fonts.OpenType; +using EPPlus.Fonts.OpenType; using EPPlus.Fonts.OpenType.TrueTypeMeasurer.DataHolders; using EPPlus.Fonts.OpenType.Utils; using EPPlus.Graphics; diff --git a/src/EPPlus.Export.ImageRenderer/RenderItems/SvgRenderRectItem.cs b/src/EPPlus.Export.ImageRenderer/RenderItems/SvgRenderRectItem.cs index c9823d791..5c2261dd4 100644 --- a/src/EPPlus.Export.ImageRenderer/RenderItems/SvgRenderRectItem.cs +++ b/src/EPPlus.Export.ImageRenderer/RenderItems/SvgRenderRectItem.cs @@ -11,7 +11,6 @@ Date Author Change 27/11/2025 EPPlus Software AB EPPlus 9 *************************************************************************************************/ using EPPlusImageRenderer.Svg; -using EPPlusImageRenderer.Text; using OfficeOpenXml.Drawing; using System.Collections.Generic; using System.Globalization; diff --git a/src/EPPlus.Export.ImageRenderer/ShapeDefinitions/ShapeDefinition.cs b/src/EPPlus.Export.ImageRenderer/ShapeDefinitions/ShapeDefinition.cs index a7f200384..d8d0a236d 100644 --- a/src/EPPlus.Export.ImageRenderer/ShapeDefinitions/ShapeDefinition.cs +++ b/src/EPPlus.Export.ImageRenderer/ShapeDefinitions/ShapeDefinition.cs @@ -10,7 +10,6 @@ Date Author Change ************************************************************************************************* 27/11/2025 EPPlus Software AB EPPlus 9 *************************************************************************************************/ -using EPPlus.Export.ImageRenderer.Text; using OfficeOpenXml.Drawing; using OfficeOpenXml.FormulaParsing.Excel.Functions.MathFunctions; using System; diff --git a/src/EPPlus.Export.ImageRenderer/Svg/Chart/SvgChartAxis.cs b/src/EPPlus.Export.ImageRenderer/Svg/Chart/SvgChartAxis.cs index 1d4a9aebe..d300b150e 100644 --- a/src/EPPlus.Export.ImageRenderer/Svg/Chart/SvgChartAxis.cs +++ b/src/EPPlus.Export.ImageRenderer/Svg/Chart/SvgChartAxis.cs @@ -18,7 +18,6 @@ Date Author Change using EPPlus.Fonts.OpenType.Utils; using EPPlus.Graphics; using EPPlusImageRenderer.RenderItems; -using EPPlusImageRenderer.Text; using OfficeOpenXml.Drawing; using OfficeOpenXml.Drawing.Chart; using OfficeOpenXml.Drawing.Chart.Style; diff --git a/src/EPPlus.Export.ImageRenderer/Svg/Chart/SvgChartLegend.cs b/src/EPPlus.Export.ImageRenderer/Svg/Chart/SvgChartLegend.cs index 71565d3e5..fee86ef6a 100644 --- a/src/EPPlus.Export.ImageRenderer/Svg/Chart/SvgChartLegend.cs +++ b/src/EPPlus.Export.ImageRenderer/Svg/Chart/SvgChartLegend.cs @@ -14,7 +14,6 @@ Date Author Change using EPPlus.Export.ImageRenderer.RenderItems.SvgItem; using EPPlus.Export.ImageRenderer.Svg; using EPPlusImageRenderer.RenderItems; -using EPPlusImageRenderer.Text; using OfficeOpenXml; using OfficeOpenXml.ConditionalFormatting; using OfficeOpenXml.Drawing; diff --git a/src/EPPlus.Export.ImageRenderer/Svg/Chart/SvgChartTitle.cs b/src/EPPlus.Export.ImageRenderer/Svg/Chart/SvgChartTitle.cs index b66862f97..081b9c36c 100644 --- a/src/EPPlus.Export.ImageRenderer/Svg/Chart/SvgChartTitle.cs +++ b/src/EPPlus.Export.ImageRenderer/Svg/Chart/SvgChartTitle.cs @@ -15,7 +15,6 @@ Date Author Change using EPPlus.Fonts.OpenType.Utils; using EPPlus.Graphics; using EPPlusImageRenderer.RenderItems; -using EPPlusImageRenderer.Text; using OfficeOpenXml; using OfficeOpenXml.Drawing; using OfficeOpenXml.Drawing.Chart; diff --git a/src/EPPlus.Export.ImageRenderer/Svg/SvgRange.cs b/src/EPPlus.Export.ImageRenderer/Svg/SvgRange.cs index 9d0da8dc6..cc7ca1206 100644 --- a/src/EPPlus.Export.ImageRenderer/Svg/SvgRange.cs +++ b/src/EPPlus.Export.ImageRenderer/Svg/SvgRange.cs @@ -1,5 +1,4 @@ using EPPlusImageRenderer.RenderItems; -using EPPlusImageRenderer.Text; using OfficeOpenXml; using System; using System.Collections.Generic; diff --git a/src/EPPlus.Export.ImageRenderer/Svg/SvgShape.cs b/src/EPPlus.Export.ImageRenderer/Svg/SvgShape.cs index fac57a614..6950c835c 100644 --- a/src/EPPlus.Export.ImageRenderer/Svg/SvgShape.cs +++ b/src/EPPlus.Export.ImageRenderer/Svg/SvgShape.cs @@ -12,14 +12,12 @@ Date Author Change *************************************************************************************************/ using EPPlus.Export.ImageRenderer.RenderItems.Shared; using EPPlus.Export.ImageRenderer.RenderItems.SvgItem; -using EPPlus.Export.ImageRenderer.Text; using EPPlus.Export.ImageRenderer.Utils; using EPPlus.Fonts.OpenType; using EPPlus.Fonts.OpenType.Utils; using EPPlus.Graphics; using EPPlusImageRenderer.RenderItems; using EPPlusImageRenderer.ShapeDefinitions; -using EPPlusImageRenderer.Text; using EPPlusImageRenderer.Utils; using OfficeOpenXml; using OfficeOpenXml.Drawing; diff --git a/src/EPPlus.Export.ImageRenderer/Text/EpplusTextRun.cs b/src/EPPlus.Export.ImageRenderer/Text/EpplusTextRun.cs deleted file mode 100644 index a9cdd5481..000000000 --- a/src/EPPlus.Export.ImageRenderer/Text/EpplusTextRun.cs +++ /dev/null @@ -1,66 +0,0 @@ -/************************************************************************************************* - Required Notice: Copyright (C) EPPlus Software AB. - This software is licensed under PolyForm Noncommercial License 1.0.0 - and may only be used for noncommercial purposes - https://polyformproject.org/licenses/noncommercial/1.0.0/ - - A commercial license to use this software can be purchased at https://epplussoftware.com - ************************************************************************************************* - Date Author Change - ************************************************************************************************* - 27/11/2025 EPPlus Software AB EPPlus 9 - *************************************************************************************************/ -using OfficeOpenXml.Drawing; -using OfficeOpenXml.Style; - -namespace EPPlusImageRenderer.Text -{ - internal class EpplusTextRun - { - /// - /// The capitalization that is to be applied - /// - public eTextCapsType Capitalization; - - /// - /// The minimum font size at which character kerning occurs - /// - public double Kerning; - - /// - /// Fontsize - /// Spans from 0-4000 - /// - public double FontSize; - - /// - /// The spacing between between characters - /// - public double Spacing; - - /// - /// The baseline for both the superscript and subscript fonts in percentage - /// - public double Baseline; - - /// - /// FontBold text - /// - public bool Bold; - - /// - /// FontItalic text - /// - public bool Italic; - - /// - /// FontStrike-out text - /// - public eStrikeType Strike; - - /// - /// Underlined text - /// - public eUnderLineType UnderLine; - } -} diff --git a/src/EPPlus.Export.ImageRenderer/Text/FontWrapContainer.cs b/src/EPPlus.Export.ImageRenderer/Text/FontWrapContainer.cs deleted file mode 100644 index 3a2ddbe38..000000000 --- a/src/EPPlus.Export.ImageRenderer/Text/FontWrapContainer.cs +++ /dev/null @@ -1,95 +0,0 @@ -using EPPlus.Fonts.OpenType; -using EPPlus.Graphics; -using OfficeOpenXml.Interfaces.Drawing.Text; -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; - -namespace EPPlus.Export.ImageRenderer.Text -{ - /// - /// Only measures widths and handles the actual strings - /// This class only changes its bounding box if initDefaults = true - /// Any other changes to its size must be determined by the caller - /// If wrapping is true, uses parent rects width as maximum - /// - internal class FontWrapContainer : TextContainerBase - { - protected FontMeasurerTrueType _measurer; - - /// - /// If this is true it is presumed there is also a parent with a maxwidth - /// If there is no MaxWidth this class will not wrap - /// - public bool WrapText = false; - - public double MaxWidthPixels - { - get - { - if(Parent != null) - { - return Parent.Size.X; - } - else - { - return double.NaN; - } - } - } - - public FontWrapContainer(FontMeasurerTrueType txtMeasurer, bool initDefaults = true) : base(initDefaults) - { - Initialize(txtMeasurer); - } - - public FontWrapContainer(FontMeasurerTrueType txtMeasurer, string content, bool initDefaults = false) : base(content, initDefaults) - { - Initialize(txtMeasurer); - } - - private void Initialize(FontMeasurerTrueType txtMeasurer) - { - _measurer = txtMeasurer; - //Transform = parent.Transform; - //if (parent != null) - //{ - // SetParent(parent); - //} - } - - //void SetParent(Rect parent) - //{ - // TopDrawingHandler = parent; - // Transform.TopDrawingHandler = parent.Transform; - //} - - private void SplitContentToLines() - { - if (Content.Length == 1) - { - //If width is NaN textWrapper only applies line endings within the text itself - var inputWidth = WrapText ? MaxWidthPixels : double.NaN; - - Content = TextWrapper.GetLines(Content[0], _measurer, inputWidth).ToArray(); - } - } - - internal int GetNumberOfLines() - { - SplitContentToLines(); - return Content.Length; - } - - /// - /// Returns lines of content in pixel width - /// - /// - public double[] GetContentWidths() - { - SplitContentToLines(); - return TextWrapper.GetContentWidths(Content,_measurer, MaxWidthPixels).ToArray(); - } - } -} diff --git a/src/EPPlus.Export.ImageRenderer/Text/LineFormatter.cs b/src/EPPlus.Export.ImageRenderer/Text/LineFormatter.cs deleted file mode 100644 index 63e182b21..000000000 --- a/src/EPPlus.Export.ImageRenderer/Text/LineFormatter.cs +++ /dev/null @@ -1,104 +0,0 @@ -using EPPlus.Fonts.OpenType; -using OfficeOpenXml.Drawing; -using OfficeOpenXml.Interfaces.Drawing.Text; -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; - -namespace EPPlus.Export.ImageRenderer.Text -{ - internal static class LineFormatter - { - /// - /// Gets the starting indices of each individual run in the total text string - /// And the individual glyph advance widths in points for each textrun - /// - /// - /// - /// - internal static List GetTextRunIndiciesAndWidths(ExcelTextBody body, out List> AdvanceWidths) - { - var paragraphs = body.Paragraphs; - var text = paragraphs.Text; - - List txtRunIndicies = new List(); - - AdvanceWidths = new List>(); - - FontMeasurerTrueType measurer = new FontMeasurerTrueType(); - - int lastIndex = 0; - for (int i = 0; i < paragraphs.Count; i++) - { - var p = paragraphs[i]; - - for (int j = 0; j < p.TextRuns.Count; j++) - { - var run = p.TextRuns[j]; - - var indexOfRun = text.IndexOf(run.Text, lastIndex); - txtRunIndicies.Add(indexOfRun); - AdvanceWidths.Add(measurer.GetGlyphAdvanceWidths(run.Text, run.GetMeasurementFont())); - lastIndex = indexOfRun; - } - } - - return txtRunIndicies; - } - - - //internal static List GetFormattedLines(ExcelTextBody body, out List lineWidths, double MaxWidth = double.NaN) - //{ - // var paragraphs = body.Paragraphs; - // var text = paragraphs.Text; - - // List txtRunIndicies = new List(); - // List fonts = new List(); - - // int lastIndex = 0; - - // FontMeasurerTrueType measurer = new FontMeasurerTrueType(); - - // for (int i = 0; i < paragraphs.Count; i++) - // { - // var p = paragraphs[i]; - - // for (int j = 0; j < p.TextRuns.Count; j++) - // { - // var run = p.TextRuns[j]; - // TextWrapper.GetLines() - - // fonts.Add(run.GetMeasurementFont()); - // var indexOfRun = text.IndexOf(run.Text, lastIndex); - // txtRunIndicies.Add(indexOfRun); - // lastIndex = indexOfRun; - // } - // } - - - - // //return List test = new List(); - // //foreach (var font in fonts) - // //{ - // // FontMeasurerTrueType measurer = new FontMeasurerTrueType(); - // // measurer.SetFont(font); - - - // //} - - // //} - - // //internal static List WrapLines(ExcelDrawingTextRunCollection txtRuns, double maxWidth) - // //{ - // // txtRun - // // foreach (var run in txtRuns) - // // { - - // // } - // //} - //} - } -} - - diff --git a/src/EPPlus.Export.ImageRenderer/Text/TextContainer.cs b/src/EPPlus.Export.ImageRenderer/Text/TextContainer.cs deleted file mode 100644 index 352c5fdb6..000000000 --- a/src/EPPlus.Export.ImageRenderer/Text/TextContainer.cs +++ /dev/null @@ -1,140 +0,0 @@ -using EPPlus.Fonts.OpenType; -using EPPlusImageRenderer.Text; -using OfficeOpenXml.Drawing; -using OfficeOpenXml.FormulaParsing.Excel.Functions.MathFunctions; -using OfficeOpenXml.Interfaces.Drawing.Text; -using System; -using System.Collections.Generic; -using System.Linq; -using EPPlus.Graphics; - -namespace EPPlus.Export.ImageRenderer.Text -{ - internal class TextContainer : RectMargins - { - /// - /// Should we resize the container depending on the text - /// - bool ResizeToFit = false; - - - List _textContentCollection = new List(); - - MeasurementFont MeasurementFont; - FontMeasurerTrueType _textMeasurerTrueType = null; - - public bool AddDeltaHeight = false; - - List TextContent - { - get - { - return _textContentCollection; - } - set - { - _textContentCollection = value; - if (ResizeToFit) - { - AutofitContainer(); - } - } - } - - - public RectBase GetTextArea() - { - return GetInnerRect(); - } - - - //To create a placeholder with default values - public TextContainer() : base() - { - ResizeToFit = false; - MeasurementFont = new MeasurementFont() - { - FontFamily = "Aptos Narrow", - Style = MeasurementFontStyles.Regular, - Size = 11 - }; - - //Excel at 100% size appears to have this pixel right and bottom for cells - Left = 0; Top = 0; Right = 64; Bottom = 20d; - - //Initalize margins to 0 - MarginLeft = MarginRight = MarginTop = MarginBottom = 0; - - _textContentCollection = new List(); - } - - /// - /// - /// - /// - /// - public TextContainer(List textCollection, MeasurementFont font, bool resizeToFit = false) - { - MeasurementFont = font; - ResizeToFit = resizeToFit; - TextContent = textCollection; - } - - /// - /// - /// - /// - /// - /// - public TextContainer(string text, MeasurementFont font, bool resizeToFit = false, bool addDeltaHeight = false) - { - MeasurementFont = font; - AddDeltaHeight = addDeltaHeight; - ResizeToFit = resizeToFit; - TextContent = SplitStrings(text); - } - - public static readonly string[] lineEndings = { "\r\n", "\r", "\n" }; - - List SplitStrings(string text) - { - string[] splitLines = text.Split - ( - lineEndings, - StringSplitOptions.None - ); - return splitLines.ToList(); - } - - void AutofitContainer() - { - if (_textMeasurerTrueType == null) - { - _textMeasurerTrueType = new FontMeasurerTrueType(MeasurementFont); - } - - double maxWidth = 0; - foreach (var line in TextContent) - { - var lineWidth = _textMeasurerTrueType.MeasureTextWidthInPixels(line); - //var lineWidthAlt = _textMeasurerTrueType.MeasureTextWidthInPixels(line+"\r\n"); - //lineWidth += _textMeasurerTrueType.GetMinXInPixels(); - if (lineWidth > maxWidth) - { - maxWidth = lineWidth; - } - } - var totalHeight = _textMeasurerTrueType.GetHeightOfTextInPixels(_textContentCollection); - - //if(AddDeltaHeight) - //{ - // //Textbox boxes in excel have a slight additional pixel size roughly equal to difference between typoAscent and winAscent - // var deltaAscent = _textMeasurerTrueType.GetDeltaAscent(TextUnit.Pixels); - // totalHeight += deltaAscent; - //} - - Width = maxWidth; - Height = totalHeight; - } - } -} diff --git a/src/EPPlus.Export.ImageRenderer/Text/TextContainerBase.cs b/src/EPPlus.Export.ImageRenderer/Text/TextContainerBase.cs deleted file mode 100644 index a812a6244..000000000 --- a/src/EPPlus.Export.ImageRenderer/Text/TextContainerBase.cs +++ /dev/null @@ -1,54 +0,0 @@ -using System; -using EPPlus.Graphics; - -namespace EPPlus.Export.ImageRenderer.Text -{ - /// - /// Simple base-class. - /// A Rect that also holds an arr of strings - /// No assumptions are made about where or if the text is placed inside the rect. - /// - internal class TextContainerBase : BoundingBox - { - protected string[] Content = null; - - /// - /// - /// - /// If true initializes the container to 64 width and 20 height - public TextContainerBase(bool initDefaults = true) - { - if (initDefaults) - { - //Right and Bottom Pixel defaults for a Cell in excel at 96 PPI - //(15pts height, 8.43pts width) - Left = 0; Top = 0; Width = 64; Height = 20d; - SetContent("Some Text"); - } - } - - public TextContainerBase(string content, bool initDefaults = true) - { - if (initDefaults) - { - //Right and Bottom Pixel defaults for a Cell in excel at 96 PPI - //(15pts height, 8.43pts width) - Left = 0; Top = 0; Width = 64; Height = 20d; - } - - SetContent(content); - } - - public void SetContent(string content) - { - Content = new string[] { content }; - } - - public string GetContent() - { - var combinedString = ""; - combinedString = string.Join(Environment.NewLine, Content); - return combinedString; - } - } -} diff --git a/src/EPPlus.Export.ImageRenderer/Text/TextWrapper.cs b/src/EPPlus.Export.ImageRenderer/Text/TextWrapper.cs deleted file mode 100644 index 80931d0b9..000000000 --- a/src/EPPlus.Export.ImageRenderer/Text/TextWrapper.cs +++ /dev/null @@ -1,54 +0,0 @@ -using EPPlus.Fonts.OpenType; -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; - -namespace EPPlus.Export.ImageRenderer.Text -{ - internal static class TextWrapper - { - internal static List GetLines(string content, FontMeasurerTrueType measurer, double maxWidth = double.NaN) - { - if (string.IsNullOrEmpty(content)) return null; - - List lines = new List(); - - if (double.IsNaN(maxWidth)) - { - lines = measurer.MeasureAndWrapText(content, maxWidth); - } - else - { - lines = content.Split(new string[] { Environment.NewLine }, StringSplitOptions.None).ToList(); - } - - return lines; - } - - internal static List GetContentWidths(string content, FontMeasurerTrueType measurer, double maxWidth = double.NaN) - { - var lines = GetLines(content, measurer, maxWidth); - return GetContentWidths(lines.ToArray(), measurer, maxWidth); - } - - /// - /// Assumes you already have correctly calculated lines - /// - /// - /// - /// - /// - internal static List GetContentWidths(string[] content, FontMeasurerTrueType measurer, double maxWidth = double.NaN) - { - double[] Widths = new double[content.Length]; - - for (int i = 0; i < content.Length; i++) - { - Widths[i] = measurer.MeasureTextWidth(content[i]); - } - - return Widths.ToList(); - } - } -} From a22f7f2a63dcd720815d19a2449af17df593ff2d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ossian=20Edstr=C3=B6m?= Date: Tue, 10 Feb 2026 15:12:43 +0100 Subject: [PATCH 053/151] Fixed textLines wrapper --- .../TextFragmentCollectionTests.cs | 26 +++++++++++++++++++ .../TrueTypeMeasurer/TextData.cs | 4 ++- 2 files changed, 29 insertions(+), 1 deletion(-) diff --git a/src/EPPlus.Fonts.OpenType.Tests/TextFragmentCollectionTests.cs b/src/EPPlus.Fonts.OpenType.Tests/TextFragmentCollectionTests.cs index a35ef6132..7c3ce7e37 100644 --- a/src/EPPlus.Fonts.OpenType.Tests/TextFragmentCollectionTests.cs +++ b/src/EPPlus.Fonts.OpenType.Tests/TextFragmentCollectionTests.cs @@ -278,5 +278,31 @@ public void MeasureWrappedWidths() Assert.AreEqual(169, pixelsWholeLine3); } + + [TestMethod] + public void CorrectTextLinesAreReturnedWhenSmallMaxWidth() + { + var defaultFont = new MeasurementFont() + { + FontFamily = "Aptos Narrow", + Size = 11, + Style = MeasurementFontStyles.Regular + }; + + var ttMeasurer = new FontMeasurerTrueType(defaultFont); + + var txt = "This is my text"; + + //This should be small enough to put each word on a new row. + var maxWidth = 21.5d; + + var txtLines = ttMeasurer.MeasureAndWrapTextLines(txt, defaultFont, maxWidth); + + Assert.AreEqual(4, txtLines.Count); + Assert.AreEqual("This", txtLines[0].Text); + Assert.AreEqual("is", txtLines[1].Text); + Assert.AreEqual("my", txtLines[2].Text); + Assert.AreEqual("text", txtLines[3].Text); + } } } diff --git a/src/EPPlus.Fonts.OpenType/TrueTypeMeasurer/TextData.cs b/src/EPPlus.Fonts.OpenType/TrueTypeMeasurer/TextData.cs index 4b8824b82..796cc8d64 100644 --- a/src/EPPlus.Fonts.OpenType/TrueTypeMeasurer/TextData.cs +++ b/src/EPPlus.Fonts.OpenType/TrueTypeMeasurer/TextData.cs @@ -427,8 +427,10 @@ private static void MeasureAndWrapTextLine(string line, OpenTypeFont font, ref i List tmpLst = new List(); WrapAtCharPos(line, i, ref nextLineStartIndex, ref lineWidth, ref wordWidth, advanceWidth, tmpLst, spaceWidth, out int prevLineWidth); - actualLine.Text = line; + actualLine.Text = tmpLst.Last(); actualLine.Width = prevLineWidth; + wrappedStrings.Add(actualLine); + actualLine = new TextLineSimple(); } } From 395f75aa223edaebabb2b52a148e70f0355598c6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jan=20K=C3=A4llman?= Date: Wed, 11 Feb 2026 11:15:31 +0100 Subject: [PATCH 054/151] WIP: Fixed several issues related to Textboxes --- .../SvgPathTests.cs | 6 +- .../RenderItems/Shared/ParagraphItem.cs | 8 +- .../RenderItems/Shared/TextBodyItem.cs | 33 +++- .../RenderItems/SvgItem/SvgTextBox.cs | 141 ++++++++++++++ .../RenderItems/SvgItem/SvgTextBoxItem.cs | 174 ------------------ .../Svg/Chart/SvgChart.cs | 65 ++++--- .../Svg/Chart/SvgChartAxis.cs | 14 +- .../Svg/Chart/SvgChartLegend.cs | 15 +- .../Svg/Chart/SvgChartObject.cs | 34 +++- .../Svg/Chart/SvgChartPlotarea.cs | 4 +- .../Svg/Chart/SvgChartTitle.cs | 38 ++-- .../Svg/LineMarkerHelper.cs | 8 +- .../Svg/SvgShape.cs | 10 +- src/EPPlus.Graphics/BoundingBox.cs | 2 +- src/EPPlus/Drawing/Chart/ExcelChartAxis.cs | 8 + src/EPPlus/NumberFormatToTextArgs.cs | 2 +- 16 files changed, 312 insertions(+), 250 deletions(-) create mode 100644 src/EPPlus.Export.ImageRenderer/RenderItems/SvgItem/SvgTextBox.cs delete mode 100644 src/EPPlus.Export.ImageRenderer/RenderItems/SvgItem/SvgTextBoxItem.cs diff --git a/src/EPPlus.Export.ImageRenderer.Test/SvgPathTests.cs b/src/EPPlus.Export.ImageRenderer.Test/SvgPathTests.cs index fd2824687..9de1cbd58 100644 --- a/src/EPPlus.Export.ImageRenderer.Test/SvgPathTests.cs +++ b/src/EPPlus.Export.ImageRenderer.Test/SvgPathTests.cs @@ -346,7 +346,7 @@ public void CustomPath() var ws = p.Workbook.Worksheets[0]; //var d = ws.Drawings[0].As.Shape; //Assert.AreEqual(1, d.CustomGeom.DrawingPaths.Count); - //d.Textbox = "Rectangle Rectangle Rectangle Rectangle"; + //d.Textbox = "GetRectangle GetRectangle GetRectangle GetRectangle"; //d.TextAlignment = OfficeOpenXml.Drawing.eTextAlignment.Left; //d.TextAnchoring = OfficeOpenXml.Drawing.eTextAnchoringType.Bottom; var renderer = new EPPlusImageRenderer.ImageRenderer(); @@ -511,10 +511,12 @@ public void GenerateSvgForCharts() { var ws = p.Workbook.Worksheets[0]; var renderer = new EPPlusImageRenderer.ImageRenderer(); - var ix = 1; + + var ix = 2; var c = ws.Drawings[ix]; var svg = renderer.RenderDrawingToSvg(c); SaveTextFileToWorkbook($"svg\\ChartForSvg_ind{ix++}.svg", svg); + //var ix = 1; //foreach (ExcelChart c in ws.Drawings) //{ diff --git a/src/EPPlus.Export.ImageRenderer/RenderItems/Shared/ParagraphItem.cs b/src/EPPlus.Export.ImageRenderer/RenderItems/Shared/ParagraphItem.cs index 9ca3ab6cd..cb61e2895 100644 --- a/src/EPPlus.Export.ImageRenderer/RenderItems/Shared/ParagraphItem.cs +++ b/src/EPPlus.Export.ImageRenderer/RenderItems/Shared/ParagraphItem.cs @@ -26,8 +26,7 @@ internal abstract class ParagraphItem : RenderItem ITextMeasurerWrap _measurer; double _leftMargin; - double _rightMargin; - protected eTextAlignment _hAlign; + double _rightMargin; eDrawingTextLineSpacing _lsType; double _lineSpacingAscendantOnly; @@ -40,6 +39,7 @@ internal abstract class ParagraphItem : RenderItem internal protected MeasurementFont _paragraphFont; internal TextBodyItem ParentTextBody { get; set; } internal double ParagraphLineSpacing { get; private set; } + internal eTextAlignment HorizontalAlignment { get; private set; } = eTextAlignment.Left; internal List Runs { get; set; } = new List(); public ParagraphItem(TextBodyItem textBody, DrawingBase renderer, BoundingBox parent) : base(renderer, parent) @@ -89,11 +89,11 @@ public ParagraphItem(TextBodyItem textBody, DrawingBase renderer, BoundingBox pa _leftMargin = _leftMargin.PixelToPoint(); _rightMargin = _rightMargin.PixelToPoint(); - _hAlign = p.HorizontalAlignment; + HorizontalAlignment = p.HorizontalAlignment; if (ParentTextBody.AutoSize == false) { - Bounds.Left = GetAlignmentHorizontal(_hAlign); + Bounds.Left = GetAlignmentHorizontal(HorizontalAlignment); Bounds.Width = parent.Width - _rightMargin - _leftMargin; } diff --git a/src/EPPlus.Export.ImageRenderer/RenderItems/Shared/TextBodyItem.cs b/src/EPPlus.Export.ImageRenderer/RenderItems/Shared/TextBodyItem.cs index c887a5ce7..b49f168fd 100644 --- a/src/EPPlus.Export.ImageRenderer/RenderItems/Shared/TextBodyItem.cs +++ b/src/EPPlus.Export.ImageRenderer/RenderItems/Shared/TextBodyItem.cs @@ -80,11 +80,40 @@ public void ImportParagraph(ExcelDrawingParagraph item, double startingY, string Bounds.Height += paragraph.Bounds.Height; } - if(Bounds.Width < paragraph.Bounds.Width) + if(Bounds.Width < paragraph.Bounds.Width || (Bounds.Width == MaxWidth && Paragraphs.Count==0)) { Bounds.Width = paragraph.Bounds.Width; } Paragraphs.Add(paragraph); + SetHorizontalAlignmentPosition(); + } + + private void SetHorizontalAlignmentPosition() + { + if (AutoSize) + { + foreach (var p in Paragraphs) + { + switch (p.HorizontalAlignment) + { + case eTextAlignment.Left: + p.Bounds.Left = 0; + break; + case eTextAlignment.Center: + p.Bounds.Left = Bounds.Width / 2 - p.Bounds.Width / 2; + break; + case eTextAlignment.Right: + p.Bounds.Left = Bounds.Right - p.Bounds.Width; + break; + case eTextAlignment.Distributed: + case eTextAlignment.Justified: + case eTextAlignment.JustifiedLow: + case eTextAlignment.ThaiDistributed: + p.Bounds.Left = 0; //TODO: Set left for now as we do not support distributed spacing yet + break; + } + } + } } internal virtual void ImportTextBody(ExcelTextBody body) @@ -205,7 +234,7 @@ private double GetAlignmentVertical() alignmentY = 0; break; //Center means center of a Shape's ENTIRE bounding box height. - //Not center of the Inset Rectangle + //Not center of the Inset GetRectangle case eTextAnchoringType.Center: var globalHeight = (DrawingRenderer.Bounds.Height / 2) + Bounds.Top+2; var adjustedHeight = globalHeight - Bounds.Position.Y; //Global position. diff --git a/src/EPPlus.Export.ImageRenderer/RenderItems/SvgItem/SvgTextBox.cs b/src/EPPlus.Export.ImageRenderer/RenderItems/SvgItem/SvgTextBox.cs new file mode 100644 index 000000000..af3cfda0c --- /dev/null +++ b/src/EPPlus.Export.ImageRenderer/RenderItems/SvgItem/SvgTextBox.cs @@ -0,0 +1,141 @@ +using EPPlus.Fonts.OpenType.Utils; +using EPPlus.Graphics; +using EPPlusImageRenderer; +using EPPlusImageRenderer.RenderItems; +using EPPlusImageRenderer.Svg; +using OfficeOpenXml.Drawing; +using OfficeOpenXml.FormulaParsing.Excel.Functions.Text; +using System; +using System.Collections.Generic; +using System.Drawing; +using System.Xml.Serialization; +using static System.Net.Mime.MediaTypeNames; + +namespace EPPlus.Export.ImageRenderer.RenderItems.SvgItem +{ + internal class SvgTextBox : DrawingObjectNoBounds + { + internal SvgTextBox(DrawingBase renderer, BoundingBox parent, double left, double top, double width, double height, double maxWidth = double.NaN, double maxHeight = double.NaN) : base(renderer) + { + Left = left; + Top = top; + + Init(renderer, parent, maxWidth, maxHeight); + } + + private void Init(DrawingBase renderer, BoundingBox parent, double maxWidth, double maxHeight) + { + Parent = parent; + _rectangle = new SvgRenderRectItem(DrawingRenderer, Parent); + TextBody = new SvgTextBodyItem(renderer, Rectangle.Bounds, true); + TextBody.MaxWidth = maxWidth; + TextBody.MaxHeight = maxHeight; + } + + internal SvgTextBox(DrawingBase renderer, BoundingBox parent, double maxWidth, double maxHeight) : base(renderer) + { + Init(renderer, parent, maxWidth, maxHeight); + } + + //Simplified input + internal SvgTextBox(DrawingBase renderer, BoundingBox parent, BoundingBox maxBounds) : this( + renderer, parent, maxBounds.Left, maxBounds.Top, maxBounds.Width, maxBounds.Height, maxBounds.Width, maxBounds.Height) + { + } + SvgRenderItem _rectangle=null; + public SvgRenderItem Rectangle + { + get + { + _rectangle.Bounds.Width = Width; + _rectangle.Bounds.Height = Height; + return _rectangle; + } + } + public SvgTextBodyItem TextBody {get;set;} + public double Left { get; set; } + public double Top { get; set; } + public double Width + { + get + { + return LeftMargin + (TextBody?.Width ?? 0D) + RightMargin; + } + } + public double Height + { + get + { + return TopMargin + (TextBody?.Height ?? 0d) + BottomMargin; + } + } + internal double LeftMargin + { + get; set; + } + + internal double TopMargin + { + get; set; + } + + internal double RightMargin + { + get; set; + } + + internal double BottomMargin + { + get; set; + } + internal BoundingBox Parent { get; private set; } + internal double Rotation + { + get + { + return Rectangle.Bounds.Rotation; + } + set + { + Rectangle.Bounds.Rotation = value; + } + } + + internal void ImportTextBody(ExcelTextBody body) + { + double l, r, t, b; + body.GetInsetsOrDefaults(out l, out t, out r, out b); + LeftMargin = l; + TopMargin = t; + RightMargin = r; + BottomMargin = b; + + TextBody.ImportTextBody(body); + } + + 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); + } + renderItems.Add(groupItem); + renderItems.Add(rect); + TextBody.Bounds.Left = LeftMargin; + TextBody.Bounds.Top = TopMargin; + TextBody.AppendRenderItems(renderItems); + renderItems.Add(new SvgEndGroupItem(DrawingRenderer, rect.Bounds)); + } + + internal void ImportParagraph(ExcelDrawingParagraph item, double startingY, string text = null) + { + TextBody.ImportParagraph(item, startingY, text); + } + } +} diff --git a/src/EPPlus.Export.ImageRenderer/RenderItems/SvgItem/SvgTextBoxItem.cs b/src/EPPlus.Export.ImageRenderer/RenderItems/SvgItem/SvgTextBoxItem.cs deleted file mode 100644 index 722435e7a..000000000 --- a/src/EPPlus.Export.ImageRenderer/RenderItems/SvgItem/SvgTextBoxItem.cs +++ /dev/null @@ -1,174 +0,0 @@ -using EPPlus.Fonts.OpenType.Utils; -using EPPlus.Graphics; -using EPPlusImageRenderer; -using EPPlusImageRenderer.RenderItems; -using EPPlusImageRenderer.Svg; -using OfficeOpenXml.Drawing; -using OfficeOpenXml.FormulaParsing.Excel.Functions.Text; -using System; -using System.Collections.Generic; -using System.Drawing; -using System.Xml.Serialization; -using static System.Net.Mime.MediaTypeNames; - -namespace EPPlus.Export.ImageRenderer.RenderItems.SvgItem -{ - internal class SvgTextBoxItem : DrawingObject - { - internal SvgTextBoxItem(DrawingBase renderer, BoundingBox parent, double left, double top, double width, double height, double maxWidth = double.NaN, double maxHeight = double.NaN) : base(renderer, parent) - { - Bounds.Left = left; - Bounds.Top = top; - Bounds.Width = maxWidth < width ? width : maxWidth; - Bounds.Height = maxHeight < width ? width : maxHeight; - Init(renderer, parent, maxWidth, maxHeight); - } - - private void Init(DrawingBase renderer, BoundingBox parent, double maxWidth, double maxHeight) - { - Bounds.Name = "TextBox"; - - TextBody = new SvgTextBodyItem(renderer, Bounds, true); - TextBody.MaxWidth = maxWidth; - TextBody.MaxHeight = maxHeight; - - Rectangle = new SvgRenderRectItem(renderer, parent); - - //NEVER do this. You are assigning this objects bounds to an underlying objects bounds - //Worst case you are making it its own parent. Set only the properties. - //Rectangle.Bounds = Bounds; - - Rectangle.Bounds.Top = Bounds.Top; - Rectangle.Bounds.Left = Bounds.Left; - - Rectangle.Bounds.Width = Bounds.Width; - Rectangle.Bounds.Height = Bounds.Height; - } - - internal SvgTextBoxItem(DrawingBase renderer, BoundingBox parent, double maxWidth, double maxHeight) : base(renderer, parent) - { - Init(renderer, parent, maxWidth, maxHeight); - } - - //Simplified input - internal SvgTextBoxItem(DrawingBase renderer, BoundingBox parent, BoundingBox maxBounds) : this( - renderer, parent, maxBounds.Left, maxBounds.Top, maxBounds.Width, maxBounds.Height, maxBounds.Width, maxBounds.Height) - { - } - - public SvgRenderItem Rectangle { get; set; } - public SvgTextBodyItem TextBody {get;set;} - double _leftMargin; - internal double LeftMargin - { - get - { - return _leftMargin; - } - set - { - Bounds.Width += (value - _leftMargin); - _leftMargin = value; - } - } - - double _topMargin; - internal double TopMargin - { - get - { - return _topMargin; - } - set - { - Bounds.Height += (value - _topMargin); - _topMargin = value; - } - } - - double _rightMargin; - internal double RightMargin - { - get - { - return _rightMargin; - } - set - { - Bounds.Width += (value - _rightMargin); - _rightMargin = value; - } - } - double _bottomMargin; - internal double BottomMargin - { - get - { - return _bottomMargin; - } - set - { - Bounds.Height += (value - _bottomMargin); - _bottomMargin = value; - } - } - - internal void ImportTextBody(ExcelTextBody body, bool autoSize) - { - double l, r, t, b; - body.GetInsetsOrDefaults(out l, out t, out r, out b); - LeftMargin = l; - TopMargin = t; - RightMargin = r; - BottomMargin = b; - - TextBody.ImportTextBody(body); - - if (autoSize) - { - Bounds.Width = TextBody.Bounds.Width + LeftMargin + RightMargin; - Bounds.Height = TextBody.Bounds.Height + TopMargin + BottomMargin; - - Rectangle.Bounds.Width = Bounds.Width; - Rectangle.Bounds.Height = Bounds.Height; - } - } - - internal override void AppendRenderItems(List renderItems) - { - SvgGroupItem groupItem; - if (Bounds.Rotation == 0) - { - groupItem = new SvgGroupItem(DrawingRenderer, Bounds); - } - else - { - groupItem = new SvgGroupItem(DrawingRenderer, Bounds, Bounds.Rotation); - } - renderItems.Add(groupItem); - //handled by group now - Rectangle.Bounds.Top = 0; - Rectangle.Bounds.Left = 0; - Rectangle.Bounds.Width = Bounds.Width; - Rectangle.Bounds.Height = Bounds.Height; - - renderItems.Add(Rectangle); - TextBody.AppendRenderItems(renderItems); - renderItems.Add(new SvgEndGroupItem(DrawingRenderer, Bounds)); - } - - internal void ImportParagraph(ExcelDrawingParagraph item, double startingY, string text = null) - { - TextBody.ImportParagraph(item, startingY, text); - Bounds.Width = TextBody.Width; - Bounds.Height = TextBody.Height; - } - - internal void ImportTextBody(ExcelTextBody textBody) - { - TextBody.ImportTextBody(textBody); - Bounds.Width = TextBody.Width; - Bounds.Height = TextBody.Height; - } - } -} diff --git a/src/EPPlus.Export.ImageRenderer/Svg/Chart/SvgChart.cs b/src/EPPlus.Export.ImageRenderer/Svg/Chart/SvgChart.cs index 3e4fc4c1e..83528fdce 100644 --- a/src/EPPlus.Export.ImageRenderer/Svg/Chart/SvgChart.cs +++ b/src/EPPlus.Export.ImageRenderer/Svg/Chart/SvgChart.cs @@ -57,7 +57,7 @@ public SvgChart(ExcelChart chart) : base(chart) Plotarea = new SvgChartPlotarea(this); SetAxisPositionsFromPlotarea(this); - foreach(var ct in chart.PlotArea.ChartTypes) + foreach (var ct in chart.PlotArea.ChartTypes) { Plotarea.ChartTypeDrawers = ChartTypeDrawer.Create(this); } @@ -79,9 +79,18 @@ private void SetAxisPositionsFromPlotarea(SvgChart sc) if(VerticalAxis.Title!=null) { + VerticalAxis.Title.Rectangle.Height = Plotarea.Rectangle.Height; + VerticalAxis.Title.Rectangle.Width = sc.Bounds.Width / 4; VerticalAxis.Title.InitTextBox(); - VerticalAxis.Title.TextBox.Bounds.Top = Plotarea.Rectangle.Top + (Plotarea.Rectangle.Height / 2) - (VerticalAxis.Title.TextBox.Bounds.Height / 2); - VerticalAxis.Title.TextBox.Bounds.Left = VerticalAxis.Title.Rectangle.Left; + VerticalAxis.Title.TextBox.Top = Plotarea.Rectangle.Top + (Plotarea.Rectangle.Height / 2) - (VerticalAxis.Title.TextBox.Height / 2); + if (VerticalAxis.Rectangle == null) + { + VerticalAxis.Title.TextBox.Left = sc.ChartArea.LeftMargin; + } + else + { + VerticalAxis.Title.TextBox.Left = VerticalAxis.Rectangle.Left - VerticalAxis.Title.TextBox.Width ; + } } VerticalAxis.AddTickmarksAndValues(); @@ -98,31 +107,41 @@ private void SetAxisPositionsFromPlotarea(SvgChart sc) if (HorizontalAxis.Title != null) { + HorizontalAxis.Title.Rectangle.Height = sc.Bounds.Height / 4; + HorizontalAxis.Title.Rectangle.Width = HorizontalAxis.Rectangle?.Width ?? sc.Plotarea.Rectangle.Width; HorizontalAxis.Title.InitTextBox(); - HorizontalAxis.Title.TextBox.Bounds.Left = Plotarea.Rectangle.Left + (Plotarea.Rectangle.Width / 2) - (HorizontalAxis.Title.TextBox.Bounds.Width / 2); - HorizontalAxis.Title.TextBox.Bounds.Top = HorizontalAxis.Title.Rectangle.Top; + HorizontalAxis.Title.TextBox.Left = Plotarea.Rectangle.Left + (Plotarea.Rectangle.Width / 2) - (HorizontalAxis.Title.TextBox.Width / 2); + HorizontalAxis.Title.TextBox.Top = HorizontalAxis.Rectangle.Bottom; } HorizontalAxis.AddTickmarksAndValues(); } if (SecondVerticalAxis!=null) { - SecondVerticalAxis.Rectangle.Top = Plotarea.Rectangle.Top; - SecondVerticalAxis.Rectangle.Height = Plotarea.Rectangle.Height; - SecondVerticalAxis.Rectangle.Left = Plotarea.Rectangle.Right; - VerticalAxis.Line.X1 = VerticalAxis.Line.X2 = (float)Plotarea.Rectangle.Right; - SecondVerticalAxis.Line.Y1 = (float)SecondVerticalAxis.Rectangle.Top; - SecondVerticalAxis.Line.Y2 = (float)SecondVerticalAxis.Rectangle.Bottom; + if (SecondVerticalAxis.Rectangle != null) + { + SecondVerticalAxis.Rectangle.Top = Plotarea.Rectangle.Top; + SecondVerticalAxis.Rectangle.Height = Plotarea.Rectangle.Height; + SecondVerticalAxis.Rectangle.Left = Plotarea.Rectangle.Left - SecondVerticalAxis.Rectangle.Width; + SecondVerticalAxis.Line.X1 = SecondVerticalAxis.Line.X2 = (float)Plotarea.Rectangle.Left; + SecondVerticalAxis.Line.Y1 = (float)SecondVerticalAxis.Rectangle.Top; + SecondVerticalAxis.Line.Y2 = (float)SecondVerticalAxis.Rectangle.Bottom; + } + if (SecondVerticalAxis.Title != null) { - SecondVerticalAxis.Title.Rectangle.Top = Plotarea.Rectangle.Top + (Plotarea.Rectangle.Height / 2) - (SecondVerticalAxis.Title.Rectangle.Height / 2); + SecondVerticalAxis.Title.Rectangle.Height = SecondVerticalAxis.Rectangle.Height; + SecondVerticalAxis.Title.Rectangle.Width = sc.Bounds.Width / 4; SecondVerticalAxis.Title.InitTextBox(); + SecondVerticalAxis.Title.TextBox.Top = Plotarea.Rectangle.Top + (Plotarea.Rectangle.Height / 2) - (SecondVerticalAxis.Title.TextBox.Height / 2); + SecondVerticalAxis.Title.TextBox.Left = SecondVerticalAxis.Title.Rectangle.Left; } + SecondVerticalAxis.AddTickmarksAndValues(); } } - internal SvgRenderRectItem ChartArea { get; set; } + internal SvgChartObject ChartArea { get; set; } internal SvgChartLegend Legend { get; set; } internal SvgChartTitle Title { get; set; } internal SvgChartPlotarea Plotarea { get; set; } @@ -135,12 +154,12 @@ private void SetAxisPositionsFromPlotarea(SvgChart sc) private void SetChartArea() { - var item = new SvgRenderRectItem(this, Bounds); - item.Width = Bounds.Width; - item.Height = Bounds.Height; - item.SetDrawingPropertiesFill(Chart.Fill, Chart.StyleManager.Style.ChartArea.FillReference.Color); - item.SetDrawingPropertiesBorder(Chart.Border, Chart.StyleManager.Style.ChartArea.BorderReference.Color, Chart.Border.Width > 0); - RenderItems.Add(item); + var item = new SvgChartArea(this); + item.Rectangle.Width = Bounds.Width; + item.Rectangle.Height = Bounds.Height; + item.Rectangle.SetDrawingPropertiesFill(Chart.Fill, Chart.StyleManager.Style.ChartArea.FillReference.Color); + item.Rectangle.SetDrawingPropertiesBorder(Chart.Border, Chart.StyleManager.Style.ChartArea.BorderReference.Color, Chart.Border.Width > 0); + item.AppendRenderItems(RenderItems); ChartArea = item; } internal List DefItems { get; } = new List(); @@ -156,11 +175,13 @@ public void Render(StringBuilder sb) VerticalAxis?.AppendRenderItems(RenderItems); SecondVerticalAxis?.AppendRenderItems(RenderItems); - foreach (var drawer in Plotarea?.ChartTypeDrawers) + if (Plotarea != null) { - drawer.AppendRenderItems(RenderItems); + foreach (var drawer in Plotarea?.ChartTypeDrawers) + { + drawer.AppendRenderItems(RenderItems); + } } - Legend?.AppendRenderItems(RenderItems); Title?.AppendRenderItems(RenderItems); diff --git a/src/EPPlus.Export.ImageRenderer/Svg/Chart/SvgChartAxis.cs b/src/EPPlus.Export.ImageRenderer/Svg/Chart/SvgChartAxis.cs index 1d4a9aebe..64e0f962a 100644 --- a/src/EPPlus.Export.ImageRenderer/Svg/Chart/SvgChartAxis.cs +++ b/src/EPPlus.Export.ImageRenderer/Svg/Chart/SvgChartAxis.cs @@ -88,7 +88,7 @@ internal SvgChartAxis(SvgChart sc, ExcelChartAxisStandard ax) : base(sc) else { Rectangle.Width = GetTextWidest(sc, ax) + RightMargin; - var lp = sc.ChartArea.Width - Rectangle.Width - 8D; + var lp = sc.ChartArea.Rectangle.Width - Rectangle.Width - 8D; if (sc.Chart.Legend.Position == eLegendPosition.Right) { lp = sc.Legend.Rectangle.Left + -Rectangle.Width; @@ -99,7 +99,7 @@ internal SvgChartAxis(SvgChart sc, ExcelChartAxisStandard ax) : base(sc) else { Rectangle.Height = GetTextHeight(sc, ax); - Rectangle.Top = Title == null || ax.AxisPosition == eAxisPosition.Top ? sc.ChartArea.Height - 8 - Rectangle.Height : Title.Rectangle.Top - Rectangle.Height - 8; + Rectangle.Top = Title == null || ax.AxisPosition == eAxisPosition.Top ? sc.ChartArea.Rectangle.Height - 8 - Rectangle.Height : Title.Rectangle.Top - Rectangle.Height - 8; } } @@ -182,7 +182,7 @@ public List Values public List MinorAxisPositions { get; private set; } public List MajorGridlinePositions { get; private set; } public List MinorGridlinePositions { get; private set; } - public List AxisValuesTextBoxes + public List AxisValuesTextBoxes { get; private set; @@ -278,16 +278,16 @@ internal void AddTickmarksAndValues() } } - private List GetAxisValueTextBoxes() + private List GetAxisValueTextBoxes() { var tm = Chart.WorkSheet._package.Settings.TextSettings.GenericTextMeasurerTrueType; var mf = Axis.Font.GetMeasureFont(); var axisStyle = GetAxisStyleEntry(); - var ret= new List(); + var ret= new List(); double maxWidth, maxHeight; if(Axis.AxisPosition==eAxisPosition.Left || Axis.AxisPosition == eAxisPosition.Right) { - maxWidth = SvgChart.ChartArea.Bounds.Width / 3; //TODO: Check this value. + maxWidth = SvgChart.ChartArea.Rectangle.Width / 3; //TODO: Check this value. maxHeight = Rectangle.Height / AxisValues.Count; } else @@ -307,7 +307,7 @@ private List GetAxisValueTextBoxes() //bounds.Top = y; var width = m.Width.PointToPixel(); var height = m.Height.PointToPixel(); - var tb = new SvgTextBoxItem(SvgChart, Rectangle.Bounds, x, y, width, height, maxWidth, maxHeight); + var tb = new SvgTextBox(SvgChart, Rectangle.Bounds, x, y, width, height, maxWidth, maxHeight); var p = Axis.TextBody.Paragraphs.FirstOrDefault(); tb.TextBody.ImportParagraph(p, 0, v); diff --git a/src/EPPlus.Export.ImageRenderer/Svg/Chart/SvgChartLegend.cs b/src/EPPlus.Export.ImageRenderer/Svg/Chart/SvgChartLegend.cs index 71565d3e5..ca6f609a3 100644 --- a/src/EPPlus.Export.ImageRenderer/Svg/Chart/SvgChartLegend.cs +++ b/src/EPPlus.Export.ImageRenderer/Svg/Chart/SvgChartLegend.cs @@ -46,8 +46,8 @@ internal SvgChartLegend(SvgChart sc) : base(sc) } var l = ((ExcelChartStandard)sc.Chart).Legend; - LeftMargin = RightMargin = 4; - TopMargin = BottomMargin = 4; + LeftMargin = RightMargin = 3; //4px + TopMargin = BottomMargin = 3; //4px if (l.Layout.HasLayout) { @@ -57,6 +57,7 @@ internal SvgChartLegend(SvgChart sc) : base(sc) { Rectangle = GetLegendRectangle(sc, l); } + Bounds.Left = Rectangle.Left; Bounds.Top = Rectangle.Top; Bounds.Width = Rectangle.Width; @@ -127,14 +128,14 @@ private SvgRenderRectItem GetLegendRectangle(SvgChart sc, ExcelChartLegend l) case eLegendPosition.Bottom: rect.Width = textWidth + LeftMargin + RightMargin + ((LineLength + MarginExtra) * index + (MiddleMargin*Math.Max(index-1,0))) ; // 28 is for the line length + 2px between line and text rect.Height = TopMargin + BottomMargin + highest + MarginExtra; - rect.Left = (sc.ChartArea.Width - rect.Width) / 2; + rect.Left = (sc.ChartArea.Rectangle.Width - rect.Width) / 2; if (l.Position == eLegendPosition.Top) { rect.Top = sc.Title.Rectangle.Top+ sc.Title.Rectangle.Height + MiddleMargin; } else { - rect.Top = sc.ChartArea.Height - rect.Height - BottomMargin; + rect.Top = sc.ChartArea.Rectangle.Height - rect.Height - BottomMargin; } break; case eLegendPosition.Right: @@ -145,7 +146,7 @@ private SvgRenderRectItem GetLegendRectangle(SvgChart sc, ExcelChartLegend l) if (l.Position == eLegendPosition.Right || l.Position == eLegendPosition.TopRight) { - rect.Left = sc.ChartArea.Width - rect.Width - TopMargin; + rect.Left = sc.ChartArea.Rectangle.Width - rect.Width - TopMargin; } else { @@ -154,7 +155,7 @@ private SvgRenderRectItem GetLegendRectangle(SvgChart sc, ExcelChartLegend l) if (l.Position == eLegendPosition.Left || l.Position == eLegendPosition.Right) { - rect.Top = sc.ChartArea.Height / 2 + TopMargin + 2; + rect.Top = sc.ChartArea.Rectangle.Height / 2 + TopMargin + 2; } else { @@ -172,7 +173,7 @@ private SvgRenderRectItem GetLegendRectangle(SvgChart sc, ExcelChartLegend l) if (isVertical) { - //var top = sc.Title.Rectangle.Height+8+10; + //var top = sc.Title.GetRectangle.Height+8+10; //var width = margin; } return rect; diff --git a/src/EPPlus.Export.ImageRenderer/Svg/Chart/SvgChartObject.cs b/src/EPPlus.Export.ImageRenderer/Svg/Chart/SvgChartObject.cs index 11b143e96..52aab8346 100644 --- a/src/EPPlus.Export.ImageRenderer/Svg/Chart/SvgChartObject.cs +++ b/src/EPPlus.Export.ImageRenderer/Svg/Chart/SvgChartObject.cs @@ -25,6 +25,10 @@ internal abstract class DrawingObject internal protected DrawingBase DrawingRenderer { get; } internal virtual BoundingBox Bounds { get; set; } + protected DrawingObject(DrawingBase renderer) + { + DrawingRenderer = renderer; + } protected DrawingObject(DrawingBase renderer, BoundingBox parent) { DrawingRenderer = renderer; @@ -32,6 +36,28 @@ protected DrawingObject(DrawingBase renderer, BoundingBox parent) } internal abstract void AppendRenderItems(List renderItems); } + internal abstract class DrawingObjectNoBounds + { + internal protected DrawingBase DrawingRenderer { get; } + + protected DrawingObjectNoBounds(DrawingBase renderer) + { + DrawingRenderer = renderer; + } + internal abstract void AppendRenderItems(List renderItems); + } + internal class SvgChartArea : SvgChartObject + { + public SvgChartArea(SvgChart sc) : base(sc) + { + Rectangle = new SvgRenderRectItem(sc, sc.Bounds); + } + + internal override void AppendRenderItems(List renderItems) + { + renderItems.Add(Rectangle); + } + } internal abstract class SvgChartObject : DrawingObject { internal DrawingChart ChartRenderer; @@ -43,10 +69,10 @@ internal SvgChartObject(DrawingChart chart) : base(chart, chart.Bounds) internal void SetMargins(ExcelTextBody tb) { tb.GetInsetsOrDefaults(out double l, out double r, out double t, out double b); - LeftMargin = l.PointToPixel(); - RightMargin = r.PointToPixel(); - TopMargin = t.PointToPixel(); - BottomMargin = b.PointToPixel(); + LeftMargin = l; + RightMargin = r; + TopMargin = t; + BottomMargin = b; } internal double LeftMargin { get; set; } internal double RightMargin { get; set; } diff --git a/src/EPPlus.Export.ImageRenderer/Svg/Chart/SvgChartPlotarea.cs b/src/EPPlus.Export.ImageRenderer/Svg/Chart/SvgChartPlotarea.cs index 295f00ce2..c70a230ac 100644 --- a/src/EPPlus.Export.ImageRenderer/Svg/Chart/SvgChartPlotarea.cs +++ b/src/EPPlus.Export.ImageRenderer/Svg/Chart/SvgChartPlotarea.cs @@ -30,7 +30,7 @@ public SvgChartPlotarea(SvgChart sc) : base(sc) internal SvgRenderRectItem GetPlotAreaRectangle(SvgChart sc) { var pa = sc.Chart.PlotArea; - TopMargin = BottomMargin = LeftMargin = RightMargin = 14; + TopMargin = BottomMargin = LeftMargin = RightMargin = 10.5; //14px var rect = new SvgRenderRectItem(sc, sc.Bounds); if (pa.Layout.HasLayout) { @@ -52,7 +52,7 @@ internal SvgRenderRectItem GetPlotAreaRectangle(SvgChart sc) rect.Width = (lp == eLegendPosition.Right || lp == eLegendPosition.TopRight ? sc.Legend.Bounds.GlobalLeft - RightMargin : - sc.ChartArea.Width - RightMargin) + sc.ChartArea.Rectangle.Width - RightMargin) - rect.GlobalLeft; double vaHeight=0, vaTitleHeight=0; diff --git a/src/EPPlus.Export.ImageRenderer/Svg/Chart/SvgChartTitle.cs b/src/EPPlus.Export.ImageRenderer/Svg/Chart/SvgChartTitle.cs index b66862f97..aec09b72f 100644 --- a/src/EPPlus.Export.ImageRenderer/Svg/Chart/SvgChartTitle.cs +++ b/src/EPPlus.Export.ImageRenderer/Svg/Chart/SvgChartTitle.cs @@ -44,9 +44,10 @@ internal class SvgChartTitle : SvgChartObject internal SvgChartTitle(SvgChart sc, ExcelChartTitleStandard t, string defaultText, SvgChartAxis axis=null) : base(sc) { _svgChart = sc; + //These are hard coded margins for the title box. - LeftMargin = RightMargin = 4; - TopMargin = BottomMargin = 2; + LeftMargin = RightMargin = 3; //4px + TopMargin = BottomMargin = 1.5; //2px var maxWidth = sc.Bounds.Width * 0.8; var maxHeight = sc.Bounds.Height / 2D; @@ -67,16 +68,17 @@ internal SvgChartTitle(SvgChart sc, ExcelChartTitleStandard t, string defaultTex } } - if (t.Layout.HasLayout) + if (t.Layout.HasLayout) //Only for the main chart title, axis titles don't support manual layout in Excel. { Rectangle = GetRectFromManualLayout(sc, t.Layout); + var top = Rectangle.Top; + var left = Rectangle.Left; Rectangle.Width = (float)maxWidth; Rectangle.Height = (float)maxHeight; + InitTextBox(); - TextBox.Bounds.Top = Rectangle.Top; - TextBox.Bounds.Left = Rectangle.Left; - Rectangle.Width = TextBox.Bounds.Width; - Rectangle.Height = TextBox.Bounds.Height; + TextBox.Top = top; + TextBox.Left = left; } else { @@ -87,13 +89,17 @@ internal SvgChartTitle(SvgChart sc, ExcelChartTitleStandard t, string defaultTex Rectangle.Height = (float)maxHeight; InitTextBox(); - TextBox.Bounds.Top = (float)6; //6 point for the chart title standard offset - TextBox.Bounds.Left = (float)(sc.Bounds.Width - TextBox.Bounds.Width) / 2; - Rectangle.Height = (float)TextBox.Bounds.Height; - Rectangle.Width = (float)TextBox.Bounds.Width; + TextBox.Top = (float)6; //6 point for the chart title standard offset. + TextBox.Left = (float)(sc.Bounds.Width - TextBox.Width) / 2; } else { + var isVertical = axis.Axis.IsVertical; + Rectangle.Width = sc.Bounds.Width * (isVertical ? 0.2 : 0.8); //Max Width. + Rectangle.Height = sc.Bounds.Height * (isVertical ? 0.8 : 0.2); //Max Height. + + InitTextBox(); + Rectangle = (SvgRenderRectItem)TextBox.Rectangle; SetAxisTitleRect(sc, axis); } } @@ -112,7 +118,7 @@ private void SetAxisTitleRect(SvgChart sc, SvgChartAxis axis) Rectangle.Left = sc.Chart.HasLegend && sc.Chart.Legend.Position == eLegendPosition.Left ? sc.Legend.Rectangle.Right : margin; break; case eAxisPosition.Bottom: - Rectangle.Top = sc.ChartArea.Height - margin - Rectangle.Height; + Rectangle.Top = sc.ChartArea.Rectangle.Height - margin - Rectangle.Height; Rectangle.Left = GetHorizontalLeft(sc); break; } @@ -170,10 +176,10 @@ private static string GetDefaultChartTitleText(SvgChart sc, ExcelChartTitleStand internal void InitTextBox() { - TextBox = new SvgTextBoxItem(_svgChart, _svgChart.ChartArea.Bounds, Rectangle.Width, Rectangle.Height); + TextBox = new SvgTextBox(_svgChart, _svgChart.ChartArea.Bounds, Rectangle.Width, Rectangle.Height); if(_title.Rotation != 0) { - TextBox.Bounds.Rotation = _title.Rotation; + TextBox.Rotation = _title.Rotation; } if (_title.TextBody.Paragraphs.Count > 0) { @@ -185,14 +191,16 @@ internal void InitTextBox() var p = _title.DefaultTextBody.Paragraphs.FirstOrDefault(); TextBox.ImportParagraph(p, 0, text); } + TextBox.LeftMargin = LeftMargin; TextBox.RightMargin = RightMargin; TextBox.TopMargin = TopMargin; TextBox.BottomMargin = BottomMargin; TextBox.TextBody.VerticalAlignment = eTextAnchoringType.Top; + Rectangle = (SvgRenderRectItem)TextBox.Rectangle; } - public SvgTextBoxItem TextBox + public SvgTextBox TextBox { get; private set; } diff --git a/src/EPPlus.Export.ImageRenderer/Svg/LineMarkerHelper.cs b/src/EPPlus.Export.ImageRenderer/Svg/LineMarkerHelper.cs index 1c4738169..4542fdf01 100644 --- a/src/EPPlus.Export.ImageRenderer/Svg/LineMarkerHelper.cs +++ b/src/EPPlus.Export.ImageRenderer/Svg/LineMarkerHelper.cs @@ -18,10 +18,10 @@ internal static RenderItem GetMarkerItem(SvgChart sc, ExcelLineChartSerie ls, do float maxSize = isLegend ? 7f : float.MaxValue; var size = m.Size > maxSize ? maxSize : m.Size; var halfSize = size / 2; - var xPath = x / sc.ChartArea.Width; - var yPath = y / sc.ChartArea.Height; - var halfY = halfSize / sc.ChartArea.Height; - var halfX = halfSize / sc.ChartArea.Width; + var xPath = x / sc.ChartArea.Rectangle.Width; + var yPath = y / sc.ChartArea.Rectangle.Height; + var halfY = halfSize / sc.ChartArea.Rectangle.Height; + var halfX = halfSize / sc.ChartArea.Rectangle.Width; switch (m.Style) { case eMarkerStyle.Circle: diff --git a/src/EPPlus.Export.ImageRenderer/Svg/SvgShape.cs b/src/EPPlus.Export.ImageRenderer/Svg/SvgShape.cs index fac57a614..696061b2d 100644 --- a/src/EPPlus.Export.ImageRenderer/Svg/SvgShape.cs +++ b/src/EPPlus.Export.ImageRenderer/Svg/SvgShape.cs @@ -45,7 +45,7 @@ internal class SvgShape : DrawingShape /// /// Textbox from memory /// - public SvgTextBoxItem TextBox { get; internal set; } + public SvgTextBox TextBox { get; internal set; } public SvgShape(ExcelShape shape) : base(shape) { @@ -112,7 +112,7 @@ public SvgShape(ExcelShape shape) : base(shape) TextBox = CreateTextBodyItem(); TextBox.Rectangle.FillOpacity = 0.3; - TextBox.ImportTextBody(_shape.TextBody, false); + TextBox.ImportTextBody(_shape.TextBody); TextBox.AppendRenderItems(RenderItems); } } @@ -248,7 +248,7 @@ public void Render(StringBuilder sb) sb.AppendLine(""); } - SvgTextBoxItem CreateTextBodyItem() + SvgTextBox CreateTextBodyItem() { if (InsetTextBox == null) { @@ -258,9 +258,9 @@ SvgTextBoxItem CreateTextBodyItem() InsetTextBox.Bounds.Top = y.PixelToPoint(); InsetTextBox.Width = width.PixelToPoint(); InsetTextBox.Height = height.PixelToPoint(); - InsetTextBox.Bounds.Parent = TextBox.Bounds; //TODO:Check that textBody is correct. + InsetTextBox.Bounds.Parent = TextBox.Parent; //TODO:Check that textBody is correct. } - var txtBodyItem = new SvgTextBoxItem(this, Bounds, InsetTextBox.Bounds); + var txtBodyItem = new SvgTextBox(this, Bounds, InsetTextBox.Bounds); return txtBodyItem; } diff --git a/src/EPPlus.Graphics/BoundingBox.cs b/src/EPPlus.Graphics/BoundingBox.cs index eef4bfdc4..5116e4857 100644 --- a/src/EPPlus.Graphics/BoundingBox.cs +++ b/src/EPPlus.Graphics/BoundingBox.cs @@ -15,7 +15,7 @@ internal BoundingBox() : base() internal BoundingBox(double width, double height) : base(0, 0, width, height) { } - internal BoundingBox(double left, double top, double right, double bottom) : base(left,top, right-left, bottom-top) + internal BoundingBox(double left, double top, double width, double height) : base(left,top, width, height) { } diff --git a/src/EPPlus/Drawing/Chart/ExcelChartAxis.cs b/src/EPPlus/Drawing/Chart/ExcelChartAxis.cs index c83c9b381..1df76b94d 100644 --- a/src/EPPlus/Drawing/Chart/ExcelChartAxis.cs +++ b/src/EPPlus/Drawing/Chart/ExcelChartAxis.cs @@ -472,6 +472,14 @@ public abstract bool HasTitle { get; } + internal bool IsVertical + { + get + { + return AxisPosition == eAxisPosition.Left || AxisPosition == eAxisPosition.Right; + } + } + ///  /// Removes Major and Minor gridlines from the Axis ///  diff --git a/src/EPPlus/NumberFormatToTextArgs.cs b/src/EPPlus/NumberFormatToTextArgs.cs index a4a3ec55d..01fcc949c 100644 --- a/src/EPPlus/NumberFormatToTextArgs.cs +++ b/src/EPPlus/NumberFormatToTextArgs.cs @@ -49,7 +49,7 @@ internal NumberFormatToTextArgs(ExcelWorksheet ws, int row, int column, object v public ExcelNumberFormatXml NumberFormat { get - { + { return ValueToTextHandler.GetNumberFormat(_styleId, Worksheet.Workbook.Styles); } } From a2f5fbbf0e3b39fec2d1bfa829793a24d869e93a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ossian=20Edstr=C3=B6m?= Date: Wed, 11 Feb 2026 12:55:19 +0100 Subject: [PATCH 055/151] fixed bug in textData. Started textLines refactor --- .../Integration/LineFragment.cs | 23 +++++++++++++++++++ 1 file changed, 23 insertions(+) create mode 100644 src/EPPlus.Fonts.OpenType/Integration/LineFragment.cs diff --git a/src/EPPlus.Fonts.OpenType/Integration/LineFragment.cs b/src/EPPlus.Fonts.OpenType/Integration/LineFragment.cs new file mode 100644 index 000000000..5b0b8ffa2 --- /dev/null +++ b/src/EPPlus.Fonts.OpenType/Integration/LineFragment.cs @@ -0,0 +1,23 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; + +namespace EPPlus.Fonts.OpenType.Integration +{ + /// + /// The fragment of a richTextFragment that is within a line + /// + public class LineFragment + { + public int StartIdx { get; set; } + public double Width { get; set; } + public int RtIdx { get; set; } + + internal LineFragment(int rtFragmentIdx, int idxWithinLine) + { + RtIdx = rtFragmentIdx; + StartIdx = idxWithinLine; + } + } +} From 8fdea6c3134e0764511c62d6104e2a1af062ec75 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ossian=20Edstr=C3=B6m?= Date: Wed, 11 Feb 2026 13:06:28 +0100 Subject: [PATCH 056/151] Recommit last commit --- .../Integration/TextLayoutEngine.RichText.cs | 73 +++++++++++++------ .../Integration/WrapStateBase.cs | 12 ++- .../Integration/WrapStateRichText.cs | 14 +++- .../DataHolders/RichTextFragmentSimple.cs | 8 ++ .../DataHolders/TextLineSimple.cs | 5 +- .../TrueTypeMeasurer/TextData.cs | 8 +- 6 files changed, 91 insertions(+), 29 deletions(-) diff --git a/src/EPPlus.Fonts.OpenType/Integration/TextLayoutEngine.RichText.cs b/src/EPPlus.Fonts.OpenType/Integration/TextLayoutEngine.RichText.cs index 3df9fe747..61f85f5f2 100644 --- a/src/EPPlus.Fonts.OpenType/Integration/TextLayoutEngine.RichText.cs +++ b/src/EPPlus.Fonts.OpenType/Integration/TextLayoutEngine.RichText.cs @@ -12,6 +12,7 @@ Date Author Change 01/22/2025 EPPlus Software AB Optimized with shaping cache 01/23/2025 EPPlus Software AB Fixed lastSpaceIndex bug in multi-fragment wrapping *************************************************************************************************/ +using EPPlus.Fonts.OpenType.TrueTypeMeasurer.DataHolders; using EPPlus.Fonts.OpenType.Utilities; using OfficeOpenXml.Interfaces.Drawing.Text; using System; @@ -81,6 +82,10 @@ private void ProcessFragment( Array.Clear(charWidths, 0, len); FillCharWidths(shaped.Glyphs, scale, len, charWidths); + state.LineFrag = new LineFragment(state.CurrentFragmentIdx, lineBuilder.Length); + state.LineFrag.StartIdx = lineBuilder.Length; + state.LineFrag.RtIdx = state.CurrentFragmentIdx; + int i = 0; while (i < len) { @@ -90,6 +95,7 @@ private void ProcessFragment( { HandleLineBreak(lineBuilder, state); SkipLineBreakChars(fragment.Text, ref i); + state.CurrentLineWidth = 0; state.CurrentWordWidth = 0; state.WordStart = -1; // Reset after line break @@ -99,6 +105,7 @@ private void ProcessFragment( state.CurrentLineWidth += charWidths[i]; state.CurrentWordWidth += charWidths[i]; + state.LineFrag.Width += charWidths[i]; lineBuilder.Append(c); @@ -110,28 +117,13 @@ private void ProcessFragment( if (state.CurrentLineWidth > maxWidthPoints) { - WrapCurrentLine(lineBuilder, state, maxWidthPoints); - - state.CurrentWordWidth = state.CurrentLineWidth; - - state.WordStart = -1; - state.LineStart = -1; - //We do not append ending spaces to the new line - if (c != ' ') - { - //lineBuilder.Append(c); - if(state.CurrentWordWidth == 0) - { - //The char that made us move past maxWidth - //must be added to the new line - //A whole word being moved down is handled in wrapCurrentLine. - state.CurrentWordWidth = charWidths[i]; - state.CurrentLineWidth = charWidths[i]; - } - } + WrapCurrentLine(lineBuilder, state, maxWidthPoints, charWidths[i]); } i++; } + + state.CurrentTextLine.LineFragments.Add(state.LineFrag); + state.CurrentFragmentIdx++; } private void FillCharWidths(ShapedGlyph[] glyphs, double scale, int textLength, double[] charWidths) @@ -160,12 +152,20 @@ private void HandleLineBreak(StringBuilder lineBuilder, WrapStateRichText state) if (lineBuilder.Length > 0) { _lineListBuffer.Add(lineBuilder.ToString()); + state.CurrentTextLine.Text = lineBuilder.ToString(); } else if (state.CurrentLineWidth > 0) { _lineListBuffer.Add(string.Empty); + state.CurrentTextLine.Text = string.Empty; } + state.CurrentTextLine.Width = state.CurrentLineWidth; + state.CurrentTextLine.LineFragments.Add(state.LineFrag); + state.Lines.Add(state.CurrentTextLine); + + state.EndCurrentTextLineAndIntializeNext(state.CurrentFragmentIdx, 0); + lineBuilder.Length = 0; } @@ -178,7 +178,7 @@ private void SkipLineBreakChars(string text, ref int i) i++; } - private void WrapCurrentLine(StringBuilder lineBuilder, WrapStateRichText state, double maxWidthPoints) + private void WrapCurrentLine(StringBuilder lineBuilder, WrapStateRichText state, double maxWidthPoints, double advanceWidth) { // Bounds check to prevent ArgumentOutOfRangeException if (state.WordStart >= 0 && state.WordStart < lineBuilder.Length) @@ -187,20 +187,49 @@ private void WrapCurrentLine(StringBuilder lineBuilder, WrapStateRichText state, _lineListBuffer.Add(line); lineBuilder.Remove(0, state.WordStart + 1); + //handle line data + state.CurrentTextLine.Width = state.CurrentLineWidth - state.CurrentWordWidth; + state.CurrentTextLine.Text = line; + //A word was moved down. The new line must have the width and pos of the word. state.CurrentLineWidth = state.CurrentWordWidth; state.LineStart = state.WordStart; } else { + var lastChar = lineBuilder[lineBuilder.Length-1]; + var line = lineBuilder.ToString(0, lineBuilder.Length - 1); // No valid space found - wrap entire line - _lineListBuffer.Add(lineBuilder.ToString(0, lineBuilder.Length -1)); + _lineListBuffer.Add(line); + + //handle line data + state.CurrentTextLine.Width = state.CurrentLineWidth - advanceWidth; + state.CurrentTextLine.Text = line; + + //Add the char that went over max to the next line state.CurrentLineWidth = 0; lineBuilder.Length = 0; - lineBuilder.Append(lastChar); + //Append the char that goes over max unless it is a space + if (lastChar != ' ') + { + lineBuilder.Append(lastChar); + + //The char that made us move past maxWidth + //must be added to the new line + state.CurrentWordWidth = advanceWidth; + state.CurrentLineWidth = advanceWidth; + } } + + state.EndCurrentTextLineAndIntializeNext(state.CurrentFragmentIdx, lineBuilder.Length); + state.LineFrag.Width = state.CurrentLineWidth; + + state.CurrentWordWidth = state.CurrentLineWidth; + + state.WordStart = -1; + state.LineStart = -1; } private void FinalizeCurrentLine(StringBuilder lineBuilder, double lineWidth, int lastSpaceIndex) diff --git a/src/EPPlus.Fonts.OpenType/Integration/WrapStateBase.cs b/src/EPPlus.Fonts.OpenType/Integration/WrapStateBase.cs index 4bd7f1420..d0fee80b1 100644 --- a/src/EPPlus.Fonts.OpenType/Integration/WrapStateBase.cs +++ b/src/EPPlus.Fonts.OpenType/Integration/WrapStateBase.cs @@ -1,4 +1,5 @@ -using System; +using EPPlus.Fonts.OpenType.TrueTypeMeasurer.DataHolders; +using System; using System.Collections.Generic; using System.Linq; using System.Text; @@ -12,6 +13,15 @@ internal abstract class WrapStateBase public double CurrentLineWidth { get; set; } public double CurrentWordWidth { get; set; } + /// + /// Data holders for the individual lines + /// + public List Lines = new List(); + + public TextLineSimple CurrentTextLine = new TextLineSimple(); + + internal int CurrentFragmentIdx = 0; + public bool IsCompleteWordReady(CharacterType charType, int currentPosition) { return (charType == CharacterType.Space || charType == CharacterType.EndOfText) diff --git a/src/EPPlus.Fonts.OpenType/Integration/WrapStateRichText.cs b/src/EPPlus.Fonts.OpenType/Integration/WrapStateRichText.cs index 043cefa2c..b414293ef 100644 --- a/src/EPPlus.Fonts.OpenType/Integration/WrapStateRichText.cs +++ b/src/EPPlus.Fonts.OpenType/Integration/WrapStateRichText.cs @@ -1,4 +1,5 @@ -using System; +using EPPlus.Fonts.OpenType.TrueTypeMeasurer.DataHolders; +using System; using System.Collections.Generic; using System.Linq; using System.Text; @@ -7,9 +8,20 @@ namespace EPPlus.Fonts.OpenType.Integration { internal class WrapStateRichText : WrapStateBase { + internal LineFragment LineFrag = null; + public WrapStateRichText(double lineWidth) { CurrentLineWidth = lineWidth; } + + internal void EndCurrentTextLineAndIntializeNext(int fragIdx, int startIdxOfNewFragment) + { + CurrentTextLine.LineFragments.Add(LineFrag); + Lines.Add(CurrentTextLine); + + CurrentTextLine = new TextLineSimple(); + LineFrag = new LineFragment(fragIdx, startIdxOfNewFragment); + } } } diff --git a/src/EPPlus.Fonts.OpenType/TrueTypeMeasurer/DataHolders/RichTextFragmentSimple.cs b/src/EPPlus.Fonts.OpenType/TrueTypeMeasurer/DataHolders/RichTextFragmentSimple.cs index 654434d6a..0afd62182 100644 --- a/src/EPPlus.Fonts.OpenType/TrueTypeMeasurer/DataHolders/RichTextFragmentSimple.cs +++ b/src/EPPlus.Fonts.OpenType/TrueTypeMeasurer/DataHolders/RichTextFragmentSimple.cs @@ -25,6 +25,14 @@ public RichTextFragmentSimple() charStarIdxWithinCurrentLine = 0; } + //public RichTextFragmentSimple(int fragIdx, ) + //{ + // Width = 0; + // Fragidx = 0; + // OverallParagraphStartCharIdx = 0; + // charStarIdxWithinCurrentLine = 0; + //} + internal RichTextFragmentSimple Clone() { var fragment = new RichTextFragmentSimple(); diff --git a/src/EPPlus.Fonts.OpenType/TrueTypeMeasurer/DataHolders/TextLineSimple.cs b/src/EPPlus.Fonts.OpenType/TrueTypeMeasurer/DataHolders/TextLineSimple.cs index bda52fe14..7c58bbc09 100644 --- a/src/EPPlus.Fonts.OpenType/TrueTypeMeasurer/DataHolders/TextLineSimple.cs +++ b/src/EPPlus.Fonts.OpenType/TrueTypeMeasurer/DataHolders/TextLineSimple.cs @@ -1,4 +1,5 @@ -using System; +using EPPlus.Fonts.OpenType.Integration; +using System; using System.Collections.Generic; using System.Linq; using System.Text; @@ -9,6 +10,8 @@ public class TextLineSimple { public List RtFragments { get; private set; } = new List(); + public List LineFragments { get; private set; } = new List(); + public string Text { get; internal set; } /// /// The largest font size within this line diff --git a/src/EPPlus.Fonts.OpenType/TrueTypeMeasurer/TextData.cs b/src/EPPlus.Fonts.OpenType/TrueTypeMeasurer/TextData.cs index 796cc8d64..64d67c3fc 100644 --- a/src/EPPlus.Fonts.OpenType/TrueTypeMeasurer/TextData.cs +++ b/src/EPPlus.Fonts.OpenType/TrueTypeMeasurer/TextData.cs @@ -1284,8 +1284,8 @@ internal static List WrapMultipleTextFragmentsToTextLines(TextPa //Largest font might be wrong here as we may linebreak at a different font than current font if we break at the start of a word //and not current i - largestFontCurrentLine = null; - CurrentLineLargestFontSize = 0; + largestFontCurrentLine = paragraph.FontIndexDict[currentLineRtFragments[0].Fragidx]; + CurrentLineLargestFontSize = paragraph.FontSizes[currentLineRtFragments[0].Fragidx]; for (int j = currentLineRtFragments[0].Fragidx; j < fragIdxAtBreak + 1; j++) { @@ -1302,8 +1302,8 @@ internal static List WrapMultipleTextFragmentsToTextLines(TextPa AddDataToSimpleLine(currentTextLine, paragraph, currentFont, largestFontCurrentLine, CurrentLineLargestFontSize, prevLineWidth, fragmentIdx); //Largest font is not neccesarily current font as word wrap might have wrapped another font between - largestFontCurrentLine = null; - CurrentLineLargestFontSize = 0; + largestFontCurrentLine = paragraph.FontIndexDict[fragIdxAtBreak]; + CurrentLineLargestFontSize = paragraph.FontSizes[fragIdxAtBreak]; for (int j = fragIdxAtBreak; j < fragmentIdx + 1; j++) { From 34231edd8a904413aaa36c36ac78088131a2fc57 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ossian=20Edstr=C3=B6m?= Date: Wed, 11 Feb 2026 17:10:23 +0100 Subject: [PATCH 057/151] Nearly functrional refactor --- .../ImageRenderer.cs | 2 + .../ChartTypeDrawers/LineChartTypeDrawer.cs | 2 +- .../Integration/TextLayoutEngineTests.cs | 175 ++++++++++++++++++ .../Integration/LineFragment.cs | 4 +- .../Integration/TextLayoutEngine.RichText.cs | 59 +++++- .../Integration/WrapStateRichText.cs | 74 +++++++- .../DataHolders/TextLineSimple.cs | 13 +- src/EPPlusTest/Drawing/CopyDrawingTests.cs | 1 - 8 files changed, 316 insertions(+), 14 deletions(-) diff --git a/src/EPPlus.Export.ImageRenderer/ImageRenderer.cs b/src/EPPlus.Export.ImageRenderer/ImageRenderer.cs index 9ad5dbfca..0642b6dc5 100644 --- a/src/EPPlus.Export.ImageRenderer/ImageRenderer.cs +++ b/src/EPPlus.Export.ImageRenderer/ImageRenderer.cs @@ -52,6 +52,8 @@ public string RenderDrawingToSvg(ExcelDrawing drawing) throw new NotImplementedException("Image rendering for drawing type not implemented."); } + + //public string RenderRangeToSvg(ExcelRange range) //{ // var ws = range.Worksheet; diff --git a/src/EPPlus.Export.ImageRenderer/Svg/Chart/ChartTypeDrawers/LineChartTypeDrawer.cs b/src/EPPlus.Export.ImageRenderer/Svg/Chart/ChartTypeDrawers/LineChartTypeDrawer.cs index be422752e..d71446e78 100644 --- a/src/EPPlus.Export.ImageRenderer/Svg/Chart/ChartTypeDrawers/LineChartTypeDrawer.cs +++ b/src/EPPlus.Export.ImageRenderer/Svg/Chart/ChartTypeDrawers/LineChartTypeDrawer.cs @@ -37,7 +37,7 @@ internal LineChartTypeDrawer(SvgChart svgChart, ExcelChart chartType) : base(svg ExcelChartAxisStandard.CalculateStacked100(yValues); } - for(var i= 0;i < xValues.Count;i++) + for(var i= 0; i < xValues.Count; i++) { var xSerie = xValues[i]; var ySerie = yValues[i]; diff --git a/src/EPPlus.Fonts.OpenType.Tests/Integration/TextLayoutEngineTests.cs b/src/EPPlus.Fonts.OpenType.Tests/Integration/TextLayoutEngineTests.cs index 6b4990090..bc88236ec 100644 --- a/src/EPPlus.Fonts.OpenType.Tests/Integration/TextLayoutEngineTests.cs +++ b/src/EPPlus.Fonts.OpenType.Tests/Integration/TextLayoutEngineTests.cs @@ -3,6 +3,7 @@ using EPPlus.Fonts.OpenType.TrueTypeMeasurer.DataHolders; using EPPlus.Fonts.OpenType.Utils; using Microsoft.VisualStudio.TestTools.UnitTesting; +using OfficeOpenXml.Export.HtmlExport.StyleCollectors.StyleContracts; using OfficeOpenXml.Interfaces.Drawing.Text; using System.Collections.Generic; using System.Diagnostics; @@ -431,6 +432,180 @@ public void WrapRichTextDifficultCase() Assert.AreEqual("16SvgSize 24", wrappedLines[4]); } + [TestMethod] + public void EnsureLineFragmentsAreMeasuredCorrectlyWhenWrapping() + { + List lstOfRichText = new() { "TextBox2", "ra underline", "La Strike", "Goudy size 16"}; + var font2 = new MeasurementFont() + { + FontFamily = "Aptos Narrow", + Size = 11, + Style = MeasurementFontStyles.Bold + }; + + var font3 = new MeasurementFont() + { + FontFamily = "Aptos Narrow", + Size = 11, + Style = MeasurementFontStyles.Underline + }; + + var font4 = new MeasurementFont() + { + FontFamily = "Aptos Narrow", + Size = 11, + Style = MeasurementFontStyles.Strikeout + }; + + var font5 = new MeasurementFont() + { + FontFamily = "Goudy Stout", + Size = 16, + Style = MeasurementFontStyles.Regular + }; + + + List fonts = new() { font2, font3, font4, font5}; + var fragments = new List(); + + for (int i = 0; i < lstOfRichText.Count(); i++) + { + var currentFrag = new TextFragment() { Text = lstOfRichText[i], Font = fonts[i] }; + fragments.Add(currentFrag); + } + + var maxSizePoints = Math.Round(300d, 0, MidpointRounding.AwayFromZero).PixelToPoint(); + var startFont = TextData.GetFontData(font2.FontFamily, GetFontSubType(font2.Style)); + + var shaper = new TextShaper(startFont); + var layout = new TextLayoutEngine(shaper); + + var wrappedLines = layout.WrapRichTextLines(fragments, maxSizePoints); + + + } + + [TestMethod] + public void WrapRichTextDifficultCaseCompare() + { + List lstOfRichText = new() { "TextBox\r\na\r\n", "TextBox2", "ra underline", "La Strike", "Goudy size 16", "SvgSize 24" }; + + var font1 = new MeasurementFont() + { + FontFamily = "Aptos Narrow", + Size = 11, + Style = MeasurementFontStyles.Regular + }; ; + + var font2 = new MeasurementFont() + { + FontFamily = "Aptos Narrow", + Size = 11, + Style = MeasurementFontStyles.Bold + }; + + var font3 = new MeasurementFont() + { + FontFamily = "Aptos Narrow", + Size = 11, + Style = MeasurementFontStyles.Underline + }; + + var font4 = new MeasurementFont() + { + FontFamily = "Aptos Narrow", + Size = 11, + Style = MeasurementFontStyles.Strikeout + }; + + var font5 = new MeasurementFont() + { + FontFamily = "Goudy Stout", + Size = 16, + Style = MeasurementFontStyles.Regular + }; + + + var font6 = new MeasurementFont() + { + FontFamily = "Aptos Narrow", + Size = 24, + Style = MeasurementFontStyles.Regular + }; + + List fonts = new() { font1, font2, font3, font4, font5, font6 }; + var fragments = new List(); + + for (int i = 0; i < lstOfRichText.Count(); i++) + { + var currentFrag = new TextFragment() { Text = lstOfRichText[i], Font = fonts[i] }; + fragments.Add(currentFrag); + } + + var maxSizePoints = Math.Round(300d, 0, MidpointRounding.AwayFromZero).PixelToPoint(); + var startFont = TextData.GetFontData(font1.FontFamily, GetFontSubType(font1.Style)); + + var shaper = new TextShaper(startFont); + var layout = new TextLayoutEngine(shaper); + + var wrappedLines = layout.WrapRichTextLines(fragments, maxSizePoints); + + var measurer = new FontMeasurerTrueType(font1); + + TextFragmentCollection collection = new TextFragmentCollection(lstOfRichText); + var simpleLines = measurer.WrapMultipleTextFragmentsToTextLines(collection, fonts, maxSizePoints); + + Assert.AreEqual("TextBox", wrappedLines[0].Text); + Assert.AreEqual("a", wrappedLines[1].Text); + Assert.AreEqual("TextBox2ra underlineLa", wrappedLines[2].Text); + Assert.AreEqual("StrikeGoudy size", wrappedLines[3].Text); + Assert.AreEqual("16SvgSize 24", wrappedLines[4].Text); + + Assert.AreEqual(simpleLines[0].Text, wrappedLines[0].Text); + Assert.AreEqual(simpleLines[1].Text, wrappedLines[1].Text); + Assert.AreEqual(simpleLines[2].Text, wrappedLines[2].Text); + Assert.AreEqual(simpleLines[3].Text, wrappedLines[3].Text); + Assert.AreEqual(simpleLines[4].Text, wrappedLines[4].Text); + + //Rather large epsilon but the char widths are each individually more correct now + var epsilon = 0.1d; + + Assert.AreEqual(simpleLines[0].Width, wrappedLines[0].Width, epsilon); + Assert.AreEqual(simpleLines[1].Width, wrappedLines[1].Width, epsilon); + Assert.AreEqual(simpleLines[2].Width, wrappedLines[2].Width, epsilon); + Assert.AreEqual(simpleLines[3].Width, wrappedLines[3].Width, epsilon); + Assert.AreEqual(simpleLines[4].Width, wrappedLines[4].Width, epsilon); + + var line1FragmentsOld = simpleLines[0].RtFragments; + var line1FragmentsNew = wrappedLines[0].LineFragments; + Assert.AreEqual(line1FragmentsOld[0].Width, line1FragmentsNew[0].Width, epsilon); + + var line2FragmentsOld = simpleLines[1].RtFragments; + var line2FragmentsNew = wrappedLines[1].LineFragments; + + Assert.AreEqual(line2FragmentsOld[0].Width, line2FragmentsNew[0].Width, epsilon); + + var line3FragmentsOld = simpleLines[2].RtFragments; + var line3FragmentsNew = wrappedLines[2].LineFragments; + + Assert.AreEqual(line3FragmentsOld[0].Width, line3FragmentsNew[0].Width, epsilon); + Assert.AreEqual(line3FragmentsOld[1].Width, line3FragmentsNew[1].Width, epsilon); + Assert.AreEqual(line3FragmentsOld[2].Width, line3FragmentsNew[2].Width, epsilon); + + + var line4FragmentsOld = simpleLines[3].RtFragments; + var line4FragmentsNew = wrappedLines[3].LineFragments; + + Assert.AreEqual(line4FragmentsOld[0].Width, line4FragmentsNew[0].Width, epsilon); + Assert.AreEqual(line4FragmentsOld[1].Width, line4FragmentsNew[1].Width, epsilon); + + var line5FragmentsOld = simpleLines[4].RtFragments; + var line5FragmentsNew = wrappedLines[4].LineFragments; + + Assert.AreEqual(line5FragmentsOld[0].Width, line5FragmentsNew[0].Width, epsilon); + Assert.AreEqual(line5FragmentsOld[1].Width, line5FragmentsNew[1].Width, epsilon); + } + [TestMethod] public void WrapRichText_WordSpanningFragments_MeasuresCorrectly() { diff --git a/src/EPPlus.Fonts.OpenType/Integration/LineFragment.cs b/src/EPPlus.Fonts.OpenType/Integration/LineFragment.cs index 5b0b8ffa2..f36eac26e 100644 --- a/src/EPPlus.Fonts.OpenType/Integration/LineFragment.cs +++ b/src/EPPlus.Fonts.OpenType/Integration/LineFragment.cs @@ -12,11 +12,11 @@ public class LineFragment { public int StartIdx { get; set; } public double Width { get; set; } - public int RtIdx { get; set; } + public int RtFragIdx { get; set; } internal LineFragment(int rtFragmentIdx, int idxWithinLine) { - RtIdx = rtFragmentIdx; + RtFragIdx = rtFragmentIdx; StartIdx = idxWithinLine; } } diff --git a/src/EPPlus.Fonts.OpenType/Integration/TextLayoutEngine.RichText.cs b/src/EPPlus.Fonts.OpenType/Integration/TextLayoutEngine.RichText.cs index 61f85f5f2..6b8a8cb77 100644 --- a/src/EPPlus.Fonts.OpenType/Integration/TextLayoutEngine.RichText.cs +++ b/src/EPPlus.Fonts.OpenType/Integration/TextLayoutEngine.RichText.cs @@ -17,6 +17,7 @@ Date Author Change using OfficeOpenXml.Interfaces.Drawing.Text; using System; using System.Collections.Generic; +using System.Linq; using System.Text; namespace EPPlus.Fonts.OpenType.Integration @@ -54,6 +55,7 @@ public List WrapRichText( } FinalizeCurrentLine(lineBuilder, state.CurrentLineWidth, state.WordStart); + state.EndCurrentTextLine(); if (_lineListBuffer.Count == 0) { @@ -63,6 +65,42 @@ public List WrapRichText( return new List(_lineListBuffer); } + public List WrapRichTextLines( + List fragments, + double maxWidthPoints) + { + if (fragments == null || fragments.Count == 0) + { + return new List(); + } + + _lineListBuffer.Clear(); + + var lineBuilder = new StringBuilder(512); + var state = new WrapStateRichText(0); + state.WordStart = -1; + state.LineStart = -1; + + foreach (var fragment in fragments) + { + if (string.IsNullOrEmpty(fragment.Text)) continue; + + ProcessFragment(fragment, maxWidthPoints, lineBuilder, state); + } + + FinalizeCurrentLine(lineBuilder, state.CurrentLineWidth, state.WordStart); + state.CurrentTextLine.Width = state.CurrentLineWidth; + state.CurrentTextLine.Text = lineBuilder.ToString(); + state.EndCurrentTextLine(); + + if (_lineListBuffer.Count == 0) + { + _lineListBuffer.Add(string.Empty); + } + + return state.Lines; + } + private void ProcessFragment( TextFragment fragment, double maxWidthPoints, @@ -84,7 +122,7 @@ private void ProcessFragment( state.LineFrag = new LineFragment(state.CurrentFragmentIdx, lineBuilder.Length); state.LineFrag.StartIdx = lineBuilder.Length; - state.LineFrag.RtIdx = state.CurrentFragmentIdx; + state.LineFrag.RtFragIdx = state.CurrentFragmentIdx; int i = 0; while (i < len) @@ -111,8 +149,7 @@ private void ProcessFragment( if (c == ' ') { - state.WordStart = lineBuilder.Length - 1; - state.CurrentWordWidth = 0; + state.SetAndLogWordStartState(lineBuilder.Length - 1); } if (state.CurrentLineWidth > maxWidthPoints) @@ -122,7 +159,11 @@ private void ProcessFragment( i++; } - state.CurrentTextLine.LineFragments.Add(state.LineFrag); + if(state.LineFrag.Width > 0) + { + state.CurrentTextLine.LineFragments.Add(state.LineFrag); + } + state.CurrentFragmentIdx++; } @@ -161,9 +202,6 @@ private void HandleLineBreak(StringBuilder lineBuilder, WrapStateRichText state) } state.CurrentTextLine.Width = state.CurrentLineWidth; - state.CurrentTextLine.LineFragments.Add(state.LineFrag); - state.Lines.Add(state.CurrentTextLine); - state.EndCurrentTextLineAndIntializeNext(state.CurrentFragmentIdx, 0); lineBuilder.Length = 0; @@ -180,6 +218,9 @@ private void SkipLineBreakChars(string text, ref int i) private void WrapCurrentLine(StringBuilder lineBuilder, WrapStateRichText state, double maxWidthPoints, double advanceWidth) { + int fragIdxAtBreak = state.CurrentFragmentIdx; + LineFragment lineAfterBreak = null; + // Bounds check to prevent ArgumentOutOfRangeException if (state.WordStart >= 0 && state.WordStart < lineBuilder.Length) { @@ -191,6 +232,10 @@ private void WrapCurrentLine(StringBuilder lineBuilder, WrapStateRichText state, state.CurrentTextLine.Width = state.CurrentLineWidth - state.CurrentWordWidth; state.CurrentTextLine.Text = line; + fragIdxAtBreak = state.GetFragIdxAtWordStart(); + //Because of word-wrap we may have richTextFragments on the current line that is no longer part of it after wrap. + state.AdjustLineFragmentsForNextLine(); + //A word was moved down. The new line must have the width and pos of the word. state.CurrentLineWidth = state.CurrentWordWidth; state.LineStart = state.WordStart; diff --git a/src/EPPlus.Fonts.OpenType/Integration/WrapStateRichText.cs b/src/EPPlus.Fonts.OpenType/Integration/WrapStateRichText.cs index b414293ef..744f87e3a 100644 --- a/src/EPPlus.Fonts.OpenType/Integration/WrapStateRichText.cs +++ b/src/EPPlus.Fonts.OpenType/Integration/WrapStateRichText.cs @@ -15,13 +15,83 @@ public WrapStateRichText(double lineWidth) CurrentLineWidth = lineWidth; } - internal void EndCurrentTextLineAndIntializeNext(int fragIdx, int startIdxOfNewFragment) + internal void EndCurrentTextLine() { CurrentTextLine.LineFragments.Add(LineFrag); Lines.Add(CurrentTextLine); + } + + internal void EndCurrentTextLineAndIntializeNext(int fragIdx, int startIdxOfNewFragment) + { + EndCurrentTextLine(); CurrentTextLine = new TextLineSimple(); - LineFrag = new LineFragment(fragIdx, startIdxOfNewFragment); + + if (_fragmentsForNextLine == null) + { + LineFrag = new LineFragment(fragIdx, startIdxOfNewFragment); + } + else + { + //Set pre-calculated fragments + CurrentTextLine.LineFragments = _fragmentsForNextLine; + _fragmentsForNextLine = null; + } + } + + int _rtIdxAtWordStart = -1; + int _lstIdxWithinLine = -1; + double _prevWordWidthAtWordStart = -1; + double _lineWidthAtWordStart = -1; + double _lineFragWidthAtWordStart = -1; + + internal void SetAndLogWordStartState(int wordStart) + { + WordStart = wordStart; + + _prevWordWidthAtWordStart = CurrentWordWidth; + CurrentWordWidth = 0; + + _lineWidthAtWordStart = CurrentLineWidth; + _rtIdxAtWordStart = CurrentFragmentIdx; + _lineFragWidthAtWordStart = LineFrag.Width; + //Count okay since the fragment we are on has not yet been added to currentline at this point + //So its index Will Be this. + _lstIdxWithinLine = CurrentTextLine.LineFragments.Count; + } + + internal int GetFragIdxAtWordStart() + { + return _rtIdxAtWordStart; + } + + internal double GetFragmentWidthAtWordStart() + { + return _lineFragWidthAtWordStart; + } + + List _fragmentsForNextLine = null; + + internal void AdjustLineFragmentsForNextLine() + { + var origFragment = CurrentTextLine.LineFragments[_lstIdxWithinLine]; + + var resultingFragment = CurrentTextLine.SplitAndGetLeftoverLineFragment(ref origFragment, _lineFragWidthAtWordStart); + CurrentTextLine.LineFragments[_lstIdxWithinLine] = origFragment; + + _fragmentsForNextLine = new List(); + + //Iterate backwards from back of list until we hit fragment + for (int i = CurrentTextLine.LineFragments.Count; i > _lstIdxWithinLine; i--) + { + //Add fragment to the new list + _fragmentsForNextLine.Insert(0, CurrentTextLine.LineFragments[i-1]); + //Remove it from the old + CurrentTextLine.LineFragments.RemoveAt(i-1); + } + + //We also insert the fragment we've split out + _fragmentsForNextLine.Insert(0, resultingFragment); } } } diff --git a/src/EPPlus.Fonts.OpenType/TrueTypeMeasurer/DataHolders/TextLineSimple.cs b/src/EPPlus.Fonts.OpenType/TrueTypeMeasurer/DataHolders/TextLineSimple.cs index 7c58bbc09..af9f9b2ca 100644 --- a/src/EPPlus.Fonts.OpenType/TrueTypeMeasurer/DataHolders/TextLineSimple.cs +++ b/src/EPPlus.Fonts.OpenType/TrueTypeMeasurer/DataHolders/TextLineSimple.cs @@ -10,7 +10,7 @@ public class TextLineSimple { public List RtFragments { get; private set; } = new List(); - public List LineFragments { get; private set; } = new List(); + public List LineFragments { get; internal set; } = new List(); public string Text { get; internal set; } /// @@ -73,5 +73,16 @@ public string GetFragmentText(RichTextFragmentSimple rtFragment) return Text.Substring(startIdx, endIdx - startIdx); } } + + internal LineFragment SplitAndGetLeftoverLineFragment(ref LineFragment lf, double widthAtSplit) + { + //If we are splitting a fragment its position in the new line should be 0 + var newLineFragment = new LineFragment(lf.RtFragIdx, 0); + newLineFragment.Width = lf.Width - widthAtSplit; + + lf.Width = widthAtSplit; + + return newLineFragment; + } } } diff --git a/src/EPPlusTest/Drawing/CopyDrawingTests.cs b/src/EPPlusTest/Drawing/CopyDrawingTests.cs index 26cf4e86a..d4317b3eb 100644 --- a/src/EPPlusTest/Drawing/CopyDrawingTests.cs +++ b/src/EPPlusTest/Drawing/CopyDrawingTests.cs @@ -760,7 +760,6 @@ public void CopyNamedRangeToTargetSheetWB(ExcelPackage src, ExcelPackage target, target.Workbook.Worksheets.Delete(tmpWs); } - [TestMethod] public void s814CopySameImageTwiceToEmptyNamedRanges() { From 2c9555a24fd1a66ff9ad7f86b4c446a92493fd2a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ossian=20Edstr=C3=B6m?= Date: Thu, 19 Feb 2026 16:17:17 +0100 Subject: [PATCH 058/151] Wrapping improvements --- .../Integration/TextLayoutEngine.RichText.cs | 1 - .../Integration/WrapStateRichText.cs | 49 ++++++++++---- .../DataHolders/TextLineSimple.cs | 8 +-- src/EPPlusTest/Drawing/Chart/DatalabelTest.cs | 64 +++++++++++++++++++ 4 files changed, 103 insertions(+), 19 deletions(-) diff --git a/src/EPPlus.Fonts.OpenType/Integration/TextLayoutEngine.RichText.cs b/src/EPPlus.Fonts.OpenType/Integration/TextLayoutEngine.RichText.cs index 6b8a8cb77..a4e767a91 100644 --- a/src/EPPlus.Fonts.OpenType/Integration/TextLayoutEngine.RichText.cs +++ b/src/EPPlus.Fonts.OpenType/Integration/TextLayoutEngine.RichText.cs @@ -270,7 +270,6 @@ private void WrapCurrentLine(StringBuilder lineBuilder, WrapStateRichText state, state.EndCurrentTextLineAndIntializeNext(state.CurrentFragmentIdx, lineBuilder.Length); state.LineFrag.Width = state.CurrentLineWidth; - state.CurrentWordWidth = state.CurrentLineWidth; state.WordStart = -1; diff --git a/src/EPPlus.Fonts.OpenType/Integration/WrapStateRichText.cs b/src/EPPlus.Fonts.OpenType/Integration/WrapStateRichText.cs index 744f87e3a..63626d4d8 100644 --- a/src/EPPlus.Fonts.OpenType/Integration/WrapStateRichText.cs +++ b/src/EPPlus.Fonts.OpenType/Integration/WrapStateRichText.cs @@ -23,24 +23,26 @@ internal void EndCurrentTextLine() internal void EndCurrentTextLineAndIntializeNext(int fragIdx, int startIdxOfNewFragment) { - EndCurrentTextLine(); - - CurrentTextLine = new TextLineSimple(); + var nextLine = new TextLineSimple(); if (_fragmentsForNextLine == null) { + EndCurrentTextLine(); LineFrag = new LineFragment(fragIdx, startIdxOfNewFragment); } else { - //Set pre-calculated fragments - CurrentTextLine.LineFragments = _fragmentsForNextLine; + //_fragmentsForNextLine.Add(LineFrag); + nextLine.LineFragments = _fragmentsForNextLine; + + Lines.Add(CurrentTextLine); _fragmentsForNextLine = null; } + CurrentTextLine = nextLine; } int _rtIdxAtWordStart = -1; - int _lstIdxWithinLine = -1; + int _listIdxWithinLine = -1; double _prevWordWidthAtWordStart = -1; double _lineWidthAtWordStart = -1; double _lineFragWidthAtWordStart = -1; @@ -55,9 +57,15 @@ internal void SetAndLogWordStartState(int wordStart) _lineWidthAtWordStart = CurrentLineWidth; _rtIdxAtWordStart = CurrentFragmentIdx; _lineFragWidthAtWordStart = LineFrag.Width; - //Count okay since the fragment we are on has not yet been added to currentline at this point - //So its index Will Be this. - _lstIdxWithinLine = CurrentTextLine.LineFragments.Count; + + _listIdxWithinLine = CurrentTextLine.LineFragments.Count - 1; + + //if (CurrentTextLine.LineFragments[_listIdxWithinLine].RtFragIdx < _rtIdxAtWordStart) + //{ + // //When the word begins we are on a fragment that has not yet been added to the list. + // //It will be the next index when added + // _listIdxWithinLine += 1; + //} } internal int GetFragIdxAtWordStart() @@ -74,24 +82,37 @@ internal double GetFragmentWidthAtWordStart() internal void AdjustLineFragmentsForNextLine() { - var origFragment = CurrentTextLine.LineFragments[_lstIdxWithinLine]; + if(_rtIdxAtWordStart == CurrentFragmentIdx) + { + //If we are On the fragment we have not added it yet + //Do so before splitting + CurrentTextLine.LineFragments.Add(LineFrag); + } + + var origFragment = CurrentTextLine.LineFragments[_listIdxWithinLine]; var resultingFragment = CurrentTextLine.SplitAndGetLeftoverLineFragment(ref origFragment, _lineFragWidthAtWordStart); - CurrentTextLine.LineFragments[_lstIdxWithinLine] = origFragment; + CurrentTextLine.LineFragments[_listIdxWithinLine] = origFragment; _fragmentsForNextLine = new List(); //Iterate backwards from back of list until we hit fragment - for (int i = CurrentTextLine.LineFragments.Count; i > _lstIdxWithinLine; i--) + for (int i = CurrentTextLine.LineFragments.Count()-1; i > _listIdxWithinLine; i--) { //Add fragment to the new list - _fragmentsForNextLine.Insert(0, CurrentTextLine.LineFragments[i-1]); + _fragmentsForNextLine.Insert(0, CurrentTextLine.LineFragments[i]); //Remove it from the old - CurrentTextLine.LineFragments.RemoveAt(i-1); + CurrentTextLine.LineFragments.RemoveAt(i); } //We also insert the fragment we've split out _fragmentsForNextLine.Insert(0, resultingFragment); + + _rtIdxAtWordStart = -1; + _listIdxWithinLine = -1; + _prevWordWidthAtWordStart = -1; + _lineWidthAtWordStart = -1; + _lineFragWidthAtWordStart = -1; } } } diff --git a/src/EPPlus.Fonts.OpenType/TrueTypeMeasurer/DataHolders/TextLineSimple.cs b/src/EPPlus.Fonts.OpenType/TrueTypeMeasurer/DataHolders/TextLineSimple.cs index af9f9b2ca..08918654b 100644 --- a/src/EPPlus.Fonts.OpenType/TrueTypeMeasurer/DataHolders/TextLineSimple.cs +++ b/src/EPPlus.Fonts.OpenType/TrueTypeMeasurer/DataHolders/TextLineSimple.cs @@ -74,13 +74,13 @@ public string GetFragmentText(RichTextFragmentSimple rtFragment) } } - internal LineFragment SplitAndGetLeftoverLineFragment(ref LineFragment lf, double widthAtSplit) + internal LineFragment SplitAndGetLeftoverLineFragment(ref LineFragment origLf, double widthAtSplit) { //If we are splitting a fragment its position in the new line should be 0 - var newLineFragment = new LineFragment(lf.RtFragIdx, 0); - newLineFragment.Width = lf.Width - widthAtSplit; + var newLineFragment = new LineFragment(origLf.RtFragIdx, 0); + newLineFragment.Width = origLf.Width - widthAtSplit; - lf.Width = widthAtSplit; + origLf.Width = widthAtSplit; return newLineFragment; } diff --git a/src/EPPlusTest/Drawing/Chart/DatalabelTest.cs b/src/EPPlusTest/Drawing/Chart/DatalabelTest.cs index b5348fa16..5492a3455 100644 --- a/src/EPPlusTest/Drawing/Chart/DatalabelTest.cs +++ b/src/EPPlusTest/Drawing/Chart/DatalabelTest.cs @@ -3,6 +3,7 @@ using OfficeOpenXml.Drawing.Chart; using System; using System.Drawing; +using System.Linq; namespace EPPlusTest.Drawing.Chart { @@ -435,5 +436,68 @@ public void ReadChart() SaveAndCleanup(p); } } + + [TestMethod] + public void GenerateExample() + { + using (var package = OpenPackage("manualLayoutChartCommentRange.xlsx",true)) + { + var ws = package.Workbook.Worksheets.Add("ManualLayout"); + + //Create some values + ws.Cells["A1:A2"].Value = 5; + ws.Cells["B1:B2"].Value = 10; + + //Create a column chart + var sChart = ws.Drawings.AddBarChart("ColumnChart", eBarChartType.ColumnClustered); + + //Add series (clustered columns) to the chart. In this case 2 per series + var s1 = sChart.Series.Add(ws.Cells["A1:A2"]); + var s2 = sChart.Series.Add(ws.Cells["B1:B2"]); + + //Add a general datalabel + var label = s1.DataLabel; + label.ShowValue = true; + + //Add a specific datalabel to the first column in the cluster + var dl = label.DataLabels.Add(0); + + var myLabel = s1.DataLabel.DataLabels[0]; + + //Offset the data label 10% of the charts width to the left + //AKA Remove 10 from x coordinate + dl.Layout.ManualLayout.Left = -10; + + //Offset the data label 10% of the charts height to the top + //AKA remove 10 from y coordinate + dl.Layout.ManualLayout.Top = -10; + + //Save the package at a path + package.SaveAs(@"C:\temp\manualLayoutChart.xlsx"); + } + } + + + [TestMethod] + public void ReadFile() + { + using (var package = OpenTemplatePackage("S1008.xlsx")) + { + var ws = package.Workbook.Worksheets[0]; + + var chart = ws.Drawings[0].As.Chart.LineChart; + + var theCustomLabels = chart.Series[0].DataLabel.DataLabels; + + var label21 = theCustomLabels[21]; + var label26 = theCustomLabels[26]; + + var fldParagraph = label21.TextBody.Paragraphs[0]; + + chart.Series[0].StringLiteralsX[21] = "comment"; + + SaveAndCleanup(package); + } + } } } From 238cd7755113fd78fa1e15cb3b08bb36e35868a1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ossian=20Edstr=C3=B6m?= Date: Thu, 19 Feb 2026 16:40:24 +0100 Subject: [PATCH 059/151] Removed irrelevant tests --- src/EPPlusTest/Drawing/Chart/DatalabelTest.cs | 63 ------------------- 1 file changed, 63 deletions(-) diff --git a/src/EPPlusTest/Drawing/Chart/DatalabelTest.cs b/src/EPPlusTest/Drawing/Chart/DatalabelTest.cs index 5492a3455..b4afd3ee1 100644 --- a/src/EPPlusTest/Drawing/Chart/DatalabelTest.cs +++ b/src/EPPlusTest/Drawing/Chart/DatalabelTest.cs @@ -436,68 +436,5 @@ public void ReadChart() SaveAndCleanup(p); } } - - [TestMethod] - public void GenerateExample() - { - using (var package = OpenPackage("manualLayoutChartCommentRange.xlsx",true)) - { - var ws = package.Workbook.Worksheets.Add("ManualLayout"); - - //Create some values - ws.Cells["A1:A2"].Value = 5; - ws.Cells["B1:B2"].Value = 10; - - //Create a column chart - var sChart = ws.Drawings.AddBarChart("ColumnChart", eBarChartType.ColumnClustered); - - //Add series (clustered columns) to the chart. In this case 2 per series - var s1 = sChart.Series.Add(ws.Cells["A1:A2"]); - var s2 = sChart.Series.Add(ws.Cells["B1:B2"]); - - //Add a general datalabel - var label = s1.DataLabel; - label.ShowValue = true; - - //Add a specific datalabel to the first column in the cluster - var dl = label.DataLabels.Add(0); - - var myLabel = s1.DataLabel.DataLabels[0]; - - //Offset the data label 10% of the charts width to the left - //AKA Remove 10 from x coordinate - dl.Layout.ManualLayout.Left = -10; - - //Offset the data label 10% of the charts height to the top - //AKA remove 10 from y coordinate - dl.Layout.ManualLayout.Top = -10; - - //Save the package at a path - package.SaveAs(@"C:\temp\manualLayoutChart.xlsx"); - } - } - - - [TestMethod] - public void ReadFile() - { - using (var package = OpenTemplatePackage("S1008.xlsx")) - { - var ws = package.Workbook.Worksheets[0]; - - var chart = ws.Drawings[0].As.Chart.LineChart; - - var theCustomLabels = chart.Series[0].DataLabel.DataLabels; - - var label21 = theCustomLabels[21]; - var label26 = theCustomLabels[26]; - - var fldParagraph = label21.TextBody.Paragraphs[0]; - - chart.Series[0].StringLiteralsX[21] = "comment"; - - SaveAndCleanup(package); - } - } } } From 84a4d214f359e97c49ae7abfc3401edf70065104 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ossian=20Edstr=C3=B6m?= Date: Fri, 20 Feb 2026 16:19:53 +0100 Subject: [PATCH 060/151] Fixed richtext wrapping --- .../Integration/TextLayoutEngineTests.cs | 2 ++ .../Integration/TextLayoutEngine.RichText.cs | 15 ++++++-- .../Integration/WrapStateRichText.cs | 34 ++++++++++++++----- 3 files changed, 40 insertions(+), 11 deletions(-) diff --git a/src/EPPlus.Fonts.OpenType.Tests/Integration/TextLayoutEngineTests.cs b/src/EPPlus.Fonts.OpenType.Tests/Integration/TextLayoutEngineTests.cs index bc88236ec..63dbfce6b 100644 --- a/src/EPPlus.Fonts.OpenType.Tests/Integration/TextLayoutEngineTests.cs +++ b/src/EPPlus.Fonts.OpenType.Tests/Integration/TextLayoutEngineTests.cs @@ -483,6 +483,8 @@ public void EnsureLineFragmentsAreMeasuredCorrectlyWhenWrapping() var wrappedLines = layout.WrapRichTextLines(fragments, maxSizePoints); + Assert.AreEqual(12.55224609375d, wrappedLines[0].LineFragments[2].Width); + } [TestMethod] diff --git a/src/EPPlus.Fonts.OpenType/Integration/TextLayoutEngine.RichText.cs b/src/EPPlus.Fonts.OpenType/Integration/TextLayoutEngine.RichText.cs index a4e767a91..667ca67a3 100644 --- a/src/EPPlus.Fonts.OpenType/Integration/TextLayoutEngine.RichText.cs +++ b/src/EPPlus.Fonts.OpenType/Integration/TextLayoutEngine.RichText.cs @@ -161,7 +161,19 @@ private void ProcessFragment( if(state.LineFrag.Width > 0) { - state.CurrentTextLine.LineFragments.Add(state.LineFrag); + var preExistingFragment = state.CurrentTextLine.LineFragments.FirstOrDefault(x => x.RtFragIdx == state.LineFrag.RtFragIdx); + if (preExistingFragment != null) + { + if(preExistingFragment.Width < state.LineFrag.Width) + { + state.CurrentTextLine.LineFragments.Remove(preExistingFragment); + state.CurrentTextLine.LineFragments.Add(state.LineFrag); + } + } + else + { + state.CurrentTextLine.LineFragments.Add(state.LineFrag); + } } state.CurrentFragmentIdx++; @@ -269,7 +281,6 @@ private void WrapCurrentLine(StringBuilder lineBuilder, WrapStateRichText state, } state.EndCurrentTextLineAndIntializeNext(state.CurrentFragmentIdx, lineBuilder.Length); - state.LineFrag.Width = state.CurrentLineWidth; state.CurrentWordWidth = state.CurrentLineWidth; state.WordStart = -1; diff --git a/src/EPPlus.Fonts.OpenType/Integration/WrapStateRichText.cs b/src/EPPlus.Fonts.OpenType/Integration/WrapStateRichText.cs index 63626d4d8..21cd4a18f 100644 --- a/src/EPPlus.Fonts.OpenType/Integration/WrapStateRichText.cs +++ b/src/EPPlus.Fonts.OpenType/Integration/WrapStateRichText.cs @@ -17,7 +17,10 @@ public WrapStateRichText(double lineWidth) internal void EndCurrentTextLine() { - CurrentTextLine.LineFragments.Add(LineFrag); + if (CurrentTextLine.LineFragments.Contains(LineFrag) == false) + { + CurrentTextLine.LineFragments.Add(LineFrag); + } Lines.Add(CurrentTextLine); } @@ -58,14 +61,20 @@ internal void SetAndLogWordStartState(int wordStart) _rtIdxAtWordStart = CurrentFragmentIdx; _lineFragWidthAtWordStart = LineFrag.Width; + if(CurrentTextLine.LineFragments.Count == 0) + { + _listIdxWithinLine = 0; + return; + } + _listIdxWithinLine = CurrentTextLine.LineFragments.Count - 1; - //if (CurrentTextLine.LineFragments[_listIdxWithinLine].RtFragIdx < _rtIdxAtWordStart) - //{ - // //When the word begins we are on a fragment that has not yet been added to the list. - // //It will be the next index when added - // _listIdxWithinLine += 1; - //} + if (CurrentTextLine.LineFragments[_listIdxWithinLine].RtFragIdx < _rtIdxAtWordStart) + { + //When the word begins we are on a fragment that has not yet been added to the list. + //It will be the next index when added + _listIdxWithinLine += 1; + } } internal int GetFragIdxAtWordStart() @@ -105,8 +114,15 @@ internal void AdjustLineFragmentsForNextLine() CurrentTextLine.LineFragments.RemoveAt(i); } - //We also insert the fragment we've split out - _fragmentsForNextLine.Insert(0, resultingFragment); + if(_rtIdxAtWordStart != CurrentFragmentIdx) + { + //We also insert the fragment we've split out + _fragmentsForNextLine.Insert(0, resultingFragment); + } + else + { + LineFrag = resultingFragment; + } _rtIdxAtWordStart = -1; _listIdxWithinLine = -1; From 2a4a3d56f4d9a8d64fa959efa76bce4fe9087bbe Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ossian=20Edstr=C3=B6m?= Date: Fri, 20 Feb 2026 17:43:57 +0100 Subject: [PATCH 061/151] Removed unneccesary if --- .../Integration/TextLayoutEngine.RichText.cs | 14 +------------- 1 file changed, 1 insertion(+), 13 deletions(-) diff --git a/src/EPPlus.Fonts.OpenType/Integration/TextLayoutEngine.RichText.cs b/src/EPPlus.Fonts.OpenType/Integration/TextLayoutEngine.RichText.cs index 667ca67a3..e439e0468 100644 --- a/src/EPPlus.Fonts.OpenType/Integration/TextLayoutEngine.RichText.cs +++ b/src/EPPlus.Fonts.OpenType/Integration/TextLayoutEngine.RichText.cs @@ -161,19 +161,7 @@ private void ProcessFragment( if(state.LineFrag.Width > 0) { - var preExistingFragment = state.CurrentTextLine.LineFragments.FirstOrDefault(x => x.RtFragIdx == state.LineFrag.RtFragIdx); - if (preExistingFragment != null) - { - if(preExistingFragment.Width < state.LineFrag.Width) - { - state.CurrentTextLine.LineFragments.Remove(preExistingFragment); - state.CurrentTextLine.LineFragments.Add(state.LineFrag); - } - } - else - { - state.CurrentTextLine.LineFragments.Add(state.LineFrag); - } + state.CurrentTextLine.LineFragments.Add(state.LineFrag); } state.CurrentFragmentIdx++; From 48ae218fa572bfa7089e2717373e2d622224a775 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ossian=20Edstr=C3=B6m?= Date: Mon, 23 Feb 2026 09:10:24 +0100 Subject: [PATCH 062/151] Removed unused variables/cleanup --- .../Integration/TextLayoutEngine.RichText.cs | 1 - src/EPPlus.Fonts.OpenType/Integration/WrapStateRichText.cs | 7 ------- 2 files changed, 8 deletions(-) diff --git a/src/EPPlus.Fonts.OpenType/Integration/TextLayoutEngine.RichText.cs b/src/EPPlus.Fonts.OpenType/Integration/TextLayoutEngine.RichText.cs index e439e0468..c4aad63d2 100644 --- a/src/EPPlus.Fonts.OpenType/Integration/TextLayoutEngine.RichText.cs +++ b/src/EPPlus.Fonts.OpenType/Integration/TextLayoutEngine.RichText.cs @@ -219,7 +219,6 @@ private void SkipLineBreakChars(string text, ref int i) private void WrapCurrentLine(StringBuilder lineBuilder, WrapStateRichText state, double maxWidthPoints, double advanceWidth) { int fragIdxAtBreak = state.CurrentFragmentIdx; - LineFragment lineAfterBreak = null; // Bounds check to prevent ArgumentOutOfRangeException if (state.WordStart >= 0 && state.WordStart < lineBuilder.Length) diff --git a/src/EPPlus.Fonts.OpenType/Integration/WrapStateRichText.cs b/src/EPPlus.Fonts.OpenType/Integration/WrapStateRichText.cs index 21cd4a18f..0dcfd5cbd 100644 --- a/src/EPPlus.Fonts.OpenType/Integration/WrapStateRichText.cs +++ b/src/EPPlus.Fonts.OpenType/Integration/WrapStateRichText.cs @@ -46,18 +46,13 @@ internal void EndCurrentTextLineAndIntializeNext(int fragIdx, int startIdxOfNewF int _rtIdxAtWordStart = -1; int _listIdxWithinLine = -1; - double _prevWordWidthAtWordStart = -1; - double _lineWidthAtWordStart = -1; double _lineFragWidthAtWordStart = -1; internal void SetAndLogWordStartState(int wordStart) { WordStart = wordStart; - - _prevWordWidthAtWordStart = CurrentWordWidth; CurrentWordWidth = 0; - _lineWidthAtWordStart = CurrentLineWidth; _rtIdxAtWordStart = CurrentFragmentIdx; _lineFragWidthAtWordStart = LineFrag.Width; @@ -126,8 +121,6 @@ internal void AdjustLineFragmentsForNextLine() _rtIdxAtWordStart = -1; _listIdxWithinLine = -1; - _prevWordWidthAtWordStart = -1; - _lineWidthAtWordStart = -1; _lineFragWidthAtWordStart = -1; } } From a0a5d25a78fa1f2b72fc21ecd2e301cd04a9652c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ossian=20Edstr=C3=B6m?= Date: Wed, 25 Feb 2026 15:40:27 +0100 Subject: [PATCH 063/151] fix: textwrapper, added svg: new wrap + ctr align --- .../SvgPathTests.cs | 70 ++++++++++- .../TextRenderTests.cs | 2 +- .../RenderItems/Shared/ParagraphItem.cs | 116 ++++++++++++++---- .../RenderItems/Shared/TextBodyItem.cs | 24 ++-- .../RenderItems/SvgItem/SvgParagraphItem.cs | 2 +- .../Svg/Chart/SvgChartAxis.cs | 2 +- .../Svg/SvgShape.cs | 34 +++-- .../Integration/MeasurerComparisonTests.cs | 2 +- .../Integration/TextLayoutEngineTests.cs | 19 +++ .../Integration/LineFragment.cs | 7 +- .../Integration/TextFragment.cs | 3 + .../Integration/TextLayoutEngine.RichText.cs | 26 +++- .../Integration/TextLayoutEngine.cs | 2 +- .../Integration/WrapStateRichText.cs | 12 +- .../TextShaping/TextShaper.cs | 2 +- .../DataHolders/TextLineSimple.cs | 26 ++++ .../TrueTypeMeasurer/FontMeasurerTrueType.cs | 26 ++++ .../TrueTypeMeasurer/TextData.cs | 32 +++++ .../Drawing/Text/ITextShaper.cs | 4 +- 19 files changed, 350 insertions(+), 61 deletions(-) diff --git a/src/EPPlus.Export.ImageRenderer.Test/SvgPathTests.cs b/src/EPPlus.Export.ImageRenderer.Test/SvgPathTests.cs index 9de1cbd58..f95dbb641 100644 --- a/src/EPPlus.Export.ImageRenderer.Test/SvgPathTests.cs +++ b/src/EPPlus.Export.ImageRenderer.Test/SvgPathTests.cs @@ -1,13 +1,14 @@ -using OfficeOpenXml; +using EPPlus.Fonts.OpenType; +using OfficeOpenXml; using OfficeOpenXml.Drawing; using OfficeOpenXml.Drawing.Chart; +using OfficeOpenXml.Style; using System.Diagnostics; using System.Drawing; using System.Reflection; using System.Text; using System.Xml; using TypeConv = OfficeOpenXml.Utils.TypeConversion; -using EPPlus.Fonts.OpenType; namespace TestProject1 { @@ -525,6 +526,71 @@ public void GenerateSvgForCharts() //} } } + + [TestMethod] + public void GenerateShapeCenteredParagraph() + { + ExcelPackage.License.SetNonCommercialOrganization("EPPlus Project"); + using (var p = OpenPackage("ShapeTestCentered.xlsx",true)) + { + var sheet = p.Workbook.Worksheets.Add("ShapeSheet"); + + var _currentShape = sheet.Drawings.AddShape("CubeTest", eShapeStyle.Cube); + + _currentShape.SetPixelWidth(300d); + _currentShape.SetPixelHeight(300d); + + _currentShape.Fill.Style = eFillStyle.SolidFill; + _currentShape.Fill.Color = System.Drawing.Color.BlueViolet; + _currentShape.Font.Color = System.Drawing.Color.Goldenrod; + + _currentShape.TextBody.TopInsert = 0; + _currentShape.TextBody.BottomInsert = 0; + _currentShape.TextBody.RightInsert = 0; + _currentShape.TextBody.LeftInsert = 0; + + var para1 = _currentShape.TextBody.Paragraphs.Add("TextBox\r\na"); + //var test = _currentShape.TextBody.AnchorCenter; + + para1.LeftMargin = 5; + _currentShape.TextBody.TopInsert = 10; + + var para2 = _currentShape.TextBody.Paragraphs.Add("TextBox2"); + para2.TextRuns[0].FontItalic = true; + para2.TextRuns[0].FontBold = true; + para2.TextRuns.Add("ra underline").FontUnderLine = eUnderLineType.Dash; + para2.TextRuns.Add("La Strike").FontStrike = eStrikeType.Single; + var tRun1 = para2.TextRuns.Add("Goudy size 16"); + tRun1.SetFromFont("Goudy Stout", 16); + + _currentShape.TextBody.Paragraphs[0].HorizontalAlignment = eTextAlignment.Center; + + _currentShape.TextAnchoring = eTextAnchoringType.Top; + + //var smiley = "\ud83d\ude03"; + + tRun1.Fill.Color = System.Drawing.Color.IndianRed; + var tRun2 = para2.TextRuns.Add("SvgSize 24"); + tRun2.FontSize = 24; + + //_currentShape.TextAnchoring = eTextAnchoringType.Center; + + _currentShape.TextBody.HorizontalTextOverflow = eTextHorizontalOverflow.Clip; + _currentShape.TextBody.VerticalTextOverflow = eTextVerticalOverflow.Clip; + + + //SetFillColor(_currentShape.Fill, txtFillColor.Text); + //SetFillColor(_currentShape.Border.Fill, txtBorderColor.Text); + + var aFont = _currentShape.Font; + var paragraph0 = _currentShape.TextBody.Paragraphs[0]; + + _currentShape.GetSizeInPixels(out int testWidth, out int testHeight); + + SaveAndCleanup(p); + } + } + [TestMethod] public void GenerateSvgForLineCharts() { diff --git a/src/EPPlus.Export.ImageRenderer.Test/TextRenderTests.cs b/src/EPPlus.Export.ImageRenderer.Test/TextRenderTests.cs index 622e3974d..d21ae4cad 100644 --- a/src/EPPlus.Export.ImageRenderer.Test/TextRenderTests.cs +++ b/src/EPPlus.Export.ImageRenderer.Test/TextRenderTests.cs @@ -221,7 +221,7 @@ public void VerifyTextRunBounds() cube.SetPixelWidth(5000); var svgShape = new SvgShape(cube); - SvgTextBodyItem tbItem = svgShape.TextBox.TextBody; + SvgTextBodyItem tbItem = svgShape.TextBox; var txtRun1Bounds = tbItem.Paragraphs[0].Runs[0].Bounds; var pxWidth = txtRun1Bounds.Width.PointToPixel(); diff --git a/src/EPPlus.Export.ImageRenderer/RenderItems/Shared/ParagraphItem.cs b/src/EPPlus.Export.ImageRenderer/RenderItems/Shared/ParagraphItem.cs index d65ff42bf..04ccbb0a0 100644 --- a/src/EPPlus.Export.ImageRenderer/RenderItems/Shared/ParagraphItem.cs +++ b/src/EPPlus.Export.ImageRenderer/RenderItems/Shared/ParagraphItem.cs @@ -1,4 +1,6 @@ using EPPlus.Fonts.OpenType; +using EPPlus.Fonts.OpenType.Integration; +using EPPlus.Fonts.OpenType.TextShaping; using EPPlus.Fonts.OpenType.TrueTypeMeasurer.DataHolders; using EPPlus.Fonts.OpenType.Utils; using EPPlus.Graphics; @@ -15,7 +17,6 @@ using System.Linq; using System.Text; using System.Text.RegularExpressions; -using static System.Net.Mime.MediaTypeNames; using EPPlusColorConverter = OfficeOpenXml.Utils.TypeConversion.ColorConverter; namespace EPPlus.Export.ImageRenderer.RenderItems.Shared @@ -35,6 +36,7 @@ internal abstract class ParagraphItem : RenderItem protected List _textRunDisplayText = new List(); TextFragmentCollection _textFragments; + List _newTextFragments; internal protected MeasurementFont _paragraphFont; internal TextBodyItem ParentTextBody { get; set; } internal double ParagraphLineSpacing { get; private set; } @@ -92,8 +94,23 @@ public ParagraphItem(TextBodyItem textBody, DrawingBase renderer, BoundingBox pa if (ParentTextBody.AutoSize == false) { - Bounds.Left = GetAlignmentHorizontal(HorizontalAlignment); - Bounds.Width = parent.Width - _rightMargin - _leftMargin; + //Bounds.Width = ParentTextBody.Width; + //var globBounds = Bounds.GetGlobalBoundingbox(); + //Bounds.Width = parent.Width; + //Bounds.Left = GetAlignmentHorizontal(HorizontalAlignment); + //Bounds.Width = parent.Width - _rightMargin - _leftMargin; + Bounds.Width = parent.Width; + if (HorizontalAlignment != eTextAlignment.Center) + { + Bounds.Left = GetAlignmentHorizontal(HorizontalAlignment); + } + else + { + //Center is a bit strange the bounds really are the same as left or right aligned + //It doesn't truly matter as only left min and right max play a role + Bounds.Left = GetAlignmentHorizontal(eTextAlignment.Left); + } + Bounds.Width = parent.Width - _rightMargin - _leftMargin; } //---Get measurer--- @@ -178,17 +195,27 @@ void GenerateTextFragments(ExcelDrawingTextRunCollection runs) { List runContents = new List(); List fontSizes = new List(); + List fonts = new List(); for (int i = 0; i < runs.Count(); i++) { var txtRun = runs[i]; var runFont = txtRun.GetMeasurementFont(); + fonts.Add(runFont); runContents.Add(txtRun.Text); fontSizes.Add(runFont.Size); } _textFragments = new TextFragmentCollection(runContents, fontSizes); + + _newTextFragments = new List(); + + for (int i = 0; i < runContents.Count(); i++) + { + var currentFrag = new TextFragment() { Text = runContents[i], Font = fonts[i] }; + _newTextFragments.Add(currentFrag); + } } private void AddLinesAndTextRuns(ExcelDrawingParagraph p, string textIfEmpty) @@ -218,6 +245,13 @@ private void AddLinesAndTextRuns(ExcelDrawingParagraph p, string textIfEmpty) { double prevWidth = 0; + if (HorizontalAlignment == eTextAlignment.Center) + { + //Center the line within context + prevWidth = (Bounds.Width - line.Width)/2; + } + + if (lineSpacingIsExact == false) { runLineSpacing += line.LargestAscent + lastDescent; @@ -231,24 +265,49 @@ private void AddLinesAndTextRuns(ExcelDrawingParagraph p, string textIfEmpty) greatestWidth = line.Width; } - foreach (var rtFragment in line.RtFragments) + if(line.RtFragments.Count == 0 && line.LineFragments.Count > 0) { - var displayText = line.GetFragmentText(rtFragment); - - if(p.TextRuns.Count == 0 && string.IsNullOrEmpty(textIfEmpty) == false) + foreach (var lineFragment in line.LineFragments) { - AddText(displayText, p.DefaultRunProperties); + var displayText = line.GetLineFragmentText(lineFragment); + + if (p.TextRuns.Count == 0 && string.IsNullOrEmpty(textIfEmpty) == false) + { + AddText(displayText, p.DefaultRunProperties); + } + else + { + AddRenderItemTextRun(p.TextRuns[lineFragment.RtFragIdx], displayText, prevWidth); + } + + TextRunItem runItem = Runs.Last(); + runItem.YPosition = runLineSpacing; + + runItem.Bounds.Width = lineFragment.Width; + prevWidth += lineFragment.Width; } - else + } + else + { + foreach (var rtFragment in line.RtFragments) { - AddRenderItemTextRun(p.TextRuns[rtFragment.Fragidx], displayText, prevWidth); + var displayText = line.GetFragmentText(rtFragment); + + if (p.TextRuns.Count == 0 && string.IsNullOrEmpty(textIfEmpty) == false) + { + AddText(displayText, p.DefaultRunProperties); + } + else + { + AddRenderItemTextRun(p.TextRuns[rtFragment.Fragidx], displayText, prevWidth); + } + + TextRunItem runItem = Runs.Last(); + runItem.YPosition = runLineSpacing; + + runItem.Bounds.Width = rtFragment.Width; + prevWidth += rtFragment.Width; } - - TextRunItem runItem = Runs.Last(); - runItem.YPosition = runLineSpacing; - - runItem.Bounds.Width = rtFragment.Width; - prevWidth += rtFragment.Width; } lastDescent = line.LargestDescent; @@ -260,17 +319,24 @@ private void AddLinesAndTextRuns(ExcelDrawingParagraph p, string textIfEmpty) List WrapToSimpleTextLines(ExcelDrawingParagraph p, TextFragmentCollection fragments) { var ttMeasurer = (FontMeasurerTrueType)_measurer; - List fonts = new List(); - - for (int i = 0; i < p.TextRuns.Count(); i++) - { - var txtRun = p.TextRuns[i]; - var runFont = txtRun.GetMeasurementFont(); - fonts.Add(runFont); - } + ttMeasurer.SetFont(_newTextFragments[0].Font); var maxWidthPoints = Math.Round(ParentTextBody.MaxWidth, 0, MidpointRounding.AwayFromZero); - return ttMeasurer.WrapMultipleTextFragmentsToTextLines(fragments, fonts, maxWidthPoints); + return ttMeasurer.WrapMultipleTextFragmentsToTextLines_New(_newTextFragments, maxWidthPoints); + //List fonts = new List(); + + //for (int i = 0; i < p.TextRuns.Count(); i++) + //{ + // var txtRun = p.TextRuns[i]; + // var runFont = txtRun.GetMeasurementFont(); + // fonts.Add(runFont); + //} + //var layout = TextData.GetTextLayoutEngine(fonts[0]); + //var shaper = new TextShaper( fonts[0]); + //var layout = new TextLayoutEngine(shaper); + + //var wrappedLines = layout.WrapRichTextLines(fragments, maxSizePoints); + //return ttMeasurer.WrapMultipleTextFragmentsToTextLines(fragments, fonts, maxWidthPoints); } internal double GetAlignmentHorizontal(eTextAlignment txAlignment) diff --git a/src/EPPlus.Export.ImageRenderer/RenderItems/Shared/TextBodyItem.cs b/src/EPPlus.Export.ImageRenderer/RenderItems/Shared/TextBodyItem.cs index b49f168fd..9ce4f69e1 100644 --- a/src/EPPlus.Export.ImageRenderer/RenderItems/Shared/TextBodyItem.cs +++ b/src/EPPlus.Export.ImageRenderer/RenderItems/Shared/TextBodyItem.cs @@ -71,18 +71,22 @@ public void ImportParagraph(ExcelDrawingParagraph item, double startingY, string paragraph.Bounds.Name = $"Container{Paragraphs.Count}"; paragraph.Bounds.Top = startingY; _text = text; - if(Paragraphs.Count == 0) - { - Bounds.Height = paragraph.Bounds.Height; - } - else - { - Bounds.Height += paragraph.Bounds.Height; - } - if(Bounds.Width < paragraph.Bounds.Width || (Bounds.Width == MaxWidth && Paragraphs.Count==0)) + if(AutoSize) { - Bounds.Width = paragraph.Bounds.Width; + if (Paragraphs.Count == 0) + { + Bounds.Height = paragraph.Bounds.Height; + } + else + { + Bounds.Height += paragraph.Bounds.Height; + } + + if (Bounds.Width < paragraph.Bounds.Width || (Bounds.Width == MaxWidth && Paragraphs.Count == 0)) + { + Bounds.Width = paragraph.Bounds.Width; + } } Paragraphs.Add(paragraph); SetHorizontalAlignmentPosition(); diff --git a/src/EPPlus.Export.ImageRenderer/RenderItems/SvgItem/SvgParagraphItem.cs b/src/EPPlus.Export.ImageRenderer/RenderItems/SvgItem/SvgParagraphItem.cs index 8f4d3e9cd..acb10427e 100644 --- a/src/EPPlus.Export.ImageRenderer/RenderItems/SvgItem/SvgParagraphItem.cs +++ b/src/EPPlus.Export.ImageRenderer/RenderItems/SvgItem/SvgParagraphItem.cs @@ -51,7 +51,7 @@ public override void Render(StringBuilder sb) { var fontSize = _paragraphFont.Size.PointToPixel().ToString(CultureInfo.InvariantCulture); - sb.AppendLine($""); + sb.AppendLine($""); sb.AppendLine("paragraph "); diff --git a/src/EPPlus.Export.ImageRenderer/Svg/Chart/SvgChartAxis.cs b/src/EPPlus.Export.ImageRenderer/Svg/Chart/SvgChartAxis.cs index cedc87b09..9e22912a6 100644 --- a/src/EPPlus.Export.ImageRenderer/Svg/Chart/SvgChartAxis.cs +++ b/src/EPPlus.Export.ImageRenderer/Svg/Chart/SvgChartAxis.cs @@ -334,7 +334,7 @@ private double GetAxisItemLeft(int i, OfficeOpenXml.Interfaces.Drawing.Text.Text if (Axis.AxisType == eAxisType.Cat) { var majorWidth = Rectangle.Width / AxisValues.Count; - return Rectangle.Left + majorWidth * i + (majorWidth / 2) - m.Width.PointToPixel() / 2; + return Rectangle.Left + majorWidth * i; } else { diff --git a/src/EPPlus.Export.ImageRenderer/Svg/SvgShape.cs b/src/EPPlus.Export.ImageRenderer/Svg/SvgShape.cs index fb4bfc8b9..f58b603da 100644 --- a/src/EPPlus.Export.ImageRenderer/Svg/SvgShape.cs +++ b/src/EPPlus.Export.ImageRenderer/Svg/SvgShape.cs @@ -23,6 +23,7 @@ Date Author Change using OfficeOpenXml.Drawing; using OfficeOpenXml.Drawing.Theme; using OfficeOpenXml.FormulaParsing.Excel.Functions.Logical; +using OfficeOpenXml.FormulaParsing.Excel.Functions.Text; using OfficeOpenXml.Style; using System; using System.Collections.Generic; @@ -43,7 +44,7 @@ internal class SvgShape : DrawingShape /// /// Textbox from memory /// - public SvgTextBox TextBox { get; internal set; } + public SvgTextBodyItem TextBox { get; internal set; } public SvgShape(ExcelShape shape) : base(shape) { @@ -108,9 +109,8 @@ public SvgShape(ExcelShape shape) : base(shape) InsetTextBox = null; } - TextBox = CreateTextBodyItem(); - TextBox.Rectangle.FillOpacity = 0.3; - TextBox.ImportTextBody(_shape.TextBody); + InsetTextBox.FillOpacity = 0.3d; + TextBox = CreateTextBodyItem(_shape.TextBody); TextBox.AppendRenderItems(RenderItems); } } @@ -220,8 +220,9 @@ public void Render(StringBuilder sb) //Write defs used for gradient colors var writer = new SvgDrawingWriter(this); writer.WriteSvgDefs(sb, RenderItems); - + //SvgGroupItem gItemTest = null; + //RenderDebugTextBox(sb); foreach(var item in RenderItems) { item.Render(sb); @@ -234,7 +235,7 @@ public void Render(StringBuilder sb) // gItemTest.RenderEndGroup(sb); //} } - //if (!string.IsNullOrEmpty(_shape.Text)) + //if (!string.IsNullOrEmpty(_shape.Text))t //{ // //RenderText(sb); //} @@ -246,7 +247,7 @@ public void Render(StringBuilder sb) sb.AppendLine(""); } - SvgTextBox CreateTextBodyItem() + SvgTextBodyItem CreateTextBodyItem(ExcelTextBody bodyOrig) { if (InsetTextBox == null) { @@ -256,9 +257,22 @@ SvgTextBox CreateTextBodyItem() InsetTextBox.Bounds.Top = y.PixelToPoint(); InsetTextBox.Width = width.PixelToPoint(); InsetTextBox.Height = height.PixelToPoint(); - InsetTextBox.Bounds.Parent = TextBox.Parent; //TODO:Check that textBody is correct. + //InsetTextBox.Bounds.Parent = TextBox.Parent; //TODO:Check that textBody is correct. } - var txtBodyItem = new SvgTextBox(this, Bounds, InsetTextBox.Bounds); + + double l, r, t, b; + bodyOrig.GetInsetsOrDefaults(out l, out t, out r, out b); + + BoundingBox MarginsBB = new BoundingBox(InsetTextBox.Width - r, InsetTextBox.Height - b); + MarginsBB.Parent = InsetTextBox.Bounds; + MarginsBB.Left = l; + MarginsBB.Top = t; + + var txtBodyItem = new SvgTextBodyItem(this, MarginsBB, 0, 0, MarginsBB.Width, MarginsBB.Height); + + txtBodyItem.ImportTextBody(bodyOrig); + + //txtBodyItem.Width = InsetTextBox.Width; return txtBodyItem; } @@ -274,7 +288,7 @@ private void RenderDebugTextBox(StringBuilder sb) InsetTextBox.FillColor = "green"; InsetTextBox.Render(sb); - InsetTextBox.GetBounds(out double l, out double t, out double r, out double b); + //InsetTextBox.GetBounds(out double l, out double t, out double r, out double b); //var area = textBody.Bounds; diff --git a/src/EPPlus.Fonts.OpenType.Tests/Integration/MeasurerComparisonTests.cs b/src/EPPlus.Fonts.OpenType.Tests/Integration/MeasurerComparisonTests.cs index 70a9eca13..8ef519ce9 100644 --- a/src/EPPlus.Fonts.OpenType.Tests/Integration/MeasurerComparisonTests.cs +++ b/src/EPPlus.Fonts.OpenType.Tests/Integration/MeasurerComparisonTests.cs @@ -185,7 +185,7 @@ public void Compare_GetBaseLine_ShouldMatch() // Act var oldBaseline = oldMeasurer.GetBaseLine(); - var newBaseline = shaper.GetBaseLineInPoints(11.0); + var newBaseline = shaper.GetAscentInPoints(11.0); // Assert Debug.WriteLine($"Old Baseline: {oldBaseline}, New Baseline: {newBaseline}"); diff --git a/src/EPPlus.Fonts.OpenType.Tests/Integration/TextLayoutEngineTests.cs b/src/EPPlus.Fonts.OpenType.Tests/Integration/TextLayoutEngineTests.cs index 63dbfce6b..5a2de5fb5 100644 --- a/src/EPPlus.Fonts.OpenType.Tests/Integration/TextLayoutEngineTests.cs +++ b/src/EPPlus.Fonts.OpenType.Tests/Integration/TextLayoutEngineTests.cs @@ -485,6 +485,25 @@ public void EnsureLineFragmentsAreMeasuredCorrectlyWhenWrapping() Assert.AreEqual(12.55224609375d, wrappedLines[0].LineFragments[2].Width); + List smallestTextFragments = new List(); + + //Ensure each linefragment can get correct text + foreach(var line in wrappedLines) + { + foreach(var lf in line.LineFragments) + { + var text = line.GetLineFragmentText(lf); + smallestTextFragments.Add(text); + } + } + + Assert.AreEqual(6, smallestTextFragments.Count); + Assert.AreEqual("TextBox2", smallestTextFragments[0]); + Assert.AreEqual("ra underline", smallestTextFragments[1]); + Assert.AreEqual("La", smallestTextFragments[2]); + Assert.AreEqual("Strike", smallestTextFragments[3]); + Assert.AreEqual("Goudy size", smallestTextFragments[4]); + Assert.AreEqual("16", smallestTextFragments[5]); } [TestMethod] diff --git a/src/EPPlus.Fonts.OpenType/Integration/LineFragment.cs b/src/EPPlus.Fonts.OpenType/Integration/LineFragment.cs index f36eac26e..3dbeeb5ae 100644 --- a/src/EPPlus.Fonts.OpenType/Integration/LineFragment.cs +++ b/src/EPPlus.Fonts.OpenType/Integration/LineFragment.cs @@ -14,10 +14,15 @@ public class LineFragment public double Width { get; set; } public int RtFragIdx { get; set; } - internal LineFragment(int rtFragmentIdx, int idxWithinLine) + //public double AscentInPoints { get; private set; } + //public double DescentInPoints { get; private set; } + + internal LineFragment(int rtFragmentIdx, int idxWithinLine/*, double ascentInPoints, double descentInPoints*/) { RtFragIdx = rtFragmentIdx; StartIdx = idxWithinLine; + //AscentInPoints = ascentInPoints; + //DescentInPoints = descentInPoints; } } } diff --git a/src/EPPlus.Fonts.OpenType/Integration/TextFragment.cs b/src/EPPlus.Fonts.OpenType/Integration/TextFragment.cs index 3ac45fba4..8031fccae 100644 --- a/src/EPPlus.Fonts.OpenType/Integration/TextFragment.cs +++ b/src/EPPlus.Fonts.OpenType/Integration/TextFragment.cs @@ -22,5 +22,8 @@ public class TextFragment public string Text { get; set; } public MeasurementFont Font { get; set; } public ShapingOptions Options { get; set; } + + public double AscentPoints { get; set; } + public double DescentPoints { get; set; } } } diff --git a/src/EPPlus.Fonts.OpenType/Integration/TextLayoutEngine.RichText.cs b/src/EPPlus.Fonts.OpenType/Integration/TextLayoutEngine.RichText.cs index c4aad63d2..84d82246c 100644 --- a/src/EPPlus.Fonts.OpenType/Integration/TextLayoutEngine.RichText.cs +++ b/src/EPPlus.Fonts.OpenType/Integration/TextLayoutEngine.RichText.cs @@ -57,6 +57,7 @@ public List WrapRichText( FinalizeCurrentLine(lineBuilder, state.CurrentLineWidth, state.WordStart); state.EndCurrentTextLine(); + if (_lineListBuffer.Count == 0) { _lineListBuffer.Add(string.Empty); @@ -93,6 +94,23 @@ public List WrapRichTextLines( state.CurrentTextLine.Text = lineBuilder.ToString(); state.EndCurrentTextLine(); + //Calculate ascent and descent so later application can handle line-spacing + //This could be optimized by doing it during ProcessFragment but that is way bulkier/unclear + foreach (var line in state.Lines) + { + double largestAscent = 0; + double largestDescent = 0; + foreach(var lineFragment in line.LineFragments) + { + var frag = fragments[lineFragment.RtFragIdx]; + if (frag == null) continue; + largestAscent = Math.Max(frag.AscentPoints, largestAscent); + largestDescent = Math.Max(frag.DescentPoints, largestDescent); + } + line.LargestAscent = largestAscent; + line.LargestDescent = largestDescent; + } + if (_lineListBuffer.Count == 0) { _lineListBuffer.Add(string.Empty); @@ -120,6 +138,10 @@ private void ProcessFragment( Array.Clear(charWidths, 0, len); FillCharWidths(shaped.Glyphs, scale, len, charWidths); + //Store for after everything is done + fragment.AscentPoints = shaper.GetAscentInPoints(fragment.Font.Size); + fragment.DescentPoints = shaper.GetDescentInPoints(fragment.Font.Size); + state.LineFrag = new LineFragment(state.CurrentFragmentIdx, lineBuilder.Length); state.LineFrag.StartIdx = lineBuilder.Length; state.LineFrag.RtFragIdx = state.CurrentFragmentIdx; @@ -202,7 +224,7 @@ private void HandleLineBreak(StringBuilder lineBuilder, WrapStateRichText state) } state.CurrentTextLine.Width = state.CurrentLineWidth; - state.EndCurrentTextLineAndIntializeNext(state.CurrentFragmentIdx, 0); + state.EndCurrentTextLineAndIntializeNext(0); lineBuilder.Length = 0; } @@ -267,7 +289,7 @@ private void WrapCurrentLine(StringBuilder lineBuilder, WrapStateRichText state, } } - state.EndCurrentTextLineAndIntializeNext(state.CurrentFragmentIdx, lineBuilder.Length); + state.EndCurrentTextLineAndIntializeNext(lineBuilder.Length); state.CurrentWordWidth = state.CurrentLineWidth; state.WordStart = -1; diff --git a/src/EPPlus.Fonts.OpenType/Integration/TextLayoutEngine.cs b/src/EPPlus.Fonts.OpenType/Integration/TextLayoutEngine.cs index cc48abd73..7fe0a2ba5 100644 --- a/src/EPPlus.Fonts.OpenType/Integration/TextLayoutEngine.cs +++ b/src/EPPlus.Fonts.OpenType/Integration/TextLayoutEngine.cs @@ -70,7 +70,7 @@ public double GetLineHeightInPoints(double fontSize) public double GetBaseLineInPoints(double fontSize) { - return _shaper.GetBaseLineInPoints(fontSize); + return _shaper.GetAscentInPoints(fontSize); } public double GetDescentInPoints(double fontSize) diff --git a/src/EPPlus.Fonts.OpenType/Integration/WrapStateRichText.cs b/src/EPPlus.Fonts.OpenType/Integration/WrapStateRichText.cs index 0dcfd5cbd..c80acfbb4 100644 --- a/src/EPPlus.Fonts.OpenType/Integration/WrapStateRichText.cs +++ b/src/EPPlus.Fonts.OpenType/Integration/WrapStateRichText.cs @@ -24,20 +24,21 @@ internal void EndCurrentTextLine() Lines.Add(CurrentTextLine); } - internal void EndCurrentTextLineAndIntializeNext(int fragIdx, int startIdxOfNewFragment) + internal void EndCurrentTextLineAndIntializeNext(int startIdxOfNewFragment) { var nextLine = new TextLineSimple(); if (_fragmentsForNextLine == null) { EndCurrentTextLine(); - LineFrag = new LineFragment(fragIdx, startIdxOfNewFragment); + LineFrag = new LineFragment(CurrentFragmentIdx, startIdxOfNewFragment); } else { //_fragmentsForNextLine.Add(LineFrag); nextLine.LineFragments = _fragmentsForNextLine; + //LineFrag.StartIdx = ; Lines.Add(CurrentTextLine); _fragmentsForNextLine = null; } @@ -109,10 +110,15 @@ internal void AdjustLineFragmentsForNextLine() CurrentTextLine.LineFragments.RemoveAt(i); } - if(_rtIdxAtWordStart != CurrentFragmentIdx) + if (_rtIdxAtWordStart != CurrentFragmentIdx) { //We also insert the fragment we've split out _fragmentsForNextLine.Insert(0, resultingFragment); + //The current fragment's startidx is affected by the split if it is not the first in new line + if (LineFrag.StartIdx != 0) + { + LineFrag.StartIdx -= WordStart + 1; + } } else { diff --git a/src/EPPlus.Fonts.OpenType/TextShaping/TextShaper.cs b/src/EPPlus.Fonts.OpenType/TextShaping/TextShaper.cs index 48cece6c6..b67fb1dc5 100644 --- a/src/EPPlus.Fonts.OpenType/TextShaping/TextShaper.cs +++ b/src/EPPlus.Fonts.OpenType/TextShaping/TextShaper.cs @@ -624,7 +624,7 @@ public double GetFontHeightInPoints(double fontSize) /// /// The font size, in points, for which to calculate the baseline position. Must be a positive value. /// The distance, in points, from the top of the font's bounding box to the baseline for the given font size. - public double GetBaseLineInPoints(double fontSize) + public double GetAscentInPoints(double fontSize) { // Distance from top of box to baseline var ascent = _font.Os2Table.UseTypoMetrics diff --git a/src/EPPlus.Fonts.OpenType/TrueTypeMeasurer/DataHolders/TextLineSimple.cs b/src/EPPlus.Fonts.OpenType/TrueTypeMeasurer/DataHolders/TextLineSimple.cs index 08918654b..04d20b057 100644 --- a/src/EPPlus.Fonts.OpenType/TrueTypeMeasurer/DataHolders/TextLineSimple.cs +++ b/src/EPPlus.Fonts.OpenType/TrueTypeMeasurer/DataHolders/TextLineSimple.cs @@ -74,6 +74,32 @@ public string GetFragmentText(RichTextFragmentSimple rtFragment) } } + public string GetLineFragmentText(LineFragment rtFragment) + { + if (LineFragments.Contains(rtFragment) == false) + { + throw new InvalidOperationException($"GetFragmentText failed. Cannot retrieve {rtFragment} since it is not part of this textLine: {this}"); + } + + if (string.IsNullOrEmpty(Text)) + { + return Text; + } + + var startIdx = rtFragment.StartIdx; + + var idxInLst = LineFragments.FindIndex(x => x == rtFragment); + if (idxInLst == LineFragments.Count - 1) + { + return Text.Substring(startIdx, Text.Length - startIdx); + } + else + { + var endIdx = LineFragments[idxInLst + 1].StartIdx; + return Text.Substring(startIdx, endIdx - startIdx); + } + } + internal LineFragment SplitAndGetLeftoverLineFragment(ref LineFragment origLf, double widthAtSplit) { //If we are splitting a fragment its position in the new line should be 0 diff --git a/src/EPPlus.Fonts.OpenType/TrueTypeMeasurer/FontMeasurerTrueType.cs b/src/EPPlus.Fonts.OpenType/TrueTypeMeasurer/FontMeasurerTrueType.cs index d28557930..d63f5acc1 100644 --- a/src/EPPlus.Fonts.OpenType/TrueTypeMeasurer/FontMeasurerTrueType.cs +++ b/src/EPPlus.Fonts.OpenType/TrueTypeMeasurer/FontMeasurerTrueType.cs @@ -324,5 +324,31 @@ public List WrapMultipleTextFragmentsToTextLines(TextFragmentCol //Wrap the fragments return TextData.WrapMultipleTextFragmentsToTextLines(paragraph, maxWidthPoints); } + + public List WrapMultipleTextFragmentsToTextLines_New(List frags, double maxWidthPoints) + { + var layout = TextData.GetTextLayoutEngine(frags[0].Font); + var lines = layout.WrapRichTextLines(frags, maxWidthPoints); + return lines; + } + + public List WrapMultipleTextFragmentsToTextLines_New(List rtTextFrags, List fonts, double maxWidthPoints) + { + var layout = TextData.GetTextLayoutEngine(fonts[0]); + + var newFrags = new List(); + + for (int i = 0; i < rtTextFrags.Count(); i++) + { + var currentFrag = new Integration.TextFragment() { Text = rtTextFrags[i], Font = fonts[i] }; + newFrags.Add(currentFrag); + } + + return layout.WrapRichTextLines(newFrags, maxWidthPoints); + //TextParagraph paragraph = new TextParagraph(fragments, fonts); + + ////Wrap the fragments + //return TextData.WrapMultipleTextFragmentsToTextLines(paragraph, maxWidthPoints); + } } } diff --git a/src/EPPlus.Fonts.OpenType/TrueTypeMeasurer/TextData.cs b/src/EPPlus.Fonts.OpenType/TrueTypeMeasurer/TextData.cs index 64d67c3fc..795000516 100644 --- a/src/EPPlus.Fonts.OpenType/TrueTypeMeasurer/TextData.cs +++ b/src/EPPlus.Fonts.OpenType/TrueTypeMeasurer/TextData.cs @@ -10,9 +10,11 @@ Date Author Change ************************************************************************************************* 10/07/2025 EPPlus Software AB EPPlus.Fonts.OpenType 1.0 *************************************************************************************************/ +using EPPlus.Fonts.OpenType.Integration; using EPPlus.Fonts.OpenType.Tables.Cmap; using EPPlus.Fonts.OpenType.Tables.Cmap.Mappings; using EPPlus.Fonts.OpenType.Tables.Kern; +using EPPlus.Fonts.OpenType.TextShaping; using EPPlus.Fonts.OpenType.TrueTypeMeasurer; using EPPlus.Fonts.OpenType.TrueTypeMeasurer.DataHolders; using OfficeOpenXml.Interfaces.Drawing.Text; @@ -20,6 +22,7 @@ Date Author Change using System.Collections.Generic; using System.Linq; using System.Xml; +using static System.Net.Mime.MediaTypeNames; namespace EPPlus.Fonts.OpenType { @@ -40,6 +43,33 @@ internal static OpenTypeFont GetFontData(string fontName, FontSubFamily subFamil return OpenTypeFonts.GetFontData(FontDirectories, fontName, subFamily, SearchSystemDirectories); } + internal static TextLayoutEngine GetTextLayoutEngine(MeasurementFont mFont) + { + var startFont = TextData.GetFontData(mFont.FontFamily, GetFontSubType(mFont.Style)); + var shaper = new TextShaper(startFont); + var layout = new TextLayoutEngine(shaper); + return layout; + } + + + private static FontSubFamily GetFontSubType(MeasurementFontStyles Style) + { + if ((Style & (MeasurementFontStyles.Bold | MeasurementFontStyles.Italic)) == (MeasurementFontStyles.Bold | MeasurementFontStyles.Italic)) + { + return FontSubFamily.BoldItalic; + } + else if ((Style & MeasurementFontStyles.Bold) == MeasurementFontStyles.Bold) + { + return FontSubFamily.Bold; + } + else if ((Style & MeasurementFontStyles.Italic) == MeasurementFontStyles.Italic) + { + return FontSubFamily.Italic; + } + + return FontSubFamily.Regular; + } + /// /// Get difference between winAscent and typoAscent in points /// @@ -1282,6 +1312,8 @@ internal static List WrapMultipleTextFragmentsToTextLines(TextPa var charInfoAtBreak = fragmentCollection.CharLookup[prevLineEndIndex]; var fragIdxAtBreak = charInfoAtBreak.Fragment; + var spaceWidthAtBreak = CalcGlyphWidth(paragraph.GlyphMappings[fragmentIdx], ' ', currentFont, ref lastGlyphIndex, ref applyKerning); + //Largest font might be wrong here as we may linebreak at a different font than current font if we break at the start of a word //and not current i largestFontCurrentLine = paragraph.FontIndexDict[currentLineRtFragments[0].Fragidx]; diff --git a/src/EPPlus.Interfaces/Drawing/Text/ITextShaper.cs b/src/EPPlus.Interfaces/Drawing/Text/ITextShaper.cs index 1b36047bb..803c7ec1a 100644 --- a/src/EPPlus.Interfaces/Drawing/Text/ITextShaper.cs +++ b/src/EPPlus.Interfaces/Drawing/Text/ITextShaper.cs @@ -59,9 +59,9 @@ public interface ITextShaper double GetFontHeightInPoints(double fontSize); /// - /// Gets baseline distance from top of container in points. + /// Gets Ascent (top of container to the baseline) in points /// - double GetBaseLineInPoints(double fontSize); + double GetAscentInPoints(double fontSize); /// /// Gets descent distance (below baseline) in points. From f16548720b975a8771e0ac04ed88a75cdf54cd8e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ossian=20Edstr=C3=B6m?= Date: Thu, 26 Feb 2026 14:48:37 +0100 Subject: [PATCH 064/151] Several improvements to centering --- .../RenderItems/Shared/ParagraphItem.cs | 53 +++++++++---- .../Svg/SvgShape.cs | 21 +++-- .../Integration/TextLayoutEngineTests.cs | 76 +++++++++++++++++++ .../Integration/LineFragment.cs | 2 + .../Integration/TextLayoutEngine.RichText.cs | 5 ++ .../Integration/WrapStateRichText.cs | 2 + .../DataHolders/TextLineSimple.cs | 27 +++++++ .../Drawing/Style/Text/ExcelTextBody.cs | 2 +- 8 files changed, 165 insertions(+), 23 deletions(-) diff --git a/src/EPPlus.Export.ImageRenderer/RenderItems/Shared/ParagraphItem.cs b/src/EPPlus.Export.ImageRenderer/RenderItems/Shared/ParagraphItem.cs index 04ccbb0a0..f2c221853 100644 --- a/src/EPPlus.Export.ImageRenderer/RenderItems/Shared/ParagraphItem.cs +++ b/src/EPPlus.Export.ImageRenderer/RenderItems/Shared/ParagraphItem.cs @@ -94,23 +94,34 @@ public ParagraphItem(TextBodyItem textBody, DrawingBase renderer, BoundingBox pa if (ParentTextBody.AutoSize == false) { + Bounds.Left = 0; + Bounds.Width = ParentTextBody.MaxWidth; + + //Left is equal to left Paragraph margin + //Textbody or Textbox are assumed to handle shape/chart margins + //Paragraph handles only indentations/margins that is applied ON TOP of those margins + //Paragraph left is the exact position where the text itself starts on the left + Bounds.Left = GetAlignmentHorizontal(HorizontalAlignment); + Bounds.Width = parent.Width - _rightMargin - _leftMargin; + //Bounds.Width = ParentTextBody.Width; //var globBounds = Bounds.GetGlobalBoundingbox(); //Bounds.Width = parent.Width; //Bounds.Left = GetAlignmentHorizontal(HorizontalAlignment); //Bounds.Width = parent.Width - _rightMargin - _leftMargin; - Bounds.Width = parent.Width; - if (HorizontalAlignment != eTextAlignment.Center) - { - Bounds.Left = GetAlignmentHorizontal(HorizontalAlignment); - } - else - { - //Center is a bit strange the bounds really are the same as left or right aligned - //It doesn't truly matter as only left min and right max play a role - Bounds.Left = GetAlignmentHorizontal(eTextAlignment.Left); - } - Bounds.Width = parent.Width - _rightMargin - _leftMargin; + //Bounds.Width = parent.Width; + //Bounds.Left = _leftMargin; + ////if (HorizontalAlignment != eTextAlignment.Center) + ////{ + //// Bounds.Left = GetAlignmentHorizontal(HorizontalAlignment); + ////} + ////else + ////{ + //// //Center is a bit strange the bounds really are the same as left or right aligned + //// //It doesn't truly matter as only left min and right max play a role + //// Bounds.Left = GetAlignmentHorizontal(eTextAlignment.Left); + ////} + //Bounds.Width = parent.Width - _rightMargin - _leftMargin; } //---Get measurer--- @@ -247,11 +258,17 @@ private void AddLinesAndTextRuns(ExcelDrawingParagraph p, string textIfEmpty) if (HorizontalAlignment == eTextAlignment.Center) { - //Center the line within context - prevWidth = (Bounds.Width - line.Width)/2; + //var ctrLineWidth = line.GetWidthWithoutTrailingSpaces(); + ////Should only be applied if it was wrapped + //ctrLineWidth -= line.lastFontSpaceWidth; + //if(line.Text == "StrikeGoudy size") + //{ + // ctrLineWidth = 202.5d; + //} + //Center the line within context we must use maxWidth since left depends on greatest width + prevWidth = - line.Width / 2; } - if (lineSpacingIsExact == false) { runLineSpacing += line.LargestAscent + lastDescent; @@ -314,6 +331,10 @@ private void AddLinesAndTextRuns(ExcelDrawingParagraph p, string textIfEmpty) } Bounds.Height = runLineSpacing + lastDescent; Bounds.Width = greatestWidth; + //if (HorizontalAlignment == eTextAlignment.Center) + //{ + // Bounds.Left = Bounds.Left - greatestWidth; + //} } List WrapToSimpleTextLines(ExcelDrawingParagraph p, TextFragmentCollection fragments) @@ -357,7 +378,7 @@ internal double GetAlignmentHorizontal(eTextAlignment txAlignment) break; } - return TextUtils.RoundToWhole(x); + return x; } /// diff --git a/src/EPPlus.Export.ImageRenderer/Svg/SvgShape.cs b/src/EPPlus.Export.ImageRenderer/Svg/SvgShape.cs index f58b603da..4f15393dd 100644 --- a/src/EPPlus.Export.ImageRenderer/Svg/SvgShape.cs +++ b/src/EPPlus.Export.ImageRenderer/Svg/SvgShape.cs @@ -41,6 +41,9 @@ internal class SvgShape : DrawingShape /// Calculated shape textbox /// SvgRenderRectItem InsetTextBox; + + SvgRenderRectItem MarginTextBox; + /// /// Textbox from memory /// @@ -222,7 +225,6 @@ public void Render(StringBuilder sb) writer.WriteSvgDefs(sb, RenderItems); //SvgGroupItem gItemTest = null; - //RenderDebugTextBox(sb); foreach(var item in RenderItems) { item.Render(sb); @@ -244,6 +246,7 @@ public void Render(StringBuilder sb) //{ // gItemTest.RenderEndGroup(sb); //} + RenderDebugTextBox(sb); sb.AppendLine(""); } @@ -263,12 +266,14 @@ SvgTextBodyItem CreateTextBodyItem(ExcelTextBody bodyOrig) double l, r, t, b; bodyOrig.GetInsetsOrDefaults(out l, out t, out r, out b); - BoundingBox MarginsBB = new BoundingBox(InsetTextBox.Width - r, InsetTextBox.Height - b); - MarginsBB.Parent = InsetTextBox.Bounds; - MarginsBB.Left = l; - MarginsBB.Top = t; + MarginTextBox = new SvgRenderRectItem(this, this.Bounds); + + MarginTextBox.Width = InsetTextBox.Width - r; + MarginTextBox.Height = InsetTextBox.Height - b; + MarginTextBox.Top = t + InsetTextBox.Top; + MarginTextBox.Left = l + InsetTextBox.Left; - var txtBodyItem = new SvgTextBodyItem(this, MarginsBB, 0, 0, MarginsBB.Width, MarginsBB.Height); + var txtBodyItem = new SvgTextBodyItem(this, MarginTextBox.Bounds, 0, 0, MarginTextBox.Width, MarginTextBox.Height); txtBodyItem.ImportTextBody(bodyOrig); @@ -285,9 +290,13 @@ SvgTextBodyItem CreateTextBodyItem(ExcelTextBody bodyOrig) private void RenderDebugTextBox(StringBuilder sb) { + InsetTextBox.FillOpacity = 0.3d; InsetTextBox.FillColor = "green"; InsetTextBox.Render(sb); + MarginTextBox.FillColor = "red"; + MarginTextBox.FillOpacity = 0.3; + MarginTextBox.Render(sb); //InsetTextBox.GetBounds(out double l, out double t, out double r, out double b); //var area = textBody.Bounds; diff --git a/src/EPPlus.Fonts.OpenType.Tests/Integration/TextLayoutEngineTests.cs b/src/EPPlus.Fonts.OpenType.Tests/Integration/TextLayoutEngineTests.cs index c3e786aec..6ee843109 100644 --- a/src/EPPlus.Fonts.OpenType.Tests/Integration/TextLayoutEngineTests.cs +++ b/src/EPPlus.Fonts.OpenType.Tests/Integration/TextLayoutEngineTests.cs @@ -410,6 +410,80 @@ public void WrapRichTextDifficultCase() Assert.AreEqual("16SvgSize 24", wrappedLines[4]); } + [TestMethod] + public void MeasureBigGoudy() + { + List lstOfRichText = new() { "strike", "Goudy Size"}; + + var regFont = new MeasurementFont() + { + FontFamily = "Aptos Narrow", + Size = 11, + Style = MeasurementFontStyles.Regular + }; + + + var goudyFont = new MeasurementFont() + { + FontFamily = "Goudy Stout", + Size = 16, + Style = MeasurementFontStyles.Regular + }; + + + List fonts = new() { regFont, goudyFont }; + + var startFont = TextData.GetFontData(regFont.FontFamily, GetFontSubType(regFont.Style)); + + var shaper = new TextShaper(startFont); + var layout = new TextLayoutEngine(shaper); + + var maxSizeInPoints = 225d; + + var fragments = new List(); + + for (int i = 0; i < lstOfRichText.Count(); i++) + { + var currentFrag = new TextFragment() { Text = lstOfRichText[i], Font = fonts[i] }; + fragments.Add(currentFrag); + } + + + var wrappedLines = layout.WrapRichTextLines(fragments, maxSizeInPoints); + + + var txtWidthsingle = shaper.MeasureTextInPixels("E", 16, 96, ShapingOptions.Full); + var txtWidth = shaper.MeasureTextInPixels("EEEEEEEEEE", 16, 96, ShapingOptions.Full); + var txtWidthAlt = shaper.MeasureTextInPixels("EEEEEEEEEE", 16, 96, ShapingOptions.Fast); + + var txtWidthLowSize = shaper.MeasureTextInPixels("E", 11, 96, ShapingOptions.Fast); + var txtWidthHighSize= shaper.MeasureTextInPixels("E", 72, 96, ShapingOptions.Fast); + var txtWidthHighSize10 = shaper.MeasureTextInPixels("EEEEEEEEEE", 72, 96, ShapingOptions.Fast); + + var pts16 = shaper.MeasureTextInPoints("E", 16); + var pts = shaper.MeasureTextInPoints("E", 72); + var pts2 = shaper.MeasureTextInPoints("E", 96); + + var txtWidthMaxSizeSingle = shaper.MeasureTextInPixels("E", 96, 72, ShapingOptions.Full); + var txtWidthMaxSizeSingleFast = shaper.MeasureTextInPixels("E", 96, 72, ShapingOptions.Fast); + + var txtWidthMaxSizeSingle96 = shaper.MeasureTextInPixels("E", 96, 96, ShapingOptions.Full); + var txtWidthMaxSizeSingleFast96 = shaper.MeasureTextInPixels("E", 96, 96, ShapingOptions.Fast); + + //Assert.AreEqual(16, txtWidthLowSize); + //Assert.AreEqual(97, txtWidthHighSize); + + //Assert.AreEqual(97, txtWidthHighSize); + //Assert.AreEqual(23, txtWidthsingle); + //Assert.AreEqual(249,txtWidth); + + //var wrappedLines = layout.WrapRichTextLines(fragments, maxSizeInPoints); + + ////E in goudy stout 16 should be equal to 22 px or 16.5 points + //Assert.AreEqual(16.5d, wrappedLines[0].LineFragments[0].Width); + //Assert.AreEqual(16.5d, wrappedLines[0].LineFragments[1].Width); + } + [TestMethod] public void EnsureLineFragmentsAreMeasuredCorrectlyWhenWrapping() { @@ -462,6 +536,8 @@ public void EnsureLineFragmentsAreMeasuredCorrectlyWhenWrapping() Assert.AreEqual(12.55224609375d, wrappedLines[0].LineFragments[2].Width); + Assert.AreEqual(201.99, wrappedLines[1].Width); + List smallestTextFragments = new List(); diff --git a/src/EPPlus.Fonts.OpenType/Integration/LineFragment.cs b/src/EPPlus.Fonts.OpenType/Integration/LineFragment.cs index 3dbeeb5ae..26dbc439b 100644 --- a/src/EPPlus.Fonts.OpenType/Integration/LineFragment.cs +++ b/src/EPPlus.Fonts.OpenType/Integration/LineFragment.cs @@ -14,6 +14,8 @@ public class LineFragment public double Width { get; set; } public int RtFragIdx { get; set; } + public double SpaceWidth { get; internal set; } + //public double AscentInPoints { get; private set; } //public double DescentInPoints { get; private set; } diff --git a/src/EPPlus.Fonts.OpenType/Integration/TextLayoutEngine.RichText.cs b/src/EPPlus.Fonts.OpenType/Integration/TextLayoutEngine.RichText.cs index b8987b505..80bf1bef6 100644 --- a/src/EPPlus.Fonts.OpenType/Integration/TextLayoutEngine.RichText.cs +++ b/src/EPPlus.Fonts.OpenType/Integration/TextLayoutEngine.RichText.cs @@ -133,6 +133,8 @@ private void ProcessFragment( var charWidths = GetCharWidthBuffer(len); + //options.ApplySubstitutions = false; + //options.ApplyPositioning = false; // ShapeLight applies only kerning (sufficient for line-breaking). // Full Shape() runs SingleAdjustment + Kerning + MarkToBase which // is ~250x slower and irrelevant for wrapping decisions. @@ -146,7 +148,10 @@ private void ProcessFragment( fragment.AscentPoints = shaper.GetAscentInPoints(fragment.Font.Size); fragment.DescentPoints = shaper.GetDescentInPoints(fragment.Font.Size); + var spaceWidth = GetCachedSpaceWidth(fragment.Font.Size, options); + state.LineFrag = new LineFragment(state.CurrentFragmentIdx, lineBuilder.Length); + state.LineFrag.SpaceWidth = spaceWidth; state.LineFrag.StartIdx = lineBuilder.Length; state.LineFrag.RtFragIdx = state.CurrentFragmentIdx; diff --git a/src/EPPlus.Fonts.OpenType/Integration/WrapStateRichText.cs b/src/EPPlus.Fonts.OpenType/Integration/WrapStateRichText.cs index c80acfbb4..b4f605fa5 100644 --- a/src/EPPlus.Fonts.OpenType/Integration/WrapStateRichText.cs +++ b/src/EPPlus.Fonts.OpenType/Integration/WrapStateRichText.cs @@ -31,7 +31,9 @@ internal void EndCurrentTextLineAndIntializeNext(int startIdxOfNewFragment) if (_fragmentsForNextLine == null) { EndCurrentTextLine(); + var spcWidthTemp = LineFrag.SpaceWidth; LineFrag = new LineFragment(CurrentFragmentIdx, startIdxOfNewFragment); + LineFrag.SpaceWidth = spcWidthTemp; } else { diff --git a/src/EPPlus.Fonts.OpenType/TrueTypeMeasurer/DataHolders/TextLineSimple.cs b/src/EPPlus.Fonts.OpenType/TrueTypeMeasurer/DataHolders/TextLineSimple.cs index 04d20b057..00e491251 100644 --- a/src/EPPlus.Fonts.OpenType/TrueTypeMeasurer/DataHolders/TextLineSimple.cs +++ b/src/EPPlus.Fonts.OpenType/TrueTypeMeasurer/DataHolders/TextLineSimple.cs @@ -30,6 +30,33 @@ public class TextLineSimple public double Width { get; internal set; } + public double lastFontSpaceWidth { get; internal set; } + + /// + /// In renderers like Excel the width of trailing spaces + /// MUST be resepected in some cases. + /// In others (e.g. Centering) the spaces width must be ignored + /// + public double GetWidthWithoutTrailingSpaces() + { + lastFontSpaceWidth = LineFragments.Last().SpaceWidth; + + var trailingSpaceCount = 0; + + for(int i = Text.Count()-1; i > 0; i--) + { + if(Text[i] != ' ') + { + break; + } + + trailingSpaceCount++; + } + + var widthWithoutTrail = Width - lastFontSpaceWidth * (trailingSpaceCount); + return widthWithoutTrail; + } + public TextLineSimple() { } diff --git a/src/EPPlus/Drawing/Style/Text/ExcelTextBody.cs b/src/EPPlus/Drawing/Style/Text/ExcelTextBody.cs index 25e4c7088..15472a600 100644 --- a/src/EPPlus/Drawing/Style/Text/ExcelTextBody.cs +++ b/src/EPPlus/Drawing/Style/Text/ExcelTextBody.cs @@ -431,7 +431,7 @@ internal void SetFromXml(XmlElement copyFromElement) } } - //Excel default values for Top/Bottom and Right/Left in EMU + //Excel default values for Top/Bottom and Right/Left translated to points //They are equivalent to 0.25cm and 0.13cm internal const double DefaultTopBot = 45720d / ExcelDrawing.EMU_PER_POINT; internal const double DefaultRightLeft = 91440d / ExcelDrawing.EMU_PER_POINT; From f454c5fc1e1ef47995631fadb6dbf859b942b6f1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ossian=20Edstr=C3=B6m?= Date: Thu, 26 Feb 2026 16:44:22 +0100 Subject: [PATCH 065/151] Solved space width centering issue --- .../RenderItems/Shared/ParagraphItem.cs | 6 +++--- .../Integration/TextLayoutEngineTests.cs | 5 +++-- .../Integration/TextLayoutEngine.RichText.cs | 15 +++++++++++++-- .../DataHolders/TextLineSimple.cs | 7 +++++++ 4 files changed, 26 insertions(+), 7 deletions(-) diff --git a/src/EPPlus.Export.ImageRenderer/RenderItems/Shared/ParagraphItem.cs b/src/EPPlus.Export.ImageRenderer/RenderItems/Shared/ParagraphItem.cs index f2c221853..8fd922292 100644 --- a/src/EPPlus.Export.ImageRenderer/RenderItems/Shared/ParagraphItem.cs +++ b/src/EPPlus.Export.ImageRenderer/RenderItems/Shared/ParagraphItem.cs @@ -258,15 +258,15 @@ private void AddLinesAndTextRuns(ExcelDrawingParagraph p, string textIfEmpty) if (HorizontalAlignment == eTextAlignment.Center) { - //var ctrLineWidth = line.GetWidthWithoutTrailingSpaces(); - ////Should only be applied if it was wrapped + var ctrLineWidth = line.GetWidthWithoutTrailingSpaces(); + //Should only be applied if it was wrapped //ctrLineWidth -= line.lastFontSpaceWidth; //if(line.Text == "StrikeGoudy size") //{ // ctrLineWidth = 202.5d; //} //Center the line within context we must use maxWidth since left depends on greatest width - prevWidth = - line.Width / 2; + prevWidth = - ctrLineWidth / 2; } if (lineSpacingIsExact == false) diff --git a/src/EPPlus.Fonts.OpenType.Tests/Integration/TextLayoutEngineTests.cs b/src/EPPlus.Fonts.OpenType.Tests/Integration/TextLayoutEngineTests.cs index 6ee843109..971d86a4e 100644 --- a/src/EPPlus.Fonts.OpenType.Tests/Integration/TextLayoutEngineTests.cs +++ b/src/EPPlus.Fonts.OpenType.Tests/Integration/TextLayoutEngineTests.cs @@ -413,7 +413,7 @@ public void WrapRichTextDifficultCase() [TestMethod] public void MeasureBigGoudy() { - List lstOfRichText = new() { "strike", "Goudy Size"}; + List lstOfRichText = new() { "TextBox2ra underlineLa Strike", "Goudysize16SvgSize24" }; var regFont = new MeasurementFont() { @@ -448,6 +448,8 @@ public void MeasureBigGoudy() fragments.Add(currentFrag); } + //var test = "TextBox2ra underlineLa StrikeGoudysize16SvgSize24"; + ////var secondTest = "TextBox2ra underlineLa StrikeGoudy".ToArray(); var wrappedLines = layout.WrapRichTextLines(fragments, maxSizeInPoints); @@ -534,7 +536,6 @@ public void EnsureLineFragmentsAreMeasuredCorrectlyWhenWrapping() var wrappedLines = layout.WrapRichTextLines(fragments, maxSizePoints); - Assert.AreEqual(12.55224609375d, wrappedLines[0].LineFragments[2].Width); Assert.AreEqual(201.99, wrappedLines[1].Width); diff --git a/src/EPPlus.Fonts.OpenType/Integration/TextLayoutEngine.RichText.cs b/src/EPPlus.Fonts.OpenType/Integration/TextLayoutEngine.RichText.cs index 80bf1bef6..10307509d 100644 --- a/src/EPPlus.Fonts.OpenType/Integration/TextLayoutEngine.RichText.cs +++ b/src/EPPlus.Fonts.OpenType/Integration/TextLayoutEngine.RichText.cs @@ -148,7 +148,9 @@ private void ProcessFragment( fragment.AscentPoints = shaper.GetAscentInPoints(fragment.Font.Size); fragment.DescentPoints = shaper.GetDescentInPoints(fragment.Font.Size); - var spaceWidth = GetCachedSpaceWidth(fragment.Font.Size, options); + var spaceWidth = shaper.Shape(" ", options).GetWidthInPoints(fragment.Font.Size,shaper.UnitsPerEm); + + //GetCachedSpaceWidth(fragment.Font.Size, options); state.LineFrag = new LineFragment(state.CurrentFragmentIdx, lineBuilder.Length); state.LineFrag.SpaceWidth = spaceWidth; @@ -258,7 +260,12 @@ private void WrapCurrentLine(StringBuilder lineBuilder, WrapStateRichText state, // Bounds check to prevent ArgumentOutOfRangeException if (state.WordStart >= 0 && state.WordStart < lineBuilder.Length) { - string line = lineBuilder.ToString(0, state.WordStart).TrimEnd(); + var lineStringWithTrail = lineBuilder.ToString(0, state.WordStart+1); + if (lineStringWithTrail[lineStringWithTrail.Length-1] == ' ') + { + state.CurrentTextLine.WasWrappedOnSpace = true; + } + string line = lineStringWithTrail.TrimEnd(); _lineListBuffer.Add(line); lineBuilder.Remove(0, state.WordStart + 1); @@ -298,6 +305,10 @@ private void WrapCurrentLine(StringBuilder lineBuilder, WrapStateRichText state, state.CurrentWordWidth = advanceWidth; state.CurrentLineWidth = advanceWidth; } + else + { + state.CurrentTextLine.WasWrappedOnSpace = true; + } } state.EndCurrentTextLineAndIntializeNext(lineBuilder.Length); diff --git a/src/EPPlus.Fonts.OpenType/TrueTypeMeasurer/DataHolders/TextLineSimple.cs b/src/EPPlus.Fonts.OpenType/TrueTypeMeasurer/DataHolders/TextLineSimple.cs index 00e491251..f2c498215 100644 --- a/src/EPPlus.Fonts.OpenType/TrueTypeMeasurer/DataHolders/TextLineSimple.cs +++ b/src/EPPlus.Fonts.OpenType/TrueTypeMeasurer/DataHolders/TextLineSimple.cs @@ -32,6 +32,8 @@ public class TextLineSimple public double lastFontSpaceWidth { get; internal set; } + internal bool WasWrappedOnSpace = false; + /// /// In renderers like Excel the width of trailing spaces /// MUST be resepected in some cases. @@ -53,6 +55,11 @@ public double GetWidthWithoutTrailingSpaces() trailingSpaceCount++; } + if(WasWrappedOnSpace) + { + trailingSpaceCount++; + } + var widthWithoutTrail = Width - lastFontSpaceWidth * (trailingSpaceCount); return widthWithoutTrail; } From f2b31969b546235b8121f309c6a11e769dfd6ec8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ossian=20Edstr=C3=B6m?= Date: Fri, 27 Feb 2026 10:43:35 +0100 Subject: [PATCH 066/151] Ensured consistent behaviour of spaces --- .../Integration/TextLayoutEngineTests.cs | 106 +++++++++++++++++- .../Integration/TextLayoutEngine.RichText.cs | 9 +- 2 files changed, 109 insertions(+), 6 deletions(-) diff --git a/src/EPPlus.Fonts.OpenType.Tests/Integration/TextLayoutEngineTests.cs b/src/EPPlus.Fonts.OpenType.Tests/Integration/TextLayoutEngineTests.cs index 971d86a4e..01f861e71 100644 --- a/src/EPPlus.Fonts.OpenType.Tests/Integration/TextLayoutEngineTests.cs +++ b/src/EPPlus.Fonts.OpenType.Tests/Integration/TextLayoutEngineTests.cs @@ -486,10 +486,113 @@ public void MeasureBigGoudy() //Assert.AreEqual(16.5d, wrappedLines[0].LineFragments[1].Width); } + [TestMethod] + public void EnsureRichTextLineWrappingSameAsNonRichWhenNoWrap() + { + List comparatorLst = new() { "Strike", "Goudy size"}; + + var font = OpenTypeFonts.GetFontData(FontFolders, "Aptos Narrow", FontSubFamily.Regular); + var shaper = new TextShaper(font); + var points1 = shaper.MeasureTextInPoints(comparatorLst[0], 11); + + var font2 = OpenTypeFonts.GetFontData(FontFolders, "Goudy Stout", FontSubFamily.Regular); + var otherShaper = new TextShaper(font2); + + var points2 = otherShaper.MeasureTextInPoints(comparatorLst[1], 16); + + var pointsTotal = points1 + points2; + Assert.AreEqual(202.8916f, pointsTotal); + + var comparatorFragments = new List(); + + var font11= new MeasurementFont() + { + FontFamily = "Aptos Narrow", + Size = 11, + Style = MeasurementFontStyles.Strikeout + }; + + var font22 = new MeasurementFont() + { + FontFamily = "Goudy Stout", + Size = 16, + Style = MeasurementFontStyles.Regular + }; + + var frag1 = new TextFragment() { Font = font11, Text = comparatorLst[0] }; + var frag2 = new TextFragment() { Font = font22, Text = comparatorLst[1] }; + comparatorFragments.Add(frag1); + comparatorFragments.Add(frag2); + + var startFont = OpenTypeFonts.GetFontData(FontFolders, font11.FontFamily, GetFontSubType(font11.Style)); + var goudyFont = OpenTypeFonts.GetFontData(FontFolders, font22.FontFamily, GetFontSubType(font22.Style)); + + ITextShaper shaper2 = new TextShaper(font); + using var layoutEngine = new TextLayoutEngine(shaper); + var wrappedLines = layoutEngine.WrapRichTextLines(comparatorFragments, 225d); + + Assert.AreEqual(pointsTotal, wrappedLines[0].Width); + Assert.AreEqual(points1, wrappedLines[0].LineFragments[0].Width); + Assert.AreEqual(points2, wrappedLines[0].LineFragments[1].Width); + } + + [TestMethod] + public void EnsureRichTextLineWrappingSameAsNonRichWhenNoWrapAndSpaceTrail() + { + List comparatorLst = new() { "Strike", "Goudy size " }; + + var font = OpenTypeFonts.GetFontData(FontFolders, "Aptos Narrow", FontSubFamily.Regular); + var shaper = new TextShaper(font); + var points1 = shaper.MeasureTextInPoints(comparatorLst[0], 11); + + var font2 = OpenTypeFonts.GetFontData(FontFolders, "Goudy Stout", FontSubFamily.Regular); + var otherShaper = new TextShaper(font2); + + var points2 = otherShaper.MeasureTextInPoints(comparatorLst[1], 16); + + var pointsTotal = points1 + points2; + Assert.AreEqual(210.8916f, pointsTotal); + + var comparatorFragments = new List(); + + var font11 = new MeasurementFont() + { + FontFamily = "Aptos Narrow", + Size = 11, + Style = MeasurementFontStyles.Strikeout + }; + + var font22 = new MeasurementFont() + { + FontFamily = "Goudy Stout", + Size = 16, + Style = MeasurementFontStyles.Regular + }; + + var frag1 = new TextFragment() { Font = font11, Text = comparatorLst[0] }; + var frag2 = new TextFragment() { Font = font22, Text = comparatorLst[1] }; + comparatorFragments.Add(frag1); + comparatorFragments.Add(frag2); + + var startFont = OpenTypeFonts.GetFontData(FontFolders, font11.FontFamily, GetFontSubType(font11.Style)); + var goudyFont = OpenTypeFonts.GetFontData(FontFolders, font22.FontFamily, GetFontSubType(font22.Style)); + + ITextShaper shaper2 = new TextShaper(font); + using var layoutEngine = new TextLayoutEngine(shaper); + var wrappedLines = layoutEngine.WrapRichTextLines(comparatorFragments, 225d); + + Assert.AreEqual(pointsTotal, wrappedLines[0].Width); + Assert.AreEqual(points1, wrappedLines[0].LineFragments[0].Width); + Assert.AreEqual(points2, wrappedLines[0].LineFragments[1].Width); + var noSpaceWidth = wrappedLines[0].GetWidthWithoutTrailingSpaces(); + Assert.AreEqual(202.8916f, noSpaceWidth); + } + [TestMethod] public void EnsureLineFragmentsAreMeasuredCorrectlyWhenWrapping() { List lstOfRichText = new() { "TextBox2", "ra underline", "La Strike", "Goudy size 16"}; + List comparatorLst = new() { "Strike", "Goudy size"}; var font2 = new MeasurementFont() { FontFamily = "Aptos Narrow", @@ -537,8 +640,7 @@ public void EnsureLineFragmentsAreMeasuredCorrectlyWhenWrapping() var wrappedLines = layout.WrapRichTextLines(fragments, maxSizePoints); Assert.AreEqual(12.55224609375d, wrappedLines[0].LineFragments[2].Width); - Assert.AreEqual(201.99, wrappedLines[1].Width); - + Assert.AreEqual(202.8916f, wrappedLines[1].GetWidthWithoutTrailingSpaces()); List smallestTextFragments = new List(); diff --git a/src/EPPlus.Fonts.OpenType/Integration/TextLayoutEngine.RichText.cs b/src/EPPlus.Fonts.OpenType/Integration/TextLayoutEngine.RichText.cs index 10307509d..cfbb259c3 100644 --- a/src/EPPlus.Fonts.OpenType/Integration/TextLayoutEngine.RichText.cs +++ b/src/EPPlus.Fonts.OpenType/Integration/TextLayoutEngine.RichText.cs @@ -55,7 +55,7 @@ public List WrapRichText( ProcessFragment(fragment, maxWidthPoints, lineBuilder, state); } - FinalizeCurrentLine(lineBuilder, state.CurrentLineWidth, state.WordStart); + FinalizeCurrentLine(lineBuilder, state.CurrentLineWidth, state.WordStart, state.CurrentTextLine); state.EndCurrentTextLine(); @@ -90,7 +90,7 @@ public List WrapRichTextLines( ProcessFragment(fragment, maxWidthPoints, lineBuilder, state); } - FinalizeCurrentLine(lineBuilder, state.CurrentLineWidth, state.WordStart); + FinalizeCurrentLine(lineBuilder, state.CurrentLineWidth, state.WordStart, state.CurrentTextLine); state.CurrentTextLine.Width = state.CurrentLineWidth; state.CurrentTextLine.Text = lineBuilder.ToString(); state.EndCurrentTextLine(); @@ -260,7 +260,7 @@ private void WrapCurrentLine(StringBuilder lineBuilder, WrapStateRichText state, // Bounds check to prevent ArgumentOutOfRangeException if (state.WordStart >= 0 && state.WordStart < lineBuilder.Length) { - var lineStringWithTrail = lineBuilder.ToString(0, state.WordStart+1); + var lineStringWithTrail = lineBuilder.ToString(0, state.WordStart+1);//+1 was just added and should be here but everything else is sorta based on it being gone... if (lineStringWithTrail[lineStringWithTrail.Length-1] == ' ') { state.CurrentTextLine.WasWrappedOnSpace = true; @@ -318,12 +318,13 @@ private void WrapCurrentLine(StringBuilder lineBuilder, WrapStateRichText state, state.LineStart = -1; } - private void FinalizeCurrentLine(StringBuilder lineBuilder, double lineWidth, int lastSpaceIndex) + private void FinalizeCurrentLine(StringBuilder lineBuilder, double lineWidth, int lastSpaceIndex, TextLineSimple currentLine) { if (lineBuilder.Length > 0) { if (lineBuilder[lineBuilder.Length - 1] == ' ') { + currentLine.WasWrappedOnSpace = true; lineBuilder.Length--; } if (lineBuilder.Length > 0) From b5f8d6d401e9450e914bbca4563c5859d2f03679 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ossian=20Edstr=C3=B6m?= Date: Fri, 27 Feb 2026 14:44:13 +0100 Subject: [PATCH 067/151] Partially ruined all things chart in taking in new rt handling --- .../SvgPathTests.cs | 14 +++ .../DrawingBase.cs | 11 ++ .../ImageRenderer.cs | 8 ++ .../RenderItems/Shared/ParagraphItem.cs | 110 ++++++------------ .../RenderItems/SvgGroupItem.cs | 2 + .../RenderItems/SvgItem/SvgTextBox.cs | 4 +- .../RenderItems/SvgRenderRectItem.cs | 1 + .../Svg/Chart/SvgChart.cs | 2 +- .../Svg/Chart/SvgChartPlotarea.cs | 2 +- .../TrueTypeMeasurer/FontMeasurerTrueType.cs | 22 ++++ src/EPPlus/Drawing/Chart/ExcelChartLegend.cs | 9 +- 11 files changed, 104 insertions(+), 81 deletions(-) diff --git a/src/EPPlus.Export.ImageRenderer.Test/SvgPathTests.cs b/src/EPPlus.Export.ImageRenderer.Test/SvgPathTests.cs index f95dbb641..8c66b8031 100644 --- a/src/EPPlus.Export.ImageRenderer.Test/SvgPathTests.cs +++ b/src/EPPlus.Export.ImageRenderer.Test/SvgPathTests.cs @@ -527,6 +527,20 @@ public void GenerateSvgForCharts() } } + [TestMethod] + public void GenerateSimplestChart() + { + ExcelPackage.License.SetNonCommercialOrganization("EPPlus Project"); + using (var p = OpenTemplatePackage("SimplestChart.xlsx")) + { + var c = p.Workbook.Worksheets[0].Drawings[0]; + + var renderer = new EPPlusImageRenderer.ImageRenderer(); + var svg = renderer.RenderDrawingToSvg(c); + SaveTextFileToWorkbook($"svg\\SimplestChartTitle.svg", svg); + } + } + [TestMethod] public void GenerateShapeCenteredParagraph() { diff --git a/src/EPPlus.Export.ImageRenderer/DrawingBase.cs b/src/EPPlus.Export.ImageRenderer/DrawingBase.cs index 4f1890a59..558f81a88 100644 --- a/src/EPPlus.Export.ImageRenderer/DrawingBase.cs +++ b/src/EPPlus.Export.ImageRenderer/DrawingBase.cs @@ -34,6 +34,17 @@ internal DrawingBase(ExcelDrawing drawing) var wb = drawing._drawings.Worksheet.Workbook; Theme = wb.ThemeManager.GetOrCreateTheme(); } + + + internal DrawingBase() + { + //Drawing = drawing; + //Bounds = drawing.GetBoundingBox(); + + //var wb = drawing._drawings.Worksheet.Workbook; + //Theme = wb.ThemeManager.GetOrCreateTheme(); + } + public ExcelDrawing Drawing { get; } public ExcelTheme Theme { get;} public ExcelWorkbook Workbook => Drawing._drawings.Worksheet.Workbook; diff --git a/src/EPPlus.Export.ImageRenderer/ImageRenderer.cs b/src/EPPlus.Export.ImageRenderer/ImageRenderer.cs index 0642b6dc5..f4d826171 100644 --- a/src/EPPlus.Export.ImageRenderer/ImageRenderer.cs +++ b/src/EPPlus.Export.ImageRenderer/ImageRenderer.cs @@ -54,6 +54,14 @@ public string RenderDrawingToSvg(ExcelDrawing drawing) } + + //public string RenderTextBox(ExcelDrawing someDrawing, BoundingBox parent, double maxHeight, double maxWidth) + //{ + // var sb = new StringBuilder(); + // var svgTextBox = new SvgTextBox(someDrawing, parent, maxWidth, maxHeight); + //} + + //public string RenderRangeToSvg(ExcelRange range) //{ // var ws = range.Worksheet; diff --git a/src/EPPlus.Export.ImageRenderer/RenderItems/Shared/ParagraphItem.cs b/src/EPPlus.Export.ImageRenderer/RenderItems/Shared/ParagraphItem.cs index 8fd922292..8cf5c3e9f 100644 --- a/src/EPPlus.Export.ImageRenderer/RenderItems/Shared/ParagraphItem.cs +++ b/src/EPPlus.Export.ImageRenderer/RenderItems/Shared/ParagraphItem.cs @@ -224,8 +224,11 @@ void GenerateTextFragments(ExcelDrawingTextRunCollection runs) for (int i = 0; i < runContents.Count(); i++) { - var currentFrag = new TextFragment() { Text = runContents[i], Font = fonts[i] }; - _newTextFragments.Add(currentFrag); + if (string.IsNullOrEmpty(runContents[i]) == false) + { + var currentFrag = new TextFragment() { Text = runContents[i], Font = fonts[i] }; + _newTextFragments.Add(currentFrag); + } } } @@ -245,45 +248,38 @@ private void AddLinesAndTextRuns(ExcelDrawingParagraph p, string textIfEmpty) { var measurer = new FontMeasurerTrueType(); var maxWidth = ParentTextBody.MaxWidth + 0.001; //TODO: fix for equal width issue; - lines = measurer.MeasureAndWrapTextLines(textIfEmpty, p.DefaultRunProperties.GetMeasureFont(), maxWidth); + lines = measurer.MeasureAndWrapTextLines_New(textIfEmpty, p.DefaultRunProperties.GetMeasureFont(), maxWidth); } else { - lines = WrapToSimpleTextLines(p, _textFragments); + lines = WrapToSimpleTextLines(p); } - foreach (var line in lines) + if (lines != null) { - double prevWidth = 0; - - if (HorizontalAlignment == eTextAlignment.Center) + foreach (var line in lines) { - var ctrLineWidth = line.GetWidthWithoutTrailingSpaces(); - //Should only be applied if it was wrapped - //ctrLineWidth -= line.lastFontSpaceWidth; - //if(line.Text == "StrikeGoudy size") - //{ - // ctrLineWidth = 202.5d; - //} - //Center the line within context we must use maxWidth since left depends on greatest width - prevWidth = - ctrLineWidth / 2; - } + double prevWidth = 0; - if (lineSpacingIsExact == false) - { - runLineSpacing += line.LargestAscent + lastDescent; - } - else - { - runLineSpacing += ParagraphLineSpacing; - } - if (line.Width > greatestWidth) - { - greatestWidth = line.Width; - } + if (HorizontalAlignment == eTextAlignment.Center) + { + var ctrLineWidth = line.GetWidthWithoutTrailingSpaces(); + prevWidth = -ctrLineWidth / 2; + } + + if (lineSpacingIsExact == false) + { + runLineSpacing += line.LargestAscent + lastDescent; + } + else + { + runLineSpacing += ParagraphLineSpacing; + } + if (line.Width > greatestWidth) + { + greatestWidth = line.Width; + } - if(line.RtFragments.Count == 0 && line.LineFragments.Count > 0) - { foreach (var lineFragment in line.LineFragments) { var displayText = line.GetLineFragmentText(lineFragment); @@ -303,31 +299,9 @@ private void AddLinesAndTextRuns(ExcelDrawingParagraph p, string textIfEmpty) runItem.Bounds.Width = lineFragment.Width; prevWidth += lineFragment.Width; } - } - else - { - foreach (var rtFragment in line.RtFragments) - { - var displayText = line.GetFragmentText(rtFragment); - - if (p.TextRuns.Count == 0 && string.IsNullOrEmpty(textIfEmpty) == false) - { - AddText(displayText, p.DefaultRunProperties); - } - else - { - AddRenderItemTextRun(p.TextRuns[rtFragment.Fragidx], displayText, prevWidth); - } - TextRunItem runItem = Runs.Last(); - runItem.YPosition = runLineSpacing; - - runItem.Bounds.Width = rtFragment.Width; - prevWidth += rtFragment.Width; - } + lastDescent = line.LargestDescent; } - - lastDescent = line.LargestDescent; } Bounds.Height = runLineSpacing + lastDescent; Bounds.Width = greatestWidth; @@ -337,27 +311,17 @@ private void AddLinesAndTextRuns(ExcelDrawingParagraph p, string textIfEmpty) //} } - List WrapToSimpleTextLines(ExcelDrawingParagraph p, TextFragmentCollection fragments) + List WrapToSimpleTextLines(ExcelDrawingParagraph p) { var ttMeasurer = (FontMeasurerTrueType)_measurer; - ttMeasurer.SetFont(_newTextFragments[0].Font); - var maxWidthPoints = Math.Round(ParentTextBody.MaxWidth, 0, MidpointRounding.AwayFromZero); - return ttMeasurer.WrapMultipleTextFragmentsToTextLines_New(_newTextFragments, maxWidthPoints); - //List fonts = new List(); - - //for (int i = 0; i < p.TextRuns.Count(); i++) - //{ - // var txtRun = p.TextRuns[i]; - // var runFont = txtRun.GetMeasurementFont(); - // fonts.Add(runFont); - //} - //var layout = TextData.GetTextLayoutEngine(fonts[0]); - //var shaper = new TextShaper( fonts[0]); - //var layout = new TextLayoutEngine(shaper); - - //var wrappedLines = layout.WrapRichTextLines(fragments, maxSizePoints); - //return ttMeasurer.WrapMultipleTextFragmentsToTextLines(fragments, fonts, maxWidthPoints); + if (_newTextFragments.Count > 0) + { + ttMeasurer.SetFont(_newTextFragments[0].Font); + var maxWidthPoints = Math.Round(ParentTextBody.MaxWidth, 0, MidpointRounding.AwayFromZero); + return ttMeasurer.WrapMultipleTextFragmentsToTextLines_New(_newTextFragments, maxWidthPoints); + } + return new List(); } internal double GetAlignmentHorizontal(eTextAlignment txAlignment) diff --git a/src/EPPlus.Export.ImageRenderer/RenderItems/SvgGroupItem.cs b/src/EPPlus.Export.ImageRenderer/RenderItems/SvgGroupItem.cs index ae92137b1..1e63ada28 100644 --- a/src/EPPlus.Export.ImageRenderer/RenderItems/SvgGroupItem.cs +++ b/src/EPPlus.Export.ImageRenderer/RenderItems/SvgGroupItem.cs @@ -39,6 +39,8 @@ internal class SvgGroupItem : RenderItem public string GroupTransform = ""; + //internal SvgGroupItem(DrawingBase renderer, BoundingBox bounds) + internal SvgGroupItem(DrawingBase renderer, BoundingBox bounds) : base(renderer) { Bounds = bounds; diff --git a/src/EPPlus.Export.ImageRenderer/RenderItems/SvgItem/SvgTextBox.cs b/src/EPPlus.Export.ImageRenderer/RenderItems/SvgItem/SvgTextBox.cs index af3cfda0c..f62251650 100644 --- a/src/EPPlus.Export.ImageRenderer/RenderItems/SvgItem/SvgTextBox.cs +++ b/src/EPPlus.Export.ImageRenderer/RenderItems/SvgItem/SvgTextBox.cs @@ -7,9 +7,7 @@ using OfficeOpenXml.FormulaParsing.Excel.Functions.Text; using System; using System.Collections.Generic; -using System.Drawing; -using System.Xml.Serialization; -using static System.Net.Mime.MediaTypeNames; + namespace EPPlus.Export.ImageRenderer.RenderItems.SvgItem { diff --git a/src/EPPlus.Export.ImageRenderer/RenderItems/SvgRenderRectItem.cs b/src/EPPlus.Export.ImageRenderer/RenderItems/SvgRenderRectItem.cs index 5c2261dd4..14bbee5e7 100644 --- a/src/EPPlus.Export.ImageRenderer/RenderItems/SvgRenderRectItem.cs +++ b/src/EPPlus.Export.ImageRenderer/RenderItems/SvgRenderRectItem.cs @@ -28,6 +28,7 @@ public SvgRenderRectItem(DrawingBase renderer, BoundingBox parent) : base(render { } + public double Left { get { return Bounds.Left; } set { Bounds.Left = value; } } public double Top { get { return Bounds.Top; } set { Bounds.Top = value; } } public double Width { get { return Bounds.Width; } set { Bounds.Width = value; } } diff --git a/src/EPPlus.Export.ImageRenderer/Svg/Chart/SvgChart.cs b/src/EPPlus.Export.ImageRenderer/Svg/Chart/SvgChart.cs index 83528fdce..e6f54ad42 100644 --- a/src/EPPlus.Export.ImageRenderer/Svg/Chart/SvgChart.cs +++ b/src/EPPlus.Export.ImageRenderer/Svg/Chart/SvgChart.cs @@ -96,7 +96,7 @@ private void SetAxisPositionsFromPlotarea(SvgChart sc) VerticalAxis.AddTickmarksAndValues(); } - if (HorizontalAxis!=null) + if (HorizontalAxis!=null && HorizontalAxis.Rectangle != null) { HorizontalAxis.Rectangle.Top = Plotarea.Rectangle.Bottom; HorizontalAxis.Rectangle.Width = Plotarea.Rectangle.Width; diff --git a/src/EPPlus.Export.ImageRenderer/Svg/Chart/SvgChartPlotarea.cs b/src/EPPlus.Export.ImageRenderer/Svg/Chart/SvgChartPlotarea.cs index c70a230ac..9a56fc4a8 100644 --- a/src/EPPlus.Export.ImageRenderer/Svg/Chart/SvgChartPlotarea.cs +++ b/src/EPPlus.Export.ImageRenderer/Svg/Chart/SvgChartPlotarea.cs @@ -50,7 +50,7 @@ internal SvgRenderRectItem GetPlotAreaRectangle(SvgChart sc) rect.Left = sc.VerticalAxis.Rectangle?.GlobalRight ?? sc.VerticalAxis.Title.Rectangle.GlobalRight; } - rect.Width = (lp == eLegendPosition.Right || lp == eLegendPosition.TopRight ? + rect.Width = ((lp == eLegendPosition.Right || lp == eLegendPosition.TopRight) && sc.Legend != null ? sc.Legend.Bounds.GlobalLeft - RightMargin : sc.ChartArea.Rectangle.Width - RightMargin) - rect.GlobalLeft; diff --git a/src/EPPlus.Fonts.OpenType/TrueTypeMeasurer/FontMeasurerTrueType.cs b/src/EPPlus.Fonts.OpenType/TrueTypeMeasurer/FontMeasurerTrueType.cs index d63f5acc1..5c6581580 100644 --- a/src/EPPlus.Fonts.OpenType/TrueTypeMeasurer/FontMeasurerTrueType.cs +++ b/src/EPPlus.Fonts.OpenType/TrueTypeMeasurer/FontMeasurerTrueType.cs @@ -246,6 +246,28 @@ public List MeasureAndWrapTextLines(string text, MeasurementFont return wrappedStrings; } + public List MeasureAndWrapTextLines_New(string text, MeasurementFont font, double maxWidthPoints, double preExistingWidthPixels = 0) + { + List lines = null; + if (string.IsNullOrEmpty(text)) + { + var layout = TextData.GetTextLayoutEngine(font); + var txtFragment = new Integration.TextFragment() + { + Font = font, + Text = text + }; + + var list = new List() { txtFragment }; + lines = layout.WrapRichTextLines(list, maxWidthPoints); + } + + return lines; + //SetFont(font.Size, font.FontFamily); + //var wrappedStrings = TextData.MeasureAndWrapTextLines(text, FontSize, CurrentFont, maxWidthPoints, preExistingWidthPixels.PixelToPoint()); + //return wrappedStrings; + } + public List MeasureAndWrapTextPoints(string text, MeasurementFont font, double MaxWidthInPoints) { SetFont(font.Size, font.FontFamily); diff --git a/src/EPPlus/Drawing/Chart/ExcelChartLegend.cs b/src/EPPlus/Drawing/Chart/ExcelChartLegend.cs index 2b2267255..40e1cf5a5 100644 --- a/src/EPPlus/Drawing/Chart/ExcelChartLegend.cs +++ b/src/EPPlus/Drawing/Chart/ExcelChartLegend.cs @@ -120,10 +120,13 @@ internal List LoadLegendEntries() { if (this is ExcelChartExLegend) return new List(); //Legend entries are not applicable for extended charts. var entries = new List(); - var nodes = GetNodes("c:legendEntry"); - foreach(XmlNode n in nodes) + if (TopNode != null) { - entries.Add(new ExcelChartLegendEntry(NameSpaceManager, n, (ExcelChartStandard)_chart)); + var nodes = GetNodes("c:legendEntry"); + foreach (XmlNode n in nodes) + { + entries.Add(new ExcelChartLegendEntry(NameSpaceManager, n, (ExcelChartStandard)_chart)); + } } return entries; } From 93e707d1ab8bc7e2e4d467107b05e00f3fd56f51 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ossian=20Edstr=C3=B6m?= Date: Fri, 27 Feb 2026 16:24:19 +0100 Subject: [PATCH 068/151] Fixed axis label centering for cat on chart --- .../RenderItems/Shared/ParagraphItem.cs | 7 +++++-- .../RenderItems/Shared/TextBodyItem.cs | 5 +++-- .../RenderItems/SvgItem/SvgParagraphItem.cs | 2 +- .../Svg/Chart/SvgChartAxis.cs | 12 +++++++++++- .../TrueTypeMeasurer/FontMeasurerTrueType.cs | 2 +- 5 files changed, 21 insertions(+), 7 deletions(-) diff --git a/src/EPPlus.Export.ImageRenderer/RenderItems/Shared/ParagraphItem.cs b/src/EPPlus.Export.ImageRenderer/RenderItems/Shared/ParagraphItem.cs index 8cf5c3e9f..29ca37772 100644 --- a/src/EPPlus.Export.ImageRenderer/RenderItems/Shared/ParagraphItem.cs +++ b/src/EPPlus.Export.ImageRenderer/RenderItems/Shared/ParagraphItem.cs @@ -184,7 +184,7 @@ public void AddText(string text, ExcelTextFont font, bool isOld) } - public void AddText(string text, ExcelTextFont font) + public void AddText(string text, ExcelTextFont font, double prevWidth) { var measurer = new FontMeasurerTrueType(); //var displayText = measurer.MeasureAndWrapTextLines(text, font.GetMeasureFont(), ParentTextBody.MaxWidth); @@ -195,6 +195,7 @@ public void AddText(string text, ExcelTextFont font) Runs.Add(container); //Bounds.Width = container.Bounds.Width + 0.001; //TODO: fix for equal width issue container.Bounds.Name = $"Container{Runs.Count}"; + container.Bounds.Left = prevWidth; } /// @@ -249,6 +250,8 @@ private void AddLinesAndTextRuns(ExcelDrawingParagraph p, string textIfEmpty) var measurer = new FontMeasurerTrueType(); var maxWidth = ParentTextBody.MaxWidth + 0.001; //TODO: fix for equal width issue; lines = measurer.MeasureAndWrapTextLines_New(textIfEmpty, p.DefaultRunProperties.GetMeasureFont(), maxWidth); + Bounds.Width = maxWidth; + Bounds.Left = GetAlignmentHorizontal(HorizontalAlignment); } else { @@ -286,7 +289,7 @@ private void AddLinesAndTextRuns(ExcelDrawingParagraph p, string textIfEmpty) if (p.TextRuns.Count == 0 && string.IsNullOrEmpty(textIfEmpty) == false) { - AddText(displayText, p.DefaultRunProperties); + AddText(displayText, p.DefaultRunProperties, prevWidth); } else { diff --git a/src/EPPlus.Export.ImageRenderer/RenderItems/Shared/TextBodyItem.cs b/src/EPPlus.Export.ImageRenderer/RenderItems/Shared/TextBodyItem.cs index 9ce4f69e1..6693548a2 100644 --- a/src/EPPlus.Export.ImageRenderer/RenderItems/Shared/TextBodyItem.cs +++ b/src/EPPlus.Export.ImageRenderer/RenderItems/Shared/TextBodyItem.cs @@ -89,7 +89,7 @@ public void ImportParagraph(ExcelDrawingParagraph item, double startingY, string } } Paragraphs.Add(paragraph); - SetHorizontalAlignmentPosition(); + //SetHorizontalAlignmentPosition(); } private void SetHorizontalAlignmentPosition() @@ -123,7 +123,8 @@ private void SetHorizontalAlignmentPosition() internal virtual void ImportTextBody(ExcelTextBody body) { _text = null; - VerticalAlignment = body.Anchor; + //VerticalAlignment = body.Anchor; + VerticalAlignment = eTextAnchoringType.Top; //We already apply bounds top via the parent Transform double paragraphStartY = GetAlignmentVertical(); diff --git a/src/EPPlus.Export.ImageRenderer/RenderItems/SvgItem/SvgParagraphItem.cs b/src/EPPlus.Export.ImageRenderer/RenderItems/SvgItem/SvgParagraphItem.cs index acb10427e..8f4d3e9cd 100644 --- a/src/EPPlus.Export.ImageRenderer/RenderItems/SvgItem/SvgParagraphItem.cs +++ b/src/EPPlus.Export.ImageRenderer/RenderItems/SvgItem/SvgParagraphItem.cs @@ -51,7 +51,7 @@ public override void Render(StringBuilder sb) { var fontSize = _paragraphFont.Size.PointToPixel().ToString(CultureInfo.InvariantCulture); - sb.AppendLine($""); + sb.AppendLine($""); sb.AppendLine("paragraph "); diff --git a/src/EPPlus.Export.ImageRenderer/Svg/Chart/SvgChartAxis.cs b/src/EPPlus.Export.ImageRenderer/Svg/Chart/SvgChartAxis.cs index 9e22912a6..512c54aec 100644 --- a/src/EPPlus.Export.ImageRenderer/Svg/Chart/SvgChartAxis.cs +++ b/src/EPPlus.Export.ImageRenderer/Svg/Chart/SvgChartAxis.cs @@ -309,6 +309,14 @@ private List GetAxisValueTextBoxes() var tb = new SvgTextBox(SvgChart, Rectangle.Bounds, x, y, width, height, maxWidth, maxHeight); var p = Axis.TextBody.Paragraphs.FirstOrDefault(); + + if (p.HorizontalAlignment != eTextAlignment.Center) + { + //Axises are always seemingly center aligned + //Should be broken out as input to ImportParagraph instead of changing the base item + p.HorizontalAlignment = eTextAlignment.Center; + } + tb.TextBody.ImportParagraph(p, 0, v); //tb.TextBody.Paragraphs[0].AddText(v, Axis.Font); @@ -334,7 +342,9 @@ private double GetAxisItemLeft(int i, OfficeOpenXml.Interfaces.Drawing.Text.Text if (Axis.AxisType == eAxisType.Cat) { var majorWidth = Rectangle.Width / AxisValues.Count; - return Rectangle.Left + majorWidth * i; + var majorTickStartingPosition = Rectangle.Left + majorWidth * i; + var middleOfBounds = majorTickStartingPosition + (majorWidth / 2); + return majorTickStartingPosition; } else { diff --git a/src/EPPlus.Fonts.OpenType/TrueTypeMeasurer/FontMeasurerTrueType.cs b/src/EPPlus.Fonts.OpenType/TrueTypeMeasurer/FontMeasurerTrueType.cs index 5c6581580..f04ca479d 100644 --- a/src/EPPlus.Fonts.OpenType/TrueTypeMeasurer/FontMeasurerTrueType.cs +++ b/src/EPPlus.Fonts.OpenType/TrueTypeMeasurer/FontMeasurerTrueType.cs @@ -249,7 +249,7 @@ public List MeasureAndWrapTextLines(string text, MeasurementFont public List MeasureAndWrapTextLines_New(string text, MeasurementFont font, double maxWidthPoints, double preExistingWidthPixels = 0) { List lines = null; - if (string.IsNullOrEmpty(text)) + if (string.IsNullOrEmpty(text) == false) { var layout = TextData.GetTextLayoutEngine(font); var txtFragment = new Integration.TextFragment() From c803a51b1368964167c6d72b87b25a58403a9d7f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ossian=20Edstr=C3=B6m?= Date: Mon, 2 Mar 2026 10:16:56 +0100 Subject: [PATCH 069/151] =?UTF-8?q?Made=20textbox=20essentially=20unnecesa?= =?UTF-8?q?ry=3F=C2=A7?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../SvgPathTests.cs | 20 +++--- .../RenderItems/Shared/ParagraphItem.cs | 65 ++++++++++++++++--- .../RenderItems/SvgItem/SvgParagraphItem.cs | 17 +++++ .../Svg/Chart/SvgChartAxis.cs | 4 +- 4 files changed, 85 insertions(+), 21 deletions(-) diff --git a/src/EPPlus.Export.ImageRenderer.Test/SvgPathTests.cs b/src/EPPlus.Export.ImageRenderer.Test/SvgPathTests.cs index 8c66b8031..a303316a0 100644 --- a/src/EPPlus.Export.ImageRenderer.Test/SvgPathTests.cs +++ b/src/EPPlus.Export.ImageRenderer.Test/SvgPathTests.cs @@ -513,17 +513,17 @@ public void GenerateSvgForCharts() var ws = p.Workbook.Worksheets[0]; var renderer = new EPPlusImageRenderer.ImageRenderer(); - var ix = 2; - var c = ws.Drawings[ix]; - var svg = renderer.RenderDrawingToSvg(c); - SaveTextFileToWorkbook($"svg\\ChartForSvg_ind{ix++}.svg", svg); + //var ix = 2; + //var c = ws.Drawings[ix]; + //var svg = renderer.RenderDrawingToSvg(c); + //SaveTextFileToWorkbook($"svg\\ChartForSvg_ind{ix++}.svg", svg); - //var ix = 1; - //foreach (ExcelChart c in ws.Drawings) - //{ - // var svg = renderer.RenderDrawingToSvg(c); - // SaveTextFileToWorkbook($"svg\\ChartForSvg{ix++}.svg", svg); - //} + var ix = 0; + foreach (ExcelChart c in ws.Drawings) + { + var svg = renderer.RenderDrawingToSvg(c); + SaveTextFileToWorkbook($"svg\\ChartForSvg{ix++}.svg", svg); + } } } diff --git a/src/EPPlus.Export.ImageRenderer/RenderItems/Shared/ParagraphItem.cs b/src/EPPlus.Export.ImageRenderer/RenderItems/Shared/ParagraphItem.cs index 29ca37772..ccefd11ac 100644 --- a/src/EPPlus.Export.ImageRenderer/RenderItems/Shared/ParagraphItem.cs +++ b/src/EPPlus.Export.ImageRenderer/RenderItems/Shared/ParagraphItem.cs @@ -43,6 +43,10 @@ internal abstract class ParagraphItem : RenderItem internal eTextAlignment HorizontalAlignment { get; private set; } = eTextAlignment.Left; internal List Runs { get; set; } = new List(); + internal bool DisplayBounds { get; set; } = true; + + private double? _centerAdjustment = null; + public ParagraphItem(TextBodyItem textBody, DrawingBase renderer, BoundingBox parent) : base(renderer, parent) { ParentTextBody = textBody; @@ -101,7 +105,18 @@ public ParagraphItem(TextBodyItem textBody, DrawingBase renderer, BoundingBox pa //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(HorizontalAlignment); + if (HorizontalAlignment != eTextAlignment.Center) + { + Bounds.Left = GetAlignmentHorizontal(HorizontalAlignment); + } + else + { + //Center is a bit strange the bounds really are the same as left or right aligned + //It doesn't truly matter as only left min and right max play a role + Bounds.Left = GetAlignmentHorizontal(eTextAlignment.Left); + _centerAdjustment = GetAlignmentHorizontal(HorizontalAlignment); + + } Bounds.Width = parent.Width - _rightMargin - _leftMargin; //Bounds.Width = ParentTextBody.Width; @@ -250,16 +265,51 @@ private void AddLinesAndTextRuns(ExcelDrawingParagraph p, string textIfEmpty) var measurer = new FontMeasurerTrueType(); var maxWidth = ParentTextBody.MaxWidth + 0.001; //TODO: fix for equal width issue; lines = measurer.MeasureAndWrapTextLines_New(textIfEmpty, p.DefaultRunProperties.GetMeasureFont(), maxWidth); + Bounds.Width = maxWidth; - Bounds.Left = GetAlignmentHorizontal(HorizontalAlignment); + if (HorizontalAlignment != eTextAlignment.Center) + { + Bounds.Left = GetAlignmentHorizontal(HorizontalAlignment); + } + else + { + //Center is a bit strange the bounds really are the same as left or right aligned + //It doesn't truly matter as only left min and right max play a role + Bounds.Left = GetAlignmentHorizontal(eTextAlignment.Left); + _centerAdjustment = GetAlignmentHorizontal(HorizontalAlignment); + + } } else { lines = WrapToSimpleTextLines(p); } - if (lines != null) + + if (lines != null && lines.Count != 0) { + //This could be moved into a textLines collection class + //START + var idxOfLargestLine = 0; + double widthOfLargestLine = lines[0].Width; + + for (int i = 1; i < lines.Count; i++) + { + if (lines[i].Width > widthOfLargestLine) + { + widthOfLargestLine = lines[i].Width; + idxOfLargestLine = i; + } + } + //END + + if(HorizontalAlignment == eTextAlignment.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); + } + foreach (var line in lines) { double prevWidth = 0; @@ -267,7 +317,9 @@ private void AddLinesAndTextRuns(ExcelDrawingParagraph p, string textIfEmpty) if (HorizontalAlignment == eTextAlignment.Center) { var ctrLineWidth = line.GetWidthWithoutTrailingSpaces(); - prevWidth = -ctrLineWidth / 2; + + //center adjustment should always have value here + prevWidth = _centerAdjustment.Value - (ctrLineWidth / 2) - Bounds.Left; } if (lineSpacingIsExact == false) @@ -302,16 +354,11 @@ private void AddLinesAndTextRuns(ExcelDrawingParagraph p, string textIfEmpty) runItem.Bounds.Width = lineFragment.Width; prevWidth += lineFragment.Width; } - lastDescent = line.LargestDescent; } } Bounds.Height = runLineSpacing + lastDescent; Bounds.Width = greatestWidth; - //if (HorizontalAlignment == eTextAlignment.Center) - //{ - // Bounds.Left = Bounds.Left - greatestWidth; - //} } List WrapToSimpleTextLines(ExcelDrawingParagraph p) diff --git a/src/EPPlus.Export.ImageRenderer/RenderItems/SvgItem/SvgParagraphItem.cs b/src/EPPlus.Export.ImageRenderer/RenderItems/SvgItem/SvgParagraphItem.cs index 8f4d3e9cd..ece72c6d3 100644 --- a/src/EPPlus.Export.ImageRenderer/RenderItems/SvgItem/SvgParagraphItem.cs +++ b/src/EPPlus.Export.ImageRenderer/RenderItems/SvgItem/SvgParagraphItem.cs @@ -55,6 +55,23 @@ public override void Render(StringBuilder sb) sb.AppendLine("paragraph "); + if(DisplayBounds) + { + sb.AppendLine($""); + sb.AppendLine("Bounding-Box: Paragraph "); + SvgRenderRectItem visualBoundingBox = new SvgRenderRectItem(DrawingRenderer, ParentTextBody.Bounds); + + //Left/Top handled by transform + visualBoundingBox.Bounds.Width = Bounds.Width; + visualBoundingBox.Bounds.Height = Bounds.Height; + + visualBoundingBox.FillOpacity = 0.3; + visualBoundingBox.FillColor = "red"; + visualBoundingBox.Render(sb); + sb.AppendLine($""); + + } + //var bb = new SvgRenderRectItem(DrawingRenderer, Bounds); ////The bb is affected by the Transform so set pos to zero //if (IsFirstParagraph == false) diff --git a/src/EPPlus.Export.ImageRenderer/Svg/Chart/SvgChartAxis.cs b/src/EPPlus.Export.ImageRenderer/Svg/Chart/SvgChartAxis.cs index 512c54aec..1491145f5 100644 --- a/src/EPPlus.Export.ImageRenderer/Svg/Chart/SvgChartAxis.cs +++ b/src/EPPlus.Export.ImageRenderer/Svg/Chart/SvgChartAxis.cs @@ -310,9 +310,9 @@ private List GetAxisValueTextBoxes() var p = Axis.TextBody.Paragraphs.FirstOrDefault(); - if (p.HorizontalAlignment != eTextAlignment.Center) + if (p.HorizontalAlignment != eTextAlignment.Center && (Axis.AxisPosition == eAxisPosition.Bottom || Axis.AxisPosition == eAxisPosition.Top)) { - //Axises are always seemingly center aligned + //Horizontal axises are always center aligned visually //Should be broken out as input to ImportParagraph instead of changing the base item p.HorizontalAlignment = eTextAlignment.Center; } From 9a889bc8a55de432d963f9d1724ee8bbd71031fd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ossian=20Edstr=C3=B6m?= Date: Mon, 2 Mar 2026 10:31:47 +0100 Subject: [PATCH 070/151] Added titleSvgItem. Added textbox tooltip --- .../RenderItems/RenderItemType.cs | 1 + .../RenderItems/SvgGroupItem.cs | 4 +++ .../RenderItems/SvgItem/SvgTextBox.cs | 10 +++++++ .../RenderItems/SvgItem/SvgTitleItem.cs | 26 +++++++++++++++++++ 4 files changed, 41 insertions(+) create mode 100644 src/EPPlus.Export.ImageRenderer/RenderItems/SvgItem/SvgTitleItem.cs diff --git a/src/EPPlus.Export.ImageRenderer/RenderItems/RenderItemType.cs b/src/EPPlus.Export.ImageRenderer/RenderItems/RenderItemType.cs index c770bfdc7..7c6452b7b 100644 --- a/src/EPPlus.Export.ImageRenderer/RenderItems/RenderItemType.cs +++ b/src/EPPlus.Export.ImageRenderer/RenderItems/RenderItemType.cs @@ -22,5 +22,6 @@ internal enum RenderItemType Text = 5, TSpan = 6, Paragraph = 7, + CommentTitle = 8, } } \ No newline at end of file diff --git a/src/EPPlus.Export.ImageRenderer/RenderItems/SvgGroupItem.cs b/src/EPPlus.Export.ImageRenderer/RenderItems/SvgGroupItem.cs index 1e63ada28..2a830836e 100644 --- a/src/EPPlus.Export.ImageRenderer/RenderItems/SvgGroupItem.cs +++ b/src/EPPlus.Export.ImageRenderer/RenderItems/SvgGroupItem.cs @@ -41,6 +41,10 @@ internal class SvgGroupItem : RenderItem //internal SvgGroupItem(DrawingBase renderer, BoundingBox bounds) + internal SvgGroupItem(DrawingBase renderer) : base(renderer) + { + } + internal SvgGroupItem(DrawingBase renderer, BoundingBox bounds) : base(renderer) { Bounds = bounds; diff --git a/src/EPPlus.Export.ImageRenderer/RenderItems/SvgItem/SvgTextBox.cs b/src/EPPlus.Export.ImageRenderer/RenderItems/SvgItem/SvgTextBox.cs index f62251650..efb7e0d0b 100644 --- a/src/EPPlus.Export.ImageRenderer/RenderItems/SvgItem/SvgTextBox.cs +++ b/src/EPPlus.Export.ImageRenderer/RenderItems/SvgItem/SvgTextBox.cs @@ -124,7 +124,17 @@ internal override void AppendRenderItems(List renderItems) groupItem = new SvgGroupItem(DrawingRenderer, new BoundingBox(Left, Top, Width, Height), Rotation); } renderItems.Add(groupItem); + + var textboxGroupItem = new SvgGroupItem(DrawingRenderer); + renderItems.Add(textboxGroupItem); + + var titleItem = new SvgTitleItem(DrawingRenderer, "TextBox Rect"); + + renderItems.Add(titleItem); renderItems.Add(rect); + + renderItems.Add(new SvgEndGroupItem(DrawingRenderer, rect.Bounds)); + TextBody.Bounds.Left = LeftMargin; TextBody.Bounds.Top = TopMargin; TextBody.AppendRenderItems(renderItems); diff --git a/src/EPPlus.Export.ImageRenderer/RenderItems/SvgItem/SvgTitleItem.cs b/src/EPPlus.Export.ImageRenderer/RenderItems/SvgItem/SvgTitleItem.cs new file mode 100644 index 000000000..a053fbb56 --- /dev/null +++ b/src/EPPlus.Export.ImageRenderer/RenderItems/SvgItem/SvgTitleItem.cs @@ -0,0 +1,26 @@ +using EPPlusImageRenderer; +using EPPlusImageRenderer.RenderItems; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; + +namespace EPPlus.Export.ImageRenderer.RenderItems.SvgItem +{ + internal class SvgTitleItem : RenderItem + { + internal string Title { get; private set; } + + public SvgTitleItem(DrawingBase renderer, string titleName) : base(renderer) + { + Title = titleName; + } + + public override RenderItemType Type => RenderItemType.CommentTitle; + + public override void Render(StringBuilder sb) + { + sb.Append($"{Title}"); + } + } +} From d96a77bfd76a551f9f0752aef5adb4ed88957a74 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ossian=20Edstr=C3=B6m?= Date: Mon, 2 Mar 2026 11:32:42 +0100 Subject: [PATCH 071/151] Added support for multiple addresses in chart title linked cell --- .../SvgPathTests.cs | 20 +++++++++---------- src/EPPlus/Drawing/Chart/ExcelChartTitle.cs | 10 ++++++++++ 2 files changed, 20 insertions(+), 10 deletions(-) diff --git a/src/EPPlus.Export.ImageRenderer.Test/SvgPathTests.cs b/src/EPPlus.Export.ImageRenderer.Test/SvgPathTests.cs index a303316a0..994226e6d 100644 --- a/src/EPPlus.Export.ImageRenderer.Test/SvgPathTests.cs +++ b/src/EPPlus.Export.ImageRenderer.Test/SvgPathTests.cs @@ -513,17 +513,17 @@ public void GenerateSvgForCharts() var ws = p.Workbook.Worksheets[0]; var renderer = new EPPlusImageRenderer.ImageRenderer(); - //var ix = 2; - //var c = ws.Drawings[ix]; - //var svg = renderer.RenderDrawingToSvg(c); - //SaveTextFileToWorkbook($"svg\\ChartForSvg_ind{ix++}.svg", svg); + var ix = 2; + var c = ws.Drawings[ix]; + var svg = renderer.RenderDrawingToSvg(c); + SaveTextFileToWorkbook($"svg\\ChartForSvg_ind{ix++}.svg", svg); - var ix = 0; - foreach (ExcelChart c in ws.Drawings) - { - var svg = renderer.RenderDrawingToSvg(c); - SaveTextFileToWorkbook($"svg\\ChartForSvg{ix++}.svg", svg); - } + //var ix = 0; + //foreach (ExcelChart c in ws.Drawings) + //{ + // var svg = renderer.RenderDrawingToSvg(c); + // SaveTextFileToWorkbook($"svg\\ChartForSvg{ix++}.svg", svg); + //} } } diff --git a/src/EPPlus/Drawing/Chart/ExcelChartTitle.cs b/src/EPPlus/Drawing/Chart/ExcelChartTitle.cs index 5cc46e61b..99b920348 100644 --- a/src/EPPlus/Drawing/Chart/ExcelChartTitle.cs +++ b/src/EPPlus/Drawing/Chart/ExcelChartTitle.cs @@ -406,6 +406,16 @@ public override string Text } else { + if(LinkedCell.IsSingleCell == false) + { + string combinedString = ""; + string separator = " "; + foreach(var address in LinkedCell) + { + combinedString += address.Text + separator; + } + return combinedString; + } return LinkedCell.Text; } } From eeea50abeb83026ebe6ff048004b5e5cdae17547 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ossian=20Edstr=C3=B6m?= Date: Mon, 2 Mar 2026 14:13:01 +0100 Subject: [PATCH 072/151] fixed concatenation --- .../SvgPathTests.cs | 20 +++++++++---------- .../Svg/SvgShape.cs | 12 ++++++++--- 2 files changed, 19 insertions(+), 13 deletions(-) diff --git a/src/EPPlus.Export.ImageRenderer.Test/SvgPathTests.cs b/src/EPPlus.Export.ImageRenderer.Test/SvgPathTests.cs index 994226e6d..a303316a0 100644 --- a/src/EPPlus.Export.ImageRenderer.Test/SvgPathTests.cs +++ b/src/EPPlus.Export.ImageRenderer.Test/SvgPathTests.cs @@ -513,17 +513,17 @@ public void GenerateSvgForCharts() var ws = p.Workbook.Worksheets[0]; var renderer = new EPPlusImageRenderer.ImageRenderer(); - var ix = 2; - var c = ws.Drawings[ix]; - var svg = renderer.RenderDrawingToSvg(c); - SaveTextFileToWorkbook($"svg\\ChartForSvg_ind{ix++}.svg", svg); + //var ix = 2; + //var c = ws.Drawings[ix]; + //var svg = renderer.RenderDrawingToSvg(c); + //SaveTextFileToWorkbook($"svg\\ChartForSvg_ind{ix++}.svg", svg); - //var ix = 0; - //foreach (ExcelChart c in ws.Drawings) - //{ - // var svg = renderer.RenderDrawingToSvg(c); - // SaveTextFileToWorkbook($"svg\\ChartForSvg{ix++}.svg", svg); - //} + var ix = 0; + foreach (ExcelChart c in ws.Drawings) + { + var svg = renderer.RenderDrawingToSvg(c); + SaveTextFileToWorkbook($"svg\\ChartForSvg{ix++}.svg", svg); + } } } diff --git a/src/EPPlus.Export.ImageRenderer/Svg/SvgShape.cs b/src/EPPlus.Export.ImageRenderer/Svg/SvgShape.cs index 4f15393dd..1719f6346 100644 --- a/src/EPPlus.Export.ImageRenderer/Svg/SvgShape.cs +++ b/src/EPPlus.Export.ImageRenderer/Svg/SvgShape.cs @@ -113,8 +113,10 @@ public SvgShape(ExcelShape shape) : base(shape) } InsetTextBox.FillOpacity = 0.3d; + + RenderItems.Add(new SvgGroupItem(this, InsetTextBox.Bounds)); TextBox = CreateTextBodyItem(_shape.TextBody); - TextBox.AppendRenderItems(RenderItems); + RenderItems.Add(new SvgEndGroupItem(this, Bounds)); } } } @@ -266,17 +268,21 @@ SvgTextBodyItem CreateTextBodyItem(ExcelTextBody bodyOrig) double l, r, t, b; bodyOrig.GetInsetsOrDefaults(out l, out t, out r, out b); - MarginTextBox = new SvgRenderRectItem(this, this.Bounds); + MarginTextBox = new SvgRenderRectItem(this, InsetTextBox.Bounds); MarginTextBox.Width = InsetTextBox.Width - r; MarginTextBox.Height = InsetTextBox.Height - b; MarginTextBox.Top = t + InsetTextBox.Top; MarginTextBox.Left = l + InsetTextBox.Left; - var txtBodyItem = new SvgTextBodyItem(this, MarginTextBox.Bounds, 0, 0, MarginTextBox.Width, MarginTextBox.Height); + RenderItems.Add(new SvgGroupItem(this, MarginTextBox.Bounds)); + var txtBodyItem = new SvgTextBodyItem(this, MarginTextBox.Bounds, 0, 0, MarginTextBox.Width, MarginTextBox.Height); txtBodyItem.ImportTextBody(bodyOrig); + txtBodyItem.AppendRenderItems(RenderItems); + + RenderItems.Add(new SvgEndGroupItem(this, Bounds)); //txtBodyItem.Width = InsetTextBox.Width; return txtBodyItem; From dcd12b210409781096094b11f32debb4fdd7b08c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ossian=20Edstr=C3=B6m?= Date: Tue, 3 Mar 2026 12:21:08 +0100 Subject: [PATCH 073/151] Added right-align for svgCharts --- .../SvgPathTests.cs | 17 ++++++- .../RenderItems/Shared/ParagraphItem.cs | 50 ++++++++----------- .../RenderItems/Shared/TextBodyItem.cs | 18 +++++-- .../Svg/Chart/SvgChartAxis.cs | 2 + .../Svg/SvgShape.cs | 11 ++-- .../Integration/TextLayoutEngineTests.cs | 26 +++++++++- .../Integration/TextLayoutEngine.RichText.cs | 6 ++- 7 files changed, 87 insertions(+), 43 deletions(-) diff --git a/src/EPPlus.Export.ImageRenderer.Test/SvgPathTests.cs b/src/EPPlus.Export.ImageRenderer.Test/SvgPathTests.cs index a303316a0..d7a40b807 100644 --- a/src/EPPlus.Export.ImageRenderer.Test/SvgPathTests.cs +++ b/src/EPPlus.Export.ImageRenderer.Test/SvgPathTests.cs @@ -513,7 +513,7 @@ public void GenerateSvgForCharts() var ws = p.Workbook.Worksheets[0]; var renderer = new EPPlusImageRenderer.ImageRenderer(); - //var ix = 2; + //var ix = 0; //var c = ws.Drawings[ix]; //var svg = renderer.RenderDrawingToSvg(c); //SaveTextFileToWorkbook($"svg\\ChartForSvg_ind{ix++}.svg", svg); @@ -541,6 +541,21 @@ public void GenerateSimplestChart() } } + + [TestMethod] + public void OpenRightAligned() + { + ExcelPackage.License.SetNonCommercialOrganization("EPPlus Project"); + using (var p = OpenTemplatePackage("SimpleChartRightAlign.xlsx")) + { + var c = p.Workbook.Worksheets[0].Drawings[0]; + + var renderer = new EPPlusImageRenderer.ImageRenderer(); + var svg = renderer.RenderDrawingToSvg(c); + SaveTextFileToWorkbook($"svg\\SimplestChartRightAlign.svg", svg); + } + } + [TestMethod] public void GenerateShapeCenteredParagraph() { diff --git a/src/EPPlus.Export.ImageRenderer/RenderItems/Shared/ParagraphItem.cs b/src/EPPlus.Export.ImageRenderer/RenderItems/Shared/ParagraphItem.cs index ccefd11ac..d28536e21 100644 --- a/src/EPPlus.Export.ImageRenderer/RenderItems/Shared/ParagraphItem.cs +++ b/src/EPPlus.Export.ImageRenderer/RenderItems/Shared/ParagraphItem.cs @@ -118,25 +118,6 @@ public ParagraphItem(TextBodyItem textBody, DrawingBase renderer, BoundingBox pa } Bounds.Width = parent.Width - _rightMargin - _leftMargin; - - //Bounds.Width = ParentTextBody.Width; - //var globBounds = Bounds.GetGlobalBoundingbox(); - //Bounds.Width = parent.Width; - //Bounds.Left = GetAlignmentHorizontal(HorizontalAlignment); - //Bounds.Width = parent.Width - _rightMargin - _leftMargin; - //Bounds.Width = parent.Width; - //Bounds.Left = _leftMargin; - ////if (HorizontalAlignment != eTextAlignment.Center) - ////{ - //// Bounds.Left = GetAlignmentHorizontal(HorizontalAlignment); - ////} - ////else - ////{ - //// //Center is a bit strange the bounds really are the same as left or right aligned - //// //It doesn't truly matter as only left min and right max play a role - //// Bounds.Left = GetAlignmentHorizontal(eTextAlignment.Left); - ////} - //Bounds.Width = parent.Width - _rightMargin - _leftMargin; } //---Get measurer--- @@ -273,11 +254,8 @@ private void AddLinesAndTextRuns(ExcelDrawingParagraph p, string textIfEmpty) } else { - //Center is a bit strange the bounds really are the same as left or right aligned - //It doesn't truly matter as only left min and right max play a role Bounds.Left = GetAlignmentHorizontal(eTextAlignment.Left); _centerAdjustment = GetAlignmentHorizontal(HorizontalAlignment); - } } else @@ -297,13 +275,14 @@ private void AddLinesAndTextRuns(ExcelDrawingParagraph p, string textIfEmpty) { if (lines[i].Width > widthOfLargestLine) { - widthOfLargestLine = lines[i].Width; + var ctrLineWidth = lines[i].GetWidthWithoutTrailingSpaces(); + widthOfLargestLine = ctrLineWidth; idxOfLargestLine = i; } } //END - if(HorizontalAlignment == eTextAlignment.Center) + if (HorizontalAlignment == eTextAlignment.Center && ParentTextBody.AutoSize) { //Bounds of the paragraph should be bounds of the text itself. //Therefore we must know the starting point to set accurate left and offset from left. @@ -317,9 +296,22 @@ private void AddLinesAndTextRuns(ExcelDrawingParagraph p, string textIfEmpty) if (HorizontalAlignment == eTextAlignment.Center) { var ctrLineWidth = line.GetWidthWithoutTrailingSpaces(); - - //center adjustment should always have value here - prevWidth = _centerAdjustment.Value - (ctrLineWidth / 2) - Bounds.Left; + + //var LeftForLargestLine = _centerAdjustment.Value - (widthOfLargestLine / 2); + //var LeftForCurrentLine = _centerAdjustment.Value - (ctrLineWidth / 2); + + //Calculate distance/offset between the Left of this paragraph(LeftForLargestLine) and the Left of currentLine + //prevWidth = LeftForCurrentLine - LeftForLargestLine; + + //Calculate difference in widths and split to get offset between leftmost position and current line + prevWidth = (widthOfLargestLine - ctrLineWidth)/2; + } + else if (HorizontalAlignment == eTextAlignment.Right) + { + //Note that the actual bounds with the space will be outside max bounds. + //This appears to be how excel does it + var ctrLineWidth = line.GetWidthWithoutTrailingSpaces(); + prevWidth = widthOfLargestLine - ctrLineWidth; } if (lineSpacingIsExact == false) @@ -330,9 +322,9 @@ private void AddLinesAndTextRuns(ExcelDrawingParagraph p, string textIfEmpty) { runLineSpacing += ParagraphLineSpacing; } - if (line.Width > greatestWidth) + if (line.GetWidthWithoutTrailingSpaces() > greatestWidth) { - greatestWidth = line.Width; + greatestWidth = line.GetWidthWithoutTrailingSpaces(); } foreach (var lineFragment in line.LineFragments) diff --git a/src/EPPlus.Export.ImageRenderer/RenderItems/Shared/TextBodyItem.cs b/src/EPPlus.Export.ImageRenderer/RenderItems/Shared/TextBodyItem.cs index 6693548a2..46c9c3367 100644 --- a/src/EPPlus.Export.ImageRenderer/RenderItems/Shared/TextBodyItem.cs +++ b/src/EPPlus.Export.ImageRenderer/RenderItems/Shared/TextBodyItem.cs @@ -92,10 +92,10 @@ public void ImportParagraph(ExcelDrawingParagraph item, double startingY, string //SetHorizontalAlignmentPosition(); } - private void SetHorizontalAlignmentPosition() + internal void SetHorizontalAlignmentPosition() { - if (AutoSize) - { + //if (AutoSize) + //{ foreach (var p in Paragraphs) { switch (p.HorizontalAlignment) @@ -104,7 +104,7 @@ private void SetHorizontalAlignmentPosition() p.Bounds.Left = 0; break; case eTextAlignment.Center: - p.Bounds.Left = Bounds.Width / 2 - p.Bounds.Width / 2; + p.Bounds.Left = (Bounds.Width / 2) - (p.Bounds.Width / 2); break; case eTextAlignment.Right: p.Bounds.Left = Bounds.Right - p.Bounds.Width; @@ -117,7 +117,7 @@ private void SetHorizontalAlignmentPosition() break; } } - } + //} } internal virtual void ImportTextBody(ExcelTextBody body) @@ -127,6 +127,7 @@ internal virtual void ImportTextBody(ExcelTextBody body) VerticalAlignment = eTextAnchoringType.Top; //We already apply bounds top via the parent Transform double paragraphStartY = GetAlignmentVertical(); + double largestWidth = double.MinValue; foreach (var paragraph in body.Paragraphs) { @@ -168,7 +169,14 @@ internal virtual void ImportTextBody(ExcelTextBody body) ImportParagraph(paragraph, paragraphStartY); var addedPara = Paragraphs.Last(); paragraphStartY = addedPara.Bounds.Bottom; + largestWidth = Math.Max(largestWidth, addedPara.Bounds.Width); } + + foreach (var paragraph in body.Paragraphs) + { + SetHorizontalAlignmentPosition(); + } + if (Paragraphs != null && Paragraphs.Count() > 0) { Bounds.Height = paragraphStartY; diff --git a/src/EPPlus.Export.ImageRenderer/Svg/Chart/SvgChartAxis.cs b/src/EPPlus.Export.ImageRenderer/Svg/Chart/SvgChartAxis.cs index 1491145f5..c213b857a 100644 --- a/src/EPPlus.Export.ImageRenderer/Svg/Chart/SvgChartAxis.cs +++ b/src/EPPlus.Export.ImageRenderer/Svg/Chart/SvgChartAxis.cs @@ -317,12 +317,14 @@ private List GetAxisValueTextBoxes() p.HorizontalAlignment = eTextAlignment.Center; } + //tb.ImportTextBody(Axis.TextBody); tb.TextBody.ImportParagraph(p, 0, v); //tb.TextBody.Paragraphs[0].AddText(v, Axis.Font); tb.Rectangle.SetDrawingPropertiesFill(Axis.Fill, axisStyle.FillReference.Color); ret.Add(tb); } + //ret[0].TextBody.SetHorizontalAlignmentPosition(); return ret; } diff --git a/src/EPPlus.Export.ImageRenderer/Svg/SvgShape.cs b/src/EPPlus.Export.ImageRenderer/Svg/SvgShape.cs index 1719f6346..383820a4c 100644 --- a/src/EPPlus.Export.ImageRenderer/Svg/SvgShape.cs +++ b/src/EPPlus.Export.ImageRenderer/Svg/SvgShape.cs @@ -114,9 +114,7 @@ public SvgShape(ExcelShape shape) : base(shape) InsetTextBox.FillOpacity = 0.3d; - RenderItems.Add(new SvgGroupItem(this, InsetTextBox.Bounds)); TextBox = CreateTextBodyItem(_shape.TextBody); - RenderItems.Add(new SvgEndGroupItem(this, Bounds)); } } } @@ -227,7 +225,7 @@ public void Render(StringBuilder sb) writer.WriteSvgDefs(sb, RenderItems); //SvgGroupItem gItemTest = null; - foreach(var item in RenderItems) + foreach (var item in RenderItems) { item.Render(sb); //if(item.Type == RenderItemType.Group && gItemTest == null) @@ -248,6 +246,7 @@ public void Render(StringBuilder sb) //{ // gItemTest.RenderEndGroup(sb); //} + RenderDebugTextBox(sb); sb.AppendLine(""); } @@ -268,12 +267,12 @@ SvgTextBodyItem CreateTextBodyItem(ExcelTextBody bodyOrig) double l, r, t, b; bodyOrig.GetInsetsOrDefaults(out l, out t, out r, out b); - MarginTextBox = new SvgRenderRectItem(this, InsetTextBox.Bounds); + MarginTextBox = new SvgRenderRectItem(this, this.Bounds); - MarginTextBox.Width = InsetTextBox.Width - r; - MarginTextBox.Height = InsetTextBox.Height - b; MarginTextBox.Top = t + InsetTextBox.Top; MarginTextBox.Left = l + InsetTextBox.Left; + MarginTextBox.Width = InsetTextBox.Width - r - l; + MarginTextBox.Height = InsetTextBox.Height - b - t; RenderItems.Add(new SvgGroupItem(this, MarginTextBox.Bounds)); diff --git a/src/EPPlus.Fonts.OpenType.Tests/Integration/TextLayoutEngineTests.cs b/src/EPPlus.Fonts.OpenType.Tests/Integration/TextLayoutEngineTests.cs index 01f861e71..4165f3503 100644 --- a/src/EPPlus.Fonts.OpenType.Tests/Integration/TextLayoutEngineTests.cs +++ b/src/EPPlus.Fonts.OpenType.Tests/Integration/TextLayoutEngineTests.cs @@ -673,7 +673,7 @@ public void WrapRichTextDifficultCaseCompare() FontFamily = "Aptos Narrow", Size = 11, Style = MeasurementFontStyles.Regular - }; ; + }; var font2 = new MeasurementFont() { @@ -959,5 +959,29 @@ public void WrapText_Continous_Long_Word() Assert.AreEqual("pellentesqu", wrappedLines[0]); Assert.AreEqual("er", wrappedLines[1]); } + + [TestMethod] + public void VerifyWrappingSingleChar() + { + List lstOfRichText = new() { "SE/DKK" }; + + var font1 = new MeasurementFont() + { + FontFamily = "Aptos Narrow", + Size = 11, + Style = MeasurementFontStyles.Regular + }; + + var maxWidthPt = 31.8125234375d; + var gottenFont = OpenTypeFonts.GetFontData(FontFolders, "Aptos Narrow", FontSubFamily.Regular); + var shaper = new TextShaper(gottenFont); + var layout = new TextLayoutEngine(shaper); + + List fragments = new List() { new TextFragment() { Font = font1, Text = lstOfRichText[0] } }; + + var wrappedLines = layout.WrapRichTextLines(fragments, maxWidthPt); + + Assert.AreEqual(0, wrappedLines[1].LineFragments[0].StartIdx); + } } } \ No newline at end of file diff --git a/src/EPPlus.Fonts.OpenType/Integration/TextLayoutEngine.RichText.cs b/src/EPPlus.Fonts.OpenType/Integration/TextLayoutEngine.RichText.cs index cfbb259c3..c3eb23465 100644 --- a/src/EPPlus.Fonts.OpenType/Integration/TextLayoutEngine.RichText.cs +++ b/src/EPPlus.Fonts.OpenType/Integration/TextLayoutEngine.RichText.cs @@ -257,6 +257,8 @@ private void WrapCurrentLine(StringBuilder lineBuilder, WrapStateRichText state, { int fragIdxAtBreak = state.CurrentFragmentIdx; + int adjustmentForLineBuilderLength = 0; + // Bounds check to prevent ArgumentOutOfRangeException if (state.WordStart >= 0 && state.WordStart < lineBuilder.Length) { @@ -299,6 +301,8 @@ private void WrapCurrentLine(StringBuilder lineBuilder, WrapStateRichText state, if (lastChar != ' ') { lineBuilder.Append(lastChar); + //Since we appended we should remove it from line builder length when end current and initialize next happens + adjustmentForLineBuilderLength = 1; //The char that made us move past maxWidth //must be added to the new line @@ -311,7 +315,7 @@ private void WrapCurrentLine(StringBuilder lineBuilder, WrapStateRichText state, } } - state.EndCurrentTextLineAndIntializeNext(lineBuilder.Length); + state.EndCurrentTextLineAndIntializeNext(lineBuilder.Length - adjustmentForLineBuilderLength); state.CurrentWordWidth = state.CurrentLineWidth; state.WordStart = -1; From 4300a9862441daba0d5fa11f7307e78e0c98c365 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jan=20K=C3=A4llman?= Date: Tue, 3 Mar 2026 14:38:38 +0100 Subject: [PATCH 074/151] Fixed legend and axis issues for svg charts --- .../SvgPathTests.cs | 19 +++++++++---------- .../RenderItems/SvgItem/SvgTextBodyItem.cs | 2 +- .../Svg/Chart/SvgChartAxis.cs | 2 +- .../Svg/Chart/SvgChartLegend.cs | 6 +++--- 4 files changed, 14 insertions(+), 15 deletions(-) diff --git a/src/EPPlus.Export.ImageRenderer.Test/SvgPathTests.cs b/src/EPPlus.Export.ImageRenderer.Test/SvgPathTests.cs index d7a40b807..d64ecfc40 100644 --- a/src/EPPlus.Export.ImageRenderer.Test/SvgPathTests.cs +++ b/src/EPPlus.Export.ImageRenderer.Test/SvgPathTests.cs @@ -628,19 +628,18 @@ public void GenerateSvgForLineCharts() { var ws = p.Workbook.Worksheets[0]; var renderer = new EPPlusImageRenderer.ImageRenderer(); + var ix = 2; + var c = ws.Drawings[ix]; + var svg = renderer.RenderDrawingToSvg(c); + SaveTextFileToWorkbook($"svg\\LineChartForSvg_Single{ix++}.svg", svg); //var ix = 1; - //var c = ws.Drawings[ix]; - //var svg = renderer.RenderDrawingToSvg(c); - //SaveTextFileToWorkbook($"svg\\LineChartForSvg_Single{ix++}.svg", svg); - var ix = 1; - foreach (ExcelChart c in ws.Drawings) - { - var svg = renderer.RenderDrawingToSvg(c); - SaveTextFileToWorkbook($"svg\\LineChartForSvg{ix++}.svg", svg); - } + //foreach (ExcelChart c in ws.Drawings) + //{ + // var svg = renderer.RenderDrawingToSvg(c); + // SaveTextFileToWorkbook($"svg\\LineChartForSvg{ix++}.svg", svg); + //} } } - [TestMethod] public void GenerateSvgForCharts_sheet2() { diff --git a/src/EPPlus.Export.ImageRenderer/RenderItems/SvgItem/SvgTextBodyItem.cs b/src/EPPlus.Export.ImageRenderer/RenderItems/SvgItem/SvgTextBodyItem.cs index 5310727cb..c51b6e762 100644 --- a/src/EPPlus.Export.ImageRenderer/RenderItems/SvgItem/SvgTextBodyItem.cs +++ b/src/EPPlus.Export.ImageRenderer/RenderItems/SvgItem/SvgTextBodyItem.cs @@ -23,7 +23,7 @@ public SvgTextBodyItem(DrawingBase renderer, BoundingBox parent, bool autoSize, MaxWidth = parent.Width; MaxHeight = parent.Height; } - public SvgTextBodyItem(DrawingBase renderer, BoundingBox parent, double left, double top, double maxWidth, double maxHeight, bool clampedToParent = false) : base(renderer, parent, false) + public SvgTextBodyItem(DrawingBase renderer, BoundingBox parent, double left, double top, double maxWidth, double maxHeight, bool clampedToParent = false, bool autoSize=false) : base(renderer, parent, autoSize) { Bounds.Left = left; Bounds.Top = top; diff --git a/src/EPPlus.Export.ImageRenderer/Svg/Chart/SvgChartAxis.cs b/src/EPPlus.Export.ImageRenderer/Svg/Chart/SvgChartAxis.cs index c213b857a..0501c344c 100644 --- a/src/EPPlus.Export.ImageRenderer/Svg/Chart/SvgChartAxis.cs +++ b/src/EPPlus.Export.ImageRenderer/Svg/Chart/SvgChartAxis.cs @@ -350,7 +350,7 @@ private double GetAxisItemLeft(int i, OfficeOpenXml.Interfaces.Drawing.Text.Text } else { - var majorWidth = Rectangle.Width / (AxisValues.Count - 1); + var majorWidth = Rectangle.Width / AxisValues.Count; return Rectangle.Left + majorWidth * i - m.Width.PointToPixel() / 2; } diff --git a/src/EPPlus.Export.ImageRenderer/Svg/Chart/SvgChartLegend.cs b/src/EPPlus.Export.ImageRenderer/Svg/Chart/SvgChartLegend.cs index 6fcb1342b..45ca52d1b 100644 --- a/src/EPPlus.Export.ImageRenderer/Svg/Chart/SvgChartLegend.cs +++ b/src/EPPlus.Export.ImageRenderer/Svg/Chart/SvgChartLegend.cs @@ -202,7 +202,7 @@ internal void SetLegend(SvgChart sc) sls.SeriesIcon = si; var tbLeft = si.X2 + MarginExtra; - var tbTop = si.Y2 - tm.Height * 0.75; //TODO:Should probably be font ascent + var tbTop = si.Y2 - tm.Height * 0.5; //TODO:Should probably be font ascent double tbWidth; if (pos == eLegendPosition.Left || pos == eLegendPosition.Right) { @@ -214,7 +214,7 @@ internal void SetLegend(SvgChart sc) } var tbHeight = tm.Height; - sls.Textbox = new SvgTextBodyItem(ChartRenderer, Bounds, tbLeft, tbTop, tbWidth, tbHeight); + sls.Textbox = new SvgTextBodyItem(ChartRenderer, Bounds, tbLeft, tbTop, tbWidth, tbHeight, false, true); sls.Textbox.Bounds.Left = si.X2 + MarginExtra; var entry = Chart.Legend.Entries.FirstOrDefault(x => x.Index == index); @@ -270,7 +270,7 @@ private SvgRenderLineItem GetSeriesIcon(SvgChart sc, ExcelLineChartSerie ls, int float x = 0; if (pSls == null) { - x = (float)Rectangle.Left + (float)LeftMargin + MarginExtra; + x = (float)Rectangle.Left + (float)LeftMargin;// + MarginExtra; } else { From 0055527067fafc69757e4742099e85dd2b4e6785 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jan=20K=C3=A4llman?= Date: Tue, 3 Mar 2026 15:53:41 +0100 Subject: [PATCH 075/151] WIP:Fixes for legend and axis lable --- .../SvgPathTests.cs | 20 +++++++-------- .../RenderItems/Shared/ParagraphItem.cs | 2 +- .../Svg/Chart/SvgChartLegend.cs | 9 +++---- .../Svg/Chart/SvgChartTitle.cs | 2 +- src/EPPlus/Drawing/Chart/ExcelChartSerie.cs | 25 +++++++++++-------- 5 files changed, 30 insertions(+), 28 deletions(-) diff --git a/src/EPPlus.Export.ImageRenderer.Test/SvgPathTests.cs b/src/EPPlus.Export.ImageRenderer.Test/SvgPathTests.cs index d64ecfc40..ae075dcbe 100644 --- a/src/EPPlus.Export.ImageRenderer.Test/SvgPathTests.cs +++ b/src/EPPlus.Export.ImageRenderer.Test/SvgPathTests.cs @@ -628,16 +628,16 @@ public void GenerateSvgForLineCharts() { var ws = p.Workbook.Worksheets[0]; var renderer = new EPPlusImageRenderer.ImageRenderer(); - var ix = 2; - var c = ws.Drawings[ix]; - var svg = renderer.RenderDrawingToSvg(c); - SaveTextFileToWorkbook($"svg\\LineChartForSvg_Single{ix++}.svg", svg); - //var ix = 1; - //foreach (ExcelChart c in ws.Drawings) - //{ - // var svg = renderer.RenderDrawingToSvg(c); - // SaveTextFileToWorkbook($"svg\\LineChartForSvg{ix++}.svg", svg); - //} + //var ix = 2; + //var c = ws.Drawings[ix]; + //var svg = renderer.RenderDrawingToSvg(c); + //SaveTextFileToWorkbook($"svg\\LineChartForSvg_Single{ix++}.svg", svg); + var ix = 1; + foreach (ExcelChart c in ws.Drawings) + { + var svg = renderer.RenderDrawingToSvg(c); + SaveTextFileToWorkbook($"svg\\LineChartForSvg{ix++}.svg", svg); + } } } [TestMethod] diff --git a/src/EPPlus.Export.ImageRenderer/RenderItems/Shared/ParagraphItem.cs b/src/EPPlus.Export.ImageRenderer/RenderItems/Shared/ParagraphItem.cs index d28536e21..ca9d263d9 100644 --- a/src/EPPlus.Export.ImageRenderer/RenderItems/Shared/ParagraphItem.cs +++ b/src/EPPlus.Export.ImageRenderer/RenderItems/Shared/ParagraphItem.cs @@ -43,7 +43,7 @@ internal abstract class ParagraphItem : RenderItem internal eTextAlignment HorizontalAlignment { get; private set; } = eTextAlignment.Left; internal List Runs { get; set; } = new List(); - internal bool DisplayBounds { get; set; } = true; + internal bool DisplayBounds { get; set; } = false; private double? _centerAdjustment = null; diff --git a/src/EPPlus.Export.ImageRenderer/Svg/Chart/SvgChartLegend.cs b/src/EPPlus.Export.ImageRenderer/Svg/Chart/SvgChartLegend.cs index 45ca52d1b..571a6bfa7 100644 --- a/src/EPPlus.Export.ImageRenderer/Svg/Chart/SvgChartLegend.cs +++ b/src/EPPlus.Export.ImageRenderer/Svg/Chart/SvgChartLegend.cs @@ -35,7 +35,7 @@ internal class SvgChartLegend : SvgChartObject ITextMeasurer _ttMeasurer; const int MarginExtra = 2; const int MiddleMargin = 10; - const int LineLength = 30; + const int LineLength = 28; internal SvgChartLegend(SvgChart sc) : base(sc) { _ttMeasurer = sc.Chart.WorkSheet._package.Settings.TextSettings.GenericTextMeasurerTrueType; @@ -93,8 +93,7 @@ private SvgRenderRectItem GetLegendRectangle(SvgChart sc, ExcelChartLegend l) { foreach (var s in ct.Series) { - var text = s.GetHeaderText(); - if (string.IsNullOrEmpty(text)) text = $"Series{index + 1}"; + var text = s.GetHeaderText(index); var entry = l.Entries.FirstOrDefault(x => x.Index == index); ExcelTextFont font; if(entry==null || entry.Font.IsEmpty) @@ -125,7 +124,7 @@ private SvgRenderRectItem GetLegendRectangle(SvgChart sc, ExcelChartLegend l) { case eLegendPosition.Top: case eLegendPosition.Bottom: - rect.Width = textWidth + LeftMargin + RightMargin + ((LineLength + MarginExtra) * index + (MiddleMargin*Math.Max(index-1,0))) ; // 28 is for the line length + 2px between line and text + rect.Width = textWidth + LeftMargin + RightMargin + ((LineLength + MarginExtra) * index + (MiddleMargin*Math.Max(index-1,0))) + 2 ; // 28 is for the line length + 2px between line and text rect.Height = TopMargin + BottomMargin + highest + MarginExtra; rect.Left = (sc.ChartArea.Rectangle.Width - rect.Width) / 2; if (l.Position == eLegendPosition.Top) @@ -218,7 +217,7 @@ internal void SetLegend(SvgChart sc) sls.Textbox.Bounds.Left = si.X2 + MarginExtra; var entry = Chart.Legend.Entries.FirstOrDefault(x => x.Index == index); - var headerText = s.GetHeaderText(); + var headerText = s.GetHeaderText(index); if (entry == null || entry.Font.IsEmpty) { //sls.Textbox.AddText(s.GetHeaderText(), sc.Chart.Legend.Font); diff --git a/src/EPPlus.Export.ImageRenderer/Svg/Chart/SvgChartTitle.cs b/src/EPPlus.Export.ImageRenderer/Svg/Chart/SvgChartTitle.cs index 8717399d6..93f207e87 100644 --- a/src/EPPlus.Export.ImageRenderer/Svg/Chart/SvgChartTitle.cs +++ b/src/EPPlus.Export.ImageRenderer/Svg/Chart/SvgChartTitle.cs @@ -161,7 +161,7 @@ private static string GetDefaultChartTitleText(SvgChart sc, ExcelChartTitleStand } else { - defaultText = s.GetHeaderText(); + defaultText = s.GetHeaderText(0); } } else diff --git a/src/EPPlus/Drawing/Chart/ExcelChartSerie.cs b/src/EPPlus/Drawing/Chart/ExcelChartSerie.cs index 3d80b56a5..4e667b65c 100644 --- a/src/EPPlus/Drawing/Chart/ExcelChartSerie.cs +++ b/src/EPPlus/Drawing/Chart/ExcelChartSerie.cs @@ -10,17 +10,18 @@ Date Author Change ************************************************************************************************* 01/27/2020 EPPlus Software AB Initial release EPPlus 5 *************************************************************************************************/ -using System; -using System.Collections.Generic; -using System.Text; -using System.Xml; -using System.Linq; using OfficeOpenXml.Core.CellStore; -using System.Globalization; using OfficeOpenXml.Drawing.Interfaces; using OfficeOpenXml.Drawing.Style.Effect; using OfficeOpenXml.Drawing.Style.ThreeD; using OfficeOpenXml.FormulaParsing.Excel.Functions.RefAndLookup; +using System; +using System.Collections.Generic; +using System.Globalization; +using System.Linq; +using System.Reflection; +using System.Text; +using System.Xml; namespace OfficeOpenXml.Drawing.Chart { /// @@ -249,8 +250,9 @@ internal string ToFullAddress(string value) } } - internal string GetHeaderText() + internal string GetHeaderText(int index) { + var ret = ""; if(string.IsNullOrEmpty(Header) == false) { return Header; @@ -259,18 +261,19 @@ internal string GetHeaderText() { if (string.IsNullOrEmpty(HeaderAddress.WorkSheetName)) { - return _chart.WorkSheet.Cells[HeaderAddress.Address].Offset(0, 0).Text; + ret = _chart.WorkSheet.Cells[HeaderAddress.Address].Offset(0, 0).Text; } else { var ws = _chart.WorkSheet.Workbook.Worksheets[HeaderAddress.WorkSheetName]; if (ws != null) { - return ws.Cells[HeaderAddress.Address].Offset(0, 0).Text; + ret = ws.Cells[HeaderAddress.Address].Offset(0, 0).Text; } - } + } } - return ""; + return string.IsNullOrEmpty(ret) ? $"Series{index + 1}" : ret; + ; } } } From 126be27f8000c4a730d2dfb7cf1d4bd36dd3338d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jan=20K=C3=A4llman?= Date: Wed, 4 Mar 2026 15:27:09 +0100 Subject: [PATCH 076/151] WIP:Fixed ranges in date axis to reflect Excel better --- .../SvgPathTests.cs | 2 +- .../Svg/Chart/Axis/DateAxisScaleCalculator.cs | 194 ++++++++++++++++-- .../ChartTypeDrawers/LineChartTypeDrawer.cs | 14 +- 3 files changed, 189 insertions(+), 21 deletions(-) diff --git a/src/EPPlus.Export.ImageRenderer.Test/SvgPathTests.cs b/src/EPPlus.Export.ImageRenderer.Test/SvgPathTests.cs index ae075dcbe..9713d20b0 100644 --- a/src/EPPlus.Export.ImageRenderer.Test/SvgPathTests.cs +++ b/src/EPPlus.Export.ImageRenderer.Test/SvgPathTests.cs @@ -513,7 +513,7 @@ public void GenerateSvgForCharts() var ws = p.Workbook.Worksheets[0]; var renderer = new EPPlusImageRenderer.ImageRenderer(); - //var ix = 0; + //var ix = 3; //var c = ws.Drawings[ix]; //var svg = renderer.RenderDrawingToSvg(c); //SaveTextFileToWorkbook($"svg\\ChartForSvg_ind{ix++}.svg", svg); diff --git a/src/EPPlus.Export.ImageRenderer/Svg/Chart/Axis/DateAxisScaleCalculator.cs b/src/EPPlus.Export.ImageRenderer/Svg/Chart/Axis/DateAxisScaleCalculator.cs index f49adda98..1f5602b2c 100644 --- a/src/EPPlus.Export.ImageRenderer/Svg/Chart/Axis/DateAxisScaleCalculator.cs +++ b/src/EPPlus.Export.ImageRenderer/Svg/Chart/Axis/DateAxisScaleCalculator.cs @@ -15,93 +15,215 @@ internal class DateAxisScaleCalculator /// internal static AxisScale Calculate(double dataMin, double dataMax, double chartHeightPixels, AxisOptions axisOptions = null) { + + ComputeAutoAxis(DateTime.FromOADate(dataMin), DateTime.FromOADate(dataMax)); var range = dataMax - dataMin; // Calculate major majorUnit based on range - CalculateMajorUnit(dataMin, dataMax, out double majorValue, out double minorUnit, out eTimeUnit majorUnit); - + CalculateMajorUnit(axisOptions.LockedMin ?? dataMin, axisOptions.LockedMax ?? dataMax, out double majorValue, out double axisMin, out double axisMax, out double minorUnit, out eTimeUnit majorUnit); + if(axisOptions.LockedInterval.HasValue) + { + majorValue = axisOptions.LockedInterval.Value; + } + //dataMin = FloorToUnit(dataMin, majorUnit, (int)majorValue).ToOADate(); + //dataMax = CeilingToUnit(dataMax, majorUnit, (int)majorValue).ToOADate(); return new AxisScale { - Min = dataMin, - Max = dataMax, + Min = axisMin, + Max = axisMax, MajorInterval = majorValue, MajorDateUnit = majorUnit, MinorDateUnit = majorUnit }; } + // Excel's date serial epoch: Dec 30, 1899 + private static readonly DateTime ExcelEpoch = new DateTime(1899, 12, 30); + + // Excel legacy leap year bug: dates after Feb 28, 1900 are offset by 1 + private static readonly DateTime ExcelLeapBugThreshold = new DateTime(1900, 3, 1); + + // Excel's "nice" interval ladder in days + private static readonly int[] NiceIntervals = { 1, 2, 5, 7, 14, 30, 91, 182, 365, 730 }; + private const int TargetTicks = 11; // Excel typically targets 10–12 ticks + public class AxisResult + { + public DateTime AxisMin { get; set; } + public DateTime AxisMax { get; set; } + public int IntervalDays { get; set; } + public double AxisMinSerial { get; set; } + public double AxisMaxSerial { get; set; } + public int TickCount => (int)Math.Round((AxisMaxSerial - AxisMinSerial) / IntervalDays) + 1; + + public override string ToString() => + $"Axis Min : {AxisMin:yyyy-MM-dd} (serial {AxisMinSerial})\n" + + $"Axis Max : {AxisMax:yyyy-MM-dd} (serial {AxisMaxSerial})\n" + + $"Interval : {IntervalDays} day(s)\n" + + $"Ticks : {TickCount}"; + } + /// + /// Converts a DateTime to an Excel serial number (days since Dec 30, 1899), + /// accounting for Excel's 1900 leap year bug. + /// + public static double ToExcelSerial(DateTime date) + { + double serial = (date - ExcelEpoch).TotalDays; + if (date >= ExcelLeapBugThreshold) + serial += 1; // Excel's legacy off-by-one + return serial; + } + + /// + /// Converts an Excel serial number back to a DateTime. + /// + public static DateTime FromExcelSerial(double serial) + { + if (serial >= 61) // 61 = the phantom Feb 29, 1900 in Excel + serial -= 1; + return ExcelEpoch.AddDays(serial); + } + /// + /// Computes Excel-compatible auto axis min, max, and major unit for a date axis. + /// If the OOXML already contains explicit min/max values, use those directly instead. + /// + public static AxisResult ComputeAutoAxis(DateTime dataMin, DateTime dataMax) + { + double serialMin = ToExcelSerial(dataMin); + double serialMax = ToExcelSerial(dataMax); + + // Step 1: pick a nice major unit + double rawInterval = (serialMax - serialMin) / 10.0; + int interval = NiceIntervals.FirstOrDefault(v => v >= rawInterval); + if (interval == 0) + interval = 730; // fallback for very large ranges + + // Step 2: snap outward to multiples of the interval + double axisMin = Math.Floor(serialMin / interval) * interval; + double axisMax = Math.Ceiling(serialMax / interval) * interval; + // Step 3: expand until we have enough ticks, then re-snap + while ((axisMax - axisMin) / interval < TargetTicks - 1) + { + axisMin -= interval; + axisMax += interval; + } + + axisMin = Math.Floor(axisMin / interval) * interval; + axisMax = Math.Ceiling(axisMax / interval) * interval; + + return new AxisResult + { + AxisMin = FromExcelSerial(axisMin), + AxisMax = FromExcelSerial(axisMax), + IntervalDays = interval, + AxisMinSerial = axisMin, + AxisMaxSerial = axisMax, + }; + } /// /// Calculate major majorUnit based on data range (in days) /// - private static void CalculateMajorUnit(double min, double max,out double majorValue, out double minorUnits, out eTimeUnit majorUnit) + private static void CalculateMajorUnit(double min, double max,out double majorValue, out double axisMin, out double axisMax, out double minorUnits, out eTimeUnit majorUnit) { var dtMin = DateTime.FromOADate(ExcelNormalizeOADate(min)); var dtMax = DateTime.FromOADate(ExcelNormalizeOADate(max)); var rangeDays = (dtMax - dtMin).TotalDays; - // Target approximately 6 intervals - const int targetIntervals = 6; + // Target approximately 10 intervals + const int targetIntervals = 9; var roughUnit = rangeDays / targetIntervals; - + double interval; // Return nice round numbers based on the rough majorUnit - if (rangeDays <= 1) + if (roughUnit < 1) { - majorValue = 1; + interval = majorValue = 1; + majorUnit = eTimeUnit.Days; + } + else if (roughUnit < 2) + { + interval = majorValue = 2; + majorUnit = eTimeUnit.Days; + } + else if (roughUnit < 5) + { + // Multiple days - round to nearest day + interval = majorValue = 5; majorUnit = eTimeUnit.Days; } else if (roughUnit < 7) { // Multiple days - round to nearest day - majorValue = Math.Max(1, Math.Round(roughUnit)); + interval = majorValue = 7; majorUnit = eTimeUnit.Days; } else if (roughUnit < 14) { - majorValue = 7; + interval = majorValue = 14; majorUnit = eTimeUnit.Days; } else if (roughUnit < 21) { - majorValue = 14; + interval = majorValue = 21; majorUnit = eTimeUnit.Days; } else if (roughUnit < 60) { majorValue = 1; + interval = 30; majorUnit = eTimeUnit.Months; } else if (roughUnit < 120) { majorValue = 2; + interval = 60; majorUnit = eTimeUnit.Months; } else if (roughUnit < 180) { majorValue = 3; + interval = 90; majorUnit = eTimeUnit.Months; } else if (roughUnit < 365) { majorValue = 6; + interval = 180; majorUnit = eTimeUnit.Months; } else if (roughUnit < 730) { majorValue = 1; + interval = 365; majorUnit = eTimeUnit.Years; } else if (roughUnit < 1825) { // Multiple years majorValue = Math.Ceiling(roughUnit / 365); + interval = 365 * majorValue; majorUnit = eTimeUnit.Years; } else { // 5-year increments for very long ranges majorValue = Math.Ceiling(roughUnit / 365 / 5) * 5; + interval = 365 * majorValue; majorUnit = eTimeUnit.Years; } + + + axisMin = Math.Floor(min / interval) * interval; + axisMax = Math.Ceiling(max / interval) * interval; + if(axisMax==max) + { + axisMax+= interval; + } + + // Step 3: expand until we have enough ticks, then re-snap + while ((axisMax - axisMin) / interval < TargetTicks - 1 && ((min-axisMin) < interval * 2)) + { + axisMin -= majorValue; + } + minorUnits = 1; } @@ -109,5 +231,51 @@ private static double ExcelNormalizeOADate(double value) { return Math.Min(Math.Max(value, 0), 2958465.99999999); } + + private static DateTime FloorToUnit(double value, eTimeUnit unit, int interval) + { + var date = DateTime.FromOADate(value); + switch (unit) + { + case eTimeUnit.Days: + if(interval % 7 != 0) + { + return date.Date.AddDays(-(((int)date.DayOfWeek + 6) % 7)); + } + else + { + return date; + } + //return date.Date.AddDays(-(date.DayOfYear % interval)); + case eTimeUnit.Months: + return new DateTime(date.Year, date.Month, 1) + .AddMonths(-((date.Month - 1) % interval)); + + case eTimeUnit.Years: + return new DateTime(date.Year - (date.Year % interval), 1, 1); + default: + return date; + }; + } + + private static DateTime CeilingToUnit(double value, eTimeUnit unit, int interval) + { + DateTime floor = FloorToUnit(value, unit, interval); + + var date = DateTime.FromOADate(value); + if (floor == date) return date; // already on boundary + + switch (unit) + { + case eTimeUnit.Days: + return floor.AddDays(interval); + case eTimeUnit.Months: + return floor.AddMonths(interval); + case eTimeUnit.Years: + return floor.AddYears(interval); + default: + return date; + }; + } } } \ No newline at end of file diff --git a/src/EPPlus.Export.ImageRenderer/Svg/Chart/ChartTypeDrawers/LineChartTypeDrawer.cs b/src/EPPlus.Export.ImageRenderer/Svg/Chart/ChartTypeDrawers/LineChartTypeDrawer.cs index d71446e78..d89ddf737 100644 --- a/src/EPPlus.Export.ImageRenderer/Svg/Chart/ChartTypeDrawers/LineChartTypeDrawer.cs +++ b/src/EPPlus.Export.ImageRenderer/Svg/Chart/ChartTypeDrawers/LineChartTypeDrawer.cs @@ -76,14 +76,14 @@ private void AddLine(ExcelChart chartType, ExcelLineChartSerie serie, List Date: Thu, 5 Mar 2026 11:47:57 +0100 Subject: [PATCH 077/151] Added vertical alignments to textbody --- .../ImageRenderer.cs | 31 ++++++++++ .../RenderItems/RenderItem.cs | 47 ++++++++++++++ .../RenderItems/Shared/TextBodyItem.cs | 61 ++++++------------- .../RenderItems/SvgItem/SvgParagraphItem.cs | 4 ++ .../RenderItems/SvgItem/SvgTextBodyItem.cs | 5 ++ 5 files changed, 104 insertions(+), 44 deletions(-) diff --git a/src/EPPlus.Export.ImageRenderer/ImageRenderer.cs b/src/EPPlus.Export.ImageRenderer/ImageRenderer.cs index f4d826171..f6cdbeeb2 100644 --- a/src/EPPlus.Export.ImageRenderer/ImageRenderer.cs +++ b/src/EPPlus.Export.ImageRenderer/ImageRenderer.cs @@ -12,12 +12,15 @@ Date Author Change *************************************************************************************************/ using EPPlus.Export.ImageRenderer; +using EPPlus.Export.ImageRenderer.RenderItems.Independent.Shared; +using EPPlus.Export.ImageRenderer.RenderItems.Independent.SvgItem; using EPPlus.Export.ImageRenderer.RenderItems.Shared; using EPPlus.Export.ImageRenderer.RenderItems.SvgItem; using EPPlus.Export.ImageRenderer.Svg; using EPPlus.Export.ImageRenderer.Svg.NodeAttributes; using EPPlus.Export.ImageRenderer.Svg.Writer; using EPPlus.Fonts.OpenType; +using EPPlus.Fonts.OpenType.Utils; using EPPlus.Graphics; using EPPlusImageRenderer.Svg; using OfficeOpenXml; @@ -27,6 +30,7 @@ Date Author Change using OfficeOpenXml.FormulaParsing.Excel.Functions; using OfficeOpenXml.Utils; using System; +using System.Drawing; using System.IO; using System.Text; @@ -53,6 +57,33 @@ public string RenderDrawingToSvg(ExcelDrawing drawing) throw new NotImplementedException("Image rendering for drawing type not implemented."); } + ////Attempt at ensuring features can be created/tested individually by the system + ////Without relying on having a whole workbook or epplus project. + ////Simply: Does our positioning, sizing and parent hierarchy logic work as expected or not. + //public string RenderIndependentCanvas(double widthPixel, double heightPixel, Color bgColor) + //{ + // var widthPoint = widthPixel.PixelToPoint(); + // var heightPoint = heightPixel.PixelToPoint(); + + // var CanvasBounds = new BoundingBox(widthPoint, heightPoint); + + // var sb = new StringBuilder(); + // var canvas = new SvgIndependentCanvas(CanvasBounds, bgColor); + + // var rect = new SvgIndependentRect(canvas.Bounds, widthPoint/2, heightPoint/2); + // rect.FillColor = "red"; + // rect.Left = 10; + + // //BoundingBox boundsTextBox = new BoundingBox(rect.Bounds.Left, rect.Bounds.Top, rect.Bounds.Width, rect.Bounds.Height); + // //var independentTxtBox = new SvgIndependentTextBox(canvas, boundsTextBox); + + // //var textBox = new svgin + + // canvas.AddRenderItem(rect); + + // canvas.Render(sb); + // return sb.ToString(); + //} //public string RenderTextBox(ExcelDrawing someDrawing, BoundingBox parent, double maxHeight, double maxWidth) diff --git a/src/EPPlus.Export.ImageRenderer/RenderItems/RenderItem.cs b/src/EPPlus.Export.ImageRenderer/RenderItems/RenderItem.cs index a145fda4d..48a12d019 100644 --- a/src/EPPlus.Export.ImageRenderer/RenderItems/RenderItem.cs +++ b/src/EPPlus.Export.ImageRenderer/RenderItems/RenderItem.cs @@ -234,6 +234,53 @@ private string GetFillColor(ExcelDrawingFillBasic fill, ExcelDrawingColorManager // theme = theme; //} } + + internal abstract class RenderItemIndependent : RenderItemBase + { + public string FillColor { get; set; } + public string FilterName { get; set; } + public SvgFillType FillType { get; set; } + public double? FillOpacity { get; set; } + public string BorderColor { get; set; } + public double? BorderWidth { get; set; } + public double[] BorderDashArray { get; set; } + public double? BorderDashOffset { get; set; } + public eLineCap LineCap { get; set; } = eLineCap.Flat; + public SvgLineJoin LineJoin { get; set; } = SvgLineJoin.Miter; + public double? BorderOpacity { get; set; } + public PathFillMode FillColorSource { get; set; } = PathFillMode.Norm; + public PathFillMode BorderColorSource { get; set; } = PathFillMode.Norm; + public double? GlowRadius { get; private set; } + public string GlowColor { get; private set; } + + protected void CloneBase(RenderItem item) + { + item.FillColor = FillColor; + item.FillOpacity = FillOpacity; + item.BorderWidth = BorderWidth; + item.BorderColor = BorderColor; + item.BorderDashArray = BorderDashArray; + item.BorderDashOffset = BorderDashOffset; + item.BorderOpacity = BorderOpacity; + item.LineJoin = LineJoin; + item.LineCap = LineCap; + item.FillColorSource = FillColorSource; + } + + + internal string SetFillColor(Color color) + { + FillColor = GetAdjustedColor(color); + return FillColor; + } + + private string GetAdjustedColor(Color color) + { + var fc = ColorUtils.GetAdjustedColor(FillColorSource, color); + return "#" + fc.ToArgb().ToString("x8").Substring(2); + } + } + /// /// Base class for any item rendered. /// diff --git a/src/EPPlus.Export.ImageRenderer/RenderItems/Shared/TextBodyItem.cs b/src/EPPlus.Export.ImageRenderer/RenderItems/Shared/TextBodyItem.cs index 46c9c3367..2e95f087d 100644 --- a/src/EPPlus.Export.ImageRenderer/RenderItems/Shared/TextBodyItem.cs +++ b/src/EPPlus.Export.ImageRenderer/RenderItems/Shared/TextBodyItem.cs @@ -62,6 +62,7 @@ public TextBodyItem(DrawingBase renderer, BoundingBox parent, double maxWidth, d private FontMeasurerTrueType _measurer = null; internal string _text; + public void ImportParagraph(ExcelDrawingParagraph item, double startingY, string text=null) { var measureFont = item.DefaultRunProperties.GetMeasureFont(); @@ -92,6 +93,11 @@ public void ImportParagraph(ExcelDrawingParagraph item, double startingY, string //SetHorizontalAlignmentPosition(); } + public void AddParagraph(double startingY, string text = null) + { + CreateParagraph(this, Bounds, text); + } + internal void SetHorizontalAlignmentPosition() { //if (AutoSize) @@ -123,49 +129,14 @@ internal void SetHorizontalAlignmentPosition() internal virtual void ImportTextBody(ExcelTextBody body) { _text = null; - //VerticalAlignment = body.Anchor; - VerticalAlignment = eTextAnchoringType.Top; + VerticalAlignment = body.Anchor; + //We already apply bounds top via the parent Transform - double paragraphStartY = GetAlignmentVertical(); + double paragraphStartY = 0; double largestWidth = double.MinValue; foreach (var paragraph in body.Paragraphs) { - //if (paragraph == body.Paragraphs[0]) - //{ - // //For the first line we always add ascent for Top-aligned but this should not be done for center vertical align - // //However as paragraph and textRun should not have to deal with vertical align directly - // //We wish to achieve the effect of not applying dy to the first textrun while not chaning anything - // //thus: we change paragraphStartY to get around it. Should probably be solved in the textrun somehow - // if (VerticalAlignment == eTextAnchoringType.Center && paragraph.TextRuns != null && paragraph.TextRuns.Count > 0) - // { - // var _measurer = paragraph._prd.Package.Settings.TextSettings.GenericTextMeasurerTrueType; - // var spacingValue = GetParagraphAscendantSpacingInPixels( - // paragraph.LineSpacing.LineSpacingType, paragraph.LineSpacing.Value, _measurer, out double multiplier); - - // if(multiplier == -1) - // { - // paragraphStartY -= spacingValue; - // } - // else - // { - // var runFont = paragraph.TextRuns[0].GetMeasurementFont(); - // var startFont = paragraph.DefaultRunProperties.GetMeasureFont(); - // _measurer.SetFont(runFont); - - // paragraphStartY -= multiplier * _measurer.GetBaseLine().PointToPixel(true); - - // //Reset measurer font - // _measurer.SetFont(startFont); - // } - - - // //paragraph.TextRuns[0].GetMeasurementFont() * - // ////var baseLineSize = paragraph.TextRuns[0].FontSize.PointToPixel(); - // ////paragraphStartY = paragraphStartY - baseLineSize; - // } - //} - ImportParagraph(paragraph, paragraphStartY); var addedPara = Paragraphs.Last(); paragraphStartY = addedPara.Bounds.Bottom; @@ -181,6 +152,8 @@ internal virtual void ImportTextBody(ExcelTextBody body) { Bounds.Height = paragraphStartY; } + + Bounds.Top = GetAlignmentVertical(); } private double GetParagraphAscendantSpacingInPixels(eDrawingTextLineSpacing lineSpacingType, double spacingValue, ITextMeasurerWrap fmExact, out double multiplier) { @@ -196,6 +169,7 @@ private double GetParagraphAscendantSpacingInPixels(eDrawingTextLineSpacing line } } + //public void AddText(string text, FontMeasurerTrueType measurer) //{ // if (Paragraphs.Count == 0) @@ -244,18 +218,15 @@ private double GetAlignmentVertical() switch (VerticalAlignment) { case eTextAnchoringType.Top: - alignmentY = 0; + alignmentY = Bounds.Top; break; //Center means center of a Shape's ENTIRE bounding box height. //Not center of the Inset GetRectangle case eTextAnchoringType.Center: - var globalHeight = (DrawingRenderer.Bounds.Height / 2) + Bounds.Top+2; - var adjustedHeight = globalHeight - Bounds.Position.Y; //Global position. - - alignmentY = adjustedHeight; + alignmentY = (MaxHeight - Bounds.Height)/2 + Bounds.Top; break; case eTextAnchoringType.Bottom: - alignmentY = Bounds.Height; + alignmentY = MaxHeight - Bounds.Height; break; } @@ -271,6 +242,8 @@ private double GetAlignmentVertical() /// internal abstract ParagraphItem CreateParagraph(TextBodyItem textBody, ExcelDrawingParagraph paragraph, BoundingBox parent, string textIfEmpty=""); + internal abstract ParagraphItem CreateParagraph(TextBodyItem textBody, BoundingBox parent, string textIfEmpty = ""); + /// /// Each file format defines its own paragraph /// diff --git a/src/EPPlus.Export.ImageRenderer/RenderItems/SvgItem/SvgParagraphItem.cs b/src/EPPlus.Export.ImageRenderer/RenderItems/SvgItem/SvgParagraphItem.cs index ece72c6d3..1da38a042 100644 --- a/src/EPPlus.Export.ImageRenderer/RenderItems/SvgItem/SvgParagraphItem.cs +++ b/src/EPPlus.Export.ImageRenderer/RenderItems/SvgItem/SvgParagraphItem.cs @@ -19,6 +19,10 @@ public SvgParagraphItem(TextBodyItem textBody, DrawingBase renderer, BoundingBox { } + public SvgParagraphItem(TextBodyItem textBody, DrawingBase renderer, BoundingBox parent, string text) : base(textBody, renderer, parent) + { + } + public SvgParagraphItem(TextBodyItem textBody, DrawingBase renderer, BoundingBox parent, ExcelDrawingParagraph p, string textIfEmpty = null) : base(textBody, renderer, parent, p, textIfEmpty) { } diff --git a/src/EPPlus.Export.ImageRenderer/RenderItems/SvgItem/SvgTextBodyItem.cs b/src/EPPlus.Export.ImageRenderer/RenderItems/SvgItem/SvgTextBodyItem.cs index 5310727cb..034bb8192 100644 --- a/src/EPPlus.Export.ImageRenderer/RenderItems/SvgItem/SvgTextBodyItem.cs +++ b/src/EPPlus.Export.ImageRenderer/RenderItems/SvgItem/SvgTextBodyItem.cs @@ -63,5 +63,10 @@ internal override ParagraphItem CreateParagraph(TextBodyItem textBody, ExcelDraw { return new SvgParagraphItem(this, DrawingRenderer, parent, paragraph, textIfEmpty); } + + internal override ParagraphItem CreateParagraph(TextBodyItem textBody, BoundingBox parent, string textIfEmpty = "") + { + return new SvgParagraphItem(this, DrawingRenderer, parent, textIfEmpty); + } } } From 40389bc1eca99c812205d288f4d2e7955454f0f0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jan=20K=C3=A4llman?= Date: Thu, 5 Mar 2026 12:20:21 +0100 Subject: [PATCH 078/151] WIP:Fixed top legend position and Data asix scale --- .../Svg/Chart/Axis/DateAxisScaleCalculator.cs | 3 +-- src/EPPlus.Export.ImageRenderer/Svg/Chart/SvgChartPlotarea.cs | 4 ++-- 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/src/EPPlus.Export.ImageRenderer/Svg/Chart/Axis/DateAxisScaleCalculator.cs b/src/EPPlus.Export.ImageRenderer/Svg/Chart/Axis/DateAxisScaleCalculator.cs index 1f5602b2c..f61730bbb 100644 --- a/src/EPPlus.Export.ImageRenderer/Svg/Chart/Axis/DateAxisScaleCalculator.cs +++ b/src/EPPlus.Export.ImageRenderer/Svg/Chart/Axis/DateAxisScaleCalculator.cs @@ -25,8 +25,7 @@ internal static AxisScale Calculate(double dataMin, double dataMax, double chart { majorValue = axisOptions.LockedInterval.Value; } - //dataMin = FloorToUnit(dataMin, majorUnit, (int)majorValue).ToOADate(); - //dataMax = CeilingToUnit(dataMax, majorUnit, (int)majorValue).ToOADate(); + return new AxisScale { Min = axisMin, diff --git a/src/EPPlus.Export.ImageRenderer/Svg/Chart/SvgChartPlotarea.cs b/src/EPPlus.Export.ImageRenderer/Svg/Chart/SvgChartPlotarea.cs index 9a56fc4a8..4d50cd5fc 100644 --- a/src/EPPlus.Export.ImageRenderer/Svg/Chart/SvgChartPlotarea.cs +++ b/src/EPPlus.Export.ImageRenderer/Svg/Chart/SvgChartPlotarea.cs @@ -39,12 +39,12 @@ internal SvgRenderRectItem GetPlotAreaRectangle(SvgChart sc) else { var lp = sc.Chart.Legend?.Position; - rect.Top = (lp==eLegendPosition.Top ? sc.Legend.Rectangle.GlobalBottom : sc.Title?.Rectangle?.GlobalBottom ?? 0d) + TopMargin; + rect.Top = (lp==eLegendPosition.Top ? sc.Legend.Bounds.Bottom : sc.Title?.Rectangle?.GlobalBottom ?? 0d) + TopMargin; if(sc.HorizontalAxis!=null && sc.Chart.XAxis.LabelPosition==eTickLabelPosition.High) { rect.Top += sc.HorizontalAxis.Rectangle.Height; } - rect.Left = lp == eLegendPosition.Left ? sc.Legend.Rectangle.GlobalRight + LeftMargin : LeftMargin; + rect.Left = lp == eLegendPosition.Left ? sc.Legend.Bounds.Right + LeftMargin : LeftMargin; if(sc.VerticalAxis!=null) { rect.Left = sc.VerticalAxis.Rectangle?.GlobalRight ?? sc.VerticalAxis.Title.Rectangle.GlobalRight; From d54191d735d8934571583818af49d03c7db329d6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ossian=20Edstr=C3=B6m?= Date: Thu, 5 Mar 2026 14:52:37 +0100 Subject: [PATCH 079/151] Added multiple tests to verify vertical alignment --- .../SvgPathTests.cs | 2 +- .../TextRenderTests.cs | 147 ++++++++++-------- .../RenderItems/SvgItem/SvgTextBox.cs | 2 +- .../Svg/SvgShape.cs | 4 +- 4 files changed, 89 insertions(+), 66 deletions(-) diff --git a/src/EPPlus.Export.ImageRenderer.Test/SvgPathTests.cs b/src/EPPlus.Export.ImageRenderer.Test/SvgPathTests.cs index 9713d20b0..586704f68 100644 --- a/src/EPPlus.Export.ImageRenderer.Test/SvgPathTests.cs +++ b/src/EPPlus.Export.ImageRenderer.Test/SvgPathTests.cs @@ -578,7 +578,7 @@ public void GenerateShapeCenteredParagraph() _currentShape.TextBody.RightInsert = 0; _currentShape.TextBody.LeftInsert = 0; - var para1 = _currentShape.TextBody.Paragraphs.Add("TextBox\r\na"); + var para1 = _currentShape.TextBody.Paragraphs.Add("TextBodySvg\r\na"); //var test = _currentShape.TextBody.AnchorCenter; para1.LeftMargin = 5; diff --git a/src/EPPlus.Export.ImageRenderer.Test/TextRenderTests.cs b/src/EPPlus.Export.ImageRenderer.Test/TextRenderTests.cs index d21ae4cad..7753bd27f 100644 --- a/src/EPPlus.Export.ImageRenderer.Test/TextRenderTests.cs +++ b/src/EPPlus.Export.ImageRenderer.Test/TextRenderTests.cs @@ -95,7 +95,7 @@ List GetWrappedText(ExcelDrawingTextRunCollection runs, TextFrag [TestMethod] public void TextFragmentHandlesEndLines() { - string strWEndLines = "TextBox\r\na"; + string strWEndLines = "TextBodySvg\r\na"; List inputFrags = new List() { strWEndLines }; var textFragments = new TextFragmentCollection(inputFrags); @@ -220,20 +220,19 @@ public void VerifyTextRunBounds() cube.SetPixelWidth(5000); var svgShape = new SvgShape(cube); - - SvgTextBodyItem tbItem = svgShape.TextBox; + SvgTextBodyItem tbItem = svgShape.TextBodySvg; var txtRun1Bounds = tbItem.Paragraphs[0].Runs[0].Bounds; var pxWidth = txtRun1Bounds.Width.PointToPixel(); - Assert.AreEqual(43.835286458333336d, pxWidth); + Assert.AreEqual(43.835286458333336d, pxWidth, 0.2d); var txtRuns2 = tbItem.Paragraphs[1].Runs; - Assert.AreEqual(53.20963541666667d, txtRuns2[0].Bounds.Width.PointToPixel()); + Assert.AreEqual(53.20963541666667d, txtRuns2[0].Bounds.Width.PointToPixel(),0.2); var currentLineWidth = txtRuns2[0].Bounds.Width.PointToPixel(); - Assert.AreEqual(currentLineWidth, txtRuns2[1].Bounds.Left.PointToPixel()); - Assert.AreEqual(69.55924479166667d, txtRuns2[1].Bounds.Width.PointToPixel()); + Assert.AreEqual(currentLineWidth, txtRuns2[1].Bounds.Left.PointToPixel(),0.2); + Assert.AreEqual(69.55924479166667d, txtRuns2[1].Bounds.Width.PointToPixel(),0.2); currentLineWidth += txtRuns2[1].Bounds.Width.PointToPixel(); Assert.AreEqual(currentLineWidth, txtRuns2[2].Bounds.Left.PointToPixel(),0.0001); @@ -252,84 +251,108 @@ public void VerifyTextRunBounds() } } - [TestMethod] - public void ConceptLines() + private ExcelShape GenerateTextShapeWithDifficultText(ExcelPackage p) { - var currentChar = 'a'; - var defaultFont = new MeasurementFont - { - FontFamily = "Aptos Narrow", - Size = 11, - Style = MeasurementFontStyles.Regular - }; + var myCulture = CultureInfo.CurrentCulture; - List manyRichText = new List(); - List manyFonts = new List(); + var ws = p.Workbook.Worksheets.Add("ShapeSheet"); - for (int i = 0; i< 20; i++) - { - manyRichText.Add(currentChar.ToString()); - manyFonts.Add(defaultFont); - currentChar++; - defaultFont.Size ++; - } + var cube = ws.Drawings.AddShape("myCube", eShapeStyle.Cube); + + cube.Fill.Style = eFillStyle.SolidFill; + cube.Fill.Color = System.Drawing.Color.BlueViolet; + cube.Font.Color = System.Drawing.Color.Goldenrod; - var fragments = new TextFragmentCollection(manyRichText); - var fontMeasurer = new FontMeasurerTrueType(); + cube.TextBody.TopInsert = 0; + cube.TextBody.BottomInsert = 0; + cube.TextBody.RightInsert = 0; + cube.TextBody.LeftInsert = 0; - var strings = fontMeasurer.WrapMultipleTextFragments(fragments, manyFonts, 30d.PixelToPoint()); + var para1 = cube.TextBody.Paragraphs.Add("TextBodySvg\r\na"); - var outputLines = fragments.GetOutputLines(); + var para2 = cube.TextBody.Paragraphs.Add("TextBox2"); + para2.TextRuns[0].FontItalic = true; + para2.TextRuns[0].FontBold = true; + para2.TextRuns.Add("ra underline").FontUnderLine = eUnderLineType.Dash; + para2.TextRuns.Add("La Strike").FontStrike = eStrikeType.Single; + var tRun1 = para2.TextRuns.Add("Goudy size 16"); + tRun1.SetFromFont("Goudy Stout", 16); + tRun1.Fill.Color = System.Drawing.Color.IndianRed; + var tRun2 = para2.TextRuns.Add("SvgSize 24"); + tRun2.FontSize = 24; - //var paragraph = new TextParagraph(fragments, manyFonts); + cube.TextBody.HorizontalTextOverflow = eTextHorizontalOverflow.Clip; + cube.TextBody.VerticalTextOverflow = eTextVerticalOverflow.Clip; - //List manyRichText = new List() {"a","b","c","d","e","f","g" }; + var aFont = cube.Font; + var paragraph0 = cube.TextBody.Paragraphs[0]; + + var autofit = cube.TextBody.TextAutofit; + + cube.ChangeCellAnchor(eEditAs.Absolute); + + cube.GetSizeInPixels(out int testWidth, out int testHeight); + + cube.SetPixelHeight(400); + cube.SetPixelWidth(400); + + return cube; } + [TestMethod] - public void ConceptTextRun() + public void VerifyVerticalAlignTop() { - string rt1 = "My richtext1\r\n of len"; - string rt2 = "gth beyond and then I am richtext2"; + ExcelPackage.License.SetNonCommercialOrganization("EPPlus Project"); - string combined = rt1 + rt2; + using var p = new ExcelPackage(); + var shape = GenerateTextShapeWithDifficultText(p); - int charMax = 10; + shape.TextAnchoring = eTextAnchoringType.Top; - int lineCharCount = 0; + var svgShape = new SvgShape(shape); + SvgTextBodyItem tbItem = svgShape.TextBodySvg; - List lines = new List(); + Assert.AreEqual(100, tbItem.Bounds.GlobalTop.PointToPixel()); + } - for (int i = 0; i < combined.Length; i++) - { - if(lineCharCount > charMax) - { - var currLine = combined.Substring(i - lineCharCount, lineCharCount); - lines.Add(currLine); - lineCharCount = 0; - } + [TestMethod] + public void VerifyVerticalAlignCenter() + { + ExcelPackage.License.SetNonCommercialOrganization("EPPlus Project"); - if (combined[i] == '\r') - { - var currLine = combined.Substring(i - lineCharCount, lineCharCount); - lines.Add(currLine); - lineCharCount = 0; - i++; - continue; - } + using var p = new ExcelPackage(); + var shape = GenerateTextShapeWithDifficultText(p); - lineCharCount++; - } + shape.TextAnchoring = eTextAnchoringType.Center; - var finalLine = combined.Substring(combined.Length - lineCharCount, lineCharCount); - lines.Add(finalLine); + var svgShape = new SvgShape(shape); + SvgTextBodyItem tbItem = svgShape.TextBodySvg; - foreach (string line in lines) - { - Debug.WriteLine(line); - } + //Appears off by 1-2 px bc of border width + Assert.AreEqual(190d, tbItem.Bounds.GlobalTop.PointToPixel(),1.0); + } + + + [TestMethod] + public void VerifyVerticalAlignBottom() + { + ExcelPackage.License.SetNonCommercialOrganization("EPPlus Project"); + + using var p = new ExcelPackage(); + var shape = GenerateTextShapeWithDifficultText(p); + + shape.TextAnchoring = eTextAnchoringType.Bottom; + + var svgShape = new SvgShape(shape); + SvgTextBodyItem tbItem = svgShape.TextBodySvg; + + var ir = new EPPlusImageRenderer.ImageRenderer(); + var svg = ir.RenderDrawingToSvg(shape); + //Appears off by ~2 px because of border width + Assert.AreEqual(278d, tbItem.Bounds.GlobalTop.PointToPixel(), 1.0); } } } diff --git a/src/EPPlus.Export.ImageRenderer/RenderItems/SvgItem/SvgTextBox.cs b/src/EPPlus.Export.ImageRenderer/RenderItems/SvgItem/SvgTextBox.cs index efb7e0d0b..8039c56b9 100644 --- a/src/EPPlus.Export.ImageRenderer/RenderItems/SvgItem/SvgTextBox.cs +++ b/src/EPPlus.Export.ImageRenderer/RenderItems/SvgItem/SvgTextBox.cs @@ -128,7 +128,7 @@ internal override void AppendRenderItems(List renderItems) var textboxGroupItem = new SvgGroupItem(DrawingRenderer); renderItems.Add(textboxGroupItem); - var titleItem = new SvgTitleItem(DrawingRenderer, "TextBox Rect"); + var titleItem = new SvgTitleItem(DrawingRenderer, "TextBodySvg Rect"); renderItems.Add(titleItem); renderItems.Add(rect); diff --git a/src/EPPlus.Export.ImageRenderer/Svg/SvgShape.cs b/src/EPPlus.Export.ImageRenderer/Svg/SvgShape.cs index 383820a4c..159b68a06 100644 --- a/src/EPPlus.Export.ImageRenderer/Svg/SvgShape.cs +++ b/src/EPPlus.Export.ImageRenderer/Svg/SvgShape.cs @@ -47,7 +47,7 @@ internal class SvgShape : DrawingShape /// /// Textbox from memory /// - public SvgTextBodyItem TextBox { get; internal set; } + public SvgTextBodyItem TextBodySvg { get; internal set; } public SvgShape(ExcelShape shape) : base(shape) { @@ -114,7 +114,7 @@ public SvgShape(ExcelShape shape) : base(shape) InsetTextBox.FillOpacity = 0.3d; - TextBox = CreateTextBodyItem(_shape.TextBody); + TextBodySvg = CreateTextBodyItem(_shape.TextBody); } } } From 7889faed4d87983820bb10958277a1a07581e20e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ossian=20Edstr=C3=B6m?= Date: Fri, 6 Mar 2026 15:47:30 +0100 Subject: [PATCH 080/151] Added start of adding datalabels --- .../SvgPathTests.cs | 15 +++++ .../SvgItem/SvgChartDatalabelItem.cs | 11 ++++ .../SvgItem/SvgChartSerieDataLabel.cs | 59 +++++++++++++++++++ .../ChartTypeDrawers/LineChartTypeDrawer.cs | 19 +++++- .../Svg/Chart/SvgChart.cs | 2 + 5 files changed, 104 insertions(+), 2 deletions(-) create mode 100644 src/EPPlus.Export.ImageRenderer/RenderItems/SvgItem/SvgChartDatalabelItem.cs create mode 100644 src/EPPlus.Export.ImageRenderer/RenderItems/SvgItem/SvgChartSerieDataLabel.cs diff --git a/src/EPPlus.Export.ImageRenderer.Test/SvgPathTests.cs b/src/EPPlus.Export.ImageRenderer.Test/SvgPathTests.cs index 586704f68..a352b1d5f 100644 --- a/src/EPPlus.Export.ImageRenderer.Test/SvgPathTests.cs +++ b/src/EPPlus.Export.ImageRenderer.Test/SvgPathTests.cs @@ -542,6 +542,21 @@ public void GenerateSimplestChart() } + [TestMethod] + public void GenerateDataLabels() + { + ExcelPackage.License.SetNonCommercialOrganization("EPPlus Project"); + using (var p = OpenTemplatePackage("datalabelsSvg.xlsx")) + { + var c = p.Workbook.Worksheets[0].Drawings[0]; + + var renderer = new EPPlusImageRenderer.ImageRenderer(); + var svg = renderer.RenderDrawingToSvg(c); + SaveTextFileToWorkbook($"svg\\datalabelsAttempt.svg", svg); + } + } + + [TestMethod] public void OpenRightAligned() { diff --git a/src/EPPlus.Export.ImageRenderer/RenderItems/SvgItem/SvgChartDatalabelItem.cs b/src/EPPlus.Export.ImageRenderer/RenderItems/SvgItem/SvgChartDatalabelItem.cs new file mode 100644 index 000000000..3d45bba34 --- /dev/null +++ b/src/EPPlus.Export.ImageRenderer/RenderItems/SvgItem/SvgChartDatalabelItem.cs @@ -0,0 +1,11 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; + +namespace EPPlus.Export.ImageRenderer.RenderItems.SvgItem +{ + internal class SvgChartDatalabelItem + { + } +} diff --git a/src/EPPlus.Export.ImageRenderer/RenderItems/SvgItem/SvgChartSerieDataLabel.cs b/src/EPPlus.Export.ImageRenderer/RenderItems/SvgItem/SvgChartSerieDataLabel.cs new file mode 100644 index 000000000..11ddfeaba --- /dev/null +++ b/src/EPPlus.Export.ImageRenderer/RenderItems/SvgItem/SvgChartSerieDataLabel.cs @@ -0,0 +1,59 @@ +using EPPlus.Export.ImageRenderer.RenderItems.Shared; +using EPPlus.Export.ImageRenderer.RenderItems.SvgItem; +using EPPlus.Export.ImageRenderer.Svg.Chart.Util; +using EPPlus.Fonts.OpenType; +using EPPlus.Fonts.OpenType.Tables.Cmap; +using EPPlus.Fonts.OpenType.Utils; +using EPPlus.Graphics; +using EPPlusImageRenderer.RenderItems; +using EPPlusImageRenderer.Svg; +using OfficeOpenXml.Drawing; +using OfficeOpenXml.Drawing.Chart; +using OfficeOpenXml.Drawing.Chart.Style; +using OfficeOpenXml.Style; +using OfficeOpenXml.Style.XmlAccess; +using OfficeOpenXml.Utils.String; +using OfficeOpenXml.Utils.TypeConversion; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; + + +namespace EPPlus.Export.ImageRenderer.RenderItems.SvgItem +{ + internal class SvgChartSerieDataLabel : SvgChartObject + { + List dlblTextBoxes = new List(); + + public SvgChartSerieDataLabel(SvgChart chart, ExcelChartSerieDataLabel dlblSerie, BoundingBox maxBounds) : base(chart) + { + foreach(var dlbl in dlblSerie.DataLabels) + { + var txtBox = new SvgTextBox(chart, maxBounds, maxBounds); + txtBox.ImportTextBody(dlbl.TextBody); + dlblTextBoxes.Add(txtBox); + } + } + + internal override void AppendRenderItems(List renderItems) + { + SvgGroupItem groupItem = new SvgGroupItem(DrawingRenderer, Bounds); + renderItems.Add(groupItem); + + foreach (var renderItem in dlblTextBoxes) + { + renderItem.AppendRenderItems(renderItems); + } + + renderItems.Add(new SvgEndGroupItem(DrawingRenderer, Bounds)); + } + + //public override RenderItemType Type => throw new NotImplementedException(); + + //public override void Render(StringBuilder sb) + //{ + // throw new NotImplementedException(); + //} + } +} diff --git a/src/EPPlus.Export.ImageRenderer/Svg/Chart/ChartTypeDrawers/LineChartTypeDrawer.cs b/src/EPPlus.Export.ImageRenderer/Svg/Chart/ChartTypeDrawers/LineChartTypeDrawer.cs index d89ddf737..b31de8c4f 100644 --- a/src/EPPlus.Export.ImageRenderer/Svg/Chart/ChartTypeDrawers/LineChartTypeDrawer.cs +++ b/src/EPPlus.Export.ImageRenderer/Svg/Chart/ChartTypeDrawers/LineChartTypeDrawer.cs @@ -1,4 +1,5 @@ -using EPPlusImageRenderer.RenderItems; +using EPPlus.Export.ImageRenderer.RenderItems.SvgItem; +using EPPlusImageRenderer.RenderItems; using EPPlusImageRenderer.Svg; using OfficeOpenXml.Drawing.Chart; using OfficeOpenXml.Utils.TypeConversion; @@ -20,14 +21,23 @@ internal LineChartTypeDrawer(SvgChart svgChart, ExcelChart chartType) : base(svg var isPercentStacked = Chart.IsTypePercentStacked(); var xValues = new List>(); var yValues = new List>(); + var serieDataLabels = new List(); + foreach (ExcelLineChartSerie serie in chartType.Series) { var yValue = LoadSeriesValues(serie.Series, serie.NumberLiteralsY, serie.StringLiteralsY); var xValue = LoadSeriesValues(serie.XSeries, serie.NumberLiteralsX, serie.StringLiteralsX); + xValues.Add(xValue); yValues.Add(yValue); + + if (serie.HasDataLabel) + { + var datalabel = new SvgChartSerieDataLabel(svgChart, serie.DataLabel, svgChart.Plotarea.Bounds); + serieDataLabels.Add(datalabel); + } } - + if (Chart.IsTypeStacked()) { SumSeries(yValues); @@ -45,6 +55,11 @@ internal LineChartTypeDrawer(SvgChart svgChart, ExcelChart chartType) : base(svg AddLine(chartType, serie, xSerie, ySerie); } + foreach(var dataLabel in serieDataLabels) + { + dataLabel.AppendRenderItems(RenderItems); + } + RenderItems.Add(new SvgEndGroupItem(ChartRenderer, null)); } private void SumSeries(List> series) diff --git a/src/EPPlus.Export.ImageRenderer/Svg/Chart/SvgChart.cs b/src/EPPlus.Export.ImageRenderer/Svg/Chart/SvgChart.cs index e6f54ad42..761b50b3d 100644 --- a/src/EPPlus.Export.ImageRenderer/Svg/Chart/SvgChart.cs +++ b/src/EPPlus.Export.ImageRenderer/Svg/Chart/SvgChart.cs @@ -61,6 +61,8 @@ public SvgChart(ExcelChart chart) : base(chart) { Plotarea.ChartTypeDrawers = ChartTypeDrawer.Create(this); } + + } private void SetAxisPositionsFromPlotarea(SvgChart sc) From 64cd380b437a835ab977ea2db91666b69e6f8014 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jan=20K=C3=A4llman?= Date: Mon, 9 Mar 2026 10:28:47 +0100 Subject: [PATCH 081/151] WIP:Merge with develop8. Right align vertical axis textboxes --- appveyor8.yml | 10 +- docs/api/index.md | 5 + docs/articles/fixedissues.md | 20 + docs/docfx.json | 6 +- docs/{templates/epplus => images}/favicon.ico | Bin docs/{templates/epplus => images}/logo.png | Bin docs/index.md | 2 +- docs/templates/epplus/layout/_master.tmpl | 55 -- docs/templates/epplus/partials/_logo.liquid | 8 - .../epplus/partials/logo.tmpl.partial | 5 - docs/templates/epplus/public/favicon.ico | Bin 0 -> 32038 bytes docs/templates/epplus/public/logo.png | Bin 0 -> 3644 bytes docs/templates/epplus/styles/main.css | 14 + docs/templates/epplus/template.json | 3 + docs/toc.yml | 2 +- .../Svg/Chart/SvgChartAxis.cs | 17 +- .../CellPictures/CellPictureReferenceCache.cs | 29 + .../CellPictures/CellPicturesManager.cs | 113 +++- src/EPPlus/CellPictures/LocalImageCacheKey.cs | 6 + src/EPPlus/CellPictures/WebPictureCacheKey.cs | 6 + src/EPPlus/Constants/ExtLstUris.cs | 10 +- src/EPPlus/Core/ChangableDictionary.cs | 2 +- .../ExcelPowerQueryMetaDataEntry.cs | 8 +- .../QueryTable/ExcelQueryTableCollection.cs | 2 +- src/EPPlus/Drawing/Chart/ChartDataSource.cs | 497 ++++++++++++++++++ .../Drawing/Chart/ExcelBarChartSerie.cs | 2 +- src/EPPlus/Drawing/Chart/ExcelChart.cs | 1 + src/EPPlus/Drawing/Chart/ExcelChartAxis.cs | 2 +- .../Drawing/Chart/ExcelChartDataLabel.cs | 26 +- .../Chart/ExcelChartDataLabelCollection.cs | 18 +- .../Drawing/Chart/ExcelChartDataLabelItem.cs | 8 + .../Chart/ExcelChartDataLabelStandard.cs | 45 +- .../Drawing/Chart/ExcelChartSerieDataLabel.cs | 142 ++++- .../Drawing/Chart/ExcelChartStandardSerie.cs | 351 ++++++++----- .../Drawing/Chart/ExcelLineChartSerie.cs | 5 +- src/EPPlus/Drawing/ExcelDrawing.cs | 4 +- src/EPPlus/Drawing/ExcelImageBase.cs | 1 + .../ImageHandling/GenericImageHandler.cs | 1 + .../Drawing/ImageHandling/ImageReader.cs | 8 - .../Drawing/Style/Text/ExcelTextBody.cs | 9 +- src/EPPlus/EPPlus.csproj | 15 +- src/EPPlus/ExcelCalculationCacheSettings.cs | 42 ++ src/EPPlus/ExcelPackage.cs | 3 + src/EPPlus/ExcelPackageSettings.cs | 17 + src/EPPlus/ExcelWorkbook.cs | 31 ++ src/EPPlus/ExcelWorksheet.cs | 39 +- .../FormulaParsing/CalculateExtensions.cs | 69 ++- .../DependencyChain/ArrayFormulaOutput.cs | 85 ++- .../DependencyChain/RpnFormulaExecution.cs | 46 +- .../FormulaParsing/EpplusExcelDataProvider.cs | 17 +- .../Excel/Functions/ExcelFunction.cs | 3 +- .../Functions/MathFunctions/AverageIf.cs | 4 +- .../Functions/MathFunctions/AverageIfs.cs | 25 +- .../Excel/Functions/MathFunctions/CountIfs.cs | 24 +- .../MathFunctions/RangeCriteriaCache.cs | 244 +++++++++ .../MathFunctions/RangeCriteriaFunction.cs | 111 +++- .../Excel/Functions/MathFunctions/SumIf.cs | 4 +- .../Excel/Functions/MathFunctions/SumIfs.cs | 29 +- .../Excel/Functions/RefAndLookup/HLookup.cs | 2 +- .../LookupUtils/XlookupScanner.cs | 14 +- .../Excel/Functions/RefAndLookup/VLookup.cs | 2 +- .../Operators/RangeOperationsOperator.cs | 208 +++++++- .../FormulaParsing/ExcelCalculationOption.cs | 23 + .../ExcelUtilities/RangeFlattener.cs | 13 +- .../CustomArrayBehaviourCompiler.cs | 71 ++- .../FormulaExpressions/FunctionExpression.cs | 4 +- .../FormulaExpressions/LambdaCalculator.cs | 2 +- .../LexicalAnalysis/FormulaAddress.cs | 63 +++ src/EPPlus/FormulaParsing/ParsingContext.cs | 16 +- .../FormulaParsing/Ranges/InMemoryRange.cs | 214 ++++++-- .../FormulaParsing/Ranges/RangeHelper.cs | 50 ++ src/EPPlus/NumberFormatToTextArgs.cs | 46 +- src/EPPlus/RichData/RichDataStore.cs | 11 + .../RichData/RichValues/ExcelRichValue.cs | 2 + src/EPPlus/Style/Dxf/ExcelDxfStyle.cs | 3 +- src/EPPlus/Style/ExcelTextFont.cs | 1 + .../Style/Interfaces/IExcelNumberFormat.cs | 26 + src/EPPlus/Style/RichText/ExcelParagraph.cs | 2 + .../RichText/ExcelParagraphCollection.cs | 9 +- .../XmlAccess/ExcelNumberFormatWithoutId.cs | 23 + .../Style/XmlAccess/ExcelNumberFormatXml.cs | 13 +- .../Calculation/PivotTableCalculation.cs | 195 ++++--- .../PivotTableColumnCalculation.cs | 307 ++++++----- .../Table/PivotTable/ExcelPivotTable.cs | 49 +- .../PivotTable/PivotTableCacheInternal.cs | 16 +- src/EPPlus/Utils/String/ValueToTextHandler.cs | 9 +- src/EPPlus/XmlHelper.cs | 33 ++ src/EPPlusTest/Data/ConnectionTests.cs | 20 +- .../Drawing/Chart/ChartSeriesTest.cs | 217 +++++++- src/EPPlusTest/Drawing/OLETests.cs | 2 +- .../FormulaParsing/CancelCalculationTests.cs | 200 +++++++ .../FormulaParsing/EntireColumnTests.cs | 233 ++++++++ .../MathFunctions/AverageIfsTests.cs | 18 +- .../CriteriaRangeFunctionsTests.cs | 44 ++ .../Functions/RangeCriteriaCacheTests.cs | 234 +++++++++ .../GetPivotDataTests_CaluclatedFields2.cs | 335 ++++++++++++ .../RangeOperationsOperatorTests.cs | 13 + .../InCellImages/InCellImagesCacheTests.cs | 90 +++- src/EPPlusTest/InCellImages/WebImagesTests.cs | 154 ++++++ src/EPPlusTest/Issues/DefinedNameIssues.cs | 2 +- src/EPPlusTest/Issues/DrawingIssues.cs | 30 ++ .../Issues/FormulaCalculationIssues.cs | 1 + src/EPPlusTest/Issues/PackageIssues.cs | 6 + src/EPPlusTest/Issues/RangeTextIssues.cs | 21 + src/EPPlusTest/Issues/StylingIssues.cs | 91 ++++ src/EPPlusTest/Issues/WorksheetIssues.cs | 18 + src/EPPlusTest/VBA/VBATests.cs | 90 ++++ 107 files changed, 4745 insertions(+), 752 deletions(-) create mode 100644 docs/api/index.md rename docs/{templates/epplus => images}/favicon.ico (100%) rename docs/{templates/epplus => images}/logo.png (100%) delete mode 100644 docs/templates/epplus/layout/_master.tmpl delete mode 100644 docs/templates/epplus/partials/_logo.liquid delete mode 100644 docs/templates/epplus/partials/logo.tmpl.partial create mode 100644 docs/templates/epplus/public/favicon.ico create mode 100644 docs/templates/epplus/public/logo.png create mode 100644 docs/templates/epplus/styles/main.css create mode 100644 docs/templates/epplus/template.json create mode 100644 src/EPPlus/Drawing/Chart/ChartDataSource.cs create mode 100644 src/EPPlus/ExcelCalculationCacheSettings.cs create mode 100644 src/EPPlus/FormulaParsing/Excel/Functions/MathFunctions/RangeCriteriaCache.cs create mode 100644 src/EPPlus/FormulaParsing/Ranges/RangeHelper.cs create mode 100644 src/EPPlus/Style/Interfaces/IExcelNumberFormat.cs create mode 100644 src/EPPlus/Style/XmlAccess/ExcelNumberFormatWithoutId.cs create mode 100644 src/EPPlusTest/FormulaParsing/CancelCalculationTests.cs create mode 100644 src/EPPlusTest/FormulaParsing/EntireColumnTests.cs create mode 100644 src/EPPlusTest/FormulaParsing/Excel/Functions/RangeCriteriaCacheTests.cs create mode 100644 src/EPPlusTest/FormulaParsing/Excel/Functions/RefAndLookup/GetPivotData/GetPivotDataTests_CaluclatedFields2.cs diff --git a/appveyor8.yml b/appveyor8.yml index b264d4fa3..561865cce 100644 --- a/appveyor8.yml +++ b/appveyor8.yml @@ -1,4 +1,4 @@ -version: 8.4.2.{build} +version: 8.5.0.{build} branches: only: - develop8 @@ -10,15 +10,15 @@ install: & $env:temp\dotnet-install.ps1 -Architecture x64 -Version '10.0.100' -InstallDir "$env:ProgramFiles\dotnet" init: - ps: >- - Update-AppveyorBuild -Version "8.4.2.$env:appveyor_build_number-$(Get-Date -format yyyyMMdd)-$env:appveyor_repo_branch" + Update-AppveyorBuild -Version "8.5.0.$env:appveyor_build_number-$(Get-Date -format yyyyMMdd)-$env:appveyor_repo_branch" - Write-Host "8.4.2.$env:appveyor_build_number-$(Get-Date -format yyyyMMdd)-$env:appveyor_repo_branch" + Write-Host "8.5.0.$env:appveyor_build_number-$(Get-Date -format yyyyMMdd)-$env:appveyor_repo_branch" dotnet_csproj: patch: true file: '**\*.csproj' version: '{version}' - assembly_version: 8.4.2.{build} - file_version: 8.4.2.{build} + assembly_version: 8.5.0.{build} + file_version: 8.5.0.{build} nuget: project_feed: true before_build: diff --git a/docs/api/index.md b/docs/api/index.md new file mode 100644 index 000000000..4cb7408d0 --- /dev/null +++ b/docs/api/index.md @@ -0,0 +1,5 @@ +# EPPlus API Documentation + +This section contains the full API reference for EPPlus, generated from the XML comments in the source code. + +Browse the namespaces in the sidebar to explore the available classes and members. \ No newline at end of file diff --git a/docs/articles/fixedissues.md b/docs/articles/fixedissues.md index 40ab5376a..568447f26 100644 --- a/docs/articles/fixedissues.md +++ b/docs/articles/fixedissues.md @@ -1,4 +1,24 @@ # Features / Fixed issues - EPPlus 8 +## Version 8.5.0 +### Minor Features +* Added ´CancellationToken´ to Calculate - see [Cancelling a calculation](https://github.com/EPPlusSoftware/EPPlus/wiki/Cancelling-a-calculation). +* Added property ´ValueFromCellsRange´ and the ´SetValueFromCellsRange´ method to data labels on chart series. +* Improved performance for SUMIFS, AVERAGEIFS, COUNTIFS. +* Improved performance for formula calculation with full column references (e.g. A:A, $B:$B). +### Fixed issues +* Deleting pictures in cells, caused a corrupt workbook in rare cases. +* Fixed several formatting issues when reading and writing tables with checkboxes in them. +* Fixed several issues when using a custom ExcelPackage.Workbook.NumberFormatToTextHandler: +* Fixed unary minus coercion on dynamic array results. +* Fixed off-by-one in ´InsertAndShift´ resize condition. This caused an ´ArgumentException´ when calling ´InsertColumn´ on xlsx files where the number of defined columns was at the array boundary. +* Fixed culture-specific DateTime in PowerQuery metadata. +* Calculated field computation in pivot tables did not work correctly in some cases. +* Copying a Connection shape from one workbook to another caused an unhandled exception. +* Having the wrong content type/extension on an image in the workbook package, caused an unhandled exception on load. +Breaking Change: +* Setting dataLabelPosition.Top on BarCharts corrupted the excel file when saved. Trying to set this now throws an error instead. +* NumberFormatToTextArgs.NumberFormat now returns the interface IExcelNumberFormat rather than the ExcelNumberFormatXml class. All public variables remain the same and it can be safely cast to ´ExcelNumberFormatXml´ as long as ´Package.Workbook.NumberFormatToTextHandler´ is null. + ## Version 8.4.2 * Fixed an issue where the ExcelRange.Text property could format negative values with double minus signs in rare cases. * Fixed a performance issue when using FollowDependencyChain = false in the Calculate method. With this option, EPPlus no longer calculates dependent cells or dynamic array formula spills. diff --git a/docs/docfx.json b/docs/docfx.json index 46ca25481..db1ed279c 100644 --- a/docs/docfx.json +++ b/docs/docfx.json @@ -64,8 +64,10 @@ "default", "templates/epplus" ], - "globalMetadata":{ - "_disableContribution":true + "globalMetadata": { + "_disableContribution": true, + "_appLogoPath": "images/logo.png", + "_appFaviconPath": "images/favicon.ico" }, "postProcessors": [], "markdownEngineName": "markdig", diff --git a/docs/templates/epplus/favicon.ico b/docs/images/favicon.ico similarity index 100% rename from docs/templates/epplus/favicon.ico rename to docs/images/favicon.ico diff --git a/docs/templates/epplus/logo.png b/docs/images/logo.png similarity index 100% rename from docs/templates/epplus/logo.png rename to docs/images/logo.png diff --git a/docs/index.md b/docs/index.md index 8f4bceaa2..8c026daf7 100644 --- a/docs/index.md +++ b/docs/index.md @@ -4,4 +4,4 @@ EPPlus is a .NET Framework/.NET Core library for managing Office Open XML spread The API documentation provided is generated from the XML comments in the EPPlus source code. It is updated when we releases new versions of the library. ### Breaking changes -See this list of [breaking changes](articles/breakingchanges.html) in version 5. +See this list of [breaking changes](articles/breakingchanges.md) in version 5. diff --git a/docs/templates/epplus/layout/_master.tmpl b/docs/templates/epplus/layout/_master.tmpl deleted file mode 100644 index 92346f7b3..000000000 --- a/docs/templates/epplus/layout/_master.tmpl +++ /dev/null @@ -1,55 +0,0 @@ -{{!Copyright (c) Microsoft. All rights reserved. Licensed under the MIT license. See LICENSE file in the project root for full license information.}} -{{!include(/^styles/.*/)}} -{{!include(/^fonts/.*/)}} -{{!include(favicon.ico)}} -{{!include(logo.png)}} -{{!include(search-stopwords.json)}} - - - - {{>partials/head}} - -
-
- {{^_disableNavbar}} - {{>partials/navbar}} - {{/_disableNavbar}} - {{^_disableBreadcrumb}} - {{>partials/breadcrumb}} - {{/_disableBreadcrumb}} -
- {{#_enableSearch}} -
- {{>partials/searchResults}} -
- {{/_enableSearch}} -
- {{^_disableToc}} - {{>partials/toc}} -
- {{/_disableToc}} - {{#_disableToc}} -
- {{/_disableToc}} - {{#_disableAffix}} -
- {{/_disableAffix}} - {{^_disableAffix}} -
- {{/_disableAffix}} -
- {{!body}} -
-
- {{^_disableAffix}} - {{>partials/affix}} - {{/_disableAffix}} -
-
- {{^_disableFooter}} - {{>partials/footer}} - {{/_disableFooter}} -
- {{>partials/scripts}} - - diff --git a/docs/templates/epplus/partials/_logo.liquid b/docs/templates/epplus/partials/_logo.liquid deleted file mode 100644 index f42733674..000000000 --- a/docs/templates/epplus/partials/_logo.liquid +++ /dev/null @@ -1,8 +0,0 @@ -{% comment -%}Copyright (c) Microsoft. All rights reserved. Licensed under the MIT license. See LICENSE file in the project root for full license information.{% endcomment -%} - - {%- if _appLogoPath -%} - {{_appName}} - {%- else -%} - {{_appName}} - {%- endif -%} - diff --git a/docs/templates/epplus/partials/logo.tmpl.partial b/docs/templates/epplus/partials/logo.tmpl.partial deleted file mode 100644 index 04386bfe0..000000000 --- a/docs/templates/epplus/partials/logo.tmpl.partial +++ /dev/null @@ -1,5 +0,0 @@ -{{!Copyright (c) Microsoft. All rights reserved. Licensed under the MIT license. See LICENSE file in the project root for full license information.}} - - - {{_appName}} - diff --git a/docs/templates/epplus/public/favicon.ico b/docs/templates/epplus/public/favicon.ico new file mode 100644 index 0000000000000000000000000000000000000000..a3a799985c43bc7309d701b2cad129023377dc71 GIT binary patch literal 32038 zcmeHwX>eTEbtY7aYbrGrkNjgie?1jXjZ#zP%3n{}GObKv$BxI7Sl;Bwl5E+Qtj&t8 z*p|m4DO#HoJC-FyvNnp8NP<{Na0LMnTtO21(rBP}?EAiNjWgeO?z`{3ZoURUQlV2d zY1Pqv{m|X_oO91|?^z!6@@~od!@OH>&BN;>c@O+yUfy5w>LccTKJJ&`-k<%M^Zvi( z<$dKp=jCnNX5Qa+M_%6g|IEv~4R84q9|7E=|Ho(Wz3f-0wPjaRL;W*N^>q%^KGRr7 zxbjSORb_c&eO;oV_DZ7ua!sPH=0c+W;`vzJ#j~-x3uj};50#vqo*0w4!LUqs*UCh9 zvy2S%$#8$K4EOa&e@~aBS65_hc~Mpu=454VT2^KzWqEpBA=ME|O;1cn?8p<+{MKJf zbK#@1wzL44m$k(?85=Obido7=C|xWKe%66$z)NrzRwR>?hK?_bbwT z@Da?lBrBL}Zemo1@!9pYRau&!ld17h{f+UV0sY(R{ET$PBB|-=Nr@l-nY6w8HEAw* zRMIQU`24Jl_IFEPcS=_HdrOP5yf81z_?@M>83Vv65$QFr9nPg(wr`Ke8 zaY4ogdnMA*F7a4Q1_uXadTLUpCk;$ZPRRJ^sMOch;rlbvUGc1R9=u;dr9YANbQ<4Z z#P|Cp9BP$FXNPolgyr1XGt$^lFPF}rmBF5rj1Kh5%dforrP8W}_qJL$2qMBS-#%-|s#BPZBSETsn_EBYcr(W5dq( z@f%}C|iN7)YN`^)h7R?Cg}Do*w-!zwZb9=BMp%Wsh@nb22hA zA{`wa8Q;yz6S)zfo%sl08^GF`9csI9BlGnEy#0^Y3b);M+n<(}6jziM7nhe57a1rj zC@(2ISYBL^UtWChKzVWgf%4LW2Tqg_^7jMw`C$KvU+mcakFjV(BGAW9g%CzSyM;Df z143=mq0oxaK-H;o>F3~zJ<(3-j&?|QBn)WJfP#JR zRuA;`N?L83wQt78QIA$(Z)lGQY9r^SFal;LB^qi`8%8@y+mwcGsf~nv)bBy2S7z~9 z=;X@Gglk)^jpbNz?1;`!J3QUfAOp4U$Uxm5>92iT`mek#$>s`)M>;e4{#%HAAcb^8_Ax%ersk|}# z0bd;ZPu|2}18KtvmIo8`1@H~@2ejwo(5rFS`Z4&O{$$+ch2hC0=06Jh`@p+p8LZzY z&2M~8T6X^*X?yQ$3N5EzRv$(FtSxhW>>ABUyp!{484f8(%C1_y)3D%Qgfl_!sz`LTXOjR&L!zPA0qH_iNS!tY{!^2WfD%uT}P zI<~&?@&))5&hPPHVRl9);TPO>@UI2d!^ksb!$9T96V(F){puTsn(}qt_WXNw4VvHj zf;6A_XCvE`Z@}E-IOaG0rs>K>^=Sr&OgT_p;F@v0VCN0Y$r|Lw1?Wjt`AKK~RT*kJ z2>QPuVgLNcF+XKno;WBv$yj@d_WFJbl*#*V_Cwzo@%3n5%z4g21G*PVZ)wM5$A{klYozmGlB zT@u2+s}=f}25%IA!yNcXUr!!1)z(Nqbhojg0lv@7@0UlvUMT)*r;M$d0-t)Z?B1@qQk()o!4fqvfr_I0r7 zy1(NdkHEj#Yu{K>T#We#b#FD=c1XhS{hdTh9+8gy-vkcdkk*QS@y(xxEMb1w6z<^~ zYcETGfB#ibR#ql0EiD;PR$L&Vrh2uRv5t_$;NxC;>7_S5_OXxsi8udY3BUUdi55Sk zcyKM+PQ9YMA%D1kH1q48OFG(Gbl=FmV;yk8o>k%0$rJ8%-IYsHclnYuTskkaiCGkUlkMY~mx&K}XRlKIW;odWIeuKjtbc^8bBOTqK zjj(ot`_j?A6y_h%vxE9o*ntx#PGrnK7AljD_r58ylE*oy@{IY%+mA^!|2vW_`>`aC{#3`#3;D_$^S^cM zRcF+uTO2sICledvFgNMU@A%M)%8JbSLq{dD|2|2Sg8vvh_uV6*Q?F&rKaV{v_qz&y z`f;stIb?Cb2!Cg7CG91Bhu@D@RaIrq-+o+T2fwFu#|j>lD6ZS9-t^5cx>p|?flqUA z;Cgs#V)O#`Aw4$Kr)L5?|7f4izl!;n0jux}tEW$&&YBXz9o{+~HhoiYDJ`w5BVTl&ARya=M7zdy$FEe}iGBur8XE>rhLj&_yDk5D4n2GJZ07u7%zyAfNtOLn;)M?h*Py-Xtql5aJOtL4U8e|!t? z((sc6&OJXrPdVef^wZV&x=Z&~uA7^ix8rly^rEj?#d&~pQ{HN8Yq|fZ#*bXn-26P^ z5!)xRzYO9{u6vx5@q_{FE4#7BipS#{&J7*>y}lTyV94}dfE%Yk>@@pDe&F7J09(-0|wuI|$of-MRfK51#t@t2+U|*s=W; z!Y&t{dS%!4VEEi$efA!#<<7&04?kB}Soprd8*jYv;-Qj~h~4v>{XX~kjF+@Z7<t?^|i z#>_ag2i-CRAM8Ret^rZt*^K?`G|o>1o(mLkewxyA)38k93`<~4VFI?5VB!kBh%NNU zxb8K(^-MU1ImWQxG~nFB-Un;6n{lQz_FfsW9^H$Xcn{;+W^ZcG$0qLM#eNV=vGE@# z1~k&!h4@T|IiI<47@pS|i?Qcl=XZJL#$JKve;booMqDUYY{(xcdj6STDE=n?;fsS1 ze`h~Q{CT$K{+{t+#*I1=&&-UU8M&}AwAxD-rMa=e!{0gQXP@6azBq9(ji11uJF%@5 zCvV`#*?;ZguQ7o|nH%bm*s&jLej#@B35gy32ZAE0`Pz@#j6R&kN5w{O4~1rhDoU zEBdU)%Nl?8zi|DR((u|gg~r$aLYmGMyK%FO*qLvwxK5+cn*`;O`16c!&&XT{$j~5k zXb^fbh1GT-CI*Nj{-?r7HNg=e3E{6rxuluPXY z5Nm8ktc$o4-^SO0|Es_sp!A$8GVwOX+%)cH<;=u#R#nz;7QsHl;J@a{5NUAmAHq4D zIU5@jT!h?kUp|g~iN*!>jM6K!W5ar0v~fWrSHK@})@6Lh#h)C6F6@)&-+C3(zO! z8+kV|B7LctM3DpI*~EYo>vCj>_?x&H;>y0*vKwE0?vi$CLt zfSJB##P|M2dEUDBPKW=9cY-F;L;h3Fs4E2ERdN#NSL7ctAC z?-}_a{*L@GA7JHJudxtDVA{K5Yh*k(%#x4W7w+^ zcb-+ofbT5ieG+@QG2lx&7!MyE2JWDP@$k`M;0`*d+oQmJ2A^de!3c53HFcfW_Wtv< zKghQ;*FifmI}kE4dc@1y-u;@qs|V75Z^|Q0l0?teobTE8tGl@EB?k#q_wUjypJ*R zyEI=DJ^Z+d*&}B_xoWvs27LtH7972qqMxVFcX9}c&JbeNCXUZM0`nQIkf&C}&skSt z^9fw@b^Hb)!^hE2IJq~~GktG#ZWwWG<`@V&ckVR&r=JAO4YniJewVcG`HF;59}=bf zLyz0uxf6MhuSyH#-^!ZbHxYl^mmBVrx) zyrb8sQ*qBd_WXm9c~Of$&ZP$b^)<~0%nt#7y$1Jg$e}WCK>TeUB{P>|b1FAB?%K7>;XiOfd}JQ`|IP#Vf%kVy zXa4;XFZ+>n;F>uX&3|4zqWK2u3c<>q;tzjsb1;d{u;L$-hq3qe@82(ob<3qom#%`+ z;vzYAs7TIMl_O75BXu|r`Qhc4UT*vN$3Oo0kAC!{f2#HexDy|qUpgTF;k{o6|L>7l z=?`=*LXaow1o;oNNLXsGTrvC)$R&{m=94Tf+2iTT3Y_Or z-!;^0a{kyWtO4vksG_3cyc7HQ0~detf0+2+qxq(e1NS251N}w5iTSrM)`0p8rem!j zZ56hGD=pHI*B+dd)2B`%|9f0goozCSeXPw3 z+58k~sI02Yz#lOneJzYcG)EB0|F+ggC6D|B`6}d0khAK-gz7U3EGT|M_9$ZINqZjwf>P zJCZ=ogSoE`=yV5YXrcTQZx@Un(64*AlLiyxWnCJ9I<5Nc*eK6eV1Mk}ci0*NrJ=t| zCXuJG`#7GBbPceFtFEpl{(lTm`LX=B_!H+& z>$*Hf}}y zkt@nLXFG9%v**s{z&{H4e?aqp%&l#oU8lxUxk2o%K+?aAe6jLojA& z_|J0<-%u^<;NT*%4)n2-OdqfctSl6iCHE?W_Q2zpJken#_xUJlidzs249H=b#g z?}L4-Tnp6)t_5X?_$v)vz`s9@^BME2X@w<>sKZ3=B{%*B$T5Nj%6!-Hr;I!Scj`lH z&2dHFlOISwWJ&S2vf~@I4i~(0*T%OFiuX|eD*nd2utS4$1_JM?zmp>a#CsVy6Er^z zeNNZZDE?R3pM?>~e?H_N`C`hy%m4jb;6L#8=a7l>3eJS2LGgEUxsau-Yh9l~o7=Yh z2mYg3`m5*3Ik|lKQf~euzZlCWzaN&=vHuHtOwK!2@W6)hqq$Zm|7`Nmu%9^F6UH?+ z@2ii+=iJ;ZzhiUKu$QB()nKk3FooI>Jr_IjzY6=qxYy;&mvi7BlQ?t4kRjIhb|2q? zd^K~{-^cxjVSj?!Xs=Da5IHmFzRj!Kzh~b!?`P7c&T9s77VLYB?8_?F zauM^)p;qFG!9PHLfIsnt43UnmV?Wn?Ki7aXSosgq;f?MYUuSIYwOn(5vWhb{f%$pn z4ySN-z}_%7|B);A@PA5k*7kkdr4xZ@s{e9j+9w;*RFm;XPDQwx%~;8iBzSKTIGKO z{53ZZU*OLr@S5=k;?CM^i#zkxs3Sj%z0U`L%q`qM+tP zX$aL;*^g$7UyM2Go+_4A+f)IQcy^G$h2E zb?nT$XlgTEFJI8GN6NQf%-eVn9mPilRqUbT$pN-|;FEjq@Ao&TxpZg=mEgBHB zU@grU;&sfmqlO=6|G3sU;7t8rbK$?X0y_v9$^{X`m4jZ_BR|B|@?ZCLSPPEzz`w1n zP5nA;4(kQFKm%$enjkkBxM%Y}2si&d|62L)U(dCzCGn56HN+i#6|nV-TGIo0;W;`( zW-y=1KF4dp$$mC_|6}pbb>IHoKQeZajXQB>jVR?u`R>%l1o54?6NnS*arpVopdEF; zeC5J3*M0p`*8lif;!irrcjC?(uExejsi~>4wKYwstGY^N@KY}TujLx`S=Cu+T=!dx zKWlPm->I**E{A*q-Z^FFT5$G%7Ij0_*Mo4-y6~RmyTzUB&lfae(WZfO>um}mnsDXPEbau-!13!!xd!qh*{C)6&bz0j1I{>y$D-S)b*)JMCPk!=~KL&6Ngin0p6MCOxF2L_R9t8N!$2Wpced<#`y!F;w zKTi5V_kX&X09wAIJ#anfg9Dhn0s7(C6Nj3S-mVn(i|C6ZAVq0$hE)874co};g z^hR7pe4lU$P;*ggYc4o&UTQC%liCXooIfkI3TNaBV%t~FRr}yHu7kjQ2J*3;e%;iW zvDVCh8=G80KAeyhCuY2LjrC!Od1rvF7h}zszxGV)&!)6ChP5WAjv-zQAMNJIG!JHS zwl?pLxC-V5II#(hQ`l)ZAp&M0xd4%cxmco*MIk?{BD=BK`1vpc}D39|XlV z{c&0oGdDa~TL2FT4lh=~1NL5O-P~0?V2#ie`v^CnANfGUM!b4F=JkCwd7Q`c8Na2q zJGQQk^?6w}Vg9-{|2047((lAV84uN%sK!N2?V(!_1{{v6rdgZl56f0zDMQ+q)jKzzu^ztsVken;=DjAh6G`Cw`Q4G+BjS+n*=KI~^K{W=%t zbD-rN)O4|*Q~@<#@1Vx$E!0W9`B~IZeFn87sHMXD>$M%|Bh93rdGf1lKoX3K651t&nhsl= zXxG|%@8}Bbrlp_u#t*DZX<}_0Yb{A9*1Pd_)LtqNwy6xT4pZrOY{s?N4)pPwT(i#y zT%`lRi8U#Ken4fw>H+N`{f#FF?ZxFlLZg7z7#cr4X>id z{9kUD`d2=w_Zlb{^c`5IOxWCZ1k<0T1D1Z31IU0Q2edsZ1K0xv$pQVYq2KEp&#v#Z z?{m@Lin;*Str(C2sfF^L>{R3cjY`~#)m>Wm$Y|1fzeS0-$(Q^z@} zEO*vlb-^XK9>w&Ef^=Zzo-1AFSP#9zb~X5_+){$(eB4K z8gtW+nl{q+CTh+>v(gWrsP^DB*ge(~Q$AGxJ-eYc1isti%$%nM<_&Ev?%|??PK`$p z{f-PM{Ym8k<$$)(F9)tqzFJ?h&Dk@D?Dt{4CHKJWLs8$zy6+(R)pr@0ur)xY{=uXFFzH_> z-F^tN1y(2hG8V)GpDg%wW0Px_ep~nIjD~*HCSxDi0y`H!`V*~RHs^uQsb1*bK1qGpmd zB1m`Cjw0`nLBF2|umz+a#2X$c?Lj;M?Lj;MUp*d>7j~ayNAyj@SLpeH`)BgRH}byy zyQSat!;U{@O(<<2fp&oQkIy$z`_CQ-)O@RN;QD9T4y|wIJ^%U#(BF%=`i49}j!D-) zkOwPSJaG03SMkE~BzW}b_v>LA&y)EEYO6sbdnTX*$>UF|JhZ&^MSb4}Tgbne_4n+C zwI8U4i~PI>7a3{kVa8|))*%C0|K+bIbmV~a`|G#+`TU#g zXW;bWIcWsQi9c4X*RUDpIfyoPY)2bI-r9)xulm1CJDkQd6u+f)_N=w1ElgEBjprPF z3o?Ly0RVeY_{3~fPVckRMxe2lM8hj!B8F)JO z!`AP6>u>5Y&3o9t0QxBpNE=lJx#NyIbp1gD zzUYBIPYHIv9ngk-Zt~<)62^1Zs1LLYMh@_tP^I7EX-9)Ed0^@y{k65Gp0KRcTmMWw zU|+)qx{#q0SL+4q?Q`i0>COIIF8a0Cf&C`hbMj?LmG9K&iW-?PJt*u)38tTXAP>@R zZL6uH^!RYNq$p>PKz7f-zvg>OKXcZ8h!%Vo@{VUZp|+iUD_xb(N~G|6c#oQK^nHZU zKg#F6<)+`rf~k*Xjjye+syV{bwU2glMMMs-^ss4`bYaVroXzn`YQUd__UlZL_mLs z(vO}k!~(mi|L+(5&;>r<;|OHnbXBE78LruP;{yBxZ6y7K3)nMo-{6PCI7gQi6+rF_ zkPod!Z8n}q46ykrlQS|hVB(}(2Kf7BCZ>Vc;V>ccbk2~NGaf6wGQH@W9&?Zt3v(h*P4xDrN>ex7+jH*+Qg z%^jH$&+*!v{sQ!xkWN4+>|b}qGvEd6ANzgqoVy5Qfws}ef2QqF{iiR5{pT}PS&yjo z>lron#va-p=v;m>WB+XVz|o;UJFdjo5_!RRD|6W{4}A2a#bZv)gS_`b|KsSH)Sd_JIr%<%n06TX&t{&!H#{)?4W9hlJ`R1>FyugOh3=D_{einr zu(Wf`qTkvED+gEULO0I*Hs%f;&=`=X4;N8Ovf28x$A*11`dmfy2=$+PNqX>XcG`h% zJY&A6@&)*WT^rC(Caj}2+|X|6cICm5h0OK0cGB_!wEKFZJU)OQ+TZ1q2bTx9hxnq& z$9ee|f9|0M^)#E&Pr4)f?o&DMM4w>Ksb{hF(0|wh+5_{vPow{V%TFzU2za&gjttNi zIyR9qA56dX52Qbv2aY^g`U7R43-p`#sO1A=KS2aKgfR+Yu^bQ*i-qu z%0mP;Ap)B~zZgO9lG^`325gOf?iUHF{~7jyGC)3L(eL(SQ70VzR~wLN18tnx(Cz2~ zctBl1kI)wAe+cxWHw*NW-d;=pd+>+wd$a@GBju*wFvabSaPtHiT!o#QFC+wBVwYo3s=y;z1jM+M=Fj!FZM>UzpL-eZzOT( zhmZmEfWa=%KE#V3-ZK5#v!Hzd{zc^{ctF~- z>DT-U`}5!fk$aj24`#uGdB7r`>oX5tU|d*b|N3V1lXmv%MGrvE(dXG)^-J*LA>$LE z7kut4`zE)v{@Op|(|@i#c>tM!12FQh?}PfA0`Bp%=%*RiXVzLDXnXtE@4B)5uR}a> zbNU}q+712pIrM`k^odG8dKtG$zwHmQI^c}tfjx5?egx3!e%JRm_64e+>`Ra1IRfLb z1KQ`SxmH{cZfyVS5m(&`{V}Y4j6J{b17`h6KWqZ&hfc(oR zxM%w!$F(mKy05kY&lco3%zvLCxBW+t*rxO+i=qGMvobx0-<7`VUu)ka`){=ew+Ovt zg%52_{&UbkUA8aJPWsk)gYWV4`dnxI%s?7^fGpq{ZQuu=VH{-t7w~K%_E<8`zS;V- zKTho*>;UQQul^1GT^HCt@I-q?)&4!QDgBndn?3sNKYKCQFU4LGKJ$n@Je$&w9@E$X z^p@iJ(v&`1(tq~1zc>0Vow-KR&vm!GUzT?Eqgnc)leZ9p)-Z*C!zqb=-$XG0 z^!8RfuQs5s>Q~qcz92(a_Q+KH?C*vCTr~UdTiR`JGuNH8v(J|FTiSEcPrBpmHRtmd zI2Jng0J=bXK);YY^rM?jzn?~X-Pe`GbAy{D)Y6D&1GY-EBcy%Bq?bKh?A>DD9DD!p z?{q02wno2sraGUkZv5dx+J8)&K$)No43Zr(*S`FEdL!4C)}WE}vJd%{S6-3VUw>Wp z?Aasv`T0^%P$2vE?L+Qhj~qB~K%eW)xH(=b_jU}TLD&BP*Pc9hz@Z=e0nkpLkWl}> z_5J^i(9Z7$(XG9~I3sY)`OGZ#_L06+Dy4E>UstcP-rU@xJ$&rxvo!n1Ao`P~KLU-8 z{zDgN4-&A6N!kPSYbQ&7sLufi`YtE2uN$S?e&5n>Y4(q#|KP!cc1j)T^QrUXMPFaP z_SoYO8S8G}Z$?AL4`;pE?7J5K8yWqy23>cCT2{=-)+A$X^-I9=e!@J@A&-;Ufc)`H}c(VI&;0x zrrGv()5mjP%jXzS{^|29?bLNXS0bC%p!YXI!;O457rjCEEzMkGf~B3$T}dXBO23tP z+Ci>;5UoM?C@bU@f9G1^X3=ly&ZeFH<@|RnOG--A&)fd)AUgjw?%izq{p(KJ`EP0v z2mU)P!+3t@X14DA=E2RR-|p${GZ9ETX=d+kJRZL$nSa0daI@&oUUxnZg0xd_xu>Vz lzF#z5%kSKX?YLH3ll^(hI(_`L*t#Iva2Ede*Z;>H_Px#1ZP1_K>z@;j|==^1poj532;bRa{vGi!~g&e!~vBn4jTXf4be$NK~!i%)mjOh zPvshZ{RS}W{R2@tprkcf$Eg2-%QVwX*9+sdOMupzhWVL8$J}9i)=b1Xx)55S zCSt;Cqh9$Ms9UZ&qRUrDao+&!PK?9WW4~fY!U60)8H>HA4&vyk!$3|pXe9^zuA@uC zjz})t`PNK+W1I=+(35)LB@SJuBR}#jT^^BTB2cANc~mS}4i!p-piI#cC|;x(O86B+ zNxwjI4Dv4lKYIIGNJ>pcMs_CBveI!rBNfRP&*SvP6eM0ai(@H?IGlU}2hSYC!L!GZ zkaPq{O#!^TXq-J5QF!{~F-9KN#Q9Lm!XC-!3}!@q}AIDImXPf7;pMfem& zlY1UVkC>`2Kid?|OItaY z%<(A|hN^UM_25c~3aN_Pp;b{cxC-tFs$ibxnzHFTw~SnTumQgu+=Ar9gBI=d zBj5~w59joec(mZWz>WX@w+f=`4^bC zo(9OPKK@n&^qj(kjLB#g8s92>wdNpW|-iO9A7{`r+-)c%Zb}(AxqGU{U}C$$hzgA~H%cmB{r)4YJh+u= z6<`)`7NeQQwRRzsIfEVp705t_naFbFl~Wx}^!2-pzY63?2@r(>%r*U5bjR?Adc(_( z_5`xv1G3?8+(tSwkaI0!Gf96b%~?5FZZ8>8h>*0#rzCJ4oG2=-Fxbg8OIZ1 znKVgVO~TOH9$N5>d^HNts7D5%B^@ekjNgp`3rAta=I^Wsh|b13~>H5MW+~P#P2JR#QBfj5f|xl-zkyi)2Mm9KQ9z!1Jc;i$zFP^a9TsKNE9(fZDY z=Ci!$LrVX+Zw>Yx+Ged^86strLYnoNAGR@WV)%+LF=+PN)=gT}g}IO&$Y5ttlYY_l zAI1F6qfIo0r@tDAKsvBxWDK6D)gpJDT%^#<+`b4ixBZBpb}XjwWSDhr<|=xcx!T5* ze7C9ltZE=;-U1h9VL*{0WrE3qO}%FrDhe-<(vudA1^kPf-;E5L3Hze~tBJ>JHb?uY zR;CgVj-NYagkb!-Z*Yp~0jwNEd6%az4H_8bhMU&ij%YZrbHtZj$DwodBj(g6t7f3@ zj5p|EQs-)H5IsdPshf=B=P@*_Q{A??sY3X4?QDoy@l@KItD3JDq87{hI@OwD>#H-( zskqZeQDbB~WU#J-Deu|JYQ?_oQWD&%WX79bYYz~>4gdCTK_R~ zs`EGRVCv#e0a3=z-Zu?ZElS+#jVM?HSe@J+WAp`_?Aak}OdD{LYb9i83oEfoDGS?i zMp!Z^0fh}#)RkD#xR=XC?Y2t2?lc1gGK=GWF=^u(eKa{(#W?Jy!2dGd^X<_bvP&#=r^7nsOi z{lxhsleLeWO+XwwK6QQvSgL@ngE5K=^Qq=tF#FjN=KeB6?nja&-;vzg3fq{PBip&v z{S{$$^=BedNuy%Ylqja_D}~TvK?q}+BGt&-MLNhPh1MO}fyS(Eg$F0~z)$Qh&4z`f zSBqWE8CzQ`nuQ%D+Zgn(ZPCm=dAU5%pd98NImCM%r6hr65YF;MM)qph0Uhm5GoA8T zM5TufMQs~xA=;~>@TRub!xBc0lyBNV++n21qKGE+&li)h=h zJtjUe*nIzxrJtL~wVm1r^Ec1G!N`c?jKn<^YN7c=dWX5@wi+Ush%pz5T1?vCv+E4) zUmHaSH_WZZn^&%9=5}Mokk8#3bwmm|9jE;&<%K^IGEBdyj&q6Tx7Fmvfd^qMgc9!%IbW(*(FAmT1eTlJOcyZhIR7v(x2*f;Dm0tW4H>N7 ztxCZK7i*+~x_~R-<{uCKsT)=eYgr-t+-lo<3rJ^@eLAWorf*$fwx=8v*w6eGQ3%=Y z{|BxIK7Wa!(c<|%PN$v2+MZLeVDD;-STTi-8)eak!l|Gtc@1vS9o1Mg3Q?c5N3gb# z@_=iT^670_fxeBOHRrSiyN0d6xr__g+mEevA;5*cHTYE&0I^N4C-Q>XKJ|+;_-x%A z^rQTlQbQH5Dqlg#Om7g_3aou^C1TjaxVk90g<8*3aVR+Xf%z$lEx@){XQ2g??y{Xr zOv&$_%+jO~;N_yky4@;gJTRzLFPvA|U_;5#-`fu!+Qi^d*EFedUv7K6X#W~)IlSGJ zG`*O6HoZd0cA6~u>79rBB9#VZvX>dH%9lxQJDW1WU(&AYbbTSg>VD`&Z!a$lYu(HI znjbl;P6g__Uc{=05p=z&nwdcLPBa&Mo{QD2UK=|2HSr*pXkUQUu68w@jd2G97mUKR ztv{I8=fA!-2cb0l#s3V&4|`V`DyF=ZOaeHlq8cG!NFC``_=x+5uurByrS!eyjf2A*cKrQcV?ZnFA) z(lmrEgYM4@?F5}j_{~uBqIw+S2_L;YWY)koB1ik>_`>e6R}A%PG&h@+X}Tvn-3W$P^^iLZ zU9JW#F!4Lczc@leBjG_~oENACp%{6r9Z+rWUJI4j8b^dgn*UED^l97?n~v_m+}+E} z+H6;=g?aZmYV{19BdwL7xA*y)DPTAiZY8hXWgJn4z0Lhq+F(}4;rK72$0KRKWV(<5 O0000 GetAxisValueTextBoxes() maxWidth = Rectangle.Width / AxisValues.Count; maxHeight = SvgChart.ChartArea.Bounds.Height / 3; //TODO: Check this value. } + double widest=0; for (var i = 0; i < AxisValues.Count; i++) { var v = AxisValues[i]; @@ -322,9 +323,23 @@ private List GetAxisValueTextBoxes() //tb.TextBody.Paragraphs[0].AddText(v, Axis.Font); tb.Rectangle.SetDrawingPropertiesFill(Axis.Fill, axisStyle.FillReference.Color); + + if(widest < tb.Width) + { + widest = tb.Width; + } ret.Add(tb); } - //ret[0].TextBody.SetHorizontalAlignmentPosition(); + + if(Axis.IsVertical) + { + //If the axis is vertical, we need to adjust the left position of the textboxes to align them to the right and not have them overlap with the axis line. + foreach(var tb in ret) + { + tb.Left += (widest - tb.Width); + } + } + return ret; } diff --git a/src/EPPlus/CellPictures/CellPictureReferenceCache.cs b/src/EPPlus/CellPictures/CellPictureReferenceCache.cs index 786477fa1..fb2fc1992 100644 --- a/src/EPPlus/CellPictures/CellPictureReferenceCache.cs +++ b/src/EPPlus/CellPictures/CellPictureReferenceCache.cs @@ -10,6 +10,7 @@ Date Author Change ************************************************************************************************* 11/11/2024 EPPlus Software AB Initial release EPPlus 8 *************************************************************************************************/ +using OfficeOpenXml.RichData; using System; using System.Collections.Generic; @@ -25,6 +26,22 @@ private void OnLastReferenceRemoved(uint vmId) LastReferenceRemoved?.Invoke(this, new LastReferenceRemovedEventArgs(vmId)); } + public static PictureCacheKey CreateKey(ExcelCellPicture picture) + { + if (picture.PictureType == ExcelCellPictureTypes.LocalImage) + { + return new LocalImageCacheKey(picture); + } + else if (picture.PictureType == ExcelCellPictureTypes.WebImage) + { + return new WebPictureCacheKey(picture); + } + else + { + throw new ArgumentException("Invalid pictureType: " + picture.PictureType.ToString()); + } + } + public bool Contains(PictureCacheKey pictureCacheKey, out uint vmId) { vmId = uint.MaxValue; @@ -34,6 +51,7 @@ public bool Contains(PictureCacheKey pictureCacheKey, out uint vmId) { vmId = _referenceCache[key].VmId; } + return result; } @@ -43,6 +61,17 @@ public bool Contains(PictureCacheKey pictureCacheKey) return _referenceCache.ContainsKey(key); } + public int GetNumberOfReferences(PictureCacheKey pictureCacheKey) + { + int refNum = 0; + var key = pictureCacheKey.Key; + if (_referenceCache.ContainsKey(key)) + { + refNum = _referenceCache[key].NumberOfReferences; + } + return refNum; + } + public void Add(PictureCacheKey pictureCacheKey, uint vmId) { var key = pictureCacheKey.Key; diff --git a/src/EPPlus/CellPictures/CellPicturesManager.cs b/src/EPPlus/CellPictures/CellPicturesManager.cs index e3c98ba9e..9cc2a526d 100644 --- a/src/EPPlus/CellPictures/CellPicturesManager.cs +++ b/src/EPPlus/CellPictures/CellPicturesManager.cs @@ -11,6 +11,7 @@ Date Author Change 11/11/2024 EPPlus Software AB Initial release EPPlus 8 *************************************************************************************************/ using OfficeOpenXml.Drawing; +using OfficeOpenXml.FormulaParsing.Excel.Functions.DateAndTime; using OfficeOpenXml.FormulaParsing.Excel.Functions.RefAndLookup; using OfficeOpenXml.RichData; using OfficeOpenXml.RichData.IndexRelations; @@ -160,19 +161,51 @@ public void SetCellPicture(int row, int col, byte[] imageBytes, string altText, var imageUri = UriHelper.ResolvePartUri(rdUri, imageInfo.Uri); var cacheKey = new LocalImageCacheKey(imageUri, calcOrigin, altText); + + if (_referenceCache.Contains(cacheKey, out uint cachedVmId)) { - if(existingPic != null && existingPic.ImageUri.OriginalString != imageUri.OriginalString) + if(existingPic != null) { var cKey = GetCacheKey(existingPic); - _referenceCache.RemoveReference(cKey, out int numberOfRefsLeft); - if(numberOfRefsLeft <= 0) + + //Happens if calculate has deleted the ref + if (_richDataStore.HasValueBeenDeleted(cachedVmId)) + { + _referenceCache.RemoveReference(cKey, out int numRefs); + return; + } + + if (existingPic.ImageUri.OriginalString != imageUri.OriginalString) + { + _referenceCache.RemoveReference(cKey, out int numberOfRefsLeft); + if (numberOfRefsLeft <= 0) + { + _richDataStore.DeleteRichData(row, col); + } + _pictureStore.RemoveReference(existingPic.ImageUri); + AddReferenceToPicture(row, col, cacheKey, cachedVmId); + } + else { - _richDataStore.DeleteRichData(row, col); + //There is no need to do anything. + //This method is attempting to set the picture to the exact same as what is already in the cell + return; + } + } + else + { + //Happens if calculate has deleted the ref + if (_richDataStore.HasValueBeenDeleted(cachedVmId)) + { + _referenceCache.RemoveReference(cacheKey, out int numRefs); + return; + } + else + { + AddReferenceToPicture(row, col, cacheKey, cachedVmId); } - _pictureStore.RemoveReference(existingPic.ImageUri); } - AddReferenceToPicture(row, col, cacheKey, cachedVmId); return; } @@ -216,9 +249,38 @@ public void SetWebPicture(int row, int col, Uri addressUri, byte[] imageBytes, s var rdUri = new Uri(ExcelRichValueCollection.PART_URI_PATH, UriKind.Relative); var imageUri = UriHelper.ResolvePartUri(rdUri, imageInfo.Uri); + var existingPic = GetCellPicture(row, col, StructureTypes.WebImage); + if (existingPic == null) + { + existingPic = GetCellPicture(row, col); + } + if (_referenceCache.Contains(cacheKey, out uint cachedVmId)) { - AddReferenceToPicture(row, col, cacheKey, cachedVmId); + if (existingPic != null) + { + if(existingPic.ImageUri.OriginalString != imageUri.OriginalString) + { + var cKey = GetCacheKey(existingPic); + _referenceCache.RemoveReference(cKey, out int numberOfRefsLeft); + if (numberOfRefsLeft <= 0) + { + _richDataStore.DeleteRichData(row, col); + } + _pictureStore.RemoveReference(existingPic.ImageUri); + AddReferenceToPicture(row, col, cacheKey, cachedVmId); + } + else + { + //There is no need to do anything. + //This method is attempting to set the picture to the exact same as what is already in the cell + return; + } + } + else + { + AddReferenceToPicture(row, col, cacheKey, cachedVmId); + } return; } @@ -230,7 +292,6 @@ public void SetWebPicture(int row, int col, Uri addressUri, byte[] imageBytes, s } else { - var existingPic = GetCellPicture(row, col); if (existingPic != null) { if (existingPic.ImageUri.OriginalString == imageUri.OriginalString) @@ -297,6 +358,11 @@ private void AddNewWebPicture(int row, int col, Uri imageUri, Uri addressUri, st } } + internal void ReadAndAddReference(PictureCacheKey key, uint vmId) + { + _referenceCache.Add(key, vmId); + } + private void AddReferenceToPicture(int row, int col, PictureCacheKey key, uint vmId) { var rv = _richDataStore.GetRichValue(vmId); @@ -387,5 +453,36 @@ public void RemoveCellPicture(int row, int col) _sheet.Cells[row, col].Value = null; } } + + + /// + /// Builds an for the specified cell during worksheet XML loading. + /// This method uses the value metadata index to resolve the corresponding rich value, constructs + /// a cell picture when the rich value represents image data, and registers the picture reference + /// for internal tracking. + /// + /// The row index of the cell. + /// The column index of the cell. + /// + /// The one-based metadata index read from the worksheet XML that identifies the rich value. + /// + /// The rich value associated with the cell. + /// + /// A constructed if the rich value contains image data; + /// otherwise, null. + /// + + public ExcelCellPicture BuildCellPicture(int row, int col, uint valueMetadataIndex, ExcelRichValue fromRichValue) + { + fromRichValue?.SetStructure(_sheet.Workbook.RichData.Db); + var vmId = _sheet.Workbook.Metadata.Db.ValueMetadata.GetIdByIndex((int)valueMetadataIndex - 1); + var pic = GetExcelCellPictureByRichValue(fromRichValue, row, col, vmId); + if (pic == null) return null; + + var cacheKey = CellPictureReferenceCache.CreateKey(pic); + _referenceCache.Add(cacheKey, vmId); + + return pic; + } } } diff --git a/src/EPPlus/CellPictures/LocalImageCacheKey.cs b/src/EPPlus/CellPictures/LocalImageCacheKey.cs index f81957684..517fa1412 100644 --- a/src/EPPlus/CellPictures/LocalImageCacheKey.cs +++ b/src/EPPlus/CellPictures/LocalImageCacheKey.cs @@ -29,6 +29,12 @@ public LocalImageCacheKey(Uri imageUri, CalcOrigins calcOrigin, string altText) this.calcOrigin = calcOrigin; this.altText = altText; } + + public LocalImageCacheKey(ExcelCellPicture picture) + : this(picture.ImageUri, picture.CalcOrigin, picture.AltText) + { + } + protected override string Build() { var sb = new StringBuilder(); diff --git a/src/EPPlus/CellPictures/WebPictureCacheKey.cs b/src/EPPlus/CellPictures/WebPictureCacheKey.cs index 33ab36309..c331d412a 100644 --- a/src/EPPlus/CellPictures/WebPictureCacheKey.cs +++ b/src/EPPlus/CellPictures/WebPictureCacheKey.cs @@ -37,6 +37,12 @@ public WebPictureCacheKey(Uri addressUri, string altText, CalcOrigins calcOrigin this.width = width; } + public WebPictureCacheKey(ExcelCellPicture pic) + : this(pic.ExternalAddress, pic.AltText, pic.CalcOrigin, pic.Sizing ?? WebImageSizing.FitToCellMaintainRatio, null) + { + + } + protected override string Build() { var sb = new StringBuilder(); diff --git a/src/EPPlus/Constants/ExtLstUris.cs b/src/EPPlus/Constants/ExtLstUris.cs index fd78aeda6..d000ca8ef 100644 --- a/src/EPPlus/Constants/ExtLstUris.cs +++ b/src/EPPlus/Constants/ExtLstUris.cs @@ -56,8 +56,16 @@ internal class ExtLstUris internal const string ConditionalFormattingUri = "{78C0D931-6437-407d-A8EE-F0AAD7539E65}"; internal const string ExtChildUri = "{B025F937-C7B1-47D3-B67F-A62EFF666E3E}"; - //Feature property bag refrence used for checkboxes is styling. + //Feature property bag refrence used for checkboxes is styling. (xfComplementXf) + /// + /// NOT FOR DXF + /// internal const string FeaturePropertyBag = "{C7286773-470A-42A8-94C5-96B5CB345126}"; + //dxfComplementExt + /// + /// FOR DXF + /// + internal const string FeaturePropertyBagDxf = "{0417FA29-78FA-4A13-93AC-8FF0FAFDF519}"; internal const string Connection2010Uri = "{DE250136-89BD-433C-8126-D09CA5730AF9}"; } diff --git a/src/EPPlus/Core/ChangableDictionary.cs b/src/EPPlus/Core/ChangableDictionary.cs index 7668b18cc..260f7a484 100644 --- a/src/EPPlus/Core/ChangableDictionary.cs +++ b/src/EPPlus/Core/ChangableDictionary.cs @@ -66,7 +66,7 @@ internal virtual void InsertAndShift(int fromPosition, int add) pos = ~pos; } - if (pos + 1 >= _index[0].Length - 1) + if (_count >= _index[0].Length - 1) { Array.Resize(ref _index[0], _index[0].Length << 1); Array.Resize(ref _index[1], _index[1].Length << 1); diff --git a/src/EPPlus/Data/PowerQuery/ExcelPowerQueryMetaDataEntry.cs b/src/EPPlus/Data/PowerQuery/ExcelPowerQueryMetaDataEntry.cs index c0872575c..1ebf15ee7 100644 --- a/src/EPPlus/Data/PowerQuery/ExcelPowerQueryMetaDataEntry.cs +++ b/src/EPPlus/Data/PowerQuery/ExcelPowerQueryMetaDataEntry.cs @@ -153,11 +153,11 @@ public string GetValueAsText(CultureInfo culture) } if (Value is int i) { - return "l" + i.ToString(); + return "l" + i.ToString(CultureInfo.InvariantCulture); } if (Value is DateTime dt) { - return "d" + dt.ToString("yyyy-MM-ddTHH:mm:ss.fffffffZ"); + return "d" + dt.ToString("yyyy-MM-ddTHH:mm:ss.fffffffZ", CultureInfo.InvariantCulture); } if (Value is bool b) { @@ -165,11 +165,11 @@ public string GetValueAsText(CultureInfo culture) } if (Value is double d) { - return "f" + d.ToString(); + return "f" + d.ToString(CultureInfo.InvariantCulture); } else { - return "s" + Value.ToString(); //You shoul never end up here. + return "s" + Value.ToString(); //You should never end up here. } } /// diff --git a/src/EPPlus/Data/QueryTable/ExcelQueryTableCollection.cs b/src/EPPlus/Data/QueryTable/ExcelQueryTableCollection.cs index 231e7ada8..b3b07b324 100644 --- a/src/EPPlus/Data/QueryTable/ExcelQueryTableCollection.cs +++ b/src/EPPlus/Data/QueryTable/ExcelQueryTableCollection.cs @@ -70,7 +70,7 @@ public ExcelQueryTable this[string name] } /// /// Adds a new query table to the collection. Worksheet level query tables are legacy objects and can only be used for older types of connection. - /// For newer types of connections like PowerQuery, use instead. + /// For newer types of connections like PowerQuery, use instead. /// /// The address /// The name of the query table. diff --git a/src/EPPlus/Drawing/Chart/ChartDataSource.cs b/src/EPPlus/Drawing/Chart/ChartDataSource.cs new file mode 100644 index 000000000..048c18e39 --- /dev/null +++ b/src/EPPlus/Drawing/Chart/ChartDataSource.cs @@ -0,0 +1,497 @@ +using OfficeOpenXml.Core.CellStore; +using OfficeOpenXml.FormulaParsing.Excel.Functions.RefAndLookup; +using OfficeOpenXml.FormulaParsing.Excel.Functions.Text; +using OfficeOpenXml.FormulaParsing.Utilities; +using System; +using System.Collections.Generic; +using System.Data; +using System.Globalization; +using System.IO; +using System.Linq; +using System.Text; +using System.Xml; + +namespace OfficeOpenXml.Drawing.Chart +{ + /// + /// Base-class for things like Series, XSeries, DataLabelsRange + /// This class and children should loosely represent "CT_AxDataSource" in dml-chart.xsd + /// + internal class ChartDataSource : XmlHelper + { + private readonly bool _isPivot; + ExcelWorksheet _ws; + + private bool _isExt = false; + + string _strRef = null; + + string sourceTopPath; + string _StrRefParentPath = "{0}/{1}"; + string _StrRefPath = "{0}/{1}/c:f"; + string _xSeriesStrLitPath, _xSeriesNumLitPath; + + internal string StrRefFormulaValue + { + get { return _strRef; } + } + + internal bool RefIsValidAddress { private set; get; } = false; + + private double[] _numberLiterals; + internal double[] NumberLiterals + { + get + { + if (string.IsNullOrEmpty(_xSeriesNumLitPath) == false && GetNode(_xSeriesNumLitPath) != null) + { + ReadNumLiterals(_xSeriesNumLitPath, out _numberLiterals); + return _numberLiterals; + } + return _numberLiterals; + } + set + { + _numberLiterals = value; + } + } + + private string[] _stringLiterals; + internal string[] StringLiterals + { + get + { + if (string.IsNullOrEmpty(_xSeriesStrLitPath) == false && GetNode(_xSeriesStrLitPath) != null) + { + ReadStringLiterals(_xSeriesStrLitPath, out _stringLiterals); + return _stringLiterals; + } + return _stringLiterals; + } + set + { + _stringLiterals = value; + } + } + + /// + /// Base-class for things like Series, XSeries, DataLabelsRange + /// This class and children should loosely represent "CT_AxDataSource" in dml-chart.xsd + /// + /// If this source is part of a pivotTable + /// The xml node + /// The namespace manager + /// The worksheet + /// The path to the top node of the series this source belongs to, e.g. "c:ser[1]" + internal ChartDataSource(bool isPivot, XmlNamespaceManager ns, XmlNode node, ExcelWorksheet ws, string seriesTopPath) : base(ns, node) + { + _isPivot = isPivot; + _ws = ws; + sourceTopPath = seriesTopPath; + + var ep = string.Format(_StrRefParentPath, sourceTopPath, "c15:datalabelsRange"); + + if (ExistsNode(ep)) + { + _isExt = true; + _StrRefPath = ep + "/c15:f"; + _xSeriesStrLitPath = ep + "/c15:dlblRangeCache"; + } + } + + internal void SetStrRef(string strRef) + { + _strRef = strRef.Trim(); + if (_strRef.StartsWith("=", StringComparison.OrdinalIgnoreCase)) _strRef = _strRef.Substring(1); + + if (strRef.StartsWith("{", StringComparison.OrdinalIgnoreCase) && strRef.EndsWith("}", StringComparison.OrdinalIgnoreCase)) + { + if(_isExt) + { + CreateNode(_StrRefPath, true); + SetXmlNodeString(_StrRefPath, _strRef); + } + + GetLitValues(_strRef, out double[] numLit, out string[] strLit); + NumberLiterals = numLit; + StringLiterals = strLit; + SetLits(numLit, strLit, _xSeriesNumLitPath, _xSeriesStrLitPath); + } + else + { + NumberLiterals = null; + StringLiterals = null; + CreateNode(_StrRefPath, true); + + if (ExcelCellBase.IsValidAddress(strRef)) + { + RefIsValidAddress = true; + SetXmlNodeString(_StrRefPath, ExcelCellBase.GetFullAddress(_ws.Name, _strRef)); + } + else + { + SetXmlNodeString(_StrRefPath, _strRef); + } + SetSourceFunction(); + } + } + + private void SetLits(double[] numLit, string[] strLit, string numLitPath, string strLitPath) + { + if (strLit != null) + { + XmlNode lit = CreateNode(strLitPath); + SetLitArray(lit, strLit); + } + else if (numLit != null) + { + XmlNode lit = CreateNode(numLitPath); + SetLitArray(lit, numLit); + } + } + + + private void ReadNumLiterals(string path, out double[] numberLiterals) + { + var childNodes = GetNode(path).ChildNodes; + numberLiterals = new double[childNodes.Count]; + List numLits = new(); + + foreach (XmlNode node in childNodes) + { + if (node.NodeType == XmlNodeType.Element && node.LocalName == "pt") + { + if (double.TryParse(node.InnerText, NumberStyles.Any, CultureInfo.InvariantCulture, out double numLit) == false) + { + throw new InvalidDataException($"numberLiteral in xml node:'{node.Name}' in ws:'{_ws.Name}' with value:'{node.InnerText}' could not be parsed as double. Chart cannot be read."); + } + numLits.Add(numLit); + } + } + numberLiterals = numLits.ToArray(); + } + + private void SetLitArray(XmlNode lit, double[] numLit) + { + if (numLit.Length == 0) return; + var ci = CultureInfo.InvariantCulture; + + //Remove previous child nodes + var previousPt = lit.SelectNodes("c:pt", NameSpaceManager); + if (previousPt != null) + { + for (int i = 0; i < previousPt.Count; i++) + { + lit.RemoveChild(previousPt[i]); + } + } + + for (int i = 0; i < numLit.Length; i++) + { + var pt = lit.OwnerDocument.CreateElement("c", "pt", ExcelPackage.schemaChart); + pt.SetAttribute("idx", i.ToString(CultureInfo.InvariantCulture)); + lit.AppendChild(pt); + pt.InnerXml = $"{((double)numLit[i]).ToString("R15", ci)}"; + } + AddCount(lit, numLit.Length); + } + + private void SetLitArray(XmlNode lit, string[] strLit) + { + //Remove previous child nodes + var previousPt = lit.SelectNodes("c:pt", NameSpaceManager); + if (previousPt != null) + { + for (int i = 0; i < previousPt.Count; i++) + { + lit.RemoveChild(previousPt[i]); + } + } + + for (int i = 0; i < strLit.Length; i++) + { + var pt = lit.OwnerDocument.CreateElement("c", "pt", ExcelPackage.schemaChart); + pt.SetAttribute("idx", i.ToString(CultureInfo.InvariantCulture)); + lit.AppendChild(pt); + pt.InnerXml = $"{strLit[i]}"; + } + AddCount(lit, strLit.Length); + } + private void AddCount(XmlNode lit, int count) + { + var ct = (XmlElement)lit.SelectSingleNode("c:ptCount", NameSpaceManager); + if (ct == null) + { + ct = lit.OwnerDocument.CreateElement("c", "ptCount", ExcelPackage.schemaChart); + lit.InsertBefore(ct, lit.FirstChild); + } + ct.SetAttribute("val", count.ToString(CultureInfo.InvariantCulture)); + } + + private void ReadStringLiterals(string path, out string[] stringLiterals) + { + var parentNode = GetNode(path); + List strLits = new(); + + if (parentNode != null) + { + var childNodes = parentNode.ChildNodes; + + foreach (XmlNode node in childNodes) + { + if (node.NodeType == XmlNodeType.Element && node.LocalName == "pt") + { + strLits.Add(node.InnerText); + } + } + } + stringLiterals = strLits.ToArray(); + } + + private void SetSourceFunction() + { + if (_StrRefPath.IndexOf("c:numRef", StringComparison.OrdinalIgnoreCase) > 0) + { + XmlNode cache = TopNode.SelectSingleNode(string.Format("{0}/c:numRef/c:numCache", sourceTopPath), NameSpaceManager); + if (cache != null) + { + cache.ParentNode.RemoveChild(cache); + } + + XmlNode lit = TopNode.SelectSingleNode(_xSeriesNumLitPath, NameSpaceManager); + if (lit != null) + { + lit.ParentNode.RemoveChild(lit); + } + } + else + { + XmlNode cache = TopNode.SelectSingleNode(string.Format("{0}/c:strRef/c:strCache", sourceTopPath), NameSpaceManager); + if (cache != null) + { + cache.ParentNode.RemoveChild(cache); + } + + XmlNode lit = TopNode.SelectSingleNode(_xSeriesStrLitPath, NameSpaceManager); + if (lit != null) + { + lit.ParentNode.RemoveChild(lit); + } + + var extCacheStr = string.Format("{0}/c15:datalabelsRange/c15:dlblRangeCache", sourceTopPath); + XmlNode extCache = TopNode.SelectSingleNode(extCacheStr, NameSpaceManager); + if (extCache != null) + { + extCache.ParentNode.RemoveChild(extCache); + } + } + } + + + private void GetLitValues(string value, out double[] numberLiterals, out string[] stringLiterals) + { + value = value.Substring(1, value.Length - 2); //Remove outer {} + if (value[0] == '\"' || value[0] == '\'') + { + numberLiterals = null; + stringLiterals = SplitStringValue(value, value[0]); + } + else + { + stringLiterals = null; + var split = value.Split(','); + numberLiterals = new double[split.Length]; + + for (int i = 0; i < split.Length; i++) + { + if (double.TryParse(split[i], NumberStyles.Any, CultureInfo.InvariantCulture, out double d)) + { + numberLiterals[i] = d; + } + } + } + } + + + private string[] SplitStringValue(string value, char textQualifier) + { + var sb = new StringBuilder(); + bool insideStr = true; + var list = new List(); + for (int i = 1; i < value.Length; i++) + { + if (insideStr) + { + if (value[i] == textQualifier) + { + insideStr = false; + } + else + { + sb.Append(value[i]); + } + } + else + { + if (value[i] == textQualifier) + { + insideStr = true; + if (sb.Length > 0) + { + sb.Append(value[i]); + } + } + else if (value[i] == ',') + { + list.Add(sb.ToString()); + sb = new StringBuilder(); + } + else + { + throw (new InvalidOperationException($"String array has an invalid format at position {i}")); + } + } + } + if (sb.Length > 0) + { + list.Add(sb.ToString()); + } + + return list.ToArray(); + } + + /// + /// Creates a num cach for a chart serie. + /// Please note that a serie can only have one column to have a cache. + /// + /// should be public later + internal void CreateCache(XmlNode seriesTopNode) + { + if (_isPivot) throw (new NotImplementedException("Cache for pivotcharts has not been implemented yet.")); + + if (!string.IsNullOrEmpty(StrRefFormulaValue)) + { + var addr = new ExcelRangeBase(_ws, StrRefFormulaValue); + bool moreThanOneRow = addr.Rows > 1; + bool moreThanOneColumn = addr.Columns > 1; + + if (moreThanOneColumn) + { + throw (new InvalidOperationException("A serie cannot be multiple columns. Please add one serie per column to create a cache")); + } + + CreateCache(StrRefFormulaValue, seriesTopNode); + } + } + internal void CreateCache(string address, XmlNode node) + { + //var ws = _chart.WorkSheet; + var wb = _ws.Workbook; + var addr = new ExcelAddressBase(address); + if (addr.IsExternal) + { + var erIx = wb.ExternalLinks.GetExternalLink(addr._wb); + if (erIx >= 0 && wb.ExternalLinks[erIx].ExternalLinkType == ExternalReferences.eExternalLinkType.ExternalWorkbook) + { + var er = wb.ExternalLinks[erIx].As.ExternalWorkbook; + if (er.Package == null) + { + CreateCacheFromExternalCache(node, er, addr); + } + else + { + CreateCacheFromRange(node, er.Package.Workbook.Worksheets[addr.WorkSheetName]?.Cells[addr.LocalAddress]); + } + } + else + { + return; + } + } + else + { + var ws = string.IsNullOrEmpty(addr.WorkSheetName) ? _ws : _ws.Workbook.Worksheets[addr.WorkSheetName]; + if (ws == null) //Worksheet does not exist, exit + { + return; + } + CreateCacheFromRange(node, ws.Cells[address]); + } + + } + + private void CreateCacheFromRange(XmlNode node, ExcelRangeBase range) + { + if (range == null) return; + lastCachedValues.Clear(); + var startRow = range._fromRow; + var items = 0; + var cse = new CellStoreEnumerator(range.Worksheet._values, startRow, range._fromCol, range._toRow, range._toCol); + while (cse.Next()) + { + var v = cse.Value._value; + if (v != null) + { + string xmlValue = ""; + if (v.IsNumeric()) + { + var d = Utils.TypeConversion.ConvertUtil.GetValueDouble(v); + xmlValue = Utils.TypeConversion.ConvertUtil.GetValueForXml(d, range.Worksheet.Workbook.Date1904); + } + else + { + xmlValue = string.Format(CultureInfo.InvariantCulture, v.ToString()); + } + + var ptNode = node.OwnerDocument.CreateElement("c", "pt", ExcelPackage.schemaChart); + node.AppendChild(ptNode); + ptNode.SetAttribute("idx", (cse.Row - startRow).ToString(CultureInfo.InvariantCulture)); + lastCachedValues.Add(xmlValue); + ptNode.InnerXml = $"{xmlValue}"; + items++; + } + } + + var countNode = node.SelectSingleNode("c:ptCount", NameSpaceManager) as XmlElement; + if (countNode != null) + { + countNode.SetAttribute("val", items.ToString(CultureInfo.InvariantCulture)); + } + } + private void CreateCacheFromExternalCache(XmlNode node, ExternalReferences.ExcelExternalWorkbook er, ExcelAddressBase addr) + { + var ews = er.CachedWorksheets[addr.WorkSheetName]; + if (ews == null) return; + var startRow = addr._fromRow; + var items = 0; + var cse = new CellStoreEnumerator(ews.CellValues._values, startRow, addr._fromCol, addr._toRow, addr._toCol); + while (cse.Next()) + { + var v = cse.Value; + if (v != null) + { + var d = Utils.TypeConversion.ConvertUtil.GetValueDouble(v); + var ptNode = node.OwnerDocument.CreateElement("c", "pt", ExcelPackage.schemaChart); + node.AppendChild(ptNode); + ptNode.SetAttribute("idx", (cse.Row - startRow).ToString(CultureInfo.InvariantCulture)); + var xmlValue = Utils.TypeConversion.ConvertUtil.GetValueForXml(d, er._wb.Date1904); + lastCachedValues.Add(xmlValue); + ptNode.InnerXml = $"{xmlValue}"; + items++; + } + } + + var countNode = node.SelectSingleNode("c:ptCount", NameSpaceManager) as XmlElement; + if (countNode != null) + { + countNode.SetAttribute("val", items.ToString(CultureInfo.InvariantCulture)); + } + } + + private List lastCachedValues = new List(); + internal List GetCachedValues() + { + return lastCachedValues; + } + } +} diff --git a/src/EPPlus/Drawing/Chart/ExcelBarChartSerie.cs b/src/EPPlus/Drawing/Chart/ExcelBarChartSerie.cs index feb80bdd6..ce0cf5296 100644 --- a/src/EPPlus/Drawing/Chart/ExcelBarChartSerie.cs +++ b/src/EPPlus/Drawing/Chart/ExcelBarChartSerie.cs @@ -44,7 +44,7 @@ public ExcelChartSerieDataLabel DataLabel { if (_dataLabel == null) { - if (ExcelChartDataLabelStandard.ForbiddDataLabelPosition(_chart) == false) + if (ExcelChartDataLabelStandard.IsDataLabelPositionForbidden(_chart) == false) { _dataLabel = new ExcelChartSerieDataLabel(_chart, NameSpaceManager, TopNode, SchemaNodeOrder); } diff --git a/src/EPPlus/Drawing/Chart/ExcelChart.cs b/src/EPPlus/Drawing/Chart/ExcelChart.cs index 806858a40..dd7a49701 100644 --- a/src/EPPlus/Drawing/Chart/ExcelChart.cs +++ b/src/EPPlus/Drawing/Chart/ExcelChart.cs @@ -17,6 +17,7 @@ Date Author Change using OfficeOpenXml.Drawing.Style.ThreeD; using OfficeOpenXml.Packaging; using OfficeOpenXml.Style; + using OfficeOpenXml.Table.PivotTable; using OfficeOpenXml.Utils.FileUtils; using System; diff --git a/src/EPPlus/Drawing/Chart/ExcelChartAxis.cs b/src/EPPlus/Drawing/Chart/ExcelChartAxis.cs index 1df76b94d..5078eddbe 100644 --- a/src/EPPlus/Drawing/Chart/ExcelChartAxis.cs +++ b/src/EPPlus/Drawing/Chart/ExcelChartAxis.cs @@ -240,7 +240,7 @@ public ExcelTextBody TextBody { //TODO: CreateTopNode of ExcelTextFontXML on init here somehow //Note: txPR is a CT_Textbody node. The only difference between txPr and TextBody - _textBody = new ExcelTextBody(_chart, NameSpaceManager, TopNode, $"{_nsPrefix}:txPr/a:bodyPr", SchemaNodeOrder/*, ((ExcelTextFontXml)Font).CreateTopNode*/); + _textBody = new ExcelTextBody(_chart, NameSpaceManager, TopNode, $"{_nsPrefix}:txPr/a:bodyPr", SchemaNodeOrder, ((ExcelTextFontXml)Font).CreateTopNode); } return _textBody; } diff --git a/src/EPPlus/Drawing/Chart/ExcelChartDataLabel.cs b/src/EPPlus/Drawing/Chart/ExcelChartDataLabel.cs index d9aa99efc..363145d56 100644 --- a/src/EPPlus/Drawing/Chart/ExcelChartDataLabel.cs +++ b/src/EPPlus/Drawing/Chart/ExcelChartDataLabel.cs @@ -26,7 +26,7 @@ public abstract class ExcelChartDataLabel : XmlHelper, IDrawingStyle { internal ExcelChart _chart; internal string _nodeName; - private string _nsPrefix; + internal string NsPrefix { private set; get; } private readonly string _formatPath; private readonly string _sourceLinkedPath; @@ -35,7 +35,7 @@ internal ExcelChartDataLabel(ExcelChart chart, XmlNamespaceManager ns, XmlNode n { _nodeName = nodeName; _chart = chart; - _nsPrefix = nsPrefix; + NsPrefix = nsPrefix; _formatPath = $"{nsPrefix}:numFmt/@formatCode"; _sourceLinkedPath = $"{nsPrefix}:numFmt/@sourceLinked"; } @@ -157,7 +157,7 @@ public ExcelDrawingFill Fill { if (_fill == null) { - _fill = new ExcelDrawingFill(_chart, NameSpaceManager, TopNode, $"{_nsPrefix}:spPr", SchemaNodeOrder); + _fill = new ExcelDrawingFill(_chart, NameSpaceManager, TopNode, $"{NsPrefix}:spPr", SchemaNodeOrder); } return _fill; } @@ -172,7 +172,7 @@ public ExcelDrawingBorder Border { if (_border == null) { - _border = new ExcelDrawingBorder(_chart, NameSpaceManager, TopNode, $"{_nsPrefix}:spPr/a:ln", SchemaNodeOrder); + _border = new ExcelDrawingBorder(_chart, NameSpaceManager, TopNode, $"{NsPrefix}:spPr/a:ln", SchemaNodeOrder); } return _border; } @@ -187,7 +187,7 @@ public ExcelDrawingEffectStyle Effect { if (_effect == null) { - _effect = new ExcelDrawingEffectStyle(_chart, NameSpaceManager, TopNode, $"{_nsPrefix}:spPr/a:effectLst", SchemaNodeOrder); + _effect = new ExcelDrawingEffectStyle(_chart, NameSpaceManager, TopNode, $"{NsPrefix}:spPr/a:effectLst", SchemaNodeOrder); } return _effect; } @@ -202,7 +202,7 @@ public ExcelDrawing3D ThreeD { if (_threeD == null) { - _threeD = new ExcelDrawing3D(NameSpaceManager, TopNode, $"{_nsPrefix}:spPr", SchemaNodeOrder); + _threeD = new ExcelDrawing3D(NameSpaceManager, TopNode, $"{NsPrefix}:spPr", SchemaNodeOrder); } return _threeD; } @@ -218,7 +218,7 @@ public ExcelTextFont Font { if (_font == null) { - _font = new ExcelTextFontXml(_chart, NameSpaceManager, TopNode, $"{_nsPrefix}:txPr/a:p/a:pPr/a:defRPr", SchemaNodeOrder, CreateDefaultText); + _font = new ExcelTextFontXml(_chart, NameSpaceManager, TopNode, $"{NsPrefix}:txPr/a:p/a:pPr/a:defRPr", SchemaNodeOrder, CreateDefaultText); } return _font; } @@ -233,7 +233,7 @@ public ExcelDrawingTextSettings TextSettings { if (_textSettings == null) { - _textSettings = new ExcelDrawingTextSettings(_chart, NameSpaceManager, TopNode, $"{_nsPrefix}:txPr/a:p/a:pPr/a:defRPr", SchemaNodeOrder); + _textSettings = new ExcelDrawingTextSettings(_chart, NameSpaceManager, TopNode, $"{NsPrefix}:txPr/a:p/a:pPr/a:defRPr", SchemaNodeOrder); } return _textSettings; } @@ -245,14 +245,14 @@ void IDrawingStyleBase.CreatespPr() private void CreateDefaultText() { - if (TopNode.SelectSingleNode($"{_nsPrefix}:txPr", NameSpaceManager) == null) + if (TopNode.SelectSingleNode($"{NsPrefix}:txPr", NameSpaceManager) == null) { - if (!ExistsNode($"{_nsPrefix}:spPr")) + if (!ExistsNode($"{NsPrefix}:spPr")) { - var spNode = CreateNode($"{_nsPrefix}:spPr"); + var spNode = CreateNode($"{NsPrefix}:spPr"); spNode.InnerXml = ""; } - var node = CreateNode($"{_nsPrefix}:txPr"); + var node = CreateNode($"{NsPrefix}:txPr"); node.InnerXml = ""; } @@ -268,7 +268,7 @@ public ExcelTextBody TextBody { if (_textBody == null) { - _textBody = new ExcelTextBody(_chart, NameSpaceManager, TopNode, $"{_nsPrefix}:txPr/a:bodyPr"/*, SchemaNodeOrder, ((ExcelTextFontXml)Font).CreateTopNode*/); + _textBody = new ExcelTextBody(_chart, NameSpaceManager, TopNode, $"{NsPrefix}:txPr/a:bodyPr", SchemaNodeOrder, ((ExcelTextFontXml)Font).CreateTopNode); } return _textBody; } diff --git a/src/EPPlus/Drawing/Chart/ExcelChartDataLabelCollection.cs b/src/EPPlus/Drawing/Chart/ExcelChartDataLabelCollection.cs index 8b2b2a0a2..613500be9 100644 --- a/src/EPPlus/Drawing/Chart/ExcelChartDataLabelCollection.cs +++ b/src/EPPlus/Drawing/Chart/ExcelChartDataLabelCollection.cs @@ -13,7 +13,8 @@ Date Author Change using System; using System.Collections; using System.Collections.Generic; -using System.Reflection.Emit; +using System.ComponentModel; +using System.Data; using System.Xml; namespace OfficeOpenXml.Drawing.Chart @@ -31,14 +32,25 @@ internal ExcelChartDataLabelCollection(ExcelChart chart, XmlNamespaceManager ns, { SchemaNodeOrder = schemaNodeOrder; _list = new List(); - foreach (XmlNode dataLabelNode in TopNode.SelectNodes("c:dLbl", ns)) + var existingDataLabelNodes = TopNode.SelectNodes("c:dLbl", ns); + foreach (XmlNode dataLabelNode in existingDataLabelNodes) { _list.Add(new ExcelChartDataLabelItem(chart, ns, dataLabelNode, "", schemaNodeOrder)); } parentDatalabel = parent; _chart = chart; + } + + internal void InitializeDataLabelsXml() + { + if(_list.Count == 0) + { + var seriesNode = TopNode.ParentNode; + } + } + /// /// Adds a new chart label to the collection /// @@ -72,7 +84,7 @@ private ExcelChartDataLabelItem CreateDataLabel(int idx) dl.ShowLegendKey = parentDatalabel.ShowLegendKey; dl.ShowLeaderLines = true; dl.ShowValue = true; - dl.Position = eLabelPosition.Center; + dl.Position = parentDatalabel.Position; if (idx < _list.Count) { diff --git a/src/EPPlus/Drawing/Chart/ExcelChartDataLabelItem.cs b/src/EPPlus/Drawing/Chart/ExcelChartDataLabelItem.cs index 8e0d06e5c..c75c81844 100644 --- a/src/EPPlus/Drawing/Chart/ExcelChartDataLabelItem.cs +++ b/src/EPPlus/Drawing/Chart/ExcelChartDataLabelItem.cs @@ -10,6 +10,10 @@ Date Author Change ************************************************************************************************* 01/27/2020 EPPlus Software AB Initial release EPPlus 5 *************************************************************************************************/ +using OfficeOpenXml.FormulaParsing.Excel.Functions.Information; +using OfficeOpenXml.FormulaParsing.Excel.Functions.MathFunctions; +using OfficeOpenXml.Style; +using System.Collections.Generic; using System.Globalization; using System.Xml; @@ -20,10 +24,12 @@ namespace OfficeOpenXml.Drawing.Chart /// public class ExcelChartDataLabelItem : ExcelChartDataLabelStandard { + string _fontPropertiesPath = ""; internal ExcelChartDataLabelItem(ExcelChart chart, XmlNamespaceManager ns, XmlNode node, string nodeName, string[] schemaNodeOrder) : base(chart, ns, node, nodeName, schemaNodeOrder) { Layout = new ExcelLayout(NameSpaceManager, TopNode, $"c:layout","c:extLst/c:ext[1]/c15:layout", SchemaNodeOrder); + _fontPropertiesPath = $"{NsPrefix}:tx/{NsPrefix}:rich"; } /// @@ -45,5 +51,7 @@ public int Index SetXmlNodeString("c:idx/@val", value.ToString(CultureInfo.InvariantCulture)); } } + + internal string ValueFromSeries; } } diff --git a/src/EPPlus/Drawing/Chart/ExcelChartDataLabelStandard.cs b/src/EPPlus/Drawing/Chart/ExcelChartDataLabelStandard.cs index 743545c42..3d8673c05 100644 --- a/src/EPPlus/Drawing/Chart/ExcelChartDataLabelStandard.cs +++ b/src/EPPlus/Drawing/Chart/ExcelChartDataLabelStandard.cs @@ -77,7 +77,10 @@ internal ExcelChartDataLabelStandard(ExcelChart chart, XmlNamespaceManager ns, X const string positionPath = "c:dLblPos/@val"; /// /// Position of the labels - /// Note: Only Center, InEnd and InBase are allowed for dataLabels on stacked columns + ///
BE AWARE! For SERIES labels and all underlying labels:
+ /// Setting a position not available for this label in the Excel UI May cause a corrupt file.
+ /// Note: Only Center, InEnd and InBase are allowed for dataLabels on stacked columns
+ /// (Same applies to most BarCharts but they allow OutEnd) ///
public override eLabelPosition Position { @@ -87,14 +90,19 @@ public override eLabelPosition Position } set { - if (ForbiddDataLabelPosition(_chart)) + if (IsDataLabelPositionForbidden(_chart)) { throw new InvalidOperationException("Can't set data label position on a 3D-chart"); } + if(_chart.ChartType == eChartType.ColumnClustered && value == eLabelPosition.Top) + { + throw new InvalidOperationException($"DataLabelPosition: '{value}' is not allowed on chart of type: '{_chart.ChartType}' \n " + + $"because it would cause a corrupt file"); + } SetXmlNodeString(positionPath, GetPosText(value)); } } - internal static bool ForbiddDataLabelPosition(ExcelChart _chart) + internal static bool IsDataLabelPositionForbidden(ExcelChart _chart) { return _chart.IsType3D() && !_chart.IsTypePie() && _chart.ChartType != eChartType.Line3D || _chart.IsTypeDoughnut(); @@ -144,7 +152,7 @@ public override bool ShowSeriesName SetXmlNodeString(showSerPath, value ? "1" : "0"); } } - const string showPerentPath = "c:showPercent/@val"; + const string showPercentPath = "c:showPercent/@val"; /// /// Show percent values /// @@ -152,11 +160,11 @@ public override bool ShowPercent { get { - return GetXmlNodeBool(showPerentPath); + return GetXmlNodeBool(showPercentPath); } set { - SetXmlNodeString(showPerentPath, value ? "1" : "0"); + SetXmlNodeString(showPercentPath, value ? "1" : "0"); } } const string showLeaderLinesPath = "c:showLeaderLines/@val"; @@ -254,5 +262,30 @@ public override string Separator } } } + + internal void AddExtFieldTableEmpty() + { + CreateNode($"{extPath}/c15:dlblFieldTable"); + } + + + internal bool ShowDatalabelsRange + { + get + { + return GetXmlNodeBool($"{extPath}/c15:showDataLabelsRange"); + } + set + { + var rangePath = $"{extPath}/c15:showDataLabelsRange"; + if(ExistsNode(rangePath) == false) + { + CreateNode(rangePath); + } + SetXmlNodeBool(rangePath+"/@val", value); + } + } + + internal ExcelAddressBase SingleCellAddressFromSeries = null; } } diff --git a/src/EPPlus/Drawing/Chart/ExcelChartSerieDataLabel.cs b/src/EPPlus/Drawing/Chart/ExcelChartSerieDataLabel.cs index ce8b6c61e..b0ae3d77e 100644 --- a/src/EPPlus/Drawing/Chart/ExcelChartSerieDataLabel.cs +++ b/src/EPPlus/Drawing/Chart/ExcelChartSerieDataLabel.cs @@ -10,13 +10,12 @@ Date Author Change ************************************************************************************************* 01/27/2020 EPPlus Software AB Initial release EPPlus 5 *************************************************************************************************/ +using OfficeOpenXml.FormulaParsing.Excel.Functions.RefAndLookup; using System; -using System.Collections.Generic; -using System.Text; +using System.Data; +using System.Linq; +using System.Security.Cryptography.Xml; using System.Xml; -using OfficeOpenXml.Drawing.Interfaces; -using OfficeOpenXml.Drawing.Style.Effect; -using OfficeOpenXml.Style; namespace OfficeOpenXml.Drawing.Chart { @@ -26,9 +25,21 @@ namespace OfficeOpenXml.Drawing.Chart public sealed class ExcelChartSerieDataLabel : ExcelChartDataLabelStandard { internal ExcelChartSerieDataLabel(ExcelChart chart, XmlNamespaceManager ns, XmlNode node, string[] schemaNodeOrder) - : base(chart, ns,node,"dLbls", schemaNodeOrder) + : base(chart, ns, node, "dLbls", schemaNodeOrder) { Position = eLabelPosition.Center; + var parentSeries = GetParentSeries(); + + var strRef = parentSeries.GetDataLabelRange(); + if (string.IsNullOrEmpty(strRef) == false) + { + SetValueSource(strRef); + //ValueFromCells = strRef; + //if (ExcelCellBase.IsValidAddress(strRef)) + //{ + // DataLabelRange = chart.WorkSheet.Cells[strRef]; + //} + } } ExcelChartDataLabelCollection _dataLabels = null; /// @@ -41,9 +52,126 @@ public ExcelChartDataLabelCollection DataLabels if (_dataLabels == null) { _dataLabels = new ExcelChartDataLabelCollection(_chart, NameSpaceManager, TopNode, SchemaNodeOrder, this as ExcelChartDataLabelStandard); + + //Fill datalabel addresses + if(DataLabelRange != null) + { + var address = DataLabelRange; + for(int i = 0; i< _dataLabels.Count(); i++) + { + if (address.Rows > address.Columns) + { + _dataLabels[i].SingleCellAddressFromSeries = address.TakeSingleCell(i, 0); + } + else + { + _dataLabels[i].SingleCellAddressFromSeries = address.TakeSingleCell(0, i); + } + } + } } return _dataLabels; } } + + private string _valueFromCellsRange = null; + + /// + /// Value From Cells in excel + /// This can be a range address or a list of values defined by a formula or within {} + /// e.g {"\one\",\"two\"} + /// This follows essentially the same rules as Serie or XSerie ranges in chart.AddSerie() + /// + public string ValueFromCellsRange { + get + { + return _valueFromCellsRange; + } + set + { + SetValueSource(value); + } + } + + internal ExcelRangeBase DataLabelRange { get; private set; } = null; + + + ExcelChartStandardSerie GetParentSeries() + { + //TODO: The way we aquire the Series instance here is clumsy. + //Fix as part of datalabel refactor? + //Perhaps the series of a series label should be part of its constructor. + //Or use an eventhandler + //For a single case however that feels overkill. + + //Has to get the series index: + var idxNode = (XmlElement)TopNode.ParentNode.SelectSingleNode($"{NsPrefix}:idx", NameSpaceManager); + var idxNodeValue = int.Parse(idxNode.GetAttribute("val")); + //Get the series this datalabel is on + return (ExcelChartStandardSerie)_chart.Series[idxNodeValue]; + } + + /// + /// Select range for Value From Cells + /// sets ValueFromCellsRange to the AddressAbsolute of the address + /// + public void SetValueFromCellsRange(ExcelAddressBase address) + { + ValueFromCellsRange = address.AddressAbsolute.ToString(); + } + + /// + /// Select range for Value From Cells + /// + /// must be a single; cell, row or column. Or alternatively a collection of literals + /// Thrown when input is not a cell, a row or a column + private void SetValueSource(string reference) + { + //TODO: Arguably this is just another series with a series cache. + //Same as Cat or Val except that it is added in Ext on the Serie node + //ShowValue property essentially changes the datalabels in the same way. + //This could be unified somehow so that all serie ranges; Cat, Val and DataLabelRange are handled the same way. + //The start of this is now being done in ChartDataSource.cs + + var currentSeries = GetParentSeries(); + //Set the ext data needed in the Series node + currentSeries.SetDataLabelRange(reference); + + if(currentSeries.DataLabelRangeSource.RefIsValidAddress) + { + DataLabelRange = _chart.WorkSheet.Cells[reference]; + } + + //Create the Datalabels if they do not exist + if (DataLabels.Count < currentSeries.NumberOfItems) + { + for (int i = 0; i < currentSeries.NumberOfItems; i++) + { + ExcelChartDataLabelItem currentLabel; + if (DataLabels.Count - 1 < i) + { + currentLabel = DataLabels.Add(i); + } + else + { + currentLabel = DataLabels[i]; + } + currentLabel.AddExtFieldTableEmpty(); + currentLabel.ShowDatalabelsRange = true; + + if (DataLabelRange != null) + { + if (DataLabelRange.Rows > DataLabelRange.Columns) + { + currentLabel.SingleCellAddressFromSeries = DataLabelRange.TakeSingleCell(i, 0); + } + else + { + currentLabel.SingleCellAddressFromSeries = DataLabelRange.TakeSingleCell(0, i); + } + } + } + } + } } -} +} \ No newline at end of file diff --git a/src/EPPlus/Drawing/Chart/ExcelChartStandardSerie.cs b/src/EPPlus/Drawing/Chart/ExcelChartStandardSerie.cs index 06c03b88d..744486ee7 100644 --- a/src/EPPlus/Drawing/Chart/ExcelChartStandardSerie.cs +++ b/src/EPPlus/Drawing/Chart/ExcelChartStandardSerie.cs @@ -19,6 +19,9 @@ Date Author Change using System.Globalization; using System.Runtime.CompilerServices; using System.IO; +using OfficeOpenXml.FormulaParsing.Utilities; +using OfficeOpenXml.FormulaParsing.Excel.Functions.MathFunctions; + namespace OfficeOpenXml.Drawing.Chart { /// @@ -28,26 +31,28 @@ public class ExcelChartStandardSerie : ExcelChartSerie { private readonly bool _isPivot; - double[] _NumberLiteralsY = null; double[] _NumberLiteralsX = null; string[] _StringLiteralsX = null; string[] _StringLiteralsY = null; + const string extPath = "c:extLst/c:ext"; + const string dlblRangePath = "c:extLst/c:ext/c15:datalabelsRange"; + /// /// Literals for the Y serie, if the literal values are numeric /// - public override double[] NumberLiteralsY - { - get + public override double[] NumberLiteralsY + { + get { - if(string.IsNullOrEmpty(_seriesNumLitPath) == false && GetNode(_seriesNumLitPath) != null) + if (string.IsNullOrEmpty(_seriesNumLitPath) == false && GetNode(_seriesNumLitPath) != null) { ReadNumLiterals(_seriesNumLitPath, out _NumberLiteralsY); return _NumberLiteralsY; } return _NumberLiteralsY; - } + } protected set { _NumberLiteralsY = value; @@ -56,7 +61,7 @@ protected set /// /// Literals for the X serie, if the literal values are numeric /// - public override double[] NumberLiteralsX + public override double[] NumberLiteralsX { get { @@ -112,6 +117,8 @@ protected set } } + internal ChartDataSource DataLabelRangeSource = null; + /// /// Default constructor /// @@ -121,26 +128,27 @@ protected set /// Is pivotchart internal ExcelChartStandardSerie(ExcelChart chart, XmlNamespaceManager ns, XmlNode node, bool isPivot) : base(chart, ns, node) - { - _chart = chart; - _isPivot = isPivot; - SchemaNodeOrder = new string[] { "idx", "order", "tx", "spPr", "marker", "invertIfNegative", "pictureOptions", "explosion", "dPt", "dLbls", "trendline","errBars", "cat", "val", "xVal", "yVal", "smooth","shape", "bubbleSize", "bubble3D", "numRef", "numLit", "strRef", "strLit", "formatCode", "ptCount", "pt" }; - - if (_chart.ChartNode.LocalName=="scatterChart" || - _chart.ChartNode.LocalName.StartsWith("bubble", StringComparison.OrdinalIgnoreCase)) - { - _seriesTopPath = "c:yVal"; - _xSeriesTopPath = "c:xVal"; - } - else - { - _seriesTopPath = "c:val"; - _xSeriesTopPath = "c:cat"; - } - - _seriesPath = string.Format(_seriesPath, _seriesTopPath); - _numCachePath = string.Format(_numCachePath, _seriesTopPath); + { + _chart = chart; + _isPivot = isPivot; + SchemaNodeOrder = new string[] { "idx", "order", "tx", "spPr", "marker", "invertIfNegative", "pictureOptions", "explosion", "dPt", "dLbls", "trendline", "errBars", "cat", "val", "xVal", "yVal", "smooth", "shape", "bubbleSize", "bubble3D", "numRef", "numLit", "strRef", "strLit", "formatCode", "ptCount", "pt" }; + + if (_chart.ChartNode.LocalName == "scatterChart" || + _chart.ChartNode.LocalName.StartsWith("bubble", StringComparison.OrdinalIgnoreCase)) + { + _seriesTopPath = "c:yVal"; + _xSeriesTopPath = "c:xVal"; + } + else + { + _seriesTopPath = "c:val"; + _xSeriesTopPath = "c:cat"; + } + + _seriesPath = string.Format(_seriesPath, _seriesTopPath); + _numCachePath = string.Format(_numCachePath, _seriesTopPath); + //var XSeriesSource = new ChartDataSource(isPivot, ns, node, chart.WorkSheet); var np = string.Format(_xSeriesParentPath, _xSeriesTopPath, isPivot ? "c:multiLvlStrRef" : "c:numRef"); var sp = string.Format(_xSeriesParentPath, _xSeriesTopPath, isPivot ? "c:multiLvlStrRef" : "c:strRef"); @@ -157,54 +165,147 @@ internal ExcelChartStandardSerie(ExcelChart chart, XmlNamespaceManager ns, XmlNo _xSeriesStrLitPath = string.Format("{0}/c:strLit", _xSeriesTopPath); _xSeriesNumLitPath = string.Format("{0}/c:numLit", _xSeriesTopPath); - } - internal override void SetID(string id) - { - SetXmlNodeString("c:idx/@val",id); - SetXmlNodeString("c:order/@val", id); - } - const string headerPath="c:tx/c:v"; - /// - /// Header for the serie. - /// - public override string Header - { - get - { + } + internal override void SetID(string id) + { + SetXmlNodeString("c:idx/@val", id); + SetXmlNodeString("c:order/@val", id); + } + + internal bool HasDataLabelRange() + { + return ExistsNode(dlblRangePath); + } + + internal string GetDataLabelRange() + { + if(ExistsNode($"{dlblRangePath}/c15:f")) + { + return GetXmlNodeString($"{dlblRangePath}/c15:f"); + } + return null; + } + + internal void AddExtLstXml() + { + NameSpaceManager.AddNamespace("c15", ExcelPackage.schemaChart2012); + NameSpaceManager.AddNamespace("c16", ExcelPackage.schemaChart2014); + + XmlElement ext15Node; + + var c15Uri = "{02D57815-91ED-43cb-92C2-25804820EDAC}"; + + //Only add node if it doesn't already exist + if (ExistsNode(extPath + $"[@uri='{c15Uri}']") == false) + { + XmlElement el = (XmlElement)CreateNode($"{extPath}"); + el.SetAttribute("xmlns:c15", ExcelPackage.schemaChart2012); + SetXmlNodeString($"{extPath}/@uri", $"{c15Uri}"); + ext15Node = el; + } + else + { + ext15Node = (XmlElement)GetNode($"{extPath}"); + } + + //Only add node if it doesn't already exist + if (ExistsNode($"{extPath}[2]") == false) + { + XmlElement element = (XmlElement)CreateNode($"{extPath}", false, true); + element.SetAttribute("xmlns:c16", ExcelPackage.schemaChart2014); + SetXmlNodeString($"{extPath}[2]/@uri", "{C3380CC4-5D6E-409C-BE32-E72D297353CC}"); + var _guidId = Guid.NewGuid(); + + var extNode2 = GetNode($"{extPath}[2]"); + var uniqueIdNode = (XmlElement)CreateNode(extNode2, "c16:uniqueID"); + uniqueIdNode.SetAttribute("val", $"{{{_guidId}}}"); + } + } + + internal string DataLabelSerie + { + get + { + if(DataLabelRangeSource == null) + { + AddExtLstXml(); + var datalabelsRange = CreateNode(dlblRangePath); + DataLabelRangeSource = new ChartDataSource(_isPivot, NameSpaceManager, TopNode, _chart.WorkSheet, extPath); + if(GetDataLabelRange() != null) + { + DataLabelRangeSource.SetStrRef(GetDataLabelRange()); + } + else + { + return null; + } + } + + return DataLabelRangeSource.StrRefFormulaValue; + } + set + { + DataLabelRangeSource.SetStrRef(value); + } + } + + internal string[] GetDataLabelLiterals() + { + if(string.IsNullOrEmpty(DataLabelSerie) == false) + { + return DataLabelRangeSource.StringLiterals; + } + return null; + } + + internal void SetDataLabelRange(string strRef) + { + var dlblSer = DataLabelSerie; + DataLabelSerie = strRef; + } + + const string headerPath = "c:tx/c:v"; + /// + /// Header for the serie. + /// + public override string Header + { + get + { return GetXmlNodeString(headerPath); } set { Cleartx(); - SetXmlNodeString(headerPath, value); + SetXmlNodeString(headerPath, value); } } - private void Cleartx() - { - var n = TopNode.SelectSingleNode("c:tx", NameSpaceManager); - if (n != null) - { - n.InnerXml = ""; - } - } - const string headerAddressPath = "c:tx/c:strRef/c:f"; + private void Cleartx() + { + var n = TopNode.SelectSingleNode("c:tx", NameSpaceManager); + if (n != null) + { + n.InnerXml = ""; + } + } + const string headerAddressPath = "c:tx/c:strRef/c:f"; /// - /// Header address for the serie. - /// - public override ExcelAddressBase HeaderAddress - { - get - { - string address = GetXmlNodeString(headerAddressPath); - if (address == "") - { - return null; - } - else - { - return new ExcelAddressBase(address); - } + /// Header address for the serie. + /// + public override ExcelAddressBase HeaderAddress + { + get + { + string address = GetXmlNodeString(headerAddressPath); + if (address == "") + { + return null; + } + else + { + return new ExcelAddressBase(address); + } } set { @@ -217,7 +318,7 @@ public override ExcelAddressBase HeaderAddress SetXmlNodeString(headerAddressPath, ExcelCellBase.GetFullAddress(value.WorkSheetName, value.Address)); SetXmlNodeString("c:tx/c:strRef/c:strCache/c:ptCount/@val", "0"); } - } + } string _seriesTopPath; string _seriesPath = "{0}/c:numRef/c:f"; string _numCachePath = "{0}/c:numRef/c:numCache"; @@ -227,18 +328,18 @@ public override ExcelAddressBase HeaderAddress /// public override string Series { - get - { - return GetXmlNodeString(_seriesPath); - } - set - { + get + { + return GetXmlNodeString(_seriesPath); + } + set + { value = value.Trim(); if (value.StartsWith("=", StringComparison.OrdinalIgnoreCase)) value = value.Substring(1); if (value.StartsWith("{", StringComparison.OrdinalIgnoreCase) && value.EndsWith("}", StringComparison.OrdinalIgnoreCase)) { GetLitValues(value, out double[] numLit, out string[] strLit); - if(strLit!=null) + if (strLit != null) { throw (new ArgumentException("Value series can't contain strings")); } @@ -255,22 +356,22 @@ public override string Series } - string _xSeries=null; - string _xSeriesTopPath; - string _xSeriesParentPath = "{0}/{1}"; - string _xSeriesPath = "{0}/{1}/c:f"; - string _xSeriesStrLitPath, _xSeriesNumLitPath; + string _xSeries = null; + string _xSeriesTopPath; + string _xSeriesParentPath = "{0}/{1}"; + string _xSeriesPath = "{0}/{1}/c:f"; + string _xSeriesStrLitPath, _xSeriesNumLitPath; /// /// Set an address for the horisontal labels /// - public override string XSeries - { - get - { - return GetXmlNodeString(_xSeriesPath); - } - set - { + public override string XSeries + { + get + { + return GetXmlNodeString(_xSeriesPath); + } + set + { _xSeries = value.Trim(); if (_xSeries.StartsWith("=", StringComparison.OrdinalIgnoreCase)) _xSeries = _xSeries.Substring(1); if (value.StartsWith("{", StringComparison.OrdinalIgnoreCase) && value.EndsWith("}", StringComparison.OrdinalIgnoreCase)) @@ -285,7 +386,7 @@ public override string XSeries NumberLiteralsX = null; StringLiteralsX = null; CreateNode(_xSeriesPath, true); - if(ExcelCellBase.IsValidAddress(_xSeries)) + if (ExcelCellBase.IsValidAddress(_xSeries)) { SetXmlNodeString(_xSeriesPath, ExcelCellBase.GetFullAddress(_chart.WorkSheet.Name, _xSeries)); } @@ -296,7 +397,7 @@ public override string XSeries SetXSerieFunction(); } } - } + } private void ReadNumLiterals(string path, out double[] numberLiterals) { @@ -306,9 +407,9 @@ private void ReadNumLiterals(string path, out double[] numberLiterals) foreach (XmlNode node in childNodes) { - if(node.NodeType==XmlNodeType.Element && node.LocalName == "pt") + if (node.NodeType == XmlNodeType.Element && node.LocalName == "pt") { - if(double.TryParse(node.InnerText, NumberStyles.Any, CultureInfo.InvariantCulture, out double numLit) == false) + if (double.TryParse(node.InnerText, NumberStyles.Any, CultureInfo.InvariantCulture, out double numLit) == false) { throw new InvalidDataException($"numberLiteral in xml node:'{node.Name}' in chart:'{_chart.Name}' with value:'{node.InnerText}' could not be parsed as double. Chart cannot be read."); } @@ -323,7 +424,7 @@ private void ReadStringLiterals(string path, out string[] stringLiterals) var parentNode = GetNode(path); List strLits = new(); - if(parentNode != null) + if (parentNode != null) { var childNodes = parentNode.ChildNodes; @@ -465,12 +566,12 @@ private void SetXSerieFunction() } private void SetLits(double[] numLit, string[] strLit, string numLitPath, string strLitPath) { - if(strLit!=null) + if (strLit != null) { XmlNode lit = CreateNode(strLitPath); SetLitArray(lit, strLit); } - else if(numLit!=null) + else if (numLit != null) { XmlNode lit = CreateNode(numLitPath); SetLitArray(lit, numLit); @@ -506,7 +607,7 @@ private void SetLitArray(XmlNode lit, string[] strLit) { //Remove previous child nodes var previousPt = lit.SelectNodes("c:pt", NameSpaceManager); - if(previousPt != null) + if (previousPt != null) { for (int i = 0; i < previousPt.Count; i++) { @@ -535,9 +636,9 @@ private void AddCount(XmlNode lit, int count) } ExcelChartTrendlineCollection _trendLines = null; - /// - /// Access to the trendline collection - /// + /// + /// Access to the trendline collection + /// public override ExcelChartTrendlineCollection TrendLines { get @@ -556,7 +657,7 @@ public override int NumberOfItems { get { - if(ExcelCellBase.IsValidAddress(Series)) + if (ExcelCellBase.IsValidAddress(Series)) { var a = new ExcelAddressBase(Series); return a.Rows; @@ -574,16 +675,16 @@ public override int NumberOfItems ///
public void CreateCache() { - if (_isPivot) throw(new NotImplementedException("Cache for pivotcharts has not been implemented yet.")); + if (_isPivot) throw (new NotImplementedException("Cache for pivotcharts has not been implemented yet.")); if (!string.IsNullOrEmpty(Series)) { - if(new ExcelRangeBase(_chart.WorkSheet, Series).Columns > 1) + if (new ExcelRangeBase(_chart.WorkSheet, Series).Columns > 1) { throw (new InvalidOperationException("A serie cannot be multiple columns. Please add one serie per column to create a cache")); } var node = GetTopNode(Series, _seriesTopPath); - + CreateCache(Series, node); } @@ -598,8 +699,14 @@ public void CreateCache() CreateCache(XSeries, node); } + + if(!string.IsNullOrEmpty(DataLabelSerie)) + { + var node = GetTopNode(DataLabelSerie, extPath); + DataLabelRangeSource.CreateCache(node); + } } - private void CreateCache(string address, XmlNode node) + internal void CreateCache(string address, XmlNode node) { //var ws = _chart.WorkSheet; var wb = _chart.WorkSheet.Workbook; @@ -633,7 +740,7 @@ private void CreateCache(string address, XmlNode node) } CreateCacheFromRange(node, ws.Cells[address]); } - + } private void CreateCacheFromRange(XmlNode node, ExcelRangeBase range) @@ -641,17 +748,27 @@ private void CreateCacheFromRange(XmlNode node, ExcelRangeBase range) if (range == null) return; var startRow = range._fromRow; var items = 0; - var cse = new CellStoreEnumerator(range.Worksheet._values, startRow,range._fromCol, range._toRow, range._toCol); + var cse = new CellStoreEnumerator(range.Worksheet._values, startRow, range._fromCol, range._toRow, range._toCol); while (cse.Next()) { var v = cse.Value._value; if (v != null) { - var d = Utils.TypeConversion.ConvertUtil.GetValueDouble(v); + string xmlValue = ""; + if (v.IsNumeric()) + { + var d = Utils.TypeConversion.ConvertUtil.GetValueDouble(v); + xmlValue = Utils.TypeConversion.ConvertUtil.GetValueForXml(d, range.Worksheet.Workbook.Date1904); + } + else + { + xmlValue = string.Format(CultureInfo.InvariantCulture, v.ToString()); + } + var ptNode = node.OwnerDocument.CreateElement("c", "pt", ExcelPackage.schemaChart); node.AppendChild(ptNode); ptNode.SetAttribute("idx", (cse.Row - startRow).ToString(CultureInfo.InvariantCulture)); - ptNode.InnerXml = $"{Utils.TypeConversion.ConvertUtil.GetValueForXml(d, range.Worksheet.Workbook.Date1904)}"; + ptNode.InnerXml = $"{xmlValue}"; items++; } } @@ -700,10 +817,10 @@ private XmlNode GetTopNode(string address, string seriesTopPath) if (addr.IsExternal) { var erIx = wb.ExternalLinks.GetExternalLink(addr._wb); - if(erIx>=0) + if (erIx >= 0) { var er = wb.ExternalLinks[erIx].As.ExternalWorkbook; - if(er.Package!=null) + if (er.Package != null) { var ws = er.Package.Workbook.Worksheets[addr.WorkSheetName]; var range = ws.Cells[addr.LocalAddress]; @@ -712,7 +829,7 @@ private XmlNode GetTopNode(string address, string seriesTopPath) else { var ws = er.CachedWorksheets[addr.WorkSheetName]; - if(ws==null) + if (ws == null) { v = null; } @@ -728,7 +845,7 @@ private XmlNode GetTopNode(string address, string seriesTopPath) v = null; } } - else + else { ExcelWorksheet ws; if (string.IsNullOrEmpty(addr.WorkSheetName)) @@ -752,22 +869,22 @@ private XmlNode GetTopNode(string address, string seriesTopPath) string cachePath; bool isNum; - if(Utils.TypeConversion.ConvertUtil.IsNumericOrDate(v) || v is null) + if (Utils.TypeConversion.ConvertUtil.IsNumericOrDate(v) || v is null) { cachePath = string.Format("{0}/c:numRef/c:numCache", seriesTopPath); isNum = true; } else { - cachePath=string.Format("{0}/c:strRef/c:strCache", seriesTopPath); + cachePath = string.Format("{0}/c:strRef/c:strCache", seriesTopPath); isNum = false; } var node = CreateNode(cachePath); if (node.HasChildNodes) { - if(isNum) + if (isNum) { - if(node.FirstChild.LocalName== "formatCode") + if (node.FirstChild.LocalName == "formatCode") { node.InnerXml = node.FirstChild.OuterXml; } @@ -778,7 +895,7 @@ private XmlNode GetTopNode(string address, string seriesTopPath) } else { - node.InnerXml = ""; + node.InnerXml = ""; } } CreateNode($"{cachePath}/c:ptCount"); @@ -796,14 +913,14 @@ internal static XmlElement CreateSerieElement(ExcelChart chart) //If the chart is added from a chart template, then use the chart templates series xml if (chart._drawings._seriesTemplateXml != null) { - if(chart._drawings._seriesTemplateXml.Count != 0) + if (chart._drawings._seriesTemplateXml.Count != 0) { ser.InnerXml = chart._drawings._seriesTemplateXml[0]; return ser; } } - int idx = FindIndex(chart._topChart??chart); + int idx = FindIndex(chart._topChart ?? chart); ser.InnerXml = string.Format("{2}{5}{0}{3}{4}", AddExplosion(chart.ChartType), idx, AddSpPrAndScatterPoint(chart.ChartType), AddAxisNodes(chart.ChartType), AddSmooth(chart.ChartType), AddMarker(chart.ChartType)); return ser; } diff --git a/src/EPPlus/Drawing/Chart/ExcelLineChartSerie.cs b/src/EPPlus/Drawing/Chart/ExcelLineChartSerie.cs index f1e5f8ad6..d5f78979d 100644 --- a/src/EPPlus/Drawing/Chart/ExcelLineChartSerie.cs +++ b/src/EPPlus/Drawing/Chart/ExcelLineChartSerie.cs @@ -10,13 +10,14 @@ Date Author Change ************************************************************************************************* 01/27/2020 EPPlus Software AB Initial release EPPlus 5 *************************************************************************************************/ +using OfficeOpenXml.Drawing.Interfaces; using System; using System.Collections.Generic; +using System.Data; +using System.Drawing; using System.Globalization; using System.Text; using System.Xml; -using System.Drawing; -using OfficeOpenXml.Drawing.Interfaces; namespace OfficeOpenXml.Drawing.Chart { diff --git a/src/EPPlus/Drawing/ExcelDrawing.cs b/src/EPPlus/Drawing/ExcelDrawing.cs index 0d632dbfd..c7abe0313 100644 --- a/src/EPPlus/Drawing/ExcelDrawing.cs +++ b/src/EPPlus/Drawing/ExcelDrawing.cs @@ -2846,7 +2846,7 @@ private XmlNode CopyShape(ExcelChartStandard targetChart, bool isGroupShape = fa private XmlNode CopyShape(ExcelWorksheet worksheet, bool isGroupShape = false, XmlNode groupDrawNode = null) { - var sourceShape = this as ExcelShape; + var sourceShape = this as ExcelShapeBase; XmlNode drawNode = null; if (isGroupShape && groupDrawNode != null) { @@ -2860,7 +2860,7 @@ private XmlNode CopyShape(ExcelWorksheet worksheet, bool isGroupShape = false, X drawNode = worksheet.Drawings.CreateDocumentAndTopNode(CellAnchor, false); drawNode.InnerXml = TopNode.InnerXml; //Asign new id - var targetShape = GetDrawing(worksheet._drawings, drawNode) as ExcelShape; + var targetShape = GetDrawing(worksheet._drawings, drawNode) as ExcelShapeBase; targetShape.Id = ++worksheet.Drawings._nextDrawingId; targetShape.Name = worksheet._drawings.GetUniqueDrawingName(sourceShape.Name); } diff --git a/src/EPPlus/Drawing/ExcelImageBase.cs b/src/EPPlus/Drawing/ExcelImageBase.cs index bb813d0a1..7abe2c57f 100644 --- a/src/EPPlus/Drawing/ExcelImageBase.cs +++ b/src/EPPlus/Drawing/ExcelImageBase.cs @@ -182,6 +182,7 @@ internal void SetImageNoContainer(byte[] image, ePictureType pictureType) { ImageBytes = image; } + using (var ms = EPPlusMemoryManager.GetStream(image)) { var imageHandler = new GenericImageHandler(); diff --git a/src/EPPlus/Drawing/ImageHandling/GenericImageHandler.cs b/src/EPPlus/Drawing/ImageHandling/GenericImageHandler.cs index 2951c0706..9f04ef981 100644 --- a/src/EPPlus/Drawing/ImageHandling/GenericImageHandler.cs +++ b/src/EPPlus/Drawing/ImageHandling/GenericImageHandler.cs @@ -38,6 +38,7 @@ public HashSet SupportedTypes public bool GetImageBounds(MemoryStream image, ePictureType type, out double width, out double height, out double horizontalResolution, out double verticalResolution) { if (image.Length == 0) throw new InvalidDataException("The Image has zero-length"); + try { width = 0; diff --git a/src/EPPlus/Drawing/ImageHandling/ImageReader.cs b/src/EPPlus/Drawing/ImageHandling/ImageReader.cs index 0389431eb..a764e5535 100644 --- a/src/EPPlus/Drawing/ImageHandling/ImageReader.cs +++ b/src/EPPlus/Drawing/ImageHandling/ImageReader.cs @@ -293,7 +293,6 @@ private static bool IsJpg(MemoryStream ms, ref double width, ref double height, var precision = br.ReadByte(); //Bits height = GetUInt16BigEndian(br); width = GetUInt16BigEndian(br); - br.Close(); return true; case 0xFFD9: return height != 0 && width != 0; @@ -320,7 +319,6 @@ private static bool IsGif(MemoryStream ms, ref double width, ref double height) { width = br.ReadUInt16(); height = br.ReadUInt16(); - br.Close(); return true; } return false; @@ -401,10 +399,8 @@ private static bool IsIcon(MemoryStream ms, ref double width, ref double height) // IsPng(br, ref width, ref height, offset+fileSize); //} - br.Close(); return true; } - br.Close(); return false; } internal static bool IsIco(BinaryReader br) @@ -734,10 +730,8 @@ private static bool IsPng(BinaryReader br, ref double width, ref double height, verticalResolution /= M_TO_INCH; } - br.Close(); return true; case "IEND": - br.Close(); return width != 0 && height != 0; default: br.ReadBytes(length); @@ -746,7 +740,6 @@ private static bool IsPng(BinaryReader br, ref double width, ref double height, var crc = br.ReadInt32(); } } - br.Close(); return width != 0 && height != 0; } private static bool IsPng(BinaryReader br) @@ -784,7 +777,6 @@ private static bool IsSvg(MemoryStream ms, ref double width, ref double height) var w = reader.GetAttribute("width"); var h = reader.GetAttribute("height"); var vb = reader.GetAttribute("viewBox"); - reader.Close(); if (w == null || h == null) { if (vb == null) diff --git a/src/EPPlus/Drawing/Style/Text/ExcelTextBody.cs b/src/EPPlus/Drawing/Style/Text/ExcelTextBody.cs index 15472a600..b81820ca1 100644 --- a/src/EPPlus/Drawing/Style/Text/ExcelTextBody.cs +++ b/src/EPPlus/Drawing/Style/Text/ExcelTextBody.cs @@ -36,19 +36,16 @@ public class ExcelTextBody : XmlHelper /// /// /// - internal ExcelTextBody(IPictureRelationDocument pictureRelationDocument, XmlNamespaceManager ns, XmlNode topNode, string path, string[] schemaNodeOrder=null) : + /// + internal ExcelTextBody(IPictureRelationDocument pictureRelationDocument, XmlNamespaceManager ns, XmlNode topNode, string path, string[] schemaNodeOrder=null, Action initXml=null) : base(ns, topNode) { _pictureRelationDocument = pictureRelationDocument; _path = path; - _initXml = null; + _initXml = initXml; AddSchemaNodeOrder(schemaNodeOrder, new string[] { "ln", "noFill", "solidFill", "gradFill", "pattFill", "blipFill", "latin", "ea", "cs", "sym", "hlinkClick", "hlinkMouseOver", "rtl", "extLst", "highlight", "kumimoji", "lang", "altLang", "sz", "b", "i", "u", "strike", "kern", "cap", "spc", "normalizeH", "baseline", "noProof", "dirty", "err", "smtClean", "smtId", "bmk" }); } - - string ctTextBodyPath; - - /// /// The anchoring position within the shape /// diff --git a/src/EPPlus/EPPlus.csproj b/src/EPPlus/EPPlus.csproj index 1f3a9e97a..4e50327dc 100644 --- a/src/EPPlus/EPPlus.csproj +++ b/src/EPPlus/EPPlus.csproj @@ -1,9 +1,9 @@  net8.0;net9.0;net10.0;netstandard2.1;netstandard2.0;net462;net35 - 8.4.2.0 - 8.4.2.0 - 8.4.2 + 8.5.0.0 + 8.5.0.0 + 8.5.0 true https://epplussoftware.com EPPlus Software AB @@ -18,7 +18,7 @@ readme.md EPPlus Software AB - EPPlus 8.4.2 + EPPlus 8.5.0 IMPORTANT NOTICE! From version 5 EPPlus changes the license model using a dual license, Polyform Non Commercial / Commercial license. @@ -26,6 +26,10 @@ Commercial licenses can be purchased from https://epplussoftware.com This applies to EPPlus version 5 and later. Earlier versions are still licensed LGPL. + ## Version 8.5.0 + * + * Minor bug fixes. See https://epplussoftware.com/Developers/MinorFeaturesAndIssues + ## Version 8.4.2 * Minor bug fixes. See https://epplussoftware.com/Developers/MinorFeaturesAndIssues @@ -551,7 +555,8 @@ A list of fixed issues can be found here https://epplussoftware.com/docs/7.0/articles/fixedissues.html Version history - 8.4.2 20260204 Minor bug fixes. See https://epplussoftware.com/Developers/MinorFeaturesAndIssues + 8.5.0 20260306 Minor features and bug fixes. See https://epplussoftware.com/Developers/MinorFeaturesAndIssues + 8.4.2 20260204 Minor bug fixes. 8.4.1 20260112 Minor bug fixes. 8.4.0 20251212 Updated target frameworks. Minor bug fixes. 8.3.1 20251128 Minor bug fixes. diff --git a/src/EPPlus/ExcelCalculationCacheSettings.cs b/src/EPPlus/ExcelCalculationCacheSettings.cs new file mode 100644 index 000000000..b527b2672 --- /dev/null +++ b/src/EPPlus/ExcelCalculationCacheSettings.cs @@ -0,0 +1,42 @@ +/************************************************************************************************* + Required Notice: Copyright (C) EPPlus Software AB. + This software is licensed under PolyForm Noncommercial License 1.0.0 + and may only be used for noncommercial purposes + https://polyformproject.org/licenses/noncommercial/1.0.0/ + + A commercial license to use this software can be purchased at https://epplussoftware.com + ************************************************************************************************* + Date Author Change + ************************************************************************************************* + 02/10/2026 EPPlus Software AB Initial release + *************************************************************************************************/ +using OfficeOpenXml.FormulaParsing.Excel.Functions.MathFunctions; + +namespace OfficeOpenXml +{ + /// + /// Settings for formula calculation caching and optimization + /// + public class ExcelCalculationCacheSettings + { + /// + /// Maximum number of flattened ranges to cache for RangeCriteria functions (SUMIFS, AVERAGEIFS, COUNTIFS). + /// Default is 100. Set to 0 to disable flattened range caching. + /// + public int MaxFlattenedRanges { get; set; } = RangeCriteriaCache.DEFAULT_MAX_FLATTENED_RANGES; + + /// + /// Maximum number of match indexes to cache for RangeCriteria functions (SUMIFS, AVERAGEIFS, COUNTIFS). + /// Default is 1000. Set to 0 to disable match index caching. + /// + public int MaxMatchIndexes { get; set; } = RangeCriteriaCache.DEFAULT_MAX_MATCH_INDEXES; + + /// + /// Gets the default configuration for Excel calculation cache settings. + /// + /// Use this property to obtain a standard set of cache settings suitable for most + /// scenarios. The returned instance is immutable and can be shared safely across multiple components. + public static ExcelCalculationCacheSettings Default { get; } = new ExcelCalculationCacheSettings(); + + } +} diff --git a/src/EPPlus/ExcelPackage.cs b/src/EPPlus/ExcelPackage.cs index b9fa1c938..a6e323ec0 100644 --- a/src/EPPlus/ExcelPackage.cs +++ b/src/EPPlus/ExcelPackage.cs @@ -953,6 +953,9 @@ public void Dispose() public void Save() { CheckNotDisposed(); +#if !NET35 + Workbook.ThrowIfCalculationCanceled(); +#endif try { if (_stream is MemoryStream && _stream.Length > 0) diff --git a/src/EPPlus/ExcelPackageSettings.cs b/src/EPPlus/ExcelPackageSettings.cs index 9c1e56999..40022a641 100644 --- a/src/EPPlus/ExcelPackageSettings.cs +++ b/src/EPPlus/ExcelPackageSettings.cs @@ -71,6 +71,23 @@ public ExcelImageSettings ImageSettings return _imageSettings; } } + + private ExcelCalculationCacheSettings _calculationCacheSettings = null; + + /// + /// Cache settings for formula calculation optimization. + /// + public ExcelCalculationCacheSettings CalculationCacheSettings + { + get + { + if (_calculationCacheSettings == null) + { + _calculationCacheSettings = new ExcelCalculationCacheSettings(); + } + return _calculationCacheSettings; + } + } /// /// Any auto- or table- filters created will be applied on save. /// In the case you want to handle this manually, set this property to false. diff --git a/src/EPPlus/ExcelWorkbook.cs b/src/EPPlus/ExcelWorkbook.cs index 276d19156..b285effb1 100644 --- a/src/EPPlus/ExcelWorkbook.cs +++ b/src/EPPlus/ExcelWorkbook.cs @@ -402,6 +402,37 @@ private void GetSharedStrings() } + #region Calculation cancellation (poison flag) + + #if !NET35 + internal bool IsCalculationCanceled { get; private set; } + + internal void MarkCalculationCanceled() + { + IsCalculationCanceled = true; + } + + internal void ThrowIfCalculationCanceled() + { + if (IsCalculationCanceled) + { + throw new InvalidOperationException( + "This workbook has been left in an inconsistent state due to a canceled " + + "calculation. The workbook must be disposed and cannot be used for further " + + "operations. Reload the workbook from the source to continue."); + } + } + + /// + /// Returns true if a calculation was canceled, leaving the workbook in an inconsistent state. + /// A workbook in this state must be disposed — saving or recalculating is not permitted. + /// + public bool IsCalculationInconsistent => IsCalculationCanceled; + #endif + + #endregion + + internal void GetDefinedNames() { XmlNodeList nl = WorkbookXml.SelectNodes("//d:definedNames/d:definedName", NameSpaceManager); diff --git a/src/EPPlus/ExcelWorksheet.cs b/src/EPPlus/ExcelWorksheet.cs index 1f87e7c52..f2ecc14dc 100644 --- a/src/EPPlus/ExcelWorksheet.cs +++ b/src/EPPlus/ExcelWorksheet.cs @@ -15,9 +15,11 @@ Date Author Change using OfficeOpenXml.Constants; using OfficeOpenXml.Core; using OfficeOpenXml.Core.CellStore; +using OfficeOpenXml.Core.RangeQuadTree; using OfficeOpenXml.Core.RichValues; using OfficeOpenXml.Core.Worksheet; using OfficeOpenXml.Core.Worksheet.XmlWriter; +using OfficeOpenXml.Data.Connection.IOHandlers; using OfficeOpenXml.Data.QueryTable; using OfficeOpenXml.DataValidation; using OfficeOpenXml.Drawing; @@ -29,6 +31,7 @@ Date Author Change using OfficeOpenXml.Packaging; using OfficeOpenXml.Packaging.Ionic.Zip; using OfficeOpenXml.RichData; +using OfficeOpenXml.RichData.RichValues.WebImages; using OfficeOpenXml.Sorting; using OfficeOpenXml.Sparkline; using OfficeOpenXml.Style; @@ -48,9 +51,9 @@ Date Author Change using System.Globalization; using System.IO; using System.Linq; +using System.Security.Permissions; using System.Text; using System.Xml; -using System.Security.Permissions; namespace OfficeOpenXml { @@ -1625,7 +1628,7 @@ private void LoadCells(XmlReader xr) style = 0; SetStyleInner(address._fromRow, address._fromCol, style < 0 ? 0 : style); } - //Meta data. Meta data is only preserved by EPPlus at this point + //Cell metadata/Value metadata var cm = xr.GetAttribute("cm"); var vm = xr.GetAttribute("vm"); if (cm != null || vm != null) @@ -1666,36 +1669,14 @@ private void LoadCells(XmlReader xr) } else if (xr.LocalName == "v") { + if (currentVm > 0) { - var rd = _richDataStore.GetRichValueByOneBasedIndex(Convert.ToInt32(currentVm)); - rd?.SetStructure(_package.Workbook.RichData.Db); - if (rd != null && rd.Structure.StructureType == RichDataStructureTypes.LocalImage) - { - var rdLi = rd.As.LocalImage; - var pic = new ExcelCellPicture(currentVm, rdLi.ImageUri, Workbook._package.PictureStore, ExcelCellPictureTypes.LocalImage) - { - CellAddress = new ExcelAddress(this.Name, row, col, row, col), - AltText = rdLi.Text, - CalcOrigin = rdLi.CalcOrigin ?? CalcOrigins.None - }; - Workbook._package.PictureStore.AddImage(pic.GetImageBytes(), rdLi.ImageUri, null); - SetValueInner(row, col, pic); - while (!(xr.NodeType == XmlNodeType.EndElement && xr.LocalName == "c")) - { - xr.Read(); - } - } - else if (rd != null && rd.Structure.StructureType == RichDataStructureTypes.WebImage) + var richValue = _richDataStore.GetRichValueByOneBasedIndex(currentVm); + if (richValue != null && richValue.IsCellImage) { - var rdWi = rd.As.WebImage; - var pic = new ExcelCellPicture(currentVm, rdWi.ImageUri, Workbook._package.PictureStore, ExcelCellPictureTypes.WebImage) - { - CellAddress = new ExcelAddress(this.Name, row, col, row, col), - AltText = rdWi.Text, - CalcOrigin = rdWi.CalcOrigin ?? CalcOrigins.None - }; - Workbook._package.PictureStore.AddImage(pic.GetImageBytes(), rdWi.ImageUri, null); + var picManager = new CellPicturesManager(this); + var pic = picManager.BuildCellPicture(row, col, currentVm, richValue); SetValueInner(row, col, pic); while (!(xr.NodeType == XmlNodeType.EndElement && xr.LocalName == "c")) { diff --git a/src/EPPlus/FormulaParsing/CalculateExtensions.cs b/src/EPPlus/FormulaParsing/CalculateExtensions.cs index 5d6e6b780..6db4e071a 100644 --- a/src/EPPlus/FormulaParsing/CalculateExtensions.cs +++ b/src/EPPlus/FormulaParsing/CalculateExtensions.cs @@ -62,6 +62,10 @@ public static void Calculate(this ExcelWorkbook workbook, ActionCalculation options public static void Calculate(this ExcelWorkbook workbook, ExcelCalculationOption options) { + #if !NET35 + workbook.ThrowIfCalculationCanceled(); // Guard: prevent recalc on poisoned workbook + #endif + Init(workbook); var filterInfo = new FilterInfo(workbook); @@ -74,12 +78,25 @@ public static void Calculate(this ExcelWorkbook workbook, ExcelCalculationOption } //CalcChain(workbook, workbook.FormulaParser, dc, options); - var dc=RpnFormulaExecution.Execute(workbook, options); - if (workbook.FormulaParser.Logger != null) +#if !NET35 + try { - var msg = string.Format("Calculation done...number of cells parsed: {0}", dc.processedCells.Count); - workbook.FormulaParser.Logger.Log(msg); +#endif + var dc =RpnFormulaExecution.Execute(workbook, options); + dc._parsingContext.RangeCriteriaCache?.Clear(); + if (workbook.FormulaParser.Logger != null) + { + var msg = string.Format("Calculation done...number of cells parsed: {0}", dc.processedCells.Count); + workbook.FormulaParser.Logger.Log(msg); + } +#if !NET35 } + catch (OperationCanceledException) + { + workbook.MarkCalculationCanceled(); + throw; + } +#endif } internal static RpnOptimizedDependencyChain CalculateWithDC(this ExcelWorkbook workbook, Action configHandler) { @@ -157,7 +174,21 @@ public static void Calculate(this ExcelWorksheet worksheet, Action @@ -193,13 +224,23 @@ public static void Calculate(this ExcelRangeBase range, ActionCalculation options public static void Calculate(this ExcelRangeBase range, ExcelCalculationOption options) { - Init(range._workbook); - //var parser = range._workbook.FormulaParser; - //var filterInfo = new FilterInfo(range._workbook); - //parser.InitNewCalc(filterInfo); - //var dc = DependencyChainFactory.Create(range, options); - //CalcChain(range._workbook, parser, dc, options); - var dc = RpnFormulaExecution.Execute(range, options); +#if !NET35 + range._workbook.ThrowIfCalculationCanceled(); + try + { +#endif + Init(range._workbook); + var dc = RpnFormulaExecution.Execute(range, options); + // Clear RangeCriteriaCache after calculation completes + dc._parsingContext.RangeCriteriaCache?.Clear(); +#if !NET35 + } + catch (OperationCanceledException) + { + range._workbook.MarkCalculationCanceled(); + throw; + } +#endif } /// @@ -230,7 +271,9 @@ public static object Calculate(this ExcelWorksheet worksheet, string Formula, Ex //var filterInfo = new FilterInfo(worksheet.Workbook); //parser.InitNewCalc(filterInfo); if (Formula[0] == '=') Formula = Formula.Substring(1); //Remove any starting equal sign - return RpnFormulaExecution.ExecuteFormula(worksheet.Workbook, Formula,new FormulaCellAddress(worksheet.IndexInList, -1, 0), options); + var result = RpnFormulaExecution.ExecuteFormula(worksheet.Workbook, Formula,new FormulaCellAddress(worksheet.IndexInList, -1, 0), options); + worksheet.Workbook.FormulaParser.ParsingContext?.RangeCriteriaCache?.Clear(); + return result; } catch (Exception ex) { diff --git a/src/EPPlus/FormulaParsing/DependencyChain/ArrayFormulaOutput.cs b/src/EPPlus/FormulaParsing/DependencyChain/ArrayFormulaOutput.cs index c44e54885..f8b83172a 100644 --- a/src/EPPlus/FormulaParsing/DependencyChain/ArrayFormulaOutput.cs +++ b/src/EPPlus/FormulaParsing/DependencyChain/ArrayFormulaOutput.cs @@ -10,17 +10,18 @@ Date Author Change ************************************************************************************************* 05/14/2024 EPPlus Software AB Initial release EPPlus 7 *************************************************************************************************/ -using OfficeOpenXml.FormulaParsing.LexicalAnalysis; using OfficeOpenXml.Core.CellStore; -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using OfficeOpenXml.FormulaParsing.Excel.Functions.RefAndLookup; using OfficeOpenXml.Filter; +using OfficeOpenXml.FormulaParsing.Excel.Functions.RefAndLookup; using OfficeOpenXml.FormulaParsing.FormulaExpressions; +using OfficeOpenXml.FormulaParsing.LexicalAnalysis; +using OfficeOpenXml.FormulaParsing.Ranges; using OfficeOpenXml.Utils; using OfficeOpenXml.Utils.TypeConversion; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; namespace OfficeOpenXml.FormulaParsing { @@ -37,7 +38,14 @@ internal static void FillArrayFromRangeInfo(RpnFormula f, IRangeInfo array, Rang var rows = sf.EndRow - sf.StartRow + 1; var cols = sf.EndCol - sf.StartCol + 1; var wsIx = ws.IndexInList; - for (int r = 0; r < rows; r++) + + // Determine physical boundary and default value for virtual rows + var virtualImr = array as InMemoryRange; + var hasVirtual = virtualImr != null && virtualImr.HasVirtualRows; + var physicalRowLimit = hasVirtual ? Math.Min(rows, virtualImr.PhysicalRows) : rows; + + // Phase 1: Physical rows - read actual cell values via GetOffset + for (int r = 0; r < physicalRowLimit; r++) { for (int c = 0; c < cols; c++) { @@ -46,9 +54,8 @@ internal static void FillArrayFromRangeInfo(RpnFormula f, IRangeInfo array, Rang if (r < nr && c < nc) { var val = array.GetOffset(r, c); - if(ConvertUtil.IsNumeric(val) && val is double dbl && dbl == 0d) + if (ConvertUtil.IsNumeric(val) && val is double dbl && dbl == 0d) { - // avoid -0 val = 0d; } ws.SetValueInner(row, col, val ?? 0D); @@ -58,7 +65,35 @@ internal static void FillArrayFromRangeInfo(RpnFormula f, IRangeInfo array, Rang ws.SetValueInner(row, col, ErrorValues.NAError); } var id = ExcelCellBase.GetCellId(wsIx, row, col); - depChain.processedCells.Add(id); + depChain.processedCells.Add(id); + } + } + + // Phase 2: Virtual rows - write pre-computed default value directly + if (hasVirtual && physicalRowLimit < rows) + { + var defaultVal = virtualImr.VirtualDefaultValue ?? 0D; + if (ConvertUtil.IsNumeric(defaultVal) && defaultVal is double dblDefault && dblDefault == 0d) + { + defaultVal = 0d; + } + for (int r = physicalRowLimit; r < rows; r++) + { + for (int c = 0; c < cols; c++) + { + var row = sr + r; + var col = sc + c; + if (r < nr && c < nc) + { + ws.SetValueInner(row, col, defaultVal); + } + else + { + ws.SetValueInner(row, col, ErrorValues.NAError); + } + var id = ExcelCellBase.GetCellId(wsIx, row, col); + depChain.processedCells.Add(id); + } } } @@ -74,7 +109,7 @@ private static bool HasSpill(ExcelWorksheet ws, int fIx, int startRow, int start { if (r == startRow && c == startColumn) continue; object f = -1; - if (fIx!=-1 && ws._formulas.Exists(r, c, ref f) && f != null) + if (fIx != -1 && ws._formulas.Exists(r, c, ref f) && f != null) { if (f is int intfIx && intfIx == fIx) { @@ -87,7 +122,7 @@ private static bool HasSpill(ExcelWorksheet ws, int fIx, int startRow, int start else { var v = ws.GetValueInner(r, c); - if(v!=null) + if (v != null) { rowOff = r - startRow; colOff = c - startColumn; @@ -98,7 +133,7 @@ private static bool HasSpill(ExcelWorksheet ws, int fIx, int startRow, int start } rowOff = colOff = 0; return false; - } + } internal static SimpleAddress[] FillDynamicArrayFromRangeInfo(RpnFormula f, IRangeInfo array, RangeHashset rd, RpnOptimizedDependencyChain depChain) { var nr = array.Size.NumberOfRows; @@ -107,13 +142,13 @@ internal static SimpleAddress[] FillDynamicArrayFromRangeInfo(RpnFormula f, IRan var startRow = f._row; var startCol = f._column; var wsIx = ws.IndexInList; - + f._flags |= FormulaFlags.IsDynamic; var md = depChain._parsingContext.Package.Workbook.Metadata; //d.GetDynamicArrayIndex(out int cm); md.GetDynamicArrayId(out uint cm); var metaData = f._ws._metadataStore.GetValue(startRow, startCol); - metaData.cm= cm; + metaData.cm = cm; f._ws._metadataStore.SetValue(f._row, f._column, metaData); @@ -134,7 +169,7 @@ internal static SimpleAddress[] FillDynamicArrayFromRangeInfo(RpnFormula f, IRan } } SimpleAddress[] dirtyRange; - if(f._arrayIndex==-1) + if (f._arrayIndex == -1) { var endRow = startRow + array.Size.NumberOfRows - 1; var endCol = f._column + array.Size.NumberOfCols - 1; @@ -151,7 +186,7 @@ internal static SimpleAddress[] FillDynamicArrayFromRangeInfo(RpnFormula f, IRan CleanupSharedFormulaValues(f, ws, sf, endRow, endCol); sf.EndRow = endRow; sf.EndCol = endCol; - + } FillArrayFromRangeInfo(f, array, rd, depChain); return dirtyRange; @@ -212,10 +247,10 @@ private static void CleanupSharedFormulaValues(RpnFormula f, ExcelWorksheet ws, } } - private static SimpleAddress[] GetDirtyRange(int fromRow, int fromCol, int toRow, int toCol, int prevToRow=0, int prevToCol=0) + private static SimpleAddress[] GetDirtyRange(int fromRow, int fromCol, int toRow, int toCol, int prevToRow = 0, int prevToCol = 0) { - if(prevToRow == 0) prevToRow = fromRow; - if(prevToCol == 0) prevToCol = fromCol; + if (prevToRow == 0) prevToRow = fromRow; + if (prevToCol == 0) prevToCol = fromCol; if (prevToRow == toRow && prevToCol == toCol) { return new SimpleAddress[0]; @@ -226,15 +261,15 @@ private static SimpleAddress[] GetDirtyRange(int fromRow, int fromCol, int toRow { new SimpleAddress(fromRow, Math.Min(prevToCol+1, toCol+1), Math.Min(prevToRow, toRow), Math.Max(prevToCol, toCol)), new SimpleAddress(Math.Min(prevToRow + 1, toRow + 1), fromCol, Math.Max(prevToRow, toRow), Math.Max(prevToCol, toCol)) - }; + }; } - else if(prevToRow != toRow) + else if (prevToRow != toRow) { - return new SimpleAddress[] { new SimpleAddress(Math.Min(prevToRow+1, toRow), fromCol, Math.Max(prevToRow, toRow), Math.Max(prevToCol,toCol)) }; - } + return new SimpleAddress[] { new SimpleAddress(Math.Min(prevToRow + 1, toRow), fromCol, Math.Max(prevToRow, toRow), Math.Max(prevToCol, toCol)) }; + } else { - return new SimpleAddress[] { new SimpleAddress(fromRow, Math.Min(prevToCol+1, toCol), Math.Max(prevToRow, toRow), Math.Max(prevToCol, toCol)) }; + return new SimpleAddress[] { new SimpleAddress(fromRow, Math.Min(prevToCol + 1, toCol), Math.Max(prevToRow, toRow), Math.Max(prevToCol, toCol)) }; } } diff --git a/src/EPPlus/FormulaParsing/DependencyChain/RpnFormulaExecution.cs b/src/EPPlus/FormulaParsing/DependencyChain/RpnFormulaExecution.cs index 2fb9c29c6..42e29e4c8 100644 --- a/src/EPPlus/FormulaParsing/DependencyChain/RpnFormulaExecution.cs +++ b/src/EPPlus/FormulaParsing/DependencyChain/RpnFormulaExecution.cs @@ -46,6 +46,9 @@ internal static RpnOptimizedDependencyChain Execute(ExcelWorkbook wb, ExcelCalcu var depChain = new RpnOptimizedDependencyChain(wb, options); foreach (var ws in wb.Worksheets) { +#if !NET35 + options.CancellationToken.ThrowIfCancellationRequested(); +#endif if (ws.IsChartSheet == false) { ExecuteChain(depChain, ws.Cells, options, true); @@ -118,9 +121,15 @@ private static void ExecuteChain(RpnOptimizedDependencyChain depChain, ExcelRang { var ws = range.Worksheet; RpnFormula f = null; +#if !NET35 + var ct = options.CancellationToken; // Cache locally — avoids property lookup in hot loop +#endif var fs = new CellStoreEnumerator(ws._formulas, range._fromRow, range._fromCol, range._toRow, range._toCol); while (fs.Next()) { +#if !NET35 + ct.ThrowIfCancellationRequested(); // P0 – per cell +#endif if (fs.Value == null || fs.Value.ToString().Trim() == "") continue; var id = ExcelCellBase.GetCellId(ws.IndexInList, fs.Row, fs.Column); if (depChain.processedCells.Contains(id) == false) @@ -132,6 +141,12 @@ private static void ExecuteChain(RpnOptimizedDependencyChain depChain, ExcelRang CalculateFormulaChain(depChain, f, options, writeToCell); } } +#if !NET35 + catch (OperationCanceledException) + { + throw; // Must propagate — do not swallow + } +#endif catch (CircularReferenceException) { throw; @@ -240,6 +255,12 @@ private static void ExecuteChain(RpnOptimizedDependencyChain depChain, ExcelName ExecuteName(depChain, name, options, writeToCell); } } +#if !NET35 + catch (OperationCanceledException) + { + throw; // Must propagate + } +#endif catch (CircularReferenceException) { throw; @@ -289,6 +310,12 @@ private static object ExecuteChain(RpnOptimizedDependencyChain depChain, ExcelWo f.SetFormula(formula, depChain); return CalculateFormulaChain(depChain, f, options, writeToCell).Result; } +#if !NET35 + catch (OperationCanceledException) + { + throw; // Must propagate + } +#endif catch (CircularReferenceException) { throw; @@ -309,6 +336,12 @@ private static object ExecuteChain(RpnOptimizedDependencyChain depChain, ExcelWo f._row = -1; return CalculateFormulaChain(depChain, f, options, writeToCell).Result; } +#if !NET35 + catch (OperationCanceledException) + { + throw; // Must propagate + } +#endif catch (CircularReferenceException) { throw; @@ -419,6 +452,9 @@ private static CompileResult CalculateFormulaChain(RpnOptimizedDependencyChain d ExecuteFormula: try { +#if !NET35 + options.CancellationToken.ThrowIfCancellationRequested(); // P0 – per dependency step +#endif SetCurrentCell(depChain, f); var ws = f._ws; if (f._tokenIndex < f._tokens.Count) @@ -568,6 +604,12 @@ private static CompileResult CalculateFormulaChain(RpnOptimizedDependencyChain d goto ExecuteFormula; } +#if !NET35 + catch (OperationCanceledException) + { + throw; // Must propagate + } +#endif catch (CircularReferenceException) { throw; @@ -1562,7 +1604,7 @@ private static bool PreExecFunc(RpnOptimizedDependencyChain depChain, RpnFormula { args = CompileFunctionArguments(f, funcExp); funcExp.Status = ExpressionStatus.CanCompile; - return funcExp.SetArguments(args); + return funcExp.SetArguments(args, depChain._parsingContext); } else { @@ -1577,7 +1619,7 @@ private static bool PreExecFunc(RpnOptimizedDependencyChain depChain, RpnFormula else { args = CompileFunctionArguments(f, funcExp); - return funcExp.SetArguments(args); + return funcExp.SetArguments(args, depChain._parsingContext); } return false; } diff --git a/src/EPPlus/FormulaParsing/EpplusExcelDataProvider.cs b/src/EPPlus/FormulaParsing/EpplusExcelDataProvider.cs index 922835df6..c51ca54c1 100644 --- a/src/EPPlus/FormulaParsing/EpplusExcelDataProvider.cs +++ b/src/EPPlus/FormulaParsing/EpplusExcelDataProvider.cs @@ -24,6 +24,7 @@ Date Author Change using OfficeOpenXml.FormulaParsing.Ranges; using System.Runtime.InteropServices; using OfficeOpenXml.Utils.String; +using OfficeOpenXml.Style; namespace OfficeOpenXml.FormulaParsing { @@ -641,7 +642,21 @@ public override string GetFormat(object value, string format, out bool isValidFo { isValidFormat = true; var ws = _currentWorksheet ?? _context.CurrentWorksheet; - var arg = new NumberFormatToTextArgs(ws, _context.CurrentCell.Row, _context.CurrentCell.Column, value, ws.GetStyleInner(_context.CurrentCell.Row, _context.CurrentCell.Column)); + + var existingId = ExcelNumberFormat.GetFromBuildIdFromFormat(format); + + NumberFormatToTextArgs arg; + if (existingId == int.MinValue) + { + //The format does not have a corresponding styleId + //Still allow the NumberFormatToTextHandler to see the format + arg = new NumberFormatToTextArgs(ws, _context.CurrentCell.Row, _context.CurrentCell.Column, value, format); + } + else + { + arg = new NumberFormatToTextArgs(ws, _context.CurrentCell.Row, _context.CurrentCell.Column, value, existingId); + } + arg.FromFormula = true; return _workbook.NumberFormatToTextHandler(arg); } } diff --git a/src/EPPlus/FormulaParsing/Excel/Functions/ExcelFunction.cs b/src/EPPlus/FormulaParsing/Excel/Functions/ExcelFunction.cs index 7ce9e6d1a..e6de1e09d 100644 --- a/src/EPPlus/FormulaParsing/Excel/Functions/ExcelFunction.cs +++ b/src/EPPlus/FormulaParsing/Excel/Functions/ExcelFunction.cs @@ -122,8 +122,9 @@ internal CompileResult ExecuteInternal(IList arguments, Parsin /// /// The function arguments that will be supplied to the execute method. /// The index of the argument that should be adjusted. + /// The parsing context /// A queue of addresses that will be calculated before calling the Execute function. - public virtual void GetNewParameterAddress(IList args, int index, ref Queue addresses) + public virtual void GetNewParameterAddress(IList args, int index, ParsingContext ctx, ref Queue addresses) { } diff --git a/src/EPPlus/FormulaParsing/Excel/Functions/MathFunctions/AverageIf.cs b/src/EPPlus/FormulaParsing/Excel/Functions/MathFunctions/AverageIf.cs index 99f8fbb94..6fc9f8bdd 100644 --- a/src/EPPlus/FormulaParsing/Excel/Functions/MathFunctions/AverageIf.cs +++ b/src/EPPlus/FormulaParsing/Excel/Functions/MathFunctions/AverageIf.cs @@ -42,14 +42,14 @@ public override void ConfigureArrayBehaviour(ArrayBehaviourConfig config) { config.SetArrayParameterIndexes(1); } - public override void GetNewParameterAddress(IList args, int index, ref Queue addresses) + public override void GetNewParameterAddress(IList args, int index, ParsingContext ctx, ref Queue addresses) { if (index == 2) { if (args[0].Result is IRangeInfo rangeInfo && args[2].Result is IRangeInfo valueRange) { var rv = new RangeOrValue { Range = rangeInfo }; - var mi = GetMatchIndexes(rv, args[1].Result, null); + var mi = GetMatchIndexes(rv, args[1].Result, ctx); EnqueueMatchingAddresses(valueRange, mi, ref addresses); } } diff --git a/src/EPPlus/FormulaParsing/Excel/Functions/MathFunctions/AverageIfs.cs b/src/EPPlus/FormulaParsing/Excel/Functions/MathFunctions/AverageIfs.cs index 7ecd607ca..50c63d6ae 100644 --- a/src/EPPlus/FormulaParsing/Excel/Functions/MathFunctions/AverageIfs.cs +++ b/src/EPPlus/FormulaParsing/Excel/Functions/MathFunctions/AverageIfs.cs @@ -50,19 +50,21 @@ internal class AverageIfs : RangeCriteriaFunction } return FunctionParameterInformation.AdjustParameterAddress; })); - public override void GetNewParameterAddress(IList args, int index, ref Queue addresses) + + public override void GetNewParameterAddress(IList args, int index, ParsingContext ctx, ref Queue addresses) { if (index == 0 && args[0].Result is IRangeInfo valueRange) { - IEnumerable matchIndexes = GetMatchingIndicesFromArguments(1, args); + IEnumerable matchIndexes = GetMatchingIndicesFromArguments(1, args, ctx, convertNumericStrings: false); EnqueueMatchingAddresses(valueRange, matchIndexes, ref addresses); } else if (args[index].Result is IRangeInfo criteriaRange) { - IEnumerable matchIndexes = GetMatchingIndicesFromArguments(1, args, index); + IEnumerable matchIndexes = GetMatchingIndicesFromArguments(1, args, ctx, index, convertNumericStrings: false); EnqueueMatchingAddresses(criteriaRange, matchIndexes, ref addresses); } } + public override CompileResult Execute(IList arguments, ParsingContext context) { var valueRange = arguments[0].ValueAsRangeInfo; @@ -103,19 +105,13 @@ public override CompileResult Execute(IList arguments, Parsing } private double GetAvgValue(ParsingContext context, IRangeInfo valueRange, List argRanges, List criteria, int row, int col, out ExcelErrorValue ev) { - IEnumerable matchIndexes = GetMatchIndexes(argRanges[0], GetCriteriaValue(criteria[0], row, col), context, false); - var enumerable = matchIndexes as IList ?? matchIndexes.ToList(); - for (var ix = 1; ix < argRanges.Count && enumerable.Any(); ix++) - { - var indexes = GetMatchIndexes(argRanges[ix], GetCriteriaValue(criteria[ix], row, col), context, false); - matchIndexes = matchIndexes.Intersect(indexes); - } - var sumRange = RangeFlattener.FlattenRangeObject(valueRange); + GetFilteredValueRange(context, valueRange, argRanges, criteria, row, col, out List matchIndexes, out List flattenedRange, convertNumericString: false); + KahanSum sum = 0d; var count = 0; foreach (var index in matchIndexes) { - var obj = sumRange[index]; + var obj = flattenedRange[index]; if (obj is ExcelErrorValue e1) { ev = e1; @@ -127,14 +123,15 @@ private double GetAvgValue(ParsingContext context, IRangeInfo valueRange, List args, int index, ref Queue addresses) + public override void GetNewParameterAddress(IList args, int index, ParsingContext ctx, ref Queue addresses) { if (args[index].Result is IRangeInfo criteriaRange) { - IEnumerable matchIndexes = GetMatchingIndicesFromArguments(0, args, index); + IEnumerable matchIndexes = GetMatchingIndicesFromArguments(0, args, ctx, index); EnqueueMatchingAddresses(criteriaRange, matchIndexes, ref addresses); } } @@ -76,16 +76,24 @@ public override CompileResult Execute(IList arguments, Parsing } } + private double GetCountValue(ParsingContext context, List argRanges, List criteria, int row, int col) { - IEnumerable matchIndexes = GetMatchIndexes(argRanges[0], GetCriteriaValue(criteria[0], row, col), context, false); - var enumerable = matchIndexes as IList ?? matchIndexes.ToList(); - for (var ix = 1; ix < argRanges.Count && enumerable.Any(); ix++) + var matchIndexes = GetMatchIndexes(argRanges[0], GetCriteriaValue(criteria[0], row, col), context, false); + + // Use HashSet.IntersectWith for multi-criteria (more efficient than LINQ Intersect) + if (argRanges.Count > 1) { - var indexes = GetMatchIndexes(argRanges[ix], GetCriteriaValue(criteria[ix], row, col), context, false); - matchIndexes = matchIndexes.Intersect(indexes); + var hashSet = new HashSet(matchIndexes); + for (var ix = 1; ix < argRanges.Count && hashSet.Count > 0; ix++) + { + var indexes = GetMatchIndexes(argRanges[ix], GetCriteriaValue(criteria[ix], row, col), context, false); + hashSet.IntersectWith(indexes); + } + return (double)hashSet.Count; } - return (double)matchIndexes.Count(); + + return (double)matchIndexes.Count; } } } diff --git a/src/EPPlus/FormulaParsing/Excel/Functions/MathFunctions/RangeCriteriaCache.cs b/src/EPPlus/FormulaParsing/Excel/Functions/MathFunctions/RangeCriteriaCache.cs new file mode 100644 index 000000000..2728c4019 --- /dev/null +++ b/src/EPPlus/FormulaParsing/Excel/Functions/MathFunctions/RangeCriteriaCache.cs @@ -0,0 +1,244 @@ +/************************************************************************************************* + Required Notice: Copyright (C) EPPlus Software AB. + This software is licensed under PolyForm Noncommercial License 1.0.0 + and may only be used for noncommercial purposes + https://polyformproject.org/licenses/noncommercial/1.0.0/ + + A commercial license to use this software can be purchased at https://epplussoftware.com + ************************************************************************************************* + Date Author Change + ************************************************************************************************* + 02/09/2026 EPPlus Software AB RangeCriteria performance optimization + *************************************************************************************************/ +using System.Collections.Generic; +using OfficeOpenXml.FormulaParsing.LexicalAnalysis; + +namespace OfficeOpenXml.FormulaParsing.Excel.Functions.MathFunctions +{ + /// + /// Cache for RangeCriteria functions (SumIfs, AverageIfs, CountIfs) to improve performance + /// during calculation by avoiding repeated expensive operations. + /// Uses FIFO eviction policy to limit memory usage and integer keys for efficient lookups. + /// + internal class RangeCriteriaCache + { + internal const int DEFAULT_MAX_FLATTENED_RANGES = 100; + internal const int DEFAULT_MAX_MATCH_INDEXES = 1000; + + private readonly ExcelPackage _package; + private readonly ExcelCalculationCacheSettings _settings; + + private Dictionary> _flattenedRanges; + private Dictionary> _matchIndexes; + private HashSet _rangesWithFormulas; + + private Queue _flattenedRangesOrder; + private Queue _matchIndexesOrder; + + private Dictionary _keyToId; + private int _nextId = 1; + + /// + /// Creates a new RangeCriteriaCache with specified limits + /// + /// The ExcelPackage where the cache operates. + public RangeCriteriaCache(ExcelPackage package) + { + _package = package; + if(package != null) + { + _settings = package.Settings.CalculationCacheSettings; + } + else + { + _settings = ExcelCalculationCacheSettings.Default; + } + } + + /// + /// Gets a flattened range from cache, or null if not cached + /// + public List GetFlattenedRange(FormulaRangeAddress address) + { + if (_flattenedRanges == null || address == null) return null; + + var key = CreateRangeKey(address); + if (_keyToId != null && _keyToId.TryGetValue(key, out var id)) + { + if (_flattenedRanges.TryGetValue(id, out var cached)) + { + return cached; + } + } + return null; + } + + /// + /// Stores a flattened range in cache with FIFO eviction if cache is full + /// + public void SetFlattenedRange(FormulaRangeAddress address, List flattenedRange) + { + if (address == null || flattenedRange == null) return; + + // Guard against disabled caching + var maxCapacity = _settings?.MaxFlattenedRanges ?? DEFAULT_MAX_FLATTENED_RANGES; + if (maxCapacity <= 0) return; + + if (_flattenedRanges == null) + { + _flattenedRanges = new Dictionary>(); + _flattenedRangesOrder = new Queue(); + _keyToId = new Dictionary(); + } + + var key = CreateRangeKey(address); + + if (!_keyToId.TryGetValue(key, out var id)) + { + id = _nextId++; + _keyToId[key] = id; + + // Evict entries until we're under the limit + // This handles both: cache full + capacity reduced scenarios + while (_flattenedRanges.Count >= maxCapacity) + { + var oldestId = _flattenedRangesOrder.Dequeue(); + _flattenedRanges.Remove(oldestId); + } + + _flattenedRangesOrder.Enqueue(id); + } + + _flattenedRanges[id] = flattenedRange; + } + + /// + /// Gets cached match indexes for a range+criteria combination. + /// Returns null if the range contains formulas (which might change during calculation). + /// + public List GetMatchIndexes(FormulaRangeAddress rangeAddress, object criteriaValue) + { + if (_matchIndexes == null || rangeAddress == null || criteriaValue == null) return null; + + var rangeKey = CreateRangeKey(rangeAddress); + if (_keyToId != null && _keyToId.TryGetValue(rangeKey, out var rangeId)) + { + // Don't use cache if this range contains formulas that might change + if (_rangesWithFormulas != null && _rangesWithFormulas.Contains(rangeId)) + { + return null; + } + } + + var key = CreateMatchIndexKey(rangeAddress, criteriaValue); + if (_keyToId != null && _keyToId.TryGetValue(key, out var id)) + { + if (_matchIndexes.TryGetValue(id, out var cached)) + { + return cached; + } + } + return null; + } + + /// + /// Stores match indexes for a range+criteria combination with FIFO eviction if cache is full + /// + /// + /// Stores match indexes for a range+criteria combination with FIFO eviction if cache is full + /// + public void SetMatchIndexes(FormulaRangeAddress rangeAddress, object criteriaValue, List matchIndexes) + { + if (rangeAddress == null || criteriaValue == null || matchIndexes == null) return; + + // Guard against disabled caching + var maxCapacity = _settings?.MaxMatchIndexes ?? DEFAULT_MAX_MATCH_INDEXES; + if (maxCapacity <= 0) return; + + if (_keyToId == null) + { + _keyToId = new Dictionary(); + } + + // Track if this range has formulas + var rangeKey = CreateRangeKey(rangeAddress); + if (rangeAddress.HasFormulas(_package)) + { + if (!_keyToId.TryGetValue(rangeKey, out var rangeId)) + { + rangeId = _nextId++; + _keyToId[rangeKey] = rangeId; + } + + if (_rangesWithFormulas == null) + { + _rangesWithFormulas = new HashSet(); + } + _rangesWithFormulas.Add(rangeId); + // Don't cache matchIndexes for ranges with formulas + return; + } + + if (_matchIndexes == null) + { + _matchIndexes = new Dictionary>(); + _matchIndexesOrder = new Queue(); + } + + var key = CreateMatchIndexKey(rangeAddress, criteriaValue); + + // Get or create ID for this key + if (!_keyToId.TryGetValue(key, out var id)) + { + id = _nextId++; + _keyToId[key] = id; + + // Evict entries until we're under the limit + // This handles both: cache full + capacity reduced scenarios + while (_matchIndexes.Count >= maxCapacity) + { + var oldestId = _matchIndexesOrder.Dequeue(); + _matchIndexes.Remove(oldestId); + // Note: we keep the key in _keyToId to avoid ID reuse within same calculation + } + + _matchIndexesOrder.Enqueue(id); + } + + _matchIndexes[id] = matchIndexes; + } + + /// + /// Clears all cached data. Should be called after calculation completes. + /// + public void Clear() + { + _flattenedRanges?.Clear(); + _matchIndexes?.Clear(); + _rangesWithFormulas?.Clear(); + _flattenedRangesOrder?.Clear(); + _matchIndexesOrder?.Clear(); + _keyToId?.Clear(); + _nextId = 1; + } + + private string CreateRangeKey(FormulaRangeAddress address) + { + // Create a unique key for the range address + return $"{address.WorksheetIx}:{address.FromRow}:{address.FromCol}:{address.ToRow}:{address.ToCol}"; + } + + private string CreateMatchIndexKey(FormulaRangeAddress rangeAddress, object criteriaValue) + { + // Create a unique key combining range address and criteria value + var rangeKey = CreateRangeKey(rangeAddress); + var cv = criteriaValue; + if(criteriaValue is RangeOrValue rov) + { + cv = rov.Value != null ? rov.Value : rov?.Range?.Address.ToString() ?? "null"; + } + var criteriaKey = cv?.ToString() ?? "null"; + return $"{rangeKey}|{criteriaKey}"; + } + } +} \ No newline at end of file diff --git a/src/EPPlus/FormulaParsing/Excel/Functions/MathFunctions/RangeCriteriaFunction.cs b/src/EPPlus/FormulaParsing/Excel/Functions/MathFunctions/RangeCriteriaFunction.cs index 8c43cc620..69014fcdf 100644 --- a/src/EPPlus/FormulaParsing/Excel/Functions/MathFunctions/RangeCriteriaFunction.cs +++ b/src/EPPlus/FormulaParsing/Excel/Functions/MathFunctions/RangeCriteriaFunction.cs @@ -10,10 +10,7 @@ Date Author Change ************************************************************************************************* 01/27/2020 EPPlus Software AB Initial release EPPlus 5 *************************************************************************************************/ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; +using OfficeOpenXml.FormulaParsing.Excel.Functions.RefAndLookup; using OfficeOpenXml.FormulaParsing.ExcelUtilities; using OfficeOpenXml.FormulaParsing.FormulaExpressions; using OfficeOpenXml.FormulaParsing.LexicalAnalysis; @@ -21,6 +18,10 @@ Date Author Change using OfficeOpenXml.FormulaParsing.Utilities; using OfficeOpenXml.Sorting.Internal; using OfficeOpenXml.Utils.TypeConversion; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; using Require = OfficeOpenXml.FormulaParsing.Utilities.Require; @@ -45,7 +46,7 @@ protected static void GetArguments(ParsingContext context, IList GetMatchIndexes(RangeOrValue rangeOrValue, object searched, ParsingContext ctx, bool convertNumericString = true) { - var expressionEvaluator = new ExpressionEvaluator(ctx); + // Try to get from cache if we have a range with address + if (rangeOrValue.Range != null && rangeOrValue.Range.Address != null && searched != null) + { + var cached = ctx.RangeCriteriaCache?.GetMatchIndexes(rangeOrValue.Range.Address, searched); + if (cached != null) + { + return cached; + } + } + var result = new List(); var internalIndex = 0; + if (rangeOrValue.Range != null) { var rangeInfo = rangeOrValue.Range; var address = rangeInfo.GetAddressDimensionAdjusted(0).Address; + for (var row = address.FromRow; row <= address.ToRow; row++) { for (var col = address.FromCol; col <= address.ToCol; col++) @@ -165,6 +178,7 @@ protected List GetMatchIndexes(RangeOrValue rangeOrValue, object searched, { if (searched is RangeOrValue critRange) { + // Handle range criteria (less common case) - use original Evaluate if (critRange.Range != null) { foreach (var cell in critRange.Range) @@ -175,7 +189,7 @@ protected List GetMatchIndexes(RangeOrValue rangeOrValue, object searched, } } } - else if(critRange.Value != null && Evaluate(candidate, critRange.Value, ctx, convertNumericString)) + else if (critRange.Value != null && Evaluate(candidate, critRange.Value, ctx, convertNumericString)) { result.Add(internalIndex); } @@ -190,23 +204,32 @@ protected List GetMatchIndexes(RangeOrValue rangeOrValue, object searched, } } } - else if (Evaluate(rangeOrValue.Value, searched, ctx, convertNumericString)) + else if (searched != null && Evaluate(rangeOrValue.Value, searched, ctx, convertNumericString)) { result.Add(internalIndex); } + + // Cache the result if we have a range with address + // Pass rangeHasFormulas flag so cache knows whether to store it + if (rangeOrValue.Range != null && rangeOrValue.Range.Address != null && searched != null) + { + ctx.RangeCriteriaCache?.SetMatchIndexes(rangeOrValue.Range.Address, searched, result); + } + return result; } + protected static Queue EnqueueMatchingAddresses(IRangeInfo valueRange, IEnumerable matchIndexes, ref Queue addresses) { - if(addresses==null) + if (addresses == null) { - addresses=new Queue(); + addresses = new Queue(); } var pIx = int.MinValue; var valueAddress = valueRange.GetAddressDimensionAdjusted(0); var extRef = valueAddress.ExternalReferenceIx; var wsIx = valueAddress.WorksheetIx; - FormulaRangeAddress currentAddress=null; + FormulaRangeAddress currentAddress = null; if (valueAddress.FromCol == valueAddress.ToCol) { var c = valueAddress.FromCol; @@ -246,7 +269,7 @@ protected static Queue EnqueueMatchingAddresses(IRangeInfo return addresses; } - protected IEnumerable GetMatchingIndicesFromArguments(int argStartIx, IList args, int maxIndex=31) + protected IEnumerable GetMatchingIndicesFromArguments(int argStartIx, IList args, ParsingContext ctx, int maxIndex = 31, bool convertNumericStrings = true) { //Return the addresses matching the criteria in the queue var argRanges = new List(); @@ -273,16 +296,58 @@ protected IEnumerable GetMatchingIndicesFromArguments(int argStartIx, IList criteria.Add(new RangeOrValue { Value = args[ix + 1].ResultValue }); } } - IEnumerable matchIndexes = GetMatchIndexes(argRanges[0], criteria[0], null); + IEnumerable matchIndexes = GetMatchIndexes(argRanges[0], criteria[0], ctx, convertNumericStrings); var enumerable = matchIndexes as IList ?? matchIndexes.ToList(); for (var ix = 1; ix < argRanges.Count && enumerable.Any(); ix++) { - var indexes = GetMatchIndexes(argRanges[ix], criteria[ix], null); + var indexes = GetMatchIndexes(argRanges[ix], criteria[ix], ctx, convertNumericStrings); matchIndexes = matchIndexes.Intersect(indexes); } return matchIndexes; } + protected void GetFilteredValueRange( + ParsingContext context, + IRangeInfo valueRange, + List argRanges, + List criterias, + int row, + int col, + out List matchIndexes, + out List flattenedRange, + bool convertNumericString = true) + { + matchIndexes = GetMatchIndexes(argRanges[0], GetCriteriaValue(criterias[0], row, col), context, convertNumericString); + + if (argRanges.Count > 1) + { + var hashSet = new HashSet(matchIndexes); + for (var ix = 1; ix < argRanges.Count && hashSet.Count > 0; ix++) + { + var indexes = GetMatchIndexes(argRanges[ix], GetCriteriaValue(criterias[ix], row, col), context, convertNumericString); + hashSet.IntersectWith(indexes); + } + matchIndexes = hashSet.ToList(); + } + + // Try to get flattened range from cache only if it doesn't have formulas + var valueAddress = valueRange.Address; + if (valueAddress != null && !valueAddress.HasFormulas(context.Package)) + { + flattenedRange = context.RangeCriteriaCache?.GetFlattenedRange(valueAddress); + if (flattenedRange == null) + { + // Not in cache, flatten and cache it + flattenedRange = RangeFlattener.FlattenRangeObject(valueRange); + context.RangeCriteriaCache?.SetFlattenedRange(valueAddress, flattenedRange); + } + } + else + { + // Has formulas or no address - don't cache, always flatten fresh + flattenedRange = RangeFlattener.FlattenRangeObject(valueRange); + } + } } -} +} \ No newline at end of file diff --git a/src/EPPlus/FormulaParsing/Excel/Functions/MathFunctions/SumIf.cs b/src/EPPlus/FormulaParsing/Excel/Functions/MathFunctions/SumIf.cs index e2a9b4db7..773960b96 100644 --- a/src/EPPlus/FormulaParsing/Excel/Functions/MathFunctions/SumIf.cs +++ b/src/EPPlus/FormulaParsing/Excel/Functions/MathFunctions/SumIf.cs @@ -41,14 +41,14 @@ public override void ConfigureArrayBehaviour(ArrayBehaviourConfig config) { config.SetArrayParameterIndexes(1); } - public override void GetNewParameterAddress(IList args, int index, ref Queue addresses) + public override void GetNewParameterAddress(IList args, int index, ParsingContext ctx, ref Queue addresses) { if(index == 2) { if (args[0].Result is IRangeInfo criteriaRange && args[2].Result is IRangeInfo valueRange) { var rv = new RangeOrValue { Range = criteriaRange }; - var mi=GetMatchIndexes(rv, args[1].Result, null); + var mi=GetMatchIndexes(rv, args[1].Result, ctx); addresses = EnqueueMatchingAddresses(valueRange, mi, ref addresses); } } diff --git a/src/EPPlus/FormulaParsing/Excel/Functions/MathFunctions/SumIfs.cs b/src/EPPlus/FormulaParsing/Excel/Functions/MathFunctions/SumIfs.cs index 57f208821..2e10bde7d 100644 --- a/src/EPPlus/FormulaParsing/Excel/Functions/MathFunctions/SumIfs.cs +++ b/src/EPPlus/FormulaParsing/Excel/Functions/MathFunctions/SumIfs.cs @@ -21,6 +21,7 @@ Date Author Change using OfficeOpenXml.FormulaParsing.LexicalAnalysis; using OfficeOpenXml.FormulaParsing.Ranges; using OfficeOpenXml.Utils; +using System.Diagnostics; namespace OfficeOpenXml.FormulaParsing.Excel.Functions.MathFunctions { @@ -49,34 +50,36 @@ public override void ConfigureArrayBehaviour(ArrayBehaviourConfig config) { return FunctionParameterInformation.IgnoreErrorInPreExecute; } - else if(argumentIndex==1) + else if (argumentIndex == 1) { return FunctionParameterInformation.Normal; } return FunctionParameterInformation.AdjustParameterAddress; })); - public override void GetNewParameterAddress(IList args, int index, ref Queue addresses) + + public override void GetNewParameterAddress(IList args, int index, ParsingContext ctx, ref Queue addresses) { if (index == 0 && args[0].Result is IRangeInfo valueRange) { - IEnumerable matchIndexes = GetMatchingIndicesFromArguments(1, args); + IEnumerable matchIndexes = GetMatchingIndicesFromArguments(1, args, ctx); addresses = EnqueueMatchingAddresses(valueRange, matchIndexes, ref addresses); } - else if(args[index].Result is IRangeInfo criteriaRange) + else if (args[index].Result is IRangeInfo criteriaRange) { - IEnumerable matchIndexes = GetMatchingIndicesFromArguments(1, args, index); + IEnumerable matchIndexes = GetMatchingIndicesFromArguments(1, args, ctx, index); addresses = EnqueueMatchingAddresses(criteriaRange, matchIndexes, ref addresses); } } public override CompileResult Execute(IList arguments, ParsingContext context) { - var valueRange = arguments[0].ValueAsRangeInfo; + var valueRange = arguments[0].ValueAsRangeInfo; GetArguments(context, arguments, out List argRanges, out List criteria, out int cols, out int rows, 1); - + if (cols == 1 && rows == 1) { var result = GetSumValue(context, valueRange, argRanges, criteria, 0, 0, out ExcelErrorValue ev); + if (double.IsNaN(result) && ev != null) { return CreateResult(ev, DataType.ExcelError); @@ -108,16 +111,12 @@ public override CompileResult Execute(IList arguments, Parsing } } + private double GetSumValue(ParsingContext context, IRangeInfo valueRange, List argRanges, List criterias, int row, int col, out ExcelErrorValue ev) { - IEnumerable matchIndexes = GetMatchIndexes(argRanges[0], GetCriteriaValue(criterias[0], row, col), context); - var enumerable = matchIndexes as IList ?? matchIndexes.ToList(); - for (var ix = 1; ix < argRanges.Count && enumerable.Any(); ix++) - { - var indexes = GetMatchIndexes(argRanges[ix], GetCriteriaValue(criterias[ix], row, col), context); - matchIndexes = matchIndexes.Intersect(indexes); - } - var sumRange = RangeFlattener.FlattenRangeObject(valueRange); + + GetFilteredValueRange(context, valueRange, argRanges, criterias, row, col, out List matchIndexes, out List sumRange); + KahanSum result = 0d; foreach (var index in matchIndexes) { diff --git a/src/EPPlus/FormulaParsing/Excel/Functions/RefAndLookup/HLookup.cs b/src/EPPlus/FormulaParsing/Excel/Functions/RefAndLookup/HLookup.cs index 1ae2f96ce..82ee6f301 100644 --- a/src/EPPlus/FormulaParsing/Excel/Functions/RefAndLookup/HLookup.cs +++ b/src/EPPlus/FormulaParsing/Excel/Functions/RefAndLookup/HLookup.cs @@ -83,7 +83,7 @@ public override CompileResult Execute(IList arguments, Parsing } return CompileResultFactory.Create(lookupRange.GetOffset(lookupIndex - 1, index)); } - public override void GetNewParameterAddress(IList args, int index, ref Queue addresses) + public override void GetNewParameterAddress(IList args, int index, ParsingContext ctx, ref Queue addresses) { if (args.Count > 2) { diff --git a/src/EPPlus/FormulaParsing/Excel/Functions/RefAndLookup/LookupUtils/XlookupScanner.cs b/src/EPPlus/FormulaParsing/Excel/Functions/RefAndLookup/LookupUtils/XlookupScanner.cs index 3e22613df..25f1a4060 100644 --- a/src/EPPlus/FormulaParsing/Excel/Functions/RefAndLookup/LookupUtils/XlookupScanner.cs +++ b/src/EPPlus/FormulaParsing/Excel/Functions/RefAndLookup/LookupUtils/XlookupScanner.cs @@ -150,16 +150,28 @@ private int FindIndexInternal() return -1; } + private int GetMaxItemsRow(IRangeInfo lookupRange) { + var adjusted = lookupRange.GetAddressDimensionAdjusted(0); + if (adjusted != null) + { + return adjusted.ToRow - adjusted.FromRow + 1; + } if (lookupRange.Address.ToRow > lookupRange.Dimension.ToRow) { return lookupRange.Dimension.ToRow - lookupRange.Address.FromRow + 1; - } + } return _lookupRange.Size.NumberOfRows; } + private int GetMaxItemsColumns(IRangeInfo lookupRange) { + var adjusted = lookupRange.GetAddressDimensionAdjusted(0); + if (adjusted != null) + { + return adjusted.ToCol - adjusted.FromCol + 1; + } if (lookupRange.Address.ToCol > lookupRange.Dimension.ToCol) { return lookupRange.Dimension.ToCol - lookupRange.Address.FromCol + 1; diff --git a/src/EPPlus/FormulaParsing/Excel/Functions/RefAndLookup/VLookup.cs b/src/EPPlus/FormulaParsing/Excel/Functions/RefAndLookup/VLookup.cs index 465c5d958..6f13a89f2 100644 --- a/src/EPPlus/FormulaParsing/Excel/Functions/RefAndLookup/VLookup.cs +++ b/src/EPPlus/FormulaParsing/Excel/Functions/RefAndLookup/VLookup.cs @@ -97,7 +97,7 @@ public override CompileResult Execute(IList arguments, Parsing } return CompileResultFactory.Create(lookupRange.GetOffset(index, lookupIndex - 1)); } - public override void GetNewParameterAddress(IList args, int index, ref Queue addresses) + public override void GetNewParameterAddress(IList args, int index, ParsingContext ctx, ref Queue addresses) { if (args.Count > 2) { diff --git a/src/EPPlus/FormulaParsing/Excel/Operators/RangeOperationsOperator.cs b/src/EPPlus/FormulaParsing/Excel/Operators/RangeOperationsOperator.cs index 4197fe5db..9fc72fe63 100644 --- a/src/EPPlus/FormulaParsing/Excel/Operators/RangeOperationsOperator.cs +++ b/src/EPPlus/FormulaParsing/Excel/Operators/RangeOperationsOperator.cs @@ -10,16 +10,11 @@ Date Author Change ************************************************************************************************* 05/30/2022 EPPlus Software AB EPPlus 6.1 *************************************************************************************************/ -using OfficeOpenXml.FormulaParsing.Excel.Functions.MathFunctions; -using OfficeOpenXml.FormulaParsing.Excel.Functions.Text; using OfficeOpenXml.FormulaParsing.FormulaExpressions; using OfficeOpenXml.FormulaParsing.LexicalAnalysis; using OfficeOpenXml.FormulaParsing.Ranges; -using OfficeOpenXml.Utils; using OfficeOpenXml.Utils.TypeConversion; using System; -using System.Globalization; -using System.Linq; namespace OfficeOpenXml.FormulaParsing.Excel.Operators { @@ -42,20 +37,32 @@ private static CompileResult ApplyOperator(CompileResult l, CompileResult r, Ope internal static InMemoryRange Negate(IRangeInfo ri) { InMemoryRange imr; - if(ri.IsInMemoryRange==false) + if (ri.IsInMemoryRange == false) { - imr = new InMemoryRange(ri.Size); + var physicalRows = RangeHelper.GetPhysicalRows(ri); + var logicalRows = ri.Size.NumberOfRows; + if (physicalRows < logicalRows) + { + imr = new InMemoryRange( + new RangeDefinition(physicalRows, ri.Size.NumberOfCols), + logicalRows); + } + else + { + imr = new InMemoryRange(ri.Size); + } } else { imr = (InMemoryRange)ri; } + int rows = imr.PhysicalRows; for (int c = 0; c < ri.Size.NumberOfCols; c++) { - for (int r = 0; r < ri.Size.NumberOfRows; r++) + for (int r = 0; r < rows; r++) { - var d = ConvertUtil.GetValueDouble(ri.GetOffset(r, c), true, true); + var d = ConvertUtil.GetValueDouble(ri.GetOffset(r, c), false, true); if (double.IsNaN(d)) { @@ -67,20 +74,62 @@ internal static InMemoryRange Negate(IRangeInfo ri) } } } + + if (imr.HasVirtualRows) + { + // Compute the negation of an empty cell: -(0) = 0 + // Use the upstream default if one exists, otherwise null (= 0) + var srcDefault = imr.VirtualDefaultValue; + if (srcDefault == null) + { + srcDefault = 0d; + } + var d = ConvertUtil.GetValueDouble(srcDefault, false, false); + if (double.IsNaN(d)) + { + imr.VirtualDefaultValue = ErrorValues.ValueError; + } + else + { + imr.VirtualDefaultValue = d == 0d ? 0d : -d; + } + } + return imr; } + private static InMemoryRange CreateRange(IRangeInfo l, IRangeInfo r, FormulaRangeAddress address) { var width = Math.Max(l.Size.NumberOfCols, r.Size.NumberOfCols); - var height = Math.Max(l.Size.NumberOfRows, r.Size.NumberOfRows); - var rangeDef = new RangeDefinition(height, width); - if(address != null) + + int logicalHeight = Math.Max(l.Size.NumberOfRows, r.Size.NumberOfRows); + int physicalHeight = Math.Max( + RangeHelper.GetPhysicalRows(l), + RangeHelper.GetPhysicalRows(r)); + + if (physicalHeight >= logicalHeight) + { + // No virtual rows needed - existing behavior + var rangeDef = new RangeDefinition(logicalHeight, width); + if (address != null) + { + return new InMemoryRange(address, rangeDef); + } + else + { + return new InMemoryRange(rangeDef); + } + } + + // Virtual range: small backing array, large logical size + var physicalDef = new RangeDefinition(physicalHeight, width); + if (address != null) { - return new InMemoryRange(address, rangeDef); + return new InMemoryRange(address, physicalDef, logicalHeight); } else { - return new InMemoryRange(rangeDef); + return new InMemoryRange(physicalDef, logicalHeight); } } @@ -105,7 +154,7 @@ private static void SetValue(Operators op, InMemoryRange resultRange, int row, i { var res = ApplyOperator(leftVal, rightVal, op, out bool error, context); var resultValue = res.ResultValue; - if(!(resultValue is bool) && ConvertUtil.IsNumeric(resultValue) && res.ResultNumeric == 0d) + if (!(resultValue is bool) && ConvertUtil.IsNumeric(resultValue) && res.ResultNumeric == 0d) { // avoid -0 results. resultValue = 0d; @@ -115,7 +164,7 @@ private static void SetValue(Operators op, InMemoryRange resultRange, int row, i private static bool ShouldUseSingleRow(RangeDefinition lSize, RangeDefinition rSize) { - if((lSize.NumberOfRows == 1 || rSize.NumberOfRows == 1) && lSize.NumberOfCols == rSize.NumberOfCols) + if ((lSize.NumberOfRows == 1 || rSize.NumberOfRows == 1) && lSize.NumberOfCols == rSize.NumberOfCols) { return true; } @@ -143,11 +192,11 @@ private static bool SingleRowSingleCol(RangeDefinition lSize, RangeDefinition rS private static bool AddressIsNotAvailable(RangeDefinition lSize, RangeDefinition rSize, int row, int col) { - if(row >= lSize.NumberOfRows || row >=rSize.NumberOfRows) + if (row >= lSize.NumberOfRows || row >= rSize.NumberOfRows) { return true; } - else if(col >= lSize.NumberOfCols || col >= rSize.NumberOfCols) + else if (col >= lSize.NumberOfCols || col >= rSize.NumberOfCols) { return true; } @@ -193,22 +242,48 @@ private static object GetCellValue(IRangeInfo range, int rowOffset, int colOffse catch { throw; - } + } + } + + private static void SetVirtualDefault( + InMemoryRange resultRange, + CompileResult nullLeft, + CompileResult nullRight, + Operators op, + ParsingContext context) + { + if (!resultRange.HasVirtualRows) return; + + var res = ApplyOperator(nullLeft, nullRight, op, out bool error, context); + if (error) + { + resultRange.VirtualDefaultValue = ExcelErrorValue.Create(eErrorType.Value); + } + else + { + var resultValue = res.ResultValue; + if (!(resultValue is bool) && ConvertUtil.IsNumeric(resultValue) && res.ResultNumeric == 0d) + { + resultValue = 0d; + } + resultRange.VirtualDefaultValue = resultValue; + } } - public static InMemoryRange ApplySingleValueRight(CompileResult left, CompileResult right, Operators op, ParsingContext context) + public static InMemoryRange ApplySingleValueRight( + CompileResult left, CompileResult right, Operators op, ParsingContext context) { var lr = left.Result as IRangeInfo; - if(lr == null && left.Result is FormulaRangeAddress fra) + if (lr == null && left.Result is FormulaRangeAddress fra) { lr = context.ExcelDataProvider.GetRange(fra); } - else if(left.Address != null && left.Result is not InMemoryRange) + else if (left.Address != null && left.Result is not InMemoryRange) { lr = context.ExcelDataProvider.GetRange(left.Address); } var resultRange = CreateRange(lr, InMemoryRange.Empty, lr.Address); - for (var row = 0; row < resultRange.Size.NumberOfRows; row++) + for (var row = 0; row < resultRange.PhysicalRows; row++) { for (var col = 0; col < resultRange.Size.NumberOfCols; col++) { @@ -217,10 +292,16 @@ public static InMemoryRange ApplySingleValueRight(CompileResult left, CompileRes SetValue(op, resultRange, row, col, lcr, right, context); } } + + // Compute default for virtual rows: null op scalar + SetVirtualDefault(resultRange, + CompileResultFactory.Create(null), right, op, context); + return resultRange; } - public static InMemoryRange ApplySingleValueLeft(CompileResult left, CompileResult right, Operators op, ParsingContext context) + public static InMemoryRange ApplySingleValueLeft( + CompileResult left, CompileResult right, Operators op, ParsingContext context) { var rr = right.Result as IRangeInfo; if (rr == null && right.Result is FormulaRangeAddress fra) @@ -232,7 +313,7 @@ public static InMemoryRange ApplySingleValueLeft(CompileResult left, CompileResu rr = context.ExcelDataProvider.GetRange(right.Address); } var resultRange = CreateRange(InMemoryRange.Empty, rr, rr.Address); - for (var row = 0; row < resultRange.Size.NumberOfRows; row++) + for (var row = 0; row < resultRange.PhysicalRows; row++) { for (var col = 0; col < resultRange.Size.NumberOfCols; col++) { @@ -242,18 +323,83 @@ public static InMemoryRange ApplySingleValueLeft(CompileResult left, CompileResu SetValue(op, resultRange, row, col, left, rcr, context); } } + + // Compute default for virtual rows: scalar op null + SetVirtualDefault(resultRange, + left, CompileResultFactory.Create(null), op, context); + return resultRange; } + private static void SetVirtualDefaultForRanges( + InMemoryRange resultRange, + IRangeInfo lr, + IRangeInfo rr, + bool shouldUseSingleRow, + bool shouldUseSingleCell, + bool singleRowSingleCol, + Operators op, + ParsingContext context) + { + if (!resultRange.HasVirtualRows) return; + + CompileResult virtualLeft, virtualRight; + if (shouldUseSingleRow) + { + if (lr.Size.NumberOfRows == 1) + { + virtualLeft = CompileResultFactory.Create(GetCellValue(lr, 0, 0)); + virtualRight = CompileResultFactory.Create(null); + } + else + { + virtualLeft = CompileResultFactory.Create(null); + virtualRight = CompileResultFactory.Create(GetCellValue(rr, 0, 0)); + } + } + else if (shouldUseSingleCell) + { + if (lr.Size.NumberOfCols == 1 && lr.Size.NumberOfRows == 1) + { + virtualLeft = CompileResultFactory.Create(GetCellValue(lr, 0, 0)); + virtualRight = CompileResultFactory.Create(null); + } + else + { + virtualLeft = CompileResultFactory.Create(null); + virtualRight = CompileResultFactory.Create(GetCellValue(rr, 0, 0)); + } + } + else if (singleRowSingleCol) + { + if (lr.Size.NumberOfRows == 1) + { + virtualLeft = CompileResultFactory.Create(GetCellValue(lr, 0, 0)); + virtualRight = CompileResultFactory.Create(null); + } + else + { + virtualLeft = CompileResultFactory.Create(null); + virtualRight = CompileResultFactory.Create(GetCellValue(rr, 0, 0)); + } + } + else + { + virtualLeft = CompileResultFactory.Create(null); + virtualRight = CompileResultFactory.Create(null); + } + SetVirtualDefault(resultRange, virtualLeft, virtualRight, op, context); + } + private static InMemoryRange ApplyRanges(CompileResult left, CompileResult right, Operators op, ParsingContext context, FormulaRangeAddress intersectAddress) { var lr = left.Result as IRangeInfo; var rr = right.Result as IRangeInfo; - if(lr == null && left.Result is FormulaRangeAddress fral) + if (lr == null && left.Result is FormulaRangeAddress fral) { lr = new RangeInfo(fral); } - if(rr == null && right.Result is FormulaRangeAddress frar) + if (rr == null && right.Result is FormulaRangeAddress frar) { rr = new RangeInfo(frar); } @@ -263,7 +409,7 @@ private static InMemoryRange ApplyRanges(CompileResult left, CompileResult right var shouldUseSingleRow = ShouldUseSingleRow(lr.Size, rr.Size); var shouldUseSingleCell = ShouldUseSingleCell(lr.Size, rr.Size); var singleRowSingleCol = SingleRowSingleCol(lr.Size, rr.Size); - for (var row = 0; row < resultRange.Size.NumberOfRows; row++) + for (var row = 0; row < resultRange.PhysicalRows; row++) { for (var col = 0; col < resultRange.Size.NumberOfCols; col++) { @@ -340,7 +486,11 @@ private static InMemoryRange ApplyRanges(CompileResult left, CompileResult right } } + SetVirtualDefaultForRanges(resultRange, lr, rr, + shouldUseSingleRow, shouldUseSingleCell, singleRowSingleCol, + op, context); + return resultRange; } } -} +} \ No newline at end of file diff --git a/src/EPPlus/FormulaParsing/ExcelCalculationOption.cs b/src/EPPlus/FormulaParsing/ExcelCalculationOption.cs index 25e14bebd..646bfb6a4 100644 --- a/src/EPPlus/FormulaParsing/ExcelCalculationOption.cs +++ b/src/EPPlus/FormulaParsing/ExcelCalculationOption.cs @@ -15,6 +15,12 @@ Date Author Change using System; using System.Collections.Generic; using System.IO; +using System.Threading; +using System.Threading.Tasks; +#elif (!NET35) +using System.Collections.Generic; +using System.Threading; +using System.Threading.Tasks; #else using System.Configuration; using System.Collections.Generic; @@ -115,5 +121,22 @@ public bool EnableUnicodeAwareStringOperations { get; set; } = false; + +#if !NET35 + /// + /// A cancellation token that can be used to cancel a running calculation. + /// When cancelled, an will be thrown + /// and the workbook will be left in an inconsistent, partially calculated state. + /// The workbook must be discarded after cancellation — saving or recalculating + /// a cancelled workbook is not permitted. + /// + /// + /// + /// using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(30)); + /// workbook.Calculate(opt => opt.CancellationToken = cts.Token); + /// + /// + public CancellationToken CancellationToken { get; set; } = CancellationToken.None; +#endif } } diff --git a/src/EPPlus/FormulaParsing/ExcelUtilities/RangeFlattener.cs b/src/EPPlus/FormulaParsing/ExcelUtilities/RangeFlattener.cs index 4ec22eec5..cca8f970c 100644 --- a/src/EPPlus/FormulaParsing/ExcelUtilities/RangeFlattener.cs +++ b/src/EPPlus/FormulaParsing/ExcelUtilities/RangeFlattener.cs @@ -10,13 +10,10 @@ Date Author Change ************************************************************************************************* 21/06/2023 EPPlus Software AB Initial release EPPlus 7 *************************************************************************************************/ +using OfficeOpenXml.FormulaParsing.Ranges; using OfficeOpenXml.Utils.TypeConversion; -using OfficeOpenXml.FormulaParsing.Excel.Functions.RefAndLookup; -using OfficeOpenXml.Utils; using System; using System.Collections.Generic; -using System.Linq; -using System.Text; namespace OfficeOpenXml.FormulaParsing.ExcelUtilities { @@ -32,8 +29,9 @@ internal class RangeFlattener public static List FlattenRange(IRangeInfo r1, bool addNullifEmpty = true) { var result = new List(); + int rows = RangeHelper.GetPhysicalRows(r1); - for (var row = 0; row < r1.Size.NumberOfRows; row++) + for (var row = 0; row < rows; row++) { for (var column = 0; column < r1.Size.NumberOfCols; column++) { @@ -67,7 +65,8 @@ public static List FlattenRangeObject(IRangeInfo r1) return result; } /// - /// Produces two lists based on the supplied ranges. The lists will contain all data from positions where both ranges has numeric values. + /// Produces two lists based on the supplied ranges. + /// The lists will contain all data from positions where both ranges has numeric values. /// /// range 1 /// range 2 @@ -122,4 +121,4 @@ public static void GetNumericPairLists(IRangeInfo r1, IRangeInfo r2, bool dataPo } } } -} +} \ No newline at end of file diff --git a/src/EPPlus/FormulaParsing/FormulaExpressions/FunctionCompilers/CustomArrayBehaviourCompiler.cs b/src/EPPlus/FormulaParsing/FormulaExpressions/FunctionCompilers/CustomArrayBehaviourCompiler.cs index 933300b72..42934d567 100644 --- a/src/EPPlus/FormulaParsing/FormulaExpressions/FunctionCompilers/CustomArrayBehaviourCompiler.cs +++ b/src/EPPlus/FormulaParsing/FormulaExpressions/FunctionCompilers/CustomArrayBehaviourCompiler.cs @@ -40,18 +40,18 @@ internal CustomArrayBehaviourCompiler(ExcelFunction function, ParsingContext con public override CompileResult Compile(IEnumerable children, ParsingContext context) { var args = new List(); - + if (!children.Any()) return new CompileResult(eErrorType.Value); var rangeArgs = new Dictionary(); var otherArgs = new Dictionary(); - for(var ix = 0; ix < children.Count(); ix++) + for (var ix = 0; ix < children.Count(); ix++) { var cr = children.ElementAt(ix); - if(cr.DataType == DataType.ExcelRange && Function.ArrayBehaviourConfig.CanBeArrayArg(ix) && (cr.Result is IRangeInfo || cr.Result is FormulaRangeAddress)) + if (cr.DataType == DataType.ExcelRange && Function.ArrayBehaviourConfig.CanBeArrayArg(ix) && (cr.Result is IRangeInfo || cr.Result is FormulaRangeAddress)) { var range = cr.Result as IRangeInfo; - if(range == null && cr.Result is FormulaRangeAddress fra) + if (range == null && cr.Result is FormulaRangeAddress fra) { range = new RangeInfo(fra); } @@ -71,7 +71,7 @@ public override CompileResult Compile(IEnumerable children, Parsi } else { - if(cr.DataType == DataType.ExcelRange) + if (cr.DataType == DataType.ExcelRange) { cr = CompileResultFactory.Create(cr.Result); } @@ -79,55 +79,78 @@ public override CompileResult Compile(IEnumerable children, Parsi } } - if(rangeArgs.Count == 0) + if (rangeArgs.Count == 0) { var defaultCompiler = new DefaultCompiler(Function); return defaultCompiler.Compile(children, context); } short maxWidth = 0; - var maxHeight = 0; - foreach(var rangeArg in rangeArgs.Values) + var maxPhysicalHeight = 0; + var maxLogicalHeight = 0; + foreach (var rangeArg in rangeArgs.Values) { - if(rangeArg.Size.NumberOfCols > maxWidth) + if (rangeArg.Size.NumberOfCols > maxWidth) { maxWidth = rangeArg.Size.NumberOfCols; } - if(rangeArg.Size.NumberOfRows > maxHeight) + + int physical = RangeHelper.GetPhysicalRows(rangeArg); + if (physical > maxPhysicalHeight) + { + maxPhysicalHeight = physical; + } + if (rangeArg.Size.NumberOfRows > maxLogicalHeight) { - maxHeight= rangeArg.Size.NumberOfRows; + maxLogicalHeight = rangeArg.Size.NumberOfRows; } } - var resultRangeDef = new RangeDefinition(maxHeight, maxWidth); InMemoryRange resultRange; - if(rangeArgs.Count==1) + if (maxPhysicalHeight < maxLogicalHeight) { - resultRange = new InMemoryRange(rangeArgs.First().Value.Address, resultRangeDef); + var physicalDef = new RangeDefinition(maxPhysicalHeight, maxWidth); + if (rangeArgs.Count == 1) + { + resultRange = new InMemoryRange(rangeArgs.First().Value.Address, physicalDef, maxLogicalHeight); + } + else + { + resultRange = new InMemoryRange(physicalDef, maxLogicalHeight); + } } else { - resultRange = new InMemoryRange(resultRangeDef); + var rangeDef = new RangeDefinition(maxLogicalHeight, maxWidth); + if (rangeArgs.Count == 1) + { + resultRange = new InMemoryRange(rangeArgs.First().Value.Address, rangeDef); + } + else + { + resultRange = new InMemoryRange(rangeDef); + } } + var nArgs = children.Count(); - for(var row = 0; row < resultRange.Size.NumberOfRows; row++) + for (var row = 0; row < resultRange.PhysicalRows; row++) { - for(var col = 0; col < resultRange.Size.NumberOfCols; col++) + for (var col = 0; col < resultRange.Size.NumberOfCols; col++) { bool isError = false; var argList = new List(); - for(var argIx = 0; argIx < nArgs; argIx++) + for (var argIx = 0; argIx < nArgs; argIx++) { - if(rangeArgs.ContainsKey(argIx)) + if (rangeArgs.ContainsKey(argIx)) { var range = rangeArgs[argIx]; var r = row; var c = col; - if(range.Size.NumberOfCols == 1 && range.Size.NumberOfRows == resultRange.Size.NumberOfRows) + if (range.Size.NumberOfCols == 1 && range.Size.NumberOfRows == resultRange.Size.NumberOfRows) { c = 0; } - if(range.Size.NumberOfRows == 1 && range.Size.NumberOfCols == resultRange.Size.NumberOfCols) + if (range.Size.NumberOfRows == 1 && range.Size.NumberOfCols == resultRange.Size.NumberOfCols) { r = 0; } @@ -149,12 +172,12 @@ public override CompileResult Compile(IEnumerable children, Parsi continue; } } - + } else { var arg = otherArgs[argIx]; - if(arg.DataType == DataType.LambdaCalculation) + if (arg.DataType == DataType.LambdaCalculation) { var calculator = arg.ResultValue as LambdaCalculator; calculator.BeginCalculation(); @@ -180,4 +203,4 @@ public override CompileResult Compile(IEnumerable children, Parsi } } } -} +} \ No newline at end of file diff --git a/src/EPPlus/FormulaParsing/FormulaExpressions/FunctionExpression.cs b/src/EPPlus/FormulaParsing/FormulaExpressions/FunctionExpression.cs index 696dc409d..03fb45959 100644 --- a/src/EPPlus/FormulaParsing/FormulaExpressions/FunctionExpression.cs +++ b/src/EPPlus/FormulaParsing/FormulaExpressions/FunctionExpression.cs @@ -116,7 +116,7 @@ public override Expression Negate() } protected IList _args=null; internal Queue _dependencyAddresses = null; - internal bool SetArguments(IList argsResults) + internal bool SetArguments(IList argsResults, ParsingContext ctx) { _args = argsResults; if (_function.ParametersInfo.HasNormalArguments == false) @@ -126,7 +126,7 @@ internal bool SetArguments(IList argsResults) var pi = _function.ParametersInfo.GetParameterInfo(i); if (EnumUtil.HasFlag(pi, FunctionParameterInformation.AdjustParameterAddress) && argsResults[i].Address != null) { - _function.GetNewParameterAddress(argsResults, i, ref _dependencyAddresses); + _function.GetNewParameterAddress(argsResults, i, ctx, ref _dependencyAddresses); } } return _dependencyAddresses != null; diff --git a/src/EPPlus/FormulaParsing/FormulaExpressions/LambdaCalculator.cs b/src/EPPlus/FormulaParsing/FormulaExpressions/LambdaCalculator.cs index 823125d7f..a9eee9267 100644 --- a/src/EPPlus/FormulaParsing/FormulaExpressions/LambdaCalculator.cs +++ b/src/EPPlus/FormulaParsing/FormulaExpressions/LambdaCalculator.cs @@ -24,7 +24,7 @@ namespace OfficeOpenXml.FormulaParsing.FormulaExpressions /// /// Stores the Lambda tokens and can be called multiple times with different parameters. /// For each time the tokens are converted RPN tokens and runs through the calculation - /// via the new method. + /// via the new method. /// internal class LambdaCalculator { diff --git a/src/EPPlus/FormulaParsing/LexicalAnalysis/FormulaAddress.cs b/src/EPPlus/FormulaParsing/LexicalAnalysis/FormulaAddress.cs index 2ec9fedb2..ef5e76963 100644 --- a/src/EPPlus/FormulaParsing/LexicalAnalysis/FormulaAddress.cs +++ b/src/EPPlus/FormulaParsing/LexicalAnalysis/FormulaAddress.cs @@ -697,6 +697,69 @@ public bool IsSingleCell return FromRow == ToRow && FromCol == ToCol; } } + + + private bool? _hasFormulas; // Nullable for lazy evaluation + + /// + /// Returns true of the address contains formulas. + /// This is used to determine if we can use cached flattened ranges or if we need to evaluate + /// the formulas in the range to get the correct values. The result is cached for performance. + /// + /// The that contains the address + /// + + public bool HasFormulas(ExcelPackage package) + { + if (_hasFormulas.HasValue) return _hasFormulas.Value; + if (package == null || WorksheetIx < 0) + { + _hasFormulas = false; + return false; + } + var ws = package.Workbook.GetWorksheetByIndexInList(WorksheetIx); + if (ws == null) + { + _hasFormulas = false; + return false; + } + + // Clamp to worksheet dimension to avoid iterating 1M rows + // for full column references + var dim = ws.Dimension; + var fromRow = FromRow; + var toRow = ToRow; + var fromCol = FromCol; + var toCol = ToCol; + if (dim != null) + { + fromRow = Math.Max(fromRow, dim._fromRow); + toRow = Math.Min(toRow, dim._toRow); + fromCol = Math.Max(fromCol, dim._fromCol); + toCol = Math.Min(toCol, dim._toCol); + } + else + { + // No dimension means no data, hence no formulas + _hasFormulas = false; + return false; + } + + for (var row = fromRow; row <= toRow; row++) + { + for (var col = fromCol; col <= toCol; col++) + { + if (ws._formulas.GetValue(row, col) != null) + { + _hasFormulas = true; + return true; + } + } + } + _hasFormulas = false; + return false; + } + /// /// Empty /// diff --git a/src/EPPlus/FormulaParsing/ParsingContext.cs b/src/EPPlus/FormulaParsing/ParsingContext.cs index 56cec80ce..18abf536e 100644 --- a/src/EPPlus/FormulaParsing/ParsingContext.cs +++ b/src/EPPlus/FormulaParsing/ParsingContext.cs @@ -20,6 +20,8 @@ Date Author Change using NvProvider = OfficeOpenXml.FormulaParsing.NameValueProvider; using OfficeOpenXml.Utils.RemoteCalls; using OfficeOpenXml.FormulaParsing.FormulaExpressions.VariableStorage; +using OfficeOpenXml.FormulaParsing.Excel.Functions.MathFunctions; + #if (!NET35) @@ -33,9 +35,11 @@ namespace OfficeOpenXml.FormulaParsing /// public class ParsingContext { - private ParsingContext(ExcelPackage package) { + private ParsingContext(ExcelPackage package) + { SubtotalAddresses = new HashSet(); Package = package; + ExpressionEvaluator = new ExpressionEvaluator(this); } internal FunctionCompilerFactory FunctionCompilerFactory @@ -103,6 +107,13 @@ internal VariableStorageManager VariableStorage } } + /// + /// Cache for RangeCriteria functions to improve performance during calculation + /// + internal RangeCriteriaCache RangeCriteriaCache { get; set; } + + internal ExpressionEvaluator ExpressionEvaluator { get; private set; } + /// /// Factory method. /// @@ -112,10 +123,9 @@ public static ParsingContext Create(ExcelPackage package) { var context = new ParsingContext(package); context.Configuration = ParsingConfiguration.Create(); - //context.Scopes = new ParsingScopes(context); - //context.AddressCache = new ExcelAddressCache(); context.NameValueProvider = NvProvider.Empty; context.FunctionCompilerFactory = new FunctionCompilerFactory(context.Configuration.FunctionRepository); + context.RangeCriteriaCache = new RangeCriteriaCache(package); return context; } diff --git a/src/EPPlus/FormulaParsing/Ranges/InMemoryRange.cs b/src/EPPlus/FormulaParsing/Ranges/InMemoryRange.cs index 1ad86b06e..ce1166a4f 100644 --- a/src/EPPlus/FormulaParsing/Ranges/InMemoryRange.cs +++ b/src/EPPlus/FormulaParsing/Ranges/InMemoryRange.cs @@ -25,6 +25,7 @@ namespace OfficeOpenXml.FormulaParsing.Ranges [DebuggerDisplay("{Size}")] public class InMemoryRange : IRangeInfo { + /// /// The constructor /// @@ -32,24 +33,67 @@ public class InMemoryRange : IRangeInfo public InMemoryRange(RangeDefinition rangeDef) { _cells = new ICellInfo[rangeDef.NumberOfRows, rangeDef.NumberOfCols]; + _physicalRows = rangeDef.NumberOfRows; Size = rangeDef; _address = new FormulaRangeAddress() { FromRow = 0, FromCol = 0, ToRow = rangeDef.NumberOfRows - 1, ToCol = rangeDef.NumberOfCols - 1 }; } /// /// The constructor /// - /// The worksheet address that should be used for this range. Will be used for implicit intersection. + /// The worksheet address that should be used for this range. + /// Will be used for implicit intersection. /// Defines the size of the range public InMemoryRange(FormulaRangeAddress address, RangeDefinition rangeDef) { - if (address?._context != null) + if (address != null && address._context != null) { _ws = address._context.Package.Workbook.GetWorksheetByIndexInList(address._context.CurrentCell.WorksheetIx); } _address = address; _cells = new ICellInfo[rangeDef.NumberOfRows, rangeDef.NumberOfCols]; + _physicalRows = rangeDef.NumberOfRows; Size = rangeDef; } + + /// + /// Constructor for a virtual range with a small physical backing array but large logical size. + /// Rows beyond .NumberOfRows are virtual and return null. + /// + /// The worksheet address for implicit intersection. + /// Defines the physical (backed) size of the range. + /// The full logical row count (e.g. 1048576 for a full column). + internal InMemoryRange(FormulaRangeAddress address, RangeDefinition physicalDef, int logicalRows) + { + if (address != null && address._context != null) + { + _ws = address._context.Package.Workbook.GetWorksheetByIndexInList(address._context.CurrentCell.WorksheetIx); + } + _address = address; + _physicalRows = physicalDef.NumberOfRows; + _cells = new ICellInfo[physicalDef.NumberOfRows, physicalDef.NumberOfCols]; + Size = new RangeDefinition(logicalRows, physicalDef.NumberOfCols); + } + + /// + /// Constructor for a virtual range without an address. + /// Rows beyond .NumberOfRows are virtual and return null. + /// + /// Defines the physical (backed) size of the range. + /// The full logical row count. + internal InMemoryRange(RangeDefinition physicalDef, int logicalRows) + { + _physicalRows = physicalDef.NumberOfRows; + _cells = new ICellInfo[physicalDef.NumberOfRows, physicalDef.NumberOfCols]; + Size = new RangeDefinition(logicalRows, physicalDef.NumberOfCols); + _address = new FormulaRangeAddress() + { + FromRow = 0, + FromCol = 0, + ToRow = logicalRows - 1, + ToCol = physicalDef.NumberOfCols - 1 + }; + } + /// /// Constructor /// @@ -57,6 +101,7 @@ public InMemoryRange(FormulaRangeAddress address, RangeDefinition rangeDef) public InMemoryRange(List> range) { Size = new RangeDefinition(range.Count, (short)range[0].Count); + _physicalRows = Size.NumberOfRows; _cells = new ICellInfo[Size.NumberOfRows, Size.NumberOfCols]; for (int c = 0; c < Size.NumberOfCols; c++) { @@ -71,10 +116,12 @@ public InMemoryRange(List> range) /// /// Constructor /// - /// Another used as clone for this range. The address of the supplied range will not be copied. + /// Another used as clone for this range. + /// The address of the supplied range will not be copied. public InMemoryRange(IRangeInfo ri) { Size = ri.Size; + _physicalRows = Size.NumberOfRows; _cells = new ICellInfo[Size.NumberOfRows, Size.NumberOfCols]; for (int c = 0; c < Size.NumberOfCols; c++) { @@ -101,6 +148,8 @@ public InMemoryRange(int rows, short cols) private readonly ICellInfo[,] _cells; private int _colIx = -1; private int _rowIndex = 0; + private readonly int _physicalRows; + private object _virtualDefaultValue; // null means "no default" (backward compat) private static InMemoryRange _empty = new InMemoryRange(new RangeDefinition(0, 0)); @@ -109,6 +158,35 @@ public InMemoryRange(int rows, short cols) /// public static InMemoryRange Empty => _empty; + /// + /// Number of rows backed by the physical cell array. + /// For non-virtual ranges this equals Size.NumberOfRows. + /// + internal int PhysicalRows + { + get { return _physicalRows; } + } + + + /// + /// The value returned for rows beyond PhysicalRows. + /// When null (default), virtual rows return null. + /// When set, virtual rows return this value instead. + /// + internal object VirtualDefaultValue + { + get { return _virtualDefaultValue; } + set { _virtualDefaultValue = value; } + } + + /// + /// True if the range has virtual (unstored) rows beyond PhysicalRows. + /// + internal bool HasVirtualRows + { + get { return Size.NumberOfRows > _physicalRows; } + } + /// /// Sets the value for a cell. /// @@ -117,6 +195,7 @@ public InMemoryRange(int rows, short cols) /// The value to set public void SetValue(int row, int col, object val) { + if (row >= _physicalRows) return; var c = new InMemoryCellInfo(val); _cells[row, col] = c; } @@ -129,6 +208,7 @@ public void SetValue(int row, int col, object val) /// The cell public void SetCell(int row, int col, ICellInfo cell) { + if (row >= _physicalRows) return; _cells[row, col] = cell; } /// @@ -165,7 +245,7 @@ public void SetCell(int row, int col, ICellInfo cell) public FormulaRangeAddress Dimension { get - { + { return _address; } } @@ -190,7 +270,7 @@ object IEnumerator.Current /// /// The addresses for the range, if more than one. /// - public FormulaRangeAddress[] Addresses => [_address]; + public FormulaRangeAddress[] Addresses => new FormulaRangeAddress[] { _address }; /// /// Dispose @@ -226,12 +306,10 @@ public int GetNCells() /// The value of the cell public object GetOffset(int rowOffset, int colOffset) { + if (rowOffset >= _physicalRows) + return _virtualDefaultValue; // was: return null; var c = _cells[rowOffset, colOffset]; - if (c == null) - { - return null; - } - return c.Value; + return c == null ? null : c.Value; } /// @@ -242,23 +320,43 @@ public object GetOffset(int rowOffset, int colOffset) /// The ending row offset from the top-left cell. /// The ending column offset from the top-left cell /// The value of the cell - public IRangeInfo GetOffset(int rowOffsetStart, int colOffsetStart, int rowOffsetEnd, int colOffsetEnd) + public IRangeInfo GetOffset(int rowOffsetStart, int colOffsetStart, + int rowOffsetEnd, int colOffsetEnd) { - var nRows = Math.Abs(rowOffsetEnd - rowOffsetStart); - var nCols = (short)Math.Abs(colOffsetEnd- colOffsetStart); - nRows++; - nCols++; - var rangeDef = new RangeDefinition(nRows, nCols); - var result = new InMemoryRange(rangeDef); - var rowIx = 0; - for(var row = rowOffsetStart; row <= rowOffsetEnd; row++) + var logicalRows = rowOffsetEnd - rowOffsetStart + 1; + var nCols = (short)(colOffsetEnd - colOffsetStart + 1); + + if (rowOffsetStart >= _physicalRows) + { + var emptyRange = new InMemoryRange(new RangeDefinition(0, nCols), logicalRows); + emptyRange._virtualDefaultValue = _virtualDefaultValue; // propagate + return emptyRange; + } + + var physicalEnd = Math.Min(rowOffsetEnd, _physicalRows - 1); + var physicalRows = physicalEnd - rowOffsetStart + 1; + + InMemoryRange result; + if (physicalRows < logicalRows) + { + result = new InMemoryRange(new RangeDefinition(physicalRows, nCols), logicalRows); + result._virtualDefaultValue = _virtualDefaultValue; // propagate + } + else + { + result = new InMemoryRange(new RangeDefinition(physicalRows, nCols)); + } + + for (var row = rowOffsetStart; row <= physicalEnd; row++) { var colIx = 0; for (var col = colOffsetStart; col <= colOffsetEnd; col++) { - result.SetValue(rowIx, colIx++, _cells[row, col].Value); + var cell = _cells[row, col]; + var val = cell != null ? cell.Value : null; + result.SetValue(row - rowOffsetStart, colIx, val); + colIx++; } - rowIx++; } return result; } @@ -280,20 +378,12 @@ public bool IsHidden(int rowOffset, int colOffset) /// public object GetValue(int row, int col) { - if (_address == null) - { - var c = _cells[row, col]; - if (c == null) return null; - return c.Value; - - - } - else - { - var c = _cells[row-_address.FromRow, col-Address.FromCol]; - if (c == null) return null; - return c.Value; - } + int r = _address == null ? row : row - _address.FromRow; + if (r >= _physicalRows) return _virtualDefaultValue; + int c = _address == null ? col : col - _address.FromCol; + var cell = _cells[r, c]; + if (cell == null) return null; + return cell.Value; } /// /// Get cell @@ -303,6 +393,13 @@ public object GetValue(int row, int col) /// public ICellInfo GetCell(int row, int col) { + if (row >= _physicalRows) + { + // Return a virtual cell with the default value if set + if (_virtualDefaultValue != null) + return new InMemoryCellInfo(_virtualDefaultValue); + return null; + } var c = _cells[row, col]; if (c == null) return null; return c; @@ -320,7 +417,7 @@ public bool MoveNext() } _colIx = 0; _rowIndex++; - if (_rowIndex >= Size.NumberOfRows) return false; + if (_rowIndex >= _physicalRows) return false; return true; } /// @@ -341,15 +438,26 @@ IEnumerator IEnumerable.GetEnumerator() internal static InMemoryRange CloneRange(IRangeInfo ri) { - var ret = new InMemoryRange(ri.Size); - for(int r=0;r < ri.Size.NumberOfRows;r++) + var isVirtual = ri is InMemoryRange && ((InMemoryRange)ri).HasVirtualRows; + int physRows = isVirtual ? ((InMemoryRange)ri).PhysicalRows : ri.Size.NumberOfRows; + + var ret = isVirtual + ? new InMemoryRange(new RangeDefinition(physRows, ri.Size.NumberOfCols), + ri.Size.NumberOfRows) + : new InMemoryRange(ri.Size); + + if (isVirtual) { - for (int c = 0; c < ri.Size.NumberOfCols;c++) + ret._virtualDefaultValue = ((InMemoryRange)ri)._virtualDefaultValue; + } + + for (int r = 0; r < physRows; r++) + { + for (int c = 0; c < ri.Size.NumberOfCols; c++) { - ret.SetValue(r,c,ri.GetOffset(r,c)); + ret.SetValue(r, c, ri.GetOffset(r, c)); } } - return ret; } @@ -357,7 +465,7 @@ internal static InMemoryRange GetFromArray(params object[] values) { var rows = values.GetUpperBound(0) + 1; var ir = new InMemoryRange(rows, 1); - for(int r=0;r < rows;r++) + for (int r = 0; r < rows; r++) { ir.SetValue(r, 0, values[r]); } @@ -365,12 +473,26 @@ internal static InMemoryRange GetFromArray(params object[] values) } /// - /// Get the address adjusted inside the dimension of the worksheet. Not applicable on InMemoryRange's, as no addresses us used. + /// Get the address adjusted inside the dimension of the worksheet. + /// Not applicable on InMemoryRange's, as no addresses us used. /// /// Not applicable on InMemoryRange's. /// The address. public FormulaRangeAddress GetAddressDimensionAdjusted(int index) { + if (index > 0) return null; + + if (HasVirtualRows) + { + // Return address clamped to physical rows + return new FormulaRangeAddress() + { + FromRow = _address.FromRow, + FromCol = _address.FromCol, + ToRow = _address.FromRow + _physicalRows - 1, + ToCol = _address.ToCol + }; + } return _address; } @@ -378,7 +500,7 @@ internal IEnumerable SerializeToTokens() { var sb = new StringBuilder(); sb.Append("{"); - for (var row = 0; row < Size.NumberOfRows; row++) + for (var row = 0; row < _physicalRows; row++) { for (var col = 0; col < Size.NumberOfCols; col++) { @@ -389,7 +511,7 @@ internal IEnumerable SerializeToTokens() sb.Append(","); } } - if(row < Size.NumberOfRows - 1) + if (row < _physicalRows - 1) { sb.Append(";"); } @@ -406,4 +528,4 @@ public IRangeInfo GetRangeInfoByValue() return this; } } -} +} \ No newline at end of file diff --git a/src/EPPlus/FormulaParsing/Ranges/RangeHelper.cs b/src/EPPlus/FormulaParsing/Ranges/RangeHelper.cs new file mode 100644 index 000000000..d81bbf6b1 --- /dev/null +++ b/src/EPPlus/FormulaParsing/Ranges/RangeHelper.cs @@ -0,0 +1,50 @@ +/************************************************************************************************* + Required Notice: Copyright (C) EPPlus Software AB. + This software is licensed under PolyForm Noncommercial License 1.0.0 + and may only be used for noncommercial purposes + https://polyformproject.org/licenses/noncommercial/1.0.0/ + + A commercial license to use this software can be purchased at https://epplussoftware.com + ************************************************************************************************* + Date Author Change + ************************************************************************************************* + 03/03/2026 EPPlus Software AB Virtual InMemoryRange support + *************************************************************************************************/ + +namespace OfficeOpenXml.FormulaParsing.Ranges +{ + /// + /// Helper methods for working with range physical/logical size. + /// + internal static class RangeHelper + { + /// + /// Returns the effective number of data rows for a range. + /// For virtual InMemoryRanges: the physical row count. + /// For worksheet-backed ranges: constrained by worksheet dimension. + /// For other ranges: Size.NumberOfRows. + /// + internal static int GetPhysicalRows(IRangeInfo range) + { + if (range is InMemoryRange imr) + return imr.PhysicalRows; + + if (!range.IsInMemoryRange && range.Worksheet?.Dimension != null) + { + var adjusted = range.GetAddressDimensionAdjusted(0); + if (adjusted != null) + { + // If the adjusted address is invalid (no column overlap), + // the range column has no data at all + if (adjusted.FromCol > adjusted.ToCol || adjusted.FromRow > adjusted.ToRow) + return 0; + var rangeFromRow = range.Address != null + ? range.Address.FromRow : adjusted.FromRow; + return adjusted.ToRow - rangeFromRow + 1; + } + } + + return range.Size.NumberOfRows; + } + } +} \ No newline at end of file diff --git a/src/EPPlus/NumberFormatToTextArgs.cs b/src/EPPlus/NumberFormatToTextArgs.cs index 01fcc949c..88b87ec25 100644 --- a/src/EPPlus/NumberFormatToTextArgs.cs +++ b/src/EPPlus/NumberFormatToTextArgs.cs @@ -12,6 +12,7 @@ Date Author Change *************************************************************************************************/ using OfficeOpenXml.FormulaParsing.Excel.Functions.Text; using OfficeOpenXml.Style; +using OfficeOpenXml.Style.Interfaces; using OfficeOpenXml.Style.XmlAccess; using OfficeOpenXml.Utils.String; @@ -23,6 +24,32 @@ namespace OfficeOpenXml public class NumberFormatToTextArgs { internal int _styleId; + + ExcelNumberFormatWithoutId fallbackNumberFormat = null; + + /// + /// If these args are provided from a formula + /// + public bool FromFormula = false; + + /// + /// Constructor when numberformat is not built in + /// + /// + /// + /// + /// + /// + internal NumberFormatToTextArgs(ExcelWorksheet ws, int row, int column, object value, string numberFormat) + { + Worksheet = ws; + Row = row; + Column = column; + Value = value; + _styleId = -1; + fallbackNumberFormat = new ExcelNumberFormatWithoutId(numberFormat); + } + internal NumberFormatToTextArgs(ExcelWorksheet ws, int row, int column, object value, int styleId) { Worksheet = ws; @@ -31,6 +58,7 @@ internal NumberFormatToTextArgs(ExcelWorksheet ws, int row, int column, object v Value = value; _styleId = styleId; } + /// /// The worksheet of the cell. /// @@ -46,11 +74,18 @@ internal NumberFormatToTextArgs(ExcelWorksheet ws, int row, int column, object v /// /// The number format settings for the cell /// - public ExcelNumberFormatXml NumberFormat + public IExcelNumberFormat NumberFormat { get { - return ValueToTextHandler.GetNumberFormat(_styleId, Worksheet.Workbook.Styles); + if(fallbackNumberFormat != null) + { + return fallbackNumberFormat; + } + else + { + return ValueToTextHandler.GetNumberFormat(_styleId, Worksheet.Workbook.Styles); + } } } /// @@ -64,6 +99,13 @@ public string Text { get { + if(fallbackNumberFormat != null) + { + var ft = new ExcelFormatTranslator(NumberFormat.Format, -1); + bool isValidFormat = false; + var frmt = ValueToTextHandler.FormatValue(Value, false, ft, null, out isValidFormat); + return frmt; + } return ValueToTextHandler.GetFormattedText(Value, Worksheet.Workbook, _styleId, false); } } diff --git a/src/EPPlus/RichData/RichDataStore.cs b/src/EPPlus/RichData/RichDataStore.cs index 1f092cb20..e19f3375f 100644 --- a/src/EPPlus/RichData/RichDataStore.cs +++ b/src/EPPlus/RichData/RichDataStore.cs @@ -74,6 +74,17 @@ internal ExcelRichValue GetRichValueByOneBasedIndex(int index) return rv; } + internal ExcelRichValue GetRichValueByOneBasedIndex(uint index) + { + return GetRichValueByOneBasedIndex((int)index); + } + + internal bool HasValueBeenDeleted(uint vmId) + { + var valueMetaData = _metadata.Db.ValueMetadata.Get(vmId); + return valueMetaData.Deleted; + } + /// /// Gets a rich value by its value metadata index /// diff --git a/src/EPPlus/RichData/RichValues/ExcelRichValue.cs b/src/EPPlus/RichData/RichValues/ExcelRichValue.cs index abe8cac10..a15f127a8 100644 --- a/src/EPPlus/RichData/RichValues/ExcelRichValue.cs +++ b/src/EPPlus/RichData/RichValues/ExcelRichValue.cs @@ -55,6 +55,8 @@ public ExcelRichValue(RichDataDatabase richDatadb, IndexedSubsetCollection _structureType == RichDataStructureTypes.WebImage || _structureType == RichDataStructureTypes.LocalImage; + public IndexedSubsetCollection Values { get; private set; } private Dictionary _relations = new Dictionary(); diff --git a/src/EPPlus/Style/Dxf/ExcelDxfStyle.cs b/src/EPPlus/Style/Dxf/ExcelDxfStyle.cs index f74feaae8..64955bd68 100644 --- a/src/EPPlus/Style/Dxf/ExcelDxfStyle.cs +++ b/src/EPPlus/Style/Dxf/ExcelDxfStyle.cs @@ -35,6 +35,7 @@ internal ExcelDxfStyle(XmlNamespaceManager nameSpaceManager, XmlNode topNode, Ex Font.SetValuesFromXml(_helper); Alignment.SetValuesFromXml(_helper); Protection.SetValuesFromXml(_helper); + Checkbox = _helper.ExistsNode($"d:extLst/d:ext[@uri='{ExtLstUris.FeaturePropertyBagDxf}']/xfpb:DXFComplement"); } } /// @@ -122,7 +123,7 @@ internal override void CreateNodes(XmlHelper helper, string path) var cmplNode = (XmlElement)helper.CreateNode("d:extLst/d:ext/xfpb:DXFComplement"); var extNode = (XmlElement)cmplNode.ParentNode; extNode.SetAttribute("xmlns:xfpb", Schemas.schemaFeaturePropertyBag); - extNode.SetAttribute("uri", ExtLstUris.FeaturePropertyBag); + extNode.SetAttribute("uri", ExtLstUris.FeaturePropertyBagDxf); cmplNode.SetAttribute("i", "0"); _styles._hasDxfCheckbox = true; } diff --git a/src/EPPlus/Style/ExcelTextFont.cs b/src/EPPlus/Style/ExcelTextFont.cs index c171dcebc..13ca12934 100644 --- a/src/EPPlus/Style/ExcelTextFont.cs +++ b/src/EPPlus/Style/ExcelTextFont.cs @@ -785,6 +785,7 @@ internal MeasurementFont GetMeasureFont() { return FontUtil.GetMeasureFont(LatinFont, ComplexFont, EastAsianFont, Size, GetFontStyle(), _pictureRelationDocument.Package); } + private MeasurementFontStyles GetFontStyle() { MeasurementFontStyles ret = MeasurementFontStyles.Regular; diff --git a/src/EPPlus/Style/Interfaces/IExcelNumberFormat.cs b/src/EPPlus/Style/Interfaces/IExcelNumberFormat.cs new file mode 100644 index 000000000..764aa1cbd --- /dev/null +++ b/src/EPPlus/Style/Interfaces/IExcelNumberFormat.cs @@ -0,0 +1,26 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; + +namespace OfficeOpenXml.Style.Interfaces +{ + /// + /// An interface for number formats. + /// + public interface IExcelNumberFormat + { + /// + /// The numberformat string + /// + public string Format { get; } + /// + /// Number format Id + /// + public int NumFmtId { get; } + /// + /// If this numberformat is built in + /// + public bool BuildIn { get; } + } +} diff --git a/src/EPPlus/Style/RichText/ExcelParagraph.cs b/src/EPPlus/Style/RichText/ExcelParagraph.cs index 4167b580f..61b8c1c0e 100644 --- a/src/EPPlus/Style/RichText/ExcelParagraph.cs +++ b/src/EPPlus/Style/RichText/ExcelParagraph.cs @@ -14,6 +14,7 @@ Date Author Change using OfficeOpenXml.Drawing.Controls; using OfficeOpenXml.Drawing.Interfaces; using OfficeOpenXml.Utils.EnumUtils; +using OfficeOpenXml.FormulaParsing.Excel.Functions.Text; using System; using System.Collections.Generic; using System.Text; @@ -31,6 +32,7 @@ internal ExcelParagraph(ExcelParagraphTextRunBase textRun) : { } const string AligPath = "../../a:pPr/@algn"; + const string FldPath = "../a:fld"; /// /// Text /// diff --git a/src/EPPlus/Style/RichText/ExcelParagraphCollection.cs b/src/EPPlus/Style/RichText/ExcelParagraphCollection.cs index e2b76807d..c5837de5b 100644 --- a/src/EPPlus/Style/RichText/ExcelParagraphCollection.cs +++ b/src/EPPlus/Style/RichText/ExcelParagraphCollection.cs @@ -10,15 +10,16 @@ Date Author Change ************************************************************************************************* 01/27/2020 EPPlus Software AB Initial release EPPlus 5 *************************************************************************************************/ +using OfficeOpenXml.Drawing; using System; using System.Collections.Generic; -using System.Text; -using System.Xml; -using OfficeOpenXml.Drawing; using System.Drawing; -using System.Linq; using System.Globalization; using OfficeOpenXml.Interfaces.Drawing.Text; +using System.IO; +using System.Linq; +using System.Text; +using System.Xml; namespace OfficeOpenXml.Style { /// diff --git a/src/EPPlus/Style/XmlAccess/ExcelNumberFormatWithoutId.cs b/src/EPPlus/Style/XmlAccess/ExcelNumberFormatWithoutId.cs new file mode 100644 index 000000000..9b953ad1f --- /dev/null +++ b/src/EPPlus/Style/XmlAccess/ExcelNumberFormatWithoutId.cs @@ -0,0 +1,23 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using OfficeOpenXml.Style.Interfaces; + +namespace OfficeOpenXml.Style.XmlAccess +{ + internal class ExcelNumberFormatWithoutId : IExcelNumberFormat + { + internal ExcelNumberFormatWithoutId(string format) + { + Format = format; + NumFmtId = -1; + BuildIn = false; + } + public string Format { get; private set; } + + public int NumFmtId { get; private set; } + + public bool BuildIn { get; private set; } + } +} diff --git a/src/EPPlus/Style/XmlAccess/ExcelNumberFormatXml.cs b/src/EPPlus/Style/XmlAccess/ExcelNumberFormatXml.cs index 7b6c4a665..a0c0eb650 100644 --- a/src/EPPlus/Style/XmlAccess/ExcelNumberFormatXml.cs +++ b/src/EPPlus/Style/XmlAccess/ExcelNumberFormatXml.cs @@ -10,22 +10,23 @@ Date Author Change ************************************************************************************************* 01/27/2020 EPPlus Software AB Initial release EPPlus 5 *************************************************************************************************/ +using OfficeOpenXml.Style.Interfaces; +using OfficeOpenXml.Utils; using System; using System.Collections.Generic; -using System.Text; -using System.Xml; using System.Globalization; -using System.Text.RegularExpressions; -using OfficeOpenXml.Utils; -using System.Runtime.InteropServices; using System.Linq; +using System.Runtime.InteropServices; +using System.Text; +using System.Text.RegularExpressions; +using System.Xml; namespace OfficeOpenXml.Style.XmlAccess { /// /// Xml access class for number customFormats /// - public sealed class ExcelNumberFormatXml : StyleXmlHelper + public sealed class ExcelNumberFormatXml : StyleXmlHelper, IExcelNumberFormat { internal ExcelNumberFormatXml(XmlNamespaceManager nameSpaceManager) : base(nameSpaceManager) diff --git a/src/EPPlus/Table/PivotTable/Calculation/PivotTableCalculation.cs b/src/EPPlus/Table/PivotTable/Calculation/PivotTableCalculation.cs index d1b4ff3f6..ee96c26d1 100644 --- a/src/EPPlus/Table/PivotTable/Calculation/PivotTableCalculation.cs +++ b/src/EPPlus/Table/PivotTable/Calculation/PivotTableCalculation.cs @@ -57,47 +57,50 @@ internal partial class PivotTableCalculation }; internal static bool Calculate(ExcelPivotTable pivotTable, out List calculatedItems, out List>> keys) { - calculatedItems = new List(); - keys = new List>>(); + calculatedItems = new List(); + keys = new List>>(); var fieldIndex = pivotTable.RowColumnFieldIndicies; - pivotTable.InitCalculation(); - int dfIx = 0; - bool hasCalculatedField = false; - foreach(var df in pivotTable.GetFieldsToCalculate()) + pivotTable.InitCalculation(); + int dfIx = 0; + bool hasCalculatedField = false; + + + foreach (var df in pivotTable.GetFieldsToCalculate()) { - var dataFieldItems = new PivotCalculationStore(); - calculatedItems.Add(dataFieldItems); - var keyDict = PivotTableCalculation.GetNewKeys(); - keys.Add(keyDict); - if (string.IsNullOrEmpty(df.Field.Cache.Formula)) - { - CalculateField(pivotTable, calculatedItems[calculatedItems.Count - 1], keys, df.Field.Cache, df.Function); + var dataFieldItems = new PivotCalculationStore(); + calculatedItems.Add(dataFieldItems); + var keyDict = PivotTableCalculation.GetNewKeys(); + keys.Add(keyDict); + + if (string.IsNullOrEmpty(df.Field.Cache.Formula)) + { + CalculateField(pivotTable, calculatedItems[calculatedItems.Count - 1], keys, df.Field.Cache, df.Function); + SetRowColumnsItemsToHashSets(pivotTable); if (df.ShowDataAs.Value != eShowDataAs.Normal) - { - _calculateShowAs[df.ShowDataAs.Value].Calculate(df, fieldIndex, keys[dfIx], ref dataFieldItems); - calculatedItems[dfIx] = dataFieldItems; - } - } - else - { - hasCalculatedField=true; - } - dfIx++; - } + { + _calculateShowAs[df.ShowDataAs.Value].Calculate(df, fieldIndex, keys[dfIx], ref dataFieldItems); + calculatedItems[dfIx] = dataFieldItems; + } + } + else + { + hasCalculatedField = true; + } + dfIx++; + } - CalculateRowColumnSubtotals(pivotTable, keys); + CalculateRowColumnSubtotals(pivotTable, keys); //Handle Calculated fields after the pivot table fields has been calculated. - if (hasCalculatedField) - { - CalculateSourceFields(pivotTable); - var ptCalc = new PivotTableColumnCalculation(pivotTable); - ptCalc.CalculateFormulaFields(fieldIndex); - } - - return true; + if (hasCalculatedField) + { + CalculateSourceFields(pivotTable); + var ptCalc = new PivotTableColumnCalculation(pivotTable); + ptCalc.CalculateFormulaFields(fieldIndex); + } + return true; } private static void CalculateRowColumnSubtotals(ExcelPivotTable pivotTable, List>> keys) @@ -188,44 +191,98 @@ private static void SetRowColumnsItemsToHashSets(ExcelPivotTable pivotTable) } private static void CalculateSourceFields(ExcelPivotTable pivotTable) - { - var keys = new List>>(); - var calcFields = new Dictionary(StringComparer.InvariantCultureIgnoreCase); - foreach(var field in pivotTable.Fields.Where(x=>string.IsNullOrEmpty(x.Cache.Formula)==false).Select(x=>x.Cache)) - { - foreach(var token in field.FormulaTokens) - { - if(token.TokenType==TokenType.PivotField && calcFields.ContainsKey(token.Value)==false) - { - if(!GetSumCalcItems(pivotTable, token.Value, out PivotCalculationStore store)) - { - var keyDict = PivotTableCalculation.GetNewKeys(); - keys.Add(keyDict); - store = new PivotCalculationStore(); - CalculateField(pivotTable, store, keys, pivotTable.Fields[token.Value].Cache, DataFieldFunctions.Sum); - } - calcFields.Add(token.Value, store); - } - } - } - pivotTable.CalculatedFieldReferencedItems = calcFields; - } + { + var keys = new List>>(); + var calcFields = new Dictionary( + StringComparer.InvariantCultureIgnoreCase); - private static bool GetSumCalcItems(ExcelPivotTable pivotTable, string fieldName, out PivotCalculationStore store) - { - foreach(var ds in pivotTable.DataFields) - { - if(ds.Field!=null && ds.Field.Name.Equals(fieldName, StringComparison.InvariantCultureIgnoreCase) && ds.Function==DataFieldFunctions.Sum && ds.ShowDataAsInternal == eShowDataAs.Normal) - { - store = pivotTable.CalculatedItems[ds.Index]; - return true; - } - } - store = null; - return false; - } + // Find calculated cache fields via DataFields (not pivotTable.Fields) + var calcCacheFields = new List(); + foreach (var df in pivotTable.DataFields) + { + if (!string.IsNullOrEmpty(df.Field.Cache.Formula)) + { + if (!calcCacheFields.Contains(df.Field.Cache)) + { + calcCacheFields.Add(df.Field.Cache); + } + } + } + + // Also check transitive dependencies: calculated fields referencing + // other calculated fields + var cacheFieldsList = pivotTable.CacheDefinition._cacheReference.Fields; + for (int i = 0; i < calcCacheFields.Count; i++) // Count may grow during iteration + { + var cf = calcCacheFields[i]; + foreach (var token in cf.FormulaTokens) + { + if (token.TokenType == TokenType.PivotField) + { + var refCf = cacheFieldsList.FirstOrDefault( + x => x.Name.Equals(token.Value, StringComparison.InvariantCultureIgnoreCase)); + if (refCf != null && !string.IsNullOrEmpty(refCf.Formula) + && !calcCacheFields.Contains(refCf)) + { + calcCacheFields.Add(refCf); + } + } + } + } + + // Now resolve each referenced source field for all calculated fields + foreach (var cf in calcCacheFields) + { + foreach (var token in cf.FormulaTokens) + { + if (token.TokenType == TokenType.PivotField + && !calcFields.ContainsKey(token.Value)) + { + if (!GetSumCalcItems(pivotTable, token.Value, out PivotCalculationStore store)) + { + // Look up via cache fields, not pivotTable.Fields + var refCf = cacheFieldsList.FirstOrDefault( + x => x.Name.Equals( + token.Value, StringComparison.InvariantCultureIgnoreCase)); + if (refCf != null && string.IsNullOrEmpty(refCf.Formula)) + { + var keyDict = PivotTableCalculation.GetNewKeys(); + keys.Add(keyDict); + store = new PivotCalculationStore(); + CalculateField(pivotTable, store, keys, refCf, DataFieldFunctions.Sum); + } + else + { + // Referenced field is itself calculated or doesn't exist — + // it will be resolved during CalculateFormulaFields + store = new PivotCalculationStore(); + } + } + calcFields.Add(token.Value, store); + } + } + } + pivotTable.CalculatedFieldReferencedItems = calcFields; + } + + private static bool GetSumCalcItems(ExcelPivotTable pivotTable, string fieldName, out PivotCalculationStore store) + { + foreach (var ds in pivotTable.DataFields) + { + if (ds.Field != null && + ds.Field.Name.Equals(fieldName, StringComparison.InvariantCultureIgnoreCase) && + ds.Function == DataFieldFunctions.Sum && + ds.ShowDataAsInternal == eShowDataAs.Normal) + { + store = pivotTable.CalculatedItems[pivotTable.DataFields.IndexOf(ds)]; + return true; + } + } + store = null; + return false; + } - private static void CalculateField(ExcelPivotTable pivotTable, PivotCalculationStore dataFieldItems, List>> keys, ExcelPivotTableCacheField cacheField, DataFieldFunctions function) + private static void CalculateField(ExcelPivotTable pivotTable, PivotCalculationStore dataFieldItems, List>> keys, ExcelPivotTableCacheField cacheField, DataFieldFunctions function) { var ci = pivotTable.CacheDefinition._cacheReference; var recs = ci.Records; diff --git a/src/EPPlus/Table/PivotTable/Calculation/PivotTableColumnCalculation.cs b/src/EPPlus/Table/PivotTable/Calculation/PivotTableColumnCalculation.cs index 7648a559e..3bb7baf44 100644 --- a/src/EPPlus/Table/PivotTable/Calculation/PivotTableColumnCalculation.cs +++ b/src/EPPlus/Table/PivotTable/Calculation/PivotTableColumnCalculation.cs @@ -10,20 +10,15 @@ Date Author Change ************************************************************************************************* 03/07/2024 EPPlus Software AB EPPlus 7.2 *************************************************************************************************/ -using OfficeOpenXml.DataValidation.Exceptions; using OfficeOpenXml.FormulaParsing; using OfficeOpenXml.FormulaParsing.Excel.Functions; -using OfficeOpenXml.FormulaParsing.Excel.Functions.Finance; -using OfficeOpenXml.FormulaParsing.Excel.Functions.MathFunctions; using OfficeOpenXml.FormulaParsing.LexicalAnalysis; -using OfficeOpenXml.LoadFunctions.ReflectionHelpers; using OfficeOpenXml.Table.PivotTable.Calculation; using OfficeOpenXml.Utils.TypeConversion; using System; using System.Collections.Generic; using System.Globalization; using System.Linq; -using System.Net; namespace OfficeOpenXml.Table.PivotTable { @@ -40,55 +35,140 @@ public PivotTableColumnCalculation(ExcelPivotTable tbl) _fr = _tbl.WorkSheet.Workbook.FormulaParser.ParsingContext.Configuration.FunctionRepository; _calcItems = tbl.CalculatedItems; } - internal void CalculateFormulaFields(List fieldIndex) - { - var calcOrder = GetCalcOrder(); - foreach(var i in calcOrder) - { - var f = _tbl.Fields[i]; - var tokens = f.Cache.FormulaTokens; - var calcTokens = GetPivotFieldReferencesInFormula(f, tokens); - PivotCalculationStore store; - if(calcTokens.Any(x => x == null)) - { - //Contains invalid field reference or functions not supported in PT. - throw (new InvalidOperationException($"Pivot table {_tbl.Name} contains invalid column calculated formula : {f.Cache.Formula}. The formula contains an invalid field, an unsupported function or cell reference.")); + internal void CalculateFormulaFields(List fieldIndex) + { + var calcOrder = GetCalcOrder(); + var cacheFields = _tbl.CacheDefinition._cacheReference.Fields; + + foreach (var cfIndex in calcOrder) + { + var cf = cacheFields[cfIndex]; + var tokens = cf.FormulaTokens; + + // Build pivot field references from tokens + // (mirrors original GetPivotFieldReferencesInFormula logic, but against cache) + var calcTokens = new List(); + bool hasInvalid = false; + int ix = 0; + foreach (var t in tokens) + { + if (t.TokenType == TokenType.PivotField) + { + var refCf = cacheFields.FirstOrDefault( + x => x.Name.Equals(t.Value, StringComparison.InvariantCultureIgnoreCase)); + if (refCf != null) + { + calcTokens.Add(new int[] { ix }); + } + else + { + hasInvalid = true; + break; + } + } + else if (t.TokenType == TokenType.Array || + t.TokenType == TokenType.CellAddress || + t.TokenType == TokenType.FullColumnAddress || + t.TokenType == TokenType.FullRowAddress || + t.TokenType == TokenType.TableName || + t.TokenType == TokenType.WorksheetName) + { + hasInvalid = true; + break; + } + else if (t.TokenType == TokenType.Function) + { + var func = _fr.GetFunction(t.Value); + if (func != null && func.IsAllowedInCalculatedPivotTableField == false) + { + hasInvalid = true; + break; + } + } + ix++; + } + + PivotCalculationStore store; + if (hasInvalid) + { + throw new InvalidOperationException( + $"Pivot table {_tbl.Name} contains invalid column calculated formula : " + + $"{cf.Formula}. The formula contains an invalid field, an unsupported " + + "function or cell reference."); } else - { - store = CalculateField(f, tokens, calcTokens, fieldIndex); + { + store = new PivotCalculationStore(); + var options = new ExcelCalculationOption(); + var depChain = new RpnOptimizedDependencyChain(_tbl.WorkSheet.Workbook, options); + var ct = new List(); + ct.AddRange(tokens.Select(x => new Token(x.Value, x.TokenType, x.IsNegated))); + + // Collect ALL unique keys across all referenced source fields, + // so we don't miss keys that only exist in some fields. + var allKeys = new List(); + var seenKeys = new HashSet(); + foreach (var c in calcTokens) + { + var fieldName = tokens[c[0]].Value; + if (_tbl.CalculatedFieldReferencedItems.TryGetValue(fieldName, out var refStore)) + { + foreach (var k in refStore.Index) + { + var keyStr = string.Join(",", k.Key.Select(x => x.ToString()).ToArray()); + if (seenKeys.Add(keyStr)) + { + allKeys.Add(k.Key); + } + } + } + } + + // Calculate formula for each key combination + foreach (var key in allKeys) + { + + foreach (var c in calcTokens) + { + var fieldName = tokens[c[0]].Value; + if (_tbl.CalculatedFieldReferencedItems.TryGetValue(fieldName, out var refStore) + && refStore.ContainsKey(key)) + { + ct[c[0]] = GetTokenFromValue(refStore[key]); + } + else + { + // Key doesn't exist for this field — use 0 as default + // (Excel treats missing pivot values as 0 in calculated fields) + ct[c[0]] = new Token("0", TokenType.Decimal); + } + } + var cv = RpnFormulaExecution.ExecutePivotFieldFormula(depChain, ct, options); + store.Add(key, cv); + } } - if (f.IsDataField) + + // Map back to DataField by matching the cache field. + // Multiple DataFields can reference the same calculated cache field + // (e.g. same field with different ShowDataAs), so don't break on first match. + bool stored = false; + for (int d = 0; d < _tbl.DataFields.Count; d++) { - var ix = _tbl.DataFields.IndexOf(f.DataField); - _tbl.CalculatedItems[ix] = store; + var dfCache = _tbl.DataFields[d].Field.Cache; + if (dfCache == cf || + dfCache.Name.Equals(cf.Name, StringComparison.InvariantCultureIgnoreCase)) + { + _tbl.CalculatedItems[d] = store; + stored = true; + } } - else + if (!stored) { - _tbl.CalculatedFieldReferencedItems.Add(f.Name, store); + _tbl.CalculatedFieldReferencedItems[cf.Name] = store; } } } - private PivotCalculationStore CalculateField(ExcelPivotTableField f, IList tokens, List calcTokens, List fieldIndex) - { - var store = new PivotCalculationStore(); - var options = new ExcelCalculationOption(); - var depChain = new RpnOptimizedDependencyChain(_tbl.WorkSheet.Workbook, options); - var ct = new List(); - ct.AddRange(tokens.Select(x => new Token(x.Value, x.TokenType, x.IsNegated))); - foreach (var ci in _tbl.CalculatedFieldReferencedItems.First().Value.Index) - { - foreach (var c in calcTokens) - { - var v = _tbl.CalculatedFieldReferencedItems[tokens[c[0]].Value][ci.Key]; - ct[c[0]] = GetTokenFromValue(v); - } - var cv=RpnFormulaExecution.ExecutePivotFieldFormula(depChain, ct, options); - store.Add(ci.Key, cv); - } - return store; - } private Token GetTokenFromValue(object v) { if(ConvertUtil.IsNumericOrDate(v)) @@ -116,88 +196,67 @@ private Token GetTokenFromValue(object v) return new Token(v.ToString(),TokenType.String); } - private List GetPivotFieldReferencesInFormula(ExcelPivotTableField f, IList tokens) - { - var ret = new List(); - int ix = 0; - foreach (var t in tokens) - { - if(t.TokenType==TokenType.PivotField) - { - var ff = _tbl.Fields[t.Value]; - if (ff == null) - { - ret.Add(null); - return ret; - } - else - { - ret.Add([ix, ff.Index]); - } - } - else if( - t.TokenType == TokenType.Array || - t.TokenType == TokenType.CellAddress || - t.TokenType == TokenType.FullColumnAddress || - t.TokenType == TokenType.FullRowAddress || - t.TokenType == TokenType.TableName || - t.TokenType == TokenType.WorksheetName) - { - ret.Add(null); - return ret; - } - else if(t.TokenType==TokenType.Function) - { - var function = _fr.GetFunction(t.Value); - if(function.IsAllowedInCalculatedPivotTableField==false) - { - ret.Add(null); - return ret; - } - } - ix++; - } - return ret; - } + private List GetCalcOrder() + { + var calcOrder = new List(); + var cacheFields = _tbl.CacheDefinition._cacheReference.Fields; - private List GetCalcOrder() - { - var calcOrder = new List(); - foreach (var f in _tbl.Fields.Where(x => string.IsNullOrEmpty(x.Cache.Formula) == false)) - { - if (calcOrder.Contains(f.Index)) continue; - ValidateNoCircularReference(f, calcOrder); - } - return calcOrder; - } + // Collect cache field indices that are used as DataFields and have formulas + var relevantCfIndices = new HashSet(); + foreach (var df in _tbl.DataFields) + { + if (!string.IsNullOrEmpty(df.Field.Cache.Formula)) + { + var cfIndex = cacheFields.IndexOf(df.Field.Cache); + if (cfIndex >= 0) + { + relevantCfIndices.Add(cfIndex); + } + } + } - private bool ValidateNoCircularReference(ExcelPivotTableField f, List calcOrder, Stack prevFields = null) - { - if (prevFields == null) prevFields = new Stack(); - var tokens = SourceCodeTokenizer.PivotFormula.Tokenize(f.Cache.Formula); - foreach (var t in tokens) - { - if (t.TokenType == TokenType.PivotField) - { - var f2 = _tbl.Fields[t.Value]; - if (f2 != null && string.IsNullOrEmpty(f2.Cache.Formula)==false) - { - if (t.Value.Equals(f.Name, StringComparison.InvariantCultureIgnoreCase)) - { - throw(new InvalidOperationException($"Circular reference in pivot table {_tbl.Name} Calculated Field {f.Name}")); - } - if(prevFields.Any(x=>x.Name.Equals(t.Value, StringComparison.InvariantCultureIgnoreCase))) - { - throw(new InvalidOperationException($"Circular reference in pivot table {_tbl.Name} Calculated Field {f.Name}")); - } + foreach (var cfIndex in relevantCfIndices) + { + if (calcOrder.Contains(cfIndex)) continue; + ValidateNoCircularReferenceCacheField( + cacheFields[cfIndex], cfIndex, calcOrder, cacheFields); + } + return calcOrder; + } - prevFields.Push(f); - ValidateNoCircularReference(f2, calcOrder, prevFields); - } - } - } - calcOrder.Add(f.Index); - return true; - } + private bool ValidateNoCircularReferenceCacheField( + ExcelPivotTableCacheField cf, + int cfIndex, + List calcOrder, + List cacheFields, + Stack prevIndices = null) + { + if (prevIndices == null) prevIndices = new Stack(); + var tokens = SourceCodeTokenizer.PivotFormula.Tokenize(cf.Formula); + foreach (var t in tokens) + { + if (t.TokenType == TokenType.PivotField) + { + var refIndex = cacheFields.FindIndex( + x => x.Name.Equals(t.Value, StringComparison.InvariantCultureIgnoreCase)); + if (refIndex >= 0 && !string.IsNullOrEmpty(cacheFields[refIndex].Formula)) + { + if (refIndex == cfIndex || prevIndices.Contains(refIndex)) + { + throw new InvalidOperationException( + $"Circular reference in pivot table {_tbl.Name} Calculated Field {cf.Name}"); + } + prevIndices.Push(cfIndex); + ValidateNoCircularReferenceCacheField( + cacheFields[refIndex], refIndex, calcOrder, cacheFields, prevIndices); + } + } + } + if (!calcOrder.Contains(cfIndex)) + { + calcOrder.Add(cfIndex); + } + return true; + } } } \ No newline at end of file diff --git a/src/EPPlus/Table/PivotTable/ExcelPivotTable.cs b/src/EPPlus/Table/PivotTable/ExcelPivotTable.cs index ab3a4b1d2..955e57540 100644 --- a/src/EPPlus/Table/PivotTable/ExcelPivotTable.cs +++ b/src/EPPlus/Table/PivotTable/ExcelPivotTable.cs @@ -406,15 +406,18 @@ public bool IsCalculated /// If the pivot cache should be refreshed from the source data, before calculating the pivot table. public void Calculate(bool refreshCache = false) { - if(CacheDefinition.Connection!=null) + if (CacheDefinition.Connection != null) { return; } - if (refreshCache || CacheDefinition._cacheReference.Records == null || CacheDefinition._cacheReference.Records.RecordCount == 0) + + if (refreshCache || CacheDefinition._cacheReference.Records == null || CacheDefinition._cacheReference.Records.RecordCount == 0) { CacheDefinition.Refresh(); } + PivotTableCalculation.Calculate(this, out CalculatedItems, out Keys); + IsCalculated = true; } /// @@ -452,17 +455,36 @@ public object GetPivotData(string dataFieldName, IList uniqueItems)) @@ -564,12 +584,19 @@ public object GetPivotData(string dataFieldName, IList fields, List< //Update data field index foreach (var df in pt.DataFields) { - var ix = movedFields.IndexOf(df.Index); - if(ix<0) + // Check if the underlying field still exists in the updated fields list + // by matching field names (case-insensitive) + if (df.Field == null || !fields.Any(x => x.Name.Equals(df.Field.Name, StringComparison.InvariantCultureIgnoreCase))) { rmDfFields.Add(df); } - else if (df.Index != df.Field.Index) + else { - df.Index = df.Field.Index; + // Update the data field's index if the underlying field was moved + var newField = fields.FirstOrDefault(x => x.Name.Equals(df.Field.Name, StringComparison.InvariantCultureIgnoreCase)); + if (newField != null && df.Index != newField.Index) + { + df.Index = newField.Index; + } } } rmDfFields.ForEach(df => { df.Field.IsDataField = false; pt.DataFields.Remove(df); }); - if(pt.DataFields.Count==0) + if (pt.DataFields.Count == 0) { pt.DeleteNode("d:dataFields"); } diff --git a/src/EPPlus/Utils/String/ValueToTextHandler.cs b/src/EPPlus/Utils/String/ValueToTextHandler.cs index 6e4d47ed4..f93ed755b 100644 --- a/src/EPPlus/Utils/String/ValueToTextHandler.cs +++ b/src/EPPlus/Utils/String/ValueToTextHandler.cs @@ -263,7 +263,14 @@ private static string FormatNumberExcel(double d, string format, CultureInfo cul } else { - return d.ToString(format, cultureInfo); + try + { + return d.ToString(format, cultureInfo); + } + catch + { + return format; + } } } diff --git a/src/EPPlus/XmlHelper.cs b/src/EPPlus/XmlHelper.cs index f72c36ea2..3602778db 100644 --- a/src/EPPlus/XmlHelper.cs +++ b/src/EPPlus/XmlHelper.cs @@ -25,6 +25,7 @@ Date Author Change using OfficeOpenXml.Utils.TypeConversion; using OfficeOpenXml.Utils.EnumUtils; using OfficeOpenXml.Drawing; +using System.Xml.XPath; namespace OfficeOpenXml { @@ -596,6 +597,38 @@ internal XmlNode GetNode(string path) { return TopNode.SelectSingleNode(path, NameSpaceManager); } + + /// + /// If Get Node doesn't work. + /// Simplified way of applying the default node prefix to empty args. + /// + /// + /// + internal XmlNode GetDefaultNode(string path) + { + var retNode = GetNode(path); + + if (retNode == null) + { + var defaultPrefix = NameSpaceManager.LookupPrefix(NameSpaceManager.DefaultNamespace); + + var splitArgs = path.Split('/'); + + for (int i = 0; i < splitArgs.Length; i++) + { + if (splitArgs[i].Contains(":") == false) + { + //No prefix add default prefix + splitArgs[i] = splitArgs[i].Insert(0, defaultPrefix + ":"); + } + } + var alteredExp = string.Join("/", splitArgs.ToArray()); + retNode = GetNode(alteredExp); + } + + return retNode; + } + internal XmlNodeList GetNodes(string path) { return TopNode.SelectNodes(path, NameSpaceManager); diff --git a/src/EPPlusTest/Data/ConnectionTests.cs b/src/EPPlusTest/Data/ConnectionTests.cs index 6fc695598..12499eddb 100644 --- a/src/EPPlusTest/Data/ConnectionTests.cs +++ b/src/EPPlusTest/Data/ConnectionTests.cs @@ -3,7 +3,9 @@ using OfficeOpenXml.Data.Connection; using System; using System.Data.Common; +using System.Globalization; using System.Text; +using System.Threading; using System.Xml; namespace EPPlusTest.Data @@ -193,10 +195,26 @@ public void ReadEPPWebPowerQuery() SaveAndCleanup(p); } } + [TestMethod] - public void AddPivotTableWithConnection() + public void GetValueAsText_DateTime_ShouldUseInvariantCulture() { + var cc = Thread.CurrentThread.CurrentCulture; + try + { + Thread.CurrentThread.CurrentCulture = CultureInfo.GetCultureInfo("fi-FI"); + var entry = new ExcelPowerQueryMetaDataEntry("FillLastUpdated", + new DateTime(2026, 2, 19, 8, 15, 36, DateTimeKind.Utc)); + var result = entry.GetValueAsText(CultureInfo.GetCultureInfo("fi-FI")); + Assert.IsTrue(result.Contains(":"), + $"Expected colon as time separator but got: {result}"); + } + finally + { + Thread.CurrentThread.CurrentCulture = cc; + } } + [ClassCleanup] public static void Cleanup() { diff --git a/src/EPPlusTest/Drawing/Chart/ChartSeriesTest.cs b/src/EPPlusTest/Drawing/Chart/ChartSeriesTest.cs index 353a22c52..bd5edb1e9 100644 --- a/src/EPPlusTest/Drawing/Chart/ChartSeriesTest.cs +++ b/src/EPPlusTest/Drawing/Chart/ChartSeriesTest.cs @@ -1,14 +1,22 @@ -using Microsoft.VisualStudio.TestTools.UnitTesting; +using Castle.Components.DictionaryAdapter.Xml; +using Microsoft.VisualStudio.TestTools.UnitTesting; +using Newtonsoft.Json; using OfficeOpenXml; +using OfficeOpenXml.ConditionalFormatting; using OfficeOpenXml.Drawing; using OfficeOpenXml.Drawing.Chart; using OfficeOpenXml.Drawing.Chart.ChartEx; +using OfficeOpenXml.Drawing.Interfaces; +using OfficeOpenXml.FormulaParsing.Utilities; +using OfficeOpenXml.Style; using System; using System.Collections.Generic; using System.Drawing; using System.Linq; +using System.Runtime.CompilerServices; using System.Text; using System.Threading.Tasks; +using System.Xml; namespace EPPlusTest.Drawing.Chart { @@ -241,5 +249,212 @@ public void SimpleChartDataLabels() SaveAndCleanup(p); } } + + [TestMethod] + //TODO: This test is one instance of a larger problem + //Many datalabels have different allowed positions depending on chart type + //Going against it will often create corrupt files. + //See microsoft offical documentation: + //"MS-OE376" page 659 2.1.1475 Part 4 Section 5.7.2.48, dLblPos (Data Label Position) for details. + public void TopIsDisallowedOnBarDataLabels() + { + using (var p = new ExcelPackage()) + { + var ws = p.Workbook.Worksheets.Add("DataLabelSheet"); + + ws.Cells["A1"].Value = "Week"; + ws.Cells["B1"].Value = "Income"; + + ws.Cells["A2:A10"].Formula = $"\"Week \"&(ROW()-1)"; + ws.Cells["B2:B10"].Formula = $"(ROW()-1)*7"; + ws.Calculate(); + + var chart = ws.Drawings.AddBarChart("columnChart", eBarChartType.ColumnClustered); + chart.Series.Add(ws.Cells["B2:B10"], ws.Cells["A2:A10"]); + + var SeriesDataLabel = chart.Series[0].DataLabel; + + Assert.Throws(() => SeriesDataLabel.Position = eLabelPosition.Top); + } + } + + [TestMethod] + public void CreateFileWithDataLabelsManualAndGeneral() + { + using (var p = OpenPackage("dlblMissMatchTest.xlsx", true)) + { + var ws = p.Workbook.Worksheets.Add("DataLabelSheet"); + + ws.Cells["A1"].Value = "Week"; + ws.Cells["B1"].Value = "Income"; + + ws.Cells["A2:A10"].Formula = $"\"Week \"&(ROW()-1)"; + ws.Cells["B2:B10"].Formula = $"(ROW()-1)*7"; + ws.Cells["C2:C10"].Formula = $"\"Comment \"&(ROW()-1)"; + ws.Calculate(); + + var chart = ws.Drawings.AddBarChart("columnChart", eBarChartType.ColumnClustered); + + var barSerie = chart.Series.Add(ws.Cells["B2:B10"], ws.Cells["A2:A10"]); + var sDlbl = barSerie.DataLabel; + + sDlbl.Separator = ","; + sDlbl.ShowValue = true; + sDlbl.ShowCategory = true; + sDlbl.Position = eLabelPosition.OutEnd; + + sDlbl.SetValueFromCellsRange(ws.Cells["C2:C10"]); + Assert.AreEqual(ws.Cells["C2:C10"], barSerie.DataLabel.DataLabelRange); + + Assert.AreEqual("C7", chart.Series[0].DataLabel.DataLabels[5].SingleCellAddressFromSeries.Address); + Assert.AreEqual("Comment 6", ws.Cells["C7"].Text); + + SaveAndCleanup(p); + } + + //Ensure data is read correctly after write + using (var p = OpenPackage("dlblMissMatchTest.xlsx")) + { + var ws = p.Workbook.Worksheets[0]; + var chart = ws.Drawings[0].As.Chart.BarChart; + + var barSerie = chart.Series[0]; + Assert.AreEqual(ws.Cells["C2:C10"], barSerie.DataLabel.DataLabelRange); + + Assert.AreEqual("C7", chart.Series[0].DataLabel.DataLabels[5].SingleCellAddressFromSeries.Address); + Assert.AreEqual("Comment 6", ws.Cells["C7"].Text); + } + } + + [TestMethod] + public void ReadSimpleFile() + { + using (var package = OpenTemplatePackage("editedDataLabel.xlsx")) + { + var ws = package.Workbook.Worksheets[0]; + + var myChart = ws.Drawings[0].As.Chart.BarChart; + + var lbl = myChart.Series[0].DataLabel.DataLabels[0]; + + var lblTxtBody = myChart.Series[0].DataLabel.DataLabels[0].TextBody; + + SaveAndCleanup(package); + } + } + + [TestMethod] + public void ReadFile() + { + using (var package = OpenTemplatePackage("S1008_NoComment.xlsx")) + { + var ws = package.Workbook.Worksheets[0]; + + var chart = ws.Drawings[0].As.Chart.LineChart; + + chart.Series[0].DataLabel.Separator = " "; + + //Select comment range + chart.Series[0].DataLabel.SetValueFromCellsRange(ws.Cells["E1:E53"]); + + //Set the relevant labels to not show value + chart.Series[0].DataLabel.DataLabels[21].ShowValue = false; + chart.Series[0].DataLabel.DataLabels[26].ShowValue = false; + + Assert.AreEqual("E22", chart.Series[0].DataLabel.DataLabels[21].SingleCellAddressFromSeries.Address); + Assert.AreEqual("First comment", ws.Cells["E22"].Text); + + SaveAndCleanup(package); + } + } + + [TestMethod] + public void TestAddCommentRangeToExistingFile() + { + using (var package = OpenTemplatePackage("S1008_NoComment.xlsx")) + { + var ws = package.Workbook.Worksheets[0]; + + var commentText = "Added Comment"; + + ws.Cells["E30"].Value = commentText; + + var chart = ws.Drawings[0].As.Chart.LineChart; + chart.Series[0].DataLabel.Separator = " "; + + //Select comment range + chart.Series[0].DataLabel.SetValueFromCellsRange(ws.Cells["E2:E53"]); + + //Note that since we start on E2 the datalabel idx becomes 20 for row 22 etc. + var label1 = chart.Series[0].DataLabel.DataLabels[20]; + var label2 = chart.Series[0].DataLabel.DataLabels[25]; + var label3 = chart.Series[0].DataLabel.DataLabels[28]; + + //Set the relevant labels to not show value as we only want them to show comments + label1.ShowValue = false; + label2.ShowValue = false; + label3.ShowValue = false; + + Assert.AreEqual("E30", chart.Series[0].DataLabel.DataLabels[28].SingleCellAddressFromSeries.Address); + Assert.AreEqual(commentText, ws.Cells["E30"].Text); + + //XforSave is set soley on labels that are not truly neccesary + + SaveAndCleanup(package); + } + } + + [TestMethod] + public void DatalabelRangeLiterals() + { + string item1 = "one"; + string item2 = "two"; + string item3 = "three"; + + using (var p = OpenPackage("dlblRangeLiterals.xlsx", true)) + { + var ws = p.Workbook.Worksheets.Add("DataLabelSheet"); + + ws.Cells["A1"].Value = "Week"; + ws.Cells["B1"].Value = "Income"; + + ws.Cells["A2:A10"].Formula = $"\"Week \"&(ROW()-1)"; + ws.Cells["B2:B10"].Formula = $"(ROW()-1)*7"; + ws.Cells["C2:C10"].Formula = $"\"Comment \"&(ROW()-1)"; + ws.Calculate(); + + var chart = ws.Drawings.AddBarChart("columnChart", eBarChartType.ColumnClustered); + + var barSerie = chart.Series.Add(ws.Cells["B2:B10"], ws.Cells["A2:A10"]); + var sDlbl = barSerie.DataLabel; + + sDlbl.ShowValue = true; + sDlbl.Position = eLabelPosition.OutEnd; + + sDlbl.ValueFromCellsRange = $"{{\"{item1}\",\"{item2}\",\"{item3}\"}}"; + + var dlblLitterals = barSerie.GetDataLabelLiterals(); + + Assert.AreEqual(item1, dlblLitterals[0]); + Assert.AreEqual(item2, dlblLitterals[1]); + Assert.AreEqual(item3, dlblLitterals[2]); + + SaveAndCleanup(p); + } + + using (var p = OpenPackage("dlblRangeLiterals.xlsx")) + { + var ws = p.Workbook.Worksheets[0]; + var chart = ws.Drawings[0].As.Chart.BarChart; + + var barSerie = chart.Series[0]; + + var cache = barSerie.GetDataLabelLiterals(); + + Assert.AreEqual(item1, cache[0]); + Assert.AreEqual(item2, cache[1]); + Assert.AreEqual(item3, cache[2]); + } + } } } diff --git a/src/EPPlusTest/Drawing/OLETests.cs b/src/EPPlusTest/Drawing/OLETests.cs index 4c25fee4a..72e09546f 100644 --- a/src/EPPlusTest/Drawing/OLETests.cs +++ b/src/EPPlusTest/Drawing/OLETests.cs @@ -795,6 +795,6 @@ public void ExtractDataFromOleObjectTest() var linkOleBytes = linkOle.GetEmbeddedObjectBytes(); Assert.IsNull(linkOleBytes); } - + } } \ No newline at end of file diff --git a/src/EPPlusTest/FormulaParsing/CancelCalculationTests.cs b/src/EPPlusTest/FormulaParsing/CancelCalculationTests.cs new file mode 100644 index 000000000..dbc690f25 --- /dev/null +++ b/src/EPPlusTest/FormulaParsing/CancelCalculationTests.cs @@ -0,0 +1,200 @@ +using Microsoft.VisualStudio.TestTools.UnitTesting; +using OfficeOpenXml; +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.IO; +using System.Linq; +using System.Text; +using System.Threading; + +namespace EPPlusTest.FormulaParsing +{ + [TestClass] + public class CancelCalculationTests + { + const int WaitTimeMs = 150; + + [TestMethod] + public void CancelCalculation() + { + using var package = CreateHeavyChain(chainLength: 1500, sheetCount: 3); + using var cts = new CancellationTokenSource(TimeSpan.FromMilliseconds(WaitTimeMs)); + + var sw = Stopwatch.StartNew(); + try + { + package.Workbook.Calculate(opt => + { + opt.CancellationToken = cts.Token; + }); + Assert.Fail("Expected OperationCanceledException was not thrown."); + } + catch (OperationCanceledException) + { + sw.Stop(); + Debug.WriteLine($"Calculation cancelled after {sw.Elapsed.TotalSeconds:F2} seconds."); + Assert.IsTrue(sw.Elapsed.TotalSeconds < 10, "Cancellation took too long."); + Assert.IsTrue(package.Workbook.IsCalculationInconsistent); + } + } + + [TestMethod] + public void Save_AfterCancelledCalculation_ThrowsInvalidOperationException() + { + using var package = CreateHeavyChain(chainLength: 1500, sheetCount: 3); + using var cts = new CancellationTokenSource(TimeSpan.FromMilliseconds(WaitTimeMs)); + using var outputStream = new MemoryStream(); + + try + { + package.Workbook.Calculate(opt => + { + opt.CancellationToken = cts.Token; + }); + } + catch (OperationCanceledException) { /* expected */ } + + Assert.ThrowsExactly(() => + { + package.SaveAs(outputStream); + }); + } + + [TestMethod] + public void CancelCalculation_AlreadyCancelledToken_ThrowsImmediately() + { + using var package = CreateHeavyChain(chainLength: 1500, sheetCount: 3); + using var cts = new CancellationTokenSource(); + cts.Cancel(); // Signal before calculate + + Assert.ThrowsExactly(() => + { + package.Workbook.Calculate(opt => opt.CancellationToken = cts.Token); + }); + Assert.IsTrue(package.Workbook.IsCalculationInconsistent); + } + + [TestMethod] + public void CancelCalculation_RecalculatePoisonedWorkbook_ThrowsInvalidOperationException() + { + using var package = CreateHeavyChain(chainLength: 1500, sheetCount: 3); + using var cts = new CancellationTokenSource(TimeSpan.FromMilliseconds(WaitTimeMs)); + + try + { + package.Workbook.Calculate(opt => opt.CancellationToken = cts.Token); + } + catch (OperationCanceledException) { /* expected */ } + + Assert.ThrowsExactly(() => + { + package.Workbook.Calculate(); // Should throw — workbook is poisoned + }); + } + + [TestMethod] + public void CancelCalculation_FromAnotherThread() + { + using var package = CreateHeavyChain(chainLength: 1500, sheetCount: 3); + using var cts = new CancellationTokenSource(); + Exception caughtException = null; + + var calcThread = new Thread(() => + { + try + { + package.Workbook.Calculate(opt => opt.CancellationToken = cts.Token); + } + catch (OperationCanceledException ex) + { + caughtException = ex; + } + }); + + calcThread.Start(); + Thread.Sleep(WaitTimeMs); // Let calculation run for a while + cts.Cancel(); // Cancel from this (main) thread + calcThread.Join(TimeSpan.FromSeconds(10)); // Wait for calc thread to finish + + Assert.IsFalse(calcThread.IsAlive, "Calculation thread did not terminate."); + Assert.IsInstanceOfType(caughtException); + Assert.IsTrue(package.Workbook.IsCalculationInconsistent); + } + + [TestMethod] + public void Calculate_WithToken_CompletesNormally_IsConsistent() + { + using var package = new ExcelPackage(); + var ws = package.Workbook.Worksheets.Add("Sheet1"); + ws.Cells["A1"].Formula = "1+1"; + + using var cts = new CancellationTokenSource(); + package.Workbook.Calculate(opt => opt.CancellationToken = cts.Token); + + Assert.AreEqual(2d, ws.Cells["A1"].Value); + Assert.IsFalse(package.Workbook.IsCalculationInconsistent); + } + + [TestMethod] + public void CancelCalculation_WithHeavyNamedRanges() + { + using var package = new ExcelPackage(); + var wb = package.Workbook; + var ws = wb.Worksheets.Add("Sheet1"); + + // Fill source data + for (int row = 1; row <= 1000; row++) + { + ws.Cells[row, 1].Value = row; + } + + // Create named ranges with heavy formulas referencing each other + for (int i = 0; i < 800; i++) + { + wb.Names.Add($"HeavyName{i}", ws.Cells["A1:A500"]); + ws.Cells[1, i + 2].Formula = $"SUMPRODUCT(HeavyName{i},HeavyName{i})"; + } + + using var cts = new CancellationTokenSource(TimeSpan.FromMilliseconds(WaitTimeMs)); + + Assert.ThrowsExactly(() => + { + wb.Calculate(opt => opt.CancellationToken = cts.Token); + }); + Assert.IsTrue(wb.IsCalculationInconsistent); + } + + [TestMethod] + public void CancelCalculation_WorksheetLevel() + { + using var package = CreateHeavyChain(chainLength: 1500, sheetCount: 1); + using var cts = new CancellationTokenSource(TimeSpan.FromMilliseconds(WaitTimeMs)); + + Assert.ThrowsExactly(() => + { + package.Workbook.Worksheets[0].Calculate(opt => opt.CancellationToken = cts.Token); + }); + Assert.IsTrue(package.Workbook.IsCalculationInconsistent); + } + + public static ExcelPackage CreateHeavyChain(int chainLength = 1_000, int sheetCount = 3) + { + var package = new ExcelPackage(); + + for (int s = 1; s <= sheetCount; s++) + { + var ws = package.Workbook.Worksheets.Add($"Sheet{s}"); + ws.Cells[1, 1].Value = 1; + + for (int row = 2; row <= chainLength; row++) + { + // SUMPRODUCT over a growing range — O(N²) total work + ws.Cells[row, 1].Formula = $"SUMPRODUCT(A$1:A{row - 1})+1"; + } + } + + return package; + } + } +} diff --git a/src/EPPlusTest/FormulaParsing/EntireColumnTests.cs b/src/EPPlusTest/FormulaParsing/EntireColumnTests.cs new file mode 100644 index 000000000..8fb3cead8 --- /dev/null +++ b/src/EPPlusTest/FormulaParsing/EntireColumnTests.cs @@ -0,0 +1,233 @@ +using Microsoft.VisualStudio.TestTools.UnitTesting; +using OfficeOpenXml; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; + +namespace EPPlusTest.FormulaParsing +{ + [TestClass] + public class EntireColumnTests + { + private ExcelPackage _package; + private ExcelWorkbook _workbook; + + [TestInitialize] + public void Initialize() + { + _package = new ExcelPackage(); + _workbook = _package.Workbook; + } + + [TestCleanup] + public void Cleanup() + { + _package.Dispose(); + } + + [TestMethod] + public void FullColumnRefPlusScalar_EmptyColumn_ShouldReturnOneInFirstAndLastRow() + { + // Arrange + var ws = _workbook.Worksheets.Add("Sheet1"); + // Column B is entirely empty/blank + ws.Cells["C1"].Formula = "B:B+1"; + + // Act + ws.Calculate(); + + // Assert - first row of spill result + Assert.AreEqual(1d, ws.Cells["C1"].Value, + "First row should be 0 + 1 = 1"); + var lastRow = ws.Dimension != null ? ws.Dimension.End.Row : 1; + Assert.AreEqual(ExcelPackage.MaxRows, ws.Dimension.End.Row, + "Spill should extend to the last row of the worksheet"); + Assert.AreEqual(1d, ws.Cells["C" + lastRow].Value, + "Last row of spill should be 0 + 1 = 1"); + } + + [TestMethod] + public void FullColumnRefPlusScalar_WithData_PhysicalRowsCalculatedAndVirtualRowsGetDefault() + { + // Arrange - B has data in rows 1-3, formula in C1 + var ws = _workbook.Worksheets.Add("Sheet1"); + ws.Cells["B1"].Value = 10d; + ws.Cells["B2"].Value = 20d; + ws.Cells["B3"].Value = 30d; + ws.Cells["C1"].Formula = "B:B+5"; + + // Act + ws.Calculate(); + + // Assert - physical rows get their actual calculated values + Assert.AreEqual(15d, ws.Cells["C1"].Value); + Assert.AreEqual(25d, ws.Cells["C2"].Value); + Assert.AreEqual(35d, ws.Cells["C3"].Value); + // Virtual rows beyond data: empty + 5 = 5 + Assert.AreEqual(5d, ws.Cells["C4"].Value, + "First virtual row should be 0 + 5 = 5"); + Assert.AreEqual(5d, ws.Cells["C" + ExcelPackage.MaxRows].Value, + "Last row should be 0 + 5 = 5"); + } + + [TestMethod] + public void FullColumnRefEqualsString_VirtualRowsShouldBeFalse() + { + // Arrange - comparison operator: A:A="Hello" + // Virtual rows are empty, empty != "Hello" => FALSE + var ws = _workbook.Worksheets.Add("Sheet1"); + ws.Cells["A1"].Value = "Hello"; + ws.Cells["A2"].Value = "World"; + ws.Cells["A3"].Value = "Hello"; + ws.Cells["B1"].Formula = "A:A=\"Hello\""; + + // Act + ws.Calculate(); + + // Assert - physical rows + Assert.AreEqual(true, ws.Cells["B1"].Value); + Assert.AreEqual(false, ws.Cells["B2"].Value); + Assert.AreEqual(true, ws.Cells["B3"].Value); + // Virtual rows: empty = "Hello" => FALSE + Assert.AreEqual(false, ws.Cells["B4"].Value, + "Virtual row: empty = \"Hello\" should be FALSE"); + Assert.AreEqual(false, ws.Cells["B" + ExcelPackage.MaxRows].Value, + "Last row should also be FALSE"); + } + + [TestMethod] + public void TwoFullColumnRefsMultiplied_VirtualRowsShouldBeZero() + { + // Arrange - (A:A=x) * (B:B=y) pattern used in MATCH criteria + var ws = _workbook.Worksheets.Add("Sheet1"); + ws.Cells["A1"].Value = "Alpha"; + ws.Cells["A2"].Value = "Beta"; + ws.Cells["B1"].Value = "X"; + ws.Cells["B2"].Value = "Y"; + ws.Cells["C1"].Value = 100d; + ws.Cells["C2"].Value = 200d; + + // MATCH(1, (A:A="Alpha")*(B:B="X"), 0) should find row 1 + ws.Cells["E1"].Formula = "MATCH(1,(A:A=\"Alpha\")*(B:B=\"X\"),0)"; + // INDEX to retrieve the value + ws.Cells["F1"].Formula = "INDEX(C:C,MATCH(1,(A:A=\"Alpha\")*(B:B=\"X\"),0))"; + + // Act + ws.Calculate(); + + // Assert + Assert.AreEqual(1, ws.Cells["E1"].Value, + "MATCH should find row 1"); + Assert.AreEqual(100d, ws.Cells["F1"].Value, + "INDEX should return 100 for Alpha+X"); + } + + [TestMethod] + public void CrossSheetFullColumnRef_IndexMatch_ShouldWork() + { + // Arrange - the original problem pattern from the design doc + var dataWs = _workbook.Worksheets.Add("Data"); + dataWs.Cells["A1"].Value = "Item1"; + dataWs.Cells["A2"].Value = "Item2"; + dataWs.Cells["A3"].Value = "Item1"; + dataWs.Cells["B1"].Value = "Day Shift"; + dataWs.Cells["B2"].Value = "Day Shift"; + dataWs.Cells["B3"].Value = "Night Shift"; + dataWs.Cells["C1"].Value = 50d; + dataWs.Cells["C2"].Value = 75d; + dataWs.Cells["C3"].Value = 90d; + + var ws = _workbook.Worksheets.Add("Formulas"); + ws.Cells["A1"].Value = "Item1"; + ws.Cells["B1"].Formula = + "IFERROR(INDEX(Data!$C:$C,MATCH(1,(Data!$A:$A=A1)*(Data!$B:$B=\"Day Shift\"),0)),\"-\")"; + // Also test a lookup that should NOT match + ws.Cells["A2"].Value = "NoMatch"; + ws.Cells["B2"].Formula = + "IFERROR(INDEX(Data!$C:$C,MATCH(1,(Data!$A:$A=A2)*(Data!$B:$B=\"Day Shift\"),0)),\"-\")"; + + // Act + _workbook.Calculate(); + + // Assert + Assert.AreEqual(50d, ws.Cells["B1"].Value, + "Should find Item1 + Day Shift => 50"); + Assert.AreEqual("-", ws.Cells["B2"].Value, + "NoMatch should fall through to IFERROR => \"-\""); + } + [TestMethod] + public void ScalarDivideFullColumnRef_VirtualRowsShouldBeDivByZeroError() + { + // Arrange - 1/B:B where B has data in rows 1-2 + // Virtual default: 1 / null = 1 / 0 = #DIV/0! + var ws = _workbook.Worksheets.Add("Sheet1"); + ws.Cells["B1"].Value = 2d; + ws.Cells["B2"].Value = 4d; + ws.Cells["C1"].Formula = "1/B:B"; + + // Act + ws.Calculate(); + + // Assert - physical rows + Assert.AreEqual(0.5d, ws.Cells["C1"].Value); + Assert.AreEqual(0.25d, ws.Cells["C2"].Value); + // Virtual rows: 1 / 0 => #DIV/0! + var virtualVal = ws.Cells["C3"].Value; + Assert.IsInstanceOfType(virtualVal, typeof(ExcelErrorValue), + "Virtual row: 1/0 should produce an error"); + Assert.AreEqual(eErrorType.Div0, ((ExcelErrorValue)virtualVal).Type, + "Error should be #DIV/0!"); + // Last row should also be #DIV/0! + var lastVal = ws.Cells["C" + ExcelPackage.MaxRows].Value; + Assert.IsInstanceOfType(lastVal, typeof(ExcelErrorValue), + "Last row should also be #DIV/0!"); + } + + [TestMethod] + public void FullColumnRefConcat_VirtualRowsShouldConcatEmpty() + { + // Arrange - A:A&"!" via the Concat operator + // Virtual default: "" & "!" = "!" + var ws = _workbook.Worksheets.Add("Sheet1"); + ws.Cells["A1"].Value = "Hello"; + ws.Cells["A2"].Value = "World"; + ws.Cells["B1"].Formula = "A:A&\"!\""; + + // Act + ws.Calculate(); + + // Assert - physical rows + Assert.AreEqual("Hello!", ws.Cells["B1"].Value); + Assert.AreEqual("World!", ws.Cells["B2"].Value); + // Virtual rows: "" & "!" = "!" + Assert.AreEqual("!", ws.Cells["B3"].Value, + "Virtual row: empty & \"!\" should be \"!\""); + Assert.AreEqual("!", ws.Cells["B" + ExcelPackage.MaxRows].Value, + "Last row should also be \"!\""); + } + + [TestMethod] + public void NegateFullColumnRef_VirtualRowsShouldBeZero() + { + // Arrange - negation: -A:A + // Virtual default: -(null) = -(0) = 0 + var ws = _workbook.Worksheets.Add("Sheet1"); + ws.Cells["A1"].Value = 5d; + ws.Cells["A2"].Value = -3d; + ws.Cells["B1"].Formula = "-A:A"; + + // Act + ws.Calculate(); + + // Assert - physical rows + Assert.AreEqual(-5d, ws.Cells["B1"].Value); + Assert.AreEqual(3d, ws.Cells["B2"].Value); + // Virtual rows: -(0) = 0 + Assert.AreEqual(0d, ws.Cells["B3"].Value, + "Virtual row: -0 should be 0"); + Assert.AreEqual(0d, ws.Cells["B" + ExcelPackage.MaxRows].Value, + "Last row should also be 0"); + } + } +} \ No newline at end of file diff --git a/src/EPPlusTest/FormulaParsing/Excel/Functions/MathFunctions/AverageIfsTests.cs b/src/EPPlusTest/FormulaParsing/Excel/Functions/MathFunctions/AverageIfsTests.cs index 281d3aa60..a3022fffa 100644 --- a/src/EPPlusTest/FormulaParsing/Excel/Functions/MathFunctions/AverageIfsTests.cs +++ b/src/EPPlusTest/FormulaParsing/Excel/Functions/MathFunctions/AverageIfsTests.cs @@ -17,15 +17,15 @@ public void AverageIfsShouldNotCountNumericStringsAsNumbers() using (var package = new ExcelPackage()) { var sheet = package.Workbook.Worksheets.Add("test"); - sheet.Cells[1, 1].Value = 3; - sheet.Cells[2, 1].Value = 4; - sheet.Cells[3, 1].Value = 5; - sheet.Cells[1, 2].Value = 1; - sheet.Cells[2, 2].Value = "2"; - sheet.Cells[3, 2].Value = 3; - sheet.Cells[1, 3].Value = 2; - sheet.Cells[2, 3].Value = 1; - sheet.Cells[3, 3].Value = "4"; + sheet.Cells["A1"].Value = 3; + sheet.Cells["A2"].Value = 4; + sheet.Cells["A3"].Value = 5; + sheet.Cells["B1"].Value = 1; + sheet.Cells["B2"].Value = "2"; + sheet.Cells["B3"].Value = 3; + sheet.Cells["C1"].Value = 2; + sheet.Cells["C2"].Value = 1; + sheet.Cells["C3"].Value = "4"; sheet.Cells[4, 1].Formula = "AVERAGEIFS(A1:A3,B1:B3,\">0\",C1:C3,\">1\")"; sheet.Calculate(); diff --git a/src/EPPlusTest/FormulaParsing/Excel/Functions/MathFunctions/CriteriaRangeFunctionsTests.cs b/src/EPPlusTest/FormulaParsing/Excel/Functions/MathFunctions/CriteriaRangeFunctionsTests.cs index 2df1f3966..95e8b40a5 100644 --- a/src/EPPlusTest/FormulaParsing/Excel/Functions/MathFunctions/CriteriaRangeFunctionsTests.cs +++ b/src/EPPlusTest/FormulaParsing/Excel/Functions/MathFunctions/CriteriaRangeFunctionsTests.cs @@ -36,5 +36,49 @@ public void SumAndAverageIfFunctionsShouldNotReturnCircularReferenceIfCriteriaIs Assert.AreEqual(10.5D, ws.Cells["C7"].Value); SaveAndCleanup(p); } + + + [TestMethod] + public void AverageIfsShouldNotCacheWhenValueRangeHasFormulas() + { + using var p = new ExcelPackage(); + var ws = p.Workbook.Worksheets.Add("Sheet1"); + ws.Cells["A1"].Value = "Fruit"; + ws.Cells["B1"].Value = "Employee"; + ws.Cells["A2:A3"].Value = "Apples"; + ws.Cells["A4:A5"].Value = "Artichokes"; + ws.Cells["A6:A7"].Value = "Bananas"; + ws.Cells["A8:A9"].Value = "Carrots"; + ws.Cells["B2,B4,B8"].Value = "Mats"; + ws.Cells["B3,B5,B9"].Value = "Jan"; + ws.Cells["B6,B7"].Value = "Ossian"; // Both B6 and B7 are Ossian + + // C2, C3 have values + ws.Cells["C2"].Value = 10D; + ws.Cells["C3"].Value = 11D; + + // C4 and C5 have formulas that reference C2:C9 (circular reference scenario) + ws.Cells["C4"].Formula = "AVERAGEIFS(C2:C9,A2:A9,\"=B*\",B2:B9,\"Ossian\")"; + ws.Cells["C5"].Formula = "AVERAGEIFS(C2:C9,A2:A9,\"=A*\",B2:B9,\"Mats\")"; + + ws.Cells["C6"].Value = 12D; + ws.Cells["C7"].Value = 13D; + ws.Cells["C8"].Value = 14D; + ws.Cells["C9"].Value = 15D; + + ws.Calculate(); + + // C4 = AVERAGEIFS(C2:C9, A2:A9, "=B*", B2:B9, "Ossian") + // Matches: A6="Bananas" (B*), B6="Ossian" -> C6=12 + // A7="Bananas" (B*), B7="Ossian" -> C7=13 + // Average = (12+13)/2 = 12.5 + Assert.AreEqual(12.5D, ws.Cells["C4"].Value, "C4 should be 12.5"); + + // C5 = AVERAGEIFS(C2:C9, A2:A9, "=A*", B2:B9, "Mats") + // Matches: A2="Apples" (A*), B2="Mats" -> C2=10 + // A4="Artichokes" (A*), B4="Mats" -> C4=12.5 (calculated earlier!) + // Average = (10+12.5)/2 = 11.25 + Assert.AreEqual(11.25D, ws.Cells["C5"].Value, "C5 should be 11.25 (including calculated C4 value)"); + } } } diff --git a/src/EPPlusTest/FormulaParsing/Excel/Functions/RangeCriteriaCacheTests.cs b/src/EPPlusTest/FormulaParsing/Excel/Functions/RangeCriteriaCacheTests.cs new file mode 100644 index 000000000..b28c5f088 --- /dev/null +++ b/src/EPPlusTest/FormulaParsing/Excel/Functions/RangeCriteriaCacheTests.cs @@ -0,0 +1,234 @@ +using Microsoft.VisualStudio.TestTools.UnitTesting; +using OfficeOpenXml; +using OfficeOpenXml.FormulaParsing.Excel.Functions.MathFunctions; +using OfficeOpenXml.FormulaParsing.LexicalAnalysis; +using System.Collections.Generic; + +namespace EPPlusTest.FormulaParsing.Excel.Functions +{ + [TestClass] + public class RangeCriteriaCacheTests + { + private ExcelPackage _package; + private RangeCriteriaCache _cache; + + [TestInitialize] + public void Setup() + { + _package = new ExcelPackage(); + _package.Settings.CalculationCacheSettings.MaxFlattenedRanges = RangeCriteriaCache.DEFAULT_MAX_FLATTENED_RANGES; + _package.Settings.CalculationCacheSettings.MaxMatchIndexes = RangeCriteriaCache.DEFAULT_MAX_MATCH_INDEXES; + _package.Workbook.Worksheets.Add("Sheet1"); + } + + [TestCleanup] + public void Cleanup() + { + _package?.Dispose(); + } + + [TestMethod] + public void FlattenedRange_ShouldCacheAndRetrieve() + { + _cache = new RangeCriteriaCache(_package); + var address = new FormulaRangeAddress { WorksheetIx = 0, FromRow = 1, FromCol = 1, ToRow = 10, ToCol = 1 }; + var data = new List { 1, 2, 3, 4, 5 }; + + _cache.SetFlattenedRange(address, data); + var retrieved = _cache.GetFlattenedRange(address); + + Assert.IsNotNull(retrieved); + Assert.AreEqual(5, retrieved.Count); + Assert.AreEqual(1, retrieved[0]); + } + + [TestMethod] + public void FlattenedRange_FIFO_ShouldEvictOldest() + { + _package.Settings.CalculationCacheSettings.MaxFlattenedRanges = 3; + _cache = new RangeCriteriaCache(_package); + + var address1 = new FormulaRangeAddress { WorksheetIx = 0, FromRow = 1, FromCol = 1, ToRow = 10, ToCol = 1 }; + var address2 = new FormulaRangeAddress { WorksheetIx = 0, FromRow = 11, FromCol = 1, ToRow = 20, ToCol = 1 }; + var address3 = new FormulaRangeAddress { WorksheetIx = 0, FromRow = 21, FromCol = 1, ToRow = 30, ToCol = 1 }; + var address4 = new FormulaRangeAddress { WorksheetIx = 0, FromRow = 31, FromCol = 1, ToRow = 40, ToCol = 1 }; + + var data1 = new List { 1 }; + var data2 = new List { 2 }; + var data3 = new List { 3 }; + var data4 = new List { 4 }; + + _cache.SetFlattenedRange(address1, data1); + _cache.SetFlattenedRange(address2, data2); + _cache.SetFlattenedRange(address3, data3); + + // Cache is now full (3/3) + Assert.IsNotNull(_cache.GetFlattenedRange(address1), "Address1 should be in cache"); + Assert.IsNotNull(_cache.GetFlattenedRange(address2), "Address2 should be in cache"); + Assert.IsNotNull(_cache.GetFlattenedRange(address3), "Address3 should be in cache"); + + // Adding 4th item should evict the oldest (address1) + _cache.SetFlattenedRange(address4, data4); + + Assert.IsNull(_cache.GetFlattenedRange(address1), "Address1 should be evicted (oldest)"); + Assert.IsNotNull(_cache.GetFlattenedRange(address2), "Address2 should still be in cache"); + Assert.IsNotNull(_cache.GetFlattenedRange(address3), "Address3 should still be in cache"); + Assert.IsNotNull(_cache.GetFlattenedRange(address4), "Address4 should be in cache"); + } + + [TestMethod] + public void MatchIndexes_ShouldCacheAndRetrieve() + { + _cache = new RangeCriteriaCache(_package); + var address = new FormulaRangeAddress { WorksheetIx = 0, FromRow = 1, FromCol = 1, ToRow = 10, ToCol = 1 }; + var criteria = "test"; + var indexes = new List { 1, 3, 5, 7 }; + + _cache.SetMatchIndexes(address, criteria, indexes); + var retrieved = _cache.GetMatchIndexes(address, criteria); + + Assert.IsNotNull(retrieved); + Assert.AreEqual(4, retrieved.Count); + Assert.AreEqual(1, retrieved[0]); + Assert.AreEqual(7, retrieved[3]); + } + + [TestMethod] + public void MatchIndexes_FIFO_ShouldEvictOldest() + { + _package.Settings.CalculationCacheSettings.MaxMatchIndexes = 3; + _cache = new RangeCriteriaCache(_package); + + var address = new FormulaRangeAddress { WorksheetIx = 0, FromRow = 1, FromCol = 1, ToRow = 10, ToCol = 1 }; + var criteria1 = "A"; + var criteria2 = "B"; + var criteria3 = "C"; + var criteria4 = "D"; + + var indexes1 = new List { 1 }; + var indexes2 = new List { 2 }; + var indexes3 = new List { 3 }; + var indexes4 = new List { 4 }; + + _cache.SetMatchIndexes(address, criteria1, indexes1); + _cache.SetMatchIndexes(address, criteria2, indexes2); + _cache.SetMatchIndexes(address, criteria3, indexes3); + + // Cache is now full (3/3) + Assert.IsNotNull(_cache.GetMatchIndexes(address, criteria1), "Criteria1 should be in cache"); + Assert.IsNotNull(_cache.GetMatchIndexes(address, criteria2), "Criteria2 should be in cache"); + Assert.IsNotNull(_cache.GetMatchIndexes(address, criteria3), "Criteria3 should be in cache"); + + // Adding 4th item should evict the oldest (criteria1) + _cache.SetMatchIndexes(address, criteria4, indexes4); + + Assert.IsNull(_cache.GetMatchIndexes(address, criteria1), "Criteria1 should be evicted (oldest)"); + Assert.IsNotNull(_cache.GetMatchIndexes(address, criteria2), "Criteria2 should still be in cache"); + Assert.IsNotNull(_cache.GetMatchIndexes(address, criteria3), "Criteria3 should still be in cache"); + Assert.IsNotNull(_cache.GetMatchIndexes(address, criteria4), "Criteria4 should be in cache"); + } + + [TestMethod] + public void MatchIndexes_WithFormulas_ShouldNotCache() + { + var ws = _package.Workbook.Worksheets[0]; + ws.Cells["A1"].Formula = "=1+1"; + ws.Cells["A2"].Value = 2; + + _cache = new RangeCriteriaCache(_package); + var address = new FormulaRangeAddress { WorksheetIx = 0, FromRow = 1, FromCol = 1, ToRow = 2, ToCol = 1 }; + var criteria = "test"; + var indexes = new List { 1 }; + + _cache.SetMatchIndexes(address, criteria, indexes); + + // Should not cache because range has formulas + var retrieved = _cache.GetMatchIndexes(address, criteria); + Assert.IsNull(retrieved, "Should not cache ranges with formulas"); + } + + [TestMethod] + public void MatchIndexes_DifferentCriteria_SamRange_ShouldCacheSeparately() + { + _cache = new RangeCriteriaCache(_package); + var address = new FormulaRangeAddress { WorksheetIx = 0, FromRow = 1, FromCol = 1, ToRow = 10, ToCol = 1 }; + + var indexes1 = new List { 1, 2 }; + var indexes2 = new List { 3, 4 }; + + _cache.SetMatchIndexes(address, "criteriaA", indexes1); + _cache.SetMatchIndexes(address, "criteriaB", indexes2); + + var retrieved1 = _cache.GetMatchIndexes(address, "criteriaA"); + var retrieved2 = _cache.GetMatchIndexes(address, "criteriaB"); + + Assert.IsNotNull(retrieved1); + Assert.IsNotNull(retrieved2); + Assert.AreEqual(2, retrieved1.Count); + Assert.AreEqual(2, retrieved2.Count); + Assert.AreEqual(1, retrieved1[0]); + Assert.AreEqual(3, retrieved2[0]); + } + + [TestMethod] + public void Clear_ShouldRemoveAllCachedData() + { + _cache = new RangeCriteriaCache(_package); + var address = new FormulaRangeAddress { WorksheetIx = 0, FromRow = 1, FromCol = 1, ToRow = 10, ToCol = 1 }; + + _cache.SetFlattenedRange(address, new List { 1, 2, 3 }); + _cache.SetMatchIndexes(address, "test", new List { 1, 2 }); + + Assert.IsNotNull(_cache.GetFlattenedRange(address)); + Assert.IsNotNull(_cache.GetMatchIndexes(address, "test")); + + _cache.Clear(); + + Assert.IsNull(_cache.GetFlattenedRange(address)); + Assert.IsNull(_cache.GetMatchIndexes(address, "test")); + } + + [TestMethod] + public void FlattenedRange_UpdateExisting_ShouldNotDuplicate() + { + _package.Settings.CalculationCacheSettings.MaxFlattenedRanges = 2; + _cache = new RangeCriteriaCache(_package); + var address = new FormulaRangeAddress { WorksheetIx = 0, FromRow = 1, FromCol = 1, ToRow = 10, ToCol = 1 }; + + var data1 = new List { 1, 2, 3 }; + var data2 = new List { 4, 5, 6 }; + + // Add same address twice with different data + _cache.SetFlattenedRange(address, data1); + _cache.SetFlattenedRange(address, data2); + + var retrieved = _cache.GetFlattenedRange(address); + + // Should have updated value, not duplicated + Assert.IsNotNull(retrieved); + Assert.AreEqual(4, retrieved[0], "Should have updated data"); + } + + [TestMethod] + public void MatchIndexes_UpdateExisting_ShouldNotDuplicate() + { + _package.Settings.CalculationCacheSettings.MaxMatchIndexes = 2; + _cache = new RangeCriteriaCache(_package); + var address = new FormulaRangeAddress { WorksheetIx = 0, FromRow = 1, FromCol = 1, ToRow = 10, ToCol = 1 }; + var criteria = "test"; + + var indexes1 = new List { 1, 2 }; + var indexes2 = new List { 3, 4 }; + + // Add same address+criteria twice with different data + _cache.SetMatchIndexes(address, criteria, indexes1); + _cache.SetMatchIndexes(address, criteria, indexes2); + + var retrieved = _cache.GetMatchIndexes(address, criteria); + + // Should have updated value, not duplicated + Assert.IsNotNull(retrieved); + Assert.AreEqual(3, retrieved[0], "Should have updated indexes"); + } + } +} \ No newline at end of file diff --git a/src/EPPlusTest/FormulaParsing/Excel/Functions/RefAndLookup/GetPivotData/GetPivotDataTests_CaluclatedFields2.cs b/src/EPPlusTest/FormulaParsing/Excel/Functions/RefAndLookup/GetPivotData/GetPivotDataTests_CaluclatedFields2.cs new file mode 100644 index 000000000..932ac57f8 --- /dev/null +++ b/src/EPPlusTest/FormulaParsing/Excel/Functions/RefAndLookup/GetPivotData/GetPivotDataTests_CaluclatedFields2.cs @@ -0,0 +1,335 @@ +using Microsoft.VisualStudio.TestTools.UnitTesting; +using OfficeOpenXml; +using OfficeOpenXml.Table.PivotTable; +using OfficeOpenXml.Table.PivotTable.Calculation; +using System.Collections.Generic; + +namespace EPPlus.Core.Tests.Table.PivotTable +{ + [TestClass] + public class GetPivotDataTests_CalculatedFields2 + { + private ExcelPackage _package; + private ExcelWorksheet _dataWs; + + [TestInitialize] + public void Initialize() + { + _package = new ExcelPackage(); + _dataWs = _package.Workbook.Worksheets.Add("Data"); + + // Headers + _dataWs.Cells["A1"].Value = "Department"; + _dataWs.Cells["B1"].Value = "Basic Pay"; + _dataWs.Cells["C1"].Value = "Overtime"; + _dataWs.Cells["D1"].Value = "Bonus"; + + // Sales department + _dataWs.Cells["A2"].Value = "Sales"; + _dataWs.Cells["B2"].Value = 5000d; + _dataWs.Cells["C2"].Value = 500d; + _dataWs.Cells["D2"].Value = 1000d; + + _dataWs.Cells["A3"].Value = "Sales"; + _dataWs.Cells["B3"].Value = 6000d; + _dataWs.Cells["C3"].Value = 600d; + _dataWs.Cells["D3"].Value = 1200d; + + // IT department + _dataWs.Cells["A4"].Value = "IT"; + _dataWs.Cells["B4"].Value = 7000d; + _dataWs.Cells["C4"].Value = 300d; + _dataWs.Cells["D4"].Value = 800d; + + _dataWs.Cells["A5"].Value = "IT"; + _dataWs.Cells["B5"].Value = 7500d; + _dataWs.Cells["C5"].Value = 400d; + _dataWs.Cells["D5"].Value = 900d; + } + + [TestCleanup] + public void Cleanup() + { + _package?.Dispose(); + } + + [TestMethod] + public void GetPivotData_CalculatedField_ShouldReturnCorrectValue() + { + // Arrange + var ws = _package.Workbook.Worksheets.Add("Pivot"); + var pt = ws.PivotTables.Add(ws.Cells["A1"], _dataWs.Cells["A1:D5"], "TestPivot"); + + pt.RowFields.Add(pt.Fields["Department"]); + + pt.Fields.AddCalculatedField("Total Comp", "'Basic Pay'+'Overtime'+'Bonus'"); + var df = pt.DataFields.Add(pt.Fields["Total Comp"]); + df.Function = DataFieldFunctions.Sum; + df.Name = "Sum of Total Comp"; + + // Act + pt.Calculate(refreshCache: true); + + var criteria = new List + { + new PivotDataFieldItemSelection("Department", "Sales") + }; + + var result = pt.GetPivotData("Sum of Total Comp", criteria); + + // Assert + Assert.IsNotNull(result, "Result should not be null"); + Assert.IsFalse(result is ExcelErrorValue, + $"GetPivotData should not return #REF! error, got: {result}"); + + // Sales: (5000+500+1000) + (6000+600+1200) = 14300 + Assert.AreEqual(14300d, (double)result, 0.001, + "Calculated field value for Sales should be 14300"); + } + + [TestMethod] + public void GetPivotData_CalculatedField_GrandTotal_ShouldWork() + { + // Arrange + var ws = _package.Workbook.Worksheets.Add("Pivot"); + var pt = ws.PivotTables.Add(ws.Cells["A1"], _dataWs.Cells["A1:D5"], "TestPivot"); + + pt.RowFields.Add(pt.Fields["Department"]); + pt.Fields.AddCalculatedField("All Pay", "'Basic Pay'+'Overtime'+'Bonus'"); + var df = pt.DataFields.Add(pt.Fields["All Pay"]); + df.Function = DataFieldFunctions.Sum; + df.Name = "Sum of All Pay"; + + // Act + pt.Calculate(refreshCache: true); + var grandTotal = pt.GetPivotData("Sum of All Pay"); + + // Assert + Assert.IsNotNull(grandTotal); + Assert.IsFalse(grandTotal is ExcelErrorValue, + string.Format("Grand total should not return error, got: {0}", grandTotal)); + + // Total: Sales(14300) + IT(16900) = 31200 + Assert.AreEqual(31200d, (double)grandTotal, 0.001); + } + + [TestMethod] + public void GetPivotData_MultipleCalculatedFields_ShouldWorkIndependently() + { + // Arrange + var ws = _package.Workbook.Worksheets.Add("Pivot"); + var pt = ws.PivotTables.Add(ws.Cells["A1"], _dataWs.Cells["A1:D5"], "TestPivot"); + + pt.RowFields.Add(pt.Fields["Department"]); + + pt.Fields.AddCalculatedField("Base Plus OT", "'Basic Pay'+'Overtime'"); + pt.Fields.AddCalculatedField("Total Comp", "'Basic Pay'+'Overtime'+'Bonus'"); + + var df1 = pt.DataFields.Add(pt.Fields["Base Plus OT"]); + df1.Function = DataFieldFunctions.Sum; + df1.Name = "Sum of Base Plus OT"; + + var df2 = pt.DataFields.Add(pt.Fields["Total Comp"]); + df2.Function = DataFieldFunctions.Sum; + df2.Name = "Sum of Total Comp"; + + // Act + pt.Calculate(refreshCache: true); + + var criteriaIT = new List + { + new PivotDataFieldItemSelection("Department", "IT") + }; + + var result1 = pt.GetPivotData("Sum of Base Plus OT", criteriaIT); + var result2 = pt.GetPivotData("Sum of Total Comp", criteriaIT); + + // Assert + Assert.IsFalse(result1 is ExcelErrorValue); + Assert.IsFalse(result2 is ExcelErrorValue); + + // IT Base + OT: (7000+300) + (7500+400) = 15200 + Assert.AreEqual(15200d, (double)result1, 0.001); + + // IT Total: (7000+300+800) + (7500+400+900) = 16900 + Assert.AreEqual(16900d, (double)result2, 0.001); + } + + [TestMethod] + public void GetPivotData_CalculatedField_CalculatedItems_ShouldBePopulated() + { + // Arrange + var ws = _package.Workbook.Worksheets.Add("Pivot"); + var pt = ws.PivotTables.Add(ws.Cells["A1"], _dataWs.Cells["A1:D5"], "TestPivot"); + + pt.RowFields.Add(pt.Fields["Department"]); + pt.Fields.AddCalculatedField("Sum Total", "'Basic Pay'+'Overtime'"); + var df = pt.DataFields.Add(pt.Fields["Sum Total"]); + df.Function = DataFieldFunctions.Sum; + df.Name = "Sum of Sum Total"; + + // Act + pt.Calculate(refreshCache: true); + + // Assert + Assert.IsTrue(pt.IsCalculated, "Pivot table should be marked as calculated"); + Assert.IsNotNull(pt.CalculatedItems, "CalculatedItems should not be null"); + + var dfIndex = pt.DataFields.IndexOf(df); + Assert.IsTrue(dfIndex >= 0, "DataField should be in collection"); + Assert.IsTrue(dfIndex < pt.CalculatedItems.Count, + "Should have CalculatedItems entry for this index"); + + var store = pt.CalculatedItems[dfIndex]; + Assert.IsNotNull(store, "Store should not be null"); + Assert.IsTrue(store.Count > 0, + "CalculatedItems store should be populated (this was empty before the fix)"); + } + + [TestMethod] + public void GetPivotData_CalculatedFieldMixedWithRegular_ShouldWork() + { + // Arrange + var ws = _package.Workbook.Worksheets.Add("Pivot"); + var pt = ws.PivotTables.Add(ws.Cells["A1"], _dataWs.Cells["A1:D5"], "TestPivot"); + + pt.RowFields.Add(pt.Fields["Department"]); + + // Add regular data field first + var dfBasic = pt.DataFields.Add(pt.Fields["Basic Pay"]); + dfBasic.Function = DataFieldFunctions.Sum; + dfBasic.Name = "Sum of Basic Pay"; + + // Add calculated field + pt.Fields.AddCalculatedField("Total", "'Basic Pay'+'Overtime'+'Bonus'"); + var dfCalc = pt.DataFields.Add(pt.Fields["Total"]); + dfCalc.Function = DataFieldFunctions.Sum; + dfCalc.Name = "Sum of Total"; + + // Act + pt.Calculate(refreshCache: true); + + var criteriaSales = new List + { + new PivotDataFieldItemSelection("Department", "Sales") + }; + + var basicResult = pt.GetPivotData("Sum of Basic Pay", criteriaSales); + var calcResult = pt.GetPivotData("Sum of Total", criteriaSales); + + // Assert + Assert.IsFalse(basicResult is ExcelErrorValue, "Regular field should work"); + Assert.IsFalse(calcResult is ExcelErrorValue, "Calculated field should work"); + + Assert.AreEqual(11000d, (double)basicResult, 0.001); // 5000 + 6000 + Assert.AreEqual(14300d, (double)calcResult, 0.001); + } + + [TestMethod] + public void GetPivotData_CalculatedFieldWithComplexFormula_ShouldWork() + { + // Arrange + var ws = _package.Workbook.Worksheets.Add("Pivot"); + var pt = ws.PivotTables.Add(ws.Cells["A1"], _dataWs.Cells["A1:D5"], "TestPivot"); + + pt.RowFields.Add(pt.Fields["Department"]); + + // Complex formula with multiple operations + pt.Fields.AddCalculatedField("Complex Calc", + "'Basic Pay' * 2 + 'Overtime' - 'Bonus'"); + var df = pt.DataFields.Add(pt.Fields["Complex Calc"]); + df.Function = DataFieldFunctions.Sum; + df.Name = "Sum of Complex Calc"; + + // Act + pt.Calculate(refreshCache: true); + + var criteriaIT = new List + { + new PivotDataFieldItemSelection("Department", "IT") + }; + + var result = pt.GetPivotData("Sum of Complex Calc", criteriaIT); + + // Assert + Assert.IsFalse(result is ExcelErrorValue); + + // IT: (7000*2+300-800) + (7500*2+400-900) + // = (14000+300-800) + (15000+400-900) + // = 13500 + 14500 = 28000 + Assert.AreEqual(28000d, (double)result, 0.001); + } + + [TestMethod] + public void GetPivotData_CalculatedFieldOnlyWithFormula_NoRegularFields() + { + // Arrange + var ws = _package.Workbook.Worksheets.Add("Pivot"); + var pt = ws.PivotTables.Add(ws.Cells["A1"], _dataWs.Cells["A1:D5"], "TestPivot"); + + pt.RowFields.Add(pt.Fields["Department"]); + + // Only add calculated field, no regular data fields + pt.Fields.AddCalculatedField("Only Calc", "'Basic Pay'+'Overtime'"); + var df = pt.DataFields.Add(pt.Fields["Only Calc"]); + df.Function = DataFieldFunctions.Sum; + df.Name = "Sum of Only Calc"; + + // Act + pt.Calculate(refreshCache: true); + + var criteriaIT = new List + { + new PivotDataFieldItemSelection("Department", "IT") + }; + + var result = pt.GetPivotData("Sum of Only Calc", criteriaIT); + + // Assert + Assert.IsNotNull(result); + Assert.IsFalse(result is ExcelErrorValue, + "Should work even when pivot has only calculated fields"); + + // IT: (7000+300) + (7500+400) = 15200 + Assert.AreEqual(15200d, (double)result, 0.001); + } + + [TestMethod] + public void GetPivotData_CalculatedField_DifferentDepartments() + { + // Arrange + var ws = _package.Workbook.Worksheets.Add("Pivot"); + var pt = ws.PivotTables.Add(ws.Cells["A1"], _dataWs.Cells["A1:D5"], "TestPivot"); + + pt.RowFields.Add(pt.Fields["Department"]); + pt.Fields.AddCalculatedField("Total Pay", "'Basic Pay'+'Overtime'+'Bonus'"); + var df = pt.DataFields.Add(pt.Fields["Total Pay"]); + df.Function = DataFieldFunctions.Sum; + df.Name = "Sum of Total Pay"; + + // Act + pt.Calculate(refreshCache: true); + + var criteriaSales = new List + { + new PivotDataFieldItemSelection("Department", "Sales") + }; + + var criteriaIT = new List + { + new PivotDataFieldItemSelection("Department", "IT") + }; + + var salesResult = pt.GetPivotData("Sum of Total Pay", criteriaSales); + var itResult = pt.GetPivotData("Sum of Total Pay", criteriaIT); + + // Assert + Assert.IsFalse(salesResult is ExcelErrorValue); + Assert.IsFalse(itResult is ExcelErrorValue); + + // Sales: 14300, IT: 16900 + Assert.AreEqual(14300d, (double)salesResult, 0.001); + Assert.AreEqual(16900d, (double)itResult, 0.001); + } + } +} \ No newline at end of file diff --git a/src/EPPlusTest/FormulaParsing/Excel/OperatorsTests/RangeOperationsOperatorTests.cs b/src/EPPlusTest/FormulaParsing/Excel/OperatorsTests/RangeOperationsOperatorTests.cs index 740c5dc98..9deb10210 100644 --- a/src/EPPlusTest/FormulaParsing/Excel/OperatorsTests/RangeOperationsOperatorTests.cs +++ b/src/EPPlusTest/FormulaParsing/Excel/OperatorsTests/RangeOperationsOperatorTests.cs @@ -529,5 +529,18 @@ public void ShouldCalculateRangesDoubleWithExpOperator() Assert.AreEqual(1000d, range.GetValue(0, 0)); Assert.AreEqual(32d, range.GetValue(1, 0)); } + + [TestMethod] + public void ShouldHandleDoubleMinus() + { + using var package = new ExcelPackage(); + var sheet = package.Workbook.Worksheets.Add("Test"); + sheet.Cells["A1"].Formula = "TRUE()"; + sheet.Cells["A2"].Formula = "FALSE()"; + sheet.Cells["B1"].Formula = "--A1:A2"; + sheet.Calculate(); + Assert.AreEqual(1d, sheet.Cells["B1"].Value); + Assert.AreEqual(0d, sheet.Cells["B2"].Value); + } } } diff --git a/src/EPPlusTest/InCellImages/InCellImagesCacheTests.cs b/src/EPPlusTest/InCellImages/InCellImagesCacheTests.cs index 1d8568ada..dd0309019 100644 --- a/src/EPPlusTest/InCellImages/InCellImagesCacheTests.cs +++ b/src/EPPlusTest/InCellImages/InCellImagesCacheTests.cs @@ -1,10 +1,12 @@ -using Microsoft.VisualStudio.TestTools.UnitTesting; +using EPPlusTest.Properties; +using Microsoft.VisualStudio.TestTools.UnitTesting; +using OfficeOpenXml; using System; using System.Collections.Generic; +using System.IO; using System.Linq; using System.Text; -using OfficeOpenXml; -using EPPlusTest.Properties; +using OfficeOpenXml.CellPictures; namespace EPPlusTest.InCellImages { @@ -56,6 +58,59 @@ public void ShouldNotRemoveRichDataWhenMoreReferencesExists() SaveWorkbook("InCellPicturesReuseCache3.xlsx", package); } + [TestMethod] + public void ShouldNotRemoveRichDataWhenMoreReferencesExistsWhenReading() + { + var ms = new MemoryStream(); + + using (var package = new ExcelPackage()) + { + var sheet = package.Workbook.Worksheets.Add("Sheet 1"); + sheet.Cells["A1"].Picture.Set(Resources.Png2ByteArray); + sheet.Cells["A2"].Picture.Set(Resources.Png2ByteArray); + var vm1 = sheet._metadataStore.GetValue(1, 1); + var vm2 = sheet._metadataStore.GetValue(2, 1); + Assert.AreEqual(vm1, vm2); + Assert.AreEqual(1, package.Workbook.RichData.Db.Values.Count()); + package.SaveAs(ms); + } + ms.Position = 0; + ms.Seek(0, SeekOrigin.Begin); + + using (var package = new ExcelPackage(ms)) + { + var ws = package.Workbook.Worksheets[0]; + + var pic = ws.Cells[1, 1].Value as ExcelCellPicture; + + //Verify that the picture has been read into refs correctly + PictureCacheKey key = null; + if (pic != null) + { + if (pic.PictureType == ExcelCellPictureTypes.LocalImage) + { + key = new LocalImageCacheKey(pic.ImageUri, pic.CalcOrigin, pic.AltText); + } + Assert.IsTrue(ws.Workbook.CellPictureReferenceCache.Contains(key)); + var numberReferencesLeft = ws.Workbook.CellPictureReferenceCache.GetNumberOfReferences(key); + Assert.AreEqual(2, numberReferencesLeft); + } + + ws.Cells["A1"].Picture.Remove(); + Assert.IsNull(ws.Cells["A1"].Value); + var vm3 = ws._metadataStore.GetValue(1, 1); + Assert.AreEqual(0u, vm3.vm); + Assert.AreEqual(1, package.Workbook.RichData.Db.Values.Count()); + + //Verify that the picture ref has been removed + Assert.IsTrue(ws.Workbook.CellPictureReferenceCache.Contains(key)); + var numberOfRefs = ws.Workbook.CellPictureReferenceCache.GetNumberOfReferences(key); + Assert.AreEqual(1, numberOfRefs); + + SaveWorkbook("InCellPicturesReuseCache3_OnRead.xlsx", package); + } + } + [TestMethod] public void ShouldRemoveRichDataWhenLastReferenceRemoved() { @@ -78,5 +133,34 @@ public void ShouldRemoveRichDataWhenLastReferenceRemoved() Assert.AreEqual(0, package.Workbook.RichData.Db.Values.Count, "RichDataValue still exists"); SaveWorkbook("InCellPicturesReuseCache4.xlsx", package); } + + [TestMethod] + public void ReCalculateInCellImageShouldWork() + { + using (var p = OpenPackage("ReacalculateInCellImage.xlsx", true)) + { + var ws = p.Workbook.Worksheets.Add("Sheet"); + + ws.Cells["A1"].Picture.Set(Resources.Png2ByteArray); + var pic = ws.Cells[1, 1].Value as ExcelCellPicture; + + ws.Cells["B1"].Formula = "A1"; + + ws.Calculate(); + + //Verify that the picture has added to refs correctly + PictureCacheKey key = new LocalImageCacheKey(pic.ImageUri, pic.CalcOrigin, pic.AltText); + var numRefs = ws.Workbook.CellPictureReferenceCache.GetNumberOfReferences(key); + Assert.AreEqual(1, numRefs); + + ws.Calculate(); + + PictureCacheKey key2 = new LocalImageCacheKey(pic.ImageUri, pic.CalcOrigin, pic.AltText); + var numRefs2 = ws.Workbook.CellPictureReferenceCache.GetNumberOfReferences(key); + Assert.AreEqual(1, numRefs2); + + SaveAndCleanup(p); + } + } } } diff --git a/src/EPPlusTest/InCellImages/WebImagesTests.cs b/src/EPPlusTest/InCellImages/WebImagesTests.cs index 17d4297d6..16ec858bc 100644 --- a/src/EPPlusTest/InCellImages/WebImagesTests.cs +++ b/src/EPPlusTest/InCellImages/WebImagesTests.cs @@ -2,8 +2,10 @@ using Microsoft.VisualStudio.TestTools.UnitTesting; using OfficeOpenXml; using OfficeOpenXml.CellPictures; +using OfficeOpenXml.RichData.RichValues.WebImages; using System; using System.Collections.Generic; +using System.IO; using System.Linq; using System.Text; @@ -49,5 +51,157 @@ public void WebImagesShouldBeRemovedWhenOverwritten() SaveWorkbook("WebImages_Removed.xlsx", p); } + + [TestMethod] + public void DoublePictDelete() + { + using (var package = OpenTemplatePackage("DoublePictInCellWeb.xlsx")) + { + var ws = package.Workbook.Worksheets[0]; + + //ws.Calculate(); + + ws.Cells["B3"].Picture.Remove(); + + SaveAndCleanup(package); + } + } + + [TestMethod] + public void ShouldNotRemoveRichDataWhenMoreReferencesExistsWhenReading() + { + var ms = new MemoryStream(); + + using (var p = new ExcelPackage()) + { + var sheet = p.Workbook.Worksheets.Add("Sheet"); + sheet.Cells["A1"].Formula = "IMAGE(\"https://epplussoftware.com/img/EPPlus-logo-full.png\")"; + sheet.Cells["A2"].Formula = "IMAGE(\"https://epplussoftware.com/img/EPPlus-logo-full.png\")"; + sheet.Calculate(); + Assert.IsTrue(sheet.Cells["A1"].Picture.Exists); + Assert.IsTrue(sheet.Cells["A2"].Picture.Exists); + Assert.AreEqual(ExcelCellPictureTypes.WebImage, sheet.Cells["A1"].Picture.Get().PictureType); + Assert.AreEqual(ExcelCellPictureTypes.WebImage, sheet.Cells["A2"].Picture.Get().PictureType); + p.SaveAs(ms); + } + ms.Position = 0; + ms.Seek(0, SeekOrigin.Begin); + + using (var package = new ExcelPackage(ms)) + { + var ws = package.Workbook.Worksheets[0]; + + var pic = ws.Cells[1, 1].Value as ExcelCellPicture; + + //Verify that the picture has been read into refs correctly + PictureCacheKey key = null; + if (pic != null) + { + + key = new WebPictureCacheKey(pic.ExternalAddress, pic.AltText, pic.CalcOrigin, pic.Sizing ?? WebImageSizing.FitToCellMaintainRatio, null, null); + + Assert.IsTrue(ws.Workbook.CellPictureReferenceCache.Contains(key)); + var numberReferencesLeft = ws.Workbook.CellPictureReferenceCache.GetNumberOfReferences(key); + Assert.AreEqual(2, numberReferencesLeft); + } + + ws.Cells["A1"].Picture.Remove(); + Assert.IsNull(ws.Cells["A1"].Value); + var vm3 = ws._metadataStore.GetValue(1, 1); + Assert.AreEqual(0u, vm3.vm); + Assert.AreEqual(1, package.Workbook.RichData.Db.Values.Count()); + + //Verify that the picture ref has been removed + Assert.IsTrue(ws.Workbook.CellPictureReferenceCache.Contains(key)); + var numberOfRefs = ws.Workbook.CellPictureReferenceCache.GetNumberOfReferences(key); + Assert.AreEqual(1, numberOfRefs); + + SaveWorkbook("InCellPicturesReuseCache3_OnRead.xlsx", package); + } + } + + [TestMethod] + public void ReCalculateWebImagesShouldWork() + { + using (var p = OpenPackage("ReacalculateWebImage.xlsx", true)) + { + var ws = p.Workbook.Worksheets.Add("Sheet"); + ws.Cells["A1"].Formula = "IMAGE(\"https://epplussoftware.com/img/EPPlus-logo-full.png\")"; + ws.Calculate(); + Assert.IsTrue(ws.Cells["A1"].Picture.Exists); + Assert.AreEqual(ExcelCellPictureTypes.WebImage, ws.Cells["A1"].Picture.Get().PictureType); + + var pic = ws.Cells[1, 1].Value as ExcelCellPicture; + + //Verify that the picture has added to refs correctly + PictureCacheKey key = new WebPictureCacheKey(pic.ExternalAddress, pic.AltText, pic.CalcOrigin, pic.Sizing ?? WebImageSizing.FitToCellMaintainRatio, null, null); + var numRefs = ws.Workbook.CellPictureReferenceCache.GetNumberOfReferences(key); + Assert.AreEqual(1, numRefs); + + ws.Calculate(); + + PictureCacheKey key2 = new WebPictureCacheKey(pic.ExternalAddress, pic.AltText, pic.CalcOrigin, pic.Sizing ?? WebImageSizing.FitToCellMaintainRatio, null, null); + var numRefs2 = ws.Workbook.CellPictureReferenceCache.GetNumberOfReferences(key); + Assert.AreEqual(1, numRefs2); + + SaveAndCleanup(p); + } + } + + [TestMethod] + public void ShouldNotRemoveRichDataWhenMoreReferencesExistsWhenReadingWithCalculate() + { + var ms = new MemoryStream(); + + using (var p = new ExcelPackage()) + { + var sheet = p.Workbook.Worksheets.Add("Sheet"); + sheet.Cells["A1"].Formula = "IMAGE(\"https://epplussoftware.com/img/EPPlus-logo-full.png\")"; + sheet.Cells["A2"].Formula = "IMAGE(\"https://epplussoftware.com/img/EPPlus-logo-full.png\")"; + sheet.Calculate(); + Assert.IsTrue(sheet.Cells["A1"].Picture.Exists); + Assert.IsTrue(sheet.Cells["A2"].Picture.Exists); + Assert.AreEqual(ExcelCellPictureTypes.WebImage, sheet.Cells["A1"].Picture.Get().PictureType); + Assert.AreEqual(ExcelCellPictureTypes.WebImage, sheet.Cells["A2"].Picture.Get().PictureType); + p.SaveAs(ms); + } + ms.Position = 0; + ms.Seek(0, SeekOrigin.Begin); + + using (var package = new ExcelPackage(ms)) + { + var ws = package.Workbook.Worksheets[0]; + + //This causes issues as the NumberOfReferences is doubled while there's actually the same amount left + ws.Calculate(); + + var pic = ws.Cells[1, 1].Value as ExcelCellPicture; + + //Verify that the picture has been read into refs correctly + PictureCacheKey key = null; + if (pic != null) + { + + key = new WebPictureCacheKey(pic.ExternalAddress, pic.AltText, pic.CalcOrigin, pic.Sizing ?? WebImageSizing.FitToCellMaintainRatio, null, null); + + Assert.IsTrue(ws.Workbook.CellPictureReferenceCache.Contains(key)); + var numberReferencesLeft = ws.Workbook.CellPictureReferenceCache.GetNumberOfReferences(key); + Assert.AreEqual(2, numberReferencesLeft); + } + + ws.Cells["A1"].Picture.Remove(); + Assert.IsNull(ws.Cells["A1"].Value); + var vm3 = ws._metadataStore.GetValue(1, 1); + Assert.AreEqual(0u, vm3.vm); + Assert.AreEqual(1, package.Workbook.RichData.Db.Values.Count()); + + //Verify that the picture ref has been removed + Assert.IsTrue(ws.Workbook.CellPictureReferenceCache.Contains(key)); + var numberOfRefs = ws.Workbook.CellPictureReferenceCache.GetNumberOfReferences(key); + Assert.AreEqual(1, numberOfRefs); + + SaveWorkbook("InCellPicturesReuseCache3_WebImageOnRead.xlsx", package); + } + } } } diff --git a/src/EPPlusTest/Issues/DefinedNameIssues.cs b/src/EPPlusTest/Issues/DefinedNameIssues.cs index d180ac253..2ac08c50f 100644 --- a/src/EPPlusTest/Issues/DefinedNameIssues.cs +++ b/src/EPPlusTest/Issues/DefinedNameIssues.cs @@ -232,7 +232,7 @@ static void RunTest(string name, Func<(ExcelPackage pkg, ExcelWorksheet ws1, Exc try { fromWs1 = ctx.ws1.Calculate("'Sheet2'!C1"); } catch (Exception ex) { fromWs1 = $"EXCEPTION: {ex.GetType().Name}: {ex.Message}"; } return $"ws2.Calculate(\"'Sheet2'!C1\") => {fromWs2}\nws1.Calculate(\"'Sheet2'!C1\") => {fromWs1}"; - Assert.AreEqual(fromWs1, 10); + //Assert.AreEqual(fromWs1, 10); }); RunTest("Sanity: removing sheet-scoped name fixes formula-string eval", ctx => diff --git a/src/EPPlusTest/Issues/DrawingIssues.cs b/src/EPPlusTest/Issues/DrawingIssues.cs index e413317fd..5f8d9ba27 100644 --- a/src/EPPlusTest/Issues/DrawingIssues.cs +++ b/src/EPPlusTest/Issues/DrawingIssues.cs @@ -187,6 +187,36 @@ public void EnsureWhiteSpaceIsPreservedInShapes() Assert.AreEqual(" ", retText); } + [TestMethod] + public void i2278() + { + using (var package = OpenTemplatePackage("i2278.xlsx")) + { + using var target = new ExcelPackage(); + var targetSheet = target.Workbook.Worksheets.Add("Sheet1"); + + var worksheet = package.Workbook.Worksheets[0]; + + foreach (var drawing in worksheet.Drawings) + { + drawing.Copy(targetSheet, drawing.From.Row, drawing.From.Column, drawing.From.RowOff, drawing.From.ColumnOff); + } + + SaveAndCleanup(package); + } + } + [TestMethod] + public void i2303() + { + using (var package = OpenTemplatePackage("i2303.xlsx")) + { + var sheet = package.Workbook.Worksheets.First(); + var drawing = sheet.Drawings.First(); + var image = drawing.As.Picture.Image; + + SaveAndCleanup(package); + } + } } } diff --git a/src/EPPlusTest/Issues/FormulaCalculationIssues.cs b/src/EPPlusTest/Issues/FormulaCalculationIssues.cs index 3747cf7a1..07b9bb3a8 100644 --- a/src/EPPlusTest/Issues/FormulaCalculationIssues.cs +++ b/src/EPPlusTest/Issues/FormulaCalculationIssues.cs @@ -3,6 +3,7 @@ using OfficeOpenXml.FormulaParsing; using OfficeOpenXml.FormulaParsing.Excel.Functions.MathFunctions; using OfficeOpenXml.FormulaParsing.LexicalAnalysis; +using OfficeOpenXml.FormulaParsing.Logging; using OfficeOpenXml.Sorting; using System; using System.Collections.Generic; diff --git a/src/EPPlusTest/Issues/PackageIssues.cs b/src/EPPlusTest/Issues/PackageIssues.cs index 0a8a4f644..8bb6d73d8 100644 --- a/src/EPPlusTest/Issues/PackageIssues.cs +++ b/src/EPPlusTest/Issues/PackageIssues.cs @@ -1,6 +1,12 @@ using Microsoft.VisualStudio.TestTools.UnitTesting; using OfficeOpenXml; +using System; +using System.ComponentModel; using System.Drawing; +using System.Globalization; +using System.IO; +using System.Linq; +using System.Threading; namespace EPPlusTest.Issues { diff --git a/src/EPPlusTest/Issues/RangeTextIssues.cs b/src/EPPlusTest/Issues/RangeTextIssues.cs index 9db0536dc..027abf620 100644 --- a/src/EPPlusTest/Issues/RangeTextIssues.cs +++ b/src/EPPlusTest/Issues/RangeTextIssues.cs @@ -65,5 +65,26 @@ public void i1964() Assert.AreEqual("2021-12-31", cell); } } + [TestMethod] + public void i2276() + { + using (var package = OpenTemplatePackage("CurrencyTest2.xlsx")) + { + SwitchToCulture("nl-NL"); + ExcelWorkbook workbook = package.Workbook; + ExcelWorksheet worksheet = workbook.Worksheets[0]; + + Assert.AreEqual("€ 12 347", worksheet.Cells["A1"].Text); + Assert.AreEqual("[$€-2]\\ #,##0", worksheet.Cells["A1"].Style.Numberformat.Format); + Assert.AreEqual("€ 12 346,78", worksheet.Cells["D1"].Text); + Assert.AreEqual("[$€-2]\\ #,##0.00", worksheet.Cells["D1"].Style.Numberformat.Format); + + Assert.AreEqual("€ 12.347", worksheet.Cells["A2"].Text); + Assert.AreEqual("\"€\"\\ #,##0", worksheet.Cells["A2"].Style.Numberformat.Format); + Assert.AreEqual("€ 12.346,78", worksheet.Cells["D2"].Text); + Assert.AreEqual("\"€\"\\ #,##0.00", worksheet.Cells["D2"].Style.Numberformat.Format); + SwitchBackToCurrentCulture(); + } + } } } diff --git a/src/EPPlusTest/Issues/StylingIssues.cs b/src/EPPlusTest/Issues/StylingIssues.cs index 612630f3d..1082c3163 100644 --- a/src/EPPlusTest/Issues/StylingIssues.cs +++ b/src/EPPlusTest/Issues/StylingIssues.cs @@ -8,6 +8,8 @@ using System.IO; using System.Globalization; using OfficeOpenXml.Style; +using OfficeOpenXml.FormulaParsing; + namespace EPPlusTest { [TestClass] @@ -413,6 +415,95 @@ public void i1839() Assert.AreEqual(288, p.Workbook.Worksheets[0].Cells["E31"].StyleID); SaveWorkbook("i1839-saved.xlsx", p); } + [TestMethod] + public void s1007() + { + using var p = OpenPackage("s1007.xlsx"); + var worksheet = p.Workbook.Worksheets.Add("Sheet1"); + worksheet.Cells["A1"].Style.Numberformat.Format = "#.##0;-#.##0;X"; + worksheet.Cells["A1"].Value = 0; + Assert.AreEqual("X", worksheet.Cells["A1"].Text); + //SaveAndCleanup(p); + } + + + [TestMethod] + public void s1005() + { + SwitchToCulture("de-DE"); + + //Set specific built-in formats + if (!ExcelPackageSettings.CultureSpecificBuildInNumberFormats.ContainsKey("de-DE")) + { + ExcelPackageSettings.CultureSpecificBuildInNumberFormats.Add("de-DE", + new Dictionary + { + {14,"dd.MM.yyyy"}, + {15,"dd. MMM yy"}, + {16,"dd. MMM"}, + {17,"MMM yy"}, + {18,"hh:mm AM/PM" }, + {22,"dd.MM.yyyy hh:mm"}, + {37,"#,##0;-#,##0"}, + {38,"#,##0;[Rot]-#,##0"}, + {39,"#,##0.00;-#,##0.00"}, + {40,"#,##0.00;[Rot]-#,##0.00"}, + {47,"mm:ss,f"} + }); + } + + using (var p = OpenTemplatePackage("s1005.xlsx")) + { + //AND use a Custom Number Format + //This caused issues in the Text.cs file when we ran Calculate + p.Workbook.NumberFormatToTextHandler = CustomNumberFormatToTextExample; + + var ws1 = p.Workbook.Worksheets[1]; + + var origText = ws1.Cells["D8"].Text; + + //Verify cell contents match when test was written + Assert.IsTrue(origText.Contains(".8000")); + Assert.IsTrue(origText.Contains("(26895559.000)")); + Assert.IsTrue(origText.Contains("69928453.64000")); + + ws1.Cells["D8"].Calculate(); + var cellRich = ws1.Cells["D8"].RichText.Text; + + //Verify formatting has changed appropriately for calculated string + Assert.IsTrue(cellRich.Contains("0,80")); + Assert.IsTrue(cellRich.Contains("(26.895.559,00)")); + Assert.IsTrue(cellRich.Contains("69.928.453,64")); + + SaveAndCleanup(p); + } + + SwitchBackToCurrentCulture(); + } + + private static string CustomNumberFormatToTextExample(NumberFormatToTextArgs options) + { + var result = options.Text; + + switch (options.Value) + { + case DateTime dt: + { + switch (options.NumberFormat.Format) + { + case "dd. mmm yy": + result = dt.ToString("dd. mmm yy"); //Return your own formatted text. Example + break; + default: + result = dt.ToString(options.NumberFormat.Format); + break; + } + break; + } + } + + return result; + } public class TestData { diff --git a/src/EPPlusTest/Issues/WorksheetIssues.cs b/src/EPPlusTest/Issues/WorksheetIssues.cs index ae73c6771..ccb91d9f3 100644 --- a/src/EPPlusTest/Issues/WorksheetIssues.cs +++ b/src/EPPlusTest/Issues/WorksheetIssues.cs @@ -4,6 +4,7 @@ using OfficeOpenXml.Drawing; using OfficeOpenXml.Drawing.Chart; using OfficeOpenXml.FormulaParsing; +using OfficeOpenXml.FormulaParsing.Excel.Functions.Information; using OfficeOpenXml.FormulaParsing.Excel.Functions.Logical; using OfficeOpenXml.FormulaParsing.Excel.Functions.MathFunctions; using OfficeOpenXml.RichData; @@ -1121,5 +1122,22 @@ public void i2258() var d3 = ws.Cells["D3"].Text; Assert.AreEqual("Negative 4000,000", d3); } + + [TestMethod] + public void InsertAndShift_ShouldNotThrow_WhenArrayIsFull() + { + using var package = new ExcelPackage(); + var sheet = package.Workbook.Worksheets.Add("Sheet1"); + + // Fill 7 columns with formatting to trigger the boundary condition + for (int col = 1; col <= 7; col++) + { + sheet.Column(col).Width = 15; + } + + // These two inserts should not throw ArgumentException + sheet.InsertColumn(3, 1); + sheet.InsertColumn(5, 1); + } } } diff --git a/src/EPPlusTest/VBA/VBATests.cs b/src/EPPlusTest/VBA/VBATests.cs index 423cfcb2d..659d711f4 100644 --- a/src/EPPlusTest/VBA/VBATests.cs +++ b/src/EPPlusTest/VBA/VBATests.cs @@ -1,5 +1,7 @@ using Microsoft.VisualStudio.TestTools.UnitTesting; using OfficeOpenXml; +using OfficeOpenXml.Constants; +using OfficeOpenXml.Drawing; using OfficeOpenXml.Utils.VBA; using OfficeOpenXml.VBA; using OfficeOpenXml.VBA.ContentHash; @@ -12,6 +14,8 @@ using System.Security.Cryptography.X509Certificates; using System.Text; using System.Threading.Tasks; +using System.Xml; +using System.Xml.XPath; namespace EPPlusTest.VBA { @@ -300,5 +304,91 @@ public void VbaModuleNameShouldAllowSpace() var module = package.Workbook.VbaProject.Modules.AddModule("My BubbleChartModule"); module.Code = sb.ToString(); } + + + [TestMethod] + public void ReadAndSaveTemplateDxfIdsCheckbox() + { + using (var package = OpenTemplatePackage("i2273.xlsx")) + { + var ws = package.Workbook.Worksheets[0]; + SaveAndCleanup(package); + } + + using (var package = OpenPackage("i2273.xlsx")) + { + var ws = package.Workbook.Worksheets[0]; + + var tbl = ws.Tables[0]; + var cols = tbl.Columns; + + //Verify style bools + Assert.IsTrue(cols[0].DataStyle.Checkbox); + Assert.IsTrue(cols[1].DataStyle.Checkbox); + + var topNode = cols[0].DataStyle._helper.TopNode; + cols[0].DataStyle._helper.GetDefaultNode("extLst/ext"); + + //ws.Workbook.Styles + var extNode = (XmlElement)cols[0].DataStyle._helper.GetDefaultNode($"extLst/ext"); + var extNodeCol2 = (XmlElement)cols[0].DataStyle._helper.GetDefaultNode($"extLst/ext"); + + //Verify dxf property bag + Assert.IsTrue(extNode.GetAttribute("uri") == ExtLstUris.FeaturePropertyBagDxf); + Assert.IsTrue(extNodeCol2.GetAttribute("uri") == ExtLstUris.FeaturePropertyBagDxf); + + SaveAndCleanup(package); + } + } + + [TestMethod] + public void EnsureEpplusReadsXlsmDxfIdsForTablesCorrectly() + { + var fileName = "MyVBACheckboxes"; + var fileEnding = ".xlsm"; + + using (var package = OpenPackage(fileName + fileEnding, true)) + { + var ws = package.Workbook.Worksheets.Add("TableCheckboxes"); + package.Workbook.CreateVBAProject(); + + var tbl = ws.Tables.Add(ws.Cells["B2:C3"], "Table1"); + tbl.ShowHeader = true; + tbl.DataStyle.Checkbox = true; + tbl.Columns[0].DataStyle.Checkbox = true; + tbl.Columns[1].DataStyle.Checkbox = true; + + ws.Cells["B3:C3"].Value = false; + + SaveAndCleanup(package); + } + + using (var package = OpenPackage(fileName + fileEnding)) + { + var ws = package.Workbook.Worksheets[0]; + + var tbl = ws.Tables[0]; + var cols = tbl.Columns; + + //Verify style bools + Assert.IsTrue(tbl.DataStyle.Checkbox); + Assert.IsTrue(cols[0].DataStyle.Checkbox); + Assert.IsTrue(cols[1].DataStyle.Checkbox); + + var topNode = cols[0].DataStyle._helper.TopNode; + cols[0].DataStyle._helper.GetDefaultNode("extLst/ext"); + + //ws.Workbook.Styles + var extNode = (XmlElement)cols[0].DataStyle._helper.GetDefaultNode($"extLst/ext"); + var extNodeCol2 = (XmlElement)cols[0].DataStyle._helper.GetDefaultNode($"extLst/ext"); + + //Verify dxf property bag + Assert.IsTrue(extNode.GetAttribute("uri") == ExtLstUris.FeaturePropertyBagDxf); + Assert.IsTrue(extNodeCol2.GetAttribute("uri") == ExtLstUris.FeaturePropertyBagDxf); + + var outFile = GetOutputFile("", fileName + "_Resaved" + fileEnding); + package.SaveAs(outFile); + } + } } } From c1ec54af283d1c4bc03c1274517d973c035e7054 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ossian=20Edstr=C3=B6m?= Date: Mon, 9 Mar 2026 12:46:40 +0100 Subject: [PATCH 082/151] Added datalabels without position to line chart --- .../ImageRenderer.cs | 2 - .../RenderItems/Shared/TextBodyItem.cs | 9 +- .../SvgItem/SvgChartSerieDataLabel.cs | 84 +++++++++++++++++-- .../RenderItems/SvgItem/SvgTextBox.cs | 6 ++ .../ChartTypeDrawers/LineChartTypeDrawer.cs | 2 +- .../Drawing/Chart/ExcelChartStandardSerie.cs | 37 ++++++-- 6 files changed, 121 insertions(+), 19 deletions(-) diff --git a/src/EPPlus.Export.ImageRenderer/ImageRenderer.cs b/src/EPPlus.Export.ImageRenderer/ImageRenderer.cs index f6cdbeeb2..c6ea2f1b9 100644 --- a/src/EPPlus.Export.ImageRenderer/ImageRenderer.cs +++ b/src/EPPlus.Export.ImageRenderer/ImageRenderer.cs @@ -12,8 +12,6 @@ Date Author Change *************************************************************************************************/ using EPPlus.Export.ImageRenderer; -using EPPlus.Export.ImageRenderer.RenderItems.Independent.Shared; -using EPPlus.Export.ImageRenderer.RenderItems.Independent.SvgItem; using EPPlus.Export.ImageRenderer.RenderItems.Shared; using EPPlus.Export.ImageRenderer.RenderItems.SvgItem; using EPPlus.Export.ImageRenderer.Svg; diff --git a/src/EPPlus.Export.ImageRenderer/RenderItems/Shared/TextBodyItem.cs b/src/EPPlus.Export.ImageRenderer/RenderItems/Shared/TextBodyItem.cs index 2e95f087d..111e1489d 100644 --- a/src/EPPlus.Export.ImageRenderer/RenderItems/Shared/TextBodyItem.cs +++ b/src/EPPlus.Export.ImageRenderer/RenderItems/Shared/TextBodyItem.cs @@ -60,7 +60,6 @@ public TextBodyItem(DrawingBase renderer, BoundingBox parent, double maxWidth, d internal bool WrapText = true; - private FontMeasurerTrueType _measurer = null; internal string _text; public void ImportParagraph(ExcelDrawingParagraph item, double startingY, string text=null) @@ -199,10 +198,10 @@ private double GetParagraphAscendantSpacingInPixels(eDrawingTextLineSpacing line // Paragraphs.Add(paragraph); //} - public void SetMeasurer(FontMeasurerTrueType fontMeasurer) - { - _measurer = fontMeasurer; - } + //public void SetMeasurer(FontMeasurerTrueType fontMeasurer) + //{ + // _measurer = fontMeasurer; + //} double? _alignmentY = null; diff --git a/src/EPPlus.Export.ImageRenderer/RenderItems/SvgItem/SvgChartSerieDataLabel.cs b/src/EPPlus.Export.ImageRenderer/RenderItems/SvgItem/SvgChartSerieDataLabel.cs index 11ddfeaba..1f0eec987 100644 --- a/src/EPPlus.Export.ImageRenderer/RenderItems/SvgItem/SvgChartSerieDataLabel.cs +++ b/src/EPPlus.Export.ImageRenderer/RenderItems/SvgItem/SvgChartSerieDataLabel.cs @@ -26,13 +26,67 @@ internal class SvgChartSerieDataLabel : SvgChartObject { List dlblTextBoxes = new List(); - public SvgChartSerieDataLabel(SvgChart chart, ExcelChartSerieDataLabel dlblSerie, BoundingBox maxBounds) : base(chart) + string separator; + + public SvgChartSerieDataLabel(SvgChart chart, ExcelChartSerieDataLabel dlblSerie, BoundingBox maxBounds, ExcelChartStandardSerie serie, List xValues, List yValues) : base(chart) { - foreach(var dlbl in dlblSerie.DataLabels) + if (dlblSerie.DataLabels.Count == 0 && serie.NumberOfItems > 0) + { + separator = string.IsNullOrEmpty(dlblSerie.Separator) ? "," : dlblSerie.Separator; + + //string dlblStr = ""; + + ////dlblStr +="" + ////StringBuilder sb = new StringBuilder(); + //if (dlblSerie.ShowSeriesName) + //{ + // dlblStr += serie.Header; + // //AppendToDatalabelStr(sb, serie.Header); + //} + + for (int i = 0; i < serie.NumberOfItems; i++) + { + var txtBox = new SvgTextBox(chart, maxBounds, maxBounds); + txtBox.ImportTextBody(dlblSerie.TextBody); + + List dlblStrings = new List(); + + if (dlblSerie.ShowSeriesName) + { + dlblStrings.Add(serie.GetHeaderString()); + } + if (dlblSerie.ShowCategory) + { + dlblStrings.Add(xValues[i].ToString()); + } + if (dlblSerie.ShowValue) + { + dlblStrings.Add(yValues[i].ToString()); + } + + string finalString = ""; + for (int j = 0; j < dlblStrings.Count; j++) + { + finalString += dlblStrings[j]; + if (j != dlblStrings.Count - 1) + { + finalString += separator; + } + } + + txtBox.TextBody.ImportParagraph(dlblSerie.TextBody.Paragraphs[0], 0, finalString); + + dlblTextBoxes.Add(txtBox); + } + } + else { - var txtBox = new SvgTextBox(chart, maxBounds, maxBounds); - txtBox.ImportTextBody(dlbl.TextBody); - dlblTextBoxes.Add(txtBox); + foreach (var dlbl in dlblSerie.DataLabels) + { + var txtBox = new SvgTextBox(chart, maxBounds, maxBounds); + txtBox.ImportTextBody(dlbl.TextBody); + dlblTextBoxes.Add(txtBox); + } } } @@ -49,6 +103,26 @@ internal override void AppendRenderItems(List renderItems) renderItems.Add(new SvgEndGroupItem(DrawingRenderer, Bounds)); } + //internal void AppendToDatalabelStr(StringBuilder sb, string toAppend) + //{ + // sb.Append(toAppend); + // sb.Append(separator); + //} + + //internal string GetDatalabelString(ExcelChartSerieDataLabel dlbl, ExcelChartStandardSerie serie) + //{ + // var dlblStr = ""; + + // //if(dlbl.ShowValue) + // //{ + + // //} + // //if(dlbl.ShowSeriesName) + // //{ + // // dlb + // //} + //} + //public override RenderItemType Type => throw new NotImplementedException(); //public override void Render(StringBuilder sb) diff --git a/src/EPPlus.Export.ImageRenderer/RenderItems/SvgItem/SvgTextBox.cs b/src/EPPlus.Export.ImageRenderer/RenderItems/SvgItem/SvgTextBox.cs index 8039c56b9..63e98c911 100644 --- a/src/EPPlus.Export.ImageRenderer/RenderItems/SvgItem/SvgTextBox.cs +++ b/src/EPPlus.Export.ImageRenderer/RenderItems/SvgItem/SvgTextBox.cs @@ -114,6 +114,12 @@ internal void ImportTextBody(ExcelTextBody body) internal override void AppendRenderItems(List renderItems) { var rect = Rectangle; + + if(rect.FillColor == null) + { + rect.FillColor = "transparent"; + } + SvgGroupItem groupItem; if (Rotation == 0) { diff --git a/src/EPPlus.Export.ImageRenderer/Svg/Chart/ChartTypeDrawers/LineChartTypeDrawer.cs b/src/EPPlus.Export.ImageRenderer/Svg/Chart/ChartTypeDrawers/LineChartTypeDrawer.cs index b31de8c4f..f8cbb3325 100644 --- a/src/EPPlus.Export.ImageRenderer/Svg/Chart/ChartTypeDrawers/LineChartTypeDrawer.cs +++ b/src/EPPlus.Export.ImageRenderer/Svg/Chart/ChartTypeDrawers/LineChartTypeDrawer.cs @@ -33,7 +33,7 @@ internal LineChartTypeDrawer(SvgChart svgChart, ExcelChart chartType) : base(svg if (serie.HasDataLabel) { - var datalabel = new SvgChartSerieDataLabel(svgChart, serie.DataLabel, svgChart.Plotarea.Bounds); + var datalabel = new SvgChartSerieDataLabel(svgChart, serie.DataLabel, svgChart.Bounds, serie, xValue, yValue); serieDataLabels.Add(datalabel); } } diff --git a/src/EPPlus/Drawing/Chart/ExcelChartStandardSerie.cs b/src/EPPlus/Drawing/Chart/ExcelChartStandardSerie.cs index 06c03b88d..b39f19401 100644 --- a/src/EPPlus/Drawing/Chart/ExcelChartStandardSerie.cs +++ b/src/EPPlus/Drawing/Chart/ExcelChartStandardSerie.cs @@ -10,15 +10,17 @@ Date Author Change ************************************************************************************************* 05/15/2020 EPPlus Software AB EPPlus 5.2 *************************************************************************************************/ +using OfficeOpenXml.Core.CellStore; +using OfficeOpenXml.FormulaParsing.Excel.Functions.RefAndLookup; +using OfficeOpenXml.FormulaParsing.Excel.Functions.Text; using System; using System.Collections.Generic; -using System.Text; -using System.Xml; -using System.Linq; -using OfficeOpenXml.Core.CellStore; using System.Globalization; -using System.Runtime.CompilerServices; using System.IO; +using System.Linq; +using System.Runtime.CompilerServices; +using System.Text; +using System.Xml; namespace OfficeOpenXml.Drawing.Chart { /// @@ -217,7 +219,30 @@ public override ExcelAddressBase HeaderAddress SetXmlNodeString(headerAddressPath, ExcelCellBase.GetFullAddress(value.WorkSheetName, value.Address)); SetXmlNodeString("c:tx/c:strRef/c:strCache/c:ptCount/@val", "0"); } - } + } + + internal string GetHeaderString() + { + if(string.IsNullOrEmpty(Header)) + { + if(HeaderAddress == null) + { + return null; + } + + var ws = string.IsNullOrEmpty(HeaderAddress.WorkSheetName) ? _chart.WorkSheet : _chart.WorkSheet.Workbook.Worksheets[HeaderAddress.WorkSheetName]; + if (ws == null) //Worksheet does not exist, exit + { + return null; + } + return ws.Cells[HeaderAddress.Address].Text; + } + else + { + return Header; + } + } + string _seriesTopPath; string _seriesPath = "{0}/c:numRef/c:f"; string _numCachePath = "{0}/c:numRef/c:numCache"; From c3a7f01008c17e05a1a3fa8f1e50e44852c1cf60 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ossian=20Edstr=C3=B6m?= Date: Mon, 9 Mar 2026 14:24:29 +0100 Subject: [PATCH 083/151] Added basic datalabels with corrected positioning --- .../SvgItem/SvgChartSerieDataLabel.cs | 13 +++++--- .../Chart/Axis/ValueAxisScaleCalculator.cs | 6 ++-- .../ChartTypeDrawers/LineChartTypeDrawer.cs | 30 ++++++++++++++----- 3 files changed, 35 insertions(+), 14 deletions(-) diff --git a/src/EPPlus.Export.ImageRenderer/RenderItems/SvgItem/SvgChartSerieDataLabel.cs b/src/EPPlus.Export.ImageRenderer/RenderItems/SvgItem/SvgChartSerieDataLabel.cs index 1f0eec987..0054e44ac 100644 --- a/src/EPPlus.Export.ImageRenderer/RenderItems/SvgItem/SvgChartSerieDataLabel.cs +++ b/src/EPPlus.Export.ImageRenderer/RenderItems/SvgItem/SvgChartSerieDataLabel.cs @@ -24,7 +24,7 @@ namespace EPPlus.Export.ImageRenderer.RenderItems.SvgItem { internal class SvgChartSerieDataLabel : SvgChartObject { - List dlblTextBoxes = new List(); + internal List dlblTextBoxes = new List(); string separator; @@ -46,9 +46,6 @@ public SvgChartSerieDataLabel(SvgChart chart, ExcelChartSerieDataLabel dlblSerie for (int i = 0; i < serie.NumberOfItems; i++) { - var txtBox = new SvgTextBox(chart, maxBounds, maxBounds); - txtBox.ImportTextBody(dlblSerie.TextBody); - List dlblStrings = new List(); if (dlblSerie.ShowSeriesName) @@ -74,7 +71,15 @@ public SvgChartSerieDataLabel(SvgChart chart, ExcelChartSerieDataLabel dlblSerie } } + var txtBox = new SvgTextBox(chart, maxBounds, maxBounds); + txtBox.ImportTextBody(dlblSerie.TextBody); + txtBox.TextBody.ImportParagraph(dlblSerie.TextBody.Paragraphs[0], 0, finalString); + //Remove dummy paragraph added by ImportTextBody + txtBox.TextBody.Paragraphs.RemoveAt(0); + //Reset run y-position. + //Datalabel does not use the standard line-spacing textbody offsets + txtBox.TextBody.Paragraphs[0].Runs[0].YPosition = 0; dlblTextBoxes.Add(txtBox); } diff --git a/src/EPPlus.Export.ImageRenderer/Svg/Chart/Axis/ValueAxisScaleCalculator.cs b/src/EPPlus.Export.ImageRenderer/Svg/Chart/Axis/ValueAxisScaleCalculator.cs index ead28dc09..ffc5c0083 100644 --- a/src/EPPlus.Export.ImageRenderer/Svg/Chart/Axis/ValueAxisScaleCalculator.cs +++ b/src/EPPlus.Export.ImageRenderer/Svg/Chart/Axis/ValueAxisScaleCalculator.cs @@ -34,7 +34,7 @@ private static AxisScale GetNumberScale(ref double dataMin, ref double dataMax, } var isAllPositive = dataMin >= 0 && dataMax >= 0; - var isAllNegativ = dataMin <= 0 && dataMax <= 0; + var isAllNegative = dataMin <= 0 && dataMax <= 0; if(dataMin < 0 && dataMax > 0 && axisOptions.IsStacked100) { desiredTicks *= 2; @@ -78,7 +78,7 @@ private static AxisScale GetNumberScale(ref double dataMin, ref double dataMax, if (axisOptions.AddPadding) { dataMax = axisOptions.LockedMax.Value; - if (isAllNegativ && dataMax > 0) + if (isAllNegative && dataMax > 0) { dataMax = 0; } @@ -145,7 +145,7 @@ private static AxisScale GetNumberScale(ref double dataMin, ref double dataMax, axisMax = 1; } - if (axisMax > 0 && isAllNegativ) + if (axisMax > 0 && isAllNegative) { axisMax = 0; } diff --git a/src/EPPlus.Export.ImageRenderer/Svg/Chart/ChartTypeDrawers/LineChartTypeDrawer.cs b/src/EPPlus.Export.ImageRenderer/Svg/Chart/ChartTypeDrawers/LineChartTypeDrawer.cs index f8cbb3325..dbe61acaa 100644 --- a/src/EPPlus.Export.ImageRenderer/Svg/Chart/ChartTypeDrawers/LineChartTypeDrawer.cs +++ b/src/EPPlus.Export.ImageRenderer/Svg/Chart/ChartTypeDrawers/LineChartTypeDrawer.cs @@ -3,16 +3,14 @@ using EPPlusImageRenderer.Svg; using OfficeOpenXml.Drawing.Chart; using OfficeOpenXml.Utils.TypeConversion; -using System; -using System.Collections; using System.Collections.Generic; -using System.Linq; -using static OfficeOpenXml.ExcelErrorValue; namespace EPPlus.Export.ImageRenderer.Svg.Chart { internal class LineChartTypeDrawer : ChartTypeDrawer { + List serieDataLabels = new List(); + internal LineChartTypeDrawer(SvgChart svgChart, ExcelChart chartType) : base(svgChart, chartType) { var groupItem = new SvgGroupItem(ChartRenderer, _svgChart.Plotarea.Rectangle.Bounds); @@ -21,7 +19,6 @@ internal LineChartTypeDrawer(SvgChart svgChart, ExcelChart chartType) : base(svg var isPercentStacked = Chart.IsTypePercentStacked(); var xValues = new List>(); var yValues = new List>(); - var serieDataLabels = new List(); foreach (ExcelLineChartSerie serie in chartType.Series) { @@ -52,7 +49,14 @@ internal LineChartTypeDrawer(SvgChart svgChart, ExcelChart chartType) : base(svg var xSerie = xValues[i]; var ySerie = yValues[i]; var serie = (ExcelLineChartSerie)chartType.Series[i]; - AddLine(chartType, serie, xSerie, ySerie); + + SvgChartSerieDataLabel dataLabel = null; + if (serie.HasDataLabel) + { + dataLabel = serieDataLabels[i]; + } + + AddLine(chartType, serie, xSerie, ySerie, dataLabel); } foreach(var dataLabel in serieDataLabels) @@ -72,7 +76,7 @@ private void SumSeries(List> series) } } } - private void AddLine(ExcelChart chartType, ExcelLineChartSerie serie, List xValues, List yValues) + private void AddLine(ExcelChart chartType, ExcelLineChartSerie serie, List xValues, List yValues, SvgChartSerieDataLabel serieDataLabel) { var xAxis = _svgChart.HorizontalAxis; SvgChartAxis yAxis; @@ -108,6 +112,12 @@ private void AddLine(ExcelChart chartType, ExcelLineChartSerie serie, List Date: Mon, 9 Mar 2026 16:17:28 +0100 Subject: [PATCH 084/151] Added legendIcon to datalabels --- .../SvgPathTests.cs | 16 ++++ .../RenderItems/SvgGroupItem.cs | 1 + .../SvgItem/SvgChartSerieDataLabel.cs | 81 +++++++++---------- .../RenderItems/SvgRenderLineItem.cs | 12 +++ .../ChartTypeDrawers/LineChartTypeDrawer.cs | 14 ++-- .../Svg/Chart/SvgChartLegend.cs | 2 +- 6 files changed, 76 insertions(+), 50 deletions(-) diff --git a/src/EPPlus.Export.ImageRenderer.Test/SvgPathTests.cs b/src/EPPlus.Export.ImageRenderer.Test/SvgPathTests.cs index a352b1d5f..4a5b17bca 100644 --- a/src/EPPlus.Export.ImageRenderer.Test/SvgPathTests.cs +++ b/src/EPPlus.Export.ImageRenderer.Test/SvgPathTests.cs @@ -557,6 +557,22 @@ public void GenerateDataLabels() } + + [TestMethod] + public void GenerateDataLabelsTrueMost() + { + ExcelPackage.License.SetNonCommercialOrganization("EPPlus Project"); + using (var p = OpenTemplatePackage("datalabelsSvgTrueMost.xlsx")) + { + var c = p.Workbook.Worksheets[0].Drawings[0]; + + var renderer = new EPPlusImageRenderer.ImageRenderer(); + var svg = renderer.RenderDrawingToSvg(c); + SaveTextFileToWorkbook($"svg\\datalabelsTrueMost.svg", svg); + } + } + + [TestMethod] public void OpenRightAligned() { diff --git a/src/EPPlus.Export.ImageRenderer/RenderItems/SvgGroupItem.cs b/src/EPPlus.Export.ImageRenderer/RenderItems/SvgGroupItem.cs index 2a830836e..a316d7f19 100644 --- a/src/EPPlus.Export.ImageRenderer/RenderItems/SvgGroupItem.cs +++ b/src/EPPlus.Export.ImageRenderer/RenderItems/SvgGroupItem.cs @@ -45,6 +45,7 @@ internal SvgGroupItem(DrawingBase renderer) : base(renderer) { } + internal SvgGroupItem(DrawingBase renderer, BoundingBox bounds) : base(renderer) { Bounds = bounds; diff --git a/src/EPPlus.Export.ImageRenderer/RenderItems/SvgItem/SvgChartSerieDataLabel.cs b/src/EPPlus.Export.ImageRenderer/RenderItems/SvgItem/SvgChartSerieDataLabel.cs index 0054e44ac..c4aa026e4 100644 --- a/src/EPPlus.Export.ImageRenderer/RenderItems/SvgItem/SvgChartSerieDataLabel.cs +++ b/src/EPPlus.Export.ImageRenderer/RenderItems/SvgItem/SvgChartSerieDataLabel.cs @@ -24,25 +24,39 @@ namespace EPPlus.Export.ImageRenderer.RenderItems.SvgItem { internal class SvgChartSerieDataLabel : SvgChartObject { - internal List dlblTextBoxes = new List(); + //positioning is handled by parent item via these + internal List groupItems = new List(); + private List dlblTextBoxes = new List(); + private RenderItem seriesIcon = null; + string separator; - public SvgChartSerieDataLabel(SvgChart chart, ExcelChartSerieDataLabel dlblSerie, BoundingBox maxBounds, ExcelChartStandardSerie serie, List xValues, List yValues) : base(chart) + public SvgChartSerieDataLabel(SvgChart chart, ExcelChartSerieDataLabel dlblSerie, BoundingBox maxBounds, ExcelChartStandardSerie serie, List xValues, List yValues, int index) : base(chart) { if (dlblSerie.DataLabels.Count == 0 && serie.NumberOfItems > 0) { separator = string.IsNullOrEmpty(dlblSerie.Separator) ? "," : dlblSerie.Separator; - //string dlblStr = ""; + if(dlblSerie.ShowLegendKey) + { + SvgChartLegend legendItem = null; + if (chart.Legend == null) + { + legendItem = dlblSerie.ShowLegendKey == false ? null : new SvgChartLegend(chart); + } + else + { + legendItem = chart.Legend; + } + var seriesIconOrig = (SvgRenderLineItem)legendItem.SeriesIcon[index].SeriesIcon; + var clonedIcon = seriesIconOrig.Clone(chart); + + clonedIcon.Y1 = 0; + clonedIcon.Y2 = 0; - ////dlblStr +="" - ////StringBuilder sb = new StringBuilder(); - //if (dlblSerie.ShowSeriesName) - //{ - // dlblStr += serie.Header; - // //AppendToDatalabelStr(sb, serie.Header); - //} + seriesIcon = clonedIcon; + } for (int i = 0; i < serie.NumberOfItems; i++) { @@ -97,42 +111,21 @@ public SvgChartSerieDataLabel(SvgChart chart, ExcelChartSerieDataLabel dlblSerie internal override void AppendRenderItems(List renderItems) { - SvgGroupItem groupItem = new SvgGroupItem(DrawingRenderer, Bounds); - renderItems.Add(groupItem); - - foreach (var renderItem in dlblTextBoxes) + for(int i = 0; i< groupItems.Count; i++) { - renderItem.AppendRenderItems(renderItems); - } + if (seriesIcon != null) + { + groupItems[i].Bounds.Left += (seriesIcon.Bounds.Width / 2); + groupItems[i].GroupTransform = $"transform=\"translate({groupItems[i].Bounds.Left.PointToPixelString()}, {groupItems[i].Bounds.Top.PointToPixelString()})\""; + dlblTextBoxes[i].Left += seriesIcon.Bounds.Width + dlblTextBoxes[i].LeftMargin; + } - renderItems.Add(new SvgEndGroupItem(DrawingRenderer, Bounds)); - } + renderItems.Add(groupItems[i]); + renderItems.Add(seriesIcon); + dlblTextBoxes[i].AppendRenderItems(renderItems); - //internal void AppendToDatalabelStr(StringBuilder sb, string toAppend) - //{ - // sb.Append(toAppend); - // sb.Append(separator); - //} - - //internal string GetDatalabelString(ExcelChartSerieDataLabel dlbl, ExcelChartStandardSerie serie) - //{ - // var dlblStr = ""; - - // //if(dlbl.ShowValue) - // //{ - - // //} - // //if(dlbl.ShowSeriesName) - // //{ - // // dlb - // //} - //} - - //public override RenderItemType Type => throw new NotImplementedException(); - - //public override void Render(StringBuilder sb) - //{ - // throw new NotImplementedException(); - //} + renderItems.Add(new SvgEndGroupItem(DrawingRenderer, Bounds)); + } + } } } diff --git a/src/EPPlus.Export.ImageRenderer/RenderItems/SvgRenderLineItem.cs b/src/EPPlus.Export.ImageRenderer/RenderItems/SvgRenderLineItem.cs index fe588d782..73b1526eb 100644 --- a/src/EPPlus.Export.ImageRenderer/RenderItems/SvgRenderLineItem.cs +++ b/src/EPPlus.Export.ImageRenderer/RenderItems/SvgRenderLineItem.cs @@ -111,6 +111,18 @@ internal override SvgRenderItem Clone(SvgShape svgDocument) CloneBase(clone); return clone; } + + internal SvgRenderLineItem Clone(DrawingBase baseItem) + { + var clone = new SvgRenderLineItem(baseItem, baseItem.Bounds); + clone.X1 = X1; + clone.Y1 = Y1; + clone.X2 = X2; + clone.Y2 = Y2; + CloneBase(clone); + return clone; + } + internal override void GetBounds(out double il, out double it, out double ir, out double ib) { il = X1; diff --git a/src/EPPlus.Export.ImageRenderer/Svg/Chart/ChartTypeDrawers/LineChartTypeDrawer.cs b/src/EPPlus.Export.ImageRenderer/Svg/Chart/ChartTypeDrawers/LineChartTypeDrawer.cs index dbe61acaa..327b3bf55 100644 --- a/src/EPPlus.Export.ImageRenderer/Svg/Chart/ChartTypeDrawers/LineChartTypeDrawer.cs +++ b/src/EPPlus.Export.ImageRenderer/Svg/Chart/ChartTypeDrawers/LineChartTypeDrawer.cs @@ -1,4 +1,5 @@ using EPPlus.Export.ImageRenderer.RenderItems.SvgItem; +using EPPlus.Graphics; using EPPlusImageRenderer.RenderItems; using EPPlusImageRenderer.Svg; using OfficeOpenXml.Drawing.Chart; @@ -19,6 +20,7 @@ internal LineChartTypeDrawer(SvgChart svgChart, ExcelChart chartType) : base(svg var isPercentStacked = Chart.IsTypePercentStacked(); var xValues = new List>(); var yValues = new List>(); + int serCounter = 0; foreach (ExcelLineChartSerie serie in chartType.Series) { @@ -30,9 +32,11 @@ internal LineChartTypeDrawer(SvgChart svgChart, ExcelChart chartType) : base(svg if (serie.HasDataLabel) { - var datalabel = new SvgChartSerieDataLabel(svgChart, serie.DataLabel, svgChart.Bounds, serie, xValue, yValue); + + var datalabel = new SvgChartSerieDataLabel(svgChart, serie.DataLabel, svgChart.Bounds, serie, xValue, yValue, serCounter); serieDataLabels.Add(datalabel); } + serCounter++; } if (Chart.IsTypeStacked()) @@ -115,8 +119,8 @@ private void AddLine(ExcelChart chartType, ExcelLineChartSerie serie, List Date: Mon, 9 Mar 2026 16:35:02 +0100 Subject: [PATCH 085/151] Fixed chart legend to be in pts instead of pixels --- src/EPPlus.Export.ImageRenderer/Svg/Chart/SvgChartLegend.cs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/EPPlus.Export.ImageRenderer/Svg/Chart/SvgChartLegend.cs b/src/EPPlus.Export.ImageRenderer/Svg/Chart/SvgChartLegend.cs index 9cd8482ff..d2a9c0101 100644 --- a/src/EPPlus.Export.ImageRenderer/Svg/Chart/SvgChartLegend.cs +++ b/src/EPPlus.Export.ImageRenderer/Svg/Chart/SvgChartLegend.cs @@ -33,9 +33,9 @@ internal class SvgChartLegend : SvgChartObject List _seriesHeadersMeasure =new List(); ITextMeasurer _ttMeasurer; - const int MarginExtra = 2; - const int MiddleMargin = 10; - const int LineLength = 28; + const float MarginExtra = 1.5f; + const float MiddleMargin = 7.5f; + const float LineLength = 21; internal SvgChartLegend(SvgChart sc) : base(sc) { _ttMeasurer = sc.Chart.WorkSheet._package.Settings.TextSettings.GenericTextMeasurerTrueType; From 9f2b83748d62f712de3558291da3c944e8f9ccf8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ossian=20Edstr=C3=B6m?= Date: Mon, 9 Mar 2026 17:01:04 +0100 Subject: [PATCH 086/151] Fixed datalabel when legend is null bug --- .../SvgPathTests.cs | 4 ++-- .../SvgItem/SvgChartSerieDataLabel.cs | 18 +++++++------- .../Svg/Chart/SvgChart.cs | 24 +++++++++++++++++++ .../Svg/Chart/SvgChartLegend.cs | 4 ++-- 4 files changed, 37 insertions(+), 13 deletions(-) diff --git a/src/EPPlus.Export.ImageRenderer.Test/SvgPathTests.cs b/src/EPPlus.Export.ImageRenderer.Test/SvgPathTests.cs index 4a5b17bca..b9f51f870 100644 --- a/src/EPPlus.Export.ImageRenderer.Test/SvgPathTests.cs +++ b/src/EPPlus.Export.ImageRenderer.Test/SvgPathTests.cs @@ -562,13 +562,13 @@ public void GenerateDataLabels() public void GenerateDataLabelsTrueMost() { ExcelPackage.License.SetNonCommercialOrganization("EPPlus Project"); - using (var p = OpenTemplatePackage("datalabelsSvgTrueMost.xlsx")) + using (var p = OpenTemplatePackage("datalabelsSvgTrueMostWithFill.xlsx")) { var c = p.Workbook.Worksheets[0].Drawings[0]; var renderer = new EPPlusImageRenderer.ImageRenderer(); var svg = renderer.RenderDrawingToSvg(c); - SaveTextFileToWorkbook($"svg\\datalabelsTrueMost.svg", svg); + SaveTextFileToWorkbook($"svg\\datalabelsSvgTrueMostWithFill.svg", svg); } } diff --git a/src/EPPlus.Export.ImageRenderer/RenderItems/SvgItem/SvgChartSerieDataLabel.cs b/src/EPPlus.Export.ImageRenderer/RenderItems/SvgItem/SvgChartSerieDataLabel.cs index c4aa026e4..2b5b6993c 100644 --- a/src/EPPlus.Export.ImageRenderer/RenderItems/SvgItem/SvgChartSerieDataLabel.cs +++ b/src/EPPlus.Export.ImageRenderer/RenderItems/SvgItem/SvgChartSerieDataLabel.cs @@ -40,22 +40,22 @@ public SvgChartSerieDataLabel(SvgChart chart, ExcelChartSerieDataLabel dlblSerie if(dlblSerie.ShowLegendKey) { - SvgChartLegend legendItem = null; if (chart.Legend == null) { - legendItem = dlblSerie.ShowLegendKey == false ? null : new SvgChartLegend(chart); + seriesIcon = chart.GetSeriesIcon(serie, index, maxBounds); } else { - legendItem = chart.Legend; - } - var seriesIconOrig = (SvgRenderLineItem)legendItem.SeriesIcon[index].SeriesIcon; - var clonedIcon = seriesIconOrig.Clone(chart); + var legendItem = chart.Legend; + var seriesIconOrig = (SvgRenderLineItem)legendItem.SeriesIcon[index].SeriesIcon; + var clonedIcon = seriesIconOrig.Clone(chart); + + clonedIcon.Y1 = 0; + clonedIcon.Y2 = 0; - clonedIcon.Y1 = 0; - clonedIcon.Y2 = 0; + seriesIcon = clonedIcon; + } - seriesIcon = clonedIcon; } for (int i = 0; i < serie.NumberOfItems; i++) diff --git a/src/EPPlus.Export.ImageRenderer/Svg/Chart/SvgChart.cs b/src/EPPlus.Export.ImageRenderer/Svg/Chart/SvgChart.cs index 761b50b3d..930aae032 100644 --- a/src/EPPlus.Export.ImageRenderer/Svg/Chart/SvgChart.cs +++ b/src/EPPlus.Export.ImageRenderer/Svg/Chart/SvgChart.cs @@ -12,9 +12,12 @@ Date Author Change *************************************************************************************************/ using EPPlus.Export.ImageRenderer.Svg.Chart; using EPPlus.Fonts.OpenType.Utils; +using EPPlus.Graphics; using EPPlusImageRenderer.RenderItems; using EPPlusImageRenderer.Utils; +using OfficeOpenXml.Drawing; using OfficeOpenXml.Drawing.Chart; +using OfficeOpenXml.Interfaces.Drawing.Text; using System; using System.Collections.Generic; using System.Drawing; @@ -217,5 +220,26 @@ internal double GetPlotAreaTop() } } + + + internal SvgRenderLineItem GetSeriesIcon(ExcelChartStandardSerie s, int index, BoundingBox parentItem) + { + const float MarginExtra = 1.5f; + const float LineLength = 21; + + var item = new SvgRenderLineItem(this, parentItem); + item.SetDrawingPropertiesFill(s.Fill, this.Chart.StyleManager.Style.SeriesLine.FillReference.Color); + item.SetDrawingPropertiesBorder(s.Border, this.Chart.StyleManager.Style.SeriesLine.BorderReference.Color, s.Border.Fill.Style != eFillStyle.NoFill, 0.75); + + float y = (float)parentItem.Top + MarginExtra; + float x = 0; + item.X1 = x; + item.Y1 = y; + item.X2 = x + LineLength; + item.Y2 = y; + item.LineCap = eLineCap.Round; + + return item; + } } } \ No newline at end of file diff --git a/src/EPPlus.Export.ImageRenderer/Svg/Chart/SvgChartLegend.cs b/src/EPPlus.Export.ImageRenderer/Svg/Chart/SvgChartLegend.cs index d2a9c0101..cad69e96a 100644 --- a/src/EPPlus.Export.ImageRenderer/Svg/Chart/SvgChartLegend.cs +++ b/src/EPPlus.Export.ImageRenderer/Svg/Chart/SvgChartLegend.cs @@ -36,10 +36,10 @@ internal class SvgChartLegend : SvgChartObject const float MarginExtra = 1.5f; const float MiddleMargin = 7.5f; const float LineLength = 21; - internal SvgChartLegend(SvgChart sc) : base(sc) + internal SvgChartLegend(SvgChart sc, bool isDataLabelLegend = false) : base(sc) { _ttMeasurer = sc.Chart.WorkSheet._package.Settings.TextSettings.GenericTextMeasurerTrueType; - if (sc.Chart.HasLegend == false || sc.Chart.Series.Count == 0) + if (sc.Chart.HasLegend == false && isDataLabelLegend == false || sc.Chart.Series.Count == 0) { return; } From bdb164e79eab3596ed573ee85359226f2ea1c526 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jan=20K=C3=A4llman?= Date: Tue, 10 Mar 2026 08:50:48 +0100 Subject: [PATCH 087/151] WIP:Added new property LabelAlignment to ExcelAxisStandard --- .../SvgPathTests.cs | 22 ++-- .../ImageRenderer.cs | 12 --- .../RenderItems/Shared/ParagraphItem.cs | 100 +++++++++--------- .../RenderItems/Shared/TextBodyItem.cs | 1 - .../RenderItems/SvgItem/SvgTextBox.cs | 27 ++++- .../ChartTypeDrawers/LineChartTypeDrawer.cs | 14 +-- .../Svg/Chart/SvgChartAxis.cs | 27 +++-- .../Drawing/Chart/ExcelChartAxisStandard.cs | 52 +++++++++ .../Drawing/Chart/eAxisLabelAlignment.cs | 33 ++++++ 9 files changed, 197 insertions(+), 91 deletions(-) create mode 100644 src/EPPlus/Drawing/Chart/eAxisLabelAlignment.cs diff --git a/src/EPPlus.Export.ImageRenderer.Test/SvgPathTests.cs b/src/EPPlus.Export.ImageRenderer.Test/SvgPathTests.cs index a352b1d5f..7e8a04f83 100644 --- a/src/EPPlus.Export.ImageRenderer.Test/SvgPathTests.cs +++ b/src/EPPlus.Export.ImageRenderer.Test/SvgPathTests.cs @@ -513,7 +513,7 @@ public void GenerateSvgForCharts() var ws = p.Workbook.Worksheets[0]; var renderer = new EPPlusImageRenderer.ImageRenderer(); - //var ix = 3; + //var ix = 2; //var c = ws.Drawings[ix]; //var svg = renderer.RenderDrawingToSvg(c); //SaveTextFileToWorkbook($"svg\\ChartForSvg_ind{ix++}.svg", svg); @@ -643,16 +643,16 @@ public void GenerateSvgForLineCharts() { var ws = p.Workbook.Worksheets[0]; var renderer = new EPPlusImageRenderer.ImageRenderer(); - //var ix = 2; - //var c = ws.Drawings[ix]; - //var svg = renderer.RenderDrawingToSvg(c); - //SaveTextFileToWorkbook($"svg\\LineChartForSvg_Single{ix++}.svg", svg); - var ix = 1; - foreach (ExcelChart c in ws.Drawings) - { - var svg = renderer.RenderDrawingToSvg(c); - SaveTextFileToWorkbook($"svg\\LineChartForSvg{ix++}.svg", svg); - } + var ix = 2; + var c = ws.Drawings[ix]; + var svg = renderer.RenderDrawingToSvg(c); + SaveTextFileToWorkbook($"svg\\LineChartForSvg_Single{ix++}.svg", svg); + //var ix = 1; + //foreach (ExcelChart c in ws.Drawings) + //{ + // var svg = renderer.RenderDrawingToSvg(c); + // SaveTextFileToWorkbook($"svg\\LineChartForSvg{ix++}.svg", svg); + //} } } [TestMethod] diff --git a/src/EPPlus.Export.ImageRenderer/ImageRenderer.cs b/src/EPPlus.Export.ImageRenderer/ImageRenderer.cs index f6cdbeeb2..74f6d652f 100644 --- a/src/EPPlus.Export.ImageRenderer/ImageRenderer.cs +++ b/src/EPPlus.Export.ImageRenderer/ImageRenderer.cs @@ -11,26 +11,14 @@ Date Author Change 27/11/2025 EPPlus Software AB EPPlus 9 *************************************************************************************************/ -using EPPlus.Export.ImageRenderer; -using EPPlus.Export.ImageRenderer.RenderItems.Independent.Shared; -using EPPlus.Export.ImageRenderer.RenderItems.Independent.SvgItem; -using EPPlus.Export.ImageRenderer.RenderItems.Shared; -using EPPlus.Export.ImageRenderer.RenderItems.SvgItem; -using EPPlus.Export.ImageRenderer.Svg; using EPPlus.Export.ImageRenderer.Svg.NodeAttributes; using EPPlus.Export.ImageRenderer.Svg.Writer; -using EPPlus.Fonts.OpenType; -using EPPlus.Fonts.OpenType.Utils; using EPPlus.Graphics; using EPPlusImageRenderer.Svg; -using OfficeOpenXml; using OfficeOpenXml.Drawing; using OfficeOpenXml.Drawing.Chart; -using OfficeOpenXml.Drawing.Theme; -using OfficeOpenXml.FormulaParsing.Excel.Functions; using OfficeOpenXml.Utils; using System; -using System.Drawing; using System.IO; using System.Text; diff --git a/src/EPPlus.Export.ImageRenderer/RenderItems/Shared/ParagraphItem.cs b/src/EPPlus.Export.ImageRenderer/RenderItems/Shared/ParagraphItem.cs index ca9d263d9..ab86419ac 100644 --- a/src/EPPlus.Export.ImageRenderer/RenderItems/Shared/ParagraphItem.cs +++ b/src/EPPlus.Export.ImageRenderer/RenderItems/Shared/ParagraphItem.cs @@ -247,7 +247,7 @@ private void AddLinesAndTextRuns(ExcelDrawingParagraph p, string textIfEmpty) var maxWidth = ParentTextBody.MaxWidth + 0.001; //TODO: fix for equal width issue; lines = measurer.MeasureAndWrapTextLines_New(textIfEmpty, p.DefaultRunProperties.GetMeasureFont(), maxWidth); - Bounds.Width = maxWidth; + //Bounds.Width = maxWidth; if (HorizontalAlignment != eTextAlignment.Center) { Bounds.Left = GetAlignmentHorizontal(HorizontalAlignment); @@ -269,7 +269,7 @@ private void AddLinesAndTextRuns(ExcelDrawingParagraph p, string textIfEmpty) //This could be moved into a textLines collection class //START var idxOfLargestLine = 0; - double widthOfLargestLine = lines[0].Width; + double widthOfLargestLine = lines[0].GetWidthWithoutTrailingSpaces(); for (int i = 1; i < lines.Count; i++) { @@ -282,72 +282,72 @@ private void AddLinesAndTextRuns(ExcelDrawingParagraph p, string textIfEmpty) } //END - if (HorizontalAlignment == eTextAlignment.Center && ParentTextBody.AutoSize) + if (ParentTextBody.AutoSize) { //Bounds of the paragraph should be bounds of the text itself. //Therefore we must know the starting point to set accurate left and offset from left. - Bounds.Left = _centerAdjustment.Value - (widthOfLargestLine / 2); + Bounds.Left = 0; } - foreach (var line in lines) - { - double prevWidth = 0; - - if (HorizontalAlignment == eTextAlignment.Center) + foreach (var line in lines) { - var ctrLineWidth = line.GetWidthWithoutTrailingSpaces(); - - //var LeftForLargestLine = _centerAdjustment.Value - (widthOfLargestLine / 2); - //var LeftForCurrentLine = _centerAdjustment.Value - (ctrLineWidth / 2); + double prevWidth = 0; - //Calculate distance/offset between the Left of this paragraph(LeftForLargestLine) and the Left of currentLine - //prevWidth = LeftForCurrentLine - LeftForLargestLine; + if (HorizontalAlignment == eTextAlignment.Center) + { + var ctrLineWidth = line.GetWidthWithoutTrailingSpaces(); - //Calculate difference in widths and split to get offset between leftmost position and current line - prevWidth = (widthOfLargestLine - ctrLineWidth)/2; - } - else if (HorizontalAlignment == eTextAlignment.Right) - { - //Note that the actual bounds with the space will be outside max bounds. - //This appears to be how excel does it - var ctrLineWidth = line.GetWidthWithoutTrailingSpaces(); - prevWidth = widthOfLargestLine - ctrLineWidth; - } + //var LeftForLargestLine = _centerAdjustment.Value - (widthOfLargestLine / 2); + //var LeftForCurrentLine = _centerAdjustment.Value - (ctrLineWidth / 2); - if (lineSpacingIsExact == false) - { - runLineSpacing += line.LargestAscent + lastDescent; - } - else - { - runLineSpacing += ParagraphLineSpacing; - } - if (line.GetWidthWithoutTrailingSpaces() > greatestWidth) - { - greatestWidth = line.GetWidthWithoutTrailingSpaces(); - } + //Calculate distance/offset between the Left of this paragraph(LeftForLargestLine) and the Left of currentLine + //prevWidth = LeftForCurrentLine - LeftForLargestLine; - foreach (var lineFragment in line.LineFragments) - { - var displayText = line.GetLineFragmentText(lineFragment); + //Calculate difference in widths and split to get offset between leftmost position and current line + prevWidth = (widthOfLargestLine - ctrLineWidth) / 2; + } + else if (HorizontalAlignment == eTextAlignment.Right) + { + //Note that the actual bounds with the space will be outside max bounds. + //This appears to be how excel does it + var ctrLineWidth = line.GetWidthWithoutTrailingSpaces(); + prevWidth = widthOfLargestLine - ctrLineWidth; + } - if (p.TextRuns.Count == 0 && string.IsNullOrEmpty(textIfEmpty) == false) + if (lineSpacingIsExact == false) { - AddText(displayText, p.DefaultRunProperties, prevWidth); + runLineSpacing += line.LargestAscent + lastDescent; } else { - AddRenderItemTextRun(p.TextRuns[lineFragment.RtFragIdx], displayText, prevWidth); + runLineSpacing += ParagraphLineSpacing; + } + if (line.GetWidthWithoutTrailingSpaces() > greatestWidth) + { + greatestWidth = line.GetWidthWithoutTrailingSpaces(); } - TextRunItem runItem = Runs.Last(); - runItem.YPosition = runLineSpacing; - - runItem.Bounds.Width = lineFragment.Width; - prevWidth += lineFragment.Width; + foreach (var lineFragment in line.LineFragments) + { + var displayText = line.GetLineFragmentText(lineFragment); + + if (p.TextRuns.Count == 0 && string.IsNullOrEmpty(textIfEmpty) == false) + { + AddText(displayText, p.DefaultRunProperties, prevWidth); + } + else + { + AddRenderItemTextRun(p.TextRuns[lineFragment.RtFragIdx], displayText, prevWidth); + } + + TextRunItem runItem = Runs.Last(); + runItem.YPosition = runLineSpacing; + + runItem.Bounds.Width = lineFragment.Width; + prevWidth += lineFragment.Width; + } + lastDescent = line.LargestDescent; } - lastDescent = line.LargestDescent; - } } Bounds.Height = runLineSpacing + lastDescent; Bounds.Width = greatestWidth; diff --git a/src/EPPlus.Export.ImageRenderer/RenderItems/Shared/TextBodyItem.cs b/src/EPPlus.Export.ImageRenderer/RenderItems/Shared/TextBodyItem.cs index 2e95f087d..16ee07833 100644 --- a/src/EPPlus.Export.ImageRenderer/RenderItems/Shared/TextBodyItem.cs +++ b/src/EPPlus.Export.ImageRenderer/RenderItems/Shared/TextBodyItem.cs @@ -90,7 +90,6 @@ public void ImportParagraph(ExcelDrawingParagraph item, double startingY, string } } Paragraphs.Add(paragraph); - //SetHorizontalAlignmentPosition(); } public void AddParagraph(double startingY, string text = null) diff --git a/src/EPPlus.Export.ImageRenderer/RenderItems/SvgItem/SvgTextBox.cs b/src/EPPlus.Export.ImageRenderer/RenderItems/SvgItem/SvgTextBox.cs index 8039c56b9..98f83cd0a 100644 --- a/src/EPPlus.Export.ImageRenderer/RenderItems/SvgItem/SvgTextBox.cs +++ b/src/EPPlus.Export.ImageRenderer/RenderItems/SvgItem/SvgTextBox.cs @@ -15,10 +15,9 @@ internal class SvgTextBox : DrawingObjectNoBounds { internal SvgTextBox(DrawingBase renderer, BoundingBox parent, double left, double top, double width, double height, double maxWidth = double.NaN, double maxHeight = double.NaN) : base(renderer) { + Init(renderer, parent, maxWidth, maxHeight); Left = left; Top = top; - - Init(renderer, parent, maxWidth, maxHeight); } private void Init(DrawingBase renderer, BoundingBox parent, double maxWidth, double maxHeight) @@ -51,8 +50,28 @@ public SvgRenderItem Rectangle } } public SvgTextBodyItem TextBody {get;set;} - public double Left { get; set; } - public double Top { get; set; } + public double Left + { + get + { + return TextBody.Bounds.Left - LeftMargin; + } + set + { + TextBody.Bounds.Left = value + LeftMargin; + } + } + public double Top + { + get + { + return TextBody.Bounds.Top - TopMargin; + } + set + { + TextBody.Bounds.Top = value + TopMargin; + } + } public double Width { get diff --git a/src/EPPlus.Export.ImageRenderer/Svg/Chart/ChartTypeDrawers/LineChartTypeDrawer.cs b/src/EPPlus.Export.ImageRenderer/Svg/Chart/ChartTypeDrawers/LineChartTypeDrawer.cs index b31de8c4f..f53943c88 100644 --- a/src/EPPlus.Export.ImageRenderer/Svg/Chart/ChartTypeDrawers/LineChartTypeDrawer.cs +++ b/src/EPPlus.Export.ImageRenderer/Svg/Chart/ChartTypeDrawers/LineChartTypeDrawer.cs @@ -91,14 +91,14 @@ private void AddLine(ExcelChart chartType, ExcelLineChartSerie serie, List GetAxisValueTextBoxes() else { maxWidth = Rectangle.Width / AxisValues.Count; - maxHeight = SvgChart.ChartArea.Bounds.Height / 3; //TODO: Check this value. + maxHeight = SvgChart.ChartArea.Rectangle.Height / 3; //TODO: Check this value. } double widest=0; for (var i = 0; i < AxisValues.Count; i++) @@ -301,10 +302,6 @@ private List GetAxisValueTextBoxes() var m = tm.MeasureText(v, mf); var x = GetAxisItemLeft(i, m); var y = GetAxisItemTop(i, m); - //var tb = new TextBox(Chart, x, y, m.Width, m.Height); - //var bounds = new BoundingBox(); - //bounds.Left = x; - //bounds.Top = y; var width = m.Width.PointToPixel(); var height = m.Height.PointToPixel(); var tb = new SvgTextBox(SvgChart, Rectangle.Bounds, x, y, width, height, maxWidth, maxHeight); @@ -318,7 +315,6 @@ private List GetAxisValueTextBoxes() p.HorizontalAlignment = eTextAlignment.Center; } - //tb.ImportTextBody(Axis.TextBody); tb.TextBody.ImportParagraph(p, 0, v); //tb.TextBody.Paragraphs[0].AddText(v, Axis.Font); @@ -339,6 +335,25 @@ private List GetAxisValueTextBoxes() tb.Left += (widest - tb.Width); } } + else + { + var lblAlignment = (Axis as ExcelChartAxisStandard)?.LabelAlignment??OfficeOpenXml.eAxisLabelAlignment.Center; + var majorWidth = Rectangle.Width / AxisValues.Count; + foreach (var tb in ret) + { + switch (lblAlignment) + { + case OfficeOpenXml.eAxisLabelAlignment.Left: + break; + case OfficeOpenXml.eAxisLabelAlignment.Center: + tb.Left += majorWidth / 2 - tb.Width / 2; + break; + case OfficeOpenXml.eAxisLabelAlignment.Right: + tb.Left += majorWidth - tb.Width; + break; + } + } + } return ret; } diff --git a/src/EPPlus/Drawing/Chart/ExcelChartAxisStandard.cs b/src/EPPlus/Drawing/Chart/ExcelChartAxisStandard.cs index 12793c96c..2e513eff9 100644 --- a/src/EPPlus/Drawing/Chart/ExcelChartAxisStandard.cs +++ b/src/EPPlus/Drawing/Chart/ExcelChartAxisStandard.cs @@ -21,6 +21,7 @@ Date Author Change using System.Collections.Generic; using System.Globalization; using System.Linq; +using System.Reflection.Emit; using System.Xml; using static OfficeOpenXml.Style.XmlAccess.ExcelNumberFormatXml; namespace OfficeOpenXml.Drawing.Chart @@ -387,6 +388,57 @@ public override eTickLabelPosition TickLabelPosition SetXmlNodeString(_ticLblPos_Path, v); } } + const string _label_alignment_path = "c:lblAlgn/@val"; + /// + /// Set the alingment for the axis labels within the major tickmarks. + /// + public eAxisLabelAlignment LabelAlignment + { + get + { + switch(GetXmlNodeString(_label_alignment_path)) + { + case "l": + return eAxisLabelAlignment.Left; + case "r": + return eAxisLabelAlignment.Right; + default: + return eAxisLabelAlignment.Center; + } + } + set + { + string v; + switch (value) + { + case eAxisLabelAlignment.Left: + v = "l"; + break; + case eAxisLabelAlignment.Right: + v = "r"; + break; + default: + v = "ctr"; + break; + } + SetXmlNodeString(_label_alignment_path, v); + } + } + const string _label_offset_path = "c:lblOffset/@val"; + /// + /// Set the offset in whole percent between the labels and the axis. + /// + public int LabelOffset + { + get + { + return GetXmlNodeInt(_label_offset_path, 100); + } + set + { + SetXmlNodeInt(_label_offset_path, value, null, false); + } + } const string _displayUnitPath = "c:dispUnits/c:builtInUnit/@val"; const string _custUnitPath = "c:dispUnits/c:custUnit/@val"; /// diff --git a/src/EPPlus/Drawing/Chart/eAxisLabelAlignment.cs b/src/EPPlus/Drawing/Chart/eAxisLabelAlignment.cs new file mode 100644 index 000000000..7ecf91a74 --- /dev/null +++ b/src/EPPlus/Drawing/Chart/eAxisLabelAlignment.cs @@ -0,0 +1,33 @@ +/************************************************************************************************* + Required Notice: Copyright (C) EPPlus Software AB. + This software is licensed under PolyForm Noncommercial License 1.0.0 + and may only be used for noncommercial purposes + https://polyformproject.org/licenses/noncommercial/1.0.0/ + + A commercial license to use this software can be purchased at https://epplussoftware.com + ************************************************************************************************* + Date Author Change + ************************************************************************************************* + 04/22/2020 EPPlus Software AB Added this class + *************************************************************************************************/ +namespace OfficeOpenXml +{ + /// + /// How the axis label should be alignted within the major tickmarks. + /// + public enum eAxisLabelAlignment + { + /// + /// The text shall be centered + /// + Left, + /// + /// The text shall be left justified. + /// + Center, + /// + /// The text shall be right justified. + /// + Right + } +} \ No newline at end of file From 6397c5ef64489f0ad20c6d49dc685a6d87d4258f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ossian=20Edstr=C3=B6m?= Date: Tue, 10 Mar 2026 10:16:56 +0100 Subject: [PATCH 088/151] Added standalone test options --- .../ImageRenderer.cs | 5 + .../RenderItems/Shared/ParagraphItem.cs | 124 ++++++++++++++++-- .../RenderItems/Shared/TextBodyItem.cs | 23 +++- .../RenderItems/Shared/TextRunItem.cs | 63 ++++++--- .../RenderItems/SvgItem/SvgParagraphItem.cs | 7 + .../RenderItems/SvgItem/SvgTextBox.cs | 5 + .../RenderItems/SvgItem/SvgTextRunItem.cs | 5 + 7 files changed, 202 insertions(+), 30 deletions(-) diff --git a/src/EPPlus.Export.ImageRenderer/ImageRenderer.cs b/src/EPPlus.Export.ImageRenderer/ImageRenderer.cs index c6ea2f1b9..7c84a7b0a 100644 --- a/src/EPPlus.Export.ImageRenderer/ImageRenderer.cs +++ b/src/EPPlus.Export.ImageRenderer/ImageRenderer.cs @@ -55,6 +55,11 @@ public string RenderDrawingToSvg(ExcelDrawing drawing) throw new NotImplementedException("Image rendering for drawing type not implemented."); } + //public string RenderBaseItemToSvg(DrawingBase drawing) + //{ + + //} + ////Attempt at ensuring features can be created/tested individually by the system ////Without relying on having a whole workbook or epplus project. ////Simply: Does our positioning, sizing and parent hierarchy logic work as expected or not. diff --git a/src/EPPlus.Export.ImageRenderer/RenderItems/Shared/ParagraphItem.cs b/src/EPPlus.Export.ImageRenderer/RenderItems/Shared/ParagraphItem.cs index ca9d263d9..a6a8dcaa5 100644 --- a/src/EPPlus.Export.ImageRenderer/RenderItems/Shared/ParagraphItem.cs +++ b/src/EPPlus.Export.ImageRenderer/RenderItems/Shared/ParagraphItem.cs @@ -17,6 +17,7 @@ using System.Linq; using System.Text; using System.Text.RegularExpressions; +using static System.Net.Mime.MediaTypeNames; using EPPlusColorConverter = OfficeOpenXml.Utils.TypeConversion.ColorConverter; namespace EPPlus.Export.ImageRenderer.RenderItems.Shared @@ -53,6 +54,9 @@ public ParagraphItem(TextBodyItem textBody, DrawingBase renderer, BoundingBox pa Bounds.Name = "Paragraph"; var defaultFont = new MeasurementFont { FontFamily = "Aptos Narrow", Size = 11, Style = MeasurementFontStyles.Regular }; _paragraphFont = defaultFont; + + _measurer = new FontMeasurerTrueType(defaultFont); + ParagraphLineSpacing = GetParagraphLineSpacingInPoints(100, _measurer); } public ParagraphItem(TextBodyItem textBody, DrawingBase renderer, BoundingBox parent, ExcelDrawingParagraph p, string textIfEmpty=null) : base(renderer, parent) @@ -167,6 +171,14 @@ internal protected TextRunItem AddRenderItemTextRun(ExcelParagraphTextRunBase or return targetTxtRun; } + public void AddText(string text, double prevWidth) + { + var container = CreateTextRun(_paragraphFont, Bounds, text); + Runs.Add(container); + + container.Bounds.Name = $"Container{Runs.Count}"; + container.Bounds.Left = prevWidth; + } public void AddText(string text, ExcelTextFont font, bool isOld) { var measurer = new FontMeasurerTrueType(); @@ -194,6 +206,17 @@ public void AddText(string text, ExcelTextFont font, double prevWidth) container.Bounds.Left = prevWidth; } + void GenerateTextFragments(string text) + { + _newTextFragments = new List(); + + if (string.IsNullOrEmpty(text) == false) + { + var currentFrag = new TextFragment() { Text = text, Font = _paragraphFont}; + _newTextFragments.Add(currentFrag); + } + } + /// /// Log linebreak positions and sizes of the runs /// So that we can easily know what textfragment is on what line and what size it has later @@ -215,7 +238,7 @@ void GenerateTextFragments(ExcelDrawingTextRunCollection runs) fontSizes.Add(runFont.Size); } - _textFragments = new TextFragmentCollection(runContents, fontSizes); + //_textFragments = new TextFragmentCollection(runContents, fontSizes); _newTextFragments = new List(); @@ -229,6 +252,97 @@ void GenerateTextFragments(ExcelDrawingTextRunCollection runs) } } + internal void AddLinesAndTextRuns(string textIfEmpty) + { + GenerateTextFragments(textIfEmpty); + var lines = new List(); + + var measurer = new FontMeasurerTrueType(); + var maxWidth = ParentTextBody.MaxWidth + 0.001; //TODO: fix for equal width issue; + lines = measurer.MeasureAndWrapTextLines_New(textIfEmpty, _paragraphFont, maxWidth); + bool lineSpacingIsExact = _lsMultiplier.HasValue == false; + double runLineSpacing = 0; + double greatestWidth = 0; + //In points + double lastDescent = 0; + + if (lines != null && lines.Count != 0) + { + //This could be moved into a textLines collection class + //START + var idxOfLargestLine = 0; + double widthOfLargestLine = lines[0].Width; + + for (int i = 1; i < lines.Count; i++) + { + if (lines[i].Width > widthOfLargestLine) + { + var ctrLineWidth = lines[i].GetWidthWithoutTrailingSpaces(); + widthOfLargestLine = ctrLineWidth; + idxOfLargestLine = i; + } + } + //END + + if (HorizontalAlignment == eTextAlignment.Center && ParentTextBody.AutoSize) + { + //Bounds of the paragraph should be bounds of the text itself. + //Therefore we must know the starting point to set accurate left and offset from left. + Bounds.Left = _centerAdjustment.Value - (widthOfLargestLine / 2); + } + + foreach (var line in lines) + { + double prevWidth = 0; + + if (HorizontalAlignment == eTextAlignment.Center) + { + var ctrLineWidth = line.GetWidthWithoutTrailingSpaces(); + prevWidth = (widthOfLargestLine - ctrLineWidth) / 2; + } + else if (HorizontalAlignment == eTextAlignment.Right) + { + //Note that the actual bounds with the space will be outside max bounds. + //This appears to be how excel does it + var ctrLineWidth = line.GetWidthWithoutTrailingSpaces(); + prevWidth = widthOfLargestLine - ctrLineWidth; + } + + if (lineSpacingIsExact == false) + { + runLineSpacing += line.LargestAscent + lastDescent; + } + else + { + runLineSpacing += ParagraphLineSpacing; + } + if (line.GetWidthWithoutTrailingSpaces() > greatestWidth) + { + greatestWidth = line.GetWidthWithoutTrailingSpaces(); + } + + foreach (var lineFragment in line.LineFragments) + { + var displayText = line.GetLineFragmentText(lineFragment); + + if (string.IsNullOrEmpty(textIfEmpty) == false) + { + AddText(displayText, prevWidth); + } + + TextRunItem runItem = Runs.Last(); + runItem.YPosition = runLineSpacing; + + runItem.Bounds.Width = lineFragment.Width; + prevWidth += lineFragment.Width; + } + lastDescent = line.LargestDescent; + } + } + Bounds.Height = runLineSpacing + lastDescent; + Bounds.Width = greatestWidth; + } + private void AddLinesAndTextRuns(ExcelDrawingParagraph p, string textIfEmpty) { //Log line positions and run sizes @@ -296,13 +410,6 @@ private void AddLinesAndTextRuns(ExcelDrawingParagraph p, string textIfEmpty) if (HorizontalAlignment == eTextAlignment.Center) { var ctrLineWidth = line.GetWidthWithoutTrailingSpaces(); - - //var LeftForLargestLine = _centerAdjustment.Value - (widthOfLargestLine / 2); - //var LeftForCurrentLine = _centerAdjustment.Value - (ctrLineWidth / 2); - - //Calculate distance/offset between the Left of this paragraph(LeftForLargestLine) and the Left of currentLine - //prevWidth = LeftForCurrentLine - LeftForLargestLine; - //Calculate difference in widths and split to get offset between leftmost position and current line prevWidth = (widthOfLargestLine - ctrLineWidth)/2; } @@ -396,5 +503,6 @@ internal double GetAlignmentHorizontal(eTextAlignment txAlignment) /// internal abstract TextRunItem CreateTextRun(ExcelParagraphTextRunBase run, BoundingBox parent, string displayText); internal abstract TextRunItem CreateTextRun(string text, ExcelTextFont font, BoundingBox parent, string displayText); + internal abstract TextRunItem CreateTextRun(MeasurementFont font, BoundingBox parent, string displayText); } } diff --git a/src/EPPlus.Export.ImageRenderer/RenderItems/Shared/TextBodyItem.cs b/src/EPPlus.Export.ImageRenderer/RenderItems/Shared/TextBodyItem.cs index 111e1489d..93be545ae 100644 --- a/src/EPPlus.Export.ImageRenderer/RenderItems/Shared/TextBodyItem.cs +++ b/src/EPPlus.Export.ImageRenderer/RenderItems/Shared/TextBodyItem.cs @@ -94,7 +94,28 @@ public void ImportParagraph(ExcelDrawingParagraph item, double startingY, string public void AddParagraph(double startingY, string text = null) { - CreateParagraph(this, Bounds, text); + var paragraph = CreateParagraph(this, Bounds, text); + paragraph.Bounds.Name = $"Container{Paragraphs.Count}"; + paragraph.Bounds.Top = startingY; + _text = text; + + if (AutoSize) + { + if (Paragraphs.Count == 0) + { + Bounds.Height = paragraph.Bounds.Height; + } + else + { + Bounds.Height += paragraph.Bounds.Height; + } + + if (Bounds.Width < paragraph.Bounds.Width || (Bounds.Width == MaxWidth && Paragraphs.Count == 0)) + { + Bounds.Width = paragraph.Bounds.Width; + } + } + Paragraphs.Add(paragraph); } internal void SetHorizontalAlignmentPosition() diff --git a/src/EPPlus.Export.ImageRenderer/RenderItems/Shared/TextRunItem.cs b/src/EPPlus.Export.ImageRenderer/RenderItems/Shared/TextRunItem.cs index 0f8c8b872..e4f0dd09f 100644 --- a/src/EPPlus.Export.ImageRenderer/RenderItems/Shared/TextRunItem.cs +++ b/src/EPPlus.Export.ImageRenderer/RenderItems/Shared/TextRunItem.cs @@ -4,16 +4,12 @@ using EPPlusImageRenderer; using EPPlusImageRenderer.RenderItems; using OfficeOpenXml.Drawing; -using OfficeOpenXml.Drawing.Theme; -using OfficeOpenXml.FormulaParsing.Excel.Functions.MathFunctions; using OfficeOpenXml.Interfaces.Drawing.Text; using OfficeOpenXml.Style; using OfficeOpenXml.Utils; -using System; using System.Collections.Generic; using System.Drawing; using System.Linq; -using System.Text; using System.Text.RegularExpressions; namespace EPPlus.Export.ImageRenderer.RenderItems.Shared { @@ -26,18 +22,11 @@ internal abstract class TextRunItem : RenderItem internal protected MeasurementFont _measurementFont; internal protected bool _isFirstInParagraph; - FontMeasurerTrueType _measurer; - eTextAlignment _horizontalTextAlignment; - MeasurementFontStyles _fontStyles; + internal double FontSizeInPixels { get; private set; } public List Lines { get; private set; } - /// - /// Aka total Delta Y - /// - double _yEndPos; - protected internal bool _isItalic = false; protected internal bool _isBold = false; protected internal eUnderLineType _underLineType; @@ -45,13 +34,45 @@ internal abstract class TextRunItem : RenderItem protected internal Color _underlineColor; internal double YPosition { get; set; } - //internal double LineSpacingPerNewLine { get; set; } - //internal double BaseLineSpacing { get; set; } - - //internal List YIncreasePerLine { get; private set; } = new List(); - //internal List PerLineWidth { get; private set; } = new List(); internal double ClippingHeight = double.NaN; + internal TextRunItem(DrawingBase renderer, BoundingBox parent, MeasurementFont font, string displayText) : base(renderer, parent) + { + _originalText = displayText; + + Bounds.Name = "TextRun"; + _currentText = displayText; + + Lines = Regex.Split(_currentText, "\r\n|\r|\n").ToList(); + + _measurementFont = font; + _isFirstInParagraph = true; + + FontSizeInPixels = ((double)_measurementFont.Size).PointToPixel(true); + Bounds.Height = _measurementFont.Size; + if (parent.Height < _measurementFont.Size) + { + parent.Height = _measurementFont.Size; + } + + //To get clipping height we need to get the textbody bounds + if (parent != null && parent.Parent != null && parent.Parent.Parent != null) + { + ClippingHeight = parent.Parent.Parent.Position.Y + parent.Parent.Parent.Size.Y; + } + if (Lines.Count == 1) + { + //Bounds.Width = parent.Width; + GetBounds(out double il, out double it, out double ir, out double ib); //TODO: remove when calc works + } + else + { + //Measure text. + GetBounds(out double il, out double it, out double ir, out double ib); //TODO: remove when calc works + } + _underLineType = eUnderLineType.None; + } + internal TextRunItem(DrawingBase renderer, BoundingBox parent, string text, ExcelTextFont font, string displayText) : base(renderer, parent) { _originalText = text; @@ -68,7 +89,7 @@ internal TextRunItem(DrawingBase renderer, BoundingBox parent, string text, Exce _isFirstInParagraph = true; - _fontStyles = _measurementFont.Style; + //_fontStyles = _measurementFont.Style; FontSizeInPixels = ((double)_measurementFont.Size).PointToPixel(true); Bounds.Height = _measurementFont.Size; @@ -76,7 +97,7 @@ internal TextRunItem(DrawingBase renderer, BoundingBox parent, string text, Exce { parent.Height = _measurementFont.Size; } - _horizontalTextAlignment = eTextAlignment.Center; + //_horizontalTextAlignment = eTextAlignment.Center; if (font.Fill.Style == eFillStyle.SolidFill) { @@ -122,12 +143,12 @@ internal TextRunItem(DrawingBase renderer, BoundingBox parent, ExcelParagraphTex _measurementFont = run.GetMeasurementFont(); - _fontStyles = _measurementFont.Style; + //_fontStyles = _measurementFont.Style; FontSizeInPixels = ((double)_measurementFont.Size).PointToPixel(true); Bounds.Height = _measurementFont.Size; - _horizontalTextAlignment = run.Paragraph.HorizontalAlignment; + //_horizontalTextAlignment = run.Paragraph.HorizontalAlignment; if (run.Fill.IsEmpty == false && run.Fill.Style == eFillStyle.SolidFill) { diff --git a/src/EPPlus.Export.ImageRenderer/RenderItems/SvgItem/SvgParagraphItem.cs b/src/EPPlus.Export.ImageRenderer/RenderItems/SvgItem/SvgParagraphItem.cs index 1da38a042..f4cd5d692 100644 --- a/src/EPPlus.Export.ImageRenderer/RenderItems/SvgItem/SvgParagraphItem.cs +++ b/src/EPPlus.Export.ImageRenderer/RenderItems/SvgItem/SvgParagraphItem.cs @@ -5,6 +5,7 @@ using EPPlusImageRenderer.RenderItems; using OfficeOpenXml.Drawing; using OfficeOpenXml.Drawing.Theme; +using OfficeOpenXml.Interfaces.Drawing.Text; using OfficeOpenXml.Style; using System.Globalization; using System.Text; @@ -21,6 +22,7 @@ public SvgParagraphItem(TextBodyItem textBody, DrawingBase renderer, BoundingBox public SvgParagraphItem(TextBodyItem textBody, DrawingBase renderer, BoundingBox parent, string text) : base(textBody, renderer, parent) { + AddLinesAndTextRuns(text); } public SvgParagraphItem(TextBodyItem textBody, DrawingBase renderer, BoundingBox parent, ExcelDrawingParagraph p, string textIfEmpty = null) : base(textBody, renderer, parent, p, textIfEmpty) @@ -157,5 +159,10 @@ internal override TextRunItem CreateTextRun(string text, ExcelTextFont font, Bou { return new SvgTextRunItem(DrawingRenderer, parent, text, font, displayText); } + + internal override TextRunItem CreateTextRun(MeasurementFont font, BoundingBox parent, string displayText) + { + return new SvgTextRunItem(DrawingRenderer, parent, font, displayText); + } } } diff --git a/src/EPPlus.Export.ImageRenderer/RenderItems/SvgItem/SvgTextBox.cs b/src/EPPlus.Export.ImageRenderer/RenderItems/SvgItem/SvgTextBox.cs index 63e98c911..5b8eec416 100644 --- a/src/EPPlus.Export.ImageRenderer/RenderItems/SvgItem/SvgTextBox.cs +++ b/src/EPPlus.Export.ImageRenderer/RenderItems/SvgItem/SvgTextBox.cs @@ -151,5 +151,10 @@ internal void ImportParagraph(ExcelDrawingParagraph item, double startingY, stri { TextBody.ImportParagraph(item, startingY, text); } + + internal void AddText(double startingY, string text = null) + { + TextBody.AddParagraph(startingY, text); + } } } diff --git a/src/EPPlus.Export.ImageRenderer/RenderItems/SvgItem/SvgTextRunItem.cs b/src/EPPlus.Export.ImageRenderer/RenderItems/SvgItem/SvgTextRunItem.cs index 73934f497..b4c229a30 100644 --- a/src/EPPlus.Export.ImageRenderer/RenderItems/SvgItem/SvgTextRunItem.cs +++ b/src/EPPlus.Export.ImageRenderer/RenderItems/SvgItem/SvgTextRunItem.cs @@ -6,6 +6,7 @@ using EPPlusImageRenderer.Svg; using OfficeOpenXml.Drawing; using OfficeOpenXml.Drawing.Theme; +using OfficeOpenXml.Interfaces.Drawing.Text; using OfficeOpenXml.Style; using System; using System.Globalization; @@ -15,6 +16,10 @@ namespace EPPlus.Export.ImageRenderer.RenderItems.SvgItem { internal class SvgTextRunItem : TextRunItem { + public SvgTextRunItem(DrawingBase renderer, BoundingBox parent, MeasurementFont font, string displayText) : base(renderer, parent, font, displayText) + { + } + public SvgTextRunItem(DrawingBase renderer, BoundingBox parent, ExcelParagraphTextRunBase run, string displayText = "") : base(renderer,parent, run, displayText) { } From 30d8a84d0fc519717720ed1e2a285901e391b94a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ossian=20Edstr=C3=B6m?= Date: Tue, 10 Mar 2026 10:44:17 +0100 Subject: [PATCH 089/151] Fix datalabel bug add testcase --- .../SvgPathTests.cs | 14 ++++++++++++++ src/EPPlus.Export.ImageRenderer/ImageRenderer.cs | 7 ++++++- .../RenderItems/SvgItem/SvgChartDatalabelItem.cs | 11 ----------- .../RenderItems/SvgItem/SvgChartSerieDataLabel.cs | 7 ++++++- 4 files changed, 26 insertions(+), 13 deletions(-) delete mode 100644 src/EPPlus.Export.ImageRenderer/RenderItems/SvgItem/SvgChartDatalabelItem.cs diff --git a/src/EPPlus.Export.ImageRenderer.Test/SvgPathTests.cs b/src/EPPlus.Export.ImageRenderer.Test/SvgPathTests.cs index b9f51f870..a20b0d4f9 100644 --- a/src/EPPlus.Export.ImageRenderer.Test/SvgPathTests.cs +++ b/src/EPPlus.Export.ImageRenderer.Test/SvgPathTests.cs @@ -572,6 +572,20 @@ public void GenerateDataLabelsTrueMost() } } + [TestMethod] + public void GenerateDatalabelsLeaderLines() + { + ExcelPackage.License.SetNonCommercialOrganization("EPPlus Project"); + using (var p = OpenTemplatePackage("datalabelsSvgLeaderLines.xlsx")) + { + var c = p.Workbook.Worksheets[0].Drawings[0]; + + var renderer = new EPPlusImageRenderer.ImageRenderer(); + var svg = renderer.RenderDrawingToSvg(c); + SaveTextFileToWorkbook($"svg\\datalabelsSvgLeaderLines.svg", svg); + } + } + [TestMethod] public void OpenRightAligned() diff --git a/src/EPPlus.Export.ImageRenderer/ImageRenderer.cs b/src/EPPlus.Export.ImageRenderer/ImageRenderer.cs index 7c84a7b0a..9f27e4406 100644 --- a/src/EPPlus.Export.ImageRenderer/ImageRenderer.cs +++ b/src/EPPlus.Export.ImageRenderer/ImageRenderer.cs @@ -55,6 +55,11 @@ public string RenderDrawingToSvg(ExcelDrawing drawing) throw new NotImplementedException("Image rendering for drawing type not implemented."); } + //public string RenderTestCanvas(double widthPixel, double heightPixel, Color bgColor) + //{ + + //} + //public string RenderBaseItemToSvg(DrawingBase drawing) //{ @@ -144,7 +149,7 @@ public string RenderDrawingToSvg(ExcelDrawing drawing) // //StreamReader reader = new StreamReader(ms); // //retStr = reader.ReadToEnd(); - + // ////SvgParagraph para = new SvgParagraph(container.GetContent(),); // ////var doc = new SvgEpplusDocument(); // //return retStr; diff --git a/src/EPPlus.Export.ImageRenderer/RenderItems/SvgItem/SvgChartDatalabelItem.cs b/src/EPPlus.Export.ImageRenderer/RenderItems/SvgItem/SvgChartDatalabelItem.cs deleted file mode 100644 index 3d45bba34..000000000 --- a/src/EPPlus.Export.ImageRenderer/RenderItems/SvgItem/SvgChartDatalabelItem.cs +++ /dev/null @@ -1,11 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; - -namespace EPPlus.Export.ImageRenderer.RenderItems.SvgItem -{ - internal class SvgChartDatalabelItem - { - } -} diff --git a/src/EPPlus.Export.ImageRenderer/RenderItems/SvgItem/SvgChartSerieDataLabel.cs b/src/EPPlus.Export.ImageRenderer/RenderItems/SvgItem/SvgChartSerieDataLabel.cs index 2b5b6993c..25813b540 100644 --- a/src/EPPlus.Export.ImageRenderer/RenderItems/SvgItem/SvgChartSerieDataLabel.cs +++ b/src/EPPlus.Export.ImageRenderer/RenderItems/SvgItem/SvgChartSerieDataLabel.cs @@ -118,10 +118,15 @@ internal override void AppendRenderItems(List renderItems) groupItems[i].Bounds.Left += (seriesIcon.Bounds.Width / 2); groupItems[i].GroupTransform = $"transform=\"translate({groupItems[i].Bounds.Left.PointToPixelString()}, {groupItems[i].Bounds.Top.PointToPixelString()})\""; dlblTextBoxes[i].Left += seriesIcon.Bounds.Width + dlblTextBoxes[i].LeftMargin; + renderItems.Add(groupItems[i]); + renderItems.Add(seriesIcon); + } + else + { + renderItems.Add(groupItems[i]); } renderItems.Add(groupItems[i]); - renderItems.Add(seriesIcon); dlblTextBoxes[i].AppendRenderItems(renderItems); renderItems.Add(new SvgEndGroupItem(DrawingRenderer, Bounds)); From 37cc55dc04d2364004c7f182e09cb553ce76400a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ossian=20Edstr=C3=B6m?= Date: Tue, 10 Mar 2026 10:44:53 +0100 Subject: [PATCH 090/151] Added independent testing class --- .../TextboxTest.cs | 65 +++++++++++++++++++ .../Svg/DrawingItemForTesting.cs | 58 +++++++++++++++++ 2 files changed, 123 insertions(+) create mode 100644 src/EPPlus.Export.ImageRenderer.Test/TextboxTest.cs create mode 100644 src/EPPlus.Export.ImageRenderer/Svg/DrawingItemForTesting.cs diff --git a/src/EPPlus.Export.ImageRenderer.Test/TextboxTest.cs b/src/EPPlus.Export.ImageRenderer.Test/TextboxTest.cs new file mode 100644 index 000000000..757f95f48 --- /dev/null +++ b/src/EPPlus.Export.ImageRenderer.Test/TextboxTest.cs @@ -0,0 +1,65 @@ +using EPPlus.Export.ImageRenderer.RenderItems.SvgItem; +using EPPlus.Export.ImageRenderer.Svg; +using EPPlus.Graphics; +using EPPlusImageRenderer.RenderItems; +using OfficeOpenXml.FormulaParsing.Excel.Functions.MathFunctions; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace EPPlus.Export.ImageRenderer.Tests +{ + [TestClass] + public class TextboxTest : TestBase + { + [TestMethod] + public void TextBoxVerification() + { + var baseBB = new BoundingBox(); + + //96x96 px + baseBB.Width = 72; + baseBB.Height = 72; + + var item = new DrawingItemForTesting(baseBB); + + BoundingBox maxBounds = new BoundingBox(); + maxBounds.Width = 36; + maxBounds.Height = 36; + + var txtBox = new SvgTextBox(item, item.Bounds, maxBounds); + txtBox.AddText(0, "My new text which is fun"); + txtBox.Rectangle.FillColor = "red"; + txtBox.Rectangle.FillOpacity = 0.2d; + + txtBox.Left = 5; + txtBox.Top = 5; + + item.ExternalRenderItemsNoBounds.Add(txtBox); + + var txtBoxNotMaxed = new SvgTextBox(item, item.Bounds, maxBounds); + txtBoxNotMaxed.Left = 42; + txtBoxNotMaxed.Top = 5; + + txtBoxNotMaxed.AddText(0, "abc"); + txtBoxNotMaxed.Rectangle.FillColor = "yellow"; + txtBoxNotMaxed.Rectangle.FillOpacity = 0.2d; + + item.ExternalRenderItemsNoBounds.Add(txtBoxNotMaxed); + + //Assert.AreEqual(36d, txtBox.TextBody.Width); + Assert.AreEqual(37.5d, txtBox.Width, 0.5); + Assert.AreNotEqual(36d, txtBoxNotMaxed.Width); + Assert.AreEqual(16.29052734375d, txtBoxNotMaxed.Width); + + var sb = new StringBuilder(); + + item.Render(sb); + var svgString = sb.ToString(); + + SaveTextFileToWorkbook($"svg\\StandAloneTextBox.svg", svgString); + } + } +} diff --git a/src/EPPlus.Export.ImageRenderer/Svg/DrawingItemForTesting.cs b/src/EPPlus.Export.ImageRenderer/Svg/DrawingItemForTesting.cs new file mode 100644 index 000000000..86b91d96e --- /dev/null +++ b/src/EPPlus.Export.ImageRenderer/Svg/DrawingItemForTesting.cs @@ -0,0 +1,58 @@ +using EPPlus.Fonts.OpenType.Utils; +using EPPlus.Graphics; +using EPPlusImageRenderer; +using EPPlusImageRenderer.RenderItems; +using EPPlusImageRenderer.Svg; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; + + +namespace EPPlus.Export.ImageRenderer.Svg +{ + internal class DrawingItemForTesting : DrawingBase + { + /// + /// List of items to be added to this basic container + /// + internal List ExternalRenderItems = new List(); + internal List ExternalRenderItemsNoBounds = new List(); + + internal DrawingItemForTesting(BoundingBox bounds) : base() + { + Bounds = bounds; + SvgRenderRectItem bg = new SvgRenderRectItem(this, Bounds); + + bg.Width = Bounds.Width; + bg.Height = Bounds.Height; + bg.FillColor = "blue"; + bg.FillOpacity = 0.2d; + + RenderItems.Add(bg); + } + + public void Render(StringBuilder sb) + { + RenderItems.Add(new SvgGroupItem(this)); + foreach(var item in ExternalRenderItemsNoBounds) + { + item.AppendRenderItems(RenderItems); + } + foreach(var item in ExternalRenderItems) + { + item.AppendRenderItems(RenderItems); + } + RenderItems.Add(new SvgEndGroupItem(this, Bounds)); + + sb.Append($""); + + foreach (var item in RenderItems) + { + item.Render(sb); + } + + sb.Append(""); + } + } +} From 097446cd97bf4cccca6548e34fddc72e7b006292 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ossian=20Edstr=C3=B6m?= Date: Tue, 10 Mar 2026 12:14:49 +0100 Subject: [PATCH 091/151] Ensured handling of individual datalabels --- .../SvgItem/SvgChartDataLabelStandard.cs | 29 ++++ .../SvgItem/SvgChartSerieDataLabel.cs | 149 ++++++++++-------- 2 files changed, 115 insertions(+), 63 deletions(-) create mode 100644 src/EPPlus.Export.ImageRenderer/RenderItems/SvgItem/SvgChartDataLabelStandard.cs diff --git a/src/EPPlus.Export.ImageRenderer/RenderItems/SvgItem/SvgChartDataLabelStandard.cs b/src/EPPlus.Export.ImageRenderer/RenderItems/SvgItem/SvgChartDataLabelStandard.cs new file mode 100644 index 000000000..60093c291 --- /dev/null +++ b/src/EPPlus.Export.ImageRenderer/RenderItems/SvgItem/SvgChartDataLabelStandard.cs @@ -0,0 +1,29 @@ +using EPPlusImageRenderer; +using EPPlusImageRenderer.RenderItems; +using EPPlusImageRenderer.Svg; +using OfficeOpenXml.Drawing.Chart; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; + +namespace EPPlus.Export.ImageRenderer.RenderItems.SvgItem +{ + internal class SvgChartDataLabelStandard : SvgChartObject + { + internal bool hasLegendKey { get; private set; } = false; + + internal SvgTextBox TxtBox; + + public SvgChartDataLabelStandard(DrawingChart chart, ExcelChartDataLabelStandard standard, SvgTextBox txtBox) : base(chart) + { + hasLegendKey = standard.ShowLegendKey; + TxtBox = txtBox; + } + + internal override void AppendRenderItems(List renderItems) + { + throw new NotImplementedException(); + } + } +} diff --git a/src/EPPlus.Export.ImageRenderer/RenderItems/SvgItem/SvgChartSerieDataLabel.cs b/src/EPPlus.Export.ImageRenderer/RenderItems/SvgItem/SvgChartSerieDataLabel.cs index 25813b540..459e54df3 100644 --- a/src/EPPlus.Export.ImageRenderer/RenderItems/SvgItem/SvgChartSerieDataLabel.cs +++ b/src/EPPlus.Export.ImageRenderer/RenderItems/SvgItem/SvgChartSerieDataLabel.cs @@ -10,6 +10,7 @@ using OfficeOpenXml.Drawing; using OfficeOpenXml.Drawing.Chart; using OfficeOpenXml.Drawing.Chart.Style; +using OfficeOpenXml.FormulaParsing.Excel.Functions.RefAndLookup; using OfficeOpenXml.Style; using OfficeOpenXml.Style.XmlAccess; using OfficeOpenXml.Utils.String; @@ -17,6 +18,7 @@ using System; using System.Collections.Generic; using System.Linq; +using System.Runtime.InteropServices; using System.Text; @@ -27,97 +29,119 @@ internal class SvgChartSerieDataLabel : SvgChartObject //positioning is handled by parent item via these internal List groupItems = new List(); - private List dlblTextBoxes = new List(); private RenderItem seriesIcon = null; + private List dataLabels = new List(); string separator; public SvgChartSerieDataLabel(SvgChart chart, ExcelChartSerieDataLabel dlblSerie, BoundingBox maxBounds, ExcelChartStandardSerie serie, List xValues, List yValues, int index) : base(chart) { + bool addSeriesIcon = false; + if (dlblSerie.DataLabels.Count == 0 && serie.NumberOfItems > 0) { separator = string.IsNullOrEmpty(dlblSerie.Separator) ? "," : dlblSerie.Separator; - if(dlblSerie.ShowLegendKey) + for (int i = 0; i < serie.NumberOfItems; i++) + { + AddDatalabel(chart, serie, dlblSerie, xValues[i], yValues[i], maxBounds, ref addSeriesIcon); + } + } + else + { + for (int i = 0; i < dlblSerie.DataLabels.Count; i++) { - if (chart.Legend == null) - { - seriesIcon = chart.GetSeriesIcon(serie, index, maxBounds); - } - else - { - var legendItem = chart.Legend; - var seriesIconOrig = (SvgRenderLineItem)legendItem.SeriesIcon[index].SeriesIcon; - var clonedIcon = seriesIconOrig.Clone(chart); - - clonedIcon.Y1 = 0; - clonedIcon.Y2 = 0; - - seriesIcon = clonedIcon; - } + var dataLabel = dlblSerie.DataLabels[i]; + separator = string.IsNullOrEmpty(dataLabel.Separator) ? "," : dataLabel.Separator; + AddDatalabel(chart, serie, dataLabel, xValues[i], yValues[i], maxBounds, ref addSeriesIcon); } + } - for (int i = 0; i < serie.NumberOfItems; i++) + if(addSeriesIcon) + { + if (chart.Legend == null) { - List dlblStrings = new List(); - - if (dlblSerie.ShowSeriesName) - { - dlblStrings.Add(serie.GetHeaderString()); - } - if (dlblSerie.ShowCategory) - { - dlblStrings.Add(xValues[i].ToString()); - } - if (dlblSerie.ShowValue) - { - dlblStrings.Add(yValues[i].ToString()); - } - - string finalString = ""; - for (int j = 0; j < dlblStrings.Count; j++) - { - finalString += dlblStrings[j]; - if (j != dlblStrings.Count - 1) - { - finalString += separator; - } - } - - var txtBox = new SvgTextBox(chart, maxBounds, maxBounds); - txtBox.ImportTextBody(dlblSerie.TextBody); - - txtBox.TextBody.ImportParagraph(dlblSerie.TextBody.Paragraphs[0], 0, finalString); - //Remove dummy paragraph added by ImportTextBody - txtBox.TextBody.Paragraphs.RemoveAt(0); - //Reset run y-position. - //Datalabel does not use the standard line-spacing textbody offsets - txtBox.TextBody.Paragraphs[0].Runs[0].YPosition = 0; - - dlblTextBoxes.Add(txtBox); + seriesIcon = chart.GetSeriesIcon(serie, index, maxBounds); + } + else + { + var legendItem = chart.Legend; + var seriesIconOrig = (SvgRenderLineItem)legendItem.SeriesIcon[index].SeriesIcon; + var clonedIcon = seriesIconOrig.Clone(chart); + + clonedIcon.Y1 = 0; + clonedIcon.Y2 = 0; + + seriesIcon = clonedIcon; } } - else + + } + + private void AddDatalabel(SvgChart chart, ExcelChartStandardSerie serie, ExcelChartDataLabelStandard dataLabel, object xValue, object yValue, BoundingBox maxBounds, ref bool addSeriesIcon) + { + List dlblStrings = new List(); + + + if (addSeriesIcon == false && dataLabel.ShowLegendKey) + { + addSeriesIcon = dataLabel.ShowLegendKey; + } + + if (dataLabel.ShowSeriesName) { - foreach (var dlbl in dlblSerie.DataLabels) + dlblStrings.Add(serie.GetHeaderString()); + } + if (dataLabel.ShowCategory) + { + dlblStrings.Add(xValue.ToString()); + } + if (dataLabel.ShowValue) + { + dlblStrings.Add(yValue.ToString()); + } + + string finalString = ""; + for (int j = 0; j < dlblStrings.Count; j++) + { + finalString += dlblStrings[j]; + if (j != dlblStrings.Count - 1) { - var txtBox = new SvgTextBox(chart, maxBounds, maxBounds); - txtBox.ImportTextBody(dlbl.TextBody); - dlblTextBoxes.Add(txtBox); + finalString += separator; } } + + var txtBox = new SvgTextBox(chart, maxBounds, maxBounds); + txtBox.ImportTextBody(dataLabel.TextBody); + + if (txtBox.TextBody.Paragraphs.Count == 0) + { + txtBox.TextBody.AddParagraph(0, finalString); + } + else if (txtBox.TextBody.Paragraphs.Count == 1) + { + txtBox.TextBody.ImportParagraph(dataLabel.TextBody.Paragraphs[0], 0, finalString); + //Remove dummy paragraph added by ImportTextBody + txtBox.TextBody.Paragraphs.RemoveAt(0); + } + //Reset run y-position. + //Datalabel does not use the standard line-spacing textbody offsets + txtBox.TextBody.Paragraphs[0].Runs[0].YPosition = 0; + + var newDataLabel = new SvgChartDataLabelStandard(chart, dataLabel, txtBox); + dataLabels.Add(newDataLabel); } internal override void AppendRenderItems(List renderItems) { for(int i = 0; i< groupItems.Count; i++) { - if (seriesIcon != null) + if (seriesIcon != null && dataLabels[i].hasLegendKey) { groupItems[i].Bounds.Left += (seriesIcon.Bounds.Width / 2); groupItems[i].GroupTransform = $"transform=\"translate({groupItems[i].Bounds.Left.PointToPixelString()}, {groupItems[i].Bounds.Top.PointToPixelString()})\""; - dlblTextBoxes[i].Left += seriesIcon.Bounds.Width + dlblTextBoxes[i].LeftMargin; + dataLabels[i].TxtBox.Left += seriesIcon.Bounds.Width + dataLabels[i].TxtBox.LeftMargin; renderItems.Add(groupItems[i]); renderItems.Add(seriesIcon); } @@ -126,8 +150,7 @@ internal override void AppendRenderItems(List renderItems) renderItems.Add(groupItems[i]); } - renderItems.Add(groupItems[i]); - dlblTextBoxes[i].AppendRenderItems(renderItems); + dataLabels[i].TxtBox.AppendRenderItems(renderItems); renderItems.Add(new SvgEndGroupItem(DrawingRenderer, Bounds)); } From f9c991950a0475a7e483964e265b1498a9f4ca70 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ossian=20Edstr=C3=B6m?= Date: Wed, 11 Mar 2026 11:41:20 +0100 Subject: [PATCH 092/151] Added manual layout and font size --- .../SvgPathTests.cs | 2 +- .../SvgItem/SvgChartDataLabelStandard.cs | 135 +++++++++++++++++- .../SvgItem/SvgChartSerieDataLabel.cs | 64 ++------- .../Svg/Chart/SvgChartObject.cs | 2 + 4 files changed, 149 insertions(+), 54 deletions(-) diff --git a/src/EPPlus.Export.ImageRenderer.Test/SvgPathTests.cs b/src/EPPlus.Export.ImageRenderer.Test/SvgPathTests.cs index a20b0d4f9..372b686af 100644 --- a/src/EPPlus.Export.ImageRenderer.Test/SvgPathTests.cs +++ b/src/EPPlus.Export.ImageRenderer.Test/SvgPathTests.cs @@ -576,7 +576,7 @@ public void GenerateDataLabelsTrueMost() public void GenerateDatalabelsLeaderLines() { ExcelPackage.License.SetNonCommercialOrganization("EPPlus Project"); - using (var p = OpenTemplatePackage("datalabelsSvgLeaderLines.xlsx")) + using (var p = OpenTemplatePackage("datalabelsSvgLeaderLinesAdjustedToBeSimilar.xlsx")) { var c = p.Workbook.Worksheets[0].Drawings[0]; diff --git a/src/EPPlus.Export.ImageRenderer/RenderItems/SvgItem/SvgChartDataLabelStandard.cs b/src/EPPlus.Export.ImageRenderer/RenderItems/SvgItem/SvgChartDataLabelStandard.cs index 60093c291..fce8a9746 100644 --- a/src/EPPlus.Export.ImageRenderer/RenderItems/SvgItem/SvgChartDataLabelStandard.cs +++ b/src/EPPlus.Export.ImageRenderer/RenderItems/SvgItem/SvgChartDataLabelStandard.cs @@ -1,7 +1,10 @@ -using EPPlusImageRenderer; +using EPPlus.Graphics; +using EPPlusImageRenderer; using EPPlusImageRenderer.RenderItems; using EPPlusImageRenderer.Svg; +using OfficeOpenXml.Drawing; using OfficeOpenXml.Drawing.Chart; +using OfficeOpenXml.Style; using System; using System.Collections.Generic; using System.Linq; @@ -11,19 +14,143 @@ namespace EPPlus.Export.ImageRenderer.RenderItems.SvgItem { internal class SvgChartDataLabelStandard : SvgChartObject { - internal bool hasLegendKey { get; private set; } = false; + internal bool HasLegendKey { get; private set; } = false; + + bool _hasManualLayout = false; + + bool haveAdjustedForIcon = false; internal SvgTextBox TxtBox; + public SvgChartDataLabelStandard(DrawingChart chart, string dataLabelText) : base(chart) + { + var txtBox = new SvgTextBox(chart, chart.Bounds, chart.Bounds); + txtBox.AddText(0, dataLabelText); + FitToTextBoxContent(); + } + + public SvgChartDataLabelStandard(DrawingChart chart, ExcelChartDataLabelStandard standard) : base(chart) + { + HasLegendKey = standard.ShowLegendKey; + } + public SvgChartDataLabelStandard(DrawingChart chart, ExcelChartDataLabelStandard standard, SvgTextBox txtBox) : base(chart) { - hasLegendKey = standard.ShowLegendKey; + HasLegendKey = standard.ShowLegendKey; + TxtBox = txtBox; + FitToTextBoxContent(); + } + + private void FitToTextBoxContent() + { + Bounds.Left = TxtBox.Left; + Bounds.Top = TxtBox.Top; + Bounds.Height = TxtBox.Height; + Bounds.Width = TxtBox.Width; + } + + internal void AddSeriesIcon(double iconWidth, double iconHeight) + { + if (haveAdjustedForIcon == false) + { + if (_hasManualLayout == false) + { + TxtBox.Left += iconWidth + TxtBox.LeftMargin; + FitToTextBoxContent(); + if (iconHeight > TxtBox.Height) + { + Bounds.Height = iconHeight; + } + } + else + { + Bounds.Left += iconWidth + TxtBox.LeftMargin; + Bounds.Width += iconWidth; + Bounds.Height += iconHeight; + } + haveAdjustedForIcon = false; + } + } + + internal void ImportDataLabel(SvgChart chart, ExcelChartStandardSerie serie, ExcelChartDataLabelStandard dataLabel, object xValue, object yValue, ExcelDrawingParagraph defaultParagraph, BoundingBox maxBounds) + { + List dlblStrings = new List(); + + if (dataLabel.ShowSeriesName) + { + dlblStrings.Add(serie.GetHeaderString()); + } + if (dataLabel.ShowCategory) + { + dlblStrings.Add(xValue.ToString()); + } + if (dataLabel.ShowValue) + { + dlblStrings.Add(yValue.ToString()); + } + + var separator = string.IsNullOrEmpty(dataLabel.Separator) ? "," : dataLabel.Separator; + + string finalString = ""; + for (int j = 0; j < dlblStrings.Count; j++) + { + finalString += dlblStrings[j]; + if (j != dlblStrings.Count - 1) + { + finalString += separator; + } + } + + var txtBox = new SvgTextBox(chart, Bounds, maxBounds); + txtBox.ImportTextBody(dataLabel.TextBody); + + if (txtBox.TextBody.Paragraphs.Count == 0) + { + txtBox.TextBody.ImportParagraph(defaultParagraph, 0, finalString); + //txtBox.TextBody.AddParagraph(0, finalString); + } + else if (txtBox.TextBody.Paragraphs.Count == 1) + { + txtBox.TextBody.ImportParagraph(dataLabel.TextBody.Paragraphs[0], 0, finalString); + //Remove dummy paragraph added by ImportTextBody + txtBox.TextBody.Paragraphs.RemoveAt(0); + } + //Reset run y-position. + //Datalabel does not use the standard line-spacing textbody offsets + txtBox.TextBody.Paragraphs[0].Runs[0].YPosition = 0; + TxtBox = txtBox; + + if (dataLabel is ExcelChartDataLabelItem) + { + var individualLabel = dataLabel as ExcelChartDataLabelItem; + + if (individualLabel.Layout != null && individualLabel.Layout.HasLayout) + { + FitToTextBoxContent(); + _hasManualLayout = true; + var rect = GetRectFromManualLayout(chart, individualLabel.Layout); + Rectangle = rect; + Bounds.Left = Rectangle.Left; + Bounds.Top = Rectangle.Top; + //Bounds.Width = Rectangle.Width; + //Bounds.Height = Rectangle.Height; + } + } + else + { + FitToTextBoxContent(); + } } internal override void AppendRenderItems(List renderItems) { - throw new NotImplementedException(); + var group = new SvgGroupItem(ChartRenderer, Bounds); + renderItems.Add(group); + + TxtBox.AppendRenderItems(renderItems); + + renderItems.Add(new SvgEndGroupItem(DrawingRenderer, Bounds)); } } } diff --git a/src/EPPlus.Export.ImageRenderer/RenderItems/SvgItem/SvgChartSerieDataLabel.cs b/src/EPPlus.Export.ImageRenderer/RenderItems/SvgItem/SvgChartSerieDataLabel.cs index 459e54df3..044151180 100644 --- a/src/EPPlus.Export.ImageRenderer/RenderItems/SvgItem/SvgChartSerieDataLabel.cs +++ b/src/EPPlus.Export.ImageRenderer/RenderItems/SvgItem/SvgChartSerieDataLabel.cs @@ -11,6 +11,7 @@ using OfficeOpenXml.Drawing.Chart; using OfficeOpenXml.Drawing.Chart.Style; using OfficeOpenXml.FormulaParsing.Excel.Functions.RefAndLookup; +using OfficeOpenXml.Interfaces.Drawing.Text; using OfficeOpenXml.Style; using OfficeOpenXml.Style.XmlAccess; using OfficeOpenXml.Utils.String; @@ -34,13 +35,21 @@ internal class SvgChartSerieDataLabel : SvgChartObject string separator; + ExcelTextFont defaultFont; + ExcelDrawingParagraph defaultParagraph; + public SvgChartSerieDataLabel(SvgChart chart, ExcelChartSerieDataLabel dlblSerie, BoundingBox maxBounds, ExcelChartStandardSerie serie, List xValues, List yValues, int index) : base(chart) { bool addSeriesIcon = false; + if(dlblSerie.TextBody.Paragraphs.Count != 0) + { + defaultParagraph = dlblSerie.TextBody.Paragraphs[0]; + defaultFont = dlblSerie.TextBody.Paragraphs[0].DefaultRunProperties; + } + if (dlblSerie.DataLabels.Count == 0 && serie.NumberOfItems > 0) { - separator = string.IsNullOrEmpty(dlblSerie.Separator) ? "," : dlblSerie.Separator; for (int i = 0; i < serie.NumberOfItems; i++) { @@ -52,7 +61,6 @@ public SvgChartSerieDataLabel(SvgChart chart, ExcelChartSerieDataLabel dlblSerie for (int i = 0; i < dlblSerie.DataLabels.Count; i++) { var dataLabel = dlblSerie.DataLabels[i]; - separator = string.IsNullOrEmpty(dataLabel.Separator) ? "," : dataLabel.Separator; AddDatalabel(chart, serie, dataLabel, xValues[i], yValues[i], maxBounds, ref addSeriesIcon); } @@ -81,55 +89,13 @@ public SvgChartSerieDataLabel(SvgChart chart, ExcelChartSerieDataLabel dlblSerie private void AddDatalabel(SvgChart chart, ExcelChartStandardSerie serie, ExcelChartDataLabelStandard dataLabel, object xValue, object yValue, BoundingBox maxBounds, ref bool addSeriesIcon) { - List dlblStrings = new List(); - - if (addSeriesIcon == false && dataLabel.ShowLegendKey) { addSeriesIcon = dataLabel.ShowLegendKey; } - if (dataLabel.ShowSeriesName) - { - dlblStrings.Add(serie.GetHeaderString()); - } - if (dataLabel.ShowCategory) - { - dlblStrings.Add(xValue.ToString()); - } - if (dataLabel.ShowValue) - { - dlblStrings.Add(yValue.ToString()); - } - - string finalString = ""; - for (int j = 0; j < dlblStrings.Count; j++) - { - finalString += dlblStrings[j]; - if (j != dlblStrings.Count - 1) - { - finalString += separator; - } - } - - var txtBox = new SvgTextBox(chart, maxBounds, maxBounds); - txtBox.ImportTextBody(dataLabel.TextBody); - - if (txtBox.TextBody.Paragraphs.Count == 0) - { - txtBox.TextBody.AddParagraph(0, finalString); - } - else if (txtBox.TextBody.Paragraphs.Count == 1) - { - txtBox.TextBody.ImportParagraph(dataLabel.TextBody.Paragraphs[0], 0, finalString); - //Remove dummy paragraph added by ImportTextBody - txtBox.TextBody.Paragraphs.RemoveAt(0); - } - //Reset run y-position. - //Datalabel does not use the standard line-spacing textbody offsets - txtBox.TextBody.Paragraphs[0].Runs[0].YPosition = 0; - - var newDataLabel = new SvgChartDataLabelStandard(chart, dataLabel, txtBox); + var newDataLabel = new SvgChartDataLabelStandard(chart, dataLabel); + newDataLabel.ImportDataLabel(chart, serie, dataLabel, xValue, yValue, defaultParagraph, maxBounds); dataLabels.Add(newDataLabel); } @@ -137,11 +103,11 @@ internal override void AppendRenderItems(List renderItems) { for(int i = 0; i< groupItems.Count; i++) { - if (seriesIcon != null && dataLabels[i].hasLegendKey) + if (seriesIcon != null && dataLabels[i].HasLegendKey) { groupItems[i].Bounds.Left += (seriesIcon.Bounds.Width / 2); groupItems[i].GroupTransform = $"transform=\"translate({groupItems[i].Bounds.Left.PointToPixelString()}, {groupItems[i].Bounds.Top.PointToPixelString()})\""; - dataLabels[i].TxtBox.Left += seriesIcon.Bounds.Width + dataLabels[i].TxtBox.LeftMargin; + dataLabels[i].AddSeriesIcon(seriesIcon.Bounds.Width, seriesIcon.Bounds.Height); renderItems.Add(groupItems[i]); renderItems.Add(seriesIcon); } @@ -150,7 +116,7 @@ internal override void AppendRenderItems(List renderItems) renderItems.Add(groupItems[i]); } - dataLabels[i].TxtBox.AppendRenderItems(renderItems); + dataLabels[i].AppendRenderItems(renderItems); renderItems.Add(new SvgEndGroupItem(DrawingRenderer, Bounds)); } diff --git a/src/EPPlus.Export.ImageRenderer/Svg/Chart/SvgChartObject.cs b/src/EPPlus.Export.ImageRenderer/Svg/Chart/SvgChartObject.cs index 52aab8346..d11d1128c 100644 --- a/src/EPPlus.Export.ImageRenderer/Svg/Chart/SvgChartObject.cs +++ b/src/EPPlus.Export.ImageRenderer/Svg/Chart/SvgChartObject.cs @@ -90,6 +90,7 @@ protected static SvgRenderRectItem GetRectFromManualLayout(SvgChart sc, ExcelLay } else { + rect.Left = sc.Bounds.Width * (float)(ml.Left ?? 0D) / 100; //TODO:Add factor from default position } //Width is always factor. @@ -101,6 +102,7 @@ protected static SvgRenderRectItem GetRectFromManualLayout(SvgChart sc, ExcelLay } else { + rect.Top = sc.Bounds.Height * (float)(ml.Top ?? 0D) / 100; //TODO:Add factor from default position } //Height is always factor. From 5073fe7ffd10feb6150e72e5afb4c3868b212458 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ossian=20Edstr=C3=B6m?= Date: Wed, 11 Mar 2026 15:49:50 +0100 Subject: [PATCH 093/151] Broke out connection lines etc --- .../SvgItem/ConnectionPointsMiddle.cs | 36 +++ .../RenderItems/SvgItem/PointLines.cs | 63 +++++ .../SvgItem/SvgChartDataLabelStandard.cs | 216 +++++++++++++++++- .../SvgItem/SvgChartSerieDataLabel.cs | 19 +- .../RenderItems/SvgItem/SvgTextBox.cs | 11 +- .../ChartTypeDrawers/LineChartTypeDrawer.cs | 6 +- .../Drawing/Style/Text/ExcelTextBody.cs | 26 ++- 7 files changed, 343 insertions(+), 34 deletions(-) create mode 100644 src/EPPlus.Export.ImageRenderer/RenderItems/SvgItem/ConnectionPointsMiddle.cs create mode 100644 src/EPPlus.Export.ImageRenderer/RenderItems/SvgItem/PointLines.cs diff --git a/src/EPPlus.Export.ImageRenderer/RenderItems/SvgItem/ConnectionPointsMiddle.cs b/src/EPPlus.Export.ImageRenderer/RenderItems/SvgItem/ConnectionPointsMiddle.cs new file mode 100644 index 000000000..ef38951e6 --- /dev/null +++ b/src/EPPlus.Export.ImageRenderer/RenderItems/SvgItem/ConnectionPointsMiddle.cs @@ -0,0 +1,36 @@ +using EPPlusImageRenderer; +using OfficeOpenXml.FormulaParsing.Excel.Functions.Text; +using System.Collections.Generic; + + +namespace EPPlus.Export.ImageRenderer.RenderItems.SvgItem +{ + internal class ConnectionPointsMiddle + { + internal Coordinate Left; + internal Coordinate Top; + internal Coordinate Right; + internal Coordinate Bottom; + + internal Dictionary Points; + + internal ConnectionPointsMiddle(double left, double top, double width, double height) + { + var middleWidth = width / 2; + var middleHeight = height / 2; + + Left = new Coordinate(left, middleHeight); + Top= new Coordinate(middleWidth, top); + + Right = new Coordinate(left + width, middleHeight); + Bottom = new Coordinate(middleWidth, top + height); + + Points = new Dictionary(); + + Points.Add(0, Left); + Points.Add(1, Top); + Points.Add(2, Right); + Points.Add(3, Bottom); + } + } +} diff --git a/src/EPPlus.Export.ImageRenderer/RenderItems/SvgItem/PointLines.cs b/src/EPPlus.Export.ImageRenderer/RenderItems/SvgItem/PointLines.cs new file mode 100644 index 000000000..9a0657d12 --- /dev/null +++ b/src/EPPlus.Export.ImageRenderer/RenderItems/SvgItem/PointLines.cs @@ -0,0 +1,63 @@ +using EPPlus.Graphics; +using EPPlusImageRenderer; +using EPPlusImageRenderer.RenderItems; +using EPPlusImageRenderer.Svg; +using System; +using System.Collections.Generic; +using System.Drawing; +using System.Linq; +using System.Runtime.InteropServices; +using System.Text; + +namespace EPPlus.Export.ImageRenderer.RenderItems.SvgItem +{ + internal class PointLines : DrawingObject + { + internal List RenderLines = new List(); + + internal ConnectionPointsMiddle connectionPoints; + + private PointLines(DrawingBase renderer) : base(renderer) + { + } + + internal PointLines(DrawingBase renderer, BoundingBox parent, ConnectionPointsMiddle connectionPoints) : this(renderer) + { + Bounds = new BoundingBox(); + + Bounds.Parent = parent; + Bounds.Left = parent.Left; + Bounds.Top = parent.Top; + Bounds.Width = parent.Width; + Bounds.Height = parent.Height; + + //Add connection points to render + List ptColors = new List { "red", "green", "blue", "yellow" }; + for (int i = 0; i < connectionPoints.Points.Count; i++) + { + var cPoint = connectionPoints.Points[i]; + var cPointLine = new SvgRenderLineItem(renderer, Bounds); + cPointLine.X1 = 0; + cPointLine.Y1 = 0; + cPointLine.X2 = cPoint.X; + cPointLine.Y2 = cPoint.Y; + + cPointLine.BorderWidth = 1; + cPointLine.BorderColor = ptColors[i]; + RenderLines.Add(cPointLine); + } + } + + internal override void AppendRenderItems(List renderItems) + { + SvgGroupItem gItem = new SvgGroupItem(DrawingRenderer, Bounds); + renderItems.Add(gItem); + foreach (var line in RenderLines) + { + renderItems.Add(line); + } + SvgEndGroupItem endGItem = new SvgEndGroupItem(DrawingRenderer, Bounds); + renderItems.Add(endGItem); + } + } +} diff --git a/src/EPPlus.Export.ImageRenderer/RenderItems/SvgItem/SvgChartDataLabelStandard.cs b/src/EPPlus.Export.ImageRenderer/RenderItems/SvgItem/SvgChartDataLabelStandard.cs index fce8a9746..0e45ad199 100644 --- a/src/EPPlus.Export.ImageRenderer/RenderItems/SvgItem/SvgChartDataLabelStandard.cs +++ b/src/EPPlus.Export.ImageRenderer/RenderItems/SvgItem/SvgChartDataLabelStandard.cs @@ -4,11 +4,9 @@ using EPPlusImageRenderer.Svg; using OfficeOpenXml.Drawing; using OfficeOpenXml.Drawing.Chart; -using OfficeOpenXml.Style; using System; using System.Collections.Generic; -using System.Linq; -using System.Text; +using System.Runtime.InteropServices; namespace EPPlus.Export.ImageRenderer.RenderItems.SvgItem { @@ -17,11 +15,21 @@ internal class SvgChartDataLabelStandard : SvgChartObject internal bool HasLegendKey { get; private set; } = false; bool _hasManualLayout = false; + bool _hasLeaderLines = false; bool haveAdjustedForIcon = false; internal SvgTextBox TxtBox; + List LeaderLines = new List(); + + Coordinate orginPointOffset = new Coordinate (0, 0); + + PointLines lines; + ////Connection point coords are accurate to Internal bounds + //BoundingBox internalBounds = new BoundingBox(); + //List ConnectionPoints = new List(); + public SvgChartDataLabelStandard(DrawingChart chart, string dataLabelText) : base(chart) { var txtBox = new SvgTextBox(chart, chart.Bounds, chart.Bounds); @@ -68,7 +76,7 @@ internal void AddSeriesIcon(double iconWidth, double iconHeight) Bounds.Width += iconWidth; Bounds.Height += iconHeight; } - haveAdjustedForIcon = false; + haveAdjustedForIcon = true; } } @@ -102,7 +110,8 @@ internal void ImportDataLabel(SvgChart chart, ExcelChartStandardSerie serie, Exc } var txtBox = new SvgTextBox(chart, Bounds, maxBounds); - txtBox.ImportTextBody(dataLabel.TextBody); + txtBox.ImportTextBody(dataLabel.TextBody, false); + txtBox.TextBody.AutoSize = true; if (txtBox.TextBody.Paragraphs.Count == 0) { @@ -115,9 +124,9 @@ internal void ImportDataLabel(SvgChart chart, ExcelChartStandardSerie serie, Exc //Remove dummy paragraph added by ImportTextBody txtBox.TextBody.Paragraphs.RemoveAt(0); } - //Reset run y-position. - //Datalabel does not use the standard line-spacing textbody offsets - txtBox.TextBody.Paragraphs[0].Runs[0].YPosition = 0; + ////Reset run y-position. + ////Datalabel does not use the standard line-spacing textbody offsets + //txtBox.TextBody.Paragraphs[0].Runs[0].YPosition = 0; TxtBox = txtBox; @@ -131,10 +140,72 @@ internal void ImportDataLabel(SvgChart chart, ExcelChartStandardSerie serie, Exc _hasManualLayout = true; var rect = GetRectFromManualLayout(chart, individualLabel.Layout); Rectangle = rect; - Bounds.Left = Rectangle.Left; - Bounds.Top = Rectangle.Top; - //Bounds.Width = Rectangle.Width; - //Bounds.Height = Rectangle.Height; + + LeftMargin = Rectangle.Left; + TopMargin = Rectangle.Top; + + if (dataLabel.ShowLeaderLines) + { + _hasLeaderLines = true; + } + ////TopMargin = Rectangle.Top; + ////BottomMargin = Rectangle.Bottom; + + if (dataLabel.ShowLeaderLines) + { + var cPoints = new ConnectionPointsMiddle(Bounds.Left, Bounds.Top, Bounds.Width, Bounds.Height); + + //Since this is a child transform changes to this transform will compound + lines = new PointLines(ChartRenderer, Bounds, cPoints); + //var index = GetClosestConnectionPointToOriginIndex(); + //ConnectionPointsLines.Clear(); + + ////Add connection points to render + //List ptColors = new List { "red", "green", "blue", "yellow" }; + //for (int i = 0; i < connectionPoints.Points.Count; i++) + //{ + // var cPoint = connectionPoints.Points[i]; + // var cPointLine = new SvgRenderLineItem(chart, txtBox.TextBody.Bounds); + // cPointLine.X1 = 0; + // cPointLine.Y1 = 0; + // cPointLine.X2 = cPoint.X; + // cPointLine.Y2 = cPoint.Y; + + // cPointLine.BorderWidth = 1; + // cPointLine.BorderColor = ptColors[i]; + // ConnectionPointsLines.Add(cPointLine); + //} + } + + // double xOffset = 0; + // if(index == 0 || index == 2) + // { + // //If Left or Right + // //Add extra 7 px (5.25pt) line to the given side + // var extraLine = new SvgRenderLineItem(chart, Bounds); + + // xOffset = index == 0 ? - 5.25d : 5.25d; + + // extraLine.X1 = ConnectionPoints[index].X; + // extraLine.Y1 = ConnectionPoints[index].Y; + // extraLine.Y2 = ConnectionPoints[index].Y; + // extraLine.X2 = extraLine.X1 + xOffset; + + // extraLine.BorderColor = "black"; + // extraLine.BorderWidth = 1; + + // LeaderLines.Add(extraLine); + // } + // var mainLine = new SvgRenderLineItem(chart, Bounds); + // mainLine.X1 = ConnectionPoints[index].X + xOffset; + // mainLine.Y1 = ConnectionPoints[index].Y; + // mainLine.X2 = -Bounds.Left; + // mainLine.Y2 = -Bounds.Top; + + // mainLine.BorderColor = "black"; + // mainLine.BorderWidth = 1; + // LeaderLines.Add(mainLine); + //} } } else @@ -143,13 +214,134 @@ internal void ImportDataLabel(SvgChart chart, ExcelChartStandardSerie serie, Exc } } + //private int GetClosestConnectionPointToOriginIndex() + //{ + // //Origin in local coordinates is 0,0 + // return GetClosestConnectionPointCoordinateIndex(new Coordinate(0, 0)); + //} + + //private int GetClosestConnectionPointCoordinateIndex(Coordinate originPoint) + //{ + // //CalculateConnectionPoints(); + + // double smallestDist = double.MaxValue; + // int i = 0; + // int smallestIndex = 0; + + // foreach(var line in lines.RenderLines) + // { + + // var w = Math.Abs(line.X2 - originPoint.X); + // var h = Math.Abs(line.Y2 - originPoint.Y); + + // //Use pythagoran theorem to get diagonal distance + // var totalDist = Math.Sqrt(Math.Pow(w,2) + Math.Pow(h,2)); + + // if (totalDist < smallestDist) + // { + // smallestDist = totalDist; + // smallestIndex = i; + // } + // i++; + // } + + // return smallestIndex; + //} + + internal void SetOriginPointOffset(double xPos, double yPos) + { + orginPointOffset.X = xPos; + orginPointOffset.Y = yPos; + + Bounds.Top = yPos; + Bounds.Left = xPos; + + //if (_hasManualLayout) + //{ + // if (_hasLeaderLines) + // { + // var index = GetClosestConnectionPointCoordinateIndex(orginPointOffset); + + // LeaderLines.Clear(); + + // double xOffset = 0; + // if (index == 0 || index == 2) + // { + // //If Left or Right + // //Add extra 7 px (5.25pt) line to the given side + // var extraLine = new SvgRenderLineItem(ChartRenderer, ChartRenderer.Bounds); + + // xOffset = index == 0 ? -5.25d : 5.25d; + + // extraLine.X1 = lines.connectionPoints.Points[index].X; + // extraLine.Y1 = lines.connectionPoints.Points[index].Y; + // extraLine.Y2 = lines.connectionPoints.Points[index].Y; + // extraLine.X2 = extraLine.X1 + xOffset; + + // extraLine.BorderColor = "black"; + // extraLine.BorderWidth = 1; + + // LeaderLines.Add(extraLine); + // } + // var mainLine = new SvgRenderLineItem(ChartRenderer, ChartRenderer.Bounds); + // mainLine.X1 = lines.connectionPoints.Points[index].X + xOffset; + // mainLine.Y1 = lines.connectionPoints.Points[index].Y; + // mainLine.X2 = -Bounds.Left; + // mainLine.Y2 = -Bounds.Top; + + // mainLine.BorderColor = "black"; + // mainLine.BorderWidth = 1; + // LeaderLines.Add(mainLine); + // } + //} + } + + //private void CalculateConnectionPoints() + //{ + // internalBounds.Left = Bounds.Left; + // internalBounds.Top = Bounds.Top; + // internalBounds.Width = Bounds.Width; + // internalBounds.Height = Bounds.Height; + + // var middleWidth = Bounds.Width / 2 + LeftMargin; + // var middleHeight = Bounds.Height / 2 + TopMargin; + + // var cPointLeft = new Coordinate(Bounds.Left + LeftMargin , middleHeight); + // var cPointTop = new Coordinate(middleWidth, Bounds.Top + TopMargin); + // var cPointRight = new Coordinate(Bounds.Right + LeftMargin, middleHeight); + // var cPointBottom = new Coordinate(middleWidth, Bounds.Bottom + TopMargin); + + // ConnectionPoints = new List { cPointLeft, cPointTop, cPointRight, cPointBottom }; + //} + internal override void AppendRenderItems(List renderItems) { var group = new SvgGroupItem(ChartRenderer, Bounds); renderItems.Add(group); + var titleItem = new SvgTitleItem(DrawingRenderer, "DataLabelRect"); + renderItems.Add(titleItem); + TxtBox.AppendRenderItems(renderItems); + if (lines != null) + { + lines.AppendRenderItems(renderItems); + } + + SvgRenderRectItem rect = new SvgRenderRectItem(ChartRenderer, Bounds); + rect.Bounds = Bounds; + rect.FillColor = "red"; + rect.FillOpacity = 0.2; + renderItems.Add(rect); + + //if (LeaderLines != null && LeaderLines.Count > 0) + //{ + // foreach (var line in LeaderLines) + // { + // renderItems.Add(line); + // } + //} renderItems.Add(new SvgEndGroupItem(DrawingRenderer, Bounds)); } } diff --git a/src/EPPlus.Export.ImageRenderer/RenderItems/SvgItem/SvgChartSerieDataLabel.cs b/src/EPPlus.Export.ImageRenderer/RenderItems/SvgItem/SvgChartSerieDataLabel.cs index 044151180..e5400bbb0 100644 --- a/src/EPPlus.Export.ImageRenderer/RenderItems/SvgItem/SvgChartSerieDataLabel.cs +++ b/src/EPPlus.Export.ImageRenderer/RenderItems/SvgItem/SvgChartSerieDataLabel.cs @@ -25,7 +25,7 @@ namespace EPPlus.Export.ImageRenderer.RenderItems.SvgItem { - internal class SvgChartSerieDataLabel : SvgChartObject + internal class SvgChartSerieDataLabel : DrawingObjectNoBounds { //positioning is handled by parent item via these internal List groupItems = new List(); @@ -99,26 +99,31 @@ private void AddDatalabel(SvgChart chart, ExcelChartStandardSerie serie, ExcelCh dataLabels.Add(newDataLabel); } + internal void SetPositionOffset(double xPos, double yPos, int i) + { + dataLabels[i].SetOriginPointOffset(xPos, yPos); + } + internal override void AppendRenderItems(List renderItems) { - for(int i = 0; i< groupItems.Count; i++) + for(int i = 0; i< dataLabels.Count; i++) { if (seriesIcon != null && dataLabels[i].HasLegendKey) { - groupItems[i].Bounds.Left += (seriesIcon.Bounds.Width / 2); - groupItems[i].GroupTransform = $"transform=\"translate({groupItems[i].Bounds.Left.PointToPixelString()}, {groupItems[i].Bounds.Top.PointToPixelString()})\""; + //groupItems[i].Bounds.Left += (seriesIcon.Bounds.Width / 2); + //groupItems[i].GroupTransform = $"transform=\"translate({groupItems[i].Bounds.Left.PointToPixelString()}, {groupItems[i].Bounds.Top.PointToPixelString()})\""; dataLabels[i].AddSeriesIcon(seriesIcon.Bounds.Width, seriesIcon.Bounds.Height); - renderItems.Add(groupItems[i]); + //renderItems.Add(groupItems[i]); renderItems.Add(seriesIcon); } else { - renderItems.Add(groupItems[i]); + //renderItems.Add(groupItems[i]); } dataLabels[i].AppendRenderItems(renderItems); - renderItems.Add(new SvgEndGroupItem(DrawingRenderer, Bounds)); + //renderItems.Add(new SvgEndGroupItem(DrawingRenderer, Bounds)); } } } diff --git a/src/EPPlus.Export.ImageRenderer/RenderItems/SvgItem/SvgTextBox.cs b/src/EPPlus.Export.ImageRenderer/RenderItems/SvgItem/SvgTextBox.cs index 5b8eec416..8e1e8d472 100644 --- a/src/EPPlus.Export.ImageRenderer/RenderItems/SvgItem/SvgTextBox.cs +++ b/src/EPPlus.Export.ImageRenderer/RenderItems/SvgItem/SvgTextBox.cs @@ -99,10 +99,17 @@ internal double Rotation } } - internal void ImportTextBody(ExcelTextBody body) + internal void ImportTextBody(ExcelTextBody body, bool useDefaults = true) { double l, r, t, b; - body.GetInsetsOrDefaults(out l, out t, out r, out b); + if (useDefaults) + { + body.GetInsetsOrDefaults(out l, out t, out r, out b); + } + else + { + body.GetInsetsInPoints(out l, out t, out r, out b); + } LeftMargin = l; TopMargin = t; RightMargin = r; diff --git a/src/EPPlus.Export.ImageRenderer/Svg/Chart/ChartTypeDrawers/LineChartTypeDrawer.cs b/src/EPPlus.Export.ImageRenderer/Svg/Chart/ChartTypeDrawers/LineChartTypeDrawer.cs index 327b3bf55..3d3dd66e3 100644 --- a/src/EPPlus.Export.ImageRenderer/Svg/Chart/ChartTypeDrawers/LineChartTypeDrawer.cs +++ b/src/EPPlus.Export.ImageRenderer/Svg/Chart/ChartTypeDrawers/LineChartTypeDrawer.cs @@ -119,8 +119,7 @@ private void AddLine(ExcelChart chartType, ExcelLineChartSerie serie, List /// Get Insets in points /// - /// - /// - /// - /// - internal void GetInsetsOrDefaults(out double Left, out double Top, out double Right, out double Bottom) + /// + /// + /// + /// + internal void GetInsetsOrDefaults(out double left, out double top, out double right, out double bottom) { - Left = LeftInsert ?? DefaultRightLeft; - Top = TopInsert ?? DefaultRightLeft; - Right = RightInsert ?? DefaultTopBot; - Bottom = BottomInsert ?? DefaultTopBot; + left = LeftInsert ?? DefaultRightLeft; + top = TopInsert ?? DefaultRightLeft; + right = RightInsert ?? DefaultTopBot; + bottom = BottomInsert ?? DefaultTopBot; + } + + internal void GetInsetsInPoints(out double left, out double top, out double right, out double bottom) + { + left = (LeftInsert ?? 0) / ExcelDrawing.EMU_PER_POINT; + top = (TopInsert ?? 0) / ExcelDrawing.EMU_PER_POINT; + right = (RightInsert ?? 0) / ExcelDrawing.EMU_PER_POINT; + bottom = (BottomInsert ?? 0) / ExcelDrawing.EMU_PER_POINT; } ExcelDrawingParagraphCollection _paragraphs = null; From 142194782b2e68e9b44fb06515dd6e779ede10b1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jan=20K=C3=A4llman?= Date: Mon, 16 Mar 2026 08:12:50 +0100 Subject: [PATCH 094/151] WIP:Fixes compound lines on line charts. Fixed several issues with axis and line paths. --- docs/articles/breakingchanges.md | 8 +- .../SvgPathTests.cs | 57 +++++- .../TestBase.cs | 1 - .../RenderItems/RenderItem.cs | 36 ++-- .../RenderItems/SvgRenderItem.cs | 47 ++++- .../RenderItems/SvgRenderLineItem.cs | 34 +++- .../RenderItems/SvgRenderPathItem.cs | 21 ++- .../ChartTypeDrawers/LineChartTypeDrawer.cs | 21 ++- .../Svg/Chart/SvgChart.cs | 166 +++++++++++------- .../Svg/Chart/SvgChartAxis.cs | 60 +++++-- .../Svg/Chart/SvgChartPlotarea.cs | 118 ++++++++++--- .../Svg/Chart/SvgChartTitle.cs | 8 + src/EPPlus/Drawing/Chart/ExcelChart.cs | 9 +- .../Drawing/Chart/ExcelChartAxisStandard.cs | 6 +- src/EPPlus/Drawing/Chart/ExcelChartTitle.cs | 2 +- src/EPPlus/Drawing/Enums/EnumTransl.cs | 24 +-- ...pundLineStyle.cs => eCompoundLineStyle.cs} | 2 +- src/EPPlus/Drawing/ExcelDrawingBorder.cs | 2 +- src/EPPlus/Drawing/Theme/ExcelThemeLine.cs | 2 +- .../Utils/TypeConversion/ColorConverter.cs | 3 + src/EPPlusTest/Drawing/BorderTest.cs | 12 +- .../Drawing/Chart/Styling/StylingTest.cs | 2 +- src/EPPlusTest/Drawing/ThemeTest.cs | 2 +- 23 files changed, 468 insertions(+), 175 deletions(-) rename src/EPPlus/Drawing/Enums/{eCompundLineStyle.cs => eCompoundLineStyle.cs} (97%) diff --git a/docs/articles/breakingchanges.md b/docs/articles/breakingchanges.md index 89a45fcf1..78c7fcfdf 100644 --- a/docs/articles/breakingchanges.md +++ b/docs/articles/breakingchanges.md @@ -215,4 +215,10 @@ Renaming worksheet's will now change the formula correctly to include single quo `ExcelColor.Tint` from `decimal` to `double`. ### 8.0.2 -* Removed base class from ExcelVmlDrawingPosition and with that the Load, UpdateXml methods and the RowOff and ColOff properties as they were duplicates. \ No newline at end of file +* Removed base class from ExcelVmlDrawingPosition and with that the Load, UpdateXml methods and the RowOff and ColOff properties as they were duplicates. +### 8.5.0 +* Setting dataLabelPosition.Top on BarCharts corrupted the excel file when saved. Trying to set this now throws an error instead. +* NumberFormatToTextArgs.NumberFormat now returns the interface IExcelNumberFormat rather than the ExcelNumberFormatXml class. All public variables remain the same and it can be safely cast to ´ExcelNumberFormatXml´ as long as ´Package.Workbook.NumberFormatToTextHandler´ is null. + +### 9.0.0 +The misspelled enum eCompundLineStyle has been renamed eCompoundLineStyle. \ No newline at end of file diff --git a/src/EPPlus.Export.ImageRenderer.Test/SvgPathTests.cs b/src/EPPlus.Export.ImageRenderer.Test/SvgPathTests.cs index ad09be3e9..196ea5d3b 100644 --- a/src/EPPlus.Export.ImageRenderer.Test/SvgPathTests.cs +++ b/src/EPPlus.Export.ImageRenderer.Test/SvgPathTests.cs @@ -693,19 +693,58 @@ public void GenerateSvgForCharts_sheet2() { var ws = p.Workbook.Worksheets[1]; var renderer = new EPPlusImageRenderer.ImageRenderer(); + //var ix = 1; + //var c = ws.Drawings[ix]; + //var svg = renderer.RenderDrawingToSvg(c); + //SaveTextFileToWorkbook($"svg\\ChartForSvg_sheet2_{ix++}.svg", svg); var ix = 1; - var c = ws.Drawings[ix]; - var svg = renderer.RenderDrawingToSvg(c); - SaveTextFileToWorkbook($"svg\\ChartForSvg_sheet2_{ix++}.svg", svg); + foreach (ExcelChart c in ws.Drawings) + { + var svg = renderer.RenderDrawingToSvg(c); + SaveTextFileToWorkbook($"svg\\ChartForSvg{ix++}.svg", svg); + } + } + } + [TestMethod] + public void GenerateSvgForCharts_sheet3() + { + ExcelPackage.License.SetNonCommercialOrganization("EPPlus Project"); + using (var p = OpenTemplatePackage("ChartForSvg.xlsx")) + { + var ws = p.Workbook.Worksheets[2]; + var renderer = new EPPlusImageRenderer.ImageRenderer(); //var ix = 1; - //foreach (ExcelChart c in ws.Drawings) - //{ - // var svg = renderer.RenderDrawingToSvg(c); - // SaveTextFileToWorkbook($"svg\\ChartForSvg{ix++}.svg", svg); - //} + //var c = ws.Drawings[ix]; + //var svg = renderer.RenderDrawingToSvg(c); + //SaveTextFileToWorkbook($"svg\\ChartForSvg_sheet2_{ix++}.svg", svg); + var ix = 1; + foreach (ExcelChart c in ws.Drawings) + { + var svg = renderer.RenderDrawingToSvg(c); + SaveTextFileToWorkbook($"svg\\ChartForSvg_Sheet3{ix++}.svg", svg); + } + } + } + [TestMethod] + public void GenerateSvgForCharts_SecondaryAxis() + { + ExcelPackage.License.SetNonCommercialOrganization("EPPlus Project"); + using (var p = OpenTemplatePackage("ChartForSvg_SecondaryAxis.xlsx")) + { + var ws = p.Workbook.Worksheets[0]; + var renderer = new EPPlusImageRenderer.ImageRenderer(); + //var ix = 1; + //var c = ws.Drawings[ix]; + //var svg = renderer.RenderDrawingToSvg(c); + //SaveTextFileToWorkbook($"svg\\ChartForSvg_sheet2_{ix++}.svg", svg); + var ix = 1; + foreach (ExcelChart c in ws.Drawings) + { + var svg = renderer.RenderDrawingToSvg(c); + SaveTextFileToWorkbook($"svg\\ChartForSvg_SecAxis{ix++}.svg", svg); + } } } - [TestMethod] public void CreateChartsWithDifferentSize() { diff --git a/src/EPPlus.Export.ImageRenderer.Test/TestBase.cs b/src/EPPlus.Export.ImageRenderer.Test/TestBase.cs index 2e52c56a6..227fafa27 100644 --- a/src/EPPlus.Export.ImageRenderer.Test/TestBase.cs +++ b/src/EPPlus.Export.ImageRenderer.Test/TestBase.cs @@ -382,7 +382,6 @@ protected static ExcelRangeBase LoadHierarkiTestData(ExcelWorksheet ws) } protected static void LoadGeoTestData(ExcelWorksheet ws) { - var l = new List { new GeoData{ Country="Sweden", State = "Stockholm", Sales = 154 }, diff --git a/src/EPPlus.Export.ImageRenderer/RenderItems/RenderItem.cs b/src/EPPlus.Export.ImageRenderer/RenderItems/RenderItem.cs index 48a12d019..02175448e 100644 --- a/src/EPPlus.Export.ImageRenderer/RenderItems/RenderItem.cs +++ b/src/EPPlus.Export.ImageRenderer/RenderItems/RenderItem.cs @@ -59,6 +59,8 @@ internal virtual void GetBounds(out double il, out double it, out double ir, out public ExcelDrawingBlipFill BlipFill { get; private set; } public double? BorderWidth { get; set; } public double[] BorderDashArray { get; set; } + public int StrokeMiterLimit { get; set; } = 4; + public eCompoundLineStyle CompoundLineStyle { get; set; } = eCompoundLineStyle.Single; public double? BorderDashOffset { get; set; } public eLineCap LineCap { get; set; } = eLineCap.Flat; public SvgLineJoin LineJoin { get; set; } = SvgLineJoin.Miter; @@ -100,12 +102,13 @@ internal virtual void SetDrawingPropertiesFill(ExcelDrawingFill fill, ExcelDrawi } internal virtual void SetDrawingPropertiesFill(ExcelDrawingFillBasic fill, ExcelDrawingColorManager color) { + double? opacity=null; switch (fill.Style) { case eFillStyle.NoFill: if (fill.IsEmpty) { - FillColor = GetFillColor(fill, color, FillColorSource); + FillColor = GetFillColor(fill, color, FillColorSource, out opacity); } else { @@ -113,22 +116,27 @@ internal virtual void SetDrawingPropertiesFill(ExcelDrawingFillBasic fill, Excel } break; case eFillStyle.SolidFill: - FillColor = GetFillColor(fill, color, FillColorSource); + FillColor = GetFillColor(fill, color, FillColorSource, out opacity); break; case eFillStyle.GradientFill: GradientFill = new DrawGradientFill(DrawingRenderer.Theme, fill.GradientFill); FillColor = null; break; } + if (opacity.HasValue) + { + FillOpacity = opacity; + } } internal virtual void SetDrawingPropertiesBorder(ExcelDrawingBorder border, ExcelChartStyleColorManager color, bool hasBorder, double defaultWidth=1.5) { + double? opacity = null; switch (border.Fill.Style) { case eFillStyle.NoFill: if(border.Fill.IsEmpty) { - BorderColor = GetFillColor(border.Fill, color, BorderColorSource); + BorderColor = GetFillColor(border.Fill, color, BorderColorSource, out opacity); } else { @@ -136,7 +144,7 @@ internal virtual void SetDrawingPropertiesBorder(ExcelDrawingBorder border, Exce } break; case eFillStyle.SolidFill: - BorderColor = GetFillColor(border.Fill, color, BorderColorSource); + BorderColor = GetFillColor(border.Fill, color, BorderColorSource, out opacity); BorderGradientFill = null; break; case eFillStyle.GradientFill: @@ -145,6 +153,11 @@ internal virtual void SetDrawingPropertiesBorder(ExcelDrawingBorder border, Exce break; } + if (opacity.HasValue) + { + BorderOpacity = opacity; + } + if (hasBorder && BorderColorSource != PathFillMode.None) { BorderWidth = border.Width == 0 ? defaultWidth : border.Width; @@ -152,8 +165,9 @@ internal virtual void SetDrawingPropertiesBorder(ExcelDrawingBorder border, Exce { BorderDashArray = GetDashArray(border); } - if(border.CompoundLineStyle!=eCompundLineStyle.Single) + if(border.CompoundLineStyle!=eCompoundLineStyle.Single) { + CompoundLineStyle = border.CompoundLineStyle; //TODO:Add support double compound borders. } } @@ -197,8 +211,9 @@ private double[] GetDashArray(ExcelDrawingBorder border) return null; } - private string GetFillColor(ExcelDrawingFillBasic fill, ExcelDrawingColorManager styleFillColor, PathFillMode fillColorSource) + private string GetFillColor(ExcelDrawingFillBasic fill, ExcelDrawingColorManager styleFillColor, PathFillMode fillColorSource, out double? opacity) { + opacity = null; if (fillColorSource == PathFillMode.None) { return "none"; @@ -226,13 +241,12 @@ private string GetFillColor(ExcelDrawingFillBasic fill, ExcelDrawingColorManager } fc = ColorUtils.GetAdjustedColor(fillColorSource, fc); + if(fc.A<255 && fc!=Color.Empty) + { + opacity = fc.A/255D; + } return "#" + fc.ToArgb().ToString("x8").Substring(2); } - - //internal void SetTheme(ExcelTheme theme) - //{ - // theme = theme; - //} } internal abstract class RenderItemIndependent : RenderItemBase diff --git a/src/EPPlus.Export.ImageRenderer/RenderItems/SvgRenderItem.cs b/src/EPPlus.Export.ImageRenderer/RenderItems/SvgRenderItem.cs index 1c5b4a629..038f6cee6 100644 --- a/src/EPPlus.Export.ImageRenderer/RenderItems/SvgRenderItem.cs +++ b/src/EPPlus.Export.ImageRenderer/RenderItems/SvgRenderItem.cs @@ -15,9 +15,11 @@ Date Author Change using EPPlusImageRenderer.Svg; using OfficeOpenXml.Drawing; using OfficeOpenXml.Drawing.Theme; +using System; using System.Globalization; using System.Linq; using System.Text; +using System.Threading; namespace EPPlusImageRenderer.RenderItems { internal enum SvgFillType @@ -32,6 +34,11 @@ internal SvgRenderItem(DrawingBase renderer, BoundingBox parent) : base(renderer { } public override void Render(StringBuilder sb) + { + RenderBase(sb); + } + + private void RenderBase(StringBuilder sb) { if (string.IsNullOrEmpty(FillColor) == false) { @@ -46,7 +53,7 @@ public override void Render(StringBuilder sb) { sb.Append($"filter=\"{FilterName}\" "); } - + if (BorderWidth.HasValue) { if (string.IsNullOrEmpty(BorderColor) == false) @@ -58,15 +65,49 @@ public override void Render(StringBuilder sb) if (BorderDashArray != null) { - var BorderDashArrayStr = BorderDashArray.Select(x => + var BorderDashArrayStr = BorderDashArray.Select(x => x.ToString(CultureInfo.InvariantCulture)).ToArray(); sb.Append($"stroke-dasharray=\"" + $"{string.Join(",", BorderDashArrayStr)}\" "); } + if (BorderOpacity.HasValue) + { + sb.Append($" stroke-opacity=\"{(Math.Round(BorderOpacity.Value * 100)).ToString(CultureInfo.InvariantCulture)}%\" "); + } } - sb.Append($"stroke-miterlimit =\"8\" "); + sb.Append($"stroke-miterlimit =\"{StrokeMiterLimit}\" "); } + internal abstract SvgRenderItem Clone(SvgShape svgDocument); + private protected void RenderCompoundItems(StringBuilder sb, double? borderWidth, string color) + { + var tmpBorderWidth = BorderWidth; + string tmpBorderColor = null; + BorderWidth = borderWidth ?? BorderWidth; + if (string.IsNullOrEmpty(color) == false) + { + tmpBorderColor = BorderColor; + BorderColor = color; + } + + RenderBase(sb); + if (LineCap != eLineCap.Flat) + { + sb.AppendFormat(" stroke-linecap=\"{0}\"", LineCap == eLineCap.Round ? "round" : "square"); + } + if (LineJoin != SvgLineJoin.Miter) + { + sb.AppendFormat(" stroke-linejoin=\"{0}\"", LineJoin); + } + sb.AppendFormat("/>"); + + BorderWidth = tmpBorderWidth; + if (string.IsNullOrEmpty(color) == false) + { + BorderColor = tmpBorderColor; + } + } + } } \ No newline at end of file diff --git a/src/EPPlus.Export.ImageRenderer/RenderItems/SvgRenderLineItem.cs b/src/EPPlus.Export.ImageRenderer/RenderItems/SvgRenderLineItem.cs index 73b1526eb..cefae1388 100644 --- a/src/EPPlus.Export.ImageRenderer/RenderItems/SvgRenderLineItem.cs +++ b/src/EPPlus.Export.ImageRenderer/RenderItems/SvgRenderLineItem.cs @@ -92,17 +92,33 @@ private void UpdateBounds() public override void Render(StringBuilder sb) { - sb.AppendFormat(""); + + RenderLineItem(sb, BorderWidth, "white"); + RenderLineItem(sb, BorderWidth * (3D / 7D), "black"); + sb.Append($""); + break; + default: + RenderLineItem(sb, null, null); + break; } - sb.AppendFormat("/>"); + } + + private void RenderLineItem(StringBuilder sb, double? borderWidth, string color) + { + sb.AppendFormat(""); + + RenderPathItem(sb, BorderWidth, "white"); + RenderPathItem(sb, BorderWidth * (3D / 7D), "black"); + sb.Append($""); + break; + } + } + private void RenderPathItem(StringBuilder sb, double? borderWidth, string color) { var width = Bounds.Width.PointToPixel(); var height = Bounds.Height.PointToPixel(); @@ -45,8 +63,9 @@ public override void Render(StringBuilder sb) Commands[i].Render(width, height, sb); } sb.Append("\" "); - base.Render(sb); + RenderCompoundItems(sb, borderWidth, color); sb.Append("/>"); + } internal override SvgRenderItem Clone(SvgShape svgDocument) diff --git a/src/EPPlus.Export.ImageRenderer/Svg/Chart/ChartTypeDrawers/LineChartTypeDrawer.cs b/src/EPPlus.Export.ImageRenderer/Svg/Chart/ChartTypeDrawers/LineChartTypeDrawer.cs index 17fd969ce..0bd965f7a 100644 --- a/src/EPPlus.Export.ImageRenderer/Svg/Chart/ChartTypeDrawers/LineChartTypeDrawer.cs +++ b/src/EPPlus.Export.ImageRenderer/Svg/Chart/ChartTypeDrawers/LineChartTypeDrawer.cs @@ -16,8 +16,8 @@ internal LineChartTypeDrawer(SvgChart svgChart, ExcelChart chartType) : base(svg { var groupItem = new SvgGroupItem(ChartRenderer, _svgChart.Plotarea.Rectangle.Bounds); RenderItems.Add(groupItem); - var isStacked = Chart.IsTypeStacked(); - var isPercentStacked = Chart.IsTypePercentStacked(); + var isStacked = chartType.IsTypeStacked(); + var isPercentStacked = chartType.IsTypePercentStacked(); var xValues = new List>(); var yValues = new List>(); int serCounter = 0; @@ -39,11 +39,11 @@ internal LineChartTypeDrawer(SvgChart svgChart, ExcelChart chartType) : base(svg serCounter++; } - if (Chart.IsTypeStacked()) + if (chartType.IsTypeStacked()) { SumSeries(yValues); } - else if (Chart.IsTypePercentStacked()) + else if (chartType.IsTypePercentStacked()) { ExcelChartAxisStandard.CalculateStacked100(yValues); } @@ -81,16 +81,21 @@ private void SumSeries(List> series) } } private void AddLine(ExcelChart chartType, ExcelLineChartSerie serie, List xValues, List yValues, SvgChartSerieDataLabel serieDataLabel) - { - var xAxis = _svgChart.HorizontalAxis; - SvgChartAxis yAxis; + { + SvgChartAxis yAxis, xAxis; if (chartType.UseSecondaryAxis) { yAxis = _svgChart.SecondVerticalAxis; + xAxis = _svgChart.SecondHorizontalAxis; + if(xAxis.Axis.Deleted && xAxis.Values==null) + { + xAxis = _svgChart.HorizontalAxis; + } } else { yAxis = _svgChart.VerticalAxis; + xAxis = _svgChart.HorizontalAxis; } var linePath = new SvgRenderPathItem(ChartRenderer, _svgChart.Plotarea.Rectangle.Bounds); var coords = new List(); @@ -147,6 +152,8 @@ private void AddLine(ExcelChart chartType, ExcelLineChartSerie serie, List 2) + if (chart.Axis.Length > 3) + { + SecondVerticalAxis = new SvgChartAxis(this, (ExcelChartAxisStandard)chart.Axis[2]); + SecondHorizontalAxis = new SvgChartAxis(this, (ExcelChartAxisStandard)chart.Axis[3]); + } + else if(chart.Axis.Length > 2) { SecondVerticalAxis = new SvgChartAxis(this, (ExcelChartAxisStandard)chart.Axis[2]); } Plotarea = new SvgChartPlotarea(this); + //As we need the plotarea dimensions to calculate the axis positions we need to set the axis positions after creating the plotarea SetAxisPositionsFromPlotarea(this); - foreach (var ct in chart.PlotArea.ChartTypes) - { - Plotarea.ChartTypeDrawers = ChartTypeDrawer.Create(this); - } - + Plotarea.ChartTypeDrawers = ChartTypeDrawer.Create(this); } private void SetAxisPositionsFromPlotarea(SvgChart sc) { if(VerticalAxis != null) { - if (VerticalAxis.Rectangle != null) - { - VerticalAxis.Rectangle.Top = Plotarea.Rectangle.Top; - VerticalAxis.Rectangle.Height = Plotarea.Rectangle.Height; - VerticalAxis.Rectangle.Left = Plotarea.Rectangle.Left - VerticalAxis.Rectangle.Width; - VerticalAxis.Line.X1 = VerticalAxis.Line.X2 = (float)Plotarea.Rectangle.Left; - VerticalAxis.Line.Y1 = (float)VerticalAxis.Rectangle.Top; - VerticalAxis.Line.Y2 = (float)VerticalAxis.Rectangle.Bottom; - } - - if(VerticalAxis.Title!=null) - { - VerticalAxis.Title.Rectangle.Height = Plotarea.Rectangle.Height; - VerticalAxis.Title.Rectangle.Width = sc.Bounds.Width / 4; - VerticalAxis.Title.InitTextBox(); - VerticalAxis.Title.TextBox.Top = Plotarea.Rectangle.Top + (Plotarea.Rectangle.Height / 2) - (VerticalAxis.Title.TextBox.Height / 2); - if (VerticalAxis.Rectangle == null) - { - VerticalAxis.Title.TextBox.Left = sc.ChartArea.LeftMargin; - } - else - { - VerticalAxis.Title.TextBox.Left = VerticalAxis.Rectangle.Left - VerticalAxis.Title.TextBox.Width ; - } - } - + PlaceVerticalAxis(sc, VerticalAxis); VerticalAxis.AddTickmarksAndValues(); } if (HorizontalAxis!=null && HorizontalAxis.Rectangle != null) { - HorizontalAxis.Rectangle.Top = Plotarea.Rectangle.Bottom; - HorizontalAxis.Rectangle.Width = Plotarea.Rectangle.Width; - HorizontalAxis.Rectangle.Left = Plotarea.Rectangle.Left; - HorizontalAxis.Line.Y1 = HorizontalAxis.Line.Y2 = (float)Plotarea.Rectangle.Bottom; - HorizontalAxis.Line.X1 = (float)HorizontalAxis.Rectangle.Left; - HorizontalAxis.Line.X2 = (float)HorizontalAxis.Rectangle.Right; - - if (HorizontalAxis.Title != null) - { - HorizontalAxis.Title.Rectangle.Height = sc.Bounds.Height / 4; - HorizontalAxis.Title.Rectangle.Width = HorizontalAxis.Rectangle?.Width ?? sc.Plotarea.Rectangle.Width; - HorizontalAxis.Title.InitTextBox(); - HorizontalAxis.Title.TextBox.Left = Plotarea.Rectangle.Left + (Plotarea.Rectangle.Width / 2) - (HorizontalAxis.Title.TextBox.Width / 2); - HorizontalAxis.Title.TextBox.Top = HorizontalAxis.Rectangle.Bottom; - } + PlaceHorizontalAxis(sc, HorizontalAxis); //Make sure the horizontal axis is moved up if the vertical axis has a negative minimum value, so that the 0 value is at the correct position. - if (VerticalAxis.Axis.AxisType == eAxisType.Val && VerticalAxis.Min < 0D) + if (VerticalAxis.Axis.AxisType == eAxisType.Val && VerticalAxis.Min < 0D) { var newtop = VerticalAxis.GetPositionInPlotarea(0D) + sc.Plotarea.Rectangle.Top; var topDiff = HorizontalAxis.Rectangle.Top - newtop; @@ -134,26 +97,103 @@ private void SetAxisPositionsFromPlotarea(SvgChart sc) if (SecondVerticalAxis!=null) { - if (SecondVerticalAxis.Rectangle != null) + PlaceVerticalAxis(sc, SecondVerticalAxis); + SecondVerticalAxis.AddTickmarksAndValues(); + } + + if (SecondHorizontalAxis != null && SecondHorizontalAxis.Rectangle != null) + { + PlaceHorizontalAxis(sc, SecondHorizontalAxis); + SecondHorizontalAxis.AddTickmarksAndValues(); + } + } + + private void PlaceHorizontalAxis(SvgChart sc, SvgChartAxis horizontalAxis) + { + horizontalAxis.Rectangle.Width = Plotarea.Rectangle.Width; + horizontalAxis.Rectangle.Left = Plotarea.Rectangle.Left; + horizontalAxis.Line.X1 = (float)horizontalAxis.Rectangle.Left; + horizontalAxis.Line.X2 = (float)horizontalAxis.Rectangle.Right; + + if (horizontalAxis.Title == null) + { + if (horizontalAxis.Axis.AxisPosition == eAxisPosition.Bottom) { - SecondVerticalAxis.Rectangle.Top = Plotarea.Rectangle.Top; - SecondVerticalAxis.Rectangle.Height = Plotarea.Rectangle.Height; - SecondVerticalAxis.Rectangle.Left = Plotarea.Rectangle.Left - SecondVerticalAxis.Rectangle.Width; - SecondVerticalAxis.Line.X1 = SecondVerticalAxis.Line.X2 = (float)Plotarea.Rectangle.Left; - SecondVerticalAxis.Line.Y1 = (float)SecondVerticalAxis.Rectangle.Top; - SecondVerticalAxis.Line.Y2 = (float)SecondVerticalAxis.Rectangle.Bottom; + horizontalAxis.Rectangle.Top = Plotarea.Rectangle.Bottom; + horizontalAxis.Line.Y1 = horizontalAxis.Line.Y2 = (float)Plotarea.Rectangle.Bottom; } + else + { + horizontalAxis.Rectangle.Top = Plotarea.Rectangle.Top-horizontalAxis.Rectangle.Height; + horizontalAxis.Line.Y1 = horizontalAxis.Line.Y2 = (float)Plotarea.Rectangle.Top; + } + } + else + { + horizontalAxis.Title.Rectangle.Height = sc.Bounds.Height / 4; + horizontalAxis.Title.Rectangle.Width = horizontalAxis.Rectangle?.Width ?? sc.Plotarea.Rectangle.Width; + horizontalAxis.Title.InitTextBox(); + if (horizontalAxis.Axis.AxisPosition == eAxisPosition.Bottom) + { + horizontalAxis.Title.TextBox.Top = horizontalAxis.Rectangle.Bottom; + } + else + { + horizontalAxis.Title.TextBox.Top = horizontalAxis.Rectangle.Top - horizontalAxis.Title.TextBox.Height; + } + horizontalAxis.Title.TextBox.Left = Plotarea.Rectangle.Left + (Plotarea.Rectangle.Width / 2) - (horizontalAxis.Title.TextBox.Width / 2); + } + } - if (SecondVerticalAxis.Title != null) + private void PlaceVerticalAxis(SvgChart sc, SvgChartAxis verticalAxis) + { + if (verticalAxis.Rectangle != null) + { + verticalAxis.Rectangle.Top = Plotarea.Rectangle.Top; + verticalAxis.Rectangle.Height = Plotarea.Rectangle.Height; + verticalAxis.Line.Y1 = (float)verticalAxis.Rectangle.Top; + verticalAxis.Line.Y2 = (float)verticalAxis.Rectangle.Bottom; + if (verticalAxis.Axis.AxisPosition == eAxisPosition.Left) + { + verticalAxis.Rectangle.Left = Plotarea.Rectangle.Left - verticalAxis.Rectangle.Width; + verticalAxis.Line.X1 = verticalAxis.Line.X2 = (float)Plotarea.Rectangle.Left; + } + else { - SecondVerticalAxis.Title.Rectangle.Height = SecondVerticalAxis.Rectangle.Height; - SecondVerticalAxis.Title.Rectangle.Width = sc.Bounds.Width / 4; - SecondVerticalAxis.Title.InitTextBox(); - SecondVerticalAxis.Title.TextBox.Top = Plotarea.Rectangle.Top + (Plotarea.Rectangle.Height / 2) - (SecondVerticalAxis.Title.TextBox.Height / 2); - SecondVerticalAxis.Title.TextBox.Left = SecondVerticalAxis.Title.Rectangle.Left; + verticalAxis.Rectangle.Left = Plotarea.Rectangle.Right; + verticalAxis.Line.X1 = verticalAxis.Line.X2 = (float)Plotarea.Rectangle.Right; } + } - SecondVerticalAxis.AddTickmarksAndValues(); + if (verticalAxis.Title != null) + { + verticalAxis.Title.Rectangle.Height = Plotarea.Rectangle.Height; + verticalAxis.Title.Rectangle.Width = sc.Bounds.Width / 4; + verticalAxis.Title.InitTextBox(); + verticalAxis.Title.TextBox.Top = Plotarea.Rectangle.Top + (Plotarea.Rectangle.Height / 2) - (verticalAxis.Title.TextBox.Height / 2); + + if (verticalAxis.Axis.AxisPosition == eAxisPosition.Left) + { + if (verticalAxis.Rectangle == null) + { + verticalAxis.Title.TextBox.Left = sc.ChartArea.LeftMargin; + } + else + { + verticalAxis.Title.TextBox.Left = verticalAxis.Rectangle.Left - verticalAxis.Title.TextBox.Width; + } + } + else + { + if (verticalAxis.Rectangle == null) + { + verticalAxis.Title.TextBox.Left = Plotarea.Rectangle.Right; + } + else + { + verticalAxis.Title.TextBox.Left = verticalAxis.Rectangle.Right; + } + } } } @@ -164,6 +204,7 @@ private void SetAxisPositionsFromPlotarea(SvgChart sc) internal SvgChartAxis VerticalAxis { get; set; } internal SvgChartAxis HorizontalAxis { get; set; } internal SvgChartAxis SecondVerticalAxis { get; set; } + internal SvgChartAxis SecondHorizontalAxis { get; set; } internal SvgChartTitle VerticalAxisTitle { get; set; } internal SvgChartTitle HorizontalAxisTitle { get; set; } internal SvgChartTitle SecondVerticalAxisTitle { get; set; } @@ -189,6 +230,7 @@ public void Render(StringBuilder sb) HorizontalAxis?.AppendRenderItems(RenderItems); VerticalAxis?.AppendRenderItems(RenderItems); + SecondHorizontalAxis?.AppendRenderItems(RenderItems); SecondVerticalAxis?.AppendRenderItems(RenderItems); if (Plotarea != null) diff --git a/src/EPPlus.Export.ImageRenderer/Svg/Chart/SvgChartAxis.cs b/src/EPPlus.Export.ImageRenderer/Svg/Chart/SvgChartAxis.cs index c0c504ae8..d107a693d 100644 --- a/src/EPPlus.Export.ImageRenderer/Svg/Chart/SvgChartAxis.cs +++ b/src/EPPlus.Export.ImageRenderer/Svg/Chart/SvgChartAxis.cs @@ -27,6 +27,7 @@ Date Author Change using OfficeOpenXml.Utils.TypeConversion; using System; using System.Collections.Generic; +using System.Globalization; using System.Linq; using System.Linq.Expressions; using System.Text; @@ -37,11 +38,11 @@ internal class SvgChartAxis : SvgChartObject, IDrawingChartAxis { internal SvgChartAxis(SvgChart sc, ExcelChartAxisStandard ax) : base(sc) { - SvgChart= sc; + SvgChart = sc; Axis = ax; SetMargins(ax.TextBody); - if (sc.Chart.Series.Count == 0 || (ax.Deleted == true && ax.Title == null)) + if (sc.Chart.Series.Count == 0 || (ax.Deleted == true && ax.HasTitle==false)) { return; } @@ -100,6 +101,7 @@ internal SvgChartAxis(SvgChart sc, ExcelChartAxisStandard ax) : base(sc) else { Rectangle.Height = GetTextHeight(sc, ax); + //TODO:Fix Rectangle.Top = Title == null || ax.AxisPosition == eAxisPosition.Top ? sc.ChartArea.Rectangle.Height - 8 - Rectangle.Height : Title.Rectangle.Top - Rectangle.Height - 8; } } @@ -107,7 +109,11 @@ internal SvgChartAxis(SvgChart sc, ExcelChartAxisStandard ax) : base(sc) Rectangle.FillColor = "none"; Line = new SvgRenderLineItem(sc, Rectangle.Bounds); - Line.SetDrawingPropertiesBorder(ax.Border, sc.Chart.StyleManager.Style.Title.BorderReference.Color, ax.Border.Fill.Style != eFillStyle.NoFill, 0.75); + Line.SetDrawingPropertiesBorder(ax.Border, sc.Chart.StyleManager.Style.Title.BorderReference.Color, ax.Border.Fill.Style != eFillStyle.NoFill, 1); + if(Line.BorderWidth < 1) + { + Line.BorderWidth = 1; + } } } } @@ -332,13 +338,24 @@ private List GetAxisValueTextBoxes() if(Axis.IsVertical) { //If the axis is vertical, we need to adjust the left position of the textboxes to align them to the right and not have them overlap with the axis line. - foreach(var tb in ret) + if (Axis.AxisPosition == eAxisPosition.Left) { - tb.Left += (widest - tb.Width); + foreach (var tb in ret) + { + tb.Left += (widest - tb.Width); + } + } + else + { + foreach (var tb in ret) + { + tb.Left += LeftMargin; + } } } else { + //Align the axis labels according to the label alignment setting. This is only relevant for horizontal axis, vertical axis are always right aligned. var lblAlignment = (Axis as ExcelChartAxisStandard)?.LabelAlignment??OfficeOpenXml.eAxisLabelAlignment.Center; var majorWidth = Rectangle.Width / AxisValues.Count; foreach (var tb in ret) @@ -363,7 +380,7 @@ private List GetAxisValueTextBoxes() private double GetAxisItemLeft(int i, OfficeOpenXml.Interfaces.Drawing.Text.TextMeasurement m) { if (Axis.AxisPosition == eAxisPosition.Left) - { + { return Rectangle.Left; } else if (Axis.AxisPosition == eAxisPosition.Right) @@ -393,22 +410,22 @@ private double GetAxisItemTop(int i, OfficeOpenXml.Interfaces.Drawing.Text.TextM { if (Axis.AxisPosition == eAxisPosition.Top) { - return Rectangle.Top - m.Height.PointToPixel() - TopMargin; + return Rectangle.Bottom - m.Height - TopMargin; } else if (Axis.AxisPosition == eAxisPosition.Bottom) { - return Rectangle.Bottom - m.Height.PointToPixel() + BottomMargin; + return Rectangle.Bottom - m.Height + BottomMargin; } else { var majorHeight = Rectangle.Height / (AxisValues.Count-1); if (Axis.AxisType == eAxisType.Cat) { - return Rectangle.Top + majorHeight * (AxisValues.Count - i - 1) + (majorHeight / 2) - m.Height.PointToPixel() / 2; + return Rectangle.Top + majorHeight * (AxisValues.Count - i - 1) + (majorHeight / 2) - m.Height / 2; } else { - return Rectangle.Top + majorHeight * (AxisValues.Count - i - 1) - m.Height.PointToPixel() / 2; + return Rectangle.Top + majorHeight * (AxisValues.Count - i - 1) - m.Height / 2; } } @@ -433,7 +450,7 @@ private List AddTickmarks(double units, eTimeUnit? dateUnit, { tickMarkWidthInside = tickMarkWidth; } - if(type==eAxisTickMark.Out|| type == eAxisTickMark.Cross) + if(type==eAxisTickMark.Out || type == eAxisTickMark.Cross) { tickMarkWidthOutside = tickMarkWidth; } @@ -462,8 +479,8 @@ private List AddTickmarks(double units, eTimeUnit? dateUnit, case eAxisPosition.Top: x1 = (float)(Rectangle.Left + ((d - min) / diff * Rectangle.Width)); x2 = x1; - y1 = (float)Rectangle.Bottom - tickMarkWidthInside; - y2 = (float)Rectangle.Bottom + tickMarkWidthOutside; + y1 = (float)Rectangle.Bottom - tickMarkWidthOutside; + y2 = (float)Rectangle.Bottom + tickMarkWidthInside; break; case eAxisPosition.Bottom: x1 = (float)(Rectangle.Left + ((d - min) / diff * Rectangle.Width)); @@ -480,6 +497,10 @@ private List AddTickmarks(double units, eTimeUnit? dateUnit, tm.X2 = x2; tm.Y2 = y2; tm.SetDrawingPropertiesBorder(Axis.Border, axisStyle.BorderReference.Color, true); + if(tm.BorderWidth<1) //Excel seems to have this as minimum width for tick marks, so we enforce it here to make sure they are visible. + { + tm.BorderWidth = 1; + } tms.Add(tm); } //if (dateUnit.HasValue) @@ -490,7 +511,16 @@ private List AddTickmarks(double units, eTimeUnit? dateUnit, d = DateTime.FromOADate(d).AddYears((int)units).ToOADate(); break; case eTimeUnit.Months: - d = DateTime.FromOADate(d).AddMonths((int)units).ToOADate(); + if (units>=1D) + { + d = DateTime.FromOADate(d).AddMonths((int)units).ToOADate(); + } + else + { + var dt = DateTime.FromOADate(d); + var days = DateTime.DaysInMonth(dt.Year, dt.Month) * units; + d += days; + } break; default: d += units; @@ -660,7 +690,7 @@ protected List GetAxisValue(ExcelChartAxisStandard ax, RenderItem rect, { l.Add(i); } - return l; + return l; } if (ax.IsDate) { diff --git a/src/EPPlus.Export.ImageRenderer/Svg/Chart/SvgChartPlotarea.cs b/src/EPPlus.Export.ImageRenderer/Svg/Chart/SvgChartPlotarea.cs index 4d50cd5fc..e657a2e2f 100644 --- a/src/EPPlus.Export.ImageRenderer/Svg/Chart/SvgChartPlotarea.cs +++ b/src/EPPlus.Export.ImageRenderer/Svg/Chart/SvgChartPlotarea.cs @@ -38,33 +38,10 @@ internal SvgRenderRectItem GetPlotAreaRectangle(SvgChart sc) } else { - var lp = sc.Chart.Legend?.Position; - rect.Top = (lp==eLegendPosition.Top ? sc.Legend.Bounds.Bottom : sc.Title?.Rectangle?.GlobalBottom ?? 0d) + TopMargin; - if(sc.HorizontalAxis!=null && sc.Chart.XAxis.LabelPosition==eTickLabelPosition.High) - { - rect.Top += sc.HorizontalAxis.Rectangle.Height; - } - rect.Left = lp == eLegendPosition.Left ? sc.Legend.Bounds.Right + LeftMargin : LeftMargin; - if(sc.VerticalAxis!=null) - { - rect.Left = sc.VerticalAxis.Rectangle?.GlobalRight ?? sc.VerticalAxis.Title.Rectangle.GlobalRight; - } - - rect.Width = ((lp == eLegendPosition.Right || lp == eLegendPosition.TopRight) && sc.Legend != null ? - sc.Legend.Bounds.GlobalLeft - RightMargin : - sc.ChartArea.Rectangle.Width - RightMargin) - - rect.GlobalLeft; - - double vaHeight=0, vaTitleHeight=0; - if(sc.HorizontalAxis != null) - { - vaHeight = (sc.HorizontalAxis.Rectangle?.Height ?? 0D) + (sc.HorizontalAxis.Title?.Rectangle?.Height ?? 0D); - } - if(lp==eLegendPosition.Bottom) - { - vaHeight += sc.Legend.Rectangle.Height; - } - rect.Height = sc.Bounds.Height - rect.GlobalTop - vaHeight - vaTitleHeight - BottomMargin; + rect.Top = GetPlotAreaTop(sc); + rect.Left = GetPlotAreaLeft(sc); + rect.Width = GetPlotAreaWidth(sc, rect); + rect.Height = GetPlotAreaHeight(sc, rect); } rect.SetDrawingPropertiesFill(pa.Fill, sc.Chart.StyleManager.Style.PlotArea.FillReference.Color); @@ -72,6 +49,93 @@ internal SvgRenderRectItem GetPlotAreaRectangle(SvgChart sc) return rect; } + private double GetPlotAreaHeight(SvgChart sc, SvgRenderRectItem rect) + { + var bottomAxis = GetAxisByPosition(sc, eAxisPosition.Bottom); + double vaHeight = 0, vaTitleHeight = 0; + if (bottomAxis!=null) + { + vaHeight = (bottomAxis.Rectangle?.Height ?? 0D) + (bottomAxis.Title?.Rectangle?.Height ?? 0D); + } + if (sc.Chart.Legend?.Position == eLegendPosition.Bottom) + { + vaHeight += sc.Legend.Rectangle.Height; + } + return sc.Bounds.Height - rect.GlobalTop - vaHeight - vaTitleHeight - BottomMargin; + } + + private double GetPlotAreaWidth(SvgChart sc, SvgRenderRectItem rect) + { + var rightAxis = GetAxisByPosition(sc, eAxisPosition.Right); + var lp = sc.Chart.Legend?.Position; + var left = ((lp == eLegendPosition.Right || lp == eLegendPosition.TopRight) && sc.Legend != null ? + sc.Legend.Bounds.GlobalLeft - RightMargin : + sc.ChartArea.Rectangle.Width - RightMargin); + if (rightAxis == null) + { + return left - rect.GlobalLeft; + } + else + { + var rightWidth = rightAxis.Title?.Rectangle?.Width??0D + rightAxis.Rectangle?.Width??0D; + return left - rightWidth - rect.Left; + } + } + private double GetPlotAreaLeft(SvgChart sc) + { + var leftAxis = GetAxisByPosition(sc, eAxisPosition.Left); + if (leftAxis == null) + { + return sc.Chart.Legend?.Position == eLegendPosition.Left ? sc.Legend.Bounds.Right + LeftMargin : LeftMargin; + } + else + { + return leftAxis.Rectangle == null ? leftAxis.Title.Rectangle.GlobalRight: leftAxis.Rectangle.GlobalRight; + } + } + + private double GetPlotAreaTop(SvgChart sc) + { + double haHeight = 0; + var topAxis = GetAxisByPosition(sc, eAxisPosition.Top); + if (topAxis == null) + { + //var bottomAxis = GetAxisByPosition(sc, eAxisPosition.Bottom); + //if (bottomAxis != null && sc.Chart.XAxis.LabelPosition == eTickLabelPosition.High) + //{ + // top += bottomAxis.Rectangle.Height; + //} + //return top; + } + else + { + haHeight = (topAxis.Rectangle?.Height ?? 0D) + (topAxis.Title?.Rectangle?.Height ?? 0D); + } + + return (sc.Chart.Legend?.Position == eLegendPosition.Top ? sc.Legend.Bounds.Bottom : sc.Title?.Rectangle?.GlobalBottom ?? 0d) + haHeight + TopMargin; + } + + private SvgChartAxis GetAxisByPosition(SvgChart sc, eAxisPosition pos) + { + if (sc.HorizontalAxis != null && sc.HorizontalAxis.Axis.AxisPosition == pos) + { + return sc.HorizontalAxis; + } + else if (sc.VerticalAxis != null && sc.VerticalAxis.Axis.AxisPosition == pos) + { + return sc.VerticalAxis; + } + else if (sc.SecondHorizontalAxis != null && sc.SecondHorizontalAxis.Axis.AxisPosition == pos) + { + return sc.SecondHorizontalAxis; + } + else if (sc.SecondVerticalAxis != null && sc.SecondVerticalAxis.Axis.AxisPosition == pos) + { + return sc.SecondVerticalAxis; + } + return null; + } + internal override void AppendRenderItems(List renderItems) { renderItems.Add(Rectangle); diff --git a/src/EPPlus.Export.ImageRenderer/Svg/Chart/SvgChartTitle.cs b/src/EPPlus.Export.ImageRenderer/Svg/Chart/SvgChartTitle.cs index 93f207e87..11165564e 100644 --- a/src/EPPlus.Export.ImageRenderer/Svg/Chart/SvgChartTitle.cs +++ b/src/EPPlus.Export.ImageRenderer/Svg/Chart/SvgChartTitle.cs @@ -116,10 +116,18 @@ private void SetAxisTitleRect(SvgChart sc, SvgChartAxis axis) Rectangle.Top = sc.GetPlotAreaTop(); Rectangle.Left = sc.Chart.HasLegend && sc.Chart.Legend.Position == eLegendPosition.Left ? sc.Legend.Rectangle.Right : 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 : sc.Bounds.Right - Rectangle.Width - margin; + break; case eAxisPosition.Bottom: Rectangle.Top = sc.ChartArea.Rectangle.Height - margin - Rectangle.Height; Rectangle.Left = GetHorizontalLeft(sc); break; + case eAxisPosition.Top: + Rectangle.Top = sc.Title != null && sc.Title._title.Layout.HasLayout==false ? sc.Title.Rectangle.Bottom+margin : margin; + Rectangle.Left = GetHorizontalLeft(sc); + break; } } diff --git a/src/EPPlus/Drawing/Chart/ExcelChart.cs b/src/EPPlus/Drawing/Chart/ExcelChart.cs index dd7a49701..717eb50fd 100644 --- a/src/EPPlus/Drawing/Chart/ExcelChart.cs +++ b/src/EPPlus/Drawing/Chart/ExcelChart.cs @@ -24,6 +24,7 @@ Date Author Change using System.Collections.Generic; using System.Globalization; using System.IO; +using System.Linq; using System.Text; using System.Xml; @@ -105,7 +106,7 @@ private bool HasPrimaryAxis() return false; } internal abstract void AddAxis(); - bool _secondaryAxis = false; + bool? _secondaryAxis = null; /// /// If true the charttype will use the secondary axis. /// The chart must contain a least one other charttype that uses the primary axis. @@ -114,7 +115,11 @@ public bool UseSecondaryAxis { get { - return _secondaryAxis; + if (_secondaryAxis.HasValue == false) + { + _secondaryAxis = Array.FindIndex(Axis, x => ((ExcelChartAxis)x).Id == YAxis.Id) > 1; + } + return _secondaryAxis.Value; } set { diff --git a/src/EPPlus/Drawing/Chart/ExcelChartAxisStandard.cs b/src/EPPlus/Drawing/Chart/ExcelChartAxisStandard.cs index 2e513eff9..f8affc2b7 100644 --- a/src/EPPlus/Drawing/Chart/ExcelChartAxisStandard.cs +++ b/src/EPPlus/Drawing/Chart/ExcelChartAxisStandard.cs @@ -814,7 +814,7 @@ internal void GetSeriesValues(out bool isCount, out List> values) List pl = new List(); foreach (var ct in _chart.PlotArea.ChartTypes) { - foreach (var serie in _chart.Series) + foreach (var serie in ct.Series) { var l = new List(); values.Add(l); @@ -832,11 +832,11 @@ internal void GetSeriesValues(out bool isCount, out List> values) } else { - if (ct.YAxis == this) + if (ct.YAxis.Id == Id) { AddFromSerie(l, serie.Series, serie.NumberLiteralsY, serie.StringLiteralsY, false, pl); } - else if (ct.XAxis == this) + else if (ct.XAxis.Id == Id) { AddFromSerie(l, serie.XSeries, serie.NumberLiteralsX, serie.StringLiteralsX, false); } diff --git a/src/EPPlus/Drawing/Chart/ExcelChartTitle.cs b/src/EPPlus/Drawing/Chart/ExcelChartTitle.cs index 99b920348..bb448d676 100644 --- a/src/EPPlus/Drawing/Chart/ExcelChartTitle.cs +++ b/src/EPPlus/Drawing/Chart/ExcelChartTitle.cs @@ -340,7 +340,7 @@ public double Rotation { get { - var i=GetXmlNodeInt($"{_fontPropertiesPath}/a:bodyPr/@rot"); + var i=GetXmlNodeInt($"{_fontPropertiesPath}/a:bodyPr/@rot", 0); if (i < 0) { return 360 + (i / 60000); diff --git a/src/EPPlus/Drawing/Enums/EnumTransl.cs b/src/EPPlus/Drawing/Enums/EnumTransl.cs index d1c66ff1a..6bc27dcb3 100644 --- a/src/EPPlus/Drawing/Enums/EnumTransl.cs +++ b/src/EPPlus/Drawing/Enums/EnumTransl.cs @@ -94,34 +94,34 @@ internal static eLineCap ToLineCap(string text) return eLineCap.Flat; } } - internal static eCompundLineStyle ToLineCompound(string s) + internal static eCompoundLineStyle ToLineCompound(string s) { switch (s) { case "dbl": - return eCompundLineStyle.Double; - case "sng": - return eCompundLineStyle.Single; + return eCompoundLineStyle.Double; case "thickThin": - return eCompundLineStyle.DoubleThickThin; + return eCompoundLineStyle.DoubleThickThin; case "thinThick": - return eCompundLineStyle.DoubleThinThick; + return eCompoundLineStyle.DoubleThinThick; + case "tri": + return eCompoundLineStyle.TripleThinThickThin; default: - return eCompundLineStyle.TripleThinThickThin; + return eCompoundLineStyle.Single; } } - internal static string FromLineCompound(eCompundLineStyle v) + internal static string FromLineCompound(eCompoundLineStyle v) { switch (v) { - case eCompundLineStyle.Double: + case eCompoundLineStyle.Double: return "dbl"; - case eCompundLineStyle.Single: + case eCompoundLineStyle.Single: return "sng"; - case eCompundLineStyle.DoubleThickThin: + case eCompoundLineStyle.DoubleThickThin: return "thickThin"; - case eCompundLineStyle.DoubleThinThick: + case eCompoundLineStyle.DoubleThinThick: return "thinThick"; default: return "tri"; diff --git a/src/EPPlus/Drawing/Enums/eCompundLineStyle.cs b/src/EPPlus/Drawing/Enums/eCompoundLineStyle.cs similarity index 97% rename from src/EPPlus/Drawing/Enums/eCompundLineStyle.cs rename to src/EPPlus/Drawing/Enums/eCompoundLineStyle.cs index 468ec03c2..21d71d19c 100644 --- a/src/EPPlus/Drawing/Enums/eCompundLineStyle.cs +++ b/src/EPPlus/Drawing/Enums/eCompoundLineStyle.cs @@ -15,7 +15,7 @@ namespace OfficeOpenXml.Drawing /// /// The compound line type. Used for underlining text /// - public enum eCompundLineStyle + public enum eCompoundLineStyle { /// /// Double lines with equal width diff --git a/src/EPPlus/Drawing/ExcelDrawingBorder.cs b/src/EPPlus/Drawing/ExcelDrawingBorder.cs index fc3741bd8..f9c749bfe 100644 --- a/src/EPPlus/Drawing/ExcelDrawingBorder.cs +++ b/src/EPPlus/Drawing/ExcelDrawingBorder.cs @@ -110,7 +110,7 @@ private void InitSpPr() /// /// The compound line type that is to be used for lines with text such as underlines /// - public eCompundLineStyle CompoundLineStyle + public eCompoundLineStyle CompoundLineStyle { get { diff --git a/src/EPPlus/Drawing/Theme/ExcelThemeLine.cs b/src/EPPlus/Drawing/Theme/ExcelThemeLine.cs index 843488b07..8fa764b01 100644 --- a/src/EPPlus/Drawing/Theme/ExcelThemeLine.cs +++ b/src/EPPlus/Drawing/Theme/ExcelThemeLine.cs @@ -69,7 +69,7 @@ public eLineCap Cap /// /// The compound line type to be used for the underline stroke /// - public eCompundLineStyle CompoundLineStyle + public eCompoundLineStyle CompoundLineStyle { get { diff --git a/src/EPPlus/Utils/TypeConversion/ColorConverter.cs b/src/EPPlus/Utils/TypeConversion/ColorConverter.cs index dab444c20..aa8f65397 100644 --- a/src/EPPlus/Utils/TypeConversion/ColorConverter.cs +++ b/src/EPPlus/Utils/TypeConversion/ColorConverter.cs @@ -73,6 +73,9 @@ private static Color ApplyTransforms(Color c, ExcelColorTransformCollection tran case eColorTransformType.LumOff: c = ApplyLumMod(c, 1, v); break; + case eColorTransformType.Alpha: + c = Color.FromArgb((byte)Math.Round(255 * v), c.R, c.G, c.B); + break; } } return c; diff --git a/src/EPPlusTest/Drawing/BorderTest.cs b/src/EPPlusTest/Drawing/BorderTest.cs index 6dca521cd..600f4975f 100644 --- a/src/EPPlusTest/Drawing/BorderTest.cs +++ b/src/EPPlusTest/Drawing/BorderTest.cs @@ -84,14 +84,14 @@ public void BorderWidthStyle() shape.Border.Fill.Color = Color.Red; shape.Border.Width = 12; shape.Border.LineStyle = eLineStyle.Dot; - shape.Border.CompoundLineStyle = eCompundLineStyle.TripleThinThickThin; + shape.Border.CompoundLineStyle = eCompoundLineStyle.TripleThinThickThin; //Assert Assert.AreEqual(eFillStyle.SolidFill, shape.Border.Fill.Style); Assert.IsNotNull(shape.Border.Fill.SolidFill); Assert.AreEqual(12, shape.Border.Width); Assert.AreEqual(eLineStyle.Dot, shape.Border.LineStyle); - Assert.AreEqual(eCompundLineStyle.TripleThinThickThin, shape.Border.CompoundLineStyle); + Assert.AreEqual(eCompoundLineStyle.TripleThinThickThin, shape.Border.CompoundLineStyle); } [TestMethod] public void BorderAlignRoundJoin() @@ -105,7 +105,7 @@ public void BorderAlignRoundJoin() //Act shape.Border.Fill.Color = Color.Red; shape.Border.LineStyle = eLineStyle.LongDashDotDot; - shape.Border.CompoundLineStyle = eCompundLineStyle.Double; + shape.Border.CompoundLineStyle = eCompoundLineStyle.Double; shape.Border.Alignment = ePenAlignment.Inset; shape.Border.LineCap = eLineCap.Square; shape.Border.Join = eLineJoin.Round; @@ -114,7 +114,7 @@ public void BorderAlignRoundJoin() Assert.AreEqual(eFillStyle.SolidFill, shape.Border.Fill.Style); Assert.IsNotNull(shape.Border.Fill.SolidFill); Assert.AreEqual(eLineStyle.LongDashDotDot, shape.Border.LineStyle); - Assert.AreEqual(eCompundLineStyle.Double, shape.Border.CompoundLineStyle); + Assert.AreEqual(eCompoundLineStyle.Double, shape.Border.CompoundLineStyle); Assert.AreEqual(ePenAlignment.Inset, shape.Border.Alignment); Assert.AreEqual(eLineJoin.Round, shape.Border.Join); Assert.AreEqual(eLineCap.Square, shape.Border.LineCap); @@ -131,7 +131,7 @@ public void BorderMitterJoin() //Act shape.Border.Fill.Color = Color.Red; shape.Border.LineStyle = eLineStyle.LongDashDotDot; - shape.Border.CompoundLineStyle = eCompundLineStyle.Double; + shape.Border.CompoundLineStyle = eCompoundLineStyle.Double; shape.Border.LineCap = eLineCap.Flat; shape.Border.Join = eLineJoin.Bevel; shape.Border.MiterJoinLimit=10000; //Sets join to Miter @@ -140,7 +140,7 @@ public void BorderMitterJoin() Assert.AreEqual(eFillStyle.SolidFill, shape.Border.Fill.Style); Assert.IsNotNull(shape.Border.Fill.SolidFill); Assert.AreEqual(eLineStyle.LongDashDotDot, shape.Border.LineStyle); - Assert.AreEqual(eCompundLineStyle.Double, shape.Border.CompoundLineStyle); + Assert.AreEqual(eCompoundLineStyle.Double, shape.Border.CompoundLineStyle); Assert.AreEqual(eLineJoin.Miter, shape.Border.Join); Assert.AreEqual(10000, shape.Border.MiterJoinLimit); Assert.AreEqual(eLineCap.Flat, shape.Border.LineCap); diff --git a/src/EPPlusTest/Drawing/Chart/Styling/StylingTest.cs b/src/EPPlusTest/Drawing/Chart/Styling/StylingTest.cs index a538c8261..6f51d844e 100644 --- a/src/EPPlusTest/Drawing/Chart/Styling/StylingTest.cs +++ b/src/EPPlusTest/Drawing/Chart/Styling/StylingTest.cs @@ -140,7 +140,7 @@ public void StyleLineLoaded() Assert.AreEqual(9, chart.StyleManager.Style.CategoryAxis.DefaultTextRun.Size); Assert.AreEqual(eTextCapsType.None, chart.StyleManager.Style.CategoryAxis.DefaultTextRun.Capitalization); Assert.AreEqual(ePenAlignment.Center, chart.StyleManager.Style.CategoryAxis.Border.Alignment); - Assert.AreEqual(eCompundLineStyle.Single, chart.StyleManager.Style.CategoryAxis.Border.CompoundLineStyle); + Assert.AreEqual(eCompoundLineStyle.Single, chart.StyleManager.Style.CategoryAxis.Border.CompoundLineStyle); Assert.AreEqual(eLineCap.Flat, chart.StyleManager.Style.CategoryAxis.Border.LineCap); Assert.AreEqual(1.5, chart.StyleManager.Style.CategoryAxis.Border.Width); Assert.AreEqual(eFillStyle.SolidFill, chart.StyleManager.Style.CategoryAxis.Border.Fill.Style); diff --git a/src/EPPlusTest/Drawing/ThemeTest.cs b/src/EPPlusTest/Drawing/ThemeTest.cs index a1bb671da..55643c9b6 100644 --- a/src/EPPlusTest/Drawing/ThemeTest.cs +++ b/src/EPPlusTest/Drawing/ThemeTest.cs @@ -236,7 +236,7 @@ public void LoadThmx_BordersScheme() Assert.AreEqual(eLineStyle.Solid, currentTheme.FormatScheme.BorderStyle[0].Style); Assert.AreEqual(ePenAlignment.Center, currentTheme.FormatScheme.BorderStyle[0].Alignment); Assert.AreEqual(eLineCap.Flat, currentTheme.FormatScheme.BorderStyle[0].Cap); - Assert.AreEqual(eCompundLineStyle.Single, currentTheme.FormatScheme.BorderStyle[0].CompoundLineStyle); + Assert.AreEqual(eCompoundLineStyle.Single, currentTheme.FormatScheme.BorderStyle[0].CompoundLineStyle); Assert.IsNull(currentTheme.FormatScheme.BorderStyle[0].HeadEnd.Width); Assert.IsNull(currentTheme.FormatScheme.BorderStyle[0].HeadEnd.Height); Assert.IsNull(currentTheme.FormatScheme.BorderStyle[0].HeadEnd.Style); From a86ecc569c0375edf26766c3064d5483d873ab2e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ossian=20Edstr=C3=B6m?= Date: Mon, 16 Mar 2026 09:30:12 +0100 Subject: [PATCH 095/151] Added connection points calculation etc --- .../RenderItems/SvgGroupItem.cs | 4 + .../RenderItems/SvgItem/PointLines.cs | 7 +- .../SvgItem/SvgChartDataLabelStandard.cs | 173 ++++++++++-------- 3 files changed, 100 insertions(+), 84 deletions(-) diff --git a/src/EPPlus.Export.ImageRenderer/RenderItems/SvgGroupItem.cs b/src/EPPlus.Export.ImageRenderer/RenderItems/SvgGroupItem.cs index a316d7f19..c8981cb4e 100644 --- a/src/EPPlus.Export.ImageRenderer/RenderItems/SvgGroupItem.cs +++ b/src/EPPlus.Export.ImageRenderer/RenderItems/SvgGroupItem.cs @@ -45,6 +45,10 @@ internal SvgGroupItem(DrawingBase renderer) : base(renderer) { } + internal SvgGroupItem(DrawingBase renderer, double xPos, double yPos) : base(renderer) + { + GroupTransform = $"transform=\"translate({xPos.PointToPixelString()}, {yPos.PointToPixelString()})\""; + } internal SvgGroupItem(DrawingBase renderer, BoundingBox bounds) : base(renderer) { diff --git a/src/EPPlus.Export.ImageRenderer/RenderItems/SvgItem/PointLines.cs b/src/EPPlus.Export.ImageRenderer/RenderItems/SvgItem/PointLines.cs index 9a0657d12..7852136ea 100644 --- a/src/EPPlus.Export.ImageRenderer/RenderItems/SvgItem/PointLines.cs +++ b/src/EPPlus.Export.ImageRenderer/RenderItems/SvgItem/PointLines.cs @@ -15,7 +15,7 @@ internal class PointLines : DrawingObject { internal List RenderLines = new List(); - internal ConnectionPointsMiddle connectionPoints; + internal ConnectionPointsMiddle ConnectionPoints; private PointLines(DrawingBase renderer) : base(renderer) { @@ -30,12 +30,13 @@ internal PointLines(DrawingBase renderer, BoundingBox parent, ConnectionPointsMi Bounds.Top = parent.Top; Bounds.Width = parent.Width; Bounds.Height = parent.Height; + ConnectionPoints = connectionPoints; //Add connection points to render List ptColors = new List { "red", "green", "blue", "yellow" }; - for (int i = 0; i < connectionPoints.Points.Count; i++) + for (int i = 0; i < ConnectionPoints.Points.Count; i++) { - var cPoint = connectionPoints.Points[i]; + var cPoint = ConnectionPoints.Points[i]; var cPointLine = new SvgRenderLineItem(renderer, Bounds); cPointLine.X1 = 0; cPointLine.Y1 = 0; diff --git a/src/EPPlus.Export.ImageRenderer/RenderItems/SvgItem/SvgChartDataLabelStandard.cs b/src/EPPlus.Export.ImageRenderer/RenderItems/SvgItem/SvgChartDataLabelStandard.cs index 0e45ad199..965f0aeed 100644 --- a/src/EPPlus.Export.ImageRenderer/RenderItems/SvgItem/SvgChartDataLabelStandard.cs +++ b/src/EPPlus.Export.ImageRenderer/RenderItems/SvgItem/SvgChartDataLabelStandard.cs @@ -23,7 +23,8 @@ internal class SvgChartDataLabelStandard : SvgChartObject List LeaderLines = new List(); - Coordinate orginPointOffset = new Coordinate (0, 0); + Coordinate originPoint = new Coordinate (0, 0); + Coordinate manualLayoutOffset = new Coordinate (0, 0); PointLines lines; ////Connection point coords are accurate to Internal bounds @@ -141,8 +142,9 @@ internal void ImportDataLabel(SvgChart chart, ExcelChartStandardSerie serie, Exc var rect = GetRectFromManualLayout(chart, individualLabel.Layout); Rectangle = rect; - LeftMargin = Rectangle.Left; - TopMargin = Rectangle.Top; + //LeftMargin = Rectangle.Left; + //TopMargin = Rectangle.Top; + manualLayoutOffset = new Coordinate(Rectangle.Left, Rectangle.Top); if (dataLabel.ShowLeaderLines) { @@ -214,86 +216,91 @@ internal void ImportDataLabel(SvgChart chart, ExcelChartStandardSerie serie, Exc } } - //private int GetClosestConnectionPointToOriginIndex() - //{ - // //Origin in local coordinates is 0,0 - // return GetClosestConnectionPointCoordinateIndex(new Coordinate(0, 0)); - //} + private int GetClosestConnectionPointToOriginIndex() + { + //Origin in local coordinates is 0,0 + return GetClosestConnectionPointCoordinateIndex(new Coordinate(0, 0)); + } - //private int GetClosestConnectionPointCoordinateIndex(Coordinate originPoint) - //{ - // //CalculateConnectionPoints(); - - // double smallestDist = double.MaxValue; - // int i = 0; - // int smallestIndex = 0; - - // foreach(var line in lines.RenderLines) - // { - - // var w = Math.Abs(line.X2 - originPoint.X); - // var h = Math.Abs(line.Y2 - originPoint.Y); - - // //Use pythagoran theorem to get diagonal distance - // var totalDist = Math.Sqrt(Math.Pow(w,2) + Math.Pow(h,2)); - - // if (totalDist < smallestDist) - // { - // smallestDist = totalDist; - // smallestIndex = i; - // } - // i++; - // } - - // return smallestIndex; - //} + private int GetClosestConnectionPointCoordinateIndex(Coordinate originPoint) + { + //CalculateConnectionPoints(); + + double smallestDist = double.MaxValue; + int i = 0; + int smallestIndex = 0; + + foreach (var line in lines.RenderLines) + { + line.X1 = originPoint.X - line.Bounds.Left - Bounds.Left; + line.Y1 = originPoint.Y + line.Bounds.Top; + + var w = Math.Abs(line.X2 + manualLayoutOffset.X - originPoint.X); + var h = Math.Abs(line.Y2 + manualLayoutOffset.Y - originPoint.Y); + + //Use pythagoran theorem to get diagonal distance + var totalDist = Math.Sqrt(Math.Pow(w, 2) + Math.Pow(h, 2)); + + if (totalDist < smallestDist) + { + smallestDist = totalDist; + smallestIndex = i; + } + i++; + } + + return smallestIndex; + } internal void SetOriginPointOffset(double xPos, double yPos) { - orginPointOffset.X = xPos; - orginPointOffset.Y = yPos; + originPoint.X = xPos; + originPoint.Y = yPos; Bounds.Top = yPos; Bounds.Left = xPos; - //if (_hasManualLayout) - //{ - // if (_hasLeaderLines) - // { - // var index = GetClosestConnectionPointCoordinateIndex(orginPointOffset); - - // LeaderLines.Clear(); - - // double xOffset = 0; - // if (index == 0 || index == 2) - // { - // //If Left or Right - // //Add extra 7 px (5.25pt) line to the given side - // var extraLine = new SvgRenderLineItem(ChartRenderer, ChartRenderer.Bounds); - - // xOffset = index == 0 ? -5.25d : 5.25d; - - // extraLine.X1 = lines.connectionPoints.Points[index].X; - // extraLine.Y1 = lines.connectionPoints.Points[index].Y; - // extraLine.Y2 = lines.connectionPoints.Points[index].Y; - // extraLine.X2 = extraLine.X1 + xOffset; - - // extraLine.BorderColor = "black"; - // extraLine.BorderWidth = 1; - - // LeaderLines.Add(extraLine); - // } - // var mainLine = new SvgRenderLineItem(ChartRenderer, ChartRenderer.Bounds); - // mainLine.X1 = lines.connectionPoints.Points[index].X + xOffset; - // mainLine.Y1 = lines.connectionPoints.Points[index].Y; - // mainLine.X2 = -Bounds.Left; - // mainLine.Y2 = -Bounds.Top; - - // mainLine.BorderColor = "black"; - // mainLine.BorderWidth = 1; - // LeaderLines.Add(mainLine); - // } - //} + if (_hasManualLayout) + { + Bounds.Top += manualLayoutOffset.Y; + Bounds.Left += manualLayoutOffset.X; + + if (_hasLeaderLines) + { + var index = GetClosestConnectionPointCoordinateIndex(originPoint); + + LeaderLines.Clear(); + + double xOffset = 0; + if (index == 0 || index == 2) + { + //If Left or Right + //Add extra 7 px (5.25pt) line to the given side + var extraLine = new SvgRenderLineItem(ChartRenderer, ChartRenderer.Bounds); + + xOffset = index == 0 ? -5.25d : 5.25d; + + extraLine.X1 = lines.ConnectionPoints.Points[index].X; + extraLine.Y1 = lines.ConnectionPoints.Points[index].Y; + extraLine.Y2 = lines.ConnectionPoints.Points[index].Y; + extraLine.X2 = extraLine.X1 + xOffset; + + extraLine.BorderColor = "black"; + extraLine.BorderWidth = 1; + + LeaderLines.Add(extraLine); + } + var mainLine = new SvgRenderLineItem(ChartRenderer, ChartRenderer.Bounds); + mainLine.X1 = lines.ConnectionPoints.Points[index].X + xOffset; + mainLine.Y1 = lines.ConnectionPoints.Points[index].Y; + mainLine.X2 = -Bounds.Left; + mainLine.Y2 = -Bounds.Top; + + mainLine.BorderColor = "black"; + mainLine.BorderWidth = 1; + LeaderLines.Add(mainLine); + } + } } //private void CalculateConnectionPoints() @@ -321,6 +328,15 @@ internal override void AppendRenderItems(List renderItems) var titleItem = new SvgTitleItem(DrawingRenderer, "DataLabelRect"); renderItems.Add(titleItem); + SvgRenderRectItem rect = new SvgRenderRectItem(ChartRenderer, Bounds); + rect.Bounds.Left = 0; + rect.Bounds.Top = 0; + rect.Bounds.Width = Bounds.Width; + rect.Bounds.Height = Bounds.Height; + + rect.FillColor = "red"; + rect.FillOpacity = 0.2; + renderItems.Add(rect); TxtBox.AppendRenderItems(renderItems); @@ -328,12 +344,7 @@ internal override void AppendRenderItems(List renderItems) { lines.AppendRenderItems(renderItems); } - - SvgRenderRectItem rect = new SvgRenderRectItem(ChartRenderer, Bounds); - rect.Bounds = Bounds; - rect.FillColor = "red"; - rect.FillOpacity = 0.2; - renderItems.Add(rect); + //renderItems.Add(new SvgEndGroupItem(DrawingRenderer, Bounds)); //if (LeaderLines != null && LeaderLines.Count > 0) //{ From ecfd6dc4ba98477f20f0cac66fc3cee8ac100ac4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jan=20K=C3=A4llman?= Date: Mon, 16 Mar 2026 15:23:32 +0100 Subject: [PATCH 096/151] WIP:Fixed thickthin, thinthick and triple compound lines --- .../SvgPathTests.cs | 20 ++++----- .../RenderItems/SvgRenderItem.cs | 8 +++- .../RenderItems/SvgRenderLineItem.cs | 41 ++++++++++++++--- .../RenderItems/SvgRenderPathItem.cs | 45 ++++++++++++++++--- .../Chart/Axis/ValueAxisScaleCalculator.cs | 4 +- .../ChartTypeDrawers/LineChartTypeDrawer.cs | 4 +- .../Svg/Chart/SvgChart.cs | 20 ++++----- .../Svg/Chart/SvgChartAxis.cs | 18 +++++--- .../Utils/TypeConversion/ColorConverter.cs | 6 +++ 9 files changed, 122 insertions(+), 44 deletions(-) diff --git a/src/EPPlus.Export.ImageRenderer.Test/SvgPathTests.cs b/src/EPPlus.Export.ImageRenderer.Test/SvgPathTests.cs index 196ea5d3b..7640f3e87 100644 --- a/src/EPPlus.Export.ImageRenderer.Test/SvgPathTests.cs +++ b/src/EPPlus.Export.ImageRenderer.Test/SvgPathTests.cs @@ -513,7 +513,7 @@ public void GenerateSvgForCharts() var ws = p.Workbook.Worksheets[0]; var renderer = new EPPlusImageRenderer.ImageRenderer(); - //var ix = 2; + //var ix = 1; //var c = ws.Drawings[ix]; //var svg = renderer.RenderDrawingToSvg(c); //SaveTextFileToWorkbook($"svg\\ChartForSvg_ind{ix++}.svg", svg); @@ -733,16 +733,16 @@ public void GenerateSvgForCharts_SecondaryAxis() { var ws = p.Workbook.Worksheets[0]; var renderer = new EPPlusImageRenderer.ImageRenderer(); + var ix = 0; + var c = ws.Drawings[ix]; + var svg = renderer.RenderDrawingToSvg(c); + SaveTextFileToWorkbook($"svg\\ChartForSvg_sheet2_{ix++}.svg", svg); //var ix = 1; - //var c = ws.Drawings[ix]; - //var svg = renderer.RenderDrawingToSvg(c); - //SaveTextFileToWorkbook($"svg\\ChartForSvg_sheet2_{ix++}.svg", svg); - var ix = 1; - foreach (ExcelChart c in ws.Drawings) - { - var svg = renderer.RenderDrawingToSvg(c); - SaveTextFileToWorkbook($"svg\\ChartForSvg_SecAxis{ix++}.svg", svg); - } + //foreach (ExcelChart c in ws.Drawings) + //{ + // var svg = renderer.RenderDrawingToSvg(c); + // SaveTextFileToWorkbook($"svg\\ChartForSvg_SecAxis{ix++}.svg", svg); + //} } } [TestMethod] diff --git a/src/EPPlus.Export.ImageRenderer/RenderItems/SvgRenderItem.cs b/src/EPPlus.Export.ImageRenderer/RenderItems/SvgRenderItem.cs index 038f6cee6..19eebcf33 100644 --- a/src/EPPlus.Export.ImageRenderer/RenderItems/SvgRenderItem.cs +++ b/src/EPPlus.Export.ImageRenderer/RenderItems/SvgRenderItem.cs @@ -80,7 +80,7 @@ private void RenderBase(StringBuilder sb) } internal abstract SvgRenderItem Clone(SvgShape svgDocument); - private protected void RenderCompoundItems(StringBuilder sb, double? borderWidth, string color) + private protected void RenderCompoundItems(StringBuilder sb, double? borderWidth, string color, string filter) { var tmpBorderWidth = BorderWidth; string tmpBorderColor = null; @@ -100,6 +100,12 @@ private protected void RenderCompoundItems(StringBuilder sb, double? borderWidth { sb.AppendFormat(" stroke-linejoin=\"{0}\"", LineJoin); } + + if (string.IsNullOrEmpty(filter) == false) + { + sb.Append(" " + filter); + } + sb.AppendFormat("/>"); BorderWidth = tmpBorderWidth; diff --git a/src/EPPlus.Export.ImageRenderer/RenderItems/SvgRenderLineItem.cs b/src/EPPlus.Export.ImageRenderer/RenderItems/SvgRenderLineItem.cs index cefae1388..723097d84 100644 --- a/src/EPPlus.Export.ImageRenderer/RenderItems/SvgRenderLineItem.cs +++ b/src/EPPlus.Export.ImageRenderer/RenderItems/SvgRenderLineItem.cs @@ -100,17 +100,48 @@ public override void Render(StringBuilder sb) var name = $"double-stroke-{Guid.NewGuid().ToString()}"; sb.Append($""); - RenderLineItem(sb, BorderWidth, "white"); - RenderLineItem(sb, BorderWidth * (3D / 7D), "black"); + RenderLineItem(sb, BorderWidth, "white", null); + RenderLineItem(sb, BorderWidth * (3D / 7D), "black", null); + sb.Append($""); + break; + case eCompoundLineStyle.DoubleThickThin: + WriteThickThin(sb, "double-thick-thin-stroke-{0}", (BorderWidth ?? 1D) * 1D / 7D); + break; + case eCompoundLineStyle.DoubleThinThick: + WriteThickThin(sb, "double-thin-thick-stroke-{0}", ((BorderWidth ?? 1D) * 1D / 7D) * -1); + break; + case eCompoundLineStyle.TripleThinThickThin: + var guid= Guid.NewGuid().ToString(); + var gapOffset = 5 * BorderWidth.Value / 16; + name = $"triple-stroke-{guid}"; + sb.Append($""); + sb.Append($""); + sb.Append($""); + sb.Append($""); + RenderLineItem(sb, BorderWidth, "white", null); + RenderLineItem(sb, BorderWidth * (1D / 8D), "black", $"filter=\"url(#gap-left-{guid})\""); + RenderLineItem(sb, BorderWidth * (1D / 8D), "black", $"filter=\"url(#gap-right-{guid})\""); sb.Append($""); break; default: - RenderLineItem(sb, null, null); + RenderLineItem(sb, null, null, null); break; } } + private void WriteThickThin(StringBuilder sb, string name, double gapOffset) + { + var guid = Guid.NewGuid().ToString(); + name = string.Format(name, guid); + string gapFilterName = $"f-gap-shift-{guid}"; + sb.Append(""); + sb.Append($""); + sb.Append($""); + RenderLineItem(sb, BorderWidth, "white", null); + RenderLineItem(sb, BorderWidth * (1D / 4D), "black", $"filter=\"url(#{gapFilterName})\""); + sb.Append($""); + } - private void RenderLineItem(StringBuilder sb, double? borderWidth, string color) + private void RenderLineItem(StringBuilder sb, double? borderWidth, string color, string filter) { sb.AppendFormat(""); - RenderPathItem(sb, BorderWidth, "white"); - RenderPathItem(sb, BorderWidth * (3D / 7D), "black"); + RenderPathItem(sb, BorderWidth, "white", null); + RenderPathItem(sb, BorderWidth * (3D / 7D), "black", null); + sb.Append($""); + break; + case eCompoundLineStyle.DoubleThickThin: + WriteThickThin(sb, (BorderWidth??1D) * 1D / 7D); + break; + case eCompoundLineStyle.DoubleThinThick: + WriteThickThin(sb, ((BorderWidth ?? 1D) * 1D / 7D) * -1); + break; + case eCompoundLineStyle.TripleThinThickThin: + var guid = Guid.NewGuid().ToString(); + var gapOffset = 5 * BorderWidth.Value / 16; + name = $"triple-stroke-{guid}"; + sb.Append($""); + sb.Append($""); + sb.Append($""); + sb.Append($""); + RenderPathItem(sb, BorderWidth, "white", null); + RenderPathItem(sb, BorderWidth * (1D / 8D), "black", $"filter=\"url(#gap-left-{guid})\""); + RenderPathItem(sb, BorderWidth * (1D / 8D), "black", $"filter=\"url(#gap-right-{guid})\""); sb.Append($""); break; } } - private void RenderPathItem(StringBuilder sb, double? borderWidth, string color) + + private void WriteThickThin(StringBuilder sb, double gapOffset) + { + var guid = Guid.NewGuid().ToString(); + var name = $"double-thick-thin-stroke-{guid}"; + string gapFilterName = $"f-gap-shift-{guid}"; + sb.Append(""); + sb.Append($""); + sb.Append($""); + RenderPathItem(sb, BorderWidth, "white", null); + RenderPathItem(sb, BorderWidth * (1 / 4D), "black", $"filter=\"url(#{gapFilterName})\""); + sb.Append($""); + } + + private void RenderPathItem(StringBuilder sb, double? borderWidth, string color, string filter) { var width = Bounds.Width.PointToPixel(); var height = Bounds.Height.PointToPixel(); @@ -63,8 +97,7 @@ private void RenderPathItem(StringBuilder sb, double? borderWidth, string color) Commands[i].Render(width, height, sb); } sb.Append("\" "); - RenderCompoundItems(sb, borderWidth, color); - sb.Append("/>"); + RenderCompoundItems(sb, borderWidth, color, filter); } diff --git a/src/EPPlus.Export.ImageRenderer/Svg/Chart/Axis/ValueAxisScaleCalculator.cs b/src/EPPlus.Export.ImageRenderer/Svg/Chart/Axis/ValueAxisScaleCalculator.cs index ffc5c0083..a214f1434 100644 --- a/src/EPPlus.Export.ImageRenderer/Svg/Chart/Axis/ValueAxisScaleCalculator.cs +++ b/src/EPPlus.Export.ImageRenderer/Svg/Chart/Axis/ValueAxisScaleCalculator.cs @@ -121,7 +121,7 @@ private static AxisScale GetNumberScale(ref double dataMin, ref double dataMax, } else { - axisMin = dataMin - (dataRange * 0.1); + axisMin = dataMin - (dataRange * 0.05); if (axisOptions.IsStacked100 && axisMin < -1) { axisMin = -1; @@ -139,7 +139,7 @@ private static AxisScale GetNumberScale(ref double dataMin, ref double dataMax, } else { - axisMax = dataMax + (dataRange * 0.1); + axisMax = dataMax + (dataRange * 0.05); if (axisOptions.IsStacked100 && axisMax > 1) { axisMax = 1; diff --git a/src/EPPlus.Export.ImageRenderer/Svg/Chart/ChartTypeDrawers/LineChartTypeDrawer.cs b/src/EPPlus.Export.ImageRenderer/Svg/Chart/ChartTypeDrawers/LineChartTypeDrawer.cs index 0bd965f7a..551796593 100644 --- a/src/EPPlus.Export.ImageRenderer/Svg/Chart/ChartTypeDrawers/LineChartTypeDrawer.cs +++ b/src/EPPlus.Export.ImageRenderer/Svg/Chart/ChartTypeDrawers/LineChartTypeDrawer.cs @@ -151,8 +151,8 @@ private void AddLine(ExcelChart chartType, ExcelLineChartSerie serie, List AddTickmarks(double units, eTimeUnit? dateUnit, double parentUnit, float tickMarkWidth, eAxisTickMark type) + private List AddTickmarks(double units, eTimeUnit? dateUnit, double parentUnit, double tickMarkWidth, eAxisTickMark type) { var axisStyle = GetAxisStyleEntry(); var tms = new List(); - double min; + double min, addMinor=0D; + if(double.IsNaN(parentUnit)==false && parentUnit==units) + { + addMinor = parentUnit / 2; + } if (Axis.AxisType == eAxisType.Cat) { min = 0; @@ -445,7 +449,7 @@ private List AddTickmarks(double units, eTimeUnit? dateUnit, { min = Min; } - float tickMarkWidthInside=0, tickMarkWidthOutside=0; + double tickMarkWidthInside=0, tickMarkWidthOutside=0; if(type==eAxisTickMark.In || type==eAxisTickMark.Cross) { tickMarkWidthInside = tickMarkWidth; @@ -456,12 +460,12 @@ private List AddTickmarks(double units, eTimeUnit? dateUnit, } var diff = Max - min; - double d = min; + double d = min + addMinor; while (d <= Max) { if (double.IsNaN(parentUnit) || (d % parentUnit != 0)) { - float x1, y1, x2, y2; + double x1, y1, x2, y2; switch (Axis.AxisPosition) { case eAxisPosition.Left: diff --git a/src/EPPlus/Utils/TypeConversion/ColorConverter.cs b/src/EPPlus/Utils/TypeConversion/ColorConverter.cs index aa8f65397..c80b62d4d 100644 --- a/src/EPPlus/Utils/TypeConversion/ColorConverter.cs +++ b/src/EPPlus/Utils/TypeConversion/ColorConverter.cs @@ -76,6 +76,12 @@ private static Color ApplyTransforms(Color c, ExcelColorTransformCollection tran case eColorTransformType.Alpha: c = Color.FromArgb((byte)Math.Round(255 * v), c.R, c.G, c.B); break; + case eColorTransformType.AlphaMod: + c = Color.FromArgb((byte)Math.Round(c.A * v), c.R, c.G, c.B); + break; + case eColorTransformType.AlphaOff: + c = Color.FromArgb((byte)(c.A + v), c.R, c.G, c.B); + break; } } return c; From 97ede7bcde9ee6fac697bce73d3e9bcc70c533c2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jan=20K=C3=A4llman?= Date: Mon, 16 Mar 2026 16:21:37 +0100 Subject: [PATCH 097/151] WIP:Added outer shadow effect on lines --- .../SvgPathTests.cs | 20 ++++++------- .../RenderItems/RenderItem.cs | 29 ++++++++++++++++++- .../Utils/SvgDrawingWriter.cs | 15 ++++++++++ .../Utils/TypeConversion/ColorConverter.cs | 10 ++++--- 4 files changed, 59 insertions(+), 15 deletions(-) diff --git a/src/EPPlus.Export.ImageRenderer.Test/SvgPathTests.cs b/src/EPPlus.Export.ImageRenderer.Test/SvgPathTests.cs index 815152d28..c94bf362d 100644 --- a/src/EPPlus.Export.ImageRenderer.Test/SvgPathTests.cs +++ b/src/EPPlus.Export.ImageRenderer.Test/SvgPathTests.cs @@ -733,16 +733,16 @@ public void GenerateSvgForCharts_SecondaryAxis() { var ws = p.Workbook.Worksheets[0]; var renderer = new EPPlusImageRenderer.ImageRenderer(); - var ix = 0; - var c = ws.Drawings[ix]; - var svg = renderer.RenderDrawingToSvg(c); - SaveTextFileToWorkbook($"svg\\ChartForSvg_sheet2_{ix++}.svg", svg); - //var ix = 1; - //foreach (ExcelChart c in ws.Drawings) - //{ - // var svg = renderer.RenderDrawingToSvg(c); - // SaveTextFileToWorkbook($"svg\\ChartForSvg_SecAxis{ix++}.svg", svg); - //} + //var ix = 0; + //var c = ws.Drawings[ix]; + //var svg = renderer.RenderDrawingToSvg(c); + //SaveTextFileToWorkbook($"svg\\ChartForSvg_sheet2_{ix++}.svg", svg); + var ix = 1; + foreach (ExcelChart c in ws.Drawings) + { + var svg = renderer.RenderDrawingToSvg(c); + SaveTextFileToWorkbook($"svg\\ChartForSvg_SecAxis{ix++}.svg", svg); + } } } [TestMethod] diff --git a/src/EPPlus.Export.ImageRenderer/RenderItems/RenderItem.cs b/src/EPPlus.Export.ImageRenderer/RenderItems/RenderItem.cs index 02175448e..52f409606 100644 --- a/src/EPPlus.Export.ImageRenderer/RenderItems/RenderItem.cs +++ b/src/EPPlus.Export.ImageRenderer/RenderItems/RenderItem.cs @@ -69,7 +69,7 @@ internal virtual void GetBounds(out double il, out double it, out double ir, out public PathFillMode BorderColorSource { get; set; } = PathFillMode.Norm; public double? GlowRadius { get; private set; } public string GlowColor { get; private set; } - + public ExcelDrawingOuterShadowEffect OuterShadowEffect { get; private set; } = null; protected void CloneBase(RenderItem item) { item.FillColor = FillColor; @@ -180,6 +180,10 @@ internal void SetDrawingPropertiesEffects(ExcelDrawingEffectStyle effect) var gc = EPPlusColorConverter.GetThemeColor(DrawingRenderer.Theme, effect.Glow.Color); GlowColor = "#" + gc.ToArgb().ToString("x8").Substring(2); } + if(effect.HasOuterShadow) + { + OuterShadowEffect = effect.OuterShadow; + } } private double[] GetDashArray(ExcelDrawingBorder border) @@ -247,6 +251,29 @@ private string GetFillColor(ExcelDrawingFillBasic fill, ExcelDrawingColorManager } return "#" + fc.ToArgb().ToString("x8").Substring(2); } + + internal void GetOuterShadowColor(out string shadowColor, out double opacity) + { + if (OuterShadowEffect == null) + { + shadowColor = null; + opacity = 0; + + } + else + { + var tc=EPPlusColorConverter.GetThemeColor(DrawingRenderer.Theme, OuterShadowEffect.Color); + if (tc.A < 255 && tc != Color.Empty) + { + opacity = tc.A / 255D; + } + else + { + opacity = 1; + } + shadowColor = "#" + tc.ToArgb().ToString("x8").Substring(2); + } + } } internal abstract class RenderItemIndependent : RenderItemBase diff --git a/src/EPPlus.Export.ImageRenderer/Utils/SvgDrawingWriter.cs b/src/EPPlus.Export.ImageRenderer/Utils/SvgDrawingWriter.cs index fb2ec97db..f20f2eca0 100644 --- a/src/EPPlus.Export.ImageRenderer/Utils/SvgDrawingWriter.cs +++ b/src/EPPlus.Export.ImageRenderer/Utils/SvgDrawingWriter.cs @@ -10,6 +10,7 @@ Date Author Change ************************************************************************************************* 27/11/2025 EPPlus Software AB EPPlus 9 *************************************************************************************************/ +using EPPlus.Fonts.OpenType.Utils; using EPPlusImageRenderer.Constants; using EPPlusImageRenderer.RenderItems; using OfficeOpenXml.Drawing; @@ -86,6 +87,20 @@ internal void WriteSvgDefs(StringBuilder sb, List renderItems) $"" + $""; } + if(item.OuterShadowEffect != null) + { + if (string.IsNullOrEmpty(item.FilterName)) + { + var filterName = GetFilterName(ix); + item.FilterName = $"Url(#{filterName})"; + filter = $""; + } + item.GetOuterShadowColor(out string shadowColor, out double opacity); + var dx = Math.Round(item.OuterShadowEffect.Distance * Math.Cos(MathHelper.Radians(item.OuterShadowEffect.Direction ?? 0D)), 2); + var dy = Math.Round(item.OuterShadowEffect.Distance * Math.Sin(MathHelper.Radians(item.OuterShadowEffect.Direction ?? 0D)), 2); + var blurRadius = item.OuterShadowEffect.BlurRadius??0D / 2; + filter += $""; + } if(string.IsNullOrEmpty(filter)==false) { defSb.Append(filter+""); diff --git a/src/EPPlus/Utils/TypeConversion/ColorConverter.cs b/src/EPPlus/Utils/TypeConversion/ColorConverter.cs index c80b62d4d..caf33cc0a 100644 --- a/src/EPPlus/Utils/TypeConversion/ColorConverter.cs +++ b/src/EPPlus/Utils/TypeConversion/ColorConverter.cs @@ -30,15 +30,17 @@ public static Color GetThemeColor(ExcelTheme theme, ExcelDrawingColorManager cm) if(cm!=null && cm.ColorType==eDrawingColorType.Scheme) { var newCm=theme.ColorScheme.GetColorByEnum(cm.SchemeColor.Color); - var c = GetThemeColor(newCm); - return ApplyTransforms(c, cm.Transforms); + var nc = GetThemeColor(newCm); + return ApplyTransforms(nc, cm.Transforms); } - return GetThemeColor(cm); + var c=GetThemeColor(cm); + return ApplyTransforms(c, cm.Transforms); + } private static Color ApplyTransforms(Color c, ExcelColorTransformCollection transforms) { - if (transforms.Count == 0) return c; + if (transforms==null || transforms.Count == 0) return c; var r = c.R; var g = c.G; From 0c4bb8a0505504807cdac37eb709f3d9c09d6b83 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ossian=20Edstr=C3=B6m?= Date: Mon, 16 Mar 2026 16:48:40 +0100 Subject: [PATCH 098/151] Fixed drawing fill and alignment for line-break chart title --- .../SvgPathTests.cs | 30 +++++++++++++++++++ .../RenderItems/Shared/ParagraphItem.cs | 19 +++++++++--- .../RenderItems/Shared/TextBodyItem.cs | 4 ++- .../RenderItems/SvgItem/SvgTextBodyItem.cs | 6 ++++ .../RenderItems/SvgItem/SvgTextBox.cs | 3 +- .../Svg/Chart/SvgChartTitle.cs | 6 +++- src/EPPlus/Drawing/Chart/ExcelChartTitle.cs | 8 +++-- .../Style/Text/ExcelDrawingParagraph.cs | 7 +++-- .../RichText/ExcelParagraphCollection.cs | 5 ++-- 9 files changed, 75 insertions(+), 13 deletions(-) diff --git a/src/EPPlus.Export.ImageRenderer.Test/SvgPathTests.cs b/src/EPPlus.Export.ImageRenderer.Test/SvgPathTests.cs index 8e9217171..be4fa3241 100644 --- a/src/EPPlus.Export.ImageRenderer.Test/SvgPathTests.cs +++ b/src/EPPlus.Export.ImageRenderer.Test/SvgPathTests.cs @@ -725,6 +725,36 @@ public void GenerateSvgForCharts_sheet3() } } } + + [TestMethod] + public void TestStyling() + { + ExcelPackage.License.SetNonCommercialOrganization("EPPlus Project"); + using (var p = OpenTemplatePackage("MyCellsAdvanced.xlsx")) + { + var ws = p.Workbook.Worksheets[0]; + var myCell = ws.Cells["B3"]; + var myCellStyle = myCell.Style; + var fillStyle = myCell.Style.Fill; + var fontColor = myCell.Style.Font.Color; + + var bgRgb = myCell.Style.Fill.BackgroundColor.Rgb; + var bgFillCol = myCell.Style.Fill.PatternColor.Rgb; + + var myOtherCell = ws.Cells["B2"]; + + var myShape = ws.Drawings[0].As.Shape; + var myRichtext = myShape.RichText; + var firstDefault = myShape.TextBody.Paragraphs.FirstDefaultRunProperties; + + var firstPara = myShape.TextBody.Paragraphs[0]; + var defRun = firstPara.DefaultRunProperties; + + var secondPara = myShape.TextBody.Paragraphs[1]; + var secondDefRun = secondPara.DefaultRunProperties; + } + } + [TestMethod] public void GenerateSvgForCharts_SecondaryAxis() { diff --git a/src/EPPlus.Export.ImageRenderer/RenderItems/Shared/ParagraphItem.cs b/src/EPPlus.Export.ImageRenderer/RenderItems/Shared/ParagraphItem.cs index 7d1a62b64..dca701a31 100644 --- a/src/EPPlus.Export.ImageRenderer/RenderItems/Shared/ParagraphItem.cs +++ b/src/EPPlus.Export.ImageRenderer/RenderItems/Shared/ParagraphItem.cs @@ -41,7 +41,7 @@ internal abstract class ParagraphItem : RenderItem internal protected MeasurementFont _paragraphFont; internal TextBodyItem ParentTextBody { get; set; } internal double ParagraphLineSpacing { get; private set; } - internal eTextAlignment HorizontalAlignment { get; private set; } = eTextAlignment.Left; + internal eTextAlignment HorizontalAlignment { get; private set; } internal List Runs { get; set; } = new List(); internal bool DisplayBounds { get; set; } = false; @@ -68,7 +68,10 @@ public ParagraphItem(TextBodyItem textBody, DrawingBase renderer, BoundingBox pa { if(IsFirstParagraph) { - SetDrawingPropertiesFill(p.DefaultRunProperties.Fill, null); + if(p.DefaultRunProperties.Fill != null) + { + SetDrawingPropertiesFill(p.DefaultRunProperties.Fill, null); + } } else { @@ -87,9 +90,17 @@ public ParagraphItem(TextBodyItem textBody, DrawingBase renderer, BoundingBox pa } } } + else + { + if(p._paragraphs.FirstDefaultRunProperties!= null && p._paragraphs.FirstDefaultRunProperties.Fill != null && p._paragraphs.FirstDefaultRunProperties.Fill.IsEmpty == false) + { + var fill = p._paragraphs.FirstDefaultRunProperties.Fill; + SetDrawingPropertiesFill(fill, null); + } + } - //---Initialize Bounds / Margins-- - - Bounds.Name = "Paragraph"; + //---Initialize Bounds / Margins-- - + Bounds.Name = "Paragraph"; var indent = 48 * p.IndentLevel; _leftMargin = p.LeftMargin + p.Indent + indent; diff --git a/src/EPPlus.Export.ImageRenderer/RenderItems/Shared/TextBodyItem.cs b/src/EPPlus.Export.ImageRenderer/RenderItems/Shared/TextBodyItem.cs index 13f86fa3a..91b3a1a17 100644 --- a/src/EPPlus.Export.ImageRenderer/RenderItems/Shared/TextBodyItem.cs +++ b/src/EPPlus.Export.ImageRenderer/RenderItems/Shared/TextBodyItem.cs @@ -145,7 +145,7 @@ internal void SetHorizontalAlignmentPosition() //} } - internal virtual void ImportTextBody(ExcelTextBody body) + internal virtual void ImportTextBody(ExcelTextBody body, ExcelHorizontalAlignment horizontalDefault = ExcelHorizontalAlignment.Left) { _text = null; VerticalAlignment = body.Anchor; @@ -162,6 +162,8 @@ internal virtual void ImportTextBody(ExcelTextBody body) largestWidth = Math.Max(largestWidth, addedPara.Bounds.Width); } + + foreach (var paragraph in body.Paragraphs) { SetHorizontalAlignmentPosition(); diff --git a/src/EPPlus.Export.ImageRenderer/RenderItems/SvgItem/SvgTextBodyItem.cs b/src/EPPlus.Export.ImageRenderer/RenderItems/SvgItem/SvgTextBodyItem.cs index 1d2831d88..4ecd4da09 100644 --- a/src/EPPlus.Export.ImageRenderer/RenderItems/SvgItem/SvgTextBodyItem.cs +++ b/src/EPPlus.Export.ImageRenderer/RenderItems/SvgItem/SvgTextBodyItem.cs @@ -46,6 +46,12 @@ internal override void AppendRenderItems(List renderItems) { groupItem = new SvgGroupItem(DrawingRenderer, Bounds); } + + if (FontColorString != null) + { + groupItem.GroupTransform += $" fill=\"{FontColorString}\""; + } + renderItems.Add(groupItem); foreach (SvgParagraphItem item in Paragraphs) { diff --git a/src/EPPlus.Export.ImageRenderer/RenderItems/SvgItem/SvgTextBox.cs b/src/EPPlus.Export.ImageRenderer/RenderItems/SvgItem/SvgTextBox.cs index 8789baca4..e4d54f419 100644 --- a/src/EPPlus.Export.ImageRenderer/RenderItems/SvgItem/SvgTextBox.cs +++ b/src/EPPlus.Export.ImageRenderer/RenderItems/SvgItem/SvgTextBox.cs @@ -5,6 +5,7 @@ using EPPlusImageRenderer.Svg; using OfficeOpenXml.Drawing; using OfficeOpenXml.FormulaParsing.Excel.Functions.Text; +using OfficeOpenXml.Style; using System; using System.Collections.Generic; @@ -118,7 +119,7 @@ internal double Rotation } } - internal void ImportTextBody(ExcelTextBody body, bool useDefaults = true) + internal void ImportTextBody(ExcelTextBody body, bool useDefaults = true, ExcelHorizontalAlignment horizontalDefault = ExcelHorizontalAlignment.Left) { double l, r, t, b; if (useDefaults) diff --git a/src/EPPlus.Export.ImageRenderer/Svg/Chart/SvgChartTitle.cs b/src/EPPlus.Export.ImageRenderer/Svg/Chart/SvgChartTitle.cs index 11165564e..844f3fe06 100644 --- a/src/EPPlus.Export.ImageRenderer/Svg/Chart/SvgChartTitle.cs +++ b/src/EPPlus.Export.ImageRenderer/Svg/Chart/SvgChartTitle.cs @@ -20,6 +20,8 @@ Date Author Change using OfficeOpenXml.Drawing.Chart; using OfficeOpenXml.FormulaParsing.Excel.Functions.MathFunctions; using OfficeOpenXml.FormulaParsing.Excel.Functions.Text; +using OfficeOpenXml.Style; +using OfficeOpenXml.Utils.EnumUtils; using System; using System.Collections.Generic; using System.Globalization; @@ -190,7 +192,7 @@ internal void InitTextBox() } if (_title.TextBody.Paragraphs.Count > 0) { - TextBox.ImportTextBody(_title.TextBody); + TextBox.ImportTextBody(_title.TextBody, true, ExcelHorizontalAlignment.Center); } else { @@ -213,6 +215,8 @@ public SvgTextBox TextBox } internal override void AppendRenderItems(List renderItems) { + var p = _title.DefaultTextBody.Paragraphs.FirstOrDefault(); + TextBox.TextBody.FontColorString = "#" + p.DefaultRunProperties.Fill.Color.ToColorString(); TextBox.AppendRenderItems(renderItems); TextBox.Rectangle.SetDrawingPropertiesFill(_title.Fill, _svgChart.Chart.StyleManager.Style.Title.FillReference.Color); TextBox.Rectangle.SetDrawingPropertiesBorder(_title.Border, _svgChart.Chart.StyleManager.Style.Title.BorderReference.Color, _title.Border.Fill.Style != eFillStyle.NoFill, 0.75); diff --git a/src/EPPlus/Drawing/Chart/ExcelChartTitle.cs b/src/EPPlus/Drawing/Chart/ExcelChartTitle.cs index bb448d676..5c567b31b 100644 --- a/src/EPPlus/Drawing/Chart/ExcelChartTitle.cs +++ b/src/EPPlus/Drawing/Chart/ExcelChartTitle.cs @@ -18,6 +18,7 @@ Date Author Change using OfficeOpenXml.Style; using System; using System.Collections.Generic; +using System.Net.Security; using System.Text; using System.Xml; @@ -56,7 +57,6 @@ internal ExcelChartTitle(ExcelChart chart, XmlNamespaceManager nameSpaceManager, chart.ApplyStyleOnPart(this, chart.StyleManager?.Style?.Title, true); } } - } private void CreateTopNode() @@ -148,7 +148,11 @@ public ExcelTextBody TextBody { if (_textBody == null) { + //var defBody = DefaultTextBody; + //var firstDefaultRunProperties = DefaultTextBody.Paragraphs.CreateOrGetDefaultRunProperties($"{_defTxBodyPath}/a:bodyPr/a:p/a:pPr/a:defRPr", TopNode); _textBody = new ExcelTextBody(_chart, NameSpaceManager, TopNode, $"{_richTextPath}/a:bodyPr", SchemaNodeOrder); + //_textBody.Paragraphs.FirstDefaultRunProperties = firstDefaultRunProperties; + //_textBody.Paragraphs.FirstDefaultRunProperties = DefaultTextBody.Paragraphs.CreateOrGetDefaultRunProperties(); } return _textBody; } @@ -245,7 +249,7 @@ internal void CreateRichText() defFont = Convert.ToSingle(stylePart.DefaultTextRun.Size); } var tb = TextBody; - _richText = new ExcelParagraphCollection(tb, _chart, NameSpaceManager, TopNode, $"{_richTextPath}/a:p", SchemaNodeOrder, defFont); + _richText = new ExcelParagraphCollection(tb, _chart, NameSpaceManager, TopNode, $"{_richTextPath}/a:p", SchemaNodeOrder, defFont, eTextAlignment.Center); } private ExcelChartStyleEntry GetStylePart() diff --git a/src/EPPlus/Drawing/Style/Text/ExcelDrawingParagraph.cs b/src/EPPlus/Drawing/Style/Text/ExcelDrawingParagraph.cs index fc298a44b..fcf75f031 100644 --- a/src/EPPlus/Drawing/Style/Text/ExcelDrawingParagraph.cs +++ b/src/EPPlus/Drawing/Style/Text/ExcelDrawingParagraph.cs @@ -56,7 +56,7 @@ internal ExcelDrawingParagraph(ExcelDrawingParagraphCollection paragraphs, IPict //////Previously new paragraphs used the first DefaultRunProperties //////Uncertain if we should keep this behaviour at least as an option. TODO: Decide if breaking change or legacy setting (or keep only previous paragraph's settings?) - bool legacyDefaultRunPropertySetting = true; + bool legacyDefaultRunPropertySetting = false; if (paragraphs.Count == 0) { @@ -127,6 +127,9 @@ public string Text return sb.ToString(); } } + + + internal eTextAlignment defaultAlignment = eTextAlignment.Left; /// /// Horizontal Alignment /// @@ -134,7 +137,7 @@ public eTextAlignment HorizontalAlignment { get { - return GetXmlNodeString("a:pPr/@algn").ToEnum(eTextAlignment.Left, new Dictionary + return GetXmlNodeString("a:pPr/@algn").ToEnum(defaultAlignment, new Dictionary { ["r"] = eTextAlignment.Right, ["ctr"] = eTextAlignment.Center, diff --git a/src/EPPlus/Style/RichText/ExcelParagraphCollection.cs b/src/EPPlus/Style/RichText/ExcelParagraphCollection.cs index c5837de5b..c9ca674d1 100644 --- a/src/EPPlus/Style/RichText/ExcelParagraphCollection.cs +++ b/src/EPPlus/Style/RichText/ExcelParagraphCollection.cs @@ -33,7 +33,7 @@ public class ExcelParagraphCollection : XmlHelper, IEnumerable private readonly float _defaultFontSize; private readonly ExcelTextFont _defaultFont; private readonly ExcelTextBody _textBody; - internal ExcelParagraphCollection(ExcelTextBody tb, ExcelDrawing drawing, XmlNamespaceManager ns, XmlNode topNode, string path, string[] schemaNodeOrder, float defaultFontSize =11) : + internal ExcelParagraphCollection(ExcelTextBody tb, ExcelDrawing drawing, XmlNamespaceManager ns, XmlNode topNode, string path, string[] schemaNodeOrder, float defaultFontSize =11, eTextAlignment defaultAlignment = eTextAlignment.Left) : base(ns, topNode) { _drawing = drawing; @@ -67,8 +67,9 @@ internal ExcelParagraphCollection(ExcelTextBody tb, ExcelDrawing drawing, XmlNa //_defaultFont = tfXml; _path = path; - foreach(var p in tb.Paragraphs) + foreach (var p in tb.Paragraphs) { + p.defaultAlignment = defaultAlignment; foreach(var tr in p.TextRuns) { _list.Add(new ExcelParagraph(tr)); From e04828378c12dfde242a1675f6bc20948a84ac3f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ossian=20Edstr=C3=B6m?= Date: Tue, 17 Mar 2026 17:20:23 +0100 Subject: [PATCH 099/151] implemented labelpositions and fixed bugs --- .../SvgPathTests.cs | 14 + .../RenderItems/Shared/ParagraphItem.cs | 16 +- .../RenderItems/Shared/TextBodyItem.cs | 2 - .../SvgItem/SvgChartDataLabelStandard.cs | 288 +++++++++--------- .../SvgItem/SvgChartSerieDataLabel.cs | 14 +- .../ChartTypeDrawers/LineChartTypeDrawer.cs | 50 ++- .../Integration/MeasurerComparisonTests.cs | 16 +- .../Integration/TextLayoutEngineTests.cs | 26 ++ .../Integration/TextLayoutEngine.RichText.cs | 4 + .../Drawing/Chart/ExcelChartSerieDataLabel.cs | 2 +- .../Drawing/Style/Text/ExcelTextBody.cs | 8 +- 11 files changed, 260 insertions(+), 180 deletions(-) diff --git a/src/EPPlus.Export.ImageRenderer.Test/SvgPathTests.cs b/src/EPPlus.Export.ImageRenderer.Test/SvgPathTests.cs index be4fa3241..e572cd0c6 100644 --- a/src/EPPlus.Export.ImageRenderer.Test/SvgPathTests.cs +++ b/src/EPPlus.Export.ImageRenderer.Test/SvgPathTests.cs @@ -587,6 +587,20 @@ public void GenerateDatalabelsLeaderLines() } + [TestMethod] + public void GenerateDatalabelsLeaderLinesBg() + { + ExcelPackage.License.SetNonCommercialOrganization("EPPlus Project"); + using (var p = OpenTemplatePackage("datalabelsSvgRightAlignedWithBg.xlsx")) + { + var c = p.Workbook.Worksheets[0].Drawings[0]; + + var renderer = new EPPlusImageRenderer.ImageRenderer(); + var svg = renderer.RenderDrawingToSvg(c); + SaveTextFileToWorkbook($"svg\\datalabelsSvgLeaderLinesBg.svg", svg); + } + } + [TestMethod] public void OpenRightAligned() { diff --git a/src/EPPlus.Export.ImageRenderer/RenderItems/Shared/ParagraphItem.cs b/src/EPPlus.Export.ImageRenderer/RenderItems/Shared/ParagraphItem.cs index dca701a31..47ad80ba2 100644 --- a/src/EPPlus.Export.ImageRenderer/RenderItems/Shared/ParagraphItem.cs +++ b/src/EPPlus.Export.ImageRenderer/RenderItems/Shared/ParagraphItem.cs @@ -59,16 +59,16 @@ public ParagraphItem(TextBodyItem textBody, DrawingBase renderer, BoundingBox pa ParagraphLineSpacing = GetParagraphLineSpacingInPoints(100, _measurer); } - public ParagraphItem(TextBodyItem textBody, DrawingBase renderer, BoundingBox parent, ExcelDrawingParagraph p, string textIfEmpty=null) : base(renderer, parent) + public ParagraphItem(TextBodyItem textBody, DrawingBase renderer, BoundingBox parent, ExcelDrawingParagraph p, string textIfEmpty = null) : base(renderer, parent) { - ParentTextBody = textBody; + ParentTextBody = textBody; IsFirstParagraph = p == p._paragraphs[0]; if (p.DefaultRunProperties.Fill != null && p.DefaultRunProperties.Fill.IsEmpty == false) { - if(IsFirstParagraph) + if (IsFirstParagraph) { - if(p.DefaultRunProperties.Fill != null) + if (p.DefaultRunProperties.Fill != null) { SetDrawingPropertiesFill(p.DefaultRunProperties.Fill, null); } @@ -76,7 +76,7 @@ public ParagraphItem(TextBodyItem textBody, DrawingBase renderer, BoundingBox pa else { //Drawingproperties has fallback to firstDefault but excel does not display it so we should not either. - if(p.DefaultRunProperties != p._paragraphs.FirstDefaultRunProperties) + if (p.DefaultRunProperties != p._paragraphs.FirstDefaultRunProperties) { SetDrawingPropertiesFill(p.DefaultRunProperties.Fill, null); } @@ -92,15 +92,15 @@ public ParagraphItem(TextBodyItem textBody, DrawingBase renderer, BoundingBox pa } else { - if(p._paragraphs.FirstDefaultRunProperties!= null && p._paragraphs.FirstDefaultRunProperties.Fill != null && p._paragraphs.FirstDefaultRunProperties.Fill.IsEmpty == false) + if (p._paragraphs.FirstDefaultRunProperties != null && p._paragraphs.FirstDefaultRunProperties.Fill != null && p._paragraphs.FirstDefaultRunProperties.Fill.IsEmpty == false) { var fill = p._paragraphs.FirstDefaultRunProperties.Fill; SetDrawingPropertiesFill(fill, null); } } - //---Initialize Bounds / Margins-- - - Bounds.Name = "Paragraph"; + //---Initialize Bounds / Margins-- - + Bounds.Name = "Paragraph"; var indent = 48 * p.IndentLevel; _leftMargin = p.LeftMargin + p.Indent + indent; diff --git a/src/EPPlus.Export.ImageRenderer/RenderItems/Shared/TextBodyItem.cs b/src/EPPlus.Export.ImageRenderer/RenderItems/Shared/TextBodyItem.cs index 91b3a1a17..f52656c3c 100644 --- a/src/EPPlus.Export.ImageRenderer/RenderItems/Shared/TextBodyItem.cs +++ b/src/EPPlus.Export.ImageRenderer/RenderItems/Shared/TextBodyItem.cs @@ -162,8 +162,6 @@ internal virtual void ImportTextBody(ExcelTextBody body, ExcelHorizontalAlignmen largestWidth = Math.Max(largestWidth, addedPara.Bounds.Width); } - - foreach (var paragraph in body.Paragraphs) { SetHorizontalAlignmentPosition(); diff --git a/src/EPPlus.Export.ImageRenderer/RenderItems/SvgItem/SvgChartDataLabelStandard.cs b/src/EPPlus.Export.ImageRenderer/RenderItems/SvgItem/SvgChartDataLabelStandard.cs index 965f0aeed..8cc77eca3 100644 --- a/src/EPPlus.Export.ImageRenderer/RenderItems/SvgItem/SvgChartDataLabelStandard.cs +++ b/src/EPPlus.Export.ImageRenderer/RenderItems/SvgItem/SvgChartDataLabelStandard.cs @@ -21,12 +21,16 @@ internal class SvgChartDataLabelStandard : SvgChartObject internal SvgTextBox TxtBox; + BoundingBox _parentPoint; + List LeaderLines = new List(); Coordinate originPoint = new Coordinate (0, 0); Coordinate manualLayoutOffset = new Coordinate (0, 0); - PointLines lines; + PointLines connectionPointLines; + + eLabelPosition _labelPosition; ////Connection point coords are accurate to Internal bounds //BoundingBox internalBounds = new BoundingBox(); //List ConnectionPoints = new List(); @@ -41,6 +45,7 @@ public SvgChartDataLabelStandard(DrawingChart chart, string dataLabelText) : bas public SvgChartDataLabelStandard(DrawingChart chart, ExcelChartDataLabelStandard standard) : base(chart) { HasLegendKey = standard.ShowLegendKey; + _labelPosition = standard.Position; } public SvgChartDataLabelStandard(DrawingChart chart, ExcelChartDataLabelStandard standard, SvgTextBox txtBox) : base(chart) @@ -52,10 +57,10 @@ public SvgChartDataLabelStandard(DrawingChart chart, ExcelChartDataLabelStandard private void FitToTextBoxContent() { - Bounds.Left = TxtBox.Left; - Bounds.Top = TxtBox.Top; - Bounds.Height = TxtBox.Height; - Bounds.Width = TxtBox.Width; + //Bounds.Left = TxtBox.Left; + //Bounds.Top = TxtBox.Top; + //Bounds.Height = TxtBox.Height; + //Bounds.Width = TxtBox.Width; } internal void AddSeriesIcon(double iconWidth, double iconHeight) @@ -98,7 +103,7 @@ internal void ImportDataLabel(SvgChart chart, ExcelChartStandardSerie serie, Exc dlblStrings.Add(yValue.ToString()); } - var separator = string.IsNullOrEmpty(dataLabel.Separator) ? "," : dataLabel.Separator; + var separator = string.IsNullOrEmpty(dataLabel.Separator) ? ", " : dataLabel.Separator; string finalString = ""; for (int j = 0; j < dlblStrings.Count; j++) @@ -111,9 +116,17 @@ internal void ImportDataLabel(SvgChart chart, ExcelChartStandardSerie serie, Exc } var txtBox = new SvgTextBox(chart, Bounds, maxBounds); + txtBox.ImportTextBody(dataLabel.TextBody, false); + + txtBox.TextBody.Bounds.Top = 0; txtBox.TextBody.AutoSize = true; + //txtBox.LeftMargin = 3.1181d; + //txtBox.RightMargin = 3.1181d; + //txtBox.TopMargin = 1.4173d; + //txtBox.BottomMargin = 1.4173d; + if (txtBox.TextBody.Paragraphs.Count == 0) { txtBox.TextBody.ImportParagraph(defaultParagraph, 0, finalString); @@ -125,12 +138,23 @@ internal void ImportDataLabel(SvgChart chart, ExcelChartStandardSerie serie, Exc //Remove dummy paragraph added by ImportTextBody txtBox.TextBody.Paragraphs.RemoveAt(0); } + + Bounds.Left -= txtBox.Width / 2; + Bounds.Top -= txtBox.Height / 2; ////Reset run y-position. ////Datalabel does not use the standard line-spacing textbody offsets //txtBox.TextBody.Paragraphs[0].Runs[0].YPosition = 0; TxtBox = txtBox; + _labelPosition = dataLabel.Position; + + //TxtBox.TextBody.Bounds.Left += TxtBox.LeftMargin; + //TxtBox.TextBody.Bounds.Top += TxtBox.TopMargin; + + //TxtBox.Left = txtBox.LeftMargin; + //TxtBox.Top = txtBox.TopMargin; + if (dataLabel is ExcelChartDataLabelItem) { var individualLabel = dataLabel as ExcelChartDataLabelItem; @@ -150,64 +174,14 @@ internal void ImportDataLabel(SvgChart chart, ExcelChartStandardSerie serie, Exc { _hasLeaderLines = true; } - ////TopMargin = Rectangle.Top; - ////BottomMargin = Rectangle.Bottom; if (dataLabel.ShowLeaderLines) { var cPoints = new ConnectionPointsMiddle(Bounds.Left, Bounds.Top, Bounds.Width, Bounds.Height); //Since this is a child transform changes to this transform will compound - lines = new PointLines(ChartRenderer, Bounds, cPoints); - //var index = GetClosestConnectionPointToOriginIndex(); - //ConnectionPointsLines.Clear(); - - ////Add connection points to render - //List ptColors = new List { "red", "green", "blue", "yellow" }; - //for (int i = 0; i < connectionPoints.Points.Count; i++) - //{ - // var cPoint = connectionPoints.Points[i]; - // var cPointLine = new SvgRenderLineItem(chart, txtBox.TextBody.Bounds); - // cPointLine.X1 = 0; - // cPointLine.Y1 = 0; - // cPointLine.X2 = cPoint.X; - // cPointLine.Y2 = cPoint.Y; - - // cPointLine.BorderWidth = 1; - // cPointLine.BorderColor = ptColors[i]; - // ConnectionPointsLines.Add(cPointLine); - //} + connectionPointLines = new PointLines(ChartRenderer, Bounds, cPoints); } - - // double xOffset = 0; - // if(index == 0 || index == 2) - // { - // //If Left or Right - // //Add extra 7 px (5.25pt) line to the given side - // var extraLine = new SvgRenderLineItem(chart, Bounds); - - // xOffset = index == 0 ? - 5.25d : 5.25d; - - // extraLine.X1 = ConnectionPoints[index].X; - // extraLine.Y1 = ConnectionPoints[index].Y; - // extraLine.Y2 = ConnectionPoints[index].Y; - // extraLine.X2 = extraLine.X1 + xOffset; - - // extraLine.BorderColor = "black"; - // extraLine.BorderWidth = 1; - - // LeaderLines.Add(extraLine); - // } - // var mainLine = new SvgRenderLineItem(chart, Bounds); - // mainLine.X1 = ConnectionPoints[index].X + xOffset; - // mainLine.Y1 = ConnectionPoints[index].Y; - // mainLine.X2 = -Bounds.Left; - // mainLine.Y2 = -Bounds.Top; - - // mainLine.BorderColor = "black"; - // mainLine.BorderWidth = 1; - // LeaderLines.Add(mainLine); - //} } } else @@ -216,12 +190,6 @@ internal void ImportDataLabel(SvgChart chart, ExcelChartStandardSerie serie, Exc } } - private int GetClosestConnectionPointToOriginIndex() - { - //Origin in local coordinates is 0,0 - return GetClosestConnectionPointCoordinateIndex(new Coordinate(0, 0)); - } - private int GetClosestConnectionPointCoordinateIndex(Coordinate originPoint) { //CalculateConnectionPoints(); @@ -230,13 +198,13 @@ private int GetClosestConnectionPointCoordinateIndex(Coordinate originPoint) int i = 0; int smallestIndex = 0; - foreach (var line in lines.RenderLines) + foreach (var line in connectionPointLines.RenderLines) { - line.X1 = originPoint.X - line.Bounds.Left - Bounds.Left; - line.Y1 = originPoint.Y + line.Bounds.Top; + line.X1 = originPoint.X; + line.Y1 = originPoint.Y; - var w = Math.Abs(line.X2 + manualLayoutOffset.X - originPoint.X); - var h = Math.Abs(line.Y2 + manualLayoutOffset.Y - originPoint.Y); + var w = Math.Abs(line.X2 - line.X1); + var h = Math.Abs(line.Y2 - line.Y1); //Use pythagoran theorem to get diagonal distance var totalDist = Math.Sqrt(Math.Pow(w, 2) + Math.Pow(h, 2)); @@ -252,6 +220,32 @@ private int GetClosestConnectionPointCoordinateIndex(Coordinate originPoint) return smallestIndex; } + internal void SetParentPoint(BoundingBox parentPoint) + { + Bounds.Parent = parentPoint; + _parentPoint = parentPoint; + + switch (_labelPosition) + { + case eLabelPosition.Center: + break; + case eLabelPosition.Left: + Bounds.Left -= TxtBox.Width + (parentPoint.Width / 2); + break; + case eLabelPosition.Right: + Bounds.Left += TxtBox.Width / 2 + parentPoint.Width; + break; + case eLabelPosition.Top: + Bounds.Top -= (parentPoint.Height + TxtBox.Height) / 2; + break; + case eLabelPosition.Bottom: + Bounds.Top += (parentPoint.Height + TxtBox.Height) / 2; + break; + default: + throw new InvalidOperationException($"The datalabel position {_labelPosition} has not been implemented yet"); + } + } + internal void SetOriginPointOffset(double xPos, double yPos) { originPoint.X = xPos; @@ -262,97 +256,109 @@ internal void SetOriginPointOffset(double xPos, double yPos) if (_hasManualLayout) { - Bounds.Top += manualLayoutOffset.Y; - Bounds.Left += manualLayoutOffset.X; - - if (_hasLeaderLines) - { - var index = GetClosestConnectionPointCoordinateIndex(originPoint); - - LeaderLines.Clear(); - - double xOffset = 0; - if (index == 0 || index == 2) - { - //If Left or Right - //Add extra 7 px (5.25pt) line to the given side - var extraLine = new SvgRenderLineItem(ChartRenderer, ChartRenderer.Bounds); + //Bounds.Top += manualLayoutOffset.Y; + //Bounds.Left += manualLayoutOffset.X; + + //if (_hasLeaderLines) + //{ + // originPoint.Y -= Bounds.Top; + // originPoint.X -= Bounds.Left; + + // var index = GetClosestConnectionPointCoordinateIndex(originPoint); + + // LeaderLines.Clear(); + + // double xOffset = 0; + // if (index == 0 || index == 2) + // { + // //If Left or Right + // //Add extra 7 px (5.25pt) line to the given side + // var extraLine = new SvgRenderLineItem(ChartRenderer, ChartRenderer.Bounds); + + // xOffset = index == 0 ? -5.25d : 5.25d; + + // extraLine.X1 = connectionPointLines.ConnectionPoints.Points[index].X; + // extraLine.Y1 = connectionPointLines.ConnectionPoints.Points[index].Y; + // extraLine.Y2 = connectionPointLines.ConnectionPoints.Points[index].Y; + // extraLine.X2 = extraLine.X1 + xOffset; + + // extraLine.BorderColor = "gray"; + // extraLine.BorderWidth = 0.5; + + // LeaderLines.Add(extraLine); + // } + // var mainLine = new SvgRenderLineItem(ChartRenderer, ChartRenderer.Bounds); + // mainLine.X1 = connectionPointLines.ConnectionPoints.Points[index].X + xOffset; + // mainLine.Y1 = connectionPointLines.ConnectionPoints.Points[index].Y; + // mainLine.X2 = originPoint.X; + // mainLine.Y2 = originPoint.Y; + + // mainLine.BorderColor = "gray"; + // mainLine.BorderWidth = 0.5; + // LeaderLines.Add(mainLine); + //} + } + } - xOffset = index == 0 ? -5.25d : 5.25d; + bool renderConnectionPointLines = false; - extraLine.X1 = lines.ConnectionPoints.Points[index].X; - extraLine.Y1 = lines.ConnectionPoints.Points[index].Y; - extraLine.Y2 = lines.ConnectionPoints.Points[index].Y; - extraLine.X2 = extraLine.X1 + xOffset; + internal override void AppendRenderItems(List renderItems) + { + var parentPointGroup = new SvgGroupItem(ChartRenderer, _parentPoint); + renderItems.Add(parentPointGroup); - extraLine.BorderColor = "black"; - extraLine.BorderWidth = 1; + var titleItemOrigin = new SvgTitleItem(DrawingRenderer, "DataLabel originpoint"); + renderItems.Add(titleItemOrigin); - LeaderLines.Add(extraLine); - } - var mainLine = new SvgRenderLineItem(ChartRenderer, ChartRenderer.Bounds); - mainLine.X1 = lines.ConnectionPoints.Points[index].X + xOffset; - mainLine.Y1 = lines.ConnectionPoints.Points[index].Y; - mainLine.X2 = -Bounds.Left; - mainLine.Y2 = -Bounds.Top; - - mainLine.BorderColor = "black"; - mainLine.BorderWidth = 1; - LeaderLines.Add(mainLine); - } - } - } + SvgRenderRectItem markerRect = new SvgRenderRectItem(DrawingRenderer, _parentPoint); - //private void CalculateConnectionPoints() - //{ - // internalBounds.Left = Bounds.Left; - // internalBounds.Top = Bounds.Top; - // internalBounds.Width = Bounds.Width; - // internalBounds.Height = Bounds.Height; + markerRect.Width = _parentPoint.Width; + markerRect.Height = _parentPoint.Height; - // var middleWidth = Bounds.Width / 2 + LeftMargin; - // var middleHeight = Bounds.Height / 2 + TopMargin; + markerRect.Top -= _parentPoint.Height / 2; + markerRect.Left -= _parentPoint.Width / 2; - // var cPointLeft = new Coordinate(Bounds.Left + LeftMargin , middleHeight); - // var cPointTop = new Coordinate(middleWidth, Bounds.Top + TopMargin); - // var cPointRight = new Coordinate(Bounds.Right + LeftMargin, middleHeight); - // var cPointBottom = new Coordinate(middleWidth, Bounds.Bottom + TopMargin); + markerRect.FillColor = "blue"; + markerRect.FillOpacity = 0.2d; - // ConnectionPoints = new List { cPointLeft, cPointTop, cPointRight, cPointBottom }; - //} + renderItems.Add(markerRect); - internal override void AppendRenderItems(List renderItems) - { var group = new SvgGroupItem(ChartRenderer, Bounds); renderItems.Add(group); - var titleItem = new SvgTitleItem(DrawingRenderer, "DataLabelRect"); - renderItems.Add(titleItem); - SvgRenderRectItem rect = new SvgRenderRectItem(ChartRenderer, Bounds); - rect.Bounds.Left = 0; - rect.Bounds.Top = 0; - rect.Bounds.Width = Bounds.Width; - rect.Bounds.Height = Bounds.Height; + var titleItem = new SvgTitleItem(DrawingRenderer, "DataLabel size adjustment"); + //renderItems.Add(titleItem); + //SvgRenderRectItem rect = new SvgRenderRectItem(ChartRenderer, Bounds); + //rect.Bounds.Left = 0; + //rect.Bounds.Top = 0; + //rect.Bounds.Width = Bounds.Width; + //rect.Bounds.Height = Bounds.Height; + + //rect.FillColor = "red"; + //rect.FillOpacity = 0.2; + //renderItems.Add(rect); - rect.FillColor = "red"; - rect.FillOpacity = 0.2; - renderItems.Add(rect); + TxtBox.Rectangle.FillColor = "red"; + TxtBox.Rectangle.FillOpacity = 1d; TxtBox.AppendRenderItems(renderItems); + + if(renderConnectionPointLines) + { + if (connectionPointLines != null) + { + connectionPointLines.AppendRenderItems(renderItems); + } + } - if (lines != null) + if (LeaderLines != null && LeaderLines.Count > 0) { - lines.AppendRenderItems(renderItems); + foreach (var line in LeaderLines) + { + renderItems.Add(line); + } } - //renderItems.Add(new SvgEndGroupItem(DrawingRenderer, Bounds)); - - //if (LeaderLines != null && LeaderLines.Count > 0) - //{ - // foreach (var line in LeaderLines) - // { - // renderItems.Add(line); - // } - //} + renderItems.Add(new SvgEndGroupItem(DrawingRenderer, Bounds)); renderItems.Add(new SvgEndGroupItem(DrawingRenderer, Bounds)); } } diff --git a/src/EPPlus.Export.ImageRenderer/RenderItems/SvgItem/SvgChartSerieDataLabel.cs b/src/EPPlus.Export.ImageRenderer/RenderItems/SvgItem/SvgChartSerieDataLabel.cs index e5400bbb0..afa6e7ad3 100644 --- a/src/EPPlus.Export.ImageRenderer/RenderItems/SvgItem/SvgChartSerieDataLabel.cs +++ b/src/EPPlus.Export.ImageRenderer/RenderItems/SvgItem/SvgChartSerieDataLabel.cs @@ -10,6 +10,7 @@ using OfficeOpenXml.Drawing; using OfficeOpenXml.Drawing.Chart; using OfficeOpenXml.Drawing.Chart.Style; +using OfficeOpenXml.FormulaParsing.Excel.Functions.MathFunctions; using OfficeOpenXml.FormulaParsing.Excel.Functions.RefAndLookup; using OfficeOpenXml.Interfaces.Drawing.Text; using OfficeOpenXml.Style; @@ -38,11 +39,14 @@ internal class SvgChartSerieDataLabel : DrawingObjectNoBounds ExcelTextFont defaultFont; ExcelDrawingParagraph defaultParagraph; + BoundingBox plotAreaBounds; + public SvgChartSerieDataLabel(SvgChart chart, ExcelChartSerieDataLabel dlblSerie, BoundingBox maxBounds, ExcelChartStandardSerie serie, List xValues, List yValues, int index) : base(chart) { bool addSeriesIcon = false; + plotAreaBounds = chart.Plotarea.Rectangle.Bounds; - if(dlblSerie.TextBody.Paragraphs.Count != 0) + if (dlblSerie.TextBody.Paragraphs.Count != 0) { defaultParagraph = dlblSerie.TextBody.Paragraphs[0]; defaultFont = dlblSerie.TextBody.Paragraphs[0].DefaultRunProperties; @@ -104,8 +108,15 @@ internal void SetPositionOffset(double xPos, double yPos, int i) dataLabels[i].SetOriginPointOffset(xPos, yPos); } + internal void SetParentPoint(BoundingBox parent, int index) + { + dataLabels[index].SetParentPoint(parent); + } + internal override void AppendRenderItems(List renderItems) { + var plotAreaGroup = new SvgGroupItem(DrawingRenderer, plotAreaBounds); + renderItems.Add(plotAreaGroup); for(int i = 0; i< dataLabels.Count; i++) { if (seriesIcon != null && dataLabels[i].HasLegendKey) @@ -125,6 +136,7 @@ internal override void AppendRenderItems(List renderItems) //renderItems.Add(new SvgEndGroupItem(DrawingRenderer, Bounds)); } + renderItems.Add(new SvgEndGroupItem(DrawingRenderer, plotAreaBounds)); } } } diff --git a/src/EPPlus.Export.ImageRenderer/Svg/Chart/ChartTypeDrawers/LineChartTypeDrawer.cs b/src/EPPlus.Export.ImageRenderer/Svg/Chart/ChartTypeDrawers/LineChartTypeDrawer.cs index 112c424ac..c7b65d64e 100644 --- a/src/EPPlus.Export.ImageRenderer/Svg/Chart/ChartTypeDrawers/LineChartTypeDrawer.cs +++ b/src/EPPlus.Export.ImageRenderer/Svg/Chart/ChartTypeDrawers/LineChartTypeDrawer.cs @@ -11,6 +11,7 @@ namespace EPPlus.Export.ImageRenderer.Svg.Chart internal class LineChartTypeDrawer : ChartTypeDrawer { List serieDataLabels = new List(); + List> dataPointsPerSerie = new List>(); internal LineChartTypeDrawer(SvgChart svgChart, ExcelChart chartType) : base(svgChart, chartType) { @@ -32,7 +33,6 @@ internal LineChartTypeDrawer(SvgChart svgChart, ExcelChart chartType) : base(svg if (serie.HasDataLabel) { - var datalabel = new SvgChartSerieDataLabel(svgChart, serie.DataLabel, svgChart.Bounds, serie, xValue, yValue, serCounter); serieDataLabels.Add(datalabel); } @@ -54,21 +54,26 @@ internal LineChartTypeDrawer(SvgChart svgChart, ExcelChart chartType) : base(svg var ySerie = yValues[i]; var serie = (ExcelLineChartSerie)chartType.Series[i]; - SvgChartSerieDataLabel dataLabel = null; + var dataPoints = new List(); + + AddLine(chartType, serie, xSerie, ySerie, dataPoints); + + dataPointsPerSerie.Add(dataPoints); + if (serie.HasDataLabel) { - dataLabel = serieDataLabels[i]; + for (int j = 0; j < dataPoints.Count; j++) + { + serieDataLabels[i].SetParentPoint(dataPoints[j], j); + } } - - AddLine(chartType, serie, xSerie, ySerie, dataLabel); } + RenderItems.Add(new SvgEndGroupItem(ChartRenderer, null)); - foreach(var dataLabel in serieDataLabels) + foreach (var dataLabel in serieDataLabels) { dataLabel.AppendRenderItems(RenderItems); } - - RenderItems.Add(new SvgEndGroupItem(ChartRenderer, null)); } private void SumSeries(List> series) { @@ -80,7 +85,7 @@ private void SumSeries(List> series) } } } - private void AddLine(ExcelChart chartType, ExcelLineChartSerie serie, List xValues, List yValues, SvgChartSerieDataLabel serieDataLabel) + private void AddLine(ExcelChart chartType, ExcelLineChartSerie serie, List xValues, List yValues, List dataPoints) { SvgChartAxis yAxis, xAxis; if (chartType.UseSecondaryAxis) @@ -117,16 +122,18 @@ private void AddLine(ExcelChart chartType, ExcelLineChartSerie serie, List + { + new TextFragment + { + Text = "value, 1", + Font = new MeasurementFont { FontFamily = "Aptos Narrow", Size = 9 } + }, + }; + + // Act + var lines = layout.WrapRichTextLines(fragments, 1000); + + // Assert + Assert.AreEqual(1, lines.Count); + Assert.AreEqual(27.5712890625, lines[0].Width); + Assert.AreEqual("value, 1", lines[0].Text); + } + [TestMethod] public void VerifyWrappingSingleChar() { diff --git a/src/EPPlus.Fonts.OpenType/Integration/TextLayoutEngine.RichText.cs b/src/EPPlus.Fonts.OpenType/Integration/TextLayoutEngine.RichText.cs index c3eb23465..f591175fd 100644 --- a/src/EPPlus.Fonts.OpenType/Integration/TextLayoutEngine.RichText.cs +++ b/src/EPPlus.Fonts.OpenType/Integration/TextLayoutEngine.RichText.cs @@ -141,6 +141,10 @@ private void ProcessFragment( var glyphWidths = shaper.ShapeLight(fragment.Text, options); double scale = fragment.Font.Size / shaper.UnitsPerEm; + //var shaped = shaper.Shape(fragment.Text, options); + //var widthInPoint = shaped.GetWidthInPoints(fragment.Font.Size, shaper.UnitsPerEm); + ////FillCharWidths(gWidthsReal, scale, len, charWidths); + Array.Clear(charWidths, 0, len); FillCharWidths(glyphWidths, scale, len, charWidths); diff --git a/src/EPPlus/Drawing/Chart/ExcelChartSerieDataLabel.cs b/src/EPPlus/Drawing/Chart/ExcelChartSerieDataLabel.cs index b0ae3d77e..e34f5a268 100644 --- a/src/EPPlus/Drawing/Chart/ExcelChartSerieDataLabel.cs +++ b/src/EPPlus/Drawing/Chart/ExcelChartSerieDataLabel.cs @@ -27,7 +27,7 @@ public sealed class ExcelChartSerieDataLabel : ExcelChartDataLabelStandard internal ExcelChartSerieDataLabel(ExcelChart chart, XmlNamespaceManager ns, XmlNode node, string[] schemaNodeOrder) : base(chart, ns, node, "dLbls", schemaNodeOrder) { - Position = eLabelPosition.Center; + //Position = eLabelPosition.Center; var parentSeries = GetParentSeries(); var strRef = parentSeries.GetDataLabelRange(); diff --git a/src/EPPlus/Drawing/Style/Text/ExcelTextBody.cs b/src/EPPlus/Drawing/Style/Text/ExcelTextBody.cs index 9bc82ef4c..5ee1c7bb0 100644 --- a/src/EPPlus/Drawing/Style/Text/ExcelTextBody.cs +++ b/src/EPPlus/Drawing/Style/Text/ExcelTextBody.cs @@ -450,10 +450,10 @@ internal void GetInsetsOrDefaults(out double left, out double top, out double ri internal void GetInsetsInPoints(out double left, out double top, out double right, out double bottom) { - left = (LeftInsert ?? 0) / ExcelDrawing.EMU_PER_POINT; - top = (TopInsert ?? 0) / ExcelDrawing.EMU_PER_POINT; - right = (RightInsert ?? 0) / ExcelDrawing.EMU_PER_POINT; - bottom = (BottomInsert ?? 0) / ExcelDrawing.EMU_PER_POINT; + left = (LeftInsert ?? 0); + top = (TopInsert ?? 0); + right = (RightInsert ?? 0); + bottom = (BottomInsert ?? 0); } ExcelDrawingParagraphCollection _paragraphs = null; From ca01bd1745f33ccc348b4c79d5fef5cccba34203 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ossian=20Edstr=C3=B6m?= Date: Wed, 18 Mar 2026 12:55:40 +0100 Subject: [PATCH 100/151] Added functional manual layout and icon --- .../SvgPathTests.cs | 16 +- .../RenderItems/SvgItem/PointLines.cs | 25 ++- .../SvgItem/SvgChartDataLabelStandard.cs | 211 +++++++++--------- .../SvgItem/SvgChartSerieDataLabel.cs | 19 +- .../Drawing/Chart/ChartTitleTests.cs | 11 + 5 files changed, 151 insertions(+), 131 deletions(-) diff --git a/src/EPPlus.Export.ImageRenderer.Test/SvgPathTests.cs b/src/EPPlus.Export.ImageRenderer.Test/SvgPathTests.cs index e572cd0c6..80f37bcde 100644 --- a/src/EPPlus.Export.ImageRenderer.Test/SvgPathTests.cs +++ b/src/EPPlus.Export.ImageRenderer.Test/SvgPathTests.cs @@ -572,6 +572,20 @@ public void GenerateDataLabelsTrueMost() } } + [TestMethod] + public void GenerateDataLabelsTrueMostAndManualLayout() + { + ExcelPackage.License.SetNonCommercialOrganization("EPPlus Project"); + using (var p = OpenTemplatePackage("datalabelsSvgTrueMostWithFillANDManual.xlsx")) + { + var c = p.Workbook.Worksheets[0].Drawings[0]; + + var renderer = new EPPlusImageRenderer.ImageRenderer(); + var svg = renderer.RenderDrawingToSvg(c); + SaveTextFileToWorkbook($"svg\\datalabelsSvgTrueMostWithFillAndManual.svg", svg); + } + } + [TestMethod] public void GenerateDatalabelsLeaderLines() { @@ -588,7 +602,7 @@ public void GenerateDatalabelsLeaderLines() [TestMethod] - public void GenerateDatalabelsLeaderLinesBg() + public void GenerateDatalabelsRightAlignedWithBg() { ExcelPackage.License.SetNonCommercialOrganization("EPPlus Project"); using (var p = OpenTemplatePackage("datalabelsSvgRightAlignedWithBg.xlsx")) diff --git a/src/EPPlus.Export.ImageRenderer/RenderItems/SvgItem/PointLines.cs b/src/EPPlus.Export.ImageRenderer/RenderItems/SvgItem/PointLines.cs index 7852136ea..79bd7dd59 100644 --- a/src/EPPlus.Export.ImageRenderer/RenderItems/SvgItem/PointLines.cs +++ b/src/EPPlus.Export.ImageRenderer/RenderItems/SvgItem/PointLines.cs @@ -17,27 +17,38 @@ internal class PointLines : DrawingObject internal ConnectionPointsMiddle ConnectionPoints; + private List ptColors = new List { "red", "green", "blue", "yellow" }; + + private BoundingBox parentBounds; + private PointLines(DrawingBase renderer) : base(renderer) { } internal PointLines(DrawingBase renderer, BoundingBox parent, ConnectionPointsMiddle connectionPoints) : this(renderer) { + parentBounds = parent; + Bounds = new BoundingBox(); Bounds.Parent = parent; - Bounds.Left = parent.Left; - Bounds.Top = parent.Top; - Bounds.Width = parent.Width; - Bounds.Height = parent.Height; + //Bounds.Left = parent.Left; + //Bounds.Top = parent.Top; + //Bounds.Width = parent.Width; + //Bounds.Height = parent.Height; ConnectionPoints = connectionPoints; - //Add connection points to render - List ptColors = new List { "red", "green", "blue", "yellow" }; + UpdateLines(); + } + + internal void UpdateLines() + { + RenderLines.Clear(); + for (int i = 0; i < ConnectionPoints.Points.Count; i++) { var cPoint = ConnectionPoints.Points[i]; - var cPointLine = new SvgRenderLineItem(renderer, Bounds); + var cPointLine = new SvgRenderLineItem(DrawingRenderer, Bounds); cPointLine.X1 = 0; cPointLine.Y1 = 0; cPointLine.X2 = cPoint.X; diff --git a/src/EPPlus.Export.ImageRenderer/RenderItems/SvgItem/SvgChartDataLabelStandard.cs b/src/EPPlus.Export.ImageRenderer/RenderItems/SvgItem/SvgChartDataLabelStandard.cs index 8cc77eca3..f7d5d764a 100644 --- a/src/EPPlus.Export.ImageRenderer/RenderItems/SvgItem/SvgChartDataLabelStandard.cs +++ b/src/EPPlus.Export.ImageRenderer/RenderItems/SvgItem/SvgChartDataLabelStandard.cs @@ -4,9 +4,9 @@ using EPPlusImageRenderer.Svg; using OfficeOpenXml.Drawing; using OfficeOpenXml.Drawing.Chart; +using OfficeOpenXml.Utils.EnumUtils; using System; using System.Collections.Generic; -using System.Runtime.InteropServices; namespace EPPlus.Export.ImageRenderer.RenderItems.SvgItem { @@ -16,7 +16,6 @@ internal class SvgChartDataLabelStandard : SvgChartObject bool _hasManualLayout = false; bool _hasLeaderLines = false; - bool haveAdjustedForIcon = false; internal SvgTextBox TxtBox; @@ -25,21 +24,16 @@ internal class SvgChartDataLabelStandard : SvgChartObject List LeaderLines = new List(); - Coordinate originPoint = new Coordinate (0, 0); - Coordinate manualLayoutOffset = new Coordinate (0, 0); - - PointLines connectionPointLines; + Coordinate _manualLayoutOffset = new Coordinate (0, 0); + bool renderConnectionPointLines = false; + PointLines _connectionPointLines; eLabelPosition _labelPosition; - ////Connection point coords are accurate to Internal bounds - //BoundingBox internalBounds = new BoundingBox(); - //List ConnectionPoints = new List(); public SvgChartDataLabelStandard(DrawingChart chart, string dataLabelText) : base(chart) { var txtBox = new SvgTextBox(chart, chart.Bounds, chart.Bounds); txtBox.AddText(0, dataLabelText); - FitToTextBoxContent(); } public SvgChartDataLabelStandard(DrawingChart chart, ExcelChartDataLabelStandard standard) : base(chart) @@ -52,25 +46,23 @@ public SvgChartDataLabelStandard(DrawingChart chart, ExcelChartDataLabelStandard { HasLegendKey = standard.ShowLegendKey; TxtBox = txtBox; - FitToTextBoxContent(); } - private void FitToTextBoxContent() - { - //Bounds.Left = TxtBox.Left; - //Bounds.Top = TxtBox.Top; - //Bounds.Height = TxtBox.Height; - //Bounds.Width = TxtBox.Width; - } + RenderItem _seriesIcon = null; - internal void AddSeriesIcon(double iconWidth, double iconHeight) + internal void AddSeriesIcon(RenderItem seriesIcon) { + var iconWidth = seriesIcon.Bounds.Width; + var iconHeight = seriesIcon.Bounds.Height; + + _seriesIcon = seriesIcon; + _seriesIcon.Bounds.Parent = _parentPoint; + if (haveAdjustedForIcon == false) { if (_hasManualLayout == false) { TxtBox.Left += iconWidth + TxtBox.LeftMargin; - FitToTextBoxContent(); if (iconHeight > TxtBox.Height) { Bounds.Height = iconHeight; @@ -82,6 +74,8 @@ internal void AddSeriesIcon(double iconWidth, double iconHeight) Bounds.Width += iconWidth; Bounds.Height += iconHeight; } + //It seems there is a hard-coded margin in excel of about 4.5pt (6px) in addition to the width of the marker + Bounds.Left += 4.5d; haveAdjustedForIcon = true; } } @@ -122,11 +116,6 @@ internal void ImportDataLabel(SvgChart chart, ExcelChartStandardSerie serie, Exc txtBox.TextBody.Bounds.Top = 0; txtBox.TextBody.AutoSize = true; - //txtBox.LeftMargin = 3.1181d; - //txtBox.RightMargin = 3.1181d; - //txtBox.TopMargin = 1.4173d; - //txtBox.BottomMargin = 1.4173d; - if (txtBox.TextBody.Paragraphs.Count == 0) { txtBox.TextBody.ImportParagraph(defaultParagraph, 0, finalString); @@ -139,36 +128,42 @@ internal void ImportDataLabel(SvgChart chart, ExcelChartStandardSerie serie, Exc txtBox.TextBody.Paragraphs.RemoveAt(0); } + //Center the textbox at the origin point Bounds.Left -= txtBox.Width / 2; Bounds.Top -= txtBox.Height / 2; - ////Reset run y-position. - ////Datalabel does not use the standard line-spacing textbody offsets - //txtBox.TextBody.Paragraphs[0].Runs[0].YPosition = 0; TxtBox = txtBox; - _labelPosition = dataLabel.Position; - - //TxtBox.TextBody.Bounds.Left += TxtBox.LeftMargin; - //TxtBox.TextBody.Bounds.Top += TxtBox.TopMargin; + if (dataLabel.Fill.IsEmpty == false) + { + TxtBox.Rectangle.FillColor = "#" + dataLabel.Fill.Color.ToColorString(); + } + if (dataLabel.Font.IsEmpty == false) + { + txtBox.TextBody.FontColorString = "#" + dataLabel.Font.Color.ToColorString(); + } - //TxtBox.Left = txtBox.LeftMargin; - //TxtBox.Top = txtBox.TopMargin; + _labelPosition = dataLabel.Position; if (dataLabel is ExcelChartDataLabelItem) { var individualLabel = dataLabel as ExcelChartDataLabelItem; + if (individualLabel.Fill.IsEmpty == false) + { + TxtBox.Rectangle.FillColor = "#" + individualLabel.Fill.Color.ToColorString(); + } + if (individualLabel.Layout != null && individualLabel.Layout.HasLayout) { - FitToTextBoxContent(); _hasManualLayout = true; var rect = GetRectFromManualLayout(chart, individualLabel.Layout); Rectangle = rect; - //LeftMargin = Rectangle.Left; - //TopMargin = Rectangle.Top; - manualLayoutOffset = new Coordinate(Rectangle.Left, Rectangle.Top); + _manualLayoutOffset = new Coordinate(Rectangle.Left, Rectangle.Top); + + Bounds.Left += _manualLayoutOffset.X; + Bounds.Top += _manualLayoutOffset.Y; if (dataLabel.ShowLeaderLines) { @@ -177,17 +172,13 @@ internal void ImportDataLabel(SvgChart chart, ExcelChartStandardSerie serie, Exc if (dataLabel.ShowLeaderLines) { - var cPoints = new ConnectionPointsMiddle(Bounds.Left, Bounds.Top, Bounds.Width, Bounds.Height); + var cPoints = new ConnectionPointsMiddle(TxtBox.Rectangle.Bounds.Left, TxtBox.Rectangle.Bounds.Top, TxtBox.Rectangle.Bounds.Width, TxtBox.Rectangle.Bounds.Height); //Since this is a child transform changes to this transform will compound - connectionPointLines = new PointLines(ChartRenderer, Bounds, cPoints); + _connectionPointLines = new PointLines(ChartRenderer, Bounds, cPoints); } } } - else - { - FitToTextBoxContent(); - } } private int GetClosestConnectionPointCoordinateIndex(Coordinate originPoint) @@ -198,7 +189,7 @@ private int GetClosestConnectionPointCoordinateIndex(Coordinate originPoint) int i = 0; int smallestIndex = 0; - foreach (var line in connectionPointLines.RenderLines) + foreach (var line in _connectionPointLines.RenderLines) { line.X1 = originPoint.X; line.Y1 = originPoint.Y; @@ -233,6 +224,7 @@ internal void SetParentPoint(BoundingBox parentPoint) Bounds.Left -= TxtBox.Width + (parentPoint.Width / 2); break; case eLabelPosition.Right: + case eLabelPosition.BestFit: Bounds.Left += TxtBox.Width / 2 + parentPoint.Width; break; case eLabelPosition.Top: @@ -244,64 +236,51 @@ internal void SetParentPoint(BoundingBox parentPoint) default: throw new InvalidOperationException($"The datalabel position {_labelPosition} has not been implemented yet"); } - } - - internal void SetOriginPointOffset(double xPos, double yPos) - { - originPoint.X = xPos; - originPoint.Y = yPos; - - Bounds.Top = yPos; - Bounds.Left = xPos; if (_hasManualLayout) { - //Bounds.Top += manualLayoutOffset.Y; - //Bounds.Left += manualLayoutOffset.X; - - //if (_hasLeaderLines) - //{ - // originPoint.Y -= Bounds.Top; - // originPoint.X -= Bounds.Left; - - // var index = GetClosestConnectionPointCoordinateIndex(originPoint); - - // LeaderLines.Clear(); - - // double xOffset = 0; - // if (index == 0 || index == 2) - // { - // //If Left or Right - // //Add extra 7 px (5.25pt) line to the given side - // var extraLine = new SvgRenderLineItem(ChartRenderer, ChartRenderer.Bounds); - - // xOffset = index == 0 ? -5.25d : 5.25d; - - // extraLine.X1 = connectionPointLines.ConnectionPoints.Points[index].X; - // extraLine.Y1 = connectionPointLines.ConnectionPoints.Points[index].Y; - // extraLine.Y2 = connectionPointLines.ConnectionPoints.Points[index].Y; - // extraLine.X2 = extraLine.X1 + xOffset; - - // extraLine.BorderColor = "gray"; - // extraLine.BorderWidth = 0.5; - - // LeaderLines.Add(extraLine); - // } - // var mainLine = new SvgRenderLineItem(ChartRenderer, ChartRenderer.Bounds); - // mainLine.X1 = connectionPointLines.ConnectionPoints.Points[index].X + xOffset; - // mainLine.Y1 = connectionPointLines.ConnectionPoints.Points[index].Y; - // mainLine.X2 = originPoint.X; - // mainLine.Y2 = originPoint.Y; - - // mainLine.BorderColor = "gray"; - // mainLine.BorderWidth = 0.5; - // LeaderLines.Add(mainLine); - //} + if (_hasLeaderLines) + { + _connectionPointLines.UpdateLines(); + + var originPoint = new Coordinate(-Bounds.Left, -Bounds.Top); + + var index = GetClosestConnectionPointCoordinateIndex(originPoint); + + LeaderLines.Clear(); + + double xOffset = 0; + if (index == 0 || index == 2) + { + //If Left or Right + //Add extra 7 px (5.25pt) line to the given side + var extraLine = new SvgRenderLineItem(ChartRenderer, ChartRenderer.Bounds); + + xOffset = index == 0 ? -5.25d : 5.25d; + + extraLine.X1 = _connectionPointLines.ConnectionPoints.Points[index].X; + extraLine.Y1 = _connectionPointLines.ConnectionPoints.Points[index].Y; + extraLine.Y2 = _connectionPointLines.ConnectionPoints.Points[index].Y; + extraLine.X2 = extraLine.X1 + xOffset; + + extraLine.BorderColor = "gray"; + extraLine.BorderWidth = 0.5; + + LeaderLines.Add(extraLine); + } + var mainLine = new SvgRenderLineItem(ChartRenderer, ChartRenderer.Bounds); + mainLine.X1 = _connectionPointLines.ConnectionPoints.Points[index].X + xOffset; + mainLine.Y1 = _connectionPointLines.ConnectionPoints.Points[index].Y; + mainLine.X2 = originPoint.X; + mainLine.Y2 = originPoint.Y; + + mainLine.BorderColor = "gray"; + mainLine.BorderWidth = 0.5; + LeaderLines.Add(mainLine); + } } } - bool renderConnectionPointLines = false; - internal override void AppendRenderItems(List renderItems) { var parentPointGroup = new SvgGroupItem(ChartRenderer, _parentPoint); @@ -310,18 +289,18 @@ internal override void AppendRenderItems(List renderItems) var titleItemOrigin = new SvgTitleItem(DrawingRenderer, "DataLabel originpoint"); renderItems.Add(titleItemOrigin); - SvgRenderRectItem markerRect = new SvgRenderRectItem(DrawingRenderer, _parentPoint); + //SvgRenderRectItem markerRect = new SvgRenderRectItem(DrawingRenderer, _parentPoint); - markerRect.Width = _parentPoint.Width; - markerRect.Height = _parentPoint.Height; + //markerRect.Width = _parentPoint.Width; + //markerRect.Height = _parentPoint.Height; - markerRect.Top -= _parentPoint.Height / 2; - markerRect.Left -= _parentPoint.Width / 2; + //markerRect.Top -= _parentPoint.Height / 2; + //markerRect.Left -= _parentPoint.Width / 2; - markerRect.FillColor = "blue"; - markerRect.FillOpacity = 0.2d; + //markerRect.FillColor = "blue"; + //markerRect.FillOpacity = 0.2d; - renderItems.Add(markerRect); + //renderItems.Add(markerRect); var group = new SvgGroupItem(ChartRenderer, Bounds); renderItems.Add(group); @@ -338,16 +317,30 @@ internal override void AppendRenderItems(List renderItems) //rect.FillOpacity = 0.2; //renderItems.Add(rect); - TxtBox.Rectangle.FillColor = "red"; - TxtBox.Rectangle.FillOpacity = 1d; + //TxtBox.Rectangle.FillColor = "red"; + //TxtBox.Rectangle.FillOpacity = 1d; + + if(_seriesIcon != null) + { + var height = Bounds.Height; + if(height == 0) + { + height = TxtBox.Height; + } + //Currently series icon always has a y1 y2 of 2 + var iconGrp = new SvgGroupItem(ChartRenderer, 0, height / 2 - 2); + renderItems.Add(iconGrp); + renderItems.Add(_seriesIcon); + renderItems.Add(new SvgEndGroupItem(DrawingRenderer, Bounds)); + } TxtBox.AppendRenderItems(renderItems); if(renderConnectionPointLines) { - if (connectionPointLines != null) + if (_connectionPointLines != null) { - connectionPointLines.AppendRenderItems(renderItems); + _connectionPointLines.AppendRenderItems(renderItems); } } diff --git a/src/EPPlus.Export.ImageRenderer/RenderItems/SvgItem/SvgChartSerieDataLabel.cs b/src/EPPlus.Export.ImageRenderer/RenderItems/SvgItem/SvgChartSerieDataLabel.cs index afa6e7ad3..11d3d4741 100644 --- a/src/EPPlus.Export.ImageRenderer/RenderItems/SvgItem/SvgChartSerieDataLabel.cs +++ b/src/EPPlus.Export.ImageRenderer/RenderItems/SvgItem/SvgChartSerieDataLabel.cs @@ -103,11 +103,6 @@ private void AddDatalabel(SvgChart chart, ExcelChartStandardSerie serie, ExcelCh dataLabels.Add(newDataLabel); } - internal void SetPositionOffset(double xPos, double yPos, int i) - { - dataLabels[i].SetOriginPointOffset(xPos, yPos); - } - internal void SetParentPoint(BoundingBox parent, int index) { dataLabels[index].SetParentPoint(parent); @@ -121,16 +116,12 @@ internal override void AppendRenderItems(List renderItems) { if (seriesIcon != null && dataLabels[i].HasLegendKey) { - //groupItems[i].Bounds.Left += (seriesIcon.Bounds.Width / 2); - //groupItems[i].GroupTransform = $"transform=\"translate({groupItems[i].Bounds.Left.PointToPixelString()}, {groupItems[i].Bounds.Top.PointToPixelString()})\""; - dataLabels[i].AddSeriesIcon(seriesIcon.Bounds.Width, seriesIcon.Bounds.Height); - //renderItems.Add(groupItems[i]); - renderItems.Add(seriesIcon); - } - else - { - //renderItems.Add(groupItems[i]); + dataLabels[i].AddSeriesIcon(seriesIcon); } + //else + //{ + // //renderItems.Add(groupItems[i]); + //} dataLabels[i].AppendRenderItems(renderItems); diff --git a/src/EPPlusTest/Drawing/Chart/ChartTitleTests.cs b/src/EPPlusTest/Drawing/Chart/ChartTitleTests.cs index d39c9fdcc..0494dc10e 100644 --- a/src/EPPlusTest/Drawing/Chart/ChartTitleTests.cs +++ b/src/EPPlusTest/Drawing/Chart/ChartTitleTests.cs @@ -102,5 +102,16 @@ public void AddPiePivotTableWithTitle() chart.Title.SetAutoTitle(); chart.Font.Color = Color.Red; } + + [TestMethod] + public void AddTitleMoveTest() + { + var ws = _pck.Workbook.Worksheets.Add("TitleMoveTest"); + var chart = ws.Drawings.AddBarChart("barChart1", eBarChartType.BarClustered); + ws.Cells["A1"].Value = "Linked Cell Title"; + chart.Series.Add("Data!N1:N10", "Data!K1:K10"); + chart.XAxis.AddTitle(ws.Cells["A1"]); + chart.Title.l + } } } From 22c02d96642633fc647aa28272544deaf6e32197 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ossian=20Edstr=C3=B6m?= Date: Wed, 18 Mar 2026 17:12:29 +0100 Subject: [PATCH 101/151] Fixed datalabels all options at once. Cleanup left --- .../SvgItem/ConnectionPointsMiddle.cs | 13 +- .../SvgItem/SvgChartDataLabelStandard.cs | 163 +++++++++++------- .../SvgItem/SvgChartSerieDataLabel.cs | 80 ++++++--- .../RenderItems/SvgItem/SvgTextBox.cs | 8 +- 4 files changed, 169 insertions(+), 95 deletions(-) diff --git a/src/EPPlus.Export.ImageRenderer/RenderItems/SvgItem/ConnectionPointsMiddle.cs b/src/EPPlus.Export.ImageRenderer/RenderItems/SvgItem/ConnectionPointsMiddle.cs index ef38951e6..e8150275f 100644 --- a/src/EPPlus.Export.ImageRenderer/RenderItems/SvgItem/ConnectionPointsMiddle.cs +++ b/src/EPPlus.Export.ImageRenderer/RenderItems/SvgItem/ConnectionPointsMiddle.cs @@ -12,20 +12,21 @@ internal class ConnectionPointsMiddle internal Coordinate Right; internal Coordinate Bottom; - internal Dictionary Points; + internal Dictionary Points = new Dictionary(); internal ConnectionPointsMiddle(double left, double top, double width, double height) { var middleWidth = width / 2; var middleHeight = height / 2; - Left = new Coordinate(left, middleHeight); - Top= new Coordinate(middleWidth, top); + var middleX = left + middleWidth; + var middleY = top + middleHeight; - Right = new Coordinate(left + width, middleHeight); - Bottom = new Coordinate(middleWidth, top + height); + Left = new Coordinate(left, middleY); + Top = new Coordinate(middleX, top); - Points = new Dictionary(); + Right = new Coordinate(left + width, middleY); + Bottom = new Coordinate(left + middleX, top + height); Points.Add(0, Left); Points.Add(1, Top); diff --git a/src/EPPlus.Export.ImageRenderer/RenderItems/SvgItem/SvgChartDataLabelStandard.cs b/src/EPPlus.Export.ImageRenderer/RenderItems/SvgItem/SvgChartDataLabelStandard.cs index f7d5d764a..3a92dcf32 100644 --- a/src/EPPlus.Export.ImageRenderer/RenderItems/SvgItem/SvgChartDataLabelStandard.cs +++ b/src/EPPlus.Export.ImageRenderer/RenderItems/SvgItem/SvgChartDataLabelStandard.cs @@ -56,31 +56,60 @@ internal void AddSeriesIcon(RenderItem seriesIcon) var iconHeight = seriesIcon.Bounds.Height; _seriesIcon = seriesIcon; - _seriesIcon.Bounds.Parent = _parentPoint; + _seriesIcon.Bounds.Parent = Bounds; if (haveAdjustedForIcon == false) { - if (_hasManualLayout == false) - { - TxtBox.Left += iconWidth + TxtBox.LeftMargin; - if (iconHeight > TxtBox.Height) - { - Bounds.Height = iconHeight; - } - } - else - { - Bounds.Left += iconWidth + TxtBox.LeftMargin; - Bounds.Width += iconWidth; - Bounds.Height += iconHeight; - } + TxtBox.Left += iconWidth; + _seriesIcon.Bounds.Left -= 0.75d; + //It seems there is a hard-coded margin in excel of about 4.5pt (6px) + Bounds.Left += 4d + 2.25d; + LeftMargin -= 2.25d + 4d; + Bounds.Width += iconWidth + 2.25d; + + //Bounds.Left += TxtBox.LeftMargin; + //Bounds.Width += iconWidth; + //_seriesIcon.Bounds.Left -= 0.75; + //if (iconHeight > TxtBox.Height) + //{ + // Bounds.Height = iconHeight; + //} + //TxtBox.Left += iconWidth + 3; + //Bounds.Width += iconWidth + 3; + + + //if (iconHeight > TxtBox.Rectangle.Bounds.Height) + //{ + // Bounds.Height = iconHeight; + //} + //if (_hasManualLayout == false) + //{ + // TxtBox.Left += iconWidth + TxtBox.LeftMargin; + // if (iconHeight > TxtBox.Height) + // { + // Bounds.Height = iconHeight; + // } + //} + //else + //{ + // Bounds.Left += iconWidth + TxtBox.LeftMargin; + // Bounds.Width += iconWidth; + // Bounds.Height += iconHeight; + //} + + //Bounds.Left -= 3d; //It seems there is a hard-coded margin in excel of about 4.5pt (6px) in addition to the width of the marker - Bounds.Left += 4.5d; + //TxtBox.Left += 4.5d; + //seriesIcon.Bounds.Left += 4.5d; + //Bounds.Left -= 4.5d; + //Bounds.Left -= 4.5d; + //Bounds.Width += 4.5d; + haveAdjustedForIcon = true; } } - internal void ImportDataLabel(SvgChart chart, ExcelChartStandardSerie serie, ExcelChartDataLabelStandard dataLabel, object xValue, object yValue, ExcelDrawingParagraph defaultParagraph, BoundingBox maxBounds) + internal void ImportDataLabel(SvgChart chart, ExcelChartStandardSerie serie, ExcelChartDataLabelStandard dataLabel, object xValue, object yValue, ExcelDrawingParagraph defaultParagraph, BoundingBox maxBounds, BoundingBox defaultMargins) { List dlblStrings = new List(); @@ -128,15 +157,31 @@ internal void ImportDataLabel(SvgChart chart, ExcelChartStandardSerie serie, Exc txtBox.TextBody.Paragraphs.RemoveAt(0); } + if(txtBox.LeftMargin == 0) + { + txtBox.LeftMargin = defaultMargins.Left; + txtBox.RightMargin = defaultMargins.Width; + txtBox.TopMargin = defaultMargins.Top; + txtBox.BottomMargin = defaultMargins.Height; + } + + //Txtbox is Broken. Workaround + txtBox.Left += txtBox.LeftMargin; + txtBox.Top += txtBox.TopMargin; + //Center the textbox at the origin point - Bounds.Left -= txtBox.Width / 2; - Bounds.Top -= txtBox.Height / 2; + Bounds.Left -= txtBox.Rectangle.Bounds.Width / 2; + Bounds.Top -= txtBox.Rectangle.Bounds.Height / 2; + + //Set initial width and height to content + Bounds.Width = txtBox.Rectangle.Bounds.Width; + Bounds.Height = txtBox.Rectangle.Bounds.Height; TxtBox = txtBox; if (dataLabel.Fill.IsEmpty == false) { - TxtBox.Rectangle.FillColor = "#" + dataLabel.Fill.Color.ToColorString(); + TxtBox.Rectangle.SetDrawingPropertiesFill(dataLabel.Fill, null); } if (dataLabel.Font.IsEmpty == false) { @@ -169,22 +214,12 @@ internal void ImportDataLabel(SvgChart chart, ExcelChartStandardSerie serie, Exc { _hasLeaderLines = true; } - - if (dataLabel.ShowLeaderLines) - { - var cPoints = new ConnectionPointsMiddle(TxtBox.Rectangle.Bounds.Left, TxtBox.Rectangle.Bounds.Top, TxtBox.Rectangle.Bounds.Width, TxtBox.Rectangle.Bounds.Height); - - //Since this is a child transform changes to this transform will compound - _connectionPointLines = new PointLines(ChartRenderer, Bounds, cPoints); - } } } } private int GetClosestConnectionPointCoordinateIndex(Coordinate originPoint) { - //CalculateConnectionPoints(); - double smallestDist = double.MaxValue; int i = 0; int smallestIndex = 0; @@ -241,11 +276,21 @@ internal void SetParentPoint(BoundingBox parentPoint) { if (_hasLeaderLines) { + //With origin in top left of current bounds get the connection points + var cPoints = new ConnectionPointsMiddle(0, 0, Bounds.Width, Bounds.Height); + + //Ready to draw the lines so that we can visualize the distances to each point + _connectionPointLines = new PointLines(ChartRenderer, Bounds, cPoints); + + //Adjust if there is a margin + _connectionPointLines.Bounds.Left += LeftMargin; _connectionPointLines.UpdateLines(); - var originPoint = new Coordinate(-Bounds.Left, -Bounds.Top); + //Get the offset between those points and the origin point + var offsetToParentPoint = new Coordinate(-(Bounds.Left + LeftMargin), -(Bounds.Top + TopMargin)); - var index = GetClosestConnectionPointCoordinateIndex(originPoint); + //Calculate closest point + var index = GetClosestConnectionPointCoordinateIndex(offsetToParentPoint); LeaderLines.Clear(); @@ -256,9 +301,9 @@ internal void SetParentPoint(BoundingBox parentPoint) //Add extra 7 px (5.25pt) line to the given side var extraLine = new SvgRenderLineItem(ChartRenderer, ChartRenderer.Bounds); - xOffset = index == 0 ? -5.25d : 5.25d; + xOffset += index == 0 ? -5.25d : 5.25d; - extraLine.X1 = _connectionPointLines.ConnectionPoints.Points[index].X; + extraLine.X1 = _connectionPointLines.ConnectionPoints.Points[index].X + LeftMargin; extraLine.Y1 = _connectionPointLines.ConnectionPoints.Points[index].Y; extraLine.Y2 = _connectionPointLines.ConnectionPoints.Points[index].Y; extraLine.X2 = extraLine.X1 + xOffset; @@ -269,10 +314,10 @@ internal void SetParentPoint(BoundingBox parentPoint) LeaderLines.Add(extraLine); } var mainLine = new SvgRenderLineItem(ChartRenderer, ChartRenderer.Bounds); - mainLine.X1 = _connectionPointLines.ConnectionPoints.Points[index].X + xOffset; + mainLine.X1 = _connectionPointLines.ConnectionPoints.Points[index].X + xOffset + LeftMargin; mainLine.Y1 = _connectionPointLines.ConnectionPoints.Points[index].Y; - mainLine.X2 = originPoint.X; - mainLine.Y2 = originPoint.Y; + mainLine.X2 = offsetToParentPoint.X + LeftMargin; + mainLine.Y2 = offsetToParentPoint.Y; mainLine.BorderColor = "gray"; mainLine.BorderWidth = 0.5; @@ -306,43 +351,41 @@ internal override void AppendRenderItems(List renderItems) renderItems.Add(group); var titleItem = new SvgTitleItem(DrawingRenderer, "DataLabel size adjustment"); - //renderItems.Add(titleItem); - //SvgRenderRectItem rect = new SvgRenderRectItem(ChartRenderer, Bounds); - //rect.Bounds.Left = 0; - //rect.Bounds.Top = 0; - //rect.Bounds.Width = Bounds.Width; - //rect.Bounds.Height = Bounds.Height; + renderItems.Add(titleItem); + SvgRenderRectItem rect = new SvgRenderRectItem(ChartRenderer, Bounds); + rect.Bounds.Left = LeftMargin; + rect.Bounds.Top = 0; + rect.Bounds.Width = Bounds.Width; + rect.Bounds.Height = Bounds.Height; - //rect.FillColor = "red"; - //rect.FillOpacity = 0.2; - //renderItems.Add(rect); + rect.FillColor = "red"; + rect.FillOpacity = 0.2; + renderItems.Add(rect); - //TxtBox.Rectangle.FillColor = "red"; - //TxtBox.Rectangle.FillOpacity = 1d; + TxtBox.AppendRenderItems(renderItems); + + if(renderConnectionPointLines) + { + if (_connectionPointLines != null) + { + _connectionPointLines.AppendRenderItems(renderItems); + } + } - if(_seriesIcon != null) + if (_seriesIcon != null) { var height = Bounds.Height; - if(height == 0) + if (height == 0) { height = TxtBox.Height; } //Currently series icon always has a y1 y2 of 2 - var iconGrp = new SvgGroupItem(ChartRenderer, 0, height / 2 - 2); + var iconGrp = new SvgGroupItem(ChartRenderer, _seriesIcon.Bounds.Left, height / 2 - 2); renderItems.Add(iconGrp); renderItems.Add(_seriesIcon); renderItems.Add(new SvgEndGroupItem(DrawingRenderer, Bounds)); } - TxtBox.AppendRenderItems(renderItems); - - if(renderConnectionPointLines) - { - if (_connectionPointLines != null) - { - _connectionPointLines.AppendRenderItems(renderItems); - } - } if (LeaderLines != null && LeaderLines.Count > 0) { diff --git a/src/EPPlus.Export.ImageRenderer/RenderItems/SvgItem/SvgChartSerieDataLabel.cs b/src/EPPlus.Export.ImageRenderer/RenderItems/SvgItem/SvgChartSerieDataLabel.cs index 11d3d4741..c161402d3 100644 --- a/src/EPPlus.Export.ImageRenderer/RenderItems/SvgItem/SvgChartSerieDataLabel.cs +++ b/src/EPPlus.Export.ImageRenderer/RenderItems/SvgItem/SvgChartSerieDataLabel.cs @@ -41,8 +41,18 @@ internal class SvgChartSerieDataLabel : DrawingObjectNoBounds BoundingBox plotAreaBounds; + BoundingBox _defaultMargins; + + ExcelChartSerieDataLabel _dlblSerie; + + int _serieIndex = -1; + bool _hasAddedSeriesIcon = false; + public SvgChartSerieDataLabel(SvgChart chart, ExcelChartSerieDataLabel dlblSerie, BoundingBox maxBounds, ExcelChartStandardSerie serie, List xValues, List yValues, int index) : base(chart) { + _serieIndex = index; + _dlblSerie = dlblSerie; + bool addSeriesIcon = false; plotAreaBounds = chart.Plotarea.Rectangle.Bounds; @@ -50,6 +60,8 @@ public SvgChartSerieDataLabel(SvgChart chart, ExcelChartSerieDataLabel dlblSerie { defaultParagraph = dlblSerie.TextBody.Paragraphs[0]; defaultFont = dlblSerie.TextBody.Paragraphs[0].DefaultRunProperties; + 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) @@ -57,7 +69,7 @@ public SvgChartSerieDataLabel(SvgChart chart, ExcelChartSerieDataLabel dlblSerie for (int i = 0; i < serie.NumberOfItems; i++) { - AddDatalabel(chart, serie, dlblSerie, xValues[i], yValues[i], maxBounds, ref addSeriesIcon); + AddDatalabel(chart, serie, dlblSerie, xValues[i], yValues[i], maxBounds); } } else @@ -66,40 +78,50 @@ public SvgChartSerieDataLabel(SvgChart chart, ExcelChartSerieDataLabel dlblSerie { var dataLabel = dlblSerie.DataLabels[i]; - AddDatalabel(chart, serie, dataLabel, xValues[i], yValues[i], maxBounds, ref addSeriesIcon); + AddDatalabel(chart, serie, dataLabel, xValues[i], yValues[i], maxBounds); } } + } - if(addSeriesIcon) + private void CreateSeriesIcon(SvgChart chart, ExcelChartStandardSerie serie, BoundingBox maxBounds) + { + if (chart.Legend == null) { - if (chart.Legend == null) - { - seriesIcon = chart.GetSeriesIcon(serie, index, maxBounds); - } - else - { - var legendItem = chart.Legend; - var seriesIconOrig = (SvgRenderLineItem)legendItem.SeriesIcon[index].SeriesIcon; - var clonedIcon = seriesIconOrig.Clone(chart); + seriesIcon = chart.GetSeriesIcon(serie, _serieIndex, maxBounds); + } + else + { + var legendItem = chart.Legend; + var seriesIconOrig = (SvgRenderLineItem)legendItem.SeriesIcon[_serieIndex].SeriesIcon; + var clonedIcon = seriesIconOrig.Clone(chart); - clonedIcon.Y1 = 0; - clonedIcon.Y2 = 0; + clonedIcon.Y1 = 0; + clonedIcon.Y2 = 0; - seriesIcon = clonedIcon; - } + seriesIcon = clonedIcon; } - } - private void AddDatalabel(SvgChart chart, ExcelChartStandardSerie serie, ExcelChartDataLabelStandard dataLabel, object xValue, object yValue, BoundingBox maxBounds, ref bool addSeriesIcon) + private RenderItem GetSeriesIcon(SvgChart chart, ExcelChartStandardSerie serie, BoundingBox maxBounds) { - if (addSeriesIcon == false && dataLabel.ShowLegendKey) + if(seriesIcon == null) { - addSeriesIcon = dataLabel.ShowLegendKey; + CreateSeriesIcon(chart, serie, maxBounds); } + return seriesIcon; + } + + private void AddDatalabel(SvgChart chart, ExcelChartStandardSerie serie, ExcelChartDataLabelStandard dataLabel, object xValue, object yValue, BoundingBox maxBounds) + { var newDataLabel = new SvgChartDataLabelStandard(chart, dataLabel); - newDataLabel.ImportDataLabel(chart, serie, dataLabel, xValue, yValue, defaultParagraph, maxBounds); + newDataLabel.ImportDataLabel(chart, serie, dataLabel, xValue, yValue, defaultParagraph, maxBounds, _defaultMargins); + + if(dataLabel.ShowLegendKey) + { + newDataLabel.AddSeriesIcon(GetSeriesIcon(chart, serie, maxBounds)); + } + dataLabels.Add(newDataLabel); } @@ -111,13 +133,21 @@ internal void SetParentPoint(BoundingBox parent, int index) internal override void AppendRenderItems(List renderItems) { var plotAreaGroup = new SvgGroupItem(DrawingRenderer, plotAreaBounds); + + if(_dlblSerie.Fill.IsEmpty == false) + { + plotAreaGroup.SetDrawingPropertiesFill(_dlblSerie.Fill, null); + + plotAreaGroup.GroupTransform += $" fill=\"{plotAreaGroup.FillColor}\""; + } + renderItems.Add(plotAreaGroup); for(int i = 0; i< dataLabels.Count; i++) { - if (seriesIcon != null && dataLabels[i].HasLegendKey) - { - dataLabels[i].AddSeriesIcon(seriesIcon); - } + //if (seriesIcon != null && dataLabels[i].HasLegendKey) + //{ + // dataLabels[i].AddSeriesIcon(seriesIcon); + //} //else //{ // //renderItems.Add(groupItems[i]); diff --git a/src/EPPlus.Export.ImageRenderer/RenderItems/SvgItem/SvgTextBox.cs b/src/EPPlus.Export.ImageRenderer/RenderItems/SvgItem/SvgTextBox.cs index e4d54f419..80d553ec7 100644 --- a/src/EPPlus.Export.ImageRenderer/RenderItems/SvgItem/SvgTextBox.cs +++ b/src/EPPlus.Export.ImageRenderer/RenderItems/SvgItem/SvgTextBox.cs @@ -142,10 +142,10 @@ internal override void AppendRenderItems(List renderItems) { var rect = Rectangle; - if(rect.FillColor == null) - { - rect.FillColor = "transparent"; - } + //if (rect.FillColor == null) + //{ + // rect.FillColor = "transparent"; + //} SvgGroupItem groupItem; if (Rotation == 0) From 16938ae373a4688b7ceae6a3ffcecfbb48033876 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ossian=20Edstr=C3=B6m?= Date: Wed, 18 Mar 2026 17:13:36 +0100 Subject: [PATCH 102/151] Removed chunky comments --- .../SvgItem/SvgChartDataLabelStandard.cs | 51 ------------------- 1 file changed, 51 deletions(-) diff --git a/src/EPPlus.Export.ImageRenderer/RenderItems/SvgItem/SvgChartDataLabelStandard.cs b/src/EPPlus.Export.ImageRenderer/RenderItems/SvgItem/SvgChartDataLabelStandard.cs index 3a92dcf32..95ad6e3e3 100644 --- a/src/EPPlus.Export.ImageRenderer/RenderItems/SvgItem/SvgChartDataLabelStandard.cs +++ b/src/EPPlus.Export.ImageRenderer/RenderItems/SvgItem/SvgChartDataLabelStandard.cs @@ -66,44 +66,6 @@ internal void AddSeriesIcon(RenderItem seriesIcon) Bounds.Left += 4d + 2.25d; LeftMargin -= 2.25d + 4d; Bounds.Width += iconWidth + 2.25d; - - //Bounds.Left += TxtBox.LeftMargin; - //Bounds.Width += iconWidth; - //_seriesIcon.Bounds.Left -= 0.75; - //if (iconHeight > TxtBox.Height) - //{ - // Bounds.Height = iconHeight; - //} - //TxtBox.Left += iconWidth + 3; - //Bounds.Width += iconWidth + 3; - - - //if (iconHeight > TxtBox.Rectangle.Bounds.Height) - //{ - // Bounds.Height = iconHeight; - //} - //if (_hasManualLayout == false) - //{ - // TxtBox.Left += iconWidth + TxtBox.LeftMargin; - // if (iconHeight > TxtBox.Height) - // { - // Bounds.Height = iconHeight; - // } - //} - //else - //{ - // Bounds.Left += iconWidth + TxtBox.LeftMargin; - // Bounds.Width += iconWidth; - // Bounds.Height += iconHeight; - //} - - //Bounds.Left -= 3d; - //It seems there is a hard-coded margin in excel of about 4.5pt (6px) in addition to the width of the marker - //TxtBox.Left += 4.5d; - //seriesIcon.Bounds.Left += 4.5d; - //Bounds.Left -= 4.5d; - //Bounds.Left -= 4.5d; - //Bounds.Width += 4.5d; haveAdjustedForIcon = true; } @@ -334,19 +296,6 @@ internal override void AppendRenderItems(List renderItems) var titleItemOrigin = new SvgTitleItem(DrawingRenderer, "DataLabel originpoint"); renderItems.Add(titleItemOrigin); - //SvgRenderRectItem markerRect = new SvgRenderRectItem(DrawingRenderer, _parentPoint); - - //markerRect.Width = _parentPoint.Width; - //markerRect.Height = _parentPoint.Height; - - //markerRect.Top -= _parentPoint.Height / 2; - //markerRect.Left -= _parentPoint.Width / 2; - - //markerRect.FillColor = "blue"; - //markerRect.FillOpacity = 0.2d; - - //renderItems.Add(markerRect); - var group = new SvgGroupItem(ChartRenderer, Bounds); renderItems.Add(group); From 5d1b1843bf84a3c4f53e931c4f141fb1c31db91d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ossian=20Edstr=C3=B6m?= Date: Thu, 19 Mar 2026 10:05:49 +0100 Subject: [PATCH 103/151] Slight cleanup --- .../SvgItem/SvgChartDataLabelStandard.cs | 22 +++++---- .../SvgItem/SvgChartSerieDataLabel.cs | 48 ++----------------- 2 files changed, 17 insertions(+), 53 deletions(-) diff --git a/src/EPPlus.Export.ImageRenderer/RenderItems/SvgItem/SvgChartDataLabelStandard.cs b/src/EPPlus.Export.ImageRenderer/RenderItems/SvgItem/SvgChartDataLabelStandard.cs index 95ad6e3e3..a0849f14f 100644 --- a/src/EPPlus.Export.ImageRenderer/RenderItems/SvgItem/SvgChartDataLabelStandard.cs +++ b/src/EPPlus.Export.ImageRenderer/RenderItems/SvgItem/SvgChartDataLabelStandard.cs @@ -288,6 +288,19 @@ internal void SetParentPoint(BoundingBox parentPoint) } } + private void AppendDebugBounds(List renderItems) + { + SvgRenderRectItem rect = new SvgRenderRectItem(ChartRenderer, Bounds); + rect.Bounds.Left = LeftMargin; + rect.Bounds.Top = 0; + rect.Bounds.Width = Bounds.Width; + rect.Bounds.Height = Bounds.Height; + + rect.FillColor = "red"; + rect.FillOpacity = 0.2; + renderItems.Add(rect); + } + internal override void AppendRenderItems(List renderItems) { var parentPointGroup = new SvgGroupItem(ChartRenderer, _parentPoint); @@ -301,15 +314,8 @@ internal override void AppendRenderItems(List renderItems) var titleItem = new SvgTitleItem(DrawingRenderer, "DataLabel size adjustment"); renderItems.Add(titleItem); - SvgRenderRectItem rect = new SvgRenderRectItem(ChartRenderer, Bounds); - rect.Bounds.Left = LeftMargin; - rect.Bounds.Top = 0; - rect.Bounds.Width = Bounds.Width; - rect.Bounds.Height = Bounds.Height; - rect.FillColor = "red"; - rect.FillOpacity = 0.2; - renderItems.Add(rect); + //AppendDebugBounds(renderItems); TxtBox.AppendRenderItems(renderItems); diff --git a/src/EPPlus.Export.ImageRenderer/RenderItems/SvgItem/SvgChartSerieDataLabel.cs b/src/EPPlus.Export.ImageRenderer/RenderItems/SvgItem/SvgChartSerieDataLabel.cs index c161402d3..2fc7c1bef 100644 --- a/src/EPPlus.Export.ImageRenderer/RenderItems/SvgItem/SvgChartSerieDataLabel.cs +++ b/src/EPPlus.Export.ImageRenderer/RenderItems/SvgItem/SvgChartSerieDataLabel.cs @@ -1,28 +1,9 @@ -using EPPlus.Export.ImageRenderer.RenderItems.Shared; -using EPPlus.Export.ImageRenderer.RenderItems.SvgItem; -using EPPlus.Export.ImageRenderer.Svg.Chart.Util; -using EPPlus.Fonts.OpenType; -using EPPlus.Fonts.OpenType.Tables.Cmap; -using EPPlus.Fonts.OpenType.Utils; -using EPPlus.Graphics; +using EPPlus.Graphics; using EPPlusImageRenderer.RenderItems; using EPPlusImageRenderer.Svg; using OfficeOpenXml.Drawing; using OfficeOpenXml.Drawing.Chart; -using OfficeOpenXml.Drawing.Chart.Style; -using OfficeOpenXml.FormulaParsing.Excel.Functions.MathFunctions; -using OfficeOpenXml.FormulaParsing.Excel.Functions.RefAndLookup; -using OfficeOpenXml.Interfaces.Drawing.Text; -using OfficeOpenXml.Style; -using OfficeOpenXml.Style.XmlAccess; -using OfficeOpenXml.Utils.String; -using OfficeOpenXml.Utils.TypeConversion; -using System; using System.Collections.Generic; -using System.Linq; -using System.Runtime.InteropServices; -using System.Text; - namespace EPPlus.Export.ImageRenderer.RenderItems.SvgItem { @@ -30,36 +11,24 @@ internal class SvgChartSerieDataLabel : DrawingObjectNoBounds { //positioning is handled by parent item via these internal List groupItems = new List(); - - private RenderItem seriesIcon = null; private List dataLabels = new List(); - - string separator; - ExcelTextFont defaultFont; + private RenderItem seriesIcon = null; + private int _serieIndex = -1; ExcelDrawingParagraph defaultParagraph; - BoundingBox plotAreaBounds; - BoundingBox _defaultMargins; - ExcelChartSerieDataLabel _dlblSerie; - int _serieIndex = -1; - bool _hasAddedSeriesIcon = false; - public SvgChartSerieDataLabel(SvgChart chart, ExcelChartSerieDataLabel dlblSerie, BoundingBox maxBounds, ExcelChartStandardSerie serie, List xValues, List yValues, int index) : base(chart) { _serieIndex = index; _dlblSerie = dlblSerie; - - bool addSeriesIcon = false; plotAreaBounds = chart.Plotarea.Rectangle.Bounds; if (dlblSerie.TextBody.Paragraphs.Count != 0) { defaultParagraph = dlblSerie.TextBody.Paragraphs[0]; - defaultFont = dlblSerie.TextBody.Paragraphs[0].DefaultRunProperties; dlblSerie.TextBody.GetInsetsInPoints(out double l, out double top, out double right, out double bottom); _defaultMargins = new BoundingBox(l, top, right, bottom); } @@ -144,18 +113,7 @@ internal override void AppendRenderItems(List renderItems) renderItems.Add(plotAreaGroup); for(int i = 0; i< dataLabels.Count; i++) { - //if (seriesIcon != null && dataLabels[i].HasLegendKey) - //{ - // dataLabels[i].AddSeriesIcon(seriesIcon); - //} - //else - //{ - // //renderItems.Add(groupItems[i]); - //} - dataLabels[i].AppendRenderItems(renderItems); - - //renderItems.Add(new SvgEndGroupItem(DrawingRenderer, Bounds)); } renderItems.Add(new SvgEndGroupItem(DrawingRenderer, plotAreaBounds)); } From 1e8c306a03de23eae1c69650a0e651e6a9130b41 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ossian=20Edstr=C3=B6m?= Date: Thu, 19 Mar 2026 10:14:25 +0100 Subject: [PATCH 104/151] Structured and cleanup --- .../SvgItem/SvgChartDataLabelStandard.cs | 74 +++++++++---------- 1 file changed, 33 insertions(+), 41 deletions(-) diff --git a/src/EPPlus.Export.ImageRenderer/RenderItems/SvgItem/SvgChartDataLabelStandard.cs b/src/EPPlus.Export.ImageRenderer/RenderItems/SvgItem/SvgChartDataLabelStandard.cs index a0849f14f..c7e5bf0d4 100644 --- a/src/EPPlus.Export.ImageRenderer/RenderItems/SvgItem/SvgChartDataLabelStandard.cs +++ b/src/EPPlus.Export.ImageRenderer/RenderItems/SvgItem/SvgChartDataLabelStandard.cs @@ -12,42 +12,35 @@ namespace EPPlus.Export.ImageRenderer.RenderItems.SvgItem { internal class SvgChartDataLabelStandard : SvgChartObject { - internal bool HasLegendKey { get; private set; } = false; - bool _hasManualLayout = false; bool _hasLeaderLines = false; - bool haveAdjustedForIcon = false; - - internal SvgTextBox TxtBox; + bool _haveAdjustedForIcon = false; + bool _renderConnectionPointLines = false; + private SvgTextBox _txtBox; BoundingBox _parentPoint; - - List LeaderLines = new List(); - + List _leaderLines = new List(); Coordinate _manualLayoutOffset = new Coordinate (0, 0); - bool renderConnectionPointLines = false; - PointLines _connectionPointLines; eLabelPosition _labelPosition; - public SvgChartDataLabelStandard(DrawingChart chart, string dataLabelText) : base(chart) - { - var txtBox = new SvgTextBox(chart, chart.Bounds, chart.Bounds); - txtBox.AddText(0, dataLabelText); - } + //public SvgChartDataLabelStandard(DrawingChart chart, string dataLabelText) : base(chart) + //{ + // var txtBox = new SvgTextBox(chart, chart.Bounds, chart.Bounds); + // txtBox.AddText(0, dataLabelText); + //} + + //public SvgChartDataLabelStandard(DrawingChart chart, ExcelChartDataLabelStandard standard, SvgTextBox txtBox) : base(chart) + //{ + // HasLegendKey = standard.ShowLegendKey; + // TxtBox = txtBox; + //} public SvgChartDataLabelStandard(DrawingChart chart, ExcelChartDataLabelStandard standard) : base(chart) { - HasLegendKey = standard.ShowLegendKey; _labelPosition = standard.Position; } - public SvgChartDataLabelStandard(DrawingChart chart, ExcelChartDataLabelStandard standard, SvgTextBox txtBox) : base(chart) - { - HasLegendKey = standard.ShowLegendKey; - TxtBox = txtBox; - } - RenderItem _seriesIcon = null; internal void AddSeriesIcon(RenderItem seriesIcon) @@ -58,16 +51,16 @@ internal void AddSeriesIcon(RenderItem seriesIcon) _seriesIcon = seriesIcon; _seriesIcon.Bounds.Parent = Bounds; - if (haveAdjustedForIcon == false) + if (_haveAdjustedForIcon == false) { - TxtBox.Left += iconWidth; + _txtBox.Left += iconWidth; _seriesIcon.Bounds.Left -= 0.75d; //It seems there is a hard-coded margin in excel of about 4.5pt (6px) Bounds.Left += 4d + 2.25d; LeftMargin -= 2.25d + 4d; Bounds.Width += iconWidth + 2.25d; - haveAdjustedForIcon = true; + _haveAdjustedForIcon = true; } } @@ -139,11 +132,11 @@ internal void ImportDataLabel(SvgChart chart, ExcelChartStandardSerie serie, Exc Bounds.Width = txtBox.Rectangle.Bounds.Width; Bounds.Height = txtBox.Rectangle.Bounds.Height; - TxtBox = txtBox; + _txtBox = txtBox; if (dataLabel.Fill.IsEmpty == false) { - TxtBox.Rectangle.SetDrawingPropertiesFill(dataLabel.Fill, null); + _txtBox.Rectangle.SetDrawingPropertiesFill(dataLabel.Fill, null); } if (dataLabel.Font.IsEmpty == false) { @@ -158,7 +151,7 @@ internal void ImportDataLabel(SvgChart chart, ExcelChartStandardSerie serie, Exc if (individualLabel.Fill.IsEmpty == false) { - TxtBox.Rectangle.FillColor = "#" + individualLabel.Fill.Color.ToColorString(); + _txtBox.Rectangle.FillColor = "#" + individualLabel.Fill.Color.ToColorString(); } if (individualLabel.Layout != null && individualLabel.Layout.HasLayout) @@ -218,17 +211,17 @@ internal void SetParentPoint(BoundingBox parentPoint) case eLabelPosition.Center: break; case eLabelPosition.Left: - Bounds.Left -= TxtBox.Width + (parentPoint.Width / 2); + Bounds.Left -= _txtBox.Width + (parentPoint.Width / 2); break; case eLabelPosition.Right: case eLabelPosition.BestFit: - Bounds.Left += TxtBox.Width / 2 + parentPoint.Width; + Bounds.Left += _txtBox.Width / 2 + parentPoint.Width; break; case eLabelPosition.Top: - Bounds.Top -= (parentPoint.Height + TxtBox.Height) / 2; + Bounds.Top -= (parentPoint.Height + _txtBox.Height) / 2; break; case eLabelPosition.Bottom: - Bounds.Top += (parentPoint.Height + TxtBox.Height) / 2; + Bounds.Top += (parentPoint.Height + _txtBox.Height) / 2; break; default: throw new InvalidOperationException($"The datalabel position {_labelPosition} has not been implemented yet"); @@ -254,7 +247,7 @@ internal void SetParentPoint(BoundingBox parentPoint) //Calculate closest point var index = GetClosestConnectionPointCoordinateIndex(offsetToParentPoint); - LeaderLines.Clear(); + _leaderLines.Clear(); double xOffset = 0; if (index == 0 || index == 2) @@ -273,7 +266,7 @@ internal void SetParentPoint(BoundingBox parentPoint) extraLine.BorderColor = "gray"; extraLine.BorderWidth = 0.5; - LeaderLines.Add(extraLine); + _leaderLines.Add(extraLine); } var mainLine = new SvgRenderLineItem(ChartRenderer, ChartRenderer.Bounds); mainLine.X1 = _connectionPointLines.ConnectionPoints.Points[index].X + xOffset + LeftMargin; @@ -283,7 +276,7 @@ internal void SetParentPoint(BoundingBox parentPoint) mainLine.BorderColor = "gray"; mainLine.BorderWidth = 0.5; - LeaderLines.Add(mainLine); + _leaderLines.Add(mainLine); } } } @@ -317,9 +310,9 @@ internal override void AppendRenderItems(List renderItems) //AppendDebugBounds(renderItems); - TxtBox.AppendRenderItems(renderItems); + _txtBox.AppendRenderItems(renderItems); - if(renderConnectionPointLines) + if(_renderConnectionPointLines) { if (_connectionPointLines != null) { @@ -332,7 +325,7 @@ internal override void AppendRenderItems(List renderItems) var height = Bounds.Height; if (height == 0) { - height = TxtBox.Height; + height = _txtBox.Height; } //Currently series icon always has a y1 y2 of 2 var iconGrp = new SvgGroupItem(ChartRenderer, _seriesIcon.Bounds.Left, height / 2 - 2); @@ -341,10 +334,9 @@ internal override void AppendRenderItems(List renderItems) renderItems.Add(new SvgEndGroupItem(DrawingRenderer, Bounds)); } - - if (LeaderLines != null && LeaderLines.Count > 0) + if (_leaderLines != null && _leaderLines.Count > 0) { - foreach (var line in LeaderLines) + foreach (var line in _leaderLines) { renderItems.Add(line); } From 68ebe24464ada43d10a3a876bb9b55522b9903c8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jan=20K=C3=A4llman?= Date: Thu, 19 Mar 2026 13:21:16 +0100 Subject: [PATCH 105/151] WIP:Work on diagonal and vertical horizontal labels --- .../SvgPathTests.cs | 20 +- .../DrawingBase.cs | 2 + .../DrawingChart.cs | 8 - .../RenderItems/SvgGroupItem.cs | 23 +-- .../RenderItems/SvgItem/SvgTextBodyItem.cs | 3 +- .../RenderItems/SvgItem/SvgTextBox.cs | 20 ++ .../RenderItems/SvgItem/eTextAnchor.cs | 21 ++ .../Svg/Chart/Axis/AxisOptions.cs | 6 +- .../Svg/Chart/Axis/AxisScale.cs | 4 +- .../Svg/Chart/Axis/DateAxisScaleCalculator.cs | 188 ++++++++++++++++- .../Svg/Chart/SvgChartAxis.cs | 195 +++++++++++++++--- .../Svg/Chart/SvgChartObject.cs | 1 - .../Svg/Chart/eTextOrientation.cs | 21 ++ 13 files changed, 447 insertions(+), 65 deletions(-) create mode 100644 src/EPPlus.Export.ImageRenderer/RenderItems/SvgItem/eTextAnchor.cs create mode 100644 src/EPPlus.Export.ImageRenderer/Svg/Chart/eTextOrientation.cs diff --git a/src/EPPlus.Export.ImageRenderer.Test/SvgPathTests.cs b/src/EPPlus.Export.ImageRenderer.Test/SvgPathTests.cs index c94bf362d..96c0af038 100644 --- a/src/EPPlus.Export.ImageRenderer.Test/SvgPathTests.cs +++ b/src/EPPlus.Export.ImageRenderer.Test/SvgPathTests.cs @@ -733,16 +733,16 @@ public void GenerateSvgForCharts_SecondaryAxis() { var ws = p.Workbook.Worksheets[0]; var renderer = new EPPlusImageRenderer.ImageRenderer(); - //var ix = 0; - //var c = ws.Drawings[ix]; - //var svg = renderer.RenderDrawingToSvg(c); - //SaveTextFileToWorkbook($"svg\\ChartForSvg_sheet2_{ix++}.svg", svg); - var ix = 1; - foreach (ExcelChart c in ws.Drawings) - { - var svg = renderer.RenderDrawingToSvg(c); - SaveTextFileToWorkbook($"svg\\ChartForSvg_SecAxis{ix++}.svg", svg); - } + var ix = 3; + var c = ws.Drawings[ix]; + var svg = renderer.RenderDrawingToSvg(c); + SaveTextFileToWorkbook($"svg\\ChartForSvg_sheet2_{ix++}.svg", svg); + //var ix = 1; + //foreach (ExcelChart c in ws.Drawings) + //{ + // var svg = renderer.RenderDrawingToSvg(c); + // SaveTextFileToWorkbook($"svg\\ChartForSvg_SecAxis{ix++}.svg", svg); + //} } } [TestMethod] diff --git a/src/EPPlus.Export.ImageRenderer/DrawingBase.cs b/src/EPPlus.Export.ImageRenderer/DrawingBase.cs index 558f81a88..4c511b715 100644 --- a/src/EPPlus.Export.ImageRenderer/DrawingBase.cs +++ b/src/EPPlus.Export.ImageRenderer/DrawingBase.cs @@ -13,6 +13,7 @@ Date Author Change using EPPlus.Export.ImageRenderer; using EPPlus.Export.ImageRenderer.Utils; +using EPPlus.Fonts.OpenType; using EPPlus.Graphics; using EPPlusImageRenderer.RenderItems; using OfficeOpenXml; @@ -33,6 +34,7 @@ internal DrawingBase(ExcelDrawing drawing) var wb = drawing._drawings.Worksheet.Workbook; Theme = wb.ThemeManager.GetOrCreateTheme(); + TextMeasurer = new FontMeasurerTrueType(); } diff --git a/src/EPPlus.Export.ImageRenderer/DrawingChart.cs b/src/EPPlus.Export.ImageRenderer/DrawingChart.cs index 9f38da419..9908c7db5 100644 --- a/src/EPPlus.Export.ImageRenderer/DrawingChart.cs +++ b/src/EPPlus.Export.ImageRenderer/DrawingChart.cs @@ -11,15 +11,7 @@ Date Author Change 27/11/2025 EPPlus Software AB EPPlus 9 *************************************************************************************************/ -using EPPlus.Export.ImageRenderer.Utils; -using EPPlusImageRenderer.RenderItems; using OfficeOpenXml.Drawing.Chart; -using OfficeOpenXml.FormulaParsing.Excel.Functions.Finance; -using OfficeOpenXml.FormulaParsing.Excel.Functions.MathFunctions; -using OfficeOpenXml.Utils.TypeConversion; -using System; -using System.Collections.Generic; -using System.Linq; namespace EPPlusImageRenderer { diff --git a/src/EPPlus.Export.ImageRenderer/RenderItems/SvgGroupItem.cs b/src/EPPlus.Export.ImageRenderer/RenderItems/SvgGroupItem.cs index c8981cb4e..0b6f047f7 100644 --- a/src/EPPlus.Export.ImageRenderer/RenderItems/SvgGroupItem.cs +++ b/src/EPPlus.Export.ImageRenderer/RenderItems/SvgGroupItem.cs @@ -37,6 +37,8 @@ internal class SvgGroupItem : RenderItem { public override RenderItemType Type => RenderItemType.Group; + public string TextAnchor { get; set; } + public string GroupTransform = ""; //internal SvgGroupItem(DrawingBase renderer, BoundingBox bounds) @@ -69,16 +71,7 @@ internal SvgGroupItem(DrawingBase renderer, BoundingBox parent, double rotation) } if(rotation!=0) { - var cx = parent.Width / 2; - var cy = parent.Height / 2; - if (cx == 0 && cy == 0) - { - rot = $"rotate({rotation.ToString(CultureInfo.InvariantCulture)}))"; - } - else - { - rot = $"rotate({rotation.ToString(CultureInfo.InvariantCulture)}, {cx.ToString(CultureInfo.InvariantCulture)}, {cy.ToString(CultureInfo.InvariantCulture)})"; - } + rot = $"rotate({rotation.ToString(CultureInfo.InvariantCulture)})"; } if(string.IsNullOrEmpty(bounds)==false && string.IsNullOrEmpty(rot)==false) @@ -97,14 +90,16 @@ internal SvgGroupItem(DrawingBase renderer, BoundingBox parent, double rotation) public override void Render(StringBuilder sb) { - if(string.IsNullOrEmpty(GroupTransform)) + sb.Append($""); + sb.Append($" {GroupTransform}"); } - else + if (string.IsNullOrEmpty(TextAnchor) == false) { - sb.Append($""); + sb.Append($" text-anchor=\"{TextAnchor}\""); } + sb.Append(">"); } internal override void GetBounds(out double il, out double it, out double ir, out double ib) diff --git a/src/EPPlus.Export.ImageRenderer/RenderItems/SvgItem/SvgTextBodyItem.cs b/src/EPPlus.Export.ImageRenderer/RenderItems/SvgItem/SvgTextBodyItem.cs index 1d2831d88..98ea09a4a 100644 --- a/src/EPPlus.Export.ImageRenderer/RenderItems/SvgItem/SvgTextBodyItem.cs +++ b/src/EPPlus.Export.ImageRenderer/RenderItems/SvgItem/SvgTextBodyItem.cs @@ -19,7 +19,6 @@ internal class SvgTextBodyItem : TextBodyItem { public SvgTextBodyItem(DrawingBase renderer, BoundingBox parent, bool autoSize, bool clampedToParent = false) : base(renderer, parent, autoSize) { - //Bounds.ClampedToParent = clampedToParent; MaxWidth = parent.Width; MaxHeight = parent.Height; } @@ -31,8 +30,8 @@ public SvgTextBodyItem(DrawingBase renderer, BoundingBox parent, double left, do Bounds.Height = maxHeight; MaxWidth = maxWidth; MaxHeight = maxHeight; - //Bounds.ClampedToParent = clampedToParent; } + internal override List Paragraphs { get; set; } = new List(); internal override void AppendRenderItems(List renderItems) diff --git a/src/EPPlus.Export.ImageRenderer/RenderItems/SvgItem/SvgTextBox.cs b/src/EPPlus.Export.ImageRenderer/RenderItems/SvgItem/SvgTextBox.cs index 8789baca4..c7b9c88b4 100644 --- a/src/EPPlus.Export.ImageRenderer/RenderItems/SvgItem/SvgTextBox.cs +++ b/src/EPPlus.Export.ImageRenderer/RenderItems/SvgItem/SvgTextBox.cs @@ -5,8 +5,10 @@ using EPPlusImageRenderer.Svg; using OfficeOpenXml.Drawing; using OfficeOpenXml.FormulaParsing.Excel.Functions.Text; +using OfficeOpenXml.Utils.EnumUtils; using System; using System.Collections.Generic; +using System.Runtime.Serialization; namespace EPPlus.Export.ImageRenderer.RenderItems.SvgItem @@ -117,6 +119,14 @@ internal double Rotation Rectangle.Bounds.Rotation = value; } } + /// + /// How the text is anchored. + /// + internal eTextAnchor TextAnchor + { + get; + set; + } internal void ImportTextBody(ExcelTextBody body, bool useDefaults = true) { @@ -155,12 +165,22 @@ internal override void AppendRenderItems(List renderItems) { groupItem = new SvgGroupItem(DrawingRenderer, new BoundingBox(Left, Top, Width, Height), Rotation); } + groupItem.TextAnchor = TextAnchor.ToEnumString(); renderItems.Add(groupItem); var textboxGroupItem = new SvgGroupItem(DrawingRenderer); renderItems.Add(textboxGroupItem); var titleItem = new SvgTitleItem(DrawingRenderer, "TextBodySvg Rect"); + //The rect shound encapse the text element, so we need to set the left depending on the text anchor. + if(TextAnchor==eTextAnchor.Middle) + { + rect.Bounds.Left = -(rect.Bounds.Width / 2); + } + else if(TextAnchor==eTextAnchor.End) + { + rect.Bounds.Left = -rect.Bounds.Width; + } renderItems.Add(titleItem); renderItems.Add(rect); diff --git a/src/EPPlus.Export.ImageRenderer/RenderItems/SvgItem/eTextAnchor.cs b/src/EPPlus.Export.ImageRenderer/RenderItems/SvgItem/eTextAnchor.cs new file mode 100644 index 000000000..f1bead4f9 --- /dev/null +++ b/src/EPPlus.Export.ImageRenderer/RenderItems/SvgItem/eTextAnchor.cs @@ -0,0 +1,21 @@ +namespace EPPlus.Export.ImageRenderer.RenderItems.SvgItem +{ + /// + /// Matches the enum for an svg text anchor of a text element. + /// + internal enum eTextAnchor + { + /// + /// Text is anchored to the start of the text element. + /// + Start, + /// + /// Text is anchored in the center of the text element. + /// + Middle, + /// + /// Text is anchored in the end of the text element. + /// + End + } +} \ No newline at end of file diff --git a/src/EPPlus.Export.ImageRenderer/Svg/Chart/Axis/AxisOptions.cs b/src/EPPlus.Export.ImageRenderer/Svg/Chart/Axis/AxisOptions.cs index 04abed062..8ac618a85 100644 --- a/src/EPPlus.Export.ImageRenderer/Svg/Chart/Axis/AxisOptions.cs +++ b/src/EPPlus.Export.ImageRenderer/Svg/Chart/Axis/AxisOptions.cs @@ -1,4 +1,6 @@ -using OfficeOpenXml.Drawing.Chart; +using EPPlusImageRenderer.RenderItems; +using OfficeOpenXml.Drawing.Chart; +using System.Drawing; internal class AxisOptions { @@ -9,4 +11,6 @@ internal class AxisOptions public bool AddPadding { get; set; } = false; public ExcelChartAxisStandard Axis { get; set; } public bool IsStacked100 { get; set; } + public RenderItem ChartSize { get; set; } + public string NumberFormat { get; set; } } \ No newline at end of file diff --git a/src/EPPlus.Export.ImageRenderer/Svg/Chart/Axis/AxisScale.cs b/src/EPPlus.Export.ImageRenderer/Svg/Chart/Axis/AxisScale.cs index bec7c0a1c..a19449ab8 100644 --- a/src/EPPlus.Export.ImageRenderer/Svg/Chart/Axis/AxisScale.cs +++ b/src/EPPlus.Export.ImageRenderer/Svg/Chart/Axis/AxisScale.cs @@ -1,4 +1,5 @@ -using OfficeOpenXml.Drawing.Chart; +using EPPlus.Export.ImageRenderer; +using OfficeOpenXml.Drawing.Chart; internal class AxisScale { @@ -9,4 +10,5 @@ internal class AxisScale public int TickCount { get; set; } public eTimeUnit? MajorDateUnit { get; set; } public eTimeUnit? MinorDateUnit { get; set; } + public eTextOrientation TextOrientation { get; set; } } \ No newline at end of file diff --git a/src/EPPlus.Export.ImageRenderer/Svg/Chart/Axis/DateAxisScaleCalculator.cs b/src/EPPlus.Export.ImageRenderer/Svg/Chart/Axis/DateAxisScaleCalculator.cs index 857b7ef92..83ef7f55f 100644 --- a/src/EPPlus.Export.ImageRenderer/Svg/Chart/Axis/DateAxisScaleCalculator.cs +++ b/src/EPPlus.Export.ImageRenderer/Svg/Chart/Axis/DateAxisScaleCalculator.cs @@ -1,7 +1,12 @@ -using OfficeOpenXml.Drawing.Chart; +using OfficeOpenXml; +using OfficeOpenXml.Drawing.Chart; +using OfficeOpenXml.FormulaParsing.Excel.Functions.Information; +using OfficeOpenXml.FormulaParsing.Excel.Functions.MathFunctions; +using OfficeOpenXml.Interfaces.Drawing.Text; using OfficeOpenXml.Utils.DateUtils; using System; using System.Collections.Generic; +using System.Drawing; using System.Linq; namespace EPPlus.Export.ImageRenderer.Svg.Chart.Util @@ -292,5 +297,186 @@ private static DateTime CeilingToUnit(double value, eTimeUnit unit, int interval return date; }; } + + internal static AxisScale CalculateByWidth(double min, double max, ITextMeasurer tm, AxisOptions options) + { + //var height = options.ChartSize.Bounds.Height; + var ax = options.Axis; + var plotAreaWidth = options.ChartSize.Bounds.Width; + var mf = ax.Font.GetMeasureFont(); + + var interval = 1; //Day + var unit = eTimeUnit.Days; + var minString = DateTime.FromOADate(min).ToString(options.NumberFormat); + var res = tm.MeasureText(minString, mf); + //Get interval for maximum width with vertical text. + while (FitAsVerticalDiagonalText(min, max, interval, unit, res.Height, res.Height*0.3, plotAreaWidth)==false) + { + AddIntervall(ref interval, ref unit); + } + + //Get max text width when using diagonal text + var width = mf.Size * Math.Sqrt(2); + var margin = mf.Size * 0.5; + + if (FitAsVerticalDiagonalText(min, max, interval, unit, width, margin, plotAreaWidth)) //Check diagonal + { + if(FitAsHorizontalText(tm, options, min, max, interval, unit, res.Height, plotAreaWidth)) //Check horizontal + { + return new AxisScale() + { + MajorInterval = interval, + MinorInterval = 1, + MinorDateUnit = unit, + MajorDateUnit = unit, + Min = min, + Max = max, + TextOrientation = eTextOrientation.Horizontal + }; + } + else + { + return new AxisScale() + { + MajorInterval = interval, + MinorInterval = 1, + MinorDateUnit = unit, + MajorDateUnit = unit, + Min = min, + Max = max, + TextOrientation = eTextOrientation.Diagonal + }; + } + } + + return new AxisScale() + { + MajorInterval = interval, + MinorInterval = 1, + MinorDateUnit = unit, + MajorDateUnit = unit, + Min = min, + Max = max, + TextOrientation = eTextOrientation.Vertical + }; + + } + + //private static bool FitAsDiagonalText(double min, double max, int interval, eTimeUnit unit, float size, double plotAreaWidth) + //{ + // var margin = size * 0.5; + // var width = size * Math.Sqrt(2) + margin; + + // if (unit == eTimeUnit.Days) + // { + // return ((max - min) / interval) * width < plotAreaWidth; + // } + // else if(unit == eTimeUnit.Months) + // { + // w + // } + //} + + private static void AddIntervall(ref int interval, ref eTimeUnit unit) + { + if (unit == eTimeUnit.Days) + { + switch (interval) + { + case 1: + interval = 2; + break; + case 2: + interval = 7; //Week + break; + case 7: + interval = 1; //Month + unit = eTimeUnit.Months; + break; + default: + interval *= 10; + break; + } + } + else if (unit == eTimeUnit.Months) + { + unit = eTimeUnit.Years; + } + else if (unit == eTimeUnit.Years) + { + interval++; + } + } + + private static bool FitAsHorizontalText(ITextMeasurer tm, AxisOptions options, double min, double max, int interval, eTimeUnit unit, float height, double width) + { + var minDate = DateTime.FromOADate(min); + var maxDate = DateTime.FromOADate(max); + var date = minDate; + var horizontalWidth = 0D; + var nf = options.NumberFormat; + var mf = options.Axis.Font.GetMeasureFont(); + var angMult = Math.Sin(MathHelper.Radians(45)); + while (date < maxDate) + { + var textWidth = tm.MeasureText(date.ToString(), mf).Width; + horizontalWidth += textWidth; + if(horizontalWidth > width) + { + return false; + } + if (unit == eTimeUnit.Days) + { + date = date.AddDays(interval); + } + else if(unit == eTimeUnit.Months) + { + date = minDate.AddDays(1).AddMonths(interval).AddDays(-1); + } + else + { + date = minDate.AddDays(1).AddYears(interval).AddDays(-1); + } + } + return true; + } + + private static bool FitAsVerticalDiagonalText(double min, double max, int interval,eTimeUnit unit, double textWidth, double margin, double plotAreaWidth) + { + if (unit == eTimeUnit.Days) + { + var items = ((max - min) / interval); + return items * textWidth + (items-1) * margin < plotAreaWidth; + } + else if(unit== eTimeUnit.Months) + { + var minDate = DateTime.FromOADate(min); + var maxDate = DateTime.FromOADate(max); + var date = minDate.AddDays(1).AddMonths(1).AddDays(-1); //Handles last day of month. + var items = 1; + while(date >= maxDate) + { + items++; + if (items * textWidth + (items - 1) * margin > plotAreaWidth) return false; + date = minDate.AddDays(1).AddMonths(1).AddDays(-1); + } + return items * textWidth + (items - 1) * margin > plotAreaWidth; + } + else + { + var minDate = DateTime.FromOADate(min); + var maxDate = DateTime.FromOADate(max); + var date = minDate.AddDays(1).AddYears(1).AddDays(-1); //Handles leap year + var items = 1; + while (date >= maxDate) + { + items++; + if (items * textWidth + (items - 1) * margin > plotAreaWidth) return false; + date = minDate.AddDays(1).AddYears(1).AddDays(-1); + } + return items * textWidth + (items - 1) * margin > plotAreaWidth; + + } + } } } \ No newline at end of file diff --git a/src/EPPlus.Export.ImageRenderer/Svg/Chart/SvgChartAxis.cs b/src/EPPlus.Export.ImageRenderer/Svg/Chart/SvgChartAxis.cs index cee103635..b15faee77 100644 --- a/src/EPPlus.Export.ImageRenderer/Svg/Chart/SvgChartAxis.cs +++ b/src/EPPlus.Export.ImageRenderer/Svg/Chart/SvgChartAxis.cs @@ -10,32 +10,29 @@ Date Author Change ************************************************************************************************* 27/11/2025 EPPlus Software AB EPPlus 9 *************************************************************************************************/ -using EPPlus.Export.ImageRenderer.RenderItems.Shared; +using EPPlus.Export.ImageRenderer; using EPPlus.Export.ImageRenderer.RenderItems.SvgItem; using EPPlus.Export.ImageRenderer.Svg.Chart.Util; -using EPPlus.Fonts.OpenType; -using EPPlus.Fonts.OpenType.Tables.Cmap; using EPPlus.Fonts.OpenType.Utils; -using EPPlus.Graphics; using EPPlusImageRenderer.RenderItems; using OfficeOpenXml.Drawing; using OfficeOpenXml.Drawing.Chart; using OfficeOpenXml.Drawing.Chart.Style; +using OfficeOpenXml.FormulaParsing.Excel.Functions.Logical; +using OfficeOpenXml.FormulaParsing.Excel.Functions.MathFunctions; using OfficeOpenXml.Style; using OfficeOpenXml.Style.XmlAccess; using OfficeOpenXml.Utils.String; using OfficeOpenXml.Utils.TypeConversion; using System; using System.Collections.Generic; -using System.Globalization; using System.Linq; -using System.Linq.Expressions; -using System.Text; namespace EPPlusImageRenderer.Svg { internal class SvgChartAxis : SvgChartObject, IDrawingChartAxis { + private const double COS45 = 0.70710678118654757; //Constant for Math.Sin(Math.PI / 4) --45 degrees internal SvgChartAxis(SvgChart sc, ExcelChartAxisStandard ax) : base(sc) { SvgChart = sc; @@ -58,7 +55,7 @@ internal SvgChartAxis(SvgChart sc, ExcelChartAxisStandard ax) : base(sc) if (ax.Deleted == false || ax.HasMajorGridlines || ax.HasMinorGridlines) { - Values = GetAxisValue(ax, Rectangle, out double? min, out double? max, out double? majorUnit, out eTimeUnit? dateUnit); + Values = GetAxisValue(ax, sc.ChartArea.Rectangle, out double? min, out double? max, out double? majorUnit, out eTimeUnit? dateUnit, out eTextOrientation orientation); AxisValues = GetAxisDisplayValues(ax, Values, min, max, majorUnit); Min = min ?? 0D; @@ -66,6 +63,7 @@ internal SvgChartAxis(SvgChart sc, ExcelChartAxisStandard ax) : base(sc) MajorUnit = majorUnit ?? 1; MinorUnit = ax.MinorUnit ?? GetAutoMinUnit(MajorUnit); MajorDateUnit = dateUnit; + LabelOrientation = orientation; if (ax.Deleted == false) { if (ax.Layout.HasLayout) @@ -125,6 +123,7 @@ private double GetAutoMinUnit(double majorUnit) internal ExcelChartAxis Axis { get; } internal SvgChart SvgChart { get; } + internal SvgRenderLineItem Line { get; set; } private List GetAxisDisplayValues(ExcelChartAxisStandard ax, List values, double? min, double? max, double? majorUnit) { var displayValues = new List(); @@ -147,15 +146,33 @@ private List GetAxisDisplayValues(ExcelChartAxisStandard ax, List highest) + switch (LabelOrientation) { - highest = m.Height; + case eTextOrientation.Horizontal: + if (m.Height > highest) + { + highest = m.Height; + } + break; + case eTextOrientation.Diagonal: + var width = (m.Width + m.Height) * COS45; + if (width > highest) + { + highest = width; + } + break; + case eTextOrientation.Vertical: + if (m.Width > highest) + { + highest = m.Width; + } + break; } } return highest.PointToPixel(); @@ -201,6 +218,7 @@ public List AxisValuesTextBoxes public double MajorUnit { get; set; } public double MinorUnit { get; set; } public eTimeUnit? MajorDateUnit { get; set; } + public eTextOrientation LabelOrientation { get; set; } internal override void AppendRenderItems(List renderItems) { Title?.AppendRenderItems(renderItems); @@ -300,19 +318,117 @@ private List GetAxisValueTextBoxes() } else { - maxWidth = Rectangle.Width / AxisValues.Count; - maxHeight = SvgChart.ChartArea.Rectangle.Height / 3; //TODO: Check this value. + switch (LabelOrientation) + { + case eTextOrientation.Vertical: + maxWidth = (Rectangle.Height / AxisValues.Count) * 1; + maxHeight = SvgChart.ChartArea.Rectangle.Height / 3; //TODO: Check this value. + break; + case eTextOrientation.Diagonal: + maxWidth = (Rectangle.Width / AxisValues.Count) * 1 / COS45; + maxHeight = SvgChart.ChartArea.Rectangle.Height / 3; //TODO: Check this value. + break; + default: + maxWidth = Rectangle.Width / AxisValues.Count; + maxHeight = SvgChart.ChartArea.Rectangle.Height / 3; //TODO: Check this value. + break; + } } double widest=0; for (var i = 0; i < AxisValues.Count; i++) { + var v = AxisValues[i]; var m = tm.MeasureText(v, mf); - var x = GetAxisItemLeft(i, m); - var y = GetAxisItemTop(i, m); - var width = m.Width.PointToPixel(); - var height = m.Height.PointToPixel(); + var ticMarkX = GetAxisItemLeft(i, m); + var ticMarkY = GetAxisItemTop(i, m); + var width = m.Width; + var height = m.Height; + double x, y; + if(LabelOrientation==eTextOrientation.Horizontal) + { + if (Axis.AxisType == eAxisType.Cat) + { + x = ticMarkX; + y = ticMarkY; + } + else + { + if(Axis.IsVertical) + { + x = ticMarkX; + y = ticMarkY - height / 2; + } + else + { + x = ticMarkX - width / 2; + y = ticMarkY; + } + } + } + else + { + var rot = LabelOrientation == eTextOrientation.Diagonal ? -45 : -90; + double cos = Math.Cos(MathHelper.Radians(rot)); + double sin = Math.Sin(MathHelper.Radians(rot)); + + // Right-center of the rect in local space + double anchorX, anchorY; + if(LabelOrientation==eTextOrientation.Diagonal) + { + if (Axis.AxisPosition == eAxisPosition.Bottom) + { + anchorX = width / 2; + anchorY = height / 2; + } + else + { + anchorX = 0; + anchorY = height / 2; + } + } + else + { + anchorX = width / 2; + anchorY = height / 2; + + } + + double rotX = anchorX * cos - anchorY * sin; + double rotY = anchorX * sin + anchorY * cos; + + var majorWidth = Rectangle.Width / AxisValues.Count; + + if(Axis.AxisPosition == eAxisPosition.Bottom) + { + x = ticMarkX - rotX + 6 + majorWidth / 2; + y = ticMarkY - 6 - rotY; + } + else + { + x = ticMarkX - rotX + 6; + y = ticMarkY + 6 - rotY; + } + } + var tb = new SvgTextBox(SvgChart, Rectangle.Bounds, x, y, width, height, maxWidth, maxHeight); + if (LabelOrientation == eTextOrientation.Diagonal) + { + tb.Rotation = -45; + if(Axis.AxisPosition==eAxisPosition.Bottom) + { + tb.TextAnchor = eTextAnchor.End; + } + } + + else if (LabelOrientation == eTextOrientation.Vertical) + { + tb.Rotation = 90; + if (Axis.AxisPosition == eAxisPosition.Bottom) + { + tb.TextAnchor = eTextAnchor.End; + } + } var p = Axis.TextBody.Paragraphs.FirstOrDefault(); @@ -399,9 +515,11 @@ private double GetAxisItemLeft(int i, OfficeOpenXml.Interfaces.Drawing.Text.Text } else { - var majorWidth = Rectangle.Width / AxisValues.Count; - - return Rectangle.Left + majorWidth * i - m.Width.PointToPixel() / 2; + var min = ConvertUtil.GetValueDouble(Values[0]); + var max = ConvertUtil.GetValueDouble(Values.Last()); + var v = ConvertUtil.GetValueDouble(Values[i]); + var majorWidth = Rectangle.Width * (v-Min)/(Max-Min); + return Rectangle.Left + majorWidth; } } } @@ -414,7 +532,15 @@ private double GetAxisItemTop(int i, OfficeOpenXml.Interfaces.Drawing.Text.TextM } else if (Axis.AxisPosition == eAxisPosition.Bottom) { - return Rectangle.Bottom - m.Height + BottomMargin; + switch(LabelOrientation) + { + case eTextOrientation.Vertical: + return Rectangle.Top - m.Width + BottomMargin; + case eTextOrientation.Diagonal: + return Rectangle.Top - BottomMargin; + default: + return Rectangle.Top - m.Height + BottomMargin; + } } else { @@ -425,7 +551,8 @@ private double GetAxisItemTop(int i, OfficeOpenXml.Interfaces.Drawing.Text.TextM } else { - return Rectangle.Top + majorHeight * (AxisValues.Count - i - 1) - m.Height / 2; + //return Rectangle.Top + majorHeight * (AxisValues.Count - i - 1) - m.Height / 2; + return Rectangle.Top + majorHeight * (AxisValues.Count - i - 1); } } @@ -643,7 +770,7 @@ internal double GetPositionInPlotarea(double val) } } } - protected List GetAxisValue(ExcelChartAxisStandard ax, RenderItem rect, out double? min, out double? max, out double? majorUnit, out eTimeUnit? dateUnit) + protected List GetAxisValue(ExcelChartAxisStandard ax, RenderItem rect, out double? min, out double? max, out double? majorUnit, out eTimeUnit? dateUnit, out eTextOrientation orientation) { var values = ax.GetAxisValues(out bool isCount); if (ax.AxisType == eAxisType.Cat && @@ -653,8 +780,10 @@ protected List GetAxisValue(ExcelChartAxisStandard ax, RenderItem rect, max = values.Length; majorUnit = 1; dateUnit = null; + orientation = eTextOrientation.Horizontal; return values.ToList(); } + var l = new List(); min = double.MaxValue; max = double.MinValue; @@ -682,7 +811,8 @@ protected List GetAxisValue(ExcelChartAxisStandard ax, RenderItem rect, LockedIntervalUnit = ax.MajorTimeUnit, AddPadding = ax.AxisPosition == eAxisPosition.Left || ax.AxisPosition == eAxisPosition.Right, Axis = ax, - IsStacked100 = Chart.IsTypePercentStacked() + IsStacked100 = Chart.IsTypePercentStacked(), + ChartSize = rect }; var length = ax.AxisPosition == eAxisPosition.Left || ax.AxisPosition == eAxisPosition.Right ? SvgChart.Bounds.Height : SvgChart.Bounds.Width; //Fix and use plotarea width/height. @@ -694,11 +824,21 @@ protected List GetAxisValue(ExcelChartAxisStandard ax, RenderItem rect, { l.Add(i); } + orientation = eTextOrientation.Horizontal; return l; } if (ax.IsDate) { - var res = DateAxisScaleCalculator.Calculate(min ?? 0, max ?? 0, length, options); + AxisScale res; + if (ax.IsVertical) + { + res = DateAxisScaleCalculator.Calculate(min ?? 0, max ?? 0, length, options); + } + else + { + res = DateAxisScaleCalculator.CalculateByWidth(min ?? 0D, max ?? 0D, SvgChart.TextMeasurer, options); + } + orientation = res.TextOrientation; dateUnit = res.MajorDateUnit; var dt = DateTime.FromOADate(res.Min); var maxDt = DateTime.FromOADate(res.Max); @@ -734,7 +874,8 @@ protected List GetAxisValue(ExcelChartAxisStandard ax, RenderItem rect, min = res.Min; max = res.Max; majorUnit = res.MajorInterval; - dateUnit= null; + dateUnit= null; + orientation = eTextOrientation.Horizontal; } return l; diff --git a/src/EPPlus.Export.ImageRenderer/Svg/Chart/SvgChartObject.cs b/src/EPPlus.Export.ImageRenderer/Svg/Chart/SvgChartObject.cs index d11d1128c..93d59390d 100644 --- a/src/EPPlus.Export.ImageRenderer/Svg/Chart/SvgChartObject.cs +++ b/src/EPPlus.Export.ImageRenderer/Svg/Chart/SvgChartObject.cs @@ -79,7 +79,6 @@ internal void SetMargins(ExcelTextBody tb) internal double TopMargin { get; set; } internal double BottomMargin { get; set; } internal SvgRenderRectItem Rectangle { get; set; } - internal SvgRenderLineItem Line { get; set; } protected static SvgRenderRectItem GetRectFromManualLayout(SvgChart sc, ExcelLayout layout) { var rect = new SvgRenderRectItem(sc, sc.ChartArea.Bounds); diff --git a/src/EPPlus.Export.ImageRenderer/Svg/Chart/eTextOrientation.cs b/src/EPPlus.Export.ImageRenderer/Svg/Chart/eTextOrientation.cs new file mode 100644 index 000000000..232bae630 --- /dev/null +++ b/src/EPPlus.Export.ImageRenderer/Svg/Chart/eTextOrientation.cs @@ -0,0 +1,21 @@ +/************************************************************************************************* + Required Notice: Copyright (C) EPPlus Software AB. + This software is licensed under PolyForm Noncommercial License 1.0.0 + and may only be used for noncommercial purposes + https://polyformproject.org/licenses/noncommercial/1.0.0/ + + A commercial license to use this software can be purchased at https://epplussoftware.com + ************************************************************************************************* + Date Author Change + ************************************************************************************************* + 27/11/2025 EPPlus Software AB EPPlus 9 + *************************************************************************************************/ +namespace EPPlus.Export.ImageRenderer +{ + internal enum eTextOrientation + { + Horizontal, + Diagonal, + Vertical, + } +} \ No newline at end of file From f5ec65c19b021cd58d03f2637fbd42799d141059 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jan=20K=C3=A4llman?= Date: Thu, 19 Mar 2026 16:11:59 +0100 Subject: [PATCH 106/151] WIP:Fixed diagnal axis label placement. --- .../Svg/Chart/SvgChartAxis.cs | 43 ++++++------------- 1 file changed, 12 insertions(+), 31 deletions(-) diff --git a/src/EPPlus.Export.ImageRenderer/Svg/Chart/SvgChartAxis.cs b/src/EPPlus.Export.ImageRenderer/Svg/Chart/SvgChartAxis.cs index b15faee77..77c22d87e 100644 --- a/src/EPPlus.Export.ImageRenderer/Svg/Chart/SvgChartAxis.cs +++ b/src/EPPlus.Export.ImageRenderer/Svg/Chart/SvgChartAxis.cs @@ -321,11 +321,11 @@ private List GetAxisValueTextBoxes() switch (LabelOrientation) { case eTextOrientation.Vertical: - maxWidth = (Rectangle.Height / AxisValues.Count) * 1; - maxHeight = SvgChart.ChartArea.Rectangle.Height / 3; //TODO: Check this value. + maxWidth = SvgChart.ChartArea.Rectangle.Height / 3; + maxHeight = Rectangle.Width / AxisValues.Count; //TODO: Check this value. break; case eTextOrientation.Diagonal: - maxWidth = (Rectangle.Width / AxisValues.Count) * 1 / COS45; + maxWidth = (Rectangle.Width + Rectangle.Height) / COS45; maxHeight = SvgChart.ChartArea.Rectangle.Height / 3; //TODO: Check this value. break; default: @@ -372,42 +372,23 @@ private List GetAxisValueTextBoxes() double cos = Math.Cos(MathHelper.Radians(rot)); double sin = Math.Sin(MathHelper.Radians(rot)); - // Right-center of the rect in local space - double anchorX, anchorY; - if(LabelOrientation==eTextOrientation.Diagonal) + if (LabelOrientation == eTextOrientation.Diagonal) { if (Axis.AxisPosition == eAxisPosition.Bottom) { - anchorX = width / 2; - anchorY = height / 2; + x = ticMarkX - (height / 2) * cos; + y = ticMarkY + 4 + (height / 2) * cos; } - else + else //Top { - anchorX = 0; - anchorY = height / 2; + x = ticMarkX - (height / 2) * cos; + y = ticMarkY - 4 + (height / 2) * cos; } } else { - anchorX = width / 2; - anchorY = height / 2; - - } - - double rotX = anchorX * cos - anchorY * sin; - double rotY = anchorX * sin + anchorY * cos; - - var majorWidth = Rectangle.Width / AxisValues.Count; - - if(Axis.AxisPosition == eAxisPosition.Bottom) - { - x = ticMarkX - rotX + 6 + majorWidth / 2; - y = ticMarkY - 6 - rotY; - } - else - { - x = ticMarkX - rotX + 6; - y = ticMarkY + 6 - rotY; + x = ticMarkX - (height / 2); + y = ticMarkY + 4 + (height / 2); } } @@ -469,7 +450,7 @@ private List GetAxisValueTextBoxes() } } } - else + else if(LabelOrientation==eTextOrientation.Horizontal) //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; From 199035041e2bae6d6fa980d3f23143f9b0f8f017 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ossian=20Edstr=C3=B6m?= Date: Thu, 19 Mar 2026 17:36:55 +0100 Subject: [PATCH 107/151] Wip features --- .../ImageRenderer.cs | 41 +++++ .../RenderItems/Shared/ContainerItem.cs | 150 ++++++++++++++++++ .../RenderItems/Shared/FlexBaseContainer.cs | 18 +++ .../RenderItems/SvgItem/SvgContainerItem.cs | 34 ++++ .../Svg/Chart/SvgChart.cs | 1 - .../Svg/Chart/SvgChartLegend.cs | 12 +- .../Svg/Chart/SvgChartLegendIcon.cs | 117 ++++++++++++++ .../Svg/DrawingItemForTesting.cs | 2 +- .../Svg/SvgLegendSerie.cs | 7 +- .../Svg/SvgLegendSeriesIcon.cs | 15 ++ src/EPPlus.Graphics/BoundingBoxContainer.cs | 112 +++++++++++++ .../Drawing/Chart/ChartTitleTests.cs | 11 -- 12 files changed, 496 insertions(+), 24 deletions(-) create mode 100644 src/EPPlus.Export.ImageRenderer/RenderItems/Shared/ContainerItem.cs create mode 100644 src/EPPlus.Export.ImageRenderer/RenderItems/Shared/FlexBaseContainer.cs create mode 100644 src/EPPlus.Export.ImageRenderer/RenderItems/SvgItem/SvgContainerItem.cs create mode 100644 src/EPPlus.Export.ImageRenderer/Svg/Chart/SvgChartLegendIcon.cs create mode 100644 src/EPPlus.Export.ImageRenderer/Svg/SvgLegendSeriesIcon.cs create mode 100644 src/EPPlus.Graphics/BoundingBoxContainer.cs diff --git a/src/EPPlus.Export.ImageRenderer/ImageRenderer.cs b/src/EPPlus.Export.ImageRenderer/ImageRenderer.cs index 031fd7e35..e5e988653 100644 --- a/src/EPPlus.Export.ImageRenderer/ImageRenderer.cs +++ b/src/EPPlus.Export.ImageRenderer/ImageRenderer.cs @@ -11,9 +11,12 @@ Date Author Change 27/11/2025 EPPlus Software AB EPPlus 9 *************************************************************************************************/ +using EPPlus.Export.ImageRenderer.RenderItems.SvgItem; +using EPPlus.Export.ImageRenderer.Svg; using EPPlus.Export.ImageRenderer.Svg.NodeAttributes; using EPPlus.Export.ImageRenderer.Svg.Writer; using EPPlus.Graphics; +using EPPlusImageRenderer.RenderItems; using EPPlusImageRenderer.Svg; using OfficeOpenXml.Drawing; using OfficeOpenXml.Drawing.Chart; @@ -45,6 +48,44 @@ public string RenderDrawingToSvg(ExcelDrawing drawing) throw new NotImplementedException("Image rendering for drawing type not implemented."); } + public string RenderTest() + { + var baseBB = new BoundingBox(); + + baseBB.Width = 400; + baseBB.Height = 400; + + var baseItem = new DrawingItemForTesting(baseBB); + + //SvgRenderRectItem myBgItem = new SvgRenderRectItem(baseItem, baseItem.Bounds); + //myBgItem.FillColor = "purple"; + //myBgItem.FillOpacity = 0.2d; + + //SvgRenderRectItem myInnerItem = new SvgRenderRectItem(baseItem, myBgItem.Bounds); + + //myInnerItem.FillColor = "green"; + //myInnerItem.FillOpacity = 0.8d; + + //myInnerItem.Width = 50; + //myInnerItem.Height = 50; + + //var container = new SvgContainerItem(myInnerItem, myBgItem); + + //container.MarginLeft = 5; + //container.MarginRight = 5; + //container.MarginTop = 5; + //container.MarginBottom = 5; + + //container.ApplyMargins(); + + //baseItem.RenderItems.Add(container); + + var sb = new StringBuilder(); + + baseItem.Render(sb); + + return sb.ToString(); + } //public string RenderTestCanvas(double widthPixel, double heightPixel, Color bgColor) //{ diff --git a/src/EPPlus.Export.ImageRenderer/RenderItems/Shared/ContainerItem.cs b/src/EPPlus.Export.ImageRenderer/RenderItems/Shared/ContainerItem.cs new file mode 100644 index 000000000..21003f355 --- /dev/null +++ b/src/EPPlus.Export.ImageRenderer/RenderItems/Shared/ContainerItem.cs @@ -0,0 +1,150 @@ +using EPPlus.Graphics; +using EPPlusImageRenderer; +using EPPlusImageRenderer.RenderItems; +using OfficeOpenXml.FormulaParsing.Excel.Functions.Text; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; + +namespace EPPlus.Export.ImageRenderer.RenderItems.Shared +{ + internal abstract class ContainerItem : RenderItem + { + private RenderItem InnerItem; + private RenderItem OuterItem; + + double _marginLeft; + double _marginTop; + + internal double MarginLeft + { + get + { + return _marginLeft; + } + set + { + VerifyMarginInput(value); + _marginLeft = value; + //InnerItem.Bounds.Left = value; + } + } + + internal double MarginTop + { + get + { + return _marginTop; + } + set + { + VerifyMarginInput(value); + _marginTop = value; + //InnerItem.Bounds.Top = value; + } + } + + double _marginRight; + + internal double MarginRight + { + get + { + return _marginRight; + } + set + { + VerifyMarginInput(value); + _marginRight = value; + //InnerItem.Bounds.Width = Bounds.Width - value; + } + } + + double _marginBottom; + + internal double MarginBottom + { + get + { + return _marginBottom; + } + set + { + VerifyMarginInput(value); + _marginBottom = value; + } + } + + bool SizeToContents = true; + public ContainerItem(RenderItem innerItem, RenderItem outerItem) : base(innerItem.DrawingRenderer) + { + OuterItem = outerItem; + InnerItem = innerItem; + + OuterItem.Bounds.Parent = Bounds; + InnerItem.Bounds.Parent = OuterItem.Bounds; + } + + internal void ApplyMargins() + { + //Origin-Point: Set. Set in stone. All text moves from there + //Here it is Bounds.Top and Bounds.Left. Nothing in the content can change that + + OuterItem.Bounds.Top = MarginLeft; + OuterItem.Bounds.Left = MarginTop; + + OuterItem.Bounds.Width = InnerItem.Bounds.Width + MarginRight; + OuterItem.Bounds.Height = InnerItem.Bounds.Height + MarginBottom; + + Bounds.Width = OuterItem.Bounds.Width; + Bounds.Height = OuterItem.Bounds.Height; + } + + public override void Render(StringBuilder sb) + { + OuterItem.Render(sb); + InnerItem.Render(sb); + } + + public override RenderItemType Type => RenderItemType.Group; + + internal double GetInnerLeft() + { + return Bounds.Left + MarginLeft; + } + + internal double GetInnerTop() + { + return Bounds.Top + MarginTop; + } + + internal double GetInnerBottom() + { + return Bounds.Bottom - MarginBottom; + } + + internal double GetInnerRight() + { + return Bounds.Right - MarginRight; + } + + public double GetInnerWidth() + { + return GetInnerRight() - GetInnerLeft(); + } + + public double GetInnerHeight() + { + return GetInnerBottom() - GetInnerTop(); + } + + private void VerifyMarginInput(double value) + { + if (value < 0) + { + throw new ArgumentException("Margins cannot be set to a negative value! If you wish to set starting position, please set Left or Top for the ContainerItem"); + } + } + } +} diff --git a/src/EPPlus.Export.ImageRenderer/RenderItems/Shared/FlexBaseContainer.cs b/src/EPPlus.Export.ImageRenderer/RenderItems/Shared/FlexBaseContainer.cs new file mode 100644 index 000000000..532679f70 --- /dev/null +++ b/src/EPPlus.Export.ImageRenderer/RenderItems/Shared/FlexBaseContainer.cs @@ -0,0 +1,18 @@ +using EPPlusImageRenderer.RenderItems; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; + +namespace EPPlus.Export.ImageRenderer.RenderItems.Shared +{ + //internal class FlexBaseContainer : RenderItem + //{ + // public override RenderItemType Type => throw new NotImplementedException(); + + // public override void Render(StringBuilder sb) + // { + // throw new NotImplementedException(); + // } + //} +} diff --git a/src/EPPlus.Export.ImageRenderer/RenderItems/SvgItem/SvgContainerItem.cs b/src/EPPlus.Export.ImageRenderer/RenderItems/SvgItem/SvgContainerItem.cs new file mode 100644 index 000000000..01e411351 --- /dev/null +++ b/src/EPPlus.Export.ImageRenderer/RenderItems/SvgItem/SvgContainerItem.cs @@ -0,0 +1,34 @@ +using EPPlus.Export.ImageRenderer.RenderItems.Shared; +using EPPlusImageRenderer.RenderItems; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; + +namespace EPPlus.Export.ImageRenderer.RenderItems.SvgItem +{ + internal class SvgContainerItem : ContainerItem + { + public SvgContainerItem(RenderItem innerItem, RenderItem outerItem) : base(innerItem, outerItem) + { + } + + public override void Render(StringBuilder sb) + { + var grpItem = new SvgGroupItem(DrawingRenderer, Bounds.Left, Bounds.Top); + grpItem.Render(sb); + + var grpItem2 = new SvgGroupItem(DrawingRenderer, MarginLeft, MarginTop); + grpItem2.Render(sb); + + base.Render(sb); + + var endGroupItem2 = new SvgEndGroupItem(DrawingRenderer, Bounds); + endGroupItem2.Render(sb); + + + var endGroupItem = new SvgEndGroupItem(DrawingRenderer, Bounds); + endGroupItem.Render(sb); + } + } +} diff --git a/src/EPPlus.Export.ImageRenderer/Svg/Chart/SvgChart.cs b/src/EPPlus.Export.ImageRenderer/Svg/Chart/SvgChart.cs index e1509d656..facd3576d 100644 --- a/src/EPPlus.Export.ImageRenderer/Svg/Chart/SvgChart.cs +++ b/src/EPPlus.Export.ImageRenderer/Svg/Chart/SvgChart.cs @@ -274,7 +274,6 @@ internal double GetPlotAreaTop() } - internal SvgRenderLineItem GetSeriesIcon(ExcelChartStandardSerie s, int index, BoundingBox parentItem) { const float MarginExtra = 1.5f; diff --git a/src/EPPlus.Export.ImageRenderer/Svg/Chart/SvgChartLegend.cs b/src/EPPlus.Export.ImageRenderer/Svg/Chart/SvgChartLegend.cs index cad69e96a..c41ad5622 100644 --- a/src/EPPlus.Export.ImageRenderer/Svg/Chart/SvgChartLegend.cs +++ b/src/EPPlus.Export.ImageRenderer/Svg/Chart/SvgChartLegend.cs @@ -31,7 +31,7 @@ namespace EPPlusImageRenderer.Svg internal class SvgChartLegend : SvgChartObject { - List _seriesHeadersMeasure =new List(); + List _seriesHeadersMeasure = new List(); ITextMeasurer _ttMeasurer; const float MarginExtra = 1.5f; const float MiddleMargin = 7.5f; @@ -197,7 +197,8 @@ internal void SetLegend(SvgChart sc) case eChartType.LineStacked100: var ls=(ExcelLineChartSerie)s; var tm = _seriesHeadersMeasure[index]; - var si = GetSeriesIcon(sc, ls, index, tm, pSls); + var prevTm = _seriesHeadersMeasure[index - 1]; + var si = GetSeriesIcon(sc, ls, prevTm, tm, pSls); sls.SeriesIcon = si; var tbLeft = si.X2 + MarginExtra; @@ -256,11 +257,11 @@ internal void SetLegend(SvgChart sc) } } - private SvgRenderLineItem GetSeriesIcon(SvgChart sc, ExcelLineChartSerie ls, int index, TextMeasurement tm, SvgLegendSerie pSls) + private SvgRenderLineItem GetSeriesIcon(SvgChart sc, ExcelChartStandardSerie cStandardSerie, TextMeasurement pTm, TextMeasurement tm, SvgLegendSerie pSls) { var item = new SvgRenderLineItem(sc, Rectangle.Bounds); - item.SetDrawingPropertiesFill(ls.Fill, sc.Chart.StyleManager.Style.SeriesLine.FillReference.Color); - item.SetDrawingPropertiesBorder(ls.Border, sc.Chart.StyleManager.Style.SeriesLine.BorderReference.Color, ls.Border.Fill.Style!=eFillStyle.NoFill, 0.75); + item.SetDrawingPropertiesFill(cStandardSerie.Fill, sc.Chart.StyleManager.Style.SeriesLine.FillReference.Color); + item.SetDrawingPropertiesBorder(cStandardSerie.Border, sc.Chart.StyleManager.Style.SeriesLine.BorderReference.Color, cStandardSerie.Border.Fill.Style!=eFillStyle.NoFill, 0.75); if (sc.Chart.Legend.Position == eLegendPosition.Top || sc.Chart.Legend.Position == eLegendPosition.Bottom) @@ -291,7 +292,6 @@ private SvgRenderLineItem GetSeriesIcon(SvgChart sc, ExcelLineChartSerie ls, int } else { - var pTm = _seriesHeadersMeasure[index - 1]; y = ((SvgRenderLineItem)pSls.SeriesIcon).Y1 + pTm.Height / 2 + tm.Height / 2 + MiddleMargin; } diff --git a/src/EPPlus.Export.ImageRenderer/Svg/Chart/SvgChartLegendIcon.cs b/src/EPPlus.Export.ImageRenderer/Svg/Chart/SvgChartLegendIcon.cs new file mode 100644 index 000000000..65cc40bb3 --- /dev/null +++ b/src/EPPlus.Export.ImageRenderer/Svg/Chart/SvgChartLegendIcon.cs @@ -0,0 +1,117 @@ +using EPPlusImageRenderer.RenderItems; +using EPPlusImageRenderer.Svg; +using OfficeOpenXml.Drawing; +using OfficeOpenXml.Drawing.Chart; +using OfficeOpenXml.Interfaces.Drawing.Text; +using System; +using System.Collections.Generic; + +namespace EPPlus.Export.ImageRenderer.Svg.Chart +{ + internal class SvgChartLegendIcon : SvgRenderLineItem + { + const float MarginExtra = 1.5f; + const float MiddleMargin = 7.5f; + const float LineLength = 21; + + public SvgChartLegendIcon(SvgChart sc, SvgRenderRectItem Rectangle, ExcelChartStandardSerie s, TextMeasurement tm, double topMargin, double leftMargin, TextMeasurement sHM, SvgLegendSerie pSls) : base(sc, Rectangle.Bounds) + { + SetDrawingPropertiesFill(s.Fill, sc.Chart.StyleManager.Style.SeriesLine.FillReference.Color); + SetDrawingPropertiesBorder(s.Border, sc.Chart.StyleManager.Style.SeriesLine.BorderReference.Color, s.Border.Fill.Style != eFillStyle.NoFill, 0.75); + + if (sc.Chart.Legend.Position == eLegendPosition.Top || + sc.Chart.Legend.Position == eLegendPosition.Bottom) + { + float y = (float)Rectangle.Top + (float)topMargin + tm.Height / 2 + MarginExtra; + float x = 0; + if (pSls == null) + { + x = (float)Rectangle.Left + (float)leftMargin;// + MarginExtra; + } + else + { + x = (float)pSls.Textbox.Bounds.Right + MiddleMargin; + } + + X1 = x; + Y1 = y; + X2 = x + LineLength; + Y2 = y; + LineCap = eLineCap.Round; + } + else + { + double y; + if (pSls == null) + { + y = topMargin + tm.Height / 2 + MarginExtra; + } + else + { + var pTm = sHM; + y = ((SvgRenderLineItem)pSls.SeriesIcon).Y1 + pTm.Height / 2 + tm.Height / 2 + MiddleMargin; + } + + X1 = (float)leftMargin; //4 + Y1 = y; + X2 = (float)LineLength; + Y2 = y; + LineCap = eLineCap.Round; + } + } + + //internal override void AppendRenderItems(List renderItems) + //{ + // throw new NotImplementedException(); + //} + + //private SvgRenderLineItem GetSeriesIcon(SvgChart sc, ExcelChartStandardSerie s, TextMeasurement sHM, TextMeasurement tm, SvgLegendSerie pSls) + //{ + // var item = new SvgRenderLineItem(sc, Rectangle.Bounds); + // item.SetDrawingPropertiesFill(s.Fill, sc.Chart.StyleManager.Style.SeriesLine.FillReference.Color); + // item.SetDrawingPropertiesBorder(s.Border, sc.Chart.StyleManager.Style.SeriesLine.BorderReference.Color, s.Border.Fill.Style != eFillStyle.NoFill, 0.75); + + // if (sc.Chart.Legend.Position == eLegendPosition.Top || + // sc.Chart.Legend.Position == eLegendPosition.Bottom) + // { + // float y = (float)Rectangle.Top + (float)TopMargin + tm.Height / 2 + MarginExtra; + // float x = 0; + // if (pSls == null) + // { + // x = (float)Rectangle.Left + (float)LeftMargin;// + MarginExtra; + // } + // else + // { + // x = (float)pSls.Textbox.Bounds.Right + MiddleMargin; + // } + + // item.X1 = x; + // item.Y1 = y; + // item.X2 = x + LineLength; + // item.Y2 = y; + // item.LineCap = eLineCap.Round; + // } + // else + // { + // double y; + // if (pSls == null) + // { + // y = TopMargin + tm.Height / 2 + MarginExtra; + // } + // else + // { + // var pTm = sHM; + // y = ((SvgRenderLineItem)pSls.SeriesIcon).Y1 + pTm.Height / 2 + tm.Height / 2 + MiddleMargin; + // } + + // item.X1 = (float)LeftMargin; //4 + // item.Y1 = y; + // item.X2 = (float)LineLength; + // item.Y2 = y; + // item.LineCap = eLineCap.Round; + // } + + // return item; + //} + } +} diff --git a/src/EPPlus.Export.ImageRenderer/Svg/DrawingItemForTesting.cs b/src/EPPlus.Export.ImageRenderer/Svg/DrawingItemForTesting.cs index 86b91d96e..4035e650b 100644 --- a/src/EPPlus.Export.ImageRenderer/Svg/DrawingItemForTesting.cs +++ b/src/EPPlus.Export.ImageRenderer/Svg/DrawingItemForTesting.cs @@ -45,7 +45,7 @@ public void Render(StringBuilder sb) } RenderItems.Add(new SvgEndGroupItem(this, Bounds)); - sb.Append($""); + sb.Append($""); foreach (var item in RenderItems) { diff --git a/src/EPPlus.Export.ImageRenderer/Svg/SvgLegendSerie.cs b/src/EPPlus.Export.ImageRenderer/Svg/SvgLegendSerie.cs index eb6d199b8..ef933486e 100644 --- a/src/EPPlus.Export.ImageRenderer/Svg/SvgLegendSerie.cs +++ b/src/EPPlus.Export.ImageRenderer/Svg/SvgLegendSerie.cs @@ -15,11 +15,8 @@ Date Author Change namespace EPPlusImageRenderer.Svg { - internal class SvgLegendSerie + internal class SvgLegendSerie : SvgLegendSeriesIcon { - internal RenderItem SeriesIcon { get; set; } - internal RenderItem MarkerIcon { get; set; } - internal RenderItem MarkerBackground { get; set; } - internal TextBodyItem Textbox { get; set;} + internal TextBodyItem Textbox { get; set; } } } \ No newline at end of file diff --git a/src/EPPlus.Export.ImageRenderer/Svg/SvgLegendSeriesIcon.cs b/src/EPPlus.Export.ImageRenderer/Svg/SvgLegendSeriesIcon.cs new file mode 100644 index 000000000..c361eb47f --- /dev/null +++ b/src/EPPlus.Export.ImageRenderer/Svg/SvgLegendSeriesIcon.cs @@ -0,0 +1,15 @@ +using EPPlusImageRenderer.RenderItems; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; + +namespace EPPlusImageRenderer.Svg +{ + internal class SvgLegendSeriesIcon + { + internal RenderItem SeriesIcon { get; set; } + internal RenderItem MarkerIcon { get; set; } + internal RenderItem MarkerBackground { get; set; } + } +} diff --git a/src/EPPlus.Graphics/BoundingBoxContainer.cs b/src/EPPlus.Graphics/BoundingBoxContainer.cs new file mode 100644 index 000000000..8ab53281a --- /dev/null +++ b/src/EPPlus.Graphics/BoundingBoxContainer.cs @@ -0,0 +1,112 @@ +using OfficeOpenXml.Drawing; +using OfficeOpenXml.FormulaParsing.Excel.Functions.Text; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; + +namespace EPPlus.Graphics +{ + internal class BoundingBoxContainer : BoundingBox + { + internal double MarginLeft + { + get + { + return InnerBounds.Left; + } + set + { + InnerBounds.Left = value; + } + } + + internal double MarginTop + { + get + { + return InnerBounds.Top; + } + set + { + InnerBounds.Top = value; + } + } + + internal double MarginRight + { + get + { + return GetInnerRight(); + } + set + { + InnerBounds.Width = Width - value; + } + } + internal double MarginBottom + { + get + { + return GetInnerBottom(); + } + set + { + InnerBounds.Height = Height - value; + } + } + + BoundingBox InnerBounds; + + internal BoundingBoxContainer(BoundingBox innerBounds) : base() + { + InnerBounds = innerBounds; + } + + //internal BoundingBoxContainer(double l, double t, double r, double b) : base(l, t, r, b) + //{ + //} + + internal double GetInnerLeft() + { + return Left + MarginLeft; + } + + internal double GetInnerTop() + { + return Top + MarginTop; + } + + internal double GetInnerBottom() + { + return Bottom - MarginBottom; + } + + internal double GetInnerRight() + { + return Right - MarginRight; + } + + //public BoundingBox GetInnerBounds() + //{ + // var textArea = new BoundingBox(); + // textArea.Left = GetInnerLeft(); + // textArea.Top = GetInnerTop(); + + // textArea.Width = GetInnerWidth(); + // textArea.Height = GetInnerHeight(); + + // return textArea; + //} + + public double GetInnerWidth() + { + return GetInnerRight() - GetInnerLeft(); + } + + public double GetInnerHeight() + { + return GetInnerBottom() - GetInnerTop(); + } + } +} diff --git a/src/EPPlusTest/Drawing/Chart/ChartTitleTests.cs b/src/EPPlusTest/Drawing/Chart/ChartTitleTests.cs index 0494dc10e..d39c9fdcc 100644 --- a/src/EPPlusTest/Drawing/Chart/ChartTitleTests.cs +++ b/src/EPPlusTest/Drawing/Chart/ChartTitleTests.cs @@ -102,16 +102,5 @@ public void AddPiePivotTableWithTitle() chart.Title.SetAutoTitle(); chart.Font.Color = Color.Red; } - - [TestMethod] - public void AddTitleMoveTest() - { - var ws = _pck.Workbook.Worksheets.Add("TitleMoveTest"); - var chart = ws.Drawings.AddBarChart("barChart1", eBarChartType.BarClustered); - ws.Cells["A1"].Value = "Linked Cell Title"; - chart.Series.Add("Data!N1:N10", "Data!K1:K10"); - chart.XAxis.AddTitle(ws.Cells["A1"]); - chart.Title.l - } } } From 7d5f9d550a04e48497fde7ecf4e783b2fa82984b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jan=20K=C3=A4llman?= Date: Fri, 20 Mar 2026 10:18:18 +0100 Subject: [PATCH 108/151] WIP:Vertical and diagonal axis labels --- .../SvgPathTests.cs | 20 ++++++------ .../Svg/Chart/Axis/DateAxisScaleCalculator.cs | 7 ++-- .../Svg/Chart/SvgChartAxis.cs | 32 +++++++++++++------ 3 files changed, 36 insertions(+), 23 deletions(-) diff --git a/src/EPPlus.Export.ImageRenderer.Test/SvgPathTests.cs b/src/EPPlus.Export.ImageRenderer.Test/SvgPathTests.cs index d4443c8dc..7434c0ca2 100644 --- a/src/EPPlus.Export.ImageRenderer.Test/SvgPathTests.cs +++ b/src/EPPlus.Export.ImageRenderer.Test/SvgPathTests.cs @@ -791,16 +791,16 @@ public void GenerateSvgForCharts_SecondaryAxis() { var ws = p.Workbook.Worksheets[0]; var renderer = new EPPlusImageRenderer.ImageRenderer(); - var ix = 3; - var c = ws.Drawings[ix]; - var svg = renderer.RenderDrawingToSvg(c); - SaveTextFileToWorkbook($"svg\\ChartForSvg_sheet2_{ix++}.svg", svg); - //var ix = 1; - //foreach (ExcelChart c in ws.Drawings) - //{ - // var svg = renderer.RenderDrawingToSvg(c); - // SaveTextFileToWorkbook($"svg\\ChartForSvg_SecAxis{ix++}.svg", svg); - //} + //var ix = 3; + //var c = ws.Drawings[ix]; + //var svg = renderer.RenderDrawingToSvg(c); + //SaveTextFileToWorkbook($"svg\\ChartForSvg_sheet2_{ix++}.svg", svg); + var ix = 1; + foreach (ExcelChart c in ws.Drawings) + { + var svg = renderer.RenderDrawingToSvg(c); + SaveTextFileToWorkbook($"svg\\ChartForSvg_SecAxis{ix++}.svg", svg); + } } } [TestMethod] diff --git a/src/EPPlus.Export.ImageRenderer/Svg/Chart/Axis/DateAxisScaleCalculator.cs b/src/EPPlus.Export.ImageRenderer/Svg/Chart/Axis/DateAxisScaleCalculator.cs index 83ef7f55f..cfda9e436 100644 --- a/src/EPPlus.Export.ImageRenderer/Svg/Chart/Axis/DateAxisScaleCalculator.cs +++ b/src/EPPlus.Export.ImageRenderer/Svg/Chart/Axis/DateAxisScaleCalculator.cs @@ -410,6 +410,7 @@ private static void AddIntervall(ref int interval, ref eTimeUnit unit) private static bool FitAsHorizontalText(ITextMeasurer tm, AxisOptions options, double min, double max, int interval, eTimeUnit unit, float height, double width) { + var minMargin = 2; //2 Points var minDate = DateTime.FromOADate(min); var maxDate = DateTime.FromOADate(max); var date = minDate; @@ -420,7 +421,7 @@ private static bool FitAsHorizontalText(ITextMeasurer tm, AxisOptions options, d while (date < maxDate) { var textWidth = tm.MeasureText(date.ToString(), mf).Width; - horizontalWidth += textWidth; + horizontalWidth += textWidth + minMargin; if(horizontalWidth > width) { return false; @@ -431,11 +432,11 @@ private static bool FitAsHorizontalText(ITextMeasurer tm, AxisOptions options, d } else if(unit == eTimeUnit.Months) { - date = minDate.AddDays(1).AddMonths(interval).AddDays(-1); + date = minDate.AddDays(1).AddMonths(interval).AddDays(-1); //Extra adds to keep last day of month } else { - date = minDate.AddDays(1).AddYears(interval).AddDays(-1); + date = minDate.AddDays(1); } } return true; diff --git a/src/EPPlus.Export.ImageRenderer/Svg/Chart/SvgChartAxis.cs b/src/EPPlus.Export.ImageRenderer/Svg/Chart/SvgChartAxis.cs index 77c22d87e..9791e98ac 100644 --- a/src/EPPlus.Export.ImageRenderer/Svg/Chart/SvgChartAxis.cs +++ b/src/EPPlus.Export.ImageRenderer/Svg/Chart/SvgChartAxis.cs @@ -374,21 +374,27 @@ private List GetAxisValueTextBoxes() if (LabelOrientation == eTextOrientation.Diagonal) { + x = ticMarkX - (height / 2) * cos; if (Axis.AxisPosition == eAxisPosition.Bottom) { - x = ticMarkX - (height / 2) * cos; - y = ticMarkY + 4 + (height / 2) * cos; + y = ticMarkY + 4 + TopMargin - (height / 2 * cos); } else //Top { - x = ticMarkX - (height / 2) * cos; - y = ticMarkY - 4 + (height / 2) * cos; + y = ticMarkY - 4 - BottomMargin - (height/2 * cos); } } else { x = ticMarkX - (height / 2); - y = ticMarkY + 4 + (height / 2); + if (Axis.AxisPosition == eAxisPosition.Bottom) + { + y = ticMarkY + BottomMargin + 4; + } + else //Top + { + y = ticMarkY - TopMargin - 4; + } } } @@ -404,7 +410,7 @@ private List GetAxisValueTextBoxes() else if (LabelOrientation == eTextOrientation.Vertical) { - tb.Rotation = 90; + tb.Rotation = -90; if (Axis.AxisPosition == eAxisPosition.Bottom) { tb.TextAnchor = eTextAnchor.End; @@ -509,18 +515,24 @@ private double GetAxisItemTop(int i, OfficeOpenXml.Interfaces.Drawing.Text.TextM { if (Axis.AxisPosition == eAxisPosition.Top) { - return Rectangle.Bottom - m.Height - TopMargin; + switch (LabelOrientation) + { + case eTextOrientation.Vertical: + case eTextOrientation.Diagonal: + return Rectangle.Bottom; + default: + return Rectangle.Bottom - m.Height - TopMargin; + } } else if (Axis.AxisPosition == eAxisPosition.Bottom) { switch(LabelOrientation) { case eTextOrientation.Vertical: - return Rectangle.Top - m.Width + BottomMargin; case eTextOrientation.Diagonal: - return Rectangle.Top - BottomMargin; + return Rectangle.Top; default: - return Rectangle.Top - m.Height + BottomMargin; + return Rectangle.Top + BottomMargin; } } else From 54c1f5bcba1676e6ffd9b1c06517b4e7a91161c6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jan=20K=C3=A4llman?= Date: Fri, 20 Mar 2026 16:17:42 +0100 Subject: [PATCH 109/151] WIP:Fixed vertical/diagonal axis labels for category axis --- .../SvgPathTests.cs | 20 +++ .../Chart/Axis/CategoryAxisScaleCalculator.cs | 121 ++++++++++++++++++ .../Svg/Chart/SvgChartAxis.cs | 49 ++++--- .../Drawing/Chart/ChartEx/ExcelChartExAxis.cs | 3 +- src/EPPlus/Drawing/Chart/ExcelChartAxis.cs | 3 +- .../Drawing/Chart/ExcelChartAxisStandard.cs | 4 +- 6 files changed, 180 insertions(+), 20 deletions(-) create mode 100644 src/EPPlus.Export.ImageRenderer/Svg/Chart/Axis/CategoryAxisScaleCalculator.cs diff --git a/src/EPPlus.Export.ImageRenderer.Test/SvgPathTests.cs b/src/EPPlus.Export.ImageRenderer.Test/SvgPathTests.cs index 7434c0ca2..a01feb498 100644 --- a/src/EPPlus.Export.ImageRenderer.Test/SvgPathTests.cs +++ b/src/EPPlus.Export.ImageRenderer.Test/SvgPathTests.cs @@ -804,6 +804,26 @@ public void GenerateSvgForCharts_SecondaryAxis() } } [TestMethod] + public void GenerateSvgForCharts_SecondaryAxis_sheet2() + { + ExcelPackage.License.SetNonCommercialOrganization("EPPlus Project"); + using (var p = OpenTemplatePackage("ChartForSvg_SecondaryAxis.xlsx")) + { + var ws = p.Workbook.Worksheets[1]; + var renderer = new EPPlusImageRenderer.ImageRenderer(); + //var ix = 0; + //var c = ws.Drawings[ix]; + //var svg = renderer.RenderDrawingToSvg(c); + //SaveTextFileToWorkbook($"svg\\ChartForSvg_sheet2_{ix++}.svg", svg); + var ix = 1; + foreach (ExcelChart c in ws.Drawings) + { + var svg = renderer.RenderDrawingToSvg(c); + SaveTextFileToWorkbook($"svg\\ChartForSvg_Sheet2_SecAxis{ix++}.svg", svg); + } + } + } + [TestMethod] public void CreateChartsWithDifferentSize() { ExcelPackage.License.SetNonCommercialOrganization("EPPlus Project"); diff --git a/src/EPPlus.Export.ImageRenderer/Svg/Chart/Axis/CategoryAxisScaleCalculator.cs b/src/EPPlus.Export.ImageRenderer/Svg/Chart/Axis/CategoryAxisScaleCalculator.cs new file mode 100644 index 000000000..74b692870 --- /dev/null +++ b/src/EPPlus.Export.ImageRenderer/Svg/Chart/Axis/CategoryAxisScaleCalculator.cs @@ -0,0 +1,121 @@ +using EPPlusImageRenderer.Svg; +using OfficeOpenXml; +using OfficeOpenXml.DataValidation; +using OfficeOpenXml.Drawing.Chart; +using OfficeOpenXml.FormulaParsing.Excel.Functions.Information; +using OfficeOpenXml.FormulaParsing.Excel.Functions.MathFunctions; +using OfficeOpenXml.Interfaces.Drawing.Text; +using OfficeOpenXml.Utils.DateUtils; +using System; +using System.Collections.Generic; +using System.Drawing; +using System.Linq; + +namespace EPPlus.Export.ImageRenderer.Svg.Chart.Util +{ + internal class CategoryAxisScaleCalculator + { + + internal static AxisScale CalculateByWidth(ref List values, ITextMeasurer tm, AxisOptions options) + { + //var height = options.ChartSize.Bounds.Height; + var ax = options.Axis; + var plotAreaWidth = options.ChartSize.Bounds.Width; + var mf = ax.Font.GetMeasureFont(); + var displayValues = values.Select(x => ax.ToString()).ToList(); + var res = tm.MeasureText(displayValues[0], mf); + + //Get interval for maximum width with vertical text. + var interval = GetMinUnitVerticalText(displayValues, res.Height, plotAreaWidth); + + + //Get max text width when using diagonal text + var width = mf.Size * Math.Sqrt(2); + var margin = mf.Size * 0.5; + + if (FitAsVerticalDiagonalText(displayValues, interval, width, margin, plotAreaWidth)) //Check diagonal + { + if(FitAsHorizontalText(displayValues, interval, mf, tm, plotAreaWidth)) //Check horizontal + { + return new AxisScale() + { + MajorInterval = interval, + MinorInterval = 1, + Min = 1, + Max = displayValues.Count, + TextOrientation = eTextOrientation.Horizontal + }; + } + else + { + return new AxisScale() + { + MajorInterval = interval, + MinorInterval = 1, + Min = 1, + Max = displayValues.Count, + TextOrientation = eTextOrientation.Diagonal + }; + } + } + if(interval!=1) + { + var removeCount = interval - 1; + var c = (int)Math.Truncate(values.Count / (double)interval); + for(int i=0;i<=c;i++) + { + for(int j=0;j displayValues, int interval, MeasurementFont mf, ITextMeasurer tm, double plotAreaWidth) + { + var margin = mf.Size * 0.3; + var width = tm.MeasureText(displayValues[0], mf).Width + margin; + var pos = interval; + while (pos < displayValues.Count && width < plotAreaWidth) + { + pos += interval; + width = tm.MeasureText(displayValues[pos], mf).Width + margin; + if(width > plotAreaWidth) return false; + } + return width <= plotAreaWidth; + } + + private static bool FitAsVerticalDiagonalText(List displayValues, int interval, double textWidth, double margin, double plotAreaWidth) + { + var items = Math.Truncate(displayValues.Count * 1D / interval); + return items * textWidth + (items - 1) * margin < plotAreaWidth; + } + + private static int GetMinUnitVerticalText(List displayValues, double textHeight, double plotAreaWidth) + { + var interval = 1; + var margin = 0D; + var items = Math.Truncate((double)displayValues.Count / interval); + while (items * textHeight + (items - 1) * margin >= plotAreaWidth) + { + interval++; + items = Math.Truncate((double)displayValues.Count / interval); + } + + return interval; + } + } +} \ No newline at end of file diff --git a/src/EPPlus.Export.ImageRenderer/Svg/Chart/SvgChartAxis.cs b/src/EPPlus.Export.ImageRenderer/Svg/Chart/SvgChartAxis.cs index 9791e98ac..0357fae8a 100644 --- a/src/EPPlus.Export.ImageRenderer/Svg/Chart/SvgChartAxis.cs +++ b/src/EPPlus.Export.ImageRenderer/Svg/Chart/SvgChartAxis.cs @@ -79,7 +79,7 @@ internal SvgChartAxis(SvgChart sc, ExcelChartAxisStandard ax) : base(sc) { Rectangle.Width = GetTextWidest(sc, ax) + LeftMargin; var ll = 8D; - if (sc.Chart.Legend.Position == eLegendPosition.Left) + if (sc.Chart.HasLegend && sc.Chart.Legend.Position == eLegendPosition.Left) { ll = sc.Legend.Rectangle.Right + sc.Legend.RightMargin; } @@ -89,7 +89,7 @@ internal SvgChartAxis(SvgChart sc, ExcelChartAxisStandard ax) : base(sc) { Rectangle.Width = GetTextWidest(sc, ax) + RightMargin; var lp = sc.ChartArea.Rectangle.Width - Rectangle.Width - 8D; - if (sc.Chart.Legend.Position == eLegendPosition.Right) + if (sc.Chart.HasLegend && sc.Chart.Legend.Position == eLegendPosition.Right) { lp = sc.Legend.Rectangle.Left + -Rectangle.Width; } @@ -766,14 +766,35 @@ internal double GetPositionInPlotarea(double val) protected List GetAxisValue(ExcelChartAxisStandard ax, RenderItem rect, out double? min, out double? max, out double? majorUnit, out eTimeUnit? dateUnit, out eTextOrientation orientation) { var values = ax.GetAxisValues(out bool isCount); + + var options = new AxisOptions + { + LockedMin = ax.MinValue, + LockedMax = ax.MaxValue, + LockedInterval = ax.MajorUnit, + LockedIntervalUnit = ax.MajorTimeUnit, + AddPadding = ax.AxisPosition == eAxisPosition.Left || ax.AxisPosition == eAxisPosition.Right, + Axis = ax, + IsStacked100 = Chart.IsTypePercentStacked(), + ChartSize = rect + }; + if (ax.AxisType == eAxisType.Cat && isCount == false) { min = 0; - max = values.Length; + max = values.Count; majorUnit = 1; dateUnit = null; orientation = eTextOrientation.Horizontal; + var res = CategoryAxisScaleCalculator.CalculateByWidth(ref values, SvgChart.TextMeasurer, options); + + min = res.Min; + max = res.Max; + majorUnit = res.MajorInterval; + dateUnit = null; + orientation = res.TextOrientation; + return values.ToList(); } @@ -796,17 +817,6 @@ protected List GetAxisValue(ExcelChartAxisStandard ax, RenderItem rect, max = d; } } - var options = new AxisOptions - { - LockedMin = ax.MinValue, - LockedMax = ax.MaxValue, - LockedInterval = ax.MajorUnit, - LockedIntervalUnit = ax.MajorTimeUnit, - AddPadding = ax.AxisPosition == eAxisPosition.Left || ax.AxisPosition == eAxisPosition.Right, - Axis = ax, - IsStacked100 = Chart.IsTypePercentStacked(), - ChartSize = rect - }; var length = ax.AxisPosition == eAxisPosition.Left || ax.AxisPosition == eAxisPosition.Right ? SvgChart.Bounds.Height : SvgChart.Bounds.Width; //Fix and use plotarea width/height. if(isCount) @@ -817,8 +827,15 @@ protected List GetAxisValue(ExcelChartAxisStandard ax, RenderItem rect, { l.Add(i); } - orientation = eTextOrientation.Horizontal; - return l; + var res = CategoryAxisScaleCalculator.CalculateByWidth(ref l, SvgChart.TextMeasurer, options); + + min = res.Min; + max = res.Max; + majorUnit = res.MajorInterval; + dateUnit = null; + orientation = res.TextOrientation; + + return values.ToList(); } if (ax.IsDate) { diff --git a/src/EPPlus/Drawing/Chart/ChartEx/ExcelChartExAxis.cs b/src/EPPlus/Drawing/Chart/ChartEx/ExcelChartExAxis.cs index 9b84a6a6a..d7be5cdad 100644 --- a/src/EPPlus/Drawing/Chart/ChartEx/ExcelChartExAxis.cs +++ b/src/EPPlus/Drawing/Chart/ChartEx/ExcelChartExAxis.cs @@ -12,6 +12,7 @@ Date Author Change *************************************************************************************************/ using OfficeOpenXml.Utils.EnumUtils; using System; +using System.Collections.Generic; using System.Xml; namespace OfficeOpenXml.Drawing.Chart.ChartEx @@ -189,7 +190,7 @@ internal override ExcelChartTitle GetTitle() return _title; } - internal override object[] GetAxisValues(out bool isCount) + internal override List GetAxisValues(out bool isCount) { throw new NotImplementedException(); } diff --git a/src/EPPlus/Drawing/Chart/ExcelChartAxis.cs b/src/EPPlus/Drawing/Chart/ExcelChartAxis.cs index 5078eddbe..264249edd 100644 --- a/src/EPPlus/Drawing/Chart/ExcelChartAxis.cs +++ b/src/EPPlus/Drawing/Chart/ExcelChartAxis.cs @@ -18,6 +18,7 @@ Date Author Change using OfficeOpenXml.Utils.EnumUtils; using System; using System.Reflection.Emit; +using System.Collections.Generic; namespace OfficeOpenXml.Drawing.Chart { @@ -599,6 +600,6 @@ void IStyleMandatoryProperties.SetMandatoryProperties() CreatespPrNode($"{_nsPrefix}:spPr"); } - internal abstract object[] GetAxisValues(out bool isCount); + internal abstract List GetAxisValues(out bool isCount); } } diff --git a/src/EPPlus/Drawing/Chart/ExcelChartAxisStandard.cs b/src/EPPlus/Drawing/Chart/ExcelChartAxisStandard.cs index f8affc2b7..9173dfe16 100644 --- a/src/EPPlus/Drawing/Chart/ExcelChartAxisStandard.cs +++ b/src/EPPlus/Drawing/Chart/ExcelChartAxisStandard.cs @@ -794,7 +794,7 @@ internal bool IsXAxis return false; } } - internal override object[] GetAxisValues(out bool isCount) + internal override List GetAxisValues(out bool isCount) { List> values; GetSeriesValues(out isCount, out values); @@ -804,7 +804,7 @@ internal override object[] GetAxisValues(out bool isCount) { dl.Reverse(); } - return dl.ToArray(); + return dl; } internal void GetSeriesValues(out bool isCount, out List> values) From e34760a286ad5f706cfa71cb4d5110e13261c287 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ossian=20Edstr=C3=B6m?= Date: Fri, 20 Mar 2026 18:02:05 +0100 Subject: [PATCH 110/151] Added new handling of group items --- .../ImageRenderer.cs | 30 +++++++++---------- .../RenderItems/Shared/ContainerItem.cs | 19 ++++-------- .../RenderItems/SvgItem/SvgContainerItem.cs | 4 ++- 3 files changed, 23 insertions(+), 30 deletions(-) diff --git a/src/EPPlus.Export.ImageRenderer/ImageRenderer.cs b/src/EPPlus.Export.ImageRenderer/ImageRenderer.cs index e5e988653..60317d447 100644 --- a/src/EPPlus.Export.ImageRenderer/ImageRenderer.cs +++ b/src/EPPlus.Export.ImageRenderer/ImageRenderer.cs @@ -57,28 +57,28 @@ public string RenderTest() var baseItem = new DrawingItemForTesting(baseBB); - //SvgRenderRectItem myBgItem = new SvgRenderRectItem(baseItem, baseItem.Bounds); - //myBgItem.FillColor = "purple"; - //myBgItem.FillOpacity = 0.2d; + SvgRenderRectItem myBgItem = new SvgRenderRectItem(baseItem, baseItem.Bounds); + myBgItem.FillColor = "purple"; + myBgItem.FillOpacity = 0.2d; - //SvgRenderRectItem myInnerItem = new SvgRenderRectItem(baseItem, myBgItem.Bounds); + SvgRenderRectItem myInnerItem = new SvgRenderRectItem(baseItem, myBgItem.Bounds); - //myInnerItem.FillColor = "green"; - //myInnerItem.FillOpacity = 0.8d; + myInnerItem.FillColor = "green"; + myInnerItem.FillOpacity = 0.8d; - //myInnerItem.Width = 50; - //myInnerItem.Height = 50; + myInnerItem.Width = 50; + myInnerItem.Height = 50; - //var container = new SvgContainerItem(myInnerItem, myBgItem); + var container = new SvgContainerItem(myInnerItem, myBgItem); - //container.MarginLeft = 5; - //container.MarginRight = 5; - //container.MarginTop = 5; - //container.MarginBottom = 5; + container.MarginLeft = 5; + container.MarginRight = 5; + container.MarginTop = 5; + container.MarginBottom = 5; - //container.ApplyMargins(); + container.ApplyMargins(); - //baseItem.RenderItems.Add(container); + baseItem.RenderItems.Add(container); var sb = new StringBuilder(); diff --git a/src/EPPlus.Export.ImageRenderer/RenderItems/Shared/ContainerItem.cs b/src/EPPlus.Export.ImageRenderer/RenderItems/Shared/ContainerItem.cs index 21003f355..355a0df8b 100644 --- a/src/EPPlus.Export.ImageRenderer/RenderItems/Shared/ContainerItem.cs +++ b/src/EPPlus.Export.ImageRenderer/RenderItems/Shared/ContainerItem.cs @@ -11,8 +11,8 @@ namespace EPPlus.Export.ImageRenderer.RenderItems.Shared { internal abstract class ContainerItem : RenderItem { - private RenderItem InnerItem; - private RenderItem OuterItem; + internal RenderItem InnerItem { get; private set; } + internal RenderItem OuterItem { get; private set; } double _marginLeft; double _marginTop; @@ -83,7 +83,7 @@ public ContainerItem(RenderItem innerItem, RenderItem outerItem) : base(innerIte InnerItem = innerItem; OuterItem.Bounds.Parent = Bounds; - InnerItem.Bounds.Parent = OuterItem.Bounds; + InnerItem.Bounds.Parent = Bounds; } internal void ApplyMargins() @@ -91,22 +91,13 @@ internal void ApplyMargins() //Origin-Point: Set. Set in stone. All text moves from there //Here it is Bounds.Top and Bounds.Left. Nothing in the content can change that - OuterItem.Bounds.Top = MarginLeft; - OuterItem.Bounds.Left = MarginTop; - - OuterItem.Bounds.Width = InnerItem.Bounds.Width + MarginRight; - OuterItem.Bounds.Height = InnerItem.Bounds.Height + MarginBottom; + OuterItem.Bounds.Width = InnerItem.Bounds.Width + MarginRight + MarginLeft; + OuterItem.Bounds.Height = InnerItem.Bounds.Height + MarginBottom + MarginTop; Bounds.Width = OuterItem.Bounds.Width; Bounds.Height = OuterItem.Bounds.Height; } - public override void Render(StringBuilder sb) - { - OuterItem.Render(sb); - InnerItem.Render(sb); - } - public override RenderItemType Type => RenderItemType.Group; internal double GetInnerLeft() diff --git a/src/EPPlus.Export.ImageRenderer/RenderItems/SvgItem/SvgContainerItem.cs b/src/EPPlus.Export.ImageRenderer/RenderItems/SvgItem/SvgContainerItem.cs index 01e411351..26d992837 100644 --- a/src/EPPlus.Export.ImageRenderer/RenderItems/SvgItem/SvgContainerItem.cs +++ b/src/EPPlus.Export.ImageRenderer/RenderItems/SvgItem/SvgContainerItem.cs @@ -18,10 +18,12 @@ public override void Render(StringBuilder sb) var grpItem = new SvgGroupItem(DrawingRenderer, Bounds.Left, Bounds.Top); grpItem.Render(sb); + OuterItem.Render(sb); + var grpItem2 = new SvgGroupItem(DrawingRenderer, MarginLeft, MarginTop); grpItem2.Render(sb); - base.Render(sb); + InnerItem.Render(sb); var endGroupItem2 = new SvgEndGroupItem(DrawingRenderer, Bounds); endGroupItem2.Render(sb); From af71242ff6d6b66df29fbf6ca26b1d26d888a52f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ossian=20Edstr=C3=B6m?= Date: Fri, 20 Mar 2026 18:02:45 +0100 Subject: [PATCH 111/151] Actually added new handling of items --- .../GroupItemNewTest.cs | 131 ++++++++++++++++++ .../RenderItems/Shared/GroupItem.cs | 94 +++++++++++++ .../RenderItems/SvgItem/SvgGroupItemNew.cs | 83 +++++++++++ src/EPPlus.Graphics/Point.cs | 40 ++++++ 4 files changed, 348 insertions(+) create mode 100644 src/EPPlus.Export.ImageRenderer.Test/GroupItemNewTest.cs create mode 100644 src/EPPlus.Export.ImageRenderer/RenderItems/Shared/GroupItem.cs create mode 100644 src/EPPlus.Export.ImageRenderer/RenderItems/SvgItem/SvgGroupItemNew.cs create mode 100644 src/EPPlus.Graphics/Point.cs diff --git a/src/EPPlus.Export.ImageRenderer.Test/GroupItemNewTest.cs b/src/EPPlus.Export.ImageRenderer.Test/GroupItemNewTest.cs new file mode 100644 index 000000000..4fc794ab3 --- /dev/null +++ b/src/EPPlus.Export.ImageRenderer.Test/GroupItemNewTest.cs @@ -0,0 +1,131 @@ +using EPPlus.Export.ImageRenderer.RenderItems.SvgItem; +using EPPlus.Export.ImageRenderer.Svg; +using EPPlus.Graphics; +using EPPlusImageRenderer.RenderItems; +using System.Text; + +namespace EPPlus.Export.ImageRenderer.Tests +{ + [TestClass] + public class GroupItemNewTest : TestBase + { + [TestMethod] + public void TestGroupItemMovingTwoChildrenCorrectly() + { + var baseBB = new BoundingBox(); + + //96x96 px + baseBB.Width = 72; + baseBB.Height = 72; + + var baseItem = new DrawingItemForTesting(baseBB); + + var groupItem = new SvgGroupItemNew(baseItem, 9, 9); + + SvgRenderRectItem rectItem = new SvgRenderRectItem(baseItem, groupItem.Bounds); + + rectItem.FillColor = "red"; + rectItem.FillOpacity = 0.2d; + + rectItem.Width = 20; + rectItem.Height = 20; + + groupItem.AddChildItem(rectItem); + + + SvgRenderRectItem siblingItem = new SvgRenderRectItem(baseItem, groupItem.Bounds); + siblingItem.FillColor = "blue"; + siblingItem.FillOpacity = 0.2d; + + siblingItem.Width = 20; + siblingItem.Height = 20; + + siblingItem.Bounds.Left = 20; + siblingItem.Bounds.Top = 20; + + groupItem.AddChildItem(siblingItem); + + var worldCoordinatesRectBefore = rectItem.Bounds.GetWorldCoordinates(); + Assert.AreEqual(9, worldCoordinatesRectBefore.X); + Assert.AreEqual(9, worldCoordinatesRectBefore.Y); + + var worldCoordinatesSibBefore = siblingItem.Bounds.GetWorldCoordinates(); + Assert.AreEqual(29, worldCoordinatesSibBefore.X); + Assert.AreEqual(29, worldCoordinatesSibBefore.Y); + + groupItem.Position.Left = 18; + groupItem.Position.Top = 18; + + var worldCoordinatesRect = rectItem.Bounds.GetWorldCoordinates(); + Assert.AreEqual(18, worldCoordinatesRect.X); + Assert.AreEqual(18, worldCoordinatesRect.Y); + + var worldCoordinatesSib = siblingItem.Bounds.GetWorldCoordinates(); + Assert.AreEqual(38, worldCoordinatesSib.X); + Assert.AreEqual(38, worldCoordinatesSib.Y); + + var sb = new StringBuilder(); + + baseItem.RenderItems.Add(groupItem); + + baseItem.Render(sb); + var svgString = sb.ToString(); + + SaveTextFileToWorkbook($"svg\\StandAloneTestGroup.svg", svgString); + } + + [TestMethod] + public void TestGroupItemRotatingTwoChildrenCorrectly() + { + var baseBB = new BoundingBox(); + + //96x96 px + baseBB.Width = 72; + baseBB.Height = 72; + + var baseItem = new DrawingItemForTesting(baseBB); + + BoundingBox parent = new BoundingBox(); + + var groupItem = new SvgGroupItemNew(baseItem, parent, 45); + + groupItem.Position.Left = 10; + groupItem.Position.Top = 10; + + SvgRenderRectItem rectItem = new SvgRenderRectItem(baseItem, groupItem.Bounds); + + rectItem.FillColor = "red"; + rectItem.FillOpacity = 0.2d; + + rectItem.Width = 20; + rectItem.Height = 20; + + groupItem.AddChildItem(rectItem); + + + SvgRenderRectItem siblingItem = new SvgRenderRectItem(baseItem, groupItem.Bounds); + siblingItem.FillColor = "blue"; + siblingItem.FillOpacity = 0.2d; + + siblingItem.Width = 20; + siblingItem.Height = 20; + + siblingItem.Bounds.Left = 20; + siblingItem.Bounds.Top = 20; + + groupItem.AddChildItem(siblingItem); + + parent.Width = groupItem.Bounds.Width; + parent.Height = groupItem.Bounds.Height; + + var sb = new StringBuilder(); + + baseItem.RenderItems.Add(groupItem); + + baseItem.Render(sb); + var svgString = sb.ToString(); + + SaveTextFileToWorkbook($"svg\\StandAloneTestGroup.svg", svgString); + } + } +} diff --git a/src/EPPlus.Export.ImageRenderer/RenderItems/Shared/GroupItem.cs b/src/EPPlus.Export.ImageRenderer/RenderItems/Shared/GroupItem.cs new file mode 100644 index 000000000..0dbc9978a --- /dev/null +++ b/src/EPPlus.Export.ImageRenderer/RenderItems/Shared/GroupItem.cs @@ -0,0 +1,94 @@ +using EPPlus.Graphics; +using EPPlusImageRenderer; +using EPPlusImageRenderer.RenderItems; +using System.Collections.Generic; + +namespace EPPlus.Export.ImageRenderer.RenderItems.Shared +{ + internal abstract class GroupItem : RenderItem + { + /// + /// In degrees + /// + internal double Rotation = double.NaN; + /// + /// In points + /// + internal Point Position = null; + + internal BoundingBox BoundingBoxParent = null; + + Point _altRotationPoint = null; + + internal Point RotationPoint + { + get + { + if (_altRotationPoint == null) + { + return Position; + } + return _altRotationPoint; + } + set + { + _altRotationPoint = value; + } + } + + + //Transform _rotationPoint; + + /// + /// Items contained in this group + /// + internal protected List _childItems = new List(); + + public GroupItem(DrawingBase renderer) : base(renderer) + { + Bounds.Parent = Position; + //_rotationPoint = Bounds; + } + + public GroupItem(DrawingBase renderer, double localXPos, double localYPos) : this(renderer) + { + Position = new Point(localXPos, localYPos); + } + + + public GroupItem(DrawingBase renderer, BoundingBox parent, double rotation, Transform rotationPoint = null) : this(renderer, 0, 0) + { + BoundingBoxParent = parent; + Rotation = rotation; + if (rotationPoint != null) + { + RotationPoint = new Point(rotationPoint.LocalPosition.X, rotationPoint.LocalPosition.Y); + } + //if (rotationPoint != null) + //{ + // SetRotationPoint(rotationPoint); + //} + } + + //internal void SetRotationPoint(Transform point) + //{ + // _rotationPoint = point; + //} + + //internal void SetRotationBounds(BoundingBox rotationBounds) + //{ + // _rotationPoint = rotationBounds; + //} + + internal void AddChildItem(RenderItem item) + { + item.Bounds.Parent = Position; + _childItems.Add(item); + + Bounds.Width = item.Bounds.Right > Bounds.Width ? item.Bounds.Right : Bounds.Width; + Bounds.Height = item.Bounds.Bottom > Bounds.Height ? item.Bounds.Bottom : Bounds.Height; + } + + public override RenderItemType Type => RenderItemType.Group; + } +} diff --git a/src/EPPlus.Export.ImageRenderer/RenderItems/SvgItem/SvgGroupItemNew.cs b/src/EPPlus.Export.ImageRenderer/RenderItems/SvgItem/SvgGroupItemNew.cs new file mode 100644 index 000000000..9dfa93dfc --- /dev/null +++ b/src/EPPlus.Export.ImageRenderer/RenderItems/SvgItem/SvgGroupItemNew.cs @@ -0,0 +1,83 @@ +using EPPlus.Export.ImageRenderer.RenderItems.Shared; +using EPPlus.Fonts.OpenType.Utils; +using EPPlus.Graphics; +using EPPlusImageRenderer; +using System.Globalization; +using System.Text; + +namespace EPPlus.Export.ImageRenderer.RenderItems.SvgItem +{ + internal class SvgGroupItemNew : GroupItem + { + const string transformTranslate = "translate({0}, {1}) "; + const string transformRotate = "rotate({0}) "; + + public SvgGroupItemNew(DrawingBase renderer, double localXPos, double localYPos) : base(renderer, localXPos, localYPos) + { + } + + public SvgGroupItemNew(DrawingBase renderer, BoundingBox parent, double rotation, Transform rotationPoint = null) : base(renderer, parent, rotation, rotationPoint) + { + } + + public override void Render(StringBuilder sb) + { + string combinedTransform = GetCombinedTransformString(); + + if (string.IsNullOrEmpty(combinedTransform) == false) + { + sb.Append($""); + } + else + { + sb.Append($""); + } + + foreach (var item in _childItems) + { + item.Render(sb); + } + + sb.Append(""); + } + + string GetCombinedTransformString() + { + string positionStr = ""; + string rotationStr = GetRotationStr(); + + if (Position != null) + { + positionStr = string.Format(transformTranslate, Position.Top.PointToPixelString(), Position.Left.PointToPixelString()) + " "; + } + + return positionStr + rotationStr; + } + + string GetRotationStr() + { + if (double.IsNaN(Rotation) == false) + { + string rot = Rotation.ToString(CultureInfo.InvariantCulture); + if (BoundingBoxParent != null) + { + var cx = BoundingBoxParent.Width / 2; + var cy = BoundingBoxParent.Height / 2; + + if (cx != 0 && cy != 0) + { + rot += $", {cx.ToString(CultureInfo.InvariantCulture)}, {cy.ToString(CultureInfo.InvariantCulture)}"; + } + } + else if(RotationPoint != null && RotationPoint != Position) + { + rot += $", {RotationPoint.Left.ToString(CultureInfo.InvariantCulture)}, {RotationPoint.Top.ToString(CultureInfo.InvariantCulture)}"; + } + + return string.Format(transformRotate, rot); + } + + return string.Empty; + } + } +} diff --git a/src/EPPlus.Graphics/Point.cs b/src/EPPlus.Graphics/Point.cs new file mode 100644 index 000000000..15beb0c5d --- /dev/null +++ b/src/EPPlus.Graphics/Point.cs @@ -0,0 +1,40 @@ +using EPPlus.Graphics.Math; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; + +namespace EPPlus.Graphics +{ + internal class Point : Transform + { + internal double Top + { + get { return LocalPosition.Y; } + set + { + LocalPosition = new Vector2(LocalPosition.X, value); + } + } + + internal double Left + { + get { return LocalPosition.Y; } + set + { + LocalPosition = new Vector2(value, LocalPosition.Y); + } + } + + internal Point() + { + + } + + internal Point(double x, double y) + { + Left = x; + Top = y; + } + } +} From ed24f9f3a3e60e8ec895073b7fe2b0a13e9058a7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ossian=20Edstr=C3=B6m?= Date: Fri, 20 Mar 2026 18:24:46 +0100 Subject: [PATCH 112/151] Verified that new group item functions as expected --- .../GroupItemNewTest.cs | 17 ++++++++++--- .../RenderItems/Shared/GroupItem.cs | 24 +++++++------------ .../RenderItems/SvgItem/SvgGroupItemNew.cs | 13 ++-------- 3 files changed, 25 insertions(+), 29 deletions(-) diff --git a/src/EPPlus.Export.ImageRenderer.Test/GroupItemNewTest.cs b/src/EPPlus.Export.ImageRenderer.Test/GroupItemNewTest.cs index 4fc794ab3..8dbdeef35 100644 --- a/src/EPPlus.Export.ImageRenderer.Test/GroupItemNewTest.cs +++ b/src/EPPlus.Export.ImageRenderer.Test/GroupItemNewTest.cs @@ -115,8 +115,19 @@ public void TestGroupItemRotatingTwoChildrenCorrectly() groupItem.AddChildItem(siblingItem); - parent.Width = groupItem.Bounds.Width; - parent.Height = groupItem.Bounds.Height; + groupItem.SetRotationPointToCenterOfGroup(); + + SvgRenderRectItem centerOfGroupMarker = new SvgRenderRectItem(baseItem, baseItem.Bounds); + centerOfGroupMarker.FillColor = "green"; + centerOfGroupMarker.FillOpacity = 0.8d; + + centerOfGroupMarker.Width = 6; + centerOfGroupMarker.Height = 6; + + centerOfGroupMarker.Left = 30 - (centerOfGroupMarker.Width / 2); + centerOfGroupMarker.Top = 30 - (centerOfGroupMarker.Height / 2); + + baseItem.RenderItems.Add(centerOfGroupMarker); var sb = new StringBuilder(); @@ -125,7 +136,7 @@ public void TestGroupItemRotatingTwoChildrenCorrectly() baseItem.Render(sb); var svgString = sb.ToString(); - SaveTextFileToWorkbook($"svg\\StandAloneTestGroup.svg", svgString); + SaveTextFileToWorkbook($"svg\\TestGroupRotated.svg", svgString); } } } diff --git a/src/EPPlus.Export.ImageRenderer/RenderItems/Shared/GroupItem.cs b/src/EPPlus.Export.ImageRenderer/RenderItems/Shared/GroupItem.cs index 0dbc9978a..99b17038a 100644 --- a/src/EPPlus.Export.ImageRenderer/RenderItems/Shared/GroupItem.cs +++ b/src/EPPlus.Export.ImageRenderer/RenderItems/Shared/GroupItem.cs @@ -16,8 +16,6 @@ internal abstract class GroupItem : RenderItem /// internal Point Position = null; - internal BoundingBox BoundingBoxParent = null; - Point _altRotationPoint = null; internal Point RotationPoint @@ -58,27 +56,23 @@ public GroupItem(DrawingBase renderer, double localXPos, double localYPos) : thi public GroupItem(DrawingBase renderer, BoundingBox parent, double rotation, Transform rotationPoint = null) : this(renderer, 0, 0) { - BoundingBoxParent = parent; + Position.Parent = parent; Rotation = rotation; if (rotationPoint != null) { RotationPoint = new Point(rotationPoint.LocalPosition.X, rotationPoint.LocalPosition.Y); } - //if (rotationPoint != null) - //{ - // SetRotationPoint(rotationPoint); - //} } - //internal void SetRotationPoint(Transform point) - //{ - // _rotationPoint = point; - //} + internal void SetRotationPointToCenterOfGroup(double rotation = double.NaN) + { + RotationPoint = new Point(Bounds.Width/2, Bounds.Height/2); - //internal void SetRotationBounds(BoundingBox rotationBounds) - //{ - // _rotationPoint = rotationBounds; - //} + if (double.IsNaN(rotation) == false) + { + Rotation = rotation; + } + } internal void AddChildItem(RenderItem item) { diff --git a/src/EPPlus.Export.ImageRenderer/RenderItems/SvgItem/SvgGroupItemNew.cs b/src/EPPlus.Export.ImageRenderer/RenderItems/SvgItem/SvgGroupItemNew.cs index 9dfa93dfc..2da802ed6 100644 --- a/src/EPPlus.Export.ImageRenderer/RenderItems/SvgItem/SvgGroupItemNew.cs +++ b/src/EPPlus.Export.ImageRenderer/RenderItems/SvgItem/SvgGroupItemNew.cs @@ -59,19 +59,10 @@ string GetRotationStr() if (double.IsNaN(Rotation) == false) { string rot = Rotation.ToString(CultureInfo.InvariantCulture); - if (BoundingBoxParent != null) - { - var cx = BoundingBoxParent.Width / 2; - var cy = BoundingBoxParent.Height / 2; - if (cx != 0 && cy != 0) - { - rot += $", {cx.ToString(CultureInfo.InvariantCulture)}, {cy.ToString(CultureInfo.InvariantCulture)}"; - } - } - else if(RotationPoint != null && RotationPoint != Position) + if(RotationPoint != null && RotationPoint != Position) { - rot += $", {RotationPoint.Left.ToString(CultureInfo.InvariantCulture)}, {RotationPoint.Top.ToString(CultureInfo.InvariantCulture)}"; + rot += $", {RotationPoint.Left.PointToPixelString()}, {RotationPoint.Top.PointToPixelString()}"; } return string.Format(transformRotate, rot); From f9d8cd0c23193269d14881edba800bb1181adb7f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ossian=20Edstr=C3=B6m?= Date: Mon, 23 Mar 2026 09:07:26 +0100 Subject: [PATCH 113/151] fixed alternative chart-legend handling --- .../Svg/Chart/SvgChartLegend.cs | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/src/EPPlus.Export.ImageRenderer/Svg/Chart/SvgChartLegend.cs b/src/EPPlus.Export.ImageRenderer/Svg/Chart/SvgChartLegend.cs index c41ad5622..ad8ee573e 100644 --- a/src/EPPlus.Export.ImageRenderer/Svg/Chart/SvgChartLegend.cs +++ b/src/EPPlus.Export.ImageRenderer/Svg/Chart/SvgChartLegend.cs @@ -197,7 +197,12 @@ internal void SetLegend(SvgChart sc) case eChartType.LineStacked100: var ls=(ExcelLineChartSerie)s; var tm = _seriesHeadersMeasure[index]; - var prevTm = _seriesHeadersMeasure[index - 1]; + TextMeasurement prevTm = tm; + if (pSls != null) + { + prevTm = _seriesHeadersMeasure[index - 1]; + } + var si = GetSeriesIcon(sc, ls, prevTm, tm, pSls); sls.SeriesIcon = si; From 9f0f321ce4b9cf61a9adac73280356f6f7a9485a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jan=20K=C3=A4llman?= Date: Mon, 23 Mar 2026 11:06:08 +0100 Subject: [PATCH 114/151] WIP:Fixes several failing and hanging tests --- .../Chart/Axis/CategoryAxisScaleCalculator.cs | 4 +- .../Svg/Chart/Axis/DateAxisScaleCalculator.cs | 95 ++++++++++--------- .../Svg/Chart/SvgChartAxis.cs | 36 ++++++- 3 files changed, 85 insertions(+), 50 deletions(-) diff --git a/src/EPPlus.Export.ImageRenderer/Svg/Chart/Axis/CategoryAxisScaleCalculator.cs b/src/EPPlus.Export.ImageRenderer/Svg/Chart/Axis/CategoryAxisScaleCalculator.cs index 74b692870..a84297b6d 100644 --- a/src/EPPlus.Export.ImageRenderer/Svg/Chart/Axis/CategoryAxisScaleCalculator.cs +++ b/src/EPPlus.Export.ImageRenderer/Svg/Chart/Axis/CategoryAxisScaleCalculator.cs @@ -22,7 +22,7 @@ internal static AxisScale CalculateByWidth(ref List values, ITextMeasure var ax = options.Axis; var plotAreaWidth = options.ChartSize.Bounds.Width; var mf = ax.Font.GetMeasureFont(); - var displayValues = values.Select(x => ax.ToString()).ToList(); + var displayValues = values.Select(x => x.ToString()).ToList(); var res = tm.MeasureText(displayValues[0], mf); //Get interval for maximum width with vertical text. @@ -91,9 +91,9 @@ private static bool FitAsHorizontalText(List displayValues, int interval var pos = interval; while (pos < displayValues.Count && width < plotAreaWidth) { - pos += interval; width = tm.MeasureText(displayValues[pos], mf).Width + margin; if(width > plotAreaWidth) return false; + pos += interval; } return width <= plotAreaWidth; } diff --git a/src/EPPlus.Export.ImageRenderer/Svg/Chart/Axis/DateAxisScaleCalculator.cs b/src/EPPlus.Export.ImageRenderer/Svg/Chart/Axis/DateAxisScaleCalculator.cs index cfda9e436..6cff70fbe 100644 --- a/src/EPPlus.Export.ImageRenderer/Svg/Chart/Axis/DateAxisScaleCalculator.cs +++ b/src/EPPlus.Export.ImageRenderer/Svg/Chart/Axis/DateAxisScaleCalculator.cs @@ -300,53 +300,61 @@ private static DateTime CeilingToUnit(double value, eTimeUnit unit, int interval internal static AxisScale CalculateByWidth(double min, double max, ITextMeasurer tm, AxisOptions options) { - //var height = options.ChartSize.Bounds.Height; var ax = options.Axis; var plotAreaWidth = options.ChartSize.Bounds.Width; var mf = ax.Font.GetMeasureFont(); - - var interval = 1; //Day - var unit = eTimeUnit.Days; + int interval; + eTimeUnit unit; var minString = DateTime.FromOADate(min).ToString(options.NumberFormat); var res = tm.MeasureText(minString, mf); - //Get interval for maximum width with vertical text. - while (FitAsVerticalDiagonalText(min, max, interval, unit, res.Height, res.Height*0.3, plotAreaWidth)==false) + if (options.LockedInterval.HasValue) { - AddIntervall(ref interval, ref unit); + interval = (int)options.LockedInterval.Value; + unit = options.LockedIntervalUnit ?? eTimeUnit.Days; } - - //Get max text width when using diagonal text - var width = mf.Size * Math.Sqrt(2); - var margin = mf.Size * 0.5; - - if (FitAsVerticalDiagonalText(min, max, interval, unit, width, margin, plotAreaWidth)) //Check diagonal + else { - if(FitAsHorizontalText(tm, options, min, max, interval, unit, res.Height, plotAreaWidth)) //Check horizontal + interval = 1; + unit = eTimeUnit.Days; + //Get interval for maximum width with vertical text. + while (FitAsVerticalDiagonalText(min, max, interval, unit, res.Height, res.Height * 0.3, plotAreaWidth) == false) { - return new AxisScale() - { - MajorInterval = interval, - MinorInterval = 1, - MinorDateUnit = unit, - MajorDateUnit = unit, - Min = min, - Max = max, - TextOrientation = eTextOrientation.Horizontal - }; + AddIntervall(ref interval, ref unit); } - else + //Get max text width when using diagonal text + var width = mf.Size * Math.Sqrt(2); + var margin = mf.Size * 0.5; + + if (FitAsVerticalDiagonalText(min, max, interval, unit, width, margin, plotAreaWidth)) //Check diagonal { - return new AxisScale() + if (FitAsHorizontalText(tm, options, min, max, interval, unit, res.Height, plotAreaWidth)) //Check horizontal { - MajorInterval = interval, - MinorInterval = 1, - MinorDateUnit = unit, - MajorDateUnit = unit, - Min = min, - Max = max, - TextOrientation = eTextOrientation.Diagonal - }; + return new AxisScale() + { + MajorInterval = interval, + MinorInterval = 1, + MinorDateUnit = unit, + MajorDateUnit = unit, + Min = min, + Max = max, + TextOrientation = eTextOrientation.Horizontal + }; + } + else + { + return new AxisScale() + { + MajorInterval = interval, + MinorInterval = 1, + MinorDateUnit = unit, + MajorDateUnit = unit, + Min = min, + Max = max, + TextOrientation = eTextOrientation.Diagonal + }; + } } + } return new AxisScale() @@ -357,9 +365,8 @@ internal static AxisScale CalculateByWidth(double min, double max, ITextMeasurer MajorDateUnit = unit, Min = min, Max = max, - TextOrientation = eTextOrientation.Vertical + TextOrientation = ax.TextBody.VerticalText==OfficeOpenXml.Drawing.eTextVerticalType.Horizontal ? eTextOrientation.Horizontal : eTextOrientation.Vertical }; - } //private static bool FitAsDiagonalText(double min, double max, int interval, eTimeUnit unit, float size, double plotAreaWidth) @@ -453,29 +460,29 @@ private static bool FitAsVerticalDiagonalText(double min, double max, int interv { var minDate = DateTime.FromOADate(min); var maxDate = DateTime.FromOADate(max); - var date = minDate.AddDays(1).AddMonths(1).AddDays(-1); //Handles last day of month. + var date = minDate.AddDays(1).AddMonths(interval).AddDays(-1); //Handles last day of month. var items = 1; - while(date >= maxDate) + while(date <= maxDate) { items++; if (items * textWidth + (items - 1) * margin > plotAreaWidth) return false; - date = minDate.AddDays(1).AddMonths(1).AddDays(-1); + date = date.AddDays(1).AddMonths(interval).AddDays(-1); } - return items * textWidth + (items - 1) * margin > plotAreaWidth; + return items * textWidth + (items - 1) * margin < plotAreaWidth; } else { var minDate = DateTime.FromOADate(min); var maxDate = DateTime.FromOADate(max); - var date = minDate.AddDays(1).AddYears(1).AddDays(-1); //Handles leap year + var date = minDate.AddDays(1).AddYears(interval).AddDays(-1); //Handles leap year var items = 1; - while (date >= maxDate) + while (date <= maxDate) { items++; if (items * textWidth + (items - 1) * margin > plotAreaWidth) return false; - date = minDate.AddDays(1).AddYears(1).AddDays(-1); + date = date.AddDays(1).AddYears(interval).AddDays(-1); } - return items * textWidth + (items - 1) * margin > plotAreaWidth; + return items * textWidth + (items - 1) * margin < plotAreaWidth; } } diff --git a/src/EPPlus.Export.ImageRenderer/Svg/Chart/SvgChartAxis.cs b/src/EPPlus.Export.ImageRenderer/Svg/Chart/SvgChartAxis.cs index 0357fae8a..fb223c34d 100644 --- a/src/EPPlus.Export.ImageRenderer/Svg/Chart/SvgChartAxis.cs +++ b/src/EPPlus.Export.ImageRenderer/Svg/Chart/SvgChartAxis.cs @@ -306,10 +306,12 @@ internal void AddTickmarksAndValues() private List GetAxisValueTextBoxes() { + var ret = new List(); + if (Axis.LabelPosition == eTickLabelPosition.None) return ret; + var tm = Chart.WorkSheet._package.Settings.TextSettings.GenericTextMeasurerTrueType; var mf = Axis.Font.GetMeasureFont(); var axisStyle = GetAxisStyleEntry(); - var ret= new List(); double maxWidth, maxHeight; if(Axis.AxisPosition==eAxisPosition.Left || Axis.AxisPosition == eAxisPosition.Right) { @@ -529,10 +531,36 @@ private double GetAxisItemTop(int i, OfficeOpenXml.Interfaces.Drawing.Text.TextM switch(LabelOrientation) { case eTextOrientation.Vertical: + if (Axis.LabelPosition == eTickLabelPosition.Low) + { + return Rectangle.Bottom - m.Width; + } + else + { + return Rectangle.Top; + } case eTextOrientation.Diagonal: - return Rectangle.Top; + if (Axis.LabelPosition == eTickLabelPosition.Low) + { + return Rectangle.Bottom - (m.Width+m.Height)* COS45; + } + else + { + return Rectangle.Top; + } default: - return Rectangle.Top + BottomMargin; + if (Axis.LabelPosition == eTickLabelPosition.Low) + { + return Rectangle.Bottom - m.Height; + } + else if (Axis.LabelPosition == eTickLabelPosition.NextTo) + { + return Rectangle.Top + BottomMargin; + } + else //TODO:Add support for hight. + { + return Rectangle.Top + BottomMargin; + } } } else @@ -835,7 +863,7 @@ protected List GetAxisValue(ExcelChartAxisStandard ax, RenderItem rect, dateUnit = null; orientation = res.TextOrientation; - return values.ToList(); + return l.ToList(); } if (ax.IsDate) { From e32bdd3a86dec2925acd1f9061f855de579f5fa6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ossian=20Edstr=C3=B6m?= Date: Mon, 23 Mar 2026 14:44:00 +0100 Subject: [PATCH 115/151] Added test options to imagerenderer --- .../ImageRenderer.cs | 90 ++++++++++++++++++- 1 file changed, 89 insertions(+), 1 deletion(-) diff --git a/src/EPPlus.Export.ImageRenderer/ImageRenderer.cs b/src/EPPlus.Export.ImageRenderer/ImageRenderer.cs index 60317d447..aa2e5fc5e 100644 --- a/src/EPPlus.Export.ImageRenderer/ImageRenderer.cs +++ b/src/EPPlus.Export.ImageRenderer/ImageRenderer.cs @@ -48,7 +48,82 @@ public string RenderDrawingToSvg(ExcelDrawing drawing) throw new NotImplementedException("Image rendering for drawing type not implemented."); } - public string RenderTest() + public enum RenderPresets + { + ContainerMargins, + RotatingContainer + } + + public string RenderTest(RenderPresets preset) + { + return GenerateFromPreset(preset); + } + + + private string rotatingContainer() + { + var baseBB = new BoundingBox(); + + //96x96 px + baseBB.Width = 72; + baseBB.Height = 72; + + var baseItem = new DrawingItemForTesting(baseBB); + + BoundingBox parent = new BoundingBox(); + + var groupItem = new SvgGroupItemNew(baseItem, parent, 45); + + groupItem.Position.Left = 10; + groupItem.Position.Top = 10; + + SvgRenderRectItem rectItem = new SvgRenderRectItem(baseItem, groupItem.Bounds); + + rectItem.FillColor = "red"; + rectItem.FillOpacity = 0.2d; + + rectItem.Width = 20; + rectItem.Height = 20; + + groupItem.AddChildItem(rectItem); + + + SvgRenderRectItem siblingItem = new SvgRenderRectItem(baseItem, groupItem.Bounds); + siblingItem.FillColor = "blue"; + siblingItem.FillOpacity = 0.2d; + + siblingItem.Width = 20; + siblingItem.Height = 20; + + siblingItem.Bounds.Left = 20; + siblingItem.Bounds.Top = 20; + + groupItem.AddChildItem(siblingItem); + + groupItem.SetRotationPointToCenterOfGroup(); + + SvgRenderRectItem centerOfGroupMarker = new SvgRenderRectItem(baseItem, baseItem.Bounds); + centerOfGroupMarker.FillColor = "green"; + centerOfGroupMarker.FillOpacity = 0.8d; + + centerOfGroupMarker.Width = 6; + centerOfGroupMarker.Height = 6; + + centerOfGroupMarker.Left = 30 - (centerOfGroupMarker.Width / 2); + centerOfGroupMarker.Top = 30 - (centerOfGroupMarker.Height / 2); + + baseItem.RenderItems.Add(centerOfGroupMarker); + + var sb = new StringBuilder(); + + baseItem.RenderItems.Add(groupItem); + + baseItem.Render(sb); + + return sb.ToString(); + } + + private string containerMargins() { var baseBB = new BoundingBox(); @@ -86,6 +161,19 @@ public string RenderTest() return sb.ToString(); } + + private string GenerateFromPreset(RenderPresets preset) + { + switch (preset) + { + case RenderPresets.ContainerMargins: + return containerMargins(); + case RenderPresets.RotatingContainer: + return rotatingContainer(); + } + return ""; + } + //public string RenderTestCanvas(double widthPixel, double heightPixel, Color bgColor) //{ From 14d4749d4407a4352e5fa9ab2221397ab5b0a6ee Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jan=20K=C3=A4llman?= Date: Mon, 23 Mar 2026 14:51:18 +0100 Subject: [PATCH 116/151] WIP:Start work on column/bar charts --- .../SvgPathTests.cs | 22 +++ .../Chart/ChartTypeDrawers/ChartTypeDrawer.cs | 3 + .../ChartTypeDrawers/ColumnChartTypeDrawer.cs | 180 ++++++++++++++++++ .../Svg/Chart/SvgChart.cs | 5 +- .../Svg/Chart/SvgChartAxis.cs | 4 +- 5 files changed, 211 insertions(+), 3 deletions(-) create mode 100644 src/EPPlus.Export.ImageRenderer/Svg/Chart/ChartTypeDrawers/ColumnChartTypeDrawer.cs diff --git a/src/EPPlus.Export.ImageRenderer.Test/SvgPathTests.cs b/src/EPPlus.Export.ImageRenderer.Test/SvgPathTests.cs index a01feb498..1497d282f 100644 --- a/src/EPPlus.Export.ImageRenderer.Test/SvgPathTests.cs +++ b/src/EPPlus.Export.ImageRenderer.Test/SvgPathTests.cs @@ -526,6 +526,28 @@ public void GenerateSvgForCharts() } } } + [TestMethod, Ignore] + public void GenerateSvgForColumnCharts() + { + ExcelPackage.License.SetNonCommercialOrganization("EPPlus Project"); + using (var p = OpenTemplatePackage("ColumnChartForSvg.xlsx")) + { + var ws = p.Workbook.Worksheets[0]; + var renderer = new EPPlusImageRenderer.ImageRenderer(); + + //var ix = 1; + //var c = ws.Drawings[ix]; + //var svg = renderer.RenderDrawingToSvg(c); + //SaveTextFileToWorkbook($"svg\\ChartForSvg_ind{ix++}.svg", svg); + + var ix = 0; + foreach (ExcelChart c in ws.Drawings) + { + var svg = renderer.RenderDrawingToSvg(c); + SaveTextFileToWorkbook($"svg\\ChartForSvg{ix++}.svg", svg); + } + } + } [TestMethod] public void GenerateSimplestChart() diff --git a/src/EPPlus.Export.ImageRenderer/Svg/Chart/ChartTypeDrawers/ChartTypeDrawer.cs b/src/EPPlus.Export.ImageRenderer/Svg/Chart/ChartTypeDrawers/ChartTypeDrawer.cs index 322f3a505..33b7cbab2 100644 --- a/src/EPPlus.Export.ImageRenderer/Svg/Chart/ChartTypeDrawers/ChartTypeDrawer.cs +++ b/src/EPPlus.Export.ImageRenderer/Svg/Chart/ChartTypeDrawers/ChartTypeDrawer.cs @@ -73,6 +73,9 @@ internal static List Create(SvgChart svgChart) case eChartType.LineMarkersStacked100: drawers.Add(new LineChartTypeDrawer(svgChart, ct)); break; + case eChartType.ColumnClustered: + drawers.Add(new ColumnChartTypeDrawer(svgChart, ct)); + break; default: throw new NotImplementedException($"No Svg support for Chart type {ct} is implemented."); } diff --git a/src/EPPlus.Export.ImageRenderer/Svg/Chart/ChartTypeDrawers/ColumnChartTypeDrawer.cs b/src/EPPlus.Export.ImageRenderer/Svg/Chart/ChartTypeDrawers/ColumnChartTypeDrawer.cs new file mode 100644 index 000000000..6c96910ba --- /dev/null +++ b/src/EPPlus.Export.ImageRenderer/Svg/Chart/ChartTypeDrawers/ColumnChartTypeDrawer.cs @@ -0,0 +1,180 @@ +using EPPlus.Export.ImageRenderer.RenderItems.SvgItem; +using EPPlus.Graphics; +using EPPlusImageRenderer.RenderItems; +using EPPlusImageRenderer.Svg; +using OfficeOpenXml.Drawing.Chart; +using OfficeOpenXml.Utils.TypeConversion; +using System.Collections.Generic; + +namespace EPPlus.Export.ImageRenderer.Svg.Chart +{ + internal class ColumnChartTypeDrawer : ChartTypeDrawer + { + List serieDataLabels = new List(); + List> dataPointsPerSerie = new List>(); + + internal ColumnChartTypeDrawer(SvgChart svgChart, ExcelChart chartType) : base(svgChart, chartType) + { + var groupItem = new SvgGroupItem(ChartRenderer, _svgChart.Plotarea.Rectangle.Bounds); + RenderItems.Add(groupItem); + var isStacked = chartType.IsTypeStacked(); + var isPercentStacked = chartType.IsTypePercentStacked(); + var xValues = new List>(); + var yValues = new List>(); + int serCounter = 0; + + foreach (ExcelLineChartSerie serie in chartType.Series) + { + var yValue = LoadSeriesValues(serie.Series, serie.NumberLiteralsY, serie.StringLiteralsY); + var xValue = LoadSeriesValues(serie.XSeries, serie.NumberLiteralsX, serie.StringLiteralsX); + + xValues.Add(xValue); + yValues.Add(yValue); + + if (serie.HasDataLabel) + { + var datalabel = new SvgChartSerieDataLabel(svgChart, serie.DataLabel, svgChart.Bounds, serie, xValue, yValue, serCounter); + serieDataLabels.Add(datalabel); + } + serCounter++; + } + + if (chartType.IsTypeStacked()) + { + SumSeries(yValues); + } + else if (chartType.IsTypePercentStacked()) + { + ExcelChartAxisStandard.CalculateStacked100(yValues); + } + + for(var i= 0; i < xValues.Count; i++) + { + var xSerie = xValues[i]; + var ySerie = yValues[i]; + var serie = (ExcelLineChartSerie)chartType.Series[i]; + + var dataPoints = new List(); + + AddLine(chartType, serie, xSerie, ySerie, dataPoints); + + dataPointsPerSerie.Add(dataPoints); + + if (serie.HasDataLabel) + { + for (int j = 0; j < dataPoints.Count; j++) + { + serieDataLabels[i].SetParentPoint(dataPoints[j], j); + } + } + } + RenderItems.Add(new SvgEndGroupItem(ChartRenderer, null)); + + foreach (var dataLabel in serieDataLabels) + { + dataLabel.AppendRenderItems(RenderItems); + } + } + private void SumSeries(List> series) + { + for(var i=1;i < series.Count;i++) + { + for(var j=0;j < series[i].Count;j++) + { + series[i][j] = ConvertUtil.GetValueDouble(series[i][j]) + ConvertUtil.GetValueDouble(series[i-1][j]); + } + } + } + private void AddLine(ExcelChart chartType, ExcelLineChartSerie serie, List xValues, List yValues, List dataPoints) + { + SvgChartAxis yAxis, xAxis; + if (chartType.UseSecondaryAxis) + { + yAxis = _svgChart.SecondVerticalAxis; + xAxis = _svgChart.SecondHorizontalAxis; + if(xAxis.Axis.Deleted && xAxis.Values==null) + { + xAxis = _svgChart.HorizontalAxis; + } + } + else + { + yAxis = _svgChart.VerticalAxis; + xAxis = _svgChart.HorizontalAxis; + } + var linePath = new SvgRenderPathItem(ChartRenderer, _svgChart.Plotarea.Rectangle.Bounds); + var coords = new List(); + var markerItems = new List(); + + for (var i = 0; i < yValues.Count; i++) + { + double x; + if (xValues == null || xAxis.Axis.AxisType==eAxisType.Cat) + { + x = (double)i; + } + else + { + x = ConvertUtil.GetValueDouble(xValues[i], false, true); + } + + var y = ConvertUtil.GetValueDouble(yValues[i], false, true); + var xPos = xAxis.GetPositionInPlotarea(x); + var yPos = yAxis.GetPositionInPlotarea(y); + + BoundingBox pt = null; + + if (double.IsNaN(yPos) == false) + { + coords.Add(xPos / _svgChart.Plotarea.Rectangle.Bounds.Width); + coords.Add(yPos / _svgChart.Plotarea.Rectangle.Bounds.Height); + + //Log point within chart coordinate system + pt = new BoundingBox(xPos, yPos, 0, 0); + pt.Parent = _svgChart.Plotarea.Bounds; + } + + if (serie.HasMarker() && serie.Marker.Style != eMarkerStyle.None) + { + float mx = (float)xPos; + float my = (float)yPos; + var ls = LineMarkerHelper.GetMarkerItem(_svgChart, serie, mx, my, false); + if ((serie.Marker.Style == eMarkerStyle.Plus || serie.Marker.Style == eMarkerStyle.X || serie.Marker.Style == eMarkerStyle.Star) && + serie.Marker.Fill.IsEmpty == false) + { + markerItems.Add(LineMarkerHelper.GetMarkerBackground(_svgChart, serie, mx, my, false)); + } + markerItems.Add(ls); + + if (pt != null) + { + pt.Width = ls.Bounds.Width; + pt.Height = ls.Bounds.Height; + dataPoints.Add(pt); + } + } + else + { + + if (pt != null) + { + //Default values in excel + pt.Width = 5; + pt.Height = 5; + dataPoints.Add(pt); + } + } + } + + linePath.Commands.Add(new EPPlusImageRenderer.PathCommands(PathCommandType.Move, linePath, coords.ToArray())); + linePath.SetDrawingPropertiesBorder(serie.Border, chartType.StyleManager.Style.SeriesLine.BorderReference.Color, true); + linePath.SetDrawingPropertiesEffects(serie.Effect); + linePath.FillColor = "none"; //No fill for line + linePath.StrokeMiterLimit = 4; //A much higher value of the miter limit, might cause the "spike" to get beyond the data point on the vertical scale.. + linePath.LineJoin = SvgLineJoin.Round; + RenderItems.Add(linePath); + RenderItems.AddRange(markerItems); + } + } + +} diff --git a/src/EPPlus.Export.ImageRenderer/Svg/Chart/SvgChart.cs b/src/EPPlus.Export.ImageRenderer/Svg/Chart/SvgChart.cs index 7398e399b..e457ed49e 100644 --- a/src/EPPlus.Export.ImageRenderer/Svg/Chart/SvgChart.cs +++ b/src/EPPlus.Export.ImageRenderer/Svg/Chart/SvgChart.cs @@ -17,6 +17,7 @@ Date Author Change using EPPlusImageRenderer.Utils; using OfficeOpenXml.Drawing; using OfficeOpenXml.Drawing.Chart; +using OfficeOpenXml.FormulaParsing.Excel.Functions.MathFunctions; using OfficeOpenXml.Interfaces.Drawing.Text; using System; using System.Collections.Generic; @@ -168,7 +169,9 @@ private void PlaceVerticalAxis(SvgChart sc, SvgChartAxis verticalAxis) verticalAxis.Title.Rectangle.Height = Plotarea.Rectangle.Height; verticalAxis.Title.Rectangle.Width = sc.Bounds.Width / 4; verticalAxis.Title.InitTextBox(); - verticalAxis.Title.TextBox.Top = Plotarea.Rectangle.Top + (Plotarea.Rectangle.Height / 2) - (verticalAxis.Title.TextBox.Height / 2); + var sinRot = Math.Abs(Math.Sin(MathHelper.Radians(verticalAxis.Title.TextBox.Rotation))); + var cosRot = Math.Abs(Math.Cos(MathHelper.Radians(verticalAxis.Title.TextBox.Rotation))); + verticalAxis.Title.TextBox.Top = Plotarea.Rectangle.Top + (Plotarea.Rectangle.Height / 2) + ((verticalAxis.Title.TextBox.Height * cosRot + verticalAxis.Title.TextBox.Width * sinRot) / 2); if (verticalAxis.Axis.AxisPosition == eAxisPosition.Left) { diff --git a/src/EPPlus.Export.ImageRenderer/Svg/Chart/SvgChartAxis.cs b/src/EPPlus.Export.ImageRenderer/Svg/Chart/SvgChartAxis.cs index fb223c34d..6087eb3cd 100644 --- a/src/EPPlus.Export.ImageRenderer/Svg/Chart/SvgChartAxis.cs +++ b/src/EPPlus.Export.ImageRenderer/Svg/Chart/SvgChartAxis.cs @@ -93,7 +93,7 @@ internal SvgChartAxis(SvgChart sc, ExcelChartAxisStandard ax) : base(sc) { lp = sc.Legend.Rectangle.Left + -Rectangle.Width; } - Rectangle.Left = Title == null ? lp : Title.Rectangle.Left - Rectangle.Width; + Rectangle.Left = Title == null ? lp : Title.Rectangle.Left - Rectangle.Width-LeftMargin; } } else @@ -298,7 +298,7 @@ internal void AddTickmarksAndValues() { } - if (AxisValues != null && AxisValues.Count > 0 && Axis.Deleted==false) + if (AxisValues != null && AxisValues.Count > 0 && Axis.Deleted==false && Axis.LabelPosition != eTickLabelPosition.None) { AxisValuesTextBoxes = GetAxisValueTextBoxes(); } From 3b9f6f4843849e1182c254d488a7250aeb11f94a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ossian=20Edstr=C3=B6m?= Date: Tue, 24 Mar 2026 17:33:16 +0100 Subject: [PATCH 117/151] storage commit for proof-of-concept ideas --- .../ImageRenderer.cs | 92 +++++++++++++++++++ .../RenderItems/Interfaces/IRectItem.cs | 15 +++ .../RenderItems/Interfaces/IStylingInfo.cs | 37 ++++++++ .../Interfaces/IStylingInfoBase.cs | 12 +++ 4 files changed, 156 insertions(+) create mode 100644 src/EPPlus.Export.ImageRenderer/RenderItems/Interfaces/IRectItem.cs create mode 100644 src/EPPlus.Export.ImageRenderer/RenderItems/Interfaces/IStylingInfo.cs create mode 100644 src/EPPlus.Export.ImageRenderer/RenderItems/Interfaces/IStylingInfoBase.cs diff --git a/src/EPPlus.Export.ImageRenderer/ImageRenderer.cs b/src/EPPlus.Export.ImageRenderer/ImageRenderer.cs index aa2e5fc5e..48b6e3858 100644 --- a/src/EPPlus.Export.ImageRenderer/ImageRenderer.cs +++ b/src/EPPlus.Export.ImageRenderer/ImageRenderer.cs @@ -11,17 +11,22 @@ Date Author Change 27/11/2025 EPPlus Software AB EPPlus 9 *************************************************************************************************/ +using EPPlus.Export.ImageRenderer.RenderItems.Shared; using EPPlus.Export.ImageRenderer.RenderItems.SvgItem; using EPPlus.Export.ImageRenderer.Svg; using EPPlus.Export.ImageRenderer.Svg.NodeAttributes; using EPPlus.Export.ImageRenderer.Svg.Writer; +using EPPlus.Fonts.OpenType.Utils; using EPPlus.Graphics; using EPPlusImageRenderer.RenderItems; using EPPlusImageRenderer.Svg; using OfficeOpenXml.Drawing; using OfficeOpenXml.Drawing.Chart; using OfficeOpenXml.Utils; +using OfficeOpenXml.Utils.EnumUtils; using System; +using System.Collections.Generic; +using System.Drawing; using System.IO; using System.Text; @@ -48,6 +53,47 @@ public string RenderDrawingToSvg(ExcelDrawing drawing) throw new NotImplementedException("Image rendering for drawing type not implemented."); } + public enum RenderItemClasses + { + Rect, + TextBox, + Shape + } + + public Dictionary GetItemProperties(RenderItemClasses item) + { + switch (item) + { + case RenderItemClasses.Rect: + return new Dictionary { { "Top", 10d }, { "Left", 10d }, { "Width", 10d }, { "Height", 10d }, {"Opacity", 0.8 }, {"Fill", Color.Goldenrod } }; + case RenderItemClasses.TextBox: + default: + throw new NotImplementedException("This class has not been implemented as an option yet"); + } + } + + /// + /// For Testing the specific renderItem class + /// + /// + /// + /// + /// + public string RenderIndividualClass(RenderItemClasses item, Dictionary itemProperties, double width, double height) + { + var svgCanvas = new DrawingItemForTesting(new BoundingBox(width.PixelToPoint(), height.PixelToPoint())); + + var renderItem = GenerateFromClasses(item, svgCanvas, itemProperties); + + svgCanvas.RenderItems.Add(renderItem); + + var sb = new StringBuilder(); + + svgCanvas.Render(sb); + + return sb.ToString(); + } + public enum RenderPresets { ContainerMargins, @@ -174,6 +220,52 @@ private string GenerateFromPreset(RenderPresets preset) return ""; } + private RenderItem GenerateRect(DrawingBase baseItem, Dictionary itemProperties) + { + var rectItem = new SvgRenderRectItem(baseItem, baseItem.Bounds); + + if (itemProperties.ContainsKey("Top")) + { + rectItem.Top = (double)itemProperties["Top"]; + } + if (itemProperties.ContainsKey("Left")) + { + rectItem.Left = (double)itemProperties["Left"]; + } + if (itemProperties.ContainsKey("Width")) + { + rectItem.Width = (double)itemProperties["Width"]; + } + if (itemProperties.ContainsKey("Height")) + { + rectItem.Height = (double)itemProperties["Height"]; + } + if (itemProperties.ContainsKey("Opacity")) + { + rectItem.FillOpacity = (double)itemProperties["Opacity"]; + } + if (itemProperties.ContainsKey("Fill")) + { + rectItem.FillColor = "#" + ((Color)itemProperties["Fill"]).ToColorString(); + } + + return rectItem; + } + + private RenderItem GenerateFromClasses(RenderItemClasses preset, DrawingBase baseItem, Dictionary itemProperties) + { + switch (preset) + { + case RenderItemClasses.Rect: + return GenerateRect(baseItem, itemProperties); + case RenderItemClasses.TextBox: + + + default: + throw new NotImplementedException("This class has not been implemented as an option yet"); + } + } + //public string RenderTestCanvas(double widthPixel, double heightPixel, Color bgColor) //{ diff --git a/src/EPPlus.Export.ImageRenderer/RenderItems/Interfaces/IRectItem.cs b/src/EPPlus.Export.ImageRenderer/RenderItems/Interfaces/IRectItem.cs new file mode 100644 index 000000000..ef091d764 --- /dev/null +++ b/src/EPPlus.Export.ImageRenderer/RenderItems/Interfaces/IRectItem.cs @@ -0,0 +1,15 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; + +namespace EPPlus.Export.ImageRenderer.RenderItems.Interfaces +{ + internal interface IRectItem + { + double Top { get; set; } + double Left { get; set; } + double Height { get; set; } + double Width { get; set; } + } +} diff --git a/src/EPPlus.Export.ImageRenderer/RenderItems/Interfaces/IStylingInfo.cs b/src/EPPlus.Export.ImageRenderer/RenderItems/Interfaces/IStylingInfo.cs new file mode 100644 index 000000000..857dc4327 --- /dev/null +++ b/src/EPPlus.Export.ImageRenderer/RenderItems/Interfaces/IStylingInfo.cs @@ -0,0 +1,37 @@ +using EPPlusImageRenderer.RenderItems; +using OfficeOpenXml.Drawing; +using OfficeOpenXml.Drawing.Style.Effect; +using OfficeOpenXml.Drawing.Style.Fill; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; + +namespace EPPlus.Export.ImageRenderer.RenderItems.Interfaces +{ + public interface StylingInfo + { + string FillColor { get; set; } + string FilterName { get; set; } + //public DrawGradientFill GradientFill { get; set; } + //SvgFillType FillType { get; set; } + double? FillOpacity { get; set; } + string BorderColor { get; set; } + //DrawGradientFill BorderGradientFill { get; set; } + ExcelDrawingPatternFill PatternFill { get; } + ExcelDrawingBlipFill BlipFill { get; } + double? BorderWidth { get; set; } + double[] BorderDashArray { get; set; } + int StrokeMiterLimit { get; set; } + eCompoundLineStyle CompoundLineStyle { get; set; } + double? BorderDashOffset { get; set; } + eLineCap LineCap { get; set; } + //SvgLineJoin LineJoin { get; set; } + double? BorderOpacity { get; set; } + PathFillMode FillColorSource { get; set; } + PathFillMode BorderColorSource { get; set; } + double? GlowRadius { get; } + string GlowColor { get; } + ExcelDrawingOuterShadowEffect OuterShadowEffect { get; } + } +} diff --git a/src/EPPlus.Export.ImageRenderer/RenderItems/Interfaces/IStylingInfoBase.cs b/src/EPPlus.Export.ImageRenderer/RenderItems/Interfaces/IStylingInfoBase.cs new file mode 100644 index 000000000..12ecfe976 --- /dev/null +++ b/src/EPPlus.Export.ImageRenderer/RenderItems/Interfaces/IStylingInfoBase.cs @@ -0,0 +1,12 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; + +namespace EPPlus.Export.ImageRenderer.RenderItems.Interfaces +{ + internal interface IStylingInfoBase + { + + } +} From 113e47b79bb93f178a1d80bd8ecc35234cdf623d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jan=20K=C3=A4llman?= Date: Wed, 25 Mar 2026 07:52:03 +0100 Subject: [PATCH 118/151] WIP:Work on column charts --- .../SvgPathTests.cs | 28 ++- .../BarColumnChartTypeDrawer.cs | 154 ++++++++++++ .../Chart/ChartTypeDrawers/ChartTypeDrawer.cs | 21 +- .../ChartTypeDrawers/ColumnChartTypeDrawer.cs | 180 -------------- .../ChartTypeDrawers/LineChartTypeDrawer.cs | 2 +- .../Svg/Chart/SvgChartLegend.cs | 221 +++++++++++++----- .../Svg/Chart/SvgChartLegendIcon.cs | 2 +- .../Utils/SvgDrawingWriter.cs | 6 +- .../Drawing/Chart/ExcelChartAxisStandard.cs | 21 +- src/EPPlus/Drawing/Chart/ExcelChartSerie.cs | 7 +- 10 files changed, 384 insertions(+), 258 deletions(-) create mode 100644 src/EPPlus.Export.ImageRenderer/Svg/Chart/ChartTypeDrawers/BarColumnChartTypeDrawer.cs delete mode 100644 src/EPPlus.Export.ImageRenderer/Svg/Chart/ChartTypeDrawers/ColumnChartTypeDrawer.cs diff --git a/src/EPPlus.Export.ImageRenderer.Test/SvgPathTests.cs b/src/EPPlus.Export.ImageRenderer.Test/SvgPathTests.cs index 1497d282f..1699959db 100644 --- a/src/EPPlus.Export.ImageRenderer.Test/SvgPathTests.cs +++ b/src/EPPlus.Export.ImageRenderer.Test/SvgPathTests.cs @@ -526,8 +526,8 @@ public void GenerateSvgForCharts() } } } - [TestMethod, Ignore] - public void GenerateSvgForColumnCharts() + [TestMethod] + public void GenerateSvgForColumnCharts1() { ExcelPackage.License.SetNonCommercialOrganization("EPPlus Project"); using (var p = OpenTemplatePackage("ColumnChartForSvg.xlsx")) @@ -544,7 +544,29 @@ public void GenerateSvgForColumnCharts() foreach (ExcelChart c in ws.Drawings) { var svg = renderer.RenderDrawingToSvg(c); - SaveTextFileToWorkbook($"svg\\ChartForSvg{ix++}.svg", svg); + SaveTextFileToWorkbook($"svg\\BarChartForSvg_sheet1_{ix++}.svg", svg); + } + } + } + [TestMethod] + public void GenerateSvgForColumnCharts2() + { + ExcelPackage.License.SetNonCommercialOrganization("EPPlus Project"); + using (var p = OpenTemplatePackage("ColumnChartForSvg.xlsx")) + { + var ws = p.Workbook.Worksheets[1]; + var renderer = new EPPlusImageRenderer.ImageRenderer(); + + //var ix = 1; + //var c = ws.Drawings[ix]; + //var svg = renderer.RenderDrawingToSvg(c); + //SaveTextFileToWorkbook($"svg\\ChartForSvg_ind{ix++}.svg", svg); + + var ix = 0; + foreach (ExcelChart c in ws.Drawings) + { + var svg = renderer.RenderDrawingToSvg(c); + SaveTextFileToWorkbook($"svg\\BarChartForSvg_sheet2_{ix++}.svg", svg); } } } diff --git a/src/EPPlus.Export.ImageRenderer/Svg/Chart/ChartTypeDrawers/BarColumnChartTypeDrawer.cs b/src/EPPlus.Export.ImageRenderer/Svg/Chart/ChartTypeDrawers/BarColumnChartTypeDrawer.cs new file mode 100644 index 000000000..d36553e05 --- /dev/null +++ b/src/EPPlus.Export.ImageRenderer/Svg/Chart/ChartTypeDrawers/BarColumnChartTypeDrawer.cs @@ -0,0 +1,154 @@ +using EPPlus.Export.ImageRenderer.RenderItems.SvgItem; +using EPPlus.Graphics; +using EPPlusImageRenderer.RenderItems; +using EPPlusImageRenderer.Svg; +using OfficeOpenXml.Drawing.Chart; +using OfficeOpenXml.FormulaParsing.Excel.Functions.RefAndLookup; +using OfficeOpenXml.Utils.TypeConversion; +using System; +using System.Collections.Generic; +using System.Linq; + +namespace EPPlus.Export.ImageRenderer.Svg.Chart +{ + internal class BarColumnChartTypeDrawer : ChartTypeDrawer + { + List serieDataLabels = new List(); + List> dataPointsPerSerie = new List>(); + + internal BarColumnChartTypeDrawer(SvgChart svgChart, ExcelBarChart chartType) : base(svgChart, chartType) + { + var groupItem = new SvgGroupItem(ChartRenderer, _svgChart.Plotarea.Rectangle.Bounds); + RenderItems.Add(groupItem); + var xValues = new List>(); + var yValues = new List>(); + int serCounter = 0; + + foreach (ExcelBarChartSerie serie in chartType.Series) + { + var yValue = LoadSeriesValues(serie.Series, serie.NumberLiteralsY, serie.StringLiteralsY); + var xValue = LoadSeriesValues(serie.XSeries, serie.NumberLiteralsX, serie.StringLiteralsX); + + xValues.Add(xValue); + yValues.Add(yValue); + + //if (serie.HasDataLabel) + //{ + // var datalabel = new SvgChartSerieDataLabel(svgChart, serie.DataLabel, svgChart.Bounds, serie, xValue, yValue, serCounter); + // serieDataLabels.Add(datalabel); + //} + serCounter++; + } + + if(chartType.IsTypeStacked()) + { + SumSeries(yValues); + } + else if (chartType.IsTypePercentStacked()) + { + ExcelChartAxisStandard.CalculateStacked100(yValues); + } + //var xCount = xValues.Select(x => x.Count).Sum(y => y); + //var yCount = yValues.Select(x => x.Count).Sum(y => y); + var count = Math.Min(xValues.Count, yValues.Count); + for (var i= 0; i < xValues.Count; i++) + { + var xSerie = xValues[i]; + var ySerie = yValues[i]; + var serie = (ExcelBarChartSerie)chartType.Series[i]; + + var dataPoints = new List(); + + AddColumn(chartType, serie, xSerie, ySerie, dataPoints, count, i); + + dataPointsPerSerie.Add(dataPoints); + + //if (serie.HasDataLabel) + //{ + // for (int j = 0; j < dataPoints.Count; j++) + // { + // serieDataLabels[i].SetParentPoint(dataPoints[j], j); + // } + //} + } + RenderItems.Add(new SvgEndGroupItem(ChartRenderer, null)); + + foreach (var dataLabel in serieDataLabels) + { + dataLabel.AppendRenderItems(RenderItems); + } + } + private void AddColumn(ExcelBarChart chartType, ExcelBarChartSerie serie, List xValues, List yValues, List dataPoints, int seriesCount, int position) + { + SvgChartAxis yAxis, xAxis; + if (chartType.UseSecondaryAxis) + { + yAxis = _svgChart.SecondVerticalAxis; + xAxis = _svgChart.SecondHorizontalAxis; + if(xAxis.Axis.Deleted && xAxis.Values==null) + { + xAxis = _svgChart.HorizontalAxis; + } + } + else + { + yAxis = _svgChart.VerticalAxis; + xAxis = _svgChart.HorizontalAxis; + } + var slotSize = yValues.Count; + + var gapPercent = chartType.GapWidth / 100D; //Gap width between bars/columns in percent + var overlapPercent = chartType.Overlap / 100D; //Overlap between bars/columns in percent + var slotWidth = _svgChart.Plotarea.Rectangle.Width / seriesCount; + var slotGap = slotWidth * gapPercent; + double barWidth; + if(slotSize > 1) + { + barWidth = 1 + (slotSize - 1) * (1 - overlapPercent); + } + else + { + barWidth = slotWidth * (1+overlapPercent); + } + //barWidth = barWidth * overlapPercent; + //var xl = barWidth / 2; + var yAxisStart = yAxis.GetPositionInPlotarea(yAxis.Min); + for (var i = 0; i < yValues.Count; i++) + { + double x; + if (xValues == null || xAxis.Axis.AxisType==eAxisType.Cat) + { + x = (double)position + i; + } + else + { + x = ConvertUtil.GetValueDouble(xValues[i], false, true); + } + + var y = ConvertUtil.GetValueDouble(yValues[i], false, true); + double xPos; + if (yValues.Count == 1) + { + xPos = xAxis.GetPositionInPlotarea(x); + } + else + { + xPos = xAxis.GetPositionInPlotarea(x) + (barWidth + barWidth * -overlapPercent) * position; + } + var yPos = yAxis.GetPositionInPlotarea(y); + + var rect = new SvgRenderRectItem(ChartRenderer, _svgChart.Plotarea.Rectangle.Bounds); + rect.Left = xPos - barWidth / 2; + rect.Width = barWidth; + rect.Top = yPos; + rect.Height = yAxisStart - yPos; + rect.SetDrawingPropertiesFill(serie.Fill, chartType.StyleManager.Style.SeriesAxis.FillReference.Color); + rect.SetDrawingPropertiesBorder(serie.Border, chartType.StyleManager.Style.SeriesAxis.BorderReference.Color, true); + rect.SetDrawingPropertiesEffects(serie.Effect); + RenderItems.Add(rect); + } + + } + } + +} diff --git a/src/EPPlus.Export.ImageRenderer/Svg/Chart/ChartTypeDrawers/ChartTypeDrawer.cs b/src/EPPlus.Export.ImageRenderer/Svg/Chart/ChartTypeDrawers/ChartTypeDrawer.cs index 33b7cbab2..994508044 100644 --- a/src/EPPlus.Export.ImageRenderer/Svg/Chart/ChartTypeDrawers/ChartTypeDrawer.cs +++ b/src/EPPlus.Export.ImageRenderer/Svg/Chart/ChartTypeDrawers/ChartTypeDrawer.cs @@ -6,6 +6,7 @@ using OfficeOpenXml.Drawing.Chart.ChartEx; using OfficeOpenXml.FormulaParsing.Excel.Functions.Finance; using OfficeOpenXml.FormulaParsing.Excel.Functions.RefAndLookup; +using OfficeOpenXml.Utils.TypeConversion; using System; using System.Collections.Generic; using System.Linq; @@ -71,10 +72,15 @@ internal static List Create(SvgChart svgChart) case eChartType.LineStacked100: case eChartType.LineMarkersStacked: case eChartType.LineMarkersStacked100: - drawers.Add(new LineChartTypeDrawer(svgChart, ct)); + drawers.Add(new LineChartTypeDrawer(svgChart, (ExcelLineChart)ct)); break; case eChartType.ColumnClustered: - drawers.Add(new ColumnChartTypeDrawer(svgChart, ct)); + case eChartType.ColumnStacked: + case eChartType.ColumnStacked100: + case eChartType.BarClustered: + case eChartType.BarStacked: + case eChartType.BarStacked100: + drawers.Add(new BarColumnChartTypeDrawer(svgChart, (ExcelBarChart)ct)); break; default: throw new NotImplementedException($"No Svg support for Chart type {ct} is implemented."); @@ -82,6 +88,17 @@ internal static List Create(SvgChart svgChart) } return drawers; } + internal void SumSeries(List> series) + { + for (var i = 1; i < series.Count; i++) + { + for (var j = 0; j < series[i].Count; j++) + { + series[i][j] = ConvertUtil.GetValueDouble(series[i][j]) + ConvertUtil.GetValueDouble(series[i - 1][j]); + } + } + } + internal override void AppendRenderItems(List renderItems) { renderItems.AddRange(RenderItems); diff --git a/src/EPPlus.Export.ImageRenderer/Svg/Chart/ChartTypeDrawers/ColumnChartTypeDrawer.cs b/src/EPPlus.Export.ImageRenderer/Svg/Chart/ChartTypeDrawers/ColumnChartTypeDrawer.cs deleted file mode 100644 index 6c96910ba..000000000 --- a/src/EPPlus.Export.ImageRenderer/Svg/Chart/ChartTypeDrawers/ColumnChartTypeDrawer.cs +++ /dev/null @@ -1,180 +0,0 @@ -using EPPlus.Export.ImageRenderer.RenderItems.SvgItem; -using EPPlus.Graphics; -using EPPlusImageRenderer.RenderItems; -using EPPlusImageRenderer.Svg; -using OfficeOpenXml.Drawing.Chart; -using OfficeOpenXml.Utils.TypeConversion; -using System.Collections.Generic; - -namespace EPPlus.Export.ImageRenderer.Svg.Chart -{ - internal class ColumnChartTypeDrawer : ChartTypeDrawer - { - List serieDataLabels = new List(); - List> dataPointsPerSerie = new List>(); - - internal ColumnChartTypeDrawer(SvgChart svgChart, ExcelChart chartType) : base(svgChart, chartType) - { - var groupItem = new SvgGroupItem(ChartRenderer, _svgChart.Plotarea.Rectangle.Bounds); - RenderItems.Add(groupItem); - var isStacked = chartType.IsTypeStacked(); - var isPercentStacked = chartType.IsTypePercentStacked(); - var xValues = new List>(); - var yValues = new List>(); - int serCounter = 0; - - foreach (ExcelLineChartSerie serie in chartType.Series) - { - var yValue = LoadSeriesValues(serie.Series, serie.NumberLiteralsY, serie.StringLiteralsY); - var xValue = LoadSeriesValues(serie.XSeries, serie.NumberLiteralsX, serie.StringLiteralsX); - - xValues.Add(xValue); - yValues.Add(yValue); - - if (serie.HasDataLabel) - { - var datalabel = new SvgChartSerieDataLabel(svgChart, serie.DataLabel, svgChart.Bounds, serie, xValue, yValue, serCounter); - serieDataLabels.Add(datalabel); - } - serCounter++; - } - - if (chartType.IsTypeStacked()) - { - SumSeries(yValues); - } - else if (chartType.IsTypePercentStacked()) - { - ExcelChartAxisStandard.CalculateStacked100(yValues); - } - - for(var i= 0; i < xValues.Count; i++) - { - var xSerie = xValues[i]; - var ySerie = yValues[i]; - var serie = (ExcelLineChartSerie)chartType.Series[i]; - - var dataPoints = new List(); - - AddLine(chartType, serie, xSerie, ySerie, dataPoints); - - dataPointsPerSerie.Add(dataPoints); - - if (serie.HasDataLabel) - { - for (int j = 0; j < dataPoints.Count; j++) - { - serieDataLabels[i].SetParentPoint(dataPoints[j], j); - } - } - } - RenderItems.Add(new SvgEndGroupItem(ChartRenderer, null)); - - foreach (var dataLabel in serieDataLabels) - { - dataLabel.AppendRenderItems(RenderItems); - } - } - private void SumSeries(List> series) - { - for(var i=1;i < series.Count;i++) - { - for(var j=0;j < series[i].Count;j++) - { - series[i][j] = ConvertUtil.GetValueDouble(series[i][j]) + ConvertUtil.GetValueDouble(series[i-1][j]); - } - } - } - private void AddLine(ExcelChart chartType, ExcelLineChartSerie serie, List xValues, List yValues, List dataPoints) - { - SvgChartAxis yAxis, xAxis; - if (chartType.UseSecondaryAxis) - { - yAxis = _svgChart.SecondVerticalAxis; - xAxis = _svgChart.SecondHorizontalAxis; - if(xAxis.Axis.Deleted && xAxis.Values==null) - { - xAxis = _svgChart.HorizontalAxis; - } - } - else - { - yAxis = _svgChart.VerticalAxis; - xAxis = _svgChart.HorizontalAxis; - } - var linePath = new SvgRenderPathItem(ChartRenderer, _svgChart.Plotarea.Rectangle.Bounds); - var coords = new List(); - var markerItems = new List(); - - for (var i = 0; i < yValues.Count; i++) - { - double x; - if (xValues == null || xAxis.Axis.AxisType==eAxisType.Cat) - { - x = (double)i; - } - else - { - x = ConvertUtil.GetValueDouble(xValues[i], false, true); - } - - var y = ConvertUtil.GetValueDouble(yValues[i], false, true); - var xPos = xAxis.GetPositionInPlotarea(x); - var yPos = yAxis.GetPositionInPlotarea(y); - - BoundingBox pt = null; - - if (double.IsNaN(yPos) == false) - { - coords.Add(xPos / _svgChart.Plotarea.Rectangle.Bounds.Width); - coords.Add(yPos / _svgChart.Plotarea.Rectangle.Bounds.Height); - - //Log point within chart coordinate system - pt = new BoundingBox(xPos, yPos, 0, 0); - pt.Parent = _svgChart.Plotarea.Bounds; - } - - if (serie.HasMarker() && serie.Marker.Style != eMarkerStyle.None) - { - float mx = (float)xPos; - float my = (float)yPos; - var ls = LineMarkerHelper.GetMarkerItem(_svgChart, serie, mx, my, false); - if ((serie.Marker.Style == eMarkerStyle.Plus || serie.Marker.Style == eMarkerStyle.X || serie.Marker.Style == eMarkerStyle.Star) && - serie.Marker.Fill.IsEmpty == false) - { - markerItems.Add(LineMarkerHelper.GetMarkerBackground(_svgChart, serie, mx, my, false)); - } - markerItems.Add(ls); - - if (pt != null) - { - pt.Width = ls.Bounds.Width; - pt.Height = ls.Bounds.Height; - dataPoints.Add(pt); - } - } - else - { - - if (pt != null) - { - //Default values in excel - pt.Width = 5; - pt.Height = 5; - dataPoints.Add(pt); - } - } - } - - linePath.Commands.Add(new EPPlusImageRenderer.PathCommands(PathCommandType.Move, linePath, coords.ToArray())); - linePath.SetDrawingPropertiesBorder(serie.Border, chartType.StyleManager.Style.SeriesLine.BorderReference.Color, true); - linePath.SetDrawingPropertiesEffects(serie.Effect); - linePath.FillColor = "none"; //No fill for line - linePath.StrokeMiterLimit = 4; //A much higher value of the miter limit, might cause the "spike" to get beyond the data point on the vertical scale.. - linePath.LineJoin = SvgLineJoin.Round; - RenderItems.Add(linePath); - RenderItems.AddRange(markerItems); - } - } - -} diff --git a/src/EPPlus.Export.ImageRenderer/Svg/Chart/ChartTypeDrawers/LineChartTypeDrawer.cs b/src/EPPlus.Export.ImageRenderer/Svg/Chart/ChartTypeDrawers/LineChartTypeDrawer.cs index 3d99c7fa5..c18b43ae6 100644 --- a/src/EPPlus.Export.ImageRenderer/Svg/Chart/ChartTypeDrawers/LineChartTypeDrawer.cs +++ b/src/EPPlus.Export.ImageRenderer/Svg/Chart/ChartTypeDrawers/LineChartTypeDrawer.cs @@ -13,7 +13,7 @@ internal class LineChartTypeDrawer : ChartTypeDrawer List serieDataLabels = new List(); List> dataPointsPerSerie = new List>(); - internal LineChartTypeDrawer(SvgChart svgChart, ExcelChart chartType) : base(svgChart, chartType) + internal LineChartTypeDrawer(SvgChart svgChart, ExcelLineChart chartType) : base(svgChart, chartType) { var groupItem = new SvgGroupItem(ChartRenderer, _svgChart.Plotarea.Rectangle.Bounds); RenderItems.Add(groupItem); diff --git a/src/EPPlus.Export.ImageRenderer/Svg/Chart/SvgChartLegend.cs b/src/EPPlus.Export.ImageRenderer/Svg/Chart/SvgChartLegend.cs index ad8ee573e..37519bc8b 100644 --- a/src/EPPlus.Export.ImageRenderer/Svg/Chart/SvgChartLegend.cs +++ b/src/EPPlus.Export.ImageRenderer/Svg/Chart/SvgChartLegend.cs @@ -195,62 +195,15 @@ internal void SetLegend(SvgChart sc) case eChartType.LineMarkersStacked100: case eChartType.LineStacked: case eChartType.LineStacked100: - var ls=(ExcelLineChartSerie)s; - var tm = _seriesHeadersMeasure[index]; - TextMeasurement prevTm = tm; - if (pSls != null) - { - prevTm = _seriesHeadersMeasure[index - 1]; - } - - var si = GetSeriesIcon(sc, ls, prevTm, tm, pSls); - sls.SeriesIcon = si; - - var tbLeft = si.X2 + MarginExtra; - var tbTop = si.Y2 - tm.Height * 0.5; //TODO:Should probably be font ascent - double tbWidth; - if (pos == eLegendPosition.Left || pos == eLegendPosition.Right) - { - tbWidth = Bounds.Width - tbLeft - RightMargin; - } - else - { - tbWidth = Bounds.Width - tbLeft - RightMargin; - } - - var tbHeight = tm.Height; - sls.Textbox = new SvgTextBodyItem(ChartRenderer, Bounds, tbLeft, tbTop, tbWidth, tbHeight, false, true); - sls.Textbox.Bounds.Left = si.X2 + MarginExtra; - - var entry = Chart.Legend.Entries.FirstOrDefault(x => x.Index == index); - var headerText = s.GetHeaderText(index); - if (entry == null || entry.Font.IsEmpty) - { - //sls.Textbox.AddText(s.GetHeaderText(), sc.Chart.Legend.Font); - sls.Textbox.ImportParagraph(sc.Chart.Legend.TextBody.Paragraphs.FirstOrDefault(), 0, headerText); - } - else - { - //sls.Textbox.AddText(s.GetHeaderText(), entry.Font); - sls.Textbox.ImportParagraph(entry.TextBody.Paragraphs.FirstOrDefault(), 0, headerText); - } - - if (ls.HasMarker() && ls.Marker.Style != eMarkerStyle.None) - { - var l = sls.SeriesIcon as SvgRenderLineItem; - var x= l.X1 + (l.X2 - l.X1) / 2; - var y = l.Y1; - sls.MarkerIcon = LineMarkerHelper.GetMarkerItem(sc, ls, x, y, true); - if((ls.Marker.Style == eMarkerStyle.Plus || ls.Marker.Style == eMarkerStyle.X || ls.Marker.Style == eMarkerStyle.Star) && - ls.Marker.Fill.IsEmpty == false) - { - sls.MarkerBackground = LineMarkerHelper.GetMarkerBackground(sc, ls, x, y, true); - } - else - { - sls.MarkerBackground = null; - } - } + SetLineLegend(sc, index, pSls, pos, s, sls); + break; + case eChartType.ColumnClustered: + case eChartType.ColumnStacked: + case eChartType.ColumnStacked100: + case eChartType.BarClustered: + case eChartType.BarStacked: + case eChartType.BarStacked100: + SetBarLegend(sc, index, pSls, pos, s, sls); break; default: break; @@ -262,17 +215,120 @@ internal void SetLegend(SvgChart sc) } } - private SvgRenderLineItem GetSeriesIcon(SvgChart sc, ExcelChartStandardSerie cStandardSerie, TextMeasurement pTm, TextMeasurement tm, SvgLegendSerie pSls) + private void SetLineLegend(SvgChart sc, int index, SvgLegendSerie pSls, eLegendPosition pos, ExcelChartSerie s, SvgLegendSerie sls) + { + var ls = (ExcelLineChartSerie)s; + var tm = _seriesHeadersMeasure[index]; + TextMeasurement prevTm = tm; + if (pSls != null) + { + prevTm = _seriesHeadersMeasure[index - 1]; + } + + var si = GetLineSeriesIcon(sc, ls, prevTm, tm, pSls); + sls.SeriesIcon = si; + + var tbLeft = si.X2 + MarginExtra; + var tbTop = si.Y2 - tm.Height * 0.5; //TODO:Should probably be font ascent + double tbWidth; + if (pos == eLegendPosition.Left || pos == eLegendPosition.Right) + { + tbWidth = Bounds.Width - tbLeft - RightMargin; + } + else + { + tbWidth = Bounds.Width - tbLeft - RightMargin; + } + + var tbHeight = tm.Height; + sls.Textbox = new SvgTextBodyItem(ChartRenderer, Bounds, tbLeft, tbTop, tbWidth, tbHeight, false, true); + sls.Textbox.Bounds.Left = si.X2 + MarginExtra; + + var entry = Chart.Legend.Entries.FirstOrDefault(x => x.Index == index); + var headerText = s.GetHeaderText(index); + if (entry == null || entry.Font.IsEmpty) + { + //sls.Textbox.AddText(s.GetHeaderText(), sc.Chart.Legend.Font); + sls.Textbox.ImportParagraph(sc.Chart.Legend.TextBody.Paragraphs.FirstOrDefault(), 0, headerText); + } + else + { + //sls.Textbox.AddText(s.GetHeaderText(), entry.Font); + sls.Textbox.ImportParagraph(entry.TextBody.Paragraphs.FirstOrDefault(), 0, headerText); + } + + if (ls.HasMarker() && ls.Marker.Style != eMarkerStyle.None) + { + var l = sls.SeriesIcon as SvgRenderLineItem; + var x = l.X1 + (l.X2 - l.X1) / 2; + var y = l.Y1; + sls.MarkerIcon = LineMarkerHelper.GetMarkerItem(sc, ls, x, y, true); + if ((ls.Marker.Style == eMarkerStyle.Plus || ls.Marker.Style == eMarkerStyle.X || ls.Marker.Style == eMarkerStyle.Star) && + ls.Marker.Fill.IsEmpty == false) + { + sls.MarkerBackground = LineMarkerHelper.GetMarkerBackground(sc, ls, x, y, true); + } + else + { + sls.MarkerBackground = null; + } + } + } + + private void SetBarLegend(SvgChart sc, int index, SvgLegendSerie pSls, eLegendPosition pos, ExcelChartSerie s, SvgLegendSerie sls) + { + var bs = (ExcelBarChartSerie)s; + var tm = _seriesHeadersMeasure[index]; + TextMeasurement prevTm = tm; + if (pSls != null) + { + prevTm = _seriesHeadersMeasure[index - 1]; + } + + var si = GetBarSeriesIcon(sc, bs, prevTm, tm, pSls); + sls.SeriesIcon = si; + + var tbLeft = si.Right + MarginExtra; + var tbTop = si.Bottom * 0.5; //TODO:Should probably be font ascent + double tbWidth; + if (pos == eLegendPosition.Left || pos == eLegendPosition.Right) + { + tbWidth = Bounds.Width - tbLeft - RightMargin; + } + else + { + tbWidth = Bounds.Width - tbLeft - RightMargin; + } + + var tbHeight = tm.Height; + sls.Textbox = new SvgTextBodyItem(ChartRenderer, Bounds, tbLeft, tbTop, tbWidth, tbHeight, false, true); + sls.Textbox.Bounds.Left = si.Bottom + MarginExtra; + + var entry = Chart.Legend.Entries.FirstOrDefault(x => x.Index == index); + var headerText = s.GetHeaderText(index); + if (entry == null || entry.Font.IsEmpty) + { + //sls.Textbox.AddText(s.GetHeaderText(), sc.Chart.Legend.Font); + sls.Textbox.ImportParagraph(sc.Chart.Legend.TextBody.Paragraphs.FirstOrDefault(), 0, headerText); + } + else + { + //sls.Textbox.AddText(s.GetHeaderText(), entry.Font); + sls.Textbox.ImportParagraph(entry.TextBody.Paragraphs.FirstOrDefault(), 0, headerText); + } + } + + private SvgRenderLineItem GetLineSeriesIcon(SvgChart sc, ExcelChartStandardSerie cStandardSerie, TextMeasurement pTm, TextMeasurement tm, SvgLegendSerie pSls) { var item = new SvgRenderLineItem(sc, Rectangle.Bounds); item.SetDrawingPropertiesFill(cStandardSerie.Fill, sc.Chart.StyleManager.Style.SeriesLine.FillReference.Color); - item.SetDrawingPropertiesBorder(cStandardSerie.Border, sc.Chart.StyleManager.Style.SeriesLine.BorderReference.Color, cStandardSerie.Border.Fill.Style!=eFillStyle.NoFill, 0.75); + item.SetDrawingPropertiesBorder(cStandardSerie.Border, sc.Chart.StyleManager.Style.SeriesLine.BorderReference.Color, cStandardSerie.Border.Fill.Style != eFillStyle.NoFill, 0.75); if (sc.Chart.Legend.Position == eLegendPosition.Top || sc.Chart.Legend.Position == eLegendPosition.Bottom) { float y = (float)Rectangle.Top + (float)TopMargin + tm.Height / 2 + MarginExtra; - float x = 0; + float x = 0; if (pSls == null) { x = (float)Rectangle.Left + (float)LeftMargin;// + MarginExtra; @@ -310,6 +366,53 @@ private SvgRenderLineItem GetSeriesIcon(SvgChart sc, ExcelChartStandardSerie cSt return item; } + private SvgRenderRectItem GetBarSeriesIcon(SvgChart sc, ExcelChartStandardSerie cStandardSerie, TextMeasurement pTm, TextMeasurement tm, SvgLegendSerie pSls) + { + var item = new SvgRenderRectItem(sc, Rectangle.Bounds); + item.SetDrawingPropertiesFill(cStandardSerie.Fill, sc.Chart.StyleManager.Style.SeriesLine.FillReference.Color); + item.SetDrawingPropertiesBorder(cStandardSerie.Border, sc.Chart.StyleManager.Style.SeriesLine.BorderReference.Color, cStandardSerie.Border.Fill.Style!=eFillStyle.NoFill, 0.75); + + if (sc.Chart.Legend.Position == eLegendPosition.Top || + sc.Chart.Legend.Position == eLegendPosition.Bottom) + { + float y = (float)Rectangle.Top + (float)TopMargin + tm.Height / 2 + MarginExtra; + float x = 0; + if (pSls == null) + { + x = (float)Rectangle.Left + (float)LeftMargin;// + MarginExtra; + } + else + { + x = (float)pSls.Textbox.Bounds.Right + MiddleMargin; + } + + item.Left = x; + item.Top = y; + item.Width = 4; + item.Height = 4; + } + else + { + double y; + if (pSls == null) + { + y = TopMargin + tm.Height / 2 + MarginExtra; + } + else + { + y = ((SvgRenderRectItem)pSls.SeriesIcon).Top + pTm.Height / 2 + tm.Height / 2 + MiddleMargin; + } + + item.Left = (float)LeftMargin; //4 + item.Top = y; + item.Width = 4; + item.Height = 4; + item.LineCap = eLineCap.Round; + } + + return item; + } + internal override void AppendRenderItems(List renderItems) { var groupItem = new SvgGroupItem(ChartRenderer, Bounds); diff --git a/src/EPPlus.Export.ImageRenderer/Svg/Chart/SvgChartLegendIcon.cs b/src/EPPlus.Export.ImageRenderer/Svg/Chart/SvgChartLegendIcon.cs index 65cc40bb3..4069c8be8 100644 --- a/src/EPPlus.Export.ImageRenderer/Svg/Chart/SvgChartLegendIcon.cs +++ b/src/EPPlus.Export.ImageRenderer/Svg/Chart/SvgChartLegendIcon.cs @@ -65,7 +65,7 @@ public SvgChartLegendIcon(SvgChart sc, SvgRenderRectItem Rectangle, ExcelChartSt // throw new NotImplementedException(); //} - //private SvgRenderLineItem GetSeriesIcon(SvgChart sc, ExcelChartStandardSerie s, TextMeasurement sHM, TextMeasurement tm, SvgLegendSerie pSls) + //private SvgRenderLineItem GetLineSeriesIcon(SvgChart sc, ExcelChartStandardSerie s, TextMeasurement sHM, TextMeasurement tm, SvgLegendSerie pSls) //{ // var item = new SvgRenderLineItem(sc, Rectangle.Bounds); // item.SetDrawingPropertiesFill(s.Fill, sc.Chart.StyleManager.Style.SeriesLine.FillReference.Color); diff --git a/src/EPPlus.Export.ImageRenderer/Utils/SvgDrawingWriter.cs b/src/EPPlus.Export.ImageRenderer/Utils/SvgDrawingWriter.cs index f20f2eca0..a93506ebe 100644 --- a/src/EPPlus.Export.ImageRenderer/Utils/SvgDrawingWriter.cs +++ b/src/EPPlus.Export.ImageRenderer/Utils/SvgDrawingWriter.cs @@ -50,12 +50,12 @@ internal void WriteSvgDefs(StringBuilder sb, List renderItems) string filter = ""; if (item.GradientFill != null) { - string name = WriteGradient("Gradient", defSb, hs, item.GradientFill, item.FillColorSource, true); + string name = WriteGradient($"Gradient{ix}", defSb, hs, item.GradientFill, item.FillColorSource, true); item.FillColor = $"Url(#{name})"; } else if (item.PatternFill != null) { - string name = WritePattern($"Pattern{item.PatternFill.PatternType}", defSb, hs, item.PatternFill, item.FillColorSource); + string name = WritePattern($"Pattern{item.PatternFill.PatternType}{ix}", defSb, hs, item.PatternFill, item.FillColorSource); item.FillColor = $"Url(#{name})"; } else if (item.BlipFill != null) @@ -70,7 +70,7 @@ internal void WriteSvgDefs(StringBuilder sb, List renderItems) } if (item.BorderGradientFill != null) { - string name = WriteGradient("StrokeGradient", defSb, hs, item.BorderGradientFill, item.BorderColorSource, true); + string name = WriteGradient($"StrokeGradient{ix}", defSb, hs, item.BorderGradientFill, item.BorderColorSource, true); item.BorderColor = $"Url(#{name})"; } if(item.GlowColor!=null) diff --git a/src/EPPlus/Drawing/Chart/ExcelChartAxisStandard.cs b/src/EPPlus/Drawing/Chart/ExcelChartAxisStandard.cs index 9173dfe16..cd4f9a526 100644 --- a/src/EPPlus/Drawing/Chart/ExcelChartAxisStandard.cs +++ b/src/EPPlus/Drawing/Chart/ExcelChartAxisStandard.cs @@ -798,7 +798,16 @@ internal override List GetAxisValues(out bool isCount) { List> values; GetSeriesValues(out isCount, out values); - var dl = values.SelectMany(x => x).Distinct().ToList(); + List dl; + if (IsVertical) + { + dl = values.SelectMany(x => x).Distinct().ToList(); + + } + else + { + dl = values.SelectMany(x => x).ToList(); + } dl.Sort(); if (Orientation == eAxisOrientation.MaxMin) { @@ -812,6 +821,7 @@ internal void GetSeriesValues(out bool isCount, out List> values) values = new List>(); isCount = false; List pl = new List(); + int ix = 0; foreach (var ct in _chart.PlotArea.ChartTypes) { foreach (var serie in ct.Series) @@ -827,21 +837,22 @@ internal void GetSeriesValues(out bool isCount, out List> values) } else { - AddFromSerie(l, serie.XSeries, serie.NumberLiteralsX, serie.StringLiteralsX, true); + AddFromSerie(l, serie.XSeries, serie.NumberLiteralsX, serie.StringLiteralsX, true,serie.GetHeaderText(ix)); } } else { if (ct.YAxis.Id == Id) { - AddFromSerie(l, serie.Series, serie.NumberLiteralsY, serie.StringLiteralsY, false, pl); + AddFromSerie(l, serie.Series, serie.NumberLiteralsY, serie.StringLiteralsY, false, serie.GetHeaderText(ix), pl); } else if (ct.XAxis.Id == Id) { - AddFromSerie(l, serie.XSeries, serie.NumberLiteralsX, serie.StringLiteralsX, false); + AddFromSerie(l, serie.XSeries, serie.NumberLiteralsX, serie.StringLiteralsX, false, serie.GetHeaderText(ix)); } } pl = l; + ix++; } } if (_chart.IsTypePercentStacked() && IsYAxis) @@ -901,7 +912,7 @@ private void AddCountFromSeries(List l, string address, double[] numberL } } - private void AddFromSerie(List list, string address, double[] numberLiterals, string[] stringLiterals, bool useText, List prevList=null) + private void AddFromSerie(List list, string address, double[] numberLiterals, string[] stringLiterals, bool useText, string header, List prevList=null) { var isStacked = _chart.IsTypeStacked() && prevList != null; if (numberLiterals?.Length > 0) diff --git a/src/EPPlus/Drawing/Chart/ExcelChartSerie.cs b/src/EPPlus/Drawing/Chart/ExcelChartSerie.cs index 4e667b65c..d4e740a1c 100644 --- a/src/EPPlus/Drawing/Chart/ExcelChartSerie.cs +++ b/src/EPPlus/Drawing/Chart/ExcelChartSerie.cs @@ -261,19 +261,18 @@ internal string GetHeaderText(int index) { if (string.IsNullOrEmpty(HeaderAddress.WorkSheetName)) { - ret = _chart.WorkSheet.Cells[HeaderAddress.Address].Offset(0, 0).Text; + return _chart.WorkSheet.Cells[HeaderAddress.Address].Offset(0, 0).Text; } else { var ws = _chart.WorkSheet.Workbook.Worksheets[HeaderAddress.WorkSheetName]; if (ws != null) { - ret = ws.Cells[HeaderAddress.Address].Offset(0, 0).Text; + return ws.Cells[HeaderAddress.Address].Offset(0, 0).Text; } } } - return string.IsNullOrEmpty(ret) ? $"Series{index + 1}" : ret; - ; + return $"Series{index + 1}"; } } } From 17bc340087f099c7e8302e1d41fb2c6ff0070968 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jan=20K=C3=A4llman?= Date: Wed, 25 Mar 2026 12:34:24 +0100 Subject: [PATCH 119/151] Moved tests --- .../Chart/ColumnChartTests.cs | 61 ++++ .../Chart/LineChartToSvgTests.cs | 226 +++++++++++++ .../ShapeToSvgTests.cs} | 302 ++---------------- 3 files changed, 309 insertions(+), 280 deletions(-) create mode 100644 src/EPPlus.Export.ImageRenderer.Test/Chart/ColumnChartTests.cs create mode 100644 src/EPPlus.Export.ImageRenderer.Test/Chart/LineChartToSvgTests.cs rename src/EPPlus.Export.ImageRenderer.Test/{SvgPathTests.cs => Shape/ShapeToSvgTests.cs} (72%) diff --git a/src/EPPlus.Export.ImageRenderer.Test/Chart/ColumnChartTests.cs b/src/EPPlus.Export.ImageRenderer.Test/Chart/ColumnChartTests.cs new file mode 100644 index 000000000..e54ca78fa --- /dev/null +++ b/src/EPPlus.Export.ImageRenderer.Test/Chart/ColumnChartTests.cs @@ -0,0 +1,61 @@ +using OfficeOpenXml; +using OfficeOpenXml.Drawing.Chart; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace EPPlus.Export.ImageRenderer.Tests.Chart +{ + [TestClass] + public class ColumnChartTests : TestBase + { + [TestMethod] + public void GenerateSvgForColumnCharts1() + { + ExcelPackage.License.SetNonCommercialOrganization("EPPlus Project"); + using (var p = OpenTemplatePackage("ColumnChartForSvg.xlsx")) + { + var ws = p.Workbook.Worksheets[0]; + var renderer = new EPPlusImageRenderer.ImageRenderer(); + + //var ix = 1; + //var c = ws.Drawings[ix]; + //var svg = renderer.RenderDrawingToSvg(c); + //SaveTextFileToWorkbook($"svg\\ChartForSvg_ind{ix++}.svg", svg); + + var ix = 0; + foreach (ExcelChart c in ws.Drawings) + { + var svg = renderer.RenderDrawingToSvg(c); + SaveTextFileToWorkbook($"svg\\BarChartForSvg_sheet1_{ix++}.svg", svg); + } + } + } + [TestMethod] + public void GenerateSvgForColumnCharts2() + { + ExcelPackage.License.SetNonCommercialOrganization("EPPlus Project"); + using (var p = OpenTemplatePackage("ColumnChartForSvg.xlsx")) + { + var ws = p.Workbook.Worksheets[1]; + var renderer = new EPPlusImageRenderer.ImageRenderer(); + + //var ix = 1; + //var c = ws.Drawings[ix]; + //var svg = renderer.RenderDrawingToSvg(c); + //SaveTextFileToWorkbook($"svg\\ChartForSvg_ind{ix++}.svg", svg); + + var ix = 0; + foreach (ExcelChart c in ws.Drawings) + { + var svg = renderer.RenderDrawingToSvg(c); + SaveTextFileToWorkbook($"svg\\BarChartForSvg_sheet2_{ix++}.svg", svg); + } + } + } + + + } +} diff --git a/src/EPPlus.Export.ImageRenderer.Test/Chart/LineChartToSvgTests.cs b/src/EPPlus.Export.ImageRenderer.Test/Chart/LineChartToSvgTests.cs new file mode 100644 index 000000000..218f6178b --- /dev/null +++ b/src/EPPlus.Export.ImageRenderer.Test/Chart/LineChartToSvgTests.cs @@ -0,0 +1,226 @@ +using OfficeOpenXml; +using OfficeOpenXml.Drawing.Chart; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace EPPlus.Export.ImageRenderer.Tests.Chart +{ + [TestClass] + public class LineChartToSvgTests : TestBase + { + [TestMethod] + public void GenerateSvgForLineCharts_sheet1() + { + ExcelPackage.License.SetNonCommercialOrganization("EPPlus Project"); + using (var p = OpenTemplatePackage("ChartForSvg.xlsx")) + { + var ws = p.Workbook.Worksheets[0]; + var renderer = new EPPlusImageRenderer.ImageRenderer(); + + //var ix = 1; + //var c = ws.Drawings[ix]; + //var svg = renderer.RenderDrawingToSvg(c); + //SaveTextFileToWorkbook($"svg\\ChartForSvg_ind{ix++}.svg", svg); + + var ix = 0; + foreach (ExcelChart c in ws.Drawings) + { + var svg = renderer.RenderDrawingToSvg(c); + SaveTextFileToWorkbook($"svg\\ChartForSvg{ix++}.svg", svg); + } + } + } + + [TestMethod] + public void GenerateSvgForLineCharts_sheet2() + { + ExcelPackage.License.SetNonCommercialOrganization("EPPlus Project"); + using (var p = OpenTemplatePackage("ChartForSvg.xlsx")) + { + var ws = p.Workbook.Worksheets[1]; + var renderer = new EPPlusImageRenderer.ImageRenderer(); + //var ix = 1; + //var c = ws.Drawings[ix]; + //var svg = renderer.RenderDrawingToSvg(c); + //SaveTextFileToWorkbook($"svg\\ChartForSvg_sheet2_{ix++}.svg", svg); + var ix = 1; + foreach (ExcelChart c in ws.Drawings) + { + var svg = renderer.RenderDrawingToSvg(c); + SaveTextFileToWorkbook($"svg\\ChartForSvg{ix++}.svg", svg); + } + } + } + [TestMethod] + public void GenerateSvgForLineCharts_sheet3() + { + ExcelPackage.License.SetNonCommercialOrganization("EPPlus Project"); + using (var p = OpenTemplatePackage("ChartForSvg.xlsx")) + { + var ws = p.Workbook.Worksheets[2]; + var renderer = new EPPlusImageRenderer.ImageRenderer(); + //var ix = 1; + //var c = ws.Drawings[ix]; + //var svg = renderer.RenderDrawingToSvg(c); + //SaveTextFileToWorkbook($"svg\\ChartForSvg_sheet2_{ix++}.svg", svg); + var ix = 1; + foreach (ExcelChart c in ws.Drawings) + { + var svg = renderer.RenderDrawingToSvg(c); + SaveTextFileToWorkbook($"svg\\ChartForSvg_Sheet3{ix++}.svg", svg); + } + } + } + [TestMethod] + public void GenerateSvgForLineCharts() + { + ExcelPackage.License.SetNonCommercialOrganization("EPPlus Project"); + using (var p = OpenTemplatePackage("LineChartRenderTest.xlsx")) + { + var ws = p.Workbook.Worksheets[0]; + var renderer = new EPPlusImageRenderer.ImageRenderer(); + //var ix = 1; + //var c = ws.Drawings[ix]; + //var svg = renderer.RenderDrawingToSvg(c); + //SaveTextFileToWorkbook($"svg\\LineChartForSvg_Single{ix++}.svg", svg); + var ix = 1; + foreach (ExcelChart c in ws.Drawings) + { + var svg = renderer.RenderDrawingToSvg(c); + SaveTextFileToWorkbook($"svg\\LineChartForSvg{ix++}.svg", svg); + } + } + } + + [TestMethod] + public void GenerateSvgForCharts_SecondaryAxis() + { + ExcelPackage.License.SetNonCommercialOrganization("EPPlus Project"); + using (var p = OpenTemplatePackage("ChartForSvg_SecondaryAxis.xlsx")) + { + var ws = p.Workbook.Worksheets[0]; + var renderer = new EPPlusImageRenderer.ImageRenderer(); + //var ix = 3; + //var c = ws.Drawings[ix]; + //var svg = renderer.RenderDrawingToSvg(c); + //SaveTextFileToWorkbook($"svg\\ChartForSvg_sheet2_{ix++}.svg", svg); + var ix = 1; + foreach (ExcelChart c in ws.Drawings) + { + var svg = renderer.RenderDrawingToSvg(c); + SaveTextFileToWorkbook($"svg\\ChartForSvg_SecAxis{ix++}.svg", svg); + } + } + } + [TestMethod] + public void GenerateSvgForCharts_SecondaryAxis_sheet2() + { + ExcelPackage.License.SetNonCommercialOrganization("EPPlus Project"); + using (var p = OpenTemplatePackage("ChartForSvg_SecondaryAxis.xlsx")) + { + var ws = p.Workbook.Worksheets[1]; + var renderer = new EPPlusImageRenderer.ImageRenderer(); + //var ix = 0; + //var c = ws.Drawings[ix]; + //var svg = renderer.RenderDrawingToSvg(c); + //SaveTextFileToWorkbook($"svg\\ChartForSvg_sheet2_{ix++}.svg", svg); + var ix = 1; + foreach (ExcelChart c in ws.Drawings) + { + var svg = renderer.RenderDrawingToSvg(c); + SaveTextFileToWorkbook($"svg\\ChartForSvg_Sheet2_SecAxis{ix++}.svg", svg); + } + } + } + [TestMethod] + public void GenerateSimplestChart() + { + ExcelPackage.License.SetNonCommercialOrganization("EPPlus Project"); + using (var p = OpenTemplatePackage("SimplestChart.xlsx")) + { + var c = p.Workbook.Worksheets[0].Drawings[0]; + + var renderer = new EPPlusImageRenderer.ImageRenderer(); + var svg = renderer.RenderDrawingToSvg(c); + SaveTextFileToWorkbook($"svg\\SimplestChartTitle.svg", svg); + } + } + + + [TestMethod] + public void GenerateDataLabels() + { + ExcelPackage.License.SetNonCommercialOrganization("EPPlus Project"); + using (var p = OpenTemplatePackage("datalabelsSvg.xlsx")) + { + var c = p.Workbook.Worksheets[0].Drawings[0]; + + var renderer = new EPPlusImageRenderer.ImageRenderer(); + var svg = renderer.RenderDrawingToSvg(c); + SaveTextFileToWorkbook($"svg\\datalabelsAttempt.svg", svg); + } + } + + + + [TestMethod] + public void GenerateDataLabelsTrueMost() + { + ExcelPackage.License.SetNonCommercialOrganization("EPPlus Project"); + using (var p = OpenTemplatePackage("datalabelsSvgTrueMostWithFill.xlsx")) + { + var c = p.Workbook.Worksheets[0].Drawings[0]; + + var renderer = new EPPlusImageRenderer.ImageRenderer(); + var svg = renderer.RenderDrawingToSvg(c); + SaveTextFileToWorkbook($"svg\\datalabelsSvgTrueMostWithFill.svg", svg); + } + } + + [TestMethod] + public void GenerateDataLabelsTrueMostAndManualLayout() + { + ExcelPackage.License.SetNonCommercialOrganization("EPPlus Project"); + using (var p = OpenTemplatePackage("datalabelsSvgTrueMostWithFillANDManual.xlsx")) + { + var c = p.Workbook.Worksheets[0].Drawings[0]; + + var renderer = new EPPlusImageRenderer.ImageRenderer(); + var svg = renderer.RenderDrawingToSvg(c); + SaveTextFileToWorkbook($"svg\\datalabelsSvgTrueMostWithFillAndManual.svg", svg); + } + } + + [TestMethod] + public void GenerateDatalabelsLeaderLines() + { + ExcelPackage.License.SetNonCommercialOrganization("EPPlus Project"); + using (var p = OpenTemplatePackage("datalabelsSvgLeaderLinesAdjustedToBeSimilar.xlsx")) + { + var c = p.Workbook.Worksheets[0].Drawings[0]; + + var renderer = new EPPlusImageRenderer.ImageRenderer(); + var svg = renderer.RenderDrawingToSvg(c); + SaveTextFileToWorkbook($"svg\\datalabelsSvgLeaderLines.svg", svg); + } + } + + + [TestMethod] + public void GenerateDatalabelsRightAlignedWithBg() + { + ExcelPackage.License.SetNonCommercialOrganization("EPPlus Project"); + using (var p = OpenTemplatePackage("datalabelsSvgRightAlignedWithBg.xlsx")) + { + var c = p.Workbook.Worksheets[0].Drawings[0]; + + var renderer = new EPPlusImageRenderer.ImageRenderer(); + var svg = renderer.RenderDrawingToSvg(c); + SaveTextFileToWorkbook($"svg\\datalabelsSvgLeaderLinesBg.svg", svg); + } + } + } +} diff --git a/src/EPPlus.Export.ImageRenderer.Test/SvgPathTests.cs b/src/EPPlus.Export.ImageRenderer.Test/Shape/ShapeToSvgTests.cs similarity index 72% rename from src/EPPlus.Export.ImageRenderer.Test/SvgPathTests.cs rename to src/EPPlus.Export.ImageRenderer.Test/Shape/ShapeToSvgTests.cs index 1699959db..e6bcfd82a 100644 --- a/src/EPPlus.Export.ImageRenderer.Test/SvgPathTests.cs +++ b/src/EPPlus.Export.ImageRenderer.Test/Shape/ShapeToSvgTests.cs @@ -10,10 +10,10 @@ using System.Xml; using TypeConv = OfficeOpenXml.Utils.TypeConversion; -namespace TestProject1 +namespace EPPlus.Export.ImageRenderer.Tests.Shape { [TestClass] - public sealed class SvgPathTests : TestBase + public sealed class ShapeToSvgTests : TestBase { [TestMethod] public void Rect() @@ -505,171 +505,44 @@ public void GenerateSvgForCircle() } } [TestMethod] - public void GenerateSvgForCharts() - { - ExcelPackage.License.SetNonCommercialOrganization("EPPlus Project"); - using (var p = OpenTemplatePackage("ChartForSvg.xlsx")) - { - var ws = p.Workbook.Worksheets[0]; - var renderer = new EPPlusImageRenderer.ImageRenderer(); - - //var ix = 1; - //var c = ws.Drawings[ix]; - //var svg = renderer.RenderDrawingToSvg(c); - //SaveTextFileToWorkbook($"svg\\ChartForSvg_ind{ix++}.svg", svg); - - var ix = 0; - foreach (ExcelChart c in ws.Drawings) - { - var svg = renderer.RenderDrawingToSvg(c); - SaveTextFileToWorkbook($"svg\\ChartForSvg{ix++}.svg", svg); - } - } - } - [TestMethod] - public void GenerateSvgForColumnCharts1() - { - ExcelPackage.License.SetNonCommercialOrganization("EPPlus Project"); - using (var p = OpenTemplatePackage("ColumnChartForSvg.xlsx")) - { - var ws = p.Workbook.Worksheets[0]; - var renderer = new EPPlusImageRenderer.ImageRenderer(); - - //var ix = 1; - //var c = ws.Drawings[ix]; - //var svg = renderer.RenderDrawingToSvg(c); - //SaveTextFileToWorkbook($"svg\\ChartForSvg_ind{ix++}.svg", svg); - - var ix = 0; - foreach (ExcelChart c in ws.Drawings) - { - var svg = renderer.RenderDrawingToSvg(c); - SaveTextFileToWorkbook($"svg\\BarChartForSvg_sheet1_{ix++}.svg", svg); - } - } - } - [TestMethod] - public void GenerateSvgForColumnCharts2() - { - ExcelPackage.License.SetNonCommercialOrganization("EPPlus Project"); - using (var p = OpenTemplatePackage("ColumnChartForSvg.xlsx")) - { - var ws = p.Workbook.Worksheets[1]; - var renderer = new EPPlusImageRenderer.ImageRenderer(); - - //var ix = 1; - //var c = ws.Drawings[ix]; - //var svg = renderer.RenderDrawingToSvg(c); - //SaveTextFileToWorkbook($"svg\\ChartForSvg_ind{ix++}.svg", svg); - - var ix = 0; - foreach (ExcelChart c in ws.Drawings) - { - var svg = renderer.RenderDrawingToSvg(c); - SaveTextFileToWorkbook($"svg\\BarChartForSvg_sheet2_{ix++}.svg", svg); - } - } - } - - [TestMethod] - public void GenerateSimplestChart() - { - ExcelPackage.License.SetNonCommercialOrganization("EPPlus Project"); - using (var p = OpenTemplatePackage("SimplestChart.xlsx")) - { - var c = p.Workbook.Worksheets[0].Drawings[0]; - - var renderer = new EPPlusImageRenderer.ImageRenderer(); - var svg = renderer.RenderDrawingToSvg(c); - SaveTextFileToWorkbook($"svg\\SimplestChartTitle.svg", svg); - } - } - - - [TestMethod] - public void GenerateDataLabels() - { - ExcelPackage.License.SetNonCommercialOrganization("EPPlus Project"); - using (var p = OpenTemplatePackage("datalabelsSvg.xlsx")) - { - var c = p.Workbook.Worksheets[0].Drawings[0]; - - var renderer = new EPPlusImageRenderer.ImageRenderer(); - var svg = renderer.RenderDrawingToSvg(c); - SaveTextFileToWorkbook($"svg\\datalabelsAttempt.svg", svg); - } - } - - - - [TestMethod] - public void GenerateDataLabelsTrueMost() + public void OpenRightAligned() { ExcelPackage.License.SetNonCommercialOrganization("EPPlus Project"); - using (var p = OpenTemplatePackage("datalabelsSvgTrueMostWithFill.xlsx")) + using (var p = OpenTemplatePackage("SimpleChartRightAlign.xlsx")) { var c = p.Workbook.Worksheets[0].Drawings[0]; var renderer = new EPPlusImageRenderer.ImageRenderer(); var svg = renderer.RenderDrawingToSvg(c); - SaveTextFileToWorkbook($"svg\\datalabelsSvgTrueMostWithFill.svg", svg); + SaveTextFileToWorkbook($"svg\\SimplestChartRightAlign.svg", svg); } } - [TestMethod] - public void GenerateDataLabelsTrueMostAndManualLayout() - { - ExcelPackage.License.SetNonCommercialOrganization("EPPlus Project"); - using (var p = OpenTemplatePackage("datalabelsSvgTrueMostWithFillANDManual.xlsx")) - { - var c = p.Workbook.Worksheets[0].Drawings[0]; - - var renderer = new EPPlusImageRenderer.ImageRenderer(); - var svg = renderer.RenderDrawingToSvg(c); - SaveTextFileToWorkbook($"svg\\datalabelsSvgTrueMostWithFillAndManual.svg", svg); - } - } - - [TestMethod] - public void GenerateDatalabelsLeaderLines() + public void TestStyling() { ExcelPackage.License.SetNonCommercialOrganization("EPPlus Project"); - using (var p = OpenTemplatePackage("datalabelsSvgLeaderLinesAdjustedToBeSimilar.xlsx")) + using (var p = OpenTemplatePackage("MyCellsAdvanced.xlsx")) { - var c = p.Workbook.Worksheets[0].Drawings[0]; - - var renderer = new EPPlusImageRenderer.ImageRenderer(); - var svg = renderer.RenderDrawingToSvg(c); - SaveTextFileToWorkbook($"svg\\datalabelsSvgLeaderLines.svg", svg); - } - } + var ws = p.Workbook.Worksheets[0]; + var myCell = ws.Cells["B3"]; + var myCellStyle = myCell.Style; + var fillStyle = myCell.Style.Fill; + var fontColor = myCell.Style.Font.Color; + var bgRgb = myCell.Style.Fill.BackgroundColor.Rgb; + var bgFillCol = myCell.Style.Fill.PatternColor.Rgb; - [TestMethod] - public void GenerateDatalabelsRightAlignedWithBg() - { - ExcelPackage.License.SetNonCommercialOrganization("EPPlus Project"); - using (var p = OpenTemplatePackage("datalabelsSvgRightAlignedWithBg.xlsx")) - { - var c = p.Workbook.Worksheets[0].Drawings[0]; + var myOtherCell = ws.Cells["B2"]; - var renderer = new EPPlusImageRenderer.ImageRenderer(); - var svg = renderer.RenderDrawingToSvg(c); - SaveTextFileToWorkbook($"svg\\datalabelsSvgLeaderLinesBg.svg", svg); - } - } + var myShape = ws.Drawings[0].As.Shape; + var myRichtext = myShape.RichText; + var firstDefault = myShape.TextBody.Paragraphs.FirstDefaultRunProperties; - [TestMethod] - public void OpenRightAligned() - { - ExcelPackage.License.SetNonCommercialOrganization("EPPlus Project"); - using (var p = OpenTemplatePackage("SimpleChartRightAlign.xlsx")) - { - var c = p.Workbook.Worksheets[0].Drawings[0]; + var firstPara = myShape.TextBody.Paragraphs[0]; + var defRun = firstPara.DefaultRunProperties; - var renderer = new EPPlusImageRenderer.ImageRenderer(); - var svg = renderer.RenderDrawingToSvg(c); - SaveTextFileToWorkbook($"svg\\SimplestChartRightAlign.svg", svg); + var secondPara = myShape.TextBody.Paragraphs[1]; + var secondDefRun = secondPara.DefaultRunProperties; } } @@ -736,137 +609,6 @@ public void GenerateShapeCenteredParagraph() SaveAndCleanup(p); } } - - [TestMethod] - public void GenerateSvgForLineCharts() - { - ExcelPackage.License.SetNonCommercialOrganization("EPPlus Project"); - using (var p = OpenTemplatePackage("LineChartRenderTest.xlsx")) - { - var ws = p.Workbook.Worksheets[0]; - var renderer = new EPPlusImageRenderer.ImageRenderer(); - //var ix = 1; - //var c = ws.Drawings[ix]; - //var svg = renderer.RenderDrawingToSvg(c); - //SaveTextFileToWorkbook($"svg\\LineChartForSvg_Single{ix++}.svg", svg); - var ix = 1; - foreach (ExcelChart c in ws.Drawings) - { - var svg = renderer.RenderDrawingToSvg(c); - SaveTextFileToWorkbook($"svg\\LineChartForSvg{ix++}.svg", svg); - } - } - } - [TestMethod] - public void GenerateSvgForCharts_sheet2() - { - ExcelPackage.License.SetNonCommercialOrganization("EPPlus Project"); - using (var p = OpenTemplatePackage("ChartForSvg.xlsx")) - { - var ws = p.Workbook.Worksheets[1]; - var renderer = new EPPlusImageRenderer.ImageRenderer(); - //var ix = 1; - //var c = ws.Drawings[ix]; - //var svg = renderer.RenderDrawingToSvg(c); - //SaveTextFileToWorkbook($"svg\\ChartForSvg_sheet2_{ix++}.svg", svg); - var ix = 1; - foreach (ExcelChart c in ws.Drawings) - { - var svg = renderer.RenderDrawingToSvg(c); - SaveTextFileToWorkbook($"svg\\ChartForSvg{ix++}.svg", svg); - } - } - } - [TestMethod] - public void GenerateSvgForCharts_sheet3() - { - ExcelPackage.License.SetNonCommercialOrganization("EPPlus Project"); - using (var p = OpenTemplatePackage("ChartForSvg.xlsx")) - { - var ws = p.Workbook.Worksheets[2]; - var renderer = new EPPlusImageRenderer.ImageRenderer(); - //var ix = 1; - //var c = ws.Drawings[ix]; - //var svg = renderer.RenderDrawingToSvg(c); - //SaveTextFileToWorkbook($"svg\\ChartForSvg_sheet2_{ix++}.svg", svg); - var ix = 1; - foreach (ExcelChart c in ws.Drawings) - { - var svg = renderer.RenderDrawingToSvg(c); - SaveTextFileToWorkbook($"svg\\ChartForSvg_Sheet3{ix++}.svg", svg); - } - } - } - - [TestMethod] - public void TestStyling() - { - ExcelPackage.License.SetNonCommercialOrganization("EPPlus Project"); - using (var p = OpenTemplatePackage("MyCellsAdvanced.xlsx")) - { - var ws = p.Workbook.Worksheets[0]; - var myCell = ws.Cells["B3"]; - var myCellStyle = myCell.Style; - var fillStyle = myCell.Style.Fill; - var fontColor = myCell.Style.Font.Color; - - var bgRgb = myCell.Style.Fill.BackgroundColor.Rgb; - var bgFillCol = myCell.Style.Fill.PatternColor.Rgb; - - var myOtherCell = ws.Cells["B2"]; - - var myShape = ws.Drawings[0].As.Shape; - var myRichtext = myShape.RichText; - var firstDefault = myShape.TextBody.Paragraphs.FirstDefaultRunProperties; - - var firstPara = myShape.TextBody.Paragraphs[0]; - var defRun = firstPara.DefaultRunProperties; - - var secondPara = myShape.TextBody.Paragraphs[1]; - var secondDefRun = secondPara.DefaultRunProperties; - } - } - - [TestMethod] - public void GenerateSvgForCharts_SecondaryAxis() - { - ExcelPackage.License.SetNonCommercialOrganization("EPPlus Project"); - using (var p = OpenTemplatePackage("ChartForSvg_SecondaryAxis.xlsx")) - { - var ws = p.Workbook.Worksheets[0]; - var renderer = new EPPlusImageRenderer.ImageRenderer(); - //var ix = 3; - //var c = ws.Drawings[ix]; - //var svg = renderer.RenderDrawingToSvg(c); - //SaveTextFileToWorkbook($"svg\\ChartForSvg_sheet2_{ix++}.svg", svg); - var ix = 1; - foreach (ExcelChart c in ws.Drawings) - { - var svg = renderer.RenderDrawingToSvg(c); - SaveTextFileToWorkbook($"svg\\ChartForSvg_SecAxis{ix++}.svg", svg); - } - } - } - [TestMethod] - public void GenerateSvgForCharts_SecondaryAxis_sheet2() - { - ExcelPackage.License.SetNonCommercialOrganization("EPPlus Project"); - using (var p = OpenTemplatePackage("ChartForSvg_SecondaryAxis.xlsx")) - { - var ws = p.Workbook.Worksheets[1]; - var renderer = new EPPlusImageRenderer.ImageRenderer(); - //var ix = 0; - //var c = ws.Drawings[ix]; - //var svg = renderer.RenderDrawingToSvg(c); - //SaveTextFileToWorkbook($"svg\\ChartForSvg_sheet2_{ix++}.svg", svg); - var ix = 1; - foreach (ExcelChart c in ws.Drawings) - { - var svg = renderer.RenderDrawingToSvg(c); - SaveTextFileToWorkbook($"svg\\ChartForSvg_Sheet2_SecAxis{ix++}.svg", svg); - } - } - } [TestMethod] public void CreateChartsWithDifferentSize() { From b5e4e4f186836e84f8b0fcb70c0a5d1e8d4d5c99 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jan=20K=C3=A4llman?= Date: Thu, 26 Mar 2026 10:00:43 +0100 Subject: [PATCH 120/151] WIP:Work on column charts to svg --- .../Svg/Chart/SvgChartLegend.cs | 40 ++++++++++++++----- .../Drawing/Chart/ExcelChartAxisStandard.cs | 14 ++----- 2 files changed, 34 insertions(+), 20 deletions(-) diff --git a/src/EPPlus.Export.ImageRenderer/Svg/Chart/SvgChartLegend.cs b/src/EPPlus.Export.ImageRenderer/Svg/Chart/SvgChartLegend.cs index 37519bc8b..1b3bf7be8 100644 --- a/src/EPPlus.Export.ImageRenderer/Svg/Chart/SvgChartLegend.cs +++ b/src/EPPlus.Export.ImageRenderer/Svg/Chart/SvgChartLegend.cs @@ -36,6 +36,7 @@ internal class SvgChartLegend : SvgChartObject const float MarginExtra = 1.5f; const float MiddleMargin = 7.5f; const float LineLength = 21; + const float BarLength = 4; internal SvgChartLegend(SvgChart sc, bool isDataLabelLegend = false) : base(sc) { _ttMeasurer = sc.Chart.WorkSheet._package.Settings.TextSettings.GenericTextMeasurerTrueType; @@ -83,7 +84,7 @@ private SvgRenderRectItem GetLegendRectangle(SvgChart sc, ExcelChartLegend l) isVertical = true; break; } - + var iconLengh = sc.Chart.IsTypeLine() ? LineLength : BarLength; var widest = 0d; var highest = 0d; var textWidth = 0d; @@ -124,7 +125,7 @@ private SvgRenderRectItem GetLegendRectangle(SvgChart sc, ExcelChartLegend l) { case eLegendPosition.Top: case eLegendPosition.Bottom: - rect.Width = textWidth + LeftMargin + RightMargin + ((LineLength + MarginExtra) * index + (MiddleMargin*Math.Max(index-1,0))) + 2 ; // 28 is for the line length + 2px between line and text + rect.Width = textWidth + LeftMargin + RightMargin + ((iconLengh + MarginExtra) * index + (MiddleMargin*Math.Max(index-1,0))) + 2 ; // 28 is for the line length + 2px between line and text rect.Height = TopMargin + BottomMargin + highest + MarginExtra; rect.Left = (sc.ChartArea.Rectangle.Width - rect.Width) / 2; if (l.Position == eLegendPosition.Top) @@ -139,8 +140,14 @@ private SvgRenderRectItem GetLegendRectangle(SvgChart sc, ExcelChartLegend l) case eLegendPosition.Right: case eLegendPosition.TopRight: case eLegendPosition.Left: - rect.Width = widest + LeftMargin + RightMargin + LineLength + 2; // 28 is for the line length + 2px between line and text - rect.Height = height + BottomMargin; + var maxHeight = sc.ChartArea.Rectangle.Height * 0.8; + rect.Width = widest + LeftMargin + RightMargin + iconLengh + 2; // 28 is for the line length + 2px between line and text + rect.Height = height; + if (rect.Height > maxHeight) + { + rect.Height = maxHeight; + } + if (l.Position == eLegendPosition.Right || l.Position == eLegendPosition.TopRight) { @@ -153,7 +160,7 @@ private SvgRenderRectItem GetLegendRectangle(SvgChart sc, ExcelChartLegend l) if (l.Position == eLegendPosition.Left || l.Position == eLegendPosition.Right) { - rect.Top = sc.ChartArea.Rectangle.Height / 2 + TopMargin + 2; + rect.Top = sc.ChartArea.Rectangle.Height / 2 - rect.Height / 2; } else { @@ -208,6 +215,21 @@ internal void SetLegend(SvgChart sc) default: break; } + if (sc.Chart.Legend.Position == eLegendPosition.Top || + sc.Chart.Legend.Position == eLegendPosition.Bottom) + { + //if (sls.Textbox.Bounds.Bottom > Rectangle.Bottom) + //{ + // break; + //} + } + else + { + if (sls.Textbox.Bounds.Bottom > Rectangle.Bottom) + { + break; + } + } SeriesIcon.Add(sls); pSls = sls; index++; @@ -289,7 +311,7 @@ private void SetBarLegend(SvgChart sc, int index, SvgLegendSerie pSls, eLegendPo sls.SeriesIcon = si; var tbLeft = si.Right + MarginExtra; - var tbTop = si.Bottom * 0.5; //TODO:Should probably be font ascent + var tbTop = si.Top - (tm.Height-si.Height)/2; double tbWidth; if (pos == eLegendPosition.Left || pos == eLegendPosition.Right) { @@ -302,7 +324,7 @@ private void SetBarLegend(SvgChart sc, int index, SvgLegendSerie pSls, eLegendPo var tbHeight = tm.Height; sls.Textbox = new SvgTextBodyItem(ChartRenderer, Bounds, tbLeft, tbTop, tbWidth, tbHeight, false, true); - sls.Textbox.Bounds.Left = si.Bottom + MarginExtra; + //sls.Textbox.Bounds.Left = si.Bottom + MarginExtra; var entry = Chart.Legend.Entries.FirstOrDefault(x => x.Index == index); var headerText = s.GetHeaderText(index); @@ -383,7 +405,7 @@ private SvgRenderRectItem GetBarSeriesIcon(SvgChart sc, ExcelChartStandardSerie } else { - x = (float)pSls.Textbox.Bounds.Right + MiddleMargin; + x = (float)pSls.Textbox.Bounds.Left; } item.Left = x; @@ -400,7 +422,7 @@ private SvgRenderRectItem GetBarSeriesIcon(SvgChart sc, ExcelChartStandardSerie } else { - y = ((SvgRenderRectItem)pSls.SeriesIcon).Top + pTm.Height / 2 + tm.Height / 2 + MiddleMargin; + y = ((SvgRenderRectItem)pSls.SeriesIcon).Top + pTm.Height / 2 + tm.Height / 2 ; } item.Left = (float)LeftMargin; //4 diff --git a/src/EPPlus/Drawing/Chart/ExcelChartAxisStandard.cs b/src/EPPlus/Drawing/Chart/ExcelChartAxisStandard.cs index cd4f9a526..70f937239 100644 --- a/src/EPPlus/Drawing/Chart/ExcelChartAxisStandard.cs +++ b/src/EPPlus/Drawing/Chart/ExcelChartAxisStandard.cs @@ -799,15 +799,7 @@ internal override List GetAxisValues(out bool isCount) List> values; GetSeriesValues(out isCount, out values); List dl; - if (IsVertical) - { - dl = values.SelectMany(x => x).Distinct().ToList(); - - } - else - { - dl = values.SelectMany(x => x).ToList(); - } + dl = values.SelectMany(x => x).Distinct().ToList(); dl.Sort(); if (Orientation == eAxisOrientation.MaxMin) { @@ -837,7 +829,7 @@ internal void GetSeriesValues(out bool isCount, out List> values) } else { - AddFromSerie(l, serie.XSeries, serie.NumberLiteralsX, serie.StringLiteralsX, true,serie.GetHeaderText(ix)); + AddFromSerie(l, serie.XSeries, serie.NumberLiteralsX, serie.StringLiteralsX, true, serie.GetHeaderText(ix)); } } else @@ -942,7 +934,7 @@ private void AddFromSerie(List list, string address, double[] numberLite { var range = ws.Cells[a.Address]; var i = 0; - for(var r=0;r Date: Thu, 26 Mar 2026 16:41:55 +0100 Subject: [PATCH 121/151] Fixed axis for svg bar charts --- .../Chart/Axis/CategoryAxisScaleCalculator.cs | 35 +++++++++++++++---- .../BarColumnChartTypeDrawer.cs | 22 +++++------- .../Svg/Chart/SvgChartAxis.cs | 11 +++--- .../Drawing/Chart/ExcelChartAxisStandard.cs | 28 +++++++++++---- 4 files changed, 65 insertions(+), 31 deletions(-) diff --git a/src/EPPlus.Export.ImageRenderer/Svg/Chart/Axis/CategoryAxisScaleCalculator.cs b/src/EPPlus.Export.ImageRenderer/Svg/Chart/Axis/CategoryAxisScaleCalculator.cs index a84297b6d..29dd7e667 100644 --- a/src/EPPlus.Export.ImageRenderer/Svg/Chart/Axis/CategoryAxisScaleCalculator.cs +++ b/src/EPPlus.Export.ImageRenderer/Svg/Chart/Axis/CategoryAxisScaleCalculator.cs @@ -21,9 +21,9 @@ internal static AxisScale CalculateByWidth(ref List values, ITextMeasure //var height = options.ChartSize.Bounds.Height; var ax = options.Axis; var plotAreaWidth = options.ChartSize.Bounds.Width; - var mf = ax.Font.GetMeasureFont(); - var displayValues = values.Select(x => x.ToString()).ToList(); - var res = tm.MeasureText(displayValues[0], mf); + var mf = ax.Font.GetMeasureFont(); + var displayValues = GetUniqueValues(values).Select(x=>x.ToString()).ToList(); + var res = tm.MeasureText(displayValues[0].ToString(), mf); //Get interval for maximum width with vertical text. var interval = GetMinUnitVerticalText(displayValues, res.Height, plotAreaWidth); @@ -42,7 +42,7 @@ internal static AxisScale CalculateByWidth(ref List values, ITextMeasure MajorInterval = interval, MinorInterval = 1, Min = 1, - Max = displayValues.Count, + Max = values.Count, TextOrientation = eTextOrientation.Horizontal }; } @@ -53,7 +53,7 @@ internal static AxisScale CalculateByWidth(ref List values, ITextMeasure MajorInterval = interval, MinorInterval = 1, Min = 1, - Max = displayValues.Count, + Max = values.Count, TextOrientation = eTextOrientation.Diagonal }; } @@ -78,12 +78,35 @@ internal static AxisScale CalculateByWidth(ref List values, ITextMeasure MajorInterval = interval, MinorInterval = 1, Min = 1, - Max = displayValues.Count, + Max = values.Count, TextOrientation = eTextOrientation.Vertical }; } + internal static List GetUniqueValues(List values) + { + var ret = new List(); + var hs = new HashSet(); + foreach(var v in values) + { + if(v is string[]) + { + var s = (string[])v; + var key = s[0] + s[1]; + if(hs.Add(key)) + { + ret.Add(s[0]); + } + } + else + { + ret.Add(v); + } + } + return ret; + } + private static bool FitAsHorizontalText(List displayValues, int interval, MeasurementFont mf, ITextMeasurer tm, double plotAreaWidth) { var margin = mf.Size * 0.3; diff --git a/src/EPPlus.Export.ImageRenderer/Svg/Chart/ChartTypeDrawers/BarColumnChartTypeDrawer.cs b/src/EPPlus.Export.ImageRenderer/Svg/Chart/ChartTypeDrawers/BarColumnChartTypeDrawer.cs index d36553e05..b16d245c1 100644 --- a/src/EPPlus.Export.ImageRenderer/Svg/Chart/ChartTypeDrawers/BarColumnChartTypeDrawer.cs +++ b/src/EPPlus.Export.ImageRenderer/Svg/Chart/ChartTypeDrawers/BarColumnChartTypeDrawer.cs @@ -3,6 +3,7 @@ using EPPlusImageRenderer.RenderItems; using EPPlusImageRenderer.Svg; using OfficeOpenXml.Drawing.Chart; +using OfficeOpenXml.FormulaParsing.Excel.Functions.Finance; using OfficeOpenXml.FormulaParsing.Excel.Functions.RefAndLookup; using OfficeOpenXml.Utils.TypeConversion; using System; @@ -99,21 +100,16 @@ private void AddColumn(ExcelBarChart chartType, ExcelBarChartSerie serie, List 1) - { - barWidth = 1 + (slotSize - 1) * (1 - overlapPercent); - } - else - { - barWidth = slotWidth * (1+overlapPercent); - } + barWidth = slotWidth / (1 + (seriesCount - 1) * step + gapPercent); + var halfGap = (barWidth * gapPercent) / 2; //barWidth = barWidth * overlapPercent; //var xl = barWidth / 2; var yAxisStart = yAxis.GetPositionInPlotarea(yAxis.Min); - for (var i = 0; i < yValues.Count; i++) + for (var i = 0; i < yValues.Count; i++) { double x; if (xValues == null || xAxis.Axis.AxisType==eAxisType.Cat) @@ -133,12 +129,12 @@ private void AddColumn(ExcelBarChart chartType, ExcelBarChartSerie serie, List GetAxisDisplayValues(ExcelChartAxisStandard ax, List AddTickmarks(double units, eTimeUnit? dateUnit, var axisStyle = GetAxisStyleEntry(); var tms = new List(); - double min, addMinor=0D; + double min, max, addMinor=0D; if(double.IsNaN(parentUnit)==false && parentUnit==units) { addMinor = parentUnit / 2; @@ -592,10 +592,12 @@ private List AddTickmarks(double units, eTimeUnit? dateUnit, if (Axis.AxisType == eAxisType.Cat) { min = 0; + max = AxisValues.Count; } else { min = Min; + max = Max; } double tickMarkWidthInside=0, tickMarkWidthOutside=0; if(type==eAxisTickMark.In || type==eAxisTickMark.Cross) @@ -606,10 +608,9 @@ private List AddTickmarks(double units, eTimeUnit? dateUnit, { tickMarkWidthOutside = tickMarkWidth; } - - var diff = Max - min; + var diff = max - min; double d = min + addMinor; - while (d <= Max) + while (d <= max) { if (double.IsNaN(parentUnit) || (d % parentUnit != 0)) { diff --git a/src/EPPlus/Drawing/Chart/ExcelChartAxisStandard.cs b/src/EPPlus/Drawing/Chart/ExcelChartAxisStandard.cs index 70f937239..e31df16d0 100644 --- a/src/EPPlus/Drawing/Chart/ExcelChartAxisStandard.cs +++ b/src/EPPlus/Drawing/Chart/ExcelChartAxisStandard.cs @@ -799,8 +799,15 @@ internal override List GetAxisValues(out bool isCount) List> values; GetSeriesValues(out isCount, out values); List dl; - dl = values.SelectMany(x => x).Distinct().ToList(); - dl.Sort(); + if(AxisType==eAxisType.Cat) + { + dl = values.SelectMany(x => x).Distinct().ToList(); + } + else + { + dl = values.SelectMany(x => x).Distinct().ToList(); + dl.Sort(); + } if (Orientation == eAxisOrientation.MaxMin) { dl.Reverse(); @@ -829,18 +836,18 @@ internal void GetSeriesValues(out bool isCount, out List> values) } else { - AddFromSerie(l, serie.XSeries, serie.NumberLiteralsX, serie.StringLiteralsX, true, serie.GetHeaderText(ix)); + AddFromSerie(l, serie.XSeries, serie.NumberLiteralsX, serie.StringLiteralsX, true, new string[] { serie.HeaderAddress.Address, serie.GetHeaderText(ix) }); } } else { if (ct.YAxis.Id == Id) { - AddFromSerie(l, serie.Series, serie.NumberLiteralsY, serie.StringLiteralsY, false, serie.GetHeaderText(ix), pl); + AddFromSerie(l, serie.Series, serie.NumberLiteralsY, serie.StringLiteralsY, false, null, pl); } else if (ct.XAxis.Id == Id) { - AddFromSerie(l, serie.XSeries, serie.NumberLiteralsX, serie.StringLiteralsX, false, serie.GetHeaderText(ix)); + AddFromSerie(l, serie.XSeries, serie.NumberLiteralsX, serie.StringLiteralsX, false, null); } } pl = l; @@ -904,7 +911,7 @@ private void AddCountFromSeries(List l, string address, double[] numberL } } - private void AddFromSerie(List list, string address, double[] numberLiterals, string[] stringLiterals, bool useText, string header, List prevList=null) + private void AddFromSerie(List list, string address, double[] numberLiterals, string[] stringLiterals, bool useText, string[] headerInfo, List prevList=null) { var isStacked = _chart.IsTypeStacked() && prevList != null; if (numberLiterals?.Length > 0) @@ -941,7 +948,14 @@ private void AddFromSerie(List list, string address, double[] numberLite var v = range.Offset(r, c, 1, 1); if (useText) { - list.Add(v.Text); + if (headerInfo == null) + { + list.Add(v.Text); + } + else + { + list.Add(new string[] { v.Text, a.Address, headerInfo[1] }); + } } else { From 56150d5e33aed66258bceda99ef86120726b0c08 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ossian=20Edstr=C3=B6m?= Date: Fri, 27 Mar 2026 16:41:25 +0100 Subject: [PATCH 122/151] Added first draft of fill translator --- .../StyleTests.cs | 57 ++++++++++ .../RenderItems/Interfaces/IBorder.cs | 14 +++ .../RenderItems/Interfaces/IFill.cs | 15 +++ .../RenderItems/Interfaces/IStylingInfo.cs | 18 +--- .../Interfaces/IStylingInfoBase.cs | 3 +- .../Style/IFillBasic.cs | 21 ++++ .../Style/IStyleExportDrawing.cs | 16 +++ .../Style/SvgChartStyleTranslator.cs | 21 ++++ .../Style/SvgFill.cs | 101 ++++++++++++++++++ .../Chart/Style/ExcelChartStyleEntry.cs | 2 +- .../Chart/Style/ExcelChartStyleManager.cs | 10 +- src/EPPlus/Drawing/Controls/ExcelControl.cs | 2 +- src/EPPlus/Drawing/ExcelDrawingFillBasic.cs | 4 +- .../Drawing/Chart/DataPointsTest.cs | 2 +- src/EPPlusTest/Drawing/DrawingTest.cs | 8 +- src/EPPlusTest/Drawing/Style/FillReadTest.cs | 6 +- src/EPPlusTest/Drawing/Style/FillTest.cs | 8 +- src/EPPlusTest/Issues/ChartIssues.cs | 2 +- 18 files changed, 272 insertions(+), 38 deletions(-) create mode 100644 src/EPPlus.Export.ImageRenderer.Test/StyleTests.cs create mode 100644 src/EPPlus.Export.ImageRenderer/RenderItems/Interfaces/IBorder.cs create mode 100644 src/EPPlus.Export.ImageRenderer/RenderItems/Interfaces/IFill.cs create mode 100644 src/EPPlus.Export.ImageRenderer/Style/IFillBasic.cs create mode 100644 src/EPPlus.Export.ImageRenderer/Style/IStyleExportDrawing.cs create mode 100644 src/EPPlus.Export.ImageRenderer/Style/SvgChartStyleTranslator.cs create mode 100644 src/EPPlus.Export.ImageRenderer/Style/SvgFill.cs diff --git a/src/EPPlus.Export.ImageRenderer.Test/StyleTests.cs b/src/EPPlus.Export.ImageRenderer.Test/StyleTests.cs new file mode 100644 index 000000000..93f658e18 --- /dev/null +++ b/src/EPPlus.Export.ImageRenderer.Test/StyleTests.cs @@ -0,0 +1,57 @@ +using OfficeOpenXml; +using OfficeOpenXml.Drawing; +using System; +using System.Collections.Generic; +using System.Drawing; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace EPPlus.Export.ImageRenderer.Tests +{ + [TestClass] + public class StyleTests : TestBase + { + [TestMethod] + public void ExtractThemeStyleWorks() + { + ExcelPackage.License.SetNonCommercialOrganization("EPPlus Project"); + + using (var p = OpenTemplatePackage("MyLineIonTheme.xlsx")) + { + var wbStyles = p.Workbook.Styles; + var simpleChart = p.Workbook.Worksheets[0].Drawings[0].As.Chart.LineChart; + + simpleChart.Title.Text = "Hello"; + + p.Workbook.ThemeManager.CurrentTheme.ColorScheme.Accent1.SetPresetColor(Color.DarkGoldenrod); + + var chartDefaultStyle = simpleChart.StyleManager.Style; + //chartDefaultStyle.DataPointLine.FillReference.Color + + //Highest order of styling if datapoint does not exist + //simpleChart.StyleManager.Style.DeleteAllNode("cs:dataPointLine"); + simpleChart.StyleManager.Style.DataPointLine.BorderReference.Color.SetPresetColor(Color.Green); + + //simpleChart.StyleManager.Style.DataPointLine.Border.Fill.DeleteNode(path); + //simpleChart.StyleManager.Style.DataPointLine.Border.Fill.Color = Color.Red; + //simpleChart.StyleManager.Style.DataPointLine.Fill.Color = Color.Green; + + //simpleChart.StyleManager.Style.DataPoint.Fill.Color = Color.Yellow; + //simpleChart.StyleManager.Style.DataPoint.Border.Fill.Color = Color.Magenta; + + simpleChart.StyleManager.ApplyStyles(); + + //var serLine = chartDefaultStyle.SeriesLine; + //var lineElement = serLine.Border.LineElement; + //var serLineFillRef = serLine.Border.Fill; + //var myFillRef = chartDefaultStyle.Title.FillReference; + //var myFill = chartDefaultStyle.Title.Fill; + //var myLine = chartDefaultStyle.Title.Border; + + SaveAndCleanup(p); + } + } + } +} + diff --git a/src/EPPlus.Export.ImageRenderer/RenderItems/Interfaces/IBorder.cs b/src/EPPlus.Export.ImageRenderer/RenderItems/Interfaces/IBorder.cs new file mode 100644 index 000000000..4f70d4441 --- /dev/null +++ b/src/EPPlus.Export.ImageRenderer/RenderItems/Interfaces/IBorder.cs @@ -0,0 +1,14 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; + +namespace EPPlus.Export.ImageRenderer.RenderItems.Interfaces +{ + public interface IBorder : IFill + { + double? BorderWidth { get; set; } + double[] BorderDashArray { get; set; } + double? BorderDashOffset { get; set; } + } +} diff --git a/src/EPPlus.Export.ImageRenderer/RenderItems/Interfaces/IFill.cs b/src/EPPlus.Export.ImageRenderer/RenderItems/Interfaces/IFill.cs new file mode 100644 index 000000000..7bdc1b33a --- /dev/null +++ b/src/EPPlus.Export.ImageRenderer/RenderItems/Interfaces/IFill.cs @@ -0,0 +1,15 @@ +using OfficeOpenXml.Drawing; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; + +namespace EPPlus.Export.ImageRenderer.RenderItems.Interfaces +{ + public interface IFill + { + string Color { get; set; } + double? Opacity { get; set; } + PathFillMode FillMode { get; set; } + } +} diff --git a/src/EPPlus.Export.ImageRenderer/RenderItems/Interfaces/IStylingInfo.cs b/src/EPPlus.Export.ImageRenderer/RenderItems/Interfaces/IStylingInfo.cs index 857dc4327..3a3c86eab 100644 --- a/src/EPPlus.Export.ImageRenderer/RenderItems/Interfaces/IStylingInfo.cs +++ b/src/EPPlus.Export.ImageRenderer/RenderItems/Interfaces/IStylingInfo.cs @@ -1,35 +1,23 @@ -using EPPlusImageRenderer.RenderItems; -using OfficeOpenXml.Drawing; +using OfficeOpenXml.Drawing; using OfficeOpenXml.Drawing.Style.Effect; using OfficeOpenXml.Drawing.Style.Fill; -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; namespace EPPlus.Export.ImageRenderer.RenderItems.Interfaces { public interface StylingInfo { - string FillColor { get; set; } + IBorder Border { get; set; } + IFill Fill { get; set; } string FilterName { get; set; } //public DrawGradientFill GradientFill { get; set; } //SvgFillType FillType { get; set; } - double? FillOpacity { get; set; } - string BorderColor { get; set; } //DrawGradientFill BorderGradientFill { get; set; } ExcelDrawingPatternFill PatternFill { get; } ExcelDrawingBlipFill BlipFill { get; } - double? BorderWidth { get; set; } - double[] BorderDashArray { get; set; } int StrokeMiterLimit { get; set; } eCompoundLineStyle CompoundLineStyle { get; set; } - double? BorderDashOffset { get; set; } eLineCap LineCap { get; set; } //SvgLineJoin LineJoin { get; set; } - double? BorderOpacity { get; set; } - PathFillMode FillColorSource { get; set; } - PathFillMode BorderColorSource { get; set; } double? GlowRadius { get; } string GlowColor { get; } ExcelDrawingOuterShadowEffect OuterShadowEffect { get; } diff --git a/src/EPPlus.Export.ImageRenderer/RenderItems/Interfaces/IStylingInfoBase.cs b/src/EPPlus.Export.ImageRenderer/RenderItems/Interfaces/IStylingInfoBase.cs index 12ecfe976..de110ec79 100644 --- a/src/EPPlus.Export.ImageRenderer/RenderItems/Interfaces/IStylingInfoBase.cs +++ b/src/EPPlus.Export.ImageRenderer/RenderItems/Interfaces/IStylingInfoBase.cs @@ -7,6 +7,7 @@ namespace EPPlus.Export.ImageRenderer.RenderItems.Interfaces { internal interface IStylingInfoBase { - + string FillColor { get; set; } + string BorderColor { get; set; } } } diff --git a/src/EPPlus.Export.ImageRenderer/Style/IFillBasic.cs b/src/EPPlus.Export.ImageRenderer/Style/IFillBasic.cs new file mode 100644 index 000000000..b80bb8fef --- /dev/null +++ b/src/EPPlus.Export.ImageRenderer/Style/IFillBasic.cs @@ -0,0 +1,21 @@ +using OfficeOpenXml.Drawing; +using OfficeOpenXml.Drawing.Theme; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; + +namespace EPPlus.Export.ImageRenderer.Style +{ + public interface IFillBasic + { + /// + /// Fill type of object + /// + eFillStyle Style { get; } + + string GetBackgroundColor(ExcelTheme theme); + string GetGradientColor1(ExcelTheme theme); + string GetGradientColor2(ExcelTheme theme); + } +} diff --git a/src/EPPlus.Export.ImageRenderer/Style/IStyleExportDrawing.cs b/src/EPPlus.Export.ImageRenderer/Style/IStyleExportDrawing.cs new file mode 100644 index 000000000..8407997c2 --- /dev/null +++ b/src/EPPlus.Export.ImageRenderer/Style/IStyleExportDrawing.cs @@ -0,0 +1,16 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; + +namespace EPPlus.Export.ImageRenderer.Style +{ + internal interface IStyleExportDrawing + { + string StyleKey { get; } + + bool HasStyle { get; } + + IFillBasic Fill { get; } + } +} diff --git a/src/EPPlus.Export.ImageRenderer/Style/SvgChartStyleTranslator.cs b/src/EPPlus.Export.ImageRenderer/Style/SvgChartStyleTranslator.cs new file mode 100644 index 000000000..853bb2491 --- /dev/null +++ b/src/EPPlus.Export.ImageRenderer/Style/SvgChartStyleTranslator.cs @@ -0,0 +1,21 @@ +using OfficeOpenXml.Drawing.Chart.Style; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; + +namespace EPPlus.Export.ImageRenderer.Style +{ + internal class SvgChartStyleTranslator : IStyleExportDrawing + { + public SvgChartStyleTranslator(ExcelChartStyle chartStyle) + { + + } + public string StyleKey => throw new NotImplementedException(); + + public bool HasStyle => throw new NotImplementedException(); + + public IFillBasic Fill => throw new NotImplementedException(); + } +} diff --git a/src/EPPlus.Export.ImageRenderer/Style/SvgFill.cs b/src/EPPlus.Export.ImageRenderer/Style/SvgFill.cs new file mode 100644 index 000000000..752102a97 --- /dev/null +++ b/src/EPPlus.Export.ImageRenderer/Style/SvgFill.cs @@ -0,0 +1,101 @@ +using OfficeOpenXml.Drawing; +using OfficeOpenXml.Drawing.Theme; +using OfficeOpenXml.Style.XmlAccess; +using System; +using System.Collections.Generic; +using System.Drawing; +using System.Globalization; +using System.Linq; +using System.Text; +using OfficeOpenXml.Drawing.Theme; +using OfficeOpenXml.Style; + +namespace EPPlus.Export.ImageRenderer.Style +{ + internal class SvgFill : IFillBasic + { + ExcelDrawingFill _fill; + + public SvgFill(ExcelDrawingFill fill) + { + Style = fill.Style; + _fill = fill; + + } + + public eFillStyle Style { get; set; } + + public bool HasValue + { + get + { + return !_fill.IsEmpty; + } + } + + public string GetBackgroundColor(ExcelTheme theme) + { + return GetColor(_fill.Color, theme); + } + + /// + /// Gets hexcode color for html as a string + /// + /// + /// + internal static string GetColor(Color c, ExcelTheme theme) + { + //Color ret; + //if (!string.IsNullOrEmpty(c)) + //{ + // if (int.TryParse(c.Rgb, NumberStyles.HexNumber, null, out int hex)) + // { + // ret = Color.FromArgb(hex); + // } + // else + // { + // ret = Color.Empty; + // } + //} + //else if (c.Theme.HasValue) + //{ + // ret = Utils.TypeConversion.ColorConverter.GetThemeColor(theme, c.Theme.Value); + //} + //else if (c.Indexed >= 0) + //{ + // ret = theme._wb.Styles.GetIndexedColor(c.Indexed); + //} + //else + //{ + // //Automatic, set to black. + // if (c.Auto) + // { + // ret = Color.Black; + // } + // else if (c.Exists) + // { + // ret = Color.Empty; + // } + // else + // { + // return null; + // } + //} + //if (c.Tint != 0) + //{ + // ret = Utils.TypeConversion.ColorConverter.ApplyTint(ret, Convert.ToDouble(c.Tint)); + //} + return "#" + c.ToArgb().ToString("x8").Substring(2); + } + + public string GetGradientColor1(ExcelTheme theme) + { + throw new NotImplementedException(); + } + + public string GetGradientColor2(ExcelTheme theme) + { + throw new NotImplementedException(); + } + } +} diff --git a/src/EPPlus/Drawing/Chart/Style/ExcelChartStyleEntry.cs b/src/EPPlus/Drawing/Chart/Style/ExcelChartStyleEntry.cs index 3de9cc266..9e6bafd5b 100644 --- a/src/EPPlus/Drawing/Chart/Style/ExcelChartStyleEntry.cs +++ b/src/EPPlus/Drawing/Chart/Style/ExcelChartStyleEntry.cs @@ -27,7 +27,7 @@ namespace OfficeOpenXml.Drawing.Chart.Style public class ExcelChartStyleEntry : XmlHelper { string _fillReferencePath = "{0}/{1}:fillRef"; - string _borderReferencePath = "{0}/{1}:lnRef "; + string _borderReferencePath = "{0}/{1}:lnRef"; string _effectReferencePath = "{0}/{1}:effectRef"; string _fontReferencePath = "{0}/{1}:fontRef"; diff --git a/src/EPPlus/Drawing/Chart/Style/ExcelChartStyleManager.cs b/src/EPPlus/Drawing/Chart/Style/ExcelChartStyleManager.cs index 558dcad1c..ad863d280 100644 --- a/src/EPPlus/Drawing/Chart/Style/ExcelChartStyleManager.cs +++ b/src/EPPlus/Drawing/Chart/Style/ExcelChartStyleManager.cs @@ -517,11 +517,11 @@ public void ApplyStyles() ApplyStyle(_chart.PlotArea, Style.PlotArea); } - //Title - if (_chart.HasTitle) - { - ApplyStyle(_chart.Title, Style.Title); - } + ////Title + //if (_chart.HasTitle) + //{ + // ApplyStyle(_chart.Title, Style.Title); + //} if (_chart.PlotArea.DataTable!=null) { diff --git a/src/EPPlus/Drawing/Controls/ExcelControl.cs b/src/EPPlus/Drawing/Controls/ExcelControl.cs index eb9d51105..79aae7407 100644 --- a/src/EPPlus/Drawing/Controls/ExcelControl.cs +++ b/src/EPPlus/Drawing/Controls/ExcelControl.cs @@ -637,7 +637,7 @@ internal virtual void UpdateXml() { fill.Color = color; } - fill.Transparancy = (int)c.Fill.Opacity - 100; + fill.Transparency = (int)c.Fill.Opacity - 100; } } } diff --git a/src/EPPlus/Drawing/ExcelDrawingFillBasic.cs b/src/EPPlus/Drawing/ExcelDrawingFillBasic.cs index 23256b395..da9f1d1a7 100644 --- a/src/EPPlus/Drawing/ExcelDrawingFillBasic.cs +++ b/src/EPPlus/Drawing/ExcelDrawingFillBasic.cs @@ -303,10 +303,10 @@ public ExcelDrawingGradientFill GradientFill } } /// - /// Transparancy in percent from a solid fill. + /// Transparency in percent from a solid fill. /// This is the same as 100-Fill.Transform.Alpha /// - public int Transparancy + public int Transparency { get { diff --git a/src/EPPlusTest/Drawing/Chart/DataPointsTest.cs b/src/EPPlusTest/Drawing/Chart/DataPointsTest.cs index 36b732c83..dd7685b1b 100644 --- a/src/EPPlusTest/Drawing/Chart/DataPointsTest.cs +++ b/src/EPPlusTest/Drawing/Chart/DataPointsTest.cs @@ -93,7 +93,7 @@ public void BarChart() point.Border.Fill.Style = eFillStyle.SolidFill; point.Fill.Style = eFillStyle.SolidFill; point.Fill.SolidFill.Color.SetRgbColor(Color.Yellow); - point.Fill.Transparancy = 5; + point.Fill.Transparency = 5; Assert.AreEqual(eColorTransformType.Alpha, point.Fill.SolidFill.Color.Transforms[0].Type); Assert.AreEqual(95, point.Fill.SolidFill.Color.Transforms[0].Value); chart.SetPosition(1, 0, 5, 0); diff --git a/src/EPPlusTest/Drawing/DrawingTest.cs b/src/EPPlusTest/Drawing/DrawingTest.cs index f209f1284..9487d5c6d 100644 --- a/src/EPPlusTest/Drawing/DrawingTest.cs +++ b/src/EPPlusTest/Drawing/DrawingTest.cs @@ -95,7 +95,7 @@ public void Picture() pic.Border.Fill.Color = Color.DarkCyan; pic.Fill.Style = eFillStyle.SolidFill; pic.Fill.Color = Color.White; - pic.Fill.Transparancy = 50; + pic.Fill.Transparency = 50; pic = ws.Drawings.AddPicture("Pic3", Resources.Test1); pic.SetPosition(400, 200); @@ -328,7 +328,7 @@ public void Scatter() chrt.Title.Fill.Style = eFillStyle.SolidFill; chrt.Title.Fill.Color = Color.LightBlue; - chrt.Title.Fill.Transparancy = 50; + chrt.Title.Fill.Transparency = 50; chrt.VaryColors = true; ExcelScatterChartSerie ser = chrt.Series[0] as ExcelScatterChartSerie; ser.DataLabel.Position = eLabelPosition.Center; @@ -664,7 +664,7 @@ public void Drawings() (ws.Drawings["shape5"] as ExcelShape).Fill.Style = eFillStyle.SolidFill; (ws.Drawings["shape5"] as ExcelShape).Fill.Color = Color.Red; - (ws.Drawings["shape5"] as ExcelShape).Fill.Transparancy = 50; + (ws.Drawings["shape5"] as ExcelShape).Fill.Transparency = 50; (ws.Drawings["shape6"] as ExcelShape).Fill.Style = eFillStyle.NoFill; (ws.Drawings["shape6"] as ExcelShape).Font.Fill.Color = Color.Black; @@ -674,7 +674,7 @@ public void Drawings() (ws.Drawings["shape7"] as ExcelShape).Fill.Color = Color.Gray; (ws.Drawings["shape7"] as ExcelShape).Border.Fill.Style = eFillStyle.SolidFill; (ws.Drawings["shape7"] as ExcelShape).Border.Fill.Color = Color.Black; - (ws.Drawings["shape7"] as ExcelShape).Border.Fill.Transparancy = 43; + (ws.Drawings["shape7"] as ExcelShape).Border.Fill.Transparency = 43; (ws.Drawings["shape7"] as ExcelShape).Border.LineCap = eLineCap.Round; (ws.Drawings["shape7"] as ExcelShape).Border.LineStyle = eLineStyle.LongDash; (ws.Drawings["shape7"] as ExcelShape).Font.UnderLineColor = Color.Blue; diff --git a/src/EPPlusTest/Drawing/Style/FillReadTest.cs b/src/EPPlusTest/Drawing/Style/FillReadTest.cs index 4d1a91f02..de82a90ea 100644 --- a/src/EPPlusTest/Drawing/Style/FillReadTest.cs +++ b/src/EPPlusTest/Drawing/Style/FillReadTest.cs @@ -168,7 +168,7 @@ public void ReadSolidFill_ColorSystem() public void ReadTransparancy() { //Setup - var wsName = "Transparancy"; + var wsName = "Transparency"; var expected = 45; var ws = _pck.Workbook.Worksheets[wsName]; if (ws == null) Assert.Inconclusive($"{wsName} worksheet is missing"); @@ -176,7 +176,7 @@ public void ReadTransparancy() //Assert Assert.AreEqual(eFillStyle.SolidFill, shape.Fill.Style); - Assert.AreEqual(expected, shape.Fill.Transparancy); + Assert.AreEqual(expected, shape.Fill.Transparency); Assert.AreEqual(eColorTransformType.Alpha, shape.Fill.SolidFill.Color.Transforms[0].Type); Assert.AreEqual(100 - expected, shape.Fill.SolidFill.Color.Transforms[0].Value); } @@ -192,7 +192,7 @@ public void ReadTransformAlpha() //Assert Assert.AreEqual(eFillStyle.SolidFill, shape.Fill.Style); - Assert.AreEqual(100 - expected, shape.Fill.Transparancy); + Assert.AreEqual(100 - expected, shape.Fill.Transparency); Assert.AreEqual(eColorTransformType.Alpha, shape.Fill.SolidFill.Color.Transforms[0].Type); Assert.AreEqual(expected, shape.Fill.SolidFill.Color.Transforms[0].Value); } diff --git a/src/EPPlusTest/Drawing/Style/FillTest.cs b/src/EPPlusTest/Drawing/Style/FillTest.cs index e5dd9c9f2..4bfdf6f04 100644 --- a/src/EPPlusTest/Drawing/Style/FillTest.cs +++ b/src/EPPlusTest/Drawing/Style/FillTest.cs @@ -242,19 +242,19 @@ public void Transparancy() { //Setup var expected = 45; - var ws = _pck.Workbook.Worksheets.Add("Transparancy"); + var ws = _pck.Workbook.Worksheets.Add("Transparency"); var shape = ws.Drawings.AddShape("Shape1", eShapeStyle.Rect); shape.SetPosition(1, 0, 5, 0); //Act shape.Fill.Color = Color.Red; - shape.Fill.Transparancy = expected; + shape.Fill.Transparency = expected; //Assert Assert.AreEqual(eFillStyle.SolidFill, shape.Fill.Style); Assert.IsInstanceOfType(shape.Fill.SolidFill.Color.RgbColor, typeof(ExcelDrawingRgbColor)); - Assert.AreEqual(expected, shape.Fill.Transparancy); + Assert.AreEqual(expected, shape.Fill.Transparency); Assert.AreEqual(100 - expected, shape.Fill.SolidFill.Color.Transforms[0].Value); } [TestMethod] @@ -274,7 +274,7 @@ public void TransformAlpha() //Assert Assert.AreEqual(eFillStyle.SolidFill, shape.Fill.Style); Assert.IsInstanceOfType(shape.Fill.SolidFill.Color.RgbColor, typeof(ExcelDrawingRgbColor)); - Assert.AreEqual(100 - expected, shape.Fill.Transparancy); + Assert.AreEqual(100 - expected, shape.Fill.Transparency); Assert.AreEqual(eColorTransformType.Alpha, shape.Fill.SolidFill.Color.Transforms[0].Type); Assert.AreEqual(expected, shape.Fill.SolidFill.Color.Transforms[0].Value); } diff --git a/src/EPPlusTest/Issues/ChartIssues.cs b/src/EPPlusTest/Issues/ChartIssues.cs index f537b666c..73ffc622e 100644 --- a/src/EPPlusTest/Issues/ChartIssues.cs +++ b/src/EPPlusTest/Issues/ChartIssues.cs @@ -117,7 +117,7 @@ public void s598() chart.Legend.TextSettings.Fill.Style = OfficeOpenXml.Drawing.eFillStyle.SolidFill; chart.Legend.TextSettings.Fill.SolidFill.Color.SetRgbColor(System.Drawing.Color.Black); - chart.Legend.TextSettings.Fill.Transparancy = 0; + chart.Legend.TextSettings.Fill.Transparency = 0; chart.Legend.TextSettings.Effect.SetPresetReflection(OfficeOpenXml.Drawing.ePresetExcelReflectionType.FullTouching); SaveAndCleanup(p); From 4ffbd77a9e54aefc71a00dd9ed8da7ac6da38701 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jan=20K=C3=A4llman?= Date: Mon, 30 Mar 2026 11:04:59 +0200 Subject: [PATCH 123/151] WIP:Work on legends with multiple rows --- .../Chart/ColumnChartTests.cs | 20 +++--- .../BarColumnChartTypeDrawer.cs | 2 +- .../Svg/Chart/SvgChartAxis.cs | 27 +++----- .../Svg/Chart/SvgChartLegend.cs | 69 ++++++++++++++----- 4 files changed, 72 insertions(+), 46 deletions(-) diff --git a/src/EPPlus.Export.ImageRenderer.Test/Chart/ColumnChartTests.cs b/src/EPPlus.Export.ImageRenderer.Test/Chart/ColumnChartTests.cs index e54ca78fa..101adc816 100644 --- a/src/EPPlus.Export.ImageRenderer.Test/Chart/ColumnChartTests.cs +++ b/src/EPPlus.Export.ImageRenderer.Test/Chart/ColumnChartTests.cs @@ -20,17 +20,17 @@ public void GenerateSvgForColumnCharts1() var ws = p.Workbook.Worksheets[0]; var renderer = new EPPlusImageRenderer.ImageRenderer(); - //var ix = 1; - //var c = ws.Drawings[ix]; - //var svg = renderer.RenderDrawingToSvg(c); - //SaveTextFileToWorkbook($"svg\\ChartForSvg_ind{ix++}.svg", svg); + var ix = 1; + var c = ws.Drawings[ix]; + var svg = renderer.RenderDrawingToSvg(c); + SaveTextFileToWorkbook($"svg\\ChartForSvg_ind{ix++}.svg", svg); - var ix = 0; - foreach (ExcelChart c in ws.Drawings) - { - var svg = renderer.RenderDrawingToSvg(c); - SaveTextFileToWorkbook($"svg\\BarChartForSvg_sheet1_{ix++}.svg", svg); - } + //var ix = 0; + //foreach (ExcelChart c in ws.Drawings) + //{ + // var svg = renderer.RenderDrawingToSvg(c); + // SaveTextFileToWorkbook($"svg\\BarChartForSvg_sheet1_{ix++}.svg", svg); + //} } } [TestMethod] diff --git a/src/EPPlus.Export.ImageRenderer/Svg/Chart/ChartTypeDrawers/BarColumnChartTypeDrawer.cs b/src/EPPlus.Export.ImageRenderer/Svg/Chart/ChartTypeDrawers/BarColumnChartTypeDrawer.cs index b16d245c1..80e6aa998 100644 --- a/src/EPPlus.Export.ImageRenderer/Svg/Chart/ChartTypeDrawers/BarColumnChartTypeDrawer.cs +++ b/src/EPPlus.Export.ImageRenderer/Svg/Chart/ChartTypeDrawers/BarColumnChartTypeDrawer.cs @@ -125,7 +125,7 @@ private void AddColumn(ExcelBarChart chartType, ExcelBarChartSerie serie, List GetAxisValueTextBoxes() } else { - x = ticMarkX - width / 2; + x = ticMarkX; // - width / 2; y = ticMarkY; } } @@ -495,8 +495,9 @@ private double GetAxisItemLeft(int i, OfficeOpenXml.Interfaces.Drawing.Text.Text } else { - if (Axis.AxisType == eAxisType.Cat) + if ((Axis.AxisType == eAxisType.Cat || Axis.IsVertical==false) && LabelOrientation==eTextOrientation.Horizontal) { + //Between tickmarks var majorWidth = Rectangle.Width / AxisValues.Count; var majorTickStartingPosition = Rectangle.Left + majorWidth * i; var middleOfBounds = majorTickStartingPosition + (majorWidth / 2); @@ -589,9 +590,10 @@ private List AddTickmarks(double units, eTimeUnit? dateUnit, { addMinor = parentUnit / 2; } + if (Axis.AxisType == eAxisType.Cat) { - min = 0; + min = 1; max = AxisValues.Count; } else @@ -599,6 +601,7 @@ private List AddTickmarks(double units, eTimeUnit? dateUnit, min = Min; max = Max; } + double tickMarkWidthInside=0, tickMarkWidthOutside=0; if(type==eAxisTickMark.In || type==eAxisTickMark.Cross) { @@ -608,9 +611,9 @@ private List AddTickmarks(double units, eTimeUnit? dateUnit, { tickMarkWidthOutside = tickMarkWidth; } - var diff = max - min; + var diff = max - min + 1; double d = min + addMinor; - while (d <= max) + while (d <= max+1) { if (double.IsNaN(parentUnit) || (d % parentUnit != 0)) { @@ -656,8 +659,6 @@ private List AddTickmarks(double units, eTimeUnit? dateUnit, } tms.Add(tm); } - //if (dateUnit.HasValue) - //{ switch (dateUnit) { case eTimeUnit.Years: @@ -679,12 +680,6 @@ private List AddTickmarks(double units, eTimeUnit? dateUnit, d += units; break; } - // } - // } - // else - // { - // d += units; - // } } return tms; } @@ -773,7 +768,7 @@ internal double GetPositionInPlotarea(double val) else { if (val < Min || val > Max) return double.NaN; - var diff = Max - Min; + var diff = Max - Min+1; return (((Max-val) / diff * SvgChart.Plotarea.Rectangle.Height)); } } @@ -787,7 +782,7 @@ internal double GetPositionInPlotarea(double val) else { if (val < Min || val > Max) return double.NaN; - var diff = Max - Min; + var diff = Max - Min+1; return (((val-Min) / diff * SvgChart.Plotarea.Rectangle.Width)); } } @@ -881,7 +876,7 @@ protected List GetAxisValue(ExcelChartAxisStandard ax, RenderItem rect, dateUnit = res.MajorDateUnit; var dt = DateTime.FromOADate(res.Min); var maxDt = DateTime.FromOADate(res.Max); - while (dt < maxDt) + while (dt <= maxDt) { l.Add(dt); switch(res.MajorDateUnit ?? eTimeUnit.Days) diff --git a/src/EPPlus.Export.ImageRenderer/Svg/Chart/SvgChartLegend.cs b/src/EPPlus.Export.ImageRenderer/Svg/Chart/SvgChartLegend.cs index 1b3bf7be8..4c06a59ca 100644 --- a/src/EPPlus.Export.ImageRenderer/Svg/Chart/SvgChartLegend.cs +++ b/src/EPPlus.Export.ImageRenderer/Svg/Chart/SvgChartLegend.cs @@ -74,22 +74,27 @@ private SvgRenderRectItem GetLegendRectangle(SvgChart sc, ExcelChartLegend l) { var rect = new SvgRenderRectItem(sc, sc.Bounds); bool isVertical; + double maxWidth, maxHeight; switch (l.Position) { case eLegendPosition.Top: case eLegendPosition.Bottom: isVertical = false; + maxWidth = sc.ChartArea.Rectangle.Width * 0.8; + maxHeight = sc.ChartArea.Rectangle.Height * 0.6; break; default: - isVertical = true; + isVertical = true; + maxWidth = sc.ChartArea.Rectangle.Width * 0.6; + maxHeight = sc.ChartArea.Rectangle.Height * 0.8; break; } var iconLengh = sc.Chart.IsTypeLine() ? LineLength : BarLength; var widest = 0d; var highest = 0d; - var textWidth = 0d; - var height = TopMargin; + var hight = TopMargin; var index = 0; + double width=0d; foreach (var ct in sc.Chart.PlotArea.ChartTypes) { foreach (var s in ct.Series) @@ -107,26 +112,58 @@ private SvgRenderRectItem GetLegendRectangle(SvgChart sc, ExcelChartLegend l) } var tm = _ttMeasurer.MeasureText(text, font.GetMeasureFont()); _seriesHeadersMeasure.Add(tm); - if(tm.Width > widest) + if (isVertical) { - widest = tm.Width; + width += tm.Width; + if (tm.Width > widest) + { + widest = tm.Width; + } + if (tm.Height > hight) + { + highest = tm.Height; + } + if(hight==0) + { + hight += TopMargin + tm.Height; + } + else + { + hight += MiddleMargin + tm.Height; + } } - if (tm.Height > height) + else { - highest = tm.Height; + if(width + tm.Width >= maxWidth) + { + if(width > widest) + { + widest = width; + } + width = tm.Width; + hight += highest + MiddleMargin; + highest = 0; + } + else + { + width += tm.Width + MiddleMargin; + if (highest < tm.Height) + { + highest = tm.Height; + } + } } - textWidth += tm.Width; - height += tm.Height + MiddleMargin; + index++; } } - height = height - MiddleMargin + BottomMargin; //remove last margin and add bottom margin + hight += BottomMargin; //remove last margin and add bottom margin switch (l.Position) { case eLegendPosition.Top: case eLegendPosition.Bottom: - rect.Width = textWidth + LeftMargin + RightMargin + ((iconLengh + MarginExtra) * index + (MiddleMargin*Math.Max(index-1,0))) + 2 ; // 28 is for the line length + 2px between line and text - rect.Height = TopMargin + BottomMargin + highest + MarginExtra; + rect.Width = width + LeftMargin + RightMargin + ((iconLengh + MarginExtra) * index + (MiddleMargin*Math.Max(index-1,0))) + 2 ; // 28 is for the line length + 2px between line and text + rect.Height = TopMargin + BottomMargin + hight + MarginExtra; rect.Left = (sc.ChartArea.Rectangle.Width - rect.Width) / 2; if (l.Position == eLegendPosition.Top) { @@ -140,9 +177,8 @@ private SvgRenderRectItem GetLegendRectangle(SvgChart sc, ExcelChartLegend l) case eLegendPosition.Right: case eLegendPosition.TopRight: case eLegendPosition.Left: - var maxHeight = sc.ChartArea.Rectangle.Height * 0.8; rect.Width = widest + LeftMargin + RightMargin + iconLengh + 2; // 28 is for the line length + 2px between line and text - rect.Height = height; + rect.Height = hight; if (rect.Height > maxHeight) { rect.Height = maxHeight; @@ -175,12 +211,7 @@ private SvgRenderRectItem GetLegendRectangle(SvgChart sc, ExcelChartLegend l) } break; } - if (isVertical) - { - //var top = sc.Title.GetRectangle.Height+8+10; - //var width = margin; - } return rect; } From 6cf2ad6ca495e15cdf1291a70ca4d6a84b323a80 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ossian=20Edstr=C3=B6m?= Date: Mon, 30 Mar 2026 17:09:41 +0200 Subject: [PATCH 124/151] Created basic css-exporter for svg fill and border --- .../StyleTests.cs | 52 +++++- .../Style/SvgChartStyleTranslator.cs | 6 +- .../Style/SvgFill.cs | 28 ++- .../Style/SvgFillTranslator.cs | 90 +++++++++ src/EPPlus/Drawing/Chart/ExcelChartLine.cs | 11 ++ .../CssCollections/CssChartRuleCollection.cs | 172 ++++++++++++++++++ .../Internal/CssChartExporterSync.cs | 102 +++++++++++ .../Exporters/Internal/CssExporterBase.cs | 5 +- .../Settings/CssChartExportSettings.cs | 14 ++ .../StyleCollectors/BorderDrawing.cs | 24 +++ .../HtmlExport/StyleCollectors/FillDrawing.cs | 145 +++++++++++++++ .../StyleCollectors/FillDrawingBasic.cs | 144 +++++++++++++++ .../StyleContracts/IDrawingBorder.cs | 12 ++ .../Translators/CssFillTranslator.cs | 16 +- .../Translators/CssStrokeTranslator.cs | 33 ++++ .../Translators/TranslatorContext.cs | 5 + .../HtmlExport/CssRuleCollectionTests.cs | 52 +++++- 17 files changed, 889 insertions(+), 22 deletions(-) create mode 100644 src/EPPlus.Export.ImageRenderer/Style/SvgFillTranslator.cs create mode 100644 src/EPPlus/Export/HtmlExport/CssCollections/CssChartRuleCollection.cs create mode 100644 src/EPPlus/Export/HtmlExport/Exporters/Internal/CssChartExporterSync.cs create mode 100644 src/EPPlus/Export/HtmlExport/Settings/CssChartExportSettings.cs create mode 100644 src/EPPlus/Export/HtmlExport/StyleCollectors/BorderDrawing.cs create mode 100644 src/EPPlus/Export/HtmlExport/StyleCollectors/FillDrawing.cs create mode 100644 src/EPPlus/Export/HtmlExport/StyleCollectors/FillDrawingBasic.cs create mode 100644 src/EPPlus/Export/HtmlExport/StyleCollectors/StyleContracts/IDrawingBorder.cs create mode 100644 src/EPPlus/Export/HtmlExport/Translators/CssStrokeTranslator.cs diff --git a/src/EPPlus.Export.ImageRenderer.Test/StyleTests.cs b/src/EPPlus.Export.ImageRenderer.Test/StyleTests.cs index 93f658e18..5a478e9a6 100644 --- a/src/EPPlus.Export.ImageRenderer.Test/StyleTests.cs +++ b/src/EPPlus.Export.ImageRenderer.Test/StyleTests.cs @@ -1,9 +1,16 @@ -using OfficeOpenXml; +using EPPlus.Export.ImageRenderer.Style; +using OfficeOpenXml; using OfficeOpenXml.Drawing; +using OfficeOpenXml.Export.HtmlExport; +using OfficeOpenXml.Export.HtmlExport.CssCollections; +using OfficeOpenXml.Export.HtmlExport.Translators; +using OfficeOpenXml.Export.HtmlExport.Writers; +using OfficeOpenXml.Utils; using System; using System.Collections.Generic; using System.Drawing; using System.Linq; +using System.Runtime; using System.Text; using System.Threading.Tasks; @@ -27,6 +34,20 @@ public void ExtractThemeStyleWorks() p.Workbook.ThemeManager.CurrentTheme.ColorScheme.Accent1.SetPresetColor(Color.DarkGoldenrod); var chartDefaultStyle = simpleChart.StyleManager.Style; + + var fillStyle = simpleChart.StyleManager.Style.DataPointLine.Border.Fill; + + var svgFill = new SvgFill(fillStyle); + var fillTranslator = new SvgFillTranslator(svgFill); + + var context = new TranslatorContext(new HtmlRangeExportSettings()); + + var declarations = fillTranslator.GenerateDeclarationList(context); + + var styleClass = new CssRule("Style", 0); + + context.SetTranslator(fillTranslator); + context.AddDeclarations(styleClass); //chartDefaultStyle.DataPointLine.FillReference.Color //Highest order of styling if datapoint does not exist @@ -51,7 +72,36 @@ public void ExtractThemeStyleWorks() SaveAndCleanup(p); } + } + ///// + ///// Exports an to a html string + ///// + ///// A html table + //public string GetCssString() + //{ + // using (var ms = EPPlusMemoryManager.GetStream()) + // { + // RenderCss(ms); + // ms.Position = 0; + // using (var sr = new StreamReader(ms)) + // { + // return sr.ReadToEnd(); + // } + // } + //} + ///// + ///// Exports the css part of the html export. + ///// + ///// The stream to write the css to. + ///// + //public void RenderCss(Stream stream) + //{ + // var trueWriter = new CssWriter(stream); + // var cssRules = CreateRuleCollection(_settings); + + // trueWriter.WriteAndClearFlush(cssRules, Settings.Minify); + //} } } diff --git a/src/EPPlus.Export.ImageRenderer/Style/SvgChartStyleTranslator.cs b/src/EPPlus.Export.ImageRenderer/Style/SvgChartStyleTranslator.cs index 853bb2491..34c760aa8 100644 --- a/src/EPPlus.Export.ImageRenderer/Style/SvgChartStyleTranslator.cs +++ b/src/EPPlus.Export.ImageRenderer/Style/SvgChartStyleTranslator.cs @@ -8,9 +8,9 @@ namespace EPPlus.Export.ImageRenderer.Style { internal class SvgChartStyleTranslator : IStyleExportDrawing { - public SvgChartStyleTranslator(ExcelChartStyle chartStyle) - { - + public SvgChartStyleTranslator(ExcelChartStyleEntry chartStyle) + { + var fill = chartStyle.Fill; } public string StyleKey => throw new NotImplementedException(); diff --git a/src/EPPlus.Export.ImageRenderer/Style/SvgFill.cs b/src/EPPlus.Export.ImageRenderer/Style/SvgFill.cs index 752102a97..7783561ef 100644 --- a/src/EPPlus.Export.ImageRenderer/Style/SvgFill.cs +++ b/src/EPPlus.Export.ImageRenderer/Style/SvgFill.cs @@ -9,18 +9,24 @@ using System.Text; using OfficeOpenXml.Drawing.Theme; using OfficeOpenXml.Style; +using OfficeOpenXml.Utils.EnumUtils; namespace EPPlus.Export.ImageRenderer.Style { internal class SvgFill : IFillBasic { - ExcelDrawingFill _fill; + ExcelDrawingFillBasic _fill; - public SvgFill(ExcelDrawingFill fill) + public SvgFill(ExcelDrawingFillBasic fill) { Style = fill.Style; _fill = fill; + } + public SvgFill(ExcelDrawingFill fill) + { + Style = fill.Style; + _fill = fill; } public eFillStyle Style { get; set; } @@ -33,6 +39,15 @@ public bool HasValue } } + + public bool IsGradient + { + get + { + return Style == eFillStyle.GradientFill; + } + } + public string GetBackgroundColor(ExcelTheme theme) { return GetColor(_fill.Color, theme); @@ -46,7 +61,7 @@ public string GetBackgroundColor(ExcelTheme theme) internal static string GetColor(Color c, ExcelTheme theme) { //Color ret; - //if (!string.IsNullOrEmpty(c)) + //if (!string.IsNullOrEmpty(c.ToColorString())) //{ // if (int.TryParse(c.Rgb, NumberStyles.HexNumber, null, out int hex)) // { @@ -85,17 +100,18 @@ internal static string GetColor(Color c, ExcelTheme theme) //{ // ret = Utils.TypeConversion.ColorConverter.ApplyTint(ret, Convert.ToDouble(c.Tint)); //} + return "#" + c.ToArgb().ToString("x8").Substring(2); } public string GetGradientColor1(ExcelTheme theme) { - throw new NotImplementedException(); + return GetColor(_fill.GradientFill.Colors.ToArray()[0].Color.GetColor(), theme); } - public string GetGradientColor2(ExcelTheme theme) { - throw new NotImplementedException(); + return GetColor(_fill.GradientFill.Colors.ToArray()[1].Color.GetColor(), theme); } + } } diff --git a/src/EPPlus.Export.ImageRenderer/Style/SvgFillTranslator.cs b/src/EPPlus.Export.ImageRenderer/Style/SvgFillTranslator.cs new file mode 100644 index 000000000..634146cd3 --- /dev/null +++ b/src/EPPlus.Export.ImageRenderer/Style/SvgFillTranslator.cs @@ -0,0 +1,90 @@ +using OfficeOpenXml.Drawing; +using OfficeOpenXml.Drawing.Theme; +using OfficeOpenXml.Export.HtmlExport; +using OfficeOpenXml.Export.HtmlExport.CssCollections; +using OfficeOpenXml.Export.HtmlExport.Translators; +using OfficeOpenXml.Style; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +namespace EPPlus.Export.ImageRenderer.Style +{ + internal class SvgFillTranslator : TranslatorBase + { + ExcelTheme _theme; + IFillBasic _fill; + + internal SvgFillTranslator(IFillBasic fill) + { + _fill = fill; + } + + internal override List GenerateDeclarationList(TranslatorContext context) + { + _theme = context.Theme; + + if (context.Exclude.Fill) return null; + + if (_fill.Style == eFillStyle.GradientFill) + { + AddGradient(); + } + else + { + if (_fill.Style == eFillStyle.PatternFill) + { + var bc = _fill.GetBackgroundColor(_theme) ?? "#0"; + if (string.IsNullOrEmpty(bc) == false) + { + AddDeclaration("fill", bc); + } + } + else if (_fill.Style == eFillStyle.NoFill) + { + var fc = _fill.GetBackgroundColor(_theme); + if (string.IsNullOrEmpty(fc) == false) + { + AddDeclaration("fill", fc); + } + } + else if(_fill.Style == eFillStyle.BlipFill) + { + string bgColor = _fill.GetBackgroundColor(_theme) ?? "#0"; + //string patternColor = _fill.GetPatternColor(_theme) ?? "#0"; + + var svg = PatternFills.GetPatternSvgConvertedOnly(ExcelFillStyle.Solid, bgColor, ""); + AddDeclaration("background-repeat", "repeat"); + //arguably some of the values should be its own declaration...Should still work though. + AddDeclaration("background", $"url(data:image/svg+xml;base64,{svg})"); + } + } + + return declarations; + } + + private void AddGradient() + { + //AddDeclaration("linearGradient"); + + //GetLinearGradientNodeStop + //AddDeclaration("stop"); + //var gradientDeclaration = declarations.LastOrDefault(); + + //if (_fill.Style == eFillStyle.GradientFill) + //{ + // AddDeclaration("offset",) + //} + //else + //{ + // gradientDeclaration.AddValues($"radial-gradient(ellipse {_fill.Right * 100}% {_fill.Bottom * 100}%"); + //} + + //gradientDeclaration.AddValues + // ( + // $",{_fill.GetGradientColor1(_theme)} 0%", + // $",{_fill.GetGradientColor2(_theme)} 100%)" + // ); + } + } +} diff --git a/src/EPPlus/Drawing/Chart/ExcelChartLine.cs b/src/EPPlus/Drawing/Chart/ExcelChartLine.cs index 6e4a8f27f..ef674d442 100644 --- a/src/EPPlus/Drawing/Chart/ExcelChartLine.cs +++ b/src/EPPlus/Drawing/Chart/ExcelChartLine.cs @@ -102,6 +102,17 @@ void IDrawingStyleBase.CreatespPr() { CreatespPrNode(); } + + internal bool HasValue() + { + if(_threeD != null | _effect != null | _fill != null | _border != null) + { + return true; + } + + return false; + } + /// /// Removes the item /// diff --git a/src/EPPlus/Export/HtmlExport/CssCollections/CssChartRuleCollection.cs b/src/EPPlus/Export/HtmlExport/CssCollections/CssChartRuleCollection.cs new file mode 100644 index 000000000..453c7791e --- /dev/null +++ b/src/EPPlus/Export/HtmlExport/CssCollections/CssChartRuleCollection.cs @@ -0,0 +1,172 @@ +using OfficeOpenXml.Drawing; +using OfficeOpenXml.Drawing.Chart; +using OfficeOpenXml.Drawing.Chart.Style; +using OfficeOpenXml.Drawing.Theme; +using OfficeOpenXml.Export.HtmlExport.Exporters.Internal; +using OfficeOpenXml.Export.HtmlExport.Settings; +using OfficeOpenXml.Export.HtmlExport.StyleCollectors; +using OfficeOpenXml.Export.HtmlExport.Translators; +using OfficeOpenXml.Style; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Runtime.InteropServices; +using System.Text; + +namespace OfficeOpenXml.Export.HtmlExport.CssCollections +{ + internal class CssChartRuleCollection : CssRuleCollection + { + CssExportSettings _settings; + ExcelChart _chart; + ExcelTheme _theme; + TranslatorContext _context; + + public CssChartRuleCollection(ExcelChart chart, CssExportSettings settings) + { + _chart = chart; + _settings = settings; + + if (_chart.WorkSheet.Workbook.ThemeManager.CurrentTheme == null) + { + _chart.WorkSheet.Workbook.ThemeManager.CreateDefaultTheme(); + } + _theme = _chart.WorkSheet.Workbook.ThemeManager.CurrentTheme; + + _context = new TranslatorContext(settings); + _context.Theme = _theme; + + //RuleCollection = new CssRuleCollection(); + + AddChartFillToCollection(chart, "epp-"); + } + + internal void AddChartFillToCollection(ExcelChart chart, string chartClassPreset) + { + var chartClass = $"{chartClassPreset}{HtmlExportTableUtil.GetClassName(chart.Name, $"chartstyle{chart.Id}")}"; + + var s = chart.StyleManager.Style; + + if(s == null) + { + chart.StyleManager.ApplyStyles(); + } + + s = chart.StyleManager.Style; + + if (s != null) + { + + var chartAreaRef = chart.StyleManager.Style.ChartArea.FillReference.Color; + + AddToCollection(chartClass, chart.Fill, chart.Border, "rect"); + + + //var fallbackElementBorder = chart.StyleManager.Style.ChartArea.BorderReference.Color; + + //AddToCollection(chartClass, chart.Border, "border"); + } + } + + internal void AddToCollection(string name, ExcelDrawingBorder element, string htmlElement) + { + //if (element) return; //Dont add empty elements + + var s = element; + + var styleClass = new CssRule($"{htmlElement}.{name}", int.MaxValue); + + var translators = new List(); + + if (element != null && _context.Exclude.Fill == false) + { + //TODO: Ensure if gradients with more than 2 colors it is handled correctly. + translators.Add(new CssFillTranslator(new FillDrawingBasic(element.Fill))); + } + //if (s.Font != null && _context.Exclude.Font != eFontExclude.All) + //{ + // translators.Add(new CssFontTranslator(new FontDxf(s.Font), null)); + //} + //if (s.Border != null && _context.Exclude.Border != eBorderExclude.All) + //{ + // translators.Add(new CssBorderTranslator(new BorderDxf(s.Border))); + //} + + foreach (var translator in translators) + { + _context.SetTranslator(translator); + _context.AddDeclarations(styleClass); + } + + AddRule(styleClass); + } + + internal void AddToCollection(string name, ExcelDrawingFill element, ExcelDrawingBorder border, string htmlElement) + { + if (element.IsEmpty) return; //Dont add empty elements + + var s = element; + + var styleClass = new CssRule($"{htmlElement}.{name}", int.MaxValue); + + var translators = new List(); + + if (element != null && _context.Exclude.Fill == false) + { + var fillGeneric = new FillDrawing(element); + var fillTranslator = new CssFillTranslator(fillGeneric, true); + //TODO: Ensure if gradients with more than 2 colors it is handled correctly. + translators.Add(fillTranslator); + } + //if (s.Font != null && _context.Exclude.Font != eFontExclude.All) + //{ + // translators.Add(new CssFontTranslator(new FontDxf(s.Font), null)); + //} + if (border != null && _context.Exclude.Border != eBorderExclude.All) + { + + translators.Add(new CssStrokeTranslator(new BorderDrawing(border))); + } + + foreach (var translator in translators) + { + _context.SetTranslator(translator); + _context.AddDeclarations(styleClass); + } + + AddRule(styleClass); + } + + internal void AddToCollection(string name, ExcelChartStyleItem element, string htmlElement) + { + if (element.HasValue() == false) return; //Dont add empty elements + + var s = element; + + var styleClass = new CssRule($"{htmlElement}.{name}", int.MaxValue); + + var translators = new List(); + + if (s.Fill != null && _context.Exclude.Fill == false) + { + translators.Add(new CssFillTranslator(new FillDrawing(s.Fill))); + } + //if (s.Font != null && _context.Exclude.Font != eFontExclude.All) + //{ + // translators.Add(new CssFontTranslator(new FontDxf(s.Font), null)); + //} + //if (s.Border != null && _context.Exclude.Border != eBorderExclude.All) + //{ + // translators.Add(new CssBorderTranslator(new BorderDxf(s.Border))); + //} + + foreach (var translator in translators) + { + _context.SetTranslator(translator); + _context.AddDeclarations(styleClass); + } + + AddRule(styleClass); + } + } +} diff --git a/src/EPPlus/Export/HtmlExport/Exporters/Internal/CssChartExporterSync.cs b/src/EPPlus/Export/HtmlExport/Exporters/Internal/CssChartExporterSync.cs new file mode 100644 index 000000000..4374a53e9 --- /dev/null +++ b/src/EPPlus/Export/HtmlExport/Exporters/Internal/CssChartExporterSync.cs @@ -0,0 +1,102 @@ +using OfficeOpenXml.ConditionalFormatting; +using OfficeOpenXml.ConditionalFormatting.Contracts; +using OfficeOpenXml.ConditionalFormatting.Rules; +using OfficeOpenXml.Core; +using OfficeOpenXml.Core.CellStore; +using OfficeOpenXml.Core.RangeQuadTree; +using OfficeOpenXml.Drawing.Chart; +using OfficeOpenXml.Export.HtmlExport.CssCollections; +using OfficeOpenXml.Export.HtmlExport.Determinator; +using OfficeOpenXml.Export.HtmlExport.Settings; +using OfficeOpenXml.Export.HtmlExport.StyleCollectors; +using OfficeOpenXml.Export.HtmlExport.StyleCollectors.StyleContracts; +using OfficeOpenXml.Export.HtmlExport.Translators; +using OfficeOpenXml.Export.HtmlExport.Writers; +using OfficeOpenXml.Style.XmlAccess; +using OfficeOpenXml.Table; +using OfficeOpenXml.Utils; +using System; +using System.Collections.Generic; +using System.Data; +using System.IO; +using System.Linq; +using System.Runtime.CompilerServices; + +namespace OfficeOpenXml.Export.HtmlExport.Exporters.Internal +{ + internal class CssChartExporterSync + { + CssExportSettings _settings; + + ExcelChart _chart; + + TranslatorContext _context; + + public CssChartExporterSync(ExcelChart chart) : this(new CssChartExportSettings(), chart) + { + + } + + public CssChartExporterSync(CssChartExportSettings settings, ExcelChart chart) + { + _settings = settings; + Require.Argument(chart).IsNotNull("chart"); + _chart = chart; + } + /// + /// Exports an to a html string + /// + /// A html table + public string GetCssString() + { + using (var ms = EPPlusMemoryManager.GetStream()) + { + RenderCss(ms); + ms.Position = 0; + using (var sr = new StreamReader(ms)) + { + return sr.ReadToEnd(); + } + } + } + /// + /// Exports the css part of the html export. + /// + /// The stream to write the css to. + /// + public void RenderCss(Stream stream) + { + var trueWriter = new CssWriter(stream); + + var cssCollection = new CssChartRuleCollection(_chart, _settings); + + trueWriter.WriteAndClearFlush(cssCollection, false); + } + + // /// + // /// Exports the css part of an to a html string + // /// + // /// A html table + // public void RenderCss(Stream stream) + // { + // var cssWriter = GetTableCssWriter(stream, _table, _tableSettings); + // if (cssWriter == null) { return; } + + // var cssRules = CreateRuleCollection(_tableSettings); + // cssWriter.WriteAndClearFlush(cssRules, Settings.Minify); + // } + + // protected CssRuleCollection CreateRuleCollection(CssExportSettings settings) + // { + // var cssTranslator = new CssChartRuleCollection(_chart, settings); + + // _context = new TranslatorContext(settings); + + + // //AddCssRulesToCollection(cssTranslator, settings); + + // //return cssTranslator.RuleCollection; + // } + + } + } diff --git a/src/EPPlus/Export/HtmlExport/Exporters/Internal/CssExporterBase.cs b/src/EPPlus/Export/HtmlExport/Exporters/Internal/CssExporterBase.cs index e6bdbc9b6..789d89e7f 100644 --- a/src/EPPlus/Export/HtmlExport/Exporters/Internal/CssExporterBase.cs +++ b/src/EPPlus/Export/HtmlExport/Exporters/Internal/CssExporterBase.cs @@ -11,10 +11,12 @@ Date Author Change 6/4/2022 EPPlus Software AB ExcelTable Html Export *************************************************************************************************/ using OfficeOpenXml.ConditionalFormatting; +using OfficeOpenXml.ConditionalFormatting.Contracts; using OfficeOpenXml.ConditionalFormatting.Rules; using OfficeOpenXml.Core; using OfficeOpenXml.Core.CellStore; using OfficeOpenXml.Core.RangeQuadTree; +using OfficeOpenXml.Drawing.Chart; using OfficeOpenXml.Export.HtmlExport.CssCollections; using OfficeOpenXml.Export.HtmlExport.Determinator; using OfficeOpenXml.Export.HtmlExport.Settings; @@ -26,10 +28,9 @@ Date Author Change using OfficeOpenXml.Utils; using System; using System.Collections.Generic; +using System.Data; using System.IO; using System.Linq; -using OfficeOpenXml.ConditionalFormatting.Contracts; -using System.Data; using System.Runtime.CompilerServices; namespace OfficeOpenXml.Export.HtmlExport.Exporters.Internal diff --git a/src/EPPlus/Export/HtmlExport/Settings/CssChartExportSettings.cs b/src/EPPlus/Export/HtmlExport/Settings/CssChartExportSettings.cs new file mode 100644 index 000000000..4d9ed3573 --- /dev/null +++ b/src/EPPlus/Export/HtmlExport/Settings/CssChartExportSettings.cs @@ -0,0 +1,14 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; + +namespace OfficeOpenXml.Export.HtmlExport.Settings +{ + internal class CssChartExportSettings : CssExportSettings + { + public CssChartExportSettings() : base() + { + } + } +} diff --git a/src/EPPlus/Export/HtmlExport/StyleCollectors/BorderDrawing.cs b/src/EPPlus/Export/HtmlExport/StyleCollectors/BorderDrawing.cs new file mode 100644 index 000000000..932ee0e0d --- /dev/null +++ b/src/EPPlus/Export/HtmlExport/StyleCollectors/BorderDrawing.cs @@ -0,0 +1,24 @@ +using OfficeOpenXml.Drawing; +using OfficeOpenXml.Export.HtmlExport.StyleCollectors.StyleContracts; +using OfficeOpenXml.Style; +using OfficeOpenXml.Style.Dxf; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; + +namespace OfficeOpenXml.Export.HtmlExport.StyleCollectors +{ + internal class BorderDrawing : IDrawingBorder + { + ExcelDrawingBorder _border; + + internal BorderDrawing(ExcelDrawingBorder border) + { + _border = border; + Stroke = new FillDrawingBasic(border.Fill); + } + + public IFill Stroke { get; private set; } + } +} diff --git a/src/EPPlus/Export/HtmlExport/StyleCollectors/FillDrawing.cs b/src/EPPlus/Export/HtmlExport/StyleCollectors/FillDrawing.cs new file mode 100644 index 000000000..cfdd47818 --- /dev/null +++ b/src/EPPlus/Export/HtmlExport/StyleCollectors/FillDrawing.cs @@ -0,0 +1,145 @@ +using OfficeOpenXml.Drawing; +using OfficeOpenXml.Drawing.Theme; +using OfficeOpenXml.Export.HtmlExport.StyleCollectors.StyleContracts; +using OfficeOpenXml.Style; +using OfficeOpenXml.Style.Dxf; +using System; +using System.Collections.Generic; +using System.Drawing; +using System.Linq; +using System.Text; + +namespace OfficeOpenXml.Export.HtmlExport.StyleCollectors +{ + internal class FillDrawing : IFill + { + ExcelDrawingFill _fill; + + internal FillDrawing(ExcelDrawingFill fill) + { + _fill = fill; + } + + public bool IsGradient + { + get + { + return _fill.Style == eFillStyle.GradientFill; + } + } + + + public bool IsLinear + { + get + { + return _fill.GradientFill.ShadePath == eShadePath.Linear; + } + } + + + public double Degree + { + get + { + if (IsGradient) + { + if(IsLinear) + { + return _fill.GradientFill.LinearSettings.Angle; + } + } + + return double.NaN; + } + } + + + + public double Right + { + get + { + if (IsGradient) + { + if(IsLinear == false) + { + return _fill.GradientFill.TileRectangle.RightOffset; + } + } + + return double.NaN; + } + } + + public double Bottom + { + get + { + if (IsGradient) + { + if (IsLinear == false) + { + return _fill.GradientFill.TileRectangle.BottomOffset; + } + } + + return double.NaN; + } + } + + public string GetBackgroundColor(ExcelTheme theme) + { + return GetColor(_fill.Color, theme); + } + + public string GetPatternColor(ExcelTheme theme) + { + return GetColor(_fill.PatternFill.ForegroundColor.GetColor(), theme); + } + + public string GetGradientColor1(ExcelTheme theme) + { + return GetColor(_fill.GradientFill.Colors.ToArray()[0].Color.GetColor(), theme); + } + public string GetGradientColor2(ExcelTheme theme) + { + return GetColor(_fill.GradientFill.Colors.ToArray()[1].Color.GetColor(), theme); + } + + public bool HasValue + { + get + { + return !_fill.IsEmpty; + } + } + + ExcelFillStyle IFill.PatternType => ExcelFillStyle.Solid; + + internal static string GetColor(Color c, ExcelTheme theme) + { + return "#" + c.ToArgb().ToString("x8").Substring(2); + } + + string IFill.GetBackgroundColor(ExcelTheme theme) + { + return GetBackgroundColor(theme); + } + + string IFill.GetPatternColor(ExcelTheme theme) + { + return GetPatternColor(theme); + } + + string IFill.GetGradientColor1(ExcelTheme theme) + { + return GetGradientColor1(theme); + } + + string IFill.GetGradientColor2(ExcelTheme theme) + { + return GetGradientColor2(theme); + } + } +} diff --git a/src/EPPlus/Export/HtmlExport/StyleCollectors/FillDrawingBasic.cs b/src/EPPlus/Export/HtmlExport/StyleCollectors/FillDrawingBasic.cs new file mode 100644 index 000000000..984179087 --- /dev/null +++ b/src/EPPlus/Export/HtmlExport/StyleCollectors/FillDrawingBasic.cs @@ -0,0 +1,144 @@ +using OfficeOpenXml.Drawing; +using OfficeOpenXml.Drawing.Theme; +using OfficeOpenXml.Export.HtmlExport.StyleCollectors.StyleContracts; +using OfficeOpenXml.Style; +using System; +using System.Collections.Generic; +using System.Drawing; +using System.Linq; +using System.Text; + +namespace OfficeOpenXml.Export.HtmlExport.StyleCollectors +{ + internal class FillDrawingBasic : IFill + { + ExcelDrawingFillBasic _fill; + + internal FillDrawingBasic(ExcelDrawingFillBasic fill) + { + _fill = fill; + } + + public bool IsGradient + { + get + { + return _fill.Style == eFillStyle.GradientFill; + } + } + + + public bool IsLinear + { + get + { + return _fill.GradientFill.ShadePath == eShadePath.Linear; + } + } + + + public double Degree + { + get + { + if (IsGradient) + { + if (IsLinear) + { + return _fill.GradientFill.LinearSettings.Angle; + } + } + + return double.NaN; + } + } + + + + public double Right + { + get + { + if (IsGradient) + { + if (IsLinear == false) + { + return _fill.GradientFill.TileRectangle.RightOffset; + } + } + + return double.NaN; + } + } + + public double Bottom + { + get + { + if (IsGradient) + { + if (IsLinear == false) + { + return _fill.GradientFill.TileRectangle.BottomOffset; + } + } + + return double.NaN; + } + } + + public string GetBackgroundColor(ExcelTheme theme) + { + return GetColor(_fill.Color, theme); + } + + public string GetPatternColor(ExcelTheme theme) + { + return GetColor(Color.Empty, theme); + } + + public string GetGradientColor1(ExcelTheme theme) + { + return GetColor(_fill.GradientFill.Colors.ToArray()[0].Color.GetColor(), theme); + } + public string GetGradientColor2(ExcelTheme theme) + { + return GetColor(_fill.GradientFill.Colors.ToArray()[1].Color.GetColor(), theme); + } + + public bool HasValue + { + get + { + return !_fill.IsEmpty; + } + } + + public ExcelFillStyle PatternType => ExcelFillStyle.Solid; + + internal static string GetColor(Color c, ExcelTheme theme) + { + return "#" + c.ToArgb().ToString("x8").Substring(2); + } + + string IFill.GetBackgroundColor(ExcelTheme theme) + { + return GetBackgroundColor(theme); + } + + string IFill.GetPatternColor(ExcelTheme theme) + { + return GetPatternColor(theme); + } + + string IFill.GetGradientColor1(ExcelTheme theme) + { + return GetGradientColor1(theme); + } + + string IFill.GetGradientColor2(ExcelTheme theme) + { + return GetGradientColor2(theme); + } + } +} diff --git a/src/EPPlus/Export/HtmlExport/StyleCollectors/StyleContracts/IDrawingBorder.cs b/src/EPPlus/Export/HtmlExport/StyleCollectors/StyleContracts/IDrawingBorder.cs new file mode 100644 index 000000000..b3cbe1d49 --- /dev/null +++ b/src/EPPlus/Export/HtmlExport/StyleCollectors/StyleContracts/IDrawingBorder.cs @@ -0,0 +1,12 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; + +namespace OfficeOpenXml.Export.HtmlExport.StyleCollectors.StyleContracts +{ + public interface IDrawingBorder + { + IFill Stroke { get; } + } +} diff --git a/src/EPPlus/Export/HtmlExport/Translators/CssFillTranslator.cs b/src/EPPlus/Export/HtmlExport/Translators/CssFillTranslator.cs index 438a2a3c1..a9d5a945e 100644 --- a/src/EPPlus/Export/HtmlExport/Translators/CssFillTranslator.cs +++ b/src/EPPlus/Export/HtmlExport/Translators/CssFillTranslator.cs @@ -24,10 +24,20 @@ internal class CssFillTranslator : TranslatorBase { ExcelTheme _theme; IFill _fill; + bool _usePresentationName = false; - internal CssFillTranslator(IFill fill) + string presentationName = "fill"; + + string bgName = "background-color"; + + internal CssFillTranslator(IFill fill, bool usePresentationName = false) { _fill = fill; + _usePresentationName = usePresentationName; + if(usePresentationName) + { + bgName = presentationName; + } } internal override List GenerateDeclarationList(TranslatorContext context) @@ -47,7 +57,7 @@ internal override List GenerateDeclarationList(TranslatorContext co var bc = _fill.GetBackgroundColor(_theme) ?? "#0"; if (string.IsNullOrEmpty(bc) == false) { - AddDeclaration("background-color", bc); + AddDeclaration(bgName, bc); } } else if(_fill.PatternType == ExcelFillStyle.None) @@ -55,7 +65,7 @@ internal override List GenerateDeclarationList(TranslatorContext co var fc = _fill.GetPatternColor(_theme); if (string.IsNullOrEmpty(fc) == false) { - AddDeclaration("background-color", fc); + AddDeclaration(bgName, fc); } } else diff --git a/src/EPPlus/Export/HtmlExport/Translators/CssStrokeTranslator.cs b/src/EPPlus/Export/HtmlExport/Translators/CssStrokeTranslator.cs new file mode 100644 index 000000000..8ea839695 --- /dev/null +++ b/src/EPPlus/Export/HtmlExport/Translators/CssStrokeTranslator.cs @@ -0,0 +1,33 @@ +using OfficeOpenXml.Drawing.Theme; +using OfficeOpenXml.Export.HtmlExport.CssCollections; +using OfficeOpenXml.Export.HtmlExport.StyleCollectors.StyleContracts; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; + +namespace OfficeOpenXml.Export.HtmlExport.Translators +{ + internal class CssStrokeTranslator : TranslatorBase + { + IDrawingBorder _drawingBorder; + ExcelTheme _theme; + + public CssStrokeTranslator(IDrawingBorder drawingBorder) + { + _drawingBorder = drawingBorder; + } + + internal override List GenerateDeclarationList(TranslatorContext context) + { + var borderExclude = context.Exclude.Border; + _theme = context.Theme; + + if(_drawingBorder.Stroke.HasValue && _drawingBorder.Stroke.PatternType == Style.ExcelFillStyle.Solid) + { + AddDeclaration($"stroke", _drawingBorder.Stroke.GetBackgroundColor(_theme)); + } + return declarations; + } + } +} diff --git a/src/EPPlus/Export/HtmlExport/Translators/TranslatorContext.cs b/src/EPPlus/Export/HtmlExport/Translators/TranslatorContext.cs index 8145d2949..fceb1178d 100644 --- a/src/EPPlus/Export/HtmlExport/Translators/TranslatorContext.cs +++ b/src/EPPlus/Export/HtmlExport/Translators/TranslatorContext.cs @@ -42,6 +42,11 @@ internal class TranslatorContext internal HashSet AddedIcons = new HashSet(); + public TranslatorContext(CssExportSettings settings) + { + Settings = settings; + Exclude = new CssExclude(); + } public TranslatorContext(HtmlRangeExportSettings settings) { diff --git a/src/EPPlusTest/Export/HtmlExport/CssRuleCollectionTests.cs b/src/EPPlusTest/Export/HtmlExport/CssRuleCollectionTests.cs index 903460058..5c7fcbeb6 100644 --- a/src/EPPlusTest/Export/HtmlExport/CssRuleCollectionTests.cs +++ b/src/EPPlusTest/Export/HtmlExport/CssRuleCollectionTests.cs @@ -1,16 +1,18 @@ using Microsoft.VisualStudio.TestTools.UnitTesting; -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; using OfficeOpenXml; -using OfficeOpenXml.Export.HtmlExport.CssCollections; +using OfficeOpenXml.Drawing.Chart; using OfficeOpenXml.Export.HtmlExport; +using OfficeOpenXml.Export.HtmlExport.CssCollections; +using OfficeOpenXml.Export.HtmlExport.Exporters.Internal; +using OfficeOpenXml.Export.HtmlExport.StyleCollectors; using OfficeOpenXml.Export.HtmlExport.StyleCollectors.StyleContracts; using OfficeOpenXml.Export.HtmlExport.Translators; -using OfficeOpenXml.Export.HtmlExport.StyleCollectors; +using System; +using System.Collections.Generic; using System.Drawing; +using System.Linq; +using System.Text; +using System.Threading.Tasks; namespace EPPlusTest.Export.HtmlExport { @@ -47,5 +49,41 @@ public void ExportRangeWithNullBorderMergedCellsShouldNotThrow() var declarations = borderTranslator.GenerateDeclarationList(context); } } + + [TestMethod] + public void ExportChartCssTest() + { + using (var package = new ExcelPackage()) + { + var mySheet = package.Workbook.Worksheets.Add("chartSinglSheet"); + + mySheet.Cells["A1:C1"].Formula = "COLUMN()"; + + var lChart = mySheet.Drawings.AddLineChart("chart1", eLineChartType.Line); + + mySheet.Calculate(); + + var address = mySheet.Cells["A1:C1"]; + + lChart.Series.Add(address, address); + + lChart.StyleManager.SetChartStyle(OfficeOpenXml.Drawing.Chart.Style.ePresetChartStyle.LineChartStyle2); + + lChart.Fill.Style = OfficeOpenXml.Drawing.eFillStyle.SolidFill; + lChart.Fill.Color = Color.Aquamarine; + lChart.Border.Fill.Color = Color.DarkCyan; + + lChart.StyleManager.Style.ChartArea.Fill.Style = OfficeOpenXml.Drawing.eFillStyle.SolidFill; + + //lChart.StyleManager.Style.ChartArea.Fill.Color = Color.DarkCyan; + //lChart.StyleManager.Style.ChartArea.Border.Fill.Color = Color.DarkSeaGreen; + + var cssExporter = new CssChartExporterSync(lChart); + + var css = cssExporter.GetCssString(); + + package.SaveAs("C:\\epplusTest\\Testoutput\\generatedLineChart.xlsx"); + } + } } } From 54ec6aa14d10888f464f35400f5859dc030d3b0e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ossian=20Edstr=C3=B6m?= Date: Tue, 31 Mar 2026 09:41:05 +0200 Subject: [PATCH 125/151] Added tests for veryfying corrupt wb --- .../StyleTests.cs | 51 +++++++++++++++---- .../Chart/Style/ExcelChartStyleManager.cs | 10 ++-- .../HtmlExport/CssRuleCollectionTests.cs | 33 ++++++++++++ 3 files changed, 78 insertions(+), 16 deletions(-) diff --git a/src/EPPlus.Export.ImageRenderer.Test/StyleTests.cs b/src/EPPlus.Export.ImageRenderer.Test/StyleTests.cs index 5a478e9a6..a5c5538b5 100644 --- a/src/EPPlus.Export.ImageRenderer.Test/StyleTests.cs +++ b/src/EPPlus.Export.ImageRenderer.Test/StyleTests.cs @@ -24,38 +24,42 @@ public void ExtractThemeStyleWorks() { ExcelPackage.License.SetNonCommercialOrganization("EPPlus Project"); - using (var p = OpenTemplatePackage("MyLineIonTheme.xlsx")) + using (var p = OpenTemplatePackage("MyLineIonThemeExcel.xlsx")) { var wbStyles = p.Workbook.Styles; var simpleChart = p.Workbook.Worksheets[0].Drawings[0].As.Chart.LineChart; simpleChart.Title.Text = "Hello"; - p.Workbook.ThemeManager.CurrentTheme.ColorScheme.Accent1.SetPresetColor(Color.DarkGoldenrod); + //p.Workbook.ThemeManager.CurrentTheme.ColorScheme.Accent1.SetPresetColor(Color.DarkGoldenrod); var chartDefaultStyle = simpleChart.StyleManager.Style; + //simpleChart.StyleManager.SetChartStyle(OfficeOpenXml.Drawing.Chart.Style.ePresetChartStyle.LineChartStyle2); + ////var fillStyle = simpleChart.StyleManager.Style.DataPointLine.Border.Fill; - var fillStyle = simpleChart.StyleManager.Style.DataPointLine.Border.Fill; + //var svgFill = new SvgFill(fillStyle); + //var fillTranslator = new SvgFillTranslator(svgFill); - var svgFill = new SvgFill(fillStyle); - var fillTranslator = new SvgFillTranslator(svgFill); + //var context = new TranslatorContext(new HtmlRangeExportSettings()); - var context = new TranslatorContext(new HtmlRangeExportSettings()); + //var declarations = fillTranslator.GenerateDeclarationList(context); - var declarations = fillTranslator.GenerateDeclarationList(context); + //var styleClass = new CssRule("Style", 0); - var styleClass = new CssRule("Style", 0); + //context.SetTranslator(fillTranslator); + //context.AddDeclarations(styleClass); + simpleChart.StyleManager.ApplyStyles(); - context.SetTranslator(fillTranslator); - context.AddDeclarations(styleClass); //chartDefaultStyle.DataPointLine.FillReference.Color + simpleChart.Title.Font.Color = Color.CornflowerBlue; + //simpleChart.StyleManager.Style.Title.FontReference.Color.SetPresetColor(Color.CornflowerBlue); //Highest order of styling if datapoint does not exist //simpleChart.StyleManager.Style.DeleteAllNode("cs:dataPointLine"); simpleChart.StyleManager.Style.DataPointLine.BorderReference.Color.SetPresetColor(Color.Green); //simpleChart.StyleManager.Style.DataPointLine.Border.Fill.DeleteNode(path); - //simpleChart.StyleManager.Style.DataPointLine.Border.Fill.Color = Color.Red; + simpleChart.StyleManager.Style.DataPointLine.Border.Fill.Color = Color.Red; //simpleChart.StyleManager.Style.DataPointLine.Fill.Color = Color.Green; //simpleChart.StyleManager.Style.DataPoint.Fill.Color = Color.Yellow; @@ -63,6 +67,7 @@ public void ExtractThemeStyleWorks() simpleChart.StyleManager.ApplyStyles(); + simpleChart.Title.TextBody.Paragraphs[0].TextRuns[0].Fill.Color = Color.CornflowerBlue; //var serLine = chartDefaultStyle.SeriesLine; //var lineElement = serLine.Border.LineElement; //var serLineFillRef = serLine.Border.Fill; @@ -74,6 +79,30 @@ public void ExtractThemeStyleWorks() } } + + [TestMethod] + public void TextRunIsStyledButNotTitleFont() + { + ExcelPackage.License.SetNonCommercialOrganization("EPPlus Project"); + + var chartName = "ChartLineDefaultDownUp.xlsx"; + using (var p = OpenTemplatePackage("MyLineIonThemeExcel.xlsx")) + { + var ws = p.Workbook.Worksheets[0]; + var lChart = ws.Drawings[0].As.Chart.LineChart; + + //lChart.Title.Font.Color = Color.DeepSkyBlue; + + lChart.StyleManager.ApplyStyles(); + + SaveAndCleanup(p); + } + + //using(var p= OpenPackage(chartName,false)) + //{ + + //} + } ///// ///// Exports an to a html string ///// diff --git a/src/EPPlus/Drawing/Chart/Style/ExcelChartStyleManager.cs b/src/EPPlus/Drawing/Chart/Style/ExcelChartStyleManager.cs index ad863d280..558dcad1c 100644 --- a/src/EPPlus/Drawing/Chart/Style/ExcelChartStyleManager.cs +++ b/src/EPPlus/Drawing/Chart/Style/ExcelChartStyleManager.cs @@ -517,11 +517,11 @@ public void ApplyStyles() ApplyStyle(_chart.PlotArea, Style.PlotArea); } - ////Title - //if (_chart.HasTitle) - //{ - // ApplyStyle(_chart.Title, Style.Title); - //} + //Title + if (_chart.HasTitle) + { + ApplyStyle(_chart.Title, Style.Title); + } if (_chart.PlotArea.DataTable!=null) { diff --git a/src/EPPlusTest/Export/HtmlExport/CssRuleCollectionTests.cs b/src/EPPlusTest/Export/HtmlExport/CssRuleCollectionTests.cs index 5c7fcbeb6..c79e759a4 100644 --- a/src/EPPlusTest/Export/HtmlExport/CssRuleCollectionTests.cs +++ b/src/EPPlusTest/Export/HtmlExport/CssRuleCollectionTests.cs @@ -85,5 +85,38 @@ public void ExportChartCssTest() package.SaveAs("C:\\epplusTest\\Testoutput\\generatedLineChart.xlsx"); } } + + [TestMethod] + public void ChartTitleIssue() + { + using (var package = new ExcelPackage()) + { + var mySheet = package.Workbook.Worksheets.Add("chartSinglSheet"); + + mySheet.Cells["A1:C1"].Formula = "COLUMN()"; + + var lChart = mySheet.Drawings.AddLineChart("chart1", eLineChartType.Line); + + mySheet.Calculate(); + + var address = mySheet.Cells["A1:C1"]; + + lChart.Series.Add(address, address); + + lChart.StyleManager.SetChartStyle(OfficeOpenXml.Drawing.Chart.Style.ePresetChartStyle.LineChartStyle2); + + lChart.Fill.Style = OfficeOpenXml.Drawing.eFillStyle.SolidFill; + lChart.Fill.Color = Color.Aquamarine; + lChart.Border.Fill.Color = Color.DarkCyan; + + + lChart.Title.Text = "MyTitle"; + + lChart.StyleManager.ApplyStyles(); + + + package.SaveAs("C:\\epplusTest\\Testoutput\\generatedLineChartCrashes.xlsx"); + } + } } } From 0df30eb11815423b10667dfe3561920f9cdf93af Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ossian=20Edstr=C3=B6m?= Date: Tue, 31 Mar 2026 10:06:00 +0200 Subject: [PATCH 126/151] Ensured testing correct file --- src/EPPlus.Export.ImageRenderer.Test/StyleTests.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/EPPlus.Export.ImageRenderer.Test/StyleTests.cs b/src/EPPlus.Export.ImageRenderer.Test/StyleTests.cs index a5c5538b5..10333a7fc 100644 --- a/src/EPPlus.Export.ImageRenderer.Test/StyleTests.cs +++ b/src/EPPlus.Export.ImageRenderer.Test/StyleTests.cs @@ -86,7 +86,7 @@ public void TextRunIsStyledButNotTitleFont() ExcelPackage.License.SetNonCommercialOrganization("EPPlus Project"); var chartName = "ChartLineDefaultDownUp.xlsx"; - using (var p = OpenTemplatePackage("MyLineIonThemeExcel.xlsx")) + using (var p = OpenTemplatePackage(chartName)) { var ws = p.Workbook.Worksheets[0]; var lChart = ws.Drawings[0].As.Chart.LineChart; From ba88211045f0cf365dd590db631aecbeb4150ab3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ossian=20Edstr=C3=B6m?= Date: Tue, 31 Mar 2026 11:22:10 +0200 Subject: [PATCH 127/151] Fixed ApplyStyles creating corrupt workbook --- .../Drawing/Chart/ExcelChartStandard.cs | 2 +- src/EPPlus/Drawing/Chart/ExcelChartTitle.cs | 6 ++++ .../HtmlExport/CssRuleCollectionTests.cs | 31 +++++++++---------- 3 files changed, 22 insertions(+), 17 deletions(-) diff --git a/src/EPPlus/Drawing/Chart/ExcelChartStandard.cs b/src/EPPlus/Drawing/Chart/ExcelChartStandard.cs index ecae33d2f..f7df55c8e 100644 --- a/src/EPPlus/Drawing/Chart/ExcelChartStandard.cs +++ b/src/EPPlus/Drawing/Chart/ExcelChartStandard.cs @@ -129,7 +129,7 @@ private void Init(ExcelDrawings drawings, XmlNode chartNode) { _isChartEx = chartNode.NamespaceURI == ExcelPackage.schemaChartExMain; _chartXmlHelper = XmlHelperFactory.Create(drawings.NameSpaceManager, chartNode); - _chartXmlHelper.AddSchemaNodeOrder(new string[] { "date1904", "lang", "roundedCorners", "AlternateContent", "style", "clrMapOvr", "pivotSource", "protection", "chart", "ofPieType", "title", "autoTitleDeleted", "pivotFmts", "view3D", "floor", "sideWall", "backWall", "plotArea", "wireframe", "barDir", "grouping", "scatterStyle", "radarStyle", "varyColors", "ser", "dLbls", "bubbleScale", "showNegBubbles", "firstSliceAng", "holeSize", "dropLines", "hiLowLines", "upDownBars", "marker", "smooth", "shape", "legend", "plotVisOnly", "dispBlanksAs", "gapWidth", "upBars", "downBars", "showDLblsOverMax", "overlap", "bandFmts", "axId", "spPr", "txPr", "printSettings" }, ExcelDrawing._schemaNodeOrderSpPr); + _chartXmlHelper.AddSchemaNodeOrder(new string[] { "date1904", "lang", "roundedCorners", "AlternateContent", "style", "clrMapOvr", "pivotSource", "protection", "chart", "ofPieType", "title", "autoTitleDeleted", "pivotFmts", "view3D", "floor", "sideWall", "backWall", "plotArea", "wireframe", "barDir", "grouping", "scatterStyle", "radarStyle", "varyColors", "ser", "dLbls", "bubbleScale", "showNegBubbles", "firstSliceAng", "holeSize", "dropLines", "hiLowLines", "upDownBars", "marker", "smooth", "shape", "legend", "plotVisOnly", "dispBlanksAs", "gapWidth", "upBars", "downBars", "showDLblsOverMax", "overlap", "bandFmts", "axId", "tx", "layout", "overlay", "spPr", "txPr", "printSettings" }, ExcelDrawing._schemaNodeOrderSpPr); WorkSheet = drawings.Worksheet; } #endregion diff --git a/src/EPPlus/Drawing/Chart/ExcelChartTitle.cs b/src/EPPlus/Drawing/Chart/ExcelChartTitle.cs index 5c567b31b..6491d4623 100644 --- a/src/EPPlus/Drawing/Chart/ExcelChartTitle.cs +++ b/src/EPPlus/Drawing/Chart/ExcelChartTitle.cs @@ -386,6 +386,12 @@ void IStyleMandatoryProperties.SetMandatoryProperties() if (Font.Kerning == 0) Font.Kerning = 12; Font.Bold = Font.Bold; //Must be set + //Textbody cannot exist without a paragraph node + if(TextBody.Paragraphs.Count == 0) + { + TextBody.Paragraphs.Add("Title"); + } + CreatespPrNode($"{_nsPrefix}:spPr"); } } diff --git a/src/EPPlusTest/Export/HtmlExport/CssRuleCollectionTests.cs b/src/EPPlusTest/Export/HtmlExport/CssRuleCollectionTests.cs index c79e759a4..508d2bb7b 100644 --- a/src/EPPlusTest/Export/HtmlExport/CssRuleCollectionTests.cs +++ b/src/EPPlusTest/Export/HtmlExport/CssRuleCollectionTests.cs @@ -17,7 +17,7 @@ namespace EPPlusTest.Export.HtmlExport { [TestClass] - public class CssRuleCollectionTests + public class CssRuleCollectionTests : TestBase { [TestMethod] public void ExportRangeWithNullBorderMergedCellsShouldNotThrow() @@ -89,33 +89,32 @@ public void ExportChartCssTest() [TestMethod] public void ChartTitleIssue() { - using (var package = new ExcelPackage()) - { - var mySheet = package.Workbook.Worksheets.Add("chartSinglSheet"); - - mySheet.Cells["A1:C1"].Formula = "COLUMN()"; - var lChart = mySheet.Drawings.AddLineChart("chart1", eLineChartType.Line); - - mySheet.Calculate(); + using (var package = OpenTemplatePackage("ChartLineDefaultDownUp.xlsx")) + { + var mySheet = package.Workbook.Worksheets[0]; - var address = mySheet.Cells["A1:C1"]; + var lChart = mySheet.Drawings[0].As.Chart.LineChart; - lChart.Series.Add(address, address); + lChart.StyleManager.ApplyStyles(); - lChart.StyleManager.SetChartStyle(OfficeOpenXml.Drawing.Chart.Style.ePresetChartStyle.LineChartStyle2); + package.SaveAs("C:\\epplusTest\\Testoutput\\ChartLineDefaultApply.xlsx"); + } - lChart.Fill.Style = OfficeOpenXml.Drawing.eFillStyle.SolidFill; - lChart.Fill.Color = Color.Aquamarine; - lChart.Border.Fill.Color = Color.DarkCyan; + using (var package = OpenPackage("ChartLineDefaultApply.xlsx", false)) + { + var mySheet = package.Workbook.Worksheets[0]; + var lChart = mySheet.Drawings[0].As.Chart.LineChart; lChart.Title.Text = "MyTitle"; lChart.StyleManager.ApplyStyles(); + Assert.AreEqual("MyTitle", lChart.Title.TextBody.Paragraphs[0].Text); + Assert.AreEqual("MyTitle", lChart.Title.TextBody.Paragraphs[0].TextRuns[0].Text); - package.SaveAs("C:\\epplusTest\\Testoutput\\generatedLineChartCrashes.xlsx"); + package.SaveAs("C:\\epplusTest\\Testoutput\\ChartLineDefaultApplyAndTitle.xlsx"); } } } From c2fd7782175c638b54c47f1936c0463d7335e793 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jan=20K=C3=A4llman?= Date: Tue, 31 Mar 2026 16:27:20 +0200 Subject: [PATCH 128/151] WIP:Fixed legend issues for barcharts and linecharts --- .../Chart/ColumnChartTests.cs | 22 +- .../Chart/LineChartToSvgTests.cs | 20 +- .../Svg/Chart/SvgChartLegend.cs | 206 ++++++++++-------- 3 files changed, 135 insertions(+), 113 deletions(-) diff --git a/src/EPPlus.Export.ImageRenderer.Test/Chart/ColumnChartTests.cs b/src/EPPlus.Export.ImageRenderer.Test/Chart/ColumnChartTests.cs index 101adc816..3c0fe09ec 100644 --- a/src/EPPlus.Export.ImageRenderer.Test/Chart/ColumnChartTests.cs +++ b/src/EPPlus.Export.ImageRenderer.Test/Chart/ColumnChartTests.cs @@ -20,17 +20,17 @@ public void GenerateSvgForColumnCharts1() var ws = p.Workbook.Worksheets[0]; var renderer = new EPPlusImageRenderer.ImageRenderer(); - var ix = 1; - var c = ws.Drawings[ix]; - var svg = renderer.RenderDrawingToSvg(c); - SaveTextFileToWorkbook($"svg\\ChartForSvg_ind{ix++}.svg", svg); + //var ix = 1; + //var c = ws.Drawings[ix]; + //var svg = renderer.RenderDrawingToSvg(c); + //SaveTextFileToWorkbook($"svg\\ChartForSvg_ind{ix++}.svg", svg); - //var ix = 0; - //foreach (ExcelChart c in ws.Drawings) - //{ - // var svg = renderer.RenderDrawingToSvg(c); - // SaveTextFileToWorkbook($"svg\\BarChartForSvg_sheet1_{ix++}.svg", svg); - //} + var ix = 0; + foreach (ExcelChart c in ws.Drawings) + { + var svg = renderer.RenderDrawingToSvg(c); + SaveTextFileToWorkbook($"svg\\BarChartForSvg_sheet1_{ix++}.svg", svg); + } } } [TestMethod] @@ -55,7 +55,5 @@ public void GenerateSvgForColumnCharts2() } } } - - } } diff --git a/src/EPPlus.Export.ImageRenderer.Test/Chart/LineChartToSvgTests.cs b/src/EPPlus.Export.ImageRenderer.Test/Chart/LineChartToSvgTests.cs index 218f6178b..07b6a1d28 100644 --- a/src/EPPlus.Export.ImageRenderer.Test/Chart/LineChartToSvgTests.cs +++ b/src/EPPlus.Export.ImageRenderer.Test/Chart/LineChartToSvgTests.cs @@ -20,17 +20,17 @@ public void GenerateSvgForLineCharts_sheet1() var ws = p.Workbook.Worksheets[0]; var renderer = new EPPlusImageRenderer.ImageRenderer(); - //var ix = 1; - //var c = ws.Drawings[ix]; - //var svg = renderer.RenderDrawingToSvg(c); - //SaveTextFileToWorkbook($"svg\\ChartForSvg_ind{ix++}.svg", svg); + var ix = 5; + var c = ws.Drawings[ix]; + var svg = renderer.RenderDrawingToSvg(c); + SaveTextFileToWorkbook($"svg\\ChartForSvg_ind{ix++}.svg", svg); - var ix = 0; - foreach (ExcelChart c in ws.Drawings) - { - var svg = renderer.RenderDrawingToSvg(c); - SaveTextFileToWorkbook($"svg\\ChartForSvg{ix++}.svg", svg); - } + //var ix = 0; + //foreach (ExcelChart c in ws.Drawings) + //{ + // var svg = renderer.RenderDrawingToSvg(c); + // SaveTextFileToWorkbook($"svg\\ChartForSvg{ix++}.svg", svg); + //} } } diff --git a/src/EPPlus.Export.ImageRenderer/Svg/Chart/SvgChartLegend.cs b/src/EPPlus.Export.ImageRenderer/Svg/Chart/SvgChartLegend.cs index 4c06a59ca..3959b6104 100644 --- a/src/EPPlus.Export.ImageRenderer/Svg/Chart/SvgChartLegend.cs +++ b/src/EPPlus.Export.ImageRenderer/Svg/Chart/SvgChartLegend.cs @@ -19,6 +19,7 @@ Date Author Change using OfficeOpenXml.Drawing; using OfficeOpenXml.Drawing.Chart; using OfficeOpenXml.FormulaParsing.Excel.Functions.MathFunctions; +using OfficeOpenXml.FormulaParsing.Excel.Functions.Text; using OfficeOpenXml.Interfaces.Drawing.Text; using OfficeOpenXml.Style; using System; @@ -36,7 +37,8 @@ internal class SvgChartLegend : SvgChartObject const float MarginExtra = 1.5f; const float MiddleMargin = 7.5f; const float LineLength = 21; - const float BarLength = 4; + const float MinBarLength = 4; + double _maxWidth, _maxHeight; internal SvgChartLegend(SvgChart sc, bool isDataLabelLegend = false) : base(sc) { _ttMeasurer = sc.Chart.WorkSheet._package.Settings.TextSettings.GenericTextMeasurerTrueType; @@ -48,14 +50,25 @@ internal SvgChartLegend(SvgChart sc, bool isDataLabelLegend = false) : base(sc) LeftMargin = RightMargin = 3; //4px TopMargin = BottomMargin = 3; //4px - - if (l.Layout.HasLayout) + switch (l.Position) { - Rectangle = GetRectFromManualLayout(sc, l.Layout); + case eLegendPosition.Top: + case eLegendPosition.Bottom: + _maxWidth = sc.ChartArea.Rectangle.Width * 0.8; + _maxHeight = sc.ChartArea.Rectangle.Height * 0.6; + break; + default: + _maxWidth = sc.ChartArea.Rectangle.Width * 0.6; + _maxHeight = sc.ChartArea.Rectangle.Height * 0.8; + break; } - else + double entryWidth, entryHeight; + + Rectangle = GetLegendRectangleAndEntrySize(sc, l, out entryWidth, out entryHeight); + + if (l.Layout.HasLayout) //Manual layout will override the position and size of legend, but not the entry size which is used for calculating the position of legend entries. { - Rectangle = GetLegendRectangle(sc, l); + Rectangle = GetRectFromManualLayout(sc, l.Layout); } Bounds.Left = Rectangle.Left; @@ -67,34 +80,17 @@ internal SvgChartLegend(SvgChart sc, bool isDataLabelLegend = false) : base(sc) Rectangle.SetDrawingPropertiesFill(l.Fill, sc.Chart.StyleManager.Style.Title.FillReference.Color); Rectangle.SetDrawingPropertiesBorder(l.Border, sc.Chart.StyleManager.Style.Title.BorderReference.Color, l.Border.Fill.Style != eFillStyle.NoFill, 0.75); - SetLegend(sc); + SetLegend(sc, entryWidth, entryHeight); } - private SvgRenderRectItem GetLegendRectangle(SvgChart sc, ExcelChartLegend l) + private SvgRenderRectItem GetLegendRectangleAndEntrySize(SvgChart sc, ExcelChartLegend l, out double entryWidth, out double entryHeight) { var rect = new SvgRenderRectItem(sc, sc.Bounds); - bool isVertical; - double maxWidth, maxHeight; - switch (l.Position) - { - case eLegendPosition.Top: - case eLegendPosition.Bottom: - isVertical = false; - maxWidth = sc.ChartArea.Rectangle.Width * 0.8; - maxHeight = sc.ChartArea.Rectangle.Height * 0.6; - break; - default: - isVertical = true; - maxWidth = sc.ChartArea.Rectangle.Width * 0.6; - maxHeight = sc.ChartArea.Rectangle.Height * 0.8; - break; - } - var iconLengh = sc.Chart.IsTypeLine() ? LineLength : BarLength; + var widest = 0d; var highest = 0d; - var hight = TopMargin; var index = 0; - double width=0d; + //Find the widest and hightest legend entry, and calculate the total width and hight of the legend based on the orientation. foreach (var ct in sc.Chart.PlotArea.ChartTypes) { foreach (var s in ct.Series) @@ -110,78 +106,83 @@ private SvgRenderRectItem GetLegendRectangle(SvgChart sc, ExcelChartLegend l) { font = entry.Font; } + var tm = _ttMeasurer.MeasureText(text, font.GetMeasureFont()); _seriesHeadersMeasure.Add(tm); - if (isVertical) + + if(tm.Width > widest) { - width += tm.Width; - if (tm.Width > widest) - { - widest = tm.Width; - } - if (tm.Height > hight) - { - highest = tm.Height; - } - if(hight==0) - { - hight += TopMargin + tm.Height; - } - else - { - hight += MiddleMargin + tm.Height; - } + widest = tm.Width; } - else + + if(tm.Height> highest) { - if(width + tm.Width >= maxWidth) - { - if(width > widest) - { - widest = width; - } - width = tm.Width; - hight += highest + MiddleMargin; - highest = 0; - } - else - { - width += tm.Width + MiddleMargin; - if (highest < tm.Height) - { - highest = tm.Height; - } - } + highest = tm.Height; } - + index++; } } - hight += BottomMargin; //remove last margin and add bottom margin + var iconLengh = GetIconLenght(sc, highest); + entryWidth = iconLengh + MarginExtra + widest; + entryHeight = highest; + //hight += BottomMargin; //remove last margin and add bottom margin switch (l.Position) { case eLegendPosition.Top: case eLegendPosition.Bottom: - rect.Width = width + LeftMargin + RightMargin + ((iconLengh + MarginExtra) * index + (MiddleMargin*Math.Max(index-1,0))) + 2 ; // 28 is for the line length + 2px between line and text - rect.Height = TopMargin + BottomMargin + hight + MarginExtra; + var fullLength = LeftMargin + entryWidth * index + MarginExtra*(index-1) + RightMargin; + if(fullLength > _maxWidth) + { + var height = TopMargin + highest; + var widestLine = 0D; + var width = LeftMargin + entryWidth; + + for(int i = 0; i < index; i++) + { + if (width + entryWidth + RightMargin > _maxWidth) + { + height = height + highest; + if (width + RightMargin > widestLine) + { + widestLine = width + RightMargin; + } + width = RightMargin + widest; + } + else + { + width += entryWidth + MarginExtra; + } + } + + height+= BottomMargin; + rect.Width = Math.Max(widestLine, width); + rect.Height = height + BottomMargin; + } + else + { + rect.Width = fullLength; + rect.Height = TopMargin + highest + BottomMargin; + } rect.Left = (sc.ChartArea.Rectangle.Width - rect.Width) / 2; if (l.Position == eLegendPosition.Top) { - rect.Top = sc.Title.Rectangle.Top + sc.Title.Rectangle.Height + MiddleMargin; + rect.Top = sc.Title.Rectangle.Bottom + MiddleMargin; } else { - rect.Top = sc.ChartArea.Rectangle.Height - rect.Height - BottomMargin; + rect.Top = sc.ChartArea.Rectangle.Height - rect.Height; } break; case eLegendPosition.Right: case eLegendPosition.TopRight: case eLegendPosition.Left: - rect.Width = widest + LeftMargin + RightMargin + iconLengh + 2; // 28 is for the line length + 2px between line and text - rect.Height = hight; - if (rect.Height > maxHeight) + rect.Width = widest + LeftMargin + RightMargin + iconLengh + MarginExtra; + rect.Height = TopMargin + (highest * (index + 1)) + (index* MarginExtra) + BottomMargin; + + if (rect.Height > _maxHeight) { - rect.Height = maxHeight; + rect.Height = _maxHeight; } if (l.Position == eLegendPosition.Right || @@ -215,7 +216,12 @@ private SvgRenderRectItem GetLegendRectangle(SvgChart sc, ExcelChartLegend l) return rect; } - internal void SetLegend(SvgChart sc) + private double GetIconLenght(SvgChart sc, double heighestText) + { + return sc.Chart.IsTypeLine() ? LineLength : Math.Max(MinBarLength, heighestText * 0.6); + } + + internal void SetLegend(SvgChart sc, double entryWidth, double entryHeight) { int index = 0; SvgLegendSerie pSls=null; @@ -241,7 +247,7 @@ internal void SetLegend(SvgChart sc) case eChartType.BarClustered: case eChartType.BarStacked: case eChartType.BarStacked100: - SetBarLegend(sc, index, pSls, pos, s, sls); + SetBarLegend(sc, index, pSls, pos, s, sls, entryWidth, entryHeight); break; default: break; @@ -328,7 +334,7 @@ private void SetLineLegend(SvgChart sc, int index, SvgLegendSerie pSls, eLegendP } } - private void SetBarLegend(SvgChart sc, int index, SvgLegendSerie pSls, eLegendPosition pos, ExcelChartSerie s, SvgLegendSerie sls) + private void SetBarLegend(SvgChart sc, int index, SvgLegendSerie pSls, eLegendPosition pos, ExcelChartSerie s, SvgLegendSerie sls, double entryWidth, double entryHeight) { var bs = (ExcelBarChartSerie)s; var tm = _seriesHeadersMeasure[index]; @@ -337,8 +343,7 @@ private void SetBarLegend(SvgChart sc, int index, SvgLegendSerie pSls, eLegendPo { prevTm = _seriesHeadersMeasure[index - 1]; } - - var si = GetBarSeriesIcon(sc, bs, prevTm, tm, pSls); + var si = GetBarSeriesIcon(sc, bs, prevTm, tm, pSls, entryHeight, entryWidth); sls.SeriesIcon = si; var tbLeft = si.Right + MarginExtra; @@ -409,7 +414,7 @@ private SvgRenderLineItem GetLineSeriesIcon(SvgChart sc, ExcelChartStandardSerie y = ((SvgRenderLineItem)pSls.SeriesIcon).Y1 + pTm.Height / 2 + tm.Height / 2 + MiddleMargin; } - item.X1 = (float)LeftMargin; //4 + item.X1 = (float)LeftMargin; item.Y1 = y; item.X2 = (float)LineLength; item.Y2 = y; @@ -419,30 +424,49 @@ private SvgRenderLineItem GetLineSeriesIcon(SvgChart sc, ExcelChartStandardSerie return item; } - private SvgRenderRectItem GetBarSeriesIcon(SvgChart sc, ExcelChartStandardSerie cStandardSerie, TextMeasurement pTm, TextMeasurement tm, SvgLegendSerie pSls) + private SvgRenderRectItem GetBarSeriesIcon(SvgChart sc, ExcelChartStandardSerie cStandardSerie, TextMeasurement pTm, TextMeasurement tm, SvgLegendSerie pSls, double entryHeight, double entryWidth) { var item = new SvgRenderRectItem(sc, Rectangle.Bounds); item.SetDrawingPropertiesFill(cStandardSerie.Fill, sc.Chart.StyleManager.Style.SeriesLine.FillReference.Color); item.SetDrawingPropertiesBorder(cStandardSerie.Border, sc.Chart.StyleManager.Style.SeriesLine.BorderReference.Color, cStandardSerie.Border.Fill.Style!=eFillStyle.NoFill, 0.75); - + var iconHeight = GetIconLenght(sc, entryHeight); + var icon = pSls?.SeriesIcon as SvgRenderRectItem; + var topOffset = 0D; if (sc.Chart.Legend.Position == eLegendPosition.Top || sc.Chart.Legend.Position == eLegendPosition.Bottom) { - float y = (float)Rectangle.Top + (float)TopMargin + tm.Height / 2 + MarginExtra; - float x = 0; - if (pSls == null) + double x; + if (pSls!=null && pSls.Textbox.Bounds.Right+entryWidth+RightMargin>_maxWidth) { - x = (float)Rectangle.Left + (float)LeftMargin;// + MarginExtra; + topOffset += entryHeight + MarginExtra; + x = Rectangle.Left + LeftMargin; } else { - x = (float)pSls.Textbox.Bounds.Left; + if (pSls == null) + { + x = Rectangle.Left + (float)LeftMargin; + } + else + { + x = icon.Left + entryWidth+MarginExtra; + } + } + double y; + if (pSls==null) + { + y = Rectangle.Top + TopMargin; } + else + { + y = icon.Top + topOffset; + } + item.Left = x; item.Top = y; - item.Width = 4; - item.Height = 4; + item.Width = iconHeight; + item.Height = iconHeight; } else { @@ -458,8 +482,8 @@ private SvgRenderRectItem GetBarSeriesIcon(SvgChart sc, ExcelChartStandardSerie item.Left = (float)LeftMargin; //4 item.Top = y; - item.Width = 4; - item.Height = 4; + item.Width = iconHeight; + item.Height = iconHeight; item.LineCap = eLineCap.Round; } From a40ac820673efff88b5829c82e693e2e41d51980 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ossian=20Edstr=C3=B6m?= Date: Tue, 31 Mar 2026 17:45:40 +0200 Subject: [PATCH 129/151] Made use class for gridlines still off --- .../Chart/LineChartToSvgTests.cs | 14 +++ .../RenderItems/RenderItemType.cs | 1 + .../RenderItems/SvgRenderItem.cs | 8 ++ .../RenderItems/SvgRenderLineItem.cs | 1 + .../RenderItems/SvgUseRefItem.cs | 61 ++++++++++ .../Svg/Chart/SvgChart.cs | 8 +- .../Svg/Chart/SvgChartAxis.cs | 114 ++++++++++++++---- .../Svg/Chart/SvgChartPlotarea.cs | 1 - .../Utils/SvgDrawingWriter.cs | 13 ++ 9 files changed, 195 insertions(+), 26 deletions(-) create mode 100644 src/EPPlus.Export.ImageRenderer/RenderItems/SvgUseRefItem.cs diff --git a/src/EPPlus.Export.ImageRenderer.Test/Chart/LineChartToSvgTests.cs b/src/EPPlus.Export.ImageRenderer.Test/Chart/LineChartToSvgTests.cs index 218f6178b..1ff0ecb7a 100644 --- a/src/EPPlus.Export.ImageRenderer.Test/Chart/LineChartToSvgTests.cs +++ b/src/EPPlus.Export.ImageRenderer.Test/Chart/LineChartToSvgTests.cs @@ -222,5 +222,19 @@ public void GenerateDatalabelsRightAlignedWithBg() SaveTextFileToWorkbook($"svg\\datalabelsSvgLeaderLinesBg.svg", svg); } } + + [TestMethod] + public void GenerateSimpleLineChart() + { + ExcelPackage.License.SetNonCommercialOrganization("EPPlus Project"); + using (var p = OpenTemplatePackage("defChartLine3Points.xlsx")) + { + var c = p.Workbook.Worksheets[0].Drawings[0]; + + var renderer = new EPPlusImageRenderer.ImageRenderer(); + var svg = renderer.RenderDrawingToSvg(c); + SaveTextFileToWorkbook($"svg\\defChartLine3Points.svg", svg); + } + } } } diff --git a/src/EPPlus.Export.ImageRenderer/RenderItems/RenderItemType.cs b/src/EPPlus.Export.ImageRenderer/RenderItems/RenderItemType.cs index 7c6452b7b..aa3412ab3 100644 --- a/src/EPPlus.Export.ImageRenderer/RenderItems/RenderItemType.cs +++ b/src/EPPlus.Export.ImageRenderer/RenderItems/RenderItemType.cs @@ -23,5 +23,6 @@ internal enum RenderItemType TSpan = 6, Paragraph = 7, CommentTitle = 8, + Reference = 9, } } \ No newline at end of file diff --git a/src/EPPlus.Export.ImageRenderer/RenderItems/SvgRenderItem.cs b/src/EPPlus.Export.ImageRenderer/RenderItems/SvgRenderItem.cs index 19eebcf33..4f81ec109 100644 --- a/src/EPPlus.Export.ImageRenderer/RenderItems/SvgRenderItem.cs +++ b/src/EPPlus.Export.ImageRenderer/RenderItems/SvgRenderItem.cs @@ -30,6 +30,9 @@ internal enum SvgFillType } internal abstract class SvgRenderItem : RenderItem { + //Refrence string if this is part of a definition + internal string DefId = null; + internal SvgRenderItem(DrawingBase renderer, BoundingBox parent) : base(renderer, parent) { } @@ -40,6 +43,11 @@ public override void Render(StringBuilder sb) private void RenderBase(StringBuilder sb) { + if (string.IsNullOrEmpty(DefId) == false) + { + sb.Append($"id=\"{DefId}\" "); + } + if (string.IsNullOrEmpty(FillColor) == false) { sb.Append($"fill=\"{FillColor}\" "); diff --git a/src/EPPlus.Export.ImageRenderer/RenderItems/SvgRenderLineItem.cs b/src/EPPlus.Export.ImageRenderer/RenderItems/SvgRenderLineItem.cs index 723097d84..8f2ef1156 100644 --- a/src/EPPlus.Export.ImageRenderer/RenderItems/SvgRenderLineItem.cs +++ b/src/EPPlus.Export.ImageRenderer/RenderItems/SvgRenderLineItem.cs @@ -24,6 +24,7 @@ namespace EPPlusImageRenderer.RenderItems { internal class SvgRenderLineItem : SvgRenderItem { + public SvgRenderLineItem(DrawingBase renderer, BoundingBox parent) : base(renderer, parent) { diff --git a/src/EPPlus.Export.ImageRenderer/RenderItems/SvgUseRefItem.cs b/src/EPPlus.Export.ImageRenderer/RenderItems/SvgUseRefItem.cs new file mode 100644 index 000000000..d695f2242 --- /dev/null +++ b/src/EPPlus.Export.ImageRenderer/RenderItems/SvgUseRefItem.cs @@ -0,0 +1,61 @@ +using EPPlus.Fonts.OpenType.Utils; +using EPPlus.Graphics; +using EPPlusImageRenderer; +using EPPlusImageRenderer.RenderItems; +using System; +using System.Collections.Generic; +using System.Globalization; +using System.Linq; +using System.Text; + +namespace EPPlus.Export.ImageRenderer.RenderItems +{ + internal class SvgUseRefItem : RenderItem + { + internal string Href { get; private set; } = null; + + internal double X + { + get + { + return Bounds.Left; + } + set + { + Bounds.Left = value; + } + } + + internal double Y + { + get + { + return Bounds.Top; + } + set + { + Bounds.Top = value; + } + } + + /// + /// Any bounds on this property are considered OFFSETS to the original object + /// Width and Height ONLY matter if this refers to an or element + /// + /// + internal SvgUseRefItem(DrawingBase renderer, BoundingBox parent, string idForHref) : base(renderer, parent) + { + Href = "#" + idForHref; + } + + public override RenderItemType Type => RenderItemType.Reference; + + + + public override void Render(StringBuilder sb) + { + string renderStr = $""; + sb.Append(renderStr); + } + } +} diff --git a/src/EPPlus.Export.ImageRenderer/Svg/Chart/SvgChart.cs b/src/EPPlus.Export.ImageRenderer/Svg/Chart/SvgChart.cs index e457ed49e..e05402e15 100644 --- a/src/EPPlus.Export.ImageRenderer/Svg/Chart/SvgChart.cs +++ b/src/EPPlus.Export.ImageRenderer/Svg/Chart/SvgChart.cs @@ -76,7 +76,7 @@ private void SetAxisPositionsFromPlotarea(SvgChart sc) if(VerticalAxis != null) { PlaceVerticalAxis(sc, VerticalAxis); - VerticalAxis.AddTickmarksAndValues(); + VerticalAxis.AddTickmarksAndValues(DefItems); } if (HorizontalAxis!=null && HorizontalAxis.Rectangle != null) @@ -93,19 +93,19 @@ private void SetAxisPositionsFromPlotarea(SvgChart sc) HorizontalAxis.Line.Y1 = HorizontalAxis.Line.Y2 = newtop; } - HorizontalAxis.AddTickmarksAndValues(); + HorizontalAxis.AddTickmarksAndValues(DefItems); } if (SecondVerticalAxis!=null) { PlaceVerticalAxis(sc, SecondVerticalAxis); - SecondVerticalAxis.AddTickmarksAndValues(); + SecondVerticalAxis.AddTickmarksAndValues(DefItems); } if (SecondHorizontalAxis != null && SecondHorizontalAxis.Rectangle != null) { PlaceHorizontalAxis(sc, SecondHorizontalAxis); - SecondHorizontalAxis.AddTickmarksAndValues(); + SecondHorizontalAxis.AddTickmarksAndValues(DefItems); } } diff --git a/src/EPPlus.Export.ImageRenderer/Svg/Chart/SvgChartAxis.cs b/src/EPPlus.Export.ImageRenderer/Svg/Chart/SvgChartAxis.cs index b0daae59a..198c15804 100644 --- a/src/EPPlus.Export.ImageRenderer/Svg/Chart/SvgChartAxis.cs +++ b/src/EPPlus.Export.ImageRenderer/Svg/Chart/SvgChartAxis.cs @@ -11,9 +11,11 @@ Date Author Change 27/11/2025 EPPlus Software AB EPPlus 9 *************************************************************************************************/ using EPPlus.Export.ImageRenderer; +using EPPlus.Export.ImageRenderer.RenderItems; using EPPlus.Export.ImageRenderer.RenderItems.SvgItem; using EPPlus.Export.ImageRenderer.Svg.Chart.Util; using EPPlus.Fonts.OpenType.Utils; +using EPPlus.Graphics; using EPPlusImageRenderer.RenderItems; using OfficeOpenXml.Drawing; using OfficeOpenXml.Drawing.Chart; @@ -204,8 +206,8 @@ public List Values public List MajorAxisPositions { get; private set; } public List MinorAxisPositions { get; private set; } - public List MajorGridlinePositions { get; private set; } - public List MinorGridlinePositions { get; private set; } + public List MajorGridlinePositions { get; private set; } + public List MinorGridlinePositions { get; private set; } public List AxisValuesTextBoxes { get; @@ -268,7 +270,7 @@ internal override void AppendRenderItems(List renderItems) } - internal void AddTickmarksAndValues() + internal void AddTickmarksAndValues(List DefItems) { if (Axis.MajorTickMark != eAxisTickMark.None) { @@ -283,11 +285,15 @@ internal void AddTickmarksAndValues() if(Axis.HasMajorGridlines) { MajorGridlinePositions = AddGridlines(MajorUnit, double.NaN, Axis.MajorGridlines, Chart.StyleManager.Style.GridlineMajor); + //DefItems.Add(MajorGridlinePositions[0]); + //MajorGridlinePositions.RemoveAt(0); } if ((Axis.HasMinorGridlines)) { MinorGridlinePositions = AddGridlines(MinorUnit, MajorUnit, Axis.MinorGridlines, Chart.StyleManager.Style.GridlineMinor); + //DefItems.Add(MinorGridlinePositions[0]); + //MinorGridlinePositions.RemoveAt(0); } if (Axis.CrossBetween == eCrossBetween.MidCat) @@ -688,11 +694,11 @@ private List AddTickmarks(double units, eTimeUnit? dateUnit, } return tms; } - private List AddGridlines(double units, double parentUnit, ExcelDrawingBorder lineItem, ExcelChartStyleEntry styleEntry) + private List AddGridlines(double units, double parentUnit, ExcelDrawingBorder lineItem, ExcelChartStyleEntry styleEntry) { var axisStyle = GetAxisStyleEntry(); - var tms = new List(); + var tms = new List(); double min; if (Axis.AxisType == eAxisType.Cat) { @@ -704,41 +710,107 @@ private List AddGridlines(double units, double parentUnit, Ex } var pa = SvgChart.Plotarea; var diff = Max - min; + + List points = new List(); + for (double d = min; d <= Max; d += units) { if(d==min && Line!=null && Line.BorderWidth>0) continue; if (double.IsNaN(parentUnit) || (d % parentUnit != 0)) { - float x1, y1, x2, y2; + //float x1, y1, x2, y2; switch (Axis.AxisPosition) { case eAxisPosition.Left: case eAxisPosition.Right: - y1 = (float)(pa.Rectangle.Top + pa.Rectangle.Height - ((d - min) / diff * pa.Rectangle.Height)); - y2 = y1; - x1 = (float)pa.Rectangle.Right; - x2 = (float)pa.Rectangle.Left; + points.Add(new Point(0f, (float)(pa.Rectangle.Top + pa.Rectangle.Height - ((d - min) / diff * pa.Rectangle.Height)))); + //y1 = (float)(pa.Rectangle.Top + pa.Rectangle.Height - ((d - min) / diff * pa.Rectangle.Height)); + //y2 = y1; + //x1 = (float)pa.Rectangle.Right; + //x2 = (float)pa.Rectangle.Left; break; case eAxisPosition.Top: case eAxisPosition.Bottom: - x1 = (float)(pa.Rectangle.Left + ((d - min) / diff * pa.Rectangle.Width)); - x2 = x1; - y1 = (float)pa.Rectangle.Top; - y2 = (float)pa.Rectangle.Bottom; + points.Add(new Point((float)(pa.Rectangle.Left + ((d - min) / diff * pa.Rectangle.Width)), 0f)); + //x1 = (float)(pa.Rectangle.Left + ((d - min) / diff * pa.Rectangle.Width)); + //x2 = x1; + //y1 = (float)pa.Rectangle.Top; + //y2 = (float)pa.Rectangle.Bottom; break; default: throw new InvalidOperationException("Invalid axis position"); } - var tm = new SvgRenderLineItem(SvgChart, SvgChart.Bounds); - tm.X1 = x1; - tm.Y1 = y1; - tm.X2 = x2; - tm.Y2 = y2; - tm.SetDrawingPropertiesBorder(lineItem, styleEntry.BorderReference.Color, true, lineItem.Width); - tms.Add(tm); + //var tm = new SvgRenderLineItem(SvgChart, SvgChart.Bounds); + //tm.X1 = x1; + //tm.Y1 = y1; + //tm.X2 = x2; + //tm.Y2 = y2; + //tm.SetDrawingPropertiesBorder(lineItem, styleEntry.BorderReference.Color, true, lineItem.Width); + //tms.Add(tm); } } + + float x1, y1, x2, y2; + + string id = ""; + + switch (Axis.AxisPosition) + { + case eAxisPosition.Left: + case eAxisPosition.Right: + id = "xGridLine"; + y1 = 0; + y2 = y1; + x1 = (float)pa.Rectangle.Right; + x2 = (float)pa.Rectangle.Left; + break; + case eAxisPosition.Top: + case eAxisPosition.Bottom: + id = "yGridLine"; + x1 = 0; + x2 = x1; + y1 = (float)pa.Rectangle.Top; + y2 = (float)pa.Rectangle.Bottom; + break; + default: + throw new InvalidOperationException("Invalid axis position"); + } + + var tm = new SvgRenderLineItem(SvgChart, SvgChart.Bounds); + tm.X1 = x1; + tm.Y1 = y1; + tm.X2 = x2; + tm.Y2 = y2; + tm.SetDrawingPropertiesBorder(lineItem, styleEntry.BorderReference.Color, true, lineItem.Width); + + tm.DefId = id; + + tms.Add(tm); + + var distY = points[0].Top - (float)points.Last().Top; + var distX = points[0].Left - (float)points.Last().Left; + + var offsetX = points[0].Top / (points.Count); + var offsetY = points[0].Left / (points.Count); + + for(int i = 0; i < points.Count; i++) + { + var refItem = new SvgUseRefItem(SvgChart, SvgChart.Bounds, id); + if(id == "xGridLine") + { + refItem.Y = offsetY * (i+1); + refItem.X = 0f; + } + else if(id == "yGridLine") + { + refItem.X = offsetX * (i + 1); + refItem.Y = 0f; + } + tms.Add(refItem); + } + + return tms; } diff --git a/src/EPPlus.Export.ImageRenderer/Svg/Chart/SvgChartPlotarea.cs b/src/EPPlus.Export.ImageRenderer/Svg/Chart/SvgChartPlotarea.cs index e657a2e2f..5d64f6aa1 100644 --- a/src/EPPlus.Export.ImageRenderer/Svg/Chart/SvgChartPlotarea.cs +++ b/src/EPPlus.Export.ImageRenderer/Svg/Chart/SvgChartPlotarea.cs @@ -140,6 +140,5 @@ internal override void AppendRenderItems(List renderItems) { renderItems.Add(Rectangle); } - } } diff --git a/src/EPPlus.Export.ImageRenderer/Utils/SvgDrawingWriter.cs b/src/EPPlus.Export.ImageRenderer/Utils/SvgDrawingWriter.cs index a93506ebe..3d53145a5 100644 --- a/src/EPPlus.Export.ImageRenderer/Utils/SvgDrawingWriter.cs +++ b/src/EPPlus.Export.ImageRenderer/Utils/SvgDrawingWriter.cs @@ -10,6 +10,7 @@ Date Author Change ************************************************************************************************* 27/11/2025 EPPlus Software AB EPPlus 9 *************************************************************************************************/ +using EPPlus.Export.ImageRenderer.RenderItems; using EPPlus.Fonts.OpenType.Utils; using EPPlusImageRenderer.Constants; using EPPlusImageRenderer.RenderItems; @@ -45,6 +46,7 @@ internal void WriteSvgDefs(StringBuilder sb, List renderItems) var defSb = new StringBuilder(); var hs = new HashSet(); var ix = 1; + foreach (RenderItem item in renderItems) { string filter = ""; @@ -105,6 +107,14 @@ internal void WriteSvgDefs(StringBuilder sb, List renderItems) { defSb.Append(filter+""); } + + if(item is SvgRenderItem svgItem) + { + if(string.IsNullOrEmpty(svgItem.DefId) == false) + { + svgItem.Render(defSb); + } + } ix++; } if (defSb.Length > 0) @@ -113,6 +123,9 @@ internal void WriteSvgDefs(StringBuilder sb, List renderItems) sb.Append(defSb); sb.Append(""); } + + //Remove all items that have already been rendered + renderItems.RemoveAll(x => x is SvgRenderItem svgX && string.IsNullOrEmpty(svgX.DefId) == false); } private static string GetFilterName(int ix) From 02e117265864ac4eca3cec35814953de20572f3e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jan=20K=C3=A4llman?= Date: Wed, 1 Apr 2026 08:55:53 +0200 Subject: [PATCH 130/151] WIP:More fixes for chart legends positioning --- .../Chart/LineChartToSvgTests.cs | 2 +- src/EPPlus.Export.ImageRenderer/Svg/Chart/SvgChartLegend.cs | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/EPPlus.Export.ImageRenderer.Test/Chart/LineChartToSvgTests.cs b/src/EPPlus.Export.ImageRenderer.Test/Chart/LineChartToSvgTests.cs index 07b6a1d28..e136e3a89 100644 --- a/src/EPPlus.Export.ImageRenderer.Test/Chart/LineChartToSvgTests.cs +++ b/src/EPPlus.Export.ImageRenderer.Test/Chart/LineChartToSvgTests.cs @@ -20,7 +20,7 @@ public void GenerateSvgForLineCharts_sheet1() var ws = p.Workbook.Worksheets[0]; var renderer = new EPPlusImageRenderer.ImageRenderer(); - var ix = 5; + var ix = 4; var c = ws.Drawings[ix]; var svg = renderer.RenderDrawingToSvg(c); SaveTextFileToWorkbook($"svg\\ChartForSvg_ind{ix++}.svg", svg); diff --git a/src/EPPlus.Export.ImageRenderer/Svg/Chart/SvgChartLegend.cs b/src/EPPlus.Export.ImageRenderer/Svg/Chart/SvgChartLegend.cs index 3959b6104..75b5a7651 100644 --- a/src/EPPlus.Export.ImageRenderer/Svg/Chart/SvgChartLegend.cs +++ b/src/EPPlus.Export.ImageRenderer/Svg/Chart/SvgChartLegend.cs @@ -171,7 +171,7 @@ private SvgRenderRectItem GetLegendRectangleAndEntrySize(SvgChart sc, ExcelChart } else { - rect.Top = sc.ChartArea.Rectangle.Height - rect.Height; + rect.Top = sc.ChartArea.Rectangle.Height - rect.Height - MiddleMargin; } break; case eLegendPosition.Right: @@ -288,7 +288,7 @@ private void SetLineLegend(SvgChart sc, int index, SvgLegendSerie pSls, eLegendP sls.SeriesIcon = si; var tbLeft = si.X2 + MarginExtra; - var tbTop = si.Y2 - tm.Height * 0.5; //TODO:Should probably be font ascent + var tbTop = si.Y2 - tm.Height * 0.5; //TODO:Should probably be font ascent double tbWidth; if (pos == eLegendPosition.Left || pos == eLegendPosition.Right) { @@ -386,7 +386,7 @@ private SvgRenderLineItem GetLineSeriesIcon(SvgChart sc, ExcelChartStandardSerie sc.Chart.Legend.Position == eLegendPosition.Bottom) { float y = (float)Rectangle.Top + (float)TopMargin + tm.Height / 2 + MarginExtra; - float x = 0; + float x; if (pSls == null) { x = (float)Rectangle.Left + (float)LeftMargin;// + MarginExtra; From 13106512a7676bcad54dca35a3ca21e08611e12d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ossian=20Edstr=C3=B6m?= Date: Wed, 1 Apr 2026 09:11:12 +0200 Subject: [PATCH 131/151] "use" lines now appear the same as prev implementation --- .../Chart/LineChartToSvgTests.cs | 2 +- .../Svg/Chart/SvgChartAxis.cs | 12 ++++++------ 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/src/EPPlus.Export.ImageRenderer.Test/Chart/LineChartToSvgTests.cs b/src/EPPlus.Export.ImageRenderer.Test/Chart/LineChartToSvgTests.cs index 1ff0ecb7a..db4500422 100644 --- a/src/EPPlus.Export.ImageRenderer.Test/Chart/LineChartToSvgTests.cs +++ b/src/EPPlus.Export.ImageRenderer.Test/Chart/LineChartToSvgTests.cs @@ -25,7 +25,7 @@ public void GenerateSvgForLineCharts_sheet1() //var svg = renderer.RenderDrawingToSvg(c); //SaveTextFileToWorkbook($"svg\\ChartForSvg_ind{ix++}.svg", svg); - var ix = 0; + var ix = 1; foreach (ExcelChart c in ws.Drawings) { var svg = renderer.RenderDrawingToSvg(c); diff --git a/src/EPPlus.Export.ImageRenderer/Svg/Chart/SvgChartAxis.cs b/src/EPPlus.Export.ImageRenderer/Svg/Chart/SvgChartAxis.cs index 198c15804..1adb317bf 100644 --- a/src/EPPlus.Export.ImageRenderer/Svg/Chart/SvgChartAxis.cs +++ b/src/EPPlus.Export.ImageRenderer/Svg/Chart/SvgChartAxis.cs @@ -760,7 +760,7 @@ private List AddGridlines(double units, double parentUnit, ExcelDraw case eAxisPosition.Left: case eAxisPosition.Right: id = "xGridLine"; - y1 = 0; + y1 = (float)points.Last().Top; y2 = y1; x1 = (float)pa.Rectangle.Right; x2 = (float)pa.Rectangle.Left; @@ -768,7 +768,7 @@ private List AddGridlines(double units, double parentUnit, ExcelDraw case eAxisPosition.Top: case eAxisPosition.Bottom: id = "yGridLine"; - x1 = 0; + x1 = (float)points.Last().Left; x2 = x1; y1 = (float)pa.Rectangle.Top; y2 = (float)pa.Rectangle.Bottom; @@ -791,20 +791,20 @@ private List AddGridlines(double units, double parentUnit, ExcelDraw var distY = points[0].Top - (float)points.Last().Top; var distX = points[0].Left - (float)points.Last().Left; - var offsetX = points[0].Top / (points.Count); - var offsetY = points[0].Left / (points.Count); + var offsetX = distX / (points.Count-1); + var offsetY = distY / (points.Count-1); for(int i = 0; i < points.Count; i++) { var refItem = new SvgUseRefItem(SvgChart, SvgChart.Bounds, id); if(id == "xGridLine") { - refItem.Y = offsetY * (i+1); + refItem.Y = offsetY*i; refItem.X = 0f; } else if(id == "yGridLine") { - refItem.X = offsetX * (i + 1); + refItem.X = offsetX*i; refItem.Y = 0f; } tms.Add(refItem); From 897700f41a4a7c13b1171e01634763adf72968ce Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ossian=20Edstr=C3=B6m?= Date: Wed, 1 Apr 2026 09:16:05 +0200 Subject: [PATCH 132/151] fixed conflict --- .../Chart/LineChartToSvgTests.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/EPPlus.Export.ImageRenderer.Test/Chart/LineChartToSvgTests.cs b/src/EPPlus.Export.ImageRenderer.Test/Chart/LineChartToSvgTests.cs index 9fec90eed..878341f09 100644 --- a/src/EPPlus.Export.ImageRenderer.Test/Chart/LineChartToSvgTests.cs +++ b/src/EPPlus.Export.ImageRenderer.Test/Chart/LineChartToSvgTests.cs @@ -20,7 +20,7 @@ public void GenerateSvgForLineCharts_sheet1() var ws = p.Workbook.Worksheets[0]; var renderer = new EPPlusImageRenderer.ImageRenderer(); - var ix = 4; + var ix = 1; var c = ws.Drawings[ix]; var svg = renderer.RenderDrawingToSvg(c); SaveTextFileToWorkbook($"svg\\ChartForSvg_ind{ix++}.svg", svg); From 093f2f7c4110bf0639429f9237961580a8ec6753 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jan=20K=C3=A4llman?= Date: Wed, 1 Apr 2026 13:21:34 +0200 Subject: [PATCH 133/151] WIP:Fixes for Axis and Legends. --- .../Svg/Chart/SvgChartAxis.cs | 24 ++-- .../Svg/Chart/SvgChartLegend.cs | 117 +++++++----------- 2 files changed, 59 insertions(+), 82 deletions(-) diff --git a/src/EPPlus.Export.ImageRenderer/Svg/Chart/SvgChartAxis.cs b/src/EPPlus.Export.ImageRenderer/Svg/Chart/SvgChartAxis.cs index 78d12d7fc..de786534c 100644 --- a/src/EPPlus.Export.ImageRenderer/Svg/Chart/SvgChartAxis.cs +++ b/src/EPPlus.Export.ImageRenderer/Svg/Chart/SvgChartAxis.cs @@ -500,16 +500,26 @@ private double GetAxisItemLeft(int i, OfficeOpenXml.Interfaces.Drawing.Text.Text //Between tickmarks var majorWidth = Rectangle.Width / AxisValues.Count; var majorTickStartingPosition = Rectangle.Left + majorWidth * i; - var middleOfBounds = majorTickStartingPosition + (majorWidth / 2); + //var middleOfBounds = majorTickStartingPosition + (majorWidth / 2); return majorTickStartingPosition; } else { - var min = ConvertUtil.GetValueDouble(Values[0]); - var max = ConvertUtil.GetValueDouble(Values.Last()); - var v = ConvertUtil.GetValueDouble(Values[i]); - var majorWidth = Rectangle.Width * (v-Min)/(Max-Min); - return Rectangle.Left + majorWidth; + if(Axis.AxisType == eAxisType.Cat) + { + var majorWidth = Rectangle.Width / AxisValues.Count; + var majorTickStartingPosition = Rectangle.Left + majorWidth * i; + //var middleOfBounds = majorTickStartingPosition + (majorWidth / 2); + return majorTickStartingPosition; + } + else + { + var min = ConvertUtil.GetValueDouble(Values[0]); + var max = ConvertUtil.GetValueDouble(Values.Last()); + var v = ConvertUtil.GetValueDouble(Values[i]); + var majorWidth = Rectangle.Width * (v - Min) / (Max - Min); + return Rectangle.Left + majorWidth; + } } } } @@ -768,7 +778,7 @@ internal double GetPositionInPlotarea(double val) else { if (val < Min || val > Max) return double.NaN; - var diff = Max - Min+1; + var diff = Max - Min; return (((Max-val) / diff * SvgChart.Plotarea.Rectangle.Height)); } } diff --git a/src/EPPlus.Export.ImageRenderer/Svg/Chart/SvgChartLegend.cs b/src/EPPlus.Export.ImageRenderer/Svg/Chart/SvgChartLegend.cs index 75b5a7651..f3a66bb33 100644 --- a/src/EPPlus.Export.ImageRenderer/Svg/Chart/SvgChartLegend.cs +++ b/src/EPPlus.Export.ImageRenderer/Svg/Chart/SvgChartLegend.cs @@ -239,7 +239,7 @@ internal void SetLegend(SvgChart sc, double entryWidth, double entryHeight) case eChartType.LineMarkersStacked100: case eChartType.LineStacked: case eChartType.LineStacked100: - SetLineLegend(sc, index, pSls, pos, s, sls); + SetLineLegend(sc, index, pSls, pos, s, sls, entryWidth, entryHeight); break; case eChartType.ColumnClustered: case eChartType.ColumnStacked: @@ -274,7 +274,7 @@ internal void SetLegend(SvgChart sc, double entryWidth, double entryHeight) } } - private void SetLineLegend(SvgChart sc, int index, SvgLegendSerie pSls, eLegendPosition pos, ExcelChartSerie s, SvgLegendSerie sls) + private void SetLineLegend(SvgChart sc, int index, SvgLegendSerie pSls, eLegendPosition pos, ExcelChartSerie s, SvgLegendSerie sls, double entryWidth, double entryHeight) { var ls = (ExcelLineChartSerie)s; var tm = _seriesHeadersMeasure[index]; @@ -284,7 +284,7 @@ private void SetLineLegend(SvgChart sc, int index, SvgLegendSerie pSls, eLegendP prevTm = _seriesHeadersMeasure[index - 1]; } - var si = GetLineSeriesIcon(sc, ls, prevTm, tm, pSls); + var si = GetLineSeriesIcon(sc, ls, prevTm, tm, pSls, entryWidth, entryHeight); sls.SeriesIcon = si; var tbLeft = si.X2 + MarginExtra; @@ -301,7 +301,7 @@ private void SetLineLegend(SvgChart sc, int index, SvgLegendSerie pSls, eLegendP var tbHeight = tm.Height; sls.Textbox = new SvgTextBodyItem(ChartRenderer, Bounds, tbLeft, tbTop, tbWidth, tbHeight, false, true); - sls.Textbox.Bounds.Left = si.X2 + MarginExtra; + //sls.Textbox.Bounds.Left = si.X2 + MarginExtra; var entry = Chart.Legend.Entries.FirstOrDefault(x => x.Index == index); var headerText = s.GetHeaderText(index); @@ -347,16 +347,10 @@ private void SetBarLegend(SvgChart sc, int index, SvgLegendSerie pSls, eLegendPo sls.SeriesIcon = si; var tbLeft = si.Right + MarginExtra; - var tbTop = si.Top - (tm.Height-si.Height)/2; + var tbTop = si.Top - (tm.Height - si.Height) / 2; double tbWidth; - if (pos == eLegendPosition.Left || pos == eLegendPosition.Right) - { - tbWidth = Bounds.Width - tbLeft - RightMargin; - } - else - { - tbWidth = Bounds.Width - tbLeft - RightMargin; - } + + tbWidth = Bounds.Width - tbLeft - RightMargin; var tbHeight = tm.Height; sls.Textbox = new SvgTextBodyItem(ChartRenderer, Bounds, tbLeft, tbTop, tbWidth, tbHeight, false, true); @@ -376,67 +370,50 @@ private void SetBarLegend(SvgChart sc, int index, SvgLegendSerie pSls, eLegendPo } } - private SvgRenderLineItem GetLineSeriesIcon(SvgChart sc, ExcelChartStandardSerie cStandardSerie, TextMeasurement pTm, TextMeasurement tm, SvgLegendSerie pSls) + private SvgRenderLineItem GetLineSeriesIcon(SvgChart sc, ExcelChartStandardSerie cStandardSerie, TextMeasurement pTm, TextMeasurement tm, SvgLegendSerie pSls, double entryWidth, double entryHeight) { - var item = new SvgRenderLineItem(sc, Rectangle.Bounds); - item.SetDrawingPropertiesFill(cStandardSerie.Fill, sc.Chart.StyleManager.Style.SeriesLine.FillReference.Color); - item.SetDrawingPropertiesBorder(cStandardSerie.Border, sc.Chart.StyleManager.Style.SeriesLine.BorderReference.Color, cStandardSerie.Border.Fill.Style != eFillStyle.NoFill, 0.75); + var line = new SvgRenderLineItem(sc, Rectangle.Bounds); + line.SetDrawingPropertiesFill(cStandardSerie.Fill, sc.Chart.StyleManager.Style.SeriesLine.FillReference.Color); + line.SetDrawingPropertiesBorder(cStandardSerie.Border, sc.Chart.StyleManager.Style.SeriesLine.BorderReference.Color, cStandardSerie.Border.Fill.Style != eFillStyle.NoFill, 0.75); + var icon = pSls?.SeriesIcon as SvgRenderLineItem; - if (sc.Chart.Legend.Position == eLegendPosition.Top || - sc.Chart.Legend.Position == eLegendPosition.Bottom) - { - float y = (float)Rectangle.Top + (float)TopMargin + tm.Height / 2 + MarginExtra; - float x; - if (pSls == null) - { - x = (float)Rectangle.Left + (float)LeftMargin;// + MarginExtra; - } - else - { - x = (float)pSls.Textbox.Bounds.Right + MiddleMargin; - } + GetItemPosition(sc, pTm, tm, pSls, entryWidth, entryHeight, icon?.X1 ?? 0D, icon?.Y1 ?? 0D, out double x, out double y); - item.X1 = x; - item.Y1 = y; - item.X2 = x + LineLength; - item.Y2 = y; - item.LineCap = eLineCap.Round; - } - else - { - double y; - if (pSls == null) - { - y = TopMargin + tm.Height / 2 + MarginExtra; - } - else - { - y = ((SvgRenderLineItem)pSls.SeriesIcon).Y1 + pTm.Height / 2 + tm.Height / 2 + MiddleMargin; - } + line.X1 = x; + line.Y1 = y; + line.X2 = x + LineLength; + line.Y2 = y; + line.LineCap = eLineCap.Round; - item.X1 = (float)LeftMargin; - item.Y1 = y; - item.X2 = (float)LineLength; - item.Y2 = y; - item.LineCap = eLineCap.Round; - } - - return item; + return line; } - private SvgRenderRectItem GetBarSeriesIcon(SvgChart sc, ExcelChartStandardSerie cStandardSerie, TextMeasurement pTm, TextMeasurement tm, SvgLegendSerie pSls, double entryHeight, double entryWidth) + private SvgRenderRectItem GetBarSeriesIcon(SvgChart sc, ExcelChartStandardSerie cStandardSerie, TextMeasurement pTm, TextMeasurement tm, SvgLegendSerie pSls, double entryWidth, double entryHeight) { var item = new SvgRenderRectItem(sc, Rectangle.Bounds); item.SetDrawingPropertiesFill(cStandardSerie.Fill, sc.Chart.StyleManager.Style.SeriesLine.FillReference.Color); - item.SetDrawingPropertiesBorder(cStandardSerie.Border, sc.Chart.StyleManager.Style.SeriesLine.BorderReference.Color, cStandardSerie.Border.Fill.Style!=eFillStyle.NoFill, 0.75); + item.SetDrawingPropertiesBorder(cStandardSerie.Border, sc.Chart.StyleManager.Style.SeriesLine.BorderReference.Color, cStandardSerie.Border.Fill.Style != eFillStyle.NoFill, 0.75); var iconHeight = GetIconLenght(sc, entryHeight); var icon = pSls?.SeriesIcon as SvgRenderRectItem; + + GetItemPosition(sc, pTm, tm, pSls, entryWidth, entryHeight, icon.Left, icon.Top, out double x, out double y); + + item.LineCap = eLineCap.Round; + item.Left = x; + item.Top = y; + item.Width = iconHeight; + item.Height = iconHeight; + + return item; + } + + private double GetItemPosition(SvgChart sc, TextMeasurement pTm, TextMeasurement tm, SvgLegendSerie pSls, double entryWidth, double entryHeight, double iconLeft, double iconTop, out double x, out double y) + { var topOffset = 0D; if (sc.Chart.Legend.Position == eLegendPosition.Top || sc.Chart.Legend.Position == eLegendPosition.Bottom) { - double x; - if (pSls!=null && pSls.Textbox.Bounds.Right+entryWidth+RightMargin>_maxWidth) + if (pSls != null && pSls.Textbox.Bounds.Right + entryWidth + RightMargin > _maxWidth) { topOffset += entryHeight + MarginExtra; x = Rectangle.Left + LeftMargin; @@ -449,45 +426,35 @@ private SvgRenderRectItem GetBarSeriesIcon(SvgChart sc, ExcelChartStandardSerie } else { - x = icon.Left + entryWidth+MarginExtra; + x = iconLeft + entryWidth + MarginExtra; } } - double y; - if (pSls==null) + if (pSls == null) { y = Rectangle.Top + TopMargin; } else { - y = icon.Top + topOffset; + y = iconTop + topOffset; } - item.Left = x; - item.Top = y; - item.Width = iconHeight; - item.Height = iconHeight; } else { - double y; if (pSls == null) { y = TopMargin + tm.Height / 2 + MarginExtra; } else { - y = ((SvgRenderRectItem)pSls.SeriesIcon).Top + pTm.Height / 2 + tm.Height / 2 ; + y = pSls.Textbox.Bounds.Bottom + pTm.Height / 2 + tm.Height / 2; } + x = LeftMargin; - item.Left = (float)LeftMargin; //4 - item.Top = y; - item.Width = iconHeight; - item.Height = iconHeight; - item.LineCap = eLineCap.Round; } - return item; + return topOffset; } internal override void AppendRenderItems(List renderItems) From a43616cf1dbc3c1028c53e1093dee78fb9572cbe Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jan=20K=C3=A4llman?= Date: Wed, 1 Apr 2026 14:29:28 +0200 Subject: [PATCH 134/151] WIP:Fix for charts with no visible axis to display series. --- .../Chart/LineChartToSvgTests.cs | 22 ++--- .../Svg/Chart/SvgChartAxis.cs | 94 +++++++++---------- .../Svg/Chart/SvgChartPlotarea.cs | 2 +- .../Drawing/Chart/ExcelChartAxisStandard.cs | 2 +- 4 files changed, 59 insertions(+), 61 deletions(-) diff --git a/src/EPPlus.Export.ImageRenderer.Test/Chart/LineChartToSvgTests.cs b/src/EPPlus.Export.ImageRenderer.Test/Chart/LineChartToSvgTests.cs index 878341f09..1ff0ecb7a 100644 --- a/src/EPPlus.Export.ImageRenderer.Test/Chart/LineChartToSvgTests.cs +++ b/src/EPPlus.Export.ImageRenderer.Test/Chart/LineChartToSvgTests.cs @@ -20,17 +20,17 @@ public void GenerateSvgForLineCharts_sheet1() var ws = p.Workbook.Worksheets[0]; var renderer = new EPPlusImageRenderer.ImageRenderer(); - var ix = 1; - var c = ws.Drawings[ix]; - var svg = renderer.RenderDrawingToSvg(c); - SaveTextFileToWorkbook($"svg\\ChartForSvg_ind{ix++}.svg", svg); - - //var ix = 0;g - //foreach (ExcelChart c in ws.Drawings) - //{ - // var svg = renderer.RenderDrawingToSvg(c); - // SaveTextFileToWorkbook($"svg\\ChartForSvg{ix++}.svg", svg); - //} + //var ix = 1; + //var c = ws.Drawings[ix]; + //var svg = renderer.RenderDrawingToSvg(c); + //SaveTextFileToWorkbook($"svg\\ChartForSvg_ind{ix++}.svg", svg); + + var ix = 0; + foreach (ExcelChart c in ws.Drawings) + { + var svg = renderer.RenderDrawingToSvg(c); + SaveTextFileToWorkbook($"svg\\ChartForSvg{ix++}.svg", svg); + } } } diff --git a/src/EPPlus.Export.ImageRenderer/Svg/Chart/SvgChartAxis.cs b/src/EPPlus.Export.ImageRenderer/Svg/Chart/SvgChartAxis.cs index 3f6a90f33..efca74352 100644 --- a/src/EPPlus.Export.ImageRenderer/Svg/Chart/SvgChartAxis.cs +++ b/src/EPPlus.Export.ImageRenderer/Svg/Chart/SvgChartAxis.cs @@ -41,7 +41,7 @@ internal SvgChartAxis(SvgChart sc, ExcelChartAxisStandard ax) : base(sc) Axis = ax; SetMargins(ax.TextBody); - if (sc.Chart.Series.Count == 0 || (ax.Deleted == true && ax.HasTitle==false)) + if (sc.Chart.Series.Count == 0) { return; } @@ -55,66 +55,64 @@ internal SvgChartAxis(SvgChart sc, ExcelChartAxisStandard ax) : base(sc) Title = null; } - if (ax.Deleted == false || ax.HasMajorGridlines || ax.HasMinorGridlines) + Values = GetAxisValue(ax, sc.ChartArea.Rectangle, out double? min, out double? max, out double? majorUnit, out eTimeUnit? dateUnit, out eTextOrientation orientation); + AxisValues = GetAxisDisplayValues(ax, Values, min, max, majorUnit); + + Min = min ?? 0D; + Max = max ?? (Values.Count > 0 ? ConvertUtil.GetValueDouble(Values[Values.Count - 1], false, true) : 0D); + MajorUnit = majorUnit ?? 1; + MinorUnit = ax.MinorUnit ?? GetAutoMinUnit(MajorUnit); + MajorDateUnit = dateUnit; + LabelOrientation = orientation; + + if (ax.Deleted == false) { - Values = GetAxisValue(ax, sc.ChartArea.Rectangle, out double? min, out double? max, out double? majorUnit, out eTimeUnit? dateUnit, out eTextOrientation orientation); - AxisValues = GetAxisDisplayValues(ax, Values, min, max, majorUnit); - - Min = min ?? 0D; - Max = max ?? (Values.Count > 0 ? ConvertUtil.GetValueDouble(Values[Values.Count - 1], false, true) : 0D); - MajorUnit = majorUnit ?? 1; - MinorUnit = ax.MinorUnit ?? GetAutoMinUnit(MajorUnit); - MajorDateUnit = dateUnit; - LabelOrientation = orientation; - if (ax.Deleted == false) - { - if (ax.Layout.HasLayout) - { - Rectangle = GetRectFromManualLayout(sc, ax.Layout); - } - else + if (ax.Layout.HasLayout) + { + Rectangle = GetRectFromManualLayout(sc, ax.Layout); + } + else + { + Rectangle = new SvgRenderRectItem(sc, sc.Bounds); + if (ax.AxisPosition == eAxisPosition.Left || ax.AxisPosition == eAxisPosition.Right) { - Rectangle = new SvgRenderRectItem(sc, sc.Bounds); - if (ax.AxisPosition == eAxisPosition.Left || ax.AxisPosition == eAxisPosition.Right) + if (ax.AxisPosition == eAxisPosition.Left) { - if (ax.AxisPosition == eAxisPosition.Left) + Rectangle.Width = GetTextWidest(sc, ax) + LeftMargin; + var ll = 8D; + if (sc.Chart.HasLegend && sc.Chart.Legend.Position == eLegendPosition.Left) { - Rectangle.Width = GetTextWidest(sc, 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.Right; - } - else - { - Rectangle.Width = GetTextWidest(sc, ax) + RightMargin; - var lp = sc.ChartArea.Rectangle.Width - Rectangle.Width - 8D; - if (sc.Chart.HasLegend && sc.Chart.Legend.Position == eLegendPosition.Right) - { - lp = sc.Legend.Rectangle.Left + -Rectangle.Width; - } - Rectangle.Left = Title == null ? lp : Title.Rectangle.Left - Rectangle.Width-LeftMargin; + ll = sc.Legend.Rectangle.Right + sc.Legend.RightMargin; } + Rectangle.Left = Title == null ? ll : Title.Rectangle.Right; } else { - Rectangle.Height = GetTextHeight(sc, ax); - //TODO:Fix - Rectangle.Top = Title == null || ax.AxisPosition == eAxisPosition.Top ? sc.ChartArea.Rectangle.Height - 8 - Rectangle.Height : Title.Rectangle.Top - Rectangle.Height - 8; + Rectangle.Width = GetTextWidest(sc, ax) + RightMargin; + var lp = sc.ChartArea.Rectangle.Width - Rectangle.Width - 8D; + if (sc.Chart.HasLegend && sc.Chart.Legend.Position == eLegendPosition.Right) + { + lp = sc.Legend.Rectangle.Left + -Rectangle.Width; + } + Rectangle.Left = Title == null ? lp : Title.Rectangle.Left - Rectangle.Width-LeftMargin; } } - - Rectangle.FillColor = "none"; - - Line = new SvgRenderLineItem(sc, Rectangle.Bounds); - Line.SetDrawingPropertiesBorder(ax.Border, sc.Chart.StyleManager.Style.Title.BorderReference.Color, ax.Border.Fill.Style != eFillStyle.NoFill, 1); - if(Line.BorderWidth < 1) + else { - Line.BorderWidth = 1; + Rectangle.Height = GetTextHeight(sc, ax); + //TODO:Fix + Rectangle.Top = Title == null || ax.AxisPosition == eAxisPosition.Top ? sc.ChartArea.Rectangle.Height - 8 - Rectangle.Height : Title.Rectangle.Top - Rectangle.Height - 8; } } + + Rectangle.FillColor = "none"; + + Line = new SvgRenderLineItem(sc, Rectangle.Bounds); + Line.SetDrawingPropertiesBorder(ax.Border, sc.Chart.StyleManager.Style.Title.BorderReference.Color, ax.Border.Fill.Style != eFillStyle.NoFill, 1); + if(Line.BorderWidth < 1) + { + Line.BorderWidth = 1; + } } } diff --git a/src/EPPlus.Export.ImageRenderer/Svg/Chart/SvgChartPlotarea.cs b/src/EPPlus.Export.ImageRenderer/Svg/Chart/SvgChartPlotarea.cs index 5d64f6aa1..36d6efc1a 100644 --- a/src/EPPlus.Export.ImageRenderer/Svg/Chart/SvgChartPlotarea.cs +++ b/src/EPPlus.Export.ImageRenderer/Svg/Chart/SvgChartPlotarea.cs @@ -84,7 +84,7 @@ private double GetPlotAreaWidth(SvgChart sc, SvgRenderRectItem rect) private double GetPlotAreaLeft(SvgChart sc) { var leftAxis = GetAxisByPosition(sc, eAxisPosition.Left); - if (leftAxis == null) + if (leftAxis == null || (leftAxis.Rectangle==null && leftAxis.Title?.Rectangle==null)) { return sc.Chart.Legend?.Position == eLegendPosition.Left ? sc.Legend.Bounds.Right + LeftMargin : LeftMargin; } diff --git a/src/EPPlus/Drawing/Chart/ExcelChartAxisStandard.cs b/src/EPPlus/Drawing/Chart/ExcelChartAxisStandard.cs index e31df16d0..d897f19e4 100644 --- a/src/EPPlus/Drawing/Chart/ExcelChartAxisStandard.cs +++ b/src/EPPlus/Drawing/Chart/ExcelChartAxisStandard.cs @@ -836,7 +836,7 @@ internal void GetSeriesValues(out bool isCount, out List> values) } else { - AddFromSerie(l, serie.XSeries, serie.NumberLiteralsX, serie.StringLiteralsX, true, new string[] { serie.HeaderAddress.Address, serie.GetHeaderText(ix) }); + AddFromSerie(l, serie.XSeries, serie.NumberLiteralsX, serie.StringLiteralsX, true, new string[] { serie.HeaderAddress?.Address, serie.GetHeaderText(ix) }); } } else From 9943d15250e14ebb9c1b073324228eabbfea837b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ossian=20Edstr=C3=B6m?= Date: Wed, 1 Apr 2026 14:43:11 +0200 Subject: [PATCH 135/151] Fixed vertical gridlines --- .../Chart/LineChartToSvgTests.cs | 4 +-- .../StyleTests.cs | 24 ++++++++++++++ .../DrawingBase.cs | 2 ++ .../SvgItem/SvgChartSerieDataLabel.cs | 24 +++++++++----- .../Style/SvgFill.cs | 1 - .../Svg/Chart/SvgChart.cs | 14 +++++++++ .../Svg/Chart/SvgChartAxis.cs | 7 +++-- src/EPPlus.Graphics/Point.cs | 2 +- .../Dxf/ExcelDxfStyleConditionalFormatting.cs | 2 -- .../HtmlConditionalFormattingTest.cs | 31 +++++++++++++++++++ 10 files changed, 95 insertions(+), 16 deletions(-) diff --git a/src/EPPlus.Export.ImageRenderer.Test/Chart/LineChartToSvgTests.cs b/src/EPPlus.Export.ImageRenderer.Test/Chart/LineChartToSvgTests.cs index 878341f09..2d67c161e 100644 --- a/src/EPPlus.Export.ImageRenderer.Test/Chart/LineChartToSvgTests.cs +++ b/src/EPPlus.Export.ImageRenderer.Test/Chart/LineChartToSvgTests.cs @@ -20,12 +20,12 @@ public void GenerateSvgForLineCharts_sheet1() var ws = p.Workbook.Worksheets[0]; var renderer = new EPPlusImageRenderer.ImageRenderer(); - var ix = 1; + var ix = 4; var c = ws.Drawings[ix]; var svg = renderer.RenderDrawingToSvg(c); SaveTextFileToWorkbook($"svg\\ChartForSvg_ind{ix++}.svg", svg); - //var ix = 0;g + //var ix = 0; //foreach (ExcelChart c in ws.Drawings) //{ // var svg = renderer.RenderDrawingToSvg(c); diff --git a/src/EPPlus.Export.ImageRenderer.Test/StyleTests.cs b/src/EPPlus.Export.ImageRenderer.Test/StyleTests.cs index 10333a7fc..487249d24 100644 --- a/src/EPPlus.Export.ImageRenderer.Test/StyleTests.cs +++ b/src/EPPlus.Export.ImageRenderer.Test/StyleTests.cs @@ -1,8 +1,10 @@ using EPPlus.Export.ImageRenderer.Style; +using EPPlusImageRenderer; using OfficeOpenXml; using OfficeOpenXml.Drawing; using OfficeOpenXml.Export.HtmlExport; using OfficeOpenXml.Export.HtmlExport.CssCollections; +using OfficeOpenXml.Export.HtmlExport.Exporters.Internal; using OfficeOpenXml.Export.HtmlExport.Translators; using OfficeOpenXml.Export.HtmlExport.Writers; using OfficeOpenXml.Utils; @@ -19,6 +21,22 @@ namespace EPPlus.Export.ImageRenderer.Tests [TestClass] public class StyleTests : TestBase { + + [TestMethod] + public void BaseThemeChartStyle() + { + ExcelPackage.License.SetNonCommercialOrganization("EPPlus Project"); + + using (var p = OpenTemplatePackage("baseThemeChartStyle.xlsx")) + { + var c = p.Workbook.Worksheets[0].Drawings[0].As.Chart.LineChart; + + var renderer = new EPPlusImageRenderer.ImageRenderer(); + var svg = renderer.RenderDrawingToSvg(c); + SaveTextFileToWorkbook($"svg\\baseThemeChartStyle.svg", svg); + } + } + [TestMethod] public void ExtractThemeStyleWorks() { @@ -68,6 +86,11 @@ public void ExtractThemeStyleWorks() simpleChart.StyleManager.ApplyStyles(); simpleChart.Title.TextBody.Paragraphs[0].TextRuns[0].Fill.Color = Color.CornflowerBlue; + + var renderer = new EPPlusImageRenderer.ImageRenderer(); + var svg = renderer.RenderDrawingToSvg(simpleChart); + SaveTextFileToWorkbook($"svg\\MyLineIonThemeExcel2.svg", svg); + //var serLine = chartDefaultStyle.SeriesLine; //var lineElement = serLine.Border.LineElement; //var serLineFillRef = serLine.Border.Fill; @@ -103,6 +126,7 @@ public void TextRunIsStyledButNotTitleFont() //} } + ///// ///// Exports an to a html string ///// diff --git a/src/EPPlus.Export.ImageRenderer/DrawingBase.cs b/src/EPPlus.Export.ImageRenderer/DrawingBase.cs index 4c511b715..339b2ae6e 100644 --- a/src/EPPlus.Export.ImageRenderer/DrawingBase.cs +++ b/src/EPPlus.Export.ImageRenderer/DrawingBase.cs @@ -19,6 +19,7 @@ Date Author Change using OfficeOpenXml; using OfficeOpenXml.Drawing; using OfficeOpenXml.Drawing.Theme; +using OfficeOpenXml.Export.HtmlExport; using OfficeOpenXml.Interfaces.Drawing.Text; using System.Collections.Generic; using System.Text; @@ -47,6 +48,7 @@ internal DrawingBase() //Theme = wb.ThemeManager.GetOrCreateTheme(); } + internal readonly StyleCache _styleCache = new StyleCache(); public ExcelDrawing Drawing { get; } public ExcelTheme Theme { get;} public ExcelWorkbook Workbook => Drawing._drawings.Worksheet.Workbook; diff --git a/src/EPPlus.Export.ImageRenderer/RenderItems/SvgItem/SvgChartSerieDataLabel.cs b/src/EPPlus.Export.ImageRenderer/RenderItems/SvgItem/SvgChartSerieDataLabel.cs index 2fc7c1bef..03b2eed39 100644 --- a/src/EPPlus.Export.ImageRenderer/RenderItems/SvgItem/SvgChartSerieDataLabel.cs +++ b/src/EPPlus.Export.ImageRenderer/RenderItems/SvgItem/SvgChartSerieDataLabel.cs @@ -33,21 +33,27 @@ public SvgChartSerieDataLabel(SvgChart chart, ExcelChartSerieDataLabel dlblSerie _defaultMargins = new BoundingBox(l, top, right, bottom); } + if (dlblSerie.DataLabels.Count == 0 && serie.NumberOfItems > 0) { - - for (int i = 0; i < serie.NumberOfItems; i++) + if (xValues != null) { - AddDatalabel(chart, serie, dlblSerie, xValues[i], yValues[i], maxBounds); + for (int i = 0; i < serie.NumberOfItems; i++) + { + AddDatalabel(chart, serie, dlblSerie, xValues[i], yValues[i], maxBounds); + } } } else { - for (int i = 0; i < dlblSerie.DataLabels.Count; i++) + if (xValues != null) { - var dataLabel = dlblSerie.DataLabels[i]; + for (int i = 0; i < dlblSerie.DataLabels.Count; i++) + { + var dataLabel = dlblSerie.DataLabels[i]; - AddDatalabel(chart, serie, dataLabel, xValues[i], yValues[i], maxBounds); + AddDatalabel(chart, serie, dataLabel, xValues[i], yValues[i], maxBounds); + } } } } @@ -96,7 +102,11 @@ private void AddDatalabel(SvgChart chart, ExcelChartStandardSerie serie, ExcelCh internal void SetParentPoint(BoundingBox parent, int index) { - dataLabels[index].SetParentPoint(parent); + if (dataLabels.Count > index) + { + dataLabels[index].SetParentPoint(parent); + } + //dataLabels[index].SetParentPoint(parent); } internal override void AppendRenderItems(List renderItems) diff --git a/src/EPPlus.Export.ImageRenderer/Style/SvgFill.cs b/src/EPPlus.Export.ImageRenderer/Style/SvgFill.cs index 7783561ef..400566b21 100644 --- a/src/EPPlus.Export.ImageRenderer/Style/SvgFill.cs +++ b/src/EPPlus.Export.ImageRenderer/Style/SvgFill.cs @@ -7,7 +7,6 @@ using System.Globalization; using System.Linq; using System.Text; -using OfficeOpenXml.Drawing.Theme; using OfficeOpenXml.Style; using OfficeOpenXml.Utils.EnumUtils; diff --git a/src/EPPlus.Export.ImageRenderer/Svg/Chart/SvgChart.cs b/src/EPPlus.Export.ImageRenderer/Svg/Chart/SvgChart.cs index e05402e15..d21e86f87 100644 --- a/src/EPPlus.Export.ImageRenderer/Svg/Chart/SvgChart.cs +++ b/src/EPPlus.Export.ImageRenderer/Svg/Chart/SvgChart.cs @@ -17,6 +17,7 @@ Date Author Change using EPPlusImageRenderer.Utils; using OfficeOpenXml.Drawing; using OfficeOpenXml.Drawing.Chart; +using OfficeOpenXml.Export.HtmlExport.Exporters.Internal; using OfficeOpenXml.FormulaParsing.Excel.Functions.MathFunctions; using OfficeOpenXml.Interfaces.Drawing.Text; using System; @@ -31,6 +32,19 @@ internal class SvgChart : DrawingChart { public SvgChart(ExcelChart chart) : base(chart) { + + //chart.Fill. + //chart.Fill + //chart.Fill. + ////chart.StyleManager.Style.ChartArea. + ////_styleCache.GetOrCreateId(chart.Fill.) + ////var cssExporter = new CssChartExporterSync(chart); + ////var css = cssExporter.GetCssString(); + + //chart.Fill + //var cssExporter = new CssChartExporterSync(chart); + //var css = cssExporter.GetCssString(); + SetChartArea(); if(chart.HasTitle && chart.Series.Count > 0) diff --git a/src/EPPlus.Export.ImageRenderer/Svg/Chart/SvgChartAxis.cs b/src/EPPlus.Export.ImageRenderer/Svg/Chart/SvgChartAxis.cs index e40911ffa..e12bf5967 100644 --- a/src/EPPlus.Export.ImageRenderer/Svg/Chart/SvgChartAxis.cs +++ b/src/EPPlus.Export.ImageRenderer/Svg/Chart/SvgChartAxis.cs @@ -726,7 +726,8 @@ private List AddGridlines(double units, double parentUnit, ExcelDraw break; case eAxisPosition.Top: case eAxisPosition.Bottom: - points.Add(new Point((float)(pa.Rectangle.Left + ((d - min) / diff * pa.Rectangle.Width)), 0f)); + var xValue = (float)(pa.Rectangle.Left + ((d - min) / diff * pa.Rectangle.Width)); + points.Add(new Point(xValue, 0f)); //x1 = (float)(pa.Rectangle.Left + ((d - min) / diff * pa.Rectangle.Width)); //x2 = x1; //y1 = (float)pa.Rectangle.Top; @@ -783,8 +784,8 @@ private List AddGridlines(double units, double parentUnit, ExcelDraw tms.Add(tm); - var distY = points[0].Top - (float)points.Last().Top; var distX = points[0].Left - (float)points.Last().Left; + var distY = points[0].Top - (float)points.Last().Top; var offsetX = distX / (points.Count-1); var offsetY = distY / (points.Count-1); @@ -794,8 +795,8 @@ private List AddGridlines(double units, double parentUnit, ExcelDraw var refItem = new SvgUseRefItem(SvgChart, SvgChart.Bounds, id); if(id == "xGridLine") { - refItem.Y = offsetY*i; refItem.X = 0f; + refItem.Y = offsetY*i; } else if(id == "yGridLine") { diff --git a/src/EPPlus.Graphics/Point.cs b/src/EPPlus.Graphics/Point.cs index 15beb0c5d..b2380933f 100644 --- a/src/EPPlus.Graphics/Point.cs +++ b/src/EPPlus.Graphics/Point.cs @@ -19,7 +19,7 @@ internal double Top internal double Left { - get { return LocalPosition.Y; } + get { return LocalPosition.X; } set { LocalPosition = new Vector2(value, LocalPosition.Y); diff --git a/src/EPPlus/Style/Dxf/ExcelDxfStyleConditionalFormatting.cs b/src/EPPlus/Style/Dxf/ExcelDxfStyleConditionalFormatting.cs index ca15280b7..b92af9680 100644 --- a/src/EPPlus/Style/Dxf/ExcelDxfStyleConditionalFormatting.cs +++ b/src/EPPlus/Style/Dxf/ExcelDxfStyleConditionalFormatting.cs @@ -37,8 +37,6 @@ internal override string Id { get { - - return GetId() + ExcelDxfAlignment.GetEmptyId() + ExcelDxfProtection.GetEmptyId(); } } diff --git a/src/EPPlusTest/Export/HtmlExport/HtmlConditionalFormattingTest.cs b/src/EPPlusTest/Export/HtmlExport/HtmlConditionalFormattingTest.cs index 7e0557410..9355f2b8c 100644 --- a/src/EPPlusTest/Export/HtmlExport/HtmlConditionalFormattingTest.cs +++ b/src/EPPlusTest/Export/HtmlExport/HtmlConditionalFormattingTest.cs @@ -140,5 +140,36 @@ public void ExportingHtmlCFsWithThemeColor() SaveAndCleanup(p); } } + + [TestMethod] + public void ExportCss() + { + ExcelPackage.License.SetNonCommercialOrganization("EPPlus Project"); + + var chartName = "DifficultCssExportMb.xlsx"; + using (var p = OpenTemplatePackage(chartName)) + { + var ws = p.Workbook.Worksheets[0]; + + var range = ws.Cells["A1:G10"]; + + var html = range.CreateHtmlExporter(); + var fs = GetOutputFile("", "cssExportDatabarMb.html"); + + var cssStr = html.GetCssString(); + //var lChart = ws.Drawings[0].As.Chart.LineChart; + + ////lChart.Title.Font.Color = Color.DeepSkyBlue; + + //lChart.StyleManager.ApplyStyles(); + + SaveAndCleanup(p); + } + + //using(var p= OpenPackage(chartName,false)) + //{ + + //} + } } } From 262f882535f1aaa7aadda78cfd8ec0d3310dbeba Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ossian=20Edstr=C3=B6m?= Date: Wed, 1 Apr 2026 15:27:38 +0200 Subject: [PATCH 136/151] Made vertical lines positively signed --- src/EPPlus.Export.ImageRenderer/Svg/Chart/SvgChartAxis.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/EPPlus.Export.ImageRenderer/Svg/Chart/SvgChartAxis.cs b/src/EPPlus.Export.ImageRenderer/Svg/Chart/SvgChartAxis.cs index e12bf5967..ae609ccad 100644 --- a/src/EPPlus.Export.ImageRenderer/Svg/Chart/SvgChartAxis.cs +++ b/src/EPPlus.Export.ImageRenderer/Svg/Chart/SvgChartAxis.cs @@ -764,7 +764,7 @@ private List AddGridlines(double units, double parentUnit, ExcelDraw case eAxisPosition.Top: case eAxisPosition.Bottom: id = "yGridLine"; - x1 = (float)points.Last().Left; + x1 = (float)points[0].Left; x2 = x1; y1 = (float)pa.Rectangle.Top; y2 = (float)pa.Rectangle.Bottom; @@ -784,7 +784,7 @@ private List AddGridlines(double units, double parentUnit, ExcelDraw tms.Add(tm); - var distX = points[0].Left - (float)points.Last().Left; + var distX = (float)points.Last().Left - points[0].Left; var distY = points[0].Top - (float)points.Last().Top; var offsetX = distX / (points.Count-1); From d1269c734452b697b56f2b05ca0919ef0e783b24 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jan=20K=C3=A4llman?= Date: Wed, 1 Apr 2026 16:36:57 +0200 Subject: [PATCH 137/151] WIP:Fixed column chart rendering when having multiple series. --- .../Chart/LineChartToSvgTests.cs | 2 +- .../Chart/Axis/CategoryAxisScaleCalculator.cs | 6 ++-- .../BarColumnChartTypeDrawer.cs | 35 +++++++++++++++---- .../Svg/Chart/SvgChartAxis.cs | 34 ++++++++++++------ src/EPPlus/Drawing/Chart/ExcelBarChart.cs | 2 +- .../Drawing/Chart/ExcelChartAxisStandard.cs | 9 ++--- 6 files changed, 59 insertions(+), 29 deletions(-) diff --git a/src/EPPlus.Export.ImageRenderer.Test/Chart/LineChartToSvgTests.cs b/src/EPPlus.Export.ImageRenderer.Test/Chart/LineChartToSvgTests.cs index 1ff0ecb7a..b9d0d5e81 100644 --- a/src/EPPlus.Export.ImageRenderer.Test/Chart/LineChartToSvgTests.cs +++ b/src/EPPlus.Export.ImageRenderer.Test/Chart/LineChartToSvgTests.cs @@ -123,7 +123,7 @@ public void GenerateSvgForCharts_SecondaryAxis_sheet2() { var ws = p.Workbook.Worksheets[1]; var renderer = new EPPlusImageRenderer.ImageRenderer(); - //var ix = 0; + //var ix = 3; //var c = ws.Drawings[ix]; //var svg = renderer.RenderDrawingToSvg(c); //SaveTextFileToWorkbook($"svg\\ChartForSvg_sheet2_{ix++}.svg", svg); diff --git a/src/EPPlus.Export.ImageRenderer/Svg/Chart/Axis/CategoryAxisScaleCalculator.cs b/src/EPPlus.Export.ImageRenderer/Svg/Chart/Axis/CategoryAxisScaleCalculator.cs index 29dd7e667..e7d8bcc1f 100644 --- a/src/EPPlus.Export.ImageRenderer/Svg/Chart/Axis/CategoryAxisScaleCalculator.cs +++ b/src/EPPlus.Export.ImageRenderer/Svg/Chart/Axis/CategoryAxisScaleCalculator.cs @@ -42,7 +42,7 @@ internal static AxisScale CalculateByWidth(ref List values, ITextMeasure MajorInterval = interval, MinorInterval = 1, Min = 1, - Max = values.Count, + Max = displayValues.Count, TextOrientation = eTextOrientation.Horizontal }; } @@ -53,7 +53,7 @@ internal static AxisScale CalculateByWidth(ref List values, ITextMeasure MajorInterval = interval, MinorInterval = 1, Min = 1, - Max = values.Count, + Max = displayValues.Count, TextOrientation = eTextOrientation.Diagonal }; } @@ -78,7 +78,7 @@ internal static AxisScale CalculateByWidth(ref List values, ITextMeasure MajorInterval = interval, MinorInterval = 1, Min = 1, - Max = values.Count, + Max = displayValues.Count, TextOrientation = eTextOrientation.Vertical }; diff --git a/src/EPPlus.Export.ImageRenderer/Svg/Chart/ChartTypeDrawers/BarColumnChartTypeDrawer.cs b/src/EPPlus.Export.ImageRenderer/Svg/Chart/ChartTypeDrawers/BarColumnChartTypeDrawer.cs index 80e6aa998..1b8e452ec 100644 --- a/src/EPPlus.Export.ImageRenderer/Svg/Chart/ChartTypeDrawers/BarColumnChartTypeDrawer.cs +++ b/src/EPPlus.Export.ImageRenderer/Svg/Chart/ChartTypeDrawers/BarColumnChartTypeDrawer.cs @@ -108,13 +108,26 @@ private void AddColumn(ExcelBarChart chartType, ExcelBarChartSerie serie, List Max) return double.NaN; - var diff = Max - Min+1; - return (((val-Min) / diff * SvgChart.Plotarea.Rectangle.Width)); + var diff = Max - Min + 1; + return (((val - Min) / diff * SvgChart.Plotarea.Rectangle.Width)); } } } @@ -886,11 +900,11 @@ protected List GetAxisValue(ExcelChartAxisStandard ax, RenderItem rect, if (ax.AxisType == eAxisType.Cat && isCount == false) { - min = 0; - max = values.Count; - majorUnit = 1; - dateUnit = null; - orientation = eTextOrientation.Horizontal; + //min = 0; + //max = values.Count / Chart.Series.Count; + //majorUnit = 1; + //dateUnit = null; + //orientation = eTextOrientation.Horizontal; var res = CategoryAxisScaleCalculator.CalculateByWidth(ref values, SvgChart.TextMeasurer, options); min = res.Min; diff --git a/src/EPPlus/Drawing/Chart/ExcelBarChart.cs b/src/EPPlus/Drawing/Chart/ExcelBarChart.cs index 654fc5275..75f8d035a 100644 --- a/src/EPPlus/Drawing/Chart/ExcelBarChart.cs +++ b/src/EPPlus/Drawing/Chart/ExcelBarChart.cs @@ -240,7 +240,7 @@ public int Overlap { get { - return _chartXmlHelper.GetXmlNodeInt(_overlapPath); + return _chartXmlHelper.GetXmlNodeInt(_overlapPath, 0); } set { diff --git a/src/EPPlus/Drawing/Chart/ExcelChartAxisStandard.cs b/src/EPPlus/Drawing/Chart/ExcelChartAxisStandard.cs index d897f19e4..611e96f8d 100644 --- a/src/EPPlus/Drawing/Chart/ExcelChartAxisStandard.cs +++ b/src/EPPlus/Drawing/Chart/ExcelChartAxisStandard.cs @@ -798,14 +798,9 @@ internal override List GetAxisValues(out bool isCount) { List> values; GetSeriesValues(out isCount, out values); - List dl; - if(AxisType==eAxisType.Cat) + var dl = values.SelectMany(x => x).Distinct().ToList(); + if (AxisType!=eAxisType.Cat) { - dl = values.SelectMany(x => x).Distinct().ToList(); - } - else - { - dl = values.SelectMany(x => x).Distinct().ToList(); dl.Sort(); } if (Orientation == eAxisOrientation.MaxMin) From 7404823546925cf617ac6923f024d73304eb2ef3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jan=20K=C3=A4llman?= Date: Thu, 2 Apr 2026 13:00:28 +0200 Subject: [PATCH 138/151] WIP:Axis Title positioning --- .../RenderItems/SvgItem/SvgTextBox.cs | 33 +++++++++++++++++++ .../BarColumnChartTypeDrawer.cs | 3 +- .../Svg/Chart/SvgChart.cs | 6 ++-- .../Svg/Chart/SvgChartAxis.cs | 4 +-- .../Svg/Chart/SvgChartPlotarea.cs | 8 ++--- .../Svg/Chart/SvgChartTitle.cs | 4 +-- 6 files changed, 46 insertions(+), 12 deletions(-) diff --git a/src/EPPlus.Export.ImageRenderer/RenderItems/SvgItem/SvgTextBox.cs b/src/EPPlus.Export.ImageRenderer/RenderItems/SvgItem/SvgTextBox.cs index 3f2178dbe..2704555a7 100644 --- a/src/EPPlus.Export.ImageRenderer/RenderItems/SvgItem/SvgTextBox.cs +++ b/src/EPPlus.Export.ImageRenderer/RenderItems/SvgItem/SvgTextBox.cs @@ -10,6 +10,7 @@ using System; using System.Collections.Generic; using System.Runtime.Serialization; +using OfficeOpenXml.FormulaParsing.Excel.Functions.MathFunctions; namespace EPPlus.Export.ImageRenderer.RenderItems.SvgItem @@ -121,6 +122,38 @@ internal double Rotation } } /// + /// 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+GetActualRight(); + } + /// + /// 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 diff --git a/src/EPPlus.Export.ImageRenderer/Svg/Chart/ChartTypeDrawers/BarColumnChartTypeDrawer.cs b/src/EPPlus.Export.ImageRenderer/Svg/Chart/ChartTypeDrawers/BarColumnChartTypeDrawer.cs index 1b8e452ec..bfddbaa6a 100644 --- a/src/EPPlus.Export.ImageRenderer/Svg/Chart/ChartTypeDrawers/BarColumnChartTypeDrawer.cs +++ b/src/EPPlus.Export.ImageRenderer/Svg/Chart/ChartTypeDrawers/BarColumnChartTypeDrawer.cs @@ -106,8 +106,7 @@ private void AddColumn(ExcelBarChart chartType, ExcelBarChartSerie serie, List Date: Tue, 7 Apr 2026 11:39:14 +0200 Subject: [PATCH 139/151] Added styling items and start of new defs --- .../RenderItems/SvgRenderLineItem.cs | 15 +++- .../Svg/DefinitionUtils/DefinitionGroup.cs | 27 +++++++ .../Svg/DefinitionUtils/LinePattern.cs | 74 ++++++++++++++++++ .../Svg/DefinitionUtils/PatternItem.cs | 39 ++++++++++ .../Svg/LineMarkerHelper.cs | 4 +- .../Utils/SvgDrawingWriter.cs | 15 ++-- src/EPPlus/BorderStyleBase.cs | 76 +++++++++++++++++++ .../StyleCollectors/BorderDrawingAlt.cs | 10 +++ .../StyleCollectors/BorderItemDrawing.cs | 23 ++++++ .../StyleCollectors/StyleColorDrawing.cs | 43 +++++++++++ .../StyleCollectors/StyleDrawing.cs | 72 ++++++++++++++++++ 11 files changed, 390 insertions(+), 8 deletions(-) create mode 100644 src/EPPlus.Export.ImageRenderer/Svg/DefinitionUtils/DefinitionGroup.cs create mode 100644 src/EPPlus.Export.ImageRenderer/Svg/DefinitionUtils/LinePattern.cs create mode 100644 src/EPPlus.Export.ImageRenderer/Svg/DefinitionUtils/PatternItem.cs create mode 100644 src/EPPlus/BorderStyleBase.cs create mode 100644 src/EPPlus/Export/HtmlExport/StyleCollectors/BorderDrawingAlt.cs create mode 100644 src/EPPlus/Export/HtmlExport/StyleCollectors/BorderItemDrawing.cs create mode 100644 src/EPPlus/Export/HtmlExport/StyleCollectors/StyleColorDrawing.cs create mode 100644 src/EPPlus/Export/HtmlExport/StyleCollectors/StyleDrawing.cs diff --git a/src/EPPlus.Export.ImageRenderer/RenderItems/SvgRenderLineItem.cs b/src/EPPlus.Export.ImageRenderer/RenderItems/SvgRenderLineItem.cs index 8f2ef1156..2cee0dbb9 100644 --- a/src/EPPlus.Export.ImageRenderer/RenderItems/SvgRenderLineItem.cs +++ b/src/EPPlus.Export.ImageRenderer/RenderItems/SvgRenderLineItem.cs @@ -142,13 +142,26 @@ private void WriteThickThin(StringBuilder sb, string name, double gapOffset) sb.Append($""); } + internal string Suffix = "px"; + private void RenderLineItem(StringBuilder sb, double? borderWidth, string color, string filter) { - sb.AppendFormat(" Items; + + public override RenderItemType Type => RenderItemType.Group; + + public override void Render(StringBuilder sb) + { + sb.Append(""); + + foreach(var item in Items) + { + item.Render(sb); + } + + sb.Append(""); + } + } +} diff --git a/src/EPPlus.Export.ImageRenderer/Svg/DefinitionUtils/LinePattern.cs b/src/EPPlus.Export.ImageRenderer/Svg/DefinitionUtils/LinePattern.cs new file mode 100644 index 000000000..d739795a6 --- /dev/null +++ b/src/EPPlus.Export.ImageRenderer/Svg/DefinitionUtils/LinePattern.cs @@ -0,0 +1,74 @@ +using EPPlusImageRenderer; +using EPPlusImageRenderer.RenderItems; +using EPPlusImageRenderer.Svg; +using OfficeOpenXml.Drawing; +using OfficeOpenXml.Drawing.Style; +using OfficeOpenXml.Utils; +using System; +using System.Collections.Generic; +using System.Drawing; +using System.Linq; +using System.Text; + +namespace EPPlus.Export.ImageRenderer.Svg.DefinitionUtils +{ + enum LinePatternType + { + Vertical, + Horizontal + } + + internal class LinePattern : PatternItem + { + LinePatternType type; + + internal SvgRenderLineItem LineItem; + + public LinePattern(DrawingBase baseRend, string id, LinePatternType linesType) : base(baseRend, id) + { + type = linesType; + LineItem = new SvgRenderLineItem(baseRend, Bounds); + + LineItem.BorderWidth = 2; + LineItem.Suffix = "%"; + + LineItem.BorderColor = "#" + Color.DarkGray.To6CharHexString(); + + switch (type) + { + case LinePatternType.Vertical: + LineItem.X1 = 0; LineItem.X2 = 0; LineItem.Y1 = 0; LineItem.Y2 = 100; + break; + case LinePatternType.Horizontal: + LineItem.X1 = 0; LineItem.X2 = 100; LineItem.Y1 = 0; LineItem.Y2 = 0; + break; + } + + SetNumberOfLines(6); + } + + public override RenderItemType Type => RenderItemType.Reference; + + /// + /// Sets number of lines via the width or height percent + /// + internal void SetNumberOfLines(int numberOfLines) + { + switch (type) + { + case LinePatternType.Vertical: + widthPercent = (double)numberOfLines / 100d; + break; + case LinePatternType.Horizontal: + heightPercent = (double)numberOfLines / 100d; + break; + } + } + + public override void Render(StringBuilder sb) + { + _items.Add(LineItem); + base.Render(sb); + } + } +} diff --git a/src/EPPlus.Export.ImageRenderer/Svg/DefinitionUtils/PatternItem.cs b/src/EPPlus.Export.ImageRenderer/Svg/DefinitionUtils/PatternItem.cs new file mode 100644 index 000000000..df7b3142a --- /dev/null +++ b/src/EPPlus.Export.ImageRenderer/Svg/DefinitionUtils/PatternItem.cs @@ -0,0 +1,39 @@ +using EPPlus.Fonts.OpenType.Utils; +using EPPlusImageRenderer; +using EPPlusImageRenderer.RenderItems; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; + +namespace EPPlus.Export.ImageRenderer.Svg.DefinitionUtils +{ + internal class PatternItem : RenderItem + { + string _id = null; + + protected List _items = new List(); + + public PatternItem(DrawingBase baseRend, string id) : base(baseRend) + { + _id = id; + } + + protected double heightPercent = 100d; + protected double widthPercent = 100d; + + public override RenderItemType Type => RenderItemType.Group; + + public override void Render(StringBuilder sb) + { + sb.Append($""); + + foreach (var item in _items) + { + item.Render(sb); + } + + sb.Append(""); + } + } +} diff --git a/src/EPPlus.Export.ImageRenderer/Svg/LineMarkerHelper.cs b/src/EPPlus.Export.ImageRenderer/Svg/LineMarkerHelper.cs index 4542fdf01..b1a1d2bcb 100644 --- a/src/EPPlus.Export.ImageRenderer/Svg/LineMarkerHelper.cs +++ b/src/EPPlus.Export.ImageRenderer/Svg/LineMarkerHelper.cs @@ -11,7 +11,7 @@ namespace EPPlus.Export.ImageRenderer.Svg { internal class LineMarkerHelper { - internal static RenderItem GetMarkerItem(SvgChart sc, ExcelLineChartSerie ls, double x, double y, bool isLegend) + internal static SvgRenderItem GetMarkerItem(SvgChart sc, ExcelLineChartSerie ls, double x, double y, bool isLegend) { SvgRenderItem item; var m = ls.Marker; @@ -156,7 +156,7 @@ internal static RenderItem GetMarkerItem(SvgChart sc, ExcelLineChartSerie ls, do } return item; } - internal static RenderItem GetMarkerBackground(SvgChart sc, ExcelLineChartSerie ls, double x, double y, bool isLegend) + internal static SvgRenderItem GetMarkerBackground(SvgChart sc, ExcelLineChartSerie ls, double x, double y, bool isLegend) { SvgRenderItem item; var m = ls.Marker; diff --git a/src/EPPlus.Export.ImageRenderer/Utils/SvgDrawingWriter.cs b/src/EPPlus.Export.ImageRenderer/Utils/SvgDrawingWriter.cs index 3d53145a5..7e7891fe3 100644 --- a/src/EPPlus.Export.ImageRenderer/Utils/SvgDrawingWriter.cs +++ b/src/EPPlus.Export.ImageRenderer/Utils/SvgDrawingWriter.cs @@ -41,13 +41,13 @@ public SvgDrawingWriter(DrawingBase svgDrawing) var wb = svgDrawing.Drawing._drawings.Worksheet.Workbook; _theme = wb.ThemeManager.GetOrCreateTheme(); } - internal void WriteSvgDefs(StringBuilder sb, List renderItems) + internal void WriteSvgDefs(StringBuilder sb, List renderItems) { var defSb = new StringBuilder(); var hs = new HashSet(); var ix = 1; - foreach (RenderItem item in renderItems) + foreach (SvgRenderItem item in renderItems) { string filter = ""; if (item.GradientFill != null) @@ -133,7 +133,7 @@ private static string GetFilterName(int ix) return $"item{ix}Filter"; } - private string WriteBlip(string namePrefix, StringBuilder defSb, HashSet hs, RenderItem item, ref string filter) + private string WriteBlip(string namePrefix, StringBuilder defSb, HashSet hs, SvgRenderItem item, ref string filter) { //, item.BlipFill, item.FillColorSource var name = $"{namePrefix}"; @@ -444,7 +444,12 @@ private string GetScaling(DrawGradientFill gradientFill) private void SetStopColors(StringBuilder defSb, DrawGradientFill gradientFill, PathFillMode fillMode) { int ix = 0; - foreach (var c in gradientFill.Colors) + + //Svg requires starting at 0 and moving towards 100% Excel sometimes starts at 100 + //Sort to get around that + var sortedGradientColors = gradientFill.Colors.OrderBy(x => x.Position); + + foreach (var c in sortedGradientColors) { var color = ColorUtils.GetAdjustedColor(fillMode, c.Color); // TODO: check if ix should be increased...? @@ -482,7 +487,7 @@ private string GetXy(double? angle) x2 = 1D - Math.Sin(MathHelper.Radians(angle.Value - 180)); } - return $" x1=\"{x1.ToString("0.00", CultureInfo.InvariantCulture)}\" x2=\"{x2.ToString("0.00", CultureInfo.InvariantCulture)}\" y1=\"{y1.ToString("0.00", CultureInfo.InvariantCulture)}\" y2=\"{y2.ToString("0.00", CultureInfo.InvariantCulture)}\""; + return $" x1=\"{(x1).ToString("0.00%", CultureInfo.InvariantCulture)}\" x2=\"{(x2).ToString("0.00%", CultureInfo.InvariantCulture)}\" y1=\"{y1.ToString("0.00%", CultureInfo.InvariantCulture)}\" y2=\"{y2.ToString("0.00%", CultureInfo.InvariantCulture)}\""; } return ""; } diff --git a/src/EPPlus/BorderStyleBase.cs b/src/EPPlus/BorderStyleBase.cs new file mode 100644 index 000000000..9b0d577c6 --- /dev/null +++ b/src/EPPlus/BorderStyleBase.cs @@ -0,0 +1,76 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; + +//namespace OfficeOpenXml +//{ +// public enum BorderStyleBase +// { +// /// +// /// No border style +// /// +// None, +// /// +// /// Hairline +// /// +// Hair, + +// /// +// /// Dashed +// /// +// Dashed, //dash + +// /// +// /// Dash Dot +// /// +// DashDot, //DashDot + +// /// +// /// Dotted +// /// +// Dotted, //Dot + +// /// +// /// Single line, medium thickness +// /// +// Medium, //solid + + + + +// /// +// /// Thin single line +// /// +// Thin, + +// /// +// /// Dash Dot Dot +// /// +// DashDotDot, +// /// +// /// Dash Dot Dot, medium thickness +// /// +// MediumDashDotDot, +// /// +// /// Dashed, medium thickness +// /// +// MediumDashed, +// /// +// /// Dash Dot, medium thickness +// /// +// MediumDashDot, +// /// +// /// Single line, Thick +// /// +// Thick, +// /// +// /// Double line +// /// +// Double, +// /// +// /// Slanted Dash Dot +// /// +// SlantDashDot +// } +//} diff --git a/src/EPPlus/Export/HtmlExport/StyleCollectors/BorderDrawingAlt.cs b/src/EPPlus/Export/HtmlExport/StyleCollectors/BorderDrawingAlt.cs new file mode 100644 index 000000000..90478306a --- /dev/null +++ b/src/EPPlus/Export/HtmlExport/StyleCollectors/BorderDrawingAlt.cs @@ -0,0 +1,10 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +namespace OfficeOpenXml.Export.HtmlExport.StyleCollectors +{ + internal class BorderDrawingAlt + { + } +} diff --git a/src/EPPlus/Export/HtmlExport/StyleCollectors/BorderItemDrawing.cs b/src/EPPlus/Export/HtmlExport/StyleCollectors/BorderItemDrawing.cs new file mode 100644 index 000000000..8e7ef14f7 --- /dev/null +++ b/src/EPPlus/Export/HtmlExport/StyleCollectors/BorderItemDrawing.cs @@ -0,0 +1,23 @@ +using OfficeOpenXml.Drawing; +using OfficeOpenXml.Export.HtmlExport.StyleCollectors.StyleContracts; +using OfficeOpenXml.Style; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; + +namespace OfficeOpenXml.Export.HtmlExport.StyleCollectors +{ + //internal class BorderItemDrawing : IBorderItem + //{ + // public BorderItemDrawing(ExcelDrawingBorder border) + // { + // var fill = new FillDrawingBasic(border.Fill); + // //border.Fill.Color + // } + + // ExcelBorderStyle IBorderItem.Style => throw new NotImplementedException(); + + // IStyleColor IBorderItem.Color => throw new NotImplementedException(); + //} +} diff --git a/src/EPPlus/Export/HtmlExport/StyleCollectors/StyleColorDrawing.cs b/src/EPPlus/Export/HtmlExport/StyleCollectors/StyleColorDrawing.cs new file mode 100644 index 000000000..68463da7e --- /dev/null +++ b/src/EPPlus/Export/HtmlExport/StyleCollectors/StyleColorDrawing.cs @@ -0,0 +1,43 @@ +using OfficeOpenXml.Drawing; +using OfficeOpenXml.Drawing.Theme; +using OfficeOpenXml.Export.HtmlExport.StyleCollectors.StyleContracts; +using OfficeOpenXml.Style; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; + +//namespace OfficeOpenXml.Export.HtmlExport.StyleCollectors +//{ +// internal class StyleColorDrawing : IStyleColor +// { + // public StyleColorDrawing() + // { + // } + + // ExcelColor _color; + + // //TODO: Is this correct? + // public bool Exists { get { return true; } } + + // public bool Auto { get { return _color.Auto; } } + + // public int Indexed { get { return _color.Indexed; } } + + // public double Tint { get { return (double)_color.Tint; } } + + // public eThemeSchemeColor? Theme { get { return _color.Theme; } } + + // public string Rgb { get { return _color.Rgb; } } + + // public bool AreColorEqual(IStyleColor color) + // { + // return StyleColorShared.AreColorEqual(this, color); + // } + + // public string GetColor(ExcelTheme theme) + // { + // return StyleColorShared.GetColor(this, theme); + // } + //} +//} diff --git a/src/EPPlus/Export/HtmlExport/StyleCollectors/StyleDrawing.cs b/src/EPPlus/Export/HtmlExport/StyleCollectors/StyleDrawing.cs new file mode 100644 index 000000000..4cbc1dcf7 --- /dev/null +++ b/src/EPPlus/Export/HtmlExport/StyleCollectors/StyleDrawing.cs @@ -0,0 +1,72 @@ +using OfficeOpenXml.Drawing.Chart.Style; +using OfficeOpenXml.Export.HtmlExport.StyleCollectors.StyleContracts; +using OfficeOpenXml.Style.Dxf; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; + +//namespace OfficeOpenXml.Export.HtmlExport.StyleCollectors +//{ +// internal class StyleDrawing : IStyleExport +// { +// //ExcelChartStyleEntry _style; + +// //public bool HasStyle +// //{ +// // get { return _style.HasFill; } +// //} + +// //public string StyleKey { get; } = null; /*{ get { return _style.Id; } }*/ + +// //public IFill Fill { get; } = null; +// //public IFont Font { get; } = null; +// //public IBorder Border { get; } = null; +// //public INumberFormat NumberFormat { get; } = null; + +// ////Charts never have a checkbox. Break out of baseClass? +// //bool IStyleExport.CheckBox => false; + +// //public int StyleId; + +// //public StyleDrawing(ExcelChartStyleEntry style, int styleId) +// //{ +// // _style = style; +// // StyleId = styleId; + +// // if (style.HasFill) +// // { +// // Fill = new FillDrawing(style.Fill); +// // } +// // if (style.HasTextBody) +// // { +// // //not implemented yet +// // if (style.HasRichText) +// // { +// // if(style.HasTextRun) +// // { +// // //style.FontReference +// // } +// // } +// // //Font = new FontDxf(style.Font); +// // } +// // if(style.FontReference != null) +// // { +// // //not implemented yet +// // } +// // if (style.HasBorder) +// // { +// // //style.Border. +// // //var lineStyle = style.Border.LineStyle; +// // //Border.Top.Style = Style.ExcelBorderStyle. +// // //Border = new FillDrawingBasic(style.Border.Fill); +// // } +// // //if (style. != null && style.NumberFormat.HasValue) +// // //{ +// // // NumberFormat = new NumberFormatDxf(style.NumberFormat, styleId); +// // //} + +// // //CheckBox = false; +// //} +// } +//} From f3d704648de0fe0ce9e17a0dfc5976712000a340 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jan=20K=C3=A4llman?= Date: Tue, 7 Apr 2026 12:17:49 +0200 Subject: [PATCH 140/151] WIP:Fix for Legends and Bar Chart rendering --- .../RenderItems/SvgItem/SvgTextBox.cs | 2 +- .../Chart/Axis/CategoryAxisScaleCalculator.cs | 2 +- .../BarColumnChartTypeDrawer.cs | 14 +++++++------- .../Svg/Chart/SvgChartAxis.cs | 2 +- .../Svg/Chart/SvgChartLegend.cs | 18 +++++++++--------- 5 files changed, 19 insertions(+), 19 deletions(-) diff --git a/src/EPPlus.Export.ImageRenderer/RenderItems/SvgItem/SvgTextBox.cs b/src/EPPlus.Export.ImageRenderer/RenderItems/SvgItem/SvgTextBox.cs index 2704555a7..68b2733a6 100644 --- a/src/EPPlus.Export.ImageRenderer/RenderItems/SvgItem/SvgTextBox.cs +++ b/src/EPPlus.Export.ImageRenderer/RenderItems/SvgItem/SvgTextBox.cs @@ -135,7 +135,7 @@ internal double GetActualWidth() /// internal double GetActualRight() { - return Left+GetActualRight(); + return Left+GetActualWidth(); } /// /// Gets the actual height of the rotated textbox. diff --git a/src/EPPlus.Export.ImageRenderer/Svg/Chart/Axis/CategoryAxisScaleCalculator.cs b/src/EPPlus.Export.ImageRenderer/Svg/Chart/Axis/CategoryAxisScaleCalculator.cs index e7d8bcc1f..da0381f81 100644 --- a/src/EPPlus.Export.ImageRenderer/Svg/Chart/Axis/CategoryAxisScaleCalculator.cs +++ b/src/EPPlus.Export.ImageRenderer/Svg/Chart/Axis/CategoryAxisScaleCalculator.cs @@ -42,7 +42,7 @@ internal static AxisScale CalculateByWidth(ref List values, ITextMeasure MajorInterval = interval, MinorInterval = 1, Min = 1, - Max = displayValues.Count, + Max = values.Count, TextOrientation = eTextOrientation.Horizontal }; } diff --git a/src/EPPlus.Export.ImageRenderer/Svg/Chart/ChartTypeDrawers/BarColumnChartTypeDrawer.cs b/src/EPPlus.Export.ImageRenderer/Svg/Chart/ChartTypeDrawers/BarColumnChartTypeDrawer.cs index bfddbaa6a..5eab01670 100644 --- a/src/EPPlus.Export.ImageRenderer/Svg/Chart/ChartTypeDrawers/BarColumnChartTypeDrawer.cs +++ b/src/EPPlus.Export.ImageRenderer/Svg/Chart/ChartTypeDrawers/BarColumnChartTypeDrawer.cs @@ -135,14 +135,14 @@ private void AddColumn(ExcelBarChart chartType, ExcelBarChartSerie serie, List _maxHeight) { @@ -207,7 +207,7 @@ private SvgRenderRectItem GetLegendRectangleAndEntrySize(SvgChart sc, ExcelChart } else { - rect.Top = sc.Title.Rectangle.Height + 8 + 8; //Height+Margin Top and Bottom Title + rect.Top = sc.Title.Rectangle.Height + 8 + 8; //Height + Margin Top and Bottom Title } } break; @@ -218,7 +218,7 @@ private SvgRenderRectItem GetLegendRectangleAndEntrySize(SvgChart sc, ExcelChart private double GetIconLenght(SvgChart sc, double heighestText) { - return sc.Chart.IsTypeLine() ? LineLength : Math.Max(MinBarLength, heighestText * 0.6); + return sc.Chart.IsTypeLine() ? LineLength : Math.Max(MinBarLength, heighestText * 0.4); } internal void SetLegend(SvgChart sc, double entryWidth, double entryHeight) @@ -343,7 +343,7 @@ private void SetBarLegend(SvgChart sc, int index, SvgLegendSerie pSls, eLegendPo { prevTm = _seriesHeadersMeasure[index - 1]; } - var si = GetBarSeriesIcon(sc, bs, prevTm, tm, pSls, entryHeight, entryWidth); + var si = GetBarSeriesIcon(sc, bs, prevTm, tm, pSls, entryWidth, entryHeight); sls.SeriesIcon = si; var tbLeft = si.Right + MarginExtra; @@ -393,10 +393,10 @@ private SvgRenderRectItem GetBarSeriesIcon(SvgChart sc, ExcelChartStandardSerie var item = new SvgRenderRectItem(sc, Rectangle.Bounds); item.SetDrawingPropertiesFill(cStandardSerie.Fill, sc.Chart.StyleManager.Style.SeriesLine.FillReference.Color); item.SetDrawingPropertiesBorder(cStandardSerie.Border, sc.Chart.StyleManager.Style.SeriesLine.BorderReference.Color, cStandardSerie.Border.Fill.Style != eFillStyle.NoFill, 0.75); - var iconHeight = GetIconLenght(sc, entryHeight); + var iconHeight = GetIconLenght(sc, tm.Height); var icon = pSls?.SeriesIcon as SvgRenderRectItem; - GetItemPosition(sc, pTm, tm, pSls, entryWidth, entryHeight, icon.Left, icon.Top, out double x, out double y); + GetItemPosition(sc, pTm, tm, pSls, entryWidth, entryHeight, icon?.Left ?? 0D, icon?.Top ?? 0D, out double x, out double y); item.LineCap = eLineCap.Round; item.Left = x; @@ -444,11 +444,11 @@ private double GetItemPosition(SvgChart sc, TextMeasurement pTm, TextMeasurement { if (pSls == null) { - y = TopMargin + tm.Height / 2 + MarginExtra; + y = TopMargin;// + tm.Height / 2 + MarginExtra; } else { - y = pSls.Textbox.Bounds.Bottom + pTm.Height / 2 + tm.Height / 2; + y = pSls.Textbox.Bounds.Bottom + MiddleMargin; } x = LeftMargin; From a9d864ad96358d163169877ae4a7b16ee797640d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jan=20K=C3=A4llman?= Date: Tue, 7 Apr 2026 16:33:41 +0200 Subject: [PATCH 141/151] WIP: Fixes cap property. Fixes axis positions. --- .../RenderItems/SvgItem/SvgTextBox.cs | 21 ++++---- .../Svg/Chart/SvgChart.cs | 16 +++---- .../Svg/Chart/SvgChartPlotarea.cs | 19 +++++--- .../Svg/Chart/SvgChartTitle.cs | 48 ++++++++----------- src/EPPlus/Drawing/Chart/ExcelChartTitle.cs | 38 +++++++++++++++ .../Style/Text/ExcelParagraphTextRunBase.cs | 4 +- src/EPPlus/Style/ExcelTextFont.cs | 32 +++++++++++-- src/EPPlus/Style/RichText/ExcelParagraph.cs | 24 +++++++++- .../RichText/ExcelParagraphCollection.cs | 30 ++++++++++++ 9 files changed, 169 insertions(+), 63 deletions(-) diff --git a/src/EPPlus.Export.ImageRenderer/RenderItems/SvgItem/SvgTextBox.cs b/src/EPPlus.Export.ImageRenderer/RenderItems/SvgItem/SvgTextBox.cs index 68b2733a6..cf446d5d7 100644 --- a/src/EPPlus.Export.ImageRenderer/RenderItems/SvgItem/SvgTextBox.cs +++ b/src/EPPlus.Export.ImageRenderer/RenderItems/SvgItem/SvgTextBox.cs @@ -30,7 +30,7 @@ private void Init(DrawingBase renderer, BoundingBox parent, double maxWidth, dou _rectangle = new SvgRenderRectItem(DrawingRenderer, Parent); TextBody = new SvgTextBodyItem(renderer, Rectangle.Bounds, true); TextBody.MaxWidth = maxWidth; - TextBody.MaxHeight = maxHeight; + TextBody.MaxHeight = maxHeight; } internal SvgTextBox(DrawingBase renderer, BoundingBox parent, double maxWidth, double maxHeight) : base(renderer) @@ -58,22 +58,25 @@ public double Left { get { - return TextBody.Bounds.Left - LeftMargin; + return Rectangle.Bounds.Left; //TextBody.Bounds.Left - LeftMargin; + } set { - TextBody.Bounds.Left = value + LeftMargin; + //TextBody.Bounds.Left = value + LeftMargin; + Rectangle.Bounds.Left = value; } } public double Top { get { - return TextBody.Bounds.Top - TopMargin; + return Rectangle.Bounds.Top; //TextBody.Bounds.Top - TopMargin; } set { - TextBody.Bounds.Top = value + TopMargin; + //TextBody.Bounds.Top = value + TopMargin; + Rectangle.Bounds.Top = value; } } public double Width @@ -185,11 +188,6 @@ internal override void AppendRenderItems(List renderItems) { var rect = Rectangle; - //if (rect.FillColor == null) - //{ - // rect.FillColor = "transparent"; - //} - SvgGroupItem groupItem; if (Rotation == 0) { @@ -215,7 +213,8 @@ internal override void AppendRenderItems(List renderItems) { rect.Bounds.Left = -rect.Bounds.Width; } - + rect.Bounds.Left = 0; + rect.Bounds.Top = 0; renderItems.Add(titleItem); renderItems.Add(rect); diff --git a/src/EPPlus.Export.ImageRenderer/Svg/Chart/SvgChart.cs b/src/EPPlus.Export.ImageRenderer/Svg/Chart/SvgChart.cs index b99a0e6cf..5ab8ee760 100644 --- a/src/EPPlus.Export.ImageRenderer/Svg/Chart/SvgChart.cs +++ b/src/EPPlus.Export.ImageRenderer/Svg/Chart/SvgChart.cs @@ -132,7 +132,7 @@ private void PlaceHorizontalAxis(SvgChart sc, SvgChartAxis horizontalAxis) { horizontalAxis.Title.Rectangle.Height = sc.Bounds.Height / 4; horizontalAxis.Title.Rectangle.Width = horizontalAxis.Rectangle?.Width ?? sc.Plotarea.Rectangle.Width; - horizontalAxis.Title.InitTextBox(); + //horizontalAxis.Title.InitTextBox(); if (horizontalAxis.Axis.AxisPosition == eAxisPosition.Bottom) { horizontalAxis.Title.TextBox.Top = horizontalAxis.Rectangle.Bottom; @@ -167,22 +167,22 @@ private void PlaceVerticalAxis(SvgChart sc, SvgChartAxis verticalAxis) if (verticalAxis.Title != null) { - verticalAxis.Title.Rectangle.Height = Plotarea.Rectangle.Height; - verticalAxis.Title.Rectangle.Width = sc.Bounds.Width / 4; - verticalAxis.Title.InitTextBox(); + //verticalAxis.Title.Rectangle.Height = Plotarea.Rectangle.Height; + //verticalAxis.Title.Rectangle.Width = sc.Bounds.Width / 4; + //verticalAxis.Title.InitTextBox(); var sinRot = Math.Abs(Math.Sin(MathHelper.Radians(verticalAxis.Title.TextBox.Rotation))); var cosRot = Math.Abs(Math.Cos(MathHelper.Radians(verticalAxis.Title.TextBox.Rotation))); - verticalAxis.Title.TextBox.Top = Plotarea.Rectangle.Top + (Plotarea.Rectangle.Height / 2);// + ((verticalAxis.Title.TextBox.Height * cosRot + verticalAxis.Title.TextBox.Width * sinRot) / 2); + verticalAxis.Title.TextBox.Top = Plotarea.Rectangle.Top + (Plotarea.Rectangle.Height / 2) + ((verticalAxis.Title.TextBox.Height * cosRot + verticalAxis.Title.TextBox.Width * sinRot) / 2); if (verticalAxis.Axis.AxisPosition == eAxisPosition.Left) { if (verticalAxis.Rectangle == null) { - verticalAxis.Title.TextBox.Left = sc.ChartArea.LeftMargin; + verticalAxis.Title.TextBox.Left = sc.Plotarea.Rectangle.Left - verticalAxis.Title.TextBox.GetActualWidth() - 1.5; } else { - verticalAxis.Title.TextBox.Left = verticalAxis.Rectangle.Left - verticalAxis.Title.TextBox.Width; + verticalAxis.Title.TextBox.Left = verticalAxis.Rectangle.Left - verticalAxis.Title.TextBox.GetActualWidth() - 1.5; } } else @@ -196,7 +196,7 @@ private void PlaceVerticalAxis(SvgChart sc, SvgChartAxis verticalAxis) verticalAxis.Title.TextBox.Left = verticalAxis.Rectangle.Right; } } - verticalAxis.Title.TextBox.TextAnchor = eTextAnchor.Middle; + //verticalAxis.Title.TextBox.TextAnchor = eTextAnchor.Middle; } } diff --git a/src/EPPlus.Export.ImageRenderer/Svg/Chart/SvgChartPlotarea.cs b/src/EPPlus.Export.ImageRenderer/Svg/Chart/SvgChartPlotarea.cs index 99a2291ad..57f98994b 100644 --- a/src/EPPlus.Export.ImageRenderer/Svg/Chart/SvgChartPlotarea.cs +++ b/src/EPPlus.Export.ImageRenderer/Svg/Chart/SvgChartPlotarea.cs @@ -77,21 +77,28 @@ private double GetPlotAreaWidth(SvgChart sc, SvgRenderRectItem rect) } else { - var rightWidth = rightAxis.Title?.TextBox.GetActualWidth() ?? 0D + rightAxis.Rectangle?.Width ?? 0D; + var rightWidth = (rightAxis.Title?.TextBox.GetActualWidth() ?? 0D) + (rightAxis.Rectangle?.Width ?? 0D); return left - rightWidth - rect.Left; } } private double GetPlotAreaLeft(SvgChart sc) { - var leftAxis = GetAxisByPosition(sc, eAxisPosition.Left); - if (leftAxis == null || (leftAxis.Rectangle==null && leftAxis.Title?.Rectangle==null)) + var left = LeftMargin; + if(sc.Chart.Legend?.Position == eLegendPosition.Left) { - return sc.Chart.Legend?.Position == eLegendPosition.Left ? sc.Legend.Bounds.Right + LeftMargin : LeftMargin; + left += sc.Legend.Bounds.Width + sc.Legend.RightMargin; } - else + + var leftAxis = GetAxisByPosition(sc, eAxisPosition.Left); + if (leftAxis != null) { - return leftAxis.Rectangle == null ? leftAxis.Title.TextBox.GetActualWidth(): leftAxis.Rectangle.GlobalRight; + if(leftAxis.Title!=null) + { + left += leftAxis.Title.TextBox.GetActualWidth(); + } + left += leftAxis.Rectangle.Width + 1.5; } + return left; } private double GetPlotAreaTop(SvgChart sc) diff --git a/src/EPPlus.Export.ImageRenderer/Svg/Chart/SvgChartTitle.cs b/src/EPPlus.Export.ImageRenderer/Svg/Chart/SvgChartTitle.cs index 4e9d7904b..3aeb945c9 100644 --- a/src/EPPlus.Export.ImageRenderer/Svg/Chart/SvgChartTitle.cs +++ b/src/EPPlus.Export.ImageRenderer/Svg/Chart/SvgChartTitle.cs @@ -59,52 +59,43 @@ internal SvgChartTitle(SvgChart sc, ExcelChartTitleStandard t, string defaultTex } else { - if (string.IsNullOrEmpty(t.Text) == false) + if (string.IsNullOrEmpty(t.DisplayedText) == false) { - _titleText = t.Text; + _titleText = t.DisplayedText; } else { - _titleText = defaultText; + _titleText = t.Font.GetCapitalizedText(defaultText); } } - + if (t.Layout.HasLayout) //Only for the main chart title, axis titles don't support manual layout in Excel. { - Rectangle = GetRectFromManualLayout(sc, t.Layout); - var top = Rectangle.Top; - var left = Rectangle.Left; - Rectangle.Width = (float)maxWidth; - Rectangle.Height = (float)maxHeight; - InitTextBox(); - TextBox.Top = top; - TextBox.Left = left; + InitTextBox(maxWidth, maxHeight); + var mr = GetRectFromManualLayout(sc, t.Layout); + TextBox.Top = mr.Top; + TextBox.Left = mr.Left; } else { - Rectangle = new SvgRenderRectItem(sc, sc.Bounds); - if (axis==null) + if (axis == null) { - Rectangle.Width = (float)maxWidth; - Rectangle.Height = (float)maxHeight; - - InitTextBox(); + InitTextBox(maxWidth, maxHeight); TextBox.Top = (float)6; //6 point for the chart title standard offset. TextBox.Left = (float)(sc.Bounds.Width - TextBox.Width) / 2; } else { var isVertical = axis.Axis.IsVertical; - Rectangle.Width = sc.Bounds.Width * (isVertical ? 0.2 : 0.8); //Max Width. - Rectangle.Height = sc.Bounds.Height * (isVertical ? 0.8 : 0.2); //Max Height. + maxWidth = sc.Bounds.Width * (isVertical ? 0.2 : 0.8); //Max Width. + maxHeight = sc.Bounds.Height * (isVertical ? 0.8 : 0.2); //Max Height. - InitTextBox(); - Rectangle = (SvgRenderRectItem)TextBox.Rectangle; + InitTextBox(maxWidth, maxHeight); SetAxisTitleRect(sc, axis); } } - + Bounds = Rectangle.Bounds; Rectangle.SetDrawingPropertiesFill(t.Fill, sc.Chart.StyleManager.Style.Title.FillReference.Color); Rectangle.SetDrawingPropertiesBorder(t.Border, sc.Chart.StyleManager.Style.Title.BorderReference.Color, t.Border.Fill.Style != eFillStyle.NoFill, 0.75); } @@ -152,9 +143,9 @@ private double GetHorizontalLeft(SvgChart sc) private static string GetDefaultChartTitleText(SvgChart sc, ExcelChartTitleStandard t, string defaultText) { - if (string.IsNullOrEmpty(t.Text) == false) + if (string.IsNullOrEmpty(t.DisplayedText) == false) { - defaultText = t.Text; + defaultText = t.DisplayedText; } else if (sc.Chart.PlotArea.ChartTypes.Count == 1 && sc.Chart.Series.Count == 1) { @@ -183,9 +174,9 @@ private static string GetDefaultChartTitleText(SvgChart sc, ExcelChartTitleStand return defaultText; } - internal void InitTextBox() + internal void InitTextBox(double maxWidth, double maxHeight) { - TextBox = new SvgTextBox(_svgChart, _svgChart.ChartArea.Bounds, Rectangle.Width, Rectangle.Height); + TextBox = new SvgTextBox(_svgChart, _svgChart.ChartArea.Bounds, maxWidth, maxHeight); if(_title.Rotation != 0) { TextBox.Rotation = _title.Rotation; @@ -196,9 +187,8 @@ internal void InitTextBox() } else { - var text = string.IsNullOrEmpty(_title.Text) ? _titleText : _title.Text; var p = _title.DefaultTextBody.Paragraphs.FirstOrDefault(); - TextBox.ImportParagraph(p, 0, text); + TextBox.ImportParagraph(p, 0, _titleText); } TextBox.LeftMargin = LeftMargin; diff --git a/src/EPPlus/Drawing/Chart/ExcelChartTitle.cs b/src/EPPlus/Drawing/Chart/ExcelChartTitle.cs index 6491d4623..1d41c5b70 100644 --- a/src/EPPlus/Drawing/Chart/ExcelChartTitle.cs +++ b/src/EPPlus/Drawing/Chart/ExcelChartTitle.cs @@ -443,6 +443,44 @@ public override string Text } } /// + /// The text adjusted for the Capitalization property. + /// + public string DisplayedText + { + get + { + if (LinkedCell == null) + { + return RichText.DisplayedText; + } + else + { + if (LinkedCell.IsSingleCell == false) + { + string combinedString = ""; + string separator = " "; + foreach (var address in LinkedCell) + { + combinedString += address.Text + separator; + } + } + return LinkedCell.Text; + } + } + set + { + if (RichText == null) + { + LinkedCell = null; + CreateRichText(); + } + var applyStyle = (RichText.Count == 0); + RichText.Text = value; + _font = null; + if (applyStyle) _chart.ApplyStyleOnPart(this, _chart.StyleManager?.Style?.Title, true); + } + } + /// /// A reference to a cell used as the title text /// public ExcelRangeBase LinkedCell diff --git a/src/EPPlus/Drawing/Style/Text/ExcelParagraphTextRunBase.cs b/src/EPPlus/Drawing/Style/Text/ExcelParagraphTextRunBase.cs index 63194313d..4326858c7 100644 --- a/src/EPPlus/Drawing/Style/Text/ExcelParagraphTextRunBase.cs +++ b/src/EPPlus/Drawing/Style/Text/ExcelParagraphTextRunBase.cs @@ -409,9 +409,9 @@ public eTextCapsType Capitalization get { var v = GetXmlNodeString(_capPath); - if(v==null) + if(string.IsNullOrEmpty(v)) { - return _dtr.Capitalization; + return _dtr?.Capitalization ?? eTextCapsType.None; } else { diff --git a/src/EPPlus/Style/ExcelTextFont.cs b/src/EPPlus/Style/ExcelTextFont.cs index 13ca12934..638979c01 100644 --- a/src/EPPlus/Style/ExcelTextFont.cs +++ b/src/EPPlus/Style/ExcelTextFont.cs @@ -13,12 +13,14 @@ Date Author Change using OfficeOpenXml.Drawing; using OfficeOpenXml.Drawing.Interfaces; using OfficeOpenXml.Drawing.Style.Font; +using OfficeOpenXml.FormulaParsing.Excel.Functions.Text; using OfficeOpenXml.Interfaces.Drawing.Text; +using OfficeOpenXml.Utils; using OfficeOpenXml.Utils.EnumUtils; using System; using System.Drawing; +using System.Globalization; using System.Linq; -using OfficeOpenXml.Utils; using System.Xml; namespace OfficeOpenXml.Style @@ -330,7 +332,7 @@ public override eTextCapsType Capitalization { get { - switch (_xml.GetXmlNodeString($"{_path}/@cap")) + switch (_xml.GetXmlNodeString($"@cap")) { case "all": return eTextCapsType.All; @@ -343,7 +345,7 @@ public override eTextCapsType Capitalization set { CreateTopNode(); - _xml.SetXmlNodeString($"{_path}/@cap", value.ToEnumString()); + _xml.SetXmlNodeString($"@cap", value.ToEnumString()); } } @@ -354,12 +356,12 @@ public override double Baseline { get { - return _xml.GetXmlNodeDouble($"{_path}/@baseline", 0); + return _xml.GetXmlNodeDouble($"@baseline", 0); } set { CreateTopNode(); - _xml.SetXmlNodePercentage($"{_path}/@baseline", value); + _xml.SetXmlNodePercentage($"@baseline", value); } } @@ -803,6 +805,26 @@ private MeasurementFontStyles GetFontStyle() } return ret; } + /// + /// Returns the text according to the capitalization settings. + /// + /// The text + /// If Capitalization is None, the original text is returned. If Capitalization is All, the text is converted to upper case. If Capitalization is Small, the text is converted to lower case. + internal string GetCapitalizedText(string text) + { + if (Capitalization == eTextCapsType.All) + { + return text.ToUpper(CultureInfo.InvariantCulture); + } + else if (Capitalization == eTextCapsType.Small) + { + return text.ToLower(CultureInfo.InvariantCulture); + } + else + { + return text; + } + } internal abstract bool IsEmpty { diff --git a/src/EPPlus/Style/RichText/ExcelParagraph.cs b/src/EPPlus/Style/RichText/ExcelParagraph.cs index 61b8c1c0e..70138580b 100644 --- a/src/EPPlus/Style/RichText/ExcelParagraph.cs +++ b/src/EPPlus/Style/RichText/ExcelParagraph.cs @@ -70,14 +70,34 @@ public string Text { get { - return _textRun.Text; + return _textRun.Text; } set { _textRun.Text = value; } } - + /// + /// Text, adjusted for the Capitalization property + /// + public string DisplayedText + { + get + { + switch (_textRun.Capitalization) + { + + case eTextCapsType.All: + return _textRun.Text.ToUpper(); + case eTextCapsType.Small: + return _textRun.Text.ToLower(); + default: + return _textRun.Text; + + } + } + } + /// /// If the paragraph is the first in the collection /// diff --git a/src/EPPlus/Style/RichText/ExcelParagraphCollection.cs b/src/EPPlus/Style/RichText/ExcelParagraphCollection.cs index c9ca674d1..da6cdbd3e 100644 --- a/src/EPPlus/Style/RichText/ExcelParagraphCollection.cs +++ b/src/EPPlus/Style/RichText/ExcelParagraphCollection.cs @@ -258,6 +258,36 @@ public string Text } } } + /// + /// The displayed test adjusted for capitalization. + /// + public string DisplayedText + { + get + { + StringBuilder sb = new StringBuilder(); + foreach (var item in _list) + { + if (item.IsLastInParagraph) + { + sb.AppendLine(item.DisplayedText); + } + else + { + sb.Append(item.DisplayedText); + } + } + + var ret = sb.ToString(); + if (ret.EndsWith(Environment.NewLine)) + { + //Remove last NewLine + return ret.Substring(0, ret.Length - Environment.NewLine.Length); + } + + return ret; + } + } #region IEnumerable Members IEnumerator IEnumerable.GetEnumerator() From 89ae061cc8e39122ba52f2c0ab30e857f628b8ac Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ossian=20Edstr=C3=B6m?= Date: Tue, 7 Apr 2026 17:06:34 +0200 Subject: [PATCH 142/151] Added alternative defsItems --- .../Shape/ShapeToSvgTests.cs | 20 +++- .../ImageRenderer.cs | 77 ++++++++++++++- .../RenderItems/GradientFill.cs | 12 +++ .../RenderItems/RenderItem.cs | 5 + .../RenderItems/SvgRenderLineItem.cs | 8 +- .../RenderItems/SvgRenderRectItem.cs | 15 ++- .../Svg/DefinitionUtils/DefinitionGroup.cs | 11 ++- .../Svg/DefinitionUtils/LinePattern.cs | 8 +- .../Svg/DefinitionUtils/LinearGradient.cs | 70 ++++++++++++++ .../Svg/DefinitionUtils/MaskGroup.cs | 37 +++++++ .../Svg/DefinitionUtils/PatternItem.cs | 3 +- .../Svg/DefinitionUtils/SymbolGroup.cs | 50 ++++++++++ .../UtillNodes/DynamicGridDefGroup.cs | 96 +++++++++++++++++++ .../UtillNodes/DynamicGridItem.cs | 43 +++++++++ .../DefinitionUtils/UtillNodes/FadeOutMask.cs | 26 +++++ .../Utils/SvgDrawingWriter.cs | 12 +-- src/EPPlusTest/CompoundDocTests.cs | 2 +- src/EPPlusTest/Core/QuadTreeTest.cs | 4 +- 18 files changed, 475 insertions(+), 24 deletions(-) create mode 100644 src/EPPlus.Export.ImageRenderer/Svg/DefinitionUtils/LinearGradient.cs create mode 100644 src/EPPlus.Export.ImageRenderer/Svg/DefinitionUtils/MaskGroup.cs create mode 100644 src/EPPlus.Export.ImageRenderer/Svg/DefinitionUtils/SymbolGroup.cs create mode 100644 src/EPPlus.Export.ImageRenderer/Svg/DefinitionUtils/UtillNodes/DynamicGridDefGroup.cs create mode 100644 src/EPPlus.Export.ImageRenderer/Svg/DefinitionUtils/UtillNodes/DynamicGridItem.cs create mode 100644 src/EPPlus.Export.ImageRenderer/Svg/DefinitionUtils/UtillNodes/FadeOutMask.cs diff --git a/src/EPPlus.Export.ImageRenderer.Test/Shape/ShapeToSvgTests.cs b/src/EPPlus.Export.ImageRenderer.Test/Shape/ShapeToSvgTests.cs index e6bcfd82a..f77055d07 100644 --- a/src/EPPlus.Export.ImageRenderer.Test/Shape/ShapeToSvgTests.cs +++ b/src/EPPlus.Export.ImageRenderer.Test/Shape/ShapeToSvgTests.cs @@ -32,6 +32,25 @@ public void Rect() SaveAndCleanup(p); } } + + [TestMethod] + public void AddShapeWithPatternFill() + { + ExcelPackage.License.SetNonCommercialOrganization("EPPlus Project"); + using (var p = OpenTemplatePackage("ShapeWithPattern.xlsx")) + { + var ws = p.Workbook.Worksheets[0]; + + var myShape = ws.Drawings[0].As.Shape; + + var ir = new EPPlusImageRenderer.ImageRenderer(); + var svg = ir.RenderDrawingToSvg(myShape); + SaveTextFileToWorkbook("ShapeWithPattern.svg", svg); + SaveAndCleanup(p); + } + } + + [TestMethod] public void RoundRect() { @@ -642,6 +661,5 @@ public void CreateChartsWithDifferentSize() SaveAndCleanup(p); } } - } } diff --git a/src/EPPlus.Export.ImageRenderer/ImageRenderer.cs b/src/EPPlus.Export.ImageRenderer/ImageRenderer.cs index 48b6e3858..38e216610 100644 --- a/src/EPPlus.Export.ImageRenderer/ImageRenderer.cs +++ b/src/EPPlus.Export.ImageRenderer/ImageRenderer.cs @@ -11,9 +11,12 @@ Date Author Change 27/11/2025 EPPlus Software AB EPPlus 9 *************************************************************************************************/ +using EPPlus.Export.ImageRenderer.RenderItems; using EPPlus.Export.ImageRenderer.RenderItems.Shared; using EPPlus.Export.ImageRenderer.RenderItems.SvgItem; using EPPlus.Export.ImageRenderer.Svg; +using EPPlus.Export.ImageRenderer.Svg.DefinitionUtils; +using EPPlus.Export.ImageRenderer.Svg.DefinitionUtils.UtillNodes; using EPPlus.Export.ImageRenderer.Svg.NodeAttributes; using EPPlus.Export.ImageRenderer.Svg.Writer; using EPPlus.Fonts.OpenType.Utils; @@ -57,7 +60,7 @@ public enum RenderItemClasses { Rect, TextBox, - Shape + Shape, } public Dictionary GetItemProperties(RenderItemClasses item) @@ -97,7 +100,8 @@ public string RenderIndividualClass(RenderItemClasses item, Dictionary colors, List stops) + { + for (int i = 0; i < stops.Count; i++) + { + var c = new GradientFillColor(stops[i], colors[i]); + Colors.Add(c); + } + } + public ExcelDrawingGradientFill Settings { get; set; } public List Colors { get; set; } = new List(); } diff --git a/src/EPPlus.Export.ImageRenderer/RenderItems/RenderItem.cs b/src/EPPlus.Export.ImageRenderer/RenderItems/RenderItem.cs index 52f409606..b343c69d6 100644 --- a/src/EPPlus.Export.ImageRenderer/RenderItems/RenderItem.cs +++ b/src/EPPlus.Export.ImageRenderer/RenderItems/RenderItem.cs @@ -84,6 +84,11 @@ protected void CloneBase(RenderItem item) item.FillColorSource = FillColorSource; } + internal void SetPatternFill() + { + + } + internal virtual void SetDrawingPropertiesFill(ExcelDrawingFill fill, ExcelDrawingColorManager color) { switch (fill.Style) diff --git a/src/EPPlus.Export.ImageRenderer/RenderItems/SvgRenderLineItem.cs b/src/EPPlus.Export.ImageRenderer/RenderItems/SvgRenderLineItem.cs index 2cee0dbb9..baae1499d 100644 --- a/src/EPPlus.Export.ImageRenderer/RenderItems/SvgRenderLineItem.cs +++ b/src/EPPlus.Export.ImageRenderer/RenderItems/SvgRenderLineItem.cs @@ -149,10 +149,10 @@ private void RenderLineItem(StringBuilder sb, double? borderWidth, string color, if(Suffix == "%") { sb.AppendFormat(""); } diff --git a/src/EPPlus.Export.ImageRenderer/Svg/DefinitionUtils/DefinitionGroup.cs b/src/EPPlus.Export.ImageRenderer/Svg/DefinitionUtils/DefinitionGroup.cs index 1fd64798f..e4301600f 100644 --- a/src/EPPlus.Export.ImageRenderer/Svg/DefinitionUtils/DefinitionGroup.cs +++ b/src/EPPlus.Export.ImageRenderer/Svg/DefinitionUtils/DefinitionGroup.cs @@ -1,4 +1,5 @@ -using EPPlusImageRenderer.RenderItems; +using EPPlusImageRenderer; +using EPPlusImageRenderer.RenderItems; using System; using System.Collections.Generic; using System.Linq; @@ -6,9 +7,13 @@ namespace EPPlus.Export.ImageRenderer.Svg.DefinitionUtils { - internal class DefinitionGroup : RenderItemBase + internal class DefinitionGroup : RenderItem { - internal List Items; + internal List Items = new List(); + + public DefinitionGroup(DrawingBase renderer) : base(renderer) + { + } public override RenderItemType Type => RenderItemType.Group; diff --git a/src/EPPlus.Export.ImageRenderer/Svg/DefinitionUtils/LinePattern.cs b/src/EPPlus.Export.ImageRenderer/Svg/DefinitionUtils/LinePattern.cs index d739795a6..460ff8c80 100644 --- a/src/EPPlus.Export.ImageRenderer/Svg/DefinitionUtils/LinePattern.cs +++ b/src/EPPlus.Export.ImageRenderer/Svg/DefinitionUtils/LinePattern.cs @@ -4,6 +4,7 @@ using OfficeOpenXml.Drawing; using OfficeOpenXml.Drawing.Style; using OfficeOpenXml.Utils; +using OfficeOpenXml.Utils.EnumUtils; using System; using System.Collections.Generic; using System.Drawing; @@ -32,7 +33,7 @@ public LinePattern(DrawingBase baseRend, string id, LinePatternType linesType) : LineItem.BorderWidth = 2; LineItem.Suffix = "%"; - LineItem.BorderColor = "#" + Color.DarkGray.To6CharHexString(); + LineItem.BorderColor = "#" + Color.DarkGoldenrod.ToColorString(); switch (type) { @@ -54,13 +55,14 @@ public LinePattern(DrawingBase baseRend, string id, LinePatternType linesType) : /// internal void SetNumberOfLines(int numberOfLines) { + var percentOf = 100d / (double)numberOfLines; switch (type) { case LinePatternType.Vertical: - widthPercent = (double)numberOfLines / 100d; + widthPercent = percentOf; break; case LinePatternType.Horizontal: - heightPercent = (double)numberOfLines / 100d; + heightPercent = percentOf; break; } } diff --git a/src/EPPlus.Export.ImageRenderer/Svg/DefinitionUtils/LinearGradient.cs b/src/EPPlus.Export.ImageRenderer/Svg/DefinitionUtils/LinearGradient.cs new file mode 100644 index 000000000..dc9dea3bc --- /dev/null +++ b/src/EPPlus.Export.ImageRenderer/Svg/DefinitionUtils/LinearGradient.cs @@ -0,0 +1,70 @@ +using EPPlusImageRenderer; +using EPPlusImageRenderer.RenderItems; +using EPPlusImageRenderer.Utils; +using OfficeOpenXml.Drawing; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using EPPlus.Export.ImageRenderer.RenderItems; +using EPPlus.Fonts.OpenType.Utils; +using EPPlusImageRenderer.Constants; +using OfficeOpenXml.Drawing.Style.Coloring; +using OfficeOpenXml.Drawing.Style.Fill; +using OfficeOpenXml.Drawing.Theme; +using OfficeOpenXml.FormulaParsing.Excel.Functions.MathFunctions; +using OfficeOpenXml.Utils; +using System.Drawing; +using System.Globalization; +using TypeConv = OfficeOpenXml.Utils.TypeConversion; + + +namespace EPPlus.Export.ImageRenderer.Svg.DefinitionUtils +{ + internal class LinearGradient : RenderItem + { + internal DrawGradientFill GradientFillExtra; + internal double Degrees; + + string _id; + bool userSpaceOnUse = false; + + public LinearGradient(DrawingBase renderer, string id) : base(renderer) + { + _id = id; + } + + public override RenderItemType Type => RenderItemType.Group; + + public override void Render(StringBuilder sb) + { + sb.Append($""); + SetStopColors(sb, GradientFillExtra, PathFillMode.Norm); + sb.Append(""); + } + + private string GetOpacity(ExcelDrawingColorManager c) + { + var opacityTransform = c.Transforms?.FirstOrDefault(x => x.Type == OfficeOpenXml.Drawing.Style.Coloring.eColorTransformType.Alpha); + if (opacityTransform == null) return ""; + + return $"stop-opacity=\"{opacityTransform.Value.ToString("0")}%\""; + } + + private void SetStopColors(StringBuilder defSb, DrawGradientFill gradientFill, PathFillMode fillMode) + { + int ix = 0; + + //Svg requires starting at 0 and moving towards 100% Excel sometimes starts at 100 + //Sort to get around that + var sortedGradientColors = gradientFill.Colors.OrderBy(x => x.Position); + + foreach (var c in sortedGradientColors) + { + var color = ColorUtils.GetAdjustedColor(fillMode, c.Color); + // TODO: check if ix should be increased...? + defSb.Append($""); + } + } + } +} diff --git a/src/EPPlus.Export.ImageRenderer/Svg/DefinitionUtils/MaskGroup.cs b/src/EPPlus.Export.ImageRenderer/Svg/DefinitionUtils/MaskGroup.cs new file mode 100644 index 000000000..708313c01 --- /dev/null +++ b/src/EPPlus.Export.ImageRenderer/Svg/DefinitionUtils/MaskGroup.cs @@ -0,0 +1,37 @@ +using EPPlus.Fonts.OpenType.Utils; +using EPPlusImageRenderer; +using EPPlusImageRenderer.RenderItems; +using System; +using System.Collections.Generic; +using System.Globalization; +using System.Linq; +using System.Text; + +namespace EPPlus.Export.ImageRenderer.Svg.DefinitionUtils +{ + internal class MaskGroup : RenderItem + { + protected string _id = null; + + protected List _items = new List(); + + public MaskGroup(DrawingBase renderer, string id) : base(renderer) + { + _id = id; + } + + public override RenderItemType Type => RenderItemType.Group; + + public override void Render(StringBuilder sb) + { + sb.Append($""); + + foreach (var item in _items) + { + item.Render(sb); + } + + sb.Append(""); + } + } +} diff --git a/src/EPPlus.Export.ImageRenderer/Svg/DefinitionUtils/PatternItem.cs b/src/EPPlus.Export.ImageRenderer/Svg/DefinitionUtils/PatternItem.cs index df7b3142a..c6ac3bbf7 100644 --- a/src/EPPlus.Export.ImageRenderer/Svg/DefinitionUtils/PatternItem.cs +++ b/src/EPPlus.Export.ImageRenderer/Svg/DefinitionUtils/PatternItem.cs @@ -5,6 +5,7 @@ using System.Collections.Generic; using System.Linq; using System.Text; +using System.Globalization; namespace EPPlus.Export.ImageRenderer.Svg.DefinitionUtils { @@ -26,7 +27,7 @@ public PatternItem(DrawingBase baseRend, string id) : base(baseRend) public override void Render(StringBuilder sb) { - sb.Append($""); + sb.Append($""); foreach (var item in _items) { diff --git a/src/EPPlus.Export.ImageRenderer/Svg/DefinitionUtils/SymbolGroup.cs b/src/EPPlus.Export.ImageRenderer/Svg/DefinitionUtils/SymbolGroup.cs new file mode 100644 index 000000000..d9d332a1e --- /dev/null +++ b/src/EPPlus.Export.ImageRenderer/Svg/DefinitionUtils/SymbolGroup.cs @@ -0,0 +1,50 @@ +using EPPlus.Fonts.OpenType.Utils; +using EPPlusImageRenderer; +using EPPlusImageRenderer.RenderItems; +using System; +using System.Collections.Generic; +using System.Globalization; +using System.Linq; +using System.Text; + +namespace EPPlus.Export.ImageRenderer.Svg.DefinitionUtils +{ + internal class SymbolGroup : RenderItem + { + protected const string _urlRef = "url(#{0})"; + + protected string _id = null; + + protected List _items = new List(); + + internal string Mask = null; + + public SymbolGroup(DrawingBase renderer, string id) : base(renderer) + { + _id = id; + } + + public override RenderItemType Type => RenderItemType.Group; + + public override void Render(StringBuilder sb) + { + sb.Append($""); + + foreach (var item in _items) + { + item.Render(sb); + } + + sb.Append(""); + } + } +} diff --git a/src/EPPlus.Export.ImageRenderer/Svg/DefinitionUtils/UtillNodes/DynamicGridDefGroup.cs b/src/EPPlus.Export.ImageRenderer/Svg/DefinitionUtils/UtillNodes/DynamicGridDefGroup.cs new file mode 100644 index 000000000..2321bc63f --- /dev/null +++ b/src/EPPlus.Export.ImageRenderer/Svg/DefinitionUtils/UtillNodes/DynamicGridDefGroup.cs @@ -0,0 +1,96 @@ +using EPPlus.Fonts.OpenType.Utils; +using EPPlusImageRenderer; +using EPPlusImageRenderer.RenderItems; +using System; +using System.Collections.Generic; +using System.Drawing; +using System.Globalization; +using System.Linq; +using System.Text; + +namespace EPPlus.Export.ImageRenderer.Svg.DefinitionUtils.UtillNodes +{ + internal class DynamicGridDefGroup : RenderItem + { + protected const string _urlRef = "\"url(#{0})\""; + + internal string Id { get; private set; } + + internal List Items = new List(); + + internal LinePattern LnPatternHorizontal { get; private set; } + internal LinePattern LnPatternVertical { get; private set; } + + FadeOutMask maskItem; + + LinearGradient fadeOutGradient; + + DynamicGridItem gridItem; + + internal string TopId { get; private set; } + + public DynamicGridDefGroup(DrawingBase renderer, string id, int linesX, int linesY): base(renderer) + { + Id = id; + + string lnGradId = id + "_lnGradFade"; + string maskId = id + "_maskFade"; + string horzId = id + "_lnHorizontal"; + string verId = id + "_lnVertical"; + + fadeOutGradient = CreateFadeOutGradient(lnGradId); + Items.Add(fadeOutGradient); + + maskItem = new FadeOutMask(renderer, maskId, string.Format("url(#{0})", lnGradId)); + + Items.Add(maskItem); + + LnPatternHorizontal = new LinePattern(renderer, horzId, LinePatternType.Horizontal); + LnPatternVertical = new LinePattern(renderer, verId, LinePatternType.Vertical); + + SetNumLines(linesX, linesY); + + Items.Add(LnPatternHorizontal); + Items.Add(LnPatternVertical); + + gridItem = new DynamicGridItem(renderer, id, maskId, horzId, verId); + Items.Add(gridItem); + } + + internal void SetNumLines(int numLinesHorizontal, int numLinesVertical) + { + LnPatternHorizontal.SetNumberOfLines(numLinesHorizontal); + LnPatternVertical.SetNumberOfLines(numLinesVertical); + } + + LinearGradient CreateFadeOutGradient(string id) + { + var colors = new List(); + colors.Add(Color.Black); + colors.Add(Color.White); + + var stops = new List(); + stops.Add(0); + stops.Add(100); + + fadeOutGradient = new LinearGradient(DrawingRenderer, id); + fadeOutGradient.GradientFillExtra = new DrawGradientFill(colors, stops); + + return fadeOutGradient; + } + + public override RenderItemType Type => RenderItemType.Group; + + public override void Render(StringBuilder sb) + { + sb.Append($""); + + foreach (var item in Items) + { + item.Render(sb); + } + + sb.Append(""); + } + } +} diff --git a/src/EPPlus.Export.ImageRenderer/Svg/DefinitionUtils/UtillNodes/DynamicGridItem.cs b/src/EPPlus.Export.ImageRenderer/Svg/DefinitionUtils/UtillNodes/DynamicGridItem.cs new file mode 100644 index 000000000..bfd4837d0 --- /dev/null +++ b/src/EPPlus.Export.ImageRenderer/Svg/DefinitionUtils/UtillNodes/DynamicGridItem.cs @@ -0,0 +1,43 @@ +using EPPlus.Export.ImageRenderer.RenderItems; +using EPPlus.Export.ImageRenderer.RenderItems.Interfaces; +using EPPlusImageRenderer; +using EPPlusImageRenderer.RenderItems; +using OfficeOpenXml.Drawing.Style.Fill; +using System; +using System.Collections.Generic; +using System.Drawing; +using System.Linq; +using System.Text; + +namespace EPPlus.Export.ImageRenderer.Svg.DefinitionUtils.UtillNodes +{ + internal class DynamicGridItem : SymbolGroup + { + SvgRenderRectItem HorizontalLines = null; + SvgRenderRectItem VerticalLines = null; + + public DynamicGridItem(DrawingBase renderer, string id, string maskId, string linesHorizontalId, string linesVerticalId) : base(renderer, id) + { + Mask = string.Format(_urlRef, maskId); + + HorizontalLines = IntitalizeRect(); + HorizontalLines.FillColor = string.Format(_urlRef, linesHorizontalId); + + VerticalLines = IntitalizeRect(); + VerticalLines.FillColor = string.Format(_urlRef, linesVerticalId); + + _items.Add(HorizontalLines); + _items.Add(VerticalLines); + } + + SvgRenderRectItem IntitalizeRect() + { + var rect = new SvgRenderRectItem(DrawingRenderer, Bounds); + rect.Width = 100; + rect.Height = 100; + rect.Suffix = "%"; + + return rect; + } + } +} diff --git a/src/EPPlus.Export.ImageRenderer/Svg/DefinitionUtils/UtillNodes/FadeOutMask.cs b/src/EPPlus.Export.ImageRenderer/Svg/DefinitionUtils/UtillNodes/FadeOutMask.cs new file mode 100644 index 000000000..6ede276a6 --- /dev/null +++ b/src/EPPlus.Export.ImageRenderer/Svg/DefinitionUtils/UtillNodes/FadeOutMask.cs @@ -0,0 +1,26 @@ +using EPPlusImageRenderer; +using EPPlusImageRenderer.RenderItems; +using OfficeOpenXml.FormulaParsing.Excel.Functions.MathFunctions; +using System; +using System.Collections.Generic; +using System.Drawing; +using System.Linq; +using System.Text; + +namespace EPPlus.Export.ImageRenderer.Svg.DefinitionUtils.UtillNodes +{ + internal class FadeOutMask : MaskGroup + { + public FadeOutMask(DrawingBase renderer, string id, string rectFillId) : base(renderer, id) + { + var rect = new SvgRenderRectItem(DrawingRenderer, Bounds); + rect.Width = 100; + rect.Height = 100; + rect.Suffix = "%"; + + rect.FillColor = rectFillId; + + _items.Add(rect); + } + } +} diff --git a/src/EPPlus.Export.ImageRenderer/Utils/SvgDrawingWriter.cs b/src/EPPlus.Export.ImageRenderer/Utils/SvgDrawingWriter.cs index 7e7891fe3..6780d8bbe 100644 --- a/src/EPPlus.Export.ImageRenderer/Utils/SvgDrawingWriter.cs +++ b/src/EPPlus.Export.ImageRenderer/Utils/SvgDrawingWriter.cs @@ -41,13 +41,13 @@ public SvgDrawingWriter(DrawingBase svgDrawing) var wb = svgDrawing.Drawing._drawings.Worksheet.Workbook; _theme = wb.ThemeManager.GetOrCreateTheme(); } - internal void WriteSvgDefs(StringBuilder sb, List renderItems) + internal void WriteSvgDefs(StringBuilder sb, List renderItems) { var defSb = new StringBuilder(); var hs = new HashSet(); var ix = 1; - foreach (SvgRenderItem item in renderItems) + foreach (RenderItem item in renderItems) { string filter = ""; if (item.GradientFill != null) @@ -133,7 +133,7 @@ private static string GetFilterName(int ix) return $"item{ix}Filter"; } - private string WriteBlip(string namePrefix, StringBuilder defSb, HashSet hs, SvgRenderItem item, ref string filter) + private string WriteBlip(string namePrefix, StringBuilder defSb, HashSet hs, RenderItem item, ref string filter) { //, item.BlipFill, item.FillColorSource var name = $"{namePrefix}"; @@ -494,10 +494,10 @@ private string GetXy(double? angle) private string GetOpacity(ExcelDrawingColorManager c) { - var opacetyTransform = c.Transforms?.FirstOrDefault(x => x.Type == OfficeOpenXml.Drawing.Style.Coloring.eColorTransformType.Alpha); - if (opacetyTransform == null) return ""; + var opacityTransform = c.Transforms?.FirstOrDefault(x => x.Type == OfficeOpenXml.Drawing.Style.Coloring.eColorTransformType.Alpha); + if (opacityTransform == null) return ""; - return $"stop-opacity=\"{opacetyTransform.Value.ToString("0")}%\""; + return $"stop-opacity=\"{opacityTransform.Value.ToString("0")}%\""; } private string SetStretchTileProps(ExcelDrawingBlipFill blipFill) diff --git a/src/EPPlusTest/CompoundDocTests.cs b/src/EPPlusTest/CompoundDocTests.cs index 03a8fef16..0fe07c96c 100644 --- a/src/EPPlusTest/CompoundDocTests.cs +++ b/src/EPPlusTest/CompoundDocTests.cs @@ -74,7 +74,7 @@ public void Read() private void printitems(CompoundDocumentItem item) { - File.AppendAllText(@"c:\temp\items.txt", item.Name+ "\t"); + File.AppendAllText(@"c:\temp\_items.txt", item.Name+ "\t"); foreach(var c in item.Children) { printitems(c); diff --git a/src/EPPlusTest/Core/QuadTreeTest.cs b/src/EPPlusTest/Core/QuadTreeTest.cs index 3292dc89e..ae5a46ae4 100644 --- a/src/EPPlusTest/Core/QuadTreeTest.cs +++ b/src/EPPlusTest/Core/QuadTreeTest.cs @@ -72,7 +72,7 @@ public void QuadLargeTest() sw.Start(); var items = AddRangeItems(rows, cols, qt, 50, 50); sw.Stop(); - Debug.WriteLine($"Added {items} items in {sw.ElapsedMilliseconds} ms"); + Debug.WriteLine($"Added {items} _items in {sw.ElapsedMilliseconds} ms"); sw.Restart(); var r1 = new QuadRange(5000, 200, 10000, 300); @@ -85,7 +85,7 @@ public void QuadLargeTest() } } sw.Stop(); - Debug.WriteLine($"Queried {ir1.Count} items in {sw.ElapsedMilliseconds} ms"); + Debug.WriteLine($"Queried {ir1.Count} _items in {sw.ElapsedMilliseconds} ms"); } [TestMethod] public void QuadTree_InsertRows_Inside() From e7a4e852775a6e939cb556e485615e689b6608b1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ossian=20Edstr=C3=B6m?= Date: Tue, 7 Apr 2026 17:10:40 +0200 Subject: [PATCH 143/151] Fixed one accidental reName --- src/EPPlus.Export.ImageRenderer/Svg/LineMarkerHelper.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/EPPlus.Export.ImageRenderer/Svg/LineMarkerHelper.cs b/src/EPPlus.Export.ImageRenderer/Svg/LineMarkerHelper.cs index b1a1d2bcb..4542fdf01 100644 --- a/src/EPPlus.Export.ImageRenderer/Svg/LineMarkerHelper.cs +++ b/src/EPPlus.Export.ImageRenderer/Svg/LineMarkerHelper.cs @@ -11,7 +11,7 @@ namespace EPPlus.Export.ImageRenderer.Svg { internal class LineMarkerHelper { - internal static SvgRenderItem GetMarkerItem(SvgChart sc, ExcelLineChartSerie ls, double x, double y, bool isLegend) + internal static RenderItem GetMarkerItem(SvgChart sc, ExcelLineChartSerie ls, double x, double y, bool isLegend) { SvgRenderItem item; var m = ls.Marker; @@ -156,7 +156,7 @@ internal static SvgRenderItem GetMarkerItem(SvgChart sc, ExcelLineChartSerie ls, } return item; } - internal static SvgRenderItem GetMarkerBackground(SvgChart sc, ExcelLineChartSerie ls, double x, double y, bool isLegend) + internal static RenderItem GetMarkerBackground(SvgChart sc, ExcelLineChartSerie ls, double x, double y, bool isLegend) { SvgRenderItem item; var m = ls.Marker; From ff5ffe122f5db1cedfe2465af22a505a2caffffe Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ossian=20Edstr=C3=B6m?= Date: Tue, 7 Apr 2026 17:45:31 +0200 Subject: [PATCH 144/151] Ensured dynamic grid works when using local pos --- .../Svg/DefinitionUtils/LinearGradient.cs | 2 +- .../Svg/DefinitionUtils/MaskGroup.cs | 5 ++++- .../Svg/DefinitionUtils/PatternItem.cs | 2 +- .../Svg/DefinitionUtils/SymbolGroup.cs | 2 -- 4 files changed, 6 insertions(+), 5 deletions(-) diff --git a/src/EPPlus.Export.ImageRenderer/Svg/DefinitionUtils/LinearGradient.cs b/src/EPPlus.Export.ImageRenderer/Svg/DefinitionUtils/LinearGradient.cs index dc9dea3bc..9c50b66e8 100644 --- a/src/EPPlus.Export.ImageRenderer/Svg/DefinitionUtils/LinearGradient.cs +++ b/src/EPPlus.Export.ImageRenderer/Svg/DefinitionUtils/LinearGradient.cs @@ -38,7 +38,7 @@ public LinearGradient(DrawingBase renderer, string id) : base(renderer) public override void Render(StringBuilder sb) { - sb.Append($""); + sb.Append($""); SetStopColors(sb, GradientFillExtra, PathFillMode.Norm); sb.Append(""); } diff --git a/src/EPPlus.Export.ImageRenderer/Svg/DefinitionUtils/MaskGroup.cs b/src/EPPlus.Export.ImageRenderer/Svg/DefinitionUtils/MaskGroup.cs index 708313c01..288095c0e 100644 --- a/src/EPPlus.Export.ImageRenderer/Svg/DefinitionUtils/MaskGroup.cs +++ b/src/EPPlus.Export.ImageRenderer/Svg/DefinitionUtils/MaskGroup.cs @@ -24,7 +24,10 @@ public MaskGroup(DrawingBase renderer, string id) : base(renderer) public override void Render(StringBuilder sb) { - sb.Append($""); + sb.Append($""); foreach (var item in _items) { diff --git a/src/EPPlus.Export.ImageRenderer/Svg/DefinitionUtils/PatternItem.cs b/src/EPPlus.Export.ImageRenderer/Svg/DefinitionUtils/PatternItem.cs index c6ac3bbf7..a9a9c2693 100644 --- a/src/EPPlus.Export.ImageRenderer/Svg/DefinitionUtils/PatternItem.cs +++ b/src/EPPlus.Export.ImageRenderer/Svg/DefinitionUtils/PatternItem.cs @@ -27,7 +27,7 @@ public PatternItem(DrawingBase baseRend, string id) : base(baseRend) public override void Render(StringBuilder sb) { - sb.Append($""); + sb.Append($""); foreach (var item in _items) { diff --git a/src/EPPlus.Export.ImageRenderer/Svg/DefinitionUtils/SymbolGroup.cs b/src/EPPlus.Export.ImageRenderer/Svg/DefinitionUtils/SymbolGroup.cs index d9d332a1e..da2ffff6c 100644 --- a/src/EPPlus.Export.ImageRenderer/Svg/DefinitionUtils/SymbolGroup.cs +++ b/src/EPPlus.Export.ImageRenderer/Svg/DefinitionUtils/SymbolGroup.cs @@ -29,8 +29,6 @@ public SymbolGroup(DrawingBase renderer, string id) : base(renderer) public override void Render(StringBuilder sb) { sb.Append($" Date: Wed, 8 Apr 2026 08:46:39 +0200 Subject: [PATCH 145/151] WIP:Fixed bar chart redering. --- .../Svg/Chart/Axis/CategoryAxisScaleCalculator.cs | 2 +- .../Svg/Chart/ChartTypeDrawers/BarColumnChartTypeDrawer.cs | 4 ++-- .../Svg/Chart/SvgChartPlotarea.cs | 6 ++++-- 3 files changed, 7 insertions(+), 5 deletions(-) diff --git a/src/EPPlus.Export.ImageRenderer/Svg/Chart/Axis/CategoryAxisScaleCalculator.cs b/src/EPPlus.Export.ImageRenderer/Svg/Chart/Axis/CategoryAxisScaleCalculator.cs index da0381f81..e7d8bcc1f 100644 --- a/src/EPPlus.Export.ImageRenderer/Svg/Chart/Axis/CategoryAxisScaleCalculator.cs +++ b/src/EPPlus.Export.ImageRenderer/Svg/Chart/Axis/CategoryAxisScaleCalculator.cs @@ -42,7 +42,7 @@ internal static AxisScale CalculateByWidth(ref List values, ITextMeasure MajorInterval = interval, MinorInterval = 1, Min = 1, - Max = values.Count, + Max = displayValues.Count, TextOrientation = eTextOrientation.Horizontal }; } diff --git a/src/EPPlus.Export.ImageRenderer/Svg/Chart/ChartTypeDrawers/BarColumnChartTypeDrawer.cs b/src/EPPlus.Export.ImageRenderer/Svg/Chart/ChartTypeDrawers/BarColumnChartTypeDrawer.cs index 5eab01670..d34f98a3c 100644 --- a/src/EPPlus.Export.ImageRenderer/Svg/Chart/ChartTypeDrawers/BarColumnChartTypeDrawer.cs +++ b/src/EPPlus.Export.ImageRenderer/Svg/Chart/ChartTypeDrawers/BarColumnChartTypeDrawer.cs @@ -100,13 +100,13 @@ private void AddColumn(ExcelBarChart chartType, ExcelBarChartSerie serie, List Date: Wed, 8 Apr 2026 11:37:51 +0200 Subject: [PATCH 146/151] Fixed duplicated code lines git merge miss-match --- .../Shape/ShapeToSvgTests.cs | 1 - .../RenderItems/Shared/ParagraphItem.cs | 7 +- .../RenderItems/Shared/TextBodyItem.cs | 4 - .../RenderItems/Shared/TextRunItem.cs | 1 - .../RenderItems/SvgItem/SvgTextBodyItem.cs | 1 - .../Svg/Chart/SvgChart.cs | 42 ++-- .../Svg/Chart/SvgChartAxis.cs | 1 - .../Svg/Chart/SvgChartTitle.cs | 2 - .../Svg/SvgShape.cs | 1 - .../Integration/TextLayoutEngineTests.cs | 215 +----------------- .../Integration/TextLayoutEngine.RichText.cs | 28 +-- .../Integration/WrapStateRichText.cs | 3 - .../TrueTypeMeasurer/TextData.cs | 27 --- 13 files changed, 32 insertions(+), 301 deletions(-) diff --git a/src/EPPlus.Export.ImageRenderer.Test/Shape/ShapeToSvgTests.cs b/src/EPPlus.Export.ImageRenderer.Test/Shape/ShapeToSvgTests.cs index 0a2531784..0521fbe93 100644 --- a/src/EPPlus.Export.ImageRenderer.Test/Shape/ShapeToSvgTests.cs +++ b/src/EPPlus.Export.ImageRenderer.Test/Shape/ShapeToSvgTests.cs @@ -540,7 +540,6 @@ public void OpenRightAligned() } } [TestMethod] - public void GenerateSvgForLineCharts() public void TestStyling() { ExcelPackage.License.SetNonCommercialOrganization("EPPlus Project"); diff --git a/src/EPPlus.Export.ImageRenderer/RenderItems/Shared/ParagraphItem.cs b/src/EPPlus.Export.ImageRenderer/RenderItems/Shared/ParagraphItem.cs index 3043fe3d8..ebb2fb2f6 100644 --- a/src/EPPlus.Export.ImageRenderer/RenderItems/Shared/ParagraphItem.cs +++ b/src/EPPlus.Export.ImageRenderer/RenderItems/Shared/ParagraphItem.cs @@ -27,8 +27,7 @@ internal abstract class ParagraphItem : RenderItem { ITextMeasurerWrap _measurer; - double _leftMargin; - double _rightMargin; + double _leftMargin; double _rightMargin; eDrawingTextLineSpacing _lsType; @@ -40,7 +39,6 @@ internal abstract class ParagraphItem : RenderItem TextFragmentCollection _textFragments; List _newTextFragments; - List _newTextFragments; internal protected MeasurementFont _paragraphFont; internal TextBodyItem ParentTextBody { get; set; } internal double ParagraphLineSpacing { get; private set; } @@ -149,7 +147,6 @@ public ParagraphItem(TextBodyItem textBody, DrawingBase renderer, BoundingBox pa int numLines = _paragraphLines.Count; _lsType = p.LineSpacing.LineSpacingType; ParagraphLineSpacing = GetParagraphLineSpacingInPoints(p.LineSpacing.Value, _measurer); - ParagraphLineSpacing = GetParagraphLineSpacingInPoints(p.LineSpacing.Value, _measurer); //---Initialize / calculate lines and runs--- //measurer must be set before AddLinesAndRichText @@ -159,7 +156,6 @@ public ParagraphItem(TextBodyItem textBody, DrawingBase renderer, BoundingBox pa AddLinesAndTextRuns(p, textIfEmpty); } - private double GetParagraphLineSpacingInPoints(double spacingValue, ITextMeasurerWrap fmExact) private double GetParagraphLineSpacingInPoints(double spacingValue, ITextMeasurerWrap fmExact) { if (_lsType == eDrawingTextLineSpacing.Exactly) @@ -251,7 +247,6 @@ void GenerateTextFragments(ExcelDrawingTextRunCollection runs) List runContents = new List(); List fontSizes = new List(); List fonts = new List(); - List fonts = new List(); for (int i = 0; i < runs.Count(); i++) { diff --git a/src/EPPlus.Export.ImageRenderer/RenderItems/Shared/TextBodyItem.cs b/src/EPPlus.Export.ImageRenderer/RenderItems/Shared/TextBodyItem.cs index d73632f09..237800b59 100644 --- a/src/EPPlus.Export.ImageRenderer/RenderItems/Shared/TextBodyItem.cs +++ b/src/EPPlus.Export.ImageRenderer/RenderItems/Shared/TextBodyItem.cs @@ -22,13 +22,11 @@ namespace EPPlus.Export.ImageRenderer.RenderItems.Shared /// internal abstract class TextBodyItem : DrawingObject { - public TextBodyItem(DrawingBase renderer, BoundingBox parent, bool autoSize) : base(renderer, parent) public TextBodyItem(DrawingBase renderer, BoundingBox parent, bool autoSize) : base(renderer, parent) { Bounds.Name = "TxtBody"; Bounds.Parent = parent; AutoSize = autoSize; - AutoSize = autoSize; } public TextBodyItem(DrawingBase renderer, BoundingBox parent, double maxWidth, double maxHeight) : base(renderer, parent) { @@ -37,10 +35,8 @@ public TextBodyItem(DrawingBase renderer, BoundingBox parent, double maxWidth, d MaxWidth = maxWidth; MaxHeight = maxHeight; AutoSize = false; - AutoSize = false; } internal bool AutoSize { get; set; } - internal bool AutoSize { get; set; } internal double MaxWidth { get; set; } internal double MaxHeight { get; set; } diff --git a/src/EPPlus.Export.ImageRenderer/RenderItems/Shared/TextRunItem.cs b/src/EPPlus.Export.ImageRenderer/RenderItems/Shared/TextRunItem.cs index 34a101e8d..e4f0dd09f 100644 --- a/src/EPPlus.Export.ImageRenderer/RenderItems/Shared/TextRunItem.cs +++ b/src/EPPlus.Export.ImageRenderer/RenderItems/Shared/TextRunItem.cs @@ -182,6 +182,5 @@ internal override void GetBounds(out double il, out double it, out double ir, ou ir = Bounds.Right; ib = Bounds.Bottom; } - } } } diff --git a/src/EPPlus.Export.ImageRenderer/RenderItems/SvgItem/SvgTextBodyItem.cs b/src/EPPlus.Export.ImageRenderer/RenderItems/SvgItem/SvgTextBodyItem.cs index 283c37d30..605f8651b 100644 --- a/src/EPPlus.Export.ImageRenderer/RenderItems/SvgItem/SvgTextBodyItem.cs +++ b/src/EPPlus.Export.ImageRenderer/RenderItems/SvgItem/SvgTextBodyItem.cs @@ -17,7 +17,6 @@ namespace EPPlus.Export.ImageRenderer.RenderItems.SvgItem { internal class SvgTextBodyItem : TextBodyItem { - public SvgTextBodyItem(DrawingBase renderer, BoundingBox parent, bool autoSize, bool clampedToParent = false) : base(renderer, parent, autoSize) public SvgTextBodyItem(DrawingBase renderer, BoundingBox parent, bool autoSize, bool clampedToParent = false) : base(renderer, parent, autoSize) { MaxWidth = parent.Width; diff --git a/src/EPPlus.Export.ImageRenderer/Svg/Chart/SvgChart.cs b/src/EPPlus.Export.ImageRenderer/Svg/Chart/SvgChart.cs index 9e8405ca3..98b13f908 100644 --- a/src/EPPlus.Export.ImageRenderer/Svg/Chart/SvgChart.cs +++ b/src/EPPlus.Export.ImageRenderer/Svg/Chart/SvgChart.cs @@ -214,7 +214,6 @@ private void PlaceVerticalAxis(SvgChart sc, SvgChartAxis verticalAxis) } } - internal SvgChartObject ChartArea { get; set; } internal SvgChartObject ChartArea { get; set; } internal SvgChartLegend Legend { get; set; } internal SvgChartTitle Title { get; set; } @@ -229,12 +228,6 @@ private void PlaceVerticalAxis(SvgChart sc, SvgChartAxis verticalAxis) private void SetChartArea() { - var item = new SvgChartArea(this); - item.Rectangle.Width = Bounds.Width; - item.Rectangle.Height = Bounds.Height; - item.Rectangle.SetDrawingPropertiesFill(Chart.Fill, Chart.StyleManager.Style.ChartArea.FillReference.Color); - item.Rectangle.SetDrawingPropertiesBorder(Chart.Border, Chart.StyleManager.Style.ChartArea.BorderReference.Color, Chart.Border.Width > 0); - item.AppendRenderItems(RenderItems); var item = new SvgChartArea(this); item.Rectangle.Width = Bounds.Width; item.Rectangle.Height = Bounds.Height; @@ -263,28 +256,29 @@ public void Render(StringBuilder sb) { drawer.AppendRenderItems(RenderItems); } - if (Plotarea != null) - { - foreach (var drawer in Plotarea?.ChartTypeDrawers) + if (Plotarea != null) { - drawer.AppendRenderItems(RenderItems); + foreach (var drawer in Plotarea?.ChartTypeDrawers) + { + drawer.AppendRenderItems(RenderItems); + } } - } - Legend?.AppendRenderItems(RenderItems); - Title?.AppendRenderItems(RenderItems); + Legend?.AppendRenderItems(RenderItems); + Title?.AppendRenderItems(RenderItems); - sb.Append($""); - sb.Append($""); - //Write defs used for gradient colors - var writer = new SvgDrawingWriter(this); - writer.WriteSvgDefs(sb, RenderItems); + sb.Append($""); + sb.Append($""); + //Write defs used for gradient colors + var writer = new SvgDrawingWriter(this); + writer.WriteSvgDefs(sb, RenderItems); - foreach (var item in RenderItems) - { - item.Render(sb); - } + foreach (var item in RenderItems) + { + item.Render(sb); + } - sb.Append(""); + sb.Append(""); + } } internal double GetPlotAreaTop() diff --git a/src/EPPlus.Export.ImageRenderer/Svg/Chart/SvgChartAxis.cs b/src/EPPlus.Export.ImageRenderer/Svg/Chart/SvgChartAxis.cs index b2a4dce31..13d13461d 100644 --- a/src/EPPlus.Export.ImageRenderer/Svg/Chart/SvgChartAxis.cs +++ b/src/EPPlus.Export.ImageRenderer/Svg/Chart/SvgChartAxis.cs @@ -308,7 +308,6 @@ internal void AddTickmarksAndValues(List DefItems) } } - private List GetAxisValueTextBoxes() private List GetAxisValueTextBoxes() { var ret = new List(); diff --git a/src/EPPlus.Export.ImageRenderer/Svg/Chart/SvgChartTitle.cs b/src/EPPlus.Export.ImageRenderer/Svg/Chart/SvgChartTitle.cs index 2c47f87f3..94ea36f72 100644 --- a/src/EPPlus.Export.ImageRenderer/Svg/Chart/SvgChartTitle.cs +++ b/src/EPPlus.Export.ImageRenderer/Svg/Chart/SvgChartTitle.cs @@ -105,7 +105,6 @@ internal SvgChartTitle(SvgChart sc, ExcelChartTitleStandard t, string defaultTex Rectangle.SetDrawingPropertiesBorder(t.Border, sc.Chart.StyleManager.Style.Title.BorderReference.Color, t.Border.Fill.Style != eFillStyle.NoFill, 0.75); } - private void SetAxisTitleRect(SvgChart sc, SvgChartAxis axis) private void SetAxisTitleRect(SvgChart sc, SvgChartAxis axis) { var margin = 8F; @@ -213,7 +212,6 @@ internal void InitTextBox(double maxWidth, double maxHeight) Rectangle = (SvgRenderRectItem)TextBox.Rectangle; } - public SvgTextBox TextBox public SvgTextBox TextBox { get; private set; diff --git a/src/EPPlus.Export.ImageRenderer/Svg/SvgShape.cs b/src/EPPlus.Export.ImageRenderer/Svg/SvgShape.cs index 8908e0b75..3eaec658c 100644 --- a/src/EPPlus.Export.ImageRenderer/Svg/SvgShape.cs +++ b/src/EPPlus.Export.ImageRenderer/Svg/SvgShape.cs @@ -263,7 +263,6 @@ public void Render(StringBuilder sb) sb.AppendLine(""); } - SvgTextBodyItem CreateTextBodyItem(ExcelTextBody bodyOrig) SvgTextBodyItem CreateTextBodyItem(ExcelTextBody bodyOrig) { if (InsetTextBox == null) diff --git a/src/EPPlus.Fonts.OpenType.Tests/Integration/TextLayoutEngineTests.cs b/src/EPPlus.Fonts.OpenType.Tests/Integration/TextLayoutEngineTests.cs index 3995cab9b..64110bc52 100644 --- a/src/EPPlus.Fonts.OpenType.Tests/Integration/TextLayoutEngineTests.cs +++ b/src/EPPlus.Fonts.OpenType.Tests/Integration/TextLayoutEngineTests.cs @@ -492,11 +492,11 @@ public void EnsureRichTextLineWrappingSameAsNonRichWhenNoWrap() { List comparatorLst = new() { "Strike", "Goudy size"}; - var font = OpenTypeFonts.GetFontData(FontFolders, "Aptos Narrow", FontSubFamily.Regular); + var font = OpenTypeFonts.LoadFont("Aptos Narrow", FontSubFamily.Regular, FontFolders); var shaper = new TextShaper(font); var points1 = shaper.MeasureTextInPoints(comparatorLst[0], 11); - var font2 = OpenTypeFonts.GetFontData(FontFolders, "Goudy Stout", FontSubFamily.Regular); + var font2 = OpenTypeFonts.LoadFont("Goudy Stout", FontSubFamily.Regular, FontFolders); var otherShaper = new TextShaper(font2); var points2 = otherShaper.MeasureTextInPoints(comparatorLst[1], 16); @@ -525,8 +525,8 @@ public void EnsureRichTextLineWrappingSameAsNonRichWhenNoWrap() comparatorFragments.Add(frag1); comparatorFragments.Add(frag2); - var startFont = OpenTypeFonts.GetFontData(FontFolders, font11.FontFamily, GetFontSubType(font11.Style)); - var goudyFont = OpenTypeFonts.GetFontData(FontFolders, font22.FontFamily, GetFontSubType(font22.Style)); + var startFont = OpenTypeFonts.LoadFont(font11.FontFamily, GetFontSubType(font11.Style), FontFolders); + var goudyFont = OpenTypeFonts.LoadFont(font22.FontFamily, GetFontSubType(font22.Style), FontFolders); ITextShaper shaper2 = new TextShaper(font); using var layoutEngine = new TextLayoutEngine(shaper); @@ -542,11 +542,11 @@ public void EnsureRichTextLineWrappingSameAsNonRichWhenNoWrapAndSpaceTrail() { List comparatorLst = new() { "Strike", "Goudy size " }; - var font = OpenTypeFonts.GetFontData(FontFolders, "Aptos Narrow", FontSubFamily.Regular); + var font = OpenTypeFonts.LoadFont("Aptos Narrow", FontSubFamily.Regular, FontFolders); var shaper = new TextShaper(font); var points1 = shaper.MeasureTextInPoints(comparatorLst[0], 11); - var font2 = OpenTypeFonts.GetFontData(FontFolders, "Goudy Stout", FontSubFamily.Regular); + var font2 = OpenTypeFonts.LoadFont("Goudy Stout", FontSubFamily.Regular, FontFolders); var otherShaper = new TextShaper(font2); var points2 = otherShaper.MeasureTextInPoints(comparatorLst[1], 16); @@ -575,8 +575,8 @@ public void EnsureRichTextLineWrappingSameAsNonRichWhenNoWrapAndSpaceTrail() comparatorFragments.Add(frag1); comparatorFragments.Add(frag2); - var startFont = OpenTypeFonts.GetFontData(FontFolders, font11.FontFamily, GetFontSubType(font11.Style)); - var goudyFont = OpenTypeFonts.GetFontData(FontFolders, font22.FontFamily, GetFontSubType(font22.Style)); + var startFont = OpenTypeFonts.LoadFont(font11.FontFamily, GetFontSubType(font11.Style), FontFolders); + var goudyFont = OpenTypeFonts.LoadFont(font22.FontFamily, GetFontSubType(font22.Style), FontFolders); ITextShaper shaper2 = new TextShaper(font); using var layoutEngine = new TextLayoutEngine(shaper); @@ -785,201 +785,6 @@ public void WrapRichTextDifficultCaseCompare() Assert.AreEqual(line5FragmentsOld[1].Width, line5FragmentsNew[1].Width, epsilon); } - [TestMethod] - public void EnsureLineFragmentsAreMeasuredCorrectlyWhenWrapping() - { - List lstOfRichText = new() { "TextBox2", "ra underline", "La Strike", "Goudy size 16"}; - var font2 = new MeasurementFont() - { - FontFamily = "Aptos Narrow", - Size = 11, - Style = MeasurementFontStyles.Bold - }; - - var font3 = new MeasurementFont() - { - FontFamily = "Aptos Narrow", - Size = 11, - Style = MeasurementFontStyles.Underline - }; - - var font4 = new MeasurementFont() - { - FontFamily = "Aptos Narrow", - Size = 11, - Style = MeasurementFontStyles.Strikeout - }; - - var font5 = new MeasurementFont() - { - FontFamily = "Goudy Stout", - Size = 16, - Style = MeasurementFontStyles.Regular - }; - - - List fonts = new() { font2, font3, font4, font5}; - var fragments = new List(); - - for (int i = 0; i < lstOfRichText.Count(); i++) - { - var currentFrag = new TextFragment() { Text = lstOfRichText[i], Font = fonts[i] }; - fragments.Add(currentFrag); - } - - var maxSizePoints = Math.Round(300d, 0, MidpointRounding.AwayFromZero).PixelToPoint(); - var startFont = TextData.GetFontData(font2.FontFamily, GetFontSubType(font2.Style)); - - var shaper = new TextShaper(startFont); - var layout = new TextLayoutEngine(shaper); - - var wrappedLines = layout.WrapRichTextLines(fragments, maxSizePoints); - - - Assert.AreEqual(12.55224609375d, wrappedLines[0].LineFragments[2].Width); - - List smallestTextFragments = new List(); - - //Ensure each linefragment can get correct text - foreach(var line in wrappedLines) - { - foreach(var lf in line.LineFragments) - { - var text = line.GetLineFragmentText(lf); - smallestTextFragments.Add(text); - } - } - - Assert.AreEqual(6, smallestTextFragments.Count); - Assert.AreEqual("TextBox2", smallestTextFragments[0]); - Assert.AreEqual("ra underline", smallestTextFragments[1]); - Assert.AreEqual("La", smallestTextFragments[2]); - Assert.AreEqual("Strike", smallestTextFragments[3]); - Assert.AreEqual("Goudy size", smallestTextFragments[4]); - Assert.AreEqual("16", smallestTextFragments[5]); - } - - [TestMethod] - public void WrapRichTextDifficultCaseCompare() - { - List lstOfRichText = new() { "TextBox\r\na\r\n", "TextBox2", "ra underline", "La Strike", "Goudy size 16", "SvgSize 24" }; - - var font1 = new MeasurementFont() - { - FontFamily = "Aptos Narrow", - Size = 11, - Style = MeasurementFontStyles.Regular - }; ; - - var font2 = new MeasurementFont() - { - FontFamily = "Aptos Narrow", - Size = 11, - Style = MeasurementFontStyles.Bold - }; - - var font3 = new MeasurementFont() - { - FontFamily = "Aptos Narrow", - Size = 11, - Style = MeasurementFontStyles.Underline - }; - - var font4 = new MeasurementFont() - { - FontFamily = "Aptos Narrow", - Size = 11, - Style = MeasurementFontStyles.Strikeout - }; - - var font5 = new MeasurementFont() - { - FontFamily = "Goudy Stout", - Size = 16, - Style = MeasurementFontStyles.Regular - }; - - - var font6 = new MeasurementFont() - { - FontFamily = "Aptos Narrow", - Size = 24, - Style = MeasurementFontStyles.Regular - }; - - List fonts = new() { font1, font2, font3, font4, font5, font6 }; - var fragments = new List(); - - for (int i = 0; i < lstOfRichText.Count(); i++) - { - var currentFrag = new TextFragment() { Text = lstOfRichText[i], Font = fonts[i] }; - fragments.Add(currentFrag); - } - - var maxSizePoints = Math.Round(300d, 0, MidpointRounding.AwayFromZero).PixelToPoint(); - var startFont = TextData.GetFontData(font1.FontFamily, GetFontSubType(font1.Style)); - - var shaper = new TextShaper(startFont); - var layout = new TextLayoutEngine(shaper); - - var wrappedLines = layout.WrapRichTextLines(fragments, maxSizePoints); - - var measurer = new FontMeasurerTrueType(font1); - - TextFragmentCollection collection = new TextFragmentCollection(lstOfRichText); - var simpleLines = measurer.WrapMultipleTextFragmentsToTextLines(collection, fonts, maxSizePoints); - - Assert.AreEqual("TextBox", wrappedLines[0].Text); - Assert.AreEqual("a", wrappedLines[1].Text); - Assert.AreEqual("TextBox2ra underlineLa", wrappedLines[2].Text); - Assert.AreEqual("StrikeGoudy size", wrappedLines[3].Text); - Assert.AreEqual("16SvgSize 24", wrappedLines[4].Text); - - Assert.AreEqual(simpleLines[0].Text, wrappedLines[0].Text); - Assert.AreEqual(simpleLines[1].Text, wrappedLines[1].Text); - Assert.AreEqual(simpleLines[2].Text, wrappedLines[2].Text); - Assert.AreEqual(simpleLines[3].Text, wrappedLines[3].Text); - Assert.AreEqual(simpleLines[4].Text, wrappedLines[4].Text); - - //Rather large epsilon but the char widths are each individually more correct now - var epsilon = 0.1d; - - Assert.AreEqual(simpleLines[0].Width, wrappedLines[0].Width, epsilon); - Assert.AreEqual(simpleLines[1].Width, wrappedLines[1].Width, epsilon); - Assert.AreEqual(simpleLines[2].Width, wrappedLines[2].Width, epsilon); - Assert.AreEqual(simpleLines[3].Width, wrappedLines[3].Width, epsilon); - Assert.AreEqual(simpleLines[4].Width, wrappedLines[4].Width, epsilon); - - var line1FragmentsOld = simpleLines[0].RtFragments; - var line1FragmentsNew = wrappedLines[0].LineFragments; - Assert.AreEqual(line1FragmentsOld[0].Width, line1FragmentsNew[0].Width, epsilon); - - var line2FragmentsOld = simpleLines[1].RtFragments; - var line2FragmentsNew = wrappedLines[1].LineFragments; - - Assert.AreEqual(line2FragmentsOld[0].Width, line2FragmentsNew[0].Width, epsilon); - - var line3FragmentsOld = simpleLines[2].RtFragments; - var line3FragmentsNew = wrappedLines[2].LineFragments; - - Assert.AreEqual(line3FragmentsOld[0].Width, line3FragmentsNew[0].Width, epsilon); - Assert.AreEqual(line3FragmentsOld[1].Width, line3FragmentsNew[1].Width, epsilon); - Assert.AreEqual(line3FragmentsOld[2].Width, line3FragmentsNew[2].Width, epsilon); - - - var line4FragmentsOld = simpleLines[3].RtFragments; - var line4FragmentsNew = wrappedLines[3].LineFragments; - - Assert.AreEqual(line4FragmentsOld[0].Width, line4FragmentsNew[0].Width, epsilon); - Assert.AreEqual(line4FragmentsOld[1].Width, line4FragmentsNew[1].Width, epsilon); - - var line5FragmentsOld = simpleLines[4].RtFragments; - var line5FragmentsNew = wrappedLines[4].LineFragments; - - Assert.AreEqual(line5FragmentsOld[0].Width, line5FragmentsNew[0].Width, epsilon); - Assert.AreEqual(line5FragmentsOld[1].Width, line5FragmentsNew[1].Width, epsilon); - } - [TestMethod] public void WrapRichText_WordSpanningFragments_MeasuresCorrectly() { @@ -1160,7 +965,7 @@ public void WrapText_Continous_Long_Word() public void WrapRichText_MeasureCorrectly() { // Arrange - var font = OpenTypeFonts.GetFontData(FontFolders, "Aptos Narrow", FontSubFamily.Regular); + var font = OpenTypeFonts.LoadFont("Aptos Narrow", FontSubFamily.Regular, FontFolders); var shaper = new TextShaper(font); var layout = new TextLayoutEngine(shaper); @@ -1195,7 +1000,7 @@ public void VerifyWrappingSingleChar() }; var maxWidthPt = 31.8125234375d; - var gottenFont = OpenTypeFonts.GetFontData(FontFolders, "Aptos Narrow", FontSubFamily.Regular); + var gottenFont = OpenTypeFonts.LoadFont("Aptos Narrow", FontSubFamily.Regular, FontFolders); var shaper = new TextShaper(gottenFont); var layout = new TextLayoutEngine(shaper); diff --git a/src/EPPlus.Fonts.OpenType/Integration/TextLayoutEngine.RichText.cs b/src/EPPlus.Fonts.OpenType/Integration/TextLayoutEngine.RichText.cs index ed4bff586..7b3d7aa0c 100644 --- a/src/EPPlus.Fonts.OpenType/Integration/TextLayoutEngine.RichText.cs +++ b/src/EPPlus.Fonts.OpenType/Integration/TextLayoutEngine.RichText.cs @@ -134,28 +134,17 @@ private void ProcessFragment( var charWidths = GetCharWidthBuffer(len); Array.Clear(charWidths, 0, len); - //options.ApplySubstitutions = false; - //options.ApplyPositioning = false; // ShapeLight applies only kerning (sufficient for line-breaking). // Full Shape() runs SingleAdjustment + Kerning + MarkToBase which // is ~250x slower and irrelevant for wrapping decisions. - var glyphWidths = shaper.ShapeLight(fragment.Text, options); - double scale = fragment.Font.Size / shaper.UnitsPerEm; - - //var shaped = shaper.Shape(fragment.Text, options); - //var widthInPoint = shaped.GetWidthInPoints(fragment.Font.Size, shaper.UnitsPerEm); - ////FillCharWidths(gWidthsReal, scale, len, charWidths); - - Array.Clear(charWidths, 0, len); - FillCharWidths(glyphWidths, scale, len, charWidths); + var shaped = shaper.ShapeLight(fragment.Text, options); + shaped.FillCharWidths(fragment.Font.Size, charWidths, len); //Store for after everything is done fragment.AscentPoints = shaper.GetAscentInPoints(fragment.Font.Size); fragment.DescentPoints = shaper.GetDescentInPoints(fragment.Font.Size); - var spaceWidth = shaper.Shape(" ", options).GetWidthInPoints(fragment.Font.Size,shaper.UnitsPerEm); - - //GetCachedSpaceWidth(fragment.Font.Size, options); + var spaceWidth = shaper.Shape(" ", options).GetWidthInPoints(fragment.Font.Size); state.LineFrag = new LineFragment(state.CurrentFragmentIdx, lineBuilder.Length); state.LineFrag.SpaceWidth = spaceWidth; @@ -263,8 +252,6 @@ private void SkipLineBreakChars(string text, ref int i) } i++; } - - private void WrapCurrentLine(StringBuilder lineBuilder, WrapStateRichText state, double maxWidthPoints, double advanceWidth) private void WrapCurrentLine(StringBuilder lineBuilder, WrapStateRichText state, double maxWidthPoints, double advanceWidth) { int fragIdxAtBreak = state.CurrentFragmentIdx; @@ -313,15 +300,6 @@ private void WrapCurrentLine(StringBuilder lineBuilder, WrapStateRichText state, state.CurrentTextLine.Width = state.CurrentLineWidth - advanceWidth; state.CurrentTextLine.Text = line; - //Add the char that went over max to the next line - var line = lineBuilder.ToString(0, lineBuilder.Length - 1); - // No valid space found - wrap entire line - _lineListBuffer.Add(line); - - //handle line data - state.CurrentTextLine.Width = state.CurrentLineWidth - advanceWidth; - state.CurrentTextLine.Text = line; - //Add the char that went over max to the next line state.CurrentLineWidth = 0; lineBuilder.Length = 0; diff --git a/src/EPPlus.Fonts.OpenType/Integration/WrapStateRichText.cs b/src/EPPlus.Fonts.OpenType/Integration/WrapStateRichText.cs index 6013e0f22..b4f605fa5 100644 --- a/src/EPPlus.Fonts.OpenType/Integration/WrapStateRichText.cs +++ b/src/EPPlus.Fonts.OpenType/Integration/WrapStateRichText.cs @@ -1,5 +1,4 @@ using EPPlus.Fonts.OpenType.TrueTypeMeasurer.DataHolders; -using EPPlus.Fonts.OpenType.TrueTypeMeasurer.DataHolders; using System; using System.Collections.Generic; using System.Linq; @@ -11,8 +10,6 @@ internal class WrapStateRichText : WrapStateBase { internal LineFragment LineFrag = null; - internal LineFragment LineFrag = null; - public WrapStateRichText(double lineWidth) { CurrentLineWidth = lineWidth; diff --git a/src/EPPlus.Fonts.OpenType/TrueTypeMeasurer/TextData.cs b/src/EPPlus.Fonts.OpenType/TrueTypeMeasurer/TextData.cs index cd77f8cfb..0f350315e 100644 --- a/src/EPPlus.Fonts.OpenType/TrueTypeMeasurer/TextData.cs +++ b/src/EPPlus.Fonts.OpenType/TrueTypeMeasurer/TextData.cs @@ -54,33 +54,6 @@ internal static TextLayoutEngine GetTextLayoutEngine(MeasurementFont mFont) } - private static FontSubFamily GetFontSubType(MeasurementFontStyles Style) - { - if ((Style & (MeasurementFontStyles.Bold | MeasurementFontStyles.Italic)) == (MeasurementFontStyles.Bold | MeasurementFontStyles.Italic)) - { - return FontSubFamily.BoldItalic; - } - else if ((Style & MeasurementFontStyles.Bold) == MeasurementFontStyles.Bold) - { - return FontSubFamily.Bold; - } - else if ((Style & MeasurementFontStyles.Italic) == MeasurementFontStyles.Italic) - { - return FontSubFamily.Italic; - } - - return FontSubFamily.Regular; - } - - internal static TextLayoutEngine GetTextLayoutEngine(MeasurementFont mFont) - { - var startFont = TextData.GetFontData(mFont.FontFamily, GetFontSubType(mFont.Style)); - var shaper = new TextShaper(startFont); - var layout = new TextLayoutEngine(shaper); - return layout; - } - - private static FontSubFamily GetFontSubType(MeasurementFontStyles Style) { if ((Style & (MeasurementFontStyles.Bold | MeasurementFontStyles.Italic)) == (MeasurementFontStyles.Bold | MeasurementFontStyles.Italic)) From 939dd05c94ecdeb4cdd97271417e5e4f6b25e059 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ossian=20Edstr=C3=B6m?= Date: Wed, 8 Apr 2026 12:36:34 +0200 Subject: [PATCH 147/151] Fixed other bugs caused by merge/duplication --- .../Integration/TextLayoutEngine.RichText.cs | 13 ------------- 1 file changed, 13 deletions(-) diff --git a/src/EPPlus.Fonts.OpenType/Integration/TextLayoutEngine.RichText.cs b/src/EPPlus.Fonts.OpenType/Integration/TextLayoutEngine.RichText.cs index 7b3d7aa0c..dc9494e79 100644 --- a/src/EPPlus.Fonts.OpenType/Integration/TextLayoutEngine.RichText.cs +++ b/src/EPPlus.Fonts.OpenType/Integration/TextLayoutEngine.RichText.cs @@ -226,21 +226,16 @@ private void HandleLineBreak(StringBuilder lineBuilder, WrapStateRichText state) { _lineListBuffer.Add(lineBuilder.ToString()); state.CurrentTextLine.Text = lineBuilder.ToString(); - state.CurrentTextLine.Text = lineBuilder.ToString(); } else if (state.CurrentLineWidth > 0) { _lineListBuffer.Add(string.Empty); state.CurrentTextLine.Text = string.Empty; - state.CurrentTextLine.Text = string.Empty; } state.CurrentTextLine.Width = state.CurrentLineWidth; state.EndCurrentTextLineAndIntializeNext(0); - state.CurrentTextLine.Width = state.CurrentLineWidth; - state.EndCurrentTextLineAndIntializeNext(0); - lineBuilder.Length = 0; } @@ -278,14 +273,6 @@ private void WrapCurrentLine(StringBuilder lineBuilder, WrapStateRichText state, //Because of word-wrap we may have richTextFragments on the current line that is no longer part of it after wrap. state.AdjustLineFragmentsForNextLine(); - //handle line data - state.CurrentTextLine.Width = state.CurrentLineWidth - state.CurrentWordWidth; - state.CurrentTextLine.Text = line; - - fragIdxAtBreak = state.GetFragIdxAtWordStart(); - //Because of word-wrap we may have richTextFragments on the current line that is no longer part of it after wrap. - state.AdjustLineFragmentsForNextLine(); - state.CurrentLineWidth = state.CurrentWordWidth; state.LineStart = state.WordStart; } From 3eb477692ac15d54b46eb456c04f241454d9a702 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jan=20K=C3=A4llman?= Date: Wed, 8 Apr 2026 14:06:38 +0200 Subject: [PATCH 148/151] WIP:Moved axis textboxes to a separate class to allow separate rendering (z-order).Fixed column charts for stacked and stacked percent. --- .../Chart/ColumnChartTests.cs | 2 +- .../Chart/LineChartToSvgTests.cs | 4 +- .../RenderItems/SvgItem/SvgTextBox.cs | 5 +- .../Chart/Axis/ValueAxisScaleCalculator.cs | 11 ++- .../BarColumnChartTypeDrawer.cs | 68 +++++++++++-------- .../Svg/Chart/SvgAxisTextBoxes.cs | 43 ++++++++++++ .../Svg/Chart/SvgChart.cs | 8 ++- .../Svg/Chart/SvgChartAxis.cs | 19 ++---- .../Svg/Chart/SvgChartLegend.cs | 6 +- .../Svg/Chart/SvgChartPlotarea.cs | 7 +- .../Issues/ConditionalFormattingIssues.cs | 30 ++++++++ 11 files changed, 148 insertions(+), 55 deletions(-) create mode 100644 src/EPPlus.Export.ImageRenderer/Svg/Chart/SvgAxisTextBoxes.cs diff --git a/src/EPPlus.Export.ImageRenderer.Test/Chart/ColumnChartTests.cs b/src/EPPlus.Export.ImageRenderer.Test/Chart/ColumnChartTests.cs index 3c0fe09ec..3253e940e 100644 --- a/src/EPPlus.Export.ImageRenderer.Test/Chart/ColumnChartTests.cs +++ b/src/EPPlus.Export.ImageRenderer.Test/Chart/ColumnChartTests.cs @@ -42,7 +42,7 @@ public void GenerateSvgForColumnCharts2() var ws = p.Workbook.Worksheets[1]; var renderer = new EPPlusImageRenderer.ImageRenderer(); - //var ix = 1; + //var ix = 2; //var c = ws.Drawings[ix]; //var svg = renderer.RenderDrawingToSvg(c); //SaveTextFileToWorkbook($"svg\\ChartForSvg_ind{ix++}.svg", svg); diff --git a/src/EPPlus.Export.ImageRenderer.Test/Chart/LineChartToSvgTests.cs b/src/EPPlus.Export.ImageRenderer.Test/Chart/LineChartToSvgTests.cs index b9d0d5e81..cd707f426 100644 --- a/src/EPPlus.Export.ImageRenderer.Test/Chart/LineChartToSvgTests.cs +++ b/src/EPPlus.Export.ImageRenderer.Test/Chart/LineChartToSvgTests.cs @@ -20,7 +20,7 @@ public void GenerateSvgForLineCharts_sheet1() var ws = p.Workbook.Worksheets[0]; var renderer = new EPPlusImageRenderer.ImageRenderer(); - //var ix = 1; + //var ix = 2; //var c = ws.Drawings[ix]; //var svg = renderer.RenderDrawingToSvg(c); //SaveTextFileToWorkbook($"svg\\ChartForSvg_ind{ix++}.svg", svg); @@ -123,7 +123,7 @@ public void GenerateSvgForCharts_SecondaryAxis_sheet2() { var ws = p.Workbook.Worksheets[1]; var renderer = new EPPlusImageRenderer.ImageRenderer(); - //var ix = 3; + //var ix = 0; //var c = ws.Drawings[ix]; //var svg = renderer.RenderDrawingToSvg(c); //SaveTextFileToWorkbook($"svg\\ChartForSvg_sheet2_{ix++}.svg", svg); diff --git a/src/EPPlus.Export.ImageRenderer/RenderItems/SvgItem/SvgTextBox.cs b/src/EPPlus.Export.ImageRenderer/RenderItems/SvgItem/SvgTextBox.cs index cf446d5d7..071e63fcc 100644 --- a/src/EPPlus.Export.ImageRenderer/RenderItems/SvgItem/SvgTextBox.cs +++ b/src/EPPlus.Export.ImageRenderer/RenderItems/SvgItem/SvgTextBox.cs @@ -213,7 +213,10 @@ internal override void AppendRenderItems(List renderItems) { rect.Bounds.Left = -rect.Bounds.Width; } - rect.Bounds.Left = 0; + else + { + rect.Bounds.Left = 0; + } rect.Bounds.Top = 0; renderItems.Add(titleItem); renderItems.Add(rect); diff --git a/src/EPPlus.Export.ImageRenderer/Svg/Chart/Axis/ValueAxisScaleCalculator.cs b/src/EPPlus.Export.ImageRenderer/Svg/Chart/Axis/ValueAxisScaleCalculator.cs index a214f1434..7bf7c10fc 100644 --- a/src/EPPlus.Export.ImageRenderer/Svg/Chart/Axis/ValueAxisScaleCalculator.cs +++ b/src/EPPlus.Export.ImageRenderer/Svg/Chart/Axis/ValueAxisScaleCalculator.cs @@ -122,9 +122,16 @@ private static AxisScale GetNumberScale(ref double dataMin, ref double dataMax, else { axisMin = dataMin - (dataRange * 0.05); - if (axisOptions.IsStacked100 && axisMin < -1) + if (axisOptions.IsStacked100) { - axisMin = -1; + if (axisMin < -1) + { + axisMin = -1; + } + else if(axisMin>0) + { + axisMin = 0; + } } //Normalize to zero if all data is positive or negative if (axisMin < 0 && isAllPositive) diff --git a/src/EPPlus.Export.ImageRenderer/Svg/Chart/ChartTypeDrawers/BarColumnChartTypeDrawer.cs b/src/EPPlus.Export.ImageRenderer/Svg/Chart/ChartTypeDrawers/BarColumnChartTypeDrawer.cs index d34f98a3c..d9c6abe3c 100644 --- a/src/EPPlus.Export.ImageRenderer/Svg/Chart/ChartTypeDrawers/BarColumnChartTypeDrawer.cs +++ b/src/EPPlus.Export.ImageRenderer/Svg/Chart/ChartTypeDrawers/BarColumnChartTypeDrawer.cs @@ -49,18 +49,14 @@ internal BarColumnChartTypeDrawer(SvgChart svgChart, ExcelBarChart chartType) : { ExcelChartAxisStandard.CalculateStacked100(yValues); } - //var xCount = xValues.Select(x => x.Count).Sum(y => y); - //var yCount = yValues.Select(x => x.Count).Sum(y => y); var count = Math.Min(xValues.Count, yValues.Count); - for (var i= 0; i < xValues.Count; i++) + for (var i= 0; i < xValues.Count; i++) { - var xSerie = xValues[i]; - var ySerie = yValues[i]; var serie = (ExcelBarChartSerie)chartType.Series[i]; var dataPoints = new List(); - AddColumn(chartType, serie, xSerie, ySerie, dataPoints, count, i); + AddColumn(chartType, serie, xValues, yValues, dataPoints, count, i); dataPointsPerSerie.Add(dataPoints); @@ -79,7 +75,7 @@ internal BarColumnChartTypeDrawer(SvgChart svgChart, ExcelBarChart chartType) : dataLabel.AppendRenderItems(RenderItems); } } - private void AddColumn(ExcelBarChart chartType, ExcelBarChartSerie serie, List xValues, List yValues, List dataPoints, int seriesCount, int position) + private void AddColumn(ExcelBarChart chartType, ExcelBarChartSerie serie, List> xSeries, List> ySeries, List dataPoints, int seriesCount, int position) { SvgChartAxis yAxis, xAxis; if (chartType.UseSecondaryAxis) @@ -96,8 +92,10 @@ private void AddColumn(ExcelBarChart chartType, ExcelBarChartSerie serie, List 0 && (isStacked || isStacked100)) { - rect.Top = yAxisStart; - rect.Height = yPos - yAxisStart; + //Stacked + var pYValues = ySeries[position - 1]; + var pAxisEnd = ConvertUtil.GetValueDouble(pYValues[i]); + var yPrevPos = yAxis.GetPositionInPlotarea(pAxisEnd, false); + if (y < 0) + { + rect.Top = yPrevPos; + rect.Height = yPos - yPrevPos; + } + else + { + rect.Top = yPos; + rect.Height = yPrevPos - yPos; + } } else { - rect.Top = yPos; - rect.Height = yAxisStart - yPos; + if (y < 0) + { + rect.Top = yAxisStart; + rect.Height = yPos - yAxisStart; + } + else + { + rect.Top = yPos; + rect.Height = yAxisStart - yPos; + } } rect.SetDrawingPropertiesFill(serie.Fill, chartType.StyleManager.Style.SeriesAxis.FillReference.Color); rect.SetDrawingPropertiesBorder(serie.Border, chartType.StyleManager.Style.SeriesAxis.BorderReference.Color, true); diff --git a/src/EPPlus.Export.ImageRenderer/Svg/Chart/SvgAxisTextBoxes.cs b/src/EPPlus.Export.ImageRenderer/Svg/Chart/SvgAxisTextBoxes.cs new file mode 100644 index 000000000..e87d13d97 --- /dev/null +++ b/src/EPPlus.Export.ImageRenderer/Svg/Chart/SvgAxisTextBoxes.cs @@ -0,0 +1,43 @@ +/************************************************************************************************* + Required Notice: Copyright (C) EPPlus Software AB. + This software is licensed under PolyForm Noncommercial License 1.0.0 + and may only be used for noncommercial purposes + https://polyformproject.org/licenses/noncommercial/1.0.0/ + + A commercial license to use this software can be purchased at https://epplussoftware.com + ************************************************************************************************* + Date Author Change + ************************************************************************************************* + 27/11/2025 EPPlus Software AB EPPlus 9 + *************************************************************************************************/ +using EPPlus.Export.ImageRenderer.RenderItems.SvgItem; +using EPPlusImageRenderer.RenderItems; +using System.Collections.Generic; + +namespace EPPlusImageRenderer.Svg +{ + internal class SvgAxisTextBoxes : SvgChartObject + { + internal SvgAxisTextBoxes(DrawingChart chart) : base(chart) + { + } + + internal List TextBoxes + { + get; + set; + }=new List(); + + internal override void AppendRenderItems(List renderItems) + { + if (TextBoxes != null && TextBoxes.Count > 0) + { + foreach (var tb in TextBoxes) + { + tb.AppendRenderItems(renderItems); + } + } + + } + } +} \ No newline at end of file diff --git a/src/EPPlus.Export.ImageRenderer/Svg/Chart/SvgChart.cs b/src/EPPlus.Export.ImageRenderer/Svg/Chart/SvgChart.cs index 98b13f908..bb98d8a8f 100644 --- a/src/EPPlus.Export.ImageRenderer/Svg/Chart/SvgChart.cs +++ b/src/EPPlus.Export.ImageRenderer/Svg/Chart/SvgChart.cs @@ -263,8 +263,14 @@ public void Render(StringBuilder sb) drawer.AppendRenderItems(RenderItems); } } - Legend?.AppendRenderItems(RenderItems); + + HorizontalAxis?.Textboxes?.AppendRenderItems(RenderItems); + VerticalAxis?.Textboxes?.AppendRenderItems(RenderItems); + SecondHorizontalAxis?.Textboxes?.AppendRenderItems(RenderItems); + SecondVerticalAxis?.Textboxes?.AppendRenderItems(RenderItems); + Title?.AppendRenderItems(RenderItems); + Legend?.AppendRenderItems(RenderItems); sb.Append($""); sb.Append($""); diff --git a/src/EPPlus.Export.ImageRenderer/Svg/Chart/SvgChartAxis.cs b/src/EPPlus.Export.ImageRenderer/Svg/Chart/SvgChartAxis.cs index 13d13461d..19046bc74 100644 --- a/src/EPPlus.Export.ImageRenderer/Svg/Chart/SvgChartAxis.cs +++ b/src/EPPlus.Export.ImageRenderer/Svg/Chart/SvgChartAxis.cs @@ -67,6 +67,7 @@ internal SvgChartAxis(SvgChart sc, ExcelChartAxisStandard ax) : base(sc) if (ax.Deleted == false) { + Textboxes = new SvgAxisTextBoxes(sc); if (ax.Layout.HasLayout) { Rectangle = GetRectFromManualLayout(sc, ax.Layout); @@ -206,12 +207,7 @@ public List Values public List MinorAxisPositions { get; private set; } public List MajorGridlinePositions { get; private set; } public List MinorGridlinePositions { get; private set; } - public List AxisValuesTextBoxes - { - get; - private set; - } - + public SvgAxisTextBoxes Textboxes{get; private set;} public SvgChartTitle Title { get; set; } public double Min { get; set; } public double Max { get; set; } @@ -256,15 +252,8 @@ internal override void AppendRenderItems(List renderItems) renderItems.Add(tm); } } - - if (AxisValuesTextBoxes != null && AxisValuesTextBoxes.Count > 0) - { - foreach (var tb in AxisValuesTextBoxes) - { - tb.AppendRenderItems(renderItems); - } - } + //The axis text boxes is rendered later as they have a higher Z-order. } @@ -304,7 +293,7 @@ internal void AddTickmarksAndValues(List DefItems) } if (AxisValues != null && AxisValues.Count > 0 && Axis.Deleted==false && Axis.LabelPosition != eTickLabelPosition.None) { - AxisValuesTextBoxes = GetAxisValueTextBoxes(); + Textboxes.TextBoxes = GetAxisValueTextBoxes(); } } diff --git a/src/EPPlus.Export.ImageRenderer/Svg/Chart/SvgChartLegend.cs b/src/EPPlus.Export.ImageRenderer/Svg/Chart/SvgChartLegend.cs index 45a7b7232..67317d7c9 100644 --- a/src/EPPlus.Export.ImageRenderer/Svg/Chart/SvgChartLegend.cs +++ b/src/EPPlus.Export.ImageRenderer/Svg/Chart/SvgChartLegend.cs @@ -171,7 +171,7 @@ private SvgRenderRectItem GetLegendRectangleAndEntrySize(SvgChart sc, ExcelChart } else { - rect.Top = sc.ChartArea.Rectangle.Height - rect.Height - MiddleMargin; + rect.Top = sc.ChartArea.Rectangle.Height - rect.Height - BottomMargin; } break; case eLegendPosition.Right: @@ -432,7 +432,7 @@ private double GetItemPosition(SvgChart sc, TextMeasurement pTm, TextMeasurement } if (pSls == null) { - y = Rectangle.Top + TopMargin; + y = Rectangle.Top + TopMargin + tm.Height / 2; } else { @@ -445,7 +445,7 @@ private double GetItemPosition(SvgChart sc, TextMeasurement pTm, TextMeasurement { if (pSls == null) { - y = TopMargin;// + tm.Height / 2 + MarginExtra; + y = TopMargin + tm.Height / 2; } else { diff --git a/src/EPPlus.Export.ImageRenderer/Svg/Chart/SvgChartPlotarea.cs b/src/EPPlus.Export.ImageRenderer/Svg/Chart/SvgChartPlotarea.cs index f33481521..c4bb427f2 100644 --- a/src/EPPlus.Export.ImageRenderer/Svg/Chart/SvgChartPlotarea.cs +++ b/src/EPPlus.Export.ImageRenderer/Svg/Chart/SvgChartPlotarea.cs @@ -53,16 +53,16 @@ internal SvgRenderRectItem GetPlotAreaRectangle(SvgChart sc) private double GetPlotAreaHeight(SvgChart sc, SvgRenderRectItem rect) { var bottomAxis = GetAxisByPosition(sc, eAxisPosition.Bottom); - double vaHeight = 0, vaTitleHeight = 0; + double vaHeight = 0; if (bottomAxis!=null) { vaHeight = (bottomAxis.Rectangle?.Height ?? 0D) + (bottomAxis.Title?.TextBox?.GetActualHeight() ?? 0D); } if (sc.Chart.Legend?.Position == eLegendPosition.Bottom) { - vaHeight += sc.Legend.Rectangle.Height; + vaHeight += sc.Legend.Rectangle.Height + sc.Legend.TopMargin; } - return sc.Bounds.Height - rect.GlobalTop - vaHeight - vaTitleHeight - BottomMargin; + return sc.Bounds.Height - rect.GlobalTop - vaHeight - BottomMargin; } private double GetPlotAreaWidth(SvgChart sc, SvgRenderRectItem rect) @@ -72,6 +72,7 @@ private double GetPlotAreaWidth(SvgChart sc, SvgRenderRectItem rect) var left = ((lp == eLegendPosition.Right || lp == eLegendPosition.TopRight) && sc.Legend != null ? sc.Legend.Bounds.GlobalLeft - RightMargin : sc.ChartArea.Rectangle.Width - RightMargin); + if (rightAxis == null) { return left - rect.GlobalLeft; diff --git a/src/EPPlusTest/Issues/ConditionalFormattingIssues.cs b/src/EPPlusTest/Issues/ConditionalFormattingIssues.cs index 1c6e8aa05..75caf82d1 100644 --- a/src/EPPlusTest/Issues/ConditionalFormattingIssues.cs +++ b/src/EPPlusTest/Issues/ConditionalFormattingIssues.cs @@ -1,6 +1,8 @@ using Microsoft.VisualStudio.TestTools.UnitTesting; using OfficeOpenXml; +using OfficeOpenXml.Compatibility.System.Drawing; using OfficeOpenXml.ConditionalFormatting; +using OfficeOpenXml.ConditionalFormatting.Contracts; using OfficeOpenXml.Style; using System.Globalization; using System.Threading; @@ -75,5 +77,33 @@ public void Test1_Input_ExpectedOutput() SaveAndCleanup(package); Thread.CurrentThread.CurrentCulture = currentCulture; } + [TestMethod] + public void s1025() + { + using var package = OpenTemplatePackage("s1025.xlsx"); //attached template, ExampleWB.xlsx + ExcelWorksheet ws = package.Workbook.Worksheets[0]; + + IExcelConditionalFormattingBetween condGreen = ws.ConditionalFormatting.AddBetween(ws.Cells[5, 2, 25, 2]); + condGreen.Style.Fill.PatternType = OfficeOpenXml.Style.ExcelFillStyle.Solid; + condGreen.Style.Fill.BackgroundColor.Color = ColorTranslator.FromHtml("#A9D08E"); + condGreen.Formula = ws.Cells[6, 8].FullAddressAbsolute.ToString(); + condGreen.Formula2 = ws.Cells[6, 9].FullAddressAbsolute.ToString(); + condGreen.Priority = 104; + + IExcelConditionalFormattingBetween condYellow = ws.ConditionalFormatting.AddBetween(ws.Cells[5, 2, 25, 2]); + condYellow.Style.Fill.PatternType = OfficeOpenXml.Style.ExcelFillStyle.Solid; + condYellow.Style.Fill.BackgroundColor.Color = ColorTranslator.FromHtml("#FFE699"); + condYellow.Formula = ws.Cells[7, 8].FullAddressAbsolute.ToString(); + condYellow.Formula2 = ws.Cells[7, 9].FullAddressAbsolute.ToString(); + condYellow.Priority = 105; + + IExcelConditionalFormattingGreaterThan condRed = ws.ConditionalFormatting.AddGreaterThan(ws.Cells[5, 2, 25, 2]); + condRed.Style.Fill.PatternType = OfficeOpenXml.Style.ExcelFillStyle.Solid; + condRed.Style.Fill.BackgroundColor.Color = ColorTranslator.FromHtml("#FF7979"); + condRed.Formula = ws.Cells[9, 8].FullAddressAbsolute.ToString(); + condRed.Priority = 106; + + SaveAndCleanup(package); + } } } From 52f87c50585a1a40473a6809a82c980cb831b96c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jan=20K=C3=A4llman?= Date: Wed, 8 Apr 2026 15:55:32 +0200 Subject: [PATCH 149/151] WIP:Started work on rendering bar charts --- .../Chart/BarChartTests.cs | 54 ++++++ .../Chart/ColumnChartTests.cs | 4 +- .../BarColumnChartTypeDrawer.cs | 175 ++++++++++++++++-- .../Svg/Chart/SvgChart.cs | 30 ++- src/EPPlus/Drawing/Chart/ExcelChart.cs | 46 ++++- 5 files changed, 272 insertions(+), 37 deletions(-) create mode 100644 src/EPPlus.Export.ImageRenderer.Test/Chart/BarChartTests.cs diff --git a/src/EPPlus.Export.ImageRenderer.Test/Chart/BarChartTests.cs b/src/EPPlus.Export.ImageRenderer.Test/Chart/BarChartTests.cs new file mode 100644 index 000000000..53d5b0b20 --- /dev/null +++ b/src/EPPlus.Export.ImageRenderer.Test/Chart/BarChartTests.cs @@ -0,0 +1,54 @@ +using OfficeOpenXml; +using OfficeOpenXml.Drawing.Chart; + +namespace EPPlus.Export.ImageRenderer.Tests.Chart +{ + [TestClass] + public class BarChartTests : TestBase + { + [TestMethod] + public void GenerateSvgForBarCharts1() + { + ExcelPackage.License.SetNonCommercialOrganization("EPPlus Project"); + using (var p = OpenTemplatePackage("BarChartForSvg.xlsx")) + { + var ws = p.Workbook.Worksheets[0]; + var renderer = new EPPlusImageRenderer.ImageRenderer(); + + //var ix = 1; + //var c = ws.Drawings[ix]; + //var svg = renderer.RenderDrawingToSvg(c); + //SaveTextFileToWorkbook($"svg\\ChartForSvg_ind{ix++}.svg", svg); + + var ix = 0; + foreach (ExcelChart c in ws.Drawings) + { + var svg = renderer.RenderDrawingToSvg(c); + SaveTextFileToWorkbook($"svg\\BarChartForSvg_sheet1_{ix++}.svg", svg); + } + } + } + [TestMethod] + public void GenerateSvgForBarCharts2() + { + ExcelPackage.License.SetNonCommercialOrganization("EPPlus Project"); + using (var p = OpenTemplatePackage("BarChartForSvg.xlsx")) + { + var ws = p.Workbook.Worksheets[1]; + var renderer = new EPPlusImageRenderer.ImageRenderer(); + + //var ix = 2; + //var c = ws.Drawings[ix]; + //var svg = renderer.RenderDrawingToSvg(c); + //SaveTextFileToWorkbook($"svg\\ChartForSvg_ind{ix++}.svg", svg); + + var ix = 0; + foreach (ExcelChart c in ws.Drawings) + { + var svg = renderer.RenderDrawingToSvg(c); + SaveTextFileToWorkbook($"svg\\BarChartForSvg_sheet2_{ix++}.svg", svg); + } + } + } + } +} diff --git a/src/EPPlus.Export.ImageRenderer.Test/Chart/ColumnChartTests.cs b/src/EPPlus.Export.ImageRenderer.Test/Chart/ColumnChartTests.cs index 3253e940e..4a33b919a 100644 --- a/src/EPPlus.Export.ImageRenderer.Test/Chart/ColumnChartTests.cs +++ b/src/EPPlus.Export.ImageRenderer.Test/Chart/ColumnChartTests.cs @@ -29,7 +29,7 @@ public void GenerateSvgForColumnCharts1() foreach (ExcelChart c in ws.Drawings) { var svg = renderer.RenderDrawingToSvg(c); - SaveTextFileToWorkbook($"svg\\BarChartForSvg_sheet1_{ix++}.svg", svg); + SaveTextFileToWorkbook($"svg\\ColumnChartForSvg_sheet1_{ix++}.svg", svg); } } } @@ -51,7 +51,7 @@ public void GenerateSvgForColumnCharts2() foreach (ExcelChart c in ws.Drawings) { var svg = renderer.RenderDrawingToSvg(c); - SaveTextFileToWorkbook($"svg\\BarChartForSvg_sheet2_{ix++}.svg", svg); + SaveTextFileToWorkbook($"svg\\ColumnChartForSvg_sheet2_{ix++}.svg", svg); } } } diff --git a/src/EPPlus.Export.ImageRenderer/Svg/Chart/ChartTypeDrawers/BarColumnChartTypeDrawer.cs b/src/EPPlus.Export.ImageRenderer/Svg/Chart/ChartTypeDrawers/BarColumnChartTypeDrawer.cs index d9c6abe3c..08b4702fb 100644 --- a/src/EPPlus.Export.ImageRenderer/Svg/Chart/ChartTypeDrawers/BarColumnChartTypeDrawer.cs +++ b/src/EPPlus.Export.ImageRenderer/Svg/Chart/ChartTypeDrawers/BarColumnChartTypeDrawer.cs @@ -56,7 +56,14 @@ internal BarColumnChartTypeDrawer(SvgChart svgChart, ExcelBarChart chartType) : var dataPoints = new List(); - AddColumn(chartType, serie, xValues, yValues, dataPoints, count, i); + if (chartType.IsTypeBar()) + { + AddBar(chartType, serie, xValues, yValues, dataPoints, count, i); + } + else + { + AddColumn(chartType, serie, xValues, yValues, dataPoints, count, i); + } dataPointsPerSerie.Add(dataPoints); @@ -75,26 +82,147 @@ internal BarColumnChartTypeDrawer(SvgChart svgChart, ExcelBarChart chartType) : dataLabel.AppendRenderItems(RenderItems); } } - private void AddColumn(ExcelBarChart chartType, ExcelBarChartSerie serie, List> xSeries, List> ySeries, List dataPoints, int seriesCount, int position) - { - SvgChartAxis yAxis, xAxis; - if (chartType.UseSecondaryAxis) + private void AddBar(ExcelBarChart chartType, ExcelBarChartSerie serie, List> xSeries, List> ySeries, List dataPoints, int seriesCount, int position) + { + GetAxis(chartType, out var yAxis, out var xAxis); + + var xValues = xSeries[position]; + var yValues = ySeries[position]; + + var isColumn = yAxis.Axis.AxisPosition == eAxisPosition.Left || yAxis.Axis.AxisPosition == eAxisPosition.Right; + + var slotSize = yValues.Count; + var gapPercent = chartType.GapWidth / 100D; //Gap width between bars/columns in percent + var overlapPercent = chartType.Overlap / 100D; //Overlap between bars/columns in percent + var slotWidth = (isColumn ? _svgChart.Plotarea.Rectangle.Width : _svgChart.Plotarea.Rectangle.Height) / slotSize; + var clusterWidth = slotWidth * 100 / (100 + chartType.GapWidth); + var step = 1 - overlapPercent; + double barWidth; + barWidth = slotWidth / (1 + (seriesCount - 1) * step + gapPercent); + var halfGap = (barWidth * gapPercent) / 2; + + double yAxisStart; + + if (yAxis.Axis.Crosses == eCrosses.AutoZero) { - yAxis = _svgChart.SecondVerticalAxis; - xAxis = _svgChart.SecondHorizontalAxis; - if(xAxis.Axis.Deleted && xAxis.Values==null) - { - xAxis = _svgChart.HorizontalAxis; - } + yAxisStart = yAxis.GetPositionInPlotarea(yAxis.Min <= 0 ? 0D : yAxis.Min, true); + } + else if (yAxis.Axis.Crosses == eCrosses.Min) + { + yAxisStart = yAxis.GetPositionInPlotarea(yAxis.Min, true); } else { - yAxis = _svgChart.VerticalAxis; - xAxis = _svgChart.HorizontalAxis; + yAxisStart = yAxis.GetPositionInPlotarea(yAxis.Max, true); } + + var isStacked = chartType.IsTypeStacked(); + var isStacked100 = chartType.IsTypePercentStacked(); + for (var i = 0; i < yValues.Count; i++) + { + double x; + if (xValues == null || xAxis.Axis.AxisType == eAxisType.Cat) + { + x = (double)i; + } + else + { + x = ConvertUtil.GetValueDouble(xValues[i], false, true); + } + + var y = ConvertUtil.GetValueDouble(yValues[i], false, true); + + var xPos = xAxis.GetPositionInPlotarea(x, true) + halfGap + position * barWidth * step; + var yPos = yAxis.GetPositionInPlotarea(y); + + var rect = new SvgRenderRectItem(ChartRenderer, _svgChart.Plotarea.Rectangle.Bounds); + if (isColumn) + { + rect.Left = xPos; + rect.Width = barWidth; + } + else + { + rect.Top = xPos; + rect.Height = barWidth; + } + + if (position > 0 && (isStacked || isStacked100)) + { + //Stacked + var pYValues = ySeries[position - 1]; + var pAxisEnd = ConvertUtil.GetValueDouble(pYValues[i]); + var yPrevPos = yAxis.GetPositionInPlotarea(pAxisEnd, false); + if (isColumn) + { + if (y < 0) + { + rect.Top = yPrevPos; + rect.Height = yPos - yPrevPos; + } + else + { + rect.Top = yPos; + rect.Height = yPrevPos - yPos; + } + } + else + { + if (y < 0) + { + rect.Left = yPrevPos; + rect.Width = yPos - yPrevPos; + } + else + { + rect.Left = yPos; + rect.Width = yPrevPos - yPos; + } + } + } + else + { + if (isColumn) + { + if (y < 0) + { + rect.Top = yAxisStart; + rect.Height = yPos - yAxisStart; + } + else + { + rect.Top = yPos; + rect.Height = yAxisStart - yPos; + } + } + else + { + if (y < 0) + { + rect.Left = yAxisStart; + rect.Width = yPos - yAxisStart; + } + else + { + rect.Left = yPos; + rect.Width = yAxisStart - yPos; + } + } + } + rect.SetDrawingPropertiesFill(serie.Fill, chartType.StyleManager.Style.SeriesAxis.FillReference.Color); + rect.SetDrawingPropertiesBorder(serie.Border, chartType.StyleManager.Style.SeriesAxis.BorderReference.Color, true); + rect.SetDrawingPropertiesEffects(serie.Effect); + RenderItems.Add(rect); + } + } + private void AddColumn(ExcelBarChart chartType, ExcelBarChartSerie serie, List> xSeries, List> ySeries, List dataPoints, int seriesCount, int position) + { + SvgChartAxis yAxis, xAxis; + GetAxis(chartType, out yAxis, out xAxis); + var xValues = xSeries[position]; var yValues = ySeries[position]; - + var slotSize = yValues.Count; var gapPercent = chartType.GapWidth / 100D; //Gap width between bars/columns in percent var overlapPercent = chartType.Overlap / 100D; //Overlap between bars/columns in percent @@ -142,7 +270,7 @@ private void AddColumn(ExcelBarChart chartType, ExcelBarChartSerie serie, List 0 && (isStacked || isStacked100)) + if (position > 0 && (isStacked || isStacked100)) { //Stacked var pYValues = ySeries[position - 1]; @@ -177,7 +305,24 @@ private void AddColumn(ExcelBarChart chartType, ExcelBarChartSerie serie, List"); - sb.Append($""); - //Write defs used for gradient colors - var writer = new SvgDrawingWriter(this); - writer.WriteSvgDefs(sb, RenderItems); + sb.Append($""); + //Write defs used for gradient colors + var writer = new SvgDrawingWriter(this); + writer.WriteSvgDefs(sb, RenderItems); - foreach (var item in RenderItems) - { - item.Render(sb); - } - - sb.Append(""); + foreach (var item in RenderItems) + { + item.Render(sb); } + + sb.Append(""); } internal double GetPlotAreaTop() diff --git a/src/EPPlus/Drawing/Chart/ExcelChart.cs b/src/EPPlus/Drawing/Chart/ExcelChart.cs index 717eb50fd..766fb0641 100644 --- a/src/EPPlus/Drawing/Chart/ExcelChart.cs +++ b/src/EPPlus/Drawing/Chart/ExcelChart.cs @@ -593,7 +593,7 @@ protected internal bool IsTypeStacked() /// Returns true if the chart is of type clustered /// /// True if the chart is of type clustered - protected bool IsTypeClustered() + protected internal bool IsTypeClustered() { return ChartType == eChartType.BarClustered || ChartType == eChartType.BarClustered3D || @@ -607,6 +607,50 @@ protected bool IsTypeClustered() ChartType == eChartType.PyramidColClustered; } /// + /// Returns true if the chart is of type bar chart + /// + /// True if the chart is of type bar + protected internal bool IsTypeBar() + { + return ChartType == eChartType.BarClustered || + ChartType == eChartType.BarStacked || + ChartType == eChartType.BarStacked100 || + ChartType == eChartType.BarClustered3D || + ChartType == eChartType.BarStacked3D || + ChartType == eChartType.BarStacked1003D || + ChartType == eChartType.ConeBarClustered || + ChartType == eChartType.ConeBarStacked || + ChartType == eChartType.ConeBarStacked100 || + ChartType == eChartType.CylinderBarClustered || + ChartType == eChartType.CylinderBarStacked || + ChartType == eChartType.CylinderBarStacked100 || + ChartType == eChartType.PyramidBarClustered || + ChartType == eChartType.PyramidBarStacked || + ChartType == eChartType.PyramidBarStacked100; + } + /// + /// Returns true if the chart is of type column chart + /// + /// True if the chart is of type column + protected internal bool IsTypeColumn() + { + return ChartType == eChartType.BarClustered || + ChartType == eChartType.ColumnStacked || + ChartType == eChartType.ColumnStacked100 || + ChartType == eChartType.ColumnClustered3D || + ChartType == eChartType.ColumnStacked3D || + ChartType == eChartType.ColumnStacked1003D || + ChartType == eChartType.ConeColClustered || + ChartType == eChartType.ConeColStacked || + ChartType == eChartType.ConeColStacked100 || + ChartType == eChartType.CylinderColClustered || + ChartType == eChartType.CylinderColStacked || + ChartType == eChartType.CylinderColStacked100 || + ChartType == eChartType.PyramidColClustered || + ChartType == eChartType.PyramidColStacked || + ChartType == eChartType.PyramidColStacked100; + } + /// /// Returns true if the chart is a pie or Doughnut chart /// /// True if the chart is a pie or Doughnut chart From 97303d325763d971379a78644c50e3497215c7bd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jan=20K=C3=A4llman?= Date: Thu, 9 Apr 2026 16:32:02 +0200 Subject: [PATCH 150/151] WIP:Work on Column and Bar Charts --- .../Chart/Axis/ValueAxisScaleCalculator.cs | 17 +++- .../BarColumnChartTypeDrawer.cs | 95 ++++++++++++------- .../Svg/Chart/SvgChart.cs | 45 +++++---- .../Svg/Chart/SvgChartAxis.cs | 14 +-- src/EPPlus/Drawing/Chart/ExcelChart.cs | 2 +- 5 files changed, 107 insertions(+), 66 deletions(-) diff --git a/src/EPPlus.Export.ImageRenderer/Svg/Chart/Axis/ValueAxisScaleCalculator.cs b/src/EPPlus.Export.ImageRenderer/Svg/Chart/Axis/ValueAxisScaleCalculator.cs index 7bf7c10fc..ff739ae89 100644 --- a/src/EPPlus.Export.ImageRenderer/Svg/Chart/Axis/ValueAxisScaleCalculator.cs +++ b/src/EPPlus.Export.ImageRenderer/Svg/Chart/Axis/ValueAxisScaleCalculator.cs @@ -105,6 +105,11 @@ private static AxisScale GetNumberScale(ref double dataMin, ref double dataMax, dataMax = axisOptions.LockedMax.Value; } + if (axisOptions.LockedMin.HasValue==false && dataMin / dataMax > 0.666666) + { + dataMin = 0; + } + double dataRange = dataMax - dataMin; double axisMin; @@ -167,11 +172,19 @@ private static AxisScale GetNumberScale(ref double dataMin, ref double dataMax, } else { - // Calculate interval roughInterval = (dataMax - dataMin) / desiredTicks; scaleInterval = GetScaleNumber(roughInterval, true); + if (dataMin / dataMax >= 0.666666666) + { + axisMin = 0; + } + else + { + axisMin = Math.Floor(dataMin / scaleInterval) * scaleInterval; + } + + // Calculate interval - axisMin = dataMin; axisMax = dataMax; } diff --git a/src/EPPlus.Export.ImageRenderer/Svg/Chart/ChartTypeDrawers/BarColumnChartTypeDrawer.cs b/src/EPPlus.Export.ImageRenderer/Svg/Chart/ChartTypeDrawers/BarColumnChartTypeDrawer.cs index 08b4702fb..7f61c2482 100644 --- a/src/EPPlus.Export.ImageRenderer/Svg/Chart/ChartTypeDrawers/BarColumnChartTypeDrawer.cs +++ b/src/EPPlus.Export.ImageRenderer/Svg/Chart/ChartTypeDrawers/BarColumnChartTypeDrawer.cs @@ -21,21 +21,30 @@ internal BarColumnChartTypeDrawer(SvgChart svgChart, ExcelBarChart chartType) : { var groupItem = new SvgGroupItem(ChartRenderer, _svgChart.Plotarea.Rectangle.Bounds); RenderItems.Add(groupItem); - var xValues = new List>(); - var yValues = new List>(); + var catValues = new List>(); + var valValues = new List>(); int serCounter = 0; foreach (ExcelBarChartSerie serie in chartType.Series) { - var yValue = LoadSeriesValues(serie.Series, serie.NumberLiteralsY, serie.StringLiteralsY); - var xValue = LoadSeriesValues(serie.XSeries, serie.NumberLiteralsX, serie.StringLiteralsX); + List valValue,catValue; + //if (chartType.IsTypeColumn()) + //{ + valValue = LoadSeriesValues(serie.Series, serie.NumberLiteralsY, serie.StringLiteralsY); + catValue = LoadSeriesValues(serie.XSeries, serie.NumberLiteralsX, serie.StringLiteralsX); + //} + //else + //{ + // catValue = LoadSeriesValues(serie.Series, serie.NumberLiteralsY, serie.StringLiteralsY); + // valValue = LoadSeriesValues(serie.XSeries, serie.NumberLiteralsX, serie.StringLiteralsX); + //} - xValues.Add(xValue); - yValues.Add(yValue); + catValues.Add(catValue); + valValues.Add(valValue); //if (serie.HasDataLabel) //{ - // var datalabel = new SvgChartSerieDataLabel(svgChart, serie.DataLabel, svgChart.Bounds, serie, xValue, yValue, serCounter); + // var datalabel = new SvgChartSerieDataLabel(svgChart, serie.DataLabel, svgChart.Bounds, serie, catValue, valValue, serCounter); // serieDataLabels.Add(datalabel); //} serCounter++; @@ -43,26 +52,27 @@ internal BarColumnChartTypeDrawer(SvgChart svgChart, ExcelBarChart chartType) : if(chartType.IsTypeStacked()) { - SumSeries(yValues); + SumSeries(valValues); } else if (chartType.IsTypePercentStacked()) { - ExcelChartAxisStandard.CalculateStacked100(yValues); + ExcelChartAxisStandard.CalculateStacked100(valValues); } - var count = Math.Min(xValues.Count, yValues.Count); - for (var i= 0; i < xValues.Count; i++) + + var count = Math.Min(catValues.Count, valValues.Count); + for (var i= 0; i < catValues.Count; i++) { var serie = (ExcelBarChartSerie)chartType.Series[i]; var dataPoints = new List(); - if (chartType.IsTypeBar()) + if (chartType.IsTypeColumn()) { - AddBar(chartType, serie, xValues, yValues, dataPoints, count, i); + AddColumn(chartType, serie, catValues, valValues, dataPoints, count, i); } else { - AddColumn(chartType, serie, xValues, yValues, dataPoints, count, i); + AddBar(chartType, serie, catValues, valValues, dataPoints, count, i); } dataPointsPerSerie.Add(dataPoints); @@ -82,19 +92,34 @@ internal BarColumnChartTypeDrawer(SvgChart svgChart, ExcelBarChart chartType) : dataLabel.AppendRenderItems(RenderItems); } } - private void AddBar(ExcelBarChart chartType, ExcelBarChartSerie serie, List> xSeries, List> ySeries, List dataPoints, int seriesCount, int position) + private void AddBar(ExcelBarChart chartType, ExcelBarChartSerie serie, List> catSeries, List> valSeries, List dataPoints, int seriesCount, int position) { GetAxis(chartType, out var yAxis, out var xAxis); + SvgChartAxis valAx, catAx; - var xValues = xSeries[position]; - var yValues = ySeries[position]; + var isColumn = chartType.IsTypeColumn(); + + if (isColumn) + { + catAx = xAxis; + valAx = yAxis; + } + else + { + catAx = yAxis; + valAx = xAxis; + } + + + var catValues = catSeries[position]; + var valValues = valSeries[position]; - var isColumn = yAxis.Axis.AxisPosition == eAxisPosition.Left || yAxis.Axis.AxisPosition == eAxisPosition.Right; + var yWidth = (isColumn ? _svgChart.Plotarea.Rectangle.Width : _svgChart.Plotarea.Rectangle.Height); - var slotSize = yValues.Count; + var slotSize = valValues.Count; var gapPercent = chartType.GapWidth / 100D; //Gap width between bars/columns in percent var overlapPercent = chartType.Overlap / 100D; //Overlap between bars/columns in percent - var slotWidth = (isColumn ? _svgChart.Plotarea.Rectangle.Width : _svgChart.Plotarea.Rectangle.Height) / slotSize; + var slotWidth = yWidth / slotSize; var clusterWidth = slotWidth * 100 / (100 + chartType.GapWidth); var step = 1 - overlapPercent; double barWidth; @@ -105,35 +130,35 @@ private void AddBar(ExcelBarChart chartType, ExcelBarChartSerie serie, List 0 && (isStacked || isStacked100)) { //Stacked - var pYValues = ySeries[position - 1]; + var pYValues = valSeries[position - 1]; var pAxisEnd = ConvertUtil.GetValueDouble(pYValues[i]); var yPrevPos = yAxis.GetPositionInPlotarea(pAxisEnd, false); if (isColumn) @@ -200,12 +225,12 @@ private void AddBar(ExcelBarChart chartType, ExcelBarChartSerie serie, List 0) @@ -66,16 +54,13 @@ public SvgChart(ExcelChart chart) : base(chart) Legend = null; } - VerticalAxis = new SvgChartAxis(this, (ExcelChartAxisStandard)chart.YAxis); - HorizontalAxis = new SvgChartAxis(this, (ExcelChartAxisStandard)chart.XAxis); - if (chart.Axis.Length > 3) - { - SecondVerticalAxis = new SvgChartAxis(this, (ExcelChartAxisStandard)chart.Axis[2]); - SecondHorizontalAxis = new SvgChartAxis(this, (ExcelChartAxisStandard)chart.Axis[3]); - } - else if(chart.Axis.Length > 2) + HorizontalAxis = GetAxis(false); + VerticalAxis = GetAxis(true); + + if(chart.Axis.Length > 2) { - SecondVerticalAxis = new SvgChartAxis(this, (ExcelChartAxisStandard)chart.Axis[2]); + SecondVerticalAxis = GetAxis(true, 2); + SecondHorizontalAxis = GetAxis(false, 2); } Plotarea = new SvgChartPlotarea(this); @@ -86,6 +71,24 @@ public SvgChart(ExcelChart chart) : base(chart) Plotarea.ChartTypeDrawers = ChartTypeDrawer.Create(this); } + private SvgChartAxis GetAxis(bool vertical, int offset=0) + { + var axis = (ExcelChartAxisStandard)Chart.Axis[offset]; + if(axis.IsVertical==vertical) + { + return new SvgChartAxis(this, axis); + } + else if(Chart.Axis.Length > offset + 1) + { + axis = (ExcelChartAxisStandard)Chart.Axis[offset + 1]; + if(axis.IsVertical==vertical) + { + return new SvgChartAxis(this, axis); + } + } + return null; + } + private void SetAxisPositionsFromPlotarea(SvgChart sc) { if(VerticalAxis != null) diff --git a/src/EPPlus.Export.ImageRenderer/Svg/Chart/SvgChartAxis.cs b/src/EPPlus.Export.ImageRenderer/Svg/Chart/SvgChartAxis.cs index 19046bc74..38d765712 100644 --- a/src/EPPlus.Export.ImageRenderer/Svg/Chart/SvgChartAxis.cs +++ b/src/EPPlus.Export.ImageRenderer/Svg/Chart/SvgChartAxis.cs @@ -67,7 +67,6 @@ internal SvgChartAxis(SvgChart sc, ExcelChartAxisStandard ax) : base(sc) if (ax.Deleted == false) { - Textboxes = new SvgAxisTextBoxes(sc); if (ax.Layout.HasLayout) { Rectangle = GetRectFromManualLayout(sc, ax.Layout); @@ -259,6 +258,8 @@ internal override void AppendRenderItems(List renderItems) internal void AddTickmarksAndValues(List DefItems) { + if (Axis.Deleted == true) return; + if (Axis.MajorTickMark != eAxisTickMark.None) { MajorAxisPositions = AddTickmarks(MajorUnit, MajorDateUnit, double.NaN, 4D.PixelToPoint(), Axis.MajorTickMark); @@ -293,6 +294,7 @@ internal void AddTickmarksAndValues(List DefItems) } if (AxisValues != null && AxisValues.Count > 0 && Axis.Deleted==false && Axis.LabelPosition != eTickLabelPosition.None) { + Textboxes = new SvgAxisTextBoxes(SvgChart); Textboxes.TextBoxes = GetAxisValueTextBoxes(); } } @@ -308,7 +310,6 @@ private List GetAxisValueTextBoxes() double maxWidth, maxHeight; if(Axis.AxisPosition==eAxisPosition.Left || Axis.AxisPosition == eAxisPosition.Right) { - maxWidth = SvgChart.ChartArea.Rectangle.Width / 3; //TODO: Check this value. maxWidth = SvgChart.ChartArea.Rectangle.Width / 3; //TODO: Check this value. maxHeight = Rectangle.Height / AxisValues.Count; } @@ -333,7 +334,6 @@ private List GetAxisValueTextBoxes() double widest=0; for (var i = 0; i < AxisValues.Count; i++) { - var v = AxisValues[i]; var m = tm.MeasureText(v, mf); var ticMarkX = GetAxisItemLeft(i, m); @@ -570,7 +570,7 @@ private double GetAxisItemTop(int i, OfficeOpenXml.Interfaces.Drawing.Text.TextM } else { - var majorHeight = Rectangle.Height / (AxisValues.Count-1); + var majorHeight = Rectangle.Height / (AxisValues.Count); if (Axis.AxisType == eAxisType.Cat) { return Rectangle.Top + majorHeight * (AxisValues.Count - i - 1) + (majorHeight / 2) - m.Height / 2; @@ -627,8 +627,8 @@ private List AddTickmarks(double units, eTimeUnit? dateUnit, case eAxisPosition.Left: y1 = (float)(Rectangle.Top + Rectangle.Height - ((d - min) / diff * Rectangle.Height)); y2 = y1; - x1 = (float)Rectangle.Right - tickMarkWidthInside; - x2 = (float)Rectangle.Right + tickMarkWidthOutside; + x1 = (float)Rectangle.Right - tickMarkWidthOutside; + x2 = (float)Rectangle.Right + tickMarkWidthInside; break; case eAxisPosition.Right: y1 = (float)(Rectangle.Top + Rectangle.Height - ((d - min) / diff * Rectangle.Height)); @@ -882,7 +882,7 @@ protected List GetAxisValue(ExcelChartAxisStandard ax, RenderItem rect, LockedMax = ax.MaxValue, LockedInterval = ax.MajorUnit, LockedIntervalUnit = ax.MajorTimeUnit, - AddPadding = ax.AxisPosition == eAxisPosition.Left || ax.AxisPosition == eAxisPosition.Right, + AddPadding = ax.AxisType!=eAxisType.Cat,//(ax.AxisPosition == eAxisPosition.Left || ax.AxisPosition == eAxisPosition.Right), Axis = ax, IsStacked100 = Chart.IsTypePercentStacked(), ChartSize = rect diff --git a/src/EPPlus/Drawing/Chart/ExcelChart.cs b/src/EPPlus/Drawing/Chart/ExcelChart.cs index 766fb0641..8403e46a8 100644 --- a/src/EPPlus/Drawing/Chart/ExcelChart.cs +++ b/src/EPPlus/Drawing/Chart/ExcelChart.cs @@ -634,7 +634,7 @@ protected internal bool IsTypeBar() /// True if the chart is of type column protected internal bool IsTypeColumn() { - return ChartType == eChartType.BarClustered || + return ChartType == eChartType.ColumnClustered || ChartType == eChartType.ColumnStacked || ChartType == eChartType.ColumnStacked100 || ChartType == eChartType.ColumnClustered3D || From add9357a547258169b457a24c64a79732e341d64 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jan=20K=C3=A4llman?= Date: Fri, 10 Apr 2026 16:22:26 +0200 Subject: [PATCH 151/151] WIP:Fixes for axis on bar charts --- .../Chart/LineChartToSvgTests.cs | 20 +++++----- .../Svg/Chart/Axis/DateAxisScaleCalculator.cs | 38 +++++++++++++++++++ .../BarColumnChartTypeDrawer.cs | 22 ++++------- .../Svg/Chart/SvgChartAxis.cs | 22 +++++++---- 4 files changed, 70 insertions(+), 32 deletions(-) diff --git a/src/EPPlus.Export.ImageRenderer.Test/Chart/LineChartToSvgTests.cs b/src/EPPlus.Export.ImageRenderer.Test/Chart/LineChartToSvgTests.cs index cd707f426..811a568b3 100644 --- a/src/EPPlus.Export.ImageRenderer.Test/Chart/LineChartToSvgTests.cs +++ b/src/EPPlus.Export.ImageRenderer.Test/Chart/LineChartToSvgTests.cs @@ -123,16 +123,16 @@ public void GenerateSvgForCharts_SecondaryAxis_sheet2() { var ws = p.Workbook.Worksheets[1]; var renderer = new EPPlusImageRenderer.ImageRenderer(); - //var ix = 0; - //var c = ws.Drawings[ix]; - //var svg = renderer.RenderDrawingToSvg(c); - //SaveTextFileToWorkbook($"svg\\ChartForSvg_sheet2_{ix++}.svg", svg); - var ix = 1; - foreach (ExcelChart c in ws.Drawings) - { - var svg = renderer.RenderDrawingToSvg(c); - SaveTextFileToWorkbook($"svg\\ChartForSvg_Sheet2_SecAxis{ix++}.svg", svg); - } + var ix = 2; + var c = ws.Drawings[ix]; + var svg = renderer.RenderDrawingToSvg(c); + SaveTextFileToWorkbook($"svg\\ChartForSvg_sheet2_{ix++}.svg", svg); + //var ix = 1; + //foreach (ExcelChart c in ws.Drawings) + //{ + // var svg = renderer.RenderDrawingToSvg(c); + // SaveTextFileToWorkbook($"svg\\ChartForSvg_Sheet2_SecAxis{ix++}.svg", svg); + //} } } [TestMethod] diff --git a/src/EPPlus.Export.ImageRenderer/Svg/Chart/Axis/DateAxisScaleCalculator.cs b/src/EPPlus.Export.ImageRenderer/Svg/Chart/Axis/DateAxisScaleCalculator.cs index 6cff70fbe..429c18413 100644 --- a/src/EPPlus.Export.ImageRenderer/Svg/Chart/Axis/DateAxisScaleCalculator.cs +++ b/src/EPPlus.Export.ImageRenderer/Svg/Chart/Axis/DateAxisScaleCalculator.cs @@ -486,5 +486,43 @@ private static bool FitAsVerticalDiagonalText(double min, double max, int interv } } + + internal static AxisScale CalculateByHeight(double min, double max, ITextMeasurer tm, AxisOptions options) + { + var ax = options.Axis; + var plotAreaHeight = options.ChartSize.Bounds.Height; + var mf = ax.Font.GetMeasureFont(); + int interval; + eTimeUnit unit; + var minString = DateTime.FromOADate(min).ToString(options.NumberFormat); + var res = tm.MeasureText(minString, mf); + if (options.LockedInterval.HasValue) + { + interval = (int)options.LockedInterval.Value; + unit = options.LockedIntervalUnit ?? eTimeUnit.Days; + } + else + { + interval = 1; + unit = eTimeUnit.Days; + //Get interval for maximum width with vertical text. + while(FitAsVerticalDiagonalText(min, max, interval, unit, res.Height, res.Height * 0.3, plotAreaHeight) == false) + { + AddIntervall(ref interval, ref unit); + } + } + + return new AxisScale() + { + MajorInterval = interval, + MinorInterval = 1, + MinorDateUnit = unit, + MajorDateUnit = unit, + Min = min, + Max = max, + TextOrientation = eTextOrientation.Horizontal + }; + + } } } \ No newline at end of file diff --git a/src/EPPlus.Export.ImageRenderer/Svg/Chart/ChartTypeDrawers/BarColumnChartTypeDrawer.cs b/src/EPPlus.Export.ImageRenderer/Svg/Chart/ChartTypeDrawers/BarColumnChartTypeDrawer.cs index 7f61c2482..7f33f9250 100644 --- a/src/EPPlus.Export.ImageRenderer/Svg/Chart/ChartTypeDrawers/BarColumnChartTypeDrawer.cs +++ b/src/EPPlus.Export.ImageRenderer/Svg/Chart/ChartTypeDrawers/BarColumnChartTypeDrawer.cs @@ -66,14 +66,8 @@ internal BarColumnChartTypeDrawer(SvgChart svgChart, ExcelBarChart chartType) : var dataPoints = new List(); - if (chartType.IsTypeColumn()) - { - AddColumn(chartType, serie, catValues, valValues, dataPoints, count, i); - } - else - { - AddBar(chartType, serie, catValues, valValues, dataPoints, count, i); - } + //Add the bar or column. + AddBar(chartType, serie, catValues, valValues, dataPoints, count, i); dataPointsPerSerie.Add(dataPoints); @@ -122,23 +116,21 @@ private void AddBar(ExcelBarChart chartType, ExcelBarChartSerie serie, List GetAxisValueTextBoxes() if(Axis.IsVertical) { x = ticMarkX; - y = ticMarkY - height / 2; + if (SvgChart.Chart.IsTypeBar()) + { + y = ticMarkY; + } + else + { + y = ticMarkY - height / 2; + } } else { @@ -415,7 +422,7 @@ private List GetAxisValueTextBoxes() var p = Axis.TextBody.Paragraphs.FirstOrDefault(); - if (p.HorizontalAlignment != eTextAlignment.Center && (Axis.AxisPosition == eAxisPosition.Bottom || Axis.AxisPosition == eAxisPosition.Top)) + if (p.HorizontalAlignment != eTextAlignment.Center && Axis.AxisType!=eAxisType.Val && (Axis.AxisPosition == eAxisPosition.Bottom || Axis.AxisPosition == eAxisPosition.Top)) { //Horizontal axises are always center aligned visually //Should be broken out as input to ImportParagraph instead of changing the base item @@ -452,7 +459,7 @@ private List GetAxisValueTextBoxes() } } } - else if(LabelOrientation==eTextOrientation.Horizontal) //Only apples when labels are horizontally aligned + else if(LabelOrientation==eTextOrientation.Horizontal && Axis.AxisType==eAxisType.Cat) //Only apples when labels are horizontally aligned { //Align the axis labels according to the label alignment setting. This is only relevant for horizontal axis, vertical axis are always right aligned. var lblAlignment = (Axis as ExcelChartAxisStandard)?.LabelAlignment??OfficeOpenXml.eAxisLabelAlignment.Center; @@ -571,7 +578,7 @@ private double GetAxisItemTop(int i, OfficeOpenXml.Interfaces.Drawing.Text.TextM else { var majorHeight = Rectangle.Height / (AxisValues.Count); - if (Axis.AxisType == eAxisType.Cat) + if (Axis.AxisType == eAxisType.Cat || Axis.AxisType == eAxisType.Date) { return Rectangle.Top + majorHeight * (AxisValues.Count - i - 1) + (majorHeight / 2) - m.Height / 2; } @@ -846,7 +853,7 @@ internal double GetPositionInPlotarea(double val, bool startValue=false) else { if (val < Min || val > Max) return double.NaN; - var diff = Max - Min; + var diff = Max - Min + 1; return (((Max-val) / diff * SvgChart.Plotarea.Rectangle.Height)); } } @@ -882,7 +889,7 @@ protected List GetAxisValue(ExcelChartAxisStandard ax, RenderItem rect, LockedMax = ax.MaxValue, LockedInterval = ax.MajorUnit, LockedIntervalUnit = ax.MajorTimeUnit, - AddPadding = ax.AxisType!=eAxisType.Cat,//(ax.AxisPosition == eAxisPosition.Left || ax.AxisPosition == eAxisPosition.Right), + AddPadding = ax.AxisType==eAxisType.Val,//(ax.AxisPosition == eAxisPosition.Left || ax.AxisPosition == eAxisPosition.Right), Axis = ax, IsStacked100 = Chart.IsTypePercentStacked(), ChartSize = rect @@ -951,7 +958,8 @@ protected List GetAxisValue(ExcelChartAxisStandard ax, RenderItem rect, AxisScale res; if (ax.IsVertical) { - res = DateAxisScaleCalculator.Calculate(min ?? 0, max ?? 0, length, options); + //res = DateAxisScaleCalculator.Calculate(min ?? 0, max ?? 0, length, options); + res = DateAxisScaleCalculator.CalculateByHeight(min ?? 0D, max ?? 0D, SvgChart.TextMeasurer, options); } else {