Um stream marcado /Predictor 12 não significa que toda linha usa o filtro PNG 2. O HotPDF, o componente VCL nativo de PDF para Delphi e C++Builder, trata os valores de predictor de 10 a 15 como uma única família: a tag de filtro real, de 0 a 4, é o primeiro byte de cada linha codificada, e HPDFDecodePredictor lê e valida essa tag linha por linha. Essa distinção é a forma de quase todo bug nesse canto do PDF, porque nada dispara alerta quando você erra. A cadeia de filtros roda, o raster tem o tamanho esperado, e a imagem sai como estática diagonal ou um gradiente que se desvia cada vez mais a cada scanline. Os cinco números em /DecodeParms (ISO 32000-1 §7.4.4) na maioria das vezes mudam o significado dos bytes em vez do seu comprimento, então um valor errado produz lixo plausível em vez de um erro
Por que /Predictor 12 não significa filtro PNG 2 em toda linha?
Porque o número do predictor só diz "predição PNG está em uso", não qual filtro. Encoders PNG escolhem um filtro por scanline e o filtro do PDF herda isso, então os valores de predictor 10 (None), 11 (Sub), 12 (Up), 13 (Average), 14 (Paeth) e 15 (Optimum) todos decodificam de forma idêntica: o byte de tag inicial de cada linha é o que o decoder precisa obedecer. A consequência de layout importa tanto quanto a semântica. Toda linha codificada tem 1 + RowBytes bytes de comprimento, a entrada portanto excede a saída exatamente pela contagem de linhas, e um stream cujo comprimento não é um múltiplo inteiro de RowBytes + 1 está truncado por definição. O HotPDF checa esse limite antes de tocar em um byte, rejeita qualquer tag acima de 4 com Invalid PNG predictor row tag, e lê a linha anterior diretamente do buffer de saída único em vez de materializar um array de linhas bidimensional. Os filtros 1 e 3 olham para trás BytesPerPixel dentro da linha atual, o filtro 2 lê diretamente acima, o filtro 4 roda a escolha Paeth sobre esquerda, acima e acima-esquerda — e todos os quatro operam sobre saída já reconstruída, e é por isso que a linha de cima tem que ser a linha decodificada e nunca a entrada filtrada
uses
HPDFPredictor;
var
Filtered, Raster: AnsiString;
ErrorText: string;
begin
// /DecodeParms << /Predictor 12 /Colors 3 /BitsPerComponent 8 /Columns 1024 >>
if HPDFTryDecodePredictor(Filtered, 12, 3, 8, 1024,
Int64(1024) * 3 * 8192, Raster, ErrorText) then
ConsumeRaster(Raster)
else
LogStreamDefect('predictor', ErrorText); // no exception, no partial raster
end;
O argumento MaxOutputBytes não é decoração. Um estágio de predictor é um estágio de descompressão disfarçado, e um valor de /Columns hostil ou simplesmente quebrado transforma alguns kilobytes de entrada em uma solicitação de alocação de múltiplos gigabytes. O HotPDF calcula bits por linha, bytes por linha e o tamanho total do raster primeiro em Int64, recusa geometria que estoura, e respeita o teto fornecido por quem chama. Passe um limite real derivado do image dictionary e o modo de falha vira uma mensagem logada em vez de um diálogo de falta de memória na máquina do cliente
Por que o TIFF Predictor 2 corrompe imagens de 4 bits?
Porque o Predictor 2 é differencing horizontal por amostra, não por byte, e a 1, 2 ou 4 bits por componente várias amostras compartilham um byte. A implementação comum soma o byte N-Colors ao byte N, o que por acaso está correto a 8 bits por componente e está silenciosamente errado em qualquer outro caso. Uma varredura RGB de 8 bits decodifica perfeitamente, e então o mesmo código destrói uma imagem indexada de 4 bits na primeira vez que uma aparece em produção
A aritmética correta opera dentro do campo de bits. O HotPDF percorre amostras do índice Colors até Colors * Columns - 1, extrai a amostra e seu vizinho à esquerda do mesmo componente com uma máscara de (1 shl BitsPerComponent) - 1 no deslocamento apropriado, soma-os módulo essa máscara, e grava o resultado de volta sem perturbar as outras amostras empacotadas no mesmo byte. A cauda também importa: uma linha é preenchida até um limite de byte, então os bits de preenchimento depois da última amostra precisam sobreviver intocados em vez de serem incorporados na aritmética. A 16 bits por componente cada amostra é um par de bytes big-endian e a soma dá a volta em $FFFF através do par em vez de carregar entre bytes independentemente; a 8 bits a recorrência de byte simples está certa, avançando por Colors para que vermelho acumule contra vermelho e alfa contra alfa. Em toda variante o primeiro pixel de uma linha é um literal, nunca uma diferença, e a recorrência reinicia a cada limite de linha — a predição TIFF nunca lê a linha acima, que é toda a diferença entre ela e a família PNG
O que o EarlyChange realmente controla no LZWDecode?
Ele controla quando o leitor amplia seu tamanho de código em um bit, e estar um código fora de sincronia corrompe tudo que vem depois. O HotPDF expressa a regra como um único invariante: depois de adicionar uma entrada ao dicionário, a próxima leitura amplia quando NextCode alcança (1 shl CodeSize) - Ord(EarlyChange). Com /EarlyChange 1, o padrão da ISO 32000-1 §7.4.4, a troca acontece um código antes; com /EarlyChange 0 acontece exatamente no limite. Ambos aparecem em arquivos reais e nada no bitstream diz qual o encoder usou. O resto da máquina de estados precisa se mover em sincronia: um código de clear reseta o tamanho de código, a máscara de bits, o próximo código livre e o armazenamento de frases juntos, e o código de end-of-information é lido na largura que estiver atual naquele momento, não nos 9 bits iniciais. O HotPDF começa em InitialCodeSize 9, limita o tamanho de código a 12 e o dicionário a 4096 entradas, e assume FillOrder como foTop por padrão porque o PDF empacota códigos com o bit de ordem alta primeiro — foBottom existe para streams estilo TIFF que não fazem isso
uses
HPDFLZW;
var
Decoder: TPDFLZWDecompressor;
Parms: TPDFLZWParms;
Plain: AnsiString;
begin
Decoder := TPDFLZWDecompressor.Create;
try
Decoder.EarlyChange := True; // /EarlyChange 1 is the PDF default
Decoder.FillOrder := foTop; // high-order bit first
Decoder.MaxOutputBytes := 256 * 1024 * 1024;
Decoder.RequireInitialClear := False;
Decoder.RequireEndOfInformation := False;
Parms.Predictor := 12;
Parms.Colors := 3;
Parms.BitsPerComponent := 8;
Parms.Columns := 1024;
Parms.ExpandedTo8Bit := False;
Parms.ColorSpace := 'DeviceRGB';
if Decoder.TryDecompress(RawStreamBytes, Parms, Plain) then
LogDecodeStats(Decoder.PeakCodeSize, Decoder.DictionaryAdds,
Decoder.KwKwKExpansions, Decoder.OutputBytes)
else
LogStreamDefect('lzw', Decoder.LastError);
finally
Decoder.Free;
end;
end;
As estatísticas existem para triagem, não vaidade. Quando um arquivo decodifica no comprimento certo mas nos pixels errados, PeakCodeSize e DictionaryAdds te dizem imediatamente se o leitor alguma vez ampliou onde o writer ampliou. Inverta o EarlyChange, decodifique de novo, compare os dois: se os números mudam, você tem sua resposta em uma execução em vez de percorrer um bit reader passo a passo
O ramo KwKwK, e quando um stream deve simplesmente falhar
O único caso legal que parece ilegal é Code = NextCode, e o HotPDF trata isso construindo a entrada antes de emiti-la. Um encoder pode emitir o código para uma frase que está definindo no mesmo passo, o que acontece sempre que a entrada contém um padrão da forma K w K w K; o decoder não consegue procurar esse código porque ele ainda não existe, então precisa construir Previous + First(Previous), adicioná-lo como a nova entrada, e emitir a entrada que acabou de criar. O HotPDF conta essas em KwKwKExpansions e verifica cruzadamente que o código que adicionou é o código que foi solicitado. Tudo acima de NextCode é corrupção, e ali um decoder deve parar em vez de improvisar: o HotPDF dispara erro em um código futuro, em um prefixo de dicionário apontando para fora da arena de frases, em um dicionário cheio, e em um primeiro código que não é um literal. Duas chaves de rigor estão deliberadamente desligadas por padrão, RequireInitialClear e RequireEndOfInformation, porque muitos PDFs de produção omitem o código de clear inicial ou ficam sem dados sem um terminador. Ligue-as ao validar sua própria saída, deixe-as desligadas ao consumir arquivos vindos do mundo real
Onde /DecodeParms é de fato lido do lado do documento carregado
O HotPDF resolve /DecodeParms ou sua abreviação /DP no stream dictionary da imagem, aceita tanto um dictionary quanto um array e pega o último elemento quando é um array, depois carrega Predictor, Colors, BitsPerComponent, Columns e EarlyChange para o caminho de raster. O caso do array é o que as pessoas esquecem: um stream filtrado por [/ASCII85Decode /FlateDecode] carrega um array de parâmetros paralelo, e as configurações de predictor pertencem ao último filtro, não ao primeiro
var
Pdf: THotPDF;
Info: THPDFLoadedImageInfo;
Bmp: TBitmap;
I: Integer;
begin
Pdf := THotPDF.Create(nil);
try
if Pdf.LoadFromFile('scanned.pdf', '') > 0 then
for I := 0 to Pdf.GetLoadedImageCount - 1 do
if Pdf.GetLoadedImageInfo(I, Info) then
begin
Bmp := Pdf.ExtractLoadedImage(I); // nil when the raster is unusable
if Bmp <> nil then
try
Bmp.SaveToFile(Format('image-%d.bmp', [I]));
finally
Bmp.Free;
end;
end;
finally
Pdf.Free;
end;
end;
Vale nomear um defeito histórico nesse caminho, porque a classe de bug se repete. A antiga rotina de Flate com parâmetros criava um stream de descompressão e depois copiava da entrada comprimida original, então o estágio de predictor recebia bytes comprimidos e diligentemente os "des-predizia": sempre errado, nunca disparava erro. O código atual lê apenas do decoder antes de entregar o resultado ao predictor compartilhado, e rejeita um raster mais curto que o tamanho calculado em vez de recair nos bytes ainda comprimidos — um fallback que costumava transformar uma falha de decodificação em um bitmap corrompido. Essa mesma implementação de predictor agora serve cross-reference streams também, o que é uma consistência útil se você também trabalha com object streams e incremental updates, e a maquinaria de extração ao redor é coberta na peça complementar sobre extração de imagens carregadas e seus filtros de decodificação. Imagens que chegam como DCTDecode ou JPXDecode nunca chegam ao predictor de forma alguma; elas carregam seu próprio modelo de pixel comprimido
Throughput: uma arena de frases contígua versus strings por entrada
Substituir o dicionário de strings por entrada por uma arena de frases contígua mediu cerca de 1,61 vezes mais rápido em uma entrada patológica: 1558 MiB/s contra 969 MiB/s em um benchmark cuja frase mais longa única chega a 7.370.880 bytes. A forma dessa entrada explica a diferença, porque as implementações clássicas escolhem uma de duas trocas ruins. Um dicionário de valores AnsiString aloca e copia uma string nova para cada uma das até 4096 entradas, cada nova entrada copiando seu pai inteiro; uma pilha de prefixo/sufixo evita essa memória inteiramente mas reconstrói cada frase percorrendo a cadeia de trás para frente um byte de cada vez e a invertendo, o que é ótimo para texto comum e doloroso quando uma frase chega a megabytes. O HotPDF anexa cada frase contiguamente a uma arena que cresce geometricamente, indexa entradas por offset e comprimento, e emite uma frase com um único Move para o buffer de saída. O custo honesto é memória: uma arena guardando toda frase por completo é limitada pela soma de todos os comprimentos de frase em vez de pela contagem de entradas, e é exatamente por isso que MaxOutputBytes existe tanto no decompressor quanto no predictor. Derive esse limite do que o image dictionary alega que o raster deveria ser, e um stream mentiroso falha rápido
O decompressor LZW, o predictor compartilhado e o caminho de extração de imagem carregada mostrados aqui vêm como parte do HotPDF Component padrão para Delphi e C++Builder, com a referência completa de filtro e DecodeParms na página de produto