@@ -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<DrawingCommand>
|
||||
{
|
||||
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<string, byte[]>());
|
||||
var provider = new FakeProvider(new Dictionary<string, PlaceholderValue> { ["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<string, PlaceholderValue> values) : ITemplateDataProvider
|
||||
{ public IReadOnlyDictionary<string, PlaceholderValue> GetValues() => values; }
|
||||
}
|
||||
@@ -3,6 +3,7 @@
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\LehrerApp.Templating\LehrerApp.Templating.csproj" />
|
||||
<PackageReference Include="Microsoft.NET.Test.Sdk" />
|
||||
<PackageReference Include="PdfPig" />
|
||||
<PackageReference Include="xunit" />
|
||||
<PackageReference Include="xunit.runner.visualstudio"><PrivateAssets>all</PrivateAssets></PackageReference>
|
||||
</ItemGroup>
|
||||
|
||||
@@ -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<string, byte[]>());
|
||||
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<string, PlaceholderValue> { ["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()
|
||||
{
|
||||
|
||||
@@ -534,18 +534,54 @@ 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<DrawingValue> 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<List<DrawingCommand>>();
|
||||
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<IReadOnlyList<DrawingCommand>> RecordPages(PagedDrawingValue value,
|
||||
float width, float height, int maxPages)
|
||||
|
||||
Reference in New Issue
Block a user