From 23a7474f1f3e67aeb0479d416e1430916a564aea Mon Sep 17 00:00:00 2001 From: Mats Alm <897655+swmal@users.noreply.github.com> Date: Mon, 10 Aug 2026 07:55:46 +0200 Subject: [PATCH 01/30] #2455 - Addedgit status hook for naming copied pivot tables during worksheet copy (#2457) --- .../Worksheet/ExcelPivotTableCopyEventArgs.cs | 43 ++++++ .../Worksheet/ExcelWorksheetCopyOptions.cs | 11 ++ .../Core/Worksheet/WorksheetCopyHelper.cs | 43 +++++- .../Table/PivotTable/ExcelPivotTable.cs | 13 +- .../Core/Worksheet/CopyWorksheetTests.cs | 138 ++++++++++++++++++ 5 files changed, 239 insertions(+), 9 deletions(-) create mode 100644 src/EPPlus/Core/Worksheet/ExcelPivotTableCopyEventArgs.cs diff --git a/src/EPPlus/Core/Worksheet/ExcelPivotTableCopyEventArgs.cs b/src/EPPlus/Core/Worksheet/ExcelPivotTableCopyEventArgs.cs new file mode 100644 index 0000000000..cf29d2404f --- /dev/null +++ b/src/EPPlus/Core/Worksheet/ExcelPivotTableCopyEventArgs.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 +************************************************************************************************* + 08/05/2026 EPPlus Software AB Added +*************************************************************************************************/ +namespace OfficeOpenXml.Core.Worksheet +{ + /// + /// Provides context for a pivot table that is being copied to a new worksheet, and allows + /// a custom name to be assigned to the copied pivot table. + /// + public class ExcelPivotTableCopyEventArgs + { + /// + /// The name of the pivot table on the source worksheet. + /// + public string SourceTableName { get; internal set; } + + /// + /// The name that was assigned to the copied pivot table by default, before this handler + /// runs. When the worksheet is copied within the same workbook, this is a generated name + /// (PivotTable1, PivotTable2, ...). When copied to another workbook, the original name is + /// kept when it is still available, in which case this equals ; + /// if a pivot table with that name already exists in the target workbook, a generated name + /// is used instead. + /// + public string DefaultName { get; internal set; } + + /// + /// The name to assign to the copied pivot table. Leave as null to keep . + /// Setting this to an existing pivot table name will cause the same validation exception + /// as a normal pivot table name assignment. + /// + public string NewName { get; set; } + } +} \ No newline at end of file diff --git a/src/EPPlus/Core/Worksheet/ExcelWorksheetCopyOptions.cs b/src/EPPlus/Core/Worksheet/ExcelWorksheetCopyOptions.cs index 4848995483..7743905f16 100644 --- a/src/EPPlus/Core/Worksheet/ExcelWorksheetCopyOptions.cs +++ b/src/EPPlus/Core/Worksheet/ExcelWorksheetCopyOptions.cs @@ -31,5 +31,16 @@ public class ExcelWorksheetCopyOptions /// formula references are updated and name uniqueness is validated. /// public Action TableCopyHandler { get; set; } + + /// + /// A handler that is invoked for each pivot table that is copied to the new worksheet. + /// Use this to assign a custom name to the copied pivot table. When a worksheet is copied + /// within the same workbook, copied pivot tables are otherwise given a generated name + /// (PivotTable1, PivotTable2, ...). Set + /// on the argument to rename the copied pivot table. The rename is applied through the same + /// path as a normal + /// assignment, so name uniqueness is validated. + /// + public Action PivotTableCopyHandler { get; set; } } } diff --git a/src/EPPlus/Core/Worksheet/WorksheetCopyHelper.cs b/src/EPPlus/Core/Worksheet/WorksheetCopyHelper.cs index 05cdf6a85e..08ebc5aa80 100644 --- a/src/EPPlus/Core/Worksheet/WorksheetCopyHelper.cs +++ b/src/EPPlus/Core/Worksheet/WorksheetCopyHelper.cs @@ -117,9 +117,10 @@ internal static ExcelWorksheet Copy(ExcelWorksheets targetWorksheets, string nam copiedTableNames = CopyTable(sourceWorksheet, targetWorksheet); } + Dictionary copiedPivotTableNames = null; if (sourceWorksheet.PivotTables.Count > 0) { - CopyPivotTable(sourceWorksheet, targetWorksheet); + copiedPivotTableNames = CopyPivotTable(sourceWorksheet, targetWorksheet); } CopyDefinedNames(sourceWorksheet, targetWorksheet); @@ -188,7 +189,7 @@ internal static ExcelWorksheet Copy(ExcelWorksheets targetWorksheets, string nam //CopyDxfStyles and the slicer copy, which resolve the copied tables //by their default name. ApplyTableCopyOptions(targetWorksheet, options, copiedTableNames); - + ApplyPivotTableCopyOptions(targetWorksheet, options, copiedPivotTableNames); return targetWorksheet; } @@ -227,6 +228,41 @@ private static void ApplyTableCopyOptions(ExcelWorksheet added, ExcelWorksheetCo } } + private static void ApplyPivotTableCopyOptions(ExcelWorksheet added, ExcelWorksheetCopyOptions options, Dictionary copiedPivotTableNames) + { + if (options == null || options.PivotTableCopyHandler == null || copiedPivotTableNames == null) + { + return; + } + + foreach (var pair in copiedPivotTableNames) + { + var sourceTableName = pair.Key; + var defaultName = pair.Value; + var copiedPivotTable = added.PivotTables[defaultName]; + if (copiedPivotTable == null) + { + continue; + } + + var args = new ExcelPivotTableCopyEventArgs + { + SourceTableName = sourceTableName, + DefaultName = defaultName + }; + options.PivotTableCopyHandler.Invoke(args); + + if (!string.IsNullOrEmpty(args.NewName) && args.NewName != defaultName) + { + //Route through the ExcelPivotTable.Name setter so name uniqueness is + //validated, exactly as for a normal rename. Pivot table references in + //GETPIVOTDATA are address based, not name based, so no formula + //adjustment is required. + copiedPivotTable.Name = args.NewName; + } + } + } + private static void SetTableFunction(ExcelWorksheet added) { foreach (var t in added.Tables) @@ -1185,7 +1221,7 @@ private static List> CopyTable(ExcelWorksheet sourc return copiedTableNames; } - private static void CopyPivotTable(ExcelWorksheet sourceWs, ExcelWorksheet destWs) + private static Dictionary CopyPivotTable(ExcelWorksheet sourceWs, ExcelWorksheet destWs) { sourceWs._package.Workbook.ReadAllPivotTables(); string prevName = ""; @@ -1274,6 +1310,7 @@ private static void CopyPivotTable(ExcelWorksheet sourceWs, ExcelWorksheet destW } //Can't have a cell selected when "group editing" avoids pop-up by not selecting sheet. destWs.View.SetTabSelected(false); + return nameMap; } private static void CreateCacheInNewPackage(ExcelWorksheet sourceWs, ExcelPivotTable tbl, ZipPackagePart partTbl) diff --git a/src/EPPlus/Table/PivotTable/ExcelPivotTable.cs b/src/EPPlus/Table/PivotTable/ExcelPivotTable.cs index 955e57540d..c39a5726cc 100644 --- a/src/EPPlus/Table/PivotTable/ExcelPivotTable.cs +++ b/src/EPPlus/Table/PivotTable/ExcelPivotTable.cs @@ -266,7 +266,8 @@ private void CreatePivotTable(ExcelWorksheet sheet, ExcelAddressBase address, in { LoadXmlSafe(PivotTableXml, copy.PivotTableXml.OuterXml, Encoding.UTF8); TopNode = PivotTableXml.DocumentElement; - Name = name; + SetXmlNodeString(NAME_PATH, name); + SetXmlNodeString(DISPLAY_NAME_PATH, CleanDisplayName(name)); } PivotTableUri = GetNewUri(pck, "/xl/pivotTables/pivotTable{0}.xml", ref tblId); @@ -361,16 +362,16 @@ public string Name } set { - if (WorkSheet.Workbook.ExistsTableName(value)) + if (WorkSheet.Workbook.ExistsPivotTableName(value)) { throw (new ArgumentException("PivotTable name is not unique")); } string prevName = Name; - if (WorkSheet.Tables._tableNames.ContainsKey(prevName)) + if (WorkSheet.PivotTables._pivotTableNames.ContainsKey(prevName)) { - int ix = WorkSheet.Tables._tableNames[prevName]; - WorkSheet.Tables._tableNames.Remove(prevName); - WorkSheet.Tables._tableNames.Add(value, ix); + int ix = WorkSheet.PivotTables._pivotTableNames[prevName]; + WorkSheet.PivotTables._pivotTableNames.Remove(prevName); + WorkSheet.PivotTables._pivotTableNames.Add(value, ix); } SetXmlNodeString(NAME_PATH, value); SetXmlNodeString(DISPLAY_NAME_PATH, CleanDisplayName(value)); diff --git a/src/EPPlusTest/Core/Worksheet/CopyWorksheetTests.cs b/src/EPPlusTest/Core/Worksheet/CopyWorksheetTests.cs index 2780e91ced..9c0855d411 100644 --- a/src/EPPlusTest/Core/Worksheet/CopyWorksheetTests.cs +++ b/src/EPPlusTest/Core/Worksheet/CopyWorksheetTests.cs @@ -20,6 +20,19 @@ private static ExcelPackage CreatePackageWithTable(out ExcelWorksheet source) return package; } + private static ExcelPackage CreatePackageWithPivotTable(out ExcelWorksheet source, string pivotName) + { + var package = new ExcelPackage(); + source = package.Workbook.Worksheets.Add("Template"); + var range = LoadItemData(source); + var pt = source.PivotTables.Add(source.Cells["A1"], range, pivotName); + pt.RowFields.Add(pt.Fields[1]); + pt.DataFields.Add(pt.Fields[3]); + return package; + } + + #region Copy with Tables + [TestMethod] public void Copy_WithTableCopyHandler_RenamesCopiedTable() { @@ -373,5 +386,130 @@ public void Copy_TableCopyHandler_NewNameCollidesWithExistingTable_Throws() }); } } + + #endregion + + #region Copy with pivot tables + [TestMethod] + public void Copy_WithoutHandler_AssignsGeneratedPivotTableName() + { + using (var package = CreatePackageWithPivotTable(out var source, "SalesPivot")) + { + var copy = package.Workbook.Worksheets.Copy(source.Name, "Copy"); + + Assert.AreEqual(1, copy.PivotTables.Count); + //Same workbook copy renames the copied pivot table to a generated name. + Assert.AreNotEqual("SalesPivot", copy.PivotTables[0].Name); + } + } + + [TestMethod] + public void Copy_WithPivotTableCopyHandler_RenamesCopiedPivotTable() + { + using (var package = CreatePackageWithPivotTable(out var source, "SalesPivot")) + { + var copy = package.Workbook.Worksheets.Copy(source.Name, "BaltimoreMD", options => + { + options.PivotTableCopyHandler = args => + { + args.NewName = "BaltimoreMD_" + args.SourceTableName; + }; + }); + + Assert.AreEqual(1, copy.PivotTables.Count); + Assert.IsNotNull(copy.PivotTables["BaltimoreMD_SalesPivot"]); + } + } + + [TestMethod] + public void Copy_WithPivotTableCopyHandler_ProvidesSourceAndDefaultName() + { + using (var package = CreatePackageWithPivotTable(out var source, "SalesPivot")) + { + string capturedSourceName = null; + string capturedDefaultName = null; + + package.Workbook.Worksheets.Copy(source.Name, "Copy", options => + { + options.PivotTableCopyHandler = args => + { + capturedSourceName = args.SourceTableName; + capturedDefaultName = args.DefaultName; + }; + }); + + Assert.AreEqual("SalesPivot", capturedSourceName); + Assert.IsNotNull(capturedDefaultName); + } + } + + [TestMethod] + public void Copy_PivotTableCopyHandler_NullNewName_KeepsDefaultName() + { + using (var package = CreatePackageWithPivotTable(out var source, "SalesPivot")) + { + string defaultName = null; + + var copy = package.Workbook.Worksheets.Copy(source.Name, "Copy", options => + { + options.PivotTableCopyHandler = args => + { + defaultName = args.DefaultName; + // NewName left null. + }; + }); + + Assert.IsNotNull(copy.PivotTables[defaultName]); + } + } + + [TestMethod] + public void Copy_PivotTableCopyHandler_RenameToExistingName_Throws() + { + using (var package = CreatePackageWithPivotTable(out var source, "SalesPivot")) + { + //A second pivot table in the workbook whose name we will collide with. + var other = package.Workbook.Worksheets.Add("Other"); + var otherPt = other.PivotTables.Add(other.Cells["A1"], source.Cells["K1:N11"], "ExistingPivot"); + otherPt.RowFields.Add(otherPt.Fields[1]); + otherPt.DataFields.Add(otherPt.Fields[3]); + + Assert.ThrowsExactly(() => + { + package.Workbook.Worksheets.Copy(source.Name, "Copy", options => + { + options.PivotTableCopyHandler = args => + { + args.NewName = "ExistingPivot"; + }; + }); + }); + } + } + + [TestMethod] + public void Copy_PivotTableCopyHandler_GetPivotDataStillResolvesAfterRename() + { + using (var package = CreatePackageWithPivotTable(out var source, "SalesPivot")) + { + //GETPIVOTDATA references the pivot by cell address, not by name, so a rename + //must not break the copied formula's resolution. + source.Cells["H1"].Formula = "GETPIVOTDATA(\"Stock\",$A$1)"; + + var copy = package.Workbook.Worksheets.Copy(source.Name, "Copy", options => + { + options.PivotTableCopyHandler = args => + { + args.NewName = "RenamedPivot"; + }; + }); + + //The copied formula is unchanged (address based) and the pivot was renamed. + Assert.AreEqual("GETPIVOTDATA(\"Stock\",$A$1)", copy.Cells["H1"].Formula); + Assert.IsNotNull(copy.PivotTables["RenamedPivot"]); + } + } + + #endregion } } \ No newline at end of file From 9a1bc62120b5f082d558d23c5b6e036d7e724aba Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ossian=20Edstr=C3=B6m?= Date: Tue, 11 Aug 2026 15:28:23 +0200 Subject: [PATCH 02/30] Base theme fallback system functional for borders --- .../Chart/ChartStyleFallbackTest.cs | 53 +++++++--- .../Drawing/Chart/ExcelChartStandard.cs | 25 +++-- .../Chart/Style/ExcelChartStyleManager.cs | 18 +++- src/EPPlus/Drawing/Renderer/ChartRenderer.cs | 97 ++++++++++++++++++- .../DrawingRenderItemExtentions.cs | 65 ++----------- 5 files changed, 179 insertions(+), 79 deletions(-) diff --git a/src/EPPlus.DrawingRenderer.Tests/Chart/ChartStyleFallbackTest.cs b/src/EPPlus.DrawingRenderer.Tests/Chart/ChartStyleFallbackTest.cs index 3697cedf92..3bbdcbd7d9 100644 --- a/src/EPPlus.DrawingRenderer.Tests/Chart/ChartStyleFallbackTest.cs +++ b/src/EPPlus.DrawingRenderer.Tests/Chart/ChartStyleFallbackTest.cs @@ -76,8 +76,8 @@ public void ReadEmptyDefaultChartStyle() var svg = c.ToSvg(); SaveTextFileToWorkbook($"svg\\emptyDefaultStyle{ws.Name}_{c.Name}.svg", svg); } - GetOutputFile("StyleExamples", ""); - SaveAndCleanup(p); + var fi = GetOutputFile("StyleExamples", "emptyDefault_out.xlsx"); + p.SaveAs(fi); } } @@ -107,9 +107,8 @@ public void RemovedStyles() SaveTextFileToWorkbook($"svg\\{fileName}_{ws.Name}_{c.Name}.svg", svg); } } - GetOutputFile("StyleExamples", ""); - SaveAndCleanup(p); - + var fi = GetOutputFile("StyleExamples", $"{fileName}_Out.xlsx"); + p.SaveAs(fi); } } @@ -139,9 +138,8 @@ public void EditedTheme() SaveTextFileToWorkbook($"svg\\{fileName}_{ws.Name}_{c.Name}.svg", svg); } } - GetOutputFile("StyleExamples", ""); - SaveAndCleanup(p); - + var fi = GetOutputFile("StyleExamples", $"{fileName}_Out.xlsx"); + p.SaveAs(fi); } } @@ -171,8 +169,8 @@ public void ManualSystemText() SaveTextFileToWorkbook($"svg\\{fileName}_{ws.Name}_{c.Name}.svg", svg); } } - GetOutputFile("StyleExamples", ""); - SaveAndCleanup(p); + var fi = GetOutputFile("StyleExamples", $"{fileName}_Out.xlsx"); + p.SaveAs(fi); } } @@ -202,8 +200,39 @@ public void ExcelThemeLnDeleted() SaveTextFileToWorkbook($"svg\\{fileName}_{ws.Name}_{c.Name}.svg", svg); } } - GetOutputFile("StyleExamples", ""); - SaveAndCleanup(p); + var fi = GetOutputFile("StyleExamples", $"{fileName}_Out.xlsx"); + p.SaveAs(fi); + } + } + + + [TestMethod] + public void PureExcelTheme() + { + ExcelPackage.License.SetNonCommercialOrganization("EPPlus Project"); + + string fileName = "PureExcelTheme"; + + using (var p = OpenTemplatePackage($"StyleExamples\\{fileName}.xlsx")) + { + var ws = p.Workbook.Worksheets[0]; + + foreach (var d in ws.Drawings) + { + if (d is ExcelChart c) + { + var borderSetting = c.Border; + var borderDirectColor = borderSetting.Fill.Color; + var theme = p.Workbook.ThemeManager.GetOrCreateTheme(); + + var defaultColorFromTheme = theme.ColorScheme.Dark1; + + var svg = c.ToSvg(); + SaveTextFileToWorkbook($"svg\\{fileName}_{ws.Name}_{c.Name}.svg", svg); + } + } + var fi = GetOutputFile("StyleExamples", $"{fileName}_Out.xlsx"); + p.SaveAs(fi); } } } diff --git a/src/EPPlus/Drawing/Chart/ExcelChartStandard.cs b/src/EPPlus/Drawing/Chart/ExcelChartStandard.cs index f114b2480d..e6aa520296 100644 --- a/src/EPPlus/Drawing/Chart/ExcelChartStandard.cs +++ b/src/EPPlus/Drawing/Chart/ExcelChartStandard.cs @@ -832,18 +832,27 @@ public override eChartStyle Style XmlNode node = ChartXml.SelectSingleNode("c:chartSpace/c:style/@val", NameSpaceManager); if (node == null) { - return eChartStyle.None; - } - else - { - if (int.TryParse(node.Value, NumberStyles.Number, CultureInfo.InvariantCulture, out int v)) + //Check if an alternateContent node contains the style node + //TODO: Handle fallback of AlternateContent + node = ChartXml.SelectSingleNode("c:chartSpace/mc:AlternateContent/mc:Choice/c14:style/@val", NameSpaceManager); + if(node == null) { - return (eChartStyle)v; + return eChartStyle.None; } - else + } + + if (int.TryParse(node.Value, NumberStyles.Number, CultureInfo.InvariantCulture, out int v)) + { + //Default + if(v == 102) { - return eChartStyle.None; + return eChartStyle.Style102; } + return (eChartStyle)v; + } + else + { + return eChartStyle.None; } } set diff --git a/src/EPPlus/Drawing/Chart/Style/ExcelChartStyleManager.cs b/src/EPPlus/Drawing/Chart/Style/ExcelChartStyleManager.cs index 558dcad1c2..8665788c4d 100644 --- a/src/EPPlus/Drawing/Chart/Style/ExcelChartStyleManager.cs +++ b/src/EPPlus/Drawing/Chart/Style/ExcelChartStyleManager.cs @@ -42,15 +42,31 @@ internal ExcelChartStyleManager(XmlNamespaceManager nameSpaceManager, ExcelChart { _chart = chart; LoadStyleAndColors(chart); + _theme = chart.WorkSheet.Workbook.ThemeManager; + bool loadStyleAndColorsFromDefault = false; if (StylePart != null) { Style = new ExcelChartStyle(nameSpaceManager, StyleXml.DocumentElement, this); } + else if(chart.Style != eChartStyle.None) + { + //LoadStyles(); + //if (StyleLibrary.ContainsKey((int)chart.Style)) + //{ + // loadStyleAndColorsFromDefault = true; + //} + } if (ColorsPart != null) { ColorsManager = new ExcelChartColorsManager(nameSpaceManager, ColorsXml.DocumentElement); } - _theme = chart.WorkSheet.Workbook.ThemeManager; + + if(loadStyleAndColorsFromDefault) + { + ////In this case the style and colors are already applied so we just want to read the data in without applying the style + LoadStyleAndColorsXml(StyleLibrary[(int)chart.Style].XmlDocument, eChartStyle.Style2, null); + //SetChartStyle((int)chart.Style); + } } /// /// A library where chart styles can be loaded for easier access. diff --git a/src/EPPlus/Drawing/Renderer/ChartRenderer.cs b/src/EPPlus/Drawing/Renderer/ChartRenderer.cs index 960ec58a85..97d512d847 100644 --- a/src/EPPlus/Drawing/Renderer/ChartRenderer.cs +++ b/src/EPPlus/Drawing/Renderer/ChartRenderer.cs @@ -21,6 +21,7 @@ Date Author Change using EPPlusImageRenderer.Svg; using OfficeOpenXml.Drawing; using OfficeOpenXml.Drawing.Chart; +using OfficeOpenXml.Drawing.Theme; using OfficeOpenXml.FormulaParsing.Excel.Functions.MathFunctions; using OfficeOpenXml.FormulaParsing.Excel.Functions.Text; using OfficeOpenXml.Style; @@ -28,6 +29,7 @@ Date Author Change using System.Collections.Generic; using System.Drawing; using System.Runtime.InteropServices; +using System.Security.Cryptography.Xml; using System.Text; using d=OfficeOpenXml.Drawing.Renderer; using tc = OfficeOpenXml.Utils.TypeConversion; @@ -331,17 +333,106 @@ private void SetChartArea(SvgRenderOptions options) //var themeColor = tc.ColorConverter.GetThemeColor(Theme, Chart.StyleManager.Style?.ChartArea.BorderReference.Color); //var borderFill = Chart.StyleManager.Style.ChartArea.Border.Fill; var test = Chart.Border.Fill; + + var styleType = Chart.Style; + var myStyleManager = Chart.StyleManager; //Chart.StyleManager.load - //var chartStyleId = Chart.StyleManager.Style.Id; + //var chartStyleId = Chart.StyleManager.; //Chart.StyleManager.SetChartStyle(202); - item.Rectangle.ResolveStyleFallbackChainBorder(Chart, Theme, Chart.StyleManager.Style?.ChartArea.BorderReference, Chart.Border, 0.75d); - + + Color? themeColor = null; + + //if (Chart.StyleManager == null && styleType != eChartStyle.None) + //{ + // var styleId = (int)styleType; + // if (styleId > (int)eChartStyle.Style48) + // { + // styleId = (int)eChartStyle.Style2; + // } + // //From table2 Default Line Formatting Per Chart Style + // if(styleId <= 40) + // { + // //AKA dk1 + // themeColor = tc.ColorConverter.GetThemeColor(Theme, eThemeSchemeColor.Text1); + // themeColor = tc.ColorConverter.ApplyTint(themeColor.Value, 0.75d); + // var themedLine = Theme.FormatScheme.BorderStyle[0]; + // themedLine.Fill.Color = themeColor.Value; + // } + // else + // { + // //41-48 + // //aka light1 + // themeColor = tc.ColorConverter.GetThemeColor(Theme, eThemeSchemeColor.Background1); + // } + //} + + var reference = Chart.StyleManager.Style?.ChartArea.BorderReference; + + item.Rectangle.ResolveStyleFallbackChainBorder( + Chart, + Theme, + reference, + Chart.Border, + 1d, + () => GetChartAreaDefaultColor((int)styleType, out ExcelThemeLine themedLine)); + //item.Rectangle.SetDrawingPropertiesBorder(Theme, Chart.Border, Chart.StyleManager.Style?.ChartArea.BorderReference.Color, Chart.Border.IsEmpty || Chart.Border.Width > 0, item.DefaultBorderColor, 0.75, UserSpaceSettings.UserSpaceOnUse_Global, Chart.Style); item.Rectangle.RoundedCornerRadius = Chart.RoundedCorners ? 9 : 0; item.AppendRenderItems(RenderItems); item.SetMargins(Chart.TextBody); ChartArea = item; } + + private Color? GetChartAreaDefaultColor(int styleId, out ExcelThemeLine themedLine) + { + themedLine = null; + Color? themeColor = null; + styleId = styleId > (int)eChartStyle.Style48 ? (int)eChartStyle.Style2 : styleId; + + if(styleId == 0) + { + return Color.Empty; + } + + + themedLine = Theme.FormatScheme.BorderStyle[0]; + + //TODO: Fix for colortypes other than solidFill + themeColor = tc.ColorConverter.GetThemeColor(Theme, themedLine.Fill.SolidFill.Color); + + //From table2 Default Line Formatting Per Chart Style + if (styleId <= 40) + { + ////AKA dk1 (in standard case) + //themeColor = tc.ColorConverter.GetThemeColor(Theme, eThemeSchemeColor.Text1); + //var tintedColor = tc.ColorConverter.ApplyTintDrawing(themeColor.Value, 0.3d); + ////////Supposedly 75% tint of tx1 + ////var themedColorAlt = tc.ColorConverter.ApplyTintDrawing(themeColor.Value, (1-0.6d)); + ////var tintedFill = tc.ColorConverter.GetThemeColor(Theme, themedLine.Fill.SolidFill.Color); + + if(themedLine.Fill.SolidFill.Color.ColorType == eDrawingColorType.Scheme && themedLine.Fill.SolidFill.Color.SchemeColor.Color == eSchemeColor.Style) + { + themeColor = tc.ColorConverter.GetThemeColor(Theme, eThemeSchemeColor.Text1); + var tintedColor = tc.ColorConverter.ApplyTintDrawing(themeColor.Value, 0.3d); + + var prevColor = themedLine.Fill.SolidFill.Color; + var colorPrev = themedLine.Fill.Color; + + themedLine.Fill.SolidFill.Color.SetRgbColor(themeColor.Value); + themedLine.Fill.SolidFill.Color.Transforms.AddTint(30d); + + themeColor = themedLine.Fill.Color; + } + } + else + { + //41-48 + //aka light1 + themeColor = tc.ColorConverter.GetThemeColor(Theme, eThemeSchemeColor.Background1); + } + return themeColor; + } + private ChartAxisRenderer GetAxis(bool vertical, int offset = 0) { var axis = (ExcelChartAxisStandard)Chart.Axis[offset]; diff --git a/src/EPPlus/Drawing/Renderer/RenderItems/DrawingRenderItemExtentions.cs b/src/EPPlus/Drawing/Renderer/RenderItems/DrawingRenderItemExtentions.cs index ec89e05273..5eae642cd5 100644 --- a/src/EPPlus/Drawing/Renderer/RenderItems/DrawingRenderItemExtentions.cs +++ b/src/EPPlus/Drawing/Renderer/RenderItems/DrawingRenderItemExtentions.cs @@ -90,61 +90,16 @@ private static Color GetSchemeColor(ExcelTheme theme, eSchemeColor schemeColor = return tc.ColorConverter.GetThemeColor(bg1); } - private static Color? GetFillColorFromTheme(ExcelTheme theme, int themeLstIdx) + private static Color? GetFillColorFromTheme(ExcelTheme theme, Func GetDefaultThemeColor) { - Color? fc = null; - - //There is no Style-Specified color. Or rather. There is no styleSheet inside of the Chart folder. Themed Fill should be applied if it exists - //Fallback to theme - if (theme.FormatScheme.BackgroundFillStyle != null) - { - ExcelDrawingFill themeFill = null; - - if(themeLstIdx == 0) - { - themeFill = theme.FormatScheme.BackgroundFillStyle[0]; - } - else if(themeLstIdx == 1) - { - var bStyle = theme.FormatScheme.BorderStyle[0]; - themeFill = bStyle.Fill; - } - - if (themeFill.IsEmpty == false) - { - if (themeFill.Style == eFillStyle.SolidFill) - { - if (themeFill.SolidFill.Color.ColorType == eDrawingColorType.Scheme) - { - var col = GetSchemeColor(theme, eSchemeColor.Dark1); - //var castInt = (int)(255d * 0.78d); - //fc = Color.FromArgb(castInt, col); - - //if (themeFill.SolidFill.Color.SchemeColor.Color == eSchemeColor.Style) - //{ - // //The definition of this elements color is based on the style of the sheet between 1-48 - - // //eChartStyle.Style2 - //} - //else - //{ - // fc = GetSchemeColor(theme, eSchemeColor.Dark1); - //} - } - } - } + Color? fc = GetDefaultThemeColor(); - if (fc == null) - { - //Bg1 or alternatively accent 1 - fc = themeFill.Color; - } - return fc; - } - else + if (fc.HasValue == false) { - return Color.Empty; + //Bg1 or alternatively accent 1 + fc = theme.FormatScheme.BackgroundFillStyle[0].Color; } + return fc; } private static Color? GetFillColorFromReference(ExcelChartStyleReference reference, ExcelTheme theme, ExcelDrawingFillBasic fill) @@ -174,7 +129,7 @@ private static Color GetSchemeColor(ExcelTheme theme, eSchemeColor schemeColor = return null; } - private static string GetFillColorNew(ExcelTheme theme, ExcelDrawingBorder border, ExcelChartStyleReference reference, PathFillMode colorSource, out double opacity, int themeLstIdx = 0) + private static string GetFillColorNew(ExcelTheme theme, ExcelDrawingBorder border, ExcelChartStyleReference reference, PathFillMode colorSource, out double opacity, Func GetDefaultThemeColor) { Color? fc = null; @@ -189,7 +144,7 @@ private static string GetFillColorNew(ExcelTheme theme, ExcelDrawingBorder borde { //Move on to 3. Theme - fc = GetFillColorFromTheme(theme, themeLstIdx); + fc = GetFillColorFromTheme(theme, GetDefaultThemeColor); } } @@ -229,7 +184,7 @@ private static string GetAdjustmentsAndTransparency(Color fc, PathFillMode color return "#" + fc.ToArgb().ToString("x8").Substring(2); } - internal static void ResolveStyleFallbackChainBorder(this RenderItem item, ExcelChart chart, ExcelTheme theme, ExcelChartStyleReference reference, ExcelDrawingBorder border, double opacity) + internal static void ResolveStyleFallbackChainBorder(this RenderItem item, ExcelChart chart, ExcelTheme theme, ExcelChartStyleReference reference, ExcelDrawingBorder border, double opacity, Func GetDefaultThemeColor) { //The Fallback chain of styles for drawing objects is: //1. Chart.Border (make sure to note the chart style ID @@ -244,7 +199,7 @@ internal static void ResolveStyleFallbackChainBorder(this RenderItem item, Excel if (border.Fill.IsEmpty) { //Fallback to style hierarhy (options 2, 3 or 4) - item.BorderColor = GetFillColorNew(theme, border, reference, item.BorderColorSource, out opacity, 1); + item.BorderColor = GetFillColorNew(theme, border, reference, item.BorderColorSource, out opacity, GetDefaultThemeColor); //item.BorderColorSource = PathFillMode.Lighten; } else From 6d69be0fc7d5902f24aaecdf9d9b1a7bc0f00b8d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ossian=20Edstr=C3=B6m?= Date: Wed, 12 Aug 2026 16:51:53 +0200 Subject: [PATCH 03/30] Progress on reasoning on true fallback color --- .../Chart/ChartStyleFallbackTest.cs | 16 ++++- src/EPPlus/Drawing/Renderer/ChartRenderer.cs | 67 ++++++++++++------- 2 files changed, 59 insertions(+), 24 deletions(-) diff --git a/src/EPPlus.DrawingRenderer.Tests/Chart/ChartStyleFallbackTest.cs b/src/EPPlus.DrawingRenderer.Tests/Chart/ChartStyleFallbackTest.cs index 3bbdcbd7d9..f692df6f70 100644 --- a/src/EPPlus.DrawingRenderer.Tests/Chart/ChartStyleFallbackTest.cs +++ b/src/EPPlus.DrawingRenderer.Tests/Chart/ChartStyleFallbackTest.cs @@ -80,13 +80,27 @@ public void ReadEmptyDefaultChartStyle() p.SaveAs(fi); } } + [TestMethod] + public void GenerateSimpleChart() + { + ExcelPackage.License.SetNonCommercialOrganization("EPPlus Project"); + + string fileName = "EpplusSimpleChart"; + + using (var p = OpenPackage($"{fileName}.xlsx",true)) + { + var ws = p.Workbook.Worksheets.Add("s1"); + ws.Drawings.AddBarChart("simpleChart", eBarChartType.ColumnClustered); + + SaveAndCleanup(p); + } + } [TestMethod] public void RemovedStyles() { ExcelPackage.License.SetNonCommercialOrganization("EPPlus Project"); - string fileName = "emptyManuallyRemovedLnStyles"; using (var p = OpenTemplatePackage($"StyleExamples\\{fileName}.xlsx")) diff --git a/src/EPPlus/Drawing/Renderer/ChartRenderer.cs b/src/EPPlus/Drawing/Renderer/ChartRenderer.cs index 97d512d847..08fe654ae7 100644 --- a/src/EPPlus/Drawing/Renderer/ChartRenderer.cs +++ b/src/EPPlus/Drawing/Renderer/ChartRenderer.cs @@ -389,49 +389,70 @@ private void SetChartArea(SvgRenderOptions options) Color? themeColor = null; styleId = styleId > (int)eChartStyle.Style48 ? (int)eChartStyle.Style2 : styleId; - if(styleId == 0) + if (styleId == 0) { return Color.Empty; } - themedLine = Theme.FormatScheme.BorderStyle[0]; + var bg = Theme.FormatScheme.BackgroundFillStyle[0]; //TODO: Fix for colortypes other than solidFill themeColor = tc.ColorConverter.GetThemeColor(Theme, themedLine.Fill.SolidFill.Color); //From table2 Default Line Formatting Per Chart Style - if (styleId <= 40) + + ////AKA dk1 (in standard case) + //themeColor = tc.ColorConverter.GetThemeColor(Theme, eThemeSchemeColor.Text1); + //var tintedColor = tc.ColorConverter.ApplyTintDrawing(themeColor.Value, 0.3d); + ////////Supposedly 75% tint of tx1 + ////var themedColorAlt = tc.ColorConverter.ApplyTintDrawing(themeColor.Value, (1-0.6d)); + ////var tintedFill = tc.ColorConverter.GetThemeColor(Theme, themedLine.Fill.SolidFill.Color); + + if (themedLine.Fill.SolidFill.Color.ColorType == eDrawingColorType.Scheme && themedLine.Fill.SolidFill.Color.SchemeColor.Color == eSchemeColor.Style) { - ////AKA dk1 (in standard case) - //themeColor = tc.ColorConverter.GetThemeColor(Theme, eThemeSchemeColor.Text1); - //var tintedColor = tc.ColorConverter.ApplyTintDrawing(themeColor.Value, 0.3d); - ////////Supposedly 75% tint of tx1 - ////var themedColorAlt = tc.ColorConverter.ApplyTintDrawing(themeColor.Value, (1-0.6d)); - ////var tintedFill = tc.ColorConverter.GetThemeColor(Theme, themedLine.Fill.SolidFill.Color); - - if(themedLine.Fill.SolidFill.Color.ColorType == eDrawingColorType.Scheme && themedLine.Fill.SolidFill.Color.SchemeColor.Color == eSchemeColor.Style) + if (styleId <= 40) { + //Text1 AKA dk1 (in standard case) themeColor = tc.ColorConverter.GetThemeColor(Theme, eThemeSchemeColor.Text1); - var tintedColor = tc.ColorConverter.ApplyTintDrawing(themeColor.Value, 0.3d); - var prevColor = themedLine.Fill.SolidFill.Color; - var colorPrev = themedLine.Fill.Color; + var test = tc.ColorConverter.GetThemeColor(Theme, eThemeSchemeColor.Accent1); + var shadedTest = tc.ColorConverter.ApplyTint(test, 0.15d); + //themeColor = tc.ColorConverter.GetThemeColor(Theme, eThemeSchemeColor.Background1); + + if (themedLine.Fill.SolidFill.Color.Transforms.Count > 0) + { + themeColor = tc.ColorConverter.ApplyTransforms(themeColor.Value, themedLine.Fill.SolidFill.Color.Transforms); + } + else + { + Color clr = Color.FromArgb(255, 128, 128, 128); + var tstClr = tc.ColorConverter.ApplyTintDrawing(themeColor.Value, 0.25d); + //Default value- Should arguably be 0.75% tint themeColor but something is strange... + //It appears closer to 50 in this specific case + themeColor = tc.ColorConverter.ApplyTintDrawing(themeColor.Value, 0.5375d); + } + ////var tintedColor = tc.ColorConverter.ApplyTintDrawing(themeColor.Value, 0.3d); + + ////var prevColor = themedLine.Fill.SolidFill.Color; + ////var colorPrev = themedLine.Fill.Color; - themedLine.Fill.SolidFill.Color.SetRgbColor(themeColor.Value); - themedLine.Fill.SolidFill.Color.Transforms.AddTint(30d); + ////themedLine.Fill.SolidFill.Color.SetRgbColor(themeColor.Value); + ////themedLine.Fill.SolidFill.Color.Transforms.AddTint(30d); - themeColor = themedLine.Fill.Color; + //themeColor = themedLine.Fill.Color; + } + else + { + //41-48 + //aka light1 + themeColor = tc.ColorConverter.GetThemeColor(Theme, eThemeSchemeColor.Background1); + themedLine = null; } - } - else - { - //41-48 - //aka light1 - themeColor = tc.ColorConverter.GetThemeColor(Theme, eThemeSchemeColor.Background1); } return themeColor; } + private ChartAxisRenderer GetAxis(bool vertical, int offset = 0) { From 5c0dc636b55616f0962d4df86f10e0212aedc47c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ossian=20Edstr=C3=B6m?= Date: Thu, 13 Aug 2026 16:49:20 +0200 Subject: [PATCH 04/30] Added check for applying tint. we do inverse --- .../Chart/ChartStyleFallbackTest.cs | 35 ++++++++++++++++--- src/EPPlus/Drawing/Renderer/ChartRenderer.cs | 31 ++++------------ .../Coloring/ExcelColorTransformCollection.cs | 2 +- .../Utils/TypeConversion/ColorConverter.cs | 10 +++--- 4 files changed, 44 insertions(+), 34 deletions(-) diff --git a/src/EPPlus.DrawingRenderer.Tests/Chart/ChartStyleFallbackTest.cs b/src/EPPlus.DrawingRenderer.Tests/Chart/ChartStyleFallbackTest.cs index f692df6f70..9bb4e0b422 100644 --- a/src/EPPlus.DrawingRenderer.Tests/Chart/ChartStyleFallbackTest.cs +++ b/src/EPPlus.DrawingRenderer.Tests/Chart/ChartStyleFallbackTest.cs @@ -2,8 +2,10 @@ using OfficeOpenXml.Drawing.Chart; using System; using System.Collections.Generic; +using System.Drawing; using System.Linq; using System.Text; +using static OfficeOpenXml.Drawing.OleObject.Structures.OleObjectDataStructures; namespace EPPlus.DrawingRenderer.Tests.Chart { @@ -96,6 +98,31 @@ public void GenerateSimpleChart() } } + [TestMethod] + public void ReadChartBorderThemeTint() + { + ExcelPackage.License.SetNonCommercialOrganization("EPPlus Project"); + + var fileName = "ChartBorderThemeTint"; + + using (var p = OpenTemplatePackage($"StyleExamples\\{fileName}.xlsx")) + { + var ws = p.Workbook.Worksheets[0]; + var lChart = ws.Drawings[0].As.Chart.LineChart; + + lChart.StyleManager.Style.ChartArea.Border.Fill.SolidFill.Color.SetSchemeColor(OfficeOpenXml.Drawing.eSchemeColor.Accent1); + + //100 - input is what excel seems to apply + //lChart.StyleManager.Style.ChartArea.BorderReference.Color.Transforms.AddTint(13); + lChart.StyleManager.Style.ChartArea.Border.Fill.SolidFill.Color.Transforms.AddTint(60); + lChart.StyleManager.Style.ChartArea.Border.Width = 10d; + lChart.StyleManager.ApplyStyles(); + + var fi = GetOutputFile("StyleExamples", $"{fileName}_Out.xlsx"); + p.SaveAs(fi); + } + } + [TestMethod] public void RemovedStyles() { @@ -111,11 +138,11 @@ public void RemovedStyles() { if (d is ExcelChart c) { - var borderSetting = c.Border; - var borderDirectColor = borderSetting.Fill.Color; - var theme = p.Workbook.ThemeManager.GetOrCreateTheme(); + //var borderSetting = c.Border; + //var borderDirectColor = borderSetting.Fill.Color; + //var theme = p.Workbook.ThemeManager.GetOrCreateTheme(); - var defaultColorFromTheme = theme.ColorScheme.Dark1; + //var defaultColorFromTheme = theme.ColorScheme.Dark1; var svg = c.ToSvg(); SaveTextFileToWorkbook($"svg\\{fileName}_{ws.Name}_{c.Name}.svg", svg); diff --git a/src/EPPlus/Drawing/Renderer/ChartRenderer.cs b/src/EPPlus/Drawing/Renderer/ChartRenderer.cs index 08fe654ae7..93be27a29d 100644 --- a/src/EPPlus/Drawing/Renderer/ChartRenderer.cs +++ b/src/EPPlus/Drawing/Renderer/ChartRenderer.cs @@ -400,15 +400,6 @@ private void SetChartArea(SvgRenderOptions options) //TODO: Fix for colortypes other than solidFill themeColor = tc.ColorConverter.GetThemeColor(Theme, themedLine.Fill.SolidFill.Color); - //From table2 Default Line Formatting Per Chart Style - - ////AKA dk1 (in standard case) - //themeColor = tc.ColorConverter.GetThemeColor(Theme, eThemeSchemeColor.Text1); - //var tintedColor = tc.ColorConverter.ApplyTintDrawing(themeColor.Value, 0.3d); - ////////Supposedly 75% tint of tx1 - ////var themedColorAlt = tc.ColorConverter.ApplyTintDrawing(themeColor.Value, (1-0.6d)); - ////var tintedFill = tc.ColorConverter.GetThemeColor(Theme, themedLine.Fill.SolidFill.Color); - if (themedLine.Fill.SolidFill.Color.ColorType == eDrawingColorType.Scheme && themedLine.Fill.SolidFill.Color.SchemeColor.Color == eSchemeColor.Style) { if (styleId <= 40) @@ -416,9 +407,7 @@ private void SetChartArea(SvgRenderOptions options) //Text1 AKA dk1 (in standard case) themeColor = tc.ColorConverter.GetThemeColor(Theme, eThemeSchemeColor.Text1); - var test = tc.ColorConverter.GetThemeColor(Theme, eThemeSchemeColor.Accent1); - var shadedTest = tc.ColorConverter.ApplyTint(test, 0.15d); - //themeColor = tc.ColorConverter.GetThemeColor(Theme, eThemeSchemeColor.Background1); + //var bg1Col = tc.ColorConverter.GetThemeColor(Theme, eThemeSchemeColor.Background1); if (themedLine.Fill.SolidFill.Color.Transforms.Count > 0) { @@ -426,21 +415,15 @@ private void SetChartArea(SvgRenderOptions options) } else { - Color clr = Color.FromArgb(255, 128, 128, 128); - var tstClr = tc.ColorConverter.ApplyTintDrawing(themeColor.Value, 0.25d); + ////Color clr = Color.FromArgb(255, 128, 128, 128); + //var tstClr = tc.ColorConverter.ApplyTintDrawing(themeColor.Value, (1d -2.5d)); //Default value- Should arguably be 0.75% tint themeColor but something is strange... //It appears closer to 50 in this specific case - themeColor = tc.ColorConverter.ApplyTintDrawing(themeColor.Value, 0.5375d); + //It also appears to be tx1 (black) and apply color and tint 0.25 in vba but for us it's 0.5372d... + //0.5372 is however consistent with 137/255 and 137 is our expected result. + themeColor = tc.ColorConverter.ApplyTint(themeColor.Value, 0.5372d); } - ////var tintedColor = tc.ColorConverter.ApplyTintDrawing(themeColor.Value, 0.3d); - - ////var prevColor = themedLine.Fill.SolidFill.Color; - ////var colorPrev = themedLine.Fill.Color; - - ////themedLine.Fill.SolidFill.Color.SetRgbColor(themeColor.Value); - ////themedLine.Fill.SolidFill.Color.Transforms.AddTint(30d); - - //themeColor = themedLine.Fill.Color; + //themedLine.Fill.SolidFill.Color.Transforms.AddTint } else { diff --git a/src/EPPlus/Drawing/Style/Coloring/ExcelColorTransformCollection.cs b/src/EPPlus/Drawing/Style/Coloring/ExcelColorTransformCollection.cs index 9c31948d8a..e2424e1d28 100644 --- a/src/EPPlus/Drawing/Style/Coloring/ExcelColorTransformCollection.cs +++ b/src/EPPlus/Drawing/Style/Coloring/ExcelColorTransformCollection.cs @@ -310,7 +310,7 @@ public void AddTint(double value) AddValue("tint", eColorTransformType.Tint, value); } /// - /// Specifies a lighter version of its input color + /// Specifies a darker version of its input color /// /// The tint value in percentage 0-100 public void AddShade(double value) diff --git a/src/EPPlus/Utils/TypeConversion/ColorConverter.cs b/src/EPPlus/Utils/TypeConversion/ColorConverter.cs index f5ed9dff9c..762b61ab5f 100644 --- a/src/EPPlus/Utils/TypeConversion/ColorConverter.cs +++ b/src/EPPlus/Utils/TypeConversion/ColorConverter.cs @@ -246,7 +246,7 @@ internal static Color ApplyTintDrawing(Color ret, double tint) //} if (tint < 0) { - double shade = 1 + tint; + double shade = 1d + tint; var r = (byte)Math.Round(ret.R * shade); var g = (byte)Math.Round(ret.G * shade); var b = (byte)Math.Round(ret.B * shade); @@ -254,10 +254,10 @@ internal static Color ApplyTintDrawing(Color ret, double tint) } else if (tint > 0) { - double blend = 1.0 - tint; - var r = (byte)Math.Round(ret.R + (255 - ret.R) * blend); - var g = (byte)Math.Round(ret.G + (255 - ret.G) * blend); - var b = (byte)Math.Round(ret.B + (255 - ret.B) * blend); + double blend = 1.0d - tint; + var r = (byte)Math.Round(ret.R + (255d - ret.R) * blend); + var g = (byte)Math.Round(ret.G + (255d - ret.G) * blend); + var b = (byte)Math.Round(ret.B + (255d - ret.B) * blend); return Color.FromArgb(ret.A, r, g, b); } return ret; From 999ee21215fb281f73a366ad10fc7194a2838c37 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ossian=20Edstr=C3=B6m?= Date: Fri, 14 Aug 2026 16:23:23 +0200 Subject: [PATCH 05/30] A version that passes all test cases.Problem:Magic numbers --- .../Chart/ChartStyleFallbackTest.cs | 23 +++++++++++++------ src/EPPlus/Drawing/Renderer/ChartRenderer.cs | 10 +++++++- src/EPPlus/Drawing/Theme/ExcelThemeLine.cs | 14 +++++++++++ .../Utils/TypeConversion/ColorConverter.cs | 2 +- 4 files changed, 40 insertions(+), 9 deletions(-) diff --git a/src/EPPlus.DrawingRenderer.Tests/Chart/ChartStyleFallbackTest.cs b/src/EPPlus.DrawingRenderer.Tests/Chart/ChartStyleFallbackTest.cs index 9bb4e0b422..3ba1c3dba5 100644 --- a/src/EPPlus.DrawingRenderer.Tests/Chart/ChartStyleFallbackTest.cs +++ b/src/EPPlus.DrawingRenderer.Tests/Chart/ChartStyleFallbackTest.cs @@ -1,11 +1,10 @@ using OfficeOpenXml; +using OfficeOpenXml.Drawing; using OfficeOpenXml.Drawing.Chart; using System; using System.Collections.Generic; using System.Drawing; -using System.Linq; -using System.Text; -using static OfficeOpenXml.Drawing.OleObject.Structures.OleObjectDataStructures; +using tc = OfficeOpenXml.Utils.TypeConversion; namespace EPPlus.DrawingRenderer.Tests.Chart { @@ -114,6 +113,8 @@ public void ReadChartBorderThemeTint() //100 - input is what excel seems to apply //lChart.StyleManager.Style.ChartArea.BorderReference.Color.Transforms.AddTint(13); + + //Adding Less Tint makes the object Lighter. Which is the inverse of how excel does it. lChart.StyleManager.Style.ChartArea.Border.Fill.SolidFill.Color.Transforms.AddTint(60); lChart.StyleManager.Style.ChartArea.Border.Width = 10d; lChart.StyleManager.ApplyStyles(); @@ -177,6 +178,14 @@ public void EditedTheme() var svg = c.ToSvg(); SaveTextFileToWorkbook($"svg\\{fileName}_{ws.Name}_{c.Name}.svg", svg); + + var theme = p.Workbook.ThemeManager.GetOrCreateTheme(); + var themeColor = tc.ColorConverter.GetThemeColor(theme, eThemeSchemeColor.Text1); + var themedLine = theme.FormatScheme.BorderStyle[0]; + //themeColor = tc.ColorConverter.ApplyTransforms(themeColor, themedLine.Fill.SolidFill.Color.Transforms); + themeColor = tc.ColorConverter.ApplyTintDrawing(themeColor, 0.285d); + var ExpectedColor = Color.FromArgb(255, 255, 199, 199); + Assert.AreEqual(ExpectedColor.ToArgb(), themeColor.ToArgb()); } } var fi = GetOutputFile("StyleExamples", $"{fileName}_Out.xlsx"); @@ -231,11 +240,11 @@ public void ExcelThemeLnDeleted() { if (d is ExcelChart c) { - var borderSetting = c.Border; - var borderDirectColor = borderSetting.Fill.Color; - var theme = p.Workbook.ThemeManager.GetOrCreateTheme(); + //var borderSetting = c.Border; + //var borderDirectColor = borderSetting.Fill.Color; + //var theme = p.Workbook.ThemeManager.GetOrCreateTheme(); - var defaultColorFromTheme = theme.ColorScheme.Dark1; + //var defaultColorFromTheme = theme.ColorScheme.Dark1; var svg = c.ToSvg(); SaveTextFileToWorkbook($"svg\\{fileName}_{ws.Name}_{c.Name}.svg", svg); diff --git a/src/EPPlus/Drawing/Renderer/ChartRenderer.cs b/src/EPPlus/Drawing/Renderer/ChartRenderer.cs index 93be27a29d..80c06f777c 100644 --- a/src/EPPlus/Drawing/Renderer/ChartRenderer.cs +++ b/src/EPPlus/Drawing/Renderer/ChartRenderer.cs @@ -397,6 +397,11 @@ private void SetChartArea(SvgRenderOptions options) themedLine = Theme.FormatScheme.BorderStyle[0]; var bg = Theme.FormatScheme.BackgroundFillStyle[0]; + if(themedLine.HasFill == false) + { + //Node exists but has no fill. Excel considers this the same as transparent/noFill + return Color.Transparent; + } //TODO: Fix for colortypes other than solidFill themeColor = tc.ColorConverter.GetThemeColor(Theme, themedLine.Fill.SolidFill.Color); @@ -411,7 +416,10 @@ private void SetChartArea(SvgRenderOptions options) if (themedLine.Fill.SolidFill.Color.Transforms.Count > 0) { - themeColor = tc.ColorConverter.ApplyTransforms(themeColor.Value, themedLine.Fill.SolidFill.Color.Transforms); + //had to guess/solve equation for values. According to excel it should still be 75%(0.25) but our calc is off bc of rounding or smth. + themeColor = tc.ColorConverter.ApplyTintDrawing(themeColor.Value, 0.285d); + //Arguably we should apply all transforms instead but even in this case if there is no ln node found in style it appears to default to 75% despite a scheme color existing in the theme + //themeColor = tc.ColorConverter.ApplyTransforms(themeColor.Value, themedLine.Fill.SolidFill.Color.Transforms); } else { diff --git a/src/EPPlus/Drawing/Theme/ExcelThemeLine.cs b/src/EPPlus/Drawing/Theme/ExcelThemeLine.cs index b0155d55f0..2a3935637d 100644 --- a/src/EPPlus/Drawing/Theme/ExcelThemeLine.cs +++ b/src/EPPlus/Drawing/Theme/ExcelThemeLine.cs @@ -98,6 +98,19 @@ public ePenAlignment Alignment } } ExcelDrawingFill _fill = null; + + public bool HasFill + { + get + { + if (_fill != null || ((TopNode.ChildNodes.Count > 0) && TopNode.ChildNodes[0].LocalName.EndsWith("Fill"))) + { + return true; + } + return false; + } + } + /// /// Access to fill properties /// @@ -113,6 +126,7 @@ public ExcelDrawingFill Fill } else { + //TODO: Checking this should not create the node. Many of our getters still create nodes. They should not. var node = CreateNode("a:solidFill"); _fill = new ExcelDrawingFill(_theme, NameSpaceManager, TopNode.ChildNodes[0], "", SchemaNodeOrder); Fill.SolidFill.Color.SetSchemeColor(eSchemeColor.Style); diff --git a/src/EPPlus/Utils/TypeConversion/ColorConverter.cs b/src/EPPlus/Utils/TypeConversion/ColorConverter.cs index 762b61ab5f..841c74a3ca 100644 --- a/src/EPPlus/Utils/TypeConversion/ColorConverter.cs +++ b/src/EPPlus/Utils/TypeConversion/ColorConverter.cs @@ -80,7 +80,7 @@ internal static Color ApplyTransforms(Color c, ExcelColorTransformCollection tra c = ApplyTintDrawing(c, -(1-v)); break; case eColorTransformType.Tint: - c = ApplyTintDrawing(c, v); + c = ApplyTintDrawing(c, 1 - v); break; case eColorTransformType.HueMod: c = ApplyHueMod(c, v); From 621dc5b53e6e21d9ab0838c00dc57aa132f386f7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ossian=20Edstr=C3=B6m?= Date: Fri, 14 Aug 2026 16:52:47 +0200 Subject: [PATCH 06/30] Started adding a system with less magic numbers --- src/EPPlus/Drawing/Renderer/ChartRenderer.cs | 1 + .../Utils/TypeConversion/ColorConverter.cs | 23 +++++++++++++++++++ 2 files changed, 24 insertions(+) diff --git a/src/EPPlus/Drawing/Renderer/ChartRenderer.cs b/src/EPPlus/Drawing/Renderer/ChartRenderer.cs index 80c06f777c..2868026932 100644 --- a/src/EPPlus/Drawing/Renderer/ChartRenderer.cs +++ b/src/EPPlus/Drawing/Renderer/ChartRenderer.cs @@ -416,6 +416,7 @@ private void SetChartArea(SvgRenderOptions options) if (themedLine.Fill.SolidFill.Color.Transforms.Count > 0) { + var testAlternative = tc.ColorConverter.AlternativeTint(themeColor.Value, 0.25d); //had to guess/solve equation for values. According to excel it should still be 75%(0.25) but our calc is off bc of rounding or smth. themeColor = tc.ColorConverter.ApplyTintDrawing(themeColor.Value, 0.285d); //Arguably we should apply all transforms instead but even in this case if there is no ln node found in style it appears to default to 75% despite a scheme color existing in the theme diff --git a/src/EPPlus/Utils/TypeConversion/ColorConverter.cs b/src/EPPlus/Utils/TypeConversion/ColorConverter.cs index 841c74a3ca..332eb384fc 100644 --- a/src/EPPlus/Utils/TypeConversion/ColorConverter.cs +++ b/src/EPPlus/Utils/TypeConversion/ColorConverter.cs @@ -225,6 +225,29 @@ internal static Color ApplyTint(Color ret, double tint) //} //return ret; } + + internal static Color AlternativeTint(Color ret, double tint) + { + if (tint < 0) + { + double shade = 1d + tint; + var r = (byte)Math.Round(ret.R * shade); + var g = (byte)Math.Round(ret.G * shade); + var b = (byte)Math.Round(ret.B * shade); + return Color.FromArgb(ret.A, r, g, b); + } + else if (tint > 0) + { + double blend = 1.0d - tint; + //Docs state 10% input means A 10% tint is 10% of the input color combined with 90% white + var r = (byte)Math.Round(ret.R * tint + (254.3d * blend)); + var g = (byte)Math.Round(ret.G * tint + (254.3d * blend)); + var b = (byte)Math.Round(ret.B * tint + (254.3d * blend)); + return Color.FromArgb(ret.A, r, g, b); + } + return ret; + } + internal static Color ApplyTintDrawing(Color ret, double tint) { //if (tint == 0) From 393fe18d1690dae2bd29b5f6d2fcab106e89ce35 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ossian=20Edstr=C3=B6m?= Date: Mon, 17 Aug 2026 09:59:23 +0200 Subject: [PATCH 07/30] Ensured directory is created for tests --- .../Chart/ChartStyleFallbackTest.cs | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/src/EPPlus.DrawingRenderer.Tests/Chart/ChartStyleFallbackTest.cs b/src/EPPlus.DrawingRenderer.Tests/Chart/ChartStyleFallbackTest.cs index 3ba1c3dba5..0ed6683b30 100644 --- a/src/EPPlus.DrawingRenderer.Tests/Chart/ChartStyleFallbackTest.cs +++ b/src/EPPlus.DrawingRenderer.Tests/Chart/ChartStyleFallbackTest.cs @@ -17,6 +17,7 @@ public void EpplusGeneratedChart() { ExcelPackage.License.SetNonCommercialOrganization("EPPlus Project"); + CreatePathIfNotExists("StyleExamples\\"); using (var p = OpenPackage("StyleExamples\\epplusDefaultTest.xlsx",true)) { @@ -63,6 +64,7 @@ public void ReadEmptyDefaultChartStyle() { ExcelPackage.License.SetNonCommercialOrganization("EPPlus Project"); + CreatePathIfNotExists("StyleExamples\\"); using (var p = OpenTemplatePackage("StyleExamples\\emptyDefault.xlsx")) { @@ -103,6 +105,7 @@ public void ReadChartBorderThemeTint() ExcelPackage.License.SetNonCommercialOrganization("EPPlus Project"); var fileName = "ChartBorderThemeTint"; + CreatePathIfNotExists("StyleExamples\\"); using (var p = OpenTemplatePackage($"StyleExamples\\{fileName}.xlsx")) { @@ -130,6 +133,7 @@ public void RemovedStyles() ExcelPackage.License.SetNonCommercialOrganization("EPPlus Project"); string fileName = "emptyManuallyRemovedLnStyles"; + CreatePathIfNotExists("StyleExamples\\"); using (var p = OpenTemplatePackage($"StyleExamples\\{fileName}.xlsx")) { @@ -159,6 +163,7 @@ public void EditedTheme() { ExcelPackage.License.SetNonCommercialOrganization("EPPlus Project"); + CreatePathIfNotExists("StyleExamples\\"); string fileName = "ExcelThemeEdited"; @@ -201,6 +206,8 @@ public void ManualSystemText() string fileName = "ExcelThemeManualSystemText"; + CreatePathIfNotExists("StyleExamples\\"); + using (var p = OpenTemplatePackage($"StyleExamples\\{fileName}.xlsx")) { var ws = p.Workbook.Worksheets[0]; @@ -230,6 +237,8 @@ public void ExcelThemeLnDeleted() { ExcelPackage.License.SetNonCommercialOrganization("EPPlus Project"); + CreatePathIfNotExists("StyleExamples\\"); + string fileName = "ExcelThemeLnDeleted"; using (var p = OpenTemplatePackage($"StyleExamples\\{fileName}.xlsx")) @@ -263,6 +272,8 @@ public void PureExcelTheme() string fileName = "PureExcelTheme"; + CreatePathIfNotExists("StyleExamples\\"); + using (var p = OpenTemplatePackage($"StyleExamples\\{fileName}.xlsx")) { var ws = p.Workbook.Worksheets[0]; From 0e8358729558075a7da8031afb59ec17542dc4b4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jan=20K=C3=A4llman?= Date: Mon, 17 Aug 2026 11:16:33 +0200 Subject: [PATCH 08/30] Fixes issue 2459 (#2461) * Fixes issue #2459. Fix for copying tables/pivottables within a worksheet. * Fix for issue #2463 * Fix for issue #2463 * Cleaned up test * Moved Range Dictionary lookup into SaveWorkbook in RpnFormulaExecute --- src/EPPlus/Core/CellStore/RangeHashset.cs | 4 + .../Core/Worksheet/WorksheetCopyHelper.cs | 32 ++++---- .../FormulaParsing/CalculateExtensions.cs | 2 +- .../DependencyChain/RpnFormulaExecution.cs | 77 ++++++++++++++++--- .../Functions/FunctionParameterInformation.cs | 4 + .../Functions/MathFunctions/AverageIfs.cs | 2 +- .../Excel/Functions/MathFunctions/CountIfs.cs | 2 +- .../MathFunctions/RangeCriteriaFunction.cs | 16 ++-- .../Excel/Functions/MathFunctions/SumIfs.cs | 2 +- .../Issues/FormulaCalculationIssues.cs | 23 +++++- 10 files changed, 125 insertions(+), 39 deletions(-) diff --git a/src/EPPlus/Core/CellStore/RangeHashset.cs b/src/EPPlus/Core/CellStore/RangeHashset.cs index e3df3b93f2..d919f4d88b 100644 --- a/src/EPPlus/Core/CellStore/RangeHashset.cs +++ b/src/EPPlus/Core/CellStore/RangeHashset.cs @@ -136,6 +136,10 @@ internal bool Merge(ref FormulaRangeAddress newAddress) { var spillRanges = new List(); byte isAdded = 0; + if(newAddress.FromCol < 1 || newAddress.FromRow < 1) + { + return false; + } for (int c = newAddress.FromCol; c <= newAddress.ToCol; c++) { var rowSpan = (((long)newAddress.FromRow - 1) << 20) | ((long)newAddress.ToRow - 1); diff --git a/src/EPPlus/Core/Worksheet/WorksheetCopyHelper.cs b/src/EPPlus/Core/Worksheet/WorksheetCopyHelper.cs index 08ebc5aa80..7e2c8c54de 100644 --- a/src/EPPlus/Core/Worksheet/WorksheetCopyHelper.cs +++ b/src/EPPlus/Core/Worksheet/WorksheetCopyHelper.cs @@ -111,13 +111,11 @@ internal static ExcelWorksheet Copy(ExcelWorksheets targetWorksheets, string nam CopySlicers(sourceWorksheet, targetWorksheet); CopyDrawing(sourceWorksheet, targetWorksheet); } - List> copiedTableNames = null; + Dictionary copiedTableNames = null, copiedPivotTableNames=null; if (sourceWorksheet.Tables.Count > 0) { - copiedTableNames = CopyTable(sourceWorksheet, targetWorksheet); - } + copiedTableNames = CopyTable(sourceWorksheet, targetWorksheet); } - Dictionary copiedPivotTableNames = null; if (sourceWorksheet.PivotTables.Count > 0) { copiedPivotTableNames = CopyPivotTable(sourceWorksheet, targetWorksheet); @@ -143,7 +141,7 @@ internal static ExcelWorksheet Copy(ExcelWorksheets targetWorksheets, string nam //Copy dfx styles used in conditional formatting. if (!(sourceWorksheet.Workbook == targetWorksheet.Workbook)) { - CopyDxfStyles(sourceWorksheet, targetWorksheet); + CopyDxfStyles(sourceWorksheet, targetWorksheet, copiedTableNames, copiedPivotTableNames); } //Copy the VBA code @@ -193,7 +191,7 @@ internal static ExcelWorksheet Copy(ExcelWorksheets targetWorksheets, string nam return targetWorksheet; } - private static void ApplyTableCopyOptions(ExcelWorksheet added, ExcelWorksheetCopyOptions options, List> copiedTableNames) + private static void ApplyTableCopyOptions(ExcelWorksheet added, ExcelWorksheetCopyOptions options, Dictionary copiedTableNames) { if (options == null || options.TableCopyHandler == null || copiedTableNames == null) { @@ -1022,6 +1020,7 @@ private static void CopyDefinedNames(ExcelWorksheet Copy, ExcelWorksheet added) wbName.IsNameHidden = name.IsNameHidden; } } + //Copy names from formulas. if (sameWorkbook == false) { @@ -1101,9 +1100,9 @@ private static bool HasExternalReference(string formula) return false; } - private static List> CopyTable(ExcelWorksheet sourceWs, ExcelWorksheet destWs) + private static Dictionary CopyTable(ExcelWorksheet sourceWs, ExcelWorksheet destWs) { - var copiedTableNames = new List>(); + var copiedTableNames = new Dictionary(); string prevName = ""; //First copy the table XML foreach (var tbl in sourceWs.Tables) @@ -1137,7 +1136,7 @@ private static List> CopyTable(ExcelWorksheet sourc int Id = destWs.Workbook._nextTableID++; prevName = name; - copiedTableNames.Add(new KeyValuePair(tbl.Name, name)); + copiedTableNames.Add(tbl.Name, name); XmlDocument xmlDoc = new XmlDocument(); xmlDoc.LoadXml(xml); @@ -1310,6 +1309,7 @@ private static Dictionary CopyPivotTable(ExcelWorksheet sourceWs } //Can't have a cell selected when "group editing" avoids pop-up by not selecting sheet. destWs.View.SetTabSelected(false); + return nameMap; } @@ -1381,32 +1381,32 @@ private static void ChangeToWsLocalPivotTable(ExcelWorksheet sourceWs, Dictionar } } } - private static void CopyDxfStyles(ExcelWorksheet sourceWs, ExcelWorksheet destWs) + private static void CopyDxfStyles(ExcelWorksheet sourceWs, ExcelWorksheet destWs, Dictionary copiedTableNames, Dictionary copiedPivotTableNames) { //DxfStyleHandler.UpdateDxfXml(copy.Workbook); var dxfStyleCashe = new Dictionary(); - CopyDxfStylesTables(sourceWs, destWs); - CopyDxfStylesPivotTables(sourceWs, destWs, dxfStyleCashe); + CopyDxfStylesTables(sourceWs, destWs, copiedTableNames); + CopyDxfStylesPivotTables(sourceWs, destWs, dxfStyleCashe, copiedPivotTableNames); CopyDxfStylesConditionalFormatting(sourceWs, destWs, dxfStyleCashe); } - private static void CopyDxfStylesTables(ExcelWorksheet sourceWs, ExcelWorksheet destWs) + private static void CopyDxfStylesTables(ExcelWorksheet sourceWs, ExcelWorksheet destWs, Dictionary copiedTableNames) { //Table formats for (int i = 0; i < sourceWs.Tables.Count; i++) { var tblFrom = sourceWs.Tables[i]; - var tblTo = destWs.Tables[i]; //Use Name, as id can differ if the worksheets are in different workbooks. + var tblTo = destWs.Tables[copiedTableNames[tblFrom.Name]]; //Use Name, as id can differ if the worksheets are in different workbooks. DxfStyleHandler.CopyDxfStylesTable(tblFrom, tblTo); } } - private static void CopyDxfStylesPivotTables(ExcelWorksheet sourceWs, ExcelWorksheet destWs, Dictionary dxfStyleCache) + private static void CopyDxfStylesPivotTables(ExcelWorksheet sourceWs, ExcelWorksheet destWs, Dictionary dxfStyleCache, Dictionary copiedPivotTableNames) { //Table formats foreach (var pt in sourceWs.PivotTables) { var ix = 0; - var newPt = destWs.PivotTables[pt.Name]; + var newPt = destWs.PivotTables[copiedPivotTableNames[pt.Name]]; foreach (var a in pt.Styles._list) { var addedStyle = newPt.Styles[ix++]; diff --git a/src/EPPlus/FormulaParsing/CalculateExtensions.cs b/src/EPPlus/FormulaParsing/CalculateExtensions.cs index 6db4e071ac..95e9c527ce 100644 --- a/src/EPPlus/FormulaParsing/CalculateExtensions.cs +++ b/src/EPPlus/FormulaParsing/CalculateExtensions.cs @@ -82,7 +82,7 @@ public static void Calculate(this ExcelWorkbook workbook, ExcelCalculationOption try { #endif - var dc =RpnFormulaExecution.Execute(workbook, options); + var dc = RpnFormulaExecution.Execute(workbook, options); dc._parsingContext.RangeCriteriaCache?.Clear(); if (workbook.FormulaParser.Logger != null) { diff --git a/src/EPPlus/FormulaParsing/DependencyChain/RpnFormulaExecution.cs b/src/EPPlus/FormulaParsing/DependencyChain/RpnFormulaExecution.cs index f40dcd62ec..f91b4574bf 100644 --- a/src/EPPlus/FormulaParsing/DependencyChain/RpnFormulaExecution.cs +++ b/src/EPPlus/FormulaParsing/DependencyChain/RpnFormulaExecution.cs @@ -449,6 +449,7 @@ private static CompileResult CalculateFormulaChain(RpnOptimizedDependencyChain d rd?.Merge(f._row, f._column); depChain.StartOfChain(); } + ExecuteFormula: try { @@ -497,15 +498,16 @@ private static CompileResult CalculateFormulaChain(RpnOptimizedDependencyChain d addresses = f._expressions[f._tokenIndex].GetAddress(); } depChain.AddFormulaToChain(f, addresses); - if (GetAddressesToFollow(depChain, f, options, ref addresses, ref rd, ref ws)) { goto FollowChain; } + f._tokenIndex++; goto ExecuteFormula; } } + CompileResult cr; if (f._tokenIndex == int.MaxValue) //int.MaxValue means we have an invalid formulas and we should return a name error { @@ -518,8 +520,8 @@ private static CompileResult CalculateFormulaChain(RpnOptimizedDependencyChain d if (cr != null && f.IsLambda == false && (writeToCell || depChain._formulaStack.Count > 0)) // If calculating single cell via the FormulaParser.Parse method we should not write to the cells { - SetValueToWorkbook(depChain, f, rd, cr, options, ref depChainPos); - + SetValueToWorkbook(depChain, f, cr, options, ref depChainPos); + //We are in a dirty cell recalculation and have a new position in the chain. //We should return to the caller and let it continue from the new position in the chain. //We use this technique to avoid stack overflow exceptions when recalculating dirty cells with long dependency chains. @@ -543,6 +545,7 @@ private static CompileResult CalculateFormulaChain(RpnOptimizedDependencyChain d f._tokenIndex++; goto ExecuteFormula; } + rd = AddOrGetRDFromWsIx(depChain, f._enumeratorWorksheetIx); goto NextFormula; } @@ -565,7 +568,9 @@ private static CompileResult CalculateFormulaChain(RpnOptimizedDependencyChain d { if (depChain.processedCells.Contains(ExcelCellBase.GetCellId(ws?.IndexInList ?? ushort.MaxValue, firstAddress.FromRow, firstAddress.FromCol)) == false) { + rd?.Merge(firstAddress.FromRow, firstAddress.FromCol); + if (ws._formulas.Exists(firstAddress.FromRow, firstAddress.FromCol, ref v) && v != null) { depChain._formulaStack.Push(f); @@ -574,6 +579,7 @@ private static CompileResult CalculateFormulaChain(RpnOptimizedDependencyChain d } } f._tokenIndex++; + goto ExecuteFormula; } else @@ -605,7 +611,6 @@ private static CompileResult CalculateFormulaChain(RpnOptimizedDependencyChain d goto NextFormula; } } - MergeToRd(rd, row, col, rPos, fe, true); f._formulaEnumerator = null; @@ -655,6 +660,12 @@ private static bool GetAddressesToFollow(RpnOptimizedDependencyChain depChain, R var needsClean = false; for (int i = 0; i < addresses.Length; i++) { + if (addresses[i].FromRow<1 || addresses[i].FromCol<1) + { + addresses[i] = null; + needsClean = true; + continue; + } var address = addresses[i].Clone(); if (address.ExternalReferenceIx > 0) //We don't follow dep chain into external references. { @@ -739,7 +750,7 @@ private static void CheckAndClearRichData(RpnFormula f) } f._ws._metadataStore.Clear(f._row, f._column, 1, 1); } - private static void SetValueToWorkbook(RpnOptimizedDependencyChain depChain, RpnFormula f, RangeHashset rd, CompileResult cr, ExcelCalculationOption options, ref int insertDepChainPos) + private static void SetValueToWorkbook(RpnOptimizedDependencyChain depChain, RpnFormula f/*, RangeHashset rd*/, CompileResult cr, ExcelCalculationOption options, ref int insertDepChainPos) { if(cr.DataType == DataType.LambdaCalculation) { @@ -760,6 +771,7 @@ private static void SetValueToWorkbook(RpnOptimizedDependencyChain depChain, Rpn } else { + var rd = AddOrGetRDFromWsIx(depChain, f._ws.IndexInList); if ((cr.DataType == DataType.ExcelRange && ((IRangeInfo)cr.Result).Address.IsSingleCell == false)) //A range. When we add support for dynamic array formulas we will alter this. { var ri = (IRangeInfo)cr.Result; @@ -774,6 +786,7 @@ private static void SetValueToWorkbook(RpnOptimizedDependencyChain depChain, Rpn { //Add dynamic array formula support here. var dirtyRange = ArrayFormulaOutput.FillDynamicArrayFromRangeInfo(f, ri, rd, depChain); + if (dirtyRange != null && dirtyRange.Length > 0) { RecalculateDirtyCells(dirtyRange, depChain, rd, options); @@ -791,11 +804,14 @@ private static void SetValueToWorkbook(RpnOptimizedDependencyChain depChain, Rpn (f._flags & FormulaFlags.IsAlwaysDynamic) == FormulaFlags.IsAlwaysDynamic) && f.CanBeDynamicArray) { + var dirtyRange = ArrayFormulaOutput.FillDynamicArraySingleValue(f, cr, rd, depChain); + if (dirtyRange != null && dirtyRange.Length > 0) { RecalculateDirtyCells(dirtyRange, depChain, rd, options); } + depChain.HasAnyArrayFormula = true; } else if (cr.ResultType == CompileResultType.LocalImage) @@ -1259,9 +1275,11 @@ private static FormulaRangeAddress[] ExecuteNextToken(RpnOptimizedDependencyChai f._tokenIndex++; continue; } - } - return e.GetAddress(); + if (t.TokenType == TokenType.CellAddress || t.TokenType == TokenType.ExcelAddress) //Full column and full row addresses will be returned when processing the : operator. + { + return e.GetAddress(); + } } break; case TokenType.NameValue: @@ -1281,7 +1299,10 @@ private static FormulaRangeAddress[] ExecuteNextToken(RpnOptimizedDependencyChai { if (IsSingleAddress(f)) { - return nameAddress; + foreach(var a in nameAddress) + { + return GetCriteriaRange(depChain._parsingContext, f, a); + } } } } @@ -1388,7 +1409,7 @@ private static FormulaRangeAddress[] ExecuteNextToken(RpnOptimizedDependencyChai { if ((f._funcStack.Count == 0 || ShouldIgnoreAddress(f._funcStack.Peek()) == false) && r.Address != null) { - return [r.Address.Clone()]; + return GetCriteriaRange(depChain._parsingContext, f, r.Address.Clone()); } } } @@ -1419,7 +1440,7 @@ private static FormulaRangeAddress[] ExecuteNextToken(RpnOptimizedDependencyChai var cr = s.Peek().Compile(); if (cr.Address != null) { - return [cr.Address]; + return GetCriteriaRange(depChain._parsingContext, f, cr.Address); } } @@ -1488,6 +1509,42 @@ private static FormulaRangeAddress[] ExecuteNextToken(RpnOptimizedDependencyChai return null; } + private static FormulaRangeAddress[] GetCriteriaRange(ParsingContext ctx,RpnFormula f, FormulaRangeAddress address) + { + + if (address.ExternalReferenceIx <=0 && f._funcStack.Count > 0) + { + var lfe = f._funcStack.Peek(); + var pi = lfe._function.ParametersInfo.GetParameterInfo(lfe._argPos); + if (pi == FunctionParameterInformation.AdjustCriteriaParameterAddress) + { + var q = new Queue(); + lfe._function.GetNewParameterAddress(CreateArgumentsForParameterAddress(f, lfe),lfe._argPos, ctx, ref q); + return q.ToArray(); + } + } + return [address]; + } + + private static IList CreateArgumentsForParameterAddress(RpnFormula f, FunctionExpression fe) + { + var ix = 0; + var l = new List(); + foreach(var e in f.ExpressionStack.Reverse()) + { + if (fe._function.ParametersInfo.GetParameterInfo(ix)!=FunctionParameterInformation.AdjustParameterAddress) + { + l.Add(e.Compile()); + } + else + { + l.Add(null); + } + ix++; + } + return l; + } + private static ExpressionCondition GetCondition(CompileResult v) { if (v.ResultValue is IRangeInfo ri) diff --git a/src/EPPlus/FormulaParsing/Excel/Functions/FunctionParameterInformation.cs b/src/EPPlus/FormulaParsing/Excel/Functions/FunctionParameterInformation.cs index 9dbfe60141..dfc9ed8cab 100644 --- a/src/EPPlus/FormulaParsing/Excel/Functions/FunctionParameterInformation.cs +++ b/src/EPPlus/FormulaParsing/Excel/Functions/FunctionParameterInformation.cs @@ -54,5 +54,9 @@ public enum FunctionParameterInformation /// The parameter is a variable which value is calculated by the next parameter. /// IsParameterVariable = 0x80, + /// + /// A hierarcal criteria + /// + AdjustCriteriaParameterAddress = 0x100 } } diff --git a/src/EPPlus/FormulaParsing/Excel/Functions/MathFunctions/AverageIfs.cs b/src/EPPlus/FormulaParsing/Excel/Functions/MathFunctions/AverageIfs.cs index 50c63d6aec..4a68d7a2e6 100644 --- a/src/EPPlus/FormulaParsing/Excel/Functions/MathFunctions/AverageIfs.cs +++ b/src/EPPlus/FormulaParsing/Excel/Functions/MathFunctions/AverageIfs.cs @@ -48,7 +48,7 @@ internal class AverageIfs : RangeCriteriaFunction { return FunctionParameterInformation.Normal; } - return FunctionParameterInformation.AdjustParameterAddress; + return FunctionParameterInformation.AdjustCriteriaParameterAddress; })); public override void GetNewParameterAddress(IList args, int index, ParsingContext ctx, ref Queue addresses) diff --git a/src/EPPlus/FormulaParsing/Excel/Functions/MathFunctions/CountIfs.cs b/src/EPPlus/FormulaParsing/Excel/Functions/MathFunctions/CountIfs.cs index 664cff771b..ae775fab68 100644 --- a/src/EPPlus/FormulaParsing/Excel/Functions/MathFunctions/CountIfs.cs +++ b/src/EPPlus/FormulaParsing/Excel/Functions/MathFunctions/CountIfs.cs @@ -36,7 +36,7 @@ internal class CountIfs : RangeCriteriaFunction } if (argumentIndex % 2 == 0) { - return FunctionParameterInformation.AdjustParameterAddress; + return FunctionParameterInformation.AdjustCriteriaParameterAddress; } return FunctionParameterInformation.IgnoreErrorInPreExecute; })); diff --git a/src/EPPlus/FormulaParsing/Excel/Functions/MathFunctions/RangeCriteriaFunction.cs b/src/EPPlus/FormulaParsing/Excel/Functions/MathFunctions/RangeCriteriaFunction.cs index 69014fcdf0..a61af1043b 100644 --- a/src/EPPlus/FormulaParsing/Excel/Functions/MathFunctions/RangeCriteriaFunction.cs +++ b/src/EPPlus/FormulaParsing/Excel/Functions/MathFunctions/RangeCriteriaFunction.cs @@ -272,7 +272,7 @@ protected static Queue EnqueueMatchingAddresses(IRangeInfo 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(); + var criteriaRanges = new List(); var criteria = new List(); for (var ix = argStartIx; ix < maxIndex; ix += 2) { @@ -280,27 +280,27 @@ protected IEnumerable GetMatchingIndicesFromArguments(int argStartIx, IList var arg = args[ix]; if (arg.Result is IRangeInfo rangeInfo) { - argRanges.Add(new RangeOrValue { Range = rangeInfo }); + criteriaRanges.Add(new RangeOrValue { Range = rangeInfo }); } else { - argRanges.Add(new RangeOrValue { Value = arg.ResultValue }); + criteriaRanges.Add(new RangeOrValue { Value = arg.ResultValue }); } if (args[ix + 1].Result is IRangeInfo critInfo) { - criteria.Add(new RangeOrValue { Range = critInfo }); + criteria.Add(critInfo.GetValue(0, 0)); } else { - criteria.Add(new RangeOrValue { Value = args[ix + 1].ResultValue }); + criteria.Add(args[ix + 1].ResultValue); } } - IEnumerable matchIndexes = GetMatchIndexes(argRanges[0], criteria[0], ctx, convertNumericStrings); + IEnumerable matchIndexes = GetMatchIndexes(criteriaRanges[0], criteria[0], ctx, convertNumericStrings); var enumerable = matchIndexes as IList ?? matchIndexes.ToList(); - for (var ix = 1; ix < argRanges.Count && enumerable.Any(); ix++) + for (var ix = 1; ix < criteriaRanges.Count && enumerable.Any(); ix++) { - var indexes = GetMatchIndexes(argRanges[ix], criteria[ix], ctx, convertNumericStrings); + var indexes = GetMatchIndexes(criteriaRanges[ix], criteria[ix], ctx, convertNumericStrings); matchIndexes = matchIndexes.Intersect(indexes); } diff --git a/src/EPPlus/FormulaParsing/Excel/Functions/MathFunctions/SumIfs.cs b/src/EPPlus/FormulaParsing/Excel/Functions/MathFunctions/SumIfs.cs index 75356df006..38e481e142 100644 --- a/src/EPPlus/FormulaParsing/Excel/Functions/MathFunctions/SumIfs.cs +++ b/src/EPPlus/FormulaParsing/Excel/Functions/MathFunctions/SumIfs.cs @@ -54,7 +54,7 @@ public override void ConfigureArrayBehaviour(ArrayBehaviourConfig config) { return FunctionParameterInformation.Normal; } - return FunctionParameterInformation.AdjustParameterAddress; + return FunctionParameterInformation.AdjustCriteriaParameterAddress; })); public override void GetNewParameterAddress(IList args, int index, ParsingContext ctx, ref Queue addresses) diff --git a/src/EPPlusTest/Issues/FormulaCalculationIssues.cs b/src/EPPlusTest/Issues/FormulaCalculationIssues.cs index 00d0a968a6..979246d25a 100644 --- a/src/EPPlusTest/Issues/FormulaCalculationIssues.cs +++ b/src/EPPlusTest/Issues/FormulaCalculationIssues.cs @@ -1749,7 +1749,28 @@ public void s1060() Assert.AreEqual(4520.75, result); } } - + [TestMethod] + public void s1065() + { + using (var p = OpenTemplatePackage("s1065.xlsx")) + { + p.Workbook.Calculate(); + var ws = p.Workbook.Worksheets[1]; + var result = (double)ws.Cells["D69"].Value; + Assert.AreEqual(-310522.61, result, 0.01); + } + } + [TestMethod] + public void s1066() + { + using (var p = OpenTemplatePackage("s1066.xlsx")) + { + p.Workbook.Calculate(); + var ws = p.Workbook.Worksheets["Tax All"]; + var result = ws.Cells["G15"].Value; + Assert.AreEqual("CH-0% output tax foreign/foreign", result); + } + } } } From 9721bafbec01272a2ea5dcaee6462628c8f84183 Mon Sep 17 00:00:00 2001 From: OssianEPPlus <122265629+OssianEPPlus@users.noreply.github.com> Date: Mon, 17 Aug 2026 13:10:35 +0200 Subject: [PATCH 09/30] Fixed #2464 (#2467) --- src/EPPlus/Drawing/Slicer/ExcelSlicerCache.cs | 31 +++++++++++++++++++ .../Drawing/Slicer/ExcelTableSlicerCache.cs | 29 ----------------- .../PivotTableCalculationSlicerTests.cs | 28 +++++++++++++++-- 3 files changed, 57 insertions(+), 31 deletions(-) diff --git a/src/EPPlus/Drawing/Slicer/ExcelSlicerCache.cs b/src/EPPlus/Drawing/Slicer/ExcelSlicerCache.cs index 790b3daeed..6885a2fd89 100644 --- a/src/EPPlus/Drawing/Slicer/ExcelSlicerCache.cs +++ b/src/EPPlus/Drawing/Slicer/ExcelSlicerCache.cs @@ -23,6 +23,7 @@ namespace OfficeOpenXml.Drawing.Slicer /// public abstract class ExcelSlicerCache : XmlHelper { + const string _extPath = "x14:extLst/d:ext"; internal ExcelSlicerCache(XmlNamespaceManager nameSpaceManager) : base(nameSpaceManager) { } @@ -108,5 +109,35 @@ internal void CreateWorkbookReference(ExcelWorkbook wb, string uriGuid) var element = (XmlElement)xh.CreateNode("x14:slicerCache", false, true); element.SetAttribute("id", ExcelPackage.schemaRelationships, CacheRel.Id); } + + const string _hideItemsWithNoDataPath = "x15:slicerCacheHideItemsWithNoData"; + /// + /// If true, items that have no data are not displayed + /// + public bool HideItemsWithNoData + { + get + { + return ExistsNode(_extPath + "/" + _hideItemsWithNoDataPath); + } + set + { + if (value) + { + var node = CreateNode("x14:extLst/d:ext", false, true); + ((XmlElement)node).SetAttribute("uri", ExtLstUris.SlicerCacheHideItemsWithNoDataUri); + var helper = XmlHelperFactory.Create(NameSpaceManager, node); + helper.CreateNode(_hideItemsWithNoDataPath, false, true); + } + else + { + var hideNode = GetNode(_extPath + "/" + _hideItemsWithNoDataPath); + if (hideNode != null) + { + hideNode.ParentNode.ParentNode.RemoveChild(hideNode.ParentNode); + } + } + } + } } } diff --git a/src/EPPlus/Drawing/Slicer/ExcelTableSlicerCache.cs b/src/EPPlus/Drawing/Slicer/ExcelTableSlicerCache.cs index 12ffa05fa4..39ebf6e4c6 100644 --- a/src/EPPlus/Drawing/Slicer/ExcelTableSlicerCache.cs +++ b/src/EPPlus/Drawing/Slicer/ExcelTableSlicerCache.cs @@ -118,35 +118,6 @@ public bool CustomListSort SetXmlNodeBool(_customListSortPath, value, true); } } - const string _hideItemsWithNoDataPath = "x15:slicerCacheHideItemsWithNoData"; - /// - /// If true, items that have no data are not displayed - /// - public bool HideItemsWithNoData - { - get - { - return ExistsNode(_extPath +"/" + _hideItemsWithNoDataPath); - } - set - { - if(value) - { - var node = CreateNode("x14:extLst/d:ext",false,true); - ((XmlElement)node).SetAttribute("uri", "{470722E0-AACD-4C17-9CDC-17EF765DBC7E}"); - var helper = XmlHelperFactory.Create(NameSpaceManager, node); - helper.CreateNode(_hideItemsWithNoDataPath, false, true); - } - else - { - var hideNode = GetNode(_extPath + "/" + _hideItemsWithNoDataPath); - if(hideNode!=null) - { - hideNode.ParentNode.ParentNode.RemoveChild(hideNode.ParentNode); - } - } - } - } const string _columnIndexPath = _topPath + "/@column"; internal int ColumnId { diff --git a/src/EPPlusTest/Table/PivotTable/Calculation/PivotTableCalculationSlicerTests.cs b/src/EPPlusTest/Table/PivotTable/Calculation/PivotTableCalculationSlicerTests.cs index b954474c55..b6b0363c9f 100644 --- a/src/EPPlusTest/Table/PivotTable/Calculation/PivotTableCalculationSlicerTests.cs +++ b/src/EPPlusTest/Table/PivotTable/Calculation/PivotTableCalculationSlicerTests.cs @@ -15,7 +15,7 @@ namespace EPPlusTest.Table.PivotTable.Calculation public class PivotTableCalculationSlicerTests : TestBase { static ExcelPackage _pck; - static ExcelTable _tbl1, _tbl2; + static ExcelTable _tbl1, _tbl2, _tbl3; [ClassInitialize] public static void Init(TestContext context) { @@ -27,6 +27,10 @@ public static void Init(TestContext context) ws = _pck.Workbook.Worksheets.Add("Data2"); r = LoadItemData(ws); _tbl2 = ws.Tables.Add(r, "Table2"); + ws = _pck.Workbook.Worksheets.Add("Data3"); + r = LoadItemData(ws); + ws.Cells["N10:N11"].Value = null; + _tbl3 = ws.Tables.Add(r, "Table3"); } [ClassCleanup] public static void Cleanup() @@ -91,5 +95,25 @@ public void FilterSlicerMultipleItems() Assert.AreEqual(ErrorValues.RefError, ws.Cells["F7"].Value); Assert.AreEqual(358.8, ws.Cells["F8"].Value); } - } + + [TestMethod] + public void FilterSlicerMultipleItemsHideWithNoData() + { + var ws = _pck.Workbook.Worksheets.Add("PivotSlicerWithNoData"); + var pt = ws.PivotTables.Add(ws.Cells["C3"], _tbl3, "PivotTableSlicerSingle"); + pt.RowFields.Add(pt.Fields[0]); + var slicer = pt.Fields[0].AddSlicer(); + slicer.SetPosition(1, 0, 8, 0); + pt.CacheDefinition.Refresh(); + var df = pt.DataFields.Add(pt.Fields["Price"]); + + Assert.AreEqual(slicer.Cache.Data.Items.Count, 6); + + slicer.Cache.HideItemsWithNoData = true; + slicer.Cache.Data.Items.Refresh(); + + Assert.AreEqual(slicer.Cache.Data.Items[4].Hidden, false); + Assert.AreEqual(slicer.Cache.Data.Items[5].Hidden, false); + } + } } \ No newline at end of file From 2d93289131b4a7d2b2084baab3ad5067a70a1b0f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ossian=20Edstr=C3=B6m?= Date: Mon, 17 Aug 2026 14:16:09 +0200 Subject: [PATCH 10/30] Changed default shape shade to 15% --- docs/articles/breakingchanges.md | 3 +- .../StyleTests.cs | 40 +++++++++++++++++++ src/EPPlus/Drawing/ExcelShape.cs | 2 +- .../DrawingRenderItemExtentions.cs | 2 +- 4 files changed, 44 insertions(+), 3 deletions(-) diff --git a/docs/articles/breakingchanges.md b/docs/articles/breakingchanges.md index 02c7473d8d..95955aec6d 100644 --- a/docs/articles/breakingchanges.md +++ b/docs/articles/breakingchanges.md @@ -232,4 +232,5 @@ The misspelled enum eCompundLineStyle has been renamed eCompoundLineStyle. The misspelled enum eCompundLineStyle has been renamed eCompoundLineStyle. The misspelled property on drawingFill `Transparancy` has been renamed to `Transparency` The `Richtext.Baseline` property now always return that value in whole percent. -ExcelChartSerie.Header now returns null instead of empty string, if the underlaying "tx/v" node does not exist. \ No newline at end of file +ExcelChartSerie.Header now returns null instead of empty string, if the underlaying "tx/v" node does not exist. +The default border.fill.color for Shapes has been changed to apply 15% shade instead of the previous default 50% shade in accordance with modern Excel \ No newline at end of file diff --git a/src/EPPlus.DrawingRenderer.Tests/StyleTests.cs b/src/EPPlus.DrawingRenderer.Tests/StyleTests.cs index 89b24c08fd..7e6d42f161 100644 --- a/src/EPPlus.DrawingRenderer.Tests/StyleTests.cs +++ b/src/EPPlus.DrawingRenderer.Tests/StyleTests.cs @@ -1,4 +1,5 @@ using OfficeOpenXml; +using OfficeOpenXml.Drawing; using System.Drawing; using System.Linq; @@ -211,6 +212,45 @@ public void TextRunIsStyledButNotTitleFont() //} } + [TestMethod] + public void EpplusGeneratedShape() + { + ExcelPackage.License.SetNonCommercialOrganization("EPPlus Project"); + + var fileName = "Style_Epp_Rect.xlsx"; + using (var p = OpenPackage(fileName, true)) + { + var ws = p.Workbook.Worksheets.Add("MyWs"); + var drawing = ws.Drawings.AddShape("rectangle", eShapeStyle.Rect); + + var svg = drawing.ToSvg(); + SaveTextFileToWorkbook($"svg\\{fileName}_{ws.Name}_{drawing.Name}.svg", svg); + + SaveAndCleanup(p); + } + } + + [TestMethod] + public void EpplusGeneratedShapeWithTheme() + { + ExcelPackage.License.SetNonCommercialOrganization("EPPlus Project"); + + var fileName = "Style_Theme_Epp_Rect.xlsx"; + using (var p = OpenPackage(fileName, true)) + { + var ws = p.Workbook.Worksheets.Add("MyWsWithTheme"); + var myThemeFile = GetTemplateFile("StyleExamples\\ParalaxTheme.thmx"); + p.Workbook.ThemeManager.Load(myThemeFile); + + var drawing = ws.Drawings.AddShape("rectangle", eShapeStyle.Rect); + + //var svg = drawing.ToSvg(); + //SaveTextFileToWorkbook($"svg\\{fileName}_{ws.Name}_{drawing.Name}.svg", svg); + + SaveAndCleanup(p); + } + } + ///// ///// Exports an to a html string ///// diff --git a/src/EPPlus/Drawing/ExcelShape.cs b/src/EPPlus/Drawing/ExcelShape.cs index cc441aa4b8..7af5c06558 100644 --- a/src/EPPlus/Drawing/ExcelShape.cs +++ b/src/EPPlus/Drawing/ExcelShape.cs @@ -82,7 +82,7 @@ internal ExcelShape(ExcelDrawings drawings, XmlNode node, eShapeStyle style, Dra private string ShapeStartXml() { StringBuilder xml = new StringBuilder(); - xml.AppendFormat("<{2}:nvSpPr><{2}:cNvPr id=\"{0}\" name=\"{1}\" /><{2}:cNvSpPr /><{2}:spPr><{2}:style><{2}:txBody>", Id, Name, NamespacePrefixes[(int)_drawings._collectionType]); + xml.AppendFormat("<{2}:nvSpPr><{2}:cNvPr id=\"{0}\" name=\"{1}\" /><{2}:cNvSpPr /><{2}:spPr><{2}:style><{2}:txBody>", Id, Name, NamespacePrefixes[(int)_drawings._collectionType]); return xml.ToString(); } diff --git a/src/EPPlus/Drawing/Renderer/RenderItems/DrawingRenderItemExtentions.cs b/src/EPPlus/Drawing/Renderer/RenderItems/DrawingRenderItemExtentions.cs index 5eae642cd5..1574ca562a 100644 --- a/src/EPPlus/Drawing/Renderer/RenderItems/DrawingRenderItemExtentions.cs +++ b/src/EPPlus/Drawing/Renderer/RenderItems/DrawingRenderItemExtentions.cs @@ -211,7 +211,7 @@ internal static void ResolveStyleFallbackChainBorder(this RenderItem item, Excel case eFillStyle.SolidFill: //1. Standard case. There is a fill color to apply. //Send in styleFill as well since a solid fill can refer to style color - fc = tc.ColorConverter.GetThemeColor(theme, border.Fill.SolidFill.Color, reference.Color); + fc = tc.ColorConverter.GetThemeColor(theme, border.Fill.SolidFill.Color, reference?.Color); item.BorderColor = GetAdjustmentsAndTransparency(fc.Value, item.BorderColorSource, out opacity); item.BorderGradientFill = null; break; From feaae2d4267a16795e2299c52f473c22dc05221f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jan=20K=C3=A4llman?= Date: Tue, 18 Aug 2026 14:17:30 +0200 Subject: [PATCH 11/30] Fixed positioning of primary and secondary axis and axis titles when setting position to low or high. --- .../Chart/LineChartToSvgTests.cs | 4 +- .../Svg/Core/SvgBaseRenderer.cs | 2 +- .../Svg/Core/SvgShapeRenderer.cs | 2 +- .../Drawing/Chart/ExcelChartAxisStandard.cs | 10 ++--- .../Renderer/Chart/ChartAxisRenderer.cs | 34 ++++++++------- .../Renderer/Chart/ChartPlotareaRenderer.cs | 26 +++++++++--- .../Trendlines/ChartTrendlineRenderer.cs | 4 +- src/EPPlus/Drawing/Renderer/ChartRenderer.cs | 41 +++++++++++++++---- 8 files changed, 84 insertions(+), 39 deletions(-) diff --git a/src/EPPlus.DrawingRenderer.Tests/Chart/LineChartToSvgTests.cs b/src/EPPlus.DrawingRenderer.Tests/Chart/LineChartToSvgTests.cs index b9b530b03d..7223d4a36c 100644 --- a/src/EPPlus.DrawingRenderer.Tests/Chart/LineChartToSvgTests.cs +++ b/src/EPPlus.DrawingRenderer.Tests/Chart/LineChartToSvgTests.cs @@ -119,11 +119,11 @@ public void GenerateSvgForLineChartSecondaryAxis() using (var p = OpenTemplatePackage("ChartForSvg_SecondaryAxis.xlsx")) { var ws = p.Workbook.Worksheets[0]; - //var ix = 1; + //var ix = 3; //var c = ws.Drawings[ix]; //var svg = c.ToSvg(); //SaveTextFileToWorkbook($"svg\\ChartForSvg_sheet2_{ix++}.svg", svg); - var ix = 1; + var ix = 0; foreach (ExcelChart c in ws.Drawings) { var svg = c.ToSvg(); diff --git a/src/EPPlus.DrawingRenderer/Svg/Core/SvgBaseRenderer.cs b/src/EPPlus.DrawingRenderer/Svg/Core/SvgBaseRenderer.cs index 8d47f5c9bd..5a83ec6104 100644 --- a/src/EPPlus.DrawingRenderer/Svg/Core/SvgBaseRenderer.cs +++ b/src/EPPlus.DrawingRenderer/Svg/Core/SvgBaseRenderer.cs @@ -40,7 +40,7 @@ protected void RenderBaseToSpecified(T item, StringBuilder sb) } if (string.IsNullOrEmpty(item.FilterName) == false) { - sb.Append($"filter=\"url(#{item.FilterName})\" "); + sb.Append($"filter=\"{item.FilterName}\" "); } if (item.BorderWidth.HasValue) diff --git a/src/EPPlus.DrawingRenderer/Svg/Core/SvgShapeRenderer.cs b/src/EPPlus.DrawingRenderer/Svg/Core/SvgShapeRenderer.cs index ee18d31929..7dea9c8e46 100644 --- a/src/EPPlus.DrawingRenderer/Svg/Core/SvgShapeRenderer.cs +++ b/src/EPPlus.DrawingRenderer/Svg/Core/SvgShapeRenderer.cs @@ -216,7 +216,7 @@ private void WriteDefsForRenderItem(StringBuilder defSb, HashSet hs, ref 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; + var blurRadius = (item.OuterShadowEffect.BlurRadius ?? 0D) / 2; filter += $""; } } diff --git a/src/EPPlus/Drawing/Chart/ExcelChartAxisStandard.cs b/src/EPPlus/Drawing/Chart/ExcelChartAxisStandard.cs index 9c1feebb0a..0fccca85b4 100644 --- a/src/EPPlus/Drawing/Chart/ExcelChartAxisStandard.cs +++ b/src/EPPlus/Drawing/Chart/ExcelChartAxisStandard.cs @@ -187,7 +187,7 @@ public eActualAxisPosition ActualAxisPosition { if (LabelPosition == eTickLabelPosition.Low) { - if (_chart.Axis.FirstOrDefault(x => x.AxisPosition == eAxisPosition.Left)?.LabelPosition != eTickLabelPosition.Low) + if (_chart.Axis.FirstOrDefault(x => x.AxisPosition == eAxisPosition.Left)?.LabelPosition == eTickLabelPosition.High) { return eActualAxisPosition.Left; } @@ -198,7 +198,7 @@ public eActualAxisPosition ActualAxisPosition } else { - if (_chart.Axis.FirstOrDefault(x => x.AxisPosition == eAxisPosition.Left)?.LabelPosition != eTickLabelPosition.High) + if (_chart.Axis.FirstOrDefault(x => x.AxisPosition == eAxisPosition.Left)?.LabelPosition == eTickLabelPosition.Low) { return eActualAxisPosition.Right; } @@ -211,8 +211,8 @@ public eActualAxisPosition ActualAxisPosition else if(ap==eAxisPosition.Top) { if (LabelPosition == eTickLabelPosition.Low) - { - if (_chart.Axis.FirstOrDefault(x => x.AxisPosition == eAxisPosition.Bottom)?.LabelPosition != eTickLabelPosition.Low) + { + if (_chart.Axis.FirstOrDefault(x => x.AxisPosition == eAxisPosition.Bottom)?.LabelPosition == eTickLabelPosition.High) { return eActualAxisPosition.Bottom; } @@ -223,7 +223,7 @@ public eActualAxisPosition ActualAxisPosition } else { - if (_chart.Axis.FirstOrDefault(x => x.AxisPosition == eAxisPosition.Bottom)?.LabelPosition != eTickLabelPosition.High) + if (_chart.Axis.FirstOrDefault(x => x.AxisPosition == eAxisPosition.Bottom)?.LabelPosition == eTickLabelPosition.Low) { return eActualAxisPosition.Top; } diff --git a/src/EPPlus/Drawing/Renderer/Chart/ChartAxisRenderer.cs b/src/EPPlus/Drawing/Renderer/Chart/ChartAxisRenderer.cs index 3c42b9a209..d1094551ae 100644 --- a/src/EPPlus/Drawing/Renderer/Chart/ChartAxisRenderer.cs +++ b/src/EPPlus/Drawing/Renderer/Chart/ChartAxisRenderer.cs @@ -256,16 +256,17 @@ public override void AppendRenderItems(List renderItems) if(Rectangle!=null || Rectangle.Width==0 || Rectangle.Height==0) renderItems.Add(Rectangle); var plotareaGroup = ChartRenderer.Plotarea.Group; - if (MajorGridlinePositions != null) + if (MinorGridlinePositions != null) { - foreach (var tm in MajorGridlinePositions) + foreach (var tm in MinorGridlinePositions) { plotareaGroup.RenderItems.Add(tm); } } - if (MinorGridlinePositions != null) + + if (MajorGridlinePositions != null) { - foreach (var tm in MinorGridlinePositions) + foreach (var tm in MajorGridlinePositions) { plotareaGroup.RenderItems.Add(tm); } @@ -273,16 +274,17 @@ public override void AppendRenderItems(List renderItems) if (Line != null) renderItems.Add(Line); - if (MajorTickMarkPositions != null) + if (MinorTickMarkPositions != null) { - foreach (var tm in MajorTickMarkPositions) + foreach (var tm in MinorTickMarkPositions) { renderItems.Add(tm); } } - if (MinorTickMarkPositions != null) + + if (MajorTickMarkPositions != null) { - foreach (var tm in MinorTickMarkPositions) + foreach (var tm in MajorTickMarkPositions) { renderItems.Add(tm); } @@ -687,39 +689,41 @@ private List AddTickmarks(double units, eTimeUnit? dateUnit, dou } var diff = min == 0 ? max - min : max - min + 1; - + var maxPos = max == 0 ? max : max + 1; + double d = min + addMinor; - while (d <= max) + while (d <= maxPos) { - if (double.IsNaN(parentUnit) || (d % parentUnit != 0)) + var addPosition = (d - min); + if (double.IsNaN(parentUnit) || (addPosition % parentUnit != 0)) { double x1, y1, x2, y2; switch (Axis.ActualAxisPosition) { case eActualAxisPosition.Left: case eActualAxisPosition.LeftSecond: - y1 = (float)(Rectangle.Top + Rectangle.Height - ((d - min) / diff * Rectangle.Height)); + y1 = (float)(Rectangle.Top + Rectangle.Height - (addPosition / diff * Rectangle.Height)); y2 = y1; x1 = (float)Rectangle.Right - tickMarkWidthOutside; x2 = (float)Rectangle.Right + tickMarkWidthInside; break; case eActualAxisPosition.Right: case eActualAxisPosition.RightSecond: - y1 = (float)(Rectangle.Top + Rectangle.Height - ((d - min) / diff * Rectangle.Height)); + y1 = (float)(Rectangle.Top + Rectangle.Height - (addPosition / diff * Rectangle.Height)); y2 = y1; x1 = (float)Rectangle.Left - tickMarkWidthInside; x2 = (float)Rectangle.Left + tickMarkWidthOutside; break; case eActualAxisPosition.Top: case eActualAxisPosition.TopSecond: - x1 = (float)(Rectangle.Left + ((d - min) / diff * Rectangle.Width)); + x1 = (float)(Rectangle.Left + (addPosition / diff * Rectangle.Width)); x2 = x1; y1 = (float)Rectangle.Bottom - tickMarkWidthOutside; y2 = (float)Rectangle.Bottom + tickMarkWidthInside; break; case eActualAxisPosition.Bottom: case eActualAxisPosition.BottomSecond: - x1 = (float)(Rectangle.Left + ((d - min) / diff * Rectangle.Width)); + x1 = (float)(Rectangle.Left + (addPosition / diff * Rectangle.Width)); x2 = x1; y1 = (float)Rectangle.Top - tickMarkWidthInside; y2 = (float)Rectangle.Top + tickMarkWidthOutside; diff --git a/src/EPPlus/Drawing/Renderer/Chart/ChartPlotareaRenderer.cs b/src/EPPlus/Drawing/Renderer/Chart/ChartPlotareaRenderer.cs index 0ba38ab092..efda6b5778 100644 --- a/src/EPPlus/Drawing/Renderer/Chart/ChartPlotareaRenderer.cs +++ b/src/EPPlus/Drawing/Renderer/Chart/ChartPlotareaRenderer.cs @@ -75,6 +75,14 @@ private double GetPlotAreaHeight(RectRenderItem rect) var bottomSecondAxis = GetAxisActualByPosition(eActualAxisPosition.BottomSecond); vaHeight = (bottomAxis.Rectangle?.Height ?? 0D) + (bottomAxis.Title?.TextBox?.GetActualHeight() ?? 0D) + (bottomSecondAxis?.Rectangle?.Height ?? 0D); } + else + { + var bottomAx = GetAxisByPosition(eAxisPosition.Bottom); + if(bottomAx!=null) //Title is always placed on bottom. + { + vaHeight = bottomAx.Title?.TextBox?.GetActualHeight() ?? 0D; + } + } if (Chart.Legend?.Position == eLegendPosition.Bottom) { vaHeight += ChartRenderer.Legend.Rectangle.Height + ChartRenderer.Legend.TopMargin; @@ -84,7 +92,7 @@ private double GetPlotAreaHeight(RectRenderItem rect) private double GetPlotAreaWidth(RectRenderItem rect) { - var rightAxis = GetAxisActualByPosition(eActualAxisPosition.Right); + var rightActualAxis = GetAxisActualByPosition(eActualAxisPosition.Right); var rightSecondAxis = GetAxisActualByPosition(eActualAxisPosition.RightSecond); var lp = ChartRenderer.Chart.Legend?.Position; var right = ((lp == eLegendPosition.Right || lp == eLegendPosition.TopRight) && ChartRenderer.Legend != null ? @@ -93,13 +101,21 @@ private double GetPlotAreaWidth(RectRenderItem rect) double rightAxisWidth; - if (rightAxis == null) + if (rightActualAxis == null) { - rightAxisWidth = 0; + var rightAxis = GetAxisByPosition(eAxisPosition.Right); + if (rightAxis == null) + { + rightAxisWidth = 0; + } + else + { + rightAxisWidth = rightAxis.Title?.TextBox.GetActualWidth() ?? 0D; + } } else { - rightAxisWidth = (rightAxis.Title?.TextBox.GetActualWidth() ?? 0D) + (rightAxis.Rectangle?.Width ?? 0D) + (rightSecondAxis?.Rectangle?.Width ?? 0D); + rightAxisWidth = (rightActualAxis.Title?.TextBox.GetActualWidth() ?? 0D) + (rightActualAxis.Rectangle?.Width ?? 0D) + (rightSecondAxis?.Rectangle?.Width ?? 0D); } var width = right - rightAxisWidth - rect.GlobalLeft; @@ -171,7 +187,7 @@ private double GetPlotAreaTop() haHeight = (topAxis.Rectangle?.Height ?? 0D) + (topSecondAxis?.Rectangle?.Height ?? 0D) + (topAxis.Title?.TextBox?.GetActualHeight() ?? 0D); } - return (Chart.Legend?.Position == eLegendPosition.Top ? ChartRenderer.Legend.Rectangle.Bounds.Bottom : ChartRenderer.Title?.Rectangle?.GlobalBottom ?? 0d) + haHeight + TopMargin; + return (Chart.Legend?.Position == eLegendPosition.Top ? ChartRenderer.Legend.Rectangle.Bounds.Bottom : ChartRenderer.Title?.Rectangle?.GlobalBottom ?? 0d) + haHeight; } private ChartAxisRenderer GetAxisActualByPosition(eActualAxisPosition pos) diff --git a/src/EPPlus/Drawing/Renderer/Chart/ChartTypeDrawers/Trendlines/ChartTrendlineRenderer.cs b/src/EPPlus/Drawing/Renderer/Chart/ChartTypeDrawers/Trendlines/ChartTrendlineRenderer.cs index 6ef0e522b4..4d65fbf810 100644 --- a/src/EPPlus/Drawing/Renderer/Chart/ChartTypeDrawers/Trendlines/ChartTrendlineRenderer.cs +++ b/src/EPPlus/Drawing/Renderer/Chart/ChartTypeDrawers/Trendlines/ChartTrendlineRenderer.cs @@ -711,7 +711,7 @@ private void CreateRenderCoordinates() { if (isLine) { - coordinates.Add(catAxis.GetPositionInPlotarea(Coordinates[i].X)); + coordinates.Add(catAxis.GetPositionInPlotarea(Coordinates[i].X+1)); coordinates.Add(valAxis.GetPositionInPlotarea(Coordinates[i].Y)); } else @@ -735,7 +735,7 @@ private void CreateRenderCoordinates() } else { - coordinates.Add(catAxis.GetPositionInPlotarea(Coordinates[i].X)); + coordinates.Add(catAxis.GetPositionInPlotarea(Coordinates[i].X+1)); coordinates.Add(valAxis.GetPositionInPlotarea(Coordinates[i].Y)); } } diff --git a/src/EPPlus/Drawing/Renderer/ChartRenderer.cs b/src/EPPlus/Drawing/Renderer/ChartRenderer.cs index 2868026932..171410653f 100644 --- a/src/EPPlus/Drawing/Renderer/ChartRenderer.cs +++ b/src/EPPlus/Drawing/Renderer/ChartRenderer.cs @@ -148,8 +148,15 @@ private void PlaceHorizontalAxis(ChartAxisRenderer horizontalAxis, bool isSecond var axisPos = horizontalAxis.Axis.ActualAxisPosition; if (axisPos == eActualAxisPosition.Bottom) { - horizontalAxis.Rectangle.Top = Plotarea.Group.Top + Plotarea.Rectangle.Height; - horizontalAxis.Line.Y1 = horizontalAxis.Line.Y2 = (float)Plotarea.Group.Top + Plotarea.Rectangle.Height; + if(isSecondary ==false && SecondHorizontalAxis != null && SecondHorizontalAxis.Axis.ActualAxisPosition==eActualAxisPosition.Bottom) + { + horizontalAxis.Rectangle.Top = Plotarea.Group.Top + Plotarea.Rectangle.Height - SecondHorizontalAxis.Rectangle.Height; + } + else + { + horizontalAxis.Rectangle.Top = Plotarea.Group.Top + Plotarea.Rectangle.Height; + } + horizontalAxis.Line.Y1 = horizontalAxis.Line.Y2 = horizontalAxis.Rectangle.Top; } else if(axisPos == eActualAxisPosition.BottomSecond) { @@ -214,9 +221,9 @@ private void PlaceHorizontalAxisTitle(ChartAxisRenderer horizontalAxis) } else { - if (SecondHorizontalAxis.Axis.ActualAxisPosition == eActualAxisPosition.TopSecond) + if (horizontalAxis.Axis.ActualAxisPosition == eActualAxisPosition.TopSecond) { - horizontalAxis.Title.TextBox.Top = horizontalAxis.Rectangle.Top - SecondHorizontalAxis.Rectangle.Height - horizontalAxis.Title.TextBox.Height; + horizontalAxis.Title.TextBox.Top = horizontalAxis.Rectangle.Top - horizontalAxis.Title.Rectangle.Height; } else if (horizontalAxis.Axis.ActualAxisPosition == eActualAxisPosition.Top) { @@ -293,15 +300,33 @@ private void PlaceVerticalAxisTitle(ChartAxisRenderer verticalAxis) } else { - verticalAxis.Title.TextBox.Left = Plotarea.Group.Left - verticalAxis.Rectangle.Width - verticalAxis.Title.TextBox.GetActualWidth() - 1.5; + if(VerticalAxis == VerticalAxis && + SecondVerticalAxis.Axis.ActualAxisPosition==eActualAxisPosition.Left || SecondVerticalAxis.Axis.ActualAxisPosition == eActualAxisPosition.LeftSecond) + { + verticalAxis.Title.TextBox.Left = Plotarea.Group.Left - verticalAxis.Rectangle.Width - verticalAxis.Title.TextBox.GetActualWidth() - SecondVerticalAxis.Rectangle.Width - 1.5; + } + else + { + verticalAxis.Title.TextBox.Left = Plotarea.Group.Left - verticalAxis.Rectangle.Width - verticalAxis.Title.TextBox.GetActualWidth() - 1.5; + } } } else { - if (verticalAxis.Rectangle == null) + if (verticalAxis.Rectangle == null || verticalAxis == SecondVerticalAxis) { - verticalAxis.Title.TextBox.Left = Plotarea.Group.Left + Plotarea.Rectangle.Width; - } + var add = 0D; + if(VerticalAxis.Axis.ActualAxisPosition == eActualAxisPosition.Right) + { + add = VerticalAxis.Rectangle.Width; + } + if(SecondVerticalAxis.Axis.ActualAxisPosition == eActualAxisPosition.Right || + SecondVerticalAxis.Axis.ActualAxisPosition == eActualAxisPosition.RightSecond) + { + add += SecondVerticalAxis.Rectangle.Width; + } + verticalAxis.Title.TextBox.Left = Plotarea.Group.Left + Plotarea.Rectangle.Width+add; + } else { verticalAxis.Title.TextBox.Left = verticalAxis.Rectangle.Right; From dccbe6587321195fb7ff89f795df6d1992146a56 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jan=20K=C3=A4llman?= Date: Tue, 18 Aug 2026 15:56:23 +0200 Subject: [PATCH 12/30] Fixes more axis issues --- .../Chart/LineChartToSvgTests.cs | 12 +++++----- .../Drawing/Chart/ExcelChartAxisStandard.cs | 22 +++++++++++-------- .../Renderer/Chart/ChartPlotareaRenderer.cs | 6 ++--- src/EPPlus/Drawing/Renderer/ChartRenderer.cs | 4 ++-- 4 files changed, 24 insertions(+), 20 deletions(-) diff --git a/src/EPPlus.DrawingRenderer.Tests/Chart/LineChartToSvgTests.cs b/src/EPPlus.DrawingRenderer.Tests/Chart/LineChartToSvgTests.cs index 7223d4a36c..e05040b641 100644 --- a/src/EPPlus.DrawingRenderer.Tests/Chart/LineChartToSvgTests.cs +++ b/src/EPPlus.DrawingRenderer.Tests/Chart/LineChartToSvgTests.cs @@ -123,12 +123,12 @@ public void GenerateSvgForLineChartSecondaryAxis() //var c = ws.Drawings[ix]; //var svg = c.ToSvg(); //SaveTextFileToWorkbook($"svg\\ChartForSvg_sheet2_{ix++}.svg", svg); - var ix = 0; - foreach (ExcelChart c in ws.Drawings) - { - var svg = c.ToSvg(); - SaveTextFileToWorkbook($"svg\\ChartForSvg_SecAxis{ix++}.svg", svg); - } + //var ix = 0; + //foreach (ExcelChart c in ws.Drawings) + //{ + // var svg = c.ToSvg(); + // SaveTextFileToWorkbook($"svg\\ChartForSvg_SecAxis{ix++}.svg", svg); + //} } } [TestMethod] diff --git a/src/EPPlus/Drawing/Chart/ExcelChartAxisStandard.cs b/src/EPPlus/Drawing/Chart/ExcelChartAxisStandard.cs index 0fccca85b4..bf27a58a09 100644 --- a/src/EPPlus/Drawing/Chart/ExcelChartAxisStandard.cs +++ b/src/EPPlus/Drawing/Chart/ExcelChartAxisStandard.cs @@ -187,7 +187,8 @@ public eActualAxisPosition ActualAxisPosition { if (LabelPosition == eTickLabelPosition.Low) { - if (_chart.Axis.FirstOrDefault(x => x.AxisPosition == eAxisPosition.Left)?.LabelPosition == eTickLabelPosition.High) + var ax = _chart.Axis.FirstOrDefault(x => x.AxisPosition == eAxisPosition.Left); + if (ax?.LabelPosition == eTickLabelPosition.High) { return eActualAxisPosition.Left; } @@ -198,21 +199,23 @@ public eActualAxisPosition ActualAxisPosition } else { - if (_chart.Axis.FirstOrDefault(x => x.AxisPosition == eAxisPosition.Left)?.LabelPosition == eTickLabelPosition.Low) + var ax = _chart.Axis.FirstOrDefault(x => x.AxisPosition == eAxisPosition.Left); + if (ax?.LabelPosition == eTickLabelPosition.High) { - return eActualAxisPosition.Right; + return eActualAxisPosition.RightSecond; } else { - return eActualAxisPosition.RightSecond; + return eActualAxisPosition.Right; } } } else if(ap==eAxisPosition.Top) { if (LabelPosition == eTickLabelPosition.Low) - { - if (_chart.Axis.FirstOrDefault(x => x.AxisPosition == eAxisPosition.Bottom)?.LabelPosition == eTickLabelPosition.High) + { + var ax = _chart.Axis.FirstOrDefault(x => x.AxisPosition == eAxisPosition.Bottom); + if (ax.LabelPosition == eTickLabelPosition.High) { return eActualAxisPosition.Bottom; } @@ -223,13 +226,14 @@ public eActualAxisPosition ActualAxisPosition } else { - if (_chart.Axis.FirstOrDefault(x => x.AxisPosition == eAxisPosition.Bottom)?.LabelPosition == eTickLabelPosition.Low) + var ax = _chart.Axis.FirstOrDefault(x => x.AxisPosition == eAxisPosition.Bottom); + if (ax?.LabelPosition==eTickLabelPosition.High) { - return eActualAxisPosition.Top; + return eActualAxisPosition.TopSecond; } else { - return eActualAxisPosition.TopSecond; + return eActualAxisPosition.Top; } } } diff --git a/src/EPPlus/Drawing/Renderer/Chart/ChartPlotareaRenderer.cs b/src/EPPlus/Drawing/Renderer/Chart/ChartPlotareaRenderer.cs index efda6b5778..331108437c 100644 --- a/src/EPPlus/Drawing/Renderer/Chart/ChartPlotareaRenderer.cs +++ b/src/EPPlus/Drawing/Renderer/Chart/ChartPlotareaRenderer.cs @@ -106,11 +106,11 @@ private double GetPlotAreaWidth(RectRenderItem rect) var rightAxis = GetAxisByPosition(eAxisPosition.Right); if (rightAxis == null) { - rightAxisWidth = 0; + rightAxisWidth = (rightSecondAxis?.Rectangle?.Width ?? 0D); } else { - rightAxisWidth = rightAxis.Title?.TextBox.GetActualWidth() ?? 0D; + rightAxisWidth = (rightAxis.Title?.TextBox.GetActualWidth() ?? 0D) + +(rightSecondAxis?.Rectangle?.Width ?? 0D); } } else @@ -180,7 +180,7 @@ private double GetPlotAreaTop() { //If the axis is not on the top, we should check if there is an axis that has the position on the top. If there is, we should reserve space for the title of the axis. This can happen when LabelPosition is set to Low and the axis is on the bottom, but the position of the axis is set to top. topAxis = GetAxisByPosition(eAxisPosition.Top); - haHeight = topAxis?.Title?.Rectangle.Height ?? 0D; + haHeight = (topSecondAxis?.Rectangle?.Height ?? 0D) + (topAxis?.Title?.Rectangle.Height ?? 0D); } else { diff --git a/src/EPPlus/Drawing/Renderer/ChartRenderer.cs b/src/EPPlus/Drawing/Renderer/ChartRenderer.cs index 171410653f..3ed2298415 100644 --- a/src/EPPlus/Drawing/Renderer/ChartRenderer.cs +++ b/src/EPPlus/Drawing/Renderer/ChartRenderer.cs @@ -300,8 +300,8 @@ private void PlaceVerticalAxisTitle(ChartAxisRenderer verticalAxis) } else { - if(VerticalAxis == VerticalAxis && - SecondVerticalAxis.Axis.ActualAxisPosition==eActualAxisPosition.Left || SecondVerticalAxis.Axis.ActualAxisPosition == eActualAxisPosition.LeftSecond) + if(verticalAxis == VerticalAxis && + (SecondVerticalAxis?.Axis.ActualAxisPosition==eActualAxisPosition.Left || SecondVerticalAxis?.Axis.ActualAxisPosition == eActualAxisPosition.LeftSecond)) { verticalAxis.Title.TextBox.Left = Plotarea.Group.Left - verticalAxis.Rectangle.Width - verticalAxis.Title.TextBox.GetActualWidth() - SecondVerticalAxis.Rectangle.Width - 1.5; } From 23963061d17f9f57407afb18b90404f6f1486476 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jan=20K=C3=A4llman?= Date: Tue, 18 Aug 2026 16:21:47 +0200 Subject: [PATCH 13/30] Fixes axis titles when deleted primary axis --- .../Chart/LineChartToSvgTests.cs | 8 ++++---- .../Drawing/Renderer/Chart/ChartPlotareaRenderer.cs | 10 +++++++++- src/EPPlus/Drawing/Renderer/ChartRenderer.cs | 9 ++++++++- 3 files changed, 21 insertions(+), 6 deletions(-) diff --git a/src/EPPlus.DrawingRenderer.Tests/Chart/LineChartToSvgTests.cs b/src/EPPlus.DrawingRenderer.Tests/Chart/LineChartToSvgTests.cs index e05040b641..2e72da28f0 100644 --- a/src/EPPlus.DrawingRenderer.Tests/Chart/LineChartToSvgTests.cs +++ b/src/EPPlus.DrawingRenderer.Tests/Chart/LineChartToSvgTests.cs @@ -119,10 +119,10 @@ public void GenerateSvgForLineChartSecondaryAxis() using (var p = OpenTemplatePackage("ChartForSvg_SecondaryAxis.xlsx")) { var ws = p.Workbook.Worksheets[0]; - //var ix = 3; - //var c = ws.Drawings[ix]; - //var svg = c.ToSvg(); - //SaveTextFileToWorkbook($"svg\\ChartForSvg_sheet2_{ix++}.svg", svg); + var ix = 1; + var c = ws.Drawings[ix]; + var svg = c.ToSvg(); + SaveTextFileToWorkbook($"svg\\ChartForSvg_sheet2_{ix++}.svg", svg); //var ix = 0; //foreach (ExcelChart c in ws.Drawings) //{ diff --git a/src/EPPlus/Drawing/Renderer/Chart/ChartPlotareaRenderer.cs b/src/EPPlus/Drawing/Renderer/Chart/ChartPlotareaRenderer.cs index 331108437c..34cdde4040 100644 --- a/src/EPPlus/Drawing/Renderer/Chart/ChartPlotareaRenderer.cs +++ b/src/EPPlus/Drawing/Renderer/Chart/ChartPlotareaRenderer.cs @@ -73,7 +73,15 @@ private double GetPlotAreaHeight(RectRenderItem rect) if (bottomAxis!=null) { var bottomSecondAxis = GetAxisActualByPosition(eActualAxisPosition.BottomSecond); - vaHeight = (bottomAxis.Rectangle?.Height ?? 0D) + (bottomAxis.Title?.TextBox?.GetActualHeight() ?? 0D) + (bottomSecondAxis?.Rectangle?.Height ?? 0D); + if(bottomSecondAxis==null) + { + var secAxis = ChartRenderer.SecondHorizontalAxis; + if (secAxis != null && secAxis.Axis.Deleted==true && secAxis.Axis.Title!=null) //Secondary axis is deleted, but the axis title is visible. The title will be printed under the primary axis title. + { + vaHeight = secAxis.Title?.Rectangle?.Height??0D; + } + } + vaHeight += (bottomAxis.Rectangle?.Height ?? 0D) + (bottomAxis.Title?.TextBox?.GetActualHeight() ?? 0D) + (bottomSecondAxis?.Rectangle?.Height ?? 0D); } else { diff --git a/src/EPPlus/Drawing/Renderer/ChartRenderer.cs b/src/EPPlus/Drawing/Renderer/ChartRenderer.cs index 3ed2298415..8c2235ea01 100644 --- a/src/EPPlus/Drawing/Renderer/ChartRenderer.cs +++ b/src/EPPlus/Drawing/Renderer/ChartRenderer.cs @@ -188,7 +188,14 @@ private void PlaceHorizontalAxisTitle(ChartAxisRenderer horizontalAxis) { if (horizontalAxis.Axis.AxisPosition == eAxisPosition.Bottom) { - horizontalAxis.Title.TextBox.Top = Plotarea.Group.Top + Plotarea.Rectangle.Height; + if(horizontalAxis==SecondHorizontalAxis) + { + horizontalAxis.Title.TextBox.Top = Plotarea.Group.Top + Plotarea.Rectangle.Height + (HorizontalAxis?.Rectangle?.Height??0) + (HorizontalAxis?.Title?.Rectangle.Height ?? 0); + } + else + { + horizontalAxis.Title.TextBox.Top = Plotarea.Group.Top + Plotarea.Rectangle.Height; + } } else { From de58260a8ea95ea4a6366ef0586912c15978bbfe Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jan=20K=C3=A4llman?= Date: Wed, 19 Aug 2026 14:32:17 +0200 Subject: [PATCH 14/30] Fixed gradient. --- .../Chart/BarChartTests.cs | 2 +- .../Chart/LineChartToSvgTests.cs | 2 +- .../RenderItems/SvgUserSpaceSettings.cs | 4 ++++ .../Svg/Core/SvgShapeRenderer.cs | 20 ++++++++++++++++--- .../Renderer/Chart/ChartAxisRenderer.cs | 14 ++++++------- .../Renderer/Chart/ChartPlotareaRenderer.cs | 4 ++-- .../Chart/ChartTypeDrawers/ChartTypeDrawer.cs | 6 +++--- 7 files changed, 35 insertions(+), 17 deletions(-) diff --git a/src/EPPlus.DrawingRenderer.Tests/Chart/BarChartTests.cs b/src/EPPlus.DrawingRenderer.Tests/Chart/BarChartTests.cs index 94b3bfa0f7..364ea2ef7e 100644 --- a/src/EPPlus.DrawingRenderer.Tests/Chart/BarChartTests.cs +++ b/src/EPPlus.DrawingRenderer.Tests/Chart/BarChartTests.cs @@ -14,7 +14,7 @@ public void GenerateSvgForBarCharts1() { var ws = p.Workbook.Worksheets[0]; - //var ix = 1; + //var ix = 2; //var c = ws.Drawings[ix]; //var svg = c.ToSvg(); //SaveTextFileToWorkbook($"svg\\ChartForSvg_ind{ix++}.svg", svg); diff --git a/src/EPPlus.DrawingRenderer.Tests/Chart/LineChartToSvgTests.cs b/src/EPPlus.DrawingRenderer.Tests/Chart/LineChartToSvgTests.cs index 2e72da28f0..71e957cbae 100644 --- a/src/EPPlus.DrawingRenderer.Tests/Chart/LineChartToSvgTests.cs +++ b/src/EPPlus.DrawingRenderer.Tests/Chart/LineChartToSvgTests.cs @@ -19,7 +19,7 @@ public void GenerateSvgForLineCharts_sheet1() { var ws = p.Workbook.Worksheets[0]; - //var ix = 0; + //var ix = 4; //var c = ws.Drawings[ix]; //var svg = c.ToSvg(); //SaveTextFileToWorkbook($"svg\\ChartForSvg_ind{ix++}.svg", svg); diff --git a/src/EPPlus.DrawingRenderer/RenderItems/SvgUserSpaceSettings.cs b/src/EPPlus.DrawingRenderer/RenderItems/SvgUserSpaceSettings.cs index a2a14ea4dd..2fa3be83e7 100644 --- a/src/EPPlus.DrawingRenderer/RenderItems/SvgUserSpaceSettings.cs +++ b/src/EPPlus.DrawingRenderer/RenderItems/SvgUserSpaceSettings.cs @@ -26,5 +26,9 @@ public enum UserSpaceSettings /// Will set the user space to the parents coordinates. This is used for gradients and patterns that are inside a group and should be relative to the parent. /// UserSpaceOnUse_Parent = 2, + /// + /// Will set the user space to the objects coordinates. This is used for gradients and patterns that are inside a group and should be relative to the object. + /// + UserSpaceOnUse_Object = 3, } } \ No newline at end of file diff --git a/src/EPPlus.DrawingRenderer/Svg/Core/SvgShapeRenderer.cs b/src/EPPlus.DrawingRenderer/Svg/Core/SvgShapeRenderer.cs index 7dea9c8e46..d37e885f49 100644 --- a/src/EPPlus.DrawingRenderer/Svg/Core/SvgShapeRenderer.cs +++ b/src/EPPlus.DrawingRenderer/Svg/Core/SvgShapeRenderer.cs @@ -586,12 +586,26 @@ private void SetStopColors(StringBuilder defSb, RenderGradientFill gradientFill, private string GetXy(RenderItem item, UserSpaceSettings userSpace, double? angle) { - if (userSpace == UserSpaceSettings.UserSpaceOnUse_Parent) + if (userSpace != UserSpaceSettings.ObjectBoundingBox) { double theta = MathHelper.Radians((angle ?? 90) % 360); - var l = item.Bounds.Left; - var t = item.Bounds.Top; + double l, t; + switch (userSpace) + { + case UserSpaceSettings.UserSpaceOnUse_Parent: + l = item.Bounds.Left; + t = item.Bounds.Top; + break; + case UserSpaceSettings.UserSpaceOnUse_Global: + l = item.Bounds.Left; + t = item.Bounds.Top; + break; + default: + l = t = 0; + break; + } + var w = item.Bounds.Width; var h = item.Bounds.Height; diff --git a/src/EPPlus/Drawing/Renderer/Chart/ChartAxisRenderer.cs b/src/EPPlus/Drawing/Renderer/Chart/ChartAxisRenderer.cs index d1094551ae..93fb3c388c 100644 --- a/src/EPPlus/Drawing/Renderer/Chart/ChartAxisRenderer.cs +++ b/src/EPPlus/Drawing/Renderer/Chart/ChartAxisRenderer.cs @@ -242,7 +242,7 @@ public List Values public eTimeUnit? MajorDateUnit { get; set; } public eTextOrientation LabelOrientation { get; set; } public bool IsDateAutoAxis { get; set; } - public bool IsNumericAutoAxis { get; set; } + public bool IsNumericAutoAxis { get; set; } //TODO: Not used? Removed if not used. public bool IsDateScale { get; @@ -961,10 +961,10 @@ internal double GetPositionInPlotarea(double val, bool startValue=false) 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, out bool isNumeric); - if(isCount == false && isNumeric && ax.AxisType == eAxisType.Cat) - { - IsDateAutoAxis = true; - } + //if(isCount == false && isNumeric && ax.AxisType == eAxisType.Cat) + //{ + // IsDateAutoAxis = true; + //} var options = new AxisOptions { LockedMin = ax.MinValue, @@ -1047,7 +1047,7 @@ protected List GetAxisValue(ExcelChartAxisStandard ax, RenderItem rect, { majorUnit = 1; dateUnit = null; - for (int i=1;i<=max;i++) + for (int i=1;i <= max;i++) { l.Add(i); } @@ -1126,7 +1126,7 @@ protected List GetAxisValue(ExcelChartAxisStandard ax, RenderItem rect, majorUnit = res.MajorInterval; dateUnit= null; orientation = eTextOrientation.Horizontal; - IsNumericAutoAxis = true; + IsNumericAutoAxis = false; } return l; diff --git a/src/EPPlus/Drawing/Renderer/Chart/ChartPlotareaRenderer.cs b/src/EPPlus/Drawing/Renderer/Chart/ChartPlotareaRenderer.cs index 34cdde4040..cfabf254d4 100644 --- a/src/EPPlus/Drawing/Renderer/Chart/ChartPlotareaRenderer.cs +++ b/src/EPPlus/Drawing/Renderer/Chart/ChartPlotareaRenderer.cs @@ -36,7 +36,8 @@ internal void SetPlotAreaRectangle() { var pa = Chart.PlotArea; TopMargin = BottomMargin = LeftMargin = RightMargin = 10.5; //14px - var rect = new RectRenderItem(ChartRenderer.Bounds); + Group = new GroupRenderItem(ChartRenderer.Bounds); + var rect = new RectRenderItem(Group.Bounds); if (pa.Layout.HasLayout) { rect = GetRectFromManualLayout(ChartRenderer, pa.Layout); @@ -49,7 +50,6 @@ internal void SetPlotAreaRectangle() rect.Height = GetPlotAreaHeight(rect); } - Group = new GroupRenderItem(ChartRenderer.Bounds); Group.Bounds.Top = rect.Top; Group.Bounds.Left = rect.Left; rect.Top = rect.Left = 0; diff --git a/src/EPPlus/Drawing/Renderer/Chart/ChartTypeDrawers/ChartTypeDrawer.cs b/src/EPPlus/Drawing/Renderer/Chart/ChartTypeDrawers/ChartTypeDrawer.cs index cf6f684a14..9fd79c1d94 100644 --- a/src/EPPlus/Drawing/Renderer/Chart/ChartTypeDrawers/ChartTypeDrawer.cs +++ b/src/EPPlus/Drawing/Renderer/Chart/ChartTypeDrawers/ChartTypeDrawer.cs @@ -238,7 +238,7 @@ internal static void SetFillDataPoint(ExcelChart chart, ExcelChartStandardSerie var theme = chart.WorkSheet.Workbook.ThemeManager.GetOrCreateTheme(); var color = GetVaryColor(theme, chart.StyleManager?.ColorsManager, index); - item.SetDrawingPropertiesFill(theme, dp.Fill.IsEmpty ? cStandardSerie.Fill : dp.Fill, entry?.FillReference.Color, UserSpaceSettings.ObjectBoundingBox, color); + item.SetDrawingPropertiesFill(theme, dp.Fill.IsEmpty ? cStandardSerie.Fill : dp.Fill, entry?.FillReference.Color, UserSpaceSettings.UserSpaceOnUse_Object, color); item.SetDrawingPropertiesBorder(theme, dp.Border.IsEmpty ? cStandardSerie.Border : dp.Border, entry?.BorderReference.Color, dp.Border.Fill.Style != eFillStyle.NoFill, null, 0.75); } @@ -249,12 +249,12 @@ internal static void SetFillSerie(ExcelChart chart, ExcelChart ct, ExcelChartSta { //Get the color based on the index, if no style is set. Accent1, Accent2, Accent3... var color = GetVaryColor(theme, chart.StyleManager.ColorsManager, index); - item.SetDrawingPropertiesFill(theme, cStandardSerie.Fill, chart.StyleManager.Style?.SeriesLine.FillReference.Color, UserSpaceSettings.ObjectBoundingBox, color); + item.SetDrawingPropertiesFill(theme, cStandardSerie.Fill, chart.StyleManager.Style?.SeriesLine.FillReference.Color, UserSpaceSettings.UserSpaceOnUse_Object, color); } else { var color = GetVaryColor(theme, chart.StyleManager?.ColorsManager, serieIndex); - item.SetDrawingPropertiesFill(theme, cStandardSerie.Fill, chart.StyleManager.Style?.SeriesLine.FillReference.Color, UserSpaceSettings.ObjectBoundingBox, color); + item.SetDrawingPropertiesFill(theme, cStandardSerie.Fill, chart.StyleManager.Style?.SeriesLine.FillReference.Color, UserSpaceSettings.UserSpaceOnUse_Object, color); } item.SetDrawingPropertiesBorder(theme, cStandardSerie.Border, chart.StyleManager.Style?.SeriesLine.BorderReference.Color, cStandardSerie.Border.Fill.Style != eFillStyle.NoFill, null, 0.75); } From 5c0090cdd9870e3c17d60ea9e1836848846b11de Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jan=20K=C3=A4llman?= Date: Thu, 20 Aug 2026 07:25:09 +0200 Subject: [PATCH 15/30] Fixed Tint/Shade transform calculation --- .../Utils/TypeConversion/ColorConverter.cs | 29 ++++++- src/EPPlusTest/Drawing/ThemeTest.cs | 86 ++++++++++++++++++- 2 files changed, 112 insertions(+), 3 deletions(-) diff --git a/src/EPPlus/Utils/TypeConversion/ColorConverter.cs b/src/EPPlus/Utils/TypeConversion/ColorConverter.cs index 332eb384fc..41346e80f9 100644 --- a/src/EPPlus/Utils/TypeConversion/ColorConverter.cs +++ b/src/EPPlus/Utils/TypeConversion/ColorConverter.cs @@ -248,7 +248,7 @@ internal static Color AlternativeTint(Color ret, double tint) return ret; } - internal static Color ApplyTintDrawing(Color ret, double tint) + internal static Color ApplyTintDrawing_old(Color ret, double tint) { //if (tint == 0) //{ @@ -285,7 +285,34 @@ internal static Color ApplyTintDrawing(Color ret, double tint) } return ret; } + internal static Color ApplyTintDrawing(Color color, double tint) + { + if (tint > 1) tint = 1; + if (tint < -1) tint = -1; + byte r = ApplyChannel(color.R, tint); + byte g = ApplyChannel(color.G, tint); + byte b = ApplyChannel(color.B, tint); + + return Color.FromArgb(color.A, r, g, b); + } + + private static byte ApplyChannel(byte channel, double tint) + { + double linear = SrgbToLinear(channel / 255.0); + + double result = tint <= 0 + ? linear * (1.0 + tint) // shade: toward black + : linear * (1.0 - tint) + tint; // tint: toward white + + return (byte)Math.Round(LinearToSrgb(result) * 255.0); + } + + private static double SrgbToLinear(double c) => + c <= 0.04045 ? c / 12.92 : Math.Pow((c + 0.055) / 1.055, 2.4); + private static double LinearToSrgb(double c) => + c <= 0.0031308 ? c * 12.92 : 1.055 * Math.Pow(c, 1.0 / 2.4) - 0.055; + internal static Color ApplyBlend(Color color, Color blendColor, double percent) { var colorPercent = 1 - percent; diff --git a/src/EPPlusTest/Drawing/ThemeTest.cs b/src/EPPlusTest/Drawing/ThemeTest.cs index 55643c9b6e..19a07a4fd7 100644 --- a/src/EPPlusTest/Drawing/ThemeTest.cs +++ b/src/EPPlusTest/Drawing/ThemeTest.cs @@ -40,7 +40,7 @@ Date Author Change using System.Collections.Generic; using System.Drawing; using System.Reflection; - +using tc = OfficeOpenXml.Utils.TypeConversion; namespace EPPlusTest.Drawing { [TestClass] @@ -346,7 +346,6 @@ public void LoadThmx_FormatScheme_Fills() Assert.AreEqual(eColorTransformType.SatMod, currentTheme.FormatScheme.BackgroundFillStyle[1].SolidFill.Color.Transforms[1].Type); Assert.AreEqual(170, currentTheme.FormatScheme.BackgroundFillStyle[1].SolidFill.Color.Transforms[1].Value); - Assert.AreEqual(eFillStyle.GradientFill, currentTheme.FormatScheme.BackgroundFillStyle[2].Style); Assert.AreEqual(3, currentTheme.FormatScheme.BackgroundFillStyle[2].GradientFill.Colors.Count); Assert.AreEqual(eDrawingColorType.Scheme, currentTheme.FormatScheme.BackgroundFillStyle[2].GradientFill.Colors[0].Color.ColorType); @@ -381,6 +380,89 @@ public void ReadThmx() Assert.IsNotNull(_pck.Workbook.ThemeManager.CurrentTheme); } + [TestMethod] + public void Shade50percent() + { + var color1 = Color.FromArgb(0, 255, 0); + var c = tc.ColorConverter.ApplyTintDrawing(color1, -0.5); + + Assert.AreEqual((double)Color.FromArgb(0x0, 0xBC, 0x0).ToArgb(), c.ToArgb()); + } + [TestMethod] + public void Shade85percent() + { + var color1 = Color.FromArgb(0, 255, 0); + var c = tc.ColorConverter.ApplyTintDrawing(color1, -0.85); + + Assert.AreEqual((double)Color.FromArgb(0x0, 0xBC, 0x0).ToArgb(), c.ToArgb()); + } + [TestMethod] + public void Accent15Dark() + { + var expectedFill = Color.FromArgb(255, 21, 96, 130); + var c = tc.ColorConverter.ApplyTintDrawing(expectedFill, -0.85); + var expectedResult = Color.FromArgb(255, 4, 36, 51); + Assert.AreEqual((double)expectedResult.ToArgb(), c.ToArgb()); + } + + [TestMethod] + + public void GreenApply99Dark() + { + var origColor = Color.FromArgb(255, 0, 255, 0); + var c = tc.ColorConverter.ApplyTintDrawing(origColor, -0.99); + var expectedResult = Color.FromArgb(255, 0, 25, 0); + Assert.AreEqual((double)expectedResult.ToArgb(), c.ToArgb()); + } + + + [TestMethod] + public void Green25Apply50Light() + { + var origColor = Color.FromArgb(255, 0, 25, 0); + var c = tc.ColorConverter.ApplyTintDrawing(origColor, 0.5); + var expectedResult = Color.FromArgb(255, 188, 188, 188); + Assert.AreEqual((double)expectedResult.ToArgb(), c.ToArgb()); + } + [TestMethod] + public void Tint60Shade60() + { + var myColorOrig = ColorTranslator.FromHtml("#DB1BC0"); + var myColorBrightened = tc.ColorConverter.ApplyTintDrawing(myColorOrig, 0.6); + + //darken 0.6 expected output: #910E7F + var myColorDarkened = tc.ColorConverter.ApplyTintDrawing(myColorOrig, -0.6); + + Assert.AreEqual(Color.FromArgb(255, 241, 204, 232).ToArgb(), myColorBrightened.ToArgb()); + + Assert.AreEqual(Color.FromArgb(255, 145, 14, 127).ToArgb(), myColorDarkened.ToArgb()); + } + [TestMethod] + public void colormod() + { + var accent1 = Color.FromArgb(255, 21, 96, 130); + /* + + + + + */ + //var lmod1 = tc.ColorConverter.ApplyLumMod(accent1, 0.6); + //var smod1 = tc.ColorConverter.ApplySatMod(lmod1, 1.03); + //var lmod2 = tc.ColorConverter.ApplyLumMod(smod1, 1.02); + //var tint = tc.ColorConverter.ApplyTintDrawing(lmod2, 1-0.94); + + var smod1 = tc.ColorConverter.ApplySatMod(accent1, 1.03); + var lmod2 = tc.ColorConverter.ApplyLumMod(smod1, 1.02); + var tint = tc.ColorConverter.ApplyTintDrawing(lmod2, 1 - 0.94); + + var expected = ColorTranslator.FromHtml("#497592"); + + Assert.AreEqual(expected.ToArgb(), tint.ToArgb()); + + //Assert.AreEqual(Color.FromArgb(255, 145, 14, 127).ToArgb(), myColorDarkened.ToArgb()); + } + #region Theme Savon [TestMethod] public void ValidateThemeSavonWithBlipFill() From 46a3a0eb12313757aafc74627434f2ecca874aa0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jan=20K=C3=A4llman?= Date: Thu, 20 Aug 2026 08:06:31 +0200 Subject: [PATCH 16/30] #2456-Removed Finalizers from the cell store and the ExcelVmlDrawingCollection --- src/EPPlus/Core/CellStore/CellStore.cs | 6 +-- src/EPPlus/Core/CellStore/ColumnIndex.cs | 4 -- src/EPPlus/Core/CellStore/PageIndex.cs | 4 -- .../Drawing/Vml/ExcelVmlDrawingCollection.cs | 53 +------------------ 4 files changed, 3 insertions(+), 64 deletions(-) diff --git a/src/EPPlus/Core/CellStore/CellStore.cs b/src/EPPlus/Core/CellStore/CellStore.cs index 1d86c8986b..6d39a7b219 100644 --- a/src/EPPlus/Core/CellStore/CellStore.cs +++ b/src/EPPlus/Core/CellStore/CellStore.cs @@ -62,10 +62,6 @@ public CellStore() { _columnIndex = new ColumnIndex[CellStoreSettings.ColSizeMin]; } - ~CellStore() - { - _columnIndex = null; - } internal bool HasValues { get @@ -1159,9 +1155,9 @@ private void AddColumn(int pos, int Column) public void Dispose() { - if (_columnIndex == null) return; lock (_syncRoot) { + if (_columnIndex == null) return; for (var c = 0; c < ColumnCount; c++) { if (_columnIndex[c] != null) diff --git a/src/EPPlus/Core/CellStore/ColumnIndex.cs b/src/EPPlus/Core/CellStore/ColumnIndex.cs index 2339a34d7a..7c2f2cc2fe 100644 --- a/src/EPPlus/Core/CellStore/ColumnIndex.cs +++ b/src/EPPlus/Core/CellStore/ColumnIndex.cs @@ -27,10 +27,6 @@ public ColumnIndex() _pages = new PageIndex[CellStoreSettings.PagesPerColumnMin]; PageCount = 0; } - ~ColumnIndex() - { - _pages = null; - } internal int GetPagePosition(int Row) { var page = (Row >> CellStoreSettings._pageBits); diff --git a/src/EPPlus/Core/CellStore/PageIndex.cs b/src/EPPlus/Core/CellStore/PageIndex.cs index fbbbdca241..4fadaa7432 100644 --- a/src/EPPlus/Core/CellStore/PageIndex.cs +++ b/src/EPPlus/Core/CellStore/PageIndex.cs @@ -45,10 +45,6 @@ public PageIndex(PageIndex pageItem, int start, int size, short index, int offse Index = index; Offset = offset; } - ~PageIndex() - { - Rows = null; - } internal int Offset = 0; /// /// Rows in the rows collection. diff --git a/src/EPPlus/Drawing/Vml/ExcelVmlDrawingCollection.cs b/src/EPPlus/Drawing/Vml/ExcelVmlDrawingCollection.cs index 0f00683fcb..eeb897e558 100644 --- a/src/EPPlus/Drawing/Vml/ExcelVmlDrawingCollection.cs +++ b/src/EPPlus/Drawing/Vml/ExcelVmlDrawingCollection.cs @@ -49,11 +49,6 @@ internal ExcelVmlDrawingCollection(ExcelWorksheet ws, Uri uri) : AddDrawingsFromXml(ws); } } - ~ExcelVmlDrawingCollection() - { - _drawingsCellStore?.Dispose(); - _drawingsCellStore = null; - } protected internal void AddDrawingsFromXml(ExcelWorksheet ws) { var nodes = VmlDrawingXml.SelectNodes("//v:shape", NameSpaceManager); @@ -198,7 +193,6 @@ private XmlNode AddCommentDrawing(ExcelRangeBase cell) node.SetAttribute("id", GetNewId()); node.SetAttribute("type", "#_x0000_t202"); node.SetAttribute("style", "position:absolute;z-index:1; visibility:hidden"); - //node.SetAttribute("style", "position:absolute; margin-left:59.25pt;margin-top:1.5pt;width:108pt;height:59.25pt;z-index:1; visibility:hidden"); node.SetAttribute("fillcolor", "#ffffe1"); node.SetAttribute("insetmode", ExcelPackage.schemaMicrosoftOffice, "auto"); @@ -307,7 +301,6 @@ internal XmlNode AddSignatureLineDrawing(Guid lineId) public XmlNode AddDigitalSignatureLineDrawing(Guid id) { CreateVmlPart(false); //Create the vml part to be able to create related parts (like signatureLine). - //var vmlRel = Part.CreateRelationship(mediaUri, TargetMode.Internal, ExcelPackage.schemaRelationships + "/image"); var shapeElement = VmlDrawingXml.CreateElement("v", "shape", ExcelPackage.schemaMicrosoftVml); VmlDrawingXml.DocumentElement.AppendChild(shapeElement); @@ -450,14 +443,10 @@ internal XmlNode AddOleObjectDrawing(string spid, Uri mediaUri) vml.Append(""); vml.AppendFormat("", vmlRel.Id); vml.Append(""); - //vml.Append(""); vml.Append(""); vml.AppendFormat("0, 0, 0, 0, 1, 32, 3, 12"); //SET VALUE BASED ON MEDIA - //vml.Append("False"); vml.Append("Pict"); vml.Append(""); - //vml.Append(""); - //vml.Append(""); vml.Append(""); shapeElement.InnerXml = vml.ToString(); @@ -590,10 +579,8 @@ private void SetShapeAttributes(ExcelControl ctrl, XmlElement shapeElement) case eControlType.RadioButton: shapeElement.SetAttribute("fillcolor", "windows [65]"); shapeElement.SetAttribute("strokecolor", "windowText [64]"); - //shapeElement.SetAttribute("button", ExcelPackage.schemaMicrosoftOffice, "t"); shapeElement.SetAttribute("stroked", "f"); shapeElement.SetAttribute("filled", "f"); - //style = "position:absolute; margin-left:15pt;margin-top:10.5pt;width:120.75pt;height:23.25pt;z-index:1; mso-wrap-style:tight" type = "#_x0000_t201" > break; case eControlType.ListBox: case eControlType.DropDown: @@ -718,48 +705,12 @@ IEnumerator IEnumerable.GetEnumerator() return _drawings.GetEnumerator(); } - ///// - ///// The current range when enumerating - ///// - //public ExcelVmlDrawingComment Current - //{ - // get - // { - // return _enum.Current; - // } - //} - - ///// - ///// The current range when enumerating - ///// - //object IEnumerator.Current - //{ - // get - // { - // return _enum.Current; - // } - //} - - //public bool MoveNext() - //{ - // return _enum.Next(); - //} - - //public void Reset() - //{ - // if (_enum != null) _enum.Dispose(); - // _enum = new CellStoreEnumerator(_drawingsCellStore, 1, 1, ExcelPackage.MaxRows, ExcelPackage.MaxColumns); - //} void IDisposable.Dispose() { - _drawingsCellStore.Dispose(); + _drawingsCellStore?.Dispose(); + _drawingsCellStore = null; } - //public void Dispose() - //{ - // throw new NotImplementedException(); - //} - internal string GetOuterXmlWithoutSignatureLines() { var outerXml = VmlDrawingXml.OuterXml; From e89ff1d10ccd47c828cde9e8455262f959b3fb03 Mon Sep 17 00:00:00 2001 From: AdrianEPPlus <162118292+AdrianEPPlus@users.noreply.github.com> Date: Thu, 20 Aug 2026 08:22:59 +0200 Subject: [PATCH 17/30] fixed issue (#2475) --- src/EPPlus/ExcelWorksheet.cs | 3 ++- src/EPPlusTest/Issues/WorksheetIssues.cs | 20 ++++++++++++++++++++ 2 files changed, 22 insertions(+), 1 deletion(-) diff --git a/src/EPPlus/ExcelWorksheet.cs b/src/EPPlus/ExcelWorksheet.cs index 88925be54a..3242218366 100644 --- a/src/EPPlus/ExcelWorksheet.cs +++ b/src/EPPlus/ExcelWorksheet.cs @@ -2150,6 +2150,7 @@ internal ExcelColumn CopyColumn(ExcelColumn c, int col, int maxCol) { ExcelColumn newC = new ExcelColumn(this, col); newC.ColumnMax = maxCol < ExcelPackage.MaxColumns ? maxCol : ExcelPackage.MaxColumns; + SetValueInner(0, col, newC); if (c.StyleName != "") newC.StyleName = c.StyleName; else @@ -2160,7 +2161,7 @@ internal ExcelColumn CopyColumn(ExcelColumn c, int col, int maxCol) newC.BestFit = c.BestFit; newC._width = c._width; newC._hidden = c._hidden; - SetValueInner(0, col, newC); + return newC; } /// diff --git a/src/EPPlusTest/Issues/WorksheetIssues.cs b/src/EPPlusTest/Issues/WorksheetIssues.cs index 96038fff17..b034e344be 100644 --- a/src/EPPlusTest/Issues/WorksheetIssues.cs +++ b/src/EPPlusTest/Issues/WorksheetIssues.cs @@ -1203,5 +1203,25 @@ public void Issue2445() Assert.AreEqual(true, worksheet.OutLineSummaryBelow); Assert.AreEqual(true, worksheet.OutLineSummaryRight); } + + [TestMethod] + public void MultiColumnStyle_WhenSplitBySubRange_ShouldInheritStyle() + { + using (var p = new ExcelPackage()) + { + var ws = p.Workbook.Worksheets.Add("TestSheet"); + var e1 = ws.Cells["E1"]; + // 1. Set font size 9 on columns A to J + ws.Cells["A:J"].Style.Font.Size = 9; + // 2. Modify a style property on sub-range B:C + ws.Cells["B:C"].Style.Font.Bold = true; + // 3. Populate cell E1 in column E + ws.Cells["E1"].Value = "Test"; + // Expected: Font size 9 + // Actual: Font size 11 (Assert.AreEqual failed. Expected:<9>. Actual:<11>.) + Assert.AreEqual(9f, ws.Cells["E1"].Style.Font.Size, "Cell E1 font size should be 9"); + } + } + } } From 3fc7fe169b427e774db7e105b42ea979cfab7915 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jan=20K=C3=A4llman?= Date: Thu, 20 Aug 2026 09:11:54 +0200 Subject: [PATCH 18/30] #2456-Removed finalizers on cell store classes. (#2477) --- src/EPPlus/Core/CellStore/ColumnIndex.cs | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/EPPlus/Core/CellStore/ColumnIndex.cs b/src/EPPlus/Core/CellStore/ColumnIndex.cs index 7c2f2cc2fe..734fc2915d 100644 --- a/src/EPPlus/Core/CellStore/ColumnIndex.cs +++ b/src/EPPlus/Core/CellStore/ColumnIndex.cs @@ -270,7 +270,11 @@ public void Dispose() (_pages[p] as IDisposable)?.Dispose(); } _pages = null; - if (_values != null) _values.Clear(); + if (_values != null) + { + _values.Clear(); + _values = null; + } } } } \ No newline at end of file From f8c1a9f05bf77cc45b94b3f48179727242bd4adc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jan=20K=C3=A4llman?= Date: Thu, 20 Aug 2026 12:59:02 +0200 Subject: [PATCH 19/30] EPPlus version 8.7.0 --- appveyor8.yml | 10 +++++----- docs/articles/breakingchanges.md | 4 +++- docs/articles/fixedissues.md | 14 ++++++++++++++ src/EPPlus/EPPlus.csproj | 19 ++++++++++++------- src/EPPlus/EPPlusLicense.cs | 2 +- .../Excel/Functions/ExcelFunction.cs | 4 +++- .../Excel/FunctionRepositoryTests.cs | 4 ++-- src/EPPlusTest/Issues/DefinedNameIssues.cs | 12 ++++++------ 8 files changed, 46 insertions(+), 23 deletions(-) diff --git a/appveyor8.yml b/appveyor8.yml index c7dc848c4c..df36d41cf0 100644 --- a/appveyor8.yml +++ b/appveyor8.yml @@ -1,4 +1,4 @@ -version: 8.6.3.{build} +version: 8.7.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.6.3.$env:appveyor_build_number-$(Get-Date -format yyyyMMdd)-$env:appveyor_repo_branch" + Update-AppveyorBuild -Version "8.7.0.$env:appveyor_build_number-$(Get-Date -format yyyyMMdd)-$env:appveyor_repo_branch" - Write-Host "8.6.3.$env:appveyor_build_number-$(Get-Date -format yyyyMMdd)-$env:appveyor_repo_branch" + Write-Host "8.7.0.$env:appveyor_build_number-$(Get-Date -format yyyyMMdd)-$env:appveyor_repo_branch" dotnet_csproj: patch: true file: '**\*.csproj' version: '{version}' - assembly_version: 8.6.3.{build} - file_version: 8.6.3.{build} + assembly_version: 8.7.0.{build} + file_version: 8.7.0.{build} nuget: project_feed: true before_build: diff --git a/docs/articles/breakingchanges.md b/docs/articles/breakingchanges.md index d6abb22c67..e08919ba35 100644 --- a/docs/articles/breakingchanges.md +++ b/docs/articles/breakingchanges.md @@ -220,4 +220,6 @@ Renaming worksheet's will now change the formula correctly to include single quo ### 8.5.5 * `ws.Cells["A1"].RichText` no longer sets cells with `null` to `string.empty` * .RichText no longer sets the cell or contents to be RichText automatically. - This is instead done when properties such as; `.Text`, `.Add` or `.Insert` are set on the .RichText property. \ No newline at end of file + This is instead done when properties such as; `.Text`, `.Add` or `.Insert` are set on the .RichText property. +### 8.7.0 +* The default value of the ´OutLineSummaryRight´ and ´OutLineSummaryBelow´ properties on ExcelWorksheet is now true. diff --git a/docs/articles/fixedissues.md b/docs/articles/fixedissues.md index 3183bd4990..dc3b0be009 100644 --- a/docs/articles/fixedissues.md +++ b/docs/articles/fixedissues.md @@ -1,4 +1,18 @@ # Features / Fixed issues - EPPlus 8 +## Version 8.7.0 +### Minor Features +* The ´ExcelPackage´ Save functions now support saving as a template (.xltx, .xltm), see https://github.com/EPPlusSoftware/EPPlus/wiki/Save-as-template(.xltx,-.xltm). +* Added support for ´HideItemsWithNoData´ on pivot table slicer caches (Slicer.Cache.HideItemsWithNoData) +* When copying a worksheet, you can now assign custom names to copied tables and pivot tables through callbacks on ExcelWorksheetCopyOptions (TableCopyHandler / PivotTableCopyHandler), instead of relying on generated names like Table1. Formula references in copied tables are updated automatically. See https://github.com/EPPlusSoftware/EPPlus/wiki/Copy-Ranges-or-Entire-Worksheets#naming-tables-and-pivot-tables-when-copying-a-worksheet +* The new ´DisableImageFunctionDownloads´ property on ´ExcelCalculationOption´ (default false) lets you turn off the outbound network request that ´IMAGE´ makes during calculation. When true, ´IMAGE´ returns ´#NAME?´ instead of downloading. Useful when calculating untrusted workbooks. Existing images are unaffected. Thanks to Derin (Paranoidgrinch) for reporting this. +### Fixed issues +* When calculating the formulas accessed ranges, EPPlus could sometimes update the wrong worksheet's dictionary after updating dirty ranges for dynamic array formulas. +* SUMIFS/AVERAGEIFS/COUNTIFS did not create the dependency chain correctly when having multiple criteria ranges, which could cause incorrect circular references. +* The default value of the ´OutLineSummaryRight´ and ´OutLineSummaryBelow´ properties on ExcelWorksheet is now true. +* Removed unnecessary finalizers from the cell store- and the ´ExcelVmlDrawingCollection´- classes. +* Fix for header/footer picture loss when copying worksheets. +* Fixed a regression (introduced in 8.5.0) where ´XLOOKUP´, ´VLOOKUP´, ´HLOOKUP´ and ´MATCH´ returned ´#N/A´ when the lookup range started before the first populated cell of the worksheet, for example a full-column lookup like A:A on a sheet whose data begins further down. +* Fixed column style lost on remaining columns when a sub-range column style was modified. Thanks to Lieven De Foor. ## Version 8.6.3 ### Security * Updated System.Security.Cryptography.Xml to address five security vulnerabilities in the .NET XML signing dependency: four denial of service vulnerabilities (CVE-2026-47302, CVE-2026-50525, CVE-2026-50527, CVE-2026-50648) and one security feature bypass (CVE-2026-47304). The package is updated to 8.0.4 (.NET Framework, .NET 8 and .NET Standard), 9.0.18 (.NET 9) and 10.0.10 (.NET 10). diff --git a/src/EPPlus/EPPlus.csproj b/src/EPPlus/EPPlus.csproj index dbf10f6e65..3baa9895ad 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.6.3.0 - 8.6.3.0 - 8.6.3 + 8.7.0.0 + 8.7.0.0 + 8.7.0 true https://epplussoftware.com EPPlus Software AB @@ -18,7 +18,7 @@ readme.md EPPlus Software AB - EPPlus 8.6.3 + EPPlus 8.7.0 IMPORTANT NOTICE! From version 5 EPPlus changes the license model using a dual license, Polyform Non Commercial / Commercial license. @@ -26,16 +26,20 @@ 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.7.0 + * New overloads for ExcelPackage.Save functions to save a package as a template (xlst or xltm). + * Minor bug fixes. See https://epplussoftware.com/Developers/MinorFeaturesAndIssues + ## Version 8.6.3 * Updated System.Security.Cryptography.Xml to address security vulnerabilities (CVE-2026-47302, CVE-2026-47304, CVE-2026-50525, CVE-2026-50527, CVE-2026-50648). ## Version 8.6.2 - * Minor bug fixes. See https://epplussoftware.com/Developers/MinorFeaturesAndIssues + * Minor bug fixes. ## Version 8.6.1 * New functions: * REGEXEXTRACT, REGEXREPLACE, REGEXTEST - * Minor bug fixes. See https://epplussoftware.com/Developers/MinorFeaturesAndIssues + * Minor bug fixes. ## Version 8.6.0 * New functions: @@ -586,8 +590,9 @@ A list of fixed issues can be found here https://epplussoftware.com/docs/8.6/articles/fixedissues.html Version history + 8.7.0 20260820 Save as template. Minor bug fixes. See https://epplussoftware.com/Developers/MinorFeaturesAndIssues 8.6.3 20260724 Updated System.Security.Cryptography.Xml for security vulnerabilities. - 8.6.2 20260721 Minor bug fixes. See https://epplussoftware.com/Developers/MinorFeaturesAndIssues + 8.6.2 20260721 Minor bug fixes. 8.6.1 20260616 3 new functions. Minor bug fixes. 8.6.0 20260529 9 new functions. Support for trim Reference operator. 8.5.4 20260430 Minor bug fixes. diff --git a/src/EPPlus/EPPlusLicense.cs b/src/EPPlus/EPPlusLicense.cs index a4ec732421..07e9a49fa2 100644 --- a/src/EPPlus/EPPlusLicense.cs +++ b/src/EPPlus/EPPlusLicense.cs @@ -19,7 +19,7 @@ public class EPPlusLicense { private static ExcelPackageConfiguration _configuration = new ExcelPackageConfiguration(); static bool _licenseSet = false; - internal const string _versionDate = "2026-05-28"; + internal const string _versionDate = "2026-08-20"; /// /// The license key used for a commercial license. /// diff --git a/src/EPPlus/FormulaParsing/Excel/Functions/ExcelFunction.cs b/src/EPPlus/FormulaParsing/Excel/Functions/ExcelFunction.cs index 0a98779c10..91fc8d0a92 100644 --- a/src/EPPlus/FormulaParsing/Excel/Functions/ExcelFunction.cs +++ b/src/EPPlus/FormulaParsing/Excel/Functions/ExcelFunction.cs @@ -128,7 +128,9 @@ public virtual void GetNewParameterAddress(IList args, int index, { } - + /// + /// The name of the function. By default the name of the class is used. + /// public virtual string Name { get diff --git a/src/EPPlusTest/FormulaParsing/Excel/FunctionRepositoryTests.cs b/src/EPPlusTest/FormulaParsing/Excel/FunctionRepositoryTests.cs index b86c1e9931..94e379be6a 100644 --- a/src/EPPlusTest/FormulaParsing/Excel/FunctionRepositoryTests.cs +++ b/src/EPPlusTest/FormulaParsing/Excel/FunctionRepositoryTests.cs @@ -61,14 +61,14 @@ public TestFunctionModule() { var myFunction = new MyFunction(); var customCompiler = new MyFunctionCompiler(myFunction); - base.Functions.Add(MyFunction.Name, myFunction); + base.Functions.Add(myFunction.Name, myFunction); base.CustomCompilers.Add(typeof(MyFunction), customCompiler); } } public class MyFunction : ExcelFunction { - public const string Name = "MyFunction"; + public override string Name => "MyFunction"; public override int ArgumentMinLength => 0; public override CompileResult Execute(IList arguments, ParsingContext context) { diff --git a/src/EPPlusTest/Issues/DefinedNameIssues.cs b/src/EPPlusTest/Issues/DefinedNameIssues.cs index 15f7b8e221..fede10bdcc 100644 --- a/src/EPPlusTest/Issues/DefinedNameIssues.cs +++ b/src/EPPlusTest/Issues/DefinedNameIssues.cs @@ -184,8 +184,8 @@ static void RunTest(string name, Func<(ExcelPackage pkg, ExcelWorksheet ws1, Exc RunTest("Mode A: ws.Calculate(formula-string) is wrong", ctx => { - object? inWs2; - object? inWs1; + object inWs2; + object inWs1; try { inWs2 = ctx.ws2.Calculate(ctx.ws2.Cells["C1"].Formula); } catch (Exception ex) { inWs2 = $"EXCEPTION: {ex.GetType().Name}: {ex.Message}"; } Assert.AreEqual(inWs2, 10); @@ -224,8 +224,8 @@ static void RunTest(string name, Func<(ExcelPackage pkg, ExcelWorksheet ws1, Exc RunTest("Mode C: ws.Calculate(address) is right", ctx => { - object? fromWs2; - object? fromWs1; + object fromWs2; + object fromWs1; try { fromWs2 = ctx.ws2.Calculate("'Sheet2'!C1"); } catch (Exception ex) { fromWs2 = $"EXCEPTION: {ex.GetType().Name}: {ex.Message}"; } Assert.AreEqual(fromWs2, 10); @@ -238,8 +238,8 @@ static void RunTest(string name, Func<(ExcelPackage pkg, ExcelWorksheet ws1, Exc RunTest("Sanity: removing sheet-scoped name fixes formula-string eval", ctx => { // Demonstrate the fix within one workbook instance. - object? before; - object? after; + object before; + object after; try { before = ctx.ws2.Calculate(ctx.ws2.Cells["C1"].Formula); } catch (Exception ex) { before = $"EXCEPTION: {ex.GetType().Name}: {ex.Message}"; } From ff25e7934708f167dbf809e473b61e04e1be4869 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ossian=20Edstr=C3=B6m?= Date: Thu, 20 Aug 2026 13:55:34 +0200 Subject: [PATCH 20/30] Fixed edge-case fallbacks to use new fix --- .../Chart/ChartStyleFallbackTest.cs | 27 +++- src/EPPlus/Drawing/Renderer/ChartRenderer.cs | 122 ++++++++++-------- .../Utils/TypeConversion/ColorConverter.cs | 2 +- 3 files changed, 93 insertions(+), 58 deletions(-) diff --git a/src/EPPlus.DrawingRenderer.Tests/Chart/ChartStyleFallbackTest.cs b/src/EPPlus.DrawingRenderer.Tests/Chart/ChartStyleFallbackTest.cs index 0ed6683b30..dc62a3c150 100644 --- a/src/EPPlus.DrawingRenderer.Tests/Chart/ChartStyleFallbackTest.cs +++ b/src/EPPlus.DrawingRenderer.Tests/Chart/ChartStyleFallbackTest.cs @@ -11,6 +11,31 @@ namespace EPPlus.DrawingRenderer.Tests.Chart [TestClass] public class ChartStyleFallbackTest : TestBase { + [TestMethod] + public void ReadExcelFile() + { + ExcelPackage.License.SetNonCommercialOrganization("EPPlus Project"); + + CreatePathIfNotExists("StyleExamples\\"); + + using (var p = OpenTemplatePackage("StyleExamples\\ExcelUnchangedEmptyChart.xlsx")) + { + var ws = p.Workbook.Worksheets[0]; + + foreach (ExcelChart c in ws.Drawings) + { + var borderRef = c.StyleManager.Style.ChartArea.BorderReference; + var borderSetting = c.Border; + var borderDirectColor = borderSetting.Fill.Color; + + var svg = c.ToSvg(); + SaveTextFileToWorkbook($"svg\\ExcelDefault{ws.Name}_{c.Name}.svg", svg); + } + var fi = GetOutputFile("StyleExamples", "ExcelUnchangedEmptyChart_out.xlsx"); + p.SaveAs(fi); + } + } + [TestMethod] public void EpplusGeneratedChart() @@ -188,7 +213,7 @@ public void EditedTheme() var themeColor = tc.ColorConverter.GetThemeColor(theme, eThemeSchemeColor.Text1); var themedLine = theme.FormatScheme.BorderStyle[0]; //themeColor = tc.ColorConverter.ApplyTransforms(themeColor, themedLine.Fill.SolidFill.Color.Transforms); - themeColor = tc.ColorConverter.ApplyTintDrawing(themeColor, 0.285d); + themeColor = tc.ColorConverter.ApplyTintDrawing(themeColor, 0.55d); var ExpectedColor = Color.FromArgb(255, 255, 199, 199); Assert.AreEqual(ExpectedColor.ToArgb(), themeColor.ToArgb()); } diff --git a/src/EPPlus/Drawing/Renderer/ChartRenderer.cs b/src/EPPlus/Drawing/Renderer/ChartRenderer.cs index 8c2235ea01..8168fe4621 100644 --- a/src/EPPlus/Drawing/Renderer/ChartRenderer.cs +++ b/src/EPPlus/Drawing/Renderer/ChartRenderer.cs @@ -142,36 +142,39 @@ private void PlaceHorizontalAxis(ChartAxisRenderer horizontalAxis, bool isSecond { horizontalAxis.Rectangle.Width = Plotarea.Rectangle.Width; horizontalAxis.Rectangle.Left = Plotarea.Group.Left; - horizontalAxis.Line.X1 = (float)horizontalAxis.Rectangle.Left; - horizontalAxis.Line.X2 = (float)horizontalAxis.Rectangle.Right; - - var axisPos = horizontalAxis.Axis.ActualAxisPosition; - if (axisPos == eActualAxisPosition.Bottom) + horizontalAxis.Line?.X1 = (float)horizontalAxis.Rectangle.Left; + horizontalAxis.Line?.X2 = (float)horizontalAxis.Rectangle.Right; + + if (horizontalAxis.Line != null) { - if(isSecondary ==false && SecondHorizontalAxis != null && SecondHorizontalAxis.Axis.ActualAxisPosition==eActualAxisPosition.Bottom) + var axisPos = horizontalAxis.Axis.ActualAxisPosition; + if (axisPos == eActualAxisPosition.Bottom) { - horizontalAxis.Rectangle.Top = Plotarea.Group.Top + Plotarea.Rectangle.Height - SecondHorizontalAxis.Rectangle.Height; + if (isSecondary == false && SecondHorizontalAxis != null && SecondHorizontalAxis.Axis.ActualAxisPosition == eActualAxisPosition.Bottom) + { + horizontalAxis.Rectangle.Top = Plotarea.Group.Top + Plotarea.Rectangle.Height - SecondHorizontalAxis.Rectangle.Height; + } + else + { + horizontalAxis.Rectangle.Top = Plotarea.Group.Top + Plotarea.Rectangle.Height; + } + horizontalAxis.Line.Y1 = horizontalAxis.Line.Y2 = horizontalAxis.Rectangle.Top; + } + else if (axisPos == eActualAxisPosition.BottomSecond) + { + horizontalAxis.Rectangle.Top = Plotarea.Group.Top + Plotarea.Rectangle.Height + HorizontalAxis.Rectangle.Height; + horizontalAxis.Line.Y1 = horizontalAxis.Line.Y2 = horizontalAxis.Rectangle.Top; + } + else if (axisPos == eActualAxisPosition.Top) + { + horizontalAxis.Rectangle.Top = Plotarea.Group.Top - horizontalAxis.Rectangle.Height; + horizontalAxis.Line.Y1 = horizontalAxis.Line.Y2 = (float)Plotarea.Group.Top; } else { - horizontalAxis.Rectangle.Top = Plotarea.Group.Top + Plotarea.Rectangle.Height; + horizontalAxis.Rectangle.Top = Plotarea.Group.Top - horizontalAxis.Rectangle.Height - HorizontalAxis.Rectangle.Height; + horizontalAxis.Line.Y1 = horizontalAxis.Line.Y2 = horizontalAxis.Rectangle.Bottom; } - horizontalAxis.Line.Y1 = horizontalAxis.Line.Y2 = horizontalAxis.Rectangle.Top; - } - else if(axisPos == eActualAxisPosition.BottomSecond) - { - horizontalAxis.Rectangle.Top = Plotarea.Group.Top + Plotarea.Rectangle.Height + HorizontalAxis.Rectangle.Height; - horizontalAxis.Line.Y1 = horizontalAxis.Line.Y2 = horizontalAxis.Rectangle.Top; - } - else if(axisPos == eActualAxisPosition.Top) - { - horizontalAxis.Rectangle.Top = Plotarea.Group.Top - horizontalAxis.Rectangle.Height; - horizontalAxis.Line.Y1 = horizontalAxis.Line.Y2 = (float)Plotarea.Group.Top; - } - else - { - horizontalAxis.Rectangle.Top = Plotarea.Group.Top - horizontalAxis.Rectangle.Height - HorizontalAxis.Rectangle.Height; - horizontalAxis.Line.Y1 = horizontalAxis.Line.Y2 = horizontalAxis.Rectangle.Bottom; } } if (horizontalAxis.Title != null) @@ -262,29 +265,32 @@ private void PlaceVerticalAxis(ChartAxisRenderer verticalAxis) { verticalAxis.Rectangle.Top = Plotarea.Group.Top; verticalAxis.Rectangle.Height = Plotarea.Rectangle.Height; - verticalAxis.Line.Y1 = (float)verticalAxis.Rectangle.Top; - verticalAxis.Line.Y2 = (float)verticalAxis.Rectangle.Bottom; + verticalAxis.Line?.Y1 = (float)verticalAxis.Rectangle.Top; + verticalAxis.Line?.Y2 = (float)verticalAxis.Rectangle.Bottom; var axisPos = verticalAxis.Axis.ActualAxisPosition; - if (axisPos == eActualAxisPosition.Left) + if(verticalAxis.Line != null) { - verticalAxis.Rectangle.Left = Plotarea.Group.Left - verticalAxis.Rectangle.Width; - verticalAxis.Line.X1 = verticalAxis.Line.X2 = (float)Plotarea.Group.Left; - } - else if (axisPos == eActualAxisPosition.LeftSecond) - { - verticalAxis.Rectangle.Left = Plotarea.Group.Left - verticalAxis.Rectangle.Width - VerticalAxis.Rectangle.Width; - verticalAxis.Line.X1 = verticalAxis.Line.X2 = (float)Plotarea.Group.Left; - } - else if (axisPos == eActualAxisPosition.Right) - { - verticalAxis.Rectangle.Left = Plotarea.Group.Left + Plotarea.Rectangle.Width; - verticalAxis.Line.X1 = verticalAxis.Line.X2 = (float)Plotarea.Group.Left + Plotarea.Rectangle.Width; - } - else - { - verticalAxis.Rectangle.Left = Plotarea.Group.Left + Plotarea.Rectangle.Width + VerticalAxis.Rectangle.Width; - verticalAxis.Line.X1 = verticalAxis.Line.X2 = (float)Plotarea.Group.Left + Plotarea.Rectangle.Width; + if (axisPos == eActualAxisPosition.Left) + { + verticalAxis.Rectangle.Left = Plotarea.Group.Left - verticalAxis.Rectangle.Width; + verticalAxis.Line.X1 = verticalAxis.Line.X2 = (float)Plotarea.Group.Left; + } + else if (axisPos == eActualAxisPosition.LeftSecond) + { + verticalAxis.Rectangle.Left = Plotarea.Group.Left - verticalAxis.Rectangle.Width - VerticalAxis.Rectangle.Width; + verticalAxis.Line.X1 = verticalAxis.Line.X2 = (float)Plotarea.Group.Left; + } + else if (axisPos == eActualAxisPosition.Right) + { + verticalAxis.Rectangle.Left = Plotarea.Group.Left + Plotarea.Rectangle.Width; + verticalAxis.Line.X1 = verticalAxis.Line.X2 = (float)Plotarea.Group.Left + Plotarea.Rectangle.Width; + } + else + { + verticalAxis.Rectangle.Left = Plotarea.Group.Left + Plotarea.Rectangle.Width + VerticalAxis.Rectangle.Width; + verticalAxis.Line.X1 = verticalAxis.Line.X2 = (float)Plotarea.Group.Left + Plotarea.Rectangle.Width; + } } } @@ -448,21 +454,25 @@ private void SetChartArea(SvgRenderOptions options) if (themedLine.Fill.SolidFill.Color.Transforms.Count > 0) { - var testAlternative = tc.ColorConverter.AlternativeTint(themeColor.Value, 0.25d); - //had to guess/solve equation for values. According to excel it should still be 75%(0.25) but our calc is off bc of rounding or smth. - themeColor = tc.ColorConverter.ApplyTintDrawing(themeColor.Value, 0.285d); - //Arguably we should apply all transforms instead but even in this case if there is no ln node found in style it appears to default to 75% despite a scheme color existing in the theme - //themeColor = tc.ColorConverter.ApplyTransforms(themeColor.Value, themedLine.Fill.SolidFill.Color.Transforms); + //themeColor = tc.ColorConverter.ApplyTintDrawing(themeColor.Value, 0.15d); + //but even in this case if there is no ln node found in style it appears to default to 75% despite a scheme color existing in the theme + var convertedTheme = tc.ColorConverter.ApplyTransforms(themeColor.Value, themedLine.Fill.SolidFill.Color.Transforms); + var drawingTint = tc.ColorConverter.ApplyTintDrawing(themeColor.Value, 0.6d); + //themeColor = convertedTheme; + //Arguably we should apply all transforms instead + //themeColor = tc.ColorConverter.ApplyTintDrawing(themeColor.Value, 0.25d); + ////themeColor = tc.ColorConverter.ApplyTintDrawing(themeColor.Value, 0.285d); + ////Arguably we should apply all transforms instead but even in this case if there is no ln node found in style it appears to default to 75% despite a scheme color existing in the theme + ////themeColor = tc.ColorConverter.ApplyTransforms(themeColor.Value, themedLine.Fill.SolidFill.Color.Transforms); } else { - ////Color clr = Color.FromArgb(255, 128, 128, 128); - //var tstClr = tc.ColorConverter.ApplyTintDrawing(themeColor.Value, (1d -2.5d)); - //Default value- Should arguably be 0.75% tint themeColor but something is strange... - //It appears closer to 50 in this specific case - //It also appears to be tx1 (black) and apply color and tint 0.25 in vba but for us it's 0.5372d... - //0.5372 is however consistent with 137/255 and 137 is our expected result. - themeColor = tc.ColorConverter.ApplyTint(themeColor.Value, 0.5372d); + //Default value Should arguably be 75% tint themeColor but something is strange... + //It appears closer to 50% in this specific case + //It also appears to be tx1 (black) and apply color and tint 0.25 in vba + var newTheme = tc.ColorConverter.ApplyTintDrawing(themeColor.Value, 0.25d); + themeColor = newTheme; + } //themedLine.Fill.SolidFill.Color.Transforms.AddTint } diff --git a/src/EPPlus/Utils/TypeConversion/ColorConverter.cs b/src/EPPlus/Utils/TypeConversion/ColorConverter.cs index 41346e80f9..21c6eb07b0 100644 --- a/src/EPPlus/Utils/TypeConversion/ColorConverter.cs +++ b/src/EPPlus/Utils/TypeConversion/ColorConverter.cs @@ -304,7 +304,7 @@ private static byte ApplyChannel(byte channel, double tint) ? linear * (1.0 + tint) // shade: toward black : linear * (1.0 - tint) + tint; // tint: toward white - return (byte)Math.Round(LinearToSrgb(result) * 255.0); + return (byte)Math.Round(LinearToSrgb(result) * 255.0d); } private static double SrgbToLinear(double c) => From 4a7bf9d5e4fdef90222cc08062fd7d010cf18304 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jan=20K=C3=A4llman?= Date: Thu, 20 Aug 2026 15:47:21 +0200 Subject: [PATCH 21/30] Fixed glow filter --- .../Chart/LineChartToSvgTests.cs | 20 +++++++++--------- .../Shape/ShapeToSvgTests.cs | 2 -- .../Svg/Core/SvgShapeRenderer.cs | 2 +- .../BarColumnChartTypeDrawer.cs | 1 + src/EPPlusTest/Drawing/ThemeTest.cs | 21 +++++++++++++++++++ 5 files changed, 33 insertions(+), 13 deletions(-) diff --git a/src/EPPlus.DrawingRenderer.Tests/Chart/LineChartToSvgTests.cs b/src/EPPlus.DrawingRenderer.Tests/Chart/LineChartToSvgTests.cs index 71e957cbae..f8a712e9ab 100644 --- a/src/EPPlus.DrawingRenderer.Tests/Chart/LineChartToSvgTests.cs +++ b/src/EPPlus.DrawingRenderer.Tests/Chart/LineChartToSvgTests.cs @@ -309,17 +309,17 @@ public void GenerateLineChartWithDropLine() { var ws = p.Workbook.Worksheets[1]; - var ix = 1; - var c = ws.Drawings[ix]; - var svg = c.ToSvg(); - SaveTextFileToWorkbook($"svg\\5.3-SampleLines{ix}.svg", svg); + //var ix = 1; + //var c = ws.Drawings[ix]; + //var svg = c.ToSvg(); + //SaveTextFileToWorkbook($"svg\\5.3-SampleLines{ix}.svg", svg); - //for (int i = 0; i < ws.Drawings.Count; i++) - //{ - // var c = ws.Drawings[i]; - // var svg = c.ToSvg(); - // SaveTextFileToWorkbook($"svg\\5.3-SampleLines{i}.svg", svg); - //} + for (int i = 0; i < ws.Drawings.Count; i++) + { + var c = ws.Drawings[i]; + var svg = c.ToSvg(); + SaveTextFileToWorkbook($"svg\\5.3-SampleLines{i}.svg", svg); + } } } //2.4-CreateAFileSystemReport.xlsx diff --git a/src/EPPlus.DrawingRenderer.Tests/Shape/ShapeToSvgTests.cs b/src/EPPlus.DrawingRenderer.Tests/Shape/ShapeToSvgTests.cs index d56766ae27..6ebaad615f 100644 --- a/src/EPPlus.DrawingRenderer.Tests/Shape/ShapeToSvgTests.cs +++ b/src/EPPlus.DrawingRenderer.Tests/Shape/ShapeToSvgTests.cs @@ -506,8 +506,6 @@ public void GenerateSvgForBlipFillShapes() } } } - - [TestMethod] public void GenerateSvgForCircle() { diff --git a/src/EPPlus.DrawingRenderer/Svg/Core/SvgShapeRenderer.cs b/src/EPPlus.DrawingRenderer/Svg/Core/SvgShapeRenderer.cs index d37e885f49..786a22ba98 100644 --- a/src/EPPlus.DrawingRenderer/Svg/Core/SvgShapeRenderer.cs +++ b/src/EPPlus.DrawingRenderer/Svg/Core/SvgShapeRenderer.cs @@ -191,7 +191,7 @@ private void WriteDefsForRenderItem(StringBuilder defSb, HashSet hs, ref } else { - item.FilterName = name; + item.FilterName = $"Url(#{name})"; } } if (item.OuterShadowEffect != null) diff --git a/src/EPPlus/Drawing/Renderer/Chart/ChartTypeDrawers/BarColumnChartTypeDrawer.cs b/src/EPPlus/Drawing/Renderer/Chart/ChartTypeDrawers/BarColumnChartTypeDrawer.cs index 5dbe8f2d27..ddd4124bd8 100644 --- a/src/EPPlus/Drawing/Renderer/Chart/ChartTypeDrawers/BarColumnChartTypeDrawer.cs +++ b/src/EPPlus/Drawing/Renderer/Chart/ChartTypeDrawers/BarColumnChartTypeDrawer.cs @@ -21,6 +21,7 @@ internal class BarColumnChartTypeDrawer : ChartTypeDrawer List> dataPointsPerSerie = new List>(); internal override bool SupportsTrendlines => true; internal override bool SupportsErrorBars => true; + internal override bool SupportsDataTable => true; internal BarColumnChartTypeDrawer(ChartRenderer svgChart, ExcelBarChart chartType) : base(svgChart, chartType) { diff --git a/src/EPPlusTest/Drawing/ThemeTest.cs b/src/EPPlusTest/Drawing/ThemeTest.cs index 19a07a4fd7..58424656ad 100644 --- a/src/EPPlusTest/Drawing/ThemeTest.cs +++ b/src/EPPlusTest/Drawing/ThemeTest.cs @@ -457,12 +457,33 @@ public void colormod() var tint = tc.ColorConverter.ApplyTintDrawing(lmod2, 1 - 0.94); var expected = ColorTranslator.FromHtml("#497592"); + /*#475A67*/ + Assert.AreEqual(expected.ToArgb(), tint.ToArgb()); + + //Assert.AreEqual(Color.FromArgb(255, 145, 14, 127).ToArgb(), myColorDarkened.ToArgb()); + } + [TestMethod] + public void ColorTransformMulti() + { + var accent1 = Color.FromArgb(255, 21, 96, 130); //Accent 1, default theme. + /* + + + + + */ + var lmod1 = tc.ColorConverter.ApplyLumMod(accent1, 0.6); + var smod1 = tc.ColorConverter.ApplySatMod(lmod1, 1.03); + var lmod2 = tc.ColorConverter.ApplyLumMod(smod1, 1.02); + var tint = tc.ColorConverter.ApplyTintDrawing(lmod2, 1-0.94); + var expected = ColorTranslator.FromHtml("#475A67"); Assert.AreEqual(expected.ToArgb(), tint.ToArgb()); //Assert.AreEqual(Color.FromArgb(255, 145, 14, 127).ToArgb(), myColorDarkened.ToArgb()); } + #region Theme Savon [TestMethod] public void ValidateThemeSavonWithBlipFill() From 5a1aa7eaa1c9a0be02197fc7824e0e05ea9cb344 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ossian=20Edstr=C3=B6m?= Date: Fri, 21 Aug 2026 10:42:49 +0200 Subject: [PATCH 22/30] Implemented new fill fallbacks lotta stuff crashes --- .../Chart/ChartStyleFallbackTest.cs | 27 ++++ src/EPPlus/Drawing/Renderer/ChartRenderer.cs | 50 +------ .../DrawingRenderItemExtentions.cs | 137 +++++++++++------- .../Utils/TypeConversion/ColorConverter.cs | 16 ++ 4 files changed, 131 insertions(+), 99 deletions(-) diff --git a/src/EPPlus.DrawingRenderer.Tests/Chart/ChartStyleFallbackTest.cs b/src/EPPlus.DrawingRenderer.Tests/Chart/ChartStyleFallbackTest.cs index dc62a3c150..4dd4b58e92 100644 --- a/src/EPPlus.DrawingRenderer.Tests/Chart/ChartStyleFallbackTest.cs +++ b/src/EPPlus.DrawingRenderer.Tests/Chart/ChartStyleFallbackTest.cs @@ -321,5 +321,32 @@ public void PureExcelTheme() p.SaveAs(fi); } } + + + [TestMethod] + public void ChartWithChartStyle() + { + ExcelPackage.License.SetNonCommercialOrganization("EPPlus Project"); + + string fileName = "ChartWithChartStyleMEdit"; + + CreatePathIfNotExists("StyleExamples\\"); + + using (var p = OpenTemplatePackage($"StyleExamples\\{fileName}.xlsx")) + { + var ws = p.Workbook.Worksheets[0]; + + foreach (var d in ws.Drawings) + { + if (d is ExcelChart c) + { + var svg = c.ToSvg(); + SaveTextFileToWorkbook($"svg\\{fileName}_{ws.Name}_{c.Name}.svg", svg); + } + } + var fi = GetOutputFile("StyleExamples", $"{fileName}_Out.xlsx"); + p.SaveAs(fi); + } + } } } diff --git a/src/EPPlus/Drawing/Renderer/ChartRenderer.cs b/src/EPPlus/Drawing/Renderer/ChartRenderer.cs index 8168fe4621..1a115b9e53 100644 --- a/src/EPPlus/Drawing/Renderer/ChartRenderer.cs +++ b/src/EPPlus/Drawing/Renderer/ChartRenderer.cs @@ -368,53 +368,17 @@ private void SetChartArea(SvgRenderOptions options) //Note that a NoFill node for Charts means Transparent and that no nodes at all become bg1 as shown above - //var themeColor = tc.ColorConverter.GetThemeColor(Theme, Chart.StyleManager.Style?.ChartArea.BorderReference.Color); - //var borderFill = Chart.StyleManager.Style.ChartArea.Border.Fill; - var test = Chart.Border.Fill; - var styleType = Chart.Style; - var myStyleManager = Chart.StyleManager; - //Chart.StyleManager.load - //var chartStyleId = Chart.StyleManager.; - //Chart.StyleManager.SetChartStyle(202); - - Color? themeColor = null; - - //if (Chart.StyleManager == null && styleType != eChartStyle.None) - //{ - // var styleId = (int)styleType; - // if (styleId > (int)eChartStyle.Style48) - // { - // styleId = (int)eChartStyle.Style2; - // } - // //From table2 Default Line Formatting Per Chart Style - // if(styleId <= 40) - // { - // //AKA dk1 - // themeColor = tc.ColorConverter.GetThemeColor(Theme, eThemeSchemeColor.Text1); - // themeColor = tc.ColorConverter.ApplyTint(themeColor.Value, 0.75d); - // var themedLine = Theme.FormatScheme.BorderStyle[0]; - // themedLine.Fill.Color = themeColor.Value; - // } - // else - // { - // //41-48 - // //aka light1 - // themeColor = tc.ColorConverter.GetThemeColor(Theme, eThemeSchemeColor.Background1); - // } - //} var reference = Chart.StyleManager.Style?.ChartArea.BorderReference; - item.Rectangle.ResolveStyleFallbackChainBorder( - Chart, + item.Rectangle.SetDrawingBorderPropertiesNew( Theme, - reference, + reference?.Color, Chart.Border, 1d, () => GetChartAreaDefaultColor((int)styleType, out ExcelThemeLine themedLine)); - //item.Rectangle.SetDrawingPropertiesBorder(Theme, Chart.Border, Chart.StyleManager.Style?.ChartArea.BorderReference.Color, Chart.Border.IsEmpty || Chart.Border.Width > 0, item.DefaultBorderColor, 0.75, UserSpaceSettings.UserSpaceOnUse_Global, Chart.Style); item.Rectangle.RoundedCornerRadius = Chart.RoundedCorners ? 9 : 0; item.AppendRenderItems(RenderItems); item.SetMargins(Chart.TextBody); @@ -456,14 +420,7 @@ private void SetChartArea(SvgRenderOptions options) { //themeColor = tc.ColorConverter.ApplyTintDrawing(themeColor.Value, 0.15d); //but even in this case if there is no ln node found in style it appears to default to 75% despite a scheme color existing in the theme - var convertedTheme = tc.ColorConverter.ApplyTransforms(themeColor.Value, themedLine.Fill.SolidFill.Color.Transforms); - var drawingTint = tc.ColorConverter.ApplyTintDrawing(themeColor.Value, 0.6d); - //themeColor = convertedTheme; - //Arguably we should apply all transforms instead - //themeColor = tc.ColorConverter.ApplyTintDrawing(themeColor.Value, 0.25d); - ////themeColor = tc.ColorConverter.ApplyTintDrawing(themeColor.Value, 0.285d); - ////Arguably we should apply all transforms instead but even in this case if there is no ln node found in style it appears to default to 75% despite a scheme color existing in the theme - ////themeColor = tc.ColorConverter.ApplyTransforms(themeColor.Value, themedLine.Fill.SolidFill.Color.Transforms); + themeColor = tc.ColorConverter.ApplyTransforms(themeColor.Value, themedLine.Fill.SolidFill.Color.Transforms); } else { @@ -474,7 +431,6 @@ private void SetChartArea(SvgRenderOptions options) themeColor = newTheme; } - //themedLine.Fill.SolidFill.Color.Transforms.AddTint } else { diff --git a/src/EPPlus/Drawing/Renderer/RenderItems/DrawingRenderItemExtentions.cs b/src/EPPlus/Drawing/Renderer/RenderItems/DrawingRenderItemExtentions.cs index 1574ca562a..8fc32d625a 100644 --- a/src/EPPlus/Drawing/Renderer/RenderItems/DrawingRenderItemExtentions.cs +++ b/src/EPPlus/Drawing/Renderer/RenderItems/DrawingRenderItemExtentions.cs @@ -23,6 +23,7 @@ Date Author Change using OfficeOpenXml.Drawing.Theme; using OfficeOpenXml.Style; using System; +using System.ComponentModel.DataAnnotations.Schema; using System.Drawing; using System.Runtime.InteropServices; using System.Security.Cryptography.Xml; @@ -55,32 +56,53 @@ internal static void SetDrawingPropertiesFill(this RenderItem item, ExcelTheme t } internal static void SetDrawingPropertiesFillBasic(this RenderItem item, ExcelTheme theme, ExcelDrawingFillBasic fill, ExcelDrawingColorManager color, UserSpaceSettings gradientUserSpaceOnUse, Color? nullColor) { - double? opacity = null; - switch (fill.Style) + double opacity = double.NaN; + + var fillNew = GetFillNew(fill, theme, color, item.FillColorSource, out opacity, () => { return nullColor; }, out DrawingRenderGradientFill gradFill); + + if(gradFill != null) { - case eFillStyle.NoFill: - if (fill.IsEmpty) //Do NOT remove. This if is required for Shapes - { - item.FillColor = GetFillColor(theme, fill, color, item.FillColorSource, out opacity, nullColor); - } - else - { - item.FillColor = "none"; - } - break; - case eFillStyle.SolidFill: - item.FillColor = GetFillColor(theme, fill, color, item.FillColorSource, out opacity); - break; - case eFillStyle.GradientFill: - item.GradientFill = new DrawingRenderGradientFill(theme, fill.GradientFill, gradientUserSpaceOnUse); - item.FillType = FillType.GradientFill; - item.FillColor = null; - break; + //Special case for gradFIll as it does not return string + item.GradientFill = gradFill; + item.FillType = FillType.GradientFill; + item.FillColor = null; } - if (opacity.HasValue) + else + { + item.FillColor = fillNew; + } + + if (opacity != double.NaN) { item.FillOpacity = opacity; } + + //switch (fill.Style) + //{ + // case eFillStyle.NoFill: + // item.FillColor = GetFillNew(fill) + // //if (fill.IsEmpty) //Do NOT remove. This if is required for Shapes + // //{ + // // item.FillColor = GetFillColor(theme, fill, color, item.FillColorSource, out opacity, nullColor); + // //} + // //else + // //{ + // // item.FillColor = "none"; + // //} + // break; + // case eFillStyle.SolidFill: + // item.FillColor = GetFillColor(theme, fill, color, item.FillColorSource, out opacity); + // break; + // case eFillStyle.GradientFill: + // item.GradientFill = new DrawingRenderGradientFill(theme, fill.GradientFill, gradientUserSpaceOnUse); + // item.FillType = FillType.GradientFill; + // item.FillColor = null; + // break; + //} + //if (opacity.HasValue) + //{ + // item.FillOpacity = opacity; + //} } //bg1 is the hard-coded default of solid fill according to ooxml docs (MS-OE376) @@ -102,17 +124,20 @@ private static Color GetSchemeColor(ExcelTheme theme, eSchemeColor schemeColor = return fc; } - private static Color? GetFillColorFromReference(ExcelChartStyleReference reference, ExcelTheme theme, ExcelDrawingFillBasic fill) + private static Color? GetFillColorFromReference(ExcelDrawingColorManager styleFillColor, ExcelTheme theme, ExcelDrawingFillBasic fill) { - if(reference != null && reference.HasColor) + if(styleFillColor != null) { - var styleFillColor = reference.Color; Color? fc; + //fc = tc.ColorConverter.GetThemeColor(theme, fill.SolidFill?.Color, styleFillColor); + if (styleFillColor.ColorType == eDrawingColorType.Scheme) { var bg1 = theme.ColorScheme.GetColorByEnum(styleFillColor.SchemeColor.Color); fc = bg1.GetColor(); + var differentResultMB = tc.ColorConverter.GetThemeColor(theme, fill.SolidFill?.Color, styleFillColor); + //fc = tc.ColorConverter.GetThemeColor(theme, fill.SolidFill?.Color, styleFillColor); } else { @@ -129,16 +154,17 @@ private static Color GetSchemeColor(ExcelTheme theme, eSchemeColor schemeColor = return null; } - private static string GetFillColorNew(ExcelTheme theme, ExcelDrawingBorder border, ExcelChartStyleReference reference, PathFillMode colorSource, out double opacity, Func GetDefaultThemeColor) + private static string GetFallbackFill(ExcelTheme theme, ExcelDrawingFillBasic itemFill, ExcelDrawingColorManager reference, PathFillMode colorSource, out double opacity, Func GetDefaultThemeColor) { Color? fc = null; + //We already know the fill has "NoFill" //NoFill has two cases. Either the node does not exist. Or it has been set to NoFill specifically - if (border.Fill.IsEmpty) + if (itemFill.IsEmpty) { //The node itself does not exist. It needs to check for potential fallbacks //Move on to 2. StyleManager - fc = GetFillColorFromReference(reference, theme, border.Fill); + fc = GetFillColorFromReference(reference, theme, itemFill); if (fc.HasValue == false) { @@ -148,12 +174,6 @@ private static string GetFillColorNew(ExcelTheme theme, ExcelDrawingBorder borde } } - else if (border.Fill.Style == eFillStyle.SolidFill) - { - //1. Standard case. There is a fill color to apply. - //Send in styleFill as well since a solid fill can refer to style color - fc = tc.ColorConverter.GetThemeColor(theme, border.Fill.SolidFill.Color, reference.Color); - } else { opacity = 0d; @@ -184,43 +204,54 @@ private static string GetAdjustmentsAndTransparency(Color fc, PathFillMode color return "#" + fc.ToArgb().ToString("x8").Substring(2); } - internal static void ResolveStyleFallbackChainBorder(this RenderItem item, ExcelChart chart, ExcelTheme theme, ExcelChartStyleReference reference, ExcelDrawingBorder border, double opacity, Func GetDefaultThemeColor) + internal static string GetFillNew(ExcelDrawingFillBasic fill, ExcelTheme theme, ExcelDrawingColorManager reference, PathFillMode fillMode, out double opacity, Func GetHardCodedDefaultForItem, out DrawingRenderGradientFill gradFill) { + string fillStr = string.Empty; + gradFill = null; + opacity = 1d; + //The Fallback chain of styles for drawing objects is: //1. Chart.Border (make sure to note the chart style ID //2. Chart.StyleManager.ChartArea.BorderReference //3. Theme.FormatScheme.BorderStyle[0] for subtle, [1] Moderate [2] Intense //4. If none of these contain even an empty node for the relevant property, Fallback to hardcoded documentation defaults - Color? fc = null; - switch (border.Fill.Style) + switch (fill.Style) { case eFillStyle.NoFill: - if (border.Fill.IsEmpty) - { - //Fallback to style hierarhy (options 2, 3 or 4) - item.BorderColor = GetFillColorNew(theme, border, reference, item.BorderColorSource, out opacity, GetDefaultThemeColor); - //item.BorderColorSource = PathFillMode.Lighten; - } - else - { - //The node has specifically been set to NoFill AKA Transparent - item.BorderColor = "none"; - } + //Either transparent or Fallback to style hierarhy (options 2, 3 or 4) + fillStr = GetFallbackFill(theme, fill, reference, fillMode, out opacity, GetHardCodedDefaultForItem); break; case eFillStyle.SolidFill: //1. Standard case. There is a fill color to apply. //Send in styleFill as well since a solid fill can refer to style color - fc = tc.ColorConverter.GetThemeColor(theme, border.Fill.SolidFill.Color, reference?.Color); - item.BorderColor = GetAdjustmentsAndTransparency(fc.Value, item.BorderColorSource, out opacity); - item.BorderGradientFill = null; + var fc = tc.ColorConverter.GetThemeColor(theme, fill.SolidFill.Color, reference); + fillStr = GetAdjustmentsAndTransparency(fc, fillMode, out opacity); break; case eFillStyle.GradientFill: - item.BorderGradientFill = new DrawingRenderGradientFill(theme, border.Fill.GradientFill, UserSpaceSettings.UserSpaceOnUse_Global); - item.BorderColor = null; + gradFill = new DrawingRenderGradientFill(theme, fill.GradientFill, UserSpaceSettings.UserSpaceOnUse_Global); break; } + return fillStr; + } + + internal static void SetDrawingBorderPropertiesNew(this RenderItem item, ExcelTheme theme, ExcelChartStyleColorManager reference, ExcelDrawingBorder border, double opacity, Func GetHardCodedDefaultForItem) + { + var fillColorStr = GetFillNew(border.Fill, theme, reference, item.BorderColorSource, out opacity, GetHardCodedDefaultForItem, out DrawingRenderGradientFill gradFill); + + if(gradFill != null) + { + //Special case as gradfill does not return a string + item.BorderGradientFill = new DrawingRenderGradientFill(theme, border.Fill.GradientFill, UserSpaceSettings.UserSpaceOnUse_Global); + item.BorderColor = null; + } + else + { + item.BorderColor = fillColorStr; + item.BorderGradientFill = null; + } + item.BorderOpacity = opacity; if (item.BorderColorSource != PathFillMode.None) @@ -438,6 +469,8 @@ private static string GetFillColor(ExcelTheme theme, ExcelDrawingFillBasic fill, } else if (fill.Style == eFillStyle.SolidFill) { + fc = fill.Color; + tc.ColorConverter.GetThemeColor(theme, fill.SolidFill.Color); //Send in styleFill as well since a solid fill can refer to style color fc = tc.ColorConverter.GetThemeColor(theme, fill.SolidFill.Color, styleFillColor); } diff --git a/src/EPPlus/Utils/TypeConversion/ColorConverter.cs b/src/EPPlus/Utils/TypeConversion/ColorConverter.cs index 21c6eb07b0..3c2638d3ad 100644 --- a/src/EPPlus/Utils/TypeConversion/ColorConverter.cs +++ b/src/EPPlus/Utils/TypeConversion/ColorConverter.cs @@ -58,6 +58,22 @@ public static Color GetThemeColor(ExcelTheme theme, ExcelDrawingColorManager cm, var nc = GetThemeColor(newCm); return ApplyTransforms(nc, cm.Transforms); } + //else if(cm == null) + //{ + // ExcelDrawingThemeColorManager newCm; + + // if (cmStyle.ColorType == eDrawingColorType.Scheme) + // { + // return GetThemeColor(theme, cmStyle); + // } + // else + // { + // newCm = theme.ColorScheme.GetColorByEnum(cm.SchemeColor.Color); + // } + // var nc = GetThemeColor(newCm); + // return ApplyTransforms(nc, cm.Transforms); + //} + var c = GetThemeColor(cm); return ApplyTransforms(c, cm.Transforms); From 07a141553b226562c927941152cdfe578ab69b70 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ossian=20Edstr=C3=B6m?= Date: Fri, 21 Aug 2026 10:54:21 +0200 Subject: [PATCH 23/30] Fixed null issue --- .../DrawingRenderItemExtentions.cs | 42 +++++++++---------- .../Utils/TypeConversion/ColorConverter.cs | 28 ++++++------- 2 files changed, 34 insertions(+), 36 deletions(-) diff --git a/src/EPPlus/Drawing/Renderer/RenderItems/DrawingRenderItemExtentions.cs b/src/EPPlus/Drawing/Renderer/RenderItems/DrawingRenderItemExtentions.cs index 8fc32d625a..520b51d5f1 100644 --- a/src/EPPlus/Drawing/Renderer/RenderItems/DrawingRenderItemExtentions.cs +++ b/src/EPPlus/Drawing/Renderer/RenderItems/DrawingRenderItemExtentions.cs @@ -128,28 +128,26 @@ private static Color GetSchemeColor(ExcelTheme theme, eSchemeColor schemeColor = { if(styleFillColor != null) { - Color? fc; - - //fc = tc.ColorConverter.GetThemeColor(theme, fill.SolidFill?.Color, styleFillColor); - - if (styleFillColor.ColorType == eDrawingColorType.Scheme) - { - var bg1 = theme.ColorScheme.GetColorByEnum(styleFillColor.SchemeColor.Color); - fc = bg1.GetColor(); - var differentResultMB = tc.ColorConverter.GetThemeColor(theme, fill.SolidFill?.Color, styleFillColor); - //fc = tc.ColorConverter.GetThemeColor(theme, fill.SolidFill?.Color, styleFillColor); - } - else - { - if (fill != null && fill.Style != eFillStyle.NoFill) - { - fc = tc.ColorConverter.GetThemeColor(theme, fill.SolidFill?.Color, styleFillColor); - } - else - { - return Color.Empty; - } - } + Color? fc = tc.ColorConverter.GetThemeColor(theme, fill.SolidFill?.Color, styleFillColor); + + //if (styleFillColor.ColorType == eDrawingColorType.Scheme) + //{ + // var bg1 = theme.ColorScheme.GetColorByEnum(styleFillColor.SchemeColor.Color); + // fc = bg1.GetColor(); + // var differentResultMB = tc.ColorConverter.GetThemeColor(theme, fill.SolidFill?.Color, styleFillColor); + // //fc = tc.ColorConverter.GetThemeColor(theme, fill.SolidFill?.Color, styleFillColor); + //} + //else + //{ + // if (fill != null && fill.Style != eFillStyle.NoFill) + // { + // fc = tc.ColorConverter.GetThemeColor(theme, fill.SolidFill?.Color, styleFillColor); + // } + // else + // { + // return Color.Empty; + // } + //} } return null; } diff --git a/src/EPPlus/Utils/TypeConversion/ColorConverter.cs b/src/EPPlus/Utils/TypeConversion/ColorConverter.cs index 3c2638d3ad..8e54fea97b 100644 --- a/src/EPPlus/Utils/TypeConversion/ColorConverter.cs +++ b/src/EPPlus/Utils/TypeConversion/ColorConverter.cs @@ -58,21 +58,21 @@ public static Color GetThemeColor(ExcelTheme theme, ExcelDrawingColorManager cm, var nc = GetThemeColor(newCm); return ApplyTransforms(nc, cm.Transforms); } - //else if(cm == null) - //{ - // ExcelDrawingThemeColorManager newCm; + else if(cm == null) + { + ExcelDrawingThemeColorManager newCm; - // if (cmStyle.ColorType == eDrawingColorType.Scheme) - // { - // return GetThemeColor(theme, cmStyle); - // } - // else - // { - // newCm = theme.ColorScheme.GetColorByEnum(cm.SchemeColor.Color); - // } - // var nc = GetThemeColor(newCm); - // return ApplyTransforms(nc, cm.Transforms); - //} + if (cmStyle.ColorType == eDrawingColorType.Scheme) + { + return GetThemeColor(theme, cmStyle); + } + else + { + newCm = theme.ColorScheme.GetColorByEnum(cm.SchemeColor.Color); + } + var nc = GetThemeColor(newCm); + return ApplyTransforms(nc, cm.Transforms); + } var c = GetThemeColor(cm); return ApplyTransforms(c, cm.Transforms); From a13d2ea299c97afb759778227bf66276ac7ad208 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jan=20K=C3=A4llman?= Date: Fri, 21 Aug 2026 11:27:57 +0200 Subject: [PATCH 24/30] Fixes date axis label positioning --- .../Chart/LineChartToSvgTests.cs | 22 ++++---- .../Drawing/Chart/ChartEx/ExcelChartExAxis.cs | 2 +- src/EPPlus/Drawing/Chart/ExcelChartAxis.cs | 2 +- .../Drawing/Chart/ExcelChartAxisStandard.cs | 10 +++- .../Renderer/Chart/ChartAxisRenderer.cs | 50 +++++++++++++------ .../Renderer/Chart/ChartDataTableRenderer.cs | 35 +++++++++++++ .../ChartTypeDrawers/LineChartTypeDrawer.cs | 1 + src/EPPlus/Drawing/Renderer/ChartRenderer.cs | 12 ++++- 8 files changed, 104 insertions(+), 30 deletions(-) create mode 100644 src/EPPlus/Drawing/Renderer/Chart/ChartDataTableRenderer.cs diff --git a/src/EPPlus.DrawingRenderer.Tests/Chart/LineChartToSvgTests.cs b/src/EPPlus.DrawingRenderer.Tests/Chart/LineChartToSvgTests.cs index f8a712e9ab..6d55224e92 100644 --- a/src/EPPlus.DrawingRenderer.Tests/Chart/LineChartToSvgTests.cs +++ b/src/EPPlus.DrawingRenderer.Tests/Chart/LineChartToSvgTests.cs @@ -20,7 +20,7 @@ public void GenerateSvgForLineCharts_sheet1() var ws = p.Workbook.Worksheets[0]; //var ix = 4; - //var c = ws.Drawings[ix]; + //var c = ws.Drawings[ix]; //var svg = c.ToSvg(); //SaveTextFileToWorkbook($"svg\\ChartForSvg_ind{ix++}.svg", svg); @@ -119,16 +119,16 @@ public void GenerateSvgForLineChartSecondaryAxis() using (var p = OpenTemplatePackage("ChartForSvg_SecondaryAxis.xlsx")) { var ws = p.Workbook.Worksheets[0]; - var ix = 1; - var c = ws.Drawings[ix]; - var svg = c.ToSvg(); - SaveTextFileToWorkbook($"svg\\ChartForSvg_sheet2_{ix++}.svg", svg); - //var ix = 0; - //foreach (ExcelChart c in ws.Drawings) - //{ - // var svg = c.ToSvg(); - // SaveTextFileToWorkbook($"svg\\ChartForSvg_SecAxis{ix++}.svg", svg); - //} + //var ix = 1; + //var c = ws.Drawings[ix]; + //var svg = c.ToSvg(); + //SaveTextFileToWorkbook($"svg\\ChartForSvg_sheet2_{ix++}.svg", svg); + var ix = 0; + foreach (ExcelChart c in ws.Drawings) + { + var svg = c.ToSvg(); + SaveTextFileToWorkbook($"svg\\ChartForSvg_SecAxis{ix++}.svg", svg); + } } } [TestMethod] diff --git a/src/EPPlus/Drawing/Chart/ChartEx/ExcelChartExAxis.cs b/src/EPPlus/Drawing/Chart/ChartEx/ExcelChartExAxis.cs index 3d68716255..8b87afea83 100644 --- a/src/EPPlus/Drawing/Chart/ChartEx/ExcelChartExAxis.cs +++ b/src/EPPlus/Drawing/Chart/ChartEx/ExcelChartExAxis.cs @@ -190,7 +190,7 @@ internal override ExcelChartTitle GetTitle() return _title; } - internal override List GetAxisValues(out bool isCount, out bool isNumeric) + internal override List GetAxisValues(out bool isCount, out bool isNumeric, out bool isDate) { throw new NotImplementedException(); } diff --git a/src/EPPlus/Drawing/Chart/ExcelChartAxis.cs b/src/EPPlus/Drawing/Chart/ExcelChartAxis.cs index 2526d62e38..e5fec2f806 100644 --- a/src/EPPlus/Drawing/Chart/ExcelChartAxis.cs +++ b/src/EPPlus/Drawing/Chart/ExcelChartAxis.cs @@ -634,6 +634,6 @@ void IStyleMandatoryProperties.SetMandatoryProperties() CreatespPrNode($"{_nsPrefix}:spPr"); } - internal abstract List GetAxisValues(out bool isCount, out bool isNumeric); + internal abstract List GetAxisValues(out bool isCount, out bool isNumeric, out bool isDate); } } diff --git a/src/EPPlus/Drawing/Chart/ExcelChartAxisStandard.cs b/src/EPPlus/Drawing/Chart/ExcelChartAxisStandard.cs index bf27a58a09..67345c5f20 100644 --- a/src/EPPlus/Drawing/Chart/ExcelChartAxisStandard.cs +++ b/src/EPPlus/Drawing/Chart/ExcelChartAxisStandard.cs @@ -901,12 +901,20 @@ internal bool IsXAxis return false; } } - internal override List GetAxisValues(out bool isCount, out bool isNumeric) + internal override List GetAxisValues(out bool isCount, out bool isNumeric, out bool isDate) { List> values; GetSeriesValues(out isCount, out values); var dl = values.SelectMany(x => x).Distinct().ToList(); isNumeric = dl.Any(x => x == null || x.IsNumeric() || (x is object[] a && a[3].IsNumeric())); + isDate = IsDate; + + if (isDate == false) + { + var fv = dl.FirstOrDefault(x => x != null && !(x is object[] a && a[3] == null)); + isDate = (fv is DateTime) || (fv is object[] a && a[3] is DateTime); + } + if (isNumeric) { if (dl[0] is object[]) diff --git a/src/EPPlus/Drawing/Renderer/Chart/ChartAxisRenderer.cs b/src/EPPlus/Drawing/Renderer/Chart/ChartAxisRenderer.cs index 93fb3c388c..9bd578f978 100644 --- a/src/EPPlus/Drawing/Renderer/Chart/ChartAxisRenderer.cs +++ b/src/EPPlus/Drawing/Renderer/Chart/ChartAxisRenderer.cs @@ -306,6 +306,10 @@ internal void AddTickmarksAndValues(List DefItems) { MinorTickMarkPositions = AddTickmarks(MinorUnit, MajorDateUnit, MajorUnit, 2D.PixelToPoint(), Axis.MinorTickMark); } + else + { + MinorTickMarkPositions = null; + } if(Axis.HasMajorGridlines) { @@ -348,7 +352,7 @@ private List GetAxisValueTextBoxes() case eTextOrientation.Vertical: maxWidth = ChartRenderer.ChartArea.Rectangle.Height / 3; maxHeight = Rectangle.Width / AxisValues.Count; //TODO: Check this value. - break; + break; case eTextOrientation.Diagonal: maxWidth = (Rectangle.Width + Rectangle.Height) / COS45; maxHeight = ChartRenderer.ChartArea.Rectangle.Height / 3; //TODO: Check this value. @@ -362,8 +366,9 @@ 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 v = Values[i]; + var t = AxisValues[i]; + var m = tm.MeasureText(t, mf); var ticMarkX = GetAxisItemLeft(i, m); var ticMarkY = GetAxisItemTop(i, m); var width = m.Width; @@ -457,7 +462,7 @@ private List GetAxisValueTextBoxes() p.HorizontalAlignment = eTextAlignment.Center; } - tb.ImportParagraph(p, 0, v); + tb.ImportParagraph(p, 0, t); //tb.TextBody.Paragraphs[0].AddText(v, Axis.Font); tb.Rectangle.SetDrawingPropertiesFill(ChartRenderer.Theme, Axis.Fill, axisStyle?.FillReference.Color, UserSpaceSettings.UserSpaceOnUse_Global, DefaultFillColor); @@ -491,7 +496,18 @@ private List GetAxisValueTextBoxes() { //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; + double majorWidth; + if (IsDateAutoAxis || IsDateScale) + { + var min = ConvertUtil.GetValueDouble(Values[0]); + var max = ConvertUtil.GetValueDouble(Values.Last()); + var minUnit = (max - min) / MinorUnit; + majorWidth = (min - min) / minUnit; + } + else + { + majorWidth = Rectangle.Width / AxisValues.Count; + } if (Axis.CrossingAxis == null || Axis.CrossingAxis.CrossBetween == eCrossBetween.MidCat) { foreach (var tb in ret) @@ -556,7 +572,7 @@ private double GetAxisItemLeft(int i, OfficeOpenXml.Interfaces.Drawing.Text.Text } else { - if (IsCatAx()) + if (IsCatAx() && IsDateAutoAxis==false) //A text axis { double majorWidth; if (Axis.CrossingAxis == null || Axis.CrossingAxis.CrossBetween == eCrossBetween.Between) @@ -575,7 +591,16 @@ private double GetAxisItemLeft(int i, OfficeOpenXml.Interfaces.Drawing.Text.Text 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); + double majorWidth; + if (IsDateAutoAxis || IsDateScale) + { + majorWidth = Rectangle.Width * (v - Min) / (Max - Min); + } + else + { + majorWidth = Rectangle.Width * (v - Min) / (Max - Min); + } + return Rectangle.Left + majorWidth; } //} @@ -660,7 +685,7 @@ private List AddTickmarks(double units, eTimeUnit? dateUnit, dou addMinor = parentUnit / 2; } - if (Axis.AxisType == eAxisType.Cat) + if (Axis.AxisType == eAxisType.Cat && IsDateAutoAxis==false) { min = 0; if (Axis.CrossingAxis==null || Axis.CrossingAxis.CrossBetween == eCrossBetween.Between) @@ -960,7 +985,7 @@ internal double GetPositionInPlotarea(double val, bool startValue=false) } 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, out bool isNumeric); + var values = ax.GetAxisValues(out bool isCount, out bool isNumeric, out bool isDate); //if(isCount == false && isNumeric && ax.AxisType == eAxisType.Cat) //{ // IsDateAutoAxis = true; @@ -977,7 +1002,7 @@ protected List GetAxisValue(ExcelChartAxisStandard ax, RenderItem rect, ChartSize = rect }; - if (AutoAxisType == eAxisType.Cat && isCount == false) + if (AutoAxisType == eAxisType.Cat && isCount == false && isDate == false) { AxisScale res; if (ax.IsVertical) @@ -1010,7 +1035,6 @@ protected List GetAxisValue(ExcelChartAxisStandard ax, RenderItem rect, var l = new List(); min = double.MaxValue; max = double.MinValue; - var isDate = values.Count > 0; //If any values set to true so we can check for non-date values. foreach (var v in values) { double d; @@ -1023,10 +1047,6 @@ protected List GetAxisValue(ExcelChartAxisStandard ax, RenderItem rect, { ov = v; } - if(!(ov is DateTime)) - { - isDate = false; - } d = ConvertUtil.GetValueDouble(ov, false, true); if (double.IsNaN(d)) { diff --git a/src/EPPlus/Drawing/Renderer/Chart/ChartDataTableRenderer.cs b/src/EPPlus/Drawing/Renderer/Chart/ChartDataTableRenderer.cs new file mode 100644 index 0000000000..068447bdbe --- /dev/null +++ b/src/EPPlus/Drawing/Renderer/Chart/ChartDataTableRenderer.cs @@ -0,0 +1,35 @@ +/************************************************************************************************* + 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 + ************************************************************************************************* + 20/08/2026 EPPlus Software AB EPPlus 9 + *************************************************************************************************/ + +using EPPlus.DrawingRenderer.RenderItems; +using EPPlusImageRenderer; +using EPPlusImageRenderer.Svg; +using System; +using System.Collections.Generic; + +namespace OfficeOpenXml.Drawing.Renderer.Chart +{ + internal class ChartDataTableRenderer : ChartDrawingObject + { + internal ChartDataTableRenderer(ChartRenderer svgChart) : base(svgChart) + { + var chartDataTable = svgChart.Chart.PlotArea.DataTable; + + } + public override void AppendRenderItems(List renderItems) + { + base.AppendRenderItems(renderItems); + } + + } +} \ No newline at end of file diff --git a/src/EPPlus/Drawing/Renderer/Chart/ChartTypeDrawers/LineChartTypeDrawer.cs b/src/EPPlus/Drawing/Renderer/Chart/ChartTypeDrawers/LineChartTypeDrawer.cs index f35dfe106d..5ba1d1bcb3 100644 --- a/src/EPPlus/Drawing/Renderer/Chart/ChartTypeDrawers/LineChartTypeDrawer.cs +++ b/src/EPPlus/Drawing/Renderer/Chart/ChartTypeDrawers/LineChartTypeDrawer.cs @@ -26,6 +26,7 @@ internal class LineChartTypeDrawer : ChartTypeDrawer List> dataPointsPerSerie = new List>(); internal override bool SupportsTrendlines => true; internal override bool SupportsErrorBars => true; + internal override bool SupportsDataTable => true; internal LineChartTypeDrawer(ChartRenderer svgChart, ExcelLineChart chartType) : base(svgChart, chartType) { var isStacked = chartType.IsTypeStacked(); diff --git a/src/EPPlus/Drawing/Renderer/ChartRenderer.cs b/src/EPPlus/Drawing/Renderer/ChartRenderer.cs index 8168fe4621..4d1acf9870 100644 --- a/src/EPPlus/Drawing/Renderer/ChartRenderer.cs +++ b/src/EPPlus/Drawing/Renderer/ChartRenderer.cs @@ -21,12 +21,14 @@ Date Author Change using EPPlusImageRenderer.Svg; using OfficeOpenXml.Drawing; using OfficeOpenXml.Drawing.Chart; +using OfficeOpenXml.Drawing.Renderer.Chart; using OfficeOpenXml.Drawing.Theme; using OfficeOpenXml.FormulaParsing.Excel.Functions.MathFunctions; using OfficeOpenXml.FormulaParsing.Excel.Functions.Text; using OfficeOpenXml.Style; using System; using System.Collections.Generic; +using System.Data; using System.Drawing; using System.Runtime.InteropServices; using System.Security.Cryptography.Xml; @@ -77,8 +79,13 @@ public ChartRenderer(ExcelChart chart, SvgRenderOptions options) : base(chart) SecondHorizontalAxis = GetAxis(false, 2); } - Plotarea.SetPlotAreaRectangle(); + if (HasDataTable) + { + DataTable = new ChartDataTableRenderer(this); + } + Plotarea.SetPlotAreaRectangle(); + //As we need the plotarea dimensions to calculate the axis positions we need to set the axis positions after creating the plotarea. SetAxisPositionsFromPlotarea(); @@ -520,6 +527,9 @@ public ExcelChart Chart internal ChartAxisRenderer SecondHorizontalAxis { get; set; } internal List DefItems { get; } = new List(); + public bool HasDataTable { get => Chart.PlotArea.DataTable != null; } + public ChartDataTableRenderer DataTable { get; private set; } + internal void AddDefs(RenderItem item) { DefItems.Add(item); From 97973691ccfb0966dc08ea2bf3bbbf73c3b81119 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ossian=20Edstr=C3=B6m?= Date: Fri, 21 Aug 2026 13:36:46 +0200 Subject: [PATCH 25/30] Re-fixed smiley --- src/EPPlus/Drawing/Renderer/ChartRenderer.cs | 1 + .../DrawingRenderItemExtentions.cs | 175 +++++++++--------- .../Utils/TypeConversion/ColorConverter.cs | 20 +- 3 files changed, 104 insertions(+), 92 deletions(-) diff --git a/src/EPPlus/Drawing/Renderer/ChartRenderer.cs b/src/EPPlus/Drawing/Renderer/ChartRenderer.cs index 1a115b9e53..ba100a59a0 100644 --- a/src/EPPlus/Drawing/Renderer/ChartRenderer.cs +++ b/src/EPPlus/Drawing/Renderer/ChartRenderer.cs @@ -377,6 +377,7 @@ private void SetChartArea(SvgRenderOptions options) reference?.Color, Chart.Border, 1d, + Chart.Border.Fill.Style != eFillStyle.NoFill, () => GetChartAreaDefaultColor((int)styleType, out ExcelThemeLine themedLine)); item.Rectangle.RoundedCornerRadius = Chart.RoundedCorners ? 9 : 0; diff --git a/src/EPPlus/Drawing/Renderer/RenderItems/DrawingRenderItemExtentions.cs b/src/EPPlus/Drawing/Renderer/RenderItems/DrawingRenderItemExtentions.cs index 520b51d5f1..82a6098d2f 100644 --- a/src/EPPlus/Drawing/Renderer/RenderItems/DrawingRenderItemExtentions.cs +++ b/src/EPPlus/Drawing/Renderer/RenderItems/DrawingRenderItemExtentions.cs @@ -57,7 +57,9 @@ internal static void SetDrawingPropertiesFill(this RenderItem item, ExcelTheme t internal static void SetDrawingPropertiesFillBasic(this RenderItem item, ExcelTheme theme, ExcelDrawingFillBasic fill, ExcelDrawingColorManager color, UserSpaceSettings gradientUserSpaceOnUse, Color? nullColor) { double opacity = double.NaN; + double? opacityOld = double.NaN; + var oldFill = GetFillColor(theme, fill, color, item.FillColorSource, out opacityOld, nullColor); var fillNew = GetFillNew(fill, theme, color, item.FillColorSource, out opacity, () => { return nullColor; }, out DrawingRenderGradientFill gradFill); if(gradFill != null) @@ -118,6 +120,7 @@ private static Color GetSchemeColor(ExcelTheme theme, eSchemeColor schemeColor = if (fc.HasValue == false) { + //Hardcoded default. //Bg1 or alternatively accent 1 fc = theme.FormatScheme.BackgroundFillStyle[0].Color; } @@ -129,25 +132,7 @@ private static Color GetSchemeColor(ExcelTheme theme, eSchemeColor schemeColor = if(styleFillColor != null) { Color? fc = tc.ColorConverter.GetThemeColor(theme, fill.SolidFill?.Color, styleFillColor); - - //if (styleFillColor.ColorType == eDrawingColorType.Scheme) - //{ - // var bg1 = theme.ColorScheme.GetColorByEnum(styleFillColor.SchemeColor.Color); - // fc = bg1.GetColor(); - // var differentResultMB = tc.ColorConverter.GetThemeColor(theme, fill.SolidFill?.Color, styleFillColor); - // //fc = tc.ColorConverter.GetThemeColor(theme, fill.SolidFill?.Color, styleFillColor); - //} - //else - //{ - // if (fill != null && fill.Style != eFillStyle.NoFill) - // { - // fc = tc.ColorConverter.GetThemeColor(theme, fill.SolidFill?.Color, styleFillColor); - // } - // else - // { - // return Color.Empty; - // } - //} + return fc; } return null; } @@ -158,7 +143,7 @@ private static string GetFallbackFill(ExcelTheme theme, ExcelDrawingFillBasic it //We already know the fill has "NoFill" //NoFill has two cases. Either the node does not exist. Or it has been set to NoFill specifically - if (itemFill.IsEmpty) + if (itemFill == null || itemFill.IsEmpty) { //The node itself does not exist. It needs to check for potential fallbacks //Move on to 2. StyleManager @@ -214,29 +199,48 @@ internal static string GetFillNew(ExcelDrawingFillBasic fill, ExcelTheme theme, //3. Theme.FormatScheme.BorderStyle[0] for subtle, [1] Moderate [2] Intense //4. If none of these contain even an empty node for the relevant property, Fallback to hardcoded documentation defaults - switch (fill.Style) + if(fill == null) { - case eFillStyle.NoFill: - //Either transparent or Fallback to style hierarhy (options 2, 3 or 4) - fillStr = GetFallbackFill(theme, fill, reference, fillMode, out opacity, GetHardCodedDefaultForItem); - break; - case eFillStyle.SolidFill: - //1. Standard case. There is a fill color to apply. - //Send in styleFill as well since a solid fill can refer to style color - var fc = tc.ColorConverter.GetThemeColor(theme, fill.SolidFill.Color, reference); - fillStr = GetAdjustmentsAndTransparency(fc, fillMode, out opacity); - break; - case eFillStyle.GradientFill: - gradFill = new DrawingRenderGradientFill(theme, fill.GradientFill, UserSpaceSettings.UserSpaceOnUse_Global); - break; + fillStr = GetFallbackFill(theme, fill, reference, fillMode, out opacity, GetHardCodedDefaultForItem); } + else + { + switch (fill.Style) + { + case eFillStyle.NoFill: + //Either transparent or Fallback to style hierarhy (options 2, 3 or 4) + fillStr = GetFallbackFill(theme, fill, reference, fillMode, out opacity, GetHardCodedDefaultForItem); + break; + case eFillStyle.SolidFill: + //1. Standard case. There is a fill color to apply. + //Send in styleFill as well since a solid fill can refer to style color + var fc = tc.ColorConverter.GetThemeColor(theme, fill.SolidFill.Color, reference); + fillStr = GetAdjustmentsAndTransparency(fc, fillMode, out opacity); + break; + case eFillStyle.GradientFill: + gradFill = new DrawingRenderGradientFill(theme, fill.GradientFill, UserSpaceSettings.UserSpaceOnUse_Global); + break; + } + } return fillStr; } - internal static void SetDrawingBorderPropertiesNew(this RenderItem item, ExcelTheme theme, ExcelChartStyleColorManager reference, ExcelDrawingBorder border, double opacity, Func GetHardCodedDefaultForItem) + internal static void SetDrawingBorderPropertiesNew(this RenderItem item, ExcelTheme theme, ExcelChartStyleColorManager reference, ExcelDrawingBorder border, double opacity, bool hasBorder, Func GetHardCodedDefaultForItem) { - var fillColorStr = GetFillNew(border.Fill, theme, reference, item.BorderColorSource, out opacity, GetHardCodedDefaultForItem, out DrawingRenderGradientFill gradFill); + string fillColorStr = null; + DrawingRenderGradientFill gradFill = null; + if (border == null) + { + if (hasBorder) + { + fillColorStr = GetFillNew(null, theme, reference, item.BorderColorSource, out opacity, GetHardCodedDefaultForItem, out gradFill); + } + } + else + { + fillColorStr = GetFillNew(border.Fill, theme, reference, item.BorderColorSource, out opacity, GetHardCodedDefaultForItem, out gradFill); + } if(gradFill != null) { @@ -270,56 +274,59 @@ internal static void SetDrawingBorderPropertiesNew(this RenderItem item, ExcelTh internal static void SetDrawingPropertiesBorder(this RenderItem item, ExcelTheme theme, ExcelDrawingBorder border, ExcelChartStyleColorManager color, bool hasBorder, Color? nullColor=null, double defaultWidth = 1.5, UserSpaceSettings gradientUserSpaceOnUse = UserSpaceSettings.UserSpaceOnUse_Global, eChartStyle styleId = eChartStyle.Style2) { double? opacity = null; - if (border == null) - { - if (hasBorder) - { - item.BorderColor = GetFillColor(theme, null, color, item.BorderColorSource, out opacity, nullColor ?? theme.ColorScheme.Dark1.GetColor()); - } - } - else - { - switch (border.Fill.Style) - { - case eFillStyle.NoFill: - if (border.Fill.IsEmpty) - { - item.BorderColor = GetFillColor(theme, border.Fill, color, item.BorderColorSource, out opacity, nullColor ?? theme.ColorScheme.Dark1.GetColor()); - } - else - { - item.BorderColor = "none"; - } - break; - case eFillStyle.SolidFill: - item.BorderColor = GetFillColor(theme, border.Fill, color, item.BorderColorSource, out opacity); - item.BorderGradientFill = null; - break; - case eFillStyle.GradientFill: - item.BorderGradientFill = new DrawingRenderGradientFill(theme, border.Fill.GradientFill, gradientUserSpaceOnUse); - item.BorderColor = null; - break; - } - } + GetFillColor(theme, null, color, item.BorderColorSource, out opacity, nullColor ?? theme.ColorScheme.Dark1.GetColor()); + opacity = double.NaN; + SetDrawingBorderPropertiesNew(item, theme, color, border, opacity.Value, hasBorder, () => { return nullColor; }); + //if (border == null) + //{ + // if (hasBorder) + // { + // item.BorderColor = GetFillColor(theme, null, color, item.BorderColorSource, out opacity, nullColor ?? theme.ColorScheme.Dark1.GetColor()); + // } + //} + //else + //{ + // switch (border.Fill.Style) + // { + // case eFillStyle.NoFill: + // if (border.Fill.IsEmpty) + // { + // item.BorderColor = GetFillColor(theme, border.Fill, color, item.BorderColorSource, out opacity, nullColor ?? theme.ColorScheme.Dark1.GetColor()); + // } + // else + // { + // item.BorderColor = "none"; + // } + // break; + // case eFillStyle.SolidFill: + // item.BorderColor = GetFillColor(theme, border.Fill, color, item.BorderColorSource, out opacity); + // item.BorderGradientFill = null; + // break; + // case eFillStyle.GradientFill: + // item.BorderGradientFill = new DrawingRenderGradientFill(theme, border.Fill.GradientFill, gradientUserSpaceOnUse); + // item.BorderColor = null; + // break; + // } + //} - if (opacity.HasValue) - { - item.BorderOpacity = opacity; - } + //if (opacity != double.NaN) + //{ + // item.BorderOpacity = opacity; + //} - if (hasBorder && item.BorderColorSource != PathFillMode.None) - { - item.BorderWidth = (border?.Width??0D) == 0D ? defaultWidth : border.Width; - if (border!=null && border.LineStyle.HasValue && border.LineStyle != eLineStyle.Solid) - { - item.BorderDashArray = GetDashArray(border, item.BorderWidth.Value); - } - if (border != null && border.CompoundLineStyle != eCompoundLineStyle.Single) - { - item.CompoundLineStyle = (CompoundLineStyle)border.CompoundLineStyle; - //TODO:Add support double compound borders. - } - } + //if (hasBorder && item.BorderColorSource != PathFillMode.None) + //{ + // item.BorderWidth = (border?.Width??0D) == 0D ? defaultWidth : border.Width; + // if (border!=null && border.LineStyle.HasValue && border.LineStyle != eLineStyle.Solid) + // { + // item.BorderDashArray = GetDashArray(border, item.BorderWidth.Value); + // } + // if (border != null && border.CompoundLineStyle != eCompoundLineStyle.Single) + // { + // item.CompoundLineStyle = (CompoundLineStyle)border.CompoundLineStyle; + // //TODO:Add support double compound borders. + // } + //} } internal static void SetDrawingPropertiesEffects(this RenderItem item, ExcelTheme theme, ExcelDrawingEffectStyle effect) { diff --git a/src/EPPlus/Utils/TypeConversion/ColorConverter.cs b/src/EPPlus/Utils/TypeConversion/ColorConverter.cs index 8e54fea97b..2734e2e798 100644 --- a/src/EPPlus/Utils/TypeConversion/ColorConverter.cs +++ b/src/EPPlus/Utils/TypeConversion/ColorConverter.cs @@ -62,16 +62,20 @@ public static Color GetThemeColor(ExcelTheme theme, ExcelDrawingColorManager cm, { ExcelDrawingThemeColorManager newCm; - if (cmStyle.ColorType == eDrawingColorType.Scheme) + if(cmStyle.ColorType != eDrawingColorType.None) { - return GetThemeColor(theme, cmStyle); + if (cmStyle.ColorType == eDrawingColorType.Scheme) + { + return GetThemeColor(theme, cmStyle); + } + else + { + newCm = theme.ColorScheme.GetColorByEnum(cmStyle.SchemeColor.Color); + } + var nc = GetThemeColor(newCm); + return ApplyTransforms(nc, cm.Transforms); } - else - { - newCm = theme.ColorScheme.GetColorByEnum(cm.SchemeColor.Color); - } - var nc = GetThemeColor(newCm); - return ApplyTransforms(nc, cm.Transforms); + return Color.Empty; } var c = GetThemeColor(cm); From c53cf731dc18b8185f2bbda43048c454ffa45de4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ossian=20Edstr=C3=B6m?= Date: Fri, 21 Aug 2026 17:22:55 +0200 Subject: [PATCH 26/30] Started on each chart element providing style info --- .../Renderer/Chart/ChartAreaRenderer.cs | 20 +++ .../Renderer/Chart/ChartDrawingObject.cs | 117 ++++++++++++++++++ .../Renderer/Chart/ChartPlotareaRenderer.cs | 15 +++ .../ChartElementStyleTables.cs | 45 +++++++ .../Utils/TypeConversion/ColorConverter.cs | 15 ++- 5 files changed, 210 insertions(+), 2 deletions(-) create mode 100644 src/EPPlus/Drawing/Renderer/Chart/ChartStyleDefaults/ChartElementStyleTables.cs diff --git a/src/EPPlus/Drawing/Renderer/Chart/ChartAreaRenderer.cs b/src/EPPlus/Drawing/Renderer/Chart/ChartAreaRenderer.cs index 841fef4cd5..d875a08f0e 100644 --- a/src/EPPlus/Drawing/Renderer/Chart/ChartAreaRenderer.cs +++ b/src/EPPlus/Drawing/Renderer/Chart/ChartAreaRenderer.cs @@ -14,6 +14,7 @@ Date Author Change using EPPlus.DrawingRenderer.Svg; using System.Collections.Generic; using System.Drawing; +using OfficeOpenXml.Drawing; namespace EPPlusImageRenderer.Svg { @@ -41,6 +42,25 @@ internal override Color? DefaultBorderColor return Color.FromArgb(0x89, 0x89, 0x89); } } + + internal void InitStyleColors() + { + StyleBorderColor1 = GetThemeColorTint(eThemeSchemeColor.Text1, 0.75d); + StyleBorderColor2 = GetThemeColorTint(eThemeSchemeColor.Background1, 0.75d); + StyleBorderColor3 = GetThemeColorTint(eThemeSchemeColor.Background1, 0.75d); + StyleBorderColor4 = GetThemeColorTint(eThemeSchemeColor.Text1, 1d); + + var themedFill = ChartRenderer.Theme.FormatScheme.BorderStyle[0]; + + StyleColor1 = GetThemeColorTint(eThemeSchemeColor.Background1, 1d); + StyleColor2 = GetThemeColorTint(eThemeSchemeColor.Background1, 0.2d); + + //Make this go up by 1 per styleID somehow + StyleColor3 = GetThemeColorTint(eThemeSchemeColor.Accent1, 1d); + + StyleColor4 = GetThemeColorTint(eThemeSchemeColor.Background1, 0.95d); + } + public override void AppendRenderItems(List renderItems) { renderItems.Add(Rectangle); diff --git a/src/EPPlus/Drawing/Renderer/Chart/ChartDrawingObject.cs b/src/EPPlus/Drawing/Renderer/Chart/ChartDrawingObject.cs index 61cf694691..8162caaeca 100644 --- a/src/EPPlus/Drawing/Renderer/Chart/ChartDrawingObject.cs +++ b/src/EPPlus/Drawing/Renderer/Chart/ChartDrawingObject.cs @@ -19,11 +19,13 @@ Date Author Change using OfficeOpenXml.Drawing; using OfficeOpenXml.Drawing.Chart; using OfficeOpenXml.Drawing.Style.Coloring; +using OfficeOpenXml.Drawing.Theme; using OfficeOpenXml.FormulaParsing.Excel.Functions.MathFunctions; using OfficeOpenXml.Utils.TypeConversion; using System.Collections.Generic; using System.Drawing; using System.Linq; +using tc = OfficeOpenXml.Utils.TypeConversion; namespace EPPlusImageRenderer.Svg { @@ -38,6 +40,7 @@ internal ChartDrawingObject(ChartRenderer chart) ChartRenderer = chart; //Fixes null ref but might be inaccurate for some objects... Rectangle = new RectRenderItem(chart.Bounds); + //InitStyleColors(); } internal void SetMargins(ExcelTextBody tb) { @@ -107,6 +110,120 @@ internal List GetXSerie(List xSerie) return l; } + /// + /// Default style color for style 1-32 + /// + protected internal Color StyleColor1 { get; internal set; } + /// + /// Styles 33-34 + /// + protected internal Color StyleColor2 { get; internal set; } + /// + /// Styles 35-40 + /// + protected internal Color StyleColor3 { get; internal set; } + /// + /// Styles 41-48 + /// + protected internal Color StyleColor4 { get; internal set; } + + /// + /// Default style color for style 1-32 + /// + protected internal Color StyleBorderColor1 { get; internal set; } + /// + /// Styles 33-34 + /// + protected internal Color StyleBorderColor2 { get; internal set; } + /// + /// Styles 35-40 + /// + protected internal Color StyleBorderColor3 { get; internal set; } + /// + /// Styles 41-48 + /// + protected internal Color StyleBorderColor4 { get; internal set; } + + protected Color GetThemeColorTint(eThemeSchemeColor themeColor, double tint = 0.0d ) + { + var schemeClr = tc.ColorConverter.GetThemeColor(ChartRenderer.Theme, themeColor); + var tintedSchemeColor = tc.ColorConverter.ApplyTintDrawing(schemeClr, tint); + return tintedSchemeColor; + } + + //internal abstract void InitStyleColors(); + + /// + /// This function provides the default chart color for a given chart object + /// + /// Chart style Id + /// + internal Color? GetStyleColorOrDefault(int styleId) + { + Color? themeColor = null; + styleId = styleId > (int)eChartStyle.Style48 ? (int)eChartStyle.Style2 : styleId; + + if (styleId == 0) + { + return Color.Empty; + } + + var bg = ChartRenderer.Theme.FormatScheme.BackgroundFillStyle[0]; + + themeColor = tc.ColorConverter.GetThemeColor(ChartRenderer.Theme, eThemeSchemeColor.Background1); + + if (bg.SolidFill.Color.ColorType == eDrawingColorType.Scheme && bg.SolidFill.Color.SchemeColor.Color == eSchemeColor.Style) + { + if(styleId <= 32) + { + themeColor = StyleColor1; + } + else if(styleId <= 34) + { + themeColor = StyleColor2; + } + else if(styleId <= 40) + { + themeColor = StyleColor3; + } + else if(styleId <= 48) + { + themeColor = StyleColor4; + } + + //if (styleId <= 40) + //{ + // //Text1 AKA dk1 (in standard case) + // themeColor = tc.ColorConverter.GetThemeColor(ChartRenderer.Theme, eThemeSchemeColor.Text1); + + // //var bg1Col = tc.ColorConverter.GetThemeColor(Theme, eThemeSchemeColor.Background1); + + // if (bg.SolidFill.Color.Transforms.Count > 0) + // { + // //themeColor = tc.ColorConverter.ApplyTintDrawing(themeColor.Value, 0.15d); + // //but even in this case if there is no ln node found in style it appears to default to 75% despite a scheme color existing in the theme + // themeColor = tc.ColorConverter.ApplyTransforms(themeColor.Value, bg.SolidFill.Color.Transforms); + // } + // else + // { + // //Default value Should arguably be 75% tint themeColor but something is strange... + // //It appears closer to 50% in this specific case + // //It also appears to be tx1 (black) and apply color and tint 0.25 in vba + // var newTheme = tc.ColorConverter.ApplyTintDrawing(themeColor.Value, 0.25d); + // themeColor = newTheme; + + // } + //} + //else + //{ + // //41-48 + // //aka light1 + // themeColor = tc.ColorConverter.GetThemeColor(ChartRenderer.Theme, eThemeSchemeColor.Background1); + // //themedLine = null; + //} + } + return themeColor; + } } } diff --git a/src/EPPlus/Drawing/Renderer/Chart/ChartPlotareaRenderer.cs b/src/EPPlus/Drawing/Renderer/Chart/ChartPlotareaRenderer.cs index cfabf254d4..8436bd1445 100644 --- a/src/EPPlus/Drawing/Renderer/Chart/ChartPlotareaRenderer.cs +++ b/src/EPPlus/Drawing/Renderer/Chart/ChartPlotareaRenderer.cs @@ -252,5 +252,20 @@ internal void DrawSeries() } } internal override Color? DefaultFillColor { get => null; } + + internal void InitStyleColors() + { + //Plot area has no line + //therefore we do not set styleBorderColor + var themedFill = ChartRenderer.Theme.FormatScheme.BackgroundFillStyle[0]; + + StyleColor1 = GetThemeColorTint(eThemeSchemeColor.Background1, 1d); + StyleColor2 = GetThemeColorTint(eThemeSchemeColor.Background1, 0.2d); + + //Make this go up by 1 per styleID somehow + StyleColor3 = GetThemeColorTint(eThemeSchemeColor.Accent1, 1d); + + StyleColor4 = GetThemeColorTint(eThemeSchemeColor.Background1, 0.95d); + } } } diff --git a/src/EPPlus/Drawing/Renderer/Chart/ChartStyleDefaults/ChartElementStyleTables.cs b/src/EPPlus/Drawing/Renderer/Chart/ChartStyleDefaults/ChartElementStyleTables.cs new file mode 100644 index 0000000000..12de0bafb4 --- /dev/null +++ b/src/EPPlus/Drawing/Renderer/Chart/ChartStyleDefaults/ChartElementStyleTables.cs @@ -0,0 +1,45 @@ +using System; +using System.Collections.Generic; +using System.Drawing; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace OfficeOpenXml.Drawing.Renderer.Chart.ChartElementStyleTables +{ + [Flags] + enum ChartElement + { + None = 0, + ChartArea = 1, + PlotArea2d = 2, + PloatArea3d = 4, + Axis = 8, + MinorGridLines = 16, + MajorGridLines = 32, + DataTable = 64, + Floor = 128, + Walls = 256, + OtherLines = 512, + } + + internal static class ChartElementStyleTables + { + static Color GetLineColorForChartElement(ChartElement element, int ChartStyleId) + { + return Color.Empty; + if(element.HasFlag(ChartElement.Axis | ChartElement.MajorGridLines)) + { + if(ChartStyleId <= 32) + { + //return Tx1 + } + else + { + //return dk1 + } + } + + } + } +} diff --git a/src/EPPlus/Utils/TypeConversion/ColorConverter.cs b/src/EPPlus/Utils/TypeConversion/ColorConverter.cs index 2734e2e798..82870c4be8 100644 --- a/src/EPPlus/Utils/TypeConversion/ColorConverter.cs +++ b/src/EPPlus/Utils/TypeConversion/ColorConverter.cs @@ -14,6 +14,7 @@ Date Author Change using OfficeOpenXml.Drawing; using OfficeOpenXml.Drawing.Style.Coloring; using OfficeOpenXml.Drawing.Theme; +using OfficeOpenXml.Style; using System; using System.Drawing; using System.Linq; @@ -32,8 +33,18 @@ public static Color GetThemeColor(ExcelTheme theme, ExcelDrawingColorManager cm) { if(cm!=null && cm.ColorType==eDrawingColorType.Scheme) { - var newCm=theme.ColorScheme.GetColorByEnum(cm.SchemeColor.Color); - if (newCm == null) return Color.Empty; + ExcelDrawingThemeColorManager newCm; + if (cm.SchemeColor.Color == eSchemeColor.Style) + { + //At this stage we have no style and must use + //Hardcoded fallback. For fills (on charts) this is bg1 + //For shapes Accent1 + newCm = theme.ColorScheme.GetColorByEnum(eThemeSchemeColor.Background1); + } + else + { + newCm = theme.ColorScheme.GetColorByEnum(cm.SchemeColor.Color); + } var nc = GetThemeColor(newCm); return ApplyTransforms(nc, cm.Transforms); } From 3c396de403ea8a2effa82411eda829c90e1914ce Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ossian=20Edstr=C3=B6m?= Date: Mon, 24 Aug 2026 11:37:21 +0200 Subject: [PATCH 27/30] Added new default chartDrawingObject --- .../Renderer/Chart/ChartPlotareaRenderer.cs | 2 +- .../ChartElementStyleTables.cs | 244 +++++++++++++++++- src/EPPlus/Drawing/Renderer/ChartRenderer.cs | 1 - .../Utils/TypeConversion/ColorConverter.cs | 6 + 4 files changed, 244 insertions(+), 9 deletions(-) diff --git a/src/EPPlus/Drawing/Renderer/Chart/ChartPlotareaRenderer.cs b/src/EPPlus/Drawing/Renderer/Chart/ChartPlotareaRenderer.cs index 8436bd1445..489c73a530 100644 --- a/src/EPPlus/Drawing/Renderer/Chart/ChartPlotareaRenderer.cs +++ b/src/EPPlus/Drawing/Renderer/Chart/ChartPlotareaRenderer.cs @@ -263,7 +263,7 @@ internal void InitStyleColors() StyleColor2 = GetThemeColorTint(eThemeSchemeColor.Background1, 0.2d); //Make this go up by 1 per styleID somehow - StyleColor3 = GetThemeColorTint(eThemeSchemeColor.Accent1, 1d); + StyleColor3 = GetThemeColorTint(eThemeSchemeColor.Accent1, 0.2d); StyleColor4 = GetThemeColorTint(eThemeSchemeColor.Background1, 0.95d); } diff --git a/src/EPPlus/Drawing/Renderer/Chart/ChartStyleDefaults/ChartElementStyleTables.cs b/src/EPPlus/Drawing/Renderer/Chart/ChartStyleDefaults/ChartElementStyleTables.cs index 12de0bafb4..e609aeb24d 100644 --- a/src/EPPlus/Drawing/Renderer/Chart/ChartStyleDefaults/ChartElementStyleTables.cs +++ b/src/EPPlus/Drawing/Renderer/Chart/ChartStyleDefaults/ChartElementStyleTables.cs @@ -1,9 +1,15 @@ -using System; +using EPPlusImageRenderer; +using EPPlusImageRenderer.Svg; +using OfficeOpenXml.Drawing.Chart; +using OfficeOpenXml.Drawing.Theme; +using OfficeOpenXml.FormulaParsing.Excel.Functions.MathFunctions; +using System; using System.Collections.Generic; using System.Drawing; using System.Linq; using System.Text; -using System.Threading.Tasks; +using System.Xml.Linq; +using tc = OfficeOpenXml.Utils.TypeConversion; namespace OfficeOpenXml.Drawing.Renderer.Chart.ChartElementStyleTables { @@ -23,23 +29,247 @@ enum ChartElement OtherLines = 512, } - internal static class ChartElementStyleTables + internal class ChartDrawingObjectWithDefaults : ChartDrawingObject { - static Color GetLineColorForChartElement(ChartElement element, int ChartStyleId) + public ChartDrawingObjectWithDefaults(ChartRenderer chart) : base(chart) { - return Color.Empty; + + } + + private Color GetSchemeColorTint(eSchemeColor sColor, double tint = 0.0d) + { + var schemeClr = tc.ColorConverter.GetSchemeColor(ChartRenderer.Theme, sColor); + var tintedSchemeColor = tc.ColorConverter.ApplyTintDrawing(schemeClr, tint); + return tintedSchemeColor; + } + + private Color GetThemeColorTint(eThemeSchemeColor themeColor, double tint = 0.0d) + { + var schemeClr = tc.ColorConverter.GetThemeColor(ChartRenderer.Theme, themeColor); + var tintedSchemeColor = tc.ColorConverter.ApplyTintDrawing(schemeClr, tint); + return tintedSchemeColor; + } + + internal Color? GetStyleColorOrDefault(int styleId, Color col1, Color col2, Color col3, Color col4) + { + Color? themeColor = null; + //Chart style can only be above 48 if it is Style102 which in this case should be equivalent with style2 + //Alternatively it's an unkown or unset style which should also default to style2 + styleId = styleId > (int)eChartStyle.Style48 ? (int)eChartStyle.Style2 : styleId; + + if (styleId == 0) + { + return Color.Empty; + } + + if (styleId <= 32) + { + themeColor = col1; + } + else if (styleId <= 34) + { + themeColor = col2; + } + else if (styleId <= 40) + { + themeColor = col3; + } + else if (styleId <= 48) + { + themeColor = col4; + } + + return themeColor; + } + + /// + /// + /// + /// + /// + /// The line color with fill styles etc applied + /// + /// + protected ExcelThemeLine GetThemedLine(ChartElement element, int ChartStyleId, out Color? lineColor) + { + if(element.HasFlag(ChartElement.Floor | ChartElement.ChartArea)) + { + lineColor = GetDefaultBorderColorForElement(element, ChartStyleId); + var themedLine = ChartRenderer.Theme.FormatScheme.BorderStyle[0]; + + if (themedLine.HasFill == false) + { + //Node exists but has no fill. Excel considers this the same as transparent/noFill + lineColor = Color.Transparent; + return themedLine; + } + + if (ChartStyleId < 41) + { + if (themedLine.Fill.SolidFill.Color.Transforms.Count > 0) + { + //themeColor = tc.ColorConverter.ApplyTintDrawing(themeColor.Value, 0.15d); + //but even in this case if there is no ln node found in style it appears to default to 75% despite a scheme color existing in the theme + lineColor = tc.ColorConverter.ApplyTransforms(lineColor.Value, themedLine.Fill.SolidFill.Color.Transforms); + } + else + { + //Default value Should arguably be 75% tint themeColor but something is strange... + //It appears closer to 50% in this specific case + //It also appears to be tx1 (black) and apply color and tint 0.25 in vba + var newTheme = tc.ColorConverter.ApplyTintDrawing(lineColor.Value, 0.25d); + lineColor = newTheme; + } + } + else + { + //No Line + lineColor = Color.Transparent; + return null; + } + + return themedLine; + } + else + { + throw new InvalidOperationException( + $"The enum option: '{Enum.GetName(typeof(ChartElement), element)}' is invalid. " + + $"Only ChartArea or Floor has a default themed line"); + } + } + + /// + /// + /// + /// + /// + /// The fill color with fill styles etc applied + /// + /// + protected ExcelDrawingFill GetThemedFill(ChartElement element, int ChartStyleId, out Color? fillColor) + { + var bg = ChartRenderer.Theme.FormatScheme.BackgroundFillStyle[0]; + + if (element.HasFlag(ChartElement.Floor | ChartElement.Walls)) + { + fillColor = GetDefaultFillColorForElement(element, ChartStyleId); + var themedFill = ChartRenderer.Theme.FormatScheme.BackgroundFillStyle[0]; + + if (ChartStyleId > 32) + { + if (themedFill.SolidFill.Color.Transforms.Count > 0) + { + fillColor = tc.ColorConverter.ApplyTransforms(fillColor.Value, themedFill.SolidFill.Color.Transforms); + } + else + { + //Hardcoded default for fills without any actual info in excel + var newTheme = tc.ColorConverter.ApplyTintDrawing(fillColor.Value, 0.75d); + fillColor = newTheme; + } + } + else + { + //No Fill + fillColor = Color.Transparent; + return null; + } + + return themedFill; + } + else + { + throw new InvalidOperationException( + $"The enum option: '{Enum.GetName(typeof(ChartElement), element)}' is invalid. " + + $"Only Walls or Floor has a default themed line"); + } + } + + protected Color? GetDefaultBorderColorForElement(ChartElement element, int ChartStyleId) + { + //return Color.Empty; + if(element.HasFlag(ChartElement.Axis | ChartElement.MajorGridLines)) { + //There's only really two options in this particular case if(ChartStyleId <= 32) { - //return Tx1 + return GetSchemeColorTint(eSchemeColor.Text1, 0.75d); } else { - //return dk1 + return GetSchemeColorTint(eSchemeColor.Background1, 0.75d); } } + else if(element.HasFlag(ChartElement.MinorGridLines)) + { + var retCol = GetSchemeColorTint(eSchemeColor.Text1, 0.5d); + var retCol2and3 = GetSchemeColorTint(eSchemeColor.Background1, 0.5d); + var retCol4 = GetSchemeColorTint(eSchemeColor.Background1, 0.9d); + + return GetStyleColorOrDefault(ChartStyleId, retCol, retCol2and3, retCol2and3, retCol4); + } + else if (element.HasFlag(ChartElement.ChartArea | ChartElement.DataTable | ChartElement.Floor)) + { + var retCol = GetSchemeColorTint(eSchemeColor.Text1, 0.75d); + var retCol2and3 = GetSchemeColorTint(eSchemeColor.Background1, 0.75d); + var retCol4 = GetSchemeColorTint(eSchemeColor.Text1, 1d); + + return GetStyleColorOrDefault(ChartStyleId, retCol, retCol2and3, retCol2and3, retCol4); + } + else + { + //Other lines should technically always be the enum here but keep it as Else just in case + var retCol = GetSchemeColorTint(eSchemeColor.Text1, 1d); + var retCol2and3 = GetSchemeColorTint(eSchemeColor.Background1, 1d); + var retCol4 = GetSchemeColorTint(eSchemeColor.Text1, 1d); + + return GetStyleColorOrDefault(ChartStyleId, retCol, retCol2and3, retCol2and3, retCol4); + } + } + + + private Color? GetDefaultAccent(int ChartStyleId) + { + if(ChartStyleId < 35 || ChartStyleId > 40) + { + throw new InvalidOperationException($"Invalid ChartStyleId '{ChartStyleId}'" + + $"Default Accent tint must be between 35 and 40"); + } + //35 == accent1, 36 == accent2 etc. + var accentColor = (eSchemeColor.Accent1 + (ChartStyleId) - 35); + return GetSchemeColorTint(accentColor, 0.2d); + } + + protected Color? GetDefaultFillColorForElement(ChartElement element, int ChartStyleId) + { + if (element.HasFlag(ChartElement.ChartArea)) + { + var retCol = GetSchemeColorTint(eSchemeColor.Background1); + var retCol2And3 = GetSchemeColorTint(eSchemeColor.Text1); + var retCol4 = GetSchemeColorTint(eSchemeColor.Background1); + + return GetStyleColorOrDefault(ChartStyleId, retCol, retCol2And3, retCol2And3, retCol4); + } + else if(element.HasFlag(ChartElement.Floor | ChartElement.Walls | ChartElement.PlotArea2d)) + { + var retCol = GetSchemeColorTint(eSchemeColor.Background1); + var retCol2 = GetSchemeColorTint(eSchemeColor.Background1, 0.2d); + var retCol3 = GetDefaultAccent(ChartStyleId); + var retCol4 = GetSchemeColorTint(eSchemeColor.Background1, 0.95d); + return GetStyleColorOrDefault(ChartStyleId, retCol, retCol2, retCol3.Value, retCol4); + } + else + { + return null; + } } + + protected Color GetEffectForChartElement(ChartElement element, int ChartStyleId) + { + throw new NotImplementedException("This method has not been implmented yet"); + } + } } diff --git a/src/EPPlus/Drawing/Renderer/ChartRenderer.cs b/src/EPPlus/Drawing/Renderer/ChartRenderer.cs index fb2d9e2197..5e6bfe9443 100644 --- a/src/EPPlus/Drawing/Renderer/ChartRenderer.cs +++ b/src/EPPlus/Drawing/Renderer/ChartRenderer.cs @@ -437,7 +437,6 @@ private void SetChartArea(SvgRenderOptions options) //It also appears to be tx1 (black) and apply color and tint 0.25 in vba var newTheme = tc.ColorConverter.ApplyTintDrawing(themeColor.Value, 0.25d); themeColor = newTheme; - } } else diff --git a/src/EPPlus/Utils/TypeConversion/ColorConverter.cs b/src/EPPlus/Utils/TypeConversion/ColorConverter.cs index 82870c4be8..3feec76f69 100644 --- a/src/EPPlus/Utils/TypeConversion/ColorConverter.cs +++ b/src/EPPlus/Utils/TypeConversion/ColorConverter.cs @@ -24,6 +24,12 @@ namespace OfficeOpenXml.Utils.TypeConversion { public class ColorConverter { + public static Color GetSchemeColor(ExcelTheme theme, eSchemeColor sColor) + { + var cm = theme.ColorScheme.GetColorByEnum(sColor); + return GetThemeColor(cm); + } + public static Color GetThemeColor(ExcelTheme theme, eThemeSchemeColor tc) { var cm = theme.ColorScheme.GetColorByEnum(tc); From cafc59ed24a76f3205eabf206c62054f7244c963 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ossian=20Edstr=C3=B6m?= Date: Mon, 24 Aug 2026 14:04:10 +0200 Subject: [PATCH 28/30] Fixed multiple bugs in new system removed some of old --- .../Chart/ChartStyleFallbackTest.cs | 8 +- .../Renderer/Chart/ChartAreaRenderer.cs | 31 ++--- .../Renderer/Chart/ChartDrawingObject.cs | 116 ------------------ .../Renderer/Chart/ChartPlotareaRenderer.cs | 15 --- .../ChartElementStyleTables.cs | 44 +++++-- src/EPPlus/Drawing/Renderer/ChartRenderer.cs | 6 +- 6 files changed, 54 insertions(+), 166 deletions(-) diff --git a/src/EPPlus.DrawingRenderer.Tests/Chart/ChartStyleFallbackTest.cs b/src/EPPlus.DrawingRenderer.Tests/Chart/ChartStyleFallbackTest.cs index 4dd4b58e92..110a010eb7 100644 --- a/src/EPPlus.DrawingRenderer.Tests/Chart/ChartStyleFallbackTest.cs +++ b/src/EPPlus.DrawingRenderer.Tests/Chart/ChartStyleFallbackTest.cs @@ -241,11 +241,11 @@ public void ManualSystemText() { if (d is ExcelChart c) { - var borderSetting = c.Border; - var borderDirectColor = borderSetting.Fill.Color; - var theme = p.Workbook.ThemeManager.GetOrCreateTheme(); + //var borderSetting = c.Border; + //var borderDirectColor = borderSetting.Fill.Color; + //var theme = p.Workbook.ThemeManager.GetOrCreateTheme(); - var defaultColorFromTheme = theme.ColorScheme.Dark1; + //var defaultColorFromTheme = theme.ColorScheme.Dark1; var svg = c.ToSvg(); SaveTextFileToWorkbook($"svg\\{fileName}_{ws.Name}_{c.Name}.svg", svg); diff --git a/src/EPPlus/Drawing/Renderer/Chart/ChartAreaRenderer.cs b/src/EPPlus/Drawing/Renderer/Chart/ChartAreaRenderer.cs index d875a08f0e..f4bab0d977 100644 --- a/src/EPPlus/Drawing/Renderer/Chart/ChartAreaRenderer.cs +++ b/src/EPPlus/Drawing/Renderer/Chart/ChartAreaRenderer.cs @@ -12,13 +12,14 @@ Date Author Change *************************************************************************************************/ using EPPlus.DrawingRenderer.RenderItems; using EPPlus.DrawingRenderer.Svg; +using OfficeOpenXml.Drawing; +using OfficeOpenXml.Drawing.Renderer.Chart.ChartElementStyleTables; using System.Collections.Generic; using System.Drawing; -using OfficeOpenXml.Drawing; namespace EPPlusImageRenderer.Svg { - internal class ChartAreaRenderer : ChartDrawingObject + internal class ChartAreaRenderer : ChartDrawingObjectWithDefaults { public ChartAreaRenderer(ChartRenderer sc, SvgRenderOptions options) : base(sc) { @@ -43,27 +44,21 @@ internal override Color? DefaultBorderColor } } - internal void InitStyleColors() + public override void AppendRenderItems(List renderItems) { - StyleBorderColor1 = GetThemeColorTint(eThemeSchemeColor.Text1, 0.75d); - StyleBorderColor2 = GetThemeColorTint(eThemeSchemeColor.Background1, 0.75d); - StyleBorderColor3 = GetThemeColorTint(eThemeSchemeColor.Background1, 0.75d); - StyleBorderColor4 = GetThemeColorTint(eThemeSchemeColor.Text1, 1d); - - var themedFill = ChartRenderer.Theme.FormatScheme.BorderStyle[0]; - - StyleColor1 = GetThemeColorTint(eThemeSchemeColor.Background1, 1d); - StyleColor2 = GetThemeColorTint(eThemeSchemeColor.Background1, 0.2d); - - //Make this go up by 1 per styleID somehow - StyleColor3 = GetThemeColorTint(eThemeSchemeColor.Accent1, 1d); + renderItems.Add(Rectangle); + } - StyleColor4 = GetThemeColorTint(eThemeSchemeColor.Background1, 0.95d); + internal override Color? GetDefaultBorderColor() + { + //Kept here in case needed in future for effect etc. + var themedLine = GetThemedLine(ChartElement.ChartArea, (int)Chart.Style, out Color? lineCol); + return lineCol; } - public override void AppendRenderItems(List renderItems) + internal override Color? GetDefaultFillColor() { - renderItems.Add(Rectangle); + return GetDefaultFillColorForElement(ChartElement.ChartArea, (int)Chart.Style); } } } diff --git a/src/EPPlus/Drawing/Renderer/Chart/ChartDrawingObject.cs b/src/EPPlus/Drawing/Renderer/Chart/ChartDrawingObject.cs index 8162caaeca..7f59f29a20 100644 --- a/src/EPPlus/Drawing/Renderer/Chart/ChartDrawingObject.cs +++ b/src/EPPlus/Drawing/Renderer/Chart/ChartDrawingObject.cs @@ -109,121 +109,5 @@ internal List GetXSerie(List xSerie) } return l; } - - /// - /// Default style color for style 1-32 - /// - protected internal Color StyleColor1 { get; internal set; } - /// - /// Styles 33-34 - /// - protected internal Color StyleColor2 { get; internal set; } - /// - /// Styles 35-40 - /// - protected internal Color StyleColor3 { get; internal set; } - /// - /// Styles 41-48 - /// - protected internal Color StyleColor4 { get; internal set; } - - - /// - /// Default style color for style 1-32 - /// - protected internal Color StyleBorderColor1 { get; internal set; } - /// - /// Styles 33-34 - /// - protected internal Color StyleBorderColor2 { get; internal set; } - /// - /// Styles 35-40 - /// - protected internal Color StyleBorderColor3 { get; internal set; } - /// - /// Styles 41-48 - /// - protected internal Color StyleBorderColor4 { get; internal set; } - - protected Color GetThemeColorTint(eThemeSchemeColor themeColor, double tint = 0.0d ) - { - var schemeClr = tc.ColorConverter.GetThemeColor(ChartRenderer.Theme, themeColor); - var tintedSchemeColor = tc.ColorConverter.ApplyTintDrawing(schemeClr, tint); - return tintedSchemeColor; - } - - //internal abstract void InitStyleColors(); - - /// - /// This function provides the default chart color for a given chart object - /// - /// Chart style Id - /// - internal Color? GetStyleColorOrDefault(int styleId) - { - Color? themeColor = null; - styleId = styleId > (int)eChartStyle.Style48 ? (int)eChartStyle.Style2 : styleId; - - if (styleId == 0) - { - return Color.Empty; - } - - var bg = ChartRenderer.Theme.FormatScheme.BackgroundFillStyle[0]; - - themeColor = tc.ColorConverter.GetThemeColor(ChartRenderer.Theme, eThemeSchemeColor.Background1); - - if (bg.SolidFill.Color.ColorType == eDrawingColorType.Scheme && bg.SolidFill.Color.SchemeColor.Color == eSchemeColor.Style) - { - if(styleId <= 32) - { - themeColor = StyleColor1; - } - else if(styleId <= 34) - { - themeColor = StyleColor2; - } - else if(styleId <= 40) - { - themeColor = StyleColor3; - } - else if(styleId <= 48) - { - themeColor = StyleColor4; - } - - //if (styleId <= 40) - //{ - // //Text1 AKA dk1 (in standard case) - // themeColor = tc.ColorConverter.GetThemeColor(ChartRenderer.Theme, eThemeSchemeColor.Text1); - - // //var bg1Col = tc.ColorConverter.GetThemeColor(Theme, eThemeSchemeColor.Background1); - - // if (bg.SolidFill.Color.Transforms.Count > 0) - // { - // //themeColor = tc.ColorConverter.ApplyTintDrawing(themeColor.Value, 0.15d); - // //but even in this case if there is no ln node found in style it appears to default to 75% despite a scheme color existing in the theme - // themeColor = tc.ColorConverter.ApplyTransforms(themeColor.Value, bg.SolidFill.Color.Transforms); - // } - // else - // { - // //Default value Should arguably be 75% tint themeColor but something is strange... - // //It appears closer to 50% in this specific case - // //It also appears to be tx1 (black) and apply color and tint 0.25 in vba - // var newTheme = tc.ColorConverter.ApplyTintDrawing(themeColor.Value, 0.25d); - // themeColor = newTheme; - - // } - //} - //else - //{ - // //41-48 - // //aka light1 - // themeColor = tc.ColorConverter.GetThemeColor(ChartRenderer.Theme, eThemeSchemeColor.Background1); - // //themedLine = null; - //} - } - return themeColor; - } } } diff --git a/src/EPPlus/Drawing/Renderer/Chart/ChartPlotareaRenderer.cs b/src/EPPlus/Drawing/Renderer/Chart/ChartPlotareaRenderer.cs index 489c73a530..cfabf254d4 100644 --- a/src/EPPlus/Drawing/Renderer/Chart/ChartPlotareaRenderer.cs +++ b/src/EPPlus/Drawing/Renderer/Chart/ChartPlotareaRenderer.cs @@ -252,20 +252,5 @@ internal void DrawSeries() } } internal override Color? DefaultFillColor { get => null; } - - internal void InitStyleColors() - { - //Plot area has no line - //therefore we do not set styleBorderColor - var themedFill = ChartRenderer.Theme.FormatScheme.BackgroundFillStyle[0]; - - StyleColor1 = GetThemeColorTint(eThemeSchemeColor.Background1, 1d); - StyleColor2 = GetThemeColorTint(eThemeSchemeColor.Background1, 0.2d); - - //Make this go up by 1 per styleID somehow - StyleColor3 = GetThemeColorTint(eThemeSchemeColor.Accent1, 0.2d); - - StyleColor4 = GetThemeColorTint(eThemeSchemeColor.Background1, 0.95d); - } } } diff --git a/src/EPPlus/Drawing/Renderer/Chart/ChartStyleDefaults/ChartElementStyleTables.cs b/src/EPPlus/Drawing/Renderer/Chart/ChartStyleDefaults/ChartElementStyleTables.cs index e609aeb24d..39835b0c12 100644 --- a/src/EPPlus/Drawing/Renderer/Chart/ChartStyleDefaults/ChartElementStyleTables.cs +++ b/src/EPPlus/Drawing/Renderer/Chart/ChartStyleDefaults/ChartElementStyleTables.cs @@ -2,6 +2,7 @@ using EPPlusImageRenderer.Svg; using OfficeOpenXml.Drawing.Chart; using OfficeOpenXml.Drawing.Theme; +using OfficeOpenXml.Encryption; using OfficeOpenXml.FormulaParsing.Excel.Functions.MathFunctions; using System; using System.Collections.Generic; @@ -29,7 +30,7 @@ enum ChartElement OtherLines = 512, } - internal class ChartDrawingObjectWithDefaults : ChartDrawingObject + internal abstract class ChartDrawingObjectWithDefaults : ChartDrawingObject { public ChartDrawingObjectWithDefaults(ChartRenderer chart) : base(chart) { @@ -38,6 +39,14 @@ public ChartDrawingObjectWithDefaults(ChartRenderer chart) : base(chart) private Color GetSchemeColorTint(eSchemeColor sColor, double tint = 0.0d) { + if(tint < 0) + { + tint = 1 + tint; + } + else if(tint > 0) + { + tint = 1 - tint; + } var schemeClr = tc.ColorConverter.GetSchemeColor(ChartRenderer.Theme, sColor); var tintedSchemeColor = tc.ColorConverter.ApplyTintDrawing(schemeClr, tint); return tintedSchemeColor; @@ -92,11 +101,17 @@ private Color GetThemeColorTint(eThemeSchemeColor themeColor, double tint = 0.0d /// protected ExcelThemeLine GetThemedLine(ChartElement element, int ChartStyleId, out Color? lineColor) { - if(element.HasFlag(ChartElement.Floor | ChartElement.ChartArea)) + //Chart style can only be above 48 if it is Style102 which in this case should be equivalent with style2 + //Alternatively it's an unkown or unset style which should also default to style2 + var styleId = ChartStyleId > (int)eChartStyle.Style48 ? (int)eChartStyle.Style2 : ChartStyleId; + + var AreaOrFloor = (ChartElement.ChartArea | ChartElement.Floor); + if (AreaOrFloor.HasFlag(element)) { - lineColor = GetDefaultBorderColorForElement(element, ChartStyleId); + lineColor = GetDefaultBorderColorForElement(element, styleId); var themedLine = ChartRenderer.Theme.FormatScheme.BorderStyle[0]; + var themeColor = tc.ColorConverter.GetThemeColor(ChartRenderer.Theme, themedLine.Fill.SolidFill.Color); if (themedLine.HasFill == false) { //Node exists but has no fill. Excel considers this the same as transparent/noFill @@ -104,7 +119,7 @@ protected ExcelThemeLine GetThemedLine(ChartElement element, int ChartStyleId, o return themedLine; } - if (ChartStyleId < 41) + if (styleId < 41) { if (themedLine.Fill.SolidFill.Color.Transforms.Count > 0) { @@ -148,14 +163,18 @@ protected ExcelThemeLine GetThemedLine(ChartElement element, int ChartStyleId, o /// protected ExcelDrawingFill GetThemedFill(ChartElement element, int ChartStyleId, out Color? fillColor) { + //Chart style can only be above 48 if it is Style102 which in this case should be equivalent with style2 + //Alternatively it's an unkown or unset style which should also default to style2 + var styleId = ChartStyleId > (int)eChartStyle.Style48 ? (int)eChartStyle.Style2 : ChartStyleId; + var bg = ChartRenderer.Theme.FormatScheme.BackgroundFillStyle[0]; - if (element.HasFlag(ChartElement.Floor | ChartElement.Walls)) + if ((ChartElement.Floor | ChartElement.Walls).HasFlag(element)) { - fillColor = GetDefaultFillColorForElement(element, ChartStyleId); + fillColor = GetDefaultFillColorForElement(element, styleId); var themedFill = ChartRenderer.Theme.FormatScheme.BackgroundFillStyle[0]; - if (ChartStyleId > 32) + if (styleId > 32) { if (themedFill.SolidFill.Color.Transforms.Count > 0) { @@ -181,7 +200,7 @@ protected ExcelDrawingFill GetThemedFill(ChartElement element, int ChartStyleId, { throw new InvalidOperationException( $"The enum option: '{Enum.GetName(typeof(ChartElement), element)}' is invalid. " + - $"Only Walls or Floor has a default themed line"); + $"Only Walls or Floor has a default themed fill"); } } @@ -189,7 +208,7 @@ protected ExcelDrawingFill GetThemedFill(ChartElement element, int ChartStyleId, { //return Color.Empty; - if(element.HasFlag(ChartElement.Axis | ChartElement.MajorGridLines)) + if((ChartElement.Axis | ChartElement.MajorGridLines).HasFlag(element)) { //There's only really two options in this particular case if(ChartStyleId <= 32) @@ -209,7 +228,7 @@ protected ExcelDrawingFill GetThemedFill(ChartElement element, int ChartStyleId, return GetStyleColorOrDefault(ChartStyleId, retCol, retCol2and3, retCol2and3, retCol4); } - else if (element.HasFlag(ChartElement.ChartArea | ChartElement.DataTable | ChartElement.Floor)) + else if ((ChartElement.ChartArea | ChartElement.DataTable | ChartElement.Floor).HasFlag(element)) { var retCol = GetSchemeColorTint(eSchemeColor.Text1, 0.75d); var retCol2and3 = GetSchemeColorTint(eSchemeColor.Background1, 0.75d); @@ -251,7 +270,7 @@ protected ExcelDrawingFill GetThemedFill(ChartElement element, int ChartStyleId, return GetStyleColorOrDefault(ChartStyleId, retCol, retCol2And3, retCol2And3, retCol4); } - else if(element.HasFlag(ChartElement.Floor | ChartElement.Walls | ChartElement.PlotArea2d)) + else if((ChartElement.Floor | ChartElement.Walls | ChartElement.PlotArea2d).HasFlag(element)) { var retCol = GetSchemeColorTint(eSchemeColor.Background1); var retCol2 = GetSchemeColorTint(eSchemeColor.Background1, 0.2d); @@ -271,5 +290,8 @@ protected Color GetEffectForChartElement(ChartElement element, int ChartStyleId) throw new NotImplementedException("This method has not been implmented yet"); } + + abstract internal Color? GetDefaultFillColor(); + abstract internal Color? GetDefaultBorderColor(); } } diff --git a/src/EPPlus/Drawing/Renderer/ChartRenderer.cs b/src/EPPlus/Drawing/Renderer/ChartRenderer.cs index 5e6bfe9443..00bb34e175 100644 --- a/src/EPPlus/Drawing/Renderer/ChartRenderer.cs +++ b/src/EPPlus/Drawing/Renderer/ChartRenderer.cs @@ -361,7 +361,7 @@ private void SetChartArea(SvgRenderOptions options) item.Rectangle.Width = Bounds.Width; item.Rectangle.Height = Bounds.Height; - item.Rectangle.SetDrawingPropertiesFill(Theme, Chart.Fill, Chart.StyleManager.Style?.ChartArea.FillReference.Color, UserSpaceSettings.UserSpaceOnUse_Global, item.DefaultFillColor); + item.Rectangle.SetDrawingPropertiesFill(Theme, Chart.Fill, Chart.StyleManager.Style?.ChartArea.FillReference.Color, UserSpaceSettings.UserSpaceOnUse_Global, item.GetDefaultFillColor()); var borderstyle = Theme.FormatScheme.BorderStyle[0]; @@ -379,13 +379,15 @@ private void SetChartArea(SvgRenderOptions options) var reference = Chart.StyleManager.Style?.ChartArea.BorderReference; + var chartBorder = GetChartAreaDefaultColor((int)styleType, out ExcelThemeLine themedLine); + item.Rectangle.SetDrawingBorderPropertiesNew( Theme, reference?.Color, Chart.Border, 1d, Chart.Border.Fill.Style != eFillStyle.NoFill, - () => GetChartAreaDefaultColor((int)styleType, out ExcelThemeLine themedLine)); + () => item.GetDefaultBorderColor()); item.Rectangle.RoundedCornerRadius = Chart.RoundedCorners ? 9 : 0; item.AppendRenderItems(RenderItems); From 168b9366be84c11ec43499d5b2b7f09525364a9e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jan=20K=C3=A4llman?= Date: Mon, 24 Aug 2026 14:05:16 +0200 Subject: [PATCH 29/30] Fixed failing tests --- src/EPPlus.Compression/AssemblyInfo.cs | 4 +- src/EPPlus.DrawingRenderer.Tests/TestBase.cs | 1 + src/EPPlus.Export.Pdf.Tests/PdfTests.cs | 2 + src/EPPlus/Drawing/Renderer/ChartRenderer.cs | 2 +- src/EPPlus/ExcelWorksheet.cs | 146 +++++++++---------- src/EPPlusTest/Drawing/ThemeTest.cs | 4 +- src/EPPlusTest/TestBase.cs | 1 + 7 files changed, 79 insertions(+), 81 deletions(-) diff --git a/src/EPPlus.Compression/AssemblyInfo.cs b/src/EPPlus.Compression/AssemblyInfo.cs index 40174a912a..cbb79fc75f 100644 --- a/src/EPPlus.Compression/AssemblyInfo.cs +++ b/src/EPPlus.Compression/AssemblyInfo.cs @@ -1,3 +1,5 @@ using System.Runtime.CompilerServices; +using System.Security; -[assembly: InternalsVisibleTo("EPPlus, PublicKey=00240000048000009400000006020000002400005253413100040000010001002981343969ed86fe604c56a84c61e33109424ef07bb458ff12e9533c11ea23ac8ef7e014b2a2de4ceb5f7528f963c755fe9b32f09cc35d21de94319d2a952a6e663cd46d6d98465998c77b52093d4f17cdc20ec054751244696f08afa6f4417d85267b147b73b6a3f5e9015b9dfd3dcc3328ce63df53a7c08a5544c1526ea5a5")] \ No newline at end of file +[assembly: InternalsVisibleTo("EPPlus, PublicKey=00240000048000009400000006020000002400005253413100040000010001002981343969ed86fe604c56a84c61e33109424ef07bb458ff12e9533c11ea23ac8ef7e014b2a2de4ceb5f7528f963c755fe9b32f09cc35d21de94319d2a952a6e663cd46d6d98465998c77b52093d4f17cdc20ec054751244696f08afa6f4417d85267b147b73b6a3f5e9015b9dfd3dcc3328ce63df53a7c08a5544c1526ea5a5")] +[assembly: AllowPartiallyTrustedCallers] \ No newline at end of file diff --git a/src/EPPlus.DrawingRenderer.Tests/TestBase.cs b/src/EPPlus.DrawingRenderer.Tests/TestBase.cs index 227fafa276..6c4f4dff6f 100644 --- a/src/EPPlus.DrawingRenderer.Tests/TestBase.cs +++ b/src/EPPlus.DrawingRenderer.Tests/TestBase.cs @@ -219,6 +219,7 @@ static void CreateWorksheetPathIfNotExists() } protected static void CreatePathIfNotExists(string path) { + if (!path.StartsWith(_worksheetPath)) path = Path.Combine(_worksheetPath, path); if (!Directory.Exists(path)) { Directory.CreateDirectory(path); diff --git a/src/EPPlus.Export.Pdf.Tests/PdfTests.cs b/src/EPPlus.Export.Pdf.Tests/PdfTests.cs index 8ab741f0c7..ae78dbaa77 100644 --- a/src/EPPlus.Export.Pdf.Tests/PdfTests.cs +++ b/src/EPPlus.Export.Pdf.Tests/PdfTests.cs @@ -634,6 +634,8 @@ public void EPPlusToPdf() ws.PrinterSettings.RightMargin = 0.1d; ws.PrinterSettings.HorizontalCentered = true; ws.PrinterSettings.VerticalCentered = true; + CreatePathIfNotExists(_pdfPath); + p.Workbook.SaveAsPdf(_pdfPath + "Snake.Pdf"); p.SaveAs(_pdfPath + "Snake.xlsx"); } diff --git a/src/EPPlus/Drawing/Renderer/ChartRenderer.cs b/src/EPPlus/Drawing/Renderer/ChartRenderer.cs index fb2d9e2197..d3c256a85d 100644 --- a/src/EPPlus/Drawing/Renderer/ChartRenderer.cs +++ b/src/EPPlus/Drawing/Renderer/ChartRenderer.cs @@ -33,7 +33,7 @@ Date Author Change using System.Runtime.InteropServices; using System.Security.Cryptography.Xml; using System.Text; -using d=OfficeOpenXml.Drawing.Renderer; +using d = OfficeOpenXml.Drawing.Renderer; using tc = OfficeOpenXml.Utils.TypeConversion; namespace EPPlusImageRenderer { diff --git a/src/EPPlus/ExcelWorksheet.cs b/src/EPPlus/ExcelWorksheet.cs index ad51a2080f..ca5d16f08b 100644 --- a/src/EPPlus/ExcelWorksheet.cs +++ b/src/EPPlus/ExcelWorksheet.cs @@ -3007,7 +3007,35 @@ public ExcelRangeBase DimensionByVisibility { get { - return GetDimension(true); + CheckSheetTypeAndNotDisposed(); + if (_values.GetDimension(out int fromRow, out int fromCol, out int toRow, out int toCol)) + { + var fc = fromCol; + var tc = toCol; + // ---- Extend column range by visible styling (visibility mode only) ---- + // Scan the whole used column span and pull fromCol/toCol outward to + // include any column that has a visible style somewhere in the row range. + for (int c = fc; c <= tc; c++) + { + //if (c >= fromCol && c <= toCol) + //{ + // continue; // already inside the range + //} + for (int r = fromRow; r <= toRow; r++) + { + if (HasValueOrVisibleStyle(r, c)) + { + if (c < fromCol) fromCol = c; + if (c > toCol) toCol = c; + break; // this column qualifies; move to the next column + } + } + } + + return Cells[System.Math.Min(fromRow, toRow), System.Math.Min(fromCol, toCol), System.Math.Max(fromRow, toRow), System.Math.Max(fromCol, toCol)]; + } + return null; + } } @@ -3020,99 +3048,61 @@ public ExcelRangeBase DimensionByValue { get { - return GetDimension(false); - } - } - - - private ExcelRangeBase GetDimension(bool byVisibility) - { - CheckSheetTypeAndNotDisposed(); - if (_values.GetDimension(out int fr, out int fc, out int tr, out int tc)) - { - var fvc = Cells[fr, fc]; - var lvc = Cells[tr, tc]; - // Row range comes from values only — styling never extends the height. - var fromRow = fvc._fromRow; - var toRow = lvc._toRow; - - // For a single value cell, the value-based dimension is just that cell, - // but visible styling on other columns within the same row may still - // extend the column range, so we keep going rather than early-returning - // when byVisibility is requested. - if (byVisibility == false && fvc.Address == lvc.Address) + //return GetDimension(false); + CheckSheetTypeAndNotDisposed(); + if (_values.GetDimension(out int fr, out int fc, out int tr, out int tc)) { - return Cells[fvc.Address]; - } - - int fromCol, toCol; + var fvc = FirstValueCell; + var lvc = LastValueCell; + if (fvc.Address == lvc.Address) return Cells[fvc.Address]; + var fromRow = fvc._fromRow; + var toRow = lvc._toRow; + int fromCol, toCol; - // ---- Leftmost column ---- - if (fvc._fromCol == fc) - { - fromCol = fvc._fromCol; - } - else - { - int r = fromRow, c = fc; - while (_values.NextCellByColumn(ref r, ref c, fromRow, toRow, _values.ColumnCount - 1)) + if (fvc._fromCol == fc) { - if (_values.GetValue(r, c)._value != null) - { - break; - } - r++; + fromCol = fvc._fromCol; } - fromCol = c; - } - - // ---- Rightmost column ---- - if (lvc._toCol == tc) - { - toCol = lvc._toCol; - } - else - { - int r = toRow, c = tc; - while (_values.PrevCellByColumn(ref r, ref c, fromRow, toRow, _values.ColumnCount - 1)) + else { - if (_values.GetValue(r, c)._value != null) + int r = fromRow, c = fc; + while (_values.NextCellByColumn(ref r, ref c, fromRow, toRow, _values.ColumnCount - 1)) { - break; + if (_values.GetValue(r, c)._value != null) + { + break; + } + r++; } - r--; + fromCol = c; } - toCol = c; - } - // ---- Extend column range by visible styling (visibility mode only) ---- - // Scan the whole used column span and pull fromCol/toCol outward to - // include any column that has a visible style somewhere in the row range. - if (byVisibility) - { - for (int c = fc; c <= tc; c++) + if (lvc._toCol == tc) { - //if (c >= fromCol && c <= toCol) - //{ - // continue; // already inside the range - //} - for (int r = fromRow; r <= toRow; r++) + toCol = lvc._toCol; + } + else + { + int r = toRow, c = tc; + while (_values.PrevCellByColumn(ref r, ref c, fromRow, toRow, _values.ColumnCount - 1)) { - if (HasVisibleStyle(r, c)) + if (_values.GetValue(r, c)._value != null) { - if (c < fromCol) fromCol = c; - if (c > toCol) toCol = c; - break; // this column qualifies; move to the next column + break; } + r--; } + toCol = c; } - } - return Cells[System.Math.Min(fromRow, toRow), System.Math.Min(fromCol, toCol), System.Math.Max(fromRow, toRow), System.Math.Max(fromCol, toCol)]; + return Cells[Math.Min(fromRow, toRow), Math.Min(fromCol, toCol), Math.Max(fromRow, toRow), Math.Max(fromCol, toCol)]; + } + return null; } - return null; } + + /// /// Returns true if the cell at (row, col) has a style that is visible on an /// empty cell: a fill pattern other than None, or a border edge other than @@ -3120,10 +3110,12 @@ private ExcelRangeBase GetDimension(bool byVisibility) /// The style id is resolved through the cell -> row -> column inheritance /// chain so that fills/borders applied to a whole column or row are detected. /// - private bool HasVisibleStyle(int row, int col) + private bool HasValueOrVisibleStyle(int row, int col) { // Resolve the effective style id, following cell -> row -> column. - int styleId = Workbook.Styles.GetStyleId(this, row, col); + var ev = _values.GetValue(row, col); + if (ev._value != null) return true; + int styleId = ev._styleId; if (styleId <= 0) { return false; // 0 == the default style, which is not visible diff --git a/src/EPPlusTest/Drawing/ThemeTest.cs b/src/EPPlusTest/Drawing/ThemeTest.cs index 58424656ad..4d62b55fbb 100644 --- a/src/EPPlusTest/Drawing/ThemeTest.cs +++ b/src/EPPlusTest/Drawing/ThemeTest.cs @@ -394,7 +394,7 @@ public void Shade85percent() var color1 = Color.FromArgb(0, 255, 0); var c = tc.ColorConverter.ApplyTintDrawing(color1, -0.85); - Assert.AreEqual((double)Color.FromArgb(0x0, 0xBC, 0x0).ToArgb(), c.ToArgb()); + Assert.AreEqual((double)Color.FromArgb(0x0, 0x6C, 0x0).ToArgb(), c.ToArgb()); } [TestMethod] public void Accent15Dark() @@ -477,7 +477,7 @@ public void ColorTransformMulti() var lmod2 = tc.ColorConverter.ApplyLumMod(smod1, 1.02); var tint = tc.ColorConverter.ApplyTintDrawing(lmod2, 1-0.94); - var expected = ColorTranslator.FromHtml("#475A67"); + var expected = ColorTranslator.FromHtml("#475A68"); Assert.AreEqual(expected.ToArgb(), tint.ToArgb()); //Assert.AreEqual(Color.FromArgb(255, 145, 14, 127).ToArgb(), myColorDarkened.ToArgb()); diff --git a/src/EPPlusTest/TestBase.cs b/src/EPPlusTest/TestBase.cs index 3e36ffd660..6496ca8bc2 100644 --- a/src/EPPlusTest/TestBase.cs +++ b/src/EPPlusTest/TestBase.cs @@ -251,6 +251,7 @@ protected static void SaveWorkbook(string name, ExcelPackage pck) { fi.Delete(); } + pck.SaveAs(fi); } protected static readonly DateTime _loadDataStartDate = new DateTime(2022, 11, 1); /// From 55725e6b60a501136393cef7bb1cc55d1c5111e3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ossian=20Edstr=C3=B6m?= Date: Mon, 24 Aug 2026 14:37:04 +0200 Subject: [PATCH 30/30] Fixed edge-case node-exists but is empty --- .../Renderer/Chart/ChartAreaRenderer.cs | 8 +++++--- .../ChartElementStyleTables.cs | 18 +++++++++++------- 2 files changed, 16 insertions(+), 10 deletions(-) diff --git a/src/EPPlus/Drawing/Renderer/Chart/ChartAreaRenderer.cs b/src/EPPlus/Drawing/Renderer/Chart/ChartAreaRenderer.cs index f4bab0d977..4168999ec0 100644 --- a/src/EPPlus/Drawing/Renderer/Chart/ChartAreaRenderer.cs +++ b/src/EPPlus/Drawing/Renderer/Chart/ChartAreaRenderer.cs @@ -51,9 +51,11 @@ public override void AppendRenderItems(List renderItems) internal override Color? GetDefaultBorderColor() { - //Kept here in case needed in future for effect etc. - var themedLine = GetThemedLine(ChartElement.ChartArea, (int)Chart.Style, out Color? lineCol); - return lineCol; + //We only get here if the node is null or empty + var themedLine = GetThemedLine(ChartElement.ChartArea, (int)Chart.Style, Chart.Border.Fill != null && Chart.Border.Fill.IsEmpty, out Color? lineColor); + ////Kept here in case needed in future for effect etc. + //var themedLine = GetThemedLine(ChartElement.ChartArea, (int)Chart.Style, out Color? lineCol); + return lineColor; } internal override Color? GetDefaultFillColor() diff --git a/src/EPPlus/Drawing/Renderer/Chart/ChartStyleDefaults/ChartElementStyleTables.cs b/src/EPPlus/Drawing/Renderer/Chart/ChartStyleDefaults/ChartElementStyleTables.cs index 39835b0c12..24bf5c9866 100644 --- a/src/EPPlus/Drawing/Renderer/Chart/ChartStyleDefaults/ChartElementStyleTables.cs +++ b/src/EPPlus/Drawing/Renderer/Chart/ChartStyleDefaults/ChartElementStyleTables.cs @@ -3,13 +3,8 @@ using OfficeOpenXml.Drawing.Chart; using OfficeOpenXml.Drawing.Theme; using OfficeOpenXml.Encryption; -using OfficeOpenXml.FormulaParsing.Excel.Functions.MathFunctions; using System; -using System.Collections.Generic; using System.Drawing; -using System.Linq; -using System.Text; -using System.Xml.Linq; using tc = OfficeOpenXml.Utils.TypeConversion; namespace OfficeOpenXml.Drawing.Renderer.Chart.ChartElementStyleTables @@ -99,7 +94,7 @@ private Color GetThemeColorTint(eThemeSchemeColor themeColor, double tint = 0.0d /// The line color with fill styles etc applied /// /// - protected ExcelThemeLine GetThemedLine(ChartElement element, int ChartStyleId, out Color? lineColor) + protected ExcelThemeLine GetThemedLine(ChartElement element, int ChartStyleId, bool nodeIsEmpty , out Color? lineColor) { //Chart style can only be above 48 if it is Style102 which in this case should be equivalent with style2 //Alternatively it's an unkown or unset style which should also default to style2 @@ -108,9 +103,18 @@ protected ExcelThemeLine GetThemedLine(ChartElement element, int ChartStyleId, o var AreaOrFloor = (ChartElement.ChartArea | ChartElement.Floor); if (AreaOrFloor.HasFlag(element)) { - lineColor = GetDefaultBorderColorForElement(element, styleId); var themedLine = ChartRenderer.Theme.FormatScheme.BorderStyle[0]; + //When the node exists but is empty Excel does not apply default styles + //It directly applies the themedLineColor + if (nodeIsEmpty) + { + lineColor = themedLine.Fill.Color; + return themedLine; + } + + lineColor = GetDefaultBorderColorForElement(element, styleId); + var themeColor = tc.ColorConverter.GetThemeColor(ChartRenderer.Theme, themedLine.Fill.SolidFill.Color); if (themedLine.HasFill == false) {