O HotPDF 2.747.0 descodifica imagens WebP com um decoder VP8L (WebP sem perdas) escrito de raiz em Object Pascal, pelo que THotPDF.AddImageFromFile aceita diretamente um caminho .webp, sem uma DLL libwebp para distribuir e sem um processo auxiliar para iniciar. O decoder implementa integralmente a secção 3 do RFC 9649: o percurso do contentor RIFF, códigos de prefixo canónicos, referências para trás LZ77, a cache de cores e as quatro transformações inversas. Frames VP8 com perdas são recusados claramente, em vez de serem parcialmente descodificados
O gatilho foi banal. Uma ferramenta de design exporta todos os assets como WebP porque esse é o default moderno, os assets chegam a um gerador de faturas ou catálogos que comeu PNG e JPEG tranquilamente durante uma década e, de repente, metade das entradas é rejeitada. A correção óbvia é ligar a libwebp e seguir em frente. A correção óbvia é também a que transforma um componente VCL autocontido em algo com uma história de deployment
Porque é que o VP8L foi implementado em vez de ligar a libwebp?
O HotPDF implementa o codec em Pascal porque um componente Delphi que os clientes compilam no seu próprio executável não pode adquirir silenciosamente uma DLL de runtime. Uma dependência nativa significa acompanhar um binário de 32 bits e outro de 64 bits, fixar uma versão, explicar uma cadeia de assinatura de código a quem executa a instalação e acrescentar mais um ficheiro que o antivírus de um terminal bloqueado pode decidir não gostar. Para um componente cujo principal argumento é entrar num projeto e funcionar, esse é um custo real, não teórico. A outra metade do argumento é que o VP8L é pequeno: um formato de códigos de prefixo mais LZ77 com quatro transformações inversas e um mapa de distâncias de vizinhança com 120 entradas, e o decoder inteiro em HPDFWebP.pas tem menos de 900 linhas de Pascal. Dentro de THotPDF.AddImage, o ramo WebP fica no mesmo dispatch de extensões que já encaminha .jp2, .j2k, .jpt e .jpc pelo percurso JPEG 2000, pelo que a canalização já existia, no mesmo local descrito no guia sobre adicionar imagens JPEG 2000 a PDFs em Delphi. Os chamadores que querem pixels brutos em vez de uma imagem PDF podem ir diretamente a HPDFDecodeWebPLossless, que preenche um TWebPCardinalArray de valores $AARRGGBB pela ordem das scanlines
uses
HPDFDoc, HPDFWebP;
var
Pdf: THotPDF;
Idx: Integer;
begin
Pdf := THotPDF.Create(nil);
try
Pdf.FileName := 'catalog.pdf';
Pdf.BeginDoc;
// .webp é encaminhado para o decoder VP8L integrado, sem DLL
Idx := Pdf.AddImageFromFile('product-shot.webp', icFlate);
Pdf.CurrentPage.ShowImage(Idx, 50, 500, 240, 180, 0);
Pdf.EndDoc;
finally
Pdf.Free;
end;
end;
Porque é que um bitstream VP8L é lido em duas direções ao mesmo tempo?
Porque a ordem dos bits do contentor e a ordem dos bits do código de prefixo são especificadas independentemente, e o VP8L escolhe convenções opostas para elas. A secção 3.2 do RFC 9649 afirma claramente que o bitstream é lido primeiro pelo bit menos significativo: o leitor começa no bit 0 de um byte e avança. Os códigos de prefixo canónicos transportados dentro desse stream chegam primeiro pelo bit mais significativo, com a raiz da árvore primeiro, pelo que o percurso de descodificação desloca o acumulador para a esquerda e faz OR de cada bit novo na parte inferior. O leitor e o percurso do código correm, portanto, em direções opostas dentro do mesmo ciclo, o que parece um bug sempre que se volta a ler o código
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;
// o percurso canónico corre na outra direção: o primeiro bit retirado do stream
// é o bit mais significativo do 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;
Três detalhes do RFC que dessincronizam silenciosamente o stream
Três semânticas no RFC 9649 são declaradas exatamente uma vez, são fáceis de passar por cima e cada uma custa ou poupa um único bit, o suficiente para transformar todas as tabelas seguintes em ruído. As três foram encontradas no decoder VP8L do HotPDF e as três produzem o mesmo sintoma: uma imagem com aspeto plausível, mas errada em todo o lado
- Uma imagem codificada por entropia num papel não primário não escreve qualquer bit de meta-prefixo. A ABNF de
entropy-coded-imagesimplesmente não contém esse item, pelo que ler um dessincroniza o stream por um bit. O HotPDF passaAllowMeta = Falsepara a própria imagem de entropia, para os dados da transformação de predição e de cor e para a paleta de indexação de cores - Um código de prefixo com uma única folha consome zero bits. A secção 3.7.2.1 do RFC 9649 diz isso diretamente e o percurso canónico leria alegremente um bit e depois não conseguiria colocá-lo, pelo que
BuildHuffdeteta uma contagem total de símbolos de 1 e marca a árvore comoSingle, descodificando esse símbolo único sem tocar no leitor - Um valor cache_bits de 0 significa que o tamanho da cache de cores é 0, não
1 shl 0. O shift conveniente dá 1, fazendo com que o alfabeto verde256 + 24 + CacheSizeresulte em 281 em vez de 280, e todas as tabelas de códigos de prefixo lidas depois ficam desalinhadas
CacheBits := 0;
CacheSize := 0; // cache_bits = 0 significa mesmo nenhum
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: apenas a imagem codificada espacialmente (ARGB) transporta o
// bit de meta-prefixo; os papéis codificados por entropia nunca o escrevem
if AllowMeta then
UseMeta := BR.ReadBit
else
UseMeta := 0;
// ...
ReadHuffCode(256 + 24 + CacheSize, Groups[I].Green); // 280, não 281
ReadHuffCode(256, Groups[I].Red);
ReadHuffCode(256, Groups[I].Blue);
ReadHuffCode(256, Groups[I].Alpha);
ReadHuffCode(40, Groups[I].Dist);
No fixture usado durante a integração, os três surgiram no bit 47, no bit 81 e no bit 89, por essa ordem. Esses números são o objetivo desta secção. Nenhum dos três se anunciou como um off-by-one; cada um apresentou uma imagem que terminou de descodificar e parecia estática, e a única coisa que os separou foi a posição exata do bit em que o stream deixou de concordar com uma referência
O que se ganha ao comparar posições de bits?
A comparação de posições de bits transforma uma pergunta inútil numa pergunta de uma linha: não porque está esta imagem errada, mas porque divergiu o stream no bit 81. A preparação é barata. O Pillow escreve cada fixture .webp juntamente com um dump .rgba da sua própria descodificação da mesma imagem; uma probe Pascal e um pequeno modelo de referência Python registam um contador de bits corrente ao lado de cada leitura; a primeira posição em que os dois logs discordam é onde vive o bug. Comece por um fixture que exercite o mínimo possível: uma imagem plana de 32x32 que só percorra o caminho de código simples. Faça essa passar e acrescente depois gradientes, dimensões ímpares e alfa, um fixture de cada vez. Adivinhar a ordem dos bits é, em vez disso, uma forma de gastar um dia
A ressalva honesta é que a referência também estava errada. O modelo Python esqueceu-se de ler cache_bits e o seu ciclo de transformações não chegava ao fim, pelo que alguns pontos de divergência eram o decoder de referência a perder a sincronização, não o Pascal. Uma implementação de referência estar errada não torna correta a implementação em teste e nenhum dos lados recebe o benefício da dúvida: cada divergência tem de ser decidida contra o texto do RFC. Extraia esse texto da fonte também. Os resumos de pesquisa deformam regularmente tabelas numéricas e o mapa de distâncias com 120 entradas, os 14 modos de predição e o multiplicador da cache de cores $1e35a7bd têm todos de ser transcritos exatamente
Onde a divisão inteira do Pascal diverge da do C
A transformação de cores VP8L é de ponto fixo 3.5 com deltas assinados e é aí que Pascal e C deixam de concordar. O C desloca inteiros negativos aritmeticamente, o que faz floor; o div do Pascal trunca para zero. Para qualquer produto negativo, os dois diferem em um, pelo que a transformação inversa de cores deriva um passo de canal por pixel ao longo de toda a imagem. Por isso o HotPDF faz o floor explicitamente em FloorDiv32, em vez de depender de div
// O C faz shifts aritméticos e aplica floor aos negativos; div do Pascal trunca
// para zero, pelo que o caso negativo precisa de uma correção 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 ponto fixo 3.5 entre o byte de um elemento de transformação e um
// byte de canal de cor, ambos estendidos com sinal primeiro
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;
Vale a pena nomear esta classe de defeito porque é invisível em qualquer teste cujos fixtures produzam por acaso produtos não negativos, onde div e floor coincidem. É também por isso que os testes WebP do HotPDF afirmam igualdade exata de pixels contra descodificações Pillow dos mesmos ficheiros, em vez de uma tolerância: gradientes, um tamanho ímpar de 100x37, uma imagem de 40x40 com um canal alfa real e uma imagem plana de 32x32, cada pixel comparado bit a bit. Um desvio de um passo passa numa verificação percetual e falha numa bitwise
O que o suporte WebP recusa deliberadamente
O HotPDF descodifica o primeiro chunk VP8L de um ficheiro WebP e nada mais. Frames VP8 com perdas, animações e qualquer contentor cujo chunk correspondente não seja VP8L devolvem False de HPDFDecodeWebPLossless, e AddImage transforma isso numa exceção que nomeia o ficheiro: Failed to decode WebP image (lossless VP8L only). Esse é um limite deliberado, não uma omissão: um ficheiro no formato errado deve falhar onde o chamador o pode pré-converter, em vez de produzir um retângulo cinzento. O campo de versão tem de ser 0, a stack de transformações está limitada a quatro entradas e cada violação de limites levanta EWebPDecode, que o ponto de entrada público converte num False simples. A descodificação na importação é também a direção oposta à extração de imagens de um documento aberto, que passa pelo percurso de imagens carregadas descrito em extrair imagens de um PDF carregado e dos seus filtros de descodificação. E qualquer decoder de imagens é um parser alimentado por ficheiros que não criou: se os assets WebP chegam de clientes ou da internet pública, as verificações de limites aqui são o piso, não o teto, e a resposta mais forte é executar os codecs de imagem num processo worker isolado para que um frame malformado não arraste o host consigo
O resultado prático é que uma aplicação Delphi ou C++Builder pode agora colocar assets WebP num PDF da mesma forma que coloca PNG: uma chamada a AddImageFromFile, uma chamada a ShowImage e nada de extra no instalador. Se quiser o resto do pipeline de imagens e documentos à sua volta, o componente PDF HotPDF para Delphi cobre os lados da escrita, do carregamento e da renderização a partir do mesmo conjunto de unidades