Duas reclamações chegam na semana seguinte ao lançamento de uma funcionalidade de compressão: o contrato digitalizado agora tem letras serrilhadas e peludas, e o logo transparente na capa fica dentro de um halo pálido. O PDFiumPas responde às duas em um só lugar. O TPdf.OptimizeImages mede cada imagem antes de reduzi-la, então escolhe um kernel de reamostragem e acumula cor em forma consciente de alpha
Isso nem sempre foi verdade. Antes da v3.100.0 o mesmo método reduzia toda imagem não bilevel com um passo fixo de nearest-neighbour, que é exatamente o algoritmo que produz as duas reclamações: ele faz point-sample de um pixel de origem por pixel de saída, e trata o RGB sob um pixel totalmente transparente como se um leitor o visse um dia. A reescrita na v3.100.0 substitui esse único caminho por cinco kernels, uma regra de seleção medida, e um orçamento explícito de memória de trabalho
Por que a redução faz texto digitalizado parecer serrilhado?
Porque o point sampling responde à pergunta errada. Quando uma varredura de 300 DPI é re-mirada para 150 DPI, cada pixel de destino representa um bloco de dois por dois de pixels de origem, e o nearest-neighbour mantém um dos quatro e descarta o resto. Qual sobrevive depende de arredondamento, então uma borda de traço que era suavemente antialiased na origem vira um cara ou coroa por pixel. O resultado é a clássica escada com aliasing ao longo das bordas dos glifos, mais moiré em regiões de meio-tom onde as amostras descartadas por acaso carregavam o padrão. Isso importa mais em um PDF do que na tela porque o dano é permanente. Um image XObject carrega seus dados de amostra junto com /Width, /Height e /BitsPerComponent (ISO 32000-1 §8.9.5), e a reamostragem reescreve os três dentro do arquivo. Um zoom ruim em um viewer é um quadro que você pode redesenhar, e o PDFiumPas tem maquinaria separada para isso em render cache e performance de zoom. Uma redução ruim é um documento novo que você entrega ao cliente
Como o PDFiumPas mede detalhe e escolhe um kernel
O PDFiumPas decide por imagem, não por documento. Antes de escolher um kernel ele calcula um score de detalhe de luminância normalizado a partir de uma grade de amostragem limitada: os passos horizontal e vertical são (Width + 63) div 64 e (Height + 63) div 64, então uma varredura de 12000 pixels e uma miniatura de 300 pixels custam aproximadamente a mesma varredura de 64 por 64. Em cada posição amostrada ele soma a diferença absoluta para o vizinho à direita e o vizinho abaixo, em até três canais, então divide pela contagem de amostras vezes 255. O score cai entre 0 e 1, onde gráficos de negócios planos ficam perto de zero e textura fotográfica densa sobe
A escada de seleção então roda em ordem fixa. Se ResampleFilter é qualquer coisa diferente de pirfAdaptive, esse filtro é usado verbatim. Caso contrário: conteúdo de 1 bit recebe pirfBilevel; um ContentClass de piccLineArt recebe pirfBox; um fator de escala de 4 ou mais também recebe pirfBox, porque nessa redução uma média de área é ao mesmo tempo a resposta mais barata e a mais correta; piccPhoto, um score de detalhe de 0.08 ou mais alto, ou um PreferredQuality de 0.9 ou mais alto recebe pirfLanczos com seu kernel de três lóbulos; uma escala de 2 ou mais ou uma qualidade de 0.7 ou mais alta recebe pirfBicubic de raio 2; tudo o que sobra recebe pirfBilinear. Como TPdfImageOptimizeOptions.Default define PreferredQuality como 0.85, uma execução padrão nunca cai de volta ao bilinear a menos que a redução seja leve e o conteúdo plano
uses
PDFium;
procedure ShrinkScannedPdf(const InputFile, OutputFile: string);
var
Pdf: TPdf;
Options: TPdfImageOptimizeOptions;
Report: TPdfImageOptimizeReport;
begin
Pdf := TPdf.Create(nil);
try
Pdf.FileName := InputFile;
// Padrões: TargetDpi 150, MinDpiRatio 1.5, PreserveBilevel True,
// MinDimension 8, pirfAdaptive, piccAuto, qualidade 0.85, orçamento 64 MiB.
Options := TPdfImageOptimizeOptions.Default;
Options.TargetDpi := 150;
Options.MinDpiRatio := 1.5;
Options.ContentClass := piccAuto;
Options.PreferredQuality := 0.85;
if Pdf.OptimizeImages(Options, Report) and (Report.OptimizedCount > 0) then
Pdf.SaveAs(OutputFile);
finally
Pdf.Free;
end;
end;
Uma imagem só é tocada quando o maior entre seu DPI de posicionamento horizontal e vertical, dividido por TargetDpi, atinge MinDpiRatio. Esse guard existe para que uma foto de 160 DPI mirada em um alvo de 150 DPI não seja re-codificada por um ganho de seis por cento que custa uma geração de qualidade. Imagens abaixo de MinDimension em qualquer eixo, 8 por padrão, são puladas como ícones ou réguas
Por que logos transparentes ganham uma franja branca?
Porque a cor sob um pixel totalmente transparente é arbitrária, e uma média ponderada simples deixa ela votar. Exporte um logo de uma ferramenta de design e a margem invisível frequentemente é branca, ou preta, ou o que quer que o canvas fosse; o canal alpha a esconde, e uma soma direta sobre a pegada do kernel prontamente a mistura de volta na borda visível. O PDFiumPas evita isso acumulando amostras BGRA em forma premultiplicada e desfazendo a premultiplicação só no pixel de destino
Concretamente, cada amostra contribuidora adiciona channel * alpha * weight ao acumulador de cor, alpha * weight a um acumulador de alpha, e weight à soma de pesos. A cor de destino então é dividida pelo acumulador de alpha em vez de pela soma de pesos, e esse é o passo que importa: dividir pela soma de pesos arrastaria a cor em direção aos pixels invisíveis, enquanto dividir pelo alpha acumulado reconstrói a cor na qual as amostras visíveis realmente concordaram. O alpha de destino é uma quantidade separada, 255 * AlphaSum / WeightSum. Formatos sem alpha dividem pela soma de pesos como de costume, o byte de padding de um destino FPDFBitmap_BGRx é escrito como constante 255, e todo canal é limitado a 0 através de 255 antes de ser armazenado. Esse alpha normalmente se origina de uma entrada de soft mask no dicionário da imagem (ISO 32000-1 §11.4), que o PDFium já compôs no buffer BGRA que o resampler recebe
// Forma do loop interno de acumulação, por amostra de origem contribuidora
if SrcFormat = FPDFBitmap_BGRA then
Alpha := PByte(PAnsiChar(Pixel) + 3)^ / 255
else
Alpha := 1;
for Channel := 0 to Min(BytesPerPixel, 3) - 1 do
Accumulated[Channel] := Accumulated[Channel] +
PByte(PAnsiChar(Pixel) + Channel)^ * Alpha * Weight;
AlphaSum := AlphaSum + Alpha * Weight;
WeightSum := WeightSum + Weight;
// ... e no pixel de destino, desfazer a premultiplicação contra a soma de alpha
if SrcFormat = FPDFBitmap_BGRA then
begin
if Abs(AlphaSum) > 1E-12 then
ValueSum := Accumulated[Channel] / AlphaSum
else
ValueSum := 0;
end
else
ValueSum := Accumulated[Channel] / WeightSum;
Mantendo line art de 1 bit fora da zona cinza
Qualquer kernel contínuo aplicado a uma varredura bilevel produz cinza, e cinza é precisamente o que uma imagem estilo fax não pode conter. O PDFiumPas portanto deixa imagens de 1 bit em paz por padrão: PreserveBilevel é True em TPdfImageOptimizeOptions.Default, e tais imagens caem em SkippedCount intocadas. Defina como False e o caminho pirfBilevel assume no lugar de um kernel de suavização. Ele percorre o retângulo exato de origem cobrindo cada pixel de destino, faz a média da luminância com os pesos 0.114, 0.587 e 0.299 na ordem de memória BGR, e aplica threshold no resultado em 127.5 para um 0 ou 255 plano. Nada intermediário pode ser escrito, então as bordas permanecem nítidas e nenhum halo cinza se forma ao redor de traços finos; o canal alpha de uma origem BGRA é feito a média normalmente, e um destino BGRx recebe a constante 255. Se você precisa dos pixels por baixo em vez de um documento menor, extrair imagens de documentos PDF é o caminho separado
O que acontece quando uma imagem excede o orçamento de memória de trabalho?
Ela fica exatamente como estava, e é contada. MaxWorkingBytes tem padrão de 64 MiB e é aplicado duas vezes. Antes do bitmap de destino ser criado, o PDFiumPas rejeita a imagem se largura vezes altura vezes bytes por pixel excede o orçamento. Depois que FPDFBitmap_CreateEx tem sucesso ele confere de novo usando o stride real vezes altura, porque o padding de linha pode empurrar uma alocação além de um limite que o produto ingênuo liberava. Qualquer rejeição destrói o destino e não retorna nada. Seja claro sobre a degradação que isso implica: uma imagem acima do orçamento não é reamostrada em qualidade mais baixa, e não é dividida em tiles. A original permanece no documento, BudgetExceededCount e SkippedCount ambos aumentam, e uma execução pode portanto relatar sucesso enquanto um documento está só parcialmente otimizado. Esse é um comportamento fail-safe deliberado, mas significa que o report não é leitura opcional. Um modo de falha distinto também existe: imagens cujo bitmap o PDFium não consegue produzir de forma alguma, como CMYK, JPX, JBIG2 ou origens mascaradas, aumentam FailedCount em vez disso e também ficam intocadas
procedure OptimizeBatch(const Files: array of string);
var
Pdf: TPdf;
Options: TPdfImageOptimizeOptions;
Report: TPdfImageOptimizeReport;
I: Integer;
begin
Options := TPdfImageOptimizeOptions.Default;
Options.PreserveBilevel := False; // usa o voto de área bilevel
Options.ContentClass := piccPhoto; // força Lanczos para conjuntos de fotos
Options.MaxWorkingBytes := 256 * 1024 * 1024; // folga para varreduras grandes
Pdf := TPdf.Create(nil);
try
for I := Low(Files) to High(Files) do
begin
Pdf.FileName := Files[I];
if not Pdf.OptimizeImages(Options, Report) then
begin
WriteLn('optimize failed: ', Report.ErrorMessage);
Continue;
end;
if Report.BudgetExceededCount > 0 then
WriteLn(Files[I], ': ', Report.BudgetExceededCount,
' image(s) over budget and kept at full size');
if Report.FailedCount > 0 then
WriteLn(Files[I], ': ', Report.FailedCount,
' image(s) could not be decoded to a bitmap');
if Report.OptimizedCount > 0 then
Pdf.SaveAs(ChangeFileExt(Files[I], '.opt.pdf'));
end;
finally
Pdf.Free;
end;
end;
Lendo o report antes de entregar o arquivo
O TPdfImageOptimizeReport é feito para ser diagnosticado, não meramente registrado. Junto de OptimizedCount, SkippedCount e FailedCount ele expõe um contador por kernel, então BoxFilterCount, BilinearFilterCount, BicubicFilterCount, LanczosFilterCount e BilevelFilterCount dizem o que a regra adaptativa realmente concluiu sobre seu corpus. Um resultado todo-box significa que as reduções foram íngremes ou o conteúdo foi classificado como line art; um resultado todo-Lanczos em um documento que você acreditava ser line art é um sinal de que ContentClass deveria ser definido explicitamente. O AverageDetailScore é o número para comparar contra o threshold de Lanczos de 0.08 ao ajustar PreferredQuality, e o PeakWorkingBytes mostra quanto de MaxWorkingBytes a execução realmente precisou. Opções inválidas falham ruidosamente em vez de silenciosamente: um TargetDpi não positivo, um MinDpiRatio abaixo de 1, um PreferredQuality fora de 0 a 1, ou um MaxWorkingBytes não positivo levanta EPdfError antes de qualquer página ser tocada. E o OptimizeImages edita apenas o documento em memória; cada página modificada é commitada com FPDFPage_GenerateContent, depois do qual você ainda chama SaveAs você mesmo. Para olhar o que mudou, renderize os documentos antes e depois para bitmaps como descrito em converter páginas PDF para imagens JPEG e compare-os em zoom total
Reamostragem adaptativa é uma daquelas funcionalidades que são invisíveis quando funcionam e geram tickets de suporte quando não funcionam, e é por isso que a medição, o tratamento de alpha e o orçamento de memória tiveram que pousar juntos em vez de como três refinamentos separados. Se você está avaliando isto para um produto Delphi, C++Builder ou Lazarus, a superfície API completa e os detalhes de licenciamento estão na página do PDFiumPas Delphi PDFium component