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

Πεδία multi-select PDF σε round trips FDF και XFDF (Delphi)

Το HotPDF κάνει round trip τιμές multi-select list box μέσα από FDF και XFDF κρατώντας την τιμή πεδίου ως πίνακα από άκρη σε άκρη. Από την έκδοση 2.755.0, τα ExportLoadedFormToFDF, ExportLoadedInterchangeToFDF και ExportLoadedFormToXFDF γράφουν κάθε επιλεγμένη επιλογή ως δικό της string FDF ή στοιχείο <value> XFDF, και οι αντίστοιχες μέθοδοι εισαγωγής ελέγχουν κάθε τιμή απέναντι στις επιλογές πεδίου και ξαναχτίζουν τους δείκτες επιλογής /I πριν αλλάξουν οτιδήποτε. Τίποτα δεν κολλάται σε ένα string στην πορεία

Η αποτυχία που διορθώνει αυτό είναι εύκολο να αναπαραχθεί. Πάρτε μια φόρμα παραγγελίας με multi-select list box επιλογών προϊόντων, αφήστε έναν χρήστη να διαλέξει δύο από αυτά, εξάγετε τα δεδομένα φόρμας για σύστημα back-office, μετά εισάγετε το επεξεργασμένο αρχείο πίσω στο PDF. Πριν από αυτή την αλλαγή ο list box επέστρεφε κενός ή λάθος. Η αιτία είναι ότι μία από τις τιμές εξαγωγής περιείχε αλλαγή γραμμής, και το παλιό μονοπάτι είχε ισοπεδώσει τις επιλογές σε ένα μοναδικό string χωρισμένο με γραμμές. Το να βγεις με πολλαπλές επιλογές από εκείνο το string δεν ήταν ποτέ αξιόπιστο, και με τιμή εξαγωγής που η ίδια περιέχει αλλαγή γραμμής δεν μπορεί καθόλου να δουλέψει

Γιατί η ένωση τιμών multi-select με αλλαγές γραμμής σπάει το round trip;

Η ένωση των επιλογών σε ένα string πετάει τα όρια ανάμεσα στις τιμές, και μια τιμή μπορεί να περιέχει τον διαχωριστικό, οπότε κανένας importer δεν μπορεί να ξανακόψει το string σωστά. Το ISO 32000-1 §12.7.4.4 επιτρέπει στην εγγραφή /V ενός πεδίου choice είτε σκέτο text string είτε πίνακα text strings, και ένας list box με τη σημαία MultiSelect (bit 22 του /Ff) χρησιμοποιεί τη μορφή πίνακα μόλις διαλεχτεί πάνω από μία επιλογή. Η ίδια ενότητα ορίζει το /I ως πίνακα δεικτών επιλογών 0-based σε αύξουσα σειρά, που οι viewers χρησιμοποιούν για να ξεχωρίσουν δύο επιλογές που τυχαίνει να μοιράζονται τιμή εξαγωγής. Στο HotPDF ο scalar getter GetFormFieldValue διαβάζει μόνο τη μορφή string, οπότε το πέρασμα πίνακα από εκείνο υποβάθμιζε την εξαγωγή σε κενό string, και η παλιά εισαγωγή XFDF ένωνε επαναλαμβανόμενα στοιχεία <value> με LF. Φανταστείτε επιλογή εξαγόμενη ως Deep, αλλαγή γραμμής, Blue: μετά την ένωση, Deep\nBlue\nRed μπορεί να είναι δύο επιλογές ή τρεις, και το αρχείο δεν δίνει τρόπο να ξέρετε ποιο. Το fix ήταν να σταματήσει εντελώς η χρήση scalar στη μέση του round trip

Παλιό round trip multi-select του HotPDF όπου δύο διαλεγμένες επιλογές list box, η μία με ενσωματωμένη αλλαγή γραμμής, ισοπεδώνονται από το μονοπάτι scalar GetFormFieldValue στο μοναδικό string Deep, αλλαγή γραμμής, Blue, αλλαγή γραμμής, Red, που οι κατάντη readers μπορούν να αναλύσουν είτε ως δύο επιλογές είτε ως τρεις
Η ένωση τιμών multi-select σε ένα string καταστρέφει τα όρια τιμών, και τιμή εξαγωγής που η ίδια περιέχει αλλαγή γραμμής κάνει την ισοπεδωμένη μορφή ασαφή

Τι περιέχουν τα εξαγόμενα αρχεία FDF και XFDF;

Το HotPDF γράφει τιμή multi-select ως typed πίνακα σε FDF και ως ένα στοιχείο <value> ανά επιλογή σε XFDF, οπότε τα όρια μένουν ορατά στον δίσκο. Σε FDF κάθε στοιχείο κρατά την ορθογραφία που είχε στο PDF πηγή: hexadecimal strings βγαίνουν ως hex, και literal strings escaped-αρονται από έναν μόνο helper που μετατρέπει CR και LF σε \r και \n. Σε XFDF η ρίζα κουβαλά xml:space="preserve" όπως απαιτεί το ISO 19444-1, που σημαίνει ότι οποιοδήποτε whitespace μέσα σε στοιχείο κειμένου μετράει ως δεδομένα. Το HotPDF λοιπόν γράφει το tag εκκίνησης, το escaped κείμενο και το tag λήξης κάθε <value> σε ένα κομμάτι, κρατά εσοχές έξω από το στοιχείο, και κωδικοποιεί CR, LF και TAB ως character references ώστε XML parser που εφαρμόζει κανονικοποίηση τελών γραμμής να μην μπορεί να αλλάξει τα πρωτότυπα bytes

Μορφές εξαγωγής που γράφει το HotPDF για multi-select list box από το 2.755.0: το FDF κουβαλά έναν typed πίνακα ανά πεδίο με /V [(Deep αλλαγή γραμμής Blue) (Red)] και τιμή region hex, ενώ το XFDF κουβαλά ένα στοιχείο value ανά επιλογή κάτω από xml:space preserve ώστε το whitespace να μετρά ως δεδομένα
Τα όρια μένουν ορατά στον δίσκο: το FDF κρατά κάθε επιλογή ως δικό της στοιχείο πίνακα και το XFDF γράφει κάθε μία σε ξεχωριστό στοιχείο value, οπότε κανένας importer δεν χρειάζεται να μαντέψει
<!-- FDF: ένας typed πίνακας ανά πεδίο -->
<< /T (options) /V [(Deep\nBlue) (Red)] >>
<< /T (region) /V [<45553132>] >>

<!-- XFDF: ένα <value> ανά επιλογή -->
<xfdf xmlns="http://ns.adobe.com/xfdf/" xml:space="preserve">
  <fields>
    <field name="options">
      <value>Deep&#xA;Blue</value>
      <value>Red</value>
    </field>
  </fields>
</xfdf>

Δύο οριακές περιπτώσεις εξαγωγής αξίζει να ξέρετε πριν γράψετε τον κώδικα κλήσης. Πρώτον, το ExportLoadedFormToFDF χτίζει ολόκληρο το body FDF στη μνήμη πριν δημιουργήσει το αρχείο στόχο (διορθώθηκε στο 2.755.1), οπότε τιμή που δεν μπορεί να εξαχθεί, όπως πίνακας που κρατά οτιδήποτε άλλο εκτός strings, πετάει εξαίρεση χωρίς να περικόψει υπάρχον αρχείο. Δεύτερον, κενή επιλογή σε list box που προσφέρει επίσης τιμή εξαγωγής κενού string είναι ασαφής σε XFDF, επειδή <value/> μπορεί να σημαίνει τίποτα δεν είναι επιλεγμένο ή ότι η κενή επιλογή είναι επιλεγμένη. Το ExportLoadedFormToXFDF πετάει εξαίρεση σε εκείνη την περίπτωση αντί να μαντέψει, και πετάει πριν ανοίξει το αρχείο στόχος. Το FDF δεν έχει τέτοια ασάφεια, αφού /V [] και /V [()] είναι διακριτά. Και οι δύο FDF exporters επίσης παραλείπουν terminals μόνο-widget χωρίς όνομα /T, ταιριάζοντας τον XFDF exporter, επειδή κανένας importer δεν θα μπορούσε ποτέ να αντιστοιχίσει εκείνες τις εγγραφές πίσω σε πεδίο

var
  Pdf: THotPDF;
  Written: Integer;
begin
  Pdf := THotPDF.Create(nil);
  try
    if Pdf.LoadFromFile('order-form.pdf', '') > 0 then
    begin
      // Multi-select list boxes γράφονται ως /V [(...) (...)]
      Written := Pdf.ExportLoadedFormToFDF('order-form.fdf');
      try
        Pdf.ExportLoadedFormToXFDF('order-form.xfdf');
      except
        on E: Exception do
          // Κενή επιλογή συν κενή επιλογή εξαγωγής: το XFDF δεν μπορεί να
          // τις ξεχωρίσει, και το υπάρχον αρχείο .xfdf μένει άθικτο
          ShowMessage('XFDF export refused: ' + E.Message);
      end;
    end;
  finally
    Pdf.Free;
  end;
end;

Πώς επικυρώνει το HotPDF τιμή multi-select κατά την εισαγωγή;

Το HotPDF δέχεται εισαγόμενο πίνακα μόνο όταν ο στόχος είναι πεδίο choice με set τη σημαία MultiSelect και κάθε τιμή στον πίνακα ταιριάζει τιμή εξαγωγής στον πίνακα /Opt του πεδίου. Κάθε θέση επιλογής μπορεί να χρησιμοποιηθεί μία φορά, οπότε λίστα με δύο επιλογές που μοιράζονται την τιμή εξαγωγής b δέχεται [<62> <62>] ως δύο διακριτές επιλογές και απορρίπτει τρίτο b. Το ξαναχτισμένο /I ακολουθεί τη σειρά /Opt και όχι τη σειρά των εισερχόμενων τιμών, αφού το §12.7.4.4 απαιτεί αύξοντες δείκτες. Το HotPDF χτίζει το νέο /V και /I ως αποκομμένα objects και τα αναθέτει μόνο αφού κάθε τιμή έχει περάσει επικύρωση, οπότε απορριφθείσα τιμή δεν αφήνει ποτέ μισό πίνακα ή ξεπερασμένους δείκτες πίσω. Το αντίγραφο γράφεται στο πεδίο που εισάγεται και όχι σε κοινόχρηστο πίνακα προγόνου, hex ορθογραφίες που φτάνουν από FDF μένουν hex μέσω της αποθήκευσης, και πεδία των οποίων οι υπολογισμοί εξαρτώνται από τον list box σημειώνονται για επανυπολογισμό. Αν θέλετε μόνο να ορίσετε μία τιμή, το ορισμός μίας τιμής πεδίου φόρμας σε φορτωμένο PDF περνά από το scalar μονοπάτι, που εκ σχεδιασμού δεν χειρίζεται πολλαπλές επιλογές

Επικύρωση εισαγωγής του HotPDF για τιμές multi-select: ο στόχος πρέπει να είναι πεδίο choice με MultiSelect set στο /Ff, κάθε εισερχόμενη τιμή πρέπει να ταιριάζει τιμή εξαγωγής /Opt με κάθε θέση χρησιμοποιημένη μία φορά, το /I ξαναχτίζεται αύξον σε σειρά /Opt, και αποκομμένα /V και /I αναθέτονται μόνο αφού όλες οι τιμές περάσουν
Κάθε εισερχόμενη τιμή ελέγχεται απέναντι στις επιλογές πεδίου πριν γραφτεί οτιδήποτε, οπότε απορριφθείσα τιμή δεν αφήνει ποτέ μισό πίνακα ή ξεπερασμένους δείκτες επιλογής πίσω

Κάποια άλλα εργαλεία γράφουν σκέτες τιμές εξαγωγής ASCII ως hex strings χωρίς byte order mark, για παράδειγμα <416272>, και μετά εξάγουν XFDF γράφοντας εκείνα τα hex ψηφία ως κείμενο. Μια αυστηρή σύγκριση literal στον δρόμο της επιστροφής αποτυγχάνει, και η εισαγωγή ματαιώνεται. Η έκδοση 2.755.1 προσθέτει ένα retry: όταν μια τιμή δεν ταιριάζει καμία επιλογή, το HPDFHexSpellingText αποκωδικοποιεί το κείμενο ως hex payload και ξανασυγκρίνει το αποτέλεσμα. Το retry ισχύει μόνο για είσοδο που αλλιώς θα πετούσε εξαίρεση, οπότε δεν αλλάζει ποτέ τιμή που είχε ήδη ταιριάξει. Η ίδια έκδοση επίσης έκανε το scalar και το πίνακα μονοπάτι να χρησιμοποιούν τον ίδιο Unicode decoder, που καταλαβαίνει PDFDocEncoding, UTF-16 με όποιο byte order mark και UTF-8. Πριν από αυτό, μία λογική τιμή μπορούσε να ταιριάξει σε ένα μονοπάτι και να αποτύχει στο άλλο σε έγγραφα που ανακάτευαν κωδικοποιήσεις

Γιατί ένα έγκυρο αρχείο FDF μπορεί ακόμα να χάσει πεδία κατά την ανάλυση;

Ένας σαρωτής FDF που δεν ιχνηλατεί hexadecimal strings μπορεί να κόψει στη μέση dictionary πεδίου όταν μια hex τιμή τελειώνει ακριβώς δίπλα στον τερματιστή dictionary. Στο << /T (region) /V <416273>>> το πρώτο > κλείνει το hex string, αλλά ένας αφελής σαρωτής το διαβάζει μαζί με το επόμενο > ως τέλος του dictionary και σιωπηλά πετάει το πεδίο. Ο FDF importer επιπέδου αρχείου ήδη ιχνηλατούσε αν ήταν μέσα σε hex string, και στο 2.755.1 οι σαρωτές πίνακα και dictionary πίσω από το ImportLoadedInterchangeFromFDF κάνουν το ίδιο. Ένα δεύτερο ζήτημα αφορά indirect references. Ένα αρχείο FDF είναι μικρό έγγραφο σύνταξης PDF με δική του αρίθμηση objects (ISO 32000-1 §12.7.7), οπότε τιμή όπως /V [11 0 R] αναφέρεται στο object 11 του αρχείου FDF, όχι στο object 11 του PDF που γεμίζετε. Ο απλοποιημένος FDF parser στο HotPDF δεν λύνει αναφορές μέσα στο αρχείο, οπότε απορρίπτει τέτοιον πίνακα αντί να διαβάσει ό,τι τυχαίνει να είναι το object 11 στο έγγραφο στόχο

Εισαγωγές αρχείου, stream και XFDF αναφέρουν λάθη διαφορετικά

Οι τρεις διαδρομές εισαγωγής επικυρώνουν με τον ίδιο τρόπο αλλά αναφέρουν αποτυχίες διαφορετικά, και αξίζει να διαλέξετε μία επίτηδες. Το ImportLoadedFormFromFDF παραλείπει κάθε πεδίο που αποτυγχάνει επικύρωση και επιστρέφει το πλήθος των πεδίων που όντως εφάρμοσε, οπότε πλήθος χαμηλότερο από το αναμενόμενο είναι το μόνο σημάδι προβλήματος. Το ImportLoadedInterchangeFromFDF και το ImportLoadedFormFromXFDF πετάνε εξαίρεση στο πρώτο απορριφθέν πεδίο. Κάθε πεδίο δεσμεύεται από μόνη του, οπότε πεδία επεξεργασμένα πριν την εξαίρεση κρατούν τις νέες τους τιμές. Μην μεταχειριστείτε κανένα από αυτά ως συναλλαγή πάνω σε ολόκληρο αρχείο ανταλλαγής: αν θέλετε όλα-ή-τίποτα συμπεριφορά, πετάξτε το φορτωμένο έγγραφο όταν συμβεί εξαίρεση αντί να το αποθηκεύσετε

var
  Pdf: THotPDF;
  Source: TMemoryStream;
  Status: AnsiString;
  Info: THPDFFDFInterchangeInfo;
begin
  Pdf := THotPDF.Create(nil);
  Source := TMemoryStream.Create;
  try
    Source.LoadFromFile('order-form-reviewed.fdf');
    if Pdf.LoadFromFile('order-form.pdf', '') > 0 then
    try
      // Μόνο πεδία· τιμή εκτός /Opt ή στόχος μη-multi-select πετάει εξαίρεση
      if Pdf.ImportLoadedInterchangeFromFDF(Source, True, False, Status, Info) then
        Pdf.SaveLoadedDocument('order-form-filled.pdf');
    except
      on E: Exception do
        ShowMessage('Import rejected, nothing saved: ' + E.Message);
    end;
  finally
    Source.Free;
    Pdf.Free;
  end;
end;

Επέκταση των callbacks XFDF χωρίς να σπάσουν υπάρχοντες καλούντες

Η υποστήριξη πίνακα στη χαμηλότερου επιπέδου μονάδα XFDF ζει σε ξεχωριστή εγγραφή, το THPDFXFDFArrayAccess, και σε νέα overloads των HPDFXFDFExportFields και HPDFXFDFImportFields, όχι σε επιπλέον πεδία προστεθειμένα στο τέλος της υπάρχουσας εγγραφής THPDFXFDFAccess. Ο λόγος είναι δυαδική συμβατότητα. Κώδικας που γεμίζει THPDFXFDFAccess ως τοπική μεταβλητή συχνά θέτει μόνο τις θέσεις που ξέρει και ποτέ δεν καθαρίζει τις υπόλοιπες, οπότε νέος δείκτης συνάρτησης προστεθειμένος σε εκείνη την εγγραφή θα περιείχε σκουπίδια stack, και η βιβλιοθήκη θα τον έπαιρνε για πραγματικό callback. Με ξεχωριστή εγγραφή, οι παλιοί καλούντες κρατούν την παλιά διάταξη και τα παλιά overloads, και εκείνα τα overloads περνούν εσωτερικά εγγραφή πίνακα όλα-nil. Το πρωτότυπο scalar import overload εξακολουθεί να ενώνει επαναλαμβανόμενες τιμές με LF για συμβατότητα, και μόνο το overload που γνωρίζει πίνακες τις κρατά χώρια. Όταν δένετε τη δική σας αποθήκη δεδομένων, ξεκινήστε από Default(THPDFXFDFArrayAccess). Επιστρέφετε True από το GetFormFieldValueArray για οποιοδήποτε πεδίο με τιμή λίστα, συμπεριλαμβανομένου ενός με τίποτα επιλεγμένο, και False για να γυρίσετε στο scalar callback

uses HPDFXFDF;

// Σκέτος δείκτης συνάρτησης, όχι "of object": το Context κουβαλά τη δική σας αποθήκη
function StoreGetSelections(Context: Pointer; FieldIndex: Integer;
  out Values: THPDFXFDFValueArray): Boolean;
begin
  Result := TFormStore(Context).IsListField(FieldIndex);
  if Result then
    Values := TFormStore(Context).Selections(FieldIndex);
end;

procedure ExportStore(Store: TFormStore; out Bytes: TBytes);
var
  Access: THPDFXFDFAccess;
  ArrayAccess: THPDFXFDFArrayAccess;
begin
  Access := MakeStoreAccess(Store);             // οι υπάρχουσες scalar δεσμεύσεις σας
  ArrayAccess := Default(THPDFXFDFArrayAccess); // κάθε αχρησιμοποίητη θέση είναι nil
  ArrayAccess.GetFormFieldValueArray := StoreGetSelections;
  HPDFXFDFExportFields(Access, ArrayAccess, Bytes);
end;

Η ανταλλαγή multi-select δουλεύει σε list boxes που υπάρχουν ήδη και έχουν set το bit MultiSelect στο /Ff. Για το πώς δημιουργούνται εξαρχής τα πεδία choice και τα bit σημαιών τους, δείτε το προσθήκη ListBox και άλλων πεδίων AcroForm σε φορτωμένο PDF. Για σχολιασμό markup που περνά από το δέντρο <annots> του XFDF, δείτε το XFDF import και export σχολίων στο HotPDF. Η πλήρης αναφορά API και το trial download είναι στη σελίδα HotPDF Delphi PDF component