Kalibriere zoomLevel beim Verlassen des Fit-Modus auf visuellen Skalierungsfaktor

Beim ersten Zoom-Schritt sprang die ImageView abrupt von der visuell
sichtbaren Breite (durch fitHeight aspekt-erhaltend verkleinert) auf
naturalViewportWidth × zoomLevel, weil zoomLevel mit dem Wert 1.0 nicht
zur tatsächlich angezeigten Skalierung passte und gleichzeitig setFitHeight(0)
die Höhenrestriktion entfernte.

applyZoom() initialisiert nun beim Verlassen des Fit-Modus zoomLevel
auf currentVisualWidth / naturalImageWidth (= aktueller visueller
Skalierungsfaktor) und setzt naturalViewportWidth auf die natürliche
Bildbreite. Damit entspricht zoomLevel = 1.0 der pixel-genauen
Originaldarstellung. Der vom Caller intendierte Delta-Schritt wird vor
der Kalibrierung gesichert und nach der Kalibrierung auf den neuen
zoomLevel re-appliziert, damit applyZoom(zoomLevel + 0.10) nicht
unverändert auf den kalibrierten Wert feuert.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-05-05 15:16:25 +02:00
parent 3ef8fd0dc3
commit a8d8a4a3c1
@@ -126,8 +126,12 @@ public final class PdfPreviewPane {
private double zoomAccumulator = 0.0;
/**
* Viewport-Breite zum Zeitpunkt des ersten manuellen Zooms; dient als Referenzbreite
* für alle nachfolgenden Zoomstufen. 0.0 bedeutet Fit-to-View-Modus ist aktiv.
* Referenzbreite für die manuelle Zoom-Skalierung; gilt
* {@code imageView.fitWidth = naturalViewportWidth × zoomLevel} im manuellen
* Zoom-Modus. Beim Verlassen des Fit-Modus wird der Wert auf die natürliche
* Bildbreite gesetzt, sodass {@code zoomLevel = 1.0} der pixel-genauen
* Originalgröße entspricht und {@code zoomLevel} damit gleich dem visuellen
* Skalierungsfaktor ist. {@code 0.0} bedeutet Fit-to-View-Modus ist aktiv.
*/
private double naturalViewportWidth = 0.0;
@@ -655,12 +659,25 @@ public final class PdfPreviewPane {
/**
* Setzt den Zoomfaktor und verlässt beim ersten Aufruf den Fit-to-View-Modus.
* Die Referenzbreite wird einmalig aus dem Viewport übernommen.
* <p>
* Beim ersten Aufruf (Wechsel aus dem Fit-Modus) wird {@code zoomLevel} auf
* den aktuellen visuellen Skalierungsfaktor kalibriert: aktuelle visuelle
* Breite der ImageView (mit {@code preserveRatio} bereits aspekt-korrekt
* verkleinert) geteilt durch die natürliche Bildbreite. Damit entspricht
* {@code zoomLevel = 1.0} der pixel-genauen Originalgröße, und der erste
* Zoom-Schritt addiert sich auf den realen Skalierungsfaktor. Ohne diese
* Kalibrierung springt die ImageView abrupt auf {@code Viewport-Breite × 1.10},
* weil im Fit-Modus die {@code fitHeight}-Bindung das Bild aspekt-erhaltend
* deutlich kleiner zwingt als {@code naturalViewportWidth × 1.0} ergibt.
* Da der Caller den Delta-Schritt auf dem alten {@code zoomLevel = 1.0}
* berechnet hat, wird er nach der Kalibrierung auf den neuen, kalibrierten
* {@code zoomLevel} re-appliziert.
* <p>
* Beim Wechsel aus dem Fit-to-View-Modus wird die Ansicht auf die Bildmitte
* zentriert (H/V = 0.5). Bei weiteren Zoom-Schritten bleibt die aktuelle
* Scrollposition erhalten. Ein {@code layout()}-Aufruf vor der Positionswiederherstellung
* stellt sicher, dass die neuen Inhaltsgrenzen bereits berechnet sind.
* Scrollposition erhalten. Ein {@code layout()}-Aufruf vor der
* Positionswiederherstellung stellt sicher, dass die neuen Inhaltsgrenzen
* bereits berechnet sind.
*
* @param newZoom gewünschter Zoomfaktor, wird auf [{@link #ZOOM_MIN}, {@link #ZOOM_MAX}] begrenzt
*/
@@ -669,17 +686,35 @@ public final class PdfPreviewPane {
boolean wasInFitMode = scrollPane.isFitToWidth();
if (wasInFitMode) {
Bounds viewport = scrollPane.getViewportBounds();
double vpWidth = viewport != null ? viewport.getWidth() : viewStack.getWidth();
if (vpWidth <= 0) {
return; // Layout noch nicht abgeschlossen
Image image = imageView.getImage();
if (image == null || image.getWidth() <= 0) {
return; // Kein Bild Zoom-Kalibrierung nicht möglich
}
naturalViewportWidth = vpWidth;
double naturalImageWidth = image.getWidth();
double currentVisualWidth = imageView.getBoundsInLocal().getWidth();
if (currentVisualWidth <= 0) {
Bounds viewport = scrollPane.getViewportBounds();
currentVisualWidth = viewport != null ? viewport.getWidth() : viewStack.getWidth();
if (currentVisualWidth <= 0) {
return; // Layout noch nicht abgeschlossen
}
}
// Vom Caller intendierten Delta-Schritt vor der Kalibrierung sichern
double requestedDelta = newZoom - zoomLevel;
// zoomLevel auf den aktuellen visuellen Skalierungsfaktor kalibrieren
naturalViewportWidth = naturalImageWidth;
zoomLevel = currentVisualWidth / naturalImageWidth;
// effective neu berechnen, weil zoomLevel sich geändert hat
effective = Math.min(ZOOM_MAX, Math.max(ZOOM_MIN, zoomLevel + requestedDelta));
scrollPane.setFitToWidth(false);
scrollPane.setFitToHeight(false);
imageView.fitWidthProperty().unbind();
imageView.fitHeightProperty().unbind();
// 32d: Mauszeiger signalisiert Pan-Modus
// Mauszeiger signalisiert Pan-Modus
viewStack.setCursor(Cursor.OPEN_HAND);
}
@@ -687,7 +722,7 @@ public final class PdfPreviewPane {
return;
}
// 32b: Beim ersten Zoom Mitte beibehalten; danach aktuelle Position bewahren
// Beim ersten Zoom Mitte beibehalten; danach aktuelle Position bewahren
double hval = wasInFitMode ? 0.5 : scrollPane.getHvalue();
double vval = wasInFitMode ? 0.5 : scrollPane.getVvalue();
@@ -695,7 +730,7 @@ public final class PdfPreviewPane {
imageView.setFitWidth(naturalViewportWidth * zoomLevel);
imageView.setFitHeight(0);
// 32b: layout() stellt sicher, dass die neuen Inhaltsgrenzen bekannt sind,
// layout() stellt sicher, dass die neuen Inhaltsgrenzen bekannt sind,
// bevor die Scroll-Werte restauriert werden
Platform.runLater(() -> {
scrollPane.layout();