Uno stream marcato /Predictor 12 non significa che ogni riga usi il filtro PNG 2. HotPDF, il componente VCL PDF nativo per Delphi e C++Builder, tratta i valori predictor da 10 a 15 come un'unica famiglia: il vero tag di filtro, da 0 a 4, è il primo byte di ogni riga codificata, e HPDFDecodePredictor legge e valida quel tag riga per riga. Questa distinzione è la forma di quasi ogni bug in questo angolo di PDF, perché nulla solleva un'eccezione quando la sbagli. La catena di filtri viene eseguita, il raster ha la dimensione attesa, e l'immagine esce come rumore diagonale o un gradiente che deriva sempre di più a ogni scanline. I cinque numeri in /DecodeParms (ISO 32000-1 §7.4.4) per lo più cambiano il significato dei byte anziché la loro lunghezza, quindi uno sbagliato produce spazzatura plausibile invece di un errore
Perché /Predictor 12 non significa filtro PNG 2 su ogni riga?
Perché il numero predictor dice solo "è in uso la predizione PNG", non quale filtro. Gli encoder PNG scelgono un filtro per scanline e il filtro PDF eredita questo, quindi i valori predictor 10 (None), 11 (Sub), 12 (Up), 13 (Average), 14 (Paeth) e 15 (Optimum) decodificano tutti in modo identico: il byte di tag iniziale di ogni riga è ciò a cui il decoder deve obbedire. La conseguenza di layout conta quanto la semantica. Ogni riga codificata è lunga 1 + RowBytes byte, l'input quindi supera l'output esattamente del conteggio delle righe, e uno stream la cui lunghezza non è un multiplo intero di RowBytes + 1 è troncato per definizione. HotPDF verifica quel limite prima di toccare un byte, rifiuta qualunque tag sopra 4 con Invalid PNG predictor row tag, e legge la riga precedente direttamente dall'unico buffer di output invece di materializzare un array di righe bidimensionale. I filtri 1 e 3 tornano indietro di BytesPerPixel all'interno della riga corrente, il filtro 2 legge dritto verso l'alto, il filtro 4 esegue la scelta Paeth su sinistra, alto e alto-sinistra — e tutti e quattro operano su output già ricostruito, motivo per cui la riga superiore deve essere la riga decodificata e mai l'input filtrato
uses
HPDFPredictor;
var
Filtered, Raster: AnsiString;
ErrorText: string;
begin
// /DecodeParms << /Predictor 12 /Colors 3 /BitsPerComponent 8 /Columns 1024 >>
if HPDFTryDecodePredictor(Filtered, 12, 3, 8, 1024,
Int64(1024) * 3 * 8192, Raster, ErrorText) then
ConsumeRaster(Raster)
else
LogStreamDefect('predictor', ErrorText); // no exception, no partial raster
end;
L'argomento MaxOutputBytes non è decorazione. Uno stadio predictor è uno stadio di decompressione travestito, e un valore /Columns ostile o semplicemente rotto trasforma pochi kilobyte di input in una richiesta di allocazione di più gigabyte. HotPDF calcola prima bit per riga, byte per riga e dimensione totale del raster in Int64, rifiuta geometrie che vanno in overflow, e rispetta il limite fornito dal chiamante. Passa un limite reale derivato dal dizionario immagine e la modalità di fallimento diventa un messaggio registrato anziché una finestra di out-of-memory sulla macchina del cliente
Perché TIFF Predictor 2 corrompe le immagini a 4 bit?
Perché Predictor 2 è differenziazione orizzontale per campione, non per byte, e a 1, 2 o 4 bit per componente più campioni condividono un byte. L'implementazione comune somma il byte N-Colors al byte N, il che risulta corretto a 8 bit per componente ed è silenziosamente sbagliato in tutti gli altri casi. Una scansione RGB a 8 bit si decodifica perfettamente, poi lo stesso codice distrugge un'immagine indicizzata a 4 bit la prima volta che ne compare una in produzione
L'aritmetica corretta opera dentro il campo di bit. HotPDF percorre i campioni dall'indice Colors a Colors * Columns - 1, estrae il campione e il suo vicino sinistro dello stesso componente con una maschera di (1 shl BitsPerComponent) - 1 allo shift appropriato, li somma modulo quella maschera, e scrive il risultato senza disturbare gli altri campioni impacchettati nello stesso byte. Anche la coda conta: una riga è riempita fino a un confine di byte, quindi i bit di riempimento dopo l'ultimo campione devono sopravvivere intatti invece di essere piegati nell'aritmetica. A 16 bit per componente ogni campione è una coppia di byte big-endian e la somma va in wrap a $FFFF attraverso la coppia anziché portare il riporto tra i byte indipendentemente; a 8 bit la semplice ricorrenza di byte è corretta, avanzando di Colors così il rosso si accumula contro il rosso e l'alfa contro l'alfa. In ogni variante il primo pixel di una riga è un letterale, mai una differenza, e la ricorrenza riparte a ogni confine di riga — la predizione TIFF non legge mai la riga sopra, il che è l'intera differenza tra essa e la famiglia PNG
Cosa controlla realmente EarlyChange in LZWDecode?
Controlla quando il lettore allarga la dimensione del codice di un bit, ed essere sfasati di un codice corrompe tutto ciò che segue. HotPDF esprime la regola come un unico invariante: dopo aver aggiunto una voce al dizionario, la lettura successiva si allarga quando NextCode raggiunge (1 shl CodeSize) - Ord(EarlyChange). Con /EarlyChange 1, il default di ISO 32000-1 §7.4.4, il cambio avviene un codice prima; con /EarlyChange 0 avviene esattamente al confine. Entrambi compaiono in file reali e nulla nel bitstream ti dice quale abbia usato l'encoder. Il resto della macchina a stati deve muoversi in sincronia: un codice clear azzera insieme la dimensione del codice, la maschera di bit, il prossimo codice libero e lo storage delle frasi, e il codice di fine informazione viene letto a qualunque larghezza sia corrente in quel momento, non ai 9 bit iniziali. HotPDF parte da InitialCodeSize 9, limita la dimensione del codice a 12 e il dizionario a 4096 voci, e imposta FillOrder di default a foTop perché PDF impacchetta i codici con il bit di ordine superiore per primo — foBottom esiste per gli stream in stile TIFF che non lo fanno
uses
HPDFLZW;
var
Decoder: TPDFLZWDecompressor;
Parms: TPDFLZWParms;
Plain: AnsiString;
begin
Decoder := TPDFLZWDecompressor.Create;
try
Decoder.EarlyChange := True; // /EarlyChange 1 is the PDF default
Decoder.FillOrder := foTop; // high-order bit first
Decoder.MaxOutputBytes := 256 * 1024 * 1024;
Decoder.RequireInitialClear := False;
Decoder.RequireEndOfInformation := False;
Parms.Predictor := 12;
Parms.Colors := 3;
Parms.BitsPerComponent := 8;
Parms.Columns := 1024;
Parms.ExpandedTo8Bit := False;
Parms.ColorSpace := 'DeviceRGB';
if Decoder.TryDecompress(RawStreamBytes, Parms, Plain) then
LogDecodeStats(Decoder.PeakCodeSize, Decoder.DictionaryAdds,
Decoder.KwKwKExpansions, Decoder.OutputBytes)
else
LogStreamDefect('lzw', Decoder.LastError);
finally
Decoder.Free;
end;
end;
Le statistiche esistono per il triage, non per vanità. Quando un file si decodifica alla lunghezza giusta ma con i pixel sbagliati, PeakCodeSize e DictionaryAdds ti dicono immediatamente se il lettore ha mai allargato dove lo ha fatto lo scrittore. Inverti EarlyChange, decodifica di nuovo, confronta i due: se i numeri si muovono, hai la risposta in una sola esecuzione invece di seguire passo passo un lettore di bit
Il ramo KwKwK, e quando uno stream dovrebbe semplicemente fallire
L'unico caso legale che sembra illegale è Code = NextCode, e HotPDF lo gestisce costruendo la voce prima di emetterla. Un encoder può emettere il codice per una frase che sta definendo nello stesso passo, il che accade ogni volta che l'input contiene un pattern della forma K w K w K; il decoder non può cercare quel codice perché non esiste ancora, quindi deve costruire Previous + First(Previous), aggiungerlo come nuova voce, ed emettere la voce che ha appena creato. HotPDF conta questi casi in KwKwKExpansions e verifica incrociatamente che il codice aggiunto sia il codice richiesto. Tutto ciò che è sopra NextCode è corruzione, e lì un decoder dovrebbe fermarsi anziché improvvisare: HotPDF solleva un'eccezione su un codice futuro, su un prefisso di dizionario che punta fuori dall'arena delle frasi, su un dizionario pieno, e su un primo codice che non è un letterale. Due interruttori di rigore sono deliberatamente spenti di default, RequireInitialClear e RequireEndOfInformation, perché molti PDF di produzione omettono il codice clear iniziale o esauriscono i dati senza un terminatore. Attivali quando validi il tuo output, lasciali spenti quando consumi file dal mondo reale
Dove /DecodeParms viene realmente letto sul lato documento caricato
HotPDF risolve /DecodeParms o la sua abbreviazione /DP sul dizionario dello stream immagine, accetta sia un dizionario sia un array e prende l'ultimo elemento quando è un array, poi porta Predictor, Colors, BitsPerComponent, Columns ed EarlyChange nel percorso del raster. Il caso array è quello che le persone dimenticano: uno stream filtrato da [/ASCII85Decode /FlateDecode] porta un array di parametri parallelo, e le impostazioni predictor appartengono all'ultimo filtro, non al primo
var
Pdf: THotPDF;
Info: THPDFLoadedImageInfo;
Bmp: TBitmap;
I: Integer;
begin
Pdf := THotPDF.Create(nil);
try
if Pdf.LoadFromFile('scanned.pdf', '') > 0 then
for I := 0 to Pdf.GetLoadedImageCount - 1 do
if Pdf.GetLoadedImageInfo(I, Info) then
begin
Bmp := Pdf.ExtractLoadedImage(I); // nil when the raster is unusable
if Bmp <> nil then
try
Bmp.SaveToFile(Format('image-%d.bmp', [I]));
finally
Bmp.Free;
end;
end;
finally
Pdf.Free;
end;
end;
Vale la pena citare un difetto storico su quel percorso, perché quella classe di bug si ripete. La vecchia routine Flate-con-parametri creava uno stream di decompressione e poi copiava dall'input compresso originale, così lo stadio predictor riceveva byte compressi e li de-predeva diligentemente: sempre sbagliato, mai un errore sollevato. Il codice attuale legge solo dal decoder prima di consegnare il risultato al predictor condiviso, e rifiuta un raster più corto della dimensione calcolata invece di ricadere sui byte ancora compressi — un fallback che un tempo trasformava un fallimento di decodifica in un bitmap corrotto. Quella stessa implementazione del predictor serve ora anche gli stream di cross-reference, il che è una coerenza utile se lavori anche con object stream e aggiornamenti incrementali, e il meccanismo di estrazione circostante è trattato nel pezzo di accompagnamento su l'estrazione di immagini caricate e i loro filtri di decodifica. Le immagini che arrivano come DCTDecode o JPXDecode non raggiungono affatto il predictor; portano il proprio modello di pixel compresso
Throughput: un'arena di frasi contigua contro stringhe per voce
Sostituire il dizionario di stringhe per voce con un'arena di frasi contigua è risultato circa 1,61 volte più veloce su un input patologico: 1558 MiB/s contro 969 MiB/s su un benchmark la cui singola frase più lunga raggiunge 7.370.880 byte. La forma di quell'input spiega il divario, perché le implementazioni classiche scelgono uno di due cattivi compromessi. Un dizionario di valori AnsiString alloca e copia una stringa nuova per ognuna delle fino a 4096 voci, ogni nuova voce copiando per intero il proprio genitore; uno stack prefisso/suffisso evita del tutto quella memoria ma ricostruisce ogni frase percorrendo la catena all'indietro un byte alla volta e invertendola, il che va bene per testo ordinario ed è doloroso quando una frase arriva a megabyte. HotPDF accoda ogni frase contiguamente a un'arena che cresce geometricamente, indicizza le voci per offset e lunghezza, ed emette una frase con un singolo Move nel buffer di output. Il costo onesto è la memoria: un'arena che contiene ogni frase per intero è limitata dalla somma di tutte le lunghezze delle frasi anziché dal conteggio delle voci, il che è esattamente il motivo per cui MaxOutputBytes esiste sia sul decompressore sia sul predictor. Deriva quel limite da ciò che il dizionario immagine dichiara dovrebbe essere il raster, e uno stream bugiardo fallisce rapidamente
Il decompressore LZW, il predictor condiviso e il percorso di estrazione delle immagini caricate mostrati qui vengono forniti come parte del componente standard HotPDF Component per Delphi e C++Builder, con il riferimento completo di filtri e DecodeParms sulla pagina prodotto