From 901aebb1aff95b4935fca73bc10a8006b7c93d30 Mon Sep 17 00:00:00 2001 From: JimBobSquarePants Date: Mon, 2 Jul 2012 01:00:12 +0100 Subject: [PATCH] Added dropshadow to watermark plus cleanup Former-commit-id: 12a5dedf1664752ae503559c7e4998966b8d78b8 --- src/ImageProcessor/Imaging/OctreeQuantizer.cs | 404 ++++++++++-------- src/ImageProcessor/Imaging/TextLayer.cs | 13 +- src/ImageProcessor/Processors/Filter.cs | 10 + src/ImageProcessor/Processors/Watermark.cs | 41 ++ .../color-vision-test.gif.REMOVED.git-id | 1 + 5 files changed, 278 insertions(+), 191 deletions(-) create mode 100644 src/Test/Test/Images/color-vision-test.gif.REMOVED.git-id diff --git a/src/ImageProcessor/Imaging/OctreeQuantizer.cs b/src/ImageProcessor/Imaging/OctreeQuantizer.cs index d9c9928a2..3cdd17bf0 100644 --- a/src/ImageProcessor/Imaging/OctreeQuantizer.cs +++ b/src/ImageProcessor/Imaging/OctreeQuantizer.cs @@ -121,20 +121,78 @@ namespace ImageProcessor.Imaging /// private class Octree { + #region Fields + /// + /// Mask used when getting the appropriate pixels for a given node + /// + private static int[] mask = new int[8] { 0x80, 0x40, 0x20, 0x10, 0x08, 0x04, 0x02, 0x01 }; + + /// + /// The root of the octree + /// + private 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 maxColorBits) + /// The maximum number of significant bits in the image + public Octree(int maxBits) { - this._maxColorBits = maxColorBits; - this._leafCount = 0; - this._reducibleNodes = new OctreeNode[9]; - this._root = new OctreeNode(0, this._maxColorBits, this); - this._previousColor = 0; - this._previousNode = null; + 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 /// @@ -144,25 +202,25 @@ namespace ImageProcessor.Imaging public void AddColor(Color32 pixel) { // Check if this request is for the same colour as the last - if (this._previousColor == pixel.ARGB) + 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) + if (null == this.previousNode) { - this._previousColor = pixel.ARGB; - this._root.AddColor(pixel, this._maxColorBits, 0, this); + this.previousColor = pixel.ARGB; + this.root.AddColor(pixel, this.maxColorBits, 0, this); } else { // Just update the previous node - this._previousNode.Increment(pixel); + this.previousNode.Increment(pixel); } } else { - this._previousColor = pixel.ARGB; - this._root.AddColor(pixel, this._maxColorBits, 0, this); + this.previousColor = pixel.ARGB; + this.root.AddColor(pixel, this.maxColorBits, 0, this); } } @@ -172,48 +230,22 @@ namespace ImageProcessor.Imaging 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)) + 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; + OctreeNode node = this.reducibleNodes[index]; + this.reducibleNodes[index] = node.NextReducible; // Decrement the leaf count after reducing the node - this._leafCount -= node.Reduce(); + 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; - } - - /// - /// Get or sets the number of leaves in the tree - /// - public int Leaves - { - get { return this._leafCount; } - set { this._leafCount = value; } - } - - /// - /// Return the array of reducible nodes - /// - protected OctreeNode[] ReducibleNodes - { - get { return this._reducibleNodes; } - } - - /// - /// Keep track of the previous node that was quantized - /// - /// The node last quantized - protected void TrackPrevious(OctreeNode node) - { - this._previousNode = node; + this.previousNode = null; } /// @@ -231,92 +263,132 @@ namespace ImageProcessor.Imaging // Now palletize the nodes ArrayList palette = new ArrayList(this.Leaves); int paletteIndex = 0; - this._root.ConstructPalette(palette, ref paletteIndex); + this.root.ConstructPalette(palette, ref paletteIndex); // And return the palette return palette; } /// - /// Get the palette index for the passed colour + /// 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); + return this.root.GetPaletteIndex(pixel, 0); } /// - /// Mask used when getting the appropriate pixels for a given node + /// Keep track of the previous node that was quantized /// - private static int[] mask = new int[8] { 0x80, 0x40, 0x20, 0x10, 0x08, 0x04, 0x02, 0x01 }; + /// The node last quantized + protected void TrackPrevious(OctreeNode node) + { + this.previousNode = node; + } /// - /// The root of the octree + /// Class which encapsulates each node in the tree /// - private OctreeNode _root; + protected class OctreeNode + { + #region Fields + /// + /// Flag indicating that this is a leaf node + /// + private bool leaf; - /// - /// Number of leaves in the tree - /// - private int _leafCount; + /// + /// Number of pixels in this node + /// + private int pixelCount; - /// - /// Array of reducible nodes - /// - private OctreeNode[] _reducibleNodes; + /// + /// Red component + /// + private int red; - /// - /// Maximum number of significant bits in the image - /// - private int _maxColorBits; + /// + /// Green Component + /// + private int green; - /// - /// Store the last node quantized - /// - private OctreeNode _previousNode; + /// + /// Blue component + /// + private int blue; - /// - /// Cache the previous color quantized - /// - private int _previousColor; + /// + /// Pointers to any child nodes + /// + private OctreeNode[] children; - /// - /// Class which encapsulates each node in the tree - /// - protected class OctreeNode - { /// - /// Construct the node + /// The index of this node in the palette /// - /// The level in the tree = 0 - 7 - /// The number of significant color bits in the image - /// The tree to which this node belongs + 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.leaf = level == colorBits; - this._red = _green = _blue = 0; - this._pixelCount = 0; + this.red = this.green = this.blue = 0; + this.pixelCount = 0; // If a leaf, increment the leaf count - if (this._leaf) + if (this.leaf) { octree.Leaves++; - this._nextReducible = null; - this._children = null; + this.NextReducible = null; + this.children = null; } else { // Otherwise add this to the reducible nodes - this._nextReducible = octree.ReducibleNodes[level]; + this.NextReducible = octree.ReducibleNodes[level]; octree.ReducibleNodes[level] = this; - this._children = new OctreeNode[8]; + 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 /// @@ -327,9 +399,10 @@ namespace ImageProcessor.Imaging public void AddColor(Color32 pixel, int colorBits, int level, Octree octree) { // Update the color information if this is a leaf - if (this._leaf) + if (this.leaf) { - Increment(pixel); + this.Increment(pixel); + // Setup the previous node octree.TrackPrevious(this); } @@ -339,38 +412,20 @@ namespace ImageProcessor.Imaging int shift = 7 - level; int index = ((pixel.Red & mask[level]) >> (shift - 2)) | ((pixel.Green & mask[level]) >> (shift - 1)) | - ((pixel.Blue & mask[level]) >> (shift)); + ((pixel.Blue & mask[level]) >> shift); - OctreeNode child = this._children[index]; + 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; + this.Children[index] = child; } // Add the color to the child node child.AddColor(pixel, colorBits, level + 1, octree); } - - } - - /// - /// Get or Sets the next reducible node - /// - public OctreeNode NextReducible - { - get { return _nextReducible; } - set { _nextReducible = value; } - } - - /// - /// Return the child nodes - /// - public OctreeNode[] Children - { - get { return _children; } } /// @@ -379,73 +434,84 @@ namespace ImageProcessor.Imaging /// The number of leaves removed public int Reduce() { - this._red = this._green = this._blue = 0; - int children = 0; + 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]) + 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; - ++children; - this._children[index] = null; + 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; + this.leaf = true; // Return the number of nodes to decrement the leaf count by - return children - 1; + return childPosition - 1; } /// /// Traverse the tree, building up the color palette /// /// The palette - /// The current palette index - public void ConstructPalette(ArrayList palette, ref int paletteIndex) + /// The current palette index + public void ConstructPalette(ArrayList palette, ref int currentPaletteIndex) { - if (_leaf) + if (this.leaf) { // Consume the next palette index - _paletteIndex = paletteIndex++; + this.paletteIndex = currentPaletteIndex++; // And set the color of the palette entry - palette.Add(Color.FromArgb(_red / _pixelCount, _green / _pixelCount, _blue / _pixelCount)); + 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 != _children[index]) - _children[index].ConstructPalette(palette, ref paletteIndex); + if (null != this.children[index]) + { + this.children[index].ConstructPalette(palette, ref currentPaletteIndex); + } } } } /// - /// Return the palette index for the passed color + /// 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 paletteIndex = _paletteIndex; + int currentPaletteIndex = this.paletteIndex; - if (!_leaf) + 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)); + ((pixel.Blue & mask[level]) >> shift); - if (null != _children[index]) + if (null != this.children[index]) { - paletteIndex = _children[index].GetPaletteIndex(pixel, level + 1); + currentPaletteIndex = this.children[index].GetPaletteIndex(pixel, level + 1); } else { @@ -453,59 +519,23 @@ namespace ImageProcessor.Imaging } } - return paletteIndex; + 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; - } - - /// - /// 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; - - /// - /// Pointer to next reducible node - /// - private OctreeNode _nextReducible; - - /// - /// The index of this node in the palette - /// - private int _paletteIndex; + this.pixelCount++; + this.red += pixel.Red; + this.green += pixel.Green; + this.blue += pixel.Blue; + } + #endregion } } } diff --git a/src/ImageProcessor/Imaging/TextLayer.cs b/src/ImageProcessor/Imaging/TextLayer.cs index 785daac0f..9d2bc9f61 100644 --- a/src/ImageProcessor/Imaging/TextLayer.cs +++ b/src/ImageProcessor/Imaging/TextLayer.cs @@ -51,7 +51,7 @@ namespace ImageProcessor.Imaging public string Text { get; set; } /// - /// Gets or sets TextColor. + /// Gets or sets the Color to render the font. /// public Color TextColor { @@ -74,7 +74,7 @@ namespace ImageProcessor.Imaging } /// - /// Gets or sets the FontStyle. + /// Gets or sets the FontStyle of the textlayer. /// public FontStyle Style { @@ -83,7 +83,7 @@ namespace ImageProcessor.Imaging } /// - /// Gets or sets the Opacity. + /// Gets or sets the Opacity of the textlayer. /// public int Opacity { @@ -101,13 +101,18 @@ namespace ImageProcessor.Imaging } /// - /// Gets or sets Position. + /// Gets or sets the Position of the textlayer. /// public Point Position { get { return this.position; } set { this.position = value; } } + + /// + /// Gets or sets a value indicating whether a DropShadow should be drawn. + /// + public bool DropShadow { get; set; } #endregion } } diff --git a/src/ImageProcessor/Processors/Filter.cs b/src/ImageProcessor/Processors/Filter.cs index 0dec514bf..df31ffce8 100644 --- a/src/ImageProcessor/Processors/Filter.cs +++ b/src/ImageProcessor/Processors/Filter.cs @@ -165,6 +165,7 @@ namespace ImageProcessor.Processors { Bitmap newImage = null; Image image = factory.Image; + // Bitmaps for comic pattern Bitmap hisatchBitmap = null; Bitmap patternBitmap = null; @@ -329,6 +330,15 @@ namespace ImageProcessor.Processors } } + // Add a vignette to finish the effect. + // TODO: This feels a bit mucky so I might chop it out. + if (this.DynamicParameter == "polaroid" || this.DynamicParameter == "lomograph") + { + factory.Image = newImage; + Vignette vignette = new Vignette(); + newImage = (Bitmap)vignette.ProcessImage(factory); + } + // Reassign the image. image.Dispose(); image = newImage; diff --git a/src/ImageProcessor/Processors/Watermark.cs b/src/ImageProcessor/Processors/Watermark.cs index 48a56cc05..147ee9fe9 100644 --- a/src/ImageProcessor/Processors/Watermark.cs +++ b/src/ImageProcessor/Processors/Watermark.cs @@ -7,9 +7,12 @@ namespace ImageProcessor.Processors { #region Using + + using System; using System.Collections.Generic; using System.Drawing; using System.Drawing.Text; + using System.Linq; using System.Text.RegularExpressions; using ImageProcessor.Helpers.Extensions; using ImageProcessor.Imaging; @@ -60,6 +63,11 @@ namespace ImageProcessor.Processors /// private static readonly Regex OpacityRegex = new Regex(@"opacity-\d{1,2}(?!\d)|opacity-100", RegexOptions.Compiled); + /// + /// The regular expression to search strings for the shadow attribute. + /// + private static readonly Regex ShadowRegex = new Regex(@"shadow-true", RegexOptions.Compiled); + #region IGraphicsProcessor Members /// /// Gets the name. @@ -157,6 +165,7 @@ namespace ImageProcessor.Processors textLayer.Font = this.ParseFontFamily(toParse); textLayer.Style = this.ParseFontStyle(toParse); textLayer.Opacity = this.ParseOpacity(toParse); + textLayer.DropShadow = this.ParseDropShadow(toParse); this.DynamicParameter = textLayer; } @@ -218,6 +227,23 @@ namespace ImageProcessor.Processors // Set the hinting and draw the text. graphics.TextRenderingHint = TextRenderingHint.AntiAliasGridFit; + + if (textLayer.DropShadow) + { + // Shadow opacity should change with the base opacity. + int shadowOpacity = opacity - (int)Math.Ceiling((30 / 100f) * 255); + int finalShadowOpacity = shadowOpacity > 0 ? shadowOpacity : 0; + + using (Brush shadowBrush = new SolidBrush(Color.FromArgb(finalShadowOpacity, Color.Black))) + { + // Scale the shadow position to match the font size. + // Magic number but it's based on artistic preference. + int shadowDiff = (int)Math.Ceiling(fontSize / 24f); + Point shadowPoint = new Point(origin.X + shadowDiff, origin.Y + shadowDiff); + graphics.DrawString(text, font, shadowBrush, shadowPoint, drawFormat); + } + } + graphics.DrawString(text, font, brush, origin, drawFormat); } } @@ -439,6 +465,21 @@ namespace ImageProcessor.Processors // full opacity - matches the Textlayer default. return 100; } + + /// + /// Returns a value indicating whether the watermark is to have a shadow. + /// + /// + /// The input string containing the value to parse. + /// + /// + /// The true if the watermark is to have a shadow; otherwise false. + /// + private bool ParseDropShadow(string input) + { + return ShadowRegex.Matches(input).Cast().Any(); + } + #endregion } } diff --git a/src/Test/Test/Images/color-vision-test.gif.REMOVED.git-id b/src/Test/Test/Images/color-vision-test.gif.REMOVED.git-id new file mode 100644 index 000000000..5c4f4195d --- /dev/null +++ b/src/Test/Test/Images/color-vision-test.gif.REMOVED.git-id @@ -0,0 +1 @@ +35a926115b13b61dc37308f8d903b42d14f92924 \ No newline at end of file