Zwei Beschwerden begleiten jeden eingebetteten PDF-Viewer: Große Dokumente werden bei jedem Start von Grund auf neu gerendert, und der Text wird unscharf, sobald das Fenster auf einen Monitor mit hoher DPI-Zahl gezogen wird. losLab PDF Library beantwortet beides in seinem Steuerelement TPDFlibViewer: Die Eigenschaft DiskCacheFolder schaltet einen sitzungsübergreifenden Seiten-Cache auf der Platte frei, der über den Dokument-Fingerprint adressiert wird, und die Eigenschaft ScreenDPI, gestützt auf GetDpiForWindow und einen Handler für WM_DPICHANGED, gibt dem Steuerelement echte Per-Monitor-DPI-Fähigkeit
Beide Probleme verdienen Ernst, weil Anwender sie sofort bemerken. Ein gescannter Vertrag mit 400 Seiten, der gestern acht Sekunden brauchte, bis man scrollen konnte, braucht heute wieder acht Sekunden, obwohl sich an der Datei nichts geändert hat. Und auf einem Desktop mit gemischten DPI-Werten sieht ein Viewer, der mit den 96 DPI des Hauptmonitors rendert, dort gut aus und auf dem 4K-Panel daneben sichtbar weich. Dieser Artikel zeigt, wie TPDFlibViewer beides löst und, nützlicher noch, warum der Entwurf dort gelandet ist, wo er ist. Wenn das Steuerelement selbst neu für Sie ist, beginnen Sie mit dem Überblick über das interaktive PDF-Viewer-Steuerelement und kommen Sie für die Performance-Schicht hierher zurück
Wie speichert man gerenderte PDF-Seiten in Delphi sitzungsübergreifend zwischen?
Weisen Sie TPDFlibViewer.DiskCacheFolder einen Ordner zu, und das Steuerelement legt gerenderte Seiten-Bitmaps auf der Platte ab, sodass ein kürzlich betrachtetes Dokument sofort wieder öffnet, statt neu gerendert zu werden. Auf der Platte sieht die Struktur so aus: <Folder>\PDFlibPas-PageCache\<document fingerprint>\p<page>_z<zoomkey>.bin, also ein Unterordner je Dokument und eine Datei je Kombination aus Seite und Zoom. Alles Weitere läuft automatisch; es gibt keine Cache-API aufzurufen und nichts zu leeren
uses
PDFlibViewer;
procedure TMainForm.FormCreate(Sender: TObject);
begin
FViewer := TPDFlibViewer.Create(Self);
FViewer.Parent := Self;
FViewer.Align := alClient;
// Eine Zeile schaltet den sitzungsübergreifenden Seiten-Cache ein
FViewer.DiskCacheFolder := 'C:\ProgramData\MyApp\PageCache';
FViewer.LoadFromFile('C:\Contracts\master-agreement.pdf', '');
end;
Der Zähler DiskCacheHits meldet, wie viele Seitenrenderings der Disk-Cache in der laufenden Sitzung erspart hat, und macht den Gewinn messbar statt anekdotisch. Bei einem Dokument, das Sie geschlossen und wieder geöffnet haben, erhöht ihn jede Seite, die ohne Renderdurchlauf ins Bild scrollt
Warum der Cache-Schlüssel ein Fingerprint ist und keine Versionsnummer
Der Dokument-Fingerprint ist ein 64-Bit-FNV-1a-Hash über die Zeichenkette path|size|last write time, formatiert als sechzehn Hexziffern. Dieses Tripel ist die gesamte Invalidierungsstrategie: Wird die Datei bearbeitet, ändern sich Größe oder Schreibzeit, der Hash ändert sich, und der Viewer öffnet schlicht einen anderen Cache-Unterordner. Der veraltete Ordner wird nie wieder befragt und fällt irgendwann der LRU-Bereinigung zum Opfer. Es gibt kein Protokoll mit Versionsmarken zu pflegen, keine Zeitstempelvergleiche, die auseinanderlaufen können, und keine Möglichkeit, eine alte Bitmap zu einem geänderten Dokument auszuliefern
Das ist dieselbe Idee von „Inhaltsidentität als Cache-Schlüssel“, die Buildsysteme verwenden, und sie verdient sich ihren Platz durch die Fehlerfälle, die sie beseitigt. Ein Cache, der Dateien unter dem schlichten Dokumentnamen ablegt, muss Änderungen ausdrücklich erkennen, und jede ausdrückliche Prüfung ist ein Fehler, der auf seinen Sonderfall wartet: eine aus dem Backup zurückgeholte Datei mit altem Zeitstempel, ein Speichern, das die Größe erhält, ein Pfad, der auf einer Maschine mit Groß- und Kleinschreibung verglichen wird und auf einer anderen nicht. Alle drei Signale in den Ordnernamen zu hashen, macht Veraltung strukturell unmöglich statt prozedural geprüft
Warum die Drehung bewusst nicht im Plattenschlüssel steckt
Der Name der Cache-Datei kodiert Seitennummer und Zoom und sonst nichts — der Drehwinkel der Ansicht fehlt mit Absicht. Der Disk-Cache speichert immer die ungedrehten Renderbytes; wird eine gecachte Seite bei gedrehter Ansicht geladen, werden die Pixel nach dem Dekodieren im Speicher neu ausgerichtet. Wäre die Drehung Teil des Schlüssels auf der Platte, würde ein Anwender, der die vier Ansichtswinkel durchklickt, dieselbe Seite viermal auf die Platte schreiben und das Cache-Wachstum ohne jede zusätzliche Information vervierfachen, denn eine Drehung ist im Vergleich zu einem vollen Renderdurchlauf ein billiges Umsortieren von Pixeln
Der Bitmap-Cache im Speicher verwendet dagegen einen zusammengesetzten Schlüssel, Round(Zoom * 1000) * 4 + Rotation div 90, denn dort ist die gedrehte Bitmap genau das, was der Paint-Handler braucht. Der Prefetch-Thread im Hintergrund hält dieselbe Disziplin: Er rendert stets ungedrehte Seiten und meldet schlichte Zoomschlüssel, und der Haupt-Thread wendet die Drehung beim Abholen der Ergebnisse an — dieselbe Regel, dass der Haupt-Thread den Cache besitzt, die in paralleles Seitenrendering und Thread-Sicherheit besprochen wird. Die Drehung aus allem unterhalb der Speicherschicht herauszuhalten, bedeutet ein kanonisches Rendering je Seite und Zoom, überall dort, wo Speichern teuer ist
Wie hält die LRU-Bereinigung den Cache-Ordner in Grenzen?
Der Cache behält bis zu zehn Dokumentordner und bereinigt die am längsten nicht genutzten, wenn neue Dokumente hinzukommen. Die Aktualität wird über eine Datei last-used.marker in jedem Dokumentordner verfolgt, deren Zeitstempel sowohl beim Schreiben als auch beim Lesen aufgefrischt wird, sodass ein Dokument, das Sie nur erneut lesen, warm bleibt, ohne neu geschrieben zu werden. Der Ordner des gerade geöffneten Dokuments ist vom Löschen ausgenommen, sodass die Bereinigung der aktiven Ansicht niemals die Seiten unter den Füßen wegziehen kann
Jede Plattenoperation des Caches — Fingerprint bilden, Ordner anlegen, Marker auffrischen, Seiten lesen und schreiben, bereinigen — schluckt ihre eigenen Ausnahmen. Das ist eine bewusste Haltung, keine Schlamperei: Der Cache ist ein Beschleuniger nach bestem Bemühen, und eine volle Platte, ein schreibgeschützter Ordner oder eine Sperre durch den Virenscanner müssen den Viewer auf „rendert wie zuvor“ zurückstufen, niemals auf „zeigt die Seite nicht an“. Das schlimmste Ergebnis eines E/A-Fehlers im Cache ist ein verpasster Treffer
procedure TMainForm.FormClose(Sender: TObject; var Action: TCloseAction);
begin
// Wie viele Renderdurchläufe hat der Disk-Cache in dieser Sitzung gespart?
Log(Format('Disk cache hits: %d', [FViewer.DiskCacheHits]));
end;
Wie macht man einen PDF-Viewer in Delphi DPI-fähig?
TPDFlibViewer führt jede DPI-abhängige Berechnung über ein einziges Feld, und dieser eine Änderungspunkt macht die Per-Monitor-Unterstützung überhaupt handhabbar. Layout, Prefetch und Rendercode lesen alle denselben Wert FScreenDPI; bei 100 Prozent Zoom bildet ein PDF-Punkt auf ScreenDPI / 72 Pixel ab. Der öffentliche Setter der Eigenschaft ScreenDPI leert den Bitmap-Cache, baut das Layout neu auf, aktualisiert die Scrollbereiche und invalidiert das Fenster, sodass eine DPI-Änderung eine einzige Zuweisung ist, der jeder Verbraucher automatisch folgt. Dieselbe Pixelrechnung zählt, wenn Sie für einen Drucker statt für einen Monitor rendern, ein Thema, das Druckvorschau und Rendering auf Device Contexts behandelt
Die Erkennung passiert in einer Überschreibung von CreateWnd und nicht im Konstruktor, denn GetDpiForWindow braucht ein gültiges Fensterhandle, und der Konstruktor läuft lange bevor eines existiert. Bis CreateWnd feuert, hält das Steuerelement den alten Standardwert 96, damit die Layoutrechnung vernünftig bleibt. GetDpiForWindow selbst ist eine API ab Windows 10 1607, daher bindet das Steuerelement sie dynamisch über GetProcAddress und fällt auf älteren Systemen auf GetDeviceCaps(LOGPIXELSY) zurück — die Unit lädt weiterhin überall, und älteres Windows meldet schlicht die System-DPI
Was passiert, wenn das Fenster auf einen 4K-Monitor wandert?
Windows sendet WM_DPICHANGED, wenn ein per Monitor DPI-fähiges Fenster auf einen Monitor mit anderem Skalierungsfaktor wechselt, und TPDFlibViewer behandelt die Nachricht direkt. Der Handler liest die neue DPI aus dem unteren Wort von wParam, weist sie über SetScreenDPI zu — was den nun falschen Bitmap-Cache verwirft und die Seiten neu anordnet — und übernimmt dann per SetWindowPos das vorgeschlagene Fensterrechteck, das Windows in lParam übergibt, was die logische Clientgröße über den Wechsel hinweg stabil hält. Der Anwender sieht, wie das Dokument im neuen Maßstab gestochen neu rendert, statt einer vom Betriebssystem gestreckten Bitmap
// Normalerweise fassen Sie ScreenDPI nie an: CreateWnd erkennt den Monitor
// und WM_DPICHANGED verfolgt Wechsel. Nur für Sonderziele überschreiben,
// etwa um das Layout wie für eine Anzeige mit 150 Prozent zu rendern:
FViewer.ScreenDPI := 144; // leert den Bitmap-Cache und baut das Layout neu
Grenzen, die Sie vor der Auslieferung kennen sollten
Drei praktische Grenzen gehören in Ihre Entwurfsprüfung. Erstens trifft WM_DPICHANGED nur ein, wenn der Prozess sich anmeldet: Ihr Anwendungsmanifest muss Per-Monitor-DPI-Fähigkeit erklären (auf aktuellem Windows Per-Monitor v2), was in der Delphi-IDE die Einstellung zur DPI-Fähigkeit unter den Anwendungsoptionen ist. Ein nur system-DPI-fähiger Prozess erhält die Nachricht nie, und das Steuerelement rendert dann mit der DPI, die es bei der Fenstererzeugung erkannt hat. Zweitens tauscht der Disk-Cache Plattenplatz gegen Geschwindigkeit — die Seiten-Bitmaps von zehn Dokumenten über mehrere Zoomstufen sind echter Speicherbedarf, also richten Sie DiskCacheFolder auf einen Ort, dessen Wachstum Sie in Kauf nehmen, nicht auf ein servergespeichertes Profil. Drittens braucht der Cache-Ordner Schreibrechte für den laufenden Benutzer; ohne sie arbeitet der Viewer weiter, stillschweigend, mit null Treffern — prüfen Sie im Test DiskCacheHits, wenn Sie vermuten, dass der Cache nicht greift
Zusammen verändern die beiden Funktionen die wahrgenommene Qualität einer dokumentlastigen Anwendung stärker als die meisten Rendering-Optimierungen: Dokumente, zu denen Ihre Anwender zurückkehren, öffnen sofort, und die Ansicht bleibt scharf, auf welchem Monitor sie auch landet. Beide kommen als gewöhnliche Eigenschaften von TPDFlibViewer in losLab PDF Library, sodass ihre Übernahme ein Nachmittag Verkabelung ist und kein Projekt an der Rendering-Engine