ODF Backend Processing#
docling/backend/opendocument_backend.py implements Docling's OpenDocument Format (ODF) parsing layer for .odt, .ods, and .odp files. It uses the odfdo library to traverse the underlying XML and produce a DoclingDocument. The backend was introduced in PR #3480.
All three format-specific classes inherit from the shared _OdfBaseBackend, which handles document loading, type validation, and resource cleanup. The base class is itself a DeclarativeDocumentBackend, so it is driven by SimplePipeline via a single convert() call — no page-level recognition models are involved .
| Format | Class | _odf_type |
|---|---|---|
| ODT | OdtDocumentBackend | "text" |
| ODS | OdsDocumentBackend | "spreadsheet" |
| ODP | OdpDocumentBackend | "presentation" |
Install: pip install 'docling-slim[format-opendocument]' (requires odfdo>=3.22.0,<4.0.0) .
Element Dispatch: _add_odf_child#
The central dispatch function is _add_odf_child. It inspects the runtime type of an element and routes it to the appropriate handler:
| Element type | Handler |
|---|---|
Header | _add_odf_heading — extracts text:outline-level, emits a heading node |
Paragraph (non-Header) | _add_odf_paragraph — handles Title/Subtitle styles, images, charts, and plain text |
OdfList | _add_odf_list — recurses into nested lists (see below) |
OdfTable | _add_table_from_odf |
Frame | _add_odf_charts + _add_odf_images |
| Everything else | Falls through to a get_images() probe; if absent, logs a debug-level "Ignoring ODF element" message |
The final else branch is the key gap: ODF elements not listed above — most notably text:section — are silently skipped. Their children are not recursed into; only a top-level get_images() call is attempted. This means any text, headings, or lists nested inside a text:section will be lost.
Recursive Container Traversal (ODT)#
OdtDocumentBackend.convert() starts traversal at self.odf_obj.body.children, passing the list to _walk.
_walk iterates the element list and distinguishes two cases :
OdfList— forwarded to_add_odf_listwith the previous list's state (continued_state) so that consecutive lists that share a structural context can be merged correctly.- Everything else — forwarded to
_add_odf_child. Theprevious_list_stateis reset toNone, ending any in-progress list merge.
_walk is flat — it does not recurse into container elements like text:section. If such elements appear directly under body, their children are not walked; they are dispatched to _add_odf_child, which hits the silent-ignore branch described in the previous section.
List continuation#
_add_odf_list tracks a _OdfListState dataclass across consecutive OdfList elements. When the next list starts with an empty nested item and there is a pending state, the backend treats it as a continuation of the previous list rather than starting a new list_group node .
Per-Format Variations#
ODP (Presentation)#
OdpDocumentBackend is also a PaginatedDocumentBackend — it exposes page_count() and iterates draw:page elements. Each slide becomes a GroupLabel.CHAPTER group . _walk_slide skips anim:par and presentation:notes tags, then routes Frame elements through _walk_slide_frame and other elements through _walk_textbox_children. Title detection uses presentation:class="title" or first-text-content heuristics .
ODS (Spreadsheet)#
OdsDocumentBackend is also paginated; each sheet is a page . Its key feature is _find_data_tables_in_sheet, which runs a flood-fill algorithm with configurable gap_tolerance to detect multiple disconnected table regions within a single sheet. Hidden sheets (detected via table:display="false") are emitted on ContentLayer.INVISIBLE . OdsBackendOptions exposes three tunables: treat_singleton_as_text, gap_tolerance, and sheet_names .
Rich table cells (ODT / ODP)#
When an ODT table cell contains images, nested lists, multiple paragraphs, or nested tables, it is classified as rich via _odf_cell_is_rich. A GroupLabel.UNSPECIFIED group is added to the document tree and the cell is emitted as a RichTableCell referencing that group, rather than a plain TableCell .
Text Run & Formatting Extraction#
Inline text is extracted as a list of _OdfTextRun objects (text + optional Formatting). _odf_text_runs walks element children recursively, resolving text:style-name attributes through _formatting_from_odf_text_style, which reads fo:font-weight, fo:font-style, style:text-underline-style, style:text-line-through-*, and style:text-position (bold, italic, underline, strikethrough, super/subscript). text:line-break and text:tab are handled as special leaf cases .
_normalize_odf_text_runs merges adjacent runs with identical formatting and strips leading/trailing whitespace. When multiple runs exist for a single element, _add_odf_text_runs wraps them in an inline_group node so per-run formatting is preserved .
Known gap: Rich text styling (beyond bold/italic/underline/strikethrough/script) is not fully preserved. Ordered-list markers for ODP and ODS spreadsheet rendering fidelity are also incomplete .
Image & Chart Handling#
Bitmap images are extracted via _image_ref_from_odf_image, which tries in order: image.get_data(), odf_obj.get_part(href), and direct filesystem path. SVG, EMF, WMF, and PDF attachments are skipped . Successful images are added as PictureItem nodes via _add_odf_images.
Embedded charts (ODF draw:object inside a Frame) are extracted by _add_odf_charts, which reads the chart's embedded content.xml, identifies the chart type from chart:class attributes, and stores it as a PictureItem with PictureMeta carrying both a PictureClassificationPrediction and TabularChartMetaField (containing the chart's underlying data table). Supported classifications map chart:bar, chart:line, chart:circle/chart:pie, and chart:scatter to DocItemLabel picture classification labels .