using Lib.Utils; using SharpDX.WIC; using Svg; using Svg.Transforms; namespace Lib.image.generate.load; [Guid("d05739d3-f89d-488d-85d0-c0d115265b75")] internal sealed class LoadSvgAsTexture2D : Instance, IDescriptiveFilename, IStatusProvider { [Output(Guid = "ab1e6a50-4ac1-43ba-8323-81fc8af29814")] public readonly Slot Texture = new(); public LoadSvgAsTexture2D() { _svgResource = new Resource(Path, SvgLoader.TryLoad); _svgResource.AddDependentSlots(Texture); Texture.UpdateAction = UpdateTexture; } private void UpdateTexture(EvaluationContext context) { if (_svgResource.TryGetValue(context, out var svgDocument)) { var resolution = Resolution.GetValue(context); var useViewBox = UseViewBox.GetValue(context); var scale = Scale.GetValue(context); var selectLayerRange = SelectLayerRange.GetValue(context); var splitToLayers = SplitToLayers.GetValue(context); if (svgDocument == null) { return; } try { // Work with an SvgDocument to preserve coordinate systems var workingDocument = svgDocument; if (splitToLayers) { // Get actual content layers with their parent attributes var contentLayersWithParents = GetContentLayersWithParentAttributes(svgDocument).ToList(); Log.Info($"Found {contentLayersWithParents.Count} layers in {Path.Value}", this); if (contentLayersWithParents.Count > 0) { // Validate and normalize range var startIndex = Math.Max(0, Math.Min(selectLayerRange.X, contentLayersWithParents.Count - 1)); var endIndex = Math.Max(0, Math.Min(selectLayerRange.Y, contentLayersWithParents.Count - 1)); // Make sure start <= end if (startIndex > endIndex) { (endIndex, startIndex) = (startIndex, endIndex); } // Create a new document with the same properties workingDocument = new SvgDocument { ViewBox = svgDocument.ViewBox, AspectRatio = svgDocument.AspectRatio, Width = svgDocument.Width, Height = svgDocument.Height }; // Copy document-level definitions if they exist (important for styles, gradients, etc.) var defs = svgDocument.Children.OfType().FirstOrDefault(); if (defs != null) { workingDocument.Children.Add((SvgElement)defs.DeepCopy()); } // Add selected layers to the new document // ElementRange 0,1 means layers at index 0 and 1 (first two layers) // ElementRange 1,5 means layers at indices 1,2,3,4,5 for (var i = startIndex; i <= endIndex; i++) { var (element, parentGroup) = contentLayersWithParents[i]; // Deep copy to preserve entire hierarchy var clonedElement = (SvgElement)element.DeepCopy(); // If there was a parent group with attributes, wrap the element if (parentGroup != null) { // Create a wrapper group with all parent attributes var wrapperGroup = (SvgGroup)parentGroup.DeepCopy(); // Clear any children from the parent copy wrapperGroup.Children.Clear(); // Add our cloned element wrapperGroup.Children.Add(clonedElement); workingDocument.Children.Add(wrapperGroup); } else { // No parent attributes, add directly workingDocument.Children.Add(clonedElement); } } } } // Rasterize the SVG to a bitmap System.Drawing.Bitmap rasterizedBitmap; var width = 0; var height = 0; if (resolution.X == 0 && resolution.Y == 0 && !useViewBox) { height = context.RequestedResolution.Height; } else if (!useViewBox) { width = resolution.X; height = resolution.Y; } else { width = (int)(workingDocument.ViewBox.Width * scale); height = (int)(workingDocument.ViewBox.Height * scale); } // Render to bitmap at the desired resolution rasterizedBitmap = workingDocument.Draw(width, height); if (rasterizedBitmap == null) { _lastErrorMessage = "Failed to rasterize SVG: " + Path.Value; Log.Warning(_lastErrorMessage, this); Texture.Value = null; Texture.DirtyFlag.Clear(); return; } // Convert System.Drawing.Bitmap to Texture2D using SharpDX Texture.Value = ConvertBitmapToTexture2D(rasterizedBitmap); Texture.DirtyFlag.Clear(); // Dispose the bitmap after conversion rasterizedBitmap.Dispose(); if (Texture.Value == null) { _lastErrorMessage = "Failed to convert bitmap to texture: " + Path.Value; Log.Warning(_lastErrorMessage, this); return; } var currentSrv = SrvManager.GetSrvForTexture(Texture.Value); try { ResourceManager.Device.ImmediateContext.GenerateMips(currentSrv); } catch (Exception exception) { Log.Error($"Failed to generate mipmaps for texture {Path.Value}: " + exception); } _lastErrorMessage = string.Empty; } catch (Exception ex) { _lastErrorMessage = $"Error processing SVG: {ex.Message}"; Log.Error(_lastErrorMessage, this); Texture.Value = null; Texture.DirtyFlag.Clear(); } } else { // Handle loading failure _lastErrorMessage = "Failed to load SVG document: " + Path.Value; Log.Warning(_lastErrorMessage, this); Texture.Value = null; Texture.DirtyFlag.Clear(); } } /// /// Gets the actual content layers from an SVG document, unwrapping single-child containers. /// This handles cases where SVG editors wrap everything in unnecessary container groups. /// Returns tuples of (element, parent group with all attributes to preserve). /// private static IEnumerable<(SvgElement element, SvgGroup parentGroup)> GetContentLayersWithParentAttributes(SvgDocument document) { // Get top-level children that are visual or groups (exclude defs, metadata, etc.) var topLevelChildren = document.Children .Where(child => child is SvgVisualElement || child is SvgGroup) .ToList(); // If there's only one top-level child and it's a group, check if we should unwrap it if (topLevelChildren.Count == 1 && topLevelChildren[0] is SvgGroup singleGroup) { // Get the children of this single group var groupChildren = singleGroup.Children .Where(child => child is SvgVisualElement || child is SvgGroup) .ToList(); // If the group has multiple children, those are likely the actual layers if (groupChildren.Count > 1) { // Return children with the parent group return groupChildren.Select(child => (child, singleGroup)); } else if (groupChildren.Count == 1 && groupChildren[0] is SvgGroup nestedGroup) { // Recursively unwrap nested single-child groups var nestedChildren = nestedGroup.Children .Where(child => child is SvgVisualElement || child is SvgGroup) .ToList(); if (nestedChildren.Count > 0) { // Create a merged parent group that combines attributes from both levels var mergedParent = MergeParentGroups(singleGroup, nestedGroup); return nestedChildren.Select(child => (child, mergedParent)); } } } // Default: return the top-level children with no parent group return topLevelChildren.Select(child => (child, (SvgGroup)null)); } /// /// Merges attributes from nested parent groups into a single group. /// Outer group attributes are applied first, then inner group attributes. /// private static SvgGroup MergeParentGroups(SvgGroup outer, SvgGroup inner) { var merged = new SvgGroup { // Combine transforms (outer first, then inner) Transforms = [] }; if (outer.Transforms != null) { foreach (var transform in outer.Transforms) { merged.Transforms.Add(transform.Clone() as SvgTransform); } } if (inner.Transforms != null) { foreach (var transform in inner.Transforms) { merged.Transforms.Add(transform.Clone() as SvgTransform); } } // Copy other attributes (inner overrides outer if both are set) merged.Opacity = inner.Opacity != 1.0f ? inner.Opacity : outer.Opacity; merged.Fill = inner.Fill ?? outer.Fill; merged.Stroke = inner.Stroke ?? outer.Stroke; // SvgUnit is a struct, so use IsEmpty to check for "unset" value merged.StrokeWidth = !inner.StrokeWidth.IsEmpty ? inner.StrokeWidth : outer.StrokeWidth; merged.StrokeLineCap = inner.StrokeLineCap != SvgStrokeLineCap.Butt ? inner.StrokeLineCap : outer.StrokeLineCap; merged.StrokeLineJoin = inner.StrokeLineJoin != SvgStrokeLineJoin.Miter ? inner.StrokeLineJoin : outer.StrokeLineJoin; merged.StrokeDashArray = inner.StrokeDashArray ?? outer.StrokeDashArray; // SvgUnit is a struct, so use IsEmpty to check for "unset" value merged.StrokeDashOffset = !inner.StrokeDashOffset.IsEmpty ? inner.StrokeDashOffset : outer.StrokeDashOffset; merged.FillOpacity = inner.FillOpacity != 1.0f ? inner.FillOpacity : outer.FillOpacity; merged.StrokeOpacity = inner.StrokeOpacity != 1.0f ? inner.StrokeOpacity : outer.StrokeOpacity; return merged; } private static Texture2D ConvertBitmapToTexture2D(System.Drawing.Bitmap bitmap) { using var memoryStream = new MemoryStream(); // Save the bitmap to a memory stream as PNG bitmap.Save(memoryStream, System.Drawing.Imaging.ImageFormat.Png); memoryStream.Position = 0; // Use WIC to load the bitmap using var factory = new ImagingFactory(); using var bitmapDecoder = new BitmapDecoder(factory, memoryStream, DecodeOptions.CacheOnDemand); using var formatConverter = new FormatConverter(factory); using var bitmapFrameDecode = bitmapDecoder.GetFrame(0); // Convert to RGBA format formatConverter.Initialize( bitmapFrameDecode, PixelFormat.Format32bppRGBA, BitmapDitherType.None, null, 0.0, BitmapPaletteType.Custom); return Texture2D.CreateFromBitmap(ResourceManager.Device, formatConverter); } public InputSlot SourcePathSlot => Path; private readonly Resource _svgResource; IStatusProvider.StatusLevel IStatusProvider.GetStatusLevel() => string.IsNullOrEmpty(_lastErrorMessage) ? IStatusProvider.StatusLevel.Success : IStatusProvider.StatusLevel.Warning; string IStatusProvider.GetStatusMessage() => _lastErrorMessage; private string _lastErrorMessage = string.Empty; [Input(Guid = "f4860e75-eff7-4e6e-a144-016ff5bb054e")] public readonly InputSlot Path = new(); [Input(Guid = "2063A2A5-305D-4022-A1AF-0840CDAA8BE4")] public readonly InputSlot Resolution = new(); [Input(Guid = "A01425AD-E38F-4A80-B8CF-A2EA03895FE2")] public readonly InputSlot UseViewBox = new(); [Input(Guid = "89CD0433-8646-47F3-A22E-AFE1344C6F07")] public readonly InputSlot Scale = new(); [Input(Guid = "A13F5039-65A7-45B2-9407-50D718CCB13B")] public readonly InputSlot SplitToLayers = new(); [Input(Guid = "3E8F9A1B-2C4D-5E6F-7A8B-9C0D1E2F3A4B")] public readonly InputSlot SelectLayerRange = new(); }