Техническая статья

Цвет устройства в PDF/A судят по тому, что рисует страница

veraPDF поднимает флаг ISO 19005-1 6.2.3.3 на файле, который вообще не рисует RGB, называет голый re f некалиброванным или валит страницу 2 при проходящей идентичной странице 1. PDF/A судит эти правила по тому, что рисует каждая страница, поэтому валидатор PDF/A компонента PDFium для Delphi прослеживает каждую страницу сквозь формы, паттерны, глифы и аннотации, прежде чем выносить вердикт по цвету и прозрачности

Что на самом деле требуют 6.2.3.3 и 6.2.4.3

ISO 19005-1 6.2.3.3 упаковывает три независимых правила в один абзац. Соответствующий файл может использовать DeviceRGB или DeviceCMYK, но не оба сразу; если используется какое-либо некалиброванное цветовое пространство, файл должен содержать PDF/A-1 OutputIntent; а DeviceRGB допустим только с намерением, чей профиль RGB, DeviceCMYK — только с CMYK. DeviceGray вне этого списка, потому что пункт дальше поясняет конвертацию серого под любым из намерений. Само намерение необязательно: 6.2.2 говорит, что файл может его указать, и засчитывает только запись OutputIntents с /S /GTS_PDFA1 и корректным потоком ICC-профиля в /DestOutputProfile. Поэтому проверяющий, требующий намерение от каждого файла, отвергнет соответствующие документы, которые никогда не рисуют цветом устройства. Part 1 не упоминает DefaultRGB или DefaultCMYK, но цвет, переключённый через них, рендерится в пространстве на базе CIE (ISO 32000-1 §8.6.5.6), что удовлетворяет требованию независимости от устройства из 6.2.3.1; тестовый набор veraPDF для 6.2.3.3 считает его калиброванным, и PDFium Component читает пункт так же. Запрет смешивания стоит особняком: файл PDF/A-1, рисующий и RGB, и CMYK, валится, даже когда записи Default калибруют оба. ISO 19005-2 6.2.4.3 снимает этот запрет и калибрует каждое пространство само по себе, как показывает правая колонка ниже, поэтому PDFium Component гонит один скан для каждой части и применяет проверку смешивания только к заявлениям PDF/A-1

Нарисованное пространство устройстваPDF/A-1 (ISO 19005-1 6.2.3.3)PDF/A-2 и позже (ISO 19005-2 6.2.4.3)
DeviceRGBDefaultRGB в области видимости или RGB-намерение; не рядом с DeviceCMYKDefaultRGB в области видимости или RGB OutputIntent
DeviceCMYKDefaultCMYK в области видимости или CMYK-намерение; не рядом с DeviceRGBDefaultCMYK в области видимости или CMYK OutputIntent
DeviceGrayDefaultGray в области видимости или любой PDF/A-1 OutputIntentDefaultGray в области видимости или любой PDF/A OutputIntent

Что считается использованием цветового пространства?

Использовать цветовое пространство — значит рисовать им по пути, который ридер отрендерит. Объявить его недостаточно. Набор veraPDF доказывает это проходящим файлом 6.2.3.3: именованный Form XObject выставляет CMYK через K и k, сидит в словаре ресурсов страницы, никогда не упоминается из содержимого страницы, а OutputIntent файла — RGB. Поиск имён цветов устройства по всему файлу валит этот файл и пропускает тот, что рисует только DeviceRGB под CMYK-намерением, где никакие два имени не конфликтуют. PDFium Component вместо этого обходит граф отрисовки, построенный на его побайтовом preflight-сканере PDF/A. С каждой страницы обход считает цвет, нарисованный:

  • потоками содержимого страницы и каждым Form XObject, который они вызывают через Do, включая вложенные формы
  • паттерном, выбранным через scn или SCN, читаемым с собственными Resources паттерна
  • CharProcs шрифта Type 3, выбранного через Tf
  • XObject-изображениями и инлайн-изображениями — по цветовому пространству, которое называет их собственный словарь
  • appearance-потоками аннотаций, рисуемыми поверх страницы; когда /AP /N виджета — словарь состояний, считается каждое состояние, поскольку переключение поля рисует то, чего не выбрал /AS

Более тонкая половина достижимости — цвет, который содержимое никогда не называет. q 0 0 10 10 re f Q не содержит ни одного цветового оператора. Он всё равно рисует. Начальное цветовое пространство состояния графики — DeviceGray (ISO 32000-1 Table 52), поэтому заливка — чёрный DeviceGray, некалиброванный любой частью, пока не существуют PDF/A OutputIntent или DefaultGray. Поэтому скан отслеживает, выбрал ли что-нибудь цвет до сих пор: rg, k, g и их варианты для обводки выбирают пространство устройства, cs и CS выбирают то, что называют, а оператор заливки, обводки или текста, достигнутый до любого выбора, записывает DeviceGray. /CS0 cs 1 0 0 /P0 scn уже выбрал пространство, так что этот scn — не серый

Достижимый путь отрисовки, который PDFium Component обходит в Delphi перед вердиктом по цвету устройства PDF/A: формы, паттерны, глифы Type 3, изображения и appearance-потоки аннотаций
объявить цветовое пространство недостаточно — обход считает то, что рисует каждая страница, включая серый устройства, который голый re f подбирает из начального состояния

Инлайн-изображения тащат своё цветовое пространство и сырые отсчёты прямо в поток содержимого, и сканеру приходится внимательно читать и то, и другое. /CS может быть сокращением (G, RGB, CMYK, I) или именем ресурса, скажем разрешающегося в [/Indexed /DeviceRGB 255 13 0 R], где считается база; а байты между ID и EI — произвольные двоичные, поэтому EI завершает изображение только между пробельными символами, иначе rg внутри сжатых отсчётов становится фантомным оператором

% page 1: DefaultRGB в области видимости, и XObject, который никто не вызывает
4 0 obj << /Type /Page /Parent 2 0 R /Contents 5 0 R
           /Resources << /ColorSpace << /DefaultRGB 9 0 R >>
                         /XObject << /Fm0 6 0 R >> >> >>
endobj
% page 2: тот же поток содержимого, без подсловаря ColorSpace
7 0 obj << /Type /Page /Parent 2 0 R /Contents 5 0 R /Resources << >> >>
endobj
5 0 obj << /Length 46 >> stream
1 0 0 rg 72 72 200 100 re f
q 0 0 10 10 re f Q
endstream
endobj
6 0 obj << /Type /XObject /Subtype /Form /BBox [0 0 50 50] /Length 24 >> stream
0 0 0 1 k 0 0 50 50 re f
endstream
endobj
9 0 obj [/ICCBased 10 0 R]
endobj

Каждый симптом из вступления вытекает из этого фрагмента. /Fm0 никогда не встречает Do, так что его k нерелевантен. Голый re f на обеих страницах — DeviceGray без ничего, что могло бы его калибровать. rg на странице 1 покрывается DefaultRGB из ресурсов, которыми страница 1 рисует; страница 2 гонит тот же поток против ресурсов без отображения, поэтому те же байты там некалиброваны. Ожидается для заявления PDF/A-1b: pvaiMissingOutputIntent с pvaiMissingIccProfile, вызванные RGB страницы 2 и серым обеих страниц. Форма не вносит вклада; вызовите её — и её CMYK добавит сверху запрет смешивания PDF/A-1

Пространства цвета Default принадлежат словарю ресурсов, который рисует

Отображения Default ищутся в подсловаре ColorSpace текущего словаря ресурсов (ISO 32000-1 §8.6.5.6), что делает их ровно настолько же локальными, насколько локальны сами ресурсы; у документа нет глобального дефолта. У XObject-изображения нет собственных Resources, поэтому изображение, чей словарь говорит /ColorSpace /DeviceCMYK, калибруется DefaultCMYK той страницы или формы, которая его рисует, и одно и то же изображение, нарисованное из двух мест, может быть калиброванным в одном и некалиброванным в другом. Form XObject, мозаичные паттерны и глифовые процедуры Type 3 несут собственные Resources, поэтому DefaultCMYK страницы внутрь них не доходит, а отображение из их собственного словаря — доходит. PDFium Component ограничивает отображения тем же способом. Вердикт возвращается записью TPdfAValidationResult: обнаруженная Conformance плюс набор Issues. Этого достаточно, чтобы разобрать папку

uses
  System.SysUtils, System.Classes, System.TypInfo, FPdfPdfa;

procedure ReportColorFindings(const FileName: string; Log: TStrings);
const
  ColorRules: TPdfAValidationIssues = [pvaiMissingOutputIntent,
    pvaiMissingIccProfile, pvaiMixedDeviceColorSpaces, pvaiBlendSpaceUndefined];
var
  Source: TFileStream;
  Res: TPdfAValidationResult;
  Found: TPdfAValidationIssues;
  Issue: TPdfAValidationIssue;
begin
  Source := TFileStream.Create(FileName, fmOpenRead or fmShareDenyWrite);
  try
    Res := ValidatePdfACompliance(Source);   // читает с позиции 0
  finally
    Source.Free;
  end;
  Log.Add(ExtractFileName(FileName) + ': ' +
    GetEnumName(TypeInfo(TPdfAConformance), Ord(Res.Conformance)));
  Found := Res.Issues * ColorRules;
  for Issue in Found do
    case Issue of
      pvaiMissingOutputIntent:
        Log.Add('  reachable uncalibrated device color, no PDF/A OutputIntent');
      pvaiMissingIccProfile:
        Log.Add('  no OutputIntent with a DestOutputProfile ICC stream');
      pvaiMixedDeviceColorSpaces:
        Log.Add('  RGB/CMYK use disagrees with the intent, or PDF/A-1 paints both');
      pvaiBlendSpaceUndefined:
        Log.Add('  transparent page without a blending color space');
    end;
  if Found = [] then
    Log.Add('  device color and blending rules satisfied');
end;

pvaiMissingOutputIntent и pvaiMissingIccProfile появляются вместе, когда обход достигает некалиброванного цвета устройства, а PDF/A OutputIntent не существует. pvaiMissingIccProfile в одиночку означает, что намерение есть, но профиля не несёт, — 6.2.2 такое намерением PDF/A не считает. pvaiMixedDeviceColorSpaces покрывает и запрет смешивания PDF/A-1, и несовпадение по компонентам: некалиброванный DeviceRGB под четырёхкомпонентным намерением или DeviceCMYK под трёхкомпонентным. Набор называет правило, а не страницу, поэтому в длинном или склеенном документе сравнивайте, какие страницы объявляют записи Default, а какие нет

Одно изображение DeviceCMYK, нарисованное из двух словарей ресурсов в Delphi: калибровано DefaultCMYK на одной странице и некалибровано на другой, как сообщает PDFium Component
пространства цвета Default принадлежат словарю ресурсов, который рисует, поэтому одно и то же изображение может быть калиброванным там, где его рисует одна страница, и некалиброванным там, где форма

Когда прозрачной странице нужно собственное пространство смешивания?

Прозрачной странице собственное цветовое пространство смешивания нужно, только когда у документа нет PDF/A OutputIntent. ISO 19005-2 6.2.10 (6.2.9 в PDF/A-4) делает намерение пространством смешивания по умолчанию в смысле ISO 32000-1 §11.3.4; без намерения каждая страница, содержащая прозрачность, должна нести Group, чей словарь атрибутов (ISO 32000-1 Table 147) включает CS. Тот CS тоже подчиняется 6.2.4, поэтому группа страницы из /DeviceCMYK под sRGB-намерением докладывается как отсутствующая, и PDFium Component поднимает pvaiBlendSpaceUndefined для обоих случаев; CalRGB или групповое пространство ICCBased такой зависимости не имеет. PDF/A-1 до этого вопроса не добирается, потому что ISO 19005-1 6.4 исключает прозрачность целиком

Трудная часть — формулировка содержит прозрачность. Приложение A ISO 19005-2 нормативно и проверяет от страницы наружу; подсчёт ключей /SMask или значений /ca по всему файлу отвечает на другой вопрос. Побайтовый поиск сталкивается и с прочим синтаксисом прозрачности: /S /Trans, запрещённое действие перехода, служит префиксом каждой группе /S /Transparency, а /Screen и запрещённый подтип аннотации, и стандартный режим смешивания, поэтому имя ничего не значит, пока не спарено со своим ключом. Следуя приложению A, PDFium Component считает страницу прозрачной, когда от неё достижимо любое из перечисленного:

  • ExtGState, который содержимое выбирает через gs, со словарём мягкой маски, с ca или CA ниже 1 или с режимом смешивания помимо Normal; объявленный, но ни разу не выбранный, не считается
  • Form XObject, вызываемый через Do, чей /Group имеет /S /Transparency или чьё собственное содержимое отвечает одному из этих условий; мозаичный паттерн судится как форма
  • XObject-изображение с потоком SMask или значением SMaskInData выше 0
  • глифовое описание шрифта Type 3, которым страница показывает текст, судится как форма
  • appearance-поток аннотации, включая каждое состояние виджета, и режим смешивания на самом словаре аннотации

Группы ещё и замыкаются на правило цвета устройства, и вот здесь корпус нас поправил. Набор veraPDF для 6.2.4.3 содержит файлы PDF/A-4 без всякого OutputIntent, которые рисуют RGB устройства и должны проходить, потому что /Group каждой страницы называет калиброванный CS. Мы считали, что калибровать могут только изолированные группы, ведь не изолированная группа композитится в свой фон, — для групп страниц это оказалось неверно: Table 147 замечает, что страница, наложенная прямо на носитель вывода, фактически изолирована независимо от I, поэтому её CS чтится. На Form XObject или appearance аннотации флаг уже решает; в наборе есть проходящий и валящийся файлы, различающиеся только /I true. DeviceGray калибровки от группы не берёт, потому что 6.2.4.3 требует DefaultGray или намерения, а группе, чей CS — пространство устройства, нужна калибровка самой. Поэтому PDFium Component позволяет группе страницы калибровать, что бы ни говорил её /I, позволяет форме или группе appearance калибровать только в изоляции, держит серый вне групповой калибровки и всё равно проверяет пространство устройства, которое группа называет

Матрица PDF/A для Delphi: когда прозрачной странице нужно собственное пространство смешивания CS и как PDFium Component судит калибровку групп страницы, формы и appearance
выходное намерение — пространство смешивания по умолчанию; группа страницы калибрует цвет устройства, что бы ни говорил её флаг изоляции, а группе формы нужен /I true

Один OutputIntent вместо группы на каждой странице

Если архив пишет ваш собственный код, начните с PDF/A OutputIntent на уровне документа. Одна запись каталога калибрует DeviceGray везде, покрывает соответствующее пространство устройства на каждой странице и служит пространством смешивания по умолчанию для каждой прозрачной страницы. Группы страниц и записи Default ограничены каждый своим словарём ресурсов, и страница, которую поздний merge добавит без них, — та самая, что валится. PDF 2.0 добавляет OutputIntents в объект страницы (ISO 32000-2 §7.7.3.3 и §14.11.5); PDFium Component принимает такое намерение уровня страницы для PDF/A-4, когда в каталоге его нет, тогда как части с 1 по 3, построенные на PDF 1.4–1.7, смотрят только в каталог. Для RGB-содержимого SaveAsPdfAToStream добавляет намерение в каталог, построенное на встроенном sRGB ICC-профиле писателя PDF/A, а валидация вывода показывает, было ли содержимое действительно только RGB

uses
  System.SysUtils, System.Classes, PDFium, FPdfPdfa;

function ArchiveWithSrgbIntent(const InFile, OutFile: string;
  out Remaining: TPdfAValidationIssues): Boolean;
var
  Pdf: TPdf;
  Opts: TPdfASaveOptions;
  Output: TMemoryStream;
  Check: TPdfAValidationResult;
begin
  Result := False;
  Remaining := [];
  Pdf := TPdf.Create(nil);
  Output := TMemoryStream.Create;
  try
    Pdf.FileName := InFile;
    Pdf.Active := True;
    if not Pdf.Active then
      Exit;                           // файл не открылся
    Opts := TPdfASaveOptions.Default;
    Opts.Conformance := pac2b;        // PDF/A-1 вовсе запретил бы прозрачность
    // IccProfileData остаётся пустым: используется встроенный профиль sRGB IEC61966-2.1
    if not Pdf.SaveAsPdfAToStream(Output, Opts) then
      Exit;
    Check := ValidatePdfACompliance(Output);
    Remaining := Check.Issues;
    // Ожидается для содержимого только из RGB: pvaiMissingOutputIntent и
    // pvaiBlendSpaceUndefined исчезают. pvaiMixedDeviceColorSpaces теперь значит
    // достижимый путь рисует DeviceCMYK, который не калибрует никакой DefaultCMYK
    if Check.IsCompliant then
    begin
      Output.SaveToFile(OutFile);
      Result := True;
    end;
  finally
    Output.Free;
    Pdf.Free;
  end;
end;

pvaiMixedDeviceColorSpaces, переживший sRGB-намерение, — это обход, сообщающий DeviceCMYK на достижимом пути, и такому документу нужно отображение DefaultCMYK на рисующих его страницах или выходной CMYK-профиль, прежде чем PDFium Component примет его как PDF/A-2b