HotPDF RenderCacheFolder biến cache trang đã render trong bộ nhớ của component HotPDF Delphi thành một page cache đĩa bền vững: các trang đã render được ghi thành tệp PNG dưới một thư mục bạn chọn, và lần sau khi cùng nguồn PDF được mở lại, RenderLoadedPageToBitmapCached đọc chúng thay vì rasterize lần nữa. Thứ tự tra là bộ nhớ, rồi đĩa, rồi renderer
Tầng đĩa đã có trong API từ v2.416.0, nhưng cho tới v2.770.140 nó chưa bao giờ thật sự trả về một trang nào cho một lời gọi LoadFromFile hay LoadFromStream bình thường. Bản sửa buộc đặt ra một câu hỏi mà mọi cache bền vững đều phải trả lời: làm sao biết tệp bạn mở hôm nay chính là tài liệu bạn render hôm qua, và các trang đã cache sẽ ra sao khi chúng không phải? Dưới đây là những câu trả lời HotPDF đã chốt, gồm cả những chỗ nó cố tình từ chối cache
Cache render đĩa của HotPDF hoạt động ra sao?
Cache render đĩa của HotPDF là một tầng thứ hai đứng sau cache raster trong bộ nhớ, và nó chỉ tham gia khi RenderCacheFolder là một đường dẫn không rỗng. Một lời gọi RenderLoadedPageToBitmapCached(PageIndex, DPI) trước hết soát các entry trong bộ nhớ, đánh khóa bằng chỉ số trang, DPI và một biến thể render-settings. Trượt thì nó hỏi tầng đĩa; trúng đĩa thì decode PNG, đưa trở lại bộ nhớ và trả về một bản sao thuộc sở hữu người gọi. Chỉ khi cả hai tầng đều trượt, trang mới đi qua bộ diễn giải content-stream được mô tả trong render một trang PDF đã nạp ra TBitmap, và bitmap mới tinh cũng được ghi xuống đĩa
Trên đĩa, bố cục được giữ cố tình nhàm chán. Mỗi tài liệu có một thư mục con đặt tên từ một khóa tài liệu 16 ký tự hex cộng một biến thể render 16 ký tự hex, mỗi trang được lưu thành <page>@<dpi>.png, và một index.txt ở gốc giữ các tài liệu theo thứ tự dùng-gần-nhất đằng sau một thẻ schema. Lệch schema sẽ xóa sạch thư mục ngay lần dùng đầu. Lệnh ghi đổ vào một tệp tạm trước rồi được đổi chỗ vào vị trí bằng replace nguyên tử, nên sập giữa chừng chỉ để lại trang cũ hoặc không gì cả, chứ không bao giờ nửa vời một PNG. Một PNG decode thất bại bị xóa và tính là trượt
Ba giới hạn chặn khung thư mục:
RenderCacheMaxDocuments(mặc định 20) chặn số thư mục con tài liệu; thư mục ít dùng nhất bị đuổi trướcRenderCacheMaxBytes(mặc định 524288000, tức 500 MB) chặn tổng cỡ của mọi tệp PNG dưới gốc- Mỗi thư mục tài liệu giữ tối đa 200 ảnh trang; mức chặn theo từng tài liệu này do THotPDF cố định và không phải một property công khai
RenderCacheCapacity (mặc định 8) là một núm vặn riêng: nó đặt số trang đã render mà tầng bộ nhớ giữ, và chẳng liên quan gì tới dấu chân đĩa
uses
SysUtils, Graphics, HPDFDoc;
procedure WarmThumbnails(const FileName: string);
var
Pdf: THotPDF;
Bmp: TBitmap;
I: Integer;
begin
Pdf := THotPDF.Create(nil);
try
// Cấu hình tầng đĩa trước lần render cache đầu tiên:
// thư mục và cả hai giới hạn được đọc khi tầng được dùng lần đầu
Pdf.RenderCacheFolder := IncludeTrailingPathDelimiter(
GetEnvironmentVariable('LOCALAPPDATA')) + 'MyViewer\PageCache';
Pdf.RenderCacheMaxDocuments := 50;
Pdf.RenderCacheMaxBytes := Int64(1024) * 1024 * 1024; // 1 GiB
Pdf.RenderCacheCapacity := 16; // số trang trong bộ nhớ
if Pdf.LoadFromFile(FileName) > 0 then
for I := 0 to Pdf.LoadedPageCount - 1 do
begin
Bmp := Pdf.RenderLoadedPageToBitmapCached(I, 96);
if Bmp <> nil then
try
// Trao bản sao cho dải thumbnail tại đây
finally
Bmp.Free; // lời gọi cache luôn trả về bản sao thuộc sở hữu người gọi
end;
end;
finally
Pdf.Free; // kể từ v2.770.140, dòng này không còn xóa entry đĩa
end;
end;
Chạy cùng thủ tục đó hai lần, lượt thứ hai sẽ không rasterize trang nào vừa được cache chứa. Đối tượng cache đĩa được tạo lười ở lần render cache đầu tiên và sống cho tới khi instance THotPDF bị giải phóng, nên việc đổi RenderCacheFolder, RenderCacheMaxDocuments hay RenderCacheMaxBytes sau điểm đó không chuyển chỗ hay đổi cỡ một cache đang mở. Những trang quá lớn so với chính sách nhận vào bộ nhớ (theo mặc định, một entry không được vượt 64 MiB pixel 32-bit) cũng không được lưu xuống, và tầng đĩa chỉ được hỏi trong khi RenderFallbackPolicy giữ mặc định rfpIgnore, vì các chẩn đoán fallback không được lưu cạnh PNG
Vì sao RenderCacheFolder chẳng bao giờ chạy được trước v2.770.140?
RenderCacheFolder không có tác dụng trước v2.770.140 vì tầng đĩa đánh khóa tài liệu bằng một hash của các byte nguồn mà những lần nạp thường xuyên chẳng bao giờ giữ lại. Khóa tài liệu đến từ một SHA-256 trên bản sao nội bộ của các byte PDF thô, nhưng LoadFromFile và LoadFromStream parse nguồn tại chỗ và không giữ lại bản sao nào như thế; trường đó chỉ được điền tạm thời trên một đường phục hồi khi mã hóa rồi bị xóa ngay sau đó. Không có byte, khóa luôn rỗng, và khóa rỗng nghĩa là tầng đĩa bị bỏ qua. Không lỗi, không cảnh báo, chỉ một thư mục mãi trống rỗng
Việc làm khóa khác rỗng lôi ra một bug thứ hai vốn đang nấp sau bug đầu. InvalidateRenderedPageCache cũ xóa thư mục đĩa của tài liệu, mà InvalidateRenderedPageCache chạy ngay đầu mỗi lần nạp, sau mỗi lần sửa và trong Free. Vậy là vừa bắt đầu hoạt động, khóa đã khiến mọi phiên viewer tự hủy cache của mình khi thoát, và phiên kế tiếp lại khởi động lạnh như thường. Tệ hơn, khóa còn được tính lại từ cùng nguồn sau một lần sửa, nên các lần render của tài liệu đã sửa sẽ được lưu dưới khóa của tệp gốc rồi trả cho phiên kế tiếp mở phiên bản PDF chưa sửa. v2.770.140 sửa định danh và vô hiệu hóa cùng một lúc; sửa riêng một trong hai sẽ giao ra ngoài hoặc một cache chết trơ hoặc một cache nói dối
HotPDF nhận diện một PDF mà không đọc cả tệp thế nào
HotPDF nhận diện một PDF nạp từ tệp cục bộ bằng một dấu vân tay gồm kích thước, thời điểm ghi cuối cùng và 64 KiB đầu cùng 64 KiB cuối, còn một nguồn dạng stream hay truy cập ngẫu nhiên thì được nhận diện bằng SHA-256 trên toàn bộ nội dung. Cả hai đều được ghi nhận một lần, khi việc nạp thành công, và 16 ký tự hex đầu của digest SHA-256 (64 bit) trở thành khóa tài liệu
| Nguồn | Định danh | Chi phí | Ghi nhận khi nào |
|---|---|---|---|
LoadFromFile | Kích thước + LastWriteTime + 64 KiB đầu và cuối, băm bằng SHA-256 | Tối đa đọc 128 KiB, không phụ thuộc cỡ tệp | Mọi lần nạp thành công, kể cả khi RenderCacheFolder được đặt sau đó |
LoadFromStream | SHA-256 trên toàn bộ stream | Một lượt quét trọn nguồn | Chỉ khi RenderCacheFolder được đặt trước khi nạp |
LoadFromRandomAccessSource | SHA-256 trên toàn bộ nguồn | Một lượt quét trọn nguồn | Chỉ khi thư mục được đặt trước và toàn dải đều có sẵn |
Bất kỳ nguồn nào có entry /Encrypt | Không | Không | Không bao giờ; tầng đĩa bị bỏ qua |
Dấu vân tay tệp là một sự đánh đổi có chủ đích. Băm trọn vẹn một kho ảnh scan 400 MB mỗi lần mở có thể tốn hơn cả việc render hai trang mà người dùng thật sự xem. Những vùng được lấy mẫu không phải ngẫu nhiên: header nằm ở đầu tệp, còn trailer và phần cross-reference cuối nằm ở cuối (ISO 32000-1 §7.5). Một cập nhật tăng dần nối thêm phần thân mới, phần cross-reference và trailer mới (§7.5.6), nên nó đổi kích thước và phần đuôi cùng một lúc. Một lần viết lại trọn vẹn bởi bất kỳ công cụ bình thường nào thì đổi last-write time. Với tệp tới 128 KiB, hai mẫu phủ mọi byte, nên tài liệu nhỏ trên thực tế được băm trọn vẹn
Rủi ro còn sót là một thay đổi cùng cỡ, tại chỗ, ngay giữa một tệp lớn mà writer khôi phục lại timestamp gốc. Điều đó cần một công cụ cố tình giữ thời điểm sửa đổi trong khi sửa nội dung — hiếm nhưng không phải không thể — và trong trường hợp đó cache sẽ trả các trang cũ. Mặt còn lại thì lành: việc chép một tệp trên Windows thường giữ nguyên last-write time, nên một bản sao của tài liệu đã nằm trong cache trúng đúng các entry ấy, và điều đó đúng vì các byte là y hệt
Stream chẳng có thời điểm sửa đổi nào cả, nên định danh trung thực duy nhất chính là nội dung. HotPDF chỉ trả giá cho lượt SHA-256 trọn vẹn đó khi bạn đã đòi một cache đĩa trước khi nạp; mọi caller khác của LoadFromStream không thấy thêm chi phí nào. Điều đó khiến thứ tự gán property trở thành sống còn:
procedure OpenDownloadedPdf(Pdf: THotPDF; Data: TStream;
const CacheRoot: string);
begin
// Sai thứ tự với stream: hash nội dung chỉ được tính khi thư mục
// đã đặt sẵn, nên tài liệu này sẽ lách qua tầng đĩa
// Pdf.LoadFromStream(Data);
// Pdf.RenderCacheFolder := CacheRoot;
Pdf.RenderCacheFolder := CacheRoot; // đặt trước
Data.Position := 0;
if Pdf.LoadFromStream(Data) <= 0 then
raise Exception.Create('The stream is not a loadable PDF');
end;
Một nguồn truy cập ngẫu nhiên đang còn tải dở (vài dải chưa có sẵn) sẽ không nhận định danh thay vì một hash trên nội dung dang dở, và nếu việc tính định danh thất bại vì bất kỳ lý do nào, lần nạp vẫn thành công; tài liệu đơn giản render mà không có tầng đĩa
Điều gì vô hiệu hóa một entry cache đĩa của HotPDF?
Một entry cache đĩa của HotPDF không bao giờ bị vô hiệu hóa bằng cách xóa nó đi khi có thay đổi; thay vào đó, việc sửa tài liệu đã nạp làm rơi định danh tài liệu, nên tầng đĩa bị bỏ qua cho phần còn lại của lần nạp ấy và các trang đã lưu vẫn hợp lệ với nguồn chưa sửa. Entry chỉ rời đĩa qua các giới hạn LRU và số byte, một PNG hỏng, hay một lần đổi schema
Khóa mô tả một nguồn trên đĩa, không phải đồ thị object trong bộ nhớ. Một khi bạn đóng dấu lên một trang hay đổi một annotation, tài liệu không còn khớp nguồn ấy nữa, nên cả việc đọc lẫn ghi dưới khóa của nó đều sai. Kể từ v2.770.140, cả vô hiệu hóa cấp tài liệu lẫn cấp trang đều xóa định danh thay vì đụng tới thư mục, và còn có một lớp giữ thứ hai cho các thay đổi không gọi InvalidateRenderedPageCache: trước khi dùng tầng đĩa, THotPDF kiểm tra xem có object nào đang dirty hay không và coi một tài liệu dirty là không có định danh
Các thiết lập render thì ngược lại. Đổi PageRenderBackend (hay gọi UseNativeGDIRenderBackend), và gọi ConfigureRenderICCWorkflow hay ClearRenderICCWorkflow, xóa sạch các trang trong bộ nhớ nhưng giữ định danh, vì tài liệu vẫn khớp nguồn của nó. Những thiết lập ấy đổi pixel mà không nằm trong biến thể bộ nhớ, nên khóa đĩa gộp thêm tên backend, cờ black-point compensation và các digest SHA-256 của profile proof và output ICC. Bản thân biến thể đã phủ color intent, dither đầu ra, overprint preview, chế độ luminosity mask, chính sách fallback và trạng thái hiển thị của mọi nhóm optional content, nên bật tắt một layer sẽ render vào một thư mục khác thay vì ghi đè view mặc định
Muốn đưa một tài liệu đã sửa trở lại tầng đĩa, hãy trao cho nó một định danh nguồn mới bằng cách lưu nó rồi nạp kết quả:
procedure CommitEditsAndRekey(Pdf: THotPDF; const EditedFile: string);
begin
// Sau khi sửa tài liệu đã nạp: làm mới các trang trong bộ nhớ.
// Định danh nguồn đã biến mất, nên chẳng gì được đọc từ hay
// ghi vào thư mục đĩa của tài liệu gốc
Pdf.InvalidateRenderedPageCache;
// Tệp đã lưu có kích thước và last-write time mới, tức một định danh
// mới; các lần render sau lần nạp này được cache dưới key mới
Pdf.SaveLoadedDocument(EditedFile);
if Pdf.LoadFromFile(EditedFile) <= 0 then
raise Exception.Create('Could not reload the edited document');
end;
Thư mục của tài liệu gốc được để yên và già đi qua RenderCacheMaxDocuments cùng RenderCacheMaxBytes như mọi entry khác. Nếu người dùng mở lại bản gốc chưa sửa, các trang của nó vẫn nằm nguyên đó
Ranh giới bảo mật: nguồn mã hóa và thư mục liên kết
Cache render đĩa của HotPDF từ chối hai loại input một cách có chủ đích: nó không bao giờ ghi các trang của một PDF mã hóa xuống đĩa, và không bao giờ bám theo một thư mục tài liệu là junction hay reparse point loại khác. Cả hai quy tắc đều đổi lấy các lần trúng cache để không làm lộ dữ liệu hay xóa nhầm tệp
PDF mã hóa không bao giờ được cache trên đĩa
Một trang đã render là nội dung đã giải mã. Ghi nó thành PNG thường vào một thư mục cache nghĩa là để lại một bản sao đọc được của một tài liệu có mật khẩu trên đĩa, ngoài tầm bảo hộ mà tác giả đã chọn (ISO 32000-1 §7.6). Vì thế HotPDF không ghi nhận định danh cho bất kỳ nguồn nào có trailer mang entry /Encrypt, gồm cả tệp mở bằng mật khẩu hay bằng mật khẩu user rỗng. Những tài liệu đó vẫn dùng tầng bộ nhớ, thứ chết cùng tiến trình
Thư mục con dạng junction bị từ chối kể từ v2.770.173
Gốc cache là lựa chọn của bạn, và trỏ nó vào một junction là được phép. Các thư mục con tài liệu dưới đó là chuyện khác: cache tự mình tạo, đọc, đụng tới và xóa chúng — trong quá trình phục hồi lúc khởi động (nơi dọn các tệp tạm còn sót), tra cứu (nơi cập nhật timestamp), ghi, vô hiệu hóa và ba giới hạn đuổi entry. Nếu ai đó có quyền ghi vào gốc cache thay một thư mục tài liệu bằng một junction trỏ tới thư mục khác, mọi đường ấy sẽ bám theo nó, và việc đuổi entry sẽ xóa tệp ở một nơi mà cache chưa bao giờ sở hữu. Kể từ v2.770.173, mỗi điểm vào trong số ấy kiểm thuộc tính reparse-point và bỏ qua một thư mục tài liệu liên kết: một lần tra tính là trượt, một lần ghi tính là thất bại, còn việc đuổi entry để nó yên
Đường dẫn Unicode và gốc dùng chung
Hai bản sửa liên quan có ý nghĩa nếu bạn triển khai vào profile người dùng. Trước v2.770.135, RenderCacheFolder là một AnsiString, nên một thư mục ngoài code page hệ thống (ví dụ một tên người dùng Trung Quốc trên bộ Windows cài tiếng Anh) bị chuyển đổi mất mát trước khi cache kịp nhìn thấy; property giờ là string Unicode, và replace nguyên tử dùng wide Windows API. Kể từ v2.770.52, nhiều instance THotPDF trong một tiến trình trỏ cùng một gốc (sau khi mở rộng đường dẫn, so sánh không phân biệt hoa thường) dùng chung một index và một khóa đếm-tham-chiếu duy nhất. Trước đó, mỗi instance ghi đè index.txt bằng bản sao của mình và thực thi các giới hạn trên phần nhìn riêng phần của nó, nên thư mục có thể phình ra gấp vài lần so với ngân sách
Sự dùng chung ấy dừng lại ở biên tiến trình. Hai tiến trình riêng biệt trên cùng một gốc vẫn giữ các index bộ nhớ riêng, nên hãy trao mỗi ứng dụng đang chạy đồng thời một gốc cache riêng. Các viewer render trên worker thread thì ổn trong một tiến trình: PrefetchLoadedPages và hàng đợi được nói trong render nền với một hàng đợi yêu cầu đều đi qua cùng một đường cache và cùng một khóa
Tra nhanh: danh mục RenderCacheFolder
- Đặt
RenderCacheFolder,RenderCacheMaxDocumentsvàRenderCacheMaxBytestrước lời gọi đầu tiên tớiRenderLoadedPageToBitmapCached; với nạp stream và truy cập ngẫu nhiên, hãy đặt thư mục trước khi nạp - Nâng lên v2.770.140 trở lên nếu bạn dựa vào tầng đĩa; các bản cũ nhận property nhưng không bao giờ trả trang nào từ đĩa với các lần nạp bình thường
- Đừng mong cache đĩa cho các PDF mã hóa, cho tài liệu bị sửa sau khi nạp, hay trong lúc
RenderFallbackPolicykhông phảirfpIgnore - Giải phóng instance THotPDF như bình thường; kể từ v2.770.140, cả
FreelẫnInvalidateRenderedPageCacheđều không xóa entry đĩa - Đổi
PageRenderBackendhay ICC workflow giữ tài liệu trên tầng đĩa dưới một khóa khác - Dùng một gốc cache cho mỗi ứng dụng đang chạy; các instance trong một tiến trình dùng chung index kể từ v2.770.52
- Giữ gốc cache ở một vị trí theo từng người dùng; các thư mục con tài liệu dạng junction bị bỏ qua kể từ v2.770.173
Một page cache bền vững trả công tốt nhất ở một viewer mở lại cùng những tài liệu suốt ngày dài, đúng hình dạng của kiến trúc viewer PDF tùy chỉnh trong Delphi đã bàn ở một bài khác trên blog này. RenderCacheFolder, cache raster trong bộ nhớ và bộ render trang đi kèm HotPDF Delphi PDF component cho Delphi và C++Builder