임베디드 PDF 뷰어를 빌드할 때 흔히 발생하는 두 가지 불만 사항이 있습니다: 즉 초대형 문서를 열 때마다 매번 처음부터 다시 렌더링을 연산해야 한다는 점과, 프로그램 창을 고해상도(High-DPI) 모니터로 이동시키는 순간 텍스트가 심하게 흐려진다는 점입니다. losLab PDF Library는 TPDFlibViewer 컨트롤 상에서 이 두 문제에 대한 명확한 해법을 제시합니다: 즉 DiskCacheFolder 속성을 지정하면 문서 고유의 핑거프린트(fingerprint) 키에 기반한 세션 간 공유 디스크 페이지 캐시를 활성화하며, GetDpiForWindow 및 WM_DPICHANGED 메시지 핸들러로 제어되는 ScreenDPI 속성은 모니터별 고유 DPI 환경을 유연하게 자동 감지해 렌더링에 반영합니다
두 결함 모두 사용자가 화면에서 오작동이나 렉 현상을 즉각 인지할 수 있는 핵심 성능 지표입니다. 어제 열어서 스크롤바를 내릴 수 있게 될 때까지 약 8초가 걸렸던 400페이지짜리 스캔 계약서 문서가, 파일 내용이 완전히 동일함에도 오늘 다시 열었을 때 또다시 8초간 로딩을 먹통으로 유발하는 상황입니다. 또한 서로 다른 DPI 해상도를 쓰는 다중 모니터 컴퓨터 환경에서, 기본 모니터의 96 DPI 해상도로 드로잉되어 깨끗하게 보이던 뷰어 화면이 옆에 놓인 4K 모니터 지면으로 창을 드래그해 옮기는 순간 글자들이 뭉개지고 흐릿하게 바뀌는 일도 잦습니다. 본 기사에서는 TPDFlibViewer가 이 두 현상을 처리하는 구조를 살펴보고 아키텍처 의도를 짚어 봅니다. 만약 컨트롤 자체의 연동 기초 정보가 생소하다면 대화형 PDF 뷰어 컨트롤 소개 기사를 먼저 읽은 뒤 성능 튜닝 가이드인 본문을 살펴보십시오
Delphi에서 세션 간 렌더링된 PDF 페이지를 어떻게 캐시합니까?
TPDFlibViewer.DiskCacheFolder 속성에 특정 로컬 폴더 경로를 지정하면, 컨트롤은 렌더링된 페이지 비트맵 이미지 파일을 디스크 공간에 보존하여 최근 탐색한 지면 로드 시 렌더링을 재실행하지 않고 즉각 고속 호출해 줍니다. 저장 공간 상의 물리 배치는 <Folder>\PDFlibPas-PageCache\<문서 핑거프린트>\p<페이지번호>_z<줌키>.bin 형태를 취합니다: 즉 문서별 서브폴더가 잡히고 페이지번호와 줌 배율 조합별로 캐시 파일이 묶입니다. 모든 과정은 완전 자동이며 개발자가 직접 제어해야 하는 수동 플러시(flush) 함수 등은 존재하지 않습니다
uses
PDFlibViewer;
procedure TMainForm.FormCreate(Sender: TObject);
begin
FViewer := TPDFlibViewer.Create(Self);
FViewer.Parent := Self;
FViewer.Align := alClient;
// One line turns on the cross-session page cache
FViewer.DiskCacheFolder := 'C:\ProgramData\MyApp\PageCache';
FViewer.LoadFromFile('C:\Contracts\master-agreement.pdf', '');
end;
DiskCacheHits 카운터 속성은 현재 실행 세션 상에서 디스크 캐시를 통해 절약한 페이지 렌더링 횟수를 모니터링하여 실질 성능 향상 수치를 계측해 줍니다. 문서를 닫았다가 다시 오픈한 후, 화면 드로잉 연산 없이 스크롤되어 표시되는 지면이 늘어날 때마다 이 속성 수치가 일관되게 누적됩니다
캐시 키에 버전 식별자 대신 핑거프린트를 채택한 이유
문서 고유 핑거프린트는 파일명경로|파일크기|마지막수정시간 형태의 메타데이터 문자열 조합을 64비트 FNV-1a 해시 함수에 대입해 추출된 16진수 문자열입니다. 이 3단 조합이 완벽한 캐시 갱신(invalidation) 방어막이 됩니다: 파일에 미세한 수정이 가해지면 파일 크기나 수정 시간이 변동되어 해시값이 교체되므로, 뷰어는 조용히 다른 신규 캐시 서브폴더를 개설해 렌더링을 적립해 나갑니다. 기존의 유효하지 않은 옛 캐시 폴더는 탐색에서 완전 제외되며 추후 LRU 정리 로직에 의해 청소됩니다. 버전 태그를 수동 기입하고 갱신할 부담이 없으며, 오차 범위 내의 타임스탬프 비교 논리 버그를 낼 우려 없이 구버전 비트맵이 신버전 지면 위에 혼입될 수 있는 가능성을 구조적으로 원천 봉쇄합니다
이는 대다수의 현대식 빌드 시스템들이 리소스 무결성을 통제할 때 적용하는 "콘텐츠 식별 키를 캐시 키로 동화시키기" 논리와 같으며 결함 방지에 탁월한 효과를 보장합니다. 문서 파일 이름을 고대로 가져다 키로 쓰는 캐시 설계는 수동으로 파일 변경을 연동 확인해야 하며, 이러한 수동 확인은 백업 파일을 복구해 타임스탬프가 거꾸로 흐르거나, 우연히 파일 용량이 그대로 유지되거나, 플랫폼 간 대소문자 검출 기준이 어긋나는 등의 온갖 엣지 케이스 버그를 유발합니다. 폴더 이름 자체에 이 3단 정보를 해시로 박아 넣으면 프로시저 조건 분기 체크 없이 변경 유무가 구조적 사양으로 자연히 성립합니다
디스크 캐시 키에 회전(rotation) 각도 사양을 배제한 이유
캐시 파일 이름은 페이지 번호와 줌 배율 단계 정보만을 맵핑하며, 지면 회전(rotation) 정보는 의도적으로 캐시 키 구성에서 배제되어 있습니다. 디스크 캐시는 항상 회전이 가해지지 않은 기본 상태의 원본 드로잉 바이트 데이터만을 기록 보존하며, 회전 뷰 탐색이 켜져 있으면 디스크에서 무회전 상태의 캐시 데이터를 로드해 메모리에 올린 후 최종 단계에서 픽셀 연산을 수행해 돌려 줍니다. 만약 회전 정보가 디스크 캐시 키에 혼입되면, 사용자가 문서를 상하좌우(4가지 각도)로 돌려 볼 때마다 동일 지면 비트맵 파일이 디스크 공간에 4배로 중복 저장되어 저장 공간 낭비를 냅니다. 고비용의 렌더링 처리 대비 픽셀 방향 회전 연산은 아주 간단하고 값싸기 때문입니다
반면 런타임 메모리 비트맵 캐시 영역에서는 페인트 핸들러로 고속 전달되어 드로잉되어야 하므로 Round(Zoom * 1000) * 4 + Rotation div 90 공식으로 조합된 복합 키를 적용해 회전 정보가 가산된 상태로 보존됩니다. 백그라운드 선독(prefetch) 스레드도 이 연동 구조를 공유합니다: 즉 항상 회전되지 않은 기본 상태로 래스터 처리를 수행하여 단순 줌 배율 정보만으로 큐에 적재하고, 메인 스레드가 결과 데이터를 꺼내 갈 때 비로소 회전 처리를 가합니다. 이는 병렬 페이지 렌더링 및 스레드 안정성 기사에서 설명하는 메인 스레드 중심 캐시 통제 규칙과 일맥상통합니다. 메모리 저장소 하위 레벨 전체에서 회전 상태를 배제하면, 디스크 저장 등 비싼 자원이 드는 공간 상에서 페이지 및 배율당 단 하나의 표준 비트맵 데이터만 보존하는 구조가 완성됩니다
LRU 정리 기능으로 캐시 폴더 용량 제한하기
캐시는 최대 10개 문서 분량의 캐시 폴더를 상한으로 유지하며, 새 문서가 인입되면 가장 오랫동안 쓰이지 않은 폴더를 LRU(Least Recently Used) 알고리즘으로 자동 정리합니다. 사용 빈도는 각 캐시 폴더 내의 last-used.marker 파일의 타임스탬프 정보 갱신을 통해 분석되는데, 캐시 읽기/쓰기가 유발될 때마다 이 마커 파일의 시간을 새로고침하므로 자주 열어보는 문서는 비트맵 재기록 없이도 항상 안전한 우선순위 지표를 띱니다. 현재 뷰어에서 열려 감상 중인 타겟 폴더는 삭제 대상에서 당연히 제외되므로 지면을 표시하고 조작하는 도중 캐시 파일이 지워지는 결함은 발생하지 않습니다
핑거프린트 계산, 폴더 개설, 마커 파일 수정, 캐시 입출력, 정리 작업 등 디스크 캐시가 수행하는 모든 파일 입출력 과정 상에서 일어나는 예외(exception)는 코드 상에서 소리 없이 흡수(swallowed) 처리됩니다. 핑거프린트 계산, 폴더 개설, 마커 파일 수정, 캐시 입출력, 정리 작업 등 디스크 캐시가 수행하는 모든 파일 입출력 과정 상에서 일어나는 예외(exception)는 코드 상에서 소리 없이 흡수(swallowed) 처리됩니다: 즉 캐시는 렌더링 속도 향상을 위한 보조 가속기일 뿐이며, 디스크 용량이 가득 찼거나 백신 프로그램이 파일 락을 거는 등의 파일 쓰기 에러가 나더라도 "옛날 기본 드로잉 속도로 로드될" 뿐 "오류 팝업을 내며 페이지 출력이 정지되는" 참사로 번져서는 안 되기 때문입니다. 파일 에러 시 발생할 수 있는 최악의 결과는 단지 캐시를 못 읽고 새로 렌더링하는 것뿐입니다
procedure TMainForm.FormClose(Sender: TObject; var Action: TCloseAction);
begin
// How many render passes did the disk cache save this session?
Log(Format('Disk cache hits: %d', [FViewer.DiskCacheHits]));
end;
Delphi에서 PDF 뷰어를 어떻게 DPI-aware로 만듭니까?
TPDFlibViewer는 모든 DPI 종속 연산식을 단 하나의 핵심 필드를 거쳐 라우팅하도록 설계하여 모니터별 해상도 동기화를 대단히 수월하게 조율합니다. 레이아웃 배치, 사전 래스터 연산, 실 드로잉 코드 모두 단 하나의 FScreenDPI 필드 값을 읽어 처리합니다: 즉 100% 비율 기준 1포인트는 ScreenDPI / 72 크기의 화면 픽셀로 환산됩니다. 퍼블릭 속성인 ScreenDPI에 배율 값을 세팅하면 컨트롤은 알아서 기존 메모리 캐시를 버리고, 지면을 재배치하며, 스크롤 범위 정보를 동기화하고 윈도우를 다시 그리도록(invalidate) 완결 조율하므로 뷰어 연동이 아주 가벼워집니다. 이 해상도 픽셀 변환 로직은 모니터 드로잉 외에 프린터 장치 등으로 이송 렌더링할 때도 동일하게 고려되며, 이와 연관된 처리는 인쇄 미리보기 및 디바이스 컨텍스트 렌더링 기사에서 설명합니다
DPI 감지는 생성자(constructor)가 아닌 CreateWnd 재정의 함수 내에서 연동됩니다. GetDpiForWindow API 조회를 돌리려면 유효한 윈도우 창 핸들이 구비되어 있어야 하는데 생성자 호출 단계에서는 핸들이 아직 개설되지 않은 상태이기 때문입니다. CreateWnd 처리가 완료되기 전까지 컨트롤은 오작동을 막기 위해 96 DPI 기본 해상도로 가배치됩니다. GetDpiForWindow는 Windows 10 1607 이상부터 지원되므로, 컨트롤은 하위 호환성을 위해 GetProcAddress를 사용해 API 주소를 동적으로 바인딩하며 구형 Windows에서는 GetDeviceCaps(LOGPIXELSY) 호출로 폴백합니다 — 유닛 로딩 오류가 유발되지 않으며 구형 OS에서는 시스템 DPI 값을 가져와 원활히 연동합니다
프로그램 창을 4K 해상도 모니터로 드래그해 옮길 때의 제어 로직
DPI 개별 설정이 허용된 다중 모니터 컴퓨터 상에서 창 경계가 다른 해상도의 모니터로 교차 이동하는 순간 Windows OS는 WM_DPICHANGED 메시지를 프로그램으로 수신하며, TPDFlibViewer가 이 이벤트를 감지해 다이렉트로 가공 연산합니다. 메시지 수신 시 wParam 인수 정보로부터 새 해상도 수치를 확인해 SetScreenDPI에 대입하고(그 결과 맞지 않게 된 캐시 데이터를 폐기하고 뷰 영역을 재배치), 이후 lParam 인수로 넘어온 윈도우 크기 가이드 좌표 정보에 맞춰 SetWindowPos를 구동하여 이물감 없이 창의 물리 크기 비율을 보정 동기화합니다. 사용자는 OS에 의해 비트맵 드로잉이 강제 확대되어 이미지가 뭉개지는 렉 현상 없이, 화면 이동 시 새 해상도 포인트에 맞춰 또렷하고 선명하게 새로 그려진 완성형 지면을 마주하게 됩니다
// Normally you never touch ScreenDPI: CreateWnd detects the host monitor
// and WM_DPICHANGED tracks moves. Override it only for special targets,
// e.g. rendering the layout as if for a 150% display:
FViewer.ScreenDPI := 144; // clears the bitmap cache and rebuilds layout
배포 전 알아두어야 할 제약 조건
상용 배포 전 체크해야 할 3가지 실무 한계점이 있습니다. 첫째, WM_DPICHANGED 메시지는 실행 프로세스가 시스템 상에서 DPI-aware 세팅으로 동작 중일 때만 인입됩니다: 프로그램의 매니페스트(manifest) 설정 상에 모니터별 DPI 인식(최신 Windows의 경우 Per-Monitor v2) 설정이 등록되어야 하며, Delphi IDE 상에서는 프로젝트 옵션의 Application -> Appearance 메뉴에서 지정 가능합니다. 프로세스가 기본 시스템 고정 DPI 설정으로 돌고 있는 경우에는 OS가 해당 메시지를 주지 않으므로, 창 생성 시 감지한 최초 DPI 사양으로만 화면이 고정 DPI 사양으로만 화면이 고정 드로잉됩니다. 둘째, 디스크 캐시 기능은 속도를 높이기 위해 디스크 용량을 일부 점유합니다 — 10개 문서 분량의 다중 배율 비트맵 이미지는 적지 않은 용량을 취하므로 DiskCacheFolder 경로는 Roaming 사용자 프로필 영역이 아닌 충분한 저장 공간이 확보되는 곳으로 유도하십시오. 셋째, 캐시가 생성될 폴더에 대해 프로그램 실행 사용자 권한 상의 쓰기(write) 권한이 보장되어야 합니다. 쓰기 불통 시 에러 팝업 대신 캐시가 꺼진 일반 드로잉 모드로 작동하므로, 테스트 시 DiskCacheHits 수치가 오르는지 검사해 보아 캐시 기능 활성화 여부를 점검할 필요가 있습니다
이 두 사양을 결합해 적용하면 래스터 속도 최적화 코딩 작업을 크게 공들이지 않고도 뷰어의 완성도 체감을 획기적으로 향상시킬 수 있습니다: 사용자가 자주 열어보는 문서는 즉각 로드되며 모니터 이동 시에도 깨짐 없는 선명한 화질이 보장됩니다. 두 기능 모두 losLab PDF Library의 TPDFlibViewer 컨트롤 상에서 단순 속성 설정으로 바로 다룰 수 있어, 코어 엔진 커스터마이징 수고 없이 단 몇 시간 만에 손쉽게 탑재할 수 있습니다