diff --git a/src/Svg.Skia/SKPictureExtensions.cs b/src/Svg.Skia/SKPictureExtensions.cs
index 0c43eeb991..4cea094419 100644
--- a/src/Svg.Skia/SKPictureExtensions.cs
+++ b/src/Svg.Skia/SKPictureExtensions.cs
@@ -6,6 +6,9 @@ namespace Svg.Skia;
public static class SKPictureExtensions
{
+ private const float DownsampleRasterOversample = 4f;
+ private const long MaxDownsampleRasterPixels = 16L * 1024L * 1024L;
+
public static void Draw(this SkiaSharp.SKPicture skPicture, SkiaSharp.SKColor background, float scaleX, float scaleY, SkiaSharp.SKCanvas skCanvas)
{
skCanvas.Clear(background);
@@ -28,6 +31,29 @@ public static void Draw(this SkiaSharp.SKPicture skPicture, SkiaSharp.SKColor ba
return null;
}
+ GetRasterRenderScales(skPicture, skImageInfo, scaleX, scaleY, out var renderScaleX, out var renderScaleY);
+ if (ShouldDownsample(scaleX, scaleY, renderScaleX, renderScaleY))
+ {
+ if (!TryCreateImageInfo(skPicture, renderScaleX, renderScaleY, skColorType, skAlphaType, skColorSpace, out var renderImageInfo))
+ {
+ return null;
+ }
+
+ using var renderSurface = SkiaSharp.SKSurface.Create(renderImageInfo);
+ if (renderSurface is null)
+ {
+ return null;
+ }
+
+ Draw(skPicture, background, renderScaleX, renderScaleY, renderSurface.Canvas);
+ using var renderImage = renderSurface.Snapshot();
+
+ var downsampledBitmap = new SkiaSharp.SKBitmap(skImageInfo);
+ using var downsampledCanvas = new SkiaSharp.SKCanvas(downsampledBitmap);
+ Downsample(renderImage, skImageInfo, downsampledCanvas);
+ return downsampledBitmap;
+ }
+
var skBitmap = new SkiaSharp.SKBitmap(skImageInfo);
using var skCanvas = new SkiaSharp.SKCanvas(skBitmap);
Draw(skPicture, background, scaleX, scaleY, skCanvas);
@@ -41,6 +67,34 @@ public static bool ToImage(this SkiaSharp.SKPicture skPicture, Stream stream, Sk
return false;
}
+ GetRasterRenderScales(skPicture, skImageInfo, scaleX, scaleY, out var renderScaleX, out var renderScaleY);
+ if (ShouldDownsample(scaleX, scaleY, renderScaleX, renderScaleY))
+ {
+ if (!TryCreateImageInfo(skPicture, renderScaleX, renderScaleY, skColorType, skAlphaType, skColorSpace, out var renderImageInfo))
+ {
+ return false;
+ }
+
+ using var renderSurface = SkiaSharp.SKSurface.Create(renderImageInfo);
+ if (renderSurface is null)
+ {
+ return false;
+ }
+
+ Draw(skPicture, background, renderScaleX, renderScaleY, renderSurface.Canvas);
+ using var renderImage = renderSurface.Snapshot();
+
+ using var downsampledSurface = SkiaSharp.SKSurface.Create(skImageInfo);
+ if (downsampledSurface is null)
+ {
+ return false;
+ }
+
+ Downsample(renderImage, skImageInfo, downsampledSurface.Canvas);
+ using var downsampledImage = downsampledSurface.Snapshot();
+ return Encode(downsampledImage, stream, format, quality);
+ }
+
using var skSurface = SkiaSharp.SKSurface.Create(skImageInfo);
if (skSurface is null)
{
@@ -49,6 +103,11 @@ public static bool ToImage(this SkiaSharp.SKPicture skPicture, Stream stream, Sk
Draw(skPicture, background, scaleX, scaleY, skSurface.Canvas);
using var skImage = skSurface.Snapshot();
+ return Encode(skImage, stream, format, quality);
+ }
+
+ private static bool Encode(SkiaSharp.SKImage skImage, Stream stream, SkiaSharp.SKEncodedImageFormat format, int quality)
+ {
using var skData = skImage.Encode(format, quality);
if (skData is { })
{
@@ -160,4 +219,81 @@ private static bool TryCreateImageInfo(this SkiaSharp.SKPicture skPicture, float
skImageInfo = new SkiaSharp.SKImageInfo((int)width, (int)height, skColorType, skAlphaType, skColorSpace);
return true;
}
+
+ private static void GetRasterRenderScales(SkiaSharp.SKPicture skPicture, SkiaSharp.SKImageInfo targetImageInfo, float scaleX, float scaleY, out float renderScaleX, out float renderScaleY)
+ {
+ renderScaleX = GetRasterRenderScale(scaleX, float.PositiveInfinity);
+ renderScaleY = GetRasterRenderScale(scaleY, float.PositiveInfinity);
+ if (!ShouldDownsample(scaleX, scaleY, renderScaleX, renderScaleY) || FitsRasterBudget(skPicture, renderScaleX, renderScaleY))
+ {
+ return;
+ }
+
+ renderScaleX = GetRasterRenderScale(scaleX, DownsampleRasterOversample);
+ renderScaleY = GetRasterRenderScale(scaleY, DownsampleRasterOversample);
+ if (FitsRasterBudget(skPicture, renderScaleX, renderScaleY))
+ {
+ return;
+ }
+
+ var targetPixels = (long)targetImageInfo.Width * targetImageInfo.Height;
+ if (targetPixels <= 0L || targetPixels >= MaxDownsampleRasterPixels)
+ {
+ renderScaleX = scaleX;
+ renderScaleY = scaleY;
+ return;
+ }
+
+ var boundedOversample = (float)System.Math.Sqrt((double)MaxDownsampleRasterPixels / targetPixels);
+ if (boundedOversample < 1f)
+ {
+ boundedOversample = 1f;
+ }
+ else if (boundedOversample > DownsampleRasterOversample)
+ {
+ boundedOversample = DownsampleRasterOversample;
+ }
+
+ renderScaleX = GetRasterRenderScale(scaleX, boundedOversample);
+ renderScaleY = GetRasterRenderScale(scaleY, boundedOversample);
+ }
+
+ private static float GetRasterRenderScale(float scale, float oversample)
+ {
+ if (scale >= 1f)
+ {
+ return scale;
+ }
+
+ var oversampledScale = scale * oversample;
+ return oversampledScale < 1f ? oversampledScale : 1f;
+ }
+
+ private static bool FitsRasterBudget(SkiaSharp.SKPicture skPicture, float scaleX, float scaleY)
+ {
+ var width = skPicture.CullRect.Width * scaleX;
+ var height = skPicture.CullRect.Height * scaleY;
+ if (!(width > 0) || !(height > 0))
+ {
+ return false;
+ }
+
+ return (double)width * height <= MaxDownsampleRasterPixels;
+ }
+
+ private static bool ShouldDownsample(float scaleX, float scaleY, float renderScaleX, float renderScaleY)
+ {
+ return renderScaleX != scaleX || renderScaleY != scaleY;
+ }
+
+ private static void Downsample(SkiaSharp.SKImage renderImage, SkiaSharp.SKImageInfo targetImageInfo, SkiaSharp.SKCanvas targetCanvas)
+ {
+ 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);
+ }
}
diff --git a/tests/Svg.Skia.UnitTests/SKSvgTests.cs b/tests/Svg.Skia.UnitTests/SKSvgTests.cs
index 323bd11e01..2b89e88e0a 100644
--- a/tests/Svg.Skia.UnitTests/SKSvgTests.cs
+++ b/tests/Svg.Skia.UnitTests/SKSvgTests.cs
@@ -155,6 +155,129 @@ public void Save_InheritedCurrentColor_UsesConsumingElementsColor()
Assert.Equal((byte)255, pixel.A);
}
+ [Fact]
+ public void Save_DownscaledMorphologyFilter_RetainsSubpixelInsideStroke()
+ {
+ const string pathData = "m228 91.1c-2.5 0.5-8.3 1.6-13 2.5-4.7 0.9-13.8 3.3-20.3 5.5-6.4 2.1-16.5 6.3-22.5 9.3-5.9 3-14.6 8-19.4 11.3-4.8 3.2-12.6 9.2-17.4 13.3-4.9 4.1-12.1 11.3-16.2 16-4.1 4.7-10.7 13.4-14.8 19.5-4 6-9.3 15.3-11.8 20.5-2.5 5.2-6.1 14.2-8 20-2 5.8-4.4 14.8-5.5 20-1.1 5.2-2.5 13.9-3 19.2-0.6 5.4-1.1 12.9-1.1 16.8 0 3.8 0.7 11.6 1.5 17.2 0.9 5.7 2.7 14.8 4.2 20.3 1.4 5.5 4.4 14.7 6.6 20.5 2.2 5.8 7 16.3 10.7 23.5 3.6 7.1 10.2 18.6 14.5 25.5 4.4 6.9 12.7 18.8 18.5 26.5 5.7 7.7 14.9 19.2 20.3 25.5 5.5 6.3 15.1 17.1 21.6 23.9 6.4 6.9 19 19.4 28.1 27.9 9.1 8.5 23.1 21 31.2 27.8 10.3 8.6 15.6 12.4 17.3 12.4 1.4 0 4-1 5.7-2.3 1.8-1.2 8-6.2 13.8-11 5.8-4.9 18.1-15.9 27.4-24.5 9.3-8.7 22.8-22 30-29.7 7.2-7.7 16.9-18.5 21.7-24 4.7-5.5 11.8-14.3 15.9-19.5 4.1-5.2 11.6-15.6 16.6-23 5-7.4 12.2-18.9 15.9-25.5 3.7-6.6 8.6-16.3 11-21.5 2.3-5.2 5.7-13.6 7.4-18.5 1.8-5 4.1-12.6 5.2-17 1.1-4.4 2.7-12.5 3.5-18 0.7-5.5 1.4-13.5 1.4-17.8 0-4.2-0.7-13-1.5-19.5-0.9-6.4-2.7-16.2-4.1-21.7-1.4-5.5-4.1-14.3-6.1-19.5-1.9-5.2-5.7-13.8-8.5-19-2.8-5.2-7.8-13.5-11.2-18.4-3.3-4.9-9.5-12.8-13.6-17.5-4.1-4.7-11.1-11.6-15.5-15.4-4.4-3.7-12.1-9.5-17-12.9-5-3.3-13.7-8.4-19.5-11.3-5.8-2.9-14.6-6.7-19.5-8.4-5-1.8-11.5-3.8-14.5-4.6-3-0.8-10.8-2.3-17.3-3.5-8.8-1.5-15.7-2-28-1.9-8.9 0.1-18.2 0.5-20.7 1z";
+ var svgMarkup = $$"""
+
+ """;
+
+ var svg = new SKSvg();
+ using var input = new MemoryStream(Encoding.UTF8.GetBytes(svgMarkup));
+ using var _ = svg.Load(input);
+ using var output = new MemoryStream();
+
+ Assert.True(svg.Save(
+ output,
+ SkiaSharp.SKColors.Transparent,
+ scaleX: 38f / 609f,
+ scaleY: 38f / 609f));
+
+ output.Position = 0;
+ using var image = Image.Load(output);
+ var whitePixels = 0;
+ for (var y = 0; y < image.Height; y++)
+ {
+ for (var x = 0; x < image.Width; x++)
+ {
+ var pixel = image[x, y];
+ if (pixel.A > 64 && pixel.R > 160 && pixel.G > 160 && pixel.B > 160)
+ {
+ whitePixels++;
+ }
+ }
+ }
+
+ Assert.Equal(31, image.Width);
+ Assert.Equal(38, image.Height);
+ Assert.True(whitePixels > 0, "Expected visible white inside stroke pixels after downscaling.");
+ }
+
+ [Fact]
+ public void Save_DownscaledTranslucentBackground_DoesNotCompositeBackgroundTwice()
+ {
+ const string svgMarkup = """
+
+ """;
+
+ var svg = new SKSvg();
+ using var input = new MemoryStream(Encoding.UTF8.GetBytes(svgMarkup));
+ using var _ = svg.Load(input);
+ using var output = new MemoryStream();
+
+ Assert.True(svg.Save(
+ output,
+ new SkiaSharp.SKColor(10, 20, 30, 128),
+ scaleX: 0.5f,
+ scaleY: 0.5f));
+
+ output.Position = 0;
+ using var image = Image.Load(output);
+ var pixel = image[25, 25];
+
+ Assert.Equal(50, image.Width);
+ Assert.Equal(50, image.Height);
+ Assert.Equal((byte)10, pixel.R);
+ Assert.Equal((byte)20, pixel.G);
+ Assert.Equal((byte)30, pixel.B);
+ Assert.Equal((byte)128, pixel.A);
+ }
+
+ [Fact]
+ public void Save_LargeDownscaledDocument_UsesBoundedIntermediate()
+ {
+ const string svgMarkup = """
+
+ """;
+
+ var svg = new SKSvg();
+ using var input = new MemoryStream(Encoding.UTF8.GetBytes(svgMarkup));
+ using var _ = svg.Load(input);
+ using var output = new MemoryStream();
+
+ Assert.True(svg.Save(
+ output,
+ SkiaSharp.SKColors.Transparent,
+ scaleX: 0.001f,
+ scaleY: 0.001f));
+
+ output.Position = 0;
+ using var image = Image.Load(output);
+ Assert.Equal(100, image.Width);
+ Assert.Equal(100, image.Height);
+ Assert.Equal((byte)255, image[50, 50].R);
+ }
+
[Fact]
public void Load_CurrentColorParameter_ProvidesRootCurrentColor()
{