Artigo Técnico

Porque É Que as Edições de Campo XFA Desaparecem ao Gravar no PDFium para Delphi

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

Diagrama dividido de SetFocusedFormFieldText no PDFium Component para Delphi: um buffer de edição vivo diverge num caminho AcroForm, em que a perda de foco confirma o valor em /V e o SaveAs o persiste, e num caminho XFA, em que o pacote datasets nunca é atualizado e o ficheiro gravado abre em branco
A mesma escrita aterra num único buffer vivo, e o modelo do formulário decide o seu destino. O AcroForm confirma em /V na perda de foco, enquanto o XFA deixa o pacote de datasets intocado
Pdf.FocusFormField(FieldIndex);
Pdf.SetFocusedFormFieldText('1284.50');
Pdf.ClearFormFieldFocus;              // força a confirmação /V agora
Pdf.SaveAs('invoice-acroform.pdf');

// Reabre e confirma - este é um documento AcroForm, por isso mantém
Pdf.Active := False;
Pdf.FileName := 'invoice-acroform.pdf';
Pdf.Active := True;
Pdf.FocusFormField(FieldIndex);
Assert(Pdf.FocusedFormFieldValue = '1284.50');   // passa

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;
  // Antes e Depois são byte-a-byte idênticos num documento XFA -
  // a edição nunca tocou no pacote que o GetXfaDatasets lê
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

Diagrama em camadas da persistência XFA em Delphi: o binding do PDFium Component envolve a API pública do PDFium, cujas funções de formulário exportadas terminam em FPDF_SaveAsCopy, enquanto FPDF_SetXFAPacket e FPDF_SaveAsXFA não existem upstream, pelo que o estado vivo do motor XFA nunca é serializado de volta para os datasets antes de uma gravação
A vinculação chama todas as exportações que o PDFium oferece e não pode inventar as que faltam. Sem um hook de serialização, o motor XFA guarda o seu estado vivo para si

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
  // O GetXfaDatasets vem com o PDFium Component; o PatchXmlNode abaixo é
  // o seu próprio auxiliar sobre a sua própria biblioteca XML, nada que o PDFium forneça
  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

Pipeline de edição XFA durável para Delphi: ler a linha de base dos datasets com GetXfaDatasets, registar as edições do utilizador campo a campo no seu próprio estado de UI, corrigir o XML com o seu próprio auxiliar e gravar um output em que o valor editado sobrevive
Trate GetXfaDatasets como a base e a sua UI como o registo das edições. Remendar você mesmo o XML do pacote põe os bytes que chegam ao disco sob o seu controlo

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