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 por coluna, S por estrutura, mais os valores A e W do PDF 2.0. O TPdf.FormFieldTabOrder devolve essa ordem, e o seu overload de estado reporta se a política declarada foi aplicada ou se se usou um fallback
A queixa que motiva tudo isto é sempre a mesma. Um utilizador preenche um formulário de seguros de duas colunas, prime Tab depois do campo do primeiro nome, e o cursor salta para o fundo da coluna da direita. Nada está partido nos índices. O produtor simplesmente escreveu a coluna da direita primeiro no /Annots e declarou /Tabs /R na página para corrigir a ordem visual, por isso um viewer que percorre o /Annots e ignora a declaração aterra no lugar errado. O artigo sobre índice de widget contra í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-lhe porque
O que declara de facto a entrada /Tabs?
O /Tabs é um nome opcional no objeto de página (ISO 32000-2 §7.7.3.3; Tabela 30 do ISO 32000-1) que diz a um processador em que ordem visitar as anotações dessa página, e as suas semânticas estão enunciadas sob anotações em §12.5.1. O PDF 1.5 introduziu três valores, e o PDF 2.0 acrescentou dois:
/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 pela ordem do array, depois as outras anotações (PDF 2.0)
O detalhe que derruba implementações escritas à mão é a herança. O /Tabs pertence só ao dicionário de página. O ISO 32000-2 §7.7.3.4 limita os atributos de página herdáveis a Resources, MediaBox, CropBox e Rotate, por isso uma chave /Tabs num nó /Pages não significa nada, e um reader que suba a cadeia de pais à procura de uma está a inventar uma política que o autor nunca pôs. O PDFium Component lê o /Tabs da própria página e de mais nenhum lugar
Ler a ordem declarada com o FormFieldTabOrder
O TPdf.FormFieldTabOrder devolve os campos de formulário da página atual como ordinais de widget base 0 pela ordem de tabulação 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 estado
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; exigido 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 estado é o que transforma uma decisão de ordem silenciosa em algo que 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 seu lugar, com uma razão curta em ErrorMessage. O SnapshotAvailable diz se o componente conseguiu ler os metadados do documento de todo. Um /Tabs em falta é reportado como fallback para a ordem do array de anotações em vez de passado por política declarada, por isso um log de suporte consegue distinguir "o autor pediu esta ordem" de "a página não pediu nada"
Porque é que a ordem por linha e coluna não pode simplesmente ordenar os valores /Rect?
A ordem por linha e coluna tem de ser computada na geometria de apresentação, não nas coordenadas cruas de anotações, 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 obtém o que o viewer mostra como colunas. O componente por isso mapeia cada retângulo de widget pela transformação de apresentação da página, rotação intrínseca incluída, a resolução de 1/64 de ponto, e agrupa a partir daí
A direção de leitura vem do dicionário ViewerPreferences do catálogo (ISO 32000-2 §12.2). Um /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, aplica-se L2R. Quando contém qualquer coisa que não L2R ou R2L, a ordem por linha e coluna recai na ordem do array de anotações com a razão no estado, porque adivinhar uma direção é pior do que admitir que não há nenhuma
O agrupamento é onde processadores legitimamente diferem, por isso vale a pena conhecer a regra exata. Para ordem por linha, o widget não visitado mais no topo torna-se a âncora, e cada widget cujo centro vertical caia dentro do topo e fundo da âncora junta-se à sua linha. A ordem por coluna espelha isto horizontalmente. Empates decidem-se de forma determinística pelo ordinal do widget. Numa grelha limpa isto casa com qualquer viewer sensato. Numa disposição com campos fortemente sobrepostos ou escalonados, outro processador com uma tolerância diferente pode agrupar de outra forma, e nenhuma parte do ISO 32000 torna qualquer das respostas errada
Como decide a ordem de estrutura a sequência?
A ordem de estrutura segue os dicionários de referência de objeto (OBJR) na árvore de estrutura, pela ordem em que uma passagem depth-first dos arrays /K realmente os alcança. Não segue a ordem das chaves do /ParentTree, e não segue a ordem que uma enumeração do StructTreeRoot aconteça produzir. Essas ordens coincidem muitas vezes em ficheiros 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 mantém essa árvore você mesmo, o guia para manter /ParentTree e /StructParents sincronizados cobre o mapeamento inverso que tem de se manter consistente com ela
Widgets que a árvore de estrutura nunca referencia ficam ao critério do processador pela especificação. O PDFium Component acrescenta-os depois dos widgets estruturados pela ordem do array de anotações e reporta quantos acrescentou em UnstructuredWidgetCount. Uma página que declare /S num documento sem árvore de estrutura de todo é tratada como travessia vazia: todos os widgets aterram nessa cauda, a contagem diz isso, e o resultado não é marcado como fallback completo. 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 recair em fallback. Esses defeitos tornam o S inutilizável, mas não perturbam o R ou o C noutras páginas
Porque é que os valores de ordem de tabulação 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 índices é o bug mais provável em qualquer código que consuma ordem de estrutura. Pegue numa 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. Dê o 2 ao FocusFormField e ou faz focus no campo errado ou em nada, dependendo de quantos widgets a página tem
O componente faz essa conversão internamente, e é por isso que o seu resultado pode ir direto ao FocusFormField e ao FormFieldInfo. Se o seu próprio código lê alvos OBJR ou outros índices de anotação, converta-os explicitamente antes de os tratar como campos:
// Converte um índice de anotação de página no 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 se mantém carregada e sem edição. Acrescentar ou remover qualquer anotação à frente de um widget desloca ambos os espaços de índices, e um mapeamento em cache de antes da edição é exatamente o tipo de off-by-one que só se reproduz em ficheiros de clientes
Navegação entre páginas com o FocusNextFormField
O FocusNextFormField(AcrossPages: Boolean; WrapDocument: Boolean = True; MaxPagesToScan: Integer = 256) percorre a ordem de tabulação da página atual, depois continua para páginas seguintes, e a partir do 3.223.0 cada página que visita aplica a sua própria política declarada. O overload entre páginas chegou na versão 3.220.0, por isso o conselho do artigo de navegação de campos de formulário de detetar 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, dá a volta
else
Field := PdfView.FocusNextFormField(True);
if Field < 0 then
Beep // nada elegível na varredura; página e focus inalterados
else
UpdateFieldStatus(PdfView.PageNumber, Field);
end;
O valor de retorno é o ordinal de campo local à página em qualquer página que agora tenha o focus, por isso leia o PageNumber ao lado dele. A travessia entre páginas salta widgets em que um utilizador não consegue tabular com sentido: widgets ocultos, invisíveis, NoView, read-only, de assinatura, de tipo desconhecido e de retângulo vazio. Pôr WrapDocument = False para na fronteira do documento com -1. O MaxPagesToScan tem de estar entre 1 e 4096, e cada página inspecionada conta, incluindo a atual. Quando a varredura não encontra nada ou o motor nativo recusa o focus no alvo, a chamada devolve -1 e deixa a página anterior e o focus exatamente como estavam
Onde é que a ordem declarada deixa de se aplicar?
A política declarada é honrada só quando o componente consegue ler os metadados da página de forma fiável, e cada caso em que não consegue recai na ordem de widgets do array de anotações com uma razão explícita. As fronteiras são específicas o suficiente para listar:
- Os metadados vêm de uma serialização privada delimitada do documento nativo atual, não dos bytes do ficheiro original, porque movimentos de páginas em memória e edições tornam o ficheiro em disco obsoleto. A fotografia é posta em cache por revisão de conteúdo, por isso edições notificadas invalidam-na, e fotografias falhadas também são postas em cache
- Documentos encriptados, e fotografias pedidas durante uma edição de page-object, não têm fotografia e recuam sempre em fallback
- A fotografia tem teto de 64 MiB, 65536 objetos ativos, 4096 páginas, 4096 anotações por página, 65536 anotações no total e profundidade de estrutura de 64; documentos maiores recuam em fallback em vez de bloquear a navegação
- Só documentos AcroForm estão cobertos. Formulários XFA ficam fora deste perfil
- A ordem por linha e coluna precisa de retângulos de widget finitos e não vazios e dimensões de página sensatas; geometria degenerada dispara o fallback
- Estas APIs conduzem a navegação a partir do seu código. O tratamento próprio do PDFium de uma tecla Tab reencaminhada ao motor não é substituído, por isso encaminhe a tecla pelo
FocusNextFormFieldse quiser a ordem declarada
As consultas preservam a página atual, o focus nativo e qualquer texto de edição pendente no campo com focus, e não correm ações de ciclo de vida de documento nem de página. Chamar o FormFieldTabOrder a cada tecla é portanto seguro, e após a primeira chamada numa dada revisão de conteúdo é barato
A ordem de tabulação declarada, os diagnósticos de estado e a navegação de focus entre páginas são parte do PDFium Component for Delphi and C++Builder; a página do produto liga a referência completa da API, incluindo as entradas TPdfFormTabOrderMode e TPdfFormTabOrderStatus