Odborný článok

Presun AcroForm polí medzi PDF v Delphi s PDFiumPas

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

Graf objektov za jedným poľom PDF formulára, keď ho PDFiumPas graftuje v Delphi: slovník formulára, pole, anotácie widgetov, polia anotácií cieľovej stránky a prúd vzhľadu a písmo, ktoré zdieľajú oba widgety, plus spätný odkaz na rodiča, ktorý uzatvára cyklus
Pole je zdieľaný cyklický podgraf, a preto skopírovanie poľa /Fields medzi dokumentmi nechá každý odkaz visieť vo vzduchu

Č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

Graft mapa PDFiumPas medzi dokumentmi v Delphi kľúčuje každý zdrojový odkaz číslom objektu a generáciou, zaregistruje cieľové mapovanie skôr, než zostúpi, takže spätný odkaz rodiča sa ukončí, a vráti existujúcu položku, takže zdieľané písmo sa naklonuje len raz
Zaregistrovanie mapovania skôr, než prejdete podriadené položky, robí cyklický graf bezpečným a zdieľaný objekt sa klonuje presne raz
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čí MaxObjects alebo rekurzia prekročí MaxDepth
  • Cieľ obsahuje podpis a AllowSignedDestination je False
  • 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

Ako GraftPdfAcroForm od PDFiumPas zlyháva bezpečne v Delphi: zapísaná revízia sa znovu prečíta a overí sa jej počet polí, každá nejednoznačná podmienka ako XFA alebo nenamapovaná stránka odmietne volanie a odmietnutie zahodí len položky mapy, ktoré toto volanie pridalo
Overená cesta zápisu a transakčná mapa sú dôvody, prečo odmietnutý graft nikdy nezanechá čiastočne zlúčený súbor

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