Artigo Técnico

Imprimir valores de formulário PDF sem distorcer a página

Um usuário preenche um formulário PDF num viewer Delphi, aperta Print, e a folha sai com todo field vazio e a página Letter esticada para preencher A4. Os dois defeitos moram no loop de impressão: o PDFium Component pinta os valores de field apenas em um bitmap (ou os assa em uma cópia flattened), e cada página quer um único fator de escala que preserve a proporção

A camada de formulário é uma segunda passada que precisa de bitmap

No Windows, o overload de device context TPdf.RenderPage(DC, Left, Top, Width, Height, Rotation, Options) entrega o HDC da impressora direto ao FPDF_RenderPage. Essa chamada pinta o conteúdo da página, mais annotations de markup quando reAnnotations está definido. A camada de formulário interativo jamais chega ao DC — ela pertence ao form-fill environment do PDFium e é desenhada pelo FPDF_FFLDraw, cujo único alvo é um FPDF_BITMAP. Não existe variante HDC. Os dois overloads de bitmap do TPdf.RenderPage rodam FPDF_RenderPageBitmap e FPDF_FFLDraw sobre os mesmos pixels sempre que o documento foi aberto com FormFill em True, o default. Com rePrinting nas opções, o PDFium Component remove o realce de field antes da passada de formulário e restaura a cor e o alpha de realce default depois, então as caixas tingidas em volta de fields editáveis ficam na tela. O rePrinting também mapeia para a flag FPDF_PRINTING do motor, que marca a saída como impressão — o caso que a ISO 32000-1 §12.5.3 (Tabela 165) endereça com o bit 3 da flag de annotation: um reader conforme deixa fora do papel uma annotation cujo bit Print está limpo, não importa como ela apareça na tela

A plataforma decide o resto. No motor PDFium, o FPDF_RenderPage desenha pela GDI do Windows dentro de um device context e só existe em builds Windows, enquanto o FPDF_FFLDraw desenha em um bitmap e nada mais, em toda plataforma. No Linux e nos demais alvos não Windows não há chamada GDI a fazer, então o overload de HDC é servido renderizando um bitmap — camada de formulário incluída — e desenhando-o na canvas atrás do handle. A chamada de device context portanto carrega valores de field fora do Windows e os deixa de fora no Windows. Mantenha o branch explícito em código compartilhado entre Delphi e Lazarus: quando o documento tem um form environment e os valores dele pertencem ao papel, peça um bitmap em toda plataforma

Imprimindo form fields no Delphi com PDFium Component: o TPdf.RenderPage no HDC da impressora chama FPDF_RenderPage sem a camada de formulário, enquanto o overload TBitmap acrescenta uma passada FPDF_FFLDraw antes do StretchDIBits
no Windows só o overload de bitmap do RenderPage roda a passada de formulário FPDF_FFLDraw, e é por isso que os valores digitados nos fields chegam ao papel por essa rota e somem na de device context

Que caminho um job de impressão deve tomar?

Escolha pelo que o papel precisa carregar. Valores de formulário vivos apontam para o caminho de bitmap, saída vetorial para o caminho de HDC, e uma cópia flattened entrega os dois pelo preço de um save e um reload. O SetPdfPrintMode vale para a biblioteca inteira, então mude-o entre jobs, nunca entre páginas

RotaValores de formulário no papelSaídaCusto
RenderPage(DC, …)Não, no WindowsGDI vetorial; o SetPdfPrintMode escolhe EMF (o default) ou um nível PostScriptSpool menor, o mais rápido
RenderPage para um TBitmap, depois StretchDIBitsSim, incluindo edições não salvasRaster no tamanho de pixel que você pedirLargura × Altura × 4 bytes por página, mais a cópia DIB
RenderPageBandedSimRaster, uma faixa por vezUma banda em memória em vez da folha inteira
Cópia flattened, depois RenderPage(DC, …)Sim, como conteúdo de página assadoVetorialSave, reload, GenerateFormAppearances, FlattenAllPages(FLAT_PRINT)

Uma folha A4 inteira a 600 dpi são cerca de 4960 × 7016 pixels de dispositivo, e a 32 bits por pixel isso dá 139.197.440 bytes para uma página; copiá-la para uma DIB para o StretchDIBits dobra o pico, carga pesada para um processo de 32 bits. Para o botão Print de um viewer o caminho de bitmap ainda é o default certo: um usuário que digita num field e aperta Print espera ver aquele valor no papel, e o banding mantém a memória limitada. Documentos que saem da aplicação, como extratos arquivados, são melhor servidos pela cópia flattened — os valores viram conteúdo de página comum e a impressora recebe saída vetorial

uses
  System.SysUtils, System.IOUtils, PDFium, FPdfUtils;  // FPdfUtils: FLAT_PRINT

function OpenFlattenedPrintCopy(Source: TPdf; out TempName: string): TPdf;
begin
  TempName := TPath.Combine(TPath.GetTempPath, TPath.GetRandomFileName + '.pdf');
  if not Source.SaveAs(TempName) then
    raise Exception.Create('Could not write the print copy');
  Result := TPdf.Create(nil);
  try
    Result.FileName := TempName;   // FormFill segue True (o default)
    Result.Password := Source.Password;  // a cópia mantém a criptografia dela
    Result.Active := True;
    if not Result.Active then      // um load falho deixa o TPdf inativo
      raise Exception.Create('Could not reopen the print copy');
    Result.GenerateFormAppearances;          // reconstrói /AP a partir do /V corrente
    if not Result.FlattenAllPages(FLAT_PRINT) then
      raise Exception.Create('Flattening failed');
  except
    Result.Free;
    TFile.Delete(TempName);
    raise;
  end;
end;

A rota flatten tem uma regra de ordem que importa mais que a flag. O FlattenAllPages assa o appearance stream de cada widget (ISO 32000-1 §12.5.5) no conteúdo da página, e um appearance stream é o que o último produtor gravou; o valor do field no /V não participa. Um formulário preenchido por uma ferramenta que nunca regenerou as appearances dela assa para o texto antigo ou para nada, então o GenerateFormAppearances roda primeiro: ele reconstrói as appearances de text fields a partir dos valores correntes e sincroniza os estados /AS de checkbox e radio, e precisa que o FormFill estivesse True quando o documento foi aberto. Faça tudo isso numa cópia, porque o flattening reescreve as páginas de qualquer TPdf em que rodar, e um viewer aberto quer os fields dele editáveis. A cópia passa pelo branch vetorial da rotina de impressão abaixo com a camada de formulário desligada; libere-a e apague o arquivo temporário quando o job terminar

Cópia de impressão flattened no Delphi com PDFium Component: SaveAs para um arquivo temporário, reabrir com FormFill, GenerateFormAppearances, FlattenAllPages(FLAT_PRINT), depois impressão vetorial via RenderPage no device context
chamar o GenerateFormAppearances antes do FlattenAllPages faz os appearance streams assados baterem com os valores correntes dos fields em vez do que o último produtor deixou no /AP

Encaixando cada página na área imprimível

Escale uma vez, pelo menor dos dois fatores, e centralize o resultado. Esticar para Printer.PageWidth × Printer.PageHeight distorce toda página cuja forma difere do papel, uma página Letter (612 × 792 pontos) em A4 ou uma página paisagem numa folha retrato. O lado da impressora é a área imprimível em pixels de dispositivo (HORZRES por VERTRES), com as margens do driver já excluídas. Do lado do PDF, o TPdf.PageWidth e o PageHeight já reportam a página como exibida: uma página A4 retrato armazenada com /Rotate 90 volta como 842 × 595 pontos. A ISO 32000-1 §7.7.3.3 (Tabela 30) diz que a página shall ser rotacionada pelo /Rotate quando exibida ou impressa, o PDFium aplica isso dentro da chamada de render, e o argumento Rotation do RenderPage é só o giro extra que um viewer acrescenta quando o usuário rotaciona a view. Passe o PageRotation da própria página aí e a página gira duas vezes. Troque largura e altura no encaixe só quando esse giro extra é de 90 ou 270 graus, o mesmo par de regras que governa encaixe e zoom de thumbnail sob rotação; a rotina abaixo estende o loop básico de imprimir documentos PDF com PDFium Component com um branch de valor de formulário e um caminho de cancelamento que descarta o job

Encaixe de página do PDFium Component para impressão no Delphi: uma página Letter esticada para Printer.PageWidth e PageHeight versus uma escala que preserva a proporção centrada na área imprimível, com /Rotate já embutido no TPdf.PageWidth
escalar pelo menor dos dois fatores mantém toda página sem distorção e centrada, e largura e altura trocam de lugar só para um giro extra ro90 ou ro270 do viewer
uses
  Winapi.Windows, System.SysUtils, Vcl.Graphics, Vcl.Forms, Vcl.Printers, PDFium;

procedure FitRect(PageW, PageH: Double; Swap: Boolean;
  AreaW, AreaH: Integer; out X, Y, W, H: Integer);
var
  T, Scale: Double;
begin
  if Swap then
  begin
    T := PageW; PageW := PageH; PageH := T;
  end;
  if (PageW <= 0) or (PageH <= 0) then
  begin
    X := 0; Y := 0; W := AreaW; H := AreaH;
    Exit;
  end;
  Scale := AreaW / PageW;
  if AreaH / PageH < Scale then
    Scale := AreaH / PageH;
  W := Round(PageW * Scale);
  H := Round(PageH * Scale);
  X := (AreaW - W) div 2;
  Y := (AreaH - H) div 2;
end;

procedure BlitBitmap(DC: HDC; Bitmap: TBitmap; X, Y: Integer);
var
  InfoSize, ImageSize: DWORD;
  Info: PBitmapInfo;
  Bits: Pointer;
begin
  GetDIBSizes(Bitmap.Handle, InfoSize, ImageSize);
  Info := AllocMem(InfoSize);
  try
    Bits := AllocMem(ImageSize);
    try
      GetDIB(Bitmap.Handle, 0, Info^, Bits^);
      StretchDIBits(DC, X, Y, Bitmap.Width, Bitmap.Height, 0, 0,
        Bitmap.Width, Bitmap.Height, Bits, Info^, DIB_RGB_COLORS, SRCCOPY);
    finally
      FreeMem(Bits);
    end;
  finally
    FreeMem(Info);
  end;
end;

function PrintPages(Pdf: TPdf; FromPage, ToPage: Integer; FormLayer: Boolean;
  ExtraRotation: TRotation; var Cancel: Boolean): Boolean;
const
  Opts: TRenderOptions = [reAnnotations, rePrinting];
var
  Page, X, Y, W, H: Integer;
  Bitmap: TBitmap;
begin
  Result := False;
  Printer.BeginDoc;
  try
    for Page := FromPage to ToPage do
    begin
      if Page > FromPage then
        Printer.NewPage;
      Pdf.PageNumber := Page;
      FitRect(Pdf.PageWidth, Pdf.PageHeight, ExtraRotation in [ro90, ro270],
        Printer.PageWidth, Printer.PageHeight, X, Y, W, H);
      if FormLayer then
      begin
        Bitmap := Pdf.RenderPage(0, 0, W, H, ExtraRotation, Opts);
        if Bitmap <> nil then          // nil para um tamanho de página degenerado
        try
          BlitBitmap(Printer.Canvas.Handle, Bitmap, X, Y);
        finally
          Bitmap.Free;
        end;
      end
      else
        Pdf.RenderPage(Printer.Canvas.Handle, X, Y, W, H, ExtraRotation, Opts);
      Application.ProcessMessages;     // deixa um botão Cancel definir a flag
      if Cancel then
        Break;
    end;
    Result := not Cancel;
  finally
    if Result then
      Printer.EndDoc                   // commita o job no spooler
    else
      Printer.Abort;                   // cancelamento ou exceção: nada imprime
  end;
end;

Um viewer chama isto com o Pdf.FormFill como FormLayer; a cópia flattened passa False. O EndDoc é o commit, não um passo de limpeza. Depois do BeginDoc, o Printer.EndDoc entrega tudo que foi renderizado até então ao spooler como um job válido, então um EndDoc incondicional no finally transforma uma impressão cancelada em uma meio impressa. A rotina decide no finally pela flag de sucesso em vez disso: EndDoc depois da última página, Abort depois de um cancelamento ou de uma exceção. A saída do loop precisa de cuidado parecido, porque Delphi e Free Pascal ambos avaliam o valor final de um loop for uma vez, na entrada. Definir ToPage ou uma contagem de cópias como zero a partir de um handler de Cancel não para um loop que já está rodando; teste a flag e dê Break, e com loops aninhados de cópia e collation, saia de cada nível. E como o Application.ProcessMessages deixa shortcuts de menu dispararem também, desabilite Open, Save e Print enquanto o job roda

Mantendo o caminho raster dentro da memória

O TPdf.RenderPageBanded renderiza uma página em faixas horizontais de uma altura que você escolhe e passa cada faixa a um callback. Cada faixa é uma fatia da página em tamanho cheio — nada é reescalonado — e carrega os mesmos valores de field de um bitmap de página inteira. Com bandas de 512 linhas, uma página de 4960 pixels de largura precisa de uns 10 MB por banda em vez de 139 MB pela folha. Mais ou menos um quatorze avos da memória. O TPdfBandCallback é um method pointer que recebe o índice da banda, o offset do topo dela em pixels de página e o bitmap, e retorna True para continuar; o RenderPageBanded retorna a contagem de bandas, 0 em falha

type
  TBandPrinter = class
  private
    FDC: HDC;
    FLeft, FTop: Integer;
  public
    constructor Create(DC: HDC; Left, Top: Integer);
    function PrintBand(BandIndex, BandTopY: Integer; Bitmap: TBitmap): Boolean;
  end;

constructor TBandPrinter.Create(DC: HDC; Left, Top: Integer);
begin
  inherited Create;
  FDC := DC;
  FLeft := Left;
  FTop := Top;
end;

function TBandPrinter.PrintBand(BandIndex, BandTopY: Integer;
  Bitmap: TBitmap): Boolean;
begin
  BlitBitmap(FDC, Bitmap, FLeft, FTop + BandTopY);
  Result := True;                      // False para as bandas restantes
end;

// chamado pelo loop de páginas no lugar do branch de bitmap de página inteira
procedure PrintPageBanded(Pdf: TPdf; Page, X, Y, W, H: Integer;
  ExtraRotation: TRotation);
var
  Bands: TBandPrinter;
begin
  Bands := TBandPrinter.Create(Printer.Canvas.Handle, X, Y);
  try
    if Pdf.RenderPageBanded(W, H, 512, Bands.PrintBand, ExtraRotation,
      [reAnnotations, rePrinting]) = 0 then
      raise Exception.CreateFmt('Page %d could not be rendered', [Page]);
  finally
    Bands.Free;
  end;
end;

O que o host deve fazer com ViewerPreferences e bits de permissão?

Leia-os e aja sobre eles no seu próprio código; o PDFium os reporta e renderiza a página de qualquer forma. Um produtor pode guardar dicas de impressão no dicionário /ViewerPreferences do catalog (ISO 32000-1 §12.2, Tabela 150), e o TPdf expõe as que moldam um print dialog:

  • O PrintScaling (Boolean) é False quando o arquivo diz /PrintScaling /None, o que significa imprimir no tamanho real, e True para /AppDefault ou entrada ausente. A norma quer que ele seja honrado mesmo quando nenhum print dialog é exibido
  • O PrintPaperHandling (TPrintPaperHandling) carrega o /Duplex como phSimplex, phDuplexFlipShortEdge, phDuplexFlipLongEdge ou phUndefined. O SetPdfPrintPaperHandlingDevMode grava isso no DEVMODE.dmDuplex e define DM_DUPLEX; para phUndefined ele retorna False e deixa o record em paz, então o default do driver sobrevive
  • O PrintCopies (Integer) é o /NumCopies, a contagem de cópias a pôr no dialog

Os bits de permissão seguem a mesma regra. Para o security handler padrão, a ISO 32000-1 §7.6.3.2 (Tabela 22) atribui o bit 3 do /P à impressão e, para handlers revision 3 e posteriores, o bit 12 à impressão em fidelidade plena; com o bit 3 definido e o bit 12 limpo, imprimir fica limitado a uma representação de baixo nível da appearance, possivelmente degradada. O TPdf.GrantedPermissions decodifica a máscara em permPrint e permPrintHighQuality, e o HasPermission testa um único valor; o motor renderiza o que você pedir, então a execução é trabalho do host, normalmente um botão Print desabilitado. Quando só o permPrint é concedido, a leitura que bate com a norma é o caminho de bitmap numa resolução modesta em vez da saída vetorial ou PostScript que uma concessão plena permite. Os bits descrevem o documento como aberto: um arquivo destravado com a owner password dele reporta acesso pleno, e o UserHasPermission responde pelos bits de nível de usuário apenas

uses
  Winapi.Windows, System.SysUtils, Vcl.Dialogs, Vcl.Printers, PDFium;

procedure PreparePrintJob(Pdf: TPdf; Dialog: TPrintDialog;
  out ActualSize, HighQuality: Boolean);
var
  Device, Driver, Port: array[0..MAX_PATH] of Char;
  DevModeHandle: THandle;
  DevMode: PDeviceMode;
begin
  if not Pdf.HasPermission(permPrint) then
    raise Exception.Create('This document does not permit printing');
  HighQuality := Pdf.HasPermission(permPrintHighQuality);
  ActualSize := not Pdf.PrintScaling;            // /PrintScaling /None

  Printer.GetPrinter(Device, Driver, Port, DevModeHandle);
  if DevModeHandle <> 0 then
  begin
    DevMode := GlobalLock(DevModeHandle);
    try
      SetPdfPrintPaperHandlingDevMode(DevMode^, Pdf.PrintPaperHandling);
    finally
      GlobalUnlock(DevModeHandle);
    end;
    Printer.SetPrinter(Device, Driver, Port, DevModeHandle);  // o dialog parte daí
  end;

  Dialog.MinPage := 1;
  Dialog.MaxPage := Pdf.PageCount;
  Dialog.Options := Dialog.Options + [poPageNums];
  if Pdf.PrintCopies > 1 then
    Dialog.Copies := Pdf.PrintCopies;
end;

// usado no lugar do FitRect quando ActualSize é True
procedure ActualSizeRect(Pdf: TPdf; out X, Y, W, H: Integer);
begin
  // 72 pontos por polegada no lado do PDF, pixels de dispositivo por polegada na impressora
  W := Round(Pdf.PageWidth / 72 * GetDeviceCaps(Printer.Handle, LOGPIXELSX));
  H := Round(Pdf.PageHeight / 72 * GetDeviceCaps(Printer.Handle, LOGPIXELSY));
  X := (Printer.PageWidth - W) div 2;  // negativo quando a página é maior
  Y := (Printer.PageHeight - H) div 2; // que a área imprimível: as bordas cortam
end;

Executando ações will-print e did-print na fronteira real de impressão

Scripts de impressão de nível de documento rodam quando o host os roda. O dicionário additional-actions do catalog (ISO 32000-1 §12.6.3, Tabela 197) define o WP, uma ação JavaScript executada antes de imprimir, e o DP, executada depois. O PDFium Component expõe ambos via ExecuteDocumentLifecycleAction(dlaWillPrint, ErrorMessage) e dlaDidPrint, e os mantém fora das chamadas de render: thumbnails, snapshots e conversões também renderizam, e nenhum deles deve parecer uma impressão de usuário para um script de documento. A ordem é tarefa sua acertar. Um script will-print que carimba uma data ou preenche um field "printed by" muda valores de formulário, então rode-o antes que a primeira página seja renderizada e, na rota flatten, antes de a cópia ser salva; dispare o did-print só depois que o EndDoc tiver commitado o job. A chamada retorna False com uma mensagem quando o form environment está ausente ou o LifecycleActionPolicy o recusa; para arquivos não confiáveis, o lapDisabled desliga ações de documento. O quadro mais amplo de scripting está em rodar AcroForm JavaScript com eventos do host

No Windows, um job que precisa carregar valores de formulário pertence ao branch de bitmap: RenderPage para um TBitmap em resoluções ordinárias, RenderPageBanded a 600 dpi e acima. Reserve o overload de device context do PDFium Component para cópias flattened e para documentos sem fields, onde a saída vetorial dele é o melhor resultado