O HotPDF expõe três kernels de downsampling de imagens pela propriedade ImageDownsampleKernel e uma passada separada de Floyd-Steinberg via RenderOutputDither. O primeiro controla como as fotografias ficam depois que você as encolhe para caber num orçamento de tamanho, o segundo controla como elas ficam depois que uma página é reduzida a preto e branco. Nenhum vem ligado por padrão, e os dois são opt-in pelo mesmo motivo: custam tempo de verdade
A pressão que leva as pessoas até aqui é conhecida. Um contrato digitalizado de 60 MB precisa sair por um gateway de e-mail que rejeita qualquer coisa acima de 10 MB, ou um lote de extratos precisa cair num dispositivo monocromático estilo fax que renderiza cada pixel cinza como papel ou toner. Os dois problemas são problemas de resampling, e os dois têm uma resposta rápida que fica feia e uma resposta lenta que fica certa
Em que os três kernels realmente diferem
THPDFResampleKernel tem três valores, e eles ficam em pontos genuinamente diferentes da curva de velocidade e qualidade. O rkHalftone delega ao caminho histórico do GDI StretchBlt com o modo HALFTONE, que apesar do nome é filtragem de classe bilinear: rápido, adequado para line art e screenshots, e propenso às bordas serrilhadas que você reconhece na hora em fotografias reduzidas. O rkBicubic roda um kernel Catmull-Rom separável, e o rkLanczos3 roda um windowed sinc separável com suporte de três lóbulos
Os dois kernels separáveis rodam em duas passadas, horizontal e depois vertical, com 6 a 12 taps por pixel de destino em Pascal puro. Isso é aproximadamente uma ordem de grandeza mais lento que o caminho do GDI, que é exatamente por que o rkHalftone permanece o padrão. Num lote noturno de milhares de páginas a diferença é uma decisão de agendamento, não uma preferência. Num documento único que um usuário está esperando, Lanczos3 é quase de graça e visivelmente melhor
Duas propriedades de implementação valem conhecer porque determinam o que a saída pode e não pode fazer. As bordas fazem clamp por replicação de borda em vez de wrapping ou esmaecimento, e os pesos são normalizados por pixel de destino. Juntas, essas duas significam que o resultado nunca sofre ringing abaixo do preto ou acima do branco, então o clássico halo de overshoot do Lanczos em volta de uma borda dura não aparece como artefatos de clipping na imagem codificada
var
Pdf: THotPDF;
Info: THPDFLoadedResourceOptimizationInfo;
Changed: Integer;
begin
Pdf := THotPDF.Create(nil);
try
Pdf.LoadFromFile('scanned-contract.pdf');
Pdf.ImageDownsampleKernel := rkLanczos3; // defina antes da chamada
Changed := Pdf.DownsampleLoadedImages(150, 82, 4096, Info);
if Changed > 0 then
begin
Writeln('resampled images: ', Info.DownsampledImageCount);
Writeln('kept calibrated : ', Info.PreservedCalibratedImageCount);
Pdf.SaveToFile('scanned-contract-150dpi.pdf');
end;
finally
Pdf.Free;
end;
end;
O argumento MinimumSavingsBytes, 4096 acima, é o guard que mantém a operação honesta. Re-encodar uma imagem que já estava eficientemente comprimida pode produzir um stream maior que o original, e um downsampler que substitui toda imagem às vezes cegamente cresce o arquivo que pediram para encolher. O limiar diz: só confirme a substituição quando ela economizar pelo menos essa quantidade de bytes. O PreservedCalibratedImageCount reporta a outra decisão conservadora, imagens deixadas intactas porque carregam um espaço de cor calibrado que o resampling comprometeria
Por que um coeficiente polinomial errado é tão difícil de notar?
Porque um kernel de interpolação quebrado não crasha nem lança exceção, ele só produz uma imagem que fica sutilmente errada de um jeito que ninguém consegue atribuir. O kernel Catmull-Rom é cúbico por partes, e o branch externo dele em forma de Horner aninhada é ((-0.5t + 2.5)t - 4)t + 2. Escreva esse coeficiente do meio como -5 em vez de -4 e a função continua avaliando, continua devolvendo números numa faixa plausível, e continua produzindo uma imagem
O dano aparece como W(1) avaliando para -1 onde deveria ser 0. Pesos negativos se acumulam, a soma faz clip em zero, e o sintoma visível é um gradiente cujo lado esquerdo vai a preto e uma step edge que perde os tons intermediários. Nada na falha aponta para um polinômio. A checagem que pega isso em segundos é aritmética, não visual: um kernel interpolador precisa satisfazer W(0) = 1 e W(±1) = W(±2) = 0, e qualquer kernel que erre esses três pontos tem um erro de coeficiente, ponto final. Faça asserts sobre esses três valores num teste unitário e toda essa classe de defeitos de digitação desaparece
Dithering Floyd-Steinberg, e onde ele cabe na pipeline
A passada de dither é um problema diferente do resampling e vive num ponto diferente da pipeline. O RenderOutputDither aplica difusão de erro Floyd-Steinberg depois da composição da página, que é o único posicionamento que faz sentido para um preview de impressão monocromático ou uma exportação estilo fax: a operação é sobre reduzir um raster pronto a um bit por pixel, não sobre como imagens individuais foram escaladas na entrada
O algoritmo em si é curto. A luminância é limiarizada em 50 por cento, e o erro de quantização é difundido para quatro vizinhos com os pesos clássicos 7/16, 3/16, 5/16 e 1/16, indo para a direita, abaixo à esquerda, abaixo e abaixo à direita. O pixel de saída é 0 ou 255 em todos os canais. O que a alternativa ingênua te dá em vez disso, um limiar duro sem difusão, transforma uma fotografia em silhueta e perde cada meio-tom que carregava o conteúdo
// Dithering na renderização para um dispositivo de preview monocromático
Pdf.RenderOutputDither := True;
// Ou aplique a mesma passada a um bitmap que você já tem. O bitmap
// precisa ser pf24bit; a função devolve False em vez de adivinhar
if not HPDFFloydSteinbergDitherBitmap(Preview) then
raise Exception.Create('dither expects a 24-bit bitmap');
// Acesso direto ao kernel quando você faz resample fora da pipeline do documento
Small := HPDFResizeBitmapKernel(Large, 640, 480, rkLanczos3);
try
Small.SaveToFile('thumb.bmp');
finally
Small.Free;
end;
Há um detalhe de implementação na difusão de erro que morde todo mundo uma vez. O buffer de erro de linha para linha precisa acumular. Cada pixel da linha seguinte recebe contribuições de três pixels diferentes da linha atual, os taps 3/16, 5/16 e 1/16, e se o código atribui em vez de somar, cada escrita descarta a contribuição anterior e só o último tap sobrevive. A imagem ainda parece com dithering, e é isso que dificulta notar, mas a textura está errada e a reprodução tonal desvia. O teste que pega isso é quantitativo: faça dither de um campo uniforme de cinza médio e exija que a cobertura interior fique entre 40 e 60 por cento
Qual combinação uma pipeline de redução de tamanho deve usar?
Case o kernel com o que as imagens realmente são, e trate o dithering como uma preocupação de dispositivo, não de compressão. Para digitalizações fotográficas que precisam sobreviver a um orçamento de tamanho, rkLanczos3 a 150 ou 200 DPI mantém o detalhe que as pessoas notam enquanto corta a contagem de pixels por um fator de quatro ou mais. Para screenshots, diagramas e line art, o rkHalftone é genuinamente ok e bem mais rápido, porque essas imagens têm poucos gradientes tonais a preservar. Para um lote misto em que você não pode inspecionar cada imagem, o rkBicubic é o meio-termo razoável: melhor que bilinear, aproximadamente metade da contagem de taps do Lanczos3
Downsampling é uma alavanca entre várias, e nem sempre é a maior. Digitalizações bilevel geralmente respondem muito melhor ao encoder coberto em compressão bilevel JBIG2 nativa em Delphi, onde o ganho vem de dicionários de símbolos em vez de contagens de pixels. Antes de decidir, ajuda saber o que realmente está no arquivo, que é o propósito de extrair imagens e seus decode filters: um inventário dos objetos de imagem e da compressão existente diz se o resampling tem algo a ganhar
Se você está construindo a superfície de preview que mostra o resultado, o mesmo caminho de renderização documentado em renderização de uma página PDF para um bitmap é onde o RenderOutputDither faz efeito, então o preview com dithering e o output com dithering vêm de um único caminho de código em vez de duas implementações que se separam
O princípio amplo por trás das duas funcionalidades é que configurações de qualidade devem ser explícitas e reversíveis. O HotPDF mantém o comportamento histórico como padrão para uma aplicação existente atualizar sem uma mudança surpresa na saída ou no tempo, e deixa os caminhos mais bonitos e mais lentos a uma atribuição de propriedade de distância. Os dois fazem parte do componente PDF HotPDF para Delphi, junto com a maquinaria de otimização de recursos e renderização sobre a qual se apoiam