From bd6c2d2a22c099e35fd69816e28b5c46a83176b1 Mon Sep 17 00:00:00 2001 From: LukePulverenti Luke Pulverenti luke pulverenti Date: Tue, 18 Sep 2012 18:32:37 -0400 Subject: [PATCH] A few more image improvements --- MediaBrowser.Api/HttpHandlers/ImageHandler.cs | 146 ++++++------------ .../Drawing/BaseImageProcessor.cs | 57 ++++--- .../Drawing/ImageProcessor.cs | 59 ++++--- 3 files changed, 127 insertions(+), 135 deletions(-) diff --git a/MediaBrowser.Api/HttpHandlers/ImageHandler.cs b/MediaBrowser.Api/HttpHandlers/ImageHandler.cs index da20e0533..c168569cd 100644 --- a/MediaBrowser.Api/HttpHandlers/ImageHandler.cs +++ b/MediaBrowser.Api/HttpHandlers/ImageHandler.cs @@ -1,6 +1,4 @@ -using System.Drawing.Imaging; -using MediaBrowser.Common.Logging; -using MediaBrowser.Common.Net; +using MediaBrowser.Common.Net; using MediaBrowser.Common.Net.Handlers; using MediaBrowser.Controller; using MediaBrowser.Controller.Drawing; @@ -24,6 +22,7 @@ namespace MediaBrowser.Api.HttpHandlers } private string _imagePath; + private async Task GetImagePath() { _imagePath = _imagePath ?? await DiscoverImagePath(); @@ -32,28 +31,34 @@ namespace MediaBrowser.Api.HttpHandlers } private BaseEntity _sourceEntity; + private async Task GetSourceEntity() { if (_sourceEntity == null) { if (!string.IsNullOrEmpty(QueryString["personname"])) { - _sourceEntity = await Kernel.Instance.ItemController.GetPerson(QueryString["personname"]).ConfigureAwait(false); + _sourceEntity = + await Kernel.Instance.ItemController.GetPerson(QueryString["personname"]).ConfigureAwait(false); } else if (!string.IsNullOrEmpty(QueryString["genre"])) { - _sourceEntity = await Kernel.Instance.ItemController.GetGenre(QueryString["genre"]).ConfigureAwait(false); + _sourceEntity = + await Kernel.Instance.ItemController.GetGenre(QueryString["genre"]).ConfigureAwait(false); } else if (!string.IsNullOrEmpty(QueryString["year"])) { - _sourceEntity = await Kernel.Instance.ItemController.GetYear(int.Parse(QueryString["year"])).ConfigureAwait(false); + _sourceEntity = + await + Kernel.Instance.ItemController.GetYear(int.Parse(QueryString["year"])).ConfigureAwait(false); } else if (!string.IsNullOrEmpty(QueryString["studio"])) { - _sourceEntity = await Kernel.Instance.ItemController.GetStudio(QueryString["studio"]).ConfigureAwait(false); + _sourceEntity = + await Kernel.Instance.ItemController.GetStudio(QueryString["studio"]).ConfigureAwait(false); } else if (!string.IsNullOrEmpty(QueryString["userid"])) @@ -74,61 +79,11 @@ namespace MediaBrowser.Api.HttpHandlers { var entity = await GetSourceEntity().ConfigureAwait(false); - var item = entity as BaseItem; - - if (item != null) - { - return GetImagePathFromTypes(item, ImageType, ImageIndex); - } - - return entity.PrimaryImagePath; + return ImageProcessor.GetImagePath(entity, ImageType, ImageIndex); } - private Stream _sourceStream; - private async Task GetSourceStream() + public override async Task GetContentType() { - await EnsureSourceStream().ConfigureAwait(false); - return _sourceStream; - } - - private bool _sourceStreamEnsured; - private async Task EnsureSourceStream() - { - if (!_sourceStreamEnsured) - { - try - { - _sourceStream = File.OpenRead(await GetImagePath().ConfigureAwait(false)); - } - catch (FileNotFoundException ex) - { - StatusCode = 404; - Logger.LogException(ex); - } - catch (DirectoryNotFoundException ex) - { - StatusCode = 404; - Logger.LogException(ex); - } - catch (UnauthorizedAccessException ex) - { - StatusCode = 403; - Logger.LogException(ex); - } - finally - { - _sourceStreamEnsured = true; - } - } - } - - public async override Task GetContentType() - { - if (await GetSourceStream().ConfigureAwait(false) == null) - { - return null; - } - if (Kernel.Instance.ImageProcessors.Any(i => i.RequiresTransparency)) { return MimeTypes.GetMimeType(".png"); @@ -139,20 +94,47 @@ namespace MediaBrowser.Api.HttpHandlers public override TimeSpan CacheDuration { - get - { - return TimeSpan.FromDays(365); - } + get { return TimeSpan.FromDays(365); } } - protected async override Task GetLastDateModified() + protected override async Task GetLastDateModified() { - if (await GetSourceStream().ConfigureAwait(false) == null) + string path = await GetImagePath().ConfigureAwait(false); + + DateTime date = File.GetLastWriteTimeUtc(path); + + // If the file does not exist it will return jan 1, 1601 + // http://msdn.microsoft.com/en-us/library/system.io.file.getlastwritetimeutc.aspx + if (date.Year == 1601) { - return null; + if (!File.Exists(path)) + { + StatusCode = 404; + return null; + } } - return File.GetLastWriteTimeUtc(await GetImagePath().ConfigureAwait(false)); + return await GetMostRecentDateModified(date); + } + + private async Task GetMostRecentDateModified(DateTime imageFileLastDateModified) + { + var date = imageFileLastDateModified; + + var entity = await GetSourceEntity().ConfigureAwait(false); + + foreach (var processor in Kernel.Instance.ImageProcessors) + { + if (processor.IsConfiguredToProcess(entity, ImageType, ImageIndex)) + { + if (processor.ProcessingConfigurationDateLastModifiedUtc > date) + { + date = processor.ProcessingConfigurationDateLastModifiedUtc; + } + } + } + + return date; } private int ImageIndex @@ -262,37 +244,9 @@ namespace MediaBrowser.Api.HttpHandlers protected override async Task WriteResponseToOutputStream(Stream stream) { - Stream sourceStream = await GetSourceStream().ConfigureAwait(false); - var entity = await GetSourceEntity().ConfigureAwait(false); - ImageProcessor.ProcessImage(sourceStream, stream, Width, Height, MaxWidth, MaxHeight, Quality, entity, ImageType, ImageIndex); - } - - private string GetImagePathFromTypes(BaseItem item, ImageType imageType, int imageIndex) - { - if (imageType == ImageType.Logo) - { - return item.LogoImagePath; - } - if (imageType == ImageType.Backdrop) - { - return item.BackdropImagePaths.ElementAt(imageIndex); - } - if (imageType == ImageType.Banner) - { - return item.BannerImagePath; - } - if (imageType == ImageType.Art) - { - return item.ArtImagePath; - } - if (imageType == ImageType.Thumbnail) - { - return item.ThumbnailImagePath; - } - - return item.PrimaryImagePath; + ImageProcessor.ProcessImage(entity, ImageType, ImageIndex, stream, Width, Height, MaxWidth, MaxHeight, Quality); } } } diff --git a/MediaBrowser.Controller/Drawing/BaseImageProcessor.cs b/MediaBrowser.Controller/Drawing/BaseImageProcessor.cs index ebd0e22c8..8fc6564e7 100644 --- a/MediaBrowser.Controller/Drawing/BaseImageProcessor.cs +++ b/MediaBrowser.Controller/Drawing/BaseImageProcessor.cs @@ -1,5 +1,6 @@ using MediaBrowser.Controller.Entities; using MediaBrowser.Model.Entities; +using System; using System.ComponentModel.Composition; using System.Drawing; using System.Drawing.Drawing2D; @@ -15,16 +16,7 @@ namespace MediaBrowser.Controller.Drawing public abstract class BaseImageProcessor { /// - /// Processes the primary image for a BaseEntity (Person, Studio, User, etc) - /// - /// The original Image, before re-sizing - /// The bitmap holding the original image, after re-sizing - /// The graphics surface on which the output is drawn - /// The entity that owns the image - public abstract void ProcessImage(Image originalImage, Bitmap bitmap, Graphics graphics, BaseEntity entity); - - /// - /// Processes an image for a BaseItem + /// Processes an image for a BaseEntity /// /// The original Image, before re-sizing /// The bitmap holding the original image, after re-sizing @@ -32,10 +24,11 @@ namespace MediaBrowser.Controller.Drawing /// The entity that owns the image /// The image type /// The image index (currently only used with backdrops) - public abstract void ProcessImage(Image originalImage, Bitmap bitmap, Graphics graphics, BaseItem entity, ImageType imageType, int imageIndex); + public abstract void ProcessImage(Image originalImage, Bitmap bitmap, Graphics graphics, BaseEntity entity, ImageType imageType, int imageIndex); /// /// If true, the image output format will be forced to png, resulting in an output size that will generally run larger than jpg + /// If false, the original image format is preserved. /// public virtual bool RequiresTransparency { @@ -44,6 +37,18 @@ namespace MediaBrowser.Controller.Drawing return false; } } + + /// + /// Determines if the image processor is configured to process the specified entity, image type and image index + /// This will aid http response caching so that we don't invalidate image caches when we don't have to + /// + public abstract bool IsConfiguredToProcess(BaseEntity entity, ImageType imageType, int imageIndex); + + /// + /// This is used for caching purposes, since a configuration change needs to invalidate a user's image cache + /// If the image processor is hosted within a plugin then this should be the plugin ConfigurationDateLastModified + /// + public abstract DateTime ProcessingConfigurationDateLastModifiedUtc { get; } } /// @@ -52,26 +57,22 @@ namespace MediaBrowser.Controller.Drawing //[Export(typeof(BaseImageProcessor))] public class MyRoundedCornerImageProcessor : BaseImageProcessor { - public override void ProcessImage(Image originalImage, Bitmap bitmap, Graphics g, BaseEntity entity) + public override void ProcessImage(Image originalImage, Bitmap bitmap, Graphics graphics, BaseEntity entity, ImageType imageType, int imageIndex) { var CornerRadius = 20; - g.Clear(Color.Transparent); - + graphics.Clear(Color.Transparent); + using (GraphicsPath gp = new GraphicsPath()) { gp.AddArc(0, 0, CornerRadius, CornerRadius, 180, 90); gp.AddArc(0 + bitmap.Width - CornerRadius, 0, CornerRadius, CornerRadius, 270, 90); gp.AddArc(0 + bitmap.Width - CornerRadius, 0 + bitmap.Height - CornerRadius, CornerRadius, CornerRadius, 0, 90); gp.AddArc(0, 0 + bitmap.Height - CornerRadius, CornerRadius, CornerRadius, 90, 90); - - g.SetClip(gp); - g.DrawImage(originalImage, 0, 0, bitmap.Width, bitmap.Height); - } - } - public override void ProcessImage(Image originalImage, Bitmap bitmap, Graphics graphics, BaseItem entity, ImageType imageType, int imageIndex) - { + graphics.SetClip(gp); + graphics.DrawImage(originalImage, 0, 0, bitmap.Width, bitmap.Height); + } } public override bool RequiresTransparency @@ -81,5 +82,19 @@ namespace MediaBrowser.Controller.Drawing return true; } } + + public override DateTime ProcessingConfigurationDateLastModifiedUtc + { + get + { + // This will result in a situation where images are never cached, but again, this is a prototype + return DateTime.UtcNow; + } + } + + public override bool IsConfiguredToProcess(BaseEntity entity, ImageType imageType, int imageIndex) + { + return true; + } } } diff --git a/MediaBrowser.Controller/Drawing/ImageProcessor.cs b/MediaBrowser.Controller/Drawing/ImageProcessor.cs index f9b6366c6..7ee4ef734 100644 --- a/MediaBrowser.Controller/Drawing/ImageProcessor.cs +++ b/MediaBrowser.Controller/Drawing/ImageProcessor.cs @@ -14,19 +14,18 @@ namespace MediaBrowser.Controller.Drawing /// /// Processes an image by resizing to target dimensions /// - /// The stream containing the source image + /// The entity that owns the image + /// The image type + /// The image index (currently only used with backdrops) /// 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 entity that owns the image - /// The image type - /// The image index (currently only used with backdrops) - public static void ProcessImage(Stream sourceImageStream, Stream toStream, int? width, int? height, int? maxWidth, int? maxHeight, int? quality, BaseEntity entity, ImageType imageType, int imageIndex) + public static void ProcessImage(BaseEntity entity, ImageType imageType, int imageIndex, Stream toStream, int? width, int? height, int? maxWidth, int? maxHeight, int? quality) { - Image originalImage = Image.FromStream(sourceImageStream); + Image originalImage = Image.FromFile(GetImagePath(entity, imageType, imageIndex)); // Determine the output size based on incoming parameters Size newSize = DrawingUtils.Resize(originalImage.Size, width, height, maxWidth, maxHeight); @@ -79,9 +78,42 @@ namespace MediaBrowser.Controller.Drawing originalImage.Dispose(); } + public static string GetImagePath(BaseEntity entity, ImageType imageType, int imageIndex) + { + var item = entity as BaseItem; + + if (item != null) + { + if (imageType == ImageType.Logo) + { + return item.LogoImagePath; + } + if (imageType == ImageType.Backdrop) + { + return item.BackdropImagePaths.ElementAt(imageIndex); + } + if (imageType == ImageType.Banner) + { + return item.BannerImagePath; + } + if (imageType == ImageType.Art) + { + return item.ArtImagePath; + } + if (imageType == ImageType.Thumbnail) + { + return item.ThumbnailImagePath; + } + } + + return entity.PrimaryImagePath; + } + + /// /// Executes additional image processors that are registered with the Kernel /// + /// The original Image, before re-sizing /// The bitmap holding the original image, after re-sizing /// The graphics surface on which the output is drawn /// The entity that owns the image @@ -89,20 +121,11 @@ namespace MediaBrowser.Controller.Drawing /// The image index (currently only used with backdrops) private static void ExecuteAdditionalImageProcessors(Image originalImage, Bitmap bitmap, Graphics graphics, BaseEntity entity, ImageType imageType, int imageIndex) { - var baseItem = entity as BaseItem; - - if (baseItem != null) + foreach (var processor in Kernel.Instance.ImageProcessors) { - foreach (var processor in Kernel.Instance.ImageProcessors) + if (processor.IsConfiguredToProcess(entity, imageType, imageIndex)) { - processor.ProcessImage(originalImage, bitmap, graphics, baseItem, imageType, imageIndex); - } - } - else - { - foreach (var processor in Kernel.Instance.ImageProcessors) - { - processor.ProcessImage(originalImage, bitmap, graphics, entity); + processor.ProcessImage(originalImage, bitmap, graphics, entity, imageType, imageIndex); } } }