Created
March 17, 2026 16:45
-
-
Save erickacevedor/0645307fa10e576a898333becf2c65fd to your computer and use it in GitHub Desktop.
Convert images to several formats and with an specific width
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| import argparse | |
| import sys | |
| from pathlib import Path | |
| from PIL import Image, ImageOps | |
| try: | |
| import pillow_heif | |
| pillow_heif.register_heif_opener() | |
| HEIC_SUPPORTED = True | |
| except ImportError: | |
| HEIC_SUPPORTED = False | |
| SUPPORTED_INPUTS = {".jpg", ".jpeg", ".png", ".webp", ".heic", ".heif"} | |
| SUPPORTED_OUTPUTS = {"jpg", "webp"} | |
| def ensure_rgb(image: Image.Image, background=(255, 255, 255)) -> Image.Image: | |
| if image.mode in ("RGBA", "LA") or (image.mode == "P" and "transparency" in image.info): | |
| bg = Image.new("RGB", image.size, background) | |
| alpha = image.convert("RGBA") | |
| bg.paste(alpha, mask=alpha.getchannel("A")) | |
| return bg | |
| if image.mode != "RGB": | |
| return image.convert("RGB") | |
| return image | |
| def resize_if_needed(image: Image.Image, max_width: int) -> Image.Image: | |
| width, height = image.size | |
| if width <= max_width: | |
| return image | |
| new_height = int((max_width / width) * height) | |
| return image.resize((max_width, new_height), Image.LANCZOS) | |
| def process_image( | |
| input_path: Path, | |
| output_dir: Path, | |
| output_format: str, | |
| max_width: int, | |
| quality: int, | |
| overwrite: bool = False, | |
| ) -> tuple[bool, str]: | |
| try: | |
| with Image.open(input_path) as img: | |
| img = ImageOps.exif_transpose(img) | |
| img = resize_if_needed(img, max_width) | |
| if output_format == "jpg": | |
| img = ensure_rgb(img) | |
| output_ext = ".jpg" | |
| elif output_format == "webp": | |
| img = ensure_rgb(img) | |
| output_ext = ".webp" | |
| else: | |
| return False, f"Formato no soportado: {output_format}" | |
| output_path = output_dir / f"{input_path.stem}{output_ext}" | |
| if output_path.exists() and not overwrite: | |
| return False, f"Ya existe: {output_path.name}" | |
| save_kwargs = {"quality": quality} | |
| if output_format == "jpg": | |
| save_kwargs.update({ | |
| "format": "JPEG", | |
| "optimize": True, | |
| "progressive": True | |
| }) | |
| elif output_format == "webp": | |
| save_kwargs.update({ | |
| "format": "WEBP", | |
| "method": 6 | |
| }) | |
| img.save(output_path, **save_kwargs) | |
| return True, f"OK -> {output_path.name}" | |
| except Exception as e: | |
| return False, f"ERROR -> {input_path.name}: {e}" | |
| def collect_images(input_dir: Path, recursive: bool) -> list[Path]: | |
| if recursive: | |
| files = [p for p in input_dir.rglob("*") if p.is_file()] | |
| else: | |
| files = [p for p in input_dir.iterdir() if p.is_file()] | |
| images = [p for p in files if p.suffix.lower() in SUPPORTED_INPUTS] | |
| return sorted(images) | |
| def main(): | |
| parser = argparse.ArgumentParser( | |
| description="Resize y conversión masiva de imágenes a JPG o WEBP." | |
| ) | |
| parser.add_argument("input_dir", help="Carpeta de entrada con las imágenes") | |
| parser.add_argument( | |
| "-o", | |
| "--output-dir", | |
| default="output_images", | |
| help="Carpeta de salida" | |
| ) | |
| parser.add_argument( | |
| "-f", | |
| "--format", | |
| default="jpg", | |
| choices=SUPPORTED_OUTPUTS, | |
| help="Formato de salida: jpg o webp" | |
| ) | |
| parser.add_argument( | |
| "-w", | |
| "--max-width", | |
| type=int, | |
| default=1200, | |
| help="Ancho máximo en px" | |
| ) | |
| parser.add_argument( | |
| "-q", | |
| "--quality", | |
| type=int, | |
| default=85, | |
| help="Calidad de salida (1-100)" | |
| ) | |
| parser.add_argument( | |
| "-r", | |
| "--recursive", | |
| action="store_true", | |
| help="Buscar imágenes en subcarpetas" | |
| ) | |
| parser.add_argument( | |
| "--overwrite", | |
| action="store_true", | |
| help="Sobrescribir archivos existentes" | |
| ) | |
| args = parser.parse_args() | |
| input_dir = Path(args.input_dir) | |
| output_dir = Path(args.output_dir) | |
| output_format = args.format.lower() | |
| if not input_dir.exists() or not input_dir.is_dir(): | |
| print("La carpeta de entrada no existe o no es válida.") | |
| sys.exit(1) | |
| if output_format not in SUPPORTED_OUTPUTS: | |
| print("Formato de salida no válido.") | |
| sys.exit(1) | |
| if not HEIC_SUPPORTED: | |
| print("Aviso: no se detectó soporte HEIC. Instala 'pillow-heif' para convertir .heic/.heif.") | |
| output_dir.mkdir(parents=True, exist_ok=True) | |
| images = collect_images(input_dir, args.recursive) | |
| if not images: | |
| print("No se encontraron imágenes compatibles.") | |
| sys.exit(0) | |
| print(f"Se encontraron {len(images)} imágenes.") | |
| print(f"Salida: {output_format.upper()} | Max width: {args.max_width}px\n") | |
| success_count = 0 | |
| error_count = 0 | |
| skipped_count = 0 | |
| for image_path in images: | |
| ok, message = process_image( | |
| input_path=image_path, | |
| output_dir=output_dir, | |
| output_format=output_format, | |
| max_width=args.max_width, | |
| quality=args.quality, | |
| overwrite=args.overwrite, | |
| ) | |
| print(message) | |
| if ok: | |
| success_count += 1 | |
| elif message.startswith("Ya existe:"): | |
| skipped_count += 1 | |
| else: | |
| error_count += 1 | |
| print("\nResumen:") | |
| print(f"Convertidas: {success_count}") | |
| print(f"Omitidas: {skipped_count}") | |
| print(f"Errores: {error_count}") | |
| if __name__ == "__main__": | |
| main() |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment