Teknisk artikel

AcroForm-felter på tværs af PDF-filer i Delphi i PDFiumPas

At flytte en blok formularfelter fra sidste års skabelon over på dette års layout er det sted, hvor FDF- og XFDF-rundture ikke længere er nok: værdierne ankommer, men appearance-streams, beregningshandlinger og standardressourcer gør ikke. PDFiumPas besvarer det tilfælde med GraftPdfAcroForm, som kloner hele feltobjektgrafen ud af én PDF og skriver den ind i en anden

Grunden til, at en dataeksport på værdiniveau ikke kan gøre dette, er strukturel. Et felt er ikke en post, det er en delgraf. ISO 32000-1 §12.7 definerer den interaktive formularordbog, der holder /Fields, /CO, /DR og /DA, §12.7.3 definerer feltordbøgerne, der hænger under den, og §12.5.6.19 definerer widgetannoteringerne, der giver felterne en synlig boks på en side. XFDF bærer bladene af den struktur. Transplantation bærer strukturen selv

Hvorfor det aldrig er nok at kopiere /Fields-arrayet

At kopiere /Fields fra ét dokument ind i et andet giver en formular, der er i stykker på alle de interessante måder, fordi arrayet holder indirekte referencer og intet andet. ISO 32000-1 §7.3.10 gør et indirekte objekt adresserbart ved objektnummer plus generation, og de tal er kun meningsfulde inde i den fil, de kom fra. Indsæt arrayet på tværs, og hver reference i det hænger enten i luften eller — værre — opløses stille til et ikke-relateret objekt, der tilfældigvis optager den plads i destinationen. Under hver reference sidder en graf, der både er delt og cyklisk. En feltordbog peger på sine børn, hvert barn peger tilbage på sin /Parent, en widget peger på sine appearance-streams og på siden, der bærer den, gennem /P, appearance-streams peger på fonts i formens standardressourceordbog, og additional-action-ordbøger under /AA peger på endnu flere objekter. To widgets på forskellige sider deler rutinemæssigt én font og én appearance-XObject. Så en korrekt transplantation er nødt til at gå den graf igennem, klone hvert nåeligt objekt præcis én gang, omskrive hver widgets /P til den tilknyttede destinationsside og tilføje den klonede widget til den sides /Annots-array — ellers findes feltet i formularen og er usynligt på siden. Hvis du har jaget forskellen mellem et felt, dets widget og sideannoteringen, der viser det, dækker vores note om widget-indeks versus annoteringsindeks netop det skel

Objektgrafen bag ét PDF-formularfelt, mens PDFiumPas transplanterer det i Delphi: formularordbogen, feltet, widgetannoteringerne, destinationssidens annoteringsarrays og den appearance-stream og font, som begge widgets deler, plus forældre-referencen, der lukker cyklussen
Et felt er en delt cyklisk delgraf, og det er derfor, det at kopiere /Fields-arrayet på tværs af dokumenter efterlader hver reference hængende

Hvad skal GraftPdfAcroForm have fra dig?

Den skal bruge tre adskilte streams og en eksplicit sidetilknytning. GraftPdfAcroForm tager Source, Destination og Output som separate TStream-instanser, et TPdfGraftPageMappings-array, en TPdfAcroFormGraftOptions-record, et valgfrit TPdfCrossDocumentGraftMap og en out-TPdfAcroFormGraftReport. Den returnerer Boolean i stedet for at rejse, og ved fejl bærer rapporten grunden i ErrorMessage. Sidetilknytningen er én-baseret på begge sider og udledes ikke: hver kildeside, der bærer en widget, du vil transplantere, skal optræde i den. At sende nil som graft map er legitimt — funktionen opretter og frigør så en privat i kaldets varighed — og TPdfAcroFormGraftOptions.Default giver dig CollisionPolicy sat til pagcpReject, RenamePrefix sat til Imported_, MaxObjects på 100000, MaxDepth på 128 og AllowSignedDestination sat til False. De sidste tre er budgetter, og de findes, fordi den objektgraf, du er ved at gå igennem, kom fra en fil, du ikke har skrevet

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;

Hvordan undgår graft-mappen at klone en delt font to gange?

TPdfCrossDocumentGraftMap holder en kilde-til-destination-referencetabel, hvis nøgler bærer både objektnummer og generation, og den rekursive kloner konsulterer den, før den går ned ad. Rækkefølgen af operationer er det, der gør cyklusser sikre: kloneren allokerer destinationsobjektnummeret og registrerer tilknytningen først, går derefter kildeobjektets børnreferencer igennem. En forælder, der når et barn, som peger tilbage på sin forælder, finder forælderen allerede registreret og returnerer den eksisterende destinationsreference i stedet for at rekursere. Det samme opslag er det, der får en font, en appearance-stream eller en handling, der deles af seks widgets, til at blive klonet én gang og refereret seks gange. Mappen bindes til kildedokumentet ved en SHA-256-hash af kildebytene, eksponeret som SourceIdentity. Hvis du rækker GraftPdfAcroForm et map, hvis identitet ikke matcher den kilde, du sendte, afviser den kaldet i stedet for at genbruge referencer, der aldrig var gyldige for denne fil. Sidetilknytningerne sædes ind i samme map, før kloningen begynder, hvilket er præcis, hvordan en widgets /P ender med at pege på destinationssiden: kildeobjektet for siden opløser allerede til det tilknyttede destinationssideobjekt, så det almindelige referenceomskrivningstrin håndterer det uden særlige tilfælde

PDFiumPas graft-mappen på tværs af dokumenter i Delphi nøgler hver kildereference efter objektnummer og generation, registrerer destinationstilknytningen, før den går ned ad, så en tilbage-reference fra en forælder terminerer, og returnerer den eksisterende post, så en delt font kun kloner én gang
At registrere tilknytningen, før børnene gennemgås, er det, der gør en cyklisk graf sikker og et delt objekt klonet præcis én gang
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
      // Poster tilføjet af dette kald er blevet rullet tilbage;
      // alt, der var registreret før det, er stadig intakt.
      Assert(GraftMap.Count = EntriesBefore);
      WriteLn('graft refused: ', Report.ErrorMessage);
    end;
  finally
    GraftMap.Free;
  end;
end;

Den tilbagrulning er hele pointen i selv at eje mappen. PDFiumPas behandler et kalder-leveret map transaktionelt: en fejlslagen transplantation kasserer de poster, kaldet tilføjede, og beholder hver tilknytning, der fandtes på forhånd, så én afvisning aldrig efterlader en cache af referencer til objekter, der aldrig blev skrevet. Hold dog ét map pr. destinationsdokument — destinationsiden af hver post er et objektnummer i netop den fil, og det betyder ingenting i en anden

Feltnavnkollisioner: afvis eller omdøb

Fuldt kvalificerede feltnavne skal forblive unikke inde i en formular, og PDFiumPas gætter ikke, hvad du mente, når de kolliderer. TPdfAcroFormCollisionPolicy tilbyder præcis to svar. Under pagcpReject, standarden, afbryder det første kildefelt, hvis titel allerede findes i destinationen, hele transplantationen med en fejl og efterlader outputstream tom. Under pagcpRename omdøbes det kolliderende kildefelt ved at sætte RenamePrefix foran, og transplantationen fortsætter, med Report.RenamedFieldCount, der fortæller dig, hvor ofte det skete

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);

Omdøbning er ikke gratis, og du bør beslutte den bevidst i stedet for at gribe den for at få en fejl til at forsvinde. Et omdøbt felt er et andet felt: enhver JavaScript i destinationen, der adresserer det ved navn, ethvert beregningsindslag i /CO, som et menneske skrev mod det gamle navn, og enhver downstream-forbruger, der nøgler på feltnavnet, får brug for at kende præfikset. Hvis de to dokumenter reelt beskriver samme felt, er den ærlige løsning som regel at afstemme navnene opstrøms, ikke ved transplantationstidspunktet. Når transplantationen er landet, er næste naturlige skridt at gå den flettede formular igennem for at bekræfte, hvad du faktisk fik, og formularfeltnavigation i PDFiumPas dækker den gennemgang

Hvor transplantationen bevidst fejler lukket

Hver tvetydig betingelse er en fejl, aldrig et best effort-resultat, og det er en designbeslutning, der er værd at forstå, før den overrasker dig i produktionen. GraftPdfAcroForm returnerer False, nulstiller outputstream og rapporterer grunden, når den rammer en af disse

  • Kildeformularen bærer en /XFA-post — XFA-pakker er en parallel formularmodel og kan ikke reduceres til AcroForm-feltordbøger
  • En widget bor på en kildeside, der ikke har en post i sidetilknytningen, hvilket ellers stille ville tabe feltet eller hægte det til den forkerte side
  • Sidetilknytninger er uden for intervallet, eller to tilknytninger genbruger samme kilde- eller destinationsside
  • Begge formularer definerer en standardressourceordbog /DR, fordi fletning af to ressourcenavnerum ville risikere at vende et eksisterende navn mod en anden font
  • Objektgrafen overskrider MaxObjects, eller rekursionen overskrider MaxDepth
  • Destinationen indeholder en signatur, og AllowSignedDestination er False
  • Det leverede graft map tilhører et andet kildedokument, eller en kildereference hænger i luften

Skrivestien er lige så konservativ. PDFiumPas udsender resultatet som en sparsom inkrementel revision, der tilføjes destinationen, materialiserer derefter det skrevne output igen og genlæser dets formular: hvis feltantallet i resultatet ikke er lig destinationens oprindelige feltantal plus kildens, afvises hele transplantationen, og outputtet ryddes. Du får aldrig en delvist transplanteret fil. Prisen på den politik er reel — en /DR-kollision eller en signeret destination stopper dig helt, og du er nødt til at løse det selv i stedet for at acceptere en flettet tilnærmelse — men alternativet er en formular, der åbner fint og regner forkert

Hvordan PDFiumPas GraftPdfAcroForm fejler lukket i Delphi: den skrevne revision genlæses, og dens feltantal verificeres, enhver tvetydig betingelse som XFA eller en utilknyttet side afviser kaldet, og en afvisning kasserer kun de map-poster, kaldet tilføjede
Den verificerede skrivesti og det transaktionelle map er grunden til, at en afvist transplantation aldrig efterlader en delvist flettet fil

Når transplantation er det forkerte værktøj

Transplantation flytter struktur, så brug den, når strukturen er det, du mangler. Hvis begge dokumenter allerede bærer samme feltsæt, og du kun skal flytte værdier og annoteringer mellem dem, er eksport- og importstien i XFDF-formulardata-artiklen lettere, standardiseret og reversibel. Grib GraftPdfAcroForm, når destinationen slet ikke har felter, eller har et andet sæt, og du får brug for, at widgets, appearance-streams, handlinger og beregningsrækkefølge kommer hel over. En sidste praktisk bemærkning om identitet: fordi graft-mappen nøgler på objektnummer plus generation og er bundet til en SHA-256 af kildebytene, giver gem-igen eller optimering af kilden mellem kørsler en anden identitet og et map, der ikke længere gælder. Tag et snapshot af kilden, du transplanterer fra, og hold den stabil for batchen; behandl den som en inputartefakt, ikke som noget, et natligt job frit kan omskrive

GraftPdfAcroForm, TPdfCrossDocumentGraftMap og det omgivende streamniveau-PDF-værktøjssæt følger med PDFiumPas Delphi PDFium Component til Delphi, C++Builder og Lazarus, hvor produktsiden bærer den fulde API-reference for graftindstillingerne, rapportfelterne og resten af dokumentredigeringsoverfladen