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

1"""Boxes, in the three frames the scan workflow has to speak. 

2 

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. 

6 

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. 

10 

11These convert between them so no caller writes the arithmetic twice. 

12""" 

13 

14from __future__ import annotations 

15 

16from typing import Optional, Tuple 

17 

18from docling_core.types.doc.base import CoordOrigin 

19from docling_core.types.doc.document import DocItem 

20 

21from quber.core.extractors.camelot.correspondence.geometry import camelot_bbox_to_norm 

22from quber.core.figures.models import Box 

23 

24#: A box as (x1, y1, x2, y2), 0..1 with the page's top-left as origin. 

25NormBox = Tuple[float, float, float, float] 

26 

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] 

29 

30 

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)) 

40 

41 

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. 

44 

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) 

53 

54 

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)