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:
@@ -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>
|
||||||
|
|||||||
+201
-119
@@ -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;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user