Um JPEG CMYK que sai de um PDF com cores turvas, deslocadas ou negativas quase sempre significa que o decodificador errou um de dois fatos: se as samples foram armazenadas como YCCK e precisam de conversão, ou se foram armazenadas invertidas pela convenção Adobe. Os decodificadores 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 marker Adobe APP14 dentro dos dados JPEG ter precedência sobre aquela entrada de dicionário, como a Tabela 13 da ISO 32000-1 exige. Duas regras vizinhas pertencem à mesma família de armadilhas: streams multi-filtro precisam carregar arrays /DecodeParms alinhados por etapa, e imagens JPEG 2000 mantêm um mapeamento /Decode explícito
Os três compartilham um tema. O PDF já diz o que significa, e a armadilha é um renderer que responde a uma pergunta ligeiramente diferente
Por que um JPEG CMYK renderiza invertido ou lavado num PDF?
Um JPEG CMYK dá errado num PDF por um de dois motivos, e eles parecem diferentes na tela. Uma imagem negativa, onde o papel branco vira preto e tons claros ficam escuros, aponta para armazenamento invertido. Uma imagem lavada ou com matiz deslocada, onde pele vira verde e pretos ficam cinza enquanto o detalhe do K continua nítido, aponta para uma conversão de cor aplicada quando não devia, ou pulada quando era exigida
O problema da transform vem de como o JPEG comprime cor. O JPEG baseline comprime melhor quando luminância é separada de crominância, então encoders normalmente convertem RGB para YCbCr antes de comprimir, e encoders de quatro canais convertem CMY para YCC passando o K intocado, que é o formato chamado YCCK. O decodificador precisa desfazer exatamente a conversão que o encoder aplicou. Desfaça uma que nunca aconteceu, ou pule uma que aconteceu, e todo pixel se move. O canal preto sobrevive de qualquer forma, e é por isso que uma imagem CMYK quebrada costuma manter texto preto nítido enquanto todo o resto deriva
O problema da inversão é histórico. O Photoshop e outras ferramentas Adobe gravam JPEGs CMYK com cada sample armazenada como 255 - value, e marcam o arquivo com um marker Adobe APP14. Nada no padrão JPEG diz isso; é uma convenção, e todo consumidor precisa conhecê-la. Num PDF, espera-se que o produtor expresse a inversão no dicionário da imagem em vez de deixar para o chute
O que o /ColorTransform diz ao decodificador?
O /ColorTransform é a declaração em nível PDF de qual conversão o decodificador precisa desfazer. A ISO 32000-1 §7.4.8, Tabela 13, define dois valores. Zero significa nenhuma transformação: o decodificador devolve as samples como comprimidas. Um significa que o encoder aplicou a conversão padrão, então uma imagem de três canais vai de YCbCr de volta a RGB e uma imagem de quatro canais vai de YCCK de volta a CMYK. O parâmetro é ignorado para imagens de um e dois canais. A Tabela 13 também estabelece uma regra de precedência fácil de perder: quando os dados codificados carregam um marker Adobe com o próprio código de transform, esse código governa e a entrada do dicionário é ignorada. Só um JPEG sem esse marker recua para o dicionário, e quando a entrada também está ausente, uma imagem de três canais tem como default a transform e uma de quatro canais tem como default nenhuma
O erro fácil é um caminho de imagem que aceita o dicionário de parâmetros mas nunca o entrega ao decodificador JPEG. Um JPEG RGB de um pixel segurando a cor (128, 63, 31) expõe isso de imediato: tal decodificador devolve [128, 63, 31] tanto para /ColorTransform 0 quanto para /ColorTransform 1, enquanto a resposta correta para zero é o trío YCbCr bruto [79, 101, 163]
O HotPDF aplica o parâmetro no nível de bytes. O HPDFDecodeHeadlessTerminalImage primeiro percorre a estrutura de segmentos JPEG até o marker start-of-scan e procura um segmento Adobe APP14; bytes que apenas se parecem com um dentro de um segmento de comentário não contam. Se os dados carregam o marker, o código de transform dele decide e o valor do dicionário é ignorado. Se não, o decodificador monta uma cópia privada do JPEG com um segmento APP14 inserido imediatamente antes do marker start-of-scan, carregando o código de transform 0, 1 ou, para uma imagem de quatro canais com valor 1, o código YCCK 2. Sempre que a transform efetiva de uma imagem de três canais é 0, a cópia também desarma o header JFIF, porque um header JFIF por si só diz ao libjpeg-turbo assumir YCbCr. O buffer do chamador nunca é tocado, e quando um segmento é inserido, os seus 16 bytes extras são debitados do orçamento de bytes de trabalho antes de o decodificador alocar qualquer coisa
Inserir só quando não existe marker importa por causa de um detalhe do libjpeg: ele usa o último segmento APP14 que vê. Insira um segmento depois de um marker existente e um valor explícito do dicionário sobrescreve silenciosamente um marker conflitante nos dados, que é exatamente o inverso da Tabela 13. O HotPDF aplica a ordem da Tabela 13 no caminho completo de image XObject e no caminho de imagem inline igualmente
Você pode chamar o mesmo decodificador terminal diretamente quando uma única imagem parece errada e você quer ver as samples dela 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 da imagem, que precisa bater com o frame header do JPEG
uses
SysUtils, HPDFObjs, HPDFHeadlessImageCodecs;
// ColorTransform < 0 significa "entrada ausente": deixe os markers JPEG decidirem
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 decodificador é 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 recuar para um default, porque um valor que a especificação não define não dá ao decodificador nada confiável para agir. A checagem continua valendo mesmo quando um marker 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 do mesmo jeito, com uma mensagem nomeando o orçamento de bytes de trabalho
Um null explícito é um caso diferente, porque não é um valor de fato. A ISO 32000-1 §7.3.9 diz que uma entrada de dicionário cujo valor é null equivale a uma entrada ausente, então /ColorTransform null recua para o marker ou para o default por contagem de componentes exatamente como se a chave estivesse faltando. O HotPDF trata assim em todos os caminhos, headless e VCL, stream e inline igualmente
Um último default merece menção, porque é onde o HotPDF deliberadamente vai além da letra da Tabela 13. Um JPEG de três componentes sem header JFIF e sem marker Adobe, cujo frame header rotula os componentes R, G e B, diz nos próprios bytes que contém RGB. A Tabela 13 ainda o colocaria na transform por default, mas o libjpeg e os leitores construídos sobre ele o decodificam sem transform, e o HotPDF também. Os decodificadores headless e VCL o tratam assim, então vermelho e azul ficam onde os dados os puseram. Um 0 ou 1 explícito no dicionário ainda sobrepõe essa inferência
uses
System.IOUtils;
var
Jpeg, Stored, Converted, ByMarkers: TBytes;
begin
// bytes DCTDecode brutos de uma imagem CMYK 640 x 480, salvos do stream do PDF
Jpeg := TFile.ReadAllBytes('suspect-image.jpg');
try
// Com um marker Adobe APP14 nos dados, as três chamadas concordam:
// o marker decide e o valor do dicionário é ignorado
Stored := DecodeDCT(Jpeg, 640, 480, 4, 0); // sem marker: samples como comprimidas
Converted := DecodeDCT(Jpeg, 640, 480, 4, 1); // sem marker: YCCK desfeito para CMYK
ByMarkers := DecodeDCT(Jpeg, 640, 480, 4, -1); // sem marker: 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 de (10, 20, 30, 40) armazenado 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 nos dois. Se o plano K parece certo e os outros três não, uma transform destoante é a primeira suspeita
A convenção CMYK invertida pertence ao /Decode
O decodificador terminal do HotPDF devolve samples CMYK exatamente como o libjpeg-turbo as produz e não as inverte com base num marker 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 encoders estilo Adobe armazenam. Transformar isso de volta em quantidades de tinta é trabalho do dicionário da imagem, onde um produtor que embute tal JPEG deve gravar /Decode [1 0 1 0 1 0 1 0]. O renderer aplica o array /Decode a cada componente, então um arquivo corretamente gravado imprime corretamente
O reverso vale ser dito com clareza. 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 arquivo descreve a imagem assim. Alguns viewers detectam o marker APP14 e compensam silenciosamente. O HotPDF não adiciona essa heurística, então a correção de tal arquivo pertence ao software que o gravou
Por que um dicionário DecodeParms único para dois filtros é um erro?
Um stream cujo /Filter é um array de várias etapas precisa fornecer /DecodeParms como um array do mesmo comprimento, um dicionário de parâmetros ou null por etapa. A ISO 32000-1 §7.3.8.2, Tabela 5, estabelece essa regra, e o HotPDF a faz cumprir: um stream multi-filtro que fornece um dicionário compartilhado único é reportado como documento inválido em vez de decodificado
A alternativa tolerante é pior do que parece. Um decodificador que confere o comprimento só quando /DecodeParms já é um array vai, diante de um dicionário único e dois filtros, reutilizar silenciosamente aquele 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 aplicado também à externa, e a saída vira lixo de aparência plausível em vez de um erro limpo. Como a intenção de tal arquivo não pode ser recuperada, o HotPDF o recusa, até no caso vazio, como um content stream de página com /Filter [/FlateDecode /FlateDecode] e /DecodeParms << >>
As formas aceitas e rejeitadas são fáceis de enumerar:
- Aceito:
/DecodeParmsomitido, ounull, para qualquer número de filtros - Aceito: um array cujo comprimento iguala a contagem de filtros, cada entrada um dicionário ou
null - Aceito: um dicionário único quando o
/Filternomeia exatamente um filtro - Rejeitado: um dicionário único compartilhado 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 checagem roda para content streams comuns e para streams de imagem cujo último filtro é um codec terminal como o DCTDecode, então uma cadeia malformada falha no mesmo lugar seja o que for que o stream carregue. A interação com limites de descompressão está coberta em o artigo sobre orçamentos de cadeia de filtros e bombas de decodificação PDF. 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 /Decode explícito
Uma imagem JPEG 2000 ignora o /Decode só quando o dicionário dela não tem /ColorSpace e ela não é image mask. A ISO 32000-1 §8.9.5 faz isso condicional, e o HotPDF segue. Descartar o /Decode para toda imagem JPX não stencil é o atalho fácil, e ele faz uma inversão explícita simplesmente desaparecer: um sample JPX DeviceGray de 64 sob /Decode [1 0] precisa renderizar como 191, não 64
Acertar essa única condição não basta por si só. Ramos de color space como Lab e ICCBased que definem faixas default de componentes sempre que a imagem é JPX sobrescreveriam uma segunda vez um array legitimamente aplicado, então o renderer registra se um array /Decode válido foi de fato aplicado, e preenche faixas default só quando nenhum foi. Faixas Lab e ICCBased explícitas, portanto, sobrevivem, e a declaração de cor do codestream ainda governa quando o dicionário está calado. O lado decodificador do formato está descrito em o artigo sobre JPEG 2000 e JPXDecode
Quais caminhos de decodificação seguem essas regras?
Os dois pipelines as seguem. No lado headless isso significa o carregamento do THPDFHeadlessDocument, o decodificador de streams compartilhado e o renderer de páginas baseado em Cairo por trá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 decodificação de imagem completa e de região reduzida igualmente, então um header JFIF não pode forçar YCbCr sobre uma transform Adobe de 0, e um valor ilegal torna a imagem indecodificável lá também. Imagens DCT inline seguem as mesmas regras de marker, null, default e rejeição que image XObjects, com nomes de chave abreviados e completos igualmente, incluindo um dicionário /DP << … >> e arrays de filtros abreviados como /F [/AHx /DCT], e uma imagem inline com parâmetros ilegais é pulada enquanto o resto da página renderiza. O guia de extração de imagens de um PDF carregado cobre o resto do tratamento de filtros dessa API
Algumas notas práticas antes de você confiar nesse comportamento:
- A decodificação JPEG precisa de uma biblioteca TurboJPEG compatível em tempo de execução;
HOTPDF_TURBOJPEG_LIBRARYsobrepõe oturbojpeg.dlloulibturbojpeg.so.0padrão - Checagens estruturais estritas rejeitam arquivos que leitores tolerantes abrem, notavelmente o dicionário
/DecodeParmsvazio compartilhado, e a correção para esses arquivos fica no lado do produtor
Para um fluxo de impressão com gestão de cor, a conclusão prática é curta. Grave JPEGs CMYK com um /ColorTransform explícito que concorde com qualquer marker Adobe nos dados mais o array /Decode que bata com o armazenamento deles, dê a cada stream multi-filtro um array /DecodeParms alinhado por etapa, e espere que o HotPDF cobre dos arquivos o que eles realmente dizem
O loader headless, os decodificadores de streams e o renderer de páginas Cairo mostrados aqui vêm com o componente HotPDF Delphi PDF para Delphi, C++Builder e Lazarus, ao lado do engine clássico de documentos VCL