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