Artigo Técnico

Tintas diretas NChannel consistentes em Delphi com HotPDF

Uma casa de pré-impressão devolve o trabalho: a mesma tinta direta separada em duas chapas. O HotPDF evita-o ao tempo de criação escrevendo NChannel como a forma DeviceN de cinco elementos da ISO 32000-2 e mantendo um espaço alternativo canónico e uma transformação de tint por nome de tinta direta para todo o documento, rejeitando a segunda definição em conflito em vez de a emitir

NChannel não é um nome de família de espaços de cor

A primeira coisa a desaprender é o próprio nome. NChannel não é uma família de espaços de cor como Separation e DeviceN são famílias. A ISO 32000-2 §8.6.6.5 descreve-o como um subtipo de DeviceN, pelo que um espaço NChannel conforme se escreve como a matriz de cinco elementos [/DeviceN names alternateSpace tintTransform attributes], e o dicionário de atributos traz /Subtype /NChannel. Não existe nenhuma matriz [/NChannel ...] na especificação. Se alguma vez construiu uma à mão e viu um RIP encolher os ombros, é por isto

O HotPDF escreve um espaço NChannel como a matriz DeviceN de cinco elementos cujo dicionário de atributos traz Subtype NChannel junto com as entradas Process, Colorants e MixingHints, porque não existe nenhuma matriz de nome de família NChannel na especificação de todo
Um espaço NChannel conforme é uma matriz DeviceN com um dicionário de atributos, e é por isso que o escritor emite apenas esta forma enquanto o leitor ainda aceita o token antigo

O HotPDF errou isto uma vez e depois corrigiu, o que vale a pena dizer sem rodeios porque molda o comportamento do componente hoje. Versões antigas do HotPDF emitiam a forma de nome de família. O THotPDF.RegisterNChannelColorSpace emite agora apenas a forma padrão DeviceN-com-atributos, e como o subtipo NChannel chegou no PDF 1.6, o ponto de entrada do produtor faz gate em RequirePDFVersion(pdf16, ...) e simplesmente declina em alvos mais antigos. O lado da renderização é deliberadamente mais tolerante do que o escritor: o HPDFResolveColorSpace ainda aceita o token antigo /NChannel como família DeviceN, para que ficheiros do escritor antigo continuem a renderizar, mas tudo o que o HotPDF escreve de novo usa a codificação padrão. Tolerante à entrada, rigoroso à saída é a assimetria certa aqui, porque o seu leitor tem de lidar com ficheiros que não criou, enquanto o seu escritor não tem essa desculpa

Porque é que um nome de tinta direta acaba em duas chapas?

Porque um nome de colorante de tinta direta é uma identidade de chapa ao nível do documento, não um argumento local. Duas chamadas que ambas nomeiam Orange mas entregam um espaço alternativo diferente, ou o mesmo espaço alternativo com uma transformação de tint diferente, descrevem duas tintas diferentes que por acaso partilham um rótulo. Um RIP a construir separações não tem forma de reconciliar isso, por isso faz a única coisa honesta e entrega-lhe duas chapas. O HotPDF mantém por isso uma assinatura canónica por documento por nome de colorante. O RegisterSpotColorantDefinition compõe essa assinatura a partir do espaço de cor alternativo e da forma da função de tint, e cada RegisterSeparation, RegisterSeparationFunc, RegisterSeparationLUT e definição de spot NChannel passa por ele. Quando uma segunda definição discorda, a chamada levanta exceção em vez de registar em silêncio uma segunda variante, e a mensagem é deliberadamente específica quanto ao modo de falha, porque a alternativa é descobri-lo numa prova de chapa três semanas depois

Cada chamada de registo de spot do HotPDF passa por RegisterSpotColorantDefinition, que regista um espaço alternativo canónico e uma assinatura de tint por nome de colorante para o documento e levanta exceção quando chega uma segunda definição discordante do mesmo nome
Um nome, uma assinatura: a segunda definição em conflito de um spot é recusada ao tempo de criação em vez de ser descoberta numa prova de chapa
// Orange já estava registado contra DeviceCMYK com a
// transformação de tint 0 / 0.55 / 1 / 0 no tint máximo.
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;

Dividir os nomes mestres entre process e spot

Um NChannel completo tem de dar conta de cada um dos seus nomes de colorante mestres exatamente uma vez, como componente de process ou como colorante de spot. A overload avançada do RegisterNChannelColorSpace recebe os ColorantNames mestres, os ProcessColorantNames, o espaço alternativo, a transformação de tint global, uma matriz de registos THPDFNChannelSpotColorant, e uma ordem de impressão opcional. Cada registo de spot traz o seu próprio nome, a sua própria transformação de tint Separation de uma entrada, uma solidity opcional, e uma função de dot-gain opcional. A transformação de tint global tem de mapear N entradas para a contagem de componentes do espaço alternativo; cada tint de spot tem de mapear uma entrada para essa mesma contagem

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);

Dessa chamada o HotPDF emite o dicionário de atributos que a especificação pede: /Subtype /NChannel, um dicionário /Process cujo /ColorSpace é o espaço de process e cujo /Components lista os nomes de process pela ordem de componentes desse espaço, um dicionário /Colorants que guarda uma matriz [/Separation name alternate tintfn] real para cada spot, e um dicionário /MixingHints que traz /Solidities, /PrintingOrder e /DotGain quando os forneceu. O nome de recurso devolvido vai para SetFillColorSpace ou SetStrokeColorSpace exatamente como os espaços mais simples cobertos no artigo sobre renderização de cores diretas Separation e DeviceN

O HotPDF divide os nomes de colorante mestres de um espaço NChannel num dicionário Process para os quatro componentes CMYK e num dicionário Colorants que guarda uma matriz Separation por spot, com todas as aridades verificadas antes de escrever
Cada nome mestre é reclamado exatamente uma vez, e a transformação de tint global, os tints por spot e a ordem de impressão são todos validados antes de qualquer objeto ser emitido

O que é que a verificação de consistência realmente rejeita?

Rejeita incoerência estrutural dentro do espaço, e fá-lo antes de um único objeto ser escrito. Os nomes de colorante têm de ser únicos e não podem ser vazios, All ou None. As definições de process e de spot em conjunto têm de cobrir os nomes mestres exatamente, sem colorante a aparecer nos dois papéis e sem nenhum ficar por definir. Quando o espaço alternativo é DeviceCMYK, os componentes de process têm de ser Cyan, Magenta, Yellow, Black por essa ordem, e a contagem de nomes de process tem de coincidir com a contagem de componentes do alternativo. Cada transformação de tint e função de dot-gain tem de ser um objeto de função indireto com a aridade de entrada e saída certa. A solidity tem de ser um valor finito dentro de 0..1. A ordem de impressão tem de ser vazia ou uma permutação completa dos nomes mestres, nunca uma lista parcial. O que não faz é julgar cor: nada aqui verifica que a sua transformação de tint de Orange realmente se parece com a tinta do latão, que a sua composição CMYK é um proxy sensato, ou que a solidity que forneceu corresponde ao comportamento medido no substrato. Essas são questões de impressão e de medição, e o componente não tem autoridade para as responder. A overload mais simples, só de process, é ainda mais rigorosa por desenho: produz um NChannel só de process e recusa deliberadamente aceitar nomes de spot, porque escrever um spot na matriz de nomes sem uma entrada /Colorants correspondente produziria um ficheiro estruturalmente não conforme, e fabricar uma definição predefinida seria pior do que falhar

Os output intents PDF/X-6n têm de cobrir todos os spots registados

Um ficheiro PDF/X-6n de N colorantes declara as suas colorantes duas vezes, e as duas declarações têm de coincidir. O AddPDFX6ExternalOutputIntent escreve a referência de perfil ICC externa com a sua ColorantTable, e antes de o fazer, o ValidateRegisteredSpotOutputColorants percorre todos os spots que o documento registou e levanta exceção se algum faltar na tabela. A verificação corre nos dois sentidos: depois de um output intent ter publicado a sua lista de colorantes, um registo de spot posterior para um nome fora dessa lista também é recusado. O AddPDFX6ExternalOutputIntentSpotData acrescenta os metadados por tinta por cima disso, e impõe as suas próprias regras, nomeadamente que um colorante traz ou um valor de solidity ou dados espectrais CxF/X-4, nunca ambos. Esta é a mesma superfície de conformidade discutida no texto sobre validação PDF/A, PDF/X e PDF/UA

Spectral := TMemoryStream.Create;
try
  LoadCxFForInk('Orange', Spectral);   // payload 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 para um colorante
    Order,
    ['Orange'], [Spectral]);           // dados espectrais para outro
finally
  Spectral.Free;
end;

Dois detalhes operacionais são fáceis de perder. O registo que suporta tudo isto é por documento e é limpo nas fronteiras de documento, pelo que carregar um ficheiro novo na mesma instância de THotPDF não herda as identidades de spot do documento anterior; esse isolamento é o ponto, porque uma assinatura que vazasse rejeitaria trabalho perfeitamente válido no trabalho seguinte. E o lado do perfil tem limites duros próprios: um output intent externo exige um alvo PDF 2.0, um URL de perfil absoluto HTTP ou HTTPS, uma assinatura de espaço de cor ICC de quatro bytes, e para PDF/X-6n entre 2 e 15 colorantes com uma assinatura correspondente de 2CLR a FCLR

Onde a verificação para

O tratamento de CxF/X-4 é a parte sobre a qual convém ser honesto. O HotPDF aplica verificações estruturais de segurança limitadas ao stream espectral e confirma que a identidade da tinta lá dentro corresponde ao colorante que nomeou. Limita o tamanho do payload, rejeita bytes nulos embebidos, recusa qualquer stream que contenha uma declaração DOCTYPE ou ENTITY, exige uma raiz CxF reconhecível, e exige exatamente um elemento SpotInkCharacterisation que transporte exatamente um SpotInkName igual ao nome do seu colorante. Isso é um gate contra entrada malformada e hostil, não um validador de schema. Não é uma implementação completa da ISO 17972-4, não verifica as suas medições espectrais, e não audita em retrocesso grafos de objetos de terceiros pré-existentes arbitrários nem as tabelas de colorantes internas de um perfil ICC embebido. Se o seu fluxo de trabalho depende de conformidade CxF completa, valide o ficheiro com uma ferramenta dedicada antes de o entregar ao componente

Uma restrição adjacente morde quem nunca esperou encontrá-la. Uma soft mask de luminosidade não pode usar Separation, DeviceN ou NChannel como /CS do seu grupo de transparência; o espaço de mistura do grupo tem de ser um espaço de dispositivo ou baseado em CIE, pelo que a tinta direta dentro do grupo é resolvida através da sua transformação de tint para o espaço alternativo antes de a luminosidade ser calculada. O RegisterLuminositySoftMaskState constrói um grupo DeviceGray precisamente para que isto não possa correr mal por acidente. A consequência prática é que um design carregado de spots mascarado desta forma está a ser avaliado através do seu proxy CMYK, não da sua tinta, o que importa quando o compara com uma prova separada como descrito nas notas sobre provas de overprint e dispositivos de renderização

Nada disto remove a necessidade de uma prova de chapa, mas move toda uma classe de rejeições de pré-impressão da sala de impressão de volta para a build. As APIs de NChannel, Separation e output intent PDF/X-6n descritas aqui são distribuídas no HotPDF Delphi Component padrão para Delphi e C++Builder, onde a referência documenta os layouts completos dos registos e as condições exatas em que cada chamada falha de forma segura