Skip to content

feat(docx): convert embedded Word, Excel and PowerPoint objects - #4716

Open
DanielNg0729 wants to merge 10 commits into
docling-project:mainfrom
DanielNg0729:feat/docx-embedded-objects
Open

DanielNg0729 wants to merge 10 commits into
docling-project:mainfrom
DanielNg0729:feat/docx-embedded-objects

Conversation

@DanielNg0729

Copy link
Copy Markdown
Member

Hi maintainers, this PR lets the Word backend convert the Excel workbooks, PowerPoint presentations and Word documents that are embedded in a .docx file as OLE objects. It is the Word part of #2051.

Summary

On main, the backend keeps only the preview picture of an embedded object (an EMF image). The table in an embedded workbook or the text on an embedded slide is lost, so a RAG pipeline never sees it.

Issue resolved by this Pull Request:
Part of #2051

Usage

The feature is opt-in. The default output does not change.

from docling.datamodel.backend_options import MsWordBackendOptions
from docling.datamodel.base_models import InputFormat
from docling.document_converter import DocumentConverter, WordFormatOption

converter = DocumentConverter(
    format_options={
        InputFormat.DOCX: WordFormatOption(
            backend_options=MsWordBackendOptions(process_embedded_objects=True)
        )
    }
)
doc = converter.convert("report.docx").document

print(doc.export_to_markdown())

# Each embedded document is a group named after its part.
for group in doc.groups:
    if group.name.startswith("embedded: "):
        print(group.name)  # e.g. "embedded: Microsoft_Excel_Worksheet.xlsx"

Markdown export of the new test document, with the option set:

## Quarterly report

The budget workbook is embedded below.

<!-- image -->

| Item   |   Amount |
|--------|----------|
| Rent   |     1200 |
| Travel |      300 |
| Total  |     1500 |

The roadmap slide is embedded below.

<!-- image -->

# Roadmap

- Ship embedded object parsing

The meeting notes document is embedded below.

<!-- image -->

Meeting notes: approve the budget.

<!-- image -->

Next meeting is on Friday.

A text file is embedded as a package.

<!-- image -->

End of report.

How Word stores an embedded document

<w:object>
  <v:shape ...><v:imagedata r:id="rId4"/></v:shape>          <!-- preview picture (EMF) -->
  <o:OLEObject Type="Embed" ProgID="Excel.Sheet.12" r:id="rId5"/>
</w:object>

rId5 is a .../relationships/package relationship to word/embeddings/Microsoft_Excel_Worksheet.xlsx, a complete OOXML package. Legacy objects (for example Excel.Sheet.8, or a PDF or text file inserted as Package) use an .../relationships/oleObject relationship to a binary compound file (oleObject1.bin).

Other choices

  1. Opt-in, default False. The output for existing users and the reference data do not change. The same pattern as render_chart_images.
  2. A group at the position of the object. The group comes in the reading order where the object is, with the preview picture as its first child. I did not put the content under the PictureItem, because the serializers do not export the children of a picture by default (traverse_pictures=False), so the content would not be in the Markdown. The group uses GroupLabel.SECTION, the label that the Excel backend gives to a sheet, and the name embedded: <part file name>.
  3. One level only. The embedded document is converted with the default options of its backend, so process_embedded_objects is False for an embedded Word document. The work stays bounded without a depth counter.
  4. Each part once. A crafted file can point many w:object elements at one large part. The backend converts a part only one time. The other objects keep their preview.
  5. Reference remapping. DoclingDocument.add_document copies the tree, but captions, footnotes, references, comments and RichTableCell.ref still point at the indexes of the embedded document. With docx_rich_cells.docx as the embedded document, 21 of 25 rich cell references point at wrong items after add_document alone. The backend maps each source item to its copy (the two trees have the same shape) and rewrites these references. A FineRef keeps its character range. docling-core 2.101.1 (main) has the same add_document code.
  6. No page provenance. The Excel and PowerPoint backends give their items page provenance. A Word document has no pages, so export_to_doctags() raised KeyError: 1 on the copied table. The backend removes the provenance of the copies.
  7. Content layer. An object in a header or footer gives furniture content, the same as the other items there. The relationship is read from the part that is walked (current_part), so an object in a header uses the header relationships.
  8. Errors. A broken or unsupported embedded document must not fail the Word conversion. The backend logs a warning and keeps the preview.

Test data

tests/data/docx/embedded_objects/docx_embedded_objects.docx (63 KB) was made in Microsoft Word 365 through COM automation, with InlineShapes.AddOLEObject. It embeds an .xlsx, a .pptx, a .docx that embeds an .xlsx of its own, and a .txt file (a binary Package object). All XML parts are Word's output. Word could not save here, so I exported the document with Document.WordOpenXML (Flat OPC) and packed the parts into a .docx. Word opens the result and reports 4 embedded OLE objects (Excel.Sheet.12, PowerPoint.Show.12, Word.Document.12, Package).

The fixture is not in tests/data/docx/sources/, because the generic e2e test converts every file there and compares the pictures. The previews are EMF images, and Pillow on Windows, LibreOffice and a system without LibreOffice give different images. The new tests disable LibreOffice and check the document tree directly.

Checklist:

  • Documentation has been updated, if necessary.
  • Examples have been added, if necessary.
  • Tests have been added, if necessary.

Word stores an embedded Office document as a package part behind an OLE
object. The backend kept only the preview picture of the object, so the
content of the document was lost.

Add the opt-in MsWordBackendOptions.process_embedded_objects option. When
it is set, the backend converts each embedded .docx, .xlsx or .pptx part
with its own backend and copies the result into a group, after the
preview picture of the object.

- The copy remaps captions, footnotes, references, comments and rich
  table cells, because DoclingDocument.add_document keeps their old
  indexes. It also removes the page provenance, because a Word document
  has no pages.
- The embedded document uses the default options of its backend, so the
  objects embedded in it are not converted.
- Each part is converted once, also when several objects share it.
- Linked objects, binary OLE objects and documents that fail to convert
  keep only their preview picture, with a warning for a failure.

Refs docling-project#2051

Signed-off-by: Daniel Nguyen <danielnguyenh07@gmail.com>
Add a fixture made in Microsoft Word. It embeds a workbook, a
presentation, a Word document that embeds a workbook of its own, and a
text file. The author name and local paths were removed from it.

The tests check the default behaviour, the position and content of each
embedded document, the single conversion level, DocTags export, rich
table cell references, broken and linked objects, a missing format extra,
and a part that two objects share.

Signed-off-by: Daniel Nguyen <danielnguyenh07@gmail.com>
Signed-off-by: Daniel Nguyen <danielnguyenh07@gmail.com>
@github-actions

github-actions Bot commented Oct 9, 2026

Copy link
Copy Markdown
Contributor

✅ DCO Check Passed

Thanks @DanielNg0729, all your commits are properly signed off. 🎉

@mergify

mergify Bot commented Oct 9, 2026

Copy link
Copy Markdown
Contributor

Merge Protections

🔴 1 of 2 protections blocking · waiting on 👀 reviews

Protection Waiting on
🔴 Require two reviewer for test updates 👀 reviews
🟢 Enforce conventional commit —

🔴 Require two reviewer for test updates

Waiting for

  • #approved-reviews-by >= 2
This rule is failing.

When test data is updated, we require two reviewers

  • #approved-reviews-by >= 2

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 ai:ci-safe AI triage found no CI-safety concern (advisory) docx issue related to docx backend enhancement New feature or request pptx issue related to pptx backend table structure xlsx issue related to xlsx backend labels Oct 9, 2026
@github-actions

github-actions Bot commented Oct 9, 2026 •

Copy link
Copy Markdown
Contributor

AI triage (advisory)

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

This PR adds support for converting embedded Word, Excel, and PowerPoint OLE objects in DOCX files. It introduces backend options to enable this feature and configure limits for object count, total decompressed bytes, and nesting depth.

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

Reference data changes

Change Files
added 3
  • added tests/data/docx/groundtruth/docx_embedded_objects.docx.itxt
  • added tests/data/docx/groundtruth/docx_embedded_objects.docx.json
  • added tests/data/docx/groundtruth/docx_embedded_objects.docx.md

Assessment: expected. The reference files are added for the new test document with embedded objects.

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 9, 2026 •

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 89.28571% with 12 lines in your changes missing coverage. Please review.

Files with missing lines Patch % Lines
docling/backend/msword_backend.py 88.88% 7 Missing and 5 partials ⚠️

📢 Thoughts on this report? Let us know!

dolfim-ibm
dolfim-ibm previously approved these changes Oct 9, 2026

@dolfim-ibm dolfim-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.

lgtm

@DanielNg0729
DanielNg0729 requested a review from ceberam October 12, 2026 08:42
@DanielNg0729

Copy link
Copy Markdown
Member Author

Hi Dr @ceberam, would you mind taking a look at this PR when you have a moment? I’d really appreciate your feedback for this PR. Thank you very much

…objects

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

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

Thanks @DanielNg0729 for suggesting this PR and contributing to new exciting features for Docling!

First of all, to put this PR in the context of the 200+ opened PRs, I would like to understand its motivation: is it to address a real need with the documents your work with? Or just to address the issue #2051 ? By the way, I closed it since we haven't received any response since more than 1 year ago and we understand that the user does not see a need for that feature.

Parsing embedded documents opens some challenges and also requires a broader contract on how to deal with them (FYI @dol since you also review this PR). For instance:

  • Do we want to parse any inserted documents in a .docx file? Why only office files and not .pdf files?
  • How many inserted files should we accept in a .docx document and what about deep nested documents? I see that you explicitly hard-coded the depth to 1, but is this is an acceptable long-term contract? We should at least explain why we set this limitation. Also, see below my comment related to security on this topic.
  • Do we want to enable this feature to the Office formats only or to others that can also embed attachments?

I think these are important decisions since they can set a precedent for other backends and formats.

Having said that, here are some points that should be addressed with the current PR implementation:

Style check fails — trailing whitespace in blank lines

The style check fails and there is a conflict with msword_backend.py. Please rebase to main.

Zip-bomb risk when converting embedded documents from untrusted sources

When process_embedded_objects=True, the backend passes each embedded part's bytes as BytesIO(part.blob) to the relevant backend. None of the three paths apply size or member-count limits:

  • Embedded .docx: load_msword_file(BytesIO(part.blob)) opens a new ZipFile over those bytes. Docling's zip-bomb guards (_sanitize_docx, _normalize_strict_ooxml) run only when the embedded document is Strict OOXML or contains fragment-only rels. For the common case — a plain Transitional-namespace DOCX without fragment rels — python-docx expands the inner archive with no size limit.

  • Embedded .xlsx: openpyxl.load_workbook opens a new ZipFile with no size limit.

  • Embedded .pptx: python-pptx's Package.open opens a new ZipFile with no size limit.

In all three cases, a crafted embedded part whose inner members have a high compression ratio can exhaust available memory when processed. Because part.blob is already in memory as an opaque byte sequence (python-docx loads it when reading the outer DOCX), the outer-document zip-bomb checks do not see the inner archive's expansion ratio.

Fixture created by an unusual path — does it represent real Word output?

The fixture docx_embedded_objects.docx was assembled manually: it was created by COM automation (InlineShapes.AddOLEObject), exported as Flat OPC XML (Document.WordOpenXML), and then repacked into a .docx by hand. The PR description says Word could not save the file directly.

Examining the fixture reveals that the inner embedded Word document (word/embeddings/Microsoft_Word_Document.docx) carries a Flat OPC artifact: its rels files declare the xmlns:pkg="http://schemas.microsoft.com/office/2006/xmlPackage" namespace, which is never used in the document content but is a remnant of the Flat OPC export step. No other .docx in the test suite has this artifact. The outer file and the embedded .xlsx and .pptx are clean.

The artifact is harmless to parsing, and the OOXML structure of the embedded objects (the v:imagedata / o:OLEObject / package relationship pattern) matches what Word produces. However, the fixture cannot be opened and re-saved in Word to verify round-trip fidelity, and it is not clear that the inner Word document as packed would survive being opened by Word.

The concern is practical: the test exercises the code path, but it does not confirm that the feature works on files that real users create and share. It would strengthen the test base to include a fixture that was saved directly by Word (or LibreOffice, which also supports Insert→OLE Object). A file that Word can open and re-save is a much stronger confidence signal than one produced by an unusual automation path.

RichTableCell.ref remapping does not guard against a missing key

_remap_refs silently drops any reference whose target is not in copies. The corresponding line for RichTableCell:

cell.ref = copies[cell.ref.cref].get_ref()

does not have this guard and raises KeyError if cell.ref.cref is absent from copies. Today this cannot happen because RichTableCell.ref always points to a group created as a direct child of the table, and the table is always a body item. But the asymmetry with _remap_refs is a latent risk if any future backend produces a RichTableCell with a ref outside the body tree.

Assertions in runtime code

First of all, there is an issue with the position of the assertion assert isinstance(backend, DeclarativeDocumentBackend). The assertion is inside the try block whose except Exception handler logs a warning and returns None. An AssertionError would be silently swallowed, hiding a programming error as a benign "cannot be converted" warning.
In any case, we should avoid using assertions in the runtime code because they are ignore in optimized python environments.

Test functions and files

I don't think we should create a new test module for this feature, as we shouldn't in general for any new feature or fix that is suggested in a PR. Otherwise the test suite will become unmanageable and inefficient (similar edge cases get dispersed, fixtures are not reused...).

Regarding the location of the test file, I don't see the issue in adding it in tests/data/docx/sources and let the end-to-end pipeline include it. Actually this would be helpful to see how the conversion is rendered in Markdown for instance.

…objects

Signed-off-by: Daniel Nguyen <danielnguyenh07@gmail.com>
The merge of main through the GitHub editor left whitespace on four blank
lines, and the lint job failed.

Signed-off-by: Daniel Nguyen <danielnguyenh07@gmail.com>
A RichTableCell of an embedded document raised KeyError when its target
was not in the copied tree. Now the cell becomes a plain TableCell that
keeps its text, the same as _remap_refs drops a reference whose target
was not copied.

Signed-off-by: Daniel Nguyen <danielnguyenh07@gmail.com>
An embedded package was opened without size limits, and the PowerPoint
backend does not check archive limits. The number of embedded objects
and the nesting depth were also fixed in the code.

- Each embedded package must pass the archive limits of the Word file
  before any backend opens it.
- New MsWordBackendOptions fields: max_embedded_objects (32),
  max_embedded_total_bytes (512 MiB) and max_embedding_depth (3). The
  count and the size are shared by all nesting levels.
- An embedded Word document is converted with the same options, so its
  own objects are converted until max_embedding_depth.
- The backend type is checked without an assertion, outside the try
  blocks, as SimplePipeline does.

Signed-off-by: Daniel Nguyen <danielnguyenh07@gmail.com>
- Replace the fixture with a file that Microsoft Word saved, and move it
  to tests/data/docx/sources. The generic e2e test converts it with
  process_embedded_objects=True and compares the reference files.
- Move the tests into tests/test_backend_msword.py and remove the
  separate module.
- Add tests for the limits over nesting levels, a zip bomb, a broken
  package, a linked object and a rich table cell whose target is not
  copied.

Signed-off-by: Daniel Nguyen <danielnguyenh07@gmail.com>
@github-actions github-actions Bot added ai:ci-safe AI triage found no CI-safety concern (advisory) and removed ai:ci-safe AI triage found no CI-safety concern (advisory) labels Oct 26, 2026
@DanielNg0729

Copy link
Copy Markdown
Member Author

Thank you very much @ceberam, for the careful review and for the time you spent on it. I pushed new commits that I hope address your points, and I add my answers to your questions below. I may be wrong on the contract questions, so please feel free to change any of them.

Motivation. My goal is to get the most out of a document. When a report embeds a workbook, a slide or another Word document, its content is lost today, so a RAG pipeline never sees it. I found #2051 when I looked at this gap. Thank you for closing it; I understand that its author did not reply.

About your question:

  1. Which embedded documents? I think that, in the long term, Docling should convert any embedded document, PDF files included. This PR converts only the OOXML packages (.docx, .xlsx, .pptx), because Word stores them as complete packages that the declarative backends already read, with no new dependency. Word stores a PDF or any other file as a binary OLE compound file (oleObject1.bin, for example AcroExch.Document or Package). That needs a compound file reader such as olefile, and a PDF also needs a pipeline, which a backend cannot start because docling.backend must not import docling.document_converter. Would it be acceptable to do this in a follow-up, after we agree on the contract?

  2. How many and how deep? Thank you for this point. The limits are now explicit options instead of a hard-coded depth:

    • max_embedded_objects (default 32) and max_embedded_total_bytes (default 512 MiB of decompressed data) are shared by all nesting levels, so an embedded Word document uses the budget of its parent.
    • max_embedding_depth (default 3) is the deepest level that is converted. An embedded Word document is converted with the same options, so a user can set it to 1 to keep the old behaviour.
    • An object that goes above a limit keeps only its preview picture, and a warning is logged.
  3. Other formats? I agree that the contract should be the same for every format that can embed documents. PowerPoint, Excel and OpenDocument files embed OLE objects in the same way. If you agree, I can add them one backend per PR, with the same option names.

The points on the implementation

  • Style check and conflict: I merged main. The style error came from my conflict resolution in the GitHub editor, which left whitespace on four blank lines. It is fixed now.
  • Zip bomb: you are right. Since fix: limit the decompressed size of DOCX, XLSX and EPUB archive members #4977, the Word and Excel backends check the archive limits, but the PowerPoint backend does not. Now every embedded package must pass the archive limits of the Word file (max_total_bytes, max_file_bytes, max_member_count) before any backend opens it. A test embeds a crafted .pptx that decompresses to 2 MiB of zeros and sets max_total_bytes to 1 MiB. The package is refused before the PowerPoint backend reads it.
  • Fixture: thank you for finding the xmlns:pkg remnant. The fixture is now saved by Word 365 itself. I saved the inner Word document again in Word, then I opened the fixture in Word and saved it. Word opens the result, saves it again, and keeps the embedded parts byte for byte. No part has the Flat OPC remnant now. The objects were inserted with InlineShapes.AddOLEObject, which is the same action as Insert > Object > Create from File.
  • RichTableCell.ref: a cell whose target was not copied now becomes a plain TableCell that keeps its text, the same as _remap_refs does. A test covers it.
  • Assertions: I removed the assertion. The backend type is now checked outside the try blocks and raises a RuntimeError, as SimplePipeline does. The try blocks wrap only the opening and the conversion of the embedded document.
  • Tests: I removed the new test module and moved the tests into tests/test_backend_msword.py. The fixture is now in tests/data/docx/sources/ with its reference files. The e2e test converts this file with process_embedded_objects=True, so the Markdown reference shows the embedded content.

Thank you again for your guidance. Please let me know if you prefer other defaults or another approach. I am happy to change it.

This branch has not been deployed

No deployments
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) docx issue related to docx backend enhancement New feature or request pptx issue related to pptx backend table structure xlsx issue related to xlsx backend

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants