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