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 — numa tabela PDF paginada, e três pontes em Addons entregam um relatório preparado do FastReport, QuickReport ou ReportBuilder ao mesmo gravador de PDF. Ambos produzem um PDF real sem controlador de impressora e sem janela visível

Os dois problemas parecem semelhantes, mas não são. Um DBGrid ou o resultado de uma consulta bruta não tem layout próprio, por isso exportá-lo significa inventar um: colunas, cabeçalhos, quebras de página. Um documento FastReport ou ReportBuilder já possui um layout desenhado, portanto exportá-lo significa reproduzir fielmente os comandos de desenho de outrem no espaço PDF. A losLab PDF Library mantém estas preocupações em unidades separadas precisamente porque os modos de falha diferem, e o resto deste artigo aborda cada um deles e onde deixam de ser fiáveis

Como exportar um TDataSet para PDF no Delphi?

O PDFlibTableExport transforma um dataset numa tabela numa única chamada. O exportador percorre a lista de campos uma vez, ignora os campos blob binários automaticamente — o ftBlob, o ftGraphic, o ftBytes e semelhantes não possuem texto de célula útil —, alinha à direita as colunas numéricas e pinta uma risca de zebra opcional sobre uma banda de cabeçalho estilizada. Por baixo do capô, constrói a grelha com CreateTable, preenche as células com SetTableCellContent e renderiza com DrawTableRows, a mesma API pública de Tabela que controlaria manualmente. O invólucro de conveniência PDFlibExportDataSet configura milímetros e uma origem no canto superior esquerdo para si, de modo que o chamador apenas fornece 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ças. Quando precisa de controlo por campo — uma etiqueta de coluna personalizada, uma largura mais estreita ou a remoção de uma coluna de chave interna que não seja um blob —, construa um TPDFlibTableExporter diretamente e ligue-o ao seu evento OnFieldFilter, que é acionado uma vez por campo e lhe entrega uma especificação mutável. Defina Include como False para descartar o campo, ou defina ColumnWidth e DisplayLabel para substituir os padrões. Esse gancho é também onde exclui uma coluna memo larga que não deseja que aumente a página

Paginação: passar o estado de desenho entre páginas

O DrawTableRows carrega o estado de paginação, e compreender o seu contrato é a chave de tudo. Chama-o com uma primeira linha e uma última linha; passar uma última linha abaixo de 1 significa desenhar até ao fim da tabela, limitado pela altura que forneceu. A função devolve a altura que realmente desenhou, e o GetTableLastDrawnRow indica a última linha que coube. Esse par é o estado que passa de uma página para a seguinte: quando a última linha desenhada fica aquém do total, abre uma nova página e retoma a partir da linha seguinte. Nada é medido novamente — a continuação recomeça exatamente onde a chamada anterior parou

// Como o exportador continua uma tabela longa através das páginas
PageHeight := PDF.PageHeight;
Y := PageHeight - Options.Top;
Row := 1;
while Row <= TotalRows do
begin
  DrawHeight := Y - Options.BottomMargin;
  // Última linha = 0 significa "desenhar até ao fim", limitado por 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 desenha opcionalmente a linha 1 novamente — o cabeçalho — antes das linhas de dados, e utiliza a altura devolvida por essa primeira chamada do DrawTableRows para desviar o corpo abaixo dela. É por isso que o DrawTableRows devolve uma altura em vez de uma coordenada da linha seguinte: o valor devolvido é o que lhe permite empilhar um cabeçalho redesenhado e o corpo que continua sem codificar rigidamente o tamanho de nenhum deles

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

As pontes de relatório adotam a abordagem oposta: nunca inventam um layout, apenas o reproduzem. Cada ponte liga-se ao seu motor no ponto de exportação nativo do motor. O PDFlibFRExport cria subclasses de TfrxCustomExportFilter e recebe os objetos de memo, imagem, forma e linha do FastReport, traduzindo cada um num primitivo PDFlibPas. O PDFlibRBDevice cria subclasses de TppFileDevice e percorre a lista DrawCommand do ReportBuilder. O PDFlibQRExport segue um terceiro caminho completamente diferente, que a secção seguinte aborda. Todos os três funcionam em modo headless, que é a principal razão para recorrer a eles num servidor

O modo headless não é gratuito, e o FastReport é o exemplo de aviso. O TfrxReport.Export passa pelas páginas de pré-visualização, que consultam a propriedade ShowDialog do filtro, e essa propriedade herda o padrão True. Numa máquina sem ambiente de trabalho interativo, a caixa de diálogo modal devolve 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 seletores paralelos — AllowPrintToFile e ShowPrintDialog —, e o QuickReport, que executa tudo através do Prepare, não precisa de qualquer supressão de caixas de diálogo

var
  Exporter: TPDFlibFRExport;
begin
  Report.PrepareReport;                  // constrói as páginas primeiro
  Exporter := TPDFlibFRExport.Create(nil);
  try
    Exporter.FileName := 'invoice.pdf';
    Exporter.ShowDialog := False;        // headless: ignora o modal, sem cancelamento
    Report.Export(Exporter);
  finally
    Exporter.Free;
  end;
end;

Três motores, three coordinate systems

Cada ponte faz a 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 píxeis a 96 PPI e redimensiona por 96/25.4 para obter milímetros. O PDFlibRBDevice lê os comandos de desenho do ReportBuilder em milésimos de milímetro e divide por 1000, e regista-se através de ppRegisterDevice, pelo que definir ppReport.DeviceType como 'PDFlibPas' e chamar Print é suficiente para encaminhar a saída através dele. O PDFlibQRExport não faz qualquer cálculo matemático de coordenadas: uma página preparada do QuickReport já é um meta-ficheiro EMF, pelo que a ponte guarda cada página num fluxo (stream) e alimenta-a no ImportEMFFromStream, permitindo que o caminho de renderização de contexto de dispositivo e meta-ficheiro da biblioteca coloque cada glifo e regra. As pontes do FastReport e do ReportBuilder, em contrapartida, emitem primitivos de desenho vetorial nativos e texto nativo

var
  PDF: TPDFlib;
begin
  PDF := TPDFlib.Create;
  try
    PDF.SetOrigin(1);
    // Prepara o relatório e anexa cada página numa única chamada; a ponte
    // transforma o EMF de cada página em PDF através do ImportEMFFromStream.
    PDFlibQRExportReport(PDF, QuickRep1, 'ledger.pdf');
  finally
    PDF.Free;
  end;
end;

Onde termina a fidelidade

Conheça os limites antes de comprometer um fluxo de trabalho com estas pontes. O exportador de datasets descarta colunas binárias silenciosamente, por isso um relatório que precise de mostrar uma imagem incorporada necessita de uma abordagem diferente de uma tabela direta. O caminho do QuickReport, porque passa por EMF, converte o texto em glifos vetoriais — a página parece correta, mas não contém texto selecionável ou pesquisável e nenhuma estrutura PDF etiquetada para acessibilidade. O FastReport e o ReportBuilder mantêm o texto real, mas os manipuladores tipados cobrem as vistas comuns — memo, imagem, forma, linha — e recorrem a uma caixa delimitadora (bounding box) ou ignoram as exóticas, pelo que um relatório que dependa de rich text, códigos de barras ou preenchimentos de gradiente perderá detalhes. Nada disto é um defeito; é o limite real de uma camada de tradução

Uma advertência operacional supera as restantes. Nenhum dos três motores é fornecido com a losLab PDF Library, e as pontes são deliberadamente excluídas da compilação principal — são compiladas apenas a partir de um projeto que já tenha o FastReport VCL 6.x, o QuickReport 8 ou o ReportBuilder 20 no seu caminho de pesquisa (search path). As superfícies meta-ficheiro e DrawCommand que eles visam permaneceram estáveis em várias versões principais do motor, mas a correspondência de versão é da sua responsabilidade. Acerte nisso e os dois caminhos cobrem toda a amplitude, desde um despejo ad-hoc de DBGrid até uma fatura desenhada. O exportador de tabelas e as pontes de relatório são fornecidos com a losLab PDF Library para Delphi e C++Builder