PDFlibPas decodifica TIFF con un analizador escrito a mano en Object Pascal en lugar de un binding de libtiff, y la versión 3.534.1 ha endurecido exactamente los puntos en los que ese analizador rechaza la entrada. El magic 43 de BigTIFF se rechaza ahora mencionándolo por su nombre, TileOffsets y TileByteCounts se rechazan 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 en una pasarela de escaneo que ha funcionado en silencio durante tres años, hasta que un cliente le pasa un archivo geoespacial o una imagen médica de portaobjetos completo. El archivo tiene una cabecera TIFF legítima. Se analiza sin errores. Lo que sale es una página de ruido rayado, o una asignación de varios gigabytes que tumba el servicio, y en ningún momento del proceso alguien declaró inválida la entrada. Esa es la forma de fallo contra la que merece la pena ingeniar: no un cuelgue, sino una respuesta incorrecta entregada con total seguridad
¿Por qué II o MM no demuestran que tienes un TIFF clásico?
Porque el marcador de orden de bytes es común a ambos dialectos. El TIFF clásico y BigTIFF abren 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 desplazamientos de 64 bits. Un cargador 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 cambió la extensión. PDFlibPas separa ahora los casos y registra cada uno en TPDFTIFF.LastError: una cabecera de menos de cuatro bytes, un marcador de orden de bytes no válido, el magic 43 y cualquier otro valor de magic producen textos distintos. La biblioteca sigue sin decodificar BigTIFF, y decirlo con claridad es precisamente el punto. Quien llama recibe la diferencia entre «esto no es un TIFF» y «esto es un TIFF cuya disposición de desplazamientos de 64 bits el decodificador integrado no implementa», que es la diferencia entre un ticket de soporte que respondes en un mensaje y uno que se convierte en una semana de adivinanzas
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 array de desplazamientos
PDFlibPas rechaza el TIFF teselado mientras analiza las etiquetas, antes de tocar un solo dato de píxel. El atajo que invita al error es fácil de ver: las etiquetas 324 (TileOffsets) y 325 (TileByteCounts) son arrays de desplazamientos de archivo y de recuentos de bytes, estructuralmente idénticos a los arrays de bandas, así que apuntar los campos de banda existentes hacia ellos cuesta dos líneas y compila limpiamente. También es incorrecto. Los mosaicos forman una cuadrícula bidimensional con bloques de borde rellenados, 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 un decodificador de bandas con datos de mosaico, por tanto, no falla de forma ruidosa. SimpleExtract y CompDecode recorren los datos con el paso equivocado y emiten una imagen con las dimensiones correctas y los píxeles equivocados. El código anterior agravaba esto manteniendo StripsAreTiles, ColumnsPerTile y RowsPerTile en TTIFFPage: geometría de mosaico registrada por un decodificador sin ningún ensamblador de mosaicos detrás. En 3.534.1 los gestores 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 aflorar semanas después como una queja de renderizado
Un límite de dimensión no es un presupuesto de memoria
Limitar el ancho y el alto a 65.535 cada uno es necesario y de ninguna manera suficiente, porque la magnitud 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 los factores alcance su propio límite, e incluso sin desbordamiento puede pedir una asignación que ningún servicio debería intentar. PDFlibPas calcula los bytes por fila en Int64 y aplica tres techos a la vez: 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 de ahí importan más que las propias constantes. La prueba del alto se escribe como una división y no como una multiplicación, de modo que el producto desbordado nunca llega a formarse. Un RowsPerStrip por debajo de 1 o por encima de la altura de la imagen se normaliza primero a la altura, que es la lectura de banda única que TIFF 6.0 ya implica y que impide que una etiqueta hostil hinche el búfer de banda. Y la rutina es compartida: ValidatePageForDecode se ejecuta al final del análisis de etiquetas y de nuevo en la entrada tanto de SimpleExtract como de CompDecode, así que el código que llega directamente a un decodificador no puede rodear el presupuesto. Es la misma regla que PDFlibPas aplica al analizar grafos de objetos PDF no confiables, porque un límite aplicado en una puerta de tres no es un límite
¿Qué debe comprobar quien llama antes de leer PageInfo?
Comprueba primero ValidTIFF, después PageCount, y solo entonces indexa 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 recuentos de muestras de camino al informe del fallo acaba leyendo ruido. La versión 3.534.1 corrigió ambos llamadores 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 fiarse por sí solo de un recuento de páginas distinto de cero. Aguas abajo, el argumento Options de AddImageFromFile es el número de página base 1 de un TIFF multipágina, así que GetImagePageCount tiene que ser fiable antes de que el bucle empiece y no después. Cero páginas es ahora una respuesta real que significa «aquí no hay nada decodificable», y no un accidente de un retorno temprano, lo cual importa sobre todo cuando estás intercalando y entrelazando lotes de escaneo dúplex y una hoja decodificada en silencio de forma incorrecta acabarí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; // cabecera no válida, BigTIFF, disposición teselada o presupuesto superado
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 unas 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 sería canjear la cobertura TIFF completa por una lista de plataformas soportadas que se reduce a la máquina que pueda ejecutar la cadena de herramientas de C, más un paso de enlazado que nadie ha recorrido todavía
Lo que eso cuesta merece enunciarse sin adornos. El decodificador integrado maneja lo que el trabajo con documentos escaneados produce realmente: CCITT Grupo 3 unidimensional y bidimensional, Grupo 4, LZW, Deflate, PackBits y JPEG en TIFF, sobre fotometrías WhiteIsZero, BlackIsZero, RGB, paleta y CMYK con Predictor 1 y 2. Esas cargas 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 canalización de escaneo. Lo que no maneja es BigTIFF, los mosaicos, el Predictor 3 de coma flotante, PixarLog y SGILog, la compresión JPEG antigua 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 sobre libtiff:
- un cliente informa de un archivo BigTIFF y necesita soporte nativo en lugar de un paso de conversión
- un cliente informa de TIFF teselado procedente de fuentes médicas, SIG o industriales y necesita decodificarlo en el sitio
- un cliente informa de TIFF con Predictor 3 de coma flotante
- una vulnerabilidad publicada afecta a las rutas de decodificación CCITT o LZW integradas
- el argumento multiplataforma deja de aplicarse, ya sea porque el soporte de macOS, iOS y Android se retira, o porque una integración reutilizable de libtiff ya cubre macOS y Linux
La propia migración 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 analizador Pascal como alternativa fuera de 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 coste con la ruta de escape ya escrita es algo muy distinto de ignorarlo
Dónde deja esto a una canalización de documentos escaneados
Trata TPDFTIFF como una puerta y no como un convertidor. Carga el archivo, lee ValidTIFF y registra LastError textualmente siempre que sea falso, porque esa cadena es ahora la ruta más corta de un informe de campo a un diagnóstico. Los archivos que no pasan la puerta siguen siendo recuperables convirtiéndolos aguas arriba, que es la respuesta práctica hoy para fuentes BigTIFF y teseladas. Para entradas que están fuera del 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, de modo que la pregunta de qué decodificador es dueño de qué formato sigue siendo explícita y no emergente
Todo esto queda detrás de la API de imágenes ordinaria, así que una canalización de documentos gana el perímetro más estricto sin cambiar ni una línea de código llamador, más allá de comprobar el recuento de páginas que ya debería estar comprobando. Si estás sopesando 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