@@ -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>
|
<ItemGroup>
|
||||||
<ProjectReference Include="..\LehrerApp.Templating\LehrerApp.Templating.csproj" />
|
<ProjectReference Include="..\LehrerApp.Templating\LehrerApp.Templating.csproj" />
|
||||||
<PackageReference Include="Microsoft.NET.Test.Sdk" />
|
<PackageReference Include="Microsoft.NET.Test.Sdk" />
|
||||||
|
<PackageReference Include="PdfPig" />
|
||||||
<PackageReference Include="xunit" />
|
<PackageReference Include="xunit" />
|
||||||
<PackageReference Include="xunit.runner.visualstudio"><PrivateAssets>all</PrivateAssets></PackageReference>
|
<PackageReference Include="xunit.runner.visualstudio"><PrivateAssets>all</PrivateAssets></PackageReference>
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|||||||
@@ -96,6 +96,43 @@ public sealed class TemplatingTests : IDisposable
|
|||||||
System.Text.Encoding.ASCII.GetString(pdf), @"/Type\s*/Page\b"));
|
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]
|
[Fact]
|
||||||
public void FlowDrawBox_PaginertHohenDeklarativenZeichenraum()
|
public void FlowDrawBox_PaginertHohenDeklarativenZeichenraum()
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -534,18 +534,54 @@ internal static class DrawingElementRenderer
|
|||||||
for (var page = 0; page < pages.Count; page++)
|
for (var page = 0; page < pages.Count; page++)
|
||||||
{
|
{
|
||||||
if (page > 0) column.Item().PageBreak();
|
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(
|
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)
|
private static List<DrawingValue> Slice(DrawingValue value, float pageHeight)
|
||||||
{
|
{
|
||||||
var pageCount = Math.Max(1, (int)Math.Ceiling(Math.Max(0, value.ContentHeight) / 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,
|
internal static List<IReadOnlyList<DrawingCommand>> RecordPages(PagedDrawingValue value,
|
||||||
float width, float height, int maxPages)
|
float width, float height, int maxPages)
|
||||||
|
|||||||
Reference in New Issue
Block a user