feat: Implement the initial IPSExtractor project structure, including application logic, GUI, core utilities, and documentation.
This commit is contained in:
@@ -0,0 +1,180 @@
|
||||
"""Utilidades para convertir documentos (PDF/TIFF) a imágenes base64.
|
||||
|
||||
En Windows se requiere Poppler. Este proyecto incluye una ruta por defecto:
|
||||
`dependencies/poppler-24.08.0/Library/bin`. Ajuste si su instalación difiere.
|
||||
"""
|
||||
import base64
|
||||
import logging
|
||||
import os
|
||||
from io import BytesIO
|
||||
from typing import List, Tuple, Optional
|
||||
import time
|
||||
|
||||
from pdf2image import convert_from_path, pdfinfo_from_path
|
||||
from PIL import Image, ImageSequence
|
||||
|
||||
Image.MAX_IMAGE_PIXELS = None
|
||||
|
||||
|
||||
from application.config import settings
|
||||
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def pdf_to_images_bs64(pdf_path: str, validate: bool = True, dpi: int = 800, thread_count: int = 10) -> List[str]:
|
||||
"""Convierte un PDF a una lista de PNGs en base64.
|
||||
|
||||
Usa pdf2image + Poppler. Lanza ValueError si una imagen no valida.
|
||||
"""
|
||||
poppler_bin = settings.imaging.poppler_path or 'dependencies/poppler-24.08.0/Library/bin'
|
||||
t0 = time.time()
|
||||
logger.info("Convirtiendo PDF a imágenes", extra={"pdf": pdf_path, "dpi": dpi})
|
||||
images = convert_from_path(pdf_path, dpi=dpi, thread_count=thread_count, poppler_path=poppler_bin)
|
||||
resultados: List[str] = []
|
||||
for indice, imagen in enumerate(images):
|
||||
buffer = BytesIO()
|
||||
imagen.save(buffer, format="PNG")
|
||||
b64 = base64.b64encode(buffer.getvalue()).decode('utf-8')
|
||||
if validate:
|
||||
try:
|
||||
raw = base64.b64decode(b64, validate=True)
|
||||
with Image.open(BytesIO(raw)) as im:
|
||||
im.verify()
|
||||
except Exception as error:
|
||||
raise ValueError(f"Imagen base64 inválida en la página {indice + 1}: {error}")
|
||||
resultados.append(b64)
|
||||
elapsed_ms = int((time.time() - t0) * 1000)
|
||||
logger.debug(f"PDF->imgs OK (pages={len(resultados)}, ms={elapsed_ms}, threads={thread_count})", extra={"pdf": pdf_path, "dpi": dpi})
|
||||
logger.info("PDF convertido", extra={"pdf": pdf_path, "dpi": dpi})
|
||||
return resultados
|
||||
|
||||
|
||||
def get_pdf_num_pages(pdf_path: str) -> int:
|
||||
"""Devuelve el número de páginas de un PDF usando pdfinfo_from_path."""
|
||||
poppler_bin = settings.imaging.poppler_path or 'dependencies/poppler-24.08.0/Library/bin'
|
||||
info = pdfinfo_from_path(pdf_path, poppler_path=poppler_bin)
|
||||
try:
|
||||
return int(info.get('Pages') or info.get('pages') or 0)
|
||||
except Exception:
|
||||
return 0
|
||||
|
||||
|
||||
def pdf_pages_to_images_bs64(
|
||||
pdf_path: str,
|
||||
first_page: int,
|
||||
last_page: int,
|
||||
*,
|
||||
dpi: int,
|
||||
grayscale: bool = True,
|
||||
validate: bool = False,
|
||||
thread_count: int = 10,
|
||||
) -> List[str]:
|
||||
"""Convierte un rango [first_page, last_page] (1-indexed, inclusivo) a PNGs base64.
|
||||
|
||||
Opcionalmente aplica escala de grises y omite validación para acelerar.
|
||||
"""
|
||||
poppler_bin = settings.imaging.poppler_path or 'dependencies/poppler-24.08.0/Library/bin'
|
||||
t0 = time.time()
|
||||
logger.debug(
|
||||
f"Convirtiendo rango PDF→imgs (first={first_page}, last={last_page}, dpi={dpi}, gray={grayscale})",
|
||||
extra={"pdf": pdf_path, "dpi": dpi},
|
||||
)
|
||||
images = convert_from_path(
|
||||
pdf_path,
|
||||
dpi=dpi,
|
||||
thread_count=thread_count,
|
||||
poppler_path=poppler_bin,
|
||||
first_page=first_page,
|
||||
last_page=last_page,
|
||||
)
|
||||
resultados: List[str] = []
|
||||
for idx, img in enumerate(images, start=first_page):
|
||||
if grayscale:
|
||||
img = img.convert('L')
|
||||
buffer = BytesIO()
|
||||
img.save(buffer, format="PNG")
|
||||
b64 = base64.b64encode(buffer.getvalue()).decode('utf-8')
|
||||
if validate:
|
||||
try:
|
||||
raw = base64.b64decode(b64, validate=True)
|
||||
with Image.open(BytesIO(raw)) as im:
|
||||
im.verify()
|
||||
except Exception as error:
|
||||
raise ValueError(f"Imagen base64 inválida en la página {idx}: {error}")
|
||||
resultados.append(b64)
|
||||
logger.debug(
|
||||
f"Rango convertido (count={len(resultados)}, ms={int((time.time()-t0)*1000)})",
|
||||
extra={"pdf": pdf_path, "dpi": dpi},
|
||||
)
|
||||
return resultados
|
||||
|
||||
def tiff_to_images_bs64(tiff_path: str, validate: bool = True, pages_range: Optional[Tuple[int, int]] = (2, 3)) -> List[str]:
|
||||
"""Convierte un TIFF multipágina a PNGs en base64.
|
||||
|
||||
Por defecto, procesa un rango de páginas inclusivo cuando se especifica
|
||||
`pages_range` como `(inicio, fin)`, usando índices 1-based. Use None para
|
||||
procesar todas las páginas. Puede pasar `None` en uno de los extremos para
|
||||
indicar “sin inicio” o “sin fin”.
|
||||
"""
|
||||
resultados: List[str] = []
|
||||
t0 = time.time()
|
||||
logger.info("Convirtiendo TIFF a imágenes", extra={"pdf": tiff_path})
|
||||
with Image.open(tiff_path) as im:
|
||||
frames = []
|
||||
for idx, frame in enumerate(ImageSequence.Iterator(im), start=1):
|
||||
if pages_range is not None:
|
||||
start, end = pages_range
|
||||
# Permitir extremos None para significar “sin límite” por ese lado
|
||||
if start is not None and idx < start:
|
||||
continue
|
||||
if end is not None and idx > end:
|
||||
continue
|
||||
frames.append(frame.copy())
|
||||
for indice, frame in enumerate(frames):
|
||||
buffer = BytesIO()
|
||||
frame.convert("RGB").save(buffer, format="PNG")
|
||||
b64 = base64.b64encode(buffer.getvalue()).decode('utf-8')
|
||||
if validate:
|
||||
try:
|
||||
raw = base64.b64decode(b64, validate=True)
|
||||
with Image.open(BytesIO(raw)) as imv:
|
||||
imv.verify()
|
||||
except Exception as error:
|
||||
raise ValueError(f"Imagen base64 inválida en la página TIFF {indice + 1}: {error}")
|
||||
resultados.append(b64)
|
||||
elapsed_ms = int((time.time() - t0) * 1000)
|
||||
logger.debug(f"TIFF->imgs OK (pages={len(resultados)}, ms={elapsed_ms})", extra={"pdf": tiff_path})
|
||||
logger.info("TIFF convertido", extra={"pdf": tiff_path})
|
||||
return resultados
|
||||
|
||||
def document_to_images_bs64(
|
||||
path: str,
|
||||
validate: bool = True,
|
||||
dpi: int = 600,
|
||||
thread_count: int = 10,
|
||||
tiff_pages: Optional[Tuple[int, int]] = (2, 3),
|
||||
tiff_page_range: Optional[Tuple[int, int]] = None,
|
||||
) -> List[str]:
|
||||
"""Puerta de entrada: decide conversión según la extensión (.pdf, .tif/.tiff).
|
||||
|
||||
`tiff_page_range` permite indicar un rango explícito (inicio, fin) 1-based
|
||||
para TIFF. Si es None, se usa `tiff_pages` como valor por defecto.
|
||||
"""
|
||||
ext = os.path.splitext(path)[1].lower()
|
||||
if ext == ".pdf":
|
||||
return pdf_to_images_bs64(path, validate=validate, dpi=dpi, thread_count=thread_count)
|
||||
if ext in (".tif", ".tiff"):
|
||||
# Prioridad:
|
||||
# - (None, None) => procesar todas las páginas del TIFF (sin recorte).
|
||||
# - rango explícito (inicio, fin) => usar ese rango.
|
||||
# - None => usar el valor por defecto de configuración (`tiff_pages`).
|
||||
effective_range = tiff_pages
|
||||
if tiff_page_range is not None:
|
||||
if tiff_page_range == (None, None):
|
||||
effective_range = None
|
||||
else:
|
||||
effective_range = tiff_page_range
|
||||
return tiff_to_images_bs64(path, validate=validate, pages_range=effective_range)
|
||||
raise ValueError(f"Formato no soportado: {ext}")
|
||||
|
||||
Reference in New Issue
Block a user