using System.Threading.Tasks; using ImageMagickSharp; using MediaBrowser.Common.Configuration; using MediaBrowser.Common.Net; using MediaBrowser.Controller.Drawing; using MediaBrowser.Model.Drawing; using Microsoft.Extensions.Logging; using System; using System.IO; using MediaBrowser.Model.IO; using MediaBrowser.Model.System; namespace Emby.Drawing.ImageMagick { public class ImageMagickEncoder : IImageEncoder, IDisposable { private readonly ILogger _logger; private readonly IApplicationPaths _appPaths; private readonly Func _httpClientFactory; private readonly IFileSystem _fileSystem; private readonly IEnvironmentInfo _environment; public ImageMagickEncoder(ILogger logger, IApplicationPaths appPaths, Func httpClientFactory, IFileSystem fileSystem, IEnvironmentInfo environment) { _logger = logger; _appPaths = appPaths; _httpClientFactory = httpClientFactory; _fileSystem = fileSystem; _environment = environment; 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", "tif", "jpeg", "jpg", "png", "aiff", "cr2", "crw", "dng", // Remove until supported //"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.LogInformation("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(_fileSystem.GetDirectoryName(tmpPath)); using (var wand = new MagickWand(1, 1, new PixelWand("none", 1))) { wand.SaveImage(tmpPath); } } catch { //_logger.LogError("Error loading webp: ", ex); _webpAvailable = false; } } 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 string EncodeImage(string inputPath, DateTime dateModified, string outputPath, bool autoOrient, ImageOrientation? orientation, 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.IsNullOrEmpty(options.BackgroundColor) || !HasTransparency(inputPath)) { using (var originalImage = new MagickWand(inputPath)) { if (options.CropWhiteSpace) { originalImage.CurrentImage.TrimImage(10); } var originalImageSize = new ImageSize(originalImage.CurrentImage.Width, originalImage.CurrentImage.Height); if (!options.CropWhiteSpace && options.HasDefaultOptions(inputPath, originalImageSize) && !autoOrient) { // Just spit out the original file if all the options are default return inputPath; } var newImageSize = ImageHelper.GetNewImageSize(options, originalImageSize); var width = Convert.ToInt32(Math.Round(newImageSize.Width)); var height = Convert.ToInt32(Math.Round(newImageSize.Height)); ScaleImage(originalImage, width, height, options.Blur ?? 0); if (autoOrient) { AutoOrientImage(originalImage); } AddForegroundLayer(originalImage, options); DrawIndicator(originalImage, width, height, options); originalImage.CurrentImage.CompressionQuality = quality; originalImage.CurrentImage.StripImage(); _fileSystem.CreateDirectory(_fileSystem.GetDirectoryName(outputPath)); originalImage.SaveImage(outputPath); } } else { using (var originalImage = new MagickWand(inputPath)) { var originalImageSize = new ImageSize(originalImage.CurrentImage.Width, originalImage.CurrentImage.Height); var newImageSize = ImageHelper.GetNewImageSize(options, originalImageSize); var width = Convert.ToInt32(Math.Round(newImageSize.Width)); var height = Convert.ToInt32(Math.Round(newImageSize.Height)); using (var wand = new MagickWand(width, height, options.BackgroundColor)) { ScaleImage(originalImage, width, height, options.Blur ?? 0); 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); } } } return outputPath; } private void AddForegroundLayer(MagickWand wand, ImageProcessingOptions options) { if (string.IsNullOrEmpty(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, int blur) { var useResize = blur > 1; if (useResize) { wand.CurrentImage.ResizeImage(width, height, FilterTypes.GaussianFilter, blur); } 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, _httpClientFactory(), _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.LogError("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, 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); } } 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; } } } }