# 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 `