Có hai phàn nàn thường xuất hiện ở bất kỳ trình xem PDF tích hợp nào: các tài liệu lớn phải dựng hình lại từ đầu mỗi khi khởi chạy, và văn bản bị mờ ngay khi cửa sổ được kéo sang một màn hình high-DPI. losLab PDF Library giải quyết cả hai vấn đề này trong control TPDFlibViewer của nó: property DiskCacheFolder kích hoạt bộ nhớ đệm trang trên đĩa hoạt động qua các phiên dựa trên vân tay tài liệu (document fingerprint), và property ScreenDPI, được hỗ trợ bởi GetDpiForWindow và một trình xử lý WM_DPICHANGED, mang lại cho control khả năng nhận biết DPI thực sự trên mỗi màn hình (per-monitor DPI awareness)
Cả hai vấn đề này đều đáng được xem xét nghiêm túc vì người dùng sẽ nhận thấy chúng ngay lập tức. Một bản hợp đồng scan dài 400 trang mất tám giây để có thể cuộn trang được vào ngày hôm qua, hôm nay lại mất tiếp tám giây nữa, mặc dù file không hề có thay đổi gì. Và trên một màn hình desktop có DPI hỗn hợp, một trình xem dựng hình ở độ phân giải 96 DPI của màn hình chính trông sẽ rất đẹp ở đó nhưng lại bị mờ rõ rệt trên màn hình 4K bên cạnh. Bài viết này sẽ hướng dẫn cách TPDFlibViewer giải quyết từng vấn đề và lý do tại sao thiết kế lại được lựa chọn như vậy. Nếu bạn mới làm quen với control này, hãy bắt đầu với tổng quan về control trình xem PDF tương tác và quay lại phần hiệu năng sau
Làm thế nào để lưu trữ bộ nhớ đệm các trang PDF đã dựng hình qua các phiên làm việc trong Delphi?
Gán một thư mục cho TPDFlibViewer.DiskCacheFolder và control sẽ lưu các bitmap trang đã dựng hình xuống đĩa, giúp tài liệu được xem gần đây mở lại lập tức thay vì phải dựng hình lại. Bố cục trên đĩa là <Folder>\PDFlibPas-PageCache\<document fingerprint>\p<page>_z<zoomkey>.bin: một thư mục con cho mỗi tài liệu, một file cho mỗi cặp kết hợp trang và thu phóng. Mọi thứ khác đều tự động; không cần gọi API cache nào và không có gì cần 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;
Bộ đếm DiskCacheHits báo cáo số lần dựng hình trang mà bộ nhớ đệm trên đĩa đã tiết kiệm được trong phiên hiện tại, giúp cho lợi ích này có thể đo lường được thay vì chỉ là phỏng đoán. Trên một tài liệu bạn đã đóng và mở lại, mỗi trang cuộn vào vùng nhìn thấy mà không cần chạy tiến trình dựng hình sẽ tăng bộ đếm này lên 1
Tại sao cache key lại là một vân tay (fingerprint) chứ không phải số phiên bản
Vân tay tài liệu (document fingerprint) là một mã băm FNV-1a 64-bit dựa trên chuỗi path|size|last write time, được định dạng dưới dạng mười sáu ký tự hex. Bộ ba thông tin đó là toàn bộ chiến lược vô hiệu hóa cache: khi file được chỉnh sửa, kích thước hoặc thời gian ghi cuối cùng thay đổi, mã băm thay đổi, và trình xem chỉ đơn giản là mở một thư mục con cache khác. Thư mục cũ không bao giờ được tham chiếu lại nữa và cuối cùng sẽ bị xóa theo cơ chế cắt tỉa LRU. Không có giao thức thẻ phiên bản (version-tag protocol) nào cần duy trì, không có logic so sánh timestamp nào có thể bị lệch, và không có cách nào để một bitmap cũ được hiển thị cho một tài liệu đã bị sửa đổi
Đây là cùng một ý tưởng "nhận diện nội dung làm cache key" mà các hệ thống build sử dụng, và nó chứng minh giá trị qua các chế độ lỗi mà nó loại bỏ. Một bộ nhớ đệm lưu trữ các file dưới tên thuần túy của tài liệu phải phát hiện các chỉnh sửa một cách rõ ràng, và mỗi lần kiểm tra rõ ràng là một lỗi tiềm ẩn trong các trường hợp đặc biệt: một file được khôi phục từ bản sao lưu với timestamp cũ, một thao tác lưu giữ nguyên kích thước, một đường dẫn được so sánh phân biệt chữ hoa-chữ thường trên máy này nhưng máy khác thì không. Việc băm cả ba tín hiệu này vào tên thư mục làm cho sự lỗi thời không thể xảy ra về mặt cấu trúc thay vì phải kiểm tra theo quy trình
Tại sao góc xoay lại được loại bỏ một cách có chủ đích khỏi disk key
Tên file cache mã hóa số trang và mức thu phóng, không có gì khác — góc xoay chế độ xem (view rotation angle) được loại bỏ một cách cố ý. Bộ nhớ đệm trên đĩa luôn lưu trữ các byte dựng hình chưa xoay; khi một trang trong cache được tải trong khi chế độ xem được xoay, các pixel sẽ được định hướng lại trong bộ nhớ sau khi giải mã. Nếu xoay là một phần của disk key, một người dùng xoay qua bốn góc xem sẽ ghi cùng một trang xuống đĩa bốn lần, làm tăng dung lượng lưu trữ cache lên gấp bốn lần mà không mang lại thông tin bổ sung nào, vì xoay là một thao tác chuyển đổi pixel rất nhẹ so với một tiến trình dựng hình đầy đủ
Bộ nhớ đệm bitmap trong bộ nhớ (in-memory bitmap cache) sử dụng một composite key, Round(Zoom * 1000) * 4 + Rotation div 90, bởi vì ở đó bitmap đã xoay chính xác là những gì trình xử lý vẽ (paint handler) cần. Luồng tìm nạp trước chạy ngầm (background prefetch thread) tuân theo cùng một quy tắc: nó luôn dựng các trang chưa xoay và báo cáo các zoom key thông thường, và luồng chính (main thread) sẽ áp dụng xoay khi xử lý kết quả — cùng một quy tắc "luồng chính sở hữu cache" được thảo luận trong bài viết về dựng hình trang song song và an toàn luồng. Việc loại bỏ góc xoay khỏi mọi thứ bên dưới lớp bộ nhớ giúp đảm bảo một bản dựng hình chuẩn cho mỗi trang và mức thu phóng, ở bất kỳ nơi nào cần tối ưu hóa dung lượng lưu trữ
Cơ chế cắt tỉa LRU giữ giới hạn thư mục cache như thế nào?
Bộ nhớ đệm giữ lại tối đa mười thư mục tài liệu và cắt tỉa những thư mục ít được sử dụng nhất khi có tài liệu mới. Độ mới được theo dõi bởi một file last-used.marker trong mỗi thư mục tài liệu với timestamp được cập nhật trên cả thao tác ghi và đọc cache, vì vậy một tài liệu bạn chỉ đọc lại vẫn được giữ ấm mà không cần ghi lại. Thư mục thuộc về tài liệu hiện đang mở được miễn trừ khỏi việc xóa, do đó việc cắt tỉa không bao giờ có thể xóa các trang ngay dưới chế độ xem đang hoạt động
Mọi thao tác trên đĩa trong cache — tạo vân tay, tạo thư mục, cập nhật marker, đọc và ghi trang, cắt tỉa — đều tự xử lý các exception của riêng nó. Đó là một thiết kế có chủ ý chứ không phải sự cẩu thả: bộ nhớ đệm là một bộ gia tốc nỗ lực tối đa (best-effort accelerator), và một ổ đĩa đầy, một thư mục chỉ đọc hoặc một khóa diệt virus phải hạ cấp trình xem về trạng thái "dựng hình bình thường", chứ không bao giờ để "lỗi không hiển thị được trang". Kết quả tồi tệ nhất mà một lỗi IO cache có thể gây ra chỉ là một lượt cache miss
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;
Làm thế nào để tạo một trình xem PDF nhận biết DPI (DPI-aware) trong Delphi?
TPDFlibViewer định tuyến mọi tính toán phụ thuộc vào DPI qua một trường duy nhất, và thiết kế một điểm thay đổi (single-change-point) này là điều giúp cho việc hỗ trợ per-monitor trở nên khả thi. Code bố cục, tìm nạp trước và dựng hình đều đọc một giá trị FScreenDPI duy nhất; ở mức zoom 100%, một point PDF ánh xạ tới ScreenDPI / 72 pixel. Trình thiết lập property ScreenDPI công khai sẽ xóa bộ nhớ đệm bitmap, xây dựng lại bố cục, cập nhật phạm vi cuộn và vô hiệu hóa cửa sổ, vì vậy thay đổi DPI chỉ là một phép gán với mọi đối tượng tiêu dùng tự động tuân theo. Phép toán pixel tương tự cũng rất quan trọng khi bạn dựng hình cho máy in thay vì màn hình, một chủ đề được đề cập trong bài viết về xem trước khi in và dựng hình ngữ cảnh thiết bị
Việc phát hiện xảy ra trong một override CreateWnd thay vì constructor, vì GetDpiForWindow cần một window handle hợp lệ và constructor chạy trước khi handle được tạo. Cho đến khi CreateWnd được kích hoạt, control giữ giá trị mặc định cũ là 96 để các phép toán bố cục duy trì tính nhất quán. Bản thân GetDpiForWindow là một Windows 10 1607+ API, do đó control liên kết động nó bằng GetProcAddress và quay về GetDeviceCaps(LOGPIXELSY) trên các hệ thống cũ hơn — unit vẫn tải ở mọi nơi, và Windows cũ hơn chỉ đơn giản là báo cáo hệ thống DPI
Điều gì xảy ra khi cửa sổ di chuyển sang màn hình 4K?
Windows gửi thông điệp WM_DPICHANGED khi một cửa sổ nhận biết per-monitor di chuyển qua một màn hình có scale factor khác, và TPDFlibViewer xử lý nó trực tiếp. Trình xử lý đọc DPI mới từ low word của wParam, gán nó thông qua SetScreenDPI — thao tác này sẽ loại bỏ bộ nhớ đệm bitmap đã sai và bố trí lại các trang — và sau đó chấp nhận hình chữ nhật cửa sổ được gợi ý mà Windows truyền trong lParam thông qua SetWindowPos, giúp giữ kích thước client hợp lý ổn định trong suốt quá trình di chuyển. Người dùng sẽ thấy tài liệu dựng hình lại sắc nét ở tỷ lệ mới thay vì bị OS phóng to pixel bitmap
// 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
Những giới hạn đáng lưu ý trước khi bạn phân phối ứng dụng
Có ba giới hạn thực tế đáng để xem xét trong quá trình duyệt thiết kế của bạn. Thứ nhất, WM_DPICHANGED chỉ đến nếu process được chọn: ứng dụng của bạn phải khai báo manifest nhận biết per-monitor DPI (per-monitor v2 trên Windows hiện tại), trong Delphi IDE là cài đặt DPI awareness dưới tùy chọn Application. Một process nhận biết system-DPI không bao giờ nhận được thông điệp này, và control khi đó sẽ dựng hình ở DPI nó phát hiện lúc tạo cửa sổ. Thứ hai, bộ nhớ đệm trên đĩa đánh đổi dung lượng đĩa lấy tốc độ — mười tài liệu chứa bitmap trang ở nhiều mức thu phóng khác nhau là một dung lượng lưu trữ thực tế, vì vậy hãy trỏ DiskCacheFolder đến một vị trí mà bạn thoải mái cho phép tăng dung lượng, không phải là một profile chuyển vùng (roaming profile). Thứ ba, thư mục cache cần quyền ghi cho người dùng đang chạy; nếu không có quyền này, trình xem vẫn hoạt động bình thường với số lượt hit bằng 0 — hãy kiểm tra DiskCacheHits khi test nếu bạn nghi ngờ cache không hoạt động
Cùng với nhau, hai tính năng này thay đổi chất lượng cảm nhận của một ứng dụng nặng về tài liệu nhiều hơn hầu hết các tối ưu hóa dựng hình khác: các tài liệu mà người dùng của bạn xem lại sẽ mở ngay lập tức, và hình ảnh hiển thị luôn sắc nét trên bất kỳ màn hình nào nó hiển thị. Cả hai đều được cung cấp dưới dạng các property thông thường của TPDFlibViewer trong losLab PDF Library, vì vậy việc áp dụng chúng chỉ mất một buổi chiều tích hợp thay vì một dự án xây dựng rendering-engine