기술 문서

losLab PDF 라이브러리를 사용한 델파이에서의 RTF에서 PDF로의 변환

RTF는 현대의 워드 프로세서보다 오래된 레거시 보고서 생성기, 편지 병합(mail merge) 파이프라인, 법률 문서 보관소 등 아무도 예상치 못한 곳에서 등장할 만큼 충분히 오래 존재해 왔습니다. 이를 즉석에서 PDF로 변환하는 것은 반복적으로 요구되는 기능이며, Windows에서 실제로 작동하는 접근 방식은 전용 RTF 파서가 아니라 Windows 자체가 이미 TRichEditEM_FORMATRANGE를 통해 제공하는 렌더링 경로입니다. losLab PDF 라이브러리 DLL 에디션은 이 파이프라인에 바로 연결할 수 있는 가상 장치 컨텍스트(virtual device context)를 제공합니다

작동 메커니즘: 가상 DC와 EM_FORMATRANGE

Rich Edit 컨트롤은 물리적 프린터뿐만 아니라 모든 디바이스 컨텍스트(device context)에 대해 콘텐츠의 페이지를 매길 수 있습니다. EM_FORMATRANGE 메시지는 컨트롤에게 지정된 문자 범위를 주어진 DC에 배치하도록 지시하고, 맞출 수 있었던 마지막 문자의 위치를 반환합니다. cpMin을 매번 진행시키면서 이를 반복해서 호출하면 페이지별 출력을 얻을 수 있습니다. losLab PDF 라이브러리의 GetCanvasDC는 지정한 페이지 크기에 맞는 인메모리 DC를 제공합니다; 페이지를 렌더링한 후, LoadFromCanvasDc는 결과를 PDF 페이지로 캡처합니다. 이것이 전체 파이프라인입니다

미리 제대로 맞춰두어야 할 것이 하나 있습니다: TRichEdit 컨트롤의 크기는 대상 페이지와 일치해야 합니다. 컨트롤이 DC 치수보다 작거나 크면 페이지 매김이 PDF에 결국 나오는 결과와 맞지 않게 됩니다. A4 출력의 경우 표준 접근 방식은 RTF 파일을 로드하기 전에 DC 크기를 지정할 때 사용하는 것과 동일한 스케일 도우미를 사용하여 96 DPI에서 210 x 297 mm와 일치하도록 컨트롤의 픽셀 치수를 설정하는 것입니다

델파이 구현

다음은 라이브러리의 DLL 에디션을 래핑하는 PDFlibAX_TLB 가져오기 유닛을 사용합니다. 폼은 TRichEdit와 버튼을 호스팅합니다; 폼의 OnCreate 핸들러는 컨트롤의 크기를 지정하고 RTF를 로드하며, 버튼 클릭은 변환 루프를 주도합니다

unit MainUnit;

interface

uses
  Windows, Messages, SysUtils, Classes, Graphics, Controls, Forms,
  Dialogs, StdCtrls, ComCtrls, PDFlibAX_TLB, ActiveX;

type
  TForm1 = class(TForm)
    RichEdit1: TRichEdit;
    Button1: TButton;
    procedure FormCreate(Sender: TObject);
    procedure Button1Click(Sender: TObject);
  private
    function PrintRtfBox(hDc: HDC; rtfBox: TRichEdit;
      FirstChar: Integer): Integer;
  end;

var
  Form1: TForm1;
  PdfDoc: TPDFLibrary;

implementation

{$R *.dfm}

procedure TForm1.FormCreate(Sender: TObject);
begin
  PdfDoc := TPDFLibrary.Create(Self);
  // 페이지 매김이 DC와 일치하도록 화면 DPI에서 A4 크기로 컨트롤 조정
  RichEdit1.Width  := Round(ScaleX(210, mmPixel));
  RichEdit1.Height := Round(ScaleY(297, mmPixel));
  RichEdit1.Lines.LoadFromFile(
    ExtractFilePath(Application.ExeName) + 'document.rtf');
end;

procedure TForm1.Button1Click(Sender: TObject);
var
  Dc: HDC;
  PageNumber, LastChar, PdfDocId: Integer;
begin
  PageNumber := 1;
  LastChar   := 0;
  repeat
    // A4 크기의 가상 DC 가져오기
    Dc := PdfDoc.GetCanvasDC(
      Round(ScaleX(210, mmPixel)),
      Round(ScaleY(297, mmPixel)));
    // RTF 콘텐츠의 다음 페이지를 DC에 렌더링
    LastChar := PrintRtfBox(Dc, RichEdit1, LastChar);
    // DC 콘텐츠를 PDF 문서로 캡처
    PdfDoc.LoadFromCanvasDc(96, 0);
    PdfDocId := PdfDoc.SelectedPdfDocument;
    PdfDoc.SaveToFile(
      ExtractFilePath(Application.ExeName)
      + 'Output' + IntToStr(PageNumber) + '.pdf');
    PdfDoc.RemovePdfDocument(PdfDocId);
    Inc(PageNumber);
  until LastChar = 0;
end;

function TForm1.PrintRtfBox(hDc: HDC; rtfBox: TRichEdit;
  FirstChar: Integer): Integer;
var
  RcDrawTo, RcPage: TRect;
  Fr: TFormatRange;
  NextCharPosition: Integer;
begin
  RcPage.Left   := 0;
  RcPage.Top    := 0;
  RcPage.Right  := rtfBox.Left + rtfBox.Width  + 100;
  RcPage.Bottom := rtfBox.Top  + rtfBox.Height + 100;

  RcDrawTo.Left   := rtfBox.Left;
  RcDrawTo.Top    := rtfBox.Top;
  RcDrawTo.Right  := rtfBox.Left + rtfBox.Width;
  RcDrawTo.Bottom := rtfBox.Top  + rtfBox.Height;

  Fr.hdc         := hDc;
  Fr.hdcTarget   := hDc;
  Fr.rc          := RcDrawTo;
  Fr.rcPage      := RcPage;
  Fr.chrg.cpMin  := FirstChar;
  Fr.chrg.cpMax  := -1;

  NextCharPosition :=
    SendMessage(rtfBox.Handle, EM_FORMATRANGE, 1, LPARAM(@Fr));
  if NextCharPosition < Length(rtfBox.Text) then
    Result := NextCharPosition
  else
    Result := 0;  // 마지막 페이지를 알림
end;

end.

루프가 하는 일

PrintRtfBoxTFormatRange 구조체를 채우고 SendMessage를 통해 Rich Edit 컨트롤에 전달합니다. 컨트롤은 cpMin에서 문자를 렌더링하기 시작하여 DC가 채워지면 중단하고 들어가지 않은 첫 문자의 위치를 반환합니다. 반환 값이 전체 텍스트 길이 이상이면 모든 문자가 렌더링된 것이고 함수는 0을 반환하여 repeat...until 루프를 종료시킵니다

각 반복은 Output1.pdf, Output2.pdf 등으로 명명된 하나의 PDF 파일을 생성합니다. 대신 단일 다중 페이지 문서를 원한다면 라이브러리의 페이지 추가 API를 사용하여 사후에 취합하거나 단일 문서 세션 내에서 AddPage를 호출하도록 루프를 재구성할 수 있습니다. 위와 같이 반복당 SaveToFile을 수행하고 이어서 RemovePdfDocument를 호출하는 패턴은 최대 메모리를 1페이지 분량의 콘텐츠로 제한하며, 이는 매우 긴 RTF 파일에 중요합니다

LoadFromCanvasDc에 대한 96 DPI 인수는 DC가 렌더링된 화면 해상도를 라이브러리에 알려 PDF 페이지의 정확한 포인트 대 픽셀(point-to-pixel) 매핑을 계산할 수 있게 합니다. 이 설정이 잘못되면 이미지가 화면에 올바르게 보이더라도 출력에서 텍스트 크기가 잘못 표시됩니다

RcPage.RightRcPage.Bottom에 더해진 +100은 컨트롤의 표시 가장자리를 넘어선 작은 여백입니다. Rich Edit는 rcPage 사각형(rect)을 사용하여 페이지 분할 위치를 결정합니다. 여백이 없으면 경계에 정확히 떨어지는 줄이 두 페이지에 걸쳐 중복될 수 있습니다. 마법의 상수는 아닙니다: 페이지 경계가 마지막 픽셀이 아니라 컨트롤의 레이아웃 영역 안쪽에 깔끔하게 떨어질 만큼 충분히 크기를 원할 뿐입니다

마지막으로, FormCreate가 실행될 때 컨트롤은 이미 표시된 폼 창에 연결되어 있어야 첫 SendMessage 호출 전에 런타임에 동적으로 생성된 TRichEdit의 창 핸들이 유효하게 됩니다. 폼이 아직 표시되지 않은 경우 렌더링 루프가 시작되기 전에 명시적인 HandleNeeded 호출이 필요합니다

글꼴 및 RTF 기능 처리

렌더링은 Windows Rich Edit 엔진에 의해 수행되므로 글꼴 대체는 디스플레이 및 인쇄에 사용되는 것과 동일한 규칙을 따릅니다. 컴퓨터에 설치되어 있고 RTF 파일에서 참조된 글꼴은 충실하게 렌더링됩니다. 없는 글꼴은 조용히 대체되며 이로 인해 줄 길이나 페이지 매김이 변경될 수 있습니다. 프로덕션 일괄 변환의 경우 이는 명시적으로 테스트할 가치가 있습니다: RTF 소스에서 사용하는 각 서체(typeface)가 포함된 문서를 로드하고 출력 페이지 수가 수동 인쇄 미리보기에서 예상한 것과 일치하는지 확인합니다

표(tables), 포함된 이미지 및 대부분의 서식 있는 텍스트(Rich Text) 형식 지정 기능은 별도의 처리 없이도 작동합니다. 왜냐하면 Rich Edit가 이들을 기본적으로 렌더링하기 때문입니다. 놀랄 수 있는 한 가지 영역은 트윕(twips)으로 표현된 사용자 정의 단락 간격이나 첫 줄 들여쓰기를 사용하는 텍스트입니다: Rich Edit의 내부 좌표 시스템은 트윕(1/1440 인치) 단위인 반면 TFormatRange에 설정한 DC 좌표는 현재 DPI의 픽셀 단위입니다. 컨트롤은 내부적으로 변환을 수행하지만, 코드로 RTF를 구성하는 경우 여백 값이 올바른 단위인지 확인해야 합니다

DPI 인식(awareness) 및 고-DPI 디스플레이

150% 스케일링(144 DPI)으로 실행되는 디스플레이에서 ScaleX(210, mmPixel)는 100% 디스플레이에서보다 더 큰 픽셀 수를 반환합니다. PDF 라이브러리는 GetCanvasDC에 전달한 픽셀 치수를 기록하고 LoadFromCanvasDc의 DPI 인수를 사용하여 PDF의 물리적 페이지 크기를 역산합니다. 전달하는 DPI 값이 애플리케이션이 실행되는 DPI와 일치하는 한, 디스플레이 스케일링에 관계없이 출력 페이지 크기는 올바르게 됩니다

애플리케이션이 DPI를 인식하지 못하는 경우(이전 기본값) Windows는 화면 DC의 크기를 조정하며 고 DPI 시스템에서는 픽셀 계산이 잘못됩니다. 가장 간단한 수정 방법은 애플리케이션 매니페스트(manifest)에서 DPI 인식을 선언하는 것입니다. 그러면 애플리케이션이 실제 디바이스 픽셀을 수신하고, LoadFromCanvasDc에 전달하는 96은 GetDeviceCaps(GetDC(0), LOGPIXELSX)에서 얻은 실제 디스플레이 DPI로 교체되어야 합니다. 위 코드 예제는 100% 스케일링 환경에 적합하고 예제를 짧게 유지하기 위해 96으로 하드코딩되었습니다

출력 구조: 페이지당 하나의 파일 대 결합된 문서

위의 루프는 각 페이지를 별도의 PDF 파일에 씁니다. 원하는 것이 그것인지 여부는 다운스트림 용도에 따라 다릅니다. 보고서 생성 시스템은 페이지를 병합하거나 재정렬하여 나중에 최종 문서를 조립하기 때문에 종종 개별 페이지가 필요합니다. 처음부터 단일 PDF를 원하는 경우 라이브러리를 사용하면 단일 세션에서 여러 페이지가 있는 문서를 만들 수 있습니다: 루프 외부에서 문서를 한 번 생성하고 루프 내부에서 SaveToFile 대신 페이지 추가 메서드를 호출하며, 루프가 종료된 후 전체 문서를 저장합니다. 이것은 중간 파일을 피하게 해주며 대부분의 단일 문서 변환 시나리오에 적합한 올바른 구조입니다

대용량 RTF 파일의 경우 변환 속도가 대략 페이지 수에 비례하며 200페이지 문서는 몇 초가 걸릴 수 있으므로 루프에 진행 상태 피드백을 추가하는 것이 좋습니다. repeat...until 구조는 확장하기 쉽습니다: RichEdit1.GetTextLen에서 얻은 총 문자 수로 LastChar를 나누어 각 반복 후 진행 표시줄(progress bar) 업데이트에 문자 오프셋을 추적합니다

여기에 표시된 GetCanvasDCLoadFromCanvasDc 메서드는 Delphi 및 C++Builder용 losLab PDF 라이브러리의 일부입니다