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:
parent
a168040cc8
commit
7f1223016d
|
@ -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>
|
||||||
|
|
|
@ -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)
|
||||||
{
|
{
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -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);
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -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>
|
||||||
|
|
Loading…
Reference in New Issue
Block a user