Techninis straipsnis

PDF peržiūros programos disko talpykla ir monitoriaus DPI pritaikymas Delphi aplinkoje

Kiekvieną integruotą PDF peržiūros programą lydi du skundai: dideli dokumentai atidarant kiekvieną kartą atvaizduojami iš naujo, o tekstas tampa neryškus, kai tik langas perkeliamas į didelės raiškos (high-DPI) monitorių. „losLab PDF Library“ atsako į abu šiuos iššūkius savo „TPDFlibViewer“ valdiklyje: savybė „DiskCacheFolder“ įjungia tarp sesijų veikiančią disko puslapių talpyklą, susietą su dokumento atspaudu (fingerprint), o savybė „ScreenDPI“, pagrįsta „GetDpiForWindow“ ir „WM_DPICHANGED“ pranešimų apdorojimu, suteikia valdikliui tikrą DPI pritaikymą kiekvienam monitoriui

Abi šias problemas verta vertinti rimtai, nes naudotojai jas pastebi iškart. Skenuota 400 puslapių sutartis, kurios slinkimas vakar pradėjo veikti per aštuonias sekundes, šiandien vėl užtrunka aštuonias sekundes, nors failas visiškai nepasikeitė. O skirtingų DPI nustatymų darbalaukyje peržiūros programa, kuri atvaizduoja puslapius pagal pagrindinio monitoriaus 96 DPI raišką, ten atrodo gerai, bet tampa pastebimai neryški greta esančiame 4K ekrane. Šiame straipsnyje apžvelgiama, jak „TPDFlibViewer“ išsprendžia kiekvieną iš jų ir, dar naudingiau, kodėl buvo pasirinktas būtent toks dizainas. Jei nesate susipažinę su pačiu valdikliu, pradėkite nuo interaktyvaus PDF peržiūros valdiklio apžvalgos, o po to sugrįžkite prie našumo sluoksnio

Kaip išsaugoti atvaizduotus PDF puslapius tarp sesijų Delphi aplinkoje?

Priskirkite aplanką savybei „TPDFlibViewer.DiskCacheFolder“ ir valdiklis išsaugos atvaizduotų puslapių bitų žemėlapius diske, todėl neseniai žiūrėtas dokumentas atsidarys akimirksniu, užuot buvęs atvaizduojamas iš naujo. Disko aplanko struktūra yra tokia: „<Folder>\PDFlibPas-PageCache\<document fingerprint>\p<page>_z<zoomkey>.bin“ — po vieną poaplankį kiekvienam dokumentui ir po vieną failą kiekvienam puslapio bei mastelio deriniui. Visa kita vyksta automatiškai: nereikia kviesti jokios talpyklos API ir nieko išvalyti

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;

Skaitiklis „DiskCacheHits“ praneša, kiek puslapių atvaizdavimų disko talpykla sutaupė esamoje sesijoje, todėl laimėjimas tampa pamatuojamas, o ne spėjamas. Dokumente, kurį uždarėte ir vėl atidarėte, kiekvienas puslapis, kuris įslenka į vaizdą neatliekant atvaizdavimo, padidina šį skaitiklį

Kodėl talpyklos raktas yra atspaudas, o ne versijos numeris

Dokumento atspaudas (fingerprint) yra 64 bitų FNV-1a maišos kodas (hash), sugeneruotas iš eilutės „path|size|last write time“ ir pateiktas kaip šešiolika šešioliktainių skaitmenų. Ši trijulė yra visa talpyklos negaliojimo strategija: kai failas redaguojamas, keičiasi jo dydis arba įrašymo laikas, keičiasi maišos kodas, o peržiūros programa tiesiog atidaro kitą talpyklos poaplankį. Senas aplankas daugiau niekada nebenaudojamas ir galiausiai pašalinamas atliekant LRU išvalymą. Nėra jokio versijos žymų protokolo, jokios laiko žymų palyginimo logikos, kuri galėtų nukrypti, ir jokios galimybės pateikti seną bitų žemėlapį pakeistam dokumentui

Tai yra ta pati „turinio tapatybės kaip talpyklos rakto“ idėja, kurią naudoja projektų surinkimo sistemų sprendimai, ir ji pasiteisina pašalindama galimas klaidų situacijas. Talpykla, kuri saugo failus tiesiog pagal dokumento pavadinimą, turi aiškiai aptikti pakeitimus, o kiekvienas toks tikrinimas yra programavimo klaida, laukianti išskirtinio atvejo: failas atkurtas iš atsarginės kopijos su sena laiko žyma, išsaugojimas išlaikant tą patį dydį, kelias, viename kompiuteryje lyginamas skiriant didžiąsias ir mažąsias raides, o kitame — ne. Sujungus visus tris signalus į aplanko pavadinimą, pasenimas tampa struktūriškai neįmanomas, o ne procedūriškai tikrinamas

Kodėl pasukimas sąmoningai neįtrauktas į disko raktą

Talpyklos failo pavadinimas koduoja puslapio numerį ir mastelį, ir nieko daugiau — vaizdo pasukimo kampo čia sąmoningai nėra. Disko talpykloje visada saugomi nepasukti atvaizdavimo baitai; kai talpykloje išsaugotas puslapis įkeliamas esant pasuktam vaizdui, pikseliai po dekodavimo perorientuojami atmintyje. Jei pasukimas būtų disko rakto dalis, naudotojas, sukdamas vaizdą per keturis kampus, įrašytų tą patį puslapį į diską keturis kartus, taip keturgubai padidindamas talpyklą be jokios papildomos naudos, nes pasukimas yra pigus pikselių sumaišymas atmintyje, palyginti su pilnu atvaizdavimo ciklu

Atmintyje esanti bitų žemėlapių talpykla naudoja sudėtinį raktą „Round(Zoom * 1000) * 4 + Rotation div 90“, nes ten pasuktas bitų žemėlapis yra būtent tai, ko reikia piešimo apdorojimo programai. Foninė išankstinio įkėlimo gija (prefetch thread) laikosi tos pačios tvarkos: ji visada atvaizduoja nepasuktus puslapius ir praneša paprastus mastelio raktus, o pagrindinė gija pritaiko pasukimą apdorodama rezultatus — tai ta pati taisyklė „pagrindinė gija valdo talpyklą“, aptarta straipsnyje apie lygiagretų puslapių atvaizdavimą ir gijų saugumą. Pasukimo nenaudojimas visur žemiau atminties sluoksnio reiškia vieną kanoninį atvaizdavimą vienam puslapiui ir masteliui visur, kur saugojimas yra brangus

Kaip LRU išvalymas apriboja talpyklos aplanko dydį?

Talpykla išlaiko iki dešimties dokumentų aplankų ir, atsiradus naujiems dokumentams, pašalina seniausiai naudotus. Naudojimo laikas sekamas per failą „last-used.marker“ kiekviename dokumento aplanke, kurio laiko žyma atnaujinama tiek rašant į talpyklą, tiek skaitant iš jos, todėl dokumentas, kurį tiesiog skaitote iš naujo, išlieka aktyvus be pakartotinio įrašymo. Šiuo metu atidarytam dokumentui priklausantis aplankas yra apsaugotas nuo trynimo, todėl išvalymas niekada negali pašalinti puslapių iš po aktyvaus rodinio

Visos disko operacijos talpykloje praryja savo išimtis

Kiekviena disko operacija talpykloje — atspaudų kūrimas, aplankų kūrimas, žymeklių atnaujinimas, puslapių skaitymas bei rašymas, išvalymas — praryja savo išimtis (exceptions). Tai yra sąmoninga pozicija, o ne aplaidumas: talpykla yra geriausios pastangos greitintuvas, o pilnas diskas, tik skaitymui skirtas aplankas ar antivirusinės programos užraktas turi tik sugrąžinti peržiūros programą prie įprasto atvaizdavimo, bet niekada nesukelti puslapio rodymo klaidos. Blogiausia, ką gali padaryti talpyklos įvesties/išvesties klaida, yra tiesiog praleistas talpyklos atitikmuo

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;

Kaip padaryti PDF peržiūros programą pritaikytą DPI Delphi aplinkoje?

„TPDFlibViewer“ visus nuo DPI priklausomus skaičiavimus nukreipia per vieną lauką, ir būtent šis vieno taško dizainas padaro monitoriaus lygio DPI palaikymą lengvai valdomą. Išdėstymo, išankstinio įkėlimo ir atvaizdavimo kodas skaito vieną reikšmę „FScreenDPI“; esant 100% masteliui, vienas PDF taškas atitinka „ScreenDPI / 72“ pikselius. Viešos savybės „ScreenDPI“ nustatymas išvalo bitų žemėlapių talpyklą, perstato išdėstymą, atnaujina slinkties diapazonus ir pažymi langą perpiešimui, todėl DPI keitimas yra vienas priskyrimas, kurį visi vartotojai seka automatiškai. Ta pati pikselių matematika yra svarbi ir atvaizduojant spausdintuvui, o ne monitoriui — ši tema aprašyta straipsnyje apie spausdinimo peržiūrą ir įrenginio konteksto atvaizdavimą

Aptikimas vyksta „CreateWnd“ perrašyme (override), o ne konstruktoriuje, nes „GetDpiForWindow“ reikalinga galiojanti lango rankenėlė (handle), o konstruktorius pasileidžia gerokai prieš jai atsirandant. Kol suveikia „CreateWnd“, valdiklis išlaiko senąją numatytąją 96 DPI reikšmę, kad išdėstymo matematika išliktų teisinga. Pati „GetDpiForWindow“ yra „Windows 10 1607+“ API, todėl valdiklis susieja ją dinamiškai per „GetProcAddress“, o senesnėse sistemose naudoja „GetDeviceCaps(LOGPIXELSY)“ — modulis vis tiek užsikrauna visur, o senesnės „Windows“ versijos tiesiog praneša sistemos DPI

Kas nutinka, kai langas perkeliamas į 4K monitorių?

„Windows“ atsiunčia „WM_DPICHANGED“ pranešimą, kai monitoriaus DPI atžvilgiu pritaikytas langas pereina į monitorių su kitu mastelio koeficientu, ir „TPDFlibViewer“ apdoroja jį tiesiogiai. Apdorojimo programa nuskaito naują DPI iš žemesniojo „wParam“ žodžio, priskiria jį per „SetScreenDPI“ (kas atmeta dabar jau netinkamą bitų žemėlapių talpyklą ir iš naujo išdėsto puslapius) ir priima siūlomą lango stačiakampį, kurį „Windows“ perduoda „lParam“ per „SetWindowPos“, kad perkelto lango loginis dydis išliktų stabilus. Naudotojas mato, kaip dokumentas atvaizduojamas iš naujo aiškiai ir ryškiai nauju masteliu, užuot matęs OS ištemptą bitų žemėlapį

// 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

Ribos, kurias verta žinoti prieš išleidžiant programą

Trys praktiniai apribojimai nusipelno vietos jūsų dizaino peržiūroje. Pirma, „WM_DPICHANGED“ pranešimas ateina tik tada, kai procesas sutinka su tuo: jūsų programos manifeste turi būti deklaruotas DPI pritaikymas kiekvienam monitoriui („per-monitor v2“ dabartinėje „Windows“ versijoje), kas Delphi IDE yra DPI pritaikymo nustatymas po programos parinktimis (Application options). Sistemos DPI pritaikytas procesas niekada negauna šio pranešimo, o valdiklis atvaizdoja puslapius pagal DPI, kurį aptiko lango kūrimo metu. Antra, disko talpykla keičia disko vietą į greitį — dešimties dokumentų puslapių bitų žemėlapiai skirtingais masteliais užima realios vietos diske, todėl nukreipkite „DiskCacheFolder“ į vietą, kurioje patogu augti, o ne į klajojantį profilį (roaming profile). Trečia, talpyklos aplankui reikalingi rašymo leidimai veikiančiam naudotojui; be jų peržiūros programa vis tiek veikia, tyliai, su nuliu atitikmenų talpykloje — testavimo metu patikrinkite „DiskCacheHits“, jei įtariate, kad talpykla neveikia

Kartu šios dvi funkcijos pakeičia programos, kurioje gausu dokumentų, suvokiamą kokybę labiau nei daugelis kitų atvaizdavimo optimizavimų: dokumentai, kuriuos jūsų naudotojai aplanko pakartotinai, atsidaro akimirksniu, o vaizdas išlieka ryškus bet kuriame monitoriuje, kuriame atsiduria. Abi šios funkcijos tiekiamos kaip įprastos „TPDFlibViewer“ savybės „losLab PDF Library“ bibliotekoje, todėl jų pritaikymas yra vienos popietės pajungimo darbas, o ne atvaizdavimo variklio projektas