Presunúť blok formulárových polí zo šablóny z minulého roka do tohtoročného layoutu je moment, kedy prenosy cez FDF a XFDF prestávajú stačiť: hodnoty dorazia, ale prúdy vzhľadu (appearance streams), výpočtové akcie a predvolené zdroje nie. PDFiumPas na tento prípad odpovedá funkciou GraftPdfAcroForm, ktorá naklonuje celý objektový graf polí z jedného PDF a zapíše ho do druhého
Dôvod, prečo export na úrovni dát to nedokáže, je štrukturálny. Pole nie je záznam, je to podgraf. ISO 32000-1 §12.7 definuje slovník interaktívneho formulára, ktorý drží /Fields, /CO, /DR a /DA, §12.7.3 definuje slovníky polí pod ním a §12.5.6.19 definuje anotácie widgetov, ktoré tým poliam dajú viditeľné rámečky na stránke. XFDF nesie listy tejto štruktúry. Graftovanie nesie samotnú štruktúru
Prečo kópia poľa /Fields nikdy nestačí
Skopírovanie /Fields z jedného dokumentu do druhého vyrobí formulár rozbitý každým predstaviteľným spôsobom, pretože toto pole obsahuje výhradne nepriame odkazy a nič iné. ISO 32000-1 §7.3.10 adresuje nepriamy objekt číslom objektu plus generáciou a tieto čísla majú zmysel len vnútri súboru, z ktorého pochádzajú. Keď pole takto presuniete do cieľa, každý odkaz v ňom buď visí do prázdna, alebo — čo je horšie — sa ticho skončí na nesúvisiacom objekte, ktorý sa v cieli náhodou nachádza na tom istom mieste. Pod každým odkazom sedí graf, ktorý je zároveň zdieľaný aj cyklický. Slovník poľa ukazuje na svoje dieťa, každé dieťa ukazuje späť na svoj /Parent, widget ukazuje na svoje prúdy vzhľadu a cez /P na stránku, ktorá ho nesie, prúdy vzhľadu ukazujú na písma v predvolenom zdrojovom slovníku formulára a slovníky ďalších akcií pod /AA ukazujú na ďalšie objekty. Dva widgety na rôznych stránkach bežne zdieľajú jedno písmo a jeden XObject vzhľadu. Správny graft preto musí týmto grafom prejsť, každý dosiahnuteľný objekt naklonovať presne raz, presmerovať /P každého widgetu na namapovanú cieľovú stránku a naklonovaný widget pridať do poľa /Annots tejto stránky — inak pole vo formulári existuje, ale na stránke je neviditeľné. Ak ste už riešili rozdiel medzi poľom, jeho widgetom a anotáciou stránky, ktorá ho zobrazuje, naša poznámka o indexe widgetov verzus indexe anotácií pokrýva presne toto rozdelenie
Čo všetko GraftPdfAcroForm potrebuje od vás?
Potrebuje tri oddelené prúdy a explicitné mapovanie stránok. GraftPdfAcroForm berie Source, Destination a Output ako samostatné inštancie TStream, pole TPdfGraftPageMappings, záznam TPdfAcroFormGraftOptions, voliteľnú TPdfCrossDocumentGraftMap a výstupný TPdfAcroFormGraftReport. Namiesto výnimky vracia Boolean a pri zlyhaní nesie report dôvod v ErrorMessage. Mapovanie stránok je na oboch stranách indexované od jednej a neodhaduje sa: každá zdrojová stránka, ktorá nesie widget, ktorý chcete graftovať, sa v ňom musí objaviť. Predať za graft mapu nil je legitímne — funkcia si potom na čas volania vytvorí a uvoľní súkromnú mapu — a TPdfAcroFormGraftOptions.Default vám dá CollisionPolicy nastavené na pagcpReject, RenamePrefix nastavené na Imported_, MaxObjects 100000, MaxDepth 128 a AllowSignedDestination nastavené na False. Posledné tri sú rozpočty a existujú preto, že objektový graf, ktorým sa chystáte prechádzať, prišiel zo súboru, ktorý ste nepísali vy
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;
Ako graft mapa zabráni dvojnásobnému klonovaniu zdieľaného písma?
TPdfCrossDocumentGraftMap drží referenčnú tabuľku zdroj–cieľ, ktorej kľúče nesú číslo objektu aj generáciu, a rekurzívny klonér sa do nej pozerá skôr, než zostúpi nižšie. Poradie krokov je to, čo robí cykly bezpečnými: klonér najprv alokuje číslo cieľového objektu a zaregistruje mapovanie, potom prechádza podriadené odkazy zdrojového objektu. Rodič, ktorý dorazí k dieťaťu ukazujúcemu späť na svojho rodiča, nájde rodiča už zaregistrovaného a vráti existujúci cieľový odkaz namiesto ďalšej rekurzie. Rovnaké vyhľadanie spôsobí, že písmo, prúd vzhľadu alebo akcia zdieľaná šiestimi widgetmi sa naklonuje raz a odkáže šesťkrát. Mapa je viazaná na zdrojový dokument cez hash SHA-256 zdrojových bajtov, vystavený ako SourceIdentity. Ak GraftPdfAcroForm odovzdáte mapu, ktorej identita sa nezhoduje s odovzdaným zdrojom, volanie odmietne namiesto toho, aby znovu použila odkazy, ktoré pre tento súbor nikdy neboli platné. Mapovania stránok sa zasadia do tej istej mapy skôr, než klonovanie začne, a presne tak dopadne /P widgetu na cieľovú stránku: zdrojový objekt stránky sa už vyrieši na namapovaný cieľový objekt stránky, takže bežný priechod prepisujúci odkazy to ošetrí bez špeciálneho prípadu
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
// Položky pridané týmto volaním boli vrátené späť;
// všetko zaregistrované pred ním zostáva nedotknuté.
Assert(GraftMap.Count = EntriesBefore);
WriteLn('graft refused: ', Report.ErrorMessage);
end;
finally
GraftMap.Free;
end;
end;
Práve táto transakčná vrátená zmena je dôvod, prečo mapu vlastniť sami. PDFiumPas berie mapu odovzdanú volajúcim transakčne: neúspešný graft zahodí položky, ktoré toto volanie pridalo, a ponechá každé mapovanie, ktoré existovalo vopred, takže jedno odmietnutie nikdy nezanechá cache odkazov na objekty, ktoré nikdy neboli zapísané. Však si tiež pre jeden cieľový dokument držte jednu mapu — cieľová strana každej položky je číslo objektu v tom konkrétnom súbore a v inom súbore neznamená nič
Kolízie názvov polí: odmietnuť alebo premenovať
Plne kvalifikované názvy polí musia vo vnútri formulára zostať unikátne a PDFiumPas nebude pri ich zrážke hádať, čo ste mali na mysli. TPdfAcroFormCollisionPolicy ponúka presne dve odpovede. Pri pagcpReject, predvolenom, preruší prvé zdrojové pole, ktorého názov už v cieli existuje, celý graft chybou a nechá výstupný prúd prázdny. Pri pagcpRename sa zrážajúce zdrojové pole premenuje pridaním predpony RenamePrefix a graft pokračuje, pričom Report.RenamedFieldCount vám povie, ako často sa to stalo
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);
Premenovanie nie je zadarmo a mali by ste sa preň rozhodnúť vedome, nie siahnuť po ňom len preto, aby chyba zmizla. Premenované pole je iné pole: každý JavaScript v cieli, ktorý ho adresuje menom, každá položka výpočtu v /CO, ktorú človek napísal proti starému názvu, a každý nadväzujúci konzument, ktorý sa opiera o názov poľa, bude o predpone potrebovať vedieť. Ak oba dokumenty skutočne opisujú to isté pole, úprimná oprava zvykne byť zjednotiť názvy proti prúdu, nie v momente graftovania. Keď graft dosadne, prirodzeným ďalším krokom je prejsť zlúčený formulár a potvrdiť, čo ste vlastne dostali; tejto prechádzke sa venuje navigácia po formulárových poliach v PDFiumPas
Kde graft zámerne zlyháva bezpečne
Každá nejednoznačná podmienka je chyba, nikdy nie výsledok na najlepšie úsilie, a toto rozhodnutie o návrhu stojí za to pochopiť skôr, než vás v produkcii prekvapí. GraftPdfAcroForm vráti False, resetuje výstupný prúd a nahlási dôvod, keď narazí na ktorúkoľvek z týchto situácií
- Zdrojový formulár nesie položku
/XFA— XFA pakety sú paralelný model formulára a nedajú sa zredukovať na slovníky polí AcroForm - Widget sedí na zdrojovej stránke, ktorá nemá v mapovaní stránok položku — inak by sa pole ticho stratilo alebo pripojilo na nesprávnu stránku
- Mapovania stránok sú mimo rozsahu, alebo dve mapovania znovu použijú tú istú zdrojovú či cieľovú stránku
- Oba formuláre definujú predvolený zdrojový slovník
/DR, pretože zlúčenie dvoch priestorov názvov zdrojov by riskovalo presmerovanie existujúceho názvu na iné písmo - Objektový graf prekročí
MaxObjectsalebo rekurzia prekročíMaxDepth - Cieľ obsahuje podpis a
AllowSignedDestinationjeFalse - Odovzdaná graft mapa patrí inému zdrojovému dokumentu, alebo nejaký zdrojový odkaz visí do prázdna
Cesta zápisu je rovnako konzervatívna. PDFiumPas vyhotoví výsledok ako riedku prírastkovú revíziu pripojenú k cieľu, potom znovu zmaterializuje zapísaný výstup a znovu prečíta jeho formulár: ak sa počet polí výsledku nerovná pôvodnému počtu polí cieľa plus počtu polí zdroja, celý graft sa odmietne a výstup sa vyčistí. Čiastočne graftovaný súbor nikdy nedostanete. Cena tejto politiky je reálna — kolízia /DR alebo podpísaný cieľ vás zastaví úplne a musíte to vyriešiť sami, namiesto aby ste prijali zlúčenú aproximáciu — ale alternatívou je formulár, ktorý sa otvorí v poriadku a počíta zle
Kedy je graftovanie nesprávny nástroj
Graftovanie presúva štruktúru, takže použite ho, keď vám chýba práve štruktúra. Ak oba dokumenty už nesú tú istú sadu polí a potrebujete presunúť len hodnoty a anotácie, cesta exportu a importu v článku o XFDF údajoch formulára je ľahšia, štandardná a vratká. Siahnite po GraftPdfAcroForm, keď cieľ nemá žiadne polia alebo má inú sadu, a potrebujete, aby widgety, prúdy vzhľadu, akcie a poradie výpočtov prešli neporušené. Posledná praktická poznámka k identite: pretože graft mapa kľúčuje na číslo objektu plus generáciu a je viazaná na SHA-256 zdrojových bajtov, opätovné uloženie alebo optimalizácia zdroja medzi behmi vyrobí inú identitu a mapu, ktorá už neplatí. Uložte si snímku zdroja, z ktorého graftujete, a držte ju pre dávku stabilnú; berte ju ako vstupný artefakt, nie ako niečo, čo si nočná úloha môže voľne prepísať
GraftPdfAcroForm, TPdfCrossDocumentGraftMap a okolitá PDF sada nástrojov na úrovni prúdov sa dodáva s PDFiumPas Delphi PDFium Component pre Delphi, C++Builder a Lazarus, kde produktová stránka nesie úplnú referenciu API pre graft voľby, polia reportu a zvyšok povrchu na úpravu dokumentov