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

Diagrama de pipeline do caminho de decodificação JPEG 2000 em Delphi: uma fonte JP2 ou J2K com amostras de 12 ou 16 bits, cor SYCC e croma subamostrado é reamostrada para 8 bits, convertida para RGB, e com croma reamostrado para cima em um raster intercalado DeviceGray ou DeviceRGB pronto para XObject
Não importa o que um stream JPXDecode contenha, o decodificador chega a um raster entrelaçado comum de 8 bits adequado para XObjects DeviceGray ou DeviceRGB

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

Diagrama HotPDF da detecção do tipo de arquivo JPEG 2000 pelos bytes iniciais: um contêiner JP2 boxed abrindo com a assinatura 00 00 00 0C 6A 50 20 20 e um codestream J2K bruto abrindo com o marcador SOC FF 4F FF 51 alimentam uma verificação do primeiro byte que retorna jtJP2, jtJ2K ou jtInvalid para entrada não suportada
HotPDF identifica o contêiner pelos primeiros bytes apenas e mapeia uma assinatura não suportada para jtInvalid para que a decodificação falhe de forma limpa
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 é 8 bits intercalado, com ColorComponents canais de largura,
        // em row-major de cima para baixo: pronto para um XObject DeviceGray/DeviceRGB
        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);              // decodifica um JP2/J2K existente
    Bmp.LosslessCompression := True;         // reversible 5/3 wavelet
    // ou, para um arquivo lossy menor:
    // Bmp.LosslessCompression := False;
    // Bmp.CompressionQuality := 80;         // igual ao padrão
    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

Diagrama de duas vias contrastando o caminho de carga no HotPDF, em que bytes JPXDecode incorporados viajam por uma cópia de página ou mesclagem intocados e byte a byte, com o caminho de criação em que AddImage decodifica um arquivo JP2 uma vez pelo OpenJPEG antes de ShowImage posicionar e reincorporar o raster
Mover uma imagem existente entre documentos copia bytes literalmente, enquanto o trabalho de decodificação acontece apenas quando AddImage transforma um arquivo JP2 ou J2K em pixels

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;
    // A origem .jp2 é decodificada pelo backend OpenJPEG e depois
    // re-embutida com a compressão que você pedir aqui
    ImgIndex := Pdf.AddImage('Scan_16bit.jp2', icJpeg);
    // x, y, width, height 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 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 Delphi Component para Delphi e C++Builder, ao lado das APIs de imagem, fonte e documento cobertas em outros lugares neste blog