Przeniesienie bloku pól formularza z zeszłorocznego szablonu na układ z tego roku to moment, w którym rundy FDF i XFDF przestają wystarczać: wartości przybywają, ale strumienie wyglądu, akcje obliczeń i zasoby domyślne już nie. PDFiumPas odpowiada na ten przypadek przez GraftPdfAcroForm, który klonuje cały graf obiektów pól z jednego PDF-a i zapisuje go w drugim
Powód, dla którego eksport na poziomie danych tego nie zrobi, jest strukturalny. Pole nie jest rekordem, jest podgrafem. ISO 32000-1 §12.7 definiuje słownik formularza interaktywnego trzymający /Fields, /CO, /DR i /DA, §12.7.3 definiuje wiszące pod nim słowniki pól, a §12.5.6.19 definiuje adnotacje widgetów, które dają tym polom widoczny prostokąt na stronie. XFDF przenosi liście tej struktury. Przeszczep przenosi samą strukturę
Dlaczego skopiowanie tablicy /Fields nigdy nie wystarcza
Skopiowanie /Fields z jednego dokumentu do drugiego daje formularz zepsuty na każdy ciekawy sposób, bo tablica trzyma wyłącznie referencje pośrednie. ISO 32000-1 §7.3.10 czyni obiekt pośredni adresowalnym przez numer obiektu plus generację, a te numery mają sens tylko wewnątrz pliku, z którego pochodzą. Wklej tablicę między pliki, a każda referencja w niej albo wisi, albo, gorzej, po cichu rozwiązuje się do niepowiązanego obiektu, który akurat zajmuje ten slot w dokumencie docelowym. Pod każdą referencją siedzi graf, który jest zarazem współdzielony i cykliczny. Słownik pola wskazuje swoje dzieci, każde dziecko wskazuje z powrotem swój /Parent, widget wskazuje swoje strumienie wyglądu i stronę, która go niesie, przez /P, strumienie wyglądu wskazują czcionki w domyślnym słowniku zasobów formularza, a słowniki akcji dodatkowych pod /AA wskazują jeszcze więcej obiektów. Dwa widgety na różnych stronach rutynowo dzielą jedną czcionkę i jeden XObject wyglądu. Poprawny przeszczep musi więc przejść ten graf, sklonować każdy osiągalny obiekt dokładnie raz, przekierować /P każdego widgeta na zmapowaną stronę docelową i dodać sklonowany widget do tablicy /Annots tej strony — w przeciwnym razie pole istnieje w formularzu i jest niewidoczne na stronie. Jeśli chcesz rozplątać różnicę między polem, jego widgetem a wyświetlającą go adnotacją strony, nasza notka o indeksie widgeta a indeksie adnotacji obejmuje dokładnie ten podział
Czego GraftPdfAcroForm potrzebuje od ciebie?
Potrzebuje trzech osobnych strumieni i jawnego mapowania stron. GraftPdfAcroForm przyjmuje Source, Destination i Output jako osobne instancje TStream, tablicę TPdfGraftPageMappings, rekord TPdfAcroFormGraftOptions, opcjonalną TPdfCrossDocumentGraftMap oraz wychodzący TPdfAcroFormGraftReport. Zwraca Boolean zamiast rzucać wyjątkiem, a przy niepowodzeniu raport niesie powód w ErrorMessage. Mapowanie stron jest liczone od jedynki po obu stronach i nie jest wnioskowane: każda strona źródłowa, na której siedzi widget, który zamierzasz przeszczepić, musi się w nim znaleźć. Przekazanie nil jako mapy przeszczepu jest legalne — funkcja tworzy wtedy prywatną mapę i zwalnia ją na czas wywołania — a TPdfAcroFormGraftOptions.Default daje CollisionPolicy ustawione na pagcpReject, RenamePrefix ustawiony na Imported_, MaxObjects równe 100000, MaxDepth równe 128 oraz AllowSignedDestination ustawione na False. Te ostatnie trzy to budżety, i istnieją dlatego, że graf obiektów, po którym zaraz przejdziesz, przyszedł z pliku spoza twojego kodu
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 mapa przeszczepu unika podwójnego klonowania współdzielonej czcionki?
TPdfCrossDocumentGraftMap trzyma tabelę referencji źródło–cel, której klucze niosą numer obiektu i generację, a rekurencyjny kloner konsultuje ją, zanim zejdzie głębiej. Kolejność operacji jest tym, co czyni cykle bezpiecznymi: kloner alokuje numer obiektu docelowego i rejestruje mapowanie najpierw, a dopiero potem przechodzi referencje dzieci obiektu źródłowego. Rodzic, który trafia na dziecko wskazujące z powrotem na rodzica, znajduje rodzica już zarejestrowanego i zwraca istniejącą referencję docelową zamiast rekurencji. To samo spojrzenie sprawia, że czcionka, strumień wyglądu albo akcja współdzielona przez sześć widgetów zostaje sklonowana raz i zreferencjonowana sześć razy. Mapa jest związana z dokumentem źródłowym hashem SHA-256 bajtów źródła, wystawionym jako SourceIdentity. Jeśli podasz GraftPdfAcroForm mapę, której tożsamość nie zgadza się z przekazanym źródłem, odmówi wywołania, zamiast użyć ponownie referencji, które nigdy nie były ważne dla tego pliku. Mapowania stron są zasiane do tej samej mapy przed rozpoczęciem klonowania i dokładnie tak /P widgeta kończy wskazywać stronę docelową: obiekt strony źródłowej już rozwiązuje się do zmapowanego obiektu strony docelowej, więc zwykły przebieg przepisywania referencji obsługuje to bez żadnego przypadku szczególnego
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
// Wpisy dodane przez to wywołanie zostały wycofane;
// wszystko zarejestrowane przed nim pozostaje nietknięte.
Assert(GraftMap.Count = EntriesBefore);
WriteLn('graft refused: ', Report.ErrorMessage);
end;
finally
GraftMap.Free;
end;
end;
To wycofanie jest sednem posiadania mapy na własność. PDFiumPas traktuje mapę dostarczoną przez wywołującego transakcyjnie: nieudany przeszczep wyrzuca wpisy dodane przez to wywołanie i zachowuje każde mapowanie istniejące wcześniej, więc jedna odmowa nigdy nie zostawia po sobie cache'a referencji do obiektów, które nigdy nie zostały zapisane. Trzymaj jednak jedną mapę na dokument docelowy — strona docelowa każdego wpisu to numer obiektu w tym konkretnym pliku i w innym nie znaczy nic
Kolizje nazw pól: odrzucić albo zmienić nazwę
W pełni kwalifikowane nazwy pól muszą pozostać unikalne wewnątrz formularza, a PDFiumPas nie będzie zgadywać, co masz na myśli, gdy się zderzą. TPdfAcroFormCollisionPolicy oferuje dokładnie dwie odpowiedzi. Przy pagcpReject, wartości domyślnej, pierwsze pole źródłowe, którego tytuł już istnieje w dokumencie docelowym, przerywa cały przeszczep z błędem i zostawia pusty strumień wyjściowy. Przy pagcpRename kolidujące pole źródłowe dostaje zmienioną nazwę przez doklejenie prefiksu RenamePrefix, a przeszczep trwa dalej, przy czym Report.RenamedFieldCount mówi ci, jak często to się zdarzyło
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);
Zmiana nazwy nie jest darmowa i traktuj ją jako świadomą decyzję, a nie sposób, żeby błąd zniknął. Pole po zmianie nazwy to inne pole: każdy JavaScript w dokumencie docelowym, który adresuje je po nazwie, każdy wpis obliczeń w /CO pisany przez człowieka przeciw starej nazwie i każdy konsumer downstream kluczujący po nazwie pola będzie musiał się dowiedzieć o prefiksie. Jeśli oba dokumenty naprawdę opisują to samo pole, uczciwa naprawa zwykle polega na uzgodnieniu nazw upstream, a nie w chwili przeszczepu. Gdy przeszczep już wyląduje, naturalnym następnym krokiem jest przejście scalonego formularza, żeby potwierdzić, co faktycznie trafiło do wyniku, a nawigacja po polach formularza w PDFiumPas obejmuje to przejście
Gdzie przeszczep celowo działa w trybie fail closed
Każdy niejednoznaczny warunek jest błędem, nigdy rezultatem w duchu „jakoś to będzie", i to decyzja projektowa warta zrozumienia, zanim zaskoczy cię na produkcji. GraftPdfAcroForm zwraca False, zeruje strumień wyjściowy i raportuje powód, gdy trafi na którykolwiek z tych przypadków
- Formularz źródłowy niesie wpis
/XFA— pakiety XFA to równoległy model formularzy i nie dają się zredukować do słowników pól AcroForm - Widget mieszka na stronie źródłowej, która nie ma wpisu w mapowaniu stron — bez tego pole zostałoby po cichu upuszczone albo przyczepione do złej strony
- Mapowania stron wykraczają poza zakres albo dwa mapowania używają tej samej strony źródłowej lub docelowej
- Oba formularze definiują domyślny słownik zasobów
/DR, bo scalenie dwóch przestrzeni nazw zasobów grozi przepięciem istniejącej nazwy na inną czcionkę - Graf obiektów przekracza
MaxObjectsalbo rekurencja przekraczaMaxDepth - Dokument docelowy zawiera podpis, a
AllowSignedDestinationma wartośćFalse - Dostarczona mapa przeszczepu należy do innego dokumentu źródłowego albo jakaś referencja źródłowa wisi
Ścieżka zapisu jest równie konserwatywna. PDFiumPas emituje wynik jako rzadką rewizję przyrostową doklejoną do dokumentu docelowego, potem od nowa materializuje zapisane wyjście i odczytuje jego formularz: jeśli liczba pól wyniku nie równa się pierwotnej liczbie pól dokumentu docelowego plus liczbie pól ze źródła, cały przeszczep zostaje odrzucony, a wyjście wyczyszczone. Nigdy nie dostajesz pliku przeszczepionego w połowie. Koszt tej polityki jest realny — kolizja /DR albo podpisany dokument docelowy zatrzymuje cię całkowicie i musisz to rozwiązać samodzielnie, zamiast przyjąć scalone przybliżenie — ale alternatywą jest formularz, który otwiera się dobrze i liczy źle
Kiedy przeszczep to złe narzędzie
Przeszczep przenosi strukturę, więc używaj go, gdy to struktury ci brakuje. Jeśli oba dokumenty już niosą ten sam zestaw pól, a potrzebujesz tylko przenieść między nimi wartości i adnotacje, ścieżka eksportu i importu w artykule o danych formularza XFDF jest lżejsza, standardowa i odwracalna. Sięgaj po GraftPdfAcroForm, gdy dokument docelowy nie ma pól wcale albo ma inny zestaw, a potrzebujesz, żeby widgety, strumienie wyglądu, akcje i kolejność obliczeń przeszły nietknięte. Ostatnia praktyczna notka o tożsamości: ponieważ mapa przeszczepu kluczuje numerem obiektu plus generacją i jest związana z SHA-256 bajtów źródła, ponowny zapis albo optymalizacja źródła między uruchomieniami daje inną tożsamość i mapę, która już nie pasuje. Zrób migawkę źródła, z którego przeszczepiasz, i trzymaj je stabilnym dla całej partii; traktuj je jako artefakt wejściowy, a nie coś, co nocne zadanie może sobie swobodnie nadpisać
GraftPdfAcroForm, TPdfCrossDocumentGraftMap i otaczający zestaw narzędzi PDF na poziomie strumieni są dostarczane z PDFiumPas Delphi PDFium Component dla Delphi, C++Builder i Lazarus, gdzie strona produktu niesie pełną referencję API opcji przeszczepu, pól raportu i reszty powierzchni edycji dokumentów