Table Cell Formatting in Docling#
Docling's internal representation of a table cell is the TableCell Pydantic model in docling_core/types/doc/document.py. It stores semantic and structural metadata about each cell, but does not store visual/style attributes such as background color, borders, or horizontal/vertical alignment. These properties are silently discarded during ingestion across all current backends.
Data Model: TableCell and RichTableCell#
TableCell is the base model. Its fields are:
| Field | Type | Purpose |
|---|---|---|
text | str | Plain-text cell content |
bbox | Optional[BoundingBox] | Page-level bounding box (provenance) |
row_span / col_span | int | Merge extent |
start_row_offset_idx / end_row_offset_idx / start_col_offset_idx / end_col_offset_idx | int | Span indices into the grid |
column_header | bool | Marks cells in the header row |
row_header | bool | Marks cells in a row-header column |
row_section | bool | Marks section-divider rows |
fillable | bool | Semantic flag: cell is meant to be filled in |
RichTableCell subclasses TableCell and adds a single extra field:
ref: RefItem— a JSON-pointer reference to an arbitraryDocItem(e.g., a nestedPictureItem,TableItem, or anInlineGroupof formatted text). When text is needed,_get_text()resolves the reference and delegates toMarkdownDocSerializer. Without adoccontext, it falls back to"<!-- rich cell -->".
The union type AnyTableCell = Union[RichTableCell, TableCell] is used throughout the codebase. Pydantic resolves it left-to-right, so a cell with a ref field is always deserialized as RichTableCell.
TableData.table_cells holds the flat list of AnyTableCell; the computed grid property reconstructs the full 2-D num_rows × num_cols matrix, filling span positions with a reference to the originating cell.
Text Formatting Within Cells (DOCX backend)#
The Formatting model — bold, italic, underline, strikethrough, script — lives on TextItem, not on TableCell. For the DOCX backend, cell text runs are inspected via _get_format_from_run() to determine if they carry any non-default Formatting. When they do, the cell is promoted to a RichTableCell and the formatted text items are stored as children referenced by RichTableCell.ref.
The _is_rich_table_cell() gating logic classifies a cell as "rich" if it has:
- More than one paragraph
- Non-paragraph/non-
tcPrblock-level elements (e.g., tables, images) - Embedded images (BLIP elements)
- Runs with non-default formatting (bold, italic, etc.)
Plain-text cells — a single paragraph with unstyled runs — are stored as bare TableCell for efficiency .
Cell-Level Style Properties: What Is Not Stored#
The TableCell model carries no fields for:
- Horizontal or vertical alignment (OOXML
tcPr/jc,tcPr/vAlign) - Background / fill color (OOXML
tcPr/shd) - Cell borders (OOXML
tcPr/tcBorders) - Font size, font family, or text color within a cell
The DOCX backend reads tcPr only to filter it out during rich-cell detection ; it does not extract any of its properties into the data model. This gap is consistent across all other backends as well — alignment and styling metadata from the source document is not preserved in the DoclingDocument representation.
Known Bugs and Feature Gaps#
AsciiDoc backend crash on cell format specifiers : The AsciiDoc backend's _is_table_line method used re.match(r"^\|.*\|", line), which does not match rows beginning with AsciiDoc cell specifiers such as alignment (<, ^, >), vertical alignment (.^), style (a/d/e/h/l/m/s), or span/duplication (2+, 3*). For example, ^.^h|Field ^.^h|Description was not recognized as a table row, causing the table to be treated as empty and max() over an empty iterable to raise ValueError. Fix proposed in PR #3647 (broadened regex + empty-table guard).
RichTableCell images in table cells : Images embedded in table cells are accessible via RichTableCell.ref, but there is no dedicated get_cell_image() API on TableItem. PR #652 (open as of 2026-07) proposes adding this helper.
Table images in Markdown export : MarkdownTableSerializer does not respect image_mode — table images are never embedded even when generate_table_images=True. A workaround is to crop the page image manually using TableItem.prov[0].bbox.
DOCX gridBefore rows silently drop cells: Tables with w:gridBefore (rows starting late due to merged leading cells) can cause an IndexError caught only at DEBUG level ("broken docx table"), or silently place cells in the wrong row due to positional indexing against variable-length row tuples. See _handle_tables().
Key Source Files#
| File | Relevance |
|---|---|
docling_core/types/doc/document.py | TableCell, RichTableCell, TableData, Formatting definitions |
docling/backend/msword_backend.py | DOCX table cell parsing, _is_rich_table_cell, _handle_tables |
docling_core/transforms/serializer/markdown.py | MarkdownTableSerializer (used by RichTableCell._get_text) |