HotPDF 2.747.0 decodeert WebP-afbeeldingen met een VP8L-decoder (WebP lossless) die vanaf nul in Object Pascal is geschreven, zodat THotPDF.AddImageFromFile rechtstreeks een .webp-pad accepteert, zonder libwebp-DLL om mee te leveren en zonder helperproces om te starten. De decoder implementeert RFC 9649 sectie 3 volledig: de RIFF-containerwalk, canonical prefix codes, LZ77 backward references, de color cache en alle vier inverse transforms. Lossy VP8-frames worden luid geweigerd in plaats van half te worden gedecodeerd
De aanleiding was alledaags. Een designtool exporteert elk asset als WebP omdat dat de moderne default is, de assets belanden in een factuur- of catalogusgenerator die al tien jaar probleemloos PNG en JPEG verwerkt en plotseling wordt de helft van de input geweigerd. De voor de hand liggende fix is libwebp binden en doorgaan. Die voor de hand liggende fix verandert een self-contained VCL-component ook in iets met een deploymentverhaal
Waarom VP8L implementeren in plaats van libwebp te binden?
HotPDF implementeert de codec in Pascal omdat een Delphi-component die klanten in hun eigen executable compileren niet stilletjes een runtime-DLL kan gaan vereisen. Een native dependency betekent een 32-bits en een 64-bits binary om bij te houden, een versie om vast te zetten, een code-signing chain die je aan de deploymentbeheerder moet uitleggen en nog een bestand waarvan antivirussoftware op een vergrendelde terminal kan besluiten dat het niet bevalt. Voor een component waarvan het belangrijkste verkoopargument is dat hij in een project valt en werkt, zijn dat echte kosten en geen theoretische. De andere helft van het argument is dat VP8L klein is: een prefix-code plus LZ77-formaat met vier inverse transforms en een distance map met 120 entries, en de hele decoder in HPDFWebP.pas is minder dan 900 regels Pascal. Binnen THotPDF.AddImage zit de WebP-branch op dezelfde extension dispatch die .jp2, .j2k, .jpt en .jpc al via het JPEG 2000-pad routeert, dus de plumbing was er al, op dezelfde plek die wordt beschreven in de walkthrough over JPEG 2000-afbeeldingen aan PDF's toevoegen in Delphi. Callers die raw pixels in plaats van een PDF-afbeelding willen, kunnen rechtstreeks naar HPDFDecodeWebPLossless, dat een TWebPCardinalArray met $AARRGGBB-waarden in scanlinevolgorde vult
uses
HPDFDoc, HPDFWebP;
var
Pdf: THotPDF;
Idx: Integer;
begin
Pdf := THotPDF.Create(nil);
try
Pdf.FileName := 'catalog.pdf';
Pdf.BeginDoc;
// .webp wordt naar de ingebouwde VP8L-decoder gestuurd, zonder DLL
Idx := Pdf.AddImageFromFile('product-shot.webp', icFlate);
Pdf.CurrentPage.ShowImage(Idx, 50, 500, 240, 180, 0);
Pdf.EndDoc;
finally
Pdf.Free;
end;
end;
Waarom wordt een VP8L-bitstream tegelijk in twee richtingen gelezen?
Omdat de bitvolgorde van de container en de bitvolgorde van de prefixcode onafhankelijk zijn gespecificeerd, en VP8L voor beide tegengestelde conventies kiest. RFC 9649 sectie 3.2 zegt het duidelijk: de bitstream wordt least significant bit first gelezen, de reader start bij bit 0 van een byte en loopt omhoog. De canonical prefix codes in die stream komen most significant bit first binnen, met de root van de boom voorop, dus de decodewalk schuift zijn accumulator naar links en OR't elke nieuwe bit onderaan in. Readerwalk en codewalk lopen daardoor binnen dezelfde loop tegengesteld, wat er bij elke herlezing als een bug uitziet
function TWebPBitReader.ReadBit: Integer;
begin
if BytePos >= Length(Data) then
raise EWebPDecode.Create('WebP bitstream exhausted');
Result := (Data[BytePos] shr BitPos) and 1; // LSB eerst, RFC 9649 3.2
Inc(BitPos);
if BitPos = 8 then
begin
BitPos := 0;
Inc(BytePos);
end;
end;
// De canonieke walk gaat de andere kant op: de eerste bit uit de stream
// is de meest significante bit van de code
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;
Drie RFC-details die de stream stilletjes desynchroniseren
Drie semantics in RFC 9649 worden maar één keer genoemd, zijn makkelijk over te lezen en kosten of besparen elk één bit, genoeg om elke latere tabel in ruis te veranderen. Alle drie zijn in de HotPDF VP8L-decoder gevonden en alle drie produceren hetzelfde symptoom: een afbeelding die er plausibel uitziet maar overal fout is
- Een entropy-coded image in een niet-primaire rol schrijft helemaal geen meta-prefixbit. De ABNF voor
entropy-coded-imagebevat het item simpelweg niet, dus één bit lezen desynchroniseert de stream. HotPDF geeftAllowMeta = Falsedoor voor de entropy image zelf, voor predictor- en color-transformdata en voor het color-indexing-palette - Een prefixcode met één leaf consumeert nul bits. RFC 9649 sectie 3.7.2.1 zegt dat rechtstreeks, en de canonical walk zou graag een bit lezen en die vervolgens niet kunnen plaatsen. Daarom detecteert
BuildHuffeen totaal aantal symbolen van 1 en markeert de boom alsSingle, waarna dat ene symbool wordt gedecodeerd zonder de reader aan te raken - Een cache_bits-waarde van 0 betekent dat de grootte van de color cache 0 is en niet
1 shl 0. De handige shift geeft 1, waardoor het green-alphabet256 + 24 + CacheSizeop 281 in plaats van 280 uitkomt en elke prefixcodetabel die daarna wordt gelezen verkeerd uitgelijnd is
CacheBits := 0;
CacheSize := 0; // cache_bits = 0 betekent echt niets
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: alleen de ruimtelijk gecodeerde (ARGB)-afbeelding draagt de
// meta-prefixbit; entropy-gecodeerde rollen schrijven die nooit
if AllowMeta then
UseMeta := BR.ReadBit
else
UseMeta := 0;
// ...
ReadHuffCode(256 + 24 + CacheSize, Groups[I].Green); // 280, niet 281
ReadHuffCode(256, Groups[I].Red);
ReadHuffCode(256, Groups[I].Blue);
ReadHuffCode(256, Groups[I].Alpha);
ReadHuffCode(40, Groups[I].Dist);
Op de fixture die tijdens de bring-up werd gebruikt, kwamen de drie naar boven bij bit 47, bit 81 en bit 89, in die volgorde. Die getallen zijn het punt van deze sectie. Geen van de drie kondigde zichzelf aan als off-by-one; elk presenteerde zich als een afbeelding die tot het einde decodeerde en eruitzag als static, en alleen de exacte bitpositie waarop de stream niet meer overeenkwam met een referentie maakte het verschil
Wat levert diffen op bitpositie op?
Diffen op bitpositie maakt van een nutteloze vraag een vraag van één regel: niet waarom is dit beeld fout maar waarom week de stream af bij bit 81. De setup is goedkoop. Pillow schrijft naast elke .webp-fixture ook een .rgba-dump van zijn eigen decode van dezelfde afbeelding; een Pascal-probe en een klein Python-referentiemodel loggen allebei naast elke read een lopende bitteller; de eerste positie waarop de twee logs verschillen is waar de bug zit. Begin met een fixture die zo weinig mogelijk uitoefent: een vlakke afbeelding van 32x32 die alleen het simple-code-pad gebruikt. Krijg die groen en voeg daarna gradients, oneven afmetingen en alpha één fixture per keer toe. In plaats daarvan raden naar de bitvolgorde is een manier om een dag kwijt te zijn
De eerlijke caveat is dat de referentie ook fout was. Het Python-model vergat cache_bits te lezen en zijn transformloop liep niet tot het einde, dus sommige divergentiepunten waren de referentiedecoder die de synchronisatie verloor en niet de Pascal-code. Dat een referentie-implementatie fout is, maakt de implementatie onder test niet automatisch juist, en geen van beide kanten krijgt het voordeel van de twijfel: elke divergentie moet worden getoetst aan de tekst van de RFC. Haal die tekst ook uit de bron. Zoekoverzichten verminken numerieke tabellen vaak, en de distance map met 120 entries, de 14 predictormodi en de color-cache-multiplier $1e35a7bd moeten allemaal exact worden overgeschreven
Waar Pascal-integerdeling afwijkt van C
De VP8L-color transform gebruikt 3.5 fixed point met signed deltas, en daar houden Pascal en C op met hetzelfde te doen. C shift negatieve integers arithmetisch en floor't ze; Pascal div kapt af naar nul. Voor elk negatief product verschillen de twee één, waardoor de inverse color transform over de hele afbeelding per pixel één channelstep afwijkt. Daarom maakt HotPDF de floor expliciet in FloorDiv32 in plaats van op div te vertrouwen
// C shiftt arithmetisch en floor't bij negatieve waarden; Pascal div kapt af
// naar nul, dus het negatieve geval heeft een expliciete correctie nodig
function FloorDiv32(V: Integer): Integer;
begin
Result := V div 32;
if (V < 0) and (V mod 32 <> 0) then
Dec(Result);
end;
// 3.5 fixedpoint-delta tussen een byte van een transformelement en een
// byte van een color channel, beide eerst met teken uitgebreid
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;
Deze defectklasse is het benoemen waard omdat hij onzichtbaar is voor elke test waarvan de fixtures toevallig niet-negatieve producten opleveren, waarbij div en floor overeenkomen. Het is ook de reden dat de HotPDF-WebP-tests pixel-exacte gelijkheid controleren tegen Pillow-decodes van dezelfde bestanden en niet tegen een tolerantie: gradients, een oneven formaat van 100x37, een afbeelding van 40x40 met een echt alpha-channel en een vlakke afbeelding van 32x32, elke pixel bit voor bit vergeleken. Een drift van één stap slaagt voor een perceptuele check en faalt voor een bitwise check
Wat weigert WebP-support bewust?
HotPDF decodeert de eerste VP8L-chunk van een WebP-bestand en niets anders. Lossy VP8-frames, animaties en elke container waarvan de matching chunk niet VP8L is, retourneren False uit HPDFDecodeWebPLossless, en AddImage maakt daar een exception van die het bestand noemt: Failed to decode WebP image (lossless VP8L only). Dat is een bewuste grens en geen omissie: een bestand met het verkeerde formaat moet falen waar de caller het vooraf kan converteren, in plaats van een grijze rechthoek te produceren. Het version field moet 0 zijn, de transformstack is beperkt tot vier entries en elke bounds violation werpt EWebPDecode, die de publieke entrypoint omzet in een gewone False. Decoderen bij import is ook de tegenovergestelde richting van afbeeldingen uit een geopend document terughalen, wat via het loaded-image-pad loopt dat wordt beschreven in afbeeldingen uit een geladen PDF en hun decodefilters extraheren. En elke image decoder is een parser die bestanden krijgt die je niet zelf hebt gemaakt: als WebP-assets van klanten of het publieke internet komen, zijn de bounds checks hier de ondergrens en geen bovengrens, en het sterkere antwoord is image codecs in een geïsoleerd workerproces draaien, zodat een malformed frame de host niet met zich mee naar beneden kan trekken
Het praktische resultaat is dat een Delphi- of C++Builder-applicatie nu WebP-assets op dezelfde manier in een PDF kan zetten als PNG: één call naar AddImageFromFile, één call naar ShowImage en niets extra's in het installatieprogramma. Als je de rest van de image- en documentpipeline eromheen wilt, dekt de HotPDF Delphi PDF-component de schrijf-, laad- en renderkanten vanuit dezelfde unitset