Repository navigation
fix(docx): leave hidden text out of the default export - #4958
Conversation
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>
|
✅ DCO Check Passed Thanks @ghaly-m, all your commits are properly signed off. 🎉 |
Merge Protections🟢 Merge protection satisfied — ready to merge. Show 1 satisfied protection🟢 Enforce conventional commitMake sure that we follow https://www.conventionalcommits.org/en/v1.0.0/
|
AI triage (advisory)Checked commit The PR makes the DOCX backend honor Word's Hidden font property ( CI safety: no concern found. A maintainer must still approve CI. 1 related pull request(s) in the same code
The |
Codecov Report✅ All modified and coverable lines are covered by tests. 📢 Thoughts on this report? Let us know! |
DanielNg0729
left a comment
There was a problem hiding this comment.
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. secretThis PR:
## Hidden heading
- Hidden item
- Shown item 1
- Shown item 2
Value end.- 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.
- 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!
44ff571 to
c3282c3
Compare
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>
|
Thanks for the careful review, @DanielNg0729! All four points are addressed in 7e13581:
I also found that python-docx loads footnotes and endnotes as plain parts, so |
Signed-off-by: Nguyen Hoang Duong <danielnguyenh07@gmail.com>
Signed-off-by: Daniel Nguyen <danielnguyenh07@gmail.com>
There was a problem hiding this comment.
hi @ghaly-m, Thank you for the follow up, i add a commit to resolve the merge conflict. Looks good to me now!
Text with Word's Hidden font property (
w:vanish) was emitted as body content, although Word does not show or print it.ContentLayer.INVISIBLEwith its text, the same treatment a hidden PPTX slide (fix(pptx): move hidden slides to the invisible content layer #4647) or shape (fix(pptx): move hidden shapes to the invisible content layer #4733) gets.w:vanish w:val="0"on a run overrides a hidden style.w:vanishskip the checks.Issue resolved by this Pull Request:
Resolves #4957
Checklist:
Review evidence:
Source: the repro in #4957 (a hidden run, a hidden paragraph and a hidden table cell).
Before (main at 29caea8):
After:
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.pyreports no new diagnostics.Performance: converting the 36 files in
tests/data/docx/sourcestakes 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(makeis not available on my Windows machine; I ran its ruff, ty and tach steps directly).