Teknisk artikel

PDFium Component: Tab-rækkefølge og /Tabs i Delphi

Fra version 3.223.0 sorterer PDFium Component til Delphi formularfelt-navigation efter hver sides deklarerede /Tabs-politik: R for rækkeorden, C for kolonneorden, S for strukturorden plus PDF 2.0-værdierne A og W. TPdf.FormFieldTabOrder returnerer den orden, og dens status-overload rapporterer, om den deklarerede politik blev anvendt, eller en fallback blev brugt

Klagen, der motiverer alt dette, er altid den samme. En bruger udfylder en tokolonnet forsikringsformular, trykker Tab efter fornavnsfeltet, og cursoren hopper til bunden af højre kolonne. Intet er i stykker i indeksene. Producenten skrev blot højre kolonne ind i /Annots først og har deklareret /Tabs /R på siden for at rette den visuelle orden, så en viewer, der går /Annots igennem og ignorerer deklarationen, lander det forkerte sted. Artiklen om widget-indeks versus annotation-indeks dækker, hvordan widget-ordinaler forholder sig til /Annots-arrayet. I PDFium Component vinder den deklarerede politik, når den kan fastlægges, og når den ikke kan, fortæller komponenten dig hvorfor

Hvad deklarerer /Tabs-posten faktisk?

/Tabs er et valgfrit navn i sideobjektet (ISO 32000-2 §7.7.3.3; Table 30 i ISO 32000-1), der fortæller en processor, i hvilken orden den skal besøge annotationerne på den side, og dens semantik er udfoldet under annotationer i §12.5.1. PDF 1.5 introducerede tre værdier, og PDF 2.0 tilføjede to mere:

  • /R, rækkeorden: rækker oppefra og ned, og inden for hver række viewerens læseretning
  • /C, kolonneorden: kolonner i læseretningen, og oppefra og ned inden for hver kolonne
  • /S, strukturorden: den orden, hvori det logiske strukturtræ refererer til annotationerne
  • /A, annotation-array-orden: ordenen af /Annots selv (PDF 2.0)
  • /W, widget-orden: widgets først i array-orden, derefter de andre annotationer (PDF 2.0)

Detaljen, der får håndskrevne implementationer til at snuble, er nedarvning. /Tabs hører kun til side-dictionary'en. ISO 32000-2 §7.7.3.4 begrænser nedarvbare sideattributter til Resources, MediaBox, CropBox og Rotate, så en /Tabs-nøgle på en /Pages-node er meningsløs, og en læser, der går op ad forælderkæden for at finde én, opfinder en politik, forfatteren aldrig satte. PDFium Component læser /Tabs fra siden selv og intet andet sted

At læse den deklarerede orden med FormFieldTabOrder

TPdf.FormFieldTabOrder returnerer den aktuelle sides formularfelter som 0-baserede widget-ordinaler i den deklarerede Tab-orden, og overloaden med en out Status: TPdfFormTabOrderStatus-parameter forklarer, hvordan den orden blev produceret. Samme par findes på TPdfView, og den rene overload bruger identisk politik, mens den kasserer statussen

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;          // allerede default; påkrævet for felter
    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;

Status-recorden er det, der gør en lydløs sorteringsbeslutning til noget, du kan logge. RequestedMode er det, siden deklarerer, EffectiveMode er det, der blev anvendt, og UsedFallback er true, når den deklarerede politik ikke kunne efterleves, og annotation-array-orden kom tilbage i stedet, med en kort grund i ErrorMessage. SnapshotAvailable siger, om komponenten overhovedet kunne læse dokumentmetadata. En manglende /Tabs rapporteres som en fallback til annotation-array-orden frem for at blive givet ud som en deklareret politik, så en supportlog kan skelne "forfatteren bad om denne orden" fra "siden bad om intet"

Sidens /Tabs-politikværdier R, C, S, A og W med de statusfelter, PDFium Component til Delphi rapporterer, når den deklarerede orden anvendes eller falder tilbage
en manglende /Tabs er en rapporteret fallback til annotation-array-orden, ikke en deklareret politik givet ud som én

Hvorfor kan række- og kolonneorden ikke bare sortere /Rect-værdierne?

Række- og kolonneorden skal beregnes i visningsgeometri, ikke rå annotation-koordinater, for en sides /Rotate og dokumentets læseretning ændrer begge, hvad "række" betyder. Sorterer du /Rect-arraysene på en side med /Rotate 90 efter deres y-koordinat, får du det, viewerne viser som kolonner. Komponenten mapper derfor hver widgetrektangel gennem sidens visningstransform, iboende rotation inkluderet, ved 1/64 point-opløsning og grupperer derfra

Læseretningen kommer fra katalogets ViewerPreferences-dictionary (ISO 32000-2 §12.2). /Direction /R2L vender ordenen inden for en række og vender kolonneordenen, hvilket er det, en arabisk eller hebraisk formular forventer. Mangler /Direction, gælder L2R. Indeholder den noget andet end L2R eller R2L, falder række- og kolonneorden tilbage til annotation-array-orden med grunden i statussen, for at gætte en retning er værre end at indrømme, der ikke er nogen

Gruppering er der, hvor processorer legitimt adskiller sig, så det er værd at kende den præcise regel. Til rækkeorden bliver den øverste ubesøgte widget ankeret, og hver widget, hvis lodrette center falder inden for ankerets top og bund, deltager i dens række. Kolonneorden spejler det vandret. Uafgjorte afgøres deterministisk efter widget-ordinalen. På et rent gitter matcher det enhver fornuftig viewer. På et layout med kraftigt overlappende eller forskudde felter kan en anden processor med en anden tolerance gruppere anderledes, og ingen del af ISO 32000 gør nogen af svarene forkerte

PDFium Component i Delphi: annotation-array Tab-orden hopper ned ad højre kolonne, mens den deklarererede /Tabs R-rækkeorden besøger den tokolonnedelte formular række for række
rækkeordenen beregnes i visningsgeometri efter /Rotate og læseretningen, ikke i rå /Annots-orden

Hvordan afgør strukturordenen sekvensen?

Strukturorden følger objektreference-dictionaries'ene (OBJR) i strukturtræet, i den orden, et depth-first gennemløb af /K-arraysene faktisk når dem. Den følger ikke /ParentTree-nøgleordenen, og den følger ikke den orden, en opnumring af StructTreeRoot tilfældigvis producerer. De ordener falder ofte sammen i filer fra én enkelt producent og divergerer, efter en editor har omrokeret træet, hvilket er præcis, når en tagget formulars læseorden betyder mest. Vedligeholder du træet selv, dækker guiden til at holde /ParentTree og /StructParents synkroniseret den omvendte mapping, der skal forblive konsistent med det

Widgets, strukturtræet aldrig refererer til, overlades til processoren af specifikationen. PDFium Component tilføjer dem efter de strukturerede widgets i annotation-array-orden og rapporterer, hvor mange den tilføjede i UnstructuredWidgetCount. En side, der deklarerer /S på et dokument uden noget strukturtræ overhovedet, behandles som en tom gennemløbning: hver widget lander i den hale, antallet siger det, og resultatet markeres ikke som en fuld fallback. Et strukturtræ med en cykel, et OBJR refereret to gange eller et OBJR, hvis /Pg strider mod siden, der ejer annotationen, får strukturorden til at falde tilbage. De defekter gør S ubrugelig, men de forstyrrer ikke R eller C på andre sider

Hvorfor er Tab-ordenværdier ikke annotation-indekser?

Værdierne i arrayet returneret af FormFieldTabOrder tæller kun widgets, mens et OBJR identificerer et annotation-objekt, der bor i det fulde /Annots-array. At blande de to indeksrum er den mest sandsynlige bug i al kode, der indtager strukturorden. Tag en side, hvis /Annots er [Link, Widget A, Widget B]. OBJR'et for Widget B peger på annotation-indeks 2. Widget B er felt-ordinal 1. Giver du 2 til FocusFormField, fokuserer du enten det forkerte felt eller ingenting, afhængigt af hvor mange widgets siden har

Strukturens OBJR peger på annotation-indeks 2, mens PDFium Components form-API'er i Delphi tæller widgets, så Widget B er felt-ordinal 1
annotation-indeks 2 er ikke felt-ordinal 1 — konvertér eksplicit, før du kalder FocusFormField

Komponenten udfører den konvertering internt, hvilket er hvorfor dens resultat kan gå direkte ind i FocusFormField og FormFieldInfo. Læser din egen kode OBJR-targets eller andre annotation-indekser, så konvertér dem eksplicit, før du behandler dem som felter:

// Konverterer et sideannotation-indeks til widget-ordinalen brugt af
// FocusFormField, FormFieldInfo og FormFieldTabOrder; -1 hvis ikke en 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, noter og popups er ikke felter
  Result := 0;
  for I := 0 to AnnotIndex - 1 do
    if Pdf.Annotation[I].Subtype = anWidget then
      Inc(Result);
end;

Behandl resultatet som gyldigt kun, mens siden forbliver loadet og uret. Tilføjes eller fjernes en annotation foran en widget, flyttes begge indeksrum, og en cachet mapping fra før redigeringen er præcis den slags off-by-one, der kun reproducerer sig på kundefiler

Cross-page navigation med FocusNextFormField

FocusNextFormField(AcrossPages: Boolean; WrapDocument: Boolean = True; MaxPagesToScan: Integer = 256) går den aktuelle sides Tab-orden igennem, fortsætter derefter ind i senere sider, og fra 3.223.0 anvender hver side, den besøger, sin egen deklarerede politik. Cross-page-overloaden kom i version 3.220.0, så rådet i artiklen om formularfelt-navigation om selv at opdage wrapningen og skifte PageNumber gælder nu kun for overloaden uden argumenter, som stadig cykler inden for én side

procedure TFormViewer.HandleTabKey(Shift: TShiftState);
var
  Field: Integer;
begin
  if ssShift in Shift then
    Field := PdfView.FocusPreviousFormField(True)  // på tværs af sider, wrappes
  else
    Field := PdfView.FocusNextFormField(True);
  if Field < 0 then
    Beep   // intet kvalificeret inden for scannet; side og fokus uændret
  else
    UpdateFieldStatus(PdfView.PageNumber, Field);
end;

Returværdien er det side-lokale felt-ordinal på den side, der nu holder fokus, så læs PageNumber ved siden af. Cross-page-gennemløbning springer widgets over, som en bruger ikke meningsfuldt kan tabbe til: skjulte, usynlige, NoView-, read-only-, signatur-, ukendt-type- og tom-rektangel-widgets. Giver du WrapDocument = False, stopper den ved dokumentgrænsen med -1. MaxPagesToScan skal ligge mellem 1 og 4096, og hver inspiceret side tæller, inklusiv den aktuelle. Finder scannet intet, eller nægter den native motor fokus på targetet, returnerer kaldet -1 og efterlader den forrige side og fokus præcis, som de var

Hvor holder den deklarerede orden op med at gælde?

Den deklarerede politik efterleves kun, når komponenten pålideligt kan læse sidemetadataene, og hvert tilfælde, hvor den ikke kan, falder tilbage til annotation-array widget-orden med en eksplicit grund. Grænserne er specifikke nok til at opliste:

  • Metadata kommer fra en afgrænset privat serialisering af det aktuelle native dokument, ikke de oprindelige filbyte, for in-memory sideflytninger og redigeringer gør filen på disken forældet. Snapshotet caches pr. indholdsrevision, så notificerede redigeringer ugyldiggør det, og fejlede snapshots caches også
  • Krypterede dokumenter og snapshots efterspurgt under en page-object-redigering har intet snapshot og falder altid tilbage
  • Snapshotet er cappet ved 64 MiB, 65.536 aktive objekter, 4.096 sider, 4.096 annotationer pr. side, 65.536 annotationer i alt og en strukturdybde på 64; større dokumenter falder tilbage i stedet for at stoppe navigationen
  • Kun AcroForm-dokumenter er dækket. XFA-formularer forbliver uden for denne profil
  • Række- og kolonneorden behøver endelige, ikke-tomme widgetrektangler og fornuftige sidedimensioner; degenereret geometri udløser fallbacken
  • Disse API'er driver navigation fra din kode. PDFiums egen håndtering af en Tab-tast videresendt til motoren erstattes ikke, så rout tasten gennem FocusNextFormField, hvis du vil have den deklarerede orden

Queries bevarer den aktuelle side, native fokus og ventende redigeringstekst i det fokuserede felt, og de kører ingen dokument- eller side-livscyklushandlinger. At kalde FormFieldTabOrder ved hvert tastetryk er derfor sikkert, og efter det første kald på en given indholdsrevision er det billigt

Deklareret Tab-orden, statusdiagnostikken og cross-page fokusnavigation er en del af PDFium Component til Delphi og C++Builder; productsiden linker den fulde API-reference, inklusiv TPdfFormTabOrderMode- og TPdfFormTabOrderStatus-posterne