From bfe859f782727e9e9f1eb825094f922eb74ec6eb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Wies=C5=82aw=20=C5=A0olt=C3=A9s?= Date: Wed, 22 Apr 2026 23:39:30 +0200 Subject: [PATCH 1/2] Fix gradient stop offset normalization --- src/Svg.Model/Services/PaintingService.cs | 12 ++++++++++-- src/Svg.SceneGraph/SvgScenePaintingService.cs | 12 ++++++++++-- 2 files changed, 20 insertions(+), 4 deletions(-) diff --git a/src/Svg.Model/Services/PaintingService.cs b/src/Svg.Model/Services/PaintingService.cs index 5529c88992..6ae06d3361 100644 --- a/src/Svg.Model/Services/PaintingService.cs +++ b/src/Svg.Model/Services/PaintingService.cs @@ -134,8 +134,7 @@ private static void GetStopsImpl( { stopColor = ToLinear(stopColor); } - var offset = svgGradientStop.Offset.ToDeviceValue(UnitRenderingType.Horizontal, svgGradientServer, skBounds); - offset /= skBounds.Width; + var offset = ToGradientStopOffset(svgGradientStop.Offset); colors.Add(stopColor); colorPos.Add(offset); } @@ -183,6 +182,15 @@ private static void AdjustStopColorPos(List colorPos) } } + private static float ToGradientStopOffset(SvgUnit offset) + { + var value = offset.Type == SvgUnitType.Percentage + ? offset.Value / 100f + : offset.Value; + + return Math.Min(Math.Max(value, 0f), 1f); + } + internal static SKColorF[] ToSkColorF(this SKColor[] skColors) { var skColorsF = new SKColorF[skColors.Length]; diff --git a/src/Svg.SceneGraph/SvgScenePaintingService.cs b/src/Svg.SceneGraph/SvgScenePaintingService.cs index 968735ac53..e00d16d7aa 100644 --- a/src/Svg.SceneGraph/SvgScenePaintingService.cs +++ b/src/Svg.SceneGraph/SvgScenePaintingService.cs @@ -466,8 +466,7 @@ private static void GetStops( stopColor = ToLinear(stopColor); } - var offset = svgGradientStop.Offset.ToDeviceValue(UnitRenderingType.Horizontal, svgReferencedGradientServer, skBounds); - offset /= skBounds.Width; + var offset = ToGradientStopOffset(svgGradientStop.Offset); colors.Add(stopColor); colorPos.Add(offset); } @@ -491,6 +490,15 @@ private static void AdjustStopColorPos(List colorPos) } } + private static float ToGradientStopOffset(SvgUnit offset) + { + var value = offset.Type == SvgUnitType.Percentage + ? offset.Value / 100f + : offset.Value; + + return Math.Min(Math.Max(value, 0f), 1f); + } + private static SKColorF[] ToSkColorF(IReadOnlyList skColors) { var skColorsF = new SKColorF[skColors.Count]; From d90eedaf9e9897537d7bfbe1226ca4d28e03006b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Wies=C5=82aw=20=C5=A0olt=C3=A9s?= Date: Wed, 22 Apr 2026 23:39:38 +0200 Subject: [PATCH 2/2] Add issue 441 gradient stroke regression --- tests/Svg.Skia.UnitTests/Issue441Tests.cs | 135 ++++++++++++++++++++++ 1 file changed, 135 insertions(+) create mode 100644 tests/Svg.Skia.UnitTests/Issue441Tests.cs diff --git a/tests/Svg.Skia.UnitTests/Issue441Tests.cs b/tests/Svg.Skia.UnitTests/Issue441Tests.cs new file mode 100644 index 0000000000..775ec1104f --- /dev/null +++ b/tests/Svg.Skia.UnitTests/Issue441Tests.cs @@ -0,0 +1,135 @@ +using System.Linq; +using ShimSkiaSharp; +using ShimSkiaSharp.Editing; +using SkiaSharp; +using Xunit; +using ShimPaintStyle = ShimSkiaSharp.SKPaintStyle; +using ShimPoint = ShimSkiaSharp.SKPoint; +using SkiaAlphaType = SkiaSharp.SKAlphaType; +using SkiaBitmap = SkiaSharp.SKBitmap; +using SkiaColor = SkiaSharp.SKColor; +using SkiaColorSpace = SkiaSharp.SKColorSpace; +using SkiaColorType = SkiaSharp.SKColorType; +using SkiaPicture = SkiaSharp.SKPicture; + +namespace Svg.Skia.UnitTests; + +public class Issue441Tests +{ + [Fact] + public void CssLinearGradientStroke_ResolvesStylesheetPaintServerAndRendersStroke() + { + using var svg = new SKSvg(); + svg.FromSvg(Issue441Svg); + + Assert.NotNull(svg.Model); + Assert.NotNull(svg.Picture); + + var gradientStroke = svg.Model! + .FindCommands() + .Single(command => command.Paint?.Shader is LinearGradientShader); + AssertIssue441GradientStroke(gradientStroke); + + var retainedModel = svg.CreateRetainedSceneGraphModel(); + Assert.NotNull(retainedModel); + + var retainedGradientStroke = retainedModel! + .FindCommands() + .Single(command => command.Paint?.Shader is LinearGradientShader); + AssertIssue441GradientStroke(retainedGradientStroke); + + using var bitmap = RenderBitmap(svg.Picture!, 4f); + AssertContainsYellowPixel(bitmap, 16, 16, 24, 28); + } + + private static void AssertIssue441GradientStroke(DrawPathCanvasCommand gradientStroke) + { + var shader = Assert.IsType(gradientStroke.Paint!.Shader); + Assert.Equal(ShimPaintStyle.Stroke, gradientStroke.Paint.Style); + Assert.Equal(3f, gradientStroke.Paint.StrokeWidth); + Assert.Equal(new ShimPoint(6.5f, 5f), shader.Start); + Assert.Equal(new ShimPoint(6.5f, 17f), shader.End); + Assert.NotNull(shader.Colors); + Assert.NotNull(shader.ColorPos); + Assert.Equal(6, shader.Colors!.Length); + Assert.Equal(shader.Colors.Length, shader.ColorPos!.Length); + AssertGradientStops(shader.ColorPos); + AssertYellow(shader.Colors[0]); + } + + private static void AssertGradientStops(float[] colorPos) + { + float[] expected = [0.16f, 0.33f, 0.72f, 0.86f, 0.9f, 1f]; + + Assert.Equal(expected.Length, colorPos.Length); + for (var i = 0; i < expected.Length; i++) + { + Assert.Equal(expected[i], colorPos[i], precision: 3); + } + } + + private static void AssertYellow(ShimSkiaSharp.SKColorF color) + { + Assert.True(color.Red > 0.9f, $"Expected yellow red channel but got {color}."); + Assert.True(color.Green > 0.9f, $"Expected yellow green channel but got {color}."); + Assert.True(color.Blue < 0.2f, $"Expected yellow blue channel but got {color}."); + } + + private static void AssertContainsYellowPixel(SkiaBitmap bitmap, int left, int top, int width, int height) + { + for (var y = top; y < top + height; y++) + { + for (var x = left; x < left + width; x++) + { + var color = bitmap.GetPixel(x, y); + if (IsYellowPixel(color)) + { + return; + } + } + } + + Assert.Fail("Expected the rendered CSS gradient stroke to contain a visible yellow pixel."); + } + + private static bool IsYellowPixel(SkiaColor color) + { + return color.Alpha > 180 + && color.Red > 180 + && color.Green > 170 + && color.Blue < 100; + } + + private static SkiaBitmap RenderBitmap(SkiaPicture picture, float scale) + { + var bitmap = picture.ToBitmap( + SKColors.Transparent, + scale, + scale, + SkiaColorType.Rgba8888, + SkiaAlphaType.Unpremul, + SkiaColorSpace.CreateSrgb()); + + return Assert.IsType(bitmap); + } + + private const string Issue441Svg = """ + + + + + + + + + + + + + + + + """; +}