Technisch artikel

PDF Viewer-schijfcache en DPI per monitor in Delphi

Er zijn twee klachten die bij elke ingebedde PDF-viewer terugkeren: grote documenten worden bij elke start volledig opnieuw gerenderd, en tekst wordt wazig zodra het venster naar een high-DPI-monitor wordt gesleept. losLab PDF Library lost beide problemen op in de control TPDFlibViewer: de eigenschap DiskCacheFolder maakt een sessie-overstijgende schijfpaginacache mogelijk op basis van de vingerafdruk van het document, en de eigenschap ScreenDPI, ondersteund door GetDpiForWindow en een WM_DPICHANGED-handler, zorgt voor een correcte DPI-detectie per monitor

Beide problemen zijn serieus te nemen omdat gebruikers ze onmiddellijk opmerken. Een gescand contract van 400 pagina's dat gisteren acht seconden nodig had om schuifbaar te worden, heeft vandaag opnieuw acht seconden nodig, hoewel er niets in het bestand is gewijzigd. En op een desktop met verschillende DPI-instellingen ziet een viewer die op de 96 DPI van de hoofdmonitor rendert er daar prima uit, maar is deze zichtbaar onscherp op het 4K-scherm ernaast. Dit artikel bespreekt hoe TPDFlibViewer beide problemen oplost en, belangrijker nog, waarom dit ontwerp is gekozen. Als u nog niet bekend bent met de control zelf, begin dan met het overzicht van de interactieve PDF-viewer control en kom later terug voor de prestatielaag

Hoe cacht u gerenderde PDF-pagina's over sessies heen in Delphi?

Wijs een map toe aan TPDFlibViewer.DiskCacheFolder en de control slaat gerenderde paginabitmaps op de schijf op, zodat een recent bekeken document direct opnieuw wordt geopend in plaats van opnieuw te moeten worden gerenderd. De structuur op de schijf is <Folder>\PDFlibPas-PageCache\<documentvingerafdruk>\p<pagina>_z<zoomkey>.bin: één submap per document, één bestand per pagina-en-zoomcombinatie. Al het andere verloopt automatisch; er is geen cache-API om aan te roepen en er hoeft niets te worden leeggemaakt

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;

De teller DiskCacheHits rapporteert hoeveel paginarenderings de schijfcache in de huidige sessie heeft bespaard, wat de winst meetbaar maakt in plaats van anekdotisch. Bij een document dat u hebt gesloten en opnieuw hebt geopend, verhoogt elke pagina die in beeld schuift zonder renderingsstap deze teller

Waarom de cache-sleutel een vingerafdruk is en geen versienummer

De documentvingerafdruk is een 64-bits FNV-1a-hash over de tekenreeks pad|grootte|laatste schrijftijd, geformatteerd als zestien hexadecimale cijfers. Deze combinatie vormt de volledige ongeldigverklaringstrategie: wanneer het bestand wordt bewerkt, verandert de grootte of de schrijftijd, verandert de hash en opent de viewer simpelweg een andere cache-submap. De verouderde map wordt nooit meer geraadpleegd en wordt uiteindelijk via LRU-opschoning verwijderd. Er is geen versietag-protocol te onderhouden, geen tijdstempelvergelijkingslogica die kan verlopen, en er is geen manier waarop een oude bitmap kan worden gebruikt voor een gewijzigd document

Dit is hetzelfde "inhoudsidentiteit als cache-sleutel"-idee dat buildsystemen gebruiken, en het bewijst zijn nut door de foutmodi die het elimineert. Een cache die bestanden opslaat onder de gewone naam van het document moet wijzigingen expliciet detecteren, en elke expliciete controle is een potentieel probleem bij uitzonderlijke situaties: een bestand dat is hersteld van een back-up met een oude tijdstempel, een opslagactie die de grootte behoudt, of een pad dat op de ene machine hoofdlettergevoelig wordt vergeleken en op de andere niet. Door al deze drie signalen in de mapnaam te hashen, is veroudering structureel onmogelijk in plaats van procedureel gecontroleerd

Waarom rotatie bewust buiten de schijfsleutel is gelaten

De naam van het cachebestand codeert het paginanummer en de zoom, en niets anders — de rotatiehoek van de weergave is bewust afwezig. De schijfcache slaat altijd de niet-geroteerde renderingsbytes op; wanneer een gecachte pagina wordt geladen terwijl de weergave is geroteerd, worden de pixels na het decoderen in het geheugen opnieuw georiënteerd. Als rotatie deel zou uitmaken van de sleutel op de schijf, zou een gebruiker die door de vier weergavehoeken bladert dezelfde pagina vier keer naar de schijf schrijven, wat de cachegroei verviervoudigt voor nul extra informatie, aangezien rotatie een goedkope pixelverschuiving is vergeleken met een volledige renderingsstap

De bitmapcache in het geheugen gebruikt wel een samengestelde sleutel, Round(Zoom * 1000) * 4 + Rotation div 90, omdat daar de geroteerde bitmap precies is wat de paint-handler nodig heeft. De prefetch-thread op de achtergrond volgt dezelfde discipline: deze rendert altijd niet-geroteerde pagina's en rapporteert gewone zoomsleutels, en de hoofd-thread past rotatie toe bij het verwerken van de resultaten — dezelfde regel dat de hoofd-thread de cache beheert die is besproken in parallelle paginarendering en draadveiligheid. Door rotatie buiten alles onder de geheugenlaag te houden, is er één canonieke rendering per pagina en zoom, overal waar het duur is om op te slaan

Hoe houdt LRU-opschoning de omvang van de cachemap begrensd?

De cache bewaart maximaal tien documentmappen en verwijdert de minst recent gebruikte wanneer er nieuwe documenten binnenkomen. De recentheid wordt bijgehouden door een last-used.marker-bestand in elke documentmap, waarvan de tijdstempel wordt bijgewerkt bij zowel het schrijven naar als het lezen uit de cache. Hierdoor blijft een document dat u alleen herleest actueel zonder dat het opnieuw hoeft te worden geschreven. De map van het momenteel geopende document is vrijgesteld van verwijdering, zodat opschoning nooit pagina's kan verwijderen die actief worden weergegeven

Elke schijfbewerking in de cache — vingerafdrukken maken, mappen aanmaken, markeringen bijwerken, pagina's lezen en schrijven, opschonen — onderdrukt de eigen uitzonderingen. Dat is een bewuste keuze, geen slordigheid: de cache is een best-effort versneller, en een volle schijf, een alleen-lezen map of een blokkering door een virusscanner mag de viewer alleen degraderen naar "rendert zoals voorheen", nooit naar "mislukt om de pagina weer te geven". Het slechtste resultaat dat een cache-I/O-fout kan veroorzaken, is een gemiste cache-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;

Hoe maakt u een PDF-viewer DPI-bewust in Delphi?

TPDFlibViewer leidt elke DPI-afhankelijke berekening door één enkel veld, en dat ontwerp met één wijzigingspunt is wat per-monitor ondersteuning haalbaar maakt. De lay-out-, prefetch- en renderingscode lezen allemaal dezelfde waarde FScreenDPI; bij 100% zoom komt één PDF-punt overeen met ScreenDPI / 72 pixels. De publieke setter van de eigenschap ScreenDPI maakt de bitmapcache leeg, bouwt de lay-out opnieuw op, werkt de schuifbereiken bij en maakt het venster ongeldig, zodat het wijzigen van de DPI slechts één toewijzing vereist en alle afhankelijkheden automatisch volgen. Dezelfde pixelberekening is van belang wanneer u rendert voor een printer in plaats van een monitor, een onderwerp dat wordt behandeld in afdrukvoorbeeld en renderings in apparaatcontext

Detectie vindt plaats in een CreateWnd-override in plaats van in de constructor, omdat GetDpiForWindow een geldige venster-handle nodig heeft en de constructor lang voordat deze bestaat wordt uitgevoerd. Totdat CreateWnd wordt aangeroepen, behoudt de control de oude standaardwaarde van 96, zodat de lay-outberekeningen correct blijven. GetDpiForWindow zelf is een Windows 10 1607+ API, dus de control koppelt deze dynamisch met GetProcAddress en valt terug op GetDeviceCaps(LOGPIXELSY) op oudere systemen — de unit laadt nog steeds overal, en oudere Windows-versies rapporteren simpelweg de systeem-DPI

Wat gebeurt er als het venster naar een 4K-monitor verhuist?

Windows verzendt WM_DPICHANGED wanneer een venster met DPI-bewustzijn per monitor naar een monitor met een andere schaalfactor wordt verplaatst, en TPDFlibViewer verwerkt dit rechtstreeks. De handler leest de nieuwe DPI uit het lage woord van wParam, wijst deze toe via SetScreenDPI — wat de nu onjuiste bitmapcache verwijdert en de pagina's opnieuw rangschikt — en accepteert vervolgens de voorgestelde vensterrechthoek die Windows doorgeeft in lParam via SetWindowPos, waardoor de logische grootte van de client stabiel blijft tijdens de verplaatsing. De gebruiker ziet het document scherp opnieuw gerenderd op de nieuwe schaal, in plaats van een door het besturingssysteem uitgerekte bitmap

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

Grenzen die u moet kennen voordat u software levert

Drie praktische limieten verdienen een plaats in uw ontwerpevaluatie. Ten eerste wordt WM_DPICHANGED alleen ontvangen als het proces hiervoor kiest: uw applicatiemanifest moet DPI-bewustzijn per monitor declareren (per-monitor v2 op de huidige Windows-versies), wat in de Delphi IDE de DPI-bewustzijnsinstelling is onder de applicatie-opties. Een proces dat alleen systeem-DPI-bewust is, ontvangt het bericht nooit en de control rendert dan op de DPI die is gedetecteerd bij het maken van het venster. Ten tweede ruilt de schijfcache schijfruimte in voor snelheid — tien documenten aan paginabitmaps op verschillende zoomniveaus is aanzienlijke opslag, dus verwijs DiskCacheFolder naar een locatie waarvan u het prima vindt dat deze groeit, niet naar een roaming-profiel. Ten derde heeft de cachemap schrijfrechten nodig voor de actieve gebruiker; zonder deze rechten werkt de viewer nog steeds, in stilte, met nul hits — controleer DiskCacheHits tijdens het testen als u vermoedt dat de cache niet actief is

Samen veranderen deze twee functies de waargenomen kwaliteit van een document-intensieve applicatie meer dan de meeste renderingsoptimalisaties: documenten die uw gebruikers opnieuw bezoeken openen direct, en de weergave blijft scherp op welke monitor het document ook terechtkomt. Beide worden geleverd als gewone eigenschappen op TPDFlibViewer in de losLab PDF Library, dus de implementatie ervan is een kwestie van een middagje koppelen in plaats van een renderingsengine-project