Qué son CBZ y CBR
CBZ y CBR son formatos contenedor para cómics digitales. Su nombre lo dice todo:
| Formato | Contenedor real | Extensión |
|---|---|---|
| CBZ | ZIP | .cbz |
| CBR | RAR | .cbr |
| CB7 | 7-Zip | .cb7 |
| CBT | TAR | .cbt |
| CBA | ACE | .cba (obsoleto) |
Internamente son archivos comprimidos que contienen imágenes (JPG, PNG, WebP) ordenadas por nombre de archivo. Los lectores de cómics (CDisplayEx, YACReader, Komga, Kavita, Ubooquity) extraen las imágenes en orden y las muestran como páginas.
Por qué CBZ es el rey hoy en día:
- ZIP es un estándar libre sin dependencias de patentes
- Python tiene soporte nativo con el módulo
zipfile - RAR requiere bibliotecas adicionales (unrar) que pueden tener restricciones de licencia
- CB7 con 7-Zip ofrece mejor compresión pero menor compatibilidad universal
Leer y extraer CBZ con Python
import zipfile
import os
from pathlib import Path
def listar_paginas_cbz(ruta_cbz):
"""Lista todas las imágenes de un CBZ en orden correcto."""
extensiones_imagen = {'.jpg', '.jpeg', '.png', '.webp', '.gif', '.bmp'}
with zipfile.ZipFile(ruta_cbz, 'r') as cbz:
archivos = sorted([
f for f in cbz.namelist()
if Path(f).suffix.lower() in extensiones_imagen
and not f.startswith('__MACOSX') # Excluir metadatos macOS
])
return archivos
def extraer_cbz(ruta_cbz, directorio_salida):
"""Extrae todas las páginas de un CBZ a un directorio."""
Path(directorio_salida).mkdir(parents=True, exist_ok=True)
paginas = listar_paginas_cbz(ruta_cbz)
with zipfile.ZipFile(ruta_cbz, 'r') as cbz:
for i, pagina in enumerate(paginas, 1):
ext = Path(pagina).suffix.lower()
nombre_salida = f"pagina_{i:04d}{ext}"
ruta_salida = Path(directorio_salida) / nombre_salida
datos = cbz.read(pagina)
ruta_salida.write_bytes(datos)
print(f"Extraídas {len(paginas)} páginas en '{directorio_salida}'")
return paginas
def info_cbz(ruta_cbz):
"""Muestra información detallada de un CBZ."""
paginas = listar_paginas_cbz(ruta_cbz)
tamaño = os.path.getsize(ruta_cbz) / 1024 / 1024
print(f"Archivo: {ruta_cbz}")
print(f"Páginas: {len(paginas)}")
print(f"Tamaño: {tamaño:.2f} MB")
print(f"Primera: {paginas[0] if paginas else 'N/A'}")
print(f"Última: {paginas[-1] if paginas else 'N/A'}")
return {'paginas': len(paginas), 'tamaño_mb': tamaño}
# Ejemplo
info_cbz('amazing_spiderman_001.cbz')
extraer_cbz('batman_001.cbz', 'paginas_batman')
Crear CBZ desde imágenes
import zipfile
from pathlib import Path
def crear_cbz(carpeta_imagenes, nombre_salida, calidad_jpg=85):
"""
Crea un CBZ desde una carpeta de imágenes.
Ordena los archivos numéricamente para orden correcto de páginas.
"""
carpeta = Path(carpeta_imagenes)
extensiones = {'.jpg', '.jpeg', '.png', '.webp', '.gif'}
imagenes = sorted([
f for f in carpeta.iterdir()
if f.suffix.lower() in extensiones
], key=lambda p: p.stem) # Orden alfanumérico del nombre
if not imagenes:
raise ValueError(f"No se encontraron imágenes en {carpeta}")
cbz_path = Path(nombre_salida)
if not cbz_path.suffix.lower() == '.cbz':
cbz_path = cbz_path.with_suffix('.cbz')
with zipfile.ZipFile(cbz_path, 'w', zipfile.ZIP_STORED) as cbz:
# ZIP_STORED: sin compresión adicional (las JPGs ya están comprimidas)
for i, imagen in enumerate(imagenes, 1):
# Renombrar con ceros a la izquierda para orden correcto
nombre_interno = f"{i:04d}{imagen.suffix.lower()}"
cbz.write(imagen, nombre_interno)
tamaño = cbz_path.stat().st_size / 1024 / 1024
print(f"CBZ creado: {cbz_path} ({len(imagenes)} páginas, {tamaño:.2f} MB)")
return str(cbz_path)
def crear_cbz_desde_pdf(ruta_pdf, nombre_salida, dpi=150):
"""Convierte un PDF a CBZ (útil para escaneos)."""
try:
from pdf2image import convert_from_path
except ImportError:
raise ImportError("pip install pdf2image")
import tempfile, zipfile
from pathlib import Path
print(f"Convirtiendo PDF a imágenes ({dpi} DPI)...")
paginas = convert_from_path(ruta_pdf, dpi=dpi)
cbz_path = Path(nombre_salida).with_suffix('.cbz')
with zipfile.ZipFile(cbz_path, 'w', zipfile.ZIP_STORED) as cbz:
for i, pagina in enumerate(paginas, 1):
nombre = f"{i:04d}.jpg"
# Guardar en memoria como bytes JPEG
import io
buf = io.BytesIO()
pagina.save(buf, 'JPEG', quality=90, optimize=True)
cbz.writestr(nombre, buf.getvalue())
print(f"CBZ creado: {cbz_path} ({len(paginas)} páginas)")
return str(cbz_path)
# Ejemplo
crear_cbz('escaneos/comic_001/', 'comic_001.cbz')
Extraer CBR (RAR) con Python
CBR requiere la herramienta unrar instalada en el sistema o la biblioteca rarfile:
def extraer_cbr(ruta_cbr, directorio_salida):
"""Extrae un CBR usando rarfile (requiere unrar/unar instalado)."""
try:
import rarfile
except ImportError:
raise ImportError("pip install rarfile # y además: apt install unrar")
Path(directorio_salida).mkdir(parents=True, exist_ok=True)
extensiones = {'.jpg', '.jpeg', '.png', '.webp'}
with rarfile.RarFile(ruta_cbr) as cbr:
archivos = sorted([
f for f in cbr.namelist()
if Path(f).suffix.lower() in extensiones
])
for i, archivo in enumerate(archivos, 1):
ext = Path(archivo).suffix.lower()
dest = Path(directorio_salida) / f"pagina_{i:04d}{ext}"
datos = cbr.read(archivo)
dest.write_bytes(datos)
print(f"CBR extraído: {len(archivos)} páginas en '{directorio_salida}'")
return archivos
def cbr_a_cbz(ruta_cbr, ruta_cbz=None):
"""Convierte un CBR a CBZ (más portable y sin dependencias RAR)."""
try:
import rarfile
except ImportError:
raise ImportError("pip install rarfile")
import tempfile, zipfile
cbr_path = Path(ruta_cbr)
if ruta_cbz is None:
ruta_cbz = cbr_path.with_suffix('.cbz')
extensiones = {'.jpg', '.jpeg', '.png', '.webp', '.gif'}
with rarfile.RarFile(ruta_cbr) as cbr:
archivos = sorted([
f for f in cbr.namelist()
if Path(f).suffix.lower() in extensiones
])
with zipfile.ZipFile(ruta_cbz, 'w', zipfile.ZIP_STORED) as cbz:
for i, archivo in enumerate(archivos, 1):
ext = Path(archivo).suffix.lower()
nombre = f"{i:04d}{ext}"
datos = cbr.read(archivo)
cbz.writestr(nombre, datos)
orig = os.path.getsize(ruta_cbr) / 1024 / 1024
nuevo = os.path.getsize(ruta_cbz) / 1024 / 1024
print(f"CBR→CBZ: {ruta_cbz} ({orig:.1f}MB → {nuevo:.1f}MB)")
return str(ruta_cbz)
Convertir CBZ a PDF
from pathlib import Path
import zipfile
from PIL import Image
import io
def cbz_a_pdf(ruta_cbz, ruta_pdf=None, dpi_destino=150):
"""
Convierte un CBZ a PDF.
Ideal para enviar a lectores de eBooks o impresión.
"""
cbz_path = Path(ruta_cbz)
if ruta_pdf is None:
ruta_pdf = cbz_path.with_suffix('.pdf')
extensiones = {'.jpg', '.jpeg', '.png', '.webp', '.gif'}
print(f"Convirtiendo {cbz_path.name} a PDF...")
paginas_pil = []
with zipfile.ZipFile(ruta_cbz, 'r') as cbz:
archivos = sorted([
f for f in cbz.namelist()
if Path(f).suffix.lower() in extensiones
and not f.startswith('__MACOSX')
])
for archivo in archivos:
datos = cbz.read(archivo)
img = Image.open(io.BytesIO(datos))
if img.mode != 'RGB':
img = img.convert('RGB')
paginas_pil.append(img)
if not paginas_pil:
raise ValueError("No se encontraron imágenes en el CBZ")
# La primera página como base, el resto como páginas adicionales
paginas_pil[0].save(
ruta_pdf,
save_all=True,
append_images=paginas_pil[1:],
resolution=dpi_destino
)
tamaño = Path(ruta_pdf).stat().st_size / 1024 / 1024
print(f"PDF creado: {ruta_pdf} ({len(paginas_pil)} páginas, {tamaño:.1f} MB)")
return str(ruta_pdf)
# Ejemplo
cbz_a_pdf('comic_001.cbz')
Gestionar una colección de cómics
import zipfile
from pathlib import Path
import json
from datetime import datetime
def escanear_coleccion(directorio_raiz):
"""Indexa todos los CBZ/CBR de una colección."""
raiz = Path(directorio_raiz)
coleccion = []
for archivo in sorted(raiz.rglob('*')):
if archivo.suffix.lower() not in {'.cbz', '.cbr', '.cb7'}:
continue
try:
info = {
'ruta': str(archivo.relative_to(raiz)),
'nombre': archivo.stem,
'formato': archivo.suffix.lower()[1:].upper(),
'tamaño_mb': archivo.stat().st_size / 1024 / 1024,
'modificado': datetime.fromtimestamp(
archivo.stat().st_mtime
).strftime('%Y-%m-%d'),
}
if archivo.suffix.lower() == '.cbz':
with zipfile.ZipFile(archivo) as cbz:
exts = {'.jpg', '.jpeg', '.png', '.webp'}
paginas = [
f for f in cbz.namelist()
if Path(f).suffix.lower() in exts
]
info['paginas'] = len(paginas)
else:
info['paginas'] = None # CBR requiere rarfile
coleccion.append(info)
except Exception as e:
print(f" Error leyendo {archivo.name}: {e}")
print(f"Colección indexada: {len(coleccion)} cómics")
return coleccion
def exportar_indice(coleccion, archivo_json='coleccion.json'):
"""Guarda el índice de la colección como JSON."""
with open(archivo_json, 'w', encoding='utf-8') as f:
json.dump(coleccion, f, ensure_ascii=False, indent=2)
print(f"Índice guardado: {archivo_json}")
def renombrar_coleccion(directorio):
"""
Estandariza nombres de archivos CBZ:
'batman #001 (2011).cbz' → 'batman_001_2011.cbz'
"""
import re
cambios = []
for f in Path(directorio).glob('*.cbz'):
nombre = f.stem
# Normalizar: minúsculas, espacios→guion_bajo, eliminar caracteres especiales
nuevo = re.sub(r'[^\w\s-]', '', nombre.lower())
nuevo = re.sub(r'[\s-]+', '_', nuevo).strip('_')
nuevo = f"{nuevo}.cbz"
if nuevo != f.name:
f.rename(f.parent / nuevo)
cambios.append((f.name, nuevo))
print(f"Renombrados: {len(cambios)} archivos")
return cambios
# Uso
coleccion = escanear_coleccion('D:/Comics/')
exportar_indice(coleccion)
Añadir portada a un CBZ
from PIL import Image, ImageDraw, ImageFont
import zipfile, io
from pathlib import Path
def generar_portada(titulo, numero, salida='portada.jpg'):
"""Genera una portada básica con Pillow."""
ancho, alto = 1200, 1800
img = Image.new('RGB', (ancho, alto), color='#1a1a2e')
d = ImageDraw.Draw(img)
# Marco decorativo
d.rectangle([30, 30, ancho-30, alto-30], outline='#e0e0e0', width=4)
# Título
d.text((ancho//2, alto//3), titulo, fill='#f72585',
anchor='mm', font_size=96)
# Número
d.text((ancho//2, alto//2), f'#{numero:03d}', fill='#4cc9f0',
anchor='mm', font_size=72)
img.save(salida, 'JPEG', quality=90)
return salida
def añadir_portada_cbz(ruta_cbz, imagen_portada):
"""Inserta una portada como primera imagen del CBZ."""
cbz_path = Path(ruta_cbz)
tmp_path = cbz_path.with_suffix('.tmp.cbz')
with zipfile.ZipFile(ruta_cbz, 'r') as original:
with zipfile.ZipFile(tmp_path, 'w', zipfile.ZIP_STORED) as nuevo:
# Portada como página 0000
datos_portada = Path(imagen_portada).read_bytes()
ext = Path(imagen_portada).suffix.lower()
nuevo.writestr(f'0000_portada{ext}', datos_portada)
# Resto de páginas
exts = {'.jpg', '.jpeg', '.png', '.webp'}
paginas = sorted([
f for f in original.namelist()
if Path(f).suffix.lower() in exts
])
for pagina in paginas:
nuevo.writestr(pagina, original.read(pagina))
tmp_path.replace(cbz_path)
print(f"Portada añadida a {ruta_cbz}")
CBZ vs PDF para cómics: comparativa
| Criterio | CBZ | |
|---|---|---|
| Calidad imagen | Original sin pérdida | Depende de exportación |
| Tamaño | Similar al ZIP de imágenes | Similar o mayor |
| Soporte lectores cómics | ✅ Universal (Komga, YACReader…) | ⚠️ Parcial |
| Soporte eReaders | ⚠️ Solo algunos (Kindle no) | ✅ Universal |
| Zoom/pan | ✅ Nativo en lectores | ✅ Nativo en visores PDF |
| Metadatos | ⚠️ Básico (ComicInfo.xml) | ✅ Avanzados |
| Búsqueda texto | ❌ Solo imágenes | ✅ Si tiene OCR |
| DRM | ❌ No soportado | ✅ Opcional |
ComicInfo.xml — metadatos estándar
def crear_comicinfo(titulo, numero, año, escritor, artista, editorial):
"""Genera el archivo ComicInfo.xml estándar para CBZ."""
xml = f'''<?xml version="1.0" encoding="utf-8"?>
<ComicInfo xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:xsd="http://www.w3.org/2001/XMLSchema">
<Title>{titulo}</Title>
<Number>{numero}</Number>
<Year>{año}</Year>
<Writer>{escritor}</Writer>
<Penciller>{artista}</Penciller>
<Publisher>{editorial}</Publisher>
<LanguageISO>es</LanguageISO>
<Format>Comic Book Archive</Format>
</ComicInfo>'''
return xml
def añadir_metadatos_cbz(ruta_cbz, meta):
"""Inserta ComicInfo.xml en un CBZ existente."""
cbz_path = Path(ruta_cbz)
tmp_path = cbz_path.with_suffix('.tmp.cbz')
xml = crear_comicinfo(**meta)
with zipfile.ZipFile(ruta_cbz, 'r') as orig:
with zipfile.ZipFile(tmp_path, 'w', zipfile.ZIP_STORED) as nuevo:
nuevo.writestr('ComicInfo.xml', xml)
for item in orig.namelist():
nuevo.writestr(item, orig.read(item))
tmp_path.replace(cbz_path)
print(f"Metadatos añadidos a {ruta_cbz}")
# Uso
añadir_metadatos_cbz('batman_001.cbz', {
'titulo': 'Batman',
'numero': 1,
'año': 2011,
'escritor': 'Scott Snyder',
'artista': 'Greg Capullo',
'editorial': 'DC Comics',
})
Conclusión
CBZ es el formato preferido para cómics digitales por su simplicidad (ZIP estándar) y soporte universal. Python permite trabajar con colecciones completas sin instalar software adicional para CBZ. Para CBR (RAR), la biblioteca rarfile facilita la migración a CBZ, eliminando la dependencia de unrar. El flujo recomendado para colecciones: escanear → convertir CBR a CBZ → añadir ComicInfo.xml → organizar con nombres estándar.
Conversiones relacionadas
Conversiones frecuentes del catálogo: