HotPDF 2.747.0 decodifica imágenes WebP con un decoder VP8L (WebP lossless) escrito desde cero en Object Pascal, de modo que THotPDF.AddImageFromFile acepta directamente una ruta .webp, sin distribuir una DLL de libwebp ni lanzar un proceso auxiliar. El decoder implementa por completo la sección 3 de RFC 9649: el recorrido del contenedor RIFF, los códigos prefijo canónicos, las referencias inversas LZ77, la color cache y las cuatro transformaciones inversas. Los frames VP8 con pérdida se rechazan de forma explícita en lugar de decodificarse a medias
El desencadenante fue de lo más normal. Una herramienta de diseño exporta todos los recursos como WebP porque es el valor predeterminado moderno, los recursos llegan a un generador de facturas o catálogos que llevaba una década aceptando PNG y JPEG y, de repente, se rechaza la mitad de las entradas. La solución obvia es enlazar libwebp y seguir. También es la solución que convierte un componente VCL autocontenido en algo con una historia de despliegue
¿Por qué implementar VP8L en lugar de enlazar libwebp?
HotPDF implementa el codec en Pascal porque un componente Delphi que los clientes compilan dentro de su propio ejecutable no puede adquirir silenciosamente una DLL de runtime. Una dependencia nativa implica seguir un binario de 32 bits y otro de 64, fijar una versión, explicar una cadena de firma de código a quien ejecute el despliegue y añadir otro archivo que el antivirus de un terminal bloqueado puede decidir que no le gusta. Para un componente cuyo principal argumento es que se incorpora a un proyecto y funciona, es un costo real, no teórico. La otra mitad del argumento es que VP8L es pequeño: un formato de códigos prefijo más LZ77, con cuatro transformaciones inversas y un mapa de distancias de vecindad de 120 entradas, y todo el decoder de HPDFWebP.pas ocupa menos de 900 líneas de Pascal. Dentro de THotPDF.AddImage, la rama WebP está en el mismo dispatch de extensiones que ya dirige .jp2, .j2k, .jpt y .jpc hacia la ruta JPEG 2000, justo en el punto que se describe en el recorrido sobre añadir imágenes JPEG 2000 a PDF en Delphi. Los callers que quieran píxeles sin procesar en lugar de una imagen PDF pueden ir directamente a HPDFDecodeWebPLossless, que rellena un TWebPCardinalArray con valores $AARRGGBB en orden de scanline
uses
HPDFDoc, HPDFWebP;
var
Pdf: THotPDF;
Idx: Integer;
begin
Pdf := THotPDF.Create(nil);
try
Pdf.FileName := 'catalog.pdf';
Pdf.BeginDoc;
// .webp se dirige al decoder VP8L integrado, sin ninguna DLL
Idx := Pdf.AddImageFromFile('product-shot.webp', icFlate);
Pdf.CurrentPage.ShowImage(Idx, 50, 500, 240, 180, 0);
Pdf.EndDoc;
finally
Pdf.Free;
end;
end;
¿Por qué un bitstream VP8L se lee en dos direcciones a la vez?
Porque el orden de bits del contenedor y el orden de bits de los códigos prefijo se especifican por separado, y VP8L elige convenciones opuestas para ellos. La sección 3.2 de RFC 9649 lo dice claramente: el bitstream se lee empezando por el bit menos significativo: el lector comienza en el bit 0 de un byte y avanza hacia arriba. Los códigos prefijo canónicos contenidos en ese stream llegan empezando por el bit más significativo, desde la raíz del árbol, así que el recorrido de decodificación desplaza su acumulador a la izquierda y hace OR con cada bit nuevo en la parte inferior. Por tanto, el lector y el recorrido del código avanzan en direcciones opuestas dentro del mismo bucle, algo que parece un bug cada vez que se vuelve a leer
function TWebPBitReader.ReadBit: Integer;
begin
if BytePos >= Length(Data) then
raise EWebPDecode.Create('WebP bitstream exhausted');
Result := (Data[BytePos] shr BitPos) and 1; // LSB primero, RFC 9649 3.2
Inc(BitPos);
if BitPos = 8 then
begin
BitPos := 0;
Inc(BytePos);
end;
end;
// El recorrido canónico va en la otra dirección: el primer bit que sale del stream
// es el bit más significativo del código
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;
Tres detalles de RFC que desincronizan el stream en silencio
Tres semánticas de RFC 9649 aparecen escritas una sola vez, son fáciles de pasar por alto y cada una cuesta o ahorra un único bit, suficiente para convertir en ruido todas las tablas posteriores. Las tres aparecieron en el decoder VP8L de HotPDF y las tres producen el mismo síntoma: una imagen de aspecto plausible pero incorrecta en todas partes
- Una imagen codificada por entropía en un rol no primario no escribe ningún bit de meta-prefijo. La ABNF de
entropy-coded-imagesencillamente no contiene ese elemento, por lo que leer uno desincroniza el stream en un bit. HotPDF pasaAllowMeta = Falsepara la propia imagen de entropía, para los datos de transformación de predictor y color y para la paleta de indexación de color - Un código prefijo de una sola hoja consume cero bits. La sección 3.7.2.1 de RFC 9649 lo dice directamente, y el recorrido canónico leería alegremente un bit para después no saber dónde colocarlo, así que
BuildHuffdetecta un recuento total de símbolos igual a 1, marca el árbol comoSingley decodifica ese único símbolo sin tocar el lector - Un valor cache_bits de 0 significa que el tamaño de la color cache es 0, no
1 shl 0. El desplazamiento cómodo da 1, por lo que el alfabeto verde256 + 24 + CacheSizeresulta ser 281 en lugar de 280 y todas las lecturas posteriores de tablas de códigos prefijo quedan desalineadas
CacheBits := 0;
CacheSize := 0; // cache_bits = 0 significa realmente ninguno
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: solo la imagen codificada espacialmente (ARGB) lleva
// el bit de prefijo meta; los roles codificados por entropía nunca lo escriben
if AllowMeta then
UseMeta := BR.ReadBit
else
UseMeta := 0;
// ...
ReadHuffCode(256 + 24 + CacheSize, Groups[I].Green); // 280, no 281
ReadHuffCode(256, Groups[I].Red);
ReadHuffCode(256, Groups[I].Blue);
ReadHuffCode(256, Groups[I].Alpha);
ReadHuffCode(40, Groups[I].Dist);
En el fixture utilizado durante la puesta en marcha, los tres aparecieron en el bit 47, el bit 81 y el bit 89, en ese orden. Esas cifras son el motivo de esta sección. Ninguno de los tres se anunció como un off-by-one; cada uno se presentó como una imagen que terminaba de decodificarse y parecía estática, y lo único que los separó fue la posición exacta del bit en la que el stream dejó de coincidir con una referencia
¿Qué aporta comparar posiciones de bits?
La comparación de posiciones de bits convierte una pregunta inútil en otra de una sola línea: no «por qué está mal esta imagen», sino «por qué se desvió el stream en el bit 81». La preparación es barata. Pillow escribe cada fixture .webp más un volcado .rgba de su propia decodificación de la misma imagen; un probe Pascal y un pequeño modelo de referencia Python registran un contador de bits acumulado junto a cada lectura; la primera posición en la que discrepan ambos logs es donde vive el bug. Empiece con un fixture que ejercite lo mínimo posible: una imagen plana de 32x32 que solo utilice la ruta de código simple. Cuando esté verde, añada degradados, dimensiones impares y alfa, un fixture cada vez. Adivinar el orden de bits en su lugar es una forma de perder un día
La salvedad honesta es que la referencia también era incorrecta. El modelo Python olvidaba leer cache_bits y su bucle de transformación no llegaba al final, así que algunos puntos de divergencia eran el decoder de referencia perdiendo la sincronización, no el Pascal. Que una implementación de referencia esté mal no convierte en correcta la implementación que se está probando, y ninguna de las dos recibe el beneficio de la duda: cada divergencia tiene que decidirse contra el texto de la RFC. Extraiga también ese texto de la fuente. Los resúmenes de búsqueda suelen deformar las tablas numéricas, y el mapa de distancias de 120 entradas, los 14 modos de predictor y el multiplicador de color cache $1e35a7bd tienen que transcribirse exactamente
Dónde diverge la división entera de Pascal respecto a C
La transformación de color VP8L es de punto fijo 3.5 con deltas con signo, y ahí es donde Pascal y C dejan de coincidir. C desplaza los enteros negativos aritméticamente, lo que hace floor; div de Pascal trunca hacia cero. Para cualquier producto negativo, los dos resultados difieren en uno, por lo que la transformación inversa de color se desvía un paso de canal por píxel en toda la imagen. Por eso HotPDF hace explícito el floor en FloorDiv32 en lugar de confiar en div
// C desplaza aritméticamente y hace floor con negativos; div de Pascal trunca
// hacia cero, así que el caso negativo necesita una corrección explícita
function FloorDiv32(V: Integer): Integer;
begin
Result := V div 32;
if (V < 0) and (V mod 32 <> 0) then
Dec(Result);
end;
// Delta de punto fijo 3.5 entre el byte de un elemento de transformación y un
// byte de canal de color, ambos extendidos con signo primero
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;
Merece la pena nombrar esta clase de defecto porque es invisible en cualquier prueba cuyos fixtures produzcan por casualidad productos no negativos, donde div y floor coinciden. También es el motivo por el que las pruebas WebP de HotPDF afirman la igualdad exacta de píxeles frente a las decodificaciones de Pillow de los mismos archivos, en lugar de una tolerancia: degradados, un tamaño impar de 100x37, una imagen de 40x40 con un canal alfa real y una imagen plana de 32x32, cada píxel se compara bit a bit. Un desvío de un paso supera una comprobación perceptual y falla una bitwise
Qué rechaza deliberadamente la compatibilidad WebP
HotPDF decodifica el primer chunk VP8L de un archivo WebP y nada más. Los frames VP8 con pérdida, las animaciones y cualquier contenedor cuyo chunk coincidente no sea VP8L devuelven False desde HPDFDecodeWebPLossless, y AddImage lo convierte en una excepción que nombra el archivo: Failed to decode WebP image (lossless VP8L only). Es un límite deliberado, no un descuido: un archivo con formato equivocado debe fallar donde el caller pueda preconvertirlo, en lugar de producir un rectángulo gris. El campo de versión debe ser 0, la pila de transformaciones está limitada a cuatro entradas y cada violación de límites genera EWebPDecode, que el punto de entrada público convierte en un False simple. Decodificar durante la importación es también la dirección opuesta a extraer imágenes de un documento que se ha abierto, algo que pasa por la ruta de imágenes cargadas descrita en la extracción de imágenes de un PDF cargado y sus filtros de decodificación. Además, cualquier decoder de imágenes es un parser alimentado por archivos que usted no ha creado: si los recursos WebP llegan de clientes o de internet, estas comprobaciones de límites son el suelo, no el techo, y la respuesta más sólida es ejecutar los codecs de imagen en un proceso worker aislado para que un frame malformado no pueda derribar el host
El resultado práctico es que una aplicación Delphi o C++Builder puede introducir recursos WebP en un PDF igual que introduce PNG: una llamada a AddImageFromFile, una llamada a ShowImage y nada más en el instalador. Si quiere la parte restante de la pipeline de imágenes y documentos que lo rodea, el componente PDF Delphi HotPDF cubre los lados de escritura, carga y renderizado desde el mismo conjunto de unidades