Skip to content

fix(docx): leave hidden text out of the default export - #4958

Merged
DanielNg0729 merged 4 commits into
docling-project:mainfrom
ghaly-m:fix/docx-hidden-text
Oct 23, 2026
Merged

DanielNg0729 merged 4 commits into
docling-project:mainfrom
ghaly-m:fix/docx-hidden-text

Conversation

@ghaly-m

@ghaly-m ghaly-m commented Oct 21, 2026

Copy link
Copy Markdown
Contributor

Text with Word's Hidden font property (w:vanish) was emitted as body content, although Word does not show or print it.

Issue resolved by this Pull Request:
Resolves #4957

Checklist:

  • Documentation has been updated, if necessary. (not needed)
  • Examples have been added, if necessary. (not needed)
  • Tests have been added, if necessary.

Review evidence:

Source: the repro in #4957 (a hidden run, a hidden paragraph and a hidden table cell).

Before (main at 29caea8):

'Shown start. Hidden run. Shown end.': body
'Hidden paragraph.': body
table cells ['Shown cell', 'Hidden cell']: body
default markdown: 'Shown start. Hidden run. Shown end.\n\nHidden paragraph.\n\n| Shown cell   | Hidden cell   |\n|--------------|---------------|'

After:

'Shown start. Shown end.': body
'Hidden paragraph.': invisible
table cells ['Shown cell', '']: body
default markdown: 'Shown start. Shown end.\n\n| Shown cell   |    |\n|--------------|----|'

Tests:

  • tests/test_backend_msword_hidden.py (new): direct formatting (run, paragraph, table cell) and style inheritance (character style, paragraph style, run override). Both tests fail on main and pass with this change.
  • uv run pytest tests/test_backend_msword*.py: 296 passed, 1 skipped. No reference data changed.
  • ruff format --check, ruff check, tach check: pass. ty check docling/backend/msword_backend.py reports no new diagnostics.

Performance: converting the 36 files in tests/data/docx/sources takes 0.80 s on this branch and 0.78-0.80 s on main (best of 3, two runs each).

Not run: the full test suite (only the Word backend tests), and make validate (make is not available on my Windows machine; I ran its ruff, ty and tach steps directly).

Text with Word's Hidden font property (w:vanish) was emitted as body
content, although Word does not show or print it. A fully hidden
paragraph now goes to ContentLayer.INVISIBLE, the same treatment a
hidden PPTX slide (docling-project#4647) or shape (docling-project#4733) gets. A hidden run inside a
shown paragraph or a table cell cannot have its own layer, so it is
dropped from the text.

The hidden state is resolved from the run, then its character style
chain, then the paragraph style chain. Documents with no w:vanish skip
the checks, so conversion speed is unchanged.

Signed-off-by: Michael Ghaly <m.ghaly.bo@gmail.com>
@github-actions

Copy link
Copy Markdown
Contributor

✅ DCO Check Passed

Thanks @ghaly-m, all your commits are properly signed off. 🎉

@mergify

mergify Bot commented Oct 21, 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 added bug Something isn't working docx issue related to docx backend labels Oct 21, 2026
@github-actions

github-actions Bot commented Oct 21, 2026 •

Copy link
Copy Markdown
Contributor

AI triage (advisory)

Checked commit edb8e4b273. The analysis read the diff only. It did not run code from this pull request.

The PR makes the DOCX backend honor Word's Hidden font property (w:vanish). A fully hidden paragraph moves to ContentLayer.INVISIBLE. Hidden runs, hidden table cell text and hidden note paragraphs are dropped from the text.

CI safety: no concern found. A maintainer must still approve CI.

1 related pull request(s) in the same code
PR State Signal Assessment
#3891 open changes the same code related: Both PRs change how msword_backend.py builds paragraph runs, but 3891 changes whitespace handling and this PR changes hidden text. Both can merge, but they may conflict near _get_paragraph_elements and _iter_paragraph_content.

The ai:* labels are advisory. They do not approve CI and they do not add tests:full. A new push runs this check again.

@codecov

codecov Bot commented Oct 22, 2026 •

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.

📢 Thoughts on this report? Let us know!

@DanielNg0729 DanielNg0729 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.

Hi @ghaly-m, thank you for this fix! I could reproduce #4957 on main, and moving hidden paragraphs to ContentLayer.INVISIBLE matches what we do for hidden slides and shapes, so the approach looks right to me. I found two cases that still leak or lose content, though.

from io import BytesIO

from docx import Document
from docx.oxml import parse_xml
from docx.oxml.ns import nsdecls

from docling.datamodel.base_models import DocumentStream
from docling.document_converter import DocumentConverter

document = Document()
document.add_heading(level=1).add_run("Hidden heading").font.hidden = True
document.add_paragraph(style="List Bullet").add_run("Hidden item").font.hidden = True
document.add_paragraph("Shown item 1", style="List Bullet")
document.add_paragraph("Shown item 2", style="List Bullet")
para = document.add_paragraph("Value ")
para._p.append(parse_xml(f'<m:oMath {nsdecls("m")}><m:r><m:t>x=1</m:t></m:r></m:oMath>'))
para.add_run(" end.")
para.add_run(" secret").font.hidden = True

buf = BytesIO()
document.save(buf)
buf.seek(0)
result = DocumentConverter().convert(DocumentStream(name="hidden.docx", stream=buf))
print(result.document.export_to_markdown())

Main:

## Hidden heading

- Hidden item
- Shown item 1
- Shown item 2

Value  $x=1$ end. secret

This PR:

## Hidden heading

- Hidden item

- Shown item 1

- Shown item 2

Value  end.
  1. Hidden headings and hidden list items are still in the default export. Also, the shown list items after a hidden first item become a loose list.
  2. A paragraph with an inline equation and a hidden run now loses the equation, which main keeps.

I left inline comments with what I found for each. Thanks again for working on this!

Comment thread docling/backend/msword_backend.py
Comment thread docling/backend/msword_backend.py
Comment thread docling/backend/msword_backend.py
Comment thread tests/test_backend_msword_hidden.py
@ghaly-m
ghaly-m force-pushed the fix/docx-hidden-text branch from 44ff571 to c3282c3 Compare October 22, 2026 14:59
Address the review of the first commit:

- Headings and list items take their own add calls, which did not pass
  the content layer. They now go to ContentLayer.INVISIBLE when hidden.
  List groups stay on the surrounding layer, so a hidden first item no
  longer hides the shown items after it or makes the list loose.
- The equation parser read the w:t of hidden runs, so its rebuilt text
  did not match and the equation was lost. It now skips hidden runs. A
  table cell with an equation also kept its hidden text; it does not now.
- A paragraph with an equation is never fully hidden, and an equation
  with only hidden text beside it is a standalone formula again.
- python-docx loads footnotes and endnotes as plain parts, so w:vanish
  only there was not found. A hidden paragraph in a note is now left out.
- Documents without w:vanish skip the style chain lookups for every run
  and table cell run.

Signed-off-by: Michael Ghaly <m.ghaly.bo@gmail.com>
@ghaly-m

ghaly-m commented Oct 22, 2026

Copy link
Copy Markdown
Contributor Author

Thanks for the careful review, @DanielNg0729! All four points are addressed in 7e13581:

  1. Headings, list items and their inline groups now pass content_layer. List groups are created on the surrounding layer, so a hidden first item leaves the shown items in one tight list.
  2. _handle_equations_in_text skips the w:t of hidden runs, as you suggested. That also fixed a table cell with an equation, which still kept its hidden text.
  3. Documents without w:vanish now skip the style-chain work for runs and table cells. With cProfile, _is_hidden_run is never called on tests/data/docx/sources.
  4. Added tests for a hidden heading, a hidden first list item, equations next to hidden runs (paragraph, list item, standalone, table cell), and hidden runs in a hyperlink and a content control.

I also found that python-docx loads footnotes and endnotes as plain parts, so w:vanish only there was missed. That is fixed, with a test. A hidden numbered item keeps its number, as Word does.

Signed-off-by: Nguyen Hoang Duong <danielnguyenh07@gmail.com>
@github-actions github-actions Bot added the ai:ci-safe AI triage found no CI-safety concern (advisory) label Oct 23, 2026
Signed-off-by: Daniel Nguyen <danielnguyenh07@gmail.com>
@github-actions github-actions Bot removed the ai:ci-safe AI triage found no CI-safety concern (advisory) label Oct 23, 2026

@DanielNg0729 DanielNg0729 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.

hi @ghaly-m, Thank you for the follow up, i add a commit to resolve the merge conflict. Looks good to me now!

@github-actions github-actions Bot added the ai:ci-safe AI triage found no CI-safety concern (advisory) label Oct 23, 2026
@DanielNg0729
DanielNg0729 merged commit 36be1d2 into docling-project:main Oct 23, 2026
33 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

ai:ci-safe AI triage found no CI-safety concern (advisory) bug Something isn't working docx issue related to docx backend

Projects

None yet

Development

Successfully merging this pull request may close these issues.

DOCX: hidden text (w:vanish) is emitted as body content

2 participants