Artículo técnico

Informes de comprobación previa de PDF por lotes en Delphi con PDFium Component CLI

Una herramienta de comprobación previa (preflight) por lotes es un programa de consola sin ventana, dirigido a una carpeta de PDF, que valida cada uno conforme a los estándares de conformidad que usted indique y deja una prueba legible por máquina de lo que encontró. Nadie se sienta a observarla. Se ejecuta a las dos de la mañana bajo cron o el Programador de tareas de Windows, o como una compuerta en una tubería de CI, y la siguiente persona que se preocupará por su resultado será un programador leyendo un código de salida o un auditor abriendo un informe semanas después. Esto cambia lo que significa "correcto". El motor de comprobación previa de PDFium Component, una biblioteca PDF en código fuente para Delphi, C++Builder y Lazarus, hace que las llamadas de validación en sí sean casi triviales. El trabajo que determina si la herramienta justifica su existencia gira en torno a esas llamadas: qué perfil se comprobó, qué le comunicó el código de salida al programador, y si el informe que habría detectado un error sigue existiendo cuando alguien vaya a buscarlo

El contrato: lo que realmente puede ver un programador

Un ejecutor de CI o el Programador de tareas de Windows ve exactamente dos cosas provenientes de su herramienta: el código de salida y cualquier archivo que haya dejado. Las líneas de registro, los colores de la consola, la salida del progreso: todo eso está destinado a un humano que observe en directo, y a las dos de la mañana no hay nadie. Por lo tanto, fije el vocabulario del código de salida antes de tocar la API, y manténgalo aburrido:

  • 0: todo archivo cumplió con todos los perfiles solicitados
  • 1: al menos un archivo generó hallazgos de validación
  • 2: la herramienta en sí falló en al menos un archivo (entrada corrupta, bloqueo, caída)

La distinción entre los códigos 1 y 2 es aquella que los equipos suelen omitir y posteriormente lamentan. Un PDF corrupto que no se abre no constituye un fallo de validación. Si lo agrupa bajo el código 1, un aluvión de escaneos dañados aparecerá en sus paneles de control como un colapso repentino de conformidad, provocando que alguien persiga una regresión de estándares que nunca ocurrió, cuando la verdadera historia es un escáner averiado en fases previas

Hay dos elementos más que pertenecen al contrato. El primero es un tiempo de espera (timeout) por archivo. Un PDF patológico, miles de páginas con estructuras de objetos profundamente anidadas, puede mantener un único paso de validación durante minutos, y una ventana nocturna no tiene paciencia para ello. Cancele el trabajo de ese archivo llegado el límite de tiempo, cuéntelo como un fallo de la herramienta y mantenga el lote en marcha. El segundo es un directorio de cuarentena: traslade a un lado cada entrada que exceda el tiempo de espera o que resulte imposible abrir, en lugar de dejarla donde estaba. En el transcurso de unos meses, dicho directorio acumulará silenciosamente los peores documentos que envían sus clientes reales, y ese corpus reviste más valor para las pruebas de lanzamiento (release testing) que cualquier muestra sintética que pudiese redactar a mano

Selección de estándares, y por qué importa el nivel de conformidad

La enumeración TPdfPreflightStandard abarca las familias que surgen en la práctica: ppsPdfA para el cumplimiento de archivos ISO 19005, ppsPdfUa para accesibilidad ISO 14289, ppsPdfX para intercambio de impresión, además de ppsPdfE, ppsPdfR y ppsPdfVT para ingeniería, tramas y trabajo con datos variables. Dentro de una familia, el motor lee el nivel de conformidad que afirma poseer el documento y lo reporta por estándar en la propiedad ConformanceName del resultado. Nombrar la familia rara vez resulta suficiente, porque es en el nivel donde radica la auténtica diferencia. PDF/A-2b garantiza la reproducibilidad visual y nada más. PDF/A-3a suma la exigencia de etiquetado de estructura lógica y admite archivos de origen incrustados, lo cual es un listón mucho más difícil de franquear para el material escaneado que carece en absoluto de árbol de etiquetas (tag tree). Si se equivoca en cualquiera de ambos sentidos, el lote le mentirá. Si su política de retención demanda en realidad PDF/A-2b pero suspende archivos por omisión de etiquetas de estructura, el informe se colmará de hallazgos que nadie solucionará jamás. Acepte cualquier etiqueta PDF/A sin corroborar el nivel y avalará documentos que satisfacen un límite menos exigente que el estipulado. Las normativas de accesibilidad provenientes de compradores gubernamentales apilan de manera creciente PDF/UA por encima de todo esto, lo que no supone costo añadido para la ejecución dado que BuildPdfPreflightReport (desde la unidad FPdfPreflightReport) asume un conjunto de estándares:

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

Una sola llamada evalúa ambos estándares y devuelve un único registro de informe consolidado

Por qué una lista vacía de hallazgos no implica aprobación

El informe detalla los hallazgos según el estándar, y una lista de incidencias vacía solo se traduce como "no se encontraron problemas en los estándares que realmente se ejecutaron". Esa afirmación es más acotada que "el archivo cumple con el estándar que le interesa", y la brecha que separa a ambas es donde la comprobación previa por lotes se corrompe inadvertidamente. Un error tipográfico de configuración que elimine ppsPdfA del conjunto arrojará exactamente la misma lista vacía de incidencias que un archivo genuinamente impecable. En vista de esto, trate el silencio como sospechoso. Recorra Report.Results y corrobore dos elementos para cada estándar que pretendía evaluar: que exista sin falta una entrada de resultado correspondiente a él, y que su indicador IsCompliant, respaldado por Status = pfsPass, sea verdadero. Un trabajo nocturno que equipara "sin hallazgos" a "listo para archivo" sin haber confirmado en ningún momento qué estándares se evaluaron, constituye la manera clásica en la que una carpeta llena de archivos no conformes se cuela durante meses, hasta que un auditor externo abre uno de ellos con veraPDF y pone en tela de juicio todo el archivo

Una segunda trampa se oculta en la propia definición de un hallazgo. Cada TPdfPreflightIssue contiene un Code, una Category, una Description y una Recommendation, y nombra la regla infringida, no una página o un objeto. Esa es una decisión de diseño con implicaciones para el bucle de retroalimentación. El informe le comunica al equipo de producción qué clase de defecto existe, una fuente no incrustada o un identificador XMP faltante, y localizar el objeto infractor específico recae sobre la herramienta de reparación de cara a la cadena, no sobre el validador. Diseñe a los consumidores de sus informes amparándose en los valores estables de Code, nunca en el texto de la descripción legible por humanos, dado que éste puede reescribirse entre versiones sin aviso previo

Archivos de informes para las máquinas y para la persona de guardia

El registro de informes elabora los mismos hallazgos en cinco formatos: SaveJsonToFile, SaveCsvToFile, SaveHtmlToFile, SaveTextToFile y SaveMarkdownToFile, todos provistos de una función estilo ToJson correspondiente si se busca tener la cadena de texto (string) en memoria y no en el disco. Resista el impulso de quedarse con una sola opción. Escriba JSON para la tubería (pipeline), a fin de que CI pueda asociarlo al registro del trabajo y analice los códigos de incidencia y el estado por estándar sin tener que raspar (scrape) el texto. Redacte HTML para la persona que sea contactada en caso de alarma, ya que éste se despliega en cualquier navegador y sin herramientas de ningún tipo. Ambos reportes juntos tienen un costo de una sola línea adicional por archivo, y le librarán a su ingeniero de guardia de la peor tarea imaginable del procesamiento por lotes: efectuar ingeniería inversa a un amasijo JSON a las dos de la mañana para saber qué archivo colapsó. Con respecto a elegir formatos, prevalece una norma: deduzca el nombre de cada informe a partir del nombre del archivo de entrada, y en ningún caso de una marca de tiempo; de obrar así, las distintas ejecuciones paralelas entremezclarían los informes haciendo imposible rastrearlos hasta sus orígenes

Los umbrales de gravedad conciernen a la configuración en vez del código. Una anotación sin descripción alternativa representa una falla grave para un portal de envío PDF/UA y un apunte prescindible para un archivo interno, por más que constituya un hallazgo idéntico en ambos. Habilite un nivel de fallo por perfil de tal manera que las políticas logren modificarse sin obligar a recompilar, e imprima el nivel que se hallaba en vigor en el propio resumen del trabajo. Llegado el siguiente trimestre nadie recordará qué nivel umbral empleó el lote de octubre anterior, y el resumen es el único sitio donde este historial persiste

Aislamiento de los archivos para que un mal PDF no arruine 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;

En ese bucle conviven tres decisiones premeditadas. Un nuevo TPdf por cada archivo avala que el documento que dañe el estado del motor no intoxique a los que le sigan. La comprobación explícita de Active tiene todo su mérito porque Active := True se traga los errores de carga en vez de elevarlos (raise errors); quite la guardia y un archivo mutilado desembocará en la llamada de validación fallando más adelante en la secuencia mediante un mensaje ambiguo. El bloque interior try..except se encuentra en el ámbito individual de los archivos a propósito; una solitaria excepción dispara el contador de errores, y prosigue la iteración. Usted exige reportes pulcros referentes a los 4999 archivos válidos aun si el documento número 5000 queda hecho trizas. Asimismo, los dos formatos de informes se imprimen en el disco de manera anticipada al veredicto totalizado; con ello, la prueba sobrevivirá aunque una pifia posterior en la lógica del sumario arroje sumas equivocadas

A partir de entonces, la correspondencia del código de salida se reduce a un puñado de renglones en el archivo del 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 el preflight no hará por usted

El motor detecta; no repara. Una incidencia relacionada con una fuente que no haya sido incrustada o un espacio de color supeditado a un dispositivo, constituyen en rigor órdenes de trabajo destinadas a quienes produzcan dichos archivos, al punto que el comprobador carece de herramientas de parcheo en su posición. Por ende, articule concienzudamente el mecanismo retroalimentador (feedback loop). Será forzoso depositar los reportes allí donde sus operarios productivos puedan acceder a ellos; en caso adverso, la infinidad de reportes diarios atestiguará incesantemente la misma batería de incidencias hasta que alguno, por fortuna, interpele las razones relativas al inamovible estado del índice de aceptación. Al mismo tiempo, siempre paga dividendos contraponer el dictamen sobre las muestras obtenidas mediante una herramienta validadora disociada e independiente (ya fuere veraPDF tratándose de formato PDF/A, o el programa Adobe Acrobat preflight refiriéndose a un PDF/X), ganando ventaja por si un observador ajeno tomase por costumbre escrudiñarlos al son de su cuenta y riesgo. Puestos dos sistemas validadores en disputa ante un expediente auténtico subido por un usuario, ni lo considere un incordio fortuito sino la contingencia sintomática precisa que había carecido el programa de evaluación: regístrela, aségurele título y vuélvala test de obligación a superar toda vez que lance nuevas compilaciones

Es recomendable conocer una pareja adicional. El mismo motor de validación gestiona las comprobaciones interactivas en una interfaz de revisión, de modo que esta CLI en segundo plano (headless) y el entorno de revisión de admisión de PDF orientado a analistas logren compartir un vocabulario de validación unitario en lugar de ir bifurcándose con los años. De igual forma, por el hecho que la indicación [ppsPdfA, ppsPdfUa] evalúe pautas de accesibilidad en la sola tirada, toda la franja PDF/UA de este conjunto empalma estupendamente a los propósitos visores del trabajo tal cual ilustra crear un lector de PDF accesible en Delphi. Los correspondientes perfiles, formatos documentales albergados y toda la extensión pertinente al protocolo API quedan enumerados y disponibles para la revisión del usuario dentro del recinto que ampara a PDFium Component