Technický článek

Přenos AcroForm polí mezi PDF v Delphi s PDFiumPas

Přesunout blok polí formuláře ze šablony loňského roku na rozvržení letošního roku je místo, kde zpáteční cesty FDF a XFDF přestávají stačit: hodnoty dorazí, ale proudy vzhledu, akce výpočtů a výchozí zdroje ne. PDFiumPas odpovídá na tento případ pomocí GraftPdfAcroForm, které naklonuje celý graf objektů polí z jednoho PDF a zapíše jej do druhého

Důvod, proč export na úrovni dat to nedokáže, je strukturální. Pole není záznam, je to podgraf. ISO 32000-1 §12.7 definuje slovník interaktivního formuláře držící /Fields, /CO, /DR a /DA, §12.7.3 definuje slovníky polí pod ním a §12.5.6.19 definuje anotace widgetů, které dávají těmto polům viditelný rámeček na stránce. XFDF nese listy této struktury. Grafting nese samotnou strukturu

Proč kopírování pole /Fields nikdy nestačí

Zkopírování /Fields z jednoho dokumentu do druhého vytvoří formulář rozbitý každým zajímavým způsobem, protože pole drží nepřímé reference a nic jiného. ISO 32000-1 §7.3.10 činí nepřímý objekt adresovatelným číslem objektu plus generací a ta čísla mají význam jen uvnitř souboru, ze kterého přišla. Vložte pole napříč a každá reference v něm buď visí, nebo, hůř, tiše vyřeší na nesouvisející objekt, který náhodou obsadí ten slot v cíli. Pod každou referencí sedí graf, který je sdílený i cyklický zároveň. Slovník pole ukazuje na své děti, každé dítě ukazuje zpět na své /Parent, widget ukazuje na své proudy vzhledu a na stránku, která jej nese, přes /P, proudy vzhledu ukazují na fonty ve výchozím zdrojovém slovníku formuláře a slovníky dalších akcí pod /AA ukazují na další objekty. Dva widgety na různých stránkách běžně sdílí jeden font a jeden appearance XObject. Správný graft tedy musí projít tím grafem, naklonovat každý dosažitelný objekt přesně jednou, přesměrovat /P každého widgetu na mapovanou cílovou stránku a přidat klonovaný widget do pole /Annots té stránky — jinak pole ve formuláři existuje a na stránce je neviditelné. Pokud jste pronásledovali rozdíl mezi polem, jeho widgetem a anotací stránky, která jej zobrazuje, naši poznámku widget index versus annotation index pokrývá přesně toto rozdělení

Graf objektů za jedním polem formuláře PDF při graftování PDFiumPas v Delphi: slovník formuláře, pole, anotace widgetů, pole anotací cílové stránky a proud vzhledu a font, které oba widgety sdílí, plus zpětná reference rodiče uzavírající cyklus
Pole je sdílený cyklický podgraf, proto kopírování pole /Fields napříč dokumenty nechává každou referenci viset

Co GraftPdfAcroForm potřebuje od vás?

Potřebuje tři oddělené proudy a explicitní mapování stránek. GraftPdfAcroForm bere Source, Destination a Output jako oddělené instance TStream, pole TPdfGraftPageMappings, záznam TPdfAcroFormGraftOptions, volitelnou TPdfCrossDocumentGraftMap a výstupní TPdfAcroFormGraftReport. Vrací Boolean místo vyvolávání výjimky a při selhání nese report důvod v ErrorMessage. Mapování stránek je na obou stranách indexováno od 1 a není inferováno: každá zdrojová stránka nesoucí widget, který chcete graftovat, se v něm musí objevit. Předání nil za graft mapu je legitimní — funkce pak vytvoří a uvolní soukromou na dobu volání — a TPdfAcroFormGraftOptions.Default dává CollisionPolicy nastavené na pagcpReject, RenamePrefix na Imported_, MaxObjects 100000, MaxDepth 128 a AllowSignedDestination na False. Těm posledním třem se říká rozpočty a existují proto, že graf objektů, který se chystáte projít, přišel ze souboru, který jste nepsali 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;

Jak graft mapa zabrání dvojímu klonování sdíleného fontu?

TPdfCrossDocumentGraftMap drží referenční tabulku ze zdroje do cíle, jejíž klíče nesou číslo objektu i generaci, a rekurzivní kloner se jí dotazuje, než sestoupí. Pořadí operací je to, co činí cykly bezpečnými: kloner alokuje cílové číslo objektu a zaregistruje mapování nejprve, pak projde reference dětí zdrojového objektu. Rodič, který dojde k dítěti ukazujícímu zpět na rodiče, najde rodiče již zaregistrovaného a vrátí existující cílovou referenci místo rekurze. Tentýž lookup je to, co zařídí, že font, proud vzhledu nebo akce sdílené šesti widgety se naklonují jednou a referencují šestkrát. Mapa je vázána na zdrojový dokument hashem SHA-256 zdrojových bajtů, vystaveným jako SourceIdentity. Předáte-li GraftPdfAcroForm mapu, jejíž identita nesouhlasí se zdrojem, který jste předali, odmítne volání, místo aby znovu použila reference, které pro tento soubor nikdy nebyly platné. Mapování stránek se do téže mapy zaseje před začátkem klonování a právě proto končí /P widgetu ukazující na cílovou stránku: zdrojový objekt stránky se už vyřeší na mapovaný cílový objekt stránky, takže běžný průchod přepisující reference to zařídí bez zvláštního případu

Graft mapa PDFiumPas napříč dokumenty v Delphi klíčuje každou zdrojovou referenci číslem objektu a generací, zaregistruje cílové mapování před sestupem, takže zpětná reference rodiče terminuje, a vrátí existující položku, takže sdílený font se klonuje jen jednou
Registrace mapování před průchodem dětmi je to, co činí cyklický graf bezpečným a sdílený objekt se klonuje přesně jednou
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 přidané tímto voláním byly vráceny zpět;
      // cokoli zaregistrovaného před ním je stále neporušené.
      Assert(GraftMap.Count = EntriesBefore);
      WriteLn('graft refused: ', Report.ErrorMessage);
    end;
  finally
    GraftMap.Free;
  end;
end;

Ta rollback je pointa vlastnit mapu sami. PDFiumPas zachází s mapou dodanou volajícím transakčně: nezdařený graft zahodí položky, které toto volání přidalo, a podrží každé mapování, které existovalo předtím, takže jedno odmítnutí nikdy nezanechá cache referencí na objekty, které nikdy nebyly zapsány. Vedejte však jednu mapu na cílový dokument — cílová strana každé položky je číslo objektu v tom konkrétním souboru a v jiném neznamená nic

Kolize názvů polí: odmítnout nebo přejmenovat

Plně kvalifikované názvy polí musí zůstat uvnitř formuláře jedinečné a PDFiumPas nebude hádat, co jste mysleli, když se srazí. TPdfAcroFormCollisionPolicy nabízí přesně dvě odpovědi. Pod pagcpReject, výchozí, první zdrojové pole, jehož název už v cíli existuje, přeruší celý graft chybou a nechá výstupní proud prázdný. Pod pagcpRename se srazující zdrojové pole přejmenuje předponou RenamePrefix a graft pokračuje, přičemž Report.RenamedFieldCount řekne, jak často se 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);

Přejmenování není zdarma a měli byste se rozhodnout záměrně, místo abyste po něm sáhli, aby chyba zmizela. Přejmenované pole je jiné pole: jakýkoli JavaScript v cíli, který jej adresuje názvem, jakákoli položka výpočtu v /CO, kterou člověk napsal proti starému názvu, a jakýkoli downstream konzument klíčující na název pole budou potřebovat vědět o předponě. Pokud dva dokumenty skutečně popisují totéž pole, poctivná oprava bývá smířit názvy naproud, ne v momentě graftu. Jakmile graft dopadne, přirozeným dalším krokem je projít sloučený formulář a potvrdit, co jste skutečně dostali, a navigace polí formuláře v PDFiumPas pokrývá tento průchod

Kde graft záměrně selhává uzavřeně

Každá dvojznačná podmínka je chyba, nikdy výsledek na nejlepší úsilí, a to je návrhové rozhodnutí, které stojí za pochopení, dřív než vás v produkci překvapí. GraftPdfAcroForm vrací False, resetuje výstupní proud a hlásí důvod, když narazí na kterékoli z těchto

  • Zdrojový formulář nese položku /XFA — pakety XFA jsou paralelní model formulářů a nedají se zredukovat na slovníky polí AcroForm
  • Widget žije na zdrojové stránce bez položky v mapování stránek, což by jinak tiše zahodilo pole nebo je připojilo ke špatné stránce
  • Mapování stránek jsou mimo rozsah, nebo dvě mapování znovu použijí tutéž zdrojovou či cílovou stránku
  • Oba formuláře definují výchozí zdrojový slovník /DR, protože sloučení dvou jmenných prostorů zdrojů riskuje přesměrování existujícího názvu na jiný font
  • Graf objektů překročí MaxObjects nebo rekurze překročí MaxDepth
  • Cíl obsahuje podpis a AllowSignedDestination je False
  • Dodaná graft mapa patří jinému zdrojovému dokumentu, nebo zdrojová reference visí

Zápisová cesta je stejně konzervativní. PDFiumPas vydá výsledek jako řídkou inkrementální revizi připojenou k cíli, pak re-materializuje zapsaný výstup a znovu načte jeho formulář: nerovná-li se počet polí výsledku původnímu počtu polí cíle plus zdroje, celý graft je odmítnut a výstup vymazán. Nikdy nedostanete částečně graftovaný soubor. Cena té politiky je reálná — kolize /DR nebo podepsaný cíl vás zastaví rovnou a musíte to vyřešit sami, místo abyste přijali sloučenou aproximaci — ale alternativa je formulář, který se otevře v pohodě a počítá špatně

Jak PDFiumPas GraftPdfAcroForm selhává uzavřeně v Delphi: zapsaná revize se znovu načte a ověří počet jejích polí, jakákoli dvojznačná podmínka, jako je XFA nebo nemapovaná stránka, odmítne volání a odmítnutí zahodí jen položky mapy, které toto volání přidalo
Ověřená zápisová cesta a transakční mapa jsou důvodem, proč odmítnutý graft nikdy nezanechá částečně sloučený soubor

Kdy je grafting špatný nástroj

Grafting přesouvá strukturu, takže jej použijte, když vám chybí právě struktura. Nesou-li oba dokumenty už tutéž sadu polí a potřebujete jen přesunout hodnoty a anotace mezi nimi, cesta exportu a importu v článku o datu formulářů XFDF je lehčí, standardní a vratná. Sáhněte po GraftPdfAcroForm, když cíl nemá vůbec žádná pole, nebo má jinou sadu, a potřebujete, aby widgety, proudy vzhledu, akce a pořadí výpočtů přišly neporušené. Poslední praktická poznámka k identitě: protože graft mapa klíčuje na číslo objektu plus generaci a je vázána na SHA-256 zdrojových bajtů, znovuuložení nebo optimalizace zdroje mezi běhy vytvoří jinou identitu a mapu, která už neplatí. Vyfoťte zdroj, ze kterého graftujete, a podržte jej stabilní pro dávku; berte jej jako vstupní artefakt, ne jako něco, co noční úloha smí přepsat

GraftPdfAcroForm, TPdfCrossDocumentGraftMap a okolní PDF toolkit na úrovni proudu se dodávají s PDFiumPas Delphi PDFium Component pro Delphi, C++Builder a Lazarus, kde produktová stránka nese úplnou referenci API pro volby graftu, pole reportu a zbytek povrchu editace dokumentů