Articolo tecnico

Automatizzare il preflight PDF in Delphi con HotPDF

Il file si apre senza problemi sulla vostra macchina. Acrobat lo mostra, l'anteprima di stampa sembra giusta, tutte le pagine ci sono. Poi va in tipografia, o dentro il sistema di archiviazione che ingerisce il vostro lotto mensile, e torna indietro respinto: immagini RGB in un lavoro CMYK, nessuna chiave /Trapped, un output intent che non corrisponde alla macchina da stampa. Nel documento non c'era nulla di sbagliato che qualcuno potesse vedere. Era sbagliato rispetto a un profilo, e il profilo è stato controllato in un posto dove voi non eravate. Preflight è il nome che la prestampa dà a quel controllo, e la domanda vera è dove esso vada collocato quando i PDF escono dal vostro codice Delphi anziché dalla scrivania di un grafico

HotPDF non vi mette a disposizione una funzione di preflight da chiamare. Il componente porta con sé una finestra di rapporto di preflight nella sua demo grafica, ma dietro non c'è alcuna API che un servizio o uno script di build possa invocare, e far finta del contrario vi manderebbe a caccia di un metodo che non esiste. Sembra una lacuna finché non notate che, per i file che generate voi stessi, chiamare un validatore sulla vostra uscita è comunque la forma sbagliata. Controllate già ogni proprietà che un validatore andrebbe a ispezionare. La divisione utile è rendere il generatore incapace di emettere un file cattivo, e poi dimostrarlo con uno strumento che non avete scritto voi

Diagramma di una catena di preflight in Delphi in cui le impostazioni di conformità di HotPDF impediscono PDF difettosi durante la generazione e veraPDF con Acrobat Preflight dimostrano esternamente il risultato
La prevenzione incorpora le regole PDF/A e PDF/X nella generazione stessa con HotPDF, mentre i validatori esterni forniscono il verdetto che il generatore non può darsi da solo

Perché la vostra uscita si controlla in modo diverso

Il preflight tradizionale presuppone il file di uno sconosciuto. Un grafico qualsiasi, una qualche altra applicazione, una catena ignota di modifiche lo hanno prodotto, e voi lo ispezionate perché non avete idea di che cosa contenga. Un documento prodotto dal vostro codice non è uno sconosciuto. Incorporamento dei font, spazio colore, output intent, il blocco di metadati: il vostro programma ha deciso tutto qualche millisecondo prima che il file arrivasse su disco. Ispezionarlo dopo per scoprire scelte che avete appena fatto è lavoro a vuoto. La mossa più economica è vincolare quelle scelte così che un file non conforme non esista proprio da intercettare

C'è anche una ragione di credibilità per tenere esterna la verifica. Una libreria che benedice la propria uscita si sta correggendo il compito da sola. Quando il sistema di archiviazione di un cliente o il RIP di una tipografia respinge il vostro file, «il nostro componente dice che va bene» non pesa nulla. Un verdetto di veraPDF o di Acrobat sì, perché dall'altra parte usano gli stessi strumenti

Rendete la conformità una impostazione, non una lista di controllo

Lo strato di prevenzione è pura configurazione. Impostate PDFACompliance o PDFXCompliance prima di BeginDoc e HotPDF tiene le regole corrispondenti per l'intera passata di generazione: incorpora i font, sorveglia l'uso di DeviceRGB e DeviceCMYK rispetto allo output intent che avete dichiarato e rifiuta le funzioni che il profilo vieta. Le contraddizioni affiorano in EndDoc, dove i cancelli di conformità sollevano una eccezione invece di consegnare in silenzio qualcosa che fallirà a valle. Una volta salvato il file, le stesse proprietà rileggono ciò che è stato davvero applicato, che è il dato di cui il log della vostra catena ha più bisogno:

// Dopo EndDoc: registrate i profili applicati insieme ai metadati di esecuzione
if Pdf.PDFACompliance <> '' then
  Log('Generated as PDF/A level ' + Pdf.PDFACompliance);
if Pdf.PDFXCompliance <> '' then
  Log('Generated as PDF/X profile ' + Pdf.PDFXCompliance);

Mettete quei flag sulla stessa riga di log dell'hash dei dati di ingresso e della versione di HotPDF. Il giorno in cui un validatore e il vostro generatore non concorderanno su un file, quella riga vi dirà quale modello lo ha prodotto e quale build della libreria era caricata, e la discussione che altrimenti si mangerebbe un pomeriggio diventa un grep. Gli output intent, i profili ICC e il tagging che stanno dietro questi flag sono descritti nella guida all'uscita PDF/A, PDF/X e PDF/UA con HotPDF

Un primo cancello economico per i file che non avete generato

Non tutte le catene sono puramente generative. I clienti caricano PDF, gli scanner li depositano in una cartella, i partner li allegano alle email. Spingere ognuno di essi attraverso un validatore strutturale completo spreca tempo di coda su file che non si aprono nemmeno. La Direct File API di HotPDF legge abbastanza della struttura di un file da rispondere alla domanda «è un PDF utilizzabile o no» senza caricare l'intero albero degli oggetti, il che ne fa un buon posto dove fallire in fretta:

function TriagePdf(Pdf: THotPDF; const FileName: string): Boolean;
var
  Handle, Pages: Integer;
begin
  Result := False;
  Handle := Pdf.DAOpenFileReadOnly(FileName, '');
  if Handle <= 0 then
    Exit;  // strutturalmente illeggibile: in quarantena, non validare
  try
    Pages := Pdf.DAGetPageCount(Handle);
    Result := Pages > 0;
  finally
    Pdf.DACloseFile(Handle);
  end;
end;

Due fatti su questa API decidono come la avvolgete. La scorciatoia a memoria piatta vale soltanto per ingressi non cifrati; passate una password a DAOpenFileReadOnly e ripiega in silenzio su una analisi completa, quindi un file che sapete cifrato dovrebbe passare per DecryptFile verso una copia di lavoro in chiaro prima dello smistamento. E DAGetPageCount non significa nulla su un handle che non si è aperto correttamente, perciò il controllo sull'handle resta rigido e un risultato non positivo è un rifiuto, non un nuovo tentativo. Altri schemi di questo tipo vivono nell'articolo sulla Direct File API per i flussi con PDF di grandi dimensioni

veraPDF, eseguito come parte della build

Per tutto ciò che dichiarate PDF/A o PDF/UA, veraPDF è il validatore da collegare. Gira senza interfaccia, accetta un lotto, emette XML o JSON e nomina ogni fallimento con la sua clausola ISO, così che una regola violata rispetto alla clausola 6.2.2 della ISO 19005-1 rimandi direttamente a una impostazione del generatore invece di lasciarvi a indovinare. Pilotarlo da Delphi è semplice controllo di processo:

function RunVeraPdf(const PdfFile, ReportFile: string): Cardinal;
var
  Cmd: string;
  SI: TStartupInfo;
  PI: TProcessInformation;
begin
  Cmd := Format('cmd /c verapdf.bat --format xml "%s" > "%s"',
    [PdfFile, ReportFile]);
  FillChar(SI, SizeOf(SI), 0);
  SI.cb := SizeOf(SI);
  if not CreateProcess(nil, PChar(Cmd), nil, nil, False,
      CREATE_NO_WINDOW, nil, nil, SI, PI) then
    RaiseLastOSError;
  try
    WaitForSingleObject(PI.hProcess, 120000);  // limita l'attesa per ogni file
    GetExitCodeProcess(PI.hProcess, Result);
  finally
    CloseHandle(PI.hThread);
    CloseHandle(PI.hProcess);
  end;
end;

Quel timeout si guadagna il posto. Un file malformato può spingere qualsiasi parser in un angolo da cui non esce più, e una attesa senza limite dentro un processo di coda si trascina dietro il resto della coda. Limitate l'attesa, date al timeout un proprio codice di fallimento e mettete il file da parte per una persona. Quando leggete il risultato, analizzate l'XML cercando gli identificativi di regola, non il testo leggibile. Gli ID delle regole sopravvivono agli aggiornamenti del validatore; il modo in cui i messaggi sono formulati no, e un codice stabile è qualcosa che un tecnico dell'assistenza può cercare nei vecchi ticket

Il modo in cui fate girare il lotto conta quanto il fatto che ogni file passi. Un processo per file, non uno per lotto, così un ingresso velenoso vi costa il timeout di quel file e nient'altro. Limitate il numero di processi del validatore al numero di core, perché costruire il rapporto XML è vincolato dalla CPU e sovraccaricarla non fa che sprecare cicli. E mettete un tetto di dimensione all'ingresso, perché un libro scansionato da due gigabyte si impadronirà della coda per quanto paziente sia il parser. Nulla di tutto ciò è preflight in senso stretto. È la differenza fra un cancello che sopravvive ai volumi di fine mese e uno che viene disattivato la prima notte in cui blocca la catena alle due del mattino

Diagramma di un cancello di lotto in Delphi che esegue un processo veraPDF per ciascun PDF con un timeout limitato, estrae dall XML gli ID delle regole invece dei messaggi e archivia ogni rapporto accanto al proprio file
Una guardia in ingresso limita il carico della coda mentre un processo veraPDF nuovo per ciascun file impedisce a un ingresso avvelenato di bloccare la build

È sul PDF/X che tutto questo si ferma. veraPDF non lo valida, quindi il controllo praticabile resta il Preflight di Acrobat con il profilo ISO 15930 che la vostra tipografia ha indicato. Acrobat vuole una persona, il che significa campionare anziché coprire tutto: il primo file uscito da un modello nuovo, più una piccola estrazione casuale da ciascun lotto, mentre il cancello automatico gestisce tutto ciò che si può gestire senza qualcuno davanti. Un controllo a campione che gira davvero vale più di una automazione completa che resta per sempre a metà

Un rapporto che vorrete ancora fra un anno

Un cancello di preflight ripaga due volte. Una quando ferma un file cattivo alla porta, e di nuovo molto più tardi quando qualcuno chiede perché un certo file sia passato. È quel secondo momento a dover dettare il formato, perché è quello in cui un rapporto scarno vi lascia in mezzo al guado. Per ogni file controllato conservate l'hash dell'ingresso, i flag di conformità del generatore e la versione della libreria dalla riga di log vista sopra, il nome e la versione del validatore, il profilo rispetto a cui è stato controllato, l'esito positivo o negativo e gli ID delle regole fallite con i numeri di pagina ovunque il validatore li fornisca. Conservate quel rapporto accanto al file che descrive. Mettetelo in un sistema separato e quel sistema verrà dismesso prima dell'archivio che documenta

Anche le eccezioni vanno messe per iscritto. Quando un cliente insiste per consegnare un file che al cancello non piace, la risposta non è allentare la regola per tutti. Registrate chi ha approvato quel file, su quali basi e fino a quale data, poi allegate quella deroga al suo rapporto. Una deroga con un nome e una scadenza è una decisione di cui qualcuno risponde. Un controllo commentato «temporaneamente» è un incidente in attesa della propria data

Un'altra abitudine si ripaga da sola: quando un file fallisce, copiatelo in una cartella di regressione dedicata prima che qualcuno lo tocchi. Quasi ogni problema di preflight che valga la pena analizzare risale a un ingresso specifico, e i gruppi che conservano quegli ingressi risolvono la ricomparsa in un'ora invece di aspettare che riaffiori in produzione. Le proprietà di conformità e la Direct File API mostrate qui fanno parte dello HotPDF Delphi Component per Delphi e C++Builder, la cui documentazione tratta per intero ogni chiamata