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

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;  // 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 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:

// 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. 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