No PDFium Component, o componente VCL/LCL baseado em PDFium para Delphi, C++Builder e Lazarus, um índice de campo de formulário não é um índice de anotação. Uma página transporta anotações Link, Text e Ink ao lado dos seus widgets, pelo que a enumeração de campos tem de filtrar por FPDFAnnot_GetSubtype e expor um índice lógico de base zero, mapeado de volta para uma posição de anotação real apenas na chamada nativa
O bug que expõe isto é inconfundível assim que já o tenha visto. Um testador prime Tab num formulário de fatura preenchido e o cursor desaparece, porque o foco foi para uma hiperligação no rodapé. Ou pior, não acontece nada: o seu código regista o campo 3 como focado, o painel de interface atualiza-se, e FORM_SetFocusedAnnot devolveu silenciosamente falso o tempo todo. Ambos os sintomas vêm do mesmo erro de conceção, e um deles esconde uma segunda causa-raiz por baixo
Os dois espaços de índice que o PDFium fornece
O PDFium expõe dois esquemas de numeração sobre a mesma página, e só coincidem em documentos que por acaso não contenham senão widgets de formulário. O primeiro é o índice de anotação: uma posição no array /Annots da página, que é o que FPDFPage_GetAnnotCount conta e o que FPDFPage_GetAnnot recebe (ISO 32000-1 §12.5.2). O segundo é o índice lógico de campo que uma API ao nível da aplicação deveria oferecer, correndo a partir de zero sobre os campos interativos que um utilizador consegue realmente alcançar. A ISO 32000-1 §12.5.6.19 define as anotações de widget como a representação visual dos campos de formulário interativos, e o §12.7 define o próprio formulário. Tudo o resto na página é um subtipo diferente com semântica diferente: uma anotação Link tem um destino, uma anotação Ink tem uma lista de traços, uma anotação Text é uma nota adesiva. Nenhuma delas pertence a uma contagem de campos, e nenhuma pode aceitar foco de formulário. No entanto, no array /Annots ficam intercaladas com os widgets pela ordem em que a aplicação produtora as escreveu, o que frequentemente não corresponde à ordem sugerida por mais nada no documento
Porque é que Tab pousa numa hiperligação em vez do campo seguinte?
Porque a contagem de campos era, na realidade, uma contagem de anotações. A implementação original devolvia FPDFPage_GetAnnotCount diretamente a partir de FormFieldCount, enquanto o acessor de informação de campo, o auxiliar de ordem de tabulação e o auxiliar de foco tratavam todos esse mesmo número inteiro como uma posição de widget. Numa página AcroForm limpa com seis widgets e mais nada, seis é igual a seis e todos os testes passam. Acrescente uma hiperligação no rodapé e um comentário de revisor na margem, e a contagem passa a reportar oito campos, os índices 6 e 7 resolvem para objetos que não são de formulário, e Tab entra a direito neles
A correção do lado da enumeração é contar subtipos em vez de anotações. Abra cada anotação, pergunte o seu subtipo, mantenha os widgets, e feche o handle num bloco finally, porque FPDFPage_GetAnnot devolve um handle possuído que tem de voltar através de FPDFPage_CloseAnnot
function WidgetCountForPage(Page: FPDF_PAGE): Integer;
var
Count, I: Integer;
Annot: FPDF_ANNOTATION;
begin
Result := 0;
if Page = nil then
Exit;
Count := FPDFPage_GetAnnotCount(Page); // every annotation, not just fields
for I := 0 to Count - 1 do
begin
Annot := FPDFPage_GetAnnot(Page, I);
if Annot = nil then
Continue;
try
if FPDFAnnot_GetSubtype(Annot) = FPDF_ANNOT_WIDGET then
Inc(Result);
finally
FPDFPage_CloseAnnot(Annot);
end;
end;
end;
Note o que isto deliberadamente não faz. Não pergunta nada ao ambiente de preenchimento de formulário, e não precisa de um handle de formulário, porque o subtipo vive no dicionário de anotação e é legível a partir da própria página. Isso importa para o ordenamento: a contagem está disponível antes de sequer se decidir se o documento merece um ambiente de preenchimento de formulário, o que o artigo sobre JavaScript AcroForm e eventos do anfitrião trata como uma decisão de segurança e não de conveniência
Mapear o índice lógico de volta na fronteira nativa
A regra que evita que os dois espaços se contaminem mutuamente é simples: o índice lógico é o único número que atravessa a sua API pública, e é convertido em índice de anotação na última função antes da chamada nativa. Um único auxiliar de mapeamento, usado por igual pela informação de campo, foco, definidores de flags e ordem de tabulação, é o que torna essa regra aplicável
function AnnotationIndexForField(Page: FPDF_PAGE;
FieldIndex: Integer): Integer;
var
Count, I, Current: Integer;
Annot: FPDF_ANNOTATION;
begin
Result := -1;
if (Page = nil) or (FieldIndex < 0) then
Exit;
Count := FPDFPage_GetAnnotCount(Page);
Current := 0;
for I := 0 to Count - 1 do
begin
Annot := FPDFPage_GetAnnot(Page, I);
if Annot = nil then
Continue;
try
if FPDFAnnot_GetSubtype(Annot) = FPDF_ANNOT_WIDGET then
begin
if Current = FieldIndex then
Exit(I); // real /Annots position: native calls only
Inc(Current);
end;
finally
FPDFPage_CloseAnnot(Annot);
end;
end;
end;
Vale a pena afirmar claramente duas propriedades deste auxiliar. É uma varredura linear, pelo que um ciclo ingénuo sobre cada campo custa um número quadrático de aberturas de anotação numa página com centenas de widgets; se estiver a enumerar a página inteira, percorra as anotações uma vez e recolha os handles de widget à medida que avança, em vez de chamar o mapeador por campo. E devolve -1 em vez de levantar exceção, o que deixa ao chamador a decisão de saber se um índice desatualizado é um erro de programação que merece uma exceção ou uma corrida a ignorar, por exemplo depois de uma edição ter removido uma anotação à qual uma lista de interface em cache ainda se refere
Porque é que FORM_SetFocusedAnnot falha numa página sem interface?
Porque o PDFium recusa focar um widget cuja vista de página nunca foi marcada como válida. FORM_SetFocusedAnnot resolve a anotação a uma vista de página dentro do ambiente de preenchimento de formulário, e se essa vista de página não existir devolve falso sem qualquer diagnóstico. Corrigir apenas o mapeamento de índices resolve, por isso, o Tab que pousa numa hiperligação, mas deixa intocado o segundo sintoma: o seu registo lógico de foco diz campo 3, o widget focado nativo continua vazio, e todos os acessores construídos sobre o foco nativo, texto focado, valor focado, estado de seleção de escolha, continuam a devolver vazio. A vista de página é criada por FORM_OnAfterLoadPage e destruída por FORM_OnBeforeClosePage. Num visualizador construído à volta de um controlo visual, essas chamadas acontecem como parte da apresentação de uma página, razão pela qual a falha tantas vezes parece um bug exclusivo de contexto sem interface: o mesmo código que funciona na demonstração gráfica falha na ferramenta em lote. O ciclo de vida pertence ao objeto documento, não ao visualizador, pelo que o PDFium Component emite agora ambas as chamadas sempre que uma página é carregada ou descarregada com um handle de formulário presente. A assinatura C recebe primeiro a página e depois o handle de formulário, o que é fácil de inverter ao escrever o binding à mão
procedure ReportFirstField(const FileName: string);
var
Pdf: TPdf;
Idx: Integer;
begin
Pdf := TPdf.Create(nil);
try
Pdf.FormFill := True; // form-fill environment, before Active
Pdf.FileName := FileName;
Pdf.Active := True;
Pdf.PageNumber := 1; // page load also runs FORM_OnAfterLoadPage
Idx := Pdf.FocusNextFormField; // logical index, 0-based over widgets
if Idx < 0 then
Exit; // page holds no widget annotations
Writeln(string(Pdf.FormFieldInfo[Idx].Name), ' = ',
string(Pdf.FocusedFormFieldValue)); // reads the native focused widget
finally
Pdf.Free; // page unload runs FORM_OnBeforeClosePage
end;
end;
A verificação que prova a correção é a que compara os dois lados. Chame FocusFormField com um índice lógico, e depois leia um valor através de um acessor que passa pelo widget focado nativo em vez de pelo seu próprio registo, como FocusedFormFieldValue ou FocusedFormOptionSelected. Se o índice lógico voltar corretamente mas o acessor nativo vier vazio, é a vista de página que falta, não o mapeamento
O que o índice lógico de campo não promete
Um índice de campo de base zero é uma conveniência, não uma identidade semântica, e daí decorrem quatro limites. É por página, não por documento, pelo que o índice 0 na página 2 é um widget diferente do índice 0 na página 1, e compará-los não faz sentido. É posicional, pelo que inserir ou eliminar uma anotação invalida todos os índices em cache acima da alteração; trate um índice guardado como válido apenas enquanto a página permanecer carregada e não editada
O terceiro limite é o que surpreende quem revê uma lista de campos. O índice enumera widgets, não campos. Um grupo de opções é um único campo com vários widgets filhos, pelo que um grupo de três botões contribui com três índices consecutivos que reportam todos o mesmo Name. O registo TPdfFormFieldInfo transporta GroupCount e GroupIndex precisamente para este caso, e uma interface de lista que os ignore mostra o mesmo campo três vezes. O quarto limite diz respeito à ordem de percurso: a ordem de tabulação aqui exposta é a ordem de enumeração de widgets, que segue o array /Annots, não a entrada /Tabs da página (ISO 32000-1 §7.7.3.3) nem a árvore de campos AcroForm. Na maioria dos produtores estas coincidem; num formulário disposto em duas colunas por um gerador que tenha emitido primeiro a coluna direita, não coincidem, e o percurso por teclado descrito no artigo sobre navegação de campos de formulário vai parecer errado mesmo que cada índice esteja correto. Quando um ficheiro de cliente se comporta de forma estranha, despeje ambos os espaços de índice lado a lado antes de teorizar: a vista de anotações e a vista de campos da mesma página, impressas juntas, tornam normalmente a causa óbvia num único olhar
procedure DumpIndexSpaces(Pdf: TPdf);
var
I: Integer;
Info: TPdfFormFieldInfo;
begin
for I := 0 to Pdf.AnnotationCount - 1 do
Writeln('annot ', I, ': subtype ', Ord(Pdf.Annotation[I].Subtype));
for I := 0 to Pdf.FormFieldCount - 1 do
begin
Info := Pdf.FormFieldInfo[I];
Writeln('field ', I, ': ', string(Info.Name),
' widget ', Info.GroupIndex, ' of ', Info.GroupCount);
end;
end;
Uma contagem de anotações muito acima da contagem de campos significa que a página mistura subtipos, o que é normal em documentos revistos e é exatamente a situação para a qual o mapeamento existe; o artigo sobre o fluxo de revisão de anotações olha para a mesma página a partir do lado da anotação. Contagens iguais em todos os ficheiros de teste, por outro lado, significam que os seus fixtures não conseguem detetar esta classe de bug de forma alguma, e a resposta honesta é acrescentar um fixture de formulário que transporte uma hiperligação e uma nota adesiva
As APIs de enumeração de campos, foco e anotações aqui descritas fazem parte do PDFium Component para Delphi, C++Builder e Lazarus, cuja página de produto traz a referência completa de campos de formulário, incluindo o registo de informação de campo e os acessores de foco