HotPDF RenderCacheFolder zamienia pamięciowy cache wyrenderowanych stron komponentu HotPDF Delphi w trwały cache stron na dysku: wyrenderowane strony są zapisywane jako pliki PNG w folderze, który wybierzesz, a przy następnym otwarciu tego samego źródła PDF RenderLoadedPageToBitmapCached czyta je z powrotem zamiast rasteryzować od nowa. Kolejność szukania to pamięć, potem dysk, potem renderer
Warstwa dyskowa siedzi w API od v2.416.0, ale do v2.770.140 nigdy faktycznie nie wydała strony dla zwykłego wywołania LoadFromFile albo LoadFromStream. Poprawka wymusiła pytanie, na które musi odpowiedzieć każdy trwały cache: skąd wiesz, że plik otwarty dzisiaj to dokument, który renderowałeś wczoraj, i co się dzieje z cache'owanymi stronami, gdy nie jest? Poniżej odpowiedzi, na które osiadł HotPDF, łącznie z miejscami, w których cache'owania odmawia celowo
Jak działa dyskowy cache renderowania w HotPDF?
Dyskowy cache renderowania HotPDF to druga warstwa za pamięciowym cache rastrowym i bierze udział tylko wtedy, gdy RenderCacheFolder jest niepustą ścieżką. Wywołanie RenderLoadedPageToBitmapCached(PageIndex, DPI) najpierw skanuje wpisy w pamięci, kluczowane indeksem strony, DPI i wariantem ustawień renderowania. Przy chybieniu pyta warstwę dyskową; trafienie na dysku dekoduje PNG, promuje go z powrotem do pamięci i zwraca kopię należącą do wołającego. Dopiero gdy obie warstwy chybią, strona przechodzi przez interpreter strumienia treści opisany w renderowaniu wczytanej strony PDF do TBitmap, a świeża bitmapa jest potem zapisywana też na dysk
Na dysku układ jest celowo nudny. Każdy dokument dostaje podfolder nazwany 16-znakowym kluczem dokumentu w hex plus 16-znakowym wariantem renderowania w hex, każda strona jest trzymana jako <page>@<dpi>.png, a index.txt w korzeniu trzyma dokumenty w kolejności od ostatnio używanych za znacznikiem schematu. Niedopasowanie schematu czyści folder przy pierwszym użyciu. Zapisy lecą najpierw do pliku tymczasowego i są podmieniane na miejsce atomową zamianą, więc awaria w trakcie zapisu zostawia starą stronę albo nic — nigdy pół PNG. PNG, którego nie da się zdekodować, jest kasowany i liczony jako chybienie
Folder ograniczają trzy limity:
RenderCacheMaxDocuments(domyślnie 20) ogranicza liczbę podfolderów dokumentów; najdawniej używany folder jest wysiedlany pierwszyRenderCacheMaxBytes(domyślnie 524288000, czyli 500 MB) ogranicza łączny rozmiar wszystkich plików PNG pod korzeniem- Każdy folder dokumentu trzyma co najwyżej 200 obrazów stron; ten limit per dokument jest zaszyty w THotPDF i nie jest opublikowaną właściwością
RenderCacheCapacity (domyślnie 8) to osobne pokrętło: ustawia, ile wyrenderowanych stron trzyma warstwa w pamięci, i nie ma nic wspólnego z zajętością dysku
uses
SysUtils, Graphics, HPDFDoc;
procedure WarmThumbnails(const FileName: string);
var
Pdf: THotPDF;
Bmp: TBitmap;
I: Integer;
begin
Pdf := THotPDF.Create(nil);
try
// Skonfiguruj warstwę dyskową przed pierwszym renderem z cache:
// folder i oba limity są czytane przy pierwszym użyciu warstwy
Pdf.RenderCacheFolder := IncludeTrailingPathDelimiter(
GetEnvironmentVariable('LOCALAPPDATA')) + 'MyViewer\PageCache';
Pdf.RenderCacheMaxDocuments := 50;
Pdf.RenderCacheMaxBytes := Int64(1024) * 1024 * 1024; // 1 GiB
Pdf.RenderCacheCapacity := 16; // strony w pamięci
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
// Przekaż kopię do paska miniatur tutaj
finally
Bmp.Free; // wywołanie z cache zawsze zwraca kopię należącą do wołającego
end;
end;
finally
Pdf.Free; // od v2.770.140 to już nie kasuje wpisów dyskowych
end;
end;
Uruchom tę samą procedurę dwa razy, a drugi przebieg nie rasteryzuje już żadnej strony, która zmieściła się w cache. Obiekt dyskowego cache powstaje leniwie przy pierwszym renderze z cache i żyje do zwolnienia instancji THotPDF, więc zmiana RenderCacheFolder, RenderCacheMaxDocuments albo RenderCacheMaxBytes po tym momencie nie przenosi ani nie przemieszcza już otwartego cache. Stron zbyt dużych dla polityki przyjęć do pamięci (domyślnie pojedynczy wpis nie może przekroczyć 64 MiB pikseli 32-bitowych) nie utrwala się również, a warstwa dyskowa jest odpytywana tylko dopóki RenderFallbackPolicy trzyma domyślne rfpIgnore, bo diagnostyka fallbacku nie jest przechowywana obok PNG
Dlaczego RenderCacheFolder nigdy nie działał przed v2.770.140?
RenderCacheFolder nie dawało efektu przed v2.770.140, bo warstwa dyskowa kluczowała dokumenty hashem bajtów źródła, którego zwykłe wczytania nigdy nie trzymały. Klucz dokumentu pochodził z SHA-256 nad wewnętrzną kopią surowych bajtów PDF, ale LoadFromFile i LoadFromStream parsują źródło w miejscu i takiej kopii nie zatrzymują; pole było wypełniane tylko czasowo na ścieżce odzyskiwania szyfrowanych plików i zaraz potem czyszczone. Bez bajtów klucz był zawsze pusty, a pusty klucz znaczy, że warstwa dyskowa jest omijana. Żadnego błędu, żadnego ostrzeżenia — po prostu folder, który zostawał pusty
Niepusty klucz odsłonił drugiego błęda, który do tej pory krył się za pierwszym. Stary InvalidateRenderedPageCache kasował dyskowy folder dokumentu, a InvalidateRenderedPageCache wykonuje się na początku każdego wczytania, przy każdej edycji i wewnątrz Free. W chwili, gdy klucz by zadziałał, każda sesja przeglądarki niszczyłaby własny cache przy wyjściu, a następna sesja i tak startowałaby na zimno. Gorzej: klucz był przeliczany z tego samego źródła po edycji, więc rendery edytowanego dokumentu lądowałyby pod kluczem oryginalnego pliku i byłyby serwowane następnej sesji, która otworzyła niezmodyfikowany PDF. v2.770.140 naprawia tożsamość i unieważnianie razem; naprawa tylko jednej z tych rzeczy wydałaby martwy cache albo kłamiący
Jak HotPDF identyfikuje PDF bez czytania całego pliku
HotPDF identyfikuje PDF wczytany z pliku lokalnego odciskiem palca złożonym z rozmiaru, czasu ostatniego zapisu oraz pierwszych i ostatnich 64 KiB, a źródło strumieniowe albo o dostępie losowym — SHA-256 całej treści. Oba są przechwytywane raz, gdy wczytanie się uda, a pierwsze 16 znaków hex skrótu SHA-256 (64 bity) staje się kluczem dokumentu
| Źródło | Tożsamość | Koszt | Kiedy przechwycona |
|---|---|---|---|
LoadFromFile | Rozmiar + LastWriteTime + pierwsze i ostatnie 64 KiB, hashowane SHA-256 | Odczyt co najwyżej 128 KiB, niezależny od rozmiaru pliku | Każde udane wczytanie, nawet gdy RenderCacheFolder ustawi się później |
LoadFromStream | SHA-256 całego strumienia | Jeden pełny przebieg po źródle | Tylko gdy RenderCacheFolder był ustawiony przed wczytaniem |
LoadFromRandomAccessSource | SHA-256 całego źródła | Jeden pełny przebieg po źródle | Tylko gdy folder był ustawiony najpierw i cały zakres jest dostępny |
Każde źródło z wpisem /Encrypt | Brak | Brak | Nigdy; warstwa dyskowa jest omijana |
Odcisk palca pliku to świadomy kompromis. Hashowanie 400-megabajtowego zeskanowanego archiwum w całości przy każdym otwarciu może kosztować więcej niż wyrenderowanie dwóch stron, na które użytkownik faktycznie zagląda. Próbkowane regiony nie są przypadkowe: nagłówek siedzi na początku pliku, a trailer i ostatnia sekcja krzyżowych referencji na końcu (ISO 32000-1 §7.5). Aktualizacja przyrostowa dokleja nowe ciało, sekcję krzyżowych referencji i trailer (§7.5.6), więc zmienia od razu rozmiar i ogon. Pełny zapis dowolnym normalnym narzędziem zmienia czas ostatniego zapisu. Dla plików do 128 KiB obie próbki pokrywają każdy bajt, więc małe dokumenty są faktycznie hashowane w całości
Ryzyko resztkowe to zmiana o tym samym rozmiarze, w miejscu, w środku dużego pliku, po której zapisujący przywraca oryginalny znacznik czasu. Wymaga to narzędzia, które celowo zachowuje czasy modyfikacji przy edycji treści — rzadkość, ale nie niemożliwość, a w takim wypadku cache serwuje stare strony. Druga strona medalu jest łagodna: skopiowanie pliku na Windows normalnie zachowuje czas ostatniego zapisu, więc kopia dokumentu już obecnego w cache trafia w te same wpisy, co jest poprawne, bo bajty są identyczne
Strumienie nie mają żadnego czasu modyfikacji, więc jedyną uczciwą tożsamością jest treść. HotPDF płaci za ten pełny przebieg SHA-256 tylko wtedy, gdy poprosiłeś o dyskowy cache przed wczytaniem; każdy inny wołający LoadFromStream nie widzi żadnego dodatkowego kosztu. To czyni kolejność przypisań właściwości nośną:
procedure OpenDownloadedPdf(Pdf: THotPDF; Data: TStream;
const CacheRoot: string);
begin
// Zła kolejność dla strumieni: hash treści jest liczony tylko, gdy
// folder jest już ustawiony, więc ten dokument ominąłby warstwę dyskową
// Pdf.LoadFromStream(Data);
// Pdf.RenderCacheFolder := CacheRoot;
Pdf.RenderCacheFolder := CacheRoot; // ustaw najpierw
Data.Position := 0;
if Pdf.LoadFromStream(Data) <= 0 then
raise Exception.Create('The stream is not a loadable PDF');
end;
Źródło o dostępie losowym, które wciąż się pobiera (część zakresów jeszcze niedostępna), nie dostaje tożsamości zamiast hasha treści częściowej, a jeśli policzenie tożsamości zawiedzie z jakiegokolwiek powodu, wczytanie i tak się udaje; dokument po prostu renderuje się bez warstwy dyskowej
Co unieważnia wpis dyskowego cache w HotPDF?
Wpis dyskowego cache HotPDF nie jest nigdy unieważniany przez skasowanie go przy edycji; zamiast tego edycja wczytanego dokumentu zrzuca tożsamość dokumentu, więc warstwa dyskowa jest omijana do końca tego wczytania, a przechowane strony pozostają ważne dla niezmodyfikowanego źródła. Wpisy schodzą z dysku tylko przez limity LRU i bajtów, uszkodzony PNG albo zmianę schematu
Klucz opisuje źródło na dysku, nie graf obiektów w pamięci. Skoro oznaczysz stronę pieczątką albo zmienisz adnotację, dokument przestaje pasować do tego źródła, więc ani czytanie, ani pisanie pod jego kluczem nie byłyby poprawne. Od v2.770.140 unieważnienie i na poziomie dokumentu, i na poziomie strony czyści tożsamość zamiast ruszać folder, a jest i druga straż dla edycji, które nie wołały InvalidateRenderedPageCache: przed użyciem warstwy dyskowej THotPDF sprawdza, czy jakiś wczytany obiekt jest brudny, i traktuje brudny dokument jako pozbawiony tożsamości
Ustawienia renderowania działają w drugą stronę. Przełączenie PageRenderBackend (albo wołanie UseNativeGDIRenderBackend) oraz wołanie ConfigureRenderICCWorkflow albo ClearRenderICCWorkflow wypłukuje strony w pamięci, ale trzyma tożsamość, bo dokument wciąż pasuje do swojego źródła. Te ustawienia zmieniają piksele, nie będąc częścią wariantu w pamięci, więc klucz dyskowy ma wplecioną nazwę backendu, flagę kompensacji czerni i skróty SHA-256 profili ICC proof oraz wyjściowych. Sam wariant pokrywa już intencję kolorów, dithering wyjścia, podgląd overprint, tryb maski luminancji, politykę fallbacku i widoczność każdej grupy opcjonalnej treści, więc przełączenie warstwy renderuje do innego folderu zamiast nadpisywać widok domyślny
Żeby wrócić z edytowanym dokumentem na warstwę dyskową, daj mu nową tożsamość źródła, zapisując go i wczytując wynik:
procedure CommitEditsAndRekey(Pdf: THotPDF; const EditedFile: string);
begin
// Po edycji wczytanego dokumentu: odśwież strony w pamięci.
// Tożsamość źródła już nie istnieje, więc nic nie jest czytane ani
// zapisywane w dyskowym folderze oryginalnego dokumentu
Pdf.InvalidateRenderedPageCache;
// Zapisany plik ma nowy rozmiar i czas ostatniego zapisu, czyli nową
// tożsamość; rendery po tym wczytaniu jadą do cache pod nowym kluczem
Pdf.SaveLoadedDocument(EditedFile);
if Pdf.LoadFromFile(EditedFile) <= 0 then
raise Exception.Create('Could not reload the edited document');
end;
Folder oryginalnego dokumentu zostaje w spokoju i starzeje się przez RenderCacheMaxDocuments i RenderCacheMaxBytes jak każdy inny wpis. Gdy użytkownik otworzy ponownie nieedytowany oryginał, jego strony wciąż tam są
Granice bezpieczeństwa: źródła zaszyfrowane i foldery dowiązane
Dyskowy cache renderowania HotPDF odmawia dwóch rodzajów wejścia celowo: nigdy nie zapisuje stron zaszyfrowanego PDF na dysk i nigdy nie podąża za podfolderem dokumentu, który jest junctionem albo innym punktem reparse. Obie reguły wymieniają trafienia w cache za to, że dane nie wyciekną i nie zostaną skasowane złe pliki
Zaszyfrowane PDF-y nigdy nie lądują w cache na dysku
Wyrenderowana strona to odszyfrowana treść. Zapisanie jej jako gołego PNG do folderu cache zostawiłoby na dysku czytelną kopię dokumentu chronionego hasłem, poza ochroną, którą wybrał autor (ISO 32000-1 §7.6). HotPDF nie przechwytuje więc tożsamości dla żadnego źródła, którego trailer niesie wpis /Encrypt, łącznie z plikami otwartymi z hasłem albo z pustym hasłem użytkownika. Te dokumenty wciąż używają warstwy w pamięci, która umiera razem z procesem
Podfoldery-junctione są odrzucane od v2.770.173
Korzeń cache wybierasz sam i wskazanie na niego junctiona jest dozwolone. Podfoldery dokumentów pod nim to inna sprawa: cache tworzy je, czyta, dotyka i kasuje na własną rękę, przy odzyskiwaniu na starcie (które usuwa zostałe pliki tymczasowe), przy wyszukiwaniu (które aktualizuje znaczniki czasu), przy zapisie, unieważnianiu i trzech limitach wysiedlania. Gdyby ktoś z prawem zapisu do korzenia cache podmienił folder dokumentu na junctiona do innego katalogu, każda z tych ścieżek poszłaby za nim, a wysiedlanie kasowałoby pliki gdzieś, gdzie cache nigdy nic nie posiadał. Od v2.770.173 każdy z tych punktów wejścia sprawdza atrybut punktu reparse i pomija dowiązany folder dokumentu: wyszukiwanie liczy chybienie, zapis liczy porażkę zapisu, a wysiedlanie zostawia go w spokoju
Ścieżki Unicode i wspólne korzenie
Dwie powiązane poprawki mają znaczenie, gdy wdrażasz do profili użytkowników. Przed v2.770.135 RenderCacheFolder był AnsiString, więc folder poza systemową stroną kodową (chińska nazwa użytkownika na anglojęzycznym Windows, na przykład) był traconie konwertowany, zanim cache go zobaczył; właściwość jest teraz Unicode'owym string, a atomowa podmiana używa szerokiego API Windows. Od v2.770.52 kilka instancji THotPDF w jednym procesie wskazujących na ten sam korzeń (po rozwinięciu ścieżki, porównywanej bez rozróżniania wielkości liter) dzieli jeden indeks i jedną blokadę z licznikiem referencji. Wcześniej każda instancja nadpisywała index.txt swoją kopią i egzekwowała limity względem swojego częściowego widoku, więc folder mógł urosnąć kilka razy ponad budżet
To współdzielenie kończy się na granicy procesu. Dwa osobne procesy na tym samym korzeniu wciąż trzymają osobne indeksy w pamięci, więc daj każdej równolegle działającej aplikacji własny korzeń cache. Przeglądarki renderujące na wątkach roboczych są w porządku w obrębie jednego procesu: PrefetchLoadedPages i kolejka opisana w renderowaniu w tle z kolejką żądań przechodzą oba przez tę samą ścieżkę cache i tę samą blokadę
Ściąga: lista kontrolna RenderCacheFolder
- Ustaw
RenderCacheFolder,RenderCacheMaxDocumentsiRenderCacheMaxBytesprzed pierwszym wywołaniemRenderLoadedPageToBitmapCached; przy wczytaniach strumieniowych i o dostępie losowym ustaw folder przed wczytaniem - Zaktualizuj do v2.770.140 lub nowszego, jeśli polegasz na warstwie dyskowej; wcześniejsze wersje przyjmują właściwość, ale nigdy nie wydają strony z dysku przy zwykłych wczytaniach
- Nie oczekuj cache'owania na dysku dla zaszyfrowanych PDF-ów, dla dokumentów edytowanych po wczytaniu ani gdy
RenderFallbackPolicynie jestrfpIgnore - Zwalniaj instancję THotPDF normalnie; od v2.770.140 ani
Free, aniInvalidateRenderedPageCachenie kasują wpisów dyskowych - Zmiana
PageRenderBackendalbo workflow ICC trzyma dokument na warstwie dyskowej pod innym kluczem - Używaj jednego korzenia cache na działającą aplikację; instancje wewnątrz jednego procesu dzielą indeks od v2.770.52
- Trzymaj korzeń cache w lokalizacji per użytkownik; podfoldery dokumentów będące junctionami są pomijane od v2.770.173
Trwały cache stron opłaca się najbardziej w przeglądarce, która przez cały dzień otwiera te same dokumenty, a dokładnie taki jest architektura własnej przeglądarki PDF w Delphi opisana gdzie indziej na tym blogu. RenderCacheFolder, pamięciowy cache rastrowy i renderer stron jadą w pakiecie z komponentem HotPDF Delphi PDF dla Delphi i C++Buildera