Skip to content

feat: Visualizer for table cells #325

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 4 commits into from
Jun 11, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 8 additions & 4 deletions docling_core/transforms/visualizer/layout_visualizer.py
Original file line number Diff line number Diff line change
Expand Up @@ -119,7 +119,10 @@ def _draw_clusters(
)

def _draw_doc_layout(
self, doc: DoclingDocument, images: Optional[dict[Optional[int], Image]] = None
self,
doc: DoclingDocument,
images: Optional[dict[Optional[int], Image]] = None,
included_content_layers: Optional[set[ContentLayer]] = None,
):
"""Draw the document clusters and optionaly the reading order."""
clusters = []
Expand All @@ -128,6 +131,9 @@ def _draw_doc_layout(
if images is not None:
my_images = images

if included_content_layers is None:
included_content_layers = {c for c in ContentLayer}

# Initialise `my_images` beforehand: sometimes, you have the
# page-images but no DocItems!
for page_nr, page in doc.pages.items():
Expand All @@ -141,9 +147,7 @@ def _draw_doc_layout(
prev_image = None
prev_page_nr = None
for idx, (elem, _) in enumerate(
doc.iterate_items(
included_content_layers={ContentLayer.BODY, ContentLayer.FURNITURE}
)
doc.iterate_items(included_content_layers=included_content_layers)
):
if not isinstance(elem, DocItem):
continue
Expand Down
135 changes: 135 additions & 0 deletions docling_core/transforms/visualizer/table_visualizer.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,135 @@
"""Define classes for layout visualization."""

import logging
from copy import deepcopy
from typing import Optional

from PIL import ImageDraw
from PIL.Image import Image
from pydantic import BaseModel
from typing_extensions import override

from docling_core.transforms.visualizer.base import BaseVisualizer
from docling_core.types.doc.document import ContentLayer, DoclingDocument, TableItem

_log = logging.getLogger(__name__)


class TableVisualizer(BaseVisualizer):
"""Table visualizer."""

class Params(BaseModel):
"""Table visualization parameters."""

# show_Label: bool = False
show_cells: bool = True
# show_rows: bool = False
# show_cols: bool = False

base_visualizer: Optional[BaseVisualizer] = None
params: Params = Params()

def _draw_table_cells(
self,
table: TableItem,
page_image: Image,
page_height: float,
scale_x: float,
scale_y: float,
):
"""Draw individual table cells."""
draw = ImageDraw.Draw(page_image, "RGBA")

for cell in table.data.table_cells:
if cell.bbox is not None:

tl_bbox = cell.bbox.to_top_left_origin(page_height=page_height)

cell_color = (256, 0, 0, 32) # Transparent black for cells

cx0, cy0, cx1, cy1 = tl_bbox.as_tuple()
cx0 *= scale_x
cx1 *= scale_x
cy0 *= scale_y
cy1 *= scale_y

draw.rectangle(
[(cx0, cy0), (cx1, cy1)],
outline=(256, 0, 0, 128),
fill=cell_color,
)

def _draw_doc_tables(
self,
doc: DoclingDocument,
images: Optional[dict[Optional[int], Image]] = None,
included_content_layers: Optional[set[ContentLayer]] = None,
):
"""Draw the document tables."""
my_images: dict[Optional[int], Image] = {}

if images is not None:
my_images = images

Check warning on line 72 in docling_core/transforms/visualizer/table_visualizer.py

View check run for this annotation

Codecov / codecov/patch

docling_core/transforms/visualizer/table_visualizer.py#L72

Added line #L72 was not covered by tests

if included_content_layers is None:
included_content_layers = {c for c in ContentLayer}

# Initialise `my_images` beforehand: sometimes, you have the
# page-images but no DocItems!
for page_nr, page in doc.pages.items():
page_image = doc.pages[page_nr].image
if page_image is None or (pil_img := page_image.pil_image) is None:
raise RuntimeError("Cannot visualize document without images")

Check warning on line 82 in docling_core/transforms/visualizer/table_visualizer.py

View check run for this annotation

Codecov / codecov/patch

docling_core/transforms/visualizer/table_visualizer.py#L82

Added line #L82 was not covered by tests
elif page_nr not in my_images:
image = deepcopy(pil_img)
my_images[page_nr] = image

for idx, (elem, _) in enumerate(
doc.iterate_items(included_content_layers=included_content_layers)
):
if not isinstance(elem, TableItem):
continue
if len(elem.prov) == 0:
continue # Skip elements without provenances

Check warning on line 93 in docling_core/transforms/visualizer/table_visualizer.py

View check run for this annotation

Codecov / codecov/patch

docling_core/transforms/visualizer/table_visualizer.py#L93

Added line #L93 was not covered by tests

if len(elem.prov) == 1:

page_nr = elem.prov[0].page_no

if page_nr in my_images:
image = my_images[page_nr]

if self.params.show_cells:
self._draw_table_cells(
table=elem,
page_height=doc.pages[page_nr].size.height,
page_image=image,
scale_x=image.width / doc.pages[page_nr].size.width,
scale_y=image.height / doc.pages[page_nr].size.height,
)

else:
raise RuntimeError(f"Cannot visualize page-image for {page_nr}")

Check warning on line 112 in docling_core/transforms/visualizer/table_visualizer.py

View check run for this annotation

Codecov / codecov/patch

docling_core/transforms/visualizer/table_visualizer.py#L112

Added line #L112 was not covered by tests

else:
_log.error("Can not yet visualise tables with multiple provenances")

Check warning on line 115 in docling_core/transforms/visualizer/table_visualizer.py

View check run for this annotation

Codecov / codecov/patch

docling_core/transforms/visualizer/table_visualizer.py#L115

Added line #L115 was not covered by tests
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As this does not stop the execution, I would rather use a warning instead of error.


return my_images

@override
def get_visualization(
self,
*,
doc: DoclingDocument,
**kwargs,
) -> dict[Optional[int], Image]:
"""Get visualization of the document as images by page."""
base_images = (
self.base_visualizer.get_visualization(doc=doc, **kwargs)
if self.base_visualizer
else None
)
return self._draw_doc_tables(
doc=doc,
images=base_images,
)
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
14 changes: 14 additions & 0 deletions test/test_visualization.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

import PIL.Image

from docling_core.transforms.visualizer.table_visualizer import TableVisualizer
from docling_core.types.doc.document import DoclingDocument

from .test_data_gen_flag import GEN_TEST_DATA
Expand Down Expand Up @@ -52,3 +53,16 @@ def test_doc_visualization_no_label():
exp_file=VIZ_TEST_DATA_PATH / f"{src.stem}_viz_wout_lbl_p{k}.png",
actual=viz_pages[k],
)


def test_table_visualization_no_label():
src = Path("./test/data/doc/2408.09869v3_enriched.json")
doc = DoclingDocument.load_from_json(src)

visualizer = TableVisualizer()
viz_pages = visualizer.get_visualization(doc=doc)

verify(
exp_file=VIZ_TEST_DATA_PATH / f"{src.stem}_table_viz_wout_lbl_p5.png",
actual=viz_pages[5],
)
Loading