/// <summary> /// Creates a copy of an image allowing you to set the pixel format. /// Disposing of the original is the responsibility of the user. /// <remarks> /// Unlike the native <see cref="Image.Clone"/> method this also copies animation frames. /// </remarks> /// </summary> /// <param name="source">The source image to copy.</param> /// <param name="animationProcessMode">The process mode for frames in animated images.</param> /// <param name="format">The <see cref="PixelFormat"/> to set the copied image to.</param> /// <returns> /// The <see cref="Image"/>. /// </returns> public static Image Copy(this Image source, AnimationProcessMode animationProcessMode, PixelFormat format = PixelFormat.Format32bppPArgb) { if (FormatUtilities.IsAnimated(source)) { // Read from the correct first frame when performing additional processing source.SelectActiveFrame(FrameDimension.Time, 0); // TODO: Ensure that we handle other animated types. GifDecoder decoder = new GifDecoder(source, animationProcessMode); GifEncoder encoder = new GifEncoder(null, null, decoder.LoopCount); for (int i = 0; i < decoder.FrameCount; i++) { GifFrame frame = decoder.GetFrame(source, i); frame.Image = ((Bitmap)frame.Image).Clone(new Rectangle(0, 0, frame.Image.Width, frame.Image.Height), format); ((Bitmap)frame.Image).SetResolution(source.HorizontalResolution, source.VerticalResolution); encoder.AddFrame(frame); } return(encoder.Save()); } Bitmap copy = ((Bitmap)source).Clone(new Rectangle(0, 0, source.Width, source.Height), format); copy.SetResolution(source.HorizontalResolution, source.VerticalResolution); return(copy); }
/// <summary> /// Gets the animation mode passed in through the querystring, defaults to the default behaviour (All) if nothing found. /// </summary> /// <param name="queryString">The query string to search.</param> /// <returns> /// The process mode for frames in animated images. /// </returns> private AnimationProcessMode ParseAnimationMode(string queryString) { AnimationProcessMode mode = AnimationProcessMode.All; NameValueCollection queryCollection = HttpUtility.ParseQueryString(queryString); if (queryCollection.AllKeys.Contains("animationprocessmode", StringComparer.InvariantCultureIgnoreCase)) { mode = QueryParamParser.Instance.ParseValue <AnimationProcessMode>(queryCollection["animationprocessmode"]); } return(mode); }
/// <summary> /// Initializes a new instance of the <see cref="GifDecoder"/> class. /// </summary> /// <param name="image"> /// The <see cref="Image"/> to decode. /// </param> /// <param name="animationProcessMode"> /// The <see cref="AnimationProcessMode" /> to use. /// </param> public GifDecoder(Image image, AnimationProcessMode animationProcessMode) { this.Height = image.Height; this.Width = image.Width; if (FormatUtilities.IsAnimated(image) && animationProcessMode == AnimationProcessMode.All) { this.IsAnimated = true; this.FrameCount = image.GetFrameCount(FrameDimension.Time); // Loop info is stored at byte 20737. this.LoopCount = BitConverter.ToInt16(image.GetPropertyItem((int)ExifPropertyTag.LoopCount).Value, 0); } else { this.FrameCount = 1; } }
/// <summary> /// Initializes a new instance of the <see cref="GifDecoder"/> class. /// </summary> /// <param name="image"> /// The <see cref="Image"/> to decode. /// </param> /// <param name="animationProcessMode"> /// The <see cref="AnimationProcessMode" /> to use. /// </param> public GifDecoder(Image image, AnimationProcessMode animationProcessMode) { this.Height = image.Height; this.Width = image.Width; if (FormatUtilities.IsAnimated(image) && animationProcessMode == AnimationProcessMode.All) { this.IsAnimated = true; this.FrameCount = image.GetFrameCount(FrameDimension.Time); // Loop info is stored at byte 20737. this.LoopCount = BitConverter.ToInt16(image.GetPropertyItem((int)ExifPropertyTag.LoopCount).Value, 0); } else { this.FrameCount = 1; } }
/// <summary> /// Gets the animation mode passed in through the querystring, defaults to the default behaviour (All) if nothing found. /// </summary> /// <param name="queryString">The query string to search.</param> /// <param name="process"> /// Whether to process the request. True if <see cref="AnimationProcessMode.First"/> /// has been explicitly requested. /// </param> /// <returns> /// The process mode for frames in animated images. /// </returns> private AnimationProcessMode ParseAnimationMode(string queryString, out bool process) { AnimationProcessMode mode = AnimationProcessMode.All; NameValueCollection queryCollection = HttpUtility.ParseQueryString(queryString); process = false; if (queryCollection.AllKeys.Contains("animationprocessmode", StringComparer.InvariantCultureIgnoreCase)) { mode = QueryParamParser.Instance.ParseValue <AnimationProcessMode>(queryCollection["animationprocessmode"]); // Common sense would dictate that requesting AnimationProcessMode.All is a pointless request // since that is the default behaviour and shouldn't be requested on its own. // But hey! this is the internet and it's impossible to stop people twisting your intent into // whatever bizarre thing they choose. Those cats are crazy! process = true; } return(mode); }
/// <summary> /// Initializes a new instance of the <see cref="GifDecoder"/> class. /// </summary> /// <param name="image"> /// The <see cref="Image"/> to decode. /// </param> /// <param name="animationProcessMode"> /// The <see cref="AnimationProcessMode" /> to use. /// </param> public GifDecoder(Image image, AnimationProcessMode animationProcessMode) { Height = image.Height; Width = image.Width; if (FormatUtilities.IsAnimated(image) && animationProcessMode == AnimationProcessMode.All) { IsAnimated = true; FrameCount = image.GetFrameCount(FrameDimension.Time); // Loop info is stored at byte 20737. Default to infinite loop if not found. LoopCount = image.PropertyIdList.Contains((int)ExifPropertyTag.LoopCount) ? BitConverter.ToInt16(image.GetPropertyItem((int)ExifPropertyTag.LoopCount).Value, 0) : 0; } else { FrameCount = 1; } }
/// <summary> /// Creates a deep copy of an image allowing you to set the pixel format. /// Disposing of the original is the responsibility of the user. /// <remarks> /// Unlike the native <see cref="Image.Clone"/> method this also copies animation frames. /// </remarks> /// </summary> /// <param name="source">The source image to copy.</param> /// <param name="animationProcessMode">The process mode for frames in animated images.</param> /// <param name="format">The <see cref="PixelFormat"/> to set the copied image to.</param> /// <param name="preserveExifData">Whether to preserve exif metadata. Defaults to false.</param> /// <returns> /// The <see cref="Image"/>. /// </returns> public static Image Copy(this Image source, AnimationProcessMode animationProcessMode, PixelFormat format = PixelFormat.Format32bppPArgb, bool preserveExifData = false) { if (source.RawFormat.Equals(ImageFormat.Gif)) { // Read from the correct first frame when performing additional processing source.SelectActiveFrame(FrameDimension.Time, 0); var decoder = new GifDecoder(source, animationProcessMode); var encoder = new GifEncoder(null, null, decoder.LoopCount); // Have to use Octree here, there's no way to inject it. var quantizer = new OctreeQuantizer(); for (var i = 0; i < decoder.FrameCount; i++) { var frame = decoder.GetFrame(source, i); frame.Image = quantizer.Quantize(((Bitmap)frame.Image).Clone(new Rectangle(0, 0, frame.Image.Width, frame.Image.Height), format)); ((Bitmap)frame.Image).SetResolution(source.HorizontalResolution, source.VerticalResolution); encoder.AddFrame(frame); } return(encoder.Save()); } // Create a new image and copy it's pixels. var copy = new Bitmap(source.Width, source.Height, format); copy.SetResolution(source.HorizontalResolution, source.VerticalResolution); using (var graphics = Graphics.FromImage(copy)) { graphics.DrawImageUnscaled(source, 0, 0); } if (preserveExifData) { foreach (var item in source.PropertyItems) { copy.SetPropertyItem(item); } } return(copy); }
/// <summary> /// Creates a deep copy of an image allowing you to set the pixel format. /// Disposing of the original is the responsibility of the user. /// <remarks> /// Unlike the native <see cref="Image.Clone"/> method this also copies animation frames. /// </remarks> /// </summary> /// <param name="source">The source image to copy.</param> /// <param name="animationProcessMode">The process mode for frames in animated images.</param> /// <param name="format">The <see cref="PixelFormat"/> to set the copied image to.</param> /// <param name="preserveExifData">Whether to preserve exif metadata. Defaults to false.</param> /// <returns> /// The <see cref="Image"/>. /// </returns> public static Image Copy(this Image source, AnimationProcessMode animationProcessMode, PixelFormat format = PixelFormat.Format32bppPArgb, bool preserveExifData = false) { if (source.RawFormat.Equals(ImageFormat.Gif)) { // Read from the correct first frame when performing additional processing source.SelectActiveFrame(FrameDimension.Time, 0); GifDecoder decoder = new GifDecoder(source, animationProcessMode); GifEncoder encoder = new GifEncoder(null, null, decoder.LoopCount); // Have to use Octree here, there's no way to inject it. OctreeQuantizer quantizer = new OctreeQuantizer(); for (int i = 0; i < decoder.FrameCount; i++) { GifFrame frame = decoder.GetFrame(source, i); frame.Image = quantizer.Quantize(((Bitmap)frame.Image).Clone(new Rectangle(0, 0, frame.Image.Width, frame.Image.Height), format)); ((Bitmap)frame.Image).SetResolution(source.HorizontalResolution, source.VerticalResolution); encoder.AddFrame(frame); } return encoder.Save(); } // Create a new image and copy it's pixels. Bitmap copy = new Bitmap(source.Width, source.Height, format); copy.SetResolution(source.HorizontalResolution, source.VerticalResolution); using (Graphics graphics = Graphics.FromImage(copy)) { graphics.DrawImageUnscaled(source, 0, 0); } if (preserveExifData) { foreach (PropertyItem item in source.PropertyItems) { copy.SetPropertyItem(item); } } return copy; }
/// <summary> /// Gets the animation mode passed in through the querystring, defaults to the default behaviour (All) if nothing found. /// </summary> /// <param name="queryString">The query string to search.</param> /// <param name="process"> /// Whether to process the request. True if <see cref="AnimationProcessMode.First"/> /// has been explicitly requested. /// </param> /// <returns> /// The process mode for frames in animated images. /// </returns> private AnimationProcessMode ParseAnimationMode(string queryString, out bool process) { AnimationProcessMode mode = AnimationProcessMode.All; string decoded = !string.IsNullOrWhiteSpace(queryString) ? HttpUtility.HtmlDecode(queryString) : string.Empty; NameValueCollection queryCollection = HttpUtility.ParseQueryString(decoded); process = false; if (queryCollection["animationprocessmode"] is string animationProcessMode) { mode = QueryParamParser.Instance.ParseValue <AnimationProcessMode>(animationProcessMode); // Common sense would dictate that requesting AnimationProcessMode.All is a pointless request // since that is the default behaviour and shouldn't be requested on its own. // But hey! this is the internet and it's impossible to stop people twisting your intent into // whatever bizarre thing they choose. Those cats are crazy! process = true; } return(mode); }
/// <summary> /// Processes the image. /// </summary> /// <param name="context"> /// the <see cref="T:System.Web.HttpContext">HttpContext</see> object that provides /// references to the intrinsic server objects /// </param> /// <returns> /// The <see cref="T:System.Threading.Tasks.Task"/>. /// </returns> private async Task ProcessImageAsync(HttpContext context) { HttpRequest request = context.Request; string rawUrl = this.GetRequestUrl(request); // Should we ignore this request? if (string.IsNullOrWhiteSpace(rawUrl) || rawUrl.IndexOf("IPIGNORE=TRUE", StringComparison.InvariantCultureIgnoreCase) >= 0) { return; } // Sometimes the request is url encoded // See https://github.com/JimBobSquarePants/ImageProcessor/issues/478 // This causes a bit of a nightmare as the incoming request is corrupted and cannot be used for splitting // out each url part. This becomes a manual job. string url = rawUrl; string applicationPath = request.ApplicationPath; IImageService currentService = this.GetImageServiceForRequest(url, applicationPath); if (currentService == null) { return; } // Parse url UrlParser.ParseUrl(url, currentService.Prefix, out string requestPath, out string queryString); string originalQueryString = queryString; // Replace any presets in the querystring with the actual value. queryString = this.ReplacePresetsInQueryString(queryString); var httpContextBase = new HttpContextWrapper(context); // Execute the handler which can change the querystring var validatingArgs = new ValidatingRequestEventArgs(httpContextBase, queryString); this.OnValidatingRequest(validatingArgs); // If the validation has failed based on events, return if (validatingArgs.Cancel) { ImageProcessorBootstrapper.Instance.Logger.Log <ImageProcessingModule>($"Image processing for {url} has been cancelled by an event"); return; } // Re-assign based on event handlers queryString = validatingArgs.QueryString; if (string.IsNullOrWhiteSpace(originalQueryString) && !string.IsNullOrWhiteSpace(queryString)) { url = url + "?" + queryString; } else if (!string.IsNullOrWhiteSpace(queryString)) { url = Regex.Replace(url, originalQueryString, queryString, RegexOptions.IgnoreCase); } // Map the request path if file local. bool isFileLocal = currentService.IsFileLocalService; if (currentService.IsFileLocalService) { requestPath = HostingEnvironment.MapPath(requestPath); } if (string.IsNullOrWhiteSpace(requestPath)) { return; } // Parse any protocol values from settings if no protocol is present. if (currentService.Settings.ContainsKey("Protocol") && (ProtocolRegex.Matches(requestPath).Count == 0 || ProtocolRegex.Matches(requestPath)[0].Index > 0)) { // ReSharper disable once PossibleNullReferenceException requestPath = currentService.Settings["Protocol"] + "://" + requestPath.TrimStart('/'); } // Break out if we don't meet critera. // First check that the request path is valid and whether we are intercepting all requests or the querystring is valid. bool interceptAll = interceptAllRequests != null && interceptAllRequests.Value; if (string.IsNullOrWhiteSpace(requestPath) || (!interceptAll && string.IsNullOrWhiteSpace(queryString))) { return; } // Check whether the path is valid for other requests. // We've already checked the unprefixed requests in GetImageServiceForRequest(). if (!string.IsNullOrWhiteSpace(currentService.Prefix) && !currentService.IsValidRequest(requestPath)) { return; } bool isNewOrUpdated = false; string cachedPath = string.Empty; bool processing = false; IWebGraphicsProcessor[] processors = null; AnimationProcessMode mode = AnimationProcessMode.First; using (await Locker.ReaderLockAsync(rawUrl).ConfigureAwait(false)) { // Parse the url to see whether we should be doing any work. // If we're not intercepting all requests and we don't have valid instructions we shoul break here. if (!string.IsNullOrWhiteSpace(queryString)) { // Attempt to match querystring and processors. processors = ImageFactoryExtensions.GetMatchingProcessors(queryString); // Animation is not a processor but can be a specific request so we should allow it. mode = this.ParseAnimationMode(queryString, out bool processAnimation); // Are we processing or cache busting? processing = processors != null && (processors.Length > 0 || processAnimation); bool cacheBusting = ParseCacheBuster(queryString); if (!processing && !cacheBusting) { // No? Someone is either attacking the server or hasn't read the instructions. string message = $"The request {request.Unvalidated.RawUrl} could not be understood by the server due to malformed syntax."; ImageProcessorBootstrapper.Instance.Logger.Log <ImageProcessingModule>(message); return; } } // Create a new cache to help process and cache the request. this.imageCache = (IImageCache)ImageProcessorConfiguration.Instance .ImageCache.GetInstance(requestPath, url, queryString); // Is the file new or updated? isNewOrUpdated = await this.imageCache.IsNewOrUpdatedAsync().ConfigureAwait(false); cachedPath = this.imageCache.CachedPath; if (!isNewOrUpdated) { // The cached file is valid so just rewrite the path. this.imageCache.RewritePath(context); // Redirect if not a locally store file. if (!new Uri(cachedPath).IsFile) { context.ApplicationInstance.CompleteRequest(); } return; } } // Only process if the file has been updated. using (await Locker.WriterLockAsync(rawUrl).ConfigureAwait(false)) { // Ok let's get the image byte[] imageBuffer = null; string mimeType; try { if (currentService is IImageService2 imageService2) { imageBuffer = await imageService2.GetImage(requestPath, context).ConfigureAwait(false); } else { imageBuffer = await currentService.GetImage(requestPath).ConfigureAwait(false); } } catch (HttpException ex) { // We want 404's to be handled by IIS so that other handlers/modules can still run. if (ex.GetHttpCode() == (int)HttpStatusCode.NotFound) { ImageProcessorBootstrapper.Instance.Logger.Log <ImageProcessingModule>(ex.Message); return; } } if (imageBuffer == null) { return; } // Using recyclable streams here should dramatically reduce the overhead required using (MemoryStream inStream = MemoryStreamPool.Shared.GetStream("inStream", imageBuffer, 0, imageBuffer.Length)) { // Process the Image. Use a recyclable stream here to reduce the allocations MemoryStream outStream = MemoryStreamPool.Shared.GetStream(); if (!string.IsNullOrWhiteSpace(queryString)) { if (processing) { // Process the image. bool exif = preserveExifMetaData != null && preserveExifMetaData.Value; MetaDataMode metaMode = !exif ? MetaDataMode.None : metaDataMode.Value; bool gamma = fixGamma != null && fixGamma.Value; try { using (var imageFactory = new ImageFactory(metaMode, gamma) { AnimationProcessMode = mode }) { imageFactory.Load(inStream).AutoProcess(processors).Save(outStream); mimeType = imageFactory.CurrentImageFormat.MimeType; } } catch (ImageFormatException) { ImageProcessorBootstrapper.Instance.Logger.Log <ImageProcessingModule>($"Request {url} is not a valid image."); return; } } else { // We're cache-busting. Allow the value to be cached await inStream.CopyToAsync(outStream).ConfigureAwait(false); mimeType = FormatUtilities.GetFormat(outStream).MimeType; } } else { // We're capturing all requests. await inStream.CopyToAsync(outStream).ConfigureAwait(false); mimeType = FormatUtilities.GetFormat(outStream).MimeType; } // Fire the post processing event. EventHandler <PostProcessingEventArgs> handler = OnPostProcessing; if (handler != null) { string extension = Path.GetExtension(cachedPath); var args = new PostProcessingEventArgs { Context = context, ImageStream = outStream, ImageExtension = extension }; handler(this, args); outStream = args.ImageStream; } // Add to the cache. await this.imageCache.AddImageToCacheAsync(outStream, mimeType).ConfigureAwait(false); // Cleanup outStream.Dispose(); } // Store the response type and cache dependency in the context for later retrieval. context.Items[CachedResponseTypeKey] = mimeType; bool isFileCached = new Uri(cachedPath).IsFile; if (isFileLocal) { if (isFileCached) { // Some services might only provide filename so we can't monitor for the browser. context.Items[CachedResponseFileDependency] = Path.GetFileName(requestPath) == requestPath ? new[] { cachedPath } : new[] { requestPath, cachedPath }; } else { context.Items[CachedResponseFileDependency] = Path.GetFileName(requestPath) == requestPath ? null : new[] { requestPath }; } } else if (isFileCached) { context.Items[CachedResponseFileDependency] = new[] { cachedPath }; } // The cached file has been saved so now rewrite the path. this.imageCache.RewritePath(context); // Redirect if not a locally store file. if (!new Uri(cachedPath).IsFile) { context.ApplicationInstance.CompleteRequest(); } // Trim the cache. await this.imageCache.TrimCacheAsync().ConfigureAwait(false); } }
/// <summary> /// Gets an image resized using the linear color space with gamma correction adjustments. /// </summary> /// <param name="source">The source image.</param> /// <param name="width">The width to resize to.</param> /// <param name="height">The height to resize to.</param> /// <param name="destination">The destination rectangle.</param> /// <param name="animationProcessMode">The process mode for frames in animated images.</param> /// <returns> /// The <see cref="Bitmap"/>. /// </returns> protected virtual Bitmap ResizeLinear(Image source, int width, int height, Rectangle destination, AnimationProcessMode animationProcessMode) { // Adjust the gamma value so that the image is in the linear color space. Bitmap linear = Adjustments.ToLinear(source.Copy(animationProcessMode)); Bitmap resized = new Bitmap(width, height, PixelFormat.Format32bppPArgb); resized.SetResolution(source.HorizontalResolution, source.VerticalResolution); using (Graphics graphics = Graphics.FromImage(resized)) { // We want to use two different blending algorithms for enlargement/shrinking. if (source.Width < width || source.Height < height) { // We are making it larger. graphics.SmoothingMode = SmoothingMode.AntiAlias; } else { // We are making it smaller. graphics.SmoothingMode = SmoothingMode.None; } graphics.InterpolationMode = InterpolationMode.HighQualityBicubic; graphics.PixelOffsetMode = PixelOffsetMode.HighQuality; graphics.CompositingQuality = CompositingQuality.HighQuality; using (ImageAttributes attributes = new ImageAttributes()) { attributes.SetWrapMode(WrapMode.TileFlipXY); graphics.DrawImage(linear, destination, 0, 0, source.Width, source.Height, GraphicsUnit.Pixel, attributes); } } // Return to composite color space. resized = Adjustments.ToSRGB(resized); linear.Dispose(); return(resized); }
/// <summary> /// Processes the image. /// </summary> /// <param name="context"> /// the <see cref="T:System.Web.HttpContext">HttpContext</see> object that provides /// references to the intrinsic server objects /// </param> /// <returns> /// The <see cref="T:System.Threading.Tasks.Task"/>. /// </returns> private async Task ProcessImageAsync(HttpContext context) { HttpRequest request = context.Request; // Should we ignore this request? if (request.Unvalidated.RawUrl.ToUpperInvariant().Contains("IPIGNORE=TRUE")) { return; } IImageService currentService = this.GetImageServiceForRequest(request); if (currentService != null) { bool isFileLocal = currentService.IsFileLocalService; string url = request.Url.ToString(); bool isLegacy = ProtocolRegex.Matches(url).Count > 1; bool hasMultiParams = url.Count(f => f == '?') > 1; string requestPath; string queryString = string.Empty; string urlParameters = string.Empty; // Legacy support. I'd like to remove this asap. if (isLegacy && hasMultiParams) { // We need to split the querystring to get the actual values we want. string[] paths = url.Split('?'); requestPath = paths[1]; // Handle extension-less urls. if (paths.Length > 3) { queryString = paths[3]; urlParameters = paths[2]; } else if (paths.Length > 1) { queryString = paths[2]; } } else { if (string.IsNullOrWhiteSpace(currentService.Prefix)) { requestPath = currentService.IsFileLocalService ? HostingEnvironment.MapPath(request.Path) : request.Path; queryString = request.QueryString.ToString(); } else { // Parse any protocol values from settings. string protocol = currentService.Settings.ContainsKey("Protocol") ? currentService.Settings["Protocol"] + "://" : currentService.GetType() == typeof(RemoteImageService) ? request.Url.Scheme + "://" : string.Empty; // Handle requests that require parameters. if (hasMultiParams) { string[] paths = url.Split('?'); requestPath = protocol + request.Path.TrimStart('/').Remove(0, currentService.Prefix.Length).TrimStart('/') + "?" + paths[1]; queryString = paths[2]; } else { requestPath = protocol + request.Path.TrimStart('/').Remove(0, currentService.Prefix.Length).TrimStart('/'); queryString = request.QueryString.ToString(); } } } // Replace any presets in the querystring with the actual value. queryString = this.ReplacePresetsInQueryString(queryString); // Execute the handler which can change the querystring queryString = this.CheckQuerystringHandler(context, queryString, request.Unvalidated.RawUrl); // Break out if we don't meet critera. bool interceptAll = interceptAllRequests != null && interceptAllRequests.Value; if (string.IsNullOrWhiteSpace(requestPath) || (!interceptAll && string.IsNullOrWhiteSpace(queryString))) { return; } string parts = !string.IsNullOrWhiteSpace(urlParameters) ? "?" + urlParameters : string.Empty; string fullPath = string.Format("{0}{1}?{2}", requestPath, parts, queryString); object resourcePath; // More legacy support code. if (hasMultiParams) { resourcePath = string.IsNullOrWhiteSpace(urlParameters) ? new Uri(requestPath, UriKind.RelativeOrAbsolute) : new Uri(requestPath + "?" + urlParameters, UriKind.RelativeOrAbsolute); } else { resourcePath = requestPath; } // Check whether the path is valid for other requests. if (!currentService.IsValidRequest(resourcePath.ToString())) { return; } string combined = requestPath + fullPath + queryString; using (await Locker.LockAsync(combined)) { // Create a new cache to help process and cache the request. this.imageCache = (IImageCache)ImageProcessorConfiguration.Instance .ImageCache.GetInstance(requestPath, fullPath, queryString); // Is the file new or updated? bool isNewOrUpdated = await this.imageCache.IsNewOrUpdatedAsync(); string cachedPath = this.imageCache.CachedPath; // Only process if the file has been updated. if (isNewOrUpdated) { // Process the image. bool exif = preserveExifMetaData != null && preserveExifMetaData.Value; bool gamma = fixGamma != null && fixGamma.Value; AnimationProcessMode mode = this.ParseAnimationMode(queryString); using (ImageFactory imageFactory = new ImageFactory(exif, gamma) { AnimationProcessMode = mode }) { byte[] imageBuffer = null; string mimeType; try { imageBuffer = await currentService.GetImage(resourcePath); } catch (HttpException ex) { // We want 404's to be handled by IIS so that other handlers/modules can still run. if (ex.GetHttpCode() == (int)HttpStatusCode.NotFound) { return; } } if (imageBuffer == null) { return; } using (MemoryStream inStream = new MemoryStream(imageBuffer)) { // Process the Image MemoryStream outStream = new MemoryStream(); if (!string.IsNullOrWhiteSpace(queryString)) { imageFactory.Load(inStream).AutoProcess(queryString).Save(outStream); mimeType = imageFactory.CurrentImageFormat.MimeType; } else { await inStream.CopyToAsync(outStream); mimeType = FormatUtilities.GetFormat(outStream).MimeType; } // Fire the post processing event. EventHandler <PostProcessingEventArgs> handler = OnPostProcessing; if (handler != null) { string extension = Path.GetExtension(cachedPath); PostProcessingEventArgs args = new PostProcessingEventArgs { Context = context, ImageStream = outStream, ImageExtension = extension }; handler(this, args); outStream = args.ImageStream; } // Add to the cache. await this.imageCache.AddImageToCacheAsync(outStream, mimeType); // Cleanup outStream.Dispose(); } // Store the response type and cache dependency in the context for later retrieval. context.Items[CachedResponseTypeKey] = mimeType; bool isFileCached = new Uri(cachedPath).IsFile; if (isFileLocal) { if (isFileCached) { // Some services might only provide filename so we can't monitor for the browser. context.Items[CachedResponseFileDependency] = Path.GetFileName(requestPath) == requestPath ? new List <string> { cachedPath } : new List <string> { requestPath, cachedPath }; } else { context.Items[CachedResponseFileDependency] = Path.GetFileName(requestPath) == requestPath ? null : new List <string> { requestPath }; } } else if (isFileCached) { context.Items[CachedResponseFileDependency] = new List <string> { cachedPath }; } } } // The cached file is valid so just rewrite the path. this.imageCache.RewritePath(context); // Redirect if not a locally store file. if (!new Uri(cachedPath).IsFile) { context.ApplicationInstance.CompleteRequest(); } } } }
/// <summary> /// Gets an image resized using the linear color space with gamma correction adjustments. /// </summary> /// <param name="source">The source image.</param> /// <param name="width">The width to resize to.</param> /// <param name="height">The height to resize to.</param> /// <param name="destination">The destination rectangle.</param> /// <param name="animationProcessMode">The process mode for frames in animated images.</param> /// <returns> /// The <see cref="Bitmap"/>. /// </returns> protected virtual Bitmap ResizeLinear(Image source, int width, int height, Rectangle destination, AnimationProcessMode animationProcessMode) { // Adjust the gamma value so that the image is in the linear color space. var linear = Adjustments.ToLinear(source.Copy(animationProcessMode)); var resized = new Bitmap(width, height, PixelFormat.Format32bppPArgb); resized.SetResolution(source.HorizontalResolution, source.VerticalResolution); using (var graphics = Graphics.FromImage(resized)) { GraphicsHelper.SetGraphicsOptions(graphics); using (var attributes = new ImageAttributes()) { attributes.SetWrapMode(WrapMode.TileFlipXY); graphics.DrawImage(linear, destination, 0, 0, source.Width, source.Height, GraphicsUnit.Pixel, attributes); } } // Return to composite color space. resized = Adjustments.ToSRGB(resized); linear.Dispose(); return(resized); }
/// <summary> /// Gets an image resized using the linear color space with gamma correction adjustments. /// </summary> /// <param name="source">The source image.</param> /// <param name="width">The width to resize to.</param> /// <param name="height">The height to resize to.</param> /// <param name="destination">The destination rectangle.</param> /// <param name="animationProcessMode">The process mode for frames in animated images.</param> /// <returns> /// The <see cref="Bitmap"/>. /// </returns> protected virtual Bitmap ResizeLinear(Image source, int width, int height, Rectangle destination, AnimationProcessMode animationProcessMode) { // Adjust the gamma value so that the image is in the linear color space. Bitmap linear = Adjustments.ToLinear(source.Copy(animationProcessMode)); Bitmap resized = new Bitmap(width, height, PixelFormat.Format32bppPArgb); resized.SetResolution(source.HorizontalResolution, source.VerticalResolution); using (Graphics graphics = Graphics.FromImage(resized)) { // We want to use two different blending algorithms for enlargement/shrinking. if (source.Width < width || source.Height < height) { // We are making it larger. graphics.SmoothingMode = SmoothingMode.AntiAlias; } else { // We are making it smaller. graphics.SmoothingMode = SmoothingMode.None; } graphics.InterpolationMode = InterpolationMode.HighQualityBicubic; graphics.PixelOffsetMode = PixelOffsetMode.HighQuality; graphics.CompositingQuality = CompositingQuality.HighQuality; using (ImageAttributes attributes = new ImageAttributes()) { attributes.SetWrapMode(WrapMode.TileFlipXY); graphics.DrawImage(linear, destination, 0, 0, source.Width, source.Height, GraphicsUnit.Pixel, attributes); } } // Return to composite color space. resized = Adjustments.ToSRGB(resized); linear.Dispose(); return resized; }
/// <summary> /// Gets an image resized using the linear color space with gamma correction adjustments. /// </summary> /// <param name="source">The source image.</param> /// <param name="width">The width to resize to.</param> /// <param name="height">The height to resize to.</param> /// <param name="destination">The destination rectangle.</param> /// <param name="animationProcessMode">The process mode for frames in animated images.</param> /// <returns> /// The <see cref="Bitmap"/>. /// </returns> protected virtual Bitmap ResizeLinear(Image source, int width, int height, Rectangle destination, AnimationProcessMode animationProcessMode) { // Adjust the gamma value so that the image is in the linear color space. Bitmap linear = Adjustments.ToLinear(source.Copy(animationProcessMode)); Bitmap resized = new Bitmap(width, height, PixelFormat.Format32bppPArgb); resized.SetResolution(source.HorizontalResolution, source.VerticalResolution); using (Graphics graphics = Graphics.FromImage(resized)) { GraphicsHelper.SetGraphicsOptions(graphics); using (ImageAttributes attributes = new ImageAttributes()) { attributes.SetWrapMode(WrapMode.TileFlipXY); graphics.DrawImage(linear, destination, 0, 0, source.Width, source.Height, GraphicsUnit.Pixel, attributes); } } // Return to composite color space. resized = Adjustments.ToSRGB(resized); linear.Dispose(); return resized; }
/// <summary> /// Processes the image. /// </summary> /// <param name="context"> /// the <see cref="T:System.Web.HttpContext">HttpContext</see> object that provides /// references to the intrinsic server objects /// </param> /// <returns> /// The <see cref="T:System.Threading.Tasks.Task"/>. /// </returns> private async Task ProcessImageAsync(HttpContext context) { HttpRequest request = context.Request; string rawUrl = request.Unvalidated.RawUrl; // Should we ignore this request? if (string.IsNullOrWhiteSpace(rawUrl) || rawUrl.ToUpperInvariant().Contains("IPIGNORE=TRUE")) { return; } // Sometimes the request is url encoded so we have to decode. // See https://github.com/JimBobSquarePants/ImageProcessor/issues/478 // This causes a bit of a nightmare as the incoming request is corrupted and cannot be used for splitting // out each url part. This becomes a manual job. string url = this.DecodeUrlString(rawUrl); string applicationPath = request.ApplicationPath; IImageService currentService = this.GetImageServiceForRequest(url, applicationPath); if (currentService != null) { // Remove any service identifier prefixes from the url. string prefix = currentService.Prefix; if (!string.IsNullOrWhiteSpace(prefix)) { url = url.Split(new[] { prefix }, StringSplitOptions.None)[1].TrimStart("?"); } // Identify each part of the incoming request. int queryCount = url.Count(f => f == '?'); bool hasParams = queryCount > 0; bool hasMultiParams = queryCount > 1; string[] splitPath = url.Split('?'); // Ensure we include any relevent querystring parameters into our request path for third party requests. string requestPath = hasMultiParams ? string.Join("?", splitPath.Take(splitPath.Length - 1)) : splitPath[0]; string queryString = hasParams ? splitPath[splitPath.Length - 1] : string.Empty; // Map the request path if file local. bool isFileLocal = currentService.IsFileLocalService; if (currentService.IsFileLocalService) { requestPath = HostingEnvironment.MapPath(requestPath); } // Parse any protocol values from settings if no protocol is present. if (currentService.Settings.ContainsKey("Protocol") && (ProtocolRegex.Matches(url).Count == 0 || ProtocolRegex.Matches(url)[0].Index > 0)) { // ReSharper disable once PossibleNullReferenceException requestPath = currentService.Settings["Protocol"] + "://" + requestPath.TrimStart('/'); } // Replace any presets in the querystring with the actual value. queryString = this.ReplacePresetsInQueryString(queryString); HttpContextWrapper httpContextBase = new HttpContextWrapper(context); // Execute the handler which can change the querystring // LEGACY: #pragma warning disable 618 queryString = this.CheckQuerystringHandler(context, queryString, rawUrl); #pragma warning restore 618 // NEW WAY: ValidatingRequestEventArgs validatingArgs = new ValidatingRequestEventArgs(httpContextBase, queryString); this.OnValidatingRequest(validatingArgs); // If the validation has failed based on events, return if (validatingArgs.Cancel) { ImageProcessorBootstrapper.Instance.Logger.Log <ImageProcessingModule>("Image processing has been cancelled by an event"); return; } // Re-assign based on event handlers queryString = validatingArgs.QueryString; // Break out if we don't meet critera. bool interceptAll = interceptAllRequests != null && interceptAllRequests.Value; if (string.IsNullOrWhiteSpace(requestPath) || (!interceptAll && string.IsNullOrWhiteSpace(queryString))) { return; } // Check whether the path is valid for other requests. // We've already checked the unprefixed requests in GetImageServiceForRequest(). if (!string.IsNullOrWhiteSpace(prefix) && !currentService.IsValidRequest(requestPath)) { return; } using (await Locker.LockAsync(rawUrl)) { // Create a new cache to help process and cache the request. this.imageCache = (IImageCache)ImageProcessorConfiguration.Instance .ImageCache.GetInstance(requestPath, url, queryString); // Is the file new or updated? bool isNewOrUpdated = await this.imageCache.IsNewOrUpdatedAsync(); string cachedPath = this.imageCache.CachedPath; // Only process if the file has been updated. if (isNewOrUpdated) { byte[] imageBuffer = null; string mimeType; try { imageBuffer = await currentService.GetImage(requestPath); } catch (HttpException ex) { // We want 404's to be handled by IIS so that other handlers/modules can still run. if (ex.GetHttpCode() == (int)HttpStatusCode.NotFound) { ImageProcessorBootstrapper.Instance.Logger.Log <ImageProcessingModule>(ex.Message); return; } } if (imageBuffer == null) { return; } using (MemoryStream inStream = new MemoryStream(imageBuffer)) { // Process the Image MemoryStream outStream = new MemoryStream(); if (!string.IsNullOrWhiteSpace(queryString)) { // Animation is not a processor but can be a specific request so we should allow it. bool processAnimation; AnimationProcessMode mode = this.ParseAnimationMode(queryString, out processAnimation); // Attempt to match querystring and processors. IWebGraphicsProcessor[] processors = ImageFactoryExtensions.GetMatchingProcessors(queryString); if (processors.Any() || processAnimation) { // Process the image. bool exif = preserveExifMetaData != null && preserveExifMetaData.Value; bool gamma = fixGamma != null && fixGamma.Value; using (ImageFactory imageFactory = new ImageFactory(exif, gamma) { AnimationProcessMode = mode }) { imageFactory.Load(inStream).AutoProcess(processors).Save(outStream); mimeType = imageFactory.CurrentImageFormat.MimeType; } } else if (this.ParseCacheBuster(queryString)) { // We're cachebustng. Allow the value to be cached await inStream.CopyToAsync(outStream); mimeType = FormatUtilities.GetFormat(outStream).MimeType; } else { // No match? Someone is either attacking the server or hasn't read the instructions. // Either way throw an exception to prevent caching. string message = $"The request {request.Unvalidated.RawUrl} could not be understood by the server due to malformed syntax."; ImageProcessorBootstrapper.Instance.Logger.Log <ImageProcessingModule>(message); throw new HttpException((int)HttpStatusCode.BadRequest, message); } } else { // We're capturing all requests. await inStream.CopyToAsync(outStream); mimeType = FormatUtilities.GetFormat(outStream).MimeType; } // Fire the post processing event. EventHandler <PostProcessingEventArgs> handler = OnPostProcessing; if (handler != null) { string extension = Path.GetExtension(cachedPath); PostProcessingEventArgs args = new PostProcessingEventArgs { Context = context, ImageStream = outStream, ImageExtension = extension }; handler(this, args); outStream = args.ImageStream; } // Add to the cache. await this.imageCache.AddImageToCacheAsync(outStream, mimeType); // Cleanup outStream.Dispose(); } // Store the response type and cache dependency in the context for later retrieval. context.Items[CachedResponseTypeKey] = mimeType; bool isFileCached = new Uri(cachedPath).IsFile; if (isFileLocal) { if (isFileCached) { // Some services might only provide filename so we can't monitor for the browser. context.Items[CachedResponseFileDependency] = Path.GetFileName(requestPath) == requestPath ? new[] { cachedPath } : new[] { requestPath, cachedPath }; } else { context.Items[CachedResponseFileDependency] = Path.GetFileName(requestPath) == requestPath ? null : new[] { requestPath }; } } else if (isFileCached) { context.Items[CachedResponseFileDependency] = new[] { cachedPath }; } } // The cached file is valid so just rewrite the path. this.imageCache.RewritePath(context); // Redirect if not a locally store file. if (!new Uri(cachedPath).IsFile) { context.ApplicationInstance.CompleteRequest(); } } } }