Teknisk artikel

PDFium Component: formulär-tabbordning och /Tabs i Delphi

Från och med version 3.223.0 ordnar PDFium Component for Delphi formulärfältsnavigering efter varje sidas deklarerade /Tabs-policy: R för radordning, C för kolumnordning, S för strukturordning, plus PDF 2.0-värdena A och W. TPdf.FormFieldTabOrder returnerar den ordningen, och dess statusoverload rapporterar om den deklarerade policyn tillämpades eller ett återfall användes

Klagomålet som driver allt det här är alltid samma. En användare fyller ett tvåkolumns försäkringsformulär, trycker Tab efter förnamnsfältet, och cursorn hoppar till botten av högerkolumnen. Ingenting är trasigt i indexen. Producenten skrev bara högerkolumnen i /Annots först och deklarerade /Tabs /R på sidan för att fixa visuell ordning, så en viewer som går /Annots och ignorerar deklarationen landar på fel ställe. Artikeln om widget-index mot annotationsindex täcker hur widget-ordinal förhåller sig till /Annots-arrayen. I PDFium Component vinner den deklarerade policyn närhelst den kan fastställas, och när den inte kan berättar komponenten varför

Vad deklarerar /Tabs-posten egentligen?

/Tabs är ett valfritt namn i sideobjektet (ISO 32000-2 §7.7.3.3; Table 30 i ISO 32000-1) som talar om för en processor i vilken ordning den ska besöka annotationerna på den sidan, och dess semantik är utstavad under annotationer i §12.5.1. PDF 1.5 introducerade tre värden, och PDF 2.0 lade till två till:

  • /R, radordning: rader uppifrån och ner, och inom varje rad viewerens läsriktning
  • /C, kolumnordning: kolumner i läsriktningen, och uppifrån och ner inom varje kolumn
  • /S, strukturordning: ordningen i vilken det logiska strukturträdet refererar annotationerna
  • /A, annotationsarrayordning: ordningen av /Annots själv (PDF 2.0)
  • /W, widgetordning: widgets först i arrayordning, sedan de andra annotationerna (PDF 2.0)

Detaljen som snärjer handskrivna implementationer är arv. /Tabs tillhör bara siddictionaryn. ISO 32000-2 §7.7.3.4 begränsar ärvbara sidattribut till Resources, MediaBox, CropBox och Rotate, så en /Tabs-nyckel på en /Pages-nod är meningslös, och en läsare som vandrar upp föräldrakedjan för att hitta en hittar på en policy författaren aldrig satte. PDFium Component läser /Tabs från sidan själv och ingenstans annat

Läsa den deklarerade ordningen med FormFieldTabOrder

TPdf.FormFieldTabOrder returnerar den aktuella sidans formulärfält som 0-baserade widget-ordinal i den deklarerade tabbordningen, och overloaden med en out Status: TPdfFormTabOrderStatus-parameter förklarar hur den ordningen producerades. Samma par finns på TPdfView, och den enkla overloaden använder identisk policy medan den slänger statusen

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;          // already the default; required for fields
    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;

Statusrecorden är det som gör ett tyst ordningsbeslut till något du kan logga. RequestedMode är vad sidan deklarerade, EffectiveMode är vad som tillämpades, och UsedFallback är true när den deklarerade policyn inte kunde hedras och annotationsarrayordning kom tillbaka i stället, med en kort orsak i ErrorMessage. SnapshotAvailable säger om komponenten kunde läsa dokumentmetadata alls. En saknad /Tabs rapporteras som ett återfall till annotationsarrayordning i stället för att utges som en deklarerad policy, så en supportlogg kan skilja ”författaren bad om den här ordningen” från ”sidan bad om ingenting”

Sidans /Tabs-policyvärden R, C, S, A och W med statusfälten PDFium Component för Delphi rapporterar när den deklarerade ordningen tillämpas eller återfaller
en saknad /Tabs är ett rapporterat återfall till annotationsarrayordning, inte en deklarerad policy utgiven som en

Varför kan inte rad- och kolumnordning bara sortera /Rect-värdena?

Rad- och kolumnordning måste beräknas i visningsgeometri, inte råa annotationskoordinater, för en sidas /Rotate och dokumentets läsriktning ändrar båda vad ”rad” betyder. Sortera /Rect-arrayerna på en sida med /Rotate 90 efter deras y-koordinat och du får det viewer visar som kolumner. Komponenten mappar därför varje widgetrektangel genom sidans visningstransform, inneboende rotation inkluderad, vid 1/64-punktsupplösning, och grupperar därifrån

Läsriktningen kommer från katalogens ViewerPreferences-dictionary (ISO 32000-2 §12.2). /Direction /R2L vänder ordningen inom en rad och vänder kolumnordningen, vilket är vad ett arabiskt eller hebreiskt formulär förväntar. Saknas /Direction gäller L2R. Håller den något annat än L2R eller R2L faller rad- och kolumnordning tillbaka till annotationsarrayordning med orsaken i statusen, för att gissa en riktning är värre än att erkänna att det inte finns någon

Gruppering är där processorer legitimt skiljer sig, så det är värt att känna till exakt regel. För radordning blir den översta obesökta widgeten ankaret, och varje widget vars vertikala centrum faller inuti ankarets topp och botten ansluter till dess rad. Kolumnordning speglar detta horisontellt. Lika fall avgörs deterministiskt på widget-ordinalet. På ett rent rutnät matchar detta vilken förnuftig viewer som helst. På en layout med hårt överlappande eller förskjutna fält kan en annan processor med en annan tolerans gruppera annorlunda, och ingen del av ISO 32000 gör något av svaren fel

PDFium Component i Delphi: annotationsarray-tabbordning hoppar ner i högerkolumnen medan den deklarerade /Tabs R-radordningen besöker det tvåkolumnsformuläret rad för rad
radordning beräknas i visningsgeometri efter /Rotate och läsriktningen, inte i rå /Annots-ordning

Hur avgör strukturordning sekvensen?

Strukturordning följer objektreferens-dictionaryerna (OBJR) i strukturträdet, i den ordning en djupet-först-genomgång av /K-arrayerna faktiskt når dem. Den följer inte /ParentTree-nyckelordningen, och den följer inte vilken ordning en uppräkning av StructTreeRoot råkar producera. De ordningarna sammanfaller ofta i filer från en enskild producent och divergerar efter att en redigerare arrangerat om trädet, vilket är precis när ett taggat formulärs läsordning spelar störst roll. Underhåller du det trädet själv täcker guiden till att hålla /ParentTree och /StructParents synkade den omvända mappningen som måste stå i överensstämmelse med det

Widgets strukturträdet aldrig refererar lämnas till processorn av specifikationen. PDFium Component bifogar dem efter de strukturerade widgeterna i annotationsarrayordning och rapporterar hur många den bifogade i UnstructuredWidgetCount. En sida som deklarerar /S på ett dokument utan strukturträd alls behandlas som en tom traversering: varje widget landar i den svansen, antalet säger det, och resultatet flaggas inte som ett fullständigt återfall. Ett strukturträd med en cykel, en OBJR refererad två gånger, eller en OBJR vars /Pg motsäger sidan som äger annotationen får strukturordning att återfalla. De defekterna gör S oanvändbar, men de stör inte R eller C på andra sidor

Varför är tabbordningsvärden inte annotationsindex?

Värdena i arrayen som FormFieldTabOrder returnerar räknar bara widgets, medan en OBJR identifierar ett annotationsobjekt som bor i hela /Annots-arrayen. Att blanda de två indexrymderna är den enskilt mest sannolika buggen i kod som konsumerar strukturordning. Ta en sida vars /Annots är [Link, Widget A, Widget B]. OBJR:n för Widget B pekar på annotationsindex 2. Widget B är fältordinal 1. Mata 2 till FocusFormField och du fokuserar antingen fel fält eller ingenting alls, beroende på hur många widgets sidan har

Struktur-OBJR:n riktar sig mot annotationsindex 2 medan PDFium Component-formulär-API:er i Delphi räknar widgets, så Widget B är fältordinal 1
annotationsindex 2 är inte fältordinal 1 — konvertera explicit innan du anropar FocusFormField

Komponenten gör den konverteringen internt, vilket är varför dess resultat kan gå rakt in i FocusFormField och FormFieldInfo. Läser din egen kod OBJR-mål eller andra annotationsindex, konvertera dem explicit innan du behandlar dem som fält:

// Konverterar ett sidannotationsindex till widget-ordinalet som används av
// FocusFormField, FormFieldInfo och FormFieldTabOrder; -1 om inte 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;                          // länkar, notiser och popups är inte fält
  Result := 0;
  for I := 0 to AnnotIndex - 1 do
    if Pdf.Annotation[I].Subtype = anWidget then
      Inc(Result);
end;

Behandla resultatet som giltigt bara medan sidan förblir laddad och oredigerad. Att lägga till eller ta bort vilken annotation som helst framför en widget skiftar båda indexrymderna, och en cachad mappning från före ändringen är precis den sortens off-by-one som reproduceras bara på kundfiler

Över-sidor-navigering med FocusNextFormField

FocusNextFormField(AcrossPages: Boolean; WrapDocument: Boolean = True; MaxPagesToScan: Integer = 256) går den aktuella sidans tabbordning, fortsätter sedan in i senare sidor, och från 3.223.0 tillämpar varje sida den besöker sin egen deklarerade policy. Över-sidor-overloaden kom i version 3.220.0, så rådet i formulärfältsnavigeringsartikeln om att detektera wrappen och ändra PageNumber själv gäller nu bara den argumentlösa overloaden, som fortfarande cyklar inom en sida

procedure TFormViewer.HandleTabKey(Shift: TShiftState);
var
  Field: Integer;
begin
  if ssShift in Shift then
    Field := PdfView.FocusPreviousFormField(True)  // över sidor, wrappar
  else
    Field := PdfView.FocusNextFormField(True);
  if Field < 0 then
    Beep   // inget kvalificerat inom skanningen; sida och fokus oförändrade
  else
    UpdateFieldStatus(PdfView.PageNumber, Field);
end;

Returvärdet är det sidlokala fältordinalet på vilken sida som nu håller fokus, så läs PageNumber bredvid det. Traversering över sidor hoppar över widgets en användare inte meningsfullt kan tabba in i: gömda, osynliga, NoView-, read-only-, signatur-, okänd-typ- och tom-rektangel-widgets. Att skicka WrapDocument = False stannar vid dokumentgränsen med -1. MaxPagesToScan måste ligga mellan 1 och 4096, och varje inspekterad sida räknas, inklusive den aktuella. När skanningen inte hittar något eller den nativa motorn vägrar fokus på målet returnerar anropet -1 och lämnar föregående sida och fokus exakt som de var

Var slutar den deklarerade ordningen gälla?

Den deklarerade policyn hedras bara när komponenten kan läsa sidmetadata pålitligt, och vart och ett fall där den inte kan återfaller till annotationsarray-widgetordning med en explicit orsak. Gränserna är specifika nog att lista:

  • Metadata kommer från en avgränsad privat serialisering av det aktuella nativa dokumentet, inte originalfilens byte, för sidflyttar och ändringar i minnet gör filen på disken inaktuell. Snapshoten cachas per innehållsrevision, så anmälda ändringar ogiltigförklarar den, och fallerade snapshots cachas också
  • Krypterade dokument, och snapshots begärda under en page-object-ändring, har ingen snapshot och återfaller alltid
  • Snapshoten takas vid 64 MiB, 65536 aktiva objekt, 4096 sidor, 4096 annotationer per sida, 65536 annotationer totalt och en strukturdjup på 64; större dokument återfaller i stället för att ställa navigeringen
  • Bara AcroForm-dokument täcks. XFA-formulär stannar utanför den profilen
  • Rad- och kolumnordning behöver ändliga, icke-tomma widgetrektanglar och vettiga siddimensioner; degenererad geometri utlöser återfallet
  • Dessa API:er driver navigering från din kod. PDFiums egen hantering av en Tab-tangent vidarebefordrad till motorn ersätts inte, så routa tangenten genom FocusNextFormField om du vill ha den deklarerade ordningen

Frågor bevarar den aktuella sidan, nativt fokus och eventuell väntande redigeringstext i det fokuserade fältet, och de kör inga dokument- eller sidlivscykelåtgärder. Att anropa FormFieldTabOrder vid varje tangenttryckning är därför säkert, och efter det första anropet på en given innehållsrevision är det billigt

Deklarerad tabbordning, statusdiagnostiken och över-sidor-fokusnavigering ingår i PDFium Component for Delphi and C++Builder; produktsidan länkar hela API-referensen, inklusive TPdfFormTabOrderMode- och TPdfFormTabOrderStatus-posterna