Spaces:
Sleeping
Sleeping
| from __future__ import annotations | |
| import re | |
| import shutil | |
| import tempfile | |
| import time | |
| from dataclasses import dataclass | |
| from pathlib import Path | |
| from typing import Iterable | |
| import fitz | |
| from PIL import Image, ImageOps | |
| Image.MAX_IMAGE_PIXELS = 80_000_000 | |
| SUPPORTED_IMAGE_SUFFIXES = { | |
| ".png", | |
| ".jpg", | |
| ".jpeg", | |
| ".webp", | |
| ".bmp", | |
| ".tif", | |
| ".tiff", | |
| } | |
| class DocumentPage: | |
| page_number: int | |
| image_path: Path | |
| class DocumentError(ValueError): | |
| pass | |
| def cleanup_stale_runs(max_age_hours: float = 12.0) -> None: | |
| """Remove old gateway work directories and archives from the ephemeral disk.""" | |
| cutoff = time.time() - max(0.5, float(max_age_hours)) * 3600 | |
| root = Path(tempfile.gettempdir()) | |
| for pattern in ("ocr_workbench_*", "ocr_workbench_*_ocr_result.zip"): | |
| for path in root.glob(pattern): | |
| try: | |
| if path.stat().st_mtime >= cutoff: | |
| continue | |
| if path.is_dir(): | |
| shutil.rmtree(path, ignore_errors=True) | |
| else: | |
| path.unlink(missing_ok=True) | |
| except OSError: | |
| continue | |
| def normalize_gradio_path(value: object) -> str: | |
| if value is None: | |
| return "" | |
| if isinstance(value, str): | |
| return value | |
| path = getattr(value, "name", None) | |
| return str(path or value) | |
| def parse_page_selection(selection: str, total_pages: int, max_pages: int) -> list[int]: | |
| if total_pages < 1: | |
| return [] | |
| max_pages = max(1, int(max_pages)) | |
| text = (selection or "").strip() | |
| if not text: | |
| return list(range(min(total_pages, max_pages))) | |
| selected: set[int] = set() | |
| for token in re.split(r"\s*,\s*", text): | |
| if not token: | |
| continue | |
| if "-" in token: | |
| match = re.fullmatch(r"(\d+)\s*-\s*(\d+)", token) | |
| if not match: | |
| raise DocumentError(f"Invalid page range: {token!r}") | |
| start, end = map(int, match.groups()) | |
| if start > end: | |
| start, end = end, start | |
| selected.update(range(start, end + 1)) | |
| else: | |
| if not token.isdigit(): | |
| raise DocumentError(f"Invalid page number: {token!r}") | |
| selected.add(int(token)) | |
| invalid = sorted(page for page in selected if page < 1 or page > total_pages) | |
| if invalid: | |
| raise DocumentError( | |
| f"Page selection contains pages outside 1-{total_pages}: {invalid}" | |
| ) | |
| ordered = sorted(selected) | |
| if len(ordered) > max_pages: | |
| raise DocumentError( | |
| f"Selected {len(ordered)} pages, but the current limit is {max_pages}." | |
| ) | |
| return [page - 1 for page in ordered] | |
| def _save_normalized_image(input_path: Path, output_path: Path) -> None: | |
| try: | |
| with Image.open(input_path) as image: | |
| normalized = ImageOps.exif_transpose(image).convert("RGB") | |
| normalized.save(output_path, format="PNG", optimize=False) | |
| except Exception as exc: | |
| raise DocumentError(f"Unable to decode image: {exc}") from exc | |
| def prepare_document( | |
| source_path: str, | |
| *, | |
| dpi: int, | |
| selection: str, | |
| max_pages: int, | |
| max_input_mb: int, | |
| ) -> tuple[Path, list[DocumentPage]]: | |
| source = Path(source_path) | |
| if not source.exists() or not source.is_file(): | |
| raise DocumentError("Upload an image or PDF first.") | |
| size_mb = source.stat().st_size / (1024 * 1024) | |
| if size_mb > max_input_mb: | |
| raise DocumentError( | |
| f"Input is {size_mb:.1f} MB; the configured limit is {max_input_mb} MB." | |
| ) | |
| run_dir = Path(tempfile.mkdtemp(prefix="ocr_workbench_")) | |
| input_dir = run_dir / "input" | |
| pages_dir = run_dir / "source_pages" | |
| input_dir.mkdir(parents=True, exist_ok=True) | |
| pages_dir.mkdir(parents=True, exist_ok=True) | |
| shutil.copy2(source, input_dir / source.name) | |
| suffix = source.suffix.lower() | |
| pages: list[DocumentPage] = [] | |
| if suffix == ".pdf": | |
| try: | |
| document = fitz.open(source) | |
| except Exception as exc: | |
| raise DocumentError(f"Unable to open PDF: {exc}") from exc | |
| try: | |
| indices = parse_page_selection(selection, document.page_count, max_pages) | |
| scale = max(72, min(360, int(dpi))) / 72.0 | |
| matrix = fitz.Matrix(scale, scale) | |
| for index in indices: | |
| output = pages_dir / f"page_{index + 1:04d}.png" | |
| pixmap = document.load_page(index).get_pixmap( | |
| matrix=matrix, | |
| alpha=False, | |
| colorspace=fitz.csRGB, | |
| ) | |
| pixmap.save(output) | |
| pages.append(DocumentPage(index + 1, output)) | |
| finally: | |
| document.close() | |
| elif suffix in SUPPORTED_IMAGE_SUFFIXES: | |
| output = pages_dir / "page_0001.png" | |
| _save_normalized_image(source, output) | |
| pages.append(DocumentPage(1, output)) | |
| else: | |
| raise DocumentError( | |
| f"Unsupported file type {suffix!r}. Use PDF, PNG, JPEG, WebP, BMP, or TIFF." | |
| ) | |
| if not pages: | |
| raise DocumentError("No pages were selected.") | |
| return run_dir, pages | |
| def gallery_items(pages: Iterable[DocumentPage]) -> list[tuple[str, str]]: | |
| return [(str(page.image_path), f"Page {page.page_number}") for page in pages] | |