Artigo Técnico

Conteúdo raster misto (MRC) em Delphi: JPEG + CCITT

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

Digitalização em JPEG único versus a divisão em camadas de Mixed Raster Content criada por PDFlibPas CompressPageAsMRC em Delphi: o codec DCT borra bordas duras de texto e força qualidade alta pela página inteira, enquanto a tinta vira um stencil CCITT de 1 bit sem perdas a 300 DPI e o tom do papel vira um fundo JPEG DeviceRGB de 75 DPI
O Mixed Raster Content para de pedir que um codec faça dois trabalhos: a camada bilevel mantém as letras nítidas em resolução total e o fundo mantém a cor a uma fração do custo, então qualidade de 50 a 70 costuma ser invisível

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

Fluxo de dados do CompressPageAsMRC no PDFlibPas para Delphi: uma renderização de 300 DPI e 24 bits tem limiar aplicado por luminância Rec. 601 virando um stencil ImageMask CCITTFaxDecode K 1 em resolução total, é reduzida por média de caixa com BGScale 4 para um fundo DCTDecode DeviceRGB, e ambas as imagens substituem os operadores da página original com a máscara por cima
Ambas as camadas derivam de uma única renderização: o DPI decide que traço fino sobrevive à limiarização, o BGScale troca detalhe do fundo de 2 para fotografias até 8 para texto simples, e a página remonta desenha o stencil na cor de preenchimento atual

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 de PageCount
  • DPI é a resolução de renderização do raster de origem, 50 a 1200
  • BGScale é o fator de redução do fundo, 1 a 8
  • JPEGQuality é a qualidade JPEG do fundo, 1 a 100
  • Threshold é o corte de luminância, 1 a 255; pixels no valor ou acima dele são papel, pixels mais escuros são tinta
  • Options é 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

Sequência de emissão preguiçosa de cor de preenchimento para a camada stencil MRC do PDFlibPas em Delphi: SetFillColor registra a cor como alteração pendente, o desenho do stencil ImageMask escreve o operador rg dentro do próprio bloco q e Q enquanto a alteração segue pendente, e uma forma preenchida intermediária consome a alteração pendente
Uma máscara stencil não tem cor própria, então todo pixel de tinta recebe uma cor de preenchimento: um carimbo vermelho, uma assinatura azul e texto preto colapsam juntos, e por isso o SetFillColor deve ser chamado imediatamente antes de cada página
// 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 /CCITTFaxDecode com /K 1; o encoder também tem um modo Group 4 por meio do AddImageCCITTFromBitmapStream, mas o CompressPageAsMRC nã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óprio SetFillColor é 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