Dalla versione 3.223.0, PDFium Component for Delphi ordina la navigazione dei campi form secondo la policy /Tabs dichiarata da ogni pagina: R per l'ordine di riga, C per quello di colonna, S per l'ordine di struttura, più i valori A e W del PDF 2.0. TPdf.FormFieldTabOrder restituisce quell'ordine, e il suo overload con stato riporta se la policy dichiarata è stata applicata o se è stato usato un fallback
Il lamento che motiva tutto questo è sempre lo stesso. Un utente riempie una form assicurativa a due colonne, preme Tab dopo il campo del nome, e il cursore salta in fondo alla colonna di destra. Nulla è rotto negli indici. Il produttore ha semplicemente scritto la colonna di destra in /Annots per prima e ha dichiarato /Tabs /R sulla pagina per sistemare l'ordine visivo, così un viewer che percorre /Annots ignorando la dichiarazione atterra nel posto sbagliato. L'articolo su indice widget contro indice annotation copre come gli ordinali dei widget si rapportano all'array /Annots. In PDFium Component la policy dichiarata vince quando può essere stabilita, e quando non può, il componente dice perché
Cosa dichiara davvero la voce /Tabs?
/Tabs è un nome opzionale nell'oggetto pagina (ISO 32000-2 §7.7.3.3; Table 30 in ISO 32000-1) che dice a un processore in quale ordine visitare le annotation di quella pagina, e la sua semantica è enunciata sotto annotations in §12.5.1. Il PDF 1.5 introdusse tre valori, e il PDF 2.0 ne aggiunse altri due:
/R, ordine di riga: righe dall'alto in basso, e dentro ogni riga la direzione di lettura del viewer/C, ordine di colonna: colonne nella direzione di lettura, e dall'alto in basso dentro ogni colonna/S, ordine di struttura: l'ordine in cui l'albero di struttura logica riferisce le annotation/A, ordine dell'array di annotation: l'ordine di/Annotsstessa (PDF 2.0)/W, ordine dei widget: prima i widget in ordine di array, poi le altre annotation (PDF 2.0)
Il dettaglio che fa inciampare le implementazioni scritte a mano è l'ereditarietà. /Tabs appartiene solo al dizionario pagina. ISO 32000-2 §7.7.3.4 limita gli attributi pagina ereditabili a Resources, MediaBox, CropBox e Rotate, quindi una chiave /Tabs appoggiata su un nodo /Pages è priva di senso, e un reader che risale la catena dei padri per trovarne una sta inventando una policy che l'autore non ha mai impostato. PDFium Component legge /Tabs dalla pagina stessa e da nessun altro posto
Leggere l'ordine dichiarato con FormFieldTabOrder
TPdf.FormFieldTabOrder restituisce i campi form della pagina corrente come ordinali di widget a base 0 nell'ordine di tab dichiarato, e l'overload con un parametro out Status: TPdfFormTabOrderStatus spiega come quell'ordine è stato prodotto. La stessa coppia esiste su TPdfView, e l'overload semplice usa la policy identica buttando via lo stato
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; // già il default; richiesto per i campi
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;
Il record di stato è ciò che trasforma una decisione di ordinamento silenziosa in qualcosa che potete registrare nel log. RequestedMode è ciò che la pagina ha dichiarato, EffectiveMode è ciò che è stato applicato, e UsedFallback è true quando la policy dichiarata non poteva essere onorata ed è tornato invece l'ordine dell'array di annotation, con un motivo breve in ErrorMessage. SnapshotAvailable dice se il componente è riuscito a leggere i metadati del documento. Un /Tabs mancante viene riportato come fallback all'ordine dell'array di annotation invece di essere spacciato per una policy dichiarata, così un log di supporto può distinguere "l'autore ha chiesto quest'ordine" da "la pagina non ha chiesto niente"
Perché l'ordine di riga e colonna non può semplicemente ordinare i valori /Rect?
L'ordine di riga e colonna va calcolato nella geometria di visualizzazione, non nelle coordinate grezze delle annotation, perché il /Rotate di una pagina e la direzione di lettura del documento cambiano entrambi ciò che "riga" significa. Ordinate gli array /Rect di una pagina con /Rotate 90 per la loro coordinata y e ottenete ciò che il viewer mostra come colonne. Il componente quindi mappa ogni rettangolo widget attraverso la trasformazione di visualizzazione della pagina, rotazione intrinseca inclusa, a risoluzione di 1/64 di punto, e raggruppa da lì
La direzione di lettura viene dal dizionario ViewerPreferences del catalogo (ISO 32000-2 §12.2). /Direction /R2L ribalta l'ordine dentro una riga e ribalta l'ordine delle colonne, che è ciò che una form araba o ebraica si aspetta. Quando /Direction manca, si applica L2R. Quando contiene qualunque altra cosa oltre a L2R o R2L, l'ordine di riga e colonna ripiega sull'ordine dell'array di annotation con il motivo nello stato, perché indovinare una direzione è peggio che ammettere che non c'è
Il raggruppamento è dove i processori differiscono legittimamente, quindi vale la pena conoscere la regola esatta. Per l'ordine di riga, il widget non visitato più in alto diventa l'ancora, e ogni widget il cui centro verticale cade dentro top e bottom dell'ancora entra nella sua riga. L'ordine di colonna rispecchia questo in orizzontale. I pareggi si risolvono in modo deterministico sull'ordinale del widget. Su una griglia pulita questo combacia con qualunque viewer sensato. Su un layout con campi molto sovrapposti o sfalsati, un altro processore con una tolleranza diversa può raggruppare diversamente, e nessuna parte di ISO 32000 rende sbagliata una delle due risposte
Come decide la sequenza l'ordine di struttura?
L'ordine di struttura segue i dizionari di riferimento a oggetto (OBJR) nell'albero di struttura, nell'ordine in cui una passeggiata depth-first degli array /K li raggiunge davvero. Non segue l'ordine delle chiavi del /ParentTree, e non segue l'ordine che capita a produrre a un'enumerazione di StructTreeRoot. Quegli ordini spesso coincidono nei file di un solo produttore e divergono dopo che un editor riarrangia l'albero, che è esattamente quando l'ordine di lettura di una form tagged conta di più. Se mantenete quell'albero voi, la guida a tenere /ParentTree e /StructParents sincronizzati copre la mappatura inversa che deve restare coerente con esso
I widget che l'albero di struttura non riferisce mai sono lasciati al processore dalla specifica. PDFium Component li accoda dopo i widget strutturati in ordine di array di annotation e riporta quanti ne ha accodati in UnstructuredWidgetCount. Una pagina che dichiara /S su un documento privo del tutto di albero di struttura viene trattata come una traversata vuota: ogni widget atterra in quella coda, il conteggio lo dice, e il risultato non viene segnato come fallback completo. Un albero di struttura con un ciclo, un OBJR riferito due volte, o un OBJR il cui /Pg contraddice la pagina che possiede l'annotation fa ripiegare l'ordine di struttura. Quei difetti rendono S inutilizzabile, ma non disturbano R o C sulle altre pagine
Perché i valori dell'ordine di tab non sono indici annotation?
I valori nell'array restituito da FormFieldTabOrder contano solo widget, mentre un OBJR identifica un oggetto annotation che vive nell'intero array /Annots. Mescolare i due spazi di indici è il bug singolarmente più probabile in qualunque codice che consumi l'ordine di struttura. Prendete una pagina il cui /Annots è [Link, Widget A, Widget B]. L'OBJR per Widget B punta all'indice annotation 2. Widget B è l'ordinale campo 1. Dare 2 in pasto a FocusFormField focalizza o il campo sbagliato o niente affatto, a seconda di quanti widget ha la pagina
Il componente esegue quella conversione internamente, ecco perché il suo risultato può andare dritto in FocusFormField e FormFieldInfo. Se il vostro codice legge obiettivi OBJR o altri indici annotation, convertiteli esplicitamente prima di trattarli come campi:
// Converte un indice annotation di pagina nell'ordinale widget usato da
// FocusFormField, FormFieldInfo e FormFieldTabOrder; -1 se non è un 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; // link, note e popup non sono campi
Result := 0;
for I := 0 to AnnotIndex - 1 do
if Pdf.Annotation[I].Subtype = anWidget then
Inc(Result);
end;
Trattate il risultato come valido solo finché la pagina resta caricata e non modificata. Aggiungere o togliere qualunque annotation davanti a un widget sposta entrambi gli spazi di indici, e una mappatura in cache da prima della modifica è esattamente il tipo di off-by-one che si riproduce solo sui file dei clienti
Navigazione tra pagine con FocusNextFormField
FocusNextFormField(AcrossPages: Boolean; WrapDocument: Boolean = True; MaxPagesToScan: Integer = 256) percorre l'ordine di tab della pagina corrente, poi prosegue nelle pagine successive, e dal 3.223.0 ogni pagina che visita applica la sua policy dichiarata. L'overload tra pagine è arrivato nella versione 3.220.0, quindi il consiglio dell'articolo sulla navigazione dei campi form di rilevare il wrap e cambiare PageNumber voi stessi ora vale solo per l'overload senza argomenti, che continua a ciclare dentro una pagina
procedure TFormViewer.HandleTabKey(Shift: TShiftState);
var
Field: Integer;
begin
if ssShift in Shift then
Field := PdfView.FocusPreviousFormField(True) // tra pagine, con wrap
else
Field := PdfView.FocusNextFormField(True);
if Field < 0 then
Beep // niente di idoneo dentro la scansione; pagina e focus invariati
else
UpdateFieldStatus(PdfView.PageNumber, Field);
end;
Il valore di ritorno è l'ordinale campo locale alla pagina su qualunque pagina tenga ora il focus, quindi leggete PageNumber accanto. La traversata tra pagine salta i widget in cui un utente non può tabbare in modo sensato: widget hidden, invisible, NoView, read-only, firma, di tipo sconosciuto e a rettangolo vuoto. Passare WrapDocument = False si ferma al confine del documento con -1. MaxPagesToScan deve stare tra 1 e 4096, e ogni pagina ispezionata conta, inclusa la corrente. Quando la scansione non trova niente o il motore nativo rifiuta il focus sull'obiettivo, la chiamata restituisce -1 e lascia la pagina e il focus precedenti esattamente com'erano
Dove smette di valere l'ordine dichiarato?
La policy dichiarata viene onorata solo quando il componente può leggere i metadati della pagina in modo affidabile, e ogni caso in cui non può ripiega sull'ordine dei widget dell'array di annotation con un motivo esplicito. I confini sono abbastanza specifici da elencarli:
- i metadati arrivano da una serializzazione privata delimitata del documento nativo corrente, non dai byte del file originale, perché spostamenti di pagina ed edit in memoria rendono stantio il file su disco. L'istantanea viene messa in cache per revisione di contenuto, quindi gli edit notificati la invalidano, e anche le istantanee fallite vengono messe in cache
- i documenti cifrati, e le istantanee richieste durante un edit di page-object, non hanno istantanea e ripiegano sempre
- l'istantanea è tappata a 64 MiB, 65536 oggetti attivi, 4096 pagine, 4096 annotation per pagina, 65536 annotation in tutto e una profondità di struttura di 64; i documenti più grandi ripiegano invece di bloccare la navigazione
- solo i documenti AcroForm sono coperti. Le form XFA restano fuori da questo profilo
- l'ordine di riga e colonna vuole rettangoli widget finiti e non vuoti e dimensioni pagina sensate; una geometria degenerata innesca il fallback
- queste API guidano la navigazione dal vostro codice. La gestione propria di PDFium di un tasto Tab inoltrato al motore non viene rimpiazzata, quindi instradate il tasto attraverso
FocusNextFormFieldse volete l'ordine dichiarato
Le interrogazioni conservano la pagina corrente, il focus nativo e qualunque testo di edit in sospeso nel campo focalizzato, e non eseguono azioni di ciclo di vita su documento o pagina. Chiamare FormFieldTabOrder a ogni tasto è quindi sicuro, e dopo la prima chiamata su una data revisione di contenuto costa poco
L'ordine di tab dichiarato, i diagnostici di stato e la navigazione del focus tra pagine fanno parte del PDFium Component for Delphi and C++Builder; la pagina del prodotto collega il riferimento API completo, incluse le voci TPdfFormTabOrderMode e TPdfFormTabOrderStatus