PDFlibPas, рідна бібліотека PDF для Delphi та C++Builder, дає документу PDF два окремі місця для підвішування автоматичної поведінки: дії життєвого циклу на рівні документа, такі як WillClose, WillSave, DidSave, WillPrint та DidPrint, збережені в словнику /AA каталогу, та дії життєвого циклу на рівні сторінки — Open та Close — збережені натомість у власному словнику /AA кожного об'єкта сторінки. Сплутати ці два контейнери — найпоширеніший спосіб, яким дія життєвого циклу мовчки нічого не робить
Мотивуючі випадки звичайні. Фінансовій команді потрібен шаблон виписки, що ставить штамп часу друку та логує, хто надрукував, тієї миті, коли друк справді починається, не коли файл просто відкривається. Робочому процесу, насиченому формами, потрібно, щоб значення полів автоматично проштовхувалися на сервер до того, як 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, віддаленого, вбудованого та запуску — ця залишається з питанням контейнера, Catalog чи Page, а не питанням виду дії
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;
Чим тригер рівня сторінки відрізняється від рівня документа?
Тригер рівня сторінки спрацьовує лише для однієї сторінки, до якої прикріплений, і PDFlibPas зберігає його у власному словнику /AA цієї сторінки, а не в каталозі. Існує лише два тригери сторінки, Open та Close, що відповідають ключам O та C, які ISO 32000-1 визначає для словника додаткових дій сторінки, і PDFlibPas відкриває їх як patOpen та patClose через SetPageAction, що прикріплюється до будь-якої сторінки, наразі обраної через SelectPage — деталь, що важить, коли вперше проходите циклом по документу, очікуючи, що один виклик застосується всюди, бо цього ніколи не станеться. Прикріплення будь-якого виду тригера теж піднімає мінімальну версію PDF файлу, і два контейнери просять різні підлоги: PDFlibPas піднімає документ щонайменше до PDF 1.4 при першому записі запису Catalog /AA, і щонайменше до PDF 1.5 при першому записі запису Page /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, що просто відкриває корпоративну веб-сторінку, чи іменована дія, що означає лише перейти на наступну сторінку, потрапляє в ту саму сітку, що й небезпечна, — нічого, що зазвичай позначив би рецензент безпеки, усе одно заблоковано, бо обмеження структурне, а не для кожного випадку окремо. Практична небезпека в тому, що відхилення тихе: 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, працює точно як очікується — лише шлях запису, прикріплення нового тригера, обмежений режимом відповідності
Куди вписується друк-при-відкритті без тригера 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 будівельника дій, якого торкається ця стаття, постачаються як частина стандартної бібліотеки PDF Delphi PDFlibPas, з повним довідником тригерів та видів дій у документації продукту