dougvk's picture
Publish Unlimited-OCR RDNA 4 runtime v0.1.0
f340984 verified
Raw
History Blame Contribute Delete
4.78 kB
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