PDFlibPas, natívna PDF knižnica pre Delphi a C++Builder, dáva PDF dokumentu dve oddelené miesta, na ktoré možno zavesiť automatické správanie: akcie životného cyklu na úrovni dokumentu, ako WillClose, WillSave, DidSave, WillPrint a DidPrint, uložené v slovníku /AA objektu Catalog, a akcie životného cyklu na úrovni strany – Open a Close – uložené namiesto toho vo vlastnom slovníku /AA každého objektu Page. Zámena týchto dvoch kontajnerov je najčastejší jediný spôsob, akým akcia životného cyklu potichu nič nespraví
Motivujúce prípady sú celkom bežné. Finančný tím chce šablónu výpisu, ktorá odtlačí časovú pečiatku tlače a zaloguje, kto ju vytlačil, presne vo chvíli, keď tlač skutočne začne, nie keď sa súbor len otvorí. Pracovný postup bohatý na formuláre potrebuje hodnoty polí automaticky odoslané na server ešte predtým, než sa PDF klientu čítačky dovolí zatvoriť okno, aby zatvorená karta nikdy neznamenala stratenú úpravu. Viacstranový report chce banner špecifický pre danú stranu, ktorý sa zobrazí len počas jej zobrazenia na obrazovke. PDF v skutočnosti ponúka aj tretiu úroveň pod dokumentom a stranou pre tento druh správania – akcie pripojené k vlastnému záznamu /A jednotlivého poľa formulára alebo odkazu, čo je predmetom sprievodného článku o interaktívnych akciách formulárov a JavaScripte – tento článok však zostáva pri dvoch vyšších úrovniach: celý dokument a jedna strana
Aké spúšťače žijú na Catalog /AA dokumentu?
Na slovníku /AA objektu Catalog žije päť spúšťačov, a každý jeden z nich sa spúšťa pri udalosti, ktorá ovplyvňuje celý dokument, nie jednu stranu. ISO 32000-1 §12.6.3 (Trigger Events) uvádza kľúče na úrovni dokumentu ako WC, WS, DS, WP a DP – doslovné dvojpísmenové názvy zapísané do slovníka /AA – pre WillClose, WillSave, DidSave, WillPrint a DidPrint, a PDFlibPas túto sadu presne odzrkadľuje vo výčtovom type TPDFlibDocumentActionTrigger: datWillClose, datWillSave, datDidSave, datWillPrint, datDidPrint. SetDocumentAction je jediný vstupný bod, ktorý pripája ktorúkoľvek z týchto piatich, a parameter ActionKind, ktorý prijíma, je jedna z desiatich konštánt PDF_ACTION_BUILDER_* zdieľaných naprieč každým volaním generátora akcií v knižnici, od obyčajného URI cez skript až po skok na cieľ. Čo presne robí akcia GoTo, vzdialený súbor, vložený súbor alebo Launch po spustení, je iná otázka než to, kam sa pripája, a to je predmetom sprievodného článku o akciách GoTo, vzdialených, vložených a spúšťacích – tento zostáva pri otázke kontajnera, Catalog alebo Page, nie pri otázke druhu akcie
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;
Čím sa spúšťač na úrovni strany líši od spúšťača na úrovni dokumentu?
Spúšťač na úrovni strany sa spustí len pre jediný objekt Page, ku ktorému je pripojený, a PDFlibPas ho ukladá do vlastného slovníka /AA tejto strany, nie do slovníka Catalog. Existujú len dva spúšťače strany, Open a Close, zodpovedajúce kľúčom O a C, ktoré ISO 32000-1 definuje pre slovník dodatočných akcií strany, a PDFlibPas ich vystavuje ako patOpen a patClose cez SetPageAction, ktorá sa pripája k práve vybranej strane cez SelectPage – detail, na ktorom záleží pri prvom prechádzaní dokumentom v cykle s očakávaním, že jedno volanie sa uplatní všade, pretože to tak nikdy nefunguje. Pripojenie ktoréhokoľvek druhu spúšťača tiež zvyšuje minimálnu verziu PDF súboru, a oba kontajnery žiadajú odlišné dolné hranice: PDFlibPas zvýši dokument na aspoň PDF 1.4 pri prvom zápise záznamu Catalog /AA, a na aspoň PDF 1.5 pri prvom zápise záznamu Page /AA, bez ohľadu na to, aký druh akcie sa v ňom nachádza. Ide o požiadavku na úrovni kontajnera vrstvenú nad tým, čo si samotná akcia vyžaduje sama osebe, takže holá akcia URI, ktorá by osamotene vyžadovala len PDF 1.1, vytiahne celý súbor na PDF 1.5, akonáhle je zabalená do spúšťača otvorenia strany
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);
Čítanie a odstraňovanie akcií životného cyklu
GetDocumentActionInfo aj GetPageActionInfo vracajú záznam TPDFlibActionInfo, a pole Kind sa vráti ako akNone vždy, keď daný spúšťač nemá nič pripojené, takže pred spoľahnutím sa na akékoľvek iné pole záznamu skontrolujte Kind – URI, JavaScript, FileName a zvyšok majú zmysel len pre ten jeden druh akcie, ktorý Kind skutočne hlási, keďže rovnaký tvar záznamu sa opätovne používa naprieč každým druhom akcie, ktorý generátor dokáže vytvoriť. RemoveDocumentAction a RemovePageAction každý vyčistí jediný spúšťač a nahlási 1, keď našiel niečo na odstránenie, 0, keď bol spúšťač už prázdny; keď bola odstránená položka poslednou zostávajúcou v slovníku /AA, PDFlibPas odstráni už prázdny /AA samotný, namiesto toho, aby ponechal na Catalogu alebo strane visieť nezmyselný prázdny kontajner
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;
Povoľuje PDF/A akcie životného cyklu vôbec?
Nie. Zhoda s PDF/A odmieta celý kontajner dodatočných akcií, nielen tie druhy akcií, ktoré znejú rizikovo, pretože ISO 19005 obmedzuje interaktívny model akcií PDF na základe predpokladu, že archívny súbor sa musí rovnako vykresľovať aj o desaťročia neskôr, bez závislosti na skriptovacom engine alebo sieťovom pripojení, ktoré vtedy nemusí existovať. SetLifecycleAction, zdieľaný generátor stojaci za SetDocumentAction aj SetPageAction, skontroluje PDFAMode ešte predtým, než sa vôbec pozrie na ActionKind, takže akcia URI, ktorá len otvára firemnú webovú stránku, alebo pomenovaná akcia, ktorá len znamená prejsť na ďalšiu stranu, sa chytí do tej istej siete ako nebezpečná – nič, čo by bežne bezpečnostný recenzent označil, sa aj tak zablokuje, pretože obmedzenie je štrukturálne, nie posudzované prípad od prípadu. Praktickým nebezpečenstvom je, že toto odmietnutie je tiché: SetDocumentAction aj SetPageAction vracajú 0 bez vyvolania výnimky, takže miesto volania, ktoré nikdy neskontroluje návratovú hodnotu, vydá dokument, ktorému potichu chýba spúšťač, ktorý mal niesť
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');
Jednu asymetriu stojí za to mať na pamäti. RemoveDocumentAction a RemovePageAction nikdy nekontrolujú PDFAMode, takže načítanie súboru, ktorý už nesie nekonformné akcie životného cyklu, a ich odstránenie na ceste k uloženiu v súlade s PDF/A funguje presne podľa očakávania – bránou zhody je podmienená len zápisová cesta, pripájanie nového spúšťača
Kam zapadá tlač pri otvorení bez spúšťača WillOpen?
Slovník /AA objektu Catalog nemá vôbec žiadny záznam WillOpen, zámerne – /AA na úrovni dokumentu v ISO 32000-1 definuje presne päť kľúčov, WillClose, WillSave, DidSave, WillPrint a DidPrint, a nič z tohto zoznamu sa nespúšťa len preto, že sa súbor otvoril. Hák pre okamih otvorenia žije v samostatnom zázname Catalog, /OpenAction, ktorý PDFlibPas vystavuje cez vlastnú rodinu volaní, medzi nimi SetOpenActionJavaScript, SetOpenActionDestination a SetOpenActionNamedDestination, z ktorých žiadne sa vôbec nedotýkajú slovníka /AA ani výčtu TPDFlibDocumentActionTrigger. Oba mechanizmy sa však dajú skladať, a to je zvyčajne presne to, čo šablóna s tlačou pri otvorení skutočne potrebuje: zostaviť šablónu tak, aby jej /OpenAction spustila tlačovú úlohu, typicky akciu JavaScript volajúcu vlastný tlačový príkaz prehliadača, a samotná tlač je to, čo dáva WillPrint a DidPrint niečo, voči čomu bežať – časová pečiatka odtlačená ešte pred zaradením strán do fronty, auditný záznam zapísaný, keď sú hotové
Nakoľko sú tieto spúšťače spoľahlivé naprieč prehliadačmi PDF?
Nie každý prehliadač ich spúšťa, dokonca aj mimo PDF/A, takže s akciou životného cyklu zaobchádzajte skôr ako so žiadosťou než ako so zárukou. Acrobat a väčšina plnohodnotných desktopových čítačiek vykonáva celú sadu verne, no veľký podiel skutočnej konzumácie PDF sa nikdy nedotkne slovníka dodatočných akcií vôbec: prehliadačmi vložené prehliadače PDF, väčšina mobilných čítačiek a takmer každý pipeline na serverové vykresľovanie alebo extrakciu textu buď /AA úplne ignoruje, alebo rešpektuje len jeho úzku výseč, pričom WillPrint a DidPrint zvyčajne dopadajú najhoršie, keďže bezhlavá konverzia nemá žiadnu tlačovú operáciu, na ktorú by sa mohli napojiť. Ak je akcia odoslania formulára cez WillClose jediná cesta, ktorá zachytáva dáta formulára, nie je to spoľahlivá cesta – spárujte ju s explicitným tlačidlom odoslania a s automatickým spúšťačom zaobchádzajte ako s vhodnosťou pre čítačky, ktoré ho náhodou podporujú
Spúšťače dokumentu, strany a poľa sú tri úrovne toho istého podkladového mechanizmu slovníka akcií, a akonáhle je kontajner jasný, zvyšok je len výber správnej konštanty ActionKind a kontrola návratového kódu. Tieto spúšťače životného cyklu, spolu so širším API generátora akcií, ktorého sa tento článok dotýka, sú súčasťou štandardnej PDF knižnice PDFlibPas pre Delphi, s úplnou referenciou spúšťačov a druhov akcií v dokumentácii k produktu