Preflight Automático de PDFs con Python

Auditoría programática de cajas de sangrado, resolución efectiva y cobertura de tinta con PyMuPDF y Ghostscript

← Volver al Blog
✍️ Por Juan García 🐍 Python & Preimpresión ⏱️ 14 min de lectura 📅 Publicado: Noviembre 2026

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))
Integración en Portales Web: Este script tarda menos de 120 milisegundos en analizar un folleto de 16 páginas. Integrado detrás de un endpoint FastAPI o Flask, permite mostrar al cliente una luz verde o una lista exacta de páginas a corregir en el mismo instante en que sube el PDF a la web de la imprenta.

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:

  1. 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.
  2. 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.

JG

Juan García Fernández-Miranda

Técnico en Preimpresión & Desarrollador Python

Especialista en automatización de flujos de trabajo gráficos, auditoría programática de archivos PDF/X y optimización de procesos de taller mediante herramientas de código abierto.

← Anterior: Costes y OEE con SQL y Power BI Siguiente: Inteligencia de Precios y ETL con Python →