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

Согласованность плашечных красок NChannel в Delphi с HotPDF

Типография возвращает макет: одна и та же плашечная краска развелась на две формы. HotPDF предотвращает это ещё на этапе создания, записывая NChannel в пятиэлементной DeviceN-форме по ISO 32000-2 и удерживая одну каноническую альтернативную палитру и тинт-трансформ на каждое имя плашки в целом документе, отказывая второй, конфликтующей декларации вместо её записи

NChannel — не имя семейства цветовых пространств

Первое, что придётся разучить, — само имя. NChannel — не семейство цветовых пространств, как Separation и DeviceN. ISO 32000-2 §8.6.6.5 описывает его как подтип DeviceN, поэтому корректное пространство NChannel записывается пятиэлементным массивом [/DeviceN names alternateSpace tintTransform attributes], а словарь атрибутов несёт /Subtype /NChannel. Массива [/NChannel ...] в спецификации нет. Если вы когда-то собирали такой вручную и наблюдали, как RIP пожимает плечами, — вот почему

HotPDF записывает пространство NChannel пятиэлементным DeviceN-массивом, чей словарь атрибутов несёт Subtype NChannel вместе с записями Process, Colorants и MixingHints, потому что массива с именем семейства NChannel в спецификации вообще нет
Корректное пространство NChannel — это DeviceN-массив со словарём атрибутов, поэтому писатель выдаёт только эту форму, а читатель по-прежнему принимает унаследованный токен

HotPDF ошибся в этом однажды и затем починил, и об этом стоит сказать прямо, потому что это определяет сегодняшнее поведение компонента. Старые выпуски HotPDF выдавали форму с именем семейства. THotPDF.RegisterNChannelColorSpace теперь выдаёт только стандартную форму DeviceN-плюс-атрибуты, а поскольку подтип NChannel появился в PDF 1.6, точка входа производителя стоит на вратах RequirePDFVersion(pdf16, ...) и просто отказывает на более старых целях. Сторона рендеринга намеренно снисходительнее пишущего: HPDFResolveColorSpace всё ещё принимает унаследованный токен /NChannel как семейство DeviceN, чтобы файлы старого писателя продолжали рендериться, но всё, что HotPDF записывает обратно, использует стандартную кодировку. Снисходительность на входе, строгость на выходе — правильная асимметрия здесь, ведь вашему читателю приходится справляться с файлами, которые он не создавал, а у вашего писателя такого оправдания нет

Почему одно имя плашки оказывается на двух формах?

Потому что имя плашечного красителя — это идентичность формы в масштабе документа, а не локальный аргумент. Два вызова, оба именующие Orange, но передающие разные альтернативные палитры, или одну палитру с другим тинт-трансформом, описывают две разные краски, случайно разделившие этикетку. RIP, строящий цветоделения, не может это примирить, поэтому делает единственно честное — даёт вам две формы. Поэтому HotPDF ведёт каноническую подпись на имя красителя в масштабе документа. RegisterSpotColorantDefinition составляет эту подпись из альтернативного цветового пространства и формы тинт-функции, и каждый RegisterSeparation, RegisterSeparationFunc, RegisterSeparationLUT и плашечная декларация NChannel проходят через неё. Когда вторая декларация расходится, вызов выбрасывает исключение вместо молчаливой регистрации второго варианта, и сообщение намеренно конкретно о режиме сбоя, ведь альтернатива — обнаружить это на цветопробе формы три недели спустя

Каждый вызов регистрации плашки HotPDF проходит через RegisterSpotColorantDefinition, который записывает одну каноническую альтернативную палитру и подпись тинта на имя красителя в документе и выбрасывает исключение, когда приходит вторая, расходящаяся декларация того же имени
Одно имя — одна подпись: вторая, конфликтующая декларация плашки отвергается на этапе создания, а не обнаруживается на цветопробе
// Orange уже был зарегистрирован против DeviceCMYK с
// тинт-трансформом 0 / 0.55 / 1 / 0 при полном тинте.
Conflicting := Pdf.RegisterExponentialFunction(
  Domain1, NoInk, OtherOrangeCMYK, 1, []);
try
  Pdf.RegisterSeparationFunc('Orange', 'DeviceCMYK', Conflicting);
except
  on E: Exception do
    // 'Spot colourant "Orange" has inconsistent alternate colour
    //  space or tint definition in this document'
    LogPrepressWarning(E.Message);
end;

Разделение главных имён на process и spot

Полный NChannel должен учесть каждое из главных имён красителей ровно один раз — либо как компонент process, либо как плашечный краситель. Расширенная перегрузка RegisterNChannelColorSpace принимает главные ColorantNames, ProcessColorantNames, альтернативную палитру, общий тинт-трансформ, массив записей THPDFNChannelSpotColorant и необязательный порядок печати. Каждая плашечная запись несёт собственное имя, собственный одновходовый Separation тинт-трансформ, необязательную плотность (solidity) и необязательную функцию растискивания. Общий тинт-трансформ должен отображать N входов в число компонентов альтернативной палитры; каждый плашечный тинт — один вход в то же число

const
  Colorants: array[0..4] of AnsiString =
    ('Cyan', 'Magenta', 'Yellow', 'Black', 'Orange');
  ProcessNames: array[0..3] of AnsiString =
    ('Cyan', 'Magenta', 'Yellow', 'Black');
  Order: array[0..4] of AnsiString =
    ('Yellow', 'Magenta', 'Cyan', 'Orange', 'Black');
  Domain5: array[0..9] of Single = (0, 1, 0, 1, 0, 1, 0, 1, 0, 1);
  Range4: array[0..7] of Single = (0, 1, 0, 1, 0, 1, 0, 1);
  Domain1: array[0..1] of Single = (0, 1);
  NoInk: array[0..3] of Single = (0, 0, 0, 0);
  OrangeCMYK: array[0..3] of Single = (0, 0.55, 1, 0);
  GainC0: array[0..0] of Single = (0);
  GainC1: array[0..0] of Single = (1);
var
  Pdf: THotPDF;
  Spots: array[0..0] of THPDFNChannelSpotColorant;
  CSName: AnsiString;
begin
  Pdf.Version := pdf20;
  Pdf.BeginDoc;
  Spots[0].Name := 'Orange';
  Spots[0].TintTransform := Pdf.RegisterExponentialFunction(
    Domain1, NoInk, OrangeCMYK, 1, []);
  Spots[0].HasSolidity := True;
  Spots[0].Solidity := 0.82;
  Spots[0].DotGainFunction := Pdf.RegisterExponentialFunction(
    Domain1, GainC0, GainC1, 1, []);
  CSName := Pdf.RegisterNChannelColorSpace(Colorants, ProcessNames,
    'DeviceCMYK',
    Pdf.RegisterPostScriptFunction(Domain5, Range4,
      '{ pop pop pop pop pop 0 0 0 0 }'),
    Spots, Order);

Из этого вызова HotPDF выдаёт словарь атрибутов, который требует спецификация: /Subtype /NChannel, словарь /Process, чей /ColorSpace — process-пространство, а /Components перечисляет process-имена в порядке компонентов того пространства, словарь /Colorants, держащий настоящий массив [/Separation name alternate tintfn] для каждой плашки, и словарь /MixingHints с /Solidities, /PrintingOrder и /DotGain, когда вы их передали. Возвращённое имя ресурса идёт в SetFillColorSpace или SetStrokeColorSpace точно так же, как более простые пространства из статьи о рендеринге плашечных цветов Separation и DeviceN

HotPDF разделяет главные имена красителей пространства NChannel на словарь Process для четырёх CMYK-компонентов и словарь Colorants, держащий по одному Separation-массиву на плашку, проверяя каждую арность до записи
Каждое главное имя занято ровно один раз, а общий тинт-трансформ, плашечные тинты и порядок печати все проверены до выдачи любого объекта

Что на самом деле отклоняет проверка согласованности?

Она отклоняет структурную несогласованность внутри пространства и делает это прежде, чем записан хоть один объект. Имена красителей должны быть уникальны и не могут быть пустыми, All или None. Декларации process и spot вместе должны покрывать главные имена точно, без красителя в обеих ролях и без неопределённых. Когда альтернативная палитра — DeviceCMYK, process-компоненты должны быть Cyan, Magenta, Yellow, Black именно в этом порядке, а число process-имён должно совпадать с числом компонентов альтернативы. Каждый тинт-трансформ и функция растискивания должны быть косвенным функциональным объектом с правильной арностью входов и выходов. Solidity должна быть конечным значением внутри 0..1. Порядок печати должен быть пустым или полной перестановкой главных имён, никогда частичным списком. Чего она не делает — судит цвет: ничто здесь не проверяет, что ваш тинт-трансформ Orange действительно похож на краску в банке, что его CMYK-сборка — вменяемый прокси, или что переданная solidity совпадает с измеренным поведением на материале. Это вопросы печатного станка и измерений, и компонент не имеет полномочий их решать. Более простая перегрузка только для process строже по дизайну: она выдаёт NChannel только с process и намеренно отказывается принимать имена плашек, ведь запись плашки в массив имён без соответствующей записи /Colorants дала бы структурно некорректный файл, а выдумывание декларации по умолчанию хуже, чем честный отказ

Выходные намерения PDF/X-6n должны покрывать каждую зарегистрированную плашку

Файл PDF/X-6n с N красителями декларирует красители дважды, и обе декларации должны совпадать. AddPDFX6ExternalOutputIntent записывает внешнюю ссылку ICC-профиля с его ColorantTable, и прежде чем сделать это, ValidateRegisteredSpotOutputColorants обходит каждую плашку, зарегистрированную документом, и выбрасывает исключение, если какой-то нет в таблице. Проверка идёт в обе стороны: как только выходное намерение опубликовало свой список красителей, более поздняя регистрация плашки с именем вне списка также отклоняется. AddPDFX6ExternalOutputIntentSpotData добавляет поверх метаданные по краскам и навязывает собственные правила, в частности краситель несёт либо значение solidity, либо спектральные данные CxF/X-4, никогда оба. Это та же поверхность соответствия, обсуждаемая в статье о валидации PDF/A, PDF/X и PDF/UA

Spectral := TMemoryStream.Create;
try
  LoadCxFForInk('Orange', Spectral);   // полезная нагрузка ISO 17972-4
  Pdf.AddPDFX6ExternalOutputIntentSpotData(
    'ECG-5', 'Five-colour output condition',
    'https://profiles.example.com/ecg-5.icc', '5CLR',
    Colorants,
    '00112233445566778899AABBCCDDEEFF', #4#3#0#0,
    ['Cyan'], [0.70],                  // solidity для одного красителя
    Order,
    ['Orange'], [Spectral]);           // спектральные данные для другого
finally
  Spectral.Free;
end;

Две операционные детали легко пропустить. Реестр, стоящий за всем этим, — на документ и очищается на границах документов, поэтому загрузка нового файла в тот же экземпляр THotPDF не наследует плашечные идентичности предыдущего документа; изоляция и есть смысл, ведь утёкшая подпись отвергла бы совершенно корректную работу в следующем задании. А у стороны профиля свои жёсткие пределы: внешнее выходное намерение требует цель PDF 2.0, абсолютный URL профиля HTTP или HTTPS, четырёхбайтовую подпись цветового пространства ICC, а для PDF/X-6n — от 2 до 15 красителей с соответствующей подписью от 2CLR до FCLR

Где проверка останавливается

Обработка CxF/X-4 — та часть, о которой стоит быть честным. HotPDF применяет ограниченные структурные проверки безопасности к спектральному потоку и подтверждает, что идентичность краски внутри совпадает с названным красителем. Он ограничивает размер полезной нагрузки, отвергает встроенные нулевые байты, отказывает любому потоку с декларацией DOCTYPE или ENTITY, требует узнаваемый корень CxF и требует ровно один элемент SpotInkCharacterisation, несущий ровно один SpotInkName, равный имени вашего красителя. Это врата против искажённого и враждебного ввода, а не валидатор схемы. Это не полная реализация ISO 17972-4, она не проверяет ваши спектральные измерения и не проводит обратный аудит произвольных заранее существующих сторонних графов объектов или внутренних таблиц красителей внутри встроенного ICC-профиля. Если ваш поток работы зависит от полного соответствия CxF, валидируйте файл выделенным инструментом, прежде чем отдавать его компоненту

Одно соседнее ограничение кусает тех, кто не ожидал с ним встретиться. Маска мягкости по светимости не может использовать Separation, DeviceN или NChannel как /CS группы прозрачности; пространство смешивания группы должно быть device- или CIE-пространством, поэтому плашечная краска внутри группы разрешается через свой тинт-трансформ в альтернативную палитру до вычисления светимости. RegisterLuminositySoftMaskState строит группу DeviceGray именно затем, чтобы это не могло сломаться случайно. Практическое следствие: насыщенный плашками дизайн, замаскированный так, оценивается через свой CMYK-прокси, а не через краску, что важно при сравнении с цветоделённой пробой, как описано в заметках о пробах с overprint и устройствах рендеринга

Ничто из этого не отменяет нужду в цветопробе формы, но перемещает целый класс допечатных отказов из печатного цеха обратно в сборку. Описанные здесь API NChannel, Separation и выходных намерений PDF/X-6n поставляются в стандартном HotPDF Delphi Component для Delphi и C++Builder, где справочник документирует полные раскладки записей и точные условия, при которых каждый вызов завершается явным отказом