Artigo Técnico

Imprimindo Documentos PDF com o Componente PDFium no 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 nada a ver uma com a outra até que você as converta deliberadamente. Essa incompatibilidade é a raiz da maioria das saídas de impressão incorretas em aplicativos Delphi: o código envia o arquivo certo, mas a página sai cortada, esticada ou em branco. O Componente PDFium lida com o lado da renderização de forma limpa; a infraestrutura da impressora é VCL padrão. Os dois se encaixam com uma quantidade modesta de código, uma vez que você entenda o que cada lado espera

Como o pipeline renderizar-então-imprimir funciona

O Componente PDFium não se comunica diretamente com as impressoras. O padrão é: renderizar uma página em um TBitmap na resolução que você deseja, depois transferir esse bitmap para o canvas da impressora usando StretchDIBits. O TPdf.RenderPage retorna um bitmap de propriedade do chamador, de modo que você controla as dimensões em pixels. Passe [rePrinting] no conjunto de opções e o PDFium mudará seu caminho de renderização para um que omite efeitos apenas de tela, como hinting de subpixel LCD, e lida corretamente com a MediaBox da página para a saída de impressão. Deixe rePrinting de fora e o que você enviará para a impressora será uma renderização de tela, que parece ótima em um monitor, mas tende a produzir uma saída mais suave em impressoras de alto DPI porque as decisões de hinting tomadas para telas de 96 DPI não são adequadas para impressão a 300 ou 600 DPI

O TPdf.Active é o único portão a verificar antes de tocar em qualquer propriedade da página. O componente engole erros de carregamento silenciosamente: definir Active := True em um arquivo danificado ou protegido por senha não levanta uma exceção; simplesmente deixa o Active como False. Sempre verifique isso após a atribuição. Ler PageCount ou PageWidth em um documento inativo retorna zero, o que produz operações silenciosas sem efeito (no-ops) que são muito difíceis de diagnosticar assim que chegam ao spooler

Pipeline de renderizar-depois-imprimir em Delphi com PDFium Component: a página de PDF carregada renderiza por TPdf.RenderPage com o flag rePrinting em um TBitmap do tamanho da impressora, então StretchDIBits a transfere ao canvas da impressora, comparado com a saída soft renderizada para tela quando rePrinting é omitido
O PDFium Component nunca fala diretamente com o spooler. RenderPage produz o bitmap, StretchDIBits o move para o canvas da impressora, e rePrinting escolhe o caminho de renderização destinado ao papel

Um loop de impressão mínimo

O caso prático mais simples carrega um arquivo, abre um trabalho de impressão, itera pelas páginas e fecha. O único detalhe complicado é que Printer.NewPage não deve ser chamado antes da primeira página, daí o sinalizador FirstPage. A transferência via StretchDIBits passa por GetDIBSizes e GetDIB para extrair bits independentes de dispositivo do manipulador do bitmap e, em seguida, os pinta 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 load falhou em silêncio; caia fora

    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 na 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 você renderiza no tamanho nativo de pixels da impressora, o que já considera o DPI do dispositivo. A chamada a StretchDIBits então mapeia esses pixels em 1:1 na página. Isso lhe dá a melhor fidelidade possível sem qualquer aritmética explícita de DPI, mas funciona apenas quando a página do PDF e o papel físico coincidentemente tiverem o mesmo tamanho. Quando eles divergem, você precisa de dimensionamento explícito

Dimensionamento quando o tamanho da página e do papel divergem

Uma página PDF no formato A4 retrato não se ajusta automaticamente a uma impressora configurada para Carta (US Letter), e uma página paisagem enviada a uma impressora orientada como retrato será cortada. A abordagem padrão é calcular um fator de escala uniforme a partir da proporção de pixels da impressora para pontos do PDF e, em seguida, aplicá-lo a ambas as dimensões para que a proporção da imagem (aspect ratio) seja preservada. O Pdf.PageWidth e Pdf.PageHeight expõem as dimensões atuais da página em pontos, onde um ponto equivale a 1/72 polegada. Multiplicar por um DPI de destino e dividir por 72 converte para pixels nessa resolução. Use Min nas proporções X e Y para obter a maior escala que ainda caiba dentro da área de impressão:

Escalonamento fit-to-page para impressão de PDF em Delphi: uma página de PDF de 595 por 842 pontos assenta escalada dentro da área imprimível de pixels da impressora, convertida em pontos vezes Dpi sobre 72, escalada pelo menor das razões de largura e altura, limitada ao shrink-to-fit, e dimensionada no bitmap de destino
Pontos viram pixels em pontos vezes Dpi dividido por 72, e a menor razão vence para que nada seja cortado. Limitar a 1,0 mantém o ajuste apenas como redução para caber
// Encaixa a página PDF na área imprimível, preservando 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]);
  // ... transfere com StretchDIBits como acima
end;

Renderizar a Dpi = 300 é adequado para a maioria das impressoras de escritório. Em 600 DPI, o bitmap para uma única página A4 chega a cerca de 34 megapixels, o que dá cerca de 100 MB como um bitmap de 32 bits; o ganho na qualidade de documentos de texto comuns é mínimo e o custo de memória por página é significativo. Mantenha os 600 DPI para gráficas ou desenhos técnicos ricos em vetores, onde isso genuinamente importa

O sinalizador reAnnotations no segundo bloco de código é independente de rePrinting. Inclua-o quando o usuário esperar que carimbos, destaques e caixas de comentários apareçam no papel. Omita-o para saída apenas de conteúdo. Ambos os sinalizadores podem ser combinados livremente

Rotação de página

O PDFium armazena a rotação de página no PDF como uma entrada /Rotate, acessível por meio de Pdf.PageRotation, a qual retorna um valor TRotation (ro0, ro90, ro180, ro270). O sistema de coordenadas da impressora inverte rotações de 90 e 270 graus em relação à tela. Se você passar o valor bruto de PageRotation diretamente para o RenderPage sem qualquer ajuste, as páginas em paisagem embutidas em um documento retrato serão impressas de cabeça para baixo na maioria dos drivers de impressora do Windows. A correção é uma troca simples antes da chamada de renderização: mapeie ro90 para ro270 e ro270 de volta para ro90, deixando ro0 e ro180 inalterados

Verifique esse comportamento na sua impressora de destino específica antes do envio (shipping). O comportamento do driver com a rotação não é uniforme entre os fornecedores, e alguns drivers aplicam sua própria correção de rotação no nível da GDI. Se você vir uma dupla rotação, remova a troca; se não houver correção alguma, adicione-a. Um documento de orientação mista alternando páginas de retrato e paisagem é a maneira mais rápida de pegar qualquer um desses modos de falha durante os testes

Gerenciamento de memória em um longo trabalho de impressão

Cada chamada a RenderPage aloca um novo TBitmap do qual o chamador é proprietário e deve liberar. No loop acima, o bloco try/finally Bitmap.Free lida com isso corretamente para uma página de cada vez. Não acumule bitmaps ao longo das páginas: a renderização a 300 DPI de um documento de 200 páginas consumiria gigabytes antes de a primeira página chegar ao spooler. Libere cada bitmap antes de avançar para a próxima página

O par AllocMem / FreeMem dentro do bloco de transferência segue a mesma regra. GetDIBSizes lhe diz de quanta memória o cabeçalho DIB e os dados de pixel precisam; você aloca, preenche, pinta e libera tudo dentro do escopo de uma página. Deixar qualquer um dos blocos vazar fará com que o trabalho de impressão esgote a pilha do processo em documentos maiores do que algumas dúzias de páginas

Se você precisar executar trabalhos de impressão em uma thread de plano de fundo, mantenha TPdf e todas as chamadas de impressora VCL na mesma thread. O TPdf por si só não é seguro para threads (thread-safe) entre instâncias que compartilham o estado global da DLL do PDFium; o modelo mais seguro é um TPdf por thread, com cada um carregando sua própria cópia do arquivo

A API de renderização e documento mostrada aqui faz parte do Componente PDFium para Delphi e C++Builder