Το PDFium Component αποθηκεύει επεξεργασμένες τιμές φορμών XFA ακριβώς, δια μέσου save και reopen, όταν τρέχει το Windows V8 runtime pdfium.v8.dll που στάλθηκε στο v3.125.2 ή νεότερο. Παλαιότερα runtimes πρόσθεταν line feeds σε τιμές fields, έκοβαν emoji σε άσχετο χαρακτήρα BMP, προσπερνούσαν σιωπηλά saves XFA μονού stream και μπορούσαν να καταπιούν αποτυχημένη τελική εγγραφή. Ένα σύμπτωμα reopen δεν είναι καθόλου ελάττωμα βιβλιοθήκης: dynamic φόρμα της οποίας το root subform λείπει restoreState="auto" ξαναχτίζει το layout της από το template
Τα bug reports γι’ αυτά έμοιαζαν όλα. Πελάτης συμπληρώνει XFA φόρμα κλήσης σε Delphi viewer, σώζει, ξανανοίγει, και κάτι είναι ελαφρώς πέρα. Ένα άδειο box σχολίων τώρα κρατά κενή γραμμή, και μετά από δεύτερο save κρατά δύο. Όνομα πληκτρολογημένο με emoji γυρίζει με glyph ιδιωτικής χρήσης. Κανείς δεν παίρνει σφάλμα, που είναι ό,τι κάνει αυτά τα bugs ακριβά: η παρέκκλιση βγαίνει στην επιφάνεια εβδομάδες αργότερα στην εξαγωγή κάποιου άλλου
Τι πάει στραβά όταν μια XFA φόρμα σώζεται και ξανανοίγει;
Τέσσερα ξεχωριστά ελαττώματα στη native διαδρομή save XFA προκάλεσαν παρέκκλιση τιμών, και το καθένα κρυβόταν πίσω από save που έμοιαζε επιτυχημένο. Δύο προήλθαν από serialization, ένα από τη διάταξη αποθήκευσης μονού stream, και ένα από τον ίδιο τον PDF writer. Ο πίνακας αντιστοιχίζει κάθε σύμπτωμα στην αιτία του και στο release που το διόρθωσε το PDFium Component
| Σύμπτωμα μετά το reopen | Αιτία | Διορθώθηκε σε |
|---|---|---|
| Άδειο field κρατά line feed· οι τιμές μεγαλώνουν ένα newline ανά save | Και οι δύο XFA writers εισήγαγαν layout newlines μετά από start tags | v3.125.2, pdfium.v8.dll |
| Το U+1F642 γυρίζει ως U+F642, ή το emoji εξαφανίζεται από το form packet | Περικοπή wchar_t 16 bit στο decoding· φιλτράρισμα surrogates στον form serializer | v3.125.2, pdfium.v8.dll |
| Οι επεξεργασίες σε single-stream XFA έγγραφο απλώς χάθηκαν | Το native save απέρριψε τη διάταξη stream, αλλά η επιστρεφόμενη τιμή αγνοήθηκε | v3.125.2· comments και processing instructions διατηρούνται από το v3.126.0 |
| Κομμένο αρχείο παρόλο που το save ανέφερε επιτυχία | Τελική buffered εγγραφή απέτυχε αφού ο writer είχε ήδη επιστρέψει επιτυχία | v3.125.2 V8 runtime· v3.125.3 συνηθισμένο pdfium.dll |
| Dynamic φόρμα τριών σελίδων ξανανοίγει ως δύο σελίδες | Το root subform δεν ζητά restoreState="auto" | Συγγραφή φόρμας, όχι ελάττωμα βιβλιοθήκης |
Παλαιότερες εκθέσεις κατέληγαν ότι οι επεξεργασίες fields XFA δεν μπορούσαν να διατηρηθούν καθόλου με PDFium, που ήταν ακριβές για τα runtimes της εποχής. Το νεότερο V8 runtime σώζει τιμές XFA εγγενώς, οπότε επεξεργασία που έγινε στη ζωντανή φόρμα φτάνει στο αποθηκευμένο datasets packet χωρίς packet χειρουργική από τη δική σας πλευρά
Ποιο PDFium runtime σώζει τιμές XFA;
Η πίστη save XFA εξαρτάται από τη native DLL, όχι από τον Delphi wrapper, οπότε ο πρώτος έλεγχος είναι ποιο runtime φόρτωσε όντως η διεργασία σας. Το PDFium Component στέλνει δύο Windows builds ανά αρχιτεκτονική: το συνηθισμένο pdfium.dll, χτισμένο χωρίς V8 και XFA, και το pdfium.v8.dll, που κουβαλά τη JavaScript engine και το XFA form runtime. Μόνο το pdfium.v8.dll μπορεί να τρέξει XFA φόρμα, οπότε κάθε XFA διόρθωση που περιγράφεται εδώ ζει εκεί, ξεκινώντας με τις ξαναχτισμένες Win32 και Win64 V8 βιβλιοθήκες στο v3.125.2
Η διόρθωση τελικής εγγραφής είναι γενικός κώδικας PDF writer, οπότε μετράει και για συνηθισμένα έγγραφα. Το v3.125.3 ξαναέχτισε τις συνηθισμένες βιβλιοθήκες pdfium.dll να κουβαλάνε εκείνη την ίδια επισκευή. Μοιρασμένος πηγαίος κώδικας δεν είναι απόδειξη μοιρασμένης συμπεριφοράς· μέχρι να ξαναχτιστεί το binary, η παλιά DLL κρατά το παλιό bug
Δεύτερη παγίδα καθόταν στον loader. Πριν το v3.125.2, το να οριστεί το EnableV8Engine σε True έκανε το binding να διαλέξει το default όνομα pdfium.v8.dll και να αγνοήσει πλήρη διαδρομή στο LibraryName. Εφαρμογή που έδειχνε σε φρεσκοdeployαρισμένο runtime μπορούσε να εξακολουθεί να φορτώνει παλαιότερο αντίγραφο από άλλο φάκελο. Από το v3.125.2, LibraryName που περιέχει κατάλογο διαλέγει ακριβώς εκείνο το αρχείο σε όποια engine λειτουργία κι αν βρίσκεσαι, και απόν διαδρομή αποτυγχάνει αντί να πέσει πίσω σε άλλη bundled βιβλιοθήκη
uses
System.SysUtils, PDFium;
procedure SelectXfaRuntime;
begin
// Κατάλογος στο LibraryName καρφώνει αυτό ακριβώς το αρχείο (v3.125.2 και μετά);
// αν το αρχείο λείπει, η φόρτωση σηκώνει αντί να πέσει σε fallback
{$IFDEF WIN64}
PDFium.LibraryName := ExtractFilePath(ParamStr(0)) + 'DLLs\Win64\pdfium.v8.dll';
{$ELSE}
PDFium.LibraryName := ExtractFilePath(ParamStr(0)) + 'DLLs\Win32\pdfium.v8.dll';
{$ENDIF}
PDFium.EnableV8Engine := True;
PDFium.LoadLibrary; // απότυχε στην εκκίνηση, όχι στο πρώτο save
end;
Μετά το άνοιγμα εγγράφου, το TPdf.XFA σας λέει ότι το αρχείο περιέχει XFA και το TPdf.XfaRuntimeAvailable σας λέει ότι η φορτωμένη DLL μπορεί όντως να το εκτελέσει. Αν χρειάζεστε επίσης να ξεχωρίσετε static και dynamic φόρμες, το TPdf.FormType επιστρέφει ftXfaFull ή ftXfaForeground· το άρθρο για ανίχνευση XFA φορμών και εξαγωγή XFA packets στο Delphi καλύπτει εκείνη τη διερεύνηση λεπτομερώς
Γιατί τα σωσμένα XFA fields βγάζουν επιπλέον line feeds;
Τα σωσμένα XFA fields κέρδιζαν line feeds επειδή και οι δύο native XFA writers, ο γενικός XML element writer και ο form packet serializer, έκαναν pretty-print την έξοδό τους με newline μετά από start tags. Στα περισσότερα XML εκείνο το whitespace είναι καλλωπιστικό. Σε XFA δεδομένα δεν είναι: όταν το datasets packet γίνεται parse ξανά, το κείμενο ανάμεσα στο <Comments> και το </Comments> είναι η τιμή του field, newline συμπεριλαμβανομένου. Ένα άδειο field άρα ξανανοίγαγε κρατώντας μοναχικό LF, και κάθε περαιτέρω κύκλος save-and-reopen μπορούσε να προσθέσει άλλον έναν
Η προφανής επισκευή, το να κόβεις τιμές στο φόρτωμα, θα ήταν λάθος. Οι χρήστες πληκτρολογούν αρχικά κενά, τελικά κενά και σκόπιμο πολλαπλών γραμμών κείμενο σε XFA fields, και μπλοκ διεύθυνσης ή κωδικός σταθερού πλάτους πρέπει να επιβιώνει byte προς byte. Η διόρθωση v3.125.2 αφαιρεί άρα μόνο το whitespace που ο ίδιος ο serializer συνέθεσε γύρω από tags. Τιμές χρηστών, υπάρχοντα text nodes και CDATA sections περνάνε ανέγγιχτα, οπότε το " indented" μένει με εσοχή και ένα σκόπιμα άδειο field μένει άδειο
Γιατί ένα emoji γυρίζει ως διαφορετικός χαρακτήρας;
Ένα emoji γυρνούσε λάθος επειδή το Windows wchar_t έχει πλάτος 16 bits, και δύο διαδρομές decoding αποθήκευαν πλήρη Unicode scalar τιμή σε μοναχικό wchar_t. Και ο αποκωδικοποιητής stream UTF-8 και ο parser για αριθμητικές αναφορές χαρακτήρων όπως 🙂 το έκαναν αυτό. Το U+1F642, το ελαφρώς χαμογελαστό πρόσωπο, δεν χωράει σε 16 bits, οπότε τα υψηλά bits έπεσαν και εμφανίστηκε U+F642: code point στην Private Use Area που οι περισσότερες γραμματοσειρές αποδίδουν ως κουτί ή τίποτα
Ο form serializer είχε το αντίστροφο πρόβλημα. Φιλτράριζε χαρακτήρες έναν wchar_t τη φορά, έβλεπε δύο surrogate code units που είναι άκυρες μεμονωμένες, και τα έπεφτε και τα δύο, οπότε το emoji εξαφανιζόταν τελείως από το form packet. Στο v3.125.2 ο αποκωδικοποιητής καταναλώνει κάθε scalar τιμή πλήρως και εκπέμπει σωστό surrogate ζευγάρι. Όταν απομένει μόνο μία υποδοχή εξόδου, κρατά το low surrogate εκκρεμές και δεν αναφέρει end-of-stream όσο εκείνη η μονάδα εξακολουθεί να βρίσκεται σε buffer. Ακολουθία UTF-8 χωρισμένη ανάμεσα σε blocks ανάγνωσης μεταφέρεται στην επόμενη ανάγνωση αντί να πεταθεί. Ο form exporter τώρα κρατά τα έγκυρα surrogate ζευγάρια μαζί, και οι αριθμητικές αναφορές χαρακτήρων παράγουν σωστά ζευγάρια κι αυτά
Test δεδομένα Latin-1 δεν δείχνουν ποτέ τίποτα από αυτά, οπότε κάθε test round-trip XFA χρειάζεται τουλάχιστον έναν χαρακτήρα supplementary-plane
Single-stream XFA και αποτυχίες save που κανείς δεν είδε
Ένα single-stream XFA έγγραφο έχασε τις επεξεργασίες του επειδή ο native save helper απέρριπτε εκείνη τη διάταξη αποθήκευσης και ο καλών του αγνοούσε την αστοχία. Το ISO 32000-1 §12.7.8 επιτρέπει την εγγραφή /XFA του interactive form dictionary είτε ως array ονομάτων packets και streams είτε ως μοναδικό stream που κρατά όλο το XDP έγγραφο. Τα packet arrays είναι η συνηθισμένη περίπτωση, αλλά τα μοναδικά streams είναι απόλυτα νόμιμα, και το PDF save ολοκληρώνονταν σαν τίποτα να μην είχε συμβεί ενώ τα δεδομένα της φόρμας μένανε στις παλιές τους τιμές
Από το v3.125.2, το V8 runtime χειρίζεται το υποστηριζόμενο υποσύνολο μονού stream. Πρώτα εξάγει και τα δύο ζωντανά packets, datasets και form, σε staging περιοχή και τα επαληθεύει, και μόνο τότε αντικαθιστά τα ταιριαστά packets στο αρχικό XDP. Άλλα packets και οι δηλώσεις root namespace διατηρούνται. Αν το staging αποτύχει, το μόνιμο XFA stream δεν αγγίζεται ποτέ και το έγγραφο κρατά το σημάδι τροποποίησής του
XML comments και processing instructions χρειάζονταν επιπλέον φροντίδα επειδή το εσωτερικό XML DOM τα πετάει. Στο v3.125.2 η παρουσία τους έκανε το save να αποτυγχάνει ευθέως αντί να χάνει περιεχόμενο σιωπηλά. Το v3.126.0 τα διατηρεί: πριν το parsing, κάθε comment ή processing instruction ανταλλάσσεται με marker χτισμένο από πρόθεμα που δεν εμφανίζεται πουθενά στο αρχικό κείμενο. Αφού αντικατασταθούν τα ζωντανά packets, κάθε marker πρέπει να εμφανιστεί ακριβώς μία φορά πριν το αρχικό token επαναφερθεί και το stream γραφτεί. Tokens έξω από τα αντικατεστημένα packets κρατούν άρα το κείμενο και τη σειρά τους, συμπεριλαμβανομένων tokens στο prolog, το template και άλλα packets
Κάποιοι είσοδοι εξακολουθούν να απορρίπτονται επίτηδες, και κάθε άρνηση είναι ρητή αποτυχία save:
- Comments ή processing instructions μέσα στα ζωντανά packets
datasetsήform, αφού οι αρχικές τους θέσεις δεν μπορούν να αντιστοιχιστούν σε φρεσκοεξαγόμενο περιεχόμενο - Δηλώσεις DTD και υπογραφές XMLDSig, αφού η ξαναγραφή του XDP δεν μπορεί να κρατήσει έγκυρη XML υπογραφή
- Άκυρη κωδικοποίηση UTF-8 ή UTF-16, ημιτελή tags, άκυρες αναφορές χαρακτήρων, άγνωστα entities και παραμορφωμένες processing instructions, που απορρίπτονται αντί να επισκευαστούν σιωπηλά
Η έξοδος μονού stream είναι UTF-8 και διατηρεί το XML content μοντέλο, όχι την αρχική διάταξη bytes ή τη δήλωση κωδικοποίησης
Το τελευταίο ελάττωμα καθόταν κάτω από το XFA. Ο native file writer δουλεύει την έξοδο σε blocks 32 KB και έκανε flush το τελικό μισό block μόνο στον destructor του, αφού ο document writer είχε ήδη αναφέρει επιτυχία. Disk-full ή I/O σφάλμα σε εκείνο το τελευταίο block ήταν αόρατο στον καλούντα. Από το v3.125.2 στο V8 runtime και το v3.125.3 στο συνηθισμένο runtime, εκείνο το τελικό flush είναι μέρος του αποτελέσματος save, και το σημάδι τροποποίησης XFA καθαρίζεται μόνο μετά από πραγματική επιτυχία. Από την πλευρά Delphi, το TPdf.SaveAs(const FileName: string; Option: TSaveOption = saNone; PdfVersion: TPdfVersion = pvUnknown): Boolean γράφει σε προσωρινό αρχείο δίπλα στον στόχο και το μετακινεί στη θέση του μόνο όταν το save επιστρέψει True, οπότε αποτυχημένο save αφήνει το προηγούμενο αρχείο άθικτο
Γιατί μια dynamic XFA φόρμα ξανανοίγει με λιγότερες σελίδες;
Μια dynamic XFA φόρμα ξανανοίγει με λιγότερες σελίδες όταν το root subform της δεν δηλώνει restoreState="auto", και αυτό είναι απόφαση συγγραφής φόρμας και όχι ελάττωμα PDFium Component. Στο XFA 3.3, το restoreState στο root subform έχει προεπιλογή manual. Υπό manual, ο XFA processor επαναφέρει μόνο περιορισμένη κατάσταση από το αποθηκευμένο form packet και αφήνει τα υπόλοιπα στα scripts του συγγραφέα. Αποθηκευμένες τιμές fields και πλήθη επαναλαμβανόμενων subform instances εξακολουθούν να γυρνάνε, αλλά γεωμετρικές ιδιότητες ορισμένες runtime δεν
Η περίπτωση που το ξεσκέπασε ήταν φόρμα τριών σελίδων της οποίας το script μεγάλωσε subform σε h="450pt". Το αποθηκευμένο form packet κρατούσε το νέο ύψος, τις τιμές και τα πλήθη instances. Στο reopen όμως, το layout ξαναχτίστηκε από τα ύψη του template και η φόρμα αναδιατάχθηκε σε δύο σελίδες. Το runtime είχε δίκιο: το template δεν είχε ζητήσει ποτέ αυτόματη επαναφορά. Η δήλωσή του στο root subform διορθώνει το reopen:
<template xmlns="http://www.xfa.org/schema/xfa-template/3.3/">
<subform name="form1" layout="tb" restoreState="auto">
<pageSet>
<pageArea name="Page1">
<contentArea x="0.25in" y="0.25in" w="8in" h="10.5in"/>
<medium stock="letter"/>
</pageArea>
</pageSet>
<subform name="Details" layout="tb" w="7.5in">
<!-- fields· scripts μπορούν να αλλάξουν h ή να προσθέσουν instances runtime -->
</subform>
</subform>
</template>
Αν δεν κατέχετε το template, μην επιχειρείτε παρακάμψεις γύρω του στον viewer: φόρμα που βασίζεται σε λειτουργία manual περιμένει τα δικά της scripts να ξαναχτίσουν την κατάσταση. Ζωντανή repagination όσο πληκτρολογεί ο χρήστης είναι ξεχωριστό θέμα, που καλύπτεται στο πώς παρακολουθεί το PDFium Component dynamic XFA page counts και μετακινημένα fields
Πώς επαληθεύετε ένα XFA save στο Delphi;
Ο μόνος αξιόπιστος έλεγχος XFA save είναι να ξανανοίξετε το σωσμένο αρχείο σε φρέσκο instance TPdf και να διαβάσετε τα αποθηκευμένα δεδομένα πίσω. Το TPdf.GetXfaDatasets επιστρέφει το datasets packet όπως αποθηκεύεται στο έγγραφο, όχι το ζωντανό XFA data model, οπότε η κλήση του πριν το save δείχνει τις παλιές τιμές. Μετά το ξαναάνοιγμα δείχνει ακριβώς όσα γράφτηκαν. Έγγραφο μονού stream δεν έχει ξεχωριστά ονομασμένα packets· το PDFium αναφέρει όλο το XDP ως ένα packet με κενό όνομα, οπότε τα GetXfaPacketByName('datasets') και GetXfaDatasets δεν γυρίζουν τίποτα, και το fallback διαβάζει το πλήρες stream μέσω GetXfaFormPackets
uses
System.SysUtils, PDFium, FPdfXfa;
function ReadSavedXfaData(const FileName: string): string;
var
Pdf: TPdf;
Packets: TXfaPacketList;
Bytes: TBytes;
begin
Pdf := TPdf.Create(nil);
try
Pdf.FileName := FileName;
Pdf.Active := True;
Bytes := Pdf.GetXfaDatasets; // διάταξη packet-array
if Length(Bytes) = 0 then
begin
Packets := Pdf.GetXfaFormPackets; // μοναδικό stream: ένα ανώνυμο packet
if Length(Packets) = 1 then
begin
SetLength(Bytes, Length(Packets[0].Content));
if Length(Bytes) > 0 then
Move(Packets[0].Content[0], Bytes[0], Length(Bytes));
end;
end;
Result := TEncoding.UTF8.GetString(Bytes); // η σωσμένη XDP έξοδος είναι UTF-8
finally
Pdf.Free;
end;
end;
Η ρουτίνα save μετά δεσμεύει την εκκρεμή επεξεργασία, τσεκάρει το αποτέλεσμα SaveAs και συγκρίνει την ξανανοιγμένη τιμή. Το TPdf.ClearFormFieldFocus σκοτώνει τη form εστίαση, που είναι η στιγμή που το PDFium δεσμεύει το edit buffer του εστιασμένου field. Το TPdf.SetFocusedFormFieldText(const Value: WString): Boolean γεμίζει το εστιασμένο field προγραμματιστικά, αλλά βασίζεται σε εστίαση που παρακολουθεί ο wrapper μέσω FocusFormField, που περπατά widget annotations. Μια dynamic XFA σελίδα συνήθως δεν έχει κανένα, οπότε εκεί το κείμενο συνήθως φτάνει μέσω πληκτρολόγησης στο TPdfView, και η συνάρτηση επιστρέφει False όταν κανένα παρακολουθούμενο field δεν έχει εστίαση
function XmlText(const S: string): string;
begin
Result := StringReplace(S, '&', '&', [rfReplaceAll]);
Result := StringReplace(Result, '<', '<', [rfReplaceAll]);
end;
procedure SaveXfaAndVerify(Pdf: TPdf; const FileName, FieldTag,
Expected: string);
var
Saved: string;
begin
// Προαιρετική scripted συμπλήρωση· False σημαίνει κανένα παρακολουθούμενο field δεν έχει εστίαση
if (Pdf.FocusedFormFieldIndex >= 0) and
not Pdf.SetFocusedFormFieldText(Expected) then
raise EPdfError.Create('Could not write the focused field');
Pdf.ClearFormFieldFocus; // δέσμευση του edit buffer
if not Pdf.SaveAs(FileName) then // περιλαμβάνει το τελικό flush (v3.125.2+)
raise EPdfError.CreateFmt('Saving %s failed', [FileName]);
Saved := ReadSavedXfaData(FileName);
if Pos('<' + FieldTag + '>' + XmlText(Expected) + '</' + FieldTag + '>',
Saved) = 0 then
raise EPdfError.CreateFmt('%s did not survive the round trip', [FieldTag]);
end;
Μεταχειριστείτε το substring test ως smoke test. Ένα κενό στοιχείο ίσως σεριάζεται ως <Tag/>, attributes μπορούν να εμφανιστούν σε data elements, και escaping πέρα από & και < είναι επιλογή serializer. Για παραγωγικούς ελέγχους, φορτώστε το ξανανοιγμένο XML με πραγματικό XML parser και συγκρίνετε το text node του δεσμευμένου data element. Τρέξτε τον έλεγχο δύο φορές στη σειρά κι αυτό, επειδή το ελάττωμα newline έδειξε το πλήρες σχήμα του μόνο στη δεύτερη γενιά
Σύντομη αναφορά: checklist πιστότητας XFA save
- Στείλτε
pdfium.v8.dllαπό v3.125.2 ή νεότερο για XFA φόρμες, και v3.125.3 ή νεότερο για το συνηθισμένοpdfium.dll, ώστε η διόρθωση τελικής εγγραφής να βρίσκεται και στα δύο - Δείξτε το
LibraryNameσε πλήρη διαδρομή και ορίστεEnableV8Engineσε True· απόν διαδρομή αποτυγχάνει αντί να φορτώσει άλλο αντίγραφο - Επιβεβαιώστε
TPdf.XFAκαιTPdf.XfaRuntimeAvailableμετά το άνοιγμα του εγγράφου - Καλέστε
ClearFormFieldFocusπριν τοSaveAsώστε το εστιασμένο field να δεσμευτεί - Μην αγνοείτε ποτέ το Boolean αποτέλεσμα του
SaveAs· αποτέλεσμα False αφήνει το προηγούμενο αρχείο στη θέση του - Επαληθεύστε ξανανοίγοντας σε νέο
TPdfκαι διαβάζονταςGetXfaDatasets, με fallback στοGetXfaFormPacketsγια single-stream XFA - Τεστάρετε με κενές τιμές, αρχικά κενά, κείμενο πολλών γραμμών,
&και χαρακτήρα supplementary-plane, σε δύο γενιές save - Περιμένετε ρητές αποτυχίες save για DTD, XMLDSig και comments μέσα στα ζωντανά packets του single-stream XFA
- Αν dynamic φόρμα χάνει runtime γεωμετρία στο reopen, τσεκάρετε το root subform για
restoreState="auto"πριν υποψιαστείτε τη βιβλιοθήκη
Για τη δομή callbacks που περιμένει το XFA runtime από εφαρμογή host, δείτε FPDF_FORMFILLINFO version 2 και το XFA ABI στο Delphi. Το V8 runtime, ο Delphi και C++Builder wrapper και το viewer control είναι όλα μέρος του PDFium Component for Delphi and C++Builder, που περιλαμβάνει και τα δύο Windows runtimes για Win32 και Win64