Перенесення блоку полів форми з шаблону торішнього року на компонування цього року — місце, де кола FDF і XFDF перестають бути достатніми: значення приходять, а потоки вигляду, дії обчислень і типові ресурси — ні. PDFiumPas відповідає на той випадок через GraftPdfAcroForm, який клонує весь граф обʼєктів полів з одного PDF і записує його в інший
Причина, з якої експорт на рівні даних цього не може, — структурна. Поле — не запис, воно є підграфом. ISO 32000-1 §12.7 визначає словник інтерактивної форми, що тримає /Fields, /CO, /DR і /DA, §12.7.3 визначає словники полів, що висять під ним, а §12.5.6.19 визначає анотації віджетів, які дають цим полям видиму коробку на сторінці. XFDF несе листя тієї структури. Перенесення (grafting) несе саму структуру
Чому копіювання масиву /Fields ніколи не достатньо
Копіювання /Fields з одного документа в інший дає форму, зламану всіма цікавими способами, бо масив тримає непрямі посилання і більше нічого. ISO 32000-1 §7.3.10 робить непрямий обʼект адресованим за номером обʼєкта плюс поколінням, а ті числа мають сенс лише всередині файла, з якого прийшли. Вклейте масив через — і кожне посилання в ньому або висне в повітрі, або, гірше, мовчки розвʼязується в сторонній обʼєкт, що випадково займає той слот у цільовому файлі. Під кожним посиланням сидить граф, що одночасно спільний і циклічний. Словник поля вказує на свої дітей, кожна дитина вказує назад на свій /Parent, віджет вказує на свої потоки вигляду і на сторінку, що несе його, через /P, потоки вигляду вказують на шрифти в типовому ресурсному словнику форми, а словники additional-action під /AA вказують на ще більше обʼєктів. Два віджети на різних сторінках регулярно ділять один шрифт і один appearance XObject. Тож правильне перенесення мусить обійти той граф, клонувати кожен досяжний обʼєкт рівно один раз, перенаправити /P кожного віджета на замаплену сторінку призначення і додати склонований віджет до масиву /Annots тієї сторінки — інакше поле існує у формі і невидиме на сторінці. Якщо ви ганялися за різницею між полем, його віджетом і анотацією сторінки, що його показує, наша замітка про індекс віджета проти індекса анотації покриває рівно той поділ
Що GraftPdfAcroForm потребує від вас?
Він потребує трьох окремих потоків і явного мапування сторінок. GraftPdfAcroForm бере Source, Destination і Output як окремі примірники TStream, масив TPdfGraftPageMappings, запис TPdfAcroFormGraftOptions, необовʼязковий TPdfCrossDocumentGraftMap і вихідний TPdfAcroFormGraftReport. Він повертає Boolean, а не кидає виняток, і при невдачі звіт несе причину в ErrorMessage. Мапування сторінок з одиниці з обох боків і не виводиться: кожна вихідна сторінка, що несе віджет, який ви збираєтеся перенести, мусить у ньому фігурувати. Передати nil замість graft map — легітимно: функція тоді створює і звільняє приватну на час виклику, а TPdfAcroFormGraftOptions.Default дає вам CollisionPolicy у pagcpReject, RenamePrefix у Imported_, MaxObjects 100000, MaxDepth 128 і AllowSignedDestination у False. Три останні — бюджети, і вони існують тому, що граф обʼєктів, який ви збираєтеся обходити, прийшов із файла, якого ви не писали
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;
Як graft map уникає подвійного клонування спільного шрифту?
TPdfCrossDocumentGraftMap тримає таблицю посилань джерело-призначення, чиї ключі несуть і номер обʼєкта, і покоління, і рекурсивний клонер звіряється з нею, перш ніж спуститися. Порядок операцій — те, що робить цикли безпечними: клонер виділяє номер обʼєкта призначення і реєструє мапування спершу, а потім обходить дочірні посилання вихідного обʼєкта. Батько, що дістався дитини, яка вказує назад на свого батька, знаходить батька вже зареєстрованим і повертає наявне посилання призначення замість рекурсії. Той самий пошук — те, що змушує шрифт, потік вигляду або дію, спільні для шести віджетів, клонуватися один раз і посилатися шість разів. Мапа привʼязана до вихідного документа SHA-256 хешем вихідних байтів, виставленим як SourceIdentity. Якщо ви подасте GraftPdfAcroForm мапу, чия ідентичність не збігається з переданим джерелом, вона відмовить виклик, замість перевикористовувати посилання, які ніколи не були валідні для цього файла. Мапування сторінок засіваються в ту саму мапу до початку клонування — рівно тому /P віджета опиняється вказуючи на сторінку призначення: вихідний обʼєкт сторінки вже розвʼязується в замаплений обʼєкт сторінки призначення, тож звичайний прохід переписування посилань обробляє це без особливого випадку
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
// Записи, додані цим викликом, відкочено назад;
// усе, що зареєстровано до нього, досі ціле.
Assert(GraftMap.Count = EntriesBefore);
WriteLn('graft refused: ', Report.ErrorMessage);
end;
finally
GraftMap.Free;
end;
end;
Той відкат — суть володіння мапою власноруч. PDFiumPas трактує подану викликачем мапу транзакційно: невдалий graft відкидає записи, які додав той виклик, і тримає кожне мапування, що існувало заздалегідь, тож одна відмова ніколи не лишає по собі кеш посилань на обʼєкти, які ніколи не були записані. Тримайте одну мапу на документ призначення, щоправда — сторона призначення кожного запису є номером обʼєкта в тому конкретному файлі, і в іншому вона не означає нічого
Колізії назв полів: відхилити чи перейменувати
Повністю кваліфіковані назви полів мусять лишатися унікальними всередині форми, і PDFiumPas не буде вгадувати, що ви мали на увазі, коли вони стикаються. TPdfAcroFormCollisionPolicy пропонує рівно дві відповіді. За pagcpReject, типовим, перше вихідне поле, чия назва вже існує в призначенні, перериває весь graft з помилкою і лишає вихідний потік порожнім. За pagcpRename, поле, що зіткнулося, перейменовується додаванням префікса RenamePrefix, і graft триває, а Report.RenamedFieldCount каже, як часто це сталося
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);
Перейменування не безкоштовне, і вирішувати його варто свідомо, а не тягнутися до нього, щоб прибрати помилку. Перейменоване поле — інше поле: будь-який JavaScript у призначенні, що адресує його за назвою, будь-який запис обчислень у /CO, який людина написала під стару назву, і будь-який нижчий за течією споживач, що ключується на назву поля, муситимуть дізнатися про префікс. Якщо два документа справді описують те саме поле, чесне виправлення зазвичай — звірити назви вище за течією, а не в момент graft. Коли graft приземлився, обійти злиту форму, щоб підтвердити, що ви справді отримали, — природний наступний крок, і навігація полями форми в PDFiumPas покриває той обхід
Де graft свідомо відмовляє закрито
Кожна неоднозначна умова — помилка, ніколи не результат «як найкраще вийде», і це дизайн-рішення, варте розуміння до того, як воно вас здивує в продакшені. GraftPdfAcroForm повертає False, скидає вихідний потік і звітує причину, коли наштовхується на будь-що з цього
- Вихідна форма несе запис
/XFA— пакети XFA є паралельною моделлю форм і не зводяться до словників полів AcroForm - Віджет живе на вихідній сторінці, якої немає в мапуванні сторінок, — інакше поле було б мовчки втрачено або прикріплено до неправильної сторінки
- Мапування сторінок поза діапазоном, або два мапування перевикористовують ту саму вихідну чи цільову сторінку
- Обидві форми визначають типовий ресурсний словник
/DR, бо злиття двох просторів ресурсних назв ризикувало б перенаправити наявну назву на інший шрифт - Граф обʼєктів перевищує
MaxObjects, або рекурсія перевищуєMaxDepth - Призначення містить підпис, а
AllowSignedDestination—False - Подана graft map належить іншому вихідному документу, або вихідне посилання висне
Шлях запису так само консервативний. PDFiumPas випромінює результат як розріджену інкрементну ревізію, додану до призначення, потім рематеріалізує записаний вихід і перечитує його форму: якщо кількість полів результату не дорівнює початковій кількості полів призначення плюс вихідній, весь graft відхилено, а вихід очищено. Ви ніколи не отримаєте частково перенесеного файла. Ціна тієї політики реальна — колізія /DR чи підписане призначення зупиняють вас начисто, і розвʼязати це треба самому, замість прийняти злите наближення, — але альтернатива — форма, що відкривається гаразд і обчислює неправильно
Коли перенесення — неправильний інструмент
Перенесення рухає структуру, тож застосовуйте його, коли структури вам і бракує. Якщо обидва документи вже несуть той самий набір полів, а вам треба лише перемістити значення та анотації між ними, шлях експорту та імпорту в статті про XFDF-дані форм легший, стандартний і оборотний. Тягніться до GraftPdfAcroForm, коли призначення взагалі не має полів або має інший набір, і вам потрібно, щоб віджети, потоки вигляду, дії та порядок обчислень перейшли неушкодженими. Остання практична примітка про ідентичність: оскільки graft map ключує за номером обʼєкта плюс поколінням і привʼязана до SHA-256 вихідних байтів, повторне збереження чи оптимізація джерела між прогонами дає іншу ідентичність і мапу, що більше не застосовується. Знімайте знімок джерела, з якого переносите, і тримайте його сталим для партії; ставтеся до нього як до вхідного артефакту, а не як до того, що нічний жоб вільний переписати
GraftPdfAcroForm, TPdfCrossDocumentGraftMap і навколишній потоковий PDF-інструментарій постачаються з PDFiumPas Delphi PDFium Component для Delphi, C++Builder і Lazarus, де продуктова сторінка несе повний довідник API опцій перенесення, полів звіту та решти поверхні редагування документів