Zwei Beschwerden begleiten fast jeden eingebetteten PDF-Viewer: Große Dokumente werden bei jedem Start komplett neu gerendert, und der Text wird unscharf, sobald das Fenster auf einen High-DPI-Monitor gezogen wird. Die losLab PDF Library löst beide Probleme in ihrem Steuerelement TPDFlibViewer: Die Eigenschaft DiskCacheFolder ermöglicht einen sitzungsübergreifenden Seiten-Festplatten-Cache, der über einen Dokumenten-Fingerabdruck referenziert wird, und die Eigenschaft ScreenDPI, unterstützt durch GetDpiForWindow und einen Handler für WM_DPICHANGED, verleiht dem Steuerelement echte Per-Monitor-DPI-Erkennung
Beide Probleme sollten ernst genommen werden, da Benutzer sie sofort bemerken. Ein 400-seitiger gescannter Vertrag, dessen Laden gestern acht Sekunden dauerte, benötigt 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 gerendert wird, dort gut aus, wirkt aber auf dem daneben stehenden 4K-Bildschirm sichtlich unscharf. Dieser Artikel beschreibt, wie TPDFlibViewer beide Probleme löst und vor allem, warum das Design so gewählt wurde. Wenn das Steuerelement neu für Sie ist, beginnen Sie mit der Übersicht über das interaktive PDF-Viewer-Steuerelement und kehren Sie dann für die Leistungsschicht hierher zurück
Wie speichert man gerenderte PDF-Seiten sitzungsübergreifend in Delphi?
Weisen Sie TPDFlibViewer.DiskCacheFolder einen Ordner zu, und das Steuerelement speichert gerenderte Seiten-Bitmaps auf der Festplatte. So öffnet sich ein kürzlich angesehenes Dokument sofort wieder, anstatt neu gerendert zu werden. Auf der Festplatte sieht das Layout so aus: <Folder>\PDFlibPas-PageCache\<document fingerprint>\p<page>_z<zoomkey>.bin: ein Unterordner pro Dokument, eine Datei pro Seite-und-Zoom-Kombination. Alles Weitere geschieht automatisch; es gibt keine Cache-API, die aufgerufen werden muss, und nichts, was geleert werden muss
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;
Der Zähler DiskCacheHits gibt an, wie viele Seitenvorgänge der Festplatten-Cache in der aktuellen Sitzung eingespart hat, wodurch der Gewinn messbar statt nur anekdotisch wird. Bei einem Dokument, das Sie geschlossen und wieder geöffnet haben, erhöht jede Seite, die ohne Rendervorgang in den Sichtbereich scrollt, diesen Wert
Warum der Cache-Schlüssel ein Fingerabdruck und keine Versionsnummer ist
Warum rotation is deliberately left out of the disk key
Der Dokument-Fingerabdruck ist ein 64-Bit-FNV-1a-Hash über die Zeichenfolge path|size|last write time, formatiert als sechzehn Hex-Ziffern. Dieses Tripel bildet die gesamte Invalidierungsstrategie: Wenn die Datei bearbeitet wird, ändert sich ihre Größe oder ihr Schreibzeitpunkt, der Hash ändert sich, und der Viewer öffnet einfach einen anderen Cache-Unterordner. Der veraltete Ordner wird nie wieder abgefragt und fällt schließlich der LRU-Bereinigung zum Opfer. Es gibt kein Versionstag-Protokoll, das gepflegt werden muss, keine Zeitstempel-Vergleichslogik, die abweichen kann, und keine Möglichkeit, eine alte Bitmap für ein modifiziertes Dokument bereitzustellen
Dies ist derselbe Ansatz „Inhaltsidentität als Cache-Schlüssel“, den auch Build-Systeme verwenden, und er bewährt sich durch die Fehlerquellen, die er eliminiert. Ein Cache, der Dateien unter dem einfachen Namen des Dokuments speichert, muss Änderungen explizit erkennen, und jede explizite Prüfung birgt das Risiko von Grenzfällen: eine Datei, die mit einem alten Zeitstempel aus einem Backup wiederhergestellt wurde, ein Speichervorgang, der die Dateigröße beibehält, oder ein Pfad, der auf einem Computer mit Groß-/Kleinschreibung und auf einem anderen ohne verglichen wird. Das Hashen aller drei Signale in den Ordnernamen macht Veraltungen strukturell unmöglich, anstatt sie prozedural prüfen zu müssen
Warum die Rotation bewusst aus dem Festplattenschlüssel herausgelassen wird
Der Cache-Dateiname kodiert die Seitennummer und den Zoom und sonst nichts – der Drehwinkel der Ansicht fehlt absichtlich. Der Festplatten-Cache speichert immer die unrotierten Render-Bytes; wenn eine zwischengespeicherte Seite geladen wird, während die Ansicht rotiert ist, werden die Pixel nach dem Dekodieren im Speicher neu ausgerichtet. Wäre die Rotation Teil des Schlüssels auf der Festplatte, würde ein Benutzer, der die vier Ansichtswinkel durchläuft, dieselbe Seite viermal auf die Festplatte schreiben, was das Cache-Wachstum bei null Zusatzinformationen vervierfachen würde, da die Rotation im Vergleich zu einem vollständigen Rendervorgang ein günstiges Pixel-Shuffling ist
Der Bitmap-Cache im Speicher verwendet einen zusammengesetzten Schlüssel, Round(Zoom * 1000) * 4 + Rotation div 90, da dort die rotierte Bitmap genau das ist, was der Paint-Handler benötigt. Der Hintergrund-Prefetch-Thread folgt derselben Disziplin: Er rendert immer unrotierte Seiten und meldet einfache Zoom-Schlüssel, und the main thread wendet die Rotation an, wenn er Ergebnisse abruft – dieselbe Regel „Hauptthread besitzt den Cache“, die in parallelem Seiten-Rendering und Thread-Sicherheit besprochen wird. Die Rotation aus allem unterhalb der Speicherschicht herauszuhalten bedeutet, dass es nur ein einziges kanonisches Rendering pro Seite und Zoom gibt, überall dort, wo die Speicherung teuer ist
Wie hält die LRU-Bereinigung den Cache-Ordner begrenzt?
Der Cache behält bis zu zehn Dokumentenordner und bereinigt die am längsten nicht verwendeten, wenn neue Dokumente eintreffen. Die Aktualität wird durch eine Datei last-used.marker in jedem Dokumentenordner verfolgt, deren Zeitstempel sowohl bei Cache-Schreib- als auch bei Cache-Lesevorgängen aktualisiert wird. So bleibt ein Dokument, das Sie lediglich erneut lesen, aktuell, ohne neu geschrieben zu werden. Der Ordner, der zum aktuell geöffneten Dokument gehört, ist von der Löschung ausgenommen, sodass die Bereinigung niemals Seiten unter der aktiven Ansicht wegziehen kann
Jeder Festplattenvorgang im Cache – Fingerabdruckerstellung, Ordnererstellung, Aktualisierung von Markern, Lesen und Schreiben von Seiten, Bereinigung – fängt seine eigenen Ausnahmen ab. Das ist eine bewusste Entscheidung und keine Nachlässigkeit: Der Cache ist ein Best-Effort-Beschleuniger, und eine volle Festplatte, ein schreibgeschützter Ordner oder eine Virenscanner-Sperre darf den Viewer nur auf „rendert wie zuvor“ zurückstufen, niemals zu „Seite kann nicht angezeigt werden“ führen. Das schlimmste Ergebnis, das ein Cache-E/A-Fehler erzeugen kann, ist ein Cache-Miss
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;
Wie macht man einen PDF-Viewer in Delphi DPI-fähig?
TPDFlibViewer leitet jede DPI-abhängige Berechnung über ein einziges Feld, und dieses Single-Change-Point-Design ist das, was die Unterstützung pro Monitor handhabbar macht. Layout-, Prefetch- und Render-Code lesen alle denselben Wert FScreenDPI; bei 100 % Zoom entspricht ein PDF-Punkt ScreenDPI / 72 Pixeln. Der öffentliche Eigenschafts-Setter ScreenDPI leert den Bitmap-Cache, baut das Layout neu auf, aktualisiert die Scrollbereiche und invalidiert das Fenster. Daher ist das Ändern der DPI eine einzige Zuweisung, der jeder Verbraucher automatisch folgt. Dieselbe Pixelmathematik ist wichtig, wenn Sie für einen Drucker statt für einen Monitor rendern, ein Thema, das in Druckvorschau und Rendern im Gerätekontext behandelt wird
Die Erkennung erfolgt in einem Override von CreateWnd und nicht im Konstruktor, da GetDpiForWindow ein gültiges Fensterhandle benötigt und der Konstruktor lange vor dessen Vorhandensein ausgeführt wird. Bis CreateWnd ausgelöst wird, behält das Steuerelement den alten Standardwert von 96 bei, damit die Layout-Mathematik stabil bleibt. GetDpiForWindow selbst ist eine Windows 10 1607+ API, daher bindet das Steuerelement sie dynamisch mit GetProcAddress und fällt auf älteren Systemen auf GetDeviceCaps(LOGPIXELSY) zurück – die Unit wird weiterhin überall geladen, und älteres Windows meldet einfach die System-DPI
Was passiert, wenn sich das Fenster auf einen 4K-Monitor bewegt?
Windows sendet WM_DPICHANGED, wenn ein für Per-Monitor-DPI ausgelegtes Fenster auf einen Monitor mit einem anderen Skalierungsfaktor wechselt, und TPDFlibViewer verarbeitet dies direkt. Der Handler liest die neue DPI aus dem niederwertigen Wort von wParam, weist sie über SetScreenDPI zu – was den nun falschen Bitmap-Cache verwirft und die Seiten neu anordnet – und akzeptiert dann das vorgeschlagene Fensterrechteck, das Windows in lParam übergibt, über SetWindowPos. Dies hält die logische Client-Größe über den Wechsel hinweg stabil. Der Benutzer sieht, wie das Dokument im neuen Maßstab scharf neu gerendert wird, anstatt einer vom Betriebssystem gestreckten 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 man vor der Auslieferung kennen sollte
Drei praktische Grenzen verdienen einen Platz in Ihrer Design-Review. Erstens wird WM_DPICHANGED nur gesendet, wenn der Prozess sich dafür entscheidet: Ihr Anwendungsmanifest muss die Per-Monitor-DPI-Erkennung deklarieren (Per-Monitor v2 auf aktuellem Windows), was in der Delphi-IDE die Einstellung für die DPI-Erkennung unter den Anwendungsoptionen ist. Ein system-DPI-fähiger Prozess erhält die Nachricht nie, und das Steuerelement rendert dann mit den DPI, die es bei der Fenstererstellung erkannt hat. Zweitens tauscht der Festplatten-Cache Speicherplatz gegen Geschwindigkeit aus – Seiten-Bitmaps für zehn Dokumente über mehrere Zoomstufen hinweg erfordern echten Speicherplatz, weisen Sie DiskCacheFolder also auf einen Ort hin, an dem das Wachstum kein Problem darstellt, nicht auf ein Roaming-Profil. Drittens benötigt der Cache-Ordner Schreibrechte für den ausführenden Benutzer; andernfalls funktioniert der Viewer zwar weiterhin geräuschlos, aber mit null Treffern – überprüfen Sie DiskCacheHits beim Testen, wenn Sie vermuten, dass der Cache nicht greift
Zusammen verändern die beiden Funktionen die wahrgenommene Qualität einer dokumentenintensiven Anwendung stärker als die meisten Rendering-Optimierungen: Dokumente, die Ihre Benutzer erneut aufrufen, öffnen sich sofort, und die Ansicht bleibt auf jedem Monitor scharf, auf dem sie landet. Beide werden als normale Eigenschaften von TPDFlibViewer in der losLab PDF Library ausgeliefert, sodass ihre Implementierung eher ein Nachmittag voller Verdrahtung als ein Rendering-Engine-Projekt ist