Merge pull request #9065 from barronpm/drawing-use-file-namespaces
This commit is contained in:
commit
515e69dcf7
|
@ -2,35 +2,34 @@ using System;
|
|||
using MediaBrowser.Model.Drawing;
|
||||
using SkiaSharp;
|
||||
|
||||
namespace Jellyfin.Drawing.Skia
|
||||
namespace Jellyfin.Drawing.Skia;
|
||||
|
||||
/// <summary>
|
||||
/// Static helper class used to draw percentage-played indicators on images.
|
||||
/// </summary>
|
||||
public static class PercentPlayedDrawer
|
||||
{
|
||||
private const int IndicatorHeight = 8;
|
||||
|
||||
/// <summary>
|
||||
/// Static helper class used to draw percentage-played indicators on images.
|
||||
/// Draw a percentage played indicator on a canvas.
|
||||
/// </summary>
|
||||
public static class PercentPlayedDrawer
|
||||
/// <param name="canvas">The canvas to draw the indicator on.</param>
|
||||
/// <param name="imageSize">The size of the image being drawn on.</param>
|
||||
/// <param name="percent">The percentage played to display with the indicator.</param>
|
||||
public static void Process(SKCanvas canvas, ImageDimensions imageSize, double percent)
|
||||
{
|
||||
private const int IndicatorHeight = 8;
|
||||
using var paint = new SKPaint();
|
||||
var endX = imageSize.Width - 1;
|
||||
var endY = imageSize.Height - 1;
|
||||
|
||||
/// <summary>
|
||||
/// Draw a percentage played indicator on a canvas.
|
||||
/// </summary>
|
||||
/// <param name="canvas">The canvas to draw the indicator on.</param>
|
||||
/// <param name="imageSize">The size of the image being drawn on.</param>
|
||||
/// <param name="percent">The percentage played to display with the indicator.</param>
|
||||
public static void Process(SKCanvas canvas, ImageDimensions imageSize, double percent)
|
||||
{
|
||||
using var paint = new SKPaint();
|
||||
var endX = imageSize.Width - 1;
|
||||
var endY = imageSize.Height - 1;
|
||||
paint.Color = SKColor.Parse("#99000000");
|
||||
paint.Style = SKPaintStyle.Fill;
|
||||
canvas.DrawRect(SKRect.Create(0, (float)endY - IndicatorHeight, endX, endY), paint);
|
||||
|
||||
paint.Color = SKColor.Parse("#99000000");
|
||||
paint.Style = SKPaintStyle.Fill;
|
||||
canvas.DrawRect(SKRect.Create(0, (float)endY - IndicatorHeight, endX, endY), paint);
|
||||
double foregroundWidth = (endX * percent) / 100;
|
||||
|
||||
double foregroundWidth = (endX * percent) / 100;
|
||||
|
||||
paint.Color = SKColor.Parse("#FF00A4DC");
|
||||
canvas.DrawRect(SKRect.Create(0, (float)endY - IndicatorHeight, Convert.ToInt32(foregroundWidth), endY), paint);
|
||||
}
|
||||
paint.Color = SKColor.Parse("#FF00A4DC");
|
||||
canvas.DrawRect(SKRect.Create(0, (float)endY - IndicatorHeight, Convert.ToInt32(foregroundWidth), endY), paint);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,48 +1,47 @@
|
|||
using MediaBrowser.Model.Drawing;
|
||||
using SkiaSharp;
|
||||
|
||||
namespace Jellyfin.Drawing.Skia
|
||||
namespace Jellyfin.Drawing.Skia;
|
||||
|
||||
/// <summary>
|
||||
/// Static helper class for drawing 'played' indicators.
|
||||
/// </summary>
|
||||
public static class PlayedIndicatorDrawer
|
||||
{
|
||||
private const int OffsetFromTopRightCorner = 38;
|
||||
|
||||
/// <summary>
|
||||
/// Static helper class for drawing 'played' indicators.
|
||||
/// Draw a 'played' indicator in the top right corner of a canvas.
|
||||
/// </summary>
|
||||
public static class PlayedIndicatorDrawer
|
||||
/// <param name="canvas">The canvas to draw the indicator on.</param>
|
||||
/// <param name="imageSize">
|
||||
/// The dimensions of the image to draw the indicator on. The width is used to determine the x-position of the
|
||||
/// indicator.
|
||||
/// </param>
|
||||
public static void DrawPlayedIndicator(SKCanvas canvas, ImageDimensions imageSize)
|
||||
{
|
||||
private const int OffsetFromTopRightCorner = 38;
|
||||
var x = imageSize.Width - OffsetFromTopRightCorner;
|
||||
|
||||
/// <summary>
|
||||
/// Draw a 'played' indicator in the top right corner of a canvas.
|
||||
/// </summary>
|
||||
/// <param name="canvas">The canvas to draw the indicator on.</param>
|
||||
/// <param name="imageSize">
|
||||
/// The dimensions of the image to draw the indicator on. The width is used to determine the x-position of the
|
||||
/// indicator.
|
||||
/// </param>
|
||||
public static void DrawPlayedIndicator(SKCanvas canvas, ImageDimensions imageSize)
|
||||
using var paint = new SKPaint
|
||||
{
|
||||
var x = imageSize.Width - OffsetFromTopRightCorner;
|
||||
Color = SKColor.Parse("#CC00A4DC"),
|
||||
Style = SKPaintStyle.Fill
|
||||
};
|
||||
|
||||
using var paint = new SKPaint
|
||||
{
|
||||
Color = SKColor.Parse("#CC00A4DC"),
|
||||
Style = SKPaintStyle.Fill
|
||||
};
|
||||
canvas.DrawCircle(x, OffsetFromTopRightCorner, 20, paint);
|
||||
|
||||
canvas.DrawCircle(x, OffsetFromTopRightCorner, 20, paint);
|
||||
paint.Color = new SKColor(255, 255, 255, 255);
|
||||
paint.TextSize = 30;
|
||||
paint.IsAntialias = true;
|
||||
|
||||
paint.Color = new SKColor(255, 255, 255, 255);
|
||||
paint.TextSize = 30;
|
||||
paint.IsAntialias = true;
|
||||
// or:
|
||||
// var emojiChar = 0x1F680;
|
||||
const string Text = "✔️";
|
||||
var emojiChar = StringUtilities.GetUnicodeCharacterCode(Text, SKTextEncoding.Utf32);
|
||||
|
||||
// or:
|
||||
// var emojiChar = 0x1F680;
|
||||
const string Text = "✔️";
|
||||
var emojiChar = StringUtilities.GetUnicodeCharacterCode(Text, SKTextEncoding.Utf32);
|
||||
// ask the font manager for a font with that character
|
||||
paint.Typeface = SKFontManager.Default.MatchCharacter(emojiChar);
|
||||
|
||||
// ask the font manager for a font with that character
|
||||
paint.Typeface = SKFontManager.Default.MatchCharacter(emojiChar);
|
||||
|
||||
canvas.DrawText(Text, (float)x - 12, OffsetFromTopRightCorner + 12, paint);
|
||||
}
|
||||
canvas.DrawText(Text, (float)x - 12, OffsetFromTopRightCorner + 12, paint);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,45 +1,44 @@
|
|||
using System.Globalization;
|
||||
using SkiaSharp;
|
||||
|
||||
namespace Jellyfin.Drawing.Skia
|
||||
namespace Jellyfin.Drawing.Skia;
|
||||
|
||||
/// <summary>
|
||||
/// Represents errors that occur during interaction with Skia codecs.
|
||||
/// </summary>
|
||||
public class SkiaCodecException : SkiaException
|
||||
{
|
||||
/// <summary>
|
||||
/// Represents errors that occur during interaction with Skia codecs.
|
||||
/// Initializes a new instance of the <see cref="SkiaCodecException" /> class.
|
||||
/// </summary>
|
||||
public class SkiaCodecException : SkiaException
|
||||
/// <param name="result">The non-successful codec result returned by Skia.</param>
|
||||
public SkiaCodecException(SKCodecResult result)
|
||||
{
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="SkiaCodecException" /> class.
|
||||
/// </summary>
|
||||
/// <param name="result">The non-successful codec result returned by Skia.</param>
|
||||
public SkiaCodecException(SKCodecResult result)
|
||||
{
|
||||
CodecResult = result;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="SkiaCodecException" /> class
|
||||
/// with a specified error message.
|
||||
/// </summary>
|
||||
/// <param name="result">The non-successful codec result returned by Skia.</param>
|
||||
/// <param name="message">The message that describes the error.</param>
|
||||
public SkiaCodecException(SKCodecResult result, string message)
|
||||
: base(message)
|
||||
{
|
||||
CodecResult = result;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the non-successful codec result returned by Skia.
|
||||
/// </summary>
|
||||
public SKCodecResult CodecResult { get; }
|
||||
|
||||
/// <inheritdoc />
|
||||
public override string ToString()
|
||||
=> string.Format(
|
||||
CultureInfo.InvariantCulture,
|
||||
"Non-success codec result: {0}\n{1}",
|
||||
CodecResult,
|
||||
base.ToString());
|
||||
CodecResult = result;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="SkiaCodecException" /> class
|
||||
/// with a specified error message.
|
||||
/// </summary>
|
||||
/// <param name="result">The non-successful codec result returned by Skia.</param>
|
||||
/// <param name="message">The message that describes the error.</param>
|
||||
public SkiaCodecException(SKCodecResult result, string message)
|
||||
: base(message)
|
||||
{
|
||||
CodecResult = result;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the non-successful codec result returned by Skia.
|
||||
/// </summary>
|
||||
public SKCodecResult CodecResult { get; }
|
||||
|
||||
/// <inheritdoc />
|
||||
public override string ToString()
|
||||
=> string.Format(
|
||||
CultureInfo.InvariantCulture,
|
||||
"Non-success codec result: {0}\n{1}",
|
||||
CodecResult,
|
||||
base.ToString());
|
||||
}
|
||||
|
|
|
@ -12,534 +12,533 @@ using Microsoft.Extensions.Logging;
|
|||
using SkiaSharp;
|
||||
using SKSvg = SkiaSharp.Extended.Svg.SKSvg;
|
||||
|
||||
namespace Jellyfin.Drawing.Skia
|
||||
namespace Jellyfin.Drawing.Skia;
|
||||
|
||||
/// <summary>
|
||||
/// Image encoder that uses <see cref="SkiaSharp"/> to manipulate images.
|
||||
/// </summary>
|
||||
public class SkiaEncoder : IImageEncoder
|
||||
{
|
||||
private static readonly HashSet<string> _transparentImageTypes = new(StringComparer.OrdinalIgnoreCase) { ".png", ".gif", ".webp" };
|
||||
|
||||
private readonly ILogger<SkiaEncoder> _logger;
|
||||
private readonly IApplicationPaths _appPaths;
|
||||
|
||||
/// <summary>
|
||||
/// Image encoder that uses <see cref="SkiaSharp"/> to manipulate images.
|
||||
/// Initializes a new instance of the <see cref="SkiaEncoder"/> class.
|
||||
/// </summary>
|
||||
public class SkiaEncoder : IImageEncoder
|
||||
/// <param name="logger">The application logger.</param>
|
||||
/// <param name="appPaths">The application paths.</param>
|
||||
public SkiaEncoder(ILogger<SkiaEncoder> logger, IApplicationPaths appPaths)
|
||||
{
|
||||
private static readonly HashSet<string> _transparentImageTypes = new(StringComparer.OrdinalIgnoreCase) { ".png", ".gif", ".webp" };
|
||||
_logger = logger;
|
||||
_appPaths = appPaths;
|
||||
}
|
||||
|
||||
private readonly ILogger<SkiaEncoder> _logger;
|
||||
private readonly IApplicationPaths _appPaths;
|
||||
/// <inheritdoc/>
|
||||
public string Name => "Skia";
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="SkiaEncoder"/> class.
|
||||
/// </summary>
|
||||
/// <param name="logger">The application logger.</param>
|
||||
/// <param name="appPaths">The application paths.</param>
|
||||
public SkiaEncoder(ILogger<SkiaEncoder> logger, IApplicationPaths appPaths)
|
||||
/// <inheritdoc/>
|
||||
public bool SupportsImageCollageCreation => true;
|
||||
|
||||
/// <inheritdoc/>
|
||||
public bool SupportsImageEncoding => true;
|
||||
|
||||
/// <inheritdoc/>
|
||||
public IReadOnlyCollection<string> SupportedInputFormats =>
|
||||
new HashSet<string>(StringComparer.OrdinalIgnoreCase)
|
||||
{
|
||||
_logger = logger;
|
||||
_appPaths = appPaths;
|
||||
"jpeg",
|
||||
"jpg",
|
||||
"png",
|
||||
"dng",
|
||||
"webp",
|
||||
"gif",
|
||||
"bmp",
|
||||
"ico",
|
||||
"astc",
|
||||
"ktx",
|
||||
"pkm",
|
||||
"wbmp",
|
||||
// TODO: check if these are supported on multiple platforms
|
||||
// https://github.com/google/skia/blob/master/infra/bots/recipes/test.py#L454
|
||||
// working on windows at least
|
||||
"cr2",
|
||||
"nef",
|
||||
"arw"
|
||||
};
|
||||
|
||||
/// <inheritdoc/>
|
||||
public IReadOnlyCollection<ImageFormat> SupportedOutputFormats
|
||||
=> new HashSet<ImageFormat> { ImageFormat.Webp, ImageFormat.Jpg, ImageFormat.Png };
|
||||
|
||||
/// <summary>
|
||||
/// Check if the native lib is available.
|
||||
/// </summary>
|
||||
/// <returns>True if the native lib is available, otherwise false.</returns>
|
||||
public static bool IsNativeLibAvailable()
|
||||
{
|
||||
try
|
||||
{
|
||||
// test an operation that requires the native library
|
||||
SKPMColor.PreMultiply(SKColors.Black);
|
||||
return true;
|
||||
}
|
||||
catch (Exception)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Convert a <see cref="ImageFormat"/> to a <see cref="SKEncodedImageFormat"/>.
|
||||
/// </summary>
|
||||
/// <param name="selectedFormat">The format to convert.</param>
|
||||
/// <returns>The converted format.</returns>
|
||||
public static SKEncodedImageFormat GetImageFormat(ImageFormat selectedFormat)
|
||||
{
|
||||
return selectedFormat switch
|
||||
{
|
||||
ImageFormat.Bmp => SKEncodedImageFormat.Bmp,
|
||||
ImageFormat.Jpg => SKEncodedImageFormat.Jpeg,
|
||||
ImageFormat.Gif => SKEncodedImageFormat.Gif,
|
||||
ImageFormat.Webp => SKEncodedImageFormat.Webp,
|
||||
_ => SKEncodedImageFormat.Png
|
||||
};
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
/// <exception cref="FileNotFoundException">The path is not valid.</exception>
|
||||
public ImageDimensions GetImageSize(string path)
|
||||
{
|
||||
if (!File.Exists(path))
|
||||
{
|
||||
throw new FileNotFoundException("File not found", path);
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public string Name => "Skia";
|
||||
|
||||
/// <inheritdoc/>
|
||||
public bool SupportsImageCollageCreation => true;
|
||||
|
||||
/// <inheritdoc/>
|
||||
public bool SupportsImageEncoding => true;
|
||||
|
||||
/// <inheritdoc/>
|
||||
public IReadOnlyCollection<string> SupportedInputFormats =>
|
||||
new HashSet<string>(StringComparer.OrdinalIgnoreCase)
|
||||
{
|
||||
"jpeg",
|
||||
"jpg",
|
||||
"png",
|
||||
"dng",
|
||||
"webp",
|
||||
"gif",
|
||||
"bmp",
|
||||
"ico",
|
||||
"astc",
|
||||
"ktx",
|
||||
"pkm",
|
||||
"wbmp",
|
||||
// TODO: check if these are supported on multiple platforms
|
||||
// https://github.com/google/skia/blob/master/infra/bots/recipes/test.py#L454
|
||||
// working on windows at least
|
||||
"cr2",
|
||||
"nef",
|
||||
"arw"
|
||||
};
|
||||
|
||||
/// <inheritdoc/>
|
||||
public IReadOnlyCollection<ImageFormat> SupportedOutputFormats
|
||||
=> new HashSet<ImageFormat> { ImageFormat.Webp, ImageFormat.Jpg, ImageFormat.Png };
|
||||
|
||||
/// <summary>
|
||||
/// Check if the native lib is available.
|
||||
/// </summary>
|
||||
/// <returns>True if the native lib is available, otherwise false.</returns>
|
||||
public static bool IsNativeLibAvailable()
|
||||
var extension = Path.GetExtension(path.AsSpan());
|
||||
if (extension.Equals(".svg", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
try
|
||||
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);
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
/// <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>
|
||||
public string GetImageBlurHash(int xComp, int yComp, string path)
|
||||
{
|
||||
ArgumentException.ThrowIfNullOrEmpty(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);
|
||||
}
|
||||
|
||||
private bool RequiresSpecialCharacterHack(string path)
|
||||
{
|
||||
for (int i = 0; i < path.Length; i++)
|
||||
{
|
||||
if (char.GetUnicodeCategory(path[i]) == UnicodeCategory.OtherLetter)
|
||||
{
|
||||
// test an operation that requires the native library
|
||||
SKPMColor.PreMultiply(SKColors.Black);
|
||||
return true;
|
||||
}
|
||||
catch (Exception)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Convert a <see cref="ImageFormat"/> to a <see cref="SKEncodedImageFormat"/>.
|
||||
/// </summary>
|
||||
/// <param name="selectedFormat">The format to convert.</param>
|
||||
/// <returns>The converted format.</returns>
|
||||
public static SKEncodedImageFormat GetImageFormat(ImageFormat selectedFormat)
|
||||
return path.HasDiacritics();
|
||||
}
|
||||
|
||||
private string NormalizePath(string path)
|
||||
{
|
||||
if (!RequiresSpecialCharacterHack(path))
|
||||
{
|
||||
return selectedFormat switch
|
||||
{
|
||||
ImageFormat.Bmp => SKEncodedImageFormat.Bmp,
|
||||
ImageFormat.Jpg => SKEncodedImageFormat.Jpeg,
|
||||
ImageFormat.Gif => SKEncodedImageFormat.Gif,
|
||||
ImageFormat.Webp => SKEncodedImageFormat.Webp,
|
||||
_ => SKEncodedImageFormat.Png
|
||||
};
|
||||
return path;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
/// <exception cref="FileNotFoundException">The path is not valid.</exception>
|
||||
public ImageDimensions GetImageSize(string path)
|
||||
var tempPath = Path.Combine(_appPaths.TempDirectory, Guid.NewGuid() + Path.GetExtension(path));
|
||||
var directory = Path.GetDirectoryName(tempPath) ?? throw new ResourceNotFoundException($"Provided path ({tempPath}) is not valid.");
|
||||
Directory.CreateDirectory(directory);
|
||||
File.Copy(path, tempPath, true);
|
||||
|
||||
return tempPath;
|
||||
}
|
||||
|
||||
private static SKEncodedOrigin GetSKEncodedOrigin(ImageOrientation? orientation)
|
||||
{
|
||||
if (!orientation.HasValue)
|
||||
{
|
||||
if (!File.Exists(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);
|
||||
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);
|
||||
}
|
||||
return SKEncodedOrigin.TopLeft;
|
||||
}
|
||||
|
||||
/// <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>
|
||||
public string GetImageBlurHash(int xComp, int yComp, string path)
|
||||
return orientation.Value switch
|
||||
{
|
||||
ArgumentException.ThrowIfNullOrEmpty(path);
|
||||
ImageOrientation.TopRight => SKEncodedOrigin.TopRight,
|
||||
ImageOrientation.RightTop => SKEncodedOrigin.RightTop,
|
||||
ImageOrientation.RightBottom => SKEncodedOrigin.RightBottom,
|
||||
ImageOrientation.LeftTop => SKEncodedOrigin.LeftTop,
|
||||
ImageOrientation.LeftBottom => SKEncodedOrigin.LeftBottom,
|
||||
ImageOrientation.BottomRight => SKEncodedOrigin.BottomRight,
|
||||
ImageOrientation.BottomLeft => SKEncodedOrigin.BottomLeft,
|
||||
_ => SKEncodedOrigin.TopLeft
|
||||
};
|
||||
}
|
||||
|
||||
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);
|
||||
/// <summary>
|
||||
/// Decode an image.
|
||||
/// </summary>
|
||||
/// <param name="path">The filepath of the image to decode.</param>
|
||||
/// <param name="forceCleanBitmap">Whether to force clean the bitmap.</param>
|
||||
/// <param name="orientation">The orientation of the image.</param>
|
||||
/// <param name="origin">The detected origin of the image.</param>
|
||||
/// <returns>The resulting bitmap of the image.</returns>
|
||||
internal SKBitmap? Decode(string path, bool forceCleanBitmap, ImageOrientation? orientation, out SKEncodedOrigin origin)
|
||||
{
|
||||
if (!File.Exists(path))
|
||||
{
|
||||
throw new FileNotFoundException("File not found", path);
|
||||
}
|
||||
|
||||
private bool RequiresSpecialCharacterHack(string path)
|
||||
var requiresTransparencyHack = _transparentImageTypes.Contains(Path.GetExtension(path));
|
||||
|
||||
if (requiresTransparencyHack || forceCleanBitmap)
|
||||
{
|
||||
for (int i = 0; i < path.Length; i++)
|
||||
using SKCodec codec = SKCodec.Create(NormalizePath(path), out SKCodecResult res);
|
||||
if (res != SKCodecResult.Success)
|
||||
{
|
||||
if (char.GetUnicodeCategory(path[i]) == UnicodeCategory.OtherLetter)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
origin = GetSKEncodedOrigin(orientation);
|
||||
return null;
|
||||
}
|
||||
|
||||
return path.HasDiacritics();
|
||||
// create the bitmap
|
||||
var bitmap = new SKBitmap(codec.Info.Width, codec.Info.Height, !requiresTransparencyHack);
|
||||
|
||||
// decode
|
||||
_ = codec.GetPixels(bitmap.Info, bitmap.GetPixels());
|
||||
|
||||
origin = codec.EncodedOrigin;
|
||||
|
||||
return bitmap;
|
||||
}
|
||||
|
||||
private string NormalizePath(string path)
|
||||
var resultBitmap = SKBitmap.Decode(NormalizePath(path));
|
||||
|
||||
if (resultBitmap is null)
|
||||
{
|
||||
if (!RequiresSpecialCharacterHack(path))
|
||||
{
|
||||
return path;
|
||||
}
|
||||
|
||||
var tempPath = Path.Combine(_appPaths.TempDirectory, Guid.NewGuid() + Path.GetExtension(path));
|
||||
var directory = Path.GetDirectoryName(tempPath) ?? throw new ResourceNotFoundException($"Provided path ({tempPath}) is not valid.");
|
||||
Directory.CreateDirectory(directory);
|
||||
File.Copy(path, tempPath, true);
|
||||
|
||||
return tempPath;
|
||||
return Decode(path, true, orientation, out origin);
|
||||
}
|
||||
|
||||
private static SKEncodedOrigin GetSKEncodedOrigin(ImageOrientation? orientation)
|
||||
// If we have to resize these they often end up distorted
|
||||
if (resultBitmap.ColorType == SKColorType.Gray8)
|
||||
{
|
||||
if (!orientation.HasValue)
|
||||
{
|
||||
return SKEncodedOrigin.TopLeft;
|
||||
}
|
||||
|
||||
return orientation.Value switch
|
||||
{
|
||||
ImageOrientation.TopRight => SKEncodedOrigin.TopRight,
|
||||
ImageOrientation.RightTop => SKEncodedOrigin.RightTop,
|
||||
ImageOrientation.RightBottom => SKEncodedOrigin.RightBottom,
|
||||
ImageOrientation.LeftTop => SKEncodedOrigin.LeftTop,
|
||||
ImageOrientation.LeftBottom => SKEncodedOrigin.LeftBottom,
|
||||
ImageOrientation.BottomRight => SKEncodedOrigin.BottomRight,
|
||||
ImageOrientation.BottomLeft => SKEncodedOrigin.BottomLeft,
|
||||
_ => SKEncodedOrigin.TopLeft
|
||||
};
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Decode an image.
|
||||
/// </summary>
|
||||
/// <param name="path">The filepath of the image to decode.</param>
|
||||
/// <param name="forceCleanBitmap">Whether to force clean the bitmap.</param>
|
||||
/// <param name="orientation">The orientation of the image.</param>
|
||||
/// <param name="origin">The detected origin of the image.</param>
|
||||
/// <returns>The resulting bitmap of the image.</returns>
|
||||
internal SKBitmap? Decode(string path, bool forceCleanBitmap, ImageOrientation? orientation, out SKEncodedOrigin origin)
|
||||
{
|
||||
if (!File.Exists(path))
|
||||
{
|
||||
throw new FileNotFoundException("File not found", path);
|
||||
}
|
||||
|
||||
var requiresTransparencyHack = _transparentImageTypes.Contains(Path.GetExtension(path));
|
||||
|
||||
if (requiresTransparencyHack || forceCleanBitmap)
|
||||
{
|
||||
using SKCodec codec = SKCodec.Create(NormalizePath(path), out SKCodecResult res);
|
||||
if (res != SKCodecResult.Success)
|
||||
{
|
||||
origin = GetSKEncodedOrigin(orientation);
|
||||
return null;
|
||||
}
|
||||
|
||||
// create the bitmap
|
||||
var bitmap = new SKBitmap(codec.Info.Width, codec.Info.Height, !requiresTransparencyHack);
|
||||
|
||||
// decode
|
||||
_ = codec.GetPixels(bitmap.Info, bitmap.GetPixels());
|
||||
|
||||
origin = codec.EncodedOrigin;
|
||||
|
||||
return bitmap;
|
||||
}
|
||||
|
||||
var resultBitmap = SKBitmap.Decode(NormalizePath(path));
|
||||
|
||||
if (resultBitmap is null)
|
||||
using (resultBitmap)
|
||||
{
|
||||
return Decode(path, true, orientation, out origin);
|
||||
}
|
||||
}
|
||||
|
||||
// If we have to resize these they often end up distorted
|
||||
if (resultBitmap.ColorType == SKColorType.Gray8)
|
||||
origin = SKEncodedOrigin.TopLeft;
|
||||
return resultBitmap;
|
||||
}
|
||||
|
||||
private SKBitmap? GetBitmap(string path, bool autoOrient, ImageOrientation? orientation)
|
||||
{
|
||||
if (autoOrient)
|
||||
{
|
||||
var bitmap = Decode(path, true, orientation, out var origin);
|
||||
|
||||
if (bitmap is not null && origin != SKEncodedOrigin.TopLeft)
|
||||
{
|
||||
using (resultBitmap)
|
||||
using (bitmap)
|
||||
{
|
||||
return Decode(path, true, orientation, out origin);
|
||||
return OrientImage(bitmap, origin);
|
||||
}
|
||||
}
|
||||
|
||||
origin = SKEncodedOrigin.TopLeft;
|
||||
return resultBitmap;
|
||||
return bitmap;
|
||||
}
|
||||
|
||||
private SKBitmap? GetBitmap(string path, bool autoOrient, ImageOrientation? orientation)
|
||||
return Decode(path, false, orientation, out _);
|
||||
}
|
||||
|
||||
private SKBitmap OrientImage(SKBitmap bitmap, SKEncodedOrigin origin)
|
||||
{
|
||||
var needsFlip = origin == SKEncodedOrigin.LeftBottom
|
||||
|| origin == SKEncodedOrigin.LeftTop
|
||||
|| origin == SKEncodedOrigin.RightBottom
|
||||
|| origin == SKEncodedOrigin.RightTop;
|
||||
var rotated = needsFlip
|
||||
? new SKBitmap(bitmap.Height, bitmap.Width)
|
||||
: new SKBitmap(bitmap.Width, bitmap.Height);
|
||||
using var surface = new SKCanvas(rotated);
|
||||
var midX = (float)rotated.Width / 2;
|
||||
var midY = (float)rotated.Height / 2;
|
||||
|
||||
switch (origin)
|
||||
{
|
||||
if (autoOrient)
|
||||
{
|
||||
var bitmap = Decode(path, true, orientation, out var origin);
|
||||
|
||||
if (bitmap is not null && origin != SKEncodedOrigin.TopLeft)
|
||||
{
|
||||
using (bitmap)
|
||||
{
|
||||
return OrientImage(bitmap, origin);
|
||||
}
|
||||
}
|
||||
|
||||
return bitmap;
|
||||
}
|
||||
|
||||
return Decode(path, false, orientation, out _);
|
||||
case SKEncodedOrigin.TopRight:
|
||||
surface.Scale(-1, 1, midX, midY);
|
||||
break;
|
||||
case SKEncodedOrigin.BottomRight:
|
||||
surface.RotateDegrees(180, midX, midY);
|
||||
break;
|
||||
case SKEncodedOrigin.BottomLeft:
|
||||
surface.Scale(1, -1, midX, midY);
|
||||
break;
|
||||
case SKEncodedOrigin.LeftTop:
|
||||
surface.Translate(0, -rotated.Height);
|
||||
surface.Scale(1, -1, midX, midY);
|
||||
surface.RotateDegrees(-90);
|
||||
break;
|
||||
case SKEncodedOrigin.RightTop:
|
||||
surface.Translate(rotated.Width, 0);
|
||||
surface.RotateDegrees(90);
|
||||
break;
|
||||
case SKEncodedOrigin.RightBottom:
|
||||
surface.Translate(rotated.Width, 0);
|
||||
surface.Scale(1, -1, midX, midY);
|
||||
surface.RotateDegrees(90);
|
||||
break;
|
||||
case SKEncodedOrigin.LeftBottom:
|
||||
surface.Translate(0, rotated.Height);
|
||||
surface.RotateDegrees(-90);
|
||||
break;
|
||||
}
|
||||
|
||||
private SKBitmap OrientImage(SKBitmap bitmap, SKEncodedOrigin origin)
|
||||
surface.DrawBitmap(bitmap, 0, 0);
|
||||
return rotated;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Resizes an image on the CPU, by utilizing a surface and canvas.
|
||||
///
|
||||
/// The convolutional matrix kernel used in this resize function gives a (light) sharpening effect.
|
||||
/// This technique is similar to effect that can be created using for example the [Convolution matrix filter in GIMP](https://docs.gimp.org/2.10/en/gimp-filter-convolution-matrix.html).
|
||||
/// </summary>
|
||||
/// <param name="source">The source bitmap.</param>
|
||||
/// <param name="targetInfo">This specifies the target size and other information required to create the surface.</param>
|
||||
/// <param name="isAntialias">This enables anti-aliasing on the SKPaint instance.</param>
|
||||
/// <param name="isDither">This enables dithering on the SKPaint instance.</param>
|
||||
/// <returns>The resized image.</returns>
|
||||
internal static SKImage ResizeImage(SKBitmap source, SKImageInfo targetInfo, bool isAntialias = false, bool isDither = false)
|
||||
{
|
||||
using var surface = SKSurface.Create(targetInfo);
|
||||
using var canvas = surface.Canvas;
|
||||
using var paint = new SKPaint
|
||||
{
|
||||
var needsFlip = origin == SKEncodedOrigin.LeftBottom
|
||||
|| origin == SKEncodedOrigin.LeftTop
|
||||
|| origin == SKEncodedOrigin.RightBottom
|
||||
|| origin == SKEncodedOrigin.RightTop;
|
||||
var rotated = needsFlip
|
||||
? new SKBitmap(bitmap.Height, bitmap.Width)
|
||||
: new SKBitmap(bitmap.Width, bitmap.Height);
|
||||
using var surface = new SKCanvas(rotated);
|
||||
var midX = (float)rotated.Width / 2;
|
||||
var midY = (float)rotated.Height / 2;
|
||||
FilterQuality = SKFilterQuality.High,
|
||||
IsAntialias = isAntialias,
|
||||
IsDither = isDither
|
||||
};
|
||||
|
||||
switch (origin)
|
||||
{
|
||||
case SKEncodedOrigin.TopRight:
|
||||
surface.Scale(-1, 1, midX, midY);
|
||||
break;
|
||||
case SKEncodedOrigin.BottomRight:
|
||||
surface.RotateDegrees(180, midX, midY);
|
||||
break;
|
||||
case SKEncodedOrigin.BottomLeft:
|
||||
surface.Scale(1, -1, midX, midY);
|
||||
break;
|
||||
case SKEncodedOrigin.LeftTop:
|
||||
surface.Translate(0, -rotated.Height);
|
||||
surface.Scale(1, -1, midX, midY);
|
||||
surface.RotateDegrees(-90);
|
||||
break;
|
||||
case SKEncodedOrigin.RightTop:
|
||||
surface.Translate(rotated.Width, 0);
|
||||
surface.RotateDegrees(90);
|
||||
break;
|
||||
case SKEncodedOrigin.RightBottom:
|
||||
surface.Translate(rotated.Width, 0);
|
||||
surface.Scale(1, -1, midX, midY);
|
||||
surface.RotateDegrees(90);
|
||||
break;
|
||||
case SKEncodedOrigin.LeftBottom:
|
||||
surface.Translate(0, rotated.Height);
|
||||
surface.RotateDegrees(-90);
|
||||
break;
|
||||
}
|
||||
var kernel = new float[9]
|
||||
{
|
||||
0, -.1f, 0,
|
||||
-.1f, 1.4f, -.1f,
|
||||
0, -.1f, 0,
|
||||
};
|
||||
|
||||
surface.DrawBitmap(bitmap, 0, 0);
|
||||
return rotated;
|
||||
var kernelSize = new SKSizeI(3, 3);
|
||||
var kernelOffset = new SKPointI(1, 1);
|
||||
|
||||
paint.ImageFilter = SKImageFilter.CreateMatrixConvolution(
|
||||
kernelSize,
|
||||
kernel,
|
||||
1f,
|
||||
0f,
|
||||
kernelOffset,
|
||||
SKShaderTileMode.Clamp,
|
||||
true);
|
||||
|
||||
canvas.DrawBitmap(
|
||||
source,
|
||||
SKRect.Create(0, 0, source.Width, source.Height),
|
||||
SKRect.Create(0, 0, targetInfo.Width, targetInfo.Height),
|
||||
paint);
|
||||
|
||||
return surface.Snapshot();
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public string EncodeImage(string inputPath, DateTime dateModified, string outputPath, bool autoOrient, ImageOrientation? orientation, int quality, ImageProcessingOptions options, ImageFormat outputFormat)
|
||||
{
|
||||
ArgumentException.ThrowIfNullOrEmpty(inputPath);
|
||||
ArgumentException.ThrowIfNullOrEmpty(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;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Resizes an image on the CPU, by utilizing a surface and canvas.
|
||||
///
|
||||
/// The convolutional matrix kernel used in this resize function gives a (light) sharpening effect.
|
||||
/// This technique is similar to effect that can be created using for example the [Convolution matrix filter in GIMP](https://docs.gimp.org/2.10/en/gimp-filter-convolution-matrix.html).
|
||||
/// </summary>
|
||||
/// <param name="source">The source bitmap.</param>
|
||||
/// <param name="targetInfo">This specifies the target size and other information required to create the surface.</param>
|
||||
/// <param name="isAntialias">This enables anti-aliasing on the SKPaint instance.</param>
|
||||
/// <param name="isDither">This enables dithering on the SKPaint instance.</param>
|
||||
/// <returns>The resized image.</returns>
|
||||
internal static SKImage ResizeImage(SKBitmap source, SKImageInfo targetInfo, bool isAntialias = false, bool isDither = false)
|
||||
var skiaOutputFormat = GetImageFormat(outputFormat);
|
||||
|
||||
var hasBackgroundColor = !string.IsNullOrWhiteSpace(options.BackgroundColor);
|
||||
var hasForegroundColor = !string.IsNullOrWhiteSpace(options.ForegroundLayer);
|
||||
var blur = options.Blur ?? 0;
|
||||
var hasIndicator = options.AddPlayedIndicator || options.UnplayedCount.HasValue || !options.PercentPlayed.Equals(0);
|
||||
|
||||
using var bitmap = GetBitmap(inputPath, autoOrient, orientation);
|
||||
if (bitmap is null)
|
||||
{
|
||||
using var surface = SKSurface.Create(targetInfo);
|
||||
using var canvas = surface.Canvas;
|
||||
using var paint = new SKPaint
|
||||
{
|
||||
FilterQuality = SKFilterQuality.High,
|
||||
IsAntialias = isAntialias,
|
||||
IsDither = isDither
|
||||
};
|
||||
|
||||
var kernel = new float[9]
|
||||
{
|
||||
0, -.1f, 0,
|
||||
-.1f, 1.4f, -.1f,
|
||||
0, -.1f, 0,
|
||||
};
|
||||
|
||||
var kernelSize = new SKSizeI(3, 3);
|
||||
var kernelOffset = new SKPointI(1, 1);
|
||||
|
||||
paint.ImageFilter = SKImageFilter.CreateMatrixConvolution(
|
||||
kernelSize,
|
||||
kernel,
|
||||
1f,
|
||||
0f,
|
||||
kernelOffset,
|
||||
SKShaderTileMode.Clamp,
|
||||
true);
|
||||
|
||||
canvas.DrawBitmap(
|
||||
source,
|
||||
SKRect.Create(0, 0, source.Width, source.Height),
|
||||
SKRect.Create(0, 0, targetInfo.Width, targetInfo.Height),
|
||||
paint);
|
||||
|
||||
return surface.Snapshot();
|
||||
throw new InvalidDataException($"Skia unable to read image {inputPath}");
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public string EncodeImage(string inputPath, DateTime dateModified, string outputPath, bool autoOrient, ImageOrientation? orientation, int quality, ImageProcessingOptions options, ImageFormat outputFormat)
|
||||
var originalImageSize = new ImageDimensions(bitmap.Width, bitmap.Height);
|
||||
|
||||
if (options.HasDefaultOptions(inputPath, originalImageSize) && !autoOrient)
|
||||
{
|
||||
ArgumentException.ThrowIfNullOrEmpty(inputPath);
|
||||
ArgumentException.ThrowIfNullOrEmpty(outputPath);
|
||||
// Just spit out the original file if all the options are default
|
||||
return inputPath;
|
||||
}
|
||||
|
||||
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 newImageSize = ImageHelper.GetNewImageSize(options, originalImageSize);
|
||||
|
||||
var skiaOutputFormat = GetImageFormat(outputFormat);
|
||||
var width = newImageSize.Width;
|
||||
var height = newImageSize.Height;
|
||||
|
||||
var hasBackgroundColor = !string.IsNullOrWhiteSpace(options.BackgroundColor);
|
||||
var hasForegroundColor = !string.IsNullOrWhiteSpace(options.ForegroundLayer);
|
||||
var blur = options.Blur ?? 0;
|
||||
var hasIndicator = options.AddPlayedIndicator || options.UnplayedCount.HasValue || !options.PercentPlayed.Equals(0);
|
||||
|
||||
using var bitmap = GetBitmap(inputPath, autoOrient, orientation);
|
||||
if (bitmap is null)
|
||||
{
|
||||
throw new InvalidDataException($"Skia unable to read image {inputPath}");
|
||||
}
|
||||
|
||||
var originalImageSize = new ImageDimensions(bitmap.Width, bitmap.Height);
|
||||
|
||||
if (options.HasDefaultOptions(inputPath, originalImageSize) && !autoOrient)
|
||||
{
|
||||
// Just spit out the original file if all the options are default
|
||||
return inputPath;
|
||||
}
|
||||
|
||||
var newImageSize = ImageHelper.GetNewImageSize(options, originalImageSize);
|
||||
|
||||
var width = newImageSize.Width;
|
||||
var height = newImageSize.Height;
|
||||
|
||||
// scale image (the FromImage creates a copy)
|
||||
var imageInfo = new SKImageInfo(width, height, bitmap.ColorType, bitmap.AlphaType, bitmap.ColorSpace);
|
||||
using var resizedBitmap = SKBitmap.FromImage(ResizeImage(bitmap, imageInfo));
|
||||
|
||||
// If all we're doing is resizing then we can stop now
|
||||
if (!hasBackgroundColor && !hasForegroundColor && blur == 0 && !hasIndicator)
|
||||
{
|
||||
var outputDirectory = Path.GetDirectoryName(outputPath) ?? throw new ArgumentException($"Provided path ({outputPath}) is not valid.", nameof(outputPath));
|
||||
Directory.CreateDirectory(outputDirectory);
|
||||
using var outputStream = new SKFileWStream(outputPath);
|
||||
using var pixmap = new SKPixmap(new SKImageInfo(width, height), resizedBitmap.GetPixels());
|
||||
resizedBitmap.Encode(outputStream, skiaOutputFormat, quality);
|
||||
return outputPath;
|
||||
}
|
||||
|
||||
// create bitmap to use for canvas drawing used to draw into bitmap
|
||||
using var saveBitmap = new SKBitmap(width, height);
|
||||
using var canvas = new SKCanvas(saveBitmap);
|
||||
// set background color if present
|
||||
if (hasBackgroundColor)
|
||||
{
|
||||
canvas.Clear(SKColor.Parse(options.BackgroundColor));
|
||||
}
|
||||
|
||||
// Add blur if option is present
|
||||
if (blur > 0)
|
||||
{
|
||||
// create image from resized bitmap to apply blur
|
||||
using var paint = new SKPaint();
|
||||
using var filter = SKImageFilter.CreateBlur(blur, blur);
|
||||
paint.ImageFilter = filter;
|
||||
canvas.DrawBitmap(resizedBitmap, SKRect.Create(width, height), paint);
|
||||
}
|
||||
else
|
||||
{
|
||||
// draw resized bitmap onto canvas
|
||||
canvas.DrawBitmap(resizedBitmap, SKRect.Create(width, height));
|
||||
}
|
||||
|
||||
// If foreground layer present then draw
|
||||
if (hasForegroundColor)
|
||||
{
|
||||
if (!double.TryParse(options.ForegroundLayer, out double opacity))
|
||||
{
|
||||
opacity = .4;
|
||||
}
|
||||
|
||||
canvas.DrawColor(new SKColor(0, 0, 0, (byte)((1 - opacity) * 0xFF)), SKBlendMode.SrcOver);
|
||||
}
|
||||
|
||||
if (hasIndicator)
|
||||
{
|
||||
DrawIndicator(canvas, width, height, options);
|
||||
}
|
||||
|
||||
var directory = Path.GetDirectoryName(outputPath) ?? throw new ArgumentException($"Provided path ({outputPath}) is not valid.", nameof(outputPath));
|
||||
Directory.CreateDirectory(directory);
|
||||
using (var outputStream = new SKFileWStream(outputPath))
|
||||
{
|
||||
using (var pixmap = new SKPixmap(new SKImageInfo(width, height), saveBitmap.GetPixels()))
|
||||
{
|
||||
pixmap.Encode(outputStream, skiaOutputFormat, quality);
|
||||
}
|
||||
}
|
||||
// scale image (the FromImage creates a copy)
|
||||
var imageInfo = new SKImageInfo(width, height, bitmap.ColorType, bitmap.AlphaType, bitmap.ColorSpace);
|
||||
using var resizedBitmap = SKBitmap.FromImage(ResizeImage(bitmap, imageInfo));
|
||||
|
||||
// If all we're doing is resizing then we can stop now
|
||||
if (!hasBackgroundColor && !hasForegroundColor && blur == 0 && !hasIndicator)
|
||||
{
|
||||
var outputDirectory = Path.GetDirectoryName(outputPath) ?? throw new ArgumentException($"Provided path ({outputPath}) is not valid.", nameof(outputPath));
|
||||
Directory.CreateDirectory(outputDirectory);
|
||||
using var outputStream = new SKFileWStream(outputPath);
|
||||
using var pixmap = new SKPixmap(new SKImageInfo(width, height), resizedBitmap.GetPixels());
|
||||
resizedBitmap.Encode(outputStream, skiaOutputFormat, quality);
|
||||
return outputPath;
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public void CreateImageCollage(ImageCollageOptions options, string? libraryName)
|
||||
// create bitmap to use for canvas drawing used to draw into bitmap
|
||||
using var saveBitmap = new SKBitmap(width, height);
|
||||
using var canvas = new SKCanvas(saveBitmap);
|
||||
// set background color if present
|
||||
if (hasBackgroundColor)
|
||||
{
|
||||
double ratio = (double)options.Width / options.Height;
|
||||
canvas.Clear(SKColor.Parse(options.BackgroundColor));
|
||||
}
|
||||
|
||||
if (ratio >= 1.4)
|
||||
// Add blur if option is present
|
||||
if (blur > 0)
|
||||
{
|
||||
// create image from resized bitmap to apply blur
|
||||
using var paint = new SKPaint();
|
||||
using var filter = SKImageFilter.CreateBlur(blur, blur);
|
||||
paint.ImageFilter = filter;
|
||||
canvas.DrawBitmap(resizedBitmap, SKRect.Create(width, height), paint);
|
||||
}
|
||||
else
|
||||
{
|
||||
// draw resized bitmap onto canvas
|
||||
canvas.DrawBitmap(resizedBitmap, SKRect.Create(width, height));
|
||||
}
|
||||
|
||||
// If foreground layer present then draw
|
||||
if (hasForegroundColor)
|
||||
{
|
||||
if (!double.TryParse(options.ForegroundLayer, out double opacity))
|
||||
{
|
||||
new StripCollageBuilder(this).BuildThumbCollage(options.InputPaths, options.OutputPath, options.Width, options.Height, libraryName);
|
||||
opacity = .4;
|
||||
}
|
||||
else if (ratio >= .9)
|
||||
|
||||
canvas.DrawColor(new SKColor(0, 0, 0, (byte)((1 - opacity) * 0xFF)), SKBlendMode.SrcOver);
|
||||
}
|
||||
|
||||
if (hasIndicator)
|
||||
{
|
||||
DrawIndicator(canvas, width, height, options);
|
||||
}
|
||||
|
||||
var directory = Path.GetDirectoryName(outputPath) ?? throw new ArgumentException($"Provided path ({outputPath}) is not valid.", nameof(outputPath));
|
||||
Directory.CreateDirectory(directory);
|
||||
using (var outputStream = new SKFileWStream(outputPath))
|
||||
{
|
||||
using (var pixmap = new SKPixmap(new SKImageInfo(width, height), saveBitmap.GetPixels()))
|
||||
{
|
||||
new StripCollageBuilder(this).BuildSquareCollage(options.InputPaths, options.OutputPath, options.Width, options.Height);
|
||||
}
|
||||
else
|
||||
{
|
||||
// TODO: Create Poster collage capability
|
||||
new StripCollageBuilder(this).BuildSquareCollage(options.InputPaths, options.OutputPath, options.Width, options.Height);
|
||||
pixmap.Encode(outputStream, skiaOutputFormat, quality);
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public void CreateSplashscreen(IReadOnlyList<string> posters, IReadOnlyList<string> backdrops)
|
||||
return outputPath;
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public void CreateImageCollage(ImageCollageOptions options, string? libraryName)
|
||||
{
|
||||
double ratio = (double)options.Width / options.Height;
|
||||
|
||||
if (ratio >= 1.4)
|
||||
{
|
||||
var splashBuilder = new SplashscreenBuilder(this);
|
||||
var outputPath = Path.Combine(_appPaths.DataPath, "splashscreen.png");
|
||||
splashBuilder.GenerateSplash(posters, backdrops, outputPath);
|
||||
new StripCollageBuilder(this).BuildThumbCollage(options.InputPaths, options.OutputPath, options.Width, options.Height, libraryName);
|
||||
}
|
||||
|
||||
private void DrawIndicator(SKCanvas canvas, int imageWidth, int imageHeight, ImageProcessingOptions options)
|
||||
else if (ratio >= .9)
|
||||
{
|
||||
try
|
||||
{
|
||||
var currentImageSize = new ImageDimensions(imageWidth, imageHeight);
|
||||
new StripCollageBuilder(this).BuildSquareCollage(options.InputPaths, options.OutputPath, options.Width, options.Height);
|
||||
}
|
||||
else
|
||||
{
|
||||
// TODO: Create Poster collage capability
|
||||
new StripCollageBuilder(this).BuildSquareCollage(options.InputPaths, options.OutputPath, options.Width, options.Height);
|
||||
}
|
||||
}
|
||||
|
||||
if (options.AddPlayedIndicator)
|
||||
{
|
||||
PlayedIndicatorDrawer.DrawPlayedIndicator(canvas, currentImageSize);
|
||||
}
|
||||
else if (options.UnplayedCount.HasValue)
|
||||
{
|
||||
UnplayedCountIndicator.DrawUnplayedCountIndicator(canvas, currentImageSize, options.UnplayedCount.Value);
|
||||
}
|
||||
/// <inheritdoc />
|
||||
public void CreateSplashscreen(IReadOnlyList<string> posters, IReadOnlyList<string> backdrops)
|
||||
{
|
||||
var splashBuilder = new SplashscreenBuilder(this);
|
||||
var outputPath = Path.Combine(_appPaths.DataPath, "splashscreen.png");
|
||||
splashBuilder.GenerateSplash(posters, backdrops, outputPath);
|
||||
}
|
||||
|
||||
if (options.PercentPlayed > 0)
|
||||
{
|
||||
PercentPlayedDrawer.Process(canvas, currentImageSize, options.PercentPlayed);
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
private void DrawIndicator(SKCanvas canvas, int imageWidth, int imageHeight, ImageProcessingOptions options)
|
||||
{
|
||||
try
|
||||
{
|
||||
var currentImageSize = new ImageDimensions(imageWidth, imageHeight);
|
||||
|
||||
if (options.AddPlayedIndicator)
|
||||
{
|
||||
_logger.LogError(ex, "Error drawing indicator overlay");
|
||||
PlayedIndicatorDrawer.DrawPlayedIndicator(canvas, currentImageSize);
|
||||
}
|
||||
else if (options.UnplayedCount.HasValue)
|
||||
{
|
||||
UnplayedCountIndicator.DrawUnplayedCountIndicator(canvas, currentImageSize, options.UnplayedCount.Value);
|
||||
}
|
||||
|
||||
if (options.PercentPlayed > 0)
|
||||
{
|
||||
PercentPlayedDrawer.Process(canvas, currentImageSize, options.PercentPlayed);
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "Error drawing indicator overlay");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,39 +1,38 @@
|
|||
using System;
|
||||
|
||||
namespace Jellyfin.Drawing.Skia
|
||||
namespace Jellyfin.Drawing.Skia;
|
||||
|
||||
/// <summary>
|
||||
/// Represents errors that occur during interaction with Skia.
|
||||
/// </summary>
|
||||
public class SkiaException : Exception
|
||||
{
|
||||
/// <summary>
|
||||
/// Represents errors that occur during interaction with Skia.
|
||||
/// Initializes a new instance of the <see cref="SkiaException"/> class.
|
||||
/// </summary>
|
||||
public class SkiaException : Exception
|
||||
public SkiaException()
|
||||
{
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="SkiaException"/> class.
|
||||
/// </summary>
|
||||
public SkiaException()
|
||||
{
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="SkiaException"/> class with a specified error message.
|
||||
/// </summary>
|
||||
/// <param name="message">The message that describes the error.</param>
|
||||
public SkiaException(string message) : base(message)
|
||||
{
|
||||
}
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="SkiaException"/> class with a specified error message.
|
||||
/// </summary>
|
||||
/// <param name="message">The message that describes the error.</param>
|
||||
public SkiaException(string message) : base(message)
|
||||
{
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="SkiaException"/> class with a specified error message and a
|
||||
/// reference to the inner exception that is the cause of this exception.
|
||||
/// </summary>
|
||||
/// <param name="message">The error message that explains the reason for the exception.</param>
|
||||
/// <param name="innerException">
|
||||
/// The exception that is the cause of the current exception, or a null reference (Nothing in Visual Basic) if
|
||||
/// no inner exception is specified.
|
||||
/// </param>
|
||||
public SkiaException(string message, Exception innerException)
|
||||
: base(message, innerException)
|
||||
{
|
||||
}
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="SkiaException"/> class with a specified error message and a
|
||||
/// reference to the inner exception that is the cause of this exception.
|
||||
/// </summary>
|
||||
/// <param name="message">The error message that explains the reason for the exception.</param>
|
||||
/// <param name="innerException">
|
||||
/// The exception that is the cause of the current exception, or a null reference (Nothing in Visual Basic) if
|
||||
/// no inner exception is specified.
|
||||
/// </param>
|
||||
public SkiaException(string message, Exception innerException)
|
||||
: base(message, innerException)
|
||||
{
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,47 +1,46 @@
|
|||
using System.Collections.Generic;
|
||||
using SkiaSharp;
|
||||
|
||||
namespace Jellyfin.Drawing.Skia
|
||||
namespace Jellyfin.Drawing.Skia;
|
||||
|
||||
/// <summary>
|
||||
/// Class containing helper methods for working with SkiaSharp.
|
||||
/// </summary>
|
||||
public static class SkiaHelper
|
||||
{
|
||||
/// <summary>
|
||||
/// Class containing helper methods for working with SkiaSharp.
|
||||
/// Gets the next valid image as a bitmap.
|
||||
/// </summary>
|
||||
public static class SkiaHelper
|
||||
/// <param name="skiaEncoder">The current skia encoder.</param>
|
||||
/// <param name="paths">The list of image paths.</param>
|
||||
/// <param name="currentIndex">The current checked index.</param>
|
||||
/// <param name="newIndex">The new index.</param>
|
||||
/// <returns>A valid bitmap, or null if no bitmap exists after <c>currentIndex</c>.</returns>
|
||||
public static SKBitmap? GetNextValidImage(SkiaEncoder skiaEncoder, IReadOnlyList<string> paths, int currentIndex, out int newIndex)
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets the next valid image as a bitmap.
|
||||
/// </summary>
|
||||
/// <param name="skiaEncoder">The current skia encoder.</param>
|
||||
/// <param name="paths">The list of image paths.</param>
|
||||
/// <param name="currentIndex">The current checked index.</param>
|
||||
/// <param name="newIndex">The new index.</param>
|
||||
/// <returns>A valid bitmap, or null if no bitmap exists after <c>currentIndex</c>.</returns>
|
||||
public static SKBitmap? GetNextValidImage(SkiaEncoder skiaEncoder, IReadOnlyList<string> paths, int currentIndex, out int newIndex)
|
||||
var imagesTested = new Dictionary<int, int>();
|
||||
SKBitmap? bitmap = null;
|
||||
|
||||
while (imagesTested.Count < paths.Count)
|
||||
{
|
||||
var imagesTested = new Dictionary<int, int>();
|
||||
SKBitmap? bitmap = null;
|
||||
|
||||
while (imagesTested.Count < paths.Count)
|
||||
if (currentIndex >= paths.Count)
|
||||
{
|
||||
if (currentIndex >= paths.Count)
|
||||
{
|
||||
currentIndex = 0;
|
||||
}
|
||||
|
||||
bitmap = skiaEncoder.Decode(paths[currentIndex], false, null, out _);
|
||||
|
||||
imagesTested[currentIndex] = 0;
|
||||
|
||||
currentIndex++;
|
||||
|
||||
if (bitmap is not null)
|
||||
{
|
||||
break;
|
||||
}
|
||||
currentIndex = 0;
|
||||
}
|
||||
|
||||
newIndex = currentIndex;
|
||||
return bitmap;
|
||||
bitmap = skiaEncoder.Decode(paths[currentIndex], false, null, out _);
|
||||
|
||||
imagesTested[currentIndex] = 0;
|
||||
|
||||
currentIndex++;
|
||||
|
||||
if (bitmap is not null)
|
||||
{
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
newIndex = currentIndex;
|
||||
return bitmap;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -2,147 +2,146 @@ using System;
|
|||
using System.Collections.Generic;
|
||||
using SkiaSharp;
|
||||
|
||||
namespace Jellyfin.Drawing.Skia
|
||||
namespace Jellyfin.Drawing.Skia;
|
||||
|
||||
/// <summary>
|
||||
/// Used to build the splashscreen.
|
||||
/// </summary>
|
||||
public class SplashscreenBuilder
|
||||
{
|
||||
private const int FinalWidth = 1920;
|
||||
private const int FinalHeight = 1080;
|
||||
// generated collage resolution should be higher than the final resolution
|
||||
private const int WallWidth = FinalWidth * 3;
|
||||
private const int WallHeight = FinalHeight * 2;
|
||||
private const int Rows = 6;
|
||||
private const int Spacing = 20;
|
||||
|
||||
private readonly SkiaEncoder _skiaEncoder;
|
||||
|
||||
/// <summary>
|
||||
/// Used to build the splashscreen.
|
||||
/// Initializes a new instance of the <see cref="SplashscreenBuilder"/> class.
|
||||
/// </summary>
|
||||
public class SplashscreenBuilder
|
||||
/// <param name="skiaEncoder">The SkiaEncoder.</param>
|
||||
public SplashscreenBuilder(SkiaEncoder skiaEncoder)
|
||||
{
|
||||
private const int FinalWidth = 1920;
|
||||
private const int FinalHeight = 1080;
|
||||
// generated collage resolution should be higher than the final resolution
|
||||
private const int WallWidth = FinalWidth * 3;
|
||||
private const int WallHeight = FinalHeight * 2;
|
||||
private const int Rows = 6;
|
||||
private const int Spacing = 20;
|
||||
_skiaEncoder = skiaEncoder;
|
||||
}
|
||||
|
||||
private readonly SkiaEncoder _skiaEncoder;
|
||||
/// <summary>
|
||||
/// Generate a splashscreen.
|
||||
/// </summary>
|
||||
/// <param name="posters">The poster paths.</param>
|
||||
/// <param name="backdrops">The landscape paths.</param>
|
||||
/// <param name="outputPath">The output path.</param>
|
||||
public void GenerateSplash(IReadOnlyList<string> posters, IReadOnlyList<string> backdrops, string outputPath)
|
||||
{
|
||||
using var wall = GenerateCollage(posters, backdrops);
|
||||
using var transformed = Transform3D(wall);
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="SplashscreenBuilder"/> class.
|
||||
/// </summary>
|
||||
/// <param name="skiaEncoder">The SkiaEncoder.</param>
|
||||
public SplashscreenBuilder(SkiaEncoder skiaEncoder)
|
||||
using var outputStream = new SKFileWStream(outputPath);
|
||||
using var pixmap = new SKPixmap(new SKImageInfo(FinalWidth, FinalHeight), transformed.GetPixels());
|
||||
pixmap.Encode(outputStream, StripCollageBuilder.GetEncodedFormat(outputPath), 90);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Generates a collage of posters and landscape pictures.
|
||||
/// </summary>
|
||||
/// <param name="posters">The poster paths.</param>
|
||||
/// <param name="backdrops">The landscape paths.</param>
|
||||
/// <returns>The created collage as a bitmap.</returns>
|
||||
private SKBitmap GenerateCollage(IReadOnlyList<string> posters, IReadOnlyList<string> backdrops)
|
||||
{
|
||||
var posterIndex = 0;
|
||||
var backdropIndex = 0;
|
||||
|
||||
var bitmap = new SKBitmap(WallWidth, WallHeight);
|
||||
using var canvas = new SKCanvas(bitmap);
|
||||
canvas.Clear(SKColors.Black);
|
||||
|
||||
int posterHeight = WallHeight / 6;
|
||||
|
||||
for (int i = 0; i < Rows; i++)
|
||||
{
|
||||
_skiaEncoder = skiaEncoder;
|
||||
}
|
||||
int imageCounter = Random.Shared.Next(0, 5);
|
||||
int currentWidthPos = i * 75;
|
||||
int currentHeight = i * (posterHeight + Spacing);
|
||||
|
||||
/// <summary>
|
||||
/// Generate a splashscreen.
|
||||
/// </summary>
|
||||
/// <param name="posters">The poster paths.</param>
|
||||
/// <param name="backdrops">The landscape paths.</param>
|
||||
/// <param name="outputPath">The output path.</param>
|
||||
public void GenerateSplash(IReadOnlyList<string> posters, IReadOnlyList<string> backdrops, string outputPath)
|
||||
{
|
||||
using var wall = GenerateCollage(posters, backdrops);
|
||||
using var transformed = Transform3D(wall);
|
||||
|
||||
using var outputStream = new SKFileWStream(outputPath);
|
||||
using var pixmap = new SKPixmap(new SKImageInfo(FinalWidth, FinalHeight), transformed.GetPixels());
|
||||
pixmap.Encode(outputStream, StripCollageBuilder.GetEncodedFormat(outputPath), 90);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Generates a collage of posters and landscape pictures.
|
||||
/// </summary>
|
||||
/// <param name="posters">The poster paths.</param>
|
||||
/// <param name="backdrops">The landscape paths.</param>
|
||||
/// <returns>The created collage as a bitmap.</returns>
|
||||
private SKBitmap GenerateCollage(IReadOnlyList<string> posters, IReadOnlyList<string> backdrops)
|
||||
{
|
||||
var posterIndex = 0;
|
||||
var backdropIndex = 0;
|
||||
|
||||
var bitmap = new SKBitmap(WallWidth, WallHeight);
|
||||
using var canvas = new SKCanvas(bitmap);
|
||||
canvas.Clear(SKColors.Black);
|
||||
|
||||
int posterHeight = WallHeight / 6;
|
||||
|
||||
for (int i = 0; i < Rows; i++)
|
||||
while (currentWidthPos < WallWidth)
|
||||
{
|
||||
int imageCounter = Random.Shared.Next(0, 5);
|
||||
int currentWidthPos = i * 75;
|
||||
int currentHeight = i * (posterHeight + Spacing);
|
||||
SKBitmap? currentImage;
|
||||
|
||||
while (currentWidthPos < WallWidth)
|
||||
switch (imageCounter)
|
||||
{
|
||||
SKBitmap? currentImage;
|
||||
case 0:
|
||||
case 2:
|
||||
case 3:
|
||||
currentImage = SkiaHelper.GetNextValidImage(_skiaEncoder, posters, posterIndex, out int newPosterIndex);
|
||||
posterIndex = newPosterIndex;
|
||||
break;
|
||||
default:
|
||||
currentImage = SkiaHelper.GetNextValidImage(_skiaEncoder, backdrops, backdropIndex, out int newBackdropIndex);
|
||||
backdropIndex = newBackdropIndex;
|
||||
break;
|
||||
}
|
||||
|
||||
switch (imageCounter)
|
||||
{
|
||||
case 0:
|
||||
case 2:
|
||||
case 3:
|
||||
currentImage = SkiaHelper.GetNextValidImage(_skiaEncoder, posters, posterIndex, out int newPosterIndex);
|
||||
posterIndex = newPosterIndex;
|
||||
break;
|
||||
default:
|
||||
currentImage = SkiaHelper.GetNextValidImage(_skiaEncoder, backdrops, backdropIndex, out int newBackdropIndex);
|
||||
backdropIndex = newBackdropIndex;
|
||||
break;
|
||||
}
|
||||
if (currentImage is null)
|
||||
{
|
||||
throw new ArgumentException("Not enough valid pictures provided to create a splashscreen!");
|
||||
}
|
||||
|
||||
if (currentImage is null)
|
||||
{
|
||||
throw new ArgumentException("Not enough valid pictures provided to create a splashscreen!");
|
||||
}
|
||||
// resize to the same aspect as the original
|
||||
var imageWidth = Math.Abs(posterHeight * currentImage.Width / currentImage.Height);
|
||||
using var resizedBitmap = new SKBitmap(imageWidth, posterHeight);
|
||||
currentImage.ScalePixels(resizedBitmap, SKFilterQuality.High);
|
||||
|
||||
// resize to the same aspect as the original
|
||||
var imageWidth = Math.Abs(posterHeight * currentImage.Width / currentImage.Height);
|
||||
using var resizedBitmap = new SKBitmap(imageWidth, posterHeight);
|
||||
currentImage.ScalePixels(resizedBitmap, SKFilterQuality.High);
|
||||
// draw on canvas
|
||||
canvas.DrawBitmap(resizedBitmap, currentWidthPos, currentHeight);
|
||||
|
||||
// draw on canvas
|
||||
canvas.DrawBitmap(resizedBitmap, currentWidthPos, currentHeight);
|
||||
currentWidthPos += imageWidth + Spacing;
|
||||
|
||||
currentWidthPos += imageWidth + Spacing;
|
||||
currentImage.Dispose();
|
||||
|
||||
currentImage.Dispose();
|
||||
|
||||
if (imageCounter >= 4)
|
||||
{
|
||||
imageCounter = 0;
|
||||
}
|
||||
else
|
||||
{
|
||||
imageCounter++;
|
||||
}
|
||||
if (imageCounter >= 4)
|
||||
{
|
||||
imageCounter = 0;
|
||||
}
|
||||
else
|
||||
{
|
||||
imageCounter++;
|
||||
}
|
||||
}
|
||||
|
||||
return bitmap;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Transform the collage in 3D space.
|
||||
/// </summary>
|
||||
/// <param name="input">The bitmap to transform.</param>
|
||||
/// <returns>The transformed image.</returns>
|
||||
private SKBitmap Transform3D(SKBitmap input)
|
||||
return bitmap;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Transform the collage in 3D space.
|
||||
/// </summary>
|
||||
/// <param name="input">The bitmap to transform.</param>
|
||||
/// <returns>The transformed image.</returns>
|
||||
private SKBitmap Transform3D(SKBitmap input)
|
||||
{
|
||||
var bitmap = new SKBitmap(FinalWidth, FinalHeight);
|
||||
using var canvas = new SKCanvas(bitmap);
|
||||
canvas.Clear(SKColors.Black);
|
||||
var matrix = new SKMatrix
|
||||
{
|
||||
var bitmap = new SKBitmap(FinalWidth, FinalHeight);
|
||||
using var canvas = new SKCanvas(bitmap);
|
||||
canvas.Clear(SKColors.Black);
|
||||
var matrix = new SKMatrix
|
||||
{
|
||||
ScaleX = 0.324108899f,
|
||||
ScaleY = 0.563934922f,
|
||||
SkewX = -0.244337708f,
|
||||
SkewY = 0.0377609022f,
|
||||
TransX = 42.0407715f,
|
||||
TransY = -198.104706f,
|
||||
Persp0 = -9.08959337E-05f,
|
||||
Persp1 = 6.85242048E-05f,
|
||||
Persp2 = 0.988209724f
|
||||
};
|
||||
ScaleX = 0.324108899f,
|
||||
ScaleY = 0.563934922f,
|
||||
SkewX = -0.244337708f,
|
||||
SkewY = 0.0377609022f,
|
||||
TransX = 42.0407715f,
|
||||
TransY = -198.104706f,
|
||||
Persp0 = -9.08959337E-05f,
|
||||
Persp1 = 6.85242048E-05f,
|
||||
Persp2 = 0.988209724f
|
||||
};
|
||||
|
||||
canvas.SetMatrix(matrix);
|
||||
canvas.DrawBitmap(input, 0, 0);
|
||||
canvas.SetMatrix(matrix);
|
||||
canvas.DrawBitmap(input, 0, 0);
|
||||
|
||||
return bitmap;
|
||||
}
|
||||
return bitmap;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -4,183 +4,182 @@ using System.IO;
|
|||
using System.Text.RegularExpressions;
|
||||
using SkiaSharp;
|
||||
|
||||
namespace Jellyfin.Drawing.Skia
|
||||
namespace Jellyfin.Drawing.Skia;
|
||||
|
||||
/// <summary>
|
||||
/// Used to build collages of multiple images arranged in vertical strips.
|
||||
/// </summary>
|
||||
public class StripCollageBuilder
|
||||
{
|
||||
private readonly SkiaEncoder _skiaEncoder;
|
||||
|
||||
/// <summary>
|
||||
/// Used to build collages of multiple images arranged in vertical strips.
|
||||
/// Initializes a new instance of the <see cref="StripCollageBuilder"/> class.
|
||||
/// </summary>
|
||||
public class StripCollageBuilder
|
||||
/// <param name="skiaEncoder">The encoder to use for building collages.</param>
|
||||
public StripCollageBuilder(SkiaEncoder skiaEncoder)
|
||||
{
|
||||
private readonly SkiaEncoder _skiaEncoder;
|
||||
_skiaEncoder = skiaEncoder;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="StripCollageBuilder"/> class.
|
||||
/// </summary>
|
||||
/// <param name="skiaEncoder">The encoder to use for building collages.</param>
|
||||
public StripCollageBuilder(SkiaEncoder skiaEncoder)
|
||||
/// <summary>
|
||||
/// Check which format an image has been encoded with using its filename extension.
|
||||
/// </summary>
|
||||
/// <param name="outputPath">The path to the image to get the format for.</param>
|
||||
/// <returns>The image format.</returns>
|
||||
public static SKEncodedImageFormat GetEncodedFormat(string outputPath)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(outputPath);
|
||||
|
||||
var ext = Path.GetExtension(outputPath);
|
||||
|
||||
if (string.Equals(ext, ".jpg", StringComparison.OrdinalIgnoreCase)
|
||||
|| string.Equals(ext, ".jpeg", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
_skiaEncoder = skiaEncoder;
|
||||
return SKEncodedImageFormat.Jpeg;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Check which format an image has been encoded with using its filename extension.
|
||||
/// </summary>
|
||||
/// <param name="outputPath">The path to the image to get the format for.</param>
|
||||
/// <returns>The image format.</returns>
|
||||
public static SKEncodedImageFormat GetEncodedFormat(string outputPath)
|
||||
if (string.Equals(ext, ".webp", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(outputPath);
|
||||
|
||||
var ext = Path.GetExtension(outputPath);
|
||||
|
||||
if (string.Equals(ext, ".jpg", StringComparison.OrdinalIgnoreCase)
|
||||
|| string.Equals(ext, ".jpeg", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
return SKEncodedImageFormat.Jpeg;
|
||||
}
|
||||
|
||||
if (string.Equals(ext, ".webp", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
return SKEncodedImageFormat.Webp;
|
||||
}
|
||||
|
||||
if (string.Equals(ext, ".gif", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
return SKEncodedImageFormat.Gif;
|
||||
}
|
||||
|
||||
if (string.Equals(ext, ".bmp", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
return SKEncodedImageFormat.Bmp;
|
||||
}
|
||||
|
||||
// default to png
|
||||
return SKEncodedImageFormat.Png;
|
||||
return SKEncodedImageFormat.Webp;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Create a square collage.
|
||||
/// </summary>
|
||||
/// <param name="paths">The paths of the images to use in the collage.</param>
|
||||
/// <param name="outputPath">The path at which to place the resulting collage image.</param>
|
||||
/// <param name="width">The desired width of the collage.</param>
|
||||
/// <param name="height">The desired height of the collage.</param>
|
||||
public void BuildSquareCollage(IReadOnlyList<string> paths, string outputPath, int width, int height)
|
||||
if (string.Equals(ext, ".gif", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
using var bitmap = BuildSquareCollageBitmap(paths, width, height);
|
||||
using var outputStream = new SKFileWStream(outputPath);
|
||||
using var pixmap = new SKPixmap(new SKImageInfo(width, height), bitmap.GetPixels());
|
||||
pixmap.Encode(outputStream, GetEncodedFormat(outputPath), 90);
|
||||
return SKEncodedImageFormat.Gif;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Create a thumb collage.
|
||||
/// </summary>
|
||||
/// <param name="paths">The paths of the images to use in the collage.</param>
|
||||
/// <param name="outputPath">The path at which to place the resulting image.</param>
|
||||
/// <param name="width">The desired width of the collage.</param>
|
||||
/// <param name="height">The desired height of the collage.</param>
|
||||
/// <param name="libraryName">The name of the library to draw on the collage.</param>
|
||||
public void BuildThumbCollage(IReadOnlyList<string> paths, string outputPath, int width, int height, string? libraryName)
|
||||
if (string.Equals(ext, ".bmp", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
using var bitmap = BuildThumbCollageBitmap(paths, width, height, libraryName);
|
||||
using var outputStream = new SKFileWStream(outputPath);
|
||||
using var pixmap = new SKPixmap(new SKImageInfo(width, height), bitmap.GetPixels());
|
||||
pixmap.Encode(outputStream, GetEncodedFormat(outputPath), 90);
|
||||
return SKEncodedImageFormat.Bmp;
|
||||
}
|
||||
|
||||
private SKBitmap BuildThumbCollageBitmap(IReadOnlyList<string> paths, int width, int height, string? libraryName)
|
||||
// default to png
|
||||
return SKEncodedImageFormat.Png;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Create a square collage.
|
||||
/// </summary>
|
||||
/// <param name="paths">The paths of the images to use in the collage.</param>
|
||||
/// <param name="outputPath">The path at which to place the resulting collage image.</param>
|
||||
/// <param name="width">The desired width of the collage.</param>
|
||||
/// <param name="height">The desired height of the collage.</param>
|
||||
public void BuildSquareCollage(IReadOnlyList<string> paths, string outputPath, int width, int height)
|
||||
{
|
||||
using var bitmap = BuildSquareCollageBitmap(paths, width, height);
|
||||
using var outputStream = new SKFileWStream(outputPath);
|
||||
using var pixmap = new SKPixmap(new SKImageInfo(width, height), bitmap.GetPixels());
|
||||
pixmap.Encode(outputStream, GetEncodedFormat(outputPath), 90);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Create a thumb collage.
|
||||
/// </summary>
|
||||
/// <param name="paths">The paths of the images to use in the collage.</param>
|
||||
/// <param name="outputPath">The path at which to place the resulting image.</param>
|
||||
/// <param name="width">The desired width of the collage.</param>
|
||||
/// <param name="height">The desired height of the collage.</param>
|
||||
/// <param name="libraryName">The name of the library to draw on the collage.</param>
|
||||
public void BuildThumbCollage(IReadOnlyList<string> paths, string outputPath, int width, int height, string? libraryName)
|
||||
{
|
||||
using var bitmap = BuildThumbCollageBitmap(paths, width, height, libraryName);
|
||||
using var outputStream = new SKFileWStream(outputPath);
|
||||
using var pixmap = new SKPixmap(new SKImageInfo(width, height), bitmap.GetPixels());
|
||||
pixmap.Encode(outputStream, GetEncodedFormat(outputPath), 90);
|
||||
}
|
||||
|
||||
private SKBitmap BuildThumbCollageBitmap(IReadOnlyList<string> paths, int width, int height, string? libraryName)
|
||||
{
|
||||
var bitmap = new SKBitmap(width, height);
|
||||
|
||||
using var canvas = new SKCanvas(bitmap);
|
||||
canvas.Clear(SKColors.Black);
|
||||
|
||||
using var backdrop = SkiaHelper.GetNextValidImage(_skiaEncoder, paths, 0, out _);
|
||||
if (backdrop is null)
|
||||
{
|
||||
var bitmap = new SKBitmap(width, height);
|
||||
|
||||
using var canvas = new SKCanvas(bitmap);
|
||||
canvas.Clear(SKColors.Black);
|
||||
|
||||
using var backdrop = SkiaHelper.GetNextValidImage(_skiaEncoder, paths, 0, out _);
|
||||
if (backdrop is null)
|
||||
{
|
||||
return bitmap;
|
||||
}
|
||||
|
||||
// resize to the same aspect as the original
|
||||
var backdropHeight = Math.Abs(width * backdrop.Height / backdrop.Width);
|
||||
using var residedBackdrop = SkiaEncoder.ResizeImage(backdrop, new SKImageInfo(width, backdropHeight, backdrop.ColorType, backdrop.AlphaType, backdrop.ColorSpace));
|
||||
// draw the backdrop
|
||||
canvas.DrawImage(residedBackdrop, 0, 0);
|
||||
|
||||
// draw shadow rectangle
|
||||
using var paintColor = new SKPaint
|
||||
{
|
||||
Color = SKColors.Black.WithAlpha(0x78),
|
||||
Style = SKPaintStyle.Fill
|
||||
};
|
||||
canvas.DrawRect(0, 0, width, height, paintColor);
|
||||
|
||||
var typeFace = SKTypeface.FromFamilyName("sans-serif", SKFontStyleWeight.Bold, SKFontStyleWidth.Normal, SKFontStyleSlant.Upright);
|
||||
|
||||
// use the system fallback to find a typeface for the given CJK character
|
||||
var nonCjkPattern = @"[^\p{IsCJKUnifiedIdeographs}\p{IsCJKUnifiedIdeographsExtensionA}\p{IsKatakana}\p{IsHiragana}\p{IsHangulSyllables}\p{IsHangulJamo}]";
|
||||
var filteredName = Regex.Replace(libraryName ?? string.Empty, nonCjkPattern, string.Empty);
|
||||
if (!string.IsNullOrEmpty(filteredName))
|
||||
{
|
||||
typeFace = SKFontManager.Default.MatchCharacter(null, SKFontStyleWeight.Bold, SKFontStyleWidth.Normal, SKFontStyleSlant.Upright, null, filteredName[0]);
|
||||
}
|
||||
|
||||
// draw library name
|
||||
using var textPaint = new SKPaint
|
||||
{
|
||||
Color = SKColors.White,
|
||||
Style = SKPaintStyle.Fill,
|
||||
TextSize = 112,
|
||||
TextAlign = SKTextAlign.Center,
|
||||
Typeface = typeFace,
|
||||
IsAntialias = true
|
||||
};
|
||||
|
||||
// scale down text to 90% of the width if text is larger than 95% of the width
|
||||
var textWidth = textPaint.MeasureText(libraryName);
|
||||
if (textWidth > width * 0.95)
|
||||
{
|
||||
textPaint.TextSize = 0.9f * width * textPaint.TextSize / textWidth;
|
||||
}
|
||||
|
||||
canvas.DrawText(libraryName, width / 2f, (height / 2f) + (textPaint.FontMetrics.XHeight / 2), textPaint);
|
||||
|
||||
return bitmap;
|
||||
}
|
||||
|
||||
private SKBitmap BuildSquareCollageBitmap(IReadOnlyList<string> paths, int width, int height)
|
||||
{
|
||||
var bitmap = new SKBitmap(width, height);
|
||||
var imageIndex = 0;
|
||||
var cellWidth = width / 2;
|
||||
var cellHeight = height / 2;
|
||||
// resize to the same aspect as the original
|
||||
var backdropHeight = Math.Abs(width * backdrop.Height / backdrop.Width);
|
||||
using var residedBackdrop = SkiaEncoder.ResizeImage(backdrop, new SKImageInfo(width, backdropHeight, backdrop.ColorType, backdrop.AlphaType, backdrop.ColorSpace));
|
||||
// draw the backdrop
|
||||
canvas.DrawImage(residedBackdrop, 0, 0);
|
||||
|
||||
using var canvas = new SKCanvas(bitmap);
|
||||
for (var x = 0; x < 2; x++)
|
||||
// draw shadow rectangle
|
||||
using var paintColor = new SKPaint
|
||||
{
|
||||
Color = SKColors.Black.WithAlpha(0x78),
|
||||
Style = SKPaintStyle.Fill
|
||||
};
|
||||
canvas.DrawRect(0, 0, width, height, paintColor);
|
||||
|
||||
var typeFace = SKTypeface.FromFamilyName("sans-serif", SKFontStyleWeight.Bold, SKFontStyleWidth.Normal, SKFontStyleSlant.Upright);
|
||||
|
||||
// use the system fallback to find a typeface for the given CJK character
|
||||
var nonCjkPattern = @"[^\p{IsCJKUnifiedIdeographs}\p{IsCJKUnifiedIdeographsExtensionA}\p{IsKatakana}\p{IsHiragana}\p{IsHangulSyllables}\p{IsHangulJamo}]";
|
||||
var filteredName = Regex.Replace(libraryName ?? string.Empty, nonCjkPattern, string.Empty);
|
||||
if (!string.IsNullOrEmpty(filteredName))
|
||||
{
|
||||
typeFace = SKFontManager.Default.MatchCharacter(null, SKFontStyleWeight.Bold, SKFontStyleWidth.Normal, SKFontStyleSlant.Upright, null, filteredName[0]);
|
||||
}
|
||||
|
||||
// draw library name
|
||||
using var textPaint = new SKPaint
|
||||
{
|
||||
Color = SKColors.White,
|
||||
Style = SKPaintStyle.Fill,
|
||||
TextSize = 112,
|
||||
TextAlign = SKTextAlign.Center,
|
||||
Typeface = typeFace,
|
||||
IsAntialias = true
|
||||
};
|
||||
|
||||
// scale down text to 90% of the width if text is larger than 95% of the width
|
||||
var textWidth = textPaint.MeasureText(libraryName);
|
||||
if (textWidth > width * 0.95)
|
||||
{
|
||||
textPaint.TextSize = 0.9f * width * textPaint.TextSize / textWidth;
|
||||
}
|
||||
|
||||
canvas.DrawText(libraryName, width / 2f, (height / 2f) + (textPaint.FontMetrics.XHeight / 2), textPaint);
|
||||
|
||||
return bitmap;
|
||||
}
|
||||
|
||||
private SKBitmap BuildSquareCollageBitmap(IReadOnlyList<string> paths, int width, int height)
|
||||
{
|
||||
var bitmap = new SKBitmap(width, height);
|
||||
var imageIndex = 0;
|
||||
var cellWidth = width / 2;
|
||||
var cellHeight = height / 2;
|
||||
|
||||
using var canvas = new SKCanvas(bitmap);
|
||||
for (var x = 0; x < 2; x++)
|
||||
{
|
||||
for (var y = 0; y < 2; y++)
|
||||
{
|
||||
for (var y = 0; y < 2; y++)
|
||||
using var currentBitmap = SkiaHelper.GetNextValidImage(_skiaEncoder, paths, imageIndex, out int newIndex);
|
||||
imageIndex = newIndex;
|
||||
|
||||
if (currentBitmap is null)
|
||||
{
|
||||
using var currentBitmap = SkiaHelper.GetNextValidImage(_skiaEncoder, paths, imageIndex, out int newIndex);
|
||||
imageIndex = newIndex;
|
||||
|
||||
if (currentBitmap is null)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
// Scale image. The FromBitmap creates a copy
|
||||
var imageInfo = new SKImageInfo(cellWidth, cellHeight, currentBitmap.ColorType, currentBitmap.AlphaType, currentBitmap.ColorSpace);
|
||||
using var resizedBitmap = SKBitmap.FromImage(SkiaEncoder.ResizeImage(currentBitmap, imageInfo));
|
||||
|
||||
// draw this image into the strip at the next position
|
||||
var xPos = x * cellWidth;
|
||||
var yPos = y * cellHeight;
|
||||
canvas.DrawBitmap(resizedBitmap, xPos, yPos);
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
return bitmap;
|
||||
// Scale image. The FromBitmap creates a copy
|
||||
var imageInfo = new SKImageInfo(cellWidth, cellHeight, currentBitmap.ColorType, currentBitmap.AlphaType, currentBitmap.ColorSpace);
|
||||
using var resizedBitmap = SKBitmap.FromImage(SkiaEncoder.ResizeImage(currentBitmap, imageInfo));
|
||||
|
||||
// draw this image into the strip at the next position
|
||||
var xPos = x * cellWidth;
|
||||
var yPos = y * cellHeight;
|
||||
canvas.DrawBitmap(resizedBitmap, xPos, yPos);
|
||||
}
|
||||
}
|
||||
|
||||
return bitmap;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -2,63 +2,62 @@ using System.Globalization;
|
|||
using MediaBrowser.Model.Drawing;
|
||||
using SkiaSharp;
|
||||
|
||||
namespace Jellyfin.Drawing.Skia
|
||||
namespace Jellyfin.Drawing.Skia;
|
||||
|
||||
/// <summary>
|
||||
/// Static helper class for drawing unplayed count indicators.
|
||||
/// </summary>
|
||||
public static class UnplayedCountIndicator
|
||||
{
|
||||
/// <summary>
|
||||
/// Static helper class for drawing unplayed count indicators.
|
||||
/// The x-offset used when drawing an unplayed count indicator.
|
||||
/// </summary>
|
||||
public static class UnplayedCountIndicator
|
||||
private const int OffsetFromTopRightCorner = 38;
|
||||
|
||||
/// <summary>
|
||||
/// Draw an unplayed count indicator in the top right corner of a canvas.
|
||||
/// </summary>
|
||||
/// <param name="canvas">The canvas to draw the indicator on.</param>
|
||||
/// <param name="imageSize">
|
||||
/// The dimensions of the image to draw the indicator on. The width is used to determine the x-position of the
|
||||
/// indicator.
|
||||
/// </param>
|
||||
/// <param name="count">The number to draw in the indicator.</param>
|
||||
public static void DrawUnplayedCountIndicator(SKCanvas canvas, ImageDimensions imageSize, int count)
|
||||
{
|
||||
/// <summary>
|
||||
/// The x-offset used when drawing an unplayed count indicator.
|
||||
/// </summary>
|
||||
private const int OffsetFromTopRightCorner = 38;
|
||||
var x = imageSize.Width - OffsetFromTopRightCorner;
|
||||
var text = count.ToString(CultureInfo.InvariantCulture);
|
||||
|
||||
/// <summary>
|
||||
/// Draw an unplayed count indicator in the top right corner of a canvas.
|
||||
/// </summary>
|
||||
/// <param name="canvas">The canvas to draw the indicator on.</param>
|
||||
/// <param name="imageSize">
|
||||
/// The dimensions of the image to draw the indicator on. The width is used to determine the x-position of the
|
||||
/// indicator.
|
||||
/// </param>
|
||||
/// <param name="count">The number to draw in the indicator.</param>
|
||||
public static void DrawUnplayedCountIndicator(SKCanvas canvas, ImageDimensions imageSize, int count)
|
||||
using var paint = new SKPaint
|
||||
{
|
||||
var x = imageSize.Width - OffsetFromTopRightCorner;
|
||||
var text = count.ToString(CultureInfo.InvariantCulture);
|
||||
Color = SKColor.Parse("#CC00A4DC"),
|
||||
Style = SKPaintStyle.Fill
|
||||
};
|
||||
|
||||
using var paint = new SKPaint
|
||||
{
|
||||
Color = SKColor.Parse("#CC00A4DC"),
|
||||
Style = SKPaintStyle.Fill
|
||||
};
|
||||
canvas.DrawCircle(x, OffsetFromTopRightCorner, 20, paint);
|
||||
|
||||
canvas.DrawCircle(x, OffsetFromTopRightCorner, 20, paint);
|
||||
paint.Color = new SKColor(255, 255, 255, 255);
|
||||
paint.TextSize = 24;
|
||||
paint.IsAntialias = true;
|
||||
|
||||
paint.Color = new SKColor(255, 255, 255, 255);
|
||||
paint.TextSize = 24;
|
||||
paint.IsAntialias = true;
|
||||
var y = OffsetFromTopRightCorner + 9;
|
||||
|
||||
var y = OffsetFromTopRightCorner + 9;
|
||||
|
||||
if (text.Length == 1)
|
||||
{
|
||||
x -= 7;
|
||||
}
|
||||
|
||||
if (text.Length == 2)
|
||||
{
|
||||
x -= 13;
|
||||
}
|
||||
else if (text.Length >= 3)
|
||||
{
|
||||
x -= 15;
|
||||
y -= 2;
|
||||
paint.TextSize = 18;
|
||||
}
|
||||
|
||||
canvas.DrawText(text, x, y, paint);
|
||||
if (text.Length == 1)
|
||||
{
|
||||
x -= 7;
|
||||
}
|
||||
|
||||
if (text.Length == 2)
|
||||
{
|
||||
x -= 13;
|
||||
}
|
||||
else if (text.Length >= 3)
|
||||
{
|
||||
x -= 15;
|
||||
y -= 2;
|
||||
paint.TextSize = 18;
|
||||
}
|
||||
|
||||
canvas.DrawText(text, x, y, paint);
|
||||
}
|
||||
}
|
||||
|
|
File diff suppressed because it is too large
Load Diff
|
@ -3,56 +3,55 @@ using System.Collections.Generic;
|
|||
using MediaBrowser.Controller.Drawing;
|
||||
using MediaBrowser.Model.Drawing;
|
||||
|
||||
namespace Jellyfin.Drawing
|
||||
{
|
||||
/// <summary>
|
||||
/// A fallback implementation of <see cref="IImageEncoder" />.
|
||||
/// </summary>
|
||||
public class NullImageEncoder : IImageEncoder
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public IReadOnlyCollection<string> SupportedInputFormats
|
||||
=> new HashSet<string>(StringComparer.OrdinalIgnoreCase) { "png", "jpeg", "jpg" };
|
||||
namespace Jellyfin.Drawing;
|
||||
|
||||
/// <inheritdoc />
|
||||
public IReadOnlyCollection<ImageFormat> SupportedOutputFormats
|
||||
/// <summary>
|
||||
/// A fallback implementation of <see cref="IImageEncoder" />.
|
||||
/// </summary>
|
||||
public class NullImageEncoder : IImageEncoder
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public IReadOnlyCollection<string> SupportedInputFormats
|
||||
=> new HashSet<string>(StringComparer.OrdinalIgnoreCase) { "png", "jpeg", "jpg" };
|
||||
|
||||
/// <inheritdoc />
|
||||
public IReadOnlyCollection<ImageFormat> SupportedOutputFormats
|
||||
=> new HashSet<ImageFormat>() { ImageFormat.Jpg, ImageFormat.Png };
|
||||
|
||||
/// <inheritdoc />
|
||||
public string Name => "Null Image Encoder";
|
||||
/// <inheritdoc />
|
||||
public string Name => "Null Image Encoder";
|
||||
|
||||
/// <inheritdoc />
|
||||
public bool SupportsImageCollageCreation => false;
|
||||
/// <inheritdoc />
|
||||
public bool SupportsImageCollageCreation => false;
|
||||
|
||||
/// <inheritdoc />
|
||||
public bool SupportsImageEncoding => false;
|
||||
/// <inheritdoc />
|
||||
public bool SupportsImageEncoding => false;
|
||||
|
||||
/// <inheritdoc />
|
||||
public ImageDimensions GetImageSize(string path)
|
||||
=> throw new NotImplementedException();
|
||||
/// <inheritdoc />
|
||||
public ImageDimensions GetImageSize(string path)
|
||||
=> throw new NotImplementedException();
|
||||
|
||||
/// <inheritdoc />
|
||||
public string EncodeImage(string inputPath, DateTime dateModified, string outputPath, bool autoOrient, ImageOrientation? orientation, int quality, ImageProcessingOptions options, ImageFormat outputFormat)
|
||||
{
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
/// <inheritdoc />
|
||||
public string EncodeImage(string inputPath, DateTime dateModified, string outputPath, bool autoOrient, ImageOrientation? orientation, int quality, ImageProcessingOptions options, ImageFormat outputFormat)
|
||||
{
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public void CreateImageCollage(ImageCollageOptions options, string? libraryName)
|
||||
{
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
/// <inheritdoc />
|
||||
public void CreateImageCollage(ImageCollageOptions options, string? libraryName)
|
||||
{
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public void CreateSplashscreen(IReadOnlyList<string> posters, IReadOnlyList<string> backdrops)
|
||||
{
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
/// <inheritdoc />
|
||||
public void CreateSplashscreen(IReadOnlyList<string> posters, IReadOnlyList<string> backdrops)
|
||||
{
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public string GetImageBlurHash(int xComp, int yComp, string path)
|
||||
{
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
/// <inheritdoc />
|
||||
public string GetImageBlurHash(int xComp, int yComp, string path)
|
||||
{
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue
Block a user