Artículo técnico

Cómo agregar imágenes JPEG 2000 a documentos PDF en Delphi con HotPDF

Una diapositiva médica escaneada, una porción de un estudio aéreo, un fotograma de película archivado con rango dinámico completo. Estas son las imágenes que llegan como JPEG 2000, y lo hacen así por una razón. El formato mantiene 12 o 16 bits por canal, se comprime con una transformación wavelet en lugar del bloque DCT que usa JPEG, y puede codificar la misma imagen sin pérdidas (lossless) o con pérdida (lossy) a partir de un flujo de código. Cuando un documento creado a partir de esas fuentes tiene que convertirse en un PDF, la imagen tiene que viajar a través de 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 (stub functions) que devolvían nil, de modo que la API existía pero no decodificaba nada. El motor actual vincula OpenJPEG 2.5.4 de forma estática y convierte un origen JP2 o J2K en píxeles que HotPDF puede colocar en una página

El filtro JPXDecode en PDF

La norma ISO 32000-1 define el filtro JPXDecode en la sección 7.4.9. Un image XObject de 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ódigo JPEG 2000 en lugar del JPEG base (baseline) que transporta /DCTDecode. El filtro es lo que permite que un PDF contenga datos de imagen comprimidos mediante wavelet con alta profundidad de bits, y admite tanto los modos sin pérdidas como con pérdida del códec, ya que el modo es una propiedad del flujo de código en sí y no del envoltorio que lo rodea

Ese último punto es el que vale la pena tener en cuenta. JPEG 2000 es un solo algoritmo con un caso especial sin pérdidas, no dos formatos separados. La wavelet reversible 5/3 reconstruye exactamente las muestras originales; la wavelet irreversible 9/7 intercambia esa exactitud por un archivo más pequeño. Un decodificador trata a ambas de la misma manera al momento de leer, razón por la cual HotPDF necesita solo una ruta de decodificación para aceptar todo lo que le entregue un flujo JPXDecode

Lo que el decodificador les hace a los píxeles

Los image XObjects de PDF en el caso común esperan 8 bits por componente en DeviceGray o DeviceRGB. JPEG 2000 supera esto de manera habitual, y su modelo de componentes es más general que un ráster empaquetado, por lo que el decodificador tiene tres trabajos que hacer antes de que los datos puedan usarse como una imagen normal

Primero, los componentes con alta profundidad de bits se remuestrean (resample) a 8 bits. Una muestra de 12 o 16 bits se reduce a la escala de 0 a 255 de modo que el resultado es un ráster ordinario de 8 bits. Los componentes con signo se cambian primero al rango sin signo. Este detalle importa porque implica pérdida de información 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 un nuevo archivado

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 luminancia y crominancia (luma-chroma) para lograr una compresión más eficiente, la misma idea que usa el JPEG base, y el decodificador aplica la transformación inversa estándar para que la página reciba el RGB real

En tercer lugar, los componentes submuestreados (subsampled) se sobremuestrean (upsample) mediante la replicación del vecino más cercano (nearest-neighbor). Los canales de crominancia frecuentemente se almacenan a la mitad de la resolución, por lo que el decodificador lee cada componente en sus propias dimensiones y su propio factor de muestreo, para luego replicar las muestras y llevar cada canal al tamaño de la imagen completa antes del entrelazado. La replicación del vecino más cercano hace que el paso sea económico; la crominancia que está llenando era de baja frecuencia para empezar, por lo que el costo visual es pequeño

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

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

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

uses
  HPDFJpeg2000;

var
  Decoder: THPDFJpeg2000Decoder;
  Pixels: TJpeg2000ByteArray;
begin
  Decoder := THPDFJpeg2000Decoder.Create;
  try
    if Decoder.LoadFromStream(Input) then          // JP2 o J2K, detectado automáticamente
      if Decoder.GetImageData(Pixels) then
        // Pixels se entrelaza en 8 bits, su ancho es ColorComponents canales,
        // ordenados por filas (row-major) de arriba hacia abajo: listo para un XObject DeviceGray/DeviceRGB.
        ProcessRaster(Decoder.Width, Decoder.Height,
                      Decoder.ColorComponents, Pixels);
  finally
    Decoder.Free;
  end;
end;

Compresión sin pérdidas y con pérdida en el lado de la 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 la otra dirección y se produce un archivo JPEG 2000, lo que HotPDF también puede hacer a través de la clase TJpeg2000Bitmap, descendiente de TBitmap, que carga y guarda los datos rasterizados como JP2. Dos propiedades rigen la salida. LosslessCompression es un valor booleano que, si es verdadero (true), selecciona la wavelet reversible; CompressionQuality es de tipo TJpeg2000QualityRange, un entero del 1 al 100 donde 1 es pequeño y feo, y 100 es grande y fiel al original. Los valores predeterminados residen en constantes con nombre: Jpeg2000DefaultLosslessCompression es False y Jpeg2000DefaultLossyQuality es 80

La decisión está basada en el contenido. La compresión sin pérdidas (lossless) es adecuada para una copia maestra, un escaneo médico o legal, cualquier cosa que pueda ser re-codificada más adelante y no deba acumular pérdida generacional. La compresión con pérdida a calidad 80 se ajusta a una imagen destinada a la pantalla o la impresión, donde la degradación elegante de la wavelet ofrece un archivo notablemente más pequeño sin ningún artefacto (artifact) que un lector pueda notar. Hay una advertencia para CMYK que se debe señalar: el mapa de bits expone SetCMYK para marcar los datos de cuatro canales como CMYK en lugar de RGBA, lo que importa para los flujos de impresión que mantienen intactas las separaciones

uses
  HPDFJpeg2000;

var
  Bmp: TJpeg2000Bitmap;
begin
  Bmp := TJpeg2000Bitmap.Create;
  try
    Bmp.LoadFromStream(Source);              // decodificar un JP2/J2K existente
    Bmp.LosslessCompression := True;         // wavelet 5/3 reversible
    // o, para un archivo con pérdida más pequeño:
    // Bmp.LosslessCompression := False;
    // Bmp.CompressionQuality := 80;         // coincide con el predeterminado
    Bmp.SaveToStream(Output);                // siempre escribe un archivo JP2
  finally
    Bmp.Free;
  end;
end;

Por qué no hay una canalización de filtros de decodificación al cargar

Un hecho arquitectónico da forma a cómo se usa todo esto, y es fácil asumir lo contrario. HotPDF no tiene un filtro de imagen general de decodificación al cargar (decode-on-load). Cuando se abre un PDF que ya contiene una imagen JPXDecode, el motor no decodifica ese flujo. Mantiene los bytes JPEG 2000 exactamente como están, por lo que una copia de página o una fusión de documentos transfiere la imagen intacta, byte por byte. El decodificador tiene un solo punto de entrada, y es en el lado de la creación: la función AddImage basada en archivos, que la despacha por la extensión de archivo para manejar las fuentes .jp2, .j2k, .jpt y .jpc

Esa separación es el diseño correcto y no una limitación. Decodificar un flujo JPX incrustado al cargarlo, solo para volver a codificarlo al guardarlo, convertiría una imagen archivada sin pérdidas en una con pérdida e inflaría cada fusión (merge), todo por una imagen que solo quería mover de un PDF a otro. Transmitir el flujo textualmente es una operación rápida y sin pérdidas. La decodificación se pospone para el único momento en que es genuinamente requerida: cuando le entrega al motor un archivo JPEG 2000 desde el disco y le pide que rasterice esa imagen para ubicarla en una nueva página. En ese punto, el archivo debe convertirse en píxeles y el decodificador se ejecuta

Cómo registrar el soporte y ubicar una imagen

El registro de imágenes JPEG 2000 es opcional detrás del interruptor de compilación (compile switch) HPDF_REGISTER_JPEG2000_PICTURE, que está desactivado de manera predeterminada. El motivo es un conflicto real y no por precaución: el registro global de los formatos de archivo jp2, j2k y jpc con TPicture puede interferir con la detección de formato BLOB de la que depende el TppDBImage de ReportBuilder. Defina el interruptor cuando no esté en juego esa integración y los formatos de archivo se registrarán para que TPicture los reconozca; si lo deja sin definir, el envío de extensión de AddImage de todos modos decodificará directamente los archivos JPEG 2000, debido a que esa ruta no pasa por TPicture en absoluto

Comprendido esto, ubicar una imagen JPEG 2000 sigue el mismo ritmo de tres llamadas que con cualquier otra imagen en HotPDF. Envíe a AddImage una ruta a un archivo .jp2 y un tipo de compresión que defina cómo debe almacenarse la imagen en la salida y luego coloque el índice de la imagen devuelto en la página con ShowImage

var
  Pdf: THotPDF;
  ImgIndex: Integer;
begin
  Pdf := THotPDF.Create(nil);
  try
    Pdf.BeginDoc;
    Pdf.AddPage;
    // La fuente .jp2 es decodificada a través del backend OpenJPEG, luego
    // se vuelve a incrustar con la compresión que usted solicite aquí.
    ImgIndex := Pdf.AddImage('Scan_16bit.jp2', icJpeg);
    // x, y, ancho, alto en puntos; el 0 final es el ángulo de rotación.
    Pdf.ShowImage(ImgIndex, 72, 72, 400, 300, 0);
    Pdf.EndDoc;
  finally
    Pdf.Free;
  end;
end;

La compresión que usted pasa a AddImage controla de qué manera se vuelve a guardar la imagen decodificada, y no la forma en que se leyó. Un archivo JPEG 2000 decodificado a un mapa de bits puede salir nuevamente como un JPEG DCTDecode, un ráster Flate u otro filtro admitido, el que más convenga para el documento. La decodificación de un JP2 o J2K se da primero en cualquier caso, por lo que la misma llamada acepta una fuente comprimida mediante wavelet y la incrusta en cualquier forma que requiera el resto de su flujo de trabajo

Para obtener una imagen más amplia de cómo las imágenes y las fuentes se posicionan en la salida generada, consulte nuestras notas sobre la salida de informes con fuentes e imágenes en Delphi. Cuando el documento que está ensamblando reutiliza contenido proveniente de archivos PDF ya existentes, el comportamiento de transferencia (passthrough) que aquí se describe se combina con la mecánica de fusión (merge) y revisión que se explica en flujos de objetos y actualizaciones incrementales. El motor de decodificación de JPEG 2000 se distribuye como parte del Componente HotPDF para Delphi y C++Builder, junto a las API para el manejo de imágenes, fuentes y documentos de las que ya hablamos en otras partes de este blog