TemplateDesigner: Redesign des TemplateSkripts

This commit is contained in:
2026-09-01 10:07:25 +02:00
parent 95e3f677e4
commit b2d0ccd30d
12 changed files with 807 additions and 55 deletions
+152 -1
View File
@@ -15,9 +15,14 @@ public sealed class QuestTemplateRenderer : ITemplateRenderer, ITemplatePreviewR
BuildDocument(template, ValidateData(template, data)).GeneratePdf();
public byte[] RenderFirstPageToPng(LoadedTemplate template, ITemplateDataProvider data, int dpi = 120)
{
return RenderPagesToPng(template, data, dpi).First();
}
public IReadOnlyList<byte[]> RenderPagesToPng(LoadedTemplate template, ITemplateDataProvider data, int dpi = 120)
{
var settings = new ImageGenerationSettings { ImageFormat = ImageFormat.Png, RasterDpi = dpi };
return BuildDocument(template, ValidateData(template, data)).GenerateImages(settings).First();
return BuildDocument(template, ValidateData(template, data)).GenerateImages(settings).ToList();
}
private static IReadOnlyDictionary<string, PlaceholderValue> ValidateData(LoadedTemplate template,
@@ -30,6 +35,11 @@ public sealed class QuestTemplateRenderer : ITemplateRenderer, ITemplatePreviewR
}
private static IDocument BuildDocument(LoadedTemplate template,
IReadOnlyDictionary<string, PlaceholderValue> values) => template.Layout.UsesPageTemplates
? BuildFlowDocument(template, values)
: BuildLegacyDocument(template, values);
private static IDocument BuildLegacyDocument(LoadedTemplate template,
IReadOnlyDictionary<string, PlaceholderValue> values) => Document.Create(document =>
{
document.Page(page =>
@@ -50,6 +60,147 @@ public sealed class QuestTemplateRenderer : ITemplateRenderer, ITemplatePreviewR
});
});
private static IDocument BuildFlowDocument(LoadedTemplate template,
IReadOnlyDictionary<string, PlaceholderValue> values)
{
var firstPage = template.Layout.PageTemplates.First(x =>
x.Name.Equals("first", StringComparison.OrdinalIgnoreCase));
var continuationPage = template.Layout.PageTemplates.FirstOrDefault(x =>
x.Name.Equals("continuation", StringComparison.OrdinalIgnoreCase)) ?? firstPage;
var populatedFlows = template.Layout.ContentFlows.Where(x => x.Elements.Count > 0).ToList();
if (populatedFlows.Count > 1)
throw new InvalidDataException("Aktuell darf genau ein Content-Flow Inhalt enthalten. Weitere Flow-Slots können bereits gestaltet werden, parallele paginierende Flows folgen in einer späteren Formatstufe.");
var primaryFlow = populatedFlows.FirstOrDefault() ?? template.Layout.ContentFlows.FirstOrDefault();
return Document.Create(document =>
{
document.Page(page =>
{
var pageWidth = UnitConverter.Points(template.Layout.Width, template.Layout.Unit);
var pageHeight = UnitConverter.Points(template.Layout.Height, template.Layout.Unit);
page.Size(pageWidth, pageHeight);
page.Margin(0);
ConfigurePageCanvas(page.Background(), firstPage, continuationPage, template, values);
if (primaryFlow is null)
{
page.Content().Height(1);
return;
}
var firstSlot = FindSlot(firstPage, primaryFlow.Name);
var continuationSlot = FindSlot(continuationPage, primaryFlow.Name) ?? firstSlot;
if (firstSlot is null)
throw new InvalidDataException($"Für content-flow „{primaryFlow.Name}“ fehlt ein flow-slot auf der ersten Seite.");
if (Math.Abs(firstSlot.X - continuationSlot!.X) > 0.01f ||
Math.Abs(firstSlot.Width - continuationSlot.Width) > 0.01f)
throw new InvalidDataException($"Der primäre Flow „{primaryFlow.Name}“ muss vorerst auf erster und Folgeseite dieselbe X-Position und Breite besitzen.");
var left = UnitConverter.Points(firstSlot.X, template.Layout.Unit);
var right = UnitConverter.Points(template.Layout.Width - firstSlot.X - firstSlot.Width, template.Layout.Unit);
page.MarginLeft(left);
page.MarginRight(right);
ConfigureVariableVerticalSlot(page, firstSlot, continuationSlot, template.Layout);
page.Content().Column(column => RenderFlowColumn(column, primaryFlow.Elements, template, values));
});
});
}
private static void ConfigureVariableVerticalSlot(PageDescriptor page, FlowSlotDefinition first,
FlowSlotDefinition continuation, TemplateLayout layout)
{
var firstTop = UnitConverter.Points(first.Y, layout.Unit);
var continuationTop = UnitConverter.Points(continuation.Y, layout.Unit);
var firstBottom = UnitConverter.Points(layout.Height - first.Y - first.Height, layout.Unit);
var continuationBottom = UnitConverter.Points(layout.Height - continuation.Y - continuation.Height, layout.Unit);
page.Header().Column(column =>
{
column.Item().ShowOnce().Height(firstTop);
column.Item().SkipOnce().Height(continuationTop);
});
page.Footer().Column(column =>
{
column.Item().ShowOnce().Height(firstBottom);
column.Item().SkipOnce().Height(continuationBottom);
});
}
private static FlowSlotDefinition? FindSlot(PageTemplateDefinition page, string name) =>
page.FlowSlots.FirstOrDefault(x => x.Name.Equals(name, StringComparison.OrdinalIgnoreCase));
private static void ConfigurePageCanvas(IContainer canvas, PageTemplateDefinition first,
PageTemplateDefinition continuation, LoadedTemplate template,
IReadOnlyDictionary<string, PlaceholderValue> values)
{
canvas.Layers(layers =>
{
layers.PrimaryLayer().Background(Colors.White);
foreach (var element in first.Elements)
{
var current = element;
layers.Layer().ShowOnce().Element(root => RenderElement(root, current, template, values));
}
foreach (var element in continuation.Elements)
{
var current = element;
layers.Layer().SkipOnce().Element(root => RenderElement(root, current, template, values));
}
});
}
private static void RenderFlowColumn(ColumnDescriptor column, IReadOnlyList<TemplateElement> elements,
LoadedTemplate template, IReadOnlyDictionary<string, PlaceholderValue> values)
{
for (var index = 0; index < elements.Count; index++)
{
var element = elements[index];
var gap = UnitConverter.Points(ParseFloat(element.Attributes, "gap", 0), template.Layout.Unit);
var keepWithNext = ParseBool(element.Attributes, "keep-with-next") && index + 1 < elements.Count;
if (keepWithNext)
{
var next = elements[++index];
column.Item().PaddingTop(gap).PreventPageBreak().Column(group =>
{
group.Item().Element(container => RenderFlowElement(container, element, template, values));
var nextGap = UnitConverter.Points(ParseFloat(next.Attributes, "gap", 0), template.Layout.Unit);
group.Item().PaddingTop(nextGap).Element(container => RenderFlowElement(container, next, template, values));
});
}
else
column.Item().PaddingTop(gap).Element(container => RenderFlowElement(container, element, template, values));
}
}
private static void RenderFlowElement(IContainer container, TemplateElement element, LoadedTemplate template,
IReadOnlyDictionary<string, PlaceholderValue> values)
{
switch (element)
{
case TextElement text:
RenderResolvedText(container, text.Content, text.Placeholder, text.Format,
values, template.Manifest, text.Attributes);
break;
case TextBoxElement box:
RenderResolvedText(container, box.Content, box.Placeholder, box.Format,
values, template.Manifest, box.Attributes);
break;
case ImageElement image:
var imageContainer = container;
if (image.Width > 0) imageContainer = imageContainer.Width(UnitConverter.Points(image.Width, template.Layout.Unit));
if (image.Height > 0) imageContainer = imageContainer.Height(UnitConverter.Points(image.Height, template.Layout.Unit));
imageContainer.Image(GetAsset(template, image.Path)).FitArea();
break;
case TableElement table when values.GetValueOrDefault(table.Placeholder) is TableValue tableValue:
TableElementRenderer.Render(container, tableValue, table.Attributes);
break;
case ChartElement chart when values.GetValueOrDefault(chart.Placeholder) is ChartValue chartValue:
var chartHeight = UnitConverter.Points(ParseFloat(chart.Attributes, "h", 50), template.Layout.Unit);
ChartElementRenderer.Render(container.Height(chartHeight), chartValue, chart.ChartType, chart.Attributes);
break;
}
}
private static void RenderElement(IContainer root, TemplateElement element, LoadedTemplate template,
IReadOnlyDictionary<string, PlaceholderValue> values)
{