Artigo Técnico

Adicionar Imagens JPEG 2000 a PDF em Delphi com o HotPDF

Uma lâmina médica digitalizada, um mosaico de levantamento aéreo, um fotograma de filme arquivado com toda a gama dinâmica. São estas as imagens que chegam em JPEG 2000, e chegam assim por uma razão. O formato guarda 12 ou 16 bits por canal, comprime com uma transformada wavelet em vez do DCT por blocos que o JPEG usa, e consegue codificar a mesma imagem com ou sem perdas a partir de um único fluxo de código. Quando um documento construído a partir dessas origens tem de se tornar um PDF, a imagem tem de atravessar um filtro que a especificação PDF reserva exatamente para este codec

O HotPDF v2.228.0 repôs um motor de descodificação JPEG 2000 funcional para esse caminho. Uma versão anterior tinha distribuído a unit com funções de esboço que devolviam nil, pelo que a API existia mas não descodificava nada. O motor atual liga o OpenJPEG 2.5.4 de forma estática e transforma uma origem JP2 ou J2K em píxeis que o HotPDF pode colocar numa página

O filtro JPXDecode no PDF

A ISO 32000-1 define o filtro JPXDecode no §7.4.9. Um XObject de imagem PDF nomeia a sua compressão na entrada /Filter do dicionário do fluxo, e JPXDecode é o valor que indica que os dados do fluxo são um fluxo de código JPEG 2000 e não o JPEG de base que o /DCTDecode transporta. É este filtro que permite a um PDF conter dados de imagem comprimidos por wavelet com elevada profundidade de bits, e admite tanto o modo com perdas como o modo sem perdas do codec, porque o modo é uma propriedade do próprio fluxo de código e não do invólucro que o rodeia

Este último ponto é o que vale a pena reter. O JPEG 2000 é um único algoritmo com um caso especial sem perdas, não dois formatos distintos. A wavelet reversível 5/3 reconstrói exatamente as amostras originais; a wavelet irreversível 9/7 troca essa exatidão por um ficheiro mais pequeno. Um descodificador trata as duas da mesma forma no momento da leitura, e é por isso que o HotPDF só precisa de um caminho de descodificação para aceitar o que quer que um fluxo JPXDecode lhe atire

O que o descodificador faz aos píxeis

No caso corrente, os XObjects de imagem PDF esperam 8 bits por componente em DeviceGray ou DeviceRGB. O JPEG 2000 excede isso com frequência, e o seu modelo de componentes é mais geral do que um raster empacotado, pelo que o descodificador tem três tarefas a cumprir antes de os dados serem utilizáveis como uma imagem normal

Primeiro, os componentes de elevada profundidade de bits são reamostrados para 8 bits. Uma amostra de 12 ou 16 bits é escalada para o intervalo de 0 a 255, para que o resultado seja um raster comum de 8 bits. Os componentes com sinal são primeiro deslocados para o intervalo sem sinal. O detalhe importa porque é, em si mesmo, uma perda: uma digitalização em tons de cinzento de 16 bits perde a sua gama tonal profunda no momento em que se torna uma imagem PDF de 8 bits, o que é o compromisso certo para saída em ecrã e em impressão, mas não para voltar a arquivar

Segundo, um espaço de cor YCbCr (o codec chama-lhe SYCC) é convertido para RGB. O JPEG 2000 guarda muitas vezes a cor num espaço de luminância e crominância por eficiência de compressão, a mesma ideia que o JPEG de base usa, e o descodificador aplica a transformada inversa padrão para que a página receba RGB verdadeiro

Terceiro, os componentes subamostrados são sobreamostrados por replicação do vizinho mais próximo. Os canais de crominância são frequentemente guardados a metade da resolução, pelo que o descodificador lê cada componente com as suas próprias dimensões e o seu próprio fator de amostragem, e depois replica amostras para levar todos os canais ao tamanho total da imagem antes de os intercalar. O vizinho mais próximo mantém o passo barato; a crominância que está a preencher já era de baixa frequência à partida, pelo que o custo visível é pequeno

Diagrama do pipeline de descodificacao JPEG 2000 em Delphi: uma origem JP2 ou J2K com amostras de 12 ou 16 bits, cor SYCC e crominancia subamostrada e reamostrada para 8 bits, convertida para RGB e com crominancia sobreamostrada num raster intercalado pronto para XObjects DeviceGray ou DeviceRGB
Seja qual for o conteúdo de um fluxo JPXDecode, o descodificador acaba num raster intercalado comum de 8 bits adequado a XObjects DeviceGray ou DeviceRGB

Caixas JP2 face a um fluxo de código J2K em bruto

Um ficheiro JPEG 2000 aparece em duas formas, e o HotPDF deteta qual delas está a ler a partir dos primeiros bytes e não da extensão do ficheiro. Um ficheiro JP2 é um contentor estruturado em caixas: abre com a caixa de assinatura de doze bytes 00 00 00 0C 6A 50 20 20 e envolve o fluxo de código a par de caixas que descrevem espaço de cor, resolução e metadados. Um fluxo de código J2K em bruto não transporta contentor nenhum e começa com o marcador SOC FF 4F FF 51. O descodificador lê esses bytes iniciais, reconhece a assinatura e seleciona o codec OpenJPEG correspondente a cada caso

As duas formas são suportadas porque ambas ocorrem no mundo real. Os dispositivos de captura e os arquivos que precisam dos metadados laterais emitem JP2; as ferramentas que querem a carga mais pequena possível emitem o fluxo de código nu. O tipo de formato é modelado como um enum, TJpeg2000FileType, com os membros jtInvalid, jtJP2, jtJ2K e jtJPT. O membro JPT nomeia a variante de streaming JPIP; o detetor de assinatura por bytes resolve as duas formas que consegue descodificar, JP2 e J2K, e reporta qualquer outra como jtInvalid, para que uma entrada não suportada falhe de forma limpa em vez de produzir lixo

Diagrama do HotPDF sobre a detecao do tipo de ficheiro JPEG 2000 a partir dos primeiros bytes: um contentor JP2 em caixas que abre com a assinatura 00 00 00 0C 6A 50 20 20 e um fluxo de codigo J2K em bruto que abre com o marcador SOC FF 4F FF 51 alimentam uma verificacao dos primeiros bytes que devolve jtJP2, jtJ2K ou jtInvalid para entradas nao suportadas
O HotPDF identifica o contentor apenas pelos primeiros bytes e mapeia uma assinatura não suportada para jtInvalid, de modo que a descodificação falhe de forma limpa
uses
  HPDFJpeg2000;

var
  Decoder: THPDFJpeg2000Decoder;
  Pixels: TJpeg2000ByteArray;
begin
  Decoder := THPDFJpeg2000Decoder.Create;
  try
    if Decoder.LoadFromStream(Input) then          // JP2 ou J2K, detetado automaticamente
      if Decoder.GetImageData(Pixels) then
        // Pixels é intercalado de 8 bits, com ColorComponents canais de largura,
        // por linhas de cima para baixo: pronto para um XObject DeviceGray/DeviceRGB.
        ProcessRaster(Decoder.Width, Decoder.Height,
                      Decoder.ColorComponents, Pixels);
  finally
    Decoder.Free;
  end;
end;

Com e sem perdas do lado da codificação

O descodificador lê os dois modos sem que lhe digam qual é. A escolha só se torna um parâmetro quando se segue no sentido contrário e se produz um ficheiro JPEG 2000, coisa que o HotPDF também sabe fazer através da classe TJpeg2000Bitmap, um descendente de TBitmap que carrega e grava dados raster como JP2. Duas propriedades governam a saída. LosslessCompression é um booleano que seleciona a wavelet reversível quando verdadeiro; CompressionQuality é um TJpeg2000QualityRange, um inteiro de 1 a 100 em que 1 é pequeno e feio e 100 é grande e fiel. Os valores por omissão vivem em constantes nomeadas: Jpeg2000DefaultLosslessCompression é False e Jpeg2000DefaultLossyQuality é 80

A decisão é uma decisão de conteúdo. Sem perdas serve uma cópia mestra, uma digitalização médica ou jurídica, tudo o que possa vir a ser recodificado mais tarde e não deva acumular perdas de geração. Com perdas a qualidade 80 serve uma imagem destinada a ecrã ou impressão, onde a degradação suave da wavelet dá um ficheiro sensivelmente mais pequeno sem artefactos que um leitor consiga notar. Há uma ressalva CMYK a assinalar: o bitmap expõe SetCMYK para marcar dados de quatro canais como CMYK em vez de RGBA, o que conta para fluxos de impressão que mantêm as separações intactas

uses
  HPDFJpeg2000;

var
  Bmp: TJpeg2000Bitmap;
begin
  Bmp := TJpeg2000Bitmap.Create;
  try
    Bmp.LoadFromStream(Source);              // descodifica um JP2/J2K existente
    Bmp.LosslessCompression := True;         // wavelet reversível 5/3
    // ou, para um ficheiro com perdas mais pequeno:
    // Bmp.LosslessCompression := False;
    // Bmp.CompressionQuality := 80;         // corresponde ao valor por omissão
    Bmp.SaveToStream(Output);                // grava sempre um ficheiro JP2
  finally
    Bmp.Free;
  end;
end;

Porque não existe um pipeline de filtros que descodifique ao carregar

Há um facto de arquitetura que molda a forma de usar tudo isto, e é fácil assumir o contrário. O HotPDF não tem um filtro geral de imagem que descodifique ao carregar. Quando se abre um PDF que já contém uma imagem JPXDecode, o motor não descodifica esse fluxo. Mantém os bytes JPEG 2000 exatamente como estão, pelo que uma cópia de página ou uma combinação de documentos transporta a imagem intocada, byte a byte. O descodificador tem um único ponto de entrada, e está do lado da criação: o AddImage baseado em ficheiro, encaminhado pela extensão do ficheiro para tratar origens .jp2, .j2k, .jpt e .jpc

Essa separação é o desenho correto e não uma limitação. Descodificar um fluxo JPX incorporado ao carregar, só para o voltar a codificar ao gravar, converteria uma imagem arquivada sem perdas numa imagem com perdas e inflacionaria todas as combinações, tudo isto por uma imagem que apenas se queria mover de um PDF para outro. Passar o fluxo tal e qual é uma operação sem perdas e rápida. A descodificação é adiada para o único momento em que é genuinamente necessária: quando se entrega ao motor um ficheiro JPEG 2000 em disco e se lhe pede que rasterize essa imagem para a colocar numa página nova. Nesse ponto o ficheiro tem de se tornar píxeis, e o descodificador entra em ação

Diagrama de duas vias que contrasta o caminho de carregamento no HotPDF, onde os bytes JPXDecode incorporados atravessam uma copia de pagina ou uma combinacao intocados e byte a byte, com o caminho de criacao onde o AddImage descodifica um ficheiro JP2 uma vez atraves do OpenJPEG antes de o ShowImage colocar e voltar a incorporar o raster
Mover uma imagem existente entre documentos copia os bytes tal e qual, enquanto o trabalho de descodificação só acontece quando o AddImage transforma um ficheiro JP2 ou J2K em píxeis

Registar o suporte e colocar uma imagem

O registo de imagens JPEG 2000 é opcional e fica atrás do interruptor de compilação HPDF_REGISTER_JPEG2000_PICTURE, desligado por omissão. A razão é um conflito real, não excesso de cautela: registar globalmente os formatos de ficheiro jp2, j2k e jpc no TPicture pode interferir com a deteção de formato BLOB de que o TppDBImage do ReportBuilder depende. Defina o interruptor quando essa integração não estiver em jogo, e os formatos de ficheiro registam-se para que o TPicture os reconheça; deixe-o por definir e o encaminhamento por extensão do AddImage continua a descodificar ficheiros JPEG 2000 diretamente, porque esse caminho não passa de todo pelo TPicture

Entendido isso, colocar uma imagem JPEG 2000 segue a mesma cadência de três chamadas de qualquer outra imagem do HotPDF. Entregue ao AddImage um caminho .jp2 e um tipo de compressão que diga como a imagem deve ser guardada na saída, e depois posicione na página o índice de imagem devolvido com ShowImage

var
  Pdf: THotPDF;
  ImgIndex: Integer;
begin
  Pdf := THotPDF.Create(nil);
  try
    Pdf.BeginDoc;
    Pdf.AddPage;
    // A origem .jp2 é descodificada pelo backend OpenJPEG e depois
    // reincorporada com a compressão que aqui pedir.
    ImgIndex := Pdf.AddImage('Scan_16bit.jp2', icJpeg);
    // x, y, largura, altura em pontos; o 0 final é o ângulo de rotação.
    Pdf.CurrentPage.ShowImage(ImgIndex, 72, 72, 400, 300, 0);
    Pdf.EndDoc;
  finally
    Pdf.Free;
  end;
end;

A compressão que passa ao AddImage controla como a imagem descodificada é novamente guardada, não como foi lida. Um ficheiro JPEG 2000 descodificado para um bitmap pode voltar a sair como um JPEG DCTDecode, um raster Flate, ou outro filtro suportado, conforme convier ao documento. A descodificação a partir de JP2 ou J2K acontece primeiro em qualquer caso, pelo que a mesma chamada aceita uma origem comprimida por wavelet e incorpora-a na forma que o resto do seu pipeline esperar

Para o panorama mais amplo de como imagens e fontes aterram na saída gerada, veja as nossas notas sobre saída de relatórios com fontes e imagens. Quando o documento que está a montar reutiliza conteúdo de PDFs existentes, o comportamento de passagem direta aqui descrito conjuga-se com a mecânica de combinação e revisão em fluxos de objetos e atualizações incrementais. O motor de descodificação JPEG 2000 faz parte do HotPDF Delphi Component para Delphi e C++Builder, a par das APIs de imagem, fonte e documento tratadas noutros artigos deste blogue