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