PDFlibPas, la libreria PDF nativa per Delphi e C++Builder, offre a un documento PDF due luoghi separati in cui agganciare comportamento automatico: azioni di ciclo di vita a livello documento come WillClose, WillSave, DidSave, WillPrint e DidPrint, memorizzate nel dizionario /AA del Catalog, e azioni di ciclo di vita a livello pagina — Open e Close — memorizzate invece nel proprio dizionario /AA di ogni oggetto Page. Confondere i due contenitori è il modo più comune in assoluto per far sì che un'azione di ciclo di vita non faccia silenziosamente nulla
I casi motivanti sono ordinari. Un team finanziario vuole un modello di estratto conto che timbri un timestamp di stampa e registri chi lo ha stampato nel momento in cui la stampa effettivamente inizia, non quando il file semplicemente si apre. Un flusso di lavoro ricco di moduli ha bisogno che i valori di campo vengano inviati automaticamente a un server prima che al client PDF del lettore sia permesso chiudere la finestra, cosicché una scheda chiusa non significhi mai una modifica persa. Un report multipagina vuole un banner specifico per pagina che appaia solo mentre quella pagina è a schermo. PDF offre in realtà un terzo livello sotto documento e pagina per questo tipo di comportamento — azioni collegate all'entry /A propria di un singolo campo modulo o link, l'argomento di un articolo di approfondimento sulle azioni di modulo interattive e JavaScript — ma questo articolo resta ai due livelli sopra: l'intero documento, e una singola pagina
Quali trigger vivono sul /AA del Catalog documento?
Cinque trigger vivono sul dizionario /AA del Catalog, e ognuno di essi si genera per un evento che riguarda l'intero documento, non una singola pagina. ISO 32000-1 §12.6.3 (Trigger Events) elenca le chiavi a livello documento come WC, WS, DS, WP e DP — i nomi letterali a due lettere scritti nel dizionario /AA — rispettivamente per WillClose, WillSave, DidSave, WillPrint e DidPrint, e PDFlibPas rispecchia esattamente quell'insieme nell'enumerazione TPDFlibDocumentActionTrigger: datWillClose, datWillSave, datDidSave, datWillPrint, datDidPrint. SetDocumentAction è l'unico punto di ingresso che collega uno qualsiasi dei cinque, e il parametro ActionKind che accetta è una delle dieci costanti PDF_ACTION_BUILDER_* condivise su ogni chiamata builder di azione nella libreria, da un semplice URI a uno script a un salto di destinazione. Cosa faccia realmente un'azione GoTo, file remoto, file incorporato o Launch una volta scatenata è una domanda diversa da dove venga collegata, ed è l'argomento di un articolo di approfondimento sulle azioni GoTo, remote, incorporate e launch — questo resta sulla domanda del contenitore, Catalog o Page, invece che sulla domanda del tipo di azione
var
Lib: TPDFlib;
begin
Lib := TPDFlib.Create;
try
Lib.AddStandardFont(4);
Lib.DrawText(40, 700, 'Quarterly statement');
Lib.SetDocumentAction(datWillSave, PDF_ACTION_BUILDER_WEB,
'https://example.com/audit/will-save', '', 0, 0);
Lib.SetDocumentAction(datWillClose, PDF_ACTION_BUILDER_SUBMIT,
'https://example.com/forms/submit', 'CustomerName;OrderTotal', 0, 0);
Lib.SaveToFile('statement.pdf');
finally
Lib.Free;
end;
end;
In cosa differisce un trigger a livello pagina da uno a livello documento?
Un trigger a livello pagina si genera solo per il singolo oggetto Page a cui è collegato, e PDFlibPas lo memorizza nel dizionario /AA proprio di quella pagina invece che in quello del Catalog. Ci sono solo due trigger di pagina, Open e Close, corrispondenti alle chiavi O e C che ISO 32000-1 definisce per il dizionario di azioni aggiuntive di una pagina, e PDFlibPas li espone come patOpen e patClose tramite SetPageAction, che si collega a qualunque pagina sia attualmente selezionata tramite SelectPage — un dettaglio che conta la prima volta che si percorre in ciclo un documento aspettandosi che una chiamata si applichi ovunque, perché non lo fa mai. Collegare uno dei due tipi di trigger alza anche la versione PDF minima del file, e i due contenitori richiedono pavimenti diversi: PDFlibPas alza il documento ad almeno PDF 1.4 la prima volta che scrive una voce Catalog /AA, e ad almeno PDF 1.5 la prima volta che scrive una voce Page /AA, indipendentemente da quale tipo di azione risieda all'interno. Questo è un requisito a livello di contenitore stratificato sopra a qualunque cosa l'azione stessa richieda da sola, quindi una semplice azione URI che richiederebbe solo PDF 1.1 da sola tira comunque l'intero file fino a PDF 1.5 una volta avvolta in un trigger di apertura pagina
Lib.SelectPage(3);
Lib.SetPageAction(patOpen, PDF_ACTION_BUILDER_JAVASCRIPT,
'app.alert("Section 3: internal review only");', '', 0, 0);
Lib.SetPageAction(patClose, PDF_ACTION_BUILDER_WEB,
'https://example.com/analytics/page-3-closed', '', 0, 0);
Leggere e rimuovere le azioni di ciclo di vita
GetDocumentActionInfo e GetPageActionInfo restituiscono entrambi un record TPDFlibActionInfo, e il campo Kind torna akNone ogni volta che quel trigger non ha nulla collegato, quindi controlla Kind prima di fidarti di qualsiasi altro campo del record — URI, JavaScript, FileName e il resto sono significativi solo per l'unico tipo di azione che Kind effettivamente segnala, poiché la stessa forma di record viene riutilizzata su ogni tipo di azione che il builder può produrre. RemoveDocumentAction e RemovePageAction cancellano ciascuno un singolo trigger e segnalano 1 quando hanno trovato qualcosa da rimuovere, 0 quando il trigger era già vuoto; quando la voce rimossa era l'ultima rimasta nel dizionario /AA, PDFlibPas elimina il /AA ora vuoto stesso invece di lasciarlo penzolante e senza significato sul Catalog o sulla pagina
var
Info: TPDFlibActionInfo;
begin
Info := Lib.GetDocumentActionInfo(datWillSave);
if Info.Kind = akURI then
WriteLn('WillSave calls out to: ', string(Info.URI));
if Lib.RemoveDocumentAction(datWillSave) = 1 then
Lib.SetDocumentAction(datWillSave, PDF_ACTION_BUILDER_WEB,
'https://example.com/audit/will-save-v2', '', 0, 0);
end;
Il PDF/A permette affatto le azioni di ciclo di vita?
No. La conformità PDF/A rifiuta l'intero contenitore di azioni aggiuntive, non solo i tipi di azione che suonano rischiosi, perché ISO 19005 restringe il modello di azione interattiva del PDF sul presupposto che un file archivistico debba renderizzarsi allo stesso modo tra decenni, senza dipendere da un motore di scripting o una connessione di rete che potrebbe non esistere più allora. SetLifecycleAction, il builder condiviso dietro sia SetDocumentAction sia SetPageAction, controlla PDFAMode prima ancora di guardare ActionKind, quindi un'azione URI che si limita ad aprire una pagina web aziendale o un'azione Named che significa solo vai alla pagina successiva viene catturata nella stessa rete di una pericolosa — nulla che un revisore di sicurezza normalmente segnalerebbe, bloccato comunque, perché la restrizione è strutturale piuttosto che caso per caso. Il pericolo pratico è che il rifiuto è silenzioso: SetDocumentAction e SetPageAction restituiscono entrambi 0 senza sollevare un'eccezione, quindi un punto di chiamata che non controlla mai il valore di ritorno distribuisce un documento silenziosamente privo del trigger che avrebbe dovuto portare
Lib.SetPDFAMode(2); // PDF/A-1b
if Lib.SetDocumentAction(datWillClose, PDF_ACTION_BUILDER_NAMED,
'', '', 0, 0) = 0 then
// rejected: PDF/A-1b forbids Catalog /AA, even a plain Named action
WriteLn('lifecycle action not attached');
Un'asimmetria vale la pena tenerla a mente. RemoveDocumentAction e RemovePageAction non controllano mai PDFAMode, quindi caricare un file che già porta azioni di ciclo di vita non conformi e rimuoverle sulla via verso un salvataggio conforme a PDF/A funziona esattamente come atteso — solo il percorso di scrittura, il collegamento di un nuovo trigger, è vincolato dalla modalità di conformità
Dove si inserisce la stampa-all'apertura senza un trigger WillOpen?
Il dizionario Catalog /AA non ha affatto una voce WillOpen, per progetto — /AA a livello documento in ISO 32000-1 definisce esattamente cinque chiavi, WillClose, WillSave, DidSave, WillPrint e DidPrint, e nulla in quell'elenco si genera puramente perché un file è stato aperto. L'aggancio al momento dell'apertura vive in una voce Catalog separata, /OpenAction, che PDFlibPas espone tramite la propria famiglia di chiamate, SetOpenActionJavaScript, SetOpenActionDestination e SetOpenActionNamedDestination tra queste, nessuna delle quali tocca affatto il dizionario /AA o l'enumerazione TPDFlibDocumentActionTrigger. I due meccanismi si compongono, tuttavia, ed è di solito ciò di cui un modello stampa-all'apertura ha realmente bisogno: costruisci il modello cosicché il suo /OpenAction avvii il lavoro di stampa, tipicamente un'azione JavaScript che chiama il comando di stampa proprio del visualizzatore, e la stampa stessa è ciò che dà a WillPrint e DidPrint qualcosa contro cui girare — un timestamp timbrato prima che le pagine vadano in coda, una voce di audit scritta una volta che sono finite
Quanto sono affidabili questi trigger nei vari visualizzatori PDF?
Non ogni visualizzatore li esegue, persino al di fuori di PDF/A, quindi tratta un'azione di ciclo di vita come una richiesta piuttosto che una garanzia. Acrobat e la maggior parte dei lettori desktop completi eseguono l'intero insieme fedelmente, ma una grande quota del consumo PDF nel mondo reale non tocca mai affatto un dizionario di azioni aggiuntive: i visualizzatori incorporati nel browser, la maggior parte dei lettori mobili, e quasi ogni pipeline di rendering o estrazione testo lato server o ignorano del tutto /AA o ne rispettano solo una fetta ristretta, con WillPrint e DidPrint che tipicamente se la cavano peggio poiché la conversione headless non ha alcuna operazione di stampa a cui agganciarsi. Se un'azione submit-form WillClose è l'unico percorso che cattura i dati del modulo, non è un percorso affidabile — abbinalo a un pulsante di invio esplicito, e tratta il trigger automatico come una comodità per i lettori che capitano di supportarlo
I trigger documento, pagina e campo sono tre livelli dello stesso meccanismo di dizionario azioni sottostante, e una volta chiaro il contenitore, il resto è scegliere la costante ActionKind giusta e controllare il codice di ritorno. Questi trigger di ciclo di vita, insieme alla più ampia API builder di azioni che questo articolo tocca, fanno parte della libreria PDF Delphi PDFlibPas standard, con il riferimento completo di trigger e tipo di azione nella documentazione del prodotto