from __future__ import annotations import math from pathlib import Path from types import ModuleType from typing import Any from .errors import InferenceError class PDFPageRenderer: """Bounded, page-at-a-time PDFium renderer.""" def __init__( self, pdf_path: Path, work_dir: Path, *, dpi: int, start_page: int, max_pages: int, max_page_pixels: int, max_total_pixels: int, max_page_rendered_bytes: int, max_rendered_bytes: int, ) -> None: self.pdf_path = pdf_path self.work_dir = work_dir self.dpi = dpi self.start_page = start_page self.max_pages = max_pages self.max_page_pixels = max_page_pixels self.max_total_pixels = max_total_pixels self.max_page_rendered_bytes = max_page_rendered_bytes self.max_rendered_bytes = max_rendered_bytes self._pdfium: ModuleType | None = None self._document: Any = None self.total_pages = 0 self._rendered_pixels = 0 self._rendered_bytes = 0 def __enter__(self) -> PDFPageRenderer: try: import pypdfium2 as pdfium except ImportError as exc: raise InferenceError("pypdfium2 is missing; reinstall unlimited-ocr-rdna4") from exc self._pdfium = pdfium try: self._document = pdfium.PdfDocument(str(self.pdf_path)) self.total_pages = len(self._document) except Exception as exc: raise InferenceError(f"could not open PDF {self.pdf_path}: {exc}") from exc if self.total_pages < 1: self.close() raise InferenceError(f"PDF has no pages: {self.pdf_path}") if self.start_page > self.total_pages: self.close() raise InferenceError(f"start page {self.start_page} exceeds PDF page count {self.total_pages}") return self def __exit__(self, _exc_type: object, _exc: object, _traceback: object) -> None: self.close() @property def page_numbers(self) -> range: last_page = min(self.total_pages, self.start_page - 1 + self.max_pages) return range(self.start_page, last_page + 1) def render(self, page_number: int) -> Path: if self._document is None: raise InferenceError("PDF renderer is not open") page = None bitmap = None image = None output = self.work_dir / f"page-{page_number:06d}.png" try: page = self._document[page_number - 1] width_points, height_points = page.get_size() scale = self.dpi / 72 width_pixels = math.ceil(width_points * scale) height_pixels = math.ceil(height_points * scale) pixels = width_pixels * height_pixels if pixels <= 0 or pixels > self.max_page_pixels: raise InferenceError( f"PDF page {page_number} would render {pixels:,} pixels; limit is {self.max_page_pixels:,}" ) if self._rendered_pixels + pixels > self.max_total_pixels: raise InferenceError( f"PDF rendered-pixel total would exceed {self.max_total_pixels:,} on page {page_number}" ) bitmap = page.render(scale=scale) image = bitmap.to_pil() image.save(output, format="PNG", optimize=True) rendered_bytes = output.stat().st_size if rendered_bytes > self.max_page_rendered_bytes: output.unlink(missing_ok=True) raise InferenceError( f"PDF page {page_number} rendered to {rendered_bytes:,} bytes; " f"limit is {self.max_page_rendered_bytes:,}" ) if self._rendered_bytes + rendered_bytes > self.max_rendered_bytes: output.unlink(missing_ok=True) raise InferenceError( f"PDF rendered-byte total would exceed {self.max_rendered_bytes:,} on page {page_number}" ) self._rendered_pixels += pixels self._rendered_bytes += rendered_bytes return output except InferenceError: raise except Exception as exc: output.unlink(missing_ok=True) raise InferenceError(f"could not render PDF page {page_number}: {exc}") from exc finally: if image is not None: image.close() if bitmap is not None: bitmap.close() if page is not None: page.close() def close(self) -> None: if self._document is not None: self._document.close() self._document = None