Files
2025-06-11 14:41:03 +02:00

289 lines
14 KiB
Python

import os
import io
import base64
import numpy as np
import torch
from PIL import Image
import folder_paths # Nécessaire pour obtenir le dossier output de ComfyUI
import datetime
# Gardez vos imports try-except pour les dépendances
try:
import fitz # PyMuPDF
except ImportError:
print("AVERTISSEMENT: PyMuPDF (fitz) n'est pas installé. L'extraction PDF ne fonctionnera pas.")
fitz = None
# ... (autres imports de dépendances) ...
try:
from docx import Document
except ImportError:
print("AVERTISSEMENT: python-docx n'est pas installé. L'extraction DOCX ne fonctionnera pas.")
Document = None
try:
from bs4 import BeautifulSoup
except ImportError:
print("AVERTISSEMENT: beautifulsoup4 n'est pas installé. L'extraction HTML/Markdown ne fonctionnera pas.")
BeautifulSoup = None
try:
from markdown import markdown
except ImportError:
print("AVERTISSEMENT: markdown n'est pas installé. L'extraction Markdown ne fonctionnera pas.")
markdown = None
class ExtractAndSaveImagesFromDocument: # Renommé pour plus de clarté
@classmethod
def INPUT_TYPES(cls):
return {
"required": {
"document_path": ("STRING", {"default": ""}),
"min_width": ("INT", {"default": 256, "min": 1, "max": 8192, "step": 1}),
"min_height": ("INT", {"default": 256, "min": 1, "max": 8192, "step": 1}),
"filename_prefix": ("STRING", {"default": "extracted_doc_img"}),
},
}
RETURN_TYPES = ("IMAGE", "STRING") # On retourne la première image et le chemin du dossier
RETURN_NAMES = ("first_saved_image_preview", "output_folder_path")
FUNCTION = "extract_and_save_images"
CATEGORY = "document_processing"
OUTPUT_NODE = True # Indique que ce node sauvegarde des fichiers
def _pil_to_rgb_if_needed(self, image_pil):
"""Convertit une image PIL en RGB si nécessaire, gère la transparence."""
try:
if image_pil is None: return None
original_mode = image_pil.mode
if image_pil.mode == "P": # Palette
image_pil = image_pil.convert("RGBA") # Convertir en RGBA pour gérer la transparence de la palette
if image_pil.mode == "LA" or (original_mode == "P" and "transparency" in image_pil.info):
image_pil = image_pil.convert("RGBA")
if image_pil.mode == "RGBA":
background = Image.new("RGB", image_pil.size, (255, 255, 255))
if image_pil.splitAlpha(): # Vérifie s'il y a un canal alpha
background.paste(image_pil, mask=image_pil.splitAlpha())
else: # Pas de canal alpha, on peut juste convertir
background.paste(image_pil)
image_pil = background
elif image_pil.mode == "CMYK":
image_pil = image_pil.convert("RGB")
elif image_pil.mode == "L": # Grayscale
image_pil = image_pil.convert("RGB")
elif image_pil.mode != "RGB":
image_pil = image_pil.convert("RGB")
# Vérification finale
if image_pil.mode != "RGB":
print(f"[ExtractAndSaveImages] AVERTISSEMENT: L'image n'a pas pu être convertie en RGB. Mode final: {image_pil.mode}")
return None
return image_pil
except Exception as e:
print(f"[ExtractAndSaveImages] ERREUR _pil_to_rgb_if_needed: {e}")
return None
def _process_pil_image(self, pil_image, min_width, min_height):
"""Filtre l'image PIL par taille et la convertit en RGB."""
if pil_image is None: return None
if pil_image.width < min_width or pil_image.height < min_height:
# print(f"[ExtractAndSaveImages] DEBUG: Image {pil_image.size} ignorée (trop petite pour {min_width}x{min_height}).")
return None
rgb_pil_image = self._pil_to_rgb_if_needed(pil_image)
return rgb_pil_image
def _extract_pil_images_from_pdf(self, pdf_path, min_width, min_height):
if fitz is None: raise ImportError("PyMuPDF (fitz) requis.")
pil_images_list = []
doc = fitz.open(pdf_path)
# print(f"[ExtractAndSaveImages] INFO: PDF ouvert: {os.path.basename(pdf_path)}, Pages: {len(doc)}")
for page_num, page in enumerate(doc, 1):
for img_index, img_info in enumerate(page.get_images(full=True)):
xref = img_info[0]
try:
base_image = doc.extract_image(xref)
if base_image and base_image.get("image"):
pil_img = Image.open(io.BytesIO(base_image["image"]))
processed_pil_img = self._process_pil_image(pil_img, min_width, min_height)
if processed_pil_img:
pil_images_list.append({"pil_image": processed_pil_img, "page": page_num, "index_on_page": img_index})
except Exception as e:
print(f"[ExtractAndSaveImages] ERREUR extraction image PDF p.{page_num} #{img_index}: {e}")
doc.close()
return pil_images_list
def _extract_pil_images_from_docx(self, docx_path, min_width, min_height):
if Document is None: raise ImportError("python-docx requis.")
pil_images_list = []
doc = Document(docx_path)
for rel_idx, rel in enumerate(doc.part.rels.values()):
if "image" in rel.target_ref and hasattr(rel.target_part, 'blob'):
try:
pil_img = Image.open(io.BytesIO(rel.target_part.blob))
processed_pil_img = self._process_pil_image(pil_img, min_width, min_height)
if processed_pil_img:
pil_images_list.append({"pil_image": processed_pil_img, "page": 0, "index_on_page": rel_idx}) # page 0 pour docx
except Exception as e:
print(f"[ExtractAndSaveImages] ERREUR extraction image DOCX rel #{rel_idx}: {e}")
return pil_images_list
def _extract_pil_images_from_html(self, html_path_or_filelike, min_width, min_height, base_path_for_relative_imgs=None):
if BeautifulSoup is None: raise ImportError("BeautifulSoup4 requis.")
pil_images_list = []
html_content = ""
if hasattr(html_path_or_filelike, "read"): # Vient de Markdown (StringIO)
html_content = html_path_or_filelike.read()
# base_path_for_relative_imgs doit être fourni par l'appelant (_extract_pil_images_from_markdown)
else: # C'est un chemin de fichier HTML
if base_path_for_relative_imgs is None: # Si non fourni, déduire du chemin html
base_path_for_relative_imgs = os.path.dirname(html_path_or_filelike)
with open(html_path_or_filelike, "r", encoding="utf-8") as f:
html_content = f.read()
soup = BeautifulSoup(html_content, "lxml")
for idx, img_tag in enumerate(soup.find_all("img")):
src = img_tag.get("src")
if not src: continue
pil_img = None
try:
if src.startswith("data:image/"):
header, encoded = src.split(",", 1)
image_bytes = base64.b64decode(encoded)
pil_img = Image.open(io.BytesIO(image_bytes))
elif not (src.startswith("http://") or src.startswith("https://")):
local_image_path = src
if base_path_for_relative_imgs and not os.path.isabs(src):
local_image_path = os.path.normpath(os.path.join(base_path_for_relative_imgs, src))
if os.path.exists(local_image_path):
pil_img = Image.open(local_image_path)
# else:
# print(f"[ExtractAndSaveImages] DEBUG: Image locale HTML non trouvée: {local_image_path}")
if pil_img:
processed_pil_img = self._process_pil_image(pil_img, min_width, min_height)
if processed_pil_img:
pil_images_list.append({"pil_image": processed_pil_img, "page": 0, "index_on_page": idx}) # page 0 pour html
except Exception as e:
print(f"[ExtractAndSaveImages] ERREUR extraction image HTML #{idx} src: {src[:60]}: {e}")
return pil_images_list
def _extract_pil_images_from_markdown(self, md_path, min_width, min_height):
if markdown is None: raise ImportError("markdown requis.")
md_base_dir = os.path.dirname(md_path)
with open(md_path, "r", encoding="utf-8") as f:
html_from_md = markdown(f.read())
# Passe le base_dir du markdown pour résoudre les images relatives dans le HTML généré
return self._extract_pil_images_from_html(io.StringIO(html_from_md), min_width, min_height, base_path_for_relative_imgs=md_base_dir)
def pil_to_tensor(self, pil_image_rgb):
if pil_image_rgb is None: return None
try:
arr = np.array(pil_image_rgb, dtype=np.uint8) # Doit être RGB HWC uint8
if arr.ndim != 3 or arr.shape[2] != 3: return None # Vérif finale
arr_float = arr.astype(np.float32) / 255.0
tensor = torch.from_numpy(arr_float).unsqueeze(0) # BHWC, B=1
return tensor
except Exception as e:
print(f"[ExtractAndSaveImages] ERREUR pil_to_tensor: {e}")
return None
def extract_and_save_images(self, document_path, min_width, min_height, filename_prefix):
if not document_path or not os.path.isfile(document_path):
print(f"[ExtractAndSaveImages] ERREUR: Fichier introuvable: {document_path}")
return (torch.zeros((1, 64, 64, 3), dtype=torch.float32), "error: file not found",)
print(f"[ExtractAndSaveImages] INFO: Début extraction pour: {document_path} (min size: {min_width}x{min_height})")
ext = os.path.splitext(document_path)[1].lower()
# Liste pour stocker les objets PIL.Image valides et leurs métadonnées
valid_pil_images_with_meta = []
try:
if ext == ".pdf":
valid_pil_images_with_meta = self._extract_pil_images_from_pdf(document_path, min_width, min_height)
elif ext == ".docx":
valid_pil_images_with_meta = self._extract_pil_images_from_docx(document_path, min_width, min_height)
elif ext in [".html", ".htm"]:
valid_pil_images_with_meta = self._extract_pil_images_from_html(document_path, min_width, min_height)
elif ext in [".md", ".markdown"]:
valid_pil_images_with_meta = self._extract_pil_images_from_markdown(document_path, min_width, min_height)
else:
print(f"[ExtractAndSaveImages] ERREUR: Format non supporté: {ext}")
return (torch.zeros((1, 64, 64, 3), dtype=torch.float32), f"error: unsupported format {ext}",)
except ImportError as e:
print(f"[ExtractAndSaveImages] ERREUR: Dépendance manquante: {e}")
return (torch.zeros((1, 64, 64, 3), dtype=torch.float32), f"error: missing dependency {e}",)
except Exception as e:
print(f"[ExtractAndSaveImages] ERREUR inattendue pendant extraction: {e}")
import traceback; traceback.print_exc()
return (torch.zeros((1, 64, 64, 3), dtype=torch.float32), f"error: extraction failed {e}",)
if not valid_pil_images_with_meta:
print(f"[ExtractAndSaveImages] INFO: Aucune image trouvée/valide pour les critères de taille dans {os.path.basename(document_path)}.")
return (torch.zeros((1, 64, 64, 3), dtype=torch.float32), "no images found or matching criteria",)
# Création du sous-dossier de sortie
doc_basename = os.path.splitext(os.path.basename(document_path))[0]
ts = datetime.datetime.now().strftime("%Y%m%d-%H%M%S")
# Utiliser folder_paths pour le dossier output de ComfyUI
output_dir_base = folder_paths.get_output_directory()
# Créer un sous-dossier spécifique pour ce document et cette exécution
# pour éviter d'écraser des fichiers si le même document est traité plusieurs fois.
specific_output_folder_name = f"{filename_prefix}_{doc_basename}_{ts}"
full_output_folder_path = os.path.join(output_dir_base, specific_output_folder_name)
if not os.path.exists(full_output_folder_path):
os.makedirs(full_output_folder_path)
print(f"[ExtractAndSaveImages] INFO: Sauvegarde de {len(valid_pil_images_with_meta)} images dans: {full_output_folder_path}")
first_saved_pil_image = None
saved_count = 0
for i, img_data in enumerate(valid_pil_images_with_meta):
pil_image_to_save = img_data["pil_image"]
page_num = img_data["page"]
idx_on_page = img_data["index_on_page"]
try:
# Nom de fichier : prefix_docname_pXXX_idxYYY_numZZZ.png
# numZZZ est un compteur global pour s'assurer de l'unicité si page/index se répètent
image_filename = f"{filename_prefix}_p{page_num:03d}_idx{idx_on_page:03d}_num{i:03d}.png"
full_save_path = os.path.join(full_output_folder_path, image_filename)
pil_image_to_save.save(full_save_path, "PNG")
# print(f"[ExtractAndSaveImages] DEBUG: Image sauvegardée: {full_save_path}")
if first_saved_pil_image is None:
first_saved_pil_image = pil_image_to_save
saved_count += 1
except Exception as e:
print(f"[ExtractAndSaveImages] ERREUR sauvegarde image #{i}: {e}")
print(f"[ExtractAndSaveImages] INFO: {saved_count} images sauvegardées avec succès.")
preview_tensor = torch.zeros((1, 64, 64, 3), dtype=torch.float32) # Placeholder
if first_saved_pil_image:
tensor = self.pil_to_tensor(first_saved_pil_image)
if tensor is not None:
preview_tensor = tensor
return (preview_tensor, full_output_folder_path,)
NODE_CLASS_MAPPINGS = {
"ExtractAndSaveImagesFromDocument": ExtractAndSaveImagesFromDocument,
}
NODE_DISPLAY_NAME_MAPPINGS = {
"ExtractAndSaveImagesFromDocument": "Extract & Save Images From Document",
}