Dve sťažnosti sprevádzajú takmer každý integrovaný prehliadač PDF: veľké dokumenty sa pri každom spustení vykresľujú od nuly a text sa rozmaže vo chvíli, keď sa okno presunie na monitor s vysokým rozlíšením (High-DPI). losLab PDF Library rieši oba tieto problémy vo svojom ovládacom prvku TPDFlibViewer: vlastnosť DiskCacheFolder povoľuje medzirelačnú diskovú vyrovnávaciu pamäť (cache) stránok kódovanú podľa odtlačku prsta (fingerprint) dokumentu a vlastnosť ScreenDPI, podporená funkciou GetDpiForWindow a obsluhou správy WM_DPICHANGED, dáva ovládaciemu prvku skutočné prispôsobenie DPI pre každý monitor (per-monitor DPI awareness)
Oba problémy stoja za vážne zamyslenie, pretože používatelia si ich okamžite všimnú. Naskenovaná zmluva s 400 stranami, ktorej posúvanie včera trvalo osem sekúnd, kým sa stalo plynulým, dnes trvá znova osem sekúnd, hoci sa v súbore nič nezmenilo. A na pracovnej ploche s rôznymi hodnotami DPI vyzerá prehliadač vykresľujúci na predvolených 96 DPI hlavného monitora dobre tam, ale je viditeľne neostrý na vedľajšom 4K paneli. Tento článok popisuje, ako TPDFlibViewer rieši každý z nich, a čo je ešte užitočnejšie, prečo sa návrh ustálil práve na tomto riešení. Ak s ovládacím prvkom začínate, začnite prehľadom interaktívneho ovládacieho prvku prehliadača PDF a potom sa vráťte sem pre výkonnostnú vrstvu
Ako ukladať vykreslené stránky PDF do vyrovnávacej pamäte naprieč reláciami v Delphi?
Priraďte priečinok vlastnosti TPDFlibViewer.DiskCacheFolder a ovládací prvok uloží vykreslené bitmapy stránok na disk, takže nedávno zobrazený dokument sa namiesto opätovného vykresľovania otvorí okamžite. Štruktúra na disku je <Folder>\PDFlibPas-PageCache\<odtlačok dokumentu>\p<stránka>_z<kľúč_priblíženia>.bin: jeden podpriečinok na dokument, jeden súbor na kombináciu stránky a priblíženia. Všetko ostatné je automatické; netreba volať žiadne API pre cache ani nič vymazávať
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;
Počítadlo DiskCacheHits hlási, koľko vykreslení stránok ušetril diskový cache v aktuálnej relácii, vďaka čomu je úspora merateľná a nie iba domnelá. Pri dokumente, ktorý ste zavreli a znova otvorili, sa počítadlo zvýši pri každej stránke, ktorá sa vroluje do zobrazenia bez potreby vykresľovania
Prečo je kľúčom vyrovnávacej pamäte odtlačok prsta (fingerprint) a nie číslo verzie
Odtlačok prsta dokumentu (fingerprint) je 64-bitový FNV-1a hash nad reťazcom cesta|veľkosť|čas posledného zápisu, formátovaný ako šesťnásť hexadecimálnych číslic. Táto trojica predstavuje celú stratégiu invalidácie vyrovnávacej pamäte: pri úprave súboru sa zmení jeho veľkosť alebo čas zápisu, zmení sa hash a prehliadač jednoducho otvorí iný podpriečinok vyrovnávacej pamäte. Zastaraný priečinok sa už nikdy nepoužije a časom sa odstráni v rámci čistenia LRU. Netreba udržiavať žiadny protokol verzií, žiadnu logiku porovnávania časových pečiatok, ktorá by sa mohla odchýliť, a neexistuje spôsob, ako by stará bitmapa mohla byť predložená pri modifikovanom dokumente
Ide o rovnakú myšlienku „identita obsahu ako kľúč vyrovnávacej pamäte“, ktorú používajú zostavovacie systémy (build systems), a svoju hodnotu dokazuje v eliminácii chybových stavov. Vyrovnávacia pamäť, ktorá ukladá súbory pod prostým názvom dokumentu, musí úpravy zisťovať explicitne a každá takáto kontrola je chybou čakajúcou na hraničný prípad (edge case): súbor obnovený zo zálohy so starou časovou pečiatkou, uloženie, ktoré zachová veľkosť, cesta porovnávaná s rozlišovaním malých a veľkých písmen na jednom stroji a bez rozlišovania na druhom. Zahešovanie všetkých troch signálov do názvu priečinka robí zastaranie štrukturálne nemožným, namiesto toho, aby sa kontrolovalo procedurálne
Prečo je rotácia zámerne vynechaná z diskového kľúča
Názov súboru vyrovnávacej pamäte kóduje číslo stránky a priblíženie, nič iné — uhol otočenia zobrazenia zámerne chýba. Diskový cache vždy ukladá neotočené bajty vykreslenia; ak sa načíta stránka z cache, kým je zobrazenie otočené, pixely sa po dekódovaní preorientujú v pamäti. Ak by bola rotácia súčasťou kľúča na disku, používateľ prechádzajúci štyrmi uhlami otočenia by zapísal rovnakú stránku na disk štyrikrát, čo by štvornásobne zvýšilo rast cache bez akejkoľvek pridanej hodnoty, keďže rotácia je lacná operácia s pixelmi v porovnaní s plným vykresľovacím priechodom
Pamäťový cache bitmap naozaj používa zložený kľúč, Round(Zoom * 1000) * 4 + Rotation div 90, pretože tam je otočená bitmapa presne tým, čo vykresľovač (paint handler) potrebuje. Prefetch vlákno na pozadí dodržiava rovnakú disciplínu: vždy vykresľuje neotočené stránky a hlási čisté kľúče priblíženia, pričom hlavné vlákno aplikuje rotáciu pri spracovaní výsledkov — ide o rovnaké pravidlo, že hlavné vlákno vlastní vyrovnávaciu pamäť, o ktorom sa hovorí v článku o paralelnom vykresľovaní stránok a bezpečnosti vlákien. Uchovanie rotácie mimo všetkého pod pamäťovou vrstvou znamená jedno kánonické vykreslenie na stránku a priblíženie všade tam, kde je ukladanie náročné
Ako premazávanie LRU udržiava priečinok vyrovnávacej pamäte ohraničený?
Vyrovnávacia pamäť si uchováva až desať priečinkov dokumentov a pri príchode nových dokumentov premazáva tie, ktoré boli použité najdávnejšie. Používanosť sa sleduje pomocou súboru last-used.marker v každom priečinku dokumentu, ktorého časová pečiatka sa aktualizuje pri zápisoch aj čítaniach z cache, takže dokument, ktorý si len opätovne čítate, zostáva udržiavaný bez nutnosti opätovného zápisu. Priečinok patriaci aktuálne otvorenému dokumentu je vyňatý z mazania, takže čistenie nikdy nemôže odstrániť stránky priamo pod aktívnym zobrazením
Každá operácia na disku vo vyrovnávacej pamäti — výpočet odtlačku, vytváranie priečinkov, zápis markerov, čítanie a zápis stránok, čistenie — potláča svoje vlastné výnimky. Ide o zámerný prístup, nie o nedbalosť: vyrovnávacia pamäť je len akcelerátor s najlepším úsilím (best-effort) a plný disk, priečinok iba na čítanie alebo zámok antivírusu musia len prepnúť prehliadač into režim „vykresľuje ako predtým“, nikdy nesmú spôsobiť „zlyhanie zobrazenia stránky“. Najhorším výsledkom, ktorý môže chyba I/O vyrovnávacej pamäte spôsobiť, je strata zásahu (missed hit)
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;
Ako urobiť prehliadač PDF prispôsobivým pre DPI v Delphi?
Ovládací prvok TPDFlibViewer smeruje každý výpočet závislý od DPI cez jedno jediné pole a tento dizajn s jedným miestom zmeny je tým, čo robí podporu pre viacero monitorov zvládnuteľnou. Kód rozloženia, predbežného načítania (prefetch) aj vykresľovania číta jednu hodnotu FScreenDPI; pri 100% priblížení sa jeden bod PDF mapuje na ScreenDPI / 72 pixelov. Setter verejnej vlastnosti ScreenDPI vymaže cache bitmap, prebuduje rozloženie, aktualizuje rozsahy rolovania a invaliduje okno, takže zmena DPI je záležitosťou jedného priradenia, pričom všetci odberatelia ho nasledujú automaticky. Rovnaká matematika s pixelmi je dôležitá aj pri vykresľovaní pre tlačiareň namiesto monitora, čo je téma pokrytá v článku o náhľade tlače a vykresľovaní do kontextu zariadenia
Detekcia prebieha v prepísanej metóde CreateWnd a nie v konštruktore, pretože GetDpiForWindow vyžaduje platný handle okna (window handle) a konštruktor beží oveľa skôr, než nejaký existuje. Kým sa nespustí CreateWnd, ovládací prvok si zachováva historicky predvolenú hodnotu 96, aby matematika rozloženia zostala konzistentná. Samotná funkcia GetDpiForWindow je API z Windows 10 1607+, takže ovládací prvok ju viaže dynamicky pomocou GetProcAddress a na starších systémoch prechádza na GetDeviceCaps(LOGPIXELSY) — jednotka sa stále úspešne načíta všade a starší systém Windows jednoducho oznámi systémové DPI
Čo sa stane, keď sa okno presunie na 4K monitor?
Keď okno reagujúce na DPI pre jednotlivé monitory prejde na monitor s iným faktorom mierky, systém Windows odošle správu WM_DPICHANGED a TPDFlibViewer ju spracuje priamo. Obsluha správy prečíta nové DPI z nižšieho slova parametra wParam, priradí ho prostredníctvom SetScreenDPI — čo zahodí teraz už neplatnú vyrovnávaciu pamäť bitmap a preusporiada stránky — a potom prijme navrhovaný obdĺžnik okna, ktorý Windows odovzdá v parametri lParam cez SetWindowPos, čím sa zachová stabilná logická veľkosť klientskej oblasti počas presunu. Používateľ vidí, že dokument sa vykreslí ostro v novej mierke, namiesto toho, aby videl operačným systémom roztiahnutú bitmapu
// 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
Obmedzenia, ktoré by ste mali vedieť pred nasadením
Tri praktické obmedzenia si zaslúžia miesto vo vašom posúdení návrhu. Po prvé, správa WM_DPICHANGED príde iba vtedy, ak sa proces k nej prihlási: manifest vašej aplikácie must deklarovať podporu per-monitor DPI (per-monitor v2 na súčasnom systéme Windows), čo je v Delphi IDE nastavenie DPI awareness v sekcii Application options. Proces s podporou system-DPI túto správu nikdy nedostane a ovládací prvok potom vykresľuje s hodnotou DPI, ktorú zistil pri vytváraní okna. Po druhé, diskový cache vymieňa diskový priestor za rýchlosť — bitmapy stránok pre desať dokumentov naprieč rôznymi úrovňami priblíženia predstavujú reálne dáta, preto nasmerujte DiskCacheFolder na miesto, kde vám nevadí nárast objemu dát, nie do cestovného profilu (roaming profile). Po tretie, priečinok cache potrebuje povolenie na zápis pre spusteného používateľa; bez neho prehliadač stále funguje, avšak potichu s nulovými zásahmi (zero hits) — ak máte podozrenie, že sa cache nezapája, skontrolujte hodnotu DiskCacheHits počas testovania
Tieto dve funkcie spoločne menia vnímanú kvalitu aplikácie pracujúcej s veľkým množstvom dokumentov viac ako väčšina optimalizácií vykresľovania: dokumenty, ku ktorým sa vaši používatelia vracajú, sa otvárajú okamžite a zobrazenie zostáva ostré na každom monitore, na ktorom skončí. Obe sú k dispozícii ako bežné vlastnosti na TPDFlibViewer v knižnici losLab PDF Library, takže ich nasadenie predstavuje skôr jedno popoludnie prepájania vlastností než projekt pre písanie vykresľovacieho jadra