Articol tehnic

Câmpuri AcroForm grefate între PDF-uri în Delphi: PDFiumPas

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

Graful de obiecte din spatele unui câmp de formular PDF pe când PDFiumPas îl grefează în Delphi: dicționarul de formular, câmpul, adnotările widget, tablourile de adnotări ale paginii de destinație și fluxul de aparență plus fontul pe care ambele widget-uri le partajează, plus referința înapoi spre părinte care închide ciclul
Un câmp e un subgraf ciclic partajat, motiv pentru care copierea tabloului /Fields între documente lasă fiecare referință atârnată

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

Harta de grefare cross-document PDFiumPas în Delphi cheiază fiecare referință sursă după număr de obiect și generație, înregistrează maparea de destinație înainte de a coborî astfel încât o referință înapoi spre părinte se termină, și întoarce intrarea existentă astfel încât un font partajat se clonează doar o dată
Înregistrarea mapării înainte de a parcurge copiii e ceea ce face un graf ciclic sigur și un obiect partajat clonat exact o dată
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 MaxObjects sau recursivitatea depășește MaxDepth
  • Destinația conține o semnătură și AllowSignedDestination e False
  • 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

Cum eșuează închis PDFiumPas GraftPdfAcroForm în Delphi: revizia scrisă e recitită iar numărul ei de câmpuri verificat, orice condiție ambiguă precum XFA sau o pagină nemapată refuză apelul, iar un refuz aruncă doar intrările de hartă pe care acel apel le-a adăugat
Calea de scriere verificată și harta tranzacțională sunt motivul pentru care o grefare refuzată nu lasă niciodată în urmă un fișier fuzionat parțial

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