Pressione Tab num formulário PDF criado pelo seu código e o cursor poderá aterrar dois campos além de onde deveria, ignorar completamente a segunda coluna ou voltar ao topo após o terceiro campo em vez do quarto. A pessoa que preenche uma fatura no seu visualizador espera que o teclado percorra o formulário da mesma forma que percorre qualquer formulário web que já tenha utilizado. Quando isso não acontece, recorre ao rato, procura a caixa seguinte e decide silenciosamente que a sua ferramenta está inacabada. O percurso previsível dos campos é a diferença entre um visualizador de introdução de dados que as pessoas toleram e um em que confiam, sendo quase inteiramente uma questão de utilizar a API de foco correta em vez de simular a introdução do teclado com cliques simulados.
Os exemplos abaixo utilizam o PDFium Component, um componente VCL/LCL baseado em PDFium para Delphi, C++Builder e Lazarus. A navegação é uma das três coisas que um visualizador de formulários deve fazer bem; as outras duas (abrir o formulário corretamente e guardar os valores preenchidos para que realmente apareçam) são onde a maioria das surpresas se esconde, pelo que as três são abordadas abaixo.
Abrir um formulário: FormFill, FormType e a questão do XFA
O acesso aos campos exige que o subsistema de preenchimento de formulários, controlado pela propriedade FormFill, seja ativado antes de o documento ser aberto. Uma vez ativo, o FormType indica o tipo de formulário com que se está a deparar, e a resposta altera o conjunto de funcionalidades que pode prometer:
Pdf.FileName := FormPath;
Pdf.FormFill := True; // enable before Active; required for any field access
Pdf.Active := True;
case Pdf.FormType of
ftNone:
DisableFormPanel('This document has no interactive form');
ftAcroForm:
BuildFieldList; // full field navigation and editing available
ftXfaFull:
ShowXfaNotice; // XFA renders from its own XML template;
// treat field editing as limited
end;
Duas notas práticas decorrem dessa decisão. O AcroForm é o modelo de formulário padrão da norma ISO 32000, e é o alvo de todas as APIs aqui apresentadas. Os documentos XFA incorporam a sua própria arquitetura de formulários XML (XML Forms Architecture), pelo que prometer a um cliente a edição completa de XFA após uma rápida demonstração de AcroForm é um compromisso do qual se arrependerá. A segunda nota refere-se aos efeitos secundários: definir FormFill como True também inicializa o JavaScript do documento. Num visualizador de introdução de dados isso é exatamente o correto, porque os scripts de cálculo mantêm o total atualizado à medida que o utilizador digita. Numa janela de pré-visualização de ficheiros de origem desconhecida isso é totalmente incorreto. O artigo sobre pré-visualização segura de PDF aborda o lado de FormFill := False desse equilíbrio.
Percurso com a tecla Tab que aterra onde os utilizadores esperam
Voltando ao problema do teclado mencionado no início. A tentação é simular o Tab através da sintetização de um clique do rato no retângulo do widget seguinte, o que falha no instante em que um campo é deslocado para fora do ecrã ou quando dois widgets se sobrepõem. Em vez disso, a API de foco move o foco do próprio formulário diretamente, sem adivinhações geométricas. Cinco chamadas cobrem esta necessidade: FocusFormField por índice, FocusNextFormField e FocusPreviousFormField para avançar ou recuar, FocusedFormFieldIndex para ler a posição atual e ClearFormFieldFocus para retirar o foco completamente.
procedure TFormViewer.HandleTabKey(Shift: TShiftState);
begin
if ssShift in Shift then
PdfView.FocusPreviousFormField
else
PdfView.FocusNextFormField;
UpdateFieldStatus; // e.g. "Field 4 of 17: InvoiceDate"
end;
O único comportamento que baralha os programadores é o reinício do ciclo (wrap). O percurso funciona através da ordem de tabulação da página atual e entra em ciclo dentro dela: passe o último campo e regressará ao primeiro. Ambas as funções de passo devolvem o índice do novo campo, ou -1 se a página não contiver campos. Este ciclo ocorre por página, não por documento, o que significa que passar para a página seguinte é da sua responsabilidade, não da biblioteca. Compare o índice devolvido com o índice inicial, detete quando ocorreu o reinício do ciclo e avance o PageNumber manualmente se o formulário for desenhado para ser lido como uma sequência contínua. Ignore essa verificação e um formulário de duas páginas prenderá silenciosamente o cursor na página um, o que gera queixas de que a tecla Tab não funciona.
O percurso torna-se útil assim que o resto da interface de utilizador reage a ele. O evento OnFormFieldEnter é acionado quando o foco chega e, no visualizador, o OnFormFieldFocusChange reporta o novo índice do campo, para que um painel lateral possa acompanhar o que o teclado acabou de selecionar. Quando necessita do mapeamento inverso, de uma posição do ecrã para um campo, a propriedade indexada FormFieldAt realiza o teste de clique para pré-visualizações de ferramentas de ajuda (tooltips) e painéis de clique para editar. Existe um benefício silencioso de acessibilidade em tudo isto: como o foco segue a ordem de campos do próprio documento, o caminho que configura para a tecla Tab é o mesmo percurso que um leitor de ecrã anuncia, sem trabalho adicional.
Mostrar nomes de campos em vez de números de índice brutos requer mais uma propriedade. FormFieldInfo[] devolve um registo TPdfFormFieldInfo por índice, contendo o nome do campo, tipo, tamanho da fonte, estado de marcação, valor de exportação e pertença a grupo, que é o que uma lista de navegação deve exibir (por exemplo, "Campo 4 de 17: DataFatura" em vez de "4"). Os grupos de botões de rádio (radio groups) são o caso que merece um ficheiro de teste dedicado. Vários widgets podem partilhar um único nome de campo, pelo que uma lista montada ingenuamente a partir de widgets mostra o mesmo grupo várias vezes e confunde quem a lê.
Por que os valores preenchidos aparecem em branco e a chamada que resolve o problema
A outra reclamação comum nas filas de suporte é mais alarmante do que uma tecla Tab com comportamento incorreto: um formulário é preenchido programaticamente, o cliente abre-o no Acrobat e todos os campos parecem vazios. Clique num campo e o seu valor aparece instantaneamente. Os dados estiveram no ficheiro o tempo todo. O que falta é a imagem dos dados, e a razão merece ser compreendida uma vez porque explica toda uma família de erros.
Um campo de texto AcroForm armazena o seu valor na entrada /V do dicionário de campos (ISO 32000-1 §12.7.3.3). O que um visualizador realmente desenha é algo separado: o fluxo de aparência do widget sob /AP (§12.5.5), um pequeno trecho de conteúdo pré-renderizado. Escreva /V e deixe /AP inalterado, e os dois afastar-se-ão. O valor está lá; a versão desenhada do mesmo está desatualizada ou ausente. O Acrobat reconstrói a aparência de um campo quando este ganha foco, o que explica inteiramente os valores que aparecem apenas após um clique. O antigo sinalizador NeedAppearances, que solicitava aos visualizadores que regenerassem as aparências por si, nunca funcionou de forma uniforme e foi descontinuado no PDF 2.0, sendo completamente ignorado por servidores de impressão e geradores de miniaturas. Estes desenham o /AP e mais nada, pelo que se o /AP estiver vazio, imprimem uma caixa em branco.
Atribuir um valor através de FormField[i] escreve apenas /V. É por isso que preencher um formulário é uma sequência de três passos, e o passo que as equipas costumam esquecer é o do meio:
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]; // writes /V only
// Rebuild the /AP appearance streams; without this the form
// looks blank in Acrobat until each field is clicked
Pdf.GenerateFormAppearances;
Pdf.SaveAs(OutputPath);
end;
GenerateFormAppearances é a solução completa. Reconstrói o fluxo de aparência de cada widget a partir dos valores atuais, fontes e alinhamento (quadding), de modo a que um visualizador que nunca execute um evento de foco, um servidor de impressão ou um gerador de miniaturas desenhe o estado preenchido de qualquer forma. Chame a função uma vez após o lote de atribuições, e não uma vez por campo. A geração de aparência realiza trabalho real de paginação e as chamadas por campo multiplicam esse esforço inutilmente num formulário grande.
A regeneração de aparências é também o momento em que as fontes e o alinhamento se impõem, o que é a fonte de uma surpresa de segunda ordem. O novo fluxo dispõe cada valor dentro do retângulo do widget usando a fonte, o tamanho e o alinhamento do campo. Um valor que cabe confortavelmente no seu formulário de teste pode ser cortado ou encolher na cópia de um cliente onde o mesmo campo é mais estreito. Os campos de tamanho automático (tamanho de fonte zero) encolhem o texto para caber; os campos de tamanho fixo apenas o cortam. Ambos são legais, e a única forma honesta de saber o que um determinado formulário faz é olhar para o resultado regenerado e não para a string que escreveu. Quando alguém reporta texto cortado na extremidade de uma caixa, esta é quase sempre a razão.
Trate a verificação como parte da conclusão do trabalho e não como uma reflexão tardia. Abra o ficheiro guardado no Acrobat e confirme que os valores estão visíveis antes de tocar em qualquer campo. Em seguida, imprima-o para PDF ou para uma imagem a partir de um visualizador diferente, um que ignore completamente a lógica do formulário, e confirme se os valores também sobrevivem a esse caminho. Entre si, estas duas verificações detetam todas as variantes do desfasamento entre /V e /AP.
Configurações de campos que passam na demonstração e falham no terreno
Os formulários de demonstração limpos ocultam um conjunto de casos limite que os ficheiros dos clientes não evitam. Quatro deles justificam a maioria dos relatórios de "funcionou na minha máquina":
- Valores de exportação de caixas de seleção. O estado "ativo" nem sempre é
Yes. Um formulário é livre de definir o seu próprio valor de exportação, e escrever a string incorreta deixa a caixa visualmente desmarcada enquanto o seu código está convencido de que a ativou. Leia o valor de exportação a partir deFormFieldInfo[]em vez de assumir um valor por omissão. - Grupos de botões de rádio com nome partilhado. Um campo, vários widgets. O valor que atribui decide qual o widget que é lido como selecionado, pelo que o código da interface de utilizador que assume que um nome mapeia para um único retângulo acaba por desenhar o anel de foco no botão incorreto.
- Campos calculados. Os totais mantidos pelo JavaScript do documento são atualizados em resposta a eventos de campo. Um preenchimento programático que contorne estes eventos tem de acionar a recalculação ou substituir diretamente os campos calculados. Um formulário onde as linhas de detalhe e o total não coincidem é pior do que qualquer uma das soluções.
- Campos obrigatórios ocultos. Os formulários condicionais ocultam campos que continuam sinalizados como obrigatórios. Decida antecipadamente se a sua validação respeita a visibilidade ou o sinalizador bruto de obrigatório e registe essa decisão para que o suporte a possa encontrar.
Uma distinção que vale a pena estabelecer antes que o afete: a geração de aparências não é o mesmo que o achatamento (flattening). GenerateFormAppearances torna os valores visíveis em todo o lado, mantendo os campos editáveis. O achatamento incorpora a aparência no conteúdo estático da página e remove a interatividade definitivamente, o que é correto para uma cópia de arquivo e incorreto para um formulário que a pessoa seguinte ainda tem de preencher. Se o FormType reportar ftXfaFull em vez de ftAcroForm, nenhuma das opções de edição aqui mostradas se aplica de forma limpa, uma vez que o documento é desenhado a partir do seu próprio modelo XML; detete esse caso e informe o utilizador, em vez de deixar que descubra o limite sozinho.
O subsistema de preenchimento de formulários, o percurso de foco e a geração de aparência mostrados aqui fazem parte do PDFium Component para Delphi, C++Builder e Lazarus/FPC. Se o seu visualizador também processa marcações de revisor juntamente com dados de formulário, o artigo sobre revisão de anotações aborda esse modelo adjacente.