Technický článek

Export TDataSet a reportů do PDF v Delphi

Pro exportování TDataSet nebo reportu do PDF v Delphi nabízí losLab PDF Library dvě cesty. `PDFlibTableExport` přizpůsobí jakýkoli `TDataSet` — dotaz FireDAC, ClientDataSet, tabulku v paměti — do paginované PDF tabulky, a tři přemostění pod `Addons` předají připravený report z FastReport, QuickReport nebo ReportBuilder stejnému PDF zapisovači. Oba způsoby generují skutečné PDF bez tiskového ovladače a bez viditelného okna

Tyto dva problémy vypadají podobně, ale nejsou. DBGrid nebo surový výsledek dotazu nemá své vlastní rozvržení, takže jeho export znamená nějaké navrhnout: sloupce, záhlaví, konce stránek. Dokument FastReport nebo ReportBuilder již navržené rozvržení nese, takže jeho export znamená věrně zopakovat kreslicí příkazy někoho jiného do prostoru PDF. losLab PDF Library udržuje tyto záležitosti v oddělených jednotkách právě proto, že způsoby selhání se liší, a zbytek tohoto článku se věnuje každému z nich a místům, kde přestávají být spolehlivé

Jak exportovat TDataSet do PDF v Delphi?

`PDFlibTableExport` převede dataset na tabulku v jediném volání. Exportér jednou projde seznam polí, automaticky přeskočí binární pole typu blob — `ftBlob`, `ftGraphic`, `ftBytes` a jim podobné nemají žádný užitečný text buňky — zarovná číselné sloupce doprava a vykreslí volitelné zebra pruhování přes nastylovaný pás záhlaví. Pod kapotou sestaví mřížku pomocí `CreateTable`, vyplní buňky pomocí `SetTableCellContent` a vykreslí pomocí `DrawTableRows`, což je stejné veřejné API tabulky, které byste ovládali ručně. Pohodlný obal `PDFlibExportDataSet` za vás nakonfiguruje milimetry a počátek vlevo nahoře, takže volající poskytuje pouze stránku

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;

Automatické přeskakování blobů je výchozí nastavení, nikoli svěrací kazajka. Když potřebujete kontrolu nad jednotlivými poli — vlastní popisek sloupce, užší šířku nebo vynechání interního klíčového sloupce, který není typu blob — vytvořte přímo `TPDFlibTableExporter` a připojte se k jeho události `OnFieldFilter`, která se spustí jednou pro každé pole a předá vám modifikovatelnou specifikaci. Nastavením `Include` na `False` pole vynecháte, případně nastavte `ColumnWidth` a `DisplayLabel` pro přepsání výchozích hodnot. Tento háček (hook) je také místem, kde vyloučíte široký sloupec typu memo, který nechcete mít roztažený na celou stránku

Stránkování: předávání stavu kreslení mezi stránkami

`DrawTableRows` nese stav stránkování a pochopení jeho kontraktu je základem všeho. Voláte jej s prvním a posledním řádkem; předání posledního řádku menšího než 1 znamená kreslit až do konce tabulky, omezeno výškou, kterou jste zadali. Funkce vrací výšku, kterou skutečně vykreslila, a `GetTableLastDrawnRow` hlásí poslední řádek, který se vešel. Tato dvojice představuje stav, který předáváte z jedné stránky na druhou: pokud je poslední vykreslený řádek kratší než celkový počet, otevřete novou stránku a pokračujete od řádku následujícího. Nic se znovu neměří — pokračování navazuje přesně tam, kde předchozí volání skončilo

// Jak exportér pokračuje s dlouhou tabulkou napříč stránkami
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;

Smyčka je také místem, kde žije opakující se záhlaví. Na každé navazující stránce exportér volitelně vykreslí řádek 1 znovu — záhlaví — před datovými řádky a použije výšku vrácenou tímto prvním voláním `DrawTableRows` k posunutí těla pod ním. Proto `DrawTableRows` vrací výšku namísto souřadnice dalšího řádku: vrácená hodnota je to, co vám umožňuje poskládat překreslené záhlaví a pokračující tělo bez pevného kódování velikosti kteréhokoli z nich

Jak exportovat report FastReport nebo ReportBuilder do PDF?

Přemostění pro reporty volí opačný přístup: nikdy nenavrhují rozvržení, ale přehrávají ho. Každé přemostění se připojuje ke svému stroji na jeho nativním exportním rozhraní. `PDFlibFRExport` dědí z `TfrxCustomExportFilter` a přijímá objekty memo, obrázky, tvary a čáry z FastReportu a překládá každý z nich na primitivum PDFlibPas. `PDFlibRBDevice` dědí z `TppFileDevice` a prochází seznam DrawCommand z ReportBuilderu. `PDFlibQRExport` volí zcela třetí cestu, kterou se zabývá další část. Všechny tři běží bezhlavě (headless), což je hlavním důvodem, proč po nich na serveru sáhnout

Headless režim není zadarmo a FastReport je toho varovným příkladem. `TfrxReport.Export` prochází přes náhledové stránky, které konzultují vlastnost filtru `ShowDialog`, přičemž tato vlastnost dědí výchozí hodnotu `True`. Na stroji bez interaktivní plochy vrátí modální dialog výsledek stornování a export potichu selže. Před voláním `Export` nastavte `ShowDialog` na `False` a report se vykreslí bez dialogů. ReportBuilder má paralelní přepínače — `AllowPrintToFile` a `ShowPrintDialog` — a QuickReport, který vše řídí přes `Prepare`, nepotřebuje potlačení dialogů vůbec

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;

Tři stroje, tři souřadnicové systémy

Každé přemostění provádí vlastní aritmetiku souřadnic, protože každý stroj měří svět jinak. `PDFlibFRExport` považuje pozice objektů FastReport za pixely při 96 PPI a měřítko upravuje poměrem 96/25.4, aby dosáhl milimetrů. `PDFlibRBDevice` čte kreslicí příkazy ReportBuilderu v tisícinách mm a dělí je 1000, přičemž se registruje přes `ppRegisterDevice`, takže nastavení `ppReport.DeviceType` na `'PDFlibPas'` a volání `Print` stačí k nasměrování výstupu přes něj. `PDFlibQRExport` neprovádí vůbec žádnou matematiku souřadnic: připravená stránka QuickReportu je již EMF metaoobrázek (metafile), takže přemostění uloží každou stránku do streamu a předá ji do `ImportEMFFromStream`, čímž nechá kontext zařízení a cestu vykreslování metasouborů knihovny umístit každý znak a pravidlo. Mosty pro FastReport a ReportBuilder naproti tomu emitují nativní vektorové kreslicí primitivy a nativní text

var
  PDF: TPDFlib;
begin
  PDF := TPDFlib.Create;
  try
    PDF.SetOrigin(1);
    // Připraví report a připojí každou stránku v jednom volání; přemostění
    // převede EMF každé stránky do PDF pomocí ImportEMFFromStream
    PDFlibQRExportReport(PDF, QuickRep1, 'ledger.pdf');
  finally
    PDF.Free;
  end;
end;

Kde věrnost končí

Seznamte se s hranicemi dříve, než těmto přemostěním svěříte svůj pracovní postup. Exportér datasetů potichu vynechává binární sloupce, takže report, který musí zobrazovat vložený obrázek, vyžaduje jiný přístup než přímá tabulka. Cesta přes QuickReport kvůli použití EMF převádí text na vektorové znaky — stránka vypadá správně, ale neobsahuje žádný vybratelný nebo prohledávatelný text ani strukturu tagovaného PDF pro usnadnění přístupu (accessibility). FastReport a ReportBuilder uchovávají skutečný text, ale typové obsluhy pokrývají běžné pohledy — memo, obrázek, tvar, čáru — a u těch exotických se vracejí k ohraničujícímu rámečku (bounding box) nebo je přeskakují, takže report spoléhající na bohatý text (rich text), čárové kódy nebo přechodové výplně ztratí detaily. Nic z toho není vada; je to upřímná hranice překladové vrstvy

Jedno provozní upozornění převyšuje ostatní. Žádný ze tří strojů se nedodává uvnitř losLab PDF Library a přemostění jsou záměrně vyloučena z hlavního sestavení — kompilují se pouze z projektu, který již má na své vyhledávací cestě FastReport VCL 6.x, QuickReport 8 nebo ReportBuilder 20. Rozhraní metaoobrázků (metafile) a DrawCommand, na která cílí, zůstala napříč několika hlavními verzemi strojů stabilní, ale shoda verzí je na vás. Pokud to uděláte správně, obě cesty pokryjí celou škálu, od ad-hoc výpisu DBGrid po navrženou fakturu. Exportér tabulek i přemostění reportů se dodávají s losLab PDF Library pro Delphi a C++Builder