A HotPDF 2.747.0 Object Pascalban a nulláról írt VP8L (WebP lossless) dekóderrel dekódolja a WebP-képeket, ezért a THotPDF.AddImageFromFile közvetlenül elfogad egy .webp útvonalat, telepítendő libwebp DLL és indítandó segédfolyamat nélkül. A dekóder teljes egészében megvalósítja az RFC 9649 3. szakaszát: a RIFF-konténer bejárását, a kanonikus prefixkódokat, az LZ77 backward reference-eket, a color cache-t és mind a négy inverz transzformációt. A veszteséges VP8 frame-eket határozottan elutasítja, nem próbálja félig dekódolni
A kiváltó ok hétköznapi volt. Egy designeszköz minden assetet WebP-ként exportál, mert ez a modern alapértelmezés, az assetek pedig egy olyan számla- vagy katalógusgenerátorba kerülnek, amely évtizede gond nélkül fogyaszt PNG-t és JPEG-et, és hirtelen a bemenetek fele elutasításra kerül. A kézenfekvő javítás a libwebp bekötése és továbblépés. Ez a kézenfekvő javítás ugyanakkor egy önálló VCL-komponenst telepítési történettel terhel
Miért VP8L-t valósított meg a HotPDF a libwebp bekötése helyett?
A HotPDF azért Object Pascalban valósította meg a codecet, mert egy olyan Delphi-komponens, amelyet az ügyfelek a saját futtathatójukba fordítanak, nem szerezhet csendben runtime DLL-t. Egy natív függőség egy 32 és egy 64 bites bináris követését, rögzítendő verziót, elmagyarázandó code-signing láncot és még egy fájlt jelent, amelyről egy lezárt terminál vírusirtója eldöntheti, hogy nem kedveli. Egy olyan komponensnél, amelynek fő ígérete, hogy projektbe ejted és működik, ez valós költség, nem elméleti. Az érv másik fele, hogy a VP8L kicsi: prefixkód- és LZ77-formátum négy inverz transzformációval és 120 elemű szomszédsági távolságtérképpel, a teljes dekóder pedig a HPDFWebP.pas-ban 900 Pascal-sor alatti. A THotPDF.AddImage-ben a WebP-ág ugyanazon extension-dispatchban ül, amely már a .jp2, .j2k, .jpt és .jpc kiterjesztést a JPEG 2000 útvonalra küldi, ezért a plumbing már adott volt, ugyanott, ahol a JPEG 2000 képek PDF-be adásáról szóló Delphi-útmutató is tárgyalja. Aki PDF-kép helyett nyers pixeleket akar, közvetlenül a HPDFDecodeWebPLossless-t hívhatja, amely scan-line sorrendben egy TWebPCardinalArray $AARRGGBB értékeit tölti ki
uses
HPDFDoc, HPDFWebP;
var
Pdf: THotPDF;
Idx: Integer;
begin
Pdf := THotPDF.Create(nil);
try
Pdf.FileName := 'catalog.pdf';
Pdf.BeginDoc;
// A .webp a beépített VP8L dekóderre kerül, nincs DLL
Idx := Pdf.AddImageFromFile('product-shot.webp', icFlate);
Pdf.CurrentPage.ShowImage(Idx, 50, 500, 240, 180, 0);
Pdf.EndDoc;
finally
Pdf.Free;
end;
end;
Miért olvas a VP8L bitstream egyszerre két irányba?
Azért, mert a konténer bitrendje és a prefixkód bitrendje egymástól függetlenül van specifikálva, a VP8L pedig ellentétes konvenciót választ hozzájuk. Az RFC 9649 3.2 szakasza világosan kimondja, hogy a bitstreamet least-significant-bit first kell olvasni: az olvasó egy byte 0. bitjéről indul, és felfelé halad. A streamben hordozott kanonikus prefixkódok most a legjelentősebb bittel érkeznek először, a fa gyökerétől indulva, ezért a dekódoló ciklus balra tolja az akkumulátort, és az új bitet alul OR-olja be. Az olvasás és a kódbejárás tehát ugyanabban a ciklusban ellentétes irányban halad, ami minden újraolvasáskor hibának tűnik
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;
// a kanonikus bejárás ellenkező irányú: a streamből elsőként érkező bit
// a kód legjelentősebb bitje
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;
Három RFC-részlet, amely csendben deszinkronizálja a streamet
Az RFC 9649 három szemantikát pontosan egyszer mond ki, könnyű átsiklani felettük, és mindegyik egyetlen bitet takarít meg vagy fogyaszt el, ami elég ahhoz, hogy minden későbbi tábla zajjá váljon. Mindhárom előkerült a HotPDF VP8L-dekóderében, és mindhárom ugyanazt a tünetet adja: hihetőnek látszó, de mindenütt hibás képet
- Egy nem elsődleges szerepben lévő entropy-coded image egyáltalán nem ír meta-prefix bitet. Az
entropy-coded-imageABNF-je egyszerűen nem tartalmazza ezt az elemet, ezért egy bit olvasása eltolná a streamet. A HotPDFAllowMeta = Falseértéket ad magához az entropy image-hez, a predictor- és color-transform adatokhoz, valamint a color-indexing palettához - Egyetlen levélből álló prefixkód nulla bitet fogyaszt. Az RFC 9649 3.7.2.1 ezt közvetlenül kimondja, a kanonikus bejárás viszont készségesen olvasna egy bitet, majd nem tudná elhelyezni, ezért a
BuildHufffelismeri az 1-es teljes szimbólumszámot,Single-ként jelöli a fát, és az egyetlen szimbólumot az olvasó érintése nélkül dekódolja - A 0 értékű cache_bits a color cache mérete 0, nem
1 shl 0. A kényelmes shift 1-et adna, ezért a zöld ábécé256 + 24 + CacheSizeértéke 280 helyett 281 lenne, és minden utána olvasott prefixkódtábla elcsúszna
CacheBits := 0;
CacheSize := 0; // a cache_bits = 0 valóban semmit jelent
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: csak a térben kódolt (ARGB) image hordozza a
// meta prefix bitet; az entropy-coded szerepek soha nem írják ki
if AllowMeta then
UseMeta := BR.ReadBit
else
UseMeta := 0;
// ...
ReadHuffCode(256 + 24 + CacheSize, Groups[I].Green); // 280, nem 281
ReadHuffCode(256, Groups[I].Red);
ReadHuffCode(256, Groups[I].Blue);
ReadHuffCode(256, Groups[I].Alpha);
ReadHuffCode(40, Groups[I].Dist);
A bring-up során használt fixtúrán a három hiba a 47., 81. és 89. bitnél jelent meg, ebben a sorrendben. Ezek a számok ennek a résznek a lényege. Egyik sem off-by-one hibaként jelentkezett; mindegyik olyan képként, amely a dekódolást befejezte, de statikus zajnak látszott, és csak a pontos bitpozíció választotta el őket a referenciától, ahol a stream többé nem egyezett
Mit ad a bitpozíciós diffelés?
A bitpozíciós diffelés egy haszontalan kérdést egysoros kérdéssé alakít: nem azt kérdezed, miért hibás ez a kép, hanem azt, miért tért el a stream a 81. bitnél. A beállítás olcsó. A Pillow minden .webp fixtúrát és ugyanannak a képnek a saját dekódolásából származó .rgba dumpot kiírja; egy Pascal-probe és egy kis Python-referenciamodell minden olvasás mellé futó bitszámlálót logol; az első pont, ahol a két log eltér, jelzi a hibát. Olyan fixtúrával kezdj, amely a lehető legkevesebbet gyakorolja: egy lapos 32×32-es képpel, amely csak az egyszerű kódútvonalat használja. Ha ez zöld, adj hozzá gradienst, furcsa méreteket és alphát egyesével. A bitrend találgatása ehelyett csak egy nap elpazarlásának módja
Az őszinte kiegészítés, hogy a referencia is hibás volt. A Python-modell elfelejtette olvasni a cache_bits-et, a transform ciklusa pedig nem futott végig, ezért néhány eltérési pont a referencia dekóder szinkronvesztése volt, nem a Pascal-kódé. Egy hibás referencia-implementáció nem teszi helyessé a tesztelt implementációt, és egyik fél sem kapja meg a jóhiszeműség előnyét: minden eltérést az RFC szövege alapján kell eldönteni. Ezt a szöveget is a forrásból vedd. A keresési összefoglalók gyakran elrontják a numerikus táblákat, a 120 elemű távolságtérképet, a 14 predictor-módot és a $1e35a7bd color-cache szorzót pedig pontosan kell átírni
Hol tér el a Pascal egészosztása a C-től?
A VP8L color transform 3,5-ös fixpontos, előjeles deltákkal, és itt szűnik meg a Pascal és a C egyetértése. A C az előjeles egész számokat aritmetikusan shifteli, ami lefelé kerekít, a Pascal div viszont nulla felé csonkol. Minden negatív szorzatnál eggyel eltérnek, ezért az inverz color transform képenként és csatornánként egy lépéssel elcsúszik. A HotPDF ezért a FloorDiv32-ben expliciten lefelé kerekít, nem a div-re hagyatkozik
// A C aritmetikusan shiftel és negatív értéknél lefelé kerekít; a Pascal div
// nulla felé csonkol, ezért a negatív esethez explicit korrekció kell
function FloorDiv32(V: Integer): Integer;
begin
Result := V div 32;
if (V < 0) and (V mod 32 <> 0) then
Dec(Result);
end;
// 3,5-ös fixpontos delta egy transform elem byte-ja és egy
// color channel byte-ja között, előbb mindkettőt előjelesen kiterjesztve
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;
Ezt a hibacsoportot érdemes megnevezni, mert láthatatlan minden olyan teszten, amelynek fixtúrái véletlenül csak nem negatív szorzatokat állítanak elő, ahol a div és a floor egybeesik. Ezért ellenőrzik a HotPDF WebP-tesztek a pixelek bitpontos azonosságát a Pillow ugyanazon fájlokból készített dekódolásával, nem pedig toleranciát használnak: gradienteket, furcsa 100×37-es méretet, valódi alphacsatornás 40×40-es képet és lapos 32×32-es képet, minden pixelt bitről bitre. Az egylépéses elcsúszás átmegy a perceptuális ellenőrzésen, a bitenkéntin elbukik
Mit utasít el szándékosan a WebP-támogatás?
A HotPDF egy WebP-fájl első VP8L chunkját dekódolja, mást nem. A veszteséges VP8 frame-ek, animációk és minden olyan konténer, amelynek illeszkedő chunkja nem VP8L, False értékkel tér vissza a HPDFDecodeWebPLossless-ből, az AddImage pedig ezt a fájlt megnevező kivétellé alakítja: Failed to decode WebP image (lossless VP8L only). Ez tudatos határ, nem hiányosság: a rossz formátumú fájl ott bukjon el, ahol a hívó előre átkonvertálhatja, ahelyett hogy szürke téglalapot kapna. A verziómezőnek 0-nak kell lennie, a transform stack legfeljebb négy bejegyzéses, minden bounds-sértés EWebPDecode-ot emel, a publikus belépési pont pedig ezt sima False értékké alakítja. Az import közbeni dekódolás a dokumentumból visszanyert képek ellentétes iránya, amelyet a betöltött PDF képeinek és dekódolási filtereinek kinyeréséről szóló útvonal kezel. Bármely képdekóder olyan parser, amelyet nem te általad készített fájlok táplálnak: ha a WebP-assetek ügyfelektől vagy a nyilvános internetről jönnek, az itteni bounds-ellenőrzések az alsó szintet jelentik, az erősebb válasz pedig a kép-codecek izolált worker-folyamatban futtatása, hogy egy hibás frame ne rántsa magával a hostot
A gyakorlati eredmény, hogy egy Delphi vagy C++Builder alkalmazás ugyanúgy tehet WebP-assetet PDF-be, mint PNG-t: egy AddImageFromFile hívás, egy ShowImage hívás, semmi extra az installerben. Ha a körülötte lévő teljes kép- és dokumentumpipeline is érdekel, a HotPDF Delphi PDF component ugyanabból a unitkészletből fedezi az írási, betöltési és renderelési oldalt