Technical Article

Imprimir Documentos PDF com PDFium VCL 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 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.