Artículo técnico

TIFF de 12 bits, lossless y JPEG aritmético a PDF en Delphi

PDFlibPas, la PDF Library para Delphi, importa imágenes TIFF cuya compresión JPEG va más allá del baseline de 8 bits: secuencia extendida de 12 bits (SOF1), Huffman lossless (SOF3), codificación aritmética (SOF9 y SOF10) y el JPEG antiguo de TIFF 6.0 con offsets de tablas. Como el DCTDecode de PDF no puede transportar esos procesos, PDFlibPas los decodifica en Pascal nativo e incrusta las muestras sin pérdidas a 8 o 16 bits por componente

El conjunto completo exige v3.724.0 o posterior, y apunta directamente a TIFF de escáneres, software de captura médica y científica, y sistemas de archivo congelados en los noventa. Abajo están por qué estas variantes no se pueden copiar al PDF tal cual, cómo las aterriza PDFlibPas y las trampas de Pascal encontradas por el camino

¿Por qué un JPEG de 12 bits no puede entrar directo en un PDF?

Un JPEG de 12 bits no se puede pasar a través porque PDF no tiene ningún filtro obligado a decodificarlo. ISO 32000-1 §7.4.8 define DCTDecode alrededor del formato baseline de JPEG, con la extensión progressive añadida en PDF 1.3. Nada en esa cláusula cubre la precisión de muestra de 12 bits, el proceso lossless del Anexo H de ITU-T T.81 o el codificador de entropía aritmético del Anexo D. Un lector que se encuentre un frame SOF1 de 12 bits o un marcador SOF9 dentro de un stream DCTDecode está en su derecho de no dibujar nada, y la mayoría de los visores hacen exactamente eso

Para las strips Huffman baseline y progressive de 8 bits que forman la mayoría de los archivos JPEG-en-TIFF, PDFlibPas sigue validando cada scan y deja pasar los bytes comprimidos como DCTDecode, lo que mantiene el PDF pequeño. Todo lo demás se decodifica. PDFlibPas lee el marcador de frame y la precisión de muestra del primer bloque JPEG, y cuando encuentra SOF3, SOF9, SOF10 o una precisión distinta de 8, la página pasa por el decodificador de muestras en vez de por la ruta de paso directo. Detectar la codificación aritmética por su marcador de frame importa más de lo que suena: un check que solo mira la precisión de muestra deja que un TIFF clásico cuele una strip SOF9 de 8 bits por la ruta DCTDecode y produzca un PDF que los lectores no pueden mostrar

Flujo de decisión para bloques JPEG TIFF en PDFlibPas para Delphi: el marcador de frame enruta los scans de 8 bits a DCTDecode y los demás procesos a decodificación nativa
el marcador de frame, y no solo la precisión de muestra, decide si una strip JPEG TIFF pasa a través como DCTDecode o se decodifica de forma nativa

Qué escribe PDFlibPas en el PDF en su lugar

PDFlibPas convierte los bloques JPEG decodificados en un image XObject corriente con BitsPerComponent de 8 o 16. Las muestras se guardan sin comprimir por defecto; llama a CompressImages(1) antes de añadir la imagen y PDFlibPas las escribe con FlateDecode. ISO 32000-1 §8.9.5.1 permite 16 bits por componente desde PDF 1.5, así que PDFlibPas sube la versión del documento a 1.5 como mínimo cada vez que se añade una imagen con muestras de 16 bits. Eso incluye imágenes multi-strip y teseladas, cuyo objeto raíz es un Form XObject sin BitsPerComponent propio que inspeccionar

El mapeo es fijo por proceso JPEG:

  • El JPEG Huffman de 12 bits (secuencial SOF1, progressive SOF2) se dequantiza, se transforma inversamente y se ensancha a muestras de 16 bits
  • El JPEG con codificación aritmética (secuencial SOF9, progressive SOF10) se convierte en muestras de 8 bits con precisión de 8 bits y en muestras de 16 bits con precisión de 12 bits
  • El JPEG Huffman lossless (SOF3) se convierte en muestras de 8 o 16 bits, coincidiendo exactamente con su precisión declarada

El ensanchamiento de 12 a 16 bits usa replicación de bits, (V shl 4) or (V shr 8), así que 4095 mapea a 65535 y el blanco se queda blanco en vez de quedarse en 65520. PDFlibPas nunca reduce estas imágenes a 8 bits a tus espaldas. Cuando la biblioteca rasteriza tal página para su propia superficie de visualización usa el byte alto de cada muestra, pero la imagen PDF incrustada conserva los dieciséis

Mapeo de procesos JPEG decodificados a anchos de muestra en PDFlibPas para Delphi: el Huffman de 12 bits se ensancha a 16 bits, aritmético y lossless se mapean según la precisión declarada
cada proceso JPEG tiene un ancho de muestra fijo en el PDF, y los datos de 12 bits se ensanchan por replicación de bits para que el blanco siga siendo blanco

El precio es el tamaño de archivo: incluso con CompressImages(1), las muestras de 16 bits comprimidas con Flate son mayores que los datos DCT de los que venían, y sin él lo son aún más. Para flujos de diagnóstico y archivado ese intercambio suele ser el correcto. Si el tamaño importa más para un trabajo concreto, la guía para reducir el tamaño de archivo PDF en Delphi muestra a dónde van realmente los bytes

Importar estos TIFF con TPDFlib

El código llamador no cambia en absoluto. TPDFlib.AddImageFromFile(Const FileName: WideString; Options: Integer): Integer toma en Options el número de página TIFF base-1 y devuelve un ID de imagen, o 0 cuando la página no se puede importar. Las variantes JPEG no soportadas se rechazan antes de insertar la imagen, así que un 0 significa que nada a medio decodificar llegó al documento

uses
  PDFlibrary;

function TiffToPdf(const TiffName, PdfName: WideString): Boolean;
var
  Lib: TPDFlib;
  Page, PageCount, ImageID: Integer;
  DPI, W, H: Double;
begin
  Result := False;
  Lib := TPDFlib.Create;
  try
    Lib.SetMeasurementUnits(0);   // user space por defecto, 1/72 de pulgada
    Lib.SetOrigin(1);             // origen arriba-izquierda para DrawImage
    Lib.CompressImages(1);        // Flate para las muestras decodificadas
    PageCount := Lib.GetImagePageCount(TiffName);
    if PageCount = 0 then
      Exit;
    for Page := 1 to PageCount do
    begin
      ImageID := Lib.AddImageFromFile(TiffName, Page);  // Options = página TIFF
      if ImageID = 0 then
        Exit;                     // datos de imagen no soportados o dañados
      Lib.SelectImage(ImageID);
      case Lib.ImageResolutionUnits of
        2: DPI := Lib.ImageHorizontalResolution;          // puntos por pulgada
        3: DPI := Lib.ImageHorizontalResolution * 2.54;   // puntos por cm
      else
        DPI := 0;
      end;
      if DPI <= 0 then
        DPI := 300;
      W := Lib.ImageWidth * 72 / DPI;
      H := Lib.ImageHeight * 72 / DPI;
      if Page > 1 then
        Lib.NewPage;
      Lib.SetPageDimensions(W, H);
      Lib.DrawImage(0, 0, W, H);
    end;
    Result := Lib.SaveToFile(PdfName) = 1;
  finally
    Lib.Free;
  end;
end;

No hace falta ramificar por formato, y AddImageFromStream y AddImageFromString siguen las mismas reglas para datos TIFF en memoria

JPEG old-style: tablas en offsets, strips sin cabeceras

Desde v3.713.0, PDFlibPas importa TIFF JPEG old-style (Compression = 6) cuyas tablas vienen de los tags de offset JPEGQTables, JPEGDCTables y JPEGACTables o de un stream JPEGInterchangeFormat solo de tablas. La Sección 22 de TIFF 6.0 definió ese layout, y TIFF Technical Note 2 lo reemplazó después con Compression = 7 y un tag compartido JPEGTables, pero los escáneres siguieron escribiendo la forma antigua durante años

El lado del contenedor, BigTIFF y layouts teselados, queda cubierto en el artículo sobre endurecer el decodificador TIFF; aquí el foco es el payload JPEG. Cada tag de offset de tabla apunta a una tabla de cuantización cruda de 64 bytes, o a una tabla Huffman de 16 counts seguidos de sus valores, una entrada por componente. PDFlibPas convierte eso en segmentos DQT y DHT dentro de un stream JPEGTables sintetizado. Cada strip de datos entropy-coded pelados recibe entonces su propia cabecera: SOI, un marcador de frame que lleva el recuento de filas de esa strip, DRI cuando JPEGRestartInterval está puesto, SOS, los datos de entropía y EOI. Las strips no se pueden pegar en un solo scan, porque el JPEG old-style reinicia la predicción DC en cada frontera de strip

Dos detalles se equivocan con facilidad. Los tres componentes de una imagen YCbCr suelen apuntar sus offsets de tabla a los mismos bytes, así que PDFlibPas deduplica tablas por contenido antes de asignar números de tabla. Si quedan más de dos tablas Huffman de una clase, el frame sintetizado usa SOF1 en vez de SOF0, ya que un frame baseline solo puede referenciar dos tablas DC y dos AC. Cuando JPEGInterchangeFormat apunta en cambio a un stream JPEG completo que contiene scans, PDFlibPas trata ese stream como la imagen entera y lo importa como una única strip new-style. El JPEG old-style lossless, teselado y de planos separados se rechaza con un error explícito en vez de andar por las ramas, así que convierte esos a JPEG new-style aguas arriba

¿Por qué un JPEG CMYK dentro de un TIFF sale invertido?

Un JPEG CMYK dentro de un TIFF sale invertido cuando el importador aplica la convención Adobe APP14 que pertenece a los archivos JPEG independientes. Photoshop escribe CMYK invertido en los JPEG independientes, así que los productores PDF tradicionalmente añaden /Decode [1 0 1 0 1 0 1 0] para compensar. PhotometricInterpretation 5 de TIFF define las muestras como cantidades de tinta donde 0 significa sin tinta, y los lectores TIFF mayoritarios toman las muestras JPEG decodificadas a la cara sin mirar APP14

PDFlibPas incrusta strips CMYK TIFF comprimidas con JPEG sin el array Decode invertido, haya o no un marcador APP14. La bandera transform de APP14 sigue decidiendo si YCCK se convierte a CMYK, y DCTDecode maneja ese paso por sí solo. Los archivos JPEG independientes de cuatro componentes conservan la convención invertida de Adobe, así que bytes JPEG idénticos se tratan de forma distinta según el contenedor, como exigen los dos formatos

¿Por qué el decode de JPEG lossless se rompe con un shift de Pascal?

El decode de JPEG lossless se rompe en Pascal porque los predictores 5 y 6 necesitan un desplazamiento a la derecha aritmético y el shr de Pascal es lógico. SOF3 predice cada muestra desde sus vecinos izquierdo (Ra), superior (Rb) y superior-izquierdo (Rc) y solo codifica la diferencia. De los siete predictores de la Tabla H.1 de T.81, el predictor 5 es Ra + ((Rb - Rc) >> 1) y el 6 es Rb + ((Ra - Rc) >> 1), y la diferencia entre corchetes se vuelve negativa siempre que el vecino superior-izquierdo sea el más brillante. Dale a shr un Integer negativo y el bit de signo se convierte en un bit de magnitud, así que la imagen se disuelve en ruido

Comportamiento de shift para los predictores JPEG lossless en Delphi y PDFlibPas: el shr lógico corrompe valores negativos, div 2 trunca hacia cero, HalfDown redondea a la baja como la referencia
los predictores 5 y 6 necesitan semántica de floor, y un helper pequeño iguala al decodificador de referencia en Delphi y Free Pascal

Cambiar el shift por div 2 tampoco lo arregla. div trunca hacia cero mientras un desplazamiento aritmético redondea hacia infinito negativo, y los dos discrepan en cada valor negativo impar. Free Pascal tiene SarLongint en su unidad System; Delphi no tiene equivalente, así que PDFlibPas usa un helper que compila idéntico en ambos:

// Desplazamiento aritmético a la derecha en uno: floor(Value / 2)
// shr es un desplazamiento lógico, y div 2 trunca hacia cero
function HalfDown(Value: Integer): Integer;
begin
  if Value >= 0 then
    Result := Value shr 1
  else
    Result := -((1 - Value) shr 1);
end;

// Value = -3:  -3 shr 1 = 2147483646,  -3 div 2 = -1,  HalfDown(-3) = -2

Para el lado de ancho de tipos de la misma clase de problema, véase trampas de Longint y punteros en Delphi Linux64

El resto del decodificador lossless tiene reglas de frontera propias. La primera fila, y la primera fila tras cada marcador de restart, predice solo desde el vecino izquierdo y arranca en 2^(P−Pt−1) para precisión P y point transform Pt; cada fila posterior arranca desde la muestra de arriba. La reconstrucción da la vuelta módulo 2^16, y el point transform solo escala la salida, nunca la cadena de predicción. La categoría DC 16 de Huffman, una diferencia de 32768 sin bits extra, es legal solo en modo lossless, así que el parser de tablas tiene que aceptarla antes de saber qué proceso declarará el frame

PDFlibPas soporta los siete predictores, los point transforms y los intervalos de restart que cubren filas enteras. La precisión JPEG debe igualar BitsPerSample y ser 8 o 16, y cada componente debe muestrearse 1x1. Los datos YCbCr lossless y el JPEG lossless de 12 bits se rechazan con un error explícito

DCT de 12 bits y codificación aritmética en Pascal nativo

Los decodificadores de 12 bits y aritmético de PDFlibPas comparten una IDCT inversa y solo difieren en la decodificación de entropía. En el JPEG Huffman de 12 bits, las categorías DC llegan a 15 y las AC a 14, las tablas de cuantización pueden llevar entradas de 16 bits, y la predicción DC se reinicia en cada scan y cada restart. Los coeficientes permanecen en orden zigzag hasta la dequantización, que debe usar el valor de cuantización en el mismo índice zigzag, y solo entonces pasan a orden natural para la IDCT separable de dos pasadas. Las muestras decodificadas caen dentro de ±1 de un decodificador de referencia en coma flotante independiente

La codificación aritmética reemplaza Huffman por el codificador QM del Anexo D de T.81: una tabla de estados de probabilidad de 114 entradas, un área de estadísticas DC de 64 bins y un área AC de 256 bins, dirigida por parámetros de acondicionamiento del segmento DAC (por defecto L = 0, U = 1, Kx = 5). Las estadísticas, el contexto DC y la predicción se reinician en cada intervalo de restart y al comienzo de cada scan. Por eso PDFlibPas conserva los segmentos DAC cuando normaliza los JPEGTables compartidos para imágenes aritméticas; suéltalos y cada strip se decodifica con el modelo de estadísticas equivocado

v3.723.0 añadió a estas rutas decodificadas componentes submuestreados y conversión YCbCr a RGB con las ecuaciones JFIF, y v3.724.0 añadió scans progressive tanto para datos Huffman de 12 bits (SOF2) como aritméticos (SOF10). La chroma submuestreada se replica a resolución completa en vez de suavizarse, que es lo que produce un decodificador de referencia con el suavizado de chroma apagado. El JPEG jerárquico y el JPEG lossless con codificación aritmética (SOF11) se rechazan con un error explícito

Probar fixtures JPEG binarios en Delphi

Los fixtures JPEG hechos a mano en tests unitarios de Delphi pueden fallar de una forma que parece exactamente un bug del decodificador. Una constante de cadena construida con caracteres #$xx que pasa de 255 caracteres se tipa como UnicodeString, y asignarla a un AnsiString la convierte a través de la code page activa. Los bytes $80 a $9F, rutinarios en datos entropy-coded, se reescriben por el camino. Construir cabeceras de marcadores en tiempo de ejecución tiene la misma trampa: en Delphi, AnsiChar + AnsiChar produce un UnicodeString, así que añade bytes sueltos a un AnsiString o escríbelos con SetLength y asignación indexada. Para los fixtures, guarda texto hex y decodifícalo en tiempo de ejecución:

uses
  SysUtils, PDFlibrary;

function HexToBytes(const Hex: AnsiString): AnsiString;
var
  I: Integer;
begin
  SetLength(Result, Length(Hex) div 2);
  for I := 1 to Length(Result) do
    Result[I] := AnsiChar(StrToInt('$' + string(Copy(Hex, I * 2 - 1, 2))));
end;

// El fixture se queda en ASCII puro, así que ninguna code page puede tocarlo
ImageID := Lib.AddImageFromString(HexToBytes(TiffHex), 1);

Al generar imágenes de test, ten en cuenta que jpegtran -arithmetic escribe SOF9 secuencial; SOF10 necesita además -progressive. Y un ID de imagen distinto de cero solo prueba que la importación se aceptó. Renderiza la página y compara píxeles, porque un error de paso directo produce un PDF bien formado que ningún visor puede dibujar

Llevar archivos de escaneo legados a PDF

Estas variantes JPEG tienden a asomar todas de golpe durante una migración de archivo. PDFlibPas lee TIFF JPEG de 12 bits, lossless, aritmético y old-style con la misma llamada AddImageFromFile que todo lo demás, conserva la precisión completa en el PDF y rechaza los procesos raros fuera de ese conjunto con un error explícito. Para licencias, la lista completa de funciones y descargas de prueba, véase la página del producto PDFlibPas PDF Library for Delphi