using System.Globalization;
using System.Numerics;
using AngleSharp.Dom;
using AngleSharp.Html.Parser;
namespace XFESystem.Browser;
/// <summary>Bounded SVG icon rasterization. No scripts, external references, fonts or filters.</summary>
internal sealed class BrowserSvgRasterizer
{
private readonly BrowserCanvasSurface surface;
private int segments, work, draws;
private readonly Dictionary<string, IElement> references = new(StringComparer.Ordinal);
private readonly record struct Paint(CssColor Fill, CssColor Stroke, double StrokeWidth, double Opacity, bool EvenOdd, CssColor CurrentColor);
private readonly record struct Edge(Vector2 A, Vector2 B);
private BrowserSvgRasterizer(int width, int height) => surface = new(width, height);
internal static BrowserCanvasSurface Decode(byte[] bytes, int maximumPixels)
{
if (bytes.Length > 1_048_576) throw new BrowserLimitException("SVG text exceeds its byte budget.");
var document = new HtmlParser().ParseDocument(System.Text.Encoding.UTF8.GetString(bytes));
var svg = document.QuerySelector("svg") ?? throw new IOException("The SVG root is missing.");
double[] view = Numbers(svg.GetAttribute("viewBox") ?? "").ToArray();
double width = Dimension(svg.GetAttribute("width"), view.Length == 4 ? view[2] : 300);
double height = Dimension(svg.GetAttribute("height"), view.Length == 4 ? view[3] : 150);
if (!double.IsFinite(width + height) || width <= 0 || height <= 0 || width > 4096 || height > 4096 || Math.Ceiling(width) * Math.Ceiling(height) > maximumPixels)
throw new BrowserLimitException("SVG dimensions exceed the pixel budget.");
var raster = new BrowserSvgRasterizer((int)Math.Ceiling(width), (int)Math.Ceiling(height));
var elements = svg.QuerySelectorAll("*").Take(20_001).ToArray();
if (elements.Length > 20_000) throw new BrowserLimitException("SVG node budget exceeded.");
if (!string.IsNullOrEmpty(svg.Id)) raster.references.TryAdd(svg.Id, svg);
foreach (var element in elements) if (!string.IsNullOrEmpty(element.Id)) raster.references.TryAdd(element.Id, element);
Matrix3x2 transform = Matrix3x2.Identity;
if (view.Length == 4 && view[2] > 0 && view[3] > 0)
{
double scale = Math.Min(width / view[2], height / view[3]);
transform = Matrix3x2.CreateTranslation(-(float)view[0], -(float)view[1]) * Matrix3x2.CreateScale((float)scale) *
Matrix3x2.CreateTranslation((float)((width - view[2] * scale) / 2), (float)((height - view[3] * scale) / 2));
}
raster.Draw(svg, transform, new(new(0), new(0, 0), 1, 1, false, new(0)), 0);
raster.surface.Commit(); return raster.surface;
}
private static double Dimension(string? value, double fallback)
{
if (value is null) return fallback;
string text = value.Trim().ToLowerInvariant();
if (double.TryParse(text, NumberStyles.Float, CultureInfo.InvariantCulture, out double number)) return number;
foreach (var (unit, multiplier) in new[] { ("px", 1d), ("mm", 96d / 25.4), ("cm", 96d / 2.54), ("in", 96d), ("pt", 96d / 72) })
if (text.EndsWith(unit)) return CssValue.Number(text[..^unit.Length], fallback) * multiplier;
return fallback;
}
private void Draw(IElement node, Matrix3x2 parent, Paint inherited, int depth, bool referenced = false)
{
if (depth > 64 || ++draws > 20_000) throw new BrowserLimitException("SVG nesting/reference expansion exceeds its budget.");
if (node.LocalName is "title" or "desc" or "metadata" or "defs") return;
if (node.LocalName == "symbol" && !referenced) return;
if (node.LocalName is not ("svg" or "g" or "symbol" or "use" or "path" or "rect" or "circle" or "ellipse" or "line" or "polygon" or "polyline"))
throw new NotSupportedException("Unsupported SVG feature: " + node.LocalName);
var style = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
foreach (var part in (node.GetAttribute("style") ?? "").Split(';'))
{ int colon = part.IndexOf(':'); if (colon > 0) style[part[..colon].Trim()] = part[(colon + 1)..].Trim(); }
string Get(string name, string fallback) => style.GetValueOrDefault(name) ?? node.GetAttribute(name) ?? fallback;
if (Get("display", "") == "none" || Get("visibility", "") == "hidden") return;
var currentColor = CssValue.Color(Get("color", CssValue.SerializeColor(inherited.CurrentColor)), inherited.CurrentColor);
CssColor Color(string name, CssColor fallback)
{
string text = Get(name, CssValue.SerializeColor(fallback));
return text == "none" ? new(0, 0) : CssValue.Color(text, currentColor, fallback);
}
var paint = new Paint(Color("fill", inherited.Fill), Color("stroke", inherited.Stroke), Dimension(Get("stroke-width", ""), inherited.StrokeWidth),
inherited.Opacity * Math.Clamp(CssValue.Number(Get("opacity", "1"), 1), 0, 1), Get("fill-rule", inherited.EvenOdd ? "evenodd" : "nonzero") == "evenodd", currentColor);
paint = paint with { Fill = paint.Fill with { Alpha = paint.Fill.Alpha * Math.Clamp(CssValue.Number(Get("fill-opacity", "1"), 1), 0, 1) },
Stroke = paint.Stroke with { Alpha = paint.Stroke.Alpha * Math.Clamp(CssValue.Number(Get("stroke-opacity", "1"), 1), 0, 1) } };
Matrix3x2 matrix = Transform(node.GetAttribute("transform") ?? "") * parent;
double N(string name, double fallback = 0) => Dimension(node.GetAttribute(name), fallback);
if (node.LocalName == "use")
{
string reference = node.GetAttribute("href") ?? node.GetAttribute("xlink:href") ?? "";
if (reference.Length == 0) return;
if (!reference.StartsWith('#')) throw new NotSupportedException("SVG use supports only local fragment references.");
if (references.TryGetValue(reference[1..], out var target))
Draw(target, Matrix3x2.CreateTranslation((float)N("x"), (float)N("y")) * matrix, paint, depth + 1, true);
return;
}
if (node.LocalName == "symbol" && node.HasAttribute("viewBox"))
throw new NotSupportedException("Referenced SVG symbol viewports are not implemented.");
var paths = new List<List<Vector2>>();
if (node.LocalName == "path") paths = Path(node.GetAttribute("d") ?? "");
else if (node.LocalName == "rect")
{
double x = N("x"), y = N("y"), w = N("width"), h = N("height"), rx = Math.Min(w / 2, N("rx", N("ry"))), ry = Math.Min(h / 2, N("ry", rx));
if (w > 0 && h > 0)
{
var points = new List<Vector2>();
for (int corner = 0; corner < 4; corner++) for (int step = 0; step <= 8; step++)
{
double angle = (-90 + corner * 90 + step * 90d / 8) * Math.PI / 180;
double cx = corner is 0 or 1 ? x + w - rx : x + rx, cy = corner is 1 or 2 ? y + h - ry : y + ry;
points.Add(new((float)(cx + rx * Math.Cos(angle)), (float)(cy + ry * Math.Sin(angle))));
}
points.Add(points[0]); paths.Add(points);
}
}
else if (node.LocalName is "circle" or "ellipse")
{
double cx = N("cx"), cy = N("cy"), rx = N("rx", N("r")), ry = N("ry", N("r"));
paths.Add(Enumerable.Range(0, 65).Select(i => new Vector2((float)(cx + rx * Math.Cos(i * Math.PI / 32)), (float)(cy + ry * Math.Sin(i * Math.PI / 32)))).ToList());
}
else if (node.LocalName == "line") paths.Add([new((float)N("x1"), (float)N("y1")), new((float)N("x2"), (float)N("y2"))]);
else if (node.LocalName is "polygon" or "polyline")
{
var numbers = Numbers(node.GetAttribute("points") ?? "").ToArray(); var points = new List<Vector2>();
for (int i = 0; i + 1 < numbers.Length; i += 2) points.Add(new((float)numbers[i], (float)numbers[i + 1]));
if (node.LocalName == "polygon" && points.Count > 0) points.Add(points[0]); paths.Add(points);
}
var mapped = paths.Select(path => path.Select(point => Vector2.Transform(point, matrix)).ToList()).ToArray();
if (mapped.Any(path => path.Any(point => !float.IsFinite(point.X) || !float.IsFinite(point.Y))))
throw new BrowserLimitException("SVG coordinates exceed the numeric range.");
if (paint.Fill.Alpha * paint.Opacity > 0 && node.LocalName != "line") Fill(mapped, paint.Fill, paint.Opacity, paint.EvenOdd);
if (paint.Stroke.Alpha * paint.Opacity > 0)
Stroke(mapped, paint.Stroke, paint.Opacity, paint.StrokeWidth * Math.Sqrt(Math.Abs(matrix.GetDeterminant())));
foreach (var child in node.Children) Draw(child, matrix, paint, depth + 1);
}
private void Fill(IReadOnlyList<List<Vector2>> paths, CssColor color, double opacity, bool evenOdd)
{
var edges = new List<Edge>();
foreach (var path in paths)
for (int i = 0; i < path.Count; i++) if (path.Count > 2) edges.Add(new(path[i], path[(i + 1) % path.Count]));
if ((segments += edges.Count) > 20_000) throw new BrowserLimitException("SVG segment budget exceeded.");
var hits = new List<(double X, int Winding)>(); var coverage = new byte[surface.Width];
for (int y = 0; y < surface.Height; y++)
{
Array.Clear(coverage);
for (int sample = 0; sample < 2; sample++)
{
double scan = y + .25 + .5 * sample; hits.Clear();
foreach (var edge in edges)
{
if (++work > 20_000_000) throw new BrowserLimitException("SVG raster work budget exceeded.");
if ((edge.A.Y <= scan && edge.B.Y > scan) || (edge.B.Y <= scan && edge.A.Y > scan))
hits.Add((edge.A.X + (scan - edge.A.Y) * (edge.B.X - edge.A.X) / (edge.B.Y - edge.A.Y), edge.B.Y > edge.A.Y ? 1 : -1));
}
hits.Sort((a, b) => a.X.CompareTo(b.X)); int winding = 0;
for (int i = 0; i + 1 < hits.Count; i++)
{
winding += hits[i].Winding;
if (evenOdd ? (i & 1) != 0 : winding == 0) continue;
int left = (int)Math.Clamp(Math.Floor(hits[i].X), 0, surface.Width), right = (int)Math.Clamp(Math.Ceiling(hits[i + 1].X), 0, surface.Width);
for (int x = left; x < right; x++)
coverage[x] += (byte)((x + .25 >= hits[i].X && x + .25 < hits[i + 1].X ? 1 : 0) + (x + .75 >= hits[i].X && x + .75 < hits[i + 1].X ? 1 : 0));
}
}
for (int x = 0; x < coverage.Length; x++) if (coverage[x] != 0)
surface.BlendPixel(x, y, (uint)Math.Clamp(Math.Round(coverage[x] * 255 / 4d * opacity * color.Alpha), 0, 255) << 24 | color.Rgb);
}
}
private void Stroke(IReadOnlyList<List<Vector2>> paths, CssColor color, double opacity, double width)
{
if (!double.IsFinite(width) || width <= 0 || width > 512) return;
double radius = width / 2;
foreach (var path in paths) for (int i = 1; i < path.Count; i++)
{
Vector2 a = path[i - 1], b = path[i], delta = b - a; double length = delta.LengthSquared();
int left = (int)Math.Clamp(Math.Floor(Math.Min(a.X, b.X) - radius - 1), 0, surface.Width), right = (int)Math.Clamp(Math.Ceiling(Math.Max(a.X, b.X) + radius + 1), 0, surface.Width);
int top = (int)Math.Clamp(Math.Floor(Math.Min(a.Y, b.Y) - radius - 1), 0, surface.Height), bottom = (int)Math.Clamp(Math.Ceiling(Math.Max(a.Y, b.Y) + radius + 1), 0, surface.Height);
for (int y = top; y < bottom; y++) for (int x = left; x < right; x++)
{
if (++work > 20_000_000) throw new BrowserLimitException("SVG stroke work budget exceeded.");
var p = new Vector2(x + .5f, y + .5f); double t = length <= 0 ? 0 : Math.Clamp(Vector2.Dot(p - a, delta) / length, 0, 1);
double alpha = Math.Clamp(radius + .5 - Vector2.Distance(p, a + (float)t * delta), 0, 1);
if (alpha > 0) surface.BlendPixel(x, y, (uint)Math.Round(alpha * opacity * color.Alpha * 255) << 24 | color.Rgb);
}
}
}
private static Matrix3x2 Transform(string source)
{
var result = Matrix3x2.Identity; int position = 0;
while (position < source.Length)
{
int open = source.IndexOf('(', position), close = open < 0 ? -1 : source.IndexOf(')', open);
if (close < 0) break;
string name = source[position..open].Trim(' ', ','); var n = Numbers(source[(open + 1)..close]).ToArray();
Matrix3x2 next = name switch
{
"translate" when n.Length >= 1 => Matrix3x2.CreateTranslation((float)n[0], (float)n.ElementAtOrDefault(1)),
"scale" when n.Length >= 1 => Matrix3x2.CreateScale((float)n[0], (float)(n.Length > 1 ? n[1] : n[0])),
"matrix" when n.Length == 6 => new((float)n[0], (float)n[1], (float)n[2], (float)n[3], (float)n[4], (float)n[5]),
"rotate" when n.Length == 1 => Matrix3x2.CreateRotation((float)(n[0] * Math.PI / 180)),
"rotate" when n.Length == 3 => Matrix3x2.CreateRotation((float)(n[0] * Math.PI / 180), new((float)n[1], (float)n[2])),
_ => throw new NotSupportedException("Unsupported SVG transform: " + name)
};
result = next * result; position = close + 1;
}
return result;
}
private static IEnumerable<double> Numbers(string text, bool arcArguments = false)
{
int cursor = 0, argument = 0;
while (cursor < text.Length)
{
while (cursor < text.Length && (char.IsWhiteSpace(text[cursor]) || text[cursor] == ',')) cursor++;
if (cursor == text.Length) yield break;
if (arcArguments && argument % 7 is 3 or 4)
{
if (text[cursor] is not ('0' or '1')) throw new IOException("Invalid SVG arc flag.");
yield return text[cursor++] - '0'; argument++; continue;
}
int start = cursor;
if (text[cursor] is '+' or '-') cursor++;
while (cursor < text.Length && char.IsAsciiDigit(text[cursor])) cursor++;
if (cursor < text.Length && text[cursor] == '.') { cursor++; while (cursor < text.Length && char.IsAsciiDigit(text[cursor])) cursor++; }
if (cursor < text.Length && text[cursor] is 'e' or 'E') { cursor++; if (cursor < text.Length && text[cursor] is '+' or '-') cursor++; while (cursor < text.Length && char.IsAsciiDigit(text[cursor])) cursor++; }
if (cursor == start || !double.TryParse(text[start..cursor], NumberStyles.Float, CultureInfo.InvariantCulture, out double number) || !double.IsFinite(number) || Math.Abs(number) > 1_000_000)
throw new IOException("Invalid SVG number.");
yield return number;
if (++argument > 140_000) throw new BrowserLimitException("SVG argument budget exceeded.");
}
}
private static List<List<Vector2>> Path(string text)
{
var result = new List<List<Vector2>>(); List<Vector2> current = []; Vector2 point = default, first = default, control = default;
char command = ' ', previous = ' '; int position = 0, total = 0, generated = 0;
while (position < text.Length)
{
while (position < text.Length && (char.IsWhiteSpace(text[position]) || text[position] == ',')) position++;
if (position == text.Length) break;
if (char.IsAsciiLetter(text[position])) command = text[position++];
bool relative = char.IsLower(command); char op = char.ToUpperInvariant(command);
if (op == 'Z') { if (current.Count > 0) { current.Add(first); generated++; } point = first; previous = command; command = ' '; continue; }
int start = position; while (position < text.Length && (!char.IsAsciiLetter(text[position]) || text[position] is 'e' or 'E')) position++;
double[] n = Numbers(text[start..position], op == 'A').ToArray(); int size = op switch { 'M' or 'L' or 'T' => 2, 'H' or 'V' => 1, 'C' => 6, 'S' or 'Q' => 4, 'A' => 7, _ => throw new NotSupportedException("Unsupported SVG path command.") };
if (n.Length == 0 || n.Length % size != 0) throw new IOException("Invalid SVG path arguments.");
for (int index = 0; index < n.Length; index += size)
{
if (++total > 20_000) throw new BrowserLimitException("SVG path budget exceeded.");
int before = op == 'M' && index == 0 ? 0 : current.Count;
Vector2 P(int offset) => new Vector2((float)n[index + offset], (float)n[index + offset + 1]) + (relative ? point : Vector2.Zero);
if (op == 'M') { if (index == 0) { current = []; result.Add(current); point = P(0); first = point; current.Add(point); } else { point = P(0); current.Add(point); } }
else if (op == 'L') { point = P(0); current.Add(point); }
else if (op == 'H') { point.X = (float)n[index] + (relative ? point.X : 0); current.Add(point); }
else if (op == 'V') { point.Y = (float)n[index] + (relative ? point.Y : 0); current.Add(point); }
else if (op is 'C' or 'S' or 'Q' or 'T')
{
Vector2 a = point, b, c, end;
if (op == 'C') { b = P(0); c = P(2); end = P(4); }
else if (op == 'S') { b = char.ToUpperInvariant(previous) is 'C' or 'S' ? 2 * point - control : point; c = P(0); end = P(2); }
else { Vector2 q = op == 'Q' ? P(0) : char.ToUpperInvariant(previous) is 'Q' or 'T' ? 2 * point - control : point; end = op == 'Q' ? P(2) : P(0); b = a + (q - a) * (2f / 3); c = end + (q - end) * (2f / 3); control = q; }
int steps = (int)Math.Clamp(Math.Ceiling((Vector2.Distance(a, b) + Vector2.Distance(b, c) + Vector2.Distance(c, end)) / 2), 4, 128);
for (int step = 1; step <= steps; step++) { float t = (float)step / steps, u = 1 - t; current.Add(u * u * u * a + 3 * u * u * t * b + 3 * u * t * t * c + t * t * t * end); }
if (op is 'C' or 'S') control = c; point = end;
}
else if (op == 'A')
{
Vector2 end = P(5); double rx = Math.Abs(n[index]), ry = Math.Abs(n[index + 1]), phi = n[index + 2] * Math.PI / 180, cos = Math.Cos(phi), sin = Math.Sin(phi);
if (rx == 0 || ry == 0 || end == point) current.Add(end);
else
{
double dx = (point.X - end.X) / 2d, dy = (point.Y - end.Y) / 2d, x = cos * dx + sin * dy, y = -sin * dx + cos * dy;
double factor = x * x / (rx * rx) + y * y / (ry * ry); if (factor > 1) { rx *= Math.Sqrt(factor); ry *= Math.Sqrt(factor); }
double coefficient = Math.Sqrt(Math.Max(0, (rx * rx * ry * ry - rx * rx * y * y - ry * ry * x * x) / (rx * rx * y * y + ry * ry * x * x)));
if ((n[index + 3] != 0) == (n[index + 4] != 0)) coefficient = -coefficient;
double cx = coefficient * rx * y / ry, cy = -coefficient * ry * x / rx, centerX = cos * cx - sin * cy + (point.X + end.X) / 2d, centerY = sin * cx + cos * cy + (point.Y + end.Y) / 2d;
double angle = Math.Atan2((y - cy) / ry, (x - cx) / rx), delta = Math.Atan2((-y - cy) / ry, (-x - cx) / rx) - angle;
if (n[index + 4] != 0 && delta < 0) delta += 2 * Math.PI; else if (n[index + 4] == 0 && delta > 0) delta -= 2 * Math.PI;
int steps = (int)Math.Clamp(Math.Ceiling(Math.Abs(delta) * Math.Max(rx, ry) / 2), 4, 128);
for (int step = 1; step <= steps; step++) { double theta = angle + delta * step / steps; current.Add(new((float)(centerX + cos * rx * Math.Cos(theta) - sin * ry * Math.Sin(theta)), (float)(centerY + sin * rx * Math.Cos(theta) + cos * ry * Math.Sin(theta)))); }
}
point = end;
}
previous = command;
generated += current.Count - before;
if (generated > 20_000) throw new BrowserLimitException("SVG flattened path budget exceeded.");
}
}
return result;
}
}
using System.Globalization;
using System.Numerics;
using AngleSharp.Dom;
using AngleSharp.Html.Parser;
namespace XFESystem.Browser;
/// <summary>Bounded SVG icon rasterization. No scripts, external references, fonts or filters.</summary>
internal sealed class BrowserSvgRasterizer
{
private readonly BrowserCanvasSurface surface;
private int segments, work, draws;
private readonly Dictionary<string, IElement> references = new(StringComparer.Ordinal);
private readonly record struct Paint(CssColor Fill, CssColor Stroke, double StrokeWidth, double Opacity, bool EvenOdd, CssColor CurrentColor);
private readonly record struct Edge(Vector2 A, Vector2 B);
private BrowserSvgRasterizer(int width, int height) => surface = new(width, height);
internal static BrowserCanvasSurface Decode(byte[] bytes, int maximumPixels)
{
if (bytes.Length > 1_048_576) throw new BrowserLimitException("SVG text exceeds its byte budget.");
var document = new HtmlParser().ParseDocument(System.Text.Encoding.UTF8.GetString(bytes));
var svg = document.QuerySelector("svg") ?? throw new IOException("The SVG root is missing.");
double[] view = Numbers(svg.GetAttribute("viewBox") ?? "").ToArray();
double width = Dimension(svg.GetAttribute("width"), view.Length == 4 ? view[2] : 300);
double height = Dimension(svg.GetAttribute("height"), view.Length == 4 ? view[3] : 150);
if (!double.IsFinite(width + height) || width <= 0 || height <= 0 || width > 4096 || height > 4096 || Math.Ceiling(width) * Math.Ceiling(height) > maximumPixels)
throw new BrowserLimitException("SVG dimensions exceed the pixel budget.");
var raster = new BrowserSvgRasterizer((int)Math.Ceiling(width), (int)Math.Ceiling(height));
var elements = svg.QuerySelectorAll("*").Take(20_001).ToArray();
if (elements.Length > 20_000) throw new BrowserLimitException("SVG node budget exceeded.");
if (!string.IsNullOrEmpty(svg.Id)) raster.references.TryAdd(svg.Id, svg);
foreach (var element in elements) if (!string.IsNullOrEmpty(element.Id)) raster.references.TryAdd(element.Id, element);
Matrix3x2 transform = Matrix3x2.Identity;
if (view.Length == 4 && view[2] > 0 && view[3] > 0)
{
double scale = Math.Min(width / view[2], height / view[3]);
transform = Matrix3x2.CreateTranslation(-(float)view[0], -(float)view[1]) * Matrix3x2.CreateScale((float)scale) *
Matrix3x2.CreateTranslation((float)((width - view[2] * scale) / 2), (float)((height - view[3] * scale) / 2));
}
raster.Draw(svg, transform, new(new(0), new(0, 0), 1, 1, false, new(0)), 0);
raster.surface.Commit(); return raster.surface;
}
private static double Dimension(string? value, double fallback)
{
if (value is null) return fallback;
string text = value.Trim().ToLowerInvariant();
if (double.TryParse(text, NumberStyles.Float, CultureInfo.InvariantCulture, out double number)) return number;
foreach (var (unit, multiplier) in new[] { ("px", 1d), ("mm", 96d / 25.4), ("cm", 96d / 2.54), ("in", 96d), ("pt", 96d / 72) })
if (text.EndsWith(unit)) return CssValue.Number(text[..^unit.Length], fallback) * multiplier;
return fallback;
}
private void Draw(IElement node, Matrix3x2 parent, Paint inherited, int depth, bool referenced = false)
{
if (depth > 64 || ++draws > 20_000) throw new BrowserLimitException("SVG nesting/reference expansion exceeds its budget.");
if (node.LocalName is "title" or "desc" or "metadata" or "defs") return;
if (node.LocalName == "symbol" && !referenced) return;
if (node.LocalName is not ("svg" or "g" or "symbol" or "use" or "path" or "rect" or "circle" or "ellipse" or "line" or "polygon" or "polyline"))
throw new NotSupportedException("Unsupported SVG feature: " + node.LocalName);
var style = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
foreach (var part in (node.GetAttribute("style") ?? "").Split(';'))
{ int colon = part.IndexOf(':'); if (colon > 0) style[part[..colon].Trim()] = part[(colon + 1)..].Trim(); }
string Get(string name, string fallback) => style.GetValueOrDefault(name) ?? node.GetAttribute(name) ?? fallback;
if (Get("display", "") == "none" || Get("visibility", "") == "hidden") return;
var currentColor = CssValue.Color(Get("color", CssValue.SerializeColor(inherited.CurrentColor)), inherited.CurrentColor);
CssColor Color(string name, CssColor fallback)
{
string text = Get(name, CssValue.SerializeColor(fallback));
return text == "none" ? new(0, 0) : CssValue.Color(text, currentColor, fallback);
}
var paint = new Paint(Color("fill", inherited.Fill), Color("stroke", inherited.Stroke), Dimension(Get("stroke-width", ""), inherited.StrokeWidth),
inherited.Opacity * Math.Clamp(CssValue.Number(Get("opacity", "1"), 1), 0, 1), Get("fill-rule", inherited.EvenOdd ? "evenodd" : "nonzero") == "evenodd", currentColor);
paint = paint with { Fill = paint.Fill with { Alpha = paint.Fill.Alpha * Math.Clamp(CssValue.Number(Get("fill-opacity", "1"), 1), 0, 1) },
Stroke = paint.Stroke with { Alpha = paint.Stroke.Alpha * Math.Clamp(CssValue.Number(Get("stroke-opacity", "1"), 1), 0, 1) } };
Matrix3x2 matrix = Transform(node.GetAttribute("transform") ?? "") * parent;
double N(string name, double fallback = 0) => Dimension(node.GetAttribute(name), fallback);
if (node.LocalName == "use")
{
string reference = node.GetAttribute("href") ?? node.GetAttribute("xlink:href") ?? "";
if (reference.Length == 0) return;
if (!reference.StartsWith('#')) throw new NotSupportedException("SVG use supports only local fragment references.");
if (references.TryGetValue(reference[1..], out var target))
Draw(target, Matrix3x2.CreateTranslation((float)N("x"), (float)N("y")) * matrix, paint, depth + 1, true);
return;
}
if (node.LocalName == "symbol" && node.HasAttribute("viewBox"))
throw new NotSupportedException("Referenced SVG symbol viewports are not implemented.");
var paths = new List<List<Vector2>>();
if (node.LocalName == "path") paths = Path(node.GetAttribute("d") ?? "");
else if (node.LocalName == "rect")
{
double x = N("x"), y = N("y"), w = N("width"), h = N("height"), rx = Math.Min(w / 2, N("rx", N("ry"))), ry = Math.Min(h / 2, N("ry", rx));
if (w > 0 && h > 0)
{
var points = new List<Vector2>();
for (int corner = 0; corner < 4; corner++) for (int step = 0; step <= 8; step++)
{
double angle = (-90 + corner * 90 + step * 90d / 8) * Math.PI / 180;
double cx = corner is 0 or 1 ? x + w - rx : x + rx, cy = corner is 1 or 2 ? y + h - ry : y + ry;
points.Add(new((float)(cx + rx * Math.Cos(angle)), (float)(cy + ry * Math.Sin(angle))));
}
points.Add(points[0]); paths.Add(points);
}
}
else if (node.LocalName is "circle" or "ellipse")
{
double cx = N("cx"), cy = N("cy"), rx = N("rx", N("r")), ry = N("ry", N("r"));
paths.Add(Enumerable.Range(0, 65).Select(i => new Vector2((float)(cx + rx * Math.Cos(i * Math.PI / 32)), (float)(cy + ry * Math.Sin(i * Math.PI / 32)))).ToList());
}
else if (node.LocalName == "line") paths.Add([new((float)N("x1"), (float)N("y1")), new((float)N("x2"), (float)N("y2"))]);
else if (node.LocalName is "polygon" or "polyline")
{
var numbers = Numbers(node.GetAttribute("points") ?? "").ToArray(); var points = new List<Vector2>();
for (int i = 0; i + 1 < numbers.Length; i += 2) points.Add(new((float)numbers[i], (float)numbers[i + 1]));
if (node.LocalName == "polygon" && points.Count > 0) points.Add(points[0]); paths.Add(points);
}
var mapped = paths.Select(path => path.Select(point => Vector2.Transform(point, matrix)).ToList()).ToArray();
if (mapped.Any(path => path.Any(point => !float.IsFinite(point.X) || !float.IsFinite(point.Y))))
throw new BrowserLimitException("SVG coordinates exceed the numeric range.");
if (paint.Fill.Alpha * paint.Opacity > 0 && node.LocalName != "line") Fill(mapped, paint.Fill, paint.Opacity, paint.EvenOdd);
if (paint.Stroke.Alpha * paint.Opacity > 0)
Stroke(mapped, paint.Stroke, paint.Opacity, paint.StrokeWidth * Math.Sqrt(Math.Abs(matrix.GetDeterminant())));
foreach (var child in node.Children) Draw(child, matrix, paint, depth + 1);
}
private void Fill(IReadOnlyList<List<Vector2>> paths, CssColor color, double opacity, bool evenOdd)
{
var edges = new List<Edge>();
foreach (var path in paths)
for (int i = 0; i < path.Count; i++) if (path.Count > 2) edges.Add(new(path[i], path[(i + 1) % path.Count]));
if ((segments += edges.Count) > 20_000) throw new BrowserLimitException("SVG segment budget exceeded.");
var hits = new List<(double X, int Winding)>(); var coverage = new byte[surface.Width];
for (int y = 0; y < surface.Height; y++)
{
Array.Clear(coverage);
for (int sample = 0; sample < 2; sample++)
{
double scan = y + .25 + .5 * sample; hits.Clear();
foreach (var edge in edges)
{
if (++work > 20_000_000) throw new BrowserLimitException("SVG raster work budget exceeded.");
if ((edge.A.Y <= scan && edge.B.Y > scan) || (edge.B.Y <= scan && edge.A.Y > scan))
hits.Add((edge.A.X + (scan - edge.A.Y) * (edge.B.X - edge.A.X) / (edge.B.Y - edge.A.Y), edge.B.Y > edge.A.Y ? 1 : -1));
}
hits.Sort((a, b) => a.X.CompareTo(b.X)); int winding = 0;
for (int i = 0; i + 1 < hits.Count; i++)
{
winding += hits[i].Winding;
if (evenOdd ? (i & 1) != 0 : winding == 0) continue;
int left = (int)Math.Clamp(Math.Floor(hits[i].X), 0, surface.Width), right = (int)Math.Clamp(Math.Ceiling(hits[i + 1].X), 0, surface.Width);
for (int x = left; x < right; x++)
coverage[x] += (byte)((x + .25 >= hits[i].X && x + .25 < hits[i + 1].X ? 1 : 0) + (x + .75 >= hits[i].X && x + .75 < hits[i + 1].X ? 1 : 0));
}
}
for (int x = 0; x < coverage.Length; x++) if (coverage[x] != 0)
surface.BlendPixel(x, y, (uint)Math.Clamp(Math.Round(coverage[x] * 255 / 4d * opacity * color.Alpha), 0, 255) << 24 | color.Rgb);
}
}
private void Stroke(IReadOnlyList<List<Vector2>> paths, CssColor color, double opacity, double width)
{
if (!double.IsFinite(width) || width <= 0 || width > 512) return;
double radius = width / 2;
foreach (var path in paths) for (int i = 1; i < path.Count; i++)
{
Vector2 a = path[i - 1], b = path[i], delta = b - a; double length = delta.LengthSquared();
int left = (int)Math.Clamp(Math.Floor(Math.Min(a.X, b.X) - radius - 1), 0, surface.Width), right = (int)Math.Clamp(Math.Ceiling(Math.Max(a.X, b.X) + radius + 1), 0, surface.Width);
int top = (int)Math.Clamp(Math.Floor(Math.Min(a.Y, b.Y) - radius - 1), 0, surface.Height), bottom = (int)Math.Clamp(Math.Ceiling(Math.Max(a.Y, b.Y) + radius + 1), 0, surface.Height);
for (int y = top; y < bottom; y++) for (int x = left; x < right; x++)
{
if (++work > 20_000_000) throw new BrowserLimitException("SVG stroke work budget exceeded.");
var p = new Vector2(x + .5f, y + .5f); double t = length <= 0 ? 0 : Math.Clamp(Vector2.Dot(p - a, delta) / length, 0, 1);
double alpha = Math.Clamp(radius + .5 - Vector2.Distance(p, a + (float)t * delta), 0, 1);
if (alpha > 0) surface.BlendPixel(x, y, (uint)Math.Round(alpha * opacity * color.Alpha * 255) << 24 | color.Rgb);
}
}
}
private static Matrix3x2 Transform(string source)
{
var result = Matrix3x2.Identity; int position = 0;
while (position < source.Length)
{
int open = source.IndexOf('(', position), close = open < 0 ? -1 : source.IndexOf(')', open);
if (close < 0) break;
string name = source[position..open].Trim(' ', ','); var n = Numbers(source[(open + 1)..close]).ToArray();
Matrix3x2 next = name switch
{
"translate" when n.Length >= 1 => Matrix3x2.CreateTranslation((float)n[0], (float)n.ElementAtOrDefault(1)),
"scale" when n.Length >= 1 => Matrix3x2.CreateScale((float)n[0], (float)(n.Length > 1 ? n[1] : n[0])),
"matrix" when n.Length == 6 => new((float)n[0], (float)n[1], (float)n[2], (float)n[3], (float)n[4], (float)n[5]),
"rotate" when n.Length == 1 => Matrix3x2.CreateRotation((float)(n[0] * Math.PI / 180)),
"rotate" when n.Length == 3 => Matrix3x2.CreateRotation((float)(n[0] * Math.PI / 180), new((float)n[1], (float)n[2])),
_ => throw new NotSupportedException("Unsupported SVG transform: " + name)
};
result = next * result; position = close + 1;
}
return result;
}
private static IEnumerable<double> Numbers(string text, bool arcArguments = false)
{
int cursor = 0, argument = 0;
while (cursor < text.Length)
{
while (cursor < text.Length && (char.IsWhiteSpace(text[cursor]) || text[cursor] == ',')) cursor++;
if (cursor == text.Length) yield break;
if (arcArguments && argument % 7 is 3 or 4)
{
if (text[cursor] is not ('0' or '1')) throw new IOException("Invalid SVG arc flag.");
yield return text[cursor++] - '0'; argument++; continue;
}
int start = cursor;
if (text[cursor] is '+' or '-') cursor++;
while (cursor < text.Length && char.IsAsciiDigit(text[cursor])) cursor++;
if (cursor < text.Length && text[cursor] == '.') { cursor++; while (cursor < text.Length && char.IsAsciiDigit(text[cursor])) cursor++; }
if (cursor < text.Length && text[cursor] is 'e' or 'E') { cursor++; if (cursor < text.Length && text[cursor] is '+' or '-') cursor++; while (cursor < text.Length && char.IsAsciiDigit(text[cursor])) cursor++; }
if (cursor == start || !double.TryParse(text[start..cursor], NumberStyles.Float, CultureInfo.InvariantCulture, out double number) || !double.IsFinite(number) || Math.Abs(number) > 1_000_000)
throw new IOException("Invalid SVG number.");
yield return number;
if (++argument > 140_000) throw new BrowserLimitException("SVG argument budget exceeded.");
}
}
private static List<List<Vector2>> Path(string text)
{
var result = new List<List<Vector2>>(); List<Vector2> current = []; Vector2 point = default, first = default, control = default;
char command = ' ', previous = ' '; int position = 0, total = 0, generated = 0;
while (position < text.Length)
{
while (position < text.Length && (char.IsWhiteSpace(text[position]) || text[position] == ',')) position++;
if (position == text.Length) break;
if (char.IsAsciiLetter(text[position])) command = text[position++];
bool relative = char.IsLower(command); char op = char.ToUpperInvariant(command);
if (op == 'Z') { if (current.Count > 0) { current.Add(first); generated++; } point = first; previous = command; command = ' '; continue; }
int start = position; while (position < text.Length && (!char.IsAsciiLetter(text[position]) || text[position] is 'e' or 'E')) position++;
double[] n = Numbers(text[start..position], op == 'A').ToArray(); int size = op switch { 'M' or 'L' or 'T' => 2, 'H' or 'V' => 1, 'C' => 6, 'S' or 'Q' => 4, 'A' => 7, _ => throw new NotSupportedException("Unsupported SVG path command.") };
if (n.Length == 0 || n.Length % size != 0) throw new IOException("Invalid SVG path arguments.");
for (int index = 0; index < n.Length; index += size)
{
if (++total > 20_000) throw new BrowserLimitException("SVG path budget exceeded.");
int before = op == 'M' && index == 0 ? 0 : current.Count;
Vector2 P(int offset) => new Vector2((float)n[index + offset], (float)n[index + offset + 1]) + (relative ? point : Vector2.Zero);
if (op == 'M') { if (index == 0) { current = []; result.Add(current); point = P(0); first = point; current.Add(point); } else { point = P(0); current.Add(point); } }
else if (op == 'L') { point = P(0); current.Add(point); }
else if (op == 'H') { point.X = (float)n[index] + (relative ? point.X : 0); current.Add(point); }
else if (op == 'V') { point.Y = (float)n[index] + (relative ? point.Y : 0); current.Add(point); }
else if (op is 'C' or 'S' or 'Q' or 'T')
{
Vector2 a = point, b, c, end;
if (op == 'C') { b = P(0); c = P(2); end = P(4); }
else if (op == 'S') { b = char.ToUpperInvariant(previous) is 'C' or 'S' ? 2 * point - control : point; c = P(0); end = P(2); }
else { Vector2 q = op == 'Q' ? P(0) : char.ToUpperInvariant(previous) is 'Q' or 'T' ? 2 * point - control : point; end = op == 'Q' ? P(2) : P(0); b = a + (q - a) * (2f / 3); c = end + (q - end) * (2f / 3); control = q; }
int steps = (int)Math.Clamp(Math.Ceiling((Vector2.Distance(a, b) + Vector2.Distance(b, c) + Vector2.Distance(c, end)) / 2), 4, 128);
for (int step = 1; step <= steps; step++) { float t = (float)step / steps, u = 1 - t; current.Add(u * u * u * a + 3 * u * u * t * b + 3 * u * t * t * c + t * t * t * end); }
if (op is 'C' or 'S') control = c; point = end;
}
else if (op == 'A')
{
Vector2 end = P(5); double rx = Math.Abs(n[index]), ry = Math.Abs(n[index + 1]), phi = n[index + 2] * Math.PI / 180, cos = Math.Cos(phi), sin = Math.Sin(phi);
if (rx == 0 || ry == 0 || end == point) current.Add(end);
else
{
double dx = (point.X - end.X) / 2d, dy = (point.Y - end.Y) / 2d, x = cos * dx + sin * dy, y = -sin * dx + cos * dy;
double factor = x * x / (rx * rx) + y * y / (ry * ry); if (factor > 1) { rx *= Math.Sqrt(factor); ry *= Math.Sqrt(factor); }
double coefficient = Math.Sqrt(Math.Max(0, (rx * rx * ry * ry - rx * rx * y * y - ry * ry * x * x) / (rx * rx * y * y + ry * ry * x * x)));
if ((n[index + 3] != 0) == (n[index + 4] != 0)) coefficient = -coefficient;
double cx = coefficient * rx * y / ry, cy = -coefficient * ry * x / rx, centerX = cos * cx - sin * cy + (point.X + end.X) / 2d, centerY = sin * cx + cos * cy + (point.Y + end.Y) / 2d;
double angle = Math.Atan2((y - cy) / ry, (x - cx) / rx), delta = Math.Atan2((-y - cy) / ry, (-x - cx) / rx) - angle;
if (n[index + 4] != 0 && delta < 0) delta += 2 * Math.PI; else if (n[index + 4] == 0 && delta > 0) delta -= 2 * Math.PI;
int steps = (int)Math.Clamp(Math.Ceiling(Math.Abs(delta) * Math.Max(rx, ry) / 2), 4, 128);
for (int step = 1; step <= steps; step++) { double theta = angle + delta * step / steps; current.Add(new((float)(centerX + cos * rx * Math.Cos(theta) - sin * ry * Math.Sin(theta)), (float)(centerY + sin * rx * Math.Cos(theta) + cos * ry * Math.Sin(theta)))); }
}
point = end;
}
previous = command;
generated += current.Count - before;
if (generated > 20_000) throw new BrowserLimitException("SVG flattened path budget exceeded.");
}
}
return result;
}
}