From 2fc8b537d3f361f377d755068151a1a8b7ddcf40 Mon Sep 17 00:00:00 2001 From: JimBobSquarePants Date: Thu, 1 Aug 2013 01:45:57 +0100 Subject: [PATCH] Fixed quantizer, added png8 and tif support Former-commit-id: 67480b7c57d1eb642dc7130080c3d6f046b3af49 --- src/ImageProcessor.Web/NET4/Settings.StyleCop | 11 + .../NET45/ImageFactoryExtensions.cs | 6 +- src/ImageProcessor/ImageFactory.cs | 85 +- src/ImageProcessor/ImageProcessor.csproj | 3 +- src/ImageProcessor/Imaging/ColorQuantizer.cs | 836 ++++++++++++++++++ .../Imaging/Filters/GothamMatrixFilter.cs | 4 +- .../Imaging/Filters/LomographMatrixFilter.cs | 2 +- .../Imaging/Filters/PolaroidMatrixFilter.cs | 2 +- src/ImageProcessor/Imaging/ImageUtils.cs | 61 +- src/ImageProcessor/Imaging/OctreeQuantizer.cs | 543 ------------ src/ImageProcessor/Imaging/Quantizer.cs | 315 ------- src/ImageProcessor/Processors/Format.cs | 24 +- src/ImageProcessor/Settings.StyleCop | 11 + .../Images/Penguins-8.png.REMOVED.git-id | 1 + .../Images/Penguins.tif.REMOVED.git-id | 1 + .../Images/Thumbs.db.REMOVED.git-id | 1 + .../Test_Website_NET45.csproj | 1 + .../Views/Home/Index.cshtml | 192 ++++ 18 files changed, 1173 insertions(+), 926 deletions(-) create mode 100644 src/ImageProcessor.Web/NET4/Settings.StyleCop create mode 100644 src/ImageProcessor/Imaging/ColorQuantizer.cs delete mode 100644 src/ImageProcessor/Imaging/OctreeQuantizer.cs delete mode 100644 src/ImageProcessor/Imaging/Quantizer.cs create mode 100644 src/ImageProcessor/Settings.StyleCop create mode 100644 src/TestWebsites/NET45/Test_Website_NET45/Images/Penguins-8.png.REMOVED.git-id create mode 100644 src/TestWebsites/NET45/Test_Website_NET45/Images/Penguins.tif.REMOVED.git-id create mode 100644 src/TestWebsites/NET45/Test_Website_NET45/Images/Thumbs.db.REMOVED.git-id diff --git a/src/ImageProcessor.Web/NET4/Settings.StyleCop b/src/ImageProcessor.Web/NET4/Settings.StyleCop new file mode 100644 index 0000000000..2538255678 --- /dev/null +++ b/src/ImageProcessor.Web/NET4/Settings.StyleCop @@ -0,0 +1,11 @@ + + + + + James South + Copyright (c) James South. +Licensed under the Apache License, Version 2.0. + + + + \ No newline at end of file diff --git a/src/ImageProcessor.Web/NET45/ImageFactoryExtensions.cs b/src/ImageProcessor.Web/NET45/ImageFactoryExtensions.cs index 2a6eb26a90..bbf71e4d8f 100644 --- a/src/ImageProcessor.Web/NET45/ImageFactoryExtensions.cs +++ b/src/ImageProcessor.Web/NET45/ImageFactoryExtensions.cs @@ -9,6 +9,7 @@ namespace ImageProcessor.Web { #region Using using System.Collections.Generic; + using System.Drawing; using System.Linq; using ImageProcessor.Processors; using ImageProcessor.Web.Config; @@ -41,7 +42,7 @@ namespace ImageProcessor.Web // It's faster to lock and run through our activated list than to create new instances. lock (SyncRoot) { - // Get a list of all graphics processors that have parsed and matched the querystring. + // Get a list of all graphics processors that have parsed and matched the query string. List graphicsProcessors = ImageProcessorConfig.Instance.GraphicsProcessors .Where(x => x.MatchRegexIndex(factory.QueryString) != int.MaxValue) @@ -51,7 +52,8 @@ namespace ImageProcessor.Web // Loop through and process the image. foreach (IGraphicsProcessor graphicsProcessor in graphicsProcessors) { - factory.Image = graphicsProcessor.ProcessImage(factory); + Image img = graphicsProcessor.ProcessImage(factory); + factory.Update(img); } } } diff --git a/src/ImageProcessor/ImageFactory.cs b/src/ImageProcessor/ImageFactory.cs index 328bd4e2d2..02f8028f86 100644 --- a/src/ImageProcessor/ImageFactory.cs +++ b/src/ImageProcessor/ImageFactory.cs @@ -1,22 +1,25 @@ -// ----------------------------------------------------------------------- +// -------------------------------------------------------------------------------------------------------------------- // -// Copyright (c) James South. -// Licensed under the Apache License, Version 2.0. +// Copyright (c) James South. +// Licensed under the Apache License, Version 2.0. // -// ----------------------------------------------------------------------- +// +// Encapsulates methods for processing image files. +// +// -------------------------------------------------------------------------------------------------------------------- namespace ImageProcessor { #region Using using System; using System.Collections.Generic; + using System.Diagnostics.CodeAnalysis; using System.Drawing; using System.Drawing.Imaging; using System.IO; using System.Linq; using ImageProcessor.Imaging; using ImageProcessor.Processors; - #endregion /// @@ -35,6 +38,11 @@ namespace ImageProcessor /// private ImageFormat backupImageFormat; + /// + /// Whether the image is indexed. + /// + private bool isIndexed; + /// /// A value indicating whether this instance of the given entity has been disposed. /// @@ -72,9 +80,9 @@ namespace ImageProcessor #region Properties /// - /// Gets or sets the local image for manipulation. + /// Gets the local image for manipulation. /// - public Image Image { get; set; } + public Image Image { get; private set; } /// /// Gets the path to the local image for manipulation. @@ -114,7 +122,7 @@ namespace ImageProcessor /// public ImageFactory Load(MemoryStream memoryStream) { - // Set our image as the memorystream value. + // Set our image as the memory stream value. this.Image = Image.FromStream(memoryStream); // Store the stream in the image Tag property so we can dispose of it later. @@ -124,6 +132,7 @@ namespace ImageProcessor this.JpegQuality = DefaultJpegQuality; this.backupImageFormat = ImageFormat.Jpeg; this.ImageFormat = ImageFormat.Jpeg; + this.isIndexed = ImageUtils.IsIndexed(this.Image); this.ShouldProcess = true; return this; @@ -154,7 +163,7 @@ namespace ImageProcessor this.ImagePath = path; this.QueryString = query; - // Open a filstream to prevent the need for lock. + // Open a file stream to prevent the need for lock. using (FileStream fileStream = new FileStream(path, FileMode.Open, FileAccess.Read)) { MemoryStream memoryStream = new MemoryStream(); @@ -165,7 +174,7 @@ namespace ImageProcessor // Set the position to 0 afterwards. fileStream.Position = memoryStream.Position = 0; - // Set our image as the memorystream value. + // Set our image as the memory stream value. this.Image = Image.FromStream(memoryStream); // Store the stream in the image Tag property so we can dispose of it later. @@ -176,6 +185,7 @@ namespace ImageProcessor ImageFormat imageFormat = ImageUtils.GetImageFormat(imageName); this.backupImageFormat = imageFormat; this.ImageFormat = imageFormat; + this.isIndexed = ImageUtils.IsIndexed(this.Image); this.ShouldProcess = true; } } @@ -183,6 +193,23 @@ namespace ImageProcessor return this; } + /// + /// Updates the specified image. Used by the various IProcessors. + /// + /// The image. + /// + /// The current instance of the class. + /// + public ImageFactory Update(Image image) + { + if (this.ShouldProcess) + { + this.Image = image; + } + + return this; + } + /// /// Resets the current image to its original loaded state. /// @@ -195,7 +222,7 @@ namespace ImageProcessor { MemoryStream memoryStream = (MemoryStream)this.Image.Tag; - // Set our new image as the memorystream value. + // Set our new image as the memory stream value. Image newImage = Image.FromStream(memoryStream); // Store the stream in the image Tag property so we can dispose of it later. @@ -208,6 +235,7 @@ namespace ImageProcessor // Set the other properties. this.JpegQuality = DefaultJpegQuality; this.ImageFormat = this.backupImageFormat; + this.isIndexed = ImageUtils.IsIndexed(this.Image); } return this; @@ -386,13 +414,16 @@ namespace ImageProcessor /// Sets the output format of the current image to the matching . /// /// The . to set the image to. + /// Whether the pixel format of the image should be indexed. Used for generating Png8 images. /// /// The current instance of the class. /// - public ImageFactory Format(ImageFormat imageFormat) + [SuppressMessage("StyleCop.CSharp.DocumentationRules", "SA1650:ElementDocumentationMustBeSpelledCorrectly", Justification = "Reviewed. Suppression is OK here.")] + public ImageFactory Format(ImageFormat imageFormat, bool indexedFormat = false) { if (this.ShouldProcess) { + this.isIndexed = indexedFormat; this.ImageFormat = imageFormat; } @@ -584,8 +615,8 @@ namespace ImageProcessor string extension = ImageUtils.GetExtensionFromImageFormat(this.ImageFormat); filePath = length == -1 ? filePath + extension : filePath.Substring(0, length) + extension; - // Fix the colour palette of gif images. - this.FixGifs(); + // Fix the colour palette of indexed images. + this.FixIndexedPallete(); if (this.ImageFormat.Equals(ImageFormat.Jpeg)) { @@ -594,12 +625,14 @@ namespace ImageProcessor using (EncoderParameters encoderParameters = ImageUtils.GetEncodingParameters(this.JpegQuality)) { ImageCodecInfo imageCodecInfo = - ImageCodecInfo.GetImageEncoders().FirstOrDefault( - ici => ici.MimeType.Equals("image/jpeg", StringComparison.OrdinalIgnoreCase)); + ImageCodecInfo.GetImageEncoders() + .FirstOrDefault( + ici => ici.MimeType.Equals("image/jpeg", StringComparison.OrdinalIgnoreCase)); - // ReSharper disable AssignNullToNotNullAttribute - this.Image.Save(filePath, imageCodecInfo, encoderParameters); - // ReSharper restore AssignNullToNotNullAttribute + if (imageCodecInfo != null) + { + this.Image.Save(filePath, imageCodecInfo, encoderParameters); + } } } else @@ -625,7 +658,7 @@ namespace ImageProcessor if (this.ShouldProcess) { // Fix the colour palette of gif images. - this.FixGifs(); + this.FixIndexedPallete(); if (this.ImageFormat.Equals(ImageFormat.Jpeg)) { @@ -704,17 +737,15 @@ namespace ImageProcessor #endregion /// - /// Uses the + /// Uses the /// to fix the color palette of gif images. /// - private void FixGifs() + private void FixIndexedPallete() { - // Fix the colour palette of gif images. - // TODO: Why does the palette not get fixed when resized to the same dimensions. - if (object.Equals(this.ImageFormat, ImageFormat.Gif)) + // Fix the colour palette of indexed images. + if (this.isIndexed) { - OctreeQuantizer quantizer = new OctreeQuantizer(255, 8); - this.Image = quantizer.Quantize(this.Image); + this.Image = ColorQuantizer.Quantize(this.Image, PixelFormat.Format8bppIndexed); } } #endregion diff --git a/src/ImageProcessor/ImageProcessor.csproj b/src/ImageProcessor/ImageProcessor.csproj index df2f93ff8e..038064bffd 100644 --- a/src/ImageProcessor/ImageProcessor.csproj +++ b/src/ImageProcessor/ImageProcessor.csproj @@ -61,6 +61,7 @@ + @@ -74,8 +75,6 @@ - - diff --git a/src/ImageProcessor/Imaging/ColorQuantizer.cs b/src/ImageProcessor/Imaging/ColorQuantizer.cs new file mode 100644 index 0000000000..932da35090 --- /dev/null +++ b/src/ImageProcessor/Imaging/ColorQuantizer.cs @@ -0,0 +1,836 @@ +// -------------------------------------------------------------------------------------------------------------------- +// +// Copyright (c) James South. +// Licensed under the Apache License, Version 2.0. +// +// +// The color quantizer. +// +// -------------------------------------------------------------------------------------------------------------------- + +namespace ImageProcessor.Imaging +{ + #region Using + using System; + using System.Diagnostics.CodeAnalysis; + using System.Drawing; + using System.Drawing.Imaging; + using System.Runtime.InteropServices; + #endregion + + /// + /// The color quantizer. + /// + [SuppressMessage("StyleCop.CSharp.DocumentationRules", "SA1650:ElementDocumentationMustBeSpelledCorrectly", Justification = "Reviewed. Suppression is OK here.")] + public static class ColorQuantizer + { + #region Quantize methods + /// The quantize. + /// The image. + /// The bitmap pixel format. + /// The quantized image with the recalculated color palette. + public static Bitmap Quantize(Image image, PixelFormat bitmapPixelFormat) + { + // Use dither by default + return Quantize(image, bitmapPixelFormat, true); + } + + /// The quantize. + /// The image. + /// The pixel format. + /// The use dither. + /// The quantized image with the recalculated color palette. + public static Bitmap Quantize(Image image, PixelFormat pixelFormat, bool useDither) + { + Bitmap tryBitmap = image as Bitmap; + + if (tryBitmap != null && tryBitmap.PixelFormat == PixelFormat.Format32bppArgb) + { + // The image passed to us is ALREADY a bitmap in the right format. No need to create + // a copy and work from there. + return DoQuantize(tryBitmap, pixelFormat, useDither); + } + + // We use these values a lot + int width = image.Width; + int height = image.Height; + Rectangle sourceRect = Rectangle.FromLTRB(0, 0, width, height); + + // create a 24-bit rgb version of the source image + using (Bitmap bitmapSource = new Bitmap(width, height, PixelFormat.Format32bppArgb)) + { + using (Graphics grfx = Graphics.FromImage(bitmapSource)) + { + grfx.DrawImage(image, sourceRect, 0, 0, width, height, GraphicsUnit.Pixel); + } + + return DoQuantize(bitmapSource, pixelFormat, useDither); + } + } + + /// + /// Does the quantize. + /// + /// The bitmap source. + /// The pixel format. + /// if set to true [use dither]. + /// The quantized image with the recalculated color palette. + private static Bitmap DoQuantize(Bitmap bitmapSource, PixelFormat pixelFormat, bool useDither) + { + // We use these values a lot + int width = bitmapSource.Width; + int height = bitmapSource.Height; + Rectangle sourceRect = Rectangle.FromLTRB(0, 0, width, height); + + Bitmap bitmapOptimized = null; + + try + { + // Create a bitmap with the same dimensions and the desired format + bitmapOptimized = new Bitmap(width, height, pixelFormat); + + // Lock the bits of the source image for reading. + // we will need to write if we do the dither. + BitmapData bitmapDataSource = bitmapSource.LockBits( + sourceRect, + ImageLockMode.ReadWrite, + PixelFormat.Format32bppArgb); + + try + { + // Perform the first pass, which generates the octree data + // Create an Octree + Octree octree = new Octree(pixelFormat); + + // Stride might be negative, indicating inverted row order. + // Allocate a managed buffer for the pixel data, and copy it from the unmanaged pointer. + int strideSource = Math.Abs(bitmapDataSource.Stride); + byte[] sourceDataBuffer = new byte[strideSource * height]; + Marshal.Copy(bitmapDataSource.Scan0, sourceDataBuffer, 0, sourceDataBuffer.Length); + + // We could skip every other row and/or every other column when sampling the colors + // of the source image, rather than hitting every other pixel. It doesn't seem to + // degrade the resulting image too much. But it doesn't really help the performance + // too much because the majority of the time seems to be spent in other places. + + // For every row + int rowStartSource = 0; + for (int ndxRow = 0; ndxRow < height; ndxRow += 1) + { + // For each column + for (int ndxCol = 0; ndxCol < width; ndxCol += 1) + { + // Add the color (4 bytes per pixel - ARGB) + Pixel pixel = GetSourcePixel(sourceDataBuffer, rowStartSource, ndxCol); + octree.AddColor(pixel); + } + + rowStartSource += strideSource; + } + + // Get the optimized colors + Color[] colors = octree.GetPaletteColors(); + + // Set the palette from the octree + ColorPalette palette = bitmapOptimized.Palette; + for (var ndx = 0; ndx < palette.Entries.Length; ++ndx) + { + // Use the colors we calculated + // for the rest, just set to transparent + palette.Entries[ndx] = (ndx < colors.Length) + ? colors[ndx] + : Color.Transparent; + } + + bitmapOptimized.Palette = palette; + + // Lock the bits of the optimized bitmap for writing. + // we will also need to read if we are doing 1bpp or 4bpp + BitmapData bitmapDataOutput = bitmapOptimized.LockBits(sourceRect, ImageLockMode.ReadWrite, pixelFormat); + try + { + // Create a managed array for the destination bytes given the desired color depth + // and marshal the unmanaged data to the managed array + int strideOutput = Math.Abs(bitmapDataOutput.Stride); + byte[] bitmapOutputBuffer = new byte[strideOutput * height]; + + // For each source pixel, compute the appropriate color index + rowStartSource = 0; + int rowStartOutput = 0; + + for (int ndxRow = 0; ndxRow < height; ++ndxRow) + { + // For each column + for (int ndxCol = 0; ndxCol < width; ++ndxCol) + { + // Get the source color + Pixel pixel = GetSourcePixel(sourceDataBuffer, rowStartSource, ndxCol); + + // Get the closest palette index + int paletteIndex = octree.GetPaletteIndex(pixel); + + // If we want to dither and this isn't the transparent pixel + if (useDither && pixel.Alpha != 0) + { + // Calculate the error + Color paletteColor = colors[paletteIndex]; + int deltaRed = pixel.Red - paletteColor.R; + int deltaGreen = pixel.Green - paletteColor.G; + int deltaBlue = pixel.Blue - paletteColor.B; + + // Propagate the dither error. + // we'll use a standard Floyd-Steinberg matrix (1/16): + // | 0 0 0 | + // | 0 x 7 | + // | 3 5 1 | + + // Make sure we're not on the right-hand edge + if (ndxCol + 1 < width) + { + DitherSourcePixel(sourceDataBuffer, rowStartSource, ndxCol + 1, deltaRed, deltaGreen, deltaBlue, 7); + } + + // Make sure we're not already on the bottom row + if (ndxRow + 1 < height) + { + int nextRow = rowStartSource + strideSource; + + // Make sure we're not on the left-hand column + if (ndxCol > 0) + { + // Down one row, but back one pixel + DitherSourcePixel(sourceDataBuffer, nextRow, ndxCol - 1, deltaRed, deltaGreen, deltaBlue, 3); + } + + // pixel directly below us + DitherSourcePixel(sourceDataBuffer, nextRow, ndxCol, deltaRed, deltaGreen, deltaBlue, 5); + + // Make sure we're not on the right-hand column + if (ndxCol + 1 < width) + { + // Down one row, but right one pixel + DitherSourcePixel(sourceDataBuffer, nextRow, ndxCol + 1, deltaRed, deltaGreen, deltaBlue, 1); + } + } + } + + // Set the bitmap index based on the format + switch (pixelFormat) + { + case PixelFormat.Format8bppIndexed: + // Each byte is a palette index + bitmapOutputBuffer[rowStartOutput + ndxCol] = (byte)paletteIndex; + break; + + case PixelFormat.Format4bppIndexed: + // Each byte contains two pixels + bitmapOutputBuffer[rowStartOutput + (ndxCol >> 1)] |= ((ndxCol & 1) == 1) + ? (byte)(paletteIndex & 0x0f) // lower nibble + : (byte)(paletteIndex << 4); // upper nibble + break; + + case PixelFormat.Format1bppIndexed: + // Each byte contains eight pixels + if (paletteIndex != 0) + { + bitmapOutputBuffer[rowStartOutput + (ndxCol >> 3)] |= (byte)(0x80 >> (ndxCol & 0x07)); + } + + break; + } + } + + rowStartSource += strideSource; + rowStartOutput += strideOutput; + } + + // Now copy the calculated pixel bytes from the managed array to the unmanaged bitmap + Marshal.Copy(bitmapOutputBuffer, 0, bitmapDataOutput.Scan0, bitmapOutputBuffer.Length); + } + finally + { + bitmapOptimized.UnlockBits(bitmapDataOutput); + bitmapDataOutput = null; + } + } + finally + { + bitmapSource.UnlockBits(bitmapDataSource); + bitmapDataSource = null; + } + } + catch (Exception) + { + // If any exception is thrown, dispose of the bitmap object + // we've been working on before we rethrow and bail + if (bitmapOptimized != null) + { + bitmapOptimized.Dispose(); + } + + throw; + } + + // Caller is responsible for disposing of this bitmap! + return bitmapOptimized; + } + + /// + /// Dithers the source pixel. + /// + /// The buffer. + /// The row start. + /// The column. + /// The delta red. + /// The delta green. + /// The delta blue. + /// The weight. + private static void DitherSourcePixel(byte[] buffer, int rowStart, int col, int deltaRed, int deltaGreen, int deltaBlue, int weight) + { + int colorIndex = rowStart + (col * 4); + buffer[colorIndex + 2] = ChannelAdjustment(buffer[colorIndex + 2], (deltaRed * weight) >> 4); + buffer[colorIndex + 1] = ChannelAdjustment(buffer[colorIndex + 1], (deltaGreen * weight) >> 4); + buffer[colorIndex] = ChannelAdjustment(buffer[colorIndex], (deltaBlue * weight) >> 4); + } + + /// + /// Gets the source pixel. + /// + /// The buffer. + /// The row start. + /// The column. + /// The source pixel. + private static Pixel GetSourcePixel(byte[] buffer, int rowStart, int col) + { + int colorIndex = rowStart + (col * 4); + return new Pixel + { + Alpha = buffer[colorIndex + 3], + Red = buffer[colorIndex + 2], + Green = buffer[colorIndex + 1], + Blue = buffer[colorIndex] + }; + } + + #endregion + + /// Gets the channel adjustment. + /// The current. + /// The offset. + /// The channel adjustment. + private static byte ChannelAdjustment(byte current, int offset) + { + return (byte)Math.Min(255, Math.Max(0, current + offset)); + } + + #region Octree class + + /// data structure for storing and reducing colors used in the source image + private class Octree + { + /// The m_max colors. + private readonly int octreeMaxColors; + + /// The m_reducible nodes. + private readonly OctreeNode[] octreeReducibleNodes; + + /// The m_color count. + private int octreeColorCount; + + /// The m_has transparent. + private bool octreeHasTransparent; + + /// The m_last argb. + private int octreeLastArgb; + + /// The m_last node. + private OctreeNode octreeLastNode; + + /// The m_palette. + private Color[] octreePalette; + + /// The m_root. + private OctreeNode octreeRoot; + + /// Initializes a new instance of the class. Constructor + /// desired pixel format + internal Octree(PixelFormat pixelFormat) + { + // figure out the maximum colors from the pixel format passed in + switch (pixelFormat) + { + case PixelFormat.Format1bppIndexed: + this.octreeMaxColors = 2; + break; + + case PixelFormat.Format4bppIndexed: + this.octreeMaxColors = 16; + break; + + case PixelFormat.Format8bppIndexed: + this.octreeMaxColors = 256; + break; + + default: + throw new ArgumentException("Invalid Pixel Format", "pixelFormat"); + } + + // we need a list for each level that may have reducible nodes. + // since the last level (level 7) is only made up of leaf nodes, + // we don't need an array entry for it. + this.octreeReducibleNodes = new OctreeNode[7]; + + // add the initial level-0 root node + this.octreeRoot = new OctreeNode(0, this); + } + + /// Add the given pixel color to the octree + /// points to the pixel color we want to add + internal void AddColor(Pixel pixel) + { + // If the A value is non-zero (ignore the transparent color) + if (pixel.Alpha > 0) + { + // If we have a previous node and this color is the same as the last... + if (this.octreeLastNode != null && pixel.Argb == this.octreeLastArgb) + { + // Just add this color to the same last node + this.octreeLastNode.AddColor(pixel); + } + else + { + // Just start at the root. If a new color is added, + // add one to the count (otherwise 0). + this.octreeColorCount += this.octreeRoot.AddColor(pixel) ? 1 : 0; + } + } + else + { + // Flag that we have a transparent color. + this.octreeHasTransparent = true; + } + } + + /// + /// Given a pixel color, return the index of the palette entry + /// we want to use in the reduced image. If the color is not in the + /// octree, OctreeNode.GetPaletteIndex will return a negative number. + /// In that case, we will have to calculate the palette index the brute-force + /// method by computing the least distance to each color in the palette array. + /// + /// pointer to the pixel color we want to look up + /// index of the palette entry we want to use for this color + internal int GetPaletteIndex(Pixel pixel) + { + int paletteIndex = 0; + + // transparent is always the first entry, so if this is transparent, + // don't do anything. + if (pixel.Alpha > 0) + { + paletteIndex = this.octreeRoot.GetPaletteIndex(pixel); + + // returns -1 if this value isn't in the octree. + if (paletteIndex < 0) + { + // Use the brute-force method of calculating the closest color + // in the palette to the one we want + int minDistance = int.MaxValue; + for (int ndx = 0; ndx < this.octreePalette.Length; ++ndx) + { + Color paletteColor = this.octreePalette[ndx]; + + // Calculate the delta for each channel + int deltaRed = pixel.Red - paletteColor.R; + int deltaGreen = pixel.Green - paletteColor.G; + int deltaBlue = pixel.Blue - paletteColor.B; + + // Calculate the distance-squared by summing each channel's square + int distance = (deltaRed * deltaRed) + (deltaGreen * deltaGreen) + (deltaBlue * deltaBlue); + if (distance < minDistance) + { + minDistance = distance; + paletteIndex = ndx; + } + } + } + } + + return paletteIndex; + } + + /// + /// Return a color palette for the computed octree. + /// + /// A color palette for the computed octree + internal Color[] GetPaletteColors() + { + // If we haven't already computed it, compute it now + if (this.octreePalette == null) + { + // Start at the second-to-last reducible level + int reductionLevel = this.octreeReducibleNodes.Length - 1; + + // We want to subtract one from the target if we have a transparent + // bit because we want to save room for that special color + int targetCount = this.octreeMaxColors - (this.octreeHasTransparent ? 1 : 0); + + // While we still have more colors than the target... + while (this.octreeColorCount > targetCount) + { + // Find the first reducible node, starting with the last level + // that can have reducible nodes + while (reductionLevel > 0 && this.octreeReducibleNodes[reductionLevel] == null) + { + --reductionLevel; + } + + if (this.octreeReducibleNodes[reductionLevel] == null) + { + // Shouldn't get here + break; + } + + // We should have a node now + OctreeNode newLeaf = this.octreeReducibleNodes[reductionLevel]; + this.octreeReducibleNodes[reductionLevel] = newLeaf.NextReducibleNode; + this.octreeColorCount -= newLeaf.Reduce() - 1; + } + + if (reductionLevel == 0 && !this.octreeHasTransparent) + { + // If this was the top-most level, we now only have a single color + // representing the average. That's not what we want. + // use just black and white + this.octreePalette = new Color[2]; + this.octreePalette[0] = Color.Black; + this.octreePalette[1] = Color.White; + + // And empty the octree so it always picks the closer of the black and white entries + this.octreeRoot = new OctreeNode(0, this); + } + else + { + // Now walk the tree, adding all the remaining colors to the list + int paletteIndex = 0; + this.octreePalette = new Color[this.octreeColorCount + (this.octreeHasTransparent ? 1 : 0)]; + + // Add the transparent color if we need it + if (this.octreeHasTransparent) + { + this.octreePalette[paletteIndex++] = Color.Transparent; + } + + // Have the nodes insert their leaf colors + this.octreeRoot.AddColorsToPalette(this.octreePalette, ref paletteIndex); + } + } + + return this.octreePalette; + } + + /// set up the values we need to reuse the given pointer if the next color is argb + /// last node to which we added a color + /// last color we added + private void SetLastNode(OctreeNode node, int argb) + { + this.octreeLastNode = node; + this.octreeLastArgb = argb; + } + + /// When a reducible node is added, this method is called to add it to the appropriate + /// reducible node list (given its level) + /// node to add to a reducible list + private void AddReducibleNode(OctreeNode reducibleNode) + { + // hook this node into the front of the list. + // this means the last one added will be the first in the list. + reducibleNode.NextReducibleNode = this.octreeReducibleNodes[reducibleNode.Level]; + this.octreeReducibleNodes[reducibleNode.Level] = reducibleNode; + } + + #region OctreeNode class + + /// Node for an Octree structure + private class OctreeNode + { + /// The s_level masks. + private static readonly byte[] NodeLevelMasks = { 0x80, 0x40, 0x20, 0x10, 0x08, 0x04, 0x02, 0x01 }; + + /// The m_level. + private readonly int nodeLevel; + + /// The m_octree. + private readonly Octree nodeOctree; + + /// The m_blue sum. + private int nodeBlueSum; + + /// The m_child nodes. + private OctreeNode[] nodeChildNodes; + + /// The m_green sum. + private int nodeGreenSum; + + /// The m_is leaf. + private bool nodeIsLeaf; + + /// The m_palette index. + private int nodePaletteIndex; + + /// + /// The pixel count.Information we need to calculate the average color for a set of pixels + /// + private int nodePixelCount; + + /// The m_red sum. + private int nodeRedSum; + + /// Initializes a new instance of the class. Constructor + /// level for this node + /// owning octree + internal OctreeNode(int level, Octree octree) + { + this.nodeOctree = octree; + this.nodeLevel = level; + + // Since there are only eight levels, if we get to level 7 + // We automatically make this a leaf node + this.nodeIsLeaf = level == 7; + + if (!this.nodeIsLeaf) + { + // Create the child array + this.nodeChildNodes = new OctreeNode[8]; + + // Add it to the tree's reducible node list + this.nodeOctree.AddReducibleNode(this); + } + } + + /// Gets Level. + internal int Level + { + get { return this.nodeLevel; } + } + + /// + /// Gets or sets NextReducibleNode. + /// Once we compute a palette, this will be set + /// to the palette index associated with this leaf node. + /// Nodes are arranged in linked lists of reducible nodes for a given level. + /// this field and property is used to traverse that list. + /// + internal OctreeNode NextReducibleNode { get; set; } + + /// + /// Gets the average color for this node. + /// + private Color NodeColor + { + get + { + // Average color is the sum of each channel divided by the pixel count + return Color.FromArgb( + this.nodeRedSum / this.nodePixelCount, + this.nodeGreenSum / this.nodePixelCount, + this.nodeBlueSum / this.nodePixelCount); + } + } + + /// + /// Add the given color to this node if it is a leaf, otherwise recurse + /// down the appropriate child + /// + /// color to add + /// true if a new color was added to the octree + internal bool AddColor(Pixel pixel) + { + bool colorAdded; + if (this.nodeIsLeaf) + { + // Increase the pixel count for this node, and if + // the result is 1, then this is a new color + colorAdded = ++this.nodePixelCount == 1; + + // Add the color to the running sum for this node + this.nodeRedSum += pixel.Red; + this.nodeGreenSum += pixel.Green; + this.nodeBlueSum += pixel.Blue; + + // Set the last node so we can quickly process adjacent pixels + // with the same color + this.nodeOctree.SetLastNode(this, pixel.Argb); + } + else + { + // Get the index at this level for the rgb values + int childIndex = this.GetChildIndex(pixel); + + // If there is no child, add one now to the next level + if (this.nodeChildNodes[childIndex] == null) + { + this.nodeChildNodes[childIndex] = new OctreeNode(this.nodeLevel + 1, this.nodeOctree); + } + + // Recurse... + colorAdded = this.nodeChildNodes[childIndex].AddColor(pixel); + } + + return colorAdded; + } + + /// + /// Given a source color, return the palette index to use for the reduced image. + /// Returns -1 if the color is not represented in the octree (this happens if + /// the color has been dithered into a new color that did not appear in the + /// original image when the octree was formed in pass 1. + /// + /// source color to look up + /// palette index to use + internal int GetPaletteIndex(Pixel pixel) + { + int paletteIndex = -1; + if (this.nodeIsLeaf) + { + // Use this leaf node's palette index + paletteIndex = this.nodePaletteIndex; + } + else + { + // Get the index at this level for the rgb values + var childIndex = this.GetChildIndex(pixel); + if (this.nodeChildNodes[childIndex] != null) + { + // Recurse... + paletteIndex = this.nodeChildNodes[childIndex].GetPaletteIndex(pixel); + } + } + + return paletteIndex; + } + + /// Reduce this node by combining all child nodes + /// number of nodes removed + internal int Reduce() + { + int numReduced = 0; + if (!this.nodeIsLeaf) + { + // For each child + foreach (OctreeNode node in this.nodeChildNodes) + { + if (node != null) + { + OctreeNode childNode = node; + + // add the pixel count from the child + this.nodePixelCount += childNode.nodePixelCount; + + // add the running color sums from the child + this.nodeRedSum += childNode.nodeRedSum; + this.nodeGreenSum += childNode.nodeGreenSum; + this.nodeBlueSum += childNode.nodeBlueSum; + + ++numReduced; + } + } + + this.nodeChildNodes = null; + this.nodeIsLeaf = true; + } + + return numReduced; + } + + /// + /// If this is a leaf node, add its color to the palette array at the given index + /// and increment the index. If not a leaf, recurse the children nodes. + /// + /// array of colors + /// index of the next empty slot in the array + internal void AddColorsToPalette(Color[] colorArray, ref int paletteIndex) + { + if (this.nodeIsLeaf) + { + // Save our index and increment the running index + this.nodePaletteIndex = paletteIndex++; + + // The color for this node is the average color, which is created by + // dividing the running sums for each channel by the pixel count + colorArray[this.nodePaletteIndex] = this.NodeColor; + } + else + { + // Just run through all the non-null children and recurse + foreach (OctreeNode node in this.nodeChildNodes) + { + if (node != null) + { + node.AddColorsToPalette(colorArray, ref paletteIndex); + } + } + } + } + + /// + /// Return the child index for a given color. + /// Depends on which level this node is in. + /// + /// color pixel to compute + /// child index (0-7) + private int GetChildIndex(Pixel pixel) + { + // lvl: 0 1 2 3 4 5 6 7 + // bit: 7 6 5 4 3 2 1 0 + var shift = 7 - this.nodeLevel; + int mask = NodeLevelMasks[this.nodeLevel]; + return ((pixel.Red & mask) >> (shift - 2)) | + ((pixel.Green & mask) >> (shift - 1)) | + ((pixel.Blue & mask) >> shift); + } + } + #endregion + } + #endregion + + #region Pixel class for ARGB values + /// + /// Structure of a Format32bppArgb pixel in memory. + /// + private class Pixel + { + /// + /// Gets or sets the blue component of the pixel. + /// + public byte Blue { get; set; } + + /// + /// Gets or sets the green component of the pixel. + /// + public byte Green { get; set; } + + /// + /// Gets or sets the red component of the pixel. + /// + public byte Red { get; set; } + + /// + /// Gets or sets the alpha component of the pixel. + /// + public byte Alpha { get; set; } + + /// + /// Gets the argb combination of the pixel. + /// + public int Argb + { + get + { + return this.Alpha << 24 | this.Red << 16 | this.Green << 8 | this.Blue; + } + } + } + #endregion + } +} diff --git a/src/ImageProcessor/Imaging/Filters/GothamMatrixFilter.cs b/src/ImageProcessor/Imaging/Filters/GothamMatrixFilter.cs index 04e0b55280..8f46d9f1d8 100644 --- a/src/ImageProcessor/Imaging/Filters/GothamMatrixFilter.cs +++ b/src/ImageProcessor/Imaging/Filters/GothamMatrixFilter.cs @@ -76,11 +76,11 @@ namespace ImageProcessor.Imaging.Filters } // Add brightness and contrast to finish the effect. - factory.Image = newImage; + factory.Update(newImage); Brightness brightness = new Brightness { DynamicParameter = 5 }; newImage = (Bitmap)brightness.ProcessImage(factory); - factory.Image = newImage; + factory.Update(newImage); Contrast contrast = new Contrast { DynamicParameter = 85 }; newImage = (Bitmap)contrast.ProcessImage(factory); diff --git a/src/ImageProcessor/Imaging/Filters/LomographMatrixFilter.cs b/src/ImageProcessor/Imaging/Filters/LomographMatrixFilter.cs index 12e6927f06..121d2a887a 100644 --- a/src/ImageProcessor/Imaging/Filters/LomographMatrixFilter.cs +++ b/src/ImageProcessor/Imaging/Filters/LomographMatrixFilter.cs @@ -57,7 +57,7 @@ namespace ImageProcessor.Imaging.Filters } // Add a vignette to finish the effect. - factory.Image = newImage; + factory.Update(newImage); Vignette vignette = new Vignette(); newImage = (Bitmap)vignette.ProcessImage(factory); diff --git a/src/ImageProcessor/Imaging/Filters/PolaroidMatrixFilter.cs b/src/ImageProcessor/Imaging/Filters/PolaroidMatrixFilter.cs index 6a708d49b1..0253e2c56d 100644 --- a/src/ImageProcessor/Imaging/Filters/PolaroidMatrixFilter.cs +++ b/src/ImageProcessor/Imaging/Filters/PolaroidMatrixFilter.cs @@ -86,7 +86,7 @@ namespace ImageProcessor.Imaging.Filters } // Add a vignette to finish the effect. - factory.Image = newImage; + factory.Update(newImage); Vignette vignette = new Vignette(); newImage = (Bitmap)vignette.ProcessImage(factory); diff --git a/src/ImageProcessor/Imaging/ImageUtils.cs b/src/ImageProcessor/Imaging/ImageUtils.cs index 8a62f2e3c1..fdfb0ca83c 100644 --- a/src/ImageProcessor/Imaging/ImageUtils.cs +++ b/src/ImageProcessor/Imaging/ImageUtils.cs @@ -1,14 +1,18 @@ -// ----------------------------------------------------------------------- +// -------------------------------------------------------------------------------------------------------------------- // -// Copyright (c) James South. -// Licensed under the Apache License, Version 2.0. +// Copyright (c) James South. +// Licensed under the Apache License, Version 2.0. // -// ----------------------------------------------------------------------- +// +// Encapsulates useful image utility methods. +// +// -------------------------------------------------------------------------------------------------------------------- namespace ImageProcessor.Imaging { #region Using using System; + using System.Drawing; using System.Drawing.Imaging; using System.IO; using System.Linq; @@ -24,7 +28,7 @@ namespace ImageProcessor.Imaging /// /// The image format regex. /// - private static readonly Regex FormatRegex = new Regex(@"j(pg|peg)|bmp|png|gif", RegexOptions.Compiled | RegexOptions.IgnoreCase | RegexOptions.RightToLeft); + private static readonly Regex FormatRegex = new Regex(@"(\.?)(j(pg|peg)|bmp|png|gif|ti(f|ff))$", RegexOptions.Compiled | RegexOptions.IgnoreCase | RegexOptions.RightToLeft); /// /// Returns the correct response type based on the given request path. @@ -39,14 +43,17 @@ namespace ImageProcessor.Imaging { foreach (Match match in FormatRegex.Matches(request)) { - switch (match.Value) + switch (match.Value.ToUpperInvariant()) { - case "png": + case "PNG": return ResponseType.Png; - case "bmp": + case "BMP": return ResponseType.Bmp; - case "gif": + case "GIF": return ResponseType.Gif; + case "TIF": + case "TIFF": + return ResponseType.Tiff; default: return ResponseType.Jpeg; } @@ -75,6 +82,9 @@ namespace ImageProcessor.Imaging return ImageFormat.Bmp; case ".GIF": return ImageFormat.Gif; + case ".TIF": + case ".TIFF": + return ImageFormat.Tiff; default: // Should be a jpeg. return ImageFormat.Jpeg; @@ -104,6 +114,10 @@ namespace ImageProcessor.Imaging return ".bmp"; case "Png": return ".png"; + case "Tif": + return ".tif"; + case "Tiff": + return ".tif"; default: return ".jpg"; } @@ -126,6 +140,8 @@ namespace ImageProcessor.Imaging return ImageFormat.Bmp; case ResponseType.Gif: return ImageFormat.Gif; + case ResponseType.Tiff: + return ImageFormat.Tiff; default: // Should be a jpeg. return ImageFormat.Jpeg; @@ -181,25 +197,16 @@ namespace ImageProcessor.Imaging /// True the value contains a valid image extension, otherwise false. public static bool IsValidImageExtension(string fileName) { - bool isValid = false; - - if (!string.IsNullOrWhiteSpace(fileName)) - { - string[] fileExtensions = { ".BMP", ".JPG", ".PNG", ".GIF", ".JPEG" }; - - Parallel.ForEach( - fileExtensions, - (extension, loop) => - { - if (fileName.ToUpperInvariant().EndsWith(extension)) - { - isValid = true; - loop.Stop(); - } - }); - } + return FormatRegex.IsMatch(fileName); + } - return isValid; + /// Returns a value indicating whether or not the given bitmap is indexed. + /// The image to check + /// Whether or not the given bitmap is indexed. + public static bool IsIndexed(Image image) + { + // Test value of flags using bitwise AND. + return (image.PixelFormat & PixelFormat.Indexed) != 0; } } } diff --git a/src/ImageProcessor/Imaging/OctreeQuantizer.cs b/src/ImageProcessor/Imaging/OctreeQuantizer.cs deleted file mode 100644 index af30b3f379..0000000000 --- a/src/ImageProcessor/Imaging/OctreeQuantizer.cs +++ /dev/null @@ -1,543 +0,0 @@ -// ----------------------------------------------------------------------- -// -// Copyright (c) James South. -// Licensed under the Apache License, Version 2.0. -// -// ----------------------------------------------------------------------- - -namespace ImageProcessor.Imaging -{ - #region Using - using System; - using System.Collections; - using System.Drawing; - using System.Drawing.Imaging; - #endregion - - /// - /// Encapsulates methods to calculate the colour palette if an image using an octree pattern. - /// - internal class OctreeQuantizer : Quantizer - { - #region Fields - /// - /// Stores the tree. - /// - private readonly Octree octree; - - /// - /// The maximum allowed color depth. - /// - private readonly int maxColors; - #endregion - - /// - /// Initializes a new instance of the OctreeQuantizer class. - /// - /// - /// The Octree quantizer is a two pass algorithm. The initial pass sets up the octree, - /// the second pass quantizes a colour based on the nodes in the tree - /// - /// The maximum number of colours to return, maximum 255. - /// The number of significant bits minimum 1, maximum 8. - public OctreeQuantizer(int maxColors, int maxColorBits) - : base(false) - { - if (maxColors > 255) - { - throw new ArgumentOutOfRangeException("maxColors", maxColors, "The number of colours should be less than 256"); - } - - if ((maxColorBits < 1) | (maxColorBits > 8)) - { - throw new ArgumentOutOfRangeException("maxColorBits", maxColorBits, "This should be between 1 and 8"); - } - - // Construct the octree - this.octree = new Octree(maxColorBits); - this.maxColors = maxColors; - } - - /// - /// Process the pixel in the first pass of the algorithm. - /// - /// The pixel to quantize - /// - /// This function need only be overridden if your quantize algorithm needs two passes, - /// such as an Octree quantizer. - /// - protected override void InitialQuantizePixel(Color32 pixel) - { - // Add the colour to the octree - this.octree.AddColor(pixel); - } - - /// - /// Override this to process the pixel in the second pass of the algorithm. - /// - /// The pixel to quantize - /// The quantized value. - protected override byte QuantizePixel(Color32 pixel) - { - // The colour at [this.maxColors] is set to transparent - byte paletteIndex; - - // Get the palette index if this non-transparent - if (pixel.Alpha > 0) - { - paletteIndex = (byte)this.octree.GetPaletteIndex(pixel); - } - else - { - paletteIndex = (byte)this.maxColors; - } - - return paletteIndex; - } - - /// - /// Retrieve the palette for the quantized image - /// - /// Any old palette, this is overwritten - /// The new colour palette - protected override ColorPalette GetPalette(ColorPalette original) - { - // First off convert the octree to this.maxColors colours - ArrayList palette = this.octree.Palletize(this.maxColors - 1); - - // Then convert the palette based on those colours - for (int index = 0; index < palette.Count; index++) - { - original.Entries[index] = (Color)palette[index]; - } - - // Add the transparent colour - original.Entries[this.maxColors] = Color.FromArgb(0, 0, 0, 0); - - return original; - } - - /// - /// Describes a tree data structure in which each internal node has exactly eight children. - /// - private class Octree - { - #region Fields - /// - /// Mask used when getting the appropriate pixels for a given node - /// - private static readonly int[] mask = new int[8] { 0x80, 0x40, 0x20, 0x10, 0x08, 0x04, 0x02, 0x01 }; - - /// - /// The root of the octree - /// - private readonly OctreeNode root; - - /// - /// Number of leaves in the tree - /// - private int leafCount; - - /// - /// Array of reducible nodes - /// - private OctreeNode[] reducibleNodes; - - /// - /// Maximum number of significant bits in the image - /// - private int maxColorBits; - - /// - /// Store the last node quantized - /// - private OctreeNode previousNode; - - /// - /// Cache the previous color quantized - /// - private int previousColor; - #endregion - - #region Constructors - /// - /// Initializes a new instance of the Octree class. - /// - /// The maximum number of significant bits in the image - public Octree(int maxBits) - { - this.maxColorBits = maxBits; - this.leafCount = 0; - this.reducibleNodes = new OctreeNode[9]; - this.root = new OctreeNode(0, this.maxColorBits, this); - this.previousColor = 0; - this.previousNode = null; - } - #endregion - - #region Properties - /// - /// Gets or sets the number of leaves in the tree - /// - public int Leaves - { - get { return this.leafCount; } - set { this.leafCount = value; } - } - - /// - /// Gets the array of reducible nodes - /// - protected OctreeNode[] ReducibleNodes - { - get { return this.reducibleNodes; } - } - #endregion - - /// - /// Add a given colour value to the octree - /// - /// - /// The color value to add. - /// - public void AddColor(Color32 pixel) - { - // Check if this request is for the same colour as the last - if (this.previousColor == pixel.ARGB) - { - // If so, check if I have a previous node setup. This will only occur if the first colour in the image - // happens to be black, with an alpha component of zero. - if (null == this.previousNode) - { - this.previousColor = pixel.ARGB; - this.root.AddColor(pixel, this.maxColorBits, 0, this); - } - else - { - // Just update the previous node - this.previousNode.Increment(pixel); - } - } - else - { - this.previousColor = pixel.ARGB; - this.root.AddColor(pixel, this.maxColorBits, 0, this); - } - } - - /// - /// Reduce the depth of the tree - /// - public void Reduce() - { - // Find the deepest level containing at least one reducible node - int index = this.maxColorBits - 1; - while ((index > 0) && (this.reducibleNodes[index] == null)) - { - index--; - } - - // Reduce the node most recently added to the list at level 'index' - OctreeNode node = this.reducibleNodes[index]; - this.reducibleNodes[index] = node.NextReducible; - - // Decrement the leaf count after reducing the node - this.leafCount -= node.Reduce(); - - // And just in case I've reduced the last color to be added, and the next color to - // be added is the same, invalidate the previousNode... - this.previousNode = null; - } - - /// - /// Convert the nodes in the octree to a palette with a maximum of colorCount colours - /// - /// The maximum number of colours - /// An array list with the palletized colours - public ArrayList Palletize(int colorCount) - { - while (this.Leaves > colorCount) - { - this.Reduce(); - } - - // Now palletize the nodes - ArrayList palette = new ArrayList(this.Leaves); - int paletteIndex = 0; - this.root.ConstructPalette(palette, ref paletteIndex); - - // And return the palette - return palette; - } - - /// - /// Get the palette index for the passed colour. - /// - /// - /// The color to return the palette index for. - /// - /// - /// The palette index for the passed colour. - /// - public int GetPaletteIndex(Color32 pixel) - { - return this.root.GetPaletteIndex(pixel, 0); - } - - /// - /// Keep track of the previous node that was quantized - /// - /// The node last quantized - protected void TrackPrevious(OctreeNode node) - { - this.previousNode = node; - } - - /// - /// Class which encapsulates each node in the tree - /// - protected class OctreeNode - { - #region Fields - /// - /// Flag indicating that this is a leaf node - /// - private bool leaf; - - /// - /// Number of pixels in this node - /// - private int pixelCount; - - /// - /// Red component - /// - private int red; - - /// - /// Green Component - /// - private int green; - - /// - /// Blue component - /// - private int blue; - - /// - /// Pointers to any child nodes - /// - private OctreeNode[] children; - - /// - /// The index of this node in the palette - /// - private int paletteIndex; - #endregion - - #region Constructors - /// - /// Initializes a new instance of the class. - /// - /// - /// The level in the tree = 0 - 7 - /// - /// - /// The number of significant color bits in the image - /// - /// - /// The tree to which this node belongs - /// - public OctreeNode(int level, int colorBits, Octree octree) - { - // Construct the new node - this.leaf = level == colorBits; - - this.red = this.green = this.blue = 0; - this.pixelCount = 0; - - // If a leaf, increment the leaf count - if (this.leaf) - { - octree.Leaves++; - this.NextReducible = null; - this.children = null; - } - else - { - // Otherwise add this to the reducible nodes - this.NextReducible = octree.ReducibleNodes[level]; - octree.ReducibleNodes[level] = this; - this.children = new OctreeNode[8]; - } - } - #endregion - - #region Properties - - /// - /// Gets or the next reducible node - /// - public OctreeNode NextReducible { get; private set; } - - /// - /// Gets the child nodes - /// - private OctreeNode[] Children - { - get { return this.children; } - } - #endregion - - #region Methods - /// - /// Add a color into the tree - /// - /// The color - /// The number of significant color bits - /// The level in the tree - /// The tree to which this node belongs - public void AddColor(Color32 pixel, int colorBits, int level, Octree octree) - { - // Update the color information if this is a leaf - if (this.leaf) - { - this.Increment(pixel); - - // Setup the previous node - octree.TrackPrevious(this); - } - else - { - // Go to the next level down in the tree - int shift = 7 - level; - int index = ((pixel.Red & mask[level]) >> (shift - 2)) | - ((pixel.Green & mask[level]) >> (shift - 1)) | - ((pixel.Blue & mask[level]) >> shift); - - OctreeNode child = this.Children[index]; - - if (null == child) - { - // Create a new child node & store in the array - child = new OctreeNode(level + 1, colorBits, octree); - this.Children[index] = child; - } - - // Add the color to the child node - child.AddColor(pixel, colorBits, level + 1, octree); - } - } - - /// - /// Reduce this node by removing all of its children - /// - /// The number of leaves removed - public int Reduce() - { - this.red = this.green = this.blue = 0; - int childPosition = 0; - - // Loop through all children and add their information to this node - for (int index = 0; index < 8; index++) - { - if (null != this.Children[index]) - { - this.red += this.Children[index].red; - this.green += this.Children[index].green; - this.blue += this.Children[index].blue; - this.pixelCount += this.Children[index].pixelCount; - ++childPosition; - this.Children[index] = null; - } - } - - // Now change this to a leaf node - this.leaf = true; - - // Return the number of nodes to decrement the leaf count by - return childPosition - 1; - } - - /// - /// Traverse the tree, building up the color palette - /// - /// The palette - /// The current palette index - public void ConstructPalette(ArrayList palette, ref int currentPaletteIndex) - { - if (this.leaf) - { - // Consume the next palette index - this.paletteIndex = currentPaletteIndex++; - - // And set the color of the palette entry - palette.Add(Color.FromArgb(this.red / this.pixelCount, this.green / this.pixelCount, this.blue / this.pixelCount)); - } - else - { - // Loop through children looking for leaves - for (int index = 0; index < 8; index++) - { - if (null != this.children[index]) - { - this.children[index].ConstructPalette(palette, ref currentPaletteIndex); - } - } - } - } - - /// - /// Return the palette index for the passed color. - /// - /// - /// The pixel. - /// - /// - /// The level. - /// - /// - /// The palette index for the passed color. - /// - public int GetPaletteIndex(Color32 pixel, int level) - { - int currentPaletteIndex = this.paletteIndex; - - if (!this.leaf) - { - int shift = 7 - level; - int index = ((pixel.Red & mask[level]) >> (shift - 2)) | - ((pixel.Green & mask[level]) >> (shift - 1)) | - ((pixel.Blue & mask[level]) >> shift); - - if (null != this.children[index]) - { - currentPaletteIndex = this.children[index].GetPaletteIndex(pixel, level + 1); - } - else - { - throw new Exception("Didn't expect this!"); - } - } - - return currentPaletteIndex; - } - - /// - /// Increment the pixel count and add to the color information - /// - /// - /// The pixel. - /// - public void Increment(Color32 pixel) - { - this.pixelCount++; - this.red += pixel.Red; - this.green += pixel.Green; - this.blue += pixel.Blue; - } - #endregion - } - } - } -} diff --git a/src/ImageProcessor/Imaging/Quantizer.cs b/src/ImageProcessor/Imaging/Quantizer.cs deleted file mode 100644 index f2506fb5a4..0000000000 --- a/src/ImageProcessor/Imaging/Quantizer.cs +++ /dev/null @@ -1,315 +0,0 @@ -// ----------------------------------------------------------------------- -// -// Copyright (c) James South. -// Licensed under the Apache License, Version 2.0. -// -// ----------------------------------------------------------------------- - -namespace ImageProcessor.Imaging -{ - #region Using - using System; - using System.Drawing; - using System.Drawing.Imaging; - using System.Runtime.InteropServices; - #endregion - - /// - /// Encapsulates methods to calculate the color palette of an image. - /// - internal abstract class Quantizer - { - #region Fields - /// - /// The flag used to indicate whether a single pass or two passes are needed for quantization. - /// - private readonly bool singlePass; - - /// - /// The size in bytes of the 32 bpp Colour structure. - /// - private readonly int pixelSize; - #endregion - - /// - /// Initializes a new instance of the Quantizer class. - /// - /// - /// If set to , then the quantizer will loop through the source pixels once; - /// otherwise, . - /// - protected Quantizer(bool singlePass) - { - this.singlePass = singlePass; - this.pixelSize = Marshal.SizeOf(typeof(Color32)); - } - - /// - /// Quantizes the given Image and returns the resulting output - /// Bitmap. - /// - /// The image to quantize - /// - /// A quantized Bitmap version of the Image - /// - public Bitmap Quantize(Image source) - { - // Get the size of the source image - int height = source.Height; - int width = source.Width; - - // And construct a rectangle from these dimensions - Rectangle bounds = new Rectangle(0, 0, width, height); - - // First off take a 32bpp copy of the image - using (Bitmap copy = new Bitmap(width, height, PixelFormat.Format32bppArgb)) - { - Bitmap output = null; - - // Define a pointer to the bitmap data - BitmapData sourceData = null; - try - { - // And construct an 8bpp version - output = new Bitmap(width, height, PixelFormat.Format8bppIndexed); - - // Now lock the bitmap into memory - using (Graphics graphics = Graphics.FromImage(copy)) - { - graphics.PageUnit = GraphicsUnit.Pixel; - - // Draw the source image onto the copy bitmap, - // which will effect a widening as appropriate. - graphics.DrawImage(source, bounds); - } - - // Get the source image bits and lock into memory - sourceData = copy.LockBits(bounds, ImageLockMode.ReadOnly, PixelFormat.Format32bppArgb); - - // Call the FirstPass function if not a single pass algorithm. - // For something like an octree quantizer, this will run through - // all image pixels, build a data structure, and create a palette. - if (!this.singlePass) - { - this.FirstPass(sourceData, width, height); - } - - // Then set the colour palette on the output bitmap. I'm passing in the current palette - // as there's no way to construct a new, empty palette. - output.Palette = this.GetPalette(output.Palette); - - // Then call the second pass which actually does the conversion - this.SecondPass(sourceData, output, width, height, bounds); - } - catch - { - if (output != null) - { - output.Dispose(); - } - } - finally - { - // Ensure that the bits are unlocked - copy.UnlockBits(sourceData); - } - - // Last but not least, return the output bitmap - return output; - } - } - - /// - /// Execute the first pass through the pixels in the image - /// - /// The source data - /// The width in pixels of the image - /// The height in pixels of the image - protected virtual void FirstPass(BitmapData sourceData, int width, int height) - { - // Define the source data pointers. The source row is a byte to - // keep addition of the stride value easier (as this is in bytes) - IntPtr sourceRow = sourceData.Scan0; - - // Loop through each row - for (int row = 0; row < height; row++) - { - // Set the source pixel to the first pixel in this row - IntPtr sourcePixel = sourceRow; - - // And loop through each column - for (int col = 0; col < width; col++) - { - this.InitialQuantizePixel(new Color32(sourcePixel)); - sourcePixel = (IntPtr)((int)sourcePixel + this.pixelSize); - } - - // Now I have the pixel, call the FirstPassQuantize function. - // Add the stride to the source row - sourceRow = (IntPtr)((long)sourceRow + sourceData.Stride); - } - } - - /// - /// Execute a second pass through the bitmap - /// - /// The source bitmap, locked into memory - /// The output bitmap - /// The width in pixels of the image - /// The height in pixels of the image - /// The bounding rectangle - protected virtual void SecondPass(BitmapData sourceData, Bitmap output, int width, int height, Rectangle bounds) - { - BitmapData outputData = null; - - try - { - // Lock the output bitmap into memory - outputData = output.LockBits(bounds, ImageLockMode.WriteOnly, PixelFormat.Format8bppIndexed); - - // Define the source data pointers. The source row is a byte to - // keep addition of the stride value easier (as this is in bytes) - IntPtr sourceRow = sourceData.Scan0; - IntPtr sourcePixel = sourceRow; - IntPtr previousPixel = sourcePixel; - - // Now define the destination data pointers - IntPtr destinationRow = outputData.Scan0; - IntPtr destinationPixel = destinationRow; - - // And convert the first pixel, so that I have values going into the loop. - byte pixelValue = this.QuantizePixel(new Color32(sourcePixel)); - - // Assign the value of the first pixel - Marshal.WriteByte(destinationPixel, pixelValue); - - // Loop through each row - for (int row = 0; row < height; row++) - { - // Set the source pixel to the first pixel in this row - sourcePixel = sourceRow; - - // And set the destination pixel pointer to the first pixel in the row - destinationPixel = destinationRow; - - // Loop through each pixel on this scan line - for (int col = 0; col < width; col++) - { - // Check if this is the same as the last pixel. If so use that value - // rather than calculating it again. This is an inexpensive optimisation. - if (Marshal.ReadByte(previousPixel) != Marshal.ReadByte(sourcePixel)) - { - // Quantize the pixel - pixelValue = this.QuantizePixel(new Color32(sourcePixel)); - - // And setup the previous pointer - previousPixel = sourcePixel; - } - - // And set the pixel in the output - Marshal.WriteByte(destinationPixel, pixelValue); - - sourcePixel = (IntPtr)((long)sourcePixel + this.pixelSize); - destinationPixel = (IntPtr)((long)destinationPixel + 1); - } - - // Add the stride to the source row - sourceRow = (IntPtr)((long)sourceRow + sourceData.Stride); - - // And to the destination row - destinationRow = (IntPtr)((long)destinationRow + outputData.Stride); - } - } - finally - { - // Ensure that I unlock the output bits - output.UnlockBits(outputData); - } - } - - /// - /// Override this to process the pixel in the first pass of the algorithm - /// - /// The pixel to quantize - /// - /// This function need only be overridden if your quantize algorithm needs two passes, - /// such as an Octree quantizer. - /// - protected virtual void InitialQuantizePixel(Color32 pixel) - { - } - - /// - /// Override this to process the pixel in the second pass of the algorithm. - /// - /// The pixel to quantize - /// The quantized value. - protected abstract byte QuantizePixel(Color32 pixel); - - /// - /// Retrieve the palette for the quantized image - /// - /// Any old palette, this is overwritten - /// The new color palette - protected abstract ColorPalette GetPalette(ColorPalette original); - - /// - /// Structure that defines a 32 bit color - /// - /// - /// This structure is used to read data from a 32 bits per pixel image - /// in memory, and is ordered in this manner as this is the way that - /// the data is laid out in memory - /// - [StructLayout(LayoutKind.Explicit)] - public struct Color32 - { - /// - /// Holds the blue component of the color - /// - [FieldOffset(0)] - public byte Blue; - - /// - /// Holds the green component of the color - /// - [FieldOffset(1)] - public byte Green; - - /// - /// Holds the red component of the color - /// - [FieldOffset(2)] - public byte Red; - - /// - /// Holds the alpha component of the color - /// - [FieldOffset(3)] - public byte Alpha; - - /// - /// Permits the color32 to be treated as a 32 bit integer. - /// - [FieldOffset(0)] - public int ARGB; - - /// - /// Initializes a new instance of the Color32 structure. - /// - /// The pointer to the pixel. - public Color32(IntPtr sourcePixel) - { - this = (Color32)Marshal.PtrToStructure(sourcePixel, typeof(Color32)); - } - - /// - /// Gets the color for this Color32 object - /// - public Color Color - { - get { return Color.FromArgb(this.Alpha, this.Red, this.Green, this.Blue); } - } - } - } -} diff --git a/src/ImageProcessor/Processors/Format.cs b/src/ImageProcessor/Processors/Format.cs index 336e5dd4c6..42efc8bd83 100644 --- a/src/ImageProcessor/Processors/Format.cs +++ b/src/ImageProcessor/Processors/Format.cs @@ -1,9 +1,12 @@ -// ----------------------------------------------------------------------- +// -------------------------------------------------------------------------------------------------------------------- // -// Copyright (c) James South. -// Licensed under the Apache License, Version 2.0. +// Copyright (c) James South. +// Licensed under the Apache License, Version 2.0. // -// ----------------------------------------------------------------------- +// +// Sets the output of the image to a specific format. +// +// -------------------------------------------------------------------------------------------------------------------- namespace ImageProcessor.Processors { @@ -22,7 +25,7 @@ namespace ImageProcessor.Processors /// /// The regular expression to search strings for. /// - private static readonly Regex QueryRegex = new Regex(@"format=(jpeg|png|bmp|gif)", RegexOptions.Compiled); + private static readonly Regex QueryRegex = new Regex(@"format=(jpeg|png|png8|bmp|gif|tif)", RegexOptions.Compiled); #region IGraphicsProcessor Members /// @@ -110,17 +113,26 @@ namespace ImageProcessor.Processors public Image ProcessImage(ImageFactory factory) { string format = this.DynamicParameter; + bool isIndexed = false; ImageFormat imageFormat; switch (format) { case "png": imageFormat = ImageFormat.Png; break; + case "png8": + imageFormat = ImageFormat.Png; + isIndexed = true; + break; case "bmp": imageFormat = ImageFormat.Bmp; break; case "gif": imageFormat = ImageFormat.Gif; + isIndexed = true; + break; + case "tif": + imageFormat = ImageFormat.Tiff; break; default: // Should be a jpeg. @@ -129,7 +141,7 @@ namespace ImageProcessor.Processors } // Set the internal property. - factory.Format(imageFormat); + factory.Format(imageFormat, isIndexed); return factory.Image; } diff --git a/src/ImageProcessor/Settings.StyleCop b/src/ImageProcessor/Settings.StyleCop new file mode 100644 index 0000000000..2538255678 --- /dev/null +++ b/src/ImageProcessor/Settings.StyleCop @@ -0,0 +1,11 @@ + + + + + James South + Copyright (c) James South. +Licensed under the Apache License, Version 2.0. + + + + \ No newline at end of file diff --git a/src/TestWebsites/NET45/Test_Website_NET45/Images/Penguins-8.png.REMOVED.git-id b/src/TestWebsites/NET45/Test_Website_NET45/Images/Penguins-8.png.REMOVED.git-id new file mode 100644 index 0000000000..aa9a70e0f4 --- /dev/null +++ b/src/TestWebsites/NET45/Test_Website_NET45/Images/Penguins-8.png.REMOVED.git-id @@ -0,0 +1 @@ +c3d556d9d486b8b8b49cdbcc9c12a9d3a2db4c1f \ No newline at end of file diff --git a/src/TestWebsites/NET45/Test_Website_NET45/Images/Penguins.tif.REMOVED.git-id b/src/TestWebsites/NET45/Test_Website_NET45/Images/Penguins.tif.REMOVED.git-id new file mode 100644 index 0000000000..5f7b97e71a --- /dev/null +++ b/src/TestWebsites/NET45/Test_Website_NET45/Images/Penguins.tif.REMOVED.git-id @@ -0,0 +1 @@ +c789aaec248568c24394b05c02db4233e0c5a4eb \ No newline at end of file diff --git a/src/TestWebsites/NET45/Test_Website_NET45/Images/Thumbs.db.REMOVED.git-id b/src/TestWebsites/NET45/Test_Website_NET45/Images/Thumbs.db.REMOVED.git-id new file mode 100644 index 0000000000..367d139ca7 --- /dev/null +++ b/src/TestWebsites/NET45/Test_Website_NET45/Images/Thumbs.db.REMOVED.git-id @@ -0,0 +1 @@ +4815095bb7f32beb6d9e9564a77c83a64120b5c6 \ No newline at end of file diff --git a/src/TestWebsites/NET45/Test_Website_NET45/Test_Website_NET45.csproj b/src/TestWebsites/NET45/Test_Website_NET45/Test_Website_NET45.csproj index 76b9beb86c..499234b9f2 100644 --- a/src/TestWebsites/NET45/Test_Website_NET45/Test_Website_NET45.csproj +++ b/src/TestWebsites/NET45/Test_Website_NET45/Test_Website_NET45.csproj @@ -136,6 +136,7 @@ + diff --git a/src/TestWebsites/NET45/Test_Website_NET45/Views/Home/Index.cshtml b/src/TestWebsites/NET45/Test_Website_NET45/Views/Home/Index.cshtml index 8e99ec3743..2efa215112 100644 --- a/src/TestWebsites/NET45/Test_Website_NET45/Views/Home/Index.cshtml +++ b/src/TestWebsites/NET45/Test_Website_NET45/Views/Home/Index.cshtml @@ -308,6 +308,102 @@ +
+

Png8

+
+
+
+

Resized

+ +
+
+

Cropped

+ +
+
+
+
+

Filter

+
+
+

blackwhite

+ +
+
+

comic

+ +
+
+
+
+

lomograph

+ +
+
+

greyscale

+ +
+
+
+
+

polaroid

+ +
+
+

sepia

+ +
+
+
+
+

gotham

+ +
+
+

hisatch

+ +
+
+
+
+

losatch

+ +
+
+
+
+
+
+

Watermark

+ +
+
+

Format

+ +
+
+
+
+
+
+

Rotate

+ +
+
+

Quality

+ +
+
+
+
+
+
+

Alpha

+ +
+
+
+

Bmp

@@ -404,3 +500,99 @@
+
+

Tiff

+
+
+
+

Resized

+ +
+
+

Cropped

+ +
+
+
+
+

Filter

+
+
+

blackwhite

+ +
+
+

comic

+ +
+
+
+
+

lomograph

+ +
+
+

greyscale

+ +
+
+
+
+

polaroid

+ +
+
+

sepia

+ +
+
+
+
+

gotham

+ +
+
+

hisatch

+ +
+
+
+
+

losatch

+ +
+
+
+
+
+
+

Watermark

+ +
+
+

Format

+ +
+
+
+
+
+
+

Rotate

+ +
+
+

Quality

+ +
+
+
+
+
+
+

Alpha

+ +
+
+
+