Merge pull request #7946 from cvium/svg

(cherry picked from commit 4ebe70cf6a12be4f4eae0b815a269a483ee238bb)
Signed-off-by: Joshua Boniface <joshua@boniface.me>
This commit is contained in:
Cody Robibero 2022-06-16 07:14:56 -06:00 committed by Joshua Boniface
parent a168040cc8
commit 7f1223016d
6 changed files with 95 additions and 65 deletions

View File

@ -395,7 +395,13 @@ namespace Emby.Drawing
public string GetImageBlurHash(string path) public string GetImageBlurHash(string path)
{ {
var size = GetImageDimensions(path); var size = GetImageDimensions(path);
if (size.Width <= 0 || size.Height <= 0) return GetImageBlurHash(path, size);
}
/// <inheritdoc />
public string GetImageBlurHash(string path, ImageDimensions imageDimensions)
{
if (imageDimensions.Width <= 0 || imageDimensions.Height <= 0)
{ {
return string.Empty; 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. // 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. // 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 // 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 xCompF = MathF.Sqrt(16.0f * imageDimensions.Width / imageDimensions.Height);
float yCompF = xCompF * size.Height / size.Width; float yCompF = xCompF * imageDimensions.Height / imageDimensions.Width;
int xComp = Math.Min((int)xCompF + 1, 9); int xComp = Math.Min((int)xCompF + 1, 9);
int yComp = Math.Min((int)yCompF + 1, 9); int yComp = Math.Min((int)yCompF + 1, 9);
@ -439,47 +445,46 @@ namespace Emby.Drawing
.ToString("N", CultureInfo.InvariantCulture); .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) var inputFormat = Path.GetExtension(originalImagePath.AsSpan()).TrimStart('.').ToString();
.TrimStart('.')
.Replace("jpeg", "jpg", StringComparison.OrdinalIgnoreCase);
// These are just jpg files renamed as tbn // These are just jpg files renamed as tbn
if (string.Equals(inputFormat, "tbn", StringComparison.OrdinalIgnoreCase)) if (string.Equals(inputFormat, "tbn", StringComparison.OrdinalIgnoreCase))
{ {
return (originalImagePath, dateModified); return Task.FromResult((originalImagePath, dateModified));
} }
if (!_imageEncoder.SupportedInputFormats.Contains(inputFormat)) // TODO _mediaEncoder.ConvertImage is not implemented
{ // if (!_imageEncoder.SupportedInputFormats.Contains(inputFormat))
try // {
{ // try
string filename = (originalImagePath + dateModified.Ticks.ToString(CultureInfo.InvariantCulture)).GetMD5().ToString("N", CultureInfo.InvariantCulture); // {
// 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"; return Task.FromResult((originalImagePath, dateModified));
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);
} }
/// <summary> /// <summary>

View File

@ -1860,7 +1860,9 @@ namespace Emby.Server.Implementations.Library
throw new ArgumentNullException(nameof(item)); 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 // Skip image processing if current or live tv source
if (outdated.Length == 0 || item.SourceType != SourceType.Library) 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); _logger.LogWarning("Cannot get image index for {ImagePath}", img.Path);
continue; 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); _logger.LogWarning(ex, "Cannot fetch image from {ImagePath}", img.Path);
continue; continue;
@ -1895,23 +1897,24 @@ namespace Emby.Server.Implementations.Library
} }
} }
ImageDimensions size;
try try
{ {
ImageDimensions size = _imageProcessor.GetImageDimensions(item, image); size = _imageProcessor.GetImageDimensions(item, image);
image.Width = size.Width; image.Width = size.Width;
image.Height = size.Height; image.Height = size.Height;
} }
catch (Exception ex) catch (Exception ex)
{ {
_logger.LogError(ex, "Cannot get image dimensions for {ImagePath}", image.Path); _logger.LogError(ex, "Cannot get image dimensions for {ImagePath}", image.Path);
size = new ImageDimensions(0, 0);
image.Width = 0; image.Width = 0;
image.Height = 0; image.Height = 0;
continue;
} }
try try
{ {
image.BlurHash = _imageProcessor.GetImageBlurHash(image.Path); image.BlurHash = _imageProcessor.GetImageBlurHash(image.Path, size);
} }
catch (Exception ex) catch (Exception ex)
{ {

View File

@ -20,6 +20,7 @@
<PackageReference Include="BlurHashSharp.SkiaSharp" Version="1.2.0" /> <PackageReference Include="BlurHashSharp.SkiaSharp" Version="1.2.0" />
<PackageReference Include="SkiaSharp" Version="2.88.1-preview.1" /> <PackageReference Include="SkiaSharp" Version="2.88.1-preview.1" />
<PackageReference Include="SkiaSharp.NativeAssets.Linux" Version="2.88.1-preview.1" /> <PackageReference Include="SkiaSharp.NativeAssets.Linux" Version="2.88.1-preview.1" />
<PackageReference Include="SkiaSharp.Svg" Version="1.60.0" />
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>

View File

@ -10,7 +10,7 @@ using MediaBrowser.Controller.Drawing;
using MediaBrowser.Model.Drawing; using MediaBrowser.Model.Drawing;
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
using SkiaSharp; using SkiaSharp;
using static Jellyfin.Drawing.Skia.SkiaHelper; using SKSvg = SkiaSharp.Extended.Svg.SKSvg;
namespace Jellyfin.Drawing.Skia namespace Jellyfin.Drawing.Skia
{ {
@ -19,8 +19,7 @@ namespace Jellyfin.Drawing.Skia
/// </summary> /// </summary>
public class SkiaEncoder : IImageEncoder public class SkiaEncoder : IImageEncoder
{ {
private static readonly HashSet<string> _transparentImageTypes private static readonly HashSet<string> _transparentImageTypes = new(StringComparer.OrdinalIgnoreCase) { ".png", ".gif", ".webp" };
= new HashSet<string>(StringComparer.OrdinalIgnoreCase) { ".png", ".gif", ".webp" };
private readonly ILogger<SkiaEncoder> _logger; private readonly ILogger<SkiaEncoder> _logger;
private readonly IApplicationPaths _appPaths; private readonly IApplicationPaths _appPaths;
@ -71,7 +70,7 @@ namespace Jellyfin.Drawing.Skia
/// <inheritdoc/> /// <inheritdoc/>
public IReadOnlyCollection<ImageFormat> SupportedOutputFormats public IReadOnlyCollection<ImageFormat> SupportedOutputFormats
=> new HashSet<ImageFormat>() { ImageFormat.Webp, ImageFormat.Jpg, ImageFormat.Png }; => new HashSet<ImageFormat> { ImageFormat.Webp, ImageFormat.Jpg, ImageFormat.Png };
/// <summary> /// <summary>
/// Check if the native lib is available. /// Check if the native lib is available.
@ -109,9 +108,7 @@ namespace Jellyfin.Drawing.Skia
} }
/// <inheritdoc /> /// <inheritdoc />
/// <exception cref="ArgumentNullException">The path is null.</exception>
/// <exception cref="FileNotFoundException">The path is not valid.</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 ImageDimensions GetImageSize(string path) public ImageDimensions GetImageSize(string path)
{ {
if (!File.Exists(path)) if (!File.Exists(path))
@ -119,12 +116,27 @@ namespace Jellyfin.Drawing.Skia
throw new FileNotFoundException("File not found", path); 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); using var codec = SKCodec.Create(path, out SKCodecResult result);
EnsureSuccess(result); switch (result)
{
var info = codec.Info; case SKCodecResult.Success:
var info = codec.Info;
return new ImageDimensions(info.Width, info.Height); 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);
}
} }
/// <inheritdoc /> /// <inheritdoc />
@ -138,6 +150,13 @@ namespace Jellyfin.Drawing.Skia
throw new ArgumentNullException(nameof(path)); 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 // Any larger than 128x128 is too slow and there's no visually discernible difference
return BlurHashEncoder.Encode(xComp, yComp, path, 128, 128); 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)); 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 skiaOutputFormat = GetImageFormat(outputFormat);
var hasBackgroundColor = !string.IsNullOrWhiteSpace(options.BackgroundColor); var hasBackgroundColor = !string.IsNullOrWhiteSpace(options.BackgroundColor);

View File

@ -8,19 +8,6 @@ namespace Jellyfin.Drawing.Skia
/// </summary> /// </summary>
public static class SkiaHelper public static class SkiaHelper
{ {
/// <summary>
/// Ensures the result is a success
/// by throwing an exception when that's not the case.
/// </summary>
/// <param name="result">The result returned by Skia.</param>
public static void EnsureSuccess(SKCodecResult result)
{
if (result != SKCodecResult.Success)
{
throw new SkiaCodecException(result);
}
}
/// <summary> /// <summary>
/// Gets the next valid image as a bitmap. /// Gets the next valid image as a bitmap.
/// </summary> /// </summary>

View File

@ -50,6 +50,14 @@ namespace MediaBrowser.Controller.Drawing
/// <returns>BlurHash.</returns> /// <returns>BlurHash.</returns>
string GetImageBlurHash(string path); string GetImageBlurHash(string path);
/// <summary>
/// Gets the blurhash of the image.
/// </summary>
/// <param name="path">Path to the image file.</param>
/// <param name="imageDimensions">The image dimensions.</param>
/// <returns>BlurHash.</returns>
string GetImageBlurHash(string path, ImageDimensions imageDimensions);
/// <summary> /// <summary>
/// Gets the image cache tag. /// Gets the image cache tag.
/// </summary> /// </summary>