Coverage for src / quber / core / figures / geometry.py: 86%
29 statements
« prev ^ index » next coverage.py v7.14.0, created at 2026-09-23 22:14 -0400
« prev ^ index » next coverage.py v7.14.0, created at 2026-09-23 22:14 -0400
1"""Boxes, in the three frames the scan workflow has to speak.
3A scan returns a box as `{left, top, right, bottom}`, each 0..1 with the page's
4top-left as origin. The page it scanned was the real page, so that box is already
5a source coordinate and needs no mapping back.
7The parse states a box in PDF points, usually with the page's bottom-left as
8origin. The table pipeline states a box in PDF points bottom-left too, and states
9a region normalized top-left.
11These convert between them so no caller writes the arithmetic twice.
12"""
14from __future__ import annotations
16from typing import Optional, Tuple
18from docling_core.types.doc.base import CoordOrigin
19from docling_core.types.doc.document import DocItem
21from quber.core.extractors.camelot.correspondence.geometry import camelot_bbox_to_norm
22from quber.core.figures.models import Box
24#: A box as (x1, y1, x2, y2), 0..1 with the page's top-left as origin.
25NormBox = Tuple[float, float, float, float]
27#: A box as (x1, y1, x2, y2) in PDF points with the page's bottom-left as origin.
28PointBox = Tuple[float, float, float, float]
31def norm_box(box: Optional[Box]) -> Optional[NormBox]:
32 """A returned box (0..1, top-left origin, keyed left/top/right/bottom) as a tuple."""
33 if not box:
34 return None
35 try:
36 left, top, right, bottom = box["left"], box["top"], box["right"], box["bottom"]
37 except KeyError:
38 return None
39 return (min(left, right), min(top, bottom), max(left, right), max(top, bottom))
42def point_box(box: Optional[Box], width: float, height: float) -> Optional[PointBox]:
43 """A returned box as PDF points with the page's bottom-left as origin.
45 That is the frame the table pipeline measures in, so a cell box read off a
46 scan lands in the same frame as one Camelot measured.
47 """
48 normalized = norm_box(box)
49 if normalized is None:
50 return None
51 x1, y1, x2, y2 = normalized
52 return (x1 * width, (1.0 - y2) * height, x2 * width, (1.0 - y1) * height)
55def prov_box(item: DocItem, width: float, height: float) -> Optional[NormBox]:
56 """An element's provenance box in the normalized top-left frame."""
57 if not item.prov:
58 return None
59 bbox = item.prov[0].bbox
60 if bbox.coord_origin == CoordOrigin.TOPLEFT:
61 return (bbox.l / width, bbox.t / height, bbox.r / width, bbox.b / height)
62 return camelot_bbox_to_norm((bbox.l, bbox.b, bbox.r, bbox.t), width, height)