Coverage for src / quber / files / pdf.py: 72%

32 statements  

« prev     ^ index     » next       coverage.py v7.14.0, created at 2026-09-23 22:14 -0400

1"""Page-level operations on a source PDF. 

2 

3`slice_page` writes one page out as its own one-page PDF. `render_page` 

4rasterizes one page to a PNG. `render_region` rasterizes one rectangle of a page. 

5All three address pages 1-based, the numbering the document artifacts and the CLI 

6use. 

7 

8Single-page work has two callers that pay per page: an ADE submission bills by 

9the page submitted, so a one-page slice bills one page's credits instead of the 

10whole document's, and a vision agent looking at one page needs that page and no 

11other. 

12 

13A region is rendered when an element's picture has to be re-cut because its 

14boundary moved: the document model stores a bitmap of each detected picture, and 

15a bitmap of the old boundary would depict something the element no longer is. 

16""" 

17 

18from __future__ import annotations 

19 

20from pathlib import Path 

21from typing import TYPE_CHECKING, Tuple 

22 

23import fitz 

24 

25if TYPE_CHECKING: 

26 from PIL.Image import Image 

27 

28#: A normalized rectangle on a page: 0..1 with the page's top-left as origin, 

29#: ordered (left, top, right, bottom). 

30NormBox = Tuple[float, float, float, float] 

31 

32 

33def slice_page(pdf: Path, page: int, dest: Path) -> Path: 

34 """Write the single 1-based `page` of `pdf` to `dest` and return it.""" 

35 with fitz.open(str(pdf)) as src: 

36 if not 1 <= page <= src.page_count: 

37 raise ValueError(f"page {page} out of range: {pdf.name} has {src.page_count} pages") 

38 with fitz.open() as sliced: 

39 sliced.insert_pdf(src, from_page=page - 1, to_page=page - 1) 

40 sliced.save(str(dest)) 

41 return dest 

42 

43 

44def render_page(pdf: Path, page: int, dpi: int, dest: Path) -> Tuple[Path, float, float]: 

45 """Rasterize the 1-based `page` of `pdf` to `dest`; return it with the page size. 

46 

47 The size is the page's width and height in PDF points, so a caller can map a 

48 normalized box back onto the source page. 

49 """ 

50 with fitz.open(str(pdf)) as doc: 

51 if not 1 <= page <= doc.page_count: 

52 raise ValueError(f"page {page} out of range: {pdf.name} has {doc.page_count} pages") 

53 pg = doc[page - 1] 

54 width, height = pg.rect.width, pg.rect.height 

55 pg.get_pixmap(dpi=dpi).save(str(dest)) 

56 return dest, width, height 

57 

58 

59def render_region(pdf: Path, page: int, box: NormBox, dpi: int) -> "Image": 

60 """Rasterize the rectangle `box` of the 1-based `page` of `pdf` at `dpi`. 

61 

62 The box is normalized against the page's own rectangle rather than assumed to 

63 start at the origin, so a page whose media box is offset crops correctly. 

64 """ 

65 from PIL import Image as PILImage 

66 

67 with fitz.open(str(pdf)) as doc: 

68 if not 1 <= page <= doc.page_count: 

69 raise ValueError(f"page {page} out of range: {pdf.name} has {doc.page_count} pages") 

70 pg = doc[page - 1] 

71 rect = pg.rect 

72 left, top, right, bottom = box 

73 clip = fitz.Rect( 

74 rect.x0 + min(left, right) * rect.width, 

75 rect.y0 + min(top, bottom) * rect.height, 

76 rect.x0 + max(left, right) * rect.width, 

77 rect.y0 + max(top, bottom) * rect.height, 

78 ) 

79 pix = pg.get_pixmap(clip=clip, dpi=dpi) 

80 return PILImage.frombytes("RGB", (pix.width, pix.height), pix.samples)