Транзакционная типография возвращает ваш прогон из 80,000 страниц одной строкой: "not PDF/VT, RIP cannot cache". Файл отлично открывается во всех viewer на вашем столе, цвета правильные, данные слились корректно. Но цифровую машину это не интересует. Высокоскоростная печать с переменными данными живет и умирает на способности пресса распознать, что блок с логотипом клиента на странице 1 байт в байт совпадает с блоком на странице 40,000, отрендерить его один раз и затем повторно использовать. PDF/VT и является тем стандартом, который делает это обещание проверяемым машиной. Именно поэтому ловушка "выглядит правильно" здесь особенно опасна: структура, которую читает RIP, на экране вообще не видна
PDFiumPas выводит эту структуру через небольшую поверхность у TPdf : SaveAsPdfVT ее записывает, ValidatePdfVT ее проверяет. Эта статья о том, что именно эти два метода кладут на диск и что инспектируют, где ISO 16612-2 оказывается строже, чем кажется на первый взгляд, и какие части являются честными структурными якорями, а не полноценным preflight, который можно выставлять клиенту в счет
Что стандартизует PDF/VT и почему PDF/X идет первым
PDF/VT, ISO 16612-2:2010, - это не новый файловый формат. Это слой оптимизационных метаданных, привинченный поверх PDF/X, и именно этот порядок несет на себе всю нагрузку. Стандарт определяет три уровня conformance, но PDF-файл как таковой обозначают только два из них: PDF/VT-1 , то есть один самодостаточный документ, и PDF/VT-2 , то есть модель file set, где страницы ссылаются на общие внешние ресурсы. Третий токен, который иногда встречается, PDF/VT-2s , вообще не является file-level значением. Он живет в MIME stream header, описанном в Annex A. Если вы видите код, который штампует GTS_PDFVTVersion = "PDF/VT-2s" в XMP документа, значит, этот код ошибочен
Для одиночного файла правило без переговоров одно: база PDF/X. ISO 16612-2 §6.2.1 требует, чтобы каждый файл PDF/VT-1 одновременно был корректным PDF/X-4. File set у PDF/VT-2 по §6.2.2 должен опираться на PDF/X-4p, PDF/X-5g или PDF/X-5pg. Именно поэтому writer PDF/VT не может просто приписать пару идентификационных ключей: он обязан нести с собой весь набор marker PDF/X-4, а значит, OutputIntent, встроенный ICC destination profile, согласованные XMP и document Info entries, trailer /ID и отсутствие шифрования. Пропустите любой из этих пунктов, и вы получите файл, который заявляет PDF/VT и проваливается в ту же секунду, когда совместимый consumer проверяет базу. PDFiumPas рассматривает слой PDF/X-4 как часть save PDF/VT, поэтому отдельный SaveAsPdfX перед этим вы не вызываете. Injector записывает оба слоя за один проход
Запись файла через SaveAsPdfVT
Минимальный вызов не требует ничего, кроме активного документа, потому что TPdfVTSaveOptions.Default поставляет встроенный профиль sRGB ICC и conformance pvc1 . Внутри сохранение идет в три шага: оно удаляет любую security, потому что внедрение plaintext marker в зашифрованный object stream привело бы к его порче, переносит существующие Info dictionary и trailer /ID документа в набор marker, чтобы XMP и Info совпадали, а затем дописывает объекты PDF/X-4 и PDF/VT через incremental update
var
Pdf: TPdf;
begin
Pdf := TPdf.Create(nil);
try
if Pdf.LoadFromFile('statements-merged.pdf') then
begin
// Default options: built-in sRGB OutputIntent, PDF/VT-1, synthesised DPart
if Pdf.SaveAsPdfVT('statements-pdfvt.pdf') then
Writeln('PDF/VT-1 written')
else
Writeln('Save failed (document not active?)');
end;
finally
Pdf.Free;
end;
end;
Для реального production output почти всегда стоит переопределять OutputIntent профилем конкретной машины, а не generic sRGB fallback. Передайте байты ICC и идентификаторы condition через TPdfVTSaveOptions :
var
Pdf: TPdf;
Opt: TPdfVTSaveOptions;
Icc: TBytes;
begin
Pdf := TPdf.Create(nil);
try
Pdf.LoadFromFile('directmail-merged.pdf');
Icc := LoadIccProfile('GRACoL2013_CRPC6.icc'); // your own loader
Opt := TPdfVTSaveOptions.Default;
Opt.Conformance := pvc1; // pvc2 is normalised to pvc1 on write
Opt.IccProfileData := Icc;
Opt.OutputConditionIdentifier := 'CGATS21_CRPC6';
Opt.OutputCondition := 'Commercial print, coated, CRPC6';
Opt.RegistryName := 'http://www.color.org';
Opt.Title := 'Spring 2026 Direct Mail Run';
Opt.Trapped := ptvFalse; // PDF/X Info /Trapped state
Pdf.SaveAsPdfVT('directmail-pdfvt.pdf', Opt);
finally
Pdf.Free;
end;
end;
Одна деталь в этом фрагменте представляет собой не ограничение, с которым можно спорить, а намеренную guardrail. Установка Opt.Conformance := pvc2 не создает файл PDF/VT-2. Writer нормализует любой запрос, отличный от pvc1 , обратно к pvc1 , потому что PDF/VT-2 - это file-set format, а single-file writer, который выводит один документ, физически не может собрать внешний набор ресурсов, которого требует §6.2.2. Значение pvc2 существует для пути чтения, чтобы ValidatePdfVT мог распознать и сообщить об уже существующем документе file set. Это не цель для записи
Дерево DPart: структура, которую действительно читает RIP
Сердце PDF/VT - это иерархия Document Part, DPart. Именно она позволяет прессу разбивать длинный прогон на записи, группировать записи по получателям или почтовым пачкам и вешать Document Part Metadata, чтобы downstream оборудование могло маршрутизировать и тарифицировать каждый экземпляр. ISO 16612-2 §6.5 задает проводку: catalog несет /DPartRoot , корневой узел DPart несет /DPartRootNode и /NodeNameList , перечисляющее уровни иерархии, leaf DPart покрывают диапазоны дерева страниц, а каждая страница, принадлежащая part, ссылается обратно на свой leaf через page-level запись /DPart
Если ваш исходный документ уже содержит пригодную иерархию, SaveAsPdfVT ее сохраняет. Если нет, writer синтезирует минимальный вариант: один document-level DPart, покрывающий текущее дерево страниц по порядку, с обратной ссылкой /DPart , дописанной в каждый живой page object, и одноуровневым /NodeNameList [/Document] . Нужно честно понимать, что такое это минимальное дерево. Это структурный якорь, удовлетворяющий требованиям формы из §6.5. Это не business metadata. Оно не может выдумать получателей, границы почтовых экземпляров или партии продукции, потому что этой информации не было в исходнике. Если у вас есть данные по получателям, от вас ожидается, что вы сами построите более глубокое DPart tree и расширите /NodeNameList так, чтобы оно соответствовало созданным уровням
Validation, выходящий за пределы простого наличия ключей
ValidatePdfVT возвращает запись TPdfVTValidationResult с тремя вещами: обнаруженным Conformance , набором Issues и helper IsCompliant , который равен true только тогда, когда найден реальный уровень conformance и набор issues пуст. Перечисление issues намеренно сделано конкретным, чтобы провал говорил, какой именно пункт вы нарушили, а не просто "invalid":
var
Pdf: TPdf;
Res: TPdfVTValidationResult;
begin
Pdf := TPdf.Create(nil);
try
Pdf.LoadFromFile('statements-pdfvt.pdf');
Res := Pdf.ValidatePdfVT;
if Res.IsCompliant then
Writeln('PDF/VT compliant: ', VTLevelName(Res.Conformance))
else
begin
if pvviMissingDPartRoot in Res.Issues then
Writeln('DPart hierarchy missing or unusable');
if pvviMissingPdfXIdentifier in Res.Issues then
Writeln('PDF/X-4 base identifier absent');
if pvviMissingOutputIntent in Res.Issues then
Writeln('OutputIntent / ICC profile missing');
if pvviEncryptionPresent in Res.Issues then
Writeln('Encrypted - PDF/X forbids this');
end;
finally
Pdf.Free;
end;
end;
Две проверки здесь особенно стоит понять глубже - pairing conformance и обход DPart, потому что раньше обе были слишком мягкими, а потом их подтянули к тексту стандарта. Со стороны pairing validator делает точное сопоставление, а не считает, что "любой PDF/X сойдет": файл PDF/VT-1 принимается только на базе PDF/X-4 , а файл PDF/VT-2 только на PDF/X-4p , PDF/X-5g или PDF/X-5pg . Marker PDF/VT-1 поверх базы PDF/X-1a не прощается, а явно репортится
Основная строгость живет именно в обходе DPart. Недостаточно, чтобы у catalog просто был ключ /DPartRoot , потому что поддельный пустой объект или структура без ссылок на страницы все равно не потребляются. HasValidDPartHierarchy и рекурсивный ValidateDPartNode проходят по всей структуре: следуют по parent link, отвергают duplicate child и cycle, проверяют, что /Start и /DParts взаимоисключают друг друга, и требуют, чтобы leaf page range покрывали page tree в порядке depth-first, а /DPart у каждой страницы указывал на тот leaf, в который эта страница входит. Все эти внутренние поломки схлопываются в один публичный бит issue pvviMissingDPartRoot , а не раздувают публичный enum. Поэтому относитесь к этому флагу как к "иерархия DPart непригодна", а не буквально как к "отсутствует корневой ключ"
Три синтаксические ловушки, которые validator теперь принудительно проверяет
Последовательные проходы по §6.5 Table 4 выявили формы, которые ранние версии принимали, хотя стандарт их не допускает. Именно такие ошибки и появляются в DPart tree, собранном вручную, поэтому их стоит выделить отдельно:
/DPartsявляется массивом массивов, а не плоским массивом. Каждый элемент внешнего массива сам должен быть массивом косвенных ссылок. Плоский/DParts [9 0 R]отвергается, а корректная форма выглядит как/DParts [[9 0 R] [10 0 R]]. Это не дает неиерархической структуре притворяться корректным уровнем/Endпомечает только настоящий диапазон из нескольких страниц. Leaf DPart может нести/Endтолько если у него также есть/Start, а/Endобязан находиться позже, чем/Start, в порядке page-tree. Вырожденный/Start 3 0 R /End 3 0 Rтеперь делает всю иерархию непригодной вместо того, чтобы читаться как part на одну страницу/NodeNameListИмена должны переживать снятие экранирования имени PDF в виде XML NMTOKEN./Bad#20NameИмя вроде.,-,_,:и не-ASCII байты, что позволяет ловить ошибки с пробелами и разделителями, не отвергая при этом легитимные локализованные или vendor-specific имена
XMP marker: два способа записать одно и то же свойство
Идентификация PDF/VT живет в XMP под namespace pdfvtid , а именно в свойствах GTS_PDFVTVersion и GTS_PDFVTModDate , рядом со стандартными xmp:CreateDate и xmp:ModifyDate . Тонкость, которая вызывает ложные сообщения о "missing" у наивных reader, состоит в том, что каждое из этих свойств может быть сериализовано двумя способами: как текст элемента, <pdfvtid:GTS_PDFVTVersion>PDF/VT-1</pdfvtid:GTS_PDFVTVersion> , или как RDF-атрибут у элемента description. PDFiumPas читает обе формы, поэтому файл, который другой инструмент записал в атрибутном стиле, не штрафуется. Он также принудительно соблюдает правило согласованности из §6.3, согласно которому GTS_PDFVTModDate должно совпадать с xmp:ModifyDate . Несовпадение поднимает pvviModDateMismatch
Еще одно правило из того же пункта: неизвестное значение GTS_PDFVTVersion сохраняется как pvcUnknown , а не сворачивается обратно в pvcNone . С операционной точки зрения это важное различие. pvcNone означает "вообще нет marker PDF/VT, это обычный PDF", а pvcUnknown означает "кто-то проштамповал версию, которую этот validator не распознает", включая случай PDF/VT-2s . Смешивать эти состояния нельзя, иначе поврежденный файл спрячется в той же корзине, что и обычный документ
Где заканчивается гарантия
Здесь важно точно обозначить границу того, что обещают эти методы, потому что в compliance variable-data print крутятся вполне реальные деньги. Проверки DPart и pairing являются byte-level validation структуры. Они подтверждают, что optimization skeleton, base marker PDF/X-4, OutputIntent и XMP присутствуют и внутренне согласованы. Но это не content-level preflight PDF/X-4: они не проверяют, что каждый цвет лежит внутри заявленного output condition, что все шрифты встроены или что какой-нибудь запрещенный edge case с transparency blending не просочился мимо. Для задачи, которая реально пойдет на контрактную машину, сочетайте структурную validation от PDFiumPas с отдельным PDF/X preflight engine и тестовой печатью, точно так же, как вы перепроверяли бы любое другое заявление о соответствии. Структурный слой ловит ошибки, которые тихо ломают RIP caching. Это половина полного контроля, а не весь контроль
Если вы встраиваете эти проверки в более широкий release gate, тот же подход byte-level scanning лежит под остальными стандартными функциями библиотеки, включая validating object and cross-reference streams еще до того, как файл вообще достигнет preflight, и дисциплину shared object за reusable page stamps with Form XObjects , которая и делает документ дружелюбным к RIP с самого начала. Описанные здесь API сохранения и validation для PDF/VT и PDF/X входят в PDFium VCL component для Delphi и C++Builder, а на странице продукта есть полный reference по compliance