Articol tehnic

Acțiuni de ciclu de viață PDF: Catalog /AA versus Page /AA în Delphi

PDFlibPas, biblioteca PDF nativă pentru Delphi și C++Builder, oferă unui document PDF două locuri separate pentru a atârna comportament automat: acțiuni de ciclu de viață la nivel de document precum WillClose, WillSave, DidSave, WillPrint și DidPrint, stocate în dicționarul /AA al Catalogului, și acțiuni de ciclu de viață la nivel de pagină — Open și Close — stocate în schimb în propriul dicționar /AA al fiecărui obiect Page. Confundarea celor două containere este cel mai comun mod în care o acțiune de ciclu de viață nu face nimic silențios

Cazurile motivante sunt obișnuite. O echipă financiară dorește un șablon de extras de cont care ștampilează un marcaj de timp de tipărire și înregistrează cine l-a tipărit chiar în momentul în care tipărirea efectiv începe, nu atunci când fișierul doar se deschide. Un flux de lucru intensiv în formulare are nevoie ca valorile câmpurilor să fie trimise automat la un server înainte ca clientul PDF al cititorului să aibă voie să închidă fereastra, astfel încât o filă închisă să nu însemne niciodată o editare pierdută. Un raport pe mai multe pagini dorește un banner specific de pagină care apare doar cât timp acea pagină este pe ecran. PDF oferă efectiv un al treilea nivel sub document și pagină pentru acest tip de comportament — acțiuni atașate propriei intrări /A a unui câmp de formular sau link individual, subiectul unui articol complementar despre acțiunile de formular interactive și JavaScript — dar acest articol rămâne la cele două niveluri de deasupra lui: întregul document, și o singură pagină

Ce declanșatori trăiesc pe /AA al Catalogului documentului?

Cinci declanșatori trăiesc pe dicționarul /AA al Catalogului, iar fiecare din ei se declanșează pentru un eveniment care afectează întregul document, nu o singură pagină. ISO 32000-1 §12.6.3 (Trigger Events) listează cheile la nivel de document ca WC, WS, DS, WP și DP — numele literale de două litere scrise în dicționarul /AA — pentru WillClose, WillSave, DidSave, WillPrint și, respectiv, DidPrint, iar PDFlibPas oglindește acel set exact în enumerarea TPDFlibDocumentActionTrigger: datWillClose, datWillSave, datDidSave, datWillPrint, datDidPrint. SetDocumentAction este singurul punct de intrare care atașează oricare din cele cinci, iar parametrul ActionKind pe care îl ia este una din zece constante PDF_ACTION_BUILDER_* partajate pe fiecare apel de constructor de acțiune din bibliotecă, de la un simplu URI la un script la un salt de destinație. Ce face efectiv o acțiune GoTo, fișier-la-distanță, fișier-încorporat, sau Launch odată declanșată este o întrebare diferită de unde este atașată, iar acesta este subiectul unui articol complementar despre acțiunile GoTo, la distanță, încorporate și launch — acesta rămâne cu întrebarea de container, Catalog sau Page, în loc de întrebarea de tip-de-acțiune

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;

Cum diferă un declanșator la nivel de pagină de unul la nivel de document?

Un declanșator la nivel de pagină se declanșează doar pentru singurul obiect Page la care este atașat, iar PDFlibPas îl stochează în propriul dicționar /AA al acelei pagini, nu în cel al Catalogului. Există doar doi declanșatori de pagină, Open și Close, corespunzând cheilor O și C pe care ISO 32000-1 le definește pentru dicționarul de acțiuni-suplimentare al unei pagini, iar PDFlibPas îi expune ca patOpen și patClose prin SetPageAction, care se atașează la orice pagină este selectată curent prin SelectPage — un detaliu care contează prima dată când parcurgeți în buclă un document așteptând ca un apel să se aplice peste tot, pentru că nu se aplică niciodată. Atașarea oricărui tip de declanșator ridică de asemenea versiunea PDF minimă a fișierului, iar cele două containere cer planșee diferite: PDFlibPas ridică documentul la cel puțin PDF 1.4 prima dată când scrie o intrare /AA a Catalogului, și la cel puțin PDF 1.5 prima dată când scrie o intrare /AA a Page-ului, indiferent de ce tip de acțiune stă în interior. Aceasta este o cerință la nivel de container stratificată peste orice are nevoie acțiunea însăși singură, așa că o simplă acțiune URI care ar necesita doar PDF 1.1 de una singură tot trage întregul fișier până la PDF 1.5 odată ce este învelită într-un declanșator de deschidere-de-pagină

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);

Citirea și eliminarea acțiunilor de ciclu de viață

GetDocumentActionInfo și GetPageActionInfo ambele returnează o înregistrare TPDFlibActionInfo, iar câmpul Kind revine akNone ori de câte ori acel declanșator nu are nimic atașat, așa că verificați Kind înainte de a avea încredere în orice alt câmp al înregistrării — URI, JavaScript, FileName și restul au sens doar pentru singurul tip de acțiune pe care îl raportează efectiv Kind, întrucât aceeași formă de înregistrare este refolosită pentru fiecare tip de acțiune pe care îl poate produce constructorul. RemoveDocumentAction și RemovePageAction fiecare golește un singur declanșator și raportează 1 când a găsit ceva de eliminat, 0 când declanșatorul era deja gol; când intrarea eliminată era ultima rămasă în dicționarul /AA, PDFlibPas șterge acum-golul /AA însuși, în loc să lase în urmă un container atârnat, fără sens, pe Catalog sau pe pagină

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;

Permite deloc PDF/A acțiuni de ciclu de viață?

Nu. Conformitatea PDF/A respinge întregul container de acțiuni-suplimentare, nu doar tipurile de acțiune care sună riscante, pentru că ISO 19005 restrânge modelul de acțiune interactivă al PDF-ului pe presupunerea că un fișier de arhivă trebuie să se redea la fel decenii de acum înainte, fără a depinde de un motor de scripting sau o conexiune de rețea care ar putea să nu existe până atunci. SetLifecycleAction, constructorul partajat din spatele atât al SetDocumentAction, cât și al SetPageAction, verifică PDFAMode înainte de a se uita vreodată la ActionKind, așa că o acțiune URI care doar deschide o pagină web a companiei sau o acțiune Named care doar înseamnă mergi la pagina următoare este prinsă în aceeași plasă ca una periculoasă — nimic ce un revizor de securitate ar semnala în mod normal, blocat oricum, pentru că restricția este structurală, nu caz-cu-caz. Pericolul practic este că respingerea este silențioasă: atât SetDocumentAction, cât și SetPageAction returnează 0 fără a ridica o excepție, așa că un loc de apel care nu verifică niciodată valoarea de retur livrează un document care lipsește silențios declanșatorul pe care trebuia să îl poarte

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');

O asimetrie merită ținută minte. RemoveDocumentAction și RemovePageAction nu verifică niciodată PDFAMode, așa că încărcarea unui fișier care deja poartă acțiuni de ciclu de viață neconforme și eliminarea lor pe drumul spre o salvare conformă PDF/A funcționează exact cum era de așteptat — doar calea de scriere, atașarea unui declanșator nou, este controlată de modul de conformitate

Unde se potrivește tipărirea-la-deschidere fără un declanșator WillOpen?

Dicționarul /AA al Catalogului nu are deloc nicio intrare WillOpen, prin design — /AA la nivel de document în ISO 32000-1 definește exact cinci chei, WillClose, WillSave, DidSave, WillPrint și DidPrint, iar nimic din acea listă nu se declanșează pur pentru că un fișier a fost deschis. Cârligul de la momentul deschiderii trăiește într-o intrare separată de Catalog, /OpenAction, pe care PDFlibPas o expune prin propria sa familie de apeluri, SetOpenActionJavaScript, SetOpenActionDestination, și SetOpenActionNamedDestination printre ele, niciuna din acestea neatingând deloc dicționarul /AA sau enumerarea TPDFlibDocumentActionTrigger. Cele două mecanisme totuși se compun, iar asta este de obicei ce are nevoie efectiv un șablon de tipărire-la-deschidere: construiți șablonul astfel încât propriul său /OpenAction pornește job-ul de tipărire, de obicei o acțiune JavaScript care apelează propria comandă de tipărire a vizualizatorului, iar tipărirea însăși este ce oferă WillPrint și DidPrint ceva față de care să ruleze — un marcaj de timp ștampilat înainte ca paginile să fie puse la coadă, o intrare de audit scrisă odată ce s-au terminat

Cât de fiabili sunt acești declanșatori pe diferite vizualizatoare PDF?

Nu fiecare vizualizator îi rulează, chiar și în afara PDF/A, așa că tratați o acțiune de ciclu de viață ca o cerere, nu o garanție. Acrobat și majoritatea cititoarelor desktop complete execută întregul set fidel, dar o mare parte din consumul PDF real din lume nu atinge deloc un dicționar de acțiuni-suplimentare: vizualizatoarele încorporate în browser, majoritatea cititoarelor mobile, și aproape fiecare conductă de randare pe partea de server sau extracție de text fie ignoră complet /AA, fie respectă doar o felie restrânsă din el, cu WillPrint și DidPrint de obicei descurcându-se cel mai rău, întrucât conversia headless nu are nicio operație de tipărire în care să se conecteze. Dacă o acțiune de trimitere-formular WillClose este singura cale care captează date de formular, nu este o cale fiabilă — asociați-o cu un buton de trimitere explicit, și tratați declanșatorul automat ca o comoditate pentru cititoarele care se întâmplă să o susțină

Declanșatorii de document, pagină și câmp sunt trei niveluri ale aceluiași mecanism de bază de dicționar de acțiune, iar odată ce containerul este clar, restul este alegerea constantei ActionKind corecte și verificarea codului de retur. Acești declanșatori de ciclu de viață, împreună cu API-ul de constructor de acțiuni mai larg pe care îl atinge acest articol, sunt livrați ca parte a bibliotecii PDF Delphi PDFlibPas standard, cu referința completă de declanșator și tip-de-acțiune în documentația de produs