Teknisk artikkel

PDF-viser side-hurtigbuffer på disk og DPI per skjerm i Delphi

To klager følger enhver innebygd PDF-viser: store dokumenter tegnes på nytt fra bunnen av ved hver oppstart, og tekst blir uklar i det øyeblikket vinduet dras over til en skjerm med høy DPI. losLab PDF Library svarer på begge i sin TPDFlibViewer-kontroll: egenskapen DiskCacheFolder muliggjør et side-hurtigbuffer på disk på tvers av sesjoner som er identifisert med dokument-fingeravtrykk, og egenskapen ScreenDPI, støttet av GetDpiForWindow og en WM_DPICHANGED-håndterer, gir kontrollen ekte DPI-bevissthet per skjerm

Begge problemene er verdt å ta på alvor fordi brukerne legger merke til dem umiddelbart. En skannet kontrakt på 400 sider som tok åtte sekunder å bli rullbar i går, tar åtte sekunder igjen i dag, selv om ingenting i filen er endret. Og på et skrivebord med blandet DPI ser en viser som tegnes med primærskjermens 96 DPI bra ut der, men er synlig uklar på 4K-panelet ved siden av. Denne artikkelen går gjennom jakten på hvordan TPDFlibViewer løser hver av dem, og enda viktigere, hvorfor designet landet der det gjorde. Hvis du er ny til selve kontrollen, kan du starte med oversikten over den interaktive PDF-visningskontrollen og komme tilbake hit for ytelseslaget

Hvordan lagrer du tegnede PDF-sider i hurtigbuffer på disk på tvers av sesjoner i Delphi?

Tilordne en mappe til TPDFlibViewer.DiskCacheFolder, så lagrer kontrollen tegnede side-bitmaps til disk, slik at et nylig vist dokument åpnes umiddelbart i stedet for å tegnes på nytt. På disken er oppsettet <Mappe>\PDFlibPas-PageCache\<dokument-fingeravtrykk>\p<side>_z<zoomnøkkel>.bin: én undermappe per dokument, én fil per side- og zoom-kombinasjon. Alt annet skjer automatisk; det er ikke noe API for hurtigbufferet som må kalles, og ingenting som må tømmes

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;

Telleren DiskCacheHits rapporterer hvor mange sidetegninger disk-hurtigbufferet sparte i den gjeldende sesjonen, noe som gjør gevinsten målbare fremfor anekdotisk. For et dokument du lukket og åpnet igjen, vil hver side som ruller inn i visningen uten en tegningspassering, øke telleren

Hvorfor hurtigbuffernøkkelen er et fingeravtrykk, ikke et versjonsnummer

Dokumentets fingeravtrykk er en 64-bit FNV-1a-hash over strengen bane|størrelse|siste skrivetid, formatert som seksten heksadesimale sifre. Denne trippelen er hele ugyldiggjøringsstrategien: når filen redigeres, endres størrelsen eller skrivetiden, hashen endres, og viseren åpner ganske enkelt en annen undermappe i hurtigbufferet. Den utdaterte mappen blir aldri konsultert igjen og blir til slutt fjernet av LRU-beskjæring. Det er ingen protokoll for versjonsmerking å vedlikeholde, ingen tidsstempel-sammenligningslogikk som kan drifte, og ingen mulighet for at et gammelt bitmap serveres for et modifisert dokument

Dette er den samme ideen om "innholdsidentitet som hurtigbuffernøkkel" som byggesystemer bruker, og den viser sin verdi i feilmodusene den fjerner. Et hurtigbuffer som lagrer filer under dokumentets vanlige navn, må oppdage redigeringer eksplisitt, og hver eksplisitt sjekk er en potensiell feil som venter på et grensetilfelle: en fil gjenopprettet fra sikkerhetskopi med et gammelt tidsstempel, en lagring som bevarer størrelsen, en bane sammenlignet med skille mellom store og små bokstaver på én maskin og ikke på en annen. Ved å hashe alle tre signalene inn i mappenavnet blir foreldelse strukturelt umulig fremfor prosedyremessig kontrollert

Hvorfor rotasjon bevisst er utelatt fra disknøkkelen

Hurtigbufferfilnavnet koder sidenummer og zoom, og ingenting annet — visningsrotasjonsvinkelen er bevisst fraværende. Disk-hurtigbufferet lagrer alltid de uroterte tegningsbytene; når en bufret side lastes mens visningen er rotert, blir pikslene reorientert i minnet etter dekoding. Hvis rotasjon var en del av nøkkelen på disken, ville en bruker som blar gjennom de fire visningsvinklene skrive samme side til disk fire ganger, noe som firedobler veksten av hurtigbufferet for null ekstra informasjon, siden rotasjon er en billig piksel-omstokking sammenlignet med en full tegningspassering

Bitmap-hurtigbufferet i minnet bruker en sammensatt nøkkel, Round(Zoom * 1000) * 4 + Rotation div 90, fordi der er det roterte bitmapet akkurat det tegningshåndtereren trenger. Bakgrunnstråden for forhåndshenting følger samme disiplin: den tegner alltid uroterte sider og rapporterer enkle zoomnøkler, og hovedtråden bruker rotasjon når den henter ut resultater — den samme regelen om at hovedtråden eier hurtigbufferet som er diskutert i parallell sidetegning og trådsikkerhet. Å holde rotasjon unna alt under minnelaget betyr én kanonisk tegning per side og zoom, overalt der det er kostbart å lagre

Hvordan holder LRU-beskjæring hurtigbuffermappen begrenset?

Hurtigbufferet beholder opptil ti dokumentmapper og beskjærer de minst nylig brukte når nye dokumenter ankommer. Hvor nylig en mappe har vært brukt spores av en last-used.marker-fil i hver dokumentmappe, hvis tidsstempel oppdateres ved både skriving og lesing i hurtigbufferet, slik at et dokument du bare leser på nytt forblir "varmt" uten å bli skrevet på nytt. Mappen som tilhører det for øyeblikket åpne dokumentet er unntatt fra sletting, slik at beskjæring aldri kan fjerne sidetegninger rett foran nesen på den aktive visningen

Hver diskoperasjon i hurtigbufferet — generering av fingeravtrykk, opprettelse av mapper, oppdatering av markører, lesing og skriving av sider, beskjæring — svelger sine egne unntak. Dette er en bevisst holdning, ikke slurv: hurtigbufferet er en akselerator som gjør sitt beste, og en full disk, en skrivebeskyttet mappe eller en antivirus-lås må bare degradere viseren til å "tegne sider som før", nevnt til "feil ved visning av siden". Det verste resultatet en IO-feil i hurtigbufferet kan produsere er et bommet treff

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;

Hvordan gjør du en PDF viser DPI-bevisst i Delphi?

TPDFlibViewer ruter hver DPI-avhengig beregning gjennom et enkelt felt, og dette designet med et enkelt endringspunkt er det som gjør støtte per skjerm håndterbar. Layout-, forhåndshentings- og tegningskode leser alle én enkelt FScreenDPI-verdi; ved 100 % zoom kartlegges ett PDF-punkt til ScreenDPI / 72 piksler. Setter-funksjonen for den offentlige egenskapen ScreenDPI tømmer bitmap-hurtigbufferet, bygger opp layouten på nytt, oppretter rulleområdene og ugyldiggjør vinduet, slik at endring av DPI er én enkelt tilordning som alle konsumenter følger automatisk. Den samme pikselmatematikken har betydning når du tegner for en skriver i stedet for en skjerm, et emne som dekkes i forhåndsvisning og tegning i enhetskontekst

Deteksjon skjer i en CreateWnd-tilsidesettelse i stedet for i konstruktøren, fordi GetDpiForWindow trenger et gyldig vindushåndtak, og konstruktøren kjører lenge før et slikt eksisterer. Før CreateWnd utløses, har kontrollen standardverdien 96, slik at layoutmatematikken forblir fornuftig. GetDpiForWindow i seg selv er et Windows 10 1607+-API, så kontrollen binder det dynamisk med GetProcAddress og faller tilbake på GetDeviceCaps(LOGPIXELSY) på eldre systemer — enheten lastes fremdeles overalt, og eldre Windows rapporterer ganske enkelt systemets DPI

Hva skjer når vinduet flyttes til en 4K-skjerm?

Windows sender WM_DPICHANGED når et vindu med DPI-bevissthet per skjerm flyttes over til en skjerm med en annen skaleringsfaktor, og TPDFlibViewer håndterer dette direkte. Håndterereren leser den nye DPI-en fra det lave ordet av wParam, tilordner den via SetScreenDPI — som sletter det nå feilaktige bitmap-hurtigbufferet og legger ut sidene på nytt — og godtar deretter det foreslåtte vindusrektangelet som Windows sender i lParam via SetWindowPos, noe som holder den logiske klientstørrelsen stabil under flyttingen. Brukeren ser at dokumentet tegnes skarpt på nytt i den nye skalaen i stedet for et bitmap strukket av operativsystemet

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

Grenser verdt å kjenne til før du leverer

Tre praktiske grenser fortjener en plass i din designrevisjon. For det første kommer WM_DPICHANGED bare hvis prosessen melder seg på: applikasjonsmanifestet ditt må deklarere DPI-bevissthet per skjerm (per-monitor v2 på gjeldende Windows), som i Delphi IDE er DPI-bevissthetsinnstillingen under Application options. En system-DPI-bevisst prosess mottar aldri meldingen, og kontrollen tegner da med den DPI-en den detekterte ved opprettelsen av vinduet. For det andre bytter disk-hurtigbufferet diskplass mot hastighet — ti dokumenters verdi av side-bitmaps over flere zoomnivåer er reell lagringsplass, så pek DiskCacheFolder til et sted du er komfortabel med at vokser, ikke en profil på avveie (roaming profile). For det tredje trenger hurtigbuffermappen skrivetilgang for den kjørende brukeren; uten dette fungerer viseren fremdeles, lydløst, med null treff — sjekk DiskCacheHits under testing hvis du har mistanke om at hurtigbufferet ikke er aktivt

Tilsammen endrer de to funksjonene den oppfattede kvaliteten på en dokumenttung applikasjon mer enn de fleste tegningsoptimaliseringer: dokumenter brukerne dine besøker på nytt åpnes umiddelbart, og visningen forblir skarp på hvilken som helst skjerm den havner på. Begge leveres som vanlige egenskaper på TPDFlibViewer i losLab PDF Library, så å ta dem i bruk er en ettermiddag med oppkobling i stedet for et prosjekt med en tegningsmotor