Техническа статия

Присаждане на AcroForm полета между PDF файлове в Delphi

Местенето на блок от полета на форма от миналогодишния шаблон върху тазгодишното оформление е мястото, където обиколките през FDF и XFDF спират да стигат: стойностите пристигат, но потоците за външен вид, изчислителните действия и ресурсите по подразбиране не. PDFiumPas отговаря на този случай с GraftPdfAcroForm, който клонира целия обектен граф на полето от единия PDF и го записва в другия

Причината износ на ниво данни да не може да свърши това е структурна. Полето не е запис, а подграф. ISO 32000-1 §12.7 дефинира речника на интерактивната форма, който държи /Fields, /CO, /DR и /DA, §12.7.3 дефинира речниците на полетата, увиснали под него, а §12.5.6.19 дефинира widget анотациите, които дават на тези полета видима кутия върху страница. XFDF носи листата на тази структура. Присаждането носи самата структура

Защо копирането на масива /Fields никога не е достатъчно

Копирането на /Fields от един документ в друг произвежда форма, счупена по всеки интересен начин, защото масивът съдържа индиректни препратки и нищо друго. ISO 32000-1 §7.3.10 прави индиректния обект адресируем по номер на обект плюс поколение, а тези номера имат смисъл само вътре във файла, от който идват. Поставете масива в друг файл и всяка препратка в него или увисва, или, което е по-лошо, тихо се разрешава до несвързан обект, който случайно заема този слот в целевия документ. Под всяка препратка стои граф, който е едновременно споделен и цикличен. Речникът на полето сочи към децата си, всяко дете сочи обратно към своя /Parent, widget сочи към потоците си за външен вид и към страницата, която го носи, чрез /P, потоците за външен вид сочат към шрифтове в речника с ресурси по подразбиране на формата, а речниците с допълнителни действия под /AA сочат към още обекти. Два widget-а на различни страници редовно споделят един шрифт и един XObject за външен вид. Затова коректното присаждане трябва да обходи този граф, да клонира всеки достижим обект точно веднъж, да пренасочи /P на всеки widget към съпоставената целева страница и да добави клонирания widget в масива /Annots на тази страница — иначе полето съществува във формата и е невидимо на страницата. Ако сте гонили разликата между поле, неговия widget и анотацията на страницата, която го показва, бележката ни за индекс на widget срещу индекс на анотация покрива точно това разделение

Обектният граф зад едно PDF поле на форма, както PDFiumPas го присажда в Delphi: речникът на формата, полето, widget анотациите, масивите с анотации на целевите страници и потокът за външен вид и шрифтът, които двата widget-а споделят, плюс обратната препратка към родителя, която затваря цикъла
Полето е споделен цикличен подграф, затова копирането на масива /Fields между документи оставя всяка препратка увиснала

Какво иска GraftPdfAcroForm от вас?

Иска три отделни потока и явно съпоставяне на страници. GraftPdfAcroForm приема Source, Destination и Output като отделни инстанции на TStream, масив TPdfGraftPageMappings, запис TPdfAcroFormGraftOptions, незадължителна TPdfCrossDocumentGraftMap и изходен TPdfAcroFormGraftReport. Връща Boolean, вместо да вдига изключение, а при неуспех отчетът носи причината в ErrorMessage. Съпоставянето на страници започва от едно и от двете страни и не се подразбира: всяка изходна страница, която носи widget, който смятате да присадите, трябва да фигурира в него. Подаването на nil за graft картата е законно — тогава функцията създава и освобождава своя частна карта за времето на извикването — а 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 картата избягва двойно клониране на споделен шрифт?

TPdfCrossDocumentGraftMap държи таблица от препратки източник към цел, чиито ключове носят и номер на обект, и поколение, а рекурсивният клониращ механизъм я проверява, преди да слезе надолу. Редът на операциите е това, което прави циклите безопасни: клониращият механизъм заделя целевия номер на обект и регистрира съпоставянето първо, после обхожда препратките към децата на изходния обект. Родител, който стигне до дете, сочещо обратно към родителя си, намира родителя вече регистриран и връща съществуващата целева препратка, вместо да рекурсира. Същото търсене е това, което кара шрифт, поток за външен вид или действие, споделени от шест widget-а, да бъдат клонирани веднъж и посочени шест пъти. Картата е обвързана с изходния документ чрез SHA-256 хеш на изходните байтове, изложен като SourceIdentity. Ако подадете на GraftPdfAcroForm карта, чиято идентичност не съвпада с източника, който сте подали, тя отказва извикването, вместо да преизползва препратки, които никога не са били валидни за този файл. Съпоставянията на страници се засяват в същата карта, преди клонирането да започне, и точно така /P на widget-а накрая сочи към целевата страница: обектът на изходната страница вече се разрешава до съпоставения обект на целевата страница, така че обикновеното преминаване за пренаписване на препратки се справя без специален случай

Междудокументната graft карта на PDFiumPas в Delphi използва за ключ номера на обекта и поколението на всяка изходна препратка, регистрира целевото съпоставяне, преди да слезе надолу, така че обратната препратка към родителя приключва, и връща съществуващия запис, така че споделеният шрифт се клонира само веднъж
Регистрирането на съпоставянето преди обхождането на децата е това, което прави цикличния граф безопасен, а споделения обект клониран точно веднъж
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 третира подадената от извикващия карта транзакционно: неуспешното присаждане изхвърля записите, добавени от това извикване, и запазва всяко съпоставяне, съществувало преди него, така че един отказ никога не оставя след себе си кеш от препратки към обекти, които никога не са били записани. Но пазете по една карта за целеви документ — целевата страна на всеки запис е номер на обект в точно този файл и не означава нищо в друг

Сблъсъци на имена на полета: отказ или преименуване

Напълно квалифицираните имена на полета трябва да останат уникални вътре във формата, а PDFiumPas няма да гадае какво сте имали предвид, когато се сблъскат. TPdfAcroFormCollisionPolicy предлага точно два отговора. При pagcpReject, стойността по подразбиране, първото изходно поле, чието заглавие вече съществува в целевия документ, прекратява цялото присаждане с грешка и оставя изходния поток празен. При pagcpRename сблъскващото се изходно поле се преименува с префикс RenamePrefix и присаждането продължава, а 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, който човек е писал срещу старото име, и всеки последващ консуматор, който използва името на полето за ключ, ще трябва да знае за префикса. Ако двата документа наистина описват едно и също поле, честната поправка обикновено е имената да се съгласуват нагоре по веригата, а не по време на присаждане. Щом присаждането мине, обхождането на слятата форма, за да потвърдите какво всъщност сте получили, е естествената следваща стъпка, а навигацията в полета на форми в PDFiumPas покрива това обхождане

Къде присаждането умишлено се проваля безопасно

Всяко двусмислено условие е грешка, никога резултат по най-добро усилие, и това е проектно решение, което си струва да разберете, преди да ви изненада в продукция. GraftPdfAcroForm връща False, нулира изходния поток и докладва причината, когато срещне някое от тези

  • Изходната форма носи запис /XFA — XFA пакетите са паралелен модел на форма и не могат да бъдат сведени до AcroForm речници на полета
  • Widget живее на изходна страница, която няма запис в съпоставянето на страници, което иначе би изпуснало тихо полето или би го закачило за грешна страница
  • Съпоставянията на страници са извън обхват или две съпоставяния преизползват една и съща изходна или целева страница
  • И двете форми дефинират речник с ресурси по подразбиране /DR, защото сливането на две именни пространства за ресурси би рискувало съществуващо име да сочи към различен шрифт
  • Обектният граф надхвърля MaxObjects или рекурсията надхвърля MaxDepth
  • Целевият документ съдържа подпис, а AllowSignedDestination е False
  • Подадената graft карта принадлежи на друг изходен документ или изходна препратка увисва

Пътят на записа е също толкова консервативен. PDFiumPas издава резултата като разредена инкрементална ревизия, добавена към целевия документ, после материализира наново записания изход и прочита формата му отново: ако броят полета на резултата не е равен на оригиналния брой полета на целевия документ плюс този на източника, цялото присаждане се отхвърля и изходът се изчиства. Никога не получавате частично присаден файл. Цената на тази политика е реална — сблъсък на /DR или подписан целеви документ ви спират направо и трябва сами да го решите, вместо да приемете слято приближение — но алтернативата е форма, която се отваря добре и смята грешно

Как GraftPdfAcroForm на PDFiumPas се проваля безопасно в Delphi: записаната ревизия се прочита наново и броят на полетата ѝ се проверява, всяко двусмислено условие като XFA или несъпоставена страница отказва извикването, а отказът изхвърля само записите в картата, добавени от това извикване
Проверяваният път на запис и транзакционната карта са причината отказано присаждане никога да не остави частично слят файл

Кога присаждането е грешният инструмент

Присаждането мести структура, затова го ползвайте, когато структурата е онова, което ви липсва. Ако и двата документа вече носят едно и също множество полета и трябва само да местите стойности и анотации между тях, пътят на износ и внос в статията за XFDF данни на форми е по-лек, стандартен и обратим. Посегнете към GraftPdfAcroForm, когато целевият документ изобщо няма полета или има различно множество и ви трябват widget-ите, потоците за външен вид, действията и редът на изчисление да преминат непокътнати. Последна практична бележка за идентичността: тъй като graft картата използва за ключ номер на обект плюс поколение и е обвързана със SHA-256 на изходните байтове, повторно записване или оптимизиране на източника между изпълненията произвежда друга идентичност и карта, която вече не важи. Направете моментна снимка на източника, от който присаждате, и я пазете стабилна за целия пакет; третирайте я като входен артефакт, а не като нещо, което нощна задача е свободна да пренапише

GraftPdfAcroForm, TPdfCrossDocumentGraftMap и заобикалящият ги PDF инструментариум на ниво поток се доставят с PDFiumPas Delphi PDFium Component за Delphi, C++Builder и Lazarus, като продуктовата страница носи пълния API справочник за опциите на присаждането, полетата на отчета и останалата повърхност за редакция на документи