기술 문서

Delphi에서 PDFium Component로 PDF 뷰어 구축하기

Delphi의 PDF 뷰어는 두 개의 컴포넌트와 이들 사이의 연결로 귀결됩니다. TPdf는 문서를 소유하며 파일을 열고 해독하며 페이지 수와 메타데이터에 대한 쿼리에 응답합니다. TPdfView는 화면에 페이지를 그리고 스크롤, 확대/축소 및 사용자가 현재 보고 있는 페이지를 처리하는 시각적 컨트롤입니다. PDFium Component는 Chrome에 탑재된 것과 동일한 렌더링 엔진을 래핑하므로 캔버스에서 얻는 글리프, 안티앨리어싱 및 색상은 사용자가 이미 브라우저에서 보는 것과 일치합니다. 작업은 렌더링에 있는 것이 아닙니다. 문서 객체를 뷰에 연결하고, 손상되거나 암호로 보호된 파일에서 충돌 없이 불러오며, 사용자에게 뷰어를 완성된 느낌으로 만들어주는 몇 가지 컨트롤(페이지 넘기기, 확대/축소 변경, 창에 페이지 맞추기)을 제공하는 데 있습니다

이 글에서는 실제로 구축하는 순서대로 조립 과정을 살펴봅니다. 여기 있는 모든 것은 한 번에 한 페이지씩 렌더링하며, 이는 대부분의 문서 워크플로우가 원하는 방식입니다. 한 번의 연속 스크롤 열에 페이지를 쌓아야 한다면 이는 다른 레이아웃 결정이며 이 글에서 다루는 경로가 아닙니다

TPdf를 TPdfView에 연결하기

폼에 TPdfTPdfView를 놓은 다음 뷰에 어떤 문서를 표시할지 알려주세요. 그 단일 할당이 비시각적 문서와 이를 그리는 컨트롤 사이의 전체 링크입니다

procedure TFormMain.FormCreate(Sender: TObject);
begin
  // Pdf and PdfView were dropped at design time.
  PdfView.Pdf := Pdf;                 // the view paints whatever this document holds
  PdfView.FitMode := pfmFitWidth;     // start the user at a sensible zoom
end;

이 모든 것이 실행되기 전에 머신에 PDFium 네이티브 라이브러리가 있어야 합니다. PDFium Component는 대상 플랫폼에 따라 pdfium32.dll 또는 pdfium64.dll을 호출하며, DLL을 찾을 수 없으면 문서는 단순히 열기를 거부합니다. 실행 파일 옆에 일치하는 DLL을 배포하거나 시스템 로더가 찾을 수 있는 곳에 배치하세요. V8이 활성화된 빌드는 실행하려는 JavaScript가 포함된 PDF에만 존재하며, 일반 뷰어에는 해당되지 않으므로 구체적인 이유가 없다면 표준 DLL을 선택하세요

입력을 신뢰하지 않고 문서 불러오기

본능적으로 불러오기를 try/except로 감싸고 던져진 예외를 실패로 처리하려고 할 것입니다. 하지만 여기서는 그 본능이 틀렸으며, 이를 잘못 이해하면 누군가 손상된 파일을 넘겨주기 전까지는 멀쩡해 보이는 뷰어가 만들어집니다. Active := True를 설정해도 불러오기 실패 시 예외가 발생하지 않습니다. PDFium Component는 내부 오류를 포착하고 ActiveFalse 상태로 두므로, 문서가 열렸는지 아는 유일한 정직한 방법은 설정 후 속성을 다시 읽는 것입니다

procedure TFormMain.OpenDocument(const FileName: string);
begin
  Pdf.FileName := FileName;
  Pdf.Active := True;                 // never raises; failure leaves Active = False
  if not Pdf.Active then
  begin
    ShowMessage('Could not open ' + FileName);
    Exit;
  end;
  PdfView.PageNumber := 1;            // the view tracks its own current page
  UpdatePageLabel;
end;

두 가지에 주의해야 합니다. 첫째는 PageNumber가 두 객체 모두에 존재하며 둘은 독립적이라는 것입니다. Pdf.PageNumber는 문서의 현재 페이지 개념입니다. 반면 PdfView.PageNumber는 컨트롤이 실제로 표시하는 페이지이며, 사용자를 파일 내에서 이동시키기 위해 설정하는 값입니다. 하나를 설정한다고 해서 다른 하나가 이동하지 않으므로 뷰어는 항상 뷰의 속성을 통해 구동됩니다. 둘째는 1 기반 인덱싱입니다. 페이지는 0이 아니라 1부터 Pdf.PageCount까지 실행되므로, 0 기반 배열에 익숙한 사람이라면 주의해야 합니다

암호화된 파일 처리하기

암호화된 문서는 동일한 불러오기 경로에 포함됩니다. 활성화하기 전에 열기 암호가 설정되어 있으면 문서가 열릴 때 해독됩니다. 암호가 틀리거나 누락된 경우 손상된 파일과 마찬가지로 ActiveFalse로 유지됩니다. 따라서 복구 방법은 암호를 묻고 다시 활성화를 시도하는 것입니다

procedure TFormMain.OpenWithPassword(const FileName: string);
var
  Password: string;
begin
  Pdf.FileName := FileName;
  Pdf.Active := True;
  if not Pdf.Active then
  begin
    if InputQuery('Password required', 'Password:', Password) then
    begin
      Pdf.Password := Password;       // must be set before Active := True
      Pdf.Active := True;
    end;
    if not Pdf.Active then
    begin
      ShowMessage('Unable to open the document.');
      Exit;
    end;
  end;
  PdfView.PageNumber := 1;
end;

잘못된 암호와 손상된 파일 모두 조용히 실패하므로 Active 속성만으로는 이 둘을 구별할 수 없습니다. 실제로 이는 뷰어에서 수용할 수 있습니다. 사용자는 올바른 암호를 제공하거나 파일이 열리지 않는다는 것을 알게 되며 메시지는 어느 쪽이든 동일하게 읽힙니다

문서를 통해 페이징하기

문서가 열린 상태에서 탐색은 Pdf.PageCount로 제한된 PdfView.PageNumber에 대한 산술 연산입니다. 유일한 실제 작업은 범위를 고정(clamping)하여 버튼이 페이지를 범위 밖으로 밀어내지 않게 하고 파일의 끝에서 처음 및 마지막 버튼이 비활성화된 상태를 유지하도록 하는 것입니다

procedure TFormMain.GoToPage(NewPage: Integer);
begin
  if not Pdf.Active then
    Exit;
  if NewPage < 1 then
    NewPage := 1
  else if NewPage > Pdf.PageCount then
    NewPage := Pdf.PageCount;
  PdfView.PageNumber := NewPage;
  UpdatePageLabel;
end;

// the four navigation buttons reduce to one call each
procedure TFormMain.FirstClick(Sender: TObject);  begin GoToPage(1); end;
procedure TFormMain.PrevClick(Sender: TObject);   begin GoToPage(PdfView.PageNumber - 1); end;
procedure TFormMain.NextClick(Sender: TObject);   begin GoToPage(PdfView.PageNumber + 1); end;
procedure TFormMain.LastClick(Sender: TObject);   begin GoToPage(Pdf.PageCount); end;

"N 페이지로 이동" 텍스트 상자는 구문 분석된 정수에서 제공되는 것과 동일한 GoToPage 호출이며, 범위 고정은 사용자가 10페이지 분량의 파일에 9999를 입력하는 경우를 커버합니다. 표시되는 내용이 뷰가 보여주는 것과 어긋나지 않도록 "Page 3 of 12"와 같이 작성하는 유일한 위치로 UpdatePageLabel을 유지하세요

확대/축소: 명시적 비율 및 맞춤 모드

TPdfView의 확대/축소는 상호 작용하는 두 가지 방식으로 제공되며, 이 상호 작용을 이해하는 것이 제대로 작동하는 확대/축소 컨트롤과 사용자와 씨름하는 컨트롤의 차이입니다. 직접적인 경로는 100이 실제 크기를 의미하는 백분율인 Zoom 속성입니다. 다른 경로는 창 크기가 조정될 때 뷰가 알아서 확대/축소를 계산하고 계속 다시 계산하도록 지시하는 FitMode입니다

// fixed magnifications
PdfView.Zoom := 100;     // actual size
PdfView.Zoom := 50;      // half
PdfView.Zoom := 200;     // double

// let the view size the page to the window, and keep it sized on resize
PdfView.FitMode := pfmFitWidth;   // page width fills the control
PdfView.FitMode := pfmFitPage;    // whole page visible
PdfView.FitMode := pfmActualSize; // 1:1 with the document's points

이 부분에서 사람들이 헷갈리기 쉽습니다. Zoom을 직접 할당하면 FitModepfmNone으로 재설정됩니다. 이는 버그가 아니라 올바른 동작입니다. 사용자가 정확히 150%를 선택하는 순간, 두 요청이 충돌하기 때문에 뷰는 더 이상 "너비에 맞추기"를 따를 수 없습니다. UI에 대한 결과로 확대 버튼과 페이지 맞춤 버튼은 상호 배타적인 상태이며 도구 모음에서 활성 모드가 표시되어야 합니다. 사용자가 페이지 맞춤을 클릭하면 FitMode를 설정하고, 숫자 기반 확대를 클릭하면 Zoom을 설정하여 자체적으로 맞춤 모드가 해제되도록 하세요

확대/축소 슬라이더에 현재 맞춤 백분율을 적용하는 등의 목적으로 맞춤 값을 직접 계산하려는 경우, 페이지별 도우미가 모드를 변경하지 않고도 값을 제공합니다. PageWidthZoom[N], PageZoom[N]ActualSizeZoom[N]은 페이지 N을 너비에 맞추거나 전체로 맞추거나 실제 크기로 렌더링할 백분율을 반환합니다

// seed a zoom readout from the fit-to-width value of the current page
var
  FitPercent: Double;
begin
  FitPercent := PdfView.PageWidthZoom[PdfView.PageNumber];
  ZoomEdit.Text := Format('%.0f%%', [FitPercent]);
end;

완성된 뷰어에 실제로 필요한 것

위 뷰어는 수십 줄에 불과하지만 문서 워크플로우에 필요한 작업을 이미 수행합니다. 즉, 파일을 열고 잘못된 파일에서 살아남으며 페이지를 표시하고 페이지 사이를 이동하며 수동으로 또는 맞춤을 통해 배율을 변경합니다. PDFium은 어려운 부분을 조용히 처리합니다. 포함된 글꼴이 해결되고 주석과 양식 필드가 문서가 배치한 위치에 그려지며 보게 되는 페이지는 둘 다 같은 엔진으로 그리기 때문에 Chrome 사용자가 보는 것과 일치합니다

이 기반부터의 추가 사항은 구조적이라기보다는 점진적입니다. 텍스트 선택 및 검색은 PDFium이 이미 구축한 동일한 텍스트 레이어에서 읽어들입니다. Pdf.TitlePdf.Author와 같은 메타데이터는 단일 속성 읽기 거리에 있습니다. 회전과 회색조는 비트맵에 페이지를 그릴 때 전달하는 렌더링 옵션입니다. 이 중 어느 것도 여기 있는 문서 객체, 뷰, 그리고 이들을 연결하는 불러오기 후 탐색 흐름이라는 중추를 변경하지 않습니다. 이 중추를 올바르게 잡으면 나머지는 장식일 뿐입니다

전반적으로 사용된 TPdfTPdfView 컴포넌트는 Delphi 및 C++Builder용 PDFium Component의 일부로, 해당 제품 페이지에 전체 뷰어 참조가 포함되어 있습니다