| | | 1 | | using LifeChart.Application.DTOs; |
| | | 2 | | using LifeChart.Application.Interfaces; |
| | | 3 | | using SkiaSharp; |
| | | 4 | | |
| | | 5 | | namespace LifeChart.Infrastructure.Pdf; |
| | | 6 | | |
| | | 7 | | public 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 | | |
| | 0 | 17 | | private static readonly SKColor MoodColor = new(0, 114, 178); |
| | 0 | 18 | | private static readonly SKColor FuncColor = new(230, 159, 0); |
| | 0 | 19 | | private static readonly SKColor TextPrimary = new(26, 26, 26); |
| | 0 | 20 | | private static readonly SKColor TextSecondary = new(107, 107, 107); |
| | 0 | 21 | | private static readonly SKColor GridColor = new(200, 200, 200); |
| | 0 | 22 | | private static readonly SKColor BandMania = new(255, 230, 0, 30); |
| | 0 | 23 | | private static readonly SKColor BandDepression = new(0, 114, 178, 30); |
| | | 24 | | |
| | 0 | 25 | | private static readonly SKTypeface Arial = |
| | 0 | 26 | | SKTypeface.FromFamilyName("Arial", SKFontStyleWeight.Normal, SKFontStyleWidth.Normal, SKFontStyleSlant.Upright); |
| | | 27 | | |
| | 0 | 28 | | private static readonly SKTypeface ArialBold = |
| | 0 | 29 | | SKTypeface.FromFamilyName("Arial", SKFontStyleWeight.Bold, SKFontStyleWidth.Normal, SKFontStyleSlant.Upright); |
| | | 30 | | |
| | | 31 | | public Task<byte[]> RenderAsync(PdfReportData data) |
| | | 32 | | { |
| | 0 | 33 | | using var stream = new MemoryStream(); |
| | 0 | 34 | | using var wstream = new SKManagedWStream(stream); |
| | 0 | 35 | | using var document = SKDocument.CreatePdf(wstream); |
| | | 36 | | |
| | 0 | 37 | | var ctx = new PageContext(document); |
| | 0 | 38 | | DrawHeader(ctx, data); |
| | 0 | 39 | | DrawChart(ctx, data.ChartData); |
| | 0 | 40 | | DrawLegend(ctx); |
| | 0 | 41 | | DrawDailyTable(ctx, data.Entries); |
| | 0 | 42 | | DrawNotes(ctx, data.Entries); |
| | 0 | 43 | | DrawMedications(ctx, data.ActiveMedications); |
| | 0 | 44 | | DrawFooter(ctx); |
| | 0 | 45 | | ctx.Close(); |
| | | 46 | | |
| | 0 | 47 | | return Task.FromResult(stream.ToArray()); |
| | 0 | 48 | | } |
| | | 49 | | |
| | | 50 | | private static SKPaint Fill(SKColor color) |
| | 0 | 51 | | => new() { Color = color, Style = SKPaintStyle.Fill, IsAntialias = true }; |
| | | 52 | | |
| | | 53 | | private static SKPaint Stroke(SKColor color, float strokeWidth) |
| | 0 | 54 | | => 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 | | { |
| | 0 | 59 | | font.GetFontMetrics(out var metrics); |
| | 0 | 60 | | var textWidth = font.MeasureText(text); |
| | 0 | 61 | | var textHeight = metrics.Descent - metrics.Ascent; |
| | | 62 | | |
| | 0 | 63 | | var x = centerX ? rect.MidX - textWidth / 2 : rect.Left; |
| | 0 | 64 | | var y = centerY ? rect.MidY - textHeight / 2 : rect.Top; |
| | | 65 | | |
| | 0 | 66 | | canvas.DrawText(text, x, y - metrics.Ascent, font, paint); |
| | 0 | 67 | | } |
| | | 68 | | |
| | | 69 | | // ── Header ────────────────────────────────────────────────────────────── |
| | | 70 | | |
| | | 71 | | private void DrawHeader(PageContext ctx, PdfReportData data) |
| | | 72 | | { |
| | 0 | 73 | | var canvas = ctx.Canvas; |
| | | 74 | | |
| | 0 | 75 | | using var titleFont = new SKFont(ArialBold, 14); |
| | 0 | 76 | | using var titlePaint = Fill(TextPrimary); |
| | 0 | 77 | | DrawText(canvas, "LifeChart — Mood & Functionality Report", |
| | 0 | 78 | | new SKRect(Margin, ctx.Y, Margin + ContentW, ctx.Y + 20), titleFont, titlePaint); |
| | 0 | 79 | | ctx.Y += 22; |
| | | 80 | | |
| | 0 | 81 | | var periodLabel = $"{data.From:dd.MM.yyyy} – {data.To:dd.MM.yyyy}"; |
| | 0 | 82 | | using var subFont = new SKFont(Arial, 9); |
| | 0 | 83 | | using var subPaint = Fill(TextSecondary); |
| | 0 | 84 | | DrawText(canvas, periodLabel, |
| | 0 | 85 | | new SKRect(Margin, ctx.Y, Margin + ContentW, ctx.Y + 14), subFont, subPaint); |
| | 0 | 86 | | ctx.Y += 18; |
| | | 87 | | |
| | 0 | 88 | | using var linePaint = Stroke(GridColor, 0.5f); |
| | 0 | 89 | | canvas.DrawLine(Margin, ctx.Y, PageW - Margin, ctx.Y, linePaint); |
| | 0 | 90 | | ctx.Y += 10; |
| | 0 | 91 | | } |
| | | 92 | | |
| | | 93 | | // ── Chart ─────────────────────────────────────────────────────────────── |
| | | 94 | | |
| | | 95 | | private void DrawChart(PageContext ctx, ChartDataDto chartData) |
| | | 96 | | { |
| | 0 | 97 | | ctx.EnsureSpace(ChartH + 20); |
| | 0 | 98 | | var canvas = ctx.Canvas; |
| | | 99 | | |
| | 0 | 100 | | var chartX = Margin + 30; |
| | 0 | 101 | | var chartY = ctx.Y; |
| | 0 | 102 | | var chartW = ContentW - 30; |
| | | 103 | | |
| | 0 | 104 | | using var maniaFill = Fill(BandMania); |
| | 0 | 105 | | canvas.DrawRect(chartX, chartY, chartW, ChartH / 2, maniaFill); |
| | | 106 | | |
| | 0 | 107 | | using var depFill = Fill(BandDepression); |
| | 0 | 108 | | canvas.DrawRect(chartX, chartY + ChartH / 2, chartW, ChartH / 2, depFill); |
| | | 109 | | |
| | 0 | 110 | | using var gridFont = new SKFont(Arial, 7); |
| | 0 | 111 | | using var gridPaint = Fill(TextSecondary); |
| | 0 | 112 | | for (var v = -5; v <= 5; v++) |
| | | 113 | | { |
| | 0 | 114 | | var y = ValueToY(v, chartY); |
| | 0 | 115 | | var penColor = v == 0 |
| | 0 | 116 | | ? new SKColor(0, 0, 0, 80) |
| | 0 | 117 | | : new SKColor(0, 0, 0, 40); |
| | 0 | 118 | | using var pen = Stroke(penColor, v == 0 ? 0.7f : 0.4f); |
| | 0 | 119 | | canvas.DrawLine(chartX, y, chartX + chartW, y, pen); |
| | | 120 | | |
| | 0 | 121 | | DrawText(canvas, v.ToString(), |
| | 0 | 122 | | new SKRect(Margin, y - 5, Margin + 25, y + 5), gridFont, gridPaint); |
| | | 123 | | } |
| | | 124 | | |
| | 0 | 125 | | using var borderPen = Stroke(GridColor, 0.5f); |
| | 0 | 126 | | canvas.DrawRect(chartX, chartY, chartW, ChartH, borderPen); |
| | | 127 | | |
| | 0 | 128 | | if (chartData.Points.Count == 0) |
| | | 129 | | { |
| | 0 | 130 | | using var noDataFont = new SKFont(Arial, 10); |
| | 0 | 131 | | DrawText(canvas, "Keine Daten für diesen Zeitraum.", |
| | 0 | 132 | | new SKRect(chartX, chartY, chartX + chartW, chartY + ChartH), noDataFont, gridPaint, |
| | 0 | 133 | | centerX: true, centerY: true); |
| | 0 | 134 | | ctx.Y += ChartH + 8; |
| | 0 | 135 | | return; |
| | | 136 | | } |
| | | 137 | | |
| | 0 | 138 | | var n = chartData.Points.Count; |
| | | 139 | | |
| | 0 | 140 | | DrawSeries(canvas, chartData.Points, n, chartX, chartY, chartW, |
| | 0 | 141 | | p => p.Mood, MoodColor, dashStyle: false); |
| | 0 | 142 | | DrawSeries(canvas, chartData.Points, n, chartX, chartY, chartW, |
| | 0 | 143 | | p => p.Functionality, FuncColor, dashStyle: true); |
| | | 144 | | |
| | 0 | 145 | | DrawXDates(canvas, chartData.Points, n, chartX, chartY, chartW, gridFont, gridPaint); |
| | | 146 | | |
| | 0 | 147 | | ctx.Y += ChartH + 8; |
| | 0 | 148 | | } |
| | | 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 | | { |
| | 0 | 159 | | using var pen = Stroke(color, 1.5f); |
| | 0 | 160 | | if (dashStyle) |
| | 0 | 161 | | pen.PathEffect = SKPathEffect.CreateDash([5, 5], 0); |
| | | 162 | | |
| | 0 | 163 | | for (var i = 1; i < n; i++) |
| | | 164 | | { |
| | 0 | 165 | | var x1 = PointToX(i - 1, n, chartX, chartW); |
| | 0 | 166 | | var y1 = ValueToY(valueSelector(points[i - 1]), chartY); |
| | 0 | 167 | | var x2 = PointToX(i, n, chartX, chartW); |
| | 0 | 168 | | var y2 = ValueToY(valueSelector(points[i]), chartY); |
| | 0 | 169 | | canvas.DrawLine(x1, y1, x2, y2, pen); |
| | | 170 | | } |
| | | 171 | | |
| | 0 | 172 | | using var brush = Fill(color); |
| | | 173 | | const float r = 2.5f; |
| | 0 | 174 | | for (var i = 0; i < n; i++) |
| | | 175 | | { |
| | 0 | 176 | | var x = PointToX(i, n, chartX, chartW); |
| | 0 | 177 | | var y = ValueToY(valueSelector(points[i]), chartY); |
| | | 178 | | |
| | 0 | 179 | | if (dashStyle) |
| | 0 | 180 | | canvas.DrawRect(x - r, y - r, r * 2, r * 2, brush); |
| | | 181 | | else |
| | 0 | 182 | | canvas.DrawCircle(x, y, r, brush); |
| | | 183 | | } |
| | 0 | 184 | | } |
| | | 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 | | { |
| | 0 | 193 | | var step = Math.Max(1, n / 9); |
| | | 194 | | |
| | 0 | 195 | | for (var i = 0; i < n; i++) |
| | | 196 | | { |
| | 0 | 197 | | if (i % step != 0 && i != n - 1) continue; |
| | 0 | 198 | | var x = PointToX(i, n, chartX, chartW); |
| | 0 | 199 | | var label = points[i].Date.ToString("dd.MM"); |
| | 0 | 200 | | DrawText(canvas, label, |
| | 0 | 201 | | new SKRect(x - 15, chartY + ChartH + 2, x + 15, chartY + ChartH + 12), font, paint, |
| | 0 | 202 | | centerX: true); |
| | | 203 | | } |
| | 0 | 204 | | } |
| | | 205 | | |
| | | 206 | | // ── Legende ───────────────────────────────────────────────────────────── |
| | | 207 | | |
| | | 208 | | private void DrawLegend(PageContext ctx) |
| | | 209 | | { |
| | 0 | 210 | | var canvas = ctx.Canvas; |
| | 0 | 211 | | var y = ctx.Y; |
| | | 212 | | |
| | 0 | 213 | | using var moodFill = Fill(MoodColor); |
| | 0 | 214 | | canvas.DrawRect(Margin + 30, y, 20, 4, moodFill); |
| | | 215 | | |
| | 0 | 216 | | using var legendFont = new SKFont(Arial, 8); |
| | 0 | 217 | | using var legendPaint = Fill(TextPrimary); |
| | 0 | 218 | | DrawText(canvas, "Stimmung", |
| | 0 | 219 | | new SKRect(Margin + 56, y - 2, Margin + 136, y + 10), legendFont, legendPaint); |
| | | 220 | | |
| | 0 | 221 | | using var dashedPen = Stroke(FuncColor, 1.5f); |
| | 0 | 222 | | dashedPen.PathEffect = SKPathEffect.CreateDash([5, 5], 0); |
| | 0 | 223 | | canvas.DrawLine(Margin + 150, y + 2, Margin + 170, y + 2, dashedPen); |
| | | 224 | | |
| | 0 | 225 | | DrawText(canvas, "Funktionsfähigkeit", |
| | 0 | 226 | | new SKRect(Margin + 176, y - 2, Margin + 296, y + 10), legendFont, legendPaint); |
| | | 227 | | |
| | 0 | 228 | | ctx.Y += 20; |
| | | 229 | | |
| | 0 | 230 | | using var scaleFont = new SKFont(Arial, 7); |
| | 0 | 231 | | DrawText(canvas, "+5 = Manie · 0 = Normal · −5 = Schwere Depression", |
| | 0 | 232 | | new SKRect(Margin + 30, ctx.Y, Margin + 30 + ContentW, ctx.Y + 10), scaleFont, legendPaint); |
| | | 233 | | |
| | 0 | 234 | | ctx.Y += 16; |
| | | 235 | | |
| | 0 | 236 | | using var linePaint = Stroke(GridColor, 0.5f); |
| | 0 | 237 | | canvas.DrawLine(Margin, ctx.Y, PageW - Margin, ctx.Y, linePaint); |
| | 0 | 238 | | ctx.Y += 12; |
| | 0 | 239 | | } |
| | | 240 | | |
| | | 241 | | // ── Tagesübersicht ────────────────────────────────────────────────────── |
| | | 242 | | |
| | | 243 | | private void DrawDailyTable(PageContext ctx, IReadOnlyList<DailyEntryDto> entries) |
| | | 244 | | { |
| | 0 | 245 | | if (entries.Count == 0) return; |
| | | 246 | | |
| | 0 | 247 | | ctx.EnsureSpace(60); |
| | 0 | 248 | | var canvas = ctx.Canvas; |
| | | 249 | | |
| | 0 | 250 | | using var headerFont = new SKFont(ArialBold, 9); |
| | 0 | 251 | | using var headerPaint = Fill(TextPrimary); |
| | 0 | 252 | | DrawText(canvas, "Tagesübersicht", |
| | 0 | 253 | | new SKRect(Margin, ctx.Y, Margin + ContentW, ctx.Y + 14), headerFont, headerPaint); |
| | 0 | 254 | | ctx.Y += 16; |
| | | 255 | | |
| | 0 | 256 | | float[] colW = [70, 45, 50, 45, ContentW - 210]; |
| | 0 | 257 | | string[] headers = ["Datum", "Schlaf", "Medik.", "Mens.", "Symptome"]; |
| | | 258 | | |
| | 0 | 259 | | DrawTableRow(canvas, ctx.Y, colW, headers, headerFont, headerPaint, isHeader: true); |
| | 0 | 260 | | ctx.Y += 16; |
| | | 261 | | |
| | 0 | 262 | | using var cellFont = new SKFont(Arial, 8); |
| | 0 | 263 | | using var cellPaint = Fill(TextSecondary); |
| | 0 | 264 | | foreach (var entry in entries) |
| | | 265 | | { |
| | 0 | 266 | | ctx.EnsureSpace(14); |
| | | 267 | | |
| | 0 | 268 | | var symptoms = entry.Symptoms is { Length: > 0 } |
| | 0 | 269 | | ? entry.Symptoms |
| | 0 | 270 | | : "-"; |
| | | 271 | | |
| | 0 | 272 | | if (symptoms.Length > 40) |
| | 0 | 273 | | symptoms = symptoms[..37] + "..."; |
| | | 274 | | |
| | 0 | 275 | | string[] cells = |
| | 0 | 276 | | [ |
| | 0 | 277 | | entry.Date.ToString("dd.MM.yyyy"), |
| | 0 | 278 | | $"{entry.SleepHours}h", |
| | 0 | 279 | | entry.MedicationTaken ? "✓" : "-", |
| | 0 | 280 | | entry.MenstrualCycle ? "✓" : "-", |
| | 0 | 281 | | symptoms |
| | 0 | 282 | | ]; |
| | | 283 | | |
| | 0 | 284 | | DrawTableRow(canvas, ctx.Y, colW, cells, cellFont, cellPaint, isHeader: false); |
| | 0 | 285 | | ctx.Y += 14; |
| | | 286 | | } |
| | | 287 | | |
| | 0 | 288 | | ctx.Y += 10; |
| | | 289 | | |
| | 0 | 290 | | using var linePaint = Stroke(GridColor, 0.5f); |
| | 0 | 291 | | canvas.DrawLine(Margin, ctx.Y, PageW - Margin, ctx.Y, linePaint); |
| | 0 | 292 | | ctx.Y += 12; |
| | 0 | 293 | | } |
| | | 294 | | |
| | | 295 | | private static void DrawTableRow( |
| | | 296 | | SKCanvas canvas, float y, float[] colW, string[] cells, |
| | | 297 | | SKFont font, SKPaint paint, bool isHeader) |
| | | 298 | | { |
| | 0 | 299 | | if (isHeader) |
| | | 300 | | { |
| | 0 | 301 | | using var bg = Fill(new SKColor(240, 240, 240)); |
| | 0 | 302 | | canvas.DrawRect(Margin, y, ContentW, 14, bg); |
| | | 303 | | } |
| | | 304 | | |
| | 0 | 305 | | var x = Margin; |
| | 0 | 306 | | for (var i = 0; i < cells.Length; i++) |
| | | 307 | | { |
| | 0 | 308 | | DrawText(canvas, cells[i], |
| | 0 | 309 | | new SKRect(x + 2, y + 2, x + colW[i] - 2, y + 14), font, paint); |
| | 0 | 310 | | x += colW[i]; |
| | | 311 | | } |
| | | 312 | | |
| | 0 | 313 | | using var linePaint = Stroke(GridColor, 0.3f); |
| | 0 | 314 | | canvas.DrawLine(Margin, y + 14, PageW - Margin, y + 14, linePaint); |
| | 0 | 315 | | } |
| | | 316 | | |
| | | 317 | | // ── Notizen ───────────────────────────────────────────────────────────── |
| | | 318 | | |
| | | 319 | | private void DrawNotes(PageContext ctx, IReadOnlyList<DailyEntryDto> entries) |
| | | 320 | | { |
| | 0 | 321 | | var withNotes = entries.Where(e => !string.IsNullOrWhiteSpace(e.Notes)).ToList(); |
| | 0 | 322 | | if (withNotes.Count == 0) return; |
| | | 323 | | |
| | 0 | 324 | | ctx.EnsureSpace(30); |
| | 0 | 325 | | var canvas = ctx.Canvas; |
| | | 326 | | |
| | 0 | 327 | | using var headerFont = new SKFont(ArialBold, 9); |
| | 0 | 328 | | using var headerPaint = Fill(TextPrimary); |
| | 0 | 329 | | DrawText(canvas, "Notizen", |
| | 0 | 330 | | new SKRect(Margin, ctx.Y, Margin + ContentW, ctx.Y + 14), headerFont, headerPaint); |
| | 0 | 331 | | ctx.Y += 16; |
| | | 332 | | |
| | 0 | 333 | | using var noteFont = new SKFont(Arial, 8); |
| | 0 | 334 | | using var notePaint = Fill(TextSecondary); |
| | 0 | 335 | | foreach (var entry in withNotes) |
| | | 336 | | { |
| | 0 | 337 | | ctx.EnsureSpace(24); |
| | | 338 | | |
| | 0 | 339 | | using var dateFont = new SKFont(ArialBold, 8); |
| | 0 | 340 | | DrawText(canvas, entry.Date.ToString("dd.MM.yyyy") + ":", |
| | 0 | 341 | | new SKRect(Margin, ctx.Y, Margin + 70, ctx.Y + 12), dateFont, headerPaint); |
| | | 342 | | |
| | 0 | 343 | | DrawText(canvas, entry.Notes!, |
| | 0 | 344 | | new SKRect(Margin + 75, ctx.Y, Margin + 75 + ContentW - 75, ctx.Y + 24), noteFont, notePaint); |
| | | 345 | | |
| | 0 | 346 | | ctx.Y += 20; |
| | | 347 | | } |
| | | 348 | | |
| | 0 | 349 | | ctx.Y += 8; |
| | 0 | 350 | | using var linePaint = Stroke(GridColor, 0.5f); |
| | 0 | 351 | | canvas.DrawLine(Margin, ctx.Y, PageW - Margin, ctx.Y, linePaint); |
| | 0 | 352 | | ctx.Y += 12; |
| | 0 | 353 | | } |
| | | 354 | | |
| | | 355 | | // ── Medikamente ───────────────────────────────────────────────────────── |
| | | 356 | | |
| | | 357 | | private void DrawMedications(PageContext ctx, IReadOnlyList<MedicationDto> medications) |
| | | 358 | | { |
| | 0 | 359 | | if (medications.Count == 0) return; |
| | | 360 | | |
| | 0 | 361 | | ctx.EnsureSpace(30); |
| | 0 | 362 | | var canvas = ctx.Canvas; |
| | | 363 | | |
| | 0 | 364 | | using var headerFont = new SKFont(ArialBold, 9); |
| | 0 | 365 | | using var headerPaint = Fill(TextPrimary); |
| | 0 | 366 | | DrawText(canvas, "Aktuelle Medikamente", |
| | 0 | 367 | | new SKRect(Margin, ctx.Y, Margin + ContentW, ctx.Y + 14), headerFont, headerPaint); |
| | 0 | 368 | | ctx.Y += 16; |
| | | 369 | | |
| | 0 | 370 | | using var itemFont = new SKFont(Arial, 8); |
| | 0 | 371 | | foreach (var med in medications) |
| | | 372 | | { |
| | 0 | 373 | | ctx.EnsureSpace(14); |
| | | 374 | | |
| | 0 | 375 | | var times = string.Join(", ", |
| | 0 | 376 | | med.IntakeTimes.Select(i => $"{i.Time} ({i.DoseCount}x)")); |
| | | 377 | | |
| | 0 | 378 | | var line = $"• {med.Name} {med.Dosage}"; |
| | 0 | 379 | | if (!string.IsNullOrEmpty(times)) |
| | 0 | 380 | | line += $" — {times}"; |
| | | 381 | | |
| | 0 | 382 | | DrawText(canvas, line, |
| | 0 | 383 | | new SKRect(Margin, ctx.Y, Margin + ContentW, ctx.Y + 12), itemFont, headerPaint); |
| | | 384 | | |
| | 0 | 385 | | ctx.Y += 14; |
| | | 386 | | } |
| | 0 | 387 | | } |
| | | 388 | | |
| | | 389 | | // ── Footer ────────────────────────────────────────────────────────────── |
| | | 390 | | |
| | | 391 | | private void DrawFooter(PageContext ctx) |
| | | 392 | | { |
| | 0 | 393 | | foreach (var canvas in ctx.AllPages) |
| | | 394 | | { |
| | 0 | 395 | | var lineY = PageH - Margin - 16; |
| | | 396 | | |
| | 0 | 397 | | using var linePaint = Stroke(GridColor, 0.4f); |
| | 0 | 398 | | canvas.DrawLine(Margin, lineY, PageW - Margin, lineY, linePaint); |
| | | 399 | | |
| | 0 | 400 | | using var font = new SKFont(Arial, 7); |
| | 0 | 401 | | using var paint = Fill(TextSecondary); |
| | 0 | 402 | | var ts = DateTime.Now.ToString("yyyy-MM-dd HH:mm"); |
| | | 403 | | |
| | 0 | 404 | | DrawText(canvas, $"Erstellt von LifeChart am {ts}", |
| | 0 | 405 | | new SKRect(Margin, lineY + 3, Margin + ContentW / 2, lineY + 13), font, paint); |
| | 0 | 406 | | DrawText(canvas, "Basierend auf der NIMH Life Chart Methode", |
| | 0 | 407 | | new SKRect(Margin + ContentW / 2, lineY + 3, PageW - Margin, lineY + 13), font, paint, |
| | 0 | 408 | | centerX: true); |
| | | 409 | | } |
| | 0 | 410 | | } |
| | | 411 | | |
| | | 412 | | // ── Hilfsfunktionen ───────────────────────────────────────────────────── |
| | | 413 | | |
| | | 414 | | private static float ValueToY(int value, float chartY) |
| | 0 | 415 | | => chartY + ChartH - ((value + 5f) / 10f) * ChartH; |
| | | 416 | | |
| | | 417 | | private static float PointToX(int index, int n, float chartX, float chartW) |
| | 0 | 418 | | => chartX + ((float)index / Math.Max(n - 1, 1)) * chartW; |
| | | 419 | | |
| | | 420 | | // ── PageContext ───────────────────────────────────────────────────────── |
| | | 421 | | |
| | | 422 | | private sealed class PageContext |
| | | 423 | | { |
| | | 424 | | private readonly SKDocument _document; |
| | 0 | 425 | | private readonly List<SKCanvas> _pages = []; |
| | | 426 | | private bool _pageOpen; |
| | | 427 | | |
| | 0 | 428 | | public SKCanvas Canvas => _pages[^1]; |
| | 0 | 429 | | public float Y { get; set; } |
| | 0 | 430 | | public IReadOnlyList<SKCanvas> AllPages => _pages; |
| | | 431 | | |
| | 0 | 432 | | public PageContext(SKDocument document) |
| | | 433 | | { |
| | 0 | 434 | | _document = document; |
| | 0 | 435 | | AddPage(); |
| | 0 | 436 | | Y = Margin; |
| | 0 | 437 | | } |
| | | 438 | | |
| | | 439 | | public void EnsureSpace(float needed) |
| | | 440 | | { |
| | 0 | 441 | | if (Y + needed > ContentBottom) |
| | | 442 | | { |
| | 0 | 443 | | AddPage(); |
| | 0 | 444 | | Y = Margin; |
| | | 445 | | } |
| | 0 | 446 | | } |
| | | 447 | | |
| | | 448 | | private void AddPage() |
| | | 449 | | { |
| | 0 | 450 | | if (_pageOpen) |
| | 0 | 451 | | _document.EndPage(); |
| | 0 | 452 | | _pages.Add(_document.BeginPage(PageW, PageH)); |
| | 0 | 453 | | _pageOpen = true; |
| | 0 | 454 | | } |
| | | 455 | | |
| | | 456 | | public void Close() |
| | | 457 | | { |
| | 0 | 458 | | if (_pageOpen) |
| | | 459 | | { |
| | 0 | 460 | | _document.EndPage(); |
| | 0 | 461 | | _pageOpen = false; |
| | | 462 | | } |
| | 0 | 463 | | _document.Close(); |
| | 0 | 464 | | } |
| | | 465 | | } |
| | | 466 | | } |