Combinar múltiples imágenes en un único PDF es una tarea habitual para digitalizar documentos escaneados, crear catálogos o archivar fotos. Python ofrece tres herramientas con enfoques distintos para este propósito.
## Comparativa de herramientas
| Biblioteca | Calidad | Pérdida | Control layout | Velocidad | Instalación |
|---|---|---|---|---|---|
| **Pillow** | Buena | Sí (recomprime) | Básico | Rápida | `pip install pillow` |
| **img2pdf** | **Sin pérdida** | **No** | Limitado | **Muy rápida** | `pip install img2pdf` |
| **ReportLab** | Configurable | Sí | **Total** | Media | `pip install reportlab` |
## Pillow — método sencillo y rápido
```python
from PIL import Image
from pathlib import Path
def imagenes_a_pdf_pillow(rutas_imagenes, ruta_pdf, calidad=85):
"""Combina imágenes en un PDF usando Pillow."""
imagenes = []
for ruta in rutas_imagenes:
img = Image.open(ruta)
# Convertir a RGB si es necesario (RGBA, paleta, etc.)
if img.mode != 'RGB':
img = img.convert('RGB')
imagenes.append(img)
if not imagenes:
raise ValueError("No se encontraron imágenes")
# Primera imagen como base, resto como páginas adicionales
imagenes[0].save(
ruta_pdf,
format='PDF',
save_all=True,
append_images=imagenes[1:],
resolution=150, # DPI del PDF
quality=calidad,
)
print(f"PDF creado: {ruta_pdf} ({len(imagenes)} páginas)")
# Uso básico
rutas = sorted(Path('escaneos/').glob('*.jpg'))
imagenes_a_pdf_pillow(rutas, 'documento.pdf')
# Con orden personalizado
rutas_ordenadas = [
'pagina_01.jpg',
'pagina_02.jpg',
'pagina_03.jpg',
]
imagenes_a_pdf_pillow(rutas_ordenadas, 'documento_ordenado.pdf')
```
## img2pdf — sin pérdida de calidad (recomendado para escaneos)
img2pdf inserta las imágenes en el PDF sin recodificarlas. Un JPEG se guarda como JPEG dentro del PDF — cero pérdida adicional:
```python
import img2pdf
from pathlib import Path
# Conversión básica sin pérdida
with open('escaneos.pdf', 'wb') as f:
f.write(img2pdf.convert(['pagina1.jpg', 'pagina2.jpg', 'pagina3.jpg']))
# Desde un directorio (ordenado por nombre)
directorio = Path('escaneos/')
rutas = sorted(directorio.glob('*.jpg'))
with open('escaneos_completo.pdf', 'wb') as f:
f.write(img2pdf.convert([str(r) for r in rutas]))
# Con tamaño de página A4 (en mm)
layout = img2pdf.get_layout_fun(img2pdf.mm_to_pt(210), img2pdf.mm_to_pt(297))
with open('escaneos_a4.pdf', 'wb') as f:
f.write(img2pdf.convert([str(r) for r in rutas], layout_fun=layout))
# Mixto: JPG + PNG
rutas_mixtas = sorted(directorio.glob('*.[jp][pn]g'))
with open('mixto.pdf', 'wb') as f:
f.write(img2pdf.convert([str(r) for r in rutas_mixtas]))
print(f"PDF sin pérdida creado con {len(rutas)} páginas")
```
## ReportLab — control total del layout
ReportLab permite posicionar imágenes con precisión milimétrica, añadir texto, encabezados, pie de página y watermarks:
```python
from reportlab.lib.pagesizes import A4, letter
from reportlab.platypus import SimpleDocTemplate, Image as RLImage, Spacer
from reportlab.lib.units import cm, mm
from reportlab.lib.utils import ImageReader
from pathlib import Path
def crear_pdf_con_layout(rutas_imagenes, ruta_pdf, titulo='Documento'):
"""Crea un PDF con encabezado y layout controlado."""
doc = SimpleDocTemplate(
ruta_pdf,
pagesize=A4,
topMargin=2*cm,
bottomMargin=2*cm,
leftMargin=1.5*cm,
rightMargin=1.5*cm,
)
ancho_pagina = A4[0] - 3*cm # ancho disponible
elementos = []
for ruta in rutas_imagenes:
# Ajustar imagen al ancho de página manteniendo proporción
img = RLImage(str(ruta), width=ancho_pagina)
img.hAlign = 'CENTER'
elementos.append(img)
elementos.append(Spacer(1, 0.5*cm))
doc.build(elementos)
print(f"PDF creado: {ruta_pdf}")
rutas = sorted(Path('imagenes/').glob('*.jpg'))
crear_pdf_con_layout(rutas, 'catalogo.pdf', titulo='Catálogo de Productos')
```
## ReportLab — PDF con encabezado, pie de página y número de página
```python
from reportlab.lib.pagesizes import A4
from reportlab.pdfgen import canvas
from reportlab.lib.utils import ImageReader
from pathlib import Path
def pdf_con_cabecera(rutas_imagenes, ruta_salida, titulo='Documento Escaneado'):
ancho, alto = A4
c = canvas.Canvas(str(ruta_salida), pagesize=A4)
margen = 40 # puntos (1 punto = 1/72 pulgada)
for num_pag, ruta_img in enumerate(rutas_imagenes, 1):
# Encabezado
c.setFont('Helvetica-Bold', 12)
c.drawString(margen, alto - 30, titulo)
# Pie de página con número
c.setFont('Helvetica', 9)
c.drawString(margen, 20, f'Página {num_pag} de {len(rutas_imagenes)}')
# Imagen centrada en la página (con margen)
ancho_img = ancho - 2 * margen
alto_img = alto - 80 # espacio para encabezado y pie
img_reader = ImageReader(str(ruta_img))
img_ancho_orig, img_alto_orig = img_reader.getSize()
escala = min(ancho_img / img_ancho_orig, alto_img / img_alto_orig)
ancho_final = img_ancho_orig * escala
alto_final = img_alto_orig * escala
x = (ancho - ancho_final) / 2
y = (alto - alto_final) / 2 - 10
c.drawImage(str(ruta_img), x, y, width=ancho_final, height=alto_final)
if num_pag < len(rutas_imagenes):
c.showPage()
c.save()
print(f"PDF con layout completo: {ruta_salida} ({len(rutas_imagenes)} páginas)")
rutas = sorted(Path('escaneos/').glob('*.jpg'))
pdf_con_cabecera(list(rutas), 'informe_completo.pdf', 'Informe de Auditoría 2024')
```
## Convertir múltiples formatos de imagen
```python
from PIL import Image
from pathlib import Path
import img2pdf, tempfile, os
def imagenes_mixtas_a_pdf(directorio, pdf_salida):
"""Convierte JPG+PNG+WEBP a PDF sin pérdida (convierte PNG/WEBP a JPG temporalmente)."""
directorio = Path(directorio)
temporales = []
rutas_jpg = []
try:
for archivo in sorted(directorio.iterdir()):
ext = archivo.suffix.lower()
if ext in ('.jpg', '.jpeg'):
rutas_jpg.append(str(archivo))
elif ext in ('.png', '.webp', '.bmp', '.tiff', '.tif'):
# Convertir a JPG temporal
tmp = tempfile.NamedTemporaryFile(suffix='.jpg', delete=False)
temporales.append(tmp.name)
img = Image.open(archivo).convert('RGB')
img.save(tmp.name, 'JPEG', quality=95)
rutas_jpg.append(tmp.name)
with open(pdf_salida, 'wb') as f:
f.write(img2pdf.convert(rutas_jpg))
print(f"PDF creado: {pdf_salida} ({len(rutas_jpg)} páginas)")
finally:
for tmp in temporales:
os.unlink(tmp)
imagenes_mixtas_a_pdf('documentos/', 'resultado.pdf')
```
## Cuándo usar cada herramienta
- **img2pdf**: documentos escaneados donde preservar calidad original es crítico. Es la opción más rápida y sin pérdida.
- **Pillow**: conversión sencilla cuando el tamaño del archivo importa y una ligera recompresión es aceptable.
- **ReportLab**: catálogos, informes o cualquier PDF donde necesitas control de layout, texto superpuesto, encabezados y pie de página.
Guía