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/Annotssjä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”
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
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
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
FocusNextFormFieldom 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