En una imprenta comercial o plataforma Web-to-Print que recibe entre 80 y 300 archivos PDF diarios, abrir cada documento manualmente en Acrobat Pro y pasar un perfil de Enfocus PitStop consume decenas de horas semanales de personal altamente cualificado. Lo más frustrante es que el 85% de los rechazos se deben siempre a los mismos cuatro errores: ausencia de sangrado (BleedBox igual al TrimBox), imágenes por debajo de 150 ppp efectivos, fuentes sin incrustar o espacios de color RGB sin convertir.
En esta entrega avanzada de nuestra serie de ingeniería gráfica vamos un paso más allá de nuestro artículo introductorio de automatización en Python: construiremos un motor completo de Preflight sin cabeza (Headless Preflight Pipeline) combinando la velocidad de PyMuPDF (fitz) con el intérprete industrial Ghostscript para emitir diagnósticos JSON instantáneos y normalizar archivos CMYK.
1. Anatomía de las 5 Cajas Geométricas de un PDF (PDF Boxes)
Para auditar un archivo de imprenta mediante código, primero debemos comprender cómo almacena el estándar ISO 32000 (PDF) las coordenadas físicas de una página. Toda unidad interna en un PDF se expresa en puntos postscript (1 pulgada = 72 pt; 1 mm = 2,83465 pt):
| Caja PDF (Box) | Propósito Técnico en Preimpresión | Relación Matemática Esperada |
|---|---|---|
| MediaBox | Tamaño total del pliego o lienzo virtual que incluye marcas de corte, tiras de control y sangrados. | La caja más grande que engloba a todas las demás. |
| BleedBox | Límite hasta donde debe extenderse la mancha gráfica de sangrado (habitualmente +3 mm por lado). | TrimBox + 6 mm en ancho y alto. |
| TrimBox | Formato final exacto por donde caerá la cuchilla de la guillotina (ej. 148 × 210 mm en un A5). | Referencia maestra para el software de imposición (Preps / Signa Station). |
| ArtBox / CropBox | Caja visual utilizada por lectores de pantalla como Adobe Reader. | No debe usarse como referencia de guillotina en taller. |
2. Auditoría de Sangrado, Fuentes y Resolución Efectiva Real con PyMuPDF
Un error muy común al auditar imágenes dentro de un PDF con scripts básicos es leer únicamente el ancho en píxeles del objeto XObject. Si una foto de 1.500 × 1.500 píxeles se coloca en un sello de 2 × 2 cm, su resolución efectiva supera los 1.900 ppp; pero si el diseñador la estira en un cartel de 50 × 50 cm, su resolución efectiva cae a unos inaceptables 76 ppp.
Con el método page.get_image_info(xrefs=True) de PyMuPDF podemos recuperar el rectángulo exacto (bbox) donde se dibuja cada imagen en la página y calcular sus DPI reales en el impreso:
import fitz # PyMuPDF
import json
PT_TO_MM = 25.4 / 72.0
PT_TO_INCH = 1.0 / 72.0
def auditar_pdf_preimpresion(ruta_pdf, sangrado_min_mm=2.9, dpi_minimo=250):
doc = fitz.open(ruta_pdf)
informe = {
"archivo": ruta_pdf,
"paginas": len(doc),
"apto_imprenta": True,
"alertas": []
}
for num_pag, page in enumerate(doc, start=1):
# 1. Auditar TrimBox y BleedBox
trim = page.trimbox
bleed = page.bleedbox
media = page.mediabox
ancho_trim_mm = round(trim.width * PT_TO_MM, 1)
alto_trim_mm = round(trim.height * PT_TO_MM, 1)
# Calcular sangrado efectivo disponible alrededor del TrimBox
caja_ref = bleed if bleed != trim else media
sangrado_izq = (trim.x0 - caja_ref.x0) * PT_TO_MM
sangrado_der = (caja_ref.x1 - trim.x1) * PT_TO_MM
sangrado_sup = (trim.y0 - caja_ref.y0) * PT_TO_MM
sangrado_inf = (caja_ref.y1 - trim.y1) * PT_TO_MM
min_sangrado = min(sangrado_izq, sangrado_der, sangrado_sup, sangrado_inf)
if min_sangrado < sangrado_min_mm:
informe["apto_imprenta"] = False
informe["alertas"].append(
f"Pág {num_pag} ({ancho_trim_mm}x{alto_trim_mm} mm): Sangrado insuficiente ({min_sangrado:.1f} mm). Mínimo exigido: 3.0 mm."
)
# 2. Auditar fuentes NO incrustadas
for font in page.get_fonts(full=True):
# font = (xref, ext, type, basefont, name, encoding, referencer)
ext, nombre_fuente = font[1], font[3]
if ext == "n/a":
informe["apto_imprenta"] = False
informe["alertas"].append(
f"Pág {num_pag}: La fuente '{nombre_fuente}' NO está incrustada en el PDF."
)
# 3. Auditar Resolución Efectiva (DPI en página) y Espacio de Color
for img in page.get_image_info(xrefs=True):
bbox = fitz.Rect(img["bbox"])
ancho_pulgadas = bbox.width * PT_TO_INCH
alto_pulgadas = bbox.height * PT_TO_INCH
if ancho_pulgadas > 0.05 and alto_pulgadas > 0.05:
dpi_x = img["width"] / ancho_pulgadas
dpi_y = img["height"] / alto_pulgadas
dpi_efectivo = round(min(dpi_x, dpi_y))
if dpi_efectivo < dpi_minimo:
informe["apto_imprenta"] = False
informe["alertas"].append(
f"Pág {num_pag}: Imagen (xref {img['xref']}) con baja resolución efectiva ({dpi_efectivo} ppp)."
)
if img["cs-name"] in ("DeviceRGB", "CalRGB", "ICCBased-RGB"):
informe["alertas"].append(
f"Pág {num_pag}: Imagen (xref {img['xref']}) en espacio {img['cs-name']} (se requiere CMYK)."
)
doc.close()
return informe
if __name__ == "__main__":
resultado = auditar_pdf_preimpresion("catalogo_cliente.pdf")
print(json.dumps(resultado, indent=2, ensure_ascii=False))
3. Cálculo de Cobertura de Tinta CMYK con Ghostscript (inkcov)
Mientras que PyMuPDF inspecciona la estructura vectorial y los diccionarios del PDF, Ghostscript actúa como un procesador RIP completo capaz de rasterizar cada página en memoria y medir qué porcentaje de la superficie consume cada canal de tinta (Cyan, Magenta, Amarillo y Negro). Esto tiene dos aplicaciones de altísimo valor económico:
- Presupuestación dinámica en impresión digital e inkjet industrial: No cuesta lo mismo imprimir una novela con un 4% de cobertura K que un catálogo con fondos negros y fotografías al 240% de tinta acumulada.
- Detección de páginas en color infiltradas en libros blanco y negro: Identifica al instante qué páginas exactas contienen algún elemento residual en CMY cuando el cliente ha contratado impresión interior solo en negro (1+1).
import subprocess
import re
def calcular_cobertura_tinta_gs(ruta_pdf, ejecutable_gs="gswin64c"):
"""
Invoca el dispositivo 'ink_cov' de Ghostscript para obtener el % medio de C, M, Y, K por página.
En Linux/macOS usa ejecutable_gs='gs'.
"""
comando = [
ejecutable_gs,
"-q",
"-o", "-",
"-sDEVICE=ink_cov",
ruta_pdf
]
proc = subprocess.run(comando, capture_output=True, text=True, check=True)
# Patrón de salida: " 0.04512 0.03100 0.02890 0.11240 CMYK OK"
patron = re.compile(r"^\s*([\d.]+)\s+([\d.]+)\s+([\d.]+)\s+([\d.]+)\s+CMYK", re.MULTILINE)
paginas_tinta = []
for idx, match in enumerate(patron.finditer(proc.stdout), start=1):
c, m, y, k = [round(float(val) * 100, 2) for val in match.groups()]
es_solo_negro = (c == 0.0 and m == 0.0 and y == 0.0)
paginas_tinta.append({
"pagina": idx,
"C_%": c, "M_%": m, "Y_%": y, "K_%": k,
"suma_media_%": round(c + m + y + k, 2),
"solo_negro_K": es_solo_negro
})
return paginas_tinta
4. Conversión Automatizada de RGB a CMYK FOGRA39 por Consola
Cuando el departamento de preimpresión necesita normalizar un lote de archivos enviados en RGB por clientes no técnicos a un espacio CMYK estandarizado (por ejemplo, CoatedFOGRA39.icc), Ghostscript permite realizar la conversión colorimétrica gestionada por perfil ICC sin abrir Adobe Acrobat:
gswin64c -dSAFER -dBATCH -dNOPAUSE -sDEVICE=pdfwrite ^ -sColorConversionStrategy=CMYK ^ -sDefaultCMYKProfile="C:/PerfilesICC/CoatedFOGRA39.icc" ^ -sOutputICCProfile="C:/PerfilesICC/CoatedFOGRA39.icc" ^ -dRenderIntent=1 ^ -sOutputFile="arte_final_cmyk_fogra39.pdf" ^ "archivo_original_rgb.pdf"
Observa el parámetro -dRenderIntent=1, que aplica el propósito de conversión Colorimétrico Relativo (el estándar en preimpresión europea ISO 12647-2, tal y como explicamos en nuestra guía de perfiles de color ICC). Si necesitas verificar antes si un color oscuro supera el límite de secado del soporte, recuerda que puedes usar directamente nuestra Calculadora Interactiva de Cobertura TAC.
5. Preguntas Frecuentes sobre Preflight de PDF con Python
¿Qué diferencia hay entre MediaBox, TrimBox y BleedBox dentro de un archivo PDF?
MediaBox es el tamaño total de la hoja virtual; TrimBox indica exactamente por dónde cortará la guillotina el producto terminado; y BleedBox delimita el área de sangrado exterior (normalmente 3 mm más por cada lado respecto al TrimBox). Un PDF exportado desde Word o Canva sin opciones de imprenta suele tener las tres cajas idénticas, careciendo de sangrado real.
¿Cómo detectar si una página de un libro en blanco y negro tiene color CMY oculto?
Ejecutando el dispositivo -sDEVICE=ink_cov de Ghostscript desde Python. Si en una página supuestamente en escala de grises los valores de C_%, M_% o Y_% son mayores que 0.00, significa que hay algún texto o imagen en negro compuesto (RGB/CMYK) que el contador del RIP facturaría a precio de clic de color.
¿Por qué no basta con leer los píxeles de una imagen incrustada en el PDF para saber sus DPI reales?
Porque un mismo mapa de bits puede colocarse a diferentes escalas dentro del documento PDF. Los ppp reales se obtienen dividiendo los píxeles de la matriz original entre el tamaño físico en pulgadas del marco (bbox) donde se renderiza en la página.