PDF Library for Delphi объединяет два документа AcroForm с явной политикой для полей, разделяющих одно имя. MergeDocumentEx принимает идентификатор исходного документа и одну из трёх стратегий: dfsReject отклоняет слияние, dfsMerge сохраняет общее имя и синхронизирует значения, а dfsAutoNumber детерминированно переименовывает входящие поля. Сканирование имён происходит до того, как сдвинутся какие-либо номера объектов, поэтому отклонённое слияние оставляет оба документа полностью пригодными к использованию
С этим сталкивался любой, кто собирал пакет форм PDF. Три формы, в каждой из которых есть поле с именем Signature, Date или Total, объединяются в один файл. В AcroForm полностью квалифицированное имя поля — это идентичность поля, поэтому два поля с одним и тем же именем — вовсе не два поля: заполнение одного заполняет и другое, а подпись, применённая к одному, покрывает область, которую никто не намеревался охватить
Почему конфликт имён решается до слияния?
Более старая MergeDocument конкатенирует корневые массивы полей AcroForm обоих документов и не даёт выбора. Хуже того, когда результат оказывается непригоден к использованию, обнаружение происходит после того, как номера объектов уже перенумерованы, а деревья страниц сшиты, что оставляет вызывающий код с документом в состоянии, в котором не был ни один из оригиналов
MergeDocumentEx переворачивает этот порядок. Она собирает имена полей верхнего уровня из обоих документов, сравнивает их и применяет стратегию до того, как что-либо сдвинется. Отклонение поэтому оказывается чистой no-op операцией: целевой документ не тронут, исходный документ не тронут, и оба остаются открытыми и пригодными к использованию, что тест слияния проверяет, читая значение поля обратно из исходника после отклонённого слияния
Сравнение использует упорядоченное, чувствительное к регистру множество имён, поэтому стоимость пропорциональна суммарному числу полей, умноженному на логарифмический множитель, а не произведению двух счётчиков. Чувствительность к регистру здесь правильный выбор, поскольку имена полей PDF чувствительны к регистру; их свёртка объединила бы поля, которые спецификация считает различными
Три стратегии и когда какая уместна
dfsReject — стратегия для автоматизированных конвейеров, которые не должны производить неоднозначные документы. Слияние возвращает ноль, а LastErrorCode сообщает 705 — выделенный код, позволяющий отличить дублирующиеся имена от любого другого сбоя слияния и направить к конкретному средству исправления, обычно к переименованию полей выше по цепочке
dfsMerge намеренно сохраняет общее имя и синхронизирует целевое значение и значение по умолчанию в исходное поле, так что соответствующий стандарту просмотрщик обрабатывает несколько виджетов как одно логически именованное поле, что является стандартным поведением AcroForm для поля с несколькими виджет-аннотациями. Чего она не делает — так это не сворачивает разные словари полей в один объект. Каждое поле сохраняет собственную привязку к странице, внешний вид и действия, поскольку их объединение молча отбросило бы форматирование и поведение, принадлежащие входящему документу
dfsAutoNumber переименовывает входящие дубликаты, добавляя числовой суффикс, начинающийся с _2 и берущий первый свободный. Результат воспроизводим: он зависит только от присутствующих имён, никогда от номеров объектов полей, поэтому слияние одной и той же пары документов дважды даёт одинаковые имена оба раза. Это свойство важно, когда нижестоящий код, импорт FDF или сопоставление с базой данных ссылается на поля по имени
uses
PDFlibrary;
var
Lib: TPDFlib;
TargetDoc, SourceDoc: Integer;
begin
Lib := TPDFlib.Create;
try
TargetDoc := Lib.SelectedDocument;
Lib.LoadFromFile('application-part1.pdf', '');
SourceDoc := Lib.NewDocument;
Lib.LoadFromFile('application-part2.pdf', '');
Lib.SelectDocument(TargetDoc);
if Lib.MergeDocumentEx(SourceDoc, dfsReject) = 0 then
begin
if Lib.LastErrorCode = 705 then
begin
// Оба документа всё ещё целы - повторяем с политикой
Log('duplicate field names; retrying with auto-numbering');
Lib.MergeDocumentEx(SourceDoc, dfsAutoNumber);
end;
end;
Lib.SaveToFile('application-complete.pdf');
finally
Lib.Free;
end;
end;
Обратите внимание на двухшаговый паттерн в этом коде, возможный только потому, что отклонение не разрушительно. Сначала попробуйте строгую политику, изучите ошибку, затем решайте. При слиянии, падающем на середине, откат пришлось бы начинать заново с перезагрузки обоих файлов
Как выглядит объединённая форма после слияния
При dfsMerge целевое поле с именем Shared, несущее "Target value", и исходное поле с тем же именем дают два поля, оба названные Shared, оба сообщающие целевое значение, поскольку целевое значение и значение по умолчанию синхронизированы во входящее поле. Именно такова предполагаемая семантика общего имени: одно логическое поле, несколько виджетов, одно значение
При dfsAutoNumber тот же вход даёт Shared и Shared_2 как отдельные поля с независимыми значениями. Выбирайте между ними, задавая один вопрос: должно ли заполнение одного элемента управления заполнять и другой? Для имени подписанта, повторяющегося на каждой части пакета, — да, и правильна dfsMerge. Для итога, означающего разное на каждой форме, — нет, и правильна автонумерация
// После слияния перечислите, что вы на самом деле получили
for I := 1 to Lib.FormFieldCount do
Log(Format('%d: %s = %s',
[I, Lib.GetFormFieldTitle(I), Lib.GetFormFieldValue(I)]));
Практические заметки для сборки пакетов форм
Успешное слияние поглощает исходный документ: он удаляется из списка документов библиотеки, поэтому DocumentCount падает с двух до одного. Не используйте идентификатор источника после этого. Версия документа поднимается до более высокой из двух, поэтому слияние формы PDF 2.0 в документ 1.7 даёт файл 2.0
Порядок важен для имён. Слияние A в B и слияние B в A дают разные автонумерованные результаты, поскольку документ, выполняющий слияние, сохраняет свои имена без изменений. Когда у пакета есть каноническая основная форма, сделайте её целью
Поля подписи заслуживают отдельного рассмотрения. Подпись, применённая до слияния, покрывает только ту редакцию, которую она подписала, поэтому слияние делает её недействительной в практическом смысле — файл изменился с момента подписания. Сначала соберите документ и подпишите собранный, а не сливайте уже подписанные части. Когда слияние касается содержимого страниц, а не форм, более быстрый путь, описанный в статье быстрое слияние PDF со сдвигом байтовых ссылок, — лучший инструмент
Наконец, планируйте сторону данных пакета вместе со слиянием. Если значения полей приходят из внешней системы, решите, обращается ли эта система к полям по имени, прежде чем выбирать автонумерацию, поскольку Shared_2 не совпадёт с сопоставлением, ожидающим Shared. Форматы импорта и экспорта описаны в статье обмен данными форм FDF, XFDF и XFA, а поведение скриптов на уровне полей, на которое переименование также может повлиять, описано в статье интерактивные действия форм и JavaScript
Слияние форм, обмен данными и подписание работают в одной библиотеке для Delphi, C++Builder и Free Pascal; полный список возможностей — на странице PDF Library for Delphi