From 7ce99aaf785d3567633febfb60de1b26c43f7d4d Mon Sep 17 00:00:00 2001 From: Erwin de Haan Date: Fri, 31 Jul 2020 21:20:05 +0200 Subject: [PATCH 1/5] Update SkiaSharp to 2.80.1 and replace resize code. This fixed the blurry resized images in the Web UI. --- .../Jellyfin.Drawing.Skia.csproj | 5 +-- Jellyfin.Drawing.Skia/SkiaEncoder.cs | 43 +++++++++++++++++-- Jellyfin.Drawing.Skia/StripCollageBuilder.cs | 13 +++--- 3 files changed, 46 insertions(+), 15 deletions(-) diff --git a/Jellyfin.Drawing.Skia/Jellyfin.Drawing.Skia.csproj b/Jellyfin.Drawing.Skia/Jellyfin.Drawing.Skia.csproj index 65a4394594..c71c76f08f 100644 --- a/Jellyfin.Drawing.Skia/Jellyfin.Drawing.Skia.csproj +++ b/Jellyfin.Drawing.Skia/Jellyfin.Drawing.Skia.csproj @@ -20,9 +20,8 @@ - - - + + diff --git a/Jellyfin.Drawing.Skia/SkiaEncoder.cs b/Jellyfin.Drawing.Skia/SkiaEncoder.cs index 1f7c43de81..a66ecc0813 100644 --- a/Jellyfin.Drawing.Skia/SkiaEncoder.cs +++ b/Jellyfin.Drawing.Skia/SkiaEncoder.cs @@ -395,6 +395,42 @@ namespace Jellyfin.Drawing.Skia return rotated; } + /// + /// Resizes an image on the CPU, by utilizing a surface and canvas. + /// + /// The source bitmap. + /// This specifies the target size and other information required to create the surface. + /// This enables anti-aliasing on the SKPaint instance. + /// This enables dithering on the SKPaint instance. + /// The resized image. + internal static SKImage ResizeImage(SKBitmap source, SKImageInfo targetInfo, bool isAntialias = false, bool isDither = false) + { + using var surface = SKSurface.Create(targetInfo); + using var canvas = surface.Canvas; + using var paint = new SKPaint(); + + paint.FilterQuality = SKFilterQuality.High; + paint.IsAntialias = isAntialias; + paint.IsDither = isDither; + + var kernel = new float[9] + { + 0, -.1f, 0, + -.1f, 1.4f, -.1f, + 0, -.1f, 0, + }; + + var kernelSize = new SKSizeI(3, 3); + var kernelOffset = new SKPointI(1, 1); + + paint.ImageFilter = SKImageFilter.CreateMatrixConvolution( + kernelSize, kernel, 1f, 0f, kernelOffset, SKShaderTileMode.Clamp, false); + + canvas.DrawBitmap(source, SKRect.Create(0, 0, source.Width, source.Height), SKRect.Create(0, 0, targetInfo.Width, targetInfo.Height), paint); + + return surface.Snapshot(); + } + /// public string EncodeImage(string inputPath, DateTime dateModified, string outputPath, bool autoOrient, ImageOrientation? orientation, int quality, ImageProcessingOptions options, ImageFormat selectedOutputFormat) { @@ -436,9 +472,8 @@ namespace Jellyfin.Drawing.Skia var width = newImageSize.Width; var height = newImageSize.Height; - using var resizedBitmap = new SKBitmap(width, height, bitmap.ColorType, bitmap.AlphaType); - // scale image - bitmap.ScalePixels(resizedBitmap, SKFilterQuality.High); + // scale image (the FromImage creates a copy) + using var resizedBitmap = SKBitmap.FromImage(ResizeImage(bitmap, new SKImageInfo(width, height, bitmap.ColorType, bitmap.AlphaType, bitmap.ColorSpace))); // If all we're doing is resizing then we can stop now if (!hasBackgroundColor && !hasForegroundColor && blur == 0 && !hasIndicator) @@ -446,7 +481,7 @@ namespace Jellyfin.Drawing.Skia Directory.CreateDirectory(Path.GetDirectoryName(outputPath)); using var outputStream = new SKFileWStream(outputPath); using var pixmap = new SKPixmap(new SKImageInfo(width, height), resizedBitmap.GetPixels()); - pixmap.Encode(outputStream, skiaOutputFormat, quality); + resizedBitmap.Encode(outputStream, skiaOutputFormat, quality); return outputPath; } diff --git a/Jellyfin.Drawing.Skia/StripCollageBuilder.cs b/Jellyfin.Drawing.Skia/StripCollageBuilder.cs index e0ee4a342d..3b35594865 100644 --- a/Jellyfin.Drawing.Skia/StripCollageBuilder.cs +++ b/Jellyfin.Drawing.Skia/StripCollageBuilder.cs @@ -115,15 +115,13 @@ namespace Jellyfin.Drawing.Skia // resize to the same aspect as the original int iWidth = Math.Abs(iHeight * currentBitmap.Width / currentBitmap.Height); - using var resizeBitmap = new SKBitmap(iWidth, iHeight, currentBitmap.ColorType, currentBitmap.AlphaType); - currentBitmap.ScalePixels(resizeBitmap, SKFilterQuality.High); + using var resizedImage = SkiaEncoder.ResizeImage(bitmap, new SKImageInfo(iWidth, iHeight, currentBitmap.ColorType, currentBitmap.AlphaType, currentBitmap.ColorSpace)); // crop image int ix = Math.Abs((iWidth - iSlice) / 2); - using var image = SKImage.FromBitmap(resizeBitmap); - using var subset = image.Subset(SKRectI.Create(ix, 0, iSlice, iHeight)); + using var subset = resizedImage.Subset(SKRectI.Create(ix, 0, iSlice, iHeight)); // draw image onto canvas - canvas.DrawImage(subset ?? image, iSlice * i, 0); + canvas.DrawImage(subset ?? resizedImage, iSlice * i, 0); } return bitmap; @@ -177,9 +175,8 @@ namespace Jellyfin.Drawing.Skia continue; } - using var resizedBitmap = new SKBitmap(cellWidth, cellHeight, currentBitmap.ColorType, currentBitmap.AlphaType); - // scale image - currentBitmap.ScalePixels(resizedBitmap, SKFilterQuality.High); + // Scale image. The FromBitmap creates a copy + using var resizedBitmap = SKBitmap.FromImage(SkiaEncoder.ResizeImage(bitmap, new SKImageInfo(cellWidth, cellHeight, currentBitmap.ColorType, currentBitmap.AlphaType, currentBitmap.ColorSpace))); // draw this image into the strip at the next position var xPos = x * cellWidth; From af38e5469f30b1b08e341fa00a7a7eea53388b8a Mon Sep 17 00:00:00 2001 From: Erwin de Haan Date: Fri, 31 Jul 2020 21:33:25 +0200 Subject: [PATCH 2/5] Update Jellyfin.Drawing.Skia/SkiaEncoder.cs indentation. Co-authored-by: Cody Robibero --- Jellyfin.Drawing.Skia/SkiaEncoder.cs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/Jellyfin.Drawing.Skia/SkiaEncoder.cs b/Jellyfin.Drawing.Skia/SkiaEncoder.cs index a66ecc0813..4e08758f62 100644 --- a/Jellyfin.Drawing.Skia/SkiaEncoder.cs +++ b/Jellyfin.Drawing.Skia/SkiaEncoder.cs @@ -415,9 +415,9 @@ namespace Jellyfin.Drawing.Skia var kernel = new float[9] { - 0, -.1f, 0, - -.1f, 1.4f, -.1f, - 0, -.1f, 0, + 0, -.1f, 0, + -.1f, 1.4f, -.1f, + 0, -.1f, 0, }; var kernelSize = new SKSizeI(3, 3); From 526eea41f0b74c5717575887839ecc41ca22067f Mon Sep 17 00:00:00 2001 From: Erwin de Haan Date: Fri, 31 Jul 2020 22:02:16 +0200 Subject: [PATCH 3/5] Add a note on the convolutional matrix filter. --- Jellyfin.Drawing.Skia/SkiaEncoder.cs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/Jellyfin.Drawing.Skia/SkiaEncoder.cs b/Jellyfin.Drawing.Skia/SkiaEncoder.cs index 4e08758f62..58d3039557 100644 --- a/Jellyfin.Drawing.Skia/SkiaEncoder.cs +++ b/Jellyfin.Drawing.Skia/SkiaEncoder.cs @@ -397,6 +397,9 @@ namespace Jellyfin.Drawing.Skia /// /// Resizes an image on the CPU, by utilizing a surface and canvas. + /// + /// The convolutional matrix kernel used in this resize function gives a (light) sharpening effect. + /// This technique is similar to effect that can be created using for example the [Convolution matrix filter in GIMP](https://docs.gimp.org/2.10/en/gimp-filter-convolution-matrix.html). /// /// The source bitmap. /// This specifies the target size and other information required to create the surface. From 44aca4dc6ff478776ca56c95763b3db7177234f3 Mon Sep 17 00:00:00 2001 From: Erwin de Haan Date: Fri, 31 Jul 2020 22:12:20 +0200 Subject: [PATCH 4/5] Formatting in SkiaEncoder.cs --- Jellyfin.Drawing.Skia/SkiaEncoder.cs | 25 ++++++++++++++++++------- 1 file changed, 18 insertions(+), 7 deletions(-) diff --git a/Jellyfin.Drawing.Skia/SkiaEncoder.cs b/Jellyfin.Drawing.Skia/SkiaEncoder.cs index 58d3039557..62e1d6ed14 100644 --- a/Jellyfin.Drawing.Skia/SkiaEncoder.cs +++ b/Jellyfin.Drawing.Skia/SkiaEncoder.cs @@ -410,11 +410,12 @@ namespace Jellyfin.Drawing.Skia { using var surface = SKSurface.Create(targetInfo); using var canvas = surface.Canvas; - using var paint = new SKPaint(); - - paint.FilterQuality = SKFilterQuality.High; - paint.IsAntialias = isAntialias; - paint.IsDither = isDither; + using var paint = new SKPaint + { + FilterQuality = SKFilterQuality.High, + IsAntialias = isAntialias, + IsDither = isDither + }; var kernel = new float[9] { @@ -427,9 +428,19 @@ namespace Jellyfin.Drawing.Skia var kernelOffset = new SKPointI(1, 1); paint.ImageFilter = SKImageFilter.CreateMatrixConvolution( - kernelSize, kernel, 1f, 0f, kernelOffset, SKShaderTileMode.Clamp, false); + kernelSize, + kernel, + 1f, + 0f, + kernelOffset, + SKShaderTileMode.Clamp, + false); - canvas.DrawBitmap(source, SKRect.Create(0, 0, source.Width, source.Height), SKRect.Create(0, 0, targetInfo.Width, targetInfo.Height), paint); + canvas.DrawBitmap( + source, + SKRect.Create(0, 0, source.Width, source.Height), + SKRect.Create(0, 0, targetInfo.Width, targetInfo.Height), + paint); return surface.Snapshot(); } From 0f43780c8c50a6d933091056aaa523905f6e7b1a Mon Sep 17 00:00:00 2001 From: Erwin de Haan Date: Sun, 2 Aug 2020 12:43:25 +0200 Subject: [PATCH 5/5] Apply suggestions from code review Co-authored-by: Claus Vium --- Jellyfin.Drawing.Skia/SkiaEncoder.cs | 3 ++- Jellyfin.Drawing.Skia/StripCollageBuilder.cs | 3 ++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/Jellyfin.Drawing.Skia/SkiaEncoder.cs b/Jellyfin.Drawing.Skia/SkiaEncoder.cs index 62e1d6ed14..a1caa751b1 100644 --- a/Jellyfin.Drawing.Skia/SkiaEncoder.cs +++ b/Jellyfin.Drawing.Skia/SkiaEncoder.cs @@ -487,7 +487,8 @@ namespace Jellyfin.Drawing.Skia var height = newImageSize.Height; // scale image (the FromImage creates a copy) - using var resizedBitmap = SKBitmap.FromImage(ResizeImage(bitmap, new SKImageInfo(width, height, bitmap.ColorType, bitmap.AlphaType, bitmap.ColorSpace))); + var imageInfo = new SKImageInfo(width, height, bitmap.ColorType, bitmap.AlphaType, bitmap.ColorSpace); + using var resizedBitmap = SKBitmap.FromImage(ResizeImage(bitmap, imageInfo)); // If all we're doing is resizing then we can stop now if (!hasBackgroundColor && !hasForegroundColor && blur == 0 && !hasIndicator) diff --git a/Jellyfin.Drawing.Skia/StripCollageBuilder.cs b/Jellyfin.Drawing.Skia/StripCollageBuilder.cs index 3b35594865..b08c3750d7 100644 --- a/Jellyfin.Drawing.Skia/StripCollageBuilder.cs +++ b/Jellyfin.Drawing.Skia/StripCollageBuilder.cs @@ -176,7 +176,8 @@ namespace Jellyfin.Drawing.Skia } // Scale image. The FromBitmap creates a copy - using var resizedBitmap = SKBitmap.FromImage(SkiaEncoder.ResizeImage(bitmap, new SKImageInfo(cellWidth, cellHeight, currentBitmap.ColorType, currentBitmap.AlphaType, currentBitmap.ColorSpace))); + var imageInfo = new SKImageInfo(cellWidth, cellHeight, currentBitmap.ColorType, currentBitmap.AlphaType, currentBitmap.ColorSpace); + using var resizedBitmap = SKBitmap.FromImage(SkiaEncoder.ResizeImage(bitmap, imageInfo)); // draw this image into the strip at the next position var xPos = x * cellWidth;