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/73] #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/73] 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/73] 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/73] 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/73] 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/73] 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/73] 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/73] 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/73] 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/73] 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/73] 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/73] 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/73] 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/73] 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/73] 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/73] #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/73] 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/73] #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/73] 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/73] 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/73] 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/73] 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/73] 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/73] 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/73] 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/73] 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/73] 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/73] 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/73] 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/73] 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) { From c1e12498a2b2e7616f69cbd37fec8048cd421a96 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ossian=20Edstr=C3=B6m?= Date: Mon, 24 Aug 2026 17:02:22 +0200 Subject: [PATCH 31/73] Condenced fallbackTests and made assertive --- .../Chart/ChartStyleFallbackTest.cs | 277 +++++++++--------- .../ChartElementStyleTables.cs | 11 +- 2 files changed, 153 insertions(+), 135 deletions(-) diff --git a/src/EPPlus.DrawingRenderer.Tests/Chart/ChartStyleFallbackTest.cs b/src/EPPlus.DrawingRenderer.Tests/Chart/ChartStyleFallbackTest.cs index 110a010eb7..efc76f9ab4 100644 --- a/src/EPPlus.DrawingRenderer.Tests/Chart/ChartStyleFallbackTest.cs +++ b/src/EPPlus.DrawingRenderer.Tests/Chart/ChartStyleFallbackTest.cs @@ -5,132 +5,142 @@ using System.Collections.Generic; using System.Drawing; using tc = OfficeOpenXml.Utils.TypeConversion; +using System.Globalization; namespace EPPlus.DrawingRenderer.Tests.Chart { [TestClass] public class ChartStyleFallbackTest : TestBase { - [TestMethod] - public void ReadExcelFile() + + [AssemblyInitialize] + public static async Task AssemblyInit(TestContext context) { ExcelPackage.License.SetNonCommercialOrganization("EPPlus Project"); - CreatePathIfNotExists("StyleExamples\\"); - using (var p = OpenTemplatePackage("StyleExamples\\ExcelUnchangedEmptyChart.xlsx")) + } + + private void CreateStyleExampleAndExportIt(string fileName, Func, bool> assertIfTestSuccessful) + { + bool testSucceded = false; + + using (var p = OpenTemplatePackage($"StyleExamples\\{fileName}.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; + List outputSvgs = new List(); - var svg = c.ToSvg(); - SaveTextFileToWorkbook($"svg\\ExcelDefault{ws.Name}_{c.Name}.svg", svg); + foreach (var d in ws.Drawings) + { + if (d is ExcelChart c) + { + var svg = c.ToSvg(); + outputSvgs.Add(svg); + SaveTextFileToWorkbook($"svg\\{fileName}_{ws.Name}_{c.Name}.svg", svg); + } } - var fi = GetOutputFile("StyleExamples", "ExcelUnchangedEmptyChart_out.xlsx"); + + testSucceded = assertIfTestSuccessful(outputSvgs); + + var fi = GetOutputFile("StyleExamples", $"{fileName}_Out.xlsx"); p.SaveAs(fi); } + Assert.IsTrue(testSucceded); } [TestMethod] - public void EpplusGeneratedChart() + public void ReadExcelFile() { - ExcelPackage.License.SetNonCommercialOrganization("EPPlus Project"); - - CreatePathIfNotExists("StyleExamples\\"); - - using (var p = OpenPackage("StyleExamples\\epplusDefaultTest.xlsx",true)) + CreateStyleExampleAndExportIt("ExcelUnchangedEmptyChart", (List outputSvgs) => { - var ws = p.Workbook.Worksheets.Add("EpplusGeneratedChart"); + //Create expected color + var col = Color.FromArgb(217, 217, 217); + var expectedStr = ColorTranslator.ToHtml(col).ToLower(); - //ws.Workbook.ThemeManager.GetOrCreateTheme(); + var svgSplitOnSpace = outputSvgs[0].Split(' '); + //Get the first stroke and extract the hexCode for the expected color + var firstStroke = svgSplitOnSpace.First(s => s.StartsWith("stroke")); + var colorResult = firstStroke.Substring(8, 7).ToLower(); - ws.Cells["A1:A3"].Formula = "ROW()+COLUMN()"; + //Get the resulting width + var strokeWidth = svgSplitOnSpace.First(s => s.StartsWith("stroke-width")); + var widthStr = strokeWidth.Substring(14, strokeWidth.Length - 14 -1).ToLower(); + var widthResult = double.Parse(widthStr, CultureInfo.InvariantCulture); - ws.Calculate(); + //Assert + Assert.AreEqual(expectedStr, colorResult); + Assert.AreEqual(1d, widthResult); - var emptyLines = ws.Drawings.AddLineChart("EmptyLineChart", eLineChartType.Line); - var generatedBar = ws.Drawings.AddBarChart("EpplusBarChart", eBarChartType.ColumnClustered); + return expectedStr == colorResult && 1d == widthResult; + }); + } - generatedBar.SetPosition(1, 1000); + [TestMethod] + public void ReadEmptyDefaultChartStyle() + { + CreateStyleExampleAndExportIt("emptyDefault", (List outputSvgs) => + { + //Create expected color + var col = Color.FromArgb(217, 217, 217); + var expectedStr = ColorTranslator.ToHtml(col).ToLower(); - var defaultRect = ws.Drawings.AddShape("MyDefaultShape", OfficeOpenXml.Drawing.eShapeStyle.Round1Rect); - var gradientRect = ws.Drawings.AddShape("GradRect", OfficeOpenXml.Drawing.eShapeStyle.Round1Rect); + var svgSplitOnSpace = outputSvgs[0].Split(' '); - defaultRect.SetPosition(300, 1); - gradientRect.SetPosition(300, 1000); + //Get the first stroke and extract the hexCode for the expected color + var firstStroke = svgSplitOnSpace.First(s => s.StartsWith("stroke")); + var colorResult = firstStroke.Substring(8, 7).ToLower(); - defaultRect.Fill.Style = OfficeOpenXml.Drawing.eFillStyle.SolidFill; - gradientRect.Fill.Style = OfficeOpenXml.Drawing.eFillStyle.GradientFill; - generatedBar.Series.Add(ws.Cells["A1:A3"]); + //Get the resulting width + var strokeWidth = svgSplitOnSpace.First(s => s.StartsWith("stroke-width")); + var widthStr = strokeWidth.Substring(14, strokeWidth.Length - 14 - 1).ToLower(); + var widthResult = double.Parse(widthStr, CultureInfo.InvariantCulture); - //foreach (ExcelChart c in ws.Drawings) - //{ - // var borderRef = c.StyleManager.Style.ChartArea.BorderReference; - // var borderSetting = c.Border; - // var borderDirectColor = borderSetting.Fill.Color; + //Assert + Assert.AreEqual(expectedStr, colorResult); + Assert.AreEqual(13.3333d, widthResult, 0.003); - // var svg = c.ToSvg(); - // SaveTextFileToWorkbook($"svg\\epplusDefault{ws.Name}_{c.Name}.svg", svg); - //} - //GetOutputFile("StyleExamples", ""); - SaveAndCleanup(p); - } + return expectedStr == colorResult && 13.3333d == Math.Round(widthResult,4); + }); } + [TestMethod] - public void ReadEmptyDefaultChartStyle() + public void ReadExcelEditedRemovedStyles() { - ExcelPackage.License.SetNonCommercialOrganization("EPPlus Project"); - - CreatePathIfNotExists("StyleExamples\\"); + string fileName = "emptyManuallyRemovedLnStyles"; - using (var p = OpenTemplatePackage("StyleExamples\\emptyDefault.xlsx")) + CreateStyleExampleAndExportIt(fileName, (List outputSvgs) => { - var ws = p.Workbook.Worksheets[0]; + //Create expected color + var col = Color.FromArgb(137, 137, 137); + var expectedStr = ColorTranslator.ToHtml(col).ToLower(); - foreach (ExcelChart c in ws.Drawings) - { - var borderRef = c.StyleManager.Style.ChartArea.BorderReference; - var borderSetting = c.Border; - var borderDirectColor = borderSetting.Fill.Color; + var svgSplitOnSpace = outputSvgs[0].Split(' '); - var svg = c.ToSvg(); - SaveTextFileToWorkbook($"svg\\emptyDefaultStyle{ws.Name}_{c.Name}.svg", svg); - } - var fi = GetOutputFile("StyleExamples", "emptyDefault_out.xlsx"); - p.SaveAs(fi); - } - } - [TestMethod] - public void GenerateSimpleChart() - { - ExcelPackage.License.SetNonCommercialOrganization("EPPlus Project"); + //Get the first stroke and extract the hexCode for the expected color + var firstStroke = svgSplitOnSpace.First(s => s.StartsWith("stroke")); + var colorResult = firstStroke.Substring(8, 7).ToLower(); - string fileName = "EpplusSimpleChart"; + //Get the resulting width + var strokeWidth = svgSplitOnSpace.First(s => s.StartsWith("stroke-width")); + var widthStr = strokeWidth.Substring(14, strokeWidth.Length - 14 - 1).ToLower(); + var widthResult = double.Parse(widthStr, CultureInfo.InvariantCulture); - using (var p = OpenPackage($"{fileName}.xlsx",true)) - { - var ws = p.Workbook.Worksheets.Add("s1"); - ws.Drawings.AddBarChart("simpleChart", eBarChartType.ColumnClustered); + //Assert + Assert.AreEqual(expectedStr, colorResult); + Assert.AreEqual(13.3333d, widthResult, 0.003); - SaveAndCleanup(p); - } + return expectedStr == colorResult && 13.3333d == Math.Round(widthResult, 4); + }); } [TestMethod] public void ReadChartBorderThemeTint() { - ExcelPackage.License.SetNonCommercialOrganization("EPPlus Project"); - var fileName = "ChartBorderThemeTint"; - CreatePathIfNotExists("StyleExamples\\"); using (var p = OpenTemplatePackage($"StyleExamples\\{fileName}.xlsx")) { @@ -152,44 +162,9 @@ public void ReadChartBorderThemeTint() } } - [TestMethod] - public void RemovedStyles() - { - ExcelPackage.License.SetNonCommercialOrganization("EPPlus Project"); - - string fileName = "emptyManuallyRemovedLnStyles"; - 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 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); - } - } - [TestMethod] public void EditedTheme() { - ExcelPackage.License.SetNonCommercialOrganization("EPPlus Project"); - - CreatePathIfNotExists("StyleExamples\\"); - string fileName = "ExcelThemeEdited"; using (var p = OpenTemplatePackage($"StyleExamples\\{fileName}.xlsx")) @@ -227,12 +202,8 @@ public void EditedTheme() [TestMethod] public void ManualSystemText() { - ExcelPackage.License.SetNonCommercialOrganization("EPPlus Project"); - string fileName = "ExcelThemeManualSystemText"; - CreatePathIfNotExists("StyleExamples\\"); - using (var p = OpenTemplatePackage($"StyleExamples\\{fileName}.xlsx")) { var ws = p.Workbook.Worksheets[0]; @@ -241,12 +212,6 @@ public void ManualSystemText() { 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); } @@ -274,12 +239,6 @@ public void ExcelThemeLnDeleted() { 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); } @@ -293,12 +252,8 @@ public void ExcelThemeLnDeleted() [TestMethod] public void PureExcelTheme() { - ExcelPackage.License.SetNonCommercialOrganization("EPPlus Project"); - string fileName = "PureExcelTheme"; - CreatePathIfNotExists("StyleExamples\\"); - using (var p = OpenTemplatePackage($"StyleExamples\\{fileName}.xlsx")) { var ws = p.Workbook.Worksheets[0]; @@ -326,12 +281,8 @@ public void PureExcelTheme() [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]; @@ -348,5 +299,63 @@ public void ChartWithChartStyle() p.SaveAs(fi); } } + + [TestMethod] + public void EpplusGeneratedChart() + { + using (var p = OpenPackage("StyleExamples\\epplusDefaultTest.xlsx", true)) + { + var ws = p.Workbook.Worksheets.Add("EpplusGeneratedChart"); + + //ws.Workbook.ThemeManager.GetOrCreateTheme(); + + + ws.Cells["A1:A3"].Formula = "ROW()+COLUMN()"; + + ws.Calculate(); + + var emptyLines = ws.Drawings.AddLineChart("EmptyLineChart", eLineChartType.Line); + var generatedBar = ws.Drawings.AddBarChart("EpplusBarChart", eBarChartType.ColumnClustered); + + generatedBar.SetPosition(1, 1000); + + var defaultRect = ws.Drawings.AddShape("MyDefaultShape", OfficeOpenXml.Drawing.eShapeStyle.Round1Rect); + var gradientRect = ws.Drawings.AddShape("GradRect", OfficeOpenXml.Drawing.eShapeStyle.Round1Rect); + + defaultRect.SetPosition(300, 1); + gradientRect.SetPosition(300, 1000); + + defaultRect.Fill.Style = OfficeOpenXml.Drawing.eFillStyle.SolidFill; + gradientRect.Fill.Style = OfficeOpenXml.Drawing.eFillStyle.GradientFill; + generatedBar.Series.Add(ws.Cells["A1:A3"]); + + //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\\epplusDefault{ws.Name}_{c.Name}.svg", svg); + //} + //GetOutputFile("StyleExamples", ""); + SaveAndCleanup(p); + } + } + + [TestMethod] + public void GenerateSimpleChart() + { + 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); + } + } + } } diff --git a/src/EPPlus/Drawing/Renderer/Chart/ChartStyleDefaults/ChartElementStyleTables.cs b/src/EPPlus/Drawing/Renderer/Chart/ChartStyleDefaults/ChartElementStyleTables.cs index 24bf5c9866..5cd5778a5e 100644 --- a/src/EPPlus/Drawing/Renderer/Chart/ChartStyleDefaults/ChartElementStyleTables.cs +++ b/src/EPPlus/Drawing/Renderer/Chart/ChartStyleDefaults/ChartElementStyleTables.cs @@ -105,11 +105,20 @@ protected ExcelThemeLine GetThemedLine(ChartElement element, int ChartStyleId, b { var themedLine = ChartRenderer.Theme.FormatScheme.BorderStyle[0]; + bool isSchemeColor = themedLine.Fill.SolidFill.Color.ColorType == eDrawingColorType.Scheme && themedLine.Fill.SolidFill.Color.SchemeColor.Color == eSchemeColor.Style; + //When the node exists but is empty Excel does not apply default styles //It directly applies the themedLineColor if (nodeIsEmpty) { - lineColor = themedLine.Fill.Color; + if(isSchemeColor) + { + lineColor = GetDefaultBorderColorForElement(element, styleId); + } + else + { + lineColor = themedLine.Fill.Color; + } return themedLine; } From eec7d57dde70b7e845b44636dcc42a7e11545867 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ossian=20Edstr=C3=B6m?= Date: Tue, 25 Aug 2026 09:50:29 +0200 Subject: [PATCH 32/73] Resolved bug For ExcelEdited --- .../Chart/ChartStyleFallbackTest.cs | 42 ++++++++----------- .../ChartElementStyleTables.cs | 9 ++++ 2 files changed, 27 insertions(+), 24 deletions(-) diff --git a/src/EPPlus.DrawingRenderer.Tests/Chart/ChartStyleFallbackTest.cs b/src/EPPlus.DrawingRenderer.Tests/Chart/ChartStyleFallbackTest.cs index efc76f9ab4..57f855075f 100644 --- a/src/EPPlus.DrawingRenderer.Tests/Chart/ChartStyleFallbackTest.cs +++ b/src/EPPlus.DrawingRenderer.Tests/Chart/ChartStyleFallbackTest.cs @@ -167,35 +167,29 @@ public void EditedTheme() { string fileName = "ExcelThemeEdited"; - using (var p = OpenTemplatePackage($"StyleExamples\\{fileName}.xlsx")) + CreateStyleExampleAndExportIt(fileName, (List outputSvgs) => { - var ws = p.Workbook.Worksheets[0]; + //Create expected color + var col = Color.FromArgb(255, 255, 199, 199); + var expectedStr = ColorTranslator.ToHtml(col).ToLower(); - 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 svgSplitOnSpace = outputSvgs[0].Split(' '); - //var defaultColorFromTheme = theme.ColorScheme.Dark1; + //Get the first stroke and extract the hexCode for the expected color + var firstStroke = svgSplitOnSpace.First(s => s.StartsWith("stroke")); + var colorResult = firstStroke.Substring(8, 7).ToLower(); - var svg = c.ToSvg(); - SaveTextFileToWorkbook($"svg\\{fileName}_{ws.Name}_{c.Name}.svg", svg); + //Get the resulting width + var strokeWidth = svgSplitOnSpace.First(s => s.StartsWith("stroke-width")); + var widthStr = strokeWidth.Substring(14, strokeWidth.Length - 14 - 1).ToLower(); + var widthResult = double.Parse(widthStr, CultureInfo.InvariantCulture); - 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.55d); - var ExpectedColor = Color.FromArgb(255, 255, 199, 199); - Assert.AreEqual(ExpectedColor.ToArgb(), themeColor.ToArgb()); - } - } - var fi = GetOutputFile("StyleExamples", $"{fileName}_Out.xlsx"); - p.SaveAs(fi); - } + //Assert + Assert.AreEqual(expectedStr, colorResult); + Assert.AreEqual(13.3333d, widthResult, 0.003); + + return expectedStr == colorResult && 13.3333d == Math.Round(widthResult, 4); + }); } diff --git a/src/EPPlus/Drawing/Renderer/Chart/ChartStyleDefaults/ChartElementStyleTables.cs b/src/EPPlus/Drawing/Renderer/Chart/ChartStyleDefaults/ChartElementStyleTables.cs index 5cd5778a5e..3cff27f2bf 100644 --- a/src/EPPlus/Drawing/Renderer/Chart/ChartStyleDefaults/ChartElementStyleTables.cs +++ b/src/EPPlus/Drawing/Renderer/Chart/ChartStyleDefaults/ChartElementStyleTables.cs @@ -114,6 +114,15 @@ protected ExcelThemeLine GetThemedLine(ChartElement element, int ChartStyleId, b if(isSchemeColor) { lineColor = GetDefaultBorderColorForElement(element, styleId); + + if (themedLine.Fill.SolidFill.Color.Transforms.Count > 0 && lineColor.HasValue) + { + //var schemeClr = tc.ColorConverter.GetSchemeColor(ChartRenderer.Theme, eSchemeColor.Dark1); + //var tint = GetSchemeColorTint(eSchemeColor.Dark1, 0.45d); + lineColor = tc.ColorConverter.ApplyTransforms(lineColor.Value, themedLine.Fill.SolidFill.Color.Transforms); + } + + return themedLine; } else { From b5e1d4c1e5a15340c06813742db3f87da18ff3db Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ossian=20Edstr=C3=B6m?= Date: Tue, 25 Aug 2026 10:32:27 +0200 Subject: [PATCH 33/73] Ensured handling transparency/none --- .../Chart/ChartStyleFallbackTest.cs | 65 ++++++++++--------- .../ChartElementStyleTables.cs | 13 +++- .../DrawingRenderItemExtentions.cs | 5 ++ 3 files changed, 50 insertions(+), 33 deletions(-) diff --git a/src/EPPlus.DrawingRenderer.Tests/Chart/ChartStyleFallbackTest.cs b/src/EPPlus.DrawingRenderer.Tests/Chart/ChartStyleFallbackTest.cs index 57f855075f..8367f58e81 100644 --- a/src/EPPlus.DrawingRenderer.Tests/Chart/ChartStyleFallbackTest.cs +++ b/src/EPPlus.DrawingRenderer.Tests/Chart/ChartStyleFallbackTest.cs @@ -198,48 +198,53 @@ public void ManualSystemText() { string fileName = "ExcelThemeManualSystemText"; - using (var p = OpenTemplatePackage($"StyleExamples\\{fileName}.xlsx")) + CreateStyleExampleAndExportIt(fileName, (List outputSvgs) => { - var ws = p.Workbook.Worksheets[0]; + //Create expected color + var col = Color.FromArgb(255, 0, 0, 0); + var expectedStr = ColorTranslator.ToHtml(col).ToLower(); - 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); - } + var svgSplitOnSpace = outputSvgs[0].Split(' '); + + //Get the first stroke and extract the hexCode for the expected color + var firstStroke = svgSplitOnSpace.First(s => s.StartsWith("stroke")); + var colorResult = firstStroke.Substring(8, 7).ToLower(); + + //Get the resulting width + var strokeWidth = svgSplitOnSpace.First(s => s.StartsWith("stroke-width")); + var widthStr = strokeWidth.Substring(14, strokeWidth.Length - 14 - 1).ToLower(); + var widthResult = double.Parse(widthStr, CultureInfo.InvariantCulture); + + //Assert + Assert.AreEqual(expectedStr, colorResult); + Assert.AreEqual(13.3333d, widthResult, 0.003); + + return expectedStr == colorResult && 13.3333d == Math.Round(widthResult, 4); + }); } [TestMethod] public void ExcelThemeLnDeleted() { - ExcelPackage.License.SetNonCommercialOrganization("EPPlus Project"); - - CreatePathIfNotExists("StyleExamples\\"); - string fileName = "ExcelThemeLnDeleted"; - using (var p = OpenTemplatePackage($"StyleExamples\\{fileName}.xlsx")) + CreateStyleExampleAndExportIt(fileName, (List outputSvgs) => { - var ws = p.Workbook.Worksheets[0]; + //Create expected color + var col = Color.Transparent; + var expectedStr = ColorTranslator.ToHtml(col).ToLower(); - 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); - } + var svgSplitOnSpace = outputSvgs[0].Split(' '); + + //Get the first stroke and extract the hexCode for the expected color + var firstStroke = svgSplitOnSpace.First(s => s.StartsWith("stroke")); + var colorResult = firstStroke.Substring(8, 4).ToLower(); + + Assert.AreEqual("none", colorResult); + + return "none" == colorResult; + }); } diff --git a/src/EPPlus/Drawing/Renderer/Chart/ChartStyleDefaults/ChartElementStyleTables.cs b/src/EPPlus/Drawing/Renderer/Chart/ChartStyleDefaults/ChartElementStyleTables.cs index 3cff27f2bf..86f2ecf7e0 100644 --- a/src/EPPlus/Drawing/Renderer/Chart/ChartStyleDefaults/ChartElementStyleTables.cs +++ b/src/EPPlus/Drawing/Renderer/Chart/ChartStyleDefaults/ChartElementStyleTables.cs @@ -105,13 +105,20 @@ protected ExcelThemeLine GetThemedLine(ChartElement element, int ChartStyleId, b { var themedLine = ChartRenderer.Theme.FormatScheme.BorderStyle[0]; - bool isSchemeColor = themedLine.Fill.SolidFill.Color.ColorType == eDrawingColorType.Scheme && themedLine.Fill.SolidFill.Color.SchemeColor.Color == eSchemeColor.Style; - //When the node exists but is empty Excel does not apply default styles //It directly applies the themedLineColor if (nodeIsEmpty) { - if(isSchemeColor) + //Is node empty inside the theme + if(themedLine.HasFill == false) + { + lineColor = Color.Transparent; + return themedLine; + } + + bool isSchemeColor = themedLine.Fill.SolidFill.Color.ColorType == eDrawingColorType.Scheme && themedLine.Fill.SolidFill.Color.SchemeColor.Color == eSchemeColor.Style; + + if (isSchemeColor) { lineColor = GetDefaultBorderColorForElement(element, styleId); diff --git a/src/EPPlus/Drawing/Renderer/RenderItems/DrawingRenderItemExtentions.cs b/src/EPPlus/Drawing/Renderer/RenderItems/DrawingRenderItemExtentions.cs index 82a6098d2f..58a9ecc598 100644 --- a/src/EPPlus/Drawing/Renderer/RenderItems/DrawingRenderItemExtentions.cs +++ b/src/EPPlus/Drawing/Renderer/RenderItems/DrawingRenderItemExtentions.cs @@ -155,6 +155,11 @@ private static string GetFallbackFill(ExcelTheme theme, ExcelDrawingFillBasic it //Move on to 3. Theme fc = GetFillColorFromTheme(theme, GetDefaultThemeColor); + if(fc.HasValue && fc.Value.ToArgb() == Color.Transparent.ToArgb()) + { + opacity = 0d; + return "none"; + } } } else From 961c2b80d0f9d68d13e8d07dbc38bb9af8790a8c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ossian=20Edstr=C3=B6m?= Date: Tue, 25 Aug 2026 11:08:57 +0200 Subject: [PATCH 34/73] Asserted additional tests. Fixed fillStyle=0 --- .../Chart/ChartStyleFallbackTest.cs | 158 +++++++++++------- .../Renderer/Chart/ChartAxisRenderer.cs | 13 +- .../ChartElementStyleTables.cs | 5 +- src/EPPlus/Drawing/Renderer/ChartRenderer.cs | 2 +- 4 files changed, 110 insertions(+), 68 deletions(-) diff --git a/src/EPPlus.DrawingRenderer.Tests/Chart/ChartStyleFallbackTest.cs b/src/EPPlus.DrawingRenderer.Tests/Chart/ChartStyleFallbackTest.cs index 8367f58e81..85b4677d09 100644 --- a/src/EPPlus.DrawingRenderer.Tests/Chart/ChartStyleFallbackTest.cs +++ b/src/EPPlus.DrawingRenderer.Tests/Chart/ChartStyleFallbackTest.cs @@ -137,31 +137,6 @@ public void ReadExcelEditedRemovedStyles() }); } - [TestMethod] - public void ReadChartBorderThemeTint() - { - 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); - - //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(); - - var fi = GetOutputFile("StyleExamples", $"{fileName}_Out.xlsx"); - p.SaveAs(fi); - } - } - [TestMethod] public void EditedTheme() { @@ -253,27 +228,30 @@ public void PureExcelTheme() { string fileName = "PureExcelTheme"; - using (var p = OpenTemplatePackage($"StyleExamples\\{fileName}.xlsx")) - { - var ws = p.Workbook.Worksheets[0]; + //Border and Fill for chartArea expected colors + List ExpectedColors = new List() { "#ffcaca", "#bbd7f9" }; - foreach (var d in ws.Drawings) + //Test un-edited excel theme with custom colors set in excel + CreateStyleExampleAndExportIt(fileName, (List outputSvgs) => + { + foreach(var svg in outputSvgs) { - if (d is ExcelChart c) - { - var borderSetting = c.Border; - var borderDirectColor = borderSetting.Fill.Color; - var theme = p.Workbook.ThemeManager.GetOrCreateTheme(); + var svgSplitOnSpace = svg.Split(' '); - var defaultColorFromTheme = theme.ColorScheme.Dark1; + //Get the first stroke and extract the hexCode for the expected color + var firstStroke = svgSplitOnSpace.First(s => s.StartsWith("stroke")); + var borderResult = firstStroke.Substring(8, 7).ToLower(); - var svg = c.ToSvg(); - SaveTextFileToWorkbook($"svg\\{fileName}_{ws.Name}_{c.Name}.svg", svg); - } + //Get the first stroke and extract the hexCode for the expected color + var firstFill = svgSplitOnSpace.First(s => s.StartsWith("fill")); + var fillResult = firstFill.Substring(6, 7).ToLower(); + + Assert.AreEqual(ExpectedColors[0], borderResult); + Assert.AreEqual(ExpectedColors[1], fillResult); } - var fi = GetOutputFile("StyleExamples", $"{fileName}_Out.xlsx"); - p.SaveAs(fi); - } + + return true; + }); } @@ -282,21 +260,79 @@ public void ChartWithChartStyle() { string fileName = "ChartWithChartStyleMEdit"; - using (var p = OpenTemplatePackage($"StyleExamples\\{fileName}.xlsx")) + //Read test for if chart style is applied appropriately + CreateStyleExampleAndExportIt(fileName, (List outputSvgs) => { - var ws = p.Workbook.Worksheets[0]; + //Create expected color + var col = Color.FromArgb(255, 217, 217, 217); + var expectedStr = ColorTranslator.ToHtml(col).ToLower(); - 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); - } + var svgSplitOnSpace = outputSvgs[0].Split(' '); + + //Get the first stroke and extract the hexCode for the expected color + var firstStroke = svgSplitOnSpace.First(s => s.StartsWith("stroke")); + var colorResult = firstStroke.Substring(8, 7).ToLower(); + + //Get the resulting width + var strokeWidth = svgSplitOnSpace.First(s => s.StartsWith("stroke-width")); + var widthStr = strokeWidth.Substring(14, strokeWidth.Length - 14 - 1).ToLower(); + var widthResult = double.Parse(widthStr, CultureInfo.InvariantCulture); + + //Assert + Assert.AreEqual(expectedStr, colorResult); + Assert.AreEqual(1d, widthResult, 0.003); + + return expectedStr == colorResult && 1d == Math.Round(widthResult, 1); + }); + } + + [TestMethod] + public void ReadChartBorderThemeTint() + { + var fileName = "ChartBorderThemeTint"; + + //Read test for if chart style is applied appropriately + CreateStyleExampleAndExportIt(fileName, (List outputSvgs) => + { + //Create expected color + var col = Color.FromArgb(255, 217, 217, 217); + var expectedStr = ColorTranslator.ToHtml(col).ToLower(); + + var svgSplitOnSpace = outputSvgs[0].Split(' '); + + //Get the first stroke and extract the hexCode for the expected color + var firstStroke = svgSplitOnSpace.First(s => s.StartsWith("stroke")); + var colorResult = firstStroke.Substring(8, 7).ToLower(); + + //Get the resulting width + var strokeWidth = svgSplitOnSpace.First(s => s.StartsWith("stroke-width")); + var widthStr = strokeWidth.Substring(14, strokeWidth.Length - 14 - 1).ToLower(); + var widthResult = double.Parse(widthStr, CultureInfo.InvariantCulture); + + //Assert + Assert.AreEqual(expectedStr, colorResult); + Assert.AreEqual(1d, widthResult, 0.003); + + return expectedStr == colorResult && 1d == Math.Round(widthResult, 1); + }); + //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); + + // //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(); + + // var fi = GetOutputFile("StyleExamples", $"{fileName}_Out.xlsx"); + // p.SaveAs(fi); + //} } [TestMethod] @@ -328,15 +364,11 @@ public void EpplusGeneratedChart() gradientRect.Fill.Style = OfficeOpenXml.Drawing.eFillStyle.GradientFill; generatedBar.Series.Add(ws.Cells["A1:A3"]); - //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\\epplusDefault{ws.Name}_{c.Name}.svg", svg); - //} + foreach (ExcelDrawing d in ws.Drawings) + { + var svg = d.ToSvg(); + SaveTextFileToWorkbook($"svg\\epplusDefault{ws.Name}_{d.Name}.svg", svg); + } //GetOutputFile("StyleExamples", ""); SaveAndCleanup(p); } diff --git a/src/EPPlus/Drawing/Renderer/Chart/ChartAxisRenderer.cs b/src/EPPlus/Drawing/Renderer/Chart/ChartAxisRenderer.cs index 9bd578f978..f076910016 100644 --- a/src/EPPlus/Drawing/Renderer/Chart/ChartAxisRenderer.cs +++ b/src/EPPlus/Drawing/Renderer/Chart/ChartAxisRenderer.cs @@ -688,13 +688,20 @@ private List AddTickmarks(double units, eTimeUnit? dateUnit, dou if (Axis.AxisType == eAxisType.Cat && IsDateAutoAxis==false) { min = 0; - if (Axis.CrossingAxis==null || Axis.CrossingAxis.CrossBetween == eCrossBetween.Between) + if(AxisValues != null) { - max = AxisValues.Count; + if (Axis.CrossingAxis == null || Axis.CrossingAxis.CrossBetween == eCrossBetween.Between) + { + max = AxisValues.Count; + } + else + { + max = AxisValues.Count - 1; + } } else { - max = AxisValues.Count - 1; + max = 0; } } else diff --git a/src/EPPlus/Drawing/Renderer/Chart/ChartStyleDefaults/ChartElementStyleTables.cs b/src/EPPlus/Drawing/Renderer/Chart/ChartStyleDefaults/ChartElementStyleTables.cs index 86f2ecf7e0..e24b00d1b7 100644 --- a/src/EPPlus/Drawing/Renderer/Chart/ChartStyleDefaults/ChartElementStyleTables.cs +++ b/src/EPPlus/Drawing/Renderer/Chart/ChartStyleDefaults/ChartElementStyleTables.cs @@ -63,7 +63,10 @@ private Color GetThemeColorTint(eThemeSchemeColor themeColor, double tint = 0.0d if (styleId == 0) { - return Color.Empty; + //Set to default instead for export + //Otherwise epplus generated get weird. + styleId = 2; + //return Color.Empty; } if (styleId <= 32) diff --git a/src/EPPlus/Drawing/Renderer/ChartRenderer.cs b/src/EPPlus/Drawing/Renderer/ChartRenderer.cs index 26b2fa0244..3f17804a52 100644 --- a/src/EPPlus/Drawing/Renderer/ChartRenderer.cs +++ b/src/EPPlus/Drawing/Renderer/ChartRenderer.cs @@ -379,7 +379,7 @@ private void SetChartArea(SvgRenderOptions options) var reference = Chart.StyleManager.Style?.ChartArea.BorderReference; - var chartBorder = GetChartAreaDefaultColor((int)styleType, out ExcelThemeLine themedLine); + //var chartBorder = GetChartAreaDefaultColor((int)styleType, out ExcelThemeLine themedLine); item.Rectangle.SetDrawingBorderPropertiesNew( Theme, From 1a16eaf3686cd88d894404871a6ff8630938a2d2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ossian=20Edstr=C3=B6m?= Date: Tue, 25 Aug 2026 17:34:38 +0200 Subject: [PATCH 35/73] Started adding object defaults --- .../Chart/ChartStyleFallbackTest.cs | 97 +++++++++++++++++++ src/EPPlus/Drawing/Renderer/ShapeRenderer.cs | 2 +- src/EPPlus/Drawing/Theme/ExcelThemeBase.cs | 4 + .../Drawing/Theme/ExcelThemeObjectDefaults.cs | 18 ++++ 4 files changed, 120 insertions(+), 1 deletion(-) create mode 100644 src/EPPlus/Drawing/Theme/ExcelThemeObjectDefaults.cs diff --git a/src/EPPlus.DrawingRenderer.Tests/Chart/ChartStyleFallbackTest.cs b/src/EPPlus.DrawingRenderer.Tests/Chart/ChartStyleFallbackTest.cs index 85b4677d09..c91e104192 100644 --- a/src/EPPlus.DrawingRenderer.Tests/Chart/ChartStyleFallbackTest.cs +++ b/src/EPPlus.DrawingRenderer.Tests/Chart/ChartStyleFallbackTest.cs @@ -335,6 +335,100 @@ public void ReadChartBorderThemeTint() //} } + + [TestMethod] + public void Epp_Gen_DefaultLine() + { + using (var p = OpenPackage("StyleExamples\\epplusDefaultChart_Line.xlsx", true)) + { + var ws = p.Workbook.Worksheets.Add("Empty"); + + ws.Cells["A1:A3"].Formula = "ROW()+COLUMN()"; + + ws.Calculate(); + + var emptyLines = ws.Drawings.AddLineChart("Chart", eLineChartType.Line); + foreach (ExcelDrawing d in ws.Drawings) + { + var svg = d.ToSvg(); + + SaveTextFileToWorkbook($"svg\\epplusDefaultChart_Line{ws.Name}_{d.Name}.svg", svg); + + //Create expected color + var fill = Color.FromArgb(255, 255, 255, 255); + var expectedFill = ColorTranslator.ToHtml(fill).ToLower(); + var col = Color.FromArgb(255, 137, 137, 137); + var expectedStroke = ColorTranslator.ToHtml(col).ToLower(); + + var svgSplitOnSpace = svg.Split(' '); + + //Get the first stroke and extract the hexCode for the expected color + var firstFill = svgSplitOnSpace.First(s => s.StartsWith("fill")); + var fillResult = firstFill.Substring(6, 7).ToLower(); + + //Get the first stroke and extract the hexCode for the expected color + var firstStroke = svgSplitOnSpace.First(s => s.StartsWith("stroke")); + var strokeResult = firstStroke.Substring(8, 7).ToLower(); + + //Get the resulting width + var strokeWidth = svgSplitOnSpace.First(s => s.StartsWith("stroke-width")); + var widthStr = strokeWidth.Substring(14, strokeWidth.Length - 14 - 1).ToLower(); + var widthResult = double.Parse(widthStr, CultureInfo.InvariantCulture); + + //Assert + Assert.AreEqual(expectedFill, fillResult); + Assert.AreEqual(expectedStroke, strokeResult); + Assert.AreEqual(1d, widthResult, 0.003); + } + } + } + + [TestMethod] + public void Epp_Gen_DefaultShape() + { + string fileName = "epplusShape"; + + using (var p = OpenPackage($"StyleExamples\\{fileName}.xlsx", true)) + { + var ws = p.Workbook.Worksheets.Add("ws1"); + + var defaultRect = ws.Drawings.AddShape("Default", OfficeOpenXml.Drawing.eShapeStyle.Round1Rect); + var gradientRect = ws.Drawings.AddShape("GradRect", OfficeOpenXml.Drawing.eShapeStyle.Round1Rect); + + defaultRect.SetPosition(300, 1); + gradientRect.SetPosition(300, 1000); + + var svgDefault = defaultRect.ToSvg(); + SaveTextFileToWorkbook($"svg\\{fileName}_{ws.Name}_{defaultRect.Name}.svg", svgDefault); + + //Create expected color + var fill = Color.FromArgb(255, 21, 96, 130); + var expectedFill = ColorTranslator.ToHtml(fill).ToLower(); + var col = Color.FromArgb(255, 4, 36, 51); + var expectedStroke = ColorTranslator.ToHtml(col).ToLower(); + + var svgSplitOnSpace = svgDefault.Split(' '); + + //Get the first fill and extract the hexCode for the expected color + var firstFill = svgSplitOnSpace.First(s => s.StartsWith("fill")); + var fillResult = firstFill.Substring(6, 7).ToLower(); + + //Get the first stroke and extract the hexCode for the expected color + var firstStroke = svgSplitOnSpace.First(s => s.StartsWith("stroke")); + var strokeResult = firstStroke.Substring(8, 7).ToLower(); + + //Get the resulting width + var strokeWidth = svgSplitOnSpace.First(s => s.StartsWith("stroke-width")); + var widthStr = strokeWidth.Substring(14, strokeWidth.Length - 14 - 1).ToLower(); + var widthResult = double.Parse(widthStr, CultureInfo.InvariantCulture); + + //Assert + Assert.AreEqual(expectedFill, fillResult); + Assert.AreEqual(expectedStroke, strokeResult); + Assert.AreEqual(1d, widthResult, 0.003); + } + } + [TestMethod] public void EpplusGeneratedChart() { @@ -364,9 +458,12 @@ public void EpplusGeneratedChart() gradientRect.Fill.Style = OfficeOpenXml.Drawing.eFillStyle.GradientFill; generatedBar.Series.Add(ws.Cells["A1:A3"]); + List outputSvgs = new List(); + foreach (ExcelDrawing d in ws.Drawings) { var svg = d.ToSvg(); + outputSvgs.Add(svg); SaveTextFileToWorkbook($"svg\\epplusDefault{ws.Name}_{d.Name}.svg", svg); } //GetOutputFile("StyleExamples", ""); diff --git a/src/EPPlus/Drawing/Renderer/ShapeRenderer.cs b/src/EPPlus/Drawing/Renderer/ShapeRenderer.cs index e00d01666c..0a964ba7f8 100644 --- a/src/EPPlus/Drawing/Renderer/ShapeRenderer.cs +++ b/src/EPPlus/Drawing/Renderer/ShapeRenderer.cs @@ -182,7 +182,7 @@ protected RenderItem AddFromPaths(BoundingBox parent, DrawingPath path, bool dra var shape = (ExcelShape)Drawing; if (drawFill) { - pi.FillColorSource = path.Fill; + pi.FillColorSource = path.Fill; pi.SetDrawingPropertiesFill(Theme, shape.Fill, shape.ThemeStyles.FillReference.Color); } else diff --git a/src/EPPlus/Drawing/Theme/ExcelThemeBase.cs b/src/EPPlus/Drawing/Theme/ExcelThemeBase.cs index 9d16bbd2dd..f8872f0ff0 100644 --- a/src/EPPlus/Drawing/Theme/ExcelThemeBase.cs +++ b/src/EPPlus/Drawing/Theme/ExcelThemeBase.cs @@ -27,6 +27,8 @@ public class ExcelThemeBase : XmlHelper, IPictureRelationDocument readonly string _colorSchemePath = "{0}a:clrScheme"; readonly string _fontSchemePath = "{0}a:fontScheme"; readonly string _fmtSchemePath = "{0}a:fmtScheme"; + readonly string _objectDefaultsPath = "{0}a:ObjectDefaults"; + readonly ExcelPackage _pck; Dictionary _hashes=new Dictionary(); internal ExcelThemeBase(ExcelPackage package, XmlNamespaceManager nsm, ZipPackageRelationship rel, string path) @@ -42,6 +44,8 @@ internal ExcelThemeBase(ExcelPackage package, XmlNamespaceManager nsm, ZipPackag _colorSchemePath = string.Format(_colorSchemePath, path); _fontSchemePath = string.Format(_fontSchemePath, path); _fmtSchemePath = string.Format(_fmtSchemePath, path); + //ObjectDefaults is part of the Theme node rather than themeElements + _objectDefaultsPath = string.Format(_objectDefaultsPath, ""); _pck = package; if (!NameSpaceManager.HasNamespace("a")) NameSpaceManager.AddNamespace("a", ExcelPackage.schemaDrawings); } diff --git a/src/EPPlus/Drawing/Theme/ExcelThemeObjectDefaults.cs b/src/EPPlus/Drawing/Theme/ExcelThemeObjectDefaults.cs new file mode 100644 index 0000000000..39e61f61bb --- /dev/null +++ b/src/EPPlus/Drawing/Theme/ExcelThemeObjectDefaults.cs @@ -0,0 +1,18 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Xml; + +namespace OfficeOpenXml.Drawing.Theme +{ + internal class ExcelThemeObjectDefaults : XmlHelper + { + private readonly ExcelThemeBase _theme; + + public ExcelThemeObjectDefaults(XmlNamespaceManager nameSpaceManager, XmlNode topNode, ExcelThemeBase theme) : base(nameSpaceManager, topNode) + { + _theme = theme; + } + } +} From 3a04a3a8d9ae7c01de4cd0b07c15bcca05480630 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ossian=20Edstr=C3=B6m?= Date: Tue, 25 Aug 2026 17:37:07 +0200 Subject: [PATCH 36/73] added missing file --- src/EPPlus/Drawing/Theme/ExcelThemeBase.cs | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/src/EPPlus/Drawing/Theme/ExcelThemeBase.cs b/src/EPPlus/Drawing/Theme/ExcelThemeBase.cs index f8872f0ff0..e3e4661429 100644 --- a/src/EPPlus/Drawing/Theme/ExcelThemeBase.cs +++ b/src/EPPlus/Drawing/Theme/ExcelThemeBase.cs @@ -102,6 +102,23 @@ public ExcelFormatScheme FormatScheme } } + ExcelThemeObjectDefaults _objectDefaults; + + /// + /// + /// + internal ExcelThemeObjectDefaults ObjectDefaults + { + get + { + if (_objectDefaults == null) + { + _objectDefaults = new ExcelThemeObjectDefaults(NameSpaceManager, TopNode.SelectSingleNode(_objectDefaultsPath, NameSpaceManager), this); + } + return _objectDefaults; + } + } + ExcelPackage IPictureRelationDocument.Package { get => _pck; } Dictionary IPictureRelationDocument.Hashes { get => _hashes; } From 75cc3421f4919ff7a86ae37c71b1715aca43bee1 Mon Sep 17 00:00:00 2001 From: Mats Alm <897655+swmal@users.noreply.github.com> Date: Wed, 26 Aug 2026 08:00:38 +0200 Subject: [PATCH 37/73] #2479 - Fix header/footer section parsing when &L is not first (#2482) * #2479 - Fix header/footer section parsing when &L is not first * #2479 - added test to replicate -2 index error --- .../FontScanning/NameTableSubfamilyTests.cs | 219 +++++++++++ src/EPPlus/ExcelHeaderFooter.cs | 6 +- .../HeaderFooterSectionOrderTests.cs | 367 ++++++++++++++++++ 3 files changed, 589 insertions(+), 3 deletions(-) create mode 100644 src/EPPlus.Fonts.OpenType.Tests/FontScanning/NameTableSubfamilyTests.cs create mode 100644 src/EPPlusTest/Core/Worksheet/HeaderFooterSectionOrderTests.cs diff --git a/src/EPPlus.Fonts.OpenType.Tests/FontScanning/NameTableSubfamilyTests.cs b/src/EPPlus.Fonts.OpenType.Tests/FontScanning/NameTableSubfamilyTests.cs new file mode 100644 index 0000000000..3f4d4eab8a --- /dev/null +++ b/src/EPPlus.Fonts.OpenType.Tests/FontScanning/NameTableSubfamilyTests.cs @@ -0,0 +1,219 @@ +/************************************************************************************************* + 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/25/2026 EPPlus Software AB Initial tests for NameTable subfamily mapping + *************************************************************************************************/ +using EPPlus.Fonts.OpenType.Tables.Name; +using OfficeOpenXml.Interfaces.Fonts; + +namespace EPPlus.Fonts.OpenType.Tests.FontScanning +{ + /// + /// Low-level unit tests for — bare table instances, + /// no font file loading. These tests exercise the string-to-enum heuristic directly, so they + /// stay fast and deterministic regardless of which fonts are installed on the test machine. + /// + /// Background: fonts whose subfamily name carries a weight beyond Bold (e.g. "Black", "Heavy", + /// "Demi" — as seen in real-world builds of "Arial Black") were previously mapped to + /// FontSubFamily.Bold. That made an exact match against a Regular request impossible, so a + /// query for "Arial Black" + Regular fell through to the fallback chain even though the font + /// was installed and found by FindBestMatch. See NameTable.GetSubfamilyEnum for the fix. + /// + [TestClass] + public class NameTableSubfamilyTests : FontTestBase + { + public override TestContext? TestContext { get; set; } + + #region Weight names beyond Bold — should map to Regular, not Bold + + [TestMethod] + public void GetSubfamilyEnum_Black_ReturnsRegular() + { + var nameTable = CreateNameTable(subfamilyName: "Black"); + + var result = nameTable.GetSubfamilyEnum(); + + Assert.AreEqual(FontSubFamily.Regular, result, + "'Black' is a separate typographic weight, not a Bold variant. The family " + + "name (e.g. 'Arial Black') already distinguishes it, so within the 4-value " + + "FontSubFamily enum it must be treated as Regular."); + } + + [TestMethod] + public void GetSubfamilyEnum_Heavy_ReturnsRegular() + { + var nameTable = CreateNameTable(subfamilyName: "Heavy"); + + var result = nameTable.GetSubfamilyEnum(); + + Assert.AreEqual(FontSubFamily.Regular, result); + } + + [TestMethod] + public void GetSubfamilyEnum_Demi_ReturnsRegular() + { + var nameTable = CreateNameTable(subfamilyName: "Demi"); + + var result = nameTable.GetSubfamilyEnum(); + + Assert.AreEqual(FontSubFamily.Regular, result); + } + + [TestMethod] + public void GetSubfamilyEnum_Light_ReturnsRegular() + { + var nameTable = CreateNameTable(subfamilyName: "Light"); + + var result = nameTable.GetSubfamilyEnum(); + + Assert.AreEqual(FontSubFamily.Regular, result); + } + + #endregion + + #region True RIBBI styles — must keep working (regression guard) + + [TestMethod] + public void GetSubfamilyEnum_Regular_ReturnsRegular() + { + var nameTable = CreateNameTable(subfamilyName: "Regular"); + + Assert.AreEqual(FontSubFamily.Regular, nameTable.GetSubfamilyEnum()); + } + + [TestMethod] + public void GetSubfamilyEnum_Bold_ReturnsBold() + { + var nameTable = CreateNameTable(subfamilyName: "Bold"); + + Assert.AreEqual(FontSubFamily.Bold, nameTable.GetSubfamilyEnum()); + } + + [TestMethod] + public void GetSubfamilyEnum_Italic_ReturnsItalic() + { + var nameTable = CreateNameTable(subfamilyName: "Italic"); + + Assert.AreEqual(FontSubFamily.Italic, nameTable.GetSubfamilyEnum()); + } + + [TestMethod] + public void GetSubfamilyEnum_BoldItalic_ReturnsBoldItalic() + { + var nameTable = CreateNameTable(subfamilyName: "Bold Italic"); + + Assert.AreEqual(FontSubFamily.BoldItalic, nameTable.GetSubfamilyEnum()); + } + + [TestMethod] + public void GetSubfamilyEnum_SemiBold_StillContainsBold_ReturnsBold() + { + // "Semibold" legitimately contains the substring "bold" and is a reasonable + // approximation of Bold within the 4-value enum — unlike "Black"/"Heavy"/"Demi", + // which don't contain "bold" at all. This must keep matching Bold. + var nameTable = CreateNameTable(subfamilyName: "SemiBold"); + + Assert.AreEqual(FontSubFamily.Bold, nameTable.GetSubfamilyEnum()); + } + + [TestMethod] + public void GetSubfamilyEnum_Oblique_ReturnsItalic() + { + var nameTable = CreateNameTable(subfamilyName: "Oblique"); + + Assert.AreEqual(FontSubFamily.Italic, nameTable.GetSubfamilyEnum()); + } + + #endregion + + #region Field priority — Typographic Subfamily (17) over legacy Subfamily (2) + + [TestMethod] + public void GetSubfamilyEnum_PrefersTypographicSubfamily17OverLegacySubfamily2() + { + // Real-world "Arial Black"-style layout: legacy subfamily (2) carries the extra + // weight name, while the typographic subfamily (17) correctly says "Regular". + // GetSubfamilyName() must prefer 17, so the enum should resolve to Regular even + // without the Black/Heavy/Demi fix above. + var nameTable = new NameTable + { + NameRecords = new[] + { + MakeRecord(NameRecordTypes.FontSubfamilyName, "Black"), + MakeRecord(NameRecordTypes.TypographicSubfamilyName, "Regular"), + } + }; + + Assert.AreEqual(FontSubFamily.Regular, nameTable.GetSubfamilyEnum()); + } + + #endregion + + #region Fallback to OS/2 fsSelection when name table has no usable subfamily + + [TestMethod] + public void GetSubfamilyEnum_NoNameRecords_FallsBackToFsSelectionBold() + { + const ushort fsSelectionBold = 0x0020; + var nameTable = new NameTable + { + NameRecords = new NameRecord[0], + Os2FsSelection = fsSelectionBold + }; + + Assert.AreEqual(FontSubFamily.Bold, nameTable.GetSubfamilyEnum()); + } + + [TestMethod] + public void GetSubfamilyEnum_NoNameRecords_FallsBackToFsSelectionRegular() + { + var nameTable = new NameTable + { + NameRecords = new NameRecord[0], + Os2FsSelection = 0 + }; + + Assert.AreEqual(FontSubFamily.Regular, nameTable.GetSubfamilyEnum()); + } + + #endregion + + #region Helpers + + /// + /// Builds a minimal bare NameTable with a single Font Subfamily Name (nameID 2) record — + /// enough to exercise GetSubfamilyEnum's string-matching heuristic in isolation. + /// + private static NameTable CreateNameTable(string subfamilyName) + { + return new NameTable + { + NameRecords = new[] + { + MakeRecord(NameRecordTypes.FontSubfamilyName, subfamilyName) + } + }; + } + + private static NameRecord MakeRecord(NameRecordTypes type, string name) + { + return new NameRecord + { + RecordType = type, + nameId = (ushort)type, + platformId = 3, + encodingId = 1, + Name = name + }; + } + + #endregion + } +} \ No newline at end of file diff --git a/src/EPPlus/ExcelHeaderFooter.cs b/src/EPPlus/ExcelHeaderFooter.cs index 8da37d5dae..401c9fe600 100644 --- a/src/EPPlus/ExcelHeaderFooter.cs +++ b/src/EPPlus/ExcelHeaderFooter.cs @@ -65,14 +65,14 @@ internal ExcelHeaderFooterText(XmlNode TextNode, ExcelWorksheet ws, string hf) string text = TextNode.InnerText; string code = text.Substring(0, 2); int startPos = 2; - for (int pos = startPos; pos < text.Length - 2; pos++) + for (int pos = startPos; pos < text.Length - 1; pos++) { string newCode = text.Substring(pos, 2); - if (newCode == "&C" || newCode == "&R") + if (newCode == "&L" || newCode == "&C" || newCode == "&R") { SetText(code, text.Substring(startPos, pos - startPos)); startPos = pos + 2; - pos = startPos; + pos = startPos - 1; code = newCode; } } diff --git a/src/EPPlusTest/Core/Worksheet/HeaderFooterSectionOrderTests.cs b/src/EPPlusTest/Core/Worksheet/HeaderFooterSectionOrderTests.cs new file mode 100644 index 0000000000..58be5d0ae2 --- /dev/null +++ b/src/EPPlusTest/Core/Worksheet/HeaderFooterSectionOrderTests.cs @@ -0,0 +1,367 @@ +/******************************************************************************* + * You may amend and distribute as you like, but don't remove this header! + * + * Required Notice: Copyright (C) EPPlus Software AB. + * https://epplussoftware.com + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. + * See the GNU Lesser General Public License for more details. + * + * The GNU Lesser General Public License can be viewed at http://www.opensource.org/licenses/lgpl-license.php + * If you unfamiliar with this license or have questions about it, here is an http://www.gnu.org/licenses/gpl-faq.html + * + * All code and executables are provided "" as is "" with no warranty either express or implied. + * The author accepts no liability for any damage or loss of business that this product may cause. + * + * Code change notes: + * + Date Author Change + ******************************************************************************* + 08/25/2026 EPPlus Software AB Regression tests: header/footer sections + must parse regardless of their order + *******************************************************************************/ +using System; +using System.IO; +using System.Xml; +using Microsoft.VisualStudio.TestTools.UnitTesting; +using OfficeOpenXml; + +namespace EPPlusTest.Core.Worksheet +{ + /// + /// Regression tests for header/footer sections being lost when they are not stored in + /// Left, Center, Right order. + /// + /// The ExcelHeaderFooterText constructor takes the first section code from the first two + /// characters of the raw string, then scans for further section codes. Before the fix its + /// scan only recognized "&C" and "&R" - never "&L" - so a "&L" appearing + /// anywhere other than at position 0 was not treated as the start of a new section. Its + /// content was swallowed into the preceding section, and then discarded when that section + /// was normalized by ReadHeaderFooterFormat/WriteHeaderFooterFormat, taking any "&G" + /// picture placeholder with it. The picture survived in the VML collection, so + /// HeaderFooter.Pictures.Count was unchanged, but nothing referenced it any more and it + /// stopped rendering in Excel. + /// + /// Excel writes the sections in the order the user created them, which is frequently not + /// Left, Center, Right - so this affected ordinary files produced by Excel itself. + /// + /// Reported case (oddfooter-left-corruption-template.xlsx), oddFooter raw value: + /// &C&"-,Bold"&12{FORMID}&"-,Regular"&11\n&L&G&R&G + /// which before the fix was persisted as: + /// &C&"-,Bold"&12{FORMID}&"-,Regular"&11\n&R&G + /// + /// The same file's oddHeader ("&L&G&R&G") was unaffected, both because it + /// starts with "&L" and because Save() only rewrites a node whose backing field is + /// non-null - and the reported repro only ever touched OddFooter. + /// + [TestClass] + public class HeaderFooterSectionOrderTests : TestBase + { + /// The customer's exact oddFooter value. Section order: Center, Left, Right. + private const string CustomerOddFooter = "&C&\"-,Bold\"&12{FORMID}&\"-,Regular\"&11\n&L&G&R&G"; + + /// The customer's exact oddHeader value. Section order: Left, Right. + private const string CustomerOddHeader = "&L&G&R&G"; + + [ClassInitialize] + public static void Init(TestContext testContext) + { + InitBase(); + } + + [TestMethod] + public void CustomerOddFooter_SurvivesRoundTrip_WhenOddFooterIsTouched() + { + // The exact scenario from the support ticket: a footer whose sections are stored + // in Center, Left, Right order, with a single read of a header/footer property as + // the only interaction before saving. + using (var pkg = new ExcelPackage()) + { + var ws = pkg.Workbook.Worksheets.Add("Sheet1"); + SetRawHeaderFooterNode(ws, "oddFooter", CustomerOddFooter); + SetRawHeaderFooterNode(ws, "oddHeader", CustomerOddHeader); + + // The single interaction from the customer's repro script. + var _ = ws.HeaderFooter.OddFooter.LeftAlignedText; + + var persistedFooter = SaveAndReadRawNode(pkg, "oddFooter"); + var persistedHeader = SaveAndReadRawNode(pkg, "oddHeader"); + + StringAssert.Contains(persistedFooter, "&L" + ExcelHeaderFooter.Image, + "The Left section's picture placeholder must survive the round trip. " + + $"Persisted footer was: \"{Escape(persistedFooter)}\"."); + + Assert.AreEqual(CustomerOddHeader, persistedHeader, + "The untouched oddHeader must be unchanged by the save."); + } + } + + [TestMethod] + public void LeftSection_IsParsed_RegardlessOfSectionOrder() + { + // Every one of these holds the same three logical sections, only reordered. The + // Left section always contains a picture placeholder, and must always come back. + var variants = new[] + { + new { Name = "L,C,R", Raw = "&L&G&CCenterText&R&G" }, + new { Name = "C,L,R", Raw = "&CCenterText&L&G&R&G" }, + new { Name = "R,L,C", Raw = "&R&G&L&G&CCenterText" }, + new { Name = "C,R,L", Raw = "&CCenterText&R&G&L&G" }, + new { Name = "R,C,L", Raw = "&R&G&CCenterText&L&G" }, + new { Name = "L,R", Raw = "&L&G&R&G" }, + new { Name = "C,L", Raw = "&CCenterText&L&G" }, + new { Name = "R,L", Raw = "&R&G&L&G" }, + }; + + foreach (var v in variants) + { + using (var pkg = new ExcelPackage()) + { + var ws = pkg.Workbook.Worksheets.Add("Sheet1"); + SetRawHeaderFooterNode(ws, "oddFooter", v.Raw); + + var oddFooter = ws.HeaderFooter.OddFooter; + + StringAssert.Contains(oddFooter.LeftAlignedText, ExcelHeaderFooter.Image, + $"[{v.Name}] The Left section's '&G' placeholder was not parsed from " + + $"\"{Escape(v.Raw)}\". Left parsed as \"{Escape(oddFooter.LeftAlignedText)}\"."); + + // Guard against the content merely being relocated into another section. + Assert.IsFalse(Contains(oddFooter.CenteredText, "&L"), + $"[{v.Name}] The Center section swallowed a stray '&L': " + + $"\"{Escape(oddFooter.CenteredText)}\"."); + Assert.IsFalse(Contains(oddFooter.RightAlignedText, "&L"), + $"[{v.Name}] The Right section swallowed a stray '&L': " + + $"\"{Escape(oddFooter.RightAlignedText)}\"."); + } + } + } + + [TestMethod] + public void AllSections_SurviveRoundTrip_RegardlessOfSectionOrder() + { + // As above, but verifying what actually reaches the file. Section order is not + // required to be preserved - only the content of each section. + var variants = new[] + { + new { Name = "L,C,R", Raw = "&L&G&CCenterText&R&G" }, + new { Name = "C,L,R", Raw = "&CCenterText&L&G&R&G" }, + new { Name = "R,L,C", Raw = "&R&G&L&G&CCenterText" }, + new { Name = "C,R,L", Raw = "&CCenterText&R&G&L&G" }, + new { Name = "R,C,L", Raw = "&R&G&CCenterText&L&G" }, + }; + + foreach (var v in variants) + { + using (var pkg = new ExcelPackage()) + { + var ws = pkg.Workbook.Worksheets.Add("Sheet1"); + SetRawHeaderFooterNode(ws, "oddFooter", v.Raw); + + // Touch the object so Save() rewrites the node. + var _ = ws.HeaderFooter.OddFooter.CenteredText; + + var persisted = SaveAndReadRawNode(pkg, "oddFooter"); + + StringAssert.Contains(persisted, "&L" + ExcelHeaderFooter.Image, + $"[{v.Name}] Left section lost. Raw was \"{Escape(v.Raw)}\", " + + $"persisted \"{Escape(persisted)}\"."); + StringAssert.Contains(persisted, "&CCenterText", + $"[{v.Name}] Center section lost. Persisted \"{Escape(persisted)}\"."); + StringAssert.Contains(persisted, "&R" + ExcelHeaderFooter.Image, + $"[{v.Name}] Right section lost. Persisted \"{Escape(persisted)}\"."); + } + } + } + + [TestMethod] + public void EmptySectionBetweenTwoSections_DoesNotConsumeFollowingSectionCode() + { + // Covers the "pos = startPos - 1" part of the fix. With the previous + // "pos = startPos" the loop's pos++ skipped the character at startPos, so a + // section code starting immediately after a consumed code - i.e. an empty + // section - was missed, and the following section was swallowed by it. + // + // The empty section must sit in the MIDDLE for this to bite: the first section + // code is taken outside the loop, so no skip has happened yet at that point. + // "&C&L&G&R&G" therefore parses correctly even without the fix and would not + // catch a regression here - the empty Center has to follow a match made inside + // the loop, as below. + const string raw = "&L&G&C&R&G"; + + using (var pkg = new ExcelPackage()) + { + var ws = pkg.Workbook.Worksheets.Add("Sheet1"); + SetRawHeaderFooterNode(ws, "oddFooter", raw); + + var oddFooter = ws.HeaderFooter.OddFooter; + + Console.WriteLine($"Raw : \"{Escape(raw)}\""); + Console.WriteLine($"Left : \"{Escape(oddFooter.LeftAlignedText)}\""); + Console.WriteLine($"Center : \"{Escape(oddFooter.CenteredText)}\""); + Console.WriteLine($"Right : \"{Escape(oddFooter.RightAlignedText)}\""); + + StringAssert.Contains(oddFooter.LeftAlignedText, ExcelHeaderFooter.Image, + $"Left section not parsed from \"{Escape(raw)}\"."); + + // The empty Center must not swallow the Right section's code. + Assert.IsFalse(Contains(oddFooter.CenteredText, "&R"), + $"The empty Center section swallowed the following '&R': " + + $"\"{Escape(oddFooter.CenteredText)}\"."); + + // This is the assertion that should fail if "pos = startPos - 1" is reverted: + // Right is never set, so RightAlignedText comes back as just "&R". + StringAssert.Contains(oddFooter.RightAlignedText, ExcelHeaderFooter.Image, + $"Right section not parsed from \"{Escape(raw)}\" - it was most likely " + + $"consumed by the empty Center section. Right parsed as " + + $"\"{Escape(oddFooter.RightAlignedText)}\"."); + } + } + + [TestMethod] + public void SectionCodeAtEndOfString_IsRecognized() + { + // Covers the "text.Length - 1" part of the fix. The previous "text.Length - 2" + // bound meant a section code occupying the final two characters was never seen, + // so it was swallowed into the preceding section instead of starting an empty one. + const string raw = "&L&G&R"; + + using (var pkg = new ExcelPackage()) + { + var ws = pkg.Workbook.Worksheets.Add("Sheet1"); + SetRawHeaderFooterNode(ws, "oddFooter", raw); + + var oddFooter = ws.HeaderFooter.OddFooter; + + Assert.IsFalse(Contains(oddFooter.LeftAlignedText, "&R"), + $"The trailing '&R' was swallowed into the Left section: " + + $"\"{Escape(oddFooter.LeftAlignedText)}\"."); + StringAssert.Contains(oddFooter.LeftAlignedText, ExcelHeaderFooter.Image, + "The Left section's picture placeholder should still be parsed."); + } + } + + [TestMethod] + public void HeaderSections_AreParsed_RegardlessOfSectionOrder() + { + // The same parsing path backs OddHeader, so it needs the same coverage - the + // reported case simply never touched the header. + const string raw = "&CHeaderCenter&L&G&R&G"; + + using (var pkg = new ExcelPackage()) + { + var ws = pkg.Workbook.Worksheets.Add("Sheet1"); + SetRawHeaderFooterNode(ws, "oddHeader", raw); + + var oddHeader = ws.HeaderFooter.OddHeader; + + StringAssert.Contains(oddHeader.LeftAlignedText, ExcelHeaderFooter.Image, + $"Left header section not parsed from \"{Escape(raw)}\". " + + $"Left parsed as \"{Escape(oddHeader.LeftAlignedText)}\"."); + } + } + + [TestMethod] + public void PictureCount_IsUnchanged_ByHeaderFooterTextRoundTrip() + { + // The reported symptom that made this hard to spot: the picture object itself was + // never lost, only the text reference to it, so Pictures.Count kept reporting the + // original value. This pins that behavior down so a future change cannot start + // silently dropping the VML pictures instead. + using (var pkg = new ExcelPackage()) + { + var ws = pkg.Workbook.Worksheets.Add("Sheet1"); + SetRawHeaderFooterNode(ws, "oddFooter", CustomerOddFooter); + + var countBefore = ws.HeaderFooter.Pictures.Count; + var _ = ws.HeaderFooter.OddFooter.LeftAlignedText; + + using (var stream = new MemoryStream()) + { + pkg.SaveAs(stream); + using (var reloaded = new ExcelPackage(stream)) + { + Assert.AreEqual(countBefore, + reloaded.Workbook.Worksheets[0].HeaderFooter.Pictures.Count, + "HeaderFooter.Pictures.Count changed across the round trip."); + } + } + } + } + + #region Helpers + + private static string Escape(string s) + { + return s == null ? "" : s.Replace("\n", "\\n").Replace("\r", "\\r"); + } + + private static bool Contains(string haystack, string needle) + { + return haystack != null && haystack.Contains(needle); + } + + private static XmlNamespaceManager GetNsm(ExcelWorksheet ws) + { + var nsm = new XmlNamespaceManager(ws.WorksheetXml.NameTable); + nsm.AddNamespace("d", ExcelPackage.schemaMain); + return nsm; + } + + /// + /// Writes a raw string straight into d:headerFooter/d:{nodeName}, so section ordering + /// is under the test's control instead of EPPlus's own always-Left-Center-Right + /// authoring order - which is what makes these orderings reachable at all. + /// + private static void SetRawHeaderFooterNode(ExcelWorksheet ws, string nodeName, string rawText) + { + var nsm = GetNsm(ws); + var wsNode = ws.WorksheetXml.SelectSingleNode("d:worksheet", nsm); + + var hfNode = wsNode.SelectSingleNode("d:headerFooter", nsm); + if (hfNode == null) + { + hfNode = ws.WorksheetXml.CreateElement("headerFooter", ExcelPackage.schemaMain); + wsNode.AppendChild(hfNode); + } + + var node = hfNode.SelectSingleNode("d:" + nodeName, nsm); + if (node == null) + { + node = ws.WorksheetXml.CreateElement(nodeName, ExcelPackage.schemaMain); + hfNode.AppendChild(node); + } + node.InnerText = rawText; + } + + /// + /// Saves the package to a stream, reloads it, and returns the raw text of the + /// requested header/footer node as persisted - i.e. what Excel would read. + /// + private static string SaveAndReadRawNode(ExcelPackage pkg, string nodeName) + { + using (var stream = new MemoryStream()) + { + pkg.SaveAs(stream); + + using (var reloaded = new ExcelPackage(stream)) + { + var ws = reloaded.Workbook.Worksheets[0]; + var nsm = GetNsm(ws); + var node = ws.WorksheetXml.SelectSingleNode( + "d:worksheet/d:headerFooter/d:" + nodeName, nsm); + return node == null ? null : node.InnerText; + } + } + } + + #endregion + } +} \ No newline at end of file From bee3cb83bc2b72e98d7d11702f9ffd8994cf1ccd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ossian=20Edstr=C3=B6m?= Date: Wed, 26 Aug 2026 15:21:48 +0200 Subject: [PATCH 38/73] Implemented object defaults(initial) --- .../Chart/ChartStyleFallbackTest.cs | 4 +- .../Chart/Style/ExcelChartStyleReference.cs | 33 +------- .../Drawing/Shape/DefaultShapeDefinition.cs | 84 +++++++++++++++++++ .../Shape/Style/ExcelShapeStyleEntry.cs | 83 ++++++++++++++++++ .../Style/ExcelShapeStyleFontReference.cs | 65 ++++++++++++++ .../Shape/Style/ShapeStyleReference.cs | 68 +++++++++++++++ .../Drawing/Theme/ExcelThemeObjectDefaults.cs | 52 ++++++++++-- 7 files changed, 354 insertions(+), 35 deletions(-) create mode 100644 src/EPPlus/Drawing/Shape/DefaultShapeDefinition.cs create mode 100644 src/EPPlus/Drawing/Shape/Style/ExcelShapeStyleEntry.cs create mode 100644 src/EPPlus/Drawing/Shape/Style/ExcelShapeStyleFontReference.cs create mode 100644 src/EPPlus/Drawing/Shape/Style/ShapeStyleReference.cs diff --git a/src/EPPlus.DrawingRenderer.Tests/Chart/ChartStyleFallbackTest.cs b/src/EPPlus.DrawingRenderer.Tests/Chart/ChartStyleFallbackTest.cs index c91e104192..c47251ff3c 100644 --- a/src/EPPlus.DrawingRenderer.Tests/Chart/ChartStyleFallbackTest.cs +++ b/src/EPPlus.DrawingRenderer.Tests/Chart/ChartStyleFallbackTest.cs @@ -395,6 +395,8 @@ public void Epp_Gen_DefaultShape() var defaultRect = ws.Drawings.AddShape("Default", OfficeOpenXml.Drawing.eShapeStyle.Round1Rect); var gradientRect = ws.Drawings.AddShape("GradRect", OfficeOpenXml.Drawing.eShapeStyle.Round1Rect); + var aThemeStyle = defaultRect.ThemeStyles.BorderReference; + defaultRect.SetPosition(300, 1); gradientRect.SetPosition(300, 1000); @@ -438,7 +440,7 @@ public void EpplusGeneratedChart() //ws.Workbook.ThemeManager.GetOrCreateTheme(); - + //ws.Workbook.ThemeManager.GetOrCreateTheme().FormatScheme.BackgroundFillStyle[0] = true; ws.Cells["A1:A3"].Formula = "ROW()+COLUMN()"; ws.Calculate(); diff --git a/src/EPPlus/Drawing/Chart/Style/ExcelChartStyleReference.cs b/src/EPPlus/Drawing/Chart/Style/ExcelChartStyleReference.cs index 194869e0f3..6f90f72312 100644 --- a/src/EPPlus/Drawing/Chart/Style/ExcelChartStyleReference.cs +++ b/src/EPPlus/Drawing/Chart/Style/ExcelChartStyleReference.cs @@ -10,6 +10,7 @@ Date Author Change ************************************************************************************************* 01/27/2020 EPPlus Software AB Initial release EPPlus 5 *************************************************************************************************/ +using OfficeOpenXml.Drawing.Shape.Style; using System; using System.Globalization; using System.Xml; @@ -19,29 +20,14 @@ namespace OfficeOpenXml.Drawing.Chart.Style /// /// A reference from a chart style to the theme collection /// - public class ExcelChartStyleReference : XmlHelper + public class ExcelChartStyleReference : ShapeStyleReference { string _path; - internal ExcelChartStyleReference(XmlNamespaceManager nsm, XmlNode topNode, string path) : base(nsm, topNode) + internal ExcelChartStyleReference(XmlNamespaceManager nsm, XmlNode topNode, string path) : base(nsm, topNode, path) { _path = path; } - /// - /// The index to the theme style matrix. - /// - /// - public int Index - { - get - { - return GetXmlNodeInt($"{_path}/@idx"); - } - set - { - if (value < 0) throw new ArgumentOutOfRangeException("Index", "Can't be negative"); - SetXmlNodeString($"{_path}/@idx", value.ToString(CultureInfo.InvariantCulture)); - } - } + ExcelChartStyleColorManager _color = null; /// /// The color to be used for the reference. @@ -59,16 +45,5 @@ public ExcelChartStyleColorManager Color return _color; } } - /// - /// If the reference has a color - /// - public bool HasColor - { - get - { - var node = GetNode(_path); - return node!=null && node.HasChildNodes; - } - } } } \ No newline at end of file diff --git a/src/EPPlus/Drawing/Shape/DefaultShapeDefinition.cs b/src/EPPlus/Drawing/Shape/DefaultShapeDefinition.cs new file mode 100644 index 0000000000..7fe02e7775 --- /dev/null +++ b/src/EPPlus/Drawing/Shape/DefaultShapeDefinition.cs @@ -0,0 +1,84 @@ +using OfficeOpenXml.Drawing.Interfaces; +using OfficeOpenXml.Drawing.Shape.Style; +using System.Xml; + +namespace OfficeOpenXml.Drawing.Shape +{ + /// + /// Roughly Represents CT_DefaultShapeDefinition + /// + internal class DefaultShapeDefinition : XmlHelper + { + string _fillPath = "{0}/{1}:spPr"; + string _defaultTextBodyPath = "{0}/{1}:bodyPr"; + string _stylePath = "{0}/{1}:style"; + + //Do we support this? Does Excel? Excel appears to in this specific case. + //TODO: ImplementTextList + + //TODO: Implement ExtLst + //private ExtLst + + private readonly IPictureRelationDocument _pictureRelationDocument; + + string _prefix; + + internal DefaultShapeDefinition(XmlNamespaceManager nsm, XmlNode topNode, string path, IPictureRelationDocument pictureRelationDocument, string prefix = "a") : base(nsm, topNode) + { + _prefix = prefix; + } + + + private ExcelDrawingFill _fill; + /// + /// + /// Reference to fill settings for a chart part + /// + public ExcelDrawingFill Fill + { + get + { + if (_fill == null) + { + _fill = new ExcelDrawingFill(_pictureRelationDocument, NameSpaceManager, TopNode, _fillPath, SchemaNodeOrder); + } + return _fill; + } + } + + private ExcelTextBody _defaultTextBody = null; + /// + /// Reference to default text body run settings for a chart part + /// + public ExcelTextBody DefaultTextBody + { + get + { + if (_defaultTextBody == null) + { + _defaultTextBody = new ExcelTextBody(_pictureRelationDocument, NameSpaceManager, TopNode, _defaultTextBodyPath); + } + return _defaultTextBody; + + } + } + + ExcelShapeStyleEntry _style; + + /// + /// Reference to default text body run settings for a chart part + /// + public ExcelShapeStyleEntry Style + { + get + { + if (_style == null) + { + _style = new ExcelShapeStyleEntry(NameSpaceManager, TopNode, _stylePath, _pictureRelationDocument, _prefix); + } + return _style; + + } + } + } +} diff --git a/src/EPPlus/Drawing/Shape/Style/ExcelShapeStyleEntry.cs b/src/EPPlus/Drawing/Shape/Style/ExcelShapeStyleEntry.cs new file mode 100644 index 0000000000..566f6809a7 --- /dev/null +++ b/src/EPPlus/Drawing/Shape/Style/ExcelShapeStyleEntry.cs @@ -0,0 +1,83 @@ +using OfficeOpenXml.Drawing.Chart.Style; +using OfficeOpenXml.Drawing.Interfaces; +using System.Xml; + +namespace OfficeOpenXml.Drawing.Shape.Style +{ + //Style (CT_ShapeStyle node) + internal class ExcelShapeStyleEntry : XmlHelper + { + string _fillReferencePath = "{0}/{1}:fillRef"; + string _borderReferencePath = "{0}/{1}:lnRef"; + string _effectReferencePath = "{0}/{1}:effectRef"; + string _fontReferencePath = "{0}/{1}:fontRef"; + + private readonly IPictureRelationDocument _pictureRelationDocument; + internal ExcelShapeStyleEntry(XmlNamespaceManager nsm, XmlNode topNode, string path, IPictureRelationDocument pictureRelationDocument, string prefix = "a") : base(nsm, topNode) + { + + } + private ShapeStyleReference _borderReference = null; + /// Border reference. + /// Contains an index reference to the theme and a color to be used in border styling + public ShapeStyleReference BorderReference + { + get + { + if (_borderReference == null) + { + _borderReference = new ShapeStyleReference(NameSpaceManager, TopNode, _borderReferencePath); + } + return _borderReference; + } + } + private ShapeStyleReference _fillReference = null; + /// + /// Fill reference. + /// Contains an index reference to the theme and a fill color to be used in fills + /// + public ShapeStyleReference FillReference + { + get + { + if (_fillReference == null) + { + _fillReference = new ShapeStyleReference(NameSpaceManager, TopNode, _fillReferencePath); + } + return _fillReference; + } + } + private ShapeStyleReference _effectReference = null; + /// + /// Effect reference. + /// Contains an index reference to the theme and a color to be used in effects + /// + public ShapeStyleReference EffectReference + { + get + { + if (_effectReference == null) + { + _effectReference = new ShapeStyleReference(NameSpaceManager, TopNode, _effectReferencePath); + } + return _effectReference; + } + } + ExcelChartStyleFontReference _fontReference = null; + /// + /// Font reference. + /// Contains an index reference to the theme and a color to be used for font styling + /// + public ExcelChartStyleFontReference FontReference + { + get + { + if (_fontReference == null) + { + _fontReference = new ExcelChartStyleFontReference(NameSpaceManager, TopNode, _fontReferencePath); + } + return _fontReference; + } + } + } +} diff --git a/src/EPPlus/Drawing/Shape/Style/ExcelShapeStyleFontReference.cs b/src/EPPlus/Drawing/Shape/Style/ExcelShapeStyleFontReference.cs new file mode 100644 index 0000000000..3ca7e56d8b --- /dev/null +++ b/src/EPPlus/Drawing/Shape/Style/ExcelShapeStyleFontReference.cs @@ -0,0 +1,65 @@ +using OfficeOpenXml.Drawing.Chart.Style; +using OfficeOpenXml.Drawing.Style.Coloring; +using OfficeOpenXml.Drawing.Theme; +using OfficeOpenXml.Utils.EnumUtils; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Xml; + +namespace OfficeOpenXml.Drawing.Shape.Style +{ + //Represents CT_FontReference + internal class ExcelShapeStyleFontReference : XmlHelper + { + string _path; + internal ExcelShapeStyleFontReference(XmlNamespaceManager nsm, XmlNode topNode, string path) : base(nsm, topNode) + { + _path = path; + } + /// + /// The index to the style matrix. + /// This property referes to the theme + /// + public eThemeFontCollectionType Index + { + get + { + return GetXmlNodeString($"{_path}/@idx").ToEnum(eThemeFontCollectionType.None); + } + set + { + SetXmlNodeString($"{_path}/@idx", value.ToEnumString()); + } + } + ExcelDrawingColorManager _color = null; + /// + /// The color of the font + /// This will replace any the StyleClr node in the chart style xml. + /// + public ExcelDrawingColorManager Color + { + get + { + if (_color == null) + { + _color = new ExcelDrawingColorManager(NameSpaceManager, TopNode, _path, SchemaNodeOrder); + } + + return _color; + } + } + /// + /// If the reference has a color + /// + public bool HasColor + { + get + { + var node = GetNode(_path); + return node != null && node.HasChildNodes; + } + } + } +} diff --git a/src/EPPlus/Drawing/Shape/Style/ShapeStyleReference.cs b/src/EPPlus/Drawing/Shape/Style/ShapeStyleReference.cs new file mode 100644 index 0000000000..2a75151f02 --- /dev/null +++ b/src/EPPlus/Drawing/Shape/Style/ShapeStyleReference.cs @@ -0,0 +1,68 @@ +using OfficeOpenXml.Drawing.Chart.Style; +using OfficeOpenXml.Drawing.Style.Coloring; +using System; +using System.Collections.Generic; +using System.Globalization; +using System.Linq; +using System.Text; +using System.Xml; + +//StyleMatrixReference +namespace OfficeOpenXml.Drawing.Shape.Style +{ + public class ShapeStyleReference : XmlHelper + { + string _path; + internal ShapeStyleReference(XmlNamespaceManager nsm, XmlNode topNode, string path) : base(nsm, topNode) + { + _path = path; + } + + /// + /// The index to the theme style matrix. + /// + /// + public int Index + { + get + { + return GetXmlNodeInt($"{_path}/@idx"); + } + set + { + if (value < 0) throw new ArgumentOutOfRangeException("Index", "Can't be negative"); + SetXmlNodeString($"{_path}/@idx", value.ToString(CultureInfo.InvariantCulture)); + } + } + + ExcelDrawingColorManager _color; + /// + /// The color to be used for the reference. + /// This will replace any the StyleClr node in the shape style xml. + /// + public ExcelDrawingColorManager ShapeColor + { + get + { + if (_color == null) + { + _color = new ExcelDrawingColorManager(NameSpaceManager, TopNode, _path, SchemaNodeOrder); + } + + return _color; + } + } + + /// + /// If the reference has a color + /// + public bool HasColor + { + get + { + var node = GetNode(_path); + return node != null && node.HasChildNodes; + } + } + } +} diff --git a/src/EPPlus/Drawing/Theme/ExcelThemeObjectDefaults.cs b/src/EPPlus/Drawing/Theme/ExcelThemeObjectDefaults.cs index 39e61f61bb..99964a4bda 100644 --- a/src/EPPlus/Drawing/Theme/ExcelThemeObjectDefaults.cs +++ b/src/EPPlus/Drawing/Theme/ExcelThemeObjectDefaults.cs @@ -1,7 +1,4 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; +using OfficeOpenXml.Drawing.Shape; using System.Xml; namespace OfficeOpenXml.Drawing.Theme @@ -9,10 +6,55 @@ namespace OfficeOpenXml.Drawing.Theme internal class ExcelThemeObjectDefaults : XmlHelper { private readonly ExcelThemeBase _theme; - + private readonly string _path = "objectDefaults"; public ExcelThemeObjectDefaults(XmlNamespaceManager nameSpaceManager, XmlNode topNode, ExcelThemeBase theme) : base(nameSpaceManager, topNode) { _theme = theme; } + + DefaultShapeDefinition _spDef = null; + DefaultShapeDefinition _lnDef = null; + DefaultShapeDefinition _txDef = null; + + public DefaultShapeDefinition ShapeDefinition + { + get + { + if (_spDef == null) + { + _spDef = new DefaultShapeDefinition(NameSpaceManager, TopNode, _path +"\\spDef", _theme); + } + + return _spDef; + } + } + + public DefaultShapeDefinition LineDefinition + { + get + { + if (_lnDef == null) + { + _lnDef = new DefaultShapeDefinition(NameSpaceManager, TopNode, _path + "\\lnDef", _theme); + } + + return _lnDef; + } + } + + public DefaultShapeDefinition TextDefinition + { + get + { + if (_txDef == null) + { + _txDef = new DefaultShapeDefinition(NameSpaceManager, TopNode, _path + "\\txDef", _theme); + } + + return _txDef; + } + } + + //TODO: Implement ExtLst } } From 1d92931c03dbf2f89eefeb058cdb24900a35bef6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ossian=20Edstr=C3=B6m?= Date: Wed, 26 Aug 2026 16:15:08 +0200 Subject: [PATCH 39/73] Resolved multiple ObjectDefaults reading errors --- .../Chart/ChartStyleFallbackTest.cs | 34 +++++++++++++++++++ .../Drawing/Shape/DefaultShapeDefinition.cs | 4 +++ .../Shape/Style/ExcelShapeStyleEntry.cs | 5 ++- .../Shape/Style/ShapeStyleReference.cs | 2 +- src/EPPlus/Drawing/Theme/ExcelThemeBase.cs | 4 +-- .../Drawing/Theme/ExcelThemeObjectDefaults.cs | 10 ++++-- 6 files changed, 52 insertions(+), 7 deletions(-) diff --git a/src/EPPlus.DrawingRenderer.Tests/Chart/ChartStyleFallbackTest.cs b/src/EPPlus.DrawingRenderer.Tests/Chart/ChartStyleFallbackTest.cs index c47251ff3c..032ed9199c 100644 --- a/src/EPPlus.DrawingRenderer.Tests/Chart/ChartStyleFallbackTest.cs +++ b/src/EPPlus.DrawingRenderer.Tests/Chart/ChartStyleFallbackTest.cs @@ -6,6 +6,8 @@ using System.Drawing; using tc = OfficeOpenXml.Utils.TypeConversion; using System.Globalization; +using OfficeOpenXml.Drawing.Style.Coloring; +using OfficeOpenXml.Drawing.Theme; namespace EPPlus.DrawingRenderer.Tests.Chart { @@ -487,5 +489,37 @@ public void GenerateSimpleChart() } } + [TestMethod] + public void ReadObjectDefaults() + { + string fileName = "ObjectDefaultsChanged"; + + using (var p = OpenTemplatePackage($"{fileName}.xlsx")) + { + var theme = p.Workbook.ThemeManager.GetOrCreateTheme(); + var shapeStyle = theme.ObjectDefaults.ShapeDefinition.Style; + var fillRef = shapeStyle.FillReference; + + var lnRef = shapeStyle.BorderReference; + + Assert.AreEqual(eSchemeColor.Accent2, lnRef.ShapeColor.SchemeColor.Color); + Assert.IsTrue(lnRef.ShapeColor.Transforms.Count > 0); + Assert.AreEqual(eColorTransformType.Shade, lnRef.ShapeColor.Transforms[0].Type); + Assert.AreEqual(15, lnRef.ShapeColor.Transforms[0].Value); + Assert.AreEqual(2, lnRef.Index); + + Assert.IsTrue(fillRef.HasColor); + var schemeClr = fillRef.ShapeColor.SchemeColor; + var col = schemeClr.Color; + Assert.AreEqual(eSchemeColor.Accent2, col); + Assert.AreEqual(1, fillRef.Index); + + var fontRef = theme.ObjectDefaults.ShapeDefinition.Style.FontReference; + Assert.AreEqual(eSchemeColor.Light1, fontRef.Color.SchemeColor.Color); + Assert.AreEqual(eThemeFontCollectionType.Minor, fontRef.Index); + + SaveAndCleanup(p); + } + } } } diff --git a/src/EPPlus/Drawing/Shape/DefaultShapeDefinition.cs b/src/EPPlus/Drawing/Shape/DefaultShapeDefinition.cs index 7fe02e7775..28c1645bb0 100644 --- a/src/EPPlus/Drawing/Shape/DefaultShapeDefinition.cs +++ b/src/EPPlus/Drawing/Shape/DefaultShapeDefinition.cs @@ -26,6 +26,10 @@ internal class DefaultShapeDefinition : XmlHelper internal DefaultShapeDefinition(XmlNamespaceManager nsm, XmlNode topNode, string path, IPictureRelationDocument pictureRelationDocument, string prefix = "a") : base(nsm, topNode) { _prefix = prefix; + + _fillPath = string.Format(_fillPath, path, _prefix); + _defaultTextBodyPath = string.Format(_defaultTextBodyPath, path, _prefix); + _stylePath = string.Format(_stylePath, path, _prefix); } diff --git a/src/EPPlus/Drawing/Shape/Style/ExcelShapeStyleEntry.cs b/src/EPPlus/Drawing/Shape/Style/ExcelShapeStyleEntry.cs index 566f6809a7..23251d360b 100644 --- a/src/EPPlus/Drawing/Shape/Style/ExcelShapeStyleEntry.cs +++ b/src/EPPlus/Drawing/Shape/Style/ExcelShapeStyleEntry.cs @@ -15,7 +15,10 @@ internal class ExcelShapeStyleEntry : XmlHelper private readonly IPictureRelationDocument _pictureRelationDocument; internal ExcelShapeStyleEntry(XmlNamespaceManager nsm, XmlNode topNode, string path, IPictureRelationDocument pictureRelationDocument, string prefix = "a") : base(nsm, topNode) { - + _fillReferencePath = string.Format(_fillReferencePath, path, prefix); + _borderReferencePath = string.Format(_borderReferencePath, path, prefix); + _effectReferencePath = string.Format(_effectReferencePath, path, prefix); + _fontReferencePath = string.Format(_fontReferencePath, path, prefix); } private ShapeStyleReference _borderReference = null; /// Border reference. diff --git a/src/EPPlus/Drawing/Shape/Style/ShapeStyleReference.cs b/src/EPPlus/Drawing/Shape/Style/ShapeStyleReference.cs index 2a75151f02..cbd03366da 100644 --- a/src/EPPlus/Drawing/Shape/Style/ShapeStyleReference.cs +++ b/src/EPPlus/Drawing/Shape/Style/ShapeStyleReference.cs @@ -38,7 +38,7 @@ public int Index ExcelDrawingColorManager _color; /// /// The color to be used for the reference. - /// This will replace any the StyleClr node in the shape style xml. + /// simplerForm of Color on ChartNodes /// public ExcelDrawingColorManager ShapeColor { diff --git a/src/EPPlus/Drawing/Theme/ExcelThemeBase.cs b/src/EPPlus/Drawing/Theme/ExcelThemeBase.cs index e3e4661429..66b71568d4 100644 --- a/src/EPPlus/Drawing/Theme/ExcelThemeBase.cs +++ b/src/EPPlus/Drawing/Theme/ExcelThemeBase.cs @@ -27,7 +27,7 @@ public class ExcelThemeBase : XmlHelper, IPictureRelationDocument readonly string _colorSchemePath = "{0}a:clrScheme"; readonly string _fontSchemePath = "{0}a:fontScheme"; readonly string _fmtSchemePath = "{0}a:fmtScheme"; - readonly string _objectDefaultsPath = "{0}a:ObjectDefaults"; + readonly string _objectDefaultsPath = "{0}a:objectDefaults"; readonly ExcelPackage _pck; Dictionary _hashes=new Dictionary(); @@ -102,7 +102,7 @@ public ExcelFormatScheme FormatScheme } } - ExcelThemeObjectDefaults _objectDefaults; + ExcelThemeObjectDefaults _objectDefaults = null; /// /// diff --git a/src/EPPlus/Drawing/Theme/ExcelThemeObjectDefaults.cs b/src/EPPlus/Drawing/Theme/ExcelThemeObjectDefaults.cs index 99964a4bda..77768fc3d7 100644 --- a/src/EPPlus/Drawing/Theme/ExcelThemeObjectDefaults.cs +++ b/src/EPPlus/Drawing/Theme/ExcelThemeObjectDefaults.cs @@ -7,6 +7,9 @@ internal class ExcelThemeObjectDefaults : XmlHelper { private readonly ExcelThemeBase _theme; private readonly string _path = "objectDefaults"; + private readonly string _spDefPath = "a:spDef"; + private readonly string _lnDefPath = "a:lnDef"; + private readonly string _txDefPath = "a:txDef"; public ExcelThemeObjectDefaults(XmlNamespaceManager nameSpaceManager, XmlNode topNode, ExcelThemeBase theme) : base(nameSpaceManager, topNode) { _theme = theme; @@ -22,7 +25,8 @@ public DefaultShapeDefinition ShapeDefinition { if (_spDef == null) { - _spDef = new DefaultShapeDefinition(NameSpaceManager, TopNode, _path +"\\spDef", _theme); + var test = TopNode.SelectSingleNode(_spDefPath, NameSpaceManager); + _spDef = new DefaultShapeDefinition(NameSpaceManager, TopNode, _spDefPath, _theme); } return _spDef; @@ -35,7 +39,7 @@ public DefaultShapeDefinition LineDefinition { if (_lnDef == null) { - _lnDef = new DefaultShapeDefinition(NameSpaceManager, TopNode, _path + "\\lnDef", _theme); + _lnDef = new DefaultShapeDefinition(NameSpaceManager, TopNode, _lnDefPath, _theme); } return _lnDef; @@ -48,7 +52,7 @@ public DefaultShapeDefinition TextDefinition { if (_txDef == null) { - _txDef = new DefaultShapeDefinition(NameSpaceManager, TopNode, _path + "\\txDef", _theme); + _txDef = new DefaultShapeDefinition(NameSpaceManager, TopNode, _txDefPath, _theme); } return _txDef; From e8130c64c1a0643b9874e7ce814573c20cabfc5c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jan=20K=C3=A4llman?= Date: Wed, 26 Aug 2026 16:38:32 +0200 Subject: [PATCH 40/73] Fixed horizontal axis. Format number for vertical axis. --- src/Directory.Packages.props | 1 - .../EPPlus.Compression.csproj | 1 + .../Chart/LineChartToSvgTests.cs | 21 ++++ .../EPPlus.DrawingRenderer.csproj | 1 + src/EPPlus.Export.Pdf.Tests/PdfTests.cs | 119 ++++++++++++++++++ .../EPPlus.Export.Pdf.csproj | 2 +- .../EPPlus.Interfaces.csproj | 6 +- .../EPPlus.System.Drawing.csproj | 6 +- src/EPPlus/Drawing/Chart/ExcelChartAxis.cs | 41 ++++-- .../Chart/Axis/DateAxisScaleCalculator.cs | 109 +++++++++++----- .../Renderer/Chart/ChartAxisRenderer.cs | 26 +++- .../Renderer/Chart/ChartPlotareaRenderer.cs | 2 +- .../Renderer/Chart/eTextOrientation.cs | 1 + src/EPPlus/Drawing/Renderer/ChartRenderer.cs | 2 +- src/EPPlus/EPPlus.csproj | 74 +++++------ src/EPPlus/XmlHelper.cs | 2 +- 16 files changed, 317 insertions(+), 97 deletions(-) diff --git a/src/Directory.Packages.props b/src/Directory.Packages.props index 04f80c67f3..6ccdb3125f 100644 --- a/src/Directory.Packages.props +++ b/src/Directory.Packages.props @@ -4,7 +4,6 @@ - diff --git a/src/EPPlus.Compression/EPPlus.Compression.csproj b/src/EPPlus.Compression/EPPlus.Compression.csproj index de594e155c..c7093d41c9 100644 --- a/src/EPPlus.Compression/EPPlus.Compression.csproj +++ b/src/EPPlus.Compression/EPPlus.Compression.csproj @@ -6,5 +6,6 @@ OfficeOpenXml.Packaging.Ionic true EPPlus.Compression.snk + false \ No newline at end of file diff --git a/src/EPPlus.DrawingRenderer.Tests/Chart/LineChartToSvgTests.cs b/src/EPPlus.DrawingRenderer.Tests/Chart/LineChartToSvgTests.cs index 6d55224e92..2e520346ee 100644 --- a/src/EPPlus.DrawingRenderer.Tests/Chart/LineChartToSvgTests.cs +++ b/src/EPPlus.DrawingRenderer.Tests/Chart/LineChartToSvgTests.cs @@ -322,6 +322,27 @@ public void GenerateLineChartWithDropLine() } } } + [TestMethod] + public void GenerateBlazorSample1() + { + ExcelPackage.License.SetNonCommercialOrganization("EPPlus Project"); + using (var p = OpenTemplatePackage("BlazorSample1.xlsx")) + { + 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); + + for (int i = 0; i < ws.Drawings.Count; i++) + { + var c = ws.Drawings[i]; + var svg = c.ToSvg(); + SaveTextFileToWorkbook($"svg\\BlazorSample1{i}.svg", svg); + } + } + } //2.4-CreateAFileSystemReport.xlsx //3.3-FxReportFromDatabase.xlsx } diff --git a/src/EPPlus.DrawingRenderer/EPPlus.DrawingRenderer.csproj b/src/EPPlus.DrawingRenderer/EPPlus.DrawingRenderer.csproj index a8ab2f7e66..5d22f79446 100644 --- a/src/EPPlus.DrawingRenderer/EPPlus.DrawingRenderer.csproj +++ b/src/EPPlus.DrawingRenderer/EPPlus.DrawingRenderer.csproj @@ -11,6 +11,7 @@ latest True EPPlus.DrawingRenderer.snk + false diff --git a/src/EPPlus.Export.Pdf.Tests/PdfTests.cs b/src/EPPlus.Export.Pdf.Tests/PdfTests.cs index ae78dbaa77..9f97a7e2ac 100644 --- a/src/EPPlus.Export.Pdf.Tests/PdfTests.cs +++ b/src/EPPlus.Export.Pdf.Tests/PdfTests.cs @@ -639,5 +639,124 @@ public void EPPlusToPdf() p.Workbook.SaveAsPdf(_pdfPath + "Snake.Pdf"); p.SaveAs(_pdfPath + "Snake.xlsx"); } +<<<<<<< Updated upstream +======= + + [TestMethod] + public void Testing() + { + using var p = OpenTemplatePackage("PDFTestKarl.xlsx"); + var wb = p.Workbook; + string path = _pdfPath + "WorksheetTest1.pdf"; + wb.SaveAsPdf(path); + AssertLooksLikePdf(File.ReadAllBytes(path)); + } + + [TestMethod] + public void EachWorksheetUsesItsOwnOrientation() + { + using (var package = OpenTemplatePackage("PDFTestKarl.xlsx")) + { + package.Workbook.Worksheets[0].PrinterSettings.Orientation = eOrientation.Portrait; + package.Workbook.Worksheets[1].PrinterSettings.Orientation = eOrientation.Landscape; + + var settings = GetPdfSettings.GetPdfSettingsFromPrinterSettings( + package.Workbook, + package.Workbook.Worksheets[0].PrinterSettings); + + byte[] pdf; + using (var ms = new MemoryStream()) + { + new PdfCatalog(ms, settings, package.Workbook); + pdf = ms.ToArray(); + } + + var matches = Regex.Matches( + Encoding.ASCII.GetString(pdf), + @"/MediaBox\s*\[\s*0\s+0\s+(?[\d.]+)\s+(?[\d.]+)\s*\]"); + + Assert.AreEqual(2, matches.Count, "Expected one page per worksheet."); + + var ci = CultureInfo.InvariantCulture; + double w1 = double.Parse(matches[0].Groups["w"].Value, ci); + double h1 = double.Parse(matches[0].Groups["h"].Value, ci); + double w2 = double.Parse(matches[1].Groups["w"].Value, ci); + double h2 = double.Parse(matches[1].Groups["h"].Value, ci); + + Assert.IsTrue(h1 > w1, "Page 1 should be portrait."); + Assert.IsTrue(w2 > h2, "Page 2 should be landscape."); + // Landscape is the same paper transposed, not a different paper size. + Assert.AreEqual(w1, h2, 0.01d); + Assert.AreEqual(h1, w2, 0.01d); + } + } + + [TestMethod] + public void EachWorksheetUsesItsOwnShowGridLines() + { + using (var package = OpenTemplatePackage("PDFTestKarl.xlsx")) + { + package.Workbook.Worksheets[0].PrinterSettings.ShowGridLines = false; + package.Workbook.Worksheets[1].PrinterSettings.ShowGridLines = true; + + var baseSettings = GetPdfSettings.GetPdfSettingsFromPrinterSettings( + package.Workbook, + package.Workbook.Worksheets[0].PrinterSettings); + + var s0 = GetPdfSettings.GetPdfSettingsForSheet( + baseSettings, package.Workbook.Worksheets[0].PrinterSettings); + var s1 = GetPdfSettings.GetPdfSettingsForSheet( + baseSettings, package.Workbook.Worksheets[1].PrinterSettings); + + Assert.IsFalse(s0.ShowGridLines, "Sheet 1 did not ask for gridlines."); + Assert.IsTrue(s1.ShowGridLines, "Sheet 2 asked for gridlines."); + Assert.IsFalse(baseSettings.ShowGridLines, "The base object must not be mutated."); + } + } + + [TestMethod] + public void EachWorksheetUsesItsOwnPaperSize() + { + using (var package = OpenTemplatePackage("PDFTestKarl.xlsx")) + { + // Orientation is set explicitly so a transposed page size cannot be + // mistaken for a different paper size. + package.Workbook.Worksheets[0].PrinterSettings.Orientation = eOrientation.Portrait; + package.Workbook.Worksheets[1].PrinterSettings.Orientation = eOrientation.Portrait; + package.Workbook.Worksheets[0].PrinterSettings.PaperSize = ePaperSize.A4; + package.Workbook.Worksheets[1].PrinterSettings.PaperSize = ePaperSize.A3; + + var settings = GetPdfSettings.GetPdfSettingsFromPrinterSettings( + package.Workbook, + package.Workbook.Worksheets[0].PrinterSettings); + + byte[] pdf; + using (var ms = new MemoryStream()) + { + new PdfCatalog(ms, settings, package.Workbook); + pdf = ms.ToArray(); + } + + var matches = Regex.Matches( + Encoding.ASCII.GetString(pdf), + @"/MediaBox\s*\[\s*0\s+0\s+(?[\d.]+)\s+(?[\d.]+)\s*\]"); + + Assert.AreEqual(2, matches.Count, "Expected one page per worksheet."); + + var ci = CultureInfo.InvariantCulture; + double w1 = double.Parse(matches[0].Groups["w"].Value, ci); + double h1 = double.Parse(matches[0].Groups["h"].Value, ci); + double w2 = double.Parse(matches[1].Groups["w"].Value, ci); + double h2 = double.Parse(matches[1].Groups["h"].Value, ci); + + // Compare against the source of truth rather than literal point values. + // PdfPageSize rounds mm to whole points, so 210x297 mm becomes 595x842. + Assert.AreEqual(PdfPageSize.A4.WidthPu, w1, "Page 1 should be A4."); + Assert.AreEqual(PdfPageSize.A4.HeightPu, h1, "Page 1 should be A4."); + Assert.AreEqual(PdfPageSize.A3.WidthPu, w2, "Page 2 should be A3, not sheet 1's A4."); + Assert.AreEqual(PdfPageSize.A3.HeightPu, h2, "Page 2 should be A3, not sheet 1's A4."); + } + } +>>>>>>> Stashed changes } } diff --git a/src/EPPlus.Export.Pdf/EPPlus.Export.Pdf.csproj b/src/EPPlus.Export.Pdf/EPPlus.Export.Pdf.csproj index 87beb9760a..eb14a2dee8 100644 --- a/src/EPPlus.Export.Pdf/EPPlus.Export.Pdf.csproj +++ b/src/EPPlus.Export.Pdf/EPPlus.Export.Pdf.csproj @@ -7,7 +7,7 @@ false true EPPlus.Export.Pdf.snk - latest + latest diff --git a/src/EPPlus.Interfaces/EPPlus.Interfaces.csproj b/src/EPPlus.Interfaces/EPPlus.Interfaces.csproj index 6ae5284ef3..346b81e4e2 100644 --- a/src/EPPlus.Interfaces/EPPlus.Interfaces.csproj +++ b/src/EPPlus.Interfaces/EPPlus.Interfaces.csproj @@ -1,9 +1,9 @@  net10.0;net9.0;net8.0;netstandard2.1;netstandard2.0;net462 - 8.6.2.0 - 8.6.2.0 - 8.6.2 + 9.0.0.0 + 9.0.0.0 + 9.0.0-preview true license.md git diff --git a/src/EPPlus.System.Drawing/EPPlus.System.Drawing.csproj b/src/EPPlus.System.Drawing/EPPlus.System.Drawing.csproj index 2560da378d..356ea70064 100644 --- a/src/EPPlus.System.Drawing/EPPlus.System.Drawing.csproj +++ b/src/EPPlus.System.Drawing/EPPlus.System.Drawing.csproj @@ -2,9 +2,9 @@ net10.0;net9.0;net8.0;netstandard2.1;netstandard2.0;net462 - 8.6.2.0 - 8.6.2.0 - 8.6.2 + 9.0.0.0 + 9.0.0.0 + 9.0.0-preview true license.md true diff --git a/src/EPPlus/Drawing/Chart/ExcelChartAxis.cs b/src/EPPlus/Drawing/Chart/ExcelChartAxis.cs index e5fec2f806..89c895e1e2 100644 --- a/src/EPPlus/Drawing/Chart/ExcelChartAxis.cs +++ b/src/EPPlus/Drawing/Chart/ExcelChartAxis.cs @@ -132,6 +132,9 @@ public string Format } } } + /// + /// Returns the number format code for the axis. If the axis has no format code, it will return the format code of the first series in the chart that uses this axis. If no series has a format code, it will return an empty string. + /// public string FormatOrFirstValueFormat { get @@ -144,19 +147,11 @@ public string FormatOrFirstValueFormat { if(ct.XAxis.Id == Id) { - foreach (var serie in ct.Series) - { - if(string.IsNullOrEmpty(serie.XSeries)) - { - continue; - } - var adr = new ExcelAddressBase(serie.XSeries); - var ws = wb.Worksheets[adr.WorkSheetName]; - if(ws!=null) - { - return ws.Cells[adr.Address].Style.Numberformat.Format; - } - } + return GetFormatFromSeries(ct, true); + } + else if (ct.YAxis.Id == Id) + { + return GetFormatFromSeries(ct, false); } } return ""; @@ -165,6 +160,26 @@ public string FormatOrFirstValueFormat return f; } } + + private string GetFormatFromSeries(ExcelChart ct, bool isXAxis) + { + foreach (var serie in ct.Series) + { + var address = isXAxis ? serie.XSeries : serie.Series; + if (string.IsNullOrEmpty(address)) + { + continue; + } + var adr = new ExcelAddressBase(address); + var ws = _chart.WorkSheet.Workbook.Worksheets[adr.WorkSheetName]; + if (ws != null) + { + return ws.Cells[adr.Address].FirstOrDefault(x=>string.IsNullOrEmpty(x.Style.Numberformat.Format)==false).Style.Numberformat.Format ?? ""; + } + } + return ""; + } + /// /// The Numberformats are linked to the source data. /// diff --git a/src/EPPlus/Drawing/Renderer/Chart/Axis/DateAxisScaleCalculator.cs b/src/EPPlus/Drawing/Renderer/Chart/Axis/DateAxisScaleCalculator.cs index 30551fb198..e0e3c9802f 100644 --- a/src/EPPlus/Drawing/Renderer/Chart/Axis/DateAxisScaleCalculator.cs +++ b/src/EPPlus/Drawing/Renderer/Chart/Axis/DateAxisScaleCalculator.cs @@ -36,7 +36,8 @@ internal static AxisScale Calculate(double dataMin, double dataMax, AxisOptions Max = axisMax, MajorInterval = majorValue, MajorDateUnit = majorUnit, - MinorDateUnit = majorUnit + MinorDateUnit = majorUnit, + TextOrientation = eTextOrientation.Horizontal }; } // Excel's date serial epoch: Dec 30, 1899 @@ -315,46 +316,89 @@ internal static AxisScale CalculateByWidthAllowDiagonal(List values, dou else { GetStartInterval(values.Count, max - min, out interval, out unit); - //Get interval for maximum width with vertical text. - while (FitAsVerticalDiagonalText(min, max, interval, unit, res.Height, res.Height * 0.3, plotAreaWidth) == false) + if(ax.TextBody.Rotation.HasValue==false || ax.TextBody.Rotation==-1000) { - AddIntervall(ref interval, ref unit); - } + //Get interval for maximum width with vertical text. + while (FitAsVerticalDiagonalText(min, max, interval, unit, res.Height, res.Height * 0.3, plotAreaWidth) == false) + { + AddIntervall(ref interval, ref unit); + } - //Get max text width when using diagonal text - var width = mf.Size * Math.Sqrt(2); - var margin = mf.Size * 0.5; + //Get max text width when using diagonal text + var width = mf.Size * Math.Sqrt(2); + var margin = mf.Size * 0.5; - if (FitAsVerticalDiagonalText(min, max, interval, unit, width, margin, plotAreaWidth)) //Check diagonal - { - if (FitAsHorizontalText(tm, options, min, max, interval, unit, res.Height, plotAreaWidth)) //Check horizontal + if (FitAsVerticalDiagonalText(min, max, interval, unit, width, margin, plotAreaWidth)) //Check diagonal { - return new AxisScale() + if (FitAsHorizontalText(tm, options, min, max, interval, unit, res.Height, plotAreaWidth)) //Check horizontal + { + return new AxisScale() + { + MajorInterval = interval, + MinorInterval = 1, + MinorDateUnit = unit, + MajorDateUnit = unit, + Min = min, + Max = max, + TextOrientation = eTextOrientation.Horizontal, + }; + } + else { - MajorInterval = interval, - MinorInterval = 1, - MinorDateUnit = unit, - MajorDateUnit = unit, - Min = min, - Max = max, - TextOrientation = eTextOrientation.Horizontal, - }; + return new AxisScale() + { + MajorInterval = interval, + MinorInterval = 1, + MinorDateUnit = unit, + MajorDateUnit = unit, + Min = min, + Max = max, + TextOrientation = eTextOrientation.Diagonal, + }; + } } - else + } + else + { + var rot = ax.TextBody.Rotation.Value % 360; + var sin = Math.Sin(MathHelper.Radians(rot)); + var cos = Math.Cos(MathHelper.Radians(rot)); + var width = res.Width * cos + res.Height * sin; + //Get interval for maximum width with vertical text. + while (FitAsVerticalDiagonalText(min, max, interval, unit, width, width * 0.3, plotAreaWidth) == false) { - return new AxisScale() - { - MajorInterval = interval, - MinorInterval = 1, - MinorDateUnit = unit, - MajorDateUnit = unit, - Min = min, - Max = max, - TextOrientation = eTextOrientation.Diagonal, - }; + AddIntervall(ref interval, ref unit); } + eTextOrientation orientation; + switch(rot) + { + case 45: + case 315: + orientation = eTextOrientation.Diagonal; + break; + case 90: + case 270: + orientation = eTextOrientation.Vertical; + break; + case 0: + case 180: + orientation = eTextOrientation.Horizontal; + break; + default: + orientation = eTextOrientation.Custom; + break; + } + return new AxisScale() + { + MajorInterval = interval, + MinorInterval = 1, + MinorDateUnit = unit, + MajorDateUnit = unit, + Min = min, + Max = max, + TextOrientation = orientation, + }; } - } return new AxisScale() @@ -464,7 +508,6 @@ private static bool FitAsHorizontalText(ITextMeasurer tm, AxisOptions options, d var horizontalWidth = 0D; var nf = options.NumberFormat; var mf = options.Axis.Font.GetMeasureFont(); - var angMult = Math.Sin(MathHelper.Radians(45)); while (date < maxDate) { var textWidth = tm.MeasureText(date.ToString(), mf).Width; diff --git a/src/EPPlus/Drawing/Renderer/Chart/ChartAxisRenderer.cs b/src/EPPlus/Drawing/Renderer/Chart/ChartAxisRenderer.cs index f076910016..e97cc2a379 100644 --- a/src/EPPlus/Drawing/Renderer/Chart/ChartAxisRenderer.cs +++ b/src/EPPlus/Drawing/Renderer/Chart/ChartAxisRenderer.cs @@ -74,9 +74,9 @@ internal ChartAxisRenderer(ChartRenderer sc, ExcelChartAxisStandard ax) : base(s Min = min ?? 0D; Max = max ?? (Values.Count > 0 ? ConvertUtil.GetValueDouble(Values[Values.Count - 1], false, true) : 0D); MajorUnit = majorUnit ?? 1; - if (AutoAxisType == eAxisType.Cat || (dateUnit.HasValue && dateUnit == eTimeUnit.Days)) + if (AutoAxisType == eAxisType.Cat || IsDateAutoAxis || IsDateScale) { - MinorUnit = 1; + MinorUnit = ax.MinorUnit ?? 1; } else { @@ -357,10 +357,15 @@ private List GetAxisValueTextBoxes() maxWidth = (Rectangle.Width + Rectangle.Height) / COS45; maxHeight = ChartRenderer.ChartArea.Rectangle.Height / 3; //TODO: Check this value. break; - default: + case eTextOrientation.Horizontal: maxWidth = Rectangle.Width / AxisValues.Count; maxHeight = ChartRenderer.ChartArea.Rectangle.Height / 3; //TODO: Check this value. break; + default: // custom + var radRot = MathHelper.Radians(Axis.TextBody.Rotation.Value); + maxWidth = (Rectangle.Width * Math.Sin(radRot) + Rectangle.Height * Math.Cos(radRot)) ; + maxHeight = ChartRenderer.ChartArea.Rectangle.Height / 3; //TODO: Check this value. + break; } } double widest=0; @@ -502,7 +507,7 @@ private List GetAxisValueTextBoxes() var min = ConvertUtil.GetValueDouble(Values[0]); var max = ConvertUtil.GetValueDouble(Values.Last()); var minUnit = (max - min) / MinorUnit; - majorWidth = (min - min) / minUnit; + majorWidth = Rectangle.Width / minUnit; } else { @@ -548,7 +553,18 @@ private List GetAxisValueTextBoxes() { if (!(Axis.CrossingAxis == null || Axis.CrossingAxis.CrossBetween == eCrossBetween.MidCat)) { - 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 = Rectangle.Width / minUnit; + } + else + { + majorWidth = Rectangle.Width / AxisValues.Count; + } foreach (var tb in ret) { tb.Left += majorWidth / 2; diff --git a/src/EPPlus/Drawing/Renderer/Chart/ChartPlotareaRenderer.cs b/src/EPPlus/Drawing/Renderer/Chart/ChartPlotareaRenderer.cs index cfabf254d4..afe94de389 100644 --- a/src/EPPlus/Drawing/Renderer/Chart/ChartPlotareaRenderer.cs +++ b/src/EPPlus/Drawing/Renderer/Chart/ChartPlotareaRenderer.cs @@ -195,7 +195,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; + return (Chart.Legend?.Position == eLegendPosition.Top ? ChartRenderer.Legend.Rectangle.Bounds.Bottom : ChartRenderer.Title?.Rectangle?.GlobalBottom ?? TopMargin) + haHeight; } private ChartAxisRenderer GetAxisActualByPosition(eActualAxisPosition pos) diff --git a/src/EPPlus/Drawing/Renderer/Chart/eTextOrientation.cs b/src/EPPlus/Drawing/Renderer/Chart/eTextOrientation.cs index 8677f954b9..9fa95cef02 100644 --- a/src/EPPlus/Drawing/Renderer/Chart/eTextOrientation.cs +++ b/src/EPPlus/Drawing/Renderer/Chart/eTextOrientation.cs @@ -17,5 +17,6 @@ internal enum eTextOrientation Horizontal, Diagonal, Vertical, + Custom } } \ No newline at end of file diff --git a/src/EPPlus/Drawing/Renderer/ChartRenderer.cs b/src/EPPlus/Drawing/Renderer/ChartRenderer.cs index 3f17804a52..96c429b9d3 100644 --- a/src/EPPlus/Drawing/Renderer/ChartRenderer.cs +++ b/src/EPPlus/Drawing/Renderer/ChartRenderer.cs @@ -118,7 +118,7 @@ private void SetAxisPositionsFromPlotarea() PlaceHorizontalAxis(HorizontalAxis, false); //Make sure the horizontal axis is moved up if the vertical axis has a negative minimum value, so that the 0 value is at the correct position. - if (HorizontalAxis.Axis.TickLabelPosition == eTickLabelPosition.NextTo && VerticalAxis.Axis.AxisType == eAxisType.Val && VerticalAxis.Min < 0D) + if (HorizontalAxis.Axis.TickLabelPosition == eTickLabelPosition.NextTo && VerticalAxis.Axis.AxisType == eAxisType.Val && VerticalAxis.Min < 0D && HorizontalAxis.Axis.Crosses == eCrosses.AutoZero) { var newtop = VerticalAxis.GetPositionInPlotarea(0D) + Plotarea.Group.Top; var topDiff = HorizontalAxis.Rectangle.Top - newtop; diff --git a/src/EPPlus/EPPlus.csproj b/src/EPPlus/EPPlus.csproj index 1719f8fcc2..73c0fd93ee 100644 --- a/src/EPPlus/EPPlus.csproj +++ b/src/EPPlus/EPPlus.csproj @@ -1,10 +1,12 @@  net8.0;net9.0;net10.0;netstandard2.1;netstandard2.0;net462 - 9.0.0.0 - 9.0.0.0 - 9.0.0-preview + 9.0.0.1 + 9.0.0.1 + 9.0.0-preview2 true + + $(TargetsForTfmSpecificBuildOutput);IncludeReferencedProjectsInPackage https://epplussoftware.com EPPlus Software AB license.md @@ -26,20 +28,16 @@ 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. + * Minor bug fixes. See https://epplussoftware.com/Developers/MinorFeaturesAndIssues ## Version 8.6.1 * New functions: * REGEXEXTRACT, REGEXREPLACE, REGEXTEST - * Minor bug fixes. + * Minor bug fixes. See https://epplussoftware.com/Developers/MinorFeaturesAndIssues ## Version 8.6.0 * New functions: @@ -189,7 +187,7 @@ * Minor features and bug fixes. ## Version 7.4.1 - * Updated for vulnerability in System.Text.Json - Microsoft.Extensions.Configuration.Json 8.0.0 -> 8.0.1 + * Updated for vulnerability in System.Text.Json - Microsoft.Extensions.Configuration.Json 8.0.0 -> 8.0.1 ## Version 7.4.0 * Conditional formatting in Pivot Tables. @@ -590,9 +588,8 @@ 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. + 8.6.2 20260721 Minor bug fixes. See https://epplussoftware.com/Developers/MinorFeaturesAndIssues 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. @@ -736,16 +733,35 @@ EPPlusLogo.png - + - - - - - + + + + + + + - + + + + + + + + bin\$(Configuration)\$(TargetFramework)\EPPlus.xml @@ -771,18 +787,6 @@ - - - - - - - - - - - - @@ -794,7 +798,7 @@ - + @@ -841,7 +845,7 @@ - Never + Never @@ -869,10 +873,10 @@ Never - PreserveNewest + PreserveNewest PreserveNewest - + \ No newline at end of file diff --git a/src/EPPlus/XmlHelper.cs b/src/EPPlus/XmlHelper.cs index 9c6ef4d656..37148b1e43 100644 --- a/src/EPPlus/XmlHelper.cs +++ b/src/EPPlus/XmlHelper.cs @@ -1141,7 +1141,7 @@ internal int GetXmlNodeInt(string path, int defaultValue = int.MinValue) internal double GetXmlNodeAngle(string path, double defaultValue = 0) { int a = GetXmlNodeInt(path); - if (a < 0) return defaultValue; + if (a == int.MinValue) return defaultValue; return a / 60000D; } internal double GetXmlNodeEmuToPt(string path, double defaultValue = 0) From 073cec56359ac39f0bb365ef1609439045fede24 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ossian=20Edstr=C3=B6m?= Date: Wed, 26 Aug 2026 17:37:41 +0200 Subject: [PATCH 41/73] Fixed default gradient. Set userSpace to object --- .../Chart/ChartStyleFallbackTest.cs | 7 +++++++ .../RenderItems/DrawingRenderItemExtentions.cs | 9 +++++++-- .../RenderItems/Fill/DrawingRenderGradientFill.cs | 11 +++++++++++ 3 files changed, 25 insertions(+), 2 deletions(-) diff --git a/src/EPPlus.DrawingRenderer.Tests/Chart/ChartStyleFallbackTest.cs b/src/EPPlus.DrawingRenderer.Tests/Chart/ChartStyleFallbackTest.cs index 032ed9199c..a288b5ee36 100644 --- a/src/EPPlus.DrawingRenderer.Tests/Chart/ChartStyleFallbackTest.cs +++ b/src/EPPlus.DrawingRenderer.Tests/Chart/ChartStyleFallbackTest.cs @@ -430,6 +430,13 @@ public void Epp_Gen_DefaultShape() Assert.AreEqual(expectedFill, fillResult); Assert.AreEqual(expectedStroke, strokeResult); Assert.AreEqual(1d, widthResult, 0.003); + + gradientRect.Fill.Style = OfficeOpenXml.Drawing.eFillStyle.GradientFill; + + var svgGradient = gradientRect.ToSvg(); + SaveTextFileToWorkbook($"svg\\{fileName}_{ws.Name}_{gradientRect.Name}.svg", svgGradient); + + SaveAndCleanup(p); } } diff --git a/src/EPPlus/Drawing/Renderer/RenderItems/DrawingRenderItemExtentions.cs b/src/EPPlus/Drawing/Renderer/RenderItems/DrawingRenderItemExtentions.cs index 58a9ecc598..12f0c97481 100644 --- a/src/EPPlus/Drawing/Renderer/RenderItems/DrawingRenderItemExtentions.cs +++ b/src/EPPlus/Drawing/Renderer/RenderItems/DrawingRenderItemExtentions.cs @@ -223,7 +223,12 @@ internal static string GetFillNew(ExcelDrawingFillBasic fill, ExcelTheme theme, fillStr = GetAdjustmentsAndTransparency(fc, fillMode, out opacity); break; case eFillStyle.GradientFill: - gradFill = new DrawingRenderGradientFill(theme, fill.GradientFill, UserSpaceSettings.UserSpaceOnUse_Global); + gradFill = new DrawingRenderGradientFill(theme, fill.GradientFill, UserSpaceSettings.ObjectBoundingBox); + + //if(gradFill.Colors.Count == 0) + //{ + + //} break; } @@ -250,7 +255,7 @@ internal static void SetDrawingBorderPropertiesNew(this RenderItem item, ExcelTh if(gradFill != null) { //Special case as gradfill does not return a string - item.BorderGradientFill = new DrawingRenderGradientFill(theme, border.Fill.GradientFill, UserSpaceSettings.UserSpaceOnUse_Global); + item.BorderGradientFill = new DrawingRenderGradientFill(theme, border.Fill.GradientFill, UserSpaceSettings.ObjectBoundingBox); item.BorderColor = null; } else diff --git a/src/EPPlus/Drawing/Renderer/RenderItems/Fill/DrawingRenderGradientFill.cs b/src/EPPlus/Drawing/Renderer/RenderItems/Fill/DrawingRenderGradientFill.cs index b29c8ef8a1..973b959abb 100644 --- a/src/EPPlus/Drawing/Renderer/RenderItems/Fill/DrawingRenderGradientFill.cs +++ b/src/EPPlus/Drawing/Renderer/RenderItems/Fill/DrawingRenderGradientFill.cs @@ -34,6 +34,17 @@ public DrawingRenderGradientFill(ExcelTheme theme, ExcelDrawingGradientFill grad Colors.Add(c); } + //Node is empty. Add excel's hardcoded default + if(gradientFill._topNode.HasChildNodes == false) + { + var c = new GradientFillColor(0, Color.Black); + c.Opacity = 1; + var c2 = new GradientFillColor(100, Color.White); + c2.Opacity = 1; + Colors.Add(c); + Colors.Add(c2); + } + if (gradientFill.FocusPoint != null) { FocusPoint = gradientFill.FocusPoint.AsOffsetRectangle(); From b4b2c6027bafa815af3dc1a3639ff494e0a2384b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ossian=20Edstr=C3=B6m?= Date: Thu, 27 Aug 2026 09:49:26 +0200 Subject: [PATCH 42/73] Re-applied user-space-units + fixed vertLines --- .../Chart/ChartStyleFallbackTest.cs | 2 ++ .../Renderer/Chart/ChartAxisRenderer.cs | 24 ++++++++++--------- .../DrawingRenderItemExtentions.cs | 21 +++++++--------- 3 files changed, 23 insertions(+), 24 deletions(-) diff --git a/src/EPPlus.DrawingRenderer.Tests/Chart/ChartStyleFallbackTest.cs b/src/EPPlus.DrawingRenderer.Tests/Chart/ChartStyleFallbackTest.cs index a288b5ee36..c83e9410ee 100644 --- a/src/EPPlus.DrawingRenderer.Tests/Chart/ChartStyleFallbackTest.cs +++ b/src/EPPlus.DrawingRenderer.Tests/Chart/ChartStyleFallbackTest.cs @@ -382,6 +382,8 @@ public void Epp_Gen_DefaultLine() Assert.AreEqual(expectedStroke, strokeResult); Assert.AreEqual(1d, widthResult, 0.003); } + + SaveAndCleanup(p); } } diff --git a/src/EPPlus/Drawing/Renderer/Chart/ChartAxisRenderer.cs b/src/EPPlus/Drawing/Renderer/Chart/ChartAxisRenderer.cs index e97cc2a379..c0b69b3579 100644 --- a/src/EPPlus/Drawing/Renderer/Chart/ChartAxisRenderer.cs +++ b/src/EPPlus/Drawing/Renderer/Chart/ChartAxisRenderer.cs @@ -296,19 +296,21 @@ public override void AppendRenderItems(List renderItems) internal void AddTickmarksAndValues(List DefItems) { - if (Axis.Deleted == true) return; - if (Axis.MajorTickMark != eAxisTickMark.None) + if (Axis.Deleted == false) { - MajorTickMarkPositions = AddTickmarks(MajorUnit, MajorDateUnit, double.NaN, 4D.PixelToPoint(), Axis.MajorTickMark); - } + if (Axis.MajorTickMark != eAxisTickMark.None) + { + MajorTickMarkPositions = AddTickmarks(MajorUnit, MajorDateUnit, double.NaN, 4D.PixelToPoint(), Axis.MajorTickMark); + } - if (Axis.MinorTickMark != eAxisTickMark.None && MinorUnit < MajorUnit) - { - MinorTickMarkPositions = AddTickmarks(MinorUnit, MajorDateUnit, MajorUnit, 2D.PixelToPoint(), Axis.MinorTickMark); - } - else - { - MinorTickMarkPositions = null; + if (Axis.MinorTickMark != eAxisTickMark.None && MinorUnit < MajorUnit) + { + MinorTickMarkPositions = AddTickmarks(MinorUnit, MajorDateUnit, MajorUnit, 2D.PixelToPoint(), Axis.MinorTickMark); + } + else + { + MinorTickMarkPositions = null; + } } if(Axis.HasMajorGridlines) diff --git a/src/EPPlus/Drawing/Renderer/RenderItems/DrawingRenderItemExtentions.cs b/src/EPPlus/Drawing/Renderer/RenderItems/DrawingRenderItemExtentions.cs index 12f0c97481..2f58bbf2d3 100644 --- a/src/EPPlus/Drawing/Renderer/RenderItems/DrawingRenderItemExtentions.cs +++ b/src/EPPlus/Drawing/Renderer/RenderItems/DrawingRenderItemExtentions.cs @@ -59,8 +59,8 @@ internal static void SetDrawingPropertiesFillBasic(this RenderItem item, ExcelTh 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); + //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, gradientUserSpaceOnUse); if(gradFill != null) { @@ -192,7 +192,7 @@ private static string GetAdjustmentsAndTransparency(Color fc, PathFillMode color return "#" + fc.ToArgb().ToString("x8").Substring(2); } - internal static string GetFillNew(ExcelDrawingFillBasic fill, ExcelTheme theme, ExcelDrawingColorManager reference, PathFillMode fillMode, out double opacity, Func GetHardCodedDefaultForItem, out DrawingRenderGradientFill gradFill) + internal static string GetFillNew(ExcelDrawingFillBasic fill, ExcelTheme theme, ExcelDrawingColorManager reference, PathFillMode fillMode, out double opacity, Func GetHardCodedDefaultForItem, out DrawingRenderGradientFill gradFill, UserSpaceSettings gradientUserSpaceOnUse = UserSpaceSettings.UserSpaceOnUse_Global) { string fillStr = string.Empty; gradFill = null; @@ -223,12 +223,7 @@ internal static string GetFillNew(ExcelDrawingFillBasic fill, ExcelTheme theme, fillStr = GetAdjustmentsAndTransparency(fc, fillMode, out opacity); break; case eFillStyle.GradientFill: - gradFill = new DrawingRenderGradientFill(theme, fill.GradientFill, UserSpaceSettings.ObjectBoundingBox); - - //if(gradFill.Colors.Count == 0) - //{ - - //} + gradFill = new DrawingRenderGradientFill(theme, fill.GradientFill, gradientUserSpaceOnUse); break; } @@ -236,7 +231,7 @@ internal static string GetFillNew(ExcelDrawingFillBasic fill, ExcelTheme theme, return fillStr; } - internal static void SetDrawingBorderPropertiesNew(this RenderItem item, ExcelTheme theme, ExcelChartStyleColorManager reference, ExcelDrawingBorder border, double opacity, bool hasBorder, Func GetHardCodedDefaultForItem) + internal static void SetDrawingBorderPropertiesNew(this RenderItem item, ExcelTheme theme, ExcelChartStyleColorManager reference, ExcelDrawingBorder border, double opacity, bool hasBorder, Func GetHardCodedDefaultForItem, UserSpaceSettings gradientUserSpaceOnUse = UserSpaceSettings.UserSpaceOnUse_Global) { string fillColorStr = null; DrawingRenderGradientFill gradFill = null; @@ -255,7 +250,7 @@ internal static void SetDrawingBorderPropertiesNew(this RenderItem item, ExcelTh if(gradFill != null) { //Special case as gradfill does not return a string - item.BorderGradientFill = new DrawingRenderGradientFill(theme, border.Fill.GradientFill, UserSpaceSettings.ObjectBoundingBox); + item.BorderGradientFill = new DrawingRenderGradientFill(theme, border.Fill.GradientFill, gradientUserSpaceOnUse); item.BorderColor = null; } else @@ -284,9 +279,9 @@ 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; - GetFillColor(theme, null, color, item.BorderColorSource, out opacity, nullColor ?? theme.ColorScheme.Dark1.GetColor()); + //GetFillColor(theme, null, color, item.BorderColorSource, out opacity, nullColor ?? theme.ColorScheme.Dark1.GetColor(), styleId); opacity = double.NaN; - SetDrawingBorderPropertiesNew(item, theme, color, border, opacity.Value, hasBorder, () => { return nullColor; }); + SetDrawingBorderPropertiesNew(item, theme, color, border, opacity.Value, hasBorder, () => { return nullColor; }, gradientUserSpaceOnUse); //if (border == null) //{ // if (hasBorder) From 2d709bd3f0758ae3b45154efd246433e5f1d56fc Mon Sep 17 00:00:00 2001 From: swmal <{ID}+username}@users.noreply.github.com> Date: Fri, 28 Aug 2026 09:34:06 +0200 Subject: [PATCH 43/73] #2486 - Increased AutoFilterArrowWidthPixels from 15 to 19 --- src/EPPlus/Core/AutofitHelper.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/EPPlus/Core/AutofitHelper.cs b/src/EPPlus/Core/AutofitHelper.cs index 27b643320d..73a57e4470 100644 --- a/src/EPPlus/Core/AutofitHelper.cs +++ b/src/EPPlus/Core/AutofitHelper.cs @@ -24,7 +24,7 @@ namespace OfficeOpenXml.Core internal class AutofitHelper { // Approximate width in pixels (at 96 DPI) of the autofilter dropdown arrow rendered by Excel. - private const double AutoFilterArrowWidthPixels = 15d; + private const double AutoFilterArrowWidthPixels = 19d; private ExcelRangeBase _range; ITextMeasurer _genericMeasurer = new GenericFontMetricsTextMeasurer(); MeasurementFont _nonExistingFont = new MeasurementFont() { FontFamily = FontSize.NonExistingFont }; From 2600db1121671725587b24c68af3f30dce5f7449 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ossian=20Edstr=C3=B6m?= Date: Fri, 28 Aug 2026 11:04:52 +0200 Subject: [PATCH 44/73] Edge-case tempfix --- .../Chart/LineChartToSvgTests.cs | 2 +- src/EPPlus.DrawingRenderer/Svg/Core/SvgShapeRenderer.cs | 8 ++++++++ src/EPPlus/Drawing/Renderer/Chart/ChartAxisRenderer.cs | 5 +++++ 3 files changed, 14 insertions(+), 1 deletion(-) diff --git a/src/EPPlus.DrawingRenderer.Tests/Chart/LineChartToSvgTests.cs b/src/EPPlus.DrawingRenderer.Tests/Chart/LineChartToSvgTests.cs index 2e520346ee..04471a718b 100644 --- a/src/EPPlus.DrawingRenderer.Tests/Chart/LineChartToSvgTests.cs +++ b/src/EPPlus.DrawingRenderer.Tests/Chart/LineChartToSvgTests.cs @@ -24,7 +24,7 @@ public void GenerateSvgForLineCharts_sheet1() //var svg = c.ToSvg(); //SaveTextFileToWorkbook($"svg\\ChartForSvg_ind{ix++}.svg", svg); - for (int i = 0; i < ws.Drawings.Count; i++) + for (int i = 4; i < ws.Drawings.Count; i++) { var c = ws.Drawings[i]; var svg = c.ToSvg(); diff --git a/src/EPPlus.DrawingRenderer/Svg/Core/SvgShapeRenderer.cs b/src/EPPlus.DrawingRenderer/Svg/Core/SvgShapeRenderer.cs index 786a22ba98..2259c87d6b 100644 --- a/src/EPPlus.DrawingRenderer/Svg/Core/SvgShapeRenderer.cs +++ b/src/EPPlus.DrawingRenderer/Svg/Core/SvgShapeRenderer.cs @@ -588,6 +588,8 @@ private string GetXy(RenderItem item, UserSpaceSettings userSpace, double? angle { if (userSpace != UserSpaceSettings.ObjectBoundingBox) { + var angle2 = angle % 360; + double theta = MathHelper.Radians((angle ?? 90) % 360); double l, t; @@ -622,6 +624,12 @@ private string GetXy(RenderItem item, UserSpaceSettings userSpace, double? angle double x1 = cx - halfX, y1 = cy - halfY; double x2 = cx + halfX, y2 = cy + halfY; + if(item.DefId == "xGridLine") + { + x1 = item.Bounds.Left; + x2 = w - x1; + } + return $" x1=\"{(x1).PointToPixelString("0.00")}\" x2=\"{(x2).PointToPixelString("0.00")}\" y1=\"{y1.PointToPixelString("0.00")}\" y2=\"{y2.PointToPixelString("0.00")}\""; } return ""; diff --git a/src/EPPlus/Drawing/Renderer/Chart/ChartAxisRenderer.cs b/src/EPPlus/Drawing/Renderer/Chart/ChartAxisRenderer.cs index c0b69b3579..256c330525 100644 --- a/src/EPPlus/Drawing/Renderer/Chart/ChartAxisRenderer.cs +++ b/src/EPPlus/Drawing/Renderer/Chart/ChartAxisRenderer.cs @@ -891,6 +891,11 @@ private List AddGridlines(double units, double parentUnit, ExcelDraw tm.Y1 = y1; tm.X2 = x2; tm.Y2 = y2; + + if(id == "xGridLine") + { + tm.Bounds.Width = pa.Rectangle.Width; + } //var lineWidth = lineItem.Width <= 0 ? 0.75 : lineItem.Width; tm.SetDrawingPropertiesBorder(ChartRenderer.Theme, lineItem, styleEntry?.BorderReference.Color, true, ChartRenderer.Theme.ColorScheme.Dark1.GetColor(), 0.75); From aa5bc8f4aeccd962eba8923c130e1562a4e5a6db Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ossian=20Edstr=C3=B6m?= Date: Fri, 28 Aug 2026 11:26:43 +0200 Subject: [PATCH 45/73] Added comment removed leftover line --- src/EPPlus.DrawingRenderer/Svg/Core/SvgShapeRenderer.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/EPPlus.DrawingRenderer/Svg/Core/SvgShapeRenderer.cs b/src/EPPlus.DrawingRenderer/Svg/Core/SvgShapeRenderer.cs index 2259c87d6b..c45b1f9c05 100644 --- a/src/EPPlus.DrawingRenderer/Svg/Core/SvgShapeRenderer.cs +++ b/src/EPPlus.DrawingRenderer/Svg/Core/SvgShapeRenderer.cs @@ -588,8 +588,6 @@ private string GetXy(RenderItem item, UserSpaceSettings userSpace, double? angle { if (userSpace != UserSpaceSettings.ObjectBoundingBox) { - var angle2 = angle % 360; - double theta = MathHelper.Radians((angle ?? 90) % 360); double l, t; @@ -626,6 +624,8 @@ private string GetXy(RenderItem item, UserSpaceSettings userSpace, double? angle if(item.DefId == "xGridLine") { + //If global, has to stretch the whole length. + //This case is special because it stretches in the same direction as the attempted gradient x1 = item.Bounds.Left; x2 = w - x1; } From 02ea748628553cd1d91b5b2fd6a9f92b5ac8689a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ossian=20Edstr=C3=B6m?= Date: Fri, 28 Aug 2026 14:55:53 +0200 Subject: [PATCH 46/73] Applied defaults except for dataPts and upDownBars --- .../Chart/LineChartToSvgTests.cs | 2 +- .../Renderer/Chart/ChartAreaRenderer.cs | 24 +- .../Renderer/Chart/ChartAxisRenderer.cs | 26 +- .../Renderer/Chart/ChartLegendRenderer.cs | 8 +- .../Renderer/Chart/ChartPlotareaRenderer.cs | 29 +- .../ChartElementStyleTables.cs | 329 ------------------ .../Renderer/Chart/ChartTitleRenderer.cs | 6 +- .../ChartTypeDrawers/ChartErrorBarRenderer.cs | 43 ++- .../Chart/ChartTypeDrawers/ChartTypeDrawer.cs | 2 +- .../ChartTypeDrawers/LineChartTypeDrawer.cs | 11 +- .../Trendlines/ChartTrendlineRenderer.cs | 40 ++- .../Renderer/Chart/LineMarkerHelper.cs | 6 +- src/EPPlus/Drawing/Renderer/ChartRenderer.cs | 65 +--- .../DrawingRenderItemExtentions.cs | 124 +++---- src/EPPlus/Drawing/Renderer/ShapeRenderer.cs | 4 +- src/EPPlus/EPPlus.csproj | 9 +- 16 files changed, 200 insertions(+), 528 deletions(-) delete mode 100644 src/EPPlus/Drawing/Renderer/Chart/ChartStyleDefaults/ChartElementStyleTables.cs diff --git a/src/EPPlus.DrawingRenderer.Tests/Chart/LineChartToSvgTests.cs b/src/EPPlus.DrawingRenderer.Tests/Chart/LineChartToSvgTests.cs index 04471a718b..2e520346ee 100644 --- a/src/EPPlus.DrawingRenderer.Tests/Chart/LineChartToSvgTests.cs +++ b/src/EPPlus.DrawingRenderer.Tests/Chart/LineChartToSvgTests.cs @@ -24,7 +24,7 @@ public void GenerateSvgForLineCharts_sheet1() //var svg = c.ToSvg(); //SaveTextFileToWorkbook($"svg\\ChartForSvg_ind{ix++}.svg", svg); - for (int i = 4; i < ws.Drawings.Count; i++) + for (int i = 0; i < ws.Drawings.Count; i++) { var c = ws.Drawings[i]; var svg = c.ToSvg(); diff --git a/src/EPPlus/Drawing/Renderer/Chart/ChartAreaRenderer.cs b/src/EPPlus/Drawing/Renderer/Chart/ChartAreaRenderer.cs index 4168999ec0..f712910270 100644 --- a/src/EPPlus/Drawing/Renderer/Chart/ChartAreaRenderer.cs +++ b/src/EPPlus/Drawing/Renderer/Chart/ChartAreaRenderer.cs @@ -13,13 +13,13 @@ Date Author Change using EPPlus.DrawingRenderer.RenderItems; using EPPlus.DrawingRenderer.Svg; using OfficeOpenXml.Drawing; -using OfficeOpenXml.Drawing.Renderer.Chart.ChartElementStyleTables; +using OfficeOpenXml.Drawing.Renderer.Chart.Defaults; using System.Collections.Generic; using System.Drawing; namespace EPPlusImageRenderer.Svg { - internal class ChartAreaRenderer : ChartDrawingObjectWithDefaults + internal class ChartAreaRenderer : ChartDrawingDefaultObject { public ChartAreaRenderer(ChartRenderer sc, SvgRenderOptions options) : base(sc) { @@ -35,20 +35,19 @@ public ChartAreaRenderer(ChartRenderer sc, SvgRenderOptions options) : base(sc) Rectangle = new RectRenderItem(sc.Bounds); } - internal override Color? DefaultFillColor { get => ChartRenderer.Theme.ColorScheme.Light1.GetColor(); } - internal override Color? DefaultBorderColor - { - get - { - return Color.FromArgb(0x89, 0x89, 0x89); - } - } + internal override Color? DefaultFillColor { get => GetDefaultFillColor(); } + internal override Color? DefaultBorderColor { get => GetDefaultBorderColor(); } public override void AppendRenderItems(List renderItems) { renderItems.Add(Rectangle); } + internal override Color? GetDefaultFillColor() + { + return GetDefaultFillColorForElement(ChartElement.ChartArea, (int)Chart.Style); + } + internal override Color? GetDefaultBorderColor() { //We only get here if the node is null or empty @@ -57,10 +56,5 @@ public override void AppendRenderItems(List renderItems) //var themedLine = GetThemedLine(ChartElement.ChartArea, (int)Chart.Style, out Color? lineCol); return lineColor; } - - internal override Color? GetDefaultFillColor() - { - return GetDefaultFillColorForElement(ChartElement.ChartArea, (int)Chart.Style); - } } } diff --git a/src/EPPlus/Drawing/Renderer/Chart/ChartAxisRenderer.cs b/src/EPPlus/Drawing/Renderer/Chart/ChartAxisRenderer.cs index 256c330525..edf1fa9873 100644 --- a/src/EPPlus/Drawing/Renderer/Chart/ChartAxisRenderer.cs +++ b/src/EPPlus/Drawing/Renderer/Chart/ChartAxisRenderer.cs @@ -25,6 +25,7 @@ Date Author Change using OfficeOpenXml.Drawing; using OfficeOpenXml.Drawing.Chart; using OfficeOpenXml.Drawing.Chart.Style; +using OfficeOpenXml.Drawing.Renderer.Chart.Defaults; using OfficeOpenXml.Drawing.Renderer.TextBox; using OfficeOpenXml.FormulaParsing.Excel.Functions.DateAndTime; using OfficeOpenXml.FormulaParsing.Excel.Functions.Logical; @@ -36,12 +37,13 @@ Date Author Change using OfficeOpenXml.Utils.TypeConversion; using System; using System.Collections.Generic; +using System.Drawing; using System.Linq; using System.Security.AccessControl; namespace EPPlusImageRenderer.Svg { - internal class ChartAxisRenderer : ChartDrawingObject, IDrawingChartAxis + internal class ChartAxisRenderer : ChartDrawingDefaultObject, IDrawingChartAxis { private const double COS45 = 0.70710678118654757; //Constant for Math.Sin(Math.PI / 4) --45 degrees @@ -120,7 +122,7 @@ internal ChartAxisRenderer(ChartRenderer sc, ExcelChartAxisStandard ax) : base(s Rectangle.FillColor = "none"; Line = new LineRenderItem(Rectangle.Bounds); - Line.SetDrawingPropertiesBorder(ChartRenderer.Theme, ax.Border, sc.Chart.StyleManager.Style?.Title.BorderReference.Color, ax.Border.IsEmpty==true || ax.Border.Fill.Style != eFillStyle.NoFill, DefaultBorderColor, 1); + Line.SetDrawingPropertiesBorder(ChartRenderer.Theme, ax.Border, sc.Chart.StyleManager.Style?.Title.BorderReference.Color, ax.Border.IsEmpty==true || ax.Border.Fill.Style != eFillStyle.NoFill, GetDefaultBorderColor, 1); if(Line.BorderWidth < 1) { Line.BorderWidth = 1; @@ -786,7 +788,7 @@ private List AddTickmarks(double units, eTimeUnit? dateUnit, dou tm.Y1 = y1; tm.X2 = x2; tm.Y2 = y2; - tm.SetDrawingPropertiesBorder(ChartRenderer.Theme, Axis.Border, axisStyle?.BorderReference.Color, true, DefaultBorderColor, 0.75); + tm.SetDrawingPropertiesBorder(ChartRenderer.Theme, Axis.Border, axisStyle?.BorderReference.Color, true, GetDefaultBorderColor, 0.75); if(tm.BorderWidth < 0.75) //Excel seems to have this as minimum width for tick marks, so we enforce it here to make sure they are visible. { tm.BorderWidth = 0.75; @@ -835,7 +837,7 @@ private List AddGridlines(double units, double parentUnit, ExcelDraw var pa = ChartRenderer.Plotarea; var diff = Max - min; - List points = new List(); + List points = new List(); var group = ChartRenderer.Plotarea.Group; for (double d = min; d <= Max; d += units) { @@ -846,12 +848,12 @@ private List AddGridlines(double units, double parentUnit, ExcelDraw { case eAxisPosition.Left: case eAxisPosition.Right: - points.Add(new Point(0f, (float)(pa.Rectangle.Height - ((d - min) / diff * pa.Rectangle.Height)))); + points.Add(new EPPlus.Graphics.Point(0f, (float)(pa.Rectangle.Height - ((d - min) / diff * pa.Rectangle.Height)))); break; case eAxisPosition.Top: case eAxisPosition.Bottom: var xValue = (float)(((d - min) / diff * pa.Rectangle.Width)); - points.Add(new Point(xValue, 0f)); + points.Add(new EPPlus.Graphics.Point(xValue, 0f)); break; default: throw new InvalidOperationException("Invalid axis position."); @@ -897,7 +899,7 @@ private List AddGridlines(double units, double parentUnit, ExcelDraw tm.Bounds.Width = pa.Rectangle.Width; } //var lineWidth = lineItem.Width <= 0 ? 0.75 : lineItem.Width; - tm.SetDrawingPropertiesBorder(ChartRenderer.Theme, lineItem, styleEntry?.BorderReference.Color, true, ChartRenderer.Theme.ColorScheme.Dark1.GetColor(), 0.75); + tm.SetDrawingPropertiesBorder(ChartRenderer.Theme, lineItem, styleEntry?.BorderReference.Color, true, GetDefaultBorderColor, 0.75); tm.DefId = id; @@ -1233,5 +1235,15 @@ private bool ShouldHavePadding() { return Axis.AxisType == eAxisType.Val || (Chart.IsTypeLine() && Axis.AxisType == eAxisType.Date); } + + internal override Color? GetDefaultFillColor() + { + return GetDefaultFillColorForElement(ChartElement.Axis, (int)Chart.Style); + } + + internal override Color? GetDefaultBorderColor() + { + return GetDefaultBorderColorForElement(ChartElement.Axis, (int)Chart.Style); + } } } \ No newline at end of file diff --git a/src/EPPlus/Drawing/Renderer/Chart/ChartLegendRenderer.cs b/src/EPPlus/Drawing/Renderer/Chart/ChartLegendRenderer.cs index dccce0305f..0fa36b5f58 100644 --- a/src/EPPlus/Drawing/Renderer/Chart/ChartLegendRenderer.cs +++ b/src/EPPlus/Drawing/Renderer/Chart/ChartLegendRenderer.cs @@ -84,7 +84,7 @@ internal ChartLegendRenderer(ChartRenderer sc) : base(sc) } Rectangle.SetDrawingPropertiesFill(sc.Theme, l.Fill, sc.Chart.StyleManager.Style?.Title.FillReference.Color, UserSpaceSettings.UserSpaceOnUse_Global, DefaultFillColor); - Rectangle.SetDrawingPropertiesBorder(sc.Theme, l.Border, sc.Chart.StyleManager.Style?.Legend.BorderReference.Color, l.Border.Fill.Style != eFillStyle.NoFill, DefaultBorderColor, 0.75); + Rectangle.SetDrawingPropertiesBorder(sc.Theme, l.Border, sc.Chart.StyleManager.Style?.Legend.BorderReference.Color, l.Border.Fill.Style != eFillStyle.NoFill, () => DefaultBorderColor, 0.75); var pSls = SetLegendSeries(entryWidth, entryHeight); SetLegendTrendlines(entryWidth, entryHeight, pSls); @@ -854,7 +854,8 @@ private LineRenderItem GetLineSeriesIcon(ExcelChart ct, ExcelChartStandardSerie { var line = new LineRenderItem(Rectangle.Bounds); //line.SetDrawingPropertiesFill(ChartRenderer.Theme, cStandardSerie.Fill, Chart.StyleManager.Style?.SeriesLine.FillReference.Color, false, ChartRenderer.Theme.ColorScheme.Accent1.GetColor()); - line.SetDrawingPropertiesBorder(ChartRenderer.Theme, cStandardSerie.Border, Chart.StyleManager.Style?.SeriesLine.BorderReference.Color, cStandardSerie.Border.IsEmpty || cStandardSerie.Border.Fill.Style != eFillStyle.NoFill, ChartRenderer.Theme.ColorScheme.Accent1.GetColor(), 3); + //Default style is NoLine NoFill + line.SetDrawingPropertiesBorder(ChartRenderer.Theme, cStandardSerie.Border, Chart.StyleManager.Style?.SeriesLine.BorderReference.Color, cStandardSerie.Border.IsEmpty || cStandardSerie.Border.Fill.Style != eFillStyle.NoFill, () => Color.Empty, 3); double iconTop = 0, iconLeft = 0; pSls?.GetIconTopLeft(out iconTop, out iconLeft); @@ -872,7 +873,8 @@ private LineRenderItem GetTrendLineSeriesIcon(ExcelChart ct, ExcelChartTrendline { var line = new LineRenderItem(Rectangle.Bounds); line.SetDrawingPropertiesFill(ChartRenderer.Theme, tl.Fill, Chart.StyleManager.Style?.Trendline.FillReference.Color, UserSpaceSettings.UserSpaceOnUse_Global, DefaultFillColor); - line.SetDrawingPropertiesBorder(ChartRenderer.Theme, tl.Border, Chart.StyleManager.Style?.Trendline.BorderReference.Color, tl.Border.Fill.Style != eFillStyle.NoFill, DefaultBorderColor, 0.75); + //Default is actually NoLine + line.SetDrawingPropertiesBorder(ChartRenderer.Theme, tl.Border, Chart.StyleManager.Style?.Trendline.BorderReference.Color, tl.Border.Fill.Style != eFillStyle.NoFill, () => DefaultBorderColor, 0.75); double iconTop = 0, iconLeft = 0; pSls?.GetIconTopLeft(out iconTop, out iconLeft); diff --git a/src/EPPlus/Drawing/Renderer/Chart/ChartPlotareaRenderer.cs b/src/EPPlus/Drawing/Renderer/Chart/ChartPlotareaRenderer.cs index afe94de389..d92d31bc76 100644 --- a/src/EPPlus/Drawing/Renderer/Chart/ChartPlotareaRenderer.cs +++ b/src/EPPlus/Drawing/Renderer/Chart/ChartPlotareaRenderer.cs @@ -16,6 +16,7 @@ Date Author Change using EPPlusImageRenderer.RenderItems; using OfficeOpenXml.Drawing; using OfficeOpenXml.Drawing.Chart; +using OfficeOpenXml.Drawing.Renderer.Chart.Defaults; using OfficeOpenXml.FormulaParsing.Excel.Functions.Text; using System; using System.Collections.Generic; @@ -24,7 +25,7 @@ Date Author Change namespace EPPlusImageRenderer.Svg { - internal class ChartPlotareaRenderer : ChartDrawingObject + internal class ChartPlotareaRenderer : ChartDrawingDefaultObject { public ChartPlotareaRenderer(ChartRenderer sc) : base(sc) { @@ -32,15 +33,18 @@ public ChartPlotareaRenderer(ChartRenderer sc) : base(sc) } public List ChartTypeDrawers { get; set; } public GroupRenderItem Group { get; private set; } + + ExcelChartPlotArea _pa; + internal void SetPlotAreaRectangle() { - var pa = Chart.PlotArea; + _pa = Chart.PlotArea; TopMargin = BottomMargin = LeftMargin = RightMargin = 10.5; //14px Group = new GroupRenderItem(ChartRenderer.Bounds); var rect = new RectRenderItem(Group.Bounds); - if (pa.Layout.HasLayout) + if (_pa.Layout.HasLayout) { - rect = GetRectFromManualLayout(ChartRenderer, pa.Layout); + rect = GetRectFromManualLayout(ChartRenderer, _pa.Layout); } else { @@ -61,8 +65,8 @@ internal void SetPlotAreaRectangle() ChartRenderer.Legend.Rectangle.Top = Group.Top + rect.Height / 2 - ChartRenderer.Legend.Rectangle.Height / 2; } - rect.SetDrawingPropertiesFill(ChartRenderer.Theme, pa.Fill, ChartRenderer.Chart.StyleManager.Style?.PlotArea.FillReference.Color, UserSpaceSettings.ObjectBoundingBox, DefaultFillColor); - rect.SetDrawingPropertiesBorder(ChartRenderer.Theme, pa.Border, ChartRenderer.Chart.StyleManager.Style?.PlotArea.BorderReference.Color, pa.Border.Fill.Style != eFillStyle.NoFill, DefaultBorderColor, 0.75); + rect.SetDrawingPropertiesFill(ChartRenderer.Theme, _pa.Fill, ChartRenderer.Chart.StyleManager.Style?.PlotArea.FillReference.Color, UserSpaceSettings.ObjectBoundingBox, DefaultFillColor); + rect.SetDrawingPropertiesBorder(ChartRenderer.Theme, _pa.Border, ChartRenderer.Chart.StyleManager.Style?.PlotArea.BorderReference.Color, _pa.Border.Fill.Style != eFillStyle.NoFill, GetDefaultBorderColor, 0.75); Rectangle = rect; } @@ -251,6 +255,17 @@ internal void DrawSeries() drawer.DrawSeries(); } } - internal override Color? DefaultFillColor { get => null; } + + internal override Color? GetDefaultFillColor() + { + return GetDefaultFillColorForElement(ChartElement.PlotArea2d, (int)Chart.Style); + } + + internal override Color? GetDefaultBorderColor() + { + return GetDefaultBorderColorForElement(ChartElement.PlotArea2d, (int)Chart.Style); + } + + internal override Color? DefaultFillColor { get => GetDefaultFillColor(); } } } diff --git a/src/EPPlus/Drawing/Renderer/Chart/ChartStyleDefaults/ChartElementStyleTables.cs b/src/EPPlus/Drawing/Renderer/Chart/ChartStyleDefaults/ChartElementStyleTables.cs deleted file mode 100644 index e24b00d1b7..0000000000 --- a/src/EPPlus/Drawing/Renderer/Chart/ChartStyleDefaults/ChartElementStyleTables.cs +++ /dev/null @@ -1,329 +0,0 @@ -using EPPlusImageRenderer; -using EPPlusImageRenderer.Svg; -using OfficeOpenXml.Drawing.Chart; -using OfficeOpenXml.Drawing.Theme; -using OfficeOpenXml.Encryption; -using System; -using System.Drawing; -using tc = OfficeOpenXml.Utils.TypeConversion; - -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 abstract class ChartDrawingObjectWithDefaults : ChartDrawingObject - { - 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; - } - - 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) - { - //Set to default instead for export - //Otherwise epplus generated get weird. - styleId = 2; - //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, 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 - var styleId = ChartStyleId > (int)eChartStyle.Style48 ? (int)eChartStyle.Style2 : ChartStyleId; - - var AreaOrFloor = (ChartElement.ChartArea | ChartElement.Floor); - if (AreaOrFloor.HasFlag(element)) - { - 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) - { - //Is node empty inside the theme - if(themedLine.HasFill == false) - { - lineColor = Color.Transparent; - return themedLine; - } - - bool isSchemeColor = themedLine.Fill.SolidFill.Color.ColorType == eDrawingColorType.Scheme && themedLine.Fill.SolidFill.Color.SchemeColor.Color == eSchemeColor.Style; - - if (isSchemeColor) - { - lineColor = GetDefaultBorderColorForElement(element, styleId); - - if (themedLine.Fill.SolidFill.Color.Transforms.Count > 0 && lineColor.HasValue) - { - //var schemeClr = tc.ColorConverter.GetSchemeColor(ChartRenderer.Theme, eSchemeColor.Dark1); - //var tint = GetSchemeColorTint(eSchemeColor.Dark1, 0.45d); - lineColor = tc.ColorConverter.ApplyTransforms(lineColor.Value, themedLine.Fill.SolidFill.Color.Transforms); - } - - return themedLine; - } - else - { - 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) - { - //Node exists but has no fill. Excel considers this the same as transparent/noFill - lineColor = Color.Transparent; - return themedLine; - } - - if (styleId < 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) - { - //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 ((ChartElement.Floor | ChartElement.Walls).HasFlag(element)) - { - fillColor = GetDefaultFillColorForElement(element, styleId); - var themedFill = ChartRenderer.Theme.FormatScheme.BackgroundFillStyle[0]; - - if (styleId > 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 fill"); - } - } - - protected Color? GetDefaultBorderColorForElement(ChartElement element, int ChartStyleId) - { - //return Color.Empty; - - if((ChartElement.Axis | ChartElement.MajorGridLines).HasFlag(element)) - { - //There's only really two options in this particular case - if(ChartStyleId <= 32) - { - return GetSchemeColorTint(eSchemeColor.Text1, 0.75d); - } - else - { - 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 ((ChartElement.ChartArea | ChartElement.DataTable | ChartElement.Floor).HasFlag(element)) - { - 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((ChartElement.Floor | ChartElement.Walls | ChartElement.PlotArea2d).HasFlag(element)) - { - 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"); - } - - - abstract internal Color? GetDefaultFillColor(); - abstract internal Color? GetDefaultBorderColor(); - } -} diff --git a/src/EPPlus/Drawing/Renderer/Chart/ChartTitleRenderer.cs b/src/EPPlus/Drawing/Renderer/Chart/ChartTitleRenderer.cs index b8e26b8bb9..44c2a0e3a8 100644 --- a/src/EPPlus/Drawing/Renderer/Chart/ChartTitleRenderer.cs +++ b/src/EPPlus/Drawing/Renderer/Chart/ChartTitleRenderer.cs @@ -22,6 +22,7 @@ Date Author Change using OfficeOpenXml; using OfficeOpenXml.Drawing; using OfficeOpenXml.Drawing.Chart; +using OfficeOpenXml.Drawing.Renderer.Chart.Defaults; using OfficeOpenXml.Drawing.Renderer.TextBox; using OfficeOpenXml.FormulaParsing.Excel.Functions.MathFunctions; using OfficeOpenXml.FormulaParsing.Excel.Functions.Text; @@ -105,7 +106,7 @@ internal ChartTitleRenderer(ChartRenderer sc, ExcelChartTitleStandard t, string } Rectangle.SetDrawingPropertiesFill(sc.Theme, t.Fill, sc.Chart.StyleManager.Style?.Title.FillReference.Color, UserSpaceSettings.ObjectBoundingBox, DefaultFillColor); - Rectangle.SetDrawingPropertiesBorder(sc.Theme, t.Border, sc.Chart.StyleManager.Style?.Title.BorderReference.Color, t.Border.Fill.Style != eFillStyle.NoFill, DefaultBorderColor, 0.75); + Rectangle.SetDrawingPropertiesBorder(sc.Theme, t.Border, sc.Chart.StyleManager.Style?.Title.BorderReference.Color, t.Border.Fill.Style != eFillStyle.NoFill, () => DefaultBorderColor, 0.75); } private void SetAxisTitleRect(ChartRenderer sc, ChartAxisRenderer axis) @@ -227,7 +228,8 @@ public override void AppendRenderItems(List renderItems) { TextBox.TextBody.FontColorString = "#" + p.DefaultRunProperties.Fill.Color.ToColorString(); TextBox.Rectangle.SetDrawingPropertiesFill(_svgChart.Theme, _title.Fill, _svgChart.Chart.StyleManager.Style?.Title.FillReference.Color, UserSpaceSettings.ObjectBoundingBox, DefaultFillColor); - TextBox.Rectangle.SetDrawingPropertiesBorder(_svgChart.Theme, _title.Border, _svgChart.Chart.StyleManager.Style?.Title.BorderReference.Color, _title.Border.Fill.Style != eFillStyle.NoFill, DefaultBorderColor, 0.75); + //Default is actually NoLine + TextBox.Rectangle.SetDrawingPropertiesBorder(_svgChart.Theme, _title.Border, _svgChart.Chart.StyleManager.Style?.Title.BorderReference.Color, _title.Border.Fill.Style != eFillStyle.NoFill, () => DefaultBorderColor, 0.75); } TextBox.AppendRenderItems(renderItems); } diff --git a/src/EPPlus/Drawing/Renderer/Chart/ChartTypeDrawers/ChartErrorBarRenderer.cs b/src/EPPlus/Drawing/Renderer/Chart/ChartTypeDrawers/ChartErrorBarRenderer.cs index 966e935375..0c524ca674 100644 --- a/src/EPPlus/Drawing/Renderer/Chart/ChartTypeDrawers/ChartErrorBarRenderer.cs +++ b/src/EPPlus/Drawing/Renderer/Chart/ChartTypeDrawers/ChartErrorBarRenderer.cs @@ -4,6 +4,7 @@ using EPPlusImageRenderer.RenderItems; using EPPlusImageRenderer.Svg; using OfficeOpenXml.Drawing.Chart; +using OfficeOpenXml.Drawing.Renderer.Chart.Defaults; using OfficeOpenXml.FormulaParsing.Excel.Functions.MathFunctions; using OfficeOpenXml.Utils.TypeConversion; using System; @@ -17,7 +18,7 @@ namespace OfficeOpenXml.Drawing.Renderer.Chart.ChartTypeDrawers { - internal class ChartErrorBarRenderer : ChartDrawingObject + internal class ChartErrorBarRenderer : ChartDrawingDefaultObject { internal ExcelChartErrorBars _errorbars; private double[] _ySerie; @@ -228,30 +229,46 @@ internal List GetErrorBarRenderItem(int index, ChartAxisRenderer xAx { if (_errorbars.Border.LineElement == null) { - ri.SetDrawingPropertiesBorder(ChartRenderer.Theme, ChartRenderer.Chart.StyleManager.Style?.ErrorBar.Border, ChartRenderer.Chart.StyleManager.Style?.ErrorBar.BorderReference.Color, true, DefaultFillColor, 0.75); - ri.SetDrawingPropertiesBorder(ChartRenderer.Theme, ChartRenderer.Chart.StyleManager.Style?.ErrorBar.Border, ChartRenderer.Chart.StyleManager.Style?.ErrorBar.BorderReference.Color, true, DefaultBorderColor, 0.75d); + ri.SetDrawingPropertiesBorder(ChartRenderer.Theme, ChartRenderer.Chart.StyleManager.Style?.ErrorBar.Border, ChartRenderer.Chart.StyleManager.Style?.ErrorBar.BorderReference.Color, true, GetDefaultFillColor, 0.75); + ri.SetDrawingPropertiesBorder(ChartRenderer.Theme, ChartRenderer.Chart.StyleManager.Style?.ErrorBar.Border, ChartRenderer.Chart.StyleManager.Style?.ErrorBar.BorderReference.Color, true, GetDefaultBorderColor, 0.75d); } else { - ri.SetDrawingPropertiesBorder(ChartRenderer.Theme, _errorbars.Border, ChartRenderer.Chart.StyleManager.Style?.ErrorBar.BorderReference.Color, _errorbars.Border.Fill.Style != eFillStyle.NoFill, DefaultBorderColor, 0.75); + ri.SetDrawingPropertiesBorder(ChartRenderer.Theme, _errorbars.Border, ChartRenderer.Chart.StyleManager.Style?.ErrorBar.BorderReference.Color, _errorbars.Border.Fill.Style != eFillStyle.NoFill, GetDefaultBorderColor, 0.75); } ri.SetDrawingPropertiesEffects(ChartRenderer.Theme, _errorbars.Effect); } return l; } + + internal override Color? GetDefaultFillColor() + { + return GetDefaultFillColorForElement(ChartElement.OtherLines, (int)Chart.Style); + } + + internal override Color? GetDefaultBorderColor() + { + //We only get here if the node is null or empty + var themedLine = GetThemedLine(ChartElement.OtherLines, (int)Chart.Style, _errorbars.Border.Fill != null && _errorbars.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? DefaultBorderColor { get { - var borderStyleFill = ChartRenderer.Theme.FormatScheme.BorderStyle[0].Fill; - if (borderStyleFill.IsEmpty == false && borderStyleFill.SolidFill != null && borderStyleFill.SolidFill.Color.ColorType != eDrawingColorType.Scheme) - { - return ChartRenderer.Theme.FormatScheme.BorderStyle[0].Fill?.Color; - } - else - { - return null; - } + return GetDefaultBorderColor(); + //var borderStyleFill = ChartRenderer.Theme.FormatScheme.BorderStyle[0].Fill; + //if (borderStyleFill.IsEmpty == false && borderStyleFill.SolidFill != null && borderStyleFill.SolidFill.Color.ColorType != eDrawingColorType.Scheme) + //{ + // return ChartRenderer.Theme.FormatScheme.BorderStyle[0].Fill?.Color; + //} + //else + //{ + // return null; + //} } } } diff --git a/src/EPPlus/Drawing/Renderer/Chart/ChartTypeDrawers/ChartTypeDrawer.cs b/src/EPPlus/Drawing/Renderer/Chart/ChartTypeDrawers/ChartTypeDrawer.cs index 9fd79c1d94..6fcb6f1137 100644 --- a/src/EPPlus/Drawing/Renderer/Chart/ChartTypeDrawers/ChartTypeDrawer.cs +++ b/src/EPPlus/Drawing/Renderer/Chart/ChartTypeDrawers/ChartTypeDrawer.cs @@ -256,7 +256,7 @@ internal static void SetFillSerie(ExcelChart chart, ExcelChart ct, ExcelChartSta var color = GetVaryColor(theme, chart.StyleManager?.ColorsManager, serieIndex); 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); + item.SetDrawingPropertiesBorder(theme, cStandardSerie.Border, chart.StyleManager.Style?.SeriesLine.BorderReference.Color, cStandardSerie.Border.Fill.Style != eFillStyle.NoFill, () => null, 0.75); } private static Color? GetVaryColor(ExcelTheme theme, ExcelChartColorsManager colorsManager, int index) diff --git a/src/EPPlus/Drawing/Renderer/Chart/ChartTypeDrawers/LineChartTypeDrawer.cs b/src/EPPlus/Drawing/Renderer/Chart/ChartTypeDrawers/LineChartTypeDrawer.cs index 5ba1d1bcb3..b865ffa3e7 100644 --- a/src/EPPlus/Drawing/Renderer/Chart/ChartTypeDrawers/LineChartTypeDrawer.cs +++ b/src/EPPlus/Drawing/Renderer/Chart/ChartTypeDrawers/LineChartTypeDrawer.cs @@ -82,7 +82,8 @@ private void CreateDropLine(ExcelLineChart chartType, List coords) Y2 = yBottom, }; dl.Bounds.Name = $"DropLine {i/2 + 1}"; - dl.SetDrawingPropertiesBorder(ChartRenderer.Theme, chartType.DropLine.Border, chartType.StyleManager.Style?.DropLine.BorderReference.Color, true, DefaultBorderColor, 1.5,DrawingRenderer.UserSpaceSettings.UserSpaceOnUse_Parent); + //TODO: DropLines should actually use the "Other Lines" DefaultDrawingObject + dl.SetDrawingPropertiesBorder(ChartRenderer.Theme, chartType.DropLine.Border, chartType.StyleManager.Style?.DropLine.BorderReference.Color, true, () => DefaultBorderColor, 1.5,DrawingRenderer.UserSpaceSettings.UserSpaceOnUse_Parent); dl.SetDrawingPropertiesEffects(ChartRenderer.Theme, chartType.DropLine.Effect); _dropLines.Add(dl); @@ -245,10 +246,12 @@ private void AddLine(ExcelLineChart chartType, ExcelLineChartSerie serie, List ChartRenderer.Theme.FormatScheme.FillStyle[0].Color); } } - lineDp.SetDrawingPropertiesBorder(ChartRenderer.Theme, dp.Border, chartType.StyleManager.Style?.SeriesLine.BorderReference.Color, true, DefaultBorderColor, 3); + lineDp.SetDrawingPropertiesBorder(ChartRenderer.Theme, dp.Border, chartType.StyleManager.Style?.SeriesLine.BorderReference.Color, true, () => DefaultBorderColor, 3); lineDp.SetDrawingPropertiesEffects(ChartRenderer.Theme, dp.Effect); dataPointOverrides.Add(lineDp); } @@ -257,7 +260,7 @@ private void AddLine(ExcelLineChart chartType, ExcelLineChartSerie serie, List DefaultBorderColor, 3); linePath.SetDrawingPropertiesEffects(ChartRenderer.Theme, serie.Effect); linePath.FillColor = "none"; //No fill for line linePath.StrokeMiterLimit = 4; //A much higher value of the miter limit, might cause the "spike" to get beyond the data point on the vertical scale.. diff --git a/src/EPPlus/Drawing/Renderer/Chart/ChartTypeDrawers/Trendlines/ChartTrendlineRenderer.cs b/src/EPPlus/Drawing/Renderer/Chart/ChartTypeDrawers/Trendlines/ChartTrendlineRenderer.cs index 4d65fbf810..077bbe8c20 100644 --- a/src/EPPlus/Drawing/Renderer/Chart/ChartTypeDrawers/Trendlines/ChartTrendlineRenderer.cs +++ b/src/EPPlus/Drawing/Renderer/Chart/ChartTypeDrawers/Trendlines/ChartTrendlineRenderer.cs @@ -19,6 +19,7 @@ Date Author Change using EPPlusImageRenderer.Svg; using OfficeOpenXml.Drawing; using OfficeOpenXml.Drawing.Chart; +using OfficeOpenXml.Drawing.Renderer.Chart.Defaults; using OfficeOpenXml.Drawing.Renderer.TextBox; using OfficeOpenXml.FormulaParsing.Excel.Functions.Statistical; using OfficeOpenXml.Utils.TypeConversion; @@ -29,7 +30,7 @@ Date Author Change using System.Text; namespace EPPlus.Export.ImageRenderer.Svg.Chart { - internal class ChartTrendlineRenderer : ChartDrawingObject + internal class ChartTrendlineRenderer : ChartDrawingDefaultObject { private ExcelChartTrendline _trendline; private double[] _ySerie; @@ -191,7 +192,7 @@ private void CreateDatalabel() DataLabel.Rectangle.SetDrawingPropertiesFill(ChartRenderer.Theme, _trendline.Label.Fill, Chart.StyleManager.Style.TrendlineLabel.FillReference.Color, UserSpaceSettings.ObjectBoundingBox, DefaultFillColor); - DataLabel.Rectangle.SetDrawingPropertiesBorder(ChartRenderer.Theme, _trendline.Label.Border, Chart.StyleManager.Style.TrendlineLabel.BorderReference.Color, true, DefaultBorderColor, _trendline.Label.Border.Width); + DataLabel.Rectangle.SetDrawingPropertiesBorder(ChartRenderer.Theme, _trendline.Label.Border, Chart.StyleManager.Style.TrendlineLabel.BorderReference.Color, true, GetDefaultBorderColor, _trendline.Label.Border.Width); DataLabel.Rectangle.SetDrawingPropertiesEffects(ChartRenderer.Theme, _trendline.Label.Effect); } @@ -665,7 +666,7 @@ public override void AppendRenderItems(List renderItems) var pathItem = new PathRenderItem(ChartRenderer.Plotarea.Rectangle.Bounds); pathItem.Commands.Add(new EPPlusImageRenderer.PathCommands(PathCommandType.Move, RenderCoordinates)); pathItem.FillColor = "none"; - pathItem.SetDrawingPropertiesBorder(ChartRenderer.Theme, _trendline.Border, Chart.StyleManager.Style?.Trendline.BorderReference.Color, true, DefaultBorderColor, _trendline.Border.Width); + pathItem.SetDrawingPropertiesBorder(ChartRenderer.Theme, _trendline.Border, Chart.StyleManager.Style?.Trendline.BorderReference.Color, true, GetDefaultBorderColor, _trendline.Border.Width); pathItem.SetDrawingPropertiesEffects(ChartRenderer.Theme, _trendline.Effect); renderItems.Add(pathItem); } @@ -784,19 +785,34 @@ private double GetLinearValueAtPosition(double x) { return Coefficients[1] + Coefficients[0] * x; } + + internal override Color? GetDefaultFillColor() + { + return GetDefaultFillColorForElement(ChartElement.OtherLines, (int)Chart.Style); + } + + internal override Color? GetDefaultBorderColor() + { + //We only get here if the node is null or empty + var themedLine = GetThemedLine(ChartElement.OtherLines, (int)Chart.Style, _trendline.Border.Fill != null && _trendline.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? DefaultBorderColor { get { - var borderStyleFill = ChartRenderer.Theme.FormatScheme.BorderStyle[0].Fill; - if (borderStyleFill.IsEmpty == false && borderStyleFill.SolidFill != null && borderStyleFill.SolidFill.Color.ColorType != eDrawingColorType.Scheme) - { - return ChartRenderer.Theme.FormatScheme.BorderStyle[0].Fill?.Color; - } - else - { - return null; - } + return GetDefaultBorderColor(); + //var borderStyleFill = ChartRenderer.Theme.FormatScheme.BorderStyle[0].Fill; + //if (borderStyleFill.IsEmpty == false && borderStyleFill.SolidFill != null && borderStyleFill.SolidFill.Color.ColorType != eDrawingColorType.Scheme) + //{ + // return ChartRenderer.Theme.FormatScheme.BorderStyle[0].Fill?.Color; + //} + //else + //{ + // return null; + //} } } } diff --git a/src/EPPlus/Drawing/Renderer/Chart/LineMarkerHelper.cs b/src/EPPlus/Drawing/Renderer/Chart/LineMarkerHelper.cs index 1d127fd6cb..b3ae77ca89 100644 --- a/src/EPPlus/Drawing/Renderer/Chart/LineMarkerHelper.cs +++ b/src/EPPlus/Drawing/Renderer/Chart/LineMarkerHelper.cs @@ -35,11 +35,13 @@ internal static RenderItem GetMarkerItem(ChartRenderer sc, ExcelLineChartSerie l { if (marker.Border.Fill.IsEmpty) { - item?.SetDrawingPropertiesBorder(sc.Theme, ls.Border, sc.Chart.StyleManager.Style.DataPointMarker.BorderReference.Color, ls.Border.Fill.Style != eFillStyle.NoFill, sc.Theme.FormatScheme.BorderStyle[0].Fill.Color, 0.75d); + //Datapoints including markers actually have a way more complex fallback TODO: Handle later. + item?.SetDrawingPropertiesBorder(sc.Theme, ls.Border, sc.Chart.StyleManager.Style.DataPointMarker.BorderReference.Color, ls.Border.Fill.Style != eFillStyle.NoFill, () => sc.Theme.FormatScheme.BorderStyle[0].Fill.Color, 0.75d); } else { - item?.SetDrawingPropertiesBorder(sc.Theme, marker.Border, sc.Chart.StyleManager.Style.DataPointMarker.BorderReference.Color, ls.Marker.Border.Fill.Style != eFillStyle.NoFill, sc.Theme.FormatScheme.BorderStyle[0].Fill.Color, 0.75d); + //Datapoints including markers actually have a way more complex fallback TODO: Handle later. + item?.SetDrawingPropertiesBorder(sc.Theme, marker.Border, sc.Chart.StyleManager.Style.DataPointMarker.BorderReference.Color, ls.Marker.Border.Fill.Style != eFillStyle.NoFill, () => sc.Theme.FormatScheme.BorderStyle[0].Fill.Color, 0.75d); } } return item; diff --git a/src/EPPlus/Drawing/Renderer/ChartRenderer.cs b/src/EPPlus/Drawing/Renderer/ChartRenderer.cs index 1546ba0c89..520e2618a1 100644 --- a/src/EPPlus/Drawing/Renderer/ChartRenderer.cs +++ b/src/EPPlus/Drawing/Renderer/ChartRenderer.cs @@ -381,11 +381,10 @@ private void SetChartArea(SvgRenderOptions options) //var chartStyleId = Chart.StyleManager.Style.Id; - item.Rectangle.SetDrawingBorderPropertiesNew( + item.Rectangle.SetDrawingPropertiesBorder( Theme, - reference?.Color, - Chart.Border, - 1d, + Chart.Border, + reference?.Color, Chart.Border.Fill.Style != eFillStyle.NoFill, () => item.GetDefaultBorderColor()); @@ -395,64 +394,6 @@ private void SetChartArea(SvgRenderOptions options) 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]; - 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); - - 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 bg1Col = tc.ColorConverter.GetThemeColor(Theme, eThemeSchemeColor.Background1); - - 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 - themeColor = tc.ColorConverter.ApplyTransforms(themeColor.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(themeColor.Value, 0.25d); - themeColor = newTheme; - } - } - else - { - //41-48 - //aka light1 - themeColor = tc.ColorConverter.GetThemeColor(Theme, eThemeSchemeColor.Background1); - themedLine = null; - } - } - 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 2f58bbf2d3..c63e2113fe 100644 --- a/src/EPPlus/Drawing/Renderer/RenderItems/DrawingRenderItemExtentions.cs +++ b/src/EPPlus/Drawing/Renderer/RenderItems/DrawingRenderItemExtentions.cs @@ -230,21 +230,23 @@ internal static string GetFillNew(ExcelDrawingFillBasic fill, ExcelTheme theme, } return fillStr; } - - internal static void SetDrawingBorderPropertiesNew(this RenderItem item, ExcelTheme theme, ExcelChartStyleColorManager reference, ExcelDrawingBorder border, double opacity, bool hasBorder, Func GetHardCodedDefaultForItem, UserSpaceSettings gradientUserSpaceOnUse = UserSpaceSettings.UserSpaceOnUse_Global) + // 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 + internal static void SetDrawingPropertiesBorder(this RenderItem item, ExcelTheme theme, ExcelDrawingBorder border, ExcelChartStyleColorManager reference, bool hasBorder, Func GetStyleDefaultColor, double defaultWidth = 1.5d, UserSpaceSettings gradientUserSpaceOnUse = UserSpaceSettings.UserSpaceOnUse_Global) { string fillColorStr = null; DrawingRenderGradientFill gradFill = null; + double opacity = 1d; + if (border == null) { if (hasBorder) { - fillColorStr = GetFillNew(null, theme, reference, item.BorderColorSource, out opacity, GetHardCodedDefaultForItem, out gradFill); + fillColorStr = GetFillNew(null, theme, reference, item.BorderColorSource, out opacity, GetStyleDefaultColor, out gradFill); } } else { - fillColorStr = GetFillNew(border.Fill, theme, reference, item.BorderColorSource, out opacity, GetHardCodedDefaultForItem, out gradFill); + fillColorStr = GetFillNew(border.Fill, theme, reference, item.BorderColorSource, out opacity, GetStyleDefaultColor, out gradFill); } if(gradFill != null) @@ -276,63 +278,63 @@ 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; - //GetFillColor(theme, null, color, item.BorderColorSource, out opacity, nullColor ?? theme.ColorScheme.Dark1.GetColor(), styleId); - opacity = double.NaN; - SetDrawingBorderPropertiesNew(item, theme, color, border, opacity.Value, hasBorder, () => { return nullColor; }, gradientUserSpaceOnUse); - //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 != 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. - // } - //} - } + //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; + // //GetFillColor(theme, null, color, item.BorderColorSource, out opacity, nullColor ?? theme.ColorScheme.Dark1.GetColor(), styleId); + // opacity = double.NaN; + // SetDrawingPropertiesBorder(item, theme, color, border, opacity.Value, hasBorder, () => { return nullColor; }, gradientUserSpaceOnUse); + // //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 != 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. + // // } + // //} + //} internal static void SetDrawingPropertiesEffects(this RenderItem item, ExcelTheme theme, ExcelDrawingEffectStyle effect) { if (effect.HasGlow) diff --git a/src/EPPlus/Drawing/Renderer/ShapeRenderer.cs b/src/EPPlus/Drawing/Renderer/ShapeRenderer.cs index 0a964ba7f8..1be7e35176 100644 --- a/src/EPPlus/Drawing/Renderer/ShapeRenderer.cs +++ b/src/EPPlus/Drawing/Renderer/ShapeRenderer.cs @@ -183,7 +183,7 @@ protected RenderItem AddFromPaths(BoundingBox parent, DrawingPath path, bool dra if (drawFill) { pi.FillColorSource = path.Fill; - pi.SetDrawingPropertiesFill(Theme, shape.Fill, shape.ThemeStyles.FillReference.Color); + pi.SetDrawingPropertiesFill(Theme, shape.Fill, shape.ThemeStyles.FillReference.Color, UserSpaceSettings.ObjectBoundingBox, Theme.ObjectDefaults.ShapeDefinition.Style.FillReference.ShapeColor.GetColor()); } else { @@ -194,7 +194,7 @@ protected RenderItem AddFromPaths(BoundingBox parent, DrawingPath path, bool dra if (drawBorder) { pi.BorderColorSource = path.Stroke ? PathFillMode.Norm : PathFillMode.None; - pi.SetDrawingPropertiesBorder(Theme, shape.Border, shape.ThemeStyles.BorderReference.Color, path.Stroke); + pi.SetDrawingPropertiesBorder(Theme, shape.Border, shape.ThemeStyles.BorderReference.Color, path.Stroke, ()=> Theme.ObjectDefaults.ShapeDefinition.Style.BorderReference.ShapeColor.GetColor()); } else { diff --git a/src/EPPlus/EPPlus.csproj b/src/EPPlus/EPPlus.csproj index 42c104dbe4..50e82f5938 100644 --- a/src/EPPlus/EPPlus.csproj +++ b/src/EPPlus/EPPlus.csproj @@ -191,7 +191,7 @@ * Minor features and bug fixes. ## Version 7.4.1 - * Updated for vulnerability in System.Text.Json - Microsoft.Extensions.Configuration.Json 8.0.0 -> 8.0.1 + * Updated for vulnerability in System.Text.Json - Microsoft.Extensions.Configuration.Json 8.0.0 -> 8.0.1 ## Version 7.4.0 * Conditional formatting in Pivot Tables. @@ -758,12 +758,7 @@ rather than hardcoded output paths so it resolves correctly per TFM in a multitarget build. --> - + From 085743162d44768160f248586105da296fe47994 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ossian=20Edstr=C3=B6m?= Date: Fri, 28 Aug 2026 14:56:50 +0200 Subject: [PATCH 47/73] Added moved file --- .../Defaults/ChartDrawingDefaultObject.cs | 336 ++++++++++++++++++ 1 file changed, 336 insertions(+) create mode 100644 src/EPPlus/Drawing/Renderer/Chart/Defaults/ChartDrawingDefaultObject.cs diff --git a/src/EPPlus/Drawing/Renderer/Chart/Defaults/ChartDrawingDefaultObject.cs b/src/EPPlus/Drawing/Renderer/Chart/Defaults/ChartDrawingDefaultObject.cs new file mode 100644 index 0000000000..cc3f01dc4c --- /dev/null +++ b/src/EPPlus/Drawing/Renderer/Chart/Defaults/ChartDrawingDefaultObject.cs @@ -0,0 +1,336 @@ +using EPPlusImageRenderer; +using EPPlusImageRenderer.Svg; +using OfficeOpenXml.Drawing.Chart; +using OfficeOpenXml.Drawing.Theme; +using OfficeOpenXml.Encryption; +using System; +using System.Drawing; +using tc = OfficeOpenXml.Utils.TypeConversion; + +namespace OfficeOpenXml.Drawing.Renderer.Chart.Defaults +{ + [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 abstract class ChartDrawingDefaultObject : ChartDrawingObject + { + public ChartDrawingDefaultObject(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; + } + + 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) + { + //Set to default instead for export + //Otherwise epplus generated get weird. + styleId = 2; + //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, 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 + var styleId = ChartStyleId > (int)eChartStyle.Style48 ? (int)eChartStyle.Style2 : ChartStyleId; + + //Everything defaults to subtle style except Area and Floor at certain styles + var AreaOrFloorOrOther = (ChartElement.ChartArea | ChartElement.Floor | ChartElement.OtherLines); + if (AreaOrFloorOrOther.HasFlag(element)) + { + 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) + { + //Is node empty inside the theme + if(themedLine.HasFill == false) + { + lineColor = Color.Transparent; + return themedLine; + } + + bool isSchemeColor = themedLine.Fill.SolidFill.Color.ColorType == eDrawingColorType.Scheme && themedLine.Fill.SolidFill.Color.SchemeColor.Color == eSchemeColor.Style; + + if (isSchemeColor) + { + lineColor = GetDefaultBorderColorForElement(element, styleId); + + if (themedLine.Fill.SolidFill.Color.Transforms.Count > 0 && lineColor.HasValue) + { + //var schemeClr = tc.ColorConverter.GetSchemeColor(ChartRenderer.Theme, eSchemeColor.Dark1); + //var tint = GetSchemeColorTint(eSchemeColor.Dark1, 0.45d); + lineColor = tc.ColorConverter.ApplyTransforms(lineColor.Value, themedLine.Fill.SolidFill.Color.Transforms); + } + + return themedLine; + } + else + { + 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) + { + //Node exists but has no fill. Excel considers this the same as transparent/noFill + lineColor = Color.Transparent; + return themedLine; + } + + if (styleId < 41 || element == ChartElement.OtherLines) + { + 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) + { + //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 ((ChartElement.Floor | ChartElement.Walls).HasFlag(element)) + { + fillColor = GetDefaultFillColorForElement(element, styleId); + var themedFill = ChartRenderer.Theme.FormatScheme.BackgroundFillStyle[0]; + + if (styleId > 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 fill"); + } + } + + protected Color? GetDefaultBorderColorForElement(ChartElement element, int ChartStyleId) + { + //return Color.Empty; + + if((ChartElement.Axis | ChartElement.MajorGridLines).HasFlag(element)) + { + //There's only really two options in this particular case + if(ChartStyleId <= 32) + { + return GetSchemeColorTint(eSchemeColor.Text1, 0.75d); + } + else + { + 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 ((ChartElement.ChartArea | ChartElement.DataTable | ChartElement.Floor).HasFlag(element)) + { + 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) + { + ChartStyleId = ChartStyleId > (int)eChartStyle.Style48 ? (int)eChartStyle.Style2 : 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((ChartElement.Floor | ChartElement.Walls | ChartElement.PlotArea2d).HasFlag(element)) + { + var retCol = GetSchemeColorTint(eSchemeColor.Background1); + var retCol2 = GetSchemeColorTint(eSchemeColor.Background1, 0.2d); + Color? retCol3 = Color.Empty; + if(ChartStyleId > 35 && ChartStyleId < 40) + { + 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"); + } + + + abstract internal Color? GetDefaultFillColor(); + abstract internal Color? GetDefaultBorderColor(); + } +} From dce162081646b350dc9a8f9c169692156e6fa2a8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jan=20K=C3=A4llman?= Date: Fri, 28 Aug 2026 16:05:06 +0200 Subject: [PATCH 48/73] Fixes for html export --- .../Chart/LineChartToSvgTests.cs | 63 ++++++++++++ src/EPPlus.DrawingRenderer.Tests/TestBase.cs | 1 + src/EPPlus/Drawing/Chart/ExcelBarChart.cs | 5 + .../Drawing/Chart/ExcelChartAxisStandard.cs | 3 +- src/EPPlus/Drawing/Chart/ExcelLineChart.cs | 5 + src/EPPlus/Drawing/Chart/ExcelPieChart.cs | 4 + src/EPPlus/Drawing/ExcelDrawing.cs | 8 +- src/EPPlus/Drawing/ExcelPicture.cs | 4 + src/EPPlus/Drawing/ExcelShape.cs | 5 +- .../HtmlExport/Enums/eDrawingInclude.cs | 21 ++-- .../HtmlExport/Enums/ePictureInclude.cs | 6 +- .../HtmlExport/Enums/ePicturePosition.cs | 22 +++++ .../Exporters/ExcelHtmlExporterBase.cs | 15 +-- .../Internal/AbstractRangeExporter.cs | 85 +++++++++++++---- .../Exporters/Internal/CssExporterBase.cs | 27 +++--- .../Internal/HtmlExporterBaseInternal.cs | 30 +++--- .../Settings/HtmlDrawingSettings.cs | 83 +++++++++++++--- .../HtmlExport/Settings/HtmlExportSettings.cs | 18 +++- .../Settings/HtmlPictureSettings.cs | 95 ++++++++++++++++--- .../CssDrawingPropertiesTranslator.cs | 8 +- .../CssImageAlignmentTranslator.cs | 10 +- .../CssImagePropertiesTranslator.cs | 4 +- .../Translators/CssImageTranslator.cs | 10 +- .../Translators/TranslatorContext.cs | 3 - .../Export/HtmlExport/RangeExporterTests.cs | 14 ++- .../Export/HtmlExport/SvgShapeExportTests.cs | 6 +- 26 files changed, 419 insertions(+), 136 deletions(-) diff --git a/src/EPPlus.DrawingRenderer.Tests/Chart/LineChartToSvgTests.cs b/src/EPPlus.DrawingRenderer.Tests/Chart/LineChartToSvgTests.cs index 2e520346ee..ca4f3d7766 100644 --- a/src/EPPlus.DrawingRenderer.Tests/Chart/LineChartToSvgTests.cs +++ b/src/EPPlus.DrawingRenderer.Tests/Chart/LineChartToSvgTests.cs @@ -1,7 +1,12 @@ using OfficeOpenXml; using OfficeOpenXml.Drawing.Chart; +using OfficeOpenXml.Drawing.Chart.Style; +using OfficeOpenXml.Export.HtmlExport; +using OfficeOpenXml.Style; +using OfficeOpenXml.Table; using System; using System.Collections.Generic; +using System.Globalization; using System.Linq; using System.Text; using System.Threading.Tasks; @@ -343,6 +348,64 @@ public void GenerateBlazorSample1() } } } + [TestMethod] + public void HtmlExportWithLineChart() + { + using (var package = new ExcelPackage()) + { + var style = TableStyles.Dark3; + var sheet = package.Workbook.Worksheets.Add("Html export sample 8"); + var csvFileInfo = new FileInfo(Path.Combine(_dataPath, $"currencies2011weekly.csv")); + if (csvFileInfo.Exists == false) return; + var format = new ExcelTextFormat + { + Delimiter = ';', + Culture = CultureInfo.InvariantCulture, + DataTypes = new eDataTypes[] { eDataTypes.DateTime, eDataTypes.Number, eDataTypes.Number, eDataTypes.Number, eDataTypes.Number } + }; + var tableRange = sheet.Cells["A15"].LoadFromText(csvFileInfo, format, style, true); + + sheet.Cells["B1:E1"].Style.HorizontalAlignment = ExcelHorizontalAlignment.Right; + sheet.Cells[tableRange.Start.Row, 1, tableRange.End.Row, 1].Style.Numberformat.Format = "yyyy-MM-dd"; + sheet.Cells[tableRange.Start.Row, 2, tableRange.End.Row, 5].Style.Numberformat.Format = "#,##0.0000"; + tableRange.AutoFitColumns(); + + var table = sheet.Tables.GetFromRange(tableRange); + table.ShowFirstColumn = true; + var chart = sheet.Drawings.AddLineChart("LineChart1", eLineChartType.Line); + + var serie1 = chart.Series.Add(tableRange.TakeColumnsBetween(1,1).SkipRows(1), tableRange.TakeColumns(1).SkipRows(1)); + serie1.HeaderAddress = sheet.Cells["B15"]; + + var serie2 = chart.Series.Add(tableRange.TakeColumnsBetween(2, 1).SkipRows(1), tableRange.TakeColumns(1).SkipRows(1)); + serie2.HeaderAddress = sheet.Cells["C15"]; + + var serie3 = chart.Series.Add(tableRange.TakeColumnsBetween(3, 1).SkipRows(1), tableRange.TakeColumns(1).SkipRows(1)); + serie3.HeaderAddress = sheet.Cells["D15"]; + + chart.SetPosition(0, 0); + chart.To.Row = 14; + chart.To.Column = 10; + chart.StyleManager.SetChartStyle(ePresetChartStyle.LineChartStyle4); + + var exporter = sheet.Cells.CreateHtmlExporter(); + var settings = exporter.Settings; + settings.Drawings.Include = eDrawingInclude.IncludeInHtmlOnly; + settings.Culture = CultureInfo.InvariantCulture; + settings.TableId = "currency-table"; + settings.AdditionalTableClassNames.Add("table"); + settings.AdditionalTableClassNames.Add("table-sm"); + settings.AdditionalTableClassNames.Add("table-borderless"); + SaveWorkbook("HtmlExportWithLineChart.xlsx", package); + // export css and html + //var css = exporter.GetCssString(); + //var html = exporter.GetHtmlString(); + var html = exporter.GetSinglePage(); + + SaveSvg("HtmlExportWithLineChart.html", html); + } + + } //2.4-CreateAFileSystemReport.xlsx //3.3-FxReportFromDatabase.xlsx } diff --git a/src/EPPlus.DrawingRenderer.Tests/TestBase.cs b/src/EPPlus.DrawingRenderer.Tests/TestBase.cs index 6c4f4dff6f..d22ac7a764 100644 --- a/src/EPPlus.DrawingRenderer.Tests/TestBase.cs +++ b/src/EPPlus.DrawingRenderer.Tests/TestBase.cs @@ -83,6 +83,7 @@ private class GeoData protected static string _testInputPathOptional = @"c:\epplusTest\workbooks\"; //Team shared workbooks for tests protected static string _testInputLocalPathOptional = @"c:\epplusTest\workbooks\"; //Local workboks for tests protected static string _imagePath = @"c:\epplusTest\images\"; + protected static string _dataPath = @"c:\epplusTest\data\"; /// ///Gets or sets the test context which provides ///information about and functionality for the current test run. diff --git a/src/EPPlus/Drawing/Chart/ExcelBarChart.cs b/src/EPPlus/Drawing/Chart/ExcelBarChart.cs index 75f8d035ab..e390123e94 100644 --- a/src/EPPlus/Drawing/Chart/ExcelBarChart.cs +++ b/src/EPPlus/Drawing/Chart/ExcelBarChart.cs @@ -525,5 +525,10 @@ internal override bool IsAxisTypeSupported(eAxisType type, ExcelChartAxis axis) } return base.IsAxisTypeSupported(type, axis); } + /// + /// Returns true if the drawing supports svg export via the . + /// + public override bool SupportsSvgExport => true; + } } diff --git a/src/EPPlus/Drawing/Chart/ExcelChartAxisStandard.cs b/src/EPPlus/Drawing/Chart/ExcelChartAxisStandard.cs index 67345c5f20..7de2abf586 100644 --- a/src/EPPlus/Drawing/Chart/ExcelChartAxisStandard.cs +++ b/src/EPPlus/Drawing/Chart/ExcelChartAxisStandard.cs @@ -15,6 +15,7 @@ Date Author Change using OfficeOpenXml.FormulaParsing.Excel.Functions.Information; using OfficeOpenXml.FormulaParsing.Excel.Functions.MathFunctions; using OfficeOpenXml.FormulaParsing.Excel.Functions.RefAndLookup; +using OfficeOpenXml.FormulaParsing.Excel.Functions.RefAndLookup.Sorting; using OfficeOpenXml.FormulaParsing.Excel.Functions.Text; using OfficeOpenXml.FormulaParsing.Utilities; using OfficeOpenXml.Style.XmlAccess; @@ -919,7 +920,7 @@ internal override List GetAxisValues(out bool isCount, out bool isNumeri { if (dl[0] is object[]) { - dl = dl.OrderBy(x => ((object[])x)[3]).ToList(); + dl = dl.OrderBy(x => ((object[])x)[3], new SortByComparer()).ToList(); } else { diff --git a/src/EPPlus/Drawing/Chart/ExcelLineChart.cs b/src/EPPlus/Drawing/Chart/ExcelLineChart.cs index 71157f5c38..cfabca73f2 100644 --- a/src/EPPlus/Drawing/Chart/ExcelLineChart.cs +++ b/src/EPPlus/Drawing/Chart/ExcelLineChart.cs @@ -382,5 +382,10 @@ internal override bool IsAxisTypeSupported(eAxisType type, ExcelChartAxis axis) } return base.IsAxisTypeSupported(type, axis); } + /// + /// Returns true if the drawing supports svg export via the . + /// + public override bool SupportsSvgExport => true; + } } diff --git a/src/EPPlus/Drawing/Chart/ExcelPieChart.cs b/src/EPPlus/Drawing/Chart/ExcelPieChart.cs index 541abfca35..8cc6dde817 100644 --- a/src/EPPlus/Drawing/Chart/ExcelPieChart.cs +++ b/src/EPPlus/Drawing/Chart/ExcelPieChart.cs @@ -123,6 +123,10 @@ internal set /// A collection of series for a Pie Chart /// public new ExcelChartSeries Series { get; } = new ExcelChartSeries(); + /// + /// Returns true if the drawing supports svg export via the . + /// + public override bool SupportsSvgExport => true; } } diff --git a/src/EPPlus/Drawing/ExcelDrawing.cs b/src/EPPlus/Drawing/ExcelDrawing.cs index 7ba71d3f5f..1b5d38fa86 100644 --- a/src/EPPlus/Drawing/ExcelDrawing.cs +++ b/src/EPPlus/Drawing/ExcelDrawing.cs @@ -20,6 +20,7 @@ Date Author Change using OfficeOpenXml.Drawing.Controls; using OfficeOpenXml.Drawing.OleObject; using OfficeOpenXml.Drawing.Slicer; +using OfficeOpenXml.Export.HtmlExport; using OfficeOpenXml.FormulaParsing.Excel.Functions.Text; using OfficeOpenXml.Packaging; using OfficeOpenXml.Utils.Drawings; @@ -1764,7 +1765,11 @@ public ExcelGroupShape ParentGroup } internal ExcelDrawingCustomGeometry CustomGeom { get; private set; } - + /// + /// Returns true if the drawing supports svg export via the . + /// + public virtual bool SupportsSvgExport { get => false; } + internal eDrawingInclude? IncludeInHtmlExport { get; set; } = eDrawingInclude.Include; internal virtual void DeleteMe() { TopNode.ParentNode.RemoveChild(TopNode); @@ -2595,6 +2600,7 @@ internal virtual void SaveDrawing(bool hasLoadedPivotTables) /// /// Converts the drawing to a SVG image. /// This is currently only supported for shapes, line-, column-, bar- and pie- charts. + /// Please use to verify svg export is supported. /// /// The svg image. /// If the drawing type is not supported diff --git a/src/EPPlus/Drawing/ExcelPicture.cs b/src/EPPlus/Drawing/ExcelPicture.cs index aeecc7c3b0..aeee3e88ac 100644 --- a/src/EPPlus/Drawing/ExcelPicture.cs +++ b/src/EPPlus/Drawing/ExcelPicture.cs @@ -628,5 +628,9 @@ public bool VerticalFlip internal PictureLocation LocationType = PictureLocation.None; internal ZipPackageRelationship LinkedImageRel = null; + /// + /// Returns true if the drawing supports svg export via the . + /// + public override bool SupportsSvgExport => true; } } \ No newline at end of file diff --git a/src/EPPlus/Drawing/ExcelShape.cs b/src/EPPlus/Drawing/ExcelShape.cs index 7af5c06558..6467461daa 100644 --- a/src/EPPlus/Drawing/ExcelShape.cs +++ b/src/EPPlus/Drawing/ExcelShape.cs @@ -104,6 +104,9 @@ public string ToSvg(SvgRenderOptions options) svg.Render(sr.RenderItems); return sb.ToString(); } - + /// + /// Returns true if the drawing supports svg export via the . + /// + public override bool SupportsSvgExport => true; } } diff --git a/src/EPPlus/Export/HtmlExport/Enums/eDrawingInclude.cs b/src/EPPlus/Export/HtmlExport/Enums/eDrawingInclude.cs index d092f66c83..a27c8617ae 100644 --- a/src/EPPlus/Export/HtmlExport/Enums/eDrawingInclude.cs +++ b/src/EPPlus/Export/HtmlExport/Enums/eDrawingInclude.cs @@ -6,28 +6,25 @@ namespace OfficeOpenXml.Export.HtmlExport { /// - /// What drawings to include in html export + /// How to include picture drawings in the html /// - [Flags] public enum eDrawingInclude { /// - /// Include no drawings + /// Do not include supported drawing objects in the html export. Default /// - None = 0, + Exclude, /// - /// Include Shapes + /// Include in css only, so they drawing images can be added manually. /// - Shapes = 2, + IncludeInCssOnly, /// - /// Include Charts + /// Include the drawings as images in the html export. /// - Charts = 4, - - //TODO: This is already handled by image enum. We may need restructure here + Include, /// - /// Include Images ? + /// Include the drawings as images in the HTML only . /// - Images = 8, + IncludeInHtmlOnly } } diff --git a/src/EPPlus/Export/HtmlExport/Enums/ePictureInclude.cs b/src/EPPlus/Export/HtmlExport/Enums/ePictureInclude.cs index b5e8f7bb97..171532fa04 100644 --- a/src/EPPlus/Export/HtmlExport/Enums/ePictureInclude.cs +++ b/src/EPPlus/Export/HtmlExport/Enums/ePictureInclude.cs @@ -11,11 +11,15 @@ Date Author Change 05/11/2021 EPPlus Software AB ExcelTable Html Export *************************************************************************************************/ +using System; + namespace OfficeOpenXml.Export.HtmlExport { /// - /// How to include picture drawings in the html + /// Obsolete: How to include picture drawings in the html + /// /// + [Obsolete("Use general Drawings.Include property to set drawing behaviour.")] public enum ePictureInclude { /// diff --git a/src/EPPlus/Export/HtmlExport/Enums/ePicturePosition.cs b/src/EPPlus/Export/HtmlExport/Enums/ePicturePosition.cs index 4252a49e49..979d16983a 100644 --- a/src/EPPlus/Export/HtmlExport/Enums/ePicturePosition.cs +++ b/src/EPPlus/Export/HtmlExport/Enums/ePicturePosition.cs @@ -11,11 +11,14 @@ Date Author Change 05/11/2021 EPPlus Software AB ExcelTable Html Export *************************************************************************************************/ +using System; + namespace OfficeOpenXml.Export.HtmlExport { /// /// If the Blip is absolut or relative to the table cell /// + [Obsolete("Use eDrawingPosition on the Drawing.Position property instead.")] public enum ePicturePosition { /// @@ -31,4 +34,23 @@ public enum ePicturePosition /// Relative } + /// + /// If the drawing image is absolut or relative to the table cell + /// + public enum eDrawingPosition + { + /// + /// No CSS is added for Position + /// + DontSet, + /// + /// Position is Absolute in the CSS + /// + Absolute, + /// + /// Position is Relative in the CSS + /// + Relative + } + } diff --git a/src/EPPlus/Export/HtmlExport/Exporters/ExcelHtmlExporterBase.cs b/src/EPPlus/Export/HtmlExport/Exporters/ExcelHtmlExporterBase.cs index ced1a36c9f..ff75f2b98a 100644 --- a/src/EPPlus/Export/HtmlExport/Exporters/ExcelHtmlExporterBase.cs +++ b/src/EPPlus/Export/HtmlExport/Exporters/ExcelHtmlExporterBase.cs @@ -15,6 +15,7 @@ Date Author Change using System.Linq; using System.Text; using OfficeOpenXml.Core; +using OfficeOpenXml.FormulaParsing.Excel.Functions.RefAndLookup; namespace OfficeOpenXml.Export.HtmlExport.Exporters { @@ -73,14 +74,14 @@ public EPPlusReadOnlyList Ranges private void AddRange(ExcelRangeBase range) { - if (range.IsFullColumn && range.IsFullRow) - { - _ranges.Add(new ExcelRangeBase(range.Worksheet, range.Worksheet.Dimension.Address)); - } - else - { + //if (range.IsFullColumn && range.IsFullRow) + //{ + // _ranges.Add(new ExcelRangeBase(range.Worksheet, range.Worksheet.Dimension.Address)); + //} + //else + //{ _ranges.Add(range); - } + //} } } } diff --git a/src/EPPlus/Export/HtmlExport/Exporters/Internal/AbstractRangeExporter.cs b/src/EPPlus/Export/HtmlExport/Exporters/Internal/AbstractRangeExporter.cs index ca8d63f8bf..eb5c528c63 100644 --- a/src/EPPlus/Export/HtmlExport/Exporters/Internal/AbstractRangeExporter.cs +++ b/src/EPPlus/Export/HtmlExport/Exporters/Internal/AbstractRangeExporter.cs @@ -15,6 +15,7 @@ Date Author Change using OfficeOpenXml.FormulaParsing.Excel.Functions.Text; using OfficeOpenXml.Table; using OfficeOpenXml.Utils.String; +using System; using System.Collections.Generic; using System.Linq; @@ -79,26 +80,7 @@ internal void LoadRangeDrawings(List ranges) ToColumnOff = toColOff }); } - else if(d is ExcelShape s) - { - s.GetFromBounds(out int fromRow, out int fromRowOff, out int fromCol, out int fromColOff); - s.GetToBounds(out int toRow, out int toRowOff, out int toCol, out int toColOff); - - _rangeDrawings.Add(new HtmlSvgDrawing() - { - WorksheetId = worksheet.PositionId, - Drawing = s, - FromRow = fromRow, - FromRowOff = fromRowOff, - FromColumn = fromCol, - FromColumnOff = fromColOff, - ToRow = toRow, - ToRowOff = toRowOff, - ToColumn = toCol, - ToColumnOff = toColOff - }); - } - else if(d is ExcelChart) + else if(d.SupportsSvgExport && (d is ExcelShape || d is ExcelChart)) { d.GetFromBounds(out int fromRow, out int fromRowOff, out int fromCol, out int fromColOff); d.GetToBounds(out int toRow, out int toRowOff, out int toCol, out int toColOff); @@ -168,5 +150,68 @@ protected HtmlSvgDrawing GetDrawing(int worksheetId, int row, int col) } return null; } + /// + /// Adjust all drawings for the worksheets dimension and include any draings that are outside the dimension. + /// + /// + /// + protected void AdjustRangeForDimensionAndDrawings(List ranges, bool includeDrawings) + { + for(int i=0;i drawMinRow) + { + if (toRowOff > 0) toRow++; + if (toColOff > 0) toCol++; + } + + if (range.Collide(fromRow, fromCol, toRow, toCol) != ExcelAddressBase.eAddressCollition.Inside) + { + if (fromRow < drawMinRow) drawMinRow = fromRow; + if (fromCol < drawMinCol) drawMinCol = fromCol; + if (toRow > drawMaxRow) drawMaxRow = toRow; + if (toCol > drawMaxCol) drawMaxCol = toCol; + } + } + } + + if (newRange != null && + newRange._fromRow > drawMinRow || + newRange._fromCol > drawMinCol || + newRange._toRow > drawMinRow || + newRange._toCol > drawMinCol) + { + return range.Worksheet.Cells[drawMinRow < newRange._fromRow ? Math.Max(drawMinRow, range._fromRow) : newRange._fromRow, + drawMinCol < newRange._fromCol ? Math.Max(drawMinCol, range._fromCol) : newRange._fromCol, + drawMaxRow > newRange._toRow ? Math.Min(drawMaxRow, range._toRow) : newRange._toRow, + drawMaxCol > newRange._toCol ? Math.Min(drawMaxCol, range._toCol) : newRange._toCol]; + } + } + return range.Worksheet.Cells[newRange.Address]; + } } } diff --git a/src/EPPlus/Export/HtmlExport/Exporters/Internal/CssExporterBase.cs b/src/EPPlus/Export/HtmlExport/Exporters/Internal/CssExporterBase.cs index 74dada4db1..80bd11ffed 100644 --- a/src/EPPlus/Export/HtmlExport/Exporters/Internal/CssExporterBase.cs +++ b/src/EPPlus/Export/HtmlExport/Exporters/Internal/CssExporterBase.cs @@ -51,13 +51,13 @@ public CssExporterBase(HtmlExportSettings settings, ExcelRangeBase range) if (range.Addresses == null) { - AddRange(range); + AddRange(range, settings.Drawings.Include!=eDrawingInclude.Exclude); } else { foreach (var address in range.Addresses) { - AddRange(range.Worksheet.Cells[address.Address]); + AddRange(range.Worksheet.Cells[address.Address], settings.Drawings.Include != eDrawingInclude.Exclude); } } } @@ -66,6 +66,7 @@ public CssExporterBase(HtmlRangeExportSettings settings, EPPlusReadOnlyList _ranges = new EPPlusReadOnlyList(); internal const string TableStyleClassPrefix = "ts-"; - private void AddRange(ExcelRangeBase range) + private void AddRange(ExcelRangeBase range, bool includeDrawings) { - if (range.IsFullColumn && range.IsFullRow) - { - _ranges.Add(new ExcelRangeBase(range.Worksheet, range.Worksheet.Dimension.Address)); - } - else - { - _ranges.Add(range); - } + //if (range.IsFullColumn && range.IsFullRow) + //{ + // _ranges.Add(new ExcelRangeBase(range.Worksheet, range.Worksheet.Dimension.Address)); + //} + //else + //{ + _ranges.Add(AdjustRangeForDimensionAndDrawings(range, includeDrawings)); + //} } protected CssRuleCollection CreateRuleCollection(HtmlRangeExportSettings settings) @@ -137,7 +138,7 @@ protected void AddCssRulesToCollection(CssRangeRuleCollection cssTranslator, Htm } } - if (Settings.Pictures.Include == ePictureInclude.Include || Settings.Pictures.Include == ePictureInclude.IncludeInCssOnly) + if (Settings.Drawings.Include == eDrawingInclude.Include || Settings.Drawings.Include == eDrawingInclude.IncludeInCssOnly) { LoadRangeDrawings(_ranges._list); foreach (var p in _rangePictures) @@ -146,7 +147,7 @@ protected void AddCssRulesToCollection(CssRangeRuleCollection cssTranslator, Htm } } - if(Settings.Drawings.Include == ePictureInclude.Include || Settings.Drawings.Include == ePictureInclude.IncludeInCssOnly) + if(Settings.Drawings.Include == eDrawingInclude.Include || Settings.Drawings.Include == eDrawingInclude.IncludeInCssOnly) { LoadRangeDrawings(_ranges._list); foreach(var d in _rangeDrawings) diff --git a/src/EPPlus/Export/HtmlExport/Exporters/Internal/HtmlExporterBaseInternal.cs b/src/EPPlus/Export/HtmlExport/Exporters/Internal/HtmlExporterBaseInternal.cs index 997be280a4..b21880691d 100644 --- a/src/EPPlus/Export/HtmlExport/Exporters/Internal/HtmlExporterBaseInternal.cs +++ b/src/EPPlus/Export/HtmlExport/Exporters/Internal/HtmlExporterBaseInternal.cs @@ -10,6 +10,7 @@ Date Author Change ************************************************************************************************* 6/4/2022 EPPlus Software AB ExcelTable Html Export *************************************************************************************************/ +using Microsoft.VisualBasic; using OfficeOpenXml.Core; using OfficeOpenXml.Drawing; using OfficeOpenXml.Drawing.Interfaces; @@ -41,13 +42,13 @@ public HtmlExporterBaseInternal(HtmlExportSettings settings, ExcelRangeBase rang if (range.Addresses == null) { - AddRange(range); + AddRange(range, settings.Drawings.Include!=eDrawingInclude.Exclude); } else { foreach (var address in range.Addresses) { - AddRange(range.Worksheet.Cells[address.Address]); + AddRange(range.Worksheet.Cells[address.Address], settings.Drawings.Include != eDrawingInclude.Exclude); } } @@ -58,6 +59,7 @@ public HtmlExporterBaseInternal(HtmlExportSettings settings, EPPlusReadOnlyList< { Settings = settings; Require.Argument(ranges).IsNotNull("ranges"); + AdjustRangeForDimensionAndDrawings(ranges._list, settings.Drawings.Include!=eDrawingInclude.Exclude); _ranges = ranges; //TODO: Fix support for all ranges LoadRangeDrawings(_ranges._list); @@ -150,12 +152,12 @@ protected HTMLElement GetThead(ExcelRangeBase range, List headers = null AddTableData(table, contentElement, col); - if ((Settings.Pictures.Include == ePictureInclude.Include) || (Settings.Pictures.Include == ePictureInclude.IncludeInHtmlOnly)) + if ((Settings.Drawings.Include == eDrawingInclude.Include) || (Settings.Drawings.Include == eDrawingInclude.IncludeInHtmlOnly)) { image = GetImage(cell.Worksheet.PositionId, cell._fromRow, cell._fromCol); } - if ((Settings.Drawings.Include == ePictureInclude.Include) || (Settings.Drawings.Include == ePictureInclude.IncludeInHtmlOnly)) + if ((Settings.Drawings.Include == eDrawingInclude.Include) || (Settings.Drawings.Include == eDrawingInclude.IncludeInHtmlOnly)) { drawing = GetDrawing(cell.Worksheet.PositionId, cell._fromRow, cell._fromCol); } @@ -278,12 +280,12 @@ protected HTMLElement GetTableBody(ExcelRangeBase range, int row, int endRow) SetColRowSpan(range, tblData, cell); - if ((Settings.Pictures.Include == ePictureInclude.Include) || (Settings.Pictures.Include == ePictureInclude.IncludeInHtmlOnly)) + if ((Settings.Drawings.Include == eDrawingInclude.Include) || (Settings.Drawings.Include == eDrawingInclude.IncludeInHtmlOnly)) { image = GetImage(cell.Worksheet.PositionId, cell._fromRow, cell._fromCol); } - if (Settings.Drawings.Include == (ePictureInclude.Include | ePictureInclude.IncludeInHtmlOnly)) + if (Settings.Drawings.Include == (eDrawingInclude.Include | eDrawingInclude.IncludeInHtmlOnly)) { drawing = GetDrawing(cell.Worksheet.PositionId, cell._fromRow, cell._fromCol); } @@ -450,12 +452,12 @@ protected void AddDrawing(HTMLElement parent, HtmlExportSettings settings, HtmlS var child = new HTMLElement(HtmlElements.Img); string drawingName = HtmlExportTableUtil.GetClassName(d.Drawing.Name, $"drawing{d.Drawing.Id}"); child.AddAttribute("alt", d.Drawing.Name); - if (settings.Pictures.AddNameAsId) + if (settings.Drawings.AddNameAsId) { child.AddAttribute("id", drawingName); } - if (settings.Drawings.Include == ePictureInclude.IncludeInHtmlOnly) + if (settings.Drawings.Include == eDrawingInclude.IncludeInHtmlOnly) { child = new HTMLElement(HtmlElements.Svg); child.ElementName = "div"; @@ -492,18 +494,10 @@ protected void LoadVisibleColumns(ExcelRangeBase range) protected EPPlusReadOnlyList _ranges = new EPPlusReadOnlyList(); - private void AddRange(ExcelRangeBase range) + private void AddRange(ExcelRangeBase range, bool includeDrawings) { - if (range.IsFullColumn && range.IsFullRow) - { - _ranges.Add(new ExcelRangeBase(range.Worksheet, range.Worksheet.Dimension.Address)); - } - else - { - _ranges.Add(range); - } + _ranges.Add(AdjustRangeForDimensionAndDrawings(new ExcelRangeBase(range.Worksheet, range.Worksheet.Dimension.Address), includeDrawings)); } - protected void ValidateRangeIndex(int rangeIndex) { if (rangeIndex < 0 || rangeIndex >= _ranges.Count) diff --git a/src/EPPlus/Export/HtmlExport/Settings/HtmlDrawingSettings.cs b/src/EPPlus/Export/HtmlExport/Settings/HtmlDrawingSettings.cs index d94bfbb869..6db076cbb1 100644 --- a/src/EPPlus/Export/HtmlExport/Settings/HtmlDrawingSettings.cs +++ b/src/EPPlus/Export/HtmlExport/Settings/HtmlDrawingSettings.cs @@ -1,32 +1,91 @@ -using System; +using OfficeOpenXml.Drawing; +using System; using System.Collections.Generic; using System.Linq; using System.Text; namespace OfficeOpenXml.Export.HtmlExport { + /// + /// Drawing handler + /// public class HtmlDrawingSettings { internal HtmlDrawingSettings() { } - - //Use picture for now. Possibly re-name /// - /// If how drawings should be included in the html. Default is + /// Optional handle to set individual settings for a drawing. Returning null will use the default settings. /// - public ePictureInclude Include = ePictureInclude.Exclude; - + public Func IndividualDrawingHandler { get; set; } = null; /// - /// Which type of drawing should be included + /// Option to handle if a drawing should be excluded or not. /// - public eDrawingInclude DrawTypeInclude = eDrawingInclude.None; - + public Func ExcludeDrawingHandler { get; set; } = null; /// - /// Is absolute by default for charts + /// If a drawing should be included in the export or not. /// - public ePicturePosition Position = ePicturePosition.DontSet; - + public eDrawingInclude Include = eDrawingInclude.Exclude; + /// + /// If the drawing image should be added as absolut or relative in the css. + /// + public eDrawingPosition Position { get; set; } = eDrawingPosition.Relative; + /// + /// If the margin in pixels from the top corner should be used. + /// If this property is set to true, the cells vertical alignment will be set to 'top', + /// otherwise alignment will be set to middle. + /// + public bool AddMarginTop { get; set; } = false; + /// + /// If the margin in pixels from the left corner should be used. + /// If this property is set to true, the cells text alignment will be set to 'left', + /// otherwise alignment will be set to center. + /// + public bool AddMarginLeft { get; set; } = false; + /// + /// If set to true the original size of the image is used, + /// otherwise the size in the workbook is used. Default is false. + /// + public bool KeepOriginalSizeOnPictures { get; set; } = false; + /// + /// Exclude settings + /// + public PictureCssExclude PictureCssExclude { get; } = new PictureCssExclude(); + /// + /// Adds the Blip name as Id for the img element in the HTML. + /// Characters [A-Z][0-9]-_ are allowed. The first character allows [A-Z]_. + /// Other characters will be replaced with an hyphen (-). + /// + public bool AddNameAsId + { + get; + set; + } = true; + /// + /// Reset the setting to it's default values. + /// + public void ResetToDefault() + { + Include = eDrawingInclude.Exclude; + Position = eDrawingPosition.Relative; + AddMarginLeft = false; + AddMarginTop = false; + KeepOriginalSizeOnPictures = false; + PictureCssExclude.ResetToDefault(); + } + /// + /// Copy the values from another settings object. + /// + /// The object to copy. + public void Copy(HtmlDrawingSettings copy) + { + Include = copy.Include; + Position = copy.Position; + AddMarginLeft = copy.AddMarginLeft; + AddMarginTop = copy.AddMarginTop; + KeepOriginalSizeOnPictures = copy.KeepOriginalSizeOnPictures; + PictureCssExclude.Copy(copy.PictureCssExclude); + } } } diff --git a/src/EPPlus/Export/HtmlExport/Settings/HtmlExportSettings.cs b/src/EPPlus/Export/HtmlExport/Settings/HtmlExportSettings.cs index 5cde7470e7..d1c1b8d671 100644 --- a/src/EPPlus/Export/HtmlExport/Settings/HtmlExportSettings.cs +++ b/src/EPPlus/Export/HtmlExport/Settings/HtmlExportSettings.cs @@ -11,6 +11,7 @@ Date Author Change 05/11/2021 EPPlus Software AB ExcelTable Html Export *************************************************************************************************/ using OfficeOpenXml.Export.HtmlExport.Accessibility; +using System; using System.Collections.Generic; using System.Globalization; using System.Text; @@ -129,15 +130,24 @@ public string DataValueAttributeName /// public string IconPrefix { get; set; } = "ic"; + HtmlPictureSettings _legacyPicturesSettings; /// /// If picture drawings will be included. Default is true. /// + [Obsolete("Use the Drawings.Pictures property instead")] public HtmlPictureSettings Pictures { - get; - } = new HtmlPictureSettings(); - /// - /// If and which Charts and/or Shapes will be included + get + { + if(_legacyPicturesSettings==null) + { + _legacyPicturesSettings = new HtmlPictureSettings(Drawings); + } + return _legacyPicturesSettings; + } + } + /// + /// If and which drawing objects like Charts, Pictures and/or Shapes will be included /// public HtmlDrawingSettings Drawings { diff --git a/src/EPPlus/Export/HtmlExport/Settings/HtmlPictureSettings.cs b/src/EPPlus/Export/HtmlExport/Settings/HtmlPictureSettings.cs index 366f25e081..66cbc928f1 100644 --- a/src/EPPlus/Export/HtmlExport/Settings/HtmlPictureSettings.cs +++ b/src/EPPlus/Export/HtmlExport/Settings/HtmlPictureSettings.cs @@ -11,47 +11,106 @@ Date Author Change 05/11/2021 EPPlus Software AB ExcelTable Html Export *************************************************************************************************/ +using System; + namespace OfficeOpenXml.Export.HtmlExport { /// /// Setting for rendering of picture drawings /// + [Obsolete("Use HtmlDrawingSettings instead (Settings.Drawings).")] public class HtmlPictureSettings { - internal HtmlPictureSettings() + HtmlDrawingSettings _drawingsSettings; + internal HtmlPictureSettings(HtmlDrawingSettings drawingsSettings) { - + _drawingsSettings = drawingsSettings; } /// /// If picture drawings should be included in the html. Default is /// - public ePictureInclude Include { get; set; } = ePictureInclude.Exclude; + public ePictureInclude Include + { + get + { + return (ePictureInclude)_drawingsSettings.Include; + } + set + { + _drawingsSettings.Include = (eDrawingInclude)value; + } + } /// /// If the image should be added as absolut or relative in the css. /// - public ePicturePosition Position { get; set; } = ePicturePosition.Relative; + public ePicturePosition Position + { + get + { + return (ePicturePosition)_drawingsSettings.Position; + } + set + { + _drawingsSettings.Position = (eDrawingPosition)value; + } + } /// /// If the margin in pixels from the top corner should be used. /// If this property is set to true, the cells vertical alignment will be set to 'top', /// otherwise alignment will be set to middle. /// - public bool AddMarginTop { get; set; } = false; + public bool AddMarginTop + { + get + { + return _drawingsSettings.AddMarginTop; + } + set + { + _drawingsSettings.AddMarginTop = value; + } + } /// /// If the margin in pixels from the left corner should be used. /// If this property is set to true, the cells text alignment will be set to 'left', /// otherwise alignment will be set to center. /// - public bool AddMarginLeft { get; set; } = false; - /// - /// If set to true the original size of the image is used, - /// otherwise the size in the workbook is used. Default is false. - /// - public bool KeepOriginalSize { get; set; } = false; + public bool AddMarginLeft + { + get + { + return _drawingsSettings.AddMarginLeft; + } + set + { + _drawingsSettings.AddMarginLeft = value; + } + } /// + /// If set to true the original size of the image is used, + /// otherwise the size in the workbook is used. Default is false. + /// + public bool KeepOriginalSize + { + get + { + return _drawingsSettings.KeepOriginalSizeOnPictures; + } + set + { + _drawingsSettings.KeepOriginalSizeOnPictures = value; + } + } /// /// Exclude settings /// - public PictureCssExclude CssExclude { get; } = new PictureCssExclude(); + public PictureCssExclude CssExclude + { + get + { + return _drawingsSettings.PictureCssExclude; + } + } /// /// Adds the Blip name as Id for the img element in the HTML. /// Characters [A-Z][0-9]-_ are allowed. The first character allows [A-Z]_. @@ -59,9 +118,15 @@ internal HtmlPictureSettings() /// public bool AddNameAsId { - get; - set; - } = true; + get + { + return _drawingsSettings.AddMarginLeft; + } + set + { + _drawingsSettings.AddMarginLeft = value; + } + } /// /// Reset the setting to it's default values. /// diff --git a/src/EPPlus/Export/HtmlExport/Translators/CssDrawingPropertiesTranslator.cs b/src/EPPlus/Export/HtmlExport/Translators/CssDrawingPropertiesTranslator.cs index dab122d097..68b3d85c52 100644 --- a/src/EPPlus/Export/HtmlExport/Translators/CssDrawingPropertiesTranslator.cs +++ b/src/EPPlus/Export/HtmlExport/Translators/CssDrawingPropertiesTranslator.cs @@ -39,7 +39,7 @@ internal CssDrawingPropertiesTranslator(HtmlSvgDrawing d) internal override List GenerateDeclarationList(TranslatorContext context) { - if (context.Drawings.Position == ePicturePosition.Relative) + if (context.Drawings.Position == eDrawingPosition.Relative) { if (_bounds.Left != 0) { @@ -50,7 +50,7 @@ internal override List GenerateDeclarationList(TranslatorContext co AddDeclaration("top", $"{_bounds.Top.PointToPixel():F0}px"); } } - else if (context.Drawings.Position == ePicturePosition.Absolute) + else if (context.Drawings.Position == eDrawingPosition.Absolute) { if (_bounds.Left != 0) { @@ -62,7 +62,7 @@ internal override List GenerateDeclarationList(TranslatorContext co } } - if (context.Pictures.KeepOriginalSize == false) + if (context.Drawings.KeepOriginalSizeOnPictures == false) { if (_width != _bounds.Width) { @@ -74,7 +74,7 @@ internal override List GenerateDeclarationList(TranslatorContext co } } - if (_border.LineStyle != null && context.Pictures.CssExclude.Border == false) + if (_border.LineStyle != null && context.Drawings.PictureCssExclude.Border == false) { var border = GetDrawingBorder(); AddDeclaration("border", border); diff --git a/src/EPPlus/Export/HtmlExport/Translators/CssImageAlignmentTranslator.cs b/src/EPPlus/Export/HtmlExport/Translators/CssImageAlignmentTranslator.cs index 46dc89bf32..f62228a5a2 100644 --- a/src/EPPlus/Export/HtmlExport/Translators/CssImageAlignmentTranslator.cs +++ b/src/EPPlus/Export/HtmlExport/Translators/CssImageAlignmentTranslator.cs @@ -17,17 +17,17 @@ namespace OfficeOpenXml.Export.HtmlExport.Translators { internal class CssImageAlignmentTranslator : TranslatorBase { - HtmlPictureSettings _picSettings; + HtmlDrawingSettings _drawingsSettings; - internal CssImageAlignmentTranslator(HtmlPictureSettings picSettings) + internal CssImageAlignmentTranslator(HtmlDrawingSettings drawingsSettings) { - _picSettings = picSettings; + _drawingsSettings = drawingsSettings; } internal override List GenerateDeclarationList(TranslatorContext context) { - AddDeclaration("vertical-align", _picSettings.AddMarginTop ? "top" : "middle"); - AddDeclaration("text-align", _picSettings.AddMarginLeft ? "left" : "center"); + AddDeclaration("vertical-align", _drawingsSettings.AddMarginTop ? "top" : "middle"); + AddDeclaration("text-align", _drawingsSettings.AddMarginLeft ? "left" : "center"); return declarations; } diff --git a/src/EPPlus/Export/HtmlExport/Translators/CssImagePropertiesTranslator.cs b/src/EPPlus/Export/HtmlExport/Translators/CssImagePropertiesTranslator.cs index 1d3ee325fe..e1a44b77a0 100644 --- a/src/EPPlus/Export/HtmlExport/Translators/CssImagePropertiesTranslator.cs +++ b/src/EPPlus/Export/HtmlExport/Translators/CssImagePropertiesTranslator.cs @@ -34,7 +34,7 @@ internal CssImagePropertiesTranslator(HtmlImage image) internal override List GenerateDeclarationList(TranslatorContext context) { - if (context.Pictures.KeepOriginalSize == false) + if (context.Drawings.KeepOriginalSizeOnPictures == false) { if (_width != _bounds.Width) { @@ -46,7 +46,7 @@ internal override List GenerateDeclarationList(TranslatorContext co } } - if (_border.LineStyle != null && context.Pictures.CssExclude.Border == false) + if (_border.LineStyle != null && context.Drawings.PictureCssExclude.Border == false) { var border = GetDrawingBorder(); AddDeclaration("border", border); diff --git a/src/EPPlus/Export/HtmlExport/Translators/CssImageTranslator.cs b/src/EPPlus/Export/HtmlExport/Translators/CssImageTranslator.cs index e3648b2a8c..b70bbd30a7 100644 --- a/src/EPPlus/Export/HtmlExport/Translators/CssImageTranslator.cs +++ b/src/EPPlus/Export/HtmlExport/Translators/CssImageTranslator.cs @@ -47,23 +47,23 @@ internal override List GenerateDeclarationList(TranslatorContext co { AddDeclaration("content", $"url('data:{GetContentType(type.Value)};base64,{_encodedImage}')"); - if (context.Pictures.Position != ePicturePosition.DontSet) + if (context.Drawings.Position != eDrawingPosition.DontSet) { - AddDeclaration("position", $"{context.Pictures.Position.ToString().ToLower()}"); + AddDeclaration("position", $"{context.Drawings.Position.ToString().ToLower()}"); } - if(isDrawing && context.Drawings.Position != ePicturePosition.DontSet) + if(isDrawing && context.Drawings.Position != eDrawingPosition.DontSet) { AddDeclaration("position", $"{context.Drawings.Position.ToString().ToLower()}"); } - if (_p.FromColumnOff != 0 && context.Pictures.AddMarginLeft) + if (_p.FromColumnOff != 0 && context.Drawings.AddMarginLeft) { var leftOffset = _p.FromColumnOff / ExcelPicture.EMU_PER_PIXEL; AddDeclaration("margin-left", $"{leftOffset}px"); } - if (_p.FromRowOff != 0 && context.Pictures.AddMarginTop) + if (_p.FromRowOff != 0 && context.Drawings.AddMarginTop) { var topOffset = _p.FromRowOff / ExcelPicture.EMU_PER_PIXEL; AddDeclaration("margin-top", $"{topOffset}px"); diff --git a/src/EPPlus/Export/HtmlExport/Translators/TranslatorContext.cs b/src/EPPlus/Export/HtmlExport/Translators/TranslatorContext.cs index 92e90b0b6b..ee45883c06 100644 --- a/src/EPPlus/Export/HtmlExport/Translators/TranslatorContext.cs +++ b/src/EPPlus/Export/HtmlExport/Translators/TranslatorContext.cs @@ -33,7 +33,6 @@ internal class TranslatorContext internal CssExclude Exclude; internal CssExportSettings Settings; - internal HtmlPictureSettings Pictures; internal HtmlDrawingSettings Drawings; private TranslatorBase strategy; @@ -53,14 +52,12 @@ public TranslatorContext(HtmlRangeExportSettings settings) { Exclude = settings.Css.CssExclude; Settings = settings.Css; - Pictures = settings.Pictures; Drawings = settings.Drawings; } public TranslatorContext(HtmlTableExportSettings settings, CssExclude exclude) { Settings = settings.Css; - Pictures = settings.Pictures; Drawings = settings.Drawings; Exclude = exclude; } diff --git a/src/EPPlusTest/Export/HtmlExport/RangeExporterTests.cs b/src/EPPlusTest/Export/HtmlExport/RangeExporterTests.cs index 7ec061fb6a..9a27e3c33d 100644 --- a/src/EPPlusTest/Export/HtmlExport/RangeExporterTests.cs +++ b/src/EPPlusTest/Export/HtmlExport/RangeExporterTests.cs @@ -222,9 +222,8 @@ public async Task TaskWriteChartAndShape() var setting = exporter.Settings.Drawings; - setting.DrawTypeInclude = eDrawingInclude.Shapes & eDrawingInclude.Shapes; - setting.Include = ePictureInclude.Include; - setting.Position = ePicturePosition.Absolute; + setting.Include = eDrawingInclude.Include; + setting.Position = eDrawingPosition.Absolute; exporter.Settings.Minify = false; exporter.Settings.Encoding = Encoding.UTF8; @@ -253,9 +252,8 @@ public async Task TaskWriteChartSimple() var setting = exporter.Settings.Drawings; - setting.DrawTypeInclude = eDrawingInclude.Charts; - setting.Include = ePictureInclude.Include; - setting.Position = ePicturePosition.Relative; + setting.Include = eDrawingInclude.Include; + setting.Position = eDrawingPosition.Relative; exporter.Settings.Minify = false; exporter.Settings.Encoding = Encoding.UTF8; @@ -282,7 +280,7 @@ public async Task WriteImagesAsync() exporter.Settings.SetColumnWidth = true; exporter.Settings.SetRowHeight = true; - exporter.Settings.Pictures.Include = ePictureInclude.Include; + exporter.Settings.Drawings.Include = eDrawingInclude.Include; exporter.Settings.Minify = false; exporter.Settings.Encoding = Encoding.UTF8; var html = exporter.GetSinglePage(); @@ -303,7 +301,7 @@ public async Task WriteImagesAsyncHTMLOnlyEmbed() exporter.Settings.SetColumnWidth = true; exporter.Settings.SetRowHeight = true; - exporter.Settings.Pictures.Include = ePictureInclude.IncludeInHtmlOnly; + exporter.Settings.Drawings.Include = eDrawingInclude.IncludeInHtmlOnly; exporter.Settings.Minify = false; exporter.Settings.Encoding = Encoding.UTF8; diff --git a/src/EPPlusTest/Export/HtmlExport/SvgShapeExportTests.cs b/src/EPPlusTest/Export/HtmlExport/SvgShapeExportTests.cs index 9ae2dfb929..74d731f5ed 100644 --- a/src/EPPlusTest/Export/HtmlExport/SvgShapeExportTests.cs +++ b/src/EPPlusTest/Export/HtmlExport/SvgShapeExportTests.cs @@ -40,8 +40,7 @@ public void ExportBasicShapeWorksheet() var exporter = ws.Cells["A1:C20"].CreateHtmlExporter(); - exporter.Settings.Drawings.Include = ePictureInclude.IncludeInHtmlOnly; - exporter.Settings.Drawings.DrawTypeInclude = eDrawingInclude.Shapes; + exporter.Settings.Drawings.Include = eDrawingInclude.IncludeInHtmlOnly; var htmlPage = exporter.GetSinglePage(); @@ -147,8 +146,7 @@ public void ExportBarChartWithCategories() var exporter = ws.Cells["A1:C20"].CreateHtmlExporter(); - exporter.Settings.Drawings.Include = ePictureInclude.IncludeInHtmlOnly; - exporter.Settings.Drawings.DrawTypeInclude = eDrawingInclude.Charts; + exporter.Settings.Drawings.Include = eDrawingInclude.IncludeInHtmlOnly; var htmlPage = exporter.GetSinglePage(); From e64e6fe406b6dbf0388b0098e3562ba054fbc907 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ossian=20Edstr=C3=B6m?= Date: Fri, 28 Aug 2026 16:05:20 +0200 Subject: [PATCH 49/73] Fixed gradientFill for pieslices --- .../Svg/Core/SvgShapeRenderer.cs | 30 +++++++++++++++++++ .../Renderer/Chart/ChartTitleRenderer.cs | 6 ++-- .../Chart/ChartTypeDrawers/ChartTypeDrawer.cs | 4 +-- .../ChartTypeDrawers/PieChartTypeDrawer.cs | 2 +- .../Renderer/Chart/PieSliceRenderItem.cs | 2 +- 5 files changed, 37 insertions(+), 7 deletions(-) diff --git a/src/EPPlus.DrawingRenderer/Svg/Core/SvgShapeRenderer.cs b/src/EPPlus.DrawingRenderer/Svg/Core/SvgShapeRenderer.cs index c45b1f9c05..f3f5e993c2 100644 --- a/src/EPPlus.DrawingRenderer/Svg/Core/SvgShapeRenderer.cs +++ b/src/EPPlus.DrawingRenderer/Svg/Core/SvgShapeRenderer.cs @@ -632,6 +632,36 @@ private string GetXy(RenderItem item, UserSpaceSettings userSpace, double? angle return $" x1=\"{(x1).PointToPixelString("0.00")}\" x2=\"{(x2).PointToPixelString("0.00")}\" y1=\"{y1.PointToPixelString("0.00")}\" y2=\"{y2.PointToPixelString("0.00")}\""; } + else if (angle.HasValue && angle != 0) + { + var x1 = 0D; + var x2 = 0D; + var y1 = 0D; + var y2 = 0D; + angle %= 360; + if (angle <= 90) + { + x2 = 1D - Math.Sin(MathHelper.Radians(angle.Value)); + y2 = Math.Sin(MathHelper.Radians(angle.Value)); + } + else if (angle <= 180) + { + y2 = Math.Sin(MathHelper.Radians(angle.Value)); + x1 = 1D - Math.Sin(MathHelper.Radians(angle.Value)); + } + else if (angle <= 270) + { + y1 = Math.Sin(MathHelper.Radians(angle.Value - 180)); + x1 = 1D - Math.Sin(MathHelper.Radians(angle.Value - 180)); + } + else + { + y1 = Math.Sin(MathHelper.Radians(angle.Value - 180)); + x2 = 1D - Math.Sin(MathHelper.Radians(angle.Value - 180)); + } + + return $" x1=\"{(x1).ToString("0.00%", CultureInfo.InvariantCulture)}\" x2=\"{(x2).ToString("0.00%", CultureInfo.InvariantCulture)}\" y1=\"{y1.ToString("0.00%", CultureInfo.InvariantCulture)}\" y2=\"{y2.ToString("0.00%", CultureInfo.InvariantCulture)}\""; + } return ""; } diff --git a/src/EPPlus/Drawing/Renderer/Chart/ChartTitleRenderer.cs b/src/EPPlus/Drawing/Renderer/Chart/ChartTitleRenderer.cs index 44c2a0e3a8..4a3dc9f5c9 100644 --- a/src/EPPlus/Drawing/Renderer/Chart/ChartTitleRenderer.cs +++ b/src/EPPlus/Drawing/Renderer/Chart/ChartTitleRenderer.cs @@ -104,9 +104,9 @@ internal ChartTitleRenderer(ChartRenderer sc, ExcelChartTitleStandard t, string SetAxisTitleRect(sc, axis); } } - - Rectangle.SetDrawingPropertiesFill(sc.Theme, t.Fill, sc.Chart.StyleManager.Style?.Title.FillReference.Color, UserSpaceSettings.ObjectBoundingBox, DefaultFillColor); - Rectangle.SetDrawingPropertiesBorder(sc.Theme, t.Border, sc.Chart.StyleManager.Style?.Title.BorderReference.Color, t.Border.Fill.Style != eFillStyle.NoFill, () => DefaultBorderColor, 0.75); + //Default NoFill for title and axis titles if not set + Rectangle.SetDrawingPropertiesFill(sc.Theme, t.Fill, sc.Chart.StyleManager.Style?.Title.FillReference.Color, UserSpaceSettings.ObjectBoundingBox, null); + Rectangle.SetDrawingPropertiesBorder(sc.Theme, t.Border, sc.Chart.StyleManager.Style?.Title.BorderReference.Color, t.Border.Fill.Style != eFillStyle.NoFill, () => null, 0.75); } private void SetAxisTitleRect(ChartRenderer sc, ChartAxisRenderer axis) diff --git a/src/EPPlus/Drawing/Renderer/Chart/ChartTypeDrawers/ChartTypeDrawer.cs b/src/EPPlus/Drawing/Renderer/Chart/ChartTypeDrawers/ChartTypeDrawer.cs index 6fcb6f1137..69c75a57b1 100644 --- a/src/EPPlus/Drawing/Renderer/Chart/ChartTypeDrawers/ChartTypeDrawer.cs +++ b/src/EPPlus/Drawing/Renderer/Chart/ChartTypeDrawers/ChartTypeDrawer.cs @@ -233,12 +233,12 @@ internal bool IsOnAxis(ExcelChartAxisStandard ax) { return _chartType.YAxis==ax || _chartType.XAxis==ax; } - internal static void SetFillDataPoint(ExcelChart chart, ExcelChartStandardSerie cStandardSerie, int index, RenderItem item, ExcelChartDataPoint dp, ExcelChartStyleEntry entry) + internal static void SetFillDataPoint(ExcelChart chart, ExcelChartStandardSerie cStandardSerie, int index, RenderItem item, ExcelChartDataPoint dp, ExcelChartStyleEntry entry, UserSpaceSettings spaceSettings = UserSpaceSettings.UserSpaceOnUse_Object) { 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.UserSpaceOnUse_Object, color); + item.SetDrawingPropertiesFill(theme, dp.Fill.IsEmpty ? cStandardSerie.Fill : dp.Fill, entry?.FillReference.Color, spaceSettings, color); item.SetDrawingPropertiesBorder(theme, dp.Border.IsEmpty ? cStandardSerie.Border : dp.Border, entry?.BorderReference.Color, dp.Border.Fill.Style != eFillStyle.NoFill, null, 0.75); } diff --git a/src/EPPlus/Drawing/Renderer/Chart/ChartTypeDrawers/PieChartTypeDrawer.cs b/src/EPPlus/Drawing/Renderer/Chart/ChartTypeDrawers/PieChartTypeDrawer.cs index b09fac446a..ad94c2507b 100644 --- a/src/EPPlus/Drawing/Renderer/Chart/ChartTypeDrawers/PieChartTypeDrawer.cs +++ b/src/EPPlus/Drawing/Renderer/Chart/ChartTypeDrawers/PieChartTypeDrawer.cs @@ -259,7 +259,7 @@ internal override void DrawSeries() //maxBoundsForBestFit.Parent = innerGroup; var bounds = Slices[j].GetBounds(); - + //BoundingBox box = new BoundingBox(bounds.Left, bounds.Top, bounds.Width, bounds.Height); //box.Parent = innerGroup; diff --git a/src/EPPlus/Drawing/Renderer/Chart/PieSliceRenderItem.cs b/src/EPPlus/Drawing/Renderer/Chart/PieSliceRenderItem.cs index dabad2e3da..7f95d2c25b 100644 --- a/src/EPPlus/Drawing/Renderer/Chart/PieSliceRenderItem.cs +++ b/src/EPPlus/Drawing/Renderer/Chart/PieSliceRenderItem.cs @@ -327,7 +327,7 @@ internal void ImportStlyeInfo(ExcelPieChartSerie serie, ExcelPieChart chartType, if (position >= 0 && serie.DataPoints.ContainsKey(position)) { var dp = serie.DataPoints[position]; - ChartTypeDrawer.SetFillDataPoint(Chart, serie, position, _slicePath, dp, Chart.StyleManager.Style?.SeriesLine); + ChartTypeDrawer.SetFillDataPoint(Chart, serie, position, _slicePath, dp, Chart.StyleManager.Style?.SeriesLine, UserSpaceSettings.ObjectBoundingBox); } else { From fa4fc551ac35669f235378266a118351563770d1 Mon Sep 17 00:00:00 2001 From: OssianEPPlus <122265629+OssianEPPlus@users.noreply.github.com> Date: Tue, 1 Sep 2026 08:48:46 +0200 Subject: [PATCH 50/73] Fix for #2488 BorderStyle.None now writes correctly (#2489) --- src/EPPlus/Style/Dxf/DxfStyleHandler.cs | 6 +- src/EPPlus/Style/Dxf/ExcelDxfBorder.cs | 31 ++++- .../Style/Dxf/ExcelDxfStyleLimitedFont.cs | 2 +- .../Issues/ConditionalFormattingIssues.cs | 123 ++++++++++++++++++ 4 files changed, 154 insertions(+), 8 deletions(-) diff --git a/src/EPPlus/Style/Dxf/DxfStyleHandler.cs b/src/EPPlus/Style/Dxf/DxfStyleHandler.cs index e830f44400..5da4198c15 100644 --- a/src/EPPlus/Style/Dxf/DxfStyleHandler.cs +++ b/src/EPPlus/Style/Dxf/DxfStyleHandler.cs @@ -210,7 +210,9 @@ private static void UpdateConditionalFormatting(ExcelWorksheet ws, ExcelStyleCol { foreach (var cf in ws.ConditionalFormatting) { - if (cf.Style.HasValue) + //If at least one border exists then a dxf style for the border must be added even if the value for that border is empty + //(Thus meaning HasValue is false) + if (cf.Style.HasValue || cf.Style.Border != null && cf.Style.Border.AtLeastOneBorderExists()) { var standardDxfStyle = cf.Style.ToDxfStyle(); @@ -228,11 +230,9 @@ private static void UpdateConditionalFormatting(ExcelWorksheet ws, ExcelStyleCol { ((ExcelConditionalFormattingRule)cf).DxfId = ix; cf.Style.DxfId = ix; - //cf.Style.DxfId = ix; } } } - //var num = dxfs._list[129]; } internal static void CopyDxfStylesTable(ExcelTable tblFrom, ExcelTable tblTo) { diff --git a/src/EPPlus/Style/Dxf/ExcelDxfBorder.cs b/src/EPPlus/Style/Dxf/ExcelDxfBorder.cs index 5b51e8a3c7..2b369f1883 100644 --- a/src/EPPlus/Style/Dxf/ExcelDxfBorder.cs +++ b/src/EPPlus/Style/Dxf/ExcelDxfBorder.cs @@ -205,14 +205,36 @@ internal override DxfStyleBase Clone() Horizontal = (ExcelDxfBorderItem)Horizontal.Clone(), }; } + + private bool BorderItemExists(ExcelDxfBorderItem bi) + { + if (bi.Style != null) + { + return true; + } + return false; + } + + internal bool AtLeastOneBorderExists() + { + if (BorderItemExists(Left)) return true; + if (BorderItemExists(Right)) return true; + if (BorderItemExists(Bottom)) return true; + if (BorderItemExists(Top)) return true; + if (BorderItemExists(Vertical)) return true; + if (BorderItemExists(Horizontal)) return true; + + return false; + } + internal override void SetValuesFromXml(XmlHelper helper) { if (helper.ExistsNode("d:border")) { Left = GetBorderItem(helper, "d:border/d:left", eStyleClass.BorderLeft); - Right = GetBorderItem(helper, "d:border/d:right", eStyleClass.BorderLeft); - Bottom = GetBorderItem(helper, "d:border/d:bottom", eStyleClass.BorderLeft); - Top = GetBorderItem(helper, "d:border/d:top", eStyleClass.BorderLeft); + Right = GetBorderItem(helper, "d:border/d:right", eStyleClass.BorderRight); + Bottom = GetBorderItem(helper, "d:border/d:bottom", eStyleClass.BorderBottom); + Top = GetBorderItem(helper, "d:border/d:top", eStyleClass.BorderTop); Vertical = GetBorderItem(helper, "d:border/d:vertical", eStyleClass.Border); Horizontal = GetBorderItem(helper, "d:border/d:horizontal", eStyleClass.Border); } @@ -224,7 +246,8 @@ private ExcelDxfBorderItem GetBorderItem(XmlHelper helper, string path, eStyleCl if (exists) { var style = helper.GetXmlNodeString(path + "/@style"); - bi.Style = GetBorderStyleEnum(style); + //When exists and border has no style the CT_BorderPr\ST_BorderStyle node defaults to BorderNone if the node exists even when empty + bi.Style = GetBorderStyleEnum(style) ?? ExcelBorderStyle.None; bi.Color = GetColor(helper, path + "/d:color", styleClass); } return bi; diff --git a/src/EPPlus/Style/Dxf/ExcelDxfStyleLimitedFont.cs b/src/EPPlus/Style/Dxf/ExcelDxfStyleLimitedFont.cs index 4e5a15ae73..08a4d370e6 100644 --- a/src/EPPlus/Style/Dxf/ExcelDxfStyleLimitedFont.cs +++ b/src/EPPlus/Style/Dxf/ExcelDxfStyleLimitedFont.cs @@ -52,7 +52,7 @@ internal override void CreateNodes(XmlHelper helper, string path) { if (Font.HasValue) Font.CreateNodes(helper, "d:font"); if (Fill.HasValue) Fill.CreateNodes(helper, "d:fill"); - if (Border.HasValue) Border.CreateNodes(helper, "d:border"); + if (Border.HasValue || Border.AtLeastOneBorderExists()) Border.CreateNodes(helper, "d:border"); } /// /// If the object has any properties set diff --git a/src/EPPlusTest/Issues/ConditionalFormattingIssues.cs b/src/EPPlusTest/Issues/ConditionalFormattingIssues.cs index baba29937b..e7d47de04f 100644 --- a/src/EPPlusTest/Issues/ConditionalFormattingIssues.cs +++ b/src/EPPlusTest/Issues/ConditionalFormattingIssues.cs @@ -7,6 +7,8 @@ using System.Drawing; using System.Globalization; using System.IO; +using System.Linq; +using System.Runtime.InteropServices; using System.Threading; namespace EPPlusTest.Issues @@ -262,6 +264,127 @@ public void RoundTrip_ExtIconSetWithNumericFormulaCfvo_DoesNotThrow() } } + + [TestMethod] + public void ReadEppGeneratedCFBorderStylesCorrectly() + { + using (var p = OpenPackage("i2488_EppGen.xlsx", true)) + { + var ws = p.Workbook.Worksheets.Add("readBorderNone"); + + var notEqual = ws.Cells["C1:C5"].ConditionalFormatting.AddNotEqual(); + + notEqual.Formula = "1"; + + ws.Cells["D5"].Style.Border.Left.Style = ExcelBorderStyle.None; + + //Set a borderstyle to thick so that Conditional Formatting can then Set it to None + ws.Cells["C2"].Style.Border.Left.Style = ExcelBorderStyle.Thick; + ws.Cells["C2"].Style.Border.Top.Style = ExcelBorderStyle.Thick; + ws.Cells["C2"].Style.Border.Right.Style = ExcelBorderStyle.Thick; + ws.Cells["C2"].Style.Border.Bottom.Style = ExcelBorderStyle.Thick; + + notEqual.Style.Border.Left.Style = ExcelBorderStyle.None; + notEqual.Style.Border.Top.Style = ExcelBorderStyle.None; + notEqual.Style.Border.Right.Style = ExcelBorderStyle.None; + notEqual.Style.Border.Bottom.Style = ExcelBorderStyle.None; + + SaveAndCleanup(p); + } + using (var p = OpenPackage("i2488_EppGen.xlsx", false)) + { + var ws = p.Workbook.Worksheets[0]; + var cfs = ws.Cells["C1:C5"].ConditionalFormatting.GetConditionalFormattings(); + + var rightStyle = cfs[0].Style.Border.Right.Style; + Assert.AreEqual(ExcelBorderStyle.None, ws.Cells["D5"].Style.Border.Left.Style); + //Assert that we can read borderStyle None. This is different from NoNode and the same as an Empty border node + Assert.AreEqual(ExcelBorderStyle.None, cfs[0].Style.Border.Right.Style); + Assert.AreEqual(ExcelBorderStyle.None, cfs[0].Style.Border.Left.Style); + Assert.AreEqual(ExcelBorderStyle.None, cfs[0].Style.Border.Top.Style); + Assert.AreEqual(ExcelBorderStyle.None, cfs[0].Style.Border.Bottom.Style); + } + } + + /// + /// Epplus did not write out ExcelBorderStyle.None correctly into DXF styles + /// Causing Cell-Styling to be applied instead of the conditionallyFormatted "None" style + /// + [TestMethod] + public void CopyingIssue2488_ActualCase() + { + string expectedId = ""; + var fi = GetOutputFile("", "i2488_out.xlsx"); + var copiedName = ""; + + using (var p = OpenTemplatePackage("i2488.xlsx")) + { + var wkbook = p.Workbook; + var wkSheet = wkbook.Worksheets[0]; + + var conditionalFormatting = wkSheet.Cells["S15"].ConditionalFormatting.GetConditionalFormattings().First(cf => cf.Address.Address == "S7:Y22"); + var dxfId = conditionalFormatting.DxfId; + expectedId = conditionalFormatting.Style.ToDxfStyle().Id; + Assert.AreEqual(expectedId, wkbook.Styles.Dxfs[dxfId].Id); + var sheetName = wkSheet.Name; + var copyCount = 1; + + copiedName = $"{sheetName}_{1}"; + + for (int i = 1; i <= copyCount; i++) + { + p.Workbook.Worksheets.Copy( + wkSheet.Name, $"{sheetName}_{i}"); + } + + wkSheet = wkbook.Worksheets[1]; + sheetName = wkbook.Worksheets[1].Name; + + for (int i = 1; i <= copyCount; i++) + { + p.Workbook.Worksheets.Copy( + wkSheet.Name, $"{sheetName}_{i}"); + } + p.SaveAs(fi); + } + using (var p = OpenPackage(fi.Name, false)) + { + var wkbook = p.Workbook; + var wkSheet = wkbook.Worksheets[copiedName]; + + var conditionalFormatting = wkSheet.Cells["S15"].ConditionalFormatting.GetConditionalFormattings().First(cf => cf.Address.Address == "S7:Y22"); + Assert.AreEqual(expectedId, conditionalFormatting.Style.ToDxfStyle().Id); + } + } + + [TestMethod] + public void CopyingIssue2488_StyleRead() + { + string expectedId = ""; + var fi = GetOutputFile("", "i2488_Read_out.xlsx"); + + using (var p = OpenTemplatePackage("i2488.xlsx")) + { + var wkbook = p.Workbook; + var wkSheet = wkbook.Worksheets[0]; + + var conditionalFormatting = wkSheet.Cells["S15"].ConditionalFormatting.GetConditionalFormattings().First(cf=> cf.Address.Address == "S7:Y22"); + var dxfId = conditionalFormatting.DxfId; + expectedId = conditionalFormatting.Style.ToDxfStyle().Id; + Assert.AreEqual(expectedId, wkbook.Styles.Dxfs[dxfId].Id); + p.SaveAs(fi); + } + + using (var p = OpenPackage(fi.Name, false)) + { + var wkbook = p.Workbook; + var wkSheet = wkbook.Worksheets[0]; + + var conditionalFormatting = wkSheet.Cells["S15"].ConditionalFormatting.GetConditionalFormattings().First(cf => cf.Address.Address == "S7:Y22"); + Assert.AreEqual(expectedId, conditionalFormatting.Style.ToDxfStyle().Id); + } + } + [TestMethod] public void RoundTrip_RegularIconSetWithNumericFormulaCfvo_DoesNotThrow() { From 230f864c860da2dc4402fee152b2e9c6b2d664a0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jan=20K=C3=A4llman?= Date: Tue, 1 Sep 2026 10:24:50 +0200 Subject: [PATCH 51/73] Fixed html export for drawings-positioning etc --- .../Chart/LineChartToSvgTests.cs | 9 +- src/EPPlus/ExcelRange.cs | 1 + src/EPPlus/ExcelRangeBase.cs | 11 ++- .../CssCollections/CssTableRuleCollection.cs | 53 ++++++++++- .../Exporters/ExcelHtmlExporterBase.cs | 9 +- .../Internal/AbstractRangeExporter.cs | 1 + .../Exporters/Internal/CssExporterBase.cs | 28 ++++++ .../Exporters/Internal/HtmlExportTableUtil.cs | 95 +++++++++++++++++-- .../Internal/HtmlExporterBaseInternal.cs | 45 ++++++--- .../Internal/HtmlRangeExporterBase.cs | 13 +++ .../Internal/HtmlTableExporterBase.cs | 8 +- .../Translators/AttributeTranslator.cs | 6 +- .../CssDrawingPropertiesTranslator.cs | 11 +-- .../Translators/CssImageTranslator.cs | 16 ++-- src/EPPlus/Table/ExcelTable.cs | 20 ++++ 15 files changed, 268 insertions(+), 58 deletions(-) diff --git a/src/EPPlus.DrawingRenderer.Tests/Chart/LineChartToSvgTests.cs b/src/EPPlus.DrawingRenderer.Tests/Chart/LineChartToSvgTests.cs index ca4f3d7766..f1cb598734 100644 --- a/src/EPPlus.DrawingRenderer.Tests/Chart/LineChartToSvgTests.cs +++ b/src/EPPlus.DrawingRenderer.Tests/Chart/LineChartToSvgTests.cs @@ -386,22 +386,25 @@ public void HtmlExportWithLineChart() chart.SetPosition(0, 0); chart.To.Row = 14; chart.To.Column = 10; - chart.StyleManager.SetChartStyle(ePresetChartStyle.LineChartStyle4); + chart.StyleManager.SetChartStyle(ePresetChartStyle.LineChartStyle7); var exporter = sheet.Cells.CreateHtmlExporter(); var settings = exporter.Settings; - settings.Drawings.Include = eDrawingInclude.IncludeInHtmlOnly; + settings.Drawings.Include = eDrawingInclude.Include; settings.Culture = CultureInfo.InvariantCulture; + settings.SetRowHeight = true; + settings.SetColumnWidth = true; settings.TableId = "currency-table"; settings.AdditionalTableClassNames.Add("table"); settings.AdditionalTableClassNames.Add("table-sm"); settings.AdditionalTableClassNames.Add("table-borderless"); + settings.Drawings.Position = eDrawingPosition.Absolute; SaveWorkbook("HtmlExportWithLineChart.xlsx", package); // export css and html //var css = exporter.GetCssString(); //var html = exporter.GetHtmlString(); var html = exporter.GetSinglePage(); - + SaveSvg("HtmlExportWithLineChart.html", html); } diff --git a/src/EPPlus/ExcelRange.cs b/src/EPPlus/ExcelRange.cs index f253b1d5ca..d5676cb4fb 100644 --- a/src/EPPlus/ExcelRange.cs +++ b/src/EPPlus/ExcelRange.cs @@ -187,5 +187,6 @@ public void SetFormula(string formula, bool asSharedFormula = true) } } } + } } diff --git a/src/EPPlus/ExcelRangeBase.cs b/src/EPPlus/ExcelRangeBase.cs index 5e55aebba4..ecd4ffa0b6 100644 --- a/src/EPPlus/ExcelRangeBase.cs +++ b/src/EPPlus/ExcelRangeBase.cs @@ -35,6 +35,7 @@ Date Author Change using System.Collections.Generic; using System.Globalization; using System.IO; +using System.Linq; using System.Threading; using System.Threading.Tasks; @@ -774,7 +775,7 @@ internal ExcelAddressBase GetAddressDimension() { GetAddressDimensionFullRowAndColumn(out int dimFromRow, out int dimFromCol, out int dimToRow, out int dimToCol); //If the range is only full column or full row the dimension of the worksheet, return null. - if (dimFromCol==0 || dimFromRow>dimToCol || dimFromCol > dimToCol) + if (dimFromCol==0 || dimFromRow>dimToRow || dimFromCol > dimToCol) { return null; } @@ -2943,5 +2944,13 @@ public bool UseImplicitItersection } } } + internal ExcelTable GetIntersectingTable() + { + if (_worksheet == null) + { + return null; + } + return _worksheet.Tables.GetIntersectingRanges(this).Select(x => x.Value).FirstOrDefault(); + } } } diff --git a/src/EPPlus/Export/HtmlExport/CssCollections/CssTableRuleCollection.cs b/src/EPPlus/Export/HtmlExport/CssCollections/CssTableRuleCollection.cs index 762981b769..334aa31644 100644 --- a/src/EPPlus/Export/HtmlExport/CssCollections/CssTableRuleCollection.cs +++ b/src/EPPlus/Export/HtmlExport/CssCollections/CssTableRuleCollection.cs @@ -19,6 +19,8 @@ Date Author Change using OfficeOpenXml.Style.Table; using OfficeOpenXml.Table; using System.Collections.Generic; +using System.Data; +using System.Text; using static OfficeOpenXml.Export.HtmlExport.ColumnDataTypeManager; namespace OfficeOpenXml.Export.HtmlExport.CssCollections @@ -104,13 +106,22 @@ internal void AddAlignment(string name, List dataTypes) } } - internal void AddToCollection(string name, ExcelTableStyleElement element, string htmlElement) + internal void AddToCollection(string name, ExcelTableStyleElement element, string htmlElement, bool isRangeName=false) { if (element.Style.HasValue == false) return; //Dont add empty elements var s = element.Style; - var styleClass = new CssRule($"table.{name}{htmlElement}",int.MaxValue); + string rule; + if (isRangeName) + { + rule = $".{name}"; + } + else + { + rule = $"table.{name}{htmlElement}"; + } + var styleClass = new CssRule(rule,int.MaxValue); var translators = new List(); @@ -202,7 +213,45 @@ internal void AddTableToCollection(ExcelTable table, List datatypes, str var tableClassFC = $"{tableClass}-first-column"; AddToCollection($"{tableClassFC}", tblStyle.FirstColumn, " tbody tr td:first-child"); } + internal void AddRangeTableToCollection(ExcelTable table, List datatypes, string tableClassPreset) + { + var tblStyle = table.GetTableNamedStyle(); + + var tableClass = HtmlExportTableUtil.GetTableBaseClassName(table, true); + + //AddHyperlink($"{tableClass}-default", tblStyle.WholeTable); + //AddAlignment($"{tableClass}", datatypes); + + AddToCollection($"{tableClass}-default", tblStyle.WholeTable,"", true); + // AddToCollectionVH($"{tableClass}", tblStyle.WholeTable, ""); + + //Header + AddToCollection($"{tableClass}-header", tblStyle.HeaderRow, "", true); + //AddToCollectionVH($"{tableClass}", tblStyle.HeaderRow, ""); + AddToCollection($"{tableClass}-last-header-cell", tblStyle.LastHeaderCell, "", true); + AddToCollection($"{tableClass}-first-header-cell", tblStyle.FirstHeaderCell, "", true); + + //Total + AddToCollection($"{tableClass}-total", tblStyle.TotalRow, "", true); + //AddToCollectionVH($"{tableClass}", tblStyle.TotalRow, ""); + AddToCollection($"{tableClass}-last-total-cell", tblStyle.LastTotalCell, "", true); + AddToCollection($"{tableClass}-first-total-cell", tblStyle.FirstTotalCell, "", true); + + //Columns stripes + AddToCollection($"{tableClass}-first-col-stripe", tblStyle.FirstColumnStripe, "", true); + AddToCollection($"{tableClass}-second-col-stripe", tblStyle.SecondColumnStripe, "", true); + + //Row stripes + AddToCollection($"{tableClass}-first-row-stripe", tblStyle.FirstRowStripe, "", true); + AddToCollection($"{tableClass}-second-row-stripe", tblStyle.SecondRowStripe, "", true); + + //Last column + AddToCollection($"{tableClass}-last-col", tblStyle.LastColumn, "", true); + + //First column + AddToCollection($"{tableClass}-first-col", tblStyle.FirstColumn, "", true); + } internal void AddOtherCollectionToThisCollection(CssRuleCollection otherCollection) { foreach (var otherRule in otherCollection) diff --git a/src/EPPlus/Export/HtmlExport/Exporters/ExcelHtmlExporterBase.cs b/src/EPPlus/Export/HtmlExport/Exporters/ExcelHtmlExporterBase.cs index ff75f2b98a..8297c38a79 100644 --- a/src/EPPlus/Export/HtmlExport/Exporters/ExcelHtmlExporterBase.cs +++ b/src/EPPlus/Export/HtmlExport/Exporters/ExcelHtmlExporterBase.cs @@ -74,14 +74,7 @@ public EPPlusReadOnlyList Ranges private void AddRange(ExcelRangeBase range) { - //if (range.IsFullColumn && range.IsFullRow) - //{ - // _ranges.Add(new ExcelRangeBase(range.Worksheet, range.Worksheet.Dimension.Address)); - //} - //else - //{ - _ranges.Add(range); - //} + _ranges.Add(range); } } } diff --git a/src/EPPlus/Export/HtmlExport/Exporters/Internal/AbstractRangeExporter.cs b/src/EPPlus/Export/HtmlExport/Exporters/Internal/AbstractRangeExporter.cs index eb5c528c63..6d55fa85d9 100644 --- a/src/EPPlus/Export/HtmlExport/Exporters/Internal/AbstractRangeExporter.cs +++ b/src/EPPlus/Export/HtmlExport/Exporters/Internal/AbstractRangeExporter.cs @@ -30,6 +30,7 @@ public AbstractHtmlExporter() internal const string TableClass = "epplus-table"; internal List _rangePictures = null; internal List _rangeDrawings = null; + protected bool _hasIntersectingTables = false; //Intersecting tables; protected List _dataTypes = new List(); protected ExporterContext _exporterContext; diff --git a/src/EPPlus/Export/HtmlExport/Exporters/Internal/CssExporterBase.cs b/src/EPPlus/Export/HtmlExport/Exporters/Internal/CssExporterBase.cs index 80bd11ffed..1ae9d0b759 100644 --- a/src/EPPlus/Export/HtmlExport/Exporters/Internal/CssExporterBase.cs +++ b/src/EPPlus/Export/HtmlExport/Exporters/Internal/CssExporterBase.cs @@ -135,6 +135,26 @@ protected void AddCssRulesToCollection(CssRangeRuleCollection cssTranslator, Htm ); addedTableStyles.Add(table.TableStyle); } + else + { + var tables = range.Worksheet.Tables.GetIntersectingRanges(range).Select(x=>x.Value).ToList(); + if(tables.Count>0) + { + if (tableSettings == null) + { + tableSettings = new HtmlTableExportSettings() { Minify = Settings.Minify }; + } + + foreach (var t in tables) + { + cssTranslator.AddOtherCollectionToThisCollection + ( + CreateRangeTableCssRules(t, tableSettings, _dataTypes).RuleCollection + ); + addedTableStyles.Add(t.TableStyle); + } + } + } } } @@ -309,6 +329,14 @@ internal static CssTableRuleCollection CreateTableCssRules(ExcelTable table, Htm return tableRules; } + internal static CssTableRuleCollection CreateRangeTableCssRules(ExcelTable table, HtmlTableExportSettings settings, List datatypes) + { + var tableRules = new CssTableRuleCollection(table, settings); + var tableClass = HtmlExportTableUtil.GetTableBaseClassName(table, true); + tableRules.AddRangeTableToCollection(table, datatypes, tableClass); + + return tableRules; + } internal CssWriter GetTableCssWriter(Stream stream, ExcelTable table, HtmlTableExportSettings tableSettings) { diff --git a/src/EPPlus/Export/HtmlExport/Exporters/Internal/HtmlExportTableUtil.cs b/src/EPPlus/Export/HtmlExport/Exporters/Internal/HtmlExportTableUtil.cs index e6cf4ec662..95e04416ed 100644 --- a/src/EPPlus/Export/HtmlExport/Exporters/Internal/HtmlExportTableUtil.cs +++ b/src/EPPlus/Export/HtmlExport/Exporters/Internal/HtmlExportTableUtil.cs @@ -12,9 +12,14 @@ Date Author Change *************************************************************************************************/ using OfficeOpenXml.Export.HtmlExport.HtmlCollections; using OfficeOpenXml.Export.HtmlExport.Settings; +using OfficeOpenXml.FormulaParsing.Excel.Functions.MathFunctions; using OfficeOpenXml.Table; +using System; + + #if !NET35 && !NET40 using System.Threading.Tasks; +using static OfficeOpenXml.RichData.Structures.Constants.SpecialKeyNames; #endif namespace OfficeOpenXml.Export.HtmlExport.Exporters.Internal @@ -68,15 +73,7 @@ internal static string GetWorksheetClassName(string styleClassPrefix, string nam internal static string GetTableClasses(ExcelTable table) { - string styleClass; - if (table.TableStyle == TableStyles.Custom) - { - styleClass = TableStyleClassPrefix + GetClassName(table.StyleName, $"tablestyle{table.Id}"); - } - else - { - styleClass = TableStyleClassPrefix + table.TableStyle.ToString().ToLowerInvariant(); - } + string styleClass = GetTableBaseClassName(table, false); var tblClasses = $"{styleClass}"; if (table.ShowHeader) @@ -112,6 +109,25 @@ internal static string GetTableClasses(ExcelTable table) return tblClasses; } + internal static string GetTableBaseClassName(ExcelTable table, bool inRangeName) + { + string styleClass; + if (table.TableStyle == TableStyles.Custom) + { + styleClass = TableStyleClassPrefix + GetClassName(table.StyleName, $"tablestyle{table.Id}"); + } + else + { + styleClass = TableStyleClassPrefix + table.TableStyle.ToString().ToLowerInvariant(); + if(inRangeName) + { + return styleClass + $"-tbl{table.Id}"; + } + } + + return styleClass; + } + internal static void AddClassesAttributes(HTMLElement element, ExcelTable table, HtmlTableExportSettings settings) { if (table.TableStyle == TableStyles.None) @@ -137,5 +153,66 @@ internal static void AddClassesAttributes(HTMLElement element, ExcelTable table, element.AddAttribute(HtmlAttributes.Id, settings.TableId); } } + + internal static string GetInRangeTableClass(ExcelRangeBase cell, ExcelTable tbl) + { + + var tblStyle = tbl.GetTableNamedStyle(); + + var styleClass = GetTableBaseClassName(tbl, true); + var classes = styleClass + "-default"; + var tblAdr = tbl.Address; + + if (tbl.ShowHeader && tblAdr._fromRow == cell._fromRow && tblAdr._fromCol == cell._fromCol && tblStyle.FirstHeaderCell.Style.HasValue) + { + classes += " " + styleClass + "-first-header-cell"; + } + if (tbl.ShowHeader && tblAdr._fromRow == cell._fromRow && tblAdr._toCol == cell._fromCol && tblStyle.LastHeaderCell.Style.HasValue) + { + classes += " " + styleClass + "-last-header-cell"; + } + else if (tbl.ShowHeader && tblAdr._fromRow == cell._fromRow && tblStyle.HeaderRow.Style.HasValue) + { + classes += " " + styleClass + "-header"; + } + else if (tbl.ShowTotal && tblAdr._toRow == cell._fromRow && tblAdr._fromRow == cell._fromRow && tblStyle.FirstTotalCell.Style.HasValue) + { + classes += " " + styleClass + "-first-total-cell"; + } + else if (tbl.ShowTotal && tblAdr._toRow == cell._fromRow && tblAdr._toRow == cell._fromRow && tblStyle.LastTotalCell.Style.HasValue) + { + classes += " " + styleClass + "-last-total-cell"; + } + else if (tbl.ShowTotal && tblAdr._toRow == cell._fromRow && tblStyle.TotalRow.Style.HasValue) + { + classes += " " + styleClass + "-total"; + } + else if (tbl.ShowFirstColumn && tblAdr._fromCol == cell._fromCol && tblStyle.FirstColumn.Style.HasValue) + { + classes += " " + styleClass + "-first-col"; + } + else if (tbl.ShowLastColumn && tblAdr._toCol == cell._fromCol && tblStyle.LastColumn.Style.HasValue) + { + classes += " " + styleClass + "-last-col"; + } + else if (tbl.ShowRowStripes && ((cell._fromRow - (tbl.ShowHeader ? tbl.Address._fromRow + 1 : tbl.Address._fromRow)) % 2) == 0 && (tblStyle.FirstRowStripe.Style.HasValue)) + { + classes += " " + styleClass + "-first-row-stripe"; + } + else if (tbl.ShowRowStripes && ((cell._fromRow - (tbl.ShowHeader ? tbl.Address._fromRow + 1 : tbl.Address._fromRow)) % 2) == 1 && (tblStyle.SecondRowStripe.Style.HasValue)) + { + classes += " " + styleClass + "-second-row-stripe"; + } + else if (tbl.ShowColumnStripes && ((cell._fromCol - tbl.Address._fromCol) % 2) == 0) + { + classes += " " + styleClass + "-first-col-stripe"; + } + else if (tbl.ShowColumnStripes && ((cell._fromCol - tbl.Address._fromCol) % 2) == 1) + { + classes += " " + styleClass + "-second-col-stripe"; + } + + return classes; + } } } diff --git a/src/EPPlus/Export/HtmlExport/Exporters/Internal/HtmlExporterBaseInternal.cs b/src/EPPlus/Export/HtmlExport/Exporters/Internal/HtmlExporterBaseInternal.cs index b21880691d..e0c19bbfee 100644 --- a/src/EPPlus/Export/HtmlExport/Exporters/Internal/HtmlExporterBaseInternal.cs +++ b/src/EPPlus/Export/HtmlExport/Exporters/Internal/HtmlExporterBaseInternal.cs @@ -114,7 +114,7 @@ protected HTMLElement GetThead(ExcelRangeBase range, List headers = null { table = range.GetTable(); } - + ExcelTable inRangeTable = null; int headerRows = GetHeaderRows(table); for (int i = 0; i < headerRows; i++) @@ -140,16 +140,20 @@ protected HTMLElement GetThead(ExcelRangeBase range, List headers = null HTMLElement contentElement; + if (_hasIntersectingTables && Settings.TableStyle == eHtmlRangeTableInclude.Include) + { + inRangeTable = cell.GetIntersectingTable(); + } + if (Settings.IncludeCssClassNames) { - GetClassData(th, true, image, cell, Settings, _exporterContext, out contentElement, true); + GetClassData(th, true, image, cell, Settings, _exporterContext, inRangeTable, out contentElement, true); } else { contentElement = th; } - AddTableData(table, contentElement, col); if ((Settings.Drawings.Include == eDrawingInclude.Include) || (Settings.Drawings.Include == eDrawingInclude.IncludeInHtmlOnly)) @@ -231,7 +235,7 @@ protected HTMLElement GetTableBody(ExcelRangeBase range, int row, int endRow) } var table = range.GetTable(); - + ExcelTable inRangeTable=null; var ws = range.Worksheet; HtmlImage image = null; HtmlDrawing drawing = null; @@ -273,7 +277,10 @@ protected HTMLElement GetTableBody(ExcelRangeBase range, int row, int endRow) if (InMergeCellSpan(row, col)) continue; var colIx = col - range._fromCol; var cell = ws.Cells[row, col]; - + if(Settings.TableStyle== eHtmlRangeTableInclude.Include && _hasIntersectingTables) + { + inRangeTable = cell.GetIntersectingTable(); + } var dataType = HtmlRawDataProvider.GetHtmlDataTypeFromValue(cell.Value); var tblData = new HTMLElement(HtmlElements.TableData); @@ -292,12 +299,13 @@ protected HTMLElement GetTableBody(ExcelRangeBase range, int row, int endRow) if (cell.Hyperlink == null) { - var addRowScope = table == null ? false : table.ShowFirstColumn && col == table.Address._fromCol || table.ShowLastColumn && col == table.Address._toCol; - AddTableDataFromCell(cell, dataType, tblData, Settings, addRowScope, image, _exporterContext); + var t = table ?? inRangeTable; + var addRowScope = t == null ? false : t.ShowFirstColumn && col == t.Address._fromCol || t.ShowLastColumn && col == t.Address._toCol; + AddTableDataFromCell(cell, dataType, tblData, Settings, addRowScope, image, _exporterContext, inRangeTable); } else { - GetClassData(tblData, table != null, image, cell, Settings, _exporterContext, out HTMLElement contentElement); + GetClassData(tblData, table != null, image, cell, Settings, _exporterContext, inRangeTable, out HTMLElement contentElement); AddImage(contentElement, Settings, image, cell.Value); AddHyperlink(contentElement, cell, Settings); @@ -425,12 +433,12 @@ protected void AddImage(HTMLElement parent, HtmlExportSettings settings, HtmlIma var name = GetPictureName(image); string imageName = HtmlExportTableUtil.GetClassName(image.Picture.Name, ((IPictureContainer)image.Picture).ImageHash); child.AddAttribute("alt", image.Picture.Name); - if (settings.Pictures.AddNameAsId) + if (settings.Drawings.AddNameAsId) { child.AddAttribute("id", imageName); } - if(settings.Pictures.Include == ePictureInclude.IncludeInHtmlOnly) + if(settings.Drawings.Include == eDrawingInclude.IncludeInHtmlOnly) { ePictureType? type; var _encodedImage = ImageEncoder.EncodeImage(image, out type); @@ -715,12 +723,19 @@ protected void AddClassesAttributes(HTMLElement element, ExcelTable table, strin } } - internal void GetClassData(HTMLElement element, bool isTable, HtmlImage image, ExcelRangeBase cell, HtmlExportSettings settings, ExporterContext content, out HTMLElement valueElement, bool isHeader = false) + internal void GetClassData(HTMLElement element, bool isTable, HtmlImage image, ExcelRangeBase cell, HtmlExportSettings settings, ExporterContext content, ExcelTable inRangeTable, out HTMLElement valueElement, bool isHeader = false) { - var imageCellClassName = GetImageCellClassName(image, Settings, isTable); - var classString = AttributeTranslator.GetClassAttributeFromStyle(cell, isHeader, settings, imageCellClassName, content); + var additionalCellClassName = GetImageCellClassName(image, Settings, isTable); + + if (inRangeTable != null) + { + additionalCellClassName += (string.IsNullOrEmpty(additionalCellClassName) ? "" : " ") + HtmlExportTableUtil.GetInRangeTableClass(cell, inRangeTable); + } + + var classString = AttributeTranslator.GetClassAttributeFromStyle(cell, isHeader, settings, additionalCellClassName, content); var stylesAndExtras = AttributeTranslator.GetConditionalFormattings(cell, settings, content, ref classString); + if (cell.Style.Checkbox) { if (cell.Value == null || HtmlRawDataProvider.GetHtmlDataTypeFromValue(cell.Value) == HtmlDataTypes.Boolean) @@ -812,7 +827,7 @@ private static bool IsTextRotationExcluded(HtmlExportSettings settings, bool isH } - public void AddTableDataFromCell(ExcelRangeBase cell, string dataType, HTMLElement element, HtmlExportSettings settings, bool addRowScope, HtmlImage image, ExporterContext content) + public void AddTableDataFromCell(ExcelRangeBase cell, string dataType, HTMLElement element, HtmlExportSettings settings, bool addRowScope, HtmlImage image, ExporterContext content, ExcelTable inRangeTable) { if (dataType != ColumnDataTypeManager.HtmlDataTypes.String && settings.RenderDataAttributes) { @@ -831,7 +846,7 @@ public void AddTableDataFromCell(ExcelRangeBase cell, string dataType, HTMLEleme } } - GetClassData(element, true, image, cell, settings, content, out HTMLElement contentElement); + GetClassData(element, true, image, cell, settings, content, inRangeTable, out HTMLElement contentElement); AddImage(contentElement, settings, image, cell.Value); diff --git a/src/EPPlus/Export/HtmlExport/Exporters/Internal/HtmlRangeExporterBase.cs b/src/EPPlus/Export/HtmlExport/Exporters/Internal/HtmlRangeExporterBase.cs index ab8b9d5614..8acd781d7a 100644 --- a/src/EPPlus/Export/HtmlExport/Exporters/Internal/HtmlRangeExporterBase.cs +++ b/src/EPPlus/Export/HtmlExport/Exporters/Internal/HtmlRangeExporterBase.cs @@ -13,7 +13,9 @@ Date Author Change using OfficeOpenXml.Core; using OfficeOpenXml.Export.HtmlExport.HtmlCollections; using OfficeOpenXml.Table; +using System; using System.Collections.Generic; +using System.Linq; namespace OfficeOpenXml.Export.HtmlExport.Exporters.Internal { @@ -36,12 +38,18 @@ protected HTMLElement GenerateHTML(int rangeIndex, ExcelHtmlOverrideExportSettin ValidateRangeIndex(rangeIndex); _mergedCells.Clear(); var range = _ranges[rangeIndex]; + var rangeAddress = range.DimensionAdjustedAddress; + range = range.Worksheet.Cells[rangeAddress.Address]; GetDataTypes(range, _settings); ExcelTable table = null; if (Settings.TableStyle != eHtmlRangeTableInclude.Exclude) { table = range.GetTable(); + if(table==null) + { + _hasIntersectingTables = HasIntersctingTables(range); + } } var tableId = GetTableId(rangeIndex, overrideSettings); @@ -71,6 +79,11 @@ protected HTMLElement GenerateHTML(int rangeIndex, ExcelHtmlOverrideExportSettin return htmlTable; } + private bool HasIntersctingTables(ExcelRangeBase range) + { + return range.Worksheet.Tables.GetIntersectingRanges(range).Count>0; + } + private void AddTableRows(HTMLElement htmlTable, ExcelRangeBase range) { var row = range._fromRow + _settings.HeaderRows; diff --git a/src/EPPlus/Export/HtmlExport/Exporters/Internal/HtmlTableExporterBase.cs b/src/EPPlus/Export/HtmlExport/Exporters/Internal/HtmlTableExporterBase.cs index 64633caec8..0920d1539d 100644 --- a/src/EPPlus/Export/HtmlExport/Exporters/Internal/HtmlTableExporterBase.cs +++ b/src/EPPlus/Export/HtmlExport/Exporters/Internal/HtmlTableExporterBase.cs @@ -146,6 +146,7 @@ private void AddTotalRow(HTMLElement table) var address = _table.Address; HtmlImage image = null; + ExcelTable inRangeTable = null; foreach (var col in _columns) { var tblData = new HTMLElement(HtmlElements.TableData); @@ -155,7 +156,12 @@ private void AddTotalRow(HTMLElement table) { tblData.AddAttribute("role", "cell"); } - GetClassData(tblData, true, image, cell, Settings, _exporterContext, out HTMLElement contentElement); + if (Settings.TableStyle == eHtmlRangeTableInclude.Include && _hasIntersectingTables) + { + inRangeTable = cell.GetIntersectingTable(); + } + + GetClassData(tblData, true, image, cell, Settings, _exporterContext, inRangeTable, out HTMLElement contentElement); AddImage(contentElement, Settings, image, cell.Value); diff --git a/src/EPPlus/Export/HtmlExport/Translators/AttributeTranslator.cs b/src/EPPlus/Export/HtmlExport/Translators/AttributeTranslator.cs index de7595f503..b2d1964551 100644 --- a/src/EPPlus/Export/HtmlExport/Translators/AttributeTranslator.cs +++ b/src/EPPlus/Export/HtmlExport/Translators/AttributeTranslator.cs @@ -49,11 +49,11 @@ internal static string GetClassAttributeFromStyle(ExcelRangeBase cell, bool isHe { if (ConvertUtil.IsNumericOrDate(cell.Value)) { - cls = $"{styleClassPrefix}ar"; + cls += $" {styleClassPrefix}ar"; } else if (isHeader) { - cls = $"{styleClassPrefix}al"; + cls += $" {styleClassPrefix}al"; } } @@ -93,7 +93,7 @@ internal static string GetClassAttributeFromStyle(ExcelRangeBase cell, bool isHe } } - return cls; + return cls.Trim(); } internal static List GetConditionalFormattings(ExcelRangeBase cell, HtmlExportSettings settings, ExporterContext context, ref string cls) diff --git a/src/EPPlus/Export/HtmlExport/Translators/CssDrawingPropertiesTranslator.cs b/src/EPPlus/Export/HtmlExport/Translators/CssDrawingPropertiesTranslator.cs index 68b3d85c52..88e8e80590 100644 --- a/src/EPPlus/Export/HtmlExport/Translators/CssDrawingPropertiesTranslator.cs +++ b/src/EPPlus/Export/HtmlExport/Translators/CssDrawingPropertiesTranslator.cs @@ -52,14 +52,9 @@ internal override List GenerateDeclarationList(TranslatorContext co } else if (context.Drawings.Position == eDrawingPosition.Absolute) { - if (_bounds.Left != 0) - { - AddDeclaration("left", $"{_bounds.GlobalLeft.PointToPixel():F0}px"); - } - if (_bounds.Top != 0) - { - AddDeclaration("top", $"{_bounds.GlobalTop.PointToPixel():F0}px"); - } + AddDeclaration("position", $"{context.Drawings.Position.ToString().ToLower()}"); + AddDeclaration("left", $"{_bounds.GlobalLeft.PointToPixel():F0}px"); + AddDeclaration("top", $"{_bounds.GlobalTop.PointToPixel():F0}px"); } if (context.Drawings.KeepOriginalSizeOnPictures == false) diff --git a/src/EPPlus/Export/HtmlExport/Translators/CssImageTranslator.cs b/src/EPPlus/Export/HtmlExport/Translators/CssImageTranslator.cs index b70bbd30a7..c02f2f4d17 100644 --- a/src/EPPlus/Export/HtmlExport/Translators/CssImageTranslator.cs +++ b/src/EPPlus/Export/HtmlExport/Translators/CssImageTranslator.cs @@ -47,15 +47,15 @@ internal override List GenerateDeclarationList(TranslatorContext co { AddDeclaration("content", $"url('data:{GetContentType(type.Value)};base64,{_encodedImage}')"); - if (context.Drawings.Position != eDrawingPosition.DontSet) - { - AddDeclaration("position", $"{context.Drawings.Position.ToString().ToLower()}"); - } + //if (context.Drawings.Position != eDrawingPosition.DontSet) + //{ + // AddDeclaration("position", $"{context.Drawings.Position.ToString().ToLower()}"); + //} - if(isDrawing && context.Drawings.Position != eDrawingPosition.DontSet) - { - AddDeclaration("position", $"{context.Drawings.Position.ToString().ToLower()}"); - } + //if(isDrawing && context.Drawings.Position != eDrawingPosition.DontSet) + //{ + // AddDeclaration("position", $"{context.Drawings.Position.ToString().ToLower()}"); + //} if (_p.FromColumnOff != 0 && context.Drawings.AddMarginLeft) { diff --git a/src/EPPlus/Table/ExcelTable.cs b/src/EPPlus/Table/ExcelTable.cs index b4f4b07da7..ed270f97c8 100644 --- a/src/EPPlus/Table/ExcelTable.cs +++ b/src/EPPlus/Table/ExcelTable.cs @@ -33,6 +33,8 @@ Date Author Change using OfficeOpenXml.Data.QueryTable; using OfficeOpenXml.Data.Connection.IOHandlers; using OfficeOpenXml.Utils.EnumUtils; +using OfficeOpenXml.Style.Table; + @@ -1522,5 +1524,23 @@ public ExcelTable Copy(ExcelRangeBase range) Range.Copy(range); return WorkSheet.Tables.FirstOrDefault(x => x.Address.Collide(range) != ExcelAddressBase.eAddressCollition.No); } + + internal ExcelTableNamedStyle GetTableNamedStyle() + { + ExcelTableNamedStyle tblStyle; + if (TableStyle == TableStyles.Custom) + { + tblStyle = WorkSheet.Workbook.Styles.TableStyles[StyleName].As.TableStyle; + } + else + { + var tmpNode = WorkSheet.Workbook.StylesXml.CreateElement("c:tableStyle"); + tblStyle = new ExcelTableNamedStyle(WorkSheet.Workbook.Styles.NameSpaceManager, tmpNode, WorkSheet.Workbook.Styles); + tblStyle.SetFromTemplate(TableStyle); + } + + return tblStyle; + } } } + From 82a9b8ce72b57186042df71537637b124aec6e07 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ossian=20Edstr=C3=B6m?= Date: Tue, 1 Sep 2026 11:09:00 +0200 Subject: [PATCH 52/73] Fixed default position right --- .../Drawing/Renderer/Chart/DataLabels/SvgDataLabelPoint.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/src/EPPlus/Drawing/Renderer/Chart/DataLabels/SvgDataLabelPoint.cs b/src/EPPlus/Drawing/Renderer/Chart/DataLabels/SvgDataLabelPoint.cs index 2ca6fa5f45..34c1765723 100644 --- a/src/EPPlus/Drawing/Renderer/Chart/DataLabels/SvgDataLabelPoint.cs +++ b/src/EPPlus/Drawing/Renderer/Chart/DataLabels/SvgDataLabelPoint.cs @@ -73,6 +73,7 @@ eLabelPosition GetDefaultPositionBasedOnChartType(ExcelChartDataLabelStandard st case eChartType.LineStacked: case eChartType.XYScatterLines: case eChartType.Bubble: + return eLabelPosition.Right; case eChartType.StockHLC: case eChartType.StockVOHLC: case eChartType.StockVHLC: From b5ee104de2322e56f6219fa334b741774464eaa0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ossian=20Edstr=C3=B6m?= Date: Tue, 1 Sep 2026 12:32:28 +0200 Subject: [PATCH 53/73] Adjusted default borderwidth/series icon for datalabels --- src/EPPlus/Drawing/Renderer/ChartRenderer.cs | 7 ++++--- .../Renderer/RenderItems/DrawingRenderItemExtentions.cs | 2 +- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/src/EPPlus/Drawing/Renderer/ChartRenderer.cs b/src/EPPlus/Drawing/Renderer/ChartRenderer.cs index 520e2618a1..cb03fa7abd 100644 --- a/src/EPPlus/Drawing/Renderer/ChartRenderer.cs +++ b/src/EPPlus/Drawing/Renderer/ChartRenderer.cs @@ -480,17 +480,18 @@ internal double GetPlotAreaTop() internal LineRenderItem GetSeriesIcon(ExcelChartStandardSerie s, int index, BoundingBox parentItem) { const float MarginExtra = 1.5f; - const float LineLength = 21; + const float DefaultStrokeWidth = 0.75f; + const float LineLength = 21.0f; var item = new LineRenderItem(parentItem); item.SetDrawingPropertiesFill(Theme, s.Fill, Chart.StyleManager.Style.SeriesLine.FillReference.Color, UserSpaceSettings.ObjectBoundingBox); - item.SetDrawingPropertiesBorder(Theme, s.Border, Chart.StyleManager.Style.SeriesLine.BorderReference.Color, s.Border.Fill.Style != eFillStyle.NoFill, null, 0.75, UserSpaceSettings.ObjectBoundingBox); + item.SetDrawingPropertiesBorder(Theme, s.Border, Chart.StyleManager.Style.SeriesLine.BorderReference.Color, s.Border.Fill.Style != eFillStyle.NoFill, null, DefaultStrokeWidth, UserSpaceSettings.ObjectBoundingBox); float y = (float)parentItem.Top + MarginExtra; float x = 0; item.X1 = x; item.Y1 = y; - item.X2 = x + LineLength; + item.X2 = x + (LineLength - (float)item.BorderWidth); item.Y2 = y; item.LineCap = LineCap.Round; diff --git a/src/EPPlus/Drawing/Renderer/RenderItems/DrawingRenderItemExtentions.cs b/src/EPPlus/Drawing/Renderer/RenderItems/DrawingRenderItemExtentions.cs index c63e2113fe..85757c8ce7 100644 --- a/src/EPPlus/Drawing/Renderer/RenderItems/DrawingRenderItemExtentions.cs +++ b/src/EPPlus/Drawing/Renderer/RenderItems/DrawingRenderItemExtentions.cs @@ -265,7 +265,7 @@ internal static void SetDrawingPropertiesBorder(this RenderItem item, ExcelTheme if (item.BorderColorSource != PathFillMode.None) { - item.BorderWidth = (border?.Width ?? 0D) == 0D ? 0.75d : border.Width; + 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); From 7efb656116e83f0d980672d3b82f721d728ef18e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ossian=20Edstr=C3=B6m?= Date: Tue, 1 Sep 2026 13:11:40 +0200 Subject: [PATCH 54/73] Applied Datalabel fallback fill --- .../DataLabels/ChartSerieDataLabelRenderer.cs | 4 ++-- .../Chart/DataLabels/SvgDataLabelPoint.cs | 15 +++------------ 2 files changed, 5 insertions(+), 14 deletions(-) diff --git a/src/EPPlus/Drawing/Renderer/Chart/DataLabels/ChartSerieDataLabelRenderer.cs b/src/EPPlus/Drawing/Renderer/Chart/DataLabels/ChartSerieDataLabelRenderer.cs index 6bc1f99959..084143f7dd 100644 --- a/src/EPPlus/Drawing/Renderer/Chart/DataLabels/ChartSerieDataLabelRenderer.cs +++ b/src/EPPlus/Drawing/Renderer/Chart/DataLabels/ChartSerieDataLabelRenderer.cs @@ -37,7 +37,7 @@ public ChartSerieDataLabelRenderer(ChartRenderer chart, ExcelChartSerieDataLabel _dlblSerie = dlblSerie; plotAreaBounds = chart.Plotarea.Group.Bounds; - DefaultFillColor = Color.Transparent; + DefaultFillColor = dlblSerie.Fill != null && dlblSerie.Fill.Color.IsEmpty == false ? dlblSerie.Fill.Color : Color.Transparent; if(yValues != null && yValues.Count != 0) @@ -126,7 +126,7 @@ private RenderItem GetSeriesIcon(ExcelChartStandardSerie serie, BoundingBox maxB private void AddDatalabel(ExcelChartStandardSerie serie, ExcelChartDataLabelStandard dataLabel, object xValue, object yValue, BoundingBox maxBounds) { - var newDataLabel = new SvgDataLabelPoint(ChartRenderer, dataLabel); + var newDataLabel = new SvgDataLabelPoint(ChartRenderer, dataLabel, DefaultFillColor); newDataLabel.ImportDataLabel(serie, dataLabel, xValue, yValue, defaultParagraph, maxBounds, _defaultMargins, SummedSeries); if(dataLabel.ShowLegendKey) diff --git a/src/EPPlus/Drawing/Renderer/Chart/DataLabels/SvgDataLabelPoint.cs b/src/EPPlus/Drawing/Renderer/Chart/DataLabels/SvgDataLabelPoint.cs index 34c1765723..d304ddc72a 100644 --- a/src/EPPlus/Drawing/Renderer/Chart/DataLabels/SvgDataLabelPoint.cs +++ b/src/EPPlus/Drawing/Renderer/Chart/DataLabels/SvgDataLabelPoint.cs @@ -48,9 +48,9 @@ internal class SvgDataLabelPoint : ChartDrawingObject // TxtBox = txtBox; //} - public SvgDataLabelPoint(ChartRenderer chart, ExcelChartDataLabelStandard standard) : base(chart) + public SvgDataLabelPoint(ChartRenderer chart, ExcelChartDataLabelStandard standard, Color? defaultFillColor = null) : base(chart) { - DefaultFillColor = Color.Transparent; + DefaultFillColor = defaultFillColor.HasValue ? defaultFillColor : Color.Transparent; _labelPosition = GetDefaultPositionBasedOnChartType(standard); Rectangle = new RectRenderItem(chart.Bounds); } @@ -212,16 +212,7 @@ internal void ImportDataLabel(ExcelChartStandardSerie serie, ExcelChartDataLabel Rectangle.Bounds.Height = txtBox.Rectangle.Bounds.Height; _txtBox = txtBox; - - if (dataLabel.Fill.IsEmpty == false) - { - _txtBox.Rectangle.SetDrawingPropertiesFill(ChartRenderer.Theme, dataLabel.Fill, null, UserSpaceSettings.ObjectBoundingBox, DefaultFillColor); - } - else - { - _txtBox.Rectangle.SetDrawingPropertiesFill(ChartRenderer.Theme, dataLabel.Fill, null, UserSpaceSettings.ObjectBoundingBox, DefaultFillColor); - //_txtBox.Rectangle.FillColor = "transparent"; - } + _txtBox.Rectangle.SetDrawingPropertiesFill(ChartRenderer.Theme, dataLabel.Fill, null, UserSpaceSettings.ObjectBoundingBox, DefaultFillColor); if (dataLabel.Font.IsEmpty == false) { From 2327b9743a7a3b3490928237800d9a183f317814 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jan=20K=C3=A4llman?= Date: Tue, 1 Sep 2026 15:25:21 +0200 Subject: [PATCH 55/73] Fixed minor tickmarks on date axis --- .../Renderer/Chart/ChartAxisRenderer.cs | 25 ++++++++++++++++++- 1 file changed, 24 insertions(+), 1 deletion(-) diff --git a/src/EPPlus/Drawing/Renderer/Chart/ChartAxisRenderer.cs b/src/EPPlus/Drawing/Renderer/Chart/ChartAxisRenderer.cs index edf1fa9873..07d94d6689 100644 --- a/src/EPPlus/Drawing/Renderer/Chart/ChartAxisRenderer.cs +++ b/src/EPPlus/Drawing/Renderer/Chart/ChartAxisRenderer.cs @@ -27,6 +27,7 @@ Date Author Change using OfficeOpenXml.Drawing.Chart.Style; using OfficeOpenXml.Drawing.Renderer.Chart.Defaults; using OfficeOpenXml.Drawing.Renderer.TextBox; +using OfficeOpenXml.FormulaParsing.Excel.Functions; using OfficeOpenXml.FormulaParsing.Excel.Functions.DateAndTime; using OfficeOpenXml.FormulaParsing.Excel.Functions.Logical; using OfficeOpenXml.FormulaParsing.Excel.Functions.MathFunctions; @@ -747,7 +748,9 @@ private List AddTickmarks(double units, eTimeUnit? dateUnit, dou while (d <= maxPos) { var addPosition = (d - min); - if (double.IsNaN(parentUnit) || (addPosition % parentUnit != 0)) + if (double.IsNaN(parentUnit) || + (dateUnit.HasValue==false && addPosition % parentUnit != 0) || + (dateUnit.HasValue==true && IsMinorDateUnit(dateUnit.Value, parentUnit, d))) { double x1, y1, x2, y2; switch (Axis.ActualAxisPosition) @@ -820,6 +823,26 @@ private List AddTickmarks(double units, eTimeUnit? dateUnit, dou } return tms; } + + private bool IsMinorDateUnit(eTimeUnit dateUnit, double parentUnit, double d) + { + switch(dateUnit) + { + case eTimeUnit.Days: + return d % parentUnit != 0; + case eTimeUnit.Months: + var minDt = DateTime.FromOADate(Min); + var dt = DateTime.FromOADate(d); + return minDt.Month % parentUnit != dt.Month % parentUnit; + case eTimeUnit.Years: + minDt = DateTime.FromOADate(Min); + dt = DateTime.FromOADate(d); + return minDt.Year % parentUnit != dt.Year % parentUnit; + default: + throw new InvalidOperationException("Invalid date unit"); + } + } + private List AddGridlines(double units, double parentUnit, ExcelDrawingBorder lineItem, ExcelChartStyleEntry styleEntry) { var axisStyle = GetAxisStyleEntry(); From dbec5d20dca39f9a72039bef5b30382251a1eaaa Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ossian=20Edstr=C3=B6m?= Date: Tue, 1 Sep 2026 15:43:23 +0200 Subject: [PATCH 56/73] Bar chart fix using global position (for now) --- .../BarColumnChartTypeDrawer.cs | 19 ++++++---- .../Chart/DataLabels/SvgDataLabelPoint.cs | 9 +++++ .../DrawingRenderItemExtentions.cs | 36 ++----------------- 3 files changed, 24 insertions(+), 40 deletions(-) diff --git a/src/EPPlus/Drawing/Renderer/Chart/ChartTypeDrawers/BarColumnChartTypeDrawer.cs b/src/EPPlus/Drawing/Renderer/Chart/ChartTypeDrawers/BarColumnChartTypeDrawer.cs index ddd4124bd8..216e3ba46e 100644 --- a/src/EPPlus/Drawing/Renderer/Chart/ChartTypeDrawers/BarColumnChartTypeDrawer.cs +++ b/src/EPPlus/Drawing/Renderer/Chart/ChartTypeDrawers/BarColumnChartTypeDrawer.cs @@ -6,6 +6,7 @@ using EPPlusImageRenderer.RenderItems; using EPPlusImageRenderer.Svg; using OfficeOpenXml.Drawing.Chart; +using OfficeOpenXml.FormulaParsing.Excel.Functions.MathFunctions; using OfficeOpenXml.FormulaParsing.Excel.Functions.RefAndLookup; using OfficeOpenXml.Utils.TypeConversion; using System; @@ -91,6 +92,10 @@ internal override void DrawSeries() for (int j = 0; j < dataPoints.Count; j++) { + //var parentHolder = dataPoints[j].Parent; + + var tmpBounds = dataPoints[j].GetGlobalBoundingbox(); + //Initialize transforms Transform basePoint = new Transform(); Transform endPoint = new Transform(); @@ -99,15 +104,15 @@ internal override void DrawSeries() if (isColumn == true) { - var middleRight = dataPoints[j].Left + (dataPoints[j].Width / 2); + var middleRight = tmpBounds.Left + (tmpBounds.Width / 2); - if (chartBaseY <= dataPoints[j].Top) + if (chartBaseY <= tmpBounds.Top) { //We are a negative column // ----- Base-Axis // |_| Col - basePoint.Position = new Vector2(middleRight, dataPoints[j].Top); - endPoint.Position = new Vector2(middleRight, dataPoints[j].Bottom); + basePoint.Position = new Vector2(middleRight, tmpBounds.Top); + endPoint.Position = new Vector2(middleRight, tmpBounds.Bottom); } else { @@ -115,8 +120,8 @@ internal override void DrawSeries() // _ // | | Col // ----- Base-Axis - basePoint.Position = new Vector2(middleRight, dataPoints[j].Bottom); - endPoint.Position = new Vector2(middleRight, dataPoints[j].Top); + basePoint.Position = new Vector2(middleRight, tmpBounds.Bottom); + endPoint.Position = new Vector2(middleRight, tmpBounds.Top); } datalabel.SetDimensions(j, basePoint, endPoint); @@ -136,8 +141,8 @@ internal override void DrawSeries() datalabel.SetDimensions(j, basePoint, endPoint); } + //dataPoints[j].Parent = parentHolder; } - serCounter++; } } diff --git a/src/EPPlus/Drawing/Renderer/Chart/DataLabels/SvgDataLabelPoint.cs b/src/EPPlus/Drawing/Renderer/Chart/DataLabels/SvgDataLabelPoint.cs index d304ddc72a..6aebc4278c 100644 --- a/src/EPPlus/Drawing/Renderer/Chart/DataLabels/SvgDataLabelPoint.cs +++ b/src/EPPlus/Drawing/Renderer/Chart/DataLabels/SvgDataLabelPoint.cs @@ -9,6 +9,7 @@ using OfficeOpenXml.Drawing; using OfficeOpenXml.Drawing.Chart; using OfficeOpenXml.Drawing.Renderer.TextBox; +using OfficeOpenXml.FormulaParsing.Utilities; using OfficeOpenXml.Utils.EnumUtils; using OfficeOpenXml.Utils.TypeConversion; using System; @@ -130,12 +131,20 @@ internal void ImportDataLabel(ExcelChartStandardSerie serie, ExcelChartDataLabel } if (dataLabel.ShowCategory) { + if (xValue.IsNumeric()) + { + xValue = Math.Round((double)xValue, 6); + } dlblStrings.Add(xValue.ToString()); } if (dataLabel.ShowValue) { if (yValue != null) { + if(yValue.IsNumeric()) + { + yValue = Math.Round((double)yValue,6); + } dlblStrings.Add(yValue.ToString()); } } diff --git a/src/EPPlus/Drawing/Renderer/RenderItems/DrawingRenderItemExtentions.cs b/src/EPPlus/Drawing/Renderer/RenderItems/DrawingRenderItemExtentions.cs index 85757c8ce7..bab68463a0 100644 --- a/src/EPPlus/Drawing/Renderer/RenderItems/DrawingRenderItemExtentions.cs +++ b/src/EPPlus/Drawing/Renderer/RenderItems/DrawingRenderItemExtentions.cs @@ -57,9 +57,6 @@ 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, gradientUserSpaceOnUse); if(gradFill != null) @@ -78,33 +75,6 @@ internal static void SetDrawingPropertiesFillBasic(this RenderItem item, ExcelTh { 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) @@ -157,14 +127,14 @@ private static string GetFallbackFill(ExcelTheme theme, ExcelDrawingFillBasic it if(fc.HasValue && fc.Value.ToArgb() == Color.Transparent.ToArgb()) { - opacity = 0d; + opacity = 1d; return "none"; } } } else { - opacity = 0d; + opacity = 1d; //The node has specifically been set to NoFill AKA Transparent return "none"; } @@ -230,7 +200,7 @@ internal static string GetFillNew(ExcelDrawingFillBasic fill, ExcelTheme theme, } return fillStr; } - // 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 + internal static void SetDrawingPropertiesBorder(this RenderItem item, ExcelTheme theme, ExcelDrawingBorder border, ExcelChartStyleColorManager reference, bool hasBorder, Func GetStyleDefaultColor, double defaultWidth = 1.5d, UserSpaceSettings gradientUserSpaceOnUse = UserSpaceSettings.UserSpaceOnUse_Global) { string fillColorStr = null; From 45f3de9588b112318348cc3cdbead505aeb8cc05 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ossian=20Edstr=C3=B6m?= Date: Tue, 1 Sep 2026 16:36:26 +0200 Subject: [PATCH 57/73] Fixed border-width failing unit tests --- src/EPPlus/Drawing/Renderer/ChartRenderer.cs | 2 +- src/EPPlus/Drawing/Renderer/ShapeRenderer.cs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/EPPlus/Drawing/Renderer/ChartRenderer.cs b/src/EPPlus/Drawing/Renderer/ChartRenderer.cs index cb03fa7abd..bbd778db34 100644 --- a/src/EPPlus/Drawing/Renderer/ChartRenderer.cs +++ b/src/EPPlus/Drawing/Renderer/ChartRenderer.cs @@ -386,7 +386,7 @@ private void SetChartArea(SvgRenderOptions options) Chart.Border, reference?.Color, Chart.Border.Fill.Style != eFillStyle.NoFill, - () => item.GetDefaultBorderColor()); + () => item.GetDefaultBorderColor(), 0.75d); item.Rectangle.RoundedCornerRadius = Chart.RoundedCorners ? 9 : 0; item.AppendRenderItems(RenderItems); diff --git a/src/EPPlus/Drawing/Renderer/ShapeRenderer.cs b/src/EPPlus/Drawing/Renderer/ShapeRenderer.cs index 1be7e35176..706b2db5e3 100644 --- a/src/EPPlus/Drawing/Renderer/ShapeRenderer.cs +++ b/src/EPPlus/Drawing/Renderer/ShapeRenderer.cs @@ -194,7 +194,7 @@ protected RenderItem AddFromPaths(BoundingBox parent, DrawingPath path, bool dra if (drawBorder) { pi.BorderColorSource = path.Stroke ? PathFillMode.Norm : PathFillMode.None; - pi.SetDrawingPropertiesBorder(Theme, shape.Border, shape.ThemeStyles.BorderReference.Color, path.Stroke, ()=> Theme.ObjectDefaults.ShapeDefinition.Style.BorderReference.ShapeColor.GetColor()); + pi.SetDrawingPropertiesBorder(Theme, shape.Border, shape.ThemeStyles.BorderReference.Color, path.Stroke, ()=> Theme.ObjectDefaults.ShapeDefinition.Style.BorderReference.ShapeColor.GetColor(), 0.75d); } else { From 91b6fff475898c6f3f70d33ad13b569f43be2e72 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ossian=20Edstr=C3=B6m?= Date: Tue, 1 Sep 2026 16:52:28 +0200 Subject: [PATCH 58/73] Same fix for bars as for columns --- .../Chart/BarChartTests.cs | 2 +- .../BarColumnChartTypeDrawer.cs | 23 +++++++++---------- 2 files changed, 12 insertions(+), 13 deletions(-) diff --git a/src/EPPlus.DrawingRenderer.Tests/Chart/BarChartTests.cs b/src/EPPlus.DrawingRenderer.Tests/Chart/BarChartTests.cs index 364ea2ef7e..f992657e70 100644 --- a/src/EPPlus.DrawingRenderer.Tests/Chart/BarChartTests.cs +++ b/src/EPPlus.DrawingRenderer.Tests/Chart/BarChartTests.cs @@ -51,7 +51,7 @@ public void DatalabelBarCharts() { var ws = p.Workbook.Worksheets[0]; var drawings = ws.Drawings; - var ix = 1; + var ix = 0; for (int i = ix; i < drawings.Count; i++) { diff --git a/src/EPPlus/Drawing/Renderer/Chart/ChartTypeDrawers/BarColumnChartTypeDrawer.cs b/src/EPPlus/Drawing/Renderer/Chart/ChartTypeDrawers/BarColumnChartTypeDrawer.cs index 216e3ba46e..b4596b82e0 100644 --- a/src/EPPlus/Drawing/Renderer/Chart/ChartTypeDrawers/BarColumnChartTypeDrawer.cs +++ b/src/EPPlus/Drawing/Renderer/Chart/ChartTypeDrawers/BarColumnChartTypeDrawer.cs @@ -94,7 +94,7 @@ internal override void DrawSeries() { //var parentHolder = dataPoints[j].Parent; - var tmpBounds = dataPoints[j].GetGlobalBoundingbox(); + var globalDPBounds = dataPoints[j].GetGlobalBoundingbox(); //Initialize transforms Transform basePoint = new Transform(); @@ -104,15 +104,15 @@ internal override void DrawSeries() if (isColumn == true) { - var middleRight = tmpBounds.Left + (tmpBounds.Width / 2); + var middleRight = globalDPBounds.Left + (globalDPBounds.Width / 2); - if (chartBaseY <= tmpBounds.Top) + if (chartBaseY <= globalDPBounds.Top) { //We are a negative column // ----- Base-Axis // |_| Col - basePoint.Position = new Vector2(middleRight, tmpBounds.Top); - endPoint.Position = new Vector2(middleRight, tmpBounds.Bottom); + basePoint.Position = new Vector2(middleRight, globalDPBounds.Top); + endPoint.Position = new Vector2(middleRight, globalDPBounds.Bottom); } else { @@ -120,28 +120,27 @@ internal override void DrawSeries() // _ // | | Col // ----- Base-Axis - basePoint.Position = new Vector2(middleRight, tmpBounds.Bottom); - endPoint.Position = new Vector2(middleRight, tmpBounds.Top); + basePoint.Position = new Vector2(middleRight, globalDPBounds.Bottom); + endPoint.Position = new Vector2(middleRight, globalDPBounds.Top); } datalabel.SetDimensions(j, basePoint, endPoint); } else { - var middleHeight = dataPoints[j].Top + (dataPoints[j].Height / 2); + var middleHeight = globalDPBounds.Top + (globalDPBounds.Height / 2); basePoint.Position = new Vector2(chartBaseY, middleHeight); - if (chartBaseY > dataPoints[j].Left) + if (chartBaseY > globalDPBounds.Left) { - endPoint.Position = new Vector2(chartBaseY - dataPoints[j].Width, middleHeight); + endPoint.Position = new Vector2(chartBaseY - globalDPBounds.Width, middleHeight); } else { - endPoint.Position = new Vector2(dataPoints[j].Left + dataPoints[j].Width, middleHeight); + endPoint.Position = new Vector2(globalDPBounds.Left + globalDPBounds.Width, middleHeight); } datalabel.SetDimensions(j, basePoint, endPoint); } - //dataPoints[j].Parent = parentHolder; } serCounter++; } From d002d8a12fc40d98855d765d681c8de806679f37 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jan=20K=C3=A4llman?= Date: Wed, 2 Sep 2026 07:50:55 +0200 Subject: [PATCH 59/73] Fixed test --- src/EPPlus.DrawingRenderer.Tests/Chart/LineChartToSvgTests.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/EPPlus.DrawingRenderer.Tests/Chart/LineChartToSvgTests.cs b/src/EPPlus.DrawingRenderer.Tests/Chart/LineChartToSvgTests.cs index f1cb598734..a877ffe852 100644 --- a/src/EPPlus.DrawingRenderer.Tests/Chart/LineChartToSvgTests.cs +++ b/src/EPPlus.DrawingRenderer.Tests/Chart/LineChartToSvgTests.cs @@ -349,7 +349,7 @@ public void GenerateBlazorSample1() } } [TestMethod] - public void HtmlExportWithLineChart() + public async Task HtmlExportWithLineChart() { using (var package = new ExcelPackage()) { @@ -403,7 +403,7 @@ public void HtmlExportWithLineChart() // export css and html //var css = exporter.GetCssString(); //var html = exporter.GetHtmlString(); - var html = exporter.GetSinglePage(); + var html = await exporter.GetSinglePageAsync(); SaveSvg("HtmlExportWithLineChart.html", html); } From 64827b8609ca30bb4023441457d12974c601fbac Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ossian=20Edstr=C3=B6m?= Date: Wed, 2 Sep 2026 09:43:00 +0200 Subject: [PATCH 60/73] Fixed failing test --- src/EPPlus/Drawing/Renderer/Chart/ChartLegendRenderer.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/EPPlus/Drawing/Renderer/Chart/ChartLegendRenderer.cs b/src/EPPlus/Drawing/Renderer/Chart/ChartLegendRenderer.cs index 0fa36b5f58..fe8ff071ed 100644 --- a/src/EPPlus/Drawing/Renderer/Chart/ChartLegendRenderer.cs +++ b/src/EPPlus/Drawing/Renderer/Chart/ChartLegendRenderer.cs @@ -924,7 +924,7 @@ private RectRenderItem GetPieSeriesIcon(ExcelChart ct, ExcelPieChartSerie pcS, D item.Height = iconHeight; item.SetDrawingPropertiesFill(ChartRenderer.Theme, pcS.Fill, Chart.StyleManager.Style?.SeriesLine.FillReference.Color); - item.SetDrawingPropertiesBorder(ChartRenderer.Theme, pcS.Border, Chart.StyleManager.Style?.SeriesLine.BorderReference.Color, pcS.Border.Fill.Style != eFillStyle.NoFill, null, 0.75); + item.SetDrawingPropertiesBorder(ChartRenderer.Theme, pcS.Border, Chart.StyleManager.Style?.SeriesLine.BorderReference.Color, pcS.Border.Fill.Style != eFillStyle.NoFill, () => DefaultBorderColor, 0.75); return item; } From c7b8598abdbb59616c35f2396e054a60adf92374 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ossian=20Edstr=C3=B6m?= Date: Wed, 2 Sep 2026 11:28:35 +0200 Subject: [PATCH 61/73] Fixed style defaults and verified with test adjustment --- .../Chart/ChartStyleFallbackTest.cs | 16 ++++++++++++---- .../Renderer/Chart/ChartPlotareaRenderer.cs | 2 +- src/EPPlus/Drawing/Renderer/ShapeRenderer.cs | 6 ++++-- .../Drawing/Shape/Style/ShapeStyleReference.cs | 2 +- .../Style/Coloring/ExcelDrawingColorManager.cs | 1 + .../Coloring/ExcelDrawingThemeColorManager.cs | 2 +- .../Drawing/Theme/ExcelThemeObjectDefaults.cs | 11 ++++++++++- src/EPPlusTest/Drawing/Chart/DataPointsTest.cs | 3 +-- 8 files changed, 31 insertions(+), 12 deletions(-) diff --git a/src/EPPlus.DrawingRenderer.Tests/Chart/ChartStyleFallbackTest.cs b/src/EPPlus.DrawingRenderer.Tests/Chart/ChartStyleFallbackTest.cs index c83e9410ee..89d91fc863 100644 --- a/src/EPPlus.DrawingRenderer.Tests/Chart/ChartStyleFallbackTest.cs +++ b/src/EPPlus.DrawingRenderer.Tests/Chart/ChartStyleFallbackTest.cs @@ -363,11 +363,15 @@ public void Epp_Gen_DefaultLine() var expectedStroke = ColorTranslator.ToHtml(col).ToLower(); var svgSplitOnSpace = svg.Split(' '); + var fills = svgSplitOnSpace.Where(s => s.StartsWith("fill")).ToArray(); - //Get the first stroke and extract the hexCode for the expected color - var firstFill = svgSplitOnSpace.First(s => s.StartsWith("fill")); + //Get the first fill and extract the hexCode for the expected color + var firstFill = fills[0]; var fillResult = firstFill.Substring(6, 7).ToLower(); + var secondFill = fills[1]; + var secondResult = secondFill.Substring(6, 7).ToLower(); + //Get the first stroke and extract the hexCode for the expected color var firstStroke = svgSplitOnSpace.First(s => s.StartsWith("stroke")); var strokeResult = firstStroke.Substring(8, 7).ToLower(); @@ -381,6 +385,8 @@ public void Epp_Gen_DefaultLine() Assert.AreEqual(expectedFill, fillResult); Assert.AreEqual(expectedStroke, strokeResult); Assert.AreEqual(1d, widthResult, 0.003); + //Plot area should also be white as in Excel + Assert.AreEqual(expectedFill, secondResult); } SaveAndCleanup(p); @@ -415,8 +421,10 @@ public void Epp_Gen_DefaultShape() var svgSplitOnSpace = svgDefault.Split(' '); + var fills = svgSplitOnSpace.Where(s => s.StartsWith("fill")).ToArray(); + //Get the first fill and extract the hexCode for the expected color - var firstFill = svgSplitOnSpace.First(s => s.StartsWith("fill")); + var firstFill = fills[0]; var fillResult = firstFill.Substring(6, 7).ToLower(); //Get the first stroke and extract the hexCode for the expected color @@ -467,7 +475,7 @@ public void EpplusGeneratedChart() defaultRect.SetPosition(300, 1); gradientRect.SetPosition(300, 1000); - defaultRect.Fill.Style = OfficeOpenXml.Drawing.eFillStyle.SolidFill; + //defaultRect.Fill.Style = OfficeOpenXml.Drawing.eFillStyle.SolidFill; gradientRect.Fill.Style = OfficeOpenXml.Drawing.eFillStyle.GradientFill; generatedBar.Series.Add(ws.Cells["A1:A3"]); diff --git a/src/EPPlus/Drawing/Renderer/Chart/ChartPlotareaRenderer.cs b/src/EPPlus/Drawing/Renderer/Chart/ChartPlotareaRenderer.cs index d92d31bc76..1ffc3955ea 100644 --- a/src/EPPlus/Drawing/Renderer/Chart/ChartPlotareaRenderer.cs +++ b/src/EPPlus/Drawing/Renderer/Chart/ChartPlotareaRenderer.cs @@ -263,7 +263,7 @@ internal void DrawSeries() internal override Color? GetDefaultBorderColor() { - return GetDefaultBorderColorForElement(ChartElement.PlotArea2d, (int)Chart.Style); + return null; } internal override Color? DefaultFillColor { get => GetDefaultFillColor(); } diff --git a/src/EPPlus/Drawing/Renderer/ShapeRenderer.cs b/src/EPPlus/Drawing/Renderer/ShapeRenderer.cs index 706b2db5e3..7016f88513 100644 --- a/src/EPPlus/Drawing/Renderer/ShapeRenderer.cs +++ b/src/EPPlus/Drawing/Renderer/ShapeRenderer.cs @@ -22,6 +22,7 @@ Date Author Change using EPPlusImageRenderer.RenderItems; using OfficeOpenXml; using OfficeOpenXml.Drawing.Renderer.TextBox; +using OfficeOpenXml.Utils.TypeConversion; using OfficeOpenXml.Utils.Drawing; using System; using System.Collections.Generic; @@ -180,10 +181,11 @@ protected RenderItem AddFromPaths(BoundingBox parent, DrawingPath path, bool dra pi.Commands[pi.Commands.Count - 1].Coordinates = coordinates.ToArray(); } var shape = (ExcelShape)Drawing; + var shapeDefaultStyle = Theme.ObjectDefaults.ShapeDefinition.Style; if (drawFill) { pi.FillColorSource = path.Fill; - pi.SetDrawingPropertiesFill(Theme, shape.Fill, shape.ThemeStyles.FillReference.Color, UserSpaceSettings.ObjectBoundingBox, Theme.ObjectDefaults.ShapeDefinition.Style.FillReference.ShapeColor.GetColor()); + pi.SetDrawingPropertiesFill(Theme, shape.Fill, shape.ThemeStyles.FillReference.Color, UserSpaceSettings.ObjectBoundingBox, ColorConverter.GetThemeColor(Theme, shapeDefaultStyle.FillReference.ShapeColor)); } else { @@ -194,7 +196,7 @@ protected RenderItem AddFromPaths(BoundingBox parent, DrawingPath path, bool dra if (drawBorder) { pi.BorderColorSource = path.Stroke ? PathFillMode.Norm : PathFillMode.None; - pi.SetDrawingPropertiesBorder(Theme, shape.Border, shape.ThemeStyles.BorderReference.Color, path.Stroke, ()=> Theme.ObjectDefaults.ShapeDefinition.Style.BorderReference.ShapeColor.GetColor(), 0.75d); + pi.SetDrawingPropertiesBorder(Theme, shape.Border, shape.ThemeStyles.BorderReference.Color, path.Stroke, ()=> ColorConverter.GetThemeColor(Theme, shapeDefaultStyle.BorderReference.ShapeColor), 0.75d); } else { diff --git a/src/EPPlus/Drawing/Shape/Style/ShapeStyleReference.cs b/src/EPPlus/Drawing/Shape/Style/ShapeStyleReference.cs index cbd03366da..a4497ca4de 100644 --- a/src/EPPlus/Drawing/Shape/Style/ShapeStyleReference.cs +++ b/src/EPPlus/Drawing/Shape/Style/ShapeStyleReference.cs @@ -2,6 +2,7 @@ using OfficeOpenXml.Drawing.Style.Coloring; using System; using System.Collections.Generic; +using System.Drawing; using System.Globalization; using System.Linq; using System.Text; @@ -52,7 +53,6 @@ public ExcelDrawingColorManager ShapeColor return _color; } } - /// /// If the reference has a color /// diff --git a/src/EPPlus/Drawing/Style/Coloring/ExcelDrawingColorManager.cs b/src/EPPlus/Drawing/Style/Coloring/ExcelDrawingColorManager.cs index 23ba26ebc9..ec581de8e9 100644 --- a/src/EPPlus/Drawing/Style/Coloring/ExcelDrawingColorManager.cs +++ b/src/EPPlus/Drawing/Style/Coloring/ExcelDrawingColorManager.cs @@ -14,6 +14,7 @@ Date Author Change using System; using System.Linq; using System.Collections.Generic; +using System.Drawing; namespace OfficeOpenXml.Drawing.Style.Coloring { diff --git a/src/EPPlus/Drawing/Style/Coloring/ExcelDrawingThemeColorManager.cs b/src/EPPlus/Drawing/Style/Coloring/ExcelDrawingThemeColorManager.cs index 84ab4ef526..3e95304b16 100644 --- a/src/EPPlus/Drawing/Style/Coloring/ExcelDrawingThemeColorManager.cs +++ b/src/EPPlus/Drawing/Style/Coloring/ExcelDrawingThemeColorManager.cs @@ -292,7 +292,7 @@ private XmlNode GetPathNode() } return _pathNode; } - internal Color GetColor() + internal virtual Color GetColor() { return OfficeOpenXml.Utils.TypeConversion.ColorConverter.GetThemeColor(this); } diff --git a/src/EPPlus/Drawing/Theme/ExcelThemeObjectDefaults.cs b/src/EPPlus/Drawing/Theme/ExcelThemeObjectDefaults.cs index 77768fc3d7..cbf8f66f68 100644 --- a/src/EPPlus/Drawing/Theme/ExcelThemeObjectDefaults.cs +++ b/src/EPPlus/Drawing/Theme/ExcelThemeObjectDefaults.cs @@ -10,6 +10,8 @@ internal class ExcelThemeObjectDefaults : XmlHelper private readonly string _spDefPath = "a:spDef"; private readonly string _lnDefPath = "a:lnDef"; private readonly string _txDefPath = "a:txDef"; + private const string defaultSpDefInnerXml = ""; + public ExcelThemeObjectDefaults(XmlNamespaceManager nameSpaceManager, XmlNode topNode, ExcelThemeBase theme) : base(nameSpaceManager, topNode) { _theme = theme; @@ -25,7 +27,14 @@ public DefaultShapeDefinition ShapeDefinition { if (_spDef == null) { - var test = TopNode.SelectSingleNode(_spDefPath, NameSpaceManager); + var spDefNode = TopNode.SelectSingleNode(_spDefPath, NameSpaceManager); + //Despite there being no SpDef node/no child nodes Excel Acts as if the @defaultSpDefXml is there. + //Therefore if the node is not there or if it is empty create the default + if (spDefNode == null || spDefNode.HasChildNodes == false) + { + spDefNode = CreateNode(_spDefPath); + spDefNode.InnerXml = defaultSpDefInnerXml; + } _spDef = new DefaultShapeDefinition(NameSpaceManager, TopNode, _spDefPath, _theme); } diff --git a/src/EPPlusTest/Drawing/Chart/DataPointsTest.cs b/src/EPPlusTest/Drawing/Chart/DataPointsTest.cs index 6964621938..f265be254d 100644 --- a/src/EPPlusTest/Drawing/Chart/DataPointsTest.cs +++ b/src/EPPlusTest/Drawing/Chart/DataPointsTest.cs @@ -87,9 +87,8 @@ public void PieChart() var svg = chart.ToSvg(); + File.WriteAllText($"{_worksheetPath}svg\\EPPlusPieChart1.svg", svg); //SaveAndCleanup(_pck); - - //File.WriteAllText($"{_worksheetPath}svg\\EPPlusPieChart1.svg", svg); } [TestMethod] public void BarChart() From b1e105672444c2c61a366631022455617ff0385e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jan=20K=C3=A4llman?= Date: Wed, 2 Sep 2026 13:33:01 +0200 Subject: [PATCH 62/73] Fixed issues from failing tests --- .../Chart/LineChartToSvgTests.cs | 2 +- .../Integration/DataHolders/TextLineSimple.cs | 2 +- .../Integration/RichText/LayoutSystem.cs | 2 +- src/EPPlus/Drawing/Chart/ExcelBarChart.cs | 5 ----- src/EPPlus/Drawing/Chart/ExcelChart.cs | 21 +++++++++++++++++++ src/EPPlus/Drawing/Chart/ExcelLineChart.cs | 5 ----- src/EPPlus/Drawing/Chart/ExcelPieChart.cs | 5 ----- .../Internal/AbstractRangeExporter.cs | 13 ++++++++++-- .../Exporters/Internal/CssExporterBase.cs | 16 +++++++------- .../Internal/HtmlExporterBaseInternal.cs | 9 +++++++- .../Internal/HtmlRangeExporterBase.cs | 3 +-- .../Settings/HtmlDrawingSettings.cs | 2 +- .../Settings/HtmlPictureSettings.cs | 4 ++-- .../Drawing/Chart/DataPointsTest.cs | 2 +- .../Export/HtmlExport/TableExporterTests.cs | 4 ++-- 15 files changed, 58 insertions(+), 37 deletions(-) diff --git a/src/EPPlus.DrawingRenderer.Tests/Chart/LineChartToSvgTests.cs b/src/EPPlus.DrawingRenderer.Tests/Chart/LineChartToSvgTests.cs index a877ffe852..02d264ddae 100644 --- a/src/EPPlus.DrawingRenderer.Tests/Chart/LineChartToSvgTests.cs +++ b/src/EPPlus.DrawingRenderer.Tests/Chart/LineChartToSvgTests.cs @@ -386,7 +386,7 @@ public async Task HtmlExportWithLineChart() chart.SetPosition(0, 0); chart.To.Row = 14; chart.To.Column = 10; - chart.StyleManager.SetChartStyle(ePresetChartStyle.LineChartStyle7); + chart.StyleManager.SetChartStyle(ePresetChartStyle.LineChartStyle5); var exporter = sheet.Cells.CreateHtmlExporter(); var settings = exporter.Settings; diff --git a/src/EPPlus.Fonts.OpenType/Integration/DataHolders/TextLineSimple.cs b/src/EPPlus.Fonts.OpenType/Integration/DataHolders/TextLineSimple.cs index cb578c0e66..214963a2a0 100644 --- a/src/EPPlus.Fonts.OpenType/Integration/DataHolders/TextLineSimple.cs +++ b/src/EPPlus.Fonts.OpenType/Integration/DataHolders/TextLineSimple.cs @@ -57,7 +57,7 @@ public class TextLineSimple public double GetWidthWithoutTrailingSpaces() { var trailingSpaceCount = 0; - + if (string.IsNullOrEmpty(Text)) return 0D; for (int i = Text.Count() - 1; i > 0; i--) { if (Text[i] != ' ') diff --git a/src/EPPlus.Fonts.OpenType/Integration/RichText/LayoutSystem.cs b/src/EPPlus.Fonts.OpenType/Integration/RichText/LayoutSystem.cs index ce14476ae4..df5e09b552 100644 --- a/src/EPPlus.Fonts.OpenType/Integration/RichText/LayoutSystem.cs +++ b/src/EPPlus.Fonts.OpenType/Integration/RichText/LayoutSystem.cs @@ -261,7 +261,7 @@ public TextLineCollection Wrap(double maxWidth) for (int i = 1; i < wrappedLines.Count-1; i++) { var startIdx = wrappedLines[i].InternalLineFragments[0].StartOriginal; - var len = wrappedLines[i].Text.Length; + var len = wrappedLines[i].Text?.Length??0; for(int j = startIdx; j< (startIdx + len); j++) { AllChars[j].Line = i; diff --git a/src/EPPlus/Drawing/Chart/ExcelBarChart.cs b/src/EPPlus/Drawing/Chart/ExcelBarChart.cs index e390123e94..75f8d035ab 100644 --- a/src/EPPlus/Drawing/Chart/ExcelBarChart.cs +++ b/src/EPPlus/Drawing/Chart/ExcelBarChart.cs @@ -525,10 +525,5 @@ internal override bool IsAxisTypeSupported(eAxisType type, ExcelChartAxis axis) } return base.IsAxisTypeSupported(type, axis); } - /// - /// Returns true if the drawing supports svg export via the . - /// - public override bool SupportsSvgExport => true; - } } diff --git a/src/EPPlus/Drawing/Chart/ExcelChart.cs b/src/EPPlus/Drawing/Chart/ExcelChart.cs index a0a94f4968..1990f59f39 100644 --- a/src/EPPlus/Drawing/Chart/ExcelChart.cs +++ b/src/EPPlus/Drawing/Chart/ExcelChart.cs @@ -73,6 +73,23 @@ private void Init(ExcelDrawings drawings, XmlDocument chartXml) #endregion internal ExcelChartStyleManager _styleManager = null; + internal readonly static HashSet _svgSupportedChartTypes = new HashSet() + { + eChartType.Line, + eChartType.LineMarkers, + eChartType.LineMarkersStacked, + eChartType.LineStacked, + eChartType.LineStacked100, + eChartType.LineMarkersStacked100, + eChartType.ColumnClustered, + eChartType.ColumnStacked, + eChartType.ColumnStacked100, + eChartType.BarClustered, + eChartType.BarStacked, + eChartType.BarStacked100, + eChartType.Pie, + eChartType.PieExploded, + }; /// /// Manage style settings for the chart /// @@ -1240,5 +1257,9 @@ internal override void SaveDrawing(bool hasLoadedPivotTables) cs.Drawings.DrawingXml.Save(xrd); } } + /// + /// Returns true if the chart supports svg export via the . + /// + public override bool SupportsSvgExport => _svgSupportedChartTypes.Contains(ChartType); } } diff --git a/src/EPPlus/Drawing/Chart/ExcelLineChart.cs b/src/EPPlus/Drawing/Chart/ExcelLineChart.cs index cfabca73f2..71157f5c38 100644 --- a/src/EPPlus/Drawing/Chart/ExcelLineChart.cs +++ b/src/EPPlus/Drawing/Chart/ExcelLineChart.cs @@ -382,10 +382,5 @@ internal override bool IsAxisTypeSupported(eAxisType type, ExcelChartAxis axis) } return base.IsAxisTypeSupported(type, axis); } - /// - /// Returns true if the drawing supports svg export via the . - /// - public override bool SupportsSvgExport => true; - } } diff --git a/src/EPPlus/Drawing/Chart/ExcelPieChart.cs b/src/EPPlus/Drawing/Chart/ExcelPieChart.cs index 8cc6dde817..d4e0968520 100644 --- a/src/EPPlus/Drawing/Chart/ExcelPieChart.cs +++ b/src/EPPlus/Drawing/Chart/ExcelPieChart.cs @@ -123,10 +123,5 @@ internal set /// A collection of series for a Pie Chart /// public new ExcelChartSeries Series { get; } = new ExcelChartSeries(); - /// - /// Returns true if the drawing supports svg export via the . - /// - public override bool SupportsSvgExport => true; - } } diff --git a/src/EPPlus/Export/HtmlExport/Exporters/Internal/AbstractRangeExporter.cs b/src/EPPlus/Export/HtmlExport/Exporters/Internal/AbstractRangeExporter.cs index 6d55fa85d9..283ad78e9f 100644 --- a/src/EPPlus/Export/HtmlExport/Exporters/Internal/AbstractRangeExporter.cs +++ b/src/EPPlus/Export/HtmlExport/Exporters/Internal/AbstractRangeExporter.cs @@ -57,11 +57,17 @@ internal void LoadRangeDrawings(List ranges) } _rangePictures = new List(); _rangeDrawings = new List(); + var processedDrawings = new HashSet(); //Render in-cell images. - foreach (var worksheet in ranges.Select(x => x.Worksheet).Distinct()) + foreach (var range in ranges) { + var worksheet = range.Worksheet; foreach (var d in worksheet.Drawings) { + if (processedDrawings.Contains(d)) continue; + processedDrawings.Add(d); + var drawingAddress = d.GetAddress(); + if (drawingAddress.Collide(range) == ExcelAddressBase.eAddressCollition.No) continue; if (d is ExcelPicture p) { p.GetFromBounds(out int fromRow, out int fromRowOff, out int fromCol, out int fromColOff); @@ -160,7 +166,10 @@ protected void AdjustRangeForDimensionAndDrawings(List ranges, b { for(int i=0;i public Func ExcludeDrawingHandler { get; set; } = null; /// - /// If a drawing should be included in the export or not. + /// If a drawing should be included in the export or not. Only pictures and drawings with set to true will be included. /// public eDrawingInclude Include = eDrawingInclude.Exclude; /// diff --git a/src/EPPlus/Export/HtmlExport/Settings/HtmlPictureSettings.cs b/src/EPPlus/Export/HtmlExport/Settings/HtmlPictureSettings.cs index 66cbc928f1..afc2d18d9d 100644 --- a/src/EPPlus/Export/HtmlExport/Settings/HtmlPictureSettings.cs +++ b/src/EPPlus/Export/HtmlExport/Settings/HtmlPictureSettings.cs @@ -120,11 +120,11 @@ public bool AddNameAsId { get { - return _drawingsSettings.AddMarginLeft; + return _drawingsSettings.AddNameAsId; } set { - _drawingsSettings.AddMarginLeft = value; + _drawingsSettings.AddNameAsId = value; } } /// diff --git a/src/EPPlusTest/Drawing/Chart/DataPointsTest.cs b/src/EPPlusTest/Drawing/Chart/DataPointsTest.cs index 6964621938..bb98ebc87c 100644 --- a/src/EPPlusTest/Drawing/Chart/DataPointsTest.cs +++ b/src/EPPlusTest/Drawing/Chart/DataPointsTest.cs @@ -72,7 +72,7 @@ public void LineChart() File.WriteAllText($"{_worksheetPath}svg\\EPPlusLineChart1.svg", svg); } [TestMethod] - public void PieChart() +linb public void PieChart() { var ws = _pck.Workbook.Worksheets.Add("PieChart"); LoadTestdata(ws); diff --git a/src/EPPlusTest/Export/HtmlExport/TableExporterTests.cs b/src/EPPlusTest/Export/HtmlExport/TableExporterTests.cs index bf4e961317..83f704699a 100644 --- a/src/EPPlusTest/Export/HtmlExport/TableExporterTests.cs +++ b/src/EPPlusTest/Export/HtmlExport/TableExporterTests.cs @@ -497,7 +497,7 @@ public async Task WriteMultipleRangeWithTableAndRange() exporterRange.Settings.SetRowHeight = true; exporterRange.Settings.Minify = false; exporterRange.Settings.TableStyle = eHtmlRangeTableInclude.Include; - exporterRange.Settings.Pictures.Include = ePictureInclude.Include; + exporterRange.Settings.Drawings.Include = eDrawingInclude.Include; var html1 = exporterRange.GetHtmlString(0); var html2 = exporterRange.GetHtmlString(1); @@ -509,7 +509,7 @@ public async Task WriteMultipleRangeWithTableAndRange() var outputHtml = string.Format("\r\n\r\n\r\n\r\n\r\n{0}
{1}
{2}
{3}
\r\n", html1, html2, html3, html4, css); - File.WriteAllText("${_htmlOutput}RangeAndThreeTables.html", outputHtml); + File.WriteAllText($"{_htmlOutput}RangeAndThreeTables.html", outputHtml); Assert.AreEqual(css, cssAsync); } From 67e75a944b9629633636b4b3672b2b661bf764df Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ossian=20Edstr=C3=B6m?= Date: Wed, 2 Sep 2026 17:02:57 +0200 Subject: [PATCH 63/73] Fixed datalabels for point explosions --- .../Renderer/Chart/DataLabels/SvgDataLabelPoint.cs | 8 ++++---- src/EPPlus/Drawing/Renderer/Chart/PieSliceRenderItem.cs | 6 ++---- 2 files changed, 6 insertions(+), 8 deletions(-) diff --git a/src/EPPlus/Drawing/Renderer/Chart/DataLabels/SvgDataLabelPoint.cs b/src/EPPlus/Drawing/Renderer/Chart/DataLabels/SvgDataLabelPoint.cs index 6aebc4278c..8a00e4c6ad 100644 --- a/src/EPPlus/Drawing/Renderer/Chart/DataLabels/SvgDataLabelPoint.cs +++ b/src/EPPlus/Drawing/Renderer/Chart/DataLabels/SvgDataLabelPoint.cs @@ -378,7 +378,7 @@ private void SetAdjustedTextBoxPosition(Vector2 direction, bool reverseDirection var directionOnly = direction / direction.Length; //Get txtbox-size based vector - var txtBoxAdjustVector = new Vector2(_txtBox.Width / 2d, _txtBox.Height / 2d); + var txtBoxAdjustVector = new Vector2(Rectangle.Width / 2d, Rectangle.Height / 2d ); //Apply translation to current position Rectangle.Bounds.Position += directionOnly * txtBoxAdjustVector; @@ -402,7 +402,7 @@ private void AdjustPositionIfOutsideChartAndNotManualLayout() var chartBounds = ChartRenderer.ChartArea.Rectangle.Bounds; var plotBounds = ChartRenderer.GetPlotAreaTop(); - var chartMinY = chartBounds.Position.Y - ChartRenderer.GetPlotAreaTop(); + var chartMinY = ChartRenderer.Bounds.GlobalTop; var chartMinX = chartBounds.Position.X - ChartRenderer.Plotarea.LeftMargin; if (gTop < chartMinY) @@ -570,13 +570,13 @@ internal void SetShapeDimensions(Transform basePoint, Transform endPoint, Boundi else { //Set inside End - SetInOut(endToBaseVector, endPoint.LocalPosition, false); + SetInOut(endToBaseVector, endPoint.LocalPosition, true); } } else { //Set outside end - SetInOut(endToBaseVector, endPoint.LocalPosition, true); + SetInOut(endToBaseVector, endPoint.LocalPosition, false); } break; default: diff --git a/src/EPPlus/Drawing/Renderer/Chart/PieSliceRenderItem.cs b/src/EPPlus/Drawing/Renderer/Chart/PieSliceRenderItem.cs index 7f95d2c25b..4985d90257 100644 --- a/src/EPPlus/Drawing/Renderer/Chart/PieSliceRenderItem.cs +++ b/src/EPPlus/Drawing/Renderer/Chart/PieSliceRenderItem.cs @@ -87,7 +87,6 @@ bool ExistWithinRange(double target, double min, double max) void CalculateWidthHeight(double prevSliceDegrees) { - var endPointDegrees = prevSliceDegrees + Degrees; if (endPointDegrees < 0) { @@ -107,7 +106,7 @@ void CalculateWidthHeight(double prevSliceDegrees) double minY; double minX; - if (ExistWithinRange(90, startPointDegrees, endPointDegrees)) + if (/*endPointDegrees < startPointDegrees || */ExistWithinRange(90, startPointDegrees, endPointDegrees)) { maxY = _circleCenter.Top + _radius; } @@ -683,7 +682,7 @@ internal Transform GetInnerGroupWithTransformOriginTranslated() { Transform transform = new Transform(); transform.Parent = _innerGroup.Bounds.Parent; - transform.LocalPosition += new Vector2(_innerGroup.TransformOrigin.X + _innerGroup.TranslationOffset.Left, _innerGroup.TransformOrigin.Y + _innerGroup.TranslationOffset.Top); + transform.LocalPosition += new Vector2(_innerGroup.TransformOrigin.X + _innerGroup.TranslationOffset.Left - _innerGroup.Left, _innerGroup.TransformOrigin.Y + _innerGroup.TranslationOffset.Top- _innerGroup.Top); return transform; } @@ -700,7 +699,6 @@ internal BoundingBox GetBounds() BoundingBox box = new BoundingBox(LargestWidthRectangle, LargestHeightRectangle); box.Parent = ExtremePoints.Parent; box.Left = ExtremePoints.Left; - box.Top = ExtremePoints.Top; return box; } From 7101db3b31ad164308bc0354585057c00b459ec5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ossian=20Edstr=C3=B6m?= Date: Wed, 2 Sep 2026 17:05:33 +0200 Subject: [PATCH 64/73] Fixed faulty adjustment of BestFit --- .../Drawing/Renderer/Chart/DataLabels/SvgDataLabelPoint.cs | 4 ++-- src/EPPlus/Drawing/Renderer/Chart/PieSliceRenderItem.cs | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/EPPlus/Drawing/Renderer/Chart/DataLabels/SvgDataLabelPoint.cs b/src/EPPlus/Drawing/Renderer/Chart/DataLabels/SvgDataLabelPoint.cs index 8a00e4c6ad..56072ecae8 100644 --- a/src/EPPlus/Drawing/Renderer/Chart/DataLabels/SvgDataLabelPoint.cs +++ b/src/EPPlus/Drawing/Renderer/Chart/DataLabels/SvgDataLabelPoint.cs @@ -570,13 +570,13 @@ internal void SetShapeDimensions(Transform basePoint, Transform endPoint, Boundi else { //Set inside End - SetInOut(endToBaseVector, endPoint.LocalPosition, true); + SetInOut(endToBaseVector, endPoint.LocalPosition, false); } } else { //Set outside end - SetInOut(endToBaseVector, endPoint.LocalPosition, false); + SetInOut(endToBaseVector, endPoint.LocalPosition, true); } break; default: diff --git a/src/EPPlus/Drawing/Renderer/Chart/PieSliceRenderItem.cs b/src/EPPlus/Drawing/Renderer/Chart/PieSliceRenderItem.cs index 4985d90257..7ea714cdf0 100644 --- a/src/EPPlus/Drawing/Renderer/Chart/PieSliceRenderItem.cs +++ b/src/EPPlus/Drawing/Renderer/Chart/PieSliceRenderItem.cs @@ -698,7 +698,7 @@ internal BoundingBox GetBounds() { BoundingBox box = new BoundingBox(LargestWidthRectangle, LargestHeightRectangle); box.Parent = ExtremePoints.Parent; - box.Left = ExtremePoints.Left; + //box.Left = ExtremePoints.Left; return box; } From bd8cb091e06f267d2bd7dfb5db6546c8f11c2ff5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ossian=20Edstr=C3=B6m?= Date: Wed, 2 Sep 2026 17:27:02 +0200 Subject: [PATCH 65/73] Fixed bestfit issue --- src/EPPlus/Drawing/Renderer/Chart/PieSliceRenderItem.cs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/EPPlus/Drawing/Renderer/Chart/PieSliceRenderItem.cs b/src/EPPlus/Drawing/Renderer/Chart/PieSliceRenderItem.cs index 7ea714cdf0..cf847f186f 100644 --- a/src/EPPlus/Drawing/Renderer/Chart/PieSliceRenderItem.cs +++ b/src/EPPlus/Drawing/Renderer/Chart/PieSliceRenderItem.cs @@ -556,7 +556,7 @@ void CalculateLargestRectWithinCircleSegment() //Calculate thetha = alpha/4 var angleForTriangle = Degrees / 4d; - var angleForYTriangle = angleForTriangle + 1d; + var angleForYTriangle = angleForTriangle; var yTriangle = (Math.Sin(MConverter.DegreesToRadians(angleForYTriangle)) * _radius);// add 1 for small rounding fault making too small var xTriangle = (Math.Cos(MConverter.DegreesToRadians(angleForTriangle)) * _radius); @@ -598,7 +598,7 @@ void CalculateLargestRectWithinCircleSegment() { //Formula for largest (unrotated) rectangle within a semi-circle LargestWidthRectangle = Math.Sqrt(2d) *_radius; - LargestHeightRectangle = (Math.Sqrt(2d)/2) * _radius; + LargestHeightRectangle = (Math.Sqrt(2d)/2d) * _radius; } } @@ -698,7 +698,7 @@ internal BoundingBox GetBounds() { BoundingBox box = new BoundingBox(LargestWidthRectangle, LargestHeightRectangle); box.Parent = ExtremePoints.Parent; - //box.Left = ExtremePoints.Left; + box.Left = ExtremePoints.Left; return box; } From bdae92d2231f6a873af94f5efb47a13ada608883 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jan=20K=C3=A4llman?= Date: Thu, 3 Sep 2026 08:44:59 +0200 Subject: [PATCH 66/73] Fixed some pdf issues with blazor --- .../DocumentObjects/Fonts/PdfFontDescriptor.cs | 15 ++++++++------- src/EPPlus.Export.Pdf/Helpers/PdfString.cs | 9 +++++++++ src/EPPlus.Export.Pdf/Settings/PdfPageSettings.cs | 4 ++-- src/EPPlus/EPPlus.csproj | 6 +++--- src/EPPlusTest/Drawing/Chart/DataPointsTest.cs | 2 +- 5 files changed, 23 insertions(+), 13 deletions(-) diff --git a/src/EPPlus.Export.Pdf/DocumentObjects/Fonts/PdfFontDescriptor.cs b/src/EPPlus.Export.Pdf/DocumentObjects/Fonts/PdfFontDescriptor.cs index a1833327b2..8111423791 100644 --- a/src/EPPlus.Export.Pdf/DocumentObjects/Fonts/PdfFontDescriptor.cs +++ b/src/EPPlus.Export.Pdf/DocumentObjects/Fonts/PdfFontDescriptor.cs @@ -10,6 +10,7 @@ Date Author Change ************************************************************************************************* 27/11/2025 EPPlus Software AB EPPlus 9 *************************************************************************************************/ +using EPPlus.Export.Pdf.Helpers; using EPPlus.Graphics; using System; using System.IO; @@ -69,7 +70,7 @@ internal override string RenderDictionary() sb.AppendFormat($"<< /Type /FontDescriptor\n" + $" /FontName /{fontName.Replace(" ", "")}\n" + $" /Flags {flags}\n" + - $" /FontBBox [{fontBBox.X} {fontBBox.Y} {fontBBox.Width} {fontBBox.Height}]\n" + + $" /FontBBox [{fontBBox.X.ToPdfStringF0()} {fontBBox.Y.ToPdfStringF0()} {fontBBox.Width.ToPdfStringF0()} {fontBBox.Height.ToPdfStringF0()}]\n" + $" /Ascent {ascent}\n" + $" /Descent {descent}\n" + $" /CapHeight {capheight}\n" + @@ -93,12 +94,12 @@ internal override void RenderDictionary(BinaryWriter bw) sb.AppendFormat($"<< /Type /FontDescriptor\n" + $" /FontName /{fontName.Replace(" ", "")}\n" + $" /Flags {flags}\n" + - $" /FontBBox [{fontBBox.X} {fontBBox.Y} {fontBBox.Width} {fontBBox.Height}]\n" + - $" /Ascent {ascent}\n" + - $" /Descent {descent}\n" + - $" /CapHeight {capheight}\n" + - $" /ItalicAngle {(int)italicAngle}\n" + - $" /StemV {(int)stemV}"); + $" /FontBBox [{fontBBox.X.ToPdfStringF0()} {fontBBox.Y.ToPdfStringF0()} {fontBBox.Width.ToPdfStringF0()} {fontBBox.Height.ToPdfStringF0()}]\n" + + $" /Ascent {ascent.ToPdfStringF0()}\n" + + $" /Descent {descent.ToPdfStringF0()}\n" + + $" /CapHeight {capheight.ToPdfStringF0()}\n" + + $" /ItalicAngle {((int)italicAngle).ToPdfStringF0()}\n" + + $" /StemV {((int)stemV).ToPdfStringF0()}"); if (FontFile2ObjectNumber > 0) { sb.AppendFormat($"\n /FontFile2 {FontFile2ObjectNumber} 0 R"); diff --git a/src/EPPlus.Export.Pdf/Helpers/PdfString.cs b/src/EPPlus.Export.Pdf/Helpers/PdfString.cs index d26aea06f6..6c49ff4d14 100644 --- a/src/EPPlus.Export.Pdf/Helpers/PdfString.cs +++ b/src/EPPlus.Export.Pdf/Helpers/PdfString.cs @@ -45,5 +45,14 @@ internal static string ToPdfStringF0(this double val) { return val.ToString("F0", CultureInfo.InvariantCulture); } + /// + /// Returns the value formated for use in pdf document. + /// + /// Value to turn into a string. + /// The value repsented as a string with no decimals. + internal static string ToPdfStringF0(this int val) + { + return val.ToString("F0", CultureInfo.InvariantCulture); + } } } diff --git a/src/EPPlus.Export.Pdf/Settings/PdfPageSettings.cs b/src/EPPlus.Export.Pdf/Settings/PdfPageSettings.cs index 76287e5a12..fe20194ede 100644 --- a/src/EPPlus.Export.Pdf/Settings/PdfPageSettings.cs +++ b/src/EPPlus.Export.Pdf/Settings/PdfPageSettings.cs @@ -200,8 +200,8 @@ public PdfScaling Scaling internal string defaultFontName = ""; //DEBUG - internal bool Debug = false; - internal bool PrintAsText = false; + internal bool Debug = true; + internal bool PrintAsText = true; public PdfPageSettings(OpenTypeFontEngine fontEngine) { diff --git a/src/EPPlus/EPPlus.csproj b/src/EPPlus/EPPlus.csproj index 50e82f5938..2fa1b01218 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 - 9.0.0.1 - 9.0.0.1 - 9.0.0-preview2 + 9.0.0.2 + 9.0.0.2 + 9.0.0-preview true $(TargetsForTfmSpecificBuildOutput);IncludeReferencedProjectsInPackage diff --git a/src/EPPlusTest/Drawing/Chart/DataPointsTest.cs b/src/EPPlusTest/Drawing/Chart/DataPointsTest.cs index d4e5cb3a0b..f265be254d 100644 --- a/src/EPPlusTest/Drawing/Chart/DataPointsTest.cs +++ b/src/EPPlusTest/Drawing/Chart/DataPointsTest.cs @@ -72,7 +72,7 @@ public void LineChart() File.WriteAllText($"{_worksheetPath}svg\\EPPlusLineChart1.svg", svg); } [TestMethod] -linb public void PieChart() + public void PieChart() { var ws = _pck.Workbook.Worksheets.Add("PieChart"); LoadTestdata(ws); From 7e1d804326a1353fa4250d19fa6bb426d612dff9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ossian=20Edstr=C3=B6m?= Date: Thu, 3 Sep 2026 09:41:26 +0200 Subject: [PATCH 67/73] Fixed white borders on data points --- .../Renderer/Chart/ChartTypeDrawers/ChartTypeDrawer.cs | 4 ++-- src/EPPlusTest/Drawing/Chart/DataPointsTest.cs | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/EPPlus/Drawing/Renderer/Chart/ChartTypeDrawers/ChartTypeDrawer.cs b/src/EPPlus/Drawing/Renderer/Chart/ChartTypeDrawers/ChartTypeDrawer.cs index 69c75a57b1..645a4e1bc9 100644 --- a/src/EPPlus/Drawing/Renderer/Chart/ChartTypeDrawers/ChartTypeDrawer.cs +++ b/src/EPPlus/Drawing/Renderer/Chart/ChartTypeDrawers/ChartTypeDrawer.cs @@ -239,7 +239,7 @@ internal static void SetFillDataPoint(ExcelChart chart, ExcelChartStandardSerie var color = GetVaryColor(theme, chart.StyleManager?.ColorsManager, index); item.SetDrawingPropertiesFill(theme, dp.Fill.IsEmpty ? cStandardSerie.Fill : dp.Fill, entry?.FillReference.Color, spaceSettings, color); - item.SetDrawingPropertiesBorder(theme, dp.Border.IsEmpty ? cStandardSerie.Border : dp.Border, entry?.BorderReference.Color, dp.Border.Fill.Style != eFillStyle.NoFill, null, 0.75); + item.SetDrawingPropertiesBorder(theme, dp.Border.IsEmpty ? cStandardSerie.Border : dp.Border, entry?.BorderReference.Color, dp.Border.Fill.Style != eFillStyle.NoFill, () => Color.Transparent, 0.75); } internal static void SetFillSerie(ExcelChart chart, ExcelChart ct, ExcelChartStandardSerie cStandardSerie, int serieIndex, int index, RenderItem item) @@ -256,7 +256,7 @@ internal static void SetFillSerie(ExcelChart chart, ExcelChart ct, ExcelChartSta var color = GetVaryColor(theme, chart.StyleManager?.ColorsManager, serieIndex); 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); + item.SetDrawingPropertiesBorder(theme, cStandardSerie.Border, chart.StyleManager.Style?.SeriesLine.BorderReference.Color, cStandardSerie.Border.Fill.Style != eFillStyle.NoFill, () => Color.Transparent, 0.75); } private static Color? GetVaryColor(ExcelTheme theme, ExcelChartColorsManager colorsManager, int index) diff --git a/src/EPPlusTest/Drawing/Chart/DataPointsTest.cs b/src/EPPlusTest/Drawing/Chart/DataPointsTest.cs index d4e5cb3a0b..9052f04051 100644 --- a/src/EPPlusTest/Drawing/Chart/DataPointsTest.cs +++ b/src/EPPlusTest/Drawing/Chart/DataPointsTest.cs @@ -72,7 +72,7 @@ public void LineChart() File.WriteAllText($"{_worksheetPath}svg\\EPPlusLineChart1.svg", svg); } [TestMethod] -linb public void PieChart() + public void PieChart() { var ws = _pck.Workbook.Worksheets.Add("PieChart"); LoadTestdata(ws); @@ -136,7 +136,7 @@ public void GradientPieChart() } } - [TestMethod] + [TestMethod] public void DataLabelsMultipleOneSeriesExport() { using (var pck = OpenPackage("DataLabelsMultipleOneSeriesExport.xlsx", true)) From dbdd631e8acd0d6cbf3bf1905ebe9b75d84019de Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jan=20K=C3=A4llman?= Date: Thu, 3 Sep 2026 10:24:12 +0200 Subject: [PATCH 68/73] Fixed bar/column charts with date axis and negative values --- .../Chart/ColumnChartTests.cs | 43 +++++++++++++++++++ .../BarColumnChartTypeDrawer.cs | 8 ++-- 2 files changed, 47 insertions(+), 4 deletions(-) diff --git a/src/EPPlus.DrawingRenderer.Tests/Chart/ColumnChartTests.cs b/src/EPPlus.DrawingRenderer.Tests/Chart/ColumnChartTests.cs index 6d6002687e..0f69cf48d4 100644 --- a/src/EPPlus.DrawingRenderer.Tests/Chart/ColumnChartTests.cs +++ b/src/EPPlus.DrawingRenderer.Tests/Chart/ColumnChartTests.cs @@ -53,5 +53,48 @@ public void GenerateSvgForColumnCharts2() } } } + [TestMethod] + public void GenerateColumnChartFromBlazorSample() + { + ExcelPackage.License.SetNonCommercialOrganization("EPPlus Project"); + using (var p = OpenTemplatePackage("BlazorSample1-Column.xlsx")) + { + var ws = p.Workbook.Worksheets[1]; + + //var ix = 2; + //var c = ws.Drawings[ix]; + //var svg = renderer.RenderDrawingToSvg(c); + //SaveTextFileToWorkbook($"svg\\ChartForSvg_ind{ix++}.svg", svg); + + var ix = 0; + foreach (ExcelChart c in ws.Drawings) + { + var svg = c.ToSvg(); + SaveTextFileToWorkbook($"svg\\BlazorSample_Column_sheet2_{ix++}.svg", svg); + } + } + } + [TestMethod] + public void GenerateBarChartFromBlazorSample() + { + ExcelPackage.License.SetNonCommercialOrganization("EPPlus Project"); + using (var p = OpenTemplatePackage("BlazorSample1-BarChart.xlsx")) + { + var ws = p.Workbook.Worksheets[1]; + + //var ix = 2; + //var c = ws.Drawings[ix]; + //var svg = renderer.RenderDrawingToSvg(c); + //SaveTextFileToWorkbook($"svg\\ChartForSvg_ind{ix++}.svg", svg); + + var ix = 0; + foreach (ExcelChart c in ws.Drawings) + { + var svg = c.ToSvg(); + SaveTextFileToWorkbook($"svg\\BlazorSample_Bar_sheet2_{ix++}.svg", svg); + } + } + } + } } diff --git a/src/EPPlus/Drawing/Renderer/Chart/ChartTypeDrawers/BarColumnChartTypeDrawer.cs b/src/EPPlus/Drawing/Renderer/Chart/ChartTypeDrawers/BarColumnChartTypeDrawer.cs index b4596b82e0..05a4d07002 100644 --- a/src/EPPlus/Drawing/Renderer/Chart/ChartTypeDrawers/BarColumnChartTypeDrawer.cs +++ b/src/EPPlus/Drawing/Renderer/Chart/ChartTypeDrawers/BarColumnChartTypeDrawer.cs @@ -221,7 +221,7 @@ private void AddBar(ExcelBarChart chartType, ExcelBarChartSerie serie, List y)) //Below axis { rect.Top = chartBaseY; rect.Height = yPos - chartBaseY; @@ -320,7 +320,7 @@ private void AddBar(ExcelBarChart chartType, ExcelBarChartSerie serie, List y)) //Below axis { rect.Left = yPos; rect.Width = chartBaseY - yPos; From 6ea3ad63a4f82da4aca0e1c6182ab20ae18489dc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ossian=20Edstr=C3=B6m?= Date: Thu, 3 Sep 2026 11:05:08 +0200 Subject: [PATCH 69/73] Fixed transparent blazor bars --- .../Chart/ChartTypeDrawers/BarColumnChartTypeDrawer.cs | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/EPPlus/Drawing/Renderer/Chart/ChartTypeDrawers/BarColumnChartTypeDrawer.cs b/src/EPPlus/Drawing/Renderer/Chart/ChartTypeDrawers/BarColumnChartTypeDrawer.cs index 05a4d07002..1f59cd0762 100644 --- a/src/EPPlus/Drawing/Renderer/Chart/ChartTypeDrawers/BarColumnChartTypeDrawer.cs +++ b/src/EPPlus/Drawing/Renderer/Chart/ChartTypeDrawers/BarColumnChartTypeDrawer.cs @@ -211,7 +211,11 @@ private void AddBar(ExcelBarChart chartType, ExcelBarChartSerie serie, List Date: Thu, 3 Sep 2026 12:31:49 +0200 Subject: [PATCH 70/73] Adjusted rounding var for BestFit pieCharts --- src/EPPlus/Drawing/Renderer/Chart/PieSliceRenderItem.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/EPPlus/Drawing/Renderer/Chart/PieSliceRenderItem.cs b/src/EPPlus/Drawing/Renderer/Chart/PieSliceRenderItem.cs index cf847f186f..f638800802 100644 --- a/src/EPPlus/Drawing/Renderer/Chart/PieSliceRenderItem.cs +++ b/src/EPPlus/Drawing/Renderer/Chart/PieSliceRenderItem.cs @@ -556,7 +556,7 @@ void CalculateLargestRectWithinCircleSegment() //Calculate thetha = alpha/4 var angleForTriangle = Degrees / 4d; - var angleForYTriangle = angleForTriangle; + var angleForYTriangle = angleForTriangle + 0.64d; var yTriangle = (Math.Sin(MConverter.DegreesToRadians(angleForYTriangle)) * _radius);// add 1 for small rounding fault making too small var xTriangle = (Math.Cos(MConverter.DegreesToRadians(angleForTriangle)) * _radius); From 4e3989ef568f1d8f16c8c67eb7e79859f274f51d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jan=20K=C3=A4llman?= Date: Thu, 3 Sep 2026 12:50:10 +0200 Subject: [PATCH 71/73] Fixes axis positioning with negative min values --- .../Renderer/Chart/ChartAxisRenderer.cs | 20 +++++++++++++ src/EPPlus/Drawing/Renderer/ChartRenderer.cs | 30 ++++++++++++------- 2 files changed, 39 insertions(+), 11 deletions(-) diff --git a/src/EPPlus/Drawing/Renderer/Chart/ChartAxisRenderer.cs b/src/EPPlus/Drawing/Renderer/Chart/ChartAxisRenderer.cs index 07d94d6689..7bba7b2df8 100644 --- a/src/EPPlus/Drawing/Renderer/Chart/ChartAxisRenderer.cs +++ b/src/EPPlus/Drawing/Renderer/Chart/ChartAxisRenderer.cs @@ -1268,5 +1268,25 @@ private bool ShouldHavePadding() { return GetDefaultBorderColorForElement(ChartElement.Axis, (int)Chart.Style); } + + internal double GetCrossesValue() + { + if (Axis.CrossingAxis.CrossesAt.HasValue) + { + return Axis.CrossingAxis.CrossesAt.Value; + } + else + { + switch (Axis.CrossingAxis.Crosses) + { + case eCrosses.Min: + return Min; + case eCrosses.Max: + return Max; + default: + return 0D; + } + } + } } } \ No newline at end of file diff --git a/src/EPPlus/Drawing/Renderer/ChartRenderer.cs b/src/EPPlus/Drawing/Renderer/ChartRenderer.cs index bbd778db34..131d6ae4d8 100644 --- a/src/EPPlus/Drawing/Renderer/ChartRenderer.cs +++ b/src/EPPlus/Drawing/Renderer/ChartRenderer.cs @@ -103,12 +103,16 @@ private void SetAxisPositionsFromPlotarea() //Make sure the horizontal axis is moved up if the vertical axis has a negative minimum value, so that the 0 value is at the correct position. if (VerticalAxis.Axis.TickLabelPosition == eTickLabelPosition.NextTo && HorizontalAxis.Axis.AxisType == eAxisType.Val && HorizontalAxis.Min < 0D) { - Plotarea.Rectangle.Width += VerticalAxis.Rectangle.Width; - Plotarea.Group.Left = VerticalAxis.Rectangle.Left; - var newRight = Plotarea.Group.Left + HorizontalAxis.GetPositionInPlotarea(0D); - var rightDiff = newRight - VerticalAxis.Rectangle.Width; - VerticalAxis.Rectangle.Left = rightDiff; - VerticalAxis.Line.X1 = VerticalAxis.Line.X2 = newRight; + var CrossValue = HorizontalAxis.GetCrossesValue(); + var newRight = Plotarea.Group.Left + HorizontalAxis.GetPositionInPlotarea(CrossValue); + if(newRight > Plotarea.Group.Left) + { + Plotarea.Rectangle.Width += VerticalAxis.Rectangle.Width; + Plotarea.Group.Left = VerticalAxis.Rectangle.Left; + var rightDiff = newRight - VerticalAxis.Rectangle.Width; + VerticalAxis.Rectangle.Left = rightDiff; + VerticalAxis.Line.X1 = VerticalAxis.Line.X2 = newRight; + } } VerticalAxis.AddTickmarksAndValues(DefItems); } @@ -120,11 +124,15 @@ private void SetAxisPositionsFromPlotarea() //Make sure the horizontal axis is moved up if the vertical axis has a negative minimum value, so that the 0 value is at the correct position. if (HorizontalAxis.Axis.TickLabelPosition == eTickLabelPosition.NextTo && VerticalAxis.Axis.AxisType == eAxisType.Val && VerticalAxis.Min < 0D && HorizontalAxis.Axis.Crosses == eCrosses.AutoZero) { - var newtop = VerticalAxis.GetPositionInPlotarea(0D) + Plotarea.Group.Top; - var topDiff = HorizontalAxis.Rectangle.Top - newtop; - HorizontalAxis.Rectangle.Top = newtop; - HorizontalAxis.Rectangle.Height += topDiff; - HorizontalAxis.Line.Y1 = HorizontalAxis.Line.Y2 = newtop; + var CrossValue = HorizontalAxis.GetCrossesValue(); + var newtop = VerticalAxis.GetPositionInPlotarea(CrossValue) + Plotarea.Group.Top; + if (newtop > Plotarea.Group.Top) + { + var topDiff = HorizontalAxis.Rectangle.Top - newtop; + HorizontalAxis.Rectangle.Top = newtop; + HorizontalAxis.Rectangle.Height += topDiff; + HorizontalAxis.Line.Y1 = HorizontalAxis.Line.Y2 = newtop; + } } HorizontalAxis.AddTickmarksAndValues(DefItems); From 330d785d521023f673c458cbd1eae2e683b5adb9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ossian=20Edstr=C3=B6m?= Date: Thu, 3 Sep 2026 13:56:54 +0200 Subject: [PATCH 72/73] Add svg wrap tests --- .../TextRenderTests.cs | 22 +++++++++---------- .../Renderer/Chart/ChartLegendRenderer.cs | 2 +- 2 files changed, 12 insertions(+), 12 deletions(-) diff --git a/src/EPPlus.DrawingRenderer.Tests/TextRenderTests.cs b/src/EPPlus.DrawingRenderer.Tests/TextRenderTests.cs index d04da9f575..895f286aec 100644 --- a/src/EPPlus.DrawingRenderer.Tests/TextRenderTests.cs +++ b/src/EPPlus.DrawingRenderer.Tests/TextRenderTests.cs @@ -39,7 +39,7 @@ // var c = System.Drawing.Color.FromName(color); // if (c.IsEmpty) // { -// var sc = Enum.Parse(color); +// var sc = (eSchemeColor)Enum.Parse(typeof(eSchemeColor), color); // fill.SolidFill.Color.SetSchemeColor(sc); // } // else @@ -49,7 +49,7 @@ // } // catch // { -// var sc = Enum.Parse(color); +// var sc = (eSchemeColor)Enum.Parse(typeof(eSchemeColor), color); // fill.SolidFill.Color.SetSchemeColor(sc); // } // } @@ -76,13 +76,13 @@ // // fontSizes.Add(runFont.Size); // // } - + // // return new TextFragmentCollectionSimple(fonts, runContents); // //} // //List GetWrappedText(ExcelDrawingTextRunCollection runs, TextFragmentCollectionSimple fragments) // //{ - + // // List fonts = new List(); // // for (int i = 0; i < runs.Count(); i++) @@ -146,7 +146,7 @@ // Style = MeasurementFontStyles.Regular // }; -// List fonts = new() { /*font1,*/ font2, font3, font4, font5, font6}; +// List fonts = new() { /*font1,*/ font2, font3, font4, font5, font6 }; // var maxSizePoints = Math.Round(300d, 0, MidpointRounding.AwayFromZero).PixelToPoint(); // var ttMeasurer = OpenTypeFonts.GetTextLayoutEngineForFont(font2); @@ -224,14 +224,14 @@ // var txtRuns2 = tbItem.Paragraphs[1].Runs; -// Assert.AreEqual(53.20963541666667d, txtRuns2[0].Bounds.Width.PointToPixel(),0.2); +// Assert.AreEqual(53.20963541666667d, txtRuns2[0].Bounds.Width.PointToPixel(), 0.2); // var currentLineWidth = txtRuns2[0].Bounds.Width.PointToPixel(); -// Assert.AreEqual(currentLineWidth, txtRuns2[1].Bounds.Left.PointToPixel(),0.2); -// Assert.AreEqual(69.55924479166667d, txtRuns2[1].Bounds.Width.PointToPixel(),0.2); +// Assert.AreEqual(currentLineWidth, txtRuns2[1].Bounds.Left.PointToPixel(), 0.2); +// Assert.AreEqual(69.55924479166667d, txtRuns2[1].Bounds.Width.PointToPixel(), 0.2); // currentLineWidth += txtRuns2[1].Bounds.Width.PointToPixel(); -// Assert.AreEqual(currentLineWidth, txtRuns2[2].Bounds.Left.PointToPixel(),0.0001); +// Assert.AreEqual(currentLineWidth, txtRuns2[2].Bounds.Left.PointToPixel(), 0.0001); // Assert.AreEqual(49.89388020833334, txtRuns2[2].Bounds.Width.PointToPixel(), 0.0001); // currentLineWidth += txtRuns2[2].Bounds.Width.PointToPixel(); @@ -328,7 +328,7 @@ // //Appears off by 1-2 px bc of border width -// Assert.AreEqual(190d, tbItem.Bounds.GlobalTop.PointToPixel() , 1.0); +// Assert.AreEqual(190d, tbItem.Bounds.GlobalTop.PointToPixel(), 1.0); // } @@ -379,7 +379,7 @@ // //var trItem = new SvgTextRunItem(svgShape, lastpara.Bounds, font, "MyText"); // //lastpara.Runs.Add(trItem); -// lastpara.AddOwnText(new TextFragment() {Text = "my new text", Font = font }); +// lastpara.AddOwnText(new TextFragment() { Text = "my new text", Font = font }); // svgShape.Render(sb); // var str = sb.ToString(); diff --git a/src/EPPlus/Drawing/Renderer/Chart/ChartLegendRenderer.cs b/src/EPPlus/Drawing/Renderer/Chart/ChartLegendRenderer.cs index fe8ff071ed..7330b957ab 100644 --- a/src/EPPlus/Drawing/Renderer/Chart/ChartLegendRenderer.cs +++ b/src/EPPlus/Drawing/Renderer/Chart/ChartLegendRenderer.cs @@ -924,7 +924,7 @@ private RectRenderItem GetPieSeriesIcon(ExcelChart ct, ExcelPieChartSerie pcS, D item.Height = iconHeight; item.SetDrawingPropertiesFill(ChartRenderer.Theme, pcS.Fill, Chart.StyleManager.Style?.SeriesLine.FillReference.Color); - item.SetDrawingPropertiesBorder(ChartRenderer.Theme, pcS.Border, Chart.StyleManager.Style?.SeriesLine.BorderReference.Color, pcS.Border.Fill.Style != eFillStyle.NoFill, () => DefaultBorderColor, 0.75); + item.SetDrawingPropertiesBorder(ChartRenderer.Theme, pcS.Border, Chart.StyleManager.Style?.SeriesLine.BorderReference.Color, pcS.Border.Fill.Style != eFillStyle.NoFill, () => DefaultBorderColor, 1.5d); return item; } From 2c1999208ea950736a32bbc3c25812a04ceafa93 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ossian=20Edstr=C3=B6m?= Date: Thu, 3 Sep 2026 14:21:50 +0200 Subject: [PATCH 73/73] Actually added tests --- .../Shape/ShapeWrappingTests.cs | 75 +++++++++++++++++++ 1 file changed, 75 insertions(+) create mode 100644 src/EPPlus.DrawingRenderer.Tests/Shape/ShapeWrappingTests.cs diff --git a/src/EPPlus.DrawingRenderer.Tests/Shape/ShapeWrappingTests.cs b/src/EPPlus.DrawingRenderer.Tests/Shape/ShapeWrappingTests.cs new file mode 100644 index 0000000000..16c45c9749 --- /dev/null +++ b/src/EPPlus.DrawingRenderer.Tests/Shape/ShapeWrappingTests.cs @@ -0,0 +1,75 @@ +using OfficeOpenXml; +using OfficeOpenXml.Drawing; +using System; +using System.Collections.Generic; +using System.Drawing; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace EPPlus.DrawingRenderer.Tests.Shape +{ + [TestClass] + public class ShapeWrappingTests : TestBase + { + [TestMethod] + public void WrapEveryLetter() + { + using(var p = OpenPackage("WrapEveryLetterSvg.xlsx",true)) + { + var ws = p.Workbook.Worksheets.Add("wrapShapes"); + + var txtBox = ws.Drawings.AddTextbox("txtBox1", "MY WORLD"); + txtBox.As.Shape.SetSize(30, 150); + + var svg = txtBox.ToSvg(); + File.WriteAllText(GetOutputFile("svg\\", "WrapEveryLetter.svg").FullName, svg); + SaveAndCleanup(p); + } + } + + [TestMethod] + public void WrapAndColor() + { + using (var p = OpenPackage("WrapInRect.xlsx", true)) + { + var ws = p.Workbook.Worksheets.Add("wrap"); + + var _currentShape = ws.Drawings.AddShape("MyShape", OfficeOpenXml.Drawing.eShapeStyle.Rect); + _currentShape.SetSize(36, 200); + + _currentShape.Font.Color = System.Drawing.Color.Goldenrod; + _currentShape.TextBody.LeftInsert = 0; + _currentShape.TextBody.RightInsert = 0; + _currentShape.TextBody.TopInsert = 0; + _currentShape.TextBody.BottomInsert = 0; + + var rt1 = _currentShape.RichText.Add("M", true); + var rt2 = _currentShape.RichText.Add("Y ", false); + var rt3 = _currentShape.RichText.Add("W", false); + var rt4 = _currentShape.RichText.Add("O", false); + var rt5 = _currentShape.RichText.Add("R", false); + var rt6 = _currentShape.RichText.Add("L", false); + var rt7 = _currentShape.RichText.Add("D", false); + _currentShape.RichText.Add("MY WORLD", true); + + var startColor = KnownColor.Plum; + + _currentShape.RichText.Add("Default world", true); + + foreach (var item in _currentShape.RichText) + { + item.Color = System.Drawing.Color.FromKnownColor(startColor); + startColor += 1; + } + + _currentShape.TextBody.Anchor = eTextAnchoringType.Top; + + var svg = _currentShape.ToSvg(); + File.WriteAllText(GetOutputFile("svg\\", "WrapAndColor.svg").FullName, svg); + + SaveAndCleanup(p); + } + } + } +}