Skip to content

Render behindDoc wpg group fills when a child shape carries a text box - #165

Merged
shps951023 merged 4 commits into
mini-software:mainfrom
Sen-CaPoo:fix/docx-behinddoc-group-textbox-fills
Sep 11, 2026
Merged

Render behindDoc wpg group fills when a child shape carries a text box#165
shps951023 merged 4 commits into
mini-software:mainfrom
Sen-CaPoo:fix/docx-behinddoc-group-textbox-fills

Conversation

@Sen-CaPoo

@Sen-CaPoo Sen-CaPoo commented Sep 10, 2026

Copy link
Copy Markdown
Member

Problem

tests/Issue_Files/docx/Fabrikam.docx (business-card sheet) rendered as plain text on a white page. Word and LibreOffice both draw a light-blue page background, twenty white rectangles and ten card outlines. All of that comes from one behindDoc="1" wpg:wgp group with 31 wps:wsp children.

Root cause

DocxReader only calls ReadAnchorShapes when the run contains no w:txbxContent. The first rectangle of this group carries a wps:txbx holding a single soft hyphen (and the VML fallback mirrors it with v:textbox), so the whole group was skipped. The floating text box path deliberately skips fill extraction for groups (anchorHasGroupShape) and expects ReadAnchorShapes to draw them, so neither path painted the group.

Falsification check before editing: removing both the wps:txbx and the mc:Fallback from a copy of the fixture made the existing code emit 21 rectangles and 40 outline segments with the correct geometry; removing only the fallback still produced nothing.

LibreOffice keeps such groups intact: oox/source/shape/WpgContext.cxx (WpgContext::onCreateContext) creates a shape context for every wsp child and recurses into nested grpSp; oox/source/shape/WpsContext.cxx (XML_txbx) only marks that child as a text box; sw/source/writerfilter/dmapper/GraphicImport.cxx (LN_CT_Anchor_behindDoc) places the anchor behind text.

Change

When the run contains text box content but the drawing's wp:anchor directly holds wpg:wgp, still read the group shapes through ReadAnchorShapes. ReadImage keeps the original guard, and single-shape text boxes keep their existing fill handling.

Tests and validation

  • New test DocxIssueFileTests.Fabrikam_BehindDocGroupWithTextBox_RendersGroupFills (fails before the change, passes after).
  • dotnet test tests/MiniPdf.Tests --configuration Release: 193 passed, 0 failed.
  • git diff --check: clean.

Benchmark evidence (.NET, issue suite, docx, Microsoft 365 reference)

Focused run: pwsh -File scripts/Run-DotNet-VisualBenchmark.ps1 -Suite issue -Format docx -Filter "Fabrikam"
Full run: pwsh -File scripts/Run-DotNet-VisualBenchmark.ps1 -Suite issue -Format docx

Case Overall before Overall after Visual before Visual after Pages (MiniPdf/Ref)
Fabrikam 0.6223 0.9770 0.0558 0.9426 1/1 (unchanged)
issue/docx average (27 cases) 0.9231 0.9363

All 27 cases converted, all PDFs valid, all comparison images present, no page-count changes, no visual_avg decrease. 25 cases have identical scores. OSCAR WARD moved +0.0033 visual; a rebuild from main produces a byte-identical PDF for that case, so the delta comes from the Office cloud-font cache (Source Sans Pro) populated while Word generated the reference PDFs, not from this change.

Compatibility and scope

Internal DocxReader change only; no public API change. Only DOCX documents with a behindDoc wpg:wgp group whose child shape carries a text box are affected. Among the repository's docx fixtures, only Fabrikam has that structure. No new third-party material, fixtures or fonts. No documentation change needed.

Generated with Claude Code

Summary by CodeRabbit

  • Bug Fixes

    • Improved DOCX rendering when multiple drawings appear in the same text run.
    • Improved handling of grouped shapes anchored within drawings, preserving child fills and visual details.
    • Improved rendering of grouped shapes in documents containing text boxes, including background fills, rectangles, and lines behind document content.
  • Tests

    • Added coverage for multiple top-level drawings and grouped shape fills in DOCX documents.
    • Added a rendering test for affected DOCX content.

DocxReader skipped ReadAnchorShapes for any run containing w:txbxContent,
so a behindDoc wpg group lost every fill when one child rectangle held an
(empty) text box. The floating text box path already skips fill
extraction for groups and relies on ReadAnchorShapes to draw them, so
nothing rendered the group at all. Read the group shapes when the
drawing's anchor directly holds wpg:wgp, keeping the image guard as is.

Fabrikam (issue/docx): overall 0.6223 -> 0.9770, visual 0.0558 -> 0.9426;
the other 26 issue/docx cases are unchanged.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Copilot AI lite review requested due to automatic review settings September 10, 2026 13:06
@coderabbitai

coderabbitai Bot commented Sep 10, 2026

Copy link
Copy Markdown

Review Change StackReview Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Advanced

Run ID: a52a347f-e503-4b45-a8a2-1732102d7f57

📥 Commits

Reviewing files that changed from the base of the PR and between c91b31d and 1f4540e.

📒 Files selected for processing (2)
  • src/MiniPdf/DocxReader.cs
  • tests/MiniPdf.Tests/DocxDrawingTests.cs

Included review availability: Your plan provides up to 2 included reviews per hour; 1 remains after this review.


📝 Walkthrough

Walkthrough

The DOCX reader now processes every top-level drawing in a run, including grouped anchored shapes with text boxes. New tests validate multiple drawing parsing and Fabrikam rendering output. XML documentation describes the updated reader methods.

Changes

DOCX drawing processing

Layer / File(s) Summary
Process top-level DOCX drawings
src/MiniPdf/DocxReader.cs
ReadParagraph processes each directly owned w:drawing element. It routes grouped anchored drawings with text boxes to ReadAnchorShapes and documents related reader methods.
Validate drawing and rendering behavior
tests/MiniPdf.Tests/DocxDrawingTests.cs, tests/MiniPdf.Tests/DocxIssueFileTests.cs
Tests validate multiple drawings, grouped shape fills, and Fabrikam rendering output.

Priority: ➖ Normal

Estimated code review effort: 3 (Moderate) | ~20 minutes

Suggested reviewers: shps951023

Merge Risk: ⚪ Minimal · up to 1f454

The updated drawing processing is covered by focused parsing and rendering tests, with no remaining merge-blocking risk identified.

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly and concisely describes the main change: rendering fills for behindDoc wpg groups when a child shape contains a text box.
Docstring Coverage ✅ Passed Docstring coverage is 88.89% which is sufficient. The required threshold is 80.00%. Docstring coverage is scoped to functions touched by this diff. Analyzed 9 functions across 3 files.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Comment @coderabbitai help to get the list of available commands.

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Changes recommended

The new group-detection condition in DocxReader is overly strict (direct-child .Element(...) chain) and may fail to detect anchors wrapped in common DOCX structures like mc:AlternateContent, reintroducing skipped group fills.

Once you've addressed the issues Copilot identified, you can request another Copilot review.

Pull request overview

This PR fixes a DOCX rendering gap where a behindDoc="1" wpg:wgp background group could be skipped when one of its child shapes contains w:txbxContent, causing the page background and card rectangles to not be painted (notably for the Fabrikam.docx fixture).

Changes:

  • Update DocxReader to still read behindDoc group shapes via ReadAnchorShapes even when the run contains w:txbxContent, as long as the drawing contains a wpg:wgp group.
  • Add a regression test asserting the expected background fill, white rectangles, and outline segments for Fabrikam.docx.
File summaries
File Description
src/MiniPdf/DocxReader.cs Ensures behindDoc WPG group shapes are still extracted when a run also contains textbox content.
tests/MiniPdf.Tests/DocxIssueFileTests.cs Adds a fixture-based test validating that the group background + rectangles + outlines render.
Review details
  • Files reviewed: 2/2 changed files
  • Comments generated: 1
  • Review effort level: Lite

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Comment thread src/MiniPdf/DocxReader.cs Outdated
Comment on lines +1386 to +1392
else if (runDrawing?.Element(WP + "anchor")?.Element(A + "graphic")
?.Element(A + "graphicData")?.Element(WPG + "wgp") != null)
{
// A wpg group keeps its child fills even when one child carries a
// text box: the floating text box path skips fill extraction for
// groups (anchorHasGroupShape), so the group shapes are read here.
shapes.AddRange(ReadAnchorShapes(runDrawing, themeColors));

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for the review. The direct-child chain is deliberate, and I believe the wrapping premise does not apply here:

  • mc:AlternateContent wraps w:drawing from above (w:r > mc:AlternateContent > mc:Choice > w:drawing > wp:anchor). runDrawing is located with Descendants(W + "drawing"), so the wrapper is already passed before the chain starts. Fabrikam.docx, the fixture this PR targets, is exactly such a wrapped drawing, and the new test exercises it.
  • Below w:drawing the structure is fixed by the schema: CT_Drawing is a choice of wp:anchor / wp:inline, a:graphic is a direct element of CT_Anchor, and wpg:wgp sits directly under a:graphicData (see CT_Drawing, CT_Anchor and CT_GraphicalObject in LibreOffice's sw/source/writerfilter/ooxml/model.xml). Scanning every DOCX fixture under tests/Issue_Files/docx: all 107 wp:anchor elements have w:drawing as their parent, and all 11 wpg:wgp elements follow a:graphicData < a:graphic < wp:anchor|wp:inline < w:drawing < mc:Choice.
  • A Descendants(WPG + "wgp") match would also fire for a group nested inside a text box's w:txbxContent, in the very run whose text box triggered the guard. ReadAnchorShapes would then read that nested group against the outer anchor's offsets and paint it at the wrong position. The strict chain only accepts a group that is the anchor's own graphic, which is the case this PR restores.

I would therefore keep the precise check. Happy to switch to the descendant search if a maintainer prefers consistency with ReadAnchorShapes over that safeguard.

@Sen-CaPoo

Copy link
Copy Markdown
Member Author

Note on the CodeRabbit pre-merge warning (docstring coverage 0%): the functions it counts are the existing ReadParagraph body, which this diff only touches, and the new xUnit test. Neither the surrounding reader code nor the existing issue-file tests carry XML doc comments, and CONTRIBUTING.md does not ask for them, so I left them out to keep the change focused. I can add summary comments if the maintainers want them.

Documents ReadParagraph, FindIssueDocx and the two DocxIssueFileTests
methods that the previous commit touched, so the CodeRabbit docstring
coverage pre-merge check on mini-software#165 passes. No behavior change.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
@Sen-CaPoo

Copy link
Copy Markdown
Member Author

Added XML doc comments in 53541f0 to the four functions the diff touches (ReadParagraph, FindIssueDocx and the two DocxIssueFileTests methods) so the CodeRabbit docstring coverage pre-merge check passes. No behavior change; the full test suite still passes (193 tests).

The docstring coverage check counts every function whose lines fall
inside a diff hunk, including the three context lines, so documenting a
function pulled its undocumented neighbour into the check (66.67%).

Move the new Fabrikam test to the top of DocxIssueFileTests, where the
leading context is the class header, and document the first test it now
precedes. Document the two VML helpers above ReadParagraph, whose own
predecessor (Read) is already documented. Drop the doc comments on
Issue93 and FindIssueDocx, which the diff no longer touches. No behavior
change.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
@Sen-CaPoo

Copy link
Copy Markdown
Member Author

Follow-up in c91b31d: the docstring coverage check counts every function inside a hunk, including the three context lines, so each new comment pulled an undocumented neighbour into the set (66.67%). Moved the new test to the top of the class (its leading context is the class header), documented the first test it now precedes, and documented the two VML helpers above ReadParagraph, whose predecessor Read is already documented. Every function the diff touches now carries a doc comment. No behavior change; 193 tests pass.

@shps951023

Copy link
Copy Markdown
Member

Maintainer follow-up added in 1f4540e: process each drawing owned by a run independently, with a synthetic regression test for multiple drawings when a later wpg group carries textbox content. Validation: 194/194 xUnit tests passed; focused Fabrikam score remains 0.9770; full 27-case issue/docx benchmark remains 0.9561 with 0 metric, page-count, or candidate PDF hash changes versus c91b31d.

@shps951023
shps951023 merged commit 54f0648 into mini-software:main Sep 11, 2026
3 checks passed
@shps951023

Copy link
Copy Markdown
Member

@Sen-CaPoo I've sent invitation 🙌🙌🙌

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants