HotPDF 2.747.0 dekóduje obrázky WebP decoderem VP8L (WebP lossless) napsaným od nuly v Object Pascalu, takže THotPDF.AddImageFromFile přijímá cestu .webp přímo, bez DLL libwebp k distribuci a bez pomocného procesu ke spuštění. Decoder plně implementuje RFC 9649, část 3: průchod RIFF kontejnerem, kanonické prefixové kódy, zpětné reference LZ77, color cache a všechny čtyři inverzní transformace. Lossy VP8 frames se odmítnou nahlas místo napůl provedeného dekódování
Spouštěč byl všední. Designový nástroj exportuje každý asset jako WebP, protože je to moderní výchozí volba, assety se dostanou do generátoru faktur nebo katalogů, který deset let bez problémů jedl PNG a JPEG, a najednou je polovina vstupů odmítnuta. Očividná oprava je napojit libwebp a jít dál. Stejně očividná oprava také mění samostatnou VCL komponentu v něco s deploymentovým příběhem
Proč implementovat VP8L místo napojení libwebp
HotPDF implementuje kodek v Pascalu, protože komponenta Delphi, kterou si zákazníci kompilují do vlastního executable, nemůže potichu získat runtime DLL. Nativní závislost znamená sledovat 32bitový a 64bitový binární soubor, připnout verzi, vysvětlit code-signing chain tomu, kdo deployment spouští, a přidat další soubor, který antivirus na uzamčeném terminálu může odmítnout. U komponenty, jejíž hlavní výhodou je, že ji vložíte do projektu a funguje, je to skutečná cena, nikoli teorie. Druhá polovina argumentu je, že VP8L je malý: formát prefixových kódů a LZ77 se čtyřmi inverzními transformacemi a 120prvkovou mapou vzdáleností sousedství, přičemž celý decoder v HPDFWebP.pas má pod 900 řádků Pascalu. Uvnitř THotPDF.AddImage sedí větev WebP ve stejném dispatchi přípon, který už posílá .jp2, .j2k, .jpt a .jpc přes cestu JPEG 2000, takže plumbing už existoval na stejném místě popsaném v návodu přidání obrázků JPEG 2000 do PDF v Delphi. Volající, kteří místo PDF obrázku chtějí surové pixely, mohou jít přímo přes HPDFDecodeWebPLossless, který naplní TWebPCardinalArray hodnotami $AARRGGBB v pořadí scanline
uses
HPDFDoc, HPDFWebP;
var
Pdf: THotPDF;
Idx: Integer;
begin
Pdf := THotPDF.Create(nil);
try
Pdf.FileName := 'catalog.pdf';
Pdf.BeginDoc;
// .webp se pošle do vestavěného decoderu VP8L, bez DLL
Idx := Pdf.AddImageFromFile('product-shot.webp', icFlate);
Pdf.CurrentPage.ShowImage(Idx, 50, 500, 240, 180, 0);
Pdf.EndDoc;
finally
Pdf.Free;
end;
end;
Proč se bitový proud VP8L čte dvěma směry najednou
Protože pořadí bitů kontejneru a pořadí bitů prefixového kódu jsou specifikována nezávisle a VP8L pro ně vybírá opačné konvence. RFC 9649 část 3.2 říká jasně, že bitový proud se čte od nejméně významného bitu: čtečka začne na bitu 0 bajtu a postupuje nahoru. Kanonické prefixové kódy uvnitř tohoto proudu přicházejí od nejvíce významného bitu, od kořene stromu, takže dekódovací průchod posune akumulátor doleva a nový bit vloží na konec. Čtení a průchod kódem tedy v jediné smyčce běží opačnými směry, což při každém novém čtení vypadá jako chyba
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;
// kanonický průchod běží opačně: první bit z proudu je
// nejvýznamnější bit kódu
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;
Tři detaily RFC, které potichu desynchronizují proud
Tři sémantiky v RFC 9649 jsou uvedené přesně jednou, snadno se při čtení přeskočí a každá stojí nebo ušetří jediný bit, což stačí k tomu, aby se všechny pozdější tabulky změnily v šum. V decoderu HotPDF pro VP8L byly nalezeny všechny tři a všechny vytvářejí stejný příznak: obraz, který vypadá plausibilně, ale je všude špatně
- Entropy-coded image v neprimární roli nezapisuje žádný meta-prefix bit. ABNF pro
entropy-coded-imagetuto položku vůbec neobsahuje, takže její načtení desynchronizuje proud o jeden bit. HotPDF předáváAllowMeta = Falsepro samotný entropy image, data predictoru a color transformu i pro palette color-indexing - Prefixový kód s jediným listem spotřebuje nula bitů. RFC 9649 část 3.7.2.1 to říká přímo a kanonický průchod by ochotně načetl bit, který pak nedokáže umístit, takže
BuildHuffzjistí celkový počet symbolů 1, označí stromSinglea tento jeden symbol dekóduje bez zásahu do readeru - Hodnota cache_bits 0 znamená velikost color cache 0, nikoli
1 shl 0. Pohodlný shift dá 1, takže zelená abeceda256 + 24 + CacheSizevyjde jako 281 místo 280 a každý prefixový kód načtený po ní je posunutý
CacheBits := 0;
CacheSize := 0; // cache_bits = 0 opravdu znamená nic
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: meta prefix bit nese jen prostorově kódovaný
// obraz (ARGB); role entropy-coded ho nikdy nezapisují
if AllowMeta then
UseMeta := BR.ReadBit
else
UseMeta := 0;
// ...
ReadHuffCode(256 + 24 + CacheSize, Groups[I].Green); // 280, nikoli 281
ReadHuffCode(256, Groups[I].Red);
ReadHuffCode(256, Groups[I].Blue);
ReadHuffCode(256, Groups[I].Alpha);
ReadHuffCode(40, Groups[I].Dist);
Na fixture použitém při bring-up se tyto tři věci objevily na bitech 47, 81 a 89 v tomto pořadí. To je smysl této sekce. Ani jedna se neohlásila jako off-by-one; každá se tvářila jako obrázek, který se dekódoval do konce a vypadal jako statický šum, a oddělila je pouze přesná bitová pozice, na které se proud přestal shodovat s referencí
Co přináší diffování bitových pozic
Diffování bitových pozic převede neužitečnou otázku na jednořádkovou otázku: ne „proč je tento obrázek špatně“, ale „proč se proud odchýlil na bitu 81“. Příprava je levná. Pillow zapíše každý fixture .webp i dump .rgba vlastního dekódování stejného obrazu; Pascalový probe a malý referenční model v Pythonu logují vedle každého čtení průběžný bitový čítač; první pozice, na níž se logy neshodnou, je místem chyby. Začněte fixturem, který cvičí co nejméně: plochým obrazem 32x32, který používá jen cestu simple-code. Ten nejprve dostaňte do zelené a potom po jednom přidávejte gradienty, liché rozměry a alpha. Hádat pořadí bitů je způsob, jak promarnit den
Poctivá výhrada je, že chybná byla i reference. Python model zapomněl načíst cache_bits a jeho transform loop nedoběhl do konce, takže některé body divergence byly ztrátou synchronizace v referenčním decoderu, nikoli v Pascalu. Chybná referenční implementace nečiní testovanou implementaci správnou a ani jedna strana nedostane výhodu pochybnosti: každou divergenci je nutné posoudit proti textu RFC. Tento text si také vytáhněte ze zdroje. Search summaries rutinně komolí číselné tabulky a mapa 120 vzdáleností, 14 predikčních režimů i násobitel color cache $1e35a7bd se musí opsat přesně
Kde se celočíselné dělení Pascalu rozchází s C
Color transform VP8L je 3.5 fixed point se signed deltas a právě tady se Pascal s C přestává shodovat. C posouvá záporná celá čísla aritmeticky, tedy zaokrouhluje dolů; Pascalové div truncuje k nule. U každého záporného součinu se proto liší o jedničku a inverzní color transform se v celém obrázku posune o jeden krok kanálu na pixel. HotPDF proto floor explicitně provádí v FloorDiv32 místo spoléhání na div
// C posouvá aritmeticky a u záporů zaokrouhluje dolů; Pascal div
// truncuje k nule, proto záporný případ potřebuje explicitní opravu
function FloorDiv32(V: Integer): Integer;
begin
Result := V div 32;
if (V < 0) and (V mod 32 <> 0) then
Dec(Result);
end;
// 3.5 fixed-point delta mezi bajtem prvku transformace a bajtem
// barevného kanálu, obojí nejdříve 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;
Tento druh defektu stojí za pojmenování, protože je neviditelný v každém testu, jehož fixture náhodou vytváří nezáporné součiny, u nichž se div a floor shodují. Je také důvodem, proč HotPDF testy vyžadují pixelově přesnou shodu s dekódováním Pillow ze stejných souborů místo tolerance: gradienty, lichá velikost 100x37, obrázek 40x40 se skutečným alpha kanálem i plochý obraz 32x32, každý pixel porovnán bit po bitu. Jednokrokový posun projde perceptuální kontrolou a selže bitovou
Co podpora WebP záměrně odmítá
HotPDF dekóduje první chunk VP8L v souboru WebP a nic dalšího. Lossy VP8 frames, animace a každý kontejner, jehož matching chunk není VP8L, vrátí z HPDFDecodeWebPLossless False a AddImage to převede na výjimku se jménem souboru: Failed to decode WebP image (lossless VP8L only). Je to záměrná hranice, nikoli opomenutí: soubor špatného formátu má selhat tam, kde ho volající může předem převést, místo aby vyrobil šedý obdélník. Pole version musí být 0, stack transformací je omezen na čtyři položky a každé překročení hranic vyvolá EWebPDecode, kterou veřejný vstupní bod převede na prosté False. Dekódování při importu je také opačný směr než vytahování obrázků z načteného dokumentu, které prochází loaded-image cestou popsanou v extrakci obrázků z načteného PDF a jejich decode filterů. Každý image decoder je parser krmený soubory, které jste nevytvořili: pokud WebP assety přicházejí od zákazníků nebo z veřejného internetu, zdejší kontroly hranic jsou základ, nikoli strop, a silnější odpovědí je spouštění image kodeků v izolovaném worker procesu, aby chybný frame neshodil hostitele s sebou
Praktickým výsledkem je, že aplikace v Delphi nebo C++Builderu nyní vloží WebP assety do PDF stejně jako PNG: jedno volání AddImageFromFile, jedno volání ShowImage, nic navíc v instalátoru. Pokud chcete zbytek obrazové a dokumentové pipeline kolem toho, HotPDF Delphi PDF component pokrývá zápis, načítání i render ze stejné sady jednotek