기술 문서

losLab PDF Library로 델파이에서 RTF를 PDF로 변환

RTF는 아무도 계획하지 않은 자리에 나타날 만큼 오래 버텨 왔습니다. 예전 보고서 생성기, 메일 병합 파이프라인, 요즘 워드 프로세서보다 앞서는 법률 문서 보관소 같은 곳입니다. 그것을 즉석에서 PDF로 바꾸는 일은 되풀이되는 요구 사항이고, Windows에서 실제로 통하는 접근법은 전용 RTF 파서가 아니라 Windows 자신이 TRichEdit와 EM_FORMATRANGE로 이미 제공하는 렌더링 경로입니다. losLab PDF Library DLL 에디션은 그 파이프라인에 곧장 끼워지는 가상 장치 컨텍스트를 드러냅니다

작동 원리: 가상 DC와 EM_FORMATRANGE

Rich Edit 컨트롤은 물리적 프린터뿐 아니라 어떤 장치 컨텍스트에 대해서도 내용을 쪽으로 나눌 수 있습니다. EM_FORMATRANGE 메시지는 컨트롤에게 주어진 DC에 문자 범위를 배치하라고 알리고, 담아낼 수 있었던 마지막 문자의 위치를 돌려줍니다. 그것을 되풀이해 부르면서 cpMin을 매번 앞으로 밀면 쪽 단위 출력이 나옵니다. losLab PDF Library의 GetCanvasDC는 여러분이 지정한 쪽 치수에 맞춘 메모리 안 DC를 제공하고, 거기에 한 쪽을 렌더링한 뒤 LoadFromCanvasDc가 그 결과를 PDF 쪽으로 담아냅니다. 그것이 파이프라인 전부입니다

미리 제대로 해 둘 것이 하나 있습니다. TRichEdit 컨트롤은 대상 쪽에 맞게 크기를 잡아야 합니다. 컨트롤이 DC 치수보다 작거나 크면, 쪽 나눔이 PDF에 결국 담기는 것과 맞아떨어지지 않습니다. A4 출력이라면 표준적인 접근은 RTF 파일을 적재하기 전에 컨트롤의 픽셀 치수를 96 DPI에서 210 x 297 mm에 맞추는 것이며, DC 크기를 잡을 때 쓸 것과 같은 축척 도우미를 씁니다

PDF: RTF에서 PDF로 가는 파이프라인. 크기를 잡은 Rich Edit 컨트롤이 EM_FORMATRANGE로 자기 텍스트를 가상 캔버스 DC에 쪽으로 나누어 넣고, 그 통과마다 PDF 한 쪽으로 담긴다
EM_FORMATRANGE가 RTF 텍스트의 한 범위를 가상 캔버스 DC에 놓고 LoadFromCanvasDc가 그 결과를 담아내며, 마지막 문자에 이를 때까지 되풀이합니다

델파이 구현

아래 코드는 라이브러리의 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.

루프가 하는 일

PrintRtfBox는 TFormatRange 구조체를 채워 SendMessage로 Rich Edit 컨트롤에 넘깁니다. 컨트롤은 cpMin에서 시작해 문자를 렌더링하다가 DC가 차면 멈추고, 들어가지 못한 첫 문자의 위치를 돌려줍니다. 반환 값이 전체 텍스트 길이와 같거나 그것을 넘으면 모든 문자가 렌더링된 것이고, 함수는 0을 돌려주어 repeat...until 루프를 끝냅니다

반복마다 Output1.pdf, Output2.pdf 하는 식으로 이름 붙은 PDF 파일 하나가 나옵니다. 대신 여러 쪽짜리 문서 하나를 원한다면, 라이브러리의 쪽 덧붙이기 API로 나중에 조립하거나, 문서 세션 하나 안에서 AddPage를 부르도록 루프를 재구성할 수 있습니다. 위처럼 반복마다 SaveToFile 뒤에 RemovePdfDocument를 두는 방식은 최대 메모리를 한 쪽 분량의 내용으로 한정해 주며, 아주 긴 RTF 파일에서 중요합니다

사람들이 걸려 넘어지는 크기 잡기 세부

LoadFromCanvasDc에 주는 96 DPI 인수는 DC가 어느 화면 해상도에서 렌더링되었는지를 라이브러리에 알려, PDF 쪽을 위한 포인트 대 픽셀 대응을 옳게 계산하게 합니다. 이것을 틀리면 화면에서는 그림이 맞아 보여도 출력에서 글자가 엉뚱한 크기로 나타납니다

RcPage.Right와 RcPage.Bottom에 더한 +100은 컨트롤의 보이는 가장자리 너머로 둔 작은 여백입니다. Rich Edit는 쪽을 어디서 나눌지 정하는 데 rcPage 사각형을 씁니다. 그 여백이 없으면 경계에 정확히 걸치는 줄이 두 쪽에 걸쳐 겹칠 수 있습니다. 마법의 상수가 아닙니다. 쪽 경계가 마지막 픽셀이 아니라 컨트롤의 배치 영역 안쪽으로 말끔히 떨어질 만큼 크면 됩니다

PDF: EM_FORMATRANGE를 위한 중첩된 rcPage와 rc 사각형, 그리고 경계에 걸친 줄이 쪽에 걸쳐 겹치지 않게 하는 100픽셀 여백
rcPage는 컨트롤의 그리기 사각형 너머로 뻗으므로 쪽 나눔에 떨어지는 줄이 갈라지거나 겹칠 수 없습니다

끝으로, FormCreate가 돌 때 컨트롤이 이미 보이는 폼 창에 붙어 있어야 첫 SendMessage 호출 전에 그 창 핸들이 유효합니다. 실행 중에 동적으로 만든 TRichEdit는 폼이 아직 표시되지 않았다면 렌더링 루프가 시작되기 전에 HandleNeeded를 명시적으로 불러 주어야 합니다

글꼴과 RTF 기능 다루기

렌더링을 Windows Rich Edit 엔진이 하므로, 글꼴 대체는 그것이 표시와 인쇄에 쓰는 것과 같은 규칙을 따릅니다. RTF 파일이 참조하는 글꼴 중 컴퓨터에 설치된 것은 충실하게 렌더링되고, 없는 것은 말없이 대체되어 줄 길이와 쪽 나눔이 어긋날 수 있습니다. 생산 환경의 배치 변환에서는 이것을 명시적으로 시험해 볼 값어치가 있습니다. RTF 원본이 쓰는 서체마다 문서를 하나씩 적재해, 출력 쪽 수가 수동 인쇄 미리 보기에서 기대한 것과 맞는지 확인하십시오

표, 포함된 이미지, 그리고 대부분의 서식 있는 텍스트 기능은 Rich Edit가 네이티브로 렌더링하므로 별도 처리 없이 잘 됩니다. 놀라울 수 있는 한 영역은 트윕으로 표현된 사용자 지정 문단 간격이나 첫 줄 들여쓰기를 쓰는 텍스트입니다. Rich Edit의 내부 좌표계는 트윕(1/1440인치)인데, 여러분이 TFormatRange에 설정하는 DC 좌표는 현재 DPI의 픽셀입니다. 컨트롤이 내부에서 변환해 주지만, RTF를 프로그램으로 짓고 있다면 여백 값이 올바른 단위인지 확인해야 합니다

DPI 인식과 고 DPI 디스플레이

150% 배율(144 DPI)로 도는 디스플레이에서는 ScaleX(210, mmPixel)이 100% 디스플레이에서보다 큰 픽셀 수를 돌려줍니다. PDF Library는 여러분이 GetCanvasDC에 건넨 픽셀 치수를 그대로 기록하고, LoadFromCanvasDc의 DPI 인수로 PDF의 물리적 쪽 크기를 거꾸로 계산합니다. 건네는 DPI 값이 애플리케이션이 실제로 도는 DPI와 맞기만 하면, 디스플레이 배율과 무관하게 출력 쪽 크기가 옳습니다

애플리케이션이 DPI를 인식하지 않는다면(옛 기본값), Windows가 화면 DC를 배율 조정하므로 고 DPI 컴퓨터에서 픽셀 계산이 틀리게 됩니다. 가장 간단한 해결은 애플리케이션 매니페스트에 DPI 인식을 선언하는 것입니다. 그러면 애플리케이션이 진짜 장치 픽셀을 받고, LoadFromCanvasDc에 건네는 96은 GetDeviceCaps(GetDC(0), LOGPIXELSX)로 얻은 실제 디스플레이 DPI로 바꿔야 합니다. 위 코드 예제가 96을 박아 둔 것은 100% 배율 환경에 알맞고 예제를 짧게 유지하기 때문입니다

출력 구조: 쪽마다 파일 하나 대 합쳐진 문서 하나

위 루프는 각 쪽을 별도 PDF 파일에 씁니다. 그것이 원하는 바인지는 하류의 쓰임에 달려 있습니다. 보고서 생성 시스템은 나중에 쪽을 병합하거나 재배열해 최종 문서를 조립하므로 개별 쪽이 필요한 경우가 많습니다. 처음부터 PDF 하나를 원한다면, 라이브러리로 세션 하나 안에서 여러 쪽짜리 문서를 만들 수 있습니다. 문서를 루프 밖에서 한 번 만들고, 루프 안에서는 SaveToFile 대신 쪽 추가 메서드를 부르고, 루프가 끝난 뒤에 완성된 문서를 저장하십시오. 이러면 중간 파일이 없어지고, 단일 문서 변환 시나리오 대부분에 맞는 구조가 됩니다

PDF: 나중에 조립하려고 통과마다 PDF 파일을 하나씩 쓰는 방식과, 루프 안에서 쪽을 덧붙여 여러 쪽짜리 PDF 하나를 짓는 방식 사이의 선택
같은 렌더링 루프가 따로 떨어진 Output 파일들이든 자라나는 문서 하나든 먹여 주며, 하류의 유연성과 중간 파일을 맞바꿉니다

큰 RTF 파일에서는 루프에 진행 상황 표시를 얼마쯤 더할 값어치가 있습니다. 변환 속도가 쪽 수에 대략 비례하고 200쪽 문서는 몇 초가 걸릴 수 있기 때문입니다. repeat...until 구조는 늘리기 쉽습니다. 반복마다 LastChar를 RichEdit1.GetTextLen에서 얻은 전체 문자 수로 나누어 진행 막대를 갱신하며 문자 오프셋을 좇으십시오

여기서 보인 GetCanvasDC와 LoadFromCanvasDc 메서드는 델파이와 C++Builder를 위한 losLab PDF Library의 일부입니다