기술 문서

Delphi에서 TDataSet 및 보고서를 PDF로 내보내기

Delphi에서 TDataSet 또는 보고서를 PDF로 내보내기 위해 losLab PDF Library는 두 가지 경로를 제공합니다. PDFlibTableExport는 FireDAC 쿼리, ClientDataSet, 인메모리 테이블 등 모든 TDataSet을 페이지가 매겨진 PDF 표로 변환하고, Addons 아래의 세 가지 브리지는 준비된 FastReport, QuickReport 또는 ReportBuilder 보고서를 동일한 PDF 라이터에 전달합니다. 둘 다 프린터 드라이버나 표시되는 창 없이 실제 PDF를 생성합니다

두 문제는 비슷해 보이지만 실제로는 다릅니다. DBGrid나 원시 쿼리 결과에는 자체 레이아웃이 없으므로 내보내기란 열, 헤더, 페이지 나누기 등 새로운 레이아웃을 정의하는 것을 의미합니다. 반면 FastReport 또는 ReportBuilder 문서는 이미 설계된 레이아웃을 갖고 있으므로 내보내기란 다른 사람의 그리기 명령을 PDF 공간에 충실하게 재현하는 것을 의미합니다. losLab PDF Library는 실패 모드가 다르기 때문에 이러한 관심사를 별도의 유닛으로 분리하여 관리하며, 이 기사의 나머지 부분에서는 각 경로와 안정성이 보장되지 않는 지점에 대해 알아봅니다

Delphi에서 TDataSet을 PDF로 내보내려면 어떻게 해야 하나요?

PDFlibTableExport는 한 번의 호출로 데이터셋을 표로 변환합니다. 내보내기 도구는 필드 목록을 한 번 검사하여 이진 BLOB 필드(ftBlob, ftGraphic, ftBytes 등 셀 텍스트로 유용하지 않은 필드)를 자동으로 건너뛰고, 숫자 열을 오른쪽으로 정렬하며, 스타일이 지정된 헤더 밴드 위에 선택적으로 얼룩말 스트라이프를 그립니다. 내부적으로는 CreateTable로 그리드를 빌드하고, SetTableCellContent로 셀을 채우며, 직접 제어할 수 있는 것과 동일한 공개 Table API인 DrawTableRows로 렌더링합니다. 편리한 래퍼 함수인 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 이벤트를 연결하십시오. 필드를 삭제하려면 IncludeFalse로 설정하고, 기본값을 재정의하려면 ColumnWidthDisplayLabel을 설정하십시오. 이 훅은 페이지 크기를 너무 크게 만들고 싶지 않은 넓은 메모 열을 제외하는 곳이기도 합니다

페이지 매기기: 페이지 간에 그리기 상태 전달하기

DrawTableRows는 페이지 매기기 상태를 유지하며, 이 메서드의 동작 방식을 이해하는 것이 핵심입니다. 첫 번째 행과 마지막 행을 인수로 사용하여 호출하며, 마지막 행에 1보다 작은 값을 전달하면 제공된 높이 제한 내에서 테이블 끝까지 그리게 됩니다. 이 함수는 실제로 그린 높이를 반환하고, GetTableLastDrawnRow는 채워진 마지막 행을 보고합니다. 이 두 값은 다음 페이지로 전달해야 하는 상태입니다. 마지막으로 그려진 행이 전체 행보다 적으면 새 페이지를 열고 그 다음 행부터 그리기를 재개합니다. 아무것도 다시 측정되지 않으며, 이전 호출이 중단된 지점부터 정확히 이어집니다

// 내보내기 도구가 여러 페이지에 걸쳐 긴 표를 이어 그리는 방법
PageHeight := PDF.PageHeight;
Y := PageHeight - Options.Top;
Row := 1;
while Row <= TotalRows do
begin
  DrawHeight := Y - Options.BottomMargin;
  // 마지막 행 = 0은 DrawHeight 내에서 "끝까지 그리기"를 의미합니다
  PDF.DrawTableRows(TableID, Options.Left, Y, DrawHeight, Row, 0);
  LastDrawn := PDF.GetTableLastDrawnRow(TableID);
  if LastDrawn >= TotalRows then
    Break;                       // 전체 표가 이 페이지에 맞음
  if LastDrawn < Row then
    Break;                       // 안전 장치: 더 이상 진행되지 않으므로 종료합니다
  PDF.NewPage;
  Row := LastDrawn + 1;          // 아직 그려지지 않은 첫 번째 행부터 계속합니다
  Y := PageHeight - Options.Top;
end;

이 루프는 반복되는 헤더가 위치하는 곳이기도 합니다. 각 연속 페이지에서 내보내기 도구는 데이터 행을 그리기 전에 선택적으로 1행(헤더)을 다시 그리며, 첫 번째 DrawTableRows 호출에서 반환된 높이를 사용하여 그 아래의 바디를 오프셋합니다. DrawTableRows가 다음 행 좌표가 아닌 높이를 반환하는 이유가 바로 이 때문입니다. 반환 값을 사용하면 크기를 하드코딩하지 않고도 다시 그려진 헤더와 이어지는 바디를 쌓을 수 있습니다

FastReport 또는 ReportBuilder 보고서를 PDF로 내보내려면 어떻게 해야 하나요?

보고서 브리지는 반대 방식을 취합니다. 즉, 레이아웃을 새로 만들지 않고 기존 레이아웃을 재현합니다. 각 브리지는 엔진의 고유한 내보내기 접점에 연결됩니다. PDFlibFRExportTfrxCustomExportFilter를 상속받아 FastReport의 메모, 그림, 도형 및 선 객체를 수신하고 각각을 PDFlibPas 프리미티브로 변환합니다. PDFlibRBDeviceTppFileDevice를 상속받아 ReportBuilder의 DrawCommand 목록을 순회합니다. PDFlibQRExport는 완전히 다른 세 번째 경로를 취하며, 이에 대해서는 다음 섹션에서 설명합니다. 세 가지 브리지 모두 헤드리스로 작동하므로 서버에서 이를 사용하는 주된 이유가 됩니다

헤드리스 작동이 거저 되는 것은 아니며, FastReport가 대표적인 경고 사례입니다. TfrxReport.Export는 프리뷰 페이지를 거치게 되며, 이 페이지는 필터의 ShowDialog 속성을 참조하는데 이 속성의 기본값은 True입니다. 대화형 데스크톱이 없는 서버 환경에서는 모달 대화 상자가 취소 결과를 반환하여 내보내기가 실패하게 됩니다. Export를 호출하기 전에 ShowDialogFalse로 설정하면 보고서가 화면 표시 없이 자동으로 렌더링됩니다. ReportBuilder에도 이와 유사한 스위치인 AllowPrintToFileShowPrintDialog가 있으며, 모든 작업을 Prepare를 통해 처리하는 QuickReport는 대화 상자 억제가 전혀 필요하지 않습니다

var
  Exporter: TPDFlibFRExport;
begin
  Report.PrepareReport;                  // 페이지를 먼저 빌드합니다
  Exporter := TPDFlibFRExport.Create(nil);
  try
    Exporter.FileName := 'invoice.pdf';
    Exporter.ShowDialog := False;        // 헤드리스: 모달 대화 상자를 건너뛰어 취소되지 않도록 합니다
    Report.Export(Exporter);
  finally
    Exporter.Free;
  end;
end;

세 가지 엔진, 세 가지 좌표계

각 엔진마다 단위를 측정하는 방식이 다르기 때문에 브리지는 자체적으로 좌표 연산을 수행합니다. PDFlibFRExport는 FastReport 객체 위치를 96 PPI 기준의 픽셀로 취급하고 96/25.4를 곱하여 밀리미터 단위로 변환합니다. PDFlibRBDevice는 ReportBuilder의 그리기 명령을 1/1000mm 단위로 읽어 1000으로 나누며, ppRegisterDevice를 통해 자신을 등록하므로 ppReport.DeviceType'PDFlibPas'로 설정하고 Print를 호출하는 것만으로 출력을 전달할 수 있습니다. PDFlibQRExport는 좌표 연산을 전혀 수행하지 않습니다. 준비된 QuickReport 페이지는 이미 EMF 메타파일이므로, 브리지는 각 페이지를 스트림에 저장하고 이를 ImportEMFFromStream에 공급하여 라이브러리의 디바이스 컨텍스트 및 메타파일 렌더링 경로가 모든 글리프와 라인을 배치하도록 합니다. 반면 FastReport 및 ReportBuilder 브리지는 네이티브 벡터 그리기 프리미티브와 네이티브 텍스트를 출력합니다

var
  PDF: TPDFlib;
begin
  PDF := TPDFlib.Create;
  try
    PDF.SetOrigin(1);
    // 보고서를 준비하고 한 번의 호출로 모든 페이지를 추가합니다. 브리지는
    // ImportEMFFromStream을 통해 각 페이지의 EMF를 PDF로 변환합니다.
    PDFlibQRExportReport(PDF, QuickRep1, 'ledger.pdf');
  finally
    PDF.Free;
  end;
end;

충실도(Fidelity)의 한계

이러한 브리지에 워크플로우를 적용하기 전에 한계를 명확히 알아야 합니다. 데이터셋 내보내기 도구는 이진 열을 자동으로 건너뛰므로, 임베디드 이미지를 표시해야 하는 보고서는 단순한 표와는 다른 접근 방식이 필요합니다. QuickReport 경로는 EMF를 거치기 때문에 텍스트가 벡터 글리프로 변환됩니다. 즉, 페이지는 올바르게 보이지만 선택하거나 검색할 수 있는 텍스트가 없으며, 접근성을 위한 태그가 지정된 PDF 구조도 포함되지 않습니다. FastReport 및 ReportBuilder는 실제 텍스트를 유지하지만, 일반적인 뷰(메모, 그림, 도형, 선)만 처리하고 특수한 객체는 경계 상자(Bounding box)로 대체하거나 건너뛰므로, 서식 있는 텍스트(Rich text), 바코드 또는 그라데이션 채우기 등을 사용하는 보고서는 디테일이 손실될 수 있습니다. 이는 제품 결함이 아니라 변환 레이어가 가진 한계입니다

가장 유의해야 할 운영상 경고 사항이 있습니다. 세 가지 엔진 중 어느 것도 losLab PDF Library 내부에 포함되어 제공되지 않으며, 브리지는 메인 빌드에서 의도적으로 제외되어 있습니다. 즉, 검색 경로에 FastReport VCL 6.x, QuickReport 8 또는 ReportBuilder 20이 이미 존재하는 프로젝트에서만 컴파일됩니다. 이 브리지들이 대상으로 하는 메타파일 및 DrawCommand 사양은 여러 메이저 엔진 버전에 걸쳐 안정적으로 유지되었지만, 버전 호환성은 사용자가 직접 관리해야 합니다. 버전 호환성이 올바르게 맞춰지면 임시 DBGrid 덤프부터 완성된 인보이스까지 두 가지 경로가 모든 요구 사항을 충족합니다. 테이블 내보내기 도구와 보고서 브리지는 모두 Delphi 및 C++Builder용 losLab PDF Library와 함께 제공됩니다