Artigo Técnico

PDFium: form-field navigation and viewer validation

Pressione Tab num formulário PDF que seu código construiu, e o cursor aterrissa dois campos além de onde deveria, ou pula a segunda coluna inteira, ou volta ao topo depois do terceiro campo em vez do quarto. Quem preenche uma nota fiscal no seu viewer espera que o teclado caminhe pelo formulário como caminha por todo formulário web que já usou. Quando não caminha, a pessoa busca o mouse, caça o próximo campo e decide silenciosamente que sua ferramenta está inacabada. Traversal de campos previsível é a diferença entre um viewer de digitação que as pessoas toleram e um em que confiam, e é quase inteiramente questão de usar a API de focus certa em vez de fingir input de teclado com cliques simulados

Os exemplos abaixo usam o PDFium Component, um componente VCL/LCL baseado em PDFium para Delphi, C++Builder e Lazarus. Navegação é uma de três coisas que um viewer de formulários precisa acertar; as outras duas, abrir o formulário corretamente e salvar valores preenchidos para que realmente apareçam, é onde a maioria das surpresas se esconde, então as três estão cobertas abaixo

Abrindo um formulário: FormFill, FormType e a questão do XFA

O acesso a campos exige que o subsistema de preenchimento de formulários, controlado pela propriedade FormFill, esteja habilitado antes de o documento ser aberto. Uma vez ativo, o FormType diz com que tipo de formulário você está lidando, e a resposta muda o conjunto de features que você pode prometer:

Diagrama da configuração de FormFill e dos branches de detecção de FormType num viewer Delphi do PDFium Component, separando o tratamento de ftNone, ftAcroForm e ftXfaFull
O FormType bifurca assim que o FormFill é habilitado, e cada branch promete um conjunto de features diferente
Pdf.FileName := FormPath;
Pdf.FormFill := True;   // habilitar antes do Active; obrigatório para qualquer acesso a campos
Pdf.Active := True;

case Pdf.FormType of
  ftNone:
    DisableFormPanel('This document has no interactive form');
  ftAcroForm:
    BuildFieldList;     // navegação e edição completa de campos disponíveis
  ftXfaFull:
    ShowXfaNotice;      // XFA renderiza a partir do próprio template XML dele;
                        // trate a edição de campos como limitada
end;

Duas notas práticas decorrem desse switch. AcroForm é o modelo de formulários padrão da ISO 32000, e é nele que toda API aqui mira. Documentos XFA embutem a própria arquitetura XML de formulários, então prometer a um cliente edição XFA completa depois de uma demo rápida de AcroForm é um compromisso de que você vai se arrepender. A segunda nota é sobre side effects: definir FormFill como True também inicializa o JavaScript do documento. Num viewer de digitação isso é exatamente o certo, porque são os scripts de cálculo que mantêm um total corrente atualizado enquanto alguém digita. Numa janela de preview de arquivos de origem desconhecida é exatamente o errado. O artigo sobre preview seguro de PDF cobre o lado FormFill := False desse trade-off

Traversal pela tecla Tab que aterrissa onde o usuário espera

De volta ao problema do teclado, do começo. A tentação é fingir o Tab sintetizando um clique de mouse no retângulo do próximo widget, o que quebra no instante em que um campo sai da tela ou dois widgets se sobrepõem. A API de focus move o focus do próprio formulário diretamente, sem adivinhação de geometria. Cinco chamadas cobrem tudo: FocusFormField por índice, FocusNextFormField e FocusPreviousFormField para avançar, FocusedFormFieldIndex para ler onde você está e ClearFormFieldFocus para largar o focus de vez

Diagrama do traversal de focus pela tecla Tab num viewer Delphi do PDFium Component, em que o FocusNextFormField faz wrap dentro do tab order de uma página e cinco APIs de focus cobrem a navegação por teclado
O traversal faz loop dentro do tab order de uma página, então cruzar para a próxima página continua sendo trabalho do viewer
procedure TFormViewer.HandleTabKey(Shift: TShiftState);
begin
  // AcrossPages = True: continua na próxima página que tiver campos (3.220.0+)
  if ssShift in Shift then
    PdfView.FocusPreviousFormField(True)
  else
    PdfView.FocusNextFormField(True);
  UpdateFieldStatus;  // ex.: "Campo 4 de 17: InvoiceDate"
end;

O único comportamento que derruba as pessoas é o wrap. O traversal funciona pelo tab order da página atual e faz loop dentro dele: passe do último campo e você volta ao primeiro. Ambas as funções de passo retornam o índice do novo campo, ou -1 quando a página não tem campo algum. Nas chamadas sem parâmetros esse looping é por página, não por documento. Desde o PDFium Component 3.220.0, tanto TPdf quanto TPdfView também oferecem FocusNextFormField(AcrossPages, WrapDocument, MaxPagesToScan) e o overload correspondente de FocusPreviousFormField, que movem para a página seguinte ou anterior que tiver um campo elegível, opcionalmente fazem wrap nas pontas do documento e param depois de um número limitado de páginas. Passe True em AcrossPages quando o formulário deve ser lido como uma sequência contínua; senão um formulário de duas páginas prende o cursor silenciosamente na página um, o que é outra versão da reclamação do Tab quebrado. A partir da 3.223.0, o traversal de cada página também segue o /Tabs declarado dela, descrito em tab order de formulários PDF e /Tabs

O traversal fica útil quando o resto da UI reage a ele. O evento OnFormFieldEnter dispara quando o focus chega, e no viewer o OnFormFieldFocusChange reporta o índice do novo campo, então um painel lateral pode acompanhar o que o teclado acabou de selecionar. Quando você precisa do mapeamento inverso, de posição de tela para campo, a propriedade indexada FormFieldAt faz o hit-testing para previews de tooltip e painéis de clique-para-editar. Há um ganho silencioso de acessibilidade em tudo isso: como o focus segue a ordem de campos do próprio documento, o caminho que você ligar para a tecla Tab é o mesmo que um screen reader anuncia, sem trabalho extra

Mostrar nomes de campos em vez de números crus de índice toma mais uma propriedade. O FormFieldInfo[] retorna um record TPdfFormFieldInfo por índice, com o nome do campo, tipo, tamanho de fonte, estado de marcado, export value e pertencimento a grupo, que é o que uma lista de navegação deve exibir ("Campo 4 de 17: InvoiceDate" em vez de "4"). Radio groups são o caso que merece um arquivo de teste dedicado. Vários widgets podem compartilhar um único nome de campo, então uma lista montada ingenuamente a partir de widgets mostra o mesmo grupo várias vezes e confunde todo mundo que lê

Por que valores preenchidos saem em branco, e a chamada que conserta

A outra reclamação que enche filas de suporte é mais alarmante que um Tab mal comportado: um formulário é preenchido programaticamente, o cliente abre no Acrobat, e todo campo aparece vazio. Clique no campo e o valor dele aparece na hora. Os dados estão no arquivo o tempo todo. O que falta é a imagem dos dados, e o motivo vale entender uma vez porque explica uma família inteira de bugs

Um campo de texto AcroForm guarda seu valor na entrada /V do dicionário do campo (ISO 32000-1 §12.7.3.3). O que um viewer realmente pinta é algo separado: o appearance stream do widget sob /AP (§12.5.5), um pequeno trecho de conteúdo pré-renderizado. Escreva /V e deixe /AP em paz, e os dois se afastam. O valor está lá; a versão renderizada dele está desatualizada ou ausente. O Acrobat, por acaso, reconstrói o appearance de um campo quando ele ganha focus, o que é toda a explicação para valores que só aparecem no clique. A velha flag NeedAppearances, que pedia aos viewers para regenerar appearances por você, nunca funcionou de modo uniforme e está deprecada no PDF 2.0, e print servers e geradores de thumbnail a ignoram completamente. Eles pintam /AP e nada mais, então se /AP está vazio imprimem uma caixa em branco

Atribuir um valor por meio de FormField[i] escreve só o /V. É por isso que preencher um formulário é uma sequência de três passos, e o passo que as equipes derrubam é o do meio:

Diagrama do drift entre o valor /V e o appearance /AP em campos AcroForm e da sequência de preenchimento de três passos em Delphi construída em torno do GenerateFormAppearances
Atribuir valores escreve só o /V, e o passo do meio é o que repinta o que os print servers realmente renderizam
procedure TFormViewer.FillAndSave(const Values: array of WString;
  const OutputPath: string);
var
  i: Integer;
begin
  for i := 0 to Pdf.FormFieldCount - 1 do
    Pdf.FormField[i] := Values[i];   // escreve só o /V

  // Reconstrói os appearance streams /AP; sem isso o formulário
  // aparece em branco no Acrobat até cada campo ser clicado
  Pdf.GenerateFormAppearances;

  Pdf.SaveAs(OutputPath);
end;

O GenerateFormAppearances é o conserto inteiro. Ele reconstrói o appearance stream de cada widget a partir dos valores, fontes e quadding correntes, então um viewer que nunca roda evento de focus, um print server ou um thumbnailer pinta o estado preenchido mesmo assim. Chame uma vez depois do lote de atribuições, não uma vez por campo. Geração de appearance faz trabalho de layout de verdade, e chamadas por campo multiplicam isso num formulário grande de graça

Regenerar appearances é também o momento em que fontes e alinhamento se impõem, o que é a fonte de uma surpresa de segunda ordem. O novo stream compõe cada valor dentro do retângulo do widget usando a fonte, o tamanho e o quadding do campo. Um valor que cabe de folga no seu formulário de teste pode ser cortado ou encolher na cópia do cliente em que o mesmo campo é mais estreito. Campos auto dimensionados (tamanho de fonte zero) encolhem o texto para caber; campos de tamanho fixo só o cortam. Os dois são válidos, e o único jeito honesto de saber qual dos dois um formulário faz é olhar o output regenerado em vez da string que você escreveu. Quando alguém reporta texto cortado na borda de uma caixa, essa quase sempre é a razão

Trate a verificação como parte de terminar o trabalho, não como reflexão tardia. Abra o arquivo salvo no Acrobat e confirme que os valores estão visíveis antes de tocar em qualquer campo. Depois imprima para PDF ou para uma imagem a partir de outro viewer, um que ignora lógica de formulários por completo, e confirme que os valores sobrevivem a esse caminho também. Entre eles, essas duas checagens pegam toda variante do drift de /V contra /AP

Configurações de campos que passam na demo e falham em campo

Formulários de demo limpos escondem um conjunto de edge cases que arquivos de clientes não têm. Quatro deles respondem pela maioria dos relatos de "funcionava na minha máquina"

  • Export values de checkbox. O estado "on" nem sempre é Yes. Um formulário pode definir o próprio export value, e escrever a string errada deixa a caixa visualmente desmarcada enquanto seu código tem certeza de que a marcou. Leia o export value do FormFieldInfo[] em vez de assumir um
  • Radio groups de nome compartilhado. Um campo, vários widgets. O valor que você atribui decide qual widget lê como selecionado, então código de UI que assume que um nome mapeia para um retângulo acaba desenhando o focus ring no botão errado
  • Campos calculados. Totais mantidos pelo JavaScript do documento se atualizam em resposta a eventos de campo. Um preenchimento programático que bypassa esses eventos precisa ou disparar o recálculo ou sobrescrever os campos calculados diretamente. Um formulário em que os itens de linha e o total discordam é pior que qualquer um dos dois consertos
  • Campos required ocultos. Formulários condicionais escondem campos que continuam marcados como required. Decida de antemão se sua validação respeita a visibilidade ou a flag bruta de required, e registre essa decisão num lugar que o suporte consiga achar

Uma distinção vale resolver antes que ela morda você: gerar appearances não é flattening. O GenerateFormAppearances torna os valores visíveis em todo lugar deixando os campos editáveis. O flattening consolida o appearance em conteúdo estático de página e arranca a interatividade de vez, o que é certo para uma cópia de arquivo morto e errado para um formulário que a próxima pessoa ainda precisa preencher. Se o FormType reportar ftXfaFull em vez de ftAcroForm, nenhuma superfície de edição aqui se aplica de forma limpa de qualquer forma, já que o documento renderiza do próprio template XML dele; detecte esse caso e avise o usuário, em vez de deixá-lo encontrar o limite sozinho

O subsistema de preenchimento de formulários, o traversal de focus e a geração de appearances mostrados aqui fazem parte do PDFium Component para Delphi, C++Builder e Lazarus/FPC. Se o seu viewer também lida com markup de revisores junto com dados de formulário, o artigo sobre revisão de anotações cobre esse modelo adjacente