Τεχνικό Άρθρο

Πλοήγηση σε πεδία φόρμας PDF στο Delphi (PDFium Component)

Πατήστε Tab σε μια φόρμα PDF που έχτισε ο κώδικάς σας, και ο δρομέας προσγειώνεται δύο πεδία μακριά από εκεί που θα έπρεπε, ή παραλείπει εντελώς τη δεύτερη στήλη, ή πηδά πίσω στην κορυφή μετά το τρίτο πεδίο αντί για το τέταρτο. Το άτομο που συμπληρώνει ένα τιμολόγιο στον viewer σας περιμένει το πληκτρολόγιο να διατρέχει τη φόρμα με τον ίδιο τρόπο που διατρέχει κάθε φόρμα ιστού που έχει χρησιμοποιήσει ποτέ. Όταν δεν το κάνει, απλώνει το χέρι στο ποντίκι, ψάχνει για το επόμενο κουτί, και αθόρυβα αποφασίζει ότι το εργαλείο σας είναι ημιτελές. Η προβλέψιμη διάτρεξη πεδίων είναι η διαφορά ανάμεσα σε έναν viewer καταχώρισης δεδομένων που ο κόσμος ανέχεται και έναν που εμπιστεύεται, και είναι σχεδόν εξ ολοκλήρου ζήτημα χρήσης του σωστού API εστίασης αντί να προσποιείστε πληκτρολόγηση με προσομοιωμένα κλικ

Τα παραδείγματα παρακάτω χρησιμοποιούν το PDFium Component, ένα component VCL/LCL βασισμένο σε PDFium για Delphi, C++Builder και Lazarus. Η πλοήγηση είναι ένα από τα τρία πράγματα που πρέπει να κάνει σωστά ένας viewer φορμών· τα άλλα δύο, το σωστό άνοιγμα της φόρμας και η αποθήκευση συμπληρωμένων τιμών ώστε να εμφανίζονται πραγματικά, είναι εκεί που κρύβονται οι περισσότερες εκπλήξεις, οπότε καλύπτονται και τα τρία παρακάτω

Άνοιγμα μιας φόρμας: FormFill, FormType και το ζήτημα XFA

Η πρόσβαση σε πεδία απαιτεί το υποσύστημα form-fill, που ελέγχεται από την ιδιότητα FormFill, να είναι ενεργοποιημένο πριν ανοίξει το έγγραφο. Μόλις ενεργοποιηθεί, η FormType σας λέει με τι είδος φόρμας έχετε να κάνετε, και η απάντηση αλλάζει το σύνολο χαρακτηριστικών που μπορείτε να υποσχεθείτε:

Διάγραμμα της ρύθμισης FormFill και των κλάδων ανίχνευσης FormType σε viewer Delphi PDFium Component, χωρίζοντας χειρισμό ftNone, ftAcroForm και ftXfaFull
Η FormType διακλαδώνεται μόλις ενεργοποιηθεί το FormFill, και κάθε κλάδος υπόσχεται διαφορετικό σύνολο χαρακτηριστικών
Pdf.FileName := FormPath;
Pdf.FormFill := True;   // ενεργοποιήστε πριν το Active· απαιτείται για κάθε πρόσβαση πεδίου
Pdf.Active := True;

case Pdf.FormType of
  ftNone:
    DisableFormPanel('This document has no interactive form');
  ftAcroForm:
    BuildFieldList;     // διαθέσιμη πλήρης πλοήγηση και επεξεργασία πεδίων
  ftXfaFull:
    ShowXfaNotice;      // το XFA αποδίδεται από το δικό του πρότυπο XML·
                        // αντιμετωπίστε την επεξεργασία πεδίων ως περιορισμένη
end;

Δύο πρακτικές παρατηρήσεις προκύπτουν από αυτόν τον διακόπτη. Το AcroForm είναι το τυπικό μοντέλο φόρμας ISO 32000, και είναι αυτό που στοχεύει κάθε API εδώ. Τα έγγραφα XFA ενσωματώνουν τη δική τους αρχιτεκτονική φόρμας XML, οπότε το να υποσχεθείτε σε έναν πελάτη πλήρη επεξεργασία XFA μετά από ένα γρήγορο demo AcroForm είναι μια δέσμευση που θα μετανιώσετε. Η δεύτερη παρατήρηση αφορά παρενέργειες: ο ορισμός της FormFill σε True αρχικοποιεί επίσης το JavaScript του εγγράφου. Σε έναν viewer καταχώρισης δεδομένων αυτό είναι ακριβώς σωστό, επειδή τα scripts υπολογισμού είναι αυτά που κρατούν ένα τρέχον σύνολο ενημερωμένο καθώς κάποιος πληκτρολογεί. Σε ένα παράθυρο προεπισκόπησης για αρχεία άγνωστης προέλευσης είναι ακριβώς λάθος. Το άρθρο για ασφαλή προεπισκόπηση PDF καλύπτει την πλευρά FormFill := False αυτού του συμβιβασμού

Διάτρεξη με το πλήκτρο Tab που προσγειώνεται εκεί που περιμένουν οι χρήστες

Πίσω στο πρόβλημα πληκτρολογίου από την αρχή. Ο πειρασμός είναι να προσποιηθείτε το Tab συνθέτοντας ένα κλικ ποντικιού πάνω στο ορθογώνιο του επόμενου widget, κάτι που σπάει τη στιγμή που ένα πεδίο κυλήσει εκτός οθόνης ή δύο widgets επικαλύπτονται. Το API εστίασης μετακινεί απευθείας την ίδια την εστίαση της φόρμας αντ' αυτού, χωρίς μαντεψιά γεωμετρίας. Πέντε κλήσεις το καλύπτουν: η FocusFormField ανά δείκτη, οι FocusNextFormField και FocusPreviousFormField για βηματισμό, η FocusedFormFieldIndex για να διαβάσετε πού βρίσκεστε, και η ClearFormFieldFocus για να αφαιρέσετε εντελώς την εστίαση

Διάγραμμα διάσχισης εστίασης με πλήκτρο Tab σε viewer Delphi PDFium Component όπου το FocusNextFormField τυλίγει μέσα στη σειρά tab μιας σελίδας και πέντε API εστίασης καλύπτουν πλοήγηση πληκτρολογίου
Η διέλευση γίνεται βρόχος μέσα στη σειρά tab μίας σελίδας, οπότε το πέρασμα στην επόμενη σελίδα μένει δουλειά του προβολέα
procedure TFormViewer.HandleTabKey(Shift: TShiftState);
begin
  if ssShift in Shift then
    PdfView.FocusPreviousFormField
  else
    PdfView.FocusNextFormField;
  UpdateFieldStatus;  // π.χ. «Field 4 of 17: InvoiceDate»
end;

Το ένα κομμάτι συμπεριφοράς που μπερδεύει τον κόσμο είναι η αναδίπλωση. Η διάτρεξη δουλεύει μέσα από τη σειρά tab της τρέχουσας σελίδας και επαναλαμβάνεται μέσα σε αυτήν: περάστε πέρα από το τελευταίο πεδίο και επιστρέφετε στο πρώτο. Και οι δύο συναρτήσεις βηματισμού επιστρέφουν τον νέο δείκτη πεδίου, ή -1 όταν η σελίδα δεν κρατά καθόλου πεδία. Αυτή η επανάληψη είναι ανά σελίδα, όχι ανά έγγραφο, κάτι που σημαίνει ότι η μετάβαση στην επόμενη σελίδα είναι δική σας δουλειά, όχι της βιβλιοθήκης. Συγκρίνετε τον επιστρεφόμενο δείκτη με αυτόν από τον οποίο ξεκινήσατε, προσέξτε πότε έχει αναδιπλωθεί, και προχωρήστε την PageNumber εσείς οι ίδιοι αν η φόρμα προορίζεται να διαβάζεται ως μία συνεχής ακολουθία. Παραλείψτε αυτόν τον έλεγχο και μια φόρμα δύο σελίδων αθόρυβα παγιδεύει τον δρομέα στη σελίδα ένα, κάτι που είναι η δική του παραλλαγή του παραπόνου για το σπασμένο Tab

Η διάτρεξη γίνεται χρήσιμη μόλις το υπόλοιπο UI αντιδράσει σε αυτήν. Το συμβάν OnFormFieldEnter πυροδοτείται καθώς φτάνει η εστίαση, και στον viewer η OnFormFieldFocusChange αναφέρει τον νέο δείκτη πεδίου, ώστε ένα πλαϊνό πάνελ να μπορεί να μένει συγχρονισμένο με ό,τι μόλις επέλεξε το πληκτρολόγιο. Όταν χρειάζεστε την αντίστροφη αντιστοίχιση, από μια θέση οθόνης σε ένα πεδίο, η ευρετηριασμένη ιδιότητα FormFieldAt κάνει το hit-testing για προεπισκοπήσεις tooltip και πάνελ click-to-edit. Υπάρχει ένα ήσυχο όφελος προσβασιμότητας σε όλο αυτό: επειδή η εστίαση ακολουθεί τη δική του σειρά πεδίων του εγγράφου, η διαδρομή που συνδέετε για το πλήκτρο Tab είναι η ίδια διαδρομή που ανακοινώνει ένας screen reader, χωρίς επιπλέον δουλειά

Η εμφάνιση ονομάτων πεδίων αντί ακατέργαστων αριθμών δεικτών χρειάζεται ακόμα μία ιδιότητα. Η FormFieldInfo[] επιστρέφει μια εγγραφή TPdfFormFieldInfo ανά δείκτη, φέρνοντας το όνομα πεδίου, τον τύπο, το μέγεθος γραμματοσειράς, την κατάσταση επιλογής, την τιμή εξαγωγής, και τη συμμετοχή σε ομάδα, που είναι αυτό που θα έπρεπε να εμφανίζει μια λίστα πλοήγησης («Field 4 of 17: InvoiceDate» αντί για «4»). Οι ομάδες radio είναι η περίπτωση που αξίζει ένα αφιερωμένο αρχείο δοκιμής. Αρκετά widgets μπορούν να μοιράζονται ένα μοναδικό όνομα πεδίου, οπότε μια λίστα συναρμολογημένη αφελώς από widgets δείχνει την ίδια ομάδα αρκετές φορές και μπερδεύει όποιον τη διαβάζει

Γιατί οι συμπληρωμένες τιμές βγαίνουν κενές, και η κλήση που το διορθώνει

Το άλλο παράπονο που γεμίζει τις ουρές υποστήριξης είναι πιο ανησυχητικό από ένα Tab που δεν συμπεριφέρεται σωστά: μια φόρμα συμπληρώνεται προγραμματιστικά, ο πελάτης την ανοίγει στο Acrobat, και κάθε πεδίο φαίνεται άδειο. Κάντε κλικ μέσα σε ένα πεδίο και η τιμή του εμφανίζεται αμέσως. Τα δεδομένα βρίσκονται στο αρχείο όλη την ώρα. Αυτό που λείπει είναι η εικόνα των δεδομένων, και ο λόγος αξίζει να κατανοηθεί μία φορά επειδή εξηγεί μια ολόκληρη οικογένεια σφαλμάτων

Ένα πεδίο κειμένου AcroForm αποθηκεύει την τιμή του στην καταχώριση /V του λεξικού πεδίου (ISO 32000-1 §12.7.3.3). Αυτό που πραγματικά ζωγραφίζει ένας viewer είναι κάτι ξεχωριστό: η ροή εμφάνισης του widget κάτω από το /AP (§12.5.5), ένα μικρό προαποδοσμένο απόσπασμα περιεχομένου. Γράψτε το /V και αφήστε το /AP ανέγγιχτο, και τα δύο αποκλίνουν. Η τιμή είναι εκεί· η αποδοσμένη εκδοχή της είναι μπαγιάτικη ή απούσα. Το Acrobat τυχαίνει να ξαναχτίζει την εμφάνιση ενός πεδίου όταν αυτό αποκτά εστίαση, κάτι που είναι ολόκληρη η εξήγηση για τιμές που εμφανίζονται μόνο με κλικ. Η παλιά σημαία NeedAppearances, η οποία ζητούσε από τους viewers να αναδημιουργήσουν τις εμφανίσεις για εσάς, ποτέ δεν δούλεψε ομοιόμορφα και είναι deprecated στο PDF 2.0, και οι εξυπηρετητές εκτύπωσης και οι γεννήτριες thumbnail την αγνοούν εντελώς. Ζωγραφίζουν το /AP και τίποτα άλλο, οπότε αν το /AP είναι άδειο τυπώνουν ένα κενό κουτί

Η ανάθεση μιας τιμής μέσω της FormField[i] γράφει μόνο το /V. Γι' αυτό η συμπλήρωση μιας φόρμας είναι μια ακολουθία τριών βημάτων, και το βήμα που παραλείπουν οι ομάδες είναι το μεσαίο:

Διάγραμμα της απόκλισης τιμής /V έναντι εμφάνισης /AP σε πεδία AcroForm και της τριών-βημάτων ακολουθίας συμπλήρωσης Delphi χτισμένης γύρω από το GenerateFormAppearances
Η ανάθεση τιμών γράφει μόνο /V, και το μεσαίο βήμα είναι αυτό που ξαναζωγραφίζει ό,τι αποδίδουν πραγματικά οι διακομιστές εκτύπωσης
procedure TFormViewer.FillAndSave(const Values: array of WString;
  const OutputPath: string);
var
  i: Integer;
begin
  for i := 0 to Pdf.FormFieldCount - 1 do
    Pdf.FormField[i] := Values[i];   // γράφει μόνο /V

  // Ξαναχτίστε τις ροές εμφάνισης /AP· χωρίς αυτό η φόρμα
  // φαίνεται κενή στο Acrobat μέχρι να πατηθεί κάθε πεδίο
  Pdf.GenerateFormAppearances;

  Pdf.SaveAs(OutputPath);
end;

Η GenerateFormAppearances είναι όλη η διόρθωση. Ξαναχτίζει τη ροή εμφάνισης κάθε widget από τις τρέχουσες τιμές, γραμματοσειρές και quadding, ώστε ένας viewer που ποτέ δεν τρέχει συμβάν εστίασης, ένας εξυπηρετητής εκτύπωσης ή ένας δημιουργός thumbnail, να ζωγραφίζει τη συμπληρωμένη κατάσταση έτσι κι αλλιώς. Καλέστε τη μία φορά μετά την παρτίδα αναθέσεων, όχι μία φορά ανά πεδίο. Η δημιουργία εμφάνισης κάνει πραγματική δουλειά διάταξης, και οι κλήσεις ανά πεδίο πολλαπλασιάζουν αυτό σε μια μεγάλη φόρμα χωρίς λόγο

Η αναδημιουργία εμφανίσεων είναι επίσης η στιγμή που οι γραμματοσειρές και η στοίχιση επιβάλλονται, κάτι που είναι η πηγή μιας έκπληξης δεύτερης τάξης. Η νέα ροή διατάσσει κάθε τιμή μέσα στο ορθογώνιο του widget χρησιμοποιώντας τη γραμματοσειρά, το μέγεθος και το quadding του πεδίου. Μια τιμή που χωρά άνετα στη δοκιμαστική σας φόρμα μπορεί να κοπεί ή να συρρικνωθεί στο αντίγραφο ενός πελάτη όπου το ίδιο πεδίο είναι στενότερο. Τα πεδία αυτόματου μεγέθους (μέγεθος γραμματοσειράς μηδέν) συρρικνώνουν το κείμενο ώστε να χωρέσει· τα πεδία σταθερού μεγέθους απλώς το κόβουν. Και τα δύο είναι νόμιμα, και ο μόνος έντιμος τρόπος να ξέρετε ποιο από τα δύο κάνει μια δεδομένη φόρμα είναι να κοιτάξετε την αναδημιουργημένη έξοδο αντί για τη συμβολοσειρά που γράψατε. Όταν κάποιος αναφέρει κείμενο κομμένο στην άκρη ενός κουτιού, αυτός είναι σχεδόν πάντα ο λόγος

Αντιμετωπίστε την επαλήθευση ως μέρος της ολοκλήρωσης της δουλειάς, όχι ως ύστερη σκέψη. Ανοίξτε το αποθηκευμένο αρχείο στο Acrobat και επιβεβαιώστε ότι οι τιμές είναι ορατές προτού αγγίξετε κανένα πεδίο. Έπειτα τυπώστε το σε PDF ή σε εικόνα από έναν διαφορετικό viewer, έναν που αγνοεί εντελώς τη λογική φόρμας, και επιβεβαιώστε ότι οι τιμές επιβιώνουν και σε εκείνη τη διαδρομή. Μαζί, αυτοί οι δύο έλεγχοι πιάνουν κάθε παραλλαγή της απόκλισης /V-έναντι-/AP

Διαμορφώσεις πεδίων που περνούν το demo και αποτυγχάνουν στην πράξη

Οι καθαρές φόρμες demo κρύβουν ένα σύνολο ακραίων περιπτώσεων που τα αρχεία πελατών δεν κρύβουν. Τέσσερις από αυτές ευθύνονται για τις περισσότερες αναφορές τύπου «δούλεψε στο μηχάνημά μου»

  • Τιμές εξαγωγής checkbox. Η κατάσταση «on» δεν είναι πάντα Yes. Μια φόρμα είναι ελεύθερη να ορίσει τη δική της τιμή εξαγωγής, και η εγγραφή της λάθος συμβολοσειράς αφήνει το κουτί οπτικά μη τσεκαρισμένο ενώ ο κώδικάς σας είναι πεπεισμένος ότι το όρισε. Διαβάστε την τιμή εξαγωγής από την FormFieldInfo[] αντί να υποθέσετε μία
  • Ομάδες radio με κοινό όνομα. Ένα πεδίο, αρκετά widgets. Η τιμή που αναθέτετε αποφασίζει ποιο widget διαβάζεται ως επιλεγμένο, οπότε κώδικας UI που υποθέτει ότι ένα όνομα αντιστοιχεί σε ένα ορθογώνιο καταλήγει να ζωγραφίζει τον δακτύλιο εστίασης στο λάθος κουμπί
  • Υπολογισμένα πεδία. Σύνολα που διατηρούνται από JavaScript εγγράφου ενημερώνονται ως απάντηση σε συμβάντα πεδίων. Μια προγραμματιστική συμπλήρωση που παρακάμπτει αυτά τα συμβάντα πρέπει είτε να πυροδοτήσει επανυπολογισμό είτε να αντικαταστήσει απευθείας τα υπολογισμένα πεδία. Μια φόρμα όπου τα στοιχεία γραμμής και το σύνολο διαφωνούν είναι χειρότερη από οποιαδήποτε από τις δύο διορθώσεις
  • Κρυμμένα υποχρεωτικά πεδία. Οι υπό όρους φόρμες κρύβουν πεδία που εξακολουθούν να είναι σημειωμένα ως υποχρεωτικά. Αποφασίστε εκ των προτέρων αν η επικύρωσή σας σέβεται την ορατότητα ή την ακατέργαστη σημαία υποχρεωτικού, και μετά καταγράψτε αυτή την απόφαση κάπου όπου η υποστήριξη μπορεί να τη βρει

Μια διάκριση αξίζει να ξεκαθαριστεί προτού σας δαγκώσει: η δημιουργία εμφανίσεων δεν είναι flattening. Η GenerateFormAppearances κάνει τις τιμές ορατές παντού ενώ αφήνει τα πεδία επεξεργάσιμα. Το flattening ψήνει την εμφάνιση μέσα σε στατικό περιεχόμενο σελίδας και αφαιρεί την αλληλεπίδραση οριστικά, κάτι που είναι σωστό για ένα αρχειακό αντίγραφο και λάθος για μια φόρμα που ο επόμενος άνθρωπος πρέπει ακόμα να συμπληρώσει. Αν η FormType αναφέρει ftXfaFull αντί για ftAcroForm, καμία από την επιφάνεια επεξεργασίας εδώ δεν εφαρμόζεται καθαρά έτσι κι αλλιώς, αφού το έγγραφο αποδίδεται από το δικό του πρότυπο XML· εντοπίστε αυτή την περίπτωση και πείτε το στον χρήστη, αντί να τον αφήσετε να βρει το όριο μόνος του

Το υποσύστημα form-fill, η διάτρεξη εστίασης, και η δημιουργία εμφανίσεων που παρουσιάζονται εδώ είναι μέρος του PDFium Component για Delphi, C++Builder και Lazarus/FPC. Αν ο viewer σας χειρίζεται επίσης σχολιασμό ελεγκτή παράλληλα με δεδομένα φόρμας, το άρθρο για τον έλεγχο annotations καλύπτει εκείνο το γειτονικό μοντέλο