편안하게 읽을 수 있는 배율로 렌더링된 단 한 장의 A4 페이지는 32비트 비트맵 기준으로 수 메가바이트의 메모리를 소모합니다. 이를 400페이지 분량의 계약서 문서로 치환해 곱셈을 해보면 산출되는 수치는 더 이상 추상적이지 않습니다: 즉 모든 페이지를 미리 렌더링해 두려면 사용자가 한 번에 화면 단위로만 볼 데이터에 대해 Windows 시스템에 1 기가바이트를 초과하는 비트맵 영역을 요청해야 합니다. 그 결과 32비트 빌드 환경에서는 프로그램이 메모리 주소 공간 부족으로 튕기거나, 혹은 사용자가 아직 스크롤하지도 않은 영역을 GPU와 페이지 파서가 읽어 들이느라 처음 몇 초 동안 화면이 얼어붙는 현상이 발생합니다. 연속 스크롤(continuous-scroll) 방식의 리더 프로그램은 페이지들이 세로로 길게 연결된 하나의 띠처럼 부드럽게 느껴져야 하지만, 실제로 그 모든 페이지의 비트맵 데이터를 메모리에 동시에 들고 있을 수는 없습니다
이러한 제약 조건을 어떻게 조율할지가 구현의 핵심입니다. PDFium Component는 TPdfView 내부에서 이를 자동으로 조율해 주므로, 개발자가 할 일은 적절한 디스플레이 모드를 선택하고 컴포넌트가 내부적으로 수행하는 예외 처리를 이해하는 것이 전부입니다. 컴포넌트가 대신 조율해 주지 않는 부분인 읽기 흐름에 맞춰 페이지 크기를 자동 제어하고 빠른 스크롤 조작에 기민하게 반응하도록 설계하는 영역은 약간의 코딩 작업을 요합니다. 만약 뷰어 주변부 구성 요소(툴바, 섬네일, 검색창 등)를 빌드 중이라면 기능 지원 뷰어 구현 가이드를 참고하십시오; 여기서는 스크롤 동작 자체에 집중해 다룹니다
레이아웃은 디스플레이 모드일 뿐, 비트맵들의 배치 패널이 아닙니다
VCL 폼 작업 시의 본능적인 생각은 스크롤 박스를 올리고 그 안에 페이지당 하나씩 이미지 컨트롤을 채워 배치하는 방식을 구상하게 유도합니다. 이 충동을 이겨내십시오. 그러한 구조는 페이지 위치 제어, 스크롤 수학 연산 및 메모리 한계점 극복 등의 난제를 개발자가 동시에 직접 감수하게 만들어, 바퀴를 불완전하게 다시 발명하는 결과로 이어집니다. TPdfView는 이미 문서를 페이지들의 연속적인 묶음으로 모델링하고 있으며 DisplayMode 속성을 통해 해당 레이아웃을 외부에 노출합니다
Pdf := TPdf.Create(Self);
PdfView := TPdfView.Create(Self);
PdfView.Parent := Self;
PdfView.Align := alClient;
PdfView.Pdf := Pdf;
PdfView.DisplayMode := dmSingleContinuous; // one page wide, scrolls vertically
Pdf.FileName := 'contract.pdf';
Pdf.Active := True;
if not Pdf.Active then
ShowMessage('Could not open the document');
이것이 연속 스크롤을 구현하는 설정의 전부입니다. dmSingleContinuous는 페이지 간의 간격을 내부적으로 조율하며 페이지들을 하나의 vertical 컬럼으로 가로정렬해 레이아웃을 형성하고, 뷰어는 이 컬럼 영역을 하나의 거대한 평면처럼 스크롤합니다. 일반적인 화면 탐색 조작을 위해 페이지별 컨트롤을 바인딩하거나 스크롤 핸들러 코드를 작성할 필요가 없습니다. 파일 명칭 대입 후 Pdf.Active 상태를 검사하는 부분에 주의하십시오: 문서를 열 때 예외가 발생하지 않으므로, 파일이 손상되었거나 암호가 지정된 경우에는 예외 발생 없이 Active 속성이 False 상태로 남게 되며, 이 검사를 유도하지 않은 뷰어 프로그램은 빈 화면을 출력하고 오작동하게 됩니다
동일한 속성을 통해 양면 펼침 보기 모드도 제공합니다. dmTwoPageContinuous는 책자 형태의 레이아웃이 어울리는 문서를 위해 페이지를 가로로 두 장씩 병렬 배치합니다; dmTwoPageContinuousWithCover도 유사하지만 홀짝 경계선의 자연스러운 일치를 위해 1페이지를 커버 페이지로 홀로 서게 배려합니다. 세 가지 모드 모두 세로 방향으로 연속 스크롤됩니다. 단 한 번의 속성 대입으로 모드를 전환할 수 있으므로 디스플레이 모드용 콤보 박스 메뉴 등을 손쉽게 덧붙일 수 있습니다
화면에 보이는 가시적 페이지들만 래스터화됩니다
이 방식이 400페이지의 대용량 파일까지 안정적으로 수용하는 비결은 해당 컬럼이 가상(virtual) 영역이기 때문입니다. TPdfView는 문서의 페이지 구조 트리 정보를 해독해 모든 페이지의 높이를 사전에 파악하고 있으므로, 어떠한 래스터화(rasterization) 처리 없이도 전체 스크롤 크기와 개별 페이지들의 배치 위치를 계산할 수 있습니다. 페이지 콘텐츠 스트림 데이터를 픽셀 이미지로 렌더링하는 고비용의 래스터화 과정은 현재 뷰포트(viewport) 화면과 겹쳐지는 페이지들과, 스크롤 유입 시의 끊김을 막기 위한 약간의 예비 영역 페이지들에 대해서만 선별적으로 수행됩니다. 아래로 스크롤을 내리면 화면에 유입되는 페이지들이 렌더링되고 화면을 벗어나는 페이지들의 비트맵 데이터는 메모리에서 해제됩니다. 메모리 점유량은 문서 전체 크기가 아닌 현재 화면에 출력되는 크기에 비례해 고정 유지됩니다
이 설계 메커니즘을 명확히 인지해야 컴퓨터 자원의 효율성을 합리적으로 평가할 수 있습니다. 400페이지 문서 열기 작업이 빠른 이유는 콘텐츠를 배제하고 문서 구조만 분석하기 때문입니다. 렌더링 비용은 페이지 단위로 분할되며, 사용자가 해당 페이지 부근으로 스크롤하는 시점에 지연 처리(lazily)됩니다. 열 때 즉각 반응하고 스크롤이 부드러운 뷰어 프로그램은 전체 작업량 자체를 줄인 것이 아니라, 사용자의 실제 탐색 동선에 맞춰 작업을 고르게 분할하고 지나간 영역의 데이터를 신속히 폐기하는 기법을 취한 것입니다. 결과적으로 개발자가 임의로 화면 외부의 페이지들을 미리 강제 렌더링할 필요가 없습니다. 화면 표시 결정을 뷰 객체의 제어에 전적으로 맡겨 처리하십시오
페이지 크기를 가로폭에 맞추고 배율 설정을 유지하십시오
가독성 확보를 위해 페이지 크기는 특정 배율에 고정되기보다는 패널 가로폭에 맞춰져야 합니다. FitMode 속성이 이 기능을 제공하며 윈도우 크기 조절 시에도 일관되게 배율을 제어해 줍니다
PdfView.FitMode := pfmFitWidth; // each page fills the column width; height follows
pfmFitWidth 모드에서 컴포넌트는 뷰 크기가 변경될 때마다 배율을 자동으로 다시 계산하므로, 세로 컬럼 영역이 항상 가용 폭을 가득 채우고 스크롤 범위가 이에 맞춰 유연하게 조정됩니다. 여기서 한 가지 주의할 함정이 있습니다: 즉 Zoom 속성에 값을 직접 대입하면 FitMode가 자동으로 pfmNone으로 초기화된다는 점입니다. 수동 배율 조절과 자동 가로 맞춤은 양립할 수 없기 때문에 의도된 기본 사양이나, 만약 코드 상에서 PdfView.Zoom := 1.0과 같은 코드가 오작동으로 호출되면 가로 맞춤 모드가 꺼져 윈도우 크기를 조절해도 레이아웃이 연동되지 않는 문제가 발생합니다. 배율 조절 메뉴와 가로 맞춤 메뉴를 동시에 제공한다면 이를 모드 전환식으로 조율하십시오: 즉 하나의 모드가 활성화되면 다른 모드를 클리어하여 오작동을 차단하십시오
절대 배율 제어를 구현할 때 사용하기 적합하도록 뷰 객체는 화면 맞춤 시의 배율 결과치를 속성을 통해 제공합니다: PageWidthZoom[PageNumber]는 해당 페이지를 가로폭에 맞추기 위한 배율 값을 반환하고, PageZoom은 페이지 전체를 화면 높이에 맞추기 위한 배율을 반환합니다. 이 속성 값들을 활용해 가로형 페이지나 비대칭형 페이지에 오작동할 수 있는 특정 고정 비율 수치를 수동 대입하지 않고도 "가로 맞춤" 및 "페이지 맞춤" 메뉴 기능을 깔끔하게 구현할 수 있습니다
점진적 렌더링을 통한 빠른 스크롤 시의 반응성 확보
기본 렌더링 경로는 페이지의 드로잉 처리가 완료될 때까지 대기했다가 반환됩니다. 단일 페이지 단위에서는 문제가 없습니다. 그러나 대용량 문서 상에서 스크롤바를 휙휙 드래그해 탐색할 때는 다릅니다: 화면을 스쳐 지나가는 모든 페이지마다 래스터화 처리가 유발되어, 스크롤 속도가 렌더링 속도보다 빠른 경우에는 렌더링 작업이 큐에 쌓여 뷰 패널에 렉이 걸리고 정작 드로잉이 끝났을 때는 해당 페이지가 이미 화면 밖으로 벗어난 상태가 되어 자원이 낭비됩니다. 해법은 렌더링을 중도 취소 가능(cancellable)하게 구현하여 사용자가 화면을 전환하면 즉시 드로잉 처리를 폐기하는 것입니다
RenderPageProgressive는 페이지 데이터를 청크(chunk) 단위로 나누어 렌더링하고 매 청크 경계에서 취소 토큰 상태를 체크하므로, 방금 화면을 벗어난 페이지의 렌더링 작업을 끝까지 진행하지 않고 중도에 안전하게 폐기할 수 있습니다
type
TFormMain = class(TForm)
// ...
private
FRenderCancel: IPdfCancellationTokenSource;
procedure RenderPageToBitmap(PageNo: Integer; Bmp: TBitmap);
end;
procedure TFormMain.RenderPageToBitmap(PageNo: Integer; Bmp: TBitmap);
var
Status: TPdfProgressiveStatus;
begin
// Cancel whatever was rendering; the old token is now signaled.
if Assigned(FRenderCancel) then
FRenderCancel.Cancel;
FRenderCancel := TPdfCancellationTokenSource.New;
Pdf.PageNumber := PageNo;
Status := Pdf.RenderPageProgressive(Bmp, 0, 0, Bmp.Width, Bmp.Height,
FRenderCancel.Token);
case Status of
prsDone: ; // bitmap is complete, paint it
prsCancelled: Exit; // superseded, discard this result
prsFailed: ShowMessage('Render failed for page ' + IntToStr(PageNo));
end;
end;
구현 시 가장 중요한 지표는 반환 값입니다. prsDone은 비트맵이 완벽히 드로잉되어 화면에 출력 가능한 상태임을 나타냅니다; prsCancelled는 스크롤 동작에 의해 이 페이지 렌더링이 supersede(대체)되었음을 뜻하므로 부분 완성된 이미지를 출력하지 않고 버립니다; prsFailed는 실제 페이지에 오류가 있음을 의미합니다. 취소 여부는 실시간 선점 방식이 아니라 청크 경계선 마다 확인(polling)하므로, Cancel을 호출한 후 드로잉이 실제 멈출 때까지 수십 밀리초 정도의 약간의 딜레이가 발생할 수 있습니다. 그럼에도 이는 쓸모없는 렌더링이 대기 큐를 틀어막아 생기는 렉 현상을 예방하는 데 대단히 효과적입니다. 토큰 인수에 nil을 전달하면 중도 취소 없이 완료 시까지 무조건 렌더링을 진행하며, 이는 인쇄 미리보기 화면과 같이 취소 조작이 필요 없는 단일 드로잉 상황에 적합합니다
새로운 TBitmap을 반환하는 함수 형태의 RenderPage를 호출할 때는 호출자 측에서 생성된 객체를 Free해 수동 해제할 책임이 있음을 주의하십시오. 스크롤 루프 내에서 페이지마다 비트맵을 매번 할당하고 해제를 잊어버리면 사용자가 화면을 탐색할 때마다 메모리 누수가 급격하게 축적되어, 메모리 상한선 극복이라는 본 스크롤 디자인의 의의 자체가 퇴색됩니다. 가능한 범위 내에서 기존 비트맵을 재사용(reused)해 렌더링을 유도하십시오
종합적인 구현 제언
연속 스크롤 뷰어의 본질적인 기능들은 대부분 컴포넌트 라이브러리 엔진이 내부에서 알아서 처리해 줍니다. dmSingleContinuous 모드를 정해 세로 정렬을 켜고, 윈도우 창 크기에 맞춰 reflow가 되도록 pfmFitWidth를 지정하며, 손상된 파일에 에러 대응을 하도록 Pdf.Active 상태 검사를 장착하면 준비가 끝납니다. 개발자가 직접 구현해 볼 가치가 있는 필수적인 추가 사양은 취소 가능한 점진적 렌더링의 연동입니다. 사용자가 긴 문서의 스크롤바를 맨 아래로 끌어내렸을 때 화면이 부드럽게 유지되는지가 제품 완성도의 척도가 되기 때문입니다. 이 스크롤 기능이 정상 확보되고 나면 다중 페이지에 걸친 텍스트 선택, 검색어 하이라이트 표시 및 책갈피 기능 등은 이 스크롤 영역 위에서 작동하는 인터페이스 응용 사양으로 손쉽게 구현해 나갈 수 있습니다
여기에 소개된 TPdfView, DisplayMode, 및 RenderPageProgressive API 등은 Delphi 및 Lazarus용 PDFium Component의 표준 내장 사양으로 제공됩니다