Een blok formuliervelden verplaatsen van de sjabloon van vorig jaar naar de layout van dit jaar is het punt waar FDF- en XFDF-rondreizen niet langer volstaan: de waarden komen aan, maar de appearance streams, berekenacties en standaardresources niet. PDFiumPas beantwoordt dat geval met GraftPdfAcroForm, die de volledige veldobjectgrafiek uit de ene PDF kloont en in de andere wegschrijft
De reden dat een export op dataniveau dit niet kan is structureel. Een veld is geen record, het is een subgraaf. ISO 32000-1 §12.7 definieert het interactieve formulierenwoordenboek dat /Fields, /CO, /DR en /DA bevat, §12.7.3 definieert de veldwoordenboeken die eronder hangen, en §12.5.6.19 definieert de widgetannotaties die die velden een zichtbaar kader op een pagina geven. XFDF draagt de bladeren van die structuur. Enten draagt de structuur zelf
Waarom het kopiëren van de /Fields-array nooit genoeg is
/Fields uit het ene document naar het andere kopiëren levert een formulier op dat op elke interessante manier kapot is, want de array bevat indirecte referenties en niets anders. ISO 32000-1 §7.3.10 maakt een indirect object adresseerbaar met objectnummer plus generatie, en die nummers betekenen alleen iets binnen het bestand waar ze vandaan komen. Plak de array eroverheen en elke referentie erin hangt of lost, erger nog, geruisloos op op een ongerelateerd object dat toevallig die sleuf in de bestemming bezet. Onder elke referentie zit een graaf die zowel gedeeld als cyclisch is. Een veldwoordenboek wijst naar zijn kids, elke kid wijst terug naar zijn /Parent, een widget wijst naar zijn appearance streams en naar de pagina die hem draagt via /P, appearance streams wijzen naar fonts in het standaardresourcewoordenboek van het formulier, en additional-action-woordenboeken onder /AA wijzen naar nog meer objecten. Twee widgets op verschillende pagina's delen routinematig één font en één appearance XObject. Een correcte ent moet die graaf dus doorlopen, elk bereikbaar object precies één keer klonen, de /P van elke widget omleiden naar de toegewezen bestemmingspagina, en de gekloonde widget toevoegen aan de /Annots-array van die pagina — anders bestaat het veld in het formulier en is het onzichtbaar op de pagina. Als u het verschil tussen een veld, zijn widget en de pagina-annotatie die hem toont bent nagegaan, behandelt onze notitie over widget-index versus annotatie-index precies die scheiding
Wat heeft GraftPdfAcroForm van u nodig?
Hij heeft drie aparte streams en een expliciete paginatoewijzing nodig. GraftPdfAcroForm neemt Source, Destination en Output als aparte TStream-instanties, een TPdfGraftPageMappings-array, een TPdfAcroFormGraftOptions-record, een optionele TPdfCrossDocumentGraftMap, en een out-TPdfAcroFormGraftReport. Hij geeft Boolean terug in plaats van te gooien, en bij mislukking draagt het rapport de reden in ErrorMessage. De paginatoewijzing is aan beide kanten 1-based en wordt niet afgeleid: elke bronpagina die een widget draagt die u wilt enten moet erin voorkomen. nil voor de entmap doorgeven is legitiem — de functie creëert dan een privémap en geeft die vrij voor de duur van de aanroep — en TPdfAcroFormGraftOptions.Default geeft u CollisionPolicy op pagcpReject, RenamePrefix op Imported_, MaxObjects van 100000, MaxDepth van 128 en AllowSignedDestination op False. Die laatste drie zijn budgets, en ze bestaan omdat de objectgrafiek die u gaat doorlopen uit een bestand kwam dat u niet schreef
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;
Hoe voorkomt de entmap dat een gedeeld font twee keer wordt gekloond?
TPdfCrossDocumentGraftMap houdt een bron-naar-bestemming-referentietabel bij waarvan de sleutels zowel objectnummer als generatie dragen, en de recursieve klooner raadpleegt haar voordat hij afdaalt. De volgorde van operaties maakt cycli veilig: de klooner wijst het bestemmingsobjectnummer toe en registreert de toewijzing eerst, en loopt dan de kindverwijzingen van het bronobject na. Een ouder die een kid bereikt dat terugwijst naar zijn ouder vindt de ouder al geregistreerd en geeft de bestaande bestemmingsreferentie terug in plaats van te recursen. Dezelfde opzoeking zorgt ervoor dat een font, een appearance stream of een actie gedeeld door zes widgets één keer wordt gekloond en zes keer wordt gerefereerd. De map is aan het brondocument gebonden via een SHA-256-hash van de bronbytes, blootgesteld als SourceIdentity. Geeft u GraftPdfAcroForm een map waarvan de identiteit niet matcht met de bron die u doorgaf, dan weigert hij de aanroep in plaats van referenties te hergebruiken die voor dit bestand nooit geldig waren. De paginatoewijzingen worden in dezelfde map gezaaid voordat het klonen begint, en precies zo belandt de /P van een widget op de bestemmingspagina: het bronpaginaobject lost al op naar het toegewezen bestemmingspaginaobject, dus de gewone referentie-herschrijfpass behandelt het zonder speciaal geval
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 die deze aanroep toevoegde zijn teruggedraaid;
// alles wat ervoor geregistreerd was is nog intact.
Assert(GraftMap.Count = EntriesBefore);
WriteLn('graft refused: ', Report.ErrorMessage);
end;
finally
GraftMap.Free;
end;
end;
Die rollback is het punt van de map zelf bezitten. PDFiumPas behandelt een door de aanroeper geleverde map transactioneel: een mislukte ent gooit de entries weg die die aanroep toevoegde en behoudt elke toewijzing die er al was, dus één weigering laat nooit een cache van referenties naar objecten achter die nooit zijn weggeschreven. Houd wel één map per bestemmingsdocument bij — de bestemmingskant van elke entry is een objectnummer in dat specifieke bestand, en betekent niets in een ander
Veldnaambotsingen: weigeren of hernoemen
Volledig gekwalificeerde veldnamen moeten uniek blijven binnen een formulier, en PDFiumPas gist niet wat u bedoelde wanneer ze botsen. TPdfAcroFormCollisionPolicy biedt precies twee antwoorden. Onder pagcpReject, de standaard, breekt het eerste bronveld waarvan de titel al in de bestemming bestaat de hele ent af met een fout en laat de uitvoerstroom leeg. Onder pagcpRename wordt het botsende bronveld hernoemd door RenamePrefix toe te voegen en de ent gaat door, met Report.RenamedFieldCount die vertelt hoe vaak dat gebeurde
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);
Hernoemen is niet gratis, en u moet dat bewust beslissen in plaats van het te pakken om een fout te laten verdwijnen. Een hernoemd veld is een ander veld: elke JavaScript in de bestemming die hem bij naam aanspreekt, elke berekenentry in /CO die iemand tegen de oude naam schreef, en elke stroomafwaartse consument die op de veldnaam sluit wil van het voorvoegsel weten. Als de twee documenten echt hetzelfde veld beschrijven, is de eerlijke fix meestal de namen stroomopwaarts in lijn brengen, niet op entmoment. Zodra de ent landt, is het doorlopen van het samengevoegde formulier om te bevestigen wat u feitelijk kreeg de natuurlijke volgende stap, en formulierveldnavigatie in PDFiumPas behandelt die traversatie
Waar de ent bewust fail closed gaat
Elke ambigue voorwaarde is een fout, nooit een best-effort-resultaat, en dat is een ontwerpbeslissing die de moeite van begrijpen waard is voordat ze u in productie verrast. GraftPdfAcroForm geeft False terug, reset de uitvoerstroom en meldt de reden wanneer hij een van deze raakt
- Het bronformulier draagt een
/XFA-entry — XFA-pakketten zijn een parallel formuliemodel en kunnen niet worden gereduceerd tot AcroForm-veldwoordenboeken - Een widget woont op een bronpagina zonder entry in de paginatoewijzing, wat anders geruisloos het veld zou laten vallen of hem aan de verkeerde pagina zou hangen
- Paginatoewijzingen liggen buiten bereik, of twee toewijzingen hergebruiken dezelfde bron- of bestemmingspagina
- Beide formulieren definiëren een standaardresourcewoordenboek
/DR, want het samenvoegen van twee resourcenaamruimten loopt het risico een bestaande naam naar een ander font te herleiden - De objectgrafiek overschrijdt
MaxObjectsof de recursie overschrijdtMaxDepth - De bestemming bevat een handtekening en
AllowSignedDestinationisFalse - De geleverde entmap hoort bij een ander brondocument, of een bronreferentie hangt
Het schrijfpad is even conservatief. PDFiumPas emiteert het resultaat als een sparse incrementele revisie toegevoegd aan de bestemming, materialiseert het weggeschreven resultaat daarna opnieuw en leest zijn formulier opnieuw: als het veldaantal van het resultaat niet gelijk is aan het oorspronkelijke veldaantal van de bestemming plus dat van de bron, wordt de hele ent geweigerd en de uitvoer gewist. U krijgt nooit een gedeeltelijk geënt bestand. De kosten van dat beleid zijn reëel — een /DR-botsing of een ondertekende bestemming stopt u ronduit, en u moet het zelf oplossen in plaats van een samengevoegde benadering te accepteren — maar het alternatief is een formulier dat prima opent en verkeerd rekent
Wanneer enten het verkeerde gereedschap is
Enten verplaatst structuur, dus gebruik het wanneer de structuur is wat u mist. Als beide documenten al dezelfde veldset dragen en u alleen waarden en annotaties tussen hen hoeft te verplaatsen, is het export- en importpad in het XFDF-formuliergegevensartikel lichter, standaard en omkeerbaar. Pak GraftPdfAcroForm aan wanneer de bestemming helemaal geen velden heeft, of een andere set heeft, en u de widgets, appearance streams, acties en berekenvolgorde intact nodig hebt. Een laatste praktische notitie over identiteit: omdat de entmap sluit op objectnummer plus generatie en gebonden is aan een SHA-256 van de bronbytes, levert het heropslaan of optimaliseren van de bron tussen runs een andere identiteit op en een map die niet meer van toepassing is. Maak een momentopname van de bron waaruit u ent en houd haar stabiel voor de batch; behandel haar als een invoerartefact, niet als iets dat een nachtelijke taak vrij mag herschrijven
GraftPdfAcroForm, TPdfCrossDocumentGraftMap en de omringende stream-level PDF-toolkit verschepen met de PDFiumPas Delphi PDFium Component voor Delphi, C++Builder en Lazarus, waar de productpagina de volledige API-referentie draagt voor de entopties, rapportvelden en de rest van het documentbewerkingsoppervlak