Files
Module.HallOfFame/QuestPDF_Integration.md

10 KiB
Raw Blame History

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 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 (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.Community bekommt 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 laden
  • IWebHostEnvironment 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 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

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

Ergebnis

PDF Vorschau