Tekninen artikkeli

PDF-katseluohjelman levyn välimuisti ja näyttökohtainen DPI Delphissä

Jokaisen upotetun PDF-katseluohjelman mukana seuraa kaksi valitusta: suuret asiakirjat piirretään uudelleen tyhjästä jokaisella käynnistyskerralla, ja teksti muuttuu epätarkaksi heti, kun ikkuna vedetään korkean DPI-tarkkuuden näytölle. losLab PDF Library vastaa molempiin haasteisiin TPDFlibViewer-komponentillaan: DiskCacheFolder-ominaisuus mahdollistaa istuntojen välisen levyn sivuvälimuistin, jonka avaimena käytetään asiakirjan sormenjälkeä, ja ScreenDPI-ominaisuus, jota tukevat GetDpiForWindow ja WM_DPICHANGED-käsittelijä, antaa komponentille todellisen näyttökohtaisen DPI-tietoisuuden

Molemmat ongelmat on syytä ottaa vakavasti, sillä käyttäjät huomaavat ne välittömästi. Skannattu 400-sivuinen sopimus, jonka vierittäminen kesti eilen kahdeksan sekuntia, kestää tänään taas kahdeksan sekuntia, vaikka mikään tiedostossa ei muuttunut. Ja sekalaisten DPI-tasojen työpöydällä katseluohjelma, joka piirtää päämonitorin 96 DPI:n tarkkuudella, näyttää siinä hyvältä, mutta on selvästi pehmeä sen vieressä olevalla 4K-paneelilla. Tässä artikkelissa käydään läpi, miten TPDFlibViewer ratkaisee nämä ongelmat ja, mikä hyödyllisempää, miksi suunnittelussa päädyttiin kyseisiin ratkaisuihin. Jos komponentti itsessään on sinulle uusi, aloita tutustumalla interaktiivisen PDF-katselukomponentin yleiskatsaukseen ja palaa sitten tämän suorituskykykerroksen pariin

Miten PDF-sivut tallennetaan välimuistiin istuntojen välillä Delphissä?

Kun määrität kansion TPDFlibViewer.DiskCacheFolder-ominaisuuteen, komponentti tallentaa piirretyt sivujen bittikartat levylle, jolloin äskettäin katseltu asiakirja aukeaa heti uudelleenpiirtämisen sijaan. Levyn rakenne on <Kansio>\PDFlibPas-PageCache\<asiakirjan sormenjälki>\p<sivu>_z<zoomausavain>.bin: yksi alikansio asiakirjaa kohden ja yksi tiedosto sivu- ja zoomausyhdistelmää kohden. Kaikki muu tapahtuu automaattisesti; mitään välimuistin rajapintaa ei tarvitse kutsua eikä mitään tarvitse tyhjentää manuaalisesti

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;

DiskCacheHits-laskuri ilmoittaa, kuinka monta sivun piirtoa levyn välimuisti säästi nykyisessä istunnossa, mikä tekee hyödystä mitattavan pelkän arvailun sijaan. Suljetussa ja uudelleen avatussa asiakirjassa jokainen sivu, joka vierittyy näkymään ilman uutta piirtovaihetta, kasvattaa tätä laskuria

Miksi välimuistiavain on sormenjälki eikä versionumero

Asiakirjan sormenjälki on 64-bittinen FNV-1a-tiiviste merkkijonosta polku|koko|viimeisin kirjoitusaika, muotoiltuna kuudeksitoista heksadesimaalimerkiksi. Tämä kolmikko muodostaa koko välimuistin mitätöintistrategian: kun tiedostoa muokataan, sen koko tai kirjoitusaika muuttuu, tiiviste muuttuu ja katseluohjelma yksinkertaisesti avaa eri välimuistin alikansion. Vanhaan kansioon ei enää koskaan viitata, ja se poistetaan lopulta LRU-karsinnan yhteydessä. Ei ole ylläpidettävää versiotunnisteprotokollaa, ei aikaleimojen vertailulogiikkaa, joka voisi ajautua virheelliseksi, eikä mitään keinoa, jolla vanha bittikartta voitaisiin tarjota muokatulle asiakirjalle

Tämä on sama "sisällön identiteetti välimuistiavaimena" -idea, jota kääntöjärjestelmät käyttävät, ja se osoittaa hyötynsä poistamalla mahdolliset virhetilat. Välimuistin, joka tallentaa tiedostot asiakirjan tavallisella nimellä, on tunnistettava muokkaukset erikseen, ja jokainen erillinen tarkistus on altis erikoistapauksille: varmuuskopiosta palautettu tiedosto vanhalla aikaleimalla, tallennus joka säilyttää saman koon tai polku, jota vertaillaan kirjainkoosta riippuvasti yhdellä koneella ja toisella ei. Kaikkien kolmen signaalin tiivistäminen kansion nimeksi tekee vanhentuneisuudesta rakenteellisesti mahdotonta sen sijaan, että sitä tarkistettaisiin menettelyllisesti

Miksi kierto on jätetty tietoisesti pois levyn avaimesta

Välimuistitiedoston nimi koodaa sivunumeron ja zoomauksen, eikä mitään muuta — näkymän kiertokulma puuttuu tarkoituksella. Levyn välimuisti tallentaa aina kiertämättömät piirtotavut; kun välimuistiin tallennettu sivu ladataan näkymän ollessa kierretty, pikselit suunnataan uudelleen muistissa purkamisen jälkeen. Jos kierto olisi osa levyavaimen rakennetta, neljää kiertokulmaa selaava käyttäjä kirjoittaisi saman sivun levylle neljä kertaa. Tämä nelinkertaistaisi välimuistin kasvun tuomatta lisähyötyä, sillä kierto on halpa pikselien uudelleenjärjestely verrattuna täyteen piirtovaiheeseen

Muistissa oleva bittikarttavälimuisti käyttää yhdistettyä avainta Round(Zoom * 1000) * 4 + Rotation div 90, koska muistissa kierretty bittikartta on juuri se, mitä piirtokäsittelijä tarvitsee. Taustalla toimiva esihakusäie (prefetch thread) noudattaa samaa periaatetta: se piirtää aina kiertämättömiä sivuja ja ilmoittaa tavalliset zoomausavaimet, ja pääsäie soveltaa kiertoa tulosten noudon yhteydessä — tämä on sama pääsäikeen omistaman välimuistin sääntö, jota käsitellään rinnakkaista sivujen piirtämistä ja säieturvallisuutta käsittelevässä artikkelissa. Kierron pitäminen poissa kaikesta muistikerroksen alapuolella tarkoittaa yhtä kanonista piirtoa sivua ja zoomausta kohden kaikkialla, missä tallentaminen on kallista

Miten LRU-karsinta pitää välimuistikansion koon rajoitettuna?

Välimuisti säilyttää enintään kymmenen asiakirjakansiota ja karsii vähiten käytetyt (LRU), kun uusia asiakirjoja avataan. Käyttötiheyttä seurataan kussakin asiakirjakansiossa olevalla last-used.marker-tiedostolla, jonka aikaleimaa päivitetään sekä välimuistiin kirjoitettaessa että sieltä luettaessa. Siten asiakirja, jota vain luetaan uudelleen, pysyy aktiivisena ilman, että sitä kirjoitetaan uudelleen. Parhaillaan auki olevalle asiakirjalle kuuluvaa kansiota ei voida poistaa, joten karsinta ei koskaan poista sivuja aktiivisen näkymän alta

Jokainen välimuistin levytoiminto — sormenjälkien laskenta, kansion luominen, merkintöjen kosketus, sivujen luku ja kirjoitus sekä karsinta — vaimentaa omat poikkeuksensa. Tämä on tietoinen ratkaisu, ei huolimattomuutta: välimuisti on apuohjelma, joka toimii parhaan kykynsä mukaan. Täysi levy, vain luku -oikeudellinen kansio tai virustorjunnan lukitus saa katseluohjelman palautumaan tilaan "piirtää kuten ennenkin", eikä se saa koskaan johtaa tilanteeseen, jossa sivua ei voida näyttää lainkaan. Pahin mahdollinen välimuistin IO-virheen seuraus on osuman jääminen väliin (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;

Miten PDF-katseluohjelmasta tehdään DPI-tietoinen Delphissä?

TPDFlibViewer ohjaa jokaisen DPI-riippuvaisen laskennan yhden ainoan kentän kautta, ja tämä keskitetty suunnittelu tekee näyttökohtaisen tuen toteuttamisesta hallittavaa. Asettelu, esihaku ja piirtokoodi lukevat kaikki yhtä FScreenDPI-arvoa; 100 % zoomauksella yksi PDF-piste vastaa ScreenDPI / 72 pikseliä. Julkisen ScreenDPI-ominaisuuden asettaja tyhjentää bittikarttavälimuistin, rakentaa asettelun uudelleen, päivittää vieritysalueet ja invalidoi ikkunan, joten DPI:n muuttaminen vaatii vain yhden sijoituksen ja kaikki osat päivittyvät automaattisesti. Sama pikselimatematiikka on tärkeää piirrettäessä tulostimelle näytön sijaan, aihe jota käsitellään tulostuksen esikatselua ja laitekontekstiin piirtämistä käsittelevässä artikkelissa

Tunnistus tapahtuu CreateWnd-ylikirjoituksessa eikä konstruktorissa, koska GetDpiForWindow vaatii voimassa olevan ikkunakahvan (window handle) ja konstruktori suoritetaan kauan ennen sellaisen luomista. Ennen kuin CreateWnd laukeaa, komponentti käyttää oletusarvoa 96, jotta asettelulogiikka pysyy kunnossa. GetDpiForWindow on Windows 10 1607+ -rajapinta, joten komponentti sitoo sen dynaamisesti GetProcAddress-kutsulla ja turvautuu vanhemmissa järjestelmissä GetDeviceCaps(LOGPIXELSY)-funktioon — yksikkö latautuu edelleen kaikkialla, ja vanhemmat Windows-versiot raportoivat vain järjestelmän laajuisen DPI:n

Mitä tapahtuu, kun ikkuna siirretään 4K-näytölle?

Windows lähettää WM_DPICHANGED-viestin, kun näyttökohtaisesti tietoinen ikkuna siirtyy monitorille, jolla on eri skaalaustekijä, ja TPDFlibViewer käsittelee sen suoraan. Käsittelijä lukee uuden DPI:n wParam-parametrin alimmasta sanasta, asettaa sen SetScreenDPI-metodin kautta — mikä tyhjentää nyt virheellisen bittikarttavälimuistin ja asettelee sivut uudelleen — ja hyväksyy sitten ikkunansuosituksen, jonka Windows välittää lParam-parametrissa SetWindowPos-kutsun avulla. Tämä pitää loogisen ikkunakoon vakaana siirron aikana. Käyttäjä näkee asiakirjan piirtyvän terävänä uudella skaalalla sen sijaan, että käyttöjärjestelmä venyttäisi vanhaa bittikarttaa

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

Rajoitukset, jotka on hyvä tietää ennen toimitusta

Kolme käytännön rajoitusta ansaitsee paikan suunnittelukatselmuksessa. Ensinnäkin WM_DPICHANGED saapuu vain, jos prosessi on ottanut sen käyttöön: sovelluksen manifestissa on ilmoitettava näyttökohtainen DPI-tietoisuus (per-monitor v2 nykyisessä Windowsissa), mikä Delphi IDE:ssä valitaan Application options -kohdan DPI awareness -asetuksesta. Järjestelmän DPI-tietoinen prosessi ei koskaan vastaanota viestiä, ja komponentti piirtää tällöin ikkunan luonnin yhteydessä havaitun DPI:n mukaisesti. Toiseksi levyn välimuisti vaihtaa levytilaa nopeuteen — kymmenen asiakirjan sivujen bittikartat eri zoomaustasoilla vievät todellista tallennustilaa, joten osoita DiskCacheFolder sellaiseen paikkaan, jonka sallit kasvavan, ei siis roaming-profiiliin. Kolmanneksi välimuistikansio vaatii kirjoitusoikeuden käynnissä olevalle käyttäjälle; ilman sitä katseluohjelma toimii silti hiljaa ilman välimuistiosumia — tarkista DiskCacheHits testauksessa, jos epäilet, ettei välimuisti ole käytössä

Yhdessä nämä kaksi ominaisuutta parantavat asiakirjapainotteisen sovelluksen koettua laatua enemmän kuin useimmat piirto-optimoinnit: käyttäjien uudelleen avaamat asiakirjat latautuvat välittömästi, ja näkymä pysyy terävänä millä tahansa näytöllä. Molemmat toimitetaan tavallisina TPDFlibViewer-komponentin ominaisuuksina losLab PDF Library -kirjastossa Delphille, joten niiden käyttöönotto on yhden iltapäivän kytkentätyö eikä pitkäaikainen piirtomoottoriprojekti