Articolo tecnico

Preflight PDF in batch in Delphi: CLI con PDFium Component

Uno strumento di preflight in batch è un programma da console senza finestra, puntato su una cartella di PDF, che valida ciascuno di essi rispetto agli standard di conformità che indicate e lascia dietro di sé una prova leggibile dalle macchine di ciò che ha trovato. Nessuno sta lì a guardarlo. Gira alle due di notte sotto cron o Utilità di pianificazione di Windows, oppure come gate in una pipeline di CI, e la persona successiva a interessarsi del suo risultato è uno scheduler che legge un codice di uscita oppure un auditor che apre un report settimane dopo. Questo cambia il significato di corretto. Il motore di preflight di PDFium Component, una libreria PDF con sorgenti per Delphi, C++Builder e Lazarus, rende quasi banali le chiamate di validazione in sé. Il lavoro che decide se lo strumento si ripaga sta attorno a quelle chiamate: quale profilo avete controllato, che cosa ha detto il codice di uscita allo scheduler, e se il report che avrebbe intercettato un errore esiste ancora quando qualcuno va a cercarlo

Il contratto: che cosa vede davvero uno scheduler

Un runner di CI o Utilità di pianificazione di Windows vede esattamente due cose del vostro strumento: il codice di uscita e i file che ha lasciato dietro di sé. Righe di log, colori sulla console, output di avanzamento: tutto quello serve a un essere umano che guarda in diretta, e alle due di notte non c'è nessuno. Quindi fissate il vocabolario dei codici di uscita prima di toccare l'API, e tenetelo noioso:

  • 0: ogni file era conforme a ogni profilo richiesto
  • 1: almeno un file ha prodotto riscontri di validazione
  • 2: lo strumento stesso ha fallito su almeno un file (input corrotto, lock, crash)

La distinzione fra i codici 1 e 2 è quella che i team saltano e poi rimpiangono. Un PDF corrotto che non si apre non è un fallimento di validazione. Mettetelo insieme al codice 1 e un camion di scansioni danneggiate comparirà nelle vostre dashboard come un crollo improvviso della conformità, mandando qualcuno a inseguire una regressione sugli standard che non è mai avvenuta, quando la storia vera è uno scanner rotto a monte

Altri due elementi appartengono al contratto. Il primo è un timeout per file. Un PDF patologico, migliaia di pagine con strutture di oggetti profondamente annidate, può trattenere una singola passata di validazione per minuti, e una finestra notturna non ha pazienza per questo. Terminate il lavoro su quel file alla scadenza, contatelo come fallimento dello strumento e tenete il batch in movimento. Il secondo è una directory di quarantena: spostate da parte ogni input scaduto o non apribile invece di lasciarlo dov'è. Nel giro di qualche mese quella directory accumula in silenzio i peggiori documenti che i vostri clienti reali inviano, e quel corpus vale per i test di rilascio più di qualsiasi campione sintetico che potreste scrivere a mano

Diagramma del contratto sui codici di uscita per una CLI Delphi di preflight in batch costruita su PDFium Component, che mappa esecuzioni pulite, riscontri di validazione e fallimenti dello strumento sui codici 0, 1 e 2 accanto ai report JSON e HTML lasciati su disco
Uno scheduler legge soltanto il codice di uscita e i file lasciati dietro, quindi riscontri di validazione e fallimenti dello strumento devono mappare su codici diversi

Scegliere gli standard, e perché il livello di conformità conta

L'enumerazione TPdfPreflightStandard copre le famiglie che si incontrano nella pratica: ppsPdfA per la conformità di archiviazione ISO 19005, ppsPdfUa per l'accessibilità ISO 14289, ppsPdfX per lo scambio di stampa, più ppsPdfE, ppsPdfR e ppsPdfVT per lavori di ingegneria, raster e dati variabili. Dentro una famiglia il motore legge il livello di conformità dichiarato dal documento e lo riporta per standard nel campo ConformanceName del risultato. Nominare la famiglia raramente basta, perché è nel livello che sta la differenza vera. PDF/A-2b promette riproducibilità visiva e nulla di più. PDF/A-3a aggiunge la richiesta di taggatura della struttura logica e consente file sorgente incorporati, un'asticella molto più difficile da superare per materiale scansionato che non ha alcun albero di tag. Sbagliate in una direzione o nell'altra e il batch vi mente. Se la vostra politica di conservazione vuole in realtà PDF/A-2b ma bocciate i file per tag di struttura mancanti, il report si riempie di riscontri che nessuno correggerà mai. Accettate qualsiasi etichetta PDF/A senza controllare il livello e approvate documenti che soddisfano un requisito più debole di quello promesso. I mandati di accessibilità degli acquirenti pubblici impilano sempre più spesso PDF/UA sopra tutto questo, il che non aggiunge costo all'esecuzione perché BuildPdfPreflightReport (dalla unit FPdfPreflightReport) accetta un insieme di standard:

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

Una sola chiamata valuta entrambi gli standard e restituisce un unico record di report consolidato

Perché un elenco di riscontri vuoto non è una promozione

Il report enumera i riscontri per standard, e un elenco di problemi vuoto significa soltanto nessun problema trovato negli standard che sono stati effettivamente eseguiti. È un'affermazione più stretta di il file è conforme allo standard che vi interessa, e lo spazio fra le due è il punto in cui il preflight in batch marcisce in silenzio. Un errore di battitura nella configurazione che toglie ppsPdfA dall'insieme produce esattamente lo stesso elenco vuoto di un file davvero pulito. Quindi considerate sospetto il silenzio. Percorrete Report.Results e verificate due cose per ogni standard che intendevate controllare: che una voce di risultato per esso esista, e che il suo flag IsCompliant, sostenuto da Status = pfsPass, sia vero. Un lavoro notturno che equipara nessun riscontro a pronto per l'archivio senza mai confermare quali standard siano stati valutati è il modo classico in cui una cartella di file non conformi passa liscia per mesi, finché un auditor esterno non ne apre uno con veraPDF e l'intero archivio finisce sotto accusa

Una seconda trappola si nasconde in che cosa sia davvero un riscontro. Ogni TPdfPreflightIssue porta con sé un Code, una Category, una Description e una Recommendation, e nomina la regola violata, non una pagina o un oggetto. È una scelta di progetto con conseguenze sul ciclo di feedback. Il report dice al team che produce i file quale classe di difetto esiste, un font non incorporato o un identificatore XMP mancante, e trovare lo specifico oggetto colpevole è compito dello strumento di rimedio a valle, non del validatore. Costruite i consumatori del vostro report sui valori stabili di Code, mai sul testo leggibile della descrizione, che può essere riformulato fra un rilascio e l'altro senza preavviso

Diagramma decisionale che mostra perché un elenco vuoto di riscontri di preflight PDFium Component non è una promozione in Delphi: ogni standard richiesto ha bisogno di una voce di risultato e di un flag IsCompliant vero prima che l esecuzione sia una promozione verificata
Percorrere Report.Results trasforma il silenzio in un verdetto controllato: una voce di standard mancante è un errore di configurazione, non un file pulito

File di report per le macchine e per chi è di turno

Il record di report scrive gli stessi riscontri in cinque formati: SaveJsonToFile, SaveCsvToFile, SaveHtmlToFile, SaveTextToFile e SaveMarkdownToFile, ciascuno con una funzione corrispondente in stile ToJson quando volete la stringa in memoria anziché su disco. Resistete alla tentazione di sceglierne uno solo. Scrivete JSON per la pipeline, così la CI può allegarlo al record del job e interpretare i codici dei problemi e gli stati per standard senza raschiare testo. Scrivete HTML per la persona che riceve la chiamata, perché si apre in qualsiasi browser senza alcun strumento. I due insieme costano una riga in più per file e risparmiano al vostro reperibile il compito peggiore dell'elaborazione in batch, cioè decifrare un blob JSON grezzo alle due di notte per scoprire quale file si è rotto. Una disciplina conta più della scelta del formato: ricavate il nome di ogni report dal nome del file di input, mai da un timestamp, altrimenti due esecuzioni parallele intrecceranno report che non riuscirete più a ricondurre ai loro input

Diagramma di un record di preflight PDFium Component in Delphi salvato come JSON, CSV, HTML, testo e Markdown, con nomi ricavati dal file di input e una soglia di fallimento tenuta in configurazione
JSON serve la pipeline e HTML serve chi è di turno, mentre nomi di report ricavati dall input mantengono riconducibili le esecuzioni parallele

Le soglie di gravità appartengono alla configurazione, non al codice. Un'annotazione senza descrizione alternativa è un fallimento netto per un portale di invio PDF/UA e una nota trascurabile per un archivio interno, eppure è il riscontro identico in entrambi i casi. Esponete un livello di fallimento per profilo così che la politica possa cambiare senza ricompilare, e imprimete nel riepilogo del job il livello che era in vigore. Il trimestre prossimo nessuno ricorderà con quale soglia sia girato il batch dello scorso ottobre, e il riepilogo è l'unico posto in cui quella memoria sopravvive

Isolare i file perché un solo PDF difettoso 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);   // istanza nuova per file: nessuno stato residuo
      try
        try
          Pdf.FileName := InputDir + SR.Name;
          Pdf.Active := True;
          if not Pdf.Active then  // i fallimenti di caricamento sono silenziosi
            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);   // territorio del codice 2, non un verdetto di validazione
            WriteLn(ErrOutput, SR.Name + ': ' + E.Message);
          end;
        end;
      finally
        Pdf.Free;
      end;
    until FindNext(SR) <> 0;
  finally
    FindClose(SR);
  end;
end;

In quel ciclo vivono tre scelte deliberate. Un TPdf nuovo per ogni file garantisce che un documento che corrompe lo stato del motore non possa avvelenare i file che lo seguono. Il controllo esplicito su Active si guadagna il proprio posto perché Active := True inghiotte gli errori di caricamento invece di sollevarli; togliete quella guardia e un file troncato prosegue fino alla chiamata di validazione per poi fallire da qualche parte a valle con un messaggio fuorviante. Il try..except interno sta dentro lo scope del singolo file di proposito, così una sola eccezione incrementa il contatore dei fallimenti e il ciclo prosegue. Volete report puliti per i 4.999 file buoni anche quando il file numero 5.000 è a brandelli. Ed entrambi i formati di report vengono scritti su disco prima che il verdetto sia contato, il che significa che la prova sopravvive anche se un bug successivo nella logica di riepilogo conta male

La mappatura sui codici di uscita 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);
  // arrivare in fondo esce con 0: ogni file era conforme
end.

Che cosa il preflight non farà per voi

Il motore rileva; non ripara. Un riscontro su un font non incorporato o su uno spazio colore dipendente dal dispositivo è un ordine di lavoro per chi produce i file, e il validatore non ha modo di correggerlo sul posto. Quindi pianificate il ciclo di feedback in modo deliberato. I report devono arrivare dove il team di produzione li legge davvero, altrimenti gli stessi riscontri ricompariranno ogni notte finché qualcuno non chiederà finalmente perché il tasso di conformità non migliora mai. Conviene anche verificare un campione di verdetti con un validatore indipendente, veraPDF per PDF/A o il preflight di Acrobat per PDF/X, prima che lo faccia un auditor esterno al posto vostro. Quando due motori non concordano su un file reale di un cliente, quel documento non è una scocciatura; è esattamente il caso di regressione che mancava ai vostri test di rilascio. Conservatelo, dategli un nome ed eseguitelo a ogni build

Vale la pena conoscere un ultimo abbinamento. Lo stesso motore di validazione guida i controlli interattivi in una interfaccia di revisione, quindi questa CLI headless e un workbench di revisione dei PDF in ingresso rivolto agli analisti possono condividere un unico vocabolario di validazione invece di divergere nel tempo. E poiché [ppsPdfA, ppsPdfUa] valuta l'accessibilità nella stessa passata, il lato PDF/UA del batch si allinea in modo pulito con il lavoro sul viewer come costruire un reader PDF accessibile in Delphi. Profili, formati di report e l'API di preflight completa sono documentati nella pagina di prodotto di PDFium Component