A losLab PDF Library for Delphi comprime uma página digitalizada com Mixed Raster Content através do TPDFlib.CompressPageAsMRC. O método renderiza a página, aplica um limiar a uma máscara de primeiro plano de 1 bit à resolução total, reduz o resto para um fundo JPEG e reconstrói 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 que isto existe é aquela de que todos os arquivos de documentos estão cheios: uma digitalização a cores de 300 DPI de uma fatura ou de um contrato, guardada como um único JPEG grande. Codifique-a com uma qualidade que mantenha o texto legível e o ficheiro fica grande; baixe a qualidade e as letras ganham halos e serifas borradas, porque a coisa que o JPEG pior faz é uma aresta preta nítida sobre papel branco. O Mixed Raster Content deixa de pedir a um único codec que faça dois trabalhos
Por que um único JPEG deixa o texto digitalizado ao mesmo tempo borrado e grande?
Um único JPEG lida mal com texto digitalizado porque o codec DCT gasta os bits em gradientes suaves e aproxima arestas duras com coeficientes de alta frequência que a quantização descarta primeiro. O texto numa digitalização quase não é mais do que arestas duras. Manter essas arestas limpas obriga a uma qualidade alta na página inteira, incluindo a textura do papel, o logótipo 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 torna-se uma imagem bilevel, em que um codec de fax como o CCITT regista sequências de píxeis pretos e brancos sem perdas e de forma muito compacta. Todo o resto, o tom do papel, as fotografias, os fundos coloridos, torna-se uma imagem de tom contínuo que pode ser guardada 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 à resolução total; a camada de fundo mantém a cor a baixo custo
Como divide o CompressPageAsMRC uma página em duas camadas?
O TPDFlib.CompressPageAsMRC corre um pipeline fixo por página. Renderiza a página ao DPI pedido para um bitmap de 24 bits, classifica cada píxel com uma luminância ponderada Rec. 601 (299/587/114 por milésimos) contra o Threshold, e escreve o resultado numa máscara de 1 bit à mesma resolução. Em paralelo encolhe a mesma renderização por BGScale com uma média em caixa, pelo que um BGScale de 4 transforma cada bloco de píxeis de 4×4 num único píxel de fundo
O conteúdo da página é depois substituído. Os operadores originais são removidos, o fundo reduzido é incorporado como uma imagem /DCTDecode em /DeviceRGB (ISO 32000-1 §7.4.8), a máscara é codificada com o codificador CCITT da biblioteca como uma imagem /CCITTFaxDecode (§7.4.6) e marcada /ImageMask true, e ambas são desenhadas ao longo da 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 tem de 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; píxeis no valor ou acima dele são papel, píxeis mais escuros são tintaOptionsé reservado e tem de ser 0
Um lote típico sobre um ficheiro digitalizado faz-se assim. Chame o SetFillColor imediatamente antes de cada página, por uma razão que a secção seguinte 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 a 300 DPI, fundo a 75 DPI, qualidade JPEG 60, corte na 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 na 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 pelo SetFillColor, pelo que a ordem das chamadas importa. O pintor emite a cor de preenchimento preguiçosamente: o SetFillColor regista a cor e marca-a como alterada, e o desenho do stencil escreve o operador rg dentro do seu próprio bloco q/Q apenas enquanto essa alteração ainda está pendente. Desenhe uma forma preenchida entretanto e a alteração pendente é consumida por essa forma. Definir a cor imediatamente antes de cada página, como no lote acima, remove o palpite. Preto é a escolha normal; um azul-marinho escuro funciona em formulários de tinta azul em que quer que o resultado pareça menos uma fotocópia
// Formulário preenchido a caneta esferográfica azul: renderiza a camada de tinta num 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 aresta superior da caixa da página: coordenada superior 0 quando a origem é o canto superior esquerdo (SetOrigin(1)), coordenada superior PageHeight com a origem predefinida no canto inferior esquerdo. Não precisa de ajustar nada, mas explica por que as camadas aterram corretamente seja qual for a origem que o seu pipeline já usava
Uma consequência da semântica de stencil vale a pena enunciar com clareza: cada píxel classificado como tinta recebe a mesma cor. Um selo vermelho, uma assinatura azul e texto corrido preto acabam todos numa única cor de preenchimento na camada de primeiro plano. Só o JPEG de fundo mantém por baixo um rasto ténue e de baixa resolução das cores originais
Escolher DPI, BGScale, Threshold e qualidade JPEG
O valor DPI decide a nitidez do texto, e 300 é a predefinição sensata para digitalizações de escritório. A máscara é mantida à resolução total de renderização, pelo que o DPI define qual a finura de traço que sobrevive ao limiar. Ir acima da resolução da digitalização original não compra nada, porque o renderizador estaria apenas a ampliar a imagem digitalizada incorporada. A memória é o outro limite: uma página US Letter a 300 DPI renderiza para 2550×3300 píxeis, 25,245,000 bytes de dados de píxeis 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 píxeis, 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 píxeis para Letter, o que chega e sobra para tom de papel e áreas de cor chapada. Páginas com fotografias ou ilustrações em meio-tom que um 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 o aspeto da página. Subi-lo classifica mais píxeis de cinzento 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 pontos. Baixá-lo dá papel mais limpo e letras mais finas, por vezes partidas. Valores entre 128 e 170 são um intervalo de partida razoável para texto preto sobre branco; o compromisso é o mesmo discutido em renderizar páginas PDF para 1-bit monocromático no Delphi, em que o mesmo limiar de luminância comanda o RenderPageToMonochromeFileEx. Ao contrário desse método, o CompressPageAsMRC não tem passagem de remoção de pontos, pelo que digitalizações ruidosas saem melhor com um limiar ligeiramente mais baixo
O JPEGQuality importa menos do que numa digitalização de imagem única, porque o fundo já não transporta arestas de texto. Qualidade 50 a 70 é normalmente invisível num fundo de 75 DPI atrás de uma máscara nítida
O que lhe diz o valor devolvido, e o que não lhe diz?
O TPDFlib.CompressPageAsMRC devolve o tamanho em bytes do conteúdo da página que substituiu, e 0 quando algo falhou. É um indicador de sucesso, não uma medida de poupança. Numa página digitalizada típica o fluxo de conteúdo original tem poucas dezenas de bytes, apenas os operadores suficientes para colocar uma imagem, pelo que um número positivo pequeno é o resultado normal mesmo quando a página encolhe por megabytes. Uma página cujo fluxo de conteúdo original estava vazio reporta 0 apesar de ter sido reconstruída
O comportamento de falha não é atómico, e é essa a parte para ter em conta no design. As verificações de argumentos, a renderização e a codificação JPEG correm antes de a página ser tocada, pelo que um BGScale fora do intervalo, um JPEGQuality de 101, ou uma falha de renderização deixam a página exatamente como estava. Depois de o conteúdo original ter sido limpo, porém, uma falha ao incorporar uma das camadas ou desenhá-la também devolve 0, e nesse momento a página pode já estar parcialmente reconstruída. Trate qualquer 0 como razão para descartar o documento em memória em vez de o guardar, e nunca escreva o resultado por cima do ficheiro de origem
Para saber o que a conversão poupou de fato, meça o documento. Comparar o comprimento do SaveToString antes e depois é a verificação mais rápida; para uma resposta por categoria, a auditoria de tamanho de ficheiro PDF com o 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 deliberadamente simplificado. O modelo completo do ITU-T T.44 usa três camadas por faixa: uma imagem de fundo, uma imagem de primeiro plano que transporta as cores da tinta, e uma máscara bilevel que escolhe entre elas píxel a píxel, com espaço para vários pares de máscara e primeiro plano numa 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 numa só cor. A PDF Library for Delphi salta a camada de cor de primeiro plano, que é o que torna o resultado tão pequeno e também a razão por que a tinta colorida colapsa na cor de preenchimento
- O método serve para texto preto ou de uma só cor sobre papel digitalizado; documentos com código de cores, texto realçado e selos coloridos perdem a cor da tinta
- A página reconstruída é raster puro: qualquer texto real, arte vetorial ou caracteres selecionáveis na página original tornam-se píxeis, por isso corra-o sobre digitalizações, não sobre páginas born-digital, onde o subsetting de fontes e a redução de imagens são as ferramentas certas
- A máscara usa o codificador CCITT da biblioteca em modo bidimensional T.4, escrito como
/CCITTFaxDecodecom/K 1; o codificador também tem um modo Group 4 através doAddImageCCITTFromBitmapStream, mas oCompressPageAsMRCnão o expõe, e não usa JBIG2 - Anotações e campos de formulário não são achatados; ficam como objetos acima do conteúdo reconstruído
- O fundo é
/DeviceRGB, pelo que o resultado não serve para fluxos PDF/X-1a, e o próprioSetFillColoré rejeitado em modo PDF/X-1a - Uma digitalização que já precise de OCR deve ser reconhecida antes do MRC, enquanto os píxeis originais ainda existem à profundidade total de cor
FAQ
O CompressPageAsMRC mantém o texto pesquisável?
Não. O CompressPageAsMRC substitui o conteúdo da página por duas imagens, pelo que quaisquer operadores de texto na página original, incluindo uma camada de OCR invisível, são removidos com ela. Corra OCR sobre a página comprimida depois, ou acrescente a camada de texto de novo após a compressão
Posso comprimir só algumas páginas de um documento?
Sim. O CompressPageAsMRC trabalha sobre uma página base 1 por chamada, pelo que escolhe as páginas e pode até variar o BGScale e o Threshold por página, por exemplo um fator de 2 para um suplemento fotográfico e 6 para páginas de texto simples
Que valor deve ter o parâmetro Options?
Passe 0. O Options é reservado para uso futuro na versão atual e nenhum bit está definido, pelo que um valor não nulo não tem hoje significado
O CompressPageAsMRC chegou na losLab PDF Library for Delphi v3.561.0 juntamente 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 se precisar de uma variante do pipeline. A biblioteca, o seu suporte C++Builder e a referência completa da API estão na página do produto PDF Library for Delphi