TPdf.SetFocusedFormFieldText no Componente PDFium escreve no buffer de edição vivo do campo de formulário atualmente focado, e para um formulário XFA esse buffer nunca alcança o pacote datasets que é serializado em disco — de modo que um valor que um usuário digita, e que seu código confirma ter sido aceito, silenciosamente desaparece na próxima vez que o arquivo abre. Campos AcroForm não têm esse problema: a mesma chamada confirma na entrada /V do campo no instante em que o foco se move para outro lugar. Um usuário que preenche um formulário de admissão XFA, salva, e reabre para encontrar o campo de valor vazio novamente não está atingindo uma falha visual — está atingindo a borda do que o próprio motor PDFium expõe para escrever dados de formulário
Essa é uma questão mais restrita do que detectar um formulário XFA para começo de conversa, ou fazer seu JavaScript rodar: não "o PDFium suporta XFA" e não "como eu executo scripts AcroForm" mas especificamente o que acontece com um valor depois que SetFocusedFormFieldText reporta sucesso. A versão curta é que AcroForm e XFA não são dois dialetos do mesmo modelo de formulário no que diz respeito ao caminho de escrita do PDFium — são dois modelos de formulário com duas relações completamente diferentes entre o que um usuário digita e o que um salvamento de fato captura, e confundir os dois é o que transforma uma chamada de API de uma linha em um chamado de suporte três semanas depois que a implantação piloto de um cliente entra em produção. O artigo de JavaScript AcroForm mostra a chamada de uma linha e declara o resultado AcroForm-versus-XFA em um comentário de código; este permanece na mesma API e percorre o caminho de escrita interno, a prova via pacote datasets de que a escrita XFA nunca pousa, por que a lacuna fica no próprio PDFium em vez da vinculação Delphi, e uma solução alternativa de corrigir seu próprio XML para documentos que precisam que a edição sobreviva a um salvamento
Como o SetFocusedFormFieldText escreve um valor de campo?
TPdf.SetFocusedFormFieldText funciona simulando uma edição em nível de tecla, não colocando um valor diretamente no modelo de documento. Internamente, chama FORM_SelectAllText para selecionar o conteúdo atual do campo focado, depois FORM_ReplaceSelection para sobrescrever a seleção com a nova string — as mesmas duas operações que um selecionar-tudo-e-digitar conduzido por teclado disparariam. Como a escrita passa pelo caminho de edição de texto interativo do PDFium, em vez de contorná-lo, qualquer script de tecla, formato ou cálculo vinculado ao campo dispara exatamente como faria para um humano digitando, que é o que torna a API útil para preenchimento programático de formulário em um visualizador que mantém o JavaScript vivo. A contraparte de leitura é FocusedFormFieldText, apoiada por FORM_GetFocusedText, e reflete o mesmo buffer vivo que SetFocusedFormFieldText acabou de escrever
if Pdf.FocusedFormFieldIndex >= 0 then
begin
if Pdf.SetFocusedFormFieldText('1284.50') then
Log('Buffer now reads: ' + Pdf.FocusedFormFieldText)
else
Log('No field is focused, or it does not accept text');
end
else
Log('Focus a field first - FocusFormField or a real click');
Por que o AcroForm mantém o valor e o XFA o perde?
Campos de texto e combo AcroForm persistem porque o próprio ambiente de preenchimento de formulário do PDFium confirma o buffer de edição para você: no instante em que o campo perde o foco, o buffer é escrito na entrada /V do campo, a mesma chave que todo leitor de PDF em conformidade consulta para saber o valor armazenado de um campo. TPdf.ClearFormFieldFocus — que chama FORM_ForceToKillFocus por baixo dos panos — força essa confirmação sob demanda, de modo que código que define um valor programaticamente não precisa esperar por um clique de mouse real em outro lugar da UI. Salve imediatamente depois, e o novo texto é parte do grafo de objetos do documento antes mesmo de TPdf.SaveAs rodar, porque /V é uma entrada real em um dicionário de campo real, não algo aparafusado depois
Pdf.FocusFormField(FieldIndex);
Pdf.SetFocusedFormFieldText('1284.50');
Pdf.ClearFormFieldFocus; // forces the /V commit now
Pdf.SaveAs('invoice-acroform.pdf');
// Reopen and confirm - this is an AcroForm document, so it holds
Pdf.Active := False;
Pdf.FileName := 'invoice-acroform.pdf';
Pdf.Active := True;
Pdf.FocusFormField(FieldIndex);
Assert(Pdf.FocusedFormFieldValue = '1284.50'); // passes
Onde de fato mora uma edição de campo XFA?
Campos XFA não têm essa fiação. O texto que um usuário digita pousa em um buffer CPWL_Edit que pertence à camada de renderização e interação XFA do PDFium, e essa camada não tem nenhum caminho de código que copie o buffer de volta para o pacote datasets armazenado no PDF. TPdf.GetXfaDatasets torna a lacuna visível: chame-o antes e depois de uma edição em um campo XFA e os bytes que retorna são idênticos, porque o método lê o pacote original com o qual o documento foi aberto, nunca o estado vivo do widget que você acabou de editar. Nada disso é um bug de cache ou um problema de tempo de atualização — o pacote datasets no disco e o buffer de edição em memória são simplesmente dois pedaços de estado diferentes que a API pública do PDFium nunca conecta
var
Before, After: TBytes;
begin
Before := Pdf.GetXfaDatasets;
Pdf.FocusFormField(FieldIndex);
Pdf.SetFocusedFormFieldText('1284.50');
After := Pdf.GetXfaDatasets;
// Before and After are byte-for-byte identical on an XFA document -
// the edit never touched the packet GetXfaDatasets reads from
end;
Isso é um bug do Componente PDFium ou uma limitação do PDFium?
A peça faltante fica no próprio PDFium, não na vinculação Delphi por cima dele. A API pública do PDFium não tem nenhum FPDF_SetXFAPacket para injetar um pacote atualizado e nenhum FPDF_SaveAsXFA para pedir ao motor XFA que serialize seu DOM atual de volta em XML datasets antes de um salvamento. FPDF_SaveAsCopy — a exportação que apoia TPdf.SaveAs — escreve o grafo de objetos do documento que o PDFium já tem; não tem nenhum gancho para pedir ao motor XFA que descarregue seu estado vivo primeiro, porque esse gancho não existe rio acima. O Componente PDFium não consegue adicionar reconciliação que o próprio PDFium nunca implementou, e lançar um serializador caseiro de DOM-para-XML que adivinha o estado XFA interno do PDFium seria pior que a lacuna honesta: pareceria funcionar até a próxima versão do PDFium mudar algo que ninguém fora do projeto consegue ver
Essa fronteira apareceu durante a mesma auditoria da v2.13.2 que construiu SetFocusedFormFieldText para começo de conversa. FORM_ReplaceSelection havia sido vinculado na tabela de importação da DLL por versões, sem nunca ser chamado a partir de código Pascal, e adicionar o caminho de escrita que finalmente o usou é o que tornou a lacuna de persistência concreta o bastante para documentar, em vez de teórica. A mesma rodada de auditoria descobriu uma lacuna não relacionada, mas de espírito relacionado: o JavaScript AcroForm havia sido silenciosamente desativado desde a v2.13.0 porque a plataforma JS só estava conectada dentro do ramo de inicialização XFA, de modo que documentos AcroForm comuns com app.alert ou campos calculados nunca recebiam nenhum motor de script. Aquele era corrigível — estender a plataforma JS para todo documento independentemente de XFA — e foi lançado na mesma versão; a lacuna de persistência coberta aqui não era corrigível, pelos motivos acima. A correção de JavaScript e os eventos de veto do host ao redor dela são cobertos em rodando JavaScript AcroForm com o Componente PDFium
O que você deveria fazer sobre isso em Delphi?
Para documentos AcroForm, a correção não é nada além de bom hábito: chame ClearFormFieldFocus (ou de outra forma mova o foco para longe) antes de SaveAs sempre que um valor foi definido programaticamente, em vez de assumir que uma interação de UI posterior vai disparar a confirmação para você. Para um documento que pode ser AcroForm ou XFA — que é o caso comum em um visualizador de propósito geral — verifique FormType ou o booleano XFA antes de prometer a quem chama que um salvamento vai grudar, e leia detectando formulários XFA e extraindo pacotes XFA para o conjunto completo de sondagens, incluindo o caso XFAF onde conteúdo XFA é colocado em camada sobre widgets AcroForm por outro lado comuns que respeitam /V
Para um formulário XFA dinâmico genuíno onde os valores editados precisam sobreviver a um salvamento, o buffer de edição interativo simplesmente não é a ferramenta certa. O caminho durável é tratar GetXfaDatasets como sua linha de base, não seu resultado: leia-o uma vez quando o documento abre, mantenha seu próprio registro do que o usuário mudou campo por campo — exatamente os valores que sua UI já tem, já que o PDFium não vai devolvê-los a você depois do fato — corrija-os na linha de base XML você mesmo, e conduza sua própria saída. Uma escrita que passa por XML que seu próprio código controla sobrevive a um salvamento que um buffer CPWL_Edit nunca conseguiria
function ExportEditedXfaValue(Pdf: TPdf; const FieldPath,
NewValue: string): TBytes;
var
DatasetsXml: string;
begin
// GetXfaDatasets ships with PDFium Component; PatchXmlNode below is
// your own helper over your own XML library, nothing PDFium provides
DatasetsXml := TEncoding.UTF8.GetString(Pdf.GetXfaDatasets);
DatasetsXml := PatchXmlNode(DatasetsXml, FieldPath, NewValue);
Result := TEncoding.UTF8.GetBytes(DatasetsXml);
end;
Capturando a lacuna antes de um cliente
TPdf.SaveAs retorna True independentemente de um valor de campo XFA ter sobrevivido, porque do ponto de vista do PDFium o salvamento genuinamente teve sucesso — escreveu cada byte que foi pedido para escrever. Isso torna exatamente o tipo de defeito que escapa de um teste de fumaça e chega a um cliente: nada levanta exceção, nada registra em log, o arquivo abre bem, só o valor específico está errado. Um teste de ida e volta que de fato reabre o arquivo salvo e compara o valor do campo — ou compara GetXfaDatasets antes e depois, conforme o exemplo anterior — pertence à suíte de regressão de qualquer visualizador que deixe usuários editarem conteúdo XFA, não apenas os caminhos AcroForm que por acaso funcionam por padrão
Nada disso é um defeito para registrar contra o Componente PDFium tanto quanto uma fronteira para projetar em torno: SetFocusedFormFieldText faz precisamente o que seu nome diz para ambos os modelos de formulário, e a diferença no resultado se rastreia limpamente ao que AcroForm e XFA cada um conecta esse buffer no lado do PDFium. A API, os primitivos de foco e salvamento, e os leitores de pacote referenciados aqui fazem parte do Componente PDFium para Delphi e C++Builder