浏览代码

Add BlurHash support to backend

Jesús Higueras 5 年之前
父节点
当前提交
b9fc0d2628

+ 4 - 0
Emby.Drawing/ImageProcessor.cs

@@ -313,6 +313,10 @@ namespace Emby.Drawing
         public ImageDimensions GetImageDimensions(string path)
         public ImageDimensions GetImageDimensions(string path)
             => _imageEncoder.GetImageSize(path);
             => _imageEncoder.GetImageSize(path);
 
 
+        /// <inheritdoc />
+        public string GetImageHash(string path)
+            => _imageEncoder.GetImageHash(path);
+
         /// <inheritdoc />
         /// <inheritdoc />
         public string GetImageCacheTag(BaseItem item, ItemImageInfo image)
         public string GetImageCacheTag(BaseItem item, ItemImageInfo image)
             => (item.Path + image.DateModified.Ticks).GetMD5().ToString("N", CultureInfo.InvariantCulture);
             => (item.Path + image.DateModified.Ticks).GetMD5().ToString("N", CultureInfo.InvariantCulture);

+ 6 - 0
Emby.Drawing/NullImageEncoder.cs

@@ -42,5 +42,11 @@ namespace Emby.Drawing
         {
         {
             throw new NotImplementedException();
             throw new NotImplementedException();
         }
         }
+
+        /// <inheritdoc />
+        public string GetImageHash(string inputPath)
+        {
+            throw new NotImplementedException();
+        }
     }
     }
 }
 }

+ 16 - 1
Emby.Server.Implementations/Data/SqliteItemRepository.cs

@@ -1144,12 +1144,18 @@ namespace Emby.Server.Implementations.Data
             var delimeter = "*";
             var delimeter = "*";
 
 
             var path = image.Path;
             var path = image.Path;
+            var hash = image.Hash;
 
 
             if (path == null)
             if (path == null)
             {
             {
                 path = string.Empty;
                 path = string.Empty;
             }
             }
 
 
+            if (hash == null)
+            {
+                hash = string.Empty;
+            }
+
             return GetPathToSave(path) +
             return GetPathToSave(path) +
                    delimeter +
                    delimeter +
                    image.DateModified.Ticks.ToString(CultureInfo.InvariantCulture) +
                    image.DateModified.Ticks.ToString(CultureInfo.InvariantCulture) +
@@ -1158,7 +1164,11 @@ namespace Emby.Server.Implementations.Data
                    delimeter +
                    delimeter +
                    image.Width.ToString(CultureInfo.InvariantCulture) +
                    image.Width.ToString(CultureInfo.InvariantCulture) +
                    delimeter +
                    delimeter +
-                   image.Height.ToString(CultureInfo.InvariantCulture);
+                   image.Height.ToString(CultureInfo.InvariantCulture) +
+                   delimeter +
+                   // Replace delimiters with other characters.
+                   // This can be removed when we migrate to a proper DB.
+                   hash.Replace('*', '/').Replace('|', '\\');
         }
         }
 
 
         public ItemImageInfo ItemImageInfoFromValueString(string value)
         public ItemImageInfo ItemImageInfoFromValueString(string value)
@@ -1192,6 +1202,11 @@ namespace Emby.Server.Implementations.Data
                     image.Width = width;
                     image.Width = width;
                     image.Height = height;
                     image.Height = height;
                 }
                 }
+
+                if (parts.Length >= 6)
+                {
+                    image.Hash = parts[5].Replace('/', '*').Replace('\\', '|');
+                }
             }
             }
 
 
             return image;
             return image;

+ 7 - 0
Emby.Server.Implementations/Dto/DtoService.cs

@@ -718,6 +718,7 @@ namespace Emby.Server.Implementations.Dto
             if (options.EnableImages)
             if (options.EnableImages)
             {
             {
                 dto.ImageTags = new Dictionary<ImageType, string>();
                 dto.ImageTags = new Dictionary<ImageType, string>();
+                dto.ImageHashes = new Dictionary<string, string>();
 
 
                 // Prevent implicitly captured closure
                 // Prevent implicitly captured closure
                 var currentItem = item;
                 var currentItem = item;
@@ -732,6 +733,12 @@ namespace Emby.Server.Implementations.Dto
                         {
                         {
                             dto.ImageTags[image.Type] = tag;
                             dto.ImageTags[image.Type] = tag;
                         }
                         }
+
+                        var hash = image.Hash;
+                        if (hash != null && hash.Length > 0)
+                        {
+                            dto.ImageHashes[tag] = image.Hash;
+                        }
                     }
                     }
                 }
                 }
             }
             }

+ 28 - 2
Emby.Server.Implementations/Library/LibraryManager.cs

@@ -21,6 +21,7 @@ using MediaBrowser.Common.Extensions;
 using MediaBrowser.Common.Progress;
 using MediaBrowser.Common.Progress;
 using MediaBrowser.Controller;
 using MediaBrowser.Controller;
 using MediaBrowser.Controller.Configuration;
 using MediaBrowser.Controller.Configuration;
+using MediaBrowser.Controller.Drawing;
 using MediaBrowser.Controller.Dto;
 using MediaBrowser.Controller.Dto;
 using MediaBrowser.Controller.Entities;
 using MediaBrowser.Controller.Entities;
 using MediaBrowser.Controller.Entities.Audio;
 using MediaBrowser.Controller.Entities.Audio;
@@ -35,6 +36,7 @@ using MediaBrowser.Controller.Resolvers;
 using MediaBrowser.Controller.Sorting;
 using MediaBrowser.Controller.Sorting;
 using MediaBrowser.Model.Configuration;
 using MediaBrowser.Model.Configuration;
 using MediaBrowser.Model.Dlna;
 using MediaBrowser.Model.Dlna;
+using MediaBrowser.Model.Drawing;
 using MediaBrowser.Model.Dto;
 using MediaBrowser.Model.Dto;
 using MediaBrowser.Model.Entities;
 using MediaBrowser.Model.Entities;
 using MediaBrowser.Model.IO;
 using MediaBrowser.Model.IO;
@@ -109,6 +111,18 @@ namespace Emby.Server.Implementations.Library
         /// <value>The comparers.</value>
         /// <value>The comparers.</value>
         private IBaseItemComparer[] Comparers { get; set; }
         private IBaseItemComparer[] Comparers { get; set; }
 
 
+        /// <summary>
+        /// Gets or sets the active item repository
+        /// </summary>
+        /// <value>The item repository.</value>
+        public IItemRepository ItemRepository { get; set; }
+
+        /// <summary>
+        /// Gets or sets the active image processor
+        /// </summary>
+        /// <value>The image processor.</value>
+        public IImageProcessor ImageProcessor { get; set; }
+
         /// <summary>
         /// <summary>
         /// Occurs when [item added].
         /// Occurs when [item added].
         /// </summary>
         /// </summary>
@@ -1817,7 +1831,19 @@ namespace Emby.Server.Implementations.Library
 
 
         public void UpdateImages(BaseItem item)
         public void UpdateImages(BaseItem item)
         {
         {
-            _itemRepository.SaveImages(item);
+            item.ImageInfos
+                .Where(i => (i.Width == 0 || i.Height == 0))
+                .ToList()
+                .ForEach(x =>
+                {
+                    string blurhash = ImageProcessor.GetImageHash(x.Path);
+                    ImageDimensions size = ImageProcessor.GetImageDimensions(item, x, true);
+                    x.Width = size.Width;
+                    x.Height = size.Height;
+                    x.Hash = blurhash;
+                });
+
+            ItemRepository.SaveImages(item);
 
 
             RegisterItem(item);
             RegisterItem(item);
         }
         }
@@ -1839,7 +1865,7 @@ namespace Emby.Server.Implementations.Library
 
 
                 item.DateLastSaved = DateTime.UtcNow;
                 item.DateLastSaved = DateTime.UtcNow;
 
 
-                RegisterItem(item);
+                UpdateImages(item);
             }
             }
 
 
             _itemRepository.SaveItems(itemsList, cancellationToken);
             _itemRepository.SaveItems(itemsList, cancellationToken);

+ 2 - 0
Jellyfin.Drawing.Skia/Jellyfin.Drawing.Skia.csproj

@@ -21,6 +21,8 @@
     <PackageReference Include="SkiaSharp" Version="1.68.1" />
     <PackageReference Include="SkiaSharp" Version="1.68.1" />
     <PackageReference Include="SkiaSharp.NativeAssets.Linux" Version="1.68.1" />
     <PackageReference Include="SkiaSharp.NativeAssets.Linux" Version="1.68.1" />
     <PackageReference Include="Jellyfin.SkiaSharp.NativeAssets.LinuxArm" Version="1.68.1" />
     <PackageReference Include="Jellyfin.SkiaSharp.NativeAssets.LinuxArm" Version="1.68.1" />
+
+    <PackageReference Include="Blurhash.Core" Version="*" />
   </ItemGroup>
   </ItemGroup>
 
 
   <ItemGroup>
   <ItemGroup>

+ 47 - 1
Jellyfin.Drawing.Skia/SkiaEncoder.cs

@@ -1,7 +1,11 @@
 using System;
 using System;
 using System.Collections.Generic;
 using System.Collections.Generic;
+using System.Diagnostics.CodeAnalysis;
 using System.Globalization;
 using System.Globalization;
 using System.IO;
 using System.IO;
+using System.Linq;
+using System.Threading.Tasks;
+using Blurhash.Core;
 using MediaBrowser.Common.Configuration;
 using MediaBrowser.Common.Configuration;
 using MediaBrowser.Controller.Drawing;
 using MediaBrowser.Controller.Drawing;
 using MediaBrowser.Controller.Extensions;
 using MediaBrowser.Controller.Extensions;
@@ -15,7 +19,7 @@ namespace Jellyfin.Drawing.Skia
     /// <summary>
     /// <summary>
     /// Image encoder that uses <see cref="SkiaSharp"/> to manipulate images.
     /// Image encoder that uses <see cref="SkiaSharp"/> to manipulate images.
     /// </summary>
     /// </summary>
-    public class SkiaEncoder : IImageEncoder
+    public class SkiaEncoder : CoreEncoder, IImageEncoder
     {
     {
         private static readonly HashSet<string> _transparentImageTypes
         private static readonly HashSet<string> _transparentImageTypes
             = new HashSet<string>(StringComparer.OrdinalIgnoreCase) { ".png", ".gif", ".webp" };
             = new HashSet<string>(StringComparer.OrdinalIgnoreCase) { ".png", ".gif", ".webp" };
@@ -229,6 +233,48 @@ namespace Jellyfin.Drawing.Skia
             }
             }
         }
         }
 
 
+        /// <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>
+        [SuppressMessage("Microsoft.Performance", "CA1814:PreferJaggedArraysOverMultidimensional")]
+        public string GetImageHash(string path)
+        {
+            if (path == null)
+            {
+                throw new ArgumentNullException(nameof(path));
+            }
+
+            if (!File.Exists(path))
+            {
+                throw new FileNotFoundException("File not found", path);
+            }
+
+            using (var bitmap = GetBitmap(path, false, false, null))
+            {
+                if (bitmap == null)
+                {
+                    throw new ArgumentOutOfRangeException($"Skia unable to read image {path}");
+                }
+
+                var width = bitmap.Width;
+                var height = bitmap.Height;
+                var pixels = new Pixel[width, height];
+                Parallel.ForEach(Enumerable.Range(0, height), y =>
+                {
+                    for (var x = 0; x < width; x++)
+                    {
+                        var color = bitmap.GetPixel(x, y);
+                        pixels[x, y].Red = MathUtils.SRgbToLinear(color.Red);
+                        pixels[x, y].Green = MathUtils.SRgbToLinear(color.Green);
+                        pixels[x, y].Blue = MathUtils.SRgbToLinear(color.Blue);
+                    }
+                });
+
+                return CoreEncode(pixels, 4, 4);
+            }
+        }
+
         private static bool HasDiacritics(string text)
         private static bool HasDiacritics(string text)
             => !string.Equals(text, text.RemoveDiacritics(), StringComparison.Ordinal);
             => !string.Equals(text, text.RemoveDiacritics(), StringComparison.Ordinal);
 
 

+ 6 - 1
MediaBrowser.Api/Images/ImageService.cs

@@ -323,6 +323,7 @@ namespace MediaBrowser.Api.Images
         {
         {
             int? width = null;
             int? width = null;
             int? height = null;
             int? height = null;
+            string? blurhash = null;
             long length = 0;
             long length = 0;
 
 
             try
             try
@@ -332,7 +333,10 @@ namespace MediaBrowser.Api.Images
                     var fileInfo = _fileSystem.GetFileInfo(info.Path);
                     var fileInfo = _fileSystem.GetFileInfo(info.Path);
                     length = fileInfo.Length;
                     length = fileInfo.Length;
 
 
-                    ImageDimensions size = _imageProcessor.GetImageDimensions(item, info);
+                    blurhash = _imageProcessor.GetImageHash(info.Path);
+                    info.Hash = blurhash; // TODO: this doesn't seem like the right thing to do
+
+                    ImageDimensions size = _imageProcessor.GetImageDimensions(item, info, true);
                     _libraryManager.UpdateImages(item);
                     _libraryManager.UpdateImages(item);
                     width = size.Width;
                     width = size.Width;
                     height = size.Height;
                     height = size.Height;
@@ -358,6 +362,7 @@ namespace MediaBrowser.Api.Images
                     ImageType = info.Type,
                     ImageType = info.Type,
                     ImageTag = _imageProcessor.GetImageCacheTag(item, info),
                     ImageTag = _imageProcessor.GetImageCacheTag(item, info),
                     Size = length,
                     Size = length,
+                    Hash = blurhash,
                     Width = width,
                     Width = width,
                     Height = height
                     Height = height
                 };
                 };

+ 7 - 0
MediaBrowser.Controller/Drawing/IImageEncoder.cs

@@ -43,6 +43,13 @@ namespace MediaBrowser.Controller.Drawing
         /// <returns>The image dimensions.</returns>
         /// <returns>The image dimensions.</returns>
         ImageDimensions GetImageSize(string path);
         ImageDimensions GetImageSize(string path);
 
 
+        /// <summary>
+        /// Get the blurhash of an image.
+        /// </summary>
+        /// <param name="path">The filepath of the image.</param>
+        /// <returns>The blurhash.</returns>
+        string GetImageHash(string path);
+
         /// <summary>
         /// <summary>
         /// Encode an image.
         /// Encode an image.
         /// </summary>
         /// </summary>

+ 7 - 0
MediaBrowser.Controller/Drawing/IImageProcessor.cs

@@ -32,6 +32,13 @@ namespace MediaBrowser.Controller.Drawing
         /// <returns>ImageDimensions</returns>
         /// <returns>ImageDimensions</returns>
         ImageDimensions GetImageDimensions(string path);
         ImageDimensions GetImageDimensions(string path);
 
 
+        /// <summary>
+        /// Gets the blurhash of the image.
+        /// </summary>
+        /// <param name="path">Path to the image file.</param>
+        /// <returns>BlurHash</returns>
+        String GetImageHash(string path);
+
         /// <summary>
         /// <summary>
         /// Gets the dimensions of the image.
         /// Gets the dimensions of the image.
         /// </summary>
         /// </summary>

+ 1 - 0
MediaBrowser.Controller/Entities/BaseItem.cs

@@ -2222,6 +2222,7 @@ namespace MediaBrowser.Controller.Entities
                 existingImage.DateModified = image.DateModified;
                 existingImage.DateModified = image.DateModified;
                 existingImage.Width = image.Width;
                 existingImage.Width = image.Width;
                 existingImage.Height = image.Height;
                 existingImage.Height = image.Height;
+                existingImage.Hash = image.Hash;
             }
             }
             else
             else
             {
             {

+ 6 - 0
MediaBrowser.Controller/Entities/ItemImageInfo.cs

@@ -28,6 +28,12 @@ namespace MediaBrowser.Controller.Entities
 
 
         public int Height { get; set; }
         public int Height { get; set; }
 
 
+        /// <summary>
+        /// Gets or sets the blurhash.
+        /// </summary>
+        /// <value>The blurhash.</value>
+        public string Hash { get; set; }
+
         [JsonIgnore]
         [JsonIgnore]
         public bool IsLocalFile => Path == null || !Path.StartsWith("http", StringComparison.OrdinalIgnoreCase);
         public bool IsLocalFile => Path == null || !Path.StartsWith("http", StringComparison.OrdinalIgnoreCase);
     }
     }

+ 6 - 0
MediaBrowser.Model/Dto/BaseItemDto.cs

@@ -510,6 +510,12 @@ namespace MediaBrowser.Model.Dto
         /// <value>The series thumb image tag.</value>
         /// <value>The series thumb image tag.</value>
         public string SeriesThumbImageTag { get; set; }
         public string SeriesThumbImageTag { get; set; }
 
 
+        /// <summary>
+        /// Gets or sets the blurhash for the image tags.
+        /// </summary>
+        /// <value>The blurhashes.</value>
+        public Dictionary<string, string> ImageHashes { get; set; }
+
         /// <summary>
         /// <summary>
         /// Gets or sets the series studio.
         /// Gets or sets the series studio.
         /// </summary>
         /// </summary>

+ 6 - 0
MediaBrowser.Model/Dto/ImageInfo.cs

@@ -30,6 +30,12 @@ namespace MediaBrowser.Model.Dto
         /// <value>The path.</value>
         /// <value>The path.</value>
         public string Path { get; set; }
         public string Path { get; set; }
 
 
+        /// <summary>
+        /// Gets or sets the blurhash.
+        /// </summary>
+        /// <value>The blurhash.</value>
+        public string Hash { get; set; }
+
         /// <summary>
         /// <summary>
         /// Gets or sets the height.
         /// Gets or sets the height.
         /// </summary>
         /// </summary>