PDFlibPas decodifica i file TIFF con un parser Object Pascal scritto a mano invece di un binding a libtiff, e la versione 3.534.1 ha irrigidito proprio i punti in cui quel parser rifiuta l'input. Il magic 43 di BigTIFF viene ora rifiutato per nome, TileOffsets e TileByteCounts vengono respinti durante il parsing dei tag, e ogni buffer viene dimensionato con aritmetica Int64 sotto un tetto di decodifica di 256 MiB
Il difetto che questa correzione elimina non si manifesta mai in laboratorio. Si manifesta come un gateway di scansione rimasto attivo in silenzio per tre anni, finché un cliente non vi instrada un archivio geospaziale o un'immagine medica whole-slide. Il file ha un header TIFF legittimo. Si analizza senza errori. Quello che esce è una pagina di rumore a strisce, oppure un'allocazione da più gigabyte che manda giù il servizio, e lungo tutta la strada nessuno ha mai dichiarato invalido l'input. È questa la forma di guasto contro cui vale la pena progettare: non un crash, ma una risposta sbagliata consegnata con sicurezza
Perché II o MM non dimostrano di avere un TIFF classico?
Perché il marcatore di ordine dei byte è condiviso da entrambi i dialetti. Il TIFF classico e BigTIFF aprono entrambi con II o MM, e il campo che li distingue davvero è il magic a 16 bit immediatamente successivo: 42 per il TIFF classico come definito nella specifica TIFF 6.0, 43 per BigTIFF con i suoi offset a 64 bit. Un loader scritto come FValidTIFF := PopWord = 42 non sbaglia sul TIFF classico, ma comprime due rifiuti molto diversi in un solo booleano silenzioso, così un BigTIFF diventa indistinguibile da un JPEG troncato rinominato da qualcuno. PDFlibPas ora separa i casi e registra ciascuno in TPDFTIFF.LastError: un header più corto di quattro byte, un marcatore di ordine dei byte non valido, il magic 43 e qualsiasi altro valore di magic producono tutti testi distinti. La libreria continua a non decodificare BigTIFF, e dirlo apertamente è proprio il punto. Il chiamante ottiene la differenza tra "questo non è un TIFF" e "questo è un TIFF il cui layout di offset a 64 bit il decodificatore integrato non implementa", che è la differenza tra un ticket di supporto a cui si risponde in una battuta e uno che diventa una settimana di congetture
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;
I tile sono una geometria diversa, non un altro array di offset
PDFlibPas respinge i TIFF a tile durante il parsing dei tag, prima ancora di toccare dati di pixel. La scorciatoia che invita il bug è facile da vedere: il tag 324 (TileOffsets) e il tag 325 (TileByteCounts) sono array di offset di file e conteggi di byte, strutturalmente identici agli array di strip, quindi puntare i campi strip esistenti verso di essi costa due righe e compila pulito. È comunque sbagliato. I tile formano una griglia bidimensionale con blocchi di bordo riempiti, il proprio passo di riga all'interno di ogni tile e nessuna semantica RowsPerStrip, come spiega la sezione sulle immagini a tile di TIFF 6.0. Dare payload di tile a un decodificatore di strip quindi non fallisce rumorosamente. SimpleExtract e CompDecode percorrono i dati con il passo sbagliato e producono un'immagine con le dimensioni giuste e i pixel sbagliati. Il codice più vecchio aggravava la cosa tenendo StripsAreTiles, ColumnsPerTile e RowsPerTile in TTIFFPage: geometria dei tile registrata da un decodificatore senza alcun assemblatore di tile dietro. Nella 3.534.1 i gestori dei tag 324 e 325 sollevano l'errore di tile e abbandonano subito l'IFD, così il rifiuto contiene la parola "tile" invece di emergere settimane dopo come un reclamo di rendering
Un limite su una dimensione non è un budget di memoria
Limitare larghezza e altezza a 65.535 ciascuna è necessario e tutt'altro che sufficiente, perché la grandezza che guida l'allocazione è un prodotto. RowsPerStrip * Width * SamplesPerPixel può traboccare l'aritmetica a 32 bit molto prima che uno dei fattori raggiunga il proprio limite, e persino senza traboccamento può indicare un'allocazione che nessun servizio dovrebbe tentare. PDFlibPas calcola i byte di riga in Int64 e impone tre tetti insieme: 65.535 per dimensione, 32 componenti colore e 256 MiB di byte decodificati
const
PDFLIB_TIFF_MAX_IMAGE_DIM = 65535;
PDFLIB_TIFF_MAX_COLOR_COMPONENTS = 32;
PDFLIB_TIFF_MAX_DECODED_IMAGE_BYTES = 256 * 1024 * 1024;
// dentro 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);
Tre dettagli lì contano più delle costanti stesse. Il test sull'altezza è scritto come divisione anziché come moltiplicazione, così il prodotto sovradimensionato non viene mai formato. Un RowsPerStrip sotto 1 o sopra l'altezza dell'immagine viene normalizzato all'altezza, che è la lettura a strip singola che TIFF 6.0 implica già e che impedisce a un tag ostile di gonfiare il buffer di strip. E la routine è condivisa: ValidatePageForDecode gira alla fine del parsing dei tag e di nuovo all'ingresso sia di SimpleExtract sia di CompDecode, così il codice che raggiunge un decodificatore direttamente non può aggirare il budget. È la stessa regola che PDFlibPas segue quando analizza grafi di oggetti PDF non fidati, perché un limite fatto rispettare su una porta su tre non è un limite
Cosa deve controllare il chiamante prima di leggere PageInfo?
Controllare prima ValidTIFF, poi PageCount, e solo allora indicizzare PageInfo. Un file rifiutato può lasciare PageCount a zero, e GetPageInfo risponde a un indice fuori intervallo con un record TTIFFPage non inizializzato, così un percorso di errore che legge risoluzione o conteggi di campioni mentre riporta il guasto finisce per leggere rumore. La versione 3.534.1 ha corretto entrambi i chiamanti dentro la libreria: il percorso di importazione immagini legge XRes e YRes solo dentro il ramo valido, e TPDFlib.GetImagePageCount richiede ValidTIFF invece di fidarsi da solo di un conteggio pagine diverso da zero. A valle, l'argomento Options di AddImageFromFile è il numero di pagina a base 1 per un TIFF multipagina, quindi GetImagePageCount dev'essere affidabile prima che il ciclo parta, non dopo. Zero pagine è ora una risposta vera che significa "qui non c'è nulla di decodificabile", non un incidente di un return anticipato, e conta di più quando si stanno collazionando e intercalando lotti di scansione duplex e un foglio decodificato male in silenzio finirebbe nella posizione sbagliata
var
Pdf: TPDFlib;
Pages, I, ImageID: Integer;
begin
Pdf := TPDFlib.Create;
try
Pages := Pdf.GetImagePageCount('inbox\scan-0417.tif');
if Pages < 1 then
Exit; // header difettoso, BigTIFF, layout a tile o oltre budget
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;
Costruire il decodificatore o collegare libtiff?
PDFlibPas mantiene il decodificatore integrato, e il fattore decisivo è la copertura di piattaforme, non la paternità. Circa 1.873 righe di Object Pascal compilano ovunque arrivi il compilatore: Win32, Win64, macOS, iOS, Android e FPC su Linux. libtiff 4.7.1 sono circa 30.000 righe di C sparse su 34 unità di traduzione tif_*.c, e i file oggetto precompilati esistenti oggi coprono solo Windows. Adottarlo significherebbe scambiare la copertura TIFF completa per una lista di piattaforme supportate che si restringe alle macchine capaci di far girare la toolchain C, più un passaggio di link che nessuno ha ancora percorso
Quel costo merita di essere enunciato senza retorica. Il decodificatore integrato gestisce quello che il lavoro sui documenti scansionati produce davvero: CCITT Group 3 monodimensionale e bidimensionale, Group 4, LZW, Deflate, PackBits e JPEG-in-TIFF, attraverso i fotometrici WhiteIsZero, BlackIsZero, RGB, palette e CMYK con Predictor 1 e 2. Quei payload coincidono con i filtri PDF di ISO 32000-1 §7.4.4 e §7.4.6, ed è per questo che il front-end TIFF pesa così tanto in una pipeline di scansione. Quello che non gestisce è BigTIFF, i tile, il Predictor 3 in virgola mobile, PixarLog e SGILog, la compressione JPEG di vecchio tipo 6 e le piramidi di sub-IFD. Dal 3.534.1 ciascuno di questi è un rifiuto con nome invece di un'immagine sbagliata, e la libreria conserva una lista scritta di inneschi per riaprire la decisione su libtiff:
- un cliente segnala un file BigTIFF e ha bisogno di supporto nativo anziché di un passaggio di conversione
- un cliente segnala TIFF a tile da fonti mediche, GIS o industriali e lo vuole decodificato sul posto
- un cliente segnala TIFF con Predictor 3 in virgola mobile
- una vulnerabilità pubblicata atterra sui percorsi di decodifica CCITT o LZW integrati
- l'argomento multipiattaforma cessa di valere, o perché il supporto a macOS, iOS e Android viene abbandonato, o perché un'integrazione riutilizzabile di libtiff copre già macOS e Linux
La migrazione in sé ha un perimetro, non è ipotetica: un condizionale USE_LIBTIFF manterrebbe intatta la superficie pubblica di TPDFTIFF, instraderebbe LoadFromStream attraverso TIFFClientOpen con callback di stream, e lascerebbe il parser Pascal come ripiego per i non-Windows. Finché uno di quegli inneschi non scatta davvero, mantenere due decodificatori e una matrice di test raddoppiata non compra nulla di percepibile per un cliente. Rimandare un costo con la via di fuga già scritta è un'altra cosa rispetto a ignorarlo
Dove questo lascia una pipeline di documenti scansionati
Trattare TPDFTIFF come un varco anziché come un convertitore. Caricare il file, leggere ValidTIFF, e registrare LastError alla lettera ogni volta che è falso, perché quella stringa è ora il percorso più corto da una segnalazione sul campo a una diagnosi. I file che non passano il varco restano recuperabili convertendoli a monte, che oggi è la risposta pratica per sorgenti BigTIFF e a tile. Per input completamente fuori dal TIFF, PDFlibPas prende una strada separata attraverso il suo percorso di input immagini AVIF, HEIF e JPEG XL, così la domanda di quale decodificatore possieda quale formato resta esplicita invece di emergere
Tutto questo sta dietro la normale API immagini, così una pipeline di documenti ottiene il confine più stretto senza cambiare una riga di codice chiamante oltre al controllo del conteggio pagine che avrebbe già dovuto fare. Se state valutando un percorso nativo da TIFF a PDF per Delphi o C++Builder, il componente completo e il suo trattamento delle immagini sono documentati nella pagina della PDF Library per Delphi