Το HotPDF 2.747.0 αποκωδικοποιεί WebP images με VP8L decoder (WebP lossless) γραμμένο από την αρχή σε Object Pascal, οπότε το THotPDF.AddImageFromFile δέχεται απευθείας path .webp, χωρίς libwebp DLL για διανομή και χωρίς helper process για εκκίνηση. Ο decoder υλοποιεί πλήρως το RFC 9649 section 3: walk του RIFF container, canonical prefix codes, LZ77 backward references, color cache και τα τέσσερα inverse transforms. Τα lossy VP8 frames απορρίπτονται ρητά αντί να αποκωδικοποιούνται μισά
Η αφορμή ήταν πεζή. Ένα design tool εξάγει κάθε asset ως WebP επειδή αυτό είναι το σύγχρονο default, τα assets καταλήγουν σε invoice ή catalog generator που εδώ και μία δεκαετία δέχεται PNG και JPEG και ξαφνικά τα μισά inputs απορρίπτονται. Η προφανής διόρθωση είναι να συνδέσεις libwebp και να προχωρήσεις. Η προφανής διόρθωση είναι επίσης αυτή που μετατρέπει ένα self-contained VCL component σε κάτι με deployment story
Γιατί να υλοποιηθεί VP8L αντί να συνδεθεί το libwebp
Το HotPDF υλοποιεί τον codec σε Pascal επειδή ένα Delphi component που οι customers μεταγλωττίζουν μέσα στο δικό τους executable δεν μπορεί να αποκτήσει σιωπηρά runtime DLL. Native dependency σημαίνει binary 32-bit και 64-bit για παρακολούθηση, version για pinning, code-signing chain για εξήγηση σε όποιον εκτελεί το deployment και ένα ακόμη file που antivirus software σε locked-down terminal μπορεί να αποφασίσει ότι δεν του αρέσει. Για component whose selling point είναι ότι μπαίνει σε project και λειτουργεί, αυτό είναι πραγματικό κόστος και όχι θεωρητικό. Το άλλο μισό του επιχειρήματος είναι ότι το VP8L είναι μικρό: prefix-code plus LZ77 format με τέσσερα inverse transforms και 120-entry neighborhood distance map, ενώ ολόκληρος ο decoder στο HPDFWebP.pas είναι κάτω από 900 γραμμές Pascal. Μέσα στο THotPDF.AddImage το WebP branch βρίσκεται στο ίδιο extension dispatch που ήδη δρομολογεί τα .jp2, .j2k, .jpt και .jpc μέσω της JPEG 2000 path, δηλαδή στο σημείο που περιγράφεται και στον οδηγό για adding JPEG 2000 images σε PDF στο Delphi. Callers που θέλουν raw pixels αντί για PDF image μπορούν να πάνε απευθείας στο HPDFDecodeWebPLossless, το οποίο γεμίζει TWebPCardinalArray από τιμές $AARRGGBB σε 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 διαβάζεται ταυτόχρονα προς δύο κατευθύνσεις
Επειδή το bit order του container και το bit order του prefix code ορίζονται ανεξάρτητα, και το VP8L επιλέγει αντίθετες συμβάσεις για αυτά. Το RFC 9649 section 3.2 λέει καθαρά ότι το bitstream διαβάζεται least significant bit first: ο reader ξεκινά από το bit 0 ενός byte και ανεβαίνει. Τα canonical prefix codes που μεταφέρει το stream έρχονται most significant bit first, από τη ρίζα του 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 που αποσυγχρονίζουν σιωπηλά το stream
Τρεις σημασιολογίες στο RFC 9649 αναφέρονται ακριβώς μία φορά, διαβάζονται εύκολα προσπερνώντας τες και καθεμία κοστίζει ή εξοικονομεί ένα μόνο bit, αρκετό για να μετατρέψει κάθε μεταγενέστερο table σε noise. Και οι τρεις βρέθηκαν στον HotPDF VP8L decoder, και όλες δίνουν το ίδιο σύμπτωμα: εικόνα που μοιάζει πιθανή αλλά είναι λάθος παντού
- Ένα entropy-coded image σε non-primary role δεν γράφει καθόλου meta-prefix bit. Το ABNF για
entropy-coded-imageαπλώς δεν περιέχει το item, οπότε η ανάγνωση ενός αποσυγχρονίζει το 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, αποκωδικοποιώντας το μοναδικό symbol χωρίς να αγγίξει τον reader - Τιμή cache_bits 0 σημαίνει color cache size 0 και όχι
1 shl 0. Το βολικό shift δίνει 1, με αποτέλεσμα το green alphabet256 + 24 + CacheSizeνα γίνει 281 αντί για 280 και κάθε prefix code table που διαβάζεται μετά να είναι misaligned
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, με αυτή τη σειρά. Αυτοί οι αριθμοί είναι το νόημα της ενότητας. Κανένα από τα τρία δεν παρουσιάστηκε ως off-by-one· καθένα εμφανίστηκε ως image που αποκωδικοποιήθηκε μέχρι τέλους και έμοιαζε με static, και το μόνο που τα ξεχώριζε ήταν το ακριβές bit position στο οποίο το stream έπαψε να συμφωνεί με reference
Τι κερδίζεις με bit-position diffing
Το bit-position diffing μετατρέπει μια άχρηστη ερώτηση σε ερώτηση μίας γραμμής: όχι γιατί είναι λάθος αυτή η εικόνα αλλά γιατί αποκλίνει το stream στο bit 81. Το setup είναι φθηνό. Το Pillow γράφει κάθε fixture .webp μαζί με dump .rgba από το δικό του decode της ίδιας εικόνας· ένα Pascal probe και ένα μικρό Python reference model καταγράφουν running bit counter δίπλα σε κάθε read· η πρώτη position όπου διαφωνούν τα δύο logs είναι εκεί όπου ζει το 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. Το ότι μια reference implementation είναι λάθος δεν κάνει σωστή την implementation υπό test, και καμία πλευρά δεν παίρνει το benefit of the doubt: κάθε divergence πρέπει να κρίνεται απέναντι στο RFC text. Πάρε αυτό το text από την πηγή επίσης. Τα search summaries συχνά παραμορφώνουν numeric tables, και το 120-entry distance map, τα 14 predictor modes και ο color cache multiplier $1e35a7bd πρέπει να μεταγραφούν ακριβώς
Πού αποκλίνει η Pascal integer division από τη C
Το VP8L color transform είναι 3.5 fixed point με signed deltas, και εκεί σταματούν να συμφωνούν Pascal και C. Η C κάνει arithmetic shifts σε negative integers, κάτι που κάνει floor, ενώ το Pascal div κάνει truncate προς το μηδέν. Για κάθε negative product τα δύο διαφέρουν κατά ένα, οπότε το inverse color transform μετακινείται κατά ένα channel step ανά pixel σε ολόκληρη την εικόνα. Γι’ αυτό το HotPDF κάνει explicit το floor στο FloorDiv32 αντί να βασίζεται στο div
// Η C κάνει arithmetic shift και floor στα negatives· το Pascal div κάνει truncate
// προς το μηδέν, άρα η negative περίπτωση χρειάζεται 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-extension και στα δύο
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 τυχαίνει να παράγουν non-negative products, όπου το div και το floor συμφωνούν. Είναι επίσης ο λόγος που τα HotPDF WebP tests απαιτούν pixel-exact equality απέναντι σε Pillow decodes των ίδιων files και όχι tolerance: gradients, odd size 100x37, image 40x40 με πραγματικό alpha channel και flat image 32x32, κάθε pixel συγκρίνεται bit προς bit. Ένα one-step drift περνά perceptual check και αποτυγχάνει bitwise
Τι αρνείται σκόπιμα η υποστήριξη WebP
Το HotPDF αποκωδικοποιεί το πρώτο 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 λάθος format πρέπει να αποτύχει εκεί όπου ο caller μπορεί να το κάνει pre-convert, αντί να παράγει gray rectangle. Το version field πρέπει να είναι 0, το transform stack περιορίζεται σε τέσσερα entries και κάθε bounds violation σηκώνει EWebPDecode, το οποίο το public entry point μετατρέπει σε απλό False. Το decoding κατά το import είναι επίσης η αντίθετη κατεύθυνση από την ανάκτηση εικόνων από document που άνοιξες, η οποία περνά από τη loaded-image path που περιγράφεται στο extracting images από loaded PDF και τα decode filters τους. Και κάθε image decoder είναι parser που τροφοδοτείται από files που δεν δημιούργησες εσύ: αν τα WebP assets έρχονται από customers ή το public internet, οι bounds checks εδώ είναι το πάτωμα και όχι το ταβάνι, ενώ η ισχυρότερη απάντηση είναι να τρέχουν οι 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 από το ίδιο set units