HotPDF 2.747.0 giải mã image WebP bằng decoder VP8L (WebP lossless) được viết từ đầu bằng Object Pascal, nên THotPDF.AddImageFromFile nhận trực tiếp path .webp, không cần ship libwebp DLL hay chạy helper process. Decoder triển khai đầy đủ section 3 của RFC 9649: walk RIFF container, canonical prefix code, backward reference LZ77, color cache và cả bốn inverse transform. Frame VP8 lossy bị từ chối rõ ràng thay vì decode nửa vời
Trigger rất đời thường. Design tool export mọi asset thành WebP vì đó là default hiện đại, asset đi vào invoice hoặc catalog generator vốn đã xử lý PNG và JPEG suốt một thập kỷ, rồi đột nhiên một nửa input bị reject. Cách sửa hiển nhiên là bind libwebp rồi tiếp tục. Cũng chính cách sửa hiển nhiên đó biến VCL component tự chứa thành thứ phải có deployment story
Vì sao triển khai VP8L thay vì bind libwebp?
HotPDF triển khai codec bằng Pascal vì một Delphi component mà customer compile vào executable riêng không thể âm thầm kéo theo runtime DLL. Native dependency nghĩa là phải theo dõi binary 32-bit và 64-bit, pin version, giải thích code-signing chain với người chạy deployment và thêm một file mà antivirus trên terminal bị khóa có thể không thích. Với component có giá trị chính là drop vào project rồi chạy, đó là cost thật chứ không phải lý thuyết. Nửa còn lại của lập luận là VP8L nhỏ: format prefix-code cộng LZ77 với bốn inverse transform và map khoảng cách neighborhood 120 entry, còn toàn bộ decoder trong HPDFWebP.pas dưới 900 dòng Pascal. Trong THotPDF.AddImage, nhánh WebP nằm cùng extension dispatch đã route .jp2, .j2k, .jpt và .jpc qua JPEG 2000 path, nên plumbing đã sẵn ở đó, đúng vị trí được mô tả trong walkthrough thêm image JPEG 2000 vào PDF trong Delphi. Caller muốn raw pixel thay vì PDF image có thể gọi thẳng HPDFDecodeWebPLossless, method này điền TWebPCardinalArray gồm value $AARRGGBB theo thứ tự scan-line
uses
HPDFDoc, HPDFWebP;
var
Pdf: THotPDF;
Idx: Integer;
begin
Pdf := THotPDF.Create(nil);
try
Pdf.FileName := 'catalog.pdf';
Pdf.BeginDoc;
// .webp được dispatch tới VP8L decoder tích hợp, không liên quan DLL
Idx := Pdf.AddImageFromFile('product-shot.webp', icFlate);
Pdf.CurrentPage.ShowImage(Idx, 50, 500, 240, 180, 0);
Pdf.EndDoc;
finally
Pdf.Free;
end;
end;
Vì sao VP8L bitstream được đọc theo hai hướng cùng lúc?
Vì bit order của container và bit order của prefix code được quy định độc lập, còn VP8L chọn convention ngược nhau cho hai phần. RFC 9649 section 3.2 nói rõ bitstream được đọc least significant bit first: reader bắt đầu ở bit 0 của byte rồi đi lên. Canonical prefix code nằm trong stream lại đến theo most significant bit first, root của tree trước, nên decode walk shift accumulator sang trái rồi OR từng bit mới vào đáy. Reader và code walk vì thế chạy ngược hướng bên trong cùng một loop, trông như bug mỗi lần đọc lại
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 chạy theo hướng kia: bit đầu tiên lấy khỏi stream
// là most significant bit của 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;
Ba chi tiết RFC âm thầm làm lệch stream
Có ba semantics trong RFC 9649 chỉ được nói một lần, rất dễ đọc lướt qua, và mỗi cái tiêu tốn hoặc tiết kiệm một bit, đủ để biến mọi table phía sau thành noise. Cả ba đều từng xuất hiện trong HotPDF VP8L decoder, và cả ba cho cùng triệu chứng: image trông có vẻ hợp lý nhưng sai ở mọi nơi
- Image entropy-coded ở role không phải primary không ghi meta-prefix bit nào. ABNF của
entropy-coded-imageđơn giản không chứa item đó, nên đọc thêm một bit sẽ desynchronize stream. HotPDF truyềnAllowMeta = Falsecho chính entropy image, predictor và color transform data, cùng color-indexing palette - Prefix code chỉ có một leaf consume zero bit. RFC 9649 section 3.7.2.1 nói trực tiếp như vậy, còn canonical walk sẽ vui vẻ đọc một bit rồi không đặt được nó vào đâu, nên
BuildHuffphát hiện total symbol count bằng 1 và đánh dấu tree làSingle, decode symbol duy nhất mà không đụng reader - Giá trị cache_bits bằng 0 nghĩa là color cache size bằng 0, không phải
1 shl 0. Phép shift tiện lợi cho ra 1, khiến green alphabet256 + 24 + CacheSizethành 281 thay vì 280, và mọi prefix code table đọc sau đó đều bị lệch
CacheBits := 0;
CacheSize := 0; // cache_bits = 0 thực sự có nghĩa là không có cache
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: chỉ image spatially coded (ARGB) có meta prefix bit;
// role entropy-coded không bao giờ ghi nó
if AllowMeta then
UseMeta := BR.ReadBit
else
UseMeta := 0;
// ...
ReadHuffCode(256 + 24 + CacheSize, Groups[I].Green); // 280, không phải 281
ReadHuffCode(256, Groups[I].Red);
ReadHuffCode(256, Groups[I].Blue);
ReadHuffCode(256, Groups[I].Alpha);
ReadHuffCode(40, Groups[I].Dist);
Trong fixture dùng lúc bring-up, ba lỗi xuất hiện ở bit 47, bit 81 và bit 89, theo thứ tự đó. Các con số là trọng tâm của section này. Không lỗi nào tự báo là off-by-one; mỗi lỗi hiện ra như một image decode xong nhưng giống static, và thứ duy nhất tách chúng ra là bit position chính xác tại đó stream ngừng khớp reference
Bit-position diffing đem lại gì?
Bit-position diffing biến một câu hỏi vô dụng thành câu hỏi một dòng: không phải vì sao ảnh này sai mà là vì sao stream diverge ở bit 81. Setup rẻ. Pillow ghi mỗi fixture .webp cùng dump .rgba từ việc decode chính image đó; một Pascal probe và Python reference model nhỏ đều log running bit counter cạnh mỗi read; position đầu tiên hai log bất đồng là nơi bug nằm. Bắt đầu bằng fixture ít tính năng nhất: image phẳng 32 x 32 chỉ đi qua simple-code path. Làm nó đúng trước, rồi thêm gradient, kích thước lẻ và alpha từng fixture một. Đoán bit order thay vào đó là cách tiêu cả ngày
Caveat trung thực là reference cũng sai. Python model quên đọc cache_bits và transform loop của nó không chạy đến cuối, nên một số divergence point là reference decoder mất sync chứ không phải Pascal. Reference implementation sai không làm implementation đang test tự động đúng, và không bên nào được hưởng lợi từ việc bỏ qua: mỗi divergence phải được adjudicate dựa trên text RFC. Hãy lấy text đó từ source. Search summary thường làm sai numeric table, còn distance map 120 entry, 14 predictor mode và color cache multiplier $1e35a7bd đều phải chép chính xác
Phép chia integer Pascal lệch C ở đâu
Color transform VP8L là fixed point 3.5 với signed delta, và đó là nơi Pascal cùng C không còn đồng thuận. C shift integer âm theo arithmetic nên floor; Pascal div truncate về zero. Với mọi product âm, hai cách lệch nhau một, vì vậy inverse color transform trôi một channel step trên toàn image. HotPDF explicit floor trong FloorDiv32 thay vì dựa vào div
// C shift theo arithmetic và floor với số âm; Pascal div truncate
// về zero, nên trường hợp âm cần correction tường minh
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 giữa byte transform element và byte color channel,
// cả hai đều được sign-extend trước
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;
Lớp defect này đáng được gọi tên vì nó vô hình trong mọi test có fixture tình cờ tạo product không âm, nơi div và floor trùng nhau. Đây cũng là lý do HotPDF WebP test assert pixel-exact equality với Pillow decode của cùng file thay vì tolerance: gradient, kích thước lẻ 100 x 37, image 40 x 40 có alpha thật và image phẳng 32 x 32, từng pixel đều được so sánh bit by bit. Drift một bước pass được perceptual check nhưng fail bitwise check
WebP support cố ý từ chối điều gì?
HotPDF decode chunk VP8L đầu tiên của WebP và không làm gì thêm. VP8 lossy frame, animation và container có matching chunk không phải VP8L đều trả về False từ HPDFDecodeWebPLossless, còn AddImage biến nó thành exception nêu tên file: Failed to decode WebP image (lossless VP8L only). Đây là boundary có chủ ý chứ không phải thiếu sót: file sai format nên fail tại nơi caller có thể pre-convert, thay vì tạo gray rectangle. Version field phải là 0, transform stack bị giới hạn ở bốn entry và mọi bounds violation raise EWebPDecode, sau đó public entry point chuyển thành False. Decode lúc import cũng là hướng ngược với việc kéo image ra khỏi document đã mở, vốn đi qua loaded-image path được mô tả trong extract image từ PDF đã load và decode filter của chúng. Mọi image decoder đều là parser nhận file bạn không tạo: nếu WebP asset đến từ customer hoặc internet công cộng, bounds check ở đây chỉ là floor chứ chưa phải ceiling, còn câu trả lời mạnh hơn là chạy image codec trong worker process cô lập để frame malformed không kéo host sập theo
Kết quả thực tế là ứng dụng Delphi hoặc C++Builder giờ có thể đưa WebP asset vào PDF giống như đưa PNG: một call tới AddImageFromFile, một call tới ShowImage, installer không có gì thêm. Nếu bạn muốn toàn bộ image và document pipeline xung quanh nó, HotPDF Delphi PDF component bao phủ phía writing, loading và rendering từ cùng bộ unit