Vanaf versie 3.223.0 ordent PDFium Component for Delphi de formulierveldnavigatie volgens het gedeclareerde /Tabs-beleid van elke pagina: R voor rijvolgorde, C voor kolomvolgorde, S voor structuurvolgorde, plus de PDF 2.0-waarden A en W. TPdf.FormFieldTabOrder geeft die volgorde terug en zijn statusoverload meldt of het gedeclareerde beleid is toegepast of dat een fallback werd gebruikt
De klacht die dit alles motiveert is altijd dezelfde. Een gebruiker vult een verzekeraarsformulier met twee kolommen, drukt Tab na het voornaamveld en de cursor springt naar de onderkant van de rechterkolom. Er is niets mis met de indexen. De producent schreef de rechterkolom simpelweg als eerste in /Annots weg en declareerde /Tabs /R op de pagina om de visuele volgorde te herstellen, dus een viewer die /Annots doorloopt en de declaratie negeert, landt op de verkeerde plek. Het artikel over widgetindex versus annotatie-index behandelt hoe widgetordinalen zich verhouden tot de /Annots-array. In PDFium Component wint het gedeclareerde beleid telkens wanneer het kan worden vastgesteld, en kan dat niet, dan vertelt de component je waarom
Wat declareert de /Tabs-entry eigenlijk?
/Tabs is een optionele naam in het paginaobject (ISO 32000-2 §7.7.3.3; Table 30 in ISO 32000-1) die een processor vertelt in welke volgorde hij de annotaties op die pagina moet bezoeken, en zijn semantiek staat uitgeschreven onder annotaties in §12.5.1. PDF 1.5 introduceerde drie waarden en PDF 2.0 voegde er twee toe:
/R, rijvolgorde: rijen van boven naar beneden en binnen elke rij de leesrichting van de viewer/C, kolomvolgorde: kolommen in de leesrichting en van boven naar beneden binnen elke kolom/S, structuurvolgorde: de volgorde waarin de logische structuurboom de annotaties refereert/A, annotatie-arrayvolgorde: de volgorde van/Annotszelf (PDF 2.0)/W, widgetvolgorde: eerst widgets in arrayvolgorde, daarna de andere annotaties (PDF 2.0)
Het detail waar handgeschreven implementaties over struikelen, is overerving. /Tabs hoort alleen bij de page dictionary. ISO 32000-2 §7.7.3.4 beperkt overerfbare pagina-attributen tot Resources, MediaBox, CropBox en Rotate, dus een /Tabs-key op een /Pages-knoop betekent niets, en een reader die de ouderketen oploopt om er een te vinden, verzint een beleid dat de auteur nooit heeft gezet. PDFium Component leest /Tabs van de pagina zelf en nergens anders
De gedeclareerde volgorde lezen met FormFieldTabOrder
TPdf.FormFieldTabOrder geeft de formuliervelden van de huidige pagina terug als 0-based widgetordinalen in de gedeclareerde tabvolgorde, en de overload met een parameter out Status: TPdfFormTabOrderStatus legt uit hoe die volgorde tot stand kwam. Hetzelfde paar bestaat op TPdfView, en de kale overload gebruikt het identieke beleid terwijl ze de status weggooit
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; // al de default; vereist voor velden
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;
Het statusrecord is wat een stille ordeningsbeslissing in iets verandert dat je kunt loggen. RequestedMode is wat de pagina declareerde, EffectiveMode is wat werd toegepast en UsedFallback is true wanneer het gedeclareerde beleid niet kon worden gevolgd en in plaats daarvan annotatie-arrayvolgorde terugkwam, met een korte reden in ErrorMessage. SnapshotAvailable zegt of de component de documentmetadata überhaupt kon lezen. Een ontbrekende /Tabs wordt gemeld als fallback naar annotatie-arrayvolgorde in plaats van als gedeclareerd beleid te worden uitgegeven, zodat een supportlog "de auteur vroeg deze volgorde" kan onderscheiden van "de pagina vroeg om niets"
Waarom kunnen rij- en kolomvolgorde niet gewoon de /Rect-waarden sorteren?
Rij- en kolomvolgorde moeten in display-geometrie worden berekend, niet in rauwe annotatiecoördinaten, want de /Rotate van een pagina en de leesrichting van het document veranderen allebei wat "rij" betekent. Sorteer de /Rect-arrays van een pagina met /Rotate 90 op hun y-coördinaat en je krijgt wat de viewer als kolommen toont. De component mapt daarom elke widgetrechthoek door de display-transformatie van de pagina, intrinsieke rotatie incluis, op 1/64-puntresolutie en groepeert vanaf daar
De leesrichting komt uit de ViewerPreferences-dictionary van de catalog (ISO 32000-2 §12.2). /Direction /R2L keert de volgorde binnen een rij om en keert de volgorde van kolommen om, wat een Arabisch of Hebreeuws formulier verwacht. Ontbreekt /Direction, dan geldt L2R. Bevat het iets anders dan L2R of R2L, dan vallen rij- en kolomvolgorde terug op annotatie-arrayvolgorde met de reden in de status, want een richting gokken is erger dan toegeven dat er geen is
Groepeerbeslissingen zijn waar processoren terecht van elkaar mogen verschillen, dus de exacte regel is het kennen waard. Bij rijvolgorde wordt de bovenste onbezochte widget het anker en elke widget waarvan het verticale middelpunt binnen de boven- en onderkant van het anker valt, sluit zich bij zijn rij aan. Kolomvolgorde spiegelt dit horizontaal. Gelijke standen worden deterministisch beslist op de widgetordinaal. Op een net raster matcht dit elke zinnige viewer. Op een layout met zwaar overlappende of verspringende velden kan een andere processor met een andere tolerantie anders groeperen, en geen enkel deel van ISO 32000 maakt een van beide antwoorden fout
Hoe bepaalt structuurvolgorde de volgorde?
Structuurvolgorde volgt de objectreferentie- (OBJR) dictionaries in de structuurboom, in de volgorde waarin een diepte-eerst-tocht door de /K-arrays ze werkelijk bereikt. Hij volgt niet de /ParentTree-keyvolgorde en hij volgt niet de willekeurige volgorde die een opsomming van StructTreeRoot toevallig oplevert. Die volgordes vallen vaak samen in bestanden van één producent en lopen uiteen nadat een editor de boom herstructureert, precies wanneer de leesvolgorde van een getagged formulier er het meest toe doet. Onderhoud je die boom zelf, dan behandelt de gids voor het synchroon houden van /ParentTree en /StructParents de omgekeerde mapping die consistent met haar moet blijven
Widgets die de structuurboom nooit refereert, laat de specificatie over aan de processor. PDFium Component plakt ze na de gestructureerde widgets in annotatie-arrayvolgorde en meldt hoeveel hij heeft toegevoegd in UnstructuredWidgetCount. Een pagina die /S declareert op een document zonder een structuurboom wordt behandeld als een lege traversering: elke widget landt in die staart, het aantal zegt het en het resultaat wordt niet als volledige fallback gemarkeerd. Een structuurboom met een cyclus, een OBJR die tweemaal wordt gerefereerd of een OBJR waarvan de /Pg in tegenspraak is met de pagina die de annotatie bezit, laat structuurvolgorde terugvallen. Die gebreken maken S onbruikbaar, maar ze verstoren R of C op andere pagina's niet
Waarom zijn tabvolgorde-waarden geen annotatie-indexen?
De waarden in de array die FormFieldTabOrder teruggeeft tellen alleen widgets, terwijl een OBJR een annotatieobject identificeert dat in de volledige /Annots-array leeft. De twee indexruimtes door elkaar halen is de enkele meest waarschijnlijke bug in elke code die structuurvolgorde verbruikt. Neem een pagina waarvan /Annots [Link, Widget A, Widget B] is. De OBJR voor Widget B wijst naar annotatie-index 2. Widget B is veldordinaal 1. Stop 2 in FocusFormField en je focust het verkeerde veld of helemaal niets, afhankelijk van hoeveel widgets de pagina heeft
De component voert die conversie intern uit, en daarom kan haar resultaat rechtstreeks in FocusFormField en FormFieldInfo. Leest je eigen code OBJR-targets of andere annotatie-indexen, converteer ze dan expliciet voordat je ze als velden behandelt:
// Zet een pagina-annotatie-index om naar de widgetordinaal zoals gebruikt door
// FocusFormField, FormFieldInfo en FormFieldTabOrder; -1 als het geen widget is
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, notes en popups zijn geen velden
Result := 0;
for I := 0 to AnnotIndex - 1 do
if Pdf.Annotation[I].Subtype = anWidget then
Inc(Result);
end;
Beschouw het resultaat alleen als geldig zolang de pagina geladen en onbewerkt blijft. Elke annotatie toevoegen of verwijderen vóór een widget schuift beide indexruimtes op, en een gecachte mapping van vóór de bewerking is precies het soort off-by-one dat zich alleen op klantbestanden laat reproduceren
Cross-page-navigatie met FocusNextFormField
FocusNextFormField(AcrossPages: Boolean; WrapDocument: Boolean = True; MaxPagesToScan: Integer = 256) loopt door de tabvolgorde van de huidige pagina, loopt daarna door in latere pagina's en past vanaf 3.223.0 op elke bezochte pagina zijn eigen gedeclareerde beleid toe. De cross-page-overload kwam in versie 3.220.0, dus het advies in het artikel over formulierveldnavigatie om de wrap zelf te detecteren en PageNumber te wijzigen geldt nu alleen voor de overload zonder argumenten, die nog steeds binnen één pagina rondcirkelt
procedure TFormViewer.HandleTabKey(Shift: TShiftState);
var
Field: Integer;
begin
if ssShift in Shift then
Field := PdfView.FocusPreviousFormField(True) // over pagina's heen, wikkelt rond
else
Field := PdfView.FocusNextFormField(True);
if Field < 0 then
Beep // niets geschikts binnen de scan; pagina en focus onveranderd
else
UpdateFieldStatus(PdfView.PageNumber, Field);
end;
De terugkeerwaarde is de paginalokale veldordinaal op welke pagina nu focus houdt, dus lees PageNumber erbij. Cross-page-traversering slaat widgets over waar een gebruiker niet zinvol naartoe kan tabben: verborgen, onzichtbare, NoView-, read-only-, handtekening-, onbekend-type- en lege-rechthoek-widgets. WrapDocument = False meegeven stopt bij de documentgrens met -1. MaxPagesToScan moet tussen 1 en 4096 liggen en elke geïnspecteerde pagina telt, inclusief de huidige. Vindt de scan niets of weigert de native engine de focus op het doel, dan geeft de aanroep -1 terug en laat de vorige pagina en focus precies zoals ze waren
Waar houdt de gedeclareerde volgorde op te gelden?
Het gedeclareerde beleid wordt alleen gevolgd wanneer de component de pagina-metadata betrouwbaar kan lezen, en elk geval waarin dat niet kan, valt terug op annotatie-array-widgetvolgorde met een expliciete reden. De grenzen zijn specifiek genoeg om op te sommen:
- Metadata komt uit een begrensde private serialisatie van het huidige native document, niet uit de originele bestandsbytes, want pagina-verplaatsingen en bewerkingen in het geheugen maken het bestand op schijf verouderd. De snapshot wordt per inhoudsrevisie gecached, dus gemelde bewerkingen maken haar ongeldig, en mislukte snapshots worden ook gecached
- Versleutelde documenten en snapshots die tijdens een page-object-bewerking worden aangevraagd, hebben geen snapshot en vallen altijd terug
- De snapshot is begrensd op 64 MiB, 65536 actieve objecten, 4096 pagina's, 4096 annotaties per pagina, in totaal 65536 annotaties en een structuurdiepte van 64; grotere documenten vallen terug in plaats van de navigatie vast te laten lopen
- Alleen AcroForm-documenten worden gedekt. XFA-formulieren blijven buiten dit profiel
- Rij- en kolomvolgorde hebben eindige, niet-lege widgetrechthoeken en zinnige pagina-afmetingen nodig; gedegenereerde geometrie triggert de fallback
- Deze API's sturen de navigatie vanuit jouw code. De eigen afhandeling door PDFium van een Tab-toets die naar de engine wordt doorgestuurd, wordt niet vervangen, dus routeer de toets via
FocusNextFormFieldals je de gedeclareerde volgorde wilt
Query's behouden de huidige pagina, native focus en eventuele wachtende bewerkingstekst in het veld met focus, en ze draaien geen document- of paginacyclusacties. FormFieldTabOrder bij elke toetsaanslag aanroepen is daardoor veilig en na de eerste aanroep op een gegeven inhoudsrevisie goedkoop
Gedeclareerde tabvolgorde, de statusdiagnoses en cross-page-focusnavigatie maken deel uit van de PDFium Component for Delphi and C++Builder; de productpagina linkt de volledige API-referentie, inclusief de TPdfFormTabOrderMode- en TPdfFormTabOrderStatus-entries