Вложения в файлах PDF хранятся в дереве встроенных файлов документа — структуре, которую большинство программ просмотра отображают в виде панели со скрепкой или боковой панели вложений. Из кода на Delphi компонент PDFium Component предоставляет доступ к этому дереву через небольшой набор индексированных свойств класса TPdf: вы можете перебирать элементы по целочисленному индексу, считывать имена и содержимое в байтах, создавать новые слоты и удалять существующие. Этот API достаточно лаконичен; перед написанием продакшен-кода стоит изучить лишь несколько правил упорядочивания элементов и очистки путей
Чтение вложений из открытого документа
Свойство AttachmentCount возвращает количество встроенных файлов, объявленных в документе. Оно считывается напрямую из низкоуровневого вызова PDFium, отражая только реально присутствующие файлы. Свойство AttachmentName[Index] возвращает отображаемое имя файла в формате WString, а Attachment[Index] — его содержимое в виде массива TBytes. Индексация начинается с нуля. Документ должен быть открыт (Pdf.Active = True) перед обращением к любому из этих свойств; вызов их для закрытого документа вернет ноль или пустой результат без генерации ошибок
Важно помнить: чтение свойства Attachment[Index] выделяет память и возвращает все содержимое файла при каждом обращении. Для документа с большим вложенным файлом перебор всех вложений для построения списка в интерфейсе приведет к непроизводительным затратам памяти при каждом вызове. Если вам нужны только имена файлов для показа в списке, сначала считывайте AttachmentName, а чтение байтов откладывайте до момента, когда пользователь действительно запросит файл
procedure ListAttachments(Pdf: TPdf);
var
I: Integer;
Data: TBytes;
begin
if not Pdf.Active then
Exit;
for I := 0 to Pdf.AttachmentCount - 1 do
begin
Data := Pdf.Attachment[I];
Writeln(Format('%d: %s (%d bytes)',
[I, Pdf.AttachmentName[I], Length(Data)]));
end;
end;
Извлечение вложения на диск
Специального вспомогательного метода SaveAttachment нет. Вы самостоятельно считываете байты и записываете их в нужное место, то есть формирование пути и его очистка ложатся на ваш код. Это критично, когда имена вложений приходят из недоверенных документов. Имена вложений PDF — это строки, хранящиеся внутри файла; они могут содержать разделители путей, схожие по начертанию символы Unicode и другие элементы, которые вызовут непредвиденные проблемы, если передать их напрямую в конструктор TFileStream.Create. Всегда обрабатывайте имя функцией ExtractFileName перед формированием выходного пути и рассмотрите возможность отклонения имен, начинающихся с точки или содержащих спецсимволы
Массив байт, возвращаемый свойством Attachment[Index], управляется вызывающим кодом. Вы можете записать его с помощью обычного потока TFileStream и обработать по своему усмотрению, например, проверить первые несколько байт для подтверждения реального формата файла, вместо того чтобы безоговорочно верить заявленному расширению имени
procedure ExtractAttachment(Pdf: TPdf; Index: Integer; const OutputDir: string);
var
SafeName: string;
OutPath: string;
Data: TBytes;
FS: TFileStream;
begin
SafeName := ExtractFileName(Pdf.AttachmentName[Index]);
if SafeName = '' then
SafeName := Format('attachment_%d', [Index]);
OutPath := IncludeTrailingPathDelimiter(OutputDir) + SafeName;
Data := Pdf.Attachment[Index];
FS := TFileStream.Create(OutPath, fmCreate);
try
if Length(Data) > 0 then
FS.WriteBuffer(Data[0], Length(Data));
finally
FS.Free;
end;
end;
Добавление вложений и двухэтапная запись
Создание вложения требует двух вызовов. Метод CreateAttachment(Name) регистрирует новый слот в дереве встроенных файлов и возвращает True в случае успеха. Созданный слот изначально пуст. Затем вы передаете данные, записывая их в свойство Attachment[AttachmentCount - 1] (обращаясь к последнему созданному элементу). Если CreateAttachment возвращает False, слот не был создан, и присваивание повредит данные вложения, которое в данный момент находится на последней позиции
После изменения списка вложений все правки остаются только в оперативной памяти. Вызовите метод SaveAs для записи нового файла с обновленным деревом вложений. PDFium Component в настоящее время не поддерживает сохранение в тот же файл, который сейчас открыт, так как движок удерживает дескриптор чтения источника. Стандартный шаблон для перезаписи файла "на месте" состоит в сохранении во временный путь, закрытии документа, удалении или переименовании оригинала, перемещении временного файла на его место и последующем повторном открытии
procedure AddFileAttachment(Pdf: TPdf; const FilePath: string);
var
FS: TFileStream;
Data: TBytes;
AttachName: string;
begin
if not Pdf.Active then
Exit;
FS := TFileStream.Create(FilePath, fmOpenRead or fmShareDenyWrite);
try
SetLength(Data, FS.Size);
if FS.Size > 0 then
FS.ReadBuffer(Data[0], FS.Size);
finally
FS.Free;
end;
AttachName := ExtractFileName(FilePath);
if Pdf.CreateAttachment(AttachName) then
Pdf.Attachment[Pdf.AttachmentCount - 1] := Data;
end;
Сведения о типах вложений
Помимо имени и массива байт, свойство AttachmentType[Index] возвращает строку MIME-типа, хранящуюся в словаре встроенного файла PDF (если она была записана при создании вложения). Многие генераторы оставляют это поле пустым или записывают туда стандартное значение вроде application/octet-stream, поэтому на него нельзя полагаться для точного определения формата в продакшене. Для надежной идентификации считайте первые несколько байт содержимого и проверьте сигнатуры файлов: %PDF для вложенных PDF, заголовок локального файла ZIP PK\x03\x04 для документов Office Open XML, \xD0\xCF\x11\xE0 для устаревших бинарных файлов составных документов OLE. Сведения о типе из словаря подходят для показа в интерфейсе, но не должны определять логику обработки, когда у вас есть доступ к самим байтам
Удаление вложений
Метод DeleteAttachment(Index) удаляет запись в указанной позиции и возвращает True в случае успеха. После удаления оставшиеся записи сдвигаются вверх, поэтому, если вы удаляете несколько вложений в цикле, вы должны выполнять обход в обратном направлении (от большего индекса к меньшему), чтобы избежать пропуска элементов из-за сдвига. Изменения сохраняются на диск только после вызова SaveAs
Распространенный сценарий в конвейерах обработки документов — удаление всех вложений из входящего PDF перед передачей его далее по соображениям безопасности или размера файла. Считайте количество один раз перед циклом и выполните обход в обратном порядке:
procedure StripAllAttachments(Pdf: TPdf);
var
I: Integer;
begin
for I := Pdf.AttachmentCount - 1 downto 0 do
Pdf.DeleteAttachment(I);
end;
Где вложения PDF встречаются на практике
API работы со вложениями функционирует для любых PDF, которые может открыть PDFium, однако документы со встроенными файлами обычно относятся к нескольким конкретным сценариям. Стандарт PDF/A-3 (ISO 19005-3) прямо разрешает вложение файлов для связывания исходных данных с архивной версией; электронные счета стандартов ZUGFeRD и Factur-X используют этот механизм для внедрения структурированного XML-файла внутрь визуально читаемого макета PDF. Файлы PDF, созданные на основе электронных писем, иногда содержат вложенные файлы из исходного сообщения. Техническая документация, созданная в специализированных системах верстки, аналогичным образом может содержать сопутствующие ресурсы
При обработке входящих файлов PDF из внешних источников проверка свойства AttachmentCount на этапе импорта полезна по двум причинам. Во-первых, вложения могут содержать важные данные для извлечения (например, XML-код счета). Во-вторых, вложения могут нести в себе произвольный исполняемый код, поэтому важно знать об их наличии, даже если вы не планируете их извлекать. Решение обеих задач предельно просто: считайте количество, проверьте имена вложений и определите дальнейшие действия с байтами содержимого
Описанные здесь свойства работы со вложениями входят в состав компонента PDFium Component для Delphi и C++Builder