Fix #29: Fit-to-view via statischem A4-Fallback statt asynchronem Seitenverhältnis

Ersetzt fetchAspectRatioAsync/fitToView durch updateZoom() mit A4-Dimensionen
(595x842 Punkte) als Fallback. Scrollbalken werden per CSS und ScrollPane-Policy
ausgeblendet. Zoom-Listener auf pdfView statt viewStack.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-04-24 15:51:26 +02:00
parent 71d79ab30c
commit 673023d921
@@ -29,10 +29,10 @@ import javafx.scene.layout.VBox;
*
* <p>Die Komponente zeigt eine einzelne Seite der PDF-Datei vollständig eingepasst
* (fit-to-view) an. Das Laden erfolgt auf einem Hintergrund-Worker-Thread; UI-Updates
* laufen ausschließlich über den JavaFX Application Thread. Nach dem Laden wird das
* Seitenverhältnis asynchron ermittelt und der Zoom so gesetzt, dass die Seite ohne
* Scrollbalken vollständig sichtbar ist. Bei Größenänderungen der Anzeigefläche wird
* der Zoom automatisch neu berechnet.
* laufen ausschließlich über den JavaFX Application Thread. Der Zoom wird anhand der
* aktuellen Anzeigefläche und eines A4-Seiten-Fallbacks berechnet, sodass die Seite
* ohne Scrollbalken vollständig sichtbar ist. Bei Größenänderungen der Anzeigefläche
* wird der Zoom automatisch neu berechnet.
*
* <p>Es gilt das Prinzip „Latest Preview Request Wins": Veraltete Lade-Ergebnisse
* werden verworfen, sobald eine neue Anforderung eingeht.
@@ -95,12 +95,6 @@ public final class PdfPreviewPane {
/** Gibt an ob die Navigation bedienbar ist. */
private boolean enabled = true;
/**
* Zuletzt ermitteltes Seitenverhältnis (Höhe/Breite) der gerenderten Seite.
* Wird asynchron nach dem Laden gesetzt; -1.0 wenn noch nicht bekannt.
*/
private double lastKnownAspectRatio = -1.0;
/**
* Erstellt die Komponente im deaktivierten Platzhalter-Zustand.
*/
@@ -112,7 +106,12 @@ public final class PdfPreviewPane {
pdfView.setShowToolBar(false);
pdfView.setId("pdf-preview-view");
// Nach der Skin-Installation Scrollbalken im internen ScrollPane ausblenden.
// Scrollbalken per CSS ausblenden gilt für den internen ScrollPane der Skin
pdfView.setStyle(
".scroll-bar:vertical { -fx-pref-width: 0; visibility: hidden; }" +
".scroll-bar:horizontal { -fx-pref-height: 0; visibility: hidden; }");
// Nach der Skin-Installation Scrollbalken im internen ScrollPane auch per Policy deaktivieren
pdfView.skinProperty().addListener((obs, oldSkin, newSkin) -> {
if (newSkin != null) {
Platform.runLater(() -> {
@@ -143,9 +142,9 @@ public final class PdfPreviewPane {
StackPane.setAlignment(progressIndicator, Pos.CENTER);
VBox.setVgrow(viewStack, Priority.ALWAYS);
// Bei Größenänderungen der Anzeigefläche Zoom neu berechnen
viewStack.widthProperty().addListener((obs, ov, nv) -> fitToView());
viewStack.heightProperty().addListener((obs, ov, nv) -> fitToView());
// Bei Größenänderungen der PDF-Ansicht Zoom neu berechnen
pdfView.widthProperty().addListener((obs, ov, nv) -> updateZoom());
pdfView.heightProperty().addListener((obs, ov, nv) -> updateZoom());
prevButton.setId("pdf-preview-prev-button");
prevButton.setOnAction(e -> navigateToPreviousPage());
@@ -204,7 +203,6 @@ public final class PdfPreviewPane {
currentSourceFile = null;
currentPage = 0;
totalPages = -1;
lastKnownAspectRatio = -1.0;
// Neue Sequenznummer: laufende Requests werden verworfen
currentRequestSequence.incrementAndGet();
pdfView.unload();
@@ -270,7 +268,8 @@ public final class PdfPreviewPane {
currentPage = targetPage;
updatePageLabel();
updateNavigationButtons();
fitToView();
// Zoom nach dem Rendering der neuen Seite neu berechnen
Platform.runLater(this::updateZoom);
}
private void navigateToNextPage() {
@@ -282,72 +281,29 @@ public final class PdfPreviewPane {
currentPage = targetPage;
updatePageLabel();
updateNavigationButtons();
fitToView();
// Zoom nach dem Rendering der neuen Seite neu berechnen
Platform.runLater(this::updateZoom);
}
// --- Zoom-Berechnung (fit-to-view) ----------------------------------------
/**
* Berechnet den optimalen Zoomfaktor anhand des bekannten Seitenverhältnisses und
* der aktuellen Größe der Anzeigefläche und setzt ihn auf {@code pdfView}.
* Hat keinen Effekt wenn das Seitenverhältnis noch nicht bekannt ist oder die
* Anzeigefläche noch keine Größe hat.
* Berechnet den optimalen Zoomfaktor anhand der aktuellen Größe von {@code pdfView}
* und setzt ihn. Als Seitenmaß wird A4 im Hochformat (595 × 842 Punkte) als Fallback
* verwendet. Hat keinen Effekt, solange {@code pdfView} noch keine Größe hat.
*/
private void fitToView() {
if (lastKnownAspectRatio <= 0.0) {
private void updateZoom() {
double panelWidth = pdfView.getWidth();
double panelHeight = pdfView.getHeight();
if (panelWidth <= 0 || panelHeight <= 0) {
return;
}
double vw = viewStack.getWidth();
double vh = viewStack.getHeight();
if (vw <= 0.0 || vh <= 0.0) {
return;
}
PDFView.Document doc = pdfView.getDocument();
if (doc == null || currentPage <= 0) {
return;
}
// Querformat: Zoom=1 → Höhe füllt den Viewport; Breite kann überlaufen.
// Hochformat: Zoom=1 → Breite füllt den Viewport; Höhe kann überlaufen.
boolean landscape = doc.isLandscape(currentPage - 1);
double zoom;
if (!landscape) {
zoom = Math.min(1.0, vh / (lastKnownAspectRatio * vw));
} else {
zoom = Math.min(1.0, vw / (lastKnownAspectRatio * vh));
}
pdfView.setZoomFactor(Math.max(0.05, zoom));
}
/**
* Ermittelt das Seitenverhältnis der angegebenen Seite asynchron auf dem
* Worker-Thread und aktualisiert danach den Zoom auf dem FX-Thread.
* Veraltete Ergebnisse werden anhand der Sequenznummer verworfen.
*
* @param seq die Sequenznummer der aktuellen Lade-Anforderung
*/
private void fetchAspectRatioAsync(long seq) {
PDFView.Document doc = pdfView.getDocument();
if (doc == null) {
return;
}
int pageIdx = Math.max(0, currentPage - 1);
executor.submit(() -> {
try {
// Seite bei sehr kleiner Skala rendern, um das Seitenverhältnis zu ermitteln
var img = doc.renderPage(pageIdx, 0.05f);
if (img != null && img.getWidth() > 0) {
double ratio = (double) img.getHeight() / img.getWidth();
Platform.runLater(() -> {
if (currentRequestSequence.get() == seq) {
lastKnownAspectRatio = ratio;
fitToView();
}
});
}
} catch (Exception e) {
LOG.debug("PDF-Vorschau: Seitenverhältnis nicht ermittelbar: {}", e.getMessage());
}
});
double pageWidth = 595; // A4 Breite in Punkten (Fallback)
double pageHeight = 842; // A4 Höhe in Punkten (Fallback)
double zoomByWidth = panelWidth / pageWidth;
double zoomByHeight = panelHeight / pageHeight;
double zoom = Math.min(zoomByWidth, zoomByHeight) * 0.95;
pdfView.setZoomFactor(zoom);
}
// --- Asynchrones Laden ---------------------------------------------------
@@ -405,8 +361,8 @@ public final class PdfPreviewPane {
showContent();
updateNavigationButtons();
updatePageLabel();
// Seitenverhältnis asynchron ermitteln und Zoom anpassen
fetchAspectRatioAsync(seq);
// Zoom nach dem ersten Rendering berechnen
Platform.runLater(this::updateZoom);
LOG.debug("PDF-Vorschau: Rendering angestoßen {} Seite(n)", totalPages);
} catch (Exception e) {
String msg = classifyLoadException(e);