311 lines
10 KiB
Markdown
311 lines
10 KiB
Markdown
# 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
|
||
|
||

|