Technical Article

Añadir imágenes JPEG 2000 a PDFs en Delphi con HotPDF

Una placa médica escaneada, una tesela de reconocimiento aéreo, un fotograma de película archivado con todo su rango dinámico. Estas son las imágenes que llegan como JPEG 2000, y llegan así por una razón. El formato conserva 12 o 16 bits por canal, comprime con una transformada wavelet en lugar de la DCT de bloque que utiliza JPEG, y puede codificar la misma imagen sin pérdida o con pérdida desde un único flujo de códigos. Cuando un documento construido a partir de esas fuentes debe convertirse en un PDF, la imagen debe pasar por un filtro que la especificación PDF reserva exactamente para este códec

HotPDF v2.228.0 restauró un motor de decodificación JPEG 2000 funcional para esa ruta. Una compilación anterior había incluido la unidad con funciones vacías que devolvían nil, por lo que la API existía pero no decodificaba nada. El motor actual enlaza OpenJPEG 2.5.4 estáticamente y convierte una fuente JP2 o J2K en píxeles que HotPDF puede colocar en una página

El filtro JPXDecode en PDF

ISO 32000-1 define el filtro JPXDecode en §7.4.9. Un XObject de imagen PDF nombra su compresión en la entrada /Filter del diccionario de flujo, y JPXDecode es el valor que indica que los datos del flujo son un flujo de códigos JPEG 2000 en lugar del JPEG base que transporta /DCTDecode. El filtro es lo que permite a un PDF contener datos de imagen comprimidos por wavelet con alta profundidad de bits, y admite tanto el modo sin pérdida como el modo con pérdida del códec, ya que el modo es una propiedad del flujo de códigos en sí y no de la envoltura a su alrededor

Ese último punto es el que merece la pena retener. JPEG 2000 es un único algoritmo con un caso especial sin pérdida, no dos formatos separados. La wavelet 5/3 reversible reconstruye las muestras originales con exactitud; la wavelet 9/7 irreversible intercambia esa exactitud por un archivo más pequeño. Un decodificador trata ambos de la misma manera en el momento de la lectura, por lo que HotPDF necesita solo una ruta de decodificación para aceptar cualquier cosa que un flujo JPXDecode le envíe

Lo que hace el decodificador con los píxeles

Los XObjects de imagen PDF en el caso común esperan 8 bits por componente en DeviceGray o DeviceRGB. JPEG 2000 supera rutinariamente eso, y su modelo de componentes es más general que un ráster empaquetado, por lo que el decodificador tiene tres tareas que realizar antes de que los datos sean utilizables como una imagen normal

En primer lugar, los componentes de alta profundidad de bits se remuestrean a 8 bits. Una muestra de 12 o 16 bits se reduce a la escala de 0 a 255 para que el resultado sea un ráster ordinario de 8 bits. Los componentes con signo se desplazan primero al rango sin signo. El detalle importa porque conlleva pérdida en sí mismo: un escaneo en escala de grises de 16 bits pierde su profundo rango tonal en el momento en que se convierte en una imagen PDF de 8 bits, lo cual es el intercambio correcto para la salida en pantalla e impresión pero no para el rearchivado

En segundo lugar, un espacio de color YCbCr (el códec lo llama SYCC) se convierte a RGB. JPEG 2000 a menudo almacena el color en un espacio de luma-croma para lograr eficiencia en la compresión, la misma idea que utiliza JPEG base, y el decodificador aplica la transformada inversa estándar para que la página reciba RGB real

En tercer lugar, los componentes submuestreados se sobremuestrean mediante la replicación del vecino más cercano. Los canales de croma se almacenan frecuentemente a la mitad de resolución, por lo que el decodificador lee cada componente en sus propias dimensiones y su propio factor de muestreo, para luego replicar muestras y llevar cada canal al tamaño completo de la imagen antes del entrelazado. El vecino más cercano mantiene el paso barato; el croma que está llenando era de baja frecuencia para empezar, por lo que el coste visible es pequeño

Cajas JP2 frente a un flujo de códigos J2K sin procesar

Un archivo JPEG 2000 se presenta de dos formas, y HotPDF detecta cuál de ellas está leyendo por los primeros bytes en lugar de por la extensión del archivo. Un archivo JP2 es un contenedor estructurado en cajas: se abre con la caja de firma de doce bytes 00 00 00 0C 6A 50 20 20 y envuelve el flujo de códigos junto a cajas que describen el espacio de color, la resolución y los metadatos. Un flujo de códigos J2K sin procesar no lleva ningún contenedor en absoluto y comienza con el marcador SOC FF 4F FF 51. El decodificador lee esos bytes iniciales, reconoce la firma y selecciona el códec OpenJPEG correspondiente para cada caso

Ambas formas se manejan porque ambas se dan en la práctica. Los dispositivos de captura y los archivos que necesitan los metadatos complementarios emiten JP2; las herramientas que desean la carga útil más pequeña posible emiten el flujo de códigos desnudo. El tipo de formato se modela como una enumeración, TJpeg2000FileType, con los miembros jtInvalid, jtJP2, jtJ2K y jtJPT. El miembro JPT nombra la variante de transmisión JPIP; el detector de firma de bytes resuelve las dos formas que puede decodificar, JP2 y J2K, e informa de cualquier otra cosa como jtInvalid para que una entrada no admitida falle de forma limpia en lugar de producir basura

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;

Sin pérdida y con pérdida en el lado de codificación

El decodificador lee ambos modos sin que se le indique de cuál se trata. La elección solo se convierte en un parámetro cuando se va en sentido contrario y se produce un archivo JPEG 2000, lo que HotPDF también puede hacer a través de la clase TJpeg2000Bitmap, un descendiente de TBitmap que carga y guarda datos ráster como JP2. Dos propiedades gobiernan la salida. LosslessCompression es un booleano que selecciona la wavelet reversible cuando es verdadero; CompressionQuality es un TJpeg2000QualityRange, un entero del 1 al 100 donde 1 es pequeño y feo y 100 es grande y fiel. Los valores predeterminados residen en constantes con nombre: Jpeg2000DefaultLosslessCompression es False y Jpeg2000DefaultLossyQuality es 80

La decisión es una decisión de contenido. Sin pérdida se ajusta a una copia maestra, un escaneo médico o legal, cualquier cosa que pueda ser recodificada más tarde y no deba acumular pérdida generacional. Con pérdida a una calidad de 80 encaja con una imagen destinada a pantalla o impresión, donde la degradación elegante de la wavelet ofrece un archivo notablemente más pequeño sin ningún artefacto que un lector pueda percibir. Existe una salvedad sobre CMYK a destacar: el mapa de bits expone SetCMYK para marcar los datos de cuatro canales como CMYK en lugar de RGBA, lo cual importa para conductos de impresión que mantienen intactas las separaciones

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 qué no existe un conducto de filtro de decodificación al cargar

Un hecho arquitectónico moldea cómo se utiliza todo esto, y es fácil asumir lo contrario. HotPDF no tiene un filtro de imagen general de decodificación al cargar. Cuando se abre un PDF que ya contiene una imagen JPXDecode, el motor no decodifica ese flujo. Mantiene los bytes de JPEG 2000 exactamente como están, de modo que una copia de página o una combinación de documentos transporta la imagen intacta, byte por byte. El decodificador tiene un único punto de entrada, y se encuentra en el lado de creación: el AddImage basado en archivos, que es despachado por extensión de archivo para manejar las fuentes .jp2, .j2k, .jpt y .jpc

Esa división es el diseño correcto en lugar de una limitación. Decodificar un flujo JPX incrustado al cargar, solo para recodificarlo al guardar, convertiría una imagen archivada sin pérdida en una con pérdida e inflaría cada combinación, todo por una imagen que solo se pretendía mover de un PDF a otro. Pasar el flujo textualmente es una operación sin pérdida y rápida. La decodificación se pospone hasta el único momento en que es genuinamente necesaria: cuando se entrega al motor un archivo JPEG 2000 del disco y se le pide que rasterice esa imagen para colocarla en una página nueva. En ese punto, el archivo debe convertirse en píxeles y el decodificador se ejecuta

Registrar soporte y colocar una imagen

El registro de imágenes JPEG 2000 es de inclusión voluntaria a través del interruptor de compilación HPDF_REGISTER_JPEG2000_PICTURE, que está desactivado de manera predeterminada. La razón es un conflicto real, no por precaución: registrar los formatos de archivo jp2, j2k y jpc globalmente con TPicture puede interferir con la detección de formato BLOB en la que se basa TppDBImage de ReportBuilder. Defina el interruptor cuando esa integración no esté en juego, y los formatos de archivo se registrarán para que TPicture los reconozca; déjelo sin definir y el despacho de extensiones de AddImage seguirá decodificando los archivos JPEG 2000 de forma directa, porque esa ruta no pasa por TPicture en absoluto

Con esto entendido, colocar una imagen JPEG 2000 es el mismo ritmo de tres llamadas que con cualquier otra imagen de HotPDF. Entregue a AddImage una ruta .jp2 y un tipo de compresión sobre cómo debe almacenarse la imagen en la salida, para luego posicionar el índice de imagen devuelto en la página con 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;

La compresión que se pasa a AddImage controla cómo se vuelve a almacenar la imagen decodificada, no cómo se leyó. Un archivo JPEG 2000 decodificado a un mapa de bits puede volver a salir como un JPEG con DCTDecode, un ráster Flate u otro filtro admitido, el que convenga al documento. La decodificación desde JP2 o J2K ocurre primero de todas formas, por lo que la misma llamada acepta una fuente comprimida por wavelet y la incrusta en cualquier forma que espere el resto de su conducto

Para obtener un panorama más amplio de cómo las imágenes y las fuentes se introducen en la salida generada, consulte nuestras notas sobre la salida de informes con fuentes e imágenes. Cuando el documento que está ensamblando reutiliza contenido de PDFs existentes, el comportamiento de paso a través descrito aquí se combina con la mecánica de fusión y revisión en flujos de objetos y actualizaciones incrementales. El motor de decodificación JPEG 2000 se distribuye como parte del HotPDF Component para Delphi y C++Builder, junto a las API de imágenes, fuentes y documentos cubiertas en otras partes de este blog