Einen Block von Formularfeldern aus der Vorlage des letzten Jahres auf das Layout dieses Jahres zu bringen ist der Punkt, an dem FDF- und XFDF-Round-trips nicht mehr reichen: Die Werte kommen an, aber die Appearance-Streams, die Berechnungsaktionen und die Default-Ressourcen nicht. PDFiumPas beantwortet diesen Fall mit GraftPdfAcroForm, das den gesamten Feld-Objektgraphen aus einem PDF klont und in ein anderes schreibt
Der Grund, warum ein Export auf Datenebene das nicht kann, ist strukturell. Ein Feld ist kein Record, es ist ein Subgraph. ISO 32000-1 §12.7 definiert das interaktive Formular-Dictionary, das /Fields, /CO, /DR und /DA hält, §12.7.3 definiert die darunter hängenden Field-Dictionaries, und §12.5.6.19 definiert die Widget-Annotationen, die diesen Feldern eine sichtbare Box auf einer Seite geben. XFDF trägt die Blätter dieser Struktur. Grafting trägt die Struktur selbst
Warum das Kopieren des /Fields-Arrays nie reicht
Das Kopieren von /Fields aus einem Dokument in ein anderes erzeugt ein Formular, das auf jede interessante Weise kaputt ist, denn das Array hält indirekte Referenzen und sonst nichts. ISO 32000-1 §7.3.10 macht ein indirektes Objekt über Objektnummer plus Generation adressierbar, und diese Nummern sind nur innerhalb der Datei bedeutsam, aus der sie stammen. Fügt man das Array hinüber ein, hängt entweder jede Referenz darin ins Leere oder — schlimmer — löst stillschweigend zu einem unrelated Objekt auf, das zufällig diesen Slot im Ziel besetzt. Unter jeder Referenz sitzt ein Graph, der sowohl geteilt als auch zyklisch ist. Ein Field-Dictionary zeigt auf seine Kids, jedes Kid zeigt zurück auf sein /Parent, ein Widget zeigt auf seine Appearance-Streams und über /P auf die Seite, die es trägt, Appearance-Streams zeigen auf Fonts im Default-Resource-Dictionary des Formulars, und Additional-Action-Dictionaries unter /AA zeigen auf noch mehr Objekte. Zwei Widgets auf verschiedenen Seiten teilen routinemäßig einen Font und ein Appearance-XObject. Ein korrekter Graft muss also diesen Graphen durchwandern, jedes erreichbare Objekt genau einmal klonen, das /P jedes Widgets auf die gemappte Zielseite umleiten und das geklonte Widget in das /Annots-Array dieser Seite einfügen — sonst existiert das Feld im Formular und ist auf der Seite unsichtbar. Wenn Sie dem Unterschied zwischen einem Feld, seinem Widget und der Seiten-Annotation, die es darstellt, nachgegangen sind, behandelt unsere Notiz zu Widget-Index versus Annotation-Index genau diese Spaltung
Was braucht GraftPdfAcroForm von Ihnen?
Er braucht drei getrennte Streams und ein explizites Seiten-Mapping. GraftPdfAcroForm nimmt Source, Destination und Output als getrennte TStream-Instanzen, ein TPdfGraftPageMappings-Array, einen TPdfAcroFormGraftOptions-Record, eine optionale TPdfCrossDocumentGraftMap und einen Out-TPdfAcroFormGraftReport entgegen. Er gibt Boolean zurück, statt zu werfen, und im Fehlerfall trägt der Report den Grund in ErrorMessage. Das Seiten-Mapping ist beidseitig einsbasiert und wird nicht hergeleitet: Jede Quellseite, die ein Widget trägt, das Sie verpflanzen wollen, muss darin auftauchen. nil für die Graft-Map zu übergeben ist legitim — die Funktion erzeugt dann eine private und gibt sie nach dem Aufruf frei — und TPdfAcroFormGraftOptions.Default liefert Ihnen CollisionPolicy auf pagcpReject, RenamePrefix auf Imported_, MaxObjects von 100000, MaxDepth von 128 und AllowSignedDestination auf False. Die letzten drei sind Budgets, und sie existieren, weil der Objektgraph, den Sie gleich durchwandern, aus einer Datei stammt, die Sie nicht geschrieben haben
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;
Wie vermeidet die Graft-Map, einen geteilten Font zweimal zu klonen?
TPdfCrossDocumentGraftMap hält eine Quell-zu-Ziel-Referenztabelle, deren Schlüssel sowohl Objektnummer als auch Generation tragen, und der rekursive Cloner konsultiert sie, bevor er absteigt. Die Reihenfolge der Operationen ist es, die Zyklen sicher macht: Der Cloner allokiert die Ziel-Objektnummer und registriert das Mapping zuerst, dann wandert er die Child-Referenzen des Quellobjekts ab. Ein Parent, der ein Kid erreicht, das auf seinen Parent zurückzeigt, findet den Parent bereits registriert und gibt die existierende Ziel-Referenz zurück, statt zu rekursieren. Dieselbe Nachschlage bewirkt, dass ein Font, ein Appearance-Stream oder eine Aktion, die von sechs Widgets geteilt wird, einmal geklont und sechsmal referenziert wird. Die Map ist über einen SHA-256-Hash der Quell-Bytes an das Quelldokument gebunden, exponiert als SourceIdentity. Reichen Sie GraftPdfAcroForm eine Map, deren Identität nicht zur übergebenen Quelle passt, verweigert es den Aufruf, statt Referenzen wiederzuverwenden, die für diese Datei nie gültig waren. Die Seiten-Mappings werden eingesät, bevor das Klonen beginnt, und genau dadurch endet das /P eines Widgets auf der Zielseite: Das Quellseiten-Objekt löst bereits zum gemappten Zielseiten-Objekt auf, also behandelt der gewöhnliche Referenz-Umschreib-Durchlauf es ohne Sonderfall
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
// Von diesem Aufruf hinzugefügte Einträge wurden zurückgerollt;
// alles, was vorher registriert war, ist noch intakt.
Assert(GraftMap.Count = EntriesBefore);
WriteLn('graft refused: ', Report.ErrorMessage);
end;
finally
GraftMap.Free;
end;
end;
Dieses Rollback ist der Punkt, die Map selbst zu besitzen. PDFiumPas behandelt eine vom Aufrufer gelieferte Map transaktional: Ein gescheiterter Graft verwirft die Einträge, die dieser Aufruf hinzufügte, und bewahrt jedes Mapping, das vorher existierte, sodass eine Verweigerung nie einen Cache von Referenzen auf Objekte zurücklässt, die nie geschrieben wurden. Halten Sie aber eine Map pro Zieldokument — die Zielseite jedes Eintrags ist eine Objektnummer in genau dieser Datei und bedeutet in einer anderen nichts
Feldnamen-Kollisionen: verwerfen oder umbenennen
Voll qualifizierte Feldnamen müssen innerhalb eines Formulars eindeutig bleiben, und PDFiumPas rät nicht, was Sie meinten, als sie kollidieren. TPdfAcroFormCollisionPolicy bietet genau zwei Antworten. Unter pagcpReject, dem Default, bricht das erste Quellfeld, dessen Titel im Ziel bereits existiert, den ganzen Graft mit einem Fehler ab und lässt den Output-Stream leer. Unter pagcpRename wird das kollidierende Quellfeld umbenannt, indem RenamePrefix vorangestellt wird, und der Graft läuft weiter, wobei Report.RenamedFieldCount Ihnen sagt, wie oft das geschah
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);
Umbenennen ist nicht gratis, und Sie sollten es bewusst entscheiden, statt danach zu greifen, um einen Fehler verschwinden zu lassen. Ein umbenanntes Feld ist ein anderes Feld: Jegliches JavaScript im Ziel, das es beim Namen anspricht, jeder Berechnungseintrag in /CO, den ein Mensch gegen den alten Namen schrieb, und jeder nachgelagerte Konsument, der auf den Feldnamen keyt, muss über das Präfix Bescheid wissen. Wenn die beiden Dokumente dasselbe Feld wirklich beschreiben, ist der ehrliche Fix meist, die Namen vorgelagert zu versöhnen, nicht zur Graft-Zeit. Sobald der Graft gelandet ist, ist das Durchwandern des zusammengeführten Formulars, um zu bestätigen, was Sie tatsächlich bekommen haben, der natürliche nächste Schritt, und die Formularfeld-Navigation in PDFiumPas deckt diese Traversierung ab
Wo der Graft bewusst fail-closed scheitert
Jede mehrdeutige Bedingung ist ein Fehler, nie ein Best-Effort-Ergebnis, und das ist eine Designentscheidung, die es zu verstehen lohnt, bevor sie Sie in der Produktion überrascht. GraftPdfAcroForm gibt False zurück, setzt den Output-Stream zurück und meldet den Grund, wenn es auf eines davon stößt
- Das Quellformular trägt einen
/XFA-Eintrag — XFA-Pakete sind ein paralleles Formularmodell und lassen sich nicht auf AcroForm-Field-Dictionaries reduzieren - Ein Widget lebt auf einer Quellseite, die keinen Eintrag im Seiten-Mapping hat, was sonst das Feld stillschweigend fallen ließe oder an die falsche Seite anhängte
- Seiten-Mappings sind außerhalb des Bereichs, oder zwei Mappings benutzen dieselbe Quell- oder Zielseite wieder
- Beide Formulare definieren ein Default-Resource-Dictionary
/DR, denn zwei Resource-Name-Spaces zu verschmelzen riskierte, einen existierenden Namen auf einen anderen Font umzupunkten - Der Objektgraph überschreitet
MaxObjects, oder die Rekursion überschreitetMaxDepth - Das Ziel enthält eine Signatur, und
AllowSignedDestinationistFalse - Die gelieferte Graft-Map gehört zu einem anderen Quelldokument, oder eine Quellreferenz hängt ins Leere
Der Schreibpfad ist genauso konservativ. PDFiumPas gibt das Ergebnis als sparse inkrementelle Revision aus, die an das Ziel angehängt wird, dann materialisiert es die geschriebene Ausgabe neu und liest ihr Formular erneut: Entspricht die Feldzahl des Results nicht der ursprünglichen Feldzahl des Ziels plus der der Quelle, wird der ganze Graft verworfen und die Ausgabe geleert. Sie bekommen nie eine teilweise verpflanzte Datei. Die Kosten dieser Politik sind real — eine /DR-Kollision oder ein signiertes Ziel stoppt Sie kategorisch, und Sie müssen es selbst auflösen, statt eine verschmolzene Annäherung zu akzeptieren — aber die Alternative ist ein Formular, das sich ordentlich öffnet und falsch rechnet
Wann Verpflanzen das falsche Werkzeug ist
Grafting bewegt Struktur, also benutzen Sie es, wenn die Struktur das ist, was Ihnen fehlt. Wenn beide Dokumente bereits dasselbe Feldset tragen und Sie nur Werte und Annotationen zwischen ihnen bewegen müssen, ist der Export- und Import-Pfad im XFDF-Formulardaten-Artikel leichter, standardkonform und reversibel. Greifen Sie zu GraftPdfAcroForm, wenn das Ziel gar keine Felder hat oder ein anderes Set, und Sie brauchen die Widgets, Appearance-Streams, Aktionen und die Berechnungsreihenfolge intakt hinüber. Eine letzte praktische Notiz zur Identität: Weil die Graft-Map auf Objektnummer plus Generation keyt und an einen SHA-256 der Quell-Bytes gebunden ist, erzeugt ein Neu-Speichern oder Optimieren der Quelle zwischen Läufen eine andere Identität und eine Map, die nicht mehr passt. Snapshoten Sie die Quelle, von der Sie verpflanzen, und halten Sie sie für den Batch stabil; behandeln Sie sie als Eingabe-Artefakt, nicht als etwas, das ein nächtlicher Job frei umschreiben darf
GraftPdfAcroForm, TPdfCrossDocumentGraftMap und das umgebende Stream-Level-PDF-Toolkit kommen mit der PDFiumPas Delphi PDFium Component für Delphi, C++Builder und Lazarus, wo die Produktseite die vollständige API-Referenz für die Graft-Optionen, Report-Felder und den Rest der Dokumentbearbeitungs-Oberfläche trägt