Техническа статия

Жизнен цикъл на PDF действията: /AA на каталога срещу /AA на страницата в Delphi

PDFlibPas, native PDF библиотеката за Delphi и C++Builder, предоставя на PDF документа две отделни места за автоматично поведение: действия на ниво документ като WillClose, WillSave, DidSave, WillPrint и DidPrint, съхранявани в речника /AA на каталога, и действия на ниво страница — Open и Close — съхранявани в собствения речник /AA на всеки обект Page. Объркването на тези два контейнера е най-честата причина действие от жизнения цикъл да не прави нищо без видима грешка

Причините да използвате тези механизми са съвсем практични. Финансов екип може да иска шаблон за извлечение, който поставя времеви печат при печат и записва кой го е отпечатал точно когато печатът започне, а не когато файлът само се отвори. Работен процес с много формуляри може автоматично да изпрати стойностите на полетата към сървър, преди PDF клиентът на четеца да затвори прозореца, така че затворен раздел никога да не означава изгубена редакция. Многостраничен отчет може да показва банер, специфичен за страницата, само докато тя е на екрана. PDF всъщност предлага и трето ниво под документа и страницата за такова поведение — действия, прикрепени към собствения запис /A на отделно поле на формуляр или връзка, разгледани в придружаващата статия за интерактивни действия във формуляри и JavaScript — но тази статия остава при двете нива по-горе: целия документ и една страница

Кои тригери се намират в /AA на каталога на документа?

В речника /AA на каталога има пет тригера и всеки от тях се задейства от събитие, което засяга целия документ, а не отделна страница. ISO 32000-1 §12.6.3 (Trigger Events) изброява ключовете на ниво документ като WC, WS, DS, WP и DP — буквалните двубуквени имена, записвани в речника /AA — съответно за WillClose, WillSave, DidSave, WillPrint и DidPrint, а PDFlibPas отразява точно този набор в изброяването TPDFlibDocumentActionTrigger: datWillClose, datWillSave, datDidSave, datWillPrint, datDidPrint. SetDocumentAction е единствената входна точка, която прикачва всеки от петте тригера, а параметърът ActionKind, който приема, е един от десетте константи PDF_ACTION_BUILDER_*, споделени от всички извиквания на конструкторите на действия в библиотеката — от обикновен URI до скрипт или преход към местоназначение. Какво прави действие GoTo, действие към отдалечен файл, вграден файл или Launch след задействането му е отделен въпрос от това къде е прикачено и е разгледано в придружаващата статия за действията GoTo, към отдалечени и вградени файлове и Launch — тази статия остава при въпроса за контейнера, каталога или страницата, а не при въпроса за вида на действието

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;

Как тригерът на ниво страница се различава от този на ниво документ?

Тригерът на ниво страница се задейства само за конкретния обект Page, към който е прикачен, а PDFlibPas го съхранява в собствения речник /AA на страницата, а не в този на каталога. Има само два тригера на страница — Open и Close, съответстващи на ключовете O и C, които ISO 32000-1 определя за речника с допълнителни действия на страницата, а PDFlibPas ги предоставя като patOpen и patClose чрез SetPageAction, което прикачва действието към страницата, избрана в момента чрез SelectPage — подробност, която има значение при първия цикъл през документ, когато очаквате едно извикване да се приложи навсякъде, защото това никога не се случва. Прикачването на който и да е тип тригер повишава и минималната PDF версия на файла, като двата контейнера изискват различни минимални версии: PDFlibPas повишава документа поне до PDF 1.4 при първото записване на запис в /AA на каталога и поне до PDF 1.5 при първото записване на запис в /AA на страница, независимо от вида на действието. Това е изискване на ниво контейнер, добавено върху изискванията на самото действие, така че обикновено URI действие, което самостоятелно би изисквало само PDF 1.1, все пак повишава целия файл до PDF 1.5, щом бъде обвито в тригер за отваряне на страница

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

Как се четат и премахват действията от жизнения цикъл?

GetDocumentActionInfo и GetPageActionInfo връщат запис TPDFlibActionInfo, а полето Kind има стойност akNone, когато към съответния тригер няма прикачено действие, затова проверявайте Kind, преди да разчитате на което и да е друго поле в записа — URI, JavaScript, FileName и останалите имат смисъл само за вида действие, който Kind действително посочва, тъй като една и съща форма на запис се използва повторно за всеки тип действие, който конструкторът може да създаде. RemoveDocumentAction и RemovePageAction изчистват по един тригер и връщат 1, когато са намерили нещо за премахване, или 0, когато тригерът вече е бил празен. Когато премахнатият запис е последният в речника /AA, PDFlibPas изтрива и самия вече празен /AA, вместо да оставя безсмислен контейнер без съдържание в каталога или страницата

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;

Допуска ли PDF/A действия от жизнения цикъл?

Не. Съответствието с PDF/A отхвърля целия контейнер с допълнителни действия, а не само видовете действия, които изглеждат рискови, защото ISO 19005 ограничава интерактивния модел на действията в PDF с предположението, че архивният файл трябва да се визуализира по един и същи начин десетилетия наред, без да зависи от скриптов механизъм или мрежова връзка, които може вече да не съществуват. SetLifecycleAction, общият конструктор зад SetDocumentAction и SetPageAction, проверява PDFAMode, преди изобщо да разгледа ActionKind, така че URI действие, което просто отваря уеб страница на компанията, или Named действие, което означава само преминаване към следващата страница, попадат под същото ограничение като опасните действия — дори нещо, което обикновено не би предизвикало забележка от проверяващ по сигурността, пак се блокира, защото ограничението е структурно, а не според конкретния случай. Практическият риск е, че отхвърлянето е безшумно: SetDocumentAction и SetPageAction връщат 0, без да предизвикват изключение, така че код, който не проверява връщаната стойност, може тихо да създаде документ без тригера, който е трябвало да съдържа

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

Струва си да се има предвид една асиметрия. RemoveDocumentAction и RemovePageAction никога не проверяват PDFAMode, така че зареждането на файл, който вече съдържа несъответстващи на PDF/A действия от жизнения цикъл, и премахването им по пътя към запис, съответстващ на PDF/A, работи точно както се очаква — само пътят за записване, прикачването на нов тригер, се управлява от режима за съответствие

Къде се вписва печатът при отваряне без тригер WillOpen?

Речникът /AA на каталога няма запис WillOpen по замисъл — /AA на ниво документ в ISO 32000-1 определя точно пет ключа: WillClose, WillSave, DidSave, WillPrint и DidPrint, като нищо от този списък не се задейства само защото файлът е отворен. Куката за момента на отваряне се намира в отделен запис на каталога, /OpenAction, който PDFlibPas предоставя чрез собствен набор от извиквания, сред които са SetOpenActionJavaScript, SetOpenActionDestination и SetOpenActionNamedDestination, като нито едно от тях не променя речника /AA или изброяването TPDFlibDocumentActionTrigger. Двата механизма обаче могат да се комбинират и точно това обикновено е нужно на шаблон с печат при отваряне: създайте шаблона така, че неговият /OpenAction да стартира заданието за печат, обикновено чрез JavaScript действие, което извиква собствената команда за печат на визуализатора, а самият печат дава на WillPrint и DidPrint събитие, върху което да работят — времеви печат преди страниците да бъдат изпратени към принтера и одитен запис, добавен след приключването им

Колко надеждни са тези тригери в различните PDF четци?

Не всеки четец ги изпълнява, дори извън PDF/A, затова приемайте действието от жизнения цикъл като заявка, а не като гаранция. Acrobat и повечето пълнофункционални настолни четци изпълняват целия набор надеждно, но голяма част от реалното потребление на PDF изобщо не обработва речник с допълнителни действия: вградените в браузър четци, повечето мобилни четци и почти всички сървърни конвейери за визуализиране или извличане на текст или напълно игнорират /AA, или поддържат само ограничена част от него, като WillPrint и DidPrint обикновено са най-ненадеждни, защото безглавата конверсия няма операция за печат, към която да се прикачат. Ако действието за изпращане на формуляр при WillClose е единственият път за събиране на данните от формуляра, това не е надежден път — добавете изричен бутон за изпращане и приемайте автоматичния тригер като удобство за четците, които случайно го поддържат

Тригерите на ниво документ, страница и поле са три нива на една и съща основна система от речници на действията. Когато контейнерът е ясен, остава да изберете правилната константа ActionKind и да проверите връщания код. Тези тригери от жизнения цикъл, заедно с по-широкия API за конструиране на действия, засегнат в тази статия, са част от стандартната PDFlibPas Delphi PDF library, а пълният справочник за тригерите и видовете действия се намира в документацията на продукта