Tekninen artikkeli

WebP PDF:ksi Delphissä: HotPDF:n VP8L-dekooderi

HotPDF 2.747.0 purkaa WebP-kuvat Object Pascalilla alusta kirjoitetulla VP8L (WebP lossless) -dekooderilla, joten THotPDF.AddImageFromFile hyväksyy .webp-polun suoraan ilman toimitettavaa libwebp-DLL:ää tai käynnistettävää apuprosessia. Dekooderi toteuttaa RFC 9649:n kohdan 3 kokonaan: RIFF-säilön läpikäynnin, kanoniset prefix-koodit, LZ77-taaksepäinviittaukset, väricachen ja kaikki neljä käänteismuunnosta. Häviölliset VP8-kehykset hylätään selkeästi eikä niitä yritetä purkaa puolittain

Laukaisin oli arkinen. Suunnittelutyökalu vie jokaisen assetin WebP:nä, koska se on nykyinen oletus, assetit päätyvät lasku- tai katalogigeneraattoriin joka on syönyt PNG:tä ja JPEG:iä tyytyväisenä vuosikymmenen, ja yhtäkkiä puolet syötteistä hylätään. Ilmeinen korjaus on sitoa libwebp ja jatkaa. Ilmeinen korjaus on myös se joka muuttaa itsenäisen VCL-komponentin tuotteeksi jolla on erillinen käyttöönottojuttu

Miksi VP8L kannattaa toteuttaa libwebpin sitomisen sijaan?

HotPDF toteuttaa koodekin Pascalilla, koska omiin suoritettaviin tiedostoihinsa käännettävä Delphi-komponentti ei voi hankkia hiljaa ajonaikaista DLL:ää. Natiiviriippuvuus tarkoittaa seurattavaa 32- ja 64-bittistä binääriä, lukittavaa versiota, käyttöönoton ajavalle ihmiselle selitettävää koodin allekirjoitusketjua ja yhtä lisätiedostoa jonka virustorjunta voi lukitussa päätelaitteessa päättää olevan epäilyttävä. Komponentille jonka päämyyntilupaus on pudottaa se projektiin ja saada se toimimaan tämä on todellinen kustannus eikä teoreettinen. Toinen puoli perustelusta on että VP8L on pieni: prefix-koodi- ja LZ77-muoto neljällä käänteismuunnoksella ja 120 alkion naapuruston etäisyyskartalla, ja koko HPDFWebP.pas-dekooderi on alle 900 Pascal-riviä. THotPDF.AddImage-metodin sisällä WebP-haara on samassa laajennuksen dispatchissä joka ohjaa jo .jp2-, .j2k-, .jpt- ja .jpc-tiedostot JPEG 2000 -polulle, joten putkisto oli jo olemassa samassa kohdassa joka kuvataan artikkelissa JPEG 2000 -kuvien lisääminen PDF:iin Delphillä. Kutsujat jotka haluavat raakapikselit PDF-kuvan sijaan voivat kutsua suoraan HPDFDecodeWebPLossless-funktiota, joka täyttää TWebPCardinalArray-taulukon $AARRGGBB-arvoilla skannausrivin järjestyksessä

uses
  HPDFDoc, HPDFWebP;

var
  Pdf: THotPDF;
  Idx: Integer;
begin
  Pdf := THotPDF.Create(nil);
  try
    Pdf.FileName := 'catalog.pdf';
    Pdf.BeginDoc;
    // .webp ohjataan sisäänrakennetulle VP8L-dekooderille ilman DLL:ää
    Idx := Pdf.AddImageFromFile('product-shot.webp', icFlate);
    Pdf.CurrentPage.ShowImage(Idx, 50, 500, 240, 180, 0);
    Pdf.EndDoc;
  finally
    Pdf.Free;
  end;
end;

Miksi VP8L-bittivirtaa luetaan kahteen suuntaan yhtä aikaa?

Koska säilön bittijärjestys ja prefix-koodin bittijärjestys määritellään toisistaan riippumatta, ja VP8L valitsee niille vastakkaiset käytännöt. RFC 9649:n kohdassa 3.2 sanotaan suoraan että bittivirtaa luetaan vähiten merkitsevä bitti ensin: lukija aloittaa tavun bitistä 0 ja kulkee ylöspäin. Virran sisällä olevat kanoniset prefix-koodit tulevat eniten merkitsevä bitti ensin, puun juuresta alkaen, joten purku siirtää akkuaattoria vasemmalle ja OR-operaattori lisää jokaisen uuden bitin alas. Lukijan ja koodin kulku etenee siis saman silmukan sisällä vastakkaisiin suuntiin, mikä näyttää joka lukukerralla bugilta

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

// kanoninen kulku menee toiseen suuntaan: ensimmäinen virrasta luettu bitti
// on koodin eniten merkitsevä bitti
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;

Kolme RFC-yksityiskohtaa jotka synkronoivat virran huomaamatta väärin

RFC 9649:n kolme semantiikkaa mainitaan täsmälleen kerran, ne on helppo lukea ohi ja jokainen maksaa tai säästää yhden bitin, mikä riittää muuttamaan jokaisen myöhemmän taulukon roskaksi. Kaikki kolme löytyivät HotPDF:n VP8L-dekooderista ja kaikki kolme tuottavat saman oireen: uskottavalta näyttävän kuvan joka on kaikkialta väärä

  • Entropiakoodattu kuva ei-primary-roolissa ei kirjoita meta-prefix-bittiä lainkaan. entropy-coded-image-ABNF ei yksinkertaisesti sisällä tätä alkiota, joten sen lukeminen siirtää virtaa yhdellä bitillä. HotPDF antaa arvon AllowMeta = False itse entropiakuvalla, predictor- ja color transform -datalla sekä color-indexing-paletilla
  • Yksilehtinen prefix-koodi kuluttaa nolla bittiä. RFC 9649:n kohta 3.7.2.1 sanoo tämän suoraan, ja kanoninen kulku lukisi mielellään bitin mutta ei pystyisi sijoittamaan sitä, joten BuildHuff tunnistaa yhden symbolin kokonaismäärän, merkitsee puun Single-tilaan ja dekoodaa tuon yhden symbolin koskematta lukijaan
  • cache_bits-arvo 0 tarkoittaa color cachen kokoa 0 eikä arvoa 1 shl 0. Kätevä siirto antaa kooksi 1, jolloin vihreän aakkoston 256 + 24 + CacheSize arvoksi tulee 281 eikä 280 ja jokainen sen jälkeen luettava prefix-kooditaulukko on väärässä kohdassa
CacheBits := 0;
CacheSize := 0;                        // cache_bits = 0 tarkoittaa todella ei mitään
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: vain avaruudellisesti koodattu (ARGB) kuva sisältää
// meta-prefix-bitin; entropiakoodatut roolit eivät koskaan kirjoita sitä
if AllowMeta then
  UseMeta := BR.ReadBit
else
  UseMeta := 0;

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

Alustuksessa käytetyssä fixturessa nämä tulivat esiin bitissä 47, bitissä 81 ja bitissä 89 tässä järjestyksessä. Näiden numeroiden merkitys on juuri tässä. Yksikään virheistä ei ilmoittautunut yhden poikkeamana; jokainen näyttäytyi kuvana joka dekoodautui loppuun asti ja näytti staattiselta, ja ainoa erottava asia oli tarkka bitin positio jossa virta lakkasi vastaamasta viitettä

Mitä bit-position diffaus antaa?

Bit-position diffaus muuttaa hyödyttömän kysymyksen yhden rivin kysymykseksi: ei "miksi tämä kuva on väärä" vaan "miksi virta erkani bitissä 81". Asetelma on halpa. Pillow kirjoittaa jokaisen .webp-fixturen sekä saman kuvan omasta purustaan syntyvän .rgba-vedoksen; Pascal-probe ja pieni Python-viitemalli kirjaavat jokaisen luvun rinnalle juoksevan bittilaskurin; ensimmäinen kohta jossa lokit poikkeavat on kohta jossa bugi on. Aloita fixtuurella joka käyttää mahdollisimman vähän ominaisuuksia: tasainen 32x32-kuva joka kulkee vain yksinkertaisen koodin polkua. Saat se toimimaan, lisää sitten liukuvärit, oudot mitat ja alfa yksi fixtuuri kerrallaan. Bittijärjestyksen arvaaminen on vaihtoehto jolla päivä katoaa

Rehellinen huomautus on, että viitekin oli väärässä. Python-malli unohti lukea cache_bits-arvon eikä sen muunnossilmukka kulkenut loppuun, joten jotkin poikkeamakohdat olivat viitedekooderin synkronoinnin menetystä eivätkä Pascalin. Väärä viitetoteutus ei tee testattavasta toteutuksesta oikeaa, eikä kumpikaan puoli saa hyötyä epäilystä: jokainen poikkeama on ratkaistava RFC-tekstin perusteella. Hae teksti myös lähteestä. Hakuyhteenvedot sotkevat rutiininomaisesti numeeriset taulukot, ja 120 alkion etäisyyskartta, 14 predictor-tilaa sekä color cache -kerroin $1e35a7bd täytyy kaikki kirjoittaa täsmälleen

Missä Pascalin kokonaislukujako eroaa C:stä

VP8L:n color transform on 3,5 kiinteän pisteen muoto signed-delta-arvoilla, ja juuri siinä Pascal ja C lakkaavat olemasta samaa mieltä. C siirtää negatiivisia kokonaislukuja aritmeettisesti, mikä pyöristää alaspäin; Pascalin div katkaisee kohti nollaa. Jokaisen negatiivisen tulon kohdalla nämä eroavat yhdellä, joten käänteinen värimuunnos ajautuu koko kuvan läpi yhden kanava-askeleen pikseliä kohden. HotPDF tekee siksi pyöristyksen alas eksplisiittisesti FloorDiv32-funktiossa sen sijaan että luottaisi div-operaattoriin

// C siirtää aritmeettisesti ja pyöristää negatiiviset alas; Pascalin div katkaisee
// kohti nollaa, joten negatiivinen tapaus tarvitsee eksplisiittisen korjauksen
function FloorDiv32(V: Integer): Integer;
begin
  Result := V div 32;
  if (V < 0) and (V mod 32 <> 0) then
    Dec(Result);
end;

// 3,5:n kiinteän pisteen delta muunnoselementin tavun ja
// värikanavan tavun välillä, molemmat ensin sign-laajennettuina
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ämä virheluokka kannattaa nimetä, koska se on näkymätön jokaisessa testissä jonka fixturet sattuvat tuottamaan vain epänegatiivisia tuloja, joissa div ja floor ovat sama asia. Se on myös syy siihen että HotPDF:n WebP-testit tarkistavat pikselintarkan yhtäsuuruuden samoista tiedostoista tehtyjen Pillow-purkujen kanssa toleranssin sijaan: liukuvärit, omituinen 100x37-koko, 40x40-kuva oikealla alfakanavalla ja tasainen 32x32-kuva, jokainen pikseli verrataan bitti bitiltä. Yhden askeleen ajautuminen läpäisee havaintoon perustuvan tarkistuksen ja epäonnistuu bittitarkassa

Mitä WebP-tuki kieltäytyy tarkoituksella tekemästä

HotPDF purkaa WebP-tiedoston ensimmäisen VP8L-chunkin eikä mitään muuta. Häviölliset VP8-kehykset, animaatiot ja jokainen säilö jonka täsmäävä chunk ei ole VP8L palauttavat arvon False funktiosta HPDFDecodeWebPLossless, ja AddImage muuttaa tämän poikkeukseksi joka nimeää tiedoston: Failed to decode WebP image (lossless VP8L only). Tämä on tarkoituksellinen raja eikä unohdus: väärän formaatin tiedoston pitäisi epäonnistua kohdassa jossa kutsuja voi muuntaa sen etukäteen eikä tuottaa harmaata suorakulmiota. Version kentän täytyy olla 0, muunnospinon enimmäiskoko on neljä alkiota ja jokainen rajanylitys nostaa EWebPDecode-poikkeuksen, jonka julkinen entry point muuttaa tavalliseksi False-arvoksi. Tuonnin aikainen dekoodaus on myös vastakkainen suunta dokumentista kuvien hakemiselle, joka kulkee avatun dokumentin ladattujen kuvien polun kautta ja kuvataan artikkelissa kuvien poiminta ladatusta PDF:stä ja niiden dekoodausfilttien käsittely. Kaikki kuvadekooderit ovat myös parserita joita syötetään tiedostoilla joita et itse luonut: jos WebP-assetit tulevat asiakkailta tai avoimesta internetistä, nämä rajatarkistukset ovat lattia eivät katto ja vahvempi vastaus on kuvakoodekkien ajaminen eristetyssä worker-prosessissa, jotta virheellinen kehys ei kaada isäntäprosessia mukanaan

Käytännön tulos on että Delphi- tai C++Builder-sovellus voi nyt lisätä WebP-assetit PDF:ään samalla tavalla kuin PNG:n: yksi kutsu AddImageFromFile-metodille, yksi kutsu ShowImage-metodille eikä mitään ylimääräistä asennusohjelmaan. Jos haluat ympärille muun kuva- ja dokumenttiputken, HotPDF Delphi PDF -komponentti kattaa kirjoitus-, lataus- ja renderöintipuolet samasta unit-joukosta