Bladeren bron

Use file-scoped namespaces in Jellyfin.Drawing.Skia

Patrick Barron 2 jaren geleden
bovenliggende
commit
cafc454cfb

+ 22 - 23
src/Jellyfin.Drawing.Skia/PercentPlayedDrawer.cs

@@ -2,35 +2,34 @@ using System;
 using MediaBrowser.Model.Drawing;
 using SkiaSharp;
 
-namespace Jellyfin.Drawing.Skia
+namespace Jellyfin.Drawing.Skia;
+
+/// <summary>
+/// Static helper class used to draw percentage-played indicators on images.
+/// </summary>
+public static class PercentPlayedDrawer
 {
+    private const int IndicatorHeight = 8;
+
     /// <summary>
-    /// Static helper class used to draw percentage-played indicators on images.
+    /// Draw a percentage played indicator on a canvas.
     /// </summary>
-    public static class PercentPlayedDrawer
+    /// <param name="canvas">The canvas to draw the indicator on.</param>
+    /// <param name="imageSize">The size of the image being drawn on.</param>
+    /// <param name="percent">The percentage played to display with the indicator.</param>
+    public static void Process(SKCanvas canvas, ImageDimensions imageSize, double percent)
     {
-        private const int IndicatorHeight = 8;
-
-        /// <summary>
-        /// Draw a percentage played indicator on a canvas.
-        /// </summary>
-        /// <param name="canvas">The canvas to draw the indicator on.</param>
-        /// <param name="imageSize">The size of the image being drawn on.</param>
-        /// <param name="percent">The percentage played to display with the indicator.</param>
-        public static void Process(SKCanvas canvas, ImageDimensions imageSize, double percent)
-        {
-            using var paint = new SKPaint();
-            var endX = imageSize.Width - 1;
-            var endY = imageSize.Height - 1;
+        using var paint = new SKPaint();
+        var endX = imageSize.Width - 1;
+        var endY = imageSize.Height - 1;
 
-            paint.Color = SKColor.Parse("#99000000");
-            paint.Style = SKPaintStyle.Fill;
-            canvas.DrawRect(SKRect.Create(0, (float)endY - IndicatorHeight, endX, endY), paint);
+        paint.Color = SKColor.Parse("#99000000");
+        paint.Style = SKPaintStyle.Fill;
+        canvas.DrawRect(SKRect.Create(0, (float)endY - IndicatorHeight, endX, endY), paint);
 
-            double foregroundWidth = (endX * percent) / 100;
+        double foregroundWidth = (endX * percent) / 100;
 
-            paint.Color = SKColor.Parse("#FF00A4DC");
-            canvas.DrawRect(SKRect.Create(0, (float)endY - IndicatorHeight, Convert.ToInt32(foregroundWidth), endY), paint);
-        }
+        paint.Color = SKColor.Parse("#FF00A4DC");
+        canvas.DrawRect(SKRect.Create(0, (float)endY - IndicatorHeight, Convert.ToInt32(foregroundWidth), endY), paint);
     }
 }

+ 32 - 33
src/Jellyfin.Drawing.Skia/PlayedIndicatorDrawer.cs

@@ -1,48 +1,47 @@
 using MediaBrowser.Model.Drawing;
 using SkiaSharp;
 
-namespace Jellyfin.Drawing.Skia
+namespace Jellyfin.Drawing.Skia;
+
+/// <summary>
+/// Static helper class for drawing 'played' indicators.
+/// </summary>
+public static class PlayedIndicatorDrawer
 {
+    private const int OffsetFromTopRightCorner = 38;
+
     /// <summary>
-    /// Static helper class for drawing 'played' indicators.
+    /// Draw a 'played' indicator in the top right corner of a canvas.
     /// </summary>
-    public static class PlayedIndicatorDrawer
+    /// <param name="canvas">The canvas to draw the indicator on.</param>
+    /// <param name="imageSize">
+    /// The dimensions of the image to draw the indicator on. The width is used to determine the x-position of the
+    /// indicator.
+    /// </param>
+    public static void DrawPlayedIndicator(SKCanvas canvas, ImageDimensions imageSize)
     {
-        private const int OffsetFromTopRightCorner = 38;
-
-        /// <summary>
-        /// Draw a 'played' indicator in the top right corner of a canvas.
-        /// </summary>
-        /// <param name="canvas">The canvas to draw the indicator on.</param>
-        /// <param name="imageSize">
-        /// The dimensions of the image to draw the indicator on. The width is used to determine the x-position of the
-        /// indicator.
-        /// </param>
-        public static void DrawPlayedIndicator(SKCanvas canvas, ImageDimensions imageSize)
-        {
-            var x = imageSize.Width - OffsetFromTopRightCorner;
+        var x = imageSize.Width - OffsetFromTopRightCorner;
 
-            using var paint = new SKPaint
-            {
-                Color = SKColor.Parse("#CC00A4DC"),
-                Style = SKPaintStyle.Fill
-            };
+        using var paint = new SKPaint
+        {
+            Color = SKColor.Parse("#CC00A4DC"),
+            Style = SKPaintStyle.Fill
+        };
 
-            canvas.DrawCircle(x, OffsetFromTopRightCorner, 20, paint);
+        canvas.DrawCircle(x, OffsetFromTopRightCorner, 20, paint);
 
-            paint.Color = new SKColor(255, 255, 255, 255);
-            paint.TextSize = 30;
-            paint.IsAntialias = true;
+        paint.Color = new SKColor(255, 255, 255, 255);
+        paint.TextSize = 30;
+        paint.IsAntialias = true;
 
-            // or:
-            // var emojiChar = 0x1F680;
-            const string Text = "✔️";
-            var emojiChar = StringUtilities.GetUnicodeCharacterCode(Text, SKTextEncoding.Utf32);
+        // or:
+        // var emojiChar = 0x1F680;
+        const string Text = "✔️";
+        var emojiChar = StringUtilities.GetUnicodeCharacterCode(Text, SKTextEncoding.Utf32);
 
-            // ask the font manager for a font with that character
-            paint.Typeface = SKFontManager.Default.MatchCharacter(emojiChar);
+        // ask the font manager for a font with that character
+        paint.Typeface = SKFontManager.Default.MatchCharacter(emojiChar);
 
-            canvas.DrawText(Text, (float)x - 12, OffsetFromTopRightCorner + 12, paint);
-        }
+        canvas.DrawText(Text, (float)x - 12, OffsetFromTopRightCorner + 12, paint);
     }
 }

+ 33 - 34
src/Jellyfin.Drawing.Skia/SkiaCodecException.cs

@@ -1,45 +1,44 @@
 using System.Globalization;
 using SkiaSharp;
 
-namespace Jellyfin.Drawing.Skia
+namespace Jellyfin.Drawing.Skia;
+
+/// <summary>
+/// Represents errors that occur during interaction with Skia codecs.
+/// </summary>
+public class SkiaCodecException : SkiaException
 {
     /// <summary>
-    /// Represents errors that occur during interaction with Skia codecs.
+    /// Initializes a new instance of the <see cref="SkiaCodecException" /> class.
     /// </summary>
-    public class SkiaCodecException : SkiaException
+    /// <param name="result">The non-successful codec result returned by Skia.</param>
+    public SkiaCodecException(SKCodecResult result)
     {
-        /// <summary>
-        /// Initializes a new instance of the <see cref="SkiaCodecException" /> class.
-        /// </summary>
-        /// <param name="result">The non-successful codec result returned by Skia.</param>
-        public SkiaCodecException(SKCodecResult result)
-        {
-            CodecResult = result;
-        }
+        CodecResult = result;
+    }
 
-        /// <summary>
-        /// Initializes a new instance of the <see cref="SkiaCodecException" /> class
-        /// with a specified error message.
-        /// </summary>
-        /// <param name="result">The non-successful codec result returned by Skia.</param>
-        /// <param name="message">The message that describes the error.</param>
-        public SkiaCodecException(SKCodecResult result, string message)
-            : base(message)
-        {
-            CodecResult = result;
-        }
+    /// <summary>
+    /// Initializes a new instance of the <see cref="SkiaCodecException" /> class
+    /// with a specified error message.
+    /// </summary>
+    /// <param name="result">The non-successful codec result returned by Skia.</param>
+    /// <param name="message">The message that describes the error.</param>
+    public SkiaCodecException(SKCodecResult result, string message)
+        : base(message)
+    {
+        CodecResult = result;
+    }
 
-        /// <summary>
-        /// Gets the non-successful codec result returned by Skia.
-        /// </summary>
-        public SKCodecResult CodecResult { get; }
+    /// <summary>
+    /// Gets the non-successful codec result returned by Skia.
+    /// </summary>
+    public SKCodecResult CodecResult { get; }
 
-        /// <inheritdoc />
-        public override string ToString()
-            => string.Format(
-                CultureInfo.InvariantCulture,
-                "Non-success codec result: {0}\n{1}",
-                CodecResult,
-                base.ToString());
-    }
+    /// <inheritdoc />
+    public override string ToString()
+        => string.Format(
+            CultureInfo.InvariantCulture,
+            "Non-success codec result: {0}\n{1}",
+            CodecResult,
+            base.ToString());
 }

+ 427 - 428
src/Jellyfin.Drawing.Skia/SkiaEncoder.cs

@@ -12,534 +12,533 @@ using Microsoft.Extensions.Logging;
 using SkiaSharp;
 using SKSvg = SkiaSharp.Extended.Svg.SKSvg;
 
-namespace Jellyfin.Drawing.Skia
+namespace Jellyfin.Drawing.Skia;
+
+/// <summary>
+/// Image encoder that uses <see cref="SkiaSharp"/> to manipulate images.
+/// </summary>
+public class SkiaEncoder : IImageEncoder
 {
+    private static readonly HashSet<string> _transparentImageTypes = new(StringComparer.OrdinalIgnoreCase) { ".png", ".gif", ".webp" };
+
+    private readonly ILogger<SkiaEncoder> _logger;
+    private readonly IApplicationPaths _appPaths;
+
     /// <summary>
-    /// Image encoder that uses <see cref="SkiaSharp"/> to manipulate images.
+    /// Initializes a new instance of the <see cref="SkiaEncoder"/> class.
     /// </summary>
-    public class SkiaEncoder : IImageEncoder
+    /// <param name="logger">The application logger.</param>
+    /// <param name="appPaths">The application paths.</param>
+    public SkiaEncoder(ILogger<SkiaEncoder> logger, IApplicationPaths appPaths)
     {
-        private static readonly HashSet<string> _transparentImageTypes = new(StringComparer.OrdinalIgnoreCase) { ".png", ".gif", ".webp" };
+        _logger = logger;
+        _appPaths = appPaths;
+    }
 
-        private readonly ILogger<SkiaEncoder> _logger;
-        private readonly IApplicationPaths _appPaths;
+    /// <inheritdoc/>
+    public string Name => "Skia";
 
-        /// <summary>
-        /// Initializes a new instance of the <see cref="SkiaEncoder"/> class.
-        /// </summary>
-        /// <param name="logger">The application logger.</param>
-        /// <param name="appPaths">The application paths.</param>
-        public SkiaEncoder(ILogger<SkiaEncoder> logger, IApplicationPaths appPaths)
-        {
-            _logger = logger;
-            _appPaths = appPaths;
-        }
+    /// <inheritdoc/>
+    public bool SupportsImageCollageCreation => true;
 
-        /// <inheritdoc/>
-        public string Name => "Skia";
+    /// <inheritdoc/>
+    public bool SupportsImageEncoding => true;
 
-        /// <inheritdoc/>
-        public bool SupportsImageCollageCreation => true;
-
-        /// <inheritdoc/>
-        public bool SupportsImageEncoding => true;
+    /// <inheritdoc/>
+    public IReadOnlyCollection<string> SupportedInputFormats =>
+        new HashSet<string>(StringComparer.OrdinalIgnoreCase)
+        {
+            "jpeg",
+            "jpg",
+            "png",
+            "dng",
+            "webp",
+            "gif",
+            "bmp",
+            "ico",
+            "astc",
+            "ktx",
+            "pkm",
+            "wbmp",
+            // TODO: check if these are supported on multiple platforms
+            // https://github.com/google/skia/blob/master/infra/bots/recipes/test.py#L454
+            // working on windows at least
+            "cr2",
+            "nef",
+            "arw"
+        };
+
+    /// <inheritdoc/>
+    public IReadOnlyCollection<ImageFormat> SupportedOutputFormats
+        => new HashSet<ImageFormat> { ImageFormat.Webp, ImageFormat.Jpg, ImageFormat.Png };
 
-        /// <inheritdoc/>
-        public IReadOnlyCollection<string> SupportedInputFormats =>
-            new HashSet<string>(StringComparer.OrdinalIgnoreCase)
-            {
-                "jpeg",
-                "jpg",
-                "png",
-                "dng",
-                "webp",
-                "gif",
-                "bmp",
-                "ico",
-                "astc",
-                "ktx",
-                "pkm",
-                "wbmp",
-                // TODO: check if these are supported on multiple platforms
-                // https://github.com/google/skia/blob/master/infra/bots/recipes/test.py#L454
-                // working on windows at least
-                "cr2",
-                "nef",
-                "arw"
-            };
-
-        /// <inheritdoc/>
-        public IReadOnlyCollection<ImageFormat> SupportedOutputFormats
-            => new HashSet<ImageFormat> { ImageFormat.Webp, ImageFormat.Jpg, ImageFormat.Png };
-
-        /// <summary>
-        /// Check if the native lib is available.
-        /// </summary>
-        /// <returns>True if the native lib is available, otherwise false.</returns>
-        public static bool IsNativeLibAvailable()
-        {
-            try
-            {
-                // test an operation that requires the native library
-                SKPMColor.PreMultiply(SKColors.Black);
-                return true;
-            }
-            catch (Exception)
-            {
-                return false;
-            }
+    /// <summary>
+    /// Check if the native lib is available.
+    /// </summary>
+    /// <returns>True if the native lib is available, otherwise false.</returns>
+    public static bool IsNativeLibAvailable()
+    {
+        try
+        {
+            // test an operation that requires the native library
+            SKPMColor.PreMultiply(SKColors.Black);
+            return true;
         }
-
-        /// <summary>
-        /// Convert a <see cref="ImageFormat"/> to a <see cref="SKEncodedImageFormat"/>.
-        /// </summary>
-        /// <param name="selectedFormat">The format to convert.</param>
-        /// <returns>The converted format.</returns>
-        public static SKEncodedImageFormat GetImageFormat(ImageFormat selectedFormat)
+        catch (Exception)
         {
-            return selectedFormat switch
-            {
-                ImageFormat.Bmp => SKEncodedImageFormat.Bmp,
-                ImageFormat.Jpg => SKEncodedImageFormat.Jpeg,
-                ImageFormat.Gif => SKEncodedImageFormat.Gif,
-                ImageFormat.Webp => SKEncodedImageFormat.Webp,
-                _ => SKEncodedImageFormat.Png
-            };
+            return false;
         }
+    }
 
-        /// <inheritdoc />
-        /// <exception cref="FileNotFoundException">The path is not valid.</exception>
-        public ImageDimensions GetImageSize(string path)
+    /// <summary>
+    /// Convert a <see cref="ImageFormat"/> to a <see cref="SKEncodedImageFormat"/>.
+    /// </summary>
+    /// <param name="selectedFormat">The format to convert.</param>
+    /// <returns>The converted format.</returns>
+    public static SKEncodedImageFormat GetImageFormat(ImageFormat selectedFormat)
+    {
+        return selectedFormat switch
         {
-            if (!File.Exists(path))
-            {
-                throw new FileNotFoundException("File not found", path);
-            }
+            ImageFormat.Bmp => SKEncodedImageFormat.Bmp,
+            ImageFormat.Jpg => SKEncodedImageFormat.Jpeg,
+            ImageFormat.Gif => SKEncodedImageFormat.Gif,
+            ImageFormat.Webp => SKEncodedImageFormat.Webp,
+            _ => SKEncodedImageFormat.Png
+        };
+    }
 
-            var extension = Path.GetExtension(path.AsSpan());
-            if (extension.Equals(".svg", StringComparison.OrdinalIgnoreCase))
-            {
-                var svg = new SKSvg();
-                svg.Load(path);
-                return new ImageDimensions(Convert.ToInt32(svg.Picture.CullRect.Width), Convert.ToInt32(svg.Picture.CullRect.Height));
-            }
+    /// <inheritdoc />
+    /// <exception cref="FileNotFoundException">The path is not valid.</exception>
+    public ImageDimensions GetImageSize(string path)
+    {
+        if (!File.Exists(path))
+        {
+            throw new FileNotFoundException("File not found", path);
+        }
 
-            using var codec = SKCodec.Create(path, out SKCodecResult result);
-            switch (result)
-            {
-                case SKCodecResult.Success:
-                    var info = codec.Info;
-                    return new ImageDimensions(info.Width, info.Height);
-                case SKCodecResult.Unimplemented:
-                    _logger.LogDebug("Image format not supported: {FilePath}", path);
-                    return new ImageDimensions(0, 0);
-                default:
-                    _logger.LogError("Unable to determine image dimensions for {FilePath}: {SkCodecResult}", path, result);
-                    return new ImageDimensions(0, 0);
-            }
+        var extension = Path.GetExtension(path.AsSpan());
+        if (extension.Equals(".svg", StringComparison.OrdinalIgnoreCase))
+        {
+            var svg = new SKSvg();
+            svg.Load(path);
+            return new ImageDimensions(Convert.ToInt32(svg.Picture.CullRect.Width), Convert.ToInt32(svg.Picture.CullRect.Height));
         }
 
-        /// <inheritdoc />
-        /// <exception cref="ArgumentNullException">The path is null.</exception>
-        /// <exception cref="FileNotFoundException">The path is not valid.</exception>
-        /// <exception cref="SkiaCodecException">The file at the specified path could not be used to generate a codec.</exception>
-        public string GetImageBlurHash(int xComp, int yComp, string path)
+        using var codec = SKCodec.Create(path, out SKCodecResult result);
+        switch (result)
         {
-            ArgumentException.ThrowIfNullOrEmpty(path);
+            case SKCodecResult.Success:
+                var info = codec.Info;
+                return new ImageDimensions(info.Width, info.Height);
+            case SKCodecResult.Unimplemented:
+                _logger.LogDebug("Image format not supported: {FilePath}", path);
+                return new ImageDimensions(0, 0);
+            default:
+                _logger.LogError("Unable to determine image dimensions for {FilePath}: {SkCodecResult}", path, result);
+                return new ImageDimensions(0, 0);
+        }
+    }
 
-            var extension = Path.GetExtension(path.AsSpan()).TrimStart('.');
-            if (!SupportedInputFormats.Contains(extension, StringComparison.OrdinalIgnoreCase))
-            {
-                _logger.LogDebug("Unable to compute blur hash due to unsupported format: {ImagePath}", path);
-                return string.Empty;
-            }
+    /// <inheritdoc />
+    /// <exception cref="ArgumentNullException">The path is null.</exception>
+    /// <exception cref="FileNotFoundException">The path is not valid.</exception>
+    /// <exception cref="SkiaCodecException">The file at the specified path could not be used to generate a codec.</exception>
+    public string GetImageBlurHash(int xComp, int yComp, string path)
+    {
+        ArgumentException.ThrowIfNullOrEmpty(path);
 
-            // Any larger than 128x128 is too slow and there's no visually discernible difference
-            return BlurHashEncoder.Encode(xComp, yComp, path, 128, 128);
+        var extension = Path.GetExtension(path.AsSpan()).TrimStart('.');
+        if (!SupportedInputFormats.Contains(extension, StringComparison.OrdinalIgnoreCase))
+        {
+            _logger.LogDebug("Unable to compute blur hash due to unsupported format: {ImagePath}", path);
+            return string.Empty;
         }
 
-        private bool RequiresSpecialCharacterHack(string path)
+        // Any larger than 128x128 is too slow and there's no visually discernible difference
+        return BlurHashEncoder.Encode(xComp, yComp, path, 128, 128);
+    }
+
+    private bool RequiresSpecialCharacterHack(string path)
+    {
+        for (int i = 0; i < path.Length; i++)
         {
-            for (int i = 0; i < path.Length; i++)
+            if (char.GetUnicodeCategory(path[i]) == UnicodeCategory.OtherLetter)
             {
-                if (char.GetUnicodeCategory(path[i]) == UnicodeCategory.OtherLetter)
-                {
-                    return true;
-                }
+                return true;
             }
-
-            return path.HasDiacritics();
         }
 
-        private string NormalizePath(string path)
+        return path.HasDiacritics();
+    }
+
+    private string NormalizePath(string path)
+    {
+        if (!RequiresSpecialCharacterHack(path))
         {
-            if (!RequiresSpecialCharacterHack(path))
-            {
-                return path;
-            }
+            return path;
+        }
+
+        var tempPath = Path.Combine(_appPaths.TempDirectory, Guid.NewGuid() + Path.GetExtension(path));
+        var directory = Path.GetDirectoryName(tempPath) ?? throw new ResourceNotFoundException($"Provided path ({tempPath}) is not valid.");
+        Directory.CreateDirectory(directory);
+        File.Copy(path, tempPath, true);
 
-            var tempPath = Path.Combine(_appPaths.TempDirectory, Guid.NewGuid() + Path.GetExtension(path));
-            var directory = Path.GetDirectoryName(tempPath) ?? throw new ResourceNotFoundException($"Provided path ({tempPath}) is not valid.");
-            Directory.CreateDirectory(directory);
-            File.Copy(path, tempPath, true);
+        return tempPath;
+    }
 
-            return tempPath;
+    private static SKEncodedOrigin GetSKEncodedOrigin(ImageOrientation? orientation)
+    {
+        if (!orientation.HasValue)
+        {
+            return SKEncodedOrigin.TopLeft;
         }
 
-        private static SKEncodedOrigin GetSKEncodedOrigin(ImageOrientation? orientation)
+        return orientation.Value switch
         {
-            if (!orientation.HasValue)
-            {
-                return SKEncodedOrigin.TopLeft;
-            }
+            ImageOrientation.TopRight => SKEncodedOrigin.TopRight,
+            ImageOrientation.RightTop => SKEncodedOrigin.RightTop,
+            ImageOrientation.RightBottom => SKEncodedOrigin.RightBottom,
+            ImageOrientation.LeftTop => SKEncodedOrigin.LeftTop,
+            ImageOrientation.LeftBottom => SKEncodedOrigin.LeftBottom,
+            ImageOrientation.BottomRight => SKEncodedOrigin.BottomRight,
+            ImageOrientation.BottomLeft => SKEncodedOrigin.BottomLeft,
+            _ => SKEncodedOrigin.TopLeft
+        };
+    }
 
-            return orientation.Value switch
-            {
-                ImageOrientation.TopRight => SKEncodedOrigin.TopRight,
-                ImageOrientation.RightTop => SKEncodedOrigin.RightTop,
-                ImageOrientation.RightBottom => SKEncodedOrigin.RightBottom,
-                ImageOrientation.LeftTop => SKEncodedOrigin.LeftTop,
-                ImageOrientation.LeftBottom => SKEncodedOrigin.LeftBottom,
-                ImageOrientation.BottomRight => SKEncodedOrigin.BottomRight,
-                ImageOrientation.BottomLeft => SKEncodedOrigin.BottomLeft,
-                _ => SKEncodedOrigin.TopLeft
-            };
+    /// <summary>
+    /// Decode an image.
+    /// </summary>
+    /// <param name="path">The filepath of the image to decode.</param>
+    /// <param name="forceCleanBitmap">Whether to force clean the bitmap.</param>
+    /// <param name="orientation">The orientation of the image.</param>
+    /// <param name="origin">The detected origin of the image.</param>
+    /// <returns>The resulting bitmap of the image.</returns>
+    internal SKBitmap? Decode(string path, bool forceCleanBitmap, ImageOrientation? orientation, out SKEncodedOrigin origin)
+    {
+        if (!File.Exists(path))
+        {
+            throw new FileNotFoundException("File not found", path);
         }
 
-        /// <summary>
-        /// Decode an image.
-        /// </summary>
-        /// <param name="path">The filepath of the image to decode.</param>
-        /// <param name="forceCleanBitmap">Whether to force clean the bitmap.</param>
-        /// <param name="orientation">The orientation of the image.</param>
-        /// <param name="origin">The detected origin of the image.</param>
-        /// <returns>The resulting bitmap of the image.</returns>
-        internal SKBitmap? Decode(string path, bool forceCleanBitmap, ImageOrientation? orientation, out SKEncodedOrigin origin)
-        {
-            if (!File.Exists(path))
+        var requiresTransparencyHack = _transparentImageTypes.Contains(Path.GetExtension(path));
+
+        if (requiresTransparencyHack || forceCleanBitmap)
+        {
+            using SKCodec codec = SKCodec.Create(NormalizePath(path), out SKCodecResult res);
+            if (res != SKCodecResult.Success)
             {
-                throw new FileNotFoundException("File not found", path);
+                origin = GetSKEncodedOrigin(orientation);
+                return null;
             }
 
-            var requiresTransparencyHack = _transparentImageTypes.Contains(Path.GetExtension(path));
-
-            if (requiresTransparencyHack || forceCleanBitmap)
-            {
-                using SKCodec codec = SKCodec.Create(NormalizePath(path), out SKCodecResult res);
-                if (res != SKCodecResult.Success)
-                {
-                    origin = GetSKEncodedOrigin(orientation);
-                    return null;
-                }
+            // create the bitmap
+            var bitmap = new SKBitmap(codec.Info.Width, codec.Info.Height, !requiresTransparencyHack);
 
-                // create the bitmap
-                var bitmap = new SKBitmap(codec.Info.Width, codec.Info.Height, !requiresTransparencyHack);
+            // decode
+            _ = codec.GetPixels(bitmap.Info, bitmap.GetPixels());
 
-                // decode
-                _ = codec.GetPixels(bitmap.Info, bitmap.GetPixels());
+            origin = codec.EncodedOrigin;
 
-                origin = codec.EncodedOrigin;
+            return bitmap;
+        }
 
-                return bitmap;
-            }
+        var resultBitmap = SKBitmap.Decode(NormalizePath(path));
 
-            var resultBitmap = SKBitmap.Decode(NormalizePath(path));
+        if (resultBitmap is null)
+        {
+            return Decode(path, true, orientation, out origin);
+        }
 
-            if (resultBitmap is null)
+        // If we have to resize these they often end up distorted
+        if (resultBitmap.ColorType == SKColorType.Gray8)
+        {
+            using (resultBitmap)
             {
                 return Decode(path, true, orientation, out origin);
             }
+        }
+
+        origin = SKEncodedOrigin.TopLeft;
+        return resultBitmap;
+    }
+
+    private SKBitmap? GetBitmap(string path, bool autoOrient, ImageOrientation? orientation)
+    {
+        if (autoOrient)
+        {
+            var bitmap = Decode(path, true, orientation, out var origin);
 
-            // If we have to resize these they often end up distorted
-            if (resultBitmap.ColorType == SKColorType.Gray8)
+            if (bitmap is not null && origin != SKEncodedOrigin.TopLeft)
             {
-                using (resultBitmap)
+                using (bitmap)
                 {
-                    return Decode(path, true, orientation, out origin);
+                    return OrientImage(bitmap, origin);
                 }
             }
 
-            origin = SKEncodedOrigin.TopLeft;
-            return resultBitmap;
+            return bitmap;
         }
 
-        private SKBitmap? GetBitmap(string path, bool autoOrient, ImageOrientation? orientation)
-        {
-            if (autoOrient)
-            {
-                var bitmap = Decode(path, true, orientation, out var origin);
+        return Decode(path, false, orientation, out _);
+    }
 
-                if (bitmap is not null && origin != SKEncodedOrigin.TopLeft)
-                {
-                    using (bitmap)
-                    {
-                        return OrientImage(bitmap, origin);
-                    }
-                }
+    private SKBitmap OrientImage(SKBitmap bitmap, SKEncodedOrigin origin)
+    {
+        var needsFlip = origin == SKEncodedOrigin.LeftBottom
+                        || origin == SKEncodedOrigin.LeftTop
+                        || origin == SKEncodedOrigin.RightBottom
+                        || origin == SKEncodedOrigin.RightTop;
+        var rotated = needsFlip
+            ? new SKBitmap(bitmap.Height, bitmap.Width)
+            : new SKBitmap(bitmap.Width, bitmap.Height);
+        using var surface = new SKCanvas(rotated);
+        var midX = (float)rotated.Width / 2;
+        var midY = (float)rotated.Height / 2;
+
+        switch (origin)
+        {
+            case SKEncodedOrigin.TopRight:
+                surface.Scale(-1, 1, midX, midY);
+                break;
+            case SKEncodedOrigin.BottomRight:
+                surface.RotateDegrees(180, midX, midY);
+                break;
+            case SKEncodedOrigin.BottomLeft:
+                surface.Scale(1, -1, midX, midY);
+                break;
+            case SKEncodedOrigin.LeftTop:
+                surface.Translate(0, -rotated.Height);
+                surface.Scale(1, -1, midX, midY);
+                surface.RotateDegrees(-90);
+                break;
+            case SKEncodedOrigin.RightTop:
+                surface.Translate(rotated.Width, 0);
+                surface.RotateDegrees(90);
+                break;
+            case SKEncodedOrigin.RightBottom:
+                surface.Translate(rotated.Width, 0);
+                surface.Scale(1, -1, midX, midY);
+                surface.RotateDegrees(90);
+                break;
+            case SKEncodedOrigin.LeftBottom:
+                surface.Translate(0, rotated.Height);
+                surface.RotateDegrees(-90);
+                break;
+        }
 
-                return bitmap;
-            }
+        surface.DrawBitmap(bitmap, 0, 0);
+        return rotated;
+    }
 
-            return Decode(path, false, orientation, out _);
-        }
+    /// <summary>
+    /// 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).
+    /// </summary>
+    /// <param name="source">The source bitmap.</param>
+    /// <param name="targetInfo">This specifies the target size and other information required to create the surface.</param>
+    /// <param name="isAntialias">This enables anti-aliasing on the SKPaint instance.</param>
+    /// <param name="isDither">This enables dithering on the SKPaint instance.</param>
+    /// <returns>The resized image.</returns>
+    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
+        {
+            FilterQuality = SKFilterQuality.High,
+            IsAntialias = isAntialias,
+            IsDither = isDither
+        };
 
-        private SKBitmap OrientImage(SKBitmap bitmap, SKEncodedOrigin origin)
+        var kernel = new float[9]
         {
-            var needsFlip = origin == SKEncodedOrigin.LeftBottom
-                            || origin == SKEncodedOrigin.LeftTop
-                            || origin == SKEncodedOrigin.RightBottom
-                            || origin == SKEncodedOrigin.RightTop;
-            var rotated = needsFlip
-                ? new SKBitmap(bitmap.Height, bitmap.Width)
-                : new SKBitmap(bitmap.Width, bitmap.Height);
-            using var surface = new SKCanvas(rotated);
-            var midX = (float)rotated.Width / 2;
-            var midY = (float)rotated.Height / 2;
+            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,
+            true);
+
+        canvas.DrawBitmap(
+            source,
+            SKRect.Create(0, 0, source.Width, source.Height),
+            SKRect.Create(0, 0, targetInfo.Width, targetInfo.Height),
+            paint);
+
+        return surface.Snapshot();
+    }
 
-            switch (origin)
-            {
-                case SKEncodedOrigin.TopRight:
-                    surface.Scale(-1, 1, midX, midY);
-                    break;
-                case SKEncodedOrigin.BottomRight:
-                    surface.RotateDegrees(180, midX, midY);
-                    break;
-                case SKEncodedOrigin.BottomLeft:
-                    surface.Scale(1, -1, midX, midY);
-                    break;
-                case SKEncodedOrigin.LeftTop:
-                    surface.Translate(0, -rotated.Height);
-                    surface.Scale(1, -1, midX, midY);
-                    surface.RotateDegrees(-90);
-                    break;
-                case SKEncodedOrigin.RightTop:
-                    surface.Translate(rotated.Width, 0);
-                    surface.RotateDegrees(90);
-                    break;
-                case SKEncodedOrigin.RightBottom:
-                    surface.Translate(rotated.Width, 0);
-                    surface.Scale(1, -1, midX, midY);
-                    surface.RotateDegrees(90);
-                    break;
-                case SKEncodedOrigin.LeftBottom:
-                    surface.Translate(0, rotated.Height);
-                    surface.RotateDegrees(-90);
-                    break;
-            }
+    /// <inheritdoc/>
+    public string EncodeImage(string inputPath, DateTime dateModified, string outputPath, bool autoOrient, ImageOrientation? orientation, int quality, ImageProcessingOptions options, ImageFormat outputFormat)
+    {
+        ArgumentException.ThrowIfNullOrEmpty(inputPath);
+        ArgumentException.ThrowIfNullOrEmpty(outputPath);
 
-            surface.DrawBitmap(bitmap, 0, 0);
-            return rotated;
+        var inputFormat = Path.GetExtension(inputPath.AsSpan()).TrimStart('.');
+        if (!SupportedInputFormats.Contains(inputFormat, StringComparison.OrdinalIgnoreCase))
+        {
+            _logger.LogDebug("Unable to encode image due to unsupported format: {ImagePath}", inputPath);
+            return inputPath;
         }
 
-        /// <summary>
-        /// 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).
-        /// </summary>
-        /// <param name="source">The source bitmap.</param>
-        /// <param name="targetInfo">This specifies the target size and other information required to create the surface.</param>
-        /// <param name="isAntialias">This enables anti-aliasing on the SKPaint instance.</param>
-        /// <param name="isDither">This enables dithering on the SKPaint instance.</param>
-        /// <returns>The resized image.</returns>
-        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
-            {
-                FilterQuality = SKFilterQuality.High,
-                IsAntialias = isAntialias,
-                IsDither = isDither
-            };
+        var skiaOutputFormat = GetImageFormat(outputFormat);
 
-            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,
-                true);
-
-            canvas.DrawBitmap(
-                source,
-                SKRect.Create(0, 0, source.Width, source.Height),
-                SKRect.Create(0, 0, targetInfo.Width, targetInfo.Height),
-                paint);
-
-            return surface.Snapshot();
-        }
+        var hasBackgroundColor = !string.IsNullOrWhiteSpace(options.BackgroundColor);
+        var hasForegroundColor = !string.IsNullOrWhiteSpace(options.ForegroundLayer);
+        var blur = options.Blur ?? 0;
+        var hasIndicator = options.AddPlayedIndicator || options.UnplayedCount.HasValue || !options.PercentPlayed.Equals(0);
 
-        /// <inheritdoc/>
-        public string EncodeImage(string inputPath, DateTime dateModified, string outputPath, bool autoOrient, ImageOrientation? orientation, int quality, ImageProcessingOptions options, ImageFormat outputFormat)
+        using var bitmap = GetBitmap(inputPath, autoOrient, orientation);
+        if (bitmap is null)
         {
-            ArgumentException.ThrowIfNullOrEmpty(inputPath);
-            ArgumentException.ThrowIfNullOrEmpty(outputPath);
-
-            var inputFormat = Path.GetExtension(inputPath.AsSpan()).TrimStart('.');
-            if (!SupportedInputFormats.Contains(inputFormat, StringComparison.OrdinalIgnoreCase))
-            {
-                _logger.LogDebug("Unable to encode image due to unsupported format: {ImagePath}", inputPath);
-                return inputPath;
-            }
+            throw new InvalidDataException($"Skia unable to read image {inputPath}");
+        }
 
-            var skiaOutputFormat = GetImageFormat(outputFormat);
+        var originalImageSize = new ImageDimensions(bitmap.Width, bitmap.Height);
 
-            var hasBackgroundColor = !string.IsNullOrWhiteSpace(options.BackgroundColor);
-            var hasForegroundColor = !string.IsNullOrWhiteSpace(options.ForegroundLayer);
-            var blur = options.Blur ?? 0;
-            var hasIndicator = options.AddPlayedIndicator || options.UnplayedCount.HasValue || !options.PercentPlayed.Equals(0);
+        if (options.HasDefaultOptions(inputPath, originalImageSize) && !autoOrient)
+        {
+            // Just spit out the original file if all the options are default
+            return inputPath;
+        }
 
-            using var bitmap = GetBitmap(inputPath, autoOrient, orientation);
-            if (bitmap is null)
-            {
-                throw new InvalidDataException($"Skia unable to read image {inputPath}");
-            }
+        var newImageSize = ImageHelper.GetNewImageSize(options, originalImageSize);
 
-            var originalImageSize = new ImageDimensions(bitmap.Width, bitmap.Height);
+        var width = newImageSize.Width;
+        var height = newImageSize.Height;
 
-            if (options.HasDefaultOptions(inputPath, originalImageSize) && !autoOrient)
-            {
-                // Just spit out the original file if all the options are default
-                return inputPath;
-            }
+        // scale image (the FromImage creates a copy)
+        var imageInfo = new SKImageInfo(width, height, bitmap.ColorType, bitmap.AlphaType, bitmap.ColorSpace);
+        using var resizedBitmap = SKBitmap.FromImage(ResizeImage(bitmap, imageInfo));
 
-            var newImageSize = ImageHelper.GetNewImageSize(options, originalImageSize);
+        // If all we're doing is resizing then we can stop now
+        if (!hasBackgroundColor && !hasForegroundColor && blur == 0 && !hasIndicator)
+        {
+            var outputDirectory = Path.GetDirectoryName(outputPath) ?? throw new ArgumentException($"Provided path ({outputPath}) is not valid.", nameof(outputPath));
+            Directory.CreateDirectory(outputDirectory);
+            using var outputStream = new SKFileWStream(outputPath);
+            using var pixmap = new SKPixmap(new SKImageInfo(width, height), resizedBitmap.GetPixels());
+            resizedBitmap.Encode(outputStream, skiaOutputFormat, quality);
+            return outputPath;
+        }
 
-            var width = newImageSize.Width;
-            var height = newImageSize.Height;
+        // create bitmap to use for canvas drawing used to draw into bitmap
+        using var saveBitmap = new SKBitmap(width, height);
+        using var canvas = new SKCanvas(saveBitmap);
+        // set background color if present
+        if (hasBackgroundColor)
+        {
+            canvas.Clear(SKColor.Parse(options.BackgroundColor));
+        }
 
-            // scale image (the FromImage creates a copy)
-            var imageInfo = new SKImageInfo(width, height, bitmap.ColorType, bitmap.AlphaType, bitmap.ColorSpace);
-            using var resizedBitmap = SKBitmap.FromImage(ResizeImage(bitmap, imageInfo));
+        // Add blur if option is present
+        if (blur > 0)
+        {
+            // create image from resized bitmap to apply blur
+            using var paint = new SKPaint();
+            using var filter = SKImageFilter.CreateBlur(blur, blur);
+            paint.ImageFilter = filter;
+            canvas.DrawBitmap(resizedBitmap, SKRect.Create(width, height), paint);
+        }
+        else
+        {
+            // draw resized bitmap onto canvas
+            canvas.DrawBitmap(resizedBitmap, SKRect.Create(width, height));
+        }
 
-            // If all we're doing is resizing then we can stop now
-            if (!hasBackgroundColor && !hasForegroundColor && blur == 0 && !hasIndicator)
+        // If foreground layer present then draw
+        if (hasForegroundColor)
+        {
+            if (!double.TryParse(options.ForegroundLayer, out double opacity))
             {
-                var outputDirectory = Path.GetDirectoryName(outputPath) ?? throw new ArgumentException($"Provided path ({outputPath}) is not valid.", nameof(outputPath));
-                Directory.CreateDirectory(outputDirectory);
-                using var outputStream = new SKFileWStream(outputPath);
-                using var pixmap = new SKPixmap(new SKImageInfo(width, height), resizedBitmap.GetPixels());
-                resizedBitmap.Encode(outputStream, skiaOutputFormat, quality);
-                return outputPath;
+                opacity = .4;
             }
 
-            // create bitmap to use for canvas drawing used to draw into bitmap
-            using var saveBitmap = new SKBitmap(width, height);
-            using var canvas = new SKCanvas(saveBitmap);
-            // set background color if present
-            if (hasBackgroundColor)
-            {
-                canvas.Clear(SKColor.Parse(options.BackgroundColor));
-            }
+            canvas.DrawColor(new SKColor(0, 0, 0, (byte)((1 - opacity) * 0xFF)), SKBlendMode.SrcOver);
+        }
 
-            // Add blur if option is present
-            if (blur > 0)
-            {
-                // create image from resized bitmap to apply blur
-                using var paint = new SKPaint();
-                using var filter = SKImageFilter.CreateBlur(blur, blur);
-                paint.ImageFilter = filter;
-                canvas.DrawBitmap(resizedBitmap, SKRect.Create(width, height), paint);
-            }
-            else
-            {
-                // draw resized bitmap onto canvas
-                canvas.DrawBitmap(resizedBitmap, SKRect.Create(width, height));
-            }
+        if (hasIndicator)
+        {
+            DrawIndicator(canvas, width, height, options);
+        }
 
-            // If foreground layer present then draw
-            if (hasForegroundColor)
+        var directory = Path.GetDirectoryName(outputPath) ?? throw new ArgumentException($"Provided path ({outputPath}) is not valid.", nameof(outputPath));
+        Directory.CreateDirectory(directory);
+        using (var outputStream = new SKFileWStream(outputPath))
+        {
+            using (var pixmap = new SKPixmap(new SKImageInfo(width, height), saveBitmap.GetPixels()))
             {
-                if (!double.TryParse(options.ForegroundLayer, out double opacity))
-                {
-                    opacity = .4;
-                }
-
-                canvas.DrawColor(new SKColor(0, 0, 0, (byte)((1 - opacity) * 0xFF)), SKBlendMode.SrcOver);
+                pixmap.Encode(outputStream, skiaOutputFormat, quality);
             }
+        }
 
-            if (hasIndicator)
-            {
-                DrawIndicator(canvas, width, height, options);
-            }
+        return outputPath;
+    }
 
-            var directory = Path.GetDirectoryName(outputPath) ?? throw new ArgumentException($"Provided path ({outputPath}) is not valid.", nameof(outputPath));
-            Directory.CreateDirectory(directory);
-            using (var outputStream = new SKFileWStream(outputPath))
-            {
-                using (var pixmap = new SKPixmap(new SKImageInfo(width, height), saveBitmap.GetPixels()))
-                {
-                    pixmap.Encode(outputStream, skiaOutputFormat, quality);
-                }
-            }
+    /// <inheritdoc/>
+    public void CreateImageCollage(ImageCollageOptions options, string? libraryName)
+    {
+        double ratio = (double)options.Width / options.Height;
 
-            return outputPath;
+        if (ratio >= 1.4)
+        {
+            new StripCollageBuilder(this).BuildThumbCollage(options.InputPaths, options.OutputPath, options.Width, options.Height, libraryName);
+        }
+        else if (ratio >= .9)
+        {
+            new StripCollageBuilder(this).BuildSquareCollage(options.InputPaths, options.OutputPath, options.Width, options.Height);
+        }
+        else
+        {
+            // TODO: Create Poster collage capability
+            new StripCollageBuilder(this).BuildSquareCollage(options.InputPaths, options.OutputPath, options.Width, options.Height);
         }
+    }
+
+    /// <inheritdoc />
+    public void CreateSplashscreen(IReadOnlyList<string> posters, IReadOnlyList<string> backdrops)
+    {
+        var splashBuilder = new SplashscreenBuilder(this);
+        var outputPath = Path.Combine(_appPaths.DataPath, "splashscreen.png");
+        splashBuilder.GenerateSplash(posters, backdrops, outputPath);
+    }
 
-        /// <inheritdoc/>
-        public void CreateImageCollage(ImageCollageOptions options, string? libraryName)
+    private void DrawIndicator(SKCanvas canvas, int imageWidth, int imageHeight, ImageProcessingOptions options)
+    {
+        try
         {
-            double ratio = (double)options.Width / options.Height;
+            var currentImageSize = new ImageDimensions(imageWidth, imageHeight);
 
-            if (ratio >= 1.4)
+            if (options.AddPlayedIndicator)
             {
-                new StripCollageBuilder(this).BuildThumbCollage(options.InputPaths, options.OutputPath, options.Width, options.Height, libraryName);
+                PlayedIndicatorDrawer.DrawPlayedIndicator(canvas, currentImageSize);
             }
-            else if (ratio >= .9)
+            else if (options.UnplayedCount.HasValue)
             {
-                new StripCollageBuilder(this).BuildSquareCollage(options.InputPaths, options.OutputPath, options.Width, options.Height);
+                UnplayedCountIndicator.DrawUnplayedCountIndicator(canvas, currentImageSize, options.UnplayedCount.Value);
             }
-            else
+
+            if (options.PercentPlayed > 0)
             {
-                // TODO: Create Poster collage capability
-                new StripCollageBuilder(this).BuildSquareCollage(options.InputPaths, options.OutputPath, options.Width, options.Height);
+                PercentPlayedDrawer.Process(canvas, currentImageSize, options.PercentPlayed);
             }
         }
-
-        /// <inheritdoc />
-        public void CreateSplashscreen(IReadOnlyList<string> posters, IReadOnlyList<string> backdrops)
+        catch (Exception ex)
         {
-            var splashBuilder = new SplashscreenBuilder(this);
-            var outputPath = Path.Combine(_appPaths.DataPath, "splashscreen.png");
-            splashBuilder.GenerateSplash(posters, backdrops, outputPath);
-        }
-
-        private void DrawIndicator(SKCanvas canvas, int imageWidth, int imageHeight, ImageProcessingOptions options)
-        {
-            try
-            {
-                var currentImageSize = new ImageDimensions(imageWidth, imageHeight);
-
-                if (options.AddPlayedIndicator)
-                {
-                    PlayedIndicatorDrawer.DrawPlayedIndicator(canvas, currentImageSize);
-                }
-                else if (options.UnplayedCount.HasValue)
-                {
-                    UnplayedCountIndicator.DrawUnplayedCountIndicator(canvas, currentImageSize, options.UnplayedCount.Value);
-                }
-
-                if (options.PercentPlayed > 0)
-                {
-                    PercentPlayedDrawer.Process(canvas, currentImageSize, options.PercentPlayed);
-                }
-            }
-            catch (Exception ex)
-            {
-                _logger.LogError(ex, "Error drawing indicator overlay");
-            }
+            _logger.LogError(ex, "Error drawing indicator overlay");
         }
     }
 }

+ 28 - 29
src/Jellyfin.Drawing.Skia/SkiaException.cs

@@ -1,39 +1,38 @@
 using System;
 
-namespace Jellyfin.Drawing.Skia
+namespace Jellyfin.Drawing.Skia;
+
+/// <summary>
+/// Represents errors that occur during interaction with Skia.
+/// </summary>
+public class SkiaException : Exception
 {
     /// <summary>
-    /// Represents errors that occur during interaction with Skia.
+    /// Initializes a new instance of the <see cref="SkiaException"/> class.
     /// </summary>
-    public class SkiaException : Exception
+    public SkiaException()
     {
-        /// <summary>
-        /// Initializes a new instance of the <see cref="SkiaException"/> class.
-        /// </summary>
-        public SkiaException()
-        {
-        }
+    }
 
-        /// <summary>
-        /// Initializes a new instance of the <see cref="SkiaException"/> class with a specified error message.
-        /// </summary>
-        /// <param name="message">The message that describes the error.</param>
-        public SkiaException(string message) : base(message)
-        {
-        }
+    /// <summary>
+    /// Initializes a new instance of the <see cref="SkiaException"/> class with a specified error message.
+    /// </summary>
+    /// <param name="message">The message that describes the error.</param>
+    public SkiaException(string message) : base(message)
+    {
+    }
 
-        /// <summary>
-        /// Initializes a new instance of the <see cref="SkiaException"/> class with a specified error message and a
-        /// reference to the inner exception that is the cause of this exception.
-        /// </summary>
-        /// <param name="message">The error message that explains the reason for the exception.</param>
-        /// <param name="innerException">
-        /// The exception that is the cause of the current exception, or a null reference (Nothing in Visual Basic) if
-        /// no inner exception is specified.
-        /// </param>
-        public SkiaException(string message, Exception innerException)
-            : base(message, innerException)
-        {
-        }
+    /// <summary>
+    /// Initializes a new instance of the <see cref="SkiaException"/> class with a specified error message and a
+    /// reference to the inner exception that is the cause of this exception.
+    /// </summary>
+    /// <param name="message">The error message that explains the reason for the exception.</param>
+    /// <param name="innerException">
+    /// The exception that is the cause of the current exception, or a null reference (Nothing in Visual Basic) if
+    /// no inner exception is specified.
+    /// </param>
+    public SkiaException(string message, Exception innerException)
+        : base(message, innerException)
+    {
     }
 }

+ 29 - 30
src/Jellyfin.Drawing.Skia/SkiaHelper.cs

@@ -1,47 +1,46 @@
 using System.Collections.Generic;
 using SkiaSharp;
 
-namespace Jellyfin.Drawing.Skia
+namespace Jellyfin.Drawing.Skia;
+
+/// <summary>
+/// Class containing helper methods for working with SkiaSharp.
+/// </summary>
+public static class SkiaHelper
 {
     /// <summary>
-    /// Class containing helper methods for working with SkiaSharp.
+    /// Gets the next valid image as a bitmap.
     /// </summary>
-    public static class SkiaHelper
+    /// <param name="skiaEncoder">The current skia encoder.</param>
+    /// <param name="paths">The list of image paths.</param>
+    /// <param name="currentIndex">The current checked index.</param>
+    /// <param name="newIndex">The new index.</param>
+    /// <returns>A valid bitmap, or null if no bitmap exists after <c>currentIndex</c>.</returns>
+    public static SKBitmap? GetNextValidImage(SkiaEncoder skiaEncoder, IReadOnlyList<string> paths, int currentIndex, out int newIndex)
     {
-        /// <summary>
-        /// Gets the next valid image as a bitmap.
-        /// </summary>
-        /// <param name="skiaEncoder">The current skia encoder.</param>
-        /// <param name="paths">The list of image paths.</param>
-        /// <param name="currentIndex">The current checked index.</param>
-        /// <param name="newIndex">The new index.</param>
-        /// <returns>A valid bitmap, or null if no bitmap exists after <c>currentIndex</c>.</returns>
-        public static SKBitmap? GetNextValidImage(SkiaEncoder skiaEncoder, IReadOnlyList<string> paths, int currentIndex, out int newIndex)
-        {
-            var imagesTested = new Dictionary<int, int>();
-            SKBitmap? bitmap = null;
+        var imagesTested = new Dictionary<int, int>();
+        SKBitmap? bitmap = null;
 
-            while (imagesTested.Count < paths.Count)
+        while (imagesTested.Count < paths.Count)
+        {
+            if (currentIndex >= paths.Count)
             {
-                if (currentIndex >= paths.Count)
-                {
-                    currentIndex = 0;
-                }
+                currentIndex = 0;
+            }
 
-                bitmap = skiaEncoder.Decode(paths[currentIndex], false, null, out _);
+            bitmap = skiaEncoder.Decode(paths[currentIndex], false, null, out _);
 
-                imagesTested[currentIndex] = 0;
+            imagesTested[currentIndex] = 0;
 
-                currentIndex++;
+            currentIndex++;
 
-                if (bitmap is not null)
-                {
-                    break;
-                }
+            if (bitmap is not null)
+            {
+                break;
             }
-
-            newIndex = currentIndex;
-            return bitmap;
         }
+
+        newIndex = currentIndex;
+        return bitmap;
     }
 }

+ 123 - 124
src/Jellyfin.Drawing.Skia/SplashscreenBuilder.cs

@@ -2,147 +2,146 @@ using System;
 using System.Collections.Generic;
 using SkiaSharp;
 
-namespace Jellyfin.Drawing.Skia
+namespace Jellyfin.Drawing.Skia;
+
+/// <summary>
+/// Used to build the splashscreen.
+/// </summary>
+public class SplashscreenBuilder
 {
+    private const int FinalWidth = 1920;
+    private const int FinalHeight = 1080;
+    // generated collage resolution should be higher than the final resolution
+    private const int WallWidth = FinalWidth * 3;
+    private const int WallHeight = FinalHeight * 2;
+    private const int Rows = 6;
+    private const int Spacing = 20;
+
+    private readonly SkiaEncoder _skiaEncoder;
+
     /// <summary>
-    /// Used to build the splashscreen.
+    /// Initializes a new instance of the <see cref="SplashscreenBuilder"/> class.
     /// </summary>
-    public class SplashscreenBuilder
+    /// <param name="skiaEncoder">The SkiaEncoder.</param>
+    public SplashscreenBuilder(SkiaEncoder skiaEncoder)
     {
-        private const int FinalWidth = 1920;
-        private const int FinalHeight = 1080;
-        // generated collage resolution should be higher than the final resolution
-        private const int WallWidth = FinalWidth * 3;
-        private const int WallHeight = FinalHeight * 2;
-        private const int Rows = 6;
-        private const int Spacing = 20;
-
-        private readonly SkiaEncoder _skiaEncoder;
-
-        /// <summary>
-        /// Initializes a new instance of the <see cref="SplashscreenBuilder"/> class.
-        /// </summary>
-        /// <param name="skiaEncoder">The SkiaEncoder.</param>
-        public SplashscreenBuilder(SkiaEncoder skiaEncoder)
-        {
-            _skiaEncoder = skiaEncoder;
-        }
+        _skiaEncoder = skiaEncoder;
+    }
 
-        /// <summary>
-        /// Generate a splashscreen.
-        /// </summary>
-        /// <param name="posters">The poster paths.</param>
-        /// <param name="backdrops">The landscape paths.</param>
-        /// <param name="outputPath">The output path.</param>
-        public void GenerateSplash(IReadOnlyList<string> posters, IReadOnlyList<string> backdrops, string outputPath)
-        {
-            using var wall = GenerateCollage(posters, backdrops);
-            using var transformed = Transform3D(wall);
+    /// <summary>
+    /// Generate a splashscreen.
+    /// </summary>
+    /// <param name="posters">The poster paths.</param>
+    /// <param name="backdrops">The landscape paths.</param>
+    /// <param name="outputPath">The output path.</param>
+    public void GenerateSplash(IReadOnlyList<string> posters, IReadOnlyList<string> backdrops, string outputPath)
+    {
+        using var wall = GenerateCollage(posters, backdrops);
+        using var transformed = Transform3D(wall);
 
-            using var outputStream = new SKFileWStream(outputPath);
-            using var pixmap = new SKPixmap(new SKImageInfo(FinalWidth, FinalHeight), transformed.GetPixels());
-            pixmap.Encode(outputStream, StripCollageBuilder.GetEncodedFormat(outputPath), 90);
-        }
+        using var outputStream = new SKFileWStream(outputPath);
+        using var pixmap = new SKPixmap(new SKImageInfo(FinalWidth, FinalHeight), transformed.GetPixels());
+        pixmap.Encode(outputStream, StripCollageBuilder.GetEncodedFormat(outputPath), 90);
+    }
 
-        /// <summary>
-        /// Generates a collage of posters and landscape pictures.
-        /// </summary>
-        /// <param name="posters">The poster paths.</param>
-        /// <param name="backdrops">The landscape paths.</param>
-        /// <returns>The created collage as a bitmap.</returns>
-        private SKBitmap GenerateCollage(IReadOnlyList<string> posters, IReadOnlyList<string> backdrops)
-        {
-            var posterIndex = 0;
-            var backdropIndex = 0;
+    /// <summary>
+    /// Generates a collage of posters and landscape pictures.
+    /// </summary>
+    /// <param name="posters">The poster paths.</param>
+    /// <param name="backdrops">The landscape paths.</param>
+    /// <returns>The created collage as a bitmap.</returns>
+    private SKBitmap GenerateCollage(IReadOnlyList<string> posters, IReadOnlyList<string> backdrops)
+    {
+        var posterIndex = 0;
+        var backdropIndex = 0;
+
+        var bitmap = new SKBitmap(WallWidth, WallHeight);
+        using var canvas = new SKCanvas(bitmap);
+        canvas.Clear(SKColors.Black);
 
-            var bitmap = new SKBitmap(WallWidth, WallHeight);
-            using var canvas = new SKCanvas(bitmap);
-            canvas.Clear(SKColors.Black);
+        int posterHeight = WallHeight / 6;
 
-            int posterHeight = WallHeight / 6;
+        for (int i = 0; i < Rows; i++)
+        {
+            int imageCounter = Random.Shared.Next(0, 5);
+            int currentWidthPos = i * 75;
+            int currentHeight = i * (posterHeight + Spacing);
 
-            for (int i = 0; i < Rows; i++)
+            while (currentWidthPos < WallWidth)
             {
-                int imageCounter = Random.Shared.Next(0, 5);
-                int currentWidthPos = i * 75;
-                int currentHeight = i * (posterHeight + Spacing);
+                SKBitmap? currentImage;
 
-                while (currentWidthPos < WallWidth)
+                switch (imageCounter)
                 {
-                    SKBitmap? currentImage;
-
-                    switch (imageCounter)
-                    {
-                        case 0:
-                        case 2:
-                        case 3:
-                            currentImage = SkiaHelper.GetNextValidImage(_skiaEncoder, posters, posterIndex, out int newPosterIndex);
-                            posterIndex = newPosterIndex;
-                            break;
-                        default:
-                            currentImage = SkiaHelper.GetNextValidImage(_skiaEncoder, backdrops, backdropIndex, out int newBackdropIndex);
-                            backdropIndex = newBackdropIndex;
-                            break;
-                    }
-
-                    if (currentImage is null)
-                    {
-                        throw new ArgumentException("Not enough valid pictures provided to create a splashscreen!");
-                    }
-
-                    // resize to the same aspect as the original
-                    var imageWidth = Math.Abs(posterHeight * currentImage.Width / currentImage.Height);
-                    using var resizedBitmap = new SKBitmap(imageWidth, posterHeight);
-                    currentImage.ScalePixels(resizedBitmap, SKFilterQuality.High);
-
-                    // draw on canvas
-                    canvas.DrawBitmap(resizedBitmap, currentWidthPos, currentHeight);
-
-                    currentWidthPos += imageWidth + Spacing;
-
-                    currentImage.Dispose();
-
-                    if (imageCounter >= 4)
-                    {
-                        imageCounter = 0;
-                    }
-                    else
-                    {
-                        imageCounter++;
-                    }
+                    case 0:
+                    case 2:
+                    case 3:
+                        currentImage = SkiaHelper.GetNextValidImage(_skiaEncoder, posters, posterIndex, out int newPosterIndex);
+                        posterIndex = newPosterIndex;
+                        break;
+                    default:
+                        currentImage = SkiaHelper.GetNextValidImage(_skiaEncoder, backdrops, backdropIndex, out int newBackdropIndex);
+                        backdropIndex = newBackdropIndex;
+                        break;
                 }
-            }
 
-            return bitmap;
+                if (currentImage is null)
+                {
+                    throw new ArgumentException("Not enough valid pictures provided to create a splashscreen!");
+                }
+
+                // resize to the same aspect as the original
+                var imageWidth = Math.Abs(posterHeight * currentImage.Width / currentImage.Height);
+                using var resizedBitmap = new SKBitmap(imageWidth, posterHeight);
+                currentImage.ScalePixels(resizedBitmap, SKFilterQuality.High);
+
+                // draw on canvas
+                canvas.DrawBitmap(resizedBitmap, currentWidthPos, currentHeight);
+
+                currentWidthPos += imageWidth + Spacing;
+
+                currentImage.Dispose();
+
+                if (imageCounter >= 4)
+                {
+                    imageCounter = 0;
+                }
+                else
+                {
+                    imageCounter++;
+                }
+            }
         }
 
-        /// <summary>
-        /// Transform the collage in 3D space.
-        /// </summary>
-        /// <param name="input">The bitmap to transform.</param>
-        /// <returns>The transformed image.</returns>
-        private SKBitmap Transform3D(SKBitmap input)
+        return bitmap;
+    }
+
+    /// <summary>
+    /// Transform the collage in 3D space.
+    /// </summary>
+    /// <param name="input">The bitmap to transform.</param>
+    /// <returns>The transformed image.</returns>
+    private SKBitmap Transform3D(SKBitmap input)
+    {
+        var bitmap = new SKBitmap(FinalWidth, FinalHeight);
+        using var canvas = new SKCanvas(bitmap);
+        canvas.Clear(SKColors.Black);
+        var matrix = new SKMatrix
         {
-            var bitmap = new SKBitmap(FinalWidth, FinalHeight);
-            using var canvas = new SKCanvas(bitmap);
-            canvas.Clear(SKColors.Black);
-            var matrix = new SKMatrix
-            {
-                ScaleX = 0.324108899f,
-                ScaleY = 0.563934922f,
-                SkewX = -0.244337708f,
-                SkewY = 0.0377609022f,
-                TransX = 42.0407715f,
-                TransY = -198.104706f,
-                Persp0 = -9.08959337E-05f,
-                Persp1 = 6.85242048E-05f,
-                Persp2 = 0.988209724f
-            };
-
-            canvas.SetMatrix(matrix);
-            canvas.DrawBitmap(input, 0, 0);
-
-            return bitmap;
-        }
+            ScaleX = 0.324108899f,
+            ScaleY = 0.563934922f,
+            SkewX = -0.244337708f,
+            SkewY = 0.0377609022f,
+            TransX = 42.0407715f,
+            TransY = -198.104706f,
+            Persp0 = -9.08959337E-05f,
+            Persp1 = 6.85242048E-05f,
+            Persp2 = 0.988209724f
+        };
+
+        canvas.SetMatrix(matrix);
+        canvas.DrawBitmap(input, 0, 0);
+
+        return bitmap;
     }
 }

+ 142 - 143
src/Jellyfin.Drawing.Skia/StripCollageBuilder.cs

@@ -4,183 +4,182 @@ using System.IO;
 using System.Text.RegularExpressions;
 using SkiaSharp;
 
-namespace Jellyfin.Drawing.Skia
+namespace Jellyfin.Drawing.Skia;
+
+/// <summary>
+/// Used to build collages of multiple images arranged in vertical strips.
+/// </summary>
+public class StripCollageBuilder
 {
+    private readonly SkiaEncoder _skiaEncoder;
+
+    /// <summary>
+    /// Initializes a new instance of the <see cref="StripCollageBuilder"/> class.
+    /// </summary>
+    /// <param name="skiaEncoder">The encoder to use for building collages.</param>
+    public StripCollageBuilder(SkiaEncoder skiaEncoder)
+    {
+        _skiaEncoder = skiaEncoder;
+    }
+
     /// <summary>
-    /// Used to build collages of multiple images arranged in vertical strips.
+    /// Check which format an image has been encoded with using its filename extension.
     /// </summary>
-    public class StripCollageBuilder
+    /// <param name="outputPath">The path to the image to get the format for.</param>
+    /// <returns>The image format.</returns>
+    public static SKEncodedImageFormat GetEncodedFormat(string outputPath)
     {
-        private readonly SkiaEncoder _skiaEncoder;
+        ArgumentNullException.ThrowIfNull(outputPath);
+
+        var ext = Path.GetExtension(outputPath);
 
-        /// <summary>
-        /// Initializes a new instance of the <see cref="StripCollageBuilder"/> class.
-        /// </summary>
-        /// <param name="skiaEncoder">The encoder to use for building collages.</param>
-        public StripCollageBuilder(SkiaEncoder skiaEncoder)
+        if (string.Equals(ext, ".jpg", StringComparison.OrdinalIgnoreCase)
+            || string.Equals(ext, ".jpeg", StringComparison.OrdinalIgnoreCase))
         {
-            _skiaEncoder = skiaEncoder;
+            return SKEncodedImageFormat.Jpeg;
         }
 
-        /// <summary>
-        /// Check which format an image has been encoded with using its filename extension.
-        /// </summary>
-        /// <param name="outputPath">The path to the image to get the format for.</param>
-        /// <returns>The image format.</returns>
-        public static SKEncodedImageFormat GetEncodedFormat(string outputPath)
+        if (string.Equals(ext, ".webp", StringComparison.OrdinalIgnoreCase))
         {
-            ArgumentNullException.ThrowIfNull(outputPath);
-
-            var ext = Path.GetExtension(outputPath);
-
-            if (string.Equals(ext, ".jpg", StringComparison.OrdinalIgnoreCase)
-                || string.Equals(ext, ".jpeg", StringComparison.OrdinalIgnoreCase))
-            {
-                return SKEncodedImageFormat.Jpeg;
-            }
-
-            if (string.Equals(ext, ".webp", StringComparison.OrdinalIgnoreCase))
-            {
-                return SKEncodedImageFormat.Webp;
-            }
-
-            if (string.Equals(ext, ".gif", StringComparison.OrdinalIgnoreCase))
-            {
-                return SKEncodedImageFormat.Gif;
-            }
-
-            if (string.Equals(ext, ".bmp", StringComparison.OrdinalIgnoreCase))
-            {
-                return SKEncodedImageFormat.Bmp;
-            }
-
-            // default to png
-            return SKEncodedImageFormat.Png;
+            return SKEncodedImageFormat.Webp;
         }
 
-        /// <summary>
-        /// Create a square collage.
-        /// </summary>
-        /// <param name="paths">The paths of the images to use in the collage.</param>
-        /// <param name="outputPath">The path at which to place the resulting collage image.</param>
-        /// <param name="width">The desired width of the collage.</param>
-        /// <param name="height">The desired height of the collage.</param>
-        public void BuildSquareCollage(IReadOnlyList<string> paths, string outputPath, int width, int height)
+        if (string.Equals(ext, ".gif", StringComparison.OrdinalIgnoreCase))
         {
-            using var bitmap = BuildSquareCollageBitmap(paths, width, height);
-            using var outputStream = new SKFileWStream(outputPath);
-            using var pixmap = new SKPixmap(new SKImageInfo(width, height), bitmap.GetPixels());
-            pixmap.Encode(outputStream, GetEncodedFormat(outputPath), 90);
+            return SKEncodedImageFormat.Gif;
         }
 
-        /// <summary>
-        /// Create a thumb collage.
-        /// </summary>
-        /// <param name="paths">The paths of the images to use in the collage.</param>
-        /// <param name="outputPath">The path at which to place the resulting image.</param>
-        /// <param name="width">The desired width of the collage.</param>
-        /// <param name="height">The desired height of the collage.</param>
-        /// <param name="libraryName">The name of the library to draw on the collage.</param>
-        public void BuildThumbCollage(IReadOnlyList<string> paths, string outputPath, int width, int height, string? libraryName)
+        if (string.Equals(ext, ".bmp", StringComparison.OrdinalIgnoreCase))
         {
-            using var bitmap = BuildThumbCollageBitmap(paths, width, height, libraryName);
-            using var outputStream = new SKFileWStream(outputPath);
-            using var pixmap = new SKPixmap(new SKImageInfo(width, height), bitmap.GetPixels());
-            pixmap.Encode(outputStream, GetEncodedFormat(outputPath), 90);
+            return SKEncodedImageFormat.Bmp;
         }
 
-        private SKBitmap BuildThumbCollageBitmap(IReadOnlyList<string> paths, int width, int height, string? libraryName)
-        {
-            var bitmap = new SKBitmap(width, height);
+        // default to png
+        return SKEncodedImageFormat.Png;
+    }
 
-            using var canvas = new SKCanvas(bitmap);
-            canvas.Clear(SKColors.Black);
+    /// <summary>
+    /// Create a square collage.
+    /// </summary>
+    /// <param name="paths">The paths of the images to use in the collage.</param>
+    /// <param name="outputPath">The path at which to place the resulting collage image.</param>
+    /// <param name="width">The desired width of the collage.</param>
+    /// <param name="height">The desired height of the collage.</param>
+    public void BuildSquareCollage(IReadOnlyList<string> paths, string outputPath, int width, int height)
+    {
+        using var bitmap = BuildSquareCollageBitmap(paths, width, height);
+        using var outputStream = new SKFileWStream(outputPath);
+        using var pixmap = new SKPixmap(new SKImageInfo(width, height), bitmap.GetPixels());
+        pixmap.Encode(outputStream, GetEncodedFormat(outputPath), 90);
+    }
 
-            using var backdrop = SkiaHelper.GetNextValidImage(_skiaEncoder, paths, 0, out _);
-            if (backdrop is null)
-            {
-                return bitmap;
-            }
+    /// <summary>
+    /// Create a thumb collage.
+    /// </summary>
+    /// <param name="paths">The paths of the images to use in the collage.</param>
+    /// <param name="outputPath">The path at which to place the resulting image.</param>
+    /// <param name="width">The desired width of the collage.</param>
+    /// <param name="height">The desired height of the collage.</param>
+    /// <param name="libraryName">The name of the library to draw on the collage.</param>
+    public void BuildThumbCollage(IReadOnlyList<string> paths, string outputPath, int width, int height, string? libraryName)
+    {
+        using var bitmap = BuildThumbCollageBitmap(paths, width, height, libraryName);
+        using var outputStream = new SKFileWStream(outputPath);
+        using var pixmap = new SKPixmap(new SKImageInfo(width, height), bitmap.GetPixels());
+        pixmap.Encode(outputStream, GetEncodedFormat(outputPath), 90);
+    }
 
-            // resize to the same aspect as the original
-            var backdropHeight = Math.Abs(width * backdrop.Height / backdrop.Width);
-            using var residedBackdrop = SkiaEncoder.ResizeImage(backdrop, new SKImageInfo(width, backdropHeight, backdrop.ColorType, backdrop.AlphaType, backdrop.ColorSpace));
-            // draw the backdrop
-            canvas.DrawImage(residedBackdrop, 0, 0);
+    private SKBitmap BuildThumbCollageBitmap(IReadOnlyList<string> paths, int width, int height, string? libraryName)
+    {
+        var bitmap = new SKBitmap(width, height);
 
-            // draw shadow rectangle
-            using var paintColor = new SKPaint
-            {
-                Color = SKColors.Black.WithAlpha(0x78),
-                Style = SKPaintStyle.Fill
-            };
-            canvas.DrawRect(0, 0, width, height, paintColor);
+        using var canvas = new SKCanvas(bitmap);
+        canvas.Clear(SKColors.Black);
 
-            var typeFace = SKTypeface.FromFamilyName("sans-serif", SKFontStyleWeight.Bold, SKFontStyleWidth.Normal, SKFontStyleSlant.Upright);
+        using var backdrop = SkiaHelper.GetNextValidImage(_skiaEncoder, paths, 0, out _);
+        if (backdrop is null)
+        {
+            return bitmap;
+        }
 
-            // use the system fallback to find a typeface for the given CJK character
-            var nonCjkPattern = @"[^\p{IsCJKUnifiedIdeographs}\p{IsCJKUnifiedIdeographsExtensionA}\p{IsKatakana}\p{IsHiragana}\p{IsHangulSyllables}\p{IsHangulJamo}]";
-            var filteredName = Regex.Replace(libraryName ?? string.Empty, nonCjkPattern, string.Empty);
-            if (!string.IsNullOrEmpty(filteredName))
-            {
-                typeFace = SKFontManager.Default.MatchCharacter(null, SKFontStyleWeight.Bold, SKFontStyleWidth.Normal, SKFontStyleSlant.Upright, null, filteredName[0]);
-            }
+        // resize to the same aspect as the original
+        var backdropHeight = Math.Abs(width * backdrop.Height / backdrop.Width);
+        using var residedBackdrop = SkiaEncoder.ResizeImage(backdrop, new SKImageInfo(width, backdropHeight, backdrop.ColorType, backdrop.AlphaType, backdrop.ColorSpace));
+        // draw the backdrop
+        canvas.DrawImage(residedBackdrop, 0, 0);
 
-            // draw library name
-            using var textPaint = new SKPaint
-            {
-                Color = SKColors.White,
-                Style = SKPaintStyle.Fill,
-                TextSize = 112,
-                TextAlign = SKTextAlign.Center,
-                Typeface = typeFace,
-                IsAntialias = true
-            };
-
-            // scale down text to 90% of the width if text is larger than 95% of the width
-            var textWidth = textPaint.MeasureText(libraryName);
-            if (textWidth > width * 0.95)
-            {
-                textPaint.TextSize = 0.9f * width * textPaint.TextSize / textWidth;
-            }
+        // draw shadow rectangle
+        using var paintColor = new SKPaint
+        {
+            Color = SKColors.Black.WithAlpha(0x78),
+            Style = SKPaintStyle.Fill
+        };
+        canvas.DrawRect(0, 0, width, height, paintColor);
 
-            canvas.DrawText(libraryName, width / 2f, (height / 2f) + (textPaint.FontMetrics.XHeight / 2), textPaint);
+        var typeFace = SKTypeface.FromFamilyName("sans-serif", SKFontStyleWeight.Bold, SKFontStyleWidth.Normal, SKFontStyleSlant.Upright);
 
-            return bitmap;
+        // use the system fallback to find a typeface for the given CJK character
+        var nonCjkPattern = @"[^\p{IsCJKUnifiedIdeographs}\p{IsCJKUnifiedIdeographsExtensionA}\p{IsKatakana}\p{IsHiragana}\p{IsHangulSyllables}\p{IsHangulJamo}]";
+        var filteredName = Regex.Replace(libraryName ?? string.Empty, nonCjkPattern, string.Empty);
+        if (!string.IsNullOrEmpty(filteredName))
+        {
+            typeFace = SKFontManager.Default.MatchCharacter(null, SKFontStyleWeight.Bold, SKFontStyleWidth.Normal, SKFontStyleSlant.Upright, null, filteredName[0]);
         }
 
-        private SKBitmap BuildSquareCollageBitmap(IReadOnlyList<string> paths, int width, int height)
+        // draw library name
+        using var textPaint = new SKPaint
+        {
+            Color = SKColors.White,
+            Style = SKPaintStyle.Fill,
+            TextSize = 112,
+            TextAlign = SKTextAlign.Center,
+            Typeface = typeFace,
+            IsAntialias = true
+        };
+
+        // scale down text to 90% of the width if text is larger than 95% of the width
+        var textWidth = textPaint.MeasureText(libraryName);
+        if (textWidth > width * 0.95)
         {
-            var bitmap = new SKBitmap(width, height);
-            var imageIndex = 0;
-            var cellWidth = width / 2;
-            var cellHeight = height / 2;
+            textPaint.TextSize = 0.9f * width * textPaint.TextSize / textWidth;
+        }
+
+        canvas.DrawText(libraryName, width / 2f, (height / 2f) + (textPaint.FontMetrics.XHeight / 2), textPaint);
+
+        return bitmap;
+    }
+
+    private SKBitmap BuildSquareCollageBitmap(IReadOnlyList<string> paths, int width, int height)
+    {
+        var bitmap = new SKBitmap(width, height);
+        var imageIndex = 0;
+        var cellWidth = width / 2;
+        var cellHeight = height / 2;
 
-            using var canvas = new SKCanvas(bitmap);
-            for (var x = 0; x < 2; x++)
+        using var canvas = new SKCanvas(bitmap);
+        for (var x = 0; x < 2; x++)
+        {
+            for (var y = 0; y < 2; y++)
             {
-                for (var y = 0; y < 2; y++)
+                using var currentBitmap = SkiaHelper.GetNextValidImage(_skiaEncoder, paths, imageIndex, out int newIndex);
+                imageIndex = newIndex;
+
+                if (currentBitmap is null)
                 {
-                    using var currentBitmap = SkiaHelper.GetNextValidImage(_skiaEncoder, paths, imageIndex, out int newIndex);
-                    imageIndex = newIndex;
-
-                    if (currentBitmap is null)
-                    {
-                        continue;
-                    }
-
-                    // Scale image. The FromBitmap creates a copy
-                    var imageInfo = new SKImageInfo(cellWidth, cellHeight, currentBitmap.ColorType, currentBitmap.AlphaType, currentBitmap.ColorSpace);
-                    using var resizedBitmap = SKBitmap.FromImage(SkiaEncoder.ResizeImage(currentBitmap, imageInfo));
-
-                    // draw this image into the strip at the next position
-                    var xPos = x * cellWidth;
-                    var yPos = y * cellHeight;
-                    canvas.DrawBitmap(resizedBitmap, xPos, yPos);
+                    continue;
                 }
-            }
 
-            return bitmap;
+                // Scale image. The FromBitmap creates a copy
+                var imageInfo = new SKImageInfo(cellWidth, cellHeight, currentBitmap.ColorType, currentBitmap.AlphaType, currentBitmap.ColorSpace);
+                using var resizedBitmap = SKBitmap.FromImage(SkiaEncoder.ResizeImage(currentBitmap, imageInfo));
+
+                // draw this image into the strip at the next position
+                var xPos = x * cellWidth;
+                var yPos = y * cellHeight;
+                canvas.DrawBitmap(resizedBitmap, xPos, yPos);
+            }
         }
+
+        return bitmap;
     }
 }

+ 45 - 46
src/Jellyfin.Drawing.Skia/UnplayedCountIndicator.cs

@@ -2,63 +2,62 @@ using System.Globalization;
 using MediaBrowser.Model.Drawing;
 using SkiaSharp;
 
-namespace Jellyfin.Drawing.Skia
+namespace Jellyfin.Drawing.Skia;
+
+/// <summary>
+/// Static helper class for drawing unplayed count indicators.
+/// </summary>
+public static class UnplayedCountIndicator
 {
     /// <summary>
-    /// Static helper class for drawing unplayed count indicators.
+    /// The x-offset used when drawing an unplayed count indicator.
+    /// </summary>
+    private const int OffsetFromTopRightCorner = 38;
+
+    /// <summary>
+    /// Draw an unplayed count indicator in the top right corner of a canvas.
     /// </summary>
-    public static class UnplayedCountIndicator
+    /// <param name="canvas">The canvas to draw the indicator on.</param>
+    /// <param name="imageSize">
+    /// The dimensions of the image to draw the indicator on. The width is used to determine the x-position of the
+    /// indicator.
+    /// </param>
+    /// <param name="count">The number to draw in the indicator.</param>
+    public static void DrawUnplayedCountIndicator(SKCanvas canvas, ImageDimensions imageSize, int count)
     {
-        /// <summary>
-        /// The x-offset used when drawing an unplayed count indicator.
-        /// </summary>
-        private const int OffsetFromTopRightCorner = 38;
+        var x = imageSize.Width - OffsetFromTopRightCorner;
+        var text = count.ToString(CultureInfo.InvariantCulture);
 
-        /// <summary>
-        /// Draw an unplayed count indicator in the top right corner of a canvas.
-        /// </summary>
-        /// <param name="canvas">The canvas to draw the indicator on.</param>
-        /// <param name="imageSize">
-        /// The dimensions of the image to draw the indicator on. The width is used to determine the x-position of the
-        /// indicator.
-        /// </param>
-        /// <param name="count">The number to draw in the indicator.</param>
-        public static void DrawUnplayedCountIndicator(SKCanvas canvas, ImageDimensions imageSize, int count)
+        using var paint = new SKPaint
         {
-            var x = imageSize.Width - OffsetFromTopRightCorner;
-            var text = count.ToString(CultureInfo.InvariantCulture);
-
-            using var paint = new SKPaint
-            {
-                Color = SKColor.Parse("#CC00A4DC"),
-                Style = SKPaintStyle.Fill
-            };
-
-            canvas.DrawCircle(x, OffsetFromTopRightCorner, 20, paint);
+            Color = SKColor.Parse("#CC00A4DC"),
+            Style = SKPaintStyle.Fill
+        };
 
-            paint.Color = new SKColor(255, 255, 255, 255);
-            paint.TextSize = 24;
-            paint.IsAntialias = true;
+        canvas.DrawCircle(x, OffsetFromTopRightCorner, 20, paint);
 
-            var y = OffsetFromTopRightCorner + 9;
+        paint.Color = new SKColor(255, 255, 255, 255);
+        paint.TextSize = 24;
+        paint.IsAntialias = true;
 
-            if (text.Length == 1)
-            {
-                x -= 7;
-            }
+        var y = OffsetFromTopRightCorner + 9;
 
-            if (text.Length == 2)
-            {
-                x -= 13;
-            }
-            else if (text.Length >= 3)
-            {
-                x -= 15;
-                y -= 2;
-                paint.TextSize = 18;
-            }
+        if (text.Length == 1)
+        {
+            x -= 7;
+        }
 
-            canvas.DrawText(text, x, y, paint);
+        if (text.Length == 2)
+        {
+            x -= 13;
         }
+        else if (text.Length >= 3)
+        {
+            x -= 15;
+            y -= 2;
+            paint.TextSize = 18;
+        }
+
+        canvas.DrawText(text, x, y, paint);
     }
 }