From 5655787c1ac9ceedbd78c6c853a7cded33a22d49 Mon Sep 17 00:00:00 2001 From: Luke Pulverenti Date: Fri, 11 Nov 2016 12:33:10 -0500 Subject: update portable projects --- Emby.Common.Implementations/BaseApplicationHost.cs | 28 +- .../Emby.Drawing.ImageMagick.csproj | 83 + Emby.Drawing.ImageMagick/ImageHelpers.cs | 43 + Emby.Drawing.ImageMagick/ImageMagickEncoder.cs | 333 ++++ Emby.Drawing.ImageMagick/PercentPlayedDrawer.cs | 40 + Emby.Drawing.ImageMagick/PlayedIndicatorDrawer.cs | 125 ++ .../Properties/AssemblyInfo.cs | 36 + Emby.Drawing.ImageMagick/StripCollageBuilder.cs | 202 +++ Emby.Drawing.ImageMagick/UnplayedCountIndicator.cs | 75 + Emby.Drawing.ImageMagick/packages.config | 4 + Emby.Drawing.Net/DynamicImageHelpers.cs | 110 ++ Emby.Drawing.Net/Emby.Drawing.Net.csproj | 78 + Emby.Drawing.Net/GDIImageEncoder.cs | 281 ++++ Emby.Drawing.Net/ImageExtensions.cs | 217 +++ Emby.Drawing.Net/ImageHelpers.cs | 43 + Emby.Drawing.Net/PercentPlayedDrawer.cs | 34 + Emby.Drawing.Net/PlayedIndicatorDrawer.cs | 32 + Emby.Drawing.Net/Properties/AssemblyInfo.cs | 36 + Emby.Drawing.Net/UnplayedCountIndicator.cs | 50 + Emby.Drawing.Net/empty.png | Bin 0 -> 158 bytes Emby.Drawing/Common/ImageHeader.cs | 2 +- Emby.Drawing/Emby.Drawing.csproj | 42 +- Emby.Drawing/GDI/DynamicImageHelpers.cs | 110 -- Emby.Drawing/GDI/GDIImageEncoder.cs | 281 ---- Emby.Drawing/GDI/ImageExtensions.cs | 217 --- Emby.Drawing/GDI/PercentPlayedDrawer.cs | 34 - Emby.Drawing/GDI/PlayedIndicatorDrawer.cs | 32 - Emby.Drawing/GDI/UnplayedCountIndicator.cs | 50 - Emby.Drawing/GDI/empty.png | Bin 158 -> 0 bytes Emby.Drawing/IImageEncoder.cs | 61 - Emby.Drawing/ImageHelpers.cs | 43 - Emby.Drawing/ImageMagick/ImageMagickEncoder.cs | 333 ---- Emby.Drawing/ImageMagick/PercentPlayedDrawer.cs | 40 - Emby.Drawing/ImageMagick/PlayedIndicatorDrawer.cs | 125 -- Emby.Drawing/ImageMagick/StripCollageBuilder.cs | 202 --- Emby.Drawing/ImageMagick/UnplayedCountIndicator.cs | 75 - Emby.Drawing/ImageProcessor.cs | 14 +- Emby.Drawing/packages.config | 4 - Emby.Drawing/project.json | 17 + Emby.Server.Core/ApplicationHost.cs | 1629 +++++++++++++++++++ Emby.Server.Core/Emby.Server.Core.xproj | 8 + Emby.Server.Core/HttpServerFactory.cs | 107 ++ Emby.Server.Core/INativeApp.cs | 6 - Emby.Server.Core/project.json | 57 +- .../Emby.Server.Implementations.csproj | 3 - MediaBrowser.Controller/Drawing/IImageEncoder.cs | 60 + .../MediaBrowser.Controller.csproj | 1 + .../Providers/PersonXmlProvider.cs | 42 +- .../Savers/PersonXmlSaver.cs | 88 +- .../ApplicationHost.cs | 1653 -------------------- .../HttpServerFactory.cs | 107 -- .../ImageEncoderHelper.cs | 46 + .../MediaBrowser.Server.Startup.Common.csproj | 21 +- MediaBrowser.Server.Startup.Common/packages.config | 3 +- MediaBrowser.sln | 64 + 55 files changed, 3915 insertions(+), 3512 deletions(-) create mode 100644 Emby.Drawing.ImageMagick/Emby.Drawing.ImageMagick.csproj create mode 100644 Emby.Drawing.ImageMagick/ImageHelpers.cs create mode 100644 Emby.Drawing.ImageMagick/ImageMagickEncoder.cs create mode 100644 Emby.Drawing.ImageMagick/PercentPlayedDrawer.cs create mode 100644 Emby.Drawing.ImageMagick/PlayedIndicatorDrawer.cs create mode 100644 Emby.Drawing.ImageMagick/Properties/AssemblyInfo.cs create mode 100644 Emby.Drawing.ImageMagick/StripCollageBuilder.cs create mode 100644 Emby.Drawing.ImageMagick/UnplayedCountIndicator.cs create mode 100644 Emby.Drawing.ImageMagick/packages.config create mode 100644 Emby.Drawing.Net/DynamicImageHelpers.cs create mode 100644 Emby.Drawing.Net/Emby.Drawing.Net.csproj create mode 100644 Emby.Drawing.Net/GDIImageEncoder.cs create mode 100644 Emby.Drawing.Net/ImageExtensions.cs create mode 100644 Emby.Drawing.Net/ImageHelpers.cs create mode 100644 Emby.Drawing.Net/PercentPlayedDrawer.cs create mode 100644 Emby.Drawing.Net/PlayedIndicatorDrawer.cs create mode 100644 Emby.Drawing.Net/Properties/AssemblyInfo.cs create mode 100644 Emby.Drawing.Net/UnplayedCountIndicator.cs create mode 100644 Emby.Drawing.Net/empty.png delete mode 100644 Emby.Drawing/GDI/DynamicImageHelpers.cs delete mode 100644 Emby.Drawing/GDI/GDIImageEncoder.cs delete mode 100644 Emby.Drawing/GDI/ImageExtensions.cs delete mode 100644 Emby.Drawing/GDI/PercentPlayedDrawer.cs delete mode 100644 Emby.Drawing/GDI/PlayedIndicatorDrawer.cs delete mode 100644 Emby.Drawing/GDI/UnplayedCountIndicator.cs delete mode 100644 Emby.Drawing/GDI/empty.png delete mode 100644 Emby.Drawing/IImageEncoder.cs delete mode 100644 Emby.Drawing/ImageHelpers.cs delete mode 100644 Emby.Drawing/ImageMagick/ImageMagickEncoder.cs delete mode 100644 Emby.Drawing/ImageMagick/PercentPlayedDrawer.cs delete mode 100644 Emby.Drawing/ImageMagick/PlayedIndicatorDrawer.cs delete mode 100644 Emby.Drawing/ImageMagick/StripCollageBuilder.cs delete mode 100644 Emby.Drawing/ImageMagick/UnplayedCountIndicator.cs delete mode 100644 Emby.Drawing/packages.config create mode 100644 Emby.Drawing/project.json create mode 100644 Emby.Server.Core/ApplicationHost.cs create mode 100644 Emby.Server.Core/HttpServerFactory.cs create mode 100644 MediaBrowser.Controller/Drawing/IImageEncoder.cs delete mode 100644 MediaBrowser.Server.Startup.Common/ApplicationHost.cs delete mode 100644 MediaBrowser.Server.Startup.Common/HttpServerFactory.cs create mode 100644 MediaBrowser.Server.Startup.Common/ImageEncoderHelper.cs diff --git a/Emby.Common.Implementations/BaseApplicationHost.cs b/Emby.Common.Implementations/BaseApplicationHost.cs index 9571ea3889..f0309511e0 100644 --- a/Emby.Common.Implementations/BaseApplicationHost.cs +++ b/Emby.Common.Implementations/BaseApplicationHost.cs @@ -152,8 +152,6 @@ namespace Emby.Common.Implementations protected IIsoManager IsoManager { get; private set; } - protected ISystemEvents SystemEvents { get; private set; } - protected IProcessFactory ProcessFactory { get; private set; } protected ITimerFactory TimerFactory { get; private set; } protected ISocketFactory SocketFactory { get; private set; } @@ -193,22 +191,29 @@ namespace Emby.Common.Implementations get { return EnvironmentInfo.OperatingSystemName; } } - public IMemoryStreamFactory MemoryStreamProvider { get; set; } - /// /// The container /// protected readonly SimpleInjector.Container Container = new SimpleInjector.Container(); + protected ISystemEvents SystemEvents { get; private set; } + protected IMemoryStreamFactory MemoryStreamFactory { get; private set; } + /// /// Initializes a new instance of the class. /// protected BaseApplicationHost(TApplicationPathsType applicationPaths, ILogManager logManager, IFileSystem fileSystem, - IEnvironmentInfo environmentInfo) + IEnvironmentInfo environmentInfo, + ISystemEvents systemEvents, + IMemoryStreamFactory memoryStreamFactory, + INetworkManager networkManager) { + NetworkManager = networkManager; EnvironmentInfo = environmentInfo; + SystemEvents = systemEvents; + MemoryStreamFactory = memoryStreamFactory; // hack alert, until common can target .net core BaseExtensions.CryptographyProvider = CryptographyProvider; @@ -236,9 +241,6 @@ namespace Emby.Common.Implementations JsonSerializer = CreateJsonSerializer(); - MemoryStreamProvider = CreateMemoryStreamProvider(); - SystemEvents = CreateSystemEvents(); - OnLoggerLoaded(true); LogManager.LoggerLoaded += (s, e) => OnLoggerLoaded(false); @@ -270,9 +272,6 @@ namespace Emby.Common.Implementations progress.Report(100); } - protected abstract IMemoryStreamFactory CreateMemoryStreamProvider(); - protected abstract ISystemEvents CreateSystemEvents(); - protected virtual void OnLoggerLoaded(bool isFirstLoad) { Logger.Info("Application version: {0}", ApplicationVersion); @@ -524,7 +523,7 @@ return null; RegisterSingleInstance(JsonSerializer); RegisterSingleInstance(XmlSerializer); - RegisterSingleInstance(MemoryStreamProvider); + RegisterSingleInstance(MemoryStreamFactory); RegisterSingleInstance(SystemEvents); RegisterSingleInstance(LogManager); @@ -535,10 +534,9 @@ return null; RegisterSingleInstance(FileSystemManager); - HttpClient = new HttpClientManager.HttpClientManager(ApplicationPaths, LogManager.GetLogger("HttpClient"), FileSystemManager, MemoryStreamProvider); + HttpClient = new HttpClientManager.HttpClientManager(ApplicationPaths, LogManager.GetLogger("HttpClient"), FileSystemManager, MemoryStreamFactory); RegisterSingleInstance(HttpClient); - NetworkManager = CreateNetworkManager(LogManager.GetLogger("NetworkManager")); RegisterSingleInstance(NetworkManager); IsoManager = new IsoManager(); @@ -591,8 +589,6 @@ return null; } } - protected abstract INetworkManager CreateNetworkManager(ILogger logger); - /// /// Creates an instance of type and resolves all constructor dependancies /// diff --git a/Emby.Drawing.ImageMagick/Emby.Drawing.ImageMagick.csproj b/Emby.Drawing.ImageMagick/Emby.Drawing.ImageMagick.csproj new file mode 100644 index 0000000000..98e99c92b1 --- /dev/null +++ b/Emby.Drawing.ImageMagick/Emby.Drawing.ImageMagick.csproj @@ -0,0 +1,83 @@ + + + + + Debug + AnyCPU + {6CFEE013-6E7C-432B-AC37-CABF0880C69A} + Library + Properties + Emby.Drawing.ImageMagick + Emby.Drawing.ImageMagick + v4.5.2 + 512 + + + true + full + false + bin\Debug\ + DEBUG;TRACE + prompt + 4 + + + pdbonly + true + bin\Release\ + TRACE + prompt + 4 + + + + ..\packages\ImageMagickSharp.1.0.0.18\lib\net45\ImageMagickSharp.dll + True + + + + + + + + + + + + + + + + + + + + + + + + + + + + {9142eefa-7570-41e1-bfcc-468bb571af2f} + MediaBrowser.Common + + + {17e1f4e6-8abd-4fe5-9ecf-43d4b6087ba2} + MediaBrowser.Controller + + + {7eeeb4bb-f3e8-48fc-b4c5-70f0fff8329b} + MediaBrowser.Model + + + + + \ No newline at end of file diff --git a/Emby.Drawing.ImageMagick/ImageHelpers.cs b/Emby.Drawing.ImageMagick/ImageHelpers.cs new file mode 100644 index 0000000000..c623c21aa2 --- /dev/null +++ b/Emby.Drawing.ImageMagick/ImageHelpers.cs @@ -0,0 +1,43 @@ +using System; +using System.Collections.Generic; +using System.Linq; + +namespace Emby.Drawing.ImageMagick +{ + internal static class ImageHelpers + { + internal static List ProjectPaths(List paths, int count) + { + if (count <= 0) + { + throw new ArgumentOutOfRangeException("count"); + } + if (paths.Count == 0) + { + throw new ArgumentOutOfRangeException("paths"); + } + + var list = new List(); + + AddToList(list, paths, count); + + return list.Take(count).ToList(); + } + + private static void AddToList(List list, List paths, int count) + { + while (list.Count < count) + { + foreach (var path in paths) + { + list.Add(path); + + if (list.Count >= count) + { + return; + } + } + } + } + } +} diff --git a/Emby.Drawing.ImageMagick/ImageMagickEncoder.cs b/Emby.Drawing.ImageMagick/ImageMagickEncoder.cs new file mode 100644 index 0000000000..3410ef003f --- /dev/null +++ b/Emby.Drawing.ImageMagick/ImageMagickEncoder.cs @@ -0,0 +1,333 @@ +using System.Threading.Tasks; +using ImageMagickSharp; +using MediaBrowser.Common.Configuration; +using MediaBrowser.Common.Net; +using MediaBrowser.Controller.Drawing; +using MediaBrowser.Model.Drawing; +using MediaBrowser.Model.Logging; +using System; +using System.IO; +using System.Linq; +using MediaBrowser.Common.IO; +using MediaBrowser.Controller.Configuration; +using MediaBrowser.Controller.IO; +using MediaBrowser.Model.IO; + +namespace Emby.Drawing.ImageMagick +{ + public class ImageMagickEncoder : IImageEncoder + { + private readonly ILogger _logger; + private readonly IApplicationPaths _appPaths; + private readonly IHttpClient _httpClient; + private readonly IFileSystem _fileSystem; + private readonly IServerConfigurationManager _config; + + public ImageMagickEncoder(ILogger logger, IApplicationPaths appPaths, IHttpClient httpClient, IFileSystem fileSystem, IServerConfigurationManager config) + { + _logger = logger; + _appPaths = appPaths; + _httpClient = httpClient; + _fileSystem = fileSystem; + _config = config; + + 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[] + { + "tiff", + "jpeg", + "jpg", + "png", + "aiff", + "cr2", + "crw", + "dng", + "nef", + "orf", + "pef", + "arw", + "webp", + "gif", + "bmp", + "erf", + "raf", + "rw2", + "nrw" + }; + } + } + + public ImageFormat[] SupportedOutputFormats + { + get + { + if (_webpAvailable) + { + return new[] { ImageFormat.Webp, ImageFormat.Gif, ImageFormat.Jpg, ImageFormat.Png }; + } + return new[] { ImageFormat.Gif, ImageFormat.Jpg, ImageFormat.Png }; + } + } + + private void LogVersion() + { + _logger.Info("ImageMagick version: " + GetVersion()); + TestWebp(); + Wand.SetMagickThreadCount(1); + } + + public static string GetVersion() + { + return Wand.VersionString; + } + + private bool _webpAvailable = true; + private void TestWebp() + { + try + { + var tmpPath = Path.Combine(_appPaths.TempDirectory, Guid.NewGuid() + ".webp"); + _fileSystem.CreateDirectory(Path.GetDirectoryName(tmpPath)); + + using (var wand = new MagickWand(1, 1, new PixelWand("none", 1))) + { + wand.SaveImage(tmpPath); + } + } + catch + { + //_logger.ErrorException("Error loading webp: ", ex); + _webpAvailable = false; + } + } + + 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(); + + using (var wand = new MagickWand()) + { + wand.PingImage(path); + var img = wand.CurrentImage; + + return new ImageSize + { + Width = img.Width, + Height = img.Height + }; + } + } + + private bool HasTransparency(string path) + { + var ext = Path.GetExtension(path); + + return string.Equals(ext, ".png", StringComparison.OrdinalIgnoreCase) || + string.Equals(ext, ".webp", StringComparison.OrdinalIgnoreCase); + } + + public void EncodeImage(string inputPath, string outputPath, bool autoOrient, int width, int height, int quality, ImageProcessingOptions options, ImageFormat selectedOutputFormat) + { + // Even if the caller specified 100, don't use it because it takes forever + quality = Math.Min(quality, 99); + + if (string.IsNullOrWhiteSpace(options.BackgroundColor) || !HasTransparency(inputPath)) + { + using (var originalImage = new MagickWand(inputPath)) + { + ScaleImage(originalImage, width, height); + + if (autoOrient) + { + AutoOrientImage(originalImage); + } + + AddForegroundLayer(originalImage, options); + DrawIndicator(originalImage, width, height, options); + + originalImage.CurrentImage.CompressionQuality = quality; + originalImage.CurrentImage.StripImage(); + + originalImage.SaveImage(outputPath); + } + } + else + { + using (var wand = new MagickWand(width, height, options.BackgroundColor)) + { + using (var originalImage = new MagickWand(inputPath)) + { + ScaleImage(originalImage, width, height); + + if (autoOrient) + { + AutoOrientImage(originalImage); + } + + wand.CurrentImage.CompositeImage(originalImage, CompositeOperator.OverCompositeOp, 0, 0); + + AddForegroundLayer(wand, options); + DrawIndicator(wand, width, height, options); + + wand.CurrentImage.CompressionQuality = quality; + wand.CurrentImage.StripImage(); + + wand.SaveImage(outputPath); + } + } + } + } + + private void AddForegroundLayer(MagickWand wand, ImageProcessingOptions options) + { + if (string.IsNullOrWhiteSpace(options.ForegroundLayer)) + { + return; + } + + Double opacity; + if (!Double.TryParse(options.ForegroundLayer, out opacity)) opacity = .4; + + using (var pixel = new PixelWand("#000", opacity)) + using (var overlay = new MagickWand(wand.CurrentImage.Width, wand.CurrentImage.Height, pixel)) + { + wand.CurrentImage.CompositeImage(overlay, CompositeOperator.OverCompositeOp, 0, 0); + } + } + + private void AutoOrientImage(MagickWand wand) + { + wand.CurrentImage.AutoOrientImage(); + } + + public static void RotateImage(MagickWand wand, float angle) + { + using (var pixelWand = new PixelWand("none", 1)) + { + wand.CurrentImage.RotateImage(pixelWand, angle); + } + } + + private void ScaleImage(MagickWand wand, int width, int height) + { + var highQuality = false; + + if (highQuality) + { + wand.CurrentImage.ResizeImage(width, height); + } + else + { + wand.CurrentImage.ScaleImage(width, height); + } + } + + /// + /// Draws the indicator. + /// + /// The wand. + /// Width of the image. + /// Height of the image. + /// The options. + private void DrawIndicator(MagickWand wand, int imageWidth, int imageHeight, ImageProcessingOptions options) + { + if (!options.AddPlayedIndicator && !options.UnplayedCount.HasValue && options.PercentPlayed.Equals(0)) + { + return; + } + + try + { + if (options.AddPlayedIndicator) + { + var currentImageSize = new ImageSize(imageWidth, imageHeight); + + var task = new PlayedIndicatorDrawer(_appPaths, _httpClient, _fileSystem).DrawPlayedIndicator(wand, currentImageSize); + Task.WaitAll(task); + } + else if (options.UnplayedCount.HasValue) + { + var currentImageSize = new ImageSize(imageWidth, imageHeight); + + new UnplayedCountIndicator(_appPaths, _fileSystem).DrawUnplayedCountIndicator(wand, currentImageSize, options.UnplayedCount.Value); + } + + if (options.PercentPlayed > 0) + { + new PercentPlayedDrawer().Process(wand, options.PercentPlayed); + } + } + catch (Exception ex) + { + _logger.ErrorException("Error drawing indicator overlay", ex); + } + } + + public void CreateImageCollage(ImageCollageOptions options) + { + double ratio = options.Width; + ratio /= options.Height; + + if (ratio >= 1.4) + { + new StripCollageBuilder(_appPaths, _fileSystem).BuildThumbCollage(options.InputPaths.ToList(), options.OutputPath, options.Width, options.Height); + } + else if (ratio >= .9) + { + new StripCollageBuilder(_appPaths, _fileSystem).BuildSquareCollage(options.InputPaths.ToList(), options.OutputPath, options.Width, options.Height); + } + else + { + new StripCollageBuilder(_appPaths, _fileSystem).BuildPosterCollage(options.InputPaths.ToList(), options.OutputPath, options.Width, options.Height); + } + } + + public string Name + { + get { return "ImageMagick"; } + } + + private bool _disposed; + public void Dispose() + { + _disposed = true; + Wand.CloseEnvironment(); + } + + 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.ImageMagick/PercentPlayedDrawer.cs b/Emby.Drawing.ImageMagick/PercentPlayedDrawer.cs new file mode 100644 index 0000000000..90f9d56095 --- /dev/null +++ b/Emby.Drawing.ImageMagick/PercentPlayedDrawer.cs @@ -0,0 +1,40 @@ +using ImageMagickSharp; +using System; + +namespace Emby.Drawing.ImageMagick +{ + public class PercentPlayedDrawer + { + private const int IndicatorHeight = 8; + + public void Process(MagickWand wand, double percent) + { + var currentImage = wand.CurrentImage; + var height = currentImage.Height; + + using (var draw = new DrawingWand()) + { + using (PixelWand pixel = new PixelWand()) + { + var endX = currentImage.Width - 1; + var endY = height - 1; + + pixel.Color = "black"; + pixel.Opacity = 0.4; + draw.FillColor = pixel; + draw.DrawRectangle(0, endY - IndicatorHeight, endX, endY); + + double foregroundWidth = endX; + foregroundWidth *= percent; + foregroundWidth /= 100; + + pixel.Color = "#52B54B"; + pixel.Opacity = 0; + draw.FillColor = pixel; + draw.DrawRectangle(0, endY - IndicatorHeight, Convert.ToInt32(Math.Round(foregroundWidth)), endY); + wand.CurrentImage.DrawImage(draw); + } + } + } + } +} diff --git a/Emby.Drawing.ImageMagick/PlayedIndicatorDrawer.cs b/Emby.Drawing.ImageMagick/PlayedIndicatorDrawer.cs new file mode 100644 index 0000000000..14fb0ddf15 --- /dev/null +++ b/Emby.Drawing.ImageMagick/PlayedIndicatorDrawer.cs @@ -0,0 +1,125 @@ +using ImageMagickSharp; +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; + +namespace Emby.Drawing.ImageMagick +{ + public class PlayedIndicatorDrawer + { + private const int FontSize = 52; + 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(MagickWand wand, ImageSize imageSize) + { + var x = imageSize.Width - OffsetFromTopRightCorner; + + using (var draw = new DrawingWand()) + { + using (PixelWand pixel = new PixelWand()) + { + pixel.Color = "#52B54B"; + pixel.Opacity = 0.2; + draw.FillColor = pixel; + draw.DrawCircle(x, OffsetFromTopRightCorner, x - 20, OffsetFromTopRightCorner - 20); + + pixel.Opacity = 0; + pixel.Color = "white"; + draw.FillColor = pixel; + draw.Font = await DownloadFont("webdings.ttf", "https://github.com/MediaBrowser/Emby.Resources/raw/master/fonts/webdings.ttf", _appPaths, _iHttpClient, _fileSystem).ConfigureAwait(false); + draw.FontSize = FontSize; + draw.FontStyle = FontStyleType.NormalStyle; + draw.TextAlignment = TextAlignType.CenterAlign; + draw.FontWeight = FontWeightType.RegularStyle; + draw.TextAntialias = true; + draw.DrawAnnotation(x + 4, OffsetFromTopRightCorner + 14, "a"); + + draw.FillColor = pixel; + wand.CurrentImage.DrawImage(draw); + } + } + } + + 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(Path.GetDirectoryName(tempPath)); + + using (var stream = typeof(PlayedIndicatorDrawer).Assembly.GetManifestResourceStream(namespacePath)) + { + using (var fileStream = new FileStream(tempPath, FileMode.Create, FileAccess.Write, FileShare.Read)) + { + stream.CopyTo(fileStream); + } + } + + fileSystem.CreateDirectory(Path.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(Path.GetDirectoryName(filePath)); + + try + { + fileSystem.CopyFile(tempPath, filePath, false); + } + catch (IOException) + { + + } + + return tempPath; + } + } +} diff --git a/Emby.Drawing.ImageMagick/Properties/AssemblyInfo.cs b/Emby.Drawing.ImageMagick/Properties/AssemblyInfo.cs new file mode 100644 index 0000000000..1089607d67 --- /dev/null +++ b/Emby.Drawing.ImageMagick/Properties/AssemblyInfo.cs @@ -0,0 +1,36 @@ +using System.Reflection; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; + +// General Information about an assembly is controlled through the following +// set of attributes. Change these attribute values to modify the information +// associated with an assembly. +[assembly: AssemblyTitle("Emby.Drawing.ImageMagick")] +[assembly: AssemblyDescription("")] +[assembly: AssemblyConfiguration("")] +[assembly: AssemblyCompany("")] +[assembly: AssemblyProduct("Emby.Drawing.ImageMagick")] +[assembly: AssemblyCopyright("Copyright © 2016")] +[assembly: AssemblyTrademark("")] +[assembly: AssemblyCulture("")] + +// Setting ComVisible to false makes the types in this assembly not visible +// to COM components. If you need to access a type in this assembly from +// COM, set the ComVisible attribute to true on that type. +[assembly: ComVisible(false)] + +// The following GUID is for the ID of the typelib if this project is exposed to COM +[assembly: Guid("6cfee013-6e7c-432b-ac37-cabf0880c69a")] + +// Version information for an assembly consists of the following four values: +// +// Major Version +// Minor Version +// Build Number +// Revision +// +// You can specify all the values or you can default the Build and Revision Numbers +// by using the '*' as shown below: +// [assembly: AssemblyVersion("1.0.*")] +[assembly: AssemblyVersion("1.0.0.0")] +[assembly: AssemblyFileVersion("1.0.0.0")] diff --git a/Emby.Drawing.ImageMagick/StripCollageBuilder.cs b/Emby.Drawing.ImageMagick/StripCollageBuilder.cs new file mode 100644 index 0000000000..715ab46800 --- /dev/null +++ b/Emby.Drawing.ImageMagick/StripCollageBuilder.cs @@ -0,0 +1,202 @@ +using ImageMagickSharp; +using MediaBrowser.Common.Configuration; +using System; +using System.Collections.Generic; +using MediaBrowser.Common.IO; +using MediaBrowser.Controller.IO; +using MediaBrowser.Model.IO; + +namespace Emby.Drawing.ImageMagick +{ + public class StripCollageBuilder + { + private readonly IApplicationPaths _appPaths; + private readonly IFileSystem _fileSystem; + + public StripCollageBuilder(IApplicationPaths appPaths, IFileSystem fileSystem) + { + _appPaths = appPaths; + _fileSystem = fileSystem; + } + + public void BuildPosterCollage(List paths, string outputPath, int width, int height) + { + using (var wand = BuildPosterCollageWand(paths, width, height)) + { + wand.SaveImage(outputPath); + } + } + + public void BuildSquareCollage(List paths, string outputPath, int width, int height) + { + using (var wand = BuildSquareCollageWand(paths, width, height)) + { + wand.SaveImage(outputPath); + } + } + + public void BuildThumbCollage(List paths, string outputPath, int width, int height) + { + using (var wand = BuildThumbCollageWand(paths, width, height)) + { + wand.SaveImage(outputPath); + } + } + + private MagickWand BuildPosterCollageWand(List paths, int width, int height) + { + 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 MagickWand BuildThumbCollageWand(List paths, int width, int height) + { + 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 MagickWand BuildSquareCollageWand(List paths, int width, int height) + { + var inputPaths = ImageHelpers.ProjectPaths(paths, 4); + var outputWand = new MagickWand(width, height, new PixelWand("none", 1)); + var imageIndex = 0; + var cellWidth = width/2; + var cellHeight = height/2; + for (var x = 0; x < 2; x++) + { + for (var y = 0; y < 2; y++) + { + using (var temp = new MagickWand(inputPaths[imageIndex])) + { + temp.CurrentImage.ScaleImage(cellWidth, cellHeight); + // draw this image into the strip at the next position + var xPos = x*cellWidth; + var yPos = y*cellHeight; + outputWand.CurrentImage.CompositeImage(temp, CompositeOperator.OverCompositeOp, xPos, yPos); + } + imageIndex++; + } + } + + return outputWand; + } + } +} diff --git a/Emby.Drawing.ImageMagick/UnplayedCountIndicator.cs b/Emby.Drawing.ImageMagick/UnplayedCountIndicator.cs new file mode 100644 index 0000000000..c531400992 --- /dev/null +++ b/Emby.Drawing.ImageMagick/UnplayedCountIndicator.cs @@ -0,0 +1,75 @@ +using ImageMagickSharp; +using MediaBrowser.Common.Configuration; +using MediaBrowser.Model.Drawing; +using System.Globalization; +using MediaBrowser.Common.IO; +using MediaBrowser.Controller.IO; +using MediaBrowser.Model.IO; + +namespace Emby.Drawing.ImageMagick +{ + public class UnplayedCountIndicator + { + private const int OffsetFromTopRightCorner = 38; + + private readonly IApplicationPaths _appPaths; + private readonly IFileSystem _fileSystem; + + public UnplayedCountIndicator(IApplicationPaths appPaths, IFileSystem fileSystem) + { + _appPaths = appPaths; + _fileSystem = fileSystem; + } + + public void DrawUnplayedCountIndicator(MagickWand wand, ImageSize imageSize, int count) + { + var x = imageSize.Width - OffsetFromTopRightCorner; + var text = count.ToString(CultureInfo.InvariantCulture); + + using (var draw = new DrawingWand()) + { + using (PixelWand pixel = new PixelWand()) + { + pixel.Color = "#52B54B"; + pixel.Opacity = 0.2; + draw.FillColor = pixel; + draw.DrawCircle(x, OffsetFromTopRightCorner, x - 20, OffsetFromTopRightCorner - 20); + + pixel.Opacity = 0; + pixel.Color = "white"; + draw.FillColor = pixel; + draw.Font = PlayedIndicatorDrawer.ExtractFont("robotoregular.ttf", _appPaths, _fileSystem); + draw.FontStyle = FontStyleType.NormalStyle; + draw.TextAlignment = TextAlignType.CenterAlign; + draw.FontWeight = FontWeightType.RegularStyle; + draw.TextAntialias = true; + + var fontSize = 30; + var y = OffsetFromTopRightCorner + 11; + + if (text.Length == 1) + { + x += 1; + } + else if (text.Length == 2) + { + x += 1; + } + else if (text.Length >= 3) + { + //x += 1; + y -= 2; + fontSize = 24; + } + + draw.FontSize = fontSize; + draw.DrawAnnotation(x, y, text); + + draw.FillColor = pixel; + wand.CurrentImage.DrawImage(draw); + } + + } + } + } +} diff --git a/Emby.Drawing.ImageMagick/packages.config b/Emby.Drawing.ImageMagick/packages.config new file mode 100644 index 0000000000..619310d28e --- /dev/null +++ b/Emby.Drawing.ImageMagick/packages.config @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/Emby.Drawing.Net/DynamicImageHelpers.cs b/Emby.Drawing.Net/DynamicImageHelpers.cs new file mode 100644 index 0000000000..1910f7840d --- /dev/null +++ b/Emby.Drawing.Net/DynamicImageHelpers.cs @@ -0,0 +1,110 @@ +using System.Collections.Generic; +using System.Drawing; +using System.Drawing.Drawing2D; +using System.Drawing.Imaging; +using System.IO; +using MediaBrowser.Common.IO; +using MediaBrowser.Controller.IO; +using MediaBrowser.Model.IO; + +namespace Emby.Drawing.Net +{ + public static class DynamicImageHelpers + { + public static void CreateThumbCollage(List files, + IFileSystem fileSystem, + string file, + int width, + int height) + { + const int numStrips = 4; + files = ImageHelpers.ProjectPaths(files, numStrips); + + const int rows = 1; + int cols = numStrips; + + int cellWidth = 2 * (width / 3); + int cellHeight = height; + var index = 0; + + using (var img = new Bitmap(width, height, PixelFormat.Format32bppPArgb)) + { + using (var graphics = Graphics.FromImage(img)) + { + graphics.CompositingQuality = CompositingQuality.HighQuality; + graphics.SmoothingMode = SmoothingMode.HighQuality; + graphics.InterpolationMode = InterpolationMode.HighQualityBicubic; + graphics.PixelOffsetMode = PixelOffsetMode.HighQuality; + + // SourceCopy causes the image to be blank in OSX + //graphics.CompositingMode = CompositingMode.SourceCopy; + + for (var row = 0; row < rows; row++) + { + for (var col = 0; col < cols; col++) + { + var x = col * (cellWidth / 2); + var y = row * cellHeight; + + if (files.Count > index) + { + using (var imgtemp = Image.FromFile(files[index])) + { + graphics.DrawImage(imgtemp, x, y, cellWidth, cellHeight); + } + } + + index++; + } + } + img.Save(file); + } + } + } + + public static void CreateSquareCollage(List files, + IFileSystem fileSystem, + string file, + int width, + int height) + { + files = ImageHelpers.ProjectPaths(files, 4); + + const int rows = 2; + const int cols = 2; + + int singleSize = width / 2; + var index = 0; + + using (var img = new Bitmap(width, height, PixelFormat.Format32bppPArgb)) + { + using (var graphics = Graphics.FromImage(img)) + { + graphics.CompositingQuality = CompositingQuality.HighQuality; + graphics.SmoothingMode = SmoothingMode.HighQuality; + graphics.InterpolationMode = InterpolationMode.HighQualityBicubic; + graphics.PixelOffsetMode = PixelOffsetMode.HighQuality; + + // SourceCopy causes the image to be blank in OSX + //graphics.CompositingMode = CompositingMode.SourceCopy; + + for (var row = 0; row < rows; row++) + { + for (var col = 0; col < cols; col++) + { + var x = col * singleSize; + var y = row * singleSize; + + using (var imgtemp = Image.FromFile(files[index])) + { + graphics.DrawImage(imgtemp, x, y, singleSize, singleSize); + } + index++; + } + } + img.Save(file); + } + } + } + } +} diff --git a/Emby.Drawing.Net/Emby.Drawing.Net.csproj b/Emby.Drawing.Net/Emby.Drawing.Net.csproj new file mode 100644 index 0000000000..16e72a085b --- /dev/null +++ b/Emby.Drawing.Net/Emby.Drawing.Net.csproj @@ -0,0 +1,78 @@ + + + + + Debug + AnyCPU + {C97A239E-A96C-4D64-A844-CCF8CC30AECB} + Library + Properties + Emby.Drawing.Net + Emby.Drawing.Net + v4.5.2 + 512 + + + true + full + false + bin\Debug\ + DEBUG;TRACE + prompt + 4 + + + pdbonly + true + bin\Release\ + TRACE + prompt + 4 + + + + + + + + + + + + + + + + + + + + + + + + + {9142eefa-7570-41e1-bfcc-468bb571af2f} + MediaBrowser.Common + + + {17e1f4e6-8abd-4fe5-9ecf-43d4b6087ba2} + MediaBrowser.Controller + + + {7eeeb4bb-f3e8-48fc-b4c5-70f0fff8329b} + MediaBrowser.Model + + + + + + + + \ No newline at end of file diff --git a/Emby.Drawing.Net/GDIImageEncoder.cs b/Emby.Drawing.Net/GDIImageEncoder.cs new file mode 100644 index 0000000000..831a579792 --- /dev/null +++ b/Emby.Drawing.Net/GDIImageEncoder.cs @@ -0,0 +1,281 @@ +using MediaBrowser.Controller.Drawing; +using MediaBrowser.Model.Drawing; +using MediaBrowser.Model.Logging; +using System; +using System.Drawing; +using System.Drawing.Drawing2D; +using System.Drawing.Imaging; +using System.IO; +using System.Linq; +using MediaBrowser.Common.IO; +using MediaBrowser.Controller.IO; +using MediaBrowser.Model.IO; +using ImageFormat = MediaBrowser.Model.Drawing.ImageFormat; + +namespace Emby.Drawing.Net +{ + public class GDIImageEncoder : IImageEncoder + { + private readonly IFileSystem _fileSystem; + private readonly ILogger _logger; + + public GDIImageEncoder(IFileSystem fileSystem, ILogger logger) + { + _fileSystem = fileSystem; + _logger = logger; + + LogInfo(); + } + + private void LogInfo() + { + _logger.Info("GDIImageEncoder starting"); + using (var stream = GetType().Assembly.GetManifestResourceStream(GetType().Namespace + ".empty.png")) + { + using (var img = Image.FromStream(stream)) + { + + } + } + _logger.Info("GDIImageEncoder started"); + } + + public string[] SupportedInputFormats + { + get + { + return new[] + { + "png", + "jpeg", + "jpg", + "gif", + "bmp" + }; + } + } + + public ImageFormat[] SupportedOutputFormats + { + get + { + return new[] { ImageFormat.Gif, ImageFormat.Jpg, ImageFormat.Png }; + } + } + + public ImageSize GetImageSize(string path) + { + using (var image = Image.FromFile(path)) + { + return new ImageSize + { + Width = image.Width, + Height = image.Height + }; + } + } + + public void CropWhiteSpace(string inputPath, string outputPath) + { + using (var image = (Bitmap)Image.FromFile(inputPath)) + { + using (var croppedImage = image.CropWhitespace()) + { + _fileSystem.CreateDirectory(Path.GetDirectoryName(outputPath)); + + using (var outputStream = _fileSystem.GetFileStream(outputPath, FileOpenMode.Create, FileAccessMode.Write, FileShareMode.Read, false)) + { + croppedImage.Save(System.Drawing.Imaging.ImageFormat.Png, outputStream, 100); + } + } + } + } + + 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)) + { + var newWidth = Convert.ToInt32(width); + var newHeight = Convert.ToInt32(height); + + // Graphics.FromImage will throw an exception if the PixelFormat is Indexed, so we need to handle that here + // Also, Webp only supports Format32bppArgb and Format32bppRgb + var pixelFormat = selectedOutputFormat == ImageFormat.Webp + ? PixelFormat.Format32bppArgb + : PixelFormat.Format32bppPArgb; + + using (var thumbnail = new Bitmap(newWidth, newHeight, pixelFormat)) + { + // Mono throw an exeception if assign 0 to SetResolution + if (originalImage.HorizontalResolution > 0 && originalImage.VerticalResolution > 0) + { + // Preserve the original resolution + thumbnail.SetResolution(originalImage.HorizontalResolution, originalImage.VerticalResolution); + } + + using (var thumbnailGraph = Graphics.FromImage(thumbnail)) + { + thumbnailGraph.CompositingQuality = CompositingQuality.HighQuality; + thumbnailGraph.SmoothingMode = SmoothingMode.HighQuality; + thumbnailGraph.InterpolationMode = InterpolationMode.HighQualityBicubic; + thumbnailGraph.PixelOffsetMode = PixelOffsetMode.HighQuality; + + // SourceCopy causes the image to be blank in OSX + //thumbnailGraph.CompositingMode = !hasPostProcessing ? + // CompositingMode.SourceCopy : + // CompositingMode.SourceOver; + + SetBackgroundColor(thumbnailGraph, options); + + thumbnailGraph.DrawImage(originalImage, 0, 0, newWidth, newHeight); + + DrawIndicator(thumbnailGraph, newWidth, newHeight, options); + + var outputFormat = GetOutputFormat(originalImage, selectedOutputFormat); + + _fileSystem.CreateDirectory(Path.GetDirectoryName(cacheFilePath)); + + // Save to the cache location + using (var cacheFileStream = _fileSystem.GetFileStream(cacheFilePath, FileOpenMode.Create, FileAccessMode.Write, FileShareMode.Read, false)) + { + // Save to the memory stream + thumbnail.Save(outputFormat, cacheFileStream, quality); + } + } + } + + } + } + + /// + /// Sets the color of the background. + /// + /// The graphics. + /// The options. + private void SetBackgroundColor(Graphics graphics, ImageProcessingOptions options) + { + var color = options.BackgroundColor; + + if (!string.IsNullOrEmpty(color)) + { + Color drawingColor; + + try + { + drawingColor = ColorTranslator.FromHtml(color); + } + catch + { + drawingColor = ColorTranslator.FromHtml("#" + color); + } + + graphics.Clear(drawingColor); + } + } + + /// + /// Draws the indicator. + /// + /// The graphics. + /// Width of the image. + /// Height of the image. + /// The options. + private void DrawIndicator(Graphics graphics, int imageWidth, int imageHeight, ImageProcessingOptions options) + { + if (!options.AddPlayedIndicator && !options.UnplayedCount.HasValue && options.PercentPlayed.Equals(0)) + { + return; + } + + try + { + if (options.AddPlayedIndicator) + { + var currentImageSize = new Size(imageWidth, imageHeight); + + new PlayedIndicatorDrawer().DrawPlayedIndicator(graphics, currentImageSize); + } + else if (options.UnplayedCount.HasValue) + { + var currentImageSize = new Size(imageWidth, imageHeight); + + new UnplayedCountIndicator().DrawUnplayedCountIndicator(graphics, currentImageSize, options.UnplayedCount.Value); + } + + if (options.PercentPlayed > 0) + { + var currentImageSize = new Size(imageWidth, imageHeight); + + new PercentPlayedDrawer().Process(graphics, currentImageSize, options.PercentPlayed); + } + } + catch (Exception ex) + { + _logger.ErrorException("Error drawing indicator overlay", ex); + } + } + + /// + /// Gets the output format. + /// + /// The image. + /// The output format. + /// ImageFormat. + private System.Drawing.Imaging.ImageFormat GetOutputFormat(Image image, ImageFormat outputFormat) + { + switch (outputFormat) + { + case ImageFormat.Bmp: + return System.Drawing.Imaging.ImageFormat.Bmp; + case ImageFormat.Gif: + return System.Drawing.Imaging.ImageFormat.Gif; + case ImageFormat.Jpg: + return System.Drawing.Imaging.ImageFormat.Jpeg; + case ImageFormat.Png: + return System.Drawing.Imaging.ImageFormat.Png; + default: + return image.RawFormat; + } + } + + public void CreateImageCollage(ImageCollageOptions options) + { + double ratio = options.Width; + ratio /= options.Height; + + if (ratio >= 1.4) + { + DynamicImageHelpers.CreateThumbCollage(options.InputPaths.ToList(), _fileSystem, options.OutputPath, options.Width, options.Height); + } + else if (ratio >= .9) + { + DynamicImageHelpers.CreateSquareCollage(options.InputPaths.ToList(), _fileSystem, options.OutputPath, options.Width, options.Height); + } + else + { + DynamicImageHelpers.CreateSquareCollage(options.InputPaths.ToList(), _fileSystem, options.OutputPath, options.Width, options.Width); + } + } + + public void Dispose() + { + } + + public string Name + { + get { return "GDI"; } + } + + public bool SupportsImageCollageCreation + { + get { return true; } + } + + public bool SupportsImageEncoding + { + get { return true; } + } + } +} diff --git a/Emby.Drawing.Net/ImageExtensions.cs b/Emby.Drawing.Net/ImageExtensions.cs new file mode 100644 index 0000000000..dec2613d0f --- /dev/null +++ b/Emby.Drawing.Net/ImageExtensions.cs @@ -0,0 +1,217 @@ +using System; +using System.Drawing; +using System.Drawing.Drawing2D; +using System.Drawing.Imaging; +using System.IO; + +namespace Emby.Drawing.Net +{ + public static class ImageExtensions + { + /// + /// Saves the image. + /// + /// The output format. + /// The image. + /// To stream. + /// The quality. + public static void Save(this Image image, ImageFormat outputFormat, Stream toStream, int quality) + { + // Use special save methods for jpeg and png that will result in a much higher quality image + // All other formats use the generic Image.Save + if (ImageFormat.Jpeg.Equals(outputFormat)) + { + SaveAsJpeg(image, toStream, quality); + } + else if (ImageFormat.Png.Equals(outputFormat)) + { + image.Save(toStream, ImageFormat.Png); + } + else + { + image.Save(toStream, outputFormat); + } + } + + /// + /// Saves the JPEG. + /// + /// The image. + /// The target. + /// The quality. + public static void SaveAsJpeg(this Image image, Stream target, int quality) + { + using (var encoderParameters = new EncoderParameters(1)) + { + encoderParameters.Param[0] = new EncoderParameter(Encoder.Quality, quality); + image.Save(target, GetImageCodecInfo("image/jpeg"), encoderParameters); + } + } + + private static readonly ImageCodecInfo[] Encoders = ImageCodecInfo.GetImageEncoders(); + + /// + /// Gets the image codec info. + /// + /// Type of the MIME. + /// ImageCodecInfo. + private static ImageCodecInfo GetImageCodecInfo(string mimeType) + { + foreach (var encoder in Encoders) + { + if (string.Equals(encoder.MimeType, mimeType, StringComparison.OrdinalIgnoreCase)) + { + return encoder; + } + } + + return Encoders.Length == 0 ? null : Encoders[0]; + } + + /// + /// Crops an image by removing whitespace and transparency from the edges + /// + /// The BMP. + /// Bitmap. + /// + public static Bitmap CropWhitespace(this Bitmap bmp) + { + var width = bmp.Width; + var height = bmp.Height; + + var topmost = 0; + for (int row = 0; row < height; ++row) + { + if (IsAllWhiteRow(bmp, row, width)) + topmost = row; + else break; + } + + int bottommost = 0; + for (int row = height - 1; row >= 0; --row) + { + if (IsAllWhiteRow(bmp, row, width)) + bottommost = row; + else break; + } + + int leftmost = 0, rightmost = 0; + for (int col = 0; col < width; ++col) + { + if (IsAllWhiteColumn(bmp, col, height)) + leftmost = col; + else + break; + } + + for (int col = width - 1; col >= 0; --col) + { + if (IsAllWhiteColumn(bmp, col, height)) + rightmost = col; + else + break; + } + + if (rightmost == 0) rightmost = width; // As reached left + if (bottommost == 0) bottommost = height; // As reached top. + + var croppedWidth = rightmost - leftmost; + var croppedHeight = bottommost - topmost; + + if (croppedWidth == 0) // No border on left or right + { + leftmost = 0; + croppedWidth = width; + } + + if (croppedHeight == 0) // No border on top or bottom + { + topmost = 0; + croppedHeight = height; + } + + // Graphics.FromImage will throw an exception if the PixelFormat is Indexed, so we need to handle that here + var thumbnail = new Bitmap(croppedWidth, croppedHeight, PixelFormat.Format32bppPArgb); + + // Preserve the original resolution + TrySetResolution(thumbnail, bmp.HorizontalResolution, bmp.VerticalResolution); + + using (var thumbnailGraph = Graphics.FromImage(thumbnail)) + { + thumbnailGraph.CompositingQuality = CompositingQuality.HighQuality; + thumbnailGraph.SmoothingMode = SmoothingMode.HighQuality; + thumbnailGraph.InterpolationMode = InterpolationMode.HighQualityBicubic; + thumbnailGraph.PixelOffsetMode = PixelOffsetMode.HighQuality; + thumbnailGraph.CompositingMode = CompositingMode.SourceCopy; + + thumbnailGraph.DrawImage(bmp, + new RectangleF(0, 0, croppedWidth, croppedHeight), + new RectangleF(leftmost, topmost, croppedWidth, croppedHeight), + GraphicsUnit.Pixel); + } + return thumbnail; + } + + /// + /// Tries the set resolution. + /// + /// The BMP. + /// The x. + /// The y. + private static void TrySetResolution(Bitmap bmp, float x, float y) + { + if (x > 0 && y > 0) + { + bmp.SetResolution(x, y); + } + } + + /// + /// Determines whether or not a row of pixels is all whitespace + /// + /// The BMP. + /// The row. + /// The width. + /// true if [is all white row] [the specified BMP]; otherwise, false. + private static bool IsAllWhiteRow(Bitmap bmp, int row, int width) + { + for (var i = 0; i < width; ++i) + { + if (!IsWhiteSpace(bmp.GetPixel(i, row))) + { + return false; + } + } + return true; + } + + /// + /// Determines whether or not a column of pixels is all whitespace + /// + /// The BMP. + /// The col. + /// The height. + /// true if [is all white column] [the specified BMP]; otherwise, false. + private static bool IsAllWhiteColumn(Bitmap bmp, int col, int height) + { + for (var i = 0; i < height; ++i) + { + if (!IsWhiteSpace(bmp.GetPixel(col, i))) + { + return false; + } + } + return true; + } + + /// + /// Determines if a color is whitespace + /// + /// The color. + /// true if [is white space] [the specified color]; otherwise, false. + private static bool IsWhiteSpace(Color color) + { + return (color.R == 255 && color.G == 255 && color.B == 255) || color.A == 0; + } + } +} diff --git a/Emby.Drawing.Net/ImageHelpers.cs b/Emby.Drawing.Net/ImageHelpers.cs new file mode 100644 index 0000000000..1afc47cd03 --- /dev/null +++ b/Emby.Drawing.Net/ImageHelpers.cs @@ -0,0 +1,43 @@ +using System; +using System.Collections.Generic; +using System.Linq; + +namespace Emby.Drawing.Net +{ + internal static class ImageHelpers + { + internal static List ProjectPaths(List paths, int count) + { + if (count <= 0) + { + throw new ArgumentOutOfRangeException("count"); + } + if (paths.Count == 0) + { + throw new ArgumentOutOfRangeException("paths"); + } + + var list = new List(); + + AddToList(list, paths, count); + + return list.Take(count).ToList(); + } + + private static void AddToList(List list, List paths, int count) + { + while (list.Count < count) + { + foreach (var path in paths) + { + list.Add(path); + + if (list.Count >= count) + { + return; + } + } + } + } + } +} diff --git a/Emby.Drawing.Net/PercentPlayedDrawer.cs b/Emby.Drawing.Net/PercentPlayedDrawer.cs new file mode 100644 index 0000000000..fac15ba47a --- /dev/null +++ b/Emby.Drawing.Net/PercentPlayedDrawer.cs @@ -0,0 +1,34 @@ +using System; +using System.Drawing; + +namespace Emby.Drawing.Net +{ + public class PercentPlayedDrawer + { + private const int IndicatorHeight = 8; + + public void Process(Graphics graphics, Size imageSize, double percent) + { + var y = imageSize.Height - IndicatorHeight; + + using (var backdroundBrush = new SolidBrush(Color.FromArgb(225, 0, 0, 0))) + { + const int innerX = 0; + var innerY = y; + var innerWidth = imageSize.Width; + var innerHeight = imageSize.Height; + + graphics.FillRectangle(backdroundBrush, innerX, innerY, innerWidth, innerHeight); + + using (var foregroundBrush = new SolidBrush(Color.FromArgb(82, 181, 75))) + { + double foregroundWidth = innerWidth; + foregroundWidth *= percent; + foregroundWidth /= 100; + + graphics.FillRectangle(foregroundBrush, innerX, innerY, Convert.ToInt32(Math.Round(foregroundWidth)), innerHeight); + } + } + } + } +} diff --git a/Emby.Drawing.Net/PlayedIndicatorDrawer.cs b/Emby.Drawing.Net/PlayedIndicatorDrawer.cs new file mode 100644 index 0000000000..53683e6f45 --- /dev/null +++ b/Emby.Drawing.Net/PlayedIndicatorDrawer.cs @@ -0,0 +1,32 @@ +using System.Drawing; + +namespace Emby.Drawing.Net +{ + public class PlayedIndicatorDrawer + { + private const int IndicatorHeight = 40; + public const int IndicatorWidth = 40; + private const int FontSize = 40; + private const int OffsetFromTopRightCorner = 10; + + public void DrawPlayedIndicator(Graphics graphics, Size imageSize) + { + var x = imageSize.Width - IndicatorWidth - OffsetFromTopRightCorner; + + using (var backdroundBrush = new SolidBrush(Color.FromArgb(225, 82, 181, 75))) + { + graphics.FillEllipse(backdroundBrush, x, OffsetFromTopRightCorner, IndicatorWidth, IndicatorHeight); + + x = imageSize.Width - 45 - OffsetFromTopRightCorner; + + using (var font = new Font("Webdings", FontSize, FontStyle.Regular, GraphicsUnit.Pixel)) + { + using (var fontBrush = new SolidBrush(Color.White)) + { + graphics.DrawString("a", font, fontBrush, x, OffsetFromTopRightCorner - 2); + } + } + } + } + } +} diff --git a/Emby.Drawing.Net/Properties/AssemblyInfo.cs b/Emby.Drawing.Net/Properties/AssemblyInfo.cs new file mode 100644 index 0000000000..321c3a297c --- /dev/null +++ b/Emby.Drawing.Net/Properties/AssemblyInfo.cs @@ -0,0 +1,36 @@ +using System.Reflection; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; + +// General Information about an assembly is controlled through the following +// set of attributes. Change these attribute values to modify the information +// associated with an assembly. +[assembly: AssemblyTitle("Emby.Drawing.Net")] +[assembly: AssemblyDescription("")] +[assembly: AssemblyConfiguration("")] +[assembly: AssemblyCompany("")] +[assembly: AssemblyProduct("Emby.Drawing.Net")] +[assembly: AssemblyCopyright("Copyright © 2016")] +[assembly: AssemblyTrademark("")] +[assembly: AssemblyCulture("")] + +// Setting ComVisible to false makes the types in this assembly not visible +// to COM components. If you need to access a type in this assembly from +// COM, set the ComVisible attribute to true on that type. +[assembly: ComVisible(false)] + +// The following GUID is for the ID of the typelib if this project is exposed to COM +[assembly: Guid("c97a239e-a96c-4d64-a844-ccf8cc30aecb")] + +// Version information for an assembly consists of the following four values: +// +// Major Version +// Minor Version +// Build Number +// Revision +// +// You can specify all the values or you can default the Build and Revision Numbers +// by using the '*' as shown below: +// [assembly: AssemblyVersion("1.0.*")] +[assembly: AssemblyVersion("1.0.0.0")] +[assembly: AssemblyFileVersion("1.0.0.0")] diff --git a/Emby.Drawing.Net/UnplayedCountIndicator.cs b/Emby.Drawing.Net/UnplayedCountIndicator.cs new file mode 100644 index 0000000000..a38abeb324 --- /dev/null +++ b/Emby.Drawing.Net/UnplayedCountIndicator.cs @@ -0,0 +1,50 @@ +using System.Drawing; + +namespace Emby.Drawing.Net +{ + public class UnplayedCountIndicator + { + private const int IndicatorHeight = 41; + public const int IndicatorWidth = 41; + private const int OffsetFromTopRightCorner = 10; + + public void DrawUnplayedCountIndicator(Graphics graphics, Size imageSize, int count) + { + var x = imageSize.Width - IndicatorWidth - OffsetFromTopRightCorner; + + using (var backdroundBrush = new SolidBrush(Color.FromArgb(225, 82, 181, 75))) + { + graphics.FillEllipse(backdroundBrush, x, OffsetFromTopRightCorner, IndicatorWidth, IndicatorHeight); + + var text = count.ToString(); + + x = imageSize.Width - IndicatorWidth - OffsetFromTopRightCorner; + var y = OffsetFromTopRightCorner + 6; + var fontSize = 24; + + if (text.Length == 1) + { + x += 10; + } + else if (text.Length == 2) + { + x += 3; + } + else if (text.Length == 3) + { + x += 1; + y += 1; + fontSize = 20; + } + + using (var font = new Font("Sans-Serif", fontSize, FontStyle.Regular, GraphicsUnit.Pixel)) + { + using (var fontBrush = new SolidBrush(Color.White)) + { + graphics.DrawString(text, font, fontBrush, x, y); + } + } + } + } + } +} diff --git a/Emby.Drawing.Net/empty.png b/Emby.Drawing.Net/empty.png new file mode 100644 index 0000000000..42e2b375e5 Binary files /dev/null and b/Emby.Drawing.Net/empty.png differ diff --git a/Emby.Drawing/Common/ImageHeader.cs b/Emby.Drawing/Common/ImageHeader.cs index 45a8f0d474..c385779a1e 100644 --- a/Emby.Drawing/Common/ImageHeader.cs +++ b/Emby.Drawing/Common/ImageHeader.cs @@ -48,7 +48,7 @@ namespace Emby.Drawing.Common /// The image was of an unrecognised format. public static ImageSize GetDimensions(string path, ILogger logger, IFileSystem fileSystem) { - using (var fs = File.OpenRead(path)) + using (var fs = fileSystem.OpenRead(path)) { using (var binaryReader = new BinaryReader(fs)) { diff --git a/Emby.Drawing/Emby.Drawing.csproj b/Emby.Drawing/Emby.Drawing.csproj index a883d06495..90418f6317 100644 --- a/Emby.Drawing/Emby.Drawing.csproj +++ b/Emby.Drawing/Emby.Drawing.csproj @@ -9,10 +9,11 @@ Properties Emby.Drawing Emby.Drawing - v4.6 + {786C830F-07A1-408B-BD7F-6EE04809D6DB};{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC} + Profile7 + v4.5 512 ..\ - true @@ -32,18 +33,6 @@ 4 - - False - ..\packages\ImageMagickSharp.1.0.0.18\lib\net45\ImageMagickSharp.dll - - - - - - - - - ..\ThirdParty\taglib\TagLib.Portable.dll @@ -53,25 +42,9 @@ Properties\SharedVersion.cs - - - - - - - - - - - - - - - - @@ -87,13 +60,8 @@ MediaBrowser.Model - - - - - - - + +