Artigo Técnico

Converter um PDF em um Único Espaço de Cores em Delphi

Um título preenchido com 1 0 0 rg, vermelho puro, volta de uma conversão para cinza como 0.299 g. Azul puro pousa em 0.114 g, perto o suficiente do preto para que um logo azul-marinho e o texto do corpo fiquem difíceis de distinguir. Esses números não são um acidente de arredondamento: são os pesos de luminância do BT.601, os mesmos que um viewer aplica quando mostra uma página RGB num dispositivo de cinza, e eles decidem quanto contraste sobrevive quando um folheto colorido precisa virar um de cinza

O que o RecolorDocument computa para cada cor

O PDF Library for Delphi expõe a conversão como RecolorDocument(TargetSpace) no TPDFlib, com 1 para cinza, 2 para RGB e 3 para CMYK. Ele devolve o número de coisas que converteu, e geometria, posições de texto e posicionamento de imagens ficam onde estavam. Só os valores de cor e os nomes de espaço de cores em volta deles mudam

Toda cor passa pelo RGB a caminho do alvo. Cinza é a soma de luminância 0.299 R + 0.587 G + 0.114 B. CMYK chega ao RGB pela regra de dispositivo simples da ISO 32000-1 §10.3, em que cada canal é um menos a soma da tinta dele e do preto, clampeado a zero. RGB chega ao CMYK puxando o maior componente para o preto: K = 1 - max(R, G, B), e as tintas restantes são escaladas do que sobra. Alguns resultados que valem conhecer antes de mandar qualquer coisa para a impressora:

  • Um cinza de 0.4 g convertido para CMYK vira 0 0 0 0.6 k, só tinta preta, que é o que uma gráfica quer para texto e filetes
  • RGB 1 0.5 0 (laranja) vira 0 0.5 1 0 k, sem preto nenhum
  • Um preto rico como 0.6 0.4 0.4 1 k vira 0 g a caminho do cinza, então as tintas extras simplesmente desaparecem
  • Ciano puro, 1 0 0 0 k, vira 0.701 g, mais claro do que a maioria esperaria de uma tinta saturada

Operandos convertidos são escritos com no máximo três casas decimais e sempre com ponto como separador decimal, seja qual for o locale da máquina que roda o job. A ISO 32000-1 §7.3.3 não permite mais nada num content stream, e um conversor que formata números com o locale do sistema escreverá 0,5 g numa estação de trabalho alemã, o que um leitor interpreta como o número 0 seguido de um operador desconhecido

var
  Lib: TPDFlib;
  Converted: Integer;
begin
  Lib := TPDFlib.Create;
  try
    if Lib.LoadFromFile('brochure.pdf', '') <> 1 then
      raise Exception.Create('Could not open brochure.pdf');
    Converted := Lib.RecolorDocument(1);   // 1 = gray, 2 = RGB, 3 = CMYK
    WriteLn(Format('%d colors, images and gradients converted', [Converted]));
    Lib.SaveToFile('brochure-gray.pdf');
  finally
    Lib.Free;
  end;
end;

Uma página cujas cores já estão no espaço alvo mantém os bytes de conteúdo originais dela. O RecolorDocument conta só conversões que de fato cruzam de um espaço para outro, então um documento que já é cinza reporta 0 e nada nele é reescrito. Num arquivo grande isso importa: um diff do input e do output mostra exatamente as páginas que mudaram

Por que o RecolorPage deixa a foto da capa em cor?

O RecolorPage converte os operadores de cor da página selecionada e para aí. As imagens e os gradientes são objetos de nível de documento, frequentemente compartilhados por várias páginas, e só o RecolorDocument os converte. Então o jeito óbvio de converter só uma capa, selecionar a página 1 e chamar RecolorPage(3), te dá títulos e filetes em CMYK sobre uma fotografia RGB

Essa divisão é deliberada. Um image XObject desenhado na capa também pode ser desenhado na página 12, e convertê-lo para uma página o converte para as duas. Uma chamada de escopo de página que alcançasse silenciosamente recursos compartilhados mudaria páginas que o chamador nunca selecionou. Essa é a pior das duas surpresas, então a chamada por página toca só o que pertence à página

// Operadores na capa apenas; imagens e gradientes não são convertidos aqui
Lib.SelectPage(1);
Changed := Lib.RecolorPage(3);
if Changed = 0 then
  WriteLn('Cover already uses CMYK operators (or the target was out of range)');
Lib.SaveToFile('cover-cmyk-operators.pdf');

// Para um documento que precisa ser de um espaço só, converta tudo
Lib.RecolorDocument(3);
Lib.SaveToFile('all-cmyk.pdf');

Olhe o valor de retorno naquele trecho. As duas chamadas devolvem 0 para um alvo fora de 1 a 3, e 0 também é a resposta de uma página que não precisava de mudança, então um erro de digitação no alvo parece exatamente um documento que já estava correto. Valide o argumento você mesmo antes da chamada. O RecolorDocument restaura a página que estava selecionada antes de rodar, o que significa que você pode chamá-lo no meio de um job página a página sem reselecionar depois

Mais um detalhe de escopo. A passada de operadores lê o content stream próprio de cada página, as instruções de desenho listadas na própria página, e converte g, rg, k, as formas de traço delas, e sc / scn quando o espaço atual é um espaço de dispositivo. Quando um /DeviceRGB cs precede um sc convertido, o nome do espaço também é reescrito para o alvo. Pular esse rename num conversor escrito à mão deixa um leitor procurando três componentes RGB onde agora há um valor de cinza

Imagens e paletas: quais mudam, quais mantêm os bytes

Imagens importam tanto quanto operadores. Um folheto convertido para cinza com só os operadores tratados ainda mostra fotografias coloridas em páginas cinza, o que fica pior que o original ou que uma conversão completa. O RecolorDocument converte uma imagem quando ela tem oito bits por componente, um espaço de cores de dispositivo, e ou nenhuma compressão ou Flate simples sem parâmetros de decode

Imagens indexadas tomam um caminho diferente. As amostras delas são índices numa tabela de cores, então a tabela é convertida e cada amostra mantém o índice que já tinha. Um screenshot ou gráfico salvo como imagem de paleta de 256 cores é convertido assim independente de como as amostras dele estão empacotadas ou comprimidas, porque as amostras nunca são decodificadas. A base da paleta precisa ser um espaço de dispositivo; uma paleta sobre um perfil ICC mantém a tabela dela

Diagrama mostrando quais partes de um PDF o RecolorDocument reescreve no Delphi e quais partes, como imagens JPEG e soft masks, passam idênticas byte a byte
Uma passada de RecolorDocument reescreve operadores, imagens elegíveis, paletas indexadas e pares de gradiente, enquanto bytes JPEG, máscaras e cores vinculadas a perfis seguem intactos

Uma imagem JPEG ou codificada por fax mantém os bytes dela. Mudar as cores dela significaria decodificar, converter e recomprimir, o que adiciona uma segunda geração de artefatos JPEG e muda o tamanho do arquivo de jeitos que ninguém pediu. Se um job realmente precisa de toda foto em cinza, a recodificação é uma decisão separada com configuração de qualidade própria, e preferimos que o chamador a faça explicitamente em vez de uma conversão de cores fazê-la por ele

Soft masks e stencil masks ficam como estão por um motivo diferente. Uma soft mask costuma ser uma imagem DeviceGray, mas os valores dela significam cobertura. Converter uma para RGB de três componentes faz um viewer parar de tratá-la como transparência; ela acaba pintada como arte em cinza por cima do conteúdo que deveria moldar. O RecolorDocument coleta todo objeto referenciado através de /SMask ou /Mask antes de converter qualquer imagem, e uma imagem com /ImageMask true não tem cor nenhuma para converter em primeiro lugar

Cores definidas através de espaços ICCBased, Separation, Indexed ou Pattern no nível de operador também ficam como escritas. Transformar uma spot color ou um valor marcado com perfil em cinza de dispositivo sem ler o perfil mudaria o jeito como a página aparece, e uma passada cujo trabalho é manter a aparência não pode começar a adivinhar o que um perfil queria dizer

Gradientes: o espaço de cores e as paradas se movem juntos

Um shading declara o espaço de cores dele uma vez e entrega cada parada a uma função. Renomear /ColorSpace de DeviceRGB para DeviceGray enquanto a função ainda devolve três números faz um leitor pegar o primeiro deles, o canal vermelho, como nível de cinza. Um gradiente de azul para branco então renderiza como uma faixa que começa no preto. O RecolorDocument caminha pela árvore de funções inteira em modo somente leitura primeiro e não converte nada a menos que todo nó nela possa ser convertido; só então ele reescreve as paradas e renomeia o espaço

As funções que ele reescreve são as do tipo exponencial, cujas duas cores finais /C0 e /C1 são arrays simples, e stitching functions construídas a partir delas. Uma cor /Background é convertida com o resto. Um shading definido por uma tabela amostrada ou um programa PostScript mantém a definição dele, assim como os tipos mesh de 4 a 7, que guardam cores dentro de um stream em vez de numa função. Reescrever qualquer um deles é regeneração, um trabalho diferente de converter uma cor

Diagrama do PDF Library for Delphi da conversão de gradientes pareando um espaço de cores de shading com sua função de stops por objetos independentes, patterns e recursos de shading de página, enquanto shadings sampled, PostScript e mesh mantêm sua própria definição
Converter um gradiente significa mover seu espaço de cores e as paradas atrás dele juntos, onde quer que o shading viva, e shadings de mesh ou de função amostrada mantêm a própria definição

Encontrar os gradientes é uma armadilha à parte. Ferramentas de design raramente dão a um shading um objeto próprio; ele costuma ficar inline dentro do pattern dictionary que o pinta, ou dentro dos recursos /Shading de uma página. Quando construímos a passada de gradientes, uma varredura dos objetos de topo quase não os encontrou. A busca agora segue o grafo de objetos, limitada a doze níveis de profundidade e visitando cada número de objeto uma vez, então um recurso compartilhado por todas as páginas é convertido uma vez e um arquivo que aponta um objeto para ele mesmo não entra em loop

Onde a recolorização se encaixa antes de uma submissão para impressão

Trate o RecolorDocument como normalização. Ele converte valores entre espaços de dispositivo; ele não aplica um perfil ICC, honra um rendering intent nem simula uma condição de impressão. Para um job que precisa casar com uma prova contratual, um fluxo orientado por perfil é a ferramenta certa, e uma passada de recolorização é no máximo o passo que põe tudo num espaço antes de esse fluxo rodar

Onde ele ganha seu lugar é no caso comum de produção: uma submissão que precisa ser cinza e chega com um logo RGB, um relatório interno que não pode imprimir em cor, um lote que um passo de preflight vai rejeitar até todo arquivo usar um espaço. Qualquer coisa que sai do prédio deve passar pelo RecolorDocument, nunca pelo RecolorPage sozinho, porque um arquivo meio convertido passa numa checagem visual rápida e depois falha na gráfica. Rode o veredito no mesmo job: as notas sobre preflight de PDF/A e PDF/UA e sobre a workbench de conformidade e assinatura mostram como

// IsValidRecolorTarget é um helper que você adiciona; a biblioteca não tem tal chamada
function IsValidRecolorTarget(Space: Integer): Boolean;
begin
  Result := (Space >= 1) and (Space <= 3);
end;

procedure NormalizeFile(const Src, Dst: string; Space: Integer);
var
  Lib: TPDFlib;
begin
  if not IsValidRecolorTarget(Space) then
    raise EArgumentException.CreateFmt('Bad recolor target %d', [Space]);
  Lib := TPDFlib.Create;
  try
    if Lib.LoadFromFile(Src, '') <> 1 then
      raise Exception.CreateFmt('Could not open %s', [Src]);
    if Lib.RecolorDocument(Space) = 0 then
      WriteLn(Src, ': already in the target space, copied unchanged');
    Lib.SaveToFile(Dst);
  finally
    Lib.Free;
  end;
end;

Uma checagem rápida que você pode rodar no output: abra o conteúdo de uma página convertida em qualquer inspetor de PDF depois de uma conversão para cinza. O conteúdo próprio da página não deve mais conter operador rg, RG, k ou K nenhum. Cor que sobrevive à passada vem dos casos acima: um JPEG, uma cor marcada com perfil ou spot, um gradiente amostrado ou mesh, ou arte desenhada dentro de um form XObject, cujas instruções vivem num stream próprio em vez de no conteúdo da página

Se você também constrói o arte vetorial em vez de só convertê-lo, o passo a passo de gráficos vetoriais, shaders e gradientes mostra como shadings e patterns são criados em primeiro lugar, o que torna as regras de gradiente acima mais fáceis de prever

A passada de cores roda in-process: o PDF Library for Delphi é Pascal nativo para Delphi, C++Builder e Lazarus, então normalizar um lote de arquivos que chegam não precisa de conversor externo nem de ferramenta de linha de comando no servidor. A página do produto PDF Library for Delphi lista o resto da API de processamento de documentos dele