Artigo Técnico

Exportar TDataSet e relatórios para PDF no Delphi

Para exportar um TDataSet ou um relatório para PDF no Delphi, a losLab PDF Library oferece dois caminhos. O PDFlibTableExport adapta qualquer TDataSet — uma consulta FireDAC, um ClientDataSet, uma tabela em memória — em uma tabela PDF paginada, e três pontes sob Addons entregam um relatório FastReport, QuickReport ou ReportBuilder preparado para o mesmo gravador de PDF. Ambos produzem um PDF real sem driver de impressora e sem janela visível

Os dois problemas parecem semelhantes, mas não são. Um DBGrid ou um resultado de consulta bruto não tem layout próprio, então exportá-lo significa inventar um: colunas, cabeçalhos, quebras de página. Um documento FastReport ou ReportBuilder já carrega um layout projetado, então exportá-lo significa reproduzir fielmente os comandos de desenho de outra pessoa no espaço do PDF. A losLab PDF Library mantém essas preocupações em unidades separadas precisamente porque os modos de falha diferem, e o restante deste artigo aborda cada uma delas e onde elas deixam de ser confiáveis

Como exportar um TDataSet para PDF no Delphi?

O PDFlibTableExport transforma um conjunto de dados em uma tabela em uma única chamada. O exportador percorre a lista de campos uma vez, ignora campos blob binários automaticamente — ftBlob, ftGraphic, ftBytes e seus semelhantes não têm texto de célula útil — alinha à direita colunas numéricas e pinta uma faixa zebra opcional sobre uma banda de cabeçalho estilizada. Por baixo do capô, ele constrói a grade com CreateTable, preenche as células com SetTableCellContent e renderiza com DrawTableRows, a mesma API pública de Tabela que você controlaria manualmente. O wrapper de conveniência PDFlibExportDataSet configura milímetros e uma origem superior esquerda para você, de modo que o chamador fornece apenas uma página

uses
  Data.DB, FireDAC.Comp.Client, FireDAC.Stan.StorageBin,
  PDFlibrary, PDFlibTableExport;

var
  MemTable: TFDMemTable;
  PDF: TPDFlib;
  Options: TPDFlibTableExportOptions;
begin
  MemTable := TFDMemTable.Create(nil);
  PDF := TPDFlib.Create;
  try
    MemTable.LoadFromFile('customer.FDS');   // any TDataSet works here

    PDF.SetOrigin(1);
    PDF.SetMeasurementUnits(1);              // millimetres
    PDF.SetPageSize('A4');
    PDF.AddStandardFont(4);

    Options := DefaultTableExportOptions;
    Options.Title := 'Customers';
    Options.ColumnWidth := 32;               // 11 fields fit on A4 at 32 mm
    Options.RepeatHeader := True;            // redraw the header on each page

    PDFlibExportDataSet(PDF, MemTable, 'customers.pdf', Options);
  finally
    PDF.Free;
    MemTable.Free;
  end;
end;

A omissão automática de blobs é um padrão, não uma camisa de força. Quando você precisar de controle por campo — um rótulo de coluna personalizado, uma largura menor ou a remoção de uma coluna de chave interna que não seja um blob —, construa um TPDFlibTableExporter diretamente e conecte seu evento OnFieldFilter, que é disparado uma vez por campo e fornece uma especificação mutável. Defina Include como False para descartar o campo, ou defina ColumnWidth e DisplayLabel para substituir os padrões. Esse hook também é onde você exclui uma coluna de memo larga que não deseja que infle a página

Paginação: passando o estado do desenho entre páginas

O DrawTableRows carrega o estado de paginação, e entender seu contrato é o segredo do jogo. Você o chama com uma primeira linha e uma última linha; passar uma última linha menor que 1 significa desenhar até o fim da tabela, limitado pela altura informada. A função retorna a altura que ela realmente desenhou, e GetTableLastDrawnRow informa a última linha que coube. Esse par é o estado que você passa de uma página para a outra: quando a última linha desenhada é menor que o total, você abre uma nova página e retoma a partir da linha seguinte. Nada é medido novamente — a continuação recomeça exatamente de onde a chamada anterior parou

// How the exporter continues a long table across pages
PageHeight := PDF.PageHeight;
Y := PageHeight - Options.Top;
Row := 1;
while Row <= TotalRows do
begin
  DrawHeight := Y - Options.BottomMargin;
  // Last row = 0 means "draw to the end", bounded by DrawHeight
  PDF.DrawTableRows(TableID, Options.Left, Y, DrawHeight, Row, 0);
  LastDrawn := PDF.GetTableLastDrawnRow(TableID);
  if LastDrawn >= TotalRows then
    Break;                       // the whole table fit on this page
  if LastDrawn < Row then
    Break;                       // safety: no forward progress, bail out
  PDF.NewPage;
  Row := LastDrawn + 1;          // continue from the first undrawn row
  Y := PageHeight - Options.Top;
end;

O loop também é onde reside o cabeçalho repetido. Em cada página de continuação, o exportador opcionalmente desenha a linha 1 novamente — o cabeçalho — antes das linhas de dados, e usa a altura retornada por aquela primeira chamada de DrawTableRows para deslocar o corpo abaixo dela. É por isso que o DrawTableRows retorna uma altura em vez de uma coordenada de próxima linha: o valor retornado é o que permite empilhar um cabeçalho redesenhado e o corpo contínuo sem embutir o tamanho de nenhum dos dois

Como exportar um relatório FastReport ou ReportBuilder para PDF?

As pontes de relatório adotam a abordagem oposta: elas nunca inventam um layout, elas o reproduzem. Cada ponte se conecta ao seu motor no ponto de exportação nativo do motor. O PDFlibFRExport herda de TfrxCustomExportFilter e recebe os objetos de memo, imagem, forma e linha do FastReport, traduzindo cada um em um primitivo do PDFlibPas. O PDFlibRBDevice herda de TppFileDevice e percorre a lista DrawCommand do ReportBuilder. O PDFlibQRExport segue por um terceiro caminho totalmente diferente, abordado na próxima seção. Todos os três funcionam em modo headless, que é o principal motivo para utilizá-los em um servidor

Headless não é de graça, e o FastReport é o exemplo de alerta. O TfrxReport.Export passa pelas páginas de visualização, que consultam a propriedade ShowDialog do filtro, e essa propriedade herda um padrão de True. Em uma máquina sem área de trabalho interativa, a caixa de diálogo modal retorna um resultado de cancelamento e a exportação falha silenciosamente. Defina ShowDialog como False antes de chamar Export e o relatório será renderizado silenciosamente. O ReportBuilder tem chaves paralelas — AllowPrintToFile e ShowPrintDialog —, e o QuickReport, que gerencia tudo por meio de Prepare, não precisa de nenhuma supressão de diálogo

var
  Exporter: TPDFlibFRExport;
begin
  Report.PrepareReport;                  // build the pages first
  Exporter := TPDFlibFRExport.Create(nil);
  try
    Exporter.FileName := 'invoice.pdf';
    Exporter.ShowDialog := False;        // headless: skip the modal, no cancel
    Report.Export(Exporter);
  finally
    Exporter.Free;
  end;
end;

Três motores, três sistemas de coordenadas

Cada ponte faz sua própria aritmética de coordenadas, porque cada motor mede o mundo de forma diferente. O PDFlibFRExport trata as posições dos objetos do FastReport como pixels a 96 PPI e escala por 96/25.4 para chegar a milímetros. O PDFlibRBDevice lê os comandos de desenho do ReportBuilder em milésimos de mm e divide por 1000, e se registra por meio de ppRegisterDevice, de modo que definir ppReport.DeviceType como 'PDFlibPas' e chamar Print é suficiente para direcionar a saída através dele. O PDFlibQRExport não faz nenhuma matemática de coordenadas: uma página QuickReport preparada já é um meta-arquivo EMF, então a ponte salva cada página em um stream e o alimenta em ImportEMFFromStream, deixando o caminho de contexto de dispositivo e renderização de meta-arquivo da biblioteca posicionar cada glifo e linha. As pontes do FastReport e do ReportBuilder, por outro lado, emitem primitivos de desenho vetorial nativos e texto nativo

var
  PDF: TPDFlib;
begin
  PDF := TPDFlib.Create;
  try
    PDF.SetOrigin(1);
    // Prepares the report and appends every page in one call; the bridge
    // turns each page's EMF into PDF through ImportEMFFromStream.
    PDFlibQRExportReport(PDF, QuickRep1, 'ledger.pdf');
  finally
    PDF.Free;
  end;
end;

Onde a fidelidade termina

Conheça os limites antes de comprometer um fluxo de trabalho com essas pontes. O exportador de datasets descarta colunas binárias silenciosamente, de modo que um relatório que precisa exibir uma imagem incorporada exige uma abordagem diferente de uma tabela simples. O caminho do QuickReport, por passar por EMF, converte texto em glifos vetoriais — a página parece correta, mas não carrega texto selecionável ou pesquisável e nenhuma estrutura de PDF etiquetado (tagged PDF) para acessibilidade. O FastReport e o ReportBuilder mantêm o texto real, mas os manipuladores tipados cobrem as visualizações comuns — memo, imagem, forma, linha — e recorrem a uma caixa delimitadora ou ignoram as mais exóticas, de modo que um relatório baseado em rich text, códigos de barras ou preenchimentos de gradiente perderá detalhes. Nada disso é um defeito; é o limite real de uma camada de tradução

Uma ressalva operacional supera as demais. Nenhum dos três motores é fornecido com a losLab PDF Library, e as pontes são deliberadamente excluídas do build principal — elas compilam apenas a partir de um projeto que já tenha o FastReport VCL 6.x, o QuickReport 8 ou o ReportBuilder 20 em seu caminho de pesquisa (search path). As superfícies de meta-arquivo e DrawCommand que elas visam permaneceram estáveis em várias versões principais dos motores, mas você é o responsável pela correspondência de versões. Acerte isso e os dois caminhos cobrem toda a extensão, desde um dump DBGrid ad-hoc até uma fatura desenhada. O exportador de tabela e as pontes de relatório são fornecidos com a losLab PDF Library para Delphi e C++Builder