Technický článek

Akce životního cyklu PDF: Catalog /AA vs Page /AA v Delphi

PDFlibPas, nativní knihovna PDF pro Delphi a C++Builder, dává dokumentu PDF dvě samostatná místa, kam zavěsit automatické chování: akce životního cyklu na úrovni dokumentu jako WillClose, WillSave, DidSave, WillPrint a DidPrint, uložené ve slovníku /AA katalogu, a akce životního cyklu na úrovni stránky — Open a Close — uložené místo toho ve vlastním slovníku /AA každého objektu stránky. Zaměnit tyto dva kontejnery je jediný nejběžnější způsob, jak akce životního cyklu tiše nedělá nic

Motivující případy jsou obyčejné. Finanční tým chce šablonu výpisu, která otiskne časové razítko tisku a zaloguje, kdo jej vytiskl, v okamžiku, kdy tisk skutečně začne, ne když se soubor jen otevře. Pracovní postup hustý na formuláře potřebuje hodnoty polí automaticky odeslané na server dřív, než klient PDF čtenáře smí zavřít okno, takže zavřená karta nikdy neznamená ztracenou úpravu. Vícestránkový report chce banner specifický pro stránku, který se objeví jen, dokud je tato stránka na obrazovce. PDF ve skutečnosti nabízí třetí úroveň pod dokumentem a stránkou pro tento druh chování — akce připojené k vlastnímu záznamu /A jednotlivého pole formuláře nebo odkazu, předmět doprovodného článku o interaktivních akcích formuláře a JavaScriptu — ale tento článek zůstává u dvou úrovní nad ní: celý dokument, a jedna stránka

Jaké spouštěče žijí na /AA katalogu dokumentu?

Pět spouštěčů žije na slovníku /AA katalogu, a každý z nich se vyvolá pro událost, která ovlivňuje celý dokument, ne jednu stránku. ISO 32000-1 §12.6.3 (Trigger Events) uvádí klíče na úrovni dokumentu jako WC, WS, DS, WP a DP — doslovná dvoupísmenná jména zapsaná do slovníku /AA — pro WillClose, WillSave, DidSave, WillPrint a DidPrint po řadě, a PDFlibPas tuto sadu přesně zrcadlí ve výčtu TPDFlibDocumentActionTrigger: datWillClose, datWillSave, datDidSave, datWillPrint, datDidPrint. SetDocumentAction je jediný vstupní bod, který připojí kteroukoli z pěti, a parametr ActionKind, který bere, je jedna z deseti konstant PDF_ACTION_BUILDER_* sdílených napříč každým voláním stavitele akce v knihovně, od obyčejného URI po skript po skok na cíl. Co GoTo, vzdálený soubor, vložený soubor nebo akce Launch skutečně dělá jednou vyvolaná, je jiná otázka než kam se připojuje, a to je předmět doprovodného článku o akcích GoTo, vzdáleného souboru, vloženého souboru a launch — tento zůstává u otázky kontejneru, Catalog nebo Page, místo otázky druhu akce

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;

Jak se spouštěč na úrovni stránky liší od toho na úrovni dokumentu?

Spouštěč na úrovni stránky se vyvolá jen pro jeden objekt stránky, ke kterému je připojen, a PDFlibPas jej ukládá do vlastního slovníku /AA této stránky místo do slovníku katalogu. Existují jen dva spouštěče stránky, Open a Close, odpovídající klíčům O a C, které ISO 32000-1 definuje pro slovník dodatečných akcí stránky, a PDFlibPas je vystavuje jako patOpen a patClose přes SetPageAction, který se připojí ke kterékoli stránce právě vybrané přes SelectPage — detail, na kterém záleží poprvé, kdy proscrollujete napříč dokumentem v očekávání, že jedno volání platí všude, protože nikdy neplatí. Připojení kteréhokoli druhu spouštěče také zvedne minimální verzi PDF souboru, a oba kontejnery žádají o odlišné podlahy: PDFlibPas zvedne dokument na alespoň PDF 1.4 při prvním zápisu záznamu Catalog /AA, a na alespoň PDF 1.5 při prvním zápisu záznamu Page /AA, bez ohledu na to, jaký druh akce uvnitř sedí. To je požadavek na úrovni kontejneru vrstvený navrch toho, co samotná akce potřebuje sama o sobě, takže obyčejná akce URI, která by o samotě vyžadovala jen PDF 1.1, přesto vytáhne celý soubor na PDF 1.5, jakmile je obalena do spouštěče otevření stránky

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

Čtení a odebírání akcí životního cyklu

GetDocumentActionInfo a GetPageActionInfo obě vrací záznam TPDFlibActionInfo, a pole Kind se vrátí jako akNone kdykoli k tomuto spouštěči nic připojeno není, takže zkontrolujte Kind dřív, než budete věřit jakémukoli jinému poli na záznamu — URI, JavaScript, FileName a zbytek jsou smysluplné jen pro ten jeden druh akce, který Kind skutečně hlásí, protože stejný tvar záznamu se opakovaně používá napříč každým typem akce, který stavitel dokáže vyprodukovat. RemoveDocumentAction a RemovePageAction každá vyčistí jeden spouštěč a hlásí 1, když našly něco k odebrání, 0, když byl spouštěč už prázdný; když byl odebraný záznam poslední zbývající ve slovníku /AA, PDFlibPas smaže teď prázdné /AA samo, místo aby ponechalo visící, bezvýznamný kontejner za sebou na katalogu nebo stránce

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;

Dovoluje PDF/A akce životního cyklu vůbec?

Ne. Konformita PDF/A odmítá celý kontejner dodatečných akcí, ne jen druhy akcí, které znějí rizikově, protože ISO 19005 omezuje interaktivní model akcí PDF na základě předpokladu, že archivní soubor se musí vykreslovat stejně za desítky let, aniž by závisel na skriptovacím enginu nebo síťovém připojení, které za tu dobu možná nebude existovat. SetLifecycleAction, sdílený stavitel za SetDocumentAction i SetPageAction, zkontroluje PDFAMode ještě předtím, než se vůbec podívá na ActionKind, takže akce URI, která jen otevře firemní webovou stránku, nebo pojmenovaná akce, která jen znamená jdi na další stránku, se chytí do stejné sítě jako nebezpečná, nic, co by revizor bezpečnosti normálně označil, přesto zablokované, protože omezení je strukturální, ne případ od případu. Praktické nebezpečí je, že odmítnutí je tiché: SetDocumentAction i SetPageAction obě vrátí 0 bez vyvolání výjimky, takže místo volání, které nikdy nezkontroluje návratovou hodnotu, odešle dokument tiše postrádající spouštěč, který měl nést

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

Jedna asymetrie stojí za zapamatování. RemoveDocumentAction a RemovePageAction nikdy nekontrolují PDFAMode, takže načtení souboru, který už nese nekonformní akce životního cyklu, a jejich odstranění na cestě k uložení konformnímu s PDF/A, funguje přesně podle očekávání — jen zápisová cesta, připojení nového spouštěče, je bránovaná režimem shody

Kam zapadá tisk-při-otevření bez spouštěče WillOpen?

Slovník /AA katalogu vůbec nemá záznam WillOpen, záměrně — /AA na úrovni dokumentu v ISO 32000-1 definuje přesně pět klíčů, WillClose, WillSave, DidSave, WillPrint a DidPrint, a nic na tomto seznamu se nevyvolá čistě proto, že se soubor otevřel. Hák času otevření žije v samostatném záznamu katalogu, /OpenAction, který PDFlibPas vystavuje přes vlastní rodinu volání, SetOpenActionJavaScript, SetOpenActionDestination a SetOpenActionNamedDestination mezi nimi, žádné z nich se nedotýká slovníku /AA ani výčtu TPDFlibDocumentActionTrigger vůbec. Oba mechanismy se ale skládají, a to je obvykle to, co šablona tisku-při-otevření skutečně potřebuje: postavit šablonu tak, aby její /OpenAction spustila tiskovou úlohu, typicky akce JavaScript volající vlastní tiskový příkaz prohlížeče, a samotný tisk je to, co dává WillPrint a DidPrint něco, proti čemu spustit — časové razítko otisknuté dřív, než se stránky odešlou do fronty, auditní záznam zapsaný, jakmile jsou hotové

Jak spolehlivé jsou tyto spouštěče napříč prohlížeči PDF?

Ne každý prohlížeč je spouští, i mimo PDF/A, takže berte akci životního cyklu jako žádost, ne záruku. Acrobat a většina plných desktopových čteček provádí celou sadu věrně, ale velký podíl skutečné konzumace PDF se vůbec nedotkne slovníku dodatečných akcí: prohlížeče vložené do prohlížeče, většina mobilních čteček, a skoro každá pipeline pro server-side vykreslování nebo extrakci textu buď /AA úplně ignoruje, nebo respektuje jen úzký výsek z ní, s WillPrint a DidPrint typicky nejhůř na tom, protože bezhlavá konverze nemá žádnou tiskovou operaci, do které by se zaháčily. Pokud je akce odeslání formuláře WillClose jediná cesta, která zachytí data formuláře, není to spolehlivá cesta — spárujte ji s explicitním tlačítkem odeslat, a berte automatický spouštěč jako pohodlí pro čtečky, které jej náhodou podporují

Spouštěče dokumentu, stránky a pole jsou tři úrovně stejného podkladového mechanismu slovníku akcí, a jakmile je kontejner jasný, zbytek je výběr správné konstanty ActionKind a kontrola návratového kódu. Tyto spouštěče životního cyklu, spolu se širším API stavitele akcí, kterého se tento článek dotýká, se dodávají jako součást standardní knihovny PDF PDFlibPas pro Delphi, s úplnou referencí spouštěčů a druhů akcí v dokumentaci produktu