# Conflicts: # LehrerApp.TemplateDesigner.Tests/ProjectLifecycleTests.cs # LehrerApp.TemplateDesigner/DesignerViewModel.cs # LehrerApp.TemplateDesigner/MainWindow.axaml # LehrerApp.Templating/LayoutParser.cs # LehrerApp.Templating/QuestTemplateRenderer.cs
This commit is contained in:
@@ -35,30 +35,100 @@ 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);
|
||||
IReadOnlyDictionary<string, PlaceholderValue> values)
|
||||
{
|
||||
values = PreparePagedDrawings(template, values);
|
||||
return template.Layout.UsesPageTemplates
|
||||
? BuildFlowDocument(template, values)
|
||||
: BuildLegacyDocument(template, values);
|
||||
}
|
||||
|
||||
private static IDocument BuildLegacyDocument(LoadedTemplate template,
|
||||
IReadOnlyDictionary<string, PlaceholderValue> values) => Document.Create(document =>
|
||||
IReadOnlyDictionary<string, PlaceholderValue> values)
|
||||
{
|
||||
document.Page(page =>
|
||||
var flowBox = template.Layout.Elements.SingleOrDefault(x => x is FlowBoxElement or FlowDrawBoxElement);
|
||||
return Document.Create(document => document.Page(page =>
|
||||
{
|
||||
page.Size(UnitConverter.Points(template.Layout.Width, template.Layout.Unit),
|
||||
UnitConverter.Points(template.Layout.Height, template.Layout.Unit));
|
||||
page.Margin(0);
|
||||
page.Content().Layers(layers =>
|
||||
{
|
||||
layers.PrimaryLayer().Width(UnitConverter.Points(template.Layout.Width, template.Layout.Unit))
|
||||
.Height(UnitConverter.Points(template.Layout.Height, template.Layout.Unit)).Background(Colors.White);
|
||||
foreach (var element in template.Layout.Elements)
|
||||
{
|
||||
var current = element;
|
||||
layers.Layer().Element(container => RenderElement(container, current, template, values));
|
||||
}
|
||||
if (flowBox is null)
|
||||
layers.PrimaryLayer().Width(UnitConverter.Points(template.Layout.Width, template.Layout.Unit))
|
||||
.Height(UnitConverter.Points(template.Layout.Height, template.Layout.Unit)).Background(Colors.White);
|
||||
else
|
||||
RenderLegacyFlowElement(layers.PrimaryLayer(), flowBox, template, values);
|
||||
|
||||
RenderStaticLayers(layers, template.Layout, template, values,
|
||||
template.ContinuationLayout is null ? null : static container => container.ShowOnce());
|
||||
if (template.ContinuationLayout is { } continuation)
|
||||
RenderStaticLayers(layers, continuation, template, values, static container => container.SkipOnce());
|
||||
});
|
||||
});
|
||||
});
|
||||
}));
|
||||
}
|
||||
|
||||
private static IReadOnlyDictionary<string, PlaceholderValue> PreparePagedDrawings(LoadedTemplate template,
|
||||
IReadOnlyDictionary<string, PlaceholderValue> values)
|
||||
{
|
||||
var result = new Dictionary<string, PlaceholderValue>(values, StringComparer.Ordinal);
|
||||
var layouts = template.ContinuationLayout is null
|
||||
? new[] { template.Layout }
|
||||
: new[] { template.Layout, template.ContinuationLayout };
|
||||
var drawingElements = layouts.SelectMany(x => x.Elements)
|
||||
.Where(x => x is DrawBoxElement or FlowDrawBoxElement).ToList();
|
||||
foreach (var group in drawingElements.GroupBy(x => x switch
|
||||
{ DrawBoxElement box => box.Placeholder, FlowDrawBoxElement box => box.Placeholder, _ => "" }))
|
||||
{
|
||||
if (values.GetValueOrDefault(group.Key) is not PagedDrawingValue) continue;
|
||||
var signatures = group.Select(x => (x.GetType(), x.Width, x.Height)).Distinct().Count();
|
||||
if (signatures > 1)
|
||||
throw new InvalidDataException($"Der Callback-Platzhalter „{group.Key}“ wird in unterschiedlich großen oder unterschiedlichen Zeichenboxen verwendet.");
|
||||
}
|
||||
foreach (var element in drawingElements)
|
||||
{
|
||||
var placeholder = element switch
|
||||
{ DrawBoxElement box => box.Placeholder, FlowDrawBoxElement box => box.Placeholder, _ => null };
|
||||
if (placeholder is null || values.GetValueOrDefault(placeholder) is not PagedDrawingValue callback) continue;
|
||||
if (result.GetValueOrDefault(placeholder) is RecordedDrawingValue) continue;
|
||||
var pages = element is FlowDrawBoxElement
|
||||
? DrawingElementRenderer.RecordPages(callback, element.Width, element.Height, callback.MaxPages)
|
||||
: [DrawingElementRenderer.RecordSingle(callback, element.Width, element.Height)];
|
||||
result[placeholder] = new RecordedDrawingValue(pages);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
private static void RenderStaticLayers(LayersDescriptor layers, TemplateLayout layout, LoadedTemplate template,
|
||||
IReadOnlyDictionary<string, PlaceholderValue> values, Func<IContainer, IContainer>? visibility)
|
||||
{
|
||||
foreach (var element in layout.Elements.Where(x => x is not (FlowBoxElement or FlowDrawBoxElement)))
|
||||
{
|
||||
var current = element;
|
||||
layers.Layer().Element(container => RenderElement(
|
||||
visibility is null ? container : visibility(container), current, template, values));
|
||||
}
|
||||
}
|
||||
|
||||
private static void RenderLegacyFlowElement(IContainer container, TemplateElement element, LoadedTemplate template,
|
||||
IReadOnlyDictionary<string, PlaceholderValue> values)
|
||||
{
|
||||
var unit = template.Layout.Unit;
|
||||
container
|
||||
.PaddingLeft(UnitConverter.Points(element.X, unit))
|
||||
.PaddingTop(UnitConverter.Points(element.Y, unit))
|
||||
.PaddingRight(UnitConverter.Points(Math.Max(0, template.Layout.Width - element.X - element.Width), unit))
|
||||
.PaddingBottom(UnitConverter.Points(Math.Max(0, template.Layout.Height - element.Y - element.Height), unit))
|
||||
.Element(content =>
|
||||
{
|
||||
if (element is FlowBoxElement box)
|
||||
RenderResolvedText(content, box.Content, box.Placeholder, box.Format,
|
||||
values, template.Manifest, box.Attributes);
|
||||
else if (element is FlowDrawBoxElement drawing
|
||||
&& values.GetValueOrDefault(drawing.Placeholder) is { } drawingValue)
|
||||
DrawingElementRenderer.RenderFlow(content, drawingValue, drawing.Width, drawing.Height, unit);
|
||||
});
|
||||
}
|
||||
|
||||
private static IDocument BuildFlowDocument(LoadedTemplate template,
|
||||
IReadOnlyDictionary<string, PlaceholderValue> values)
|
||||
@@ -185,6 +255,20 @@ public sealed class QuestTemplateRenderer : ITemplateRenderer, ITemplatePreviewR
|
||||
RenderResolvedText(container, box.Content, box.Placeholder, box.Format,
|
||||
values, template.Manifest, box.Attributes);
|
||||
break;
|
||||
case FlowBoxElement box:
|
||||
RenderResolvedText(container, box.Content, box.Placeholder, box.Format,
|
||||
values, template.Manifest, box.Attributes);
|
||||
break;
|
||||
case DrawBoxElement drawing when values.GetValueOrDefault(drawing.Placeholder) is { } drawingValue:
|
||||
DrawingElementRenderer.RenderFixed(
|
||||
container.Width(UnitConverter.Points(drawing.Width, template.Layout.Unit))
|
||||
.Height(UnitConverter.Points(drawing.Height, template.Layout.Unit)),
|
||||
drawingValue, drawing.Width, drawing.Height, template.Layout.Unit);
|
||||
break;
|
||||
case FlowDrawBoxElement drawing when values.GetValueOrDefault(drawing.Placeholder) is { } drawingValue:
|
||||
DrawingElementRenderer.RenderFlow(container, drawingValue,
|
||||
drawing.Width, drawing.Height, template.Layout.Unit);
|
||||
break;
|
||||
case ImageElement image:
|
||||
var imageContainer = container;
|
||||
if (image.Width > 0) imageContainer = imageContainer.Width(UnitConverter.Points(image.Width, template.Layout.Unit));
|
||||
@@ -229,6 +313,20 @@ public sealed class QuestTemplateRenderer : ITemplateRenderer, ITemplatePreviewR
|
||||
RenderResolvedText(Position(root, box, unit).Shrink(), box.Content, box.Placeholder, box.Format,
|
||||
values, template.Manifest, box.Attributes);
|
||||
break;
|
||||
case FlowBoxElement box:
|
||||
RenderResolvedText(Position(root, box, unit).Shrink(), box.Content, box.Placeholder, box.Format,
|
||||
values, template.Manifest, box.Attributes);
|
||||
break;
|
||||
case DrawBoxElement drawing:
|
||||
if (values.GetValueOrDefault(drawing.Placeholder) is { } drawingValue)
|
||||
DrawingElementRenderer.RenderFixed(Position(root, drawing, unit), drawingValue,
|
||||
drawing.Width, drawing.Height, unit);
|
||||
break;
|
||||
case FlowDrawBoxElement drawing:
|
||||
if (values.GetValueOrDefault(drawing.Placeholder) is { } flowDrawingValue)
|
||||
DrawingElementRenderer.RenderFixed(Position(root, drawing, unit), flowDrawingValue,
|
||||
drawing.Width, drawing.Height, unit);
|
||||
break;
|
||||
case TableElement table:
|
||||
if (values.GetValueOrDefault(table.Placeholder) is TableValue tableValue)
|
||||
TableElementRenderer.Render(Position(root, table, unit), tableValue, table.Attributes);
|
||||
@@ -252,6 +350,15 @@ public sealed class QuestTemplateRenderer : ITemplateRenderer, ITemplatePreviewR
|
||||
{
|
||||
var aligned = attributes.GetValueOrDefault("align", "left").ToLowerInvariant() switch
|
||||
{ "center" => container.AlignCenter(), "right" => container.AlignRight(), _ => container.AlignLeft() };
|
||||
if (SystemVariables.Contains(content))
|
||||
{
|
||||
aligned.Text(text =>
|
||||
{
|
||||
text.DefaultTextStyle(style => ApplyTextStyle(style, attributes));
|
||||
AppendSystemVariables(text, content);
|
||||
});
|
||||
return;
|
||||
}
|
||||
var descriptor = aligned.Text(content);
|
||||
descriptor.FontSize(ParseFloat(attributes, "size", 11));
|
||||
if (ParseBool(attributes, "bold")) descriptor.SemiBold();
|
||||
@@ -291,15 +398,42 @@ public sealed class QuestTemplateRenderer : ITemplateRenderer, ITemplatePreviewR
|
||||
{
|
||||
var content = run.Placeholder is null ? run.Text
|
||||
: values.TryGetValue(run.Placeholder, out var value) ? Format(value, run.Format) : "";
|
||||
var span = text.Span(content).FontSize(fontSize);
|
||||
if (globalBold || run.Bold) span.SemiBold();
|
||||
if (globalItalic || run.Italic) span.Italic();
|
||||
if (globalUnderline || run.Underline) span.Underline();
|
||||
if (color is not null) span.FontColor(color);
|
||||
foreach (var span in AppendSystemVariables(text, content))
|
||||
{
|
||||
span.FontSize(fontSize);
|
||||
if (globalBold || run.Bold) span.SemiBold();
|
||||
if (globalItalic || run.Italic) span.Italic();
|
||||
if (globalUnderline || run.Underline) span.Underline();
|
||||
if (color is not null) span.FontColor(color);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private static TextStyle ApplyTextStyle(TextStyle style, IReadOnlyDictionary<string, string> attributes)
|
||||
{
|
||||
style = style.FontSize(ParseFloat(attributes, "size", 11));
|
||||
if (ParseBool(attributes, "bold")) style = style.SemiBold();
|
||||
if (ParseBool(attributes, "italic")) style = style.Italic();
|
||||
if (ParseBool(attributes, "underline")) style = style.Underline();
|
||||
if (attributes.TryGetValue("color", out var color)) style = style.FontColor(color);
|
||||
return style;
|
||||
}
|
||||
|
||||
private static IEnumerable<TextSpanDescriptor> AppendSystemVariables(TextDescriptor text, string content)
|
||||
{
|
||||
foreach (var part in SystemVariables.Split(content))
|
||||
{
|
||||
yield return part.Name switch
|
||||
{
|
||||
SystemVariables.CurrentPage => text.CurrentPageNumber(),
|
||||
SystemVariables.MaximumPageNumber => text.TotalPages(),
|
||||
SystemVariables.Today => text.Span(DateTime.Today.ToString("d", CultureInfo.GetCultureInfo("de-DE"))),
|
||||
_ => text.Span(part.Text),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
private static IReadOnlyDictionary<string, string> TextAttributes(TemplateManifest manifest, string? placeholder,
|
||||
IReadOnlyDictionary<string, string> elementAttributes)
|
||||
{
|
||||
@@ -340,6 +474,9 @@ public sealed class QuestTemplateRenderer : ITemplateRenderer, ITemplatePreviewR
|
||||
attributes.TryGetValue(key, out var raw) && bool.TryParse(raw, out var value) && value;
|
||||
}
|
||||
|
||||
internal sealed record RecordedDrawingValue(IReadOnlyList<IReadOnlyList<DrawingCommand>> Pages)
|
||||
: PlaceholderValue(PlaceholderType.Drawing);
|
||||
|
||||
public static class UnitConverter
|
||||
{
|
||||
public static float Points(float value, string unit) => unit.ToLowerInvariant() switch
|
||||
@@ -370,6 +507,186 @@ internal static class TableElementRenderer
|
||||
}
|
||||
}
|
||||
|
||||
internal static class DrawingElementRenderer
|
||||
{
|
||||
public static void RenderFixed(IContainer container, PlaceholderValue value, float width, float height, string unit)
|
||||
{
|
||||
var drawing = value switch
|
||||
{
|
||||
DrawingValue direct => direct,
|
||||
RecordedDrawingValue recorded => new DrawingValue(recorded.Pages.FirstOrDefault() ?? [], height),
|
||||
_ => throw new InvalidDataException("DRAWBOX erwartet einen Drawing-Platzhalter."),
|
||||
};
|
||||
container.Svg(BuildSvg(drawing, width, height, 0, unit));
|
||||
}
|
||||
|
||||
public static void RenderFlow(IContainer container, PlaceholderValue value, float width, float pageHeight, string unit)
|
||||
{
|
||||
var pages = value switch
|
||||
{
|
||||
DrawingValue direct => Slice(direct, pageHeight),
|
||||
RecordedDrawingValue recorded => recorded.Pages
|
||||
.Select(commands => new DrawingValue(commands, pageHeight)).ToList(),
|
||||
_ => throw new InvalidDataException("FLOWDRAWBOX erwartet einen Drawing-Platzhalter."),
|
||||
};
|
||||
container.Column(column =>
|
||||
{
|
||||
for (var page = 0; page < pages.Count; page++)
|
||||
{
|
||||
if (page > 0) column.Item().PageBreak();
|
||||
var offset = value is DrawingValue ? page * pageHeight : 0;
|
||||
column.Item().Height(UnitConverter.Points(pageHeight, unit)).Svg(
|
||||
BuildSvg(pages[page], width, pageHeight, offset, unit));
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
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();
|
||||
}
|
||||
|
||||
internal static List<IReadOnlyList<DrawingCommand>> RecordPages(PagedDrawingValue value,
|
||||
float width, float height, int maxPages)
|
||||
{
|
||||
if (maxPages is < 1 or > 10_000)
|
||||
throw new InvalidDataException("PagedDrawingValue.MaxPages muss zwischen 1 und 10000 liegen.");
|
||||
var pages = new List<IReadOnlyList<DrawingCommand>>();
|
||||
var state = value.InitialState;
|
||||
for (var pageNumber = 1; pageNumber <= maxPages; pageNumber++)
|
||||
{
|
||||
var canvas = new DrawingCanvas();
|
||||
var context = new DrawingPageContext(width, height, pageNumber, canvas, state);
|
||||
var finished = value.DrawPage(context);
|
||||
pages.Add(canvas.Commands.ToList());
|
||||
state = context.State;
|
||||
if (finished) return pages;
|
||||
}
|
||||
throw new InvalidDataException($"Der Zeichen-Callback war nach {maxPages} Seiten noch nicht beendet.");
|
||||
}
|
||||
|
||||
internal static IReadOnlyList<DrawingCommand> RecordSingle(PagedDrawingValue value, float width, float height)
|
||||
{
|
||||
var canvas = new DrawingCanvas();
|
||||
value.DrawPage(new DrawingPageContext(width, height, 1, canvas, value.InitialState));
|
||||
return canvas.Commands.ToList();
|
||||
}
|
||||
|
||||
private static string BuildSvg(DrawingValue value, float width, float height, float verticalOffset, string unit)
|
||||
{
|
||||
if (!float.IsFinite(value.ContentHeight) || value.ContentHeight < 0)
|
||||
throw new InvalidDataException("DrawingValue.ContentHeight muss eine endliche, nichtnegative Zahl sein.");
|
||||
if (value.Commands.Count > 100_000)
|
||||
throw new InvalidDataException("DrawingValue enthält zu viele Zeichenbefehle.");
|
||||
var svg = new StringBuilder();
|
||||
svg.Append(CultureInfo.InvariantCulture,
|
||||
$"<svg xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 {verticalOffset} {width} {height}\" overflow=\"hidden\">");
|
||||
float? currentX = null, currentY = null;
|
||||
foreach (var command in value.Commands)
|
||||
{
|
||||
switch (command)
|
||||
{
|
||||
case MoveTo move:
|
||||
Validate(move.X, move.Y); currentX = move.X; currentY = move.Y;
|
||||
break;
|
||||
case LineTo line when currentX is not null && currentY is not null:
|
||||
Validate(line.X, line.Y, line.StrokeWidth); Positive(line.StrokeWidth);
|
||||
AppendLine(svg, currentX.Value, currentY.Value, line.X, line.Y, line.Color, line.StrokeWidth);
|
||||
currentX = line.X; currentY = line.Y;
|
||||
break;
|
||||
case LineTo:
|
||||
throw new InvalidDataException("LineTo benötigt ein vorheriges MoveTo.");
|
||||
case DrawLine line:
|
||||
Validate(line.X1, line.Y1, line.X2, line.Y2, line.StrokeWidth); Positive(line.StrokeWidth);
|
||||
AppendLine(svg, line.X1, line.Y1, line.X2, line.Y2, line.Color, line.StrokeWidth);
|
||||
break;
|
||||
case DrawRectangle rectangle:
|
||||
Validate(rectangle.X, rectangle.Y, rectangle.Width, rectangle.Height, rectangle.StrokeWidth);
|
||||
Positive(rectangle.Width, rectangle.Height, rectangle.StrokeWidth);
|
||||
svg.Append(CultureInfo.InvariantCulture,
|
||||
$"<rect x=\"{rectangle.X}\" y=\"{rectangle.Y}\" width=\"{rectangle.Width}\" height=\"{rectangle.Height}\" stroke=\"{Attribute(rectangle.StrokeColor)}\" stroke-width=\"{rectangle.StrokeWidth}\" fill=\"{Attribute(rectangle.FillColor)}\"/>");
|
||||
break;
|
||||
case DrawString text:
|
||||
Validate(text.X, text.Y, text.FontSize);
|
||||
Positive(text.FontSize);
|
||||
var localFontSize = text.FontSize / UnitConverter.Points(1, unit);
|
||||
svg.Append(CultureInfo.InvariantCulture,
|
||||
$"<text x=\"{text.X}\" y=\"{text.Y + localFontSize}\" font-size=\"{localFontSize}\" fill=\"{Attribute(text.Color)}\" font-family=\"{FontFamily(text.FontFamily)}\" font-weight=\"{(text.Bold ? "600" : "400")}\" font-style=\"{(text.Italic ? "italic" : "normal")}\">{SecurityElement.Escape(text.Text)}</text>");
|
||||
break;
|
||||
case DrawStringEx text:
|
||||
Validate(text.X, text.Y, text.Height, text.Width, text.FontSize);
|
||||
Positive(text.Height, text.Width, text.FontSize);
|
||||
AppendTextBox(svg, text, unit);
|
||||
break;
|
||||
case DrawImage image:
|
||||
Validate(image.X, image.Y, image.Width, image.Height);
|
||||
Positive(image.Width, image.Height);
|
||||
if (image.MimeType is not ("image/png" or "image/jpeg"))
|
||||
throw new InvalidDataException("DrawImage unterstützt nur PNG und JPEG.");
|
||||
if (image.Data.Length > 20 * 1024 * 1024)
|
||||
throw new InvalidDataException("Ein DrawImage-Bild darf höchstens 20 MB groß sein.");
|
||||
svg.Append(CultureInfo.InvariantCulture,
|
||||
$"<image x=\"{image.X}\" y=\"{image.Y}\" width=\"{image.Width}\" height=\"{image.Height}\" href=\"data:{image.MimeType};base64,{Convert.ToBase64String(image.Data)}\" preserveAspectRatio=\"xMidYMid meet\"/>");
|
||||
break;
|
||||
default: throw new InvalidDataException($"Unbekannter Zeichenbefehl {command.GetType().Name}.");
|
||||
}
|
||||
}
|
||||
return svg.Append("</svg>").ToString();
|
||||
}
|
||||
|
||||
private static void AppendLine(StringBuilder svg, float x1, float y1, float x2, float y2,
|
||||
string color, float strokeWidth) => svg.Append(CultureInfo.InvariantCulture,
|
||||
$"<line x1=\"{x1}\" y1=\"{y1}\" x2=\"{x2}\" y2=\"{y2}\" stroke=\"{Attribute(color)}\" stroke-width=\"{strokeWidth}\"/>");
|
||||
|
||||
private static string Attribute(string value)
|
||||
{
|
||||
if (value != "none" && !System.Text.RegularExpressions.Regex.IsMatch(value,
|
||||
@"^(#[0-9a-fA-F]{3,8}|[a-zA-Z]{1,24})$"))
|
||||
throw new InvalidDataException($"Ungültiger Zeichenfarbwert „{value}“.");
|
||||
return value;
|
||||
}
|
||||
|
||||
private static string FontFamily(string value)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(value)) return "sans-serif";
|
||||
if (!System.Text.RegularExpressions.Regex.IsMatch(value, @"^[\p{L}\p{N} _.,-]{1,80}$"))
|
||||
throw new InvalidDataException($"Ungültige Schriftfamilie „{value}“.");
|
||||
return SecurityElement.Escape(value) ?? "sans-serif";
|
||||
}
|
||||
|
||||
private static void AppendTextBox(StringBuilder svg, DrawStringEx text, string unit)
|
||||
{
|
||||
var fontSize = text.FontSize / UnitConverter.Points(1, unit);
|
||||
var (x, anchor) = text.Alignment switch
|
||||
{
|
||||
DrawingTextAlignment.AlignCenter => (text.X + text.Width / 2, "middle"),
|
||||
DrawingTextAlignment.AlignRight => (text.X + text.Width, "end"),
|
||||
_ => (text.X, "start"),
|
||||
};
|
||||
var clipId = "clip" + Guid.NewGuid().ToString("N");
|
||||
svg.Append(CultureInfo.InvariantCulture,
|
||||
$"<defs><clipPath id=\"{clipId}\"><rect x=\"{text.X}\" y=\"{text.Y}\" width=\"{text.Width}\" height=\"{text.Height}\"/></clipPath></defs>");
|
||||
svg.Append(CultureInfo.InvariantCulture,
|
||||
$"<text x=\"{x}\" y=\"{text.Y + fontSize}\" text-anchor=\"{anchor}\" clip-path=\"url(#{clipId})\" font-size=\"{fontSize}\" fill=\"{Attribute(text.Color)}\" font-family=\"{FontFamily(text.FontFamily)}\" font-weight=\"{(text.Bold ? "600" : "400")}\" font-style=\"{(text.Italic ? "italic" : "normal")}\">");
|
||||
var lines = text.Text.Replace("\r\n", "\n", StringComparison.Ordinal).Split('\n');
|
||||
for (var index = 0; index < lines.Length; index++)
|
||||
svg.Append(CultureInfo.InvariantCulture,
|
||||
$"<tspan x=\"{x}\" dy=\"{(index == 0 ? 0 : fontSize * 1.2f)}\">{SecurityElement.Escape(lines[index])}</tspan>");
|
||||
svg.Append("</text>");
|
||||
}
|
||||
|
||||
private static void Validate(params float[] values)
|
||||
{
|
||||
if (values.Any(x => !float.IsFinite(x))) throw new InvalidDataException("Zeichenkoordinaten müssen endlich sein.");
|
||||
}
|
||||
|
||||
private static void Positive(params float[] values)
|
||||
{
|
||||
if (values.Any(x => x < 0)) throw new InvalidDataException("Zeichengrößen dürfen nicht negativ sein.");
|
||||
}
|
||||
}
|
||||
|
||||
internal static class ChartElementRenderer
|
||||
{
|
||||
public static void Render(IContainer container, ChartValue value, string chartType,
|
||||
|
||||
Reference in New Issue
Block a user