Fix #29: Eigenes PDF-Rendering mit PDFBox statt PDFViewFX

Ersetzt die PDFView-basierte Vorschau durch direktes Rendering einzelner Seiten
mit PDFBox (Loader.loadPDF + PDFRenderer.renderImageWithDPI bei 120 DPI).
BufferedImage wird über SwingFXUtils.toFXImage in eine JavaFX-Image konvertiert
und in einer ImageView angezeigt. fit-to-view entsteht nativ durch Binding von
fitWidth/fitHeight an den StackPane-Bereich bei preserveRatio=true. Keine
Scrollbalken, keine Zoom-Einschraenkungen, Seitenanfang immer sichtbar.

Lazy Rendering mit In-Memory-Cache fuer bereits gerenderte Seiten; asynchrones
Oeffnen und Rendering auf pdf-preview-worker-Thread; "latest preview request
wins"-Prinzip bleibt erhalten. pdfviewfx-Abhaengigkeit aus adapter-in-gui pom
entfernt, pdfbox stattdessen explizit aufgenommen.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-04-24 16:05:02 +02:00
parent 673023d921
commit 591c7ff94c
2 changed files with 209 additions and 128 deletions
+4 -5
View File
@@ -39,7 +39,7 @@
<artifactId>javafx-controls</artifactId> <artifactId>javafx-controls</artifactId>
<classifier>win</classifier> <classifier>win</classifier>
</dependency> </dependency>
<!-- JavaFX-Swing-Interop für PDFView (AWT-Bridge, Rendering) --> <!-- JavaFX-Swing-Interop: wird für SwingFXUtils.toFXImage (BufferedImage -> FX Image) benötigt -->
<dependency> <dependency>
<groupId>org.openjfx</groupId> <groupId>org.openjfx</groupId>
<artifactId>javafx-swing</artifactId> <artifactId>javafx-swing</artifactId>
@@ -47,11 +47,10 @@
<classifier>win</classifier> <classifier>win</classifier>
</dependency> </dependency>
<!-- PDF-Vorschau: PDFView-Control für die integrierte Dokumentvorschau --> <!-- PDF-Vorschau: PDFBox für direktes Rendering einzelner Seiten in BufferedImages -->
<dependency> <dependency>
<groupId>com.dlsc.pdfviewfx</groupId> <groupId>org.apache.pdfbox</groupId>
<artifactId>pdfviewfx</artifactId> <artifactId>pdfbox</artifactId>
<version>3.3.2</version>
</dependency> </dependency>
<!-- JBIG2-Codec für PDF-Bilddecodierung --> <!-- JBIG2-Codec für PDF-Bilddecodierung -->
<dependency> <dependency>
@@ -1,23 +1,31 @@
package de.gecheckt.pdf.umbenenner.adapter.in.gui.batchrun; package de.gecheckt.pdf.umbenenner.adapter.in.gui.batchrun;
import java.awt.image.BufferedImage;
import java.io.File; import java.io.File;
import java.nio.file.Path; import java.nio.file.Path;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ExecutorService; import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors; import java.util.concurrent.Executors;
import java.util.concurrent.atomic.AtomicLong; import java.util.concurrent.atomic.AtomicLong;
import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger; import org.apache.logging.log4j.Logger;
import org.apache.pdfbox.Loader;
import org.apache.pdfbox.pdmodel.PDDocument;
import org.apache.pdfbox.pdmodel.encryption.InvalidPasswordException;
import org.apache.pdfbox.rendering.ImageType;
import org.apache.pdfbox.rendering.PDFRenderer;
import com.dlsc.pdfviewfx.PDFView;
import javafx.application.Platform; import javafx.application.Platform;
import javafx.embed.swing.SwingFXUtils;
import javafx.geometry.Insets; import javafx.geometry.Insets;
import javafx.geometry.Pos; import javafx.geometry.Pos;
import javafx.scene.Node;
import javafx.scene.control.Button; import javafx.scene.control.Button;
import javafx.scene.control.Label; import javafx.scene.control.Label;
import javafx.scene.control.ProgressIndicator; import javafx.scene.control.ProgressIndicator;
import javafx.scene.control.ScrollPane; import javafx.scene.image.Image;
import javafx.scene.image.ImageView;
import javafx.scene.layout.HBox; import javafx.scene.layout.HBox;
import javafx.scene.layout.Priority; import javafx.scene.layout.Priority;
import javafx.scene.layout.Region; import javafx.scene.layout.Region;
@@ -27,27 +35,34 @@ import javafx.scene.layout.VBox;
/** /**
* Detailbereich-Komponente zur asynchronen Anzeige von Seiten einer Quelldatei. * Detailbereich-Komponente zur asynchronen Anzeige von Seiten einer Quelldatei.
* *
* <p>Die Komponente zeigt eine einzelne Seite der PDF-Datei vollständig eingepasst * <p>Die Komponente rendert PDF-Seiten direkt mit Apache PDFBox und zeigt das Ergebnis
* (fit-to-view) an. Das Laden erfolgt auf einem Hintergrund-Worker-Thread; UI-Updates * in einer {@link ImageView} an. Die Anzeige ist vollständig eingepasst (fit-to-view):
* laufen ausschließlich über den JavaFX Application Thread. Der Zoom wird anhand der * {@code fitWidth} und {@code fitHeight} der {@link ImageView} sind an die Größe des
* aktuellen Anzeigefläche und eines A4-Seiten-Fallbacks berechnet, sodass die Seite * umgebenden {@link StackPane} gebunden, {@code preserveRatio=true} erhält das
* ohne Scrollbalken vollständig sichtbar ist. Bei Größenänderungen der Anzeigefläche * Seitenverhältnis. Es entstehen weder Scrollbalken noch Zoom-Artefakte.
* wird der Zoom automatisch neu berechnet.
* *
* <p>Es gilt das Prinzip „Latest Preview Request Wins": Veraltete Lade-Ergebnisse * <p>Das Laden der PDF-Datei und das Rendering einzelner Seiten erfolgt auf einem
* werden verworfen, sobald eine neue Anforderung eingeht. * dedizierten Worker-Thread. UI-Updates laufen ausschließlich über den JavaFX
* Application Thread. Bereits gerenderte Seiten werden in einem In-Memory-Cache
* ({@code Map<Integer, Image>}) gehalten, sodass wiederholte Navigation kein
* erneutes Rendering erfordert. Der Cache wird beim Wechsel der Quelldatei geleert.
*
* <p>Es gilt das Prinzip „Latest Preview Request Wins": Veraltete Lade- und
* Rendering-Ergebnisse werden anhand einer Sequenznummer erkannt und verworfen,
* sobald eine neue Anforderung eingeht.
* *
* <h2>Fehlerfälle</h2> * <h2>Fehlerfälle</h2>
* <ul> * <ul>
* <li>Quelldatei nicht vorhanden → Meldungstext im Vorschaubereich</li> * <li>Quelldatei nicht vorhanden → Meldungstext im Vorschaubereich</li>
* <li>PDF nicht lesbar → Meldungstext im Vorschaubereich</li> * <li>PDF nicht lesbar → Meldungstext im Vorschaubereich</li>
* <li>PDF passwortgeschützt → Meldungstext im Vorschaubereich</li>
* <li>Keine Selektion → neutraler Platzhaltertext</li> * <li>Keine Selektion → neutraler Platzhaltertext</li>
* </ul> * </ul>
* *
* <h2>Threading</h2> * <h2>Threading</h2>
* <p>Alle öffentlichen Methoden müssen auf dem JavaFX Application Thread aufgerufen * <p>Alle öffentlichen Methoden müssen auf dem JavaFX Application Thread aufgerufen
* werden. Internes Laden und Seitenverhältnis-Ermittlung laufen auf einem dedizierten * werden. Das PDF-Öffnen, die Speicherhaltung des {@link PDDocument} und das
* Worker-Thread. * Rendering einzelner Seiten laufen ausschließlich auf dem Worker-Thread.
*/ */
public final class PdfPreviewPane { public final class PdfPreviewPane {
@@ -59,9 +74,12 @@ public final class PdfPreviewPane {
static final String PDF_PASSWORD_PROTECTED_TEXT = static final String PDF_PASSWORD_PROTECTED_TEXT =
"PDF ist passwortgeschützt und kann nicht angezeigt werden"; "PDF ist passwortgeschützt und kann nicht angezeigt werden";
/** Render-Auflösung in DPI. 120 DPI ist ein guter Kompromiss aus Qualität und Geschwindigkeit. */
private static final float RENDER_DPI = 120f;
private final VBox root = new VBox(4); private final VBox root = new VBox(4);
private final StackPane viewStack = new StackPane(); private final StackPane viewStack = new StackPane();
private final PDFView pdfView = new PDFView(); private final ImageView imageView = new ImageView();
private final Label overlayLabel = new Label(PLACEHOLDER_TEXT); private final Label overlayLabel = new Label(PLACEHOLDER_TEXT);
private final ProgressIndicator progressIndicator = new ProgressIndicator(); private final ProgressIndicator progressIndicator = new ProgressIndicator();
private final Label pageLabel = new Label(); private final Label pageLabel = new Label();
@@ -71,11 +89,18 @@ public final class PdfPreviewPane {
/** /**
* Sequenznummer der aktuell angeforderten Vorschau. Jede neue Anforderung * Sequenznummer der aktuell angeforderten Vorschau. Jede neue Anforderung
* erhöht diesen Zähler. Lade-Ergebnisse mit veralteter Sequenznummer werden verworfen. * (Laden oder Seitenwechsel) erhöht diesen Zähler. Lade-/Rendering-Ergebnisse
* mit veralteter Sequenznummer werden verworfen.
*/ */
private final AtomicLong currentRequestSequence = new AtomicLong(0); private final AtomicLong currentRequestSequence = new AtomicLong(0);
/** Hintergrund-Thread-Pool für Lade-Aufgaben. */ /**
* Cache bereits gerenderter Seiten für die aktuell geladene Quelldatei.
* Schlüssel ist die 1-basierte Seitennummer. Wird beim Wechsel der Quelldatei geleert.
*/
private final Map<Integer, Image> pageCache = new ConcurrentHashMap<>();
/** Hintergrund-Thread-Pool für Lade- und Rendering-Aufgaben. */
private final ExecutorService executor = private final ExecutorService executor =
Executors.newSingleThreadExecutor(r -> { Executors.newSingleThreadExecutor(r -> {
Thread t = new Thread(r, "pdf-preview-worker"); Thread t = new Thread(r, "pdf-preview-worker");
@@ -83,6 +108,18 @@ public final class PdfPreviewPane {
return t; return t;
}); });
/**
* Aktuell geöffnetes PDF-Dokument. Zugriff ausschließlich vom Worker-Thread.
* {@code null} wenn kein Dokument geöffnet ist.
*/
private PDDocument currentDocument = null;
/**
* Renderer für das aktuell geöffnete Dokument. Zugriff ausschließlich vom Worker-Thread.
* {@code null} wenn kein Dokument geöffnet ist.
*/
private PDFRenderer currentRenderer = null;
/** Aktuell geladene Quelldatei; null wenn keine Selektion vorliegt. */ /** Aktuell geladene Quelldatei; null wenn keine Selektion vorliegt. */
private Path currentSourceFile = null; private Path currentSourceFile = null;
@@ -101,28 +138,12 @@ public final class PdfPreviewPane {
public PdfPreviewPane() { public PdfPreviewPane() {
sectionTitle.setStyle("-fx-font-weight: bold;"); sectionTitle.setStyle("-fx-font-weight: bold;");
// PDFView-Konfiguration: Thumbnails und Toolbar ausblenden für kompakten Modus imageView.setId("pdf-preview-image-view");
pdfView.setShowThumbnails(false); imageView.setPreserveRatio(true);
pdfView.setShowToolBar(false); imageView.setSmooth(true);
pdfView.setId("pdf-preview-view"); // Fit-to-view: ImageView füllt den verfügbaren Bereich unter Wahrung des Seitenverhältnisses
imageView.fitWidthProperty().bind(viewStack.widthProperty());
// Scrollbalken per CSS ausblenden gilt für den internen ScrollPane der Skin imageView.fitHeightProperty().bind(viewStack.heightProperty());
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(() -> {
Node found = pdfView.lookup(".scroll-pane");
if (found instanceof ScrollPane sp) {
sp.setHbarPolicy(ScrollPane.ScrollBarPolicy.NEVER);
sp.setVbarPolicy(ScrollPane.ScrollBarPolicy.NEVER);
}
});
}
});
overlayLabel.setId("pdf-preview-overlay-label"); overlayLabel.setId("pdf-preview-overlay-label");
overlayLabel.setStyle("-fx-text-fill: #555555;"); overlayLabel.setStyle("-fx-text-fill: #555555;");
@@ -136,16 +157,13 @@ public final class PdfPreviewPane {
progressIndicator.setMaxWidth(60); progressIndicator.setMaxWidth(60);
progressIndicator.setMaxHeight(60); progressIndicator.setMaxHeight(60);
// Stack: PDFView hinter dem Overlay; Overlay überlagert PDFView bei Fehlern/Laden // Stack: ImageView hinter dem Overlay; Overlay überlagert das Bild bei Fehlern/Laden
viewStack.getChildren().addAll(pdfView, overlayLabel, progressIndicator); viewStack.getChildren().addAll(imageView, overlayLabel, progressIndicator);
StackPane.setAlignment(imageView, Pos.CENTER);
StackPane.setAlignment(overlayLabel, Pos.CENTER); StackPane.setAlignment(overlayLabel, Pos.CENTER);
StackPane.setAlignment(progressIndicator, Pos.CENTER); StackPane.setAlignment(progressIndicator, Pos.CENTER);
VBox.setVgrow(viewStack, Priority.ALWAYS); VBox.setVgrow(viewStack, Priority.ALWAYS);
// 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.setId("pdf-preview-prev-button");
prevButton.setOnAction(e -> navigateToPreviousPage()); prevButton.setOnAction(e -> navigateToPreviousPage());
@@ -178,6 +196,8 @@ public final class PdfPreviewPane {
/** /**
* Lädt die angegebene Quelldatei asynchron und zeigt Seite 1 an. * Lädt die angegebene Quelldatei asynchron und zeigt Seite 1 an.
* Startet eine neue Vorschau-Anforderung und verwirft etwaige laufende Anforderungen. * Startet eine neue Vorschau-Anforderung und verwirft etwaige laufende Anforderungen.
* Der Seiten-Cache wird geleert und ein etwaiges bereits geöffnetes PDF-Dokument
* wird geschlossen.
* <p> * <p>
* Muss auf dem JavaFX Application Thread aufgerufen werden. * Muss auf dem JavaFX Application Thread aufgerufen werden.
* *
@@ -191,11 +211,13 @@ public final class PdfPreviewPane {
currentSourceFile = sourceFile; currentSourceFile = sourceFile;
currentPage = 0; currentPage = 0;
totalPages = -1; totalPages = -1;
pageCache.clear();
requestLoad(sourceFile); requestLoad(sourceFile);
} }
/** /**
* Leert die Komponente und zeigt den neutralen Platzhaltertext. * Leert die Komponente und zeigt den neutralen Platzhaltertext.
* Das aktuell geöffnete PDF-Dokument wird asynchron auf dem Worker-Thread geschlossen.
* <p> * <p>
* Muss auf dem JavaFX Application Thread aufgerufen werden. * Muss auf dem JavaFX Application Thread aufgerufen werden.
*/ */
@@ -203,9 +225,12 @@ public final class PdfPreviewPane {
currentSourceFile = null; currentSourceFile = null;
currentPage = 0; currentPage = 0;
totalPages = -1; totalPages = -1;
pageCache.clear();
// Neue Sequenznummer: laufende Requests werden verworfen // Neue Sequenznummer: laufende Requests werden verworfen
currentRequestSequence.incrementAndGet(); currentRequestSequence.incrementAndGet();
pdfView.unload(); // Dokument auf dem Worker-Thread schließen, da PDDocument ausschließlich dort genutzt wird
executor.submit(this::closeCurrentDocumentOnWorker);
imageView.setImage(null);
showPlaceholder(); showPlaceholder();
updateNavigationButtons(); updateNavigationButtons();
} }
@@ -223,11 +248,16 @@ public final class PdfPreviewPane {
} }
/** /**
* Beendet den internen Executor sauber. Muss beim Schließen der Anwendung * Beendet den internen Executor sauber und schließt das eventuell noch offene
* aufgerufen werden. * PDF-Dokument. Muss beim Schließen der Anwendung aufgerufen werden.
*/ */
public void shutdown() { public void shutdown() {
executor.shutdownNow(); try {
executor.submit(this::closeCurrentDocumentOnWorker);
} catch (RuntimeException ignored) {
// Executor wurde bereits beendet keine Aktion erforderlich
}
executor.shutdown();
} }
// --- Test-Accessoren ------------------------------------------------------ // --- Test-Accessoren ------------------------------------------------------
@@ -263,50 +293,40 @@ public final class PdfPreviewPane {
if (!enabled || currentPage <= 1) { if (!enabled || currentPage <= 1) {
return; return;
} }
int targetPage = currentPage - 1; goToPage(currentPage - 1);
pdfView.setPage(targetPage - 1);
currentPage = targetPage;
updatePageLabel();
updateNavigationButtons();
// Zoom nach dem Rendering der neuen Seite neu berechnen
Platform.runLater(this::updateZoom);
} }
private void navigateToNextPage() { private void navigateToNextPage() {
if (!enabled || totalPages <= 0 || currentPage >= totalPages) { if (!enabled || totalPages <= 0 || currentPage >= totalPages) {
return; return;
} }
int targetPage = currentPage + 1; goToPage(currentPage + 1);
pdfView.setPage(targetPage - 1); }
/**
* Wechselt zur angegebenen Seite. Bereits gerenderte Seiten werden direkt aus dem
* Cache angezeigt; ansonsten wird ein Rendering-Auftrag auf den Worker-Thread gelegt.
*
* @param targetPage Ziel-Seite (1-basiert, muss im gültigen Bereich liegen)
*/
private void goToPage(int targetPage) {
currentPage = targetPage; currentPage = targetPage;
updatePageLabel(); updatePageLabel();
updateNavigationButtons(); updateNavigationButtons();
// Zoom nach dem Rendering der neuen Seite neu berechnen
Platform.runLater(this::updateZoom);
}
// --- Zoom-Berechnung (fit-to-view) ---------------------------------------- Image cached = pageCache.get(targetPage);
if (cached != null) {
/** imageView.setImage(cached);
* Berechnet den optimalen Zoomfaktor anhand der aktuellen Größe von {@code pdfView} showContent();
* 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 updateZoom() {
double panelWidth = pdfView.getWidth();
double panelHeight = pdfView.getHeight();
if (panelWidth <= 0 || panelHeight <= 0) {
return; return;
} }
double pageWidth = 595; // A4 Breite in Punkten (Fallback)
double pageHeight = 842; // A4 Höhe in Punkten (Fallback) long seq = currentRequestSequence.incrementAndGet();
double zoomByWidth = panelWidth / pageWidth; showLoading();
double zoomByHeight = panelHeight / pageHeight; executor.submit(() -> renderPageOnWorker(targetPage, seq));
double zoom = Math.min(zoomByWidth, zoomByHeight) * 0.95;
pdfView.setZoomFactor(zoom);
} }
// --- Asynchrones Laden --------------------------------------------------- // --- Asynchrones Laden und Rendering --------------------------------------
/** /**
* Startet eine asynchrone Lade-Anforderung für die angegebene Datei. * Startet eine asynchrone Lade-Anforderung für die angegebene Datei.
@@ -318,58 +338,128 @@ public final class PdfPreviewPane {
long seq = currentRequestSequence.incrementAndGet(); long seq = currentRequestSequence.incrementAndGet();
LOG.debug("PDF-Vorschau: Lade {} (Anforderung #{})", file, seq); LOG.debug("PDF-Vorschau: Lade {} (Anforderung #{})", file, seq);
// Ladeindikator zeigen (auf FX-Thread, da requestLoad immer auf FX-Thread)
showLoading(); showLoading();
updateNavigationButtons(); updateNavigationButtons();
executor.submit(() -> loadFileOnWorker(file, seq)); executor.submit(() -> loadAndRenderFirstPageOnWorker(file, seq));
} }
/** /**
* Überprüft die Datei auf dem Worker-Thread und übergibt das Ergebnis an den FX-Thread. * Öffnet die PDF-Datei, ermittelt die Seitenzahl und rendert die erste Seite.
* Läuft ausschließlich auf dem Worker-Thread.
* *
* @param file die zu ladende Datei * @param file die zu ladende Datei
* @param seq die Sequenznummer dieser Anforderung * @param seq die Sequenznummer dieser Anforderung
*/ */
private void loadFileOnWorker(Path file, long seq) { private void loadAndRenderFirstPageOnWorker(Path file, long seq) {
File ioFile = file.toFile(); File ioFile = file.toFile();
if (!ioFile.exists()) { if (!ioFile.exists()) {
LOG.warn("PDF-Vorschau: Rendering fehlgeschlagen Datei nicht gefunden: {}", file); LOG.warn("PDF-Vorschau: Rendering fehlgeschlagen Datei nicht gefunden: {}", file);
Platform.runLater(() -> { publishError(seq, FILE_NOT_FOUND_TEXT);
if (currentRequestSequence.get() == seq) {
showError(FILE_NOT_FOUND_TEXT);
updateNavigationButtons();
}
});
return; return;
} }
// Laden auf FX-Thread: PDFView.load() muss auf dem FX-Thread aufgerufen werden, // Vorheriges Dokument schließen bevor ein neues geöffnet wird
// da es JavaFX-Properties aktualisiert. closeCurrentDocumentOnWorker();
try {
PDDocument doc = Loader.loadPDF(ioFile);
currentDocument = doc;
currentRenderer = new PDFRenderer(doc);
int pages = Math.max(1, doc.getNumberOfPages());
BufferedImage buffered =
currentRenderer.renderImageWithDPI(0, RENDER_DPI, ImageType.RGB);
Image fxImage = SwingFXUtils.toFXImage(buffered, null);
final int totalPagesFinal = pages;
Platform.runLater(() -> { Platform.runLater(() -> {
if (currentRequestSequence.get() != seq) { if (currentRequestSequence.get() != seq) {
return; // Veraltet verwerfen return; // Veraltet verwerfen
} }
try { totalPages = totalPagesFinal;
pdfView.load(ioFile);
// Seitenzahl nach dem Laden ermitteln
PDFView.Document doc = pdfView.getDocument();
int pages = (doc != null) ? doc.getNumberOfPages() : 1;
totalPages = Math.max(1, pages);
currentPage = 1; currentPage = 1;
pageCache.put(1, fxImage);
imageView.setImage(fxImage);
showContent(); showContent();
updateNavigationButtons(); updateNavigationButtons();
updatePageLabel(); updatePageLabel();
// Zoom nach dem ersten Rendering berechnen LOG.debug("PDF-Vorschau: Rendering abgeschlossen {} Seite(n)", totalPagesFinal);
Platform.runLater(this::updateZoom); });
LOG.debug("PDF-Vorschau: Rendering angestoßen {} Seite(n)", totalPages); } catch (InvalidPasswordException ipe) {
LOG.warn("PDF-Vorschau: PDF ist passwortgeschützt: {}", file, ipe);
closeCurrentDocumentOnWorker();
publishError(seq, PDF_PASSWORD_PROTECTED_TEXT);
} catch (Exception e) { } catch (Exception e) {
String msg = classifyLoadException(e); LOG.warn("PDF-Vorschau: Rendering fehlgeschlagen: {}", file, e);
LOG.warn("PDF-Vorschau: Rendering fehlgeschlagen {}", msg, e); closeCurrentDocumentOnWorker();
showError(msg); publishError(seq, PDF_UNREADABLE_TEXT);
updateNavigationButtons();
} }
}
/**
* Rendert eine einzelne Seite des aktuell geöffneten Dokuments.
* Läuft ausschließlich auf dem Worker-Thread.
*
* @param page 1-basierte Seitennummer
* @param seq die Sequenznummer dieser Anforderung
*/
private void renderPageOnWorker(int page, long seq) {
PDFRenderer renderer = currentRenderer;
if (renderer == null) {
// Dokument wurde zwischenzeitlich geschlossen nichts zu tun
return;
}
try {
BufferedImage buffered = renderer.renderImageWithDPI(page - 1, RENDER_DPI, ImageType.RGB);
Image fxImage = SwingFXUtils.toFXImage(buffered, null);
Platform.runLater(() -> {
if (currentRequestSequence.get() != seq) {
return; // Veraltet verwerfen
}
pageCache.put(page, fxImage);
if (currentPage == page) {
imageView.setImage(fxImage);
showContent();
}
});
} catch (Exception e) {
LOG.warn("PDF-Vorschau: Rendering von Seite {} fehlgeschlagen", page, e);
publishError(seq, PDF_UNREADABLE_TEXT);
}
}
/**
* Schließt das aktuell geöffnete PDF-Dokument, falls vorhanden. Läuft ausschließlich
* auf dem Worker-Thread und ist idempotent.
*/
private void closeCurrentDocumentOnWorker() {
PDDocument doc = currentDocument;
currentDocument = null;
currentRenderer = null;
if (doc != null) {
try {
doc.close();
} catch (Exception e) {
LOG.debug("PDF-Vorschau: Schließen des Dokuments schlug fehl", e);
}
}
}
/**
* Übergibt eine Fehlermeldung auf den FX-Thread. Veraltete Meldungen werden verworfen.
*
* @param seq Sequenznummer der Anforderung, zu der die Meldung gehört
* @param message anzuzeigende Fehlermeldung
*/
private void publishError(long seq, String message) {
Platform.runLater(() -> {
if (currentRequestSequence.get() != seq) {
return;
}
showError(message);
updateNavigationButtons();
}); });
} }
@@ -379,8 +469,8 @@ public final class PdfPreviewPane {
overlayLabel.setText(PLACEHOLDER_TEXT); overlayLabel.setText(PLACEHOLDER_TEXT);
overlayLabel.setVisible(true); overlayLabel.setVisible(true);
overlayLabel.setManaged(true); overlayLabel.setManaged(true);
pdfView.setVisible(false); imageView.setVisible(false);
pdfView.setManaged(false); imageView.setManaged(false);
progressIndicator.setVisible(false); progressIndicator.setVisible(false);
progressIndicator.setManaged(false); progressIndicator.setManaged(false);
pageLabel.setText(""); pageLabel.setText("");
@@ -391,8 +481,8 @@ public final class PdfPreviewPane {
progressIndicator.setManaged(true); progressIndicator.setManaged(true);
overlayLabel.setVisible(false); overlayLabel.setVisible(false);
overlayLabel.setManaged(false); overlayLabel.setManaged(false);
pdfView.setVisible(false); imageView.setVisible(false);
pdfView.setManaged(false); imageView.setManaged(false);
} }
private void showContent() { private void showContent() {
@@ -400,16 +490,16 @@ public final class PdfPreviewPane {
progressIndicator.setManaged(false); progressIndicator.setManaged(false);
overlayLabel.setVisible(false); overlayLabel.setVisible(false);
overlayLabel.setManaged(false); overlayLabel.setManaged(false);
pdfView.setVisible(true); imageView.setVisible(true);
pdfView.setManaged(true); imageView.setManaged(true);
} }
private void showError(String message) { private void showError(String message) {
overlayLabel.setText(message); overlayLabel.setText(message);
overlayLabel.setVisible(true); overlayLabel.setVisible(true);
overlayLabel.setManaged(true); overlayLabel.setManaged(true);
pdfView.setVisible(false); imageView.setVisible(false);
pdfView.setManaged(false); imageView.setManaged(false);
progressIndicator.setVisible(false); progressIndicator.setVisible(false);
progressIndicator.setManaged(false); progressIndicator.setManaged(false);
pageLabel.setText(""); pageLabel.setText("");
@@ -428,12 +518,4 @@ public final class PdfPreviewPane {
pageLabel.setText(""); pageLabel.setText("");
} }
} }
private static String classifyLoadException(Exception e) {
String msg = e.getMessage() == null ? "" : e.getMessage().toLowerCase(java.util.Locale.ROOT);
if (msg.contains("password") || msg.contains("encrypted") || msg.contains("encrypt")) {
return PDF_PASSWORD_PROTECTED_TEXT;
}
return PDF_UNREADABLE_TEXT;
}
} }