As coordenadas do PDF estão em pontos, as coordenadas da impressora estão em unidades de dispositivo, e as duas não têm qualquer relação entre si até que as converta deliberadamente. Essa incompatibilidade é a origem da maioria dos problemas de impressão em aplicações Delphi: o código envia o ficheiro correto, mas a página sai cortada, esticada ou em branco. O PDFium VCL lida com a parte da renderização de forma limpa; a estrutura da impressora é o VCL padrão. Ambos integram-se com uma quantidade modesta de código quando compreende o que cada lado espera.
Como funciona o fluxo de renderização e impressão
O PDFium VCL não comunica diretamente com as impressoras. O padrão é: renderizar uma página num TBitmap na resolução pretendida e, em seguida, transferir esse bitmap para o canvas da impressora com o StretchDIBits. O TPdf.RenderPage devolve um bitmap pertencente ao chamador, pelo que controla as dimensões dos píxeis. Ao passar [rePrinting] no conjunto de opções, o PDFium altera o seu caminho de renderização para um que omite efeitos exclusivos do ecrã, tais como o hinting de subpíxeis LCD, e trata corretamente a MediaBox da página para a saída de impressão. Se omitir o rePrinting, o que envia para a impressora é uma renderização de ecrã, que parece adequada num monitor, mas tende a produzir resultados menos nítidos em impressoras de alta resolução (DPI) porque as decisões de hinting tomadas para ecrãs de 96 DPI não se adequam a impressões de 300 ou 600 DPI.
O TPdf.Active é o único controlo a verificar antes de aceder a qualquer propriedade de página. O componente ignora silenciosamente erros de carregamento: definir Active := True num ficheiro danificado ou protegido por palavra-passe não gera uma exceção; simplesmente deixa o Active como False. Verifique sempre esta propriedade após a atribuição. Ler PageCount ou PageWidth num documento inativo devolve zero, o que produz operações nulas (no-ops) silenciosas que são muito difíceis de diagnosticar quando chegam ao spooler.
Um ciclo de impressão mínimo
O caso de utilização mais simples carrega um ficheiro, inicia uma tarefa de impressão, percorre as páginas e fecha o documento. O único detalhe mais complexo é que o Printer.NewPage não deve ser chamado antes da primeira página, daí a utilização da flag FirstPage. A transferência via StretchDIBits recorre a GetDIBSizes e GetDIB para extrair os bits independentes do dispositivo a partir do handle do bitmap, pintando-os depois no canvas da impressora no tamanho total da página:
procedure PrintPdfFile(const FileName: string);
var
Pdf: TPdf;
I: Integer;
Bitmap: TBitmap;
InfoHeaderSize, ImageSize: DWORD;
InfoHeader: PBitmapInfo;
Image: Pointer;
FirstPage: Boolean;
begin
Pdf := TPdf.Create(nil);
try
Pdf.FileName := FileName;
Pdf.Active := True;
if not Pdf.Active then
Exit; // load failed silently; bail out
Printer.Title := Pdf.Title;
Printer.BeginDoc;
try
FirstPage := True;
for I := 1 to Pdf.PageCount do
begin
if FirstPage then
FirstPage := False
else
Printer.NewPage;
Pdf.PageNumber := I;
// Render at printer resolution; rePrinting adjusts the render path
Bitmap := Pdf.RenderPage(
0, 0,
Printer.PageWidth,
Printer.PageHeight,
ro0,
[rePrinting]
);
try
GetDIBSizes(Bitmap.Handle, InfoHeaderSize, ImageSize);
InfoHeader := AllocMem(InfoHeaderSize);
try
Image := AllocMem(ImageSize);
try
GetDIB(Bitmap.Handle, 0, InfoHeader^, Image^);
StretchDIBits(
Printer.Canvas.Handle,
0, 0, Printer.PageWidth, Printer.PageHeight,
0, 0, Bitmap.Width, Bitmap.Height,
Image, InfoHeader^, DIB_RGB_COLORS, SRCCOPY
);
finally
FreeMem(Image);
end;
finally
FreeMem(InfoHeader);
end;
finally
Bitmap.Free;
end;
end;
finally
Printer.EndDoc;
end;
finally
Pdf.Active := False;
Pdf.Free;
end;
end;
Passar Printer.PageWidth e Printer.PageHeight como as dimensões do bitmap significa que renderiza no tamanho nativo de píxeis da impressora, o qual já tem em conta os DPI do dispositivo. A chamada a StretchDIBits mapeia então esses píxeis numa relação de 1:1 na página. Isto proporciona a melhor fidelidade possível sem qualquer aritmética explícita de DPI, mas apenas funciona quando a página do PDF e o papel físico têm o mesmo tamanho. Quando diferem, necessita de efetuar o redimensionamento (scaling) de forma explícita.
Redimensionamento quando o tamanho da página e do papel diferem
Uma página PDF em formato vertical (portrait) A4 não se ajusta automaticamente a uma impressora configurada para US Letter, e uma página horizontal (landscape) enviada para uma impressora orientada ao alto será cortada. A abordagem padrão consiste em calcular um fator de escala uniforme a partir da proporção de píxeis da impressora em relação aos pontos do PDF, aplicando-o de seguida a ambas as dimensões para que a proporção (aspect ratio) seja preservada. Pdf.PageWidth e Pdf.PageHeight expõem as dimensões atuais da página em pontos, onde um ponto equivale a 1/72 de polegada. Multiplicar por uma resolução DPI pretendida e dividir por 72 realiza a conversão para píxeis nessa resolução. Obtenha o Min das proporções de X e Y para obter a maior escala que ainda caiba na área imprimível:
// Fit PDF page to printable area, preserving aspect ratio
var
ScaleX, ScaleY, Scale: Double;
DestWidth, DestHeight: Integer;
Dpi: Integer;
begin
Dpi := 300; // target render resolution
Pdf.PageNumber := PageIndex;
ScaleX := Printer.PageWidth / (Pdf.PageWidth * Dpi / 72);
ScaleY := Printer.PageHeight / (Pdf.PageHeight * Dpi / 72);
Scale := Min(ScaleX, ScaleY);
// Clamp to 1.0 for shrink-to-fit only (no enlargement)
if Scale > 1.0 then Scale := 1.0;
DestWidth := Round(Pdf.PageWidth * Dpi / 72 * Scale);
DestHeight := Round(Pdf.PageHeight * Dpi / 72 * Scale);
Bitmap := Pdf.RenderPage(0, 0, DestWidth, DestHeight, ro0,
[rePrinting, reAnnotations]);
// ... transfer with StretchDIBits as above
end;
Renderizar a Dpi = 300 é adequado para a maioria das impressoras de escritório. A 600 DPI, o bitmap para uma única página A4 ascende a aproximadamente 34 megapíxeis, o que representa cerca de 100 MB num bitmap de 32 bits; o ganho de qualidade para documentos de texto comuns é mínimo e o custo de memória por página é significativo. Reserve os 600 DPI para tipografias ou desenhos técnicos complexos em formato vetorial, onde esta resolução realmente faça a diferença.
A flag reAnnotations no segundo bloco de código é independente de rePrinting. Inclua-a quando o utilizador pretender que carimbos, destaques e caixas de comentários apareçam no papel. Omita-a se desejar apenas a saída do conteúdo principal. Ambas as flags podem ser combinadas livremente.
Rotação da página
O PDFium armazena a rotação da página no PDF como uma entrada /Rotate, acessível através de Pdf.PageRotation, que devolve um valor de TRotation (ro0, ro90, ro180, ro270). O sistema de coordenadas da impressora inverte as rotações de 90 e 270 graus relativamente ao ecrã. Se passar o valor bruto de PageRotation diretamente para RenderPage sem qualquer ajuste, as páginas horizontais integradas num documento vertical serão impressas invertidas na maioria dos controladores de impressora do Windows. A solução é uma simples troca antes da chamada de renderização: mapear ro90 para ro270 e ro270 de volta para ro90, mantendo ro0 e ro180 inalterados.
Verifique este comportamento na sua impressora de destino específica antes de distribuir a aplicação. O comportamento do controlador em relação à rotação não é uniforme entre fabricantes, e alguns controladores aplicam a sua própria correção de rotação ao nível do GDI. Se observar uma rotação dupla, remova a troca; se não notar qualquer correção, adicione-a. Um documento de orientação mista, que alterne entre páginas verticais e horizontais, é a forma mais rápida de detetar qualquer um destes problemas durante os testes.
Gestão de memória em tarefas de impressão longas
Cada chamada a RenderPage aloca um novo TBitmap que pertence ao chamador e que deve ser libertado. No ciclo apresentado acima, o bloco try/finally Bitmap.Free trata disto corretamente, uma página de cada vez. Não acumule bitmaps entre páginas: a renderização a 300 DPI de um documento com 200 páginas consumiria gigabytes de memória antes de a primeira página chegar ao spooler. Liberte cada bitmap antes de avançar para a página seguinte.
O par AllocMem / FreeMem dentro do bloco de transferência segue a mesma regra. O GetDIBSizes indica a quantidade de memória necessária para o cabeçalho DIB e para os dados de píxeis; aloca, preenche, pinta e liberta, tudo no âmbito de uma única página. Permitir fugas de memória (leaks) em qualquer um destes blocos fará com que a tarefa de impressão esgote o heap do processo em documentos com mais do que algumas dezenas de páginas.
Se necessitar de executar tarefas de impressão numa thread secundária, mantenha o TPdf e todas as chamadas de impressora VCL na mesma thread. O próprio TPdf não é seguro para threads (thread-safe) entre instâncias que partilham o estado global da DLL do PDFium; o modelo mais seguro consiste em ter um TPdf por thread, carregando cada um a sua própria cópia do ficheiro.
A API de renderização e de documentos apresentada aqui faz parte do PDFium VCL Component para Delphi e C++Builder.