Merge pull request #2155 from mark-monteiro/2149-jellyfin-drawing-skia-warnings

Jellyfin.Drawing.Skia Warnings and Analyzers
pull/2170/head
Bond-009 5 years ago committed by GitHub
commit 91562a4392
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23

@ -4,6 +4,7 @@
<TargetFramework>netstandard2.1</TargetFramework> <TargetFramework>netstandard2.1</TargetFramework>
<GenerateAssemblyInfo>false</GenerateAssemblyInfo> <GenerateAssemblyInfo>false</GenerateAssemblyInfo>
<GenerateDocumentationFile>true</GenerateDocumentationFile> <GenerateDocumentationFile>true</GenerateDocumentationFile>
<TreatWarningsAsErrors>true</TreatWarningsAsErrors>
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>
@ -22,4 +23,16 @@
<ProjectReference Include="..\MediaBrowser.Common\MediaBrowser.Common.csproj" /> <ProjectReference Include="..\MediaBrowser.Common\MediaBrowser.Common.csproj" />
</ItemGroup> </ItemGroup>
<!-- Code analysers-->
<ItemGroup Condition=" '$(Configuration)' == 'Debug' ">
<PackageReference Include="Microsoft.CodeAnalysis.FxCopAnalyzers" Version="2.9.7" PrivateAssets="All" />
<PackageReference Include="SerilogAnalyzer" Version="0.15.0" PrivateAssets="All" />
<PackageReference Include="StyleCop.Analyzers" Version="1.1.118" PrivateAssets="All" />
<PackageReference Include="SmartAnalyzers.MultithreadingAnalyzer" Version="1.1.31" PrivateAssets="All" />
</ItemGroup>
<PropertyGroup Condition=" '$(Configuration)' == 'Debug' ">
<CodeAnalysisRuleSet>../jellyfin.ruleset</CodeAnalysisRuleSet>
</PropertyGroup>
</Project> </Project>

@ -4,10 +4,19 @@ using SkiaSharp;
namespace Jellyfin.Drawing.Skia namespace Jellyfin.Drawing.Skia
{ {
/// <summary>
/// Static helper class used to draw percentage-played indicators on images.
/// </summary>
public static class PercentPlayedDrawer public static class PercentPlayedDrawer
{ {
private const int IndicatorHeight = 8; private const int IndicatorHeight = 8;
/// <summary>
/// Draw a percentage played indicator on a canvas.
/// </summary>
/// <param name="canvas">The canvas to draw the indicator on.</param>
/// <param name="imageSize">The size of the image being drawn on.</param>
/// <param name="percent">The percentage played to display with the indicator.</param>
public static void Process(SKCanvas canvas, ImageDimensions imageSize, double percent) public static void Process(SKCanvas canvas, ImageDimensions imageSize, double percent)
{ {
using (var paint = new SKPaint()) using (var paint = new SKPaint())

@ -3,10 +3,21 @@ using SkiaSharp;
namespace Jellyfin.Drawing.Skia namespace Jellyfin.Drawing.Skia
{ {
/// <summary>
/// Static helper class for drawing 'played' indicators.
/// </summary>
public static class PlayedIndicatorDrawer public static class PlayedIndicatorDrawer
{ {
private const int OffsetFromTopRightCorner = 38; private const int OffsetFromTopRightCorner = 38;
/// <summary>
/// Draw a 'played' indicator in the top right corner of a canvas.
/// </summary>
/// <param name="canvas">The canvas to draw the indicator on.</param>
/// <param name="imageSize">
/// The dimensions of the image to draw the indicator on. The width is used to determine the x-position of the
/// indicator.
/// </param>
public static void DrawPlayedIndicator(SKCanvas canvas, ImageDimensions imageSize) public static void DrawPlayedIndicator(SKCanvas canvas, ImageDimensions imageSize)
{ {
var x = imageSize.Width - OffsetFromTopRightCorner; var x = imageSize.Width - OffsetFromTopRightCorner;
@ -26,10 +37,10 @@ namespace Jellyfin.Drawing.Skia
paint.TextSize = 30; paint.TextSize = 30;
paint.IsAntialias = true; paint.IsAntialias = true;
// or:
// var emojiChar = 0x1F680;
var text = "✔️"; var text = "✔️";
var emojiChar = StringUtilities.GetUnicodeCharacterCode(text, SKTextEncoding.Utf32); var emojiChar = StringUtilities.GetUnicodeCharacterCode(text, SKTextEncoding.Utf32);
// or:
//var emojiChar = 0x1F680;
// ask the font manager for a font with that character // ask the font manager for a font with that character
var fontManager = SKFontManager.Default; var fontManager = SKFontManager.Default;

@ -1,3 +1,4 @@
using System.Diagnostics.CodeAnalysis;
using System.Globalization; using System.Globalization;
using SkiaSharp; using SkiaSharp;
@ -8,16 +9,10 @@ namespace Jellyfin.Drawing.Skia
/// </summary> /// </summary>
public class SkiaCodecException : SkiaException public class SkiaCodecException : SkiaException
{ {
/// <summary>
/// Returns the non-successfull codec result returned by Skia.
/// </summary>
/// <value>The non-successfull codec result returned by Skia.</value>
public SKCodecResult CodecResult { get; }
/// <summary> /// <summary>
/// Initializes a new instance of the <see cref="SkiaCodecException" /> class. /// Initializes a new instance of the <see cref="SkiaCodecException" /> class.
/// </summary> /// </summary>
/// <param name="result">The non-successfull codec result returned by Skia.</param> /// <param name="result">The non-successful codec result returned by Skia.</param>
public SkiaCodecException(SKCodecResult result) : base() public SkiaCodecException(SKCodecResult result) : base()
{ {
CodecResult = result; CodecResult = result;
@ -27,7 +22,7 @@ namespace Jellyfin.Drawing.Skia
/// Initializes a new instance of the <see cref="SkiaCodecException" /> class /// Initializes a new instance of the <see cref="SkiaCodecException" /> class
/// with a specified error message. /// with a specified error message.
/// </summary> /// </summary>
/// <param name="result">The non-successfull codec result returned by Skia.</param> /// <param name="result">The non-successful codec result returned by Skia.</param>
/// <param name="message">The message that describes the error.</param> /// <param name="message">The message that describes the error.</param>
public SkiaCodecException(SKCodecResult result, string message) public SkiaCodecException(SKCodecResult result, string message)
: base(message) : base(message)
@ -35,6 +30,11 @@ namespace Jellyfin.Drawing.Skia
CodecResult = result; CodecResult = result;
} }
/// <summary>
/// Gets the non-successful codec result returned by Skia.
/// </summary>
public SKCodecResult CodecResult { get; }
/// <inheritdoc /> /// <inheritdoc />
public override string ToString() public override string ToString()
=> string.Format( => string.Format(

@ -13,6 +13,9 @@ using static Jellyfin.Drawing.Skia.SkiaHelper;
namespace Jellyfin.Drawing.Skia namespace Jellyfin.Drawing.Skia
{ {
/// <summary>
/// Image encoder that uses <see cref="SkiaSharp"/> to manipulate images.
/// </summary>
public class SkiaEncoder : IImageEncoder public class SkiaEncoder : IImageEncoder
{ {
private readonly ILogger _logger; private readonly ILogger _logger;
@ -22,6 +25,12 @@ namespace Jellyfin.Drawing.Skia
private static readonly HashSet<string> _transparentImageTypes private static readonly HashSet<string> _transparentImageTypes
= new HashSet<string>(StringComparer.OrdinalIgnoreCase) { ".png", ".gif", ".webp" }; = new HashSet<string>(StringComparer.OrdinalIgnoreCase) { ".png", ".gif", ".webp" };
/// <summary>
/// Initializes a new instance of the <see cref="SkiaEncoder"/> class.
/// </summary>
/// <param name="logger">The application logger.</param>
/// <param name="appPaths">The application paths.</param>
/// <param name="localizationManager">The application localization manager.</param>
public SkiaEncoder( public SkiaEncoder(
ILogger<SkiaEncoder> logger, ILogger<SkiaEncoder> logger,
IApplicationPaths appPaths, IApplicationPaths appPaths,
@ -32,12 +41,16 @@ namespace Jellyfin.Drawing.Skia
_localizationManager = localizationManager; _localizationManager = localizationManager;
} }
/// <inheritdoc/>
public string Name => "Skia"; public string Name => "Skia";
/// <inheritdoc/>
public bool SupportsImageCollageCreation => true; public bool SupportsImageCollageCreation => true;
/// <inheritdoc/>
public bool SupportsImageEncoding => true; public bool SupportsImageEncoding => true;
/// <inheritdoc/>
public IReadOnlyCollection<string> SupportedInputFormats => public IReadOnlyCollection<string> SupportedInputFormats =>
new HashSet<string>(StringComparer.OrdinalIgnoreCase) new HashSet<string>(StringComparer.OrdinalIgnoreCase)
{ {
@ -65,11 +78,12 @@ namespace Jellyfin.Drawing.Skia
"arw" "arw"
}; };
/// <inheritdoc/>
public IReadOnlyCollection<ImageFormat> SupportedOutputFormats public IReadOnlyCollection<ImageFormat> SupportedOutputFormats
=> new HashSet<ImageFormat>() { ImageFormat.Webp, ImageFormat.Jpg, ImageFormat.Png }; => new HashSet<ImageFormat>() { ImageFormat.Webp, ImageFormat.Jpg, ImageFormat.Png };
/// <summary> /// <summary>
/// Test to determine if the native lib is available /// Test to determine if the native lib is available.
/// </summary> /// </summary>
public static void TestSkia() public static void TestSkia()
{ {
@ -80,6 +94,11 @@ namespace Jellyfin.Drawing.Skia
private static bool IsTransparent(SKColor color) private static bool IsTransparent(SKColor color)
=> (color.Red == 255 && color.Green == 255 && color.Blue == 255) || color.Alpha == 0; => (color.Red == 255 && color.Green == 255 && color.Blue == 255) || color.Alpha == 0;
/// <summary>
/// Convert a <see cref="ImageFormat"/> to a <see cref="SKEncodedImageFormat"/>.
/// </summary>
/// <param name="selectedFormat">The format to convert.</param>
/// <returns>The converted format.</returns>
public static SKEncodedImageFormat GetImageFormat(ImageFormat selectedFormat) public static SKEncodedImageFormat GetImageFormat(ImageFormat selectedFormat)
{ {
switch (selectedFormat) switch (selectedFormat)
@ -186,6 +205,9 @@ namespace Jellyfin.Drawing.Skia
} }
/// <inheritdoc /> /// <inheritdoc />
/// <exception cref="ArgumentNullException">The path is null.</exception>
/// <exception cref="FileNotFoundException">The path is not valid.</exception>
/// <exception cref="SkiaCodecException">The file at the specified path could not be used to generate a codec.</exception>
public ImageDimensions GetImageSize(string path) public ImageDimensions GetImageSize(string path)
{ {
if (path == null) if (path == null)
@ -269,6 +291,14 @@ namespace Jellyfin.Drawing.Skia
} }
} }
/// <summary>
/// Decode an image.
/// </summary>
/// <param name="path">The filepath of the image to decode.</param>
/// <param name="forceCleanBitmap">Whether to force clean the bitmap.</param>
/// <param name="orientation">The orientation of the image.</param>
/// <param name="origin">The detected origin of the image.</param>
/// <returns>The resulting bitmap of the image.</returns>
internal SKBitmap Decode(string path, bool forceCleanBitmap, ImageOrientation? orientation, out SKEncodedOrigin origin) internal SKBitmap Decode(string path, bool forceCleanBitmap, ImageOrientation? orientation, out SKEncodedOrigin origin)
{ {
if (!File.Exists(path)) if (!File.Exists(path))
@ -358,16 +388,6 @@ namespace Jellyfin.Drawing.Skia
private SKBitmap OrientImage(SKBitmap bitmap, SKEncodedOrigin origin) private SKBitmap OrientImage(SKBitmap bitmap, SKEncodedOrigin origin)
{ {
//var transformations = {
// 2: { rotate: 0, flip: true},
// 3: { rotate: 180, flip: false},
// 4: { rotate: 180, flip: true},
// 5: { rotate: 90, flip: true},
// 6: { rotate: 90, flip: false},
// 7: { rotate: 270, flip: true},
// 8: { rotate: 270, flip: false},
//}
switch (origin) switch (origin)
{ {
case SKEncodedOrigin.TopRight: case SKEncodedOrigin.TopRight:
@ -497,6 +517,7 @@ namespace Jellyfin.Drawing.Skia
} }
} }
/// <inheritdoc/>
public string EncodeImage(string inputPath, DateTime dateModified, string outputPath, bool autoOrient, ImageOrientation? orientation, int quality, ImageProcessingOptions options, ImageFormat selectedOutputFormat) public string EncodeImage(string inputPath, DateTime dateModified, string outputPath, bool autoOrient, ImageOrientation? orientation, int quality, ImageProcessingOptions options, ImageFormat selectedOutputFormat)
{ {
if (string.IsNullOrWhiteSpace(inputPath)) if (string.IsNullOrWhiteSpace(inputPath))
@ -520,7 +541,7 @@ namespace Jellyfin.Drawing.Skia
{ {
if (bitmap == null) if (bitmap == null)
{ {
throw new ArgumentOutOfRangeException(string.Format("Skia unable to read image {0}", inputPath)); throw new ArgumentOutOfRangeException($"Skia unable to read image {inputPath}");
} }
var originalImageSize = new ImageDimensions(bitmap.Width, bitmap.Height); var originalImageSize = new ImageDimensions(bitmap.Width, bitmap.Height);
@ -556,7 +577,7 @@ namespace Jellyfin.Drawing.Skia
} }
// create bitmap to use for canvas drawing used to draw into bitmap // 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 saveBitmap = new SKBitmap(width, height)) // , bitmap.ColorType, bitmap.AlphaType))
using (var canvas = new SKCanvas(saveBitmap)) using (var canvas = new SKCanvas(saveBitmap))
{ {
// set background color if present // set background color if present
@ -609,9 +630,11 @@ namespace Jellyfin.Drawing.Skia
} }
} }
} }
return outputPath; return outputPath;
} }
/// <inheritdoc/>
public void CreateImageCollage(ImageCollageOptions options) public void CreateImageCollage(ImageCollageOptions options)
{ {
double ratio = (double)options.Width / options.Height; double ratio = (double)options.Width / options.Height;

@ -7,17 +7,30 @@ namespace Jellyfin.Drawing.Skia
/// </summary> /// </summary>
public class SkiaException : Exception public class SkiaException : Exception
{ {
/// <inheritdoc /> /// <summary>
/// Initializes a new instance of the <see cref="SkiaException"/> class.
/// </summary>
public SkiaException() : base() public SkiaException() : base()
{ {
} }
/// <inheritdoc /> /// <summary>
/// Initializes a new instance of the <see cref="SkiaException"/> class with a specified error message.
/// </summary>
/// <param name="message">The message that describes the error.</param>
public SkiaException(string message) : base(message) public SkiaException(string message) : base(message)
{ {
} }
/// <inheritdoc /> /// <summary>
/// Initializes a new instance of the <see cref="SkiaException"/> class with a specified error message and a
/// reference to the inner exception that is the cause of this exception.
/// </summary>
/// <param name="message">The error message that explains the reason for the exception.</param>
/// <param name="innerException">
/// The exception that is the cause of the current exception, or a null reference (Nothing in Visual Basic) if
/// no inner exception is specified.
/// </param>
public SkiaException(string message, Exception innerException) public SkiaException(string message, Exception innerException)
: base(message, innerException) : base(message, innerException)
{ {

@ -5,15 +5,27 @@ using SkiaSharp;
namespace Jellyfin.Drawing.Skia namespace Jellyfin.Drawing.Skia
{ {
/// <summary>
/// Used to build collages of multiple images arranged in vertical strips.
/// </summary>
public class StripCollageBuilder public class StripCollageBuilder
{ {
private readonly SkiaEncoder _skiaEncoder; private readonly SkiaEncoder _skiaEncoder;
/// <summary>
/// Initializes a new instance of the <see cref="StripCollageBuilder"/> class.
/// </summary>
/// <param name="skiaEncoder">The encoder to use for building collages.</param>
public StripCollageBuilder(SkiaEncoder skiaEncoder) public StripCollageBuilder(SkiaEncoder skiaEncoder)
{ {
_skiaEncoder = skiaEncoder; _skiaEncoder = skiaEncoder;
} }
/// <summary>
/// Check which format an image has been encoded with using its filename extension.
/// </summary>
/// <param name="outputPath">The path to the image to get the format for.</param>
/// <returns>The image format.</returns>
public static SKEncodedImageFormat GetEncodedFormat(string outputPath) public static SKEncodedImageFormat GetEncodedFormat(string outputPath)
{ {
if (outputPath == null) if (outputPath == null)
@ -48,6 +60,13 @@ namespace Jellyfin.Drawing.Skia
return SKEncodedImageFormat.Png; return SKEncodedImageFormat.Png;
} }
/// <summary>
/// Create a square collage.
/// </summary>
/// <param name="paths">The paths of the images to use in the collage.</param>
/// <param name="outputPath">The path at which to place the resulting collage image.</param>
/// <param name="width">The desired width of the collage.</param>
/// <param name="height">The desired height of the collage.</param>
public void BuildSquareCollage(string[] paths, string outputPath, int width, int height) public void BuildSquareCollage(string[] paths, string outputPath, int width, int height)
{ {
using (var bitmap = BuildSquareCollageBitmap(paths, width, height)) using (var bitmap = BuildSquareCollageBitmap(paths, width, height))
@ -58,6 +77,13 @@ namespace Jellyfin.Drawing.Skia
} }
} }
/// <summary>
/// Create a thumb collage.
/// </summary>
/// <param name="paths">The paths of the images to use in the collage.</param>
/// <param name="outputPath">The path at which to place the resulting image.</param>
/// <param name="width">The desired width of the collage.</param>
/// <param name="height">The desired height of the collage.</param>
public void BuildThumbCollage(string[] paths, string outputPath, int width, int height) public void BuildThumbCollage(string[] paths, string outputPath, int width, int height)
{ {
using (var bitmap = BuildThumbCollageBitmap(paths, width, height)) using (var bitmap = BuildThumbCollageBitmap(paths, width, height))
@ -98,6 +124,7 @@ namespace Jellyfin.Drawing.Skia
using (var resizeBitmap = new SKBitmap(iWidth, iHeight, currentBitmap.ColorType, currentBitmap.AlphaType)) using (var resizeBitmap = new SKBitmap(iWidth, iHeight, currentBitmap.ColorType, currentBitmap.AlphaType))
{ {
currentBitmap.ScalePixels(resizeBitmap, SKFilterQuality.High); currentBitmap.ScalePixels(resizeBitmap, SKFilterQuality.High);
// crop image // crop image
int ix = (int)Math.Abs((iWidth - iSlice) / 2); int ix = (int)Math.Abs((iWidth - iSlice) / 2);
using (var image = SKImage.FromBitmap(resizeBitmap)) using (var image = SKImage.FromBitmap(resizeBitmap))

@ -4,10 +4,25 @@ using SkiaSharp;
namespace Jellyfin.Drawing.Skia namespace Jellyfin.Drawing.Skia
{ {
/// <summary>
/// Static helper class for drawing unplayed count indicators.
/// </summary>
public static class UnplayedCountIndicator public static class UnplayedCountIndicator
{ {
/// <summary>
/// The x-offset used when drawing an unplayed count indicator.
/// </summary>
private const int OffsetFromTopRightCorner = 38; private const int OffsetFromTopRightCorner = 38;
/// <summary>
/// Draw an unplayed count indicator in the top right corner of a canvas.
/// </summary>
/// <param name="canvas">The canvas to draw the indicator on.</param>
/// <param name="imageSize">
/// The dimensions of the image to draw the indicator on. The width is used to determine the x-position of the
/// indicator.
/// </param>
/// <param name="count">The number to draw in the indicator.</param>
public static void DrawUnplayedCountIndicator(SKCanvas canvas, ImageDimensions imageSize, int count) public static void DrawUnplayedCountIndicator(SKCanvas canvas, ImageDimensions imageSize, int count)
{ {
var x = imageSize.Width - OffsetFromTopRightCorner; var x = imageSize.Width - OffsetFromTopRightCorner;
@ -19,6 +34,7 @@ namespace Jellyfin.Drawing.Skia
paint.Style = SKPaintStyle.Fill; paint.Style = SKPaintStyle.Fill;
canvas.DrawCircle((float)x, OffsetFromTopRightCorner, 20, paint); canvas.DrawCircle((float)x, OffsetFromTopRightCorner, 20, paint);
} }
using (var paint = new SKPaint()) using (var paint = new SKPaint())
{ {
paint.Color = new SKColor(255, 255, 255, 255); paint.Color = new SKColor(255, 255, 255, 255);
@ -33,6 +49,7 @@ namespace Jellyfin.Drawing.Skia
{ {
x -= 7; x -= 7;
} }
if (text.Length == 2) if (text.Length == 2)
{ {
x -= 13; x -= 13;

@ -11,6 +11,7 @@ namespace MediaBrowser.Controller.Drawing
/// </summary> /// </summary>
/// <value>The supported input formats.</value> /// <value>The supported input formats.</value>
IReadOnlyCollection<string> SupportedInputFormats { get; } IReadOnlyCollection<string> SupportedInputFormats { get; }
/// <summary> /// <summary>
/// Gets the supported output formats. /// Gets the supported output formats.
/// </summary> /// </summary>
@ -18,9 +19,9 @@ namespace MediaBrowser.Controller.Drawing
IReadOnlyCollection<ImageFormat> SupportedOutputFormats { get; } IReadOnlyCollection<ImageFormat> SupportedOutputFormats { get; }
/// <summary> /// <summary>
/// Gets the name. /// Gets the display name for the encoder.
/// </summary> /// </summary>
/// <value>The name.</value> /// <value>The display name.</value>
string Name { get; } string Name { get; }
/// <summary> /// <summary>
@ -35,17 +36,22 @@ namespace MediaBrowser.Controller.Drawing
/// <value><c>true</c> if [supports image encoding]; otherwise, <c>false</c>.</value> /// <value><c>true</c> if [supports image encoding]; otherwise, <c>false</c>.</value>
bool SupportsImageEncoding { get; } bool SupportsImageEncoding { get; }
/// <summary>
/// Get the dimensions of an image from the filesystem.
/// </summary>
/// <param name="path">The filepath of the image.</param>
/// <returns>The image dimensions.</returns>
ImageDimensions GetImageSize(string path); ImageDimensions GetImageSize(string path);
/// <summary> /// <summary>
/// Encodes the image. /// Encode an image.
/// </summary> /// </summary>
string EncodeImage(string inputPath, DateTime dateModified, string outputPath, bool autoOrient, ImageOrientation? orientation, int quality, ImageProcessingOptions options, ImageFormat outputFormat); string EncodeImage(string inputPath, DateTime dateModified, string outputPath, bool autoOrient, ImageOrientation? orientation, int quality, ImageProcessingOptions options, ImageFormat outputFormat);
/// <summary> /// <summary>
/// Creates the image collage. /// Create an image collage.
/// </summary> /// </summary>
/// <param name="options">The options.</param> /// <param name="options">The options to use when creating the collage.</param>
void CreateImageCollage(ImageCollageOptions options); void CreateImageCollage(ImageCollageOptions options);
} }
} }

@ -31,6 +31,8 @@
<Rules AnalyzerId="Microsoft.CodeAnalysis.FxCopAnalyzers" RuleNamespace="Microsoft.Design"> <Rules AnalyzerId="Microsoft.CodeAnalysis.FxCopAnalyzers" RuleNamespace="Microsoft.Design">
<!-- disable warning CA1031: Do not catch general exception types --> <!-- disable warning CA1031: Do not catch general exception types -->
<Rule Id="CA1031" Action="Info" /> <Rule Id="CA1031" Action="Info" />
<!-- disable warning CA1032: Implement standard exception constructors -->
<Rule Id="CA1032" Action="Info" />
<!-- disable warning CA1062: Validate arguments of public methods --> <!-- disable warning CA1062: Validate arguments of public methods -->
<Rule Id="CA1062" Action="Info" /> <Rule Id="CA1062" Action="Info" />
<!-- disable warning CA1720: Identifiers should not contain type names --> <!-- disable warning CA1720: Identifiers should not contain type names -->

Loading…
Cancel
Save