Download src/document_intelligence/io/image.py from MHamdan/SPARKNET: direct link, hf CLI and curl.
- Browser
- Download file 6.3 kB
-
https://huggingface.co/spaces/MHamdan/SPARKNET/resolve/main/src/document_intelligence/io/image.py
- Command line
-
hf download hf://spaces/MHamdan/SPARKNET/src/document_intelligence/io/image.py
-
curl -L -o image.py https://huggingface.co/spaces/MHamdan/SPARKNET/resolve/main/src/document_intelligence/io/image.py
6.3 kB
| """ | |
| Image Document Loading | |
| Handles single images and multi-page TIFF documents. | |
| """ | |
| import logging | |
| from pathlib import Path | |
| from typing import Iterator, List, Optional, Tuple, Union | |
| import numpy as np | |
| from PIL import Image | |
| from .base import ( | |
| DocumentFormat, | |
| DocumentInfo, | |
| DocumentLoader, | |
| PageInfo, | |
| PageRenderer, | |
| RenderOptions, | |
| ) | |
| logger = logging.getLogger(__name__) | |
| class ImageLoader(DocumentLoader): | |
| """ | |
| Image document loader. | |
| Handles common image formats (JPEG, PNG, etc.) and multi-page TIFF. | |
| """ | |
| SUPPORTED_EXTENSIONS = { | |
| ".jpg", ".jpeg", ".png", ".bmp", ".gif", | |
| ".tif", ".tiff", ".webp" | |
| } | |
| def __init__(self): | |
| self._images: List[Image.Image] = [] | |
| self._info: Optional[DocumentInfo] = None | |
| self._path: Optional[Path] = None | |
| def load(self, path: Union[str, Path]) -> DocumentInfo: | |
| """Load image(s) and extract metadata.""" | |
| self._path = Path(path) | |
| if not self._path.exists(): | |
| raise FileNotFoundError(f"Image file not found: {self._path}") | |
| suffix = self._path.suffix.lower() | |
| if suffix not in self.SUPPORTED_EXTENSIONS: | |
| raise ValueError(f"Unsupported image format: {suffix}") | |
| # Close any previously loaded images | |
| self.close() | |
| # Load image(s) | |
| img = Image.open(self._path) | |
| # Handle multi-page TIFF | |
| if suffix in {".tif", ".tiff"}: | |
| self._load_multipage_tiff(img) | |
| else: | |
| # Single image | |
| self._images = [img.convert("RGB")] | |
| # Build page info | |
| pages = [] | |
| for i, page_img in enumerate(self._images): | |
| dpi = page_img.info.get("dpi", (72, 72)) | |
| if isinstance(dpi, tuple): | |
| dpi = int(dpi[0]) | |
| else: | |
| dpi = int(dpi) | |
| page_info = PageInfo( | |
| page_number=i + 1, | |
| width_pixels=page_img.width, | |
| height_pixels=page_img.height, | |
| dpi=dpi, | |
| has_images=True | |
| ) | |
| pages.append(page_info) | |
| # Determine format | |
| if suffix in {".tif", ".tiff"} and len(self._images) > 1: | |
| doc_format = DocumentFormat.TIFF_MULTIPAGE | |
| else: | |
| doc_format = DocumentFormat.IMAGE | |
| self._info = DocumentInfo( | |
| path=self._path, | |
| format=doc_format, | |
| num_pages=len(self._images), | |
| pages=pages, | |
| file_size_bytes=self._path.stat().st_size, | |
| is_scanned=True, # Images are always "scanned" | |
| has_text_layer=False | |
| ) | |
| return self._info | |
| def _load_multipage_tiff(self, img: Image.Image) -> None: | |
| """Load all pages from a multi-page TIFF.""" | |
| self._images = [] | |
| try: | |
| page_num = 0 | |
| while True: | |
| img.seek(page_num) | |
| # Copy the frame to avoid issues with lazy loading | |
| self._images.append(img.copy().convert("RGB")) | |
| page_num += 1 | |
| except EOFError: | |
| # Reached end of TIFF | |
| pass | |
| if not self._images: | |
| raise ValueError("No pages found in TIFF file") | |
| def close(self) -> None: | |
| """Close all loaded images.""" | |
| for img in self._images: | |
| try: | |
| img.close() | |
| except Exception: | |
| pass | |
| self._images = [] | |
| def is_loaded(self) -> bool: | |
| """Check if images are loaded.""" | |
| return len(self._images) > 0 | |
| def info(self) -> Optional[DocumentInfo]: | |
| """Get document info.""" | |
| return self._info | |
| def get_image(self, page_number: int) -> Image.Image: | |
| """Get PIL Image for a specific page (1-indexed).""" | |
| if not self._images: | |
| raise RuntimeError("No images loaded") | |
| if page_number < 1 or page_number > len(self._images): | |
| raise ValueError(f"Invalid page number: {page_number}") | |
| return self._images[page_number - 1] | |
| class ImageRenderer(PageRenderer): | |
| """ | |
| Image page renderer. | |
| Renders images with optional resizing and format conversion. | |
| """ | |
| def __init__(self, loader: ImageLoader): | |
| self._loader = loader | |
| def render_page( | |
| self, | |
| page_number: int, | |
| options: Optional[RenderOptions] = None | |
| ) -> np.ndarray: | |
| """Render an image page.""" | |
| if not self._loader.is_loaded(): | |
| raise RuntimeError("No document loaded") | |
| options = options or RenderOptions() | |
| img = self._loader.get_image(page_number) | |
| # Get original DPI | |
| original_dpi = img.info.get("dpi", (72, 72)) | |
| if isinstance(original_dpi, tuple): | |
| original_dpi = original_dpi[0] | |
| # Resize if DPI differs | |
| if options.dpi != original_dpi and original_dpi > 0: | |
| scale = options.dpi / original_dpi | |
| new_size = (int(img.width * scale), int(img.height * scale)) | |
| resample = Image.LANCZOS if options.antialias else Image.NEAREST | |
| img = img.resize(new_size, resample=resample) | |
| # Convert color mode | |
| if options.color_mode == "L": | |
| img = img.convert("L") | |
| elif options.color_mode == "RGBA": | |
| img = img.convert("RGBA") | |
| else: # RGB | |
| img = img.convert("RGB") | |
| return np.array(img) | |
| def render_pages( | |
| self, | |
| page_numbers: Optional[List[int]] = None, | |
| options: Optional[RenderOptions] = None | |
| ) -> Iterator[Tuple[int, np.ndarray]]: | |
| """Render multiple pages.""" | |
| if not self._loader.is_loaded(): | |
| raise RuntimeError("No document loaded") | |
| info = self._loader.info | |
| if page_numbers is None: | |
| page_numbers = list(range(1, info.num_pages + 1)) | |
| for page_num in page_numbers: | |
| yield page_num, self.render_page(page_num, options) | |
| def load_image(path: Union[str, Path]) -> Tuple[ImageLoader, ImageRenderer]: | |
| """ | |
| Convenience function to load an image document. | |
| Returns: | |
| Tuple of (loader, renderer) | |
| """ | |
| loader = ImageLoader() | |
| loader.load(path) | |
| renderer = ImageRenderer(loader) | |
| return loader, renderer | |