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

Изпълнение на AcroForm JavaScript в Delphi с PDFium Component

Формуляр, чиито общи суми се преизчисляват в Acrobat, но изглеждат замръзнали във вашия собствени Delphi четец, почти никога не се дължи на грешка в рендирането. Библиотеката PDFium деактивира целия JavaScript на документа, освен ако хостът не предостави JS платформа, а PDFium Component свързва тази платформа автоматично от версия 2.13.2 нататък. Така AcroForm JavaScript, скриптовете на ниво документ и изчисленията на полета се изпълняват винаги, когато е заредена V8-съвместима DLL библиотека. Хостът запазва контрола чрез набор от събития, които могат да визуализират, пренасочват или отхвърлят всяко действие, което скриптът се опитва да извърши

Това разяснение отговаря на въпроси за поддръжка, които сме виждали под различни форми: „моят формуляр за поръчки изчислява сумите в Acrobat, но не и в моето приложение“, „app.alert никога не се задейства“, „полето за дата не се форматира“. Всички те се свеждат до един и същ механизъм на интеграция, чието разбиране е важно, тъй като той също така определя границата на сигурност срещу потенциално опасни документи

Защо изчисленията във PDF формуляри не работят в PDFium?

PDFium третира JavaScript в документи като изрично изискващ активиране (opt-in): ако членът m_pJsPlatform на средата за попълване е NULL, всеки скрипт в документа тихомълком се пропуска. Без грешка, без запис в лог файла, без частично изпълнение. Изчисляваните полета запазват последната си записана стойност, функцията app.alert не прави нищо, а скриптовете за форматиране не се изпълняват. Acrobat, който винаги включва собствена JavaScript машина, стартира същия файл без проблеми, поради което разликата изглежда като грешка във вашия код, макар всъщност да е съзнателно поведение по подразбиране в библиотеката

При PDFium Component имаше и второ историческо ограничение. До версия 2.13.1 интеграцията свързваше m_pJsPlatform само в рамките на XFA инициализацията, така че стандартните AcroForm документи, които представляват по-голямата част от документи със скриптове, никога не получаваха платформа за JavaScript, дори при наличието на V8 DLL файл. Версия 2.13.2 промени това: методът InitializeFormFill вече изгражда таблицата IPDF_JsPlatform винаги, когато функцията V8FeaturesAvailable връща True, независимо дали документът е XFA. Практическият резултат е, че скриптовете на ниво документ, веригите за изчисления на полета и app.alert вече се изпълняват и при стандартните AcroForm документи

Коя V8 DLL библиотека е необходима за AcroForm JavaScript в Delphi?

За AcroForm JavaScript е необходим PDFium файл, компилиран с V8 машината, който PDFium Component зарежда как pdfium.v8.dll, когато глобалният флаг EnableV8Engine е настроен на True преди първото зареждане на библиотеката. Тук има един капан: компонентът автоматично разпознава XFA документи чрез сканиране за маркери /XFA и активира EnableV8Engine вместо вас, но стандартен AcroForm документ с JavaScript не съдържа такъв маркер, така че автоматично разпознаване не се случва. Ако вашият четец трябва да изпълнява скриптове във формуляри, задайте сами този флаг веднъж, преди първото зареждане на документ; след като стандартният 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, като всяко от тях по подразбиране не прави нищо, ако не е асоциирано. Събитието 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 адреси са единственото място, където исторически поведението по подразбиране беше активно, а не пасивно, поради което бе въведена опция за отхвърляне. Преди версия 2.13.1 функцията за обратна връзка FFI_DoURIActionWithKeyboardModifier стартираше безусловно всеки URI адрес, предоставен от XFA поле, което даваше възможност на опасен документ да отваря произволни уебсайтове при кликване. Събитието 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;

Списък с разрешени протоколи е минимумът; по-строг четец изисква потвърждение от потребителя или пренасочва всичко през вграден браузър в пясъчник (sandbox). Същата философия за сигурност обхваща и събитията от версия v2.13.1: OnXfaEmail, OnXfaHttpRequest и OnXfaOpenFile разкриват текстовите параметри на поисканото действие, докато самият компонент не извършва мрежови операции или операции с файлове. Начинът на интеграция на тези защити в общата стратегия за сигурност е разгледан в статията за изграждане на защитен предварителен преглед на PDF в Delphi

Записване в избраното поле чрез SetFocusedFormFieldText

Методът TPdf.SetFocusedFormFieldText, добавен във версия 2.13.2, е начинът за промяна на съдържанието на избраното поле: той избира целия текст чрез 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 няма публични методи за пренасяне на стойностите обратно в пакета с XFA данни; записването на документа няма да съхрани тези промени. Ако се изисква трайно редактиране на XFA данни, редактирайте директно XML кода на данните, а не самото визуално поле

Ограничения, които трябва да се знаят преди пускане в експлоатация

Остават две реални ограничения. Първо, имплементираният в PDFium приложен програмен интерфейс за JavaScript е работещо подмножество на обектния модел на Acrobat, фокусирано върху функциите за формуляри: достъп до полета, изчисления, форматиране, събития, app.alert и app.response, печат, поща и изпращане на данни. Документи, които използват специфични обекти на Acrobat, няма да работят напълно, затова тествайте реалните файлове на потребителите си. Второ, активирането на V8 означава разпространение на pdfium.v8.dll – много по-голяма библиотека от стандартната версия; четец, който няма нужда от скриптове или XFA, може съвсем спокойно да използва по-малкия DLL файл без поддръжка на JavaScript, което само по себе си е добра стратегия за сигурност

Препоръчителната структура е лесна: задайте EnableV8Engine при стартиране, дефинирайте събитията за предупреждение и отговор, така че диалозите да изглеждат нативно, дефинирайте OnXfaUriAction и записвайте в дневника събитията за поща и изпращане на данни, оставяйки всичко останало по подразбиране, освен ако нямате специфични изисквания. Пълният списък със събития и възможности на формулярите са документирани на продуктовата страница за PDFium Component