PDFlibPas, natywna biblioteka PDF dla Delphi i C++Buildera, daje dokumentowi PDF dwa osobne miejsca, w których można zawiesić automatyczne zachowanie: akcje cyklu życia na poziomie dokumentu, takie jak WillClose, WillSave, DidSave, WillPrint i DidPrint, przechowywane w słowniku /AA Catalog, oraz akcje cyklu życia na poziomie strony — Open i Close — przechowywane zamiast tego we własnym słowniku /AA każdego obiektu Page. Pomylenie tych dwóch kontenerów to najpowszechniejszy pojedynczy sposób, w jaki akcja cyklu życia po cichu nic nie robi
Motywujące przypadki są zwyczajne. Zespół finansowy chce szablonu wyciągu, który stempluje znacznik czasu druku i loguje, kto go wydrukował, w chwili, gdy drukowanie faktycznie się rozpoczyna, nie gdy plik po prostu się otwiera. Przepływ pracy obfitujący w formularze potrzebuje wartości pól wypychanych automatycznie na serwer, zanim klient PDF czytnika będzie mógł zamknąć okno, tak aby zamknięta karta nigdy nie oznaczała utraconej edycji. Wielostronicowy raport chce baneru specyficznego dla strony, który pojawia się tylko, gdy ta strona jest na ekranie. PDF faktycznie oferuje trzeci poziom poniżej dokumentu i strony dla tego rodzaju zachowania — akcje dołączone do własnego wpisu /A pojedynczego pola formularza lub linku, temat towarzyszącego artykułu o interaktywnych akcjach formularza i JavaScript — ale ten artykuł pozostaje na dwóch poziomach powyżej niego: całym dokumencie i pojedynczej stronie
Jakie wyzwalacze mieszkają w Catalog /AA dokumentu?
Pięć wyzwalaczy mieszka w słowniku Catalog /AA, i każdy z nich uruchamia się dla zdarzenia, które dotyczy całego dokumentu, nie pojedynczej strony. ISO 32000-1 §12.6.3 (Trigger Events) wymienia klucze na poziomie dokumentu jako WC, WS, DS, WP i DP — dosłowne dwuliterowe nazwy zapisane w słowniku /AA — odpowiednio dla WillClose, WillSave, DidSave, WillPrint i DidPrint, a PDFlibPas odzwierciedla dokładnie ten zestaw w wyliczeniu TPDFlibDocumentActionTrigger: datWillClose, datWillSave, datDidSave, datWillPrint, datDidPrint. SetDocumentAction to jedyny punkt wejścia dołączający którąkolwiek z pięciu, a parametr ActionKind, który przyjmuje, to jedna z dziesięciu stałych PDF_ACTION_BUILDER_* dzielonych między wszystkie wywołania budowniczego akcji w bibliotece, od zwykłego URI po skrypt po skok do miejsca docelowego. To, co faktycznie robi akcja GoTo, plik zdalny, plik osadzony czy Launch po wyzwoleniu, to inne pytanie niż to, gdzie zostaje dołączona, a to temat towarzyszącego artykułu o akcjach GoTo, zdalnych, osadzonych i uruchamiania — ten artykuł pozostaje przy pytaniu o kontener, Catalog czy Page, a nie przy pytaniu o rodzaj akcji
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;
Czym wyzwalacz na poziomie strony różni się od tego na poziomie dokumentu?
Wyzwalacz na poziomie strony uruchamia się tylko dla tego jednego obiektu Page, do którego jest dołączony, a PDFlibPas przechowuje go we własnym słowniku /AA tej strony, a nie w Catalog. Są tylko dwa wyzwalacze strony, Open i Close, odpowiadające kluczom O i C, które ISO 32000-1 definiuje dla słownika dodatkowych akcji strony, a PDFlibPas udostępnia je jako patOpen i patClose przez SetPageAction, która dołącza się do dowolnie aktualnie wybranej strony przez SelectPage — szczegół, który ma znaczenie za pierwszym razem, gdy przechodzisz w pętli przez dokument, oczekując, że jedno wywołanie zastosuje się wszędzie, ponieważ nigdy tak się nie dzieje. Dołączenie któregokolwiek rodzaju wyzwalacza też podnosi minimalną wersję PDF pliku, a te dwa kontenery żądają różnych podłóg: PDFlibPas podnosi dokument do przynajmniej PDF 1.4 za pierwszym razem, gdy zapisuje wpis Catalog /AA, i do przynajmniej PDF 1.5 za pierwszym razem, gdy zapisuje wpis Page /AA, niezależnie od tego, jaki rodzaj akcji siedzi w środku. To wymóg na poziomie kontenera nałożony na to, czego akcja sama w sobie potrzebuje, więc goła akcja URI, która samodzielnie wymagałaby tylko PDF 1.1, wciąż podciąga cały plik do PDF 1.5, gdy tylko zostanie owinięta w wyzwalacz otwarcia strony
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);
Odczyt i usuwanie akcji cyklu życia
GetDocumentActionInfo i GetPageActionInfo oba zwracają rekord TPDFlibActionInfo, a pole Kind wraca jako akNone, gdy tylko ten wyzwalacz nie ma niczego dołączonego, więc sprawdź Kind, zanim zaufasz jakiemukolwiek innemu polu rekordu — URI, JavaScript, FileName i reszta mają znaczenie tylko dla tego jednego rodzaju akcji, który faktycznie zgłasza Kind, ponieważ ten sam kształt rekordu jest ponownie wykorzystywany dla każdego rodzaju akcji, jaki może wyprodukować builder. RemoveDocumentAction i RemovePageAction każde czyszczą pojedynczy wyzwalacz i zgłaszają 1, gdy znalazły coś do usunięcia, 0, gdy wyzwalacz był już pusty; gdy usunięty wpis był ostatnim pozostałym w słowniku /AA, PDFlibPas usuwa teraz-pusty /AA sam z siebie, zamiast zostawić wiszący, bezsensowny kontener w Catalog czy na stronie
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;
Czy PDF/A w ogóle pozwala na akcje cyklu życia?
Nie. Zgodność PDF/A odrzuca cały kontener dodatkowych akcji, nie tylko rodzaje akcji, które brzmią ryzykownie, ponieważ ISO 19005 ogranicza model interaktywnych akcji PDF przy założeniu, że plik archiwalny musi renderować się tak samo dekady później, bez zależności od silnika skryptowego czy połączenia sieciowego, które mogą wtedy już nie istnieć. SetLifecycleAction, wspólny builder stojący za SetDocumentAction i SetPageAction, sprawdza PDFAMode, zanim w ogóle spojrzy na ActionKind, więc akcja URI, która po prostu otwiera stronę internetową firmy, albo akcja Named, która oznacza tylko przejście do następnej strony, zostaje złapana w tę samą sieć co niebezpieczna — nic, co recenzent bezpieczeństwa normalnie by oznaczył, i tak zablokowane, ponieważ ograniczenie jest strukturalne, a nie przypadek po przypadku. Praktyczne niebezpieczeństwo polega na tym, że odrzucenie jest ciche: SetDocumentAction i SetPageAction oba zwracają 0 bez zgłaszania wyjątku, więc miejsce wywołania, które nigdy nie sprawdza wartości zwracanej, wysyła dokument po cichu pozbawiony wyzwalacza, który miał nieść
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 asymetria warta zapamiętania. RemoveDocumentAction i RemovePageAction nigdy nie sprawdzają PDFAMode, więc wczytanie pliku, który już niesie niezgodne akcje cyklu życia, i wyrzucenie ich w drodze do zapisu zgodnego z PDF/A działa dokładnie zgodnie z oczekiwaniami — tylko ścieżka zapisu, dołączenie nowego wyzwalacza, jest bramkowana trybem zgodności
Gdzie mieści się drukowanie przy otwarciu bez wyzwalacza WillOpen?
Słownik Catalog /AA w ogóle nie ma wpisu WillOpen, celowo — /AA na poziomie dokumentu w ISO 32000-1 definiuje dokładnie pięć kluczy, WillClose, WillSave, DidSave, WillPrint i DidPrint, i nic z tej listy nie uruchamia się czysto dlatego, że plik został otwarty. Hak czasu otwarcia mieszka w osobnym wpisie Catalog, /OpenAction, który PDFlibPas udostępnia przez własną rodzinę wywołań, w tym SetOpenActionJavaScript, SetOpenActionDestination i SetOpenActionNamedDestination, z których żadne w ogóle nie dotyka słownika /AA ani wyliczenia TPDFlibDocumentActionTrigger. Te dwa mechanizmy jednak się komponują, i to jest zwykle to, czego faktycznie potrzebuje szablon drukowania-przy-otwarciu: zbuduj szablon tak, aby jego /OpenAction uruchamiał zadanie druku, zwykle akcja JavaScript wywołująca własną komendę druku przeglądarki, a sam druk to to, co daje WillPrint i DidPrint coś, wobec czego się uruchomić — znacznik czasu wstemplowany, zanim strony trafią do kolejki, wpis audytowy zapisany, gdy się skończą
Jak niezawodne są te wyzwalacze między różnymi przeglądarkami PDF?
Nie każda przeglądarka je uruchamia, nawet poza PDF/A, więc traktuj akcję cyklu życia jako żądanie, nie gwarancję. Acrobat i większość pełnoprawnych czytników desktopowych wykonuje cały zestaw wiernie, ale duża część rzeczywistego konsumowania PDF w ogóle nie dotyka słownika dodatkowych akcji: przeglądarki osadzone w przeglądarce internetowej, większość czytników mobilnych, i niemal każdy potok renderowania po stronie serwera czy wyodrębniania tekstu albo całkowicie ignoruje /AA, albo honoruje tylko wąski jego wycinek, przy czym WillPrint i DidPrint zwykle radzą sobie najgorzej, ponieważ konwersja bezgłowa nie ma dla nich żadnej operacji drukowania, w którą mogłyby się zaczepić. Jeśli akcja przesyłania formularza WillClose to jedyna ścieżka przechwytująca dane formularza, nie jest to niezawodna ścieżka — sparuj ją z jawnym przyciskiem przesyłania i traktuj automatyczny wyzwalacz jako wygodę dla przeglądarek, które akurat go obsługują
Wyzwalacze dokumentu, strony i pola to trzy poziomy tej samej bazowej maszynerii słownika akcji, a gdy tylko kontener jest jasny, reszta to wybór właściwej stałej ActionKind i sprawdzenie kodu zwrotnego. Te wyzwalacze cyklu życia, wraz z szerszym API budowniczego akcji, którego dotyka ten artykuł, są dostarczane jako część standardowej biblioteki PDF Delphi PDFlibPas, z pełnym odniesieniem do wyzwalaczy i rodzajów akcji w dokumentacji produktu