A partir da versão 3.223.0, o PDFium Component for Delphi ordena a navegação de campos de formulário pela política /Tabs declarada de cada página: R para ordem por linha, C para ordem por coluna, S para ordem de estrutura, mais os valores A e W do PDF 2.0. O TPdf.FormFieldTabOrder devolve essa ordem, e o overload de status dele informa se a política declarada foi aplicada ou se um fallback foi usado
A queixa que motiva tudo isso é sempre a mesma. Um usuário preenche um formulário de seguro de duas colunas, aperta Tab depois do campo de primeiro nome, e o cursor pula para o fundo da coluna da direita. Nada está quebrado nos índices. O produtor simplesmente gravou a coluna direita no /Annots primeiro e declarou /Tabs /R na página para consertar a ordem visual, então um viewer que percorre o /Annots e ignora a declaração aterrissa no lugar errado. O artigo sobre índice de widget versus índice de anotação cobre como ordinais de widget se relacionam com o array /Annots. No PDFium Component a política declarada vence sempre que puder ser estabelecida, e quando não puder, o componente diz por quê
O que a entrada /Tabs realmente declara?
O /Tabs é um nome opcional no objeto de página (ISO 32000-2 §7.7.3.3; Tabela 30 na ISO 32000-1) que diz a um processador em que ordem visitar as anotações daquela página, e a semântica dele é detalhada sob anotações na §12.5.1. O PDF 1.5 introduziu três valores, e o PDF 2.0 adicionou dois mais:
/R, ordem por linha: linhas de cima para baixo, e dentro de cada linha a direção de leitura do viewer/C, ordem por coluna: colunas na direção de leitura, e de cima para baixo dentro de cada coluna/S, ordem de estrutura: a ordem em que a árvore de estrutura lógica referencia as anotações/A, ordem do array de anotações: a ordem do próprio/Annots(PDF 2.0)/W, ordem de widgets: widgets primeiro na ordem do array, depois as outras anotações (PDF 2.0)
O detalhe que derruba implementações escritas à mão é herança. O /Tabs pertence só ao dicionário de página. A ISO 32000-2 §7.7.3.4 limita os atributos de página herdáveis a Resources, MediaBox, CropBox e Rotate, então uma chave /Tabs num nó /Pages é sem sentido, e um leitor que sobe a cadeia de pais procurando um está inventando uma política que o autor nunca definiu. O PDFium Component lê o /Tabs da própria página e de mais nenhum lugar
Lendo a ordem declarada com FormFieldTabOrder
O TPdf.FormFieldTabOrder devolve os campos de formulário da página atual como ordinais de widget com base 0 na ordem de tab declarada, e o overload com um parâmetro out Status: TPdfFormTabOrderStatus explica como essa ordem foi produzida. O mesmo par existe no TPdfView, e o overload simples usa a política idêntica enquanto descarta o status
uses
PDFium;
const
ModeNames: array[TPdfFormTabOrderMode] of string =
('unspecified', 'R', 'C', 'S', 'A', 'W', 'unsupported');
procedure DumpTabOrder(const FileName: string; PageNo: Integer);
var
Pdf: TPdf;
Order: TPdfIntegerArray;
Status: TPdfFormTabOrderStatus;
I: Integer;
begin
Pdf := TPdf.Create(nil);
try
Pdf.FileName := FileName;
Pdf.FormFill := True; // já é o default; obrigatório para campos
Pdf.Active := True;
if not Pdf.Active or (Pdf.FormType <> ftAcroForm) then
Exit;
Pdf.PageNumber := PageNo;
Order := Pdf.FormFieldTabOrder(Status);
Writeln('requested ', ModeNames[Status.RequestedMode],
', applied ', ModeNames[Status.EffectiveMode]);
if Status.UsedFallback then
Writeln('fallback: ', Status.ErrorMessage);
if Status.UnstructuredWidgetCount > 0 then
Writeln(Status.UnstructuredWidgetCount, ' widgets not in the structure tree');
for I := 0 to High(Order) do
Writeln(I, ': field ', Order[I], ' ',
string(Pdf.FormFieldInfo[Order[I]].Name));
finally
Pdf.Free;
end;
end;
O record de status é o que transforma uma decisão de ordenação silenciosa em algo que você pode registrar no log. O RequestedMode é o que a página declarou, o EffectiveMode é o que foi aplicado, e o UsedFallback é true quando a política declarada não pôde ser honrada e a ordem do array de anotações voltou em vez dela, com um motivo curto no ErrorMessage. O SnapshotAvailable diz se o componente conseguiu ler os metadados do documento de todo. Um /Tabs ausente é reportado como fallback para a ordem do array de anotações em vez de passar por política declarada, então um log de suporte consegue distinguir "o autor pediu esta ordem" de "a página não pediu nada"
Por que a ordem por linha e por coluna não pode só ordenar os valores /Rect?
Ordem por linha e por coluna precisam ser calculadas na geometria de exibição, não nas coordenadas brutas de anotação, porque o /Rotate de uma página e a direção de leitura do documento ambos mudam o que "linha" significa. Ordene os arrays /Rect de uma página com /Rotate 90 pela coordenada y e você recebe o que o viewer mostra como colunas. O componente então mapeia todo retângulo de widget pela transform de exibição da página, rotação intrínseca incluída, em resolução de 1/64 ponto, e agrupa a partir daí
A direção de leitura vem do dicionário ViewerPreferences do catálogo (ISO 32000-2 §12.2). O /Direction /R2L inverte a ordem dentro de uma linha e inverte a ordem das colunas, que é o que um formulário árabe ou hebraico espera. Quando o /Direction está ausente, L2R se aplica. Quando ele guarda qualquer coisa que não seja L2R ou R2L, a ordem por linha e por coluna recuam para a ordem do array de anotações com o motivo no status, porque chutar uma direção é pior que admitir que não há nenhuma
O agrupamento é onde processadores legitimamente divergem, então vale conhecer a regra exata. Para a ordem por linha, o widget não visitado mais ao topo vira a âncora, e todo widget cujo centro vertical cai entre o topo e o fundo da âncora entra na linha dela. A ordem por coluna espelha isso horizontalmente. Empates se resolvem deterministicamente pelo ordinal do widget. Numa grade limpa isso bate com qualquer viewer sensato. Num layout com campos pesadamente sobrepostos ou escalonados, outro processador usando uma tolerância diferente pode agrupar de forma diferente, e nenhuma parte da ISO 32000 torna uma das respostas errada
Como a ordem de estrutura decide a sequência?
A ordem de estrutura segue os dicionários de referência de objeto (OBJR) na árvore de estrutura, na ordem em que uma caminhada em profundidade pelos arrays /K realmente os alcança. Ela não segue a ordem das chaves do /ParentTree, e não segue qualquer ordem que uma enumeração do StructTreeRoot por acaso produza. Essas ordens costumam coincidir em arquivos de um único produtor e divergem depois de um editor rearranjar a árvore, que é precisamente quando a ordem de leitura de um formulário etiquetado mais importa. Se você mantém essa árvore por conta própria, o guia de manter /ParentTree e /StructParents em sincronia cobre o mapeamento reverso que precisa se manter consistente com ela
Widgets que a árvore de estrutura nunca referencia ficam a cargo do processador pela especificação. O PDFium Component os anexa depois dos widgets estruturados na ordem do array de anotações e reporta quantos anexou no UnstructuredWidgetCount. Uma página que declara /S num documento sem árvore de estrutura nenhuma é tratada como uma travessia vazia: todo widget cai nessa cauda, a contagem diz isso, e o resultado não é sinalizado como fallback total. Uma árvore de estrutura com um ciclo, um OBJR referenciado duas vezes, ou um OBJR cujo /Pg contradiz a página que possui a anotação faz a ordem de estrutura recuar. Esses defeitos tornam o S inutilizável, mas não perturbam o R nem o C nas outras páginas
Por que os valores de ordem de tab não são índices de anotação?
Os valores no array devolvido pelo FormFieldTabOrder contam só widgets, enquanto um OBJR identifica um objeto de anotação que vive no array /Annots completo. Misturar os dois espaços de índice é o bug mais provável em qualquer código que consome a ordem de estrutura. Pegue uma página cujo /Annots é [Link, Widget A, Widget B]. O OBJR para o Widget B aponta para o índice de anotação 2. O Widget B é o ordinal de campo 1. Alimente 2 ao FocusFormField e você ou foca o campo errado ou não foca nada, dependendo de quantos widgets a página tem
O componente faz essa conversão internamente, e é por isso que o resultado dele pode ir direto para o FocusFormField e o FormFieldInfo. Se o seu próprio código lê alvos de OBJR ou outros índices de anotação, converta-os explicitamente antes de tratá-los como campos:
// Converte um índice de anotação de página para o ordinal de widget usado por
// FocusFormField, FormFieldInfo e FormFieldTabOrder; -1 se não for widget
function AnnotationToFieldOrdinal(Pdf: TPdf; AnnotIndex: Integer): Integer;
var
I: Integer;
begin
Result := -1;
if (AnnotIndex < 0) or (AnnotIndex >= Pdf.AnnotationCount) then
Exit;
if Pdf.Annotation[AnnotIndex].Subtype <> anWidget then
Exit; // links, notas e popups não são campos
Result := 0;
for I := 0 to AnnotIndex - 1 do
if Pdf.Annotation[I].Subtype = anWidget then
Inc(Result);
end;
Trate o resultado como válido só enquanto a página continua carregada e não editada. Adicionar ou remover qualquer anotação na frente de um widget desloca os dois espaços de índice, e um mapeamento em cache de antes da edição é exatamente o tipo de off-by-one que só se reproduz em arquivos de clientes
Navegação entre páginas com FocusNextFormField
O FocusNextFormField(AcrossPages: Boolean; WrapDocument: Boolean = True; MaxPagesToScan: Integer = 256) percorre a ordem de tab da página atual, depois continua nas páginas seguintes, e a partir do 3.223.0 toda página que ele visita aplica a política própria declarada dela. O overload entre páginas chegou na versão 3.220.0, então o conselho no artigo de navegação de campos de formulário de detectar o wrap e mudar o PageNumber você mesmo agora se aplica só ao overload sem argumentos, que ainda cicla dentro de uma página
procedure TFormViewer.HandleTabKey(Shift: TShiftState);
var
Field: Integer;
begin
if ssShift in Shift then
Field := PdfView.FocusPreviousFormField(True) // entre páginas, com wrap
else
Field := PdfView.FocusNextFormField(True);
if Field < 0 then
Beep // nada elegível dentro do scan; página e foco inalterados
else
UpdateFieldStatus(PdfView.PageNumber, Field);
end;
O valor de retorno é o ordinal de campo local da página em que quer que o foco agora esteja, então leia o PageNumber junto. A travessia entre páginas pula widgets em que um usuário não conseguiria tabular de forma significativa: widgets hidden, invisible, NoView, read-only, de assinatura, de tipo desconhecido e de retângulo vazio. Passar WrapDocument = False para na fronteira do documento com -1. O MaxPagesToScan precisa estar entre 1 e 4096, e toda página inspecionada conta, incluindo a atual. Quando o scan não encontra nada ou o engine nativo recusa o foco no alvo, a chamada devolve -1 e deixa a página e o foco anteriores exatamente como estavam
Onde a ordem declarada para de se aplicar?
A política declarada é honrada só quando o componente consegue ler os metadados da página de forma confiável, e todo caso em que não consegue recua para a ordem de widgets do array de anotações com um motivo explícito. As fronteiras são específicas o suficiente para listar:
- Os metadados vêm de uma serialização privada limitada do documento nativo atual, não dos bytes originais do arquivo, porque movimentos de página e edições em memória deixam o arquivo em disco velho. O snapshot é cacheado por revisão de conteúdo, então edições notificadas o invalidam, e snapshots falhos também são cacheados
- Documentos criptografados, e snapshots pedidos durante uma edição de page-object, não têm snapshot e sempre recuam
- O snapshot tem teto de 64 MiB, 65536 objetos ativos, 4096 páginas, 4096 anotações por página, 65536 anotações no total e uma profundidade de estrutura de 64; documentos maiores recuam em vez de travar a navegação
- Só documentos AcroForm são cobertos. Formulários XFA ficam fora deste perfil
- A ordem por linha e por coluna precisa de retângulos de widget finitos, não vazios, e dimensões de página sãs; geometria degenerada dispara o fallback
- Essas APIs dirigem a navegação do seu código. O tratamento próprio do PDFium de uma tecla Tab encaminhada ao engine não é substituído, então roteie a tecla pelo
FocusNextFormFieldse você quer a ordem declarada
Consultas preservam a página atual, o foco nativo e qualquer texto de edição pendente no campo focado, e não rodam ações de ciclo de vida de documento ou página. Chamar o FormFieldTabOrder a cada tecla é portanto seguro, e depois da primeira chamada numa dada revisão de conteúdo é barato
Ordem de tab declarada, os diagnósticos de status e a navegação de foco entre páginas são parte do PDFium Component for Delphi and C++Builder; a página do produto linka a referência completa da API, incluindo as entradas TPdfFormTabOrderMode e TPdfFormTabOrderStatus