diff options
| author | Shadowghost <Shadowghost@users.noreply.github.com> | 2026-09-15 11:16:11 -0400 |
|---|---|---|
| committer | Cody Robibero <cody@robibe.ro> | 2026-09-15 11:16:11 -0400 |
| commit | 8e2b62331d8f6c632ad8bcc80149818ba5b8a800 (patch) | |
| tree | f792fb4559cffddb29bb0053b7acfcdfb0a9022a | |
| parent | 6a317f45ce5def14b4feccc4ae84f2e27441b997 (diff) | |
Backport pull request #17956 from jellyfin/release-12.z
Don't resample or sharpen images Skia isn't actually resizing
Original-merge: 7d7faf08b7769af0f6c544cab59fb57316167d84
Merged-by: crobibero <cody@robibe.ro>
Backported-by: Cody Robibero <cody@robibe.ro>
| -rw-r--r-- | src/Jellyfin.Drawing.Skia/SkiaEncoder.cs | 14 | ||||
| -rw-r--r-- | tests/Jellyfin.Drawing.Skia.Tests/SkiaEncoderResizeTests.cs | 100 |
2 files changed, 112 insertions, 2 deletions
diff --git a/src/Jellyfin.Drawing.Skia/SkiaEncoder.cs b/src/Jellyfin.Drawing.Skia/SkiaEncoder.cs index 4cdff055f4..b8d40614d2 100644 --- a/src/Jellyfin.Drawing.Skia/SkiaEncoder.cs +++ b/src/Jellyfin.Drawing.Skia/SkiaEncoder.cs @@ -556,6 +556,13 @@ public class SkiaEncoder : IImageEncoder /// <returns>The resized image.</returns> internal static SKImage ResizeImage(SKBitmap source, SKImageInfo targetInfo, bool isAntialias = false, bool isDither = false) { + if (source.Width == targetInfo.Width && source.Height == targetInfo.Height) + { + return SKImage.FromBitmap(source); + } + + var isDownscale = source.Width > targetInfo.Width || source.Height > targetInfo.Height; + using var target = new SKBitmap(targetInfo); using var canvas = new SKCanvas(target); using var paint = new SKPaint(); @@ -565,7 +572,7 @@ public class SkiaEncoder : IImageEncoder // Historically, kHigh implied cubic filtering, but only when upsampling. // If specified kHigh, and were down-sampling, Skia used to switch back to kMedium (bilinear filtering plus mipmaps). // With current skia API, passing Mitchell cubic when down-sampling will cause serious quality degradation. - var samplingOptions = source.Width > targetInfo.Width || source.Height > targetInfo.Height + var samplingOptions = isDownscale ? DefaultSamplingOptions : UpscaleSamplingOptions; @@ -576,7 +583,10 @@ public class SkiaEncoder : IImageEncoder samplingOptions, paint); - SharpenInPlace(target); + if (isDownscale) + { + SharpenInPlace(target); + } return SKImage.FromBitmap(target); } diff --git a/tests/Jellyfin.Drawing.Skia.Tests/SkiaEncoderResizeTests.cs b/tests/Jellyfin.Drawing.Skia.Tests/SkiaEncoderResizeTests.cs new file mode 100644 index 0000000000..18518df1c2 --- /dev/null +++ b/tests/Jellyfin.Drawing.Skia.Tests/SkiaEncoderResizeTests.cs @@ -0,0 +1,100 @@ +using SkiaSharp; +using Xunit; + +namespace Jellyfin.Drawing.Skia.Tests; + +/// <summary> +/// Covers what <see cref="SkiaEncoder.ResizeImage"/> does either side of a resize: at matching +/// dimensions it must not touch the image at all, and sharpening belongs to downscales only. +/// </summary> +public class SkiaEncoderResizeTests +{ + private static SKBitmap CreateEdgeBitmap(int width, int height) + { + var bitmap = new SKBitmap(new SKImageInfo(width, height, SKColorType.Rgba8888, SKAlphaType.Premul)); + using var canvas = new SKCanvas(bitmap); + canvas.Clear(new SKColor(40, 60, 80)); + using var paint = new SKPaint { Color = new SKColor(220, 210, 200) }; + canvas.DrawRect(SKRect.Create(0, 0, width / 2f, height), paint); + + return bitmap; + } + + private static SKImageInfo InfoFor(SKBitmap source, int width, int height) + => new SKImageInfo(width, height, source.ColorType, source.AlphaType, source.ColorSpace); + + /// <summary> + /// Draws without sharpening, which is what the resize is expected to reduce to when it is not + /// downscaling. + /// </summary> + private static SKBitmap DrawOnly(SKBitmap source, SKImageInfo targetInfo, SKSamplingOptions sampling) + { + var target = new SKBitmap(targetInfo); + using var canvas = new SKCanvas(target); + using var paint = new SKPaint(); + canvas.DrawBitmap( + source, + SKRect.Create(0, 0, source.Width, source.Height), + SKRect.Create(0, 0, targetInfo.Width, targetInfo.Height), + sampling, + paint); + + return target; + } + + private static void AssertSamePixels(SKBitmap expected, SKBitmap actual) + { + Assert.Equal(expected.Width, actual.Width); + Assert.Equal(expected.Height, actual.Height); + + for (var y = 0; y < expected.Height; y++) + { + for (var x = 0; x < expected.Width; x++) + { + Assert.Equal(expected.GetPixel(x, y), actual.GetPixel(x, y)); + } + } + } + + [Fact] + public void ResizeImage_MatchingDimensions_ReturnsTheImageUntouched() + { + using var source = CreateEdgeBitmap(16, 16); + + using var result = SkiaEncoder.ResizeImage(source, InfoFor(source, 16, 16)); + using var resultBitmap = SKBitmap.FromImage(result); + + // Unsharpened, so the edge is still exactly where it was. + AssertSamePixels(source, resultBitmap); + } + + [Fact] + public void ResizeImage_Upscale_DoesNotSharpen() + { + using var source = CreateEdgeBitmap(8, 8); + var targetInfo = InfoFor(source, 24, 24); + + using var result = SkiaEncoder.ResizeImage(source, targetInfo); + using var resultBitmap = SKBitmap.FromImage(result); + using var expected = DrawOnly(source, targetInfo, SkiaEncoder.UpscaleSamplingOptions); + + AssertSamePixels(expected, resultBitmap); + } + + [Fact] + public void ResizeImage_Downscale_StillSharpens() + { + using var source = CreateEdgeBitmap(32, 32); + var targetInfo = InfoFor(source, 16, 16); + + using var result = SkiaEncoder.ResizeImage(source, targetInfo); + using var resultBitmap = SKBitmap.FromImage(result); + using var unsharpened = DrawOnly(source, targetInfo, SkiaEncoder.DefaultSamplingOptions); + using var sharpened = DrawOnly(source, targetInfo, SkiaEncoder.DefaultSamplingOptions); + SkiaEncoder.SharpenInPlace(sharpened); + + AssertSamePixels(sharpened, resultBitmap); + // Guards the test itself: the edge has to be something sharpening actually changes. + Assert.NotEqual(unsharpened.GetPixel(8, 8), sharpened.GetPixel(8, 8)); + } +} |
