diff --git a/src/EPPlus/Core/Worksheet/XmlWriter/WorksheetXmlWriter.cs b/src/EPPlus/Core/Worksheet/XmlWriter/WorksheetXmlWriter.cs
index 28f85fe222..28cea9635f 100644
--- a/src/EPPlus/Core/Worksheet/XmlWriter/WorksheetXmlWriter.cs
+++ b/src/EPPlus/Core/Worksheet/XmlWriter/WorksheetXmlWriter.cs
@@ -512,7 +512,7 @@ private void UpdateRowCellData(StreamWriter sw, string prefix)
}
else
{
- cache.Append($"<{cTag} r=\"{cse.CellAddress}\" s=\"{styleID}\"{ConvertUtil.GetCellType(v, true)}{mdAttr}>{GetFormulaValue(v, prefix)}{cTag}>");
+ cache.Append($"<{cTag} r=\"{cse.CellAddress}\" s=\"{styleID}\"{ConvertUtil.GetCellType(v, true)}{mdAttr}><{fTag} t=\"shared\" si=\"{sfId}\" {mdAttrForFTag}/>{GetFormulaValue(v, prefix)}{cTag}>");
}
}
else
@@ -646,12 +646,13 @@ private void UpdateRowCellData(StreamWriter sw, string prefix)
private void SetCalcAttributesForArrayFormula(bool hasFlags, CellStoreEnumerator cse, ref string mdAttrForFTag)
{
+ // #2572: Reset so attributes from a previous cell are not carried over to this cell.
+ mdAttrForFTag = "";
//Set calc attributes for array formula. We preserve them from load only at this point.
if (hasFlags)
{
if (_ws._flags.Exists(cse.Row, cse.Column))
{
- mdAttrForFTag = "";
if (_ws._flags.GetFlagValue(cse.Row, cse.Column, CellFlags.CellFlagAlwaysCalculateArray))
{
mdAttrForFTag = $" aca=\"1\"";
@@ -663,6 +664,7 @@ private void SetCalcAttributesForArrayFormula(bool hasFlags, CellStoreEnumerator
}
}
}
+
private string GenerateArrayFormulaXml(string cTag, string fTag, string cellAddress, string styleID, string mdAttr, string mdAttrForFTag, string refAddress, object value, string formula, string prefix)
{
var cellType = ConvertUtil.GetCellType(value, true);
@@ -1154,7 +1156,7 @@ private string UpdateConditionalFormattingAttributes(
return cache.ToString();
}
- private string GetXMLCFExt(string prefix, ExcelConditionalFormattingRule format, int i, int count)
+ private string GetXMLCFExt(string prefix, string mainPrefix, ExcelConditionalFormattingRule format, int i, int count)
{
var cache = new StringBuilder();
@@ -1349,7 +1351,7 @@ private string GetXMLCFExt(string prefix, ExcelConditionalFormattingRule format,
cache.Append($"<{prefix}cfRule type=\"iconSet\" priority=\"{format.Priority}\" id=\"{{{uid}}}\">");
cache.Append($"<{prefix}iconSet ");
- cache.Append($"iconSet=\"{iconSetString}\"");
+ cache.Append($"iconSet=\"{iconSetString}\"");
if (showValue == false)
{
@@ -1514,7 +1516,8 @@ private string GetXMLCFExt(string prefix, ExcelConditionalFormattingRule format,
if (format.Style.HasValue)
{
- WriteStyle(cache, prefix, format);
+ // Sc1083: The dxf children belong to the main schema, so they need the main prefix.
+ WriteStyle(cache, prefix, mainPrefix, format);
}
}
cache.Append($"{prefix}cfRule>");
@@ -1578,7 +1581,8 @@ private string UpdateExtLstConditionalFormatting(string mainPrefix)
foreach (var rule in pivotCFs)
{
- cache.Append(GetXMLCFExt(prefix, rule, 0, 1));
+ // Sc1083: Pass mainPrefix so elements from the main schema get the worksheet's namespace prefix.
+ cache.Append(GetXMLCFExt(prefix, mainPrefix, rule, 0, 1));
}
foreach (var ruleList in addressDict)
@@ -1586,7 +1590,8 @@ private string UpdateExtLstConditionalFormatting(string mainPrefix)
for (int i = 0; i < ruleList.Value.Count; i++)
{
var format = ruleList.Value[i];
- cache.Append(GetXMLCFExt(prefix, ruleList.Value[i], i, ruleList.Value.Count));
+ // Sc1083: Pass mainPrefix so elements from the main schema get the worksheet's namespace prefix.
+ cache.Append(GetXMLCFExt(prefix, mainPrefix, ruleList.Value[i], i, ruleList.Value.Count));
}
}
@@ -1637,11 +1642,11 @@ string WriteColorOption(string nodeName, ExcelDxfColor color)
return returnString;
}
- private string WriteCfIcon(ExcelConditionalFormattingIconDataBarValue icon, bool gteCheck = true)
+ private string WriteCfIcon(string prefix, ExcelConditionalFormattingIconDataBarValue icon, bool gteCheck = true)
{
StringBuilder cache = new StringBuilder();
- cache.Append($"");
}
- void WriteStyle(StringBuilder cache, string prefix, ExcelConditionalFormattingRule format)
+ void WriteStyle(StringBuilder cache, string prefix, string mainPrefix, ExcelConditionalFormattingRule format)
{
+ // #2572: dxf itself is in the x14 namespace (prefix), but its children belong to the main schema (mainPrefix).
if (format.Style.HasValue)
{
cache.Append($"<{prefix}dxf>");
if (format.Style.Font.HasValue)
{
- cache.Append($"");
+ cache.Append($"<{mainPrefix}font>");
if (format.Style.Font.Bold == true)
{
- cache.Append($"");
+ cache.Append($"<{mainPrefix}b/>");
}
if (format.Style.Font.Italic == true)
{
if (format.Style.Font.Bold == false || format.Style.Font.Bold == null)
{
- cache.Append("");
+ cache.Append($"<{mainPrefix}b val=\"0\"/>");
}
- cache.Append($"");
+ cache.Append($"<{mainPrefix}i/>");
}
if (format.Style.Font.Bold == false && format.Style.Font.Italic == false)
{
- cache.Append("");
- cache.Append("");
+ cache.Append($"<{mainPrefix}b val=\"0\"/>");
+ cache.Append($"<{mainPrefix}i val=\"0\"/>");
}
if (format.Style.Font.Strike == true)
{
- cache.Append($"");
+ cache.Append($"<{mainPrefix}strike/>");
}
if (format.Style.Font.Underline.HasValue == true)
{
- cache.Append($"");
+ cache.Append($"{mainPrefix}font>");
}
if (format.Style.NumberFormat.HasValue)
{
var numberFormat = format.Style.NumberFormat.Format.EncodeXMLAttribute();
- cache.Append($"");
}
if (format.Style.Fill.HasValue)
{
- cache.Append("");
+ cache.Append($"<{mainPrefix}fill>");
switch (format.Style.Fill.Style)
{
@@ -2085,28 +2093,28 @@ void WriteStyle(StringBuilder cache, string prefix, ExcelConditionalFormattingRu
if (format.Style.Fill.PatternType != null && format.Style.Fill.PatternType != ExcelFillStyle.None)
{
- cache.Append($"");
+ cache.Append($"<{mainPrefix}patternFill patternType=\"{format.Style.Fill.PatternType.ToEnumString()}\">");
}
else
{
- cache.Append("");
+ cache.Append($"<{mainPrefix}patternFill>");
}
if (format.Style.Fill.PatternColor.HasValue)
{
- cache.Append(WriteColorOption("fgColor", format.Style.Fill.PatternColor));
+ cache.Append(WriteColorOption($"{mainPrefix}fgColor", format.Style.Fill.PatternColor));
}
if (format.Style.Fill.BackgroundColor.HasValue)
{
- cache.Append(WriteColorOption("bgColor", format.Style.Fill.BackgroundColor));
+ cache.Append(WriteColorOption($"{mainPrefix}bgColor", format.Style.Fill.BackgroundColor));
}
- cache.Append("");
+ cache.Append($"{mainPrefix}patternFill>");
break;
case Style.eDxfFillStyle.GradientFill:
- cache.Append("");
- cache.Append("");
+ cache.Append($"<{mainPrefix}stop position=\"0\">");
- cache.Append(WriteColorOption("color", format.Style.Fill.Gradient.Colors[0].Color));
+ cache.Append(WriteColorOption($"{mainPrefix}color", format.Style.Fill.Gradient.Colors[0].Color));
- cache.Append("");
- cache.Append("");
+ cache.Append($"{mainPrefix}stop>");
+ cache.Append($"<{mainPrefix}stop position=\"1.0\">");
- cache.Append(WriteColorOption("color", format.Style.Fill.Gradient.Colors[1].Color));
+ cache.Append(WriteColorOption($"{mainPrefix}color", format.Style.Fill.Gradient.Colors[1].Color));
- cache.Append("");
+ cache.Append($"{mainPrefix}stop>");
- cache.Append("");
+ cache.Append($"{mainPrefix}gradientFill>");
break;
}
- cache.Append("");
+ cache.Append($"{mainPrefix}fill>");
}
if (format.Style.Border.HasValue)
{
- cache.Append("");
+ cache.Append($"<{mainPrefix}border>");
if (format.Style.Border.Left.HasValue)
{
- cache.Append($"");
- cache.Append(WriteColorOption("color", format.Style.Border.Left.Color));
- cache.Append("");
+ cache.Append($"<{mainPrefix}left style=\"{format.Style.Border.Left.Style.ToString().ToLower()}\">");
+ cache.Append(WriteColorOption($"{mainPrefix}color", format.Style.Border.Left.Color));
+ cache.Append($"{mainPrefix}left>");
}
if (format.Style.Border.Right.HasValue)
{
- cache.Append($"");
- cache.Append(WriteColorOption("color", format.Style.Border.Right.Color));
- cache.Append("");
+ cache.Append($"<{mainPrefix}right style=\"{format.Style.Border.Right.Style.ToString().ToLower()}\">");
+ cache.Append(WriteColorOption($"{mainPrefix}color", format.Style.Border.Right.Color));
+ cache.Append($"{mainPrefix}right>");
}
if (format.Style.Border.Top.HasValue)
{
- cache.Append($"");
- cache.Append(WriteColorOption("color", format.Style.Border.Top.Color));
- cache.Append("");
+ cache.Append($"<{mainPrefix}top style=\"{format.Style.Border.Top.Style.ToString().ToLower()}\">");
+ cache.Append(WriteColorOption($"{mainPrefix}color", format.Style.Border.Top.Color));
+ cache.Append($"{mainPrefix}top>");
}
if (format.Style.Border.Bottom.HasValue)
{
- cache.Append($"");
- cache.Append(WriteColorOption("color", format.Style.Border.Bottom.Color));
- cache.Append("");
+ cache.Append($"<{mainPrefix}bottom style=\"{format.Style.Border.Bottom.Style.ToString().ToLower()}\">");
+ cache.Append(WriteColorOption($"{mainPrefix}color", format.Style.Border.Bottom.Color));
+ cache.Append($"{mainPrefix}bottom>");
}
- cache.Append("");
- cache.Append("");
- cache.Append("");
+ cache.Append($"<{mainPrefix}vertical/>");
+ cache.Append($"<{mainPrefix}horizontal/>");
+ cache.Append($"{mainPrefix}border>");
}
cache.Append($"{prefix}dxf>");
diff --git a/src/EPPlusTest/Core/Worksheet/WorksheetXmlWriterTests.cs b/src/EPPlusTest/Core/Worksheet/WorksheetXmlWriterTests.cs
new file mode 100644
index 0000000000..84b7306642
--- /dev/null
+++ b/src/EPPlusTest/Core/Worksheet/WorksheetXmlWriterTests.cs
@@ -0,0 +1,181 @@
+/*************************************************************************************************
+ 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
+ *************************************************************************************************
+ 10/12/2026 EPPlus Software AB Initial release, tests for issue #2572
+ *************************************************************************************************/
+using Microsoft.VisualStudio.TestTools.UnitTesting;
+using OfficeOpenXml;
+using OfficeOpenXml.ConditionalFormatting;
+using System;
+using System.Collections.Generic;
+using System.IO;
+using System.Xml;
+
+namespace EPPlusTest.Core.Worksheet
+{
+ ///
+ /// Tests for the XML written by the worksheet XML writer.
+ /// The prefix tests use a worksheet where the main schema has a namespace prefix (x:) and no default namespace,
+ /// as written by the Open XML SDK. Any element written without the prefix ends up in no namespace.
+ ///
+ [TestClass]
+ public class WorksheetXmlWriterTests : TestBase
+ {
+ private const string PrefixedWorkbook = "sc1083-sdk.xlsx";
+ private const string CalcFlagsWorkbook = "sc1083-flags.xlsx";
+ private const string WorksheetPart = "xl/worksheets/sheet1.xml";
+ private const string SchemaMainX14 = "http://schemas.microsoft.com/office/spreadsheetml/2009/9/main";
+
+ ///
+ /// Issue #2572: Shared formula children were written as <f> without the namespace prefix,
+ /// so Excel treated the cells as constants.
+ ///
+ [TestMethod]
+ public void SharedFormulaChildKeepsNamespacePrefix()
+ {
+ const string outputName = "WorksheetXmlWriter_SharedFormulaChild.xlsx";
+ using (var p = OpenTemplatePackage(PrefixedWorkbook))
+ {
+ SaveWorkbook(outputName, p);
+ }
+
+ var doc = LoadPartXml(outputName, WorksheetPart);
+ var nsm = CreateNamespaceManager(doc);
+
+ //Guard: the shared formula children must be written, otherwise the test does not exercise the shared formula path.
+ Assert.IsNotNull(doc.SelectSingleNode("//m:c[@r='C4']/m:f[@t='shared']", nsm), "C4 has no shared formula in the main namespace.");
+ Assert.IsNotNull(doc.SelectSingleNode("//m:c[@r='C5']/m:f[@t='shared']", nsm), "C5 has no shared formula in the main namespace.");
+ AssertNoElementsWithoutNamespace(doc);
+ }
+
+ ///
+ /// Issue #2572: Icon set cfvo elements were written without the namespace prefix,
+ /// so Excel reported the workbook as corrupt.
+ ///
+ [TestMethod]
+ public void IconSetCfvoKeepsNamespacePrefix()
+ {
+ const string outputName = "WorksheetXmlWriter_IconSetCfvo.xlsx";
+ using (var p = OpenTemplatePackage(PrefixedWorkbook))
+ {
+ var ws = p.Workbook.Worksheets[0];
+ ws.ConditionalFormatting.AddThreeIconSet(ws.Cells["A2:A5"], eExcelconditionalFormatting3IconsSetType.Arrows);
+ SaveWorkbook(outputName, p);
+ }
+
+ var doc = LoadPartXml(outputName, WorksheetPart);
+
+ //Guard: the icon set must be written, otherwise the test does not exercise the cfvo path.
+ Assert.IsTrue(doc.GetElementsByTagName("iconSet", ExcelPackage.schemaMain).Count > 0, "No iconSet element was written.");
+ AssertNoElementsWithoutNamespace(doc);
+ }
+
+ ///
+ /// Issue #2572: The children of x14:dxf in extLst conditional formatting were written without the main namespace prefix,
+ /// so Excel silently dropped the format of the rule.
+ ///
+ [TestMethod]
+ public void ExtLstDxfChildrenKeepNamespacePrefix()
+ {
+ const string outputName = "WorksheetXmlWriter_ExtLstDxf.xlsx";
+ using (var p = OpenTemplatePackage(PrefixedWorkbook))
+ {
+ var ws = p.Workbook.Worksheets[0];
+ var ws2 = p.Workbook.Worksheets.Add("Sheet2");
+ ws2.Cells["A1"].Value = 3;
+
+ //A reference to another worksheet forces the rule into extLst (x14).
+ var cf = ws.ConditionalFormatting.AddExpression(ws.Cells["A2:A5"]);
+ cf.Formula = "A2>Sheet2!$A$1";
+ cf.Style.Font.Bold = true;
+ cf.Style.Fill.BackgroundColor.Color = System.Drawing.Color.Yellow;
+ SaveWorkbook(outputName, p);
+ }
+
+ var doc = LoadPartXml(outputName, WorksheetPart);
+
+ //Guard: the rule must be written to extLst, otherwise the test does not exercise the dxf path.
+ Assert.IsTrue(doc.GetElementsByTagName("conditionalFormatting", SchemaMainX14).Count > 0, "The rule was not written to extLst.");
+ Assert.IsTrue(doc.GetElementsByTagName("dxf", SchemaMainX14).Count > 0, "No x14:dxf element was written.");
+ AssertNoElementsWithoutNamespace(doc);
+ }
+
+ ///
+ /// Issue #2572: The aca and ca attributes of an array formula leaked to the following formula cells,
+ /// as the attribute string was only reset for cells with flags.
+ ///
+ [TestMethod]
+ public void CalcAttributesDoNotLeakToNextFormulaCell()
+ {
+ const string outputName = "WorksheetXmlWriter_CalcAttributes.xlsx";
+ using (var p = OpenTemplatePackage(CalcFlagsWorkbook))
+ {
+ SaveWorkbook(outputName, p);
+ }
+
+ var doc = LoadPartXml(outputName, WorksheetPart);
+ var nsm = CreateNamespaceManager(doc);
+
+ //Guard: the array formula must keep its calc attributes, otherwise there is nothing that can leak.
+ var arrayFormula = doc.SelectSingleNode("//m:c[@r='D2']/m:f", nsm) as XmlElement;
+ Assert.IsNotNull(arrayFormula, "D2 has no formula element.");
+ Assert.AreEqual("1", arrayFormula.GetAttribute("aca"), "D2 lost its aca attribute on load or save.");
+
+ foreach (var address in new[] { "C3", "C4", "C5" })
+ {
+ var formula = doc.SelectSingleNode($"//m:c[@r='{address}']/m:f", nsm) as XmlElement;
+ Assert.IsNotNull(formula, $"{address} has no formula element.");
+ Assert.IsFalse(formula.HasAttribute("aca"), $"{address} inherited aca from a previous cell.");
+ Assert.IsFalse(formula.HasAttribute("ca"), $"{address} inherited ca from a previous cell.");
+ }
+ }
+
+ private static XmlNamespaceManager CreateNamespaceManager(XmlDocument doc)
+ {
+ var nsm = new XmlNamespaceManager(doc.NameTable);
+ nsm.AddNamespace("m", ExcelPackage.schemaMain);
+ return nsm;
+ }
+
+ private static XmlDocument LoadPartXml(string fileName, string partName)
+ {
+ using (var fileStream = File.OpenRead(_worksheetPath + fileName))
+ using (var zipStream = new OfficeOpenXml.Packaging.Ionic.Zip.ZipInputStream(fileStream))
+ {
+ OfficeOpenXml.Packaging.Ionic.Zip.ZipEntry entry;
+ while ((entry = zipStream.GetNextEntry()) != null)
+ {
+ if (entry.FileName.Equals(partName, StringComparison.OrdinalIgnoreCase))
+ {
+ var doc = new XmlDocument();
+ doc.Load(zipStream);
+ return doc;
+ }
+ }
+ }
+ Assert.Fail($"Part {partName} is missing in {fileName}");
+ return null;
+ }
+
+ private static void AssertNoElementsWithoutNamespace(XmlDocument doc)
+ {
+ var offenders = new List();
+ foreach (XmlElement element in doc.GetElementsByTagName("*"))
+ {
+ if (string.IsNullOrEmpty(element.NamespaceURI))
+ {
+ var xml = element.OuterXml;
+ offenders.Add(xml.Length > 120 ? xml.Substring(0, 120) : xml);
+ }
+ }
+ Assert.AreEqual(0, offenders.Count, "Elements without namespace: " + string.Join(" | ", offenders));
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/EPPlusTest/EPPlus.Test.csproj b/src/EPPlusTest/EPPlus.Test.csproj
index f65652daf8..5d47347490 100644
--- a/src/EPPlusTest/EPPlus.Test.csproj
+++ b/src/EPPlusTest/EPPlus.Test.csproj
@@ -251,6 +251,12 @@
PreserveNewest
+
+
+ PreserveNewest
+
+
+ PreserveNewest
PreserveNewest
diff --git a/src/EPPlusTest/Workbooks/sc1083-flags.xlsx b/src/EPPlusTest/Workbooks/sc1083-flags.xlsx
new file mode 100644
index 0000000000..d430028c69
Binary files /dev/null and b/src/EPPlusTest/Workbooks/sc1083-flags.xlsx differ
diff --git a/src/EPPlusTest/Workbooks/sc1083-sdk.xlsx b/src/EPPlusTest/Workbooks/sc1083-sdk.xlsx
new file mode 100644
index 0000000000..3556173502
Binary files /dev/null and b/src/EPPlusTest/Workbooks/sc1083-sdk.xlsx differ