O TPdf.SetFocusedFormFieldText no PDFium Component escreve no buffer de edição ativo do campo de formulário atualmente com foco, e, para um formulário XFA, esse buffer nunca chega ao pacote datasets que é serializado para disco — pelo que um valor que um utilizador escreve, e que o código confirma ter sido aceite, desaparece silenciosamente na próxima vez que o ficheiro é aberto. Os campos AcroForm não têm este problema: a mesma chamada confirma na entrada /V do campo no momento em que o foco se afasta. Um utilizador que preenche um formulário de admissão XFA, grava, e reabre para encontrar o campo do montante de novo vazio não está a deparar-se com uma falha de renderização — está a deparar-se com o limite daquilo que o próprio motor PDFium expõe para escrever dados de formulário
Esta é uma questão mais restrita do que detetar um formulário XFA em primeiro lugar, ou fazer correr o seu JavaScript: não "o PDFium suporta XFA" nem "como executo scripts AcroForm" mas especificamente o que acontece a um valor depois de SetFocusedFormFieldText reportar sucesso. A versão curta é que, no que toca ao caminho de escrita do PDFium, o AcroForm e o XFA não são dois dialetos do mesmo modelo de formulário — são dois modelos de formulário com duas relações completamente diferentes entre o que um utilizador escreve e o que uma gravação efetivamente captura, e confundir os dois é o que transforma uma chamada de API de uma linha num pedido de suporte três semanas depois de o piloto de um cliente entrar em produção. O artigo sobre JavaScript AcroForm mostra a chamada de uma linha e enuncia o resultado AcroForm-versus-XFA num comentário de código; este mantém-se na mesma API e percorre o caminho de escrita interno, a prova pelo pacote datasets de que a escrita XFA nunca chega a ser guardada, porque a lacuna reside no próprio PDFium em vez de na vinculação Delphi, e uma solução de contorno de corrigir o próprio XML para documentos que precisam que a edição sobreviva a uma gravação
Como é que o SetFocusedFormFieldText escreve um valor de campo?
O TPdf.SetFocusedFormFieldText funciona simulando uma edição ao nível das teclas premidas, não injetando um valor diretamente no modelo de documento. Internamente chama FORM_SelectAllText para selecionar o conteúdo atual do campo com foco, e depois FORM_ReplaceSelection para sobrescrever a seleção com a nova cadeia de texto — as mesmas duas operações que um select-all-e-escrever guiado por teclado despoletaria. Como a escrita passa pelo caminho de edição de texto interativo do PDFium em vez de o contornar, qualquer script de tecla premida, formato, ou cálculo associado ao campo dispara exatamente como disparia para um humano a escrever, o que torna a API útil para preenchimento programático de formulários num visualizador que mantém o JavaScript ativo. O equivalente do lado da leitura é FocusedFormFieldText, sustentado por FORM_GetFocusedText, e reflete o mesmo buffer ativo 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');
Porque é que o AcroForm mantém o valor e o XFA o perde?
Os campos de texto e combo do AcroForm persistem porque o próprio ambiente de preenchimento de formulários do PDFium confirma o buffer de edição pelo utilizador: no instante em que o campo perde o foco, o buffer é escrito na entrada /V do campo, a mesma chave que qualquer leitor de PDF conforme consulta para saber o valor guardado de um campo. O TPdf.ClearFormFieldFocus — que chama FORM_ForceToKillFocus por baixo — força essa confirmação a pedido, para que código que define um valor programaticamente não tenha de esperar por um clique de rato real noutro ponto da interface. Ao gravar imediatamente a seguir, o novo texto já faz parte do grafo de objetos do documento antes de o TPdf.SaveAs sequer correr, porque o /V é uma entrada real num dicionário de campo real, não algo acrescentado 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 reside efetivamente uma edição de campo XFA?
Os campos XFA não têm essa ligação. O texto que um utilizador escreve fica num buffer CPWL_Edit que pertence à camada de renderização e interação XFA do PDFium, e essa camada não tem qualquer caminho de código que copie o buffer de volta para o pacote datasets guardado no PDF. O TPdf.GetXfaDatasets torna a lacuna visível: chame-o antes e depois de uma edição num campo XFA e os bytes que devolve são idênticos, porque o método lê o pacote original com que o documento foi aberto, nunca o estado ativo do widget que acabou de editar. Nada disto é um erro de cache ou uma questão de tempo de atualização — o pacote datasets no disco e o buffer de edição em memória são simplesmente dois estados distintos que a API pública do PDFium nunca liga entre si
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;
Isto é um erro do PDFium Component ou uma limitação do PDFium?
A peça em falta reside no próprio PDFium, não na vinculação Delphi construída por cima. A API pública do PDFium não tem nenhum FPDF_SetXFAPacket para injetar um pacote atualizado nem nenhum FPDF_SaveAsXFA para pedir ao motor XFA que serialize o seu DOM atual de volta para XML datasets antes de uma gravação. O FPDF_SaveAsCopy — a exportação que sustenta TPdf.SaveAs — escreve o grafo de objetos do documento que o PDFium já possui; não tem qualquer gancho para pedir ao motor XFA que descarregue primeiro o seu estado ativo, porque esse gancho não existe a montante. O PDFium Component não pode acrescentar uma reconciliação que o próprio PDFium nunca implementou, e distribuir um serializador de DOM para XML caseiro que tentasse adivinhar o estado XFA interno do PDFium seria pior do que a lacuna assumida honestamente: pareceria funcionar até a versão seguinte do PDFium mudar algo que ninguém fora do projeto consegue ver
Este limite surgiu durante a mesma auditoria da v2.13.2 que construiu o SetFocusedFormFieldText em primeiro lugar. O FORM_ReplaceSelection tinha estado vinculado na tabela de importação da DLL durante várias versões sem alguma vez ser chamado a partir de código Pascal, e acrescentar o caminho de escrita que finalmente o usou é o que tornou a lacuna de persistência suficientemente concreta para documentar em vez de teórica. A mesma ronda de auditoria revelou uma lacuna sem relação direta mas afim em espírito: o JavaScript AcroForm tinha estado silenciosamente desativado desde a v2.13.0 porque a plataforma JS só estava ligada dentro do ramo de inicialização XFA, pelo que documentos AcroForm comuns com app.alert ou campos calculados nunca chegavam a obter qualquer motor de scripts. Essa era corrigível — estendendo a plataforma JS a qualquer documento, independentemente de XFA — e foi lançada na mesma versão; a lacuna de persistência aqui abordada não era corrigível, pelas razões acima. A correção do JavaScript e os eventos de veto do anfitrião à sua volta são abordados em executar JavaScript AcroForm com o PDFium Component
O que se deve fazer quanto a isto em Delphi?
Para documentos AcroForm, a correção não é mais do que um bom hábito: chamar ClearFormFieldFocus (ou de outra forma afastar o foco) antes de SaveAs sempre que um valor tenha sido definido programaticamente, em vez de presumir que uma interação de interface posterior vai despoletar a confirmação por conta própria. Para um documento que possa ser tanto AcroForm como XFA — o caso comum num visualizador de uso geral — verificar FormType ou o booleano XFA antes de prometer a um chamador que uma gravação vai persistir, e ler detetar formulários XFA e extrair pacotes XFA para o conjunto completo de verificações, incluindo o caso XFAF em que o conteúdo XFA é sobreposto a widgets AcroForm de outro modo comuns que respeitam /V
Para um verdadeiro formulário XFA dinâmico em que os valores editados têm de sobreviver a uma gravação, o buffer de edição interativo não é de todo a ferramenta certa. O caminho durável é tratar o GetXfaDatasets como a base de partida, não como o resultado: lê-lo uma vez quando o documento abre, manter o próprio registo do que o utilizador alterou campo a campo — exatamente os valores que a interface do utilizador já possui, já que o PDFium não os vai devolver depois do facto — corrigir esses valores na própria base XML, e conduzir a própria saída. Uma escrita que passe por XML controlado pelo próprio código sobrevive a uma gravação 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;
Detetar a lacuna antes que um cliente o faça
O TPdf.SaveAs devolve True quer um valor de campo XFA tenha sobrevivido quer não, porque, do ponto de vista do PDFium, a gravação foi genuinamente bem-sucedida — escreveu cada byte que lhe foi pedido para escrever. Isso torna este exatamente o tipo de defeito que escapa a um teste rápido e chega a um cliente: nada gera exceção, nada regista um erro, o ficheiro abre sem problemas, só o valor específico está errado. Um teste de ida e volta que efetivamente reabra o ficheiro gravado e compare o valor do campo — ou que compare GetXfaDatasets antes e depois, conforme o exemplo anterior — pertence ao conjunto de testes de regressão de qualquer visualizador que permita aos utilizadores editar conteúdo XFA, não apenas aos caminhos AcroForm que calham funcionar por predefinição
Nada disto é um defeito a reportar contra o PDFium Component, mas antes um limite em torno do qual desenhar: o SetFocusedFormFieldText faz precisamente o que o seu nome diz para ambos os modelos de formulário, e a diferença no resultado remonta com clareza àquilo a que o AcroForm e o XFA ligam cada um esse buffer do lado do PDFium. A API, as primitivas de foco e gravação, e os leitores de pacotes aqui referidos fazem parte do PDFium Component para Delphi e C++Builder