Artigo Técnico

Adicionando Imagens JPEG 2000 em PDFs no Delphi com HotPDF

Uma lâmina médica digitalizada, uma imagem aérea de levantamento, um quadro de filme arquivado com toda a sua faixa dinâmica. Essas são as imagens que chegam como JPEG 2000, e chegam assim por uma razão. O formato mantém 12 ou 16 bits por canal, comprime com uma transformada wavelet em vez do DCT em blocos que o JPEG usa, e pode codificar a mesma imagem tanto sem perdas quanto com perdas a partir de um único codestream. Quando um documento construído a partir dessas fontes precisa se tornar um PDF, a imagem tem que percorrer um filtro que a especificação PDF reserva especificamente para esse codec

O HotPDF v2.228.0 restaurou um motor de decodificação JPEG 2000 funcional para esse caminho. Uma versão anterior havia fornecido a unit com funções stub que retornavam nil, portanto a API existia mas não decodificava nada. O motor atual vincula o OpenJPEG 2.5.4 estaticamente e converte uma fonte JP2 ou J2K em pixels que o HotPDF pode posicionar em uma 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 sua compressão na entrada /Filter do dicionário de stream, e JPXDecode é o valor que indica que os dados do stream são um codestream JPEG 2000 em vez do JPEG base que o /DCTDecode carrega. O filtro é o que permite que um PDF contenha dados de imagem comprimidos por wavelet com alta profundidade de bits, e admite os modos sem perdas e com perdas do codec, porque o modo é uma propriedade do próprio codestream e não do invólucro ao seu redor

Esse último ponto é o mais importante a reter. O JPEG 2000 é um único algoritmo com um caso especial sem perdas, não dois formatos separados. A wavelet reversível 5/3 reconstrói as amostras originais exatamente; a wavelet irreversível 9/7 troca essa exatidão por um arquivo menor. Um decodificador trata ambos da mesma forma no momento da leitura, razão pela qual o HotPDF precisa de apenas um caminho de decodificação para aceitar qualquer coisa que um stream JPXDecode lhe envie

O que o decodificador faz com os pixels

XObjects de imagem PDF no caso comum esperam 8 bits por componente em DeviceGray ou DeviceRGB. O JPEG 2000 rotineiramente excede isso, e seu modelo de componentes é mais geral do que um raster empacotado, portanto o decodificador tem três tarefas a realizar antes que os dados possam ser usados como uma imagem normal

Primeiro, componentes de alta profundidade de bits são reamostrados para 8 bits. Uma amostra de 12 ou 16 bits é reduzida para o intervalo de 0 a 255 para que o resultado seja um raster comum de 8 bits. Componentes com sinal são deslocados para o intervalo sem sinal primeiro. O detalhe importa porque é com perdas em si mesmo: uma digitalização em tons de cinza de 16 bits perde sua profundidade tonal no momento em que se torna uma imagem PDF de 8 bits, o que é a troca correta para saída em tela e impressão, mas não para rearquivamento

Segundo, um espaço de cores YCbCr (o codec o chama de SYCC) é convertido para RGB. O JPEG 2000 frequentemente armazena cor em um espaço de luma-croma para eficiência de compressão, a mesma ideia que o JPEG base usa, e o decodificador aplica a transformada inversa padrão para que a página receba RGB verdadeiro

Terceiro, componentes com subamostragem são ampliados por replicação de vizinho mais próximo. Os canais de croma são frequentemente armazenados na metade da resolução, portanto o decodificador lê cada componente em suas próprias dimensões e seu próprio fator de amostragem, depois replica amostras para trazer cada canal ao tamanho total da imagem antes da intercalação. O vizinho mais próximo mantém o passo barato; o croma que está sendo preenchido tinha baixa frequência para começar, portanto o custo visual é pequeno

Caixas JP2 versus um codestream J2K puro

Um arquivo JPEG 2000 vem em duas formas, e o HotPDF detecta qual está lendo pelos primeiros bytes em vez da extensão do arquivo. Um arquivo JP2 é um contêiner estruturado em caixas: abre com a caixa de assinatura de doze bytes 00 00 00 0C 6A 50 20 20 e envolve o codestream junto com caixas que descrevem espaço de cores, resolução e metadados. Um codestream J2K puro não tem contêiner e começa com o marcador SOC FF 4F FF 51. O decodificador lê esses bytes iniciais, reconhece a assinatura e seleciona o codec OpenJPEG correspondente para cada caso

Ambas as formas são tratadas porque ambas ocorrem na prática. Dispositivos de captura e arquivos que precisam de metadados laterais emitem JP2; ferramentas que querem o menor payload possível emitem o codestream puro. 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 detector de assinatura de bytes resolve as duas formas que pode decodificar, JP2 e J2K, e reporta qualquer outra coisa como jtInvalid para que uma entrada não suportada falhe claramente em vez de produzir lixo

uses
  HPDFJpeg2000;

var
  Decoder: THPDFJpeg2000Decoder;
  Pixels: TJpeg2000ByteArray;
begin
  Decoder := THPDFJpeg2000Decoder.Create;
  try
    if Decoder.LoadFromStream(Input) then          // JP2 or J2K, auto-detected
      if Decoder.GetImageData(Pixels) then
        // Pixels is 8-bit interleaved, ColorComponents channels wide,
        // row-major top to bottom: ready for a DeviceGray/DeviceRGB XObject.
        ProcessRaster(Decoder.Width, Decoder.Height,
                      Decoder.ColorComponents, Pixels);
  finally
    Decoder.Free;
  end;
end;

Sem perdas e com perdas no lado de codificação

O decodificador lê ambos os modos sem ser informado qual é. A escolha só se torna um parâmetro quando você vai na direção oposta e produz um arquivo JPEG 2000, o que o HotPDF também pode fazer através da classe TJpeg2000Bitmap, um descendente de TBitmap que carrega e salva 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 onde 1 é pequeno e feio e 100 é grande e fiel. Os padrões estão em constantes nomeadas: Jpeg2000DefaultLosslessCompression é False e Jpeg2000DefaultLossyQuality é 80

A decisão é uma decisão de conteúdo. Sem perdas se adequa a uma cópia mestre, uma digitalização médica ou legal, qualquer coisa que possa ser recodificada posteriormente e não deve acumular perda de geração. Com perdas na qualidade 80 se adequa a uma imagem destinada à tela ou impressão, onde a degradação elegante da wavelet fornece um arquivo notavelmente menor sem artefato que um leitor perceberia. Há uma ressalva CMYK a destacar: o bitmap expõe SetCMYK para marcar dados de quatro canais como CMYK em vez de RGBA, o que importa para pipelines de impressão que mantêm separações intactas

uses
  HPDFJpeg2000;

var
  Bmp: TJpeg2000Bitmap;
begin
  Bmp := TJpeg2000Bitmap.Create;
  try
    Bmp.LoadFromStream(Source);              // decode an existing JP2/J2K
    Bmp.LosslessCompression := True;         // reversible 5/3 wavelet
    // or, for a smaller lossy file:
    // Bmp.LosslessCompression := False;
    // Bmp.CompressionQuality := 80;         // matches the default
    Bmp.SaveToStream(Output);                // always writes a JP2 file
  finally
    Bmp.Free;
  end;
end;

Por que não há pipeline de filtro de decodificação no carregamento

Um fato arquitetural molda como você usa qualquer coisa disso, e é fácil assumir o oposto. O HotPDF não tem um filtro de imagem geral de decodificação no carregamento. Quando você abre um PDF que já contém uma imagem JPXDecode, o motor não decodifica esse stream. Ele mantém os bytes JPEG 2000 exatamente como estão, portanto uma cópia de página ou uma mesclagem de documento carrega a imagem sem alterações, byte a byte. O decodificador tem um único ponto de entrada, e está no lado de criação: o AddImage baseado em arquivo, despachado por extensão de arquivo para lidar com fontes .jp2, .j2k, .jpt e .jpc

Essa divisão é o design correto em vez de uma limitação. Decodificar um stream JPX incorporado no carregamento, apenas para recodificá-lo ao salvar, converteria uma imagem arquivada sem perdas em uma com perdas e inflaria cada mesclagem, tudo por uma imagem que você pretendia apenas mover de um PDF para outro. Passar o stream verbatim é uma operação sem perdas e rápida. A decodificação é adiada para o único momento em que é genuinamente necessária: quando você entrega ao motor um arquivo JPEG 2000 do disco e pede que ele rasterize essa imagem para posicionamento em uma nova página. Nesse ponto o arquivo tem que se tornar pixels, e o decodificador roda

Registrando suporte e posicionando uma imagem

O registro de imagens JPEG 2000 é opcional atrás do switch de compilação HPDF_REGISTER_JPEG2000_PICTURE, que está desativado por padrão. O motivo é um conflito real, não cautela: registrar os formatos de arquivo jp2, j2k e jpc globalmente com TPicture pode interferir com a detecção de formato BLOB da qual o TppDBImage do ReportBuilder depende. Defina o switch quando essa integração não estiver em jogo, e os formatos de arquivo se registram para que o TPicture os reconheça; deixe-o indefinido e o despacho de extensão do AddImage ainda decodifica arquivos JPEG 2000 diretamente, porque esse caminho não passa pelo TPicture de forma alguma

Com isso entendido, posicionar uma imagem JPEG 2000 é o mesmo ritmo de três chamadas que qualquer outra imagem HotPDF. Passe ao AddImage um caminho .jp2 e um tipo de compressão para como a imagem deve ser armazenada na saída, depois posicione o índice de imagem retornado na página com ShowImage

var
  Pdf: THotPDF;
  ImgIndex: Integer;
begin
  Pdf := THotPDF.Create(nil);
  try
    Pdf.BeginDoc;
    Pdf.AddPage;
    // The .jp2 source is decoded through the OpenJPEG backend, then
    // re-embedded with the compression you request here.
    ImgIndex := Pdf.AddImage('Scan_16bit.jp2', icJpeg);
    // x, y, width, height in points; final 0 is the rotation angle.
    Pdf.ShowImage(ImgIndex, 72, 72, 400, 300, 0);
    Pdf.EndDoc;
  finally
    Pdf.Free;
  end;
end;

A compressão que você passa ao AddImage controla como a imagem decodificada é rearmazenada, não como foi lida. Um arquivo JPEG 2000 decodificado para um bitmap pode sair como um JPEG DCTDecode, um raster Flate ou outro filtro suportado, o que for adequado para o documento. A decodificação de JP2 ou J2K ocorre primeiro independentemente, portanto a mesma chamada aceita uma fonte comprimida por wavelet e a incorpora na forma que o restante do seu pipeline espera

Para uma visão mais ampla de como imagens e fontes aparecem na saída gerada, veja nossas notas sobre saída de relatórios com fontes e imagens. Quando o documento que você está montando reutiliza conteúdo de PDFs existentes, o comportamento de passthrough descrito aqui se combina com a mecânica de mesclagem e revisão em streams de objeto e atualizações incrementais. O motor de decodificação JPEG 2000 é fornecido como parte do HotPDF Component para Delphi e C++Builder, ao lado das APIs de imagem, fonte e documento cobertas em outros lugares neste blog