From f558c2ebe6d21d416fd737faa0a66a9ff38979b2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Wies=C5=82aw=20=C5=A0olt=C3=A9s?= Date: Wed, 13 May 2026 13:53:49 +0200 Subject: [PATCH 1/9] Update SVG upstream submodule --- externals/SVG | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/externals/SVG b/externals/SVG index 89d829c1a7..853a9ae8d7 160000 --- a/externals/SVG +++ b/externals/SVG @@ -1 +1 @@ -Subproject commit 89d829c1a71f1797baf4b7e46a8547f60382e7cf +Subproject commit 853a9ae8d7e34415ff6a0c4a24d4ef29032c72fa From 7f107ee5ef89027934ed9623d0b464db2437ba9f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Wies=C5=82aw=20=C5=A0olt=C3=A9s?= Date: Wed, 13 May 2026 13:54:04 +0200 Subject: [PATCH 2/9] Update SkiaSharp package references to v3 --- Directory.Packages.props | 14 +++++++------- Svg.Skia.slnx | 3 +-- build/SkiaSharp.Avalonia.props | 11 +++++++++++ build/SkiaSharp.Native.v3.props | 9 --------- build/SkiaSharp.v3.props | 6 ------ samples/AvalonDraw/AvalonDraw.csproj | 5 +++-- .../AvaloniaControlsSample.csproj | 5 +++-- .../AvaloniaSKPictureImageSample.csproj | 5 +++-- .../AvaloniaSvgSkiaSample.csproj | 5 +++-- .../AvaloniaSvgSkiaStylingSample.csproj | 5 +++-- .../Svg.Skia.Converter/Svg.Skia.Converter.csproj | 4 ++-- .../Svg.SourceGenerator.Skia.Sample.csproj | 4 ++-- samples/SvgToPng/SvgToPng.csproj | 4 ++-- samples/TestApp/TestApp.csproj | 5 +++-- .../Svg.Controls.Skia.Uno.csproj | 2 +- src/SvgML.Uno/SvgML.Uno.csproj | 2 +- .../Avalonia.Svg.Skia.UiTests.csproj | 5 +++-- .../Svg.Controls.Skia.Avalonia.UnitTests.csproj | 5 +++-- .../Svg.Controls.Skia.Uno.UnitTests.csproj | 4 ++-- .../Svg.Editor.Skia.Avalonia.UnitTests.csproj | 5 +++-- .../Svg.Editor.Skia.UnitTests.csproj | 4 ++-- .../Svg.Editor.Svg.UnitTests.csproj | 4 ++-- .../Svg.Skia.Benchmarks/Svg.Skia.Benchmarks.csproj | 4 ++-- tests/Svg.Skia.UnitTests/Svg.Skia.UnitTests.csproj | 4 ++-- 24 files changed, 64 insertions(+), 60 deletions(-) create mode 100644 build/SkiaSharp.Avalonia.props delete mode 100644 build/SkiaSharp.Native.v3.props delete mode 100644 build/SkiaSharp.v3.props diff --git a/Directory.Packages.props b/Directory.Packages.props index 8be8e7586d..c752e55c31 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -21,13 +21,13 @@ - - - - - - - + + + + + + + diff --git a/Svg.Skia.slnx b/Svg.Skia.slnx index 5add670187..c65fe404ee 100644 --- a/Svg.Skia.slnx +++ b/Svg.Skia.slnx @@ -38,10 +38,9 @@ + - - diff --git a/build/SkiaSharp.Avalonia.props b/build/SkiaSharp.Avalonia.props new file mode 100644 index 0000000000..ee62c59d64 --- /dev/null +++ b/build/SkiaSharp.Avalonia.props @@ -0,0 +1,11 @@ + + + + + + + + + + + diff --git a/build/SkiaSharp.Native.v3.props b/build/SkiaSharp.Native.v3.props deleted file mode 100644 index 96cbc5725f..0000000000 --- a/build/SkiaSharp.Native.v3.props +++ /dev/null @@ -1,9 +0,0 @@ - - - - - - - - - diff --git a/build/SkiaSharp.v3.props b/build/SkiaSharp.v3.props deleted file mode 100644 index cb7734c379..0000000000 --- a/build/SkiaSharp.v3.props +++ /dev/null @@ -1,6 +0,0 @@ - - - - - - diff --git a/samples/AvalonDraw/AvalonDraw.csproj b/samples/AvalonDraw/AvalonDraw.csproj index bbf12b108c..d2e5a6dc0a 100644 --- a/samples/AvalonDraw/AvalonDraw.csproj +++ b/samples/AvalonDraw/AvalonDraw.csproj @@ -16,8 +16,9 @@ - - + + + diff --git a/samples/AvaloniaControlsSample/AvaloniaControlsSample.csproj b/samples/AvaloniaControlsSample/AvaloniaControlsSample.csproj index 42dd7ee9fc..0aa16752e7 100644 --- a/samples/AvaloniaControlsSample/AvaloniaControlsSample.csproj +++ b/samples/AvaloniaControlsSample/AvaloniaControlsSample.csproj @@ -16,8 +16,9 @@ - - + + + diff --git a/samples/AvaloniaSKPictureImageSample/AvaloniaSKPictureImageSample.csproj b/samples/AvaloniaSKPictureImageSample/AvaloniaSKPictureImageSample.csproj index 128bee025c..4f801fc93a 100644 --- a/samples/AvaloniaSKPictureImageSample/AvaloniaSKPictureImageSample.csproj +++ b/samples/AvaloniaSKPictureImageSample/AvaloniaSKPictureImageSample.csproj @@ -18,8 +18,9 @@ - - + + + diff --git a/samples/AvaloniaSvgSkiaSample/AvaloniaSvgSkiaSample.csproj b/samples/AvaloniaSvgSkiaSample/AvaloniaSvgSkiaSample.csproj index 28197c8cf0..f6b18ed930 100644 --- a/samples/AvaloniaSvgSkiaSample/AvaloniaSvgSkiaSample.csproj +++ b/samples/AvaloniaSvgSkiaSample/AvaloniaSvgSkiaSample.csproj @@ -16,8 +16,9 @@ - - + + + diff --git a/samples/AvaloniaSvgSkiaStylingSample/AvaloniaSvgSkiaStylingSample.csproj b/samples/AvaloniaSvgSkiaStylingSample/AvaloniaSvgSkiaStylingSample.csproj index 54a274b595..d8e5022103 100644 --- a/samples/AvaloniaSvgSkiaStylingSample/AvaloniaSvgSkiaStylingSample.csproj +++ b/samples/AvaloniaSvgSkiaStylingSample/AvaloniaSvgSkiaStylingSample.csproj @@ -17,8 +17,9 @@ - - + + + diff --git a/samples/Svg.Skia.Converter/Svg.Skia.Converter.csproj b/samples/Svg.Skia.Converter/Svg.Skia.Converter.csproj index aec910ca9d..724ac187f1 100644 --- a/samples/Svg.Skia.Converter/Svg.Skia.Converter.csproj +++ b/samples/Svg.Skia.Converter/Svg.Skia.Converter.csproj @@ -32,8 +32,8 @@ - - + + diff --git a/samples/Svg.SourceGenerator.Skia.Sample/Svg.SourceGenerator.Skia.Sample.csproj b/samples/Svg.SourceGenerator.Skia.Sample/Svg.SourceGenerator.Skia.Sample.csproj index f41a8ad65d..4e22fce9d9 100644 --- a/samples/Svg.SourceGenerator.Skia.Sample/Svg.SourceGenerator.Skia.Sample.csproj +++ b/samples/Svg.SourceGenerator.Skia.Sample/Svg.SourceGenerator.Skia.Sample.csproj @@ -17,8 +17,8 @@ - - + + diff --git a/samples/SvgToPng/SvgToPng.csproj b/samples/SvgToPng/SvgToPng.csproj index cbeb514314..732c6bb69e 100644 --- a/samples/SvgToPng/SvgToPng.csproj +++ b/samples/SvgToPng/SvgToPng.csproj @@ -27,8 +27,8 @@ - - + + diff --git a/samples/TestApp/TestApp.csproj b/samples/TestApp/TestApp.csproj index 54d8280cc5..4012f14c54 100644 --- a/samples/TestApp/TestApp.csproj +++ b/samples/TestApp/TestApp.csproj @@ -20,8 +20,9 @@ - - + + + diff --git a/src/Svg.Controls.Skia.Uno/Svg.Controls.Skia.Uno.csproj b/src/Svg.Controls.Skia.Uno/Svg.Controls.Skia.Uno.csproj index c36d589837..52c65cf29d 100644 --- a/src/Svg.Controls.Skia.Uno/Svg.Controls.Skia.Uno.csproj +++ b/src/Svg.Controls.Skia.Uno/Svg.Controls.Skia.Uno.csproj @@ -25,7 +25,7 @@ - + diff --git a/src/SvgML.Uno/SvgML.Uno.csproj b/src/SvgML.Uno/SvgML.Uno.csproj index c8df155845..730fa70c9a 100644 --- a/src/SvgML.Uno/SvgML.Uno.csproj +++ b/src/SvgML.Uno/SvgML.Uno.csproj @@ -27,7 +27,7 @@ - + diff --git a/tests/Avalonia.Svg.Skia.UiTests/Avalonia.Svg.Skia.UiTests.csproj b/tests/Avalonia.Svg.Skia.UiTests/Avalonia.Svg.Skia.UiTests.csproj index d2fc89fd08..24fe6b8664 100644 --- a/tests/Avalonia.Svg.Skia.UiTests/Avalonia.Svg.Skia.UiTests.csproj +++ b/tests/Avalonia.Svg.Skia.UiTests/Avalonia.Svg.Skia.UiTests.csproj @@ -9,8 +9,9 @@ - - + + + diff --git a/tests/Svg.Controls.Skia.Avalonia.UnitTests/Svg.Controls.Skia.Avalonia.UnitTests.csproj b/tests/Svg.Controls.Skia.Avalonia.UnitTests/Svg.Controls.Skia.Avalonia.UnitTests.csproj index f0a7aef37a..cab8624ee2 100644 --- a/tests/Svg.Controls.Skia.Avalonia.UnitTests/Svg.Controls.Skia.Avalonia.UnitTests.csproj +++ b/tests/Svg.Controls.Skia.Avalonia.UnitTests/Svg.Controls.Skia.Avalonia.UnitTests.csproj @@ -10,8 +10,9 @@ - - + + + diff --git a/tests/Svg.Controls.Skia.Uno.UnitTests/Svg.Controls.Skia.Uno.UnitTests.csproj b/tests/Svg.Controls.Skia.Uno.UnitTests/Svg.Controls.Skia.Uno.UnitTests.csproj index 0a77f34fc8..ccf7ca44fa 100644 --- a/tests/Svg.Controls.Skia.Uno.UnitTests/Svg.Controls.Skia.Uno.UnitTests.csproj +++ b/tests/Svg.Controls.Skia.Uno.UnitTests/Svg.Controls.Skia.Uno.UnitTests.csproj @@ -10,8 +10,8 @@ - - + + diff --git a/tests/Svg.Editor.Skia.Avalonia.UnitTests/Svg.Editor.Skia.Avalonia.UnitTests.csproj b/tests/Svg.Editor.Skia.Avalonia.UnitTests/Svg.Editor.Skia.Avalonia.UnitTests.csproj index eea82096ae..4b337c5a26 100644 --- a/tests/Svg.Editor.Skia.Avalonia.UnitTests/Svg.Editor.Skia.Avalonia.UnitTests.csproj +++ b/tests/Svg.Editor.Skia.Avalonia.UnitTests/Svg.Editor.Skia.Avalonia.UnitTests.csproj @@ -10,8 +10,9 @@ - - + + + diff --git a/tests/Svg.Editor.Skia.UnitTests/Svg.Editor.Skia.UnitTests.csproj b/tests/Svg.Editor.Skia.UnitTests/Svg.Editor.Skia.UnitTests.csproj index 49d2e8f5f7..62a5d87e13 100644 --- a/tests/Svg.Editor.Skia.UnitTests/Svg.Editor.Skia.UnitTests.csproj +++ b/tests/Svg.Editor.Skia.UnitTests/Svg.Editor.Skia.UnitTests.csproj @@ -9,8 +9,8 @@ - - + + diff --git a/tests/Svg.Editor.Svg.UnitTests/Svg.Editor.Svg.UnitTests.csproj b/tests/Svg.Editor.Svg.UnitTests/Svg.Editor.Svg.UnitTests.csproj index bfbfa48cd1..12a490dd08 100644 --- a/tests/Svg.Editor.Svg.UnitTests/Svg.Editor.Svg.UnitTests.csproj +++ b/tests/Svg.Editor.Svg.UnitTests/Svg.Editor.Svg.UnitTests.csproj @@ -9,8 +9,8 @@ - - + + diff --git a/tests/Svg.Skia.Benchmarks/Svg.Skia.Benchmarks.csproj b/tests/Svg.Skia.Benchmarks/Svg.Skia.Benchmarks.csproj index ca4c6c7152..82e2182616 100644 --- a/tests/Svg.Skia.Benchmarks/Svg.Skia.Benchmarks.csproj +++ b/tests/Svg.Skia.Benchmarks/Svg.Skia.Benchmarks.csproj @@ -8,8 +8,8 @@ - - + + diff --git a/tests/Svg.Skia.UnitTests/Svg.Skia.UnitTests.csproj b/tests/Svg.Skia.UnitTests/Svg.Skia.UnitTests.csproj index f384fff76b..a07dffc6c5 100644 --- a/tests/Svg.Skia.UnitTests/Svg.Skia.UnitTests.csproj +++ b/tests/Svg.Skia.UnitTests/Svg.Skia.UnitTests.csproj @@ -10,8 +10,8 @@ - - + + From 5a1b6edd0e95939aa7bb07ca7ef5bb2e031a6ac3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Wies=C5=82aw=20=C5=A0olt=C3=A9s?= Date: Wed, 13 May 2026 13:57:00 +0200 Subject: [PATCH 3/9] Use static SkiaSharp sampling APIs --- .../SkiaCSharpModelExtensions.cs | 2 +- src/Svg.Skia/SKPictureExtensions.cs | 7 +- src/Svg.Skia/SKSvg.Model.cs | 32 --- src/Svg.Skia/SkiaModel.Caching.cs | 5 +- src/Svg.Skia/SkiaModel.cs | 231 ++---------------- 5 files changed, 28 insertions(+), 249 deletions(-) diff --git a/src/Svg.CodeGen.Skia/SkiaCSharpModelExtensions.cs b/src/Svg.CodeGen.Skia/SkiaCSharpModelExtensions.cs index e24e3b08b8..235c7bbb16 100644 --- a/src/Svg.CodeGen.Skia/SkiaCSharpModelExtensions.cs +++ b/src/Svg.CodeGen.Skia/SkiaCSharpModelExtensions.cs @@ -962,7 +962,7 @@ public static void ToSKImageFilter(this SKImageFilter? imageFilter, SkiaCSharpCo sb.AppendLine($"{indent} {counter.ImageVarName}{counterImage},"); sb.AppendLine($"{indent} {imageImageFilter.Src.ToSKRect()},"); sb.AppendLine($"{indent} {imageImageFilter.Dst.ToSKRect()},"); - sb.AppendLine($"{indent} SKFilterQuality.High);"); + sb.AppendLine($"{indent} {imageImageFilter.FilterQuality.ToSKSamplingOptions()});"); return; } case MatrixConvolutionImageFilter matrixConvolutionImageFilter: diff --git a/src/Svg.Skia/SKPictureExtensions.cs b/src/Svg.Skia/SKPictureExtensions.cs index 4cea094419..6fa1079f80 100644 --- a/src/Svg.Skia/SKPictureExtensions.cs +++ b/src/Svg.Skia/SKPictureExtensions.cs @@ -291,9 +291,12 @@ private static void Downsample(SkiaSharp.SKImage renderImage, SkiaSharp.SKImageI using var paint = new SkiaSharp.SKPaint { IsAntialias = true, - FilterQuality = SkiaSharp.SKFilterQuality.High, BlendMode = SkiaSharp.SKBlendMode.Src }; - targetCanvas.DrawImage(renderImage, SkiaSharp.SKRect.Create(0f, 0f, targetImageInfo.Width, targetImageInfo.Height), paint); + targetCanvas.DrawImage( + renderImage, + SkiaSharp.SKRect.Create(0f, 0f, targetImageInfo.Width, targetImageInfo.Height), + new SkiaSharp.SKSamplingOptions(SkiaSharp.SKCubicResampler.Mitchell), + paint); } } diff --git a/src/Svg.Skia/SKSvg.Model.cs b/src/Svg.Skia/SKSvg.Model.cs index 358f938318..b9da6f2b63 100644 --- a/src/Svg.Skia/SKSvg.Model.cs +++ b/src/Svg.Skia/SKSvg.Model.cs @@ -2,7 +2,6 @@ // Licensed under the MIT license. See LICENSE file in the project root for details. using System; using System.Collections.Generic; -using System.Diagnostics.CodeAnalysis; using System.IO; using System.Threading; using System.Xml; @@ -23,7 +22,6 @@ private enum SourceFormat public static bool CacheOriginalStream { get; set; } - [RequiresUnreferencedCode("SVG document parsing may use trim-unsafe runtime discovery paths.")] public static SKSvg CreateFromStream(System.IO.Stream stream, SvgParameters? parameters = null) { var skSvg = new SKSvg(); @@ -31,10 +29,8 @@ public static SKSvg CreateFromStream(System.IO.Stream stream, SvgParameters? par return skSvg; } - [RequiresUnreferencedCode("SVG document parsing may use trim-unsafe runtime discovery paths.")] public static SKSvg CreateFromStream(System.IO.Stream stream) => CreateFromStream(stream, null); - [RequiresUnreferencedCode("SVG document parsing may use trim-unsafe runtime discovery paths.")] public static SKSvg CreateFromFile(string path, SvgParameters? parameters = null) { var skSvg = new SKSvg(); @@ -42,10 +38,8 @@ public static SKSvg CreateFromFile(string path, SvgParameters? parameters = null return skSvg; } - [RequiresUnreferencedCode("SVG document parsing may use trim-unsafe runtime discovery paths.")] public static SKSvg CreateFromFile(string path) => CreateFromFile(path, null); - [RequiresUnreferencedCode("VectorDrawable parsing may use trim-unsafe runtime discovery paths.")] public static SKSvg CreateFromVectorDrawable(string path, SvgParameters? parameters = null) { var skSvg = new SKSvg(); @@ -53,7 +47,6 @@ public static SKSvg CreateFromVectorDrawable(string path, SvgParameters? paramet return skSvg; } - [RequiresUnreferencedCode("VectorDrawable parsing may use trim-unsafe runtime discovery paths.")] public static SKSvg CreateFromVectorDrawable(System.IO.Stream stream, SvgParameters? parameters = null) { var skSvg = new SKSvg(); @@ -61,7 +54,6 @@ public static SKSvg CreateFromVectorDrawable(System.IO.Stream stream, SvgParamet return skSvg; } - [RequiresUnreferencedCode("VectorDrawable parsing may use trim-unsafe runtime discovery paths.")] public static SKSvg CreateFromVectorDrawable(XmlReader reader) { var skSvg = new SKSvg(); @@ -69,7 +61,6 @@ public static SKSvg CreateFromVectorDrawable(XmlReader reader) return skSvg; } - [RequiresUnreferencedCode("SVG document parsing may use trim-unsafe runtime discovery paths.")] public static SKSvg CreateFromXmlReader(XmlReader reader) { var skSvg = new SKSvg(); @@ -77,7 +68,6 @@ public static SKSvg CreateFromXmlReader(XmlReader reader) return skSvg; } - [RequiresUnreferencedCode("SVG document parsing may use trim-unsafe runtime discovery paths.")] public static SKSvg CreateFromSvg(string svg) { var skSvg = new SKSvg(); @@ -85,7 +75,6 @@ public static SKSvg CreateFromSvg(string svg) return skSvg; } - [RequiresUnreferencedCode("Rendering from an SVG document may use trim-unsafe runtime discovery paths.")] public static SKSvg CreateFromSvgDocument(SvgDocument svgDocument) { var skSvg = new SKSvg(); @@ -337,7 +326,6 @@ public SKSvg() /// Creates a deep clone of the current , including model and reload data. /// /// A new instance with independent state. - [RequiresUnreferencedCode("Clone may recreate retained scene and animation state that use TypeDescriptor-based converters.")] public SKSvg Clone() { var clone = new SKSvg(); @@ -388,55 +376,45 @@ public SKSvg Clone() return clone; } - [RequiresUnreferencedCode("SVG document parsing may use trim-unsafe runtime discovery paths.")] public SkiaSharp.SKPicture? Load(System.IO.Stream stream, SvgParameters? parameters = null) { return LoadSvgInternal(stream, parameters, null); } - [RequiresUnreferencedCode("SVG document parsing may use trim-unsafe runtime discovery paths.")] public SkiaSharp.SKPicture? Load(System.IO.Stream stream) => Load(stream, null); - [RequiresUnreferencedCode("SVG document parsing may use trim-unsafe runtime discovery paths.")] public SkiaSharp.SKPicture? Load(System.IO.Stream stream, SvgParameters? parameters, Uri? baseUri) { return LoadSvgInternal(stream, parameters, baseUri); } - [RequiresUnreferencedCode("SVG document parsing may use trim-unsafe runtime discovery paths.")] public SkiaSharp.SKPicture? Load(string? path, SvgParameters? parameters = null) { return LoadSvgPath(path, parameters); } - [RequiresUnreferencedCode("SVG document parsing may use trim-unsafe runtime discovery paths.")] public SkiaSharp.SKPicture? Load(string path) => Load(path, null); - [RequiresUnreferencedCode("SVG document parsing may use trim-unsafe runtime discovery paths.")] public SkiaSharp.SKPicture? Load(XmlReader reader) { return LoadSvgReader(reader); } - [RequiresUnreferencedCode("VectorDrawable parsing may use trim-unsafe runtime discovery paths.")] public SkiaSharp.SKPicture? LoadVectorDrawable(System.IO.Stream stream, SvgParameters? parameters = null) { return LoadInternal(stream, parameters, null, SourceFormat.VectorDrawable, SvgService.OpenVectorDrawable); } - [RequiresUnreferencedCode("VectorDrawable parsing may use trim-unsafe runtime discovery paths.")] public SkiaSharp.SKPicture? LoadVectorDrawable(string? path, SvgParameters? parameters = null) { return LoadPath(path, parameters, SourceFormat.VectorDrawable, SvgService.OpenVectorDrawable); } - [RequiresUnreferencedCode("VectorDrawable parsing may use trim-unsafe runtime discovery paths.")] public SkiaSharp.SKPicture? LoadVectorDrawable(XmlReader reader) { return LoadReader(reader, SourceFormat.VectorDrawable, SvgService.OpenVectorDrawable); } - [RequiresUnreferencedCode("Calls Svg.Skia.SKSvg.LoadSvgDocument(SvgDocument, Uri)")] private SkiaSharp.SKPicture? LoadInternal( System.IO.Stream stream, SvgParameters? parameters, @@ -477,7 +455,6 @@ public SKSvg Clone() return LoadSvgDocument(svgDocument, baseUri); } - [RequiresUnreferencedCode("Calls Svg.Skia.SKSvg.LoadSvgDocument(SvgDocument, Uri)")] private SkiaSharp.SKPicture? LoadSvgInternal(System.IO.Stream stream, SvgParameters? parameters, Uri? baseUri) { SvgDocument? svgDocument; @@ -513,7 +490,6 @@ public SKSvg Clone() return LoadSvgDocument(svgDocument, baseUri); } - [RequiresUnreferencedCode("Calls Svg.Skia.SKSvg.LoadSvgDocument(SvgDocument, Uri)")] private SkiaSharp.SKPicture? LoadSvgPath(string? path, SvgParameters? parameters) { if (path is null) @@ -531,7 +507,6 @@ public SKSvg Clone() return LoadSvgDocument(SvgService.Open(path, parameters, Settings.EnableJavaScript)); } - [RequiresUnreferencedCode("Calls Svg.Skia.SKSvg.LoadSvgDocument(SvgDocument, Uri)")] private SkiaSharp.SKPicture? LoadSvgReader(XmlReader reader) { _originalPath = null; @@ -544,7 +519,6 @@ public SKSvg Clone() return LoadSvgDocument(SvgService.Open(reader, Settings.EnableJavaScript)); } - [RequiresUnreferencedCode("Calls Svg.Skia.SKSvg.LoadSvgDocument(SvgDocument, Uri)")] private SkiaSharp.SKPicture? LoadPath( string? path, SvgParameters? parameters, @@ -566,7 +540,6 @@ public SKSvg Clone() return LoadSvgDocument(loader(path, parameters)); } - [RequiresUnreferencedCode("Calls Svg.Skia.SKSvg.LoadSvgDocument(SvgDocument, Uri)")] private SkiaSharp.SKPicture? LoadReader( XmlReader reader, SourceFormat sourceFormat, @@ -582,7 +555,6 @@ public SKSvg Clone() return LoadSvgDocument(loader(reader)); } - [RequiresUnreferencedCode("Calls Svg.Skia.SvgAnimationController.SvgAnimationController(SvgDocument)")] private SkiaSharp.SKPicture? LoadSvgDocument(SvgDocument? svgDocument, Uri? baseUri = null) { if (svgDocument is null) @@ -615,7 +587,6 @@ public SKSvg Clone() return RenderSvgDocument(svgDocument); } - [RequiresUnreferencedCode("Reloading may reparse cached SVG or VectorDrawable content through trim-unsafe runtime discovery paths.")] public SkiaSharp.SKPicture? ReLoad(SvgParameters? parameters) { if (!CacheOriginalStream) @@ -653,21 +624,18 @@ public SKSvg Clone() : LoadSvgInternal(originalStream, parameters, originalBaseUri); } - [RequiresUnreferencedCode("SVG document parsing may use trim-unsafe runtime discovery paths.")] public SkiaSharp.SKPicture? FromSvg(string svg) { var svgDocument = SvgService.FromSvg(svg, Settings.EnableJavaScript); return LoadSvgDocument(svgDocument); } - [RequiresUnreferencedCode("VectorDrawable parsing may use trim-unsafe runtime discovery paths.")] public SkiaSharp.SKPicture? FromVectorDrawable(string xml) { var svgDocument = SvgService.FromVectorDrawable(xml); return LoadSvgDocument(svgDocument); } - [RequiresUnreferencedCode("Rendering from an SVG document may use trim-unsafe runtime discovery paths.")] public SkiaSharp.SKPicture? FromSvgDocument(SvgDocument? svgDocument) { return LoadSvgDocument(svgDocument); diff --git a/src/Svg.Skia/SkiaModel.Caching.cs b/src/Svg.Skia/SkiaModel.Caching.cs index dd0880103e..a13cfaff9a 100644 --- a/src/Svg.Skia/SkiaModel.Caching.cs +++ b/src/Svg.Skia/SkiaModel.Caching.cs @@ -886,8 +886,6 @@ private static bool TryGetImageFilterRevision(ShimSkiaSharp.SKImageFilter? image var imageFilter = GetRenderImageFilter(paint.ImageFilter); var pathEffect = GetRenderPathEffect(paint.PathEffect); var blendMode = ToSKBlendMode(paint.BlendMode); - var filterQuality = ToSKFilterQuality(paint.FilterQuality); - var skPaint = new SkiaSharp.SKPaint { Style = style, @@ -907,8 +905,7 @@ private static bool TryGetImageFilterRevision(ShimSkiaSharp.SKImageFilter? image ColorFilter = colorFilter, ImageFilter = imageFilter, PathEffect = pathEffect, - BlendMode = blendMode, - FilterQuality = filterQuality + BlendMode = blendMode }; ApplyTypefaceAdjustments(paint, skPaint, typefaceResolution.SuppressSyntheticBold); diff --git a/src/Svg.Skia/SkiaModel.cs b/src/Svg.Skia/SkiaModel.cs index 7776a6fead..a91afd5f6f 100644 --- a/src/Svg.Skia/SkiaModel.cs +++ b/src/Svg.Skia/SkiaModel.cs @@ -1056,7 +1056,7 @@ public SkiaSharp.SKColorChannel ToSKColorChannel(SKColorChannel colorChannel) ToSKImage(imageImageFilter.Image), ToSKRect(imageImageFilter.Src), ToSKRect(imageImageFilter.Dst), - SkiaSharp.SKFilterQuality.High); + ToSKSamplingOptions(imageImageFilter.FilterQuality)); } case MatrixConvolutionImageFilter matrixConvolutionImageFilter: { @@ -1325,50 +1325,31 @@ public SkiaSharp.SKBlendMode ToSKBlendMode(SKBlendMode blendMode) }; } - public SkiaSharp.SKFilterQuality ToSKFilterQuality(SKFilterQuality filterQuality) + public SkiaSharp.SKSamplingOptions ToSKSamplingOptions(SKFilterQuality filterQuality) { return filterQuality switch { - SKFilterQuality.None => SkiaSharp.SKFilterQuality.None, - SKFilterQuality.Low => SkiaSharp.SKFilterQuality.Low, - SKFilterQuality.Medium => SkiaSharp.SKFilterQuality.Medium, - SKFilterQuality.High => SkiaSharp.SKFilterQuality.High, - _ => SkiaSharp.SKFilterQuality.None + SKFilterQuality.None => new SkiaSharp.SKSamplingOptions(SkiaSharp.SKFilterMode.Nearest, SkiaSharp.SKMipmapMode.None), + SKFilterQuality.Low => new SkiaSharp.SKSamplingOptions(SkiaSharp.SKFilterMode.Linear, SkiaSharp.SKMipmapMode.None), + SKFilterQuality.Medium => new SkiaSharp.SKSamplingOptions(SkiaSharp.SKFilterMode.Linear, SkiaSharp.SKMipmapMode.Linear), + SKFilterQuality.High => new SkiaSharp.SKSamplingOptions(SkiaSharp.SKCubicResampler.Mitchell), + _ => SkiaSharp.SKSamplingOptions.Default }; } - private static SkiaSharp.SKFilterQuality ToLegacySKFilterQuality(SKSamplingOptions samplingOptions) + private static SkiaSharp.SKSamplingOptions ToSKSamplingOptions(SKSamplingOptions samplingOptions) { if (samplingOptions.UseCubic) { - return SkiaSharp.SKFilterQuality.High; + return new SkiaSharp.SKSamplingOptions( + new SkiaSharp.SKCubicResampler( + samplingOptions.Cubic.B, + samplingOptions.Cubic.C)); } - if (samplingOptions.Filter == SKFilterMode.Nearest) - { - return SkiaSharp.SKFilterQuality.None; - } - - return samplingOptions.Mipmap == SKMipmapMode.None - ? SkiaSharp.SKFilterQuality.Low - : SkiaSharp.SKFilterQuality.Medium; - } - - private static bool TryDrawImageWithSamplingOptions( - SkiaSharp.SKCanvas skCanvas, - SkiaSharp.SKImage image, - SkiaSharp.SKRect source, - SkiaSharp.SKRect dest, - SKSamplingOptions samplingOptions, - SkiaSharp.SKPaint? paint) - { - if (!SkiaSharpSamplingOptionsApi.IsAvailable) - { - return false; - } - - SkiaSharpSamplingOptionsApi.DrawImage(skCanvas, image, source, dest, samplingOptions, paint); - return true; + return new SkiaSharp.SKSamplingOptions( + (SkiaSharp.SKFilterMode)(int)samplingOptions.Filter, + (SkiaSharp.SKMipmapMode)(int)samplingOptions.Mipmap); } private static SkiaSharp.SKPaint CreateTextRenderPaint( @@ -1462,8 +1443,6 @@ public SkiaSharp.SKFont ToSKFont(SKPaint paint) var imageFilter = ToSKImageFilter(paint.ImageFilter); var pathEffect = ToSKPathEffect(paint.PathEffect); var blendMode = ToSKBlendMode(paint.BlendMode); - var filterQuality = ToSKFilterQuality(paint.FilterQuality); - var skPaint = new SkiaSharp.SKPaint { Style = style, @@ -1483,8 +1462,7 @@ public SkiaSharp.SKFont ToSKFont(SKPaint paint) ColorFilter = colorFilter, ImageFilter = imageFilter, PathEffect = pathEffect, - BlendMode = blendMode, - FilterQuality = filterQuality + BlendMode = blendMode }; ApplyTypefaceAdjustments(paint, skPaint, typefaceResolution.SuppressSyntheticBold); @@ -1987,37 +1965,10 @@ public void Draw(CanvasCommand canvasCommand, SkiaSharp.SKCanvas skCanvas, bool var source = ToSKRect(drawImageCanvasCommand.Source); var dest = ToSKRect(drawImageCanvasCommand.Dest); var paint = GetRenderPaint(drawImageCanvasCommand.Paint); - var drewWithSampling = drawImageCanvasCommand.Sampling.HasValue && - TryDrawImageWithSamplingOptions( - skCanvas, - image, - source, - dest, - drawImageCanvasCommand.Sampling.Value, - paint); - if (drewWithSampling) - { - break; - } - - var imagePaint = paint; - if (drawImageCanvasCommand.Sampling.HasValue) - { - imagePaint = paint?.Clone() ?? new SkiaSharp.SKPaint(); - imagePaint.FilterQuality = ToLegacySKFilterQuality(drawImageCanvasCommand.Sampling.Value); - } - - try - { - skCanvas.DrawImage(image, source, dest, imagePaint); - } - finally - { - if (imagePaint is not null && !ReferenceEquals(imagePaint, paint)) - { - imagePaint.Dispose(); - } - } + var samplingOptions = drawImageCanvasCommand.Sampling.HasValue + ? ToSKSamplingOptions(drawImageCanvasCommand.Sampling.Value) + : ToSKSamplingOptions(drawImageCanvasCommand.Paint?.FilterQuality ?? SKFilterQuality.None); + skCanvas.DrawImage(image, source, dest, samplingOptions, paint); } } break; @@ -2226,8 +2177,6 @@ private SkiaSharp.SKPaint ToWireframePaint(SKPaint? paint) var imageFilter = paint is null ? null : ToSKImageFilter(paint.ImageFilter); var pathEffect = paint is null ? null : ToSKPathEffect(paint.PathEffect); var blendMode = paint is null ? SkiaSharp.SKBlendMode.SrcOver : ToSKBlendMode(paint.BlendMode); - var filterQuality = paint is null ? SkiaSharp.SKFilterQuality.None : ToSKFilterQuality(paint.FilterQuality); - return new SkiaSharp.SKPaint { Style = SkiaSharp.SKPaintStyle.Stroke, @@ -2246,8 +2195,7 @@ private SkiaSharp.SKPaint ToWireframePaint(SKPaint? paint) ColorFilter = colorFilter, ImageFilter = imageFilter, PathEffect = pathEffect, - BlendMode = blendMode, - FilterQuality = filterQuality + BlendMode = blendMode }; } @@ -2256,141 +2204,4 @@ public void DrawWireframe(SKPicture picture, SkiaSharp.SKCanvas skCanvas) Draw(picture, skCanvas, true); } - private static class SkiaSharpSamplingOptionsApi - { -#if NET6_0_OR_GREATER - [System.Diagnostics.CodeAnalysis.DynamicallyAccessedMembers(System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicConstructors)] -#endif - private static readonly Type? s_samplingOptionsType = Type.GetType("SkiaSharp.SKSamplingOptions, SkiaSharp", throwOnError: false); - - private static readonly Type? s_filterModeType = Type.GetType("SkiaSharp.SKFilterMode, SkiaSharp", throwOnError: false); - private static readonly Type? s_mipmapModeType = Type.GetType("SkiaSharp.SKMipmapMode, SkiaSharp", throwOnError: false); - -#if NET6_0_OR_GREATER - [System.Diagnostics.CodeAnalysis.DynamicallyAccessedMembers(System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicConstructors)] -#endif - private static readonly Type? s_cubicResamplerType = Type.GetType("SkiaSharp.SKCubicResampler, SkiaSharp", throwOnError: false); - - private static readonly System.Reflection.ConstructorInfo? s_filterSamplingOptionsConstructor = GetConstructor(s_samplingOptionsType, s_filterModeType, s_mipmapModeType); - private static readonly System.Reflection.ConstructorInfo? s_cubicResamplerConstructor = GetConstructor(s_cubicResamplerType, typeof(float), typeof(float)); - private static readonly System.Reflection.ConstructorInfo? s_cubicSamplingOptionsConstructor = GetConstructor(s_samplingOptionsType, s_cubicResamplerType); - private static readonly System.Reflection.MethodInfo? s_drawImageWithSampling = GetDrawImageWithSampling(); - - public static bool IsAvailable => - s_samplingOptionsType is not null && - s_filterModeType is not null && - s_mipmapModeType is not null && - s_cubicResamplerType is not null && - s_filterSamplingOptionsConstructor is not null && - s_cubicResamplerConstructor is not null && - s_cubicSamplingOptionsConstructor is not null && - s_drawImageWithSampling is not null; - - public static void DrawImage( - SkiaSharp.SKCanvas skCanvas, - SkiaSharp.SKImage image, - SkiaSharp.SKRect source, - SkiaSharp.SKRect dest, - SKSamplingOptions samplingOptions, - SkiaSharp.SKPaint? paint) - { - var nativeSamplingOptions = ToNativeSamplingOptions(samplingOptions); - if (nativeSamplingOptions is null || s_drawImageWithSampling is null) - { - return; - } - - try - { - s_drawImageWithSampling.Invoke(skCanvas, new[] { image, source, dest, nativeSamplingOptions, paint }); - } - catch (System.Reflection.TargetInvocationException ex) when (ex.InnerException is not null) - { - System.Runtime.ExceptionServices.ExceptionDispatchInfo.Capture(ex.InnerException).Throw(); - } - } - - private static object? ToNativeSamplingOptions(SKSamplingOptions samplingOptions) - { - if (samplingOptions.UseCubic) - { - if (s_cubicResamplerType is null || s_cubicResamplerConstructor is null || s_cubicSamplingOptionsConstructor is null) - { - return null; - } - - var cubic = s_cubicResamplerConstructor.Invoke(new object[] { samplingOptions.Cubic.B, samplingOptions.Cubic.C }); - return s_cubicSamplingOptionsConstructor.Invoke(new[] { cubic }); - } - - if (s_filterModeType is null || s_mipmapModeType is null || s_filterSamplingOptionsConstructor is null) - { - return null; - } - - var filterMode = Enum.ToObject(s_filterModeType, (int)samplingOptions.Filter); - var mipmapMode = Enum.ToObject(s_mipmapModeType, (int)samplingOptions.Mipmap); - return s_filterSamplingOptionsConstructor.Invoke(new[] { filterMode, mipmapMode }); - } - - private static System.Reflection.ConstructorInfo? GetConstructor( -#if NET6_0_OR_GREATER - [System.Diagnostics.CodeAnalysis.DynamicallyAccessedMembers(System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicConstructors)] Type? type, -#else - Type? type, -#endif - params Type?[] parameterTypes) - { - if (type is null) - { - return null; - } - - var resolvedParameterTypes = new Type[parameterTypes.Length]; - for (var i = 0; i < parameterTypes.Length; i++) - { - if (parameterTypes[i] is not { } parameterType) - { - return null; - } - - resolvedParameterTypes[i] = parameterType; - } - - return type.GetConstructor(resolvedParameterTypes); - } - - private static System.Reflection.MethodInfo? GetDrawImageWithSampling() - { - if (s_samplingOptionsType is null) - { - return null; - } - - var methods = typeof(SkiaSharp.SKCanvas).GetMethods( - System.Reflection.BindingFlags.Instance | - System.Reflection.BindingFlags.Public); - for (var i = 0; i < methods.Length; i++) - { - var method = methods[i]; - if (method.Name != nameof(SkiaSharp.SKCanvas.DrawImage)) - { - continue; - } - - var parameters = method.GetParameters(); - if (parameters.Length == 5 && - parameters[0].ParameterType == typeof(SkiaSharp.SKImage) && - parameters[1].ParameterType == typeof(SkiaSharp.SKRect) && - parameters[2].ParameterType == typeof(SkiaSharp.SKRect) && - parameters[3].ParameterType == s_samplingOptionsType && - parameters[4].ParameterType == typeof(SkiaSharp.SKPaint)) - { - return method; - } - } - - return null; - } - } } From 044acd4ce7349eb27b029ab01d6571da526895c9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Wies=C5=82aw=20=C5=A0olt=C3=A9s?= Date: Wed, 13 May 2026 13:57:03 +0200 Subject: [PATCH 4/9] Make animation conversions trim safe --- .../Animation/SvgAnimationController.cs | 105 ++++++++++++------ .../RequiresUnreferencedCodeAttribute.cs | 14 --- 2 files changed, 73 insertions(+), 46 deletions(-) delete mode 100644 src/Svg.Animation/Diagnostics/RequiresUnreferencedCodeAttribute.cs diff --git a/src/Svg.Animation/Animation/SvgAnimationController.cs b/src/Svg.Animation/Animation/SvgAnimationController.cs index 2166aeaa66..af742d2e0c 100644 --- a/src/Svg.Animation/Animation/SvgAnimationController.cs +++ b/src/Svg.Animation/Animation/SvgAnimationController.cs @@ -1,7 +1,6 @@ using System; using System.Collections.Generic; using System.ComponentModel; -using System.Diagnostics.CodeAnalysis; using System.Drawing; using System.Globalization; using System.Linq; @@ -39,7 +38,6 @@ public SvgAnimationTimelineCallback(SvgElementAddress animationAddress, string e public TimeSpan Time { get; } } -[RequiresUnreferencedCode("Uses TypeDescriptor-based converters for animated SVG values.")] public sealed class SvgAnimationController : IDisposable { private static readonly ResolvedTimingInstance[] s_emptyResolvedTimingInstances = Array.Empty(); @@ -112,7 +110,6 @@ public PointerEventDependency(AnimationBinding binding) private sealed class AnimationBinding { - [RequiresUnreferencedCode("Calls Svg.Skia.SvgAnimationController.GetAttributeValue(SvgElement, String)")] public AnimationBinding(SvgAnimationElement animation, SvgElement sourceTarget, SvgElementAddress targetAddress, string attributeName) { Animation = animation; @@ -120,10 +117,13 @@ public AnimationBinding(SvgAnimationElement animation, SvgElement sourceTarget, SourceTarget = sourceTarget; TargetAddress = targetAddress; AttributeName = attributeName; + var propertyDescriptor = GetAttributePropertyDescriptor(sourceTarget, attributeName); + ValueConverter = propertyDescriptor?.Converter; + ValueContext = sourceTarget.OwnerDocument; HasExplicitBaseAttribute = sourceTarget.ContainsAttribute(attributeName); BaseValue = GetAttributeValue(sourceTarget, attributeName); BaseValueString = ConvertAttributeValueToString(BaseValue); - PropertyType = BaseValue?.GetType(); + PropertyType = propertyDescriptor?.Type ?? BaseValue?.GetType(); TargetAttributeKey = string.Concat(targetAddress.Key, "|", attributeName); BeginSpecs = ParseTimingSpecifications(animation.Begin, animation.OwnerDocument, targetAddress, includeImplicitDocumentBegin: true); EndSpecs = ParseTimingSpecifications(animation.End, animation.OwnerDocument, targetAddress, includeImplicitDocumentBegin: false); @@ -151,6 +151,10 @@ public AnimationBinding(SvgAnimationElement animation, SvgElement sourceTarget, public Type? PropertyType { get; } + public TypeConverter? ValueConverter { get; } + + public ITypeDescriptorContext? ValueContext { get; } + public string TargetAttributeKey { get; } public IReadOnlyList BeginSpecs { get; } @@ -220,7 +224,7 @@ public AnimationSample(float progress, int iterationIndex) public int IterationIndex { get; } } - private static readonly TypeConverter s_paintServerConverter = TypeDescriptor.GetConverter(typeof(SvgPaintServer)); + private static readonly TypeConverter s_paintServerConverter = new SvgPaintServerFactory(); private readonly List _bindings; private readonly Dictionary _bindingsByTargetAttributeKey; @@ -2781,7 +2785,6 @@ private static bool TryResolvePacedSegment( return false; } - [RequiresUnreferencedCode("Calls Svg.Skia.SvgAnimationController.TryConvertStringToType(String, Type, out Object)")] private static bool TryResolvePacedDistance(AnimationBinding binding, string fromValue, string toValue, bool forceColorInterpolation, out float distance) { distance = 0f; @@ -2795,8 +2798,8 @@ private static bool TryResolvePacedDistance(AnimationBinding binding, string fro } if (binding.PropertyType is { } propertyType && - TryConvertStringToType(fromValue, propertyType, out var fromObject) && - TryConvertStringToType(toValue, propertyType, out var toObject) && + TryConvertStringToType(fromValue, propertyType, binding.ValueConverter, binding.ValueContext, out var fromObject) && + TryConvertStringToType(toValue, propertyType, binding.ValueConverter, binding.ValueContext, out var toObject) && TryResolveTypedPacedDistance(fromObject, toObject, out distance)) { return true; @@ -3013,7 +3016,6 @@ private static float EvaluateCubicBezierDerivative(float control1, float control (3f * t * t * (1f - control2)); } - [RequiresUnreferencedCode("Calls Svg.Skia.SvgAnimationController.TryConvertStringToType(String, Type, out Object)")] private static bool TryInterpolateValue(AnimationBinding binding, string fromValue, string toValue, float progress, bool forceColorInterpolation, out string result) { result = string.Empty; @@ -3025,8 +3027,8 @@ private static bool TryInterpolateValue(AnimationBinding binding, string fromVal } if (binding.PropertyType is { } propertyType && - TryConvertStringToType(fromValue, propertyType, out var fromObject) && - TryConvertStringToType(toValue, propertyType, out var toObject) && + TryConvertStringToType(fromValue, propertyType, binding.ValueConverter, binding.ValueContext, out var fromObject) && + TryConvertStringToType(toValue, propertyType, binding.ValueConverter, binding.ValueContext, out var toObject) && TryInterpolateTypedValue(fromObject, toObject, progress, out result)) { return true; @@ -3090,13 +3092,12 @@ private static bool TryInterpolateNumeric(string fromValue, string toValue, floa return true; } - [RequiresUnreferencedCode("Calls Svg.Skia.SvgAnimationController.TryConvertStringToType(String, Type, out Object)")] private static bool TryInterpolateSvgUnit(string fromValue, string toValue, float progress, out string result) { result = string.Empty; - if (!TryConvertStringToType(fromValue, typeof(SvgUnit), out var fromObject) || - !TryConvertStringToType(toValue, typeof(SvgUnit), out var toObject) || + if (!TryConvertStringToType(fromValue, typeof(SvgUnit), converter: null, context: null, out var fromObject) || + !TryConvertStringToType(toValue, typeof(SvgUnit), converter: null, context: null, out var toObject) || fromObject is not SvgUnit fromUnit || toObject is not SvgUnit toUnit) { @@ -3223,21 +3224,20 @@ private static bool TryGetColor(SvgPaintServer? paintServer, out Color color) return false; } - [RequiresUnreferencedCode("Calls Svg.Skia.SvgAnimationController.TryConvertStringToType(String, Type, out Object)")] private static bool TryAddValue(AnimationBinding binding, string baseValue, string byValue, out string result) { result = string.Empty; if (binding.PropertyType is { } propertyType && - TryConvertStringToType(baseValue, propertyType, out var baseObject) && - TryConvertStringToType(byValue, propertyType, out var byObject) && + TryConvertStringToType(baseValue, propertyType, binding.ValueConverter, binding.ValueContext, out var baseObject) && + TryConvertStringToType(byValue, propertyType, binding.ValueConverter, binding.ValueContext, out var byObject) && TryAddTypedValue(baseObject, byObject, out result)) { return true; } - if (TryConvertStringToType(baseValue, typeof(SvgUnit), out var baseUnitObject) && - TryConvertStringToType(byValue, typeof(SvgUnit), out var byUnitObject) && + if (TryConvertStringToType(baseValue, typeof(SvgUnit), converter: null, context: null, out var baseUnitObject) && + TryConvertStringToType(byValue, typeof(SvgUnit), converter: null, context: null, out var byUnitObject) && baseUnitObject is SvgUnit baseUnit && byUnitObject is SvgUnit byUnit && baseUnit.Type == byUnit.Type) @@ -3267,21 +3267,20 @@ byUnitObject is SvgUnit byUnit && return false; } - [RequiresUnreferencedCode("Calls Svg.Skia.SvgAnimationController.TryConvertStringToType(String, Type, out Object)")] private static bool TrySubtractValue(AnimationBinding binding, string endValue, string startValue, bool forceColorInterpolation, out string result) { result = string.Empty; if (binding.PropertyType is { } propertyType && - TryConvertStringToType(endValue, propertyType, out var endObject) && - TryConvertStringToType(startValue, propertyType, out var startObject) && + TryConvertStringToType(endValue, propertyType, binding.ValueConverter, binding.ValueContext, out var endObject) && + TryConvertStringToType(startValue, propertyType, binding.ValueConverter, binding.ValueContext, out var startObject) && TrySubtractTypedValue(endObject, startObject, out result)) { return true; } - if (TryConvertStringToType(endValue, typeof(SvgUnit), out var endUnitObject) && - TryConvertStringToType(startValue, typeof(SvgUnit), out var startUnitObject) && + if (TryConvertStringToType(endValue, typeof(SvgUnit), converter: null, context: null, out var endUnitObject) && + TryConvertStringToType(startValue, typeof(SvgUnit), converter: null, context: null, out var startUnitObject) && endUnitObject is SvgUnit endUnit && startUnitObject is SvgUnit startUnit && endUnit.Type == startUnit.Type) @@ -3300,7 +3299,6 @@ startUnitObject is SvgUnit startUnit && return false; } - [RequiresUnreferencedCode("Calls Svg.Skia.SvgAnimationController.TryConvertStringToType(String, Type, out Object)")] private static bool TryScaleValue(AnimationBinding binding, string value, int factor, bool forceColorInterpolation, out string result) { result = string.Empty; @@ -3312,13 +3310,13 @@ private static bool TryScaleValue(AnimationBinding binding, string value, int fa } if (binding.PropertyType is { } propertyType && - TryConvertStringToType(value, propertyType, out var valueObject) && + TryConvertStringToType(value, propertyType, binding.ValueConverter, binding.ValueContext, out var valueObject) && TryScaleTypedValue(valueObject, factor, out result)) { return true; } - if (TryConvertStringToType(value, typeof(SvgUnit), out var unitObject) && + if (TryConvertStringToType(value, typeof(SvgUnit), converter: null, context: null, out var unitObject) && unitObject is SvgUnit unit) { result = new SvgUnit(unit.Type, unit.Value * factor).ToString(); @@ -3415,8 +3413,12 @@ private static bool TryScaleTypedValue(object? valueObject, int factor, out stri } } - [RequiresUnreferencedCode("Calls System.ComponentModel.TypeDescriptor.GetConverter(Type)")] - private static bool TryConvertStringToType(string value, Type targetType, out object? result) + private static bool TryConvertStringToType( + string value, + Type targetType, + TypeConverter? converter, + ITypeDescriptorContext? context, + out object? result) { result = null; @@ -3439,10 +3441,42 @@ private static bool TryConvertStringToType(string value, Type targetType, out ob return false; } - var converter = TypeDescriptor.GetConverter(targetType); - if (converter.CanConvertFrom(typeof(string))) + if (targetType == typeof(float)) + { + if (SvgAnimationParser.TryParseInvariantFloat(value, out var floatValue)) + { + result = floatValue; + return true; + } + + return false; + } + + if (targetType == typeof(double)) + { + if (SvgAnimationParser.TryParseInvariantDouble(value.AsSpan(), out var doubleValue)) + { + result = doubleValue; + return true; + } + + return false; + } + + if (targetType == typeof(int)) { - result = converter.ConvertFrom(null, CultureInfo.InvariantCulture, value); + if (int.TryParse(value, NumberStyles.Integer, CultureInfo.InvariantCulture, out var intValue)) + { + result = intValue; + return true; + } + + return false; + } + + if (converter is { } && converter.CanConvertFrom(typeof(string))) + { + result = converter.ConvertFrom(context, CultureInfo.InvariantCulture, value); return result is not null; } } @@ -3536,6 +3570,13 @@ private static string CreateEventInstanceKey(SvgElementAddress address, SvgAnima return element.GetAnimationValue(attributeName); } + private static ISvgPropertyDescriptor? GetAttributePropertyDescriptor(SvgElement element, string attributeName) + { + return element.Properties.TryGetValue(attributeName, out var propertyDescriptor) + ? propertyDescriptor + : null; + } + private static bool SetAttributeValue(SvgElement element, string attributeName, string value) { return element.TrySetAnimationValue(attributeName, value); diff --git a/src/Svg.Animation/Diagnostics/RequiresUnreferencedCodeAttribute.cs b/src/Svg.Animation/Diagnostics/RequiresUnreferencedCodeAttribute.cs deleted file mode 100644 index ec8e95deca..0000000000 --- a/src/Svg.Animation/Diagnostics/RequiresUnreferencedCodeAttribute.cs +++ /dev/null @@ -1,14 +0,0 @@ -#if NET461 || NETFRAMEWORK || NETSTANDARD || NETSTANDARD2_0 -namespace System.Diagnostics.CodeAnalysis; - -[AttributeUsage(AttributeTargets.Method | AttributeTargets.Constructor | AttributeTargets.Class, Inherited = false)] -internal sealed class RequiresUnreferencedCodeAttribute : Attribute -{ - public RequiresUnreferencedCodeAttribute(string message) - { - Message = message; - } - - public string Message { get; } -} -#endif From ddf08e7eb27ba7338da22c2b88d753855c649616 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Wies=C5=82aw=20=C5=A0olt=C3=A9s?= Date: Wed, 13 May 2026 13:57:07 +0200 Subject: [PATCH 5/9] Preserve JavaScript DOM raw attributes --- .../Animation/SvgDocument.Animation.cs | 1 + .../SvgElement.JavaScriptDomState.cs | 69 +++++++++++++++++++ .../Compatibility/SvgElementFactory.cs | 22 ++++++ .../Scripting/SvgJavaScriptElement.cs | 28 +++++++- .../SvgJavaScriptRuntimeTests.cs | 40 ++++++++++- 5 files changed, 156 insertions(+), 4 deletions(-) create mode 100644 src/Svg.Custom/Compatibility/SvgElement.JavaScriptDomState.cs diff --git a/src/Svg.Custom/Animation/SvgDocument.Animation.cs b/src/Svg.Custom/Animation/SvgDocument.Animation.cs index 1e8b3dfb96..3c7d05db32 100644 --- a/src/Svg.Custom/Animation/SvgDocument.Animation.cs +++ b/src/Svg.Custom/Animation/SvgDocument.Animation.cs @@ -21,6 +21,7 @@ public override SvgElement DeepCopy() newObj.ExternalCSSHref = ExternalCSSHref; CopyCompatibilityStyleSourcesTo(newObj); CopyCompatibilityStyleStateTo(newObj); + CopyJavaScriptDomStateTo(newObj); foreach (var ns in Namespaces) { diff --git a/src/Svg.Custom/Compatibility/SvgElement.JavaScriptDomState.cs b/src/Svg.Custom/Compatibility/SvgElement.JavaScriptDomState.cs new file mode 100644 index 0000000000..76cac57e2a --- /dev/null +++ b/src/Svg.Custom/Compatibility/SvgElement.JavaScriptDomState.cs @@ -0,0 +1,69 @@ +using System; +using System.Collections.Generic; + +#nullable enable + +namespace Svg; + +public abstract partial class SvgElement +{ + private Dictionary? _javaScriptDomAttributeValues; + + internal bool TryGetJavaScriptDomAttributeValue(string name, out string value) + { + if (_javaScriptDomAttributeValues is { } && + _javaScriptDomAttributeValues.TryGetValue(name, out var storedValue)) + { + value = storedValue; + return true; + } + + value = string.Empty; + return false; + } + + internal void SetJavaScriptDomAttributeValue(string name, string value) + { + _javaScriptDomAttributeValues = _javaScriptDomAttributeValues ?? new Dictionary(); + _javaScriptDomAttributeValues[name] = value; + } + + internal void CopyJavaScriptDomAttributeValuesTo(SvgElement target) + { + if (_javaScriptDomAttributeValues is null || _javaScriptDomAttributeValues.Count == 0) + { + return; + } + + target._javaScriptDomAttributeValues = new Dictionary(_javaScriptDomAttributeValues); + } + + internal void ClearJavaScriptDomAttributeValue(string name) + { + if (_javaScriptDomAttributeValues is null) + { + return; + } + + _javaScriptDomAttributeValues.Remove(name); + } +} + +public partial class SvgDocument +{ + internal void CopyJavaScriptDomStateTo(SvgDocument target) + { + CopyJavaScriptDomState(this, target); + } + + private static void CopyJavaScriptDomState(SvgElement source, SvgElement target) + { + source.CopyJavaScriptDomAttributeValuesTo(target); + + var count = Math.Min(source.Children.Count, target.Children.Count); + for (var i = 0; i < count; i++) + { + CopyJavaScriptDomState(source.Children[i], target.Children[i]); + } + } +} diff --git a/src/Svg.Custom/Compatibility/SvgElementFactory.cs b/src/Svg.Custom/Compatibility/SvgElementFactory.cs index 4ce73f5562..716c75450b 100644 --- a/src/Svg.Custom/Compatibility/SvgElementFactory.cs +++ b/src/Svg.Custom/Compatibility/SvgElementFactory.cs @@ -153,6 +153,7 @@ private void SetAttributes(SvgElement element, XmlReader reader, SvgDocument doc { if (PreserveJavaScriptDomState && !string.IsNullOrWhiteSpace(reader.Value)) { + element.SetJavaScriptDomAttributeValue(localName, reader.Value); element.CustomAttributes["style"] = reader.Value; TrackCompatibilityStyleStateCandidate(document, element); } @@ -170,6 +171,11 @@ private void SetAttributes(SvgElement element, XmlReader reader, SvgDocument doc } else if (prefix.Length == 0 && IsStyleAttribute(localName)) { + if (PreserveJavaScriptDomState && !string.IsNullOrWhiteSpace(reader.Value)) + { + element.SetJavaScriptDomAttributeValue(localName, reader.Value); + } + if (PreserveCompatibilityPresentationAttributes) { PreserveCompatibilityPresentationAttribute(document, element, localName, reader.Value); @@ -179,6 +185,11 @@ private void SetAttributes(SvgElement element, XmlReader reader, SvgDocument doc } else { + if (PreserveJavaScriptDomState && !string.IsNullOrWhiteSpace(reader.Value)) + { + element.SetJavaScriptDomAttributeValue(GetJavaScriptDomAttributeName(prefix, localName), reader.Value); + } + var ns = prefix.Length == 0 ? string.Empty : reader.LookupNamespace(prefix); SetPropertyValue(element, ns, localName, reader.Value, document); } @@ -200,6 +211,17 @@ private static void PreserveCompatibilityPresentationAttribute(SvgDocument docum ownerDocument?.PreserveCompatibilityPresentationAttribute(element, name, value); } + private static string GetJavaScriptDomAttributeName(string prefix, string localName) + { + if (prefix.Equals("xlink", StringComparison.OrdinalIgnoreCase) && + localName.Equals("href", StringComparison.OrdinalIgnoreCase)) + { + return "href"; + } + + return prefix.Length == 0 ? localName : $"{prefix}:{localName}"; + } + private static bool IsStyleAttribute(string name) { switch (name) diff --git a/src/Svg.JavaScript/Scripting/SvgJavaScriptElement.cs b/src/Svg.JavaScript/Scripting/SvgJavaScriptElement.cs index 67ffa161ee..3531daff1d 100644 --- a/src/Svg.JavaScript/Scripting/SvgJavaScriptElement.cs +++ b/src/Svg.JavaScript/Scripting/SvgJavaScriptElement.cs @@ -158,6 +158,11 @@ public string getAttribute(string name) } var normalizedName = NormalizeAttributeStorageName(name); + if (Element.TryGetJavaScriptDomAttributeValue(normalizedName, out var scriptSetValue)) + { + return scriptSetValue; + } + if (TryGetSpecialAttributeValue(normalizedName, out var specialValue)) { return specialValue; @@ -229,6 +234,7 @@ public void setAttribute(string name, object? value) var text = Convert.ToString(value, CultureInfo.InvariantCulture) ?? string.Empty; if (string.Equals(normalizedName, "style", StringComparison.OrdinalIgnoreCase)) { + Element.SetJavaScriptDomAttributeValue(normalizedName, text); SetInlineStyleText(text); _runtime.MarkMutation(); return; @@ -268,6 +274,7 @@ public void removeAttribute(string name) normalizedName = NormalizeAttributeStorageName(normalizedName); if (string.Equals(normalizedName, "style", StringComparison.OrdinalIgnoreCase)) { + Element.ClearJavaScriptDomAttributeValue(normalizedName); SetInlineStyleText(string.Empty); _runtime.MarkMutation(); return; @@ -298,9 +305,15 @@ public void removeAttributeNS(string? namespaceUri, string localName) public bool hasAttribute(string name) { - return !string.IsNullOrWhiteSpace(name) - && (Element.TryGetAttribute(NormalizeAttributeStorageName(name), out _) - || Element.CustomAttributes.ContainsKey(NormalizeAttributeStorageName(name))); + if (string.IsNullOrWhiteSpace(name)) + { + return false; + } + + var normalizedName = NormalizeAttributeStorageName(name); + return Element.TryGetJavaScriptDomAttributeValue(normalizedName, out _) + || Element.TryGetAttribute(normalizedName, out _) + || Element.CustomAttributes.ContainsKey(normalizedName); } public bool hasAttributeNS(string? namespaceUri, string localName) @@ -1413,12 +1426,15 @@ private void SetAttributeValue(string name, string value) { Element.CustomAttributes[name] = value; } + + Element.SetJavaScriptDomAttributeValue(name, value); } private void RemoveAttributeValue(string name) { Element.ClearAnimationValue(name); Element.CustomAttributes.Remove(name); + Element.ClearJavaScriptDomAttributeValue(name); } private void SetInlineStyleText(string styleText, Dictionary? declarations = null) @@ -1640,6 +1656,12 @@ private bool TryGetRawXLinkHrefValue(out string value) private bool TryGetRawAttributeValue(string name, out string value) { + if (Element.TryGetJavaScriptDomAttributeValue(name, out var scriptSetValue)) + { + value = scriptSetValue; + return true; + } + if (Element.CustomAttributes.TryGetValue(name, out var customValue) && !string.IsNullOrWhiteSpace(customValue)) { value = customValue ?? string.Empty; diff --git a/tests/Svg.JavaScript.UnitTests/SvgJavaScriptRuntimeTests.cs b/tests/Svg.JavaScript.UnitTests/SvgJavaScriptRuntimeTests.cs index 4b13453e51..f3e79afa01 100644 --- a/tests/Svg.JavaScript.UnitTests/SvgJavaScriptRuntimeTests.cs +++ b/tests/Svg.JavaScript.UnitTests/SvgJavaScriptRuntimeTests.cs @@ -3,6 +3,7 @@ using System.Xml; using Svg; using Svg.JavaScript; +using Svg.Model.Services; using Xunit; namespace Svg.JavaScript.UnitTests; @@ -103,6 +104,38 @@ public void ExecuteDocumentScripts_ExposesDefaultViewAsWindow() AssertFill(document, "target", Color.Green); } + [Fact] + public void SvgAngleValueAsString_PreservesAssignedUnitType() + { + var document = LoadDocument(""" + + + + """, captureJavaScriptDomState: true); + + var runtime = new SvgJavaScriptRuntime(document, new SvgJavaScriptSettings { ThrowOnError = true }); + var target = runtime.GetElement(document.Descendants().Single(element => element.ID == "target")); + + Assert.Equal(3, target.orientAngle.baseVal.unitType); + + target.orientAngle.baseVal.valueAsString = "2grad"; + + Assert.Equal("2grad", target.orientAngle.baseVal.valueAsString); + Assert.Equal(4, target.orientAngle.baseVal.unitType); + + target.setAttribute("data-empty", string.Empty); + Assert.True(target.hasAttribute("data-empty")); + Assert.Equal(string.Empty, target.getAttribute("data-empty")); + + var clone = Assert.IsType(document.DeepCopy()); + var cloneRuntime = new SvgJavaScriptRuntime(clone, new SvgJavaScriptSettings { ThrowOnError = true }); + var cloneTarget = cloneRuntime.GetElement(clone.Descendants().Single(element => element.ID == "target")); + + Assert.Equal("2grad", cloneTarget.orientAngle.baseVal.valueAsString); + Assert.Equal(4, cloneTarget.orientAngle.baseVal.unitType); + Assert.True(cloneTarget.hasAttribute("data-empty")); + } + [Fact] public void AppendChild_MovesTextNodeOutOfPreviousParent() { @@ -130,8 +163,13 @@ public void AppendChild_MovesTextNodeOutOfPreviousParent() Assert.Single(target.Nodes); } - private static SvgDocument LoadDocument(string svg) + private static SvgDocument LoadDocument(string svg, bool captureJavaScriptDomState = false) { + if (captureJavaScriptDomState) + { + return SvgService.FromSvg(svg, captureCompatibilityStyleState: true)!; + } + var xmlDocument = new XmlDocument(); xmlDocument.LoadXml(svg); return SvgDocument.Open(xmlDocument); From afc704902c66429fa9ba061c284245e00e0d7e70 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Wies=C5=82aw=20=C5=A0olt=C3=A9s?= Date: Wed, 13 May 2026 13:57:11 +0200 Subject: [PATCH 6/9] Make SvgML enum bridge trim safe --- .../Generated/SvgEnumSupport.g.cs | 139 +++++++++++++++--- src/SvgML.Avalonia/Manual/element.Writer.cs | 9 +- .../Generators/Generator.cs | 139 +++++++++++++++--- src/SvgML.Maui/Generated/SvgEnumSupport.g.cs | 139 +++++++++++++++--- src/SvgML.Uno/Generated/SvgEnumSupport.g.cs | 139 +++++++++++++++--- .../SvgMLContentTests.cs | 15 ++ 6 files changed, 493 insertions(+), 87 deletions(-) diff --git a/src/SvgML.Avalonia/Generated/SvgEnumSupport.g.cs b/src/SvgML.Avalonia/Generated/SvgEnumSupport.g.cs index fe3b8e6ad6..0e97b2d189 100644 --- a/src/SvgML.Avalonia/Generated/SvgEnumSupport.g.cs +++ b/src/SvgML.Avalonia/Generated/SvgEnumSupport.g.cs @@ -12,18 +12,22 @@ internal static class SvgEnumBridge { public static string ToSvgString(global::System.Enum value) { - var converter = global::System.ComponentModel.TypeDescriptor.GetConverter(value.GetType()); - if (converter.GetType() != typeof(global::System.ComponentModel.EnumConverter) - && converter.CanConvertTo(typeof(string))) + var name = value.ToString(); + if (string.IsNullOrEmpty(name)) { - var converted = converter.ConvertToInvariantString(value); - if (!string.IsNullOrWhiteSpace(converted)) - { - return converted; - } + return string.Empty; } - return value.ToString().Replace("_", "-", global::System.StringComparison.Ordinal); + var typeName = value.GetType().FullName; + return typeName switch + { + "Svg.SvgFontWeight" => FormatFontWeight(name), + "Svg.XmlSpaceHandling" or "Svg.SvgFillRule" or "Svg.SvgClipRule" => name.ToLowerInvariant(), + "Svg.SvgDominantBaseline" or "Svg.SvgFontVariant" or "Svg.SvgTextDecoration" or "Svg.SvgFontStretch" or "Svg.FilterEffects.SvgBlendMode" => ToKebabCase(name), + "Svg.FilterEffects.SvgChannelSelector" => name, + _ when typeName is not null && typeName.StartsWith("SvgML.", global::System.StringComparison.Ordinal) => name.Replace("_", "-", global::System.StringComparison.Ordinal), + _ => ToCamelCase(name), + }; } public static TEnum Parse(string value) @@ -34,29 +38,122 @@ public static TEnum Parse(string value) return default; } - var converter = global::System.ComponentModel.TypeDescriptor.GetConverter(typeof(TEnum)); - if (converter.GetType() != typeof(global::System.ComponentModel.EnumConverter) - && converter.CanConvertFrom(typeof(string))) + if (TryParseSingle(value, out var parsed)) + { + return parsed; + } + + var parts = value.Split([' ', ','], global::System.StringSplitOptions.RemoveEmptyEntries | global::System.StringSplitOptions.TrimEntries); + if (parts.Length > 1) { - try + ulong combined = 0; + for (var i = 0; i < parts.Length; i++) { - var converted = converter.ConvertFromInvariantString(value); - if (converted is TEnum typedValue) + if (!TryParseSingle(parts[i], out var part)) { - return typedValue; + throw new global::System.FormatException($"Unable to convert '{value}' to {typeof(TEnum).FullName}."); } + + combined |= global::System.Convert.ToUInt64(part, global::System.Globalization.CultureInfo.InvariantCulture); } - catch (global::System.Exception) + + return (TEnum)global::System.Enum.ToObject(typeof(TEnum), combined); + } + + throw new global::System.FormatException($"Unable to convert '{value}' to {typeof(TEnum).FullName}."); + } + + private static bool TryParseSingle(string value, out TEnum parsed) + where TEnum : struct, global::System.Enum + { + if (typeof(TEnum).FullName == "Svg.SvgFontWeight" && IsFontWeightLiteral(value)) + { + value = "W" + value; + } + + if (global::System.Enum.TryParse(value, ignoreCase: true, out parsed)) + { + return true; + } + + var normalized = NormalizeEnumToken(value); + var names = global::System.Enum.GetNames(); + for (var i = 0; i < names.Length; i++) + { + if (string.Equals(NormalizeEnumToken(names[i]), normalized, global::System.StringComparison.OrdinalIgnoreCase) + && global::System.Enum.TryParse(names[i], out parsed)) { + return true; } } - var normalized = value.Replace("-", "_", global::System.StringComparison.Ordinal); - if (global::System.Enum.TryParse(normalized, ignoreCase: true, out var parsed)) + return false; + } + + private static string FormatFontWeight(string name) + { + return name.Length == 4 && name[0] == 'W' && IsFontWeightLiteral(name.Substring(1)) + ? name.Substring(1) + : ToCamelCase(name); + } + + private static bool IsFontWeightLiteral(string value) + { + return value.Length == 3 + && value[0] >= '1' && value[0] <= '9' + && value[1] == '0' + && value[2] == '0'; + } + + private static string NormalizeEnumToken(string value) + { + return value.Replace("-", string.Empty, global::System.StringComparison.Ordinal) + .Replace("_", string.Empty, global::System.StringComparison.Ordinal) + .Replace(" ", string.Empty, global::System.StringComparison.Ordinal); + } + + private static string ToCamelCase(string value) + { + return value.Length == 0 + ? value + : char.ToLowerInvariant(value[0]) + value.Substring(1); + } + + private static string ToKebabCase(string value) + { + var builder = new global::System.Text.StringBuilder(value.Length + 4); + for (var i = 0; i < value.Length; i++) { - return parsed; + var c = value[i]; + if (c == ',') + { + continue; + } + + if (char.IsWhiteSpace(c)) + { + if (builder.Length > 0 && builder[builder.Length - 1] != ' ') + { + builder.Append(' '); + } + + continue; + } + + if (char.IsUpper(c)) + { + if (i > 0 && value[i - 1] != ' ' && value[i - 1] != ',' && !char.IsUpper(value[i - 1])) + { + builder.Append('-'); + } + + builder.Append(char.ToLowerInvariant(c)); + continue; + } + + builder.Append(c == '_' ? '-' : c); } - throw new global::System.FormatException($"Unable to convert '{value}' to {typeof(TEnum).FullName}."); + return builder.ToString(); } } diff --git a/src/SvgML.Avalonia/Manual/element.Writer.cs b/src/SvgML.Avalonia/Manual/element.Writer.cs index 6d37da8fb3..41b082e0cf 100644 --- a/src/SvgML.Avalonia/Manual/element.Writer.cs +++ b/src/SvgML.Avalonia/Manual/element.Writer.cs @@ -13,9 +13,12 @@ protected string ToSvgString(string value) protected string ToSvgString(object value) { - return value is ISvgEnumBridge bridge - ? SvgEnumBridge.ToSvgString((Enum)bridge.RawValue) - : value.ToString() ?? string.Empty; + return value switch + { + ISvgEnumBridge bridge => SvgEnumBridge.ToSvgString((Enum)bridge.RawValue), + Enum enumValue => SvgEnumBridge.ToSvgString(enumValue), + _ => value.ToString() ?? string.Empty + }; } protected string ToSvgString(Enum value) diff --git a/src/SvgML.CodeGenerator/Generators/Generator.cs b/src/SvgML.CodeGenerator/Generators/Generator.cs index 26f709a247..302902c692 100644 --- a/src/SvgML.CodeGenerator/Generators/Generator.cs +++ b/src/SvgML.CodeGenerator/Generators/Generator.cs @@ -386,18 +386,22 @@ internal static class SvgEnumBridge { public static string ToSvgString(global::System.Enum value) { - var converter = global::System.ComponentModel.TypeDescriptor.GetConverter(value.GetType()); - if (converter.GetType() != typeof(global::System.ComponentModel.EnumConverter) - && converter.CanConvertTo(typeof(string))) + var name = value.ToString(); + if (string.IsNullOrEmpty(name)) { - var converted = converter.ConvertToInvariantString(value); - if (!string.IsNullOrWhiteSpace(converted)) - { - return converted; - } + return string.Empty; } - return value.ToString().Replace("_", "-", global::System.StringComparison.Ordinal); + var typeName = value.GetType().FullName; + return typeName switch + { + "Svg.SvgFontWeight" => FormatFontWeight(name), + "Svg.XmlSpaceHandling" or "Svg.SvgFillRule" or "Svg.SvgClipRule" => name.ToLowerInvariant(), + "Svg.SvgDominantBaseline" or "Svg.SvgFontVariant" or "Svg.SvgTextDecoration" or "Svg.SvgFontStretch" or "Svg.FilterEffects.SvgBlendMode" => ToKebabCase(name), + "Svg.FilterEffects.SvgChannelSelector" => name, + _ when typeName is not null && typeName.StartsWith("SvgML.", global::System.StringComparison.Ordinal) => name.Replace("_", "-", global::System.StringComparison.Ordinal), + _ => ToCamelCase(name), + }; } public static TEnum Parse(string value) @@ -408,30 +412,123 @@ public static TEnum Parse(string value) return default; } - var converter = global::System.ComponentModel.TypeDescriptor.GetConverter(typeof(TEnum)); - if (converter.GetType() != typeof(global::System.ComponentModel.EnumConverter) - && converter.CanConvertFrom(typeof(string))) + if (TryParseSingle(value, out var parsed)) + { + return parsed; + } + + var parts = value.Split([' ', ','], global::System.StringSplitOptions.RemoveEmptyEntries | global::System.StringSplitOptions.TrimEntries); + if (parts.Length > 1) { - try + ulong combined = 0; + for (var i = 0; i < parts.Length; i++) { - var converted = converter.ConvertFromInvariantString(value); - if (converted is TEnum typedValue) + if (!TryParseSingle(parts[i], out var part)) { - return typedValue; + throw new global::System.FormatException($"Unable to convert '{value}' to {typeof(TEnum).FullName}."); } + + combined |= global::System.Convert.ToUInt64(part, global::System.Globalization.CultureInfo.InvariantCulture); } - catch (global::System.Exception) + + return (TEnum)global::System.Enum.ToObject(typeof(TEnum), combined); + } + + throw new global::System.FormatException($"Unable to convert '{value}' to {typeof(TEnum).FullName}."); + } + + private static bool TryParseSingle(string value, out TEnum parsed) + where TEnum : struct, global::System.Enum + { + if (typeof(TEnum).FullName == "Svg.SvgFontWeight" && IsFontWeightLiteral(value)) + { + value = "W" + value; + } + + if (global::System.Enum.TryParse(value, ignoreCase: true, out parsed)) + { + return true; + } + + var normalized = NormalizeEnumToken(value); + var names = global::System.Enum.GetNames(); + for (var i = 0; i < names.Length; i++) + { + if (string.Equals(NormalizeEnumToken(names[i]), normalized, global::System.StringComparison.OrdinalIgnoreCase) + && global::System.Enum.TryParse(names[i], out parsed)) { + return true; } } - var normalized = value.Replace("-", "_", global::System.StringComparison.Ordinal); - if (global::System.Enum.TryParse(normalized, ignoreCase: true, out var parsed)) + return false; + } + + private static string FormatFontWeight(string name) + { + return name.Length == 4 && name[0] == 'W' && IsFontWeightLiteral(name.Substring(1)) + ? name.Substring(1) + : ToCamelCase(name); + } + + private static bool IsFontWeightLiteral(string value) + { + return value.Length == 3 + && value[0] >= '1' && value[0] <= '9' + && value[1] == '0' + && value[2] == '0'; + } + + private static string NormalizeEnumToken(string value) + { + return value.Replace("-", string.Empty, global::System.StringComparison.Ordinal) + .Replace("_", string.Empty, global::System.StringComparison.Ordinal) + .Replace(" ", string.Empty, global::System.StringComparison.Ordinal); + } + + private static string ToCamelCase(string value) + { + return value.Length == 0 + ? value + : char.ToLowerInvariant(value[0]) + value.Substring(1); + } + + private static string ToKebabCase(string value) + { + var builder = new global::System.Text.StringBuilder(value.Length + 4); + for (var i = 0; i < value.Length; i++) { - return parsed; + var c = value[i]; + if (c == ',') + { + continue; + } + + if (char.IsWhiteSpace(c)) + { + if (builder.Length > 0 && builder[builder.Length - 1] != ' ') + { + builder.Append(' '); + } + + continue; + } + + if (char.IsUpper(c)) + { + if (i > 0 && value[i - 1] != ' ' && value[i - 1] != ',' && !char.IsUpper(value[i - 1])) + { + builder.Append('-'); + } + + builder.Append(char.ToLowerInvariant(c)); + continue; + } + + builder.Append(c == '_' ? '-' : c); } - throw new global::System.FormatException($"Unable to convert '{value}' to {typeof(TEnum).FullName}."); + return builder.ToString(); } } """); diff --git a/src/SvgML.Maui/Generated/SvgEnumSupport.g.cs b/src/SvgML.Maui/Generated/SvgEnumSupport.g.cs index 40bea7f477..ba89c4e8ee 100644 --- a/src/SvgML.Maui/Generated/SvgEnumSupport.g.cs +++ b/src/SvgML.Maui/Generated/SvgEnumSupport.g.cs @@ -12,18 +12,22 @@ internal static class SvgEnumBridge { public static string ToSvgString(global::System.Enum value) { - var converter = global::System.ComponentModel.TypeDescriptor.GetConverter(value.GetType()); - if (converter.GetType() != typeof(global::System.ComponentModel.EnumConverter) - && converter.CanConvertTo(typeof(string))) + var name = value.ToString(); + if (string.IsNullOrEmpty(name)) { - var converted = converter.ConvertToInvariantString(value); - if (!string.IsNullOrWhiteSpace(converted)) - { - return converted; - } + return string.Empty; } - return value.ToString().Replace("_", "-", global::System.StringComparison.Ordinal); + var typeName = value.GetType().FullName; + return typeName switch + { + "Svg.SvgFontWeight" => FormatFontWeight(name), + "Svg.XmlSpaceHandling" or "Svg.SvgFillRule" or "Svg.SvgClipRule" => name.ToLowerInvariant(), + "Svg.SvgDominantBaseline" or "Svg.SvgFontVariant" or "Svg.SvgTextDecoration" or "Svg.SvgFontStretch" or "Svg.FilterEffects.SvgBlendMode" => ToKebabCase(name), + "Svg.FilterEffects.SvgChannelSelector" => name, + _ when typeName is not null && typeName.StartsWith("SvgML.", global::System.StringComparison.Ordinal) => name.Replace("_", "-", global::System.StringComparison.Ordinal), + _ => ToCamelCase(name), + }; } public static TEnum Parse(string value) @@ -34,30 +38,123 @@ public static TEnum Parse(string value) return default; } - var converter = global::System.ComponentModel.TypeDescriptor.GetConverter(typeof(TEnum)); - if (converter.GetType() != typeof(global::System.ComponentModel.EnumConverter) - && converter.CanConvertFrom(typeof(string))) + if (TryParseSingle(value, out var parsed)) + { + return parsed; + } + + var parts = value.Split([' ', ','], global::System.StringSplitOptions.RemoveEmptyEntries | global::System.StringSplitOptions.TrimEntries); + if (parts.Length > 1) { - try + ulong combined = 0; + for (var i = 0; i < parts.Length; i++) { - var converted = converter.ConvertFromInvariantString(value); - if (converted is TEnum typedValue) + if (!TryParseSingle(parts[i], out var part)) { - return typedValue; + throw new global::System.FormatException($"Unable to convert '{value}' to {typeof(TEnum).FullName}."); } + + combined |= global::System.Convert.ToUInt64(part, global::System.Globalization.CultureInfo.InvariantCulture); } - catch (global::System.Exception) + + return (TEnum)global::System.Enum.ToObject(typeof(TEnum), combined); + } + + throw new global::System.FormatException($"Unable to convert '{value}' to {typeof(TEnum).FullName}."); + } + + private static bool TryParseSingle(string value, out TEnum parsed) + where TEnum : struct, global::System.Enum + { + if (typeof(TEnum).FullName == "Svg.SvgFontWeight" && IsFontWeightLiteral(value)) + { + value = "W" + value; + } + + if (global::System.Enum.TryParse(value, ignoreCase: true, out parsed)) + { + return true; + } + + var normalized = NormalizeEnumToken(value); + var names = global::System.Enum.GetNames(); + for (var i = 0; i < names.Length; i++) + { + if (string.Equals(NormalizeEnumToken(names[i]), normalized, global::System.StringComparison.OrdinalIgnoreCase) + && global::System.Enum.TryParse(names[i], out parsed)) { + return true; } } - var normalized = value.Replace("-", "_", global::System.StringComparison.Ordinal); - if (global::System.Enum.TryParse(normalized, ignoreCase: true, out var parsed)) + return false; + } + + private static string FormatFontWeight(string name) + { + return name.Length == 4 && name[0] == 'W' && IsFontWeightLiteral(name.Substring(1)) + ? name.Substring(1) + : ToCamelCase(name); + } + + private static bool IsFontWeightLiteral(string value) + { + return value.Length == 3 + && value[0] >= '1' && value[0] <= '9' + && value[1] == '0' + && value[2] == '0'; + } + + private static string NormalizeEnumToken(string value) + { + return value.Replace("-", string.Empty, global::System.StringComparison.Ordinal) + .Replace("_", string.Empty, global::System.StringComparison.Ordinal) + .Replace(" ", string.Empty, global::System.StringComparison.Ordinal); + } + + private static string ToCamelCase(string value) + { + return value.Length == 0 + ? value + : char.ToLowerInvariant(value[0]) + value.Substring(1); + } + + private static string ToKebabCase(string value) + { + var builder = new global::System.Text.StringBuilder(value.Length + 4); + for (var i = 0; i < value.Length; i++) { - return parsed; + var c = value[i]; + if (c == ',') + { + continue; + } + + if (char.IsWhiteSpace(c)) + { + if (builder.Length > 0 && builder[builder.Length - 1] != ' ') + { + builder.Append(' '); + } + + continue; + } + + if (char.IsUpper(c)) + { + if (i > 0 && value[i - 1] != ' ' && value[i - 1] != ',' && !char.IsUpper(value[i - 1])) + { + builder.Append('-'); + } + + builder.Append(char.ToLowerInvariant(c)); + continue; + } + + builder.Append(c == '_' ? '-' : c); } - throw new global::System.FormatException($"Unable to convert '{value}' to {typeof(TEnum).FullName}."); + return builder.ToString(); } } diff --git a/src/SvgML.Uno/Generated/SvgEnumSupport.g.cs b/src/SvgML.Uno/Generated/SvgEnumSupport.g.cs index 69eb272d47..e3338163fc 100644 --- a/src/SvgML.Uno/Generated/SvgEnumSupport.g.cs +++ b/src/SvgML.Uno/Generated/SvgEnumSupport.g.cs @@ -12,18 +12,22 @@ internal static class SvgEnumBridge { public static string ToSvgString(global::System.Enum value) { - var converter = global::System.ComponentModel.TypeDescriptor.GetConverter(value.GetType()); - if (converter.GetType() != typeof(global::System.ComponentModel.EnumConverter) - && converter.CanConvertTo(typeof(string))) + var name = value.ToString(); + if (string.IsNullOrEmpty(name)) { - var converted = converter.ConvertToInvariantString(value); - if (!string.IsNullOrWhiteSpace(converted)) - { - return converted; - } + return string.Empty; } - return value.ToString().Replace("_", "-", global::System.StringComparison.Ordinal); + var typeName = value.GetType().FullName; + return typeName switch + { + "Svg.SvgFontWeight" => FormatFontWeight(name), + "Svg.XmlSpaceHandling" or "Svg.SvgFillRule" or "Svg.SvgClipRule" => name.ToLowerInvariant(), + "Svg.SvgDominantBaseline" or "Svg.SvgFontVariant" or "Svg.SvgTextDecoration" or "Svg.SvgFontStretch" or "Svg.FilterEffects.SvgBlendMode" => ToKebabCase(name), + "Svg.FilterEffects.SvgChannelSelector" => name, + _ when typeName is not null && typeName.StartsWith("SvgML.", global::System.StringComparison.Ordinal) => name.Replace("_", "-", global::System.StringComparison.Ordinal), + _ => ToCamelCase(name), + }; } public static TEnum Parse(string value) @@ -34,30 +38,123 @@ public static TEnum Parse(string value) return default; } - var converter = global::System.ComponentModel.TypeDescriptor.GetConverter(typeof(TEnum)); - if (converter.GetType() != typeof(global::System.ComponentModel.EnumConverter) - && converter.CanConvertFrom(typeof(string))) + if (TryParseSingle(value, out var parsed)) + { + return parsed; + } + + var parts = value.Split([' ', ','], global::System.StringSplitOptions.RemoveEmptyEntries | global::System.StringSplitOptions.TrimEntries); + if (parts.Length > 1) { - try + ulong combined = 0; + for (var i = 0; i < parts.Length; i++) { - var converted = converter.ConvertFromInvariantString(value); - if (converted is TEnum typedValue) + if (!TryParseSingle(parts[i], out var part)) { - return typedValue; + throw new global::System.FormatException($"Unable to convert '{value}' to {typeof(TEnum).FullName}."); } + + combined |= global::System.Convert.ToUInt64(part, global::System.Globalization.CultureInfo.InvariantCulture); } - catch (global::System.Exception) + + return (TEnum)global::System.Enum.ToObject(typeof(TEnum), combined); + } + + throw new global::System.FormatException($"Unable to convert '{value}' to {typeof(TEnum).FullName}."); + } + + private static bool TryParseSingle(string value, out TEnum parsed) + where TEnum : struct, global::System.Enum + { + if (typeof(TEnum).FullName == "Svg.SvgFontWeight" && IsFontWeightLiteral(value)) + { + value = "W" + value; + } + + if (global::System.Enum.TryParse(value, ignoreCase: true, out parsed)) + { + return true; + } + + var normalized = NormalizeEnumToken(value); + var names = global::System.Enum.GetNames(); + for (var i = 0; i < names.Length; i++) + { + if (string.Equals(NormalizeEnumToken(names[i]), normalized, global::System.StringComparison.OrdinalIgnoreCase) + && global::System.Enum.TryParse(names[i], out parsed)) { + return true; } } - var normalized = value.Replace("-", "_", global::System.StringComparison.Ordinal); - if (global::System.Enum.TryParse(normalized, ignoreCase: true, out var parsed)) + return false; + } + + private static string FormatFontWeight(string name) + { + return name.Length == 4 && name[0] == 'W' && IsFontWeightLiteral(name.Substring(1)) + ? name.Substring(1) + : ToCamelCase(name); + } + + private static bool IsFontWeightLiteral(string value) + { + return value.Length == 3 + && value[0] >= '1' && value[0] <= '9' + && value[1] == '0' + && value[2] == '0'; + } + + private static string NormalizeEnumToken(string value) + { + return value.Replace("-", string.Empty, global::System.StringComparison.Ordinal) + .Replace("_", string.Empty, global::System.StringComparison.Ordinal) + .Replace(" ", string.Empty, global::System.StringComparison.Ordinal); + } + + private static string ToCamelCase(string value) + { + return value.Length == 0 + ? value + : char.ToLowerInvariant(value[0]) + value.Substring(1); + } + + private static string ToKebabCase(string value) + { + var builder = new global::System.Text.StringBuilder(value.Length + 4); + for (var i = 0; i < value.Length; i++) { - return parsed; + var c = value[i]; + if (c == ',') + { + continue; + } + + if (char.IsWhiteSpace(c)) + { + if (builder.Length > 0 && builder[builder.Length - 1] != ' ') + { + builder.Append(' '); + } + + continue; + } + + if (char.IsUpper(c)) + { + if (i > 0 && value[i - 1] != ' ' && value[i - 1] != ',' && !char.IsUpper(value[i - 1])) + { + builder.Append('-'); + } + + builder.Append(char.ToLowerInvariant(c)); + continue; + } + + builder.Append(c == '_' ? '-' : c); } - throw new global::System.FormatException($"Unable to convert '{value}' to {typeof(TEnum).FullName}."); + return builder.ToString(); } } diff --git a/tests/Svg.Controls.Skia.Uno.UnitTests/SvgMLContentTests.cs b/tests/Svg.Controls.Skia.Uno.UnitTests/SvgMLContentTests.cs index 3184b651ab..8957687eb4 100644 --- a/tests/Svg.Controls.Skia.Uno.UnitTests/SvgMLContentTests.cs +++ b/tests/Svg.Controls.Skia.Uno.UnitTests/SvgMLContentTests.cs @@ -42,4 +42,19 @@ public void TextPathContentProperty_UsesText() Assert.Equal(nameof(SvgML.text_base.Text), attribute!.Name); } + [Fact] + public void EnumBridge_FormatsSvgEnumsWithoutTypeDescriptor() + { + Assert.Equal("visiblePainted", new SvgML.SvgPointerEventsValue(global::Svg.SvgPointerEvents.VisiblePainted).ToString()); + Assert.Equal("whenNotActive", new SvgML.SvgAnimationRestartValue(global::Svg.SvgAnimationRestart.WhenNotActive).ToString()); + Assert.Equal(global::Svg.SvgPointerEvents.VisiblePainted, SvgML.SvgPointerEventsValue.Parse("visiblePainted").Value); + } + + [Fact] + public void EnumBridge_FormatsManualEnumsWithoutTypeDescriptor() + { + Assert.Equal("color-dodge", new SvgML.BlendModeValue(SvgML.blend_mode.color_dodge).ToString()); + Assert.Equal(SvgML.blend_mode.color_dodge, SvgML.BlendModeValue.Parse("color-dodge").Value); + Assert.Equal("hueRotate", new SvgML.TypeFeColorMatrixValue(SvgML.type_feColorMatrix.hueRotate).ToString()); + } } From c6c3e1e0d6b8b3b336c05d12559485727077a9d8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Wies=C5=82aw=20=C5=A0olt=C3=A9s?= Date: Wed, 13 May 2026 14:11:54 +0200 Subject: [PATCH 7/9] Copy DOM state for element clones --- externals/SVG | 2 +- .../Animation/SvgDocument.Animation.cs | 1 - .../SvgElement.JavaScriptDomState.cs | 19 ++----------------- .../SvgJavaScriptRuntimeTests.cs | 10 +++++++++- 4 files changed, 12 insertions(+), 20 deletions(-) diff --git a/externals/SVG b/externals/SVG index 853a9ae8d7..fd33bed4ff 160000 --- a/externals/SVG +++ b/externals/SVG @@ -1 +1 @@ -Subproject commit 853a9ae8d7e34415ff6a0c4a24d4ef29032c72fa +Subproject commit fd33bed4ff14c803b800214ddec977ca0a2e0f8e diff --git a/src/Svg.Custom/Animation/SvgDocument.Animation.cs b/src/Svg.Custom/Animation/SvgDocument.Animation.cs index 3c7d05db32..1e8b3dfb96 100644 --- a/src/Svg.Custom/Animation/SvgDocument.Animation.cs +++ b/src/Svg.Custom/Animation/SvgDocument.Animation.cs @@ -21,7 +21,6 @@ public override SvgElement DeepCopy() newObj.ExternalCSSHref = ExternalCSSHref; CopyCompatibilityStyleSourcesTo(newObj); CopyCompatibilityStyleStateTo(newObj); - CopyJavaScriptDomStateTo(newObj); foreach (var ns in Namespaces) { diff --git a/src/Svg.Custom/Compatibility/SvgElement.JavaScriptDomState.cs b/src/Svg.Custom/Compatibility/SvgElement.JavaScriptDomState.cs index 76cac57e2a..de08de0c82 100644 --- a/src/Svg.Custom/Compatibility/SvgElement.JavaScriptDomState.cs +++ b/src/Svg.Custom/Compatibility/SvgElement.JavaScriptDomState.cs @@ -1,4 +1,3 @@ -using System; using System.Collections.Generic; #nullable enable @@ -47,23 +46,9 @@ internal void ClearJavaScriptDomAttributeValue(string name) _javaScriptDomAttributeValues.Remove(name); } -} - -public partial class SvgDocument -{ - internal void CopyJavaScriptDomStateTo(SvgDocument target) - { - CopyJavaScriptDomState(this, target); - } - private static void CopyJavaScriptDomState(SvgElement source, SvgElement target) + partial void CopyCustomStateTo(SvgElement target) { - source.CopyJavaScriptDomAttributeValuesTo(target); - - var count = Math.Min(source.Children.Count, target.Children.Count); - for (var i = 0; i < count; i++) - { - CopyJavaScriptDomState(source.Children[i], target.Children[i]); - } + CopyJavaScriptDomAttributeValuesTo(target); } } diff --git a/tests/Svg.JavaScript.UnitTests/SvgJavaScriptRuntimeTests.cs b/tests/Svg.JavaScript.UnitTests/SvgJavaScriptRuntimeTests.cs index f3e79afa01..df2809842f 100644 --- a/tests/Svg.JavaScript.UnitTests/SvgJavaScriptRuntimeTests.cs +++ b/tests/Svg.JavaScript.UnitTests/SvgJavaScriptRuntimeTests.cs @@ -114,7 +114,8 @@ public void SvgAngleValueAsString_PreservesAssignedUnitType() """, captureJavaScriptDomState: true); var runtime = new SvgJavaScriptRuntime(document, new SvgJavaScriptSettings { ThrowOnError = true }); - var target = runtime.GetElement(document.Descendants().Single(element => element.ID == "target")); + var targetElement = document.Descendants().Single(element => element.ID == "target"); + var target = runtime.GetElement(targetElement); Assert.Equal(3, target.orientAngle.baseVal.unitType); @@ -134,6 +135,13 @@ public void SvgAngleValueAsString_PreservesAssignedUnitType() Assert.Equal("2grad", cloneTarget.orientAngle.baseVal.valueAsString); Assert.Equal(4, cloneTarget.orientAngle.baseVal.unitType); Assert.True(cloneTarget.hasAttribute("data-empty")); + + var elementClone = targetElement.DeepCopy(); + var elementCloneTarget = runtime.GetElement(elementClone); + + Assert.Equal("2grad", elementCloneTarget.orientAngle.baseVal.valueAsString); + Assert.Equal(4, elementCloneTarget.orientAngle.baseVal.unitType); + Assert.True(elementCloneTarget.hasAttribute("data-empty")); } [Fact] From a1576e25949723ddc1bb7e3f38d0db16f9e30be9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Wies=C5=82aw=20=C5=A0olt=C3=A9s?= Date: Wed, 13 May 2026 14:20:02 +0200 Subject: [PATCH 8/9] Preserve empty DOM attributes --- .../Compatibility/SvgElementFactory.cs | 6 ++--- .../SvgJavaScriptRuntimeTests.cs | 22 +++++++++++++++++++ 2 files changed, 25 insertions(+), 3 deletions(-) diff --git a/src/Svg.Custom/Compatibility/SvgElementFactory.cs b/src/Svg.Custom/Compatibility/SvgElementFactory.cs index 716c75450b..634721678b 100644 --- a/src/Svg.Custom/Compatibility/SvgElementFactory.cs +++ b/src/Svg.Custom/Compatibility/SvgElementFactory.cs @@ -151,7 +151,7 @@ private void SetAttributes(SvgElement element, XmlReader reader, SvgDocument doc } if (localName.Equals("style") && !(element is NonSvgElement)) { - if (PreserveJavaScriptDomState && !string.IsNullOrWhiteSpace(reader.Value)) + if (PreserveJavaScriptDomState) { element.SetJavaScriptDomAttributeValue(localName, reader.Value); element.CustomAttributes["style"] = reader.Value; @@ -171,7 +171,7 @@ private void SetAttributes(SvgElement element, XmlReader reader, SvgDocument doc } else if (prefix.Length == 0 && IsStyleAttribute(localName)) { - if (PreserveJavaScriptDomState && !string.IsNullOrWhiteSpace(reader.Value)) + if (PreserveJavaScriptDomState) { element.SetJavaScriptDomAttributeValue(localName, reader.Value); } @@ -185,7 +185,7 @@ private void SetAttributes(SvgElement element, XmlReader reader, SvgDocument doc } else { - if (PreserveJavaScriptDomState && !string.IsNullOrWhiteSpace(reader.Value)) + if (PreserveJavaScriptDomState) { element.SetJavaScriptDomAttributeValue(GetJavaScriptDomAttributeName(prefix, localName), reader.Value); } diff --git a/tests/Svg.JavaScript.UnitTests/SvgJavaScriptRuntimeTests.cs b/tests/Svg.JavaScript.UnitTests/SvgJavaScriptRuntimeTests.cs index df2809842f..dd41007a0d 100644 --- a/tests/Svg.JavaScript.UnitTests/SvgJavaScriptRuntimeTests.cs +++ b/tests/Svg.JavaScript.UnitTests/SvgJavaScriptRuntimeTests.cs @@ -144,6 +144,28 @@ public void SvgAngleValueAsString_PreservesAssignedUnitType() Assert.True(elementCloneTarget.hasAttribute("data-empty")); } + [Fact] + public void ParsedEmptyAttributes_PreserveJavaScriptDomState() + { + var document = LoadDocument(""" + + + + + """, captureJavaScriptDomState: true); + + var runtime = new SvgJavaScriptRuntime(document, new SvgJavaScriptSettings { ThrowOnError = true }); + var target = runtime.GetElement(document.Descendants().Single(element => element.ID == "target")); + var marker = runtime.GetElement(document.Descendants().Single(element => element.ID == "marker")); + + Assert.True(target.hasAttribute("style")); + Assert.Equal(string.Empty, target.getAttribute("style")); + Assert.True(target.hasAttribute("data-space")); + Assert.Equal(" ", target.getAttribute("data-space")); + Assert.True(marker.hasAttribute("orient")); + Assert.Equal(string.Empty, marker.getAttribute("orient")); + } + [Fact] public void AppendChild_MovesTextNodeOutOfPreviousParent() { From 31176a9452430d94ff2aa378e21c2f7341cf2439 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Wies=C5=82aw=20=C5=A0olt=C3=A9s?= Date: Wed, 13 May 2026 14:34:24 +0200 Subject: [PATCH 9/9] Sync DOM style state after CSSOM changes --- .../Scripting/SvgJavaScriptElement.cs | 30 ++++++++++++++----- .../SvgJavaScriptRuntimeTests.cs | 28 +++++++++++++++++ 2 files changed, 50 insertions(+), 8 deletions(-) diff --git a/src/Svg.JavaScript/Scripting/SvgJavaScriptElement.cs b/src/Svg.JavaScript/Scripting/SvgJavaScriptElement.cs index 3531daff1d..7dda5b0128 100644 --- a/src/Svg.JavaScript/Scripting/SvgJavaScriptElement.cs +++ b/src/Svg.JavaScript/Scripting/SvgJavaScriptElement.cs @@ -234,7 +234,6 @@ public void setAttribute(string name, object? value) var text = Convert.ToString(value, CultureInfo.InvariantCulture) ?? string.Empty; if (string.Equals(normalizedName, "style", StringComparison.OrdinalIgnoreCase)) { - Element.SetJavaScriptDomAttributeValue(normalizedName, text); SetInlineStyleText(text); _runtime.MarkMutation(); return; @@ -275,7 +274,7 @@ public void removeAttribute(string name) if (string.Equals(normalizedName, "style", StringComparison.OrdinalIgnoreCase)) { Element.ClearJavaScriptDomAttributeValue(normalizedName); - SetInlineStyleText(string.Empty); + SetInlineStyleText(string.Empty, syncJavaScriptDomAttribute: false); _runtime.MarkMutation(); return; } @@ -742,13 +741,14 @@ internal string RemoveStyleProperty(string name) var normalizedName = name.Trim(); var declarations = ParseInlineStyle(GetRawStyleText(Element)); - if (!declarations.TryGetValue(normalizedName, out var previous)) - { - previous = string.Empty; - } + var hadProperty = declarations.TryGetValue(normalizedName, out var previous); + previous ??= string.Empty; declarations.Remove(normalizedName); - SetInlineStyleText(SerializeInlineStyle(declarations), declarations); + SetInlineStyleText( + SerializeInlineStyle(declarations), + declarations, + HasRawStyleAttribute() || hadProperty); _runtime.MarkMutation(); return previous; } @@ -1437,17 +1437,31 @@ private void RemoveAttributeValue(string name) Element.ClearJavaScriptDomAttributeValue(name); } - private void SetInlineStyleText(string styleText, Dictionary? declarations = null) + private void SetInlineStyleText( + string styleText, + Dictionary? declarations = null, + bool syncJavaScriptDomAttribute = true) { var previousDeclarations = ParseInlineStyle(GetRawStyleText(Element)); declarations ??= ParseInlineStyle(styleText); CaptureInlineStyleFallbacks(previousDeclarations, declarations); SetRawStyleText(Element, styleText); + if (syncJavaScriptDomAttribute) + { + Element.SetJavaScriptDomAttributeValue("style", styleText); + } + _document.RawDocument.UpdateCompatibilityStyleText(Element, styleText); RestoreRemovedInlineStyleFallbacks(previousDeclarations, declarations); _document.RawDocument.ReapplyCompatibilityStyles(); } + private bool HasRawStyleAttribute() + { + return Element.TryGetJavaScriptDomAttributeValue("style", out _) || + Element.CustomAttributes.ContainsKey("style"); + } + private void CaptureInlineStyleFallbacks( IReadOnlyDictionary previousDeclarations, IReadOnlyDictionary nextDeclarations) diff --git a/tests/Svg.JavaScript.UnitTests/SvgJavaScriptRuntimeTests.cs b/tests/Svg.JavaScript.UnitTests/SvgJavaScriptRuntimeTests.cs index dd41007a0d..068c19f558 100644 --- a/tests/Svg.JavaScript.UnitTests/SvgJavaScriptRuntimeTests.cs +++ b/tests/Svg.JavaScript.UnitTests/SvgJavaScriptRuntimeTests.cs @@ -166,6 +166,34 @@ public void ParsedEmptyAttributes_PreserveJavaScriptDomState() Assert.Equal(string.Empty, marker.getAttribute("orient")); } + [Fact] + public void StylePropertyMutations_UpdateRawStyleAttribute() + { + var document = LoadDocument(""" + + + + """, captureJavaScriptDomState: true); + + var runtime = new SvgJavaScriptRuntime(document, new SvgJavaScriptSettings { ThrowOnError = true }); + var target = runtime.GetElement(document.Descendants().Single(element => element.ID == "target")); + + Assert.Equal("fill: red", target.getAttribute("style")); + + target.style.setProperty("fill", "green"); + + Assert.Equal("fill: green", target.getAttribute("style")); + + Assert.Equal("green", target.style.removeProperty("fill")); + Assert.True(target.hasAttribute("style")); + Assert.Equal(string.Empty, target.getAttribute("style")); + + target.removeAttribute("style"); + + Assert.False(target.hasAttribute("style")); + Assert.Equal(string.Empty, target.getAttribute("style")); + } + [Fact] public void AppendChild_MovesTextNodeOutOfPreviousParent() {