Files
LehrerApp/LehrerApp.Templating/Model.cs
admin ab1feba0f0
CI / build-and-test (push) Canceled after 0s
Merge remote-tracking branch 'origin/main'
# Conflicts:
#	LehrerApp.TemplateDesigner.Tests/ProjectLifecycleTests.cs
#	LehrerApp.TemplateDesigner/DesignerViewModel.cs
#	LehrerApp.TemplateDesigner/MainWindow.axaml
#	LehrerApp.Templating/LayoutParser.cs
#	LehrerApp.Templating/QuestTemplateRenderer.cs
2026-09-01 10:15:54 +02:00

196 lines
11 KiB
C#

using System.Text.Json.Serialization;
namespace LehrerApp.Templating;
public enum PlaceholderType { Text, Multiline, Date, Number, Image, Table, Chart, Drawing }
public abstract record PlaceholderValue(PlaceholderType Type);
public sealed record TextValue(string Value) : PlaceholderValue(PlaceholderType.Text);
public sealed record MultilineValue(string Value) : PlaceholderValue(PlaceholderType.Multiline);
public sealed record DateValue(DateOnly Value) : PlaceholderValue(PlaceholderType.Date);
public sealed record NumberValue(decimal Value) : PlaceholderValue(PlaceholderType.Number);
public sealed record ImageValue(byte[] Data, string MimeType) : PlaceholderValue(PlaceholderType.Image);
public sealed record ImageMetadata(int Width, int Height);
public sealed record TableValue(IReadOnlyList<string> Columns, IReadOnlyList<IReadOnlyList<string>> Rows)
: PlaceholderValue(PlaceholderType.Table);
public sealed record ChartPoint(string X, decimal Y);
public sealed record ChartSeries(string Label, IReadOnlyList<ChartPoint> Points);
public sealed record ChartValue(IReadOnlyList<ChartSeries> Series) : PlaceholderValue(PlaceholderType.Chart);
public abstract record DrawingCommand;
public sealed record DrawString(float X, float Y, string Text, float FontSize = 11,
string Color = "#000000", bool Bold = false, bool Italic = false, string FontFamily = "") : DrawingCommand;
public enum DrawingTextAlignment { AlignLeft, AlignCenter, AlignRight }
public sealed record DrawStringEx(float X, float Y, float Height, float Width, string Text,
DrawingTextAlignment Alignment = DrawingTextAlignment.AlignLeft, float FontSize = 11,
string FontFamily = "", string Color = "#000000", bool Bold = false, bool Italic = false)
: DrawingCommand;
public sealed record MoveTo(float X, float Y) : DrawingCommand;
public sealed record LineTo(float X, float Y, string Color = "#000000", float StrokeWidth = 1) : DrawingCommand;
public sealed record DrawLine(float X1, float Y1, float X2, float Y2,
string Color = "#000000", float StrokeWidth = 1) : DrawingCommand;
public sealed record DrawRectangle(float X, float Y, float Width, float Height,
string StrokeColor = "#000000", float StrokeWidth = 1, string FillColor = "none") : DrawingCommand;
public sealed record DrawImage(float X, float Y, float Width, float Height, byte[] Data, string MimeType) : DrawingCommand;
/// <summary>Declarative drawing supplied by an external data provider. Coordinates use the template layout unit; font sizes use points.</summary>
public sealed record DrawingValue(IReadOnlyList<DrawingCommand> Commands, float ContentHeight)
: PlaceholderValue(PlaceholderType.Drawing);
/// <summary>A recorded drawing surface handed to an in-process external renderer.</summary>
public sealed class DrawingCanvas
{
private readonly List<DrawingCommand> _commands = [];
public IReadOnlyList<DrawingCommand> Commands => _commands;
public void DrawString(float x, float y, string text, float fontSize = 11, string color = "#000000",
bool bold = false, bool italic = false, string fontFamily = "") =>
_commands.Add(new LehrerApp.Templating.DrawString(x, y, text, fontSize, color, bold, italic, fontFamily));
public void DrawStringEx(float x, float y, float height, float width, string text,
DrawingTextAlignment alignment = DrawingTextAlignment.AlignLeft, float fontSize = 11,
string fontFamily = "", string color = "#000000", bool bold = false, bool italic = false) =>
_commands.Add(new LehrerApp.Templating.DrawStringEx(x, y, height, width, text, alignment,
fontSize, fontFamily, color, bold, italic));
public void MoveTo(float x, float y) => _commands.Add(new LehrerApp.Templating.MoveTo(x, y));
public void LineTo(float x, float y, string color = "#000000", float strokeWidth = 1) =>
_commands.Add(new LehrerApp.Templating.LineTo(x, y, color, strokeWidth));
public void DrawLine(float x1, float y1, float x2, float y2, string color = "#000000", float strokeWidth = 1) =>
_commands.Add(new LehrerApp.Templating.DrawLine(x1, y1, x2, y2, color, strokeWidth));
public void DrawRectangle(float x, float y, float width, float height, string strokeColor = "#000000",
float strokeWidth = 1, string fillColor = "none") =>
_commands.Add(new LehrerApp.Templating.DrawRectangle(x, y, width, height, strokeColor, strokeWidth, fillColor));
public void DrawImage(float x, float y, float width, float height, byte[] data, string mimeType) =>
_commands.Add(new LehrerApp.Templating.DrawImage(x, y, width, height, data, mimeType));
}
public sealed class DrawingPageContext(float width, float height, int pageNumber, DrawingCanvas canvas, object? state)
{
public float Width { get; } = width;
public float Height { get; } = height;
public int PageNumber { get; } = pageNumber;
public DrawingCanvas Canvas { get; } = canvas;
public object? State { get; set; } = state;
}
public delegate bool DrawingPageCallback(DrawingPageContext context);
/// <summary>
/// In-process callback drawing. The callback returns true when finished or false to request another box.
/// State is carried across callbacks. It is recorded before QuestPDF performs layout and is never executed by a package.
/// </summary>
public sealed record PagedDrawingValue(DrawingPageCallback DrawPage, object? InitialState = null,
int MaxPages = 1_000) : PlaceholderValue(PlaceholderType.Drawing);
public interface ITemplateDataProvider
{
IReadOnlyDictionary<string, PlaceholderValue> GetValues();
}
public sealed record PageSizeDefinition(float Width, float Height, string Unit = "mm");
public sealed record PlaceholderDefinition(string Name, PlaceholderType Type, bool Required = false,
bool IsConstant = false, string? ConstantValue = null,
bool Bold = false, bool Italic = false, bool Underline = false);
public sealed class TemplateManifest
{
public int SchemaVersion { get; set; } = 1;
public string Id { get; set; } = "neue-vorlage";
public string Name { get; set; } = "Neue Vorlage";
public string Description { get; set; } = "";
public PageSizeDefinition PageSize { get; set; } = new(210, 297);
public string LayoutFile { get; set; } = "layout.tpl";
public string? ContinuationLayoutFile { get; set; }
public string MetadataFile { get; set; } = TemplateMetadataText.FileName;
[JsonIgnore]
public Dictionary<string, string> Metadata { get; set; } = new(StringComparer.OrdinalIgnoreCase);
public List<PlaceholderDefinition> Placeholders { get; set; } = [];
}
public abstract record TemplateElement(int Line, float X, float Y, float Width, float Height,
IReadOnlyDictionary<string, string> Attributes);
public sealed record BackgroundElement(int Line, string Path)
: TemplateElement(Line, 0, 0, 0, 0, EmptyAttributes.Value);
public sealed record ImageElement(int Line, string Path, float X, float Y, float Width, float Height,
IReadOnlyDictionary<string, string> Attributes)
: TemplateElement(Line, X, Y, Width, Height, Attributes);
public sealed record TextElement(int Line, float X, float Y, string Content, string? Placeholder,
string? Format, IReadOnlyDictionary<string, string> Attributes)
: TemplateElement(Line, X, Y, 0, 0, Attributes);
public sealed record TextBoxElement(int Line, float X, float Y, float Width, float Height,
string Content, string? Placeholder, string? Format, IReadOnlyDictionary<string, string> Attributes)
: TemplateElement(Line, X, Y, Width, Height, Attributes);
public sealed record FlowBoxElement(int Line, float X, float Y, float Width, float Height,
string Content, string? Placeholder, string? Format, IReadOnlyDictionary<string, string> Attributes)
: TemplateElement(Line, X, Y, Width, Height, Attributes);
public sealed record DrawBoxElement(int Line, float X, float Y, float Width, float Height,
string Placeholder, IReadOnlyDictionary<string, string> Attributes)
: TemplateElement(Line, X, Y, Width, Height, Attributes);
public sealed record FlowDrawBoxElement(int Line, float X, float Y, float Width, float Height,
string Placeholder, IReadOnlyDictionary<string, string> Attributes)
: TemplateElement(Line, X, Y, Width, Height, Attributes);
public sealed record TableElement(int Line, float X, float Y, float Width, float Height,
string Placeholder, IReadOnlyDictionary<string, string> Attributes)
: TemplateElement(Line, X, Y, Width, Height, Attributes);
public sealed record ChartElement(int Line, float X, float Y, float Width, float Height,
string Placeholder, string ChartType, IReadOnlyDictionary<string, string> Attributes)
: TemplateElement(Line, X, Y, Width, Height, Attributes);
public sealed record FlowSlotDefinition(int Line, string Name, float X, float Y, float Width, float Height);
public sealed record PageTemplateDefinition(string Name, IReadOnlyList<TemplateElement> Elements,
IReadOnlyList<FlowSlotDefinition> FlowSlots);
public sealed record ContentFlowDefinition(string Name, IReadOnlyList<TemplateElement> Elements);
internal static class EmptyAttributes
{
public static readonly IReadOnlyDictionary<string, string> Value =
new Dictionary<string, string>();
}
public sealed record TemplateLayout(float Width, float Height, string Unit,
IReadOnlyList<TemplateElement> Elements)
{
public int FormatVersion { get; init; } = 1;
public IReadOnlyList<PageTemplateDefinition> PageTemplates { get; init; } = [];
public IReadOnlyList<ContentFlowDefinition> ContentFlows { get; init; } = [];
public bool UsesPageTemplates => PageTemplates.Count > 0 || ContentFlows.Count > 0;
}
public sealed record LoadedTemplate(TemplateManifest Manifest, TemplateLayout Layout,
IReadOnlyDictionary<string, byte[]> Assets, string SourceName = "",
TemplateLayout? ContinuationLayout = null);
public enum ValidationSeverity { Warning, Error }
public sealed record ValidationIssue(ValidationSeverity Severity, string Message, int? Line = null);
public sealed record ValidationResult(IReadOnlyList<ValidationIssue> Issues)
{
public bool IsValid => Issues.All(x => x.Severity != ValidationSeverity.Error);
public static ValidationResult Success { get; } = new([]);
}
public sealed class TemplateValidationException(ValidationResult result)
: Exception(string.Join(Environment.NewLine, result.Issues.Select(x => x.Message)))
{
public ValidationResult Result { get; } = result;
}
public interface ITemplateRenderer
{
byte[] RenderToPdf(LoadedTemplate template, ITemplateDataProvider data);
}
public interface ITemplatePreviewRenderer
{
byte[] RenderFirstPageToPng(LoadedTemplate template, ITemplateDataProvider data, int dpi = 120);
}
public interface ITemplateLoader
{
LoadedTemplate LoadFromPackage(string packagePath);
LoadedTemplate LoadFromPackage(Stream package, string sourceName = "");
ValidationResult Validate(LoadedTemplate template,
IReadOnlyDictionary<string, PlaceholderType> providedTypes);
}
public interface ITemplateMigrator
{
int SourceVersion { get; }
TemplateManifest Migrate(TemplateManifest manifest);
}