Conversión de Formatos de Archivo con Python
Python es la plataforma ideal para construir pipelines de conversión de archivos gracias a sus bibliotecas especializadas y a su capacidad de orquestar herramientas externas como FFmpeg. Esta guía cubre las conversiones más comunes con código listo para producción.
Conversión de imágenes con Pillow
pip install Pillow
from PIL import Image, ImageOps, ImageFilter
import pathlib
def convertir_imagen(
entrada: str,
salida: str,
*,
max_size: tuple[int, int] | None = None,
calidad: int = 85,
strip_exif: bool = True,
) -> pathlib.Path:
"""
Convierte una imagen a cualquier formato soportado por Pillow.
Preserva transparencia al convertir a PNG; elimina el canal alfa al exportar JPEG.
"""
ruta_entrada = pathlib.Path(entrada)
ruta_salida = pathlib.Path(salida)
formato_dst = ruta_salida.suffix.lstrip('.').upper()
img = Image.open(ruta_entrada)
# Corregir orientación EXIF automáticamente
img = ImageOps.exif_transpose(img)
# Redimensionar si se especifica tamaño máximo (preserva aspecto)
if max_size:
img.thumbnail(max_size, Image.LANCZOS)
# Manejar transparencia al convertir a JPEG
if formato_dst in ('JPG', 'JPEG') and img.mode in ('RGBA', 'LA', 'P'):
fondo = Image.new('RGB', img.size, (255, 255, 255))
if img.mode == 'P':
img = img.convert('RGBA')
fondo.paste(img, mask=img.split()[-1] if 'A' in img.mode else None)
img = fondo
# Convertir modo para formatos que no admiten paleta
if formato_dst == 'PNG' and img.mode == 'P':
img = img.convert('RGBA')
# Parámetros de guardado por formato
kwargs: dict = {'optimize': True}
if formato_dst in ('JPG', 'JPEG'):
kwargs['quality'] = calidad
kwargs['progressive'] = True
elif formato_dst == 'WEBP':
kwargs['quality'] = calidad
kwargs['method'] = 6 # mejor compresión, más lento
elif formato_dst == 'PNG':
kwargs['compress_level'] = 9
# Eliminar metadatos EXIF si se solicita
if strip_exif:
data = list(img.getdata())
img_limpia = Image.new(img.mode, img.size)
img_limpia.putdata(data)
img = img_limpia
img.save(ruta_salida, **kwargs)
print(f"Convertido: {ruta_entrada.name} → {ruta_salida.name} ({ruta_salida.stat().st_size/1024:.1f} KB)")
return ruta_salida
# Conversiones comunes
convertir_imagen('foto.png', 'foto.jpg', calidad=90, max_size=(1920, 1080))
convertir_imagen('logo.jpg', 'logo.webp', calidad=80)
convertir_imagen('icono.png', 'icono.avif', calidad=75) # requiere Pillow + pillow-avif-plugin
Conversión batch de imágenes
import pathlib
from PIL import Image, ImageOps
from concurrent.futures import ThreadPoolExecutor, as_completed
def convertir_carpeta(
carpeta_entrada: str,
carpeta_salida: str,
formato_dst: str = 'webp',
max_size: tuple[int, int] = (2000, 2000),
calidad: int = 82,
trabajadores: int = 4,
):
entrada = pathlib.Path(carpeta_entrada)
salida = pathlib.Path(carpeta_salida)
salida.mkdir(parents=True, exist_ok=True)
extensiones = {'.jpg', '.jpeg', '.png', '.tiff', '.bmp', '.gif'}
archivos = [f for f in entrada.rglob('*') if f.suffix.lower() in extensiones]
ok = errores = 0
def procesar(archivo):
destino = salida / archivo.relative_to(entrada).with_suffix(f'.{formato_dst}')
destino.parent.mkdir(parents=True, exist_ok=True)
try:
convertir_imagen(str(archivo), str(destino),
max_size=max_size, calidad=calidad)
return True, archivo.name
except Exception as e:
return False, f"{archivo.name}: {e}"
with ThreadPoolExecutor(max_workers=trabajadores) as executor:
futuros = {executor.submit(procesar, f): f for f in archivos}
for futuro in as_completed(futuros):
exito, info = futuro.result()
if exito:
ok += 1
else:
errores += 1
print(f" ERROR: {info}")
print(f"\nBatch completado: {ok} OK, {errores} errores de {len(archivos)} archivos")
convertir_carpeta('fotos_originales/', 'fotos_webp/', formato_dst='webp')
Conversión de vídeo y audio con FFmpeg
import subprocess
import pathlib
import json
def info_media(ruta: str) -> dict:
"""Obtiene metadatos de un archivo multimedia con ffprobe."""
cmd = [
'ffprobe', '-v', 'quiet',
'-print_format', 'json',
'-show_streams', '-show_format',
ruta,
]
result = subprocess.run(cmd, capture_output=True, text=True, check=True)
return json.loads(result.stdout)
def convertir_video(
entrada: str,
salida: str,
*,
codec_video: str = 'libx264',
crf: int = 23, # calidad: 18 (alta) a 28 (baja)
preset: str = 'medium', # ultrafast, fast, medium, slow, veryslow
codec_audio: str = 'aac',
bitrate_audio: str = '128k',
resolucion: str | None = None, # '1280:720' o None para mantener
fps: int | None = None,
) -> None:
cmd = ['ffmpeg', '-i', entrada, '-y'] # -y: sobreescribir sin preguntar
# Vídeo
cmd += ['-c:v', codec_video, '-crf', str(crf), '-preset', preset]
if resolucion:
cmd += ['-vf', f'scale={resolucion}']
if fps:
cmd += ['-r', str(fps)]
# Audio
cmd += ['-c:a', codec_audio, '-b:a', bitrate_audio]
cmd.append(salida)
result = subprocess.run(cmd, capture_output=True, text=True)
if result.returncode != 0:
raise RuntimeError(f"FFmpeg error:\n{result.stderr[-2000:]}")
info = pathlib.Path(salida).stat()
print(f"Convertido: {salida} ({info.st_size/1024**2:.1f} MB)")
# Conversiones comunes
convertir_video('video.avi', 'video.mp4', codec_video='libx264', crf=22)
convertir_video('video.mov', 'video.webm', codec_video='libvpx-vp9', crf=30)
convertir_video('video.mp4', 'video_720p.mp4', resolucion='1280:720', crf=24)
def extraer_audio(entrada: str, salida: str, formato: str = 'mp3', bitrate: str = '192k'):
"""Extrae la pista de audio de un vídeo."""
cmd = ['ffmpeg', '-i', entrada, '-vn', # -vn: sin vídeo
'-c:a', 'libmp3lame' if formato == 'mp3' else formato,
'-b:a', bitrate, '-y', salida]
subprocess.run(cmd, check=True, capture_output=True)
print(f"Audio extraído: {salida}")
extraer_audio('pelicula.mp4', 'banda_sonora.mp3', bitrate='320k')
Convertir audio con FFmpeg
def convertir_audio(
entrada: str,
salida: str,
bitrate: str = '192k',
sample_rate: int | None = None,
) -> None:
"""Convierte entre formatos de audio: MP3, FLAC, OGG, WAV, AAC, etc."""
cmd = ['ffmpeg', '-i', entrada, '-y']
ext = pathlib.Path(salida).suffix.lower()
if ext == '.mp3':
cmd += ['-c:a', 'libmp3lame', '-b:a', bitrate]
elif ext == '.ogg':
cmd += ['-c:a', 'libvorbis', '-q:a', '5']
elif ext == '.opus':
cmd += ['-c:a', 'libopus', '-b:a', bitrate]
elif ext == '.flac':
cmd += ['-c:a', 'flac', '-compression_level', '8']
elif ext == '.aac':
cmd += ['-c:a', 'aac', '-b:a', bitrate]
elif ext == '.wav':
cmd += ['-c:a', 'pcm_s16le']
else:
cmd += ['-c:a', 'copy']
if sample_rate:
cmd += ['-ar', str(sample_rate)]
cmd.append(salida)
subprocess.run(cmd, check=True, capture_output=True)
print(f"Audio convertido: {salida}")
convertir_audio('cancion.flac', 'cancion.mp3', bitrate='320k')
convertir_audio('podcast.mp3', 'podcast.ogg')
convertir_audio('voz.wav', 'voz.opus', bitrate='64k') # muy eficiente para voz
Conversión de PDF con PyMuPDF
pip install pymupdf
import fitz # PyMuPDF
import pathlib
def pdf_a_imagenes(pdf_ruta: str, carpeta_salida: str, dpi: int = 150) -> list[str]:
"""Exporta cada página del PDF como imagen PNG."""
salida = pathlib.Path(carpeta_salida)
salida.mkdir(parents=True, exist_ok=True)
rutas = []
doc = fitz.open(pdf_ruta)
mat = fitz.Matrix(dpi / 72, dpi / 72) # factor de escala
for n, pagina in enumerate(doc):
pix = pagina.get_pixmap(matrix=mat, alpha=False)
ruta = str(salida / f"pagina_{n+1:03d}.png")
pix.save(ruta)
rutas.append(ruta)
print(f" Página {n+1}/{len(doc)}: {pix.width}x{pix.height}px")
doc.close()
print(f"PDF exportado: {len(rutas)} páginas en {carpeta_salida}")
return rutas
def extraer_texto_pdf(pdf_ruta: str) -> str:
"""Extrae texto de todas las páginas de un PDF."""
doc = fitz.open(pdf_ruta)
texto = '\n\n'.join(pagina.get_text() for pagina in doc)
doc.close()
return texto
def imagenes_a_pdf(imagenes: list[str], salida_pdf: str) -> None:
"""Combina imágenes en un PDF de una sola página por imagen."""
doc = fitz.open()
for ruta_img in imagenes:
with fitz.open(ruta_img) as img_doc:
rect = img_doc[0].rect
pagina = doc.new_page(width=rect.width, height=rect.height)
pagina.insert_image(rect, filename=ruta_img)
doc.save(salida_pdf, garbage=4, deflate=True)
print(f"PDF creado: {salida_pdf}")
# Uso
rutas_img = pdf_a_imagenes('documento.pdf', 'paginas/', dpi=200)
texto = extraer_texto_pdf('informe.pdf')
imagenes_a_pdf(['pag1.png', 'pag2.png', 'pag3.png'], 'resultado.pdf')
Conversión de documentos Word con python-docx
pip install python-docx
from docx import Document
from docx.shared import Inches, Pt, RGBColor
from docx.enum.text import WD_ALIGN_PARAGRAPH
import pathlib
def docx_a_texto(ruta_docx: str) -> str:
"""Extrae el texto completo de un archivo .docx."""
doc = Document(ruta_docx)
lineas = []
for parrafo in doc.paragraphs:
if parrafo.text.strip():
lineas.append(parrafo.text)
for tabla in doc.tables:
for fila in tabla.rows:
celdas = [c.text.strip() for c in fila.cells]
lineas.append('\t'.join(celdas))
return '\n'.join(lineas)
def crear_docx_desde_datos(datos: list[dict], salida: str) -> None:
"""Crea un .docx formateado desde una lista de registros."""
doc = Document()
# Título
titulo = doc.add_heading('Informe de Conversiones', level=1)
titulo.alignment = WD_ALIGN_PARAGRAPH.CENTER
# Tabla de datos
if datos:
tabla = doc.add_table(rows=1, cols=len(datos[0]))
tabla.style = 'Table Grid'
# Cabecera
hdr = tabla.rows[0].cells
for i, clave in enumerate(datos[0].keys()):
hdr[i].text = clave.replace('_', ' ').title()
# Filas de datos
for registro in datos:
fila = tabla.add_row().cells
for i, valor in enumerate(registro.values()):
fila[i].text = str(valor)
doc.save(salida)
print(f"DOCX creado: {salida}")
texto = docx_a_texto('contrato.docx')
print(texto[:500])
crear_docx_desde_datos([
{'nombre': 'foto.jpg', 'formato_dst': 'WebP', 'tamanyo_kb': 45, 'ratio': '68%'},
{'nombre': 'video.avi', 'formato_dst': 'MP4', 'tamanyo_kb': 12340, 'ratio': '45%'},
], 'reporte_conversiones.docx')
Pipeline de conversión completo
import pathlib
import subprocess
from concurrent.futures import ProcessPoolExecutor
from PIL import Image, ImageOps
def convertir_archivo(entrada: pathlib.Path, carpeta_salida: pathlib.Path) -> dict:
"""
Pipeline unificado que detecta el tipo y aplica la conversión adecuada.
Devuelve un dict con resultado y metadatos.
"""
ext = entrada.suffix.lower()
resultado = {'entrada': str(entrada), 'ok': False, 'error': ''}
try:
if ext in {'.jpg', '.jpeg', '.png', '.bmp', '.tiff', '.gif'}:
# Imágenes → WebP
salida = carpeta_salida / entrada.with_suffix('.webp').name
img = Image.open(entrada)
img = ImageOps.exif_transpose(img)
if img.mode in ('RGBA', 'P'):
img = img.convert('RGB')
img.save(salida, 'WEBP', quality=82, method=4)
elif ext in {'.avi', '.mov', '.mkv', '.flv', '.wmv'}:
# Vídeo → MP4 H.264
salida = carpeta_salida / entrada.with_suffix('.mp4').name
subprocess.run([
'ffmpeg', '-i', str(entrada),
'-c:v', 'libx264', '-crf', '22', '-preset', 'fast',
'-c:a', 'aac', '-b:a', '128k',
'-y', str(salida),
], check=True, capture_output=True)
elif ext in {'.flac', '.wav', '.ogg', '.wma', '.aiff'}:
# Audio → MP3
salida = carpeta_salida / entrada.with_suffix('.mp3').name
subprocess.run([
'ffmpeg', '-i', str(entrada),
'-c:a', 'libmp3lame', '-b:a', '192k',
'-y', str(salida),
], check=True, capture_output=True)
else:
resultado['error'] = f"Formato no soportado: {ext}"
return resultado
tamano_original = entrada.stat().st_size
tamano_final = salida.stat().st_size
ratio = (1 - tamano_final / tamano_original) * 100
resultado.update({
'ok': True,
'salida': str(salida),
'original_kb': tamano_original // 1024,
'final_kb': tamano_final // 1024,
'ahorro': f"{ratio:.1f}%",
})
except Exception as e:
resultado['error'] = str(e)
return resultado
# Procesar carpeta completa en paralelo
def batch_convertir(carpeta_entrada: str, carpeta_salida: str):
entrada = pathlib.Path(carpeta_entrada)
salida = pathlib.Path(carpeta_salida)
salida.mkdir(parents=True, exist_ok=True)
archivos = [f for f in entrada.rglob('*') if f.is_file()]
with ProcessPoolExecutor(max_workers=4) as executor:
resultados = list(executor.map(
lambda f: convertir_archivo(f, salida), archivos
))
ok = sum(1 for r in resultados if r['ok'])
errores = [r for r in resultados if not r['ok'] and r['error']]
print(f"\n=== Batch completado: {ok}/{len(archivos)} OK ===")
for e in errores:
print(f" ERROR {e['entrada']}: {e['error']}")
return resultados
Buenas prácticas
- Verificar la instalación de FFmpeg al inicio:
shutil.which('ffmpeg')— si no está disponible, fallar con un mensaje claro en lugar de un error críptico más adelante. capture_output=Trueen llamadas a FFmpeg — la salida de error es muy verbosa y contamina los logs si no se captura.ImageOps.exif_transpose(img)antes de cualquier manipulación — corrige la orientación EXIF que muchas cámaras y móviles insertan.- CRF (Constant Rate Factor) en FFmpeg: 18-22 para calidad alta, 23-28 para balance tamaño/calidad, 28-35 para máxima compresión.
- WebP o AVIF para imágenes web — ahorran 30-50% vs JPEG/PNG con calidad visual similar o superior.
ProcessPoolExecutorpara conversiones intensivas en CPU (imágenes, vídeo),ThreadPoolExecutorpara I/O (lectura/escritura de archivos sin procesado).
Conversiones relacionadas
Conversiones de documento que siguen este tema: