diff --git a/src/MiniExcel.OpenXml/Templates/OpenXmlTemplate.Impl.cs b/src/MiniExcel.OpenXml/Templates/OpenXmlTemplate.Impl.cs index 09009ffe..9571e9eb 100644 --- a/src/MiniExcel.OpenXml/Templates/OpenXmlTemplate.Impl.cs +++ b/src/MiniExcel.OpenXml/Templates/OpenXmlTemplate.Impl.cs @@ -936,8 +936,17 @@ private void ProcessFormulas(StringBuilder rowXml, int rowIndex) fNode.SetValue(str.Value[2..]); str.AddBeforeSelf(fNode); str.Remove(); - - var celRef = CellReferenceConverter.GetCellFromCoordinates(index, rowIndex); + // the cell no longer holds an inline string; keeping t="inlineStr" without an + // child is schema-invalid and Excel rejects the package + cell.Attribute("t")?.Remove(); + + // take the column from the cell's own reference — the running index is wrong for + // sparse rows (cells without content are not emitted, so position != column) + var cellRef = cell.Attribute("r")?.Value; + var columnLetters = cellRef is null ? "" : new string(cellRef.TakeWhile(char.IsLetter).ToArray()); + var celRef = string.IsNullOrEmpty(columnLetters) + ? CellReferenceConverter.GetCellFromCoordinates(index, rowIndex) + : $"{columnLetters}{rowIndex}"; _calcChainCellRefs.Add(celRef); } } diff --git a/src/MiniExcel.OpenXml/Templates/OpenXmlTemplate.cs b/src/MiniExcel.OpenXml/Templates/OpenXmlTemplate.cs index 60888c6d..09b76f20 100644 --- a/src/MiniExcel.OpenXml/Templates/OpenXmlTemplate.cs +++ b/src/MiniExcel.OpenXml/Templates/OpenXmlTemplate.cs @@ -158,11 +158,16 @@ await originalEntryStream.CopyToAsync(newEntryStream // batch add sheet await BatchAddSheetsToWorkbookAsync(outputFileArchive.ZipFile, originalArchive, allSheetInfos, cancellationToken).ConfigureAwait(false); - // create mode we need to not create first then create here - var calcChain = outputFileArchive.EntryCollection.FirstOrDefault(e + // The template's own calcChain cannot be reused: row insertion shifts formula cells and its + // entries would point at the old addresses. It is regenerated from the rendered formulas — + // and when none were rendered, dropped entirely, because a calcChain with no entries is + // schema-invalid and Excel rejects the whole package. Excel rebuilds the chain on open, so + // dropping it is always safe. (The stale-template copy that used to run when no chain entry + // was created here is intentionally gone for the same reason.) + var calcChain = outputFileArchive.EntryCollection.FirstOrDefault(e => e.FullName.TrimStart('/').Equals(ExcelFileNames.CalcChain, StringComparison.OrdinalIgnoreCase)); - if (calcChain is not null) + if (calcChain is not null && _calcChainContent.Length > 0) { var calcChainEntry = outputFileArchive.ZipFile.CreateEntry(calcChain.FullName); var calcChainStream = await calcChainEntry.OpenAsync(cancellationToken).ConfigureAwait(false); @@ -170,29 +175,6 @@ await originalEntryStream.CopyToAsync(newEntryStream await CalcChainHelper.GenerateCalcChainSheetAsync(calcChainStream, _calcChainContent.ToString(), cancellationToken).ConfigureAwait(false); } - else - { - foreach (var entry in originalArchive.Entries) - { - if (entry.FullName.TrimStart('/').Equals(ExcelFileNames.CalcChain, StringComparison.OrdinalIgnoreCase)) - { - var newEntry = outputFileArchive.ZipFile.CreateEntry(entry.FullName); - - // Copy the content of the original entry to the new entry - var originalEntryStream = await entry.OpenAsync(cancellationToken).ConfigureAwait(false); - await using var disposableEntryStream = originalEntryStream.ConfigureAwait(false); - - var newEntryStream = await newEntry.OpenAsync(cancellationToken).ConfigureAwait(false); - await using var disposableNewEntryStream = newEntryStream.ConfigureAwait(false); - - await originalEntryStream.CopyToAsync(newEntryStream -#if NET - , cancellationToken -#endif - ).ConfigureAwait(false); - } - } - } #if NET10_0_OR_GREATER await outputFileArchive.ZipFile.DisposeAsync().ConfigureAwait(false);