From f4104ded96a51d75881966f12eeb91934db998a7 Mon Sep 17 00:00:00 2001 From: Sergio Date: Thu, 19 Feb 2026 10:31:53 +0100 Subject: [PATCH] afegit convertir-imagenes --- README.md | 7 ++++--- core/archive.py | 26 +++++++++++++++++++------- core/pipeline.py | 24 +++++++++++++++++++++++- main.py | 9 ++++++--- processors/image_normalizer.py | 26 ++++++++++++++++++++++++++ 5 files changed, 78 insertions(+), 14 deletions(-) diff --git a/README.md b/README.md index f1486c5..06c0c1e 100644 --- a/README.md +++ b/README.md @@ -41,16 +41,17 @@ Las operaciones que tocan el contenido del archivo piden confirmación por defec |------|-------------|-------------------| | `--limpiar` | Elimina ficheros basura (`thumbs.db`, `.ds_store`, `__MACOSX`, `desktop.ini`) y reempaqueta como CBZ. | Sí | | `--convertir` | Convierte al formato indicado por `--formato` (por defecto CBZ). | No | -| `--estandarizar` | Equivale a `--limpiar` + `--convertir` en secuencia. | Solo para limpieza | +| `--estandarizar` | Equivale a `--limpiar` + `--renumerar` + `--uniformizar-imagenes` + `--convertir` en secuencia. | Sí, salvo para conversión de formato | | `--renumerar` | Renombra las páginas a numeración secuencial con zero-padding (`001.jpg`, `002.jpg`…). | Sí | -| `--uniformizar-imagenes` | Convierte todas las imágenes al formato indicado por `--formato-imagen`. | Sí | +| `--uniformizar-imagenes` | Solo actúa si el archivo contiene imágenes en **formatos mixtos**. Si todas las imágenes ya son del mismo formato no hace nada, aunque no coincidan con `--formato-imagen`. | Sí | +| `--convertir-imagenes` | Convierte todas las imágenes que no sean `--formato-imagen`, independientemente de si los formatos ya son uniformes. | Sí | ### Opciones de formato | Flag | Valores | Por defecto | Descripción | |------|---------|-------------|-------------| | `--formato` | `cbz`, `cbr` | `cbz` | Formato de archivo destino para `--convertir` y `--estandarizar`. | -| `--formato-imagen` | `jpg`, `png`, `webp` | `png` | Formato de imagen destino para `--uniformizar-imagenes`. | +| `--formato-imagen` | `jpg`, `png`, `webp` | `png` | Formato de imagen destino para `--uniformizar-imagenes` y `--convertir-imagenes`. | | `--no-preguntar` | — | — | Desactiva la confirmación interactiva: aplica todas las operaciones automáticamente. | --- diff --git a/core/archive.py b/core/archive.py index 06ef6d5..a6c37af 100644 --- a/core/archive.py +++ b/core/archive.py @@ -48,10 +48,22 @@ def extract_archive(path: str, dest_dir: str) -> str: def repack_as_cbz(source_dir: str, target_path: str) -> None: - """Empaqueta todos los archivos de source_dir en un CBZ (ZIP deflated).""" - with zipfile.ZipFile(target_path, "w", zipfile.ZIP_DEFLATED) as zf: - for root, _, files in os.walk(source_dir): - for f in files: - full = os.path.join(root, f) - rel = os.path.relpath(full, source_dir) - zf.write(full, rel) + """Empaqueta todos los archivos de source_dir en un CBZ (ZIP deflated). + + Escribe primero a un fichero temporal (.tmp) en el mismo directorio y + hace un os.replace() atómico al final, de modo que una interrupción + nunca deja un CBZ parcial ni destruye el original. + """ + tmp_path = target_path + ".tmp" + try: + with zipfile.ZipFile(tmp_path, "w", zipfile.ZIP_DEFLATED) as zf: + for root, _, files in os.walk(source_dir): + for f in files: + full = os.path.join(root, f) + rel = os.path.relpath(full, source_dir) + zf.write(full, rel) + os.replace(tmp_path, target_path) + except BaseException: + if os.path.exists(tmp_path): + os.remove(tmp_path) + raise diff --git a/core/pipeline.py b/core/pipeline.py index 00e2665..f252e8e 100644 --- a/core/pipeline.py +++ b/core/pipeline.py @@ -17,7 +17,12 @@ from processors.checks import ( check_comicinfo, ) from processors.page_normalizer import normalize_pages, preview_normalize_pages -from processors.image_normalizer import normalize_images, preview_normalize_images +from processors.image_normalizer import ( + normalize_images, + preview_normalize_images, + uniformize_images, + preview_uniformize_images, +) class Pipeline: @@ -50,6 +55,10 @@ class Pipeline: return {"renames": renames} elif step == "normalize_images": + conversions = preview_uniformize_images(temp_dir, self.desired_image_format) + return {"conversions": conversions, "target_ext": self.desired_image_format} + + elif step == "convert_images": conversions = preview_normalize_images(temp_dir, self.desired_image_format) return {"conversions": conversions, "target_ext": self.desired_image_format} @@ -107,6 +116,19 @@ class Pipeline: preview = self._compute_preview("normalize_images", temp_dir, step_results) if preview.get("conversions"): if confirm_fn is None or confirm_fn("normalize_images", preview): + img_result = uniformize_images(temp_dir, self.desired_image_format) + step_results.append(img_result) + if img_result.errors: + return ComicResult( + original_path=path, final_path=None, steps=step_results + ) + if img_result.changed: + any_changed = True + + if "convert_images" in self.steps: + preview = self._compute_preview("convert_images", temp_dir, step_results) + if preview.get("conversions"): + if confirm_fn is None or confirm_fn("convert_images", preview): img_result = normalize_images(temp_dir, self.desired_image_format) step_results.append(img_result) if img_result.errors: diff --git a/main.py b/main.py index e5984ab..bbd70d3 100644 --- a/main.py +++ b/main.py @@ -30,7 +30,7 @@ def _print_preview(step: str, preview: dict, formato: str) -> None: print(f" ... y {n - 10} más") print(f"Formato final del archivo: {fmt}") - elif step == "normalize_images": + elif step in ("normalize_images", "convert_images"): conversions = preview["conversions"] target_ext = preview["target_ext"].lstrip(".") lossless = " (sin pérdida)" if target_ext.lower() == "png" else "" @@ -65,6 +65,7 @@ def parse_args(): parser.add_argument("--formato", choices=["cbz", "cbr"], default="cbz") parser.add_argument("--renumerar", action="store_true") parser.add_argument("--uniformizar-imagenes", action="store_true") + parser.add_argument("--convertir-imagenes", action="store_true") parser.add_argument("--formato-imagen", choices=["jpg", "png", "webp"], default="png") parser.add_argument("--no-preguntar", action="store_true") @@ -94,10 +95,12 @@ def main(): steps = [] if args.limpiar or args.estandarizar: steps.append("clean") - if args.renumerar: + if args.renumerar or args.estandarizar: steps.append("normalize_pages") - if args.uniformizar_imagenes: + if args.uniformizar_imagenes or args.estandarizar: steps.append("normalize_images") + if args.convertir_imagenes: + steps.append("convert_images") if args.convertir or args.estandarizar: steps.append("convert") diff --git a/processors/image_normalizer.py b/processors/image_normalizer.py index 6713d13..42f0574 100644 --- a/processors/image_normalizer.py +++ b/processors/image_normalizer.py @@ -87,3 +87,29 @@ def normalize_images(work_dir: str, target_ext: str = ".jpg") -> StepResult: changed = True return StepResult(step="normalize_images", changed=changed, details=details) + + +def _detected_formats(work_dir: str) -> set[str]: + """Devuelve el conjunto de extensiones de imagen presentes en work_dir (normalizando .jpeg → .jpg).""" + formats = set() + for fname in os.listdir(work_dir): + ext = os.path.splitext(fname)[1].lower() + if ext == ".jpeg": + ext = ".jpg" + if ext in IMAGE_EXTENSIONS: + formats.add(ext) + return formats + + +def preview_uniformize_images(work_dir: str, target_ext: str) -> list[tuple[str, str]]: + """Como preview_normalize_images pero devuelve [] si las imágenes ya son uniformes.""" + if len(_detected_formats(work_dir)) <= 1: + return [] + return preview_normalize_images(work_dir, target_ext) + + +def uniformize_images(work_dir: str, target_ext: str = ".jpg") -> StepResult: + """Como normalize_images pero solo actúa si hay formatos mixtos.""" + if len(_detected_formats(work_dir)) <= 1: + return StepResult(step="normalize_images", changed=False) + return normalize_images(work_dir, target_ext)