Skip to content

fix(docx): read the text inside a DrawingML text box - #2514

Open
Lukas (L4XB) wants to merge 1 commit into
microsoft:mainfrom
L4XB:fix/docx-drawing-text-boxes
Open

Lukas (L4XB) wants to merge 1 commit into
microsoft:mainfrom
L4XB:fix/docx-drawing-text-boxes

Conversation

@L4XB

Copy link
Copy Markdown

Summary

A text box keeps its own paragraphs in a w:txbxContent. Mammoth reads that
element — it is in its handler table — but only reaches it through the legacy VML
path, w:pictv:shapev:textbox. A modern text box is a DrawingML
shape instead:

w:drawing → wp:inline → a:graphic → a:graphicData → wps:wsp → wps:txbx → w:txbxContent

and wp:inline is handled as a picture: it looks for an a:blip, finds none,
and returns nothing. The shape's text is dropped with no exception, no warning,
and nothing in result.messages. Callouts, pull quotes, sidebars and diagram
labels are simply absent from the Markdown.

Measured on a document whose only text box is a bare w:drawing:

output
before PARAGRAPH TEXT\n\nAFTER
after PARAGRAPH TEXT\n\nCALLOUT\n\nAFTER

What changed

One more step in pre_process_docx, which is exactly what that module is for —
it already renames w:dstrike to w:strike and repairs w:style elements
because "downstream converters do not recognize" them.

A copy of the w:txbxContent is inserted after the w:drawing that holds it,
wrapped in w:pict/v:shape/v:textbox, and the original is removed so the text
cannot be read twice. Mammoth treats a w:pict as extra content that follows the
paragraph containing it, so the text lands right after the paragraph the box is
anchored in — where a reader of the page sees it — rather than appended at the
end of the document.

Text boxes inside mc:AlternateContent are deliberately left alone. Word
writes the same text twice there: a DrawingML shape under mc:Choice and a VML
shape under mc:Fallback. Mammoth reads the fallback branch, so promoting the
choice as well would emit the text twice. That is also why this has gone
unnoticed — a file saved by Word usually carries the fallback, while a text box
written as a bare w:drawing (which is valid OOXML and what several generators
and exporters produce) has nothing for Mammoth to fall back to.

No new dependency: the step uses the same BeautifulSoup parse the neighbouring
steps use, and returns the input untouched when the document contains no
txbxContent at all — the same fast path _pre_process_strike takes.

Tests

packages/markitdown/tests/test_docx_text_boxes.py builds its fixtures by
injecting a text box into the committed test.docx, the way
test_docx_styles.py does:

  • a bare w:drawing text box appears once in the output
  • an mc:AlternateContent text box appears exactly once, not twice
  • a document with nothing injected converts byte-for-byte as before
$ pytest tests/test_docx_text_boxes.py -q
3 passed

# same tests against the current pre_process_docx
FAILED test_a_drawing_text_box_is_read
1 failed, 2 passed

The two that pass on both sides are the guards: no duplication, and no change to
documents without a text box.

$ pytest tests/ -q
895 passed, 14 skipped

black (23.7.0, the pinned pre-commit version) leaves both files unchanged.

A text box keeps its own paragraphs in a `w:txbxContent`. Mammoth reads that
element, but only reaches it through the legacy VML path
(`w:pict` -> `v:shape` -> `v:textbox`). A modern text box is a DrawingML shape
instead -- `w:drawing` -> `wp:inline` -> `wps:wsp` -> `wps:txbx` -- and
`wp:inline` is read as a picture, so the shape's text is dropped with no warning
and nothing in the messages. Callouts, pull quotes, sidebars and diagram labels
simply are not in the output.

Measured on a document whose only text box is a bare `w:drawing`:

    before: 'PARAGRAPH TEXT\n\nAFTER'
    after:  'PARAGRAPH TEXT\n\nCALLOUT\n\nAFTER'

This is the same kind of repair the file already performs for Mammoth: a copy of
the `w:txbxContent` is inserted after the `w:drawing` that holds it, wrapped in
`w:pict`/`v:shape`/`v:textbox`, and the original is removed so the text cannot be
read twice. Mammoth treats a `w:pict` as extra content following the paragraph it
sits in, which is where a reader of the page sees the text box anyway.

Text boxes inside `mc:AlternateContent` are left alone. Word writes the same text
twice there -- a DrawingML shape under `mc:Choice` and a VML shape under
`mc:Fallback` -- and Mammoth reads the fallback, so promoting the choice as well
would duplicate it. The second test pins that it appears exactly once.
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.

1 participant