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 --- .../Drawing/ImageHeader.cs | 227 +++++++ .../Drawing/ImageProcessor.cs | 752 +++++++++++++++++++++ .../Dto/DtoService.cs | 32 +- .../MediaBrowser.Server.Implementations.csproj | 3 + 4 files changed, 992 insertions(+), 22 deletions(-) create mode 100644 MediaBrowser.Server.Implementations/Drawing/ImageHeader.cs create mode 100644 MediaBrowser.Server.Implementations/Drawing/ImageProcessor.cs (limited to 'MediaBrowser.Server.Implementations') diff --git a/MediaBrowser.Server.Implementations/Drawing/ImageHeader.cs b/MediaBrowser.Server.Implementations/Drawing/ImageHeader.cs new file mode 100644 index 000000000..4da836cc6 --- /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 000000000..d16c2a4de --- /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 24b6f0fce..99878e2ec 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 22de1e898..ff9ff4735 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 @@ + -- cgit v1.2.3