Optimización eficiente de PNG en Python con Oxipng
Optimizar las imágenes es fundamental para mejorar el rendimiento web y reducir el consumo de ancho
de banda. Los archivos PNG son de alta calidad y sin pérdidas, pero a menudo se distribuyen con
metadatos de más y una compresión subóptima. En este DevTip te mostraremos cómo reducir el tamaño de
los PNG desde Python llamando al moderno motor Oxipng, impulsado por Rust, a través de su binding de
Python con mantenimiento activo, pyoxipng.
¿Por qué optimizar imágenes PNG?
Las imágenes más pequeñas se traducen en cargas de página más rápidas, mejores Core Web Vitals, facturas de CDN más bajas y usuarios más contentos. En conexiones móviles, una reducción considerable del tamaño del archivo puede marcar la diferencia entre una interfaz ágil y una espera que provoca abandonos. Una optimización PNG eficaz suele reducir el tamaño de los archivos en un 15-40 % sin ninguna pérdida de calidad visual.
OptiPNG frente a Oxipng
OptiPNG es la clásica herramienta de línea de comandos escrita en C para la compresión PNG sin pérdidas. Ha sido una opción fiable durante años. Sin embargo, Oxipng es una reescritura moderna en Rust diseñada para la velocidad y la eficiencia. Es totalmente compatible con las optimizaciones sin pérdidas de OptiPNG, pero añade multihilo, aceleración SIMD y heurísticas mejoradas que pueden ofrecer mejores ratios de compresión y una ejecución más rápida.
La biblioteca pyoxipng ofrece acceso pythónico al núcleo de Oxipng en Rust, lo que te permite
aprovechar su rendimiento directamente dentro de tus aplicaciones de Python sin necesidad de
gestionar procesos externos. Aunque este artículo se centra en pyoxipng por su rendimiento y su
mantenimiento activo, los principios de optimización son similares a los del OptiPNG original.
Requisitos previos
Antes de empezar, asegúrate de tener instaladas las herramientas necesarias:
# Check Python version (3.8+ recommended)
python --version
# Install pyoxipng and optional dependencies
pip install pyoxipng pillow numpy
- Python: usa una versión de Python 3 con soporte.
- pyoxipng: la biblioteca principal para la optimización de PNG (
pip install pyoxipng). Se recomienda la versión 9.1.0 o posterior. - Pillow: se usa para cargar y manipular imágenes en los ejemplos avanzados (
pip install pillow). - NumPy: necesario para manejar arrays al trabajar con datos de imagen en bruto (
pip install numpy).
Optimizar un solo PNG
Optimizar un único archivo es sencillo con la función optimize:
import oxipng
try:
# Optimize 'input.png' and save it as 'output.png'
# Level ranges from 0 (fastest, least compression) to 6 (slowest, best compression). Default is 2.
oxipng.optimize('input.png', 'output.png', level=4)
print("Optimization successful!")
except oxipng.PngError as e:
print(f"Error optimizing PNG: {e}")
except FileNotFoundError:
print("Error: Input file not found.")
Con la configuración predeterminada (level=2), normalmente puedes esperar una reducción del tamaño
del archivo del 15–25 %, a menudo completada en milisegundos. Subir el nivel mejora la compresión,
pero tarda más.
Optimización por lotes con hilos
Para optimizar varias imágenes, usar hilos puede acelerar notablemente el proceso, sobre todo en
sistemas multinúcleo. pyoxipng es seguro para hilos.
import oxipng
import os
from concurrent.futures import ThreadPoolExecutor
from pathlib import Path
# Define source and destination directories
SRC_DIR = Path('images_source')
DST_DIR = Path('images_optimized')
DST_DIR.mkdir(exist_ok=True) # Create destination if it doesn't exist
# Find all PNG files in the source directory
png_files = list(SRC_DIR.glob('*.png'))
# Function to optimize a single PNG file
def optimize_png(source_path: Path) -> None:
dest_path = DST_DIR / source_path.name
try:
oxipng.optimize(source_path, dest_path, level=4)
print(f'✓ Optimized: {source_path.name}')
except oxipng.PngError as e:
print(f'✗ Failed: {source_path.name} - {e}')
except Exception as e:
print(f'✗ Error processing {source_path.name}: {e}')
# Use a threadpoolexecutor for parallel processing
# Adjust max_workers based on your CPU cores (os.cpu_count() is a good starting point)
num_workers = os.cpu_count() or 4 # Default to 4 if cpu_count() fails
with ThreadPoolExecutor(max_workers=num_workers) as executor:
# Map the optimize_png function to all found PNG files
# Use list() to ensure all futures complete before moving on
list(executor.map(optimize_png, png_files))
print("\nBatch optimization complete.")
Ajusta max_workers en ThreadPoolExecutor (por ejemplo, a os.cpu_count()) para maximizar el rendimiento. En
una máquina de 8 núcleos, procesar cientos de PNG pequeños puede llevar solo unos segundos.
Elegir el nivel de optimización adecuado
pyoxipng ofrece niveles de optimización del 0 al 6. Los niveles más altos generan archivos más
pequeños, pero requieren más tiempo de procesamiento.
| Nivel | Ahorro típico | Velocidad relativa | Cuándo usarlo |
|---|---|---|---|
| 0 | 5-10 % | Muy rápida | Comprobaciones rápidas, compilaciones de desarrollo donde prima la velocidad |
| 2 (predeterminado) | 15-25 % | Rápida | Recursos web generales, buen equilibrio |
| 4 | 20-35 % | Moderada | Despliegues de producción, con el tamaño como prioridad |
| 6 | 25-40 % | La más lenta | Archivado final, optimizaciones puntuales |
Elige el nivel que mejor equilibre tus necesidades de compresión con las limitaciones de tiempo de procesamiento de tu caso de uso.
Conservar o eliminar los metadatos
De forma predeterminada, pyoxipng no solicita la eliminación de metadatos. Controla la eliminación explícita con strip:
import oxipng
try:
# Strip all possible metadata for maximum size reduction
oxipng.optimize(
'input_with_meta.png',
'output_stripped.png',
level=4,
strip=oxipng.StripChunks.all() # Strip all ancillary chunks (like text, EXIF, etc.)
)
print("Optimized with maximum metadata stripping.")
# Preserve all metadata
oxipng.optimize(
'input_with_meta.png',
'output_preserved.png',
level=4,
strip=oxipng.StripChunks.none() # Do not request metadata stripping
)
print("Optimized while preserving metadata.")
except oxipng.PngError as e:
print(f"Error during optimization: {e}")
except FileNotFoundError:
print("Error: Input file 'input_with_meta.png' not found.")
Usa strip=oxipng.StripChunks.all() para eliminar los chunks auxiliares, incluidos los perfiles de color y
los datos de animación. Eso puede cambiar el aspecto de una imagen o eliminar la animación. Usa
strip=oxipng.StripChunks.safe() para conservar los chunks relacionados con el renderizado, o
strip=oxipng.StripChunks.none() (el valor predeterminado) para no solicitar la eliminación de
metadatos. Los chunks que dejan de ser válidos tras una reducción del tipo de color o de la
profundidad de bits pueden eliminarse igualmente.
Avanzado: pipelines de optimización en memoria
Si generas o manipulas imágenes en memoria (por ejemplo, con Pillow), puedes optimizar los datos de píxeles en bruto directamente, sin escribir archivos intermedios. Para ello hay que proporcionar las dimensiones de la imagen y la información del tipo de color.
import oxipng
import numpy as np
from PIL import Image
from pathlib import Path
try:
# Load an image using Pillow and convert to a suitable format (e.g., RGB or RGBA)
img = Image.open('logo_source.png')
# Ensure image mode is compatible (e.g., RGB, RGBA, L for grayscale)
if img.mode not in ('RGB', 'RGBA', 'L'):
img = img.convert('RGBA') # Convert to RGBA if needed
# Convert image to a NumPy array
img_array = np.array(img, dtype=np.uint8)
height, width = img_array.shape[:2] # Get height and width
# Get raw bytes from the NumPy array
raw_bytes = img_array.tobytes()
# Define the color type based on Pillow's image mode
if img.mode == 'RGB':
# For RGB, specify transparency key if needed, e.g., black (0,0,0)
color_type = oxipng.ColorType.rgb(None) # None means no specific color is transparent
elif img.mode == 'RGBA':
color_type = oxipng.ColorType.rgba()
elif img.mode == 'L': # Grayscale
color_type = oxipng.ColorType.grayscale()
else:
raise ValueError(f"Unsupported image mode for Oxipng: {img.mode}")
# Create a RawImage object
raw_image = oxipng.RawImage(
data=raw_bytes,
width=width,
height=height,
color_type=color_type
)
# Optimize the raw image data in memory (level 6 for max compression)
optimized_png_bytes = raw_image.create_optimized_png(level=6)
# Save the optimized bytes to a file
output_path = Path('logo_optimized_from_memory.png')
output_path.write_bytes(optimized_png_bytes)
print(f"Successfully optimized image from memory and saved to {output_path}")
except FileNotFoundError:
print("Error: Source image file 'logo_source.png' not found.")
except ImportError:
print("Error: Pillow or NumPy not installed. Run 'pip install pillow numpy'")
except oxipng.PngError as e:
print(f"Error during in-memory optimization: {e}")
except Exception as e:
print(f"An unexpected error occurred: {e}")
Este enfoque resulta útil para integrar la optimización directamente en flujos de trabajo de procesamiento de imágenes, evitando la E/S de disco en los pasos intermedios.
Script mínimo de pruebas de rendimiento
Para hacerte una idea del rendimiento con tus propias imágenes, usa un script sencillo de pruebas de rendimiento:
import time
import oxipng
import shutil
import pathlib
import os
# Define the source file for the benchmark
source_file = pathlib.Path('sample.png')
# Check if the source file exists
if not source_file.exists():
print(f"Error: Benchmark file '{source_file}' not found. Please create a sample PNG file named 'sample.png'.")
exit()
target_file = pathlib.Path('sample_optimized_benchmark.png')
# Copy the source file to keep the original intact
shutil.copy(source_file, target_file)
original_size = source_file.stat().st_size
print(f"Optimizing '{target_file.name}' (Level 4)...")
start_time = time.perf_counter()
try:
# Optimize the copied file in place
oxipng.optimize(target_file, target_file, level=4)
end_time = time.perf_counter()
optimized_size = target_file.stat().st_size
size_diff = original_size - optimized_size
time_taken = end_time - start_time
print(f"Original size: {original_size / 1024:.1f} KiB")
print(f"Optimized size: {optimized_size / 1024:.1f} KiB")
if original_size > 0:
print(f"Saved: {size_diff / 1024:.1f} KiB ({size_diff / original_size * 100:.1f}% reduction)")
else:
print("Saved: 0.0 KiB (Original size was 0)")
print(f"Time taken: {time_taken:.3f} seconds")
except oxipng.PngError as e:
print(f"Optimization failed: {e}")
except Exception as e:
print(f"An unexpected error occurred: {e}")
finally:
# Clean up the copied benchmark file
if target_file.exists():
try:
os.remove(target_file)
print(f"Cleaned up '{target_file.name}'.")
except OSError as e:
print(f"Error removing benchmark file '{target_file.name}': {e}")
Ejecuta este script con distintos archivos PNG y niveles de optimización (level=...) para recopilar
datos de rendimiento reales y relevantes para tus recursos.
Solución de problemas
Estos son algunos problemas habituales y sus posibles soluciones al usar pyoxipng:
| Síntoma | Causa probable | Solución |
|---|---|---|
ModuleNotFoundError: No module named 'oxipng' | pyoxipng no está instalado o el entorno virtual no está activo | Activa el entorno virtual (source venv/bin/activate o .\venv\Scripts\activate) y luego ejecuta pip install pyoxipng |
oxipng.PngError: Not a PNG file | El archivo de entrada está dañado o no es un PNG | Valida los archivos de entrada; asegúrate de que sean PNG válidos antes de procesarlos. Usa bibliotecas como Pillow para comprobar el formato si es necesario. |
oxipng.PngError: Failed to parse header (IHDR) | El archivo de entrada está dañado o no es un PNG | Comprueba la integridad del archivo. Omite los archivos que no superen la validación. |
| Uso elevado de memoria o fallos con imágenes grandes | Nivel demasiado alto o demasiados hilos | Baja el level de optimización, reduce max_workers en ThreadPoolExecutor o procesa los archivos grandes de forma secuencial. Ten en cuenta los límites de memoria del sistema. |
FileNotFoundError | La ruta del archivo de entrada es incorrecta | Verifica que la ruta del archivo de entrada existe y que el script puede acceder a ella. Revisa las rutas relativas frente a las absolutas. |
Plantilla de manejo de errores
Las aplicaciones robustas deberían incluir manejo de errores para gestionar los posibles problemas durante la optimización:
import oxipng
import sys
from pathlib import Path
input_path = Path('image.png')
output_path = Path('image_optimized.png')
try:
print(f"Attempting to optimize '{input_path}' to '{output_path}'...")
oxipng.optimize(input_path, output_path, level=4)
print(f"Successfully optimized.")
except oxipng.PngError as exc:
# Handle specific Oxipng errors (e.g., invalid PNG format, processing errors)
sys.exit(f'Optimization failed for {input_path}: {exc}')
except FileNotFoundError:
# Handle case where the input file doesn't exist
sys.exit(f'Error: Input file not found at {input_path}')
except PermissionError:
# Handle case where script lacks permissions to read input or write output
sys.exit(f'Error: Permission denied for file operations involving {input_path} or {output_path}')
except Exception as exc:
# Catch any other unexpected errors during the process
sys.exit(f'An unexpected error occurred during optimization: {exc}')
Esta estructura ayuda a evitar fallos y ofrece mensajes informativos si la optimización falla por distintos motivos.
Alternativa gestionada
Si prefieres no gestionar tú mismo el proceso de optimización, los servicios en la nube como Transloadit pueden encargarse automáticamente. El Robot 🤖 /image/optimize de Transloadit puede optimizar PNG (y otros formatos) como parte de un flujo de trabajo de subida y procesamiento:
{
"steps": {
"optimized": {
"robot": "/image/optimize",
"use": ":original",
"priority": "compression-ratio",
"progressive": false,
"preserve_meta_data": true
}
}
}
Este Step de la Assembly toma el archivo original subido (:original) y le aplica
la optimización con el Robot /image/optimize, priorizando el ratio de compresión y
conservando los metadatos.
Conclusión
Con pyoxipng puedes integrar fácilmente una optimización PNG sin pérdidas y de alto rendimiento en
tus proyectos de Python. Tanto si procesas recursos por lotes durante un paso de compilación, como
si optimizas las subidas de los usuarios sobre la marcha o te integras con pipelines complejos de
procesamiento de imágenes, pyoxipng ofrece una solución rápida y eficiente basada en el moderno
motor Oxipng. Esto te ayuda a entregar páginas web y aplicaciones más ligeras, con mejores tiempos
de carga. Para una solución totalmente gestionada, plantéate usar un servicio como Transloadit que
se encargue automáticamente de la optimización de imágenes como parte de tus flujos de trabajo de
procesamiento de archivos.
