Um formulário cujos totais são recalculados no Acrobat, mas permanecem congelados em seu próprio visualizador Delphi, quase nunca é um bug de renderização. O PDFium desativa todo o JavaScript do documento, a menos que o host anexe uma plataforma JS, e o Componente PDFium conecta essa plataforma automaticamente a partir da versão 2.13.2 em diante, de modo que o JavaScript AcroForm, scripts em nível de documento e cálculos de campo sejam executados sempre que a DLL habilitada para V8 for carregada. O host permanece no controle por meio de um conjunto de eventos que podem exibir, redirecionar ou vetar tudo o que o script tenta fazer
Essa frase resolve uma dúvida de suporte que vimos em várias formas: "meu formulário de pedido calcula os totais de linha no Acrobat, mas não no meu aplicativo", "o app.alert nunca é disparado", "o campo de data não se formata". Todos eles remontam ao mesmo contrato, e entendê-lo vale dez minutos porque o mesmo contrato é também a sua barreira de segurança contra documentos hostis
Por que os cálculos de formulário PDF não funcionam no PDFium?
O PDFium trata o JavaScript do documento como estritamente opcional (opt-in): se o membro m_pJsPlatform do ambiente de preenchimento de formulário for NULL, cada script no documento é ignorado silenciosamente. Nenhum erro, nenhuma linha de registro, nenhuma execução parcial. Os campos calculados mantêm seu último valor armazenado, o app.alert desaparece, e os scripts de formato nunca são executados. O Acrobat, que sempre acompanha seu próprio mecanismo JavaScript, executa alegremente o mesmo arquivo, razão pela qual a discrepância se parece tanto com um bug em seu código, quando na verdade é um padrão deliberado da biblioteca subjacente
O Componente PDFium tinha uma segunda camada histórica para isso. Até a versão 2.13.1, a vinculação (binding) só anexava o m_pJsPlatform dentro da ramificação de inicialização do XFA, de modo que os documentos AcroForm simples, que são a grande maioria dos PDFs com scripts, nunca recebiam uma plataforma JavaScript, mesmo quando a DLL habilitada para V8 estava presente. A versão 2.13.2 retirou o anexo da decisão do XFA: o InitializeFormFill agora constrói a tabela IPDF_JsPlatform sempre que o V8FeaturesAvailable retorna True, independentemente de o documento ser XFA ou não. O resultado prático é que os scripts em nível de documento, as cadeias de cálculo de campo e o app.alert agora são executados também em documentos AcroForm comuns
De qual DLL V8 o JavaScript AcroForm precisa no Delphi?
O JavaScript AcroForm precisa de um binário PDFium compilado com o mecanismo V8, o qual o Componente PDFium carrega como pdfium.v8.dll quando a sinalização global EnableV8Engine é definida como True antes do primeiro carregamento da biblioteca. Há uma armadilha aqui que vale a pena expor claramente: o componente detecta automaticamente documentos XFA pré-escaneando marcadores /XFA e ativa o EnableV8Engine para você, mas un documento AcroForm simples com JavaScript não carrega nenhum marcador XFA, portanto a detecção automática não ocorre. Se o seu visualizador precisa executar scripts de formulário, defina a sinalização você mesmo, uma única vez, antes do carregamento do primeiro documento; após um pdfium.dll simples ser carregado no processo, não é possível alternar os mecanismos
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;
O V8FeaturesAvailable é a sonda de tempo de execução real: ele informa se o binário carregado realmente resolve as exportações dependentes de V8, de modo que a verificação funciona independentemente de qual DLL o instalador acabou implantando. O mesmo mecanismo e a mesma sinalização também alimentam a renderização dinâmica do XFA; o contexto sobre como a detecção do XFA e a seleção automática do V8 interagem é abordado em detectando formulários XFA e extraindo pacotes XFA com PDFium
Eventos de host para alertas, impressão, e-mail e envio de formulário
Cada ação visível que um script executa é enviada de volta ao seu código por meio de eventos TPdf, e cada um deles retorna por padrão a uma operação nula (no-op) segura quando não atribuído. O OnJavaScriptAlert recebe a mensagem, o título, o tipo de botão e o ícone do app.alert e permite retornar o resultado do botão pressionado; o OnJavaScriptResponse atende a avisos de entrada do app.response, incluindo o sinalizador de senha; e OnJavaScriptBeep, OnJavaScriptPrint, OnJavaScriptMail, e OnJavaScriptSubmitForm expõem as solicitações de bipe, impressão, e-mail e envio de formulário com seus conjuntos completos de parâmetros. Um visualizador que não atribui nenhum desses eventos ainda renderiza e calcula corretamente; o documento simplesmente não poderá abrir caixas de diálogo, imprimir ou enviar nada
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;
A divisão de comportamento padrão é importante para a modelagem de ameaças. O OnJavaScriptMail e o OnJavaScriptSubmitForm são no estilo de notificação: o Componente PDFium não realiza nenhuma ação de rede ou MAPI por conta própria, de modo que um documento malicioso que chama this.submitForm ou this.mailDoc contra um host não preparado realiza exatamente nada. O host deve optar por participar atribuindo um manipulador e implementando o transporte ele mesmo, o que significa que a decisão de transferir dados para fora da máquina é sempre sua, tomada em seu código, com a URL e o conteúdo (payload) em mãos
Como impedir um PDF malicioso de iniciar URLs?
As ações de URI são o único local onde o padrão histórico era ativo e não passivo, e por isso receberam um veto dedicado. Antes da versão 2.13.1, a chamada de retorno FFI_DoURIActionWithKeyboardModifier executava (via shell-execute) qualquer URI fornecida por um campo XFA, incondicionalmente, o que dava a um documento malicioso a capacidade de iniciar URLs arbitrárias ao clicar. O OnXfaUriAction fecha essa brecha: o componente consulta o evento antes de executar, e um manipulador que define Handled como True suprime totalmente o padrão do ShellExecuteW. Deixar o evento não atribuído preserva o comportamento antigo para fins de compatibilidade, portanto, um visualizador preocupado com a segurança deve sempre atribuí-lo
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;
Uma lista de permissões (allow-list) no esquema é o mínimo; um visualizador mais rígido confirma com o usuário ou direciona tudo por meio de seu próprio componente de navegador em sandbox. A mesma filosofia de veto se estende ao conjunto de eventos da v2.13.1: o OnXfaEmail, o OnXfaHttpRequest e o OnXfaOpenFile expõem os parâmetros textuais da ação solicitada, enquanto o componente em si não realiza transporte de rede ou arquivo. Como essas proteções se encaixam em uma estratégia mais ampla para documentos não confiáveis, incluindo isolamento de renderização, é o assunto de construindo uma visualização segura de PDF no Delphi
Gravando no campo focado com SetFocusedFormFieldText
O TPdf.SetFocusedFormFieldText, adicionado na versão 2.13.2, é o companheiro do lado de gravação do FocusedFormFieldText: ele seleciona o conteúdo atual do campo focado via FORM_SelectAllText e o substitui por meio do FORM_ReplaceSelection, exatamente como se o usuário tivesse digitado a substituição. Como o texto entra pelo caminho de edição interativa, quaisquer scripts de pressionamento de tecla, formatação e cálculo anexados ao campo são disparados da mesma forma que ocorrem na entrada manual, tornando-o a primitiva certa para o preenchimento programático em um visualizador que mantém o JavaScript ativo. A travessia de campos e o gerenciamento de foco em torno dele são abordados no artigo sobre navegação por campos de formulário com o Componente PDFium
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;
```
A assimetria de persistência nesse comentário é um limite real, não uma ressalva por preciosismo. Para os campos de texto e combo do AcroForm, o valor editado é gravado na entrada /V do campo ao perder o foco e persiste ao salvar. Para os campos de texto do XFA, a gravação vai apenas para o buffer de edição em memória, porque o PDFium não expõe uma API pública para reconciliar os valores dos widgets de volta no pacote de conjuntos de dados (datasets) do XFA; salvar o documento não salvará a alteração. Se a edição de dados durável do XFA for o requisito, edite o próprio XML de conjuntos de dados em vez do widget
Limites que vale a pena conhecer antes de implantar
Dois limites honestos permanecem. Primeiro, a API JavaScript que o PDFium implementa é um subconjunto funcional do modelo de objetos do Acrobat, focado no núcleo de scripts de formulário: acesso a campos, eventos de cálculo e formatação, app.alert e app.response, solicitações de impressão, e-mail e envio. Documentos que dependem de objetos exóticos exclusivos do Acrobat sofrerão degradação, geralmente de forma silenciosa, portanto teste os formulários reais que seus usuários manipulam em vez de presumir paridade. Segundo, ativar o V8 significa implantar a pdfium.v8.dll, um binário substancialmente maior do que a compilação simples; um visualizador que nunca precisa de scripts ou XFA pode legitimamente manter a DLL menor e deixar o m_pJsPlatform desconectado, o que é por si só uma postura de segurança válida
O padrão resultante de tudo isso é agradavelmente simples: defina EnableV8Engine na inicialização, atribua os eventos de alerta e resposta para que as caixas de diálogo pareçam nativas, atribua o OnXfaUriAction e registre em auditoria os eventos de e-mail e envio, e deixe todo o resto em seu padrão nulo (no-op) até que um requisito diga o contrário. A referência completa de eventos e a superfície da API de preenchimento de formulário estão documentadas na página do produto Componente PDFium