Add BlurHash support to backend

This commit is contained in:
Jesús Higueras 2020-03-23 20:05:49 +01:00 committed by Vasily
parent 2d2c1d9473
commit b9fc0d2628
14 changed files with 149 additions and 5 deletions

View File

@ -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);

View File

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

View File

@ -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;

View File

@ -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;
}
} }
} }
} }

View File

@ -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);

View File

@ -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>

View File

@ -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);

View File

@ -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
}; };

View File

@ -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>

View File

@ -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>

View File

@ -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
{ {

View File

@ -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);
} }

View File

@ -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>

View File

@ -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>