Технічна стаття

Експорт TDataSet та звітів у PDF в Delphi

Для експорту TDataSet або звіту в PDF у Delphi, losLab PDF Library пропонує два шляхи. PDFlibTableExport адаптує будь-який TDataSet — запит FireDAC, ClientDataSet, таблицю в пам'яті — у таблицю PDF з розбивкою на сторінки, а три мости в Addons передають підготовлений звіт FastReport, QuickReport або ReportBuilder тому самому записувачу PDF. Обидва варіанти створюють справжній PDF без драйвера принтера та виведення вікна.

Ці дві проблеми виглядають схожими, але це не так. DBGrid або необроблений результат запиту не мають власного макета, тому їх експорт означає створення нового: стовпців, заголовків, розривів сторінок. Документ FastReport або ReportBuilder уже містить розроблений макет, тому його експорт означає точне відтворення чиїхось команд малювання у просторі PDF. Бібліотека losLab PDF Library розділяє ці завдання по різних модулях саме тому, що режими збоїв відрізняються, і решта цієї статті детально описує кожен із них та випадки, коли вони перестають бути надійними.

Як експортувати TDataSet у PDF в Delphi?

PDFlibTableExport перетворює набір даних на таблицю за один виклик. Експортер один раз проходить по списку полів, автоматично пропускає бінарні поля blob — ftBlob, ftGraphic, ftBytes та подібні до них не містять корисного тексту комірок — вирівнює числові стовпці по правому краю та малює необов'язкові смуги («зебру») поверх стилізованого заголовка. Під капотом він створює сітку за допомогою CreateTable, заповнює комірки за допомогою SetTableCellContent і рендерить за допомогою DrawTableRows — того самого публічного Table API, яким ви керували б вручну. Зручна обгортка PDFlibExportDataSet налаштовує міліметри та початкову точку у лівому верхньому кутку, тому викликачу потрібно лише надати сторінку.

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;

Автоматичний пропуск полів blob є поведінкою за замовчуванням, а не жорстким обмеженням. Коли вам потрібен контроль для кожного поля окремо — власна мітка стовпця, менша ширина або вилучення внутрішнього стовпця ключа, який не є blob — створюйте TPDFlibTableExporter безпосередньо та підключайтеся до його події OnFieldFilter, яка спрацьовує один раз для кожного поля та надає вам специфікацію, яку можна змінювати. Встановіть Include у значення False, щоб виключити поле, або встановіть ColumnWidth та DisplayLabel для перевизначення значень за замовчуванням. Цей обробник також використовується для виключення широких полів типу memo, щоб вони не роздували сторінку.

Розбивка на сторінки: передача стану малювання між сторінками

DrawTableRows переносить стан розбивки на сторінки, і розуміння принципу його роботи — це найважливіша частина. Ви викликаєте його із першим і останнім рядком; передача останнього рядка менше 1 означає малювання до кінця таблиці, обмежене вказаною вами висотою. Функція повертає висоту, яку вона фактично намалювала, а GetTableLastDrawnRow повідомляє останній рядок, який помістився. Ця пара є станом, який ви передаєте з однієї сторінки на іншу: коли останній намальований рядок менший за загальну кількість, ви відкриваєте нову сторінку та продовжуєте з наступного рядка. Нічого не вимірюється наново — продовження починається саме там, де зупинився попередній виклик.

// 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;

У цьому циклі також реалізовано повторення заголовка. На кожній наступній сторінці експортер за бажанням знову малює рядок 1 — заголовок — перед рядками даних і використовує висоту, повернуту цим першим викликом DrawTableRows, для зсуву тіла під ним. Саме тому DrawTableRows повертає висоту, а не координату наступного рядка: це значення дозволяє розмістити повторюваний заголовок та продовження тіла таблиці без жорсткого кодування розміру будь-якого з них.

Як експортувати звіт FastReport або ReportBuilder у PDF?

Мости звітів використовують протилежний підхід: вони не створюють макет наново, вони відтворюють готовий. Кожен міст підключається до свого рушія через його власний інтерфейс експорту. PDFlibFRExport є підкласом TfrxCustomExportFilter та отримує об'єкти FastReport (текстові блоки, зображення, фігури та лінії), транслюючи кожен із них у примітив PDFlibPas. PDFlibRBDevice є підкласом TppFileDevice та обходить список DrawCommand у ReportBuilder. PDFlibQRExport використовує зовсім інший, третій шлях, який розглядається в наступному розділі. Усі три працюють у фоновому режимі (headless), що є головною причиною їх використання на сервері.

Робота в автономному режимі має свої нюанси, і FastReport є класичним прикладом. Метод TfrxReport.Export проходить через сторінки попереднього перегляду, які перевіряють властивість фільтра ShowDialog, що за замовчуванням має значення True. На машині без інтерактивного робочого столу модальне діалогове вікно повертає результат скасування, і експорт непомітно завершується помилкою. Встановіть ShowDialog у значення False перед викликом Export, і звіт буде створено без діалогових вікон. У ReportBuilder є аналогічні перемикачі — AllowPrintToFile та ShowPrintDialog, а QuickReport, який запускає все через метод Prepare, взагалі не потребує придушення діалогових вікон.

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;

Три рушії, три системи координат

Кожен міст виконує власні арифметичні обчислення координат, оскільки кожен рушій вимірює простір по-своєму. PDFlibFRExport розглядає позиції об'єктів FastReport як пікселі при 96 PPI та масштабує їх на коефіцієнт 96/25.4 для отримання міліметрів. PDFlibRBDevice зчитує команди малювання ReportBuilder у тисячних частках міліметра та ділить їх на 1000, а також реєструє себе через ppRegisterDevice, тому встановлення значення 'PDFlibPas' для ppReport.DeviceType та виклику Print достатньо, щоб спрямувати виведення через нього. PDFlibQRExport взагалі не виконує математичних обчислень координат: підготовлена сторінка QuickReport вже є метафайлом EMF, тому міст зберігає кожну сторінку в потік і передає її в ImportEMFFromStream, дозволяючи бібліотечному шляху рендерингу контексту пристрою та метафайлів розмістити кожен гліф та лінію. На відміну від цього, мости FastReport та ReportBuilder створюють власні векторні примітиви малювання та нативний текст.

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;

Де закінчується точність відтворення

Ознайомтеся з обмеженнями, перш ніж переводити свої робочі процеси на ці мости. Експортер набору даних автоматично пропускає бінарні стовпці, тому звіт, який має відображати вбудоване зображення, потребує іншого підходу, ніж звичайна таблиця. Шлях QuickReport через те, що він використовує EMF, перетворює текст на векторні гліфи — сторінка виглядає правильно, але не містить тексту, який можна виділити або знайти, а також не має структури тегованого PDF для забезпечення доступності. FastReport та ReportBuilder зберігають справжній текст, але їхні типізовані обробники підтримують лише загальні об'єкти — текстові блоки, зображення, фігури, лінії — та використовують обмежувальну рамку або пропускають специфічні елементи, тому звіт, який містить розширений текст, штрих-коди або градієнтні заливки, може втратити деталізацію. Жодне з цього не є дефектом — це просто межа можливостей трансляційного шару.

Одне експлуатаційне застереження важливіше за інші. Жоден з трьох рушіїв не постачається у складі losLab PDF Library, і мости навмисно виключені з основної збірки — вони компілюються лише у проекті, який уже містить FastReport VCL 6.x, QuickReport 8 або ReportBuilder 20 у своєму шляху пошуку. Інтерфейси метафайлів та DrawCommand, на які вони орієнтовані, залишаються стабільними в кількох основних версіях рушіїв, але ви повинні самі забезпечити сумісність версій. Якщо ви зробите це правильно, обидва шляхи охоплять усі завдання: від швидкого дампу DBGrid до розробленого рахунку-фактури. Експортер таблиць та мости звітів постачаються разом із losLab PDF Library для Delphi та C++Builder.