Η μετακίνηση ενός μπλοκ πεδίων φόρμας από το πρότυπο πέρυσι στη διάταξη φέτος είναι εκεί όπου τα round-trips FDF και XFDF παύουν να αρκούν: οι τιμές φτάνουν, αλλά τα appearance streams, οι ενέργειες υπολογισμού και οι προεπιλεγμένοι πόροι όχι. Το PDFiumPas απαντά σε αυτή την περίπτωση με το GraftPdfAcroForm, που κλωνοποιεί ολόκληρο το γράφημα αντικειμένων πεδίων από ένα PDF και το γράφει σε άλλο
Ο λόγος που μια εξαγωγή επιπέδου δεδομένων δεν μπορεί να το κάνει αυτό είναι δομικός. Ένα πεδίο δεν είναι εγγραφή, είναι υπογράφημα. Το ISO 32000-1 §12.7 ορίζει το λεξικό διαδραστικής φόρμας που κρατά /Fields, /CO, /DR και /DA, το §12.7.3 ορίζει τα λεξικά πεδίων που κρέμονται από κάτω του, και το §12.5.6.19 ορίζει τις επισημειώσεις widget που δίνουν σε αυτά τα πεδία ένα ορατό πλαίσιο σε σελίδα. Το XFDF μεταφέρει τα φύλλα αυτής της δομής. Η μεταμόσχευση μεταφέρει την ίδια τη δομή
Γιατί η αντιγραφή του πίνακα /Fields δεν είναι ποτέ αρκετή
Η αντιγραφή του /Fields από ένα έγγραφο σε άλλο παράγει μια φόρμα χαλασμένη με κάθε ενδιαφέροντα τρόπο, επειδή ο πίνακας κρατά έμμεσες αναφορές και τίποτα άλλο. Το ISO 32000-1 §7.3.10 κάνει ένα έμμεσο αντικείμενο διευθυνσιοδοτήσιμο με αριθμό αντικειμένου συν γενιά, και αυτοί οι αριθμοί έχουν νόημα μόνο μέσα στο αρχείο από το οποίο προήλθαν. Επικολλήστε τον πίνακα αλλού και κάθε αναφορά σε αυτόν είτε κρέμεται είτε, χειρότερα, επιλύεται σιωπηλά σε άσχετο αντικείμενο που τυχαίνει να καταλαμβάνει αυτή τη θέση στον προορισμό. Κάτω από κάθε αναφορά κάθεται ένα γράφημα που είναι ταυτόχρονα κοινόχρηστο και κυκλικό. Ένα λεξικό πεδίου δείχνει στα παιδιά του, κάθε παιδί δείχνει πίσω στο /Parent του, ένα widget δείχνει στα appearance streams του και στη σελίδα που το μεταφέρει μέσω /P, τα appearance streams δείχνουν σε γραμματοσειρές στο προεπιλεγμένο λεξικό πόρων της φόρμας, και τα λεξικά additional-action κάτω από /AA δείχνουν σε ακόμη περισσότερα αντικείμενα. Δύο widgets σε διαφορετικές σελίδες μοιράζονται κανονικά μία γραμματοσειρά και ένα appearance XObject. Έτσι μια σωστή μεταμόσχευση πρέπει να διασχίσει αυτό το γράφημα, να κλωνοποιήσει κάθε προσπελάσιμο αντικείμενο ακριβώς μία φορά, να αναπροσανατολίσει το /P κάθε widget στη χαρτογραφημένη σελίδα προορισμού, και να προσθέσει το κλωνοποιημένο widget στον πίνακα /Annots της σελίδας — αλλιώς το πεδίο υπάρχει στη φόρμα και είναι αόρατο στη σελίδα. Αν έχετε κυνηγήσει τη διαφορά ανάμεσα σε ένα πεδίο, το widget του και την επισημείωση σελίδας που το εμφανίζει, η σημείωσή μας για το widget index έναντι annotation index καλύπτει ακριβώς αυτό το διαχωρισμό
Τι χρειάζεται το GraftPdfAcroForm από εσάς;
Χρειάζεται τρεις διακριτές ροές και μια ρητή αντιστοίχιση σελίδων. Το GraftPdfAcroForm παίρνει Source, Destination και Output ως ξεχωριστά στιγμιότυπα TStream, έναν πίνακα TPdfGraftPageMappings, μια εγγραφή TPdfAcroFormGraftOptions, ένα προαιρετικό TPdfCrossDocumentGraftMap και ένα out TPdfAcroFormGraftReport. Επιστρέφει Boolean αντί να προκαλεί εξαίρεση, και σε αποτυχία η αναφορά μεταφέρει τον λόγο στο ErrorMessage. Η αντιστοίχιση σελίδων είναι με βάση τη μονάδα και στις δύο πλευρές και δεν συνάγεται: κάθε σελίδα πηγής που μεταφέρει widget που σκοπεύετε να μεταμοσχεύσετε πρέπει να εμφανίζεται σε αυτήν. Η παράδοση nil για τον χάρτη graft είναι νόμιμη — η συνάρτηση τότε δημιουργεί και απελευθερώνει έναν ιδιωτικό για τη διάρκεια της κλήσης — και το TPdfAcroFormGraftOptions.Default σας δίνει CollisionPolicy σε pagcpReject, RenamePrefix σε Imported_, MaxObjects 100000, MaxDepth 128 και AllowSignedDestination σε False. Τα τελευταία τρία είναι προϋπολογισμοί, και υπάρχουν επειδή το γράφημα αντικειμένων που πρόκειται να διασχίσετε προήλθε από αρχείο που δεν γράψατε εσείς
uses
Classes, SysUtils, FPdfCompress;
var
Source, Destination, Output: TMemoryStream;
Options: TPdfAcroFormGraftOptions;
Mappings: TPdfGraftPageMappings;
Report: TPdfAcroFormGraftReport;
begin
Source := TMemoryStream.Create;
Destination := TMemoryStream.Create;
Output := TMemoryStream.Create;
try
Source.LoadFromFile('claim-template-2025.pdf');
Destination.LoadFromFile('claim-layout-2026.pdf');
Source.Position := 0;
Destination.Position := 0;
Options := TPdfAcroFormGraftOptions.Default;
SetLength(Mappings, 2);
Mappings[0].SourcePageNumber := 1;
Mappings[0].DestinationPageNumber := 1;
Mappings[1].SourcePageNumber := 2;
Mappings[1].DestinationPageNumber := 3;
if GraftPdfAcroForm(Source, Destination, Output, Mappings,
Options, nil, Report) then
Output.SaveToFile('claim-2026-with-fields.pdf')
else
raise Exception.Create(Report.ErrorMessage);
finally
Output.Free;
Destination.Free;
Source.Free;
end;
end;
Πώς ο χάρτης graft αποφεύγει τη διπλή κλωνοποίηση κοινής γραμματοσειράς;
Το TPdfCrossDocumentGraftMap κρατά έναν πίνακα αναφορών πηγής-προς-προορισμό των οποίων τα κλειδιά μεταφέρουν αριθμό αντικειμένου και γενιά, και ο αναδρομικός cloner τον συμβουλεύεται πριν κατέβει. Η σειρά των λειτουργιών είναι αυτή που κάνει τους κύκλους ασφαλείς: ο cloner δεσμεύει τον αριθμό αντικειμένου προορισμού και καταχωρεί την αντιστοίχιση πρώτα, και μετά διασχίζει τις αναφορές παιδιών του αντικειμένου πηγής. Ένας γονέας που φτάνει σε παιδί που δείχνει πίσω στον γονέα του βρίσκει τον γονέα ήδη καταχωρημένο και επιστρέφει την υπάρχουσα αναφορά προορισμού αντί να αναδύεται. Η ίδια αναζήτηση είναι αυτή που κάνει μια γραμματοσειρά, ένα appearance stream ή μια ενέργεια που μοιράζονται έξι widgets να κλωνοποιηθεί μία φορά και να αναφερθεί έξι φορές. Ο χάρτης δένεται με το έγγραφο πηγής μέσω hash SHA-256 των bytes της πηγής, εκτεθειμένο ως SourceIdentity. Αν δώσετε στο GraftPdfAcroForm χάρτη του οποίου η ταυτότητα δεν ταιριάζει με την πηγή που περάσατε, αρνείται την κλήση αντί να επαναχρησιμοποιήσει αναφορές που δεν ήταν ποτέ έγκυρες για αυτό το αρχείο. Οι αντιστοιχίσεις σελίδων σπείρονται στον ίδιο χάρτη πριν ξεκινήσει η κλωνοποίηση, που είναι ακριβώς το πώς το /P ενός widget καταλήγει να δείχνει στη σελίδα προορισμού: το αντικείμενο σελίδας πηγής επιλύεται ήδη στο χαρτογραφημένο αντικείμενο σελίδας προορισμού, οπότε το συνηθισμένο πέρασμα επανεγγραφής αναφορών το χειρίζεται χωρίς ειδική περίπτωση
uses
Classes, SysUtils, FPdfCompress, FPdfSha256;
var
GraftMap: TPdfCrossDocumentGraftMap;
SourceBytes: TBytes;
EntriesBefore: Integer;
begin
SetLength(SourceBytes, Source.Size);
Source.Position := 0;
if Length(SourceBytes) > 0 then
Source.ReadBuffer(SourceBytes[0], Length(SourceBytes));
GraftMap := TPdfCrossDocumentGraftMap.Create(
AnsiString(SHA256Hex(SHA256Bytes(SourceBytes))));
try
EntriesBefore := GraftMap.Count;
Source.Position := 0;
if not GraftPdfAcroForm(Source, Destination, Output, Mappings,
Options, GraftMap, Report) then
begin
// Οι εγγραφές που πρόσθεσε αυτή η κλήση έχουν ανακτηθεί·
// οτιδήποτε καταχωρήθηκε πριν από αυτήν παραμένει ανέπαφο.
Assert(GraftMap.Count = EntriesBefore);
WriteLn('graft refused: ', Report.ErrorMessage);
end;
finally
GraftMap.Free;
end;
end;
Αυτή η αναίρεση είναι το νόημα του να κατέχετε τον χάρτη οι ίδιοι. Το PDFiumPas αντιμετωπίζει έναν χάρτη που παρέχει ο καλών συναλλαγματικά: μια αποτυχημένη μεταμόσχευση απορρίπτει τις εγγραφές που πρόσθεσε εκείνη η κλήση και κρατά κάθε αντιστοίχιση που υπήρχε προηγουμένως, ώστε μία άρνηση να μην αφήνει ποτέ πίσω της cache αναφορών σε αντικείμενα που δεν γράφτηκαν ποτέ. Κρατήστε έναν χάρτη ανά έγγραφο προορισμού πάντως — η πλευρά προορισμού κάθε εγγραφής είναι ένας αριθμός αντικειμένου σε αυτό το συγκεκριμένο αρχείο, και δεν σημαίνει τίποτα σε διαφορετικό
Συγκρούσεις ονομάτων πεδίων: απόρριψη ή μετονομασία
Τα πλήρως προσδιορισμένα ονόματα πεδίων πρέπει να μένουν μοναδικά μέσα σε μια φόρμα, και το PDFiumPas δεν θα μαντέψει τι εννοούσατε όταν συγκρουστούν. Το TPdfAcroFormCollisionPolicy προσφέρει ακριβώς δύο απαντήσεις. Υπό pagcpReject, την προεπιλογή, το πρώτο πεδίο πηγής του οποίου ο τίτλος υπάρχει ήδη στον προορισμό ματαίνει ολόκληρη τη μεταμόσχευση με σφάλμα και αφήνει τη ροή εξόδου κενή. Υπό pagcpRename, το συγκρουόμενο πεδίο πηγής μετονομάζεται με πρόθεμα το RenamePrefix και η μεταμόσχευση συνεχίζει, με το Report.RenamedFieldCount να σας λέει πόσες φορές έγινε αυτό
Options := TPdfAcroFormGraftOptions.Default;
Options.CollisionPolicy := pagcpRename;
Options.RenamePrefix := 'Y2025_';
Options.MaxObjects := 20000;
Options.MaxDepth := 64;
if GraftPdfAcroForm(Source, Destination, Output, Mappings,
Options, nil, Report) then
begin
WriteLn('source fields : ', Report.SourceFieldCount);
WriteLn('existing fields: ', Report.DestinationFieldCount);
WriteLn('grafted fields : ', Report.GraftedFieldCount);
WriteLn('renamed fields : ', Report.RenamedFieldCount);
WriteLn('cloned objects : ', Report.GraftedObjectCount);
WriteLn('reused objects : ', Report.ReusedObjectCount);
WriteLn('mapped pages : ', Report.MappedPageCount);
WriteLn('output bytes : ', Report.OutputByteCount);
end
else
WriteLn('graft refused : ', Report.ErrorMessage);
Η μετονομασία δεν είναι δωρεάν, και πρέπει να την αποφασίζετε σκόπιμα αντί να την αρπάζετε για να εξαφανίσετε ένα σφάλμα. Ένα μετονομασμένο πεδίο είναι διαφορετικό πεδίο: οποιαδήποτε JavaScript στον προορισμό που το προσφωνεί με όνομα, οποιαδήποτε εγγραφή υπολογισμού στο /CO που έγραψε κάποιος με το παλιό όνομα, και οποιοσδήποτε downstream καταναλωτής που κλειδώνει στο όνομα πεδίου θα χρειαστεί να μάθει για το πρόθεμα. Αν τα δύο έγγραφα περιγράφουν πραγματικά το ίδιο πεδίο, η ειλικρινής διόρθωση είναι συνήθως η συμφωνία των ονομάτων ανάντη, όχι τη στιγμή της μεταμόσχευσης. Μόλις προσγειωθεί η μεταμόσχευση, η διάσχιση της συγχωνευμένης φόρμας για επιβεβαίωση του τι πραγματικά πήρατε είναι το φυσικό επόμενο βήμα, και η πλοήγηση σε πεδία φόρμας στο PDFiumPas καλύπτει αυτή τη διάσχιση
Πού αποτυγχάνει σκόπιμα με κλείσιμο η μεταμόσχευση
Κάθε ασαφής συνθήκη είναι σφάλμα, ποτέ αποτέλεσμα καλής πίστης, και αυτή είναι απόφαση σχεδίασης που αξίζει να κατανοήσετε πριν σας εκπλήξει στην παραγωγή. Το GraftPdfAcroForm επιστρέφει False, επαναφέρει τη ροή εξόδου και αναφέρει τον λόγο όταν πέσει σε οποιαδήποτε από αυτές
- Η φόρμα πηγής μεταφέρει εγγραφή
/XFA— τα πακέτα XFA είναι ένα παράλληλο μοντέλο φόρμας και δεν μπορούν να αναχθούν σε λεξικά πεδίων AcroForm - Ένα widget ζει σε σελίδα πηγής που δεν έχει εγγραφή στην αντιστοίχιση σελίδων, που αλλιώς θα πετούσε σιωπηλά το πεδίο ή θα το έσυρε σε λάθος σελίδα
- Οι αντιστοιχίσεις σελίδων είναι εκτός εύρους, ή δύο αντιστοιχίσεις επαναχρησιμοποιούν την ίδια σελίδα πηγής ή προορισμού
- Και οι δύο φόρμες ορίζουν προεπιλεγμένο λεξικό πόρων
/DR, επειδή η συγχώνευση δύο χώρων ονομάτων πόρων θα διακυνδύνευε να επαναπροσανατολίσει υπάρχον όνομα σε διαφορετική γραμματοσειρά - Το γράφημα αντικειμένων ξεπερνά το
MaxObjectsή η αναδρομή ξεπερνά τοMaxDepth - Ο προορισμός περιέχει υπογραφή και το
AllowSignedDestinationείναιFalse - Ο δοσμένος χάρτης graft ανήκει σε διαφορετικό έγγραφο πηγής, ή μια αναφορά πηγής κρέμεται
Η διαδρομή εγγραφής είναι εξίσου συντηρητική. Το PDFiumPas εκπέμπει το αποτέλεσμα ως αραιή επαυξημένη αναθεώρηση προσαρτημένη στον προορισμό, και μετά επαναϋλοποιεί τη γραμμένη έξοδο και ξαναδιαβάζει τη φόρμα της: αν το πλήθος πεδίων του αποτελέσματος δεν ισούται με το αρχικό πλήθος πεδίων του προορισμού συν αυτό της πηγής, ολόκληρη η μεταμόσχευση απορρίπτεται και η έξοδος καθαρίζεται. Δεν παίρνετε ποτέ μερικώς μεταμοσχευμένο αρχείο. Το κόστος αυτής της πολιτικής είναι υπαρκτό — μια σύγκρουση /DR ή ένας υπογεγραμμένος προορισμός σας σταματά αμέσως, και πρέπει να το επιλύσετε οι ίδιοι αντί να δεχτείτε μια συγχωνευμένη προσέγγιση — αλλά η εναλλακτική είναι μια φόρμα που ανοίγει μια χαρά και υπολογίζει λάθος
Πότε η μεταμόσχευση είναι λάθος εργαλείο
Η μεταμόσχευση μετακινεί δομή, οπότε χρησιμοποιήστε την όταν η δομή είναι αυτό που σας λείπει. Αν και τα δύο έγγραφα μεταφέρουν ήδη το ίδιο σύνολο πεδίων και χρειάζεστε μόνο να μετακινήσετε τιμές και επισημειώσεις ανάμεσά τους, η διαδρομή εξαγωγής και εισαγωγής στο άρθρο δεδομένων φόρμας XFDF είναι ελαφρύτερη, τυπική και αναστρέψιμη. Απευθυνθείτε στο GraftPdfAcroForm όταν ο προορισμός δεν έχει καθόλου πεδία, ή έχει διαφορετικό σύνολο, και χρειάζεστε τα widgets, τα appearance streams, τις ενέργειες και τη σειρά υπολογισμού να έρθουν ανέπαφα. Μια τελευταία πρακτική σημείωση για την ταυτότητα: επειδή ο χάρτης graft κλειδώνει σε αριθμό αντικειμένου συν γενιά και δένεται με SHA-256 των bytes της πηγής, η επαποθήκευση ή η βελτιστοποίηση της πηγής μεταξύ εκτελέσεων παράγει διαφορετική ταυτότητα και χάρτη που δεν ισχύει πλέον. Κρατήστε στιγμιότυπο της πηγής από την οποία μεταμοσχεύετε και κρατήστε τη σταθερή για τη δέσμη· αντιμετωπίστε την ως τεχνούργημα εισόδου, όχι ως κάτι που μια νυχτερινή εργασία μπορεί ελεύθερα να ξαναγράψει
Το GraftPdfAcroForm, το TPdfCrossDocumentGraftMap και η γύρω εργαλειοθήκη PDF επιπέδου ροής έρχονται με το PDFiumPas Delphi PDFium Component για Delphi, C++Builder και Lazarus, όπου η σελίδα προϊόντος μεταφέρει ολόκληρη την αναφορά API για τις επιλογές graft, τα πεδία αναφοράς και την υπόλοιπη επιφάνεια επεξεργασίας εγγράφων