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
Š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
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
/XFAunos — 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
MaxObjectsili rekurzija premašujeMaxDepth - Odredište sadrži potpis, a
AllowSignedDestinationjeFalse - 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
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