HotPDF 2.747.0 decodifica le immagini WebP con un decoder VP8L (WebP lossless) scritto da zero in Object Pascal, così THotPDF.AddImageFromFile accetta direttamente un percorso .webp, senza distribuire una DLL libwebp né avviare un processo helper. Il decoder implementa integralmente la sezione 3 di RFC 9649: analisi del container RIFF, prefix code canonici, riferimenti backward LZ77, color cache e tutte e quattro le inverse transform. I frame VP8 lossy vengono rifiutati esplicitamente invece di essere decodificati a metà
Il trigger era banale. Un design tool esporta ogni asset come WebP perché è il default moderno, gli asset finiscono in un generatore di fatture o cataloghi che ha consumato PNG e JPEG per un decennio e improvvisamente metà degli input viene rifiutata. La correzione ovvia è collegare libwebp e andare avanti. È anche la correzione che trasforma un componente VCL autonomo in qualcosa con una storia di deployment da gestire
Perché implementare VP8L invece di collegare libwebp?
HotPDF implementa il codec in Pascal perché un componente Delphi che i clienti compilano nel proprio eseguibile non può acquisire silenziosamente una DLL runtime. Una dipendenza nativa significa seguire un binario a 32 bit e uno a 64 bit, fissare una versione, spiegare una catena di code signing a chi esegue il deployment e aggiungere un file che un antivirus su un terminale bloccato potrebbe decidere di non gradire. Per un componente il cui punto di forza principale è essere inserito in un progetto e funzionare, è un costo reale, non teorico. L'altra metà dell'argomento è che VP8L è piccolo: un formato a prefix code più LZ77 con quattro inverse transform e una mappa di distanza neighborhood da 120 entry, e l'intero decoder in HPDFWebP.pas è sotto le 900 righe di Pascal. Dentro THotPDF.AddImage il ramo WebP si trova nello stesso dispatch per estensione che instrada già .jp2, .j2k, .jpt e .jpc verso il percorso JPEG 2000, proprio nel punto descritto dalla guida su aggiungere immagini JPEG 2000 ai PDF in Delphi. I caller che vogliono pixel raw invece di un'immagine PDF possono chiamare direttamente HPDFDecodeWebPLossless, che riempie un TWebPCardinalArray di valori $AARRGGBB in ordine di scan line
uses
HPDFDoc, HPDFWebP;
var
Pdf: THotPDF;
Idx: Integer;
begin
Pdf := THotPDF.Create(nil);
try
Pdf.FileName := 'catalog.pdf';
Pdf.BeginDoc;
// .webp viene instradato al decoder VP8L integrato, senza DLL
Idx := Pdf.AddImageFromFile('product-shot.webp', icFlate);
Pdf.CurrentPage.ShowImage(Idx, 50, 500, 240, 180, 0);
Pdf.EndDoc;
finally
Pdf.Free;
end;
end;
Perché un bitstream VP8L si legge in due direzioni contemporaneamente?
Perché l'ordine dei bit del container e quello dei bit dei prefix code sono specificati separatamente, e VP8L sceglie convenzioni opposte. La sezione 3.2 di RFC 9649 lo dice chiaramente: il bitstream viene letto least significant bit first, il reader parte dal bit 0 di un byte e procede verso l'alto. I canonical prefix code contenuti nello stream arrivano most significant bit first, dalla radice dell'albero, quindi il percorso di decode sposta l'accumulatore a sinistra e fa OR di ogni nuovo bit in fondo. Reader e code walk procedono quindi in direzioni opposte dentro lo stesso loop, cosa che sembra un bug ogni volta che si rilegge il codice
function TWebPBitReader.ReadBit: Integer;
begin
if BytePos >= Length(Data) then
raise EWebPDecode.Create('WebP bitstream exhausted');
Result := (Data[BytePos] shr BitPos) and 1; // LSB first, RFC 9649 3.2
Inc(BitPos);
if BitPos = 8 then
begin
BitPos := 0;
Inc(BytePos);
end;
end;
// il percorso canonico va nell'altra direzione: il primo bit dello stream
// è il bit più significativo del codice
for Len := 1 to 15 do
begin
Code := (Code shl 1) or BR.ReadBit;
if Counts[Len] > 0 then
begin
if Code - First < Counts[Len] then
Exit(Symbols[Index + Code - First]);
First := (First + Counts[Len]) shl 1;
Index := Index + Counts[Len];
end
else
First := First shl 1;
end;
Tre dettagli RFC che desincronizzano silenziosamente lo stream
Tre semantiche di RFC 9649 sono dichiarate una sola volta, facili da saltare con gli occhi, e ciascuna costa o risparmia un singolo bit, abbastanza per trasformare ogni tabella successiva in rumore. Tutte e tre sono state trovate nel decoder VP8L di HotPDF e tutte producono lo stesso sintomo: un'immagine dall'aspetto plausibile ma sbagliata ovunque
- Un'immagine entropy-coded in un ruolo non primario non scrive alcun meta-prefix bit. L'ABNF di
entropy-coded-imagenon contiene semplicemente quell'elemento, quindi leggerne uno desincronizza lo stream di un bit. HotPDF passaAllowMeta = Falseper la entropy image stessa, per i dati di predictor e color transform e per la palette di color-indexing - Un prefix code con una sola foglia consuma zero bit. RFC 9649 section 3.7.2.1 lo dice direttamente, e il percorso canonico leggerebbe volentieri un bit per poi non riuscire a collocarlo, quindi
BuildHuffrileva un conteggio totale di simboli pari a 1, marca l'alberoSinglee decodifica quell'unico simbolo senza toccare il reader - Un valore cache_bits pari a 0 significa che la color cache ha dimensione 0, non
1 shl 0. Lo shift comodo produce 1, facendo risultare l'alfabeto verde256 + 24 + CacheSizeuguale a 281 invece che 280, e ogni lettura successiva della tabella dei prefix code si disallinea
CacheBits := 0;
CacheSize := 0; // cache_bits = 0 significa davvero nessuna cache
if BR.ReadBit = 1 then
begin
CacheBits := Integer(BR.ReadBits(4));
if (CacheBits < 1) or (CacheBits > 11) then
raise EWebPDecode.Create('WebP color cache bits out of range');
CacheSize := 1 shl CacheBits;
end;
// RFC 9649 3.8.3: solo l'immagine codificata spazialmente (ARGB) porta il
// meta prefix bit; i ruoli entropy-coded non lo scrivono mai
if AllowMeta then
UseMeta := BR.ReadBit
else
UseMeta := 0;
// ...
ReadHuffCode(256 + 24 + CacheSize, Groups[I].Green); // 280, non 281
ReadHuffCode(256, Groups[I].Red);
ReadHuffCode(256, Groups[I].Blue);
ReadHuffCode(256, Groups[I].Alpha);
ReadHuffCode(40, Groups[I].Dist);
Nel fixture usato durante il bring-up i tre casi sono emersi ai bit 47, 81 e 89, in quest'ordine. Questi numeri sono il punto della sezione. Nessuno dei tre si è annunciato come off-by-one; ciascuno si è presentato come un'immagine decodificata fino alla fine e dall'aspetto statico, e l'unica cosa che li distingueva era la posizione esatta del bit in cui lo stream smetteva di concordare con un riferimento
Cosa offre il diff della posizione dei bit?
Il diff della posizione dei bit trasforma una domanda inutile in una domanda di una riga: non "perché questa immagine è sbagliata", ma "perché lo stream diverge al bit 81". La preparazione è economica. Pillow scrive ogni fixture .webp più un dump .rgba del proprio decode della stessa immagine; una probe Pascal e un piccolo modello di riferimento Python registrano un contatore di bit progressivo accanto a ogni lettura; la prima posizione in cui i due log divergono è il punto in cui vive il bug. Iniziate con un fixture che eserciti il meno possibile: un'immagine piatta 32x32 che percorra solo il simple-code path. Fatela funzionare, poi aggiungete gradienti, dimensioni dispari e alpha, un fixture alla volta. Indovinare l'ordine dei bit è invece un modo per spendere una giornata
La cautela onesta è che anche il reference era sbagliato. Il modello Python aveva dimenticato di leggere cache_bits e il suo transform loop non arrivava alla fine, quindi alcuni punti di divergenza erano il reference decoder che perdeva la sincronizzazione, non quello Pascal. Un'implementazione di riferimento errata non rende corretta quella sotto test, e nessuno dei due lati riceve il beneficio del dubbio: ogni divergenza va giudicata rispetto al testo RFC. Anche quel testo va preso dalla fonte. I riepiloghi delle ricerche deformano spesso le tabelle numeriche, e la mappa di distanza a 120 entry, le 14 modalità di predictor e il moltiplicatore della color cache $1e35a7bd devono essere trascritti esattamente
Dove la divisione intera Pascal diverge da C
La color transform VP8L è a fixed point 3.5 con delta signed, ed è qui che Pascal e C smettono di concordare. C esegue shift aritmetici degli interi negativi, che fanno floor; il div Pascal tronca verso zero. Per qualsiasi prodotto negativo i due risultati differiscono di uno, quindi l'inverse color transform devia di un passo di canale per pixel sull'intera immagine. HotPDF rende quindi esplicito il floor in FloorDiv32 invece di affidarsi a div
// C esegue shift aritmetici e fa floor sui negativi; div Pascal tronca
// verso zero, quindi il caso negativo richiede una correzione esplicita
function FloorDiv32(V: Integer): Integer;
begin
Result := V div 32;
if (V < 0) and (V mod 32 <> 0) then
Dec(Result);
end;
// delta fixed point 3.5 tra il byte di un elemento di transform e il
// byte di un canale colore, entrambi prima sign-extended
function ColorDelta(T, C: Integer): Integer;
var
T8, C8: Integer;
begin
T8 := T;
if T8 >= 128 then
Dec(T8, 256);
C8 := C;
if C8 >= 128 then
Dec(C8, 256);
Result := FloorDiv32(T8 * C8);
end;
Vale la pena dare un nome a questa classe di difetto perché è invisibile a qualsiasi test i cui fixture producano per caso solo prodotti non negativi, dove div e floor coincidono. È anche il motivo per cui i test WebP HotPDF verificano l'uguaglianza esatta dei pixel rispetto ai decode Pillow degli stessi file invece di una tolleranza: gradienti, una dimensione dispari 100x37, un'immagine 40x40 con un canale alpha reale e un'immagine piatta 32x32, ogni pixel confrontato bit per bit. Una deviazione di un passo supera un controllo percettivo e fallisce quello bitwise
Cosa rifiuta deliberatamente il supporto WebP
HotPDF decodifica il primo chunk VP8L di un file WebP e nient'altro. Frame VP8 lossy, animazioni e qualsiasi container il cui chunk corrispondente non sia VP8L restituiscono False da HPDFDecodeWebPLossless, e AddImage lo trasforma in un'eccezione che nomina il file: Failed to decode WebP image (lossless VP8L only). È un confine deliberato, non una dimenticanza: un file del formato sbagliato deve fallire dove il caller può pre-convertirlo, invece di produrre un rettangolo grigio. Il version field deve essere 0, lo stack delle transform è limitato a quattro entry e ogni violazione dei bounds genera EWebPDecode, che l'entry point pubblico converte in un semplice False. Decodificare durante l'import è anche la direzione opposta rispetto all'estrazione delle immagini da un documento aperto, che passa dal loaded-image path descritto in estrarre immagini da un PDF caricato e dai loro decode filter. Inoltre qualsiasi image decoder è un parser alimentato da file che non avete creato: se gli asset WebP arrivano da clienti o da Internet, i controlli dei bounds qui sono il minimo, non il massimo, e la risposta più forte è eseguire i codec immagine in un worker process isolato, così un frame malformato non può abbattere l'host
Il risultato pratico è che un'applicazione Delphi o C++Builder può inserire asset WebP in un PDF nello stesso modo in cui inserisce PNG: una chiamata a AddImageFromFile, una chiamata a ShowImage, niente di extra nell'installer. Se volete il resto della pipeline immagini e documenti che la circonda, il componente PDF Delphi HotPDF copre scrittura, caricamento e rendering dallo stesso insieme di unità