Artigo Técnico

Renderizando Cores Especiais (Spot Colors) Separation e DeviceN no Delphi

O HotPDF renderiza cores especiais (spot colors) Separation e DeviceN em páginas PDF carregadas ao resolver o espaço de cores através de HPDFResolveColorSpace, avaliar a função de transformação de matiz com HPDFEvalTintTransform e converter o resultado através do espaço alternativo para RGB para a tela. Desde a versão v2.375.0, esse pipeline avalia todos os quatro tipos de função PDF, incluindo calculadoras PostScript Type 4, de modo que um arquivo pronto para impressão com tintas Pantone exibe suas cores reais em vez de um marcador. Este artigo explica como o pipeline funciona e, de forma igualmente útil, como ele falhou enquanto o construíamos

O caso de gatilho é sempre o mesmo. Um cliente recebe um PDF de uma gráfica: o título está configurado em uma tinta especial nominal, a arte da embalagem usa uma mistura DeviceN de duas tintas e o texto do corpo é preto simples. No Acrobat, parece perfeito. Em sua aplicação Delphi, o título é renderizado em preto ou, pior, não é renderizado de forma alguma, e o cliente registra um bug contra o seu software e não contra o arquivo. O arquivo está correto. O renderizador apenas não reconhece os espaços de cores que o arquivo usa

Por que as cores especiais são renderizadas em preto em um visualizador de PDF?

As cores especiais são renderizadas em preto, ou desaparecem, quando o renderizador implementa apenas os operadores de cor do dispositivo (rg, g, k) e ignora os genéricos. A ISO 32000-1 §8.6 define três grupos de espaços de cores: espaços de dispositivo (DeviceGray, DeviceRGB, DeviceCMYK), espaços baseados em CIE (CalGray, CalRGB, Lab, ICCBased) e espaços especiais (Indexed, Separation, DeviceN, Pattern). Tudo fora do grupo de dispositivos é selecionado com os operadores genéricos: cs e CS escolhem um espaço pelo nome a partir do dicionário de recursos da página, então sc, SC, scn e SCN fornecem os valores dos componentes. Um renderizador que ignora esses operadores mantém qualquer cor que tenha sido definida por último, o que para uma página que começa com seu título em cor especial é o preto inicial DeviceGray

O HotPDF adicionou o conjunto de operadores genéricos ao seu renderizador de páginas na versão v2.333.0, juntamente com um caminho de resolução unificado: cada entrada de recurso /ColorSpace, seja um nome simples, uma matriz inline ou uma referência indireta, é analisada em um registro THPDFColorSpace, e cada solicitação de cor de preenchimento ou traço passa por uma única chamada HPDFResolveColor. A enumeração da família mostra a cobertura à primeira vista

type
  THPDFColorSpaceFamily = (csfDeviceGray, csfDeviceRGB, csfDeviceCMYK,
                           csfIndexed, csfCalGray, csfCalRGB, csfLab,
                           csfICCBased, csfSeparation, csfDeviceN,
                           csfUnsupported);

function HPDFResolveColorSpace(Obj: THPDFObject): THPDFColorSpace;

function HPDFResolveColor(const CS: THPDFColorSpace;
  const Comps: THPDFColorComps; CompCount: Integer): THPDFRenderColor;

Uma decisão de design se paga repetidamente: csfUnsupported é uma família de primeira classe, não um erro. Um espaço que o renderizador não consegue interpretar degrada para um fallback definido em vez de abortar a página, o que corresponde ao comportamento dos visualizadores convencionais e evita que um único preenchimento exótico oculte um documento que de outra forma seria renderizável

Como uma transformação de matiz transforma um valor de tinta em uma cor real?

Um espaço Separation carrega três informações: o nome da tinta, um espaço de cores alternativo e uma função de transformação de matiz. A matriz [/Separation /PANTONE485 /DeviceCMYK f] diz: quando o fluxo de conteúdo grava 0.8 scn, insira o valor de matiz 0.8 na função f e pinte a quádrupla CMYK resultante. O DeviceN generaliza isso para N substituindo-o por uma função de N entradas. O nome da tinta em si é apenas informativo na tela; a transformação de matiz é toda a semântica de renderização, portanto, um renderizador que analisa o espaço mas ignora a função ainda não fez nada útil

O HPDFEvalTintTransform é o mecanismo de função por trás desse passo. Lançado na v2.334.0, ele avalia funções exponenciais Type 2 (C0 + x^N * (C1 - C0) com limitação de Domain e Range), funções de costura (stitching) Type 3 (recursão de subfunção selecionada por Bounds com mapeamento Encode) e funções amostradas Type 0 com amostras de 8, 16 e 32 bits. Type 0 é o mesmo mecanismo de tabela de busca que cobrimos do lado da criação no artigo sobre a construção de LUTs de cores Type 0; the render side walks the identical structure in reverse, from decoded sample bytes back to component values

Funções de calculadora PostScript Type 4 eram a última pendência. Até a v2.375.0, elas degradavam graciosamente para um marcador neutro; desde a v2.375.0, o HPDFEvalPostScriptCalculator executa o conjunto completo de operadores da ISO 32000-1 §7.10.5 em uma pilha de operandos delimitada: operadores aritméticos, de comparação, booleanos, bit a bit, manipulação de pilha incluindo roll e condicionais if/ifelse. A semântica de casos extremos é mais estrita do que parece. O round do PostScript arredonda metades em direção ao valor maior, de modo que o Round de arredondamento bancário do Delphi não pode ser usado; os operadores trigonométricos trabalham em graus, com o atan retornando valores em [0, 360); e o exp é uma potência de dois operandos, não a exponencial natural. O mesmo avaliador também impulsiona preenchimentos de degradê baseados em função, razão pela qual a renderização de sombreamento axial e radial adotou rampas orientadas por calculadora na mesma versão

// Evaluate a Separation/DeviceN tint transform (Type 0/2/3/4).
function HPDFEvalTintTransform(FuncObj: THPDFObject;
  const Inputs: THPDFColorComps; InputCount: Integer;
  out AltComps: THPDFColorComps): Boolean;

// Type 4 PostScript calculator, ISO 32000-1 7.10.5 operator set.
function HPDFEvalPostScriptCalculator(const Prog: TBytes;
  const Inputs: THPDFColorComps; InputCount: Integer;
  var Outputs: THPDFColorComps; OutCount: Integer): Boolean;

Sinceridade sobre a precisão: uma calculadora de software avaliada em precisão dupla não corresponderá a um RIP bit a bit, e a limitação no limite de Range pode diferir por um bit menos significativo em relação a outra implementação. Para exibição em tela e renderização de regressão, isso é irrelevante; se você estiver construindo um sistema de prova com gerenciamento de cores, a transformação de matiz é apenas o primeiro estágio e você ainda precisará de um CMM real downstream

CalGray, CalRGB, Lab e ICCBased sem um mecanismo ICC

As famílias baseadas em CIE seguem o outro ramo do mesmo resolver. O HotPDF converte valores Lab por meio da cadeia padrão Lab para XYZ para sRGB chain, incluindo o cubo de ponto de transição 6/29 na função de transferência inversa, e lida com o CalRGB com sua gama por canal mais matriz linear 3x3, e CalGray com sua gama única. O detalhe relevante para o desempenho é o tratamento do ponto branco: a matriz de conversão XYZ para sRGB é adaptada por Bradford ao ponto branco declarado no espaço de cores e armazenada em cache no registro THPDFColorSpace resolvido, de modo que o trabalho por pixel permanece como uma única multiplicação 3x3, por mais exótico que seja o iluminante declarado

Espaços ICCBased recebem um tratamento deliberadamente pragmático. A especificação do PDF exige que cada fluxo ICCBased declare um espaço /Alternate ou um implícito por meio de sua contagem de componentes /N, precisamente para que visualizadores sem um mecanismo de gerenciamento de cores ainda possam renderizar de forma sensata. O HotPDF resolve o ICCBased através desse alternativo, ou deduz DeviceGray, DeviceRGB ou DeviceCMYK com base em /N sendo 1, 3 ou 4 quando a entrada está ausente, e nunca analisa os bytes do perfil. Isso significa nenhuma dependência do lcms e nenhum custo de busca de perfil, ao preço da precisão colorimétrica: um espaço ICCBased cujo perfil divirja fortemente de seu alternativo exibirá a renderização alternativa. Para visualização na tela e miniaturas, esta é a mesma escolha que todo visualizador leve faz, e é o limite que deve ser declarado claramente em sua própria documentação

O bug que fazia falhar toda busca por espaços de cores nominais

A implementação de operadores na v2.333.0 foi lançada com um defeito que permaneceu invisível por quarenta e duas versões: os manipuladores cs and CS buscavam seu operando com a barra inicial intacta (/CS0) em chaves de dicionário de recursos armazenadas sem a barra (CS0). Toda busca por espaços de cores nominais falhava, cem por cento das vezes, e o código recorria silenciosamente ao DeviceGray padrão. O sintoma visível era sutil da pior maneira: um preenchimento Separation de 1 scn tornava-se DeviceGray 1.0, que pinta em branco, e tinta branca em uma página branca não é um erro de renderização do qual alguém tire uma captura de tela. A correção, na v2.375.0, foi um auxiliar de normalização de nomes compartilhado aplicado em cada busca de operando para recurso

Dois defeitos correlatos surgiram da mesma investigação. Primeiro, as referências indiretas a objetos com valores de matriz eram retornadas sem resolução: o acesso ao documento do renderizador tinha resolvers tipados apenas para fluxos e dicionários, de modo que /CS0 5 0 R apontando para uma matriz independente [/Separation ...] retornava como a referência bruta e o espaço era analisado como não suportado. Segundo, o HPDFReadNumericArray impunha uma semântica de comprimento estrita, exigindo que a matriz PDF fosse pelo menos tão longa quanto o buffer fornecido. A leitura de /C0 e /C1 de uma função Type 2 em um buffer de quatro elementos falhava, portanto, para alternativas de um e três componentes, deixando ambas as matrizes zeradas, e cada matiz exponencial não-CMYK era renderizado em preto até que a v2.376.0 introduzisse o leitor tolerante HPDFReadNumericArrayUpTo. A lição transferível: qualquer chave PDF documentada como contendo uma matriz numérica de comprimento variável deve ser lida com um leitor do tipo preencha-o-que-existir, porque um buffer de tamanho fixo com correspondência estrita transforma arquivos válidos em zeros silenciosos

Como testar a renderização de cores especiais sem se enganar?

A pergunta desconfortável é por que a suíte de testes permaneceu verde durante tudo isso. A regressão original, SeparationRendersDistinguishable, afirmava apenas que o bitmap renderizado não era inteiramente preto. Um pipeline que reduzia toda cor especial a DeviceGray produzia saída em cinza e branco, o que não é preto, de modo que a afirmação passava enquanto toda a funcionalidade estava inoperante. Afirmações fracas do tipo "não em branco", "não totalmente preto" ou "resumo não é zero" não conseguem distinguir um renderizador funcional de um quebrado, porque quase qualquer modo de falha ainda produz alguns pixels

O estilo de asserção que realmente detecta essas falhas fixa a cor esperada: renderize um PDF mínimo feito à mão cuja tinta Separation seja resolvida para um tom conhecido, então conte os pixels que são dominantes nesse tom. A renderização em um bitmap para inspeção usa o mesmo ponto de entrada RenderLoadedPageToBitmap descrito no guia de renderização de página para bitmap

var
  Pdf: THotPDF;
  Bmp: TBitmap;
  X, Y, RedHits: Integer;
  Px: TColor;
begin
  Pdf := THotPDF.Create(nil);
  try
    if Pdf.LoadFromFile('spot-red-fixture.pdf', '') > 0 then
    begin
      Bmp := Pdf.RenderLoadedPageToBitmap(0, 96);
      try
        RedHits := 0;
        for Y := 0 to Bmp.Height - 1 do
          for X := 0 to Bmp.Width - 1 do
          begin
            Px := Bmp.Canvas.Pixels[X, Y];
            if (GetRValue(Px) > 180) and (GetGValue(Px) < 100) and
               (GetBValue(Px) < 100) then
              Inc(RedHits);
          end;
        // Lock the expected ink: demand a real area of red-dominant
        // pixels, never settle for "not all black".
        Assert(RedHits > 500);
      finally
        Bmp.Free;
      end;
    end;
  finally
    Pdf.Free;
  end;
end;

O fixture importa tanto quanto a asserção. Um PDF criado à mão com poucas centenas de bytes, com um único preenchimento Separation e nada mais, não deixa ambiguidades sobre qual é a saída esperada; um arquivo do mundo real exercita mais código, mas não pode dizer qual estágio falhou. Agora tratamos a contagem de pixels da cor esperada como a barra mínima para qualquer teste de fumaça de renderização, porque é o único estilo de asserção que expôs esses defeitos

A renderização de cores especiais e de espaços de cores CIE faz parte do pipeline de documentos carregados no HotPDF Component para Delphi e C++Builder, juntamente com o mecanismo de função, a renderização de sombreamento e os caminhos de exportação de bitmap mostrados acima