diff --git a/src/conf.py b/src/conf.py index 01e1f39..0a8a913 100644 --- a/src/conf.py +++ b/src/conf.py @@ -144,6 +144,7 @@ class AppSettings(BaseSettings): theme: str | None = None max_workers: int = 8 # Anzahl paralleler Worker für Transformationen (Standard: 8) use_saxon_worker_pool: bool = True # SaxonWorkerPool aktivieren (schneller, benötigt JDK) + use_fop_worker_pool: bool = True # FopWorkerPool aktivieren (schneller, benötigt JDK) # UI-Zustand window_geometry: tuple[int, int, int, int] | None = None # (x, y, width, height) diff --git a/src/fop_pool.py b/src/fop_pool.py new file mode 100644 index 0000000..034916d --- /dev/null +++ b/src/fop_pool.py @@ -0,0 +1,454 @@ +""" +FOP Worker Pool - Persistente JVM-Prozesse für schnelle PDF-Generierung. + +Eliminiert JVM-Startup-Overhead durch Vorinitialisierung von N Worker-Prozessen. +Jeder Worker läuft als Daemon und verarbeitet mehrere FO→PDF Transformationen nacheinander. +""" + +import logging +import subprocess +import threading +from pathlib import Path +from queue import Queue +from typing import Optional +import tempfile + +logger = logging.getLogger(__name__) + +# Java-Worker-Code (wird zur Laufzeit kompiliert) +FOP_WORKER_JAVA = """ +import org.apache.fop.apps.*; +import org.xml.sax.SAXException; +import javax.xml.transform.*; +import javax.xml.transform.sax.SAXResult; +import javax.xml.transform.stream.StreamSource; +import java.io.*; +import java.net.URI; + +public class FopWorker { + public static void main(String[] args) { + BufferedReader reader = new BufferedReader(new InputStreamReader(System.in)); + String line; + + System.err.println("FopWorker starting..."); + System.err.flush(); + + // Create FopFactory once and reuse (major performance boost!) + FopFactory fopFactory = null; + + try { + // Check if config file is provided as first argument + if (args.length > 0 && !args[0].isEmpty()) { + File configFile = new File(args[0]); + if (configFile.exists()) { + System.err.println("Loading FOP config: " + configFile.getAbsolutePath()); + fopFactory = FopFactory.newInstance(configFile); + } else { + System.err.println("Config file not found, using default configuration"); + fopFactory = FopFactory.newInstance(new File(".").toURI()); + } + } else { + System.err.println("No config file specified, using default FOP configuration"); + fopFactory = FopFactory.newInstance(new File(".").toURI()); + } + + System.err.println("FopWorker started and ready"); + System.err.flush(); + + } catch (Exception e) { + System.err.println("FATAL: Failed to initialize FopFactory: " + e.getMessage()); + e.printStackTrace(System.err); + System.exit(1); + } + + try { + while ((line = reader.readLine()) != null) { + System.err.println("DEBUG: Received line: " + line.substring(0, Math.min(100, line.length()))); + System.err.flush(); + + if ("EXIT".equals(line.trim())) { + System.err.println("FopWorker exiting"); + break; + } + + try { + // Parse job + System.err.println("DEBUG: Parsing job..."); + System.err.flush(); + + String[] parts = line.split("\\t"); + System.err.println("DEBUG: Parts count: " + parts.length); + System.err.flush(); + + if (parts.length < 2) { + System.out.println("ERROR: Invalid job format"); + System.out.flush(); + continue; + } + + String inputFo = parts[0]; + String outputPdf = parts[1]; + + System.err.println("DEBUG: Input FO: " + inputFo); + System.err.println("DEBUG: Output PDF: " + outputPdf); + System.err.flush(); + + // Create FOUserAgent for this transformation + FOUserAgent foUserAgent = fopFactory.newFOUserAgent(); + + // Note: Event Listener für detailliertes Error-Logging könnte hier hinzugefügt werden, + // aber ist nicht kritisch - Fehler werden durch Exceptions gefangen + + // Create output stream + File outputFile = new File(outputPdf); + outputFile.getParentFile().mkdirs(); + OutputStream out = new BufferedOutputStream(new FileOutputStream(outputFile)); + + try { + System.err.println("DEBUG: Creating Fop instance..."); + System.err.flush(); + + // Create Fop instance + Fop fop = fopFactory.newFop(MimeConstants.MIME_PDF, foUserAgent, out); + + System.err.println("DEBUG: Setting up transformer..."); + System.err.flush(); + + // Setup Transformer + TransformerFactory transformerFactory = TransformerFactory.newInstance(); + Transformer transformer = transformerFactory.newTransformer(); + + // Setup input and output + Source src = new StreamSource(new File(inputFo)); + Result res = new SAXResult(fop.getDefaultHandler()); + + System.err.println("DEBUG: Running FOP transformation..."); + System.err.flush(); + + // Run transformation + transformer.transform(src, res); + + System.err.println("DEBUG: FOP transformation completed"); + System.err.flush(); + + } finally { + out.close(); + } + + // Transformation erfolgreich + System.out.println("OK"); + System.out.flush(); + + } catch (Exception e) { + System.err.println("DEBUG: Job processing exception: " + e.getClass().getName()); + System.err.flush(); + e.printStackTrace(System.err); + + String errorMsg = e.getMessage(); + if (errorMsg == null || errorMsg.isEmpty()) { + errorMsg = e.getClass().getSimpleName(); + } + System.out.println("ERROR: " + errorMsg); + System.out.flush(); + } + } + } catch (IOException e) { + System.err.println("FopWorker I/O error: " + e.getMessage()); + e.printStackTrace(System.err); + } + } +} +""" + + +class FopWorkerPool: + """ + Pool von lang-laufenden JVM-Prozessen für Apache FOP PDF-Generierung. + + Eliminiert JVM-Startup-Overhead durch Wiederverwendung von N Worker-Prozessen. + """ + + def __init__( + self, + num_workers: int, + java_vm_path: Path, + apache_fop_dir: Path, + fop_config_file: Optional[Path] = None, + log_dir: Optional[Path] = None, + ): + """ + Initialisiert den FOP-Worker-Pool. + + Args: + num_workers: Anzahl der Worker-Prozesse + java_vm_path: Pfad zur Java VM Binary + apache_fop_dir: Pfad zum Apache FOP-Verzeichnis + fop_config_file: Optionaler Pfad zur fop.xconf Konfigurationsdatei + log_dir: Optionales Verzeichnis für Worker-Logs (Standard: temp_dir/temp) + """ + self.num_workers = num_workers + self.java_vm_path = java_vm_path + self.apache_fop_dir = apache_fop_dir + self.fop_config_file = fop_config_file + self.log_dir = log_dir + + # Worker-Prozesse und Queues + self.workers: list[subprocess.Popen] = [] + self.job_queue: Queue = Queue() + self.result_queue: Queue = Queue() + self.worker_locks: list[threading.Lock] = [] + + # Temporäres Verzeichnis für kompilierte Java-Klasse + self.temp_dir: Optional[Path] = None + self.worker_class_path: Optional[Path] = None + self.worker_log_dir: Optional[Path] = None + + # Classpath für FOP + self.fop_classpath: Optional[str] = None + + # Initialisierung + self._build_fop_classpath() + self._compile_worker_class() + self._start_workers() + + logger.info(f"FopWorkerPool initialisiert mit {num_workers} Workern") + + def _build_fop_classpath(self): + """Erstellt den Classpath für Apache FOP.""" + import glob + import sys + + # Sammle alle JAR-Dateien im FOP-Verzeichnis + all_jars = glob.glob(str(self.apache_fop_dir / "build" / "*.jar")) + + # FOP lib-Verzeichnis + lib_dir = self.apache_fop_dir / "lib" + if lib_dir.exists() and lib_dir.is_dir(): + all_jars.extend(glob.glob(str(lib_dir / "*.jar"))) + + if not all_jars: + raise RuntimeError(f"Keine FOP JAR-Dateien gefunden in {self.apache_fop_dir}") + + classpath_separator = ";" if sys.platform == "win32" else ":" + self.fop_classpath = classpath_separator.join(all_jars) + + logger.debug(f"FOP Classpath: {len(all_jars)} JARs") + + def _compile_worker_class(self): + """Kompiliert die FopWorker-Java-Klasse.""" + try: + # Erstelle temporäres Verzeichnis + self.temp_dir = Path(tempfile.mkdtemp(prefix="fop_worker_")) + + # Schreibe Java-Quellcode + java_file = self.temp_dir / "FopWorker.java" + java_file.write_text(FOP_WORKER_JAVA, encoding="utf-8") + + # Kompiliere Java-Klasse + javac_cmd = [ + str(self.java_vm_path).replace("java", "javac"), + "-cp", + self.fop_classpath, + str(java_file), + ] + + logger.debug(f"Kompiliere FopWorker: {' '.join(javac_cmd[:3])}...") + + result = subprocess.run(javac_cmd, capture_output=True, text=True, timeout=30) + + if result.returncode != 0: + raise RuntimeError(f"Java-Kompilierung fehlgeschlagen: {result.stderr}") + + self.worker_class_path = self.temp_dir + + logger.info(f"FopWorker erfolgreich kompiliert: {self.temp_dir}") + + except Exception as e: + logger.error(f"Fehler beim Kompilieren von FopWorker: {e}") + raise + + def _start_workers(self): + """Startet N Worker-Prozesse.""" + import sys + + # Füge Worker-Classpath zum FOP-Classpath hinzu + classpath_separator = ";" if sys.platform == "win32" else ":" + full_classpath = str(self.worker_class_path) + classpath_separator + self.fop_classpath + + # Bestimme Log-Verzeichnis + self.worker_log_dir = self.log_dir if self.log_dir else self.temp_dir + if self.log_dir: + self.worker_log_dir.mkdir(parents=True, exist_ok=True) + + for i in range(self.num_workers): + try: + # Starte JVM-Prozess mit FopWorker + # Übergebe fop.xconf als Argument falls vorhanden + cmd = [str(self.java_vm_path), "-cp", full_classpath, "FopWorker"] + + if self.fop_config_file and self.fop_config_file.exists(): + cmd.append(str(self.fop_config_file)) + + # Öffne stderr-Log-Datei für diesen Worker + stderr_log = self.worker_log_dir / f"fop_worker_{i}_stderr.log" + stderr_file = open(stderr_log, "w", encoding="utf-8") + + process = subprocess.Popen( + cmd, + stdin=subprocess.PIPE, + stdout=subprocess.PIPE, + stderr=stderr_file, # Redirect stderr to file + text=True, + bufsize=1, # Line buffered + ) + + self.workers.append(process) + self.worker_locks.append(threading.Lock()) + + logger.debug(f"FOP Worker {i} gestartet (PID: {process.pid}, stderr: {stderr_log})") + + # Warte kurz damit Worker initialisieren kann + import time + + time.sleep(0.2) # FOP braucht etwas länger zum Initialisieren + + # Prüfe ob Worker noch läuft + if process.poll() is not None: + # Worker ist bereits beendet - Fehler! + stderr_file.close() + with open(stderr_log, "r") as f: + stderr_content = f.read() + raise RuntimeError( + f"FOP Worker {i} ist sofort beendet (Exit Code: {process.returncode})\nstderr:\n{stderr_content}" + ) + + except Exception as e: + logger.error(f"Fehler beim Starten von FOP Worker {i}: {e}") + raise + + logger.info(f"{len(self.workers)} FOP-Worker erfolgreich gestartet") + + def build_pdf(self, input_fo: Path, output_pdf: Path) -> tuple[bool, str]: + """ + Generiert PDF aus FO-Datei mit einem Worker aus dem Pool. + + Args: + input_fo: Pfad zur FO-Eingabedatei + output_pdf: Pfad zur PDF-Ausgabedatei + + Returns: + tuple[bool, str]: (Erfolg, Fehlermeldung/Info) + """ + # Finde freien Worker + worker_idx = None + for i, lock in enumerate(self.worker_locks): + if lock.acquire(blocking=False): + worker_idx = i + break + + if worker_idx is None: + # Kein freier Worker, warte auf ersten verfügbaren + for i, lock in enumerate(self.worker_locks): + lock.acquire() + worker_idx = i + break + + try: + worker = self.workers[worker_idx] + + # Prüfe ob Worker noch läuft + if worker.poll() is not None: + # Worker ist tot! + stderr_log = self.worker_log_dir / f"fop_worker_{worker_idx}_stderr.log" + try: + with open(stderr_log, "r") as f: + stderr_content = f.read() + error_msg = ( + f"FOP Worker {worker_idx} ist beendet (Exit: {worker.returncode})\nstderr:\n{stderr_content}" + ) + except Exception: + error_msg = f"FOP Worker {worker_idx} ist beendet (Exit: {worker.returncode})" + logger.error(error_msg) + return False, error_msg + + # Erstelle Job-String (Tab-separated) + job = f"{input_fo}\t{output_pdf}\n" + + logger.debug(f"Sende FOP-Job an Worker {worker_idx}: {input_fo.name} → {output_pdf.name}") + + # Sende Job an Worker + worker.stdin.write(job) + worker.stdin.flush() + + # Warte auf Antwort + response = worker.stdout.readline().strip() + + logger.debug(f"FOP Worker {worker_idx} Antwort: '{response}'") + + if response == "OK": + return True, "Erfolgreich" + elif response.startswith("ERROR:"): + error_msg = response[6:].strip() + return False, f"FOP-Fehler: {error_msg}" + else: + # Leere Antwort bedeutet Worker ist crashed + if not response: + stderr_log = self.worker_log_dir / f"fop_worker_{worker_idx}_stderr.log" + try: + with open(stderr_log, "r") as f: + stderr_content = f.read()[-500:] # Letzte 500 Zeichen + return False, f"FOP Worker {worker_idx} crashed (keine Antwort)\nstderr:\n{stderr_content}" + except Exception: + return False, f"FOP Worker {worker_idx} crashed (keine Antwort)" + return False, f"Unerwartete Antwort: {response}" + + except Exception as e: + logger.error(f"Fehler bei FOP Worker {worker_idx}: {e}") + return False, f"Worker-Fehler: {str(e)}" + + finally: + # Gebe Worker-Lock frei + self.worker_locks[worker_idx].release() + + def shutdown(self): + """Beendet alle Worker-Prozesse sauber.""" + logger.info("Beende FOP-Worker-Pool...") + + for i, worker in enumerate(self.workers): + try: + # Sende EXIT-Befehl + if worker.stdin and not worker.stdin.closed: + worker.stdin.write("EXIT\n") + worker.stdin.flush() + + # Warte auf Beendigung (max 2 Sekunden) + worker.wait(timeout=2) + logger.debug(f"FOP Worker {i} beendet") + + except subprocess.TimeoutExpired: + # Force kill falls nötig + worker.kill() + logger.warning(f"FOP Worker {i} musste gekillt werden") + + except Exception as e: + logger.error(f"Fehler beim Beenden von FOP Worker {i}: {e}") + + # Lösche temporäres Verzeichnis + if self.temp_dir and self.temp_dir.exists(): + try: + import shutil + + shutil.rmtree(self.temp_dir) + logger.debug(f"Temporäres Verzeichnis gelöscht: {self.temp_dir}") + except Exception as e: + logger.warning(f"Konnte temporäres Verzeichnis nicht löschen: {e}") + + logger.info("FOP-Worker-Pool beendet") + + def __enter__(self): + """Context manager entry.""" + return self + + def __exit__(self, exc_type, exc_val, exc_tb): + """Context manager exit.""" + self.shutdown() diff --git a/src/transform.py b/src/transform.py index 7143a56..e1ae286 100644 --- a/src/transform.py +++ b/src/transform.py @@ -15,12 +15,16 @@ from typing import Any, Optional, TYPE_CHECKING if TYPE_CHECKING: from saxon_pool import SaxonWorkerPool + from fop_pool import FopWorkerPool logger = logging.getLogger(__name__) # Globaler Saxon-Worker-Pool (wird von MainWindow initialisiert) _saxon_worker_pool: Optional["SaxonWorkerPool"] = None +# Globaler FOP-Worker-Pool (wird von MainWindow initialisiert) +_fop_worker_pool: Optional["FopWorkerPool"] = None + def set_saxon_worker_pool(pool: Optional["SaxonWorkerPool"]): """Setzt den globalen Saxon-Worker-Pool.""" @@ -32,6 +36,16 @@ def set_saxon_worker_pool(pool: Optional["SaxonWorkerPool"]): logger.info("Saxon-Worker-Pool deaktiviert (Fallback auf subprocess)") +def set_fop_worker_pool(pool: Optional["FopWorkerPool"]): + """Setzt den globalen FOP-Worker-Pool.""" + global _fop_worker_pool + _fop_worker_pool = pool + if pool: + logger.info(f"FOP-Worker-Pool aktiviert mit {pool.num_workers} Workern") + else: + logger.info("FOP-Worker-Pool deaktiviert (Fallback auf subprocess)") + + class TransformationJob: """ Repräsentiert einen einzelnen Transformations-Job. @@ -303,6 +317,49 @@ class TransformationJob: logger.error(error_msg) return False, error_msg + logger.info(f"Starte Apache FOP PDF-Generierung: {self.xml_file.name}") + + # Versuche zuerst den Worker-Pool zu nutzen (schneller!) + global _fop_worker_pool + if _fop_worker_pool: + try: + success, message = _fop_worker_pool.build_pdf( + input_fo=self.temp_fo, + output_pdf=self.new_pdf, + ) + + if success: + logger.info(f"FOP PDF-Generierung erfolgreich (Worker-Pool): {self.xml_file.name}") + + # Temporäre FO-Datei löschen + if self.temp_fo.exists(): + try: + self.temp_fo.unlink() + logger.debug(f"Temporäre FO-Datei gelöscht: {self.temp_fo}") + except Exception as e: + logger.warning(f"Konnte FO-Datei nicht löschen: {e}") + + # Wenn kein Ref-PDF existiert, erstelle es + if not self.ref_pdf.exists(): + try: + import shutil + + shutil.copy2(self.new_pdf, self.ref_pdf) + logger.info(f"Ref-PDF erstellt: {self.ref_pdf}") + except Exception as e: + logger.warning(f"Konnte Ref-PDF nicht erstellen: {e}") + + return True, "Erfolgreich" + else: + logger.error(f"FOP PDF-Generierung fehlgeschlagen (Worker-Pool): {message}") + return False, message + + except Exception as e: + logger.warning(f"FOP Worker-Pool-Fehler, Fallback auf subprocess: {e}") + # Fallback auf subprocess unten + + # Fallback: Traditionelle subprocess-Methode (langsamer, aber robuster) + # Apache FOP Kommandozeile cmd_line = [ str(self.fop_cmd), diff --git a/src/ui/MainWindow.py b/src/ui/MainWindow.py index 4d2e97e..2bdb43c 100644 --- a/src/ui/MainWindow.py +++ b/src/ui/MainWindow.py @@ -662,9 +662,10 @@ class MainWindow(QMainWindow): # Erstelle Aktion für Performance-Einstellungen performance_action = QAction("Performance-Einstellungen...", self) - pool_status = "aktiviert" if app_settings.use_saxon_worker_pool else "deaktiviert" + saxon_pool_status = "aktiviert" if app_settings.use_saxon_worker_pool else "deaktiviert" + fop_pool_status = "aktiviert" if app_settings.use_fop_worker_pool else "deaktiviert" performance_action.setToolTip( - f"Worker: {app_settings.max_workers} | SaxonWorkerPool: {pool_status}" + f"Worker: {app_settings.max_workers} | SaxonWorkerPool: {saxon_pool_status} | FopWorkerPool: {fop_pool_status}" ) performance_action.triggered.connect(self._open_performance_settings) @@ -675,7 +676,16 @@ class MainWindow(QMainWindow): def _open_performance_settings(self): """Öffnet einen Dialog für Performance-Einstellungen.""" - from PySide6.QtWidgets import QDialog, QVBoxLayout, QHBoxLayout, QLabel, QSpinBox, QCheckBox, QPushButton, QGroupBox + from PySide6.QtWidgets import ( + QDialog, + QVBoxLayout, + QHBoxLayout, + QLabel, + QSpinBox, + QCheckBox, + QPushButton, + QGroupBox, + ) # Erstelle benutzerdefinierten Dialog dialog = QDialog(self) @@ -727,6 +737,33 @@ class MainWindow(QMainWindow): pool_group.setLayout(pool_layout) layout.addWidget(pool_group) + # FopWorkerPool Einstellung + fop_pool_group = QGroupBox("FopWorkerPool Einstellungen") + fop_pool_layout = QVBoxLayout() + + fop_pool_checkbox = QCheckBox("FopWorkerPool verwenden (empfohlen)") + fop_pool_checkbox.setChecked(app_settings.use_fop_worker_pool) + fop_pool_checkbox.setToolTip( + "Aktiviert persistente JVM-Prozesse für Apache FOP PDF-Generierung.\n" + "Vorteile: Bis zu 10x schneller durch Eliminierung von JVM-Startup-Overhead\n" + "Nachteile: Benötigt JDK (javac) - funktioniert nicht mit JRE allein\n\n" + "Deaktivieren Sie diese Option, wenn:\n" + "• Sie nur ein JRE (keine JDK) installiert haben\n" + "• Sie Probleme mit dem Worker-Pool haben\n" + "• Sie die Funktion testen möchten" + ) + + fop_pool_info = QLabel( + "Hinweis: FopWorkerPool benötigt ein JDK (Java Development Kit).
" + "Mit JRE allein werden PDFs im Fallback-Modus generiert.
" + ) + fop_pool_info.setWordWrap(True) + + fop_pool_layout.addWidget(fop_pool_checkbox) + fop_pool_layout.addWidget(fop_pool_info) + fop_pool_group.setLayout(fop_pool_layout) + layout.addWidget(fop_pool_group) + # OK/Abbrechen Buttons button_layout = QHBoxLayout() ok_button = QPushButton("OK") @@ -747,9 +784,11 @@ class MainWindow(QMainWindow): # Speichere Änderungen current_workers = app_settings.max_workers current_use_pool = app_settings.use_saxon_worker_pool + current_use_fop_pool = app_settings.use_fop_worker_pool new_workers = worker_spinbox.value() new_use_pool = pool_checkbox.isChecked() + new_use_fop_pool = fop_pool_checkbox.isChecked() changes = [] @@ -764,20 +803,51 @@ class MainWindow(QMainWindow): changes.append(f"SaxonWorkerPool: {status}") logger.info(f"use_saxon_worker_pool geändert: {current_use_pool} → {new_use_pool}") + if new_use_fop_pool != current_use_fop_pool: + app_settings.use_fop_worker_pool = new_use_fop_pool + status = "aktiviert" if new_use_fop_pool else "deaktiviert" + changes.append(f"FopWorkerPool: {status}") + logger.info(f"use_fop_worker_pool geändert: {current_use_fop_pool} → {new_use_fop_pool}") + if changes: + # WICHTIG: Speichere Settings BEVOR wir Pools neu initialisieren app_settings.save() + logger.info(f"Performance-Einstellungen gespeichert: {changes}") + + # Initialisiere Worker Pools neu falls sich relevante Einstellungen geändert haben + pools_reinitialized = False + if self.project: + # Saxon Worker Pool neu initialisieren? + if new_use_pool != current_use_pool or new_workers != current_workers: + logger.info( + f"Saxon Worker Pool wird neu initialisiert " + f"(use_pool: {current_use_pool}→{new_use_pool}, workers: {current_workers}→{new_workers})" + ) + self._initialize_saxon_worker_pool() + pools_reinitialized = True + + # FOP Worker Pool neu initialisieren? + if new_use_fop_pool != current_use_fop_pool or new_workers != current_workers: + logger.info( + f"FOP Worker Pool wird neu initialisiert " + f"(use_pool: {current_use_fop_pool}→{new_use_fop_pool}, workers: {current_workers}→{new_workers})" + ) + self._initialize_fop_worker_pool() + pools_reinitialized = True + else: + logger.warning("Kein Projekt geladen - Worker Pools werden nicht neu initialisiert") # Informiere Benutzer über Änderungen changes_text = "\n".join(f"• {change}" for change in changes) - restart_hint = "" + status_hint = "" - if new_use_pool != current_use_pool and self.project: - restart_hint = "\n\nHinweis: Bitte öffnen Sie das Projekt neu, damit die Änderung wirksam wird." + if pools_reinitialized: + status_hint = "\n\nDie Worker Pools wurden automatisch mit den neuen Einstellungen neu gestartet." QMessageBox.information( self, "Einstellungen gespeichert", - f"Folgende Einstellungen wurden geändert:\n\n{changes_text}{restart_hint}", + f"Folgende Einstellungen wurden geändert:\n\n{changes_text}{status_hint}", ) def open_existing_project(self, project: Project): @@ -818,6 +888,9 @@ class MainWindow(QMainWindow): # Initialisiere Saxon-Worker-Pool für schnellere Transformationen self._initialize_saxon_worker_pool() + # Initialisiere FOP-Worker-Pool für schnellere PDF-Generierung + self._initialize_fop_worker_pool() + except Exception as e: logger.error(f"Fehler beim Laden des Projekts '{project.name}': {e}") # Fallback: Erstelle Standard-Einstellungen @@ -891,6 +964,84 @@ class MainWindow(QMainWindow): except Exception as e: logger.error(f"Fehler beim Beenden des Saxon-Worker-Pools: {e}") + def _initialize_fop_worker_pool(self): + """Initialisiert den FOP-Worker-Pool für schnelle PDF-Generierung.""" + try: + # Shutdown vorherigen Pool falls vorhanden + self._shutdown_fop_worker_pool() + + # Prüfe ob FopWorkerPool aktiviert ist + if not app_settings.use_fop_worker_pool: + logger.info("FopWorkerPool deaktiviert - Verwende Fallback-Modus (subprocess)") + return + + if not self.project: + logger.warning("Kein Projekt geladen, FOP-Worker-Pool nicht initialisiert") + return + + # Hole Tool-Konfigurationen + java_vm = next((vm for vm in app_settings.java_vms if vm.id == self.project.java_vm_id), None) + apache_fop = next((fop for fop in app_settings.apache_fops if fop.id == self.project.apache_fop_id), None) + + if not java_vm or not apache_fop: + logger.warning("Java VM oder Apache FOP nicht gefunden, Pool nicht initialisiert") + return + + # FOP-Konfigurationsdatei (falls vorhanden) + fop_config_file = None + if self.project.fop_config_dir: + fop_config_file = self.project.fop_config_dir / "fop.xconf" + else: + default_config = apache_fop.path_to_dir / "conf" / "fop.xconf" + if default_config.exists(): + fop_config_file = default_config + + # Importiere FopWorkerPool + from fop_pool import FopWorkerPool + from transform import set_fop_worker_pool + + # Erstelle Worker-Pool + num_workers = app_settings.max_workers + log_dir = self.project.project_dir / "temp" + pool = FopWorkerPool( + num_workers=num_workers, + java_vm_path=java_vm.path_to_binary_file, + apache_fop_dir=apache_fop.path_to_dir, + fop_config_file=fop_config_file, + log_dir=log_dir, + ) + + # Setze globalen Pool + set_fop_worker_pool(pool) + + logger.info( + f"FOP-Worker-Pool initialisiert: {num_workers} Worker " + f"(erwartet: {num_workers}x schneller für PDF-Generierung)" + ) + + except Exception as e: + logger.error(f"Fehler beim Initialisieren des FOP-Worker-Pools: {e}") + logger.info("Fallback auf subprocess-Modus") + # Kein Pool ist OK - Fallback auf subprocess + + def _shutdown_fop_worker_pool(self): + """Beendet den FOP-Worker-Pool sauber.""" + try: + # Importiere transform um Zugriff auf globalen Pool zu haben + import transform + + if transform._fop_worker_pool: + logger.info("Beende FOP-Worker-Pool...") + transform._fop_worker_pool.shutdown() + # Importiere set_fop_worker_pool + from transform import set_fop_worker_pool + + set_fop_worker_pool(None) + logger.info("FOP-Worker-Pool beendet") + + except Exception as e: + logger.error(f"Fehler beim Beenden des FOP-Worker-Pools: {e}") + def change_theme(self, theme_name): """ Wechselt das Theme der Anwendung. @@ -4796,9 +4947,7 @@ class MainWindow(QMainWindow): 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}" - ) + 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): @@ -4884,6 +5033,9 @@ class MainWindow(QMainWindow): # Beende Saxon-Worker-Pool self._shutdown_saxon_worker_pool() + # Beende FOP-Worker-Pool + self._shutdown_fop_worker_pool() + # PDF-Dokumente schließen ist bei QtPdf automatisch durch Garbage Collection super().closeEvent(event)