Artigo Técnico

Imprimir Documentos PDF com PDFium Component em Delphi

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 Component 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 Component 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

Pipeline de renderizar-para-depois-imprimir em Delphi com PDFium Component: a página PDF carregada renderiza através de TPdf.RenderPage com o sinalizador rePrinting para um TBitmap do tamanho da impressora; depois, o StretchDIBits transfere-a para o canvas da impressora, em comparação com o output suave renderizado para o ecrã quando rePrinting é omitido
O PDFium Component nunca fala diretamente com o spooler. RenderPage produz o bitmap, StretchDIBits move-o para o canvas da impressora, e rePrinting escolhe o caminho de renderização destinado ao papel

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;  // o carregamento falhou em silêncio; sai

    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;

        // Renderiza à resolução da impressora; rePrinting ajusta o caminho de render
        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:

Dimensionamento de ajuste à página para impressão PDF em Delphi: uma página PDF de 595 por 842 pontos fica dimensionada dentro da área imprimível em píxeis da impressora, convertida em pontos vezes Dpi sobre 72, dimensionada pelo menor dos rácios de largura e altura, limitada ao ajuste por redução e dimensionada no bitmap de destino
Os pontos tornam-se píxeis a pontos vezes Dpi sobre 72, e o rácio menor vence, para que nada se corte. Limitar a 1.0 mantém o ajuste apenas encolher-para-caber
// Ajusta a página PDF à área imprimível, mantendo a proporção
var
  ScaleX, ScaleY, Scale: Double;
  DestWidth, DestHeight: Integer;
  Dpi: Integer;
begin
  Dpi := 300;  // resolução de render alvo
  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]);
  // ... transferir com StretchDIBits como acima
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 Component para Delphi e C++Builder