From 03745c7263141973705e691d345ed34a00c3d258 Mon Sep 17 00:00:00 2001 From: Michael Chang Date: Fri, 9 Oct 2026 23:57:30 +0800 Subject: [PATCH 1/4] fix(dotnet): wrap header text box paragraphs within the box width Header and footer paragraphs were drawn as a single unwrapped line, so long text in a header text box ran past the right page edge and was clipped. - DocxReader: record the text area width (extent minus bodyPr insets) and horizontal offset of column/margin-positioned header text boxes. - DocxToPdfConverter: wrap header/footer paragraphs that exceed the available width, using the text box area when present. Text that fits is still drawn verbatim, so existing output is unchanged. --- src/MiniPdf/DocxReader.cs | 20 ++++ src/MiniPdf/DocxToPdfConverter.cs | 35 +++++-- .../MiniPdf.Tests/DocxToPdfConverterTests.cs | 99 +++++++++++++++++++ 3 files changed, 144 insertions(+), 10 deletions(-) diff --git a/src/MiniPdf/DocxReader.cs b/src/MiniPdf/DocxReader.cs index 31d9c9a8..cd2f28ed 100644 --- a/src/MiniPdf/DocxReader.cs +++ b/src/MiniPdf/DocxReader.cs @@ -4030,12 +4030,32 @@ private static List ReadHeaderFooterElementsFromSectPr( } } + // Text area of the box: extent minus bodyPr insets (OOXML default 0.1in left/right). + const float emuPerPt = 12700f; + var bodyPr = anchor.Descendants(WPS + "bodyPr").FirstOrDefault(); + var lIns = (long.TryParse(bodyPr?.Attribute("lIns")?.Value, out var li) ? li : 91440) / emuPerPt; + var rIns = (long.TryParse(bodyPr?.Attribute("rIns")?.Value, out var ri) ? ri : 91440) / emuPerPt; + var boxWidth = long.TryParse(anchor.Element(WP + "extent")?.Attribute("cx")?.Value, out var cx) ? cx / emuPerPt : 0f; + // Only column/margin-relative offsets map onto the margin-based header layout; + // page-relative and aligned boxes keep the inferred-alignment path above. + long offEmu = 0; + var hasMarginOffset = alignment == null + && posH?.Attribute("relativeFrom")?.Value is "column" or "margin" + && long.TryParse(posH.Element(WP + "posOffset")?.Value, out offEmu); + var boxOffset = offEmu / emuPerPt; + foreach (var txbxP in txbxContent.Elements(W + "p")) { var txbxPara = ReadParagraph(txbxP, styles, numbering, hfRels, archive, null, defaultFontName, defaultEastAsiaFontName); if (txbxPara == null) continue; if (alignment != null) txbxPara = txbxPara with { Alignment = alignment }; + if (hasMarginOffset && boxWidth > lIns + rIns) + txbxPara = txbxPara with + { + IndentLeft = boxOffset + lIns + txbxPara.IndentLeft, + TextBoxWidth = boxWidth - lIns - rIns - txbxPara.IndentLeft - txbxPara.IndentRight, + }; elements.Add(txbxPara); } } diff --git a/src/MiniPdf/DocxToPdfConverter.cs b/src/MiniPdf/DocxToPdfConverter.cs index 25f8a22d..0a400895 100644 --- a/src/MiniPdf/DocxToPdfConverter.cs +++ b/src/MiniPdf/DocxToPdfConverter.cs @@ -3692,17 +3692,32 @@ private static void RenderHeaderFooterElementsOnPage(PdfPage page, ConversionOpt para, runFontSize * GetFontMetricsFactor(firstRun?.FontName) * GetLineSpacingMultiple(para, options.LineSpacing)); - var textWidth = EstimateTextWidth(text, runFontSize); - var textX = para.Alignment switch + // Text-box paragraphs carry their own text area; others use the margins. + var areaLeft = options.MarginLeft + (para.TextBoxWidth > 0 ? para.IndentLeft : 0); + var areaWidth = para.TextBoxWidth > 0 ? para.TextBoxWidth : usableW; + var bold = firstRun?.Bold ?? false; + var useCalibri = options.UseCalibriWidths + && (string.IsNullOrEmpty(firstRun?.FontName) || firstRun.FontName.Contains("Calibri", StringComparison.OrdinalIgnoreCase)); + s_overrideWidths = GetFontOverrideWidths(firstRun?.FontName); + // Keep text that fits verbatim; WordWrap normalizes spacing. + var lines = EstimateWrapTextWidth(text, runFontSize, bold, 0, useCalibri) <= areaWidth + ? [text] + : WordWrap(text, areaWidth, areaWidth, runFontSize, para.TabStops, bold, useCalibriWidths: useCalibri); + s_overrideWidths = null; + foreach (var line in lines) { - "center" => options.MarginLeft + (usableW - textWidth) / 2, - "right" => options.MarginLeft + usableW - textWidth, - _ => options.MarginLeft - }; - y -= runFontSize; - page.AddText(text, textX, y, runFontSize, firstRun?.Color ?? para.Color, - bold: firstRun?.Bold ?? false, italic: firstRun?.Italic ?? false, preferredFontName: firstRun?.FontName); - y -= lineHeight - runFontSize; + var textWidth = EstimateTextWidth(line, runFontSize); + var textX = para.Alignment switch + { + "center" => areaLeft + (areaWidth - textWidth) / 2, + "right" => areaLeft + areaWidth - textWidth, + _ => areaLeft + }; + y -= runFontSize; + page.AddText(line, textX, y, runFontSize, firstRun?.Color ?? para.Color, + bold: bold, italic: firstRun?.Italic ?? false, preferredFontName: firstRun?.FontName); + y -= lineHeight - runFontSize; + } } lastSpacingAfter = para.SpacingAfter >= 0 ? para.SpacingAfter : 0f; isFirst = false; diff --git a/tests/MiniPdf.Tests/DocxToPdfConverterTests.cs b/tests/MiniPdf.Tests/DocxToPdfConverterTests.cs index 8c1f92dd..48b57c9e 100644 --- a/tests/MiniPdf.Tests/DocxToPdfConverterTests.cs +++ b/tests/MiniPdf.Tests/DocxToPdfConverterTests.cs @@ -679,6 +679,105 @@ private static uint ComputeCrc32(byte[] data) return crc ^ 0xFFFFFFFF; } + [Fact] + public void Convert_HeaderTextBox_WrapsTextWithinBox() + { + var longText = string.Concat(Enumerable.Repeat("個資蒐集處理及使用聲明", 20)); + using var docxStream = CreateDocxWithHeaderTextBox(longText, offsetEmu: -457200, widthEmu: 5080000); + + var doc = DocxToPdfConverter.Convert(docxStream); + var lines = doc.Pages[0].TextBlocks.Where(b => b.Text != "Body").ToList(); + + // 400pt box minus 7.2pt insets, 72pt margin, -36pt column offset + Assert.True(lines.Count > 1, "header text box paragraph should wrap"); + Assert.Equal(longText, string.Concat(lines.Select(l => l.Text))); + Assert.All(lines, l => Assert.Equal(72f - 36f + 7.2f, l.X, 0.01f)); + Assert.All(lines, l => Assert.True(l.Text.Length * l.FontSize <= 400f - 14.4f + 0.01f)); + } + + private static MemoryStream CreateDocxWithHeaderTextBox(string text, long offsetEmu, long widthEmu) + { + var ms = new MemoryStream(); + + using (var archive = new ZipArchive(ms, ZipArchiveMode.Create, leaveOpen: true)) + { + AddEntry(archive, "[Content_Types].xml", + """ + + + + + + + + """); + + AddEntry(archive, "_rels/.rels", + """ + + + + + """); + + AddEntry(archive, "word/_rels/document.xml.rels", + """ + + + + + """); + + AddEntry(archive, "word/document.xml", + """ + + + + Body + + + + + + + + """); + + AddEntry(archive, "word/header1.xml", + $""" + + + + + + {offsetEmu} + 0 + + + + + + + + {EscapeXml(text)} + + + + + + + + """); + } + + ms.Position = 0; + return ms; + } + private static MemoryStream CreateSimpleDocx(params string[] paragraphs) { var ms = new MemoryStream(); From c3c5394b659ee72f641e2e022656e901ab4af321 Mon Sep 17 00:00:00 2001 From: Michael Chang Date: Sat, 10 Oct 2026 00:34:50 +0800 Subject: [PATCH 2/4] fix(dotnet): reserve wrapped header/footer lines in height estimate EstimateElementsHeight counted one line per header/footer paragraph, so a wrapped footer paragraph was positioned too low and its extra lines were drawn below the page edge. Share the wrap step with the renderer and reserve every wrapped line for in-flow header/footer paragraphs. Floating text-box paragraphs keep the one-line estimate: wrapNone boxes overlay the page and must not push the body down. --- src/MiniPdf/DocxToPdfConverter.cs | 49 ++++++++---- .../MiniPdf.Tests/DocxToPdfConverterTests.cs | 76 +++++++++++++++++++ 2 files changed, 111 insertions(+), 14 deletions(-) diff --git a/src/MiniPdf/DocxToPdfConverter.cs b/src/MiniPdf/DocxToPdfConverter.cs index 0a400895..26fc4a33 100644 --- a/src/MiniPdf/DocxToPdfConverter.cs +++ b/src/MiniPdf/DocxToPdfConverter.cs @@ -255,7 +255,7 @@ internal static PdfDocument Convert(Stream docxStream, ConversionOptions? option // Adjust top margin when header content is taller than the default header area if (!options.MarginTopOverride.HasValue && docxDoc.HeaderElements is { Count: > 0 }) { - var headerContentHeight = EstimateElementsHeight(TrimTrailingEmptyParagraphs(docxDoc.HeaderElements), options); + var headerContentHeight = EstimateElementsHeight(TrimTrailingEmptyParagraphs(docxDoc.HeaderElements), options, headerFooter: true); var headerAreaHeight = options.MarginTop - options.HeaderMargin; if (headerAreaHeight < 0) headerAreaHeight = 0; // header starts below marginTop if (headerContentHeight > headerAreaHeight) @@ -270,7 +270,7 @@ internal static PdfDocument Convert(Stream docxStream, ConversionOptions? option // Adjust bottom margin when footer content is taller than the default footer area if (!options.MarginBottomOverride.HasValue && docxDoc.FooterElements is { Count: > 0 }) { - var footerContentHeight = EstimateElementsHeight(TrimTrailingEmptyParagraphs(docxDoc.FooterElements), options); + var footerContentHeight = EstimateElementsHeight(TrimTrailingEmptyParagraphs(docxDoc.FooterElements), options, headerFooter: true); var footerTopFromBottom = options.FooterMargin + footerContentHeight; if (footerTopFromBottom > options.MarginBottom) options.MarginBottom = footerTopFromBottom; @@ -535,7 +535,7 @@ internal static PdfDocument Convert(Stream docxStream, ConversionOptions? option || (e is DocxParagraph pp && (pp.Runs.Any(r => !string.IsNullOrWhiteSpace(r.Text)) || pp.Images.Count > 0)))) { var trimmedFooter = TrimTrailingEmptyParagraphs(pageFooterElements); - var footerContentHeight = EstimateElementsHeight(trimmedFooter, options); + var footerContentHeight = EstimateElementsHeight(trimmedFooter, options, headerFooter: true); var footerStartY = options.FooterMargin + footerContentHeight; RenderHeaderFooterElementsOnPage(page, options, trimmedFooter, footerStartY, pi, totalPages, sectionPageNum); } @@ -3530,7 +3530,7 @@ private static List TrimTrailingEmptyParagraphs(List e /// Estimates the total height of a list of elements (paragraphs + tables) /// for header/footer sizing calculations. /// - private static float EstimateElementsHeight(List elements, ConversionOptions options) + private static float EstimateElementsHeight(List elements, ConversionOptions options, bool headerFooter = false) { const float emuPerPt = 914400f / 72f; float totalHeight = 0; @@ -3566,7 +3566,16 @@ private static float EstimateElementsHeight(List elements, Conversi } var text = string.Concat(para.Runs.Select(r => r.Text)); if (!string.IsNullOrEmpty(text)) - totalHeight += lineHeight; + { + // Header/footer paragraphs wrap at render time; reserve every line. + // Floating text-box paragraphs (TextBoxWidth > 0) overlay the page and + // must not grow the reserved area, so they keep the one-line estimate. + // Page numbers are unknown here, so placeholders resolve as page 1 of 1. + var lineCount = headerFooter && para.TextBoxWidth <= 0 + ? WrapHeaderFooterText(para, ResolvePagePlaceholders(text, 1, 1), options, usableW).Count + : 1; + totalHeight += lineHeight * lineCount; + } else if (para.Images.Count == 0) totalHeight += lineHeight; @@ -3605,6 +3614,26 @@ private static float EstimateElementsHeight(List elements, Conversi return totalHeight; } + /// + /// Splits a header/footer paragraph into the lines drawn by + /// . Text that fits is kept verbatim + /// because WordWrap normalizes spacing. + /// + private static List WrapHeaderFooterText(DocxParagraph para, string text, ConversionOptions options, float areaWidth) + { + var firstRun = para.Runs.FirstOrDefault(r => !string.IsNullOrEmpty(r.Text)); + var runFontSize = firstRun?.FontSize > 0 ? firstRun.FontSize : (para.FontSize > 0 ? para.FontSize : options.FontSize); + var bold = firstRun?.Bold ?? false; + var useCalibri = options.UseCalibriWidths + && (string.IsNullOrEmpty(firstRun?.FontName) || firstRun.FontName.Contains("Calibri", StringComparison.OrdinalIgnoreCase)); + s_overrideWidths = GetFontOverrideWidths(firstRun?.FontName); + var lines = EstimateWrapTextWidth(text, runFontSize, bold, 0, useCalibri) <= areaWidth + ? [text] + : WordWrap(text, areaWidth, areaWidth, runFontSize, para.TabStops, bold, useCalibriWidths: useCalibri); + s_overrideWidths = null; + return lines; + } + /// /// Renders header/footer elements (paragraphs + tables) directly on a page /// at the specified starting Y position, flowing downward. @@ -3696,15 +3725,7 @@ private static void RenderHeaderFooterElementsOnPage(PdfPage page, ConversionOpt var areaLeft = options.MarginLeft + (para.TextBoxWidth > 0 ? para.IndentLeft : 0); var areaWidth = para.TextBoxWidth > 0 ? para.TextBoxWidth : usableW; var bold = firstRun?.Bold ?? false; - var useCalibri = options.UseCalibriWidths - && (string.IsNullOrEmpty(firstRun?.FontName) || firstRun.FontName.Contains("Calibri", StringComparison.OrdinalIgnoreCase)); - s_overrideWidths = GetFontOverrideWidths(firstRun?.FontName); - // Keep text that fits verbatim; WordWrap normalizes spacing. - var lines = EstimateWrapTextWidth(text, runFontSize, bold, 0, useCalibri) <= areaWidth - ? [text] - : WordWrap(text, areaWidth, areaWidth, runFontSize, para.TabStops, bold, useCalibriWidths: useCalibri); - s_overrideWidths = null; - foreach (var line in lines) + foreach (var line in WrapHeaderFooterText(para, text, options, areaWidth)) { var textWidth = EstimateTextWidth(line, runFontSize); var textX = para.Alignment switch diff --git a/tests/MiniPdf.Tests/DocxToPdfConverterTests.cs b/tests/MiniPdf.Tests/DocxToPdfConverterTests.cs index 48b57c9e..2b0d03fe 100644 --- a/tests/MiniPdf.Tests/DocxToPdfConverterTests.cs +++ b/tests/MiniPdf.Tests/DocxToPdfConverterTests.cs @@ -695,6 +695,82 @@ public void Convert_HeaderTextBox_WrapsTextWithinBox() Assert.All(lines, l => Assert.True(l.Text.Length * l.FontSize <= 400f - 14.4f + 0.01f)); } + [Fact] + public void Convert_WrappedFooterParagraph_StaysAboveFooterMargin() + { + var longText = string.Join(" ", Enumerable.Repeat("Footer notice text", 30)); + using var docxStream = CreateDocxWithFooter(longText, footerTwips: 360); + + var doc = DocxToPdfConverter.Convert(docxStream); + var lines = doc.Pages[0].TextBlocks.Where(b => b.Text != "Body").ToList(); + + // Every wrapped line must fit in the reserved footer area, above the 18pt footer margin. + Assert.True(lines.Count > 1, "footer paragraph should wrap"); + Assert.All(lines, l => Assert.True(l.Y >= 18f, $"footer line at y={l.Y} is below the footer margin")); + } + + private static MemoryStream CreateDocxWithFooter(string text, int footerTwips) + { + var ms = new MemoryStream(); + + using (var archive = new ZipArchive(ms, ZipArchiveMode.Create, leaveOpen: true)) + { + AddEntry(archive, "[Content_Types].xml", + """ + + + + + + + + """); + + AddEntry(archive, "_rels/.rels", + """ + + + + + """); + + AddEntry(archive, "word/_rels/document.xml.rels", + """ + + + + + """); + + AddEntry(archive, "word/document.xml", + $""" + + + + Body + + + + + + + + """); + + AddEntry(archive, "word/footer1.xml", + $""" + + + {EscapeXml(text)} + + """); + } + + ms.Position = 0; + return ms; + } + private static MemoryStream CreateDocxWithHeaderTextBox(string text, long offsetEmu, long widthEmu) { var ms = new MemoryStream(); From 32359cd2df6f586c591d6ecfd0825e65abd96f04 Mon Sep 17 00:00:00 2001 From: Wei Lin Date: Sat, 10 Oct 2026 18:12:03 +0800 Subject: [PATCH 3/4] fix(dotnet): bound unbreakable header text box lines Split oversized header/footer lines into text elements and constrain text box rendering to its usable width. Cover an unbreakable header token with a regression test. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- src/MiniPdf/DocxToPdfConverter.cs | 29 ++++++++++++++++++- .../MiniPdf.Tests/DocxToPdfConverterTests.cs | 15 ++++++++++ 2 files changed, 43 insertions(+), 1 deletion(-) diff --git a/src/MiniPdf/DocxToPdfConverter.cs b/src/MiniPdf/DocxToPdfConverter.cs index 26fc4a33..4b695f61 100644 --- a/src/MiniPdf/DocxToPdfConverter.cs +++ b/src/MiniPdf/DocxToPdfConverter.cs @@ -3630,8 +3630,34 @@ private static List WrapHeaderFooterText(DocxParagraph para, string text var lines = EstimateWrapTextWidth(text, runFontSize, bold, 0, useCalibri) <= areaWidth ? [text] : WordWrap(text, areaWidth, areaWidth, runFontSize, para.TabStops, bold, useCalibriWidths: useCalibri); + var result = new List(); + foreach (var line in lines) + { + if (EstimateWrapTextWidth(line, runFontSize, bold, 0, useCalibri) <= areaWidth) + { + result.Add(line); + continue; + } + + var segment = ""; + var elements = System.Globalization.StringInfo.GetTextElementEnumerator(line); + while (elements.MoveNext()) + { + var next = elements.GetTextElement(); + if (segment.Length > 0 && Math.Max( + EstimateWrapTextWidth(segment + next, runFontSize, bold, 0, useCalibri), + EstimateTextWidth(segment + next, runFontSize)) > areaWidth) + { + result.Add(segment); + segment = ""; + } + segment += next; + } + if (segment.Length > 0) + result.Add(segment); + } s_overrideWidths = null; - return lines; + return result; } /// @@ -3736,6 +3762,7 @@ private static void RenderHeaderFooterElementsOnPage(PdfPage page, ConversionOpt }; y -= runFontSize; page.AddText(line, textX, y, runFontSize, firstRun?.Color ?? para.Color, + maxWidth: para.TextBoxWidth > 0 ? areaWidth : null, bold: bold, italic: firstRun?.Italic ?? false, preferredFontName: firstRun?.FontName); y -= lineHeight - runFontSize; } diff --git a/tests/MiniPdf.Tests/DocxToPdfConverterTests.cs b/tests/MiniPdf.Tests/DocxToPdfConverterTests.cs index 2b0d03fe..e25fa188 100644 --- a/tests/MiniPdf.Tests/DocxToPdfConverterTests.cs +++ b/tests/MiniPdf.Tests/DocxToPdfConverterTests.cs @@ -695,6 +695,21 @@ public void Convert_HeaderTextBox_WrapsTextWithinBox() Assert.All(lines, l => Assert.True(l.Text.Length * l.FontSize <= 400f - 14.4f + 0.01f)); } + [Fact] + public void Convert_HeaderTextBox_SplitsOversizedUnbreakableWord() + { + var longText = new string('W', 180); + using var docxStream = CreateDocxWithHeaderTextBox(longText, offsetEmu: -457200, widthEmu: 5080000); + + var doc = DocxToPdfConverter.Convert(docxStream); + var lines = doc.Pages[0].TextBlocks.Where(b => b.Text != "Body").ToList(); + + Assert.True(lines.Count > 1, "oversized word should wrap"); + Assert.Equal(longText, string.Concat(lines.Select(l => l.Text))); + Assert.All(lines, l => Assert.True(l.Text.Length * l.FontSize * 0.9f <= 385.6f)); + Assert.All(lines, l => Assert.True(l.MaxWidth.HasValue && Math.Abs(l.MaxWidth.Value - 385.6f) < 0.01f)); + } + [Fact] public void Convert_WrappedFooterParagraph_StaysAboveFooterMargin() { From e544b4a80d628947a3494df18fb7d2d9a293ac3e Mon Sep 17 00:00:00 2001 From: Wei Lin Date: Sat, 10 Oct 2026 18:32:02 +0800 Subject: [PATCH 4/4] fix(dotnet): estimate footer height with rendered run metrics Match the first text run's font size and font in header/footer height estimates. Cover oversized wrapped footers with a regression test. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- src/MiniPdf/DocxToPdfConverter.cs | 7 +++++-- tests/MiniPdf.Tests/DocxToPdfConverterTests.cs | 18 ++++++++++++++++-- 2 files changed, 21 insertions(+), 4 deletions(-) diff --git a/src/MiniPdf/DocxToPdfConverter.cs b/src/MiniPdf/DocxToPdfConverter.cs index 4b695f61..d1c7d402 100644 --- a/src/MiniPdf/DocxToPdfConverter.cs +++ b/src/MiniPdf/DocxToPdfConverter.cs @@ -3550,8 +3550,11 @@ private static float EstimateElementsHeight(List elements, Conversi totalHeight += gap; } - var fontSize = para.FontSize > 0 ? para.FontSize : options.FontSize; - var estFont = para.Runs.FirstOrDefault(r => !string.IsNullOrEmpty(r.FontName))?.FontName; + var firstRun = headerFooter ? para.Runs.FirstOrDefault(r => !string.IsNullOrEmpty(r.Text)) : null; + var fontSize = firstRun?.FontSize > 0 ? firstRun.FontSize + : para.FontSize > 0 ? para.FontSize : options.FontSize; + var estFont = headerFooter ? firstRun?.FontName + : para.Runs.FirstOrDefault(r => !string.IsNullOrEmpty(r.FontName))?.FontName; var lineHeight = ApplyLineSpacingRule( para, fontSize * GetFontMetricsFactor(estFont) * GetLineSpacingMultiple(para, options.LineSpacing)); diff --git a/tests/MiniPdf.Tests/DocxToPdfConverterTests.cs b/tests/MiniPdf.Tests/DocxToPdfConverterTests.cs index e25fa188..8fbadb97 100644 --- a/tests/MiniPdf.Tests/DocxToPdfConverterTests.cs +++ b/tests/MiniPdf.Tests/DocxToPdfConverterTests.cs @@ -724,9 +724,23 @@ public void Convert_WrappedFooterParagraph_StaysAboveFooterMargin() Assert.All(lines, l => Assert.True(l.Y >= 18f, $"footer line at y={l.Y} is below the footer margin")); } - private static MemoryStream CreateDocxWithFooter(string text, int footerTwips) + [Fact] + public void Convert_WrappedFooterWithRunFontSize_StaysAboveFooterMargin() + { + var longText = string.Join(" ", Enumerable.Repeat("Footer notice text", 30)); + using var docxStream = CreateDocxWithFooter(longText, footerTwips: 360, runHalfPoints: 36); + + var doc = DocxToPdfConverter.Convert(docxStream); + var lines = doc.Pages[0].TextBlocks.Where(b => b.Text != "Body").ToList(); + + Assert.True(lines.Count > 1, "footer paragraph should wrap"); + Assert.All(lines, l => Assert.True(l.Y >= 18f, $"footer line at y={l.Y} is below the footer margin")); + } + + private static MemoryStream CreateDocxWithFooter(string text, int footerTwips, int? runHalfPoints = null) { var ms = new MemoryStream(); + var runProperties = runHalfPoints.HasValue ? $"" : ""; using (var archive = new ZipArchive(ms, ZipArchiveMode.Create, leaveOpen: true)) { @@ -777,7 +791,7 @@ private static MemoryStream CreateDocxWithFooter(string text, int footerTwips) $""" - {EscapeXml(text)} + {runProperties}{EscapeXml(text)} """); }