From 9cd2d793be694d680aceae3e045b9cda9af89e69 Mon Sep 17 00:00:00 2001 From: Luke Pulverenti Date: Wed, 17 May 2017 14:18:18 -0400 Subject: [PATCH] update image encoding --- .../ImageMagickEncoder.cs | 18 ++-- Emby.Drawing.Skia/SkiaEncoder.cs | 39 +++++++-- Emby.Drawing/ImageProcessor.cs | 84 ++++++++++++++----- Emby.Drawing/NullImageEncoder.cs | 2 +- .../LiveTv/Listings/SchedulesDirect.cs | 8 +- .../Drawing/IImageEncoder.cs | 2 +- .../Drawing/IImageProcessor.cs | 2 + .../Drawing/ImageHelper.cs | 10 ++- .../MediaEncoding/EncodingHelper.cs | 16 +++- 9 files changed, 139 insertions(+), 42 deletions(-) diff --git a/Emby.Drawing.ImageMagick/ImageMagickEncoder.cs b/Emby.Drawing.ImageMagick/ImageMagickEncoder.cs index 13bde3ca5d..4c911cc7a3 100644 --- a/Emby.Drawing.ImageMagick/ImageMagickEncoder.cs +++ b/Emby.Drawing.ImageMagick/ImageMagickEncoder.cs @@ -130,7 +130,7 @@ namespace Emby.Drawing.ImageMagick string.Equals(ext, ".webp", StringComparison.OrdinalIgnoreCase); } - public void EncodeImage(string inputPath, ImageSize? originalImageSize, string outputPath, bool autoOrient, int quality, ImageProcessingOptions options, ImageFormat selectedOutputFormat) + public string EncodeImage(string inputPath, DateTime dateModified, string outputPath, bool autoOrient, 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); @@ -144,9 +144,13 @@ namespace Emby.Drawing.ImageMagick originalImage.CurrentImage.TrimImage(10); } - if (options.CropWhiteSpace || !originalImageSize.HasValue) + var originalImageSize = new ImageSize(originalImage.CurrentImage.Width, originalImage.CurrentImage.Height); + ImageHelper.SaveImageSize(inputPath, dateModified, originalImageSize); + + if (!options.CropWhiteSpace && options.HasDefaultOptions(inputPath, originalImageSize)) { - originalImageSize = new ImageSize(originalImage.CurrentImage.Width, originalImage.CurrentImage.Height); + // Just spit out the original file if all the options are default + return inputPath; } var newImageSize = ImageHelper.GetNewImageSize(options, originalImageSize); @@ -174,10 +178,8 @@ namespace Emby.Drawing.ImageMagick { using (var originalImage = new MagickWand(inputPath)) { - if (options.CropWhiteSpace || !originalImageSize.HasValue) - { - originalImageSize = new ImageSize(originalImage.CurrentImage.Width, originalImage.CurrentImage.Height); - } + var originalImageSize = new ImageSize(originalImage.CurrentImage.Width, originalImage.CurrentImage.Height); + ImageHelper.SaveImageSize(inputPath, dateModified, originalImageSize); var newImageSize = ImageHelper.GetNewImageSize(options, originalImageSize); @@ -205,6 +207,8 @@ namespace Emby.Drawing.ImageMagick } } } + + return outputPath; } private void AddForegroundLayer(MagickWand wand, ImageProcessingOptions options) diff --git a/Emby.Drawing.Skia/SkiaEncoder.cs b/Emby.Drawing.Skia/SkiaEncoder.cs index 018de5bc96..d742799524 100644 --- a/Emby.Drawing.Skia/SkiaEncoder.cs +++ b/Emby.Drawing.Skia/SkiaEncoder.cs @@ -191,18 +191,18 @@ namespace Emby.Drawing.Skia } private string[] TransparentImageTypes = new string[] { ".png", ".gif", ".webp" }; - private SKBitmap Decode(string path) + private SKBitmap Decode(string path, bool forceCleanBitmap = false) { var requiresTransparencyHack = TransparentImageTypes.Contains(Path.GetExtension(path) ?? string.Empty); - if (requiresTransparencyHack) + if (requiresTransparencyHack || forceCleanBitmap) { using (var stream = new SKFileStream(path)) { var codec = SKCodec.Create(stream); // create the bitmap - var bitmap = new SKBitmap(codec.Info.Width, codec.Info.Height); + var bitmap = new SKBitmap(codec.Info.Width, codec.Info.Height, !requiresTransparencyHack); // decode codec.GetPixels(bitmap.Info, bitmap.GetPixels()); @@ -210,7 +210,18 @@ namespace Emby.Drawing.Skia } } - return SKBitmap.Decode(path); + var resultBitmap = SKBitmap.Decode(path); + + // If we have to resize these they often end up distorted + if (resultBitmap.ColorType == SKColorType.Gray8) + { + using (resultBitmap) + { + return Decode(path, true); + } + } + + return resultBitmap; } private SKBitmap GetBitmap(string path, bool cropWhitespace) @@ -226,7 +237,7 @@ namespace Emby.Drawing.Skia return Decode(path); } - public void EncodeImage(string inputPath, ImageSize? originalImageSize, string outputPath, bool autoOrient, int quality, ImageProcessingOptions options, ImageFormat selectedOutputFormat) + public string EncodeImage(string inputPath, DateTime dateModified, string outputPath, bool autoOrient, int quality, ImageProcessingOptions options, ImageFormat selectedOutputFormat) { if (string.IsNullOrWhiteSpace(inputPath)) { @@ -246,9 +257,20 @@ namespace Emby.Drawing.Skia using (var bitmap = GetBitmap(inputPath, options.CropWhiteSpace)) { - if (options.CropWhiteSpace || !originalImageSize.HasValue) + if (bitmap == null) + { + throw new Exception(string.Format("Skia unable to read image {0}", inputPath)); + } + + //_logger.Info("Color type {0}", bitmap.Info.ColorType); + + var originalImageSize = new ImageSize(bitmap.Width, bitmap.Height); + ImageHelper.SaveImageSize(inputPath, dateModified, originalImageSize); + + if (!options.CropWhiteSpace && options.HasDefaultOptions(inputPath, originalImageSize)) { - originalImageSize = new ImageSize(bitmap.Width, bitmap.Height); + // Just spit out the original file if all the options are default + return inputPath; } var newImageSize = ImageHelper.GetNewImageSize(options, originalImageSize); @@ -269,7 +291,7 @@ namespace Emby.Drawing.Skia using (var outputStream = new SKFileWStream(outputPath)) { resizedBitmap.Encode(outputStream, skiaOutputFormat, quality); - return; + return outputPath; } } @@ -326,6 +348,7 @@ namespace Emby.Drawing.Skia } } } + return outputPath; } public void CreateImageCollage(ImageCollageOptions options) diff --git a/Emby.Drawing/ImageProcessor.cs b/Emby.Drawing/ImageProcessor.cs index 3fa6f64503..89bc96be5f 100644 --- a/Emby.Drawing/ImageProcessor.cs +++ b/Emby.Drawing/ImageProcessor.cs @@ -75,6 +75,7 @@ namespace Emby.Drawing ImageEnhancers = new List(); _saveImageSizeTimer = timerFactory.Create(SaveImageSizeCallback, null, Timeout.Infinite, Timeout.Infinite); + ImageHelper.ImageProcessor = this; Dictionary sizeDictionary; @@ -212,19 +213,12 @@ namespace Emby.Drawing return new Tuple(originalImagePath, MimeTypes.GetMimeType(originalImagePath), dateModified); } - ImageSize? originalImageSize = null; - try - { - originalImageSize = GetImageSize(originalImagePath, dateModified, true); - if (options.HasDefaultOptions(originalImagePath, originalImageSize.Value)) - { - // Just spit out the original file if all the options are default - return new Tuple(originalImagePath, MimeTypes.GetMimeType(originalImagePath), dateModified); - } - } - catch + ImageSize? originalImageSize = GetSavedImageSize(originalImagePath, dateModified); + if (originalImageSize.HasValue && options.HasDefaultOptions(originalImagePath, originalImageSize.Value)) { - originalImageSize = null; + // Just spit out the original file if all the options are default + _logger.Info("Returning original image {0}", originalImagePath); + return new Tuple(originalImagePath, MimeTypes.GetMimeType(originalImagePath), dateModified); } var newSize = ImageHelper.GetNewImageSize(options, originalImageSize); @@ -243,7 +237,13 @@ namespace Emby.Drawing var tmpPath = Path.ChangeExtension(Path.Combine(_appPaths.TempDirectory, Guid.NewGuid().ToString("N")), Path.GetExtension(cacheFilePath)); _fileSystem.CreateDirectory(_fileSystem.GetDirectoryName(tmpPath)); - _imageEncoder.EncodeImage(originalImagePath, originalImageSize, tmpPath, AutoOrient(options.Item), quality, options, outputFormat); + var resultPath =_imageEncoder.EncodeImage(originalImagePath, dateModified, tmpPath, AutoOrient(options.Item), quality, options, outputFormat); + + if (string.Equals(resultPath, originalImagePath, StringComparison.OrdinalIgnoreCase)) + { + return new Tuple(originalImagePath, MimeTypes.GetMimeType(originalImagePath), dateModified); + } + CopyFile(tmpPath, cacheFilePath); return new Tuple(tmpPath, GetMimeType(outputFormat, cacheFilePath), _fileSystem.GetLastWriteTimeUtc(tmpPath)); @@ -422,24 +422,70 @@ namespace Emby.Drawing throw new ArgumentNullException("path"); } - var name = path + "datemodified=" + imageDateModified.Ticks; - ImageSize size; - var cacheHash = name.GetMD5(); + var cacheHash = GetImageSizeKey(path, imageDateModified); if (!_cachedImagedSizes.TryGetValue(cacheHash, out size)) { size = GetImageSizeInternal(path, allowSlowMethod); - if (size.Width > 0 && size.Height > 0) + SaveImageSize(size, cacheHash, false); + } + + return size; + } + + public void SaveImageSize(string path, DateTime imageDateModified, ImageSize size) + { + var cacheHash = GetImageSizeKey(path, imageDateModified); + SaveImageSize(size, cacheHash, true); + } + + private void SaveImageSize(ImageSize size, Guid cacheHash, bool checkExists) + { + if (size.Width <= 0 || size.Height <= 0) + { + return; + } + + if (checkExists && _cachedImagedSizes.ContainsKey(cacheHash)) + { + return; + } + + if (checkExists) + { + if (_cachedImagedSizes.TryAdd(cacheHash, size)) { StartSaveImageSizeTimer(); - _cachedImagedSizes.AddOrUpdate(cacheHash, size, (keyName, oldValue) => size); } } + else + { + StartSaveImageSizeTimer(); + _cachedImagedSizes.AddOrUpdate(cacheHash, size, (keyName, oldValue) => size); + } + } - return size; + private Guid GetImageSizeKey(string path, DateTime imageDateModified) + { + var name = path + "datemodified=" + imageDateModified.Ticks; + return name.GetMD5(); + } + + public ImageSize? GetSavedImageSize(string path, DateTime imageDateModified) + { + ImageSize size; + + var cacheHash = GetImageSizeKey(path, imageDateModified); + + if (_cachedImagedSizes.TryGetValue(cacheHash, out size)) + { + return size; + } + + return null; } /// diff --git a/Emby.Drawing/NullImageEncoder.cs b/Emby.Drawing/NullImageEncoder.cs index 1723e0637b..2241c5a868 100644 --- a/Emby.Drawing/NullImageEncoder.cs +++ b/Emby.Drawing/NullImageEncoder.cs @@ -32,7 +32,7 @@ namespace Emby.Drawing throw new NotImplementedException(); } - public void EncodeImage(string inputPath, ImageSize? originalImageSize, string outputPath, bool autoOrient, int quality, ImageProcessingOptions options, ImageFormat selectedOutputFormat) + public string EncodeImage(string inputPath, DateTime dateModified, string outputPath, bool autoOrient, int quality, ImageProcessingOptions options, ImageFormat selectedOutputFormat) { throw new NotImplementedException(); } diff --git a/Emby.Server.Implementations/LiveTv/Listings/SchedulesDirect.cs b/Emby.Server.Implementations/LiveTv/Listings/SchedulesDirect.cs index a36cb124db..da6759b34d 100644 --- a/Emby.Server.Implementations/LiveTv/Listings/SchedulesDirect.cs +++ b/Emby.Server.Implementations/LiveTv/Listings/SchedulesDirect.cs @@ -167,10 +167,12 @@ namespace Emby.Server.Implementations.LiveTv.Listings { var programEntry = programDict[schedule.programID]; - var data = images[imageIndex].data ?? new List(); - data = data.OrderByDescending(GetSizeOrder).ToList(); + var allImages = (images[imageIndex].data ?? new List()).OrderByDescending(GetSizeOrder).ToList(); + var imagesWithText = allImages.Where(i => string.Equals(i.text, "yes", StringComparison.OrdinalIgnoreCase)).ToList(); + + programEntry.primaryImage = GetProgramImage(ApiUrl, imagesWithText, "Logo", true, 600) ?? + GetProgramImage(ApiUrl, allImages, "Logo", true, 600); - programEntry.primaryImage = GetProgramImage(ApiUrl, data, "Logo", true, 600); //programEntry.thumbImage = GetProgramImage(ApiUrl, data, "Iconic", false); //programEntry.bannerImage = GetProgramImage(ApiUrl, data, "Banner", false) ?? // GetProgramImage(ApiUrl, data, "Banner-L1", false) ?? diff --git a/MediaBrowser.Controller/Drawing/IImageEncoder.cs b/MediaBrowser.Controller/Drawing/IImageEncoder.cs index 64d997dba0..9b895587f3 100644 --- a/MediaBrowser.Controller/Drawing/IImageEncoder.cs +++ b/MediaBrowser.Controller/Drawing/IImageEncoder.cs @@ -19,7 +19,7 @@ namespace MediaBrowser.Controller.Drawing /// /// Encodes the image. /// - void EncodeImage(string inputPath, ImageSize? originalImageSize, string outputPath, bool autoOrient, int quality, ImageProcessingOptions options, ImageFormat outputFormat); + string EncodeImage(string inputPath, DateTime dateModified, string outputPath, bool autoOrient, int quality, ImageProcessingOptions options, ImageFormat outputFormat); /// /// Creates the image collage. diff --git a/MediaBrowser.Controller/Drawing/IImageProcessor.cs b/MediaBrowser.Controller/Drawing/IImageProcessor.cs index a107c12328..7d84719ab8 100644 --- a/MediaBrowser.Controller/Drawing/IImageProcessor.cs +++ b/MediaBrowser.Controller/Drawing/IImageProcessor.cs @@ -114,5 +114,7 @@ namespace MediaBrowser.Controller.Drawing bool SupportsImageCollageCreation { get; } IImageEncoder ImageEncoder { get; set; } + + void SaveImageSize(string path, DateTime imageDateModified, ImageSize size); } } diff --git a/MediaBrowser.Controller/Drawing/ImageHelper.cs b/MediaBrowser.Controller/Drawing/ImageHelper.cs index 30c4e90fb0..54f2ff987a 100644 --- a/MediaBrowser.Controller/Drawing/ImageHelper.cs +++ b/MediaBrowser.Controller/Drawing/ImageHelper.cs @@ -1,4 +1,5 @@ -using MediaBrowser.Controller.Entities; +using System; +using MediaBrowser.Controller.Entities; using MediaBrowser.Model.Drawing; using MediaBrowser.Model.Entities; @@ -18,6 +19,13 @@ namespace MediaBrowser.Controller.Drawing return GetSizeEstimate(options); } + public static IImageProcessor ImageProcessor { get; set; } + + public static void SaveImageSize(string path, DateTime dateModified, ImageSize size) + { + ImageProcessor.SaveImageSize(path, dateModified, size); + } + private static ImageSize GetSizeEstimate(ImageProcessingOptions options) { if (options.Width.HasValue && options.Height.HasValue) diff --git a/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs b/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs index 29d37f99b8..67ba633b7d 100644 --- a/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs +++ b/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs @@ -968,7 +968,7 @@ namespace MediaBrowser.Controller.MediaEncoding { if (bitrate.HasValue && videoStream.BitRate.HasValue) { - bitrate = Math.Min(bitrate.Value, videoStream.BitRate.Value); + bitrate = GetMinBitrate(bitrate.Value, videoStream.BitRate.Value); } } } @@ -981,13 +981,25 @@ namespace MediaBrowser.Controller.MediaEncoding // If a max bitrate was requested, don't let the scaled bitrate exceed it if (request.VideoBitRate.HasValue) { - bitrate = Math.Min(bitrate.Value, request.VideoBitRate.Value); + bitrate = GetMinBitrate(bitrate.Value, request.VideoBitRate.Value); } } return bitrate; } + private int GetMinBitrate(int sourceBitrate, int requestedBitrate) + { + if (sourceBitrate <= 2000000) + { + sourceBitrate *= 2; + } + + var bitrate = Math.Min(sourceBitrate, requestedBitrate); + + return bitrate; + } + public int? GetAudioBitrateParam(BaseEncodingJobOptions request, MediaStream audioStream) { if (request.AudioBitRate.HasValue)