Artigo Técnico

Renderizar páginas PDF em monocromia 1-bit no Delphi

Um gateway de fax não quer receber a sua renderização de página em 24 bits. O pipeline de arquivo que guarda um milhão de notas fiscais com aparência de scan também não quer, e a frente de OCR que reduz tudo para preto e branco antes mesmo de procurar um caractere tampouco. Nos três casos, o objetivo é o mesmo: um bitmap limpo de 1 bit, um bit por pixel, em que cada ponto seja tinta ou papel. Se você entregar um BMP colorido, eles vão jogar fora 23 bits por pixel de qualquer jeito, normalmente com uma dithering pior do que a sua. A pergunta interessante é onde essa redução deve acontecer, e a resposta no PDF Library for Delphi acaba dizendo algo útil sobre como estender um renderizador que você prefere não reescrever

PDF Library for Delphi é uma biblioteca PDF nativa em Object Pascal para Delphi e C++Builder. O núcleo de renderização rasteriza uma página para bitmap e pode emitir BMP, PNG, JPEG, WMF e alguns outros formatos. Até então, porém, ele não devolvia um bitmap realmente monocromático, nem renderizava apenas uma parte da página. As duas coisas chegaram na v3.83.0, e ambas foram construídas como camadas finas de conveniência sobre o renderizador existente, não como mudanças no rasterizador em si. Essa restrição é a história inteira

Por que a redução vem depois da renderização

O jeito óbvio de produzir uma imagem de 1 bit é mandar o rasterizador desenhar em 1 bit. Esse também é o jeito de quebrar todo o resto. O bitmap interno do renderizador é criado com PixelFormat := pf24bit fixo no construtor PDFlibRenderer, e essa superfície de 24 bits é compartilhada por todo caminho de renderização: exportação PNG, pré-visualização em device context, saída JPEG, tudo. Se você trocá-la para pf1bit na origem, você não terá adicionado um recurso monocromático, terá degradado a fidelidade de cor para todo chamador da biblioteca e assumido o trabalho de depurar uma dúzia de regressões em cascata

Então RenderPageToMonochromeFile faz o oposto. Ele renderiza a página normalmente, para um BMP temporário de 24 bits, e só depois o reduz para 1 bit como etapa de pós-processamento. O renderizador não é tocado. O comportamento monocromático vive inteiramente no método de conveniência, o que significa que ele não pode afetar quem não o chama. Esse é o tipo de troca que vale nomear explicitamente: o pós-processamento paga uma alocação extra de bitmap e um arquivo temporário, e em troca mantém um núcleo crítico completamente fora de escopo. Para um recurso que existe para atender fax e casos de arquivo, esse é o lado certo da conta

Pipeline do PDF Library for Delphi mostrando uma página de PDF renderizada em um BMP temporário de 24 bits, colapsada por um blit GDI HALFTONE em um bitmap monocromático de 1 bit, e consumida por fluxos de trabalho de fax, arquivamento e OCR
Os novos métodos renderizam primeiro uma página em cores plenas e depois convertem para baixo o raster pronto fora do renderizador. Gateways de fax, repositórios de arquivamento e front-ends de OCR recebem um bitmap pf1bit verdadeiro
var
  Pdf: TPDFlib;
begin
  Pdf := TPDFlib.Create;
  try
    Pdf.LoadFromFile('invoice.pdf');
    // 200 DPI é a resolução clássica de fax Group 4; o índice de página é baseado em 1
    Pdf.RenderPageToMonochromeFile(200, 1, 'invoice-page1.bmp');
  finally
    Pdf.Free;
  end;
end;

Como a conversão para 1 bit funciona na prática

A redução usa GDI em vez de um loop de limiar escrito na mão, e essa escolha importa para a qualidade de saída. Dentro do método, o bitmap temporário de 24 bits é carregado em um TBitmap, um segundo TBitmap é criado com PixelFormat := pf1bit nas mesmas dimensões e os pixels passam com um único blit:

PDF Library for Delphi: detalhe do colapso GDI comparando um stretch blit HALFTONE que difunde cinzas em padrões de pontos contra o limiar padrão BLACKONWHITE blocado
Dentro de RenderPageToMonochromeFile, um único StretchBlt move cada pixel para uma superfície pf1bit de tamanho idêntico. Com HALFTONE ativo, os tons de cinza viram padrões pontilhados de tinta em vez das formas em blocos que o limite padrão produz
// dentro de RenderPageToMonochromeFile, após carregar o ColorBmp de 24 bits
MonoBmp.PixelFormat := pf1bit;
MonoBmp.Width  := ColorBmp.Width;
MonoBmp.Height := ColorBmp.Height;
// HALFTONE diz ao GDI para aplicar dithering na origem de 24 bits reduzindo para 1 bit
SetStretchBltMode(MonoBmp.Canvas.Handle, HALFTONE);
StretchBlt(MonoBmp.Canvas.Handle, 0, 0, MonoBmp.Width, MonoBmp.Height,
  ColorBmp.Canvas.Handle, 0, 0, ColorBmp.Width, ColorBmp.Height, SRCCOPY);
MonoBmp.SaveToFile('out.bmp');

O truque é SetStretchBltMode com HALFTONE. Mesmo quando origem e destino têm o mesmo tamanho, então não há escala, o modo de stretch ainda governa como o GDI mapeia as cores para a paleta de 1 bit. HALFTONE faz o sistema aplicar dithering em meia-tinta, transformando regiões cinzas e bordas de texto suavizadas em padrões de pontos pretos e brancos, em vez de um corte rígido para a cor mais próxima. Remova a chamada de modo, ou use o padrão BLACKONWHITE, e o conteúdo em tons de cinza vira posterização em formas quadradas e quantizadas. Para saída de documentos escaneados e de pré-processamento de OCR, o resultado com dithering é quase sempre o que você quer

Há um detalhe inegociável e fácil de errar: a renderização temporária precisa ser BMP. RenderPageToMonochromeFile chama o renderizador geral com um código de opção 0, que é BMP. O argumento de opções em RenderPageToFile é um pequeno enum inteiro, e os valores não são intercambiáveis para essa finalidade: 0 é BMP, 1 JPEG, 2 WMF, 3 EMF, 5 PNG e assim por diante. O conversor então faz TBitmap.LoadFromStream no arquivo temporário. Se você alimentar um WMF, passando 2, esse carregamento lança "Bitmap image is not valid", porque um Windows Metafile é um fluxo de registros vetoriais, não um DIB. A redução monocromática é uma operação raster do início ao fim, então o intermediário precisa ser um formato raster

Renderizar só uma sub-região da página

O segundo método, RenderPageRegionToFile, renderiza apenas um retângulo da página em vez da página inteira. Os casos de uso ficam familiares assim que você já construiu qualquer visualizador de documentos: recortar um bloco de assinatura de um contrato, gerar um bloco para um mapa ampliado de um desenho grande ou extrair uma região carimbada para uma miniatura sem pagar para rasterizar a página inteira em DPI alto. A assinatura é simples:

PDF Library for Delphi: renderização de recorte de região em pontos PDF: um retângulo 72,72,180,72 em uma página PDF renderizada em resolução total se torna um bitmap de 375 por 150 pixels a 150 DPI porque o recorte corta em vez de escalar
RenderPageRegionToFile recorta de uma renderização em resolução plena uma janela medida em pontos PDF. O bitmap de saída é dimensionado por largura e altura vezes DPI dividido por 72, nunca por uma página inteira encolhida
// Clip é "Left,Top,Width,Height" em pontos PDF (72 pt = 1 polegada)
// Aqui: uma caixa de 2,5 x 1 polegadas, uma polegada a partir do canto superior esquerdo da página
Pdf.RenderPageRegionToFile(150, 1, '72,72,180,72', 'sig-block.bmp');

A string de recorte é composta por quatro doubles separados por vírgula em pontos PDF, analisados manualmente dentro do método para contornar particularidades de localidade e de DelimitedText. A partir da largura e da altura, o método calcula o tamanho do bitmap de saída como Round(Width * DPI / 72) por Round(Height * DPI / 72), aloca um bitmap em memória pf24bit exatamente desse tamanho e renderiza no seu device context por meio de RenderPageToDCClip. O arquivo resultante contém apenas o retângulo recortado, dimensionado para a região e não para a página inteira

O parâmetro de recorte que não fazia nada

É aqui que o trabalho era mais afiado do que parece. RenderPageToDCClip carregava há muito tempo um parâmetro Clip, e ele era mentira. A chamada aceitava o argumento, repassava para TPDFPageTree.RenderPageToDC e essa implementação o ignorava por completo, sem jamais entregá-lo ao renderizador. Você podia passar qualquer retângulo e recebia a página inteira de volta. Quem tinha ligado RenderPageToDCClip esperando um recorte recebia uma renderização da página completa e, dependendo do layout, talvez nem percebesse

A v3.83.0 conectou o fio. RenderPageToDC agora analisa o mesmo retângulo de pontos "Left,Top,Width,Height" e o aplica como uma região de recorte real do GDI no device context de destino antes de desenhar. A conversão de pontos para pixels de dispositivo usa o fator de escala usual DPI / 72, aplicado a todas as quatro bordas. A sequência em torno da renderização é a dança padrão de salvar, recortar e restaurar:

// dentro de TPDFPageTree.RenderPageToDC, quando Clip não está vazio
ScaleFactor := DPI / 72;
SaveDC(TargetDC);
IntersectClipRect(TargetDC,
  Round(ClipLeft * ScaleFactor),
  Round(ClipTop * ScaleFactor),
  Round((ClipLeft + ClipWidth) * ScaleFactor),
  Round((ClipTop + ClipHeight) * ScaleFactor));
// ... o renderizador desenha a página aqui ...
// no bloco finally:
RestoreDC(TargetDC, -1);

O par SaveDC / RestoreDC(-1) é o que torna isso seguro para chamar repetidamente: a região de recorte é empurrada para a pilha de estado do DC, a página é desenhada e o recorte original volta, independentemente de como a renderização termina. RestoreDC(TargetDC, -1) restaura o estado salvo mais recentemente, que é o idiomático padrão de salvar e restaurar em equilíbrio. Pule a restauração e um chamador que reutilize o mesmo DC para uma renderização de página inteira subsequente encontraria o desenho misteriosamente recortado para a última região. Consertar o parâmetro morto também consertou RenderPageRegionToFile de graça, já que esse novo método passa exatamente por esse caminho

Há um ponto comportamental para internalizar: o recorte corta, não escala. A página ainda é rasterizada no DPI que você pediu, na posição normal, e a região de recorte simplesmente descarta tudo que fica fora do retângulo. Você não está ampliando a região para preencher a saída, está cortando uma janela da renderização em resolução total. Se quiser ampliar uma região, aumente o DPI. As coordenadas do retângulo são interpretadas no espaço do dispositivo depois da escala de pontos para pixels, medidas a partir do canto superior esquerdo da superfície renderizada, então planeje Left e Top a partir do topo da página. Para uma visão mais profunda de como o PDF Library for Delphi dirige um device context para saída na tela, o texto companheiro sobre pré-visualização de impressão e saída em device context percorre a mesma infraestrutura de DC pelo lado da exibição

O limite que vale declarar: BMP de 1 bit, não TIFF G4

Seria fácil vender isso como "saída pronta para fax", então aqui está o limite dito de forma clara. RenderPageToMonochromeFile produz um BMP pf1bit. Ele não produz um TIFF CCITT Group 4, que é o formato que um fluxo real de fax ou um arquivo TIFF normalmente espera. O motivo é concreto, não uma omissão: a unit CCITT do PDF Library for Delphi hoje decodifica fluxos G4, mas não tem um encoder G4. Sem encoder, não há lugar para escrever sequências monocromáticas compactadas, então o caminho monocromático para em um DIB 1-bit sem compressão

Na prática isso ainda é útil. Um BMP de 1 bit é o formato correto de pixel, já ditherizado e pronto, e a maioria dos fluxos de fax, arquivo ou OCR o consome felizmente ou o converte para G4 em uma etapa posterior. Mas, se sua exigência for literalmente um TIFF Group 4 saindo direto da biblioteca, isso ainda não é o caso, e você deve planejar sua própria etapa de compressão. Saber onde um recurso para vale tanto quanto saber o que ele faz

Os dois métodos são deliberadamente pequenos, e essa é a lição de projeto que vale carregar desta página: uma API de conveniência que fica por cima de um renderizador pode adicionar capacidade real, saída monocromática e recorte de região, sem mexer no rasterizador e desestabilizar todo o resto. Quando você realmente precisar escolher entre motores de renderização para a rasterização subjacente, o panorama de renderização PDF com múltiplos motores no Delphi cobre as trocas em profundidade. Para ver a superfície completa de renderização e o restante da API, a página de produto do PDF Library for Delphi Delphi PDF Library traz o quadro completo