Artigo Técnico

Kernels de downsampling e dithering de impressão no HotPDF

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

Curvas de peso dos três kernels de downsampling do HotPDF: o rkHalftone delega no caminho GDI HALFTONE de classe bilinear com suporte de um, o rkBicubic corre uma cúbica Catmull-Rom separável com suporte de dois, e o rkLanczos3 corre um sinc com janela de suporte três, trocando aproximadamente uma ordem de grandeza de velocidade por fotografias visivelmente melhores
Os três valores de kernel sentam-se em pontos genuinamente diferentes da curva de velocidade e qualidade: um caminho GDI de classe bilinear, uma cúbica Catmull-Rom e um sinc com janela de três lóbulos, e os kernels separáveis normalizam os pesos para nada soar para além do preto ou do branco

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

Gráfico do ramo externo Catmull-Rom bicúbico do HotPDF mostrando porque um coeficiente errado se esconde: a gralha que escreve -5 em vez de -4 na forma de Horner aninhada ainda avalia e deixa W(1) em -1 e W(2) em -2 onde zero é exigido, por isso afirmar que W(0) iguala 1 mais as duas restrições de zero apanha-a em segundos
Um kernel partido nunca rebenta, apenas devolve números com ar plausível, e é por isso que o olho não apanha uma gralha num coeficiente. W(0) = 1 com zeros em mais e menos um e dois é um teste unitário de três linhas

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

Colocação no pipeline de renderização do dithering Floyd-Steinberg do HotPDF: o RenderOutputDither corre depois da composição da página sobre o raster de 24 bits terminado, limitando a luminância a 50 por cento e difundindo cada erro de quantização para a direita e para baixo com pesos 7/16, 3/16, 5/16 e 1/16 através de um buffer de linha que tem de acumular, produzindo output monocromático de um bit
O dithering pertence depois da composição porque reduz um raster terminado a um bit, e não por causa de como as imagens foram escaladas. Os pesos de difusão somam um, e o buffer de linha tem de acumular em vez de sobrescrever
// 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