O HotPDF expõe três kernels de downsampling de imagens através da propriedade ImageDownsampleKernel e uma passagem Floyd-Steinberg separada através de RenderOutputDither. A primeira controla como as fotografias ficam depois de as encolher para cumprir um orçamento de tamanho, a segunda controla como ficam depois de uma página ser reduzida a preto e branco. Nenhuma está ativa por predefinição, e ambas são opt-in pela mesma razão: custam tempo real
A pressão que leva as pessoas aqui é familiar. Um contrato digitalizado de 60 MB tem de sair por um gateway de email que rejeita tudo acima de 10 MB, ou um lote de extratos tem de chegar a um dispositivo monocromático estilo fax que renderiza cada pixel cinzento ou como papel ou como toner. Ambos os problemas são problemas de reamostragem, e ambos têm uma resposta rápida que se vê mal e uma resposta lenta que se vê bem
Em que diferem realmente os três kernels
THPDFResampleKernel tem três valores, e sentam-se em pontos genuinamente diferentes da curva de velocidade e qualidade. O rkHalftone delega no caminho histórico StretchBlt do GDI com o modo HALFTONE, que apesar do nome é filtragem de classe bilinear: rápido, adequado para line art e screenshots, e propenso às margens estaladiças que se reconhecem de imediato em fotografias reduzidas. O rkBicubic corre um kernel separável Catmull-Rom, e o rkLanczos3 corre um sinc com janela separável de suporte de três lóbulos
Ambos os kernels separáveis correm em duas passagens, horizontal e depois vertical, com 6 a 12 taps por pixel de destino em Pascal puro. Isso é aproximadamente uma ordem de grandeza mais lento do que o caminho GDI, que é precisamente porque o rkHalftone permanece a predefinição. Num lote noturno de milhares de páginas a diferença é uma decisão de agendamento, não uma preferência. Num único documento que um utilizador está à espera, o Lanczos3 é quase de graça e visivelmente melhor
Duas propriedades de implementação valem a pena conhecer porque determinam o que o output pode e não pode fazer. As margens fazem clamp por replicação de borda em vez de wrapping ou esbatimento, e os pesos são normalizados por pixel de destino. Em conjunto, essas duas coisas significam que o resultado nunca soa abaixo do preto ou acima do branco, por isso o clássico halo de overshoot do Lanczos à volta de uma margem dura não aparece como artefactos cortados 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 guarda que mantém a operação honesta. Recodificar uma imagem que já estava eficientemente comprimida pode produzir um stream maior do que o original, e um downsampler que substitui cegamente todas as imagens ocasionalmente faz crescer o ficheiro que lhe pediram para encolher. O limiar diz: só comprometa a substituição quando poupar pelo menos estes bytes. PreservedCalibratedImageCount reporta a outra decisão conservadora, imagens deixadas intocadas porque trazem um espaço de cor calibrado que a reamostragem comprometeria
Porque é que um coeficiente polinomial errado é tão difícil de detetar?
Porque um kernel de interpolação partido não rebenta nem lança exceção, apenas produz uma imagem que se parece subtilmente errada de uma forma que ninguém consegue atribuir. O kernel Catmull-Rom é cúbico por troços, e o seu ramo externo 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 a avaliar, continua a devolver números num intervalo plausível, e continua a produzir uma imagem
O dano aparece como W(1) a avaliar -1 onde tem de ser 0. Pesos negativos acumulam, a soma corta em zero, e o sintoma visível é um gradiente cuja ponta esquerda fica preta e uma margem de degrau que perde os tons intermédios. Nada na falha aponta para um polinómio. A verificação que o apanha em segundos é aritmética e não visual: um kernel interpolante tem de satisfazer W(0) = 1 e W(±1) = W(±2) = 0, e qualquer kernel que falhe esses três pontos tem um erro de coeficiente, ponto final. Afirme esses três valores num teste unitário e toda a classe de defeitos de gralha desaparece
Dithering Floyd-Steinberg, e onde pertence no pipeline
A passagem de dithering é um problema diferente da reamostragem e vive num ponto diferente do pipeline. O RenderOutputDither aplica difusão de erro Floyd-Steinberg depois da composição da página, que é a única colocação que faz sentido para uma pré-visualização de impressão monocromática ou uma exportação estilo fax: a operação trata de reduzir um raster terminado a um bit por pixel, e não de como as imagens individuais foram escaladas à entrada
O algoritmo em si é curto. A luminância é limitada a 50 por cento, e o erro de quantização é difundido a quatro vizinhos com os clássicos pesos 7/16, 3/16, 5/16 e 1/16, para a direita, abaixo à esquerda, abaixo e abaixo à direita. O pixel de output é 0 ou 255 em todos os canais. O que a alternativa ingénua lhe dá em vez disso, um limiar duro sem difusão, transforma uma fotografia numa silhueta e perde todos os tons médios que transportavam o conteúdo
// Dithering no momento de renderização para um dispositivo de pré-visualização monocromático
Pdf.RenderOutputDither := True;
// Ou aplique a mesma passagem a um bitmap que já possui. O bitmap tem de
// 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 reamostra fora do 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 toda a gente uma vez. O buffer de erro de linha para linha tem de acumular. Cada pixel da linha seguinte recebe contribuições de três pixels diferentes da linha corrente, 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 torna difícil notar, mas a textura está errada e a reprodução tonal deriva. O teste que o apanha é quantitativo: aplique dithering a um campo cinzento médio uniforme e exija que a cobertura interior caia entre 40 e 60 por cento
Que combinação deve usar um pipeline de redução de tamanho?
Casa o kernel com o que as imagens realmente são, e trate o dithering como uma preocupação do dispositivo e não da compressão. Para digitalizações fotográficas que têm de sobreviver a um orçamento de tamanho, rkLanczos3 a 150 ou 200 DPI mantém o detalhe de que as pessoas dão conta enquanto corta a contagem de pixels por um fator de quatro ou mais. Para screenshots, diagramas e line art, o rkHalftone é genuinamente suficiente e muito mais rápido, porque essas imagens têm poucos gradientes tonais a preservar. Para um lote misto em que não pode inspecionar cada imagem, o rkBicubic é o meio-termo razoável: melhor do que bilinear, aproximadamente metade da contagem de taps do Lanczos3
O downsampling é uma alavanca entre várias, e nem sempre é a maior. As digitalizações bilevel normalmente respondem muito melhor ao codificador coberto em compressão bilevel JBIG2 nativa em Delphi, onde o ganho vem de dicionários de símbolos e não de contagens de pixels. Antes de decidir, ajuda saber o que está realmente no ficheiro, que é para isso que serve extrair imagens e os seus filtros de descodificação: um inventário de objetos de imagem e da compressão existente diz-lhe se a reamostragem tem algo a ganhar
Se está a construir a superfície de pré-visualização que mostra o resultado, o mesmo caminho de renderização documentado em renderizar uma página PDF para um bitmap é onde o RenderOutputDither faz efeito, por isso a pré-visualização com dithering e o output com dithering vêm de um único caminho de código em vez de duas implementações que se afastam
O princípio geral por trás de ambas as funcionalidades é que as definições de qualidade devem ser explícitas e reversíveis. O HotPDF mantém o comportamento histórico como predefinição para que uma aplicação existente atualize sem uma mudança surpresa no output ou no timing, e põe os caminhos mais bonitos e mais lentos a uma atribuição de propriedade de distância. Ambos fazem parte do componente PDF Delphi HotPDF, a par da maquinaria de otimização de recursos e renderização sobre a qual se constroem