Odborný článok

WebP do PDF v Delphi: vnútri dekodéra HotPDF VP8L

HotPDF 2.747.0 dekóduje obrázky WebP dekóderom VP8L (WebP lossless) napísaným od nuly v Object Pascale, takže THotPDF.AddImageFromFile prijíma cestu .webp priamo, bez DLL libwebp, ktorú by bolo treba distribuovať, a bez pomocného procesu, ktorý by sa mal spustiť. Dekóder implementuje RFC 9649, section 3, v plnom rozsahu: prechod kontajnerom RIFF, kanonické prefix kódy, spätné referencie LZ77, color cache a všetky štyri inverzné transformácie. Lossy VP8 frames sa odmietnu hlasno namiesto polovičného dekódovania

Spúšťač bol všedný. Dizajnový nástroj exportuje každý asset ako WebP, pretože je to moderný predvolený formát, assety skončia v generátore faktúr alebo katalógov, ktorý desaťročie bez problémov prijímal PNG a JPEG, a zrazu je polovica vstupov odmietnutá. Očividná oprava je nalinkovať libwebp a ísť ďalej. Očividná oprava je zároveň tá, ktorá z autonómneho VCL komponentu urobí niečo s vlastným deployment príbehom

Prečo implementovať VP8L namiesto linkovania libwebp?

HotPDF implementuje kodek v Pascale, pretože Delphi komponent, ktorý si zákazníci kompilujú do vlastného executable, si nemôže potichu zaobstarať runtime DLL. Natívna závislosť znamená sledovať binárku pre 32 bitov aj 64 bitov, pripnúť verziu, vysvetliť code-signing chain tomu, kto spúšťa deployment, a pridať ďalší súbor, ktorý antivírus na uzamknutom termináli môže odmietnuť. Pri komponente, ktorého hlavnou výhodou je vložiť ho do projektu a pracovať, je to skutočný náklad, nie teória. Druhá polovica argumentu je, že VP8L je malý: formát s prefix codes, LZ77, štyrmi inverznými transformáciami a mapou vzdialeností susedstva so 120 položkami, pričom celý dekóder v HPDFWebP.pas má menej než 900 riadkov Pascalu. Vo vnútri THotPDF.AddImage sedí vetva WebP v tom istom extension dispatchi, ktorý už smeruje .jp2, .j2k, .jpt a .jpc cez cestu JPEG 2000, takže plumbing už existoval na rovnakom mieste, ktoré opisuje prehliadka pridávania obrázkov JPEG 2000 do PDF v Delphi. Volajúci, ktorí namiesto PDF obrázka chcú surové pixely, môžu ísť priamo na HPDFDecodeWebPLossless, ktorá naplní TWebPCardinalArray hodnotami $AARRGGBB v poradí scan-line

uses
  HPDFDoc, HPDFWebP;

var
  Pdf: THotPDF;
  Idx: Integer;
begin
  Pdf := THotPDF.Create(nil);
  try
    Pdf.FileName := 'catalog.pdf';
    Pdf.BeginDoc;
    // .webp sa pošle vstavanému dekóderu 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;

Prečo sa bitstream VP8L číta dvoma smermi naraz?

Pretože poradie bitov kontajnera a poradie bitov prefix kódu sú špecifikované nezávisle a VP8L pre ne vyberá opačné konvencie. RFC 9649 section 3.2 to hovorí priamo: bitstream sa číta od najmenej významného bitu, čítačka začína na bite 0 bajtu a ide nahor. Kanonické prefix kódy uložené v tomto streame prichádzajú od najviac významného bitu, najprv koreň stromu, takže dekódovací prechod posúva accumulator doľava a každý nový bit vloží dole pomocou OR. Prechod readera a prechod kódu teda v tej istej slučke smerujú opačne, čo pri každom opätovnom čítaní vyzerá ako bug

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

// Kanonický prechod ide opačne: prvý bit zo streamu je
// najvýznamnejším bitom 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;

Tri detaily RFC, ktoré potichu desynchronizujú stream

Tri sémantiky v RFC 9649 sú uvedené presne raz, ľahko sa prehliadnu a každá stojí alebo ušetrí jeden bit, čo stačí na zmenu každej ďalšej tabuľky na šum. Všetky tri sa našli v dekóderi HotPDF VP8L a všetky tri produkujú rovnaký príznak: obraz, ktorý vyzerá vierohodne, ale je všade nesprávny

  • Entropicky kódovaný obraz v neprimárnej role nezapisuje vôbec žiadny meta-prefix bit. ABNF pre entropy-coded-image túto položku jednoducho neobsahuje, takže jej prečítanie desynchronizuje stream o jeden bit. HotPDF odovzdáva AllowMeta = False pre samotný entropy image, pre údaje predictor a color transform aj pre color-indexing palette
  • Prefix kód s jediným listom spotrebuje nula bitov. RFC 9649 section 3.7.2.1 to hovorí priamo a kanonický prechod by ochotne prečítal bit a potom ho nedokázal umiestniť, takže BuildHuff zistí celkový počet symbolov 1, označí strom Single a dekóduje tento jeden symbol bez dotyku readera
  • Hodnota cache_bits 0 znamená, že veľkosť color cache je 0, nie 1 shl 0. Pohodlný shift dá 1, čím abeceda green 256 + 24 + CacheSize vyjde ako 281 namiesto 280 a každé čítanie tabuľky prefix kódov po nej je posunuté
CacheBits := 0;
CacheSize := 0;                        // cache_bits = 0 skutočne znamená nič
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: iba priestorovo kódovaný (ARGB) obraz nesie
// meta prefix bit; entropy-coded role ho nikdy nezapisujú
if AllowMeta then
  UseMeta := BR.ReadBit
else
  UseMeta := 0;

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

Na fixture použitej počas bring-upu sa tri objavili na bitoch 47, 81 a 89, v tomto poradí. To je pointa tejto sekcie. Ani jedna sa neohlásila ako off-by-one; každá sa prejavila ako obraz, ktorý sa dekódoval do konca a vyzeral ako šum, pričom jediná vec, ktorá ich odlíšila, bola presná bitová pozícia, na ktorej stream prestal súhlasiť s referenciou

Čo prinesie diffovanie bitových pozícií?

Diffovanie bitových pozícií zmení nepoužiteľnú otázku na otázku na jeden riadok: nie prečo je tento obraz nesprávny, ale prečo sa stream rozchádza na bite 81. Setup je lacný. Pillow zapíše každý fixture .webp spolu s dumpom .rgba svojho dekódovania toho istého obrazu; Pascal probe aj malý Python reference model logujú vedľa každého čítania priebežný bit counter; prvá pozícia, na ktorej sa oba logy rozídu, je miesto chyby. Začnite fixture, ktorý precvičuje čo najmenej: plochý obraz 32x32, ktorý prejde iba jednoduchou code path. Dosiahnite, aby bol správny, potom pridávajte gradienty, nepárne rozmery a alpha po jednom fixture. Hádať poradie bitov je namiesto toho spôsob, ako stráviť deň

Poctivá výhrada je, že nesprávna bola aj referencia. Python model zabudol čítať cache_bits a jeho transformačná slučka nedobehla do konca, takže niektoré body divergencie boli stratením synchronizácie v referenčnom dekóderi, nie v Pascalovom. Nesprávna reference implementation nerobí testovanú implementáciu správnou a ani jedna strana nedostane benefit pochybnosti: každú divergenciu treba posúdiť voči textu RFC. Aj ten text čerpajte zo source. Search summaries pravidelne komolia numerické tabuľky a mapa vzdialeností so 120 položkami, 14 režimov predictor a násobiteľ color cache $1e35a7bd musia byť prepísané presne

Kde sa celočíselné delenie Pascalu rozchádza s C

Color transform VP8L je fixed point 3.5 so signed deltas a práve tu sa Pascal a C prestanú zhodovať. C posúva záporné celé čísla aritmeticky, čo zaokrúhľuje nadol; Pascal div skracuje smerom k nule. Pri každom zápornom súčine sa preto líšia o jeden a inverzná color transform sa cez celý obraz posunie o jeden krok kanála na pixel. HotPDF preto v FloorDiv32 vykoná floor explicitne namiesto spoliehania sa na div

// C posúva aritmeticky a pri záporných hodnotách zaokrúhľuje nadol; Pascal div skracuje
// smerom k nule, preto záporný prípad potrebuje explicitnú korekciu
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 medzi bajtom prvku transformácie a bajtom
// farebného kanála, pričom oba sa najprv sign-extendujú
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;

Túto triedu chyby sa oplatí pomenovať, pretože je neviditeľná v každom teste, ktorého fixture náhodou produkujú nezáporné súčiny, kde div a floor súhlasia. Je to aj dôvod, prečo HotPDF WebP testy vyžadujú pixelovo presnú rovnosť voči Pillow decode rovnakých súborov namiesto tolerancie: gradienty, nepárna veľkosť 100x37, obraz 40x40 so skutočným alpha kanálom a plochý obraz 32x32, každý pixel porovnaný bit za bitom. Jednokrokový posun prejde perceptuálnou kontrolou a zlyhá bitovou

Čo podpora WebP zámerne odmieta

HotPDF dekóduje prvý chunk VP8L súboru WebP a nič ďalšie. Lossy VP8 frames, animácie a každý kontajner, ktorého matching chunk nie je VP8L, vrátia z HPDFDecodeWebPLossless False a AddImage to premení na výnimku s názvom súboru: Failed to decode WebP image (lossless VP8L only). Je to zámerná hranica, nie opomenutie: súbor s nesprávnym formátom má zlyhať tam, kde ho volajúci môže vopred skonvertovať, namiesto výroby sivého obdĺžnika. Pole version musí byť 0, zásobník transformácií je obmedzený na štyri položky a každé prekročenie hraníc vyvolá EWebPDecode, ktorú verejný vstupný bod premení na obyčajné False. Dekódovanie pri importe je tiež opačný smer než vyťahovanie obrázkov z dokumentu, ktorý ste otvorili, čo prebieha cez cestu loaded image opísanú v článku o extrakcii obrázkov z načítaného PDF a ich decode filtroch. A každý image decoder je parser napájaný súbormi, ktoré ste nevytvorili: ak WebP assety prichádzajú od zákazníkov alebo z verejného internetu, kontroly hraníc tu sú podlaha, nie strop, a silnejšou odpoveďou je spúšťať image codecs v izolovanom worker procese, aby chybný frame nestiahol so sebou hostiteľa

Praktickým výsledkom je, že aplikácia v Delphi alebo C++Builderi teraz vloží WebP assety do PDF rovnakým spôsobom ako PNG: jedno volanie AddImageFromFile, jedno volanie ShowImage, nič navyše v inštalátore. Ak chcete zvyšok image a document pipeline, ktorý okolo toho sedí, HotPDF Delphi PDF component pokrýva stranu zápisu, načítania aj renderovania z rovnakej sady unitov