HotPDF 2.747.0 decode-ва WebP images с VP8L (WebP lossless) decoder, написан от нулата на Object Pascal, така че THotPDF.AddImageFromFile приема директно path към .webp, без libwebp DLL за доставяне и без helper process за стартиране. Decoder-ът имплементира изцяло RFC 9649 section 3: RIFF container walk, canonical prefix codes, LZ77 backward references, color cache и четирите inverse transforms. Lossy VP8 frames се отказват шумно, вместо да бъдат half-decoded
Trigger-ът беше прозаичен. Design tool export-ва всеки asset като WebP, защото това е modern default, asset-ите попадат в invoice или catalog generator, който десетилетие с удоволствие е приемал PNG и JPEG, и внезапно половината inputs се отхвърлят. Очевидната поправка е да bind-нете libwebp и да продължите. Очевидната поправка е и тази, която превръща self-contained VCL component-а в нещо с deployment story
Защо да имплементирате VP8L, вместо да bind-нете libwebp?
HotPDF имплементира codec-а на Pascal, защото Delphi component, който customer-ите compile-ват в собствените си executables, не може тихо да придобие runtime DLL. Native dependency означава 32-bit и 64-bit binary за следене, version за pin-ване, code-signing chain за обясняване на човека, който изпълнява deployment-а, и още един file, който antivirus software на locked-down terminal може да реши, че не харесва. За component, чиято основна стойност е да го добавите към project и да работи, това е реална цена, не теоретична. Другата половина на аргумента е, че VP8L е малък: prefix-code плюс LZ77 format с четири inverse transforms и 120-entry neighborhood distance map, а целият decoder в HPDFWebP.pas е под 900 lines Pascal. Вътре в THotPDF.AddImage WebP branch-ът стои при същия extension dispatch, който вече насочва .jp2, .j2k, .jpt и .jpc през JPEG 2000 path-а, така че plumbing-ът вече е съществувал, на същото място, описано в walkthrough-а за добавяне на JPEG 2000 images към PDF в Delphi. Caller-ите, които искат raw pixels вместо PDF image, могат да отидат директно към HPDFDecodeWebPLossless, който запълва TWebPCardinalArray с $AARRGGBB values в scan-line order
uses
HPDFDoc, HPDFWebP;
var
Pdf: THotPDF;
Idx: Integer;
begin
Pdf := THotPDF.Create(nil);
try
Pdf.FileName := 'catalog.pdf';
Pdf.BeginDoc;
// .webp се dispatch-ва към built-in VP8L decoder, без DLL
Idx := Pdf.AddImageFromFile('product-shot.webp', icFlate);
Pdf.CurrentPage.ShowImage(Idx, 50, 500, 240, 180, 0);
Pdf.EndDoc;
finally
Pdf.Free;
end;
end;
Защо VP8L bitstream се чете в две посоки едновременно?
Защото container bit order-ът и prefix code bit order-ът са специфицирани независимо, а VP8L избира противоположни conventions за тях. RFC 9649 section 3.2 казва ясно, че bitstream се чете least significant bit first: reader-ът започва от bit 0 на byte и върви нагоре. Canonical prefix codes, съдържащи се в stream-а, пристигат most significant bit first, root-а на tree първо, така че decode walk-ът shift-ва accumulator-а наляво и OR-ва всеки нов bit отдолу. Reader и code walk следователно се движат в противоположни посоки в един и същ loop, което всеки път изглежда като bug, когато го прочетете отново
function TWebPBitReader.ReadBit: Integer;
begin
if BytePos >= Length(Data) then
raise EWebPDecode.Create('WebP bitstream exhausted');
Result := (Data[BytePos] shr BitPos) and 1; // LSB first, RFC 9649 3.2
Inc(BitPos);
if BitPos = 8 then
begin
BitPos := 0;
Inc(BytePos);
end;
end;
// canonical walk-ът върви в другата посока: първият bit от stream-а
// е most significant bit на 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;
Три RFC details, които тихо разсинхронизират stream-а
Три semantics в RFC 9649 са казани точно веднъж, лесно се пропускат и всяка струва или спестява един bit, което е достатъчно да превърне всяка следваща table в noise. И трите бяха открити в HotPDF VP8L decoder-а и трите дават един и същ symptom: image, който изглежда правдоподобно, но е грешен навсякъде
- Entropy-coded image в non-primary role не записва meta-prefix bit изобщо. ABNF за
entropy-coded-imageпросто не съдържа item-а, така че прочитането на един desynchronize-ва stream-а с bit. HotPDF подаваAllowMeta = Falseза самия entropy image, за predictor и color transform data и за color-indexing palette - Single-leaf prefix code консумира zero bits. RFC 9649 section 3.7.2.1 го казва директно, а canonical walk би прочел bit и после не би успял да го постави, затова
BuildHuffразпознава total symbol count 1 и маркира tree-тоSingle, като decode-ва този един symbol без да докосва reader-а - Стойност cache_bits 0 означава color cache size 0, а не
1 shl 0. Удобният shift дава 1, което прави green alphabet-а256 + 24 + CacheSizeда излезе 281 вместо 280 и всяко prefix code table read след него се подравнява грешно
CacheBits := 0;
CacheSize := 0; // cache_bits = 0 наистина означава none
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: само spatially coded (ARGB) image носи
// meta prefix bit; entropy-coded roles никога не го записват
if AllowMeta then
UseMeta := BR.ReadBit
else
UseMeta := 0;
// ...
ReadHuffCode(256 + 24 + CacheSize, Groups[I].Green); // 280, не 281
ReadHuffCode(256, Groups[I].Red);
ReadHuffCode(256, Groups[I].Blue);
ReadHuffCode(256, Groups[I].Alpha);
ReadHuffCode(40, Groups[I].Dist);
Във fixture-а, използван при bring-up, трите се появиха на bit 47, bit 81 и bit 89, в този ред. Тези числа са смисълът на section-а. Нито едно от трите не се обяви като off-by-one; всяко се представи като image, който се decode-ва докрай и изглежда като static, а единственото, което ги раздели, беше точната bit position, на която stream-ът спря да съвпада с reference
Какво дава bit-position diffing?
Bit-position diffing превръща безполезен въпрос в едноредов: не защо тази picture е грешна, а защо stream-ът се размина на bit 81. Setup-ът е евтин. Pillow записва всеки .webp fixture плюс .rgba dump на собствения си decode на същото image; Pascal probe и малък Python reference model log-ват running bit counter до всеки read; първата позиция, на която двата log-а не съвпадат, е мястото на bug-а. Започнете с fixture, който упражнява възможно най-малко: flat image 32x32, което използва само simple-code path. Направете него green, после добавяйте gradients, odd dimensions и alpha по един fixture. Да гадаете bit order е начин да загубите един ден
Честната caveat е, че и reference-ът е бил грешен. Python model-ът е пропуснал да прочете cache_bits, а transform loop-ът му не е стигал до completion, така че някои divergence points са били reference decoder-ът, който губи sync, а не Pascal decoder-ът. Грешна reference implementation не прави implementation-а под test правилен и нито една страна не получава benefit of the doubt: всяко divergence трябва да бъде adjudicated спрямо RFC text. Изтеглете и този text от source-а. Search summaries редовно повреждат numeric tables, а 120-entry distance map, 14 predictor modes и color cache multiplier $1e35a7bd трябва да бъдат transcribe-нати точно
Къде Pascal integer division се разминава с C
VP8L color transform е 3.5 fixed point със signed deltas и точно там Pascal и C спират да са съгласни. C shift-ва negative integers arithmetically, което floor-ва; Pascal div truncates-ва към zero. За всеки negative product двете се различават с един, затова inverse color transform-ът се измества с една channel step на pixel през целия image. Затова HotPDF прави floor explicit във FloorDiv32, вместо да разчита на div
// C shift-ва аритметично и floor-ва при negative; Pascal div truncates-ва
// към zero, затова negative case се нуждае от explicit correction
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 между transform element byte и
// color channel byte, и двата sign-extended първо
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;
Този клас defect си струва да бъде назован, защото е невидим за всеки test, чиито fixtures случайно produce-ват non-negative products, при които div и floor съвпадат. Това е и причината HotPDF WebP tests да assert-ват pixel-exact equality спрямо Pillow decodes на същите files, а не tolerance: gradients, odd 100x37 size, 40x40 image с реален alpha channel и flat 32x32 image, всеки pixel сравнен bit по bit. One-step drift минава perceptual check и fail-ва bitwise
Какво WebP support-ът умишлено отказва
HotPDF decode-ва първия VP8L chunk на WebP file и нищо друго. Lossy VP8 frames, animations и всеки container, чийто matching chunk не е VP8L, връщат False от HPDFDecodeWebPLossless, а AddImage превръща това в exception, назоваващ file-а: Failed to decode WebP image (lossless VP8L only). Това е умишлена boundary, не пропуск: file с грешен формат трябва да fail-не там, където caller може да го pre-convert-не, вместо да произведе gray rectangle. Version field-ът трябва да е 0, transform stack-ът е ограничен до четири entries и всяко bounds violation вдига EWebPDecode, което public entry point-ът превръща в plain False. Decoding on import е и обратната посока спрямо изваждането на pictures от document, който сте отворили, което минава през loaded-image path-а, описан в извличане на images от loaded PDF и техните decode filters. И всеки image decoder е parser, захранен с files, които не сте създали: ако WebP assets пристигат от customers или public internet, bounds checks тук са minimum, а по-силният отговор е image codecs в isolated worker process, така че malformed frame да не свали host-а със себе си
Практическият резултат е, че Delphi или C++Builder application вече може да сложи WebP assets в PDF по същия начин, по който слага PNG: един call към AddImageFromFile, един call към ShowImage, нищо допълнително в installer-а. Ако искате останалия image и document pipeline около него, HotPDF Delphi PDF component покрива страните за writing, loading и rendering от същия набор unit-и