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
« prev ^ index » next coverage.py v7.14.0, created at 2026-09-23 22:14 -0400
1"""Page-level operations on a source PDF.
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.
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.
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"""
18from __future__ import annotations
20from pathlib import Path
21from typing import TYPE_CHECKING, Tuple
23import fitz
25if TYPE_CHECKING:
26 from PIL.Image import Image
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]
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
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.
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
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`.
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
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)