Dvije pritužbe prate gotovo svaki ugrađeni PDF preglednik: veliki dokumenti se ponovno iscrtavaju ispočetka pri svakom pokretanju, a tekst postaje mutan onog trenutka kada se prozor povuče na monitor s visokim DPI-jem. losLab PDF Library rješava oba problema u svojoj kontroli TPDFlibViewer: svojstvo DiskCacheFolder omogućuje međusesijsku (cross-session) predmemoriju stranica na disku identificiranu otiskom prsta dokumenta, a svojstvo ScreenDPI, podržano s GetDpiForWindow i rukovateljem WM_DPICHANGED, daje kontroli stvarnu svjesnost o DPI-ju po monitoru
Oba problema vrijedi shvatiti ozbiljno jer ih korisnici odmah primjećuju. Skenirani ugovor od 400 stranica za čije je učitavanje jučer trebalo osam sekundi da bi se mogao pomicati, danas ponovno zahtijeva osam sekundi, iako se u datoteci ništa nije promijenilo. A na radnoj površini s mješovitim DPI-jem, preglednik koji se iscrtava na 96 DPI primarnog monitora ondje izgleda dobro, ali je vidljivo mutan na susjednom 4K zaslonu. Ovaj članak objašnjava kako TPDFlibViewer rješava svaki od tih problema i, što je još korisnije, zašto je dizajn postavljen na taj način. Ako ste novi u samoj kontroli, započnite s pregledom kontrole interaktivnog PDF preglednika pa se vratite za sloj performansi
Kako predmemorirati iscrtane PDF stranice kroz sesije u Delphiju?
Dodijelite mapu svojstvu TPDFlibViewer.DiskCacheFolder i kontrola će spremati bitmape iscrtanih stranica na disk, tako da se nedavno pregledani dokument otvara trenutno umjesto ponovnog iscrtavanja. Na disku je struktura <Folder>\PDFlibPas-PageCache\<document fingerprint>\p<page>_z<zoomkey>.bin: jedna podmapa po dokumentu, jedna datoteka po kombinaciji stranice i zumiranja. Sve ostalo je automatski; nema API-ja za predmemoriju koji treba pozivati niti ičega što treba čistiti
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;
Brojač DiskCacheHits bilježi koliko je iscrtavanja stranica predmemorija diska uštedjela u trenutnoj sesiji, što čini dobitak mjerljivim umjesto samo anegdotskim. Na dokumentu koji ste zatvorili i ponovno otvorili, svaka stranica koja se pomakne u vidno polje bez prolaza iscrtavanja povećava taj brojač
Zašto je ključ predmemorije otisak prsta (fingerprint), a ne broj verzije
Otisak prsta dokumenta je 64-bitni FNV-1a hash nad nizom path|size|last write time, formatiran kao šesnaest heksadecimalnih znamenki. Ta trojka je cijela strategija poništavanja (invalidation): kada se datoteka uredi, njezina veličina ili vrijeme pisanja se mijenjaju, hash se mijenja i preglednik jednostavno otvara drugu podmapu predmemorije. Zastarjela mapa se više nikada ne koristi i na kraju se briše kroz LRU čišćenje. Nema protokola oznaka verzije koji bi trebalo održavati, nema logike usporedbe vremenskih oznaka koja bi mogla odstupati, niti načina da se stara bitmapa posluži za izmijenjeni dokument
To je ista ideja 'identiteta sadržaja kao ključa predmemorije' koju koriste sustavi za izgradnju (build systems), a opravdava svoje postojanje uklanjanjem mogućih načina kvara. Predmemorija koja pohranjuje datoteke pod jednostavnim imenom dokumenta mora eksplicitno detektirati izmjene, a svaka eksplicitna provjera je potencijalna pogreška koja čeka granični slučaj: datoteka vraćena iz sigurnosne kopije sa starom vremenskom oznakom, spremanje koje zadržava istu veličinu, putanja uspoređena ovisno o velikim i malim slovima na jednom računalu, a ne na drugom. Raspršivanje (hashing) sva tri signala u naziv mape čini zastarjelost strukturalno nemogućom, umjesto proceduralno provjeravanom
Zašto rotacija je namjerno izostavljena iz ključa diska
Naziv datoteke predmemorije kodira broj stranice i zumiranje, i ništa drugo — kut rotacije prikaza namjerno je odsutan. Predmemorija diska uvijek pohranjuje nerotirane bajtove iscrtavanja; kada se predmemorirana stranica učita dok je prikaz rotiran, pikseli se ponovno orijentiraju u memoriji nakon dekodiranja. Ako bi rotacija bila dio ključa na disku, korisnik koji prolazi kroz četiri kuta prikaza zapisao bi istu stranicu na disk četiri puta, učetverostručujući rast predmemorije za nula dodatnih informacija, budući da je rotacija jeftino premještanje piksela u usporedbi s punim prolazom iscrtavanja
Predmemorija bitmape u memoriji koristi kompozitni ključ, Round(Zoom * 1000) * 4 + Rotation div 90, jer je tamo rotirana bitmapa točno ono što rukovatelj crtanjem (paint handler) treba. Pozadinska nit za unaprijedno dohvaćanje (prefetch thread) slijedi istu disciplinu: uvijek iscrtava nerotirane stranice i prijavljuje jednostavne ključeve zumiranja, a glavna nit primjenjuje rotaciju prilikom obrade rezultata — isto pravilo 'glavna nit posjeduje predmemoriju' o kojem se raspravlja u paralelnom iscrtavanju stranica i sigurnosti niti. Držanje rotacije izvan svega ispod sloja memorije znači jedno kanonsko iscrtavanje po stranici i zumiranju, svugdje gdje je pohrana skupa
Kako LRU čišćenje održava mapu predmemorije ograničenom?
Predmemorija zadržava do deset mapa dokumenata i čisti najmanje nedavno korištene (least recently used) kada stignu novi dokumenti. Učestalost korištenja prati se datotekom last-used.marker u svakoj mapi dokumenta čija se vremenska oznaka ažurira pri pisanju i čitanju predmemorije, tako da dokument koji samo ponovno čitate ostaje aktivan bez ponovnog pisanja. Mapa koja pripada trenutno otvorenom dokumentu izuzeta je od brisanja, tako da čišćenje nikada ne može ukloniti stranice ispod aktivnog prikaza
Svaka diskovna operacija u predmemoriji — izrada otiska prsta, stvaranje mapa, ažuriranje markera, čitanje i pisanje stranica, čišćenje — zanemaruje vlastite iznimke. To je namjeran stav, a ne nemar: predmemorija je akcelerator koji radi po principu najboljeg truda, a pun disk, mapa zaštićena od pisanja ili blokada antivirusnog programa moraju svesti preglednik na ponašanje 'iscrtava kao i prije', a ne na 'ne uspijeva prikazati stranicu'. Najgori ishod koji IO pogreška predmemorije može proizvesti je propušteni pogodak (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;
Kako učiniti PDF preglednik svjesnim DPI-ja u Delphiju?
TPDFlibViewer usmjerava svaki izračun ovisan o DPI-ju kroz jedno polje, a taj dizajn s jednom točkom promjene čini podršku za svaki monitor izvedivom. Kôd izgleda (layout), unaprijednog dohvaćanja (prefetch) i iscrtavanja (render) čita jednu vrijednost FScreenDPI; pri zumiranju od 100%, jedna točka PDF-a preslikava se na ScreenDPI / 72 piksela. Javni postavljač svojstva (setter) ScreenDPI čisti predmemoriju bitmape, ponovno gradi izgled, ažurira raspone pomicanja i invalidira prozor, tako da je promjena DPI-ja jedno dodjeljivanje koje svi potrošači automatski prate. Ista pikselna matematika bitna je i kada iscrtavate za pisač umjesto za monitor, tema koja je pokrivena u prikazu ispisa i iscrtavanju konteksta uređaja
Detekcija se događa u nadjačavanju CreateWnd umjesto u konstruktoru, jer GetDpiForWindow zahtijeva valjanu ručku prozora (window handle), a konstruktor se pokreće davno prije nego što ona postoji. Dok se CreateWnd ne aktivira, kontrola drži naslijeđenu zadanu vrijednost od 96 kako bi matematika izgleda ostala stabilna. Sam GetDpiForWindow je API za Windows 10 1607+, pa ga kontrola dinamički povezuje s GetProcAddress i vraća se na GetDeviceCaps(LOGPIXELSY) na starijim sustavima — jedinica se i dalje učitava svugdje, a stariji Windows jednostavno javlja DPI sustava
Što se događa kada se prozor pomakne na 4K monitor?
Windows šalje WM_DPICHANGED kada prozor svjestan pojedinačnog monitora prijeđe na monitor s različitim faktorom skaliranja, a TPDFlibViewer time upravlja izravno. Rukovatelj čita novi DPI iz niske riječi wParam, dodjeljuje ga putem SetScreenDPI — što odbacuje sada pogrešnu predmemoriju bitmape i ponovno raspoređuje stranice — a zatim prihvaća predloženi pravokutnik prozora koji Windows prosljeđuje u lParam preko SetWindowPos, što održava logičku veličinu klijenta stabilnom tijekom premještanja. Korisnik vidi kako se dokument iscrtava oštro na novoj ljestvici, umjesto bitmape koju rasteže operacijski sustav
// 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
Granice koje vrijedi znati prije isporuke
Tri praktična ograničenja zaslužuju mjesto u vašem pregledu dizajna. Prvo, WM_DPICHANGED stiže samo ako se proces prijavi (opt-in): manifest vaše aplikacije mora deklarirati svjesnost o DPI-ju po monitoru (per-monitor v2 na trenutnim Windowsima), što je u Delphi IDE-u postavka svjesnosti o DPI-ju pod opcijama aplikacije. Proces koji je svjestan samo DPI-ja sustava nikada ne prima tu poruku, a kontrola tada iscrtava na DPI-ju koji je otkrila pri stvaranju prozora. Drugo, predmemorija diska mijenja prostor na disku za brzinu — bitmape stranica za deset dokumenata na nekoliko razina zumiranja predstavljaju stvarnu pohranu, pa usmjerite DiskCacheFolder na mjesto za koje ste spremni da raste, a ne na roaming profil. Treće, mapa predmemorije zahtijeva dopuštenje za pisanje za aktivnog korisnika; bez njega preglednik i dalje radi, tiho, s nula pogodaka (hits) — provjerite DiskCacheHits u testiranju ako sumnjate da se predmemorija ne aktivira
Zajedno, ove dvije značajke mijenjaju percipiranu kvalitetu aplikacije s mnogo dokumenata više od većine optimizacija iscrtavanja: dokumenti koje vaši korisnici ponovno posjećuju otvaraju se trenutno, a prikaz ostaje oštar na bilo kojem monitoru na kojem se nađe. Obje se isporučuju kao uobičajena svojstva na TPDFlibViewer u losLab PDF knjižnici, tako da je njihovo usvajanje popodne povezivanja, a ne projekt razvoja mehanizma za iscrtavanje