Artículo técnico

Decodificador TIFF en Delphi: BigTIFF y TIFF teselado

PDFlibPas decodifica TIFF con un parser de Object Pascal escrito a mano en lugar de un binding de libtiff, y la versión 3.534.1 endureció exactamente los puntos donde ese parser rechaza la entrada. El magic 43 de BigTIFF ahora se rechaza por su nombre, TileOffsets y TileByteCounts se niegan durante el análisis de etiquetas, y cada búfer se dimensiona con aritmética Int64 bajo un techo de decodificación de 256 MiB

El defecto que esto cierra nunca aparece en un laboratorio. Aparece como una pasarela de escaneo que lleva tres años funcionando en silencio, hasta que un cliente enruta un archivo geoespacial o una imagen médica de portaobjetos completo. El archivo tiene un encabezado TIFF legítimo. Se parsea. Lo que sale es una página de ruido a franjas, o una asignación de varios gigabytes que tumba el servicio, y nada en el camino declaró inválida la entrada. Esa es la forma de fallo que vale la pena combatir con ingeniería: no un crash, sino una respuesta incorrecta entregada con total confianza

¿Por qué II o MM no prueban que tiene un TIFF clásico?

Porque el marcador de orden de bytes es compartido por ambos dialectos. El TIFF clásico y BigTIFF abren cada uno con II o MM, y el campo que realmente los distingue es el magic de 16 bits inmediatamente posterior: 42 para el TIFF clásico según la especificación TIFF 6.0, 43 para BigTIFF con sus offsets de 64 bits. Un loader escrito como FValidTIFF := PopWord = 42 no se equivoca con el TIFF clásico, pero colapsa dos rechazos muy distintos en un booleano silencioso, de modo que un BigTIFF se vuelve indistinguible de un JPEG truncado al que alguien renombró. PDFlibPas ahora separa los casos y registra cada uno en TPDFTIFF.LastError: un encabezado de menos de cuatro bytes, un marcador de orden de bytes inválido, el magic 43 y cualquier otro valor de magic producen textos distintos. La biblioteca sigue sin decodificar BigTIFF, y decirlo sin rodeos es justamente el punto. Quien llama obtiene la diferencia entre "esto no es un TIFF" y "esto es un TIFF cuya disposición de offsets de 64 bits el decodificador integrado no implementa", que es la diferencia entre un ticket de soporte que se responde en una réplica y uno que se convierte en una semana de adivinanzas

El loader TIFF de PDFlibPas lee el marcador de orden de bytes y el magic de 16 bits por separado, de modo que un encabezado corto, un marcador inválido, el magic 43 de BigTIFF y cualquier otro valor de magic producen textos LastError distintos en lugar de un booleano silencioso
El TIFF clásico y BigTIFF abren con el mismo marcador de orden de bytes, así que PDFlibPas separa cuatro casos de rechazo y nombra cada uno en LastError
var
  Tiff: TPDFTIFF;
  Page: Integer;
begin
  Tiff := TPDFTIFF.Create;
  try
    Tiff.LoadFromFile('inbox\scan-0417.tif');
    if not Tiff.ValidTIFF then
      raise Exception.Create('TIFF rejected: ' + Tiff.LastError);
    if Tiff.PageCount < 1 then
      raise Exception.Create('TIFF carries no decodable page');
    for Page := 1 to Tiff.PageCount do
      Writeln(Format('page %d: %dx%d, %d spp',
        [Page,
         Tiff.PageInfo[Page].Width,
         Tiff.PageInfo[Page].Height,
         Tiff.PageInfo[Page].SamplesPerPixel]));
  finally
    Tiff.Free;
  end;
end;

Los mosaicos son otra geometría, no otro arreglo de offsets

PDFlibPas rechaza el TIFF teselado mientras analiza las etiquetas, antes de tocar cualquier dato de píxel. El atajo que invita al error es fácil de ver: las etiquetas 324 (TileOffsets) y 325 (TileByteCounts) son arreglos de offsets de archivo y conteos de bytes, estructuralmente idénticos a los arreglos de franjas, así que apuntar los campos de franja existentes hacia ellos cuesta dos líneas y compila limpio. También es incorrecto. Los mosaicos forman una cuadrícula bidimensional con bloques de borde rellenos, su propio paso de fila dentro de cada mosaico y ninguna semántica de RowsPerStrip, como deja claro la sección de imágenes teseladas de TIFF 6.0. Alimentar los payloads de mosaico a un decodificador de franjas, por tanto, no falla de forma estridente. SimpleExtract y CompDecode recorren los datos con el paso equivocado y emiten una imagen con las dimensiones correctas y los píxeles incorrectos. El código antiguo agravaba esto al mantener StripsAreTiles, ColumnsPerTile y RowsPerTile en TTIFFPage: geometría de mosaico registrada por un decodificador sin ensamblador de mosaicos detrás. En 3.534.1 los manejadores de las etiquetas 324 y 325 lanzan el error de mosaico y abandonan el IFD de inmediato, de modo que el rechazo lleva la palabra "teselado" en lugar de aparecer semanas después como una queja de renderizado

PDFlibPas compara la disposición de franjas, donde bandas de ancho completo comparten un paso de fila, con la disposición de mosaicos, una cuadrícula bidimensional de bloques de borde rellenos, y rechaza las etiquetas 324 y 325 durante el análisis de etiquetas antes de tocar cualquier dato de píxel
Los arreglos de mosaico se ven estructuralmente idénticos a los arreglos de franjas, por eso alimentarlos a un decodificador de franjas produce las dimensiones correctas y los píxeles incorrectos

Un límite de dimensión no es un presupuesto de memoria

Limitar ancho y alto a 65,535 cada uno es necesario y no alcanza ni de cerca, porque la cantidad que impulsa la asignación es un producto. RowsPerStrip * Width * SamplesPerPixel puede desbordar la aritmética de 32 bits mucho antes de que cualquiera de las partes llegue a su propio límite, y aun sin desbordamiento puede nombrar una asignación que ningún servicio debería intentar. PDFlibPas calcula los bytes por fila en Int64 y aplica tres techos juntos: 65,535 por dimensión, 32 componentes de color y 256 MiB de bytes decodificados

const
  PDFLIB_TIFF_MAX_IMAGE_DIM = 65535;
  PDFLIB_TIFF_MAX_COLOR_COMPONENTS = 32;
  PDFLIB_TIFF_MAX_DECODED_IMAGE_BYTES = 256 * 1024 * 1024;

// dentro de TPDFTIFF.ValidatePageForDecode
BitsPerPixel := Int64(P.BitsPerSample) * P.SamplesPerPixel;
RowBytes := (Int64(P.Width) * BitsPerPixel + 7) div 8;
if (RowBytes < 1) or
   (RowBytes > PDFLIB_TIFF_MAX_DECODED_IMAGE_BYTES) or
   (Int64(P.Height) > PDFLIB_TIFF_MAX_DECODED_IMAGE_BYTES div RowBytes) then
  Exit(False);

DecodedBytes := RowBytes * P.RowsPerStrip;
if (DecodedBytes < 1) or
   (DecodedBytes > PDFLIB_TIFF_MAX_DECODED_IMAGE_BYTES) or
   (DecodedBytes > MaxInt) then
  Exit(False);

Tres detalles ahí importan más que las constantes mismas. La prueba de altura está escrita como una división en lugar de una multiplicación, así que el producto desbordado jamás se forma. Un RowsPerStrip menor que 1 o mayor que la altura de la imagen se normaliza primero a la altura, que es la lectura de franja única que TIFF 6.0 ya implica y que impide que una etiqueta hostil infle el búfer de franja. Y la rutina es compartida: ValidatePageForDecode corre al final del análisis de etiquetas y otra vez en la entrada tanto de SimpleExtract como de CompDecode, de modo que el código que llega directo a un decodificador no puede rodear el presupuesto. Es la misma regla que PDFlibPas sigue al parsear grafos de objetos PDF no confiables, porque un límite aplicado en una puerta de tres no es un límite

PDFlibPas dimensiona cada búfer TIFF con aritmética Int64, prueba la altura de la imagen con una división para que el producto desbordado jamás se forme, y ejecuta la misma rutina ValidatePageForDecode en el análisis de etiquetas y en ambas entradas del decodificador
Tres techos, aritmética Int64 para los bytes por fila y una rutina de validación compartida alcanzable desde las tres puertas, porque un límite aplicado en una puerta de tres no es un límite

¿Qué debe verificar quien llama antes de leer PageInfo?

Verifique primero ValidTIFF, luego PageCount, y solo entonces indexe PageInfo. Un archivo rechazado puede dejar PageCount en cero, y GetPageInfo responde a un índice fuera de rango con un registro TTIFFPage sin inicializar, así que una ruta de error que lee resolución o conteos de muestras en camino a reportar el fallo termina leyendo ruido. La versión 3.534.1 corrigió ambos puntos de llamada dentro de la biblioteca: la ruta de importación de imágenes lee XRes y YRes solo dentro de la rama válida, y TPDFlib.GetImagePageCount exige ValidTIFF en lugar de confiar por sí solo en un conteo de páginas distinto de cero. Aguas abajo, el argumento Options de AddImageFromFile es el número de página 1-based para un TIFF multipágina, así que GetImagePageCount debe ser confiable antes de que el ciclo arranque y no después. Cero páginas es ahora una respuesta real que significa "aquí no hay nada decodificable", no un accidente de un retorno temprano, lo que importa más cuando intercala y combina lotes de escaneo dúplex y una hoja mal decodificada en silencio caería en la posición equivocada

var
  Pdf: TPDFlib;
  Pages, I, ImageID: Integer;
begin
  Pdf := TPDFlib.Create;
  try
    Pages := Pdf.GetImagePageCount('inbox\scan-0417.tif');
    if Pages < 1 then
      Exit;  // encabezado inválido, BigTIFF, disposición teselada o fuera de presupuesto
    Pdf.NewDocument;
    for I := 1 to Pages do
    begin
      Pdf.NewPage;
      ImageID := Pdf.AddImageFromFile('inbox\scan-0417.tif', I);
      if ImageID > 0 then
      begin
        Pdf.SelectImage(ImageID);
        Pdf.DrawImage(0, 0, 595, 842);
      end;
    end;
    Pdf.SaveToFile('scan-0417.pdf');
  finally
    Pdf.Free;
  end;
end;

¿Construir el decodificador o enlazar libtiff?

PDFlibPas conserva el decodificador integrado, y el factor decisivo es el alcance de plataformas, no la autoría. Unas 1,873 líneas de Object Pascal compilan dondequiera que llegue el compilador: Win32, Win64, macOS, iOS, Android, y FPC en Linux. libtiff 4.7.1 son alrededor de 30,000 líneas de C repartidas en 34 unidades de traducción tif_*.c, y los archivos de objeto precompilados que existen hoy cubren solo Windows. Adoptarlo canjearía la cobertura completa de TIFF por una lista de plataformas soportadas que se encoge a las máquinas capaces de ejecutar la toolchain de C, más un paso de enlazado que nadie ha recorrido todavía

Lo que eso cuesta vale la pena declararlo sin adornos. El decodificador integrado maneja lo que el trabajo con documentos escaneados realmente produce: CCITT Group 3 unidimensional y bidimensional, Group 4, LZW, Deflate, PackBits y JPEG-in-TIFF, en las fotometrías WhiteIsZero, BlackIsZero, RGB, paleta y CMYK con Predictor 1 y 2. Esos payloads coinciden con los filtros PDF de ISO 32000-1 §7.4.4 y §7.4.6, y por eso el front-end TIFF pesa tanto en una pipeline de escaneo. Lo que no maneja es BigTIFF, mosaicos, Predictor 3 de punto flotante, PixarLog y SGILog, la compresión JPEG de estilo antiguo 6 y las pirámides de sub-IFD. Desde 3.534.1 cada uno de esos casos es un rechazo con nombre propio en lugar de una imagen incorrecta, y la biblioteca mantiene una lista escrita de disparadores para reabrir la decisión de libtiff:

  • un cliente reporta un archivo BigTIFF y necesita soporte nativo en lugar de un paso de conversión
  • un cliente reporta TIFF teselado de fuentes médicas, GIS o industriales y necesita que se decodifique directamente
  • un cliente reporta TIFF con Predictor 3 de punto flotante
  • una vulnerabilidad publicada afecta las rutas de decodificación CCITT o LZW integradas
  • el argumento multiplataforma deja de aplicar, sea porque el soporte de macOS, iOS y Android se elimina, o porque una integración reutilizable de libtiff ya cubre macOS y Linux

La migración en sí está acotada en lugar de ser hipotética: un condicional USE_LIBTIFF mantendría intacta la superficie pública de TPDFTIFF, enrutaría LoadFromStream a través de TIFFClientOpen con callbacks de flujo, y dejaría el parser de Pascal como respaldo para las plataformas que no son Windows. Hasta que uno de esos disparadores se active de verdad, mantener dos decodificadores y una matriz de pruebas duplicada no compra nada que un cliente pueda percibir. Aplazar un costo con la ruta de escape ya escrita es algo distinto a ignorarlo

Dónde queda una pipeline de documentos escaneados con esto

Trate TPDFTIFF como una compuerta y no como un convertidor. Cargue el archivo, lea ValidTIFF, y registre LastError textualmente siempre que sea falso, porque esa cadena es ahora la ruta más corta de un reporte de campo a un diagnóstico. Los archivos que fallan la compuerta siguen siendo recuperables convirtiéndolos aguas arriba, que es la respuesta práctica para fuentes BigTIFF y teseladas hoy. Para entradas que están fuera de TIFF por completo, PDFlibPas toma una ruta separada a través de su vía de entrada de imágenes AVIF, HEIF y JPEG XL, así que la pregunta de qué decodificador es dueño de qué formato queda explícita en lugar de emergente

Todo esto vive detrás de la API de imágenes ordinaria, así que una pipeline de documentos gana la frontera más estricta sin cambiar una línea de código de llamada más allá de verificar el conteo de páginas que ya debería estar verificando. Si está evaluando una ruta nativa de TIFF a PDF para Delphi o C++Builder, el componente completo y su manejo de imágenes están documentados en la página de PDF Library for Delphi