기술 문서

Delphi HotPDF Resolution: 드로잉 유닛과 UserWidth

HotPDF Component에서 THotPDF.Resolution은 드로잉 유닛을 정의합니다. 모든 X, Y 좌표와 모든 마진, SetFont에 넘기는 크기, TextWidth와 GetWideTextWidth의 결과가 1/Resolution 인치로 잡힙니다. THPDFPage.Width와 Height는 이를 따르지 않고 포인트에 남으므로, 레이아웃 경계는 읽기 전용인 UserWidth와 UserHeight에서 와야 합니다. Resolution을 건드리는 흔한 이유는 포팅입니다. 이미 1/96이나 1/144 인치로 생각하는 리포트 엔진은, 모든 호출 지점에 변환 인수를 붙이는 것보다 PDF 쪽이 같은 유닛을 쓸 때 옮기기 쉽습니다. 어떤 숫자가 새 유닛으로 갔고 어떤 숫자가 뒤에 남았는지 알고만 있으면 잘 동작합니다

THotPDF.Resolution이 실제로 바꾸는 것은?

THotPDF.Resolution은 HotPDF가 넘겨 받는 숫자를 읽는 방식만 바꿉니다. 쓰는 PDF는 같습니다. 세터는 두 줄입니다. SetResolution이 값을 저장하고 DocScale := Value / 72를 세팅합니다. 그 순간부터 XProjection과 YProjection은 콘텐츠 스트림으로 들어가는 길에서 모든 좌표를 DocScale로 나누고, SetFont는 기록하기 전에 크기를 같은 방식으로 나눕니다. PDF 유저 공간의 디폴트는 1/72 인치이므로(ISO 32000-1 §8.3.2.3), 디폴트 Resolution 72에서 투영은 identity이고 144에서는 드로잉 유닛 하나가 반 포인트입니다. /UserUnit 엔트리는 쓰이지 않습니다. PDF 1.6에 추가된 그 페이지 어트리뷰트는 HotPDF가 THPDFPage.SetUserUnit으로 노출하는 별개의 것입니다. TextOut 튜토리얼에서 오는 사람을 붙잡는 디테일 하나: 페이지 좌표는 왼쪽 위 코너에서 시작해 Y가 아래로 자랍니다. YProjection이 MediaBox 위에서 스케일된 Y를 빼기 때문인데, 이것은 모든 Resolution에서 참입니다

Delphi에서 THotPDF.Resolution이 드로잉 유닛을 정의하는 방식: 세터는 DocScale을 Resolution ÷ 72로 저장한 다음, XProjection, YProjection, SetFont가 콘텐츠 스트림으로 들어가는 길에서 모든 좌표와 크기를 나눕니다. Resolution 72는 identity 매핑이고 Resolution 144는 드로잉 유닛 하나를 반 포인트로 만들지만 페이지는 여전히 왼쪽 위에서 Y가 아래로 내려갑니다
출력 파일의 것은 하나도 움직이지 않습니다. 바뀌는 것은 넘기는 숫자의 의미뿐이니, 같은 콘텐츠 스트림이 72와 144에서 나타나는 이유입니다
var
  Pdf: THotPDF;
  Page: THPDFPage;
  Margin: Single;
  Title: WideString;
begin
  Pdf := THotPDF.Create(nil);
  try
    Pdf.FileName := 'invoice.pdf';
    Pdf.Resolution := 144;               // 드로잉 유닛 1 = 1/144 인치
    Pdf.BeginDoc;
    Page := Pdf.CurrentPage;             // A4: Width = 595, UserWidth = 1190
    Margin := 144;                       // 드로잉 유닛으로 1인치
    Page.SetFont('Arial', [fsBold], 28); // 28/144 인치, 14pt 폰트
    Title := 'INVOICE 2026-0417';
    // 같은 유닛으로 잰 페이지 가장자리에 우측 정렬
    Page.TextOut(Page.UserWidth - Margin - Page.GetWideTextWidth(Title),
      Margin, 0, Title);
    Page.SetLineWidth(2);                // 1pt 라인
    Page.MoveTo(Margin, Margin + 48);
    Page.LineTo(Page.UserWidth - Margin, Margin + 48);
    Page.Stroke;
    Pdf.EndDoc;
  finally
    Pdf.Free;
  end;
end;

Resolution 144에서 Page.Width가 내 좌표와 어긋나는 이유는?

THPDFPage.Width와 Height는 문서 Resolution이 뭐든 페이지를 포인트로 보고하는 반면, 여러분 좌표는 1/Resolution 인치입니다. 그래서 144에서는 페이지가 실제 절반 너비로 보입니다. A4 페이지는 Resolution 72에서 Width = 595, Height = 842를 읽히고 144에서도 여전히 595와 842를 읽히는데, 실제 오른쪽 가장자리는 X = 1190에 있습니다. v2.766.0에 추가된 UserWidth와 UserHeight는 Width * DocScale을 반환합니다. 그것이 여러분이 그리는 유닛의 페이지 크기입니다. 이들이 있기 전에는 라이브러리가 내부에서 둘을 섞었고, Resolution 144의 증상은 극적이었습니다. 문단이 글자마다 줄바꿈하고, THPDFTable.Render는 각 행을 새 페이지로 밀어 내고, HTML 임포터와 XFA 플래트너는 둘 다 콘텐츠를 절반 크기로 그려 평탄화된 폼이 왼쪽 위 코너에 뭉개졌습니다. 문단 레이아웃, 테이블 렌더링, HTML 임포트, EMF 센터링, WMF 페이지 클립, 레이아웃 진단은 이제 전부 유저 유닛 크기를 읽습니다. 여러분의 레이아웃 코드도 그래야 합니다. 드로잉 좌표와 비교하는 것(오른쪽 마진, 페이지 나눔 검사, 센터링 계산)은 UserWidth와 UserHeight에 속하지, Width와 Height에는 절대 속하지 않습니다

함정 하나: Width나 Height 대입은 페이지를 포인트로 바꿉니다

Page.Width나 Page.Height를 세팅하면 페이지가 조용히 UserDefined로 바뀌고, UserDefined 페이지는 DocScale을 완전히 무시하므로, 그 뒤에 그리는 모든 것은 1/Resolution 인치가 아니라 포인트입니다. 세터는 낡았고 설계상 포인트를 받으므로, 그 의미는 그대로 두었습니다. UserDefined 페이지의 투영은 단순 X + MinX이고, SetFont는 크기를 그대로 저장합니다. Resolution 144에서 결과는 내용이 갑자기 앞 페이지의 두 배 크기로 나오는 페이지입니다. 라이브러리 자신도 정확히 이 실수를 했습니다. 문단 계속 페이지는 예전에 이전 페이지 크기를 Width로 복사했고, 모든 넘침 페이지가 포인트로 갈아탔습니다. 그 페이지들은 이제 Size, Orientation, 페이지 Resolution을 복사하며, 원래 페이지가 이미 UserDefined였을 때만 Width와 Height로 폴백합니다

필요에 따라 나갈 길은 둘입니다. 표준 용지로 충분하다면 Page.Size와 Page.Orientation을 세팅하고 Resolution 유닛으로 계속 그리세요. 정말 커스텀 페이지 크기가 필요하다면 그것이 포인트 페이지임을 받아들이고 포인트로 그리세요. 그 지점에서 UserWidth는 Width와 같으므로, 항상 UserWidth를 읽는 레이아웃 코드는 두 종류의 페이지에서 모두 계속 동작합니다. 유닛 테스트가 이걸 못 박습니다. Resolution 144에서 A4 페이지는 UserWidth 1190을 보고하지만, Width := 500과 Height := 400 이후에는 500과 400을 보고합니다. 불러온 페이지도 같은 방식으로 동작합니다. 기존 PDF에서 재구성된 페이지는 포인트의 MediaBox만 알고 포인트로 그리기 때문입니다. 이 문서가 만든 페이지들은 CurrentPageNumber로 다른 페이지로 갔다 돌아와도 자기 유닛을 유지하며, 이것은 v2.766.26부터 그래 왔습니다

HotPDF에서 Resolution 144일 때 Page.Width가 여러분 좌표와 어긋나는 이유: Width와 Height는 포인트에 남는 동안 드로잉은 1/144 인치를 쓰므로, A4 페이지는 595로 읽히지만 오른쪽 가장자리는 UserWidth 1190에 있습니다. Width를 대입하면 페이지가 UserDefined로 바뀌어 DocScale을 무시하므로, 문단은 글자마다 줄바꿈하고 테이블은 행마다 끊기며 SetFont 크기는 절반이 됩니다
드로잉 좌표와 비교하는 것은 전부 UserWidth와 UserHeight에 속합니다. UserDefined 포인트 페이지에서는 둘이 일치하므로 같은 레이아웃 코드가 양쪽 다 살아남습니다

함정 둘: 폰트 크기는 왜 절반 크기로 나올까?

포인트로 시작한 폰트 크기가 Resolution 144에서 절반 크기로 나오는 이유는, SetFont가 크기 인자를 드로잉 유닛으로 다루고 저장 전에 포인트로 변환하기 때문입니다. 내부적으로 SetFont는 ASize / DocScale * DPI를 현재 폰트 오브젝트에 저장하므로, 저장된 값은 항상 포인트입니다. 라이브러리는 이걸로 두 번 넘어졌습니다. WideTextOutBoxEx의 폰트 폴백과 문단 계속 페이지가 둘 다 그 저장된 포인트 값을 SetFont에 되넘겨 두 번째 스케일로 텍스트를 절반으로 만들었습니다. 여러분 코드는 저장된 크기를 읽을 수 없지만, 어딘가의 포인트 값이 SetFont에 닿는 순간 같은 버그가 나타납니다. VCL 폼의 TFont.Size, 리포트 정의의 크기, CSS pt 길이 같은 것들이죠. 먼저 변환하고, 팩터에 페이지 자체의 Resolution과 UserDefined 케이스를 포함하세요. 메타파일 재생이 페이지 Canvas를 재생할 때 하듯이(그 경로는 HotPDF가 EMF와 WMF 벡터 그래픽을 임포트하는 방법 참고):

// 현재 페이지의 포인트당 드로잉 유닛. HotPDF가 쓰는 투영을 따름:
// Width/Height로 크기 정한 페이지는 1, 그 외에는
// (document Resolution / 72) * (page Resolution / 72)
function UnitsPerPoint(Pdf: THotPDF): Single;
begin
  if Pdf.CurrentPage.Size = UserDefined then
    Result := 1
  else
    Result := (Pdf.Resolution / 72) * (Pdf.CurrentPage.Resolution / 72);
end;

procedure SetFontFromVcl(Pdf: THotPDF; Font: TFont);
begin
  // TFont.Size는 포인트, SetFont는 드로잉 유닛을 기대
  Pdf.CurrentPage.SetFont(AnsiString(Font.Name), Font.Style,
    Font.Size * UnitsPerPoint(Pdf));
end;

라이브러리는 같은 규칙을 자기 포인트 상수에도 적용합니다. 모든 새 페이지가 시작하는 12포인트 폰트는 이제 내부 포인트당 유닛 팩터를 곱하므로, 어떤 Resolution에서든 12포인트입니다. 마진, 라벨 크기, 선 너비가 전부 하드코딩된 포인트인 DrawChart는 이제 스케일을 임시로 1로 두고 돕니다. 의도적으로 드로잉 유닛에 남는 것은 DrawQRCode 모듈 크기와 디폴트 테이블 폰트 크기 같은 공개 파라미터 디폴트입니다. 이들은 API 계약의 일부이므로, Resolution 144에서 72에서의 절반을 뜻합니다. 템플릿에서 리포트 크기를 잡는다면 HotPDF에서 폰트와 이미지로 리포트 출력하기 가이드가 그 값들이 보통 어디서 오는지 다룹니다

레이아웃이 Resolution 독립적인지 어떻게 검증할까?

가장 믿을 만한 검사는 바이트 비교입니다. 같은 페이지를 Resolution 72에서, 그리고 모든 좌표와 크기를 두 배로 하여 144에서 렌더링하면, 압축 안 된 콘텐츠 스트림이 동일해야 합니다. 두 런은 투영 뒤 같은 포인트 값에 착지하므로, 어떤 차이든 변환을 건너뛴 값입니다. HotPDF 테스트 스위트가 문단, 테이블, HTML 임포트, XFA 평탄화, 아크, 메타파일, 이미지를 검사하는 방법이 이것입니다. 거의 하네스 없이 여러분의 리포트 코드에도 같은 기법이 동작합니다:

procedure RenderPage(const FileName: string; Res: Integer; K: Single);
var
  Pdf: THotPDF;
begin
  Pdf := THotPDF.Create(nil);
  try
    Pdf.AutoLaunch := False;
    Pdf.Compression := cmNone;       // 읽을 수 있는 콘텐츠 스트림
    Pdf.FileName := FileName;
    Pdf.Resolution := Res;
    Pdf.BeginDoc;
    Pdf.CurrentPage.SetFont('Arial', [], 10 * K);
    Pdf.CurrentPage.TextOut(36 * K, 36 * K, 0, 'Line 1');
    Pdf.CurrentPage.Rectangle(36 * K, 60 * K, 200 * K, 40 * K);
    Pdf.CurrentPage.Stroke;
    Pdf.EndDoc;
  finally
    Pdf.Free;
  end;
end;

// RenderPage('r72.pdf', 72, 1)과 RenderPage('r144.pdf', 144, 2)는
// 바이트까지 같은 페이지 콘텐츠 스트림을 만들어야 합니다
HotPDF Delphi 코드에서 Resolution 독립성을 검증하는 방법: 같은 레이아웃을 두 번 렌더링합니다. 한 번은 스케일 1로 Resolution 72에서, 한 번은 모든 좌표와 폰트 크기를 두 배로 Resolution 144에서. 그다음 압축 안 된 콘텐츠 스트림이 바이트까지 같기를 요구합니다. 불일치는 Width로 UserDefined로 바뀐 페이지나 변환 안 된 포인트 값이 SetFont에 닿는 곳을 가리킵니다
두 런은 투영 뒤 같은 포인트 값에 착지하므로, 어떤 차이든 변환을 건너뛴 숫자입니다. HotPDF 테스트 스위트가 의지하는 같은 하네스죠

숫자를 실어 다니는 연산자를 검사하세요. Td, Tm, Tf, re, w, TJ 배열입니다. 파일 수준 바이트는 생성 날짜와 /ID에서 여전히 다르므로, 파일 전체가 아니라 스트림을 비교하세요. 불일치는 거의 항상 위의 두 함정 중 하나를 가리킵니다. Width로 크기가 바뀐 페이지, 또는 그대로 SetFont에 넘어간 포인트 값입니다. 드로잉 호출 자체가 처음이라면 크기, 스타일, 회전을 다루는 HotPDF TextOut 워크스루로 시작하고, 레이아웃이 UserWidth를 읽게 되면 돌아와 Resolution을 한번 바꿔 보세요. 전체 API 상세와 트라이얼 다운로드는 HotPDF Delphi PDF component 페이지에 있습니다