RTF는 아무도 계획하지 않은 자리에 나타날 만큼 오래 버텨 왔습니다. 예전 보고서 생성기, 메일 병합 파이프라인, 요즘 워드 프로세서보다 앞선 법률 문서 보관소 같은 곳입니다. 그것을 즉석에서 PDF로 바꾸는 일은 되풀이되는 요구 사항이고, Windows에서 실제로 통하는 접근법은 전용 RTF 파서가 아니라 Windows 자신이 TRichEdit와 EM_FORMATRANGE로 이미 제공하는 렌더링 경로입니다. losLab PDF Library는 그 파이프라인에 곧장 끼워지는 가상 장치 컨텍스트를 드러냅니다
작동 원리: 가상 DC와 EM_FORMATRANGE
Rich Edit 컨트롤은 물리적 프린터뿐 아니라 어떤 장치 컨텍스트에 대해서도 내용을 쪽으로 나눌 수 있습니다. EM_FORMATRANGE 메시지는 컨트롤에게 주어진 DC에 문자 범위를 배치하라고 알리고, 담아 낼 수 있었던 마지막 문자의 위치를 돌려줍니다. 호출을 되풀이하며 cpMin을 매번 앞으로 밀면 쪽 단위 출력이 나옵니다. losLab PDF Library의 GetCanvasDC는 여러분이 지정한 쪽 치수에 맞춘 메모리 속 DC를 제공하고, 한 쪽을 거기에 렌더링한 뒤 LoadFromCanvasDC가 그 결과를 새로운 한 쪽짜리 PDF 문서로 담아 냅니다. 그것이 파이프라인 전부입니다
미리 제대로 해 둘 것이 하나 있습니다. TRichEdit 컨트롤은 대상 쪽에 맞게 크기를 잡아야 합니다. 컨트롤이 DC 치수보다 작거나 크면 쪽 나눔이 PDF에 최종 담기는 것과 맞아떨어지지 않습니다. A4 출력의 표준적인 접근은 RTF 파일을 적재하기 전에, DC 크기를 잡을 때 쓰는 것과 같은 밀리미터-픽셀 변환으로, 나중에 LoadFromCanvasDC에 넘길 것과 같은 DPI에서 컨트롤의 픽셀 치수를 210 x 297 mm에 맞추는 것입니다
델파이 구현
다음은 라이브러리 ActiveX 에디션의 타입 라이브러리 임포트인 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: IPDFlibrary;
implementation
{$R *.dfm}
procedure TForm1.FormCreate(Sender: TObject);
begin
PdfDoc := CoPDFlibrary.Create;
// 쪽 나눔이 DC와 맞도록 컨트롤을 화면 DPI 기준 A4 크기로 잡습니다
RichEdit1.Width := MulDiv(210, Screen.PixelsPerInch * 10, 254);
RichEdit1.Height := MulDiv(297, Screen.PixelsPerInch * 10, 254);
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(
MulDiv(210, Screen.PixelsPerInch * 10, 254),
MulDiv(297, Screen.PixelsPerInch * 10, 254));
// RTF 내용의 다음 쪽을 DC에 렌더링합니다
LastChar := PrintRtfBox(Dc, RichEdit1, LastChar);
// DC 내용을 PDF 문서로 담아냅니다
PdfDoc.LoadFromCanvasDC(Screen.PixelsPerInch, 0);
PdfDocId := PdfDoc.SelectedDocument;
PdfDoc.SaveToFile(
ExtractFilePath(Application.ExeName)
+ 'Output' + IntToStr(PageNumber) + '.pdf');
PdfDoc.RemoveDocument(PdfDocId);
Inc(PageNumber);
until LastChar = 0;
// 컨트롤이 DC를 위해 캐시한 포맷 데이터를 내려놓게 한다
SendMessage(RichEdit1.Handle, EM_FORMATRANGE, 0, 0);
end;
function TForm1.PrintRtfBox(hDc: HDC; rtfBox: TRichEdit;
FirstChar: Integer): Integer;
var
Fr: TFormatRange;
Dpi, PageWidthTw, PageHeightTw, NextCharPosition: Integer;
begin
// rc와 rcPage는 픽셀이 아니라 트윕(1/1440인치)이다. 쪽 크기는
// DC와 컨트롤을 잡을 때 쓴 같은 DPI로 변환한다
Dpi := Screen.PixelsPerInch;
PageWidthTw := MulDiv(rtfBox.Width, 1440, Dpi);
PageHeightTw := MulDiv(rtfBox.Height, 1440, Dpi);
Fr.hdc := hDc;
Fr.hdcTarget := hDc;
Fr.rcPage := Rect(0, 0, PageWidthTw, PageHeightTw);
Fr.rc := Fr.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 파일 하나가 나옵니다. 대신 여러 쪽짜리 문서 하나를 원한다면 AddToFileList에 MergeFileList를 더해 나중에 쪽별 파일을 조립하거나, 루프를 재구성해 새 쪽 문서를 MergeDocument로 첫 문서에 접을 수 있습니다. 위의 반복마다 SaveToFile 뒤 RemoveDocument 패턴은 최대 메모리를 한 쪽 분량의 내용으로 한정해 주며, 아주 긴 RTF 파일에서 중요합니다
사람들이 걸려 넘어지는 크기 잡기 세부
LoadFromCanvasDC의 DPI 인수는 여러분의 픽셀 몇 개가 1인치인지를 라이브러리에 알려 줍니다. 쪽은 캔버스 너비를 DPI로 나눈 뒤 72를 곱한 포인트 폭으로 나오고, 높이도 마찬가지입니다. 이것을 틀리면 화면에서는 이미지가 올바로 보여도 출력에서 텍스트 크기가 틀리게 나옵니다
rc와 rcPage의 단위는 이 기법의 대부분의 Delphi 샘플이 틀리는 세부입니다. EM_FORMATRANGE는 그림을 그리는 DC가 픽셀로 동작하더라도 두 사각형을 트윕, 즉 1/1440인치 단위로 읽습니다. 픽셀 값을 채워 넣으면 컨트롤은 96 DPI에서 15배나 작은 쪽에 텍스트를 배치합니다. 통과마다 몇 단어씩, 그리고 10페이지 문서를 수백 개의 거의 빈 PDF로 바꾸는 루프가 되죠. DC를 잡은 DPI로 MulDiv(Pixels, 1440, Dpi) 변환을 하면 쪽 나눔이 캔버스가 실제 끝나는 곳에서 일어납니다. rcPage는 페이지 전체이고 rc는 텍스트가 차지할 수 있는 영역이라, 여백은 그 안에서 rc를 줄여 만들며 이것도 트윕입니다. 루프가 끝나면 nil lParam으로 EM_FORMATRANGE를 한 번 더 불러 컨트롤이 DC를 위해 캐시한 포맷 데이터를 해제하게 합니다
끝으로, FormCreate가 돌 때 컨트롤이 이미 보이는 폼 창에 붙어 있어야 첫 SendMessage 호출 전에 그 창 핸들이 유효합니다. 실행 중에 동적으로 만든 TRichEdit는 폼이 아직 표시되지 않았다면 렌더링 루프가 시작되기 전에 HandleNeeded를 명시적으로 불러 주어야 합니다
글꼴과 RTF 기능 다루기
렌더링을 Windows Rich Edit 엔진이 하므로, 글꼴 대체는 그것이 표시와 인쇄에 쓰는 것과 같은 규칙을 따릅니다. RTF 파일이 참조하는 글꼴 중 컴퓨터에 설치된 것은 충실하게 렌더링되고, 없는 것은 말없이 대체되어 줄 길이와 쪽 나눔이 어긋날 수 있습니다. 생산 환경의 배치 변환에서는 이것을 명시적으로 시험해 볼 값어치가 있습니다. RTF 원본이 쓰는 서체마다 문서를 하나씩 적재해, 출력 쪽 수가 수동 인쇄 미리 보기에서 기대한 것과 맞는지 확인하십시오
표, 임베드된 이미지, 대부분의 서식 있는 텍스트 기능은 Rich Edit가 네이티브로 렌더링하므로 별도 처리 없이 잘 됩니다. 의외일 수 있는 한 영역은 트윕으로 표현된 사용자 지정 문단 간격이나 첫 줄 들여쓰기를 쓰는 텍스트입니다. Rich Edit는 TFormatRange의 사각형과 같은 단위인 트윕(1/1440인치)으로 재는 반면, 그 밑의 DC는 현재 DPI의 픽셀로 동작합니다. 컨트롤이 그리는 동안 한쪽을 다른 쪽으로 매핑하지만, RTF를 프로그램으로 짓고 있다면 여백 값도 트윕인지 확인해야 합니다
DPI 인식과 고 DPI 디스플레이
150% 배율(144 DPI)로 돌아가는 디스플레이에서는 밀리미터-픽셀 변환이 100% 디스플레이보다 큰 픽셀 수를 돌려줍니다. PDF Library는 GetCanvasDC에 넘긴 픽셀 치수를 그대로 기록하고, LoadFromCanvasDC의 DPI 인수로 PDF 안의 물리적 쪽 크기를 역산합니다. 밀리미터를 픽셀로 바꿀 때 쓴 그 DPI 값을 그대로 넘기기만 하면, 디스플레이 배율과 무관하게 출력 쪽 크기는 올바릅니다
애플리케이션이 DPI 비인식(옛 기본값)이라면 Windows가 화면 DC를 배율 조정하므로 고 DPI 머신에서 픽셀 계산이 틀립니다. 가장 간단한 해결은 애플리케이션 매니페스트에 DPI 인식을 선언하는 것입니다. 그러면 애플리케이션이 진짜 장치 픽셀을 받고, 샘플이 LoadFromCanvasDC에 넘기는 Screen.PixelsPerInch 값이 100% 배율을 가정하는 대신 실제 디스플레이 DPI를 보고합니다
출력 구조: 쪽마다 파일 하나 대 합쳐진 문서 하나
위 루프는 각 쪽을 별개의 PDF 파일에 기록합니다. 그것이 원하는 것인지는 하류 용도에 달려 있습니다. 보고서 생성 시스템은 최종 문서를 나중에 쪽 병합이나 재정렬로 조립하므로 종종 개별 쪽이 필요합니다. 처음부터 PDF 하나를 원한다면 라이브러리가 한 세션에서 만들게 해 줍니다. 첫 LoadFromCanvasDC 호출이 만든 문서의 ID를 붙잡아 두고, 이후 통과마다 SelectDocument로 그것을 선택한 뒤 SaveToFile 대신 새 쪽의 문서 ID로 MergeDocument를 부르고, 루프가 끝난 뒤 한 번만 저장하세요. 중간 파일이 없어지고 대부분의 단일 문서 변환 시나리오에 맞는 구조입니다
큰 RTF 파일에서는 루프에 진행 상황 표시를 얼마쯤 더할 값어치가 있습니다. 변환 속도가 쪽 수에 대략 비례하고 200쪽 문서는 몇 초가 걸릴 수 있기 때문입니다. repeat...until 구조는 늘리기 쉽습니다. 반복마다 LastChar를 RichEdit1.GetTextLen에서 얻은 전체 문자 수로 나누어 진행 막대를 갱신하며 문자 오프셋을 좇으십시오
여기 보인 GetCanvasDC와 LoadFromCanvasDC 메서드는 Delphi와 C++Builder용 losLab PDF Library의 일부입니다