Mutarea unui bloc de câmpuri de formular de pe șablonul anului trecut pe layoutul anului acestuia e locul unde drumurile dus-întors FDF și XFDF nu mai ajung: valorile sosesc, dar fluxurile de aparență, acțiunile de calcul și resursele implicite nu. PDFiumPas răspunde acestui caz cu GraftPdfAcroForm, care clonează întregul graf de obiecte al câmpurilor dintr-un PDF și îl scrie în altul
Motivul pentru care un export la nivel de date nu poate face asta e structural. Un câmp nu e o înregistrare, e un subgraf. ISO 32000-1 §12.7 definește dicționarul de formular interactiv care deține /Fields, /CO, /DR și /DA, §12.7.3 definește dicționarele de câmpuri atârnând dedesubt, iar §12.5.6.19 definește adnotările widget care dau acelor câmpuri o cutie vizibilă pe o pagină. XFDF duce frunzele acestei structuri. Grefarea duce structura însăși
De ce copierea tabloului /Fields nu ajunge niciodată
Copierea /Fields dintr-un document în altul produce un formular stricat în toate felurile interesante, pentru că tabloul deține referințe indirecte și nimic altceva. ISO 32000-1 §7.3.10 face un obiect indirect adresabil prin număr de obiect plus generație, iar acele numere au sens doar în interiorul fișierului din care provin. Lipiți tabloul peste și fiecare referință din el fie atârnă, fie, mai rău, se rezolvă tăcut la un obiect fără legătură care se întâmplă să ocupe acel slot în destinație. Dedesubtul fiecărei referințe stă un graf care e și partajat, și ciclic. Un dicționar de câmp indică spre copiii lui, fiecare copil indică înapoi spre /Parent, un widget indică spre fluxurile lui de aparență și spre pagina care îl duce prin /P, fluxurile de aparență indică spre fonturi în dicționarul de resurse implicit al formularului, iar dicționarele de acțiuni suplimentare sub /AA indică spre și mai multe obiecte. Două widget-uri pe pagini diferite partajează de rutină un font și un XObject de aparență. Deci o grefare corectă trebuie să parcurgă acel graf, să cloneze fiecare obiect accesibil exact o dată, să redirecționeze /P-ul fiecărui widget spre pagina de destinație mapată și să adauge widget-ul clonat în tabloul /Annots al paginii — altfel câmpul există în formular și e invizibil pe pagină. Dacă ați urmărit diferența dintre un câmp, widget-ul lui și adnotarea de pagină care îl afișează, nota noastră despre index de widget versus index de adnotare acoperă exact despărțirea asta
De ce are nevoie GraftPdfAcroForm din partea dumneavoastră?
Are nevoie de trei fluxuri distincte și de o mapare explicită de pagini. GraftPdfAcroForm ia Source, Destination și Output ca instanțe TStream separate, un tablou TPdfGraftPageMappings, o înregistrare TPdfAcroFormGraftOptions, un TPdfCrossDocumentGraftMap opțional și un TPdfAcroFormGraftReport de ieșire. Întoarce Boolean în loc să ridice excepție, iar la eșec raportul duce motivul în ErrorMessage. Maparea de pagini e cu bază unu pe ambele părți și nu e dedusă: fiecare pagină sursă care poartă un widget pe care intenționați să-l grefați trebuie să apară în ea. Trecerea lui nil pentru harta de grefare e legitimă — funcția creează și eliberează atunci una privată pe durata apelului — iar TPdfAcroFormGraftOptions.Default vă dă CollisionPolicy stabilit la pagcpReject, RenamePrefix stabilit la Imported_, MaxObjects de 100000, MaxDepth de 128 și AllowSignedDestination stabilit la False. Ultimele trei sunt bugete și există pentru că graful de obiecte pe care sunteți pe cale să-l parcurgeți vine dintr-un fișier pe care nu l-ați scris dumneavoastră
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;
Cum evită harta de grefare clonarea dublă a unui font partajat?
TPdfCrossDocumentGraftMap deține o tabelă de referințe sursă-către-destinație ale cărei chei poartă atât numărul de obiect, cât și generația, iar clonerul recursiv o consultă înainte de a coborî. Ordinea operațiunilor e ceea ce face ciclurile sigure: clonerul alocă numărul de obiect de destinație și înregistrează maparea întâi, apoi parcurge referințele copil ale obiectului sursă. Un părinte care ajunge la un copil care indică înapoi spre părintele lui găsește părintele deja înregistrat și întoarce referința de destinație existentă în loc să recurgă. Aceeași căutare e ceea ce face ca un font, un flux de aparență sau o acțiune partajate de șase widget-uri să fie clonate o dată și referențiate de șase ori. Harta e legată de documentul sursă printr-un hash SHA-256 al octeților sursă, expus ca SourceIdentity. Dacă îi dați lui GraftPdfAcroForm o hartă a cărei identitate nu se potrivește cu sursa trecută, refuză apelul în loc să refolosească referințe care n-au fost niciodată valide pentru acest fișier. Mapările de pagini sunt semănate în aceeași hartă înainte de a începe clonarea, iar asta e exact felul în care /P-ul unui widget ajunge să indice spre pagina de destinație: obiectul paginii sursă se rezolvă deja la obiectul paginii de destinație mapate, deci trecerea obișnuită de rescriere a referințelor îl tratează fără niciun caz special
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
// Intrările adăugate de acest apel au fost anulate;
// orice înregistrat înaintea lui e încă intact.
Assert(GraftMap.Count = EntriesBefore);
WriteLn('graft refused: ', Report.ErrorMessage);
end;
finally
GraftMap.Free;
end;
end;
Anularea aceea e punctul de a deține dumneavoastră înșivă harta. PDFiumPas tratează o hartă furnizată de apelant tranzacțional: o grefare eșuată aruncă intrările pe care acel apel le-a adăugat și păstrează fiecare mapare existentă înainte, astfel încât un refuz nu lasă niciodată în urmă un cache de referințe către obiecte care n-au fost niciodată scrise. Totuși, țineți o hartă per document de destinație — partea de destinație a fiecărei intrări e un număr de obiect în acel fișier anume și nu înseamnă nimic într-un altul
Coliziuni de nume de câmp: respingere sau redenumire
Numele de câmpuri complet calificate trebuie să rămână unice în interiorul unui formular, iar PDFiumPas nu va ghici ce ați vrut să spuneți când se ciocnesc. TPdfAcroFormCollisionPolicy oferă exact două răspunsuri. Sub pagcpReject, implicitul, primul câmp sursă al cărui titlu există deja în destinație abandonează întreaga grefare cu o eroare și lasă fluxul de ieșire gol. Sub pagcpRename, câmpul sursă în coliziune e redenumit prin prefixarea cu RenamePrefix și grefarea continuă, cu Report.RenamedFieldCount spunându-vă cât de des s-a întâmplat asta
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);
Redenumirea nu e gratuită și ar trebui să o decideți deliberat, nu să alergați spre ea ca să faceți o eroare să dispară. Un câmp redenumit e un alt câmp: orice JavaScript în destinație care îl adresează după nume, orice intrare de calcul în /CO scrisă de un om contra numelui vechi și orice consumator din aval care cheiază pe numele câmpului vor trebui să afle despre prefix. Dacă cele două documente descriu efectiv același câmp, remedierea onestă e de regulă reconcilierea numelor în amonte, nu la momentul grefării. Odată ce grefarea a aterizat, parcurgerea formularului îmbinat pentru a confirma ce ați obținut efectiv e pasul următor natural, iar navigarea câmpurilor de formular în PDFiumPas acoperă parcurgerea aceea
Unde grefarea eșuează deliberat închisă
Fiecare condiție ambiguă e o eroare, niciodată un rezultat de tip cel mai bun efort, și asta e o decizie de design demnă de înțeles înainte să vă surprindă în producție. GraftPdfAcroForm întoarce False, resetează fluxul de ieșire și raportează motivul când lovește oricare dintre acestea
- Formularul sursă poartă o intrare
/XFA— pachetele XFA sunt un model de formular paralel și nu pot fi reduse la dicționare de câmpuri AcroForm - Un widget trăiește pe o pagină sursă fără intrare în maparea de pagini, ceea ce altfel ar arunca tăcut câmpul sau l-ar atașa paginii greșite
- Mapările de pagini sunt în afara intervalului sau două mapări refolosesc aceeași pagină sursă sau de destinație
- Ambele formulare definesc un dicționar de resurse implicit
/DR, pentru că fuzionarea a două spații de nume de resurse ar risca repointarea unui nume existent spre un alt font - Graful de obiecte depășește
MaxObjectssau recursivitatea depășeșteMaxDepth - Destinația conține o semnătură și
AllowSignedDestinationeFalse - Harta de grefare furnizată aparține unui alt document sursă sau o referință sursă atârnă
Calea de scriere e la fel de conservatoare. PDFiumPas emite rezultatul ca o revizie incrementală sparse adăugată destinației, apoi re-materializează ieșirea scrisă și recitește formularul ei: dacă numărul de câmpuri al rezultatului nu e egal cu numărul original de câmpuri al destinației plus cel al sursei, întreaga grefare e respinsă iar ieșirea e golită. Nu primiți niciodată un fișier grefat parțial. Costul acestei politici e real — o coliziune /DR sau o destinație semnată vă oprește categoric, și trebuie să o rezolvați dumneavoastră în loc să accepți o aproximare fuzionată — dar alternativa e un formular care se deschide bine și calculează greșit
Când grefarea e unealta greșită
Grefarea mută structură, deci folosiți-o când structura e ceea ce vă lipsește. Dacă ambele documente poartă deja același set de câmpuri și aveți nevoie doar să mutați valori și adnotări între ele, calea de export și import din articolul despre date de formular XFDF e mai ușoară, standard și reversibilă. Alegeți GraftPdfAcroForm când destinația n-are deloc câmpuri sau are un alt set, și aveți nevoie ca widget-urile, fluxurile de aparență, acțiunile și ordinea de calcul să vină intacte. O ultimă notă practică despre identitate: cum harta de grefare cheiază pe număr de obiect plus generație și e legată de un SHA-256 al octeților sursă, resalvarea sau optimizarea sursei între rulări produce o identitate diferită și o hartă care nu se mai aplică. Faceți un snapshot al sursei de la care grefați și păstrați-o stabilă pentru lot; tratați-o ca pe un artefact de intrare, nu ca pe ceva ce un job nocturn e liber să rescrie
GraftPdfAcroForm, TPdfCrossDocumentGraftMap și trusa PDF la nivel de flux din jur sosesc cu PDFiumPas Delphi PDFium Component pentru Delphi, C++Builder și Lazarus, unde pagina de produs duce referința API completă pentru opțiunile de grefare, câmpurile de raport și restul suprafeței de editare a documentelor