Στο PDFium Component, το VCL/LCL στοιχείο βασισμένο σε PDFium για Delphi, C++Builder, και Lazarus, ένας δείκτης πεδίου φόρμας δεν είναι δείκτης annotation. Μια σελίδα φέρει annotations Link, Text, και Ink δίπλα στα widgets της, οπότε η απαρίθμηση πεδίων πρέπει να φιλτράρει στο FPDFAnnot_GetSubtype και να εκθέτει έναν λογικό δείκτη με βάση το μηδέν, αντιστοιχισμένο πίσω σε μια πραγματική θέση annotation μόνο στην native κλήση
Το bug που εκθέτει αυτό είναι αλάνθαστο μόλις το έχεις δει. Ένας δοκιμαστής πατά Tab σε μια συμπληρωμένη φόρμα τιμολογίου και ο δρομέας εξαφανίζεται, γιατί η εστίαση πήγε σε έναν υπερσύνδεσμο στο υποσέλιδο. Ή χειρότερα, δεν συμβαίνει τίποτα καθόλου: ο κώδικάς σου καταγράφει το πεδίο 3 ως εστιασμένο, το panel UI ενημερώνεται, και το FORM_SetFocusedAnnot σιωπηρά επέστρεφε false όλη την ώρα. Και τα δύο συμπτώματα προέρχονται από το ίδιο σχεδιαστικό λάθος, και το ένα από αυτά έχει μια δεύτερη ριζική αιτία κρυμμένη από κάτω
Οι δύο χώροι δεικτών που σου δίνει το PDFium
Το PDFium εκθέτει δύο σχήματα αρίθμησης πάνω στην ίδια σελίδα, και συμπίπτουν μόνο σε έγγραφα που τυχαίνει να μην περιέχουν τίποτα άλλο εκτός από widgets φόρμας. Το πρώτο είναι ο δείκτης annotation: μια θέση στον πίνακα σελίδας /Annots, που είναι αυτό που μετρά το FPDFPage_GetAnnotCount και αυτό που παίρνει το FPDFPage_GetAnnot (ISO 32000-1 §12.5.2). Το δεύτερο είναι ο λογικός δείκτης πεδίου που ένα API σε επίπεδο εφαρμογής θα έπρεπε να προσφέρει, τρέχοντας από το μηδέν πάνω στα διαδραστικά πεδία που ένας χρήστης μπορεί πράγματι να φτάσει. Το ISO 32000-1 §12.5.6.19 ορίζει τα widget annotations ως την οπτική αναπαράσταση διαδραστικών πεδίων φόρμας, και το §12.7 ορίζει την ίδια τη φόρμα. Οτιδήποτε άλλο στη σελίδα είναι διαφορετικός υπότυπος με διαφορετική σημασιολογία: ένα annotation Link έχει προορισμό, ένα annotation Ink έχει λίστα πινελιάς, ένα annotation Text είναι κολλητικό σημείωμα. Κανένα από αυτά δεν ανήκει σε μέτρηση πεδίων, και κανένα δεν μπορεί να δεχτεί εστίαση φόρμας. Ωστόσο στον πίνακα /Annots κάθονται εναλλάξ με τα widgets με όποια σειρά τα έγραψε η εφαρμογή παραγωγής, που συχνά δεν είναι η σειρά που υποδεικνύει οτιδήποτε άλλο για το έγγραφο
Γιατί το Tab προσγειώνεται σε υπερσύνδεσμο αντί για το επόμενο πεδίο;
Γιατί η μέτρηση πεδίων ήταν στην πραγματικότητα μέτρηση annotations. Η αρχική υλοποίηση επέστρεφε απευθείας το FPDFPage_GetAnnotCount από το FormFieldCount, ενώ ο προσπελαστής πληροφοριών πεδίου, ο βοηθός σειράς tab, και ο βοηθός εστίασης αντιμετώπιζαν όλοι τον ίδιο ακέραιο ως θέση widget. Σε μια καθαρή σελίδα AcroForm με έξι widgets και τίποτα άλλο, το έξι ισούται με έξι και κάθε δοκιμή περνά. Πρόσθεσε έναν υπερσύνδεσμο στο υποσέλιδο και ένα σχόλιο κριτή στο περιθώριο, και η μέτρηση αναφέρει οκτώ πεδία, οι δείκτες 6 και 7 επιλύονται σε αντικείμενα εκτός φόρμας, και το Tab περπατά κατευθείαν μέσα τους
Η διόρθωση στην πλευρά απαρίθμησης είναι να μετράς υποτύπους αντί για annotations. Άνοιξε κάθε annotation, ρώτησε για τον υπότυπό του, κράτησε τα widgets, και κλείσε τη λαβή σε μπλοκ finally, γιατί το FPDFPage_GetAnnot επιστρέφει μια λαβή που κατέχεται και πρέπει να επιστραφεί μέσω του FPDFPage_CloseAnnot
function WidgetCountForPage(Page: FPDF_PAGE): Integer;
var
Count, I: Integer;
Annot: FPDF_ANNOTATION;
begin
Result := 0;
if Page = nil then
Exit;
Count := FPDFPage_GetAnnotCount(Page); // every annotation, not just fields
for I := 0 to Count - 1 do
begin
Annot := FPDFPage_GetAnnot(Page, I);
if Annot = nil then
Continue;
try
if FPDFAnnot_GetSubtype(Annot) = FPDF_ANNOT_WIDGET then
Inc(Result);
finally
FPDFPage_CloseAnnot(Annot);
end;
end;
end;
Πρόσεξε τι σκόπιμα δεν κάνει αυτό. Δεν ρωτά τίποτα το περιβάλλον form-fill, και δεν χρειάζεται λαβή φόρμας, γιατί ο υπότυπος ζει στο annotation dictionary και είναι αναγνώσιμος από μόνη τη σελίδα. Αυτό μετράει για τη σειρά: η μέτρηση είναι διαθέσιμη πριν αποφασίσεις αν το έγγραφο αξίζει καν ένα περιβάλλον form-fill, κάτι που το άρθρο για AcroForm JavaScript και host events καλύπτει ως απόφαση ασφάλειας παρά ως ζήτημα ευκολίας
Αντιστοίχιση του λογικού δείκτη πίσω στο native όριο
Ο κανόνας που εμποδίζει τους δύο χώρους από το να διαρρέουν ο ένας στον άλλο είναι απλός: ο λογικός δείκτης είναι ο μόνος αριθμός που διασχίζει το δημόσιο API σου, και μετατρέπεται σε δείκτη annotation στην τελευταία συνάρτηση πριν από την native κλήση. Ένας βοηθός αντιστοίχισης, που χρησιμοποιείται εξίσου από πληροφορίες πεδίου, εστίαση, θέτες σημαιών, και σειρά tab, είναι αυτό που κάνει αυτόν τον κανόνα εφαρμόσιμο
function AnnotationIndexForField(Page: FPDF_PAGE;
FieldIndex: Integer): Integer;
var
Count, I, Current: Integer;
Annot: FPDF_ANNOTATION;
begin
Result := -1;
if (Page = nil) or (FieldIndex < 0) then
Exit;
Count := FPDFPage_GetAnnotCount(Page);
Current := 0;
for I := 0 to Count - 1 do
begin
Annot := FPDFPage_GetAnnot(Page, I);
if Annot = nil then
Continue;
try
if FPDFAnnot_GetSubtype(Annot) = FPDF_ANNOT_WIDGET then
begin
if Current = FieldIndex then
Exit(I); // real /Annots position: native calls only
Inc(Current);
end;
finally
FPDFPage_CloseAnnot(Annot);
end;
end;
end;
Δύο ιδιότητες αυτού του βοηθού αξίζει να δηλωθούν καθαρά. Είναι μια γραμμική σάρωση, οπότε ένας αφελής βρόχος πάνω σε κάθε πεδίο κοστίζει τετραγωνικό αριθμό ανοιγμάτων annotation σε μια σελίδα με εκατοντάδες widgets· αν απαριθμείς ολόκληρη τη σελίδα, διάτρεξε τα annotations μία φορά και συλλέγε τις λαβές widget καθώς προχωράς αντί να καλείς τον αντιστοιχιστή ανά πεδίο. Και επιστρέφει -1 αντί να σηκώνει εξαίρεση, κάτι που αφήνει τον καλούντα να αποφασίσει αν ένας μπαγιάτικος δείκτης είναι σφάλμα προγραμματισμού που αξίζει μια εξαίρεση ή αγώνας δρόμου που αξίζει να αγνοηθεί, για παράδειγμα αφού μια επεξεργασία αφαίρεσε ένα annotation στο οποίο μια cached λίστα UI ακόμα αναφέρεται
Γιατί το FORM_SetFocusedAnnot αποτυγχάνει σε headless σελίδα;
Γιατί το PDFium αρνείται να εστιάσει ένα widget του οποίου η προβολή σελίδας ποτέ δεν σημαδεύτηκε ως έγκυρη. Το FORM_SetFocusedAnnot επιλύει το annotation σε προβολή σελίδας μέσα στο περιβάλλον form-fill, και αν αυτή η προβολή σελίδας δεν υπάρχει επιστρέφει false χωρίς κανένα διαγνωστικό. Η διόρθωση μόνο της αντιστοίχισης δεικτών επομένως διορθώνει το Tab που προσγειώνεται σε υπερσύνδεσμο αλλά αφήνει το δεύτερο σύμπτωμα ανέγγιχτο: η λογική εγγραφή εστίασής σου λέει πεδίο 3, το native εστιασμένο widget είναι ακόμα τίποτα, και κάθε προσπελαστής χτισμένος πάνω στη native εστίαση, εστιασμένο κείμενο, εστιασμένη τιμή, κατάσταση επιλογής choice, συνεχίζει να επιστρέφει κενό. Η προβολή σελίδας δημιουργείται από το FORM_OnAfterLoadPage και καταστρέφεται από το FORM_OnBeforeClosePage. Σε έναν viewer χτισμένο γύρω από ένα οπτικό control αυτές οι κλήσεις συμβαίνουν ως μέρος της εμφάνισης μιας σελίδας, γι' αυτό η αποτυχία τόσο συχνά μοιάζει με bug μόνο-headless: ο ίδιος κώδικας που δουλεύει στο demo GUI αποτυγχάνει στο εργαλείο batch. Ο κύκλος ζωής ανήκει στο αντικείμενο εγγράφου, όχι στον viewer, οπότε το PDFium Component τώρα εκδίδει και τις δύο κλήσεις όποτε μια σελίδα φορτώνεται ή αποφορτώνεται με παρούσα λαβή φόρμας. Η υπογραφή C παίρνει τη σελίδα πρώτη και τη λαβή φόρμας δεύτερη, κάτι που είναι εύκολο να αντιστραφεί όταν γράφεις το binding με το χέρι
procedure ReportFirstField(const FileName: string);
var
Pdf: TPdf;
Idx: Integer;
begin
Pdf := TPdf.Create(nil);
try
Pdf.FormFill := True; // form-fill environment, before Active
Pdf.FileName := FileName;
Pdf.Active := True;
Pdf.PageNumber := 1; // page load also runs FORM_OnAfterLoadPage
Idx := Pdf.FocusNextFormField; // logical index, 0-based over widgets
if Idx < 0 then
Exit; // page holds no widget annotations
Writeln(string(Pdf.FormFieldInfo[Idx].Name), ' = ',
string(Pdf.FocusedFormFieldValue)); // reads the native focused widget
finally
Pdf.Free; // page unload runs FORM_OnBeforeClosePage
end;
end;
Ο έλεγχος που αποδεικνύει τη διόρθωση είναι αυτός που συγκρίνει τις δύο πλευρές. Κάλεσε το FocusFormField με έναν λογικό δείκτη, μετά διάβασε μια τιμή μέσω ενός προσπελαστή που περνά από το native εστιασμένο widget αντί από τη δική σου εγγραφή, όπως το FocusedFormFieldValue ή το FocusedFormOptionSelected. Αν ο λογικός δείκτης κάνει σωστό round-trip αλλά ο native προσπελαστής επιστρέφει κενό, λείπει η προβολή σελίδας, όχι η αντιστοίχιση
Τι δεν υπόσχεται ο λογικός δείκτης πεδίου
Ένας δείκτης πεδίου με βάση το μηδέν είναι ευκολία, όχι σημασιολογική ταυτότητα, και τέσσερα όρια προκύπτουν από αυτό. Είναι ανά σελίδα, όχι ανά έγγραφο, οπότε ο δείκτης 0 στη σελίδα 2 είναι διαφορετικό widget από τον δείκτη 0 στη σελίδα 1 και η σύγκρισή τους δεν έχει νόημα. Είναι θεσιακός, οπότε η εισαγωγή ή διαγραφή ενός annotation ακυρώνει κάθε cached δείκτη πάνω από την αλλαγή· αντιμετώπισε έναν αποθηκευμένο δείκτη ως έγκυρο μόνο όσο η σελίδα παραμένει φορτωμένη και χωρίς επεξεργασία
Το τρίτο όριο είναι αυτό που εκπλήσσει τον κόσμο που ανασκοπεί μια λίστα πεδίων. Ο δείκτης απαριθμεί widgets, όχι πεδία. Μια ομάδα radio είναι ένα πεδίο με αρκετά widget παιδιά, οπότε μια ομάδα τριών κουμπιών συνεισφέρει τρεις διαδοχικούς δείκτες που όλοι αναφέρουν το ίδιο Name. Η εγγραφή TPdfFormFieldInfo φέρει GroupCount και GroupIndex ακριβώς για αυτή την περίπτωση, και ένα UI λίστας που τα αγνοεί δείχνει το ίδιο πεδίο τρεις φορές. Το τέταρτο όριο αφορά τη σειρά διάτρεξης: η σειρά tab που εκτίθεται εδώ είναι η σειρά απαρίθμησης widget, που ακολουθεί τον πίνακα /Annots, όχι την καταχώρηση σελίδας /Tabs (ISO 32000-1 §7.7.3.3) και όχι το δέντρο πεδίων AcroForm. Για τους περισσότερους παραγωγούς αυτά συμφωνούν· για μια φόρμα διατεταγμένη σε δύο στήλες από μια γεννήτρια που εξέπεμψε πρώτα τη δεξιά στήλη, δεν συμφωνούν, και το μονοπάτι πληκτρολογίου που περιγράφεται στο άρθρο πλοήγησης πεδίων φόρμας θα φανεί λάθος παρόλο που κάθε δείκτης είναι σωστός. Όταν ένα αρχείο πελάτη συμπεριφέρεται περίεργα, ξεφόρτωσε και τους δύο χώρους δεικτών δίπλα-δίπλα πριν θεωρητικολογήσεις: η προβολή annotation και η προβολή πεδίου της ίδιας σελίδας, τυπωμένες μαζί, συνήθως κάνουν την αιτία προφανή με μία ματιά
procedure DumpIndexSpaces(Pdf: TPdf);
var
I: Integer;
Info: TPdfFormFieldInfo;
begin
for I := 0 to Pdf.AnnotationCount - 1 do
Writeln('annot ', I, ': subtype ', Ord(Pdf.Annotation[I].Subtype));
for I := 0 to Pdf.FormFieldCount - 1 do
begin
Info := Pdf.FormFieldInfo[I];
Writeln('field ', I, ': ', string(Info.Name),
' widget ', Info.GroupIndex, ' of ', Info.GroupCount);
end;
end;
Μια μέτρηση annotation πολύ πάνω από τη μέτρηση πεδίων σημαίνει ότι η σελίδα αναμειγνύει υποτύπους, που είναι φυσιολογικό σε αναθεωρημένα έγγραφα και είναι ακριβώς η κατάσταση για την οποία υπάρχει η αντιστοίχιση· το άρθρο ροής εργασίας ανασκόπησης annotation κοιτάζει την ίδια σελίδα από την πλευρά της σήμανσης. Ίσες μετρήσεις σε κάθε αρχείο δοκιμής, από την άλλη, σημαίνουν ότι τα fixtures σου δεν μπορούν να ανιχνεύσουν καθόλου αυτή την κατηγορία bug, και η ειλικρινής απάντηση είναι να προσθέσεις ένα fixture φόρμας που φέρει έναν σύνδεσμο και ένα κολλητικό σημείωμα
Τα APIs απαρίθμησης πεδίων, εστίασης, και annotation που περιγράφονται εδώ διατίθενται με το PDFium Component για Delphi, C++Builder, και Lazarus, του οποίου η σελίδα προϊόντος φέρει την πλήρη αναφορά πεδίου φόρμας συμπεριλαμβανομένης της εγγραφής πληροφοριών πεδίου και των προσπελαστών εστίασης