Explorar o código

Update skiasharp monorepo (major) (#13369)

renovate[bot] hai 2 semanas
pai
achega
cdbf4752b9

+ 8 - 8
Directory.Packages.props

@@ -10,15 +10,15 @@
     <PackageVersion Include="AutoFixture" Version="4.18.1" />
     <PackageVersion Include="BDInfo" Version="0.8.0" />
     <PackageVersion Include="BitFaster.Caching" Version="2.5.3" />
-    <PackageVersion Include="BlurHashSharp.SkiaSharp" Version="1.3.4" />
-    <PackageVersion Include="BlurHashSharp" Version="1.3.4" />
+    <PackageVersion Include="BlurHashSharp.SkiaSharp" Version="1.4.0-pre.1" />
+    <PackageVersion Include="BlurHashSharp" Version="1.4.0-pre.1" />
     <PackageVersion Include="CommandLineParser" Version="2.9.1" />
     <PackageVersion Include="coverlet.collector" Version="6.0.4" />
     <PackageVersion Include="Diacritics" Version="3.3.29" />
     <PackageVersion Include="DiscUtils.Udf" Version="0.16.13" />
     <PackageVersion Include="DotNet.Glob" Version="3.1.3" />
     <PackageVersion Include="FsCheck.Xunit" Version="3.2.0" />
-    <PackageVersion Include="HarfBuzzSharp.NativeAssets.Linux" Version="7.3.0.3" />
+    <PackageVersion Include="HarfBuzzSharp.NativeAssets.Linux" Version="8.3.1.1" />
     <PackageVersion Include="ICU4N.Transliterator" Version="60.1.0-alpha.356" />
     <PackageVersion Include="IDisposableAnalyzers" Version="4.0.8" />
     <PackageVersion Include="Ignore" Version="0.2.1" />
@@ -67,12 +67,12 @@
     <PackageVersion Include="Serilog.Sinks.Graylog" Version="3.1.1" />
     <PackageVersion Include="SerilogAnalyzer" Version="0.15.0" />
     <PackageVersion Include="SharpFuzz" Version="2.2.0" />
-    <PackageVersion Include="SkiaSharp" Version="2.88.9" />
-    <PackageVersion Include="SkiaSharp.HarfBuzz" Version="2.88.9" />
-    <PackageVersion Include="SkiaSharp.NativeAssets.Linux" Version="2.88.9" />
+    <PackageVersion Include="SkiaSharp" Version="3.119.0" />
+    <PackageVersion Include="SkiaSharp.HarfBuzz" Version="3.119.0" />
+    <PackageVersion Include="SkiaSharp.NativeAssets.Linux" Version="3.119.0" />
     <PackageVersion Include="SmartAnalyzers.MultithreadingAnalyzer" Version="1.1.31" />
     <PackageVersion Include="StyleCop.Analyzers" Version="1.2.0-beta.556" />
-    <PackageVersion Include="Svg.Skia" Version="2.0.0.8" />
+    <PackageVersion Include="Svg.Skia" Version="3.0.2" />
     <PackageVersion Include="Swashbuckle.AspNetCore.ReDoc" Version="6.5.0" />
     <PackageVersion Include="Swashbuckle.AspNetCore" Version="6.2.3" />
     <PackageVersion Include="System.Globalization" Version="4.3.0" />
@@ -89,4 +89,4 @@
     <PackageVersion Include="Xunit.SkippableFact" Version="1.5.23" />
     <PackageVersion Include="xunit" Version="2.9.3" />
   </ItemGroup>
-</Project>
+</Project>

+ 29 - 10
src/Jellyfin.Drawing.Skia/SkiaEncoder.cs

@@ -27,6 +27,16 @@ public class SkiaEncoder : IImageEncoder
     private static readonly SKImageFilter _imageFilter;
     private static readonly SKTypeface[] _typefaces;
 
+    /// <summary>
+    /// The default sampling options, equivalent to old high quality filter settings when upscaling.
+    /// </summary>
+    public static readonly SKSamplingOptions UpscaleSamplingOptions;
+
+    /// <summary>
+    /// The sampling options, used for downscaling images, equivalent to old high quality filter settings when not upscaling.
+    /// </summary>
+    public static readonly SKSamplingOptions DefaultSamplingOptions;
+
 #pragma warning disable CA1810
     static SkiaEncoder()
 #pragma warning restore CA1810
@@ -63,6 +73,11 @@ public class SkiaEncoder : IImageEncoder
             SKFontManager.Default.MatchCharacter(null, SKFontStyleWeight.Bold, SKFontStyleWidth.Normal, SKFontStyleSlant.Upright, null, 'ي'), // Arabic
             SKTypeface.FromFamilyName("sans-serif", SKFontStyleWeight.Bold, SKFontStyleWidth.Normal, SKFontStyleSlant.Upright) // Default font
         ];
+
+        // use cubic for upscaling
+        UpscaleSamplingOptions = new SKSamplingOptions(SKCubicResampler.Mitchell);
+        // use bilinear for everything else
+        DefaultSamplingOptions = new SKSamplingOptions(SKFilterMode.Linear, SKMipmapMode.Linear);
     }
 
     /// <summary>
@@ -441,7 +456,7 @@ public class SkiaEncoder : IImageEncoder
                     break;
             }
 
-            surface.DrawBitmap(bitmap, 0, 0);
+            surface.DrawBitmap(bitmap, 0, 0, DefaultSamplingOptions);
             return rotated;
         }
         catch (Exception e)
@@ -467,18 +482,23 @@ public class SkiaEncoder : IImageEncoder
     {
         using var surface = SKSurface.Create(targetInfo);
         using var canvas = surface.Canvas;
-        using var paint = new SKPaint
-        {
-            FilterQuality = SKFilterQuality.High,
-            IsAntialias = isAntialias,
-            IsDither = isDither
-        };
+        using var paint = new SKPaint();
+        paint.IsAntialias = isAntialias;
+        paint.IsDither = isDither;
+
+        // 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
+            ? DefaultSamplingOptions
+            : UpscaleSamplingOptions;
 
         paint.ImageFilter = _imageFilter;
         canvas.DrawBitmap(
             source,
             SKRect.Create(0, 0, source.Width, source.Height),
             SKRect.Create(0, 0, targetInfo.Width, targetInfo.Height),
+            samplingOptions,
             paint);
 
         return surface.Snapshot();
@@ -560,11 +580,10 @@ public class SkiaEncoder : IImageEncoder
         using var paint = new SKPaint();
         // Add blur if option is present
         using var filter = blur > 0 ? SKImageFilter.CreateBlur(blur, blur) : null;
-        paint.FilterQuality = SKFilterQuality.High;
         paint.ImageFilter = filter;
 
         // create image from resized bitmap to apply blur
-        canvas.DrawBitmap(resizedBitmap, SKRect.Create(width, height), paint);
+        canvas.DrawBitmap(resizedBitmap, SKRect.Create(width, height), DefaultSamplingOptions, paint);
 
         // If foreground layer present then draw
         if (hasForegroundColor)
@@ -690,7 +709,7 @@ public class SkiaEncoder : IImageEncoder
                     throw new InvalidOperationException("Image height does not match first image height.");
                 }
 
-                canvas.DrawBitmap(img, x * imgWidth, y * imgHeight.Value);
+                canvas.DrawBitmap(img, x * imgWidth, y * imgHeight.Value, DefaultSamplingOptions);
             }
         }
 

+ 58 - 0
src/Jellyfin.Drawing.Skia/SkiaExtensions.cs

@@ -0,0 +1,58 @@
+using SkiaSharp;
+
+namespace Jellyfin.Drawing.Skia;
+
+/// <summary>
+/// The SkiaSharp extensions.
+/// </summary>
+public static class SkiaExtensions
+{
+    /// <summary>
+    /// Draws an SKBitmap on the canvas with specified SkSamplingOptions.
+    /// </summary>
+    /// <param name="canvas">The SKCanvas to draw on.</param>
+    /// <param name="bitmap">The SKBitmap to draw.</param>
+    /// <param name="dest">The destination SKRect.</param>
+    /// <param name="options">The SKSamplingOptions to use for rendering.</param>
+    /// <param name="paint">Optional SKPaint to apply additional effects or styles.</param>
+    public static void DrawBitmap(this SKCanvas canvas, SKBitmap bitmap, SKRect dest, SKSamplingOptions options, SKPaint? paint = null)
+    {
+        using var image = SKImage.FromBitmap(bitmap);
+        canvas.DrawImage(image, dest, options, paint);
+    }
+
+    /// <summary>
+    /// Draws an SKBitmap on the canvas at the specified coordinates with the given SkSamplingOptions.
+    /// </summary>
+    /// <param name="canvas">The SKCanvas to draw on.</param>
+    /// <param name="bitmap">The SKBitmap to draw.</param>
+    /// <param name="x">The x-coordinate where the bitmap will be drawn.</param>
+    /// <param name="y">The y-coordinate where the bitmap will be drawn.</param>
+    /// <param name="options">The SKSamplingOptions to use for rendering.</param>
+    /// <param name="paint">Optional SKPaint to apply additional effects or styles.</param>
+    public static void DrawBitmap(this SKCanvas canvas, SKBitmap bitmap, float x, float y, SKSamplingOptions options, SKPaint? paint = null)
+    {
+        using var image = SKImage.FromBitmap(bitmap);
+        canvas.DrawImage(image, x, y, options, paint);
+    }
+
+    /// <summary>
+    /// Draws an SKBitmap on the canvas using a specified source rectangle, destination rectangle,
+    /// and optional paint, with the given SkSamplingOptions.
+    /// </summary>
+    /// <param name="canvas">The SKCanvas to draw on.</param>
+    /// <param name="bitmap">The SKBitmap to draw.</param>
+    /// <param name="source">
+    /// The source SKRect defining the portion of the bitmap to draw.
+    /// </param>
+    /// <param name="dest">
+    /// The destination SKRect defining the area on the canvas where the bitmap will be drawn.
+    /// </param>
+    /// <param name="options">The SKSamplingOptions to use for rendering.</param>
+    /// <param name="paint">Optional SKPaint to apply additional effects or styles.</param>
+    public static void DrawBitmap(this SKCanvas canvas, SKBitmap bitmap, SKRect source, SKRect dest, SKSamplingOptions options, SKPaint? paint = null)
+    {
+        using var image = SKImage.FromBitmap(bitmap);
+        canvas.DrawImage(image, source, dest, options, paint);
+    }
+}

+ 5 - 3
src/Jellyfin.Drawing.Skia/SplashscreenBuilder.cs

@@ -101,10 +101,12 @@ public class SplashscreenBuilder
                     {
                         var imageWidth = Math.Abs(posterHeight * currentImage.Width / currentImage.Height);
                         using var resizedBitmap = new SKBitmap(imageWidth, posterHeight);
-                        currentImage.ScalePixels(resizedBitmap, SKFilterQuality.High);
-
+                        var samplingOptions = currentImage.Width > imageWidth || currentImage.Height > posterHeight
+                            ? SkiaEncoder.DefaultSamplingOptions
+                            : SkiaEncoder.UpscaleSamplingOptions;
+                        currentImage.ScalePixels(resizedBitmap, samplingOptions);
                         // draw on canvas
-                        canvas.DrawBitmap(resizedBitmap, currentWidthPos, currentHeight);
+                        canvas.DrawBitmap(resizedBitmap, currentWidthPos, currentHeight, samplingOptions);
 
                         // resize to the same aspect as the original
                         currentWidthPos += imageWidth + Spacing;

+ 38 - 43
src/Jellyfin.Drawing.Skia/StripCollageBuilder.cs

@@ -111,38 +111,31 @@ public partial class StripCollageBuilder
         var backdropHeight = Math.Abs(width * backdrop.Height / backdrop.Width);
         using var resizedBackdrop = SkiaEncoder.ResizeImage(backdrop, new SKImageInfo(width, backdropHeight, backdrop.ColorType, backdrop.AlphaType, backdrop.ColorSpace));
         using var paint = new SKPaint();
-        paint.FilterQuality = SKFilterQuality.High;
         // draw the backdrop
-        canvas.DrawImage(resizedBackdrop, 0, 0, paint);
+        canvas.DrawImage(resizedBackdrop, 0, 0, SkiaEncoder.DefaultSamplingOptions, paint);
 
         // draw shadow rectangle
-        using var paintColor = new SKPaint
-        {
-            Color = SKColors.Black.WithAlpha(0x78),
-            Style = SKPaintStyle.Fill,
-            FilterQuality = SKFilterQuality.High
-        };
+        using var paintColor = new SKPaint();
+        paintColor.Color = SKColors.Black.WithAlpha(0x78);
+        paintColor.Style = SKPaintStyle.Fill;
         canvas.DrawRect(0, 0, width, height, paintColor);
 
         var typeFace = SkiaEncoder.DefaultTypeFace;
 
         // draw library name
-        using var textPaint = new SKPaint
-        {
-            Color = SKColors.White,
-            Style = SKPaintStyle.Fill,
-            TextSize = 112,
-            TextAlign = SKTextAlign.Left,
-            Typeface = typeFace,
-            IsAntialias = true,
-            FilterQuality = SKFilterQuality.High
-        };
+        using var textFont = new SKFont();
+        textFont.Size = 112;
+        textFont.Typeface = typeFace;
+        using var textPaint = new SKPaint();
+        textPaint.Color = SKColors.White;
+        textPaint.Style = SKPaintStyle.Fill;
+        textPaint.IsAntialias = true;
 
         // scale down text to 90% of the width if text is larger than 95% of the width
-        var textWidth = textPaint.MeasureText(libraryName);
+        var textWidth = textFont.MeasureText(libraryName);
         if (textWidth > width * 0.95)
         {
-            textPaint.TextSize = 0.9f * width * textPaint.TextSize / textWidth;
+            textFont.Size = 0.9f * width * textFont.Size / textWidth;
         }
 
         if (string.IsNullOrWhiteSpace(libraryName))
@@ -150,23 +143,22 @@ public partial class StripCollageBuilder
             return bitmap;
         }
 
-        var realWidth = DrawText(null, 0, (height / 2f) + (textPaint.FontMetrics.XHeight / 2), libraryName, textPaint);
+        var realWidth = DrawText(null, 0, (height / 2f) + (textFont.Metrics.XHeight / 2), libraryName, textPaint, textFont);
         if (realWidth > width * 0.95)
         {
-            textPaint.TextSize = 0.9f * width * textPaint.TextSize / realWidth;
-            realWidth = DrawText(null, 0, (height / 2f) + (textPaint.FontMetrics.XHeight / 2), libraryName, textPaint);
+            textFont.Size = 0.9f * width * textFont.Size / realWidth;
+            realWidth = DrawText(null, 0, (height / 2f) + (textFont.Metrics.XHeight / 2), libraryName, textPaint, textFont);
         }
 
         var padding = (width - realWidth) / 2;
 
         if (IsRtlTextRegex().IsMatch(libraryName))
         {
-            textPaint.TextAlign = SKTextAlign.Right;
-            DrawText(canvas, width - padding, (height / 2f) + (textPaint.FontMetrics.XHeight / 2), libraryName, textPaint, true);
+            DrawText(canvas, width - padding, (height / 2f) + (textFont.Metrics.XHeight / 2), libraryName, textPaint, textFont, true);
         }
         else
         {
-            DrawText(canvas, padding, (height / 2f) + (textPaint.FontMetrics.XHeight / 2), libraryName, textPaint);
+            DrawText(canvas, padding, (height / 2f) + (textFont.Metrics.XHeight / 2), libraryName, textPaint, textFont);
         }
 
         return bitmap;
@@ -196,12 +188,11 @@ public partial class StripCollageBuilder
                 var imageInfo = new SKImageInfo(cellWidth, cellHeight, currentBitmap.ColorType, currentBitmap.AlphaType, currentBitmap.ColorSpace);
                 using var resizeImage = SkiaEncoder.ResizeImage(currentBitmap, imageInfo);
                 using var paint = new SKPaint();
-                paint.FilterQuality = SKFilterQuality.High;
 
                 // draw this image into the strip at the next position
                 var xPos = x * cellWidth;
                 var yPos = y * cellHeight;
-                canvas.DrawImage(resizeImage, xPos, yPos, paint);
+                canvas.DrawImage(resizeImage, xPos, yPos, SkiaEncoder.DefaultSamplingOptions, paint);
             }
         }
 
@@ -216,11 +207,13 @@ public partial class StripCollageBuilder
     /// <param name="y">y position of the canvas to draw text.</param>
     /// <param name="text">The text to draw.</param>
     /// <param name="textPaint">The SKPaint to style the text.</param>
+    /// <param name="textFont">The SKFont to style the text.</param>
+    /// <param name="alignment">The alignment of the text. Default aligns to left.</param>
     /// <returns>The width of the text.</returns>
-    private static float MeasureAndDrawText(SKCanvas? canvas, float x, float y, string text, SKPaint textPaint)
+    private static float MeasureAndDrawText(SKCanvas? canvas, float x, float y, string text, SKPaint textPaint, SKFont textFont, SKTextAlign alignment = SKTextAlign.Left)
     {
-        var width = textPaint.MeasureText(text);
-        canvas?.DrawShapedText(text, x, y, textPaint);
+        var width = textFont.MeasureText(text);
+        canvas?.DrawShapedText(text, x, y, alignment, textFont, textPaint);
         return width;
     }
 
@@ -232,16 +225,18 @@ public partial class StripCollageBuilder
     /// <param name="y">y position of the canvas to draw text.</param>
     /// <param name="text">The text to draw.</param>
     /// <param name="textPaint">The SKPaint to style the text.</param>
+    /// <param name="textFont">The SKFont to style the text.</param>
     /// <param name="isRtl">If true, render from right to left.</param>
     /// <returns>The width of the text.</returns>
-    private static float DrawText(SKCanvas? canvas, float x, float y, string text, SKPaint textPaint, bool isRtl = false)
+    private static float DrawText(SKCanvas? canvas, float x, float y, string text, SKPaint textPaint, SKFont textFont, bool isRtl = false)
     {
         float width = 0;
+        var alignment = isRtl ? SKTextAlign.Right : SKTextAlign.Left;
 
-        if (textPaint.ContainsGlyphs(text))
+        if (textFont.ContainsGlyphs(text))
         {
             // Current font can render all characters in text
-            return MeasureAndDrawText(canvas, x, y, text, textPaint);
+            return MeasureAndDrawText(canvas, x, y, text, textPaint, textFont, alignment);
         }
 
         // Iterate over all text elements using TextElementEnumerator
@@ -254,7 +249,7 @@ public partial class StripCollageBuilder
         {
             bool notAtEnd;
             var textElement = enumerator.GetTextElement();
-            if (textPaint.ContainsGlyphs(textElement))
+            if (textFont.ContainsGlyphs(textElement))
             {
                 continue;
             }
@@ -264,12 +259,12 @@ public partial class StripCollageBuilder
             if (start != enumerator.ElementIndex)
             {
                 var regularText = text.Substring(start, enumerator.ElementIndex - start);
-                width += MeasureAndDrawText(canvas, MoveX(x, width), y, regularText, textPaint);
+                width += MeasureAndDrawText(canvas, MoveX(x, width), y, regularText, textPaint, textFont, alignment);
                 start = enumerator.ElementIndex;
             }
 
             // Search for next point where current font can render the character there
-            while ((notAtEnd = enumerator.MoveNext()) && !textPaint.ContainsGlyphs(enumerator.GetTextElement()))
+            while ((notAtEnd = enumerator.MoveNext()) && !textFont.ContainsGlyphs(enumerator.GetTextElement()))
             {
                 // Do nothing, just move enumerator to the point where current font can render the character
             }
@@ -284,21 +279,21 @@ public partial class StripCollageBuilder
 
             if (fallback is not null)
             {
+                using var fallbackTextFont = new SKFont();
+                fallbackTextFont.Size = textFont.Size;
+                fallbackTextFont.Typeface = fallback;
                 using var fallbackTextPaint = new SKPaint();
                 fallbackTextPaint.Color = textPaint.Color;
                 fallbackTextPaint.Style = textPaint.Style;
-                fallbackTextPaint.TextSize = textPaint.TextSize;
-                fallbackTextPaint.TextAlign = textPaint.TextAlign;
-                fallbackTextPaint.Typeface = fallback;
                 fallbackTextPaint.IsAntialias = textPaint.IsAntialias;
 
                 // Do the search recursively to select all possible fonts
-                width += DrawText(canvas, MoveX(x, width), y, subtext, fallbackTextPaint, isRtl);
+                width += DrawText(canvas, MoveX(x, width), y, subtext, fallbackTextPaint, fallbackTextFont, isRtl);
             }
             else
             {
                 // Used up all fonts and no fonts can be found, just use current font
-                width += MeasureAndDrawText(canvas, MoveX(x, width), y, text[start..], textPaint);
+                width += MeasureAndDrawText(canvas, MoveX(x, width), y, text[start..], textPaint, textFont, alignment);
             }
 
             start = notAtEnd ? enumerator.ElementIndex : text.Length;
@@ -307,7 +302,7 @@ public partial class StripCollageBuilder
         // Render the remaining text that current fonts can render
         if (start < text.Length)
         {
-            width += MeasureAndDrawText(canvas, MoveX(x, width), y, text[start..], textPaint);
+            width += MeasureAndDrawText(canvas, MoveX(x, width), y, text[start..], textPaint, textFont, alignment);
         }
 
         return width;

+ 5 - 3
src/Jellyfin.Drawing.Skia/UnplayedCountIndicator.cs

@@ -34,10 +34,12 @@ public static class UnplayedCountIndicator
             Style = SKPaintStyle.Fill
         };
 
+        using var font = new SKFont();
+
         canvas.DrawCircle(x, OffsetFromTopRightCorner, 20, paint);
 
         paint.Color = new SKColor(255, 255, 255, 255);
-        paint.TextSize = 24;
+        font.Size = 24;
         paint.IsAntialias = true;
 
         var y = OffsetFromTopRightCorner + 9;
@@ -55,9 +57,9 @@ public static class UnplayedCountIndicator
         {
             x -= 15;
             y -= 2;
-            paint.TextSize = 18;
+            font.Size = 18;
         }
 
-        canvas.DrawText(text, x, y, paint);
+        canvas.DrawText(text, x, y, font, paint);
     }
 }