Um formulário cujos totais recalculam no Acrobat mas ficam congelados no seu próprio visualizador Delphi quase nunca é um bug de renderização. O PDFium desativa todo o JavaScript de documento a não ser que o anfitrião ligue uma plataforma JS, e o PDFium Component liga essa plataforma automaticamente a partir da versão 2.13.2, pelo que o JavaScript de AcroForm, os scripts ao nível do documento e os cálculos de campo são executados sempre que a DLL com V8 estiver carregada. O anfitrião mantém o comando através de um conjunto de eventos que podem mostrar, redirecionar ou vetar tudo o que o script tentar fazer
Essa única frase resolve uma questão de suporte que já vimos sob várias formas: "o meu formulário de encomenda calcula os totais de linha no Acrobat mas não na minha aplicação", "o app.alert nunca dispara", "o campo de data não se formata sozinho". Todas remetem para o mesmo contrato, e compreendê-lo vale dez minutos, porque o mesmo contrato é também a sua fronteira de segurança contra documentos hostis
Por que razão os cálculos de formulário PDF não funcionam no PDFium?
O PDFium trata o JavaScript de documento como estritamente por adesão: se o membro m_pJsPlatform do ambiente de preenchimento de formulários for NULL, todos os scripts do documento são silenciosamente ignorados. Nenhum erro, nenhuma linha de registo, nenhuma execução parcial. Os campos calculados mantêm o último valor guardado, o app.alert desaparece, os scripts de formatação nunca correm. O Acrobat, que traz sempre o seu próprio motor de JavaScript, corre alegremente o mesmo ficheiro, razão pela qual a discrepância se parece tanto com um bug no seu código quando é na verdade um comportamento por omissão deliberado da biblioteca por baixo
O PDFium Component tinha aqui uma segunda camada, histórica. Até à versão 2.13.1 a camada de ligação só anexava m_pJsPlatform dentro do ramo de inicialização de XFA, pelo que os documentos AcroForm simples, que são a esmagadora maioria dos PDF com scripts, nunca chegavam a receber plataforma de JavaScript nenhuma, mesmo com a DLL com V8 presente. A versão 2.13.2 retirou a ligação da decisão sobre XFA: o InitializeFormFill constrói agora a tabela IPDF_JsPlatform sempre que V8FeaturesAvailable devolver True, independentemente de o documento ser XFA. O resultado prático é que os scripts ao nível do documento, as cadeias de cálculo de campos e o app.alert passam a executar-se também em documentos AcroForm vulgares
De que DLL do V8 precisa o JavaScript de AcroForm em Delphi?
O JavaScript de AcroForm precisa de um binário do PDFium compilado com o motor V8, que o PDFium Component carrega como pdfium.v8.dll quando a flag global EnableV8Engine está a True antes de a biblioteca carregar pela primeira vez. Há aqui uma armadilha que vale a pena dizer sem rodeios: o componente deteta automaticamente documentos XFA fazendo uma pré-análise à procura de marcadores /XFA e liga por si o EnableV8Engine, mas um documento AcroForm simples com JavaScript não transporta marcador XFA nenhum, pelo que não há deteção automática. Se o seu visualizador deve correr scripts de formulário, ative a flag você mesmo, uma vez, antes de o primeiro documento carregar; depois de uma pdfium.dll simples estar carregada, o processo já não consegue trocar de motor
uses PDFium;
procedure TViewerForm.FormCreate(Sender: TObject);
begin
// Tem de correr antes de a biblioteca singleton do PDFium carregar;
// com a pdfium.dll simples no processo já não é possível trocá-la
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 honesta em tempo de execução: reporta se o binário carregado resolve de facto as exportações dependentes do V8, pelo que a verificação funciona seja qual for a DLL que um instalador acabou por implantar. O mesmo motor e a mesma flag também sustentam a renderização dinâmica de XFA; o enquadramento sobre como a deteção de XFA e a seleção automática do V8 interagem está coberto em detetar formulários XFA e extrair pacotes XFA com o PDFium
Eventos do anfitrião para alertas, impressão, correio e submissão de formulários
Tudo o que um script faz de visível regressa ao seu código através de eventos de TPdf, e cada um deles assume por omissão uma operação nula e segura quando não está atribuído. OnJavaScriptAlert recebe a mensagem, o título, o tipo de botões e o ícone vindos de app.alert e permite-lhe devolver o resultado do botão premido; OnJavaScriptResponse serve os pedidos de entrada de app.response, incluindo a flag de palavra-passe; OnJavaScriptBeep, OnJavaScriptPrint, OnJavaScriptMail e OnJavaScriptSubmitForm expõem os pedidos de sinal sonoro, impressão, correio e submissão com os respetivos conjuntos completos de parâmetros. Um visualizador que não atribua nenhum destes continua a renderizar e a calcular corretamente; o documento simplesmente não consegue abrir caixas de diálogo, imprimir ou enviar seja o que for
procedure TViewerForm.PdfJavaScriptAlert(Sender: TObject;
const Msg, Title: WString; ButtonType, Icon: Integer;
var Result_: Integer; var Handled: Boolean);
begin
// Encaminha o app.alert pela VCL para que pertença à nossa janela
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
// Nada é transmitido a não ser que este handler decida enviá-lo
LogAudit(Format('Document requested form submit to %s (%d bytes)',
[Url, Length(Data)]));
end;
A separação de comportamentos por omissão importa para a modelação de ameaças. OnJavaScriptMail e OnJavaScriptSubmitForm são do tipo notificação: o PDFium Component não executa por si nenhuma ação de rede ou MAPI, pelo que um documento malicioso que chame this.submitForm ou this.mailDoc contra um anfitrião não preparado não consegue absolutamente nada. O anfitrião tem de aderir atribuindo um handler e implementando ele próprio o transporte, o que significa que a decisão de tirar dados da máquina é sempre sua, tomada no seu código, com o URL e a carga útil em mão
Como impedir que um PDF malicioso abra URLs?
As ações de URI são o único ponto em que o comportamento histórico por omissão era ativo em vez de passivo, e é por isso que receberam um veto dedicado. Antes da versão 2.13.1 o callback FFI_DoURIActionWithKeyboardModifier executava pelo shell qualquer URI que um campo XFA fornecesse, sem condições, o que dava a um documento malicioso a capacidade de abrir URLs arbitrários ao clique. O OnXfaUriAction fecha essa porta: o componente consulta o evento antes de executar, e um handler que ponha Handled a True suprime por completo o comportamento por omissão de ShellExecuteW. Deixar o evento por atribuir preserva o comportamento antigo por compatibilidade, pelo que um visualizador atento à segurança deve atribuí-lo sempre
procedure TViewerForm.PdfXfaUriAction(Sender: TObject;
const Uri: WString; Modifiers: Integer; var Handled: Boolean);
begin
// Veta tudo o que não seja https simples; por omissão o URI
// seria passado tal e qual ao ShellExecuteW
if not SameText(Copy(Uri, 1, 8), 'https://') then
begin
LogAudit('Blocked URI action: ' + Uri);
Handled := True;
end;
end;
Uma lista de esquemas permitidos é o mínimo; um visualizador mais estrito confirma com o utilizador ou encaminha tudo através do seu próprio componente de navegação em ambiente isolado. A mesma filosofia de veto estende-se por todo o conjunto de eventos da v2.13.1: OnXfaEmail, OnXfaHttpRequest e OnXfaOpenFile expõem cada um os parâmetros textuais da ação pedida, enquanto o próprio componente não executa nenhum transporte de rede ou de ficheiros. Como estas defesas encaixam numa estratégia mais ampla para documentos não fiáveis, incluindo o isolamento da renderização, é o tema de construir uma pré-visualização segura de PDF em Delphi
Escrever no campo com foco através de SetFocusedFormFieldText
TPdf.SetFocusedFormFieldText, acrescentado na versão 2.13.2, é o complemento do lado da escrita para FocusedFormFieldText: seleciona o conteúdo atual do campo com foco através de FORM_SelectAllText e substitui-o com FORM_ReplaceSelection, exatamente como se o utilizador tivesse escrito a substituição. Como o texto entra pelo caminho de edição interativa, quaisquer scripts de tecla, formatação e cálculo ligados ao campo disparam da mesma forma que disparam para entrada manual, o que faz dele a primitiva certa para preenchimento programático num visualizador que mantém o JavaScript ativo. O percurso pelos campos e a gestão do foco à sua volta estão cobertos em navegação por campos de formulário com o 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: o valor é gravado em /V quando o foco sai do campo.
// XFA: a escrita fica apenas no buffer de edição em memória e
// não sobrevive a uma gravação
end;
A assimetria de persistência nesse comentário é uma fronteira real, não uma ressalva por desencargo. Para campos de texto e caixas de combinação de AcroForm o valor editado é gravado na entrada /V do campo quando o foco se perde e persiste através da gravação. Para campos de texto XFA a escrita fica apenas no buffer de edição em memória, porque o PDFium não expõe API pública que reconcilie os valores dos widgets de volta no pacote de datasets do XFA; gravar o documento não leva a alteração consigo. Se o requisito for edição durável de dados XFA, edite o próprio XML de datasets em vez do widget
Limites que vale a pena conhecer antes de expedir
Restam duas fronteiras honestas. Primeira, a API de JavaScript que o PDFium implementa é um subconjunto funcional do modelo de objetos do Acrobat, centrado no núcleo de scripts de formulário: acesso a campos, eventos de cálculo e de formatação, app.alert e app.response, e pedidos de impressão, correio e submissão. Documentos que se apoiem em objetos exóticos exclusivos do Acrobat vão degradar-se, normalmente em silêncio, pelo que deve testar os formulários reais que os seus utilizadores manuseiam em vez de presumir paridade. Segunda, ativar o V8 significa expedir a pdfium.v8.dll, um binário substancialmente maior do que a compilação simples; um visualizador que nunca precise de scripts nem de XFA pode legitimamente ficar-se pela DLL mais pequena e deixar o m_pJsPlatform por ligar, o que é por si só uma postura de segurança válida
O padrão que sai de tudo isto é agradavelmente aborrecido: definir EnableV8Engine no arranque, atribuir os eventos de alerta e de resposta para que as caixas de diálogo pareçam nativas, atribuir OnXfaUriAction e registar em auditoria os eventos de correio e de submissão, e deixar tudo o resto na sua operação nula por omissão até que um requisito diga o contrário. A referência completa dos eventos e a superfície de API de preenchimento de formulários estão documentadas na página de produto do PDFium Component