Tehnički članak

WebP u PDF-u u Delphi-ju: HotPDF VP8L decoder iznutra

HotPDF 2.747.0 dekodira WebP slike VP8L decoder-om za WebP lossless napisanom od nule u Object Pascal-u, pa THotPDF.AddImageFromFile direktno prihvata putanju .webp, bez libwebp DLL-a za isporuku i bez pomoćnog procesa za pokretanje. Decoder u celosti implementira odeljak 3 RFC 9649: prolaz kroz RIFF kontejner, kanonske prefix kodove, LZ77 backward reference, color cache i sve četiri inverzne transformacije. Lossy VP8 frame-ovi odbijaju se jasno, umesto da se napola dekodiraju

Okidač je bio svakodnevan. Dizajn alat izvozi svaki asset kao WebP jer je to savremeni podrazumevani format, asset-i dospeju u generator faktura ili kataloga koji je deceniju bez problema gutao PNG i JPEG, i odjednom polovina ulaza biva odbijena. Očigledna popravka je povezati libwebp i nastaviti. Očigledna popravka je ujedno ona koja samostalnu VCL komponentu pretvara u nešto sa deployment pričom

Zašto implementirati VP8L umesto povezivanja sa libwebp?

HotPDF implementira codec u Pascal-u zato što Delphi komponenta koju korisnici kompajliraju u sopstveni izvršni fajl ne može tiho da dobije runtime DLL. Native zavisnost znači 32-bitni i 64-bitni binar za praćenje, verziju koju treba zakucati, lanac potpisivanja koda koji treba objasniti osobi zaduženoj za deployment i još jedan fajl koji antivirus na zaključanom terminalu može odlučiti da mu smeta. Za komponentu čija je glavna vrednost to što se ubaci u projekat i radi, to je stvarna cena, a ne teorijska. Druga polovina argumenta jeste da je VP8L mali: format sa prefix kodom, LZ77 i četiri inverzne transformacije, uz mapu od 120 susednih distance-a, a ceo decoder u HPDFWebP.pas ima manje od 900 linija Pascal-a. Unutar THotPDF.AddImage WebP grana je na istom extension dispatch-u koji već šalje .jp2, .j2k, .jpt i .jpc kroz JPEG 2000 putanju, pa je plumbing već postojao, na istom mestu opisanom u vodiču o dodavanju JPEG 2000 slika u PDF-ove u Delphi-ju. Pozivaoci koji žele sirove piksele umesto PDF slike mogu direktno da pozovu HPDFDecodeWebPLossless, koji puni TWebPCardinalArray vrednostima $AARRGGBB redom po scan-line-u

uses
  HPDFDoc, HPDFWebP;

var
  Pdf: THotPDF;
  Idx: Integer;
begin
  Pdf := THotPDF.Create(nil);
  try
    Pdf.FileName := 'catalog.pdf';
    Pdf.BeginDoc;
    // .webp se šalje ugrađenom VP8L decoder-u, bez DLL-a
    Idx := Pdf.AddImageFromFile('product-shot.webp', icFlate);
    Pdf.CurrentPage.ShowImage(Idx, 50, 500, 240, 180, 0);
    Pdf.EndDoc;
  finally
    Pdf.Free;
  end;
end;

Zašto se VP8L bitstream čita u dva smera odjednom?

Zato što su redosled bitova kontejnera i redosled bitova prefix koda specifikovani nezavisno, a VP8L za njih bira suprotne konvencije. RFC 9649 odeljak 3.2 kaže izričito da se bitstream čita od najmanje značajnog bita naviše: čitač počinje na bitu 0 bajta i ide prema višim pozicijama. Kanonski prefix kodovi unutar tog stream-a stižu od najznačajnijeg bita naniže, od korena stabla, pa decode prolaz pomera akumulator ulevo i svaki novi bit OR-uje pri dnu. Čitanje i prolaz kroz kod zato idu u suprotnim smerovima unutar iste petlje, što svaki put izgleda kao greška kada kod ponovo pročitate

function TWebPBitReader.ReadBit: Integer;
begin
  if BytePos >= Length(Data) then
    raise EWebPDecode.Create('WebP bitstream exhausted');
  Result := (Data[BytePos] shr BitPos) and 1;   // prvo LSB, RFC 9649 3.2
  Inc(BitPos);
  if BitPos = 8 then
  begin
    BitPos := 0;
    Inc(BytePos);
  end;
end;

// kanonski prolaz ide drugim smerom: prvi bit sa stream-a
// jeste najznačajniji bit koda
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;

Tri RFC detalja koji nečujno desinhronizuju stream

Tri semantike u RFC 9649 navedene su tačno jednom, lako ih je prevideti i svaka košta ili čuva jedan bit, što je dovoljno da svaka kasnija tabela postane šum. Sve tri su pronađene u HotPDF VP8L decoder-u i sve tri daju isti simptom: slika koja izgleda uverljivo, ali je svuda pogrešna

  • Entropy-coded slika u neprimarnoj ulozi ne upisuje meta-prefix bit uopšte. ABNF za entropy-coded-image jednostavno ne sadrži tu stavku, pa čitanje jednog bita desinhronizuje stream za jedan bit. HotPDF prosleđuje AllowMeta = False za samu entropy sliku, podatke predictor i color transform-a, kao i color-indexing paletu
  • Prefix kod sa jednim listom troši nula bitova. RFC 9649 odeljak 3.7.2.1 to kaže direktno, a kanonski prolaz bi rado pročitao bit, pa zatim ne bi uspeo da ga smesti, zato BuildHuff otkriva ukupan broj simbola 1 i označava stablo sa Single, dekodujući taj jedan simbol bez dodirivanja čitača
  • Vrednost cache_bits 0 znači da je veličina color cache-a 0, a ne 1 shl 0. Pogodan shift daje 1, zbog čega zeleni alfabet 256 + 24 + CacheSize postaje 281 umesto 280, a svaka prefix code tabela posle njega se čita pogrešno poravnata
CacheBits := 0;
CacheSize := 0;                        // cache_bits = 0 zaista znači ništa
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: samo prostorno kodirana (ARGB) slika nosi
// meta prefix bit; entropy-coded uloge ga nikada ne upisuju
if AllowMeta then
  UseMeta := BR.ReadBit
else
  UseMeta := 0;

// ...
ReadHuffCode(256 + 24 + CacheSize, Groups[I].Green);   // 280, a ne 281
ReadHuffCode(256, Groups[I].Red);
ReadHuffCode(256, Groups[I].Blue);
ReadHuffCode(256, Groups[I].Alpha);
ReadHuffCode(40, Groups[I].Dist);

Na fixture-u korišćenom tokom bring-up-a tri slučaja pojavila su se na bitu 47, bitu 81 i bitu 89, tim redom. To je poenta ovog odeljka. Nijedan od ta tri slučaja nije se najavio kao off-by-one; svaki je došao kao slika koja se dekodovala do kraja i izgledala kao statika, a jedino što ih je razdvojilo bila je tačna pozicija bita na kojoj je stream prestao da se slaže sa referencom

Šta donosi diff po poziciji bita?

Diff po poziciji bita pretvara beskorisno pitanje u jednolinijsko pitanje: ne „zašto je ova slika pogrešna“, već „zašto je stream odstupio na bitu 81“. Postavka je jeftina. Pillow upisuje svaki .webp fixture zajedno sa .rgba dump-om sopstvenog dekodiranja iste slike; Pascal probe i mali Python referentni model loguju tekući brojač bitova uz svako čitanje; prva pozicija na kojoj se dva loga razlikuju jeste mesto gde živi greška. Počnite fixture-om koji vežba što manje toga: ravna slika 32x32 koja koristi samo simple-code putanju. Nju učinite ispravnom, pa dodajte gradijente, neparne dimenzije i alpha, jedan fixture po jedan. Nagađanje redosleda bitova umesto toga jeste način da potrošite dan

Poštena napomena jeste da je i referenca bila pogrešna. Python model je zaboravio da pročita cache_bits, a njegova transform petlja nije išla do kraja, pa su neke tačke odstupanja bile gubitak sinhronizacije referentnog decoder-a, a ne Pascal-a. To što je referentna implementacija pogrešna ne čini implementaciju pod testom ispravnom, niti jedna strana dobija prednost: svako odstupanje mora da se presudi prema tekstu RFC-a. Taj tekst uzmite iz izvora. Search summary-ji rutinski pogrešno prenesu numeričke tabele, a mapa distance-a od 120 unosa, 14 predictor režima i množilac color cache-a $1e35a7bd moraju biti prepisani tačno

Gde se Pascal integer division razlikuje od C-a

VP8L color transform je 3.5 fixed point sa signed delta vrednostima i upravo tu Pascal i C prestaju da se slažu. C aritmetički pomera negativne cele brojeve, što ih zaokružuje naniže; Pascal div skraćuje prema nuli. Za svaki negativni proizvod ta dva pristupa razlikuju se za jedan, pa se inverzna color transform pomera za jedan channel korak po pikselu kroz celu sliku. Zato HotPDF eksplicitno radi floor u FloorDiv32, umesto da zavisi od div

// C aritmetički pomera i zaokružuje naniže za negative; Pascal div skraćuje
// prema nuli, pa negativnom slučaju treba eksplicitna korekcija
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 između bajta transform elementa i bajta
// color channel-a, pri čemu se oba prvo proširuju sa znakom
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;

Ovu klasu defekta vredi imenovati jer je nevidljiva svakom testu čiji fixture-i slučajno daju nenegativne proizvode, gde se div i floor slažu. To je ujedno razlog što HotPDF WebP testovi zahtevaju piksel-po-piksel jednaku vrednost prema Pillow dekodiranju istih fajlova, umesto tolerancije: gradijenti, neparna veličina 100x37, slika 40x40 sa pravim alpha channel-om i ravna slika 32x32, svaki piksel poređen bit po bit. Pomak od jednog koraka prolazi perceptivnu proveru, a pada na bitovskoj

Šta WebP podrška namerno odbija

HotPDF dekodira prvi VP8L chunk WebP fajla i ništa drugo. Lossy VP8 frame-ovi, animacije i svaki kontejner čiji odgovarajući chunk nije VP8L vraćaju False iz HPDFDecodeWebPLossless, a AddImage to pretvara u izuzetak koji imenuje fajl: Failed to decode WebP image (lossless VP8L only). To je namerna granica, a ne propust: fajl pogrešnog formata treba da padne tamo gde pozivalac može da ga prekonvertuje, umesto da proizvede siv pravougaonik. Polje verzije mora biti 0, stack transformacija ograničen je na četiri unosa, a svako prekoračenje granica podiže EWebPDecode, koji javna ulazna tačka pretvara u obično False. Dekodiranje pri uvozu ujedno je suprotan smer od izvlačenja slika iz otvorenog dokumenta, koje prolazi kroz putanju učitanih slika opisanu u tekstu o izdvajanju slika iz učitanog PDF-a i njihovim decode filter-ima. A svaki image decoder jeste parser koji dobija fajlove koje niste vi napravili: ako WebP asset-i dolaze od klijenata ili sa javnog interneta, provere granica ovde su donja, a ne gornja granica, a snažniji odgovor je pokretanje image codec-a u izolovanom worker procesu, kako neispravan frame ne bi sa sobom oborio host

Praktičan rezultat jeste da Delphi ili C++Builder aplikacija sada može da stavi WebP asset-e u PDF isto kao PNG: jedan poziv AddImageFromFile, jedan poziv ShowImage, ništa dodatno u installer-u. Ako želite ostatak image i document pipeline-a oko toga, HotPDF Delphi PDF komponenta pokriva strane upisa, učitavanja i renderovanja iz istog skupa jedinica