feat: implementiert PDF-Generierung mit Hintergrundbild und Dokumentation

This commit is contained in:
Adam Gaiswinkler
2026-02-18 22:43:26 +01:00
parent 1bff5ebbbd
commit e7ee313472
7 changed files with 538 additions and 11 deletions

View File

@@ -0,0 +1,144 @@
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Hosting;
using Oqtane.Shared;
using Oqtane.Enums;
using Oqtane.Infrastructure;
using Oqtane.Controllers;
using SZUAbsolventenverein.Module.HallOfFame.Services;
using System.Linq;
using System.Threading.Tasks;
using QuestPDF.Fluent;
using QuestPDF.Helpers;
using QuestPDF.Infrastructure;
namespace SZUAbsolventenverein.Module.HallOfFame.Controllers
{
[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;
}
// GET: api/<controller>?moduleid=x&download=true/false
[HttpGet]
[Authorize(Policy = PolicyNames.ViewModule)]
public async Task<IActionResult> Get(string moduleid, bool download = false)
{
int ModuleId;
if (int.TryParse(moduleid, out ModuleId) && IsAuthorizedEntityId(EntityNames.Module, ModuleId))
{
var entries = await _hallOfFameService.GetHallOfFamesAsync(ModuleId);
var publishedEntries = entries.Where(e => e.Status == "Published").ToList();
var document = Document.Create(container =>
{
foreach (var entry in publishedEntries)
{
// Bild laden falls vorhanden
byte[] imageBytes = null;
if (!string.IsNullOrEmpty(entry.Image))
{
try
{
var fullImagePath = System.IO.Path.Combine(
_environment.WebRootPath, entry.Image.TrimStart('/'));
if (System.IO.File.Exists(fullImagePath))
imageBytes = System.IO.File.ReadAllBytes(fullImagePath);
}
catch { }
}
container.Page(page =>
{
page.Size(PageSizes.A4);
page.Margin(0);
page.Content().Layers(layers =>
{
// Hintergrund-Layer: Bild als Vollbild
if (imageBytes != null)
{
layers.Layer().Image(imageBytes).FitUnproportionally();
}
else
{
layers.Layer().Background(Colors.Grey.Darken3);
}
// Inhalt-Layer (PrimaryLayer bestimmt die Größe)
layers.PrimaryLayer()
.Column(column =>
{
// === OBEN: Name und Jahr in halbtransparenter grauer Box ===
column.Item()
.Background("#AA9E9E9E") // halbtransparentes Grau
.Padding(20)
.Row(row =>
{
row.RelativeItem().Column(inner =>
{
inner.Item().Text(entry.Name)
.FontSize(28).Bold().FontColor(Colors.White);
inner.Item().Text($"Jahrgang {entry.Year}")
.FontSize(14).FontColor(Colors.White);
});
});
// === UNTEN: Beschreibung am Seitenende ===
var description = entry.Description ?? "";
var sections = description.Split('\n')
.Select(s => s.Trim())
.Where(s => !string.IsNullOrEmpty(s))
.ToArray();
column.Item().ExtendVertical().AlignBottom()
.Background("#CC333333") // dunkles halbtransparentes Overlay
.Padding(25)
.Column(descColumn =>
{
if (sections.Length > 0)
{
descColumn.Item().PaddingBottom(10)
.Text("Beschreibung:")
.FontSize(16).Bold().FontColor(Colors.White);
foreach (var line in sections)
{
descColumn.Item().PaddingBottom(6)
.Text(line)
.FontSize(11).FontColor(Colors.White)
.LineHeight(1.4f);
}
}
});
});
});
});
}
});
byte[] pdfBytes = document.GeneratePdf();
if (download)
{
return File(pdfBytes, "application/pdf", "HallOfFame.pdf");
}
// Inline: PDF wird im Browser angezeigt (Vorschau)
return File(pdfBytes, "application/pdf");
}
else
{
_logger.Log(LogLevel.Error, this, LogFunction.Security, "Unauthorized HallOfFame PDF Get Attempt {ModuleId}", moduleid);
return Forbid();
}
}
}
}

View File

@@ -23,6 +23,7 @@
<PackageReference Include="Microsoft.AspNetCore.Identity.EntityFrameworkCore" Version="9.0.8" />
<PackageReference Include="Microsoft.EntityFrameworkCore" Version="9.0.8" />
<PackageReference Include="Microsoft.Extensions.Localization" Version="9.0.8" />
<PackageReference Include="QuestPDF" Version="2026.2.1" />
</ItemGroup>
<ItemGroup>
@@ -34,4 +35,13 @@
<Reference Include="Oqtane.Server"><HintPath>..\..\oqtane.framework\Oqtane.Server\bin\Debug\net9.0\Oqtane.Server.dll</HintPath></Reference>
<Reference Include="Oqtane.Shared"><HintPath>..\..\oqtane.framework\Oqtane.Server\bin\Debug\net9.0\Oqtane.Shared.dll</HintPath></Reference>
</ItemGroup>
<!-- Copy QuestPDF DLLs to Oqtane.Server so they are available at runtime -->
<Target Name="CopyQuestPdfToOqtane" AfterTargets="Build">
<Copy SourceFiles="$(OutputPath)QuestPDF.dll" DestinationFolder="$(ProjectDir)..\..\oqtane.framework\Oqtane.Server\bin\$(Configuration)\$(TargetFramework)\" SkipUnchangedFiles="true" />
<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>
</Project>

View File

@@ -1,4 +1,4 @@
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.Extensions.DependencyInjection;
using Oqtane.Infrastructure;
@@ -21,6 +21,9 @@ namespace SZUAbsolventenverein.Module.HallOfFame.Startup
public void ConfigureServices(IServiceCollection services)
{
// QuestPDF Lizenz konfigurieren
QuestPDF.Settings.License = QuestPDF.Infrastructure.LicenseType.Community;
services.AddTransient<IHallOfFameService, ServerHallOfFameService>();
services.AddDbContextFactory<HallOfFameContext>(opt => { }, ServiceLifetime.Transient);
}