A losLab PDF Library for Delphi comprime uma página digitalizada com Mixed Raster Content por meio de TPDFlib.CompressPageAsMRC. O método renderiza a página, aplica um limiar para gerar uma máscara de primeiro plano de 1 bit em resolução total, reduz o resto para um fundo JPEG e remonta a página como duas imagens: um fundo /DCTDecode e uma máscara stencil /CCITTFaxDecode pintada na cor de preenchimento atual
A página para a qual isso existe é aquela de que todo acervo de documentos está cheio: uma digitalização colorida de 300 DPI de uma fatura ou um contrato, armazenada como um único JPEG grande. Codifique com qualidade que mantenha o texto legível e o arquivo fica grande; baixe a qualidade e as letras ganham halos e hastes serrilhadas borradas, porque a coisa que o JPEG pior faz é uma borda preta nítida sobre papel branco. O Mixed Raster Content para de pedir que um codec faça dois trabalhos
Por que um único JPEG deixa o texto digitalizado borrado e grande ao mesmo tempo?
Um único JPEG lida mal com texto digitalizado porque o codec DCT gasta seus bits em gradientes suaves e aproxima bordas duras com coeficientes de alta frequência que a quantização descarta primeiro. Texto em uma digitalização é quase nada além de bordas duras. Manter essas bordas limpas força uma configuração de qualidade alta pela página inteira, incluindo a textura do papel, o logo e a mancha de café, nenhum dos quais precisava disso
A ideia do MRC divide a página pelo que cada região realmente é. A tinta vira uma imagem bilevel, na qual um codec de fax como o CCITT registra sequências de pixels pretos e brancos sem perdas e de forma bem compacta. Todo o resto, o tom do papel, fotos, fundos coloridos, vira uma imagem de tom contínuo que pode ser armazenada a uma fração da resolução, porque ninguém lê um fundo a 300 DPI. A camada bilevel mantém o texto nítido em resolução total; a camada de fundo mantém a cor a baixo custo
Como o CompressPageAsMRC divide uma página em duas camadas?
O TPDFlib.CompressPageAsMRC roda um pipeline fixo por página. Ele renderiza a página no DPI solicitado em um bitmap de 24 bits, classifica cada pixel com uma luminância ponderada Rec. 601 (299/587/114 por mil) contra Threshold, e escreve o resultado em uma máscara de 1 bit na mesma resolução. Em paralelo ele encolhe a mesma renderização por BGScale com média de caixa, então um BGScale de 4 transforma cada bloco de pixels 4×4 em um pixel de fundo
O conteúdo da página é então substituído. Os operadores originais são removidos, o fundo reduzido é embutido como uma imagem /DCTDecode em /DeviceRGB (ISO 32000-1 §7.4.8), a máscara é codificada com o encoder CCITT da biblioteca como uma imagem /CCITTFaxDecode (§7.4.6) marcada como /ImageMask true, e ambas são desenhadas pela caixa da página inteira com a máscara por cima. A assinatura é curta:
function CompressPageAsMRC(Page: Integer; DPI: Double;
BGScale, JPEGQuality, Threshold, Options: Integer): Integer;
Pageé base 1 e deve estar dentro dePageCountDPIé a resolução de renderização do raster de origem, 50 a 1200BGScaleé o fator de redução do fundo, 1 a 8JPEGQualityé a qualidade JPEG do fundo, 1 a 100Thresholdé o corte de luminância, 1 a 255; pixels no valor ou acima dele são papel, pixels mais escuros são tintaOptionsé reservado e deve ser 0
Um lote típico sobre um arquivo digitalizado fica assim. Chame SetFillColor imediatamente antes de cada página, por um motivo que a próxima seção explica
uses
SysUtils, PDFlibrary;
procedure CompressScannedFile(const SrcFile, DstFile: string);
var
Lib: TPDFlib;
P, Replaced: Integer;
begin
Lib := TPDFlib.Create;
try
if Lib.LoadFromFile(SrcFile, '') <> 1 then
raise Exception.CreateFmt('Cannot open %s', [SrcFile]);
for P := 1 to Lib.PageCount do
begin
Lib.SetFillColor(0, 0, 0); // cor da tinta da camada stencil
// máscara de 300 DPI, fundo de 75 DPI, qualidade JPEG 60, corte em luma 160
Replaced := Lib.CompressPageAsMRC(P, 300, 4, 60, 160, 0);
if Replaced = 0 then
raise Exception.CreateFmt('MRC failed on page %d', [P]);
end;
if Lib.SaveToFile(DstFile) <> 1 then
raise Exception.CreateFmt('Cannot save %s', [DstFile]);
finally
Lib.Free;
end;
end;
Por que o primeiro plano é pintado com a cor de preenchimento atual?
A camada de primeiro plano é uma máscara stencil, e uma máscara stencil não tem cor própria. A ISO 32000-1 §8.9.6.2 define uma imagem com /ImageMask true como uma forma de 1 bit: os valores de amostra marcam onde a tinta vai, e a tinta é a cor nonstroking que estiver atual no estado gráfico. É exatamente isso que torna a camada barata, e é também por isso que o CompressPageAsMRC não recebe parâmetro de cor
O TPDFlib.CompressPageAsMRC pinta a máscara com a cor definida por SetFillColor, então a ordem das chamadas importa. O pintor emite a cor de preenchimento de forma preguiçosa: o SetFillColor registra a cor e a marca como alterada, e o desenho do stencil escreve o operador rg dentro do próprio bloco q/Q apenas enquanto essa alteração segue pendente. Desenhe uma forma preenchida no meio do caminho e a alteração pendente é consumida por essa forma. Definir a cor imediatamente antes de cada página, como no lote acima, elimina o palpite. Preto é a escolha normal; um azul-marinho escuro funciona para formulários de tinta azul em que você quer que a saída pareça menos uma fotocópia
// Formulário de caneta esferográfica azul: renderize a camada de tinta em azul-marinho escuro
Lib.SetFillColor(0.08, 0.12, 0.35);
Replaced := Lib.CompressPageAsMRC(PageNo, 300, 4, 55, 170, 0);
O posicionamento segue a origem de coordenadas da biblioteca. O CompressPageAsMRC lê o GetOrigin e desenha ambas as imagens a partir da borda superior da caixa da página: coordenada superior 0 quando a origem é o canto superior esquerdo (SetOrigin(1)), coordenada superior PageHeight com a origem padrão no canto inferior esquerdo. Você não precisa ajustar nada, mas isso explica por que as camadas caem no lugar certo seja qual for a origem que seu pipeline já usava
Uma consequência da semântica de stencil vale dizer com clareza: todo pixel classificado como tinta recebe a mesma cor. Um carimbo vermelho, uma assinatura azul e o corpo de texto preto acabam em uma única cor de preenchimento na camada de primeiro plano. Só o JPEG de fundo mantém um traço tênue e de baixa resolução das cores originais por baixo
Escolhendo DPI, BGScale, Threshold e qualidade JPEG
O valor de DPI decide a nitidez do texto, e 300 é o padrão sensato para digitalizações de escritório. A máscara é mantida na resolução total da renderização, então o DPI define que traço fino sobrevive à limiarização. Ir acima da resolução da digitalização original não compra nada, porque o renderizador só faria upsampling da imagem digitalizada embutida. Memória é o outro limite: uma página US Letter a 300 DPI renderiza para 2550×3300 pixels, 25.245.000 bytes de dados de pixel de 24 bits antes de a máscara e o fundo serem derivados, e a mesma página no teto de 1200 DPI são 10200×13200 pixels, cerca de 404 MB
O BGScale troca detalhe do fundo por tamanho. A 300 DPI, um fator de 4 dá um fundo de 75 DPI, 638×825 pixels para Letter, o que dá e sobra para tom de papel e áreas de cor chapada. Páginas com fotografias ou ilustrações em meio-tom que o leitor vai realmente olhar merecem 2; texto simples sobre papel branco aguenta 6 ou 8, já que o fundo mal passa de uma tonalidade
O Threshold é o parâmetro que muda a cara da página. Subi-lo classifica mais pixels de cinza médio como tinta, o que engrossa traços finos e marcas de lápis, mas também arrasta ruído do scanner e sombra do papel como pontinhos. Baixá-lo dá papel mais limpo e letras mais finas, às vezes quebradas. Valores por volta de 128 a 170 são uma faixa de partida razoável para texto preto sobre branco; a troca entre os dois é a mesma discutida em renderizar páginas PDF para monocromático de 1 bit em Delphi, onde o mesmo limiar de luminância comanda o RenderPageToMonochromeFileEx. Diferente desse método, o CompressPageAsMRC não tem passe de despeckle, então digitalizações ruidosas se dão melhor com um limiar um pouco mais baixo
O JPEGQuality importa menos do que em uma digitalização de imagem única, porque o fundo não carrega mais nenhuma borda de texto. Qualidade de 50 a 70 costuma ser invisível em um fundo de 75 DPI atrás de uma máscara nítida
O que o valor de retorno diz, e o que ele não diz?
O TPDFlib.CompressPageAsMRC devolve o tamanho em bytes do conteúdo de página que substituiu, e 0 quando algo falhou. É um indicador de sucesso, não uma medida de economia. Em uma página digitalizada típica, o content stream original tem poucas dezenas de bytes, só os operadores suficientes para posicionar uma imagem, então um número positivo pequeno é o resultado normal mesmo quando a página encolhe megabytes. Uma página cujo content stream original era vazio reporta 0 apesar de ter sido remontada
O comportamento de falha não é atômico, e é essa a parte para projetar em volta. Checagens de argumentos, a renderização e a codificação JPEG rodam antes de a página ser tocada, então um BGScale fora da faixa, um JPEGQuality de 101 ou uma falha de renderização deixam a página exatamente como estava. Depois que o conteúdo original foi limpo, porém, uma falha ao embutir uma das camadas ou desenhá-la também devolve 0, e nesse ponto a página pode já estar parcialmente remontada. Trate qualquer 0 como razão para descartar o documento em memória em vez de salvá-lo, e nunca escreva o resultado por cima do arquivo de origem
Para descobrir o que a conversão realmente poupou, meça o documento. Comparar o comprimento do SaveToString antes e depois é a checagem mais rápida; para uma resposta por categoria, a auditoria de tamanho de arquivo PDF com AuditDocumentSpace mostra se o peso saiu das imagens como esperado
function MRCSavings(Lib: TPDFlib): Int64;
var
Before: Int64;
P: Integer;
begin
Before := Length(Lib.SaveToString);
for P := 1 to Lib.PageCount do
begin
Lib.SetFillColor(0, 0, 0);
if Lib.CompressPageAsMRC(P, 300, 4, 60, 160, 0) = 0 then
raise Exception.CreateFmt('Page %d failed; discard this document', [P]);
end;
Result := Before - Length(Lib.SaveToString);
end;
Onde este MRC de duas camadas fica aquém do ITU-T T.44
O CompressPageAsMRC implementa um MRC propositalmente simplificado. O modelo completo do ITU-T T.44 usa três camadas por stripe: uma imagem de fundo, uma imagem de primeiro plano que carrega as cores da tinta, e uma máscara bilevel que escolhe entre elas pixel a pixel, com espaço para vários pares de máscara e primeiro plano em uma página. Em termos de PDF, a terceira camada seria uma imagem de primeiro plano colorida de baixa resolução com máscara explícita (§8.9.6.3) em vez de um stencil pintado em uma cor. A PDF Library for Delphi pula a camada de cor de primeiro plano, que é o que torna o resultado tão pequeno e também por que a tinta colorida colapsa na cor de preenchimento
- O método serve para texto preto ou de cor única sobre papel digitalizado; documentos com cores codificando significado, texto destacado e carimbos coloridos perdem a cor da tinta
- A página remonta é raster puro: qualquer texto real, arte vetorial ou caracteres selecionáveis da página original viram pixels, então rode em digitalizações, não em páginas born-digital, onde subsetting de fontes e redução de imagens são as ferramentas certas
- A máscara usa o encoder CCITT da biblioteca em modo bidimensional T.4, gravado como
/CCITTFaxDecodecom/K 1; o encoder também tem um modo Group 4 por meio doAddImageCCITTFromBitmapStream, mas oCompressPageAsMRCnão o expõe, e não usa JBIG2 - Anotações e campos de formulário não são achatados; eles ficam como objetos acima do conteúdo remontado
- O fundo é
/DeviceRGB, então a saída não serve para fluxos PDF/X-1a, e o próprioSetFillColoré rejeitado em modo PDF/X-1a - Uma digitalização que já vai precisar de OCR deve ser reconhecida antes do MRC, enquanto os pixels originais ainda existem em profundidade de cor total
FAQ
O CompressPageAsMRC mantém o texto pesquisável?
Não. O CompressPageAsMRC substitui o conteúdo da página por duas imagens, então qualquer operador de texto na página original, incluindo uma camada de texto OCR invisível, é removido junto. Rode OCR na página comprimida depois, ou acrescente a camada de texto de volta após a compressão
Posso comprimir apenas algumas páginas de um documento?
Sim. O CompressPageAsMRC trabalha em uma página base 1 por chamada, então você escolhe as páginas e pode até variar BGScale e Threshold por página, por exemplo fator 2 para um encarte de foto e 6 para páginas de texto simples
Qual deve ser o parâmetro Options?
Passe 0. Options é reservado para uso futuro na versão atual e nenhum bit está definido, então um valor diferente de zero não carrega significado hoje
O CompressPageAsMRC chegou na losLab PDF Library for Delphi v3.561.0 junto com os blocos de construção de renderização, JPEG, CCITT e máscara stencil de que é montado, todos disponíveis como métodos públicos caso você precise de uma variante do pipeline. A biblioteca, o suporte a C++Builder e a referência completa da API estão na página do produto PDF Library for Delphi