diff --git a/Emby.Drawing/ImageProcessor.cs b/Emby.Drawing/ImageProcessor.cs index 18b413964..11256dafd 100644 --- a/Emby.Drawing/ImageProcessor.cs +++ b/Emby.Drawing/ImageProcessor.cs @@ -395,7 +395,13 @@ namespace Emby.Drawing public string GetImageBlurHash(string path) { var size = GetImageDimensions(path); - if (size.Width <= 0 || size.Height <= 0) + return GetImageBlurHash(path, size); + } + + /// + public string GetImageBlurHash(string path, ImageDimensions imageDimensions) + { + if (imageDimensions.Width <= 0 || imageDimensions.Height <= 0) { return string.Empty; } @@ -403,8 +409,8 @@ namespace Emby.Drawing // We want tiles to be as close to square as possible, and to *mostly* keep under 16 tiles for performance. // One tile is (width / xComp) x (height / yComp) pixels, which means that ideally yComp = xComp * height / width. // See more at https://github.com/woltapp/blurhash/#how-do-i-pick-the-number-of-x-and-y-components - float xCompF = MathF.Sqrt(16.0f * size.Width / size.Height); - float yCompF = xCompF * size.Height / size.Width; + float xCompF = MathF.Sqrt(16.0f * imageDimensions.Width / imageDimensions.Height); + float yCompF = xCompF * imageDimensions.Height / imageDimensions.Width; int xComp = Math.Min((int)xCompF + 1, 9); int yComp = Math.Min((int)yCompF + 1, 9); @@ -439,47 +445,46 @@ namespace Emby.Drawing .ToString("N", CultureInfo.InvariantCulture); } - private async Task<(string Path, DateTime DateModified)> GetSupportedImage(string originalImagePath, DateTime dateModified) + private Task<(string Path, DateTime DateModified)> GetSupportedImage(string originalImagePath, DateTime dateModified) { - var inputFormat = Path.GetExtension(originalImagePath) - .TrimStart('.') - .Replace("jpeg", "jpg", StringComparison.OrdinalIgnoreCase); + var inputFormat = Path.GetExtension(originalImagePath.AsSpan()).TrimStart('.').ToString(); // These are just jpg files renamed as tbn if (string.Equals(inputFormat, "tbn", StringComparison.OrdinalIgnoreCase)) { - return (originalImagePath, dateModified); + return Task.FromResult((originalImagePath, dateModified)); } - if (!_imageEncoder.SupportedInputFormats.Contains(inputFormat)) - { - try - { - string filename = (originalImagePath + dateModified.Ticks.ToString(CultureInfo.InvariantCulture)).GetMD5().ToString("N", CultureInfo.InvariantCulture); + // TODO _mediaEncoder.ConvertImage is not implemented + // if (!_imageEncoder.SupportedInputFormats.Contains(inputFormat)) + // { + // try + // { + // string filename = (originalImagePath + dateModified.Ticks.ToString(CultureInfo.InvariantCulture)).GetMD5().ToString("N", CultureInfo.InvariantCulture); + // + // string cacheExtension = _mediaEncoder.SupportsEncoder("libwebp") ? ".webp" : ".png"; + // var outputPath = Path.Combine(_appPaths.ImageCachePath, "converted-images", filename + cacheExtension); + // + // var file = _fileSystem.GetFileInfo(outputPath); + // if (!file.Exists) + // { + // await _mediaEncoder.ConvertImage(originalImagePath, outputPath).ConfigureAwait(false); + // dateModified = _fileSystem.GetLastWriteTimeUtc(outputPath); + // } + // else + // { + // dateModified = file.LastWriteTimeUtc; + // } + // + // originalImagePath = outputPath; + // } + // catch (Exception ex) + // { + // _logger.LogError(ex, "Image conversion failed for {Path}", originalImagePath); + // } + // } - string cacheExtension = _mediaEncoder.SupportsEncoder("libwebp") ? ".webp" : ".png"; - var outputPath = Path.Combine(_appPaths.ImageCachePath, "converted-images", filename + cacheExtension); - - var file = _fileSystem.GetFileInfo(outputPath); - if (!file.Exists) - { - await _mediaEncoder.ConvertImage(originalImagePath, outputPath).ConfigureAwait(false); - dateModified = _fileSystem.GetLastWriteTimeUtc(outputPath); - } - else - { - dateModified = file.LastWriteTimeUtc; - } - - originalImagePath = outputPath; - } - catch (Exception ex) - { - _logger.LogError(ex, "Image conversion failed for {Path}", originalImagePath); - } - } - - return (originalImagePath, dateModified); + return Task.FromResult((originalImagePath, dateModified)); } /// diff --git a/Emby.Server.Implementations/Library/LibraryManager.cs b/Emby.Server.Implementations/Library/LibraryManager.cs index d6754ad4a..c54945c93 100644 --- a/Emby.Server.Implementations/Library/LibraryManager.cs +++ b/Emby.Server.Implementations/Library/LibraryManager.cs @@ -1860,7 +1860,9 @@ namespace Emby.Server.Implementations.Library throw new ArgumentNullException(nameof(item)); } - var outdated = forceUpdate ? item.ImageInfos.Where(i => i.Path != null).ToArray() : item.ImageInfos.Where(ImageNeedsRefresh).ToArray(); + var outdated = forceUpdate + ? item.ImageInfos.Where(i => i.Path != null).ToArray() + : item.ImageInfos.Where(ImageNeedsRefresh).ToArray(); // Skip image processing if current or live tv source if (outdated.Length == 0 || item.SourceType != SourceType.Library) { @@ -1883,7 +1885,7 @@ namespace Emby.Server.Implementations.Library _logger.LogWarning("Cannot get image index for {ImagePath}", img.Path); continue; } - catch (Exception ex) when (ex is InvalidOperationException || ex is IOException) + catch (Exception ex) when (ex is InvalidOperationException or IOException) { _logger.LogWarning(ex, "Cannot fetch image from {ImagePath}", img.Path); continue; @@ -1895,23 +1897,24 @@ namespace Emby.Server.Implementations.Library } } + ImageDimensions size; try { - ImageDimensions size = _imageProcessor.GetImageDimensions(item, image); + size = _imageProcessor.GetImageDimensions(item, image); image.Width = size.Width; image.Height = size.Height; } catch (Exception ex) { _logger.LogError(ex, "Cannot get image dimensions for {ImagePath}", image.Path); + size = new ImageDimensions(0, 0); image.Width = 0; image.Height = 0; - continue; } try { - image.BlurHash = _imageProcessor.GetImageBlurHash(image.Path); + image.BlurHash = _imageProcessor.GetImageBlurHash(image.Path, size); } catch (Exception ex) { diff --git a/Jellyfin.Drawing.Skia/Jellyfin.Drawing.Skia.csproj b/Jellyfin.Drawing.Skia/Jellyfin.Drawing.Skia.csproj index 495c5b860..4de4b231d 100644 --- a/Jellyfin.Drawing.Skia/Jellyfin.Drawing.Skia.csproj +++ b/Jellyfin.Drawing.Skia/Jellyfin.Drawing.Skia.csproj @@ -20,6 +20,7 @@ + diff --git a/Jellyfin.Drawing.Skia/SkiaEncoder.cs b/Jellyfin.Drawing.Skia/SkiaEncoder.cs index 2358fe623..687528231 100644 --- a/Jellyfin.Drawing.Skia/SkiaEncoder.cs +++ b/Jellyfin.Drawing.Skia/SkiaEncoder.cs @@ -10,7 +10,7 @@ using MediaBrowser.Controller.Drawing; using MediaBrowser.Model.Drawing; using Microsoft.Extensions.Logging; using SkiaSharp; -using static Jellyfin.Drawing.Skia.SkiaHelper; +using SKSvg = SkiaSharp.Extended.Svg.SKSvg; namespace Jellyfin.Drawing.Skia { @@ -19,8 +19,7 @@ namespace Jellyfin.Drawing.Skia /// public class SkiaEncoder : IImageEncoder { - private static readonly HashSet _transparentImageTypes - = new HashSet(StringComparer.OrdinalIgnoreCase) { ".png", ".gif", ".webp" }; + private static readonly HashSet _transparentImageTypes = new(StringComparer.OrdinalIgnoreCase) { ".png", ".gif", ".webp" }; private readonly ILogger _logger; private readonly IApplicationPaths _appPaths; @@ -71,7 +70,7 @@ namespace Jellyfin.Drawing.Skia /// public IReadOnlyCollection SupportedOutputFormats - => new HashSet() { ImageFormat.Webp, ImageFormat.Jpg, ImageFormat.Png }; + => new HashSet { ImageFormat.Webp, ImageFormat.Jpg, ImageFormat.Png }; /// /// Check if the native lib is available. @@ -109,9 +108,7 @@ namespace Jellyfin.Drawing.Skia } /// - /// The path is null. /// The path is not valid. - /// The file at the specified path could not be used to generate a codec. public ImageDimensions GetImageSize(string path) { if (!File.Exists(path)) @@ -119,12 +116,27 @@ namespace Jellyfin.Drawing.Skia throw new FileNotFoundException("File not found", path); } + 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)); + } + using var codec = SKCodec.Create(path, out SKCodecResult result); - EnsureSuccess(result); - - var info = codec.Info; - - return new ImageDimensions(info.Width, info.Height); + 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); + } } /// @@ -138,6 +150,13 @@ namespace Jellyfin.Drawing.Skia throw new ArgumentNullException(nameof(path)); } + 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; + } + // Any larger than 128x128 is too slow and there's no visually discernible difference return BlurHashEncoder.Encode(xComp, yComp, path, 128, 128); } @@ -378,6 +397,13 @@ namespace Jellyfin.Drawing.Skia throw new ArgumentException("String can't be empty.", nameof(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; + } + var skiaOutputFormat = GetImageFormat(outputFormat); var hasBackgroundColor = !string.IsNullOrWhiteSpace(options.BackgroundColor); diff --git a/Jellyfin.Drawing.Skia/SkiaHelper.cs b/Jellyfin.Drawing.Skia/SkiaHelper.cs index 35dcebdab..c001c32b8 100644 --- a/Jellyfin.Drawing.Skia/SkiaHelper.cs +++ b/Jellyfin.Drawing.Skia/SkiaHelper.cs @@ -8,19 +8,6 @@ namespace Jellyfin.Drawing.Skia /// public static class SkiaHelper { - /// - /// Ensures the result is a success - /// by throwing an exception when that's not the case. - /// - /// The result returned by Skia. - public static void EnsureSuccess(SKCodecResult result) - { - if (result != SKCodecResult.Success) - { - throw new SkiaCodecException(result); - } - } - /// /// Gets the next valid image as a bitmap. /// diff --git a/MediaBrowser.Controller/Drawing/IImageProcessor.cs b/MediaBrowser.Controller/Drawing/IImageProcessor.cs index 03882a0b9..e5ce0aa21 100644 --- a/MediaBrowser.Controller/Drawing/IImageProcessor.cs +++ b/MediaBrowser.Controller/Drawing/IImageProcessor.cs @@ -50,6 +50,14 @@ namespace MediaBrowser.Controller.Drawing /// BlurHash. string GetImageBlurHash(string path); + /// + /// Gets the blurhash of the image. + /// + /// Path to the image file. + /// The image dimensions. + /// BlurHash. + string GetImageBlurHash(string path, ImageDimensions imageDimensions); + /// /// Gets the image cache tag. ///