Articolo tecnico

Report preflight PDF batch in Delphi con la CLI PDFium Component

Un service bureau di scansione che ho seguito eseguiva un job notturno che marcava migliaia di record digitalizzati come "pronti per l'archivio". Dopo sei mesi, un auditor esterno campionò l'archivio con veraPDF e trovò violazioni PDF/A in file che il job aveva approvato. Il job non aveva mentito, esattamente: aveva controllato il profilo sbagliato, compresso ogni esito in un singolo bit pass/fail e scartato i file di report che avrebbero esposto subito la discrepanza. Tieni a mente quell'incidente quando colleghi il motore preflight di PDFium Component, libreria PDF con sorgente per Delphi, C++Builder e Lazarus, a uno strumento da riga di comando: le chiamate di validazione sono la parte facile, mentre il contratto intorno a esse, profili, exit code e conservazione dei report, è dove il preflight batch riesce o marcisce in silenzio.

Il contratto: cosa vede davvero uno scheduler

Un runner CI o Windows Task Scheduler vede esattamente due cose dal tuo tool: l'exit code e i file lasciati indietro. Tutto il resto, righe di log, colori console, output di progresso, serve agli umani che guardano in tempo reale. Quindi prima di toccare l'API, fissa il vocabolario degli exit code e mantienilo noioso:

  • 0 — ogni file è conforme a ogni profilo richiesto
  • 1 — almeno un file ha prodotto finding di validazione
  • 2 — il tool stesso ha fallito su almeno un file, per input corrotto, lock o crash

La distinzione tra codici 1 e 2 è quella che i team saltano e poi rimpiangono. Un PDF corrotto che non può essere aperto non è un fallimento di validazione, e trattarlo come tale significa che un carico di scansioni danneggiate appare nelle metriche come un improvviso crollo della conformità invece che come l'incidente operativo che è davvero.

Altri due elementi del contratto meritano ciascuno un flag: un timeout per file e una directory di quarantena. Un PDF patologico, migliaia di pagine o strutture oggetto profondamente annidate, può tenere un passaggio di validazione per minuti, e una finestra notturna non è interessata al perché. Termina il job del file alla scadenza, contalo come failure del tool, sposta l'input per l'ispezione e fai proseguire il batch. La directory di quarantena diventa anche un corpus auto-raccolto dei peggiori documenti che i clienti inviano davvero, più utile ai test di release di qualsiasi campione sintetico.

Scegliere gli standard: PDF/A-2b non è PDF/A-3a

L'enumerazione TPdfPreflightStandard seleziona le famiglie di standard che contano in pratica: ppsPdfA per la conformità archivistica ISO 19005, ppsPdfUa per l'accessibilità ISO 14289, ppsPdfX per lo scambio stampa, più ppsPdfE, ppsPdfR e ppsPdfVT per workflow engineering, raster e dati variabili. Dentro una famiglia, il motore rileva il livello di conformità dichiarato dal documento e lo riporta per standard nel ConformanceName del risultato, e il livello conta. PDF/A-2b afferma solo la riproducibilità visiva; PDF/A-3a richiede inoltre tagging di struttura logica e permette file sorgente incorporati, una barra più severa e molto più difficile per materiale scansionato. Se la tua retention policy dice PDF/A-2b, fallire file per tag struttura mancanti inonda il report di finding che nessuno intende correggere; accettare qualunque etichetta PDF/A senza controllare il livello promette troppo poco. I mandati governativi di accessibilità aggiungono sempre più spesso PDF/UA sopra, che non costa nulla includere perché BuildPdfPreflightReport, dalla unit FPdfPreflightReport, accetta un set:

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

Una chiamata, entrambi gli standard, un solo record di report consolidato.

Leggere il report: il silenzio non è conformità

Il report enumera i finding per standard. Una lista issue vuota significa quindi "nessun problema trovato negli standard eseguiti", che non è la stessa affermazione di "il file è conforme allo standard che ti interessa". Se un refuso di configurazione ha rimosso ppsPdfA dal set, la lista issue è ugualmente vuota. Percorri sempre Report.Results e afferma due cose per ogni standard voluto: che esista davvero una voce risultato per quello standard, e che il suo flag IsCompliant, basato su Status = pfsPass, sia vero. È esattamente il failure mode dietro la storia di audit sopra: il job equiparava "nessun finding" a "pronto per l'archivio" senza mai controllare quali standard fossero stati valutati.

La seconda trappola si nasconde in cosa sia un finding: ogni TPdfPreflightIssue porta un Code, una Category, una Description e una Recommendation: nomina la regola violata, non un numero pagina. Questo plasma il ciclo di feedback: il report dice al team produttore quale classe di difetto correggere, ad esempio un font non incorporato o un identificatore XMP mancante, e localizzare l'oggetto specifico è compito dello strumento di remediation, non del validatore. Scrivi i consumer di report sui valori stabili di Code invece di analizzare testo descrittivo che può essere riscritto tra release.

File di report per macchine e per chi è reperibile

Il record di report scrive gli stessi finding in cinque formati: SaveJsonToFile, SaveCsvToFile, SaveHtmlToFile, SaveTextToFile e SaveMarkdownToFile, con funzioni stile ToJson quando vuoi la stringa invece del file. Resisti alla scelta di uno solo. JSON è per la pipeline: allegalo al record del job e lascia che la CI analizzi codici issue e stati per standard. HTML è per l'operatore chiamato alle due di notte: si apre in qualunque browser senza tooling. Scrivere entrambi costa una riga in più per file e rimuove la peggior esperienza on-call del batch processing, cioè ricostruire un blob JSON alle due del mattino. Mantieni nomi deterministici, derivati dal nome del file input e mai da un timestamp, altrimenti esecuzioni parallele mescolano report che non riesci più a ricondurre agli input.

Le soglie di severità appartengono alla configurazione, non al codice. Lo stesso finding, ad esempio un'annotazione senza descrizione alternativa, è un hard failure per un portale di submission PDF/UA e una nota ignorabile per un archivio interno. Esponi un livello fail-on per profilo in modo che la policy possa cambiare senza ricompilare, e registra il livello in vigore nel summary del job, perché il prossimo trimestre nessuno ricorderà quale soglia usava il batch dello scorso ottobre.

Isolare i file perché un PDF cattivo non affondi il batch

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;

Tre scelte deliberate vivono in quel ciclo. Un TPdf fresco per file garantisce che un documento che corrompe lo stato del motore non avveleni i successivi. Il controllo esplicito di Active conta perché impostare Active := True inghiotte gli errori di load invece di sollevarli: senza guardia, un file troncato scivolerebbe nella chiamata di validazione prima di fallire, con un messaggio fuorviante. Il try..except interno è dentro lo scope per file, quindi un'eccezione incrementa il contatore failure e il ciclo continua: l'auditor vuole report per i 4.999 file buoni anche quando il file 5.000 è troncato. Ed entrambi i formati di report vengono scritti prima del conteggio del verdetto, quindi l'evidenza sopravvive anche se la logica di summary ha un bug.

La mappatura degli exit code si riduce poi a poche righe nel file di progetto:

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.

Cosa il preflight non farà per te

Il motore rileva; non ripara. Un finding su un font non incorporato o uno spazio colore dipendente dal dispositivo è un ordine di lavoro per chi produce i file, non qualcosa che il validatore possa patchare in-place. Pianifica di conseguenza il ciclo di feedback: i report devono arrivare dove il team produttore li legge, altrimenti gli stessi finding ricompariranno ogni notte finché qualcuno si chiederà perché la curva non cambia. Vale anche la pena verificare a campione i verdetti contro un validatore indipendente, veraPDF per PDF/A, preflight di Acrobat per PDF/X, prima che lo faccia un auditor esterno. Due motori d'accordo sono una prova forte; un solo motore è un'opinione.

Domande frequenti

Posso validare PDF/A e PDF/UA nello stesso passaggio?

Sì. BuildPdfPreflightReport prende un set di standard, quindi [ppsPdfA, ppsPdfUa] valuta entrambi in una sola esecuzione con un report consolidato. I controlli PDF/UA si abbinano naturalmente anche al lavoro di accessibilità lato viewer, come i pattern in costruire un reader PDF accessibile in Delphi.

Perché il report non mostra issue per un file che veraPDF rifiuta?

Prima conferma che lo standard sia stato davvero eseguito: percorri Report.Results per una voce il cui Standard combacia e controlla il flag IsCompliant, invece di dedurre da una lista finding vuota. Se gli standard corrispondono e i motori sono ancora in disaccordo, conserva il documento come regression case nominato: verdetti divergenti su file cliente reali sono esattamente ciò che serve al release testing.

Il preflight corregge i problemi che trova?

No. Riporta violazioni di colore, font, struttura e metadati con codici, categorie e raccomandazioni; la remediation avviene nel workflow di produzione. Metti a budget quel ciclo, non solo il controllo.

Profili, formati di report e API preflight completa sono documentati nella pagina prodotto: PDFium Component. Lo stesso motore alimenta controlli interattivi in una UI di review, quindi questa CLI e il tuo workbench di intake PDF possono condividere un unico vocabolario di validazione.