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 PDFlibPas acaba dizendo algo útil sobre como estender um renderizador que você prefere não reescrever
PDFlibPas é 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
var
Pdf: TPDFlib;
begin
Pdf := TPDFlib.Create(nil);
try
Pdf.LoadFromFile('invoice.pdf');
// 200 DPI is the classic Group 4 fax resolution; page index is 1-based
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:
// inside RenderPageToMonochromeFile, after loading the 24-bit ColorBmp
MonoBmp.PixelFormat := pf1bit;
MonoBmp.Width := ColorBmp.Width;
MonoBmp.Height := ColorBmp.Height;
// HALFTONE tells GDI to dither the 24-bit source down to 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:
// Clip is "Left,Top,Width,Height" in PDF points (72 pt = 1 inch)
// Here: a 2.5in x 1in box, one inch in from the top-left of the page
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:
// inside TPDFPageTree.RenderPageToDC, when Clip is non-empty
ScaleFactor := DPI / 72;
SaveDC(TargetDC);
IntersectClipRect(TargetDC,
Round(ClipLeft * ScaleFactor),
Round(ClipTop * ScaleFactor),
Round((ClipLeft + ClipWidth) * ScaleFactor),
Round((ClipTop + ClipHeight) * ScaleFactor));
// ... renderer draws the page here ...
// in the finally block:
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 PDFlibPas 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 PDFlibPas 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 PDFlibPas Delphi PDF Library traz o quadro completo