Artículo técnico

TIFF de 12 bits, JPEG sin pérdida y aritmético a PDF

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

El conjunto completo requiere 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á por qué estas variantes no pueden copiarse al PDF tal cual, cómo las aterriza PDFlibPas y las trampas de Pascal encontradas en el camino

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

Un JPEG de 12 bits no puede pasarse directo porque PDF no tiene ningún filtro que esté obligado a decodificarlo. ISO 32000-1 §7.4.8 define DCTDecode alrededor del formato baseline de JPEG, con la extensión progressive agregada en PDF 1.3. Nada en esa cláusula cubre precisión de muestras de 12 bits, el proceso sin pérdida 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 tiene derecho a 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 pasa los bytes comprimidos directo 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 la ruta de paso directo. Detectar codificación aritmética por su marcador de frame importa más de lo que suena: una verificación 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 la decodificación nativa
el marcador de frame, y no la precisión de muestra sola, decide si una strip JPEG TIFF pasa directo 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 ordinario con BitsPerComponent 8 o 16. Las muestras se guardan sin comprimir por defecto; llame CompressImages(1) antes de agregar 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 al menos 1.5 cada vez que se agrega una imagen con muestras de 16 bits. Eso incluye imágenes multi-strip y tiled, 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 descuantiza, 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 a precisión de 8 bits y en muestras de 16 bits a precisión de 12 bits
  • El JPEG Huffman sin pérdida (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 se mapea a 65535 y el blanco se queda blanco en vez de quedarse en 65520. PDFlibPas nunca baja estas imágenes a 8 bits a sus 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, el aritmético y el sin pérdida 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 más grandes que los datos DCT de donde vinieron, y sin eso son más grandes todavía. Para flujos de diagnóstico y archivo ese intercambio suele ser correcto. Si el tamaño importa más para un trabajo dado, la guía para reducir el tamaño de archivos PDF en Delphi muestra a dónde van realmente los bytes

Importar estos TIFF con TPDFlib

El código de llamada no cambia para nada. TPDFlib.AddImageFromFile(Const FileName: WideString; Options: Integer): Integer recibe el número de página TIFF base 1 en Options y devuelve un ID de imagen, o 0 cuando la página no puede importarse. Las variantes JPEG no soportadas se rechazan antes de insertar la imagen, así que un 0 significa que nada semidecodificado 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 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 se necesita ramificación por formato, y AddImageFromStream y AddImageFromString siguen las mismas reglas para datos TIFF en memoria

JPEG estilo antiguo: tablas en offsets, strips sin encabezado

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

El lado del contenedor, BigTIFF y layouts tiled, está cubierto en el artículo sobre el endurecimiento del 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 conteos seguidos de sus valores, una entrada por componente. PDFlibPas los convierte en segmentos DQT y DHT en un stream JPEGTables sintetizado. Cada strip de datos codificados de entropía crudos recibe entonces un encabezado propio: SOI, un marcador de frame que lleva el conteo de filas de esa strip, DRI cuando JPEGRestartInterval está puesto, SOS, los datos de entropía y EOI. Las strips no pueden pegarse en un solo scan, porque el JPEG estilo antiguo reinicia la predicción DC en cada frontera de strip

Dos detalles son fáciles de equivocar. 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 puede referenciar solo dos tablas DC y dos AC. Cuando JPEGInterchangeFormat en cambio apunta a un stream JPEG completo que contiene scans, PDFlibPas trata ese stream como la imagen entera y lo importa como una única strip estilo nuevo. El JPEG sin pérdida, tiled y de planos separados estilo antiguo se rechaza con un error explícito en vez de adivinar, así que convierta esos a JPEG estilo nuevo 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 APP14 de Adobe que pertenece a los archivos JPEG independientes. Photoshop escribe CMYK invertido en los JPEG independientes, así que los productores de PDF tradicionalmente agregan /Decode [1 0 1 0 1 0 1 0] para compensar. TIFF PhotometricInterpretation 5 define las muestras como cantidades de tinta donde 0 significa sin tinta, y los lectores TIFF mainstream toman las muestras JPEG decodificadas a la cara sin mirar APP14

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

¿Por qué la decodificación JPEG sin pérdida falla con un shift de Pascal?

La decodificación JPEG sin pérdida falla en Pascal porque los predictores 5 y 6 necesitan un shift aritmético a la derecha y el shr de Pascal es lógico. SOF3 predice cada muestra desde sus vecinos izquierdo (Ra), superior (Rb) y superior-izquierdo (Rc) y codifica solo 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 paréntesis se vuelve negativa siempre que el vecino superior-izquierdo es el más brillante. Alimente un Integer negativo a shr y el bit de signo se vuelve un bit de magnitud, así que la imagen se disuelve en ruido

Comportamiento del shift para los predictores JPEG sin pérdida en Delphi y PDFlibPas: el shr lógico corrompe los valores negativos, div 2 trunca hacia cero, HalfDown redondea a piso como la referencia
los predictores 5 y 6 necesitan semántica de piso, 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 que un shift aritmético redondea hacia infinito negativo, y ambos 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:

// Shift aritmético a la derecha en uno: floor(Value / 2)
// shr es un shift 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 anchos de tipos de la misma clase de problema, vea trampas de Longint y punteros en Delphi Linux64

El resto del decodificador sin pérdida tiene reglas de frontera propias. La primera fila, y la primera fila después de cada marcador 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 envuelve módulo 2^16, y el point transform escala solo 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 sin pérdida, así que el parser de tablas tiene que aceptarla antes de saber qué proceso declarará el frame

PDFlibPas soporta los siete predictores, point transforms e intervalos restart que cubren filas completas. La precisión JPEG debe igualar BitsPerSample y ser 8 o 16, y cada componente debe muestrearse 1x1. Los datos sin pérdida YCbCr y el JPEG sin pérdida 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 en PDFlibPas comparten una DCT inversa y difieren solo 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 descuantizació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 independiente en punto flotante

La codificación aritmética reemplaza Huffman con 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, guiados 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 restart y al inicio de cada scan. Por eso PDFlibPas conserva los segmentos DAC cuando normaliza JPEGTables compartidos para imágenes aritméticas; quítelos y cada strip se decodifica con el modelo de estadísticas equivocado

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

Probar fixtures JPEG binarios en Delphi

Los fixtures JPEG construidos a mano en pruebas unitarias 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 página de códigos activa. Los bytes $80 a $9F, rutinarios en datos codificados de entropía, se reescriben al entrar. Construir encabezados de marcadores en runtime tiene la misma trampa: en Delphi, AnsiChar + AnsiChar produce un UnicodeString, así que agregue bytes individuales a un AnsiString o escríbalos con SetLength y asignación indexada. Para fixtures, guarde texto hex y decodifíquelo en runtime:

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 queda en ASCII puro, así que ninguna página de códigos puede tocarlo
ImageID := Lib.AddImageFromString(HexToBytes(TiffHex), 1);

Al generar imágenes de prueba, note que jpegtran -arithmetic escribe secuencial SOF9; SOF10 necesita además -progressive. Y un ID de imagen distinto de cero solo prueba que el import fue aceptado. Renderice la página y compare píxeles, porque un error de paso directo produce un PDF bien formado que ningún visor puede dibujar

Llevar archivos de escaneo legacy al PDF

Estas variantes JPEG tienden a aparecer todas juntas durante una migración de archivo. PDFlibPas lee TIFF JPEG de 12 bits, sin pérdida, aritmético y estilo antiguo por 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 licenciamiento, la lista completa de funciones y descargas de prueba, vea la página del producto PDFlibPas PDF Library para Delphi