Um JPEG CMYK que sai de um PDF com cores enlameadas, deslocadas ou negativas quase sempre significa que o descodificador errou num de dois factos: se as amostras foram guardadas como YCCK e precisam de conversão, ou se foram guardadas invertidas pela convenção Adobe. Os descodificadores do HotPDF honram o parâmetro /ColorTransform do DCTDecode para imagens de três e quatro canais, rejeitam valores que a especificação não define, e deixam um marcador Adobe APP14 dentro dos dados JPEG prevalecer sobre essa entrada do dicionário, como o ISO 32000-1 Tabela 13 exige. Duas regras vizinhas pertencem à mesma família de armadilhas: streams multi-filtro têm de trazer arrays /DecodeParms alinhados por etapa, e imagens JPEG 2000 mantêm um mapeamento /Decode explícito
Os três partilham um tema. O PDF já diz o que quer dizer, e a armadilha é um renderer que responde a uma pergunta ligeiramente diferente
Porque é que um JPEG CMYK rende invertido ou desbotado num PDF?
Um JPEG CMYK corre mal num PDF por uma de duas razões, e parecem diferentes no ecrã. Uma imagem negativa, em que o papel branco fica preto e os tons claros escurecem, aponta para armazenamento invertido. Uma imagem desbotada ou com matizes deslocados, em que a pele fica verde e os pretos ficam cinza enquanto o detalhe do K se mantém nítido, aponta para uma transformação de cor aplicada quando não devia, ou saltada quando era exigida
O problema da transformação vem de como o JPEG comprime cor. O JPEG baseline comprime melhor quando a luminância está separada da crominância, por isso os codificadores normalmente convertem RGB em YCbCr antes de comprimir, e os codificadores de quatro canais convertem CMY em YCC deixando o K passar intocado, que é o formato chamado YCCK. O descodificador tem de desfazer exatamente a conversão que o codificador aplicou. Desfaça uma que nunca aconteceu, ou salte uma que aconteceu, e cada pixel se move. O canal preto sobrevive de qualquer forma, e é por isso que uma imagem CMYK partida muitas vezes mantém texto preto nítido enquanto todo o resto deriva
O problema da inversão é histórico. O Photoshop e outras ferramentas Adobe escrevem JPEGs CMYK com cada amostra guardada como 255 - value, e assinalam o ficheiro com um marcador Adobe APP14. Nada no standard JPEG o diz; é uma convenção, e todos os consumidores têm de a conhecer. Num PDF, espera-se que o produtor expresse a inversão no dicionário de imagem em vez de deixar ao palpite
O que diz o /ColorTransform ao descodificador?
O /ColorTransform é a declaração ao nível do PDF de que conversão o descodificador tem de desfazer. O ISO 32000-1 §7.4.8, Tabela 13, define dois valores. Zero significa nenhuma transformação: o descodificador devolve as amostras como comprimidas. Um significa que o codificador aplicou a conversão standard, por isso uma imagem de três canais volta do YCbCr para RGB e uma de quatro canais volta do YCCK para CMYK. O parâmetro é ignorado para imagens de um e dois canais. A Tabela 13 também fixa uma regra de precedência fácil de perder: quando os dados codificados trazem um marcador Adobe com o seu próprio código de transformação, esse código governa e a entrada do dicionário é ignorada. Só um JPEG sem esse marcador recai no dicionário, e quando a entrada também está ausente, uma imagem de três canais assume por omissão a transformação e uma de quatro canais assume nenhuma
O erro fácil é um caminho de imagem que aceita o dicionário de parâmetros mas nunca o entrega ao descodificador JPEG. Um JPEG RGB de um pixel com a cor (128, 63, 31) expõe-o de imediato: tal descodificador devolve [128, 63, 31] tanto para /ColorTransform 0 como para /ColorTransform 1, enquanto a resposta correta para zero é o trio YCbCr cru [79, 101, 163]
O HotPDF aplica o parâmetro ao nível do byte. O HPDFDecodeHeadlessTerminalImage primeiro percorre a estrutura de segmentos JPEG até ao marcador start-of-scan e procura um segmento Adobe APP14; bytes que meramente se pareçam com um dentro de um segmento de comentário não contam. Se os dados trazem o marcador, o seu código de transformação decide e o valor do dicionário é ignorado. Se não trazem, o descodificador constrói uma cópia privada do JPEG com um segmento APP14 inserido imediatamente antes do marcador start-of-scan, trazendo o código de transformação 0, 1 ou, para uma imagem de quatro canais com valor 1, o código YCCK 2. Sempre que a transformação efetiva de uma imagem de três canais é 0, a cópia também desarma o cabeçalho JFIF, porque um cabeçalho JFIF por si só diz ao libjpeg-turbo que assuma YCbCr. O buffer do chamador nunca é tocado, e quando um segmento é inserido, os seus 16 bytes extra são debitados no orçamento de bytes de trabalho antes de o descodificador alocar qualquer coisa
Inserir só quando não existe marcador importa por causa de um detalhe do libjpeg: ele usa o último segmento APP14 que vê. Insira um segmento depois de um marcador existente e um valor explícito do dicionário sobrepõe-se silenciosamente a um marcador conflitante nos dados, que é exatamente o contrário da Tabela 13. O HotPDF aplica a ordem da Tabela 13 tanto no caminho do XObject de imagem completo como no caminho de imagem inline
Pode chamar o mesmo descodificador terminal diretamente quando uma única imagem parece errada e quer ver as suas amostras sem renderizar a página. A função vive na unit HPDFHeadlessImageCodecs, recebe o nome do filtro, os bytes codificados, um dicionário de parâmetros opcional e a geometria do dicionário de imagem, que tem de casar com o cabeçalho de frame do JPEG
uses
SysUtils, HPDFObjs, HPDFHeadlessImageCodecs;
// ColorTransform < 0 significa "entrada ausente": deixar os marcadores JPEG decidir
function DecodeDCT(const Encoded: TBytes; Width, Height, Channels,
ColorTransform: Integer): TBytes;
var
Parms: THPDFDictionaryObject;
begin
Parms := nil;
if ColorTransform >= 0 then
begin
Parms := THPDFDictionaryObject.Create(nil);
Parms.AddNumericValue('ColorTransform', ColorTransform);
end;
try
Result := HPDFDecodeHeadlessTerminalImage('DCTDecode', Encoded, nil, Parms,
Width, Height, Channels, 8, 64 * 1024 * 1024, nil);
finally
Parms.Free;
end;
end;
O descodificador é estrito quanto ao que conta como valor legal. Um -1 explícito, um 2, ou um não inteiro como 0.5 levanta EHPDFHeadlessImageError em vez de recair num default, porque um valor que a especificação não define não dá ao descodificador nada de confiança em que agir. A verificação mantém-se mesmo quando um marcador Adobe nos dados torna a entrada irrelevante, porque um valor indefinido é um erro estrutural no próprio documento. Um orçamento pequeno demais para a cópia transformada falha da mesma forma, com uma mensagem a nomear o orçamento de bytes de trabalho
Um null explícito é um caso diferente, porque não é um valor de todo. O ISO 32000-1 §7.3.9 diz que uma entrada de dicionário cujo valor é null equivale a uma entrada ausente, por isso o /ColorTransform null cai no marcador ou no default pela contagem de componentes exatamente como se a chave faltasse. O HotPDF trata-o assim em todos os caminhos, headless e VCL, stream e inline por igual
Mais um default merece menção, porque é onde o HotPDF deliberadamente vai além da letra da Tabela 13. Um JPEG de três componentes sem cabeçalho JFIF nem marcador Adobe, cujo cabeçalho de frame rotula os seus componentes R, G e B, diz nos seus próprios bytes que guarda RGB. A Tabela 13 ainda o deixaria no default da transformação, mas o libjpeg e os leitores construídos sobre ele descodificam-no sem transformação, e o HotPDF também. Os descodificadores headless e VCL ambos o tratam assim, por isso o vermelho e o azul ficam onde os dados os puseram. Um 0 ou 1 explícito no dicionário continua a sobrepor-se a esta inferência
uses
System.IOUtils;
var
Jpeg, Stored, Converted, ByMarkers: TBytes;
begin
// bytes DCTDecode crus de uma imagem CMYK 640 x 480, guardados do stream do PDF
Jpeg := TFile.ReadAllBytes('suspect-image.jpg');
try
// Com um marcador Adobe APP14 nos dados, as três chamadas concordam:
// o marcador decide e o valor do dicionário é ignorado
Stored := DecodeDCT(Jpeg, 640, 480, 4, 0); // sem marcador: amostras como comprimidas
Converted := DecodeDCT(Jpeg, 640, 480, 4, 1); // sem marcador: YCCK desfeito em CMYK
ByMarkers := DecodeDCT(Jpeg, 640, 480, 4, -1); // sem marcador: default de quatro canais, nenhuma
Writeln(Format('pixel 0: stored %d,%d,%d,%d converted %d,%d,%d,%d',
[Stored[0], Stored[1], Stored[2], Stored[3],
Converted[0], Converted[1], Converted[2], Converted[3]]));
except
on E: EHPDFHeadlessImageError do
Writeln('Decoder refused the image: ', E.Message);
end;
end;
Um pixel de referência de quatro canais mostra o que procurar. Um pixel CMYK (10, 20, 30, 40) guardado como YCCK volta como [245, 235, 225, 215] quando a conversão YCCK não é desfeita e como [0, 116, 0, 215] quando é. O byte K, 215, é idêntico em ambos. Se o plano K parece certo e os outros três não, uma transformação desalinhada é o primeiro suspeito
A convenção CMYK invertida pertence ao /Decode
O descodificador terminal do HotPDF devolve amostras CMYK exatamente como o libjpeg-turbo as produz e não as inverte com base num marcador Adobe. Os números acima tornam a convenção visível: 245, 235, 225, 215 são simplesmente 255 menos 10, 20, 30, 40, a forma invertida que codificadores estilo Adobe guardam. Converter isso de volta em quantidades de tinta é trabalho do dicionário de imagem, onde um produtor que embute tal JPEG deve escrever /Decode [1 0 1 0 1 0 1 0]. O renderer aplica o array /Decode a cada componente, por isso um ficheiro corretamente escrito imprime corretamente
O outro lado vale a pena enunciar claramente. Um PDF que embute um JPEG CMYK invertido estilo Adobe sem o array /Decode correspondente vai renderizar como negativo no HotPDF, porque o próprio ficheiro descreve a imagem assim. Alguns viewers detetam o marcador APP14 e compensam silenciosamente. O HotPDF não acrescenta essa heurística, por isso a correção para tal ficheiro pertence ao software que o escreveu
Porque é que um dicionário DecodeParms único para dois filtros é um erro?
Um stream cujo /Filter é um array de várias etapas tem de fornecer /DecodeParms como um array do mesmo comprimento, um dicionário de parâmetros ou null por etapa. O ISO 32000-1 §7.3.8.2, Tabela 5, enuncia essa regra, e o HotPDF impõe-na: um stream multi-filtro que forneça um dicionário único partilhado é reportado como documento inválido em vez de descodificado
A alternativa tolerante é pior do que parece. Um descodificador que só verifica o comprimento quando /DecodeParms já é um array vai, perante um dicionário único e dois filtros, reutilizar silenciosamente esse dicionário em cada etapa. Isso é inofensivo quando o dicionário está vazio e destrutivo quando não: um /Predictor 12 destinado à etapa Flate interna seria também aplicado à externa, e a saída torna-se lixo de aparência plausível em vez de um erro limpo. Como a intenção de tal ficheiro não pode ser recuperada, o HotPDF recusa-o, mesmo no caso vazio, como um stream de conteúdo de página com /Filter [/FlateDecode /FlateDecode] e /DecodeParms << >>
As formas aceites e rejeitadas são fáceis de enumerar:
- Aceite:
/DecodeParmsomitido, ounull, para qualquer número de filtros - Aceite: um array cujo comprimento seja igual à contagem de filtros, cada entrada um dicionário ou
null - Aceite: um dicionário único quando o
/Filternomeia exatamente um filtro - Rejeitado: um dicionário único partilhado por dois ou mais filtros, com o diagnóstico "Stream filter chain requires array DecodeParms"
- Rejeitado: um array cujo comprimento difere da contagem de filtros, com "DecodeParms array must match the filter chain"
A mesma verificação corre para streams de conteúdo comuns e para streams de imagem cujo último filtro é um codec terminal como o DCTDecode, por isso uma cadeia malformada falha no mesmo lugar seja o que for que o stream traga. A interação com os limites de descompressão está coberta em o artigo sobre orçamentos de cadeias de filtros e bombas de descodificação PDF. A partir do código da aplicação, o erro aparece quando a página é renderizada
uses
SysUtils, HPDFHeadlessDocument, HPDFHeadlessCairoDevice;
var
Document: THPDFHeadlessDocument;
Options: THPDFHeadlessRenderOptions;
Info: THPDFHeadlessRenderResult;
begin
Document := THPDFHeadlessDocument.Create;
try
try
Document.LoadFromFile('catalog-cmyk.pdf');
Options := THPDFHeadlessRenderOptions.Default;
Info := HPDFRenderHeadlessPagePNGFile(Document.GetRenderAccess(0),
'page1.png', Options);
Writeln('images=', Info.Images, ' decoded bytes=', Info.DecodedBytes);
except
on E: Exception do
Writeln('Rejected: ', E.ClassName, ': ', E.Message);
end;
finally
Document.Free;
end;
end;
Imagens JPEG 2000 mantêm o seu /Decode explícito
Uma imagem JPEG 2000 ignora o /Decode só quando o seu dicionário não tem /ColorSpace e não é máscara de imagem. O ISO 32000-1 §8.9.5 torna isso condicional, e o HotPDF segue-o. Descartar o /Decode para toda a imagem JPX não stencil é o atalho fácil, e faz uma inversão explícita simplesmente evaporar: uma amostra JPX DeviceGray de 64 sob /Decode [1 0] tem de renderizar como 191, não 64
Acertar nessa única condição não chega por si. Ramos de espaço de cor como Lab e ICCBased que definem intervalos de componentes por omissão sempre que a imagem é JPX sobrescreveriam uma segunda vez um array legitimamente aplicado, por isso o renderer registra se um array /Decode válido foi realmente aplicado, e só preenche intervalos por omissão quando nenhum foi. Intervalos Lab e ICCBased explícitos sobrevivem portanto, e a declaração de cor do codestream continua a governar quando o dicionário cala. O lado de descodificação do formato está descrito em o artigo JPEG 2000 e JPXDecode
Que caminhos de descodificação seguem estas regras?
Ambos os pipelines as seguem. No lado headless isso significa o carregamento THPDFHeadlessDocument, o descodificador de streams partilhado e o renderer de páginas com base em Cairo atrás do HPDFRenderHeadlessPagePNGFile. No lado VCL significa extração de imagem carregada para um TBitmap e renderização de páginas VCL, que aplicam a mesma ordem da Tabela 13 na descodificação de imagem completa e de região reduzida por igual, por isso um cabeçalho JFIF não consegue forçar YCbCr sobre uma transformação Adobe de 0, e um valor ilegal torna a imagem indescodificável também aí. Imagens DCT inline seguem as mesmas regras de marcador, null, default e rejeição que os XObjects de imagem, com nomes de chave abreviados e completos por igual, incluindo um dicionário /DP << … >> e arrays de filtros abreviados como /F [/AHx /DCT], e uma imagem inline com parâmetros ilegais é saltada enquanto o resto da página rende. O guia para extrair imagens de um PDF carregado cobre o resto do tratamento de filtros dessa API
Algumas notas práticas antes de confiar neste comportamento:
- A descodificação JPEG precisa de uma biblioteca TurboJPEG compatível em tempo de execução;
HOTPDF_TURBOJPEG_LIBRARYsobrepõe-se aoturbojpeg.dlloulibturbojpeg.so.0por omissão - As verificações estruturais estritas rejeitam ficheiros que leitores tolerantes abrem, notoriamente o dicionário
/DecodeParmsvazio partilhado, e a correção desses ficheiros fica no lado do produtor
Para um fluxo de impressão com gestão de cor, a conclusão prática é curta. Escreva JPEGs CMYK com um /ColorTransform explícito que concorde com qualquer marcador Adobe nos dados mais o array /Decode que case com o seu armazenamento, dê a cada stream multi-filtro um array /DecodeParms alinhado por etapa, e conte com o HotPDF para cobrar dos ficheiros aquilo que realmente dizem
O loader headless, os descodificadores de streams e o renderer de páginas Cairo mostrados aqui viajam com o HotPDF Delphi PDF component para Delphi, C++Builder e Lazarus, ao lado do motor clássico de documentos VCL