Форма, итоговые суммы в которой пересчитываются в Acrobat, но остаются неизменными в вашем собственном просмотрщике на Delphi — это почти никогда не является ошибкой рендеринга. Библиотека PDFium отключает выполнение любого встроенного в документ JavaScript, если хост-приложение не предоставит JS-платформу. Компонент PDFium Component подключает эту платформу автоматически начиная с версии v2.13.2, поэтому сценарии AcroForm, скрипты уровня документа и вычисления полей выполняются всегда при загрузке DLL с поддержкой движка V8. При этом хост-система полностью контролирует процесс через набор событий, которые позволяют отображать, перенаправлять или блокировать любые действия скрипта
Это утверждение отвечает на частые вопросы поддержки в духе: "моя форма заказа рассчитывает суммы строк в Acrobat, но не в моем приложении", "вызов app.alert никогда не срабатывает", "поле даты не форматируется автоматически". Все они вызваны одними и теми же правилами взаимодействия с библиотекой, и понимание этих механизмов важно, поскольку эти же правила определяют границы безопасности вашего приложения при работе с потенциально вредоносными документами
Почему вычисления в формах PDF не работают в PDFium?
PDFium рассматривает выполнение JavaScript в документах исключительно как опцию: если член m_pJsPlatform среды заполнения форм равен NULL, любой скрипт в документе просто пропускается. Ни ошибок, ни записей в логе, ни частичного выполнения. Вычисляемые поля сохраняют свое последнее записанное значение, вызовы app.alert игнорируются, а скрипты форматирования не запускаются. Редактор Acrobat, который всегда содержит собственный движок JavaScript, выполняет этот файл без проблем, из-за чего разница в поведении выглядит как ошибка в вашем коде, хотя на самом деле это заложенное по умолчанию поведение внутренней библиотеки
В PDFium Component была и историческая особенность. Вплоть до версии v2.13.1 связывание подключало m_pJsPlatform только внутри ветки инициализации XFA, поэтому обычные документы AcroForm (составляющие подавляющее большинство PDF со скриптами) вообще не получали платформу JavaScript, даже если DLL с поддержкой V8 была доступна. Версия v2.13.2 вынесла это подключение за рамки логики XFA: метод InitializeFormFill теперь создает таблицу IPDF_JsPlatform во всех случаях, когда V8FeaturesAvailable возвращает True, независимо от того, является ли документ XFA. Практический результат заключается в том, что скрипты уровня документа, цепочки вычислений полей и app.alert теперь работают и в обычных документах AcroForm
Какая библиотека V8 DLL требуется для AcroForm JavaScript в Delphi?
Для запуска сценариев AcroForm требуется бинарный файл PDFium, скомпилированный с движком V8. Компонент PDFium Component загружает его под именем pdfium.v8.dll, если глобальный флаг EnableV8Engine установлен в значение True до первой загрузки библиотеки. Здесь есть одна ловушка: компонент автоматически распознает документы XFA путем предварительного сканирования маркеров /XFA и сам включает EnableV8Engine, но обычный документ AcroForm с JavaScript не содержит маркеров XFA, поэтому автоопределение не сработает. Если ваш просмотрщик должен выполнять скрипты в формах, установите этот флаг вручную один раз перед загрузкой первого документа; после загрузки обычной библиотеки pdfium.dll переключить движки в процессе будет уже невозможно
uses PDFium;
procedure TViewerForm.FormCreate(Sender: TObject);
begin
// Must run before the singleton PDFium library first loads;
// once plain pdfium.dll is in the process it cannot be swapped
EnableV8Engine := True;
end;
procedure TViewerForm.OpenDocument(const FileName: string);
begin
FPdf.LoadDocument(FileName);
if not V8FeaturesAvailable then
StatusBar.SimpleText :=
'JavaScript disabled: pdfium.v8.dll not deployed';
end;
Свойство V8FeaturesAvailable является надежным средством проверки во время выполнения: оно сообщает, экспортирует ли загруженная библиотека функции, зависящие от V8, поэтому проверка работает независимо от того, какая именно DLL была развернута установщиком. Тот же движок и флаг также обеспечивают динамический рендеринг XFA; подробнее о взаимодействии обнаружения XFA и автоматического выбора V8 рассказано в статье об обнаружении форм XFA и извлечении пакетов XFA в PDFium
События хоста для алертов, печати, отправки почты и отправки форм
Любые действия скрипта, требующие отображения или передачи данных, перенаправляются в ваш код через события компонента TPdf, и каждое из них по умолчанию настроено как безопасное пустое действие (no-op), если обработчик не назначен. Событие OnJavaScriptAlert получает сообщение, заголовок, тип кнопки и иконку из app.alert, позволяя вам вернуть результат нажатия кнопки; OnJavaScriptResponse обрабатывает запросы ввода app.response, включая флаг пароля; события OnJavaScriptBeep, OnJavaScriptPrint, OnJavaScriptMail и OnJavaScriptSubmitForm отслеживают запросы звуковых сигналов, печати, отправки почты и отправки форм с их полным набором параметров. Просмотрщик, не использующий эти обработчики, по-прежнему корректно отображает и рассчитывает формы; документ просто не сможет открывать диалоги, отправлять данные или запускать печать
procedure TViewerForm.PdfJavaScriptAlert(Sender: TObject;
const Msg, Title: WString; ButtonType, Icon: Integer;
var Result_: Integer; var Handled: Boolean);
begin
// Route app.alert through the VCL so it is owned by our window
Result_ := MessageBox(Handle, PWideChar(Msg), PWideChar(Title),
MB_OK or MB_ICONINFORMATION);
Handled := True;
end;
procedure TViewerForm.PdfJavaScriptSubmitForm(Sender: TObject;
const Url: WString; const Data: TBytes);
begin
// Nothing is transmitted unless this handler chooses to send it
LogAudit(Format('Document requested form submit to %s (%d bytes)',
[Url, Length(Data)]));
end;
Такое разделение поведения по умолчанию крайне важно для моделирования угроз. События OnJavaScriptMail и OnJavaScriptSubmitForm работают по принципу уведомлений: PDFium Component сам по себе не выполняет никаких сетевых запросов или вызовов MAPI. Поэтому вредоносный документ, вызывающий this.submitForm or this.mailDoc на непереопределенном хосте, ничего не добьется. Хост-приложение должно явно разрешить это действие, назначив обработчик и самостоятельно реализовав передачу данных. Это означает, что решение о выводе данных за пределы машины всегда остается за вами и принимается в вашем коде с полным контролем над URL и передаваемыми данными
Как запретить вредоносному PDF открывать URL-адреса?
Действия с URI — это единственное место, где историческое поведение по умолчанию было активным, а не пассивным, именно поэтому для них была внедрена явная возможность блокировки. До версии v2.13.1 обратный вызов FFI_DoURIActionWithKeyboardModifier безусловно запускал выполнение любого URI, переданного полем XFA, с помощью системных вызовов, что позволяло вредоносному документу открывать произвольные URL по клику. Событие OnXfaUriAction закрывает эту брешь: компонент опрашивает событие перед запуском, и обработчик, устанавливающий Handled в значение True, полностью подавляет вызов ShellExecuteW по умолчанию. Отсутствие назначенного обработчика сохраняет старое поведение для совместимости, поэтому в ориентированном на безопасность просмотрщике его стоит переопределять всегда
procedure TViewerForm.PdfXfaUriAction(Sender: TObject;
const Uri: WString; Modifiers: Integer; var Handled: Boolean);
begin
// Veto anything that is not plain https; the default would
// otherwise ShellExecuteW the URI as-is
if not SameText(Copy(Uri, 1, 8), 'https://') then
begin
LogAudit('Blocked URI action: ' + Uri);
Handled := True;
end;
end;
Белый список схем (протоколов) — это минимум; более строгие просмотрщики запрашивают подтверждение у пользователя или открывают адреса во встроенном изолированном браузере. Тот же подход к блокировке реализован во всем наборе событий версии v2.13.1: события OnXfaEmail, OnXfaHttpRequest и OnXfaOpenFile передают текстовые параметры запрошенного действия, но сам компонент не выполняет никаких сетевых или файловых операций. О том, как вписать эти механизмы в общую стратегию работы с недоверенными документами (включая изоляцию рендеринга), читайте в статье об обеспечении безопасного просмотра PDF в Delphi
Запись в активное поле с помощью SetFocusedFormFieldText
Метод TPdf.SetFocusedFormFieldText, добавленный в версии v2.13.2, является дополнением к свойству чтения FocusedFormFieldText: он выделяет текущее содержимое активного поля с помощью FORM_SelectAllText и перезаписывает его через FORM_ReplaceSelection точно так же, как если бы пользователь ввел этот текст вручную. Поскольку текст вводится через интерактивный путь редактирования, любые связанные с полем сценарии нажатия клавиш, форматирования и вычислений срабатывают так же, как при ручном вводе. Это делает данный метод идеальным инструментом для программного заполнения форм в просмотрщике с включенным JavaScript. Обход полей и управление фокусом описаны в навигации по полям форм с помощью PDFium Component
if FPdf.FocusedFormFieldIndex >= 0 then
begin
if not FPdf.SetFocusedFormFieldText('42.50') then
ShowMessage('No focused field accepts text on this page');
// AcroForm: value commits to /V when focus leaves the field.
// XFA: the write stays in the in-memory edit buffer only and
// will not survive a save
end;
Асимметрия сохранения, упомянутая в комментарии — это реальное техническое ограничение, а не просто предостережение. Для текстовых полей и списков AcroForm отредактированное значение записывается в поле /V при потере фокуса и сохраняется при записи файла. Для текстовых полей XFA измененные данные остаются только во временном буфере редактирования в оперативной памяти, так как PDFium не предоставляет публичного API для обратной синхронизации значений элементов управления с пакетом данных XFA. Сохранение документа не запишет эти изменения. Если требуется надежное редактирование данных XFA, изменяйте непосредственно XML-код наборов данных (datasets), а не свойства элементов управления
Ограничения, о которых следует знать перед выпуском продукта
Остаются два важных ограничения. Во-первых, реализуемый в PDFium API JavaScript представляет собой рабочее подмножество объектной модели Acrobat, сосредоточенное вокруг базовых сценариев форм: доступ к полям, события вычислений и форматирования, вызовы app.alert и app.response, запросы печати, почты и отправки данных. Документы, использующие специфичные объекты, доступные только в Acrobat, будут работать с ограничениями (как правило, без вывода ошибок), поэтому тестируйте реальные формы, с которыми работают ваши пользователи. Во-вторых, включение V8 требует поставки библиотеки pdfium.v8.dll, которая существенно больше по размеру, чем обычная сборка. Просмотрщик, которому никогда не требуются скрипты или формы XFA, может использовать стандартную DLL меньшего размера, оставив m_pJsPlatform неподключенной, что само по себе является хорошим решением с точки зрения безопасности
Общая схема интеграции выглядит достаточно просто: установите EnableV8Engine при запуске, назначьте события алертов и ответов для придания диалогам нативного вида, переопределите OnXfaUriAction и записывайте в журнал события почты и отправки форм, оставляя остальные параметры по умолчанию, пока не возникнет иных требований. Полный справочник событий и API заполнения форм представлены на странице продукта PDFium Component