Tehnični članak

WebP v PDF v Delphiju: znotraj dekodirnika HotPDF VP8L

HotPDF 2.747.0 dekodira slike WebP z dekodirnikom VP8L (WebP brez izgub), napisanim na novo v Object Pascalu, zato THotPDF.AddImageFromFile pot .webp sprejme neposredno, brez dostave DLL-a libwebp in brez zagona pomožnega procesa. Dekodirnik v celoti izvaja RFC 9649, razdelek 3: sprehod vsebnika RIFF, kanonične predponske kode, povratne sklice LZ77, barvni predpomnilnik in vse štiri inverzne transformacije. Okvirji VP8 z izgubami se glasno zavrnejo, namesto da bi se napol dekodirali

Sprožilec je bil vsakdanji. Oblikovalsko orodje vsak vir izvozi kot WebP, ker je to sodobna privzeta možnost, viri končajo v generatorju računov ali katalogov, ki že desetletje brez težav sprejema PNG in JPEG, nato pa je nenadoma polovica vhodov zavrnjena. Očiten popravek je povezati libwebp in nadaljevati. Očiten popravek je tudi tisti, ki iz samostojne komponente VCL naredi nekaj z zgodbo o nameščanju

Zakaj implementirati VP8L namesto povezati libwebp?

HotPDF kodek implementira v Pascalu, ker komponenta Delphi, ki jo stranke prevedejo v lastno izvršno datoteko, ne more tiho dobiti izvajalnega DLL-a. Izvorna odvisnost pomeni binarno datoteko za 32-bitni in 64-bitni sistem, različico za zaklepanje, verigo podpisovanja kode, ki jo je treba razložiti izvajalcu namestitve, in še eno datoteko, ki se je protivirusna programska oprema na zaklenjenem terminalu lahko odloči ne marati. Za komponento, katere glavna prodajna prednost je, da jo dodaš v projekt in deluje, je to resničen strošek, ne teoretičen. Druga polovica argumenta je, da je VP8L majhen: format s predponskimi kodami in LZ77, s štirimi inverznimi transformacijami in zemljevidom razdalj s 120 vnosi, celoten dekodirnik v HPDFWebP.pas pa ima manj kot 900 vrstic Pascala. Znotraj THotPDF.AddImage veja WebP sedi v isti izbiri po priponi, ki .jp2, .j2k, .jpt in .jpc že pošilja skozi pot JPEG 2000, zato je bila napeljava že tam, na istem mestu, ki je opisano v vodiču o dodajanju slik JPEG 2000 v PDF-je v Delphiju. Klicatelji, ki želijo surove piksle namesto slike PDF, lahko gredo neposredno v HPDFDecodeWebPLossless, ki zapolni TWebPCardinalArray vrednosti $AARRGGBB v vrstnem redu vrstic skeniranja

uses
  HPDFDoc, HPDFWebP;

var
  Pdf: THotPDF;
  Idx: Integer;
begin
  Pdf := THotPDF.Create(nil);
  try
    Pdf.FileName := 'catalog.pdf';
    Pdf.BeginDoc;
    // .webp se pošlje v vgrajeni dekodirnik VP8L, brez 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;

Zakaj se bitni tok VP8L bere v dveh smereh hkrati?

Ker sta vrstni red bitov vsebnika in vrstni red bitov predponske kode določena neodvisno, VP8L pa za njiju izbere nasprotni konvenciji. RFC 9649, razdelek 3.2, jasno določa, da se bitni tok bere od najmanj pomembnega bita naprej: bralnik začne pri bitu 0 bajta in hodi navzgor. Kanonične predponske kode, ki so v tem toku, prispejo od najpomembnejšega bita naprej, od korena drevesa, zato dekodirni sprehod akumulator pomakne v levo in vsak nov bit z OR doda na dno. Bralnik in sprehod kode torej v isti zanki tečeta v nasprotnih smereh, kar je videti kot napaka vsakič, ko kodo znova bereš

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

// kanonični sprehod teče v drugo smer: prvi bit iz toka je
// najpomembnejši bit kode
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 podrobnosti RFC, ki tiho desinhronizirajo tok

Tri semantike v RFC 9649 so zapisane natanko enkrat, zlahka jih spregledamo in vsaka stane ali prihrani en bit, kar je dovolj, da vsaka poznejša tabela postane šum. Vse tri so bile odkrite v dekodirniku HotPDF VP8L in vse tri povzročijo isti simptom: sliko, ki je videti verjetna, vendar je povsod napačna

  • Entropijsko kodirana slika v neprimarni vlogi ne zapiše nobenega metapredponskega bita. ABNF za entropy-coded-image tega elementa preprosto ne vsebuje, zato branje enega toka desinhronizira za bit. HotPDF za samo entropijsko sliko, za podatke transformacije napovedovalnika in barvno transformacijo ter za paleto barvnega indeksiranja posreduje AllowMeta = False
  • Predponska koda z enim listom porabi nič bitov. RFC 9649, razdelek 3.7.2.1, to pove neposredno, kanonični sprehod pa bi z veseljem prebral bit in ga nato ne bi znal postaviti, zato BuildHuff zazna skupno število simbolov 1 in drevo označi kot Single, ta en simbol pa dekodira brez dotika bralnika
  • Vrednost cache_bits 0 pomeni, da je velikost barvnega predpomnilnika 0, ne 1 shl 0. Priročen pomik da 1, zaradi česar abeceda zelene barve 256 + 24 + CacheSize pride 281 namesto 280 in je vsaka tabela predponskih kod, prebrana za tem, napačno poravnana
CacheBits := 0;
CacheSize := 0;                        // cache_bits = 0 res pomeni 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: samo prostorsko kodirana slika (ARGB) nosi
// metapredponski bit; entropijsko kodirane vloge ga nikoli ne zapišejo
if AllowMeta then
  UseMeta := BR.ReadBit
else
  UseMeta := 0;

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

Na testnem primeru, uporabljenem med zagonom, so se tri pokazale pri bitu 47, bitu 81 in bitu 89, v tem vrstnem redu. To je namen tega odseka. Nobena od treh se ni javila kot napaka za ena; vsaka je bila videti kot slika, ki se je dekodirala do konca in je bila podobna šumu, ločila pa jih je samo natančna bitna pozicija, pri kateri se tok ni več ujemal z referenco

Kaj pridobiš z razlikovanjem po položaju bita?

Razlikovanje po položaju bita neuporabno vprašanje spremeni v vprašanje ene vrstice: ne »zakaj je ta slika napačna«, temveč »zakaj se je tok razšel pri bitu 81«. Priprava je poceni. Pillow zapiše vsak testni primer .webp skupaj z izpisom .rgba, ki ga iz iste slike ustvari sam; Pascalova sonda in majhen referenčni model v Pythonu ob vsakem branju beležita tekoči števec bitov; prvi položaj, kjer se dnevnika ne ujemata, je mesto napake. Začnite s primerom, ki izvaja čim manj: ravna slika 32x32, ki potrebuje samo pot preproste kode. Ko je ta pravilna, dodajajte prelive, nenavadne dimenzije in alfo, po en primer naenkrat. Ugibanje vrstnega reda bitov je način, kako porabiti cel dan

Poštena pripomba je, da je bila napačna tudi referenca. Pythonov model je pozabil prebrati cache_bits, njegova zanka transformacij pa se ni izvedla do konca, zato so bile nekatere točke razhoda referenčni dekodirnik, ki je izgubil sinhronizacijo, ne Pascalov dekodirnik. Napačna referenčna implementacija ne pomeni, da je implementacija v preizkusu pravilna, in nobena stran nima koristi od dvoma: vsak razhod je treba razsoditi glede na besedilo RFC. Tudi to besedilo pridobite iz vira. Povzetki iskanja rutinsko popačijo številske tabele, zemljevid razdalj s 120 vnosi, 14 načinov napovedovalnika in množilnik barvnega predpomnilnika $1e35a7bd pa morajo biti prepisani natanko

Kje se celoštevilsko deljenje Pascala razlikuje od C

Barvna transformacija VP8L je fiksna vejica 3.5 s predznačenimi odmiki in prav tu se Pascal in C nehata strinjati. C negativna cela števila pomika aritmetično, kar zaokroži navzdol; Pascalov div odreže proti ničli. Pri vsakem negativnem produktu se razlikujeta za ena, zato se inverzna barvna transformacija čez celotno sliko premakne za en kanalni korak na piksel. HotPDF zato tla izrecno izvede v FloorDiv32, namesto da bi se zanesel na div

// C aritmetično pomika in pri negativnih vrednostih zaokroži navzdol; Pascalov div odreže
// proti ničli, zato negativni primer potrebuje izrecni popravek
function FloorDiv32(V: Integer): Integer;
begin
  Result := V div 32;
  if (V < 0) and (V mod 32 <> 0) then
    Dec(Result);
end;

// fiksni odmik 3.5 med bajtom elementa transformacije in bajtom
// barvnega kanala, pri čemer sta oba najprej razširjena s predznakom
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;

Ta razred napake je vredno poimenovati, ker je neviden pri vsakem testu, katerega primeri po naključju ustvarijo ne-negativne produkte, pri katerih se div in tla ujemata. Zato tudi testi HotPDF WebP zahtevajo natančno enakost pikslov glede na dekodiranja Pillow istih datotek, ne tolerance: prelivi, nenavadna velikost 100x37, slika 40x40 z resničnim kanalom alfa in ravna slika 32x32, vsak piksel se primerja bit za bitom. Premik za en korak prestane zaznavni preizkus in pade pri bitnem

Česa podpora WebP namerno ne sprejema

HotPDF dekodira prvi kos VP8L datoteke WebP in ničesar drugega. Okvirji VP8 z izgubami, animacije in vsak vsebnik, katerega ujemajoči se kos ni VP8L, vrnejo False iz HPDFDecodeWebPLossless, AddImage pa to spremeni v izjemo, ki poimenuje datoteko: Failed to decode WebP image (lossless VP8L only). To je namerna meja, ne spregled: datoteka napačnega formata mora odpovedati tam, kjer jo klicatelj lahko predhodno pretvori, ne pa ustvariti sivega pravokotnika. Polje različice mora biti 0, sklad transformacij je omejen na štiri vnose, vsaka kršitev meja pa sproži EWebPDecode, ki jo javna vstopna točka spremeni v navaden False. Dekodiranje pri uvozu je tudi nasprotna smer od izvlečenja slik iz odprtega dokumenta, ki poteka po poti naloženih slik, opisani pri izločanju slik iz naloženega PDF-ja in njihovih dekodirnih filtrov. Poleg tega je vsak dekodirnik slik razčlenjevalnik, ki ga hranijo datoteke, ki jih niste ustvarili: če viri WebP pridejo od strank ali z javnega interneta, so tukajšnja preverjanja meja tla, ne strop, močnejši odgovor pa je izvajanje slikovnih kodekov v izoliranem delovnem procesu, da pokvarjen okvir s seboj ne sesuje gostitelja

Praktični rezultat je, da lahko aplikacija Delphi ali C++Builder zdaj v PDF vstavi vire WebP enako kot PNG: en klic AddImageFromFile, en klic ShowImage, ničesar dodatnega v namestitvenem programu. Če želite preostali slikovni in dokumentni cevovod okoli tega, komponenta HotPDF Delphi PDF pokriva pisanje, nalaganje in izrisovanje iz istega nabora enot