# QuestPDF in Oqtane integrieren – Schritt für Schritt ## Überblick Um PDF-Generierung in ein Oqtane-Modul zu integrieren, braucht man **5 Schritte**: ```mermaid graph TD A["1. NuGet installieren"] --> B["2. .csproj: DLL Copy Target"] B --> C["3. ServerStartup.cs: Lizenz"] C --> D["4. Controller erstellen"] D --> E["5. Razor-Seite: Vorschau"] ``` --- ## Schritt 1: QuestPDF installieren Im Terminal, im **Server-Projekt**-Ordner: ```bash cd SZUAbsolventenverein.Module.HallOfFame/Server dotnet add package QuestPDF ``` Das fügt automatisch folgende Zeile in die [Server.csproj](file:///Users/adamgaiswinkler/SZUAbsolventenverein.Module.HallOfFame/Server/SZUAbsolventenverein.Module.HallOfFame.Server.csproj) ein: ```xml ``` --- ## Schritt 2: DLL-Copy Target in der .csproj **Datei:** [Server.csproj](file:///Users/adamgaiswinkler/SZUAbsolventenverein.Module.HallOfFame/Server/SZUAbsolventenverein.Module.HallOfFame.Server.csproj) Oqtane lädt Module dynamisch – deshalb muss die QuestPDF.dll **und deren native SkiaSharp-Libraries** nach dem Build ins Oqtane-Server-Verzeichnis kopiert werden. Dieses MSBuild-Target wird **am Ende der .csproj** vor `` eingefügt: ```xml ``` > [!IMPORTANT] > Ohne diesen Copy-Step bekommt man zur Laufzeit einen `FileNotFoundException` weil Oqtane die QuestPDF.dll nicht findet! ### Was wird wohin kopiert? ``` Server/bin/Debug/net9.0/ ├── QuestPDF.dll ──→ oqtane.framework/Oqtane.Server/bin/Debug/net9.0/ └── runtimes/ ├── osx/native/libSkiaSharp.dylib ──→ .../runtimes/osx/native/ ├── win-x64/native/libSkiaSharp.dll ──→ .../runtimes/win-x64/native/ └── linux-x64/native/libSkiaSharp.so ──→ .../runtimes/linux-x64/native/ ``` --- ## Schritt 3: QuestPDF-Lizenz in ServerStartup.cs konfigurieren **Datei:** [ServerStartup.cs](file:///Users/adamgaiswinkler/SZUAbsolventenverein.Module.HallOfFame/Server/Startup/ServerStartup.cs) *(existierende Datei im Server/Startup Ordner)* QuestPDF verlangt, dass man die Lizenz setzt, bevor man PDFs generiert. Das passiert in der `ConfigureServices` Methode: ```csharp public void ConfigureServices(IServiceCollection services) { // QuestPDF Lizenz konfigurieren (MUSS vor dem ersten PDF-Aufruf stehen!) QuestPDF.Settings.License = QuestPDF.Infrastructure.LicenseType.Community; // ... andere Services services.AddTransient(); } ``` > [!CAUTION] > Ohne `QuestPDF.Settings.License = LicenseType.Community` bekommt man eine Exception beim Generieren des ersten PDFs! --- ## Schritt 4: PDF-Controller erstellen **Datei:** [HallOfFamePDFController.cs](file:///Users/adamgaiswinkler/SZUAbsolventenverein.Module.HallOfFame/Server/Controllers/HallOfFamePDFController.cs) *(neue Datei im Server/Controllers Ordner)* ### 2a) Usings und Klasse anlegen ```csharp using QuestPDF.Fluent; // Document.Create, .Text(), .Image() etc. using QuestPDF.Helpers; // PageSizes.A4, Colors using QuestPDF.Infrastructure; // IContainer, IDocument ``` Der Controller erbt von `ModuleControllerBase` (Oqtane) und braucht: - `IHallOfFameService` – Einträge aus der DB laden - `IWebHostEnvironment` – Bildpfade auflösen (`WebRootPath`) ```csharp [Route(ControllerRoutes.ApiRoute)] public class HallOfFamePdfController : ModuleControllerBase { private readonly IHallOfFameService _hallOfFameService; private readonly IWebHostEnvironment _environment; public HallOfFamePdfController( IHallOfFameService hallOfFameService, ILogManager logger, IHttpContextAccessor accessor, IWebHostEnvironment environment) : base(logger, accessor) { _hallOfFameService = hallOfFameService; _environment = environment; } } ``` ### 2b) GET-Endpoint für PDF ```csharp // GET: api/HallOfFamePdf?moduleid=40&download=true|false [HttpGet] [Authorize(Policy = PolicyNames.ViewModule)] public async Task Get(string moduleid, bool download = false) ``` - `download=false` → PDF wird inline angezeigt (für iframe-Vorschau) - `download=true` → PDF wird als Datei heruntergeladen ### 2c) Bilder laden Die Bilder liegen im `wwwroot` Ordner. Der Pfad wird mit `IWebHostEnvironment` aufgelöst: ```csharp byte[] imageBytes = null; if (!string.IsNullOrEmpty(entry.Image)) { var fullImagePath = System.IO.Path.Combine( _environment.WebRootPath, entry.Image.TrimStart('/')); if (System.IO.File.Exists(fullImagePath)) imageBytes = System.IO.File.ReadAllBytes(fullImagePath); } ``` ### 2d) PDF-Dokument mit Layers API aufbauen So funktioniert das Hintergrundbild mit Text darüber: ```csharp var document = Document.Create(container => { container.Page(page => { page.Size(PageSizes.A4); page.Margin(0); // kein Rand = Bild füllt die ganze Seite page.Content().Layers(layers => { // 1. HINTERGRUND-Layer (VOR PrimaryLayer = wird dahinter gezeichnet) layers.Layer().Image(imageBytes).FitUnproportionally(); // 2. TEXT-Layer (PrimaryLayer = bestimmt die Seitengröße) layers.PrimaryLayer().Column(column => { // Oben: Name-Box column.Item() .Background("#AA9E9E9E") // halbtransparentes Grau .Padding(20) .Text(entry.Name).FontSize(28).Bold().FontColor(Colors.White); // Mitte: Freiraum (Bild sichtbar) // Unten: Beschreibung column.Item().ExtendVertical().AlignBottom() .Background("#CC333333") // dunkles Overlay .Padding(25) .Text(description).FontSize(11).FontColor(Colors.White); }); }); }); }); // PDF als Byte-Array generieren byte[] pdfBytes = document.GeneratePdf(); return File(pdfBytes, "application/pdf"); ``` > [!TIP] > **Layers-Reihenfolge:** Layer VOR `PrimaryLayer` = Hintergrund. Layer NACH `PrimaryLayer` = Vordergrund/Wasserzeichen. ### 2e) Farben mit Transparenz QuestPDF unterstützt Hex-Farben mit Alpha-Kanal (ARGB): | Farbe | Code | Bedeutung | |---|---|---| | `#AA9E9E9E` | `AA` = 67% sichtbar, `9E9E9E` = Grau | Halbtransparentes Grau | | `#CC333333` | `CC` = 80% sichtbar, `333333` = Dunkelgrau | Dunkles Overlay | | `#FF000000` | Voll deckend Schwarz | Kein Durchschein | | `#00000000` | Komplett transparent | Unsichtbar | --- ## Schritt 5: Client-Seite (Blazor Razor) **Datei:** [Details.razor](file:///Users/adamgaiswinkler/SZUAbsolventenverein.Module.HallOfFame/Client/Modules/SZUAbsolventenverein.Module.HallOfFame/Details.razor) ### 3a) Buttons für Vorschau und Download ```html ``` ### 3b) Modal mit iframe Das PDF wird in einem ` } ``` ### 3c) C#-Methoden im @code Block ```csharp private bool _showPdfModal = false; private string _pdfPreviewUrl = ""; private void ShowPdfPreview() { // Vorschau: download=false → PDF wird inline gerendert _pdfPreviewUrl = $"/api/HallOfFamePdf?moduleid={ModuleState.ModuleId}"; _showPdfModal = true; } private void ClosePdfPreview() { _showPdfModal = false; _pdfPreviewUrl = ""; } private async Task DownloadPdf() { // Download: download=true → Browser lädt die Datei herunter var url = $"/api/HallOfFamePdf?moduleid={ModuleState.ModuleId}&download=true"; await JSRuntime.InvokeVoidAsync("eval", $"var a = document.createElement('a'); a.href = '{url}'; " + $"a.download = 'HallOfFame.pdf'; document.body.appendChild(a); " + $"a.click(); document.body.removeChild(a);"); } ``` --- ## Bauen und Testen ```bash # 1. Modul bauen (kopiert automatisch die QuestPDF DLLs) dotnet build SZUAbsolventenverein.Module.HallOfFame.sln # 2. Oqtane Server starten cd oqtane.framework dotnet run --project Oqtane.Server/Oqtane.Server.csproj # 3. Im Browser testen # http://localhost:44357 → Hall of Fame → Details ansehen → PDF Vorschau ``` --- ## Ergebnis ![PDF Vorschau](/Users/adamgaiswinkler/.gemini/antigravity/brain/fab653f4-cba7-48ce-b97b-6096f9060c6a/final_pdf_preview_check_1771447412799.png)