From d58da2a7728580f79203cfa502269c31c463775d Mon Sep 17 00:00:00 2001 From: Luke Pulverenti Date: Wed, 18 Sep 2013 14:49:06 -0400 Subject: moved image manager to an interface --- .../DefaultTheme/DefaultThemeService.cs | 33 +- MediaBrowser.Api/Images/ImageService.cs | 49 +- MediaBrowser.Api/Images/ImageWriter.cs | 6 +- .../Playback/Progressive/AudioService.cs | 13 +- .../Progressive/BaseProgressiveStreamingService.cs | 7 +- .../Playback/Progressive/VideoService.cs | 13 +- MediaBrowser.Api/SearchService.cs | 7 +- MediaBrowser.Controller/Drawing/IImageProcessor.cs | 94 +++ MediaBrowser.Controller/Drawing/ImageHeader.cs | 227 ------ MediaBrowser.Controller/Drawing/ImageManager.cs | 805 --------------------- MediaBrowser.Controller/Entities/BaseItem.cs | 66 +- MediaBrowser.Controller/Entities/Movies/Movie.cs | 2 +- MediaBrowser.Controller/Kernel.cs | 9 +- .../MediaBrowser.Controller.csproj | 3 +- .../Drawing/ImageHeader.cs | 227 ++++++ .../Drawing/ImageProcessor.cs | 752 +++++++++++++++++++ .../Dto/DtoService.cs | 32 +- .../MediaBrowser.Server.Implementations.csproj | 3 + MediaBrowser.ServerApplication/ApplicationHost.cs | 12 +- 19 files changed, 1216 insertions(+), 1144 deletions(-) create mode 100644 MediaBrowser.Controller/Drawing/IImageProcessor.cs delete mode 100644 MediaBrowser.Controller/Drawing/ImageHeader.cs delete mode 100644 MediaBrowser.Controller/Drawing/ImageManager.cs create mode 100644 MediaBrowser.Server.Implementations/Drawing/ImageHeader.cs create mode 100644 MediaBrowser.Server.Implementations/Drawing/ImageProcessor.cs diff --git a/MediaBrowser.Api/DefaultTheme/DefaultThemeService.cs b/MediaBrowser.Api/DefaultTheme/DefaultThemeService.cs index ebb583506c..e682921765 100644 --- a/MediaBrowser.Api/DefaultTheme/DefaultThemeService.cs +++ b/MediaBrowser.Api/DefaultTheme/DefaultThemeService.cs @@ -1,4 +1,4 @@ -using MediaBrowser.Controller; +using MediaBrowser.Controller.Drawing; using MediaBrowser.Controller.Dto; using MediaBrowser.Controller.Entities; using MediaBrowser.Controller.Entities.Audio; @@ -13,7 +13,6 @@ using ServiceStack.ServiceHost; using System; using System.Collections.Generic; using System.Linq; -using System.Threading.Tasks; namespace MediaBrowser.Api.DefaultTheme { @@ -68,24 +67,26 @@ namespace MediaBrowser.Api.DefaultTheme private readonly ILibraryManager _libraryManager; private readonly ILocalizationManager _localization; + private readonly IImageProcessor _imageProcessor; - public DefaultThemeService(IUserManager userManager, IDtoService dtoService, ILogger logger, ILibraryManager libraryManager, ILocalizationManager localization) + public DefaultThemeService(IUserManager userManager, IDtoService dtoService, ILogger logger, ILibraryManager libraryManager, ILocalizationManager localization, IImageProcessor imageProcessor) { _userManager = userManager; _dtoService = dtoService; _logger = logger; _libraryManager = libraryManager; _localization = localization; + _imageProcessor = imageProcessor; } public object Get(GetHomeView request) { - var result = GetHomeView(request).Result; + var result = GetHomeView(request); return ToOptimizedResult(result); } - private async Task GetHomeView(GetHomeView request) + private HomeView GetHomeView(GetHomeView request) { var user = _userManager.GetUserById(request.UserId); @@ -113,12 +114,12 @@ namespace MediaBrowser.Api.DefaultTheme public object Get(GetGamesView request) { - var result = GetGamesView(request).Result; + var result = GetGamesView(request); return ToOptimizedResult(result); } - private async Task GetGamesView(GetGamesView request) + private GamesView GetGamesView(GetGamesView request) { var user = _userManager.GetUserById(request.UserId); @@ -145,19 +146,19 @@ namespace MediaBrowser.Api.DefaultTheme public object Get(GetMovieView request) { - var result = GetMovieView(request).Result; + var result = GetMovieView(request); return ToOptimizedResult(result); } public object Get(GetTvView request) { - var result = GetTvView(request).Result; + var result = GetTvView(request); return ToOptimizedResult(result); } - private async Task GetTvView(GetTvView request) + private TvView GetTvView(GetTvView request) { var user = _userManager.GetUserById(request.UserId); @@ -210,7 +211,7 @@ namespace MediaBrowser.Api.DefaultTheme return view; } - private async Task GetMovieView(GetMovieView request) + private MoviesView GetMovieView(GetMovieView request) { var user = _userManager.GetUserById(request.UserId); @@ -348,9 +349,9 @@ namespace MediaBrowser.Api.DefaultTheme { try { - var date = Kernel.Instance.ImageManager.GetImageDateModified(item, path); + var date = item.GetImageDateModified(path); - var size = Kernel.Instance.ImageManager.GetImageSize(path, date); + var size = _imageProcessor.GetImageSize(path, date); return size.Width; } @@ -400,13 +401,11 @@ namespace MediaBrowser.Api.DefaultTheme ImageType = imageType }; - var imageManager = Kernel.Instance.ImageManager; - try { - var imagePath = imageManager.GetImagePath(item, imageType, 0); + var imagePath = item.GetImagePath(imageType, 0); - stub.ImageTag = imageManager.GetImageCacheTag(item, imageType, imagePath); + stub.ImageTag = _imageProcessor.GetImageCacheTag(item, imageType, imagePath); } catch (Exception ex) { diff --git a/MediaBrowser.Api/Images/ImageService.cs b/MediaBrowser.Api/Images/ImageService.cs index fb5cb291ef..b8c6fc8f0b 100644 --- a/MediaBrowser.Api/Images/ImageService.cs +++ b/MediaBrowser.Api/Images/ImageService.cs @@ -2,6 +2,7 @@ using MediaBrowser.Common.Extensions; using MediaBrowser.Common.Net; using MediaBrowser.Controller; +using MediaBrowser.Controller.Drawing; using MediaBrowser.Controller.Dto; using MediaBrowser.Controller.Entities; using MediaBrowser.Controller.Library; @@ -338,11 +339,12 @@ namespace MediaBrowser.Api.Images private readonly IItemRepository _itemRepo; private readonly IDtoService _dtoService; + private readonly IImageProcessor _imageProcessor; /// /// Initializes a new instance of the class. /// - public ImageService(IUserManager userManager, ILibraryManager libraryManager, IApplicationPaths appPaths, IProviderManager providerManager, IItemRepository itemRepo, IDtoService dtoService) + public ImageService(IUserManager userManager, ILibraryManager libraryManager, IApplicationPaths appPaths, IProviderManager providerManager, IItemRepository itemRepo, IDtoService dtoService, IImageProcessor imageProcessor) { _userManager = userManager; _libraryManager = libraryManager; @@ -350,6 +352,7 @@ namespace MediaBrowser.Api.Images _providerManager = providerManager; _itemRepo = itemRepo; _dtoService = dtoService; + _imageProcessor = imageProcessor; } /// @@ -403,15 +406,13 @@ namespace MediaBrowser.Api.Images var fileInfo = new FileInfo(path); - var dateModified = Kernel.Instance.ImageManager.GetImageDateModified(item, path); - - var size = Kernel.Instance.ImageManager.GetImageSize(path, dateModified); + var size = _imageProcessor.GetImageSize(path); list.Add(new ImageInfo { Path = path, ImageType = image.Key, - ImageTag = Kernel.Instance.ImageManager.GetImageCacheTag(item, image.Key, path), + ImageTag = _imageProcessor.GetImageCacheTag(item, image.Key, path), Size = fileInfo.Length, Width = Convert.ToInt32(size.Width), Height = Convert.ToInt32(size.Height) @@ -424,16 +425,14 @@ namespace MediaBrowser.Api.Images { var fileInfo = new FileInfo(image); - var dateModified = Kernel.Instance.ImageManager.GetImageDateModified(item, image); - - var size = Kernel.Instance.ImageManager.GetImageSize(image, dateModified); + var size = _imageProcessor.GetImageSize(image); list.Add(new ImageInfo { Path = image, ImageIndex = index, ImageType = ImageType.Backdrop, - ImageTag = Kernel.Instance.ImageManager.GetImageCacheTag(item, ImageType.Backdrop, image), + ImageTag = _imageProcessor.GetImageCacheTag(item, ImageType.Backdrop, image), Size = fileInfo.Length, Width = Convert.ToInt32(size.Width), Height = Convert.ToInt32(size.Height) @@ -448,16 +447,14 @@ namespace MediaBrowser.Api.Images { var fileInfo = new FileInfo(image); - var dateModified = Kernel.Instance.ImageManager.GetImageDateModified(item, image); - - var size = Kernel.Instance.ImageManager.GetImageSize(image, dateModified); + var size = _imageProcessor.GetImageSize(image); list.Add(new ImageInfo { Path = image, ImageIndex = index, ImageType = ImageType.Screenshot, - ImageTag = Kernel.Instance.ImageManager.GetImageCacheTag(item, ImageType.Screenshot, image), + ImageTag = _imageProcessor.GetImageCacheTag(item, ImageType.Screenshot, image), Size = fileInfo.Length, Width = Convert.ToInt32(size.Width), Height = Convert.ToInt32(size.Height) @@ -480,16 +477,14 @@ namespace MediaBrowser.Api.Images var fileInfo = new FileInfo(image); - var dateModified = Kernel.Instance.ImageManager.GetImageDateModified(item, image); - - var size = Kernel.Instance.ImageManager.GetImageSize(image, dateModified); + var size = _imageProcessor.GetImageSize(image); list.Add(new ImageInfo { Path = image, ImageIndex = index, ImageType = ImageType.Chapter, - ImageTag = Kernel.Instance.ImageManager.GetImageCacheTag(item, ImageType.Chapter, image), + ImageTag = _imageProcessor.GetImageCacheTag(item, ImageType.Chapter, image), Size = fileInfo.Length, Width = Convert.ToInt32(size.Width), Height = Convert.ToInt32(size.Height) @@ -721,11 +716,7 @@ namespace MediaBrowser.Api.Images /// private object GetImage(ImageRequest request, BaseItem item) { - var kernel = Kernel.Instance; - - var index = request.Index ?? 0; - - var imagePath = GetImagePath(kernel, request, item); + var imagePath = GetImagePath(request, item); if (string.IsNullOrEmpty(imagePath)) { @@ -733,9 +724,9 @@ namespace MediaBrowser.Api.Images } // See if we can avoid a file system lookup by looking for the file in ResolveArgs - var originalFileImageDateModified = kernel.ImageManager.GetImageDateModified(item, imagePath); + var originalFileImageDateModified = item.GetImageDateModified(imagePath); - var supportedImageEnhancers = request.EnableImageEnhancers ? kernel.ImageManager.ImageEnhancers.Where(i => + var supportedImageEnhancers = request.EnableImageEnhancers ? _imageProcessor.ImageEnhancers.Where(i => { try { @@ -759,7 +750,7 @@ namespace MediaBrowser.Api.Images var contentType = MimeTypes.GetMimeType(imagePath); - var cacheGuid = kernel.ImageManager.GetImageCacheTag(imagePath, originalFileImageDateModified, supportedImageEnhancers, item, request.Type); + var cacheGuid = _imageProcessor.GetImageCacheTag(item, request.Type, imagePath, originalFileImageDateModified, supportedImageEnhancers); TimeSpan? cacheDuration = null; @@ -778,7 +769,8 @@ namespace MediaBrowser.Api.Images Request = currentRequest, OriginalImageDateModified = originalFileImageDateModified, Enhancers = supportedImageEnhancers, - OriginalImagePath = imagePath + OriginalImagePath = imagePath, + ImageProcessor = _imageProcessor }, contentType); } @@ -786,15 +778,14 @@ namespace MediaBrowser.Api.Images /// /// Gets the image path. /// - /// The kernel. /// The request. /// The item. /// System.String. - private string GetImagePath(Kernel kernel, ImageRequest request, BaseItem item) + private string GetImagePath(ImageRequest request, BaseItem item) { var index = request.Index ?? 0; - return kernel.ImageManager.GetImagePath(item, request.Type, index); + return item.GetImagePath(request.Type, index); } /// diff --git a/MediaBrowser.Api/Images/ImageWriter.cs b/MediaBrowser.Api/Images/ImageWriter.cs index a734e28e80..da2c9c043a 100644 --- a/MediaBrowser.Api/Images/ImageWriter.cs +++ b/MediaBrowser.Api/Images/ImageWriter.cs @@ -1,4 +1,4 @@ -using MediaBrowser.Controller; +using MediaBrowser.Controller.Drawing; using MediaBrowser.Controller.Entities; using MediaBrowser.Controller.Providers; using MediaBrowser.Model.Entities; @@ -35,6 +35,8 @@ namespace MediaBrowser.Api.Images public string OriginalImagePath; + public IImageProcessor ImageProcessor { get; set; } + /// /// The _options /// @@ -73,7 +75,7 @@ namespace MediaBrowser.Api.Images cropwhitespace = Request.CropWhitespace.Value; } - return Kernel.Instance.ImageManager.ProcessImage(Item, Request.Type, Request.Index ?? 0, OriginalImagePath, cropwhitespace, + return ImageProcessor.ProcessImage(Item, Request.Type, Request.Index ?? 0, OriginalImagePath, cropwhitespace, OriginalImageDateModified, responseStream, Request.Width, Request.Height, Request.MaxWidth, Request.MaxHeight, Request.Quality, Enhancers); } diff --git a/MediaBrowser.Api/Playback/Progressive/AudioService.cs b/MediaBrowser.Api/Playback/Progressive/AudioService.cs index d935fea79b..915ec0c106 100644 --- a/MediaBrowser.Api/Playback/Progressive/AudioService.cs +++ b/MediaBrowser.Api/Playback/Progressive/AudioService.cs @@ -1,5 +1,6 @@ using MediaBrowser.Common.MediaInfo; using MediaBrowser.Controller; +using MediaBrowser.Controller.Drawing; using MediaBrowser.Controller.Dto; using MediaBrowser.Controller.Library; using MediaBrowser.Controller.Persistence; @@ -41,16 +42,8 @@ namespace MediaBrowser.Api.Playback.Progressive /// public class AudioService : BaseProgressiveStreamingService { - /// - /// Initializes a new instance of the class. - /// - /// The app paths. - /// The user manager. - /// The library manager. - /// The iso manager. - /// The media encoder. - public AudioService(IServerApplicationPaths appPaths, IUserManager userManager, ILibraryManager libraryManager, IIsoManager isoManager, IMediaEncoder mediaEncoder, IItemRepository itemRepo, IDtoService dtoService) - : base(appPaths, userManager, libraryManager, isoManager, mediaEncoder, itemRepo, dtoService) + public AudioService(IServerApplicationPaths appPaths, IUserManager userManager, ILibraryManager libraryManager, IIsoManager isoManager, IMediaEncoder mediaEncoder, IItemRepository itemRepo, IDtoService dtoService, IImageProcessor imageProcessor) + : base(appPaths, userManager, libraryManager, isoManager, mediaEncoder, itemRepo, dtoService, imageProcessor) { } diff --git a/MediaBrowser.Api/Playback/Progressive/BaseProgressiveStreamingService.cs b/MediaBrowser.Api/Playback/Progressive/BaseProgressiveStreamingService.cs index 806e55024f..0bc147a46c 100644 --- a/MediaBrowser.Api/Playback/Progressive/BaseProgressiveStreamingService.cs +++ b/MediaBrowser.Api/Playback/Progressive/BaseProgressiveStreamingService.cs @@ -2,6 +2,7 @@ using MediaBrowser.Common.MediaInfo; using MediaBrowser.Common.Net; using MediaBrowser.Controller; +using MediaBrowser.Controller.Drawing; using MediaBrowser.Controller.Dto; using MediaBrowser.Controller.Entities; using MediaBrowser.Controller.Entities.Audio; @@ -24,11 +25,13 @@ namespace MediaBrowser.Api.Playback.Progressive public abstract class BaseProgressiveStreamingService : BaseStreamingService { protected readonly IItemRepository ItemRepository; + protected readonly IImageProcessor ImageProcessor; - protected BaseProgressiveStreamingService(IServerApplicationPaths appPaths, IUserManager userManager, ILibraryManager libraryManager, IIsoManager isoManager, IMediaEncoder mediaEncoder, IItemRepository itemRepository, IDtoService dtoService) : + protected BaseProgressiveStreamingService(IServerApplicationPaths appPaths, IUserManager userManager, ILibraryManager libraryManager, IIsoManager isoManager, IMediaEncoder mediaEncoder, IItemRepository itemRepository, IDtoService dtoService, IImageProcessor imageProcessor) : base(appPaths, userManager, libraryManager, isoManager, mediaEncoder, dtoService) { ItemRepository = itemRepository; + ImageProcessor = imageProcessor; } /// @@ -303,7 +306,7 @@ namespace MediaBrowser.Api.Playback.Progressive } } - return new ImageService(UserManager, LibraryManager, ApplicationPaths, null, ItemRepository, DtoService) + return new ImageService(UserManager, LibraryManager, ApplicationPaths, null, ItemRepository, DtoService, ImageProcessor) { Logger = Logger, RequestContext = RequestContext, diff --git a/MediaBrowser.Api/Playback/Progressive/VideoService.cs b/MediaBrowser.Api/Playback/Progressive/VideoService.cs index eb53bc2cee..c1dd7fa017 100644 --- a/MediaBrowser.Api/Playback/Progressive/VideoService.cs +++ b/MediaBrowser.Api/Playback/Progressive/VideoService.cs @@ -1,5 +1,6 @@ using MediaBrowser.Common.MediaInfo; using MediaBrowser.Controller; +using MediaBrowser.Controller.Drawing; using MediaBrowser.Controller.Dto; using MediaBrowser.Controller.Entities; using MediaBrowser.Controller.Library; @@ -53,16 +54,8 @@ namespace MediaBrowser.Api.Playback.Progressive /// public class VideoService : BaseProgressiveStreamingService { - /// - /// Initializes a new instance of the class. - /// - /// The app paths. - /// The user manager. - /// The library manager. - /// The iso manager. - /// The media encoder. - public VideoService(IServerApplicationPaths appPaths, IUserManager userManager, ILibraryManager libraryManager, IIsoManager isoManager, IMediaEncoder mediaEncoder, IItemRepository itemRepo, IDtoService dtoService) - : base(appPaths, userManager, libraryManager, isoManager, mediaEncoder, itemRepo, dtoService) + public VideoService(IServerApplicationPaths appPaths, IUserManager userManager, ILibraryManager libraryManager, IIsoManager isoManager, IMediaEncoder mediaEncoder, IItemRepository itemRepo, IDtoService dtoService, IImageProcessor imageProcessor) + : base(appPaths, userManager, libraryManager, isoManager, mediaEncoder, itemRepo, dtoService, imageProcessor) { } diff --git a/MediaBrowser.Api/SearchService.cs b/MediaBrowser.Api/SearchService.cs index 2b3f117617..78c299fbb2 100644 --- a/MediaBrowser.Api/SearchService.cs +++ b/MediaBrowser.Api/SearchService.cs @@ -1,4 +1,5 @@ using MediaBrowser.Controller; +using MediaBrowser.Controller.Drawing; using MediaBrowser.Controller.Dto; using MediaBrowser.Controller.Entities; using MediaBrowser.Controller.Entities.Audio; @@ -66,6 +67,7 @@ namespace MediaBrowser.Api private readonly ILibrarySearchEngine _searchEngine; private readonly ILibraryManager _libraryManager; private readonly IDtoService _dtoService; + private readonly IImageProcessor _imageProcessor; /// /// Initializes a new instance of the class. @@ -73,12 +75,13 @@ namespace MediaBrowser.Api /// The user manager. /// The search engine. /// The library manager. - public SearchService(IUserManager userManager, ILibrarySearchEngine searchEngine, ILibraryManager libraryManager, IDtoService dtoService) + public SearchService(IUserManager userManager, ILibrarySearchEngine searchEngine, ILibraryManager libraryManager, IDtoService dtoService, IImageProcessor imageProcessor) { _userManager = userManager; _searchEngine = searchEngine; _libraryManager = libraryManager; _dtoService = dtoService; + _imageProcessor = imageProcessor; } /// @@ -161,7 +164,7 @@ namespace MediaBrowser.Api if (item.HasImage(ImageType.Primary)) { - result.PrimaryImageTag = Kernel.Instance.ImageManager.GetImageCacheTag(item, ImageType.Primary, item.GetImage(ImageType.Primary)); + result.PrimaryImageTag = _imageProcessor.GetImageCacheTag(item, ImageType.Primary, item.GetImage(ImageType.Primary)); } var episode = item as Episode; diff --git a/MediaBrowser.Controller/Drawing/IImageProcessor.cs b/MediaBrowser.Controller/Drawing/IImageProcessor.cs new file mode 100644 index 0000000000..55c279b0cc --- /dev/null +++ b/MediaBrowser.Controller/Drawing/IImageProcessor.cs @@ -0,0 +1,94 @@ +using MediaBrowser.Controller.Entities; +using MediaBrowser.Controller.Providers; +using MediaBrowser.Model.Drawing; +using MediaBrowser.Model.Entities; +using System; +using System.Collections.Generic; +using System.IO; +using System.Threading.Tasks; + +namespace MediaBrowser.Controller.Drawing +{ + /// + /// Interface IImageProcessor + /// + public interface IImageProcessor + { + /// + /// Gets the image enhancers. + /// + /// The image enhancers. + IEnumerable ImageEnhancers { get; } + + /// + /// Gets the size of the image. + /// + /// The path. + /// ImageSize. + ImageSize GetImageSize(string path); + + /// + /// Gets the size of the image. + /// + /// The path. + /// The image date modified. + /// ImageSize. + ImageSize GetImageSize(string path, DateTime imageDateModified); + + /// + /// Adds the parts. + /// + /// The enhancers. + void AddParts(IEnumerable enhancers); + + /// + /// Gets the supported enhancers. + /// + /// The item. + /// Type of the image. + /// IEnumerable{IImageEnhancer}. + IEnumerable GetSupportedEnhancers(BaseItem item, ImageType imageType); + + /// + /// Gets the image cache tag. + /// + /// The item. + /// Type of the image. + /// The image path. + /// Guid. + Guid GetImageCacheTag(BaseItem item, ImageType imageType, string imagePath); + + /// + /// Gets the image cache tag. + /// + /// The item. + /// Type of the image. + /// The original image path. + /// The date modified. + /// The image enhancers. + /// Guid. + Guid GetImageCacheTag(BaseItem item, ImageType imageType, string originalImagePath, DateTime dateModified, + IEnumerable imageEnhancers); + + /// + /// Processes the image. + /// + /// The entity. + /// Type of the image. + /// Index of the image. + /// The original image path. + /// if set to true [crop whitespace]. + /// The date modified. + /// To stream. + /// The width. + /// The height. + /// Width of the max. + /// Height of the max. + /// The quality. + /// The enhancers. + /// Task. + Task ProcessImage(BaseItem entity, ImageType imageType, int imageIndex, string originalImagePath, bool cropWhitespace, + DateTime dateModified, Stream toStream, int? width, int? height, int? maxWidth, int? maxHeight, + int? quality, List enhancers); + } +} diff --git a/MediaBrowser.Controller/Drawing/ImageHeader.cs b/MediaBrowser.Controller/Drawing/ImageHeader.cs deleted file mode 100644 index 95a753f004..0000000000 --- a/MediaBrowser.Controller/Drawing/ImageHeader.cs +++ /dev/null @@ -1,227 +0,0 @@ -using MediaBrowser.Model.Logging; -using System; -using System.Collections.Generic; -using System.Drawing; -using System.IO; -using System.Linq; - -namespace MediaBrowser.Controller.Drawing -{ - /// - /// Taken from http://stackoverflow.com/questions/111345/getting-image-dimensions-without-reading-the-entire-file/111349 - /// http://www.codeproject.com/Articles/35978/Reading-Image-Headers-to-Get-Width-and-Height - /// Minor improvements including supporting unsigned 16-bit integers when decoding Jfif and added logic - /// to load the image using new Bitmap if reading the headers fails - /// - public static class ImageHeader - { - /// - /// The error message - /// - const string ErrorMessage = "Could not recognize image format."; - - /// - /// The image format decoders - /// - private static readonly KeyValuePair>[] ImageFormatDecoders = new Dictionary> - { - { new byte[] { 0x42, 0x4D }, DecodeBitmap }, - { new byte[] { 0x47, 0x49, 0x46, 0x38, 0x37, 0x61 }, DecodeGif }, - { new byte[] { 0x47, 0x49, 0x46, 0x38, 0x39, 0x61 }, DecodeGif }, - { new byte[] { 0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A }, DecodePng }, - { new byte[] { 0xff, 0xd8 }, DecodeJfif } - - }.ToArray(); - - private static readonly int MaxMagicBytesLength = ImageFormatDecoders.Select(i => i.Key.Length).OrderByDescending(i => i).First(); - - /// - /// Gets the dimensions of an image. - /// - /// The path of the image to get the dimensions of. - /// The logger. - /// The dimensions of the specified image. - /// The image was of an unrecognised format. - public static Size GetDimensions(string path, ILogger logger) - { - try - { - using (var fs = File.OpenRead(path)) - { - using (var binaryReader = new BinaryReader(fs)) - { - return GetDimensions(binaryReader); - } - } - } - catch - { - logger.Info("Failed to read image header for {0}. Doing it the slow way.", path); - } - - // Buffer to memory stream to avoid image locking file - using (var fs = new FileStream(path, FileMode.Open, FileAccess.Read, FileShare.Read)) - { - using (var memoryStream = new MemoryStream()) - { - fs.CopyTo(memoryStream); - - // Co it the old fashioned way - using (var b = Image.FromStream(memoryStream, true, false)) - { - return b.Size; - } - } - } - } - - /// - /// Gets the dimensions of an image. - /// - /// The binary reader. - /// Size. - /// binaryReader - /// The image was of an unrecognized format. - private static Size GetDimensions(BinaryReader binaryReader) - { - var magicBytes = new byte[MaxMagicBytesLength]; - - for (var i = 0; i < MaxMagicBytesLength; i += 1) - { - magicBytes[i] = binaryReader.ReadByte(); - - foreach (var kvPair in ImageFormatDecoders) - { - if (StartsWith(magicBytes, kvPair.Key)) - { - return kvPair.Value(binaryReader); - } - } - } - - throw new ArgumentException(ErrorMessage, "binaryReader"); - } - - /// - /// Startses the with. - /// - /// The this bytes. - /// The that bytes. - /// true if XXXX, false otherwise - private static bool StartsWith(byte[] thisBytes, byte[] thatBytes) - { - for (int i = 0; i < thatBytes.Length; i += 1) - { - if (thisBytes[i] != thatBytes[i]) - { - return false; - } - } - - return true; - } - - /// - /// Reads the little endian int16. - /// - /// The binary reader. - /// System.Int16. - private static short ReadLittleEndianInt16(BinaryReader binaryReader) - { - var bytes = new byte[sizeof(short)]; - - for (int i = 0; i < sizeof(short); i += 1) - { - bytes[sizeof(short) - 1 - i] = binaryReader.ReadByte(); - } - return BitConverter.ToInt16(bytes, 0); - } - - /// - /// Reads the little endian int32. - /// - /// The binary reader. - /// System.Int32. - private static int ReadLittleEndianInt32(BinaryReader binaryReader) - { - var bytes = new byte[sizeof(int)]; - for (int i = 0; i < sizeof(int); i += 1) - { - bytes[sizeof(int) - 1 - i] = binaryReader.ReadByte(); - } - return BitConverter.ToInt32(bytes, 0); - } - - /// - /// Decodes the bitmap. - /// - /// The binary reader. - /// Size. - private static Size DecodeBitmap(BinaryReader binaryReader) - { - binaryReader.ReadBytes(16); - int width = binaryReader.ReadInt32(); - int height = binaryReader.ReadInt32(); - return new Size(width, height); - } - - /// - /// Decodes the GIF. - /// - /// The binary reader. - /// Size. - private static Size DecodeGif(BinaryReader binaryReader) - { - int width = binaryReader.ReadInt16(); - int height = binaryReader.ReadInt16(); - return new Size(width, height); - } - - /// - /// Decodes the PNG. - /// - /// The binary reader. - /// Size. - private static Size DecodePng(BinaryReader binaryReader) - { - binaryReader.ReadBytes(8); - int width = ReadLittleEndianInt32(binaryReader); - int height = ReadLittleEndianInt32(binaryReader); - return new Size(width, height); - } - - /// - /// Decodes the jfif. - /// - /// The binary reader. - /// Size. - /// - private static Size DecodeJfif(BinaryReader binaryReader) - { - while (binaryReader.ReadByte() == 0xff) - { - byte marker = binaryReader.ReadByte(); - short chunkLength = ReadLittleEndianInt16(binaryReader); - if (marker == 0xc0) - { - binaryReader.ReadByte(); - int height = ReadLittleEndianInt16(binaryReader); - int width = ReadLittleEndianInt16(binaryReader); - return new Size(width, height); - } - - if (chunkLength < 0) - { - var uchunkLength = (ushort)chunkLength; - binaryReader.ReadBytes(uchunkLength - 2); - } - else - { - binaryReader.ReadBytes(chunkLength - 2); - } - } - - throw new ArgumentException(ErrorMessage); - } - } -} diff --git a/MediaBrowser.Controller/Drawing/ImageManager.cs b/MediaBrowser.Controller/Drawing/ImageManager.cs deleted file mode 100644 index 05f45a4574..0000000000 --- a/MediaBrowser.Controller/Drawing/ImageManager.cs +++ /dev/null @@ -1,805 +0,0 @@ -using MediaBrowser.Common.Extensions; -using MediaBrowser.Common.IO; -using MediaBrowser.Controller.Entities; -using MediaBrowser.Controller.Entities.TV; -using MediaBrowser.Controller.Persistence; -using MediaBrowser.Controller.Providers; -using MediaBrowser.Model.Drawing; -using MediaBrowser.Model.Entities; -using MediaBrowser.Model.Logging; -using System; -using System.Collections.Concurrent; -using System.Collections.Generic; -using System.Drawing; -using System.Drawing.Drawing2D; -using System.Drawing.Imaging; -using System.Globalization; -using System.IO; -using System.Linq; -using System.Threading; -using System.Threading.Tasks; - -namespace MediaBrowser.Controller.Drawing -{ - /// - /// Class ImageManager - /// - public class ImageManager - { - /// - /// Gets the list of currently registered image processors - /// Image processors are specialized metadata providers that run after the normal ones - /// - /// The image enhancers. - public IEnumerable ImageEnhancers { get; set; } - - /// - /// Gets the image size cache. - /// - /// The image size cache. - private FileSystemRepository ImageSizeCache { get; set; } - - /// - /// Gets or sets the resized image cache. - /// - /// The resized image cache. - private FileSystemRepository ResizedImageCache { get; set; } - /// - /// Gets the cropped image cache. - /// - /// The cropped image cache. - private FileSystemRepository CroppedImageCache { get; set; } - - /// - /// Gets the cropped image cache. - /// - /// The cropped image cache. - private FileSystemRepository EnhancedImageCache { get; set; } - - /// - /// The cached imaged sizes - /// - private readonly ConcurrentDictionary _cachedImagedSizes = new ConcurrentDictionary(); - - /// - /// The _logger - /// - private readonly ILogger _logger; - - private readonly IItemRepository _itemRepo; - - /// - /// Initializes a new instance of the class. - /// - /// The logger. - /// The app paths. - /// The item repo. - public ImageManager(ILogger logger, IServerApplicationPaths appPaths, IItemRepository itemRepo) - { - _logger = logger; - _itemRepo = itemRepo; - - ImageSizeCache = new FileSystemRepository(Path.Combine(appPaths.ImageCachePath, "image-sizes")); - ResizedImageCache = new FileSystemRepository(Path.Combine(appPaths.ImageCachePath, "resized-images")); - CroppedImageCache = new FileSystemRepository(Path.Combine(appPaths.ImageCachePath, "cropped-images")); - EnhancedImageCache = new FileSystemRepository(Path.Combine(appPaths.ImageCachePath, "enhanced-images")); - } - - /// - /// Processes an image by resizing to target dimensions - /// - /// The entity that owns the image - /// The image type - /// The image index (currently only used with backdrops) - /// The original image path. - /// if set to true [crop whitespace]. - /// The last date modified of the original image file - /// The stream to save the new image to - /// Use if a fixed width is required. Aspect ratio will be preserved. - /// Use if a fixed height is required. Aspect ratio will be preserved. - /// Use if a max width is required. Aspect ratio will be preserved. - /// Use if a max height is required. Aspect ratio will be preserved. - /// Quality level, from 0-100. Currently only applies to JPG. The default value should suffice. - /// The enhancers. - /// Task. - /// entity - public async Task ProcessImage(BaseItem entity, ImageType imageType, int imageIndex, string originalImagePath, bool cropWhitespace, DateTime dateModified, Stream toStream, int? width, int? height, int? maxWidth, int? maxHeight, int? quality, List enhancers) - { - if (entity == null) - { - throw new ArgumentNullException("entity"); - } - - if (toStream == null) - { - throw new ArgumentNullException("toStream"); - } - - if (cropWhitespace) - { - originalImagePath = await GetCroppedImage(originalImagePath, dateModified).ConfigureAwait(false); - } - - // No enhancement - don't cache - if (enhancers.Count > 0) - { - try - { - // Enhance if we have enhancers - var ehnancedImagePath = await GetEnhancedImage(originalImagePath, dateModified, entity, imageType, imageIndex, enhancers).ConfigureAwait(false); - - // If the path changed update dateModified - if (!ehnancedImagePath.Equals(originalImagePath, StringComparison.OrdinalIgnoreCase)) - { - dateModified = File.GetLastWriteTimeUtc(ehnancedImagePath); - originalImagePath = ehnancedImagePath; - } - } - catch (Exception ex) - { - _logger.Error("Error enhancing image", ex); - } - } - - var originalImageSize = GetImageSize(originalImagePath, dateModified); - - // Determine the output size based on incoming parameters - var newSize = DrawingUtils.Resize(originalImageSize, width, height, maxWidth, maxHeight); - - if (!quality.HasValue) - { - quality = 90; - } - - var cacheFilePath = GetCacheFilePath(originalImagePath, newSize, quality.Value, dateModified); - - try - { - using (var fileStream = new FileStream(cacheFilePath, FileMode.Open, FileAccess.Read, FileShare.Read, StreamDefaults.DefaultFileStreamBufferSize, FileOptions.Asynchronous)) - { - await fileStream.CopyToAsync(toStream).ConfigureAwait(false); - return; - } - } - catch (IOException) - { - // Cache file doesn't exist or is currently being written ro - } - - var semaphore = GetLock(cacheFilePath); - - await semaphore.WaitAsync().ConfigureAwait(false); - - // Check again in case of lock contention - if (File.Exists(cacheFilePath)) - { - try - { - using (var fileStream = new FileStream(cacheFilePath, FileMode.Open, FileAccess.Read, FileShare.Read, StreamDefaults.DefaultFileStreamBufferSize, FileOptions.Asynchronous)) - { - await fileStream.CopyToAsync(toStream).ConfigureAwait(false); - return; - } - } - finally - { - semaphore.Release(); - } - } - - try - { - using (var fileStream = new FileStream(originalImagePath, FileMode.Open, FileAccess.Read, FileShare.Read, StreamDefaults.DefaultFileStreamBufferSize, true)) - { - // Copy to memory stream to avoid Image locking file - using (var memoryStream = new MemoryStream()) - { - await fileStream.CopyToAsync(memoryStream).ConfigureAwait(false); - - using (var originalImage = Image.FromStream(memoryStream, true, false)) - { - var newWidth = Convert.ToInt32(newSize.Width); - var newHeight = Convert.ToInt32(newSize.Height); - - // Graphics.FromImage will throw an exception if the PixelFormat is Indexed, so we need to handle that here - using (var thumbnail = !ImageExtensions.IsPixelFormatSupportedByGraphicsObject(originalImage.PixelFormat) ? new Bitmap(originalImage, newWidth, newHeight) : new Bitmap(newWidth, newHeight, originalImage.PixelFormat)) - { - // 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; - thumbnailGraph.CompositingMode = CompositingMode.SourceOver; - - thumbnailGraph.DrawImage(originalImage, 0, 0, newWidth, newHeight); - - var outputFormat = originalImage.RawFormat; - - using (var outputMemoryStream = new MemoryStream()) - { - // Save to the memory stream - thumbnail.Save(outputFormat, outputMemoryStream, quality.Value); - - var bytes = outputMemoryStream.ToArray(); - - var outputTask = toStream.WriteAsync(bytes, 0, bytes.Length); - - // kick off a task to cache the result - var cacheTask = CacheResizedImage(cacheFilePath, bytes); - - await Task.WhenAll(outputTask, cacheTask).ConfigureAwait(false); - } - } - } - - } - } - } - } - finally - { - semaphore.Release(); - } - } - - /// - /// Caches the resized image. - /// - /// The cache file path. - /// The bytes. - private async Task CacheResizedImage(string cacheFilePath, byte[] bytes) - { - var parentPath = Path.GetDirectoryName(cacheFilePath); - - if (!Directory.Exists(parentPath)) - { - Directory.CreateDirectory(parentPath); - } - - // Save to the cache location - using (var cacheFileStream = new FileStream(cacheFilePath, FileMode.Create, FileAccess.Write, FileShare.Read, StreamDefaults.DefaultFileStreamBufferSize, FileOptions.Asynchronous)) - { - // Save to the filestream - await cacheFileStream.WriteAsync(bytes, 0, bytes.Length).ConfigureAwait(false); - } - } - - /// - /// Gets the cache file path based on a set of parameters - /// - /// The path to the original image file - /// The size to output the image in - /// Quality level, from 0-100. Currently only applies to JPG. The default value should suffice. - /// The last modified date of the image - /// System.String. - private string GetCacheFilePath(string originalPath, ImageSize outputSize, int quality, DateTime dateModified) - { - var filename = originalPath; - - filename += "width=" + outputSize.Width; - - filename += "height=" + outputSize.Height; - - filename += "quality=" + quality; - - filename += "datemodified=" + dateModified.Ticks; - - return ResizedImageCache.GetResourcePath(filename, Path.GetExtension(originalPath)); - } - - - /// - /// Gets image dimensions - /// - /// The image path. - /// The date modified. - /// Task{ImageSize}. - /// imagePath - public ImageSize GetImageSize(string imagePath, DateTime dateModified) - { - if (string.IsNullOrEmpty(imagePath)) - { - throw new ArgumentNullException("imagePath"); - } - - var name = imagePath + "datemodified=" + dateModified.Ticks; - - ImageSize size; - - if (!_cachedImagedSizes.TryGetValue(name, out size)) - { - size = GetImageSize(name, imagePath); - - _cachedImagedSizes.AddOrUpdate(name, size, (keyName, oldValue) => size); - } - - return size; - } - - protected readonly CultureInfo UsCulture = new CultureInfo("en-US"); - - /// - /// Gets the size of the image. - /// - /// Name of the key. - /// The image path. - /// ImageSize. - private ImageSize GetImageSize(string keyName, string imagePath) - { - // Now check the file system cache - var fullCachePath = ImageSizeCache.GetResourcePath(keyName, ".txt"); - - try - { - var result = File.ReadAllText(fullCachePath).Split('|').Select(i => double.Parse(i, UsCulture)).ToArray(); - - return new ImageSize { Width = result[0], Height = result[1] }; - } - catch (IOException) - { - // Cache file doesn't exist or is currently being written to - } - - var syncLock = GetObjectLock(fullCachePath); - - lock (syncLock) - { - try - { - var result = File.ReadAllText(fullCachePath) - .Split('|') - .Select(i => double.Parse(i, UsCulture)) - .ToArray(); - - return new ImageSize { Width = result[0], Height = result[1] }; - } - catch (FileNotFoundException) - { - // Cache file doesn't exist no biggie - } - catch (DirectoryNotFoundException) - { - // Cache file doesn't exist no biggie - } - - var size = ImageHeader.GetDimensions(imagePath, _logger); - - var parentPath = Path.GetDirectoryName(fullCachePath); - - if (!Directory.Exists(parentPath)) - { - Directory.CreateDirectory(parentPath); - } - - // Update the file system cache - File.WriteAllText(fullCachePath, size.Width.ToString(UsCulture) + @"|" + size.Height.ToString(UsCulture)); - - return new ImageSize { Width = size.Width, Height = size.Height }; - } - } - - /// - /// Gets the image path. - /// - /// The item. - /// Type of the image. - /// Index of the image. - /// System.String. - /// item - /// - public string GetImagePath(BaseItem item, ImageType imageType, int imageIndex) - { - if (item == null) - { - throw new ArgumentNullException("item"); - } - - if (imageType == ImageType.Backdrop) - { - if (item.BackdropImagePaths == null) - { - throw new InvalidOperationException(string.Format("Item {0} does not have any Backdrops.", item.Name)); - } - - return item.BackdropImagePaths[imageIndex]; - } - - if (imageType == ImageType.Screenshot) - { - if (item.ScreenshotImagePaths == null) - { - throw new InvalidOperationException(string.Format("Item {0} does not have any Screenshots.", item.Name)); - } - - return item.ScreenshotImagePaths[imageIndex]; - } - - if (imageType == ImageType.Chapter) - { - return _itemRepo.GetChapter(item.Id, imageIndex).ImagePath; - } - - return item.GetImage(imageType); - } - - /// - /// Gets the image date modified. - /// - /// The item. - /// Type of the image. - /// Index of the image. - /// DateTime. - /// item - public DateTime GetImageDateModified(BaseItem item, ImageType imageType, int imageIndex) - { - if (item == null) - { - throw new ArgumentNullException("item"); - } - - var imagePath = GetImagePath(item, imageType, imageIndex); - - return GetImageDateModified(item, imagePath); - } - - /// - /// Gets the image date modified. - /// - /// The item. - /// The image path. - /// DateTime. - /// item - public DateTime GetImageDateModified(BaseItem item, string imagePath) - { - if (item == null) - { - throw new ArgumentNullException("item"); - } - - if (string.IsNullOrEmpty(imagePath)) - { - throw new ArgumentNullException("imagePath"); - } - - var metaFileEntry = item.ResolveArgs.GetMetaFileByPath(imagePath); - - // If we didn't the metafile entry, check the Season - if (metaFileEntry == null) - { - var episode = item as Episode; - - if (episode != null && episode.Season != null) - { - episode.Season.ResolveArgs.GetMetaFileByPath(imagePath); - } - } - - // See if we can avoid a file system lookup by looking for the file in ResolveArgs - return metaFileEntry == null ? File.GetLastWriteTimeUtc(imagePath) : metaFileEntry.LastWriteTimeUtc; - } - - /// - /// Crops whitespace from an image, caches the result, and returns the cached path - /// - /// The original image path. - /// The date modified. - /// System.String. - private async Task GetCroppedImage(string originalImagePath, DateTime dateModified) - { - var name = originalImagePath; - name += "datemodified=" + dateModified.Ticks; - - var croppedImagePath = CroppedImageCache.GetResourcePath(name, Path.GetExtension(originalImagePath)); - - var semaphore = GetLock(croppedImagePath); - - await semaphore.WaitAsync().ConfigureAwait(false); - - // Check again in case of contention - if (File.Exists(croppedImagePath)) - { - semaphore.Release(); - return croppedImagePath; - } - - try - { - using (var fileStream = new FileStream(originalImagePath, FileMode.Open, FileAccess.Read, FileShare.Read, StreamDefaults.DefaultFileStreamBufferSize, true)) - { - // Copy to memory stream to avoid Image locking file - using (var memoryStream = new MemoryStream()) - { - await fileStream.CopyToAsync(memoryStream).ConfigureAwait(false); - - using (var originalImage = (Bitmap)Image.FromStream(memoryStream, true, false)) - { - var outputFormat = originalImage.RawFormat; - - using (var croppedImage = originalImage.CropWhitespace()) - { - var parentPath = Path.GetDirectoryName(croppedImagePath); - - if (!Directory.Exists(parentPath)) - { - Directory.CreateDirectory(parentPath); - } - - using (var outputStream = new FileStream(croppedImagePath, FileMode.Create, FileAccess.Write, FileShare.Read)) - { - croppedImage.Save(outputFormat, outputStream, 100); - } - } - } - } - } - } - 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 originalImagePath; - } - finally - { - semaphore.Release(); - } - - return croppedImagePath; - } - - /// - /// Gets the enhanced image. - /// - /// The original image path. - /// The date modified. - /// The item. - /// Type of the image. - /// Index of the image. - /// Task{System.String}. - /// item - public Task GetEnhancedImage(string originalImagePath, DateTime dateModified, BaseItem item, ImageType imageType, int imageIndex) - { - if (item == null) - { - throw new ArgumentNullException("item"); - } - - var supportedImageEnhancers = ImageEnhancers.Where(i => - { - try - { - return i.Supports(item, imageType); - } - catch (Exception ex) - { - _logger.ErrorException("Error in image enhancer: {0}", ex, i.GetType().Name); - - return false; - } - - }).ToList(); - - return GetEnhancedImage(originalImagePath, dateModified, item, imageType, imageIndex, supportedImageEnhancers); - } - - /// - /// Runs an image through the image enhancers, caches the result, and returns the cached path - /// - /// The original image path. - /// The date modified of the original image file. - /// The item. - /// Type of the image. - /// Index of the image. - /// The supported enhancers. - /// System.String. - /// originalImagePath - public async Task GetEnhancedImage(string originalImagePath, DateTime dateModified, BaseItem item, ImageType imageType, int imageIndex, List supportedEnhancers) - { - if (string.IsNullOrEmpty(originalImagePath)) - { - throw new ArgumentNullException("originalImagePath"); - } - - if (item == null) - { - throw new ArgumentNullException("item"); - } - - var cacheGuid = GetImageCacheTag(originalImagePath, dateModified, supportedEnhancers, item, imageType); - - // All enhanced images are saved as png to allow transparency - var enhancedImagePath = EnhancedImageCache.GetResourcePath(cacheGuid + ".png"); - - var semaphore = GetLock(enhancedImagePath); - - await semaphore.WaitAsync().ConfigureAwait(false); - - // Check again in case of contention - if (File.Exists(enhancedImagePath)) - { - semaphore.Release(); - return enhancedImagePath; - } - - try - { - using (var fileStream = new FileStream(originalImagePath, FileMode.Open, FileAccess.Read, FileShare.Read, StreamDefaults.DefaultFileStreamBufferSize, true)) - { - // Copy to memory stream to avoid Image locking file - using (var memoryStream = new MemoryStream()) - { - await fileStream.CopyToAsync(memoryStream).ConfigureAwait(false); - - using (var originalImage = Image.FromStream(memoryStream, true, false)) - { - //Pass the image through registered enhancers - using (var newImage = await ExecuteImageEnhancers(supportedEnhancers, originalImage, item, imageType, imageIndex).ConfigureAwait(false)) - { - var parentDirectory = Path.GetDirectoryName(enhancedImagePath); - - if (!Directory.Exists(parentDirectory)) - { - Directory.CreateDirectory(parentDirectory); - } - - //And then save it in the cache - using (var outputStream = new FileStream(enhancedImagePath, FileMode.Create, FileAccess.Write, FileShare.Read)) - { - newImage.Save(ImageFormat.Png, outputStream, 100); - } - } - } - } - } - } - finally - { - semaphore.Release(); - } - - return enhancedImagePath; - } - - /// - /// Gets the image cache tag. - /// - /// The item. - /// Type of the image. - /// The image path. - /// Guid. - /// item - public Guid GetImageCacheTag(BaseItem item, ImageType imageType, string imagePath) - { - if (item == null) - { - throw new ArgumentNullException("item"); - } - - if (string.IsNullOrEmpty(imagePath)) - { - throw new ArgumentNullException("imagePath"); - } - - var dateModified = GetImageDateModified(item, imagePath); - - var supportedEnhancers = ImageEnhancers.Where(i => - { - try - { - return i.Supports(item, imageType); - } - catch (Exception ex) - { - _logger.ErrorException("Error in image enhancer: {0}", ex, i.GetType().Name); - - return false; - } - - }).ToList(); - - return GetImageCacheTag(imagePath, dateModified, supportedEnhancers, item, imageType); - } - - /// - /// Gets the image cache tag. - /// - /// The original image path. - /// The date modified of the original image file. - /// The image enhancers. - /// The item. - /// Type of the image. - /// Guid. - /// item - public Guid GetImageCacheTag(string originalImagePath, DateTime dateModified, IEnumerable imageEnhancers, BaseItem item, ImageType imageType) - { - if (item == null) - { - throw new ArgumentNullException("item"); - } - - if (imageEnhancers == null) - { - throw new ArgumentNullException("imageEnhancers"); - } - - if (string.IsNullOrEmpty(originalImagePath)) - { - throw new ArgumentNullException("originalImagePath"); - } - - // Cache name is created with supported enhancers combined with the last config change so we pick up new config changes - var cacheKeys = imageEnhancers.Select(i => i.GetConfigurationCacheKey(item, imageType)).ToList(); - cacheKeys.Add(originalImagePath + dateModified.Ticks); - - return string.Join("|", cacheKeys.ToArray()).GetMD5(); - } - - /// - /// Executes the image enhancers. - /// - /// The image enhancers. - /// The original image. - /// The item. - /// Type of the image. - /// Index of the image. - /// Task{EnhancedImage}. - private async Task ExecuteImageEnhancers(IEnumerable imageEnhancers, Image originalImage, BaseItem item, ImageType imageType, int imageIndex) - { - var result = originalImage; - - // Run the enhancers sequentially in order of priority - foreach (var enhancer in imageEnhancers) - { - var typeName = enhancer.GetType().Name; - - try - { - result = await enhancer.EnhanceImageAsync(item, result, imageType, imageIndex).ConfigureAwait(false); - } - catch (Exception ex) - { - _logger.ErrorException("{0} failed enhancing {1}", ex, typeName, item.Name); - - throw; - } - } - - return result; - } - - /// - /// The _semaphoreLocks - /// - private readonly ConcurrentDictionary _semaphoreLocks = new ConcurrentDictionary(); - - /// - /// Gets the lock. - /// - /// The filename. - /// System.Object. - private SemaphoreSlim GetLock(string filename) - { - return _semaphoreLocks.GetOrAdd(filename, key => new SemaphoreSlim(1, 1)); - } - - /// - /// The _semaphoreLocks - /// - private readonly ConcurrentDictionary _locks = new ConcurrentDictionary(); - - /// - /// Gets the lock. - /// - /// The filename. - /// System.Object. - private object GetObjectLock(string filename) - { - return _locks.GetOrAdd(filename, key => new object()); - } - } -} diff --git a/MediaBrowser.Controller/Entities/BaseItem.cs b/MediaBrowser.Controller/Entities/BaseItem.cs index f81485867b..db3e546d1e 100644 --- a/MediaBrowser.Controller/Entities/BaseItem.cs +++ b/MediaBrowser.Controller/Entities/BaseItem.cs @@ -1,5 +1,6 @@ using MediaBrowser.Common.Extensions; using MediaBrowser.Controller.Configuration; +using MediaBrowser.Controller.Entities.TV; using MediaBrowser.Controller.IO; using MediaBrowser.Controller.Library; using MediaBrowser.Controller.Localization; @@ -936,7 +937,7 @@ namespace MediaBrowser.Controller.Entities var itemsChanged = !LocalTrailerIds.SequenceEqual(newItemIds); - var tasks = newItems.Select(i => i.RefreshMetadata(cancellationToken, forceSave, forceRefresh, allowSlowProviders)); + var tasks = newItems.Select(i => i.RefreshMetadata(cancellationToken, forceSave, forceRefresh, allowSlowProviders, resetResolveArgs: false)); var results = await Task.WhenAll(tasks).ConfigureAwait(false); @@ -952,7 +953,7 @@ namespace MediaBrowser.Controller.Entities var themeVideosChanged = !ThemeVideoIds.SequenceEqual(newThemeVideoIds); - var tasks = newThemeVideos.Select(i => i.RefreshMetadata(cancellationToken, forceSave, forceRefresh, allowSlowProviders)); + var tasks = newThemeVideos.Select(i => i.RefreshMetadata(cancellationToken, forceSave, forceRefresh, allowSlowProviders, resetResolveArgs: false)); var results = await Task.WhenAll(tasks).ConfigureAwait(false); @@ -971,7 +972,7 @@ namespace MediaBrowser.Controller.Entities var themeSongsChanged = !ThemeSongIds.SequenceEqual(newThemeSongIds); - var tasks = newThemeSongs.Select(i => i.RefreshMetadata(cancellationToken, forceSave, forceRefresh, allowSlowProviders)); + var tasks = newThemeSongs.Select(i => i.RefreshMetadata(cancellationToken, forceSave, forceRefresh, allowSlowProviders, resetResolveArgs: false)); var results = await Task.WhenAll(tasks).ConfigureAwait(false); @@ -1562,5 +1563,64 @@ namespace MediaBrowser.Controller.Entities ScreenshotImagePaths.Remove(path); } } + + /// + /// Gets the image path. + /// + /// Type of the image. + /// Index of the image. + /// System.String. + /// + /// + /// item + public string GetImagePath(ImageType imageType, int imageIndex) + { + if (imageType == ImageType.Backdrop) + { + return BackdropImagePaths[imageIndex]; + } + + if (imageType == ImageType.Screenshot) + { + return ScreenshotImagePaths[imageIndex]; + } + + if (imageType == ImageType.Chapter) + { + return ItemRepository.GetChapter(Id, imageIndex).ImagePath; + } + + return GetImage(imageType); + } + + /// + /// Gets the image date modified. + /// + /// The image path. + /// DateTime. + /// item + public DateTime GetImageDateModified(string imagePath) + { + if (string.IsNullOrEmpty(imagePath)) + { + throw new ArgumentNullException("imagePath"); + } + + var metaFileEntry = ResolveArgs.GetMetaFileByPath(imagePath); + + // If we didn't the metafile entry, check the Season + if (metaFileEntry == null) + { + var episode = this as Episode; + + if (episode != null && episode.Season != null) + { + episode.Season.ResolveArgs.GetMetaFileByPath(imagePath); + } + } + + // See if we can avoid a file system lookup by looking for the file in ResolveArgs + return metaFileEntry == null ? File.GetLastWriteTimeUtc(imagePath) : metaFileEntry.LastWriteTimeUtc; + } } } diff --git a/MediaBrowser.Controller/Entities/Movies/Movie.cs b/MediaBrowser.Controller/Entities/Movies/Movie.cs index 60e1699363..152767860d 100644 --- a/MediaBrowser.Controller/Entities/Movies/Movie.cs +++ b/MediaBrowser.Controller/Entities/Movies/Movie.cs @@ -62,7 +62,7 @@ namespace MediaBrowser.Controller.Entities.Movies var itemsChanged = !SpecialFeatureIds.SequenceEqual(newItemIds); - var tasks = newItems.Select(i => i.RefreshMetadata(cancellationToken, forceSave, forceRefresh, allowSlowProviders)); + var tasks = newItems.Select(i => i.RefreshMetadata(cancellationToken, forceSave, forceRefresh, allowSlowProviders, resetResolveArgs: false)); var results = await Task.WhenAll(tasks).ConfigureAwait(false); diff --git a/MediaBrowser.Controller/Kernel.cs b/MediaBrowser.Controller/Kernel.cs index 5bf485229b..37a1648c17 100644 --- a/MediaBrowser.Controller/Kernel.cs +++ b/MediaBrowser.Controller/Kernel.cs @@ -1,5 +1,4 @@ -using MediaBrowser.Controller.Drawing; -using MediaBrowser.Controller.MediaInfo; +using MediaBrowser.Controller.MediaInfo; namespace MediaBrowser.Controller { @@ -14,12 +13,6 @@ namespace MediaBrowser.Controller /// The instance. public static Kernel Instance { get; private set; } - /// - /// Gets the image manager. - /// - /// The image manager. - public ImageManager ImageManager { get; set; } - /// /// Gets the FFMPEG controller. /// diff --git a/MediaBrowser.Controller/MediaBrowser.Controller.csproj b/MediaBrowser.Controller/MediaBrowser.Controller.csproj index 0e97a9f2a4..960f597ac3 100644 --- a/MediaBrowser.Controller/MediaBrowser.Controller.csproj +++ b/MediaBrowser.Controller/MediaBrowser.Controller.csproj @@ -71,6 +71,7 @@ Properties\SharedVersion.cs + @@ -95,8 +96,6 @@ - - diff --git a/MediaBrowser.Server.Implementations/Drawing/ImageHeader.cs b/MediaBrowser.Server.Implementations/Drawing/ImageHeader.cs new file mode 100644 index 0000000000..4da836cc68 --- /dev/null +++ b/MediaBrowser.Server.Implementations/Drawing/ImageHeader.cs @@ -0,0 +1,227 @@ +using MediaBrowser.Model.Logging; +using System; +using System.Collections.Generic; +using System.Drawing; +using System.IO; +using System.Linq; + +namespace MediaBrowser.Server.Implementations.Drawing +{ + /// + /// Taken from http://stackoverflow.com/questions/111345/getting-image-dimensions-without-reading-the-entire-file/111349 + /// http://www.codeproject.com/Articles/35978/Reading-Image-Headers-to-Get-Width-and-Height + /// Minor improvements including supporting unsigned 16-bit integers when decoding Jfif and added logic + /// to load the image using new Bitmap if reading the headers fails + /// + public static class ImageHeader + { + /// + /// The error message + /// + const string ErrorMessage = "Could not recognize image format."; + + /// + /// The image format decoders + /// + private static readonly KeyValuePair>[] ImageFormatDecoders = new Dictionary> + { + { new byte[] { 0x42, 0x4D }, DecodeBitmap }, + { new byte[] { 0x47, 0x49, 0x46, 0x38, 0x37, 0x61 }, DecodeGif }, + { new byte[] { 0x47, 0x49, 0x46, 0x38, 0x39, 0x61 }, DecodeGif }, + { new byte[] { 0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A }, DecodePng }, + { new byte[] { 0xff, 0xd8 }, DecodeJfif } + + }.ToArray(); + + private static readonly int MaxMagicBytesLength = ImageFormatDecoders.Select(i => i.Key.Length).OrderByDescending(i => i).First(); + + /// + /// Gets the dimensions of an image. + /// + /// The path of the image to get the dimensions of. + /// The logger. + /// The dimensions of the specified image. + /// The image was of an unrecognised format. + public static Size GetDimensions(string path, ILogger logger) + { + try + { + using (var fs = File.OpenRead(path)) + { + using (var binaryReader = new BinaryReader(fs)) + { + return GetDimensions(binaryReader); + } + } + } + catch + { + logger.Info("Failed to read image header for {0}. Doing it the slow way.", path); + } + + // Buffer to memory stream to avoid image locking file + using (var fs = new FileStream(path, FileMode.Open, FileAccess.Read, FileShare.Read)) + { + using (var memoryStream = new MemoryStream()) + { + fs.CopyTo(memoryStream); + + // Co it the old fashioned way + using (var b = Image.FromStream(memoryStream, true, false)) + { + return b.Size; + } + } + } + } + + /// + /// Gets the dimensions of an image. + /// + /// The binary reader. + /// Size. + /// binaryReader + /// The image was of an unrecognized format. + private static Size GetDimensions(BinaryReader binaryReader) + { + var magicBytes = new byte[MaxMagicBytesLength]; + + for (var i = 0; i < MaxMagicBytesLength; i += 1) + { + magicBytes[i] = binaryReader.ReadByte(); + + foreach (var kvPair in ImageFormatDecoders) + { + if (StartsWith(magicBytes, kvPair.Key)) + { + return kvPair.Value(binaryReader); + } + } + } + + throw new ArgumentException(ErrorMessage, "binaryReader"); + } + + /// + /// Startses the with. + /// + /// The this bytes. + /// The that bytes. + /// true if XXXX, false otherwise + private static bool StartsWith(byte[] thisBytes, byte[] thatBytes) + { + for (int i = 0; i < thatBytes.Length; i += 1) + { + if (thisBytes[i] != thatBytes[i]) + { + return false; + } + } + + return true; + } + + /// + /// Reads the little endian int16. + /// + /// The binary reader. + /// System.Int16. + private static short ReadLittleEndianInt16(BinaryReader binaryReader) + { + var bytes = new byte[sizeof(short)]; + + for (int i = 0; i < sizeof(short); i += 1) + { + bytes[sizeof(short) - 1 - i] = binaryReader.ReadByte(); + } + return BitConverter.ToInt16(bytes, 0); + } + + /// + /// Reads the little endian int32. + /// + /// The binary reader. + /// System.Int32. + private static int ReadLittleEndianInt32(BinaryReader binaryReader) + { + var bytes = new byte[sizeof(int)]; + for (int i = 0; i < sizeof(int); i += 1) + { + bytes[sizeof(int) - 1 - i] = binaryReader.ReadByte(); + } + return BitConverter.ToInt32(bytes, 0); + } + + /// + /// Decodes the bitmap. + /// + /// The binary reader. + /// Size. + private static Size DecodeBitmap(BinaryReader binaryReader) + { + binaryReader.ReadBytes(16); + int width = binaryReader.ReadInt32(); + int height = binaryReader.ReadInt32(); + return new Size(width, height); + } + + /// + /// Decodes the GIF. + /// + /// The binary reader. + /// Size. + private static Size DecodeGif(BinaryReader binaryReader) + { + int width = binaryReader.ReadInt16(); + int height = binaryReader.ReadInt16(); + return new Size(width, height); + } + + /// + /// Decodes the PNG. + /// + /// The binary reader. + /// Size. + private static Size DecodePng(BinaryReader binaryReader) + { + binaryReader.ReadBytes(8); + int width = ReadLittleEndianInt32(binaryReader); + int height = ReadLittleEndianInt32(binaryReader); + return new Size(width, height); + } + + /// + /// Decodes the jfif. + /// + /// The binary reader. + /// Size. + /// + private static Size DecodeJfif(BinaryReader binaryReader) + { + while (binaryReader.ReadByte() == 0xff) + { + byte marker = binaryReader.ReadByte(); + short chunkLength = ReadLittleEndianInt16(binaryReader); + if (marker == 0xc0) + { + binaryReader.ReadByte(); + int height = ReadLittleEndianInt16(binaryReader); + int width = ReadLittleEndianInt16(binaryReader); + return new Size(width, height); + } + + if (chunkLength < 0) + { + var uchunkLength = (ushort)chunkLength; + binaryReader.ReadBytes(uchunkLength - 2); + } + else + { + binaryReader.ReadBytes(chunkLength - 2); + } + } + + throw new ArgumentException(ErrorMessage); + } + } +} diff --git a/MediaBrowser.Server.Implementations/Drawing/ImageProcessor.cs b/MediaBrowser.Server.Implementations/Drawing/ImageProcessor.cs new file mode 100644 index 0000000000..d16c2a4ded --- /dev/null +++ b/MediaBrowser.Server.Implementations/Drawing/ImageProcessor.cs @@ -0,0 +1,752 @@ +using System.Collections.Generic; +using System.Drawing; +using System.Drawing.Drawing2D; +using System.Drawing.Imaging; +using System.Globalization; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using MediaBrowser.Common.Extensions; +using MediaBrowser.Common.IO; +using MediaBrowser.Controller; +using MediaBrowser.Controller.Configuration; +using MediaBrowser.Controller.Drawing; +using MediaBrowser.Controller.Entities; +using MediaBrowser.Controller.Providers; +using MediaBrowser.Model.Drawing; +using System; +using System.Collections.Concurrent; +using System.IO; +using MediaBrowser.Model.Entities; +using MediaBrowser.Model.Logging; + +namespace MediaBrowser.Server.Implementations.Drawing +{ + /// + /// Class ImageProcessor + /// + public class ImageProcessor : IImageProcessor + { + /// + /// The us culture + /// + protected readonly CultureInfo UsCulture = new CultureInfo("en-US"); + + /// + /// The _cached imaged sizes + /// + private readonly ConcurrentDictionary _cachedImagedSizes = new ConcurrentDictionary(); + + /// + /// Gets the list of currently registered image processors + /// Image processors are specialized metadata providers that run after the normal ones + /// + /// The image enhancers. + public IEnumerable ImageEnhancers { get; private set; } + + /// + /// The _logger + /// + private readonly ILogger _logger; + /// + /// The _app paths + /// + private readonly IServerApplicationPaths _appPaths; + + private readonly string _imageSizeCachePath; + private readonly string _croppedWhitespaceImageCachePath; + private readonly string _enhancedImageCachePath; + private readonly string _resizedImageCachePath; + + public ImageProcessor(ILogger logger, IServerApplicationPaths appPaths) + { + _logger = logger; + _appPaths = appPaths; + + _imageSizeCachePath = Path.Combine(_appPaths.ImageCachePath, "image-sizes"); + _croppedWhitespaceImageCachePath = Path.Combine(_appPaths.ImageCachePath, "cropped-images"); + _enhancedImageCachePath = Path.Combine(_appPaths.ImageCachePath, "enhanced-images"); + _resizedImageCachePath = Path.Combine(_appPaths.ImageCachePath, "resized-images"); + } + + public void AddParts(IEnumerable enhancers) + { + ImageEnhancers = enhancers.ToArray(); + } + + public async Task ProcessImage(BaseItem entity, ImageType imageType, int imageIndex, string originalImagePath, bool cropWhitespace, DateTime dateModified, Stream toStream, int? width, int? height, int? maxWidth, int? maxHeight, int? quality, List enhancers) + { + if (entity == null) + { + throw new ArgumentNullException("entity"); + } + + if (toStream == null) + { + throw new ArgumentNullException("toStream"); + } + + if (cropWhitespace) + { + originalImagePath = await GetWhitespaceCroppedImage(originalImagePath, dateModified).ConfigureAwait(false); + } + + // No enhancement - don't cache + if (enhancers.Count > 0) + { + try + { + // Enhance if we have enhancers + var ehnancedImagePath = await GetEnhancedImage(originalImagePath, dateModified, entity, imageType, imageIndex, enhancers).ConfigureAwait(false); + + // If the path changed update dateModified + if (!ehnancedImagePath.Equals(originalImagePath, StringComparison.OrdinalIgnoreCase)) + { + dateModified = File.GetLastWriteTimeUtc(ehnancedImagePath); + originalImagePath = ehnancedImagePath; + } + } + catch (Exception ex) + { + _logger.Error("Error enhancing image", ex); + } + } + + var originalImageSize = GetImageSize(originalImagePath, dateModified); + + // Determine the output size based on incoming parameters + var newSize = DrawingUtils.Resize(originalImageSize, width, height, maxWidth, maxHeight); + + if (!quality.HasValue) + { + quality = 90; + } + + var cacheFilePath = GetCacheFilePath(originalImagePath, newSize, quality.Value, dateModified); + + try + { + using (var fileStream = new FileStream(cacheFilePath, FileMode.Open, FileAccess.Read, FileShare.Read, StreamDefaults.DefaultFileStreamBufferSize, FileOptions.Asynchronous)) + { + await fileStream.CopyToAsync(toStream).ConfigureAwait(false); + return; + } + } + catch (IOException) + { + // Cache file doesn't exist or is currently being written ro + } + + var semaphore = GetLock(cacheFilePath); + + await semaphore.WaitAsync().ConfigureAwait(false); + + // Check again in case of lock contention + if (File.Exists(cacheFilePath)) + { + try + { + using (var fileStream = new FileStream(cacheFilePath, FileMode.Open, FileAccess.Read, FileShare.Read, StreamDefaults.DefaultFileStreamBufferSize, FileOptions.Asynchronous)) + { + await fileStream.CopyToAsync(toStream).ConfigureAwait(false); + return; + } + } + finally + { + semaphore.Release(); + } + } + + try + { + using (var fileStream = new FileStream(originalImagePath, FileMode.Open, FileAccess.Read, FileShare.Read, StreamDefaults.DefaultFileStreamBufferSize, true)) + { + // Copy to memory stream to avoid Image locking file + using (var memoryStream = new MemoryStream()) + { + await fileStream.CopyToAsync(memoryStream).ConfigureAwait(false); + + using (var originalImage = Image.FromStream(memoryStream, true, false)) + { + var newWidth = Convert.ToInt32(newSize.Width); + var newHeight = Convert.ToInt32(newSize.Height); + + // Graphics.FromImage will throw an exception if the PixelFormat is Indexed, so we need to handle that here + using (var thumbnail = !ImageExtensions.IsPixelFormatSupportedByGraphicsObject(originalImage.PixelFormat) ? new Bitmap(originalImage, newWidth, newHeight) : new Bitmap(newWidth, newHeight, originalImage.PixelFormat)) + { + // 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; + thumbnailGraph.CompositingMode = CompositingMode.SourceOver; + + thumbnailGraph.DrawImage(originalImage, 0, 0, newWidth, newHeight); + + var outputFormat = originalImage.RawFormat; + + using (var outputMemoryStream = new MemoryStream()) + { + // Save to the memory stream + thumbnail.Save(outputFormat, outputMemoryStream, quality.Value); + + var bytes = outputMemoryStream.ToArray(); + + var outputTask = toStream.WriteAsync(bytes, 0, bytes.Length); + + // kick off a task to cache the result + var cacheTask = CacheResizedImage(cacheFilePath, bytes); + + await Task.WhenAll(outputTask, cacheTask).ConfigureAwait(false); + } + } + } + + } + } + } + } + finally + { + semaphore.Release(); + } + } + + /// + /// Crops whitespace from an image, caches the result, and returns the cached path + /// + /// The original image path. + /// The date modified. + /// System.String. + private async Task GetWhitespaceCroppedImage(string originalImagePath, DateTime dateModified) + { + var name = originalImagePath; + name += "datemodified=" + dateModified.Ticks; + + var croppedImagePath = GetCachePath(_croppedWhitespaceImageCachePath, name, Path.GetExtension(originalImagePath)); + + var semaphore = GetLock(croppedImagePath); + + await semaphore.WaitAsync().ConfigureAwait(false); + + // Check again in case of contention + if (File.Exists(croppedImagePath)) + { + semaphore.Release(); + return croppedImagePath; + } + + try + { + using (var fileStream = new FileStream(originalImagePath, FileMode.Open, FileAccess.Read, FileShare.Read, StreamDefaults.DefaultFileStreamBufferSize, true)) + { + // Copy to memory stream to avoid Image locking file + using (var memoryStream = new MemoryStream()) + { + await fileStream.CopyToAsync(memoryStream).ConfigureAwait(false); + + using (var originalImage = (Bitmap)Image.FromStream(memoryStream, true, false)) + { + var outputFormat = originalImage.RawFormat; + + using (var croppedImage = originalImage.CropWhitespace()) + { + var parentPath = Path.GetDirectoryName(croppedImagePath); + + if (!Directory.Exists(parentPath)) + { + Directory.CreateDirectory(parentPath); + } + + using (var outputStream = new FileStream(croppedImagePath, FileMode.Create, FileAccess.Write, FileShare.Read)) + { + croppedImage.Save(outputFormat, outputStream, 100); + } + } + } + } + } + } + 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 originalImagePath; + } + finally + { + semaphore.Release(); + } + + return croppedImagePath; + } + + /// + /// Caches the resized image. + /// + /// The cache file path. + /// The bytes. + private async Task CacheResizedImage(string cacheFilePath, byte[] bytes) + { + var parentPath = Path.GetDirectoryName(cacheFilePath); + + if (!Directory.Exists(parentPath)) + { + Directory.CreateDirectory(parentPath); + } + + // Save to the cache location + using (var cacheFileStream = new FileStream(cacheFilePath, FileMode.Create, FileAccess.Write, FileShare.Read, StreamDefaults.DefaultFileStreamBufferSize, FileOptions.Asynchronous)) + { + // Save to the filestream + await cacheFileStream.WriteAsync(bytes, 0, bytes.Length).ConfigureAwait(false); + } + } + + /// + /// Gets the cache file path based on a set of parameters + /// + /// The path to the original image file + /// The size to output the image in + /// Quality level, from 0-100. Currently only applies to JPG. The default value should suffice. + /// The last modified date of the image + /// System.String. + private string GetCacheFilePath(string originalPath, ImageSize outputSize, int quality, DateTime dateModified) + { + var filename = originalPath; + + filename += "width=" + outputSize.Width; + + filename += "height=" + outputSize.Height; + + filename += "quality=" + quality; + + filename += "datemodified=" + dateModified.Ticks; + + return GetCachePath(_resizedImageCachePath, filename, Path.GetExtension(originalPath)); + } + + /// + /// Gets the size of the image. + /// + /// The path. + /// ImageSize. + public ImageSize GetImageSize(string path) + { + return GetImageSize(path, File.GetLastWriteTimeUtc(path)); + } + + /// + /// Gets the size of the image. + /// + /// The path. + /// The image date modified. + /// ImageSize. + /// path + public ImageSize GetImageSize(string path, DateTime imageDateModified) + { + if (string.IsNullOrEmpty(path)) + { + throw new ArgumentNullException("path"); + } + + var name = path + "datemodified=" + imageDateModified.Ticks; + + ImageSize size; + + if (!_cachedImagedSizes.TryGetValue(name, out size)) + { + size = GetImageSizeInternal(name, path); + + _cachedImagedSizes.AddOrUpdate(name, size, (keyName, oldValue) => size); + } + + return size; + } + + /// + /// Gets the image size internal. + /// + /// The cache key. + /// The path. + /// ImageSize. + private ImageSize GetImageSizeInternal(string cacheKey, string path) + { + // Now check the file system cache + var fullCachePath = GetCachePath(_imageSizeCachePath, cacheKey, ".txt"); + + try + { + var result = File.ReadAllText(fullCachePath).Split('|').Select(i => double.Parse(i, UsCulture)).ToArray(); + + return new ImageSize { Width = result[0], Height = result[1] }; + } + catch (IOException) + { + // Cache file doesn't exist or is currently being written to + } + + var syncLock = GetObjectLock(fullCachePath); + + lock (syncLock) + { + try + { + var result = File.ReadAllText(fullCachePath) + .Split('|') + .Select(i => double.Parse(i, UsCulture)) + .ToArray(); + + return new ImageSize { Width = result[0], Height = result[1] }; + } + catch (FileNotFoundException) + { + // Cache file doesn't exist no biggie + } + catch (DirectoryNotFoundException) + { + // Cache file doesn't exist no biggie + } + + var size = ImageHeader.GetDimensions(path, _logger); + + var parentPath = Path.GetDirectoryName(fullCachePath); + + if (!Directory.Exists(parentPath)) + { + Directory.CreateDirectory(parentPath); + } + + // Update the file system cache + File.WriteAllText(fullCachePath, size.Width.ToString(UsCulture) + @"|" + size.Height.ToString(UsCulture)); + + return new ImageSize { Width = size.Width, Height = size.Height }; + } + } + + /// + /// Gets the image cache tag. + /// + /// The item. + /// Type of the image. + /// The image path. + /// Guid. + /// item + public Guid GetImageCacheTag(BaseItem item, ImageType imageType, string imagePath) + { + if (item == null) + { + throw new ArgumentNullException("item"); + } + + if (string.IsNullOrEmpty(imagePath)) + { + throw new ArgumentNullException("imagePath"); + } + + var dateModified = item.GetImageDateModified(imagePath); + + var supportedEnhancers = GetSupportedEnhancers(item, imageType).ToList(); + + return GetImageCacheTag(item, imageType, imagePath, dateModified, supportedEnhancers); + } + + /// + /// Gets the image cache tag. + /// + /// The item. + /// Type of the image. + /// The original image path. + /// The date modified of the original image file. + /// The image enhancers. + /// Guid. + /// item + public Guid GetImageCacheTag(BaseItem item, ImageType imageType, string originalImagePath, DateTime dateModified, IEnumerable imageEnhancers) + { + if (item == null) + { + throw new ArgumentNullException("item"); + } + + if (imageEnhancers == null) + { + throw new ArgumentNullException("imageEnhancers"); + } + + if (string.IsNullOrEmpty(originalImagePath)) + { + throw new ArgumentNullException("originalImagePath"); + } + + // Cache name is created with supported enhancers combined with the last config change so we pick up new config changes + var cacheKeys = imageEnhancers.Select(i => i.GetConfigurationCacheKey(item, imageType)).ToList(); + cacheKeys.Add(originalImagePath + dateModified.Ticks); + + return string.Join("|", cacheKeys.ToArray()).GetMD5(); + } + + /// + /// Gets the enhanced image. + /// + /// The original image path. + /// The date modified. + /// The item. + /// Type of the image. + /// Index of the image. + /// Task{System.String}. + /// item + public Task GetEnhancedImage(string originalImagePath, DateTime dateModified, BaseItem item, ImageType imageType, int imageIndex) + { + if (item == null) + { + throw new ArgumentNullException("item"); + } + + var supportedImageEnhancers = ImageEnhancers.Where(i => + { + try + { + return i.Supports(item, imageType); + } + catch (Exception ex) + { + _logger.ErrorException("Error in image enhancer: {0}", ex, i.GetType().Name); + + return false; + } + + }).ToList(); + + return GetEnhancedImage(originalImagePath, dateModified, item, imageType, imageIndex, supportedImageEnhancers); + } + + /// + /// Runs an image through the image enhancers, caches the result, and returns the cached path + /// + /// The original image path. + /// The date modified of the original image file. + /// The item. + /// Type of the image. + /// Index of the image. + /// The supported enhancers. + /// System.String. + /// originalImagePath + public async Task GetEnhancedImage(string originalImagePath, DateTime dateModified, BaseItem item, ImageType imageType, int imageIndex, List supportedEnhancers) + { + if (string.IsNullOrEmpty(originalImagePath)) + { + throw new ArgumentNullException("originalImagePath"); + } + + if (item == null) + { + throw new ArgumentNullException("item"); + } + + var cacheGuid = GetImageCacheTag(item, imageType, originalImagePath, dateModified, supportedEnhancers); + + // All enhanced images are saved as png to allow transparency + var enhancedImagePath = GetCachePath(_enhancedImageCachePath, cacheGuid + ".png"); + + var semaphore = GetLock(enhancedImagePath); + + await semaphore.WaitAsync().ConfigureAwait(false); + + // Check again in case of contention + if (File.Exists(enhancedImagePath)) + { + semaphore.Release(); + return enhancedImagePath; + } + + try + { + using (var fileStream = new FileStream(originalImagePath, FileMode.Open, FileAccess.Read, FileShare.Read, StreamDefaults.DefaultFileStreamBufferSize, true)) + { + // Copy to memory stream to avoid Image locking file + using (var memoryStream = new MemoryStream()) + { + await fileStream.CopyToAsync(memoryStream).ConfigureAwait(false); + + using (var originalImage = Image.FromStream(memoryStream, true, false)) + { + //Pass the image through registered enhancers + using (var newImage = await ExecuteImageEnhancers(supportedEnhancers, originalImage, item, imageType, imageIndex).ConfigureAwait(false)) + { + var parentDirectory = Path.GetDirectoryName(enhancedImagePath); + + if (!Directory.Exists(parentDirectory)) + { + Directory.CreateDirectory(parentDirectory); + } + + //And then save it in the cache + using (var outputStream = new FileStream(enhancedImagePath, FileMode.Create, FileAccess.Write, FileShare.Read)) + { + newImage.Save(ImageFormat.Png, outputStream, 100); + } + } + } + } + } + } + finally + { + semaphore.Release(); + } + + return enhancedImagePath; + } + + /// + /// Executes the image enhancers. + /// + /// The image enhancers. + /// The original image. + /// The item. + /// Type of the image. + /// Index of the image. + /// Task{EnhancedImage}. + private async Task ExecuteImageEnhancers(IEnumerable imageEnhancers, Image originalImage, BaseItem item, ImageType imageType, int imageIndex) + { + var result = originalImage; + + // Run the enhancers sequentially in order of priority + foreach (var enhancer in imageEnhancers) + { + var typeName = enhancer.GetType().Name; + + try + { + result = await enhancer.EnhanceImageAsync(item, result, imageType, imageIndex).ConfigureAwait(false); + } + catch (Exception ex) + { + _logger.ErrorException("{0} failed enhancing {1}", ex, typeName, item.Name); + + throw; + } + } + + return result; + } + + /// + /// The _semaphoreLocks + /// + private readonly ConcurrentDictionary _locks = new ConcurrentDictionary(); + + /// + /// Gets the lock. + /// + /// The filename. + /// System.Object. + private object GetObjectLock(string filename) + { + return _locks.GetOrAdd(filename, key => new object()); + } + + /// + /// The _semaphoreLocks + /// + private readonly ConcurrentDictionary _semaphoreLocks = new ConcurrentDictionary(); + + /// + /// Gets the lock. + /// + /// The filename. + /// System.Object. + private SemaphoreSlim GetLock(string filename) + { + return _semaphoreLocks.GetOrAdd(filename, key => new SemaphoreSlim(1, 1)); + } + + /// + /// Gets the cache path. + /// + /// The path. + /// Name of the unique. + /// The file extension. + /// System.String. + /// + /// path + /// or + /// uniqueName + /// or + /// fileExtension + /// + public string GetCachePath(string path, string uniqueName, string fileExtension) + { + if (string.IsNullOrEmpty(path)) + { + throw new ArgumentNullException("path"); + } + if (string.IsNullOrEmpty(uniqueName)) + { + throw new ArgumentNullException("uniqueName"); + } + + if (string.IsNullOrEmpty(fileExtension)) + { + throw new ArgumentNullException("fileExtension"); + } + + var filename = uniqueName.GetMD5() + fileExtension; + + return GetCachePath(path, filename); + } + + /// + /// Gets the cache path. + /// + /// The path. + /// The filename. + /// System.String. + /// + /// path + /// or + /// filename + /// + public string GetCachePath(string path, string filename) + { + if (string.IsNullOrEmpty(path)) + { + throw new ArgumentNullException("path"); + } + if (string.IsNullOrEmpty(filename)) + { + throw new ArgumentNullException("filename"); + } + + var prefix = filename.Substring(0, 1); + + path = Path.Combine(path, prefix); + + return Path.Combine(path, filename); + } + + public IEnumerable GetSupportedEnhancers(BaseItem item, ImageType imageType) + { + return ImageEnhancers.Where(i => + { + try + { + return i.Supports(item as BaseItem, imageType); + } + catch (Exception ex) + { + _logger.ErrorException("Error in image enhancer: {0}", ex, i.GetType().Name); + + return false; + } + + }).ToList(); + } + } +} diff --git a/MediaBrowser.Server.Implementations/Dto/DtoService.cs b/MediaBrowser.Server.Implementations/Dto/DtoService.cs index 24b6f0fcee..99878e2ec4 100644 --- a/MediaBrowser.Server.Implementations/Dto/DtoService.cs +++ b/MediaBrowser.Server.Implementations/Dto/DtoService.cs @@ -1,5 +1,5 @@ using MediaBrowser.Common.Extensions; -using MediaBrowser.Controller; +using MediaBrowser.Controller.Drawing; using MediaBrowser.Controller.Dto; using MediaBrowser.Controller.Entities; using MediaBrowser.Controller.Entities.Audio; @@ -30,13 +30,16 @@ namespace MediaBrowser.Server.Implementations.Dto private readonly IUserDataRepository _userDataRepository; private readonly IItemRepository _itemRepo; - public DtoService(ILogger logger, ILibraryManager libraryManager, IUserManager userManager, IUserDataRepository userDataRepository, IItemRepository itemRepo) + private readonly IImageProcessor _imageProcessor; + + public DtoService(ILogger logger, ILibraryManager libraryManager, IUserManager userManager, IUserDataRepository userDataRepository, IItemRepository itemRepo, IImageProcessor imageProcessor) { _logger = logger; _libraryManager = libraryManager; _userManager = userManager; _userDataRepository = userDataRepository; _itemRepo = itemRepo; + _imageProcessor = imageProcessor; } /// @@ -209,7 +212,7 @@ namespace MediaBrowser.Server.Implementations.Dto if (!string.IsNullOrEmpty(image)) { - dto.PrimaryImageTag = Kernel.Instance.ImageManager.GetImageCacheTag(user, ImageType.Primary, image); + dto.PrimaryImageTag = _imageProcessor.GetImageCacheTag(user, ImageType.Primary, image); try { @@ -288,7 +291,7 @@ namespace MediaBrowser.Server.Implementations.Dto { try { - info.PrimaryImageTag = Kernel.Instance.ImageManager.GetImageCacheTag(item, ImageType.Primary, imagePath); + info.PrimaryImageTag = _imageProcessor.GetImageCacheTag(item, ImageType.Primary, imagePath); } catch (IOException) { @@ -409,7 +412,7 @@ namespace MediaBrowser.Server.Implementations.Dto { try { - return Kernel.Instance.ImageManager.GetImageCacheTag(item, type, path); + return _imageProcessor.GetImageCacheTag(item, type, path); } catch (IOException ex) { @@ -1154,7 +1157,7 @@ namespace MediaBrowser.Server.Implementations.Dto try { - size = Kernel.Instance.ImageManager.GetImageSize(path, dateModified); + size = _imageProcessor.GetImageSize(path, dateModified); } catch (FileNotFoundException) { @@ -1169,21 +1172,7 @@ namespace MediaBrowser.Server.Implementations.Dto dto.OriginalPrimaryImageAspectRatio = size.Width / size.Height; - var supportedEnhancers = Kernel.Instance.ImageManager.ImageEnhancers.Where(i => - { - try - { - return i.Supports(item, ImageType.Primary); - } - catch (Exception ex) - { - _logger.ErrorException("Error in image enhancer: {0}", ex, i.GetType().Name); - - return false; - } - - }).ToList(); - + var supportedEnhancers = _imageProcessor.GetSupportedEnhancers(item, ImageType.Primary).ToList(); foreach (var enhancer in supportedEnhancers) { @@ -1199,6 +1188,5 @@ namespace MediaBrowser.Server.Implementations.Dto dto.PrimaryImageAspectRatio = size.Width / size.Height; } - } } diff --git a/MediaBrowser.Server.Implementations/MediaBrowser.Server.Implementations.csproj b/MediaBrowser.Server.Implementations/MediaBrowser.Server.Implementations.csproj index 22de1e8985..ff9ff4735c 100644 --- a/MediaBrowser.Server.Implementations/MediaBrowser.Server.Implementations.csproj +++ b/MediaBrowser.Server.Implementations/MediaBrowser.Server.Implementations.csproj @@ -91,6 +91,7 @@ False ..\packages\System.Data.SQLite.x86.1.0.88.0\lib\net45\System.Data.SQLite.Linq.dll + ..\packages\Rx-Core.2.1.30214.0\lib\Net45\System.Reactive.Core.dll @@ -112,6 +113,7 @@ + @@ -128,6 +130,7 @@ + diff --git a/MediaBrowser.ServerApplication/ApplicationHost.cs b/MediaBrowser.ServerApplication/ApplicationHost.cs index d7e228b183..8f738c5dcd 100644 --- a/MediaBrowser.ServerApplication/ApplicationHost.cs +++ b/MediaBrowser.ServerApplication/ApplicationHost.cs @@ -34,6 +34,7 @@ using MediaBrowser.Providers; using MediaBrowser.Server.Implementations; using MediaBrowser.Server.Implementations.BdInfo; using MediaBrowser.Server.Implementations.Configuration; +using MediaBrowser.Server.Implementations.Drawing; using MediaBrowser.Server.Implementations.Dto; using MediaBrowser.Server.Implementations.HttpServer; using MediaBrowser.Server.Implementations.IO; @@ -160,6 +161,7 @@ namespace MediaBrowser.ServerApplication /// The HTTP server. private IHttpServer HttpServer { get; set; } private IDtoService DtoService { get; set; } + private IImageProcessor ImageProcessor { get; set; } /// /// Gets or sets the media encoder. @@ -295,7 +297,10 @@ namespace MediaBrowser.ServerApplication LocalizationManager = new LocalizationManager(ServerConfigurationManager); RegisterSingleInstance(LocalizationManager); - DtoService = new DtoService(Logger, LibraryManager, UserManager, UserDataRepository, ItemRepository); + ImageProcessor = new ImageProcessor(Logger, ServerConfigurationManager.ApplicationPaths); + RegisterSingleInstance(ImageProcessor); + + DtoService = new DtoService(Logger, LibraryManager, UserManager, UserDataRepository, ItemRepository, ImageProcessor); RegisterSingleInstance(DtoService); var displayPreferencesTask = Task.Run(async () => await ConfigureDisplayPreferencesRepositories().ConfigureAwait(false)); @@ -314,11 +319,8 @@ namespace MediaBrowser.ServerApplication /// private void SetKernelProperties() { - ServerKernel.ImageManager = new ImageManager(LogManager.GetLogger("ImageManager"), - ApplicationPaths, ItemRepository); Parallel.Invoke( () => ServerKernel.FFMpegManager = new FFMpegManager(ApplicationPaths, MediaEncoder, Logger, ItemRepository), - () => ServerKernel.ImageManager.ImageEnhancers = GetExports().OrderBy(e => e.Priority).ToArray(), () => LocalizedStrings.StringFiles = GetExports(), SetStaticProperties ); @@ -461,6 +463,8 @@ namespace MediaBrowser.ServerApplication ProviderManager.AddParts(GetExports().ToArray()); IsoManager.AddParts(GetExports().ToArray()); + + ImageProcessor.AddParts(GetExports().ToArray()); } /// -- cgit v1.2.3