80 lines
2.5 KiB
Python
80 lines
2.5 KiB
Python
"""
|
||
Image-family extractor: jpg/jpeg, png.
|
||
|
||
Reads EXIF (including capture date) into metadata, writes a downscaled JPEG
|
||
thumbnail, and — when options['ocr'] is set and pytesseract is available — OCRs
|
||
the image into content. Without OCR, content is a short placeholder note.
|
||
"""
|
||
import io
|
||
from pathlib import Path
|
||
|
||
from .base import ExtractionResult
|
||
|
||
EXTRACTOR_ID = "image/pillow@1"
|
||
THUMB_MAX = 640 # longest edge, px
|
||
|
||
|
||
def _exif(img) -> dict:
|
||
try:
|
||
from PIL.ExifTags import TAGS
|
||
except ImportError:
|
||
return {}
|
||
raw = getattr(img, "_getexif", lambda: None)()
|
||
if not raw:
|
||
return {}
|
||
out = {}
|
||
for tag_id, value in raw.items():
|
||
name = TAGS.get(tag_id, str(tag_id))
|
||
if name in ("DateTime", "DateTimeOriginal", "Make", "Model", "Orientation"):
|
||
out[name] = str(value)
|
||
return out
|
||
|
||
|
||
def _thumbnail(img) -> bytes:
|
||
from PIL import Image # noqa: F401 (ensure Pillow present)
|
||
im = img.copy()
|
||
im.thumbnail((THUMB_MAX, THUMB_MAX))
|
||
if im.mode not in ("RGB", "L"):
|
||
im = im.convert("RGB")
|
||
buf = io.BytesIO()
|
||
im.save(buf, format="JPEG", quality=80)
|
||
return buf.getvalue()
|
||
|
||
|
||
def extract(source: Path, options: dict) -> ExtractionResult:
|
||
from PIL import Image # lazy
|
||
|
||
warnings = []
|
||
metadata = {}
|
||
content = ""
|
||
thumb = None
|
||
|
||
with Image.open(source) as img:
|
||
metadata["width"], metadata["height"] = img.size
|
||
metadata["mode"] = img.mode
|
||
exif = _exif(img)
|
||
if exif:
|
||
metadata["exif"] = exif
|
||
if "DateTimeOriginal" in exif or "DateTime" in exif:
|
||
metadata["captured"] = exif.get("DateTimeOriginal", exif.get("DateTime"))
|
||
try:
|
||
thumb = _thumbnail(img)
|
||
except Exception as e:
|
||
warnings.append(f"thumbnail failed: {e}")
|
||
|
||
if options.get("ocr"):
|
||
try:
|
||
import pytesseract
|
||
content = pytesseract.image_to_string(img).strip()
|
||
metadata["ocr"] = True
|
||
except ImportError:
|
||
warnings.append("--ocr requested but pytesseract not installed")
|
||
except Exception as e:
|
||
warnings.append(f"ocr failed: {e}")
|
||
|
||
if not content:
|
||
content = f"\n\n_Image {metadata.get('width')}×{metadata.get('height')}. Run with --ocr to extract any text._\n"
|
||
|
||
return ExtractionResult(content=content, metadata=metadata, thumb=thumb,
|
||
warnings=warnings, extractor=EXTRACTOR_ID)
|