@ -29,9 +29,7 @@ namespace Jellyfin.Drawing.Skia
/// </summary>
/// <param name="logger">The application logger.</param>
/// <param name="appPaths">The application paths.</param>
public SkiaEncoder (
ILogger < SkiaEncoder > logger ,
IApplicationPaths appPaths )
public SkiaEncoder ( ILogger < SkiaEncoder > logger , IApplicationPaths appPaths )
{
_logger = logger ;
_appPaths = appPaths ;
@ -102,19 +100,14 @@ namespace Jellyfin.Drawing.Skia
/// <returns>The converted format.</returns>
public static SKEncodedImageFormat GetImageFormat ( ImageFormat selectedFormat )
{
switch ( selectedFormat )
{
case ImageFormat . Bmp :
return SKEncodedImageFormat . Bmp ;
case ImageFormat . Jpg :
return SKEncodedImageFormat . Jpeg ;
case ImageFormat . Gif :
return SKEncodedImageFormat . Gif ;
case ImageFormat . Webp :
return SKEncodedImageFormat . Webp ;
default :
return SKEncodedImageFormat . Png ;
}
return selectedFormat switch
{
ImageFormat . Bmp = > SKEncodedImageFormat . Bmp ,
ImageFormat . Jpg = > SKEncodedImageFormat . Jpeg ,
ImageFormat . Gif = > SKEncodedImageFormat . Gif ,
ImageFormat . Webp = > SKEncodedImageFormat . Webp ,
_ = > SKEncodedImageFormat . Png
} ;
}
private static bool IsTransparentRow ( SKBitmap bmp , int row )
@ -146,63 +139,34 @@ namespace Jellyfin.Drawing.Skia
private SKBitmap CropWhiteSpace ( SKBitmap bitmap )
{
var topmost = 0 ;
for ( int row = 0 ; row < bitmap . Height ; + + row )
while ( topmost < bitmap . Height & & IsTransparentRow ( bitmap , topmost ) )
{
if ( IsTransparentRow ( bitmap , row ) )
{
topmost = row + 1 ;
}
else
{
break ;
}
topmost + + ;
}
int bottommost = bitmap . Height ;
for ( int row = bitmap . Height - 1 ; row > = 0 ; - - row )
while ( bottommost > = 0 & & IsTransparentRow ( bitmap , bottommost - 1 ) )
{
if ( IsTransparentRow ( bitmap , row ) )
{
bottommost = row ;
}
else
{
break ;
}
bottommost - - ;
}
int leftmost = 0 , rightmost = bitmap . Width ;
for ( int col = 0 ; col < bitmap . Width ; + + col )
var leftmost = 0 ;
while ( leftmost < bitmap . Width & & IsTransparentColumn ( bitmap , leftmost ) )
{
if ( IsTransparentColumn ( bitmap , col ) )
{
leftmost = col + 1 ;
}
else
{
break ;
}
leftmost + + ;
}
for ( int col = bitmap . Width - 1 ; col > = 0 ; - - col )
var rightmost = bitmap . Width ;
while ( rightmost > = 0 & & IsTransparentColumn ( bitmap , rightmost - 1 ) )
{
if ( IsTransparentColumn ( bitmap , col ) )
{
rightmost = col ;
}
else
{
break ;
}
rightmost - - ;
}
var newRect = SKRectI . Create ( leftmost , topmost , rightmost - leftmost , bottommost - topmost ) ;
using ( var image = SKImage . FromBitmap ( bitmap ) )
using ( var subset = image . Subset ( newRect ) )
{
return SKBitmap . FromImage ( subset ) ;
}
using var image = SKImage . FromBitmap ( bitmap ) ;
using var subset = image . Subset ( newRect ) ;
return SKBitmap . FromImage ( subset ) ;
}
/// <inheritdoc />
@ -216,14 +180,12 @@ namespace Jellyfin.Drawing.Skia
throw new FileNotFoundException ( "File not found" , path ) ;
}
using ( var codec = SKCodec . Create ( path , out SKCodecResult result ) )
{
EnsureSuccess ( result ) ;
using var codec = SKCodec . Create ( path , out SKCodecResult result ) ;
EnsureSuccess ( result ) ;
var info = codec . Info ;
var info = codec . Info ;
return new ImageDimensions ( info . Width , info . Height ) ;
}
return new ImageDimensions ( info . Width , info . Height ) ;
}
/// <inheritdoc />
@ -237,7 +199,8 @@ namespace Jellyfin.Drawing.Skia
throw new ArgumentNullException ( nameof ( path ) ) ;
}
return BlurHashEncoder . Encode ( xComp , yComp , path ) ;
// Any larger than 128x128 is too slow and there's no visually discernible difference
return BlurHashEncoder . Encode ( xComp , yComp , path , 128 , 128 ) ;
}
private static bool HasDiacritics ( string text )
@ -253,12 +216,7 @@ namespace Jellyfin.Drawing.Skia
}
}
if ( HasDiacritics ( path ) )
{
return true ;
}
return false ;
return HasDiacritics ( path ) ;
}
private string NormalizePath ( string path )
@ -283,25 +241,17 @@ namespace Jellyfin.Drawing.Skia
return SKEncodedOrigin . TopLeft ;
}
switch ( orientation . Value )
{
case ImageOrientation . TopRight :
return SKEncodedOrigin . TopRight ;
case ImageOrientation . RightTop :
return SKEncodedOrigin . RightTop ;
case ImageOrientation . RightBottom :
return SKEncodedOrigin . RightBottom ;
case ImageOrientation . LeftTop :
return SKEncodedOrigin . LeftTop ;
case ImageOrientation . LeftBottom :
return SKEncodedOrigin . LeftBottom ;
case ImageOrientation . BottomRight :
return SKEncodedOrigin . BottomRight ;
case ImageOrientation . BottomLeft :
return SKEncodedOrigin . BottomLeft ;
default :
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>
@ -323,24 +273,22 @@ namespace Jellyfin.Drawing.Skia
if ( requiresTransparencyHack | | forceCleanBitmap )
{
using ( var codec = SKCodec . Create ( NormalizePath ( path ) ) )
using var codec = SKCodec . Create ( NormalizePath ( path ) ) ;
if ( codec = = null )
{
if ( codec = = null )
{
origin = GetSKEncodedOrigin ( orientation ) ;
return null ;
}
origin = GetSKEncodedOrigin ( orientation ) ;
return null ;
}
// create the bitmap
var bitmap = new SKBitmap ( codec . Info . Width , codec . Info . Height , ! requiresTransparencyHack ) ;
// create the bitmap
var bitmap = new SKBitmap ( codec . Info . Width , codec . Info . Height , ! requiresTransparencyHack ) ;
// decode
_ = codec . GetPixels ( bitmap . Info , bitmap . GetPixels ( ) ) ;
// decode
_ = codec . GetPixels ( bitmap . Info , bitmap . GetPixels ( ) ) ;
origin = codec . EncodedOrigin ;
origin = codec . EncodedOrigin ;
return bitmap ;
}
return bitmap ;
}
var resultBitmap = SKBitmap . Decode ( NormalizePath ( path ) ) ;
@ -367,15 +315,8 @@ namespace Jellyfin.Drawing.Skia
{
if ( cropWhitespace )
{
using ( var bitmap = Decode ( path , forceAnalyzeBitmap , orientation , out origin ) )
{
if ( bitmap = = null )
{
return null ;
}
return CropWhiteSpace ( bitmap ) ;
}
using var bitmap = Decode ( path , forceAnalyzeBitmap , orientation , out origin ) ;
return bitmap = = null ? null : CropWhiteSpace ( bitmap ) ;
}
return Decode ( path , forceAnalyzeBitmap , orientation , out origin ) ;
@ -403,133 +344,105 @@ namespace Jellyfin.Drawing.Skia
private SKBitmap OrientImage ( SKBitmap bitmap , SKEncodedOrigin origin )
{
if ( origin = = SKEncodedOrigin . Default )
{
return bitmap ;
}
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 )
{
case SKEncodedOrigin . TopRight :
{
var rotated = new SKBitmap ( bitmap . Width , bitmap . Height ) ;
using ( var surface = new SKCanvas ( rotated ) )
{
surface . Translate ( rotated . Width , 0 ) ;
surface . Scale ( - 1 , 1 ) ;
surface . DrawBitmap ( bitmap , 0 , 0 ) ;
}
return rotated ;
}
surface . Scale ( - 1 , 1 , midX , midY ) ;
break ;
case SKEncodedOrigin . BottomRight :
{
var rotated = new SKBitmap ( bitmap . Width , bitmap . Height ) ;
using ( var surface = new SKCanvas ( rotated ) )
{
float px = ( float ) bitmap . Width / 2 ;
float py = ( float ) bitmap . Height / 2 ;
surface . RotateDegrees ( 180 , px , py ) ;
surface . DrawBitmap ( bitmap , 0 , 0 ) ;
}
return rotated ;
}
surface . RotateDegrees ( 180 , midX , midY ) ;
break ;
case SKEncodedOrigin . BottomLeft :
{
var rotated = new SKBitmap ( bitmap . Width , bitmap . Height ) ;
using ( var surface = new SKCanvas ( rotated ) )
{
float px = ( float ) bitmap . Width / 2 ;
float py = ( float ) bitmap . Height / 2 ;
surface . Translate ( rotated . Width , 0 ) ;
surface . Scale ( - 1 , 1 ) ;
surface . RotateDegrees ( 180 , px , py ) ;
surface . DrawBitmap ( bitmap , 0 , 0 ) ;
}
return rotated ;
}
surface . Scale ( 1 , - 1 , midX , midY ) ;
break ;
case SKEncodedOrigin . LeftTop :
{
// TODO: Remove dual canvases, had trouble with flipping
using ( var rotated = new SKBitmap ( bitmap . Height , bitmap . Width ) )
{
using ( var surface = new SKCanvas ( rotated ) )
{
surface . Translate ( rotated . Width , 0 ) ;
surface . RotateDegrees ( 90 ) ;
surface . DrawBitmap ( bitmap , 0 , 0 ) ;
}
var flippedBitmap = new SKBitmap ( rotated . Width , rotated . Height ) ;
using ( var flippedCanvas = new SKCanvas ( flippedBitmap ) )
{
flippedCanvas . Translate ( flippedBitmap . Width , 0 ) ;
flippedCanvas . Scale ( - 1 , 1 ) ;
flippedCanvas . DrawBitmap ( rotated , 0 , 0 ) ;
}
return flippedBitmap ;
}
}
surface . Translate ( 0 , - rotated . Height ) ;
surface . Scale ( 1 , - 1 , midX , midY ) ;
surface . RotateDegrees ( - 90 ) ;
break ;
case SKEncodedOrigin . RightTop :
{
var rotated = new SKBitmap ( bitmap . Height , bitmap . Width ) ;
using ( var surface = new SKCanvas ( rotated ) )
{
surface . Translate ( rotated . Width , 0 ) ;
surface . RotateDegrees ( 90 ) ;
surface . DrawBitmap ( bitmap , 0 , 0 ) ;
}
return rotated ;
}
surface . Translate ( rotated . Width , 0 ) ;
surface . RotateDegrees ( 90 ) ;
break ;
case SKEncodedOrigin . RightBottom :
{
// TODO: Remove dual canvases, had trouble with flipping
using ( var rotated = new SKBitmap ( bitmap . Height , bitmap . Width ) )
{
using ( var surface = new SKCanvas ( rotated ) )
{
surface . Translate ( 0 , rotated . Height ) ;
surface . RotateDegrees ( 270 ) ;
surface . DrawBitmap ( bitmap , 0 , 0 ) ;
}
var flippedBitmap = new SKBitmap ( rotated . Width , rotated . Height ) ;
using ( var flippedCanvas = new SKCanvas ( flippedBitmap ) )
{
flippedCanvas . Translate ( flippedBitmap . Width , 0 ) ;
flippedCanvas . Scale ( - 1 , 1 ) ;
flippedCanvas . DrawBitmap ( rotated , 0 , 0 ) ;
}
return flippedBitmap ;
}
}
surface . Translate ( rotated . Width , 0 ) ;
surface . Scale ( 1 , - 1 , midX , midY ) ;
surface . RotateDegrees ( 90 ) ;
break ;
case SKEncodedOrigin . LeftBottom :
{
var rotated = new SKBitmap ( bitmap . Height , bitmap . Width ) ;
using ( var surface = new SKCanvas ( rotated ) )
{
surface . Translate ( 0 , rotated . Height ) ;
surface . RotateDegrees ( 270 ) ;
surface . DrawBitmap ( bitmap , 0 , 0 ) ;
}
return rotated ;
}
default : return bitmap ;
surface . Translate ( 0 , rotated . Height ) ;
surface . RotateDegrees ( - 90 ) ;
break ;
}
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
{
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 ,
false ) ;
canvas . DrawBitmap (
source ,
SKRect . Create ( 0 , 0 , source . Width , source . Height ) ,
SKRect . Create ( 0 , 0 , targetInfo . Width , targetInfo . Height ) ,
paint ) ;
return surface . Snapshot ( ) ;
}
/// <inheritdoc/>
@ -552,97 +465,87 @@ namespace Jellyfin.Drawing.Skia
var blur = options . Blur ? ? 0 ;
var hasIndicator = options . AddPlayedIndicator | | options . UnplayedCount . HasValue | | ! options . PercentPlayed . Equals ( 0 ) ;
using ( var bitmap = GetBitmap ( inputPath , options . CropWhiteSpace , autoOrient , orientation ) )
using var bitmap = GetBitmap ( inputPath , options . CropWhiteSpace , autoOrient , orientation ) ;
if ( bitmap = = null )
{
if ( bitmap = = null )
{
throw new InvalidDataException ( $"Skia unable to read image {inputPath}" ) ;
}
throw new InvalidDataException ( $"Skia unable to read image {inputPath}" ) ;
}
var originalImageSize = new ImageDimensions ( bitmap . Width , bitmap . Height ) ;
var originalImageSize = new ImageDimensions ( bitmap . Width , bitmap . Height ) ;
if ( ! options . CropWhiteSpace
& & options . HasDefaultOptions ( inputPath , originalImageSize )
& & ! autoOrient )
{
// Just spit out the original file if all the options are default
return inputPath ;
}
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 ) ;
var width = newImageSize . Width ;
var height = newImageSize . Height ;
var newImageSize = ImageHelper . GetNewImageSize ( options , originalImageSize ) ;
// 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 ) ) ;
var width = newImageSize . Width ;
var height = newImageSize . Height ;
// If all we're doing is resizing then we can stop now
if ( ! hasBackgroundColor & & ! hasForegroundColor & & blur = = 0 & & ! hasIndicator )
{
Directory . CreateDirectory ( Path . GetDirectoryName ( outputPath ) ) ;
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 ) ) ;
}
using ( var resizedBitmap = new SKBitmap ( width , height , bitmap . ColorType , bitmap . AlphaType ) )
// 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 ) )
{
// scale image
bitmap . ScalePixels ( resizedBitmap , SKFilterQuality . High ) ;
opacity = . 4 ;
}
// If all we're doing is resizing then we can stop now
if ( ! hasBackgroundColor & & ! hasForegroundColor & & blur = = 0 & & ! hasIndicator )
{
Directory . CreateDirectory ( Path . GetDirectoryName ( outputPath ) ) ;
using ( var outputStream = new SKFileWStream ( outputPath ) )
using ( var pixmap = new SKPixmap ( new SKImageInfo ( width , height ) , resizedBitmap . GetPixels ( ) ) )
{
pixmap . Encode ( outputStream , skiaOutputFormat , quality ) ;
return outputPath ;
}
}
canvas . DrawColor ( new SKColor ( 0 , 0 , 0 , ( byte ) ( ( 1 - opacity ) * 0xFF ) ) , SKBlendMode . SrcOver ) ;
}
// create bitmap to use for canvas drawing used to draw into bitmap
using ( var saveBitmap = new SKBitmap ( width , height ) ) // , bitmap.ColorType, bitmap.AlphaType))
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 ) ;
}
Directory . CreateDirectory ( Path . GetDirectoryName ( outputPath ) ) ;
using ( var outputStream = new SKFileWStream ( outputPath ) )
{
using ( var pixmap = new SKPixmap ( new SKImageInfo ( width , height ) , saveBitmap . GetPixels ( ) ) )
{
pixmap . Encode ( outputStream , skiaOutputFormat , quality ) ;
}
}
}
if ( hasIndicator )
{
DrawIndicator ( canvas , width , height , options ) ;
}
Directory . CreateDirectory ( Path . GetDirectoryName ( outputPath ) ) ;
using ( var outputStream = new SKFileWStream ( outputPath ) )
{
using ( var pixmap = new SKPixmap ( new SKImageInfo ( width , height ) , saveBitmap . GetPixels ( ) ) )
{
pixmap . Encode ( outputStream , skiaOutputFormat , quality ) ;
}
}