// Copyright (c) Six Labors. // Licensed under the Six Labors Split License. using System; using System.Collections.Concurrent; using System.Collections.Generic; using System.Diagnostics.CodeAnalysis; using System.Globalization; using System.IO; using System.Numerics; using System.Runtime.CompilerServices; using System.Xml; using System.Xml.Linq; using SixLabors.Fonts.Rendering; namespace SixLabors.Fonts.Tables.General.Svg { /// /// Supplies painted glyphs (layers + commands + paints) and canvas metadata for OT-SVG glyphs. /// Geometry coordinates are kept in SVG user space; all transforms are carried as matrices /// on the canvas (root) and on each layer. No point-transforming is performed here. /// internal sealed class SvgGlyphSource : IPaintedGlyphSource { private static readonly SolidPaint DefaultBlackFillPaint = new() { Color = GlyphColor.Black }; private readonly SvgTable svgTable; private readonly ConcurrentDictionary<(int Start, int Length), ParsedDoc> docCache = []; private readonly ConcurrentDictionary cachedGlyphs = []; /// /// Initializes a new instance of the class. /// /// The SVG table. public SvgGlyphSource(SvgTable svgTable) => this.svgTable = svgTable; /// public bool TryGetPaintedGlyph(ushort glyphId, out PaintedGlyph glyph, out PaintedCanvasMetadata canvas) { (PaintedGlyph Glyph, PaintedCanvasMetadata Canvas) result = this.cachedGlyphs.GetOrAdd(glyphId, gid => { if (this.TryGetParsedDoc(gid, out ParsedDoc? parsed)) { XElement? root = parsed.Doc.Root; if (root is not null) { FontRectangle viewBox = GetViewBox(root); Matrix3x2 rootTransform = ParseTransform(root.Attribute("transform")?.Value); // Prefer a dedicated group with id="glyph{gid}", else fall back to the root. string wantedId = "glyph" + gid.ToString(CultureInfo.InvariantCulture); XElement glyphRoot = parsed.IdMap.TryGetValue(wantedId, out XElement? ge) ? ge : root; List layers = []; Walk( glyphRoot, rootTransform, inheritedPaint: DefaultBlackFillPaint, inheritedOpacityMul: 1F, outputLayers: layers, parsedDoc: parsed); if (layers.Count > 0) { PaintedGlyph glyph = new(layers); PaintedCanvasMetadata canvas = new(viewBox, true, rootTransform); return (glyph, canvas); } } } return (default, default); }); glyph = result.Glyph; canvas = result.Canvas; return result.Glyph.Layers.Count > 0; } private bool TryGetParsedDoc(ushort glyphId, [NotNullWhen(true)] out ParsedDoc? parsed) { parsed = default; if (!this.svgTable.TryGetDocumentSpan(glyphId, out int start, out int length)) { return false; } (int Start, int Length) docKey = (start, length); if (this.docCache.TryGetValue(docKey, out parsed)) { return true; } if (!this.svgTable.TryOpenDecodedDocumentStream(glyphId, out Stream stream)) { return false; } using (stream) { XDocument doc = LoadXml(stream); if (doc.Root is null) { return false; } // TODO: How large is this likely to get? If large, consider a more memory-efficient structure. Dictionary idMap = new(1024, StringComparer.Ordinal); foreach (XElement e in doc.Root.DescendantsAndSelf()) { XAttribute? id = e.Attribute("id"); if (id is not null) { idMap[id.Value] = e; // last-wins } } parsed = new ParsedDoc { Doc = doc, IdMap = idMap }; this.docCache[docKey] = parsed; return true; } } private static XDocument LoadXml(Stream stream) { XmlReaderSettings settings = new() { DtdProcessing = DtdProcessing.Ignore, XmlResolver = null, IgnoreComments = true, IgnoreProcessingInstructions = true, IgnoreWhitespace = true }; using XmlReader reader = XmlReader.Create(stream, settings); return XDocument.Load(reader, LoadOptions.None); } private static FontRectangle GetViewBox(XElement svg) { if (TryParseViewBox(svg.Attribute("viewBox")?.Value, out float x, out float y, out float w, out float h)) { return new FontRectangle(x, y, w, h); } // No viewBox; return an empty rect. Metrics layer must decide fallback mapping. return FontRectangle.Empty; } private static void Walk( XElement node, Matrix3x2 parentLocalTransform, Paint? inheritedPaint, float inheritedOpacityMul, List outputLayers, ParsedDoc parsedDoc) { Dictionary idMap = parsedDoc.IdMap; Matrix3x2 nodeTransform = ParseTransform(node.Attribute("transform")?.Value); Matrix3x2 localTransform = parentLocalTransform * nodeTransform; FillRule fillRule = ResolveFillRule(node, FillRule.NonZero); Paint? paint = ResolvePaint(node, inheritedPaint, parsedDoc, out bool fillNone, out float opacityMul); float combinedOpacityMul = inheritedOpacityMul * opacityMul; string name = node.Name.LocalName; switch (name) { case "svg": case "g": { foreach (XElement child in node.Elements()) { Walk(child, localTransform, fillNone ? null : paint, combinedOpacityMul, outputLayers, parsedDoc); } break; } case "use": { string? href = GetHref(node); if (href is null) { break; } float ux = ParseFloat(node.Attribute("x")?.Value); float uy = ParseFloat(node.Attribute("y")?.Value); Matrix3x2 xf = parentLocalTransform * Matrix3x2.CreateTranslation(ux, uy) * nodeTransform; Paint? childInherited = fillNone ? null : paint; XElement? target = LookupById(idMap, href); if (target is not null) { Walk(target, xf, childInherited, combinedOpacityMul, outputLayers, parsedDoc); } break; } case "path": { if (fillNone) { break; } string? d = node.Attribute("d")?.Value; if (string.IsNullOrWhiteSpace(d)) { break; } List cmds = GetOrBuildPathCommands(node, d, parsedDoc); if (cmds.Count > 0) { Paint? layerPaint = ApplyOpacityToPaint(paint, combinedOpacityMul); outputLayers.Add(new(layerPaint, fillRule, localTransform, null, cmds)); } break; } case "polygon": case "polyline": { if (fillNone) { break; } string pts = node.Attribute("points")?.Value ?? string.Empty; float[] coords = ParseFloatList(pts); if (coords.Length >= 4) { bool close = string.Equals(node.Name.LocalName, "polygon", StringComparison.Ordinal); List cmds = GetOrBuildPolyCommands(node, coords, close, parsedDoc); if (cmds.Count > 0) { Paint? layerPaint = ApplyOpacityToPaint(paint, combinedOpacityMul); outputLayers.Add(new(layerPaint, fillRule, localTransform, null, cmds)); } } break; } case "rect": { if (fillNone) { break; } float x = ParseFloat(node.Attribute("x")?.Value); float y = ParseFloat(node.Attribute("y")?.Value); float w = ParseFloat(node.Attribute("width")?.Value); float h = ParseFloat(node.Attribute("height")?.Value); // TODO: Rounded corners (rx/ry) not handled here (could be approximated later if needed). if (w > 0f && h > 0f) { List cmds = GetOrBuildRectCommands(node, x, y, w, h, parsedDoc); if (cmds.Count > 0) { Paint? layerPaint = ApplyOpacityToPaint(paint, combinedOpacityMul); outputLayers.Add(new(layerPaint, fillRule, localTransform, null, cmds)); } } break; } case "circle": { if (fillNone) { break; } float cx = ParseFloat(node.Attribute("cx")?.Value); float cy = ParseFloat(node.Attribute("cy")?.Value); float r = ParseFloat(node.Attribute("r")?.Value); if (r > 0f) { List cmds = GetOrBuildEllipseCommands(node, cx, cy, r, r, parsedDoc); if (cmds.Count > 0) { Paint? layerPaint = ApplyOpacityToPaint(paint, combinedOpacityMul); outputLayers.Add(new(layerPaint, fillRule, localTransform, null, cmds)); } } break; } case "ellipse": { if (fillNone) { break; } float cx = ParseFloat(node.Attribute("cx")?.Value); float cy = ParseFloat(node.Attribute("cy")?.Value); float rx = ParseFloat(node.Attribute("rx")?.Value); float ry = ParseFloat(node.Attribute("ry")?.Value); if (rx > 0f && ry > 0f) { List cmds = GetOrBuildEllipseCommands(node, cx, cy, rx, ry, parsedDoc); if (cmds.Count > 0) { Paint? layerPaint = ApplyOpacityToPaint(paint, combinedOpacityMul); outputLayers.Add(new(layerPaint, fillRule, localTransform, null, cmds)); } } break; } default: { // Unhandled (image, text, mask, clipPath, etc.) in v1. break; } } } private static Paint? ApplyOpacityToPaint(Paint? basePaint, float opacityMul) { if (basePaint is null) { return null; } float effective = Math.Clamp(basePaint.Opacity * opacityMul, 0f, 1f); if (effective <= 0f) { return null; } return basePaint switch { SolidPaint s => new SolidPaint { Color = s.Color, Opacity = effective }, LinearGradientPaint lg => new LinearGradientPaint { Units = lg.Units, P0 = lg.P0, P1 = lg.P1, Spread = lg.Spread, Stops = lg.Stops, Transform = lg.Transform, Opacity = effective }, RadialGradientPaint rg => new RadialGradientPaint { Units = rg.Units, Center0 = rg.Center0, Radius0 = rg.Radius0, Center1 = rg.Center1, Radius1 = rg.Radius1, Spread = rg.Spread, Stops = rg.Stops, Transform = rg.Transform, Opacity = effective }, _ => null, }; } private static FillRule ResolveFillRule(XElement e, FillRule inheritedDefault) { string? styleRule = TryCss(e.Attribute("style")?.Value, "fill-rule"); string? attrRule = e.Attribute("fill-rule")?.Value; string? value = styleRule ?? attrRule; if (string.Equals(value, "evenodd", StringComparison.OrdinalIgnoreCase)) { return FillRule.EvenOdd; } if (string.Equals(value, "nonzero", StringComparison.OrdinalIgnoreCase)) { return FillRule.NonZero; } return inheritedDefault; } private static Paint? ResolvePaint( XElement e, Paint? inherited, ParsedDoc parsedDoc, out bool fillNone, out float opacityMul) { fillNone = false; opacityMul = 1f; string? style = e.Attribute("style")?.Value; string? fillAttr = e.Attribute("fill")?.Value; string? opacityAttr = e.Attribute("opacity")?.Value; string? fillOpacityAttr = e.Attribute("fill-opacity")?.Value; string? styleFill = TryCss(style, "fill"); string? styleOpacity = TryCss(style, "opacity"); string? styleFillOpacity = TryCss(style, "fill-opacity"); string? fill = styleFill ?? fillAttr; string? op = styleOpacity ?? opacityAttr; string? fop = styleFillOpacity ?? fillOpacityAttr; if (!string.IsNullOrEmpty(op) && float.TryParse(op, NumberStyles.Float, CultureInfo.InvariantCulture, out float o)) { opacityMul *= Math.Clamp(o, 0f, 1f); } if (!string.IsNullOrEmpty(fop) && float.TryParse(fop, NumberStyles.Float, CultureInfo.InvariantCulture, out float fo)) { opacityMul *= Math.Clamp(fo, 0f, 1f); } if (string.IsNullOrEmpty(fill)) { return inherited; } if (string.Equals(fill, "none", StringComparison.OrdinalIgnoreCase)) { fillNone = true; return null; } if (TryParseColor(fill, out GlyphColor color)) { return new SolidPaint { Color = color }; } if (TryExtractUrlId(fill, out string? paintId) && paintId is not null) { return ResolvePaintServer(paintId, parsedDoc) ?? inherited; } return inherited; } /// /// Resolves a referenced paint server and caches the parsed paint so repeated /// uses of the same gradient id do not rebuild the gradient definition. /// /// The referenced paint server identifier. /// The parsed SVG document and its caches. /// The resolved paint, or if the reference is unknown. private static Paint? ResolvePaintServer(string id, ParsedDoc parsedDoc) { if (parsedDoc.PaintServerCache.TryGetValue(id, out Paint? cached)) { return cached; } if (!parsedDoc.IdMap.TryGetValue(id, out XElement? server)) { return null; } string tag = server.Name.LocalName; Paint? paint = tag switch { // SVG only has linearGradient and radialGradient. "linearGradient" => BuildLinearGradient(server, parsedDoc.IdMap), "radialGradient" => BuildRadialGradient(server, parsedDoc.IdMap), _ => null }; if (paint is not null) { parsedDoc.PaintServerCache.TryAdd(id, paint); } return paint; } private static LinearGradientPaint? BuildLinearGradient(XElement grad, Dictionary idMap) { GradientUnits units = GradientUnits.ObjectBoundingBox; SpreadMethod spread = SpreadMethod.Pad; Matrix3x2 gxf = Matrix3x2.Identity; float? x1 = null, y1 = null, x2 = null, y2 = null; List<(float Offset, GlyphColor Color)> stops = []; HashSet visited = new(StringComparer.Ordinal); XElement? cur = grad; while (cur is not null) { string? u = cur.Attribute("gradientUnits")?.Value; if (u is not null) { units = ParseGradientUnits(u); } string? sm = cur.Attribute("spreadMethod")?.Value; if (sm is not null) { spread = ParseSpreadMethod(sm); } gxf = ParseTransform(cur.Attribute("gradientTransform")?.Value) * gxf; x1 ??= ParseCoordNullable(cur.Attribute("x1")?.Value); y1 ??= ParseCoordNullable(cur.Attribute("y1")?.Value); x2 ??= ParseCoordNullable(cur.Attribute("x2")?.Value); y2 ??= ParseCoordNullable(cur.Attribute("y2")?.Value); bool hadStops = false; foreach (XElement s in cur.Elements()) { if (s.Name.LocalName != "stop") { continue; } if (TryParseStop(s, out float off, out GlyphColor c)) { stops.Add((off, c)); hadStops = true; } } if (hadStops) { break; } string? href = GetHref(cur); if (href is null || href.Length <= 1 || href[0] != '#') { break; } string refId = href[1..]; if (!visited.Add(refId) || !idMap.TryGetValue(refId, out cur)) { break; } } if (!x1.HasValue) { x1 = 0f; } if (!y1.HasValue) { y1 = 0f; } if (!x2.HasValue) { x2 = units == GradientUnits.ObjectBoundingBox ? 1f : 0f; } if (!y2.HasValue) { y2 = 0f; } GradientStop[] gs = BuildStopsArray(stops); return new LinearGradientPaint { Units = units, P0 = new Vector2(x1.Value, y1.Value), P1 = new Vector2(x2.Value, y2.Value), Spread = spread, Stops = gs, Transform = gxf }; } private static RadialGradientPaint? BuildRadialGradient(XElement grad, Dictionary idMap) { GradientUnits units = GradientUnits.ObjectBoundingBox; SpreadMethod spread = SpreadMethod.Pad; Matrix3x2 gxf = Matrix3x2.Identity; float? cx = null, cy = null, r = null, fx = null, fy = null, fr = null; List<(float Offset, GlyphColor Color)> stops = []; HashSet visited = new(StringComparer.Ordinal); XElement? cur = grad; while (cur is not null) { string? u = cur.Attribute("gradientUnits")?.Value; if (u is not null) { units = ParseGradientUnits(u); } string? sm = cur.Attribute("spreadMethod")?.Value; if (sm is not null) { spread = ParseSpreadMethod(sm); } gxf = ParseTransform(cur.Attribute("gradientTransform")?.Value) * gxf; cx ??= ParseCoordNullable(cur.Attribute("cx")?.Value); cy ??= ParseCoordNullable(cur.Attribute("cy")?.Value); r ??= ParseRadiusNullable(cur.Attribute("r")?.Value); fx ??= ParseCoordNullable(cur.Attribute("fx")?.Value); fy ??= ParseCoordNullable(cur.Attribute("fy")?.Value); fr ??= ParseRadiusNullable(cur.Attribute("fr")?.Value); bool hadStops = false; foreach (XElement s in cur.Elements()) { if (s.Name.LocalName != "stop") { continue; } if (TryParseStop(s, out float off, out GlyphColor c)) { stops.Add((off, c)); hadStops = true; } } if (hadStops) { break; } string? href = GetHref(cur); if (href is null || href.Length <= 1 || href[0] != '#') { break; } string refId = href[1..]; if (!visited.Add(refId) || !idMap.TryGetValue(refId, out cur)) { break; } } if (!cx.HasValue) { cx = units == GradientUnits.ObjectBoundingBox ? 0.5f : 0f; } if (!cy.HasValue) { cy = units == GradientUnits.ObjectBoundingBox ? 0.5f : 0f; } if (!r.HasValue) { r = units == GradientUnits.ObjectBoundingBox ? 0.5f : 0f; } if (!fx.HasValue) { fx = cx.Value; } if (!fy.HasValue) { fy = cy.Value; } if (!fr.HasValue) { fr = 0f; } GradientStop[] gs = BuildStopsArray(stops); // Center0=(fx,fy), Radius0=fr; Center1=(cx,cy), Radius1=r return new RadialGradientPaint { Units = units, Center0 = new Vector2(fx.Value, fy.Value), Radius0 = fr.Value, Center1 = new Vector2(cx.Value, cy.Value), Radius1 = r.Value, Spread = spread, Stops = gs, Transform = gxf }; } private static SpreadMethod ParseSpreadMethod(string value) { if (string.Equals(value, "reflect", StringComparison.OrdinalIgnoreCase)) { return SpreadMethod.Reflect; } if (string.Equals(value, "repeat", StringComparison.OrdinalIgnoreCase)) { return SpreadMethod.Repeat; } return SpreadMethod.Pad; } private static GradientUnits ParseGradientUnits(string value) => string.Equals(value, "userSpaceOnUse", StringComparison.OrdinalIgnoreCase) ? GradientUnits.UserSpaceOnUse : GradientUnits.ObjectBoundingBox; private static float? ParseCoordNullable(string? s) { if (string.IsNullOrEmpty(s)) { return null; } if (s.EndsWith('%')) { if (float.TryParse(s.AsSpan(0, s.Length - 1), NumberStyles.Float, CultureInfo.InvariantCulture, out float p)) { return p / 100f; } return null; } if (float.TryParse(s, NumberStyles.Float, CultureInfo.InvariantCulture, out float v)) { return v; // In OBB this is already a fraction; in userSpace it is absolute user units. } return null; } [MethodImpl(MethodImplOptions.AggressiveInlining)] private static float? ParseRadiusNullable(string? s) => ParseCoordNullable(s); private static bool TryParseStop(XElement stop, out float offset, out GlyphColor color) { offset = 0f; color = default; string? style = stop.Attribute("style")?.Value; string? offAttr = stop.Attribute("offset")?.Value; string? sc = stop.Attribute("stop-color")?.Value ?? TryCss(style, "stop-color"); string? so = stop.Attribute("stop-opacity")?.Value ?? TryCss(style, "stop-opacity"); if (!string.IsNullOrEmpty(offAttr)) { if (offAttr.EndsWith('%')) { if (float.TryParse(offAttr.AsSpan(0, offAttr.Length - 1), NumberStyles.Float, CultureInfo.InvariantCulture, out float p)) { offset = Math.Clamp(p / 100f, 0f, 1f); } } else if (float.TryParse(offAttr, NumberStyles.Float, CultureInfo.InvariantCulture, out float v)) { offset = Math.Clamp(v, 0f, 1f); } } GlyphColor baseColor = new(0, 0, 0, 255); if (!string.IsNullOrEmpty(sc) && TryParseColor(sc, out GlyphColor parsed)) { baseColor = parsed; } float aMul = 1f; if (!string.IsNullOrEmpty(so) && float.TryParse(so, NumberStyles.Float, CultureInfo.InvariantCulture, out float soVal)) { aMul = Math.Clamp(soVal, 0f, 1f); } byte a = (byte)Math.Clamp((int)Math.Round(baseColor.A * aMul), 0, 255); color = new GlyphColor(baseColor.R, baseColor.G, baseColor.B, a); return true; } private static GradientStop[] BuildStopsArray(List<(float Offset, GlyphColor Color)> list) { if (list.Count == 0) { return [ new GradientStop(0f, new GlyphColor(0, 0, 0, 255)), new GradientStop(1f, new GlyphColor(0, 0, 0, 255)) ]; } list.Sort((a, b) => a.Offset.CompareTo(b.Offset)); GradientStop[] stops = new GradientStop[list.Count]; for (int i = 0; i < list.Count; i++) { (float o, GlyphColor c) = list[i]; stops[i] = new GradientStop(Math.Clamp(o, 0f, 1f), c); } return stops; } private static string? TryCss(string? style, string prop) { if (string.IsNullOrEmpty(style)) { return null; } ReadOnlySpan span = style.AsSpan(); while (span.Length > 0) { int semi = span.IndexOf(';'); ReadOnlySpan part = semi >= 0 ? span[..semi] : span; span = semi >= 0 ? span[(semi + 1)..] : []; int colon = part.IndexOf(':'); if (colon <= 0) { continue; } ReadOnlySpan name = part[..colon].Trim(); if (name.Equals(prop.AsSpan(), StringComparison.OrdinalIgnoreCase)) { return part[(colon + 1)..].Trim().ToString(); } } return null; } private static bool TryParseColor(string s, out GlyphColor color) { if (GlyphColor.TryParseNamed(s, out color)) { return true; } if (GlyphColor.TryParseHex(s, out GlyphColor hex)) { color = hex; return true; } if (s.StartsWith("rgb", StringComparison.OrdinalIgnoreCase)) { int l = s.IndexOf('('); int r = s.IndexOf(')'); if (l >= 0 && r > l) { ReadOnlySpan inner = s.AsSpan(l + 1, r - l - 1); Span ranges = stackalloc Range[5]; int count = inner.Split(ranges, ','); if (count >= 3) { byte rr = ParseByte(inner[ranges[0]]); byte gg = ParseByte(inner[ranges[1]]); byte bb = ParseByte(inner[ranges[2]]); byte aa = 255; if (count >= 4 && float.TryParse(inner[ranges[3]].Trim(), NumberStyles.Float, CultureInfo.InvariantCulture, out float af)) { aa = (byte)Math.Clamp((int)Math.Round(255f * af), 0, 255); } color = new GlyphColor(rr, gg, bb, aa); return true; } } } return false; static byte ParseByte(ReadOnlySpan x) { if (x.IsEmpty) { return 0; } ReadOnlySpan t = x.Trim(); if (t[^1] == '%') { if (float.TryParse(t[..^1], NumberStyles.Float, CultureInfo.InvariantCulture, out float p)) { return (byte)Math.Clamp((int)Math.Round(255f * (p / 100f)), 0, 255); } return 0; } if (int.TryParse(t, NumberStyles.Integer, CultureInfo.InvariantCulture, out int v)) { return (byte)Math.Clamp(v, 0, 255); } return 0; } } private static bool TryExtractUrlId(string s, [NotNullWhen(true)] out string? id) { id = null; int lp = s.IndexOf("url(", StringComparison.OrdinalIgnoreCase); if (lp < 0) { return false; } int rp = s.IndexOf(')', lp + 4); if (rp < 0) { return false; } string inner = s[(lp + 4)..rp].Trim(); if (inner.Length > 1 && inner[0] == '#') { id = inner[1..]; return true; } return false; } private static XElement? LookupById(Dictionary idMap, string href) { if (string.IsNullOrEmpty(href) || href[0] != '#') { return null; } return idMap.TryGetValue(href.AsSpan(1).ToString(), out XElement? e) ? e : null; } private static string? GetHref(XElement e) { XNamespace xlink = "http://www.w3.org/1999/xlink"; return e.Attribute(xlink + "href")?.Value ?? e.Attribute("href")?.Value; } /// /// Returns cached path commands for an SVG path element, or parses and caches them /// when the element is a reusable definition with an id. /// /// The SVG node that owns the geometry. /// The raw SVG path data. /// The parsed SVG document and its caches. /// The parsed path commands. private static List GetOrBuildPathCommands(XElement node, string d, ParsedDoc parsedDoc) { if (TryGetCachedGeometry(node, parsedDoc, out List? cached, out string? geometryId)) { return cached; } return CacheGeometry(geometryId, parsedDoc, BuildCommandsFromPathData(d)); } /// /// Returns cached path commands for a polygon or polyline definition, or builds and caches them. /// /// The SVG node that owns the geometry. /// The parsed coordinate list. /// Whether the geometry should be explicitly closed. /// The parsed SVG document and its caches. /// The parsed path commands. private static List GetOrBuildPolyCommands(XElement node, float[] coords, bool close, ParsedDoc parsedDoc) { if (TryGetCachedGeometry(node, parsedDoc, out List? cached, out string? geometryId)) { return cached; } return CacheGeometry(geometryId, parsedDoc, BuildCommandsFromPoly(coords, close)); } /// /// Returns cached path commands for a rectangle definition, or builds and caches them. /// /// The SVG node that owns the geometry. /// The rectangle origin X. /// The rectangle origin Y. /// The rectangle width. /// The rectangle height. /// The parsed SVG document and its caches. /// The parsed path commands. private static List GetOrBuildRectCommands( XElement node, float x, float y, float w, float h, ParsedDoc parsedDoc) { if (TryGetCachedGeometry(node, parsedDoc, out List? cached, out string? geometryId)) { return cached; } float[] coords = [ x, y, x + w, y, x + w, y + h, x, y + h ]; return CacheGeometry(geometryId, parsedDoc, BuildCommandsFromPoly(coords, close: true)); } /// /// Returns cached path commands for an ellipse or circle definition, or builds and caches them. /// /// The SVG node that owns the geometry. /// The ellipse center X. /// The ellipse center Y. /// The ellipse radius on the X axis. /// The ellipse radius on the Y axis. /// The parsed SVG document and its caches. /// The parsed path commands. private static List GetOrBuildEllipseCommands( XElement node, float cx, float cy, float rx, float ry, ParsedDoc parsedDoc) { if (TryGetCachedGeometry(node, parsedDoc, out List? cached, out string? geometryId)) { return cached; } return CacheGeometry(geometryId, parsedDoc, BuildCommandsForEllipse(cx, cy, rx, ry)); } /// /// Looks up cached geometry for a reusable SVG element by its id. /// /// The SVG node that may have cached geometry. /// The parsed SVG document and its caches. /// When this method returns, contains the cached commands if found. /// When this method returns, contains the element id used as the cache key. /// if cached geometry was found; otherwise, . private static bool TryGetCachedGeometry( XElement node, ParsedDoc parsedDoc, [NotNullWhen(true)] out List? cached, [NotNullWhen(true)] out string? geometryId) { geometryId = node.Attribute("id")?.Value; if (geometryId is not null && parsedDoc.GeometryCache.TryGetValue(geometryId, out List? commands)) { cached = commands; return true; } cached = null; return false; } /// /// Stores geometry in the per-document cache when the /// source element has a reusable id. /// /// The cache key, or when the element is anonymous. /// The parsed SVG document and its caches. /// The newly built commands. /// The cached or materialized command list. private static List CacheGeometry(string? geometryId, ParsedDoc parsedDoc, List commands) { if (commands.Count == 0) { return []; } if (geometryId is not null) { parsedDoc.GeometryCache.TryAdd(geometryId, commands); } return commands; } private static List BuildCommandsFromPoly(float[] coords, bool close) { List cmds = []; Vector2 start = new(coords[0], coords[1]); cmds.Add(PathCommand.MoveTo(start)); Vector2 prev = start; for (int i = 2; i + 1 < coords.Length; i += 2) { Vector2 p = new(coords[i], coords[i + 1]); if (!NearlyEqual(prev, p)) { cmds.Add(PathCommand.LineTo(p)); prev = p; } } if (close && !NearlyEqual(prev, start)) { cmds.Add(PathCommand.LineTo(start)); cmds.Add(PathCommand.Close()); } return cmds; } private static List BuildCommandsForEllipse(float cx, float cy, float rx, float ry) { List cmds = []; // Start at (cx + rx, cy) Vector2 s = new(cx + rx, cy); cmds.Add(PathCommand.MoveTo(s)); // First half to (cx - rx, cy) Vector2 p1 = new(cx - rx, cy); cmds.Add(PathCommand.ArcTo(rx, ry, 0f, true, true, p1)); // Second half back to start Vector2 p2 = new(cx + rx, cy); cmds.Add(PathCommand.ArcTo(rx, ry, 0f, true, true, p2)); cmds.Add(PathCommand.Close()); return cmds; } private static List BuildCommandsFromPathData(string d) { List cmds = []; ReadOnlySpan s = d.AsSpan(); Vector2 first = default; Vector2 curr = default; Vector2 lastc = default; Vector2 p1, p2, p3; char op = '\0'; char prevOp = '\0'; bool rel = false; bool figureOpen = false; while (true) { s = s.TrimStart(); if (s.Length == 0) { break; } char ch = s[0]; if (char.IsDigit(ch) || ch == '-' || ch == '+' || ch == '.') { if (s.Length == 0 || op == 'Z') { return []; } } else if (IsSeparator(ch)) { s = TrimSeparator(s); } else { op = ch; rel = false; if (char.IsLower(op)) { op = char.ToUpper(op, CultureInfo.InvariantCulture); rel = true; } s = TrimSeparator(s[1..]); } switch (op) { case 'M': { s = FindPoint(s, rel, curr, out p1); if (figureOpen) { cmds.Add(PathCommand.Close()); } cmds.Add(PathCommand.MoveTo(p1)); first = curr = p1; prevOp = '\0'; op = 'L'; figureOpen = true; break; } case 'L': { s = FindPoint(s, rel, curr, out p1); if (!NearlyEqual(p1, curr)) { cmds.Add(PathCommand.LineTo(p1)); } curr = p1; break; } case 'H': { s = FindScaler(s, out float x); if (rel) { x += curr.X; } p1 = new Vector2(x, curr.Y); if (!NearlyEqual(p1, curr)) { cmds.Add(PathCommand.LineTo(p1)); } curr = p1; break; } case 'V': { s = FindScaler(s, out float y); if (rel) { y += curr.Y; } p1 = new Vector2(curr.X, y); if (!NearlyEqual(p1, curr)) { cmds.Add(PathCommand.LineTo(p1)); } curr = p1; break; } case 'C': { s = FindPoint(s, rel, curr, out p1); s = FindPoint(s, rel, curr, out p2); s = FindPoint(s, rel, curr, out p3); cmds.Add(PathCommand.CubicTo(p1, p2, p3)); lastc = p2; curr = p3; break; } case 'S': { s = FindPoint(s, rel, curr, out p2); s = FindPoint(s, rel, curr, out p3); p1 = curr; if (prevOp is 'C' or 'S') { p1.X -= lastc.X - curr.X; p1.Y -= lastc.Y - curr.Y; } cmds.Add(PathCommand.CubicTo(p1, p2, p3)); lastc = p2; curr = p3; break; } case 'Q': { s = FindPoint(s, rel, curr, out p1); s = FindPoint(s, rel, curr, out p2); cmds.Add(PathCommand.QuadraticTo(p1, p2)); lastc = p1; curr = p2; break; } case 'T': { s = FindPoint(s, rel, curr, out p2); p1 = curr; if (prevOp is 'Q' or 'T') { p1.X -= lastc.X - curr.X; p1.Y -= lastc.Y - curr.Y; } cmds.Add(PathCommand.QuadraticTo(p1, p2)); lastc = p1; curr = p2; break; } case 'A': { if (TryFindScaler(ref s, out float rx) && TryTrimSeparator(ref s) && TryFindScaler(ref s, out float ry) && TryTrimSeparator(ref s) && TryFindScaler(ref s, out float angle) && TryTrimSeparator(ref s) && TryFindScaler(ref s, out float largeArc) && TryTrimSeparator(ref s) && TryFindScaler(ref s, out float sweep) && TryFindPoint(ref s, rel, curr, out p1)) { cmds.Add(PathCommand.ArcTo(rx, ry, angle, largeArc == 1, sweep == 1, p1)); curr = p1; } break; } case 'Z': { if (figureOpen) { if (!NearlyEqual(curr, first)) { cmds.Add(PathCommand.LineTo(first)); } cmds.Add(PathCommand.Close()); curr = first; figureOpen = false; } break; } default: { return []; } } if (prevOp == 0) { first = curr; } prevOp = op; if (op == 'M') { figureOpen = true; } } return cmds; } private static bool TryParseViewBox(string? s, out float x, out float y, out float w, out float h) { x = 0f; y = 0f; w = 0f; h = 0f; if (string.IsNullOrEmpty(s)) { return false; } float[] v = ParseFloatList(s); if (v.Length == 4) { x = v[0]; y = v[1]; w = v[2]; h = v[3]; return true; } return false; } private static Matrix3x2 ParseTransform(string? s) { if (string.IsNullOrEmpty(s)) { return Matrix3x2.Identity; } Matrix3x2 m = Matrix3x2.Identity; int i = 0; int n = s.Length; while (i < n) { SkipSep(s, ref i); if (i >= n) { break; } int start = i; while (i < n && char.IsLetter(s[i])) { i++; } ReadOnlySpan op = s.AsSpan(start, i - start); SkipSep(s, ref i); if (i >= n || s[i] != '(') { break; } i++; // '(' int argsStart = i; int depth = 1; while (i < n && depth > 0) { if (s[i] == '(') { depth++; } else if (s[i] == ')') { depth--; } i++; } ReadOnlySpan args = s.AsSpan(argsStart, (i - argsStart) - 1); float[] a = ParseFloatList(args); Matrix3x2 t = Matrix3x2.Identity; if (op.SequenceEqual("matrix")) { if (a.Length >= 6) { t = new Matrix3x2(a[0], a[1], a[2], a[3], a[4], a[5]); } } else if (op.SequenceEqual("translate")) { if (a.Length == 1) { t = Matrix3x2.CreateTranslation(a[0], 0f); } else if (a.Length >= 2) { t = Matrix3x2.CreateTranslation(a[0], a[1]); } } else if (op.SequenceEqual("scale")) { if (a.Length == 1) { t = Matrix3x2.CreateScale(a[0], a[0]); } else if (a.Length >= 2) { t = Matrix3x2.CreateScale(a[0], a[1]); } } else if (op.SequenceEqual("rotate")) { if (a.Length >= 1) { t = Matrix3x2.CreateRotation(a[0] * (float)(Math.PI / 180.0)); } } else if (op.SequenceEqual("skewX")) { if (a.Length >= 1) { t = new Matrix3x2(1f, 0f, MathF.Tan(a[0] * (float)(Math.PI / 180.0)), 1f, 0f, 0f); } } else if (op.SequenceEqual("skewY")) { if (a.Length >= 1) { t = new Matrix3x2(1f, MathF.Tan(a[0] * (float)(Math.PI / 180.0)), 0f, 1f, 0f, 0f); } } m *= t; SkipSep(s, ref i); } return m; static void SkipSep(string s, ref int i) { int n = s.Length; while (i < n) { char c = s[i]; if (char.IsWhiteSpace(c) || c == ',') { i++; } else { break; } } } } private static ReadOnlySpan FindPoint(ReadOnlySpan str, bool rel, Vector2 current, out Vector2 value) { str = FindScaler(str, out float x); str = FindScaler(str, out float y); if (rel) { x += current.X; y += current.Y; } value = new Vector2(x, y); return str; } private static ReadOnlySpan FindScaler(ReadOnlySpan str, out float scaler) { str = TrimSeparator(str); scaler = 0f; for (int i = 0; i < str.Length; i++) { if (IsSeparator(str[i])) { scaler = ParseFloat(str[..i]); return str[i..]; } } if (str.Length > 0) { scaler = ParseFloat(str); } return []; } private static bool TryTrimSeparator(ref ReadOnlySpan str) { ReadOnlySpan result = TrimSeparator(str); if (str[^result.Length..].StartsWith(result)) { str = result; return true; } return false; } private static bool TryFindScaler(ref ReadOnlySpan str, out float value) { ReadOnlySpan result = FindScaler(str, out float v); if (str[^result.Length..].StartsWith(result)) { value = v; str = result; return true; } value = default; return false; } private static bool TryFindPoint(ref ReadOnlySpan str, bool relative, Vector2 current, out Vector2 value) { ReadOnlySpan result = FindPoint(str, relative, current, out Vector2 v); if (str[^result.Length..].StartsWith(result)) { value = v; str = result; return true; } value = default; return false; } private static bool IsSeparator(char ch) => char.IsWhiteSpace(ch) || ch == ','; private static ReadOnlySpan TrimSeparator(ReadOnlySpan s) { int idx = 0; for (; idx < s.Length; idx++) { if (!IsSeparator(s[idx])) { break; } } return s[idx..]; } private static float ParseFloat(ReadOnlySpan str) => str.IsEmpty ? 0 : float.Parse(str, CultureInfo.InvariantCulture); private static float[] ParseFloatList(string s) => string.IsNullOrEmpty(s) ? [] : ParseFloatList(s.AsSpan()); private static float[] ParseFloatList(ReadOnlySpan s) { if (s.IsEmpty) { return []; } List vals = []; int i = 0; int n = s.Length; while (i < n) { while (i < n && (char.IsWhiteSpace(s[i]) || s[i] == ',')) { i++; } if (i >= n) { break; } int start = i; if (s[i] is '+' or '-') { i++; } bool dot = false; while (i < n) { char c = s[i]; if (char.IsDigit(c)) { i++; continue; } if (c == '.' && !dot) { dot = true; i++; continue; } break; } if (i < n && (s[i] == 'e' || s[i] == 'E')) { i++; if (i < n && (s[i] == '+' || s[i] == '-')) { i++; } while (i < n && char.IsDigit(s[i])) { i++; } } if (float.TryParse(s[start..i], NumberStyles.Float, CultureInfo.InvariantCulture, out float v)) { vals.Add(v); } } return [.. vals]; } private static bool NearlyEqual(in Vector2 a, in Vector2 b, float eps = 1e-3f) => MathF.Abs(a.X - b.X) <= eps && MathF.Abs(a.Y - b.Y) <= eps; private sealed class ParsedDoc { public required XDocument Doc { get; init; } public required Dictionary IdMap { get; init; } /// /// Gets the per-document cache of parsed geometry for reusable SVG defs. /// public ConcurrentDictionary> GeometryCache { get; } = new(StringComparer.Ordinal); /// /// Gets the per-document cache of resolved paint servers. /// public ConcurrentDictionary PaintServerCache { get; } = new(StringComparer.Ordinal); } } }