2019-01-13 20:54:44 +01:00
using System ;
2019-02-06 21:31:41 +01:00
using System.Collections.Generic ;
2019-01-13 20:16:43 +01:00
using System.Globalization ;
using System.IO ;
2020-05-19 12:46:00 +02:00
using BlurHashSharp.SkiaSharp ;
2019-01-13 20:16:43 +01:00
using MediaBrowser.Common.Configuration ;
2020-11-13 19:14:44 +01:00
using MediaBrowser.Common.Extensions ;
2017-05-09 21:53:46 +02:00
using MediaBrowser.Controller.Drawing ;
2019-01-13 20:16:43 +01:00
using MediaBrowser.Controller.Extensions ;
2017-05-09 21:53:46 +02:00
using MediaBrowser.Model.Drawing ;
2018-12-13 14:18:25 +01:00
using Microsoft.Extensions.Logging ;
2017-05-09 21:53:46 +02:00
using SkiaSharp ;
2019-09-15 06:27:42 +02:00
using static Jellyfin . Drawing . Skia . SkiaHelper ;
2017-05-09 21:53:46 +02:00
2019-01-26 20:43:13 +01:00
namespace Jellyfin.Drawing.Skia
2017-05-09 21:53:46 +02:00
{
2019-12-13 20:57:23 +01:00
/// <summary>
/// Image encoder that uses <see cref="SkiaSharp"/> to manipulate images.
/// </summary>
2020-05-19 12:46:00 +02:00
public class SkiaEncoder : IImageEncoder
2017-05-09 21:53:46 +02:00
{
2019-06-09 23:51:52 +02:00
private static readonly HashSet < string > _transparentImageTypes
= new HashSet < string > ( StringComparer . OrdinalIgnoreCase ) { ".png" , ".gif" , ".webp" } ;
2017-05-09 21:53:46 +02:00
2020-06-06 02:15:56 +02:00
private readonly ILogger < SkiaEncoder > _logger ;
2020-01-21 20:26:30 +01:00
private readonly IApplicationPaths _appPaths ;
2019-12-13 20:57:23 +01:00
/// <summary>
/// Initializes a new instance of the <see cref="SkiaEncoder"/> class.
/// </summary>
2019-12-14 11:04:22 +01:00
/// <param name="logger">The application logger.</param>
/// <param name="appPaths">The application paths.</param>
2020-07-19 23:59:54 +02:00
public SkiaEncoder ( ILogger < SkiaEncoder > logger , IApplicationPaths appPaths )
2017-05-09 21:53:46 +02:00
{
2019-06-09 23:51:52 +02:00
_logger = logger ;
2017-05-09 21:53:46 +02:00
_appPaths = appPaths ;
}
2019-12-13 20:57:23 +01:00
/// <inheritdoc/>
2019-06-09 23:51:52 +02:00
public string Name = > "Skia" ;
2019-12-13 20:57:23 +01:00
/// <inheritdoc/>
2019-06-09 23:51:52 +02:00
public bool SupportsImageCollageCreation = > true ;
2019-12-13 20:57:23 +01:00
/// <inheritdoc/>
2019-06-09 23:51:52 +02:00
public bool SupportsImageEncoding = > true ;
2019-12-13 20:57:23 +01:00
/// <inheritdoc/>
2019-02-06 21:31:41 +01:00
public IReadOnlyCollection < string > SupportedInputFormats = >
new HashSet < string > ( StringComparer . OrdinalIgnoreCase )
2017-05-09 21:53:46 +02:00
{
2019-01-13 21:31:14 +01:00
"jpeg" ,
"jpg" ,
"png" ,
"dng" ,
"webp" ,
"gif" ,
"bmp" ,
"ico" ,
"astc" ,
"ktx" ,
"pkm" ,
"wbmp" ,
2019-12-14 07:01:14 +01:00
// TODO: check if these are supported on multiple platforms
// https://github.com/google/skia/blob/master/infra/bots/recipes/test.py#L454
2019-01-13 21:31:14 +01:00
// working on windows at least
"cr2" ,
"nef" ,
"arw"
} ;
2019-12-13 20:57:23 +01:00
/// <inheritdoc/>
2019-02-06 21:31:41 +01:00
public IReadOnlyCollection < ImageFormat > SupportedOutputFormats
= > new HashSet < ImageFormat > ( ) { ImageFormat . Webp , ImageFormat . Jpg , ImageFormat . Png } ;
2017-05-09 21:53:46 +02:00
2019-06-09 23:51:52 +02:00
/// <summary>
2020-04-04 23:12:24 +02:00
/// Check if the native lib is available.
2019-06-09 23:51:52 +02:00
/// </summary>
2020-04-04 23:12:24 +02:00
/// <returns>True if the native lib is available, otherwise false.</returns>
public static bool IsNativeLibAvailable ( )
2017-05-09 21:53:46 +02:00
{
2020-04-04 23:12:24 +02:00
try
{
// test an operation that requires the native library
SKPMColor . PreMultiply ( SKColors . Black ) ;
return true ;
}
catch ( Exception )
{
return false ;
}
2017-05-09 21:53:46 +02:00
}
2019-12-13 20:57:23 +01:00
/// <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>
2017-05-09 21:53:46 +02:00
public static SKEncodedImageFormat GetImageFormat ( ImageFormat selectedFormat )
{
2020-07-19 20:16:33 +02:00
return selectedFormat switch
2017-05-09 21:53:46 +02:00
{
2020-07-19 20:16:33 +02:00
ImageFormat . Bmp = > SKEncodedImageFormat . Bmp ,
ImageFormat . Jpg = > SKEncodedImageFormat . Jpeg ,
ImageFormat . Gif = > SKEncodedImageFormat . Gif ,
ImageFormat . Webp = > SKEncodedImageFormat . Webp ,
_ = > SKEncodedImageFormat . Png
} ;
2017-05-09 21:53:46 +02:00
}
2019-09-15 06:27:42 +02:00
/// <inheritdoc />
2019-12-14 15:47:35 +01:00
/// <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>
2019-01-26 13:16:47 +01:00
public ImageDimensions GetImageSize ( string path )
2017-05-09 21:53:46 +02:00
{
2019-06-23 16:13:50 +02:00
if ( ! File . Exists ( path ) )
{
throw new FileNotFoundException ( "File not found" , path ) ;
}
2020-07-19 20:12:53 +02:00
using var codec = SKCodec . Create ( path , out SKCodecResult result ) ;
EnsureSuccess ( result ) ;
2019-09-15 06:27:42 +02:00
2020-07-19 20:12:53 +02:00
var info = codec . Info ;
2017-05-09 21:53:46 +02:00
2020-07-19 20:12:53 +02:00
return new ImageDimensions ( info . Width , info . Height ) ;
2017-05-09 21:53:46 +02:00
}
2020-03-23 20:05:49 +01:00
/// <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>
2020-06-01 17:12:49 +02:00
public string GetImageBlurHash ( int xComp , int yComp , string path )
2020-03-23 20:05:49 +01:00
{
if ( path = = null )
{
throw new ArgumentNullException ( nameof ( path ) ) ;
}
2020-07-30 22:50:13 +02:00
// Any larger than 128x128 is too slow and there's no visually discernible difference
return BlurHashEncoder . Encode ( xComp , yComp , path , 128 , 128 ) ;
2020-03-23 20:05:49 +01:00
}
2017-11-01 20:45:10 +01:00
private static bool HasDiacritics ( string text )
2019-01-26 21:10:19 +01:00
= > ! string . Equals ( text , text . RemoveDiacritics ( ) , StringComparison . Ordinal ) ;
2017-11-01 20:45:10 +01:00
2019-06-09 23:51:52 +02:00
private bool RequiresSpecialCharacterHack ( string path )
2017-11-01 20:45:10 +01:00
{
2020-01-21 20:26:30 +01:00
for ( int i = 0 ; i < path . Length ; i + + )
2017-11-01 20:45:10 +01:00
{
2020-01-21 20:26:30 +01:00
if ( char . GetUnicodeCategory ( path [ i ] ) = = UnicodeCategory . OtherLetter )
{
return true ;
}
2017-11-01 20:45:10 +01:00
}
2020-07-19 20:18:23 +02:00
return HasDiacritics ( path ) ;
2017-11-01 20:45:10 +01:00
}
2019-06-09 23:51:52 +02:00
private string NormalizePath ( string path )
2017-11-01 20:45:10 +01:00
{
if ( ! RequiresSpecialCharacterHack ( path ) )
{
return path ;
}
2019-12-14 07:01:14 +01:00
var tempPath = Path . Combine ( _appPaths . TempDirectory , Guid . NewGuid ( ) + Path . GetExtension ( path ) ) ;
2020-11-14 02:04:06 +01:00
var directory = Path . GetDirectoryName ( tempPath ) ? ? throw new ResourceNotFoundException ( $"Provided path ({tempPath}) is not valid." ) ;
2020-11-13 16:34:34 +01:00
Directory . CreateDirectory ( directory ) ;
2019-01-26 22:31:59 +01:00
File . Copy ( path , tempPath , true ) ;
2017-11-01 20:45:10 +01:00
return tempPath ;
}
2018-12-14 17:46:43 +01:00
private static SKEncodedOrigin GetSKEncodedOrigin ( ImageOrientation ? orientation )
2018-09-12 19:26:21 +02:00
{
if ( ! orientation . HasValue )
{
2018-12-13 22:34:28 +01:00
return SKEncodedOrigin . TopLeft ;
2018-09-12 19:26:21 +02:00
}
2020-07-19 20:16:33 +02:00
return orientation . Value switch
2018-09-12 19:26:21 +02:00
{
2020-07-19 20:16:33 +02:00
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
} ;
2018-09-12 19:26:21 +02:00
}
2019-12-14 11:04:22 +01:00
/// <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>
2020-04-05 21:19:04 +02:00
internal SKBitmap ? Decode ( string path , bool forceCleanBitmap , ImageOrientation ? orientation , out SKEncodedOrigin origin )
2017-05-10 21:56:59 +02:00
{
2019-01-26 22:59:53 +01:00
if ( ! File . Exists ( path ) )
2017-11-01 20:45:10 +01:00
{
throw new FileNotFoundException ( "File not found" , path ) ;
}
2019-06-09 23:51:52 +02:00
var requiresTransparencyHack = _transparentImageTypes . Contains ( Path . GetExtension ( path ) ) ;
2017-05-11 05:23:16 +02:00
2017-05-17 20:18:18 +02:00
if ( requiresTransparencyHack | | forceCleanBitmap )
2017-05-10 21:56:59 +02:00
{
2020-07-19 20:12:53 +02:00
using var codec = SKCodec . Create ( NormalizePath ( path ) ) ;
if ( codec = = null )
2017-05-11 05:23:16 +02:00
{
2020-07-19 20:12:53 +02:00
origin = GetSKEncodedOrigin ( orientation ) ;
return null ;
}
2017-08-31 05:49:38 +02:00
2020-07-19 20:12:53 +02:00
// create the bitmap
var bitmap = new SKBitmap ( codec . Info . Width , codec . Info . Height , ! requiresTransparencyHack ) ;
2017-05-10 21:56:59 +02:00
2020-07-19 20:12:53 +02:00
// decode
_ = codec . GetPixels ( bitmap . Info , bitmap . GetPixels ( ) ) ;
2017-06-09 21:24:31 +02:00
2020-07-19 20:12:53 +02:00
origin = codec . EncodedOrigin ;
2018-12-13 22:34:28 +01:00
2020-07-19 20:12:53 +02:00
return bitmap ;
2017-05-10 21:56:59 +02:00
}
2017-05-11 05:23:16 +02:00
2019-06-09 23:51:52 +02:00
var resultBitmap = SKBitmap . Decode ( NormalizePath ( path ) ) ;
2017-05-17 20:18:18 +02:00
2017-05-18 23:05:47 +02:00
if ( resultBitmap = = null )
{
2019-06-09 23:51:52 +02:00
return Decode ( path , true , orientation , out origin ) ;
2017-05-18 23:05:47 +02:00
}
2017-05-17 20:18:18 +02:00
// If we have to resize these they often end up distorted
if ( resultBitmap . ColorType = = SKColorType . Gray8 )
{
using ( resultBitmap )
{
2019-06-09 23:51:52 +02:00
return Decode ( path , true , orientation , out origin ) ;
2017-05-17 20:18:18 +02:00
}
}
2018-12-13 22:34:28 +01:00
origin = SKEncodedOrigin . TopLeft ;
2017-05-17 20:18:18 +02:00
return resultBitmap ;
2017-05-10 21:56:59 +02:00
}
2021-01-09 10:51:59 +01:00
private SKBitmap ? GetBitmap ( string path , bool autoOrient , ImageOrientation ? orientation )
2017-06-09 21:24:31 +02:00
{
if ( autoOrient )
{
2021-01-09 10:51:59 +01:00
var bitmap = Decode ( path , true , orientation , out var origin ) ;
2017-06-09 21:24:31 +02:00
2019-01-26 21:10:19 +01:00
if ( bitmap ! = null & & origin ! = SKEncodedOrigin . TopLeft )
2017-06-09 21:24:31 +02:00
{
2019-01-26 21:10:19 +01:00
using ( bitmap )
2017-06-09 21:24:31 +02:00
{
2019-01-26 21:10:19 +01:00
return OrientImage ( bitmap , origin ) ;
2017-06-09 21:24:31 +02:00
}
}
return bitmap ;
}
2021-01-09 10:51:59 +01:00
return Decode ( path , false , orientation , out _ ) ;
2017-06-09 21:24:31 +02:00
}
2018-12-13 22:34:28 +01:00
private SKBitmap OrientImage ( SKBitmap bitmap , SKEncodedOrigin origin )
2017-09-02 07:33:04 +02:00
{
2020-07-19 23:59:33 +02:00
if ( origin = = SKEncodedOrigin . Default )
2017-09-02 07:33:04 +02:00
{
2020-07-19 23:59:33 +02:00
return bitmap ;
}
2017-09-02 07:33:04 +02:00
2020-07-19 23:59:33 +02:00
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 ;
2017-09-02 07:33:04 +02:00
2020-07-19 23:59:33 +02:00
switch ( origin )
{
case SKEncodedOrigin . TopRight :
surface . Scale ( - 1 , 1 , midX , midY ) ;
break ;
2018-12-13 22:34:28 +01:00
case SKEncodedOrigin . BottomRight :
2020-07-19 23:59:33 +02:00
surface . RotateDegrees ( 180 , midX , midY ) ;
break ;
2018-12-13 22:34:28 +01:00
case SKEncodedOrigin . BottomLeft :
2020-07-19 23:59:33 +02:00
surface . Scale ( 1 , - 1 , midX , midY ) ;
break ;
2018-12-13 22:34:28 +01:00
case SKEncodedOrigin . LeftTop :
2020-07-19 23:59:33 +02:00
surface . Translate ( 0 , - rotated . Height ) ;
surface . Scale ( 1 , - 1 , midX , midY ) ;
surface . RotateDegrees ( - 90 ) ;
break ;
2018-12-13 22:34:28 +01:00
case SKEncodedOrigin . RightTop :
2020-07-19 23:59:33 +02:00
surface . Translate ( rotated . Width , 0 ) ;
surface . RotateDegrees ( 90 ) ;
break ;
2018-12-13 22:34:28 +01:00
case SKEncodedOrigin . RightBottom :
2020-07-19 23:59:33 +02:00
surface . Translate ( rotated . Width , 0 ) ;
surface . Scale ( 1 , - 1 , midX , midY ) ;
surface . RotateDegrees ( 90 ) ;
break ;
2018-12-13 22:34:28 +01:00
case SKEncodedOrigin . LeftBottom :
2020-07-19 23:59:33 +02:00
surface . Translate ( 0 , rotated . Height ) ;
surface . RotateDegrees ( - 90 ) ;
break ;
2017-06-09 21:24:31 +02:00
}
2020-07-19 23:59:33 +02:00
surface . DrawBitmap ( bitmap , 0 , 0 ) ;
return rotated ;
2017-05-10 06:49:11 +02:00
}
2020-07-31 21:20:05 +02:00
/// <summary>
/// Resizes an image on the CPU, by utilizing a surface and canvas.
2020-07-31 22:02:16 +02:00
///
/// 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).
2020-07-31 21:20:05 +02:00
/// </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 ;
2020-07-31 22:12:20 +02:00
using var paint = new SKPaint
{
FilterQuality = SKFilterQuality . High ,
IsAntialias = isAntialias ,
IsDither = isDither
} ;
2020-07-31 21:20:05 +02:00
var kernel = new float [ 9 ]
{
2020-07-31 21:33:25 +02:00
0 , - . 1f , 0 ,
- . 1f , 1.4f , - . 1f ,
0 , - . 1f , 0 ,
2020-07-31 21:20:05 +02:00
} ;
var kernelSize = new SKSizeI ( 3 , 3 ) ;
var kernelOffset = new SKPointI ( 1 , 1 ) ;
paint . ImageFilter = SKImageFilter . CreateMatrixConvolution (
2020-07-31 22:12:20 +02:00
kernelSize ,
kernel ,
1f ,
0f ,
kernelOffset ,
SKShaderTileMode . Clamp ,
2020-12-11 16:06:04 +01:00
true ) ;
2020-07-31 22:12:20 +02:00
canvas . DrawBitmap (
source ,
SKRect . Create ( 0 , 0 , source . Width , source . Height ) ,
SKRect . Create ( 0 , 0 , targetInfo . Width , targetInfo . Height ) ,
paint ) ;
2020-07-31 21:20:05 +02:00
return surface . Snapshot ( ) ;
}
2019-12-13 20:57:23 +01:00
/// <inheritdoc/>
2017-06-09 21:24:31 +02:00
public string EncodeImage ( string inputPath , DateTime dateModified , string outputPath , bool autoOrient , ImageOrientation ? orientation , int quality , ImageProcessingOptions options , ImageFormat selectedOutputFormat )
2017-05-09 21:53:46 +02:00
{
2020-04-05 21:19:04 +02:00
if ( inputPath . Length = = 0 )
2017-05-09 22:18:02 +02:00
{
2020-04-05 21:19:04 +02:00
throw new ArgumentException ( "String can't be empty." , nameof ( inputPath ) ) ;
2017-05-09 22:18:02 +02:00
}
2019-01-26 21:10:19 +01:00
2020-04-05 21:19:04 +02:00
if ( outputPath . Length = = 0 )
2017-05-09 22:18:02 +02:00
{
2020-04-05 21:19:04 +02:00
throw new ArgumentException ( "String can't be empty." , nameof ( outputPath ) ) ;
2017-05-09 22:18:02 +02:00
}
2017-05-10 06:49:11 +02:00
var skiaOutputFormat = GetImageFormat ( selectedOutputFormat ) ;
var hasBackgroundColor = ! string . IsNullOrWhiteSpace ( options . BackgroundColor ) ;
var hasForegroundColor = ! string . IsNullOrWhiteSpace ( options . ForegroundLayer ) ;
var blur = options . Blur ? ? 0 ;
2017-05-11 05:23:16 +02:00
var hasIndicator = options . AddPlayedIndicator | | options . UnplayedCount . HasValue | | ! options . PercentPlayed . Equals ( 0 ) ;
2017-05-10 06:49:11 +02:00
2021-01-09 10:51:59 +01:00
using var bitmap = GetBitmap ( inputPath , autoOrient , orientation ) ;
2020-07-19 20:12:53 +02:00
if ( bitmap = = null )
2017-05-09 21:53:46 +02:00
{
2020-07-19 20:12:53 +02:00
throw new InvalidDataException ( $"Skia unable to read image {inputPath}" ) ;
}
2017-05-17 20:18:18 +02:00
2020-07-19 20:12:53 +02:00
var originalImageSize = new ImageDimensions ( bitmap . Width , bitmap . Height ) ;
2017-05-17 20:18:18 +02:00
2021-01-09 10:51:59 +01:00
if ( options . HasDefaultOptions ( inputPath , originalImageSize ) & & ! autoOrient )
2020-07-19 20:12:53 +02:00
{
// Just spit out the original file if all the options are default
return inputPath ;
}
var newImageSize = ImageHelper . GetNewImageSize ( options , originalImageSize ) ;
2017-05-15 04:27:58 +02:00
2020-07-19 20:12:53 +02:00
var width = newImageSize . Width ;
var height = newImageSize . Height ;
2017-05-15 04:27:58 +02:00
2020-07-31 21:20:05 +02:00
// scale image (the FromImage creates a copy)
2020-08-02 12:43:25 +02:00
var imageInfo = new SKImageInfo ( width , height , bitmap . ColorType , bitmap . AlphaType , bitmap . ColorSpace ) ;
using var resizedBitmap = SKBitmap . FromImage ( ResizeImage ( bitmap , imageInfo ) ) ;
2020-07-19 20:12:53 +02:00
// If all we're doing is resizing then we can stop now
if ( ! hasBackgroundColor & & ! hasForegroundColor & & blur = = 0 & & ! hasIndicator )
{
2020-11-14 02:04:06 +01:00
var outputDirectory = Path . GetDirectoryName ( outputPath ) ? ? throw new ArgumentException ( $"Provided path ({outputPath}) is not valid." , nameof ( outputPath ) ) ;
2020-11-13 16:34:34 +01:00
Directory . CreateDirectory ( outputDirectory ) ;
2020-07-19 20:12:53 +02:00
using var outputStream = new SKFileWStream ( outputPath ) ;
using var pixmap = new SKPixmap ( new SKImageInfo ( width , height ) , resizedBitmap . GetPixels ( ) ) ;
2020-07-31 21:20:05 +02:00
resizedBitmap . Encode ( outputStream , skiaOutputFormat , quality ) ;
2020-07-19 20:12:53 +02:00
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 ) ) ;
}
2017-05-15 04:27:58 +02:00
2020-07-19 20:12:53 +02:00
// If foreground layer present then draw
if ( hasForegroundColor )
{
if ( ! double . TryParse ( options . ForegroundLayer , out double opacity ) )
2017-05-09 21:53:46 +02:00
{
2020-07-19 20:12:53 +02:00
opacity = . 4 ;
}
2017-05-10 06:49:11 +02:00
2020-07-19 20:12:53 +02:00
canvas . DrawColor ( new SKColor ( 0 , 0 , 0 , ( byte ) ( ( 1 - opacity ) * 0xFF ) ) , SKBlendMode . SrcOver ) ;
}
2017-05-09 21:53:46 +02:00
2020-07-19 20:12:53 +02:00
if ( hasIndicator )
{
DrawIndicator ( canvas , width , height , options ) ;
}
2020-11-14 02:04:06 +01:00
var directory = Path . GetDirectoryName ( outputPath ) ? ? throw new ArgumentException ( $"Provided path ({outputPath}) is not valid." , nameof ( outputPath ) ) ;
2020-11-13 16:34:34 +01:00
Directory . CreateDirectory ( directory ) ;
2020-07-19 20:12:53 +02:00
using ( var outputStream = new SKFileWStream ( outputPath ) )
{
using ( var pixmap = new SKPixmap ( new SKImageInfo ( width , height ) , saveBitmap . GetPixels ( ) ) )
{
pixmap . Encode ( outputStream , skiaOutputFormat , quality ) ;
2017-05-09 21:53:46 +02:00
}
}
2019-12-14 11:04:22 +01:00
2017-05-17 20:18:18 +02:00
return outputPath ;
2017-05-09 21:53:46 +02:00
}
2019-12-13 20:57:23 +01:00
/// <inheritdoc/>
2020-08-21 19:53:55 +02:00
public void CreateImageCollage ( ImageCollageOptions options , string? libraryName )
2017-05-09 21:53:46 +02:00
{
2019-01-26 21:10:19 +01:00
double ratio = ( double ) options . Width / options . Height ;
2017-05-09 21:53:46 +02:00
if ( ratio > = 1.4 )
{
2020-08-21 19:53:55 +02:00
new StripCollageBuilder ( this ) . BuildThumbCollage ( options . InputPaths , options . OutputPath , options . Width , options . Height , libraryName ) ;
2017-05-09 21:53:46 +02:00
}
else if ( ratio > = . 9 )
{
2019-06-09 23:51:52 +02:00
new StripCollageBuilder ( this ) . BuildSquareCollage ( options . InputPaths , options . OutputPath , options . Width , options . Height ) ;
2017-05-09 21:53:46 +02:00
}
else
{
2019-01-26 21:10:19 +01:00
// TODO: Create Poster collage capability
2019-06-09 23:51:52 +02:00
new StripCollageBuilder ( this ) . BuildSquareCollage ( options . InputPaths , options . OutputPath , options . Width , options . Height ) ;
2017-05-09 21:53:46 +02:00
}
}
private void DrawIndicator ( SKCanvas canvas , int imageWidth , int imageHeight , ImageProcessingOptions options )
{
try
{
2019-01-26 13:16:47 +01:00
var currentImageSize = new ImageDimensions ( imageWidth , imageHeight ) ;
2017-05-09 21:53:46 +02:00
if ( options . AddPlayedIndicator )
{
2019-01-20 14:25:13 +01:00
PlayedIndicatorDrawer . DrawPlayedIndicator ( canvas , currentImageSize ) ;
2017-05-09 21:53:46 +02:00
}
else if ( options . UnplayedCount . HasValue )
{
2019-01-20 14:25:13 +01:00
UnplayedCountIndicator . DrawUnplayedCountIndicator ( canvas , currentImageSize , options . UnplayedCount . Value ) ;
2017-05-09 21:53:46 +02:00
}
if ( options . PercentPlayed > 0 )
{
2019-01-20 14:25:13 +01:00
PercentPlayedDrawer . Process ( canvas , currentImageSize , options . PercentPlayed ) ;
2017-05-09 21:53:46 +02:00
}
}
catch ( Exception ex )
{
2018-12-20 13:11:26 +01:00
_logger . LogError ( ex , "Error drawing indicator overlay" ) ;
2017-05-09 21:53:46 +02:00
}
}
}
2018-12-14 17:46:43 +01:00
}