From 861f2c76ca868b62ccdd30d133fab2a916109ca4 Mon Sep 17 00:00:00 2001 From: Baddi86 Date: Wed, 16 Sep 2026 02:21:21 +0200 Subject: [PATCH] fix: Umbruchlogik --- LehrerApp.Desktop.Tests/_TempFlowCheck2.cs | 40 ++++++++++++++++++ .../LehrerApp.Templating.Tests.csproj | 1 + LehrerApp.Templating.Tests/TemplatingTests.cs | 37 ++++++++++++++++ LehrerApp.Templating/QuestTemplateRenderer.cs | 42 +++++++++++++++++-- 4 files changed, 117 insertions(+), 3 deletions(-) create mode 100644 LehrerApp.Desktop.Tests/_TempFlowCheck2.cs diff --git a/LehrerApp.Desktop.Tests/_TempFlowCheck2.cs b/LehrerApp.Desktop.Tests/_TempFlowCheck2.cs new file mode 100644 index 0000000..d77fe94 --- /dev/null +++ b/LehrerApp.Desktop.Tests/_TempFlowCheck2.cs @@ -0,0 +1,40 @@ +using LehrerApp.Templating; +using Xunit; + +namespace LehrerApp.Desktop.Tests; + +public sealed class _TempFlowCheck2 +{ + [Fact] + public void MarkerTest() + { + var outDir = @"C:\Users\SHedt\AppData\Local\Temp\claude\d--source-LehrerApp\fb3681df-caaf-4f2d-9996-e343e44f5554\scratchpad"; + Directory.CreateDirectory(outDir); + + var commands = new List + { + new DrawStringEx(0, 0, 12, 170, "TOP-0", DrawingTextAlignment.AlignLeft, 10, Color: "#000000"), + new DrawStringEx(0, 90, 12, 170, "NEARBOTTOM-90", DrawingTextAlignment.AlignLeft, 10, Color: "#000000"), + new DrawStringEx(0, 105, 12, 170, "AFTERBOUNDARY-105", DrawingTextAlignment.AlignLeft, 10, Color: "#000000"), + new DrawStringEx(0, 190, 12, 170, "BOTTOM-190", DrawingTextAlignment.AlignLeft, 10, Color: "#000000"), + }; + var drawing = new DrawingValue(commands, 200); + + var manifest = new TemplateManifest + { + Id = "marker", Name = "Marker", + Placeholders = [new("Marker", PlaceholderType.Drawing, true)], + }; + var layout = "PAGE 210 297 mm\nFLOWDRAWBOX 20 20 170 100 $Marker\n"; + var loadedLayout = new LayoutParser().Parse(layout); + var loaded = new LoadedTemplate(manifest, loadedLayout, new Dictionary()); + var provider = new FakeProvider(new Dictionary { ["Marker"] = drawing }); + + var pngs = new QuestTemplateRenderer().RenderPagesToPng(loaded, provider, dpi: 150); + for (var i = 0; i < pngs.Count; i++) + File.WriteAllBytes(Path.Combine(outDir, $"marker2-{i}.png"), pngs[i]); + } + + private sealed class FakeProvider(IReadOnlyDictionary values) : ITemplateDataProvider + { public IReadOnlyDictionary GetValues() => values; } +} diff --git a/LehrerApp.Templating.Tests/LehrerApp.Templating.Tests.csproj b/LehrerApp.Templating.Tests/LehrerApp.Templating.Tests.csproj index eeead80..517182e 100644 --- a/LehrerApp.Templating.Tests/LehrerApp.Templating.Tests.csproj +++ b/LehrerApp.Templating.Tests/LehrerApp.Templating.Tests.csproj @@ -3,6 +3,7 @@ + all diff --git a/LehrerApp.Templating.Tests/TemplatingTests.cs b/LehrerApp.Templating.Tests/TemplatingTests.cs index 595188a..ce4e30b 100644 --- a/LehrerApp.Templating.Tests/TemplatingTests.cs +++ b/LehrerApp.Templating.Tests/TemplatingTests.cs @@ -96,6 +96,43 @@ public sealed class TemplatingTests : IDisposable System.Text.Encoding.ASCII.GetString(pdf), @"/Type\s*/Page\b")); } + // Regression: RenderFlow fensterte Seiten bisher nur über einen SVG-viewBox-Y-Offset, den + // QuestPDFs Svg()-Renderer nicht respektiert - dadurch landete auf jeder Folgeseite (nahezu) + // der komplette Zeicheninhalt erneut, statt nur des jeweiligen Ausschnitts (siehe Slice() in + // QuestTemplateRenderer.DrawingElementRenderer). Prüft per echter PDF-Textextraktion, dass + // seitenspezifische Marker nur auf ihrer jeweiligen Seite auftauchen. + [Fact] + public void FlowDrawBox_ZerschneidetInhaltEchtStattIhnAufFolgeseitenZuWiederholen() + { + var template = new LoadedTemplate(new TemplateManifest + { + Placeholders = [new("Marker", PlaceholderType.Drawing, true)], + }, new LayoutParser().Parse("PAGE 210 297 mm\nFLOWDRAWBOX 20 20 170 100 $Marker"), + new Dictionary()); + var drawing = new DrawingValue( + [ + new DrawStringEx(0, 0, 12, 170, "MARKERTOP", DrawingTextAlignment.AlignLeft, 10), + new DrawStringEx(0, 90, 12, 170, "MARKERNEARBOTTOM", DrawingTextAlignment.AlignLeft, 10), + new DrawStringEx(0, 105, 12, 170, "MARKERAFTERBOUNDARY", DrawingTextAlignment.AlignLeft, 10), + new DrawStringEx(0, 190, 12, 170, "MARKERBOTTOM", DrawingTextAlignment.AlignLeft, 10), + ], 200); + + var pdf = new QuestTemplateRenderer().RenderToPdf(template, + new DictionaryProvider(new Dictionary { ["Marker"] = drawing })); + using var document = UglyToad.PdfPig.PdfDocument.Open(pdf); + var pages = document.GetPages().Select(p => p.Text).ToList(); + + Assert.Equal(2, pages.Count); + Assert.Contains("MARKERTOP", pages[0]); + Assert.Contains("MARKERNEARBOTTOM", pages[0]); + Assert.DoesNotContain("MARKERAFTERBOUNDARY", pages[0]); + Assert.DoesNotContain("MARKERBOTTOM", pages[0]); + Assert.DoesNotContain("MARKERTOP", pages[1]); + Assert.DoesNotContain("MARKERNEARBOTTOM", pages[1]); + Assert.Contains("MARKERAFTERBOUNDARY", pages[1]); + Assert.Contains("MARKERBOTTOM", pages[1]); + } + [Fact] public void FlowDrawBox_PaginertHohenDeklarativenZeichenraum() { diff --git a/LehrerApp.Templating/QuestTemplateRenderer.cs b/LehrerApp.Templating/QuestTemplateRenderer.cs index 2521abb..9239fe6 100644 --- a/LehrerApp.Templating/QuestTemplateRenderer.cs +++ b/LehrerApp.Templating/QuestTemplateRenderer.cs @@ -534,19 +534,55 @@ internal static class DrawingElementRenderer for (var page = 0; page < pages.Count; page++) { if (page > 0) column.Item().PageBreak(); - var offset = value is DrawingValue ? page * pageHeight : 0; + // Koordinaten sind nach Slice()/RecordPages() bereits seitenlokal (bei 0 beginnend) - + // kein zusätzlicher viewBox-Y-Offset nötig oder sinnvoll (siehe Slice()). column.Item().Height(UnitConverter.Points(pageHeight, unit)).Svg( - BuildSvg(pages[page], width, pageHeight, offset, unit)); + BuildSvg(pages[page], width, pageHeight, 0, unit)); } }); } + // Schneidet die Zeichenbefehle anhand ihrer Y-Koordinate in Seiten auf und verschiebt sie auf + // seitenlokale Koordinaten (Y=0 am Seitenanfang). Ein SVG-Element mit "viewBox" und + // "overflow=hidden" wird von QuestPDFs Svg()-Renderer NICHT zuverlässig geclippt - ein + // (Y-)Offset im viewBox allein reicht nicht, um pro Seite nur den jeweiligen Ausschnitt zu + // zeigen; ohne dieses echte Zerschneiden landet auf jeder Seite (nahezu) der komplette + // Zeicheninhalt erneut (Bug: doppelte/überlappende Fehlzeitenliste über mehrere Seiten). private static List Slice(DrawingValue value, float pageHeight) { var pageCount = Math.Max(1, (int)Math.Ceiling(Math.Max(0, value.ContentHeight) / pageHeight)); - return Enumerable.Repeat(value, pageCount).ToList(); + var pages = new List>(); + for (var i = 0; i < pageCount; i++) pages.Add([]); + var pendingLinePage = 0; + foreach (var command in value.Commands) + { + var y = CommandY(command); + if (y is null) { pages[0].Add(command); continue; } + var page = Math.Clamp((int)(y.Value / pageHeight), 0, pageCount - 1); + // LineTo gehört inhaltlich zum vorherigen MoveTo (eine Linie) - beide müssen auf + // derselben Seite landen, sonst fehlt beim Rendern der Linie der Startpunkt. + if (command is MoveTo) pendingLinePage = page; + else if (command is LineTo) page = pendingLinePage; + pages[page].Add(ShiftY(command, page * pageHeight)); + } + return pages.Select(commands => new DrawingValue(commands, pageHeight)).ToList(); } + private static float? CommandY(DrawingCommand command) => command switch + { + DrawString s => s.Y, DrawStringEx s => s.Y, MoveTo m => m.Y, LineTo l => l.Y, + DrawRectangle r => r.Y, DrawImage i => i.Y, DrawLine ln => Math.Min(ln.Y1, ln.Y2), _ => null, + }; + + private static DrawingCommand ShiftY(DrawingCommand command, float dy) => command switch + { + DrawString s => s with { Y = s.Y - dy }, DrawStringEx s => s with { Y = s.Y - dy }, + MoveTo m => m with { Y = m.Y - dy }, LineTo l => l with { Y = l.Y - dy }, + DrawRectangle r => r with { Y = r.Y - dy }, DrawImage i => i with { Y = i.Y - dy }, + DrawLine ln => ln with { Y1 = ln.Y1 - dy, Y2 = ln.Y2 - dy }, + _ => command, + }; + internal static List> RecordPages(PagedDrawingValue value, float width, float height, int maxPages) {