Teknisk artikel

PDF-fremviserens diskcache og DPI pr. skærm i Delphi

To klager følger enhver indlejret PDF-fremviser: Store dokumenter re-renderes fra bunden ved hver start, og tekst bliver sløret i det øjeblik, vinduet trækkes over på en skærm med høj DPI. losLab PDF Library besvarer begge i sin TPDFlibViewer-kontrol: Egenskaben DiskCacheFolder muliggør en disk-side-cache på tværs af sessioner, der er styret af dokumentets fingeraftryk, og egenskaben ScreenDPI, understøttet af GetDpiForWindow og en WM_DPICHANGED-håndtering, giver kontrollen ægte DPI-bevidsthed pr. skærm

Begge problemer er værd at tage alvorligt, fordi brugerne bemærker dem med det samme. En scannet kontrakt på 400 sider, som det tog otte sekunder at gøre rullebar i går, tager igen otte sekunder i dag, selvom intet i filen har ændret sig. Og på et skrivebord med blandet DPI ser en fremviser, der renderer ved den primære skærms 96 DPI, fin ud der, men synligt sløret på 4K-panelet ved siden af. Denne artikel gennemgår, hvordan TPDFlibViewer løser hver enkelt, og mere nyttigt, hvorfor designet endte, som det gjorde. Hvis du er ny til selve kontrollen, kan du starte med oversigten over den interaktive PDF-fremviserkontrol og vende tilbage hertil for at læse om ydeevnelaget

Hvordan cacher du renderede PDF-sider på tværs af sessioner i Delphi?

Tildel en mappe til TPDFlibViewer.DiskCacheFolder, og kontrollen gemmer renderede sidebitmaps på disken, så et nyligt vist dokument genåbnes øjeblikkeligt i stedet for at re-rendere. På disken er layoutet <Folder>\PDFlibPas-PageCache\<document fingerprint>\p<page>_z<zoomkey>.bin: Én undermappe pr. dokument, én fil pr. side- og zoom-kombination. Alt andet er automatisk; der er intet cache-API at kalde og intet at rydde

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;

Tælleren DiskCacheHits rapporterer, hvor mange siderenderinger diskcachen sparede i den aktuelle session, hvilket gør gevinsten målbart frem for anekdotisk. På et dokument, du har lukket og genåbnet, øges den for hver side, der ruller ind i visningen uden en renderingspassage

Hvorfor cachenøglen er et fingeraftryk og ikke et versionsnummer

Dokumentets fingeraftryk er et 64-bit FNV-1a-hash over strengen path|size|last write time, formateret som seksten heksadecimale cifre. Denne trippel er hele invalideringsstrategien: Når filen redigeres, ændres dens størrelse eller skrivetid, hashen ændres, og fremviseren åbner blot en anden undermappe i cachen. Den forældede mappe konsulteres aldrig igen og falder til sidst for LRU-beskæring. Der er ingen versionsmærkeprotokol at vedligeholde, ingen tidsstempelsammenligningslogik, der kan skride, og ingen måde, hvorpå et gammelt bitmap kan serveres for et ændret dokument

Dette er den samme idé om "indholdsidentitet som cachenøgle", som build-systemer bruger, og den beviser sit værd i de fejltilstande, den fjerner. En cache, der gemmer filer under dokumentets almindelige navn, skal registrere redigeringer eksplicit, og enhver eksplicit kontrol is en potentiel fejl ved grænsetilfælde: En fil gendannet fra sikkerhedskopi med et gammelt tidsstempel, en lagring, der bevarer størrelsen, en sti sammenlignet med forskel på store og små bogstaver på én maskine og ikke på en anden. At hashe alle tre signaler ind i mappens navn gør forældelse strukturelt umulig frem for proceduremæssigt kontrolleret

Hvorfor rotation bevidst er udeladt fra disknøglen

Cachefilens navn koder sidenummer og zoom, og intet andet — visningsrotationsvinklen er bevidst fraværende. Diskcachen gemmer altid de uroterede renderingsbytes; når en cachelagret side indlæses, mens visningen er roteret, omorienteres pixlerne i hukommelsen efter afkodning. Hvis rotation var en del af nøglen på disken, ville en bruger, der cykler gennem de fire visningsvinkler, skrive den samme side til disken fire gange, hvilket ville firedoble cachevæksten for nul ekstra information, da rotation er en billig pixelblanding sammenlignet med en fuld renderingspassage

Bitmapcachen i hukommelsen bruger en sammensat nøgle, Round(Zoom * 1000) * 4 + Rotation div 90, fordi det roterede bitmap der er præcis det, malingshåndteringen har brug for. Baggrunds-prefetch-tråden følger samme disciplin: Den renderer altid uroterede sider og rapporterer almindelige zoomnøgler, og hovedtråden anvender rotation, når resultaterne drænes — den samme regel om, at hovedtråden ejer cachen, som diskuteret i parallel siderendering og trådsikkerhed. At holde rotation ude af alt under hukommelseslaget betyder én kanonisk rendering pr. side og zoom, overalt hvor det er dyrt at gemme

Hvordan holder LRU-beskæring cachemappen begrænset?

Cachen gemmer op til ti dokumentmapper og beskærer de mindst nyligt brugte, når nye dokumenter ankommer. Hvor nyligt de har været brugt spores af en last-used.marker-fil i hver dokumentmappe, hvis tidsstempel berøres ved både cacheskrivninger og cachelæsninger, så et dokument, du blot genlæser, forbliver varmt uden at blive skrevet igen. Mappen, der tilhører det aktuelt åbne dokument, er fritaget for sletning, så beskæring kan aldrig trække sider ud under den aktive visning

Enhver diskoperation i cachen — fingeraftryk, mappeoprettelse, markøropdateringer, sidelæsninger og cacheskrivninger, beskæring — opsluger sine egne undtagelser. Det er en bevidst holdning, ikke sløseri: Cachen er en accelerator, der gør sit bedste, og en fuld disk, en skrivebeskyttet mappe eller en antiviruslås skal degradere fremviseren to at "rendere som før", aldrig til at "fejle i at vise siden". Det værste resultat, en cache-IO-fejl kan producere, er et tabt 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;

Hvordan gør du en PDF-fremviser DPI-bevidst i Delphi?

TPDFlibViewer leder enhver DPI-afhængig beregning gennem et enkelt felt, og dette design med et enkelt ændringspunkt er det, der gør understøttelse pr. skærm medgørlig. Layout-, prefetch- og renderingskode læser alle én FScreenDPI-værdi; ved 100% zoom kortlægges ét PDF-punkt til ScreenDPI / 72 pixels. Den offentlige ScreenDPI-egenskabs-setter rydder bitmapcachen, genopbygger layoutet, opdaterer rulleområderne og invaliderer vinduet, så ændring af DPI is én tildeling, hvor alle forbrugere følger automatisk. Den samme pixelmatematik har betydning, når du renderer til en printer i stedet for en skærm, et emne der er dækket i forhåndsvisning og rendering af enheds-kontekst

Detektering sker i en CreateWnd-tilsidesættelse frem for i konstruktøren, fordi GetDpiForWindow kræver et gyldigt vindueshåndtag, og konstruktøren kører længe før, et sådant eksisterer. Indtil CreateWnd aktiveres, beholder kontrollen standardværdien på 96, så layoutmatematikken forbliver fornuftig. GetDpiForWindow er i sig selv en Windows 10 1607+-API, så kontrollen binder den dynamisk med GetProcAddress og falder tilbage på GetDeviceCaps(LOGPIXELSY) på ældre systemer — enheden indlæses stadig overalt, og ældre Windows-versioner rapporterer blot system-DPI

Hvad sker der, når vinduet flyttes to en 4K-skærm?

Windows sender WM_DPICHANGED, når et vindue med DPI-bevidsthed pr. skærm krydser over til en skærm med en anden skaleringsfaktor, og TPDFlibViewer håndterer det direkte. Håndteringen læser den nye DPI fra det lave ord af wParam, tildeler det via SetScreenDPI — hvilket fjerner den nu forkerte bitmapcache og udlægger siderne igen — og accepterer derefter det foreslåede vinduesrektangel, som Windows sender i lParam via SetWindowPos, hvilket holder den logiske klientstørrelse stabil under flytningen. Brugeren ser dokumentet re-renderet skarpt i den nye skala i stedet for et bitmap strakt af styresystemet

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

Grænser, der er værd at kende før du sender til produktion

Tre praktiske grænser fortjener en plads i din designgennemgang. For det første ankommer WM_DPICHANGED kun, hvis processen tilmelder sig: Dit applikationsmanifest skal erklære DPI-bevidsthed pr. skærm (per-monitor v2 på nuværende Windows), hvilket i Delphi-IDE'en er indstillingen for DPI-bevidsthed under Application-indstillinger. En proces med system-DPI-bevidsthed modtager aldrig meddelelsen, og kontrollen renderer derefter ved den DPI, den detekterede ved oprettelse af vinduet. For det andet bytter diskcachen diskplads for hastighed — ti dokumenters værdi af sidebitmaps på tværs af flere zoomniveauer er reel lagerplads, så peg DiskCacheFolder mod en placering, du er tryg ved vokser, ikke en roaming-profil. Tredje, cachemappen skal have skriveadgang for den kørende bruger; uden denne fungerer fremviseren stadig lydløst med nul hits — tjek DiskCacheHits under test, hvis du har mistanke om, at cachen ikke aktiveres

Tilsammen ændrer de to funktioner den oplevede kvalitet af en dokumenttung applikation mere end de fleste rendering-optimeringer: Dokumenter, som dine brugere besøger igen, åbnes med det samme, og visningen forbliver skarp på den skærm, den lander på. Begge leveres som almindelige egenskaber på TPDFlibViewer i losLab PDF Library, så implementering er en eftermiddags arbejde frem for et renderingsmotorprojekt