Artículo técnico

Validación de PDF/X en Delphi con el PDFium Component

El PDFium Component para Delphi valida documentos PDF/X listos para imprimir a través de TPdf.ValidatePdfX, que implementa la comprobación de la norma ISO 15930 en dos capas: ocho comprobaciones de contenido a nivel de bytes (compresión LZW prohibida, JavaScript, campos de formulario, referencias OPI, falta de TrimBox, clave Trapped no configurada y más) más una pasada por el modelo de objetos de PDFium que utiliza FPDFFont_GetIsEmbedded para verificar la incrustación de fuentes en cada objeto de texto de cada página. El resultado es un registro TPdfXValidationResult que nombra el nivel de conformidad detectado y enumera cada infracción como una enumeración con tipo, de modo que su aplicación Delphi pueda decirle a un cliente exactamente por qué un archivo será rechazado en la imprenta antes de que nadie gaste una plancha

Si alguna vez ha enviado un trabajo a una imprenta comercial por mensajería y se lo han devuelto con un rechazo de una sola línea ("sin TrimBox", "fuentes no incrustadas", "Trapped no configurado"), conocerá el coste de enterarse tarde. PDF/X es el equivalente para preimpresión de PDF/A: mientras que PDF/A para archivado garantiza que un documento se renderice de forma idéntica dentro de décadas, PDF/X garantiza que un documento se separe, se imprima y se recorte de forma idéntica en el RIP de otra persona mañana por la mañana. Ambos estándares comparten maquinaria (identificación XMP, OutputIntents, perfiles ICC incrustados) pero responden a preguntas diferentes, razón por la cual el componente distribuye validadores separados para cada uno (el lado de PDF/A se cubre en el artículo sobre validación preliminar de PDF/A con el PDFium Component)

¿Qué exige realmente la norma ISO 15930 de un PDF listo para imprimir?

La norma ISO 15930 existe para hacer posible el intercambio a ciegas (blind exchange): un diseñador entrega un archivo a un impresor con el que nunca ha hablado, y el impresor puede producir una salida correcta sin llamadas telefónicas, sin correos electrónicos por fuentes faltantes y sin imágenes enlazadas que se hayan quedado en el ordenador portátil del diseñador. Cada regla del estándar sirve a ese propósito. Las fuentes deben estar incrustadas porque no se puede asumir que el RIP receptor las posea. Las referencias externas están prohibidas porque el archivo debe estar completo en sí mismo. Las funciones interactivas están prohibidas porque la tinta no tiene un controlador de evento al hacer clic (onclick)

El PDFium Component reconoce tres familias de conformidad y las notifica a través de la enumeración TPdfXConformance en el resultado de la validación: pxc1a para PDF/X-1a:2001 (ISO 15930-1, la estricta línea base CMYK más colores planos en PDF 1.3/1.4), pxc3 para PDF/X-3:2002 (ISO 15930-3, que admite color gestionado por RGB, Lab e ICC) y pxc4 para PDF/X-4:2010 (ISO 15930-7, que finalmente permite transparencia en vivo y capas sobre una base PDF 1.6). Un archivo que no lleva ninguna identificación de PDF/X regresa como pxcNone, lo que en sí mismo es una respuesta útil: el documento nunca pretendió estar listo para imprimir, y todo lo demás que informa el validador explica lo que se necesitaría para llegar allí

Las prohibiciones tienen sentido una vez que se piensa como un proveedor de RIP. /LZWDecode está prohibido en todas las variantes de PDF/X para que un consumidor conforme nunca dependa de un filtro con historial de compatibilidad y licencias; Flate hace el mismo trabajo sin ese lastre. JavaScript, los campos de AcroForm y los diccionarios de acciones adicionales /AA están prohibidos porque un archivo de impresión debe ser una descripción fija de marcas en papel; cualquier cosa que pueda mutar la apariencia en el momento de la apertura rompe la garantía de que lo que se probó es lo que se imprime. Los marcadores de posición OPI (Open Prepress Interface) están prohibidos porque son, por diseño, referencias a imágenes de alta resolución almacenadas en otro lugar, y ese "otro lugar" es exactamente lo que prohíbe el intercambio a ciegas

¿Por qué las imprentas rechazan los PDF sin un TrimBox?

El TrimBox es la página terminada: el rectángulo que queda después de los cortes de la guillotina. El MediaBox, que tienen todas las páginas PDF, es simplemente la hoja: incluye sangrado (bleed), marcas de corte, objetivos de registro y barras de color. El software de imposición posiciona las páginas en una hoja de prensa mediante sus TrimBoxes; sin uno, el operador tiene que adivinar dónde termina realmente su tarjeta de presentación, y una suposición incorrecta recorta su sangrado o deja una franja blanca en un borde. Es por eso que la norma ISO 15930 exige un TrimBox (o un ArtBox) en cada página, y por lo que ValidatePdfX genera pvxiMissingTrimBox cuando no se encuentra la clave /TrimBox en ninguna página del documento

La clave /Trapped responde a una pregunta de producción diferente. El reventado (trapping) es la técnica de preimpresión que consiste en superponer ligeramente los colores adyacentes para que los pequeños desajustes de registro de la prensa no abran huecos blancos entre ellos. El impresor necesita saber si ese trabajo ya se ha realizado: aplicar trapping a un archivo que ya lo tiene duplica las superposiciones, y omitirlo en un archivo que no lo tiene genera el riesgo de huecos visibles. Por lo tanto, PDF/X exige que el diccionario Info declare /Trapped /True o /Trapped /False de forma explícita; una clave faltante o un valor /Unknown obliga a un humano a inspeccionar el archivo, que es precisamente la conversación que el intercambio a ciegas debía eliminar. El componente marca esto como pvxiTrappedNotSet

Ejecutar la validación en dos capas con TPdf.ValidatePdfX

TPdf.ValidatePdfX no recibe argumentos y devuelve un registro TPdfXValidationResult con tres miembros: Conformance (el tipo de PDF/X detectado), Issues (un conjunto Pascal de valores TPdfXValidationIssue) y un ayudante IsCompliant. Internamente, serializa el documento cargado en un flujo de memoria, ejecuta el inspector a nivel de bytes sobre él y luego recorre el modelo de objetos de PDFium para la comprobación de incrustación de fuentes por cada fuente. Un filtro preliminar (preflight gate) mínimo tiene el siguiente aspecto:

uses PDFium, FPdfPdfx;

procedure CheckPrintReadiness(const FileName: string);
var
  Pdf: TPdf;
  Res: TPdfXValidationResult;
begin
  Pdf := TPdf.Create(nil);
  try
    Pdf.FileName := FileName;
    Pdf.Active := True;

    Res := Pdf.ValidatePdfX;
    Writeln('Detected conformance: ',
      Ord(Res.Conformance)); // pxc1a, pxc3, pxc4, pxcNone...

    if Res.IsCompliant then
      Writeln('PDF/X checks passed')
    else
    begin
      if pvxiMissingTrimBox in Res.Issues then
        Writeln('REJECT: no /TrimBox on the pages');
      if pvxiTrappedNotSet in Res.Issues then
        Writeln('REJECT: /Trapped missing or /Unknown');
      if pvxiPdfiumFontNotEmbedded in Res.Issues then
        Writeln('REJECT: a page uses a non-embedded font');
      if pvxiLzwForbidden in Res.Issues then
        Writeln('REJECT: LZWDecode filter present');
    end;
  finally
    Pdf.Free;
  end;
end;

Dado que Issues es un conjunto de Pascal común, puede particionarlo según lo necesite su flujo de trabajo: trate los problemas estructurales como rechazos definitivos (hard rejects), trate pvxiMissingTitle (un SHOULD en el estándar, no un MUST) como una advertencia y registre (log) el resto. El mismo tipo de registro también alimenta al generador de informes del componente, por lo que si prefiere emitir un documento legible por humanos en lugar de ramificar según las enumeraciones, el patrón del artículo sobre la creación de una CLI de informes de validación preliminar por lotes con el PDFium Component se aplica a PDF/X sin cambios

Lo que detecta la capa a nivel de bytes y lo que se le escapa

La capa a nivel de bytes es un escaneo de tokens sobre los bytes estructurales del documento con los cuerpos de los flujos vaciados, por lo que un JPEG que casualmente contenga el patrón de bytes /JavaScript no puede activar un falso positivo. Además de las comprobaciones de marcadores (XMP pdfxid:GTS_PDFXVersion, OutputIntent con un perfil ICC incrustado, el /ID del trailer, la prohibición de cifrado), la pasada de contenido añade ocho comprobaciones, cada una con su propio valor de enumeración:

  • pvxiLzwForbidden: aparece un filtro /LZWDecode en cualquier parte del archivo (prohibido en todas las variantes de PDF/X)
  • pvxiJavaScriptForbidden: está presente una acción /JavaScript o un árbol de nombres
  • pvxiFormFieldsForbidden: existe un diccionario /AcroForm o una entrada /XFA
  • pvxiAdditionalActions: está presente un diccionario de acciones adicionales /AA
  • pvxiEmbeddedFilesForbidden: está presente /EmbeddedFiles o una anotación /FileAttachment
  • pvxiOpiForbidden: una entrada /OPI o /Alternates hace referencia a contenido de imagen reemplazable
  • pvxiMissingTrimBox: no se encontró ningún /TrimBox en ninguna página
  • pvxiTrappedNotSet: /Trapped está ausente o configurado como /Unknown

El escaneo de bytes es rápido y no necesita un motor de renderizado, pero tiene un punto ciego inherente con las fuentes: a ese nivel, el inspector solo puede aplicar una heurística aproximada (marca un documento cuando no encuentra ningún programa de fuentes incrustado en absoluto). Un archivo con nueve fuentes incrustadas y una fuente del sistema introducida parece correcto en un escaneo de bytes. Esa única brecha es la razón por la que existe la segunda capa

Incrustación por fuente a través del modelo de objetos de PDFium

La capa del modelo de objetos del PDFium Component responde con precisión a la pregunta de las fuentes. Después de la pasada a nivel de bytes, TPdf.ValidatePdfX recorre cada página, solicita la lista de objetos a FPDFPage_CountObjects y para cada objeto de texto resuelve el controlador de fuente a través de FPDFTextObj_GetFont y consulta FPDFFont_GetIsEmbedded. Una sola fuente no incrustada en cualquier parte del documento añade pvxiPdfiumFontNotEmbedded al conjunto de problemas. El recorrido realiza un cortocircuito a dos niveles (deja de escanear objetos en una página y deja de cargar más páginas en el momento en que se confirma el problema), por lo que en un catálogo de 300 páginas con infracciones, el veredicto suele llegar después de la página uno

Vale la pena conocer dos notas de límites. Primero, esta capa necesita que la biblioteca PDFium esté cargada y requiere compilaciones que exporten FPDFFont_GetIsEmbedded; cuando la exportación está ausente, la comprobación se omite en lugar de fallar, por lo que una DLL más antigua nunca produce rechazos fantasma. Segundo, la comprobación responde "incrustado o no" y nada más (no distingue la incrustación completa del subconjunto (subsetting), ni inspecciona la cobertura de glifos). Cuando un archivo falla y necesita saber qué fuente falló en qué página, las técnicas de enumeración del artículo sobre el análisis de las propiedades de las fuentes PDF con PDFium en Delphi retoman exactamente donde lo deja el booleano del validador

Validar flujos sin cargar un documento ni la DLL

El inspector a nivel de bytes también se expone como una función independiente, ValidatePdfXCompliance(Source: TStream) en la unidad FPdfPdfx, y es Object Pascal puro sin dependencia de la DLL de PDFium. Eso hace que sea implementable en lugares donde un motor de renderizado no es bienvenido: un filtro de carga ligero en un servidor web, un trabajo de CI que revise ilustraciones generadas o un servicio Lazarus en una plataforma donde preferiría no distribuir binarios nativos. Aliméntelo con cualquier flujo con capacidad de búsqueda (seekable):

uses Classes, FPdfPdfx;

function QuickPdfXGate(const FileName: string): Boolean;
var
  Fs: TFileStream;
  Res: TPdfXValidationResult;
begin
  Fs := TFileStream.Create(FileName, fmOpenRead or fmShareDenyWrite);
  try
    Res := ValidatePdfXCompliance(Fs);
    Result := Res.IsCompliant and (Res.Conformance <> pxcNone);
  finally
    Fs.Free;
  end;
end;

El compromiso es explícito: la ruta independiente ejecuta las comprobaciones de marcadores y las ocho comprobaciones de contenido, pero no la capa de PDFium por fuente, por lo que su veredicto de fuentes recurre a la heurística aproximada. Una arquitectura sensata utiliza ValidatePdfXCompliance como primer filtro económico y reserva el TPdf.ValidatePdfX completo para los archivos que lo aprueban

Dónde termina este validador y dónde comienza una validación preliminar completa

La honestidad importa en las herramientas de validación preliminar (preflight), por lo que aquí está el límite. ValidatePdfX verifica los marcadores de identificación, las prohibiciones estructurales, las claves de geometría de la página, la declaración de Trapped y la incrustación de fuentes hasta los objetos de texto individuales. No mide la cobertura total de tinta, ni valida que cada espacio de color sea legal para la variante declarada (la regla CMYK exclusiva de X-1a, por ejemplo), ni comprueba la resolución de la imagen frente a la trama de línea, ni evalúa el comportamiento de sobreimpresión y acoplado de transparencias; esos requieren un motor de validación preliminar con gestión de color, y la propia documentación de la unidad aconseja emparejarlo con uno para la certificación final. Lo que le brinda la comprobación de dos capas es el 80% de los rechazos que son estructurales y detectables tempranamente, atrapados en milisegundos dentro de su propio código Delphi en lugar de en el correo electrónico de mañana del impresor

Ambas capas de validación, las API de inyección de marcadores PDF/X para producir una salida conforme y los validadores de PDF/A, PDF/UA, PDF/E y PDF/VT que comparten la misma arquitectura se distribuyen en el PDFium Component para Delphi y C++Builder: un solo componente, desde el renderizado hasta el control de preimpresión