Skip to content

fix(docx): descend into a content control that also holds a picture (#3950) - #3952

Open
Anai-Guo wants to merge 1 commit into
docling-project:mainfrom
Anai-Guo:fix-docx-sdt-picture-shadowing-3950
Open

fix(docx): descend into a content control that also holds a picture (#3950)#3952
Anai-Guo wants to merge 1 commit into
docling-project:mainfrom
Anai-Guo:fix-docx-sdt-picture-shadowing-3950

Conversation

@Anai-Guo

@Anai-Guo Anai-Guo commented Aug 6, 2026

Copy link
Copy Markdown
Contributor

Fixes #3950.

Problem

_walk_linear computes the image hits with descendant XPaths before it dispatches:

_raw_drawing_blip = self.blip_xpath_expr(element)      # .//a:blip
_raw_drawingml_els = element.findall(".//w:drawing", ...)

and then dispatches with a single if/elif chain in which elif tag_name == "sdt" sits after elif drawing_blip / elif vml_images / elif drawingml_els.

So whenever a content control contains a picture anywhere inside it, the sdt element itself matches the image branch, the picture is emitted, and _walk_linear(sdt_content, doc) never runs — every paragraph in that control is silently dropped.

Word's built-in "Cover Page" building blocks are exactly this shape: a body-level <w:sdt> holding the logo and the title, subtitle, company and date fields. The result is a document whose title never reaches the DoclingDocumentexport_to_markdown() starts at the table of contents, preceded by a lone <!-- image -->.

Fix

Test for the content control before the image branches, so an sdt is always descended into and its children are then dispatched normally. The sdt branch body is unchanged; only its position in the chain moves (it now sits directly after the tbl branch).

The picture is still emitted, and exactly once — the image branch now fires on the inner <w:p> rather than on the sdt. It also lands at its true position inside the control rather than at the point where the sdt starts, which is the correct reading order.

Verification

Reproduction from the issue, on main vs. this branch:

file main this branch
with_image.docx title present: False, images: 1 — '<!-- image -->\n\nBODY TEXT OUTSIDE SDT' title present: True, images: 1 — '<!-- image -->\n\nCOVER TITLE INSIDE SDT\n\nBODY TEXT OUTSIDE SDT'
no_image.docx title present: True, images: 0 title present: True, images: 0 (unchanged)

Added tests/test_backend_msword_sdt_pictures.py (a new file, so test_backend_msword.py stays under the per-file line limit). It builds the DOCX in-test — no new fixture or groundtruth — and asserts both that the control's text survives and that <!-- image --> still appears exactly once. It fails on main and passes here.

Existing DOCX suites are unchanged: tests/test_backend_msword.py, test_backend_msword_lists.py and test_backend_msword_spacer.py give the identical result before and after (56 passed / 1 failed either way; the single failure is the pre-existing test_e2e_docx_conversions DrawingML case that needs LibreOffice, which is not installed here). test_block_sdt_tables_are_extracted and test_inline_sdt_references — the two existing sdt tests — still pass, as do the textbox and picture tests.

ruff==0.15.12 check / format --check are clean on both files.

Note on the base commit: this branch is based on the fork's main rather than the current upstream tip, because syncing the fork is blocked (refusing to allow an OAuth App to create or update workflow .github/workflows/checks.yml without workflow scope). Upstream's changes to msword_backend.py since then are confined to the list-handling code and _add_header_footer, none of which touch the _walk_linear dispatch chain, so the three-way merge is clean.

Companion to #3951 (table cells inside a content control) — different code path, same theme of <w:sdt> not being descended into. The two are independent and touch disjoint regions of the file.

🤖 Generated with Claude Code

…ocling-project#3950)

The element walk computes the image branches with descendant XPaths
(``.//a:blip``, ``.//w:drawing``, ``.//v:imagedata``), so a ``w:sdt``
holding a picture anywhere inside matched ``elif drawing_blip`` first and
the ``elif tag_name == "sdt"`` branch was never reached. The picture was
emitted, every paragraph in the control was dropped.

Word's built-in cover pages are a body-level ``w:sdt`` holding the logo
together with the title, subtitle, company and date fields, so the
document's own title never reached the DoclingDocument.

Test the content control before the image branches, so an ``sdt`` is
always descended into and its children are dispatched normally. The
picture is still emitted exactly once -- the image branch now fires on
the inner ``w:p`` -- and at its true position inside the control.

Signed-off-by: Tai An <antai12232931@outlook.com>
@mergify

mergify Bot commented Aug 6, 2026

Copy link
Copy Markdown
Contributor

Merge Protections

🟢 Merge protection satisfied — ready to merge.

Show 1 satisfied protection

🟢 Enforce conventional commit

Make sure that we follow https://www.conventionalcommits.org/en/v1.0.0/

  • title ~= ^(fix|feat|docs|style|refactor|perf|test|build|ci|chore|revert)(?:\(.+\))?(!)?:

@github-actions

github-actions Bot commented Aug 6, 2026

Copy link
Copy Markdown
Contributor

DCO Check Passed

Thanks @Anai-Guo, all your commits are properly signed off. 🎉

@PeterStaar-IBM PeterStaar-IBM left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

lgmt!

@codecov

codecov Bot commented Aug 10, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.

📢 Thoughts on this report? Let us know!

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.

DOCX: text inside a content control (w:sdt) is dropped when the control also contains an image

2 participants