From 34c3783f42d643f2dcc2e93d8f0b5e07580c21c1 Mon Sep 17 00:00:00 2001 From: Luke Pulverenti Date: Tue, 9 May 2017 15:53:46 -0400 Subject: [PATCH 1/6] update project files --- Emby.Drawing.Skia/Emby.Drawing.Skia.csproj | 6 + Emby.Drawing.Skia/PercentPlayedDrawer.cs | 31 +++ Emby.Drawing.Skia/PlayedIndicatorDrawer.cs | 120 ++++++++ Emby.Drawing.Skia/SkiaEncoder.cs | 261 ++++++++++++++++++ Emby.Drawing.Skia/StripCollageBuilder.cs | 245 ++++++++++++++++ Emby.Drawing.Skia/UnplayedCountIndicator.cs | 68 +++++ .../MediaBrowser.ServerApplication.csproj | 2 + 7 files changed, 733 insertions(+) create mode 100644 Emby.Drawing.Skia/PercentPlayedDrawer.cs create mode 100644 Emby.Drawing.Skia/PlayedIndicatorDrawer.cs create mode 100644 Emby.Drawing.Skia/SkiaEncoder.cs create mode 100644 Emby.Drawing.Skia/StripCollageBuilder.cs create mode 100644 Emby.Drawing.Skia/UnplayedCountIndicator.cs diff --git a/Emby.Drawing.Skia/Emby.Drawing.Skia.csproj b/Emby.Drawing.Skia/Emby.Drawing.Skia.csproj index 8d1e221d0a..d7b33b9507 100644 --- a/Emby.Drawing.Skia/Emby.Drawing.Skia.csproj +++ b/Emby.Drawing.Skia/Emby.Drawing.Skia.csproj @@ -52,7 +52,12 @@ Properties\SharedVersion.cs + + + + + @@ -61,6 +66,7 @@ + diff --git a/Emby.Drawing.Skia/PercentPlayedDrawer.cs b/Emby.Drawing.Skia/PercentPlayedDrawer.cs new file mode 100644 index 0000000000..e291a462b9 --- /dev/null +++ b/Emby.Drawing.Skia/PercentPlayedDrawer.cs @@ -0,0 +1,31 @@ +using SkiaSharp; +using MediaBrowser.Model.Drawing; +using System; + +namespace Emby.Drawing.Skia +{ + public class PercentPlayedDrawer + { + private const int IndicatorHeight = 8; + + public void Process(SKCanvas canvas, ImageSize imageSize, double percent) + { + using (var paint = new SKPaint()) + { + var endX = imageSize.Width - 1; + var endY = imageSize.Height - 1; + + paint.Color = SKColor.Parse("#99000000"); + paint.Style = SKPaintStyle.Fill; + canvas.DrawRect(SKRect.Create(0, (float)endY - IndicatorHeight, (float)endX, (float)endY), paint); + + double foregroundWidth = endX; + foregroundWidth *= percent; + foregroundWidth /= 100; + + paint.Color = SKColor.Parse("#FF52B54B"); + canvas.DrawRect(SKRect.Create(0, (float)endY - IndicatorHeight, Convert.ToInt32(Math.Round(foregroundWidth)), (float)endY), paint); + } + } + } +} diff --git a/Emby.Drawing.Skia/PlayedIndicatorDrawer.cs b/Emby.Drawing.Skia/PlayedIndicatorDrawer.cs new file mode 100644 index 0000000000..9f3a74eb7f --- /dev/null +++ b/Emby.Drawing.Skia/PlayedIndicatorDrawer.cs @@ -0,0 +1,120 @@ +using SkiaSharp; +using MediaBrowser.Common.Configuration; +using MediaBrowser.Common.Net; +using MediaBrowser.Model.Drawing; +using System; +using System.IO; +using System.Threading.Tasks; +using MediaBrowser.Common.IO; +using MediaBrowser.Controller.IO; +using MediaBrowser.Model.IO; +using System.Reflection; + +namespace Emby.Drawing.Skia +{ + public class PlayedIndicatorDrawer + { + private const int FontSize = 42; + private const int OffsetFromTopRightCorner = 38; + + private readonly IApplicationPaths _appPaths; + private readonly IHttpClient _iHttpClient; + private readonly IFileSystem _fileSystem; + + public PlayedIndicatorDrawer(IApplicationPaths appPaths, IHttpClient iHttpClient, IFileSystem fileSystem) + { + _appPaths = appPaths; + _iHttpClient = iHttpClient; + _fileSystem = fileSystem; + } + + public async Task DrawPlayedIndicator(SKCanvas canvas, ImageSize imageSize) + { + var x = imageSize.Width - OffsetFromTopRightCorner; + + using (var paint = new SKPaint()) + { + paint.Color = SKColor.Parse("#CC52B54B"); + paint.Style = SKPaintStyle.Fill; + canvas.DrawCircle((float)x, OffsetFromTopRightCorner, 20, paint); + } + + using (var paint = new SKPaint()) + { + paint.Color = new SKColor(255, 255, 255, 255); + paint.Style = SKPaintStyle.Fill; + paint.Typeface = SKTypeface.FromFile(await DownloadFont("webdings.ttf", "https://github.com/MediaBrowser/Emby.Resources/raw/master/fonts/webdings.ttf", + _appPaths, _iHttpClient, _fileSystem).ConfigureAwait(false)); + paint.TextSize = FontSize; + paint.IsAntialias = true; + + canvas.DrawText("a", (float)x-20, OffsetFromTopRightCorner + 12, paint); + } + } + + internal static string ExtractFont(string name, IApplicationPaths paths, IFileSystem fileSystem) + { + var filePath = Path.Combine(paths.ProgramDataPath, "fonts", name); + + if (fileSystem.FileExists(filePath)) + { + return filePath; + } + + var namespacePath = typeof(PlayedIndicatorDrawer).Namespace + ".fonts." + name; + var tempPath = Path.Combine(paths.TempDirectory, Guid.NewGuid().ToString("N") + ".ttf"); + fileSystem.CreateDirectory(fileSystem.GetDirectoryName(tempPath)); + + using (var stream = typeof(PlayedIndicatorDrawer).GetTypeInfo().Assembly.GetManifestResourceStream(namespacePath)) + { + using (var fileStream = fileSystem.GetFileStream(tempPath, FileOpenMode.Create, FileAccessMode.Write, FileShareMode.Read)) + { + stream.CopyTo(fileStream); + } + } + + fileSystem.CreateDirectory(fileSystem.GetDirectoryName(filePath)); + + try + { + fileSystem.CopyFile(tempPath, filePath, false); + } + catch (IOException) + { + + } + + return tempPath; + } + + internal static async Task DownloadFont(string name, string url, IApplicationPaths paths, IHttpClient httpClient, IFileSystem fileSystem) + { + var filePath = Path.Combine(paths.ProgramDataPath, "fonts", name); + + if (fileSystem.FileExists(filePath)) + { + return filePath; + } + + var tempPath = await httpClient.GetTempFile(new HttpRequestOptions + { + Url = url, + Progress = new Progress() + + }).ConfigureAwait(false); + + fileSystem.CreateDirectory(fileSystem.GetDirectoryName(filePath)); + + try + { + fileSystem.CopyFile(tempPath, filePath, false); + } + catch (IOException) + { + + } + + return tempPath; + } + } +} diff --git a/Emby.Drawing.Skia/SkiaEncoder.cs b/Emby.Drawing.Skia/SkiaEncoder.cs new file mode 100644 index 0000000000..2c2ae03b66 --- /dev/null +++ b/Emby.Drawing.Skia/SkiaEncoder.cs @@ -0,0 +1,261 @@ +using MediaBrowser.Common.Configuration; +using MediaBrowser.Common.Net; +using MediaBrowser.Controller.Drawing; +using MediaBrowser.Model.Drawing; +using MediaBrowser.Model.IO; +using MediaBrowser.Model.Logging; +using SkiaSharp; +using System; +using System.Reflection; +using System.Threading.Tasks; + +namespace Emby.Drawing.Skia +{ + public class SkiaEncoder : IImageEncoder + { + private readonly ILogger _logger; + private readonly IApplicationPaths _appPaths; + private readonly Func _httpClientFactory; + private readonly IFileSystem _fileSystem; + + public SkiaEncoder(ILogger logger, IApplicationPaths appPaths, Func httpClientFactory, IFileSystem fileSystem) + { + _logger = logger; + _appPaths = appPaths; + _httpClientFactory = httpClientFactory; + _fileSystem = fileSystem; + + LogVersion(); + } + + public string[] SupportedInputFormats + { + get + { + // Some common file name extensions for RAW picture files include: .cr2, .crw, .dng, .nef, .orf, .rw2, .pef, .arw, .sr2, .srf, and .tif. + return new[] + { + "jpeg", + "jpg", + "png", + "dng", + "webp", + "gif", + "bmp", + "ico", + "astc", + "ktx", + "pkm", + "wbmp" + }; + } + } + + public ImageFormat[] SupportedOutputFormats + { + get + { + return new[] { ImageFormat.Webp, ImageFormat.Gif, ImageFormat.Jpg, ImageFormat.Png, ImageFormat.Bmp }; + } + } + + private void LogVersion() + { + _logger.Info("SkiaSharp version: " + GetVersion()); + } + + public static string GetVersion() + { + return typeof(SKCanvas).GetTypeInfo().Assembly.GetName().Version.ToString(); + } + + 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; + case ImageFormat.Png: + default: + return SKEncodedImageFormat.Png; + } + } + + public void CropWhiteSpace(string inputPath, string outputPath) + { + CheckDisposed(); + + using (var bitmap = SKBitmap.Decode(inputPath)) + { + // @todo + } + } + + public ImageSize GetImageSize(string path) + { + CheckDisposed(); + + using (var s = new SKFileStream(path)) + { + using (var codec = SKCodec.Create(s)) + { + var info = codec.Info; + + return new ImageSize + { + Width = info.Width, + Height = info.Height + }; + } + } + } + + public void EncodeImage(string inputPath, string outputPath, bool autoOrient, int width, int height, int quality, ImageProcessingOptions options, ImageFormat selectedOutputFormat) + { + using (var bitmap = SKBitmap.Decode(inputPath)) + { + using (var resizedBitmap = new SKBitmap(width, height, bitmap.ColorType, bitmap.AlphaType)) + { + // scale image + bitmap.Resize(resizedBitmap, SKBitmapResizeMethod.Lanczos3); + + // create bitmap to use for canvas drawing + using (var saveBitmap = new SKBitmap(width, height, bitmap.ColorType, bitmap.AlphaType)) + { + // create canvas used to draw into bitmap + using (var canvas = new SKCanvas(saveBitmap)) + { + // set background color if present + if (!string.IsNullOrWhiteSpace(options.BackgroundColor)) + { + canvas.Clear(SKColor.Parse(options.BackgroundColor)); + } + + // Add blur if option is present + if (options.Blur > 0) + { + using (var paint = new SKPaint()) + { + // create image from resized bitmap to apply blur + using (var filter = SKImageFilter.CreateBlur(5, 5)) + { + 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 (!string.IsNullOrWhiteSpace(options.ForegroundLayer)) + { + Double opacity; + if (!Double.TryParse(options.ForegroundLayer, out opacity)) opacity = .4; + + var foregroundColor = String.Format("#{0:X2}000000", (Byte)((1-opacity) * 0xFF)); + canvas.DrawColor(SKColor.Parse(foregroundColor), SKBlendMode.SrcOver); + } + + DrawIndicator(canvas, width, height, options); + + using (var outputStream = new SKFileWStream(outputPath)) + { + saveBitmap.Encode(outputStream, GetImageFormat(selectedOutputFormat), quality); + } + } + } + } + } + } + + public void CreateImageCollage(ImageCollageOptions options) + { + double ratio = options.Width; + ratio /= options.Height; + + if (ratio >= 1.4) + { + new StripCollageBuilder(_appPaths, _fileSystem).BuildThumbCollage(options.InputPaths, options.OutputPath, options.Width, options.Height); + } + else if (ratio >= .9) + { + new StripCollageBuilder(_appPaths, _fileSystem).BuildSquareCollage(options.InputPaths, options.OutputPath, options.Width, options.Height); + } + else + { + new StripCollageBuilder(_appPaths, _fileSystem).BuildPosterCollage(options.InputPaths, options.OutputPath, options.Width, options.Height); + } + } + + private void DrawIndicator(SKCanvas canvas, int imageWidth, int imageHeight, ImageProcessingOptions options) + { + if (!options.AddPlayedIndicator && !options.UnplayedCount.HasValue && options.PercentPlayed.Equals(0)) + { + return; + } + + try + { + var currentImageSize = new ImageSize(imageWidth, imageHeight); + + if (options.AddPlayedIndicator) + { + var task = new PlayedIndicatorDrawer(_appPaths, _httpClientFactory(), _fileSystem).DrawPlayedIndicator(canvas, currentImageSize); + Task.WaitAll(task); + } + else if (options.UnplayedCount.HasValue) + { + new UnplayedCountIndicator(_appPaths, _httpClientFactory(), _fileSystem).DrawUnplayedCountIndicator(canvas, currentImageSize, options.UnplayedCount.Value); + } + + if (options.PercentPlayed > 0) + { + new PercentPlayedDrawer().Process(canvas, currentImageSize, options.PercentPlayed); + } + } + catch (Exception ex) + { + _logger.ErrorException("Error drawing indicator overlay", ex); + } + } + + public string Name + { + get { return "Skia"; } + } + + private bool _disposed; + public void Dispose() + { + _disposed = true; + } + + private void CheckDisposed() + { + if (_disposed) + { + throw new ObjectDisposedException(GetType().Name); + } + } + + public bool SupportsImageCollageCreation + { + get { return true; } + } + + public bool SupportsImageEncoding + { + get { return true; } + } + } +} diff --git a/Emby.Drawing.Skia/StripCollageBuilder.cs b/Emby.Drawing.Skia/StripCollageBuilder.cs new file mode 100644 index 0000000000..fe9b885af6 --- /dev/null +++ b/Emby.Drawing.Skia/StripCollageBuilder.cs @@ -0,0 +1,245 @@ +using SkiaSharp; +using MediaBrowser.Common.Configuration; +using System; +using System.IO; +using System.Collections.Generic; +using MediaBrowser.Common.IO; +using MediaBrowser.Controller.IO; +using MediaBrowser.Model.IO; + +namespace Emby.Drawing.Skia +{ + public class StripCollageBuilder + { + private readonly IApplicationPaths _appPaths; + private readonly IFileSystem _fileSystem; + + public StripCollageBuilder(IApplicationPaths appPaths, IFileSystem fileSystem) + { + _appPaths = appPaths; + _fileSystem = fileSystem; + } + + private SKEncodedImageFormat GetEncodedFormat(string outputPath) + { + var ext = Path.GetExtension(outputPath).ToLower(); + + if (ext == ".jpg" || ext == ".jpeg") + return SKEncodedImageFormat.Jpeg; + + if (ext == ".webp") + return SKEncodedImageFormat.Webp; + + if (ext == ".gif") + return SKEncodedImageFormat.Gif; + + if (ext == ".bmp") + return SKEncodedImageFormat.Bmp; + + // default to png + return SKEncodedImageFormat.Png; + } + + public void BuildPosterCollage(string[] paths, string outputPath, int width, int height) + { + using (var bitmap = BuildPosterCollageBitmap(paths, width, height)) + { + using (var outputStream = new SKFileWStream(outputPath)) + { + bitmap.Encode(outputStream, GetEncodedFormat(outputPath), 90); + } + } + } + + public void BuildSquareCollage(string[] paths, string outputPath, int width, int height) + { + using (var bitmap = BuildSquareCollageBitmap(paths, width, height)) + { + using (var outputStream = new SKFileWStream(outputPath)) + { + bitmap.Encode(outputStream, GetEncodedFormat(outputPath), 90); + } + } + } + + public void BuildThumbCollage(string[] paths, string outputPath, int width, int height) + { + using (var bitmap = BuildThumbCollageBitmap(paths, width, height)) + { + using (var outputStream = new SKFileWStream(outputPath)) + { + bitmap.Encode(outputStream, GetEncodedFormat(outputPath), 90); + } + } + } + + private SKBitmap BuildPosterCollageBitmap(string[] paths, int width, int height) + { + return null; + /* var inputPaths = ImageHelpers.ProjectPaths(paths, 3); + using (var wandImages = new MagickWand(inputPaths.ToArray())) + { + var wand = new MagickWand(width, height); + wand.OpenImage("gradient:#111111-#111111"); + using (var draw = new DrawingWand()) + { + var iSlice = Convert.ToInt32(width * 0.3); + int iTrans = Convert.ToInt32(height * .25); + int iHeight = Convert.ToInt32(height * .65); + var horizontalImagePadding = Convert.ToInt32(width * 0.0366); + + foreach (var element in wandImages.ImageList) + { + using (var blackPixelWand = new PixelWand(ColorName.Black)) + { + int iWidth = (int)Math.Abs(iHeight * element.Width / element.Height); + element.Gravity = GravityType.CenterGravity; + element.BackgroundColor = blackPixelWand; + element.ResizeImage(iWidth, iHeight, FilterTypes.LanczosFilter); + int ix = (int)Math.Abs((iWidth - iSlice) / 2); + element.CropImage(iSlice, iHeight, ix, 0); + + element.ExtentImage(iSlice, iHeight, 0 - horizontalImagePadding, 0); + } + } + + wandImages.SetFirstIterator(); + using (var wandList = wandImages.AppendImages()) + { + wandList.CurrentImage.TrimImage(1); + using (var mwr = wandList.CloneMagickWand()) + { + using (var blackPixelWand = new PixelWand(ColorName.Black)) + { + using (var greyPixelWand = new PixelWand(ColorName.Grey70)) + { + mwr.CurrentImage.ResizeImage(wandList.CurrentImage.Width, (wandList.CurrentImage.Height / 2), FilterTypes.LanczosFilter, 1); + mwr.CurrentImage.FlipImage(); + + mwr.CurrentImage.AlphaChannel = AlphaChannelType.DeactivateAlphaChannel; + mwr.CurrentImage.ColorizeImage(blackPixelWand, greyPixelWand); + + using (var mwg = new MagickWand(wandList.CurrentImage.Width, iTrans)) + { + mwg.OpenImage("gradient:black-none"); + var verticalSpacing = Convert.ToInt32(height * 0.01111111111111111111111111111111); + mwr.CurrentImage.CompositeImage(mwg, CompositeOperator.CopyOpacityCompositeOp, 0, verticalSpacing); + + wandList.AddImage(mwr); + int ex = (int)(wand.CurrentImage.Width - mwg.CurrentImage.Width) / 2; + wand.CurrentImage.CompositeImage(wandList.AppendImages(true), CompositeOperator.AtopCompositeOp, ex, Convert.ToInt32(height * .05)); + } + } + } + } + } + } + + return wand; + }*/ + } + + private SKBitmap BuildThumbCollageBitmap(string[] paths, int width, int height) + { + return null; + /*var inputPaths = ImageHelpers.ProjectPaths(paths, 4); + using (var wandImages = new MagickWand(inputPaths.ToArray())) + { + var wand = new MagickWand(width, height); + wand.OpenImage("gradient:#111111-#111111"); + using (var draw = new DrawingWand()) + { + var iSlice = Convert.ToInt32(width * 0.24125); + int iTrans = Convert.ToInt32(height * .25); + int iHeight = Convert.ToInt32(height * .70); + var horizontalImagePadding = Convert.ToInt32(width * 0.0125); + + foreach (var element in wandImages.ImageList) + { + using (var blackPixelWand = new PixelWand(ColorName.Black)) + { + int iWidth = (int)Math.Abs(iHeight * element.Width / element.Height); + element.Gravity = GravityType.CenterGravity; + element.BackgroundColor = blackPixelWand; + element.ResizeImage(iWidth, iHeight, FilterTypes.LanczosFilter); + int ix = (int)Math.Abs((iWidth - iSlice) / 2); + element.CropImage(iSlice, iHeight, ix, 0); + + element.ExtentImage(iSlice, iHeight, 0 - horizontalImagePadding, 0); + } + } + + wandImages.SetFirstIterator(); + using (var wandList = wandImages.AppendImages()) + { + wandList.CurrentImage.TrimImage(1); + using (var mwr = wandList.CloneMagickWand()) + { + using (var blackPixelWand = new PixelWand(ColorName.Black)) + { + using (var greyPixelWand = new PixelWand(ColorName.Grey70)) + { + mwr.CurrentImage.ResizeImage(wandList.CurrentImage.Width, (wandList.CurrentImage.Height / 2), FilterTypes.LanczosFilter, 1); + mwr.CurrentImage.FlipImage(); + + mwr.CurrentImage.AlphaChannel = AlphaChannelType.DeactivateAlphaChannel; + mwr.CurrentImage.ColorizeImage(blackPixelWand, greyPixelWand); + + using (var mwg = new MagickWand(wandList.CurrentImage.Width, iTrans)) + { + mwg.OpenImage("gradient:black-none"); + var verticalSpacing = Convert.ToInt32(height * 0.01111111111111111111111111111111); + mwr.CurrentImage.CompositeImage(mwg, CompositeOperator.CopyOpacityCompositeOp, 0, verticalSpacing); + + wandList.AddImage(mwr); + int ex = (int)(wand.CurrentImage.Width - mwg.CurrentImage.Width) / 2; + wand.CurrentImage.CompositeImage(wandList.AppendImages(true), CompositeOperator.AtopCompositeOp, ex, Convert.ToInt32(height * .045)); + } + } + } + } + } + } + + return wand; + }*/ + } + + private SKBitmap BuildSquareCollageBitmap(string[] paths, int width, int height) + { + var bitmap = new SKBitmap(width, height); + var imageIndex = 0; + var cellWidth = width / 2; + var cellHeight = height / 2; + + using (var canvas = new SKCanvas(bitmap)) + { + for (var x = 0; x < 2; x++) + { + for (var y = 0; y < 2; y++) + { + using (var currentBitmap = SKBitmap.Decode(paths[imageIndex])) + { + using (var resizedBitmap = new SKBitmap(cellWidth, cellHeight, currentBitmap.ColorType, currentBitmap.AlphaType)) + { + // scale image + currentBitmap.Resize(resizedBitmap, SKBitmapResizeMethod.Lanczos3); + + // draw this image into the strip at the next position + var xPos = x * cellWidth; + var yPos = y * cellHeight; + canvas.DrawBitmap(resizedBitmap, xPos, yPos); + } + } + imageIndex++; + + if (imageIndex >= paths.Length) + imageIndex = 0; + } + } + } + + return bitmap; + } + } +} diff --git a/Emby.Drawing.Skia/UnplayedCountIndicator.cs b/Emby.Drawing.Skia/UnplayedCountIndicator.cs new file mode 100644 index 0000000000..f0283ad23e --- /dev/null +++ b/Emby.Drawing.Skia/UnplayedCountIndicator.cs @@ -0,0 +1,68 @@ +using SkiaSharp; +using MediaBrowser.Common.Configuration; +using MediaBrowser.Common.Net; +using MediaBrowser.Model.Drawing; +using System.Globalization; +using System.Threading.Tasks; +using MediaBrowser.Common.IO; +using MediaBrowser.Controller.IO; +using MediaBrowser.Model.IO; + +namespace Emby.Drawing.Skia +{ + public class UnplayedCountIndicator + { + private const int OffsetFromTopRightCorner = 38; + + private readonly IApplicationPaths _appPaths; + private readonly IHttpClient _iHttpClient; + private readonly IFileSystem _fileSystem; + + public UnplayedCountIndicator(IApplicationPaths appPaths, IHttpClient iHttpClient, IFileSystem fileSystem) + { + _appPaths = appPaths; + _iHttpClient = iHttpClient; + _fileSystem = fileSystem; + } + + public void DrawUnplayedCountIndicator(SKCanvas canvas, ImageSize imageSize, int count) + { + var x = imageSize.Width - OffsetFromTopRightCorner; + var text = count.ToString(CultureInfo.InvariantCulture); + + using (var paint = new SKPaint()) + { + paint.Color = SKColor.Parse("#CC52B54B"); + paint.Style = SKPaintStyle.Fill; + canvas.DrawCircle((float)x, OffsetFromTopRightCorner, 20, paint); + } + using (var paint = new SKPaint()) + { + paint.Color = new SKColor(255, 255, 255, 255); + paint.Style = SKPaintStyle.Fill; + paint.Typeface = SKTypeface.FromFile(PlayedIndicatorDrawer.ExtractFont("robotoregular.ttf", _appPaths, _fileSystem)); + paint.TextSize = 24; + paint.IsAntialias = true; + + var y = OffsetFromTopRightCorner + 9; + + if (text.Length == 1) + { + x -= 7; + } + if (text.Length == 2) + { + x -= 13; + } + else if (text.Length >= 3) + { + x -= 15; + y -= 2; + paint.TextSize = 18; + } + + canvas.DrawText(text, (float)x, y, paint); + } + } + } +} diff --git a/MediaBrowser.ServerApplication/MediaBrowser.ServerApplication.csproj b/MediaBrowser.ServerApplication/MediaBrowser.ServerApplication.csproj index a054382908..7c3757f5fa 100644 --- a/MediaBrowser.ServerApplication/MediaBrowser.ServerApplication.csproj +++ b/MediaBrowser.ServerApplication/MediaBrowser.ServerApplication.csproj @@ -191,9 +191,11 @@ x64\libSkiaSharp.dll + PreserveNewest x86\libSkiaSharp.dll + PreserveNewest MediaBrowser.InstallUtil.dll From 4569b6b49196eeb0c13003bfce087e2258dcc33e Mon Sep 17 00:00:00 2001 From: Luke Pulverenti Date: Tue, 9 May 2017 16:18:02 -0400 Subject: [PATCH 2/6] update images --- Emby.Drawing.Skia/SkiaEncoder.cs | 11 +++++++++++ MediaBrowser.ServerApplication/ImageEncoderHelper.cs | 10 ++++++++++ .../MediaBrowser.ServerApplication.csproj | 4 ++++ 3 files changed, 25 insertions(+) diff --git a/Emby.Drawing.Skia/SkiaEncoder.cs b/Emby.Drawing.Skia/SkiaEncoder.cs index 2c2ae03b66..3c631aef07 100644 --- a/Emby.Drawing.Skia/SkiaEncoder.cs +++ b/Emby.Drawing.Skia/SkiaEncoder.cs @@ -95,6 +95,8 @@ namespace Emby.Drawing.Skia { // @todo } + + _fileSystem.CopyFile(inputPath, outputPath, true); } public ImageSize GetImageSize(string path) @@ -118,6 +120,15 @@ namespace Emby.Drawing.Skia public void EncodeImage(string inputPath, string outputPath, bool autoOrient, int width, int height, int quality, ImageProcessingOptions options, ImageFormat selectedOutputFormat) { + if (string.IsNullOrWhiteSpace(inputPath)) + { + throw new ArgumentNullException("inputPath"); + } + if (string.IsNullOrWhiteSpace(inputPath)) + { + throw new ArgumentNullException("outputPath"); + } + using (var bitmap = SKBitmap.Decode(inputPath)) { using (var resizedBitmap = new SKBitmap(width, height, bitmap.ColorType, bitmap.AlphaType)) diff --git a/MediaBrowser.ServerApplication/ImageEncoderHelper.cs b/MediaBrowser.ServerApplication/ImageEncoderHelper.cs index ddbde2f666..8c3d8d2130 100644 --- a/MediaBrowser.ServerApplication/ImageEncoderHelper.cs +++ b/MediaBrowser.ServerApplication/ImageEncoderHelper.cs @@ -2,6 +2,7 @@ using Emby.Drawing; using Emby.Drawing.Net; using Emby.Drawing.ImageMagick; +using Emby.Drawing.Skia; using Emby.Server.Core; using Emby.Server.Implementations; using MediaBrowser.Common.Configuration; @@ -23,6 +24,15 @@ namespace MediaBrowser.Server.Startup.Common { if (!startupOptions.ContainsOption("-enablegdi")) { + try + { + return new SkiaEncoder(logManager.GetLogger("ImageMagick"), appPaths, httpClient, fileSystem); + } + catch + { + logger.Error("Error loading ImageMagick. Will revert to GDI."); + } + try { return new ImageMagickEncoder(logManager.GetLogger("ImageMagick"), appPaths, httpClient, fileSystem); diff --git a/MediaBrowser.ServerApplication/MediaBrowser.ServerApplication.csproj b/MediaBrowser.ServerApplication/MediaBrowser.ServerApplication.csproj index 7c3757f5fa..d632007d20 100644 --- a/MediaBrowser.ServerApplication/MediaBrowser.ServerApplication.csproj +++ b/MediaBrowser.ServerApplication/MediaBrowser.ServerApplication.csproj @@ -1112,6 +1112,10 @@ {c97a239e-a96c-4d64-a844-ccf8cc30aecb} Emby.Drawing.Net + + {2312da6d-ff86-4597-9777-bceec32d96dd} + Emby.Drawing.Skia + {08fff49b-f175-4807-a2b5-73b0ebd9f716} Emby.Drawing From f49d20033d81e9c2d1a335af603a5e56402014a9 Mon Sep 17 00:00:00 2001 From: Luke Pulverenti Date: Wed, 10 May 2017 00:49:11 -0400 Subject: [PATCH 3/6] move crop whitespace directly to encode image --- .../ImageMagickEncoder.cs | 16 +- Emby.Drawing.Net/GDIImageEncoder.cs | 17 +- Emby.Drawing.Skia/SkiaEncoder.cs | 150 +++++++++++++--- Emby.Drawing.Skia/StripCollageBuilder.cs | 169 +++++------------- Emby.Drawing/ImageProcessor.cs | 56 ------ .../Drawing/IImageEncoder.cs | 6 - .../Drawing/ImageProcessingOptions.cs | 1 + .../ImageEncoderHelper.cs | 2 +- 8 files changed, 187 insertions(+), 230 deletions(-) diff --git a/Emby.Drawing.ImageMagick/ImageMagickEncoder.cs b/Emby.Drawing.ImageMagick/ImageMagickEncoder.cs index 500f57aade..f603c49509 100644 --- a/Emby.Drawing.ImageMagick/ImageMagickEncoder.cs +++ b/Emby.Drawing.ImageMagick/ImageMagickEncoder.cs @@ -105,17 +105,6 @@ namespace Emby.Drawing.ImageMagick } } - public void CropWhiteSpace(string inputPath, string outputPath) - { - CheckDisposed(); - - using (var wand = new MagickWand(inputPath)) - { - wand.CurrentImage.TrimImage(10); - wand.SaveImage(outputPath); - } - } - public ImageSize GetImageSize(string path) { CheckDisposed(); @@ -150,6 +139,11 @@ namespace Emby.Drawing.ImageMagick { using (var originalImage = new MagickWand(inputPath)) { + if (options.CropWhiteSpace) + { + originalImage.CurrentImage.TrimImage(10); + } + ScaleImage(originalImage, width, height, options.Blur ?? 0); if (autoOrient) diff --git a/Emby.Drawing.Net/GDIImageEncoder.cs b/Emby.Drawing.Net/GDIImageEncoder.cs index 638415afdc..e710baaa77 100644 --- a/Emby.Drawing.Net/GDIImageEncoder.cs +++ b/Emby.Drawing.Net/GDIImageEncoder.cs @@ -75,27 +75,24 @@ namespace Emby.Drawing.Net } } - public void CropWhiteSpace(string inputPath, string outputPath) + private Image GetImage(string path, bool cropWhitespace) { - using (var image = (Bitmap)Image.FromFile(inputPath)) + if (cropWhitespace) { - using (var croppedImage = image.CropWhitespace()) + using (var originalImage = (Bitmap)Image.FromFile(path)) { - _fileSystem.CreateDirectory(_fileSystem.GetDirectoryName(outputPath)); - - using (var outputStream = _fileSystem.GetFileStream(outputPath, FileOpenMode.Create, FileAccessMode.Write, FileShareMode.Read, false)) - { - croppedImage.Save(System.Drawing.Imaging.ImageFormat.Png, outputStream, 100); - } + return originalImage.CropWhitespace(); } } + + return Image.FromFile(path); } public void EncodeImage(string inputPath, string cacheFilePath, bool autoOrient, int width, int height, int quality, ImageProcessingOptions options, ImageFormat selectedOutputFormat) { var hasPostProcessing = !string.IsNullOrEmpty(options.BackgroundColor) || options.UnplayedCount.HasValue || options.AddPlayedIndicator || options.PercentPlayed > 0; - using (var originalImage = Image.FromFile(inputPath)) + using (var originalImage = GetImage(inputPath, options.CropWhiteSpace)) { var newWidth = Convert.ToInt32(width); var newHeight = Convert.ToInt32(height); diff --git a/Emby.Drawing.Skia/SkiaEncoder.cs b/Emby.Drawing.Skia/SkiaEncoder.cs index 3c631aef07..d52ad47349 100644 --- a/Emby.Drawing.Skia/SkiaEncoder.cs +++ b/Emby.Drawing.Skia/SkiaEncoder.cs @@ -66,7 +66,15 @@ namespace Emby.Drawing.Skia public static string GetVersion() { - return typeof(SKCanvas).GetTypeInfo().Assembly.GetName().Version.ToString(); + using (var bitmap = new SKBitmap()) + { + return typeof(SKBitmap).GetTypeInfo().Assembly.GetName().Version.ToString(); + } + } + + private static bool IsWhiteSpace(SKColor color) + { + return (color.Red == 255 && color.Green == 255 && color.Blue == 255) || color.Alpha == 0; } public static SKEncodedImageFormat GetImageFormat(ImageFormat selectedFormat) @@ -81,22 +89,88 @@ namespace Emby.Drawing.Skia return SKEncodedImageFormat.Gif; case ImageFormat.Webp: return SKEncodedImageFormat.Webp; - case ImageFormat.Png: default: return SKEncodedImageFormat.Png; } } - public void CropWhiteSpace(string inputPath, string outputPath) + private static bool IsAllWhiteRow(SKBitmap bmp, int row) + { + for (var i = 0; i < bmp.Width; ++i) + { + if (!IsWhiteSpace(bmp.GetPixel(i, row))) + { + return false; + } + } + return true; + } + + private static bool IsAllWhiteColumn(SKBitmap bmp, int col) + { + for (var i = 0; i < bmp.Height; ++i) + { + if (!IsWhiteSpace(bmp.GetPixel(col, i))) + { + return false; + } + } + return true; + } + + private SKBitmap CropWhiteSpace(SKBitmap bitmap) { CheckDisposed(); - using (var bitmap = SKBitmap.Decode(inputPath)) + var topmost = 0; + for (int row = 0; row < bitmap.Height; ++row) { - // @todo + if (IsAllWhiteRow(bitmap, row)) + topmost = row; + else break; } - _fileSystem.CopyFile(inputPath, outputPath, true); + int bottommost = 0; + for (int row = bitmap.Height - 1; row >= 0; --row) + { + if (IsAllWhiteRow(bitmap, row)) + bottommost = row; + else break; + } + + int leftmost = 0, rightmost = 0; + for (int col = 0; col < bitmap.Width; ++col) + { + if (IsAllWhiteColumn(bitmap, col)) + leftmost = col; + else + break; + } + + for (int col = bitmap.Width - 1; col >= 0; --col) + { + if (IsAllWhiteColumn(bitmap, col)) + rightmost = col; + else + break; + } + + 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 data = subset.Encode(StripCollageBuilder.GetEncodedFormat(outputPath), 90)) + //{ + // using (var fileStream = _fileSystem.GetFileStream(outputPath, FileOpenMode.Create, FileAccessMode.Write, FileShareMode.Read)) + // { + // data.AsStream().CopyTo(fileStream); + // } + //} + } + } } public ImageSize GetImageSize(string path) @@ -118,6 +192,19 @@ namespace Emby.Drawing.Skia } } + private SKBitmap GetBitmap(string path, bool cropWhitespace) + { + if (cropWhitespace) + { + using (var bitmap = SKBitmap.Decode(path)) + { + return CropWhiteSpace(bitmap); + } + } + + return SKBitmap.Decode(path); + } + public void EncodeImage(string inputPath, string outputPath, bool autoOrient, int width, int height, int quality, ImageProcessingOptions options, ImageFormat selectedOutputFormat) { if (string.IsNullOrWhiteSpace(inputPath)) @@ -129,12 +216,34 @@ namespace Emby.Drawing.Skia throw new ArgumentNullException("outputPath"); } - using (var bitmap = SKBitmap.Decode(inputPath)) + var skiaOutputFormat = GetImageFormat(selectedOutputFormat); + + var hasBackgroundColor = !string.IsNullOrWhiteSpace(options.BackgroundColor); + var hasForegroundColor = !string.IsNullOrWhiteSpace(options.ForegroundLayer); + var blur = options.Blur ?? 0; + var hasIndicator = !options.AddPlayedIndicator && !options.UnplayedCount.HasValue && options.PercentPlayed.Equals(0); + + using (var bitmap = GetBitmap(inputPath, options.CropWhiteSpace)) { using (var resizedBitmap = new SKBitmap(width, height, bitmap.ColorType, bitmap.AlphaType)) { // scale image - bitmap.Resize(resizedBitmap, SKBitmapResizeMethod.Lanczos3); + var resizeMethod = options.Image.Type == MediaBrowser.Model.Entities.ImageType.Logo || + options.Image.Type == MediaBrowser.Model.Entities.ImageType.Art + ? SKBitmapResizeMethod.Lanczos3 + : SKBitmapResizeMethod.Lanczos3; + + bitmap.Resize(resizedBitmap, resizeMethod); + + // If all we're doing is resizing then we can stop now + if (!hasBackgroundColor && !hasForegroundColor && blur == 0 && !hasIndicator) + { + using (var outputStream = new SKFileWStream(outputPath)) + { + resizedBitmap.Encode(outputStream, skiaOutputFormat, quality); + return; + } + } // create bitmap to use for canvas drawing using (var saveBitmap = new SKBitmap(width, height, bitmap.ColorType, bitmap.AlphaType)) @@ -143,13 +252,13 @@ namespace Emby.Drawing.Skia using (var canvas = new SKCanvas(saveBitmap)) { // set background color if present - if (!string.IsNullOrWhiteSpace(options.BackgroundColor)) + if (hasBackgroundColor) { canvas.Clear(SKColor.Parse(options.BackgroundColor)); } // Add blur if option is present - if (options.Blur > 0) + if (blur > 0) { using (var paint = new SKPaint()) { @@ -168,20 +277,23 @@ namespace Emby.Drawing.Skia } // If foreground layer present then draw - if (!string.IsNullOrWhiteSpace(options.ForegroundLayer)) + if (hasForegroundColor) { Double opacity; if (!Double.TryParse(options.ForegroundLayer, out opacity)) opacity = .4; - var foregroundColor = String.Format("#{0:X2}000000", (Byte)((1-opacity) * 0xFF)); + var foregroundColor = String.Format("#{0:X2}000000", (Byte)((1 - opacity) * 0xFF)); canvas.DrawColor(SKColor.Parse(foregroundColor), SKBlendMode.SrcOver); } - DrawIndicator(canvas, width, height, options); + if (hasIndicator) + { + DrawIndicator(canvas, width, height, options); + } using (var outputStream = new SKFileWStream(outputPath)) { - saveBitmap.Encode(outputStream, GetImageFormat(selectedOutputFormat), quality); + saveBitmap.Encode(outputStream, skiaOutputFormat, quality); } } } @@ -204,17 +316,13 @@ namespace Emby.Drawing.Skia } else { - new StripCollageBuilder(_appPaths, _fileSystem).BuildPosterCollage(options.InputPaths, options.OutputPath, options.Width, options.Height); + // @todo create Poster collage capability + new StripCollageBuilder(_appPaths, _fileSystem).BuildSquareCollage(options.InputPaths, options.OutputPath, options.Width, options.Height); } } private void DrawIndicator(SKCanvas canvas, int imageWidth, int imageHeight, ImageProcessingOptions options) { - if (!options.AddPlayedIndicator && !options.UnplayedCount.HasValue && options.PercentPlayed.Equals(0)) - { - return; - } - try { var currentImageSize = new ImageSize(imageWidth, imageHeight); @@ -269,4 +377,4 @@ namespace Emby.Drawing.Skia get { return true; } } } -} +} \ No newline at end of file diff --git a/Emby.Drawing.Skia/StripCollageBuilder.cs b/Emby.Drawing.Skia/StripCollageBuilder.cs index fe9b885af6..7f37007697 100644 --- a/Emby.Drawing.Skia/StripCollageBuilder.cs +++ b/Emby.Drawing.Skia/StripCollageBuilder.cs @@ -2,9 +2,6 @@ using MediaBrowser.Common.Configuration; using System; using System.IO; -using System.Collections.Generic; -using MediaBrowser.Common.IO; -using MediaBrowser.Controller.IO; using MediaBrowser.Model.IO; namespace Emby.Drawing.Skia @@ -20,7 +17,7 @@ namespace Emby.Drawing.Skia _fileSystem = fileSystem; } - private SKEncodedImageFormat GetEncodedFormat(string outputPath) + public static SKEncodedImageFormat GetEncodedFormat(string outputPath) { var ext = Path.GetExtension(outputPath).ToLower(); @@ -42,13 +39,7 @@ namespace Emby.Drawing.Skia public void BuildPosterCollage(string[] paths, string outputPath, int width, int height) { - using (var bitmap = BuildPosterCollageBitmap(paths, width, height)) - { - using (var outputStream = new SKFileWStream(outputPath)) - { - bitmap.Encode(outputStream, GetEncodedFormat(outputPath), 90); - } - } + // @todo } public void BuildSquareCollage(string[] paths, string outputPath, int width, int height) @@ -73,136 +64,64 @@ namespace Emby.Drawing.Skia } } - private SKBitmap BuildPosterCollageBitmap(string[] paths, int width, int height) - { - return null; - /* var inputPaths = ImageHelpers.ProjectPaths(paths, 3); - using (var wandImages = new MagickWand(inputPaths.ToArray())) - { - var wand = new MagickWand(width, height); - wand.OpenImage("gradient:#111111-#111111"); - using (var draw = new DrawingWand()) - { - var iSlice = Convert.ToInt32(width * 0.3); - int iTrans = Convert.ToInt32(height * .25); - int iHeight = Convert.ToInt32(height * .65); - var horizontalImagePadding = Convert.ToInt32(width * 0.0366); - - foreach (var element in wandImages.ImageList) - { - using (var blackPixelWand = new PixelWand(ColorName.Black)) - { - int iWidth = (int)Math.Abs(iHeight * element.Width / element.Height); - element.Gravity = GravityType.CenterGravity; - element.BackgroundColor = blackPixelWand; - element.ResizeImage(iWidth, iHeight, FilterTypes.LanczosFilter); - int ix = (int)Math.Abs((iWidth - iSlice) / 2); - element.CropImage(iSlice, iHeight, ix, 0); - - element.ExtentImage(iSlice, iHeight, 0 - horizontalImagePadding, 0); - } - } - - wandImages.SetFirstIterator(); - using (var wandList = wandImages.AppendImages()) - { - wandList.CurrentImage.TrimImage(1); - using (var mwr = wandList.CloneMagickWand()) - { - using (var blackPixelWand = new PixelWand(ColorName.Black)) - { - using (var greyPixelWand = new PixelWand(ColorName.Grey70)) - { - mwr.CurrentImage.ResizeImage(wandList.CurrentImage.Width, (wandList.CurrentImage.Height / 2), FilterTypes.LanczosFilter, 1); - mwr.CurrentImage.FlipImage(); - - mwr.CurrentImage.AlphaChannel = AlphaChannelType.DeactivateAlphaChannel; - mwr.CurrentImage.ColorizeImage(blackPixelWand, greyPixelWand); - - using (var mwg = new MagickWand(wandList.CurrentImage.Width, iTrans)) - { - mwg.OpenImage("gradient:black-none"); - var verticalSpacing = Convert.ToInt32(height * 0.01111111111111111111111111111111); - mwr.CurrentImage.CompositeImage(mwg, CompositeOperator.CopyOpacityCompositeOp, 0, verticalSpacing); - - wandList.AddImage(mwr); - int ex = (int)(wand.CurrentImage.Width - mwg.CurrentImage.Width) / 2; - wand.CurrentImage.CompositeImage(wandList.AppendImages(true), CompositeOperator.AtopCompositeOp, ex, Convert.ToInt32(height * .05)); - } - } - } - } - } - } - - return wand; - }*/ - } - private SKBitmap BuildThumbCollageBitmap(string[] paths, int width, int height) { - return null; - /*var inputPaths = ImageHelpers.ProjectPaths(paths, 4); - using (var wandImages = new MagickWand(inputPaths.ToArray())) + var bitmap = new SKBitmap(width, height); + + using (var canvas = new SKCanvas(bitmap)) { - var wand = new MagickWand(width, height); - wand.OpenImage("gradient:#111111-#111111"); - using (var draw = new DrawingWand()) + canvas.Clear(SKColors.Black); + + var iSlice = Convert.ToInt32(width * 0.24125); + int iTrans = Convert.ToInt32(height * .25); + int iHeight = Convert.ToInt32(height * .70); + var horizontalImagePadding = Convert.ToInt32(width * 0.0125); + var verticalSpacing = Convert.ToInt32(height * 0.01111111111111111111111111111111); + int imageIndex = 0; + + for (int i = 0; i < 4; i++) { - var iSlice = Convert.ToInt32(width * 0.24125); - int iTrans = Convert.ToInt32(height * .25); - int iHeight = Convert.ToInt32(height * .70); - var horizontalImagePadding = Convert.ToInt32(width * 0.0125); - - foreach (var element in wandImages.ImageList) + using (var currentBitmap = SKBitmap.Decode(paths[imageIndex])) { - using (var blackPixelWand = new PixelWand(ColorName.Black)) + int iWidth = (int)Math.Abs(iHeight * currentBitmap.Width / currentBitmap.Height); + using (var resizeBitmap = new SKBitmap(iWidth, iHeight, currentBitmap.ColorType, currentBitmap.AlphaType)) { - int iWidth = (int)Math.Abs(iHeight * element.Width / element.Height); - element.Gravity = GravityType.CenterGravity; - element.BackgroundColor = blackPixelWand; - element.ResizeImage(iWidth, iHeight, FilterTypes.LanczosFilter); + currentBitmap.Resize(resizeBitmap, SKBitmapResizeMethod.Lanczos3); int ix = (int)Math.Abs((iWidth - iSlice) / 2); - element.CropImage(iSlice, iHeight, ix, 0); - - element.ExtentImage(iSlice, iHeight, 0 - horizontalImagePadding, 0); - } - } - - wandImages.SetFirstIterator(); - using (var wandList = wandImages.AppendImages()) - { - wandList.CurrentImage.TrimImage(1); - using (var mwr = wandList.CloneMagickWand()) - { - using (var blackPixelWand = new PixelWand(ColorName.Black)) + using (var image = SKImage.FromBitmap(resizeBitmap)) { - using (var greyPixelWand = new PixelWand(ColorName.Grey70)) + using (var subset = image.Subset(SKRectI.Create(ix, 0, iSlice, iHeight))) { - mwr.CurrentImage.ResizeImage(wandList.CurrentImage.Width, (wandList.CurrentImage.Height / 2), FilterTypes.LanczosFilter, 1); - mwr.CurrentImage.FlipImage(); + canvas.DrawImage(subset, (horizontalImagePadding * (i + 1)) + (iSlice * i), 0); - mwr.CurrentImage.AlphaChannel = AlphaChannelType.DeactivateAlphaChannel; - mwr.CurrentImage.ColorizeImage(blackPixelWand, greyPixelWand); - - using (var mwg = new MagickWand(wandList.CurrentImage.Width, iTrans)) + using (var croppedBitmap = SKBitmap.FromImage(subset)) { - mwg.OpenImage("gradient:black-none"); - var verticalSpacing = Convert.ToInt32(height * 0.01111111111111111111111111111111); - mwr.CurrentImage.CompositeImage(mwg, CompositeOperator.CopyOpacityCompositeOp, 0, verticalSpacing); + using (var flipped = new SKBitmap(croppedBitmap.Width, croppedBitmap.Height / 2, croppedBitmap.ColorType, croppedBitmap.AlphaType)) + { + croppedBitmap.Resize(flipped, SKBitmapResizeMethod.Lanczos3); - wandList.AddImage(mwr); - int ex = (int)(wand.CurrentImage.Width - mwg.CurrentImage.Width) / 2; - wand.CurrentImage.CompositeImage(wandList.AppendImages(true), CompositeOperator.AtopCompositeOp, ex, Convert.ToInt32(height * .045)); + using (var gradient = new SKPaint()) + { + var matrix = SKMatrix.MakeScale(1, -1); + matrix.SetScaleTranslate(1, -1, 0, flipped.Height); + gradient.Shader = SKShader.CreateLinearGradient(new SKPoint(0, 0), new SKPoint(0, flipped.Height), new[] { new SKColor(0, 0, 0, 0), SKColors.Black }, null, SKShaderTileMode.Clamp, matrix); + canvas.DrawBitmap(flipped, (horizontalImagePadding * (i + 1)) + (iSlice * i), iHeight + verticalSpacing, gradient); + } + } } } } } } - } - return wand; - }*/ + imageIndex++; + + if (imageIndex >= paths.Length) + imageIndex = 0; + } + } + + return bitmap; } private SKBitmap BuildSquareCollageBitmap(string[] paths, int width, int height) @@ -224,7 +143,7 @@ namespace Emby.Drawing.Skia { // scale image currentBitmap.Resize(resizedBitmap, SKBitmapResizeMethod.Lanczos3); - + // draw this image into the strip at the next position var xPos = x * cellWidth; var yPos = y * cellHeight; @@ -242,4 +161,4 @@ namespace Emby.Drawing.Skia return bitmap; } } -} +} \ No newline at end of file diff --git a/Emby.Drawing/ImageProcessor.cs b/Emby.Drawing/ImageProcessor.cs index bee0e9b69b..accabcf14b 100644 --- a/Emby.Drawing/ImageProcessor.cs +++ b/Emby.Drawing/ImageProcessor.cs @@ -136,14 +136,6 @@ namespace Emby.Drawing } } - private string CroppedWhitespaceImageCachePath - { - get - { - return Path.Combine(_appPaths.ImageCachePath, "cropped-images"); - } - } - public void AddParts(IEnumerable enhancers) { ImageEnhancers = enhancers.ToArray(); @@ -186,14 +178,6 @@ namespace Emby.Drawing return new Tuple(originalImagePath, MimeTypes.GetMimeType(originalImagePath), dateModified); } - if (options.CropWhiteSpace && _imageEncoder.SupportsImageEncoding) - { - var tuple = await GetWhitespaceCroppedImage(originalImagePath, dateModified).ConfigureAwait(false); - - originalImagePath = tuple.Item1; - dateModified = tuple.Item2; - } - if (options.Enhancers.Count > 0) { var tuple = await GetEnhancedImage(new ItemImageInfo @@ -400,46 +384,6 @@ namespace Emby.Drawing return requestedFormat; } - /// - /// Crops whitespace from an image, caches the result, and returns the cached path - /// - private async Task> GetWhitespaceCroppedImage(string originalImagePath, DateTime dateModified) - { - var name = originalImagePath; - name += "datemodified=" + dateModified.Ticks; - - var croppedImagePath = GetCachePath(CroppedWhitespaceImageCachePath, name, Path.GetExtension(originalImagePath)); - - // Check again in case of contention - if (_fileSystem.FileExists(croppedImagePath)) - { - return GetResult(croppedImagePath); - } - - try - { - _fileSystem.CreateDirectory(_fileSystem.GetDirectoryName(croppedImagePath)); - var tmpPath = Path.ChangeExtension(Path.Combine(_appPaths.TempDirectory, Guid.NewGuid().ToString("N")), Path.GetExtension(croppedImagePath)); - _fileSystem.CreateDirectory(_fileSystem.GetDirectoryName(tmpPath)); - - _imageEncoder.CropWhiteSpace(originalImagePath, tmpPath); - CopyFile(tmpPath, croppedImagePath); - return GetResult(tmpPath); - } - catch (NotImplementedException) - { - // No need to spam the log with an error message - return new Tuple(originalImagePath, dateModified); - } - catch (Exception ex) - { - // We have to have a catch-all here because some of the .net image methods throw a plain old Exception - _logger.ErrorException("Error cropping image {0}", ex, originalImagePath); - - return new Tuple(originalImagePath, dateModified); - } - } - private Tuple GetResult(string path) { return new Tuple(path, _fileSystem.GetLastWriteTimeUtc(path)); diff --git a/MediaBrowser.Controller/Drawing/IImageEncoder.cs b/MediaBrowser.Controller/Drawing/IImageEncoder.cs index de1909e542..ecc99caf94 100644 --- a/MediaBrowser.Controller/Drawing/IImageEncoder.cs +++ b/MediaBrowser.Controller/Drawing/IImageEncoder.cs @@ -16,12 +16,6 @@ namespace MediaBrowser.Controller.Drawing /// The supported output formats. ImageFormat[] SupportedOutputFormats { get; } /// - /// Crops the white space. - /// - /// The input path. - /// The output path. - void CropWhiteSpace(string inputPath, string outputPath); - /// /// Encodes the image. /// /// The input path. diff --git a/MediaBrowser.Controller/Drawing/ImageProcessingOptions.cs b/MediaBrowser.Controller/Drawing/ImageProcessingOptions.cs index f4b3d94554..70ac083430 100644 --- a/MediaBrowser.Controller/Drawing/ImageProcessingOptions.cs +++ b/MediaBrowser.Controller/Drawing/ImageProcessingOptions.cs @@ -86,6 +86,7 @@ namespace MediaBrowser.Controller.Drawing PercentPlayed.Equals(0) && !UnplayedCount.HasValue && !Blur.HasValue && + !CropWhiteSpace && string.IsNullOrEmpty(BackgroundColor) && string.IsNullOrEmpty(ForegroundLayer); } diff --git a/MediaBrowser.ServerApplication/ImageEncoderHelper.cs b/MediaBrowser.ServerApplication/ImageEncoderHelper.cs index 8c3d8d2130..99ccdbbe87 100644 --- a/MediaBrowser.ServerApplication/ImageEncoderHelper.cs +++ b/MediaBrowser.ServerApplication/ImageEncoderHelper.cs @@ -26,7 +26,7 @@ namespace MediaBrowser.Server.Startup.Common { try { - return new SkiaEncoder(logManager.GetLogger("ImageMagick"), appPaths, httpClient, fileSystem); + //return new SkiaEncoder(logManager.GetLogger("ImageMagick"), appPaths, httpClient, fileSystem); } catch { From e9ea1d4ce27452b0c29d08f5dec6f4363aee9d14 Mon Sep 17 00:00:00 2001 From: Luke Pulverenti Date: Wed, 10 May 2017 14:02:08 -0400 Subject: [PATCH 4/6] fixes #2559 - URL only movie.nfo does not seem to work at all --- .../Parsers/BaseNfoParser.cs | 25 ++++++++++++++++++- .../Parsers/SeriesNfoParser.cs | 13 ++++++++++ 2 files changed, 37 insertions(+), 1 deletion(-) diff --git a/MediaBrowser.XbmcMetadata/Parsers/BaseNfoParser.cs b/MediaBrowser.XbmcMetadata/Parsers/BaseNfoParser.cs index dfd4694c36..d8f7cb57f4 100644 --- a/MediaBrowser.XbmcMetadata/Parsers/BaseNfoParser.cs +++ b/MediaBrowser.XbmcMetadata/Parsers/BaseNfoParser.cs @@ -15,6 +15,7 @@ using System.Text; using System.Text.RegularExpressions; using System.Threading; using System.Xml; +using MediaBrowser.Controller.Entities.TV; using MediaBrowser.Model.IO; using MediaBrowser.Model.Xml; @@ -227,6 +228,11 @@ namespace MediaBrowser.XbmcMetadata.Parsers } } + protected virtual string MovieDbParserSearchString + { + get { return "themoviedb.org/movie/"; } + } + private void ParseProviderLinks(T item, string xml) { //Look for a match for the Regex pattern "tt" followed by 7 digits @@ -238,7 +244,7 @@ namespace MediaBrowser.XbmcMetadata.Parsers // Support Tmdb // http://www.themoviedb.org/movie/36557 - var srch = "themoviedb.org/movie/"; + var srch = MovieDbParserSearchString; var index = xml.IndexOf(srch, StringComparison.OrdinalIgnoreCase); if (index != -1) @@ -250,6 +256,23 @@ namespace MediaBrowser.XbmcMetadata.Parsers item.SetProviderId(MetadataProviders.Tmdb, tmdbId); } } + + if (item is Series) + { + srch = "thetvdb.com/?tab=series&id="; + + index = xml.IndexOf(srch, StringComparison.OrdinalIgnoreCase); + + if (index != -1) + { + var tvdbId = xml.Substring(index + srch.Length).TrimEnd('/'); + int value; + if (!string.IsNullOrWhiteSpace(tvdbId) && int.TryParse(tvdbId, NumberStyles.Any, CultureInfo.InvariantCulture, out value)) + { + item.SetProviderId(MetadataProviders.Tvdb, tvdbId); + } + } + } } protected virtual void FetchDataFromXmlNode(XmlReader reader, MetadataResult itemResult) diff --git a/MediaBrowser.XbmcMetadata/Parsers/SeriesNfoParser.cs b/MediaBrowser.XbmcMetadata/Parsers/SeriesNfoParser.cs index 98016f4f70..b0db4e6f38 100644 --- a/MediaBrowser.XbmcMetadata/Parsers/SeriesNfoParser.cs +++ b/MediaBrowser.XbmcMetadata/Parsers/SeriesNfoParser.cs @@ -13,6 +13,19 @@ namespace MediaBrowser.XbmcMetadata.Parsers { public class SeriesNfoParser : BaseNfoParser { + protected override bool SupportsUrlAfterClosingXmlTag + { + get + { + return true; + } + } + + protected override string MovieDbParserSearchString + { + get { return "themoviedb.org/tv/"; } + } + /// /// Fetches the data from XML node. /// From 369df3ffdaa8da2f687ad6c1028aa955d219f34e Mon Sep 17 00:00:00 2001 From: Luke Pulverenti Date: Wed, 10 May 2017 15:12:03 -0400 Subject: [PATCH 5/6] add smb fixes --- Emby.Common.Implementations/IO/SharpCifsFileSystem.cs | 5 +++++ Emby.Server.Core/ApplicationHost.cs | 5 ++++- .../AppBase/BaseConfigurationManager.cs | 4 ++-- Emby.Server.Implementations/AppBase/ConfigurationHelper.cs | 2 +- Emby.Server.Implementations/Devices/DeviceManager.cs | 2 +- Emby.Server.Implementations/Devices/DeviceRepository.cs | 4 ++-- Emby.Server.Implementations/FFMpeg/FFMpegLoader.cs | 4 ++-- Emby.Server.Implementations/Library/LibraryManager.cs | 1 + .../Library/Validators/ArtistsValidator.cs | 2 +- .../Library/Validators/GameGenresValidator.cs | 2 +- .../Library/Validators/GenresValidator.cs | 2 +- .../Library/Validators/MusicGenresValidator.cs | 2 +- .../Library/Validators/StudiosValidator.cs | 2 +- .../Library/Validators/YearsPostScanTask.cs | 4 +++- 14 files changed, 26 insertions(+), 15 deletions(-) diff --git a/Emby.Common.Implementations/IO/SharpCifsFileSystem.cs b/Emby.Common.Implementations/IO/SharpCifsFileSystem.cs index 0a407d64f5..283bcdef05 100644 --- a/Emby.Common.Implementations/IO/SharpCifsFileSystem.cs +++ b/Emby.Common.Implementations/IO/SharpCifsFileSystem.cs @@ -53,6 +53,11 @@ namespace Emby.Common.Implementations.IO if (separator == '/') { result = result.Replace('\\', '/'); + + if (result.StartsWith("smb:/", StringComparison.OrdinalIgnoreCase) && !result.StartsWith("smb://", StringComparison.OrdinalIgnoreCase)) + { + result = result.Replace("smb:/", "smb://"); + } } return result; diff --git a/Emby.Server.Core/ApplicationHost.cs b/Emby.Server.Core/ApplicationHost.cs index 6a3881caf8..5ceef0754e 100644 --- a/Emby.Server.Core/ApplicationHost.cs +++ b/Emby.Server.Core/ApplicationHost.cs @@ -761,7 +761,10 @@ namespace Emby.Server.Core return null; } - X509Certificate2 localCert = new X509Certificate2(certificateLocation, info.Password); + // Don't use an empty string password + var password = string.IsNullOrWhiteSpace(info.Password) ? null : info.Password; + + X509Certificate2 localCert = new X509Certificate2(certificateLocation, password); //localCert.PrivateKey = PrivateKey.CreateFromFile(pvk_file).RSA; if (!localCert.HasPrivateKey) { diff --git a/Emby.Server.Implementations/AppBase/BaseConfigurationManager.cs b/Emby.Server.Implementations/AppBase/BaseConfigurationManager.cs index 13874223cc..385b4bd518 100644 --- a/Emby.Server.Implementations/AppBase/BaseConfigurationManager.cs +++ b/Emby.Server.Implementations/AppBase/BaseConfigurationManager.cs @@ -126,7 +126,7 @@ namespace Emby.Server.Implementations.AppBase Logger.Info("Saving system configuration"); var path = CommonApplicationPaths.SystemConfigurationFilePath; - FileSystem.CreateDirectory(Path.GetDirectoryName(path)); + FileSystem.CreateDirectory(FileSystem.GetDirectoryName(path)); lock (_configurationSyncLock) { @@ -293,7 +293,7 @@ namespace Emby.Server.Implementations.AppBase _configurations.AddOrUpdate(key, configuration, (k, v) => configuration); var path = GetConfigurationFile(key); - FileSystem.CreateDirectory(Path.GetDirectoryName(path)); + FileSystem.CreateDirectory(FileSystem.GetDirectoryName(path)); lock (_configurationSyncLock) { diff --git a/Emby.Server.Implementations/AppBase/ConfigurationHelper.cs b/Emby.Server.Implementations/AppBase/ConfigurationHelper.cs index ad2f459459..d6a41dd67b 100644 --- a/Emby.Server.Implementations/AppBase/ConfigurationHelper.cs +++ b/Emby.Server.Implementations/AppBase/ConfigurationHelper.cs @@ -47,7 +47,7 @@ namespace Emby.Server.Implementations.AppBase // If the file didn't exist before, or if something has changed, re-save if (buffer == null || !buffer.SequenceEqual(newBytes)) { - fileSystem.CreateDirectory(Path.GetDirectoryName(path)); + fileSystem.CreateDirectory(fileSystem.GetDirectoryName(path)); // Save it after load in case we got new items fileSystem.WriteAllBytes(path, newBytes); diff --git a/Emby.Server.Implementations/Devices/DeviceManager.cs b/Emby.Server.Implementations/Devices/DeviceManager.cs index 588b42a093..b246ef1962 100644 --- a/Emby.Server.Implementations/Devices/DeviceManager.cs +++ b/Emby.Server.Implementations/Devices/DeviceManager.cs @@ -158,7 +158,7 @@ namespace Emby.Server.Implementations.Devices _libraryMonitor.ReportFileSystemChangeBeginning(path); - _fileSystem.CreateDirectory(Path.GetDirectoryName(path)); + _fileSystem.CreateDirectory(_fileSystem.GetDirectoryName(path)); try { diff --git a/Emby.Server.Implementations/Devices/DeviceRepository.cs b/Emby.Server.Implementations/Devices/DeviceRepository.cs index f739765b34..de0dfda2ed 100644 --- a/Emby.Server.Implementations/Devices/DeviceRepository.cs +++ b/Emby.Server.Implementations/Devices/DeviceRepository.cs @@ -46,7 +46,7 @@ namespace Emby.Server.Implementations.Devices public Task SaveDevice(DeviceInfo device) { var path = Path.Combine(GetDevicePath(device.Id), "device.json"); - _fileSystem.CreateDirectory(Path.GetDirectoryName(path)); + _fileSystem.CreateDirectory(_fileSystem.GetDirectoryName(path)); lock (_syncLock) { @@ -180,7 +180,7 @@ namespace Emby.Server.Implementations.Devices public void AddCameraUpload(string deviceId, LocalFileInfo file) { var path = Path.Combine(GetDevicePath(deviceId), "camerauploads.json"); - _fileSystem.CreateDirectory(Path.GetDirectoryName(path)); + _fileSystem.CreateDirectory(_fileSystem.GetDirectoryName(path)); lock (_syncLock) { diff --git a/Emby.Server.Implementations/FFMpeg/FFMpegLoader.cs b/Emby.Server.Implementations/FFMpeg/FFMpegLoader.cs index 2becebb3d3..3c8ad55fe4 100644 --- a/Emby.Server.Implementations/FFMpeg/FFMpegLoader.cs +++ b/Emby.Server.Implementations/FFMpeg/FFMpegLoader.cs @@ -97,7 +97,7 @@ namespace Emby.Server.Implementations.FFMpeg else { info = existingVersion; - versionedDirectoryPath = Path.GetDirectoryName(info.EncoderPath); + versionedDirectoryPath = _fileSystem.GetDirectoryName(info.EncoderPath); excludeFromDeletions.Add(versionedDirectoryPath); } } @@ -135,7 +135,7 @@ namespace Emby.Server.Implementations.FFMpeg { EncoderPath = encoder, ProbePath = probe, - Version = Path.GetFileName(Path.GetDirectoryName(probe)) + Version = Path.GetFileName(_fileSystem.GetDirectoryName(probe)) }; } } diff --git a/Emby.Server.Implementations/Library/LibraryManager.cs b/Emby.Server.Implementations/Library/LibraryManager.cs index 685c794b76..3c94f97842 100644 --- a/Emby.Server.Implementations/Library/LibraryManager.cs +++ b/Emby.Server.Implementations/Library/LibraryManager.cs @@ -1197,6 +1197,7 @@ namespace Emby.Server.Implementations.Library catch (OperationCanceledException) { _logger.Info("Post-scan task cancelled: {0}", task.GetType().Name); + throw; } catch (Exception ex) { diff --git a/Emby.Server.Implementations/Library/Validators/ArtistsValidator.cs b/Emby.Server.Implementations/Library/Validators/ArtistsValidator.cs index 643c5970e1..d4be2dabed 100644 --- a/Emby.Server.Implementations/Library/Validators/ArtistsValidator.cs +++ b/Emby.Server.Implementations/Library/Validators/ArtistsValidator.cs @@ -63,7 +63,7 @@ namespace Emby.Server.Implementations.Library.Validators catch (OperationCanceledException) { // Don't clutter the log - break; + throw; } catch (Exception ex) { diff --git a/Emby.Server.Implementations/Library/Validators/GameGenresValidator.cs b/Emby.Server.Implementations/Library/Validators/GameGenresValidator.cs index b1820bb917..f7fbb93318 100644 --- a/Emby.Server.Implementations/Library/Validators/GameGenresValidator.cs +++ b/Emby.Server.Implementations/Library/Validators/GameGenresValidator.cs @@ -53,7 +53,7 @@ namespace Emby.Server.Implementations.Library.Validators catch (OperationCanceledException) { // Don't clutter the log - break; + throw; } catch (Exception ex) { diff --git a/Emby.Server.Implementations/Library/Validators/GenresValidator.cs b/Emby.Server.Implementations/Library/Validators/GenresValidator.cs index d8956f78a1..d71e77a9a7 100644 --- a/Emby.Server.Implementations/Library/Validators/GenresValidator.cs +++ b/Emby.Server.Implementations/Library/Validators/GenresValidator.cs @@ -54,7 +54,7 @@ namespace Emby.Server.Implementations.Library.Validators catch (OperationCanceledException) { // Don't clutter the log - break; + throw; } catch (Exception ex) { diff --git a/Emby.Server.Implementations/Library/Validators/MusicGenresValidator.cs b/Emby.Server.Implementations/Library/Validators/MusicGenresValidator.cs index 983c881b75..98d53c1250 100644 --- a/Emby.Server.Implementations/Library/Validators/MusicGenresValidator.cs +++ b/Emby.Server.Implementations/Library/Validators/MusicGenresValidator.cs @@ -54,7 +54,7 @@ namespace Emby.Server.Implementations.Library.Validators catch (OperationCanceledException) { // Don't clutter the log - break; + throw; } catch (Exception ex) { diff --git a/Emby.Server.Implementations/Library/Validators/StudiosValidator.cs b/Emby.Server.Implementations/Library/Validators/StudiosValidator.cs index 6faab7bb9d..97b8ff0ac4 100644 --- a/Emby.Server.Implementations/Library/Validators/StudiosValidator.cs +++ b/Emby.Server.Implementations/Library/Validators/StudiosValidator.cs @@ -53,7 +53,7 @@ namespace Emby.Server.Implementations.Library.Validators catch (OperationCanceledException) { // Don't clutter the log - break; + throw; } catch (Exception ex) { diff --git a/Emby.Server.Implementations/Library/Validators/YearsPostScanTask.cs b/Emby.Server.Implementations/Library/Validators/YearsPostScanTask.cs index ae43c77f0a..4afb4c04a7 100644 --- a/Emby.Server.Implementations/Library/Validators/YearsPostScanTask.cs +++ b/Emby.Server.Implementations/Library/Validators/YearsPostScanTask.cs @@ -26,6 +26,8 @@ namespace Emby.Server.Implementations.Library.Validators while (yearNumber < maxYear) { + cancellationToken.ThrowIfCancellationRequested(); + try { var year = _libraryManager.GetYear(yearNumber); @@ -35,7 +37,7 @@ namespace Emby.Server.Implementations.Library.Validators catch (OperationCanceledException) { // Don't clutter the log - break; + throw; } catch (Exception ex) { From b9baf377e81e454438d9faad1b1b793a719bb667 Mon Sep 17 00:00:00 2001 From: Luke Pulverenti Date: Wed, 10 May 2017 15:14:22 -0400 Subject: [PATCH 6/6] 3.2.15.3 --- SharedVersion.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/SharedVersion.cs b/SharedVersion.cs index 570343be12..8a2e19849b 100644 --- a/SharedVersion.cs +++ b/SharedVersion.cs @@ -1,3 +1,3 @@ using System.Reflection; -[assembly: AssemblyVersion("3.2.15.2")] +[assembly: AssemblyVersion("3.2.15.3")]