import copy import logging import re from pathlib import Path from typing import Literal, Optional from docling.datamodel.base_models import BoundingBox, Cluster from docling.utils.visualization import draw_clusters from docling_core.transforms.serializer.html import ( HTMLDocSerializer, HTMLOutputStyle, HTMLParams, ) from docling_core.types.doc.document import ( ContentLayer, DocItem, DoclingDocument, ImageRefMode, KeyValueItem, ) from docling_core.types.doc.labels import DocItemLabel from PIL import Image, ImageDraw, ImageFont from docling_eval.utils.utils import from_pil_to_base64 from docling_eval.visualisation.constants import ( HTML_COMPARISON_PAGE, HTML_COMPARISON_PAGE_WITH_CLUSTERS, HTML_DEFAULT_HEAD_FOR_COMP, HTML_INSPECTION, HTML_DEFAULT_HEAD_FOR_COMP_v2, ) VizMode = Literal["reading_order", "key_value"] ALL_CONTENT_LAYERS: set[ContentLayer] = {layer for layer in ContentLayer} def _document_has_key_value_items(doc: DoclingDocument) -> bool: """Return True when the document includes at least one key-value annotation.""" for item, _ in doc.iterate_items( traverse_pictures=True, included_content_layers=ALL_CONTENT_LAYERS, ): if isinstance(item, KeyValueItem): return True return False def _visualization_filename_for_mode(base_filename: Path, viz_mode: VizMode) -> Path: """Derive the output filename for a visualization mode.""" if viz_mode == "reading_order": return base_filename.with_name( f"{base_filename.stem}_layout{base_filename.suffix}" ) if viz_mode == "key_value": return base_filename.with_name( f"{base_filename.stem}_key_value{base_filename.suffix}" ) raise ValueError(f"Unsupported visualization mode: {viz_mode}") def draw_arrow( draw: ImageDraw.ImageDraw, arrow_coords: tuple[float, float, float, float], line_width: int = 2, color: str = "red", ): r""" Draw an arrow inside the given draw object """ x0, y0, x1, y1 = arrow_coords # Arrow parameters start_point = (x0, y0) # Starting point of the arrow end_point = (x1, y1) # Ending point of the arrow arrowhead_length = 20 # Length of the arrowhead arrowhead_width = 10 # Width of the arrowhead # Draw the arrow shaft (line) draw.line([start_point, end_point], fill=color, width=line_width) # Calculate the arrowhead points dx = end_point[0] - start_point[0] dy = end_point[1] - start_point[1] angle = (dx**2 + dy**2) ** 0.5 + 0.01 # Length of the arrow shaft # Normalized direction vector for the arrow shaft ux, uy = dx / angle, dy / angle # Base of the arrowhead base_x = end_point[0] - ux * arrowhead_length base_y = end_point[1] - uy * arrowhead_length # Left and right points of the arrowhead left_x = base_x - uy * arrowhead_width left_y = base_y + ux * arrowhead_width right_x = base_x + uy * arrowhead_width right_y = base_y - ux * arrowhead_width # Draw the arrowhead (triangle) draw.polygon( [end_point, (left_x, left_y), (right_x, right_y)], fill=color, ) return draw def get_missing_pageimg( width: int = 800, height: int = 1100, text: str = "MISSING PAGE" ) -> Image.Image: """Get missing page image. Args: width: Image width height: Image height text: Text to display on the image Returns: PIL Image with the missing page text """ import numpy as np from PIL import Image, ImageDraw, ImageFont # Create a white background image image = Image.new("RGB", (width, height), color="white") draw = ImageDraw.Draw(image) # Try to use a standard font or fall back to default try: # For larger installations, you might have Arial or other fonts font = ImageFont.truetype("arial.ttf", size=60) except IOError: # Fall back to default font font = ImageFont.load_default().font_variant(size=60) # type: ignore # Get text size to center it text_width, text_height = ( draw.textsize(text, font=font) if hasattr(draw, "textsize") else (draw.textlength(text, font=font), font.size) ) # Position for the text (centered and angled) position = ((width - text_width) // 2, (height - text_height) // 2) # Draw the watermark text (light gray and rotated) draw.text(position, text, fill=(200, 200, 200), font=font) # Rotate the image 45 degrees to create diagonal watermark effect image = image.rotate(45, expand=False, fillcolor="white") return image def _get_document_visualization_data( doc: DoclingDocument, page_no: int, pattern: re.Pattern[str], viz_mode: VizMode, ) -> tuple[str, str]: """Get visualization data for a document page. Args: doc: Document to visualize page_no: Page number to visualize pattern: Regex pattern to extract body content Returns: Tuple of (base64_image, html_content) """ visualization_error: Optional[str] = None try: page_imgs = doc.get_visualization(show_label=False, viz_mode=viz_mode) except (IndexError, ValueError) as exc: visualization_error = str(exc) logging.error( "Could not render visualization for page %s due to geometry issue: %s", page_no, exc, ) page_imgs = {} if page_no in page_imgs: doc_img_b64 = from_pil_to_base64(page_imgs[page_no]) else: if visualization_error is None: logging.error(f"{page_no} not in page_imgs, get default image.") doc_img_b64 = from_pil_to_base64(get_missing_pageimg()) try: doc_page = doc.export_to_html( image_mode=ImageRefMode.EMBEDDED, page_no=page_no, included_content_layers={ContentLayer.BODY, ContentLayer.FURNITURE}, ) except (IndexError, ValueError) as exc: logging.error( "Could not export page %s to HTML due to serialization error: %s", page_no, exc, ) doc_page_body = ( "
ERROR: Could not render page HTML due to serialization issues. " f"Details: {exc}
" ) return doc_img_b64, doc_page_body # Search for the pattern in the HTML string mtch = pattern.search(doc_page) if mtch: doc_page_body = mtch.group(1).strip() else: logging.error(f"could not find body in doc_page") doc_page_body = "Nothing Found
" if len(doc_page_body) == 0: doc_page_body = "Nothing Found
" if visualization_error is not None: doc_page_body = ( "WARNING: Page visualization unavailable due to geometry issues. " f"Details: {visualization_error}
\n" + doc_page_body ) return doc_img_b64, doc_page_body def _create_visualization_html( filename: Path, true_doc: DoclingDocument, template: str, pred_doc: Optional[DoclingDocument] = None, draw_reading_order: bool = True, viz_mode: VizMode = "reading_order", ) -> None: """Create and save HTML visualization. Args: filename: Path to save the visualization true_doc: Document to visualize template: HTML template to use pred_doc: Optional predicted document for comparison draw_reading_order: Whether to draw reading order """ # Compile regex pattern once pattern = re.compile( r"]*>\n