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