PDFlibPas прикрепляет встроенный файл к конкретной странице, а не к документу в целом: массив /AF пишется в словарь страницы, тогда как само содержимое остаётся зарегистрированным в дереве имён EmbeddedFiles документа. Именно это разделение описывает ISO 32000-2 §14.13, и именно оно позволяет ридеру ответить на вопрос, на который вложение уровня документа ответить не может: к какой странице относятся эти данные
Сценарии здесь более конкретные, чем у обычных вложений. Отчёт об обследовании, где каждая страница несёт ряд исходных измерений за своим графиком. Пакет сканов, где каждая страница хранит OCR-результат, из которого собран её текстовый слой. Комплект чертежей, где каждый лист возит с собой CAD-выборку, из которой был отрисован. Во всех этих случаях список вложений уровня документа превратился бы в кучу файлов, чьи имена кодируют номера страниц, а это конвенция, а не структура
Одно содержимое, две точки ссылки на него
Важный структурный момент: ассоциация уровня страницы не создаёт второй копии ничего. Файл встраивается один раз и регистрируется в дереве имён EmbeddedFiles ровно так же, как вложение уровня документа, той же машинерией файловой спецификации. Разница в том, куда пишется ссылка и её ключ отношения: в словарь страницы вместо каталога документа
Из этого два следствия. Во-первых, ридер, знающий только о вложениях уровня документа, всё равно найдёт содержимое, потому что оно лежит в том дереве имён, куда такой ридер и смотрит. Во-вторых, снятие страничной ассоциации удаляет привязку, а не файл. ClearPageAssociatedFiles отвязывает страницу от её ассоциированных файлов и оставляет содержимое доступным через дерево имён — это консервативное поведение: операция, которая говорит «сними ассоциацию», не должна молча уничтожать данные, на которые может ссылаться другая часть документа
У этой функции есть одно намеренно узкое условие успеха, о котором стоит знать. Она сообщает успех только тогда, когда страница действительно несла ключ /AF. Страница, у которой ассоциаций никогда не было, возвращает неудачу, а не бодрое подтверждение, поэтому вызывающий не спутает no-op с завершённой очисткой
var
Lib: TPDFlib;
Idx, I: Integer;
begin
Lib := TPDFlib.Create(nil);
try
Lib.LoadFromFile('survey-report.pdf');
// Прикрепляем ряд измерений, по которому построен график на странице 3
Idx := Lib.AddPageAssociatedFileFromFile(3,
'series-03.csv', // файл на диске
'measurements.csv', // отображаемое имя внутри PDF
'text/csv', // MIME-тип
'Raw measurement series for figure 3',
'Data'); // AFRelationship, ISO 32000-2 14.13
if Idx < 0 then
raise Exception.Create('page association refused');
for I := 0 to Lib.GetPageAssociatedFileCount(3) - 1 do
Writeln('page 3 associated file, embedded index ',
Lib.GetPageAssociatedFileEmbeddedIndex(3, I));
Lib.SaveToFile('survey-report-with-data.pdf');
finally
Lib.Free;
end;
end;
Строка отношения на практике не является свободным текстом. ISO 32000-2 определяет словарь: Source, Data, Alternative, Supplement, EncryptedPayload, FormData, Schema и Unspecified, и консюмеры завязываются на него. Data — для чисел за графиком, Source — для документа, из которого страница сгенерирована, Alternative — для эквивалентного представления. Выбирайте из словаря, даже если пока ничто в вашем пайплайне его не читает: следующему инструменту в цепочке может понадобиться
Почему одному и тому же поиску нужен FollowRef в обе стороны?
Потому что переход по ссылкам отвечает на два разных вопроса, и код должен знать, какой из них он задаёт. Поиск ключа, который переходит по косвенным ссылкам, возвращает объект, на который ссылка указывает. Поиск без перехода возвращает саму ссылку. Оба корректны, и использование не того даёт тихое неправильное поведение вместо ошибки
Чтение ассоциированного файла демонстрирует первое направление. Чтобы получить номер объекта встроенного потока за ключами /EF и /F файловой спецификации, поиск должен не переходить по ссылке: переход разрешит ссылку в объект потока, и номер объекта потерян. Правило обобщается: любой путь кода, которому нужна идентичность объекта, а не содержимое, обязан взять сырую ссылку
Optional content показывает противоположное направление, и обошлось оно дороже в обнаружении. Словарь свойств опционального контента пишется в каталог как косвенный объект, поэтому код, читающий его обратно без перехода, получает ссылку вместо словаря. Проверка типа на этом значении проваливается, срабатывает естественная ветка фолбэка — если конфигурации нет, создай её — и затирает конфигурацию, которая уже была. Ничего не бросает исключений. Слои, описанные в группах опционального контента и слоях, просто теряют своё состояние видимости по умолчанию
Урок обобщается за пределы обоих случаев. Когда поиск может вернуть либо ссылку, либо объект, голая проверка типа — не обработка ошибок: это ветка, которая рано или поздно будет выполнена по неправильной причине. Решайте явно, что нужно каждому месту вызова, и предпочитайте публичный API, отвечающий на вопрос напрямую, например свойство количества опционального контента, вместо лазания в защищённый акцессор за словарём каталога
// Вложения уровня документа и ассоциации уровня страницы сосуществуют.
// Встроенный файл можно пометить ассоциированным и на уровне документа
if Lib.IsEmbeddedFileAssociated(0) = 0 then
Lib.SetEmbeddedFileAssociated(0, 1, 'Supplement');
Writeln('document associated files: ', Lib.GetAssociatedFileCount);
Writeln('page 3 associated files : ',
Lib.GetPageAssociatedFileCount(3));
// Очистка отвязывает страницу; содержимое остаётся в дереве имён
if Lib.ClearPageAssociatedFiles(3) > 0 then
Writeln('page 3 associations removed, payloads still reachable');
Что режимы соответствия делают с вложениями
Архивные профили ограничивают, что можно встраивать, и ограничение проверяется на входе, а не при сохранении. PDF/A-1 запрещает встроенные файлы полностью, PDF/A-2 разрешает только встроенные документы PDF/A, а PDF/A-3 — тот профиль, который открыл встраивание произвольных типов файлов, и именно поэтому гибридные форматы счетов строятся на нём
PDFlibPas отказывает во вложении, когда активный режим соответствия его не разрешает, — в месте вызова, а не через сотни операций во время вывода. Это осознанный выбор о том, где ошибку дешевле всего обработать: отказ на месте вызова называет файл, который вы добавляли, а отказ при сохранении называет документ и оставляет вам вычислять, какое из сорока вложений виновато
Поэтому же ассоциированные файлы так часто встречаются в электронных счетах. Гибридный счёт — это PDF, который читает человек, с машиночитаемым XML-содержимым, приложенным и помеченным правильным отношением, и профиль контейнера, и ключ отношения — часть спецификации, а не конвенции. Эта конструкция разобрана в построении гибридных счетов Factur-X и ZUGFeRD, сторона метаданных — в XMP extension schema для PDF/A-3
Когда ассоциация должна быть на страницу, а не на документ?
Когда консюмеру нужно знать, к какой странице относятся данные, и только тогда. Вложения уровня документа проще, шире поддерживаются вьюерами и достаточны везде, где содержимое описывает весь документ: XML счёта, манифест подписи, архив исходников. К ассоциации уровня страницы обращайтесь, когда содержимое реально привязано к странице и идентичность страницы — часть его смысла
Поддержка — практическое ограничение. Ассоциированные файлы уровня страницы — конструкция PDF 2.0, и поддержка во вьюерах тоньше, чем у вложений уровня документа. Поскольку содержимое в любом случае лежит в дереве имён, вьюер, игнорирующий /AF на страницах, всё равно покажет файл в списке вложений, так что деградация плавная. Но если страничная привязка для вашего консюмера существенна, а не просто полезные метаданные, проверяйте ридер, под который реально целитесь, а не надейтесь
Ассоциированные файлы уровня страницы, вложения уровня документа и шлюзы архивных профилей, управляющие и теми и другими, поставляются в Delphi PDF-библиотеке PDFlibPas. Если вы попутно чините старые файлы на входе, работа с метаданными и соответствием в конвертации в PDF/A с починкой метаданных — это то, что решает, какой из этих способов вложения вам вообще доступен