Files
Module.HallOfFame/QuestPDF_Integration.md

311 lines
10 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 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
<PackageReference Include="QuestPDF" Version="2026.2.1" />
```
---
## 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 `</Project>` eingefügt:
```xml
<!-- Copy QuestPDF DLLs to Oqtane.Server so they are available at runtime -->
<Target Name="CopyQuestPdfToOqtane" AfterTargets="Build">
<!-- 1. Die Haupt-DLL kopieren -->
<Copy SourceFiles="$(OutputPath)QuestPDF.dll"
DestinationFolder="$(ProjectDir)..\..\oqtane.framework\Oqtane.Server\bin\$(Configuration)\$(TargetFramework)\"
SkipUnchangedFiles="true" />
<!-- 2. Die nativen SkiaSharp-Runtimes kopieren (für Mac, Windows, Linux) -->
<ItemGroup>
<QuestPdfNativeFiles Include="$(OutputPath)runtimes\**\*.*" />
</ItemGroup>
<Copy SourceFiles="@(QuestPdfNativeFiles)"
DestinationFiles="@(QuestPdfNativeFiles->'$(ProjectDir)..\..\oqtane.framework\Oqtane.Server\bin\$(Configuration)\$(TargetFramework)\runtimes\%(RecursiveDir)%(Filename)%(Extension)')"
SkipUnchangedFiles="true" />
</Target>
```
> [!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<IHallOfFameService, ServerHallOfFameService>();
}
```
> [!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<IActionResult> 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
<button class="btn btn-primary" @onclick="ShowPdfPreview">
<i class="oi oi-eye me-2"></i> PDF Vorschau
</button>
```
### 3b) Modal mit iframe
Das PDF wird in einem `<iframe>` direkt im Browser angezeigt:
```html
@if (_showPdfModal)
{
<div class="modal" style="display:block">
<div class="modal-dialog modal-xl">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title">PDF Vorschau</h5>
<button class="btn-close" @onclick="ClosePdfPreview"></button>
</div>
<div class="modal-body">
<iframe src="@_pdfPreviewUrl"
style="width:100%; height:100%; border:none;">
</iframe>
</div>
<div class="modal-footer">
<button class="btn btn-secondary" @onclick="ClosePdfPreview">Schließen</button>
<button class="btn btn-primary" @onclick="DownloadPdf">
<i class="oi oi-data-transfer-download me-2"></i> Herunterladen
</button>
</div>
</div>
</div>
</div>
}
```
### 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)