< Summary

Information
Class: LifeChart.Infrastructure.Pdf.PdfRenderer
Assembly: LifeChart.Infrastructure
File(s): /home/runner/work/lifechart/lifechart/src/LifeChart.Infrastructure/Pdf/PdfSharpRenderer.cs
Tag: 113_29159449707
Line coverage
0%
Covered lines: 0
Uncovered lines: 264
Coverable lines: 264
Total lines: 466
Line coverage: 0%
Branch coverage
0%
Covered branches: 0
Total branches: 62
Branch coverage: 0%
Method coverage

Feature is only available for sponsors

Upgrade to PRO version

Metrics

MethodBranch coverage Crap Score Cyclomatic complexity Line coverage
.cctor()100%210%
RenderAsync(...)100%210%
Fill(...)100%210%
Stroke(...)100%210%
DrawText(...)0%2040%
DrawHeader(...)100%210%
DrawChart(...)0%7280%
DrawSeries(...)0%7280%
DrawXDates(...)0%4260%
DrawLegend(...)100%210%
DrawDailyTable(...)0%210140%
DrawTableRow(...)0%2040%
DrawNotes(...)0%2040%
DrawMedications(...)0%4260%
DrawFooter(...)0%620%
ValueToY(...)100%210%
PointToX(...)100%210%
.ctor(...)100%210%
get_Canvas()100%210%
get_Y()100%210%
get_AllPages()100%210%
EnsureSpace(...)0%620%
AddPage()0%620%
Close()0%620%

File(s)

/home/runner/work/lifechart/lifechart/src/LifeChart.Infrastructure/Pdf/PdfSharpRenderer.cs

#LineLine coverage
 1using LifeChart.Application.DTOs;
 2using LifeChart.Application.Interfaces;
 3using SkiaSharp;
 4
 5namespace LifeChart.Infrastructure.Pdf;
 6
 7public class PdfRenderer : IPdfRenderer
 8{
 9    private const float PageW = 595.28f;
 10    private const float PageH = 841.89f;
 11    private const float Margin = 40f;
 12    private const float ContentW = PageW - 2 * Margin;
 13    private const float FooterH = 30f;
 14    private const float ContentBottom = PageH - Margin - FooterH;
 15    private const float ChartH = 220f;
 16
 017    private static readonly SKColor MoodColor = new(0, 114, 178);
 018    private static readonly SKColor FuncColor = new(230, 159, 0);
 019    private static readonly SKColor TextPrimary = new(26, 26, 26);
 020    private static readonly SKColor TextSecondary = new(107, 107, 107);
 021    private static readonly SKColor GridColor = new(200, 200, 200);
 022    private static readonly SKColor BandMania = new(255, 230, 0, 30);
 023    private static readonly SKColor BandDepression = new(0, 114, 178, 30);
 24
 025    private static readonly SKTypeface Arial =
 026        SKTypeface.FromFamilyName("Arial", SKFontStyleWeight.Normal, SKFontStyleWidth.Normal, SKFontStyleSlant.Upright);
 27
 028    private static readonly SKTypeface ArialBold =
 029        SKTypeface.FromFamilyName("Arial", SKFontStyleWeight.Bold, SKFontStyleWidth.Normal, SKFontStyleSlant.Upright);
 30
 31    public Task<byte[]> RenderAsync(PdfReportData data)
 32    {
 033        using var stream = new MemoryStream();
 034        using var wstream = new SKManagedWStream(stream);
 035        using var document = SKDocument.CreatePdf(wstream);
 36
 037        var ctx = new PageContext(document);
 038        DrawHeader(ctx, data);
 039        DrawChart(ctx, data.ChartData);
 040        DrawLegend(ctx);
 041        DrawDailyTable(ctx, data.Entries);
 042        DrawNotes(ctx, data.Entries);
 043        DrawMedications(ctx, data.ActiveMedications);
 044        DrawFooter(ctx);
 045        ctx.Close();
 46
 047        return Task.FromResult(stream.ToArray());
 048    }
 49
 50    private static SKPaint Fill(SKColor color)
 051        => new() { Color = color, Style = SKPaintStyle.Fill, IsAntialias = true };
 52
 53    private static SKPaint Stroke(SKColor color, float strokeWidth)
 054        => new() { Color = color, StrokeWidth = strokeWidth, Style = SKPaintStyle.Stroke, IsAntialias = true };
 55
 56    private static void DrawText(SKCanvas canvas, string text, SKRect rect, SKFont font, SKPaint paint,
 57        bool centerX = false, bool centerY = false)
 58    {
 059        font.GetFontMetrics(out var metrics);
 060        var textWidth = font.MeasureText(text);
 061        var textHeight = metrics.Descent - metrics.Ascent;
 62
 063        var x = centerX ? rect.MidX - textWidth / 2 : rect.Left;
 064        var y = centerY ? rect.MidY - textHeight / 2 : rect.Top;
 65
 066        canvas.DrawText(text, x, y - metrics.Ascent, font, paint);
 067    }
 68
 69    // ── Header ──────────────────────────────────────────────────────────────
 70
 71    private void DrawHeader(PageContext ctx, PdfReportData data)
 72    {
 073        var canvas = ctx.Canvas;
 74
 075        using var titleFont = new SKFont(ArialBold, 14);
 076        using var titlePaint = Fill(TextPrimary);
 077        DrawText(canvas, "LifeChart — Mood & Functionality Report",
 078            new SKRect(Margin, ctx.Y, Margin + ContentW, ctx.Y + 20), titleFont, titlePaint);
 079        ctx.Y += 22;
 80
 081        var periodLabel = $"{data.From:dd.MM.yyyy} – {data.To:dd.MM.yyyy}";
 082        using var subFont = new SKFont(Arial, 9);
 083        using var subPaint = Fill(TextSecondary);
 084        DrawText(canvas, periodLabel,
 085            new SKRect(Margin, ctx.Y, Margin + ContentW, ctx.Y + 14), subFont, subPaint);
 086        ctx.Y += 18;
 87
 088        using var linePaint = Stroke(GridColor, 0.5f);
 089        canvas.DrawLine(Margin, ctx.Y, PageW - Margin, ctx.Y, linePaint);
 090        ctx.Y += 10;
 091    }
 92
 93    // ── Chart ───────────────────────────────────────────────────────────────
 94
 95    private void DrawChart(PageContext ctx, ChartDataDto chartData)
 96    {
 097        ctx.EnsureSpace(ChartH + 20);
 098        var canvas = ctx.Canvas;
 99
 0100        var chartX = Margin + 30;
 0101        var chartY = ctx.Y;
 0102        var chartW = ContentW - 30;
 103
 0104        using var maniaFill = Fill(BandMania);
 0105        canvas.DrawRect(chartX, chartY, chartW, ChartH / 2, maniaFill);
 106
 0107        using var depFill = Fill(BandDepression);
 0108        canvas.DrawRect(chartX, chartY + ChartH / 2, chartW, ChartH / 2, depFill);
 109
 0110        using var gridFont = new SKFont(Arial, 7);
 0111        using var gridPaint = Fill(TextSecondary);
 0112        for (var v = -5; v <= 5; v++)
 113        {
 0114            var y = ValueToY(v, chartY);
 0115            var penColor = v == 0
 0116                ? new SKColor(0, 0, 0, 80)
 0117                : new SKColor(0, 0, 0, 40);
 0118            using var pen = Stroke(penColor, v == 0 ? 0.7f : 0.4f);
 0119            canvas.DrawLine(chartX, y, chartX + chartW, y, pen);
 120
 0121            DrawText(canvas, v.ToString(),
 0122                new SKRect(Margin, y - 5, Margin + 25, y + 5), gridFont, gridPaint);
 123        }
 124
 0125        using var borderPen = Stroke(GridColor, 0.5f);
 0126        canvas.DrawRect(chartX, chartY, chartW, ChartH, borderPen);
 127
 0128        if (chartData.Points.Count == 0)
 129        {
 0130            using var noDataFont = new SKFont(Arial, 10);
 0131            DrawText(canvas, "Keine Daten für diesen Zeitraum.",
 0132                new SKRect(chartX, chartY, chartX + chartW, chartY + ChartH), noDataFont, gridPaint,
 0133                centerX: true, centerY: true);
 0134            ctx.Y += ChartH + 8;
 0135            return;
 136        }
 137
 0138        var n = chartData.Points.Count;
 139
 0140        DrawSeries(canvas, chartData.Points, n, chartX, chartY, chartW,
 0141            p => p.Mood, MoodColor, dashStyle: false);
 0142        DrawSeries(canvas, chartData.Points, n, chartX, chartY, chartW,
 0143            p => p.Functionality, FuncColor, dashStyle: true);
 144
 0145        DrawXDates(canvas, chartData.Points, n, chartX, chartY, chartW, gridFont, gridPaint);
 146
 0147        ctx.Y += ChartH + 8;
 0148    }
 149
 150    private static void DrawSeries(
 151        SKCanvas canvas,
 152        IReadOnlyList<ChartPointDto> points,
 153        int n,
 154        float chartX, float chartY, float chartW,
 155        Func<ChartPointDto, int> valueSelector,
 156        SKColor color,
 157        bool dashStyle)
 158    {
 0159        using var pen = Stroke(color, 1.5f);
 0160        if (dashStyle)
 0161            pen.PathEffect = SKPathEffect.CreateDash([5, 5], 0);
 162
 0163        for (var i = 1; i < n; i++)
 164        {
 0165            var x1 = PointToX(i - 1, n, chartX, chartW);
 0166            var y1 = ValueToY(valueSelector(points[i - 1]), chartY);
 0167            var x2 = PointToX(i, n, chartX, chartW);
 0168            var y2 = ValueToY(valueSelector(points[i]), chartY);
 0169            canvas.DrawLine(x1, y1, x2, y2, pen);
 170        }
 171
 0172        using var brush = Fill(color);
 173        const float r = 2.5f;
 0174        for (var i = 0; i < n; i++)
 175        {
 0176            var x = PointToX(i, n, chartX, chartW);
 0177            var y = ValueToY(valueSelector(points[i]), chartY);
 178
 0179            if (dashStyle)
 0180                canvas.DrawRect(x - r, y - r, r * 2, r * 2, brush);
 181            else
 0182                canvas.DrawCircle(x, y, r, brush);
 183        }
 0184    }
 185
 186    private static void DrawXDates(
 187        SKCanvas canvas,
 188        IReadOnlyList<ChartPointDto> points,
 189        int n,
 190        float chartX, float chartY, float chartW,
 191        SKFont font, SKPaint paint)
 192    {
 0193        var step = Math.Max(1, n / 9);
 194
 0195        for (var i = 0; i < n; i++)
 196        {
 0197            if (i % step != 0 && i != n - 1) continue;
 0198            var x = PointToX(i, n, chartX, chartW);
 0199            var label = points[i].Date.ToString("dd.MM");
 0200            DrawText(canvas, label,
 0201                new SKRect(x - 15, chartY + ChartH + 2, x + 15, chartY + ChartH + 12), font, paint,
 0202                centerX: true);
 203        }
 0204    }
 205
 206    // ── Legende ─────────────────────────────────────────────────────────────
 207
 208    private void DrawLegend(PageContext ctx)
 209    {
 0210        var canvas = ctx.Canvas;
 0211        var y = ctx.Y;
 212
 0213        using var moodFill = Fill(MoodColor);
 0214        canvas.DrawRect(Margin + 30, y, 20, 4, moodFill);
 215
 0216        using var legendFont = new SKFont(Arial, 8);
 0217        using var legendPaint = Fill(TextPrimary);
 0218        DrawText(canvas, "Stimmung",
 0219            new SKRect(Margin + 56, y - 2, Margin + 136, y + 10), legendFont, legendPaint);
 220
 0221        using var dashedPen = Stroke(FuncColor, 1.5f);
 0222        dashedPen.PathEffect = SKPathEffect.CreateDash([5, 5], 0);
 0223        canvas.DrawLine(Margin + 150, y + 2, Margin + 170, y + 2, dashedPen);
 224
 0225        DrawText(canvas, "Funktionsfähigkeit",
 0226            new SKRect(Margin + 176, y - 2, Margin + 296, y + 10), legendFont, legendPaint);
 227
 0228        ctx.Y += 20;
 229
 0230        using var scaleFont = new SKFont(Arial, 7);
 0231        DrawText(canvas, "+5 = Manie  ·  0 = Normal  ·  −5 = Schwere Depression",
 0232            new SKRect(Margin + 30, ctx.Y, Margin + 30 + ContentW, ctx.Y + 10), scaleFont, legendPaint);
 233
 0234        ctx.Y += 16;
 235
 0236        using var linePaint = Stroke(GridColor, 0.5f);
 0237        canvas.DrawLine(Margin, ctx.Y, PageW - Margin, ctx.Y, linePaint);
 0238        ctx.Y += 12;
 0239    }
 240
 241    // ── Tagesübersicht ──────────────────────────────────────────────────────
 242
 243    private void DrawDailyTable(PageContext ctx, IReadOnlyList<DailyEntryDto> entries)
 244    {
 0245        if (entries.Count == 0) return;
 246
 0247        ctx.EnsureSpace(60);
 0248        var canvas = ctx.Canvas;
 249
 0250        using var headerFont = new SKFont(ArialBold, 9);
 0251        using var headerPaint = Fill(TextPrimary);
 0252        DrawText(canvas, "Tagesübersicht",
 0253            new SKRect(Margin, ctx.Y, Margin + ContentW, ctx.Y + 14), headerFont, headerPaint);
 0254        ctx.Y += 16;
 255
 0256        float[] colW = [70, 45, 50, 45, ContentW - 210];
 0257        string[] headers = ["Datum", "Schlaf", "Medik.", "Mens.", "Symptome"];
 258
 0259        DrawTableRow(canvas, ctx.Y, colW, headers, headerFont, headerPaint, isHeader: true);
 0260        ctx.Y += 16;
 261
 0262        using var cellFont = new SKFont(Arial, 8);
 0263        using var cellPaint = Fill(TextSecondary);
 0264        foreach (var entry in entries)
 265        {
 0266            ctx.EnsureSpace(14);
 267
 0268            var symptoms = entry.Symptoms is { Length: > 0 }
 0269                ? entry.Symptoms
 0270                : "-";
 271
 0272            if (symptoms.Length > 40)
 0273                symptoms = symptoms[..37] + "...";
 274
 0275            string[] cells =
 0276            [
 0277                entry.Date.ToString("dd.MM.yyyy"),
 0278                $"{entry.SleepHours}h",
 0279                entry.MedicationTaken ? "✓" : "-",
 0280                entry.MenstrualCycle ? "✓" : "-",
 0281                symptoms
 0282            ];
 283
 0284            DrawTableRow(canvas, ctx.Y, colW, cells, cellFont, cellPaint, isHeader: false);
 0285            ctx.Y += 14;
 286        }
 287
 0288        ctx.Y += 10;
 289
 0290        using var linePaint = Stroke(GridColor, 0.5f);
 0291        canvas.DrawLine(Margin, ctx.Y, PageW - Margin, ctx.Y, linePaint);
 0292        ctx.Y += 12;
 0293    }
 294
 295    private static void DrawTableRow(
 296        SKCanvas canvas, float y, float[] colW, string[] cells,
 297        SKFont font, SKPaint paint, bool isHeader)
 298    {
 0299        if (isHeader)
 300        {
 0301            using var bg = Fill(new SKColor(240, 240, 240));
 0302            canvas.DrawRect(Margin, y, ContentW, 14, bg);
 303        }
 304
 0305        var x = Margin;
 0306        for (var i = 0; i < cells.Length; i++)
 307        {
 0308            DrawText(canvas, cells[i],
 0309                new SKRect(x + 2, y + 2, x + colW[i] - 2, y + 14), font, paint);
 0310            x += colW[i];
 311        }
 312
 0313        using var linePaint = Stroke(GridColor, 0.3f);
 0314        canvas.DrawLine(Margin, y + 14, PageW - Margin, y + 14, linePaint);
 0315    }
 316
 317    // ── Notizen ─────────────────────────────────────────────────────────────
 318
 319    private void DrawNotes(PageContext ctx, IReadOnlyList<DailyEntryDto> entries)
 320    {
 0321        var withNotes = entries.Where(e => !string.IsNullOrWhiteSpace(e.Notes)).ToList();
 0322        if (withNotes.Count == 0) return;
 323
 0324        ctx.EnsureSpace(30);
 0325        var canvas = ctx.Canvas;
 326
 0327        using var headerFont = new SKFont(ArialBold, 9);
 0328        using var headerPaint = Fill(TextPrimary);
 0329        DrawText(canvas, "Notizen",
 0330            new SKRect(Margin, ctx.Y, Margin + ContentW, ctx.Y + 14), headerFont, headerPaint);
 0331        ctx.Y += 16;
 332
 0333        using var noteFont = new SKFont(Arial, 8);
 0334        using var notePaint = Fill(TextSecondary);
 0335        foreach (var entry in withNotes)
 336        {
 0337            ctx.EnsureSpace(24);
 338
 0339            using var dateFont = new SKFont(ArialBold, 8);
 0340            DrawText(canvas, entry.Date.ToString("dd.MM.yyyy") + ":",
 0341                new SKRect(Margin, ctx.Y, Margin + 70, ctx.Y + 12), dateFont, headerPaint);
 342
 0343            DrawText(canvas, entry.Notes!,
 0344                new SKRect(Margin + 75, ctx.Y, Margin + 75 + ContentW - 75, ctx.Y + 24), noteFont, notePaint);
 345
 0346            ctx.Y += 20;
 347        }
 348
 0349        ctx.Y += 8;
 0350        using var linePaint = Stroke(GridColor, 0.5f);
 0351        canvas.DrawLine(Margin, ctx.Y, PageW - Margin, ctx.Y, linePaint);
 0352        ctx.Y += 12;
 0353    }
 354
 355    // ── Medikamente ─────────────────────────────────────────────────────────
 356
 357    private void DrawMedications(PageContext ctx, IReadOnlyList<MedicationDto> medications)
 358    {
 0359        if (medications.Count == 0) return;
 360
 0361        ctx.EnsureSpace(30);
 0362        var canvas = ctx.Canvas;
 363
 0364        using var headerFont = new SKFont(ArialBold, 9);
 0365        using var headerPaint = Fill(TextPrimary);
 0366        DrawText(canvas, "Aktuelle Medikamente",
 0367            new SKRect(Margin, ctx.Y, Margin + ContentW, ctx.Y + 14), headerFont, headerPaint);
 0368        ctx.Y += 16;
 369
 0370        using var itemFont = new SKFont(Arial, 8);
 0371        foreach (var med in medications)
 372        {
 0373            ctx.EnsureSpace(14);
 374
 0375            var times = string.Join(", ",
 0376                med.IntakeTimes.Select(i => $"{i.Time} ({i.DoseCount}x)"));
 377
 0378            var line = $"• {med.Name}  {med.Dosage}";
 0379            if (!string.IsNullOrEmpty(times))
 0380                line += $"  —  {times}";
 381
 0382            DrawText(canvas, line,
 0383                new SKRect(Margin, ctx.Y, Margin + ContentW, ctx.Y + 12), itemFont, headerPaint);
 384
 0385            ctx.Y += 14;
 386        }
 0387    }
 388
 389    // ── Footer ──────────────────────────────────────────────────────────────
 390
 391    private void DrawFooter(PageContext ctx)
 392    {
 0393        foreach (var canvas in ctx.AllPages)
 394        {
 0395            var lineY = PageH - Margin - 16;
 396
 0397            using var linePaint = Stroke(GridColor, 0.4f);
 0398            canvas.DrawLine(Margin, lineY, PageW - Margin, lineY, linePaint);
 399
 0400            using var font = new SKFont(Arial, 7);
 0401            using var paint = Fill(TextSecondary);
 0402            var ts = DateTime.Now.ToString("yyyy-MM-dd HH:mm");
 403
 0404            DrawText(canvas, $"Erstellt von LifeChart am {ts}",
 0405                new SKRect(Margin, lineY + 3, Margin + ContentW / 2, lineY + 13), font, paint);
 0406            DrawText(canvas, "Basierend auf der NIMH Life Chart Methode",
 0407                new SKRect(Margin + ContentW / 2, lineY + 3, PageW - Margin, lineY + 13), font, paint,
 0408                centerX: true);
 409        }
 0410    }
 411
 412    // ── Hilfsfunktionen ─────────────────────────────────────────────────────
 413
 414    private static float ValueToY(int value, float chartY)
 0415        => chartY + ChartH - ((value + 5f) / 10f) * ChartH;
 416
 417    private static float PointToX(int index, int n, float chartX, float chartW)
 0418        => chartX + ((float)index / Math.Max(n - 1, 1)) * chartW;
 419
 420    // ── PageContext ─────────────────────────────────────────────────────────
 421
 422    private sealed class PageContext
 423    {
 424        private readonly SKDocument _document;
 0425        private readonly List<SKCanvas> _pages = [];
 426        private bool _pageOpen;
 427
 0428        public SKCanvas Canvas => _pages[^1];
 0429        public float Y { get; set; }
 0430        public IReadOnlyList<SKCanvas> AllPages => _pages;
 431
 0432        public PageContext(SKDocument document)
 433        {
 0434            _document = document;
 0435            AddPage();
 0436            Y = Margin;
 0437        }
 438
 439        public void EnsureSpace(float needed)
 440        {
 0441            if (Y + needed > ContentBottom)
 442            {
 0443                AddPage();
 0444                Y = Margin;
 445            }
 0446        }
 447
 448        private void AddPage()
 449        {
 0450            if (_pageOpen)
 0451                _document.EndPage();
 0452            _pages.Add(_document.BeginPage(PageW, PageH));
 0453            _pageOpen = true;
 0454        }
 455
 456        public void Close()
 457        {
 0458            if (_pageOpen)
 459            {
 0460                _document.EndPage();
 0461                _pageOpen = false;
 462            }
 0463            _document.Close();
 0464        }
 465    }
 466}

Methods/Properties

.cctor()
RenderAsync(LifeChart.Application.Interfaces.PdfReportData)
Fill(SkiaSharp.SKColor)
Stroke(SkiaSharp.SKColor,System.Single)
DrawText(SkiaSharp.SKCanvas,System.String,SkiaSharp.SKRect,SkiaSharp.SKFont,SkiaSharp.SKPaint,System.Boolean,System.Boolean)
DrawHeader(LifeChart.Infrastructure.Pdf.PdfRenderer/PageContext,LifeChart.Application.Interfaces.PdfReportData)
DrawChart(LifeChart.Infrastructure.Pdf.PdfRenderer/PageContext,LifeChart.Application.DTOs.ChartDataDto)
DrawSeries(SkiaSharp.SKCanvas,System.Collections.Generic.IReadOnlyList`1<LifeChart.Application.DTOs.ChartPointDto>,System.Int32,System.Single,System.Single,System.Single,System.Func`2<LifeChart.Application.DTOs.ChartPointDto,System.Int32>,SkiaSharp.SKColor,System.Boolean)
DrawXDates(SkiaSharp.SKCanvas,System.Collections.Generic.IReadOnlyList`1<LifeChart.Application.DTOs.ChartPointDto>,System.Int32,System.Single,System.Single,System.Single,SkiaSharp.SKFont,SkiaSharp.SKPaint)
DrawLegend(LifeChart.Infrastructure.Pdf.PdfRenderer/PageContext)
DrawDailyTable(LifeChart.Infrastructure.Pdf.PdfRenderer/PageContext,System.Collections.Generic.IReadOnlyList`1<LifeChart.Application.DTOs.DailyEntryDto>)
DrawTableRow(SkiaSharp.SKCanvas,System.Single,System.Single[],System.String[],SkiaSharp.SKFont,SkiaSharp.SKPaint,System.Boolean)
DrawNotes(LifeChart.Infrastructure.Pdf.PdfRenderer/PageContext,System.Collections.Generic.IReadOnlyList`1<LifeChart.Application.DTOs.DailyEntryDto>)
DrawMedications(LifeChart.Infrastructure.Pdf.PdfRenderer/PageContext,System.Collections.Generic.IReadOnlyList`1<LifeChart.Application.DTOs.MedicationDto>)
DrawFooter(LifeChart.Infrastructure.Pdf.PdfRenderer/PageContext)
ValueToY(System.Int32,System.Single)
PointToX(System.Int32,System.Int32,System.Single,System.Single)
.ctor(SkiaSharp.SKDocument)
get_Canvas()
get_Y()
get_AllPages()
EnsureSpace(System.Single)
AddPage()
Close()