Перейти к содержимому
getnextpdf.com

PDF — это контейнер: встроенные файлы и связанные данные

Spec: ISO 32000-2, §7.11.4Spec: ISO 32000-2, §14.13Spec: ISO 19005-3, PDF/A-3

Большинство людей представляют PDF как стопку страниц. Это та часть, которую вы видите. Но PDF — ещё и контейнер, и он может нести внутри себя целые другие файлы — таблицу, XML-нагрузку, исходный документ — собранные в тот же единственный файл, который вы передаёте кому-то ещё.

На этой странице объясняется, как это работает: поток встроенного файла, в котором хранятся байты, дерево имён, которое их перечисляет, и тот единственный ключ, который решает, просто ли вложение лежит на месте или действительно что-то значит.

Нетипизированное вложение и типизированное выглядят для человека одинаково. Оба — это файл, едущий внутри PDF, и — в этом движке — оба связаны с документом. Разница в том, что одно из них сообщает машине, для чего оно, а другое оставляет связь пустой, чтобы машина догадывалась сама.

Эта разница — всё в гибридном электронном счёте. Налоговая платформа не читает страницу вашего счёта; она читает встроенный вами XML. Если этот XML приложен как недифференцированный двоичный кусок, а не как данные счёта для видимого документа, у соответствующей стандарту программы чтения нет надёжного способа узнать, что это та самая нагрузка для обработки. Страница выглядит безупречно. Счёт отклоняется. Сбой приходит спустя дни, а за ним — задержанный платёж.

Правильно задать связь в слое, который производит файл, гораздо дешевле, чем выяснять это по одному отклонённому счёту за раз.

  • PDF может встроить байты любого файла в виде потока встроенного файла (Spec: ISO 32000-2, §7.11.4). Поток несёт данные плюс небольшой словарь параметров: исходный размер, даты и контрольную сумму.
  • Встроенные файлы каталогизированы в дереве имён EmbeddedFiles, так что программа чтения может перечислить их по имени, не сканируя весь документ.
  • Связанный файл идёт ещё на шаг дальше: он объявляет AFRelationship (Spec: ISO 32000-2, §7.11.3) — одно из восьми стандартных значений (Source, Data, Alternative, Supplement, EncryptedPayload, FormData, Schema, Unspecified) или пользовательское значение, — сообщая, как файл соотносится с содержимым, к которому он приложен.
  • Эта типизированная связь — механизм, лежащий в основе гибридных электронных счетов (ZUGFeRD / Factur-X) и вложений PDF/A-3 (Spec: ISO 19005-3, PDF/A-3).
  • NextPDF поддерживает примитивы сырого контейнера в ядре: embedFile() и embedFileFromString() с явной связью. Расширенные издания добавляют поверх этих примитивов специализированный встройщик электронных счетов EN 16931 / ZUGFeRD / Factur-X.

Представьте это как два слоя, наложенных друг на друга.

Нижний слой — это хранилище. Поток встроенного файла (Spec: ISO 32000-2, §7.11.4) — это байты исходного файла, обёрнутые в объект-поток PDF, со словарём параметров, записывающим исходный размер, дату изменения и контрольную сумму несжатых данных. К потоку обращаются через словарь спецификации файла, чей словарь /EF указывает на поток встроенного файла, — сам поток /EF не несёт. Программа чтения может вытащить файл обратно байт в байт. Чтобы такие файлы можно было найти, каталог документа содержит дерево имён EmbeddedFiles — отсортированное отображение имени на каждую спецификацию файла, — так что программа просмотра может перечислить «вот 3 файла внутри этого PDF», не обходя каждую страницу.

Верхний слой — это смысл. Сам по себе встроенный файл просто присутствует. Механизм связанных файлов (Spec: ISO 32000-2, §14.13) прикрепляет файл к чему-либо — ко всему документу, к странице, к графическому объекту — и клеймит его ключом AFRelationship. ISO 32000-2 определяет небольшой словарь из восьми стандартных значений (Spec: ISO 32000-2, §7.11.3) и допускает также пользовательские значения; каждое стандартное значение отвечает на точный вопрос:

AFRelationshipЧто оно утверждает о файле
SourceЭто исходный материал, из которого было сгенерировано видимое содержимое (например, исходный документ текстового процессора).
DataЭто структурированные данные, привязанные к видимому содержимому, — канонический случай — это XML счёта за отрисованной страницей счёта.
AlternativeЭто альтернативное представление того же содержимого (например, аудио- или видеоверсия).
SupplementЭто дополнительный материал, который расширяет содержимое, но не является его частью.
EncryptedPayloadВстроенный файл — это зашифрованная нагрузка, которую PDF оборачивает как непрозрачный двоичный кусок.
FormDataФайл — это данные формы (FDF, XFDF или XML-нагрузка формы).
SchemaФайл — это схема, описывающая структуру файла Data (например, XSD для XML-данных или JSON Schema).
UnspecifiedСвязь намеренно не указана. Честно, но машине это не говорит ничего.

Помимо этих восьми, стандарт также допускает специфичные для приложения пользовательские значения связи, так что словарь расширяем, а не фиксирован.

Связанный файл определяется двумя вещами, работающими вместе, а не одним ключом. Связь /AF привязывает спецификацию файла к части документа; ключ AFRelationship в спецификации файла затем задаёт семантическую связь. Запись /AF в точке связывания (каталоге документа, странице или объекте) — это массив — этот массив содержит одну или несколько спецификаций файлов, обычно как косвенные ссылки; /AF — не одна ссылка. Связанный файл уровня документа — это спецификация файла, перечисленная в массиве /AF каталога документа и несущая свой AFRelationship. Пометьте эту таблицу как Unspecified — и вы связали её с документом, но не сообщили машине ничего о том, зачем. Пометьте ту же таблицу как Data — и вы сообщили каждой соответствующей стандарту программе чтения, что это и для чего. Байты те же. Семантика — нет.

Вот почему случай электронного счёта — это не «приложить XML-файл». Это «встроить этот XML как связанный файл Data для этого документа, внутри соответствующего стандарту носителя PDF/A-3», — при том что действительность счёта и юридическое признание остаются отдельными проверками, которые носитель не выполняет. Поток состоит из четырёх стадий, и именно порядок сохраняет его корректность.

  1. Store the bytesThe file is wrapped in an embedded file stream with its size, dates, and a checksum (ISO 32000-2 §7.11.4).
  2. Register it by nameThe file specification is added to the EmbeddedFiles name tree so a reader can enumerate attachments without scanning the document.
  3. Declare the relationshipAn AFRelationship value (one of the eight standard values such as Source or Data) marks how the file relates to the content, associated at the document level (ISO 32000-2 §14.13.3).
  4. Make it archivalA PDF/A-3 carrier permits the embedded payload to ride inside one conforming archival PDF/A document; invoice validity and legal acceptance remain separate checks (ISO 19005-3).
Как типизированное вложение становится гибридным файлом от начала до конца: движок сохраняет байты, регистрирует файл по имени, объявляет связь, а архивный профиль позволяет всему этому ехать внутри одного соответствующего стандарту архивного документа.

Эта четвёртая стадия — причина, по которой PDF/A-3 существует как отдельный профиль. Более ранние архивные профили ограничивали то, что можно встраивать; PDF/A-3 (Spec: ISO 19005-3, PDF/A-3) — это та часть, которая разрешает файлам любого формата ехать внутри соответствующего стандарту архивного документа. Он разрешает встроенную нагрузку — он не проверяет эту нагрузку и не наделяет её юридическим статусом. Без него гибридный счёт — один файл, который одновременно и страница, которую читает человек, и данные, которые разбирает налоговая система, — вообще не мог бы быть соответствующим стандарту архивным документом PDF/A; а действителен ли счёт и юридически ли он принят, остаётся отдельным вопросом. Специализированный встройщик электронных счетов, который добавляют расширенные издания, — это шов удобства ровно над этим: он встраивает нагрузку, устанавливает связь в Data и регистрирует её правильно, чтобы вам не пришлось собирать обвязку контейнера вручную. Более глубокая механика выставления счетов и архивации живёт на двух соседних страницах по ссылкам ниже; эта страница — о контейнере, на котором обе они стоят.

Небольшая полная программа. Два вызова, которые имеют значение, — это разница между нетипизированным связанным файлом и типизированным, и связь — это явный аргумент, который вам следует задать. В этом движке оба вызова производят связанный файл: embedFile() и embedFileFromString() всегда регистрируют спецификацию файла в массиве /AF каталога документа, так что единственное, что меняет связь, — это что означает связывание. По умолчанию она равна Unspecified, что связывает файл, но не говорит машине ничего о том, зачем; для нагрузки электронного счёта вы устанавливаете её в Data, чтобы программа чтения могла её найти.

<?php
declare(strict_types=1);
use NextPDF\Core\Document;
use NextPDF\Navigation\AFRelationship;
$document = Document::createStandalone();
$document->addPage();
$document->setFont('helvetica', 'B', 16);
$document->cell(0, 12, 'Invoice INV-2026-0042', newLine: true);
// An UNTYPED associated file: the bytes are embedded AND the file spec is
// added to the document catalog's /AF array, but the relationship says
// nothing about why. A reader can open it; a machine cannot tell its role.
// The relationship is left Unspecified (its default); the second argument is
// the human-readable description. embedFile accepts the AFRelationship enum.
$document->embedFile(
'/srv/invoices/INV-2026-0042-source.docx',
'Original source document',
AFRelationship::Unspecified,
);
// A TYPED associated file: the invoice XML is declared as the DATA behind
// the visible page. This is the relationship a hybrid e-invoice reader
// looks for — the same intent the dedicated e-invoice embedder sets.
// embedFileFromString takes the data, a filename, a description, and a
// relationship as a PDF-name string ('/Data').
$invoiceXml = $generateCiiXml(); // your ERP authors this; the engine never does
$document->embedFileFromString(
$invoiceXml,
'factur-x.xml',
'Factur-X invoice data',
'/Data',
);
$bytes = $document->getPdfData();

Связь '/Data' безошибочна. Первое вложение — оставленное Unspecified — связано всё равно, просто без заявленного смысла. Для обоих вызовов движок записывает поток встроенного файла, добавляет файл в дерево имён EmbeddedFiles, перечисляет его спецификацию файла в массиве /AF каталога документа и записывает заявленную вами связь — он не выбирает её за вас. В этом движке нет режима «только дерево имён»: каждый файл, который вы так встраиваете, является связанным с документом файлом, так что связь — единственный рычаг, которым вы управляете.

Частое предположение в том, что «встроенный» и «связанный» — два слова для одного и того же. Это не так. Встроенный — про хранилище: байты находятся внутри PDF. Связанный — про привязку: спецификация файла перечислена в массиве /AF на части документа, и она несёт AFRelationship. В абстрактной модели PDF файл может быть встроен в дерево имён, ни разу не будучи связанным; путь embedFile() в NextPDF не оставляет его так — он всегда записывает связь /AF, — так что для этого движка открытый вопрос никогда не в том, связан ли файл, а в том, что говорит связь.

Вторая ловушка: предполагать, что программа просмотра «сама поймёт», какое вложение является счётом. Соответствующая стандарту программа чтения не должна догадываться. Она ищет файл, чья связь говорит Data. Оставьте связь Unspecified — и вы связали нагрузку, но не сообщили машине ничего полезного о её роли.

Механизм контейнера мощен так, что об этом стоит быть честным: embedFile() читает любой путь, который может прочитать процесс PHP. Это и есть функция — и это же граница. Движок прикрепляет данные ему байты; он не решает и не может решить за вас, является ли путь тем, который вы намеревались раскрыть.

Embedding a file from a caller-supplied path — edition availability
EditionAvailability
Core

embedFile() читает любой путь, к которому имеет доступ процесс PHP, и встраивает его байты дословно. Проверка того, что путь безопасен и намеренный — не управляемое пользователем значение, не обход каталога и не секрет за пределами области документа, — это ответственность интегратора. Это документированный контракт безопасности, а не упущение: движок не станет молча угадывать, какие пути легитимны, потому что эта догадка принадлежит вашему приложению, которое знает границу доверия, невидимую движку. Пропускайте байты, подверженные влиянию злоумышленника, через строку с embedFileFromString(), чтобы слой пути вовсе не участвовал.

ProNot in this edition
EnterpriseNot in this edition

Стоит назвать прямо ещё два предела:

  • Встраивание — это не проверка. Движок несёт данные ему байты. Является ли встроенный XML соответствующей стандарту нагрузкой счёта — это отдельный вопрос, на который отвечает валидатор; см. страницу о выставлении счетов.
  • Типизированное вложение само по себе не является соответствующим стандарту архивным файлом. Чтобы сделать гибридный файл юридически корректным документом PDF/A-3, требуется архивный режим и независимая проверка соответствия; см. страницу об архивации.
  • Поток встроенного файла — объект-поток PDF, содержащий байты внешнего файла, со словарём параметров, записывающим его исходный размер, даты и контрольную сумму (ISO 32000-2 §7.11.4).
  • Дерево имён EmbeddedFiles — отсортированное отображение в каталоге документа, которое перечисляет встроенные файлы по имени, чтобы программа чтения могла перечислить вложения, не сканируя весь документ.
  • Связанный файл — встроенный файл, привязанный к части документа связью /AF (на каталоге документа, странице или объекте) и несущий AFRelationship, который задаёт, как он соотносится с этим содержимым; случай уровня документа — спецификация файла в массиве /AF каталога — это тот, на котором сосредоточена эта страница (ISO 32000-2 §14.13.3).
  • AFRelationship — ключ спецификации файла, чьё значение именует связь (ISO 32000-2 §7.11.3). Он принимает одно из восьми стандартных значений (Source, Data, Alternative, Supplement, EncryptedPayload, FormData, Schema, Unspecified) или пользовательское значение; Data — это значение, которое использует нагрузка гибридного электронного счёта.
  • PDF/A-3 — архивный профиль ISO 19005-3, который разрешает встраивать файлы любого формата, делая возможным соответствующий стандарту гибридный документ.
  • Гибридный счёт — один файл PDF, который одновременно и удобочитаемая человеком страница, и машиночитаемая встроенная нагрузка счёта.