From c5e723bccd723c4c08b7239c5205085be00bd10d Mon Sep 17 00:00:00 2001 From: Cody Robibero Date: Wed, 28 Feb 2024 09:56:02 -0700 Subject: [PATCH 1/3] Add support for converting from svg to other image types --- Jellyfin.Api/Controllers/ImageController.cs | 3 +- MediaBrowser.Model/Drawing/ImageFormat.cs | 7 ++- .../Drawing/ImageFormatExtensions.cs | 2 + src/Jellyfin.Drawing.Skia/SkiaEncoder.cs | 43 +++++++++++++++++-- .../Drawing/ImageFormatExtensionsTests.cs | 4 +- 5 files changed, 50 insertions(+), 9 deletions(-) diff --git a/Jellyfin.Api/Controllers/ImageController.cs b/Jellyfin.Api/Controllers/ImageController.cs index c031ce338d..8368b846dd 100644 --- a/Jellyfin.Api/Controllers/ImageController.cs +++ b/Jellyfin.Api/Controllers/ImageController.cs @@ -11,7 +11,6 @@ using System.Security.Cryptography; using System.Threading; using System.Threading.Tasks; using Jellyfin.Api.Attributes; -using Jellyfin.Api.Constants; using Jellyfin.Api.Helpers; using MediaBrowser.Common.Api; using MediaBrowser.Common.Configuration; @@ -1993,7 +1992,7 @@ public class ImageController : BaseJellyfinApiController { if (format.HasValue) { - return new[] { format.Value }; + return [format.Value]; } return GetClientSupportedFormats(); diff --git a/MediaBrowser.Model/Drawing/ImageFormat.cs b/MediaBrowser.Model/Drawing/ImageFormat.cs index 511c16a4e2..671243e80b 100644 --- a/MediaBrowser.Model/Drawing/ImageFormat.cs +++ b/MediaBrowser.Model/Drawing/ImageFormat.cs @@ -28,6 +28,11 @@ namespace MediaBrowser.Model.Drawing /// /// The webp. /// - Webp + Webp, + + /// + /// The svg format. + /// + Svg, } } diff --git a/MediaBrowser.Model/Drawing/ImageFormatExtensions.cs b/MediaBrowser.Model/Drawing/ImageFormatExtensions.cs index 1bb24112ec..1c60ba4601 100644 --- a/MediaBrowser.Model/Drawing/ImageFormatExtensions.cs +++ b/MediaBrowser.Model/Drawing/ImageFormatExtensions.cs @@ -22,6 +22,7 @@ public static class ImageFormatExtensions ImageFormat.Jpg => MediaTypeNames.Image.Jpeg, ImageFormat.Png => "image/png", ImageFormat.Webp => "image/webp", + ImageFormat.Svg => "image/svg+xml", _ => throw new InvalidEnumArgumentException(nameof(format), (int)format, typeof(ImageFormat)) }; @@ -39,6 +40,7 @@ public static class ImageFormatExtensions ImageFormat.Jpg => ".jpg", ImageFormat.Png => ".png", ImageFormat.Webp => ".webp", + ImageFormat.Svg => ".svg", _ => throw new InvalidEnumArgumentException(nameof(format), (int)format, typeof(ImageFormat)) }; } diff --git a/src/Jellyfin.Drawing.Skia/SkiaEncoder.cs b/src/Jellyfin.Drawing.Skia/SkiaEncoder.cs index 4ae5a9a483..9454e63aa6 100644 --- a/src/Jellyfin.Drawing.Skia/SkiaEncoder.cs +++ b/src/Jellyfin.Drawing.Skia/SkiaEncoder.cs @@ -19,8 +19,8 @@ namespace Jellyfin.Drawing.Skia; /// public class SkiaEncoder : IImageEncoder { + private const string SvgFormat = "svg"; private static readonly HashSet _transparentImageTypes = new(StringComparer.OrdinalIgnoreCase) { ".png", ".gif", ".webp" }; - private readonly ILogger _logger; private readonly IApplicationPaths _appPaths; private static readonly SKImageFilter _imageFilter; @@ -89,12 +89,13 @@ public class SkiaEncoder : IImageEncoder // working on windows at least "cr2", "nef", - "arw" + "arw", + SvgFormat }; /// public IReadOnlyCollection SupportedOutputFormats - => new HashSet { ImageFormat.Webp, ImageFormat.Jpg, ImageFormat.Png }; + => new HashSet { ImageFormat.Webp, ImageFormat.Jpg, ImageFormat.Png, ImageFormat.Svg }; /// /// Check if the native lib is available. @@ -312,6 +313,31 @@ public class SkiaEncoder : IImageEncoder return Decode(path, false, orientation, out _); } + private SKBitmap? GetBitmapFromSvg(string path) + { + if (!File.Exists(path)) + { + throw new FileNotFoundException("File not found", path); + } + + using var svg = SKSvg.CreateFromFile(path); + if (svg.Drawable is null) + { + return null; + } + + var width = (int)Math.Round(svg.Drawable.Bounds.Width); + var height = (int)Math.Round(svg.Drawable.Bounds.Height); + + var bitmap = new SKBitmap(width, height); + using var canvas = new SKCanvas(bitmap); + canvas.DrawPicture(svg.Picture); + canvas.Flush(); + canvas.Save(); + + return bitmap; + } + private SKBitmap OrientImage(SKBitmap bitmap, SKEncodedOrigin origin) { var needsFlip = origin is SKEncodedOrigin.LeftBottom or SKEncodedOrigin.LeftTop or SKEncodedOrigin.RightBottom or SKEncodedOrigin.RightTop; @@ -402,6 +428,12 @@ public class SkiaEncoder : IImageEncoder return inputPath; } + if (outputFormat == ImageFormat.Svg + && !inputFormat.Equals(SvgFormat, StringComparison.OrdinalIgnoreCase)) + { + throw new InvalidOperationException($"Requested svg output from {inputFormat} input"); + } + var skiaOutputFormat = GetImageFormat(outputFormat); var hasBackgroundColor = !string.IsNullOrWhiteSpace(options.BackgroundColor); @@ -409,7 +441,10 @@ public class SkiaEncoder : IImageEncoder var blur = options.Blur ?? 0; var hasIndicator = options.UnplayedCount.HasValue || !options.PercentPlayed.Equals(0); - using var bitmap = GetBitmap(inputPath, autoOrient, orientation); + using var bitmap = inputFormat.Equals(SvgFormat, StringComparison.OrdinalIgnoreCase) + ? GetBitmapFromSvg(inputPath) + : GetBitmap(inputPath, autoOrient, orientation); + if (bitmap is null) { throw new InvalidDataException($"Skia unable to read image {inputPath}"); diff --git a/tests/Jellyfin.Model.Tests/Drawing/ImageFormatExtensionsTests.cs b/tests/Jellyfin.Model.Tests/Drawing/ImageFormatExtensionsTests.cs index 198ad5a274..2399a10a35 100644 --- a/tests/Jellyfin.Model.Tests/Drawing/ImageFormatExtensionsTests.cs +++ b/tests/Jellyfin.Model.Tests/Drawing/ImageFormatExtensionsTests.cs @@ -27,7 +27,7 @@ public static class ImageFormatExtensionsTests [InlineData((ImageFormat)int.MinValue)] [InlineData((ImageFormat)int.MaxValue)] [InlineData((ImageFormat)(-1))] - [InlineData((ImageFormat)5)] + [InlineData((ImageFormat)6)] public static void GetMimeType_Valid_ThrowsInvalidEnumArgumentException(ImageFormat format) => Assert.Throws(() => format.GetMimeType()); @@ -40,7 +40,7 @@ public static class ImageFormatExtensionsTests [InlineData((ImageFormat)int.MinValue)] [InlineData((ImageFormat)int.MaxValue)] [InlineData((ImageFormat)(-1))] - [InlineData((ImageFormat)5)] + [InlineData((ImageFormat)6)] public static void GetExtension_Valid_ThrowsInvalidEnumArgumentException(ImageFormat format) => Assert.Throws(() => format.GetExtension()); } From a8a9f66878c7e9af4626e6f34e5e8e93f20a5620 Mon Sep 17 00:00:00 2001 From: Cody Robibero Date: Wed, 28 Feb 2024 17:39:03 -0700 Subject: [PATCH 2/3] standardize docs --- MediaBrowser.Model/Drawing/ImageFormat.cs | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/MediaBrowser.Model/Drawing/ImageFormat.cs b/MediaBrowser.Model/Drawing/ImageFormat.cs index 671243e80b..6a586f6e32 100644 --- a/MediaBrowser.Model/Drawing/ImageFormat.cs +++ b/MediaBrowser.Model/Drawing/ImageFormat.cs @@ -6,32 +6,32 @@ namespace MediaBrowser.Model.Drawing public enum ImageFormat { /// - /// The BMP. + /// BMP format. /// Bmp, /// - /// The GIF. + /// GIF format. /// Gif, /// - /// The JPG. + /// JPG format. /// Jpg, /// - /// The PNG. + /// PNG format. /// Png, /// - /// The webp. + /// WEBP format. /// Webp, /// - /// The svg format. + /// SVG format. /// Svg, } From c47bfb99bbff9f6b6cd847ba3b786936d59aa0dd Mon Sep 17 00:00:00 2001 From: Cody Robibero Date: Fri, 1 Mar 2024 17:12:45 -0700 Subject: [PATCH 3/3] Use ArgumentException --- src/Jellyfin.Drawing.Skia/SkiaEncoder.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Jellyfin.Drawing.Skia/SkiaEncoder.cs b/src/Jellyfin.Drawing.Skia/SkiaEncoder.cs index 9454e63aa6..a407194992 100644 --- a/src/Jellyfin.Drawing.Skia/SkiaEncoder.cs +++ b/src/Jellyfin.Drawing.Skia/SkiaEncoder.cs @@ -431,7 +431,7 @@ public class SkiaEncoder : IImageEncoder if (outputFormat == ImageFormat.Svg && !inputFormat.Equals(SvgFormat, StringComparison.OrdinalIgnoreCase)) { - throw new InvalidOperationException($"Requested svg output from {inputFormat} input"); + throw new ArgumentException($"Requested svg output from {inputFormat} input"); } var skiaOutputFormat = GetImageFormat(outputFormat);