Artículo técnico

Reportes preflight PDF por lotes en Delphi con PDFium Component CLI

Una oficina de digitalización para la que asesoré ejecutaba un trabajo nocturno que marcaba miles de registros digitalizados como "listos para archivo". A los seis meses, un auditor externo tomó una muestra del archivo con veraPDF y encontró violaciones PDF/A en archivos que el trabajo había dejado pasar. El trabajo no había mentido, exactamente: había comprobado el perfil equivocado, había colapsado cada resultado a un único bit de aprobado/reprobado y había descartado los archivos de reporte que habrían expuesto la discrepancia de inmediato. Tengan ese incidente presente cuando conecten el motor preflight de PDFium Component, una biblioteca PDF con código fuente para Delphi, C++Builder y Lazarus, a una herramienta de línea de comandos: las llamadas de validación son la parte fácil, y el contrato que las rodea, perfiles, códigos de salida y retención de reportes, es donde el preflight por lotes funciona o se pudre en silencio.

El contrato: lo que un programador de tareas puede ver

Un runner de CI o el Programador de tareas de Windows ve exactamente dos cosas de su herramienta: el código de salida y los archivos que dejó atrás. Todo lo demás, líneas de log, colores de consola, salida de progreso, es para humanos mirando en vivo. Así que antes de tocar la API, fijen el vocabulario de códigos de salida y manténganlo aburrido:

  • 0: cada archivo cumplió con cada perfil solicitado
  • 1: al menos un archivo produjo hallazgos de validación
  • 2: la herramienta misma falló en al menos un archivo, por entrada corrupta, bloqueo o caída

La distinción entre los códigos 1 y 2 es la que los equipos saltan y luego lamentan. Un PDF corrupto que no puede abrirse no es una falla de validación, y tratarlo como tal significa que un camión de escaneos dañados aparecerá en sus métricas como un colapso súbito de conformidad en lugar del incidente operativo que realmente es.

Dos elementos más del contrato merecen una bandera cada uno: un timeout por archivo y un directorio de cuarentena. Un PDF patológico, con miles de páginas o estructuras de objetos profundamente anidadas, puede retener una pasada de validación durante minutos, y a una ventana nocturna no le importa por qué. Terminen el trabajo del archivo en la fecha límite, cuéntenlo como falla de herramienta, muevan la entrada aparte para inspección y mantengan el lote avanzando. El directorio de cuarentena se convierte entonces en un corpus que se recolecta solo con los peores documentos que sus clientes realmente envían, algo más valioso para pruebas de release que cualquier muestra sintética.

Elegir estándares: PDF/A-2b no es PDF/A-3a

La enumeración TPdfPreflightStandard selecciona las familias de estándares que importan en la práctica: ppsPdfA para conformidad de archivo ISO 19005, ppsPdfUa para accesibilidad ISO 14289, ppsPdfX para intercambio de impresión, más ppsPdfE, ppsPdfR y ppsPdfVT para flujos de ingeniería, ráster y datos variables. Dentro de una familia, el motor detecta el nivel de conformidad que el documento declara y lo reporta por estándar en el ConformanceName del resultado; y el nivel importa. PDF/A-2b afirma solo reproducibilidad visual; PDF/A-3a además exige etiquetado de estructura lógica y permite archivos fuente incrustados, una barra más estricta y mucho más difícil para material escaneado. Si la política de retención dice PDF/A-2b, reprobar archivos por etiquetas de estructura faltantes inunda el reporte con hallazgos que nadie piensa corregir; aceptar cualquier etiqueta PDF/A sin revisar el nivel promete menos de lo debido. Los mandatos gubernamentales de accesibilidad agregan cada vez más PDF/UA encima, y no cuesta nada incluirlo porque BuildPdfPreflightReport, de la unidad FPdfPreflightReport, acepta un conjunto:

Report := BuildPdfPreflightReport(Pdf, [ppsPdfA, ppsPdfUa]);

Una llamada, ambos estándares, un registro de reporte consolidado.

Leer el reporte: el silencio no es conformidad

El reporte enumera hallazgos por estándar. Una lista vacía de problemas, por lo tanto, significa "no se encontraron problemas en los estándares que se ejecutaron", que no es la misma afirmación que "el archivo cumple con el estándar que les importa". Si un error de configuración quitó ppsPdfA del conjunto, la lista de problemas queda igual de vacía. Recorran siempre Report.Results y afirmen dos cosas por cada estándar previsto: que existe una entrada de resultado para él y que su bandera IsCompliant, respaldada por Status = pfsPass, es verdadera. Este es precisamente el modo de falla detrás de la historia de auditoría anterior: el trabajo equiparó "sin hallazgos" con "listo para archivo" sin comprobar jamás qué estándares se habían evaluado.

La segunda trampa se esconde en qué es un hallazgo: cada TPdfPreflightIssue trae un Code, una Category, una Description y una Recommendation; nombra la regla violada, no un número de página. Eso da forma al ciclo de retroalimentación: el reporte le dice al equipo productor qué clase de defecto corregir, como una fuente no incrustada o un identificador XMP faltante, y ubicar el objeto ofensivo específico es trabajo de la herramienta de remediación, no del validador. Escriban los consumidores de reportes contra los valores estables de Code, no analizando texto de descripción que puede cambiar entre releases.

Archivos de reporte para máquinas y para la persona de guardia

El registro de reporte escribe los mismos hallazgos en cinco formatos: SaveJsonToFile, SaveCsvToFile, SaveHtmlToFile, SaveTextToFile y SaveMarkdownToFile, con funciones estilo ToJson equivalentes cuando quieren la cadena en lugar de un archivo. Resistan elegir solo uno. JSON es para el pipeline: adjúntenlo al registro del trabajo y dejen que CI analice códigos de problema y estados por estándar. HTML es para el operador al que despiertan: abre en cualquier navegador sin herramientas. Escribir ambos cuesta una línea adicional por archivo y elimina la peor experiencia de guardia en procesamiento por lotes, que es descifrar un blob JSON a las dos de la mañana. Mantengan nombres determinísticos, derivados del nombre del archivo de entrada y nunca de un timestamp, o las corridas paralelas mezclarán reportes que no podrán empatar con sus entradas.

Los umbrales de severidad pertenecen a la configuración, no al código. El mismo hallazgo, por ejemplo una anotación sin descripción alternativa, es falla dura para un portal de envío PDF/UA y una nota ignorable para un archivo interno. Expongan un nivel de fail-on por perfil para que la política pueda cambiar sin recompilar, y registren el nivel vigente dentro del resumen del trabajo, porque el próximo trimestre nadie recordará qué umbral usó el lote de octubre pasado.

Aislar archivos para que un PDF malo no hunda el lote

procedure RunPreflightBatch(const InputDir, ReportDir: string;
  out FilesWithFindings, ToolFailures: Integer);
var
  SR: TSearchRec;
  Pdf: TPdf;
  Report: TPdfPreflightReport;
begin
  FilesWithFindings := 0;
  ToolFailures := 0;
  if FindFirst(InputDir + '*.pdf', faAnyFile, SR) = 0 then
  try
    repeat
      Pdf := TPdf.Create(nil);   // fresh instance per file: no state bleed
      try
        try
          Pdf.FileName := InputDir + SR.Name;
          Pdf.Active := True;
          if not Pdf.Active then  // load failures are silent, not raised
            raise EPdfError.Create('Cannot open ' + SR.Name);
          Report := BuildPdfPreflightReport(Pdf, [ppsPdfA, ppsPdfUa]);
          Report.SaveJsonToFile(ReportDir + ChangeFileExt(SR.Name, '.json'));
          Report.SaveHtmlToFile(ReportDir + ChangeFileExt(SR.Name, '.html'));
          if Report.TotalIssueCount > 0 then
            Inc(FilesWithFindings);
        except
          on E: Exception do
          begin
            Inc(ToolFailures);   // exit-code-2 territory, not a validation verdict
            WriteLn(ErrOutput, SR.Name + ': ' + E.Message);
          end;
        end;
      finally
        Pdf.Free;
      end;
    until FindNext(SR) <> 0;
  finally
    FindClose(SR);
  end;
end;

Tres decisiones deliberadas viven en ese bucle. Un TPdf nuevo por archivo garantiza que un documento que corrompa estado del motor no contamine a sus sucesores. La comprobación explícita de Active importa porque establecer Active := True absorbe errores de carga en vez de lanzarlos; sin la guarda, un archivo truncado se deslizaría hasta la llamada de validación antes de fallar, con un mensaje engañoso. El try..except interno se ubica dentro del alcance por archivo, de modo que una excepción incrementa el contador de fallas y el bucle continúa: el auditor quiere reportes para los 4,999 archivos buenos aunque el archivo 5,000 esté truncado. Y ambos formatos de reporte se escriben antes de contabilizar el veredicto, así que la evidencia sobrevive aunque la lógica de resumen tenga un bug.

El mapeo de códigos de salida se reduce entonces a unas pocas líneas en el archivo de proyecto:

begin
  RunPreflightBatch(ParamStr(1), ParamStr(2), Findings, Failures);
  if Failures > 0 then
    Halt(2)
  else if Findings > 0 then
    Halt(1);
  // falling through exits with 0: every file conformed
end.

Lo que preflight no hará por ustedes

El motor detecta; no repara. Un hallazgo sobre una fuente no incrustada o un espacio de color dependiente del dispositivo es una orden de trabajo para quien produce los archivos, no algo que el validador pueda parchear in situ. Planifiquen el ciclo de retroalimentación en consecuencia: los reportes deben llegar a donde el equipo productor los lee, o los mismos hallazgos reaparecerán cada noche hasta que alguien se pregunte por qué la curva nunca baja. También conviene cruzar una muestra de veredictos contra un validador independiente, veraPDF para PDF/A, preflight de Acrobat para PDF/X, antes de que lo haga un auditor externo. Dos motores de acuerdo son evidencia sólida; un solo motor es una opinión.

Preguntas frecuentes

¿Puedo validar PDF/A y PDF/UA en la misma pasada?

Sí. BuildPdfPreflightReport toma un conjunto de estándares, así que [ppsPdfA, ppsPdfUa] evalúa ambos en una corrida con un reporte consolidado. Las comprobaciones PDF/UA también combinan naturalmente con trabajo de accesibilidad del lado del visor, como los patrones de construir un lector PDF accesible en Delphi.

¿Por qué mi reporte no muestra problemas para un archivo que veraPDF rechaza?

Primero confirmen que el estándar realmente se ejecutó: recorran Report.Results en busca de una entrada cuyo Standard coincida y revisen su bandera IsCompliant, en lugar de inferir desde una lista vacía de hallazgos. Si los estándares coinciden y los motores siguen discrepando, conserven el documento como caso de regresión nombrado; veredictos divergentes sobre archivos reales de clientes son exactamente lo que necesitan las pruebas de release.

¿Preflight corrige los problemas que encuentra?

No. Reporta violaciones de color, fuentes, estructura y metadatos con códigos, categorías y recomendaciones; la remediación ocurre en el flujo que produce los archivos. Presupuesten ese ciclo, no solo la comprobación.

Los perfiles, formatos de reporte y la API completa de preflight están documentados en la página del producto: PDFium Component. El mismo motor impulsa comprobaciones interactivas en una UI de revisión, así que este CLI y su mesa de revisión de PDFs entrantes pueden compartir un solo vocabulario de validación.