Osadzonym przeglądarkom PDF towarzyszą zazwyczaj dwa problemy: duże dokumenty są renderowane od zera przy każdym uruchomieniu, a tekst staje się rozmazany w momencie przeciągnięcia okna na monitor o wysokiej rozdzielczości DPI. Biblioteka losLab PDF Library rozwiązuje oba te problemy w kontrolce TPDFlibViewer: właściwość DiskCacheFolder aktywuje dyskową pamięć podręczną stron między sesjami identyfikowaną skrótem (fingerprint) dokumentu, a właściwość ScreenDPI, wspierana przez GetDpiForWindow i procedurę obsługi komunikatu WM_DPICHANGED, zapewnia kontrolce pełną obsługę DPI na monitor
Oba te problemy zasługują na poważne potraktowanie, ponieważ użytkownicy zauważają je natychmiast. Zeskanowana, 400-stronicowa umowa, której wczorajsze załadowanie trwało osiem sekund, dziś ponownie ładuje się przez osiem sekund, mimo że w pliku nic się nie zmieniło. Z kolei w środowisku o zróżnicowanej rozdzielczości (mixed-DPI), przeglądarka renderująca obraz przy bazowej rozdzielczości głównego monitora wynoszącej 96 DPI będzie wyglądać dobrze na nim, lecz okaże się wyraźnie rozmyta na sąsiednim ekranie 4K. Ten artykuł opisuje, jak TPDFlibViewer radzi sobie z tymi trudnościami, a także — co bardziej użyteczne — z jakich powodów przyjęto taką strukturę. Jeśli dopiero poznajesz samą kontrolkę, zacznij od wprowadzenia do interaktywnej kontrolki przeglądarki PDF, a następnie wróć do lektury o warstwie wydajności
Jak buforować wyrenderowane strony PDF między sesjami w Delphi?
Przypisanie folderu do właściwości TPDFlibViewer.DiskCacheFolder sprawia, że kontrolka zapisuje wyrenderowane mapy bitowe stron na dysku, dzięki czemu niedawno przeglądany dokument otwiera się natychmiast, bez ponownego renderowania. Struktura na dysku ma postać <Folder>\PDFlibPas-PageCache\<document fingerprint>\p<page>_z<zoomkey>.bin: jeden podfolder przypada na dokument, a jeden plik odpowiada kombinacji strony i powiększenia. Wszystko inne odbywa się automatycznie; nie ma potrzeby wywoływania API pamięci podręcznej ani jej ręcznego czyszczenia
uses
PDFlibViewer;
procedure TMainForm.FormCreate(Sender: TObject);
begin
FViewer := TPDFlibViewer.Create(Self);
FViewer.Parent := Self;
FViewer.Align := alClient;
// Jedna linia aktywuje dyskową pamięć podręczną stron między sesjami
FViewer.DiskCacheFolder := 'C:\ProgramData\MyApp\PageCache';
FViewer.LoadFromFile('C:\Contracts\master-agreement.pdf', '');
end;
Licznik DiskCacheHits informuje, ile operacji renderowania stron zostało zaoszczędzonych dzięki dyskowej pamięci podręcznej w bieżącej sesji, co pozwala zmierzyć zysk w sposób wymierny. Dla dokumentu, który został zamknięty i ponownie otwarty, każda strona przewijana do widoku bez przebiegu renderowania zwiększa ten licznik
Dlaczego kluczem pamięci podręcznej jest skrót (fingerprint), a nie numer wersji
Skrót dokumentu (fingerprint) to 64-bitowy hash FNV-1a z ciągu path|size|last write time, sformatowany jako szesnaście cyfr szesnastkowych. Ta trójka parametrów stanowi kompletną strategię unieważniania: po edycji pliku zmienia się jego rozmiar lub czas zapisu, zmienia się hash, a przeglądarka po prostu otwiera inny podfolder cache. Nieaktualny folder nie jest już używany i ostatecznie podlega czyszczeniu LRU. Nie ma tu potrzeby utrzymywania protokołu wersji, logiki porównywania znaczników czasu ani ryzyka, że stara mapa bitowa zostanie wczytana dla zmodyfikowanego dokumentu
Jest to ta sama idea „tożsamość zawartości jako klucz cache”, którą stosują systemy budowania oprogramowania, a jej wartość polega na eliminacji stanów awaryjnych. Pamięć podręczna przechowująca pliki pod zwykłą nazwą dokumentu musi jawnie wykrywać edycje, a każda taka weryfikacja to ryzyko wystąpienia błędu w skrajnych przypadkach: przywrócenia pliku z kopii zapasowej ze starym znacznikiem czasu, zapisu bez zmiany rozmiaru czy ścieżki porównywanej z uwzględnieniem wielkości liter na jednej maszynie, a bez na innej. Haszowanie wszystkich trzech sygnałów w nazwie folderu sprawia, że nieaktualność jest niemożliwa ze względów strukturalnych, a nie proceduralnych
Dlaczego obrót jest celowo pomijany w kluczu dyskowym
Nazwa pliku w pamięci podręcznej koduje numer strony oraz powiększenie i nic więcej — kąt obrotu widoku jest celowo pomijany. Dyskowa pamięć podręczna zawsze przechowuje bajty wyrenderowane bez obrotu. Gdy buforowana strona jest ładowana przy obróconym widoku, piksele są obracane w pamięci po ich zdekodowaniu. Gdyby obrót wchodził w skład klucza dyskowego, użytkownik przełączający cztery kąty widoku zapisałby tę samą stronę na dysku cztery razy, co czterokrotnie zwiększyłoby rozmiar pamięci podręcznej przy braku dodatkowych informacji, ponieważ obrót to prosta operacja na pikselach w porównaniu z pełnym renderowaniem
Jak czyszczenie LRU ogranicza rozmiar folderu pamięci podręcznej?
Pamięć podręczna przechowuje do dziesięciu folderów dokumentów i usuwa te najdawniej używane, gdy pojawiają się nowe dokumenty. Czas ostatniego użycia jest śledzony przez plik last-used.marker w folderze każdego dokumentu, którego znacznik czasu jest aktualizowany przy odczytach i zapisach cache, dzięki czemu często otwierany dokument pozostaje aktywny bez konieczności ponownego zapisu. Folder aktualnie otwartego dokumentu jest wyłączony z usuwania, stąd czyszczenie nigdy nie usunie stron z aktywnego widoku
Każda operacja dyskowa w pamięci podręcznej — tworzenie skrótu, zakładanie folderu, aktualizacja znacznika czasu, odczyt i zapis stron, czyszczenie — przechwytuje własne wyjątki. To celowa decyzja, a nie niedopatrzenie: cache stanowi optymalizację działającą na zasadzie best-effort (w miarę możliwości). Pełny dysk, folder tylko do odczytu czy blokada antywirusowa muszą jedynie zdegradować przeglądarkę do trybu „renderuje jak dawniej”, lecz nigdy nie mogą powodować błędów wyświetlania strony. Najgorszym wynikiem błędu wejścia/wyjścia cache jest brak trafienia
procedure TMainForm.FormClose(Sender: TObject; var Action: TCloseAction);
begin
// Ile operacji renderowania zaoszczędził dyskowy cache w tej sesji?
Log(Format('Disk cache hits: %d', [FViewer.DiskCacheHits]));
end;
Jak zapewnić obsługę DPI w przeglądarce PDF w Delphi?
Komponent TPDFlibViewer kieruje wszelkie obliczenia zależne od DPI przez jedno wspólne pole, co ułatwia obsługę DPI na monitor. Kod układu, wstępnego pobierania (prefetch) oraz renderowania korzysta z jednej wartości FScreenDPI; przy powiększeniu 100% jeden punkt PDF odpowiada ScreenDPI / 72 pikselom. Publiczny seter właściwości ScreenDPI czyści pamięć podręczną map bitowych, przebudowuje układ, aktualizuje zakresy przewijania i odświeża okno, więc zmiana DPI to pojedyncze przypisanie, po którym wszystkie elementy dostosowują się automatycznie. Te same przeliczenia pikseli są istotne przy renderowaniu dla drukarki zamiast monitora, co opisano w artykule o podglądzie wydruku i renderowaniu w kontekście urządzenia
Wykrywanie rozdzielczości odbywa się w nadpisanej metodzie CreateWnd, a nie w konstruktorze, ponieważ funkcja GetDpiForWindow wymaga poprawnego uchwytu okna, który w konstruktorze jeszcze nie istnieje. Do czasu uruchomienia CreateWnd kontrolka przechowuje domyślną wartość 96, co pozwala zachować spójność obliczeń układu. Sama funkcja GetDpiForWindow wchodzi w skład API systemu Windows 10 1607+, stąd kontrolka wiąże ją dynamicznie za pomocą GetProcAddress, wycofując się do GetDeviceCaps(LOGPIXELSY) na starszych systemach — moduł ładuje się poprawnie na każdym systemie, a starsze wersje Windows zwracają po prostu systemową wartość DPI
Co dzieje się, gdy okno jest przenoszone na monitor 4K?
System Windows wysyła komunikat WM_DPICHANGED, gdy okno z obsługą DPI na monitor przechodzi na ekran o innym współczynniku skalowania, a komponent TPDFlibViewer obsługuje go bezpośrednio. Procedura obsługi odczytuje nową wartość DPI z młodszego słowa wParam, przypisuje ją przez SetScreenDPI — co usuwa nieaktualną pamięć podręczną map bitowych i na nowo rozmieszcza strony — a następnie akceptuje sugerowany prostokąt okna przekazany przez Windows w parametrze lParam za pomocą SetWindowPos, co pozwala zachować stabilny logiczny rozmiar klienta. Użytkownik widzi, że dokument renderuje się ostro w nowej skali, zamiast obserwować mapę bitową rozciągniętą przez system operacyjny
// Zazwyczaj nie modyfikuje się ręcznie ScreenDPI: CreateWnd wykrywa właściwy monitor
// a WM_DPICHANGED śledzi ruch okna. Nadpisuj wyłącznie dla celów specjalnych,
// np. renderowania układu jak dla wyświetlacza ze skalowaniem 150%:
FViewer.ScreenDPI := 144; // czyści cache map bitowych i przebudowuje układ
Ograniczenia, o których warto wiedzieć przed wdrożeniem
Trzy praktyczne ograniczenia zasługują na uwagę przy projektowaniu aplikacji. Po pierwsze, komunikat WM_DPICHANGED jest dostarczany tylko wtedy, gdy proces wyrazi na to zgodę: manifest aplikacji musi deklarować obsługę DPI na monitor (per-monitor v2 na współczesnym Windows), co w Delphi IDE konfiguruje się w opcjach aplikacji (Application options) pod hasłem DPI awareness. Proces obsługujący wyłącznie systemową wartość DPI nigdy nie otrzyma tego komunikatu, a kontrolka wyrenderuje obraz przy rozdzielczości wykrytej w momencie utworzenia okna. Po drugie, dyskowy cache zamienia wolną przestrzeń na wydajność — buforowanie stron dla dziesięciu dokumentów przy kilku poziomach powiększenia wymaga pamięci masowej, więc wskaż w DiskCacheFolder lokalizację, która może rosnąć, a nie profil mobilny (roaming profile). Po trzecie, folder pamięci podręcznej wymaga uprawnień do zapisu dla bieżącego użytkownika; bez nich przeglądarka nadal działa, lecz nie przynosi korzyści z cache — w przypadku podejrzeń o brak działania sprawdź wartość DiskCacheHits w testach
Wspólnie obie te funkcje podnoszą jakość aplikacji intensywnie korzystającej z dokumentów bardziej niż większość optymalizacji renderowania: otwieranie wcześniej przeglądanych dokumentów następuje natychmiast, a widok zachowuje ostrość na dowolnym monitorze, na który trafi okno. Obie są dostępne jako zwykłe właściwości klasy TPDFlibViewer w losLab PDF Library, so adopting them is an afternoon of wiring rather than a rendering-engine project