Tehnički članak

Presađivanje AcroForm polja između PDF-ova u Delphiju

Premještanje bloka polja obrasca s prošlogodišnjeg predloška na ovogodišnji raspored je točka gdje FDF i XFDF povratni putevi više nisu dovoljni: vrijednosti stignu, ali tokovi izgleda, računske akcije i zadani resursi ne. PDFiumPas na taj slučaj odgovara s GraftPdfAcroForm, koji klonira cijeli graf objekata polja iz jednog PDF-a i upisuje ga u drugi

Razlog zašto izvoz na razini podataka to ne može jest strukturan. Polje nije zapis, nego podgraf. ISO 32000-1 §12.7 definira rječnik interaktivnog obrasca koji drži /Fields, /CO, /DR i /DA, §12.7.3 definira rječnike polja koji ispod njega vise, a §12.5.6.19 definira widget anotacije koje tim poljima daju vidljivi okvir na stranici. XFDF nosi listove te strukture. Presađivanje nosi samu strukturu

Zašto kopiranje polja /Fields nikad nije dovoljno

Kopiranje /Fields iz jednog dokumenta u drugi proizvodi obrazac pokvaren na svaki zanimljiv način, jer polje drži neizravne reference i ništa drugo. ISO 32000-1 §7.3.10 čini neizravni objekt adresabilnim brojem objekta plus generacijom, a ti brojevi imaju smisla samo unutar datoteke iz koje su došli. Zalijepite polje preko i svaka referenca u njemu ili visi u prazno ili, gore, tiho se razriješi u nepovezani objekt koji slučajno zauzima taj utor u odredištu. Ispod svake reference sjedi graf koji je i dijeljen i cikličan. Rječnik polja pokazuje na svoju djecu, svako dijete pokazuje natrag na svoj /Parent, widget pokazuje na svoje tokove izgleda i na stranicu koja ga nosi preko /P, tokovi izgleda pokazuju na fontove u zadanom rječniku resursa obrasca, a rječnici dodatnih akcija ispod /AA pokazuju na još objekata. Dva widgeta na različitim stranicama u pravilu dijele jedan font i jedan appearance XObject. Pa ispravno presađivanje mora prošetati tim grafom, klonirati svaki dostižni objekt točno jednom, preusmjeriti /P svakog widgeta na mapiranu odredišnu stranicu i dodati klonirani widget u polje /Annots te stranice — inače polje postoji u obrascu, a nevidljivo je na stranici. Ako ste ganjali razliku između polja, njegova widgeta i anotacije stranice koja ga prikazuje, naša napomena o widget indeksu nasuprot indeksu anotacija pokriva upravo tu podjelu

Graf objekata iza jednog PDF polja obrasca dok ga PDFiumPas presađuje u Delphiju: rječnik obrasca, polje, widget anotacije, nizovi anotacija odredišne stranice te tok izgleda i font koje oba widgeta dijele, uz povratnu referencu na roditelja koja zatvara ciklus
Polje je dijeljeni ciklični podgraf, zato prepisivanje niza /Fields između dokumenata ostavlja svaku referencu da visi u prazno

Što GraftPdfAcroForm treba od vas?

Treba mu tri odvojena toka i izravno zadano mapiranje stranica. GraftPdfAcroForm prima Source, Destination i Output kao zasebne instance TStream, niz TPdfGraftPageMappings, zapis TPdfAcroFormGraftOptions, neobavezni TPdfCrossDocumentGraftMap i izlazni TPdfAcroFormGraftReport. Vraća Boolean umjesto da podiže iznimku, a u slučaju neuspjeha izvještaj nosi razlog u ErrorMessage. Mapiranje stranica je s obje strane jedinično indeksirano i ne izvodi se samo: svaka izvorna stranica koja nosi widget koji namjeravate presaditi mora u njemu stajati. Predati nil za graft mapu je legitimno — funkcija tada sama stvara i oslobađa privatnu mapu na trajanje poziva — a TPdfAcroFormGraftOptions.Default daje vam CollisionPolicy postavljen na pagcpReject, RenamePrefix na Imported_, MaxObjects od 100000, MaxDepth od 128 i AllowSignedDestination postavljeno na False. Zadnje tri vrijednosti su proračuni, i postoje jer graf objekata koji ćete za tren prošetati potječe iz datoteke koju niste napisali

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;

Kako graft mapa izbjegava dvostruko kloniranje dijeljenog fonta?

TPdfCrossDocumentGraftMap drži tablicu referenci od izvora prema odredištu čiji ključevi nose i broj objekta i generaciju, a rekurzivni kloner konzultira je prije nego što se udubi. Redoslijed operacija je ono što čini cikluse sigurnima: kloner prvo alocira broj odredišnog objekta i registrira mapiranje, a tek onda obilazi reference na djecu izvornog objekta. Roditelj koji dođe do djeteta koje pokazuje natrag na njega pronalazi roditelja već registriranoga i vraća postojeću odredišnu referencu umjesto da se ponovno udubljuje. Isto traženje čini da font, tok izgleda ili akcija koje dijeli šest widgeta budu klonirani jednom i referencirani šest puta. Mapa je vezana uz izvorni dokument SHA-256 hasheom izvornih bajtova, izloženim kao SourceIdentity. Ako GraftPdfAcroFormu predate mapu čiji identitet ne odgovara predanom izvoru, odbit će poziv umjesto da ponovno koristi reference koje za ovu datoteku nikad nisu bile valjane. Mapiranja stranica siju se u istu mapu prije početka kloniranja, i upravo tako widgetov /P završi pokazujući na odredišnu stranicu: izvorni objekt stranice već se razrješuje u mapirani odredišni objekt stranice, pa ga uobičajeni prolaz prepisivanja referenci obavi bez posebnog slučaja

Graft mapa između dokumenata u PDFiumPasu u Delphiju ključa svaku izvornu referencu brojem objekta i generacijom, registrira odredišno mapiranje prije spuštanja tako da se povratna referenca roditelja završi, i vraća postojeći unos tako da se dijeljeni font klonira samo jednom
Registracija mapiranja prije obilaska djece čini ciklični graf sigurnim, a dijeljeni objekt klonira točno jednom
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
      // Entries added by this call have been rolled back;
      // anything registered before it is still intact.
      Assert(GraftMap.Count = EntriesBefore);
      WriteLn('graft refused: ', Report.ErrorMessage);
    end;
  finally
    GraftMap.Free;
  end;
end;

Taj rollback je razlog da mapu posjedujete sami. PDFiumPas s mapom koju predate izvana radi transakcijski: neuspjeli graft odbacuje unose koje je taj poziv dodao i zadržava svako mapiranje koje je postojalo prije, pa jedan odbijeni poziv nikad ne ostavi keš referenci na objekte koji nikad nisu upisani. Ipak držite jednu mapu po odredišnom dokumentu — odredišna strana svakog unosa je broj objekta u toj konkretnoj datoteci, i u nekoj drugoj ne znači ništa

Sukobi naziva polja: odbiti ili preimenovati

Potpuno kvalificirani nazivi polja moraju ostati jedinstveni unutar obrasca, a PDFiumPas neće nagađati što ste mislili kad se sudare. TPdfAcroFormCollisionPolicy nudi točno dva odgovora. Pod pagcpReject, zadanom, prvo izvorno polje čiji naslov već postoji u odredištu prekida cijeli graft s greškom i ostavlja izlazni tok praznim. Pod pagcpRename, polje u sudaru preimenuje se dodavanjem prefiksa RenamePrefix i graft se nastavlja, a Report.RenamedFieldCount vam govori koliko se puta to dogodilo

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

Preimenovanje nije besplatno, i odlučite ga promišljeno umjesto da ga zgrabite samo da greška nestane. Preimenovano polje je drugo polje: svaki JavaScript u odredištu koji ga adresira po nazivu, svaki računski unos u /CO koji je čovjek napisao na stari naziv i svaki nizvodni potrošač koji ključa po nazivu polja morat će saznati za prefiks. Ako dva dokumenta stvarno opisuju isto polje, pošteno rješenje je obično uskladiti nazive uzvodno, a ne u trenu presađivanja. Kad graft sjedne, prirodni je sljedeći korak obići spojeni obrazac i potvrditi što ste stvarno dobili, a navigacija po poljima obrasca u PDFiumPasu pokriva taj obilazak

Gdje se graft namjerno odbija

Svaka dvosmislena okolnost je greška, nikad rezultat najboljeg truda, i to je dizajnerska odluka koju vrijedi razumjeti prije nego vas iznenadi u produkciji. GraftPdfAcroForm vraća False, resetira izlazni tok i izvještava o razlogu kad naiđe na bilo što od ovoga

  • Izvorni obrazac nosi /XFA unos — XFA paketi su paralelni model obrazaca i ne mogu se svesti na rječnike AcroForm polja
  • Widget živi na izvornoj stranici koja nema unos u mapiranju stranica, što bi inače tiho izgubilo polje ili ga prikačilo na krivu stranicu
  • Mapiranja stranica su izvan raspona, ili dva mapiranja ponovno koriste isti izvornu ili odredišnu stranicu
  • Oba obrasca definiraju zadani rječnik resursa /DR, jer bi spajanje dvaju prostora naziva resursa moglo preusmjeriti postojeći naziv na drugi font
  • Graf objekata premašuje MaxObjects ili rekurzija premašuje MaxDepth
  • Odredište sadrži potpis, a AllowSignedDestination je False
  • Predana graft mapa pripada drugom izvornom dokumentu, ili neka izvorna referenca visi u prazno

Put upisa je jednako konzervativan. PDFiumPas rezultat izbacuje kao rijetku inkrementalnu reviziju dopisanu odredištu, zatim ponovno materijalizira upisani izlaz i ponovno čita njegov obrazac: ako broj polja rezultata nije jednak izvornom broju polja odredišta plus broju polja izvora, cijeli graft se odbija i izlaz se briše. Djelomično presađenu datoteku nikad ne dobijete. Cijena te politike je stvarna — sudar /DR ili potpisano odredište vas zaustave odmah, i morate ih razriješiti sami umjesto da prihvatite spojenu aproksimaciju — ali alternativa je obrazac koji se lijepo otvara i krivo računa

Kako PDFiumPas GraftPdfAcroForm propada zatvoreno u Delphiju: upisana revizija ponovno se čita i provjerava joj se broj polja, svaka dvosmislena okolnost poput XFA ili nemapirane stranice odbija poziv, a odbijanje odbacuje samo unose mape koje je taj poziv dodao
Provjereni put upisa i transakcijska mapa razlozi su da odbijeni graft nikad ne ostavi djelomično spojenu datoteku

Kada presađivanje nije pravi alat

Presađivanje premješta strukturu, pa ga koristite kad je struktura ono što vam nedostaje. Ako oba dokumenta već nose isti skup polja i trebate samo prenijeti vrijednosti i anotacije između njih, put izvoza i uvoza u članku o XFDF podacima obrasca je lakši, standardan i reverzibilan. Krenite na GraftPdfAcroForm kad odredište uopće nema polja, ili ima drugačiji skup, i trebate da widgeti, tokovi izgleda, akcije i redoslijed računa pređu netaknuti. Zadnja praktična napomena o identitetu: graft mapa ključa po broju objekta plus generaciji i vezana je uz SHA-256 izvornih bajtova, pa ponovno spremanje ili optimiranje izvora između pokretanja daje drugi identitet i mapu koja više ne vrijedi. Snimite izvor s kojeg presađujete i držite ga stabilnim za cijelu seriju; tretirajte ga kao ulazni artefakt, a ne kao nešto što noćni posao smije slobodno prepisati

GraftPdfAcroForm, TPdfCrossDocumentGraftMap i okolni PDF alat na razini tokova stižu uz PDFiumPas Delphi PDFium komponentu za Delphi, C++Builder i Lazarus, gdje stranica proizvoda nosi potpunu API referencu za graft opcije, polja izvještaja i ostatak sučelja za uređivanje dokumenata