Teknisk artikkel

Graft AcroForm-felt mellom PDF-er i Delphi (PDFiumPas)

Å flytte en blokk med skjemafelt fra fjorårets mal over på årets oppsett er der FDF- og XFDF-rundturer slutter å være nok: verdiene ankommer, men utseendestrømmene, kalkulasjonshandlingene og standardressursene gjør ikke det. PDFiumPas svarer på det tilfellet med GraftPdfAcroForm, som kloner hele feltobjektgrafen ut av én PDF og skriver den inn i en annen

Grunnen til at en data-eksport ikke kan gjøre dette, er strukturell. Et felt er ikke en post, det er en delgraf. ISO 32000-1 §12.7 definerer den interaktive skjemaordlisten som holder /Fields, /CO, /DR og /DA, §12.7.3 definerer feltordlistene som henger under den, og §12.5.6.19 definerer widget-annoteringene som gir disse feltene en synlig boks på en side. XFDF bærer bladene av den strukturen. Transplantasjon bærer strukturen selv

Hvorfor det aldri er nok å kopiere /Fields-arrayen

Å kopiere /Fields fra ett dokument til et annet produserer et skjema som er ødelagt på hver interessante måte, fordi arrayen holder indirekte referanser og ingenting annet. ISO 32000-1 §7.3.10 gjør et indirekte objekt adresserbart ved objektnummer pluss generasjon, og de tallene er meningsfulle bare inne i filen de kom fra. Lim arrayen over, og hver referanse i den henger enten løs eller, verre, løses stille til et urelatert objekt som tilfeldigvis okkuperer den plassen i målet. Under hver referanse sitter en graf som både er delt og syklisk. En feltordliste peker på barna sine, hvert barn peker tilbake på sin /Parent, en widget peker på sine utseendestrømmer og på siden som bærer den gjennom /P, utseendestrømmer peker på fonter i skjemaets standardressursordliste, og tilleggshandlingsordlister under /AA peker på enda flere objekter. To widgeter på forskjellige sider deler rutinemessig én font og én utseende-XObject. Så en korrekt transplantasjon må gå den grafen, klone hvert nåbart objekt nøyaktig én gang, videresende hver widgets /P til den kartlagte målsiden, og legge den klonede widgeten til den sidens /Annots-array — ellers finnes feltet i skjemaet og er usynlig på siden. Hvis du har jagd forskjellen mellom et felt, dets widget og sideannoteringen som viser det, dekker notatet vårt om widget-indeks versus annoteringsindeks nøyaktig den splittingen

Objektgrafen bak ett PDF-skjemafelt idet PDFiumPas transplanterer det i Delphi: skjemaordlisten, feltet, widget-annoteringene, målsidens annoteringsarrayer og utseendestrømmen og fonten som begge widgeter deler, pluss foreldre-tilbake-referansen som lukker syklingen
Et felt er en delt syklisk delgraf, og det er derfor kopiering av /Fields-arrayen på tvers av dokumenter lar hver referanse henge løs

Hva trenger GraftPdfAcroForm fra deg?

Den trenger tre distinkte strømmer og en eksplisitt sidekartlegging. GraftPdfAcroForm tar Source, Destination og Output som separate TStream-forekomster, en TPdfGraftPageMappings-array, en TPdfAcroFormGraftOptions-post, et valgfritt TPdfCrossDocumentGraftMap og en ut-TPdfAcroFormGraftReport. Den returnerer Boolean i stedet for å kaste, og ved feil bærer rapporten grunnen i ErrorMessage. Sidekartleggingen er én-basert på begge sider og utledes ikke: hver kilde-side som bærer en widget du har tenkt å transplantere, må opptre i den. Å sende nil som graftkart er legitimt — funksjonen lager da og frigir et privat kart for varigheten av kallet — og TPdfAcroFormGraftOptions.Default gir deg CollisionPolicy satt til pagcpReject, RenamePrefix satt til Imported_, MaxObjects på 100000, MaxDepth på 128 og AllowSignedDestination satt til False. De siste tre er budsjetter, og de finnes fordi objektgrafen du er i ferd med å gå, kom fra en fil du ikke skrev

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 unngår graftkartet å klone en delt font to ganger?

TPdfCrossDocumentGraftMap holder en kilde-til-mål-referansetabell hvis nøkler bærer både objektnummer og generasjon, og den rekursive kloren konsulterer den før den stiger ned. Rekkefølgen av operasjonene er det som gjør syklinger trygge: kloren tildeler målets objektnummer og registrerer kartleggingen først, og går deretter gjennom kildeobjektets barnreferanser. En forelder som når et barn som peker tilbake på sin forelder, finner forelderen allerede registrert og returnerer den eksisterende målreferansen i stedet for å rekursere. Samme oppslag er det som får en font, en utseendestrøm eller en handling som deles av seks widgeter til å klones én gang og refereres seks ganger. Kartet er bundet til kildedokumentet ved en SHA-256-hash av kildebytene, eksponert som SourceIdentity. Hvis du gir GraftPdfAcroForm et kart hvis identitet ikke matcher kilden du sendte, avslår den kallet i stedet for å gjenbruke referanser som aldri var gyldige for denne filen. Sidekartleggingene sås inn i samme kart før kloningen begynner, og det er nøyaktig hvordan en widgets /P ender opp med å peke på målsiden: kilde-sideobjektet løser allerede til det kartlagte målsideobjektet, så det ordinære referanse-omskrivningspasset håndterer det uten spesialtilfelle

PDFiumPas tverrdokument-graftkart i Delphi nøkler hver kilde-referanse ved objektnummer og generasjon, registrerer målkartleggingen før nedstigning slik at en foreldre-tilbake-referanse terminerer, og returnerer den eksisterende oppføringen slik at en delt font kun klones én gang
Å registrere kartleggingen før barna går gjennom, er det som gjør en syklisk graf trygg og et delt objekt klonet nøyaktig é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
      // Oppføringer lagt til av dette kallet er rullet tilbake;
      // alt registrert før det er fortsatt intakt.
      Assert(GraftMap.Count = EntriesBefore);
      WriteLn('graft refused: ', Report.ErrorMessage);
    end;
  finally
    GraftMap.Free;
  end;
end;

Den tilbakerulleringen er poenget med å eie kartet selv. PDFiumPas behandler et kaller-levert kart transaksjonelt: en mislykket transplantasjon forkaster oppføringene det kallet la til og beholder hver kartlegging som fantes på forhånd, så én avvisning etterlater aldri en hurtigbuffer av referanser til objekter som aldri ble skrevet. Ha ett kart per måldokument, dog — målsiden av hver oppføring er et objektnummer i den spesielle filen, og det betyr ingenting i en annen

Feltnavn-kollisjoner: avslå eller gi nytt navn

Fullt kvalifiserte feltnavn må forbli unike inne i et skjema, og PDFiumPas vil ikke gjette hva du mente når de kolliderer. TPdfAcroFormCollisionPolicy tilbyr nøyaktig to svar. Under pagcpReject, standarden, avbryter det første kildefeltet hvis tittel allerede finnes i målet hele transplantasjonen med en feil og etterlater utdatastrømmen tom. Under pagcpRename får det kolliderende kildefeltet nytt navn ved å prefikse RenamePrefix, og transplantasjonen fortsetter, med Report.RenamedFieldCount som forteller deg hvor ofte det skjedde

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

Å gi nytt navn er ikke gratis, og du bør bestemme det bevisst i stedet for å strekke deg etter det for å få en feil til å forsvinne. Et omdøpt felt er et annet felt: enhver JavaScript i målet som adresserer det ved navn, enhver kalkulasjonsoppføring i /CO som et menneske skrev mot det gamle navnet, og enhver nedstrøms forbruker som nøkler på feltnavnet må vite om prefikset. Hvis de to dokumentene genuint beskriver samme felt, er den ærlige fiksen vanligvis å bringe navnene i samsvar oppstrøms, ikke ved transplantasjonstidspunktet. Når transplantasjonen lander, er å gå det sammenslåtte skjemaet for å bekrefte hva du faktisk fikk, det naturlige neste steget, og skjemafelt-navigasjon i PDFiumPas dekker den traverseringen

Der transplantasjonen bevisst feiler lukket

Hver tvetydig tilstand er en feil, aldri et best-effort-resultat, og det er en designbeslutning verdt å forstå før den overrasker deg i produksjon. GraftPdfAcroForm returnerer False, nullstiller utdatastrømmen og rapporterer grunnen når den treffer noen av disse

  • Kildeskjemaet bærer en /XFA-oppføring — XFA-pakker er en parallell skjemamodell og kan ikke reduseres til AcroForm-feltordlister
  • En widget bor på en kilde-side som ikke har oppføring i sidekartleggingen, som ellers stille ville miste feltet eller knytte det til feil side
  • Sidekartlegginger er utenfor rekkevidde, eller to kartlegginger gjenbruker samme kilde- eller målside
  • Begge skjemaer definerer en standardressursordliste /DR, fordi å slå sammen to ressursnavnerom ville risikere å peke et eksisterende navn om til en annen font
  • Objektgrafen overstiger MaxObjects eller rekursjonen overstiger MaxDepth
  • Målet inneholder en signatur og AllowSignedDestination er False
  • Det oppgitte graftkartet tilhører et annet kildedokument, eller en kilde-referanse henger løs

Skrivestien er like konservativ. PDFiumPas emittierer resultatet som en sparsom inkrementell revisjon tilføyd målet, deretter re-materialiserer den skrevne utdata og leser skjemaet på nytt: hvis feltantallet i resultatet ikke er lik målets opprinnelige feltantall pluss kildens, avvises hele transplantasjonen og utdataene tømmes. Du får aldri en delvis transplantert fil. Kostnaden av den politikken er reell — en /DR-kollisjon eller et signert mål stopper deg fullstendig, og du må løse det selv i stedet for å akseptere en sammenslått tilnærmelse — men alternativet er et skjema som åpner fint og regner feil

Slik feiler PDFiumPas GraftPdfAcroForm lukket i Delphi: den skrevne revisjonen leses på nytt og feltantallet verifiseres, enhver tvetydig tilstand som XFA eller en ukartlagt side avslår kallet, og en avvisning forkaster bare kartoppføringene det kallet la til
Den verifiserte skrivestien og det transaksjonelle kartet er grunnen til at en avslått transplantasjon aldri etterlater en delvis sammenslått fil

Når transplantasjon er feil verktøy

Transplantasjon flytter struktur, så bruk den når strukturen er det du mangler. Hvis begge dokumenter allerede bærer samme feltsett og du bare trenger å flytte verdier og annoteringer mellom dem, er eksport- og importstien i XFDF-skjemadata-artikkelen lettere, standard og reversibel. Strekk deg etter GraftPdfAcroForm når målet ikke har felt i det hele tatt, eller har et annet sett, og du trenger widgetene, utseendestrømmene, handlingene og kalkulasjonsrekkefølgen til å komme over intakt. Et siste praktisk notat om identitet: fordi graftkartet nøkler på objektnummer pluss generasjon og er bundet til en SHA-256 av kildebytene, gir lagring på nytt eller optimering av kilden mellom kjøringer en annen identitet og et kart som ikke lenger gjelder. Ta øyeblikksbilde av kilden du transplanterer fra, og hold den stabil for bunten; behandle den som et inndata-artefakt, ikke som noe en nattlig jobb står fritt til å skrive om

GraftPdfAcroForm, TPdfCrossDocumentGraftMap og det omkringliggende strøm-nivå PDF-verktøyksettet følger med PDFiumPas Delphi PDFium Component for Delphi, C++Builder og Lazarus, der produktsiden bærer den fullstendige API-referansen for graftalternativene, rapportfeltene og resten av dokumentredigeringsflaten