기술 문서

델파이에서 PDFium 컴포넌트를 사용하여 PDF 문서 인쇄하기

PDF 좌표는 포인트 단위이고, 프린터 좌표는 디바이스 단위이며, 이 둘은 명시적으로 변환하기 전까지는 아무런 관련이 없습니다. 이러한 불일치는 델파이 애플리케이션에서 발생하는 대부분의 잘못된 인쇄 출력의 근본 원인입니다: 코드가 올바른 파일을 전송하더라도 페이지가 잘리거나 늘어나거나 빈 상태로 나옵니다. PDFium 컴포넌트는 렌더링 측면을 깔끔하게 처리하며, 프린터 배관은 표준 VCL입니다. 이 두 가지는 양쪽이 무엇을 기대하는지 이해하고 나면 약간의 코드로 함께 작동할 수 있습니다

렌더링 후 인쇄 파이프라인의 작동 방식

PDFium 컴포넌트는 프린터와 직접 통신하지 않습니다. 패턴은 다음과 같습니다: 원하는 해상도로 페이지를 TBitmap에 렌더링한 다음 StretchDIBits를 사용하여 해당 비트맵을 프린터의 캔버스로 전송합니다. TPdf.RenderPage는 호출자가 소유한 비트맵을 반환하므로 픽셀 크기를 제어할 수 있습니다. 옵션 세트에서 [rePrinting]을 전달하면 PDFium은 렌더링 경로를 LCD 하위 픽셀 힌팅과 같은 화면 전용 효과를 생략하고 인쇄 출력에 맞게 페이지의 MediaBox를 올바르게 처리하는 경로로 전환합니다. rePrinting을 생략하면 프린터로 전송되는 것은 화면 렌더링입니다. 이는 모니터에서는 괜찮아 보이지만 96 DPI 화면을 위해 내린 힌팅 결정이 300 또는 600 DPI 인쇄에 적합하지 않기 때문에 고해상도 프린터에서는 출력이 더 부드러워지는 경향이 있습니다

TPdf.Active는 페이지 속성을 건드리기 전에 확인해야 할 유일한 관문입니다. 컴포넌트는 오류를 조용히 무시합니다: 손상되거나 암호로 보호된 파일에서 Active := True로 설정해도 예외가 발생하지 않으며, 단지 ActiveFalse로 남겨둡니다. 할당 후에 항상 확인하세요. 비활성 문서에서 PageCountPageWidth를 읽으면 0이 반환되며, 이는 스풀러에 도달하면 진단하기 매우 어려운 조용한 아무 작업도 하지 않음(no-ops)을 생성합니다

최소 인쇄 루프

가장 단순한 작업 케이스는 파일을 로드하고 인쇄 작업을 열고 페이지를 반복한 다음 닫는 것입니다. 유일하게 까다로운 세부 사항은 첫 번째 페이지 전에 Printer.NewPage를 호출해서는 안 된다는 것이며, 따라서 FirstPage 플래그가 필요합니다. StretchDIBits 전송은 GetDIBSizesGetDIB를 거쳐 비트맵 핸들에서 디바이스 독립적 비트를 가져온 다음 전체 페이지 크기로 프린터 캔버스에 그립니다:

procedure PrintPdfFile(const FileName: string);
var
  Pdf: TPdf;
  I: Integer;
  Bitmap: TBitmap;
  InfoHeaderSize, ImageSize: DWORD;
  InfoHeader: PBitmapInfo;
  Image: Pointer;
  FirstPage: Boolean;
begin
  Pdf := TPdf.Create(nil);
  try
    Pdf.FileName := FileName;
    Pdf.Active := True;
    if not Pdf.Active then
      Exit;  // load failed silently; bail out

    Printer.Title := Pdf.Title;
    Printer.BeginDoc;
    try
      FirstPage := True;
      for I := 1 to Pdf.PageCount do
      begin
        if FirstPage then
          FirstPage := False
        else
          Printer.NewPage;

        Pdf.PageNumber := I;

        // Render at printer resolution; rePrinting adjusts the render path
        Bitmap := Pdf.RenderPage(
          0, 0,
          Printer.PageWidth,
          Printer.PageHeight,
          ro0,
          [rePrinting]
        );
        try
          GetDIBSizes(Bitmap.Handle, InfoHeaderSize, ImageSize);
          InfoHeader := AllocMem(InfoHeaderSize);
          try
            Image := AllocMem(ImageSize);
            try
              GetDIB(Bitmap.Handle, 0, InfoHeader^, Image^);
              StretchDIBits(
                Printer.Canvas.Handle,
                0, 0, Printer.PageWidth, Printer.PageHeight,
                0, 0, Bitmap.Width, Bitmap.Height,
                Image, InfoHeader^, DIB_RGB_COLORS, SRCCOPY
              );
            finally
              FreeMem(Image);
            end;
          finally
            FreeMem(InfoHeader);
          end;
        finally
          Bitmap.Free;
        end;
      end;
    finally
      Printer.EndDoc;
    end;
  finally
    Pdf.Active := False;
    Pdf.Free;
  end;
end;

Printer.PageWidthPrinter.PageHeight를 비트맵 크기로 전달하면 디바이스 DPI를 이미 고려한 프린터의 기본 픽셀 크기로 렌더링하게 됩니다. 그런 다음 StretchDIBits 호출은 해당 픽셀을 페이지에 1:1로 매핑합니다. 이렇게 하면 명시적인 DPI 계산 없이 달성할 수 있는 최고의 충실도를 얻을 수 있지만, PDF 페이지와 실제 용지의 크기가 우연히 같을 때만 작동합니다. 크기가 다를 경우 명시적인 배율 조정이 필요합니다

페이지와 용지 크기가 다를 때의 배율 조정

A4 세로 PDF 페이지는 자동으로 US Letter 프린터에 맞지 않으며, 세로 방향 프린터로 보낸 가로 페이지는 잘립니다. 표준 접근 방식은 프린터 픽셀 대 PDF 포인트의 비율에서 균일한 배율 계수를 계산한 다음 가로세로 비율이 유지되도록 두 치수에 모두 적용하는 것입니다. Pdf.PageWidthPdf.PageHeight는 현재 페이지 크기를 포인트 단위로 표시하며, 1포인트는 1/72인치입니다. 대상 DPI를 곱하고 72로 나누면 해당 해상도의 픽셀로 변환됩니다. 인쇄 가능한 영역 내에 여전히 들어맞는 가장 큰 배율을 얻으려면 X와 Y 비율 중 Min을 취하세요:

// Fit PDF page to printable area, preserving aspect ratio
var
  ScaleX, ScaleY, Scale: Double;
  DestWidth, DestHeight: Integer;
  Dpi: Integer;
begin
  Dpi := 300;  // target render resolution
  Pdf.PageNumber := PageIndex;

  ScaleX := Printer.PageWidth  / (Pdf.PageWidth  * Dpi / 72);
  ScaleY := Printer.PageHeight / (Pdf.PageHeight * Dpi / 72);
  Scale  := Min(ScaleX, ScaleY);

  // Clamp to 1.0 for shrink-to-fit only (no enlargement)
  if Scale > 1.0 then Scale := 1.0;

  DestWidth  := Round(Pdf.PageWidth  * Dpi / 72 * Scale);
  DestHeight := Round(Pdf.PageHeight * Dpi / 72 * Scale);

  Bitmap := Pdf.RenderPage(0, 0, DestWidth, DestHeight, ro0,
    [rePrinting, reAnnotations]);
  // ... transfer with StretchDIBits as above
end;

Dpi = 300으로 렌더링하는 것은 대부분의 사무용 프린터에 적합합니다. 600 DPI에서 단일 A4 페이지의 비트맵은 대략 3400만 픽셀이며, 이는 32비트 비트맵으로 약 100MB입니다; 일반 텍스트 문서에 대한 품질 향상은 최소인 반면 페이지당 메모리 비용은 상당합니다. 실제로 중요한 인쇄소나 벡터가 많은 기술 도면을 위해 600 DPI를 유지하세요

두 번째 코드 블록의 reAnnotations 플래그는 rePrinting과 독립적입니다. 사용자가 용지에 스탬프, 하이라이트 및 댓글 상자가 나타날 것으로 예상할 때 포함시키세요. 내용만 출력하려면 생략합니다. 두 플래그 모두 자유롭게 결합할 수 있습니다

페이지 회전

PDFium은 페이지 회전을 TRotation 값(ro0, ro90, ro180, ro270)을 반환하는 Pdf.PageRotation을 통해 액세스할 수 있는 /Rotate 항목으로 PDF에 저장합니다. 프린터 좌표계는 화면에 비해 90도 및 270도 회전을 반전시킵니다. 조정 없이 원시 PageRotation 값을 RenderPage에 직접 전달하면, 대부분의 Windows 프린터 드라이버에서 세로 문서에 포함된 가로 페이지가 거꾸로 인쇄됩니다. 해결책은 렌더 호출 전에 간단히 교체하는 것입니다: ro90ro270으로, ro270을 다시 ro90으로 매핑하고 ro0ro180은 변경하지 않은 상태로 둡니다

배포하기 전에 특정 대상 프린터에서 이 동작을 확인하세요. 회전과 관련된 드라이버 동작은 공급업체마다 일관되지 않으며 일부 드라이버는 GDI 수준에서 자체 회전 보정을 적용합니다. 이중 회전이 나타나면 교체를 제거하고 보정이 전혀 나타나지 않으면 교체를 추가하세요. 세로와 가로 페이지가 번갈아 나타나는 혼합 방향 문서는 테스트 중 발생하는 두 가지 실패 모드를 포착하는 가장 빠른 방법입니다

긴 인쇄 작업 전반에 걸친 메모리 관리

RenderPage를 호출할 때마다 호출자가 소유하고 해제해야 하는 새 TBitmap이 할당됩니다. 위의 루프에서 try/finally Bitmap.Free 블록은 한 번에 한 페이지씩 이것을 올바르게 처리합니다. 페이지 간에 비트맵을 누적하지 마세요: 200페이지 문서의 300-DPI 렌더링은 첫 번째 페이지가 스풀러에 도달하기 전에 기가바이트를 소모합니다. 다음 페이지로 넘어가기 전에 각 비트맵을 해제하세요

전송 블록 내부의 AllocMem / FreeMem 쌍도 동일한 규칙을 따릅니다. GetDIBSizes는 DIB 헤더 및 픽셀 데이터에 필요한 메모리 양을 알려줍니다; 한 페이지의 범위 내에서 모든 것을 할당, 채우기, 칠하기 및 해제합니다. 어느 블록이든 누출되도록 두면 인쇄 작업이 수십 페이지 이상의 문서에서 프로세스 힙을 소진하게 됩니다

백그라운드 스레드에서 인쇄 작업을 실행해야 하는 경우 TPdf 및 모든 VCL 프린터 호출을 동일한 스레드에 유지하세요. TPdf 자체는 PDFium DLL의 전역 상태를 공유하는 인스턴스 전반에 걸쳐 스레드로부터 안전하지 않습니다; 가장 안전한 모델은 스레드당 하나의 TPdf를 두고 각자 자체 파일 사본을 로드하는 것입니다

여기에 표시된 렌더링 및 문서 API는 Delphi 및 C++Builder용 PDFium Component의 일부입니다