diff --git a/src/MiniPdf/DocxReader.cs b/src/MiniPdf/DocxReader.cs
index f4469d3e..31d9c9a8 100644
--- a/src/MiniPdf/DocxReader.cs
+++ b/src/MiniPdf/DocxReader.cs
@@ -255,6 +255,11 @@ internal static DocxDocument Read(Stream stream)
}
}
+ ///
+ /// Reads the opened DOCX package (relationships, styles, numbering, theme
+ /// colours, footnotes, settings and the body elements) into a
+ /// .
+ ///
private static DocxDocument ReadCore(ZipArchive archive)
{
// Read relationships to resolve image references
@@ -478,7 +483,22 @@ private static DocxDocument ReadCore(ZipArchive archive)
leftInsetPt = lIns / 914400f * 72f;
}
- if (isWrapNone)
+ // A wrapTopAndBottom text box whose vertical anchor is relative to the
+ // page or margin sits at an absolute page position (LibreOffice maps
+ // relativeFrom="page" to RelOrientation::PAGE_FRAME in
+ // writerfilter/dmapper/GraphicHelpers.cxx and wrapTopAndBottom to
+ // WrapTextMode_NONE in writerfilter/dmapper/GraphicImport.cxx). Its
+ // posOffset is a page coordinate, not a gap before the box, so emitting
+ // the content as flow paragraphs would insert that offset as bogus
+ // spacing. Render it as a floating box instead; the converter resumes
+ // the text flow below the box (see RenderFloatingTextBoxes).
+ // Every ST_RelFromV value other than paragraph and line (page, margin,
+ // topMargin, bottomMargin, insideMargin, outsideMargin) is a page-based
+ // reference; the floating renderer already positions those.
+ bool isAbsoluteWrapTopBottom = isWrapTopBottom
+ && vRelativeFrom != "paragraph" && vRelativeFrom != "line";
+
+ if (isWrapNone || isAbsoluteWrapTopBottom)
{
// wrapNone text boxes are positioned absolutely and do not
// consume space in the main flow per the OOXML spec.
@@ -522,7 +542,7 @@ private static DocxDocument ReadCore(ZipArchive archive)
if (floatingParas.Count > 0)
{
floatingTextBoxes ??= new List();
- floatingTextBoxes.Add(new DocxFloatingTextBox(anchorXPt, anchorOffsetPt, extentWidthPt, extentHeightPt, floatingParas, textBoxBorder, hRelativeFrom, vRelativeFrom, textBoxFillColor, topInsetPt, leftInsetPt, hAlign, vAlign));
+ floatingTextBoxes.Add(new DocxFloatingTextBox(anchorXPt, anchorOffsetPt, extentWidthPt, extentHeightPt, floatingParas, textBoxBorder, hRelativeFrom, vRelativeFrom, textBoxFillColor, topInsetPt, leftInsetPt, hAlign, vAlign, IsWrapTopBottom: isWrapTopBottom));
}
}
else
@@ -5191,7 +5211,10 @@ internal sealed record DocxConnectorLine(
string VRelativeFrom = "paragraph"
);
-/// Represents a floating text box (wrapNone) with absolute position.
+///
+/// Represents a floating text box with an absolute position: a wrapNone box, or a
+/// wrapTopAndBottom box whose vertical anchor is relative to the page or margin.
+///
internal sealed record DocxFloatingTextBox(
float XPt,
float YPt,
@@ -5206,7 +5229,10 @@ internal sealed record DocxFloatingTextBox(
float LeftInsetPt = 7.2f,
string? HAlign = null,
string? VAlign = null,
- bool AnchorAtParagraphTop = false
+ bool AnchorAtParagraphTop = false,
+ // True for a wrapTopAndBottom anchor rendered as a floating box: text may not
+ // flow beside it, so the converter resumes the flow below the box bottom.
+ bool IsWrapTopBottom = false
);
/// Represents a text box outline border (rectangle drawn around text box content).
diff --git a/src/MiniPdf/DocxToPdfConverter.cs b/src/MiniPdf/DocxToPdfConverter.cs
index bbb51e9f..b2bd0551 100644
--- a/src/MiniPdf/DocxToPdfConverter.cs
+++ b/src/MiniPdf/DocxToPdfConverter.cs
@@ -637,7 +637,60 @@ private sealed class RenderState
/// extra page breaks in long multi-section documents like CCU_article).
///
public bool HasRenderedAnyContent { get; set; }
- public float LastParagraphStartY { get; set; }
+ private float _lastParagraphStartY;
+ ///
+ /// Start position of the most recent paragraph. Setting it also records
+ /// , the page that paragraph started on, so
+ /// floating boxes anchored to a paragraph that ends with a page break are
+ /// still placed on the page containing the anchor.
+ ///
+ public float LastParagraphStartY
+ {
+ get => _lastParagraphStartY;
+ set { _lastParagraphStartY = value; LastParagraphPage = CurrentPage; }
+ }
+ public PdfPage? LastParagraphPage { get; private set; }
+ ///
+ /// Vertical bands (PDF coordinates, top above bottom) that text may not enter
+ /// on a page: wrapTopAndBottom boxes. Text resumes below them.
+ ///
+ public List<(PdfPage Page, float TopY, float BottomY)>? WrapObstacles { get; set; }
+
+ ///
+ /// Moves (the next baseline) below every obstacle band on
+ /// the current page that a line with the given ascent and descent would intersect.
+ ///
+ public bool AvoidWrapObstacles(float ascent, float descent)
+ {
+ if (WrapObstacles == null || CurrentPage == null) return false;
+
+ var originalPage = CurrentPage;
+ var originalColumn = CurrentColumn;
+ while (true)
+ {
+ var moved = false;
+ foreach (var (page, topY, bottomY) in WrapObstacles)
+ {
+ if (page != CurrentPage) continue;
+ if (CurrentY + ascent > bottomY && CurrentY - descent < topY)
+ {
+ CurrentY = bottomY - ascent;
+ IsTopOfPage = false;
+ moved = true;
+ }
+ }
+
+ if (!moved) break;
+ if (CurrentY - descent >= Options.MarginBottom) continue;
+
+ if (ColumnCount <= 1 || !AdvanceToNextColumn())
+ ForceNewPage();
+ EnsurePage();
+ AdvanceY(ascent);
+ }
+
+ return CurrentPage != originalPage || CurrentColumn != originalColumn;
+ }
///
/// Captures the paragraph's line-box TOP (before ascent advance) at the
/// start of each paragraph. Used as the anchor base for floating images
@@ -1158,6 +1211,10 @@ private static void RenderParagraph(RenderState state, DocxParagraph paragraph,
: fontSize * GetTopOfPageAscentRatio(paraFontName, ResolveLineSpacingMul(paragraph, options));
state.AdvanceY(emptyAscentOffset);
}
+ var emptyLineAscent = options.GridLinePitch > 0 && paragraph.SnapToGrid
+ ? GetGridAscentOffset(lineHeight, fontSize, paraFontName)
+ : fontSize * GetTopOfPageAscentRatio(paraFontName, ResolveLineSpacingMul(paragraph, options));
+ state.AvoidWrapObstacles(emptyLineAscent, Math.Max(0f, totalEmptyAdvance - emptyLineAscent));
RenderParagraphBorders(state, paragraph, state.CurrentY, state.CurrentY, isEmptyParagraph: true);
state.AdvanceY(totalEmptyAdvance);
// If the empty paragraph pushed past the bottom margin, accumulate
@@ -1360,6 +1417,19 @@ private static void RenderParagraph(RenderState state, DocxParagraph paragraph,
state.AdvanceY((lineHeight - state.LastLineHeight) * 0.541f);
}
+ // Keep the first line out of any wrapTopAndBottom band on this page. Move the
+ // captured paragraph top by the same amount so paragraph-relative anchors
+ // stay attached to the line that actually starts the paragraph.
+ var firstLineAscent = currentGridAscent > 0
+ ? currentGridAscent
+ : fontSize * GetTopOfPageAscentRatio(paraFontName, ResolveLineSpacingMul(paragraph, options));
+ var yBeforeObstacles = state.CurrentY;
+ var changedFlowContext = state.AvoidWrapObstacles(
+ firstLineAscent, Math.Max(0f, lineHeight - firstLineAscent));
+ state.CurrentParagraphTopY = changedFlowContext
+ ? state.CurrentY + firstLineAscent
+ : state.CurrentParagraphTopY - (yBeforeObstacles - state.CurrentY);
+
// Track paragraph start position for borders and floating textboxes
var paragraphStartY = state.CurrentY;
state.LastParagraphStartY = paragraphStartY;
@@ -1600,10 +1670,13 @@ private static void RenderParagraph(RenderState state, DocxParagraph paragraph,
// Paragraphs whose only visual content is wrapNone floating textboxes and/or
// connector lines (anchored shapes that are absolutely positioned overlays)
// should not consume a line height in the main flow, matching Word's behaviour.
+ // A wrapTopAndBottom box is not an overlay: its empty host paragraph still
+ // occupies its own line above the box.
var isFloatingAnchorOnlyParagraph =
paragraph.Runs.Count == 0
&& paragraph.Images.Count == 0
&& (paragraph.Shapes is null || paragraph.Shapes.Count == 0)
+ && paragraph.FloatingTextBoxes?.Any(box => box.IsWrapTopBottom) != true
&& ((paragraph.FloatingTextBoxes is { Count: > 0 })
|| (paragraph.ConnectorLines is { Count: > 0 }));
// Paragraphs whose only visual content is wrapNone anchor images
@@ -1827,13 +1900,13 @@ private static void RenderParagraph(RenderState state, DocxParagraph paragraph,
state.ForceNewPage();
}
state.EnsurePage();
+ var lineAscentOffset = options.GridLinePitch > 0 && paragraph.SnapToGrid
+ ? GetGridAscentOffset(lineHeight, runFontSize, runFontName)
+ : runFontSize * GetTopOfPageAscentRatio(runFontName, ResolveLineSpacingMul(paragraph, options));
if (state.IsTopOfPage)
- {
- var lineAscentOffset = options.GridLinePitch > 0 && paragraph.SnapToGrid
- ? GetGridAscentOffset(lineHeight, runFontSize, runFontName)
- : runFontSize * GetTopOfPageAscentRatio(runFontName, ResolveLineSpacingMul(paragraph, options));
state.AdvanceY(lineAscentOffset);
- }
+ // Wrapped lines must also stay out of wrapTopAndBottom bands.
+ state.AvoidWrapObstacles(lineAscentOffset, Math.Max(0f, lineHeight - lineAscentOffset));
var line = lines[i];
var lineX = i == 0 ? firstLineX : x;
@@ -1997,6 +2070,11 @@ private static void RenderParagraph(RenderState state, DocxParagraph paragraph,
}
+ ///
+ /// Draws the paragraph's w:pBdr borders around the box that spans
+ /// to on the
+ /// current page. Empty paragraphs drop the top and bottom border spacing.
+ ///
private static void RenderParagraphBorders(RenderState state, DocxParagraph paragraph, float paragraphTop, float paragraphBottom,
bool isEmptyParagraph = false)
{
@@ -2024,13 +2102,17 @@ private static void RenderParagraphBorders(RenderState state, DocxParagraph para
}
///
- /// Renders floating text boxes (wrapNone) at their absolute page positions.
- /// These text boxes do not affect the normal document flow.
+ /// Renders floating text boxes at their absolute positions on the page the host
+ /// paragraph started on. wrapNone boxes do not affect the normal document flow;
+ /// page- or margin-anchored wrapTopAndBottom boxes register their band as a
+ /// entry so the flow resumes below the box.
///
private static void RenderFloatingTextBoxes(RenderState state, List boxes,
DocxParagraph hostParagraph, float paragraphY)
{
- var page = state.CurrentPage;
+ // A host paragraph that ends with a page break has already moved the flow to
+ // the next page; anchor the boxes to the page the paragraph started on.
+ var page = state.LastParagraphPage ?? state.CurrentPage;
if (page == null) return;
var options = state.Options;
var hostPageIdx = -1;
@@ -2116,6 +2198,24 @@ private static void RenderFloatingTextBoxes(RenderState state, List();
+ state.WrapObstacles.Add((targetPage, boxTop, boxTop - box.HeightPt));
+ var nextLineHeight = state.LastLineHeight > 0
+ ? state.LastLineHeight
+ : hostFontSize * GetFontMetricsFactor(hostFontName);
+ var nextAscent = options.GridLinePitch > 0 && hostParagraph.SnapToGrid
+ ? GetGridAscentOffset(nextLineHeight, hostFontSize, hostFontName)
+ : hostFontSize * GetTopOfPageAscentRatio(hostFontName, ResolveLineSpacingMul(hostParagraph, options));
+ state.AvoidWrapObstacles(nextAscent, Math.Max(0f, nextLineHeight - nextAscent));
+ }
+
// Render fill background if present
if (box.FillColor is { } fill)
{
@@ -2235,7 +2335,7 @@ private static void RenderFloatingTextBoxes(RenderState state, List 0 && paragraph.SnapToGrid
+ ? (lineHeight + fontSize) / 2f
+ : fontSize * GetTopOfPageAscentRatio(fontName, ResolveLineSpacingMul(paragraph, state.Options));
+ if (state.IsTopOfPage)
+ state.AdvanceY(ascent);
+ state.AvoidWrapObstacles(ascent, Math.Max(0f, lineHeight - ascent));
+ }
+
// For center/right alignment, pre-calculate total line width of all runs
// and offset the starting X position.
// Use Helvetica widths (useCalibri=false) because the multi-format path
@@ -2771,18 +2887,7 @@ static float WrapEntryWidth((string Text, float X, float Y, float FontSize, PdfC
// The line ending at this is the last visual line
// of the preceding segment, so it must not be justified.
FlushLineEntries(isLastLine: true);
- if (!state.IsTopOfPage && state.CurrentY - runFs * (GetFontMetricsFactor(run.FontName) - 1f) < state.Options.MarginBottom)
- state.ForceNewPage();
- else
- state.AdvanceY(lineHeight);
- state.EnsurePage();
- if (state.IsTopOfPage)
- {
- var hardBrAscentOffset = state.Options.GridLinePitch > 0 && paragraph.SnapToGrid
- ? (lineHeight + runFs) / 2f
- : runFs * GetTopOfPageAscentRatio(run.FontName, ResolveLineSpacingMul(paragraph, state.Options));
- state.AdvanceY(hardBrAscentOffset);
- }
+ AdvanceToContinuationLine(runFs, run.FontName);
currentX = baseX;
isFirstLine = false;
}
@@ -3037,18 +3142,7 @@ static float WrapEntryWidth((string Text, float X, float Y, float FontSize, PdfC
}
FlushLineEntries();
// Wrap to next line
- if (!state.IsTopOfPage && state.CurrentY - runFs * (GetFontMetricsFactor(run.FontName) - 1f) < state.Options.MarginBottom)
- state.ForceNewPage();
- else
- state.AdvanceY(lineHeight);
- state.EnsurePage();
- if (state.IsTopOfPage)
- {
- var wrapAscentOffset = state.Options.GridLinePitch > 0 && paragraph.SnapToGrid
- ? (lineHeight + runFs) / 2f
- : runFs * GetTopOfPageAscentRatio(run.FontName, ResolveLineSpacingMul(paragraph, state.Options));
- state.AdvanceY(wrapAscentOffset);
- }
+ AdvanceToContinuationLine(runFs, run.FontName);
currentX = baseX;
pendingX = currentX;
isFirstLine = false;
@@ -3109,18 +3203,7 @@ static float WrapEntryWidth((string Text, float X, float Y, float FontSize, PdfC
BufferOrEmit(pendingText[..breakAt], pendingX, state.CurrentY + run.VerticalPosition, runFs, runColor, run.Bold, run.Italic, run.Underline, run.CharSpacing, run.FontName, cjkBrkMaxW > 0 ? cjkBrkMaxW : (float?)null, null, run.Shading);
FlushLineEntries();
pendingText = pendingText[breakAt..];
- if (!state.IsTopOfPage && state.CurrentY - runFs * (GetFontMetricsFactor(run.FontName) - 1f) < state.Options.MarginBottom)
- state.ForceNewPage();
- else
- state.AdvanceY(lineHeight);
- state.EnsurePage();
- if (state.IsTopOfPage)
- {
- var cjkBrkAscentOffset = state.Options.GridLinePitch > 0 && paragraph.SnapToGrid
- ? (lineHeight + runFs) / 2f
- : runFs * GetTopOfPageAscentRatio(run.FontName, ResolveLineSpacingMul(paragraph, state.Options));
- state.AdvanceY(cjkBrkAscentOffset);
- }
+ AdvanceToContinuationLine(runFs, run.FontName);
currentX = baseX + EstimateWrapTextWidth(pendingText, runFs, run.Bold, run.CharSpacing, useCalibri) * nonCalibriWidthFactor;
pendingX = baseX;
isFirstLine = false;
diff --git a/tests/MiniPdf.Tests/DocxDrawingTests.cs b/tests/MiniPdf.Tests/DocxDrawingTests.cs
index a62bf1b9..c40ed74c 100644
--- a/tests/MiniPdf.Tests/DocxDrawingTests.cs
+++ b/tests/MiniPdf.Tests/DocxDrawingTests.cs
@@ -28,6 +28,237 @@ public void Read_MultipleDrawingsInTextBoxHostRun_ReadsEachTopLevelDrawing()
Assert.Contains(shapes, shape => shape.FillColor.B > 0.99f && shape.FillColor.R < 0.01f);
}
+ ///
+ /// A wrapTopAndBottom text box anchored to the page or to the margin must be exposed as a
+ /// floating box on its host paragraph. Its offset is a page coordinate, not spacing, so
+ /// neither the box content nor the host paragraph may be pushed down the flow by it.
+ ///
+ [Theory]
+ [InlineData("page", 1333500, 105f)]
+ [InlineData("margin", 419100, 33f)]
+ public void Read_AbsoluteWrapTopAndBottomTextBox_IsFloatingBox(string relativeFrom, int posOffsetEmu, float expectedYPt)
+ {
+ using var stream = CreateDocxWithAbsoluteWrapTopAndBottomTextBox(relativeFrom, posOffsetEmu);
+
+ var document = DocxReader.Read(stream);
+ var paragraphs = document.Elements.OfType().ToArray();
+
+ Assert.DoesNotContain(paragraphs, paragraph => paragraph.Runs.Any(run => run.Text == "Boxed"));
+ var host = Assert.Single(paragraphs, paragraph => paragraph.Runs.Any(run => run.Text == "Host"));
+ var box = Assert.Single(host.FloatingTextBoxes ?? []);
+ Assert.True(box.IsWrapTopBottom);
+ Assert.Equal(relativeFrom, box.VRelativeFrom);
+ Assert.InRange(box.YPt, expectedYPt - 0.1f, expectedYPt + 0.1f);
+ Assert.InRange(host.SpacingBefore, -0.01f, 0.01f);
+ }
+
+ ///
+ /// The text flow may not run beside a wrapTopAndBottom box: the host line stays above the
+ /// box, the box text renders inside the box band, and the next paragraph resumes below it.
+ /// Both anchors put the box 105pt below the page top (the margin case is 33pt below the
+ /// 72pt top margin).
+ ///
+ [Theory]
+ [InlineData("page", 1333500)]
+ [InlineData("margin", 419100)]
+ public void Convert_AbsoluteWrapTopAndBottomTextBox_ResumesFlowBelowBox(string relativeFrom, int posOffsetEmu)
+ {
+ using var stream = CreateDocxWithAbsoluteWrapTopAndBottomTextBox(relativeFrom, posOffsetEmu);
+
+ var document = DocxToPdfConverter.Convert(stream);
+
+ var page = Assert.Single(document.Pages);
+ var host = Assert.Single(page.TextBlocks, block => block.Text == "Host");
+ var boxed = Assert.Single(page.TextBlocks, block => block.Text == "Boxed");
+ var after = Assert.Single(page.TextBlocks, block => block.Text == "After");
+
+ // The box occupies 105pt to 145pt from the top of the 792pt page.
+ const float bandTop = 792f - 105f;
+ const float bandBottom = 792f - 145f;
+ Assert.InRange(boxed.Y, bandBottom, bandTop);
+ Assert.True(host.Y > bandTop, $"Host baseline {host.Y} must stay above the box top {bandTop}.");
+ Assert.True(after.Y < bandBottom, $"Following baseline {after.Y} must resume below the box bottom {bandBottom}.");
+ }
+
+ [Fact]
+ public void Convert_AbsoluteWrapTopAndBottomTextBox_MixedFormatContinuationLinesAvoidBox()
+ {
+ var continuationText = string.Join(" ", Enumerable.Repeat("continuation", 40));
+ var afterParagraph = $"""
+
+ After
+ {continuationText}
+
+ """;
+ using var stream = CreateDocxWithAbsoluteWrapTopAndBottomTextBox(
+ "page", 1841500, afterParagraph);
+
+ var document = DocxToPdfConverter.Convert(stream);
+
+ var page = Assert.Single(document.Pages);
+ var bodyBlocks = page.TextBlocks.Where(block => block.Text != "Boxed").ToArray();
+ const float bandTop = 792f - 145f;
+ const float bandBottom = 792f - 185f;
+ Assert.Contains(bodyBlocks, block => block.Text.Contains("continuation") && block.Y < bandBottom);
+ Assert.DoesNotContain(bodyBlocks, block => block.Y < bandTop && block.Y > bandBottom);
+ }
+
+ [Fact]
+ public void Convert_OverlappingWrapTopAndBottomTextBoxes_RescansEarlierObstacles()
+ {
+ var afterParagraphs = string.Join("", Enumerable.Range(1, 12)
+ .Select(index => $"After {index}"));
+ using var stream = CreateDocxWithAbsoluteWrapTopAndBottomTextBox(
+ "page", 2222500, afterParagraphs, secondPosOffsetEmu: 1841500);
+
+ var document = DocxToPdfConverter.Convert(stream);
+
+ var page = Assert.Single(document.Pages);
+ var bodyBlocks = page.TextBlocks
+ .Where(block => !block.Text.StartsWith("Boxed", StringComparison.Ordinal))
+ .ToArray();
+ const float upperBandTop = 792f - 145f;
+ const float lowerBandBottom = 792f - 215f;
+ Assert.Contains(bodyBlocks,
+ block => block.Text.StartsWith("After", StringComparison.Ordinal) && block.Y < lowerBandBottom);
+ Assert.DoesNotContain(bodyBlocks, block => block.Y < upperBandTop && block.Y > lowerBandBottom);
+ }
+
+ [Fact]
+ public void Convert_WrapTopAndBottomTextBoxBelowMargin_MovesContinuationToNextPage()
+ {
+ var continuationText = string.Join(" ", Enumerable.Repeat("continuation", 400));
+ var afterParagraph = $"""
+
+ After
+ {continuationText}
+
+ """;
+ using var stream = CreateDocxWithAbsoluteWrapTopAndBottomTextBox(
+ "page", 8255000, afterParagraph, boxHeightEmu: 1270000);
+
+ var document = DocxToPdfConverter.Convert(stream);
+
+ Assert.True(document.Pages.Count >= 2);
+ Assert.DoesNotContain(document.Pages[0].TextBlocks,
+ block => block.Text != "Boxed" && block.Y < 72f);
+ Assert.Contains(document.Pages.Skip(1).SelectMany(page => page.TextBlocks),
+ block => block.Text.Contains("continuation"));
+ }
+
+ ///
+ /// Creates a minimal DOCX with three paragraphs; the second hosts a wrapTopAndBottom text
+ /// box whose vertical anchor uses the given relativeFrom value and EMU offset.
+ ///
+ private static MemoryStream CreateDocxWithAbsoluteWrapTopAndBottomTextBox(
+ string relativeFrom, int posOffsetEmu,
+ string afterParagraph = "After",
+ int boxHeightEmu = 508000, int? secondPosOffsetEmu = null)
+ {
+ var secondTextBoxRun = secondPosOffsetEmu.HasValue
+ ? $"""
+
+
+
+
+ 0
+ {secondPosOffsetEmu.Value}
+
+
+
+
+
+
+
+
+
+
+
+ Boxed2
+
+
+
+
+
+
+
+ """
+ : "";
+ var stream = new MemoryStream();
+ using (var archive = new ZipArchive(stream, ZipArchiveMode.Create, leaveOpen: true))
+ {
+ AddEntry(archive, "[Content_Types].xml",
+ """
+
+
+
+
+
+
+ """);
+ AddEntry(archive, "_rels/.rels",
+ """
+
+
+
+
+ """);
+ AddEntry(archive, "word/document.xml",
+ $"""
+
+
+
+ Intro
+
+ Host
+
+
+
+
+ 0
+ {posOffsetEmu}
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Boxed
+
+
+
+
+
+
+
+
+
+ {secondTextBoxRun}
+
+ {afterParagraph}
+
+
+
+
+
+
+ """);
+ }
+
+ stream.Position = 0;
+ return stream;
+ }
+
///
/// Creates a minimal DOCX whose single run has a simple shape followed by a grouped shape
/// with an empty text box.