From da37031cf8dd4eb0b5c85c67e9206e6bbcf36d39 Mon Sep 17 00:00:00 2001 From: James South Date: Tue, 21 Apr 2015 21:35:18 +0100 Subject: [PATCH] Fix constructor and add cors support. Former-commit-id: bb6daf831ba9b3efeeef392cf7b7eb53c7358030 Former-commit-id: 568e90c5570902dd0a2467960d91cf69d8036629 Former-commit-id: 24eaa11c52350199f263d023c763e9abb600bede --- build/NuSpecs/ImageProcessor.nuspec | 2 +- build/build.xml | 2 +- src/ImageProcessor.Playground/Program.cs | 150 +++++----- .../images/input/blue-balloon.jpg | 3 + .../Imaging/ColorUnitTests.cs | 1 - .../Imaging/Helpers/ImageMathsUnitTests.cs | 12 +- .../ImageProcessorConfiguration.cs | 24 +- .../Configuration/ImageSecuritySection.cs | 37 ++- .../HttpModules/ImageProcessingModule.cs | 102 +++++-- src/ImageProcessor.sln.DotSettings | 1 + src/ImageProcessor/ImageFactory.cs | 19 +- src/ImageProcessor/ImageProcessor.csproj | 1 + .../Imaging/Colors/YCbCrColor.cs | 19 +- .../Imaging/Helpers/Adjustments.cs | 43 ++- .../Imaging/Helpers/ImageMaths.cs | 35 +++ .../Imaging/Helpers/PixelOperations.cs | 58 ++++ src/ImageProcessor/Imaging/TextLayer.cs | 16 +- .../Processors/Halftone - Copy.cs | 262 ------------------ src/ImageProcessor/Processors/ReplaceColor.cs | 32 ++- src/ImageProcessor/Processors/Watermark.cs | 60 +++- src/ImageProcessor/Properties/AssemblyInfo.cs | 4 +- 21 files changed, 488 insertions(+), 395 deletions(-) create mode 100644 src/ImageProcessor.Playground/images/input/blue-balloon.jpg create mode 100644 src/ImageProcessor/Imaging/Helpers/PixelOperations.cs delete mode 100644 src/ImageProcessor/Processors/Halftone - Copy.cs diff --git a/build/NuSpecs/ImageProcessor.nuspec b/build/NuSpecs/ImageProcessor.nuspec index f73d4178d..f5eb45e28 100644 --- a/build/NuSpecs/ImageProcessor.nuspec +++ b/build/NuSpecs/ImageProcessor.nuspec @@ -2,7 +2,7 @@ ImageProcessor - 2.2.1.0 + 2.2.2.0 ImageProcessor James South James South diff --git a/build/build.xml b/build/build.xml index b6f8e05df..b1b3027ac 100644 --- a/build/build.xml +++ b/build/build.xml @@ -1,7 +1,7 @@ ImageProcessor - 2.2.1.0 + 2.2.2.0 ..\src\ImageProcessor ImageProcessor.csproj diff --git a/src/ImageProcessor.Playground/Program.cs b/src/ImageProcessor.Playground/Program.cs index 4975a3f03..590ad5989 100644 --- a/src/ImageProcessor.Playground/Program.cs +++ b/src/ImageProcessor.Playground/Program.cs @@ -61,7 +61,9 @@ namespace ImageProcessor.PlayGround //FileInfo fileInfo = new FileInfo(Path.Combine(resolvedPath, "gamma_dalai_lama_gray.jpg")); //FileInfo fileInfo = new FileInfo(Path.Combine(resolvedPath, "Arc-de-Triomphe-France.jpg")); //FileInfo fileInfo = new FileInfo(Path.Combine(resolvedPath, "Martin-Schoeller-Jack-Nicholson-Portrait.jpeg")); + //FileInfo fileInfo = new FileInfo(Path.Combine(resolvedPath, "night-bridge.png")); //FileInfo fileInfo = new FileInfo(Path.Combine(resolvedPath, "tree.jpg")); + //FileInfo fileInfo = new FileInfo(Path.Combine(resolvedPath, "blue-balloon.jpg")); //FileInfo fileInfo = new FileInfo(Path.Combine(resolvedPath, "test2.png")); //FileInfo fileInfo = new FileInfo(Path.Combine(resolvedPath, "120430.gif")); //FileInfo fileInfo = new FileInfo(Path.Combine(resolvedPath, "rickroll.original.gif")); @@ -70,9 +72,9 @@ namespace ImageProcessor.PlayGround //FileInfo fileInfo = new FileInfo(Path.Combine(resolvedPath, "cmyk.png")); //IEnumerable files = GetFilesByExtensions(di, ".gif"); //IEnumerable files = GetFilesByExtensions(di, ".png", ".jpg", ".jpeg"); - //IEnumerable files = GetFilesByExtensions(di, ".jpg", ".jpeg", ".jfif"); + IEnumerable files = GetFilesByExtensions(di, ".jpg", ".jpeg", ".jfif"); //IEnumerable files = GetFilesByExtensions(di, ".png"); - IEnumerable files = GetFilesByExtensions(di, ".gif", ".webp", ".bmp", ".jpg", ".png"); + //IEnumerable files = GetFilesByExtensions(di, ".gif", ".webp", ".bmp", ".jpg", ".png"); foreach (FileInfo fileInfo in files) { @@ -81,79 +83,91 @@ namespace ImageProcessor.PlayGround continue; } - byte[] photoBytes = File.ReadAllBytes(fileInfo.FullName); - Console.WriteLine("Processing: " + fileInfo.Name); + byte[] photoBytes = File.ReadAllBytes(fileInfo.FullName); + Console.WriteLine("Processing: " + fileInfo.Name); - Stopwatch stopwatch = new Stopwatch(); - stopwatch.Start(); + Stopwatch stopwatch = new Stopwatch(); + stopwatch.Start(); - // ImageProcessor - using (MemoryStream inStream = new MemoryStream(photoBytes)) + // ImageProcessor + using (MemoryStream inStream = new MemoryStream(photoBytes)) + { + using (ImageFactory imageFactory = new ImageFactory(true, true)) { - using (ImageFactory imageFactory = new ImageFactory(true, true)) - { - Size size = new Size(600, 0); - //CropLayer cropLayer = new CropLayer(20, 20, 20, 20, ImageProcessor.Imaging.CropMode.Percentage); - //ResizeLayer layer = new ResizeLayer(size, ResizeMode.Max, AnchorPosition.Center, false); - - //ContentAwareResizeLayer layer = new ContentAwareResizeLayer(size) - //{ - // ConvolutionType = ConvolutionType.Sobel - //}; - // Load, resize, set the format and quality and save an image. - imageFactory.Load(inStream) - //.DetectObjects(EmbeddedHaarCascades.FrontFaceDefault) - //.Overlay(new ImageLayer - // { - // Image = overlay, - // Opacity = 50 - // }) - //.Alpha(50) - //.BackgroundColor(Color.White) - //.Resize(new Size((int)(size.Width * 1.1), 0)) - //.ContentAwareResize(layer) - //.Constrain(size) - //.Mask(mask) - //.Format(new PngFormat()) - //.BackgroundColor(Color.Cyan) - //.ReplaceColor(Color.FromArgb(255, 223, 224), Color.FromArgb(121, 188, 255), 128) - //.GaussianSharpen(3) - //.Saturation(20) - //.Resize(size) - //.Resize(new ResizeLayer(size, ResizeMode.Max)) - // .Resize(new ResizeLayer(size, ResizeMode.Stretch)) - //.DetectEdges(new SobelEdgeFilter(), true) - //.DetectEdges(new LaplacianOfGaussianEdgeFilter()) - //.GaussianBlur(new GaussianLayer(10, 11)) - //.EntropyCrop() - .Halftone() - //.RotateBounded(150, false) - //.Crop(cropLayer) - //.Rotate(140) - //.Filter(MatrixFilters.Invert) - //.Brightness(-5) - //.Contrast(50) - //.Filter(MatrixFilters.Comic) - //.Flip() - //.Filter(MatrixFilters.HiSatch) - //.Pixelate(8) - //.GaussianSharpen(10) - //.Format(new PngFormat() { IsIndexed = true }) - //.Format(new PngFormat() ) - .Save(Path.GetFullPath(Path.Combine(Path.GetDirectoryName(path), @"..\..\images\output", fileInfo.Name))); - //.Save(Path.GetFullPath(Path.Combine(Path.GetDirectoryName(path), @"..\..\images\output", Path.GetFileNameWithoutExtension(fileInfo.Name) + ".png"))); - - stopwatch.Stop(); - } + Size size = new Size(600, 0); + //CropLayer cropLayer = new CropLayer(20, 20, 20, 20, ImageProcessor.Imaging.CropMode.Percentage); + //ResizeLayer layer = new ResizeLayer(size, ResizeMode.Max, AnchorPosition.Center, false); + // TextLayer textLayer = new TextLayer() + //{ + // Text = "هناك حقيقة مثبتة منذ زمن", + // FontColor = Color.White, + // DropShadow = true, + // Vertical = true, + // //RightToLeft = true, + // //Position = new Point(5, 5) + + //}; + + //ContentAwareResizeLayer layer = new ContentAwareResizeLayer(size) + //{ + // ConvolutionType = ConvolutionType.Sobel + //}; + // Load, resize, set the format and quality and save an image. + imageFactory.Load(inStream) + //.DetectObjects(EmbeddedHaarCascades.FrontFaceDefault) + //.Overlay(new ImageLayer + // { + // Image = overlay, + // Opacity = 50 + // }) + //.Alpha(50) + //.BackgroundColor(Color.White) + //.Resize(new Size((int)(size.Width * 1.1), 0)) + //.ContentAwareResize(layer) + //.Constrain(size) + //.Mask(mask) + //.Format(new PngFormat()) + //.BackgroundColor(Color.Cyan) + //.Watermark(textLayer) + //.ReplaceColor(Color.FromArgb(93, 136, 231), Color.FromArgb(94, 134, 78), 50) + //.GaussianSharpen(3) + //.Saturation(20) + //.Resize(size) + //.Resize(new ResizeLayer(size, ResizeMode.Max)) + // .Resize(new ResizeLayer(size, ResizeMode.Stretch)) + //.DetectEdges(new SobelEdgeFilter(), true) + //.DetectEdges(new LaplacianOfGaussianEdgeFilter()) + //.GaussianBlur(new GaussianLayer(10, 11)) + //.EntropyCrop() + //.Gamma(2.2F) + //.Halftone() + //.RotateBounded(150, false) + //.Crop(cropLayer) + //.Rotate(140) + //.Filter(MatrixFilters.Invert) + //.Brightness(-5) + //.Contrast(50) + .Filter(MatrixFilters.Comic) + //.Flip() + //.Filter(MatrixFilters.HiSatch) + //.Pixelate(8) + //.GaussianSharpen(10) + //.Format(new PngFormat() { IsIndexed = true }) + //.Format(new PngFormat() ) + .Save(Path.GetFullPath(Path.Combine(Path.GetDirectoryName(path), @"..\..\images\output", fileInfo.Name))); + //.Save(Path.GetFullPath(Path.Combine(Path.GetDirectoryName(path), @"..\..\images\output", Path.GetFileNameWithoutExtension(fileInfo.Name) + ".png"))); + + stopwatch.Stop(); } + } - long peakWorkingSet64 = Process.GetCurrentProcess().PeakWorkingSet64; - float mB = peakWorkingSet64 / (float)1024 / 1024; + long peakWorkingSet64 = Process.GetCurrentProcess().PeakWorkingSet64; + float mB = peakWorkingSet64 / (float)1024 / 1024; - Console.WriteLine(@"Completed {0} in {1:s\.fff} secs {2}Peak memory usage was {3} bytes or {4} Mb.", fileInfo.Name, stopwatch.Elapsed, Environment.NewLine, peakWorkingSet64.ToString("#,#"), mB); + Console.WriteLine(@"Completed {0} in {1:s\.fff} secs {2}Peak memory usage was {3} bytes or {4} Mb.", fileInfo.Name, stopwatch.Elapsed, Environment.NewLine, peakWorkingSet64.ToString("#,#"), mB); - //Console.WriteLine("Processed: " + fileInfo.Name + " in " + stopwatch.ElapsedMilliseconds + "ms"); - } + //Console.WriteLine("Processed: " + fileInfo.Name + " in " + stopwatch.ElapsedMilliseconds + "ms"); + } Console.ReadLine(); } diff --git a/src/ImageProcessor.Playground/images/input/blue-balloon.jpg b/src/ImageProcessor.Playground/images/input/blue-balloon.jpg new file mode 100644 index 000000000..3d95d80b9 --- /dev/null +++ b/src/ImageProcessor.Playground/images/input/blue-balloon.jpg @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:a83d4e0e313a749cb054c61b3c6f9dcd63546e2880e2221f23e39865ba3bed4b +size 59460 diff --git a/src/ImageProcessor.UnitTests/Imaging/ColorUnitTests.cs b/src/ImageProcessor.UnitTests/Imaging/ColorUnitTests.cs index 04b5e3d12..9370142a8 100644 --- a/src/ImageProcessor.UnitTests/Imaging/ColorUnitTests.cs +++ b/src/ImageProcessor.UnitTests/Imaging/ColorUnitTests.cs @@ -132,7 +132,6 @@ namespace ImageProcessor.UnitTests.Imaging Debug.Print(cmykColor.ToString()); - string result = ColorTranslator.ToHtml(cmykColor); result.Should().Be(expected); diff --git a/src/ImageProcessor.UnitTests/Imaging/Helpers/ImageMathsUnitTests.cs b/src/ImageProcessor.UnitTests/Imaging/Helpers/ImageMathsUnitTests.cs index aa20d9b51..18e592ede 100644 --- a/src/ImageProcessor.UnitTests/Imaging/Helpers/ImageMathsUnitTests.cs +++ b/src/ImageProcessor.UnitTests/Imaging/Helpers/ImageMathsUnitTests.cs @@ -1,4 +1,14 @@ -namespace ImageProcessor.UnitTests.Imaging.Helpers +// -------------------------------------------------------------------------------------------------------------------- +// +// Copyright (c) James South. +// Licensed under the Apache License, Version 2.0. +// +// +// Test harness for the image math unit tests +// +// -------------------------------------------------------------------------------------------------------------------- + +namespace ImageProcessor.UnitTests.Imaging.Helpers { using System.Drawing; using FluentAssertions; diff --git a/src/ImageProcessor.Web/Configuration/ImageProcessorConfiguration.cs b/src/ImageProcessor.Web/Configuration/ImageProcessorConfiguration.cs index 4d8029137..06ef9434d 100644 --- a/src/ImageProcessor.Web/Configuration/ImageProcessorConfiguration.cs +++ b/src/ImageProcessor.Web/Configuration/ImageProcessorConfiguration.cs @@ -144,6 +144,15 @@ namespace ImageProcessor.Web.Configuration }); } + /// + /// Retrieves the security configuration section from the current application configuration. + /// + /// The security configuration section from the current application configuration. + internal ImageSecuritySection GetImageSecuritySection() + { + return imageSecuritySection ?? (imageSecuritySection = ImageSecuritySection.GetConfiguration()); + } + /// /// Retrieves the processing configuration section from the current application configuration. /// @@ -162,15 +171,6 @@ namespace ImageProcessor.Web.Configuration return imageCacheSection ?? (imageCacheSection = ImageCacheSection.GetConfiguration()); } - /// - /// Retrieves the security configuration section from the current application configuration. - /// - /// The security configuration section from the current application configuration. - private static ImageSecuritySection GetImageSecuritySection() - { - return imageSecuritySection ?? (imageSecuritySection = ImageSecuritySection.GetConfiguration()); - } - #region GraphicesProcessors /// /// Gets the list of available GraphicsProcessors. @@ -282,7 +282,7 @@ namespace ImageProcessor.Web.Configuration { if (this.ImageServices == null) { - if (GetImageSecuritySection().AutoLoadServices) + if (this.GetImageSecuritySection().AutoLoadServices) { Type type = typeof(IImageService); try @@ -368,7 +368,7 @@ namespace ImageProcessor.Web.Configuration /// private Dictionary GetServiceSettings(string name) { - ImageSecuritySection.ServiceElement serviceElement = GetImageSecuritySection() + ImageSecuritySection.ServiceElement serviceElement = this.GetImageSecuritySection() .ImageServices .Cast() .FirstOrDefault(x => x.Name == name); @@ -400,7 +400,7 @@ namespace ImageProcessor.Web.Configuration /// private Uri[] GetServiceWhitelist(string name) { - ImageSecuritySection.ServiceElement serviceElement = GetImageSecuritySection() + ImageSecuritySection.ServiceElement serviceElement = this.GetImageSecuritySection() .ImageServices .Cast() .FirstOrDefault(x => x.Name == name); diff --git a/src/ImageProcessor.Web/Configuration/ImageSecuritySection.cs b/src/ImageProcessor.Web/Configuration/ImageSecuritySection.cs index 28644ee65..9cceb317a 100644 --- a/src/ImageProcessor.Web/Configuration/ImageSecuritySection.cs +++ b/src/ImageProcessor.Web/Configuration/ImageSecuritySection.cs @@ -22,6 +22,20 @@ namespace ImageProcessor.Web.Configuration /// public sealed class ImageSecuritySection : ConfigurationSection { + /// + /// Gets the + /// + /// The + [ConfigurationProperty("cors", IsRequired = false)] + public CORSOriginElement CORSOrigin + { + get + { + object o = this["cors"]; + return o as CORSOriginElement; + } + } + /// /// Gets the /// @@ -213,6 +227,27 @@ namespace ImageProcessor.Web.Configuration } } + /// + /// Represents a CORSOriginsElement configuration element within the configuration. + /// + public class CORSOriginElement : ConfigurationElement + { + /// + /// Gets the . + /// + /// + /// The . + /// + [ConfigurationProperty("whitelist", IsRequired = false)] + public WhiteListElementCollection WhiteList + { + get + { + return this["whitelist"] as WhiteListElementCollection; + } + } + } + /// /// Represents a whitelist collection configuration element within the configuration. /// @@ -275,7 +310,7 @@ namespace ImageProcessor.Web.Configuration [ConfigurationProperty("url", DefaultValue = "", IsRequired = true)] public Uri Url { - get { return (Uri)this["url"]; } + get { return new Uri(this["url"].ToString(), UriKind.RelativeOrAbsolute); } set { this["url"] = value; } } diff --git a/src/ImageProcessor.Web/HttpModules/ImageProcessingModule.cs b/src/ImageProcessor.Web/HttpModules/ImageProcessingModule.cs index 990ab2eb5..231cd7b23 100644 --- a/src/ImageProcessor.Web/HttpModules/ImageProcessingModule.cs +++ b/src/ImageProcessor.Web/HttpModules/ImageProcessingModule.cs @@ -307,7 +307,7 @@ namespace ImageProcessor.Web.HttpModules string url = request.Url.ToString(); bool isLegacy = ProtocolRegex.Matches(url).Count > 1; bool hasMultiParams = url.Count(f => f == '?') > 1; - string requestPath = string.Empty; + string requestPath; string queryString = string.Empty; string urlParameters = string.Empty; @@ -316,28 +316,18 @@ namespace ImageProcessor.Web.HttpModules { // We need to split the querystring to get the actual values we want. string[] paths = url.Split('?'); + requestPath = paths[1]; - //if (!string.IsNullOrWhiteSpace(multiQuery)) - //{ - //// UrlDecode seems to mess up in some circumstance. - //if (multiQuery.IndexOf("://", StringComparison.OrdinalIgnoreCase) == -1) - //{ - // multiQuery = multiQuery.Replace(":/", "://"); - //} - - 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]; - } - //} + // Handle extension-less urls. + if (paths.Length > 3) + { + queryString = paths[3]; + urlParameters = paths[2]; + } + else if (paths.Length > 1) + { + queryString = paths[2]; + } } else { @@ -383,6 +373,11 @@ namespace ImageProcessor.Web.HttpModules return; } + if (string.IsNullOrWhiteSpace(requestPath)) + { + return; + } + string parts = !string.IsNullOrWhiteSpace(urlParameters) ? "?" + urlParameters : string.Empty; string fullPath = string.Format("{0}{1}?{2}", requestPath, parts, queryString); object resourcePath; @@ -400,7 +395,7 @@ namespace ImageProcessor.Web.HttpModules } // Check whether the path is valid for other requests. - if (resourcePath == null || !currentService.IsValidRequest(resourcePath.ToString())) + if (!currentService.IsValidRequest(resourcePath.ToString())) { return; } @@ -522,6 +517,16 @@ namespace ImageProcessor.Web.HttpModules cache.SetRevalidation(HttpCacheRevalidation.AllCaches); this.imageCache = null; + + if (!string.IsNullOrEmpty(context.Request.Headers["Origin"])) + { + string origin = context.Request.Headers["Origin"]; + + if (this.IsValidOriginRequest(origin)) + { + response.AddHeader("Access-Control-Allow-Origin", origin); + } + } } } @@ -611,6 +616,57 @@ namespace ImageProcessor.Web.HttpModules // Return the file based service return services.FirstOrDefault(s => string.IsNullOrWhiteSpace(s.Prefix) && s.IsValidRequest(path)); } + + /// + /// Gets a value indicating whether the current origin request passes sanitizing rules. + /// + /// + /// The image path. + /// + /// + /// True if the request is valid; otherwise, False. + /// + private bool IsValidOriginRequest(string path) + { + ImageSecuritySection.CORSOriginElement origins = + ImageProcessorConfiguration.Instance.GetImageSecuritySection().CORSOrigin; + + if (origins == null || origins.WhiteList == null) + { + return false; + } + + // Check the url is from a whitelisted location. + Uri url = new Uri(path); + string upper = url.Host.ToUpperInvariant(); + + // Check for root or sub domain. + bool validUrl = false; + foreach (Uri uri in origins.WhiteList) + { + if (uri.ToString() == "*") + { + return true; + } + + if (!uri.IsAbsoluteUri) + { + Uri rebaseUri = new Uri("http://" + uri.ToString().TrimStart('.', '/')); + validUrl = upper.StartsWith(rebaseUri.Host.ToUpperInvariant()) || upper.EndsWith(rebaseUri.Host.ToUpperInvariant()); + } + else + { + validUrl = upper.StartsWith(uri.Host.ToUpperInvariant()) || upper.EndsWith(uri.Host.ToUpperInvariant()); + } + + if (validUrl) + { + break; + } + } + + return validUrl; + } #endregion } } \ No newline at end of file diff --git a/src/ImageProcessor.sln.DotSettings b/src/ImageProcessor.sln.DotSettings index d8fb7528a..6ab368df9 100644 --- a/src/ImageProcessor.sln.DotSettings +++ b/src/ImageProcessor.sln.DotSettings @@ -1,6 +1,7 @@  BGRA BPP + CORS DT FPX FR diff --git a/src/ImageProcessor/ImageFactory.cs b/src/ImageProcessor/ImageFactory.cs index 73ef61e74..4c18e0e3d 100644 --- a/src/ImageProcessor/ImageFactory.cs +++ b/src/ImageProcessor/ImageFactory.cs @@ -59,6 +59,17 @@ namespace ImageProcessor #endregion #region Constructors + /// + /// Initializes a new instance of the class. + /// + /// + /// Whether to preserve exif metadata. Defaults to false. + /// + public ImageFactory(bool preserveExifData = false) + : this(preserveExifData, true) + { + } + /// /// Initializes a new instance of the class. /// @@ -68,7 +79,7 @@ namespace ImageProcessor /// /// Whether to fix the gamma component of the image. Defaults to true. /// - public ImageFactory(bool preserveExifData = false, bool fixGamma = true) + public ImageFactory(bool preserveExifData, bool fixGamma) { this.PreserveExifData = preserveExifData; this.ExifPropertyItems = new ConcurrentDictionary(); @@ -122,6 +133,11 @@ namespace ImageProcessor /// public bool FixGamma { get; set; } + /// + /// Gets or the current gamma value. + /// + public float CurrentGamma { get; private set; } + /// /// Gets or sets the exif property items. /// @@ -635,6 +651,7 @@ namespace ImageProcessor value = 2.2F; } + this.CurrentGamma = value; Gamma gamma = new Gamma { DynamicParameter = value }; this.CurrentImageFormat.ApplyProcessor(gamma.ProcessImage, this); } diff --git a/src/ImageProcessor/ImageProcessor.csproj b/src/ImageProcessor/ImageProcessor.csproj index e1a7814a9..998a82514 100644 --- a/src/ImageProcessor/ImageProcessor.csproj +++ b/src/ImageProcessor/ImageProcessor.csproj @@ -169,6 +169,7 @@ + diff --git a/src/ImageProcessor/Imaging/Colors/YCbCrColor.cs b/src/ImageProcessor/Imaging/Colors/YCbCrColor.cs index 7cb2c236f..0035ea755 100644 --- a/src/ImageProcessor/Imaging/Colors/YCbCrColor.cs +++ b/src/ImageProcessor/Imaging/Colors/YCbCrColor.cs @@ -51,8 +51,8 @@ namespace ImageProcessor.Imaging.Colors private YCbCrColor(float y, float cb, float cr) { this.y = ImageMaths.Clamp(y, 0, 255); - this.cb = ImageMaths.Clamp(cb, -255, 255); - this.cr = ImageMaths.Clamp(cr, -255, 255); + this.cb = ImageMaths.Clamp(cb, 0, 255); + this.cr = ImageMaths.Clamp(cr, 0, 255); } /// @@ -69,7 +69,7 @@ namespace ImageProcessor.Imaging.Colors /// /// Gets the U chroma component. - /// A value ranging between -255 and 255. + /// A value ranging between 0 and 255. /// public float Cb { @@ -81,7 +81,7 @@ namespace ImageProcessor.Imaging.Colors /// /// Gets the V chroma component. - /// A value ranging between -255 and 255. + /// A value ranging between 0 and 255. /// public float Cr { @@ -189,9 +189,13 @@ namespace ImageProcessor.Imaging.Colors float cb = ycbcrColor.Cb - 128; float cr = ycbcrColor.Cr - 128; - byte r = Convert.ToByte(Math.Max(0.0f, Math.Min(255f, y + (1.402 * cr)))); - byte g = Convert.ToByte(Math.Max(0.0f, Math.Min(255f, y - (0.34414 * cb) - (0.71414 * cr)))); - byte b = Convert.ToByte(Math.Max(0.0f, Math.Min(255f, y + (1.772 * cb)))); + //byte r = Convert.ToByte(Math.Max(0.0f, Math.Min(255f, y + (1.402 * cr)))); + //byte g = Convert.ToByte(Math.Max(0.0f, Math.Min(255f, y - (0.34414 * cb) - (0.71414 * cr)))); + //byte b = Convert.ToByte(Math.Max(0.0f, Math.Min(255f, y + (1.772 * cb)))); + + byte r = Convert.ToByte(ImageMaths.Clamp(y + (1.402 * cr), 0, 255)); + byte g = Convert.ToByte(ImageMaths.Clamp(y - (0.34414 * cb) - (0.71414 * cr), 0, 255)); + byte b = Convert.ToByte(ImageMaths.Clamp(y + (1.772 * cb), 0, 255)); return Color.FromArgb(255, r, g, b); } @@ -226,7 +230,6 @@ namespace ImageProcessor.Imaging.Colors return HslaColor.FromColor(ycbcrColor); } - /// /// Allows the implicit conversion of an instance of to a /// . diff --git a/src/ImageProcessor/Imaging/Helpers/Adjustments.cs b/src/ImageProcessor/Imaging/Helpers/Adjustments.cs index fd4812bcd..cb583bd47 100644 --- a/src/ImageProcessor/Imaging/Helpers/Adjustments.cs +++ b/src/ImageProcessor/Imaging/Helpers/Adjustments.cs @@ -15,6 +15,8 @@ namespace ImageProcessor.Imaging.Helpers using System.Drawing.Imaging; using System.Threading.Tasks; + using ImageProcessor.Common.Extensions; + /// /// Provides reusable adjustment methods to apply to images. /// @@ -202,16 +204,47 @@ namespace ImageProcessor.Imaging.Helpers Bitmap destination = new Bitmap(width, height); destination.SetResolution(source.HorizontalResolution, source.VerticalResolution); - Rectangle rectangle = new Rectangle(0, 0, width, height); - using (Graphics graphics = Graphics.FromImage(destination)) + byte[] ramp = new byte[256]; + for (int x = 0; x < 256; ++x) + { + byte val = ((255.0 * Math.Pow(x / 255.0, value)) + 0.5).ToByte(); + ramp[x] = val; + } + + using (FastBitmap fastSource = new FastBitmap(source)) { - using (ImageAttributes attributes = new ImageAttributes()) + using (FastBitmap fastDestination = new FastBitmap(destination)) { - attributes.SetGamma(value); - graphics.DrawImage(source, rectangle, 0, 0, width, height, GraphicsUnit.Pixel, attributes); + Parallel.For( + 0, + height, + y => + { + for (int x = 0; x < width; x++) + { + // ReSharper disable once AccessToDisposedClosure + Color color = fastSource.GetPixel(x, y); + byte r = ramp[color.R]; + byte g = ramp[color.G]; + byte b = ramp[color.B]; + + // ReSharper disable once AccessToDisposedClosure + fastDestination.SetPixel(x, y, Color.FromArgb(color.A, r, g, b)); + } + }); } } + //Rectangle rectangle = new Rectangle(0, 0, width, height); + //using (Graphics graphics = Graphics.FromImage(destination)) + //{ + // using (ImageAttributes attributes = new ImageAttributes()) + // { + // attributes.SetGamma(value); + // graphics.DrawImage(source, rectangle, 0, 0, width, height, GraphicsUnit.Pixel, attributes); + // } + //} + source.Dispose(); return destination; } diff --git a/src/ImageProcessor/Imaging/Helpers/ImageMaths.cs b/src/ImageProcessor/Imaging/Helpers/ImageMaths.cs index 050c5e829..97338bfa3 100644 --- a/src/ImageProcessor/Imaging/Helpers/ImageMaths.cs +++ b/src/ImageProcessor/Imaging/Helpers/ImageMaths.cs @@ -12,6 +12,7 @@ namespace ImageProcessor.Imaging.Helpers { using System; using System.Drawing; + using ImageProcessor.Imaging.Colors; /// @@ -73,6 +74,40 @@ namespace ImageProcessor.Imaging.Helpers return value; } + /// + /// Returns value indicating whether the given number is with in the minimum and maximum + /// given range. + /// + /// + /// The The value to clamp. + /// + /// + /// If + /// The minimum range value. + /// + /// + /// The maximum range value. + /// + /// + /// Whether to include the minimum and maximum values. + /// Defaults to true. + /// + /// + /// The to test. + /// + /// + /// True if the value falls within the maximum and minimum; otherwise, false. + /// + public static bool InRange(T value, T min, T max, bool include = true) where T : IComparable + { + if (include) + { + return (value.CompareTo(min) >= 0) && (value.CompareTo(max) <= 0); + } + + return (value.CompareTo(min) > 0) && (value.CompareTo(max) < 0); + } + /// /// Returns the given degrees converted to radians. /// diff --git a/src/ImageProcessor/Imaging/Helpers/PixelOperations.cs b/src/ImageProcessor/Imaging/Helpers/PixelOperations.cs new file mode 100644 index 000000000..2393fcace --- /dev/null +++ b/src/ImageProcessor/Imaging/Helpers/PixelOperations.cs @@ -0,0 +1,58 @@ +// -------------------------------------------------------------------------------------------------------------------- +// +// Copyright (c) James South. +// Licensed under the Apache License, Version 2.0. +// +// +// Performs per-pixel operations. +// +// -------------------------------------------------------------------------------------------------------------------- + +namespace ImageProcessor.Imaging.Helpers +{ + using System; + using System.Drawing; + using ImageProcessor.Common.Extensions; + + /// + /// Performs per-pixel operations. + /// + public static class PixelOperations + { + /// + /// Returns the given color adjusted by the given gamma value. + /// + /// + /// The to adjust. + /// + /// + /// The gamma value - Between .1 and 5. + /// + /// + /// The adjusted . + /// + /// + /// Thrown if the given gamma value is out with the acceptable range. + /// + public static Color Gamma(Color color, float value) + { + if (value > 5 || value < .1) + { + throw new ArgumentOutOfRangeException("value", "Value should be between .1 and 5."); + } + + byte[] ramp = new byte[256]; + for (int x = 0; x < 256; ++x) + { + byte val = ((255.0 * Math.Pow(x / 255.0, value)) + 0.5).ToByte(); + ramp[x] = val; + } + + byte r = ramp[color.R]; + byte g = ramp[color.G]; + byte b = ramp[color.B]; + + return Color.FromArgb(color.A, r, g, b); + } + } +} diff --git a/src/ImageProcessor/Imaging/TextLayer.cs b/src/ImageProcessor/Imaging/TextLayer.cs index c934791d7..12b049e42 100644 --- a/src/ImageProcessor/Imaging/TextLayer.cs +++ b/src/ImageProcessor/Imaging/TextLayer.cs @@ -117,6 +117,16 @@ namespace ImageProcessor.Imaging /// Gets or sets a value indicating whether a DropShadow should be drawn. /// public bool DropShadow { get; set; } + + /// + /// Gets or sets a value indicating whether the text should be rendered vertically. + /// + public bool Vertical { get; set; } + + /// + /// Gets or sets a value indicating whether the text should be rendered right to left. + /// + public bool RightToLeft { get; set; } #endregion /// @@ -147,7 +157,9 @@ namespace ImageProcessor.Imaging && this.Style == textLayer.Style && this.DropShadow == textLayer.DropShadow && this.Opacity == textLayer.Opacity - && this.Position == textLayer.Position; + && this.Position == textLayer.Position + && this.Vertical == textLayer.Vertical + && this.RightToLeft == textLayer.RightToLeft; } /// @@ -168,6 +180,8 @@ namespace ImageProcessor.Imaging hashCode = (hashCode * 397) ^ this.Opacity; hashCode = (hashCode * 397) ^ this.FontSize; hashCode = (hashCode * 397) ^ this.Position.GetHashCode(); + hashCode = (hashCode * 397) ^ this.Vertical.GetHashCode(); + hashCode = (hashCode * 397) ^ this.RightToLeft.GetHashCode(); return hashCode; } } diff --git a/src/ImageProcessor/Processors/Halftone - Copy.cs b/src/ImageProcessor/Processors/Halftone - Copy.cs deleted file mode 100644 index e43ce19de..000000000 --- a/src/ImageProcessor/Processors/Halftone - Copy.cs +++ /dev/null @@ -1,262 +0,0 @@ -// -------------------------------------------------------------------------------------------------------------------- -// -// Copyright (c) James South. -// Licensed under the Apache License, Version 2.0. -// -// -------------------------------------------------------------------------------------------------------------------- - -namespace ImageProcessor.Processors -{ - using System; - using System.Collections.Generic; - using System.Diagnostics; - using System.Drawing; - using System.Drawing.Drawing2D; - using System.Threading.Tasks; - - using ImageProcessor.Common.Exceptions; - using ImageProcessor.Common.Extensions; - using ImageProcessor.Imaging; - using ImageProcessor.Imaging.Colors; - using ImageProcessor.Imaging.Helpers; - - /// - /// The halftone. - /// - class Halftone : IGraphicsProcessor - { - /// - /// Initializes a new instance of the class. - /// - public Halftone() - { - this.Settings = new Dictionary(); - } - - /// - /// Gets or sets the dynamic parameter. - /// - public dynamic DynamicParameter - { - get; - set; - } - - /// - /// Gets or sets any additional settings required by the processor. - /// - public Dictionary Settings - { - get; - set; - } - - /// - /// The process image. - /// - /// - /// The factory. - /// - /// - /// The . - /// - /// - /// - public Image ProcessImage(ImageFactory factory) - { - Bitmap cyan = null; - Bitmap magenta = null; - Bitmap yellow = null; - Bitmap keyline = null; - Bitmap newImage = null; - Image image = factory.Image; - - try - { - int width = image.Width; - int height = image.Height; - - // Angles taken from Wikipedia page. - float cyanAngle = 15f; - float magentaAngle = 75f; - float yellowAngle = 0f; - float keylineAngle = 45f; - - int diameter = 4; - float multiplier = 4 * (float)Math.Sqrt(2); - - // Cyan color sampled from Wikipedia page. - Brush cyanBrush = new SolidBrush(Color.FromArgb(0, 153, 239)); - Brush magentaBrush = Brushes.Magenta; - Brush yellowBrush = Brushes.Yellow; - Brush keylineBrush; - - // Create our images. - cyan = new Bitmap(width, height); - magenta = new Bitmap(width, height); - yellow = new Bitmap(width, height); - keyline = new Bitmap(width, height); - newImage = new Bitmap(width, height); - - // Ensure the correct resolution is set. - cyan.SetResolution(image.HorizontalResolution, image.VerticalResolution); - magenta.SetResolution(image.HorizontalResolution, image.VerticalResolution); - yellow.SetResolution(image.HorizontalResolution, image.VerticalResolution); - keyline.SetResolution(image.HorizontalResolution, image.VerticalResolution); - newImage.SetResolution(image.HorizontalResolution, image.VerticalResolution); - - // Check bounds against this. - Rectangle rectangle = new Rectangle(0, 0, width, height); - - using (Graphics graphicsCyan = Graphics.FromImage(cyan)) - using (Graphics graphicsMagenta = Graphics.FromImage(magenta)) - using (Graphics graphicsYellow = Graphics.FromImage(yellow)) - using (Graphics graphicsKeyline = Graphics.FromImage(keyline)) - { - // Ensure cleared out. - graphicsCyan.Clear(Color.Transparent); - graphicsMagenta.Clear(Color.Transparent); - graphicsYellow.Clear(Color.Transparent); - graphicsKeyline.Clear(Color.Transparent); - - // This is too slow. The graphics object can't be called within a parallel - // loop so we have to do it old school. :( - using (FastBitmap sourceBitmap = new FastBitmap(image)) - { - for (int y = -height * 2; y < height * 2; y += diameter) - { - for (int x = -width * 2; x < width * 2; x += diameter) - { - Color color; - CmykColor cmykColor; - float brushWidth; - - // Cyan - Point rotatedPoint = ImageMaths.RotatePoint(new Point(x, y), cyanAngle); - int angledX = rotatedPoint.X; - int angledY = rotatedPoint.Y; - if (rectangle.Contains(new Point(angledX, angledY))) - { - color = sourceBitmap.GetPixel(angledX, angledY); - cmykColor = color; - brushWidth = diameter * (cmykColor.C / 255f) * multiplier; - graphicsCyan.FillEllipse(cyanBrush, angledX, angledY, brushWidth, brushWidth); - } - - // Magenta - rotatedPoint = ImageMaths.RotatePoint(new Point(x, y), magentaAngle); - angledX = rotatedPoint.X; - angledY = rotatedPoint.Y; - if (rectangle.Contains(new Point(angledX, angledY))) - { - color = sourceBitmap.GetPixel(angledX, angledY); - cmykColor = color; - brushWidth = diameter * (cmykColor.M / 255f) * multiplier; - graphicsMagenta.FillEllipse(magentaBrush, angledX, angledY, brushWidth, brushWidth); - } - - // Yellow - rotatedPoint = ImageMaths.RotatePoint(new Point(x, y), yellowAngle); - angledX = rotatedPoint.X; - angledY = rotatedPoint.Y; - if (rectangle.Contains(new Point(angledX, angledY))) - { - color = sourceBitmap.GetPixel(angledX, angledY); - cmykColor = color; - brushWidth = diameter * (cmykColor.Y / 255f) * multiplier; - graphicsYellow.FillEllipse(yellowBrush, angledX, angledY, brushWidth, brushWidth); - } - - // Keyline - rotatedPoint = ImageMaths.RotatePoint(new Point(x, y), keylineAngle); - angledX = rotatedPoint.X; - angledY = rotatedPoint.Y; - if (rectangle.Contains(new Point(angledX, angledY))) - { - color = sourceBitmap.GetPixel(angledX, angledY); - cmykColor = color; - brushWidth = diameter * (cmykColor.K / 255f) * multiplier; - - // Just using blck is too dark. - keylineBrush = new SolidBrush(CmykColor.FromCmykColor(0, 0, 0, cmykColor.K)); - graphicsKeyline.FillEllipse(keylineBrush, angledX, angledY, brushWidth, brushWidth); - } - } - } - } - - // Set our white background. - using (Graphics graphics = Graphics.FromImage(newImage)) - { - graphics.Clear(Color.White); - } - - // Blend the colors now to mimic adaptive blending. - using (FastBitmap cyanBitmap = new FastBitmap(cyan)) - using (FastBitmap magentaBitmap = new FastBitmap(magenta)) - using (FastBitmap yellowBitmap = new FastBitmap(yellow)) - using (FastBitmap keylineBitmap = new FastBitmap(keyline)) - using (FastBitmap destinationBitmap = new FastBitmap(newImage)) - { - Parallel.For( - 0, - height, - y => - { - for (int x = 0; x < width; x++) - { - // ReSharper disable AccessToDisposedClosure - Color cyanPixel = cyanBitmap.GetPixel(x, y); - Color magentaPixel = magentaBitmap.GetPixel(x, y); - Color yellowPixel = yellowBitmap.GetPixel(x, y); - Color keylinePixel = keylineBitmap.GetPixel(x, y); - - CmykColor blended = cyanPixel.AddAsCmykColor(magentaPixel, yellowPixel, keylinePixel); - destinationBitmap.SetPixel(x, y, blended); - // ReSharper restore AccessToDisposedClosure - } - }); - } - } - - cyan.Dispose(); - magenta.Dispose(); - yellow.Dispose(); - keyline.Dispose(); - image.Dispose(); - image = newImage; - } - catch (Exception ex) - { - if (cyan != null) - { - cyan.Dispose(); - } - - if (magenta != null) - { - magenta.Dispose(); - } - - if (yellow != null) - { - yellow.Dispose(); - } - - if (keyline != null) - { - keyline.Dispose(); - } - - if (newImage != null) - { - newImage.Dispose(); - } - - throw new ImageProcessingException("Error processing image with " + this.GetType().Name, ex); - } - - return image; - } - } -} diff --git a/src/ImageProcessor/Processors/ReplaceColor.cs b/src/ImageProcessor/Processors/ReplaceColor.cs index dad33192b..4ce357780 100644 --- a/src/ImageProcessor/Processors/ReplaceColor.cs +++ b/src/ImageProcessor/Processors/ReplaceColor.cs @@ -18,6 +18,7 @@ namespace ImageProcessor.Processors using ImageProcessor.Common.Exceptions; using ImageProcessor.Common.Extensions; using ImageProcessor.Imaging; + using ImageProcessor.Imaging.Helpers; /// /// Encapsulates methods allowing the replacement of a color within an image. @@ -69,11 +70,20 @@ namespace ImageProcessor.Processors { Tuple parameters = this.DynamicParameter; Color original = parameters.Item1; + Color replacement = parameters.Item2; + + // Ensure that color comparison takes any gamma adjustments into consideration. + if (factory.FixGamma || Math.Abs(factory.CurrentGamma) > 0) + { + original = PixelOperations.Gamma(original, factory.CurrentGamma); + replacement = PixelOperations.Gamma(replacement, factory.CurrentGamma); + } + byte originalR = original.R; byte originalG = original.G; byte originalB = original.B; + byte originalA = original.A; - Color replacement = parameters.Item2; byte replacementR = replacement.R; byte replacementG = replacement.G; byte replacementB = replacement.B; @@ -81,6 +91,14 @@ namespace ImageProcessor.Processors int fuzziness = parameters.Item3; + byte minR = (originalR - fuzziness).ToByte(); + byte minG = (originalG - fuzziness).ToByte(); + byte minB = (originalB - fuzziness).ToByte(); + + byte maxR = (originalR + fuzziness).ToByte(); + byte maxG = (originalG + fuzziness).ToByte(); + byte maxB = (originalB + fuzziness).ToByte(); + newImage = new Bitmap(image); int width = image.Width; int height = image.Height; @@ -103,11 +121,11 @@ namespace ImageProcessor.Processors byte currentA = currentColor.A; // Test whether it is in the expected range. - if (currentR <= originalR + fuzziness && currentR >= originalR - fuzziness) + if (ImageMaths.InRange(currentR, minR, maxR)) { - if (currentG <= originalG + fuzziness && currentG >= originalG - fuzziness) + if (ImageMaths.InRange(currentG, minG, maxG)) { - if (currentB <= originalB + fuzziness && currentB >= originalB - fuzziness) + if (ImageMaths.InRange(currentB, minB, maxB)) { // Ensure the values are within an acceptable byte range // and set the new value. @@ -116,7 +134,11 @@ namespace ImageProcessor.Processors byte b = (originalB - currentB + replacementB).ToByte(); // Allow replacement with transparent color. - byte a = currentA != replacementA ? replacementA : currentA; + byte a = currentA; + if (originalA != replacementA) + { + a = replacementA; + } // ReSharper disable once AccessToDisposedClosure fastBitmap.SetPixel(x, y, Color.FromArgb(a, r, g, b)); diff --git a/src/ImageProcessor/Processors/Watermark.cs b/src/ImageProcessor/Processors/Watermark.cs index 5d0da759e..d40a2b619 100644 --- a/src/ImageProcessor/Processors/Watermark.cs +++ b/src/ImageProcessor/Processors/Watermark.cs @@ -72,6 +72,7 @@ namespace ImageProcessor.Processors int opacity = Math.Min((int)Math.Ceiling((textLayer.Opacity / 100f) * 255), 255); int fontSize = textLayer.FontSize; FontStyle fontStyle = textLayer.Style; + bool fallbackUsed = false; using (Graphics graphics = Graphics.FromImage(newImage)) { @@ -79,6 +80,12 @@ namespace ImageProcessor.Processors { using (StringFormat drawFormat = new StringFormat()) { + StringFormatFlags? formatFlags = this.GetFlags(textLayer); + if (formatFlags != null) + { + drawFormat.FormatFlags = formatFlags.Value; + } + using (Brush brush = new SolidBrush(Color.FromArgb(opacity, textLayer.FontColor))) { Point? origin = textLayer.Position; @@ -89,9 +96,13 @@ namespace ImageProcessor.Processors // We need to ensure that there is a position set for the watermark if (origin == null) { - int x = (int)(image.Width - textSize.Width) / 2; + int x = textLayer.RightToLeft + ? 0 + : (int)(image.Width - textSize.Width) / 2; int y = (int)(image.Height - textSize.Height) / 2; origin = new Point(x, y); + + fallbackUsed = true; } // Set the hinting and draw the text. @@ -114,14 +125,28 @@ namespace ImageProcessor.Processors Point shadowPoint = new Point(origin.Value.X + shadowDiff, origin.Value.Y + shadowDiff); // Set the bounds so any overlapping text will wrap. - bounds = new RectangleF(shadowPoint, new SizeF(image.Width - shadowPoint.X, image.Height - shadowPoint.Y)); + if (textLayer.RightToLeft && fallbackUsed) + { + bounds = new RectangleF(shadowPoint, new SizeF(image.Width - ((int)(image.Width - textSize.Width) / 2) - shadowPoint.X, image.Height - shadowPoint.Y)); + } + else + { + bounds = new RectangleF(shadowPoint, new SizeF(image.Width - shadowPoint.X, image.Height - shadowPoint.Y)); + } graphics.DrawString(text, font, shadowBrush, bounds, drawFormat); } } // Set the bounds so any overlapping text will wrap. - bounds = new RectangleF(origin.Value, new SizeF(image.Width - origin.Value.X, image.Height - origin.Value.Y)); + if (textLayer.RightToLeft && fallbackUsed) + { + bounds = new RectangleF(origin.Value, new SizeF(image.Width - ((int)(image.Width - textSize.Width) / 2), image.Height - origin.Value.Y)); + } + else + { + bounds = new RectangleF(origin.Value, new SizeF(image.Width - origin.Value.X, image.Height - origin.Value.Y)); + } graphics.DrawString(text, font, brush, bounds, drawFormat); } @@ -177,5 +202,34 @@ namespace ImageProcessor.Processors } } } + + /// + /// Returns the correct flags for the given text layer. + /// + /// + /// The to return the flags for. + /// + /// + /// The . + /// + private StringFormatFlags? GetFlags(TextLayer textLayer) + { + if (textLayer.Vertical && textLayer.RightToLeft) + { + return StringFormatFlags.DirectionVertical | StringFormatFlags.DirectionRightToLeft; + } + + if (textLayer.Vertical) + { + return StringFormatFlags.DirectionVertical; + } + + if (textLayer.RightToLeft) + { + return StringFormatFlags.DirectionRightToLeft; + } + + return null; + } } } \ No newline at end of file diff --git a/src/ImageProcessor/Properties/AssemblyInfo.cs b/src/ImageProcessor/Properties/AssemblyInfo.cs index f287baae4..520d28502 100644 --- a/src/ImageProcessor/Properties/AssemblyInfo.cs +++ b/src/ImageProcessor/Properties/AssemblyInfo.cs @@ -41,8 +41,8 @@ using System.Runtime.InteropServices; // // You can specify all the values or you can default the Build and Revision Numbers // by using the '*' as shown below: -[assembly: AssemblyVersion("2.2.1.0")] -[assembly: AssemblyFileVersion("2.2.1.0")] +[assembly: AssemblyVersion("2.2.2.0")] +[assembly: AssemblyFileVersion("2.2.2.0")] [assembly: InternalsVisibleTo("ImageProcessor.UnitTests")] [assembly: InternalsVisibleTo("ImageProcessor.Web")]