HotPDF 2.747.0 décode les images WebP avec un décodeur VP8L (WebP sans perte) écrit de zéro en Object Pascal, de sorte que THotPDF.AddImageFromFile accepte directement un chemin .webp, sans DLL libwebp à distribuer ni processus auxiliaire à lancer. Le décodeur implémente intégralement la section 3 de la RFC 9649 : parcours du conteneur RIFF, codes préfixes canoniques, références arrière LZ77, cache de couleurs et quatre transformations inverses. Les trames VP8 avec perte sont refusées explicitement au lieu d’être décodées à moitié
Le déclencheur était banal. Un outil de conception exporte chaque ressource en WebP parce que c’est le format moderne par défaut, les ressources arrivent dans un générateur de factures ou de catalogues qui avale PNG et JPEG depuis dix ans, et soudain la moitié des entrées sont rejetées. Le correctif évident consiste à lier libwebp et à passer à autre chose. C’est aussi le correctif qui transforme un composant VCL autonome en composant doté d’une vraie histoire de déploiement
Pourquoi implémenter VP8L au lieu de lier libwebp ?
HotPDF implémente le codec en Pascal parce qu’un composant Delphi que les clients compilent dans leur propre exécutable ne peut pas acquérir discrètement une DLL d’exécution. Une dépendance native signifie un binaire 32 bits et un binaire 64 bits à suivre, une version à figer, une chaîne de signature de code à expliquer à la personne qui exécute le déploiement et un fichier supplémentaire que l’antivirus d’un terminal verrouillé pourrait décider de ne pas aimer. Pour un composant dont l’argument principal est de s’intégrer à un projet et de fonctionner, c’est un coût réel, pas théorique. L’autre moitié de l’argument est que VP8L est petit : un format à codes préfixes et LZ77 avec quatre transformations inverses et une table de distances de voisinage à 120 entrées ; le décodeur complet de HPDFWebP.pas fait moins de 900 lignes de Pascal. Dans THotPDF.AddImage, la branche WebP se trouve dans la même sélection par extension qui achemine déjà .jp2, .j2k, .jpt et .jpc vers le chemin JPEG 2000, au même endroit que dans le parcours de l’ajout d’images JPEG 2000 aux PDF dans Delphi. Les appelants qui veulent les pixels bruts plutôt qu’une image PDF peuvent aller directement à HPDFDecodeWebPLossless, qui remplit un TWebPCardinalArray de valeurs $AARRGGBB dans l’ordre des lignes de balayage
uses
HPDFDoc, HPDFWebP;
var
Pdf: THotPDF;
Idx: Integer;
begin
Pdf := THotPDF.Create(nil);
try
Pdf.FileName := 'catalog.pdf';
Pdf.BeginDoc;
// .webp est dirigé vers le décodeur VP8L intégré, sans DLL
Idx := Pdf.AddImageFromFile('product-shot.webp', icFlate);
Pdf.CurrentPage.ShowImage(Idx, 50, 500, 240, 180, 0);
Pdf.EndDoc;
finally
Pdf.Free;
end;
end;
Pourquoi un flux de bits VP8L est-il lu dans deux directions à la fois ?
Parce que l’ordre des bits du conteneur et celui des bits des codes préfixes sont spécifiés indépendamment, et que VP8L choisit des conventions opposées pour les deux. La section 3.2 de la RFC 9649 indique clairement que le flux de bits est lu du bit de poids faible au bit de poids fort : le lecteur commence au bit 0 d’un octet et remonte. Les codes préfixes canoniques contenus dans ce flux arrivent du bit de poids fort au bit de poids faible, à partir de la racine de l’arbre ; le parcours de décodage décale donc son accumulateur vers la gauche et place chaque nouveau bit en bas. La lecture et le parcours des codes avancent ainsi en sens opposés dans la même boucle, ce qui ressemble à un bug chaque fois qu’on relit le code
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;
// le parcours canonique avance dans l’autre sens : le premier bit lu du flux
// est le bit de poids fort du 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;
Trois détails de la RFC qui désynchronisent silencieusement le flux
Trois sémantiques de la RFC 9649 ne sont énoncées qu’une seule fois, se lisent facilement trop vite et coûtent ou économisent chacune un seul bit, ce qui suffit à transformer toutes les tables suivantes en bruit. Toutes les trois ont été trouvées dans le décodeur VP8L de HotPDF et produisent le même symptôme : une image plausible mais fausse partout
- Une image codée par entropie dans un rôle non primaire n’écrit aucun bit de méta-préfixe. L’ABNF de
entropy-coded-imagene contient tout simplement pas cet élément ; en lire un désynchronise donc le flux d’un bit. HotPDF passeAllowMeta = Falsepour l’image elle-même, pour les données de transformation prédictive et de couleur, ainsi que pour la palette d’indexation des couleurs - Un code préfixe à une seule feuille consomme zéro bit. La RFC 9649 section 3.7.2.1 le dit directement, et le parcours canonique lirait volontiers un bit avant de ne pas savoir où le placer ;
BuildHuffdétecte donc un nombre total de symboles égal à 1, marque l’arbreSingleet décode ce symbole sans toucher au lecteur - Une valeur cache_bits égale à 0 signifie que la taille du cache de couleurs vaut 0, pas
1 shl 0. Le décalage commode donne 1, ce qui fait que l’alphabet vert256 + 24 + CacheSizevaut 281 au lieu de 280, et toute lecture de table de codes préfixes qui suit est désalignée
CacheBits := 0;
CacheSize := 0; // cache_bits = 0 signifie réellement aucun 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 : seule l’image codée spatialement (ARGB) porte le bit
// de méta-préfixe ; les rôles codés par entropie ne l’écrivent jamais
if AllowMeta then
UseMeta := BR.ReadBit
else
UseMeta := 0;
// ...
ReadHuffCode(256 + 24 + CacheSize, Groups[I].Green); // 280, pas 281
ReadHuffCode(256, Groups[I].Red);
ReadHuffCode(256, Groups[I].Blue);
ReadHuffCode(256, Groups[I].Alpha);
ReadHuffCode(40, Groups[I].Dist);
Sur le fixture utilisé lors de la mise au point, les trois problèmes sont apparus aux bits 47, 81 et 89, dans cet ordre. C’est le propos de cette section. Aucun des trois ne s’est annoncé comme un simple décalage d’une unité ; chacun s’est présenté comme une image qui finissait de se décoder et ressemblait à de la neige, et seule la position exacte du bit auquel le flux cessait de correspondre à une référence permettait de les distinguer
Que gagne-t-on avec la comparaison des positions de bits ?
La comparaison des positions de bits transforme une question inutile en question à une ligne : non pas pourquoi cette image est-elle fausse, mais pourquoi le flux diverge-t-il au bit 81. La mise en place est peu coûteuse. Pillow écrit chaque fixture .webp ainsi qu’un dump .rgba de son propre décodage de la même image ; une sonde Pascal et un petit modèle de référence Python consignent tous deux un compteur de bits courant à côté de chaque lecture ; la première position où les deux journaux divergent est l’endroit où vit le bug. Commencez par un fixture qui exerce le moins de fonctionnalités possible : une image plate de 32x32 qui n’emprunte que le chemin des codes simples. Faites fonctionner celle-ci, puis ajoutez les dégradés, les dimensions impaires et l’alpha un fixture à la fois. Deviner l’ordre des bits est plutôt une façon de perdre une journée
La réserve honnête est que la référence était également fausse. Le modèle Python oubliait de lire cache_bits et sa boucle de transformation n’allait pas jusqu’à son terme ; certains points de divergence correspondaient donc à la perte de synchronisation du décodeur de référence, pas à celle du Pascal. Une implémentation de référence erronée ne rend pas correcte l’implémentation testée, et aucune des deux parties ne bénéficie du doute : chaque divergence doit être arbitrée contre le texte de la RFC. Récupérez aussi ce texte à la source. Les résumés trouvés par recherche déforment régulièrement les tables numériques, et la table de distances à 120 entrées, les 14 modes prédictifs et le multiplicateur du cache de couleurs $1e35a7bd doivent tous être transcrits exactement
Où la division entière de Pascal diverge-t-elle de celle du C ?
La transformation de couleur VP8L est en virgule fixe 3.5 avec des deltas signés, et c’est là que Pascal et C cessent de se mettre d’accord. Le C décale arithmétiquement les entiers négatifs, ce qui réalise un floor ; le div de Pascal tronque vers zéro. Pour tout produit négatif, les deux résultats diffèrent d’une unité, si bien que la transformation inverse de couleur dérive d’un pas de canal par pixel sur toute l’image. HotPDF réalise donc explicitement le floor dans FloorDiv32 au lieu de compter sur div
// Le C décale arithmétiquement et effectue un floor pour les négatifs ; div en Pascal tronque
// vers zéro, le cas négatif a donc besoin d’une correction explicite
function FloorDiv32(V: Integer): Integer;
begin
Result := V div 32;
if (V < 0) and (V mod 32 <> 0) then
Dec(Result);
end;
// Delta en virgule fixe 3.5 entre l’octet d’un élément de transformation et celui
// d’un canal de couleur, les deux étant d’abord étendus avec leur signe
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;
Cette catégorie de défaut mérite d’être nommée parce qu’elle reste invisible pour tout test dont les fixtures produisent par hasard des produits non négatifs, cas où div et floor concordent. C’est aussi la raison pour laquelle les tests WebP de HotPDF exigent l’égalité exacte des pixels face aux décodages Pillow des mêmes fichiers, plutôt qu’une tolérance : dégradés, taille impaire de 100x37, image 40x40 avec véritable canal alpha et image plate de 32x32, chaque pixel est comparé bit par bit. Une dérive d’un pas passe un contrôle perceptuel et échoue à un contrôle binaire
Ce que la prise en charge de WebP refuse délibérément
HotPDF décode le premier chunk VP8L d’un fichier WebP et rien d’autre. Les trames VP8 avec perte, les animations et tout conteneur dont le chunk correspondant n’est pas VP8L renvoient False depuis HPDFDecodeWebPLossless, et AddImage transforme cela en exception qui nomme le fichier : Failed to decode WebP image (lossless VP8L only). C’est une limite délibérée, pas un oubli : un fichier du mauvais format doit échouer à l’endroit où l’appelant peut le convertir à l’avance, plutôt que de produire un rectangle gris. Le champ de version doit être 0, la pile de transformations est limitée à quatre entrées et chaque dépassement de limite lève EWebPDecode, que le point d’entrée public convertit en un simple False. Le décodage à l’import est aussi l’opposé de l’extraction d’images depuis un document ouvert, qui passe par le chemin d’images chargées décrit dans l’extraction d’images d’un PDF chargé et de leurs filtres de décodage. Et tout décodeur d’image est un parseur alimenté par des fichiers que vous n’avez pas créés : si les ressources WebP viennent de clients ou de l’internet public, les contrôles de limites présents ici sont le plancher, pas le plafond, et la réponse plus robuste consiste à exécuter les codecs d’image dans un processus worker isolé pour qu’une trame malformée ne puisse pas entraîner l’hôte dans sa chute
Le résultat pratique est qu’une application Delphi ou C++Builder peut maintenant placer des ressources WebP dans un PDF comme elle y place du PNG : un appel à AddImageFromFile, un appel à ShowImage, rien de plus dans l’installateur. Si vous voulez le reste du pipeline d’image et de document qui l’entoure, le composant PDF HotPDF pour Delphi couvre l’écriture, le chargement et le rendu depuis le même ensemble d’unités