2026-01-15 18:23:55 +01:00
|
|
|
"""
|
|
|
|
|
PdfViewerMixin - Mixin für PDF-Viewer-Operationen.
|
|
|
|
|
|
|
|
|
|
Dieses Mixin enthält alle Methoden für die PDF-Anzeige und -Vergleich im MainWindow:
|
|
|
|
|
- PDF-Rendering und -Anzeige
|
|
|
|
|
- Alpha-Blending und Zoom
|
|
|
|
|
- Thumbnail-Navigation
|
|
|
|
|
- Drag-to-Scroll
|
|
|
|
|
- PDF-Dokument-Management
|
|
|
|
|
"""
|
|
|
|
|
|
|
|
|
|
import gc
|
|
|
|
|
import logging
|
|
|
|
|
import time
|
|
|
|
|
from pathlib import Path
|
|
|
|
|
|
2026-03-09 20:19:42 +01:00
|
|
|
from PySide6.QtCore import Qt, QSize, QTimer, QUrl
|
2026-01-15 18:23:55 +01:00
|
|
|
from PySide6.QtGui import QCursor, QPixmap, QPainter, QDesktopServices
|
2026-02-01 15:06:22 +01:00
|
|
|
from PySide6.QtWidgets import QLabel, QMessageBox, QSpacerItem, QSizePolicy
|
2026-01-15 18:23:55 +01:00
|
|
|
from PySide6.QtPdf import QPdfDocument
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class PdfViewerMixin:
|
|
|
|
|
"""
|
|
|
|
|
Mixin-Klasse für PDF-Viewer-Operationen.
|
|
|
|
|
|
|
|
|
|
Dieses Mixin erwartet, dass die verwendende Klasse folgende Attribute hat:
|
|
|
|
|
- self.ui: UI-Objekt mit Layouts, Slidern, etc.
|
|
|
|
|
- self.project: Aktuelles Projekt
|
|
|
|
|
- self.pdf_documents: Dict für PDF-Dokumente
|
|
|
|
|
- self.current_rendered_pixmaps: Cache für gerenderte Pixmaps
|
|
|
|
|
- self.fullsize_label: QLabel für Vollbild-Anzeige
|
|
|
|
|
- self.thumbnail_to_page: Dict für Thumbnail-zu-Seite-Mapping
|
|
|
|
|
- self.current_zoom: Aktueller Zoom-Faktor
|
|
|
|
|
- self.current_page: Aktuelle Seitennummer
|
|
|
|
|
- self.current_pdf: Aktueller PDF-Dateiname
|
|
|
|
|
- self.is_dragging: Drag-Status
|
|
|
|
|
- self.last_drag_position: Letzte Drag-Position
|
|
|
|
|
- self.drag_threshold: Mindestbewegung für Drag
|
|
|
|
|
- self.scroll_sensitivity: Scroll-Empfindlichkeit
|
|
|
|
|
"""
|
|
|
|
|
|
|
|
|
|
def render_and_display_page(self, pdf_filename, page_num):
|
|
|
|
|
"""
|
|
|
|
|
Rendert und zeigt eine spezifische Seite in der Vollansicht an.
|
|
|
|
|
Cached die gerenderten Pixmaps für bessere Performance.
|
|
|
|
|
|
|
|
|
|
Args:
|
|
|
|
|
pdf_filename: Name der PDF-Datei
|
|
|
|
|
page_num: Seitennummer (0-basiert)
|
|
|
|
|
"""
|
|
|
|
|
logger.debug(f"Rendere Seite {page_num + 1} von {pdf_filename}")
|
|
|
|
|
|
|
|
|
|
if pdf_filename not in self.pdf_documents:
|
|
|
|
|
logger.warning(f"PDF-Dokument {pdf_filename} nicht gefunden")
|
|
|
|
|
return
|
|
|
|
|
|
|
|
|
|
start_time = time.time()
|
|
|
|
|
|
|
|
|
|
try:
|
|
|
|
|
docs = self.pdf_documents[pdf_filename]
|
|
|
|
|
|
|
|
|
|
# Diff-Seite laden (bestimmt die Abmessungen)
|
|
|
|
|
diff_doc = docs["diff"]
|
|
|
|
|
page_size = diff_doc.pagePointSize(page_num)
|
|
|
|
|
|
|
|
|
|
# Hohe Auflösung für Vollbild (entspricht ca. Matrix(2.0, 2.0) in PyMuPDF)
|
|
|
|
|
scale_factor = 2.0
|
|
|
|
|
render_size = QSize(int(page_size.width() * scale_factor), int(page_size.height() * scale_factor))
|
|
|
|
|
|
|
|
|
|
# Diff-Seite rendern (immer vorhanden)
|
|
|
|
|
diff_image = diff_doc.render(page_num, render_size)
|
|
|
|
|
diff_pixmap = QPixmap.fromImage(diff_image)
|
|
|
|
|
|
|
|
|
|
# Ermittle die Abmessungen für weiße Seiten
|
|
|
|
|
diff_width = diff_pixmap.width()
|
|
|
|
|
diff_height = diff_pixmap.height()
|
|
|
|
|
|
|
|
|
|
# Ref-Seite prüfen und rendern oder weiße Seite erstellen
|
|
|
|
|
ref_doc = docs["ref"]
|
|
|
|
|
if page_num < ref_doc.pageCount():
|
|
|
|
|
ref_image = ref_doc.render(page_num, render_size)
|
|
|
|
|
ref_pixmap = QPixmap.fromImage(ref_image)
|
|
|
|
|
logger.debug(f"Ref-Seite {page_num + 1} gerendert")
|
|
|
|
|
else:
|
|
|
|
|
# Erstelle weiße Seite mit gleichen Abmessungen wie Diff-Seite
|
|
|
|
|
ref_pixmap = QPixmap(diff_width, diff_height)
|
|
|
|
|
ref_pixmap.fill(Qt.GlobalColor.white)
|
|
|
|
|
logger.debug(f"Weiße Ref-Seite {page_num + 1} erstellt")
|
|
|
|
|
|
|
|
|
|
# New-Seite prüfen und rendern oder weiße Seite erstellen
|
|
|
|
|
new_doc = docs["new"]
|
|
|
|
|
if page_num < new_doc.pageCount():
|
|
|
|
|
new_image = new_doc.render(page_num, render_size)
|
|
|
|
|
new_pixmap = QPixmap.fromImage(new_image)
|
|
|
|
|
logger.debug(f"New-Seite {page_num + 1} gerendert")
|
|
|
|
|
else:
|
|
|
|
|
# Erstelle weiße Seite mit gleichen Abmessungen wie Diff-Seite
|
|
|
|
|
new_pixmap = QPixmap(diff_width, diff_height)
|
|
|
|
|
new_pixmap.fill(Qt.GlobalColor.white)
|
|
|
|
|
logger.debug(f"Weiße New-Seite {page_num + 1} erstellt")
|
|
|
|
|
|
|
|
|
|
# Cache die gerenderten Pixmaps für schnelle Alpha/Zoom-Operationen
|
|
|
|
|
self.current_rendered_pixmaps = {"ref": ref_pixmap, "diff": diff_pixmap, "new": new_pixmap}
|
|
|
|
|
|
|
|
|
|
# Aktualisiere aktuelle Seite
|
|
|
|
|
self.current_page = page_num
|
|
|
|
|
self.current_pdf = pdf_filename
|
|
|
|
|
|
|
|
|
|
# Zeige das Bild mit aktuellem Alpha- und Zoom-Wert an
|
|
|
|
|
self.update_current_display()
|
|
|
|
|
|
|
|
|
|
render_time = time.time() - start_time
|
|
|
|
|
logger.debug(f"Performance: Seite {page_num + 1} gerendert in {render_time:.3f}s")
|
|
|
|
|
|
|
|
|
|
except Exception as e:
|
|
|
|
|
logger.error(f"Fehler beim Rendern der Seite {page_num + 1}: {e}", exc_info=True)
|
|
|
|
|
|
|
|
|
|
def update_current_display(self):
|
|
|
|
|
"""
|
|
|
|
|
Aktualisiert die Anzeige der aktuellen Seite basierend auf gecachten Pixmaps.
|
|
|
|
|
Verwendet für Alpha- und Zoom-Änderungen ohne erneutes PDF-Rendering.
|
|
|
|
|
"""
|
|
|
|
|
if not self.current_rendered_pixmaps:
|
|
|
|
|
logger.warning("Keine gerenderten Pixmaps verfügbar")
|
|
|
|
|
return
|
|
|
|
|
|
|
|
|
|
if self.fullsize_label is None:
|
|
|
|
|
logger.warning("Fullsize-Label ist nicht verfügbar")
|
|
|
|
|
return
|
|
|
|
|
|
|
|
|
|
try:
|
|
|
|
|
# Hole die gecachten Pixmaps
|
|
|
|
|
ref_pixmap = self.current_rendered_pixmaps["ref"]
|
|
|
|
|
diff_pixmap = self.current_rendered_pixmaps["diff"]
|
|
|
|
|
new_pixmap = self.current_rendered_pixmaps["new"]
|
|
|
|
|
|
|
|
|
|
# Erstelle das überlagerte Bild mit aktuellem Alpha-Wert
|
|
|
|
|
alpha_value = self.ui.alpha.value()
|
|
|
|
|
layered_pixmap = self.create_layered_pixmap(ref_pixmap, diff_pixmap, new_pixmap, alpha_value)
|
|
|
|
|
|
|
|
|
|
# Wende aktuellen Zoom an
|
|
|
|
|
zoom_factor = self.current_zoom / 100.0
|
|
|
|
|
if zoom_factor != 1.0:
|
|
|
|
|
new_width = int(layered_pixmap.width() * zoom_factor)
|
|
|
|
|
layered_pixmap = layered_pixmap.scaledToWidth(new_width, Qt.TransformationMode.SmoothTransformation)
|
|
|
|
|
|
|
|
|
|
# Setze das überlagerte Bild
|
|
|
|
|
self.fullsize_label.setPixmap(layered_pixmap)
|
|
|
|
|
self.fullsize_label.setAlignment(Qt.AlignmentFlag.AlignHCenter)
|
|
|
|
|
except RuntimeError as e:
|
|
|
|
|
# C++-Objekt wurde bereits gelöscht
|
|
|
|
|
logger.warning(f"Fullsize-Label wurde bereits gelöscht: {e}")
|
|
|
|
|
self.fullsize_label = None
|
|
|
|
|
|
|
|
|
|
def create_layered_pixmap(self, ref_pixmap, diff_pixmap, new_pixmap, alpha_value):
|
|
|
|
|
"""
|
|
|
|
|
Erstellt ein übergelagertes Pixmap basierend auf dem Alpha-Wert.
|
|
|
|
|
|
|
|
|
|
Args:
|
|
|
|
|
ref_pixmap: Unterste Ebene (ref)
|
|
|
|
|
diff_pixmap: Mittlere Ebene (diff)
|
|
|
|
|
new_pixmap: Oberste Ebene (new)
|
|
|
|
|
alpha_value: Alpha-Wert (-100 bis 100)
|
|
|
|
|
|
|
|
|
|
Returns:
|
|
|
|
|
QPixmap: Das überlagerte Bild
|
|
|
|
|
"""
|
|
|
|
|
# Verwende die Größe des größten Bildes
|
|
|
|
|
max_width = max(ref_pixmap.width(), diff_pixmap.width(), new_pixmap.width())
|
|
|
|
|
max_height = max(ref_pixmap.height(), diff_pixmap.height(), new_pixmap.height())
|
|
|
|
|
|
|
|
|
|
# Erstelle ein leeres Pixmap für das Ergebnis
|
|
|
|
|
result = QPixmap(max_width, max_height)
|
|
|
|
|
result.fill(Qt.GlobalColor.white)
|
|
|
|
|
|
|
|
|
|
painter = QPainter(result)
|
|
|
|
|
painter.setRenderHint(QPainter.RenderHint.Antialiasing)
|
|
|
|
|
|
|
|
|
|
if alpha_value <= 0:
|
|
|
|
|
# Alpha von -100 bis 0: Übergang von ref zu diff
|
|
|
|
|
ref_opacity = abs(alpha_value) / 100
|
|
|
|
|
diff_opacity = 1.0 - abs(alpha_value) / 100.0
|
|
|
|
|
new_opacity = 0.0
|
|
|
|
|
else:
|
|
|
|
|
ref_opacity = 0.0
|
|
|
|
|
diff_opacity = 1.0 - alpha_value / 100.0
|
|
|
|
|
new_opacity = alpha_value / 100.0
|
|
|
|
|
|
|
|
|
|
# Zeichne die Ebenen mit entsprechender Transparenz
|
|
|
|
|
if ref_opacity > 0:
|
|
|
|
|
painter.setOpacity(ref_opacity)
|
|
|
|
|
painter.drawPixmap(0, 0, ref_pixmap)
|
|
|
|
|
|
|
|
|
|
if diff_opacity > 0:
|
|
|
|
|
painter.setOpacity(diff_opacity)
|
|
|
|
|
painter.drawPixmap(0, 0, diff_pixmap)
|
|
|
|
|
|
|
|
|
|
if new_opacity > 0:
|
|
|
|
|
painter.setOpacity(new_opacity)
|
|
|
|
|
painter.drawPixmap(0, 0, new_pixmap)
|
|
|
|
|
|
|
|
|
|
painter.end()
|
|
|
|
|
return result
|
|
|
|
|
|
2026-03-09 20:19:42 +01:00
|
|
|
def _schedule_thumbnail_rendering(self, diff_doc, pdf_basename, thumbnail_labels, page_num):
|
|
|
|
|
"""Rendert Thumbnails progressiv — ein Thumbnail pro Event-Loop-Iteration."""
|
|
|
|
|
if page_num >= len(thumbnail_labels):
|
|
|
|
|
return
|
|
|
|
|
|
|
|
|
|
thumbnail = thumbnail_labels[page_num]
|
|
|
|
|
# Prüfe ob das Label noch existiert (Benutzer könnte inzwischen anderes PDF geöffnet haben)
|
|
|
|
|
if thumbnail_labels[page_num] not in self.thumbnail_to_page:
|
|
|
|
|
return
|
|
|
|
|
|
|
|
|
|
page_size = diff_doc.pagePointSize(page_num)
|
|
|
|
|
scale_factor = 200.0 / page_size.width()
|
|
|
|
|
page_image = diff_doc.render(
|
|
|
|
|
page_num,
|
|
|
|
|
QSize(int(page_size.width() * scale_factor), int(page_size.height() * scale_factor)),
|
|
|
|
|
)
|
|
|
|
|
thumbnail.setPixmap(QPixmap.fromImage(page_image).scaledToWidth(200, Qt.TransformationMode.SmoothTransformation))
|
|
|
|
|
thumbnail.setMinimumHeight(0)
|
|
|
|
|
|
|
|
|
|
QTimer.singleShot(0, lambda: self._schedule_thumbnail_rendering(diff_doc, pdf_basename, thumbnail_labels, page_num + 1))
|
|
|
|
|
|
2026-01-15 18:23:55 +01:00
|
|
|
def _clear_layout(self, layout):
|
|
|
|
|
"""Entfernt alle Widgets aus einem Layout."""
|
|
|
|
|
if layout is not None:
|
|
|
|
|
while layout.count():
|
|
|
|
|
item = layout.takeAt(0)
|
|
|
|
|
widget = item.widget()
|
|
|
|
|
if widget is not None:
|
|
|
|
|
widget.deleteLater()
|
|
|
|
|
|
|
|
|
|
def on_alpha_changed(self, alpha_value):
|
|
|
|
|
"""
|
|
|
|
|
Wird ausgeführt, wenn der Alpha-Slider geändert wird.
|
|
|
|
|
Optimiert: Verwendet gecachte Pixmaps statt erneutes PDF-Rendering.
|
|
|
|
|
|
|
|
|
|
Args:
|
|
|
|
|
alpha_value: Der neue Alpha-Wert (-100 bis 100)
|
|
|
|
|
"""
|
|
|
|
|
logger.debug(f"Alpha geändert auf {alpha_value}")
|
|
|
|
|
|
|
|
|
|
start_time = time.time()
|
|
|
|
|
# Verwende gecachte Pixmaps für schnelle Alpha-Änderungen
|
|
|
|
|
self.update_current_display()
|
|
|
|
|
alpha_time = time.time() - start_time
|
|
|
|
|
logger.debug(f"Alpha-Update in {alpha_time:.6f}s")
|
|
|
|
|
|
|
|
|
|
def on_thumbnail_clicked(self, event, thumbnail):
|
|
|
|
|
"""
|
|
|
|
|
Wird ausgeführt, wenn ein Thumbnail angeklickt wird.
|
|
|
|
|
|
|
|
|
|
Args:
|
|
|
|
|
event: Das Maus-Event
|
|
|
|
|
thumbnail: Das geklickte Thumbnail-Label
|
|
|
|
|
"""
|
|
|
|
|
page_info = self.thumbnail_to_page.get(thumbnail)
|
|
|
|
|
if page_info:
|
|
|
|
|
pdf_filename = page_info["pdf_filename"]
|
|
|
|
|
page_num = page_info["page_num"]
|
|
|
|
|
|
|
|
|
|
logger.debug(f"Thumbnail für Seite {page_num + 1} von {pdf_filename} wurde angeklickt")
|
|
|
|
|
|
|
|
|
|
# Rendere und zeige die gewählte Seite an
|
|
|
|
|
self.render_and_display_page(pdf_filename, page_num)
|
|
|
|
|
|
|
|
|
|
def apply_zoom(self, zoom_value):
|
|
|
|
|
"""
|
|
|
|
|
Wendet den Zoom-Faktor auf das aktuelle Bild an.
|
|
|
|
|
Optimiert: Verwendet gecachte Pixmaps statt erneutes PDF-Rendering.
|
|
|
|
|
|
|
|
|
|
Args:
|
|
|
|
|
zoom_value: Der neue Zoom-Wert (in Prozent)
|
|
|
|
|
"""
|
|
|
|
|
self.current_zoom = zoom_value
|
|
|
|
|
logger.debug(f"Zoom geändert auf {zoom_value}%")
|
|
|
|
|
|
|
|
|
|
# Verwende gecachte Pixmaps für schnelle Zoom-Änderungen
|
|
|
|
|
self.update_current_display()
|
|
|
|
|
|
|
|
|
|
def on_fullsize_mouse_press(self, event, fullsize_label):
|
|
|
|
|
"""Wird ausgeführt, wenn die Maustaste auf einem großen Bild gedrückt wird."""
|
|
|
|
|
if event.button() == Qt.MouseButton.LeftButton:
|
|
|
|
|
self.is_dragging = True
|
|
|
|
|
self.last_drag_position = event.globalPosition().toPoint()
|
|
|
|
|
fullsize_label.setCursor(QCursor(Qt.CursorShape.ClosedHandCursor))
|
|
|
|
|
|
|
|
|
|
def on_fullsize_mouse_move(self, event, fullsize_label):
|
|
|
|
|
"""Wird ausgeführt, wenn die Maus über einem großen Bild bewegt wird."""
|
|
|
|
|
if self.is_dragging and self.last_drag_position is not None:
|
|
|
|
|
current_pos = event.globalPosition().toPoint()
|
|
|
|
|
delta = current_pos - self.last_drag_position
|
|
|
|
|
|
|
|
|
|
if abs(delta.x()) >= self.drag_threshold or abs(delta.y()) >= self.drag_threshold:
|
|
|
|
|
v_scrollbar = self.ui.scrollArea_2.verticalScrollBar()
|
|
|
|
|
h_scrollbar = self.ui.scrollArea_2.horizontalScrollBar()
|
|
|
|
|
|
|
|
|
|
scroll_delta_y = int(-delta.y() * self.scroll_sensitivity)
|
|
|
|
|
scroll_delta_x = int(-delta.x() * self.scroll_sensitivity)
|
|
|
|
|
|
|
|
|
|
new_v_value = v_scrollbar.value() + scroll_delta_y
|
|
|
|
|
new_h_value = h_scrollbar.value() + scroll_delta_x
|
|
|
|
|
|
|
|
|
|
v_scrollbar.setValue(new_v_value)
|
|
|
|
|
h_scrollbar.setValue(new_h_value)
|
|
|
|
|
|
|
|
|
|
self.last_drag_position = current_pos
|
|
|
|
|
|
|
|
|
|
def on_fullsize_mouse_release(self, event, fullsize_label):
|
|
|
|
|
"""Wird ausgeführt, wenn die Maustaste auf einem großen Bild losgelassen wird."""
|
|
|
|
|
if event.button() == Qt.MouseButton.LeftButton:
|
|
|
|
|
self.is_dragging = False
|
|
|
|
|
self.last_drag_position = None
|
|
|
|
|
fullsize_label.setCursor(QCursor(Qt.CursorShape.OpenHandCursor))
|
|
|
|
|
|
|
|
|
|
def _load_pdf_for_comparison(self, xml_file_path: Path, xsl_id_str: str):
|
|
|
|
|
"""
|
|
|
|
|
Lädt die PDFs (diff, ref, new) einer Transformation in den Vergleichs-Viewer.
|
|
|
|
|
|
|
|
|
|
Args:
|
|
|
|
|
xml_file_path: Pfad zur XML-Datei (relativ)
|
|
|
|
|
xsl_id_str: XSL-ID als String (z.B. "2002_1_128")
|
|
|
|
|
"""
|
|
|
|
|
try:
|
|
|
|
|
if not self.project:
|
|
|
|
|
QMessageBox.warning(self, "Fehler", "Kein Projekt geöffnet")
|
|
|
|
|
return
|
|
|
|
|
|
|
|
|
|
# Ermittle PDF-Dateinamen basierend auf XML und XSL-ID
|
|
|
|
|
xml_stem = xml_file_path.stem
|
|
|
|
|
pdf_basename = f"{xml_stem}_xsl_{xsl_id_str}.pdf"
|
|
|
|
|
|
|
|
|
|
# Pfade zu den drei PDFs
|
|
|
|
|
diff_dir = self.project.project_dir / "diff"
|
|
|
|
|
ref_dir = self.project.project_dir / "ref"
|
|
|
|
|
new_dir = self.project.project_dir / "new"
|
|
|
|
|
|
|
|
|
|
diff_pdf_path = diff_dir / pdf_basename
|
|
|
|
|
ref_pdf_path = ref_dir / pdf_basename
|
|
|
|
|
new_pdf_path = new_dir / pdf_basename
|
|
|
|
|
|
|
|
|
|
# Prüfe ob PDFs existieren
|
|
|
|
|
if not diff_pdf_path.exists():
|
|
|
|
|
QMessageBox.information(self, "Keine Diff-PDF", f"Diff-PDF nicht gefunden:\n{pdf_basename}")
|
|
|
|
|
return
|
|
|
|
|
|
|
|
|
|
if not ref_pdf_path.exists() or not new_pdf_path.exists():
|
|
|
|
|
QMessageBox.warning(
|
|
|
|
|
self,
|
|
|
|
|
"Fehlende PDFs",
|
|
|
|
|
f"Ref-PDF oder New-PDF nicht gefunden:\n{pdf_basename}\n\nNur Diff-PDF vorhanden.",
|
|
|
|
|
)
|
|
|
|
|
return
|
|
|
|
|
|
|
|
|
|
logger.info(f"Lade PDFs für Vergleich: {pdf_basename}")
|
|
|
|
|
|
|
|
|
|
# Entferne bestehende Widgets aus den Layouts
|
|
|
|
|
self._clear_layout(self.ui.verticalLayout_2)
|
|
|
|
|
self._clear_layout(self.ui.verticalLayout_3)
|
|
|
|
|
|
2026-02-01 15:06:22 +01:00
|
|
|
# Setze kompaktes Spacing für Thumbnail-Layout
|
|
|
|
|
self.ui.verticalLayout_2.setSpacing(5) # Minimaler Abstand zwischen Widgets
|
|
|
|
|
self.ui.verticalLayout_2.setContentsMargins(0, 0, 0, 0) # Keine Ränder
|
|
|
|
|
|
2026-01-15 18:23:55 +01:00
|
|
|
# Dicts zurücksetzen
|
|
|
|
|
self.thumbnail_to_page = {}
|
|
|
|
|
self.pdf_documents = {}
|
|
|
|
|
self.current_rendered_pixmaps = None
|
|
|
|
|
self.fullsize_label = None # Label wurde durch _clear_layout gelöscht
|
|
|
|
|
|
|
|
|
|
# Alle drei PDF-Dateien öffnen mit QtPdf
|
|
|
|
|
diff_doc = QPdfDocument()
|
|
|
|
|
ref_doc = QPdfDocument()
|
|
|
|
|
new_doc = QPdfDocument()
|
|
|
|
|
|
|
|
|
|
# PDF-Dateien laden
|
|
|
|
|
diff_doc.load(str(diff_pdf_path))
|
|
|
|
|
ref_doc.load(str(ref_pdf_path))
|
|
|
|
|
new_doc.load(str(new_pdf_path))
|
|
|
|
|
|
|
|
|
|
# Warten bis PDFs geladen sind
|
|
|
|
|
if (
|
|
|
|
|
diff_doc.status() != QPdfDocument.Status.Ready
|
|
|
|
|
or ref_doc.status() != QPdfDocument.Status.Ready
|
|
|
|
|
or new_doc.status() != QPdfDocument.Status.Ready
|
|
|
|
|
):
|
|
|
|
|
QMessageBox.critical(self, "Fehler", f"Fehler beim Laden der PDFs:\n{pdf_basename}")
|
|
|
|
|
return
|
|
|
|
|
|
|
|
|
|
# PDF-Dokumente speichern
|
|
|
|
|
self.pdf_documents[pdf_basename] = {"diff": diff_doc, "ref": ref_doc, "new": new_doc}
|
|
|
|
|
|
|
|
|
|
# PDF-Pfade für System-Viewer speichern
|
|
|
|
|
self.current_ref_pdf_path = ref_pdf_path
|
|
|
|
|
self.current_new_pdf_path = new_pdf_path
|
|
|
|
|
|
|
|
|
|
# Buttons zum Öffnen der PDFs im System-Viewer aktivieren
|
|
|
|
|
self.ui.view_ref_pdf.setEnabled(True)
|
|
|
|
|
self.ui.view_new_pdf.setEnabled(True)
|
|
|
|
|
|
|
|
|
|
# Slider aktivieren
|
|
|
|
|
self.ui.alpha.setEnabled(True)
|
|
|
|
|
self.ui.zoom.setEnabled(True)
|
|
|
|
|
|
|
|
|
|
logger.info(f"PDFs geladen: {pdf_basename}")
|
|
|
|
|
logger.info(f" diff: {diff_doc.pageCount()} Seiten")
|
|
|
|
|
logger.info(f" ref: {ref_doc.pageCount()} Seiten")
|
|
|
|
|
logger.info(f" new: {new_doc.pageCount()} Seiten")
|
|
|
|
|
|
|
|
|
|
# Nehme die Seitenzahl der diff-PDF als Basis
|
|
|
|
|
max_pages = diff_doc.pageCount()
|
|
|
|
|
|
2026-03-09 20:19:42 +01:00
|
|
|
# Erstelle Placeholder-Labels für alle Seiten (sofort, ohne Rendern)
|
|
|
|
|
thumbnail_labels = []
|
2026-01-15 18:23:55 +01:00
|
|
|
for page_num in range(max_pages):
|
|
|
|
|
thumbnail = QLabel()
|
|
|
|
|
thumbnail.setObjectName(f"thumbnail_{pdf_basename}_page_{page_num + 1}")
|
2026-03-09 20:19:42 +01:00
|
|
|
thumbnail.setText(f"Seite {page_num + 1}\n…")
|
2026-01-15 18:23:55 +01:00
|
|
|
thumbnail.setCursor(QCursor(Qt.CursorShape.PointingHandCursor))
|
|
|
|
|
thumbnail.setMouseTracking(True)
|
2026-02-01 15:06:22 +01:00
|
|
|
thumbnail.setAlignment(Qt.AlignmentFlag.AlignCenter)
|
2026-03-09 20:19:42 +01:00
|
|
|
thumbnail.setMinimumHeight(150)
|
2026-01-15 18:23:55 +01:00
|
|
|
self.ui.verticalLayout_2.addWidget(thumbnail)
|
|
|
|
|
|
|
|
|
|
thumbnail_info = QLabel(f"Seite {page_num + 1}")
|
|
|
|
|
thumbnail_info.setAlignment(Qt.AlignmentFlag.AlignCenter)
|
2026-03-09 20:19:42 +01:00
|
|
|
thumbnail_info.setMaximumHeight(18)
|
|
|
|
|
thumbnail_info.setContentsMargins(0, 0, 0, 0)
|
2026-01-15 18:23:55 +01:00
|
|
|
self.ui.verticalLayout_2.addWidget(thumbnail_info)
|
|
|
|
|
|
|
|
|
|
self.thumbnail_to_page[thumbnail] = {"pdf_filename": pdf_basename, "page_num": page_num}
|
|
|
|
|
thumbnail.mousePressEvent = lambda event, t=thumbnail: self.on_thumbnail_clicked(event, t)
|
2026-03-09 20:19:42 +01:00
|
|
|
thumbnail_labels.append(thumbnail)
|
|
|
|
|
|
|
|
|
|
# Thumbnails progressiv rendern (nicht blockierend)
|
|
|
|
|
self._schedule_thumbnail_rendering(diff_doc, pdf_basename, thumbnail_labels, 0)
|
2026-01-15 18:23:55 +01:00
|
|
|
|
2026-02-01 15:06:22 +01:00
|
|
|
# Füge expandierenden Spacer am Ende hinzu, damit Thumbnails oben bleiben
|
|
|
|
|
spacer = QSpacerItem(20, 40, QSizePolicy.Policy.Minimum, QSizePolicy.Policy.Expanding)
|
|
|
|
|
self.ui.verticalLayout_2.addItem(spacer)
|
|
|
|
|
|
2026-01-15 18:23:55 +01:00
|
|
|
# Erstelle das Vollbild-Label für die rechte Spalte (falls noch nicht vorhanden)
|
|
|
|
|
if self.fullsize_label is None:
|
|
|
|
|
self.fullsize_label = QLabel()
|
|
|
|
|
self.fullsize_label.setObjectName("fullsize_current_page")
|
|
|
|
|
self.fullsize_label.setAlignment(Qt.AlignmentFlag.AlignHCenter)
|
|
|
|
|
self.fullsize_label.setCursor(QCursor(Qt.CursorShape.OpenHandCursor))
|
|
|
|
|
self.ui.verticalLayout_3.addWidget(self.fullsize_label)
|
|
|
|
|
|
|
|
|
|
# Drag-to-Scroll Events für das große Bild einrichten
|
|
|
|
|
self.fullsize_label.mousePressEvent = lambda event: self.on_fullsize_mouse_press(
|
|
|
|
|
event, self.fullsize_label
|
|
|
|
|
)
|
|
|
|
|
self.fullsize_label.mouseMoveEvent = lambda event: self.on_fullsize_mouse_move(
|
|
|
|
|
event, self.fullsize_label
|
|
|
|
|
)
|
|
|
|
|
self.fullsize_label.mouseReleaseEvent = lambda event: self.on_fullsize_mouse_release(
|
|
|
|
|
event, self.fullsize_label
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
# Setze die aktuelle PDF
|
|
|
|
|
self.current_pdf = pdf_basename
|
|
|
|
|
|
|
|
|
|
# Speichere Diff-PDF-Informationen für Accept Changes
|
|
|
|
|
self.current_diff_xml_path = xml_file_path
|
|
|
|
|
self.current_diff_xsl_id = xsl_id_str
|
|
|
|
|
|
|
|
|
|
# Aktiviere Accept-Changes-Button
|
|
|
|
|
self.ui.accept_changes.setEnabled(True)
|
|
|
|
|
|
|
|
|
|
# Zeige die erste Seite initial an
|
|
|
|
|
self.render_and_display_page(pdf_basename, 0)
|
|
|
|
|
|
|
|
|
|
logger.info(f"PDF-Vergleich geladen: {pdf_basename}")
|
|
|
|
|
|
|
|
|
|
except Exception as e:
|
|
|
|
|
logger.error(f"Fehler beim Laden der PDFs für Vergleich: {e}")
|
|
|
|
|
QMessageBox.critical(self, "Fehler", f"Konnte PDFs nicht laden:\n{str(e)}")
|
|
|
|
|
|
|
|
|
|
def _close_all_pdf_documents(self):
|
|
|
|
|
"""Schließt alle geöffneten PDF-Dokumente explizit (wichtig für Windows)."""
|
|
|
|
|
if self.pdf_documents:
|
|
|
|
|
for pdf_basename, docs in self.pdf_documents.items():
|
|
|
|
|
for doc_type, doc in docs.items():
|
|
|
|
|
if doc:
|
|
|
|
|
doc.close()
|
|
|
|
|
logger.debug(f"PDF-Dokument geschlossen: {pdf_basename} ({doc_type})")
|
|
|
|
|
|
|
|
|
|
# Lösche alle Referenzen
|
|
|
|
|
self.pdf_documents.clear()
|
|
|
|
|
|
|
|
|
|
# Lösche gerenderte Pixmaps
|
|
|
|
|
self.current_rendered_pixmaps = None
|
|
|
|
|
|
|
|
|
|
# Erzwinge Garbage Collection um Dateihandles freizugeben (wichtig für Windows)
|
|
|
|
|
gc.collect()
|
|
|
|
|
|
|
|
|
|
logger.info("Alle PDF-Dokumente geschlossen und Referenzen freigegeben")
|
|
|
|
|
|
|
|
|
|
def _clear_pdf_viewer(self):
|
|
|
|
|
"""Leert den PDF-Viewer und alle Thumbnails."""
|
|
|
|
|
# Schließe alle PDF-Dokumente explizit (wichtig für Windows)
|
|
|
|
|
self._close_all_pdf_documents()
|
|
|
|
|
|
|
|
|
|
# Entferne Widgets aus Layouts
|
|
|
|
|
self._clear_layout(self.ui.verticalLayout_2)
|
|
|
|
|
self._clear_layout(self.ui.verticalLayout_3)
|
|
|
|
|
|
|
|
|
|
# Zurücksetzen der Datenstrukturen
|
|
|
|
|
self.thumbnail_to_page = {}
|
|
|
|
|
self.pdf_documents = {}
|
|
|
|
|
self.current_rendered_pixmaps = None
|
|
|
|
|
self.fullsize_label = None
|
|
|
|
|
self.current_pdf = None
|
|
|
|
|
self.current_diff_xml_path = None
|
|
|
|
|
self.current_diff_xsl_id = None
|
|
|
|
|
|
|
|
|
|
# PDF-Pfade zurücksetzen und Buttons deaktivieren
|
|
|
|
|
self.current_ref_pdf_path = None
|
|
|
|
|
self.current_new_pdf_path = None
|
|
|
|
|
self.ui.view_ref_pdf.setEnabled(False)
|
|
|
|
|
self.ui.view_new_pdf.setEnabled(False)
|
2026-01-17 20:08:54 +01:00
|
|
|
self.ui.accept_changes.setEnabled(False)
|
2026-01-15 18:23:55 +01:00
|
|
|
|
|
|
|
|
# Slider deaktivieren
|
|
|
|
|
self.ui.alpha.setEnabled(False)
|
|
|
|
|
self.ui.zoom.setEnabled(False)
|
|
|
|
|
|
|
|
|
|
logger.info("PDF-Viewer geleert")
|
|
|
|
|
|
|
|
|
|
def _on_view_ref_pdf_clicked(self):
|
|
|
|
|
"""
|
|
|
|
|
Handler für view_ref_pdf Button.
|
|
|
|
|
Öffnet die Referenz-PDF im systemseitig installierten PDF-Viewer.
|
|
|
|
|
"""
|
|
|
|
|
if not self.current_ref_pdf_path or not self.current_ref_pdf_path.exists():
|
|
|
|
|
QMessageBox.warning(self, "Fehler", "Referenz-PDF nicht gefunden")
|
|
|
|
|
logger.warning("Referenz-PDF nicht verfügbar")
|
|
|
|
|
return
|
|
|
|
|
|
|
|
|
|
logger.info(f"Öffne Referenz-PDF im System-Viewer: {self.current_ref_pdf_path}")
|
|
|
|
|
url = QUrl.fromLocalFile(str(self.current_ref_pdf_path))
|
|
|
|
|
if not QDesktopServices.openUrl(url):
|
|
|
|
|
QMessageBox.critical(self, "Fehler", f"Konnte Referenz-PDF nicht öffnen:\n{self.current_ref_pdf_path}")
|
|
|
|
|
logger.error(f"Fehler beim Öffnen der Referenz-PDF: {self.current_ref_pdf_path}")
|
|
|
|
|
|
|
|
|
|
def _on_view_new_pdf_clicked(self):
|
|
|
|
|
"""
|
|
|
|
|
Handler für view_new_pdf Button.
|
|
|
|
|
Öffnet die neue PDF im systemseitig installierten PDF-Viewer.
|
|
|
|
|
"""
|
|
|
|
|
if not self.current_new_pdf_path or not self.current_new_pdf_path.exists():
|
|
|
|
|
QMessageBox.warning(self, "Fehler", "Neue PDF nicht gefunden")
|
|
|
|
|
logger.warning("Neue PDF nicht verfügbar")
|
|
|
|
|
return
|
|
|
|
|
|
|
|
|
|
logger.info(f"Öffne neue PDF im System-Viewer: {self.current_new_pdf_path}")
|
|
|
|
|
url = QUrl.fromLocalFile(str(self.current_new_pdf_path))
|
|
|
|
|
if not QDesktopServices.openUrl(url):
|
|
|
|
|
QMessageBox.critical(self, "Fehler", f"Konnte neue PDF nicht öffnen:\n{self.current_new_pdf_path}")
|
|
|
|
|
logger.error(f"Fehler beim Öffnen der neuen PDF: {self.current_new_pdf_path}")
|