10 KiB
QuestPDF in Oqtane integrieren – Schritt für Schritt
Überblick
Um PDF-Generierung in ein Oqtane-Modul zu integrieren, braucht man 5 Schritte:
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:
cd SZUAbsolventenverein.Module.HallOfFame/Server
dotnet add package QuestPDF
Das fügt automatisch folgende Zeile in die Server.csproj ein:
<PackageReference Include="QuestPDF" Version="2026.2.1" />
Schritt 2: DLL-Copy Target in der .csproj
Datei: 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:
<!-- 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
FileNotFoundExceptionweil 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 (existierende Datei im Server/Startup Ordner)
QuestPDF verlangt, dass man die Lizenz setzt, bevor man PDFs generiert. Das passiert in der ConfigureServices Methode:
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.Communitybekommt man eine Exception beim Generieren des ersten PDFs!
Schritt 4: PDF-Controller erstellen
Datei: HallOfFamePDFController.cs (neue Datei im Server/Controllers Ordner)
2a) Usings und Klasse anlegen
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 ladenIWebHostEnvironment– Bildpfade auflösen (WebRootPath)
[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
// 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:
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:
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 NACHPrimaryLayer= 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
3a) Buttons für Vorschau und Download
<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:
@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
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
# 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
