diff --git a/src/ImageProcessor/Extensions/ImageExtensions.cs b/src/ImageProcessor/Extensions/ImageExtensions.cs
index e45bb8881..5ddcd96de 100644
--- a/src/ImageProcessor/Extensions/ImageExtensions.cs
+++ b/src/ImageProcessor/Extensions/ImageExtensions.cs
@@ -11,9 +11,12 @@
namespace ImageProcessor.Extensions
{
using System;
+ using System.Collections.Generic;
using System.Drawing;
using System.Drawing.Imaging;
+ using ImageProcessor.Imaging;
+
///
/// Encapsulates a series of time saving extension methods to the class.
///
@@ -48,61 +51,41 @@ namespace ImageProcessor.Extensions
int frameCount = image.GetFrameCount(frameDimension);
int delay = 0;
+ int[] delays = new int[frameCount];
int index = 0;
+ List gifFrames = new List();
for (int f = 0; f < frameCount; f++)
{
int thisDelay = BitConverter.ToInt32(image.GetPropertyItem(20736).Value, index) * 10;
- delay += thisDelay < 100 ? 100 : thisDelay; // Minimum delay is 100 ms
+ thisDelay = thisDelay < 100 ? 100 : thisDelay; // Minimum delay is 100 ms
+ delays[f] = thisDelay;
+
+ // Find the frame
+ image.SelectActiveFrame(frameDimension, f);
+
+ // TODO: Get positions.
+ gifFrames.Add(new GifFrame
+ {
+ Delay = thisDelay,
+ Image = (Image)image.Clone()
+ });
+
+ delay += thisDelay;
index += 4;
}
info.AnimationLength = delay;
info.IsAnimated = true;
+ info.LoopCount = BitConverter.ToInt16(image.GetPropertyItem(20737).Value, 0);
+
// Loop info is stored at byte 20737.
- info.IsLooped = BitConverter.ToInt16(image.GetPropertyItem(20737).Value, 0) != 1;
+ info.IsLooped = info.LoopCount != 1;
}
}
return info;
}
-
- ///
- /// Provides information about an image.
- ///
- ///
- public struct ImageInfo
- {
- ///
- /// The image width.
- ///
- public int Width;
-
- ///
- /// The image height.
- ///
- public int Height;
-
- ///
- /// Whether the is indexed.
- ///
- public bool IsIndexed;
-
- ///
- /// Whether the is animated.
- ///
- public bool IsAnimated;
-
- ///
- /// The is looped.
- ///
- public bool IsLooped;
-
- ///
- /// The animation length in milliseconds.
- ///
- public int AnimationLength;
- }
}
}
diff --git a/src/ImageProcessor/Extensions/ImageInfo.cs b/src/ImageProcessor/Extensions/ImageInfo.cs
new file mode 100644
index 000000000..50efa85d9
--- /dev/null
+++ b/src/ImageProcessor/Extensions/ImageInfo.cs
@@ -0,0 +1,64 @@
+// --------------------------------------------------------------------------------------------------------------------
+//
+// Copyright (c) James South.
+// Licensed under the Apache License, Version 2.0.
+//
+//
+// Provides information about an image.
+//
+//
+// --------------------------------------------------------------------------------------------------------------------
+
+namespace ImageProcessor.Extensions
+{
+ using System.Collections.Generic;
+
+ using ImageProcessor.Imaging;
+
+ ///
+ /// Provides information about an image.
+ ///
+ ///
+ public class ImageInfo
+ {
+ ///
+ /// Gets or sets the image width.
+ ///
+ public int Width { get; set; }
+
+ ///
+ /// Gets or sets the image height.
+ ///
+ public int Height { get; set; }
+
+ ///
+ /// Gets or sets a value indicating whether the image is indexed.
+ ///
+ public bool IsIndexed { get; set; }
+
+ ///
+ /// Gets or sets a value indicating whether the image is animated.
+ ///
+ public bool IsAnimated { get; set; }
+
+ ///
+ /// Gets or sets a value indicating whether the image is looped.
+ ///
+ public bool IsLooped { get; set; }
+
+ ///
+ /// Gets or sets the loop count.
+ ///
+ public int LoopCount { get; set; }
+
+ ///
+ /// Gets or sets the gif frames.
+ ///
+ public ICollection GifFrames { get; set; }
+
+ ///
+ /// Gets or sets the animation length in milliseconds.
+ ///
+ public int AnimationLength { get; set; }
+ }
+}
\ No newline at end of file
diff --git a/src/ImageProcessor/ImageFactory.cs b/src/ImageProcessor/ImageFactory.cs
index 714b6983a..5facec8cd 100644
--- a/src/ImageProcessor/ImageFactory.cs
+++ b/src/ImageProcessor/ImageFactory.cs
@@ -653,7 +653,25 @@ namespace ImageProcessor
Resize resize = new Resize { DynamicParameter = resizeLayer, Settings = resizeSettings };
- this.Image = resize.ProcessImage(this);
+ ImageInfo imageInfo = this.Image.GetImageInfo();
+
+ if (imageInfo.IsAnimated)
+ {
+ using (GifEncoder encoder = new GifEncoder(new MemoryStream(4096), resizeLayer.Size.Width, resizeLayer.Size.Height, imageInfo.LoopCount))
+ {
+ foreach (GifFrame frame in imageInfo.GifFrames)
+ {
+ frame.Image = ColorQuantizer.Quantize(resize.ProcessImage(this), PixelFormat.Format8bppIndexed);
+ encoder.AddFrame(frame);
+ }
+
+
+ }
+ }
+ else
+ {
+ this.Image = resize.ProcessImage(this);
+ }
}
return this;
@@ -957,6 +975,32 @@ namespace ImageProcessor
}
#endregion
+ ///
+ /// Gets a list of images contained within an animated gif.
+ ///
+ ///
+ /// The gif image.
+ ///
+ ///
+ /// The .
+ ///
+ protected IEnumerable GetImageFrames(Image gifImage)
+ {
+ // Gets the GUID
+ FrameDimension dimension = new FrameDimension(gifImage.FrameDimensionsList[0]);
+
+ // Total frames in the animation
+ int frameCount = gifImage.GetFrameCount(dimension);
+ for (int index = 0; index < frameCount; index++)
+ {
+ // Find the frame
+ gifImage.SelectActiveFrame(dimension, index);
+
+ // Return a copy of it
+ yield return (Image)gifImage.Clone();
+ }
+ }
+
///
/// Uses the
/// to fix the color palette of gif images.
diff --git a/src/ImageProcessor/ImageProcessor.csproj b/src/ImageProcessor/ImageProcessor.csproj
index 723d88c68..66959a3d7 100644
--- a/src/ImageProcessor/ImageProcessor.csproj
+++ b/src/ImageProcessor/ImageProcessor.csproj
@@ -62,6 +62,7 @@
+
@@ -72,6 +73,7 @@
+
diff --git a/src/ImageProcessor/Imaging/GifEncoder.cs b/src/ImageProcessor/Imaging/GifEncoder.cs
index 0e300ce47..4b00a873e 100644
--- a/src/ImageProcessor/Imaging/GifEncoder.cs
+++ b/src/ImageProcessor/Imaging/GifEncoder.cs
@@ -1,8 +1,27 @@
-
+// --------------------------------------------------------------------------------------------------------------------
+//
+// Copyright (c) James South.
+// Licensed under the Apache License, Version 2.0.
+//
+//
+// Encodes multiple images as an animated gif to a stream.
+//
+// Always wire this up in a using block.
+// Disposing the encoder will complete the file.
+// Uses default .NET GIF encoding and adds animation headers.
+//
+//
+// --------------------------------------------------------------------------------------------------------------------
namespace ImageProcessor.Imaging
{
+ #region Using
using System;
+ using System.Drawing;
+ using System.Drawing.Imaging;
+ using System.IO;
+ using System.Linq;
+ #endregion
///
/// Encodes multiple images as an animated gif to a stream.
@@ -14,25 +33,93 @@ namespace ImageProcessor.Imaging
///
public class GifEncoder : IDisposable
{
- #region Fields
- private const string FileType = "GIF";
- private const string FileVersion = "89a";
- private const byte FileTrailer = 0x3b;
+ #region Constants
+ ///
+ /// The application block size.
+ ///
+ private const byte ApplicationBlockSize = 0x0b;
+ ///
+ /// The application extension block identifier.
+ ///
private const int ApplicationExtensionBlockIdentifier = 0xff21;
- private const byte ApplicationBlockSize = 0x0b;
+
+ ///
+ /// The application identification.
+ ///
private const string ApplicationIdentification = "NETSCAPE2.0";
+ ///
+ /// The file trailer.
+ ///
+ private const byte FileTrailer = 0x3b;
+
+ ///
+ /// The file type.
+ ///
+ private const string FileType = "GIF";
+
+ ///
+ /// The file version.
+ ///
+ private const string FileVersion = "89a";
+
+ ///
+ /// The graphic control extension block identifier.
+ ///
private const int GraphicControlExtensionBlockIdentifier = 0xf921;
+
+ ///
+ /// The graphic control extension block size.
+ ///
private const byte GraphicControlExtensionBlockSize = 0x04;
+ ///
+ /// The source color block length.
+ ///
+ private const long SourceColorBlockLength = 768;
+
+ ///
+ /// The source color block position.
+ ///
+ private const long SourceColorBlockPosition = 13;
+
+ ///
+ /// The source global color info position.
+ ///
private const long SourceGlobalColorInfoPosition = 10;
- private const long SourceGraphicControlExtensionPosition = 781;
+
+ ///
+ /// The source graphic control extension length.
+ ///
private const long SourceGraphicControlExtensionLength = 8;
- private const long SourceImageBlockPosition = 789;
+
+ ///
+ /// The source graphic control extension position.
+ ///
+ private const long SourceGraphicControlExtensionPosition = 781;
+
+ ///
+ /// The source image block header length.
+ ///
private const long SourceImageBlockHeaderLength = 11;
- private const long SourceColorBlockPosition = 13;
- private const long SourceColorBlockLength = 768;
+
+ ///
+ /// The source image block position.
+ ///
+ private const long SourceImageBlockPosition = 789;
+ #endregion
+
+ #region Fields
+ ///
+ /// The stream.
+ ///
+ private Stream inputStream;
+
+ ///
+ /// The height.
+ ///
+ private int? height;
///
/// A value indicating whether this instance of the given entity has been disposed.
@@ -46,9 +133,103 @@ namespace ImageProcessor.Imaging
/// life in the Garbage Collector.
///
private bool isDisposed;
+
+ ///
+ /// The is first image.
+ ///
+ private bool isFirstImage = true;
+
+ ///
+ /// The repeat count.
+ ///
+ private int? repeatCount;
+
+ ///
+ /// The width.
+ ///
+ private int? width;
+ #endregion
+
+ #region Constructors and Destructors
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ ///
+ /// The stream that will be written to.
+ ///
+ ///
+ /// Sets the width for this gif or null to use the first frame's width.
+ ///
+ ///
+ /// Sets the height for this gif or null to use the first frame's height.
+ ///
+ ///
+ /// The number of times to repeat the animation.
+ ///
+ public GifEncoder(Stream stream, int? width = null, int? height = null, int? repeatCount = null)
+ {
+ this.inputStream = stream;
+ this.width = width;
+ this.height = height;
+ this.repeatCount = repeatCount;
+ }
+
+ ///
+ /// Finalizes an instance of the class.
+ ///
+ ///
+ /// Use C# destructor syntax for finalization code.
+ /// This destructor will run only if the Dispose method
+ /// does not get called.
+ /// It gives your base class the opportunity to finalize.
+ /// Do not provide destructors in types derived from this class.
+ ///
+ ~GifEncoder()
+ {
+ // Do not re-create Dispose clean-up code here.
+ // Calling Dispose(false) is optimal in terms of
+ // readability and maintainability.
+ this.Dispose(false);
+ }
+ #endregion
+
+ #region Properties
+ ///
+ /// Gets or sets the frame delay.
+ ///
+ public TimeSpan FrameDelay { get; set; }
#endregion
- #region IDisposable Members
+ #region Public Methods and Operators
+ ///
+ /// Adds a frame to the gif.
+ ///
+ ///
+ /// The containing the image.
+ ///
+ public void AddFrame(GifFrame frame)
+ {
+ using (MemoryStream gifStream = new MemoryStream())
+ {
+ frame.Image.Save(gifStream, ImageFormat.Gif);
+ if (this.isFirstImage)
+ {
+ // Steal the global color table info
+ this.WriteHeaderBlock(gifStream, frame.Image.Width, frame.Image.Height);
+ }
+
+ this.WriteGraphicControlBlock(gifStream, frame.Delay);
+ this.WriteImageBlock(gifStream, !this.isFirstImage, frame.X, frame.Y, frame.Image.Width, frame.Image.Height);
+ }
+
+ this.isFirstImage = false;
+ }
+
+ public Image Save()
+ {
+
+ }
+
///
/// Disposes the object and frees resources for the Garbage Collector.
///
@@ -63,11 +244,15 @@ namespace ImageProcessor.Imaging
// from executing a second time.
GC.SuppressFinalize(this);
}
+ #endregion
+ #region Methods
///
/// Disposes the object and frees resources for the Garbage Collector.
///
- /// If true, the object gets disposed.
+ ///
+ /// If true, the object gets disposed.
+ ///
protected virtual void Dispose(bool disposing)
{
if (this.isDisposed)
@@ -78,18 +263,21 @@ namespace ImageProcessor.Imaging
if (disposing)
{
// Dispose of any managed resources here.
- //if (this.Image != null)
- //{
- // // Dispose of the memory stream from Load and the image.
- // if (this.inputStream != null)
- // {
- // this.inputStream.Dispose();
- // this.inputStream = null;
- // }
-
- // this.Image.Dispose();
- // this.Image = null;
- //}
+ // Complete Application Block
+ this.WriteByte(0);
+
+ // Complete File
+ this.WriteByte(FileTrailer);
+
+ // Pushing data
+ this.inputStream.Flush();
+
+ // Dispose of the memory stream from Load and the image.
+ if (this.inputStream != null)
+ {
+ this.inputStream.Dispose();
+ this.inputStream = null;
+ }
}
// Call the appropriate methods to clean up
@@ -97,6 +285,183 @@ namespace ImageProcessor.Imaging
// Note disposing is done.
this.isDisposed = true;
}
+
+ ///
+ /// Writes the header block of the animated gif to the stream.
+ ///
+ ///
+ /// The source gif.
+ ///
+ ///
+ /// The width of the image.
+ ///
+ ///
+ /// The height of the image.
+ ///
+ private void WriteHeaderBlock(Stream sourceGif, int w, int h)
+ {
+ int count = this.repeatCount.GetValueOrDefault(0);
+
+ // File Header
+ this.WriteString(FileType);
+ this.WriteString(FileVersion);
+ this.WriteShort(this.width.GetValueOrDefault(w)); // Initial Logical Width
+ this.WriteShort(this.height.GetValueOrDefault(h)); // Initial Logical Height
+ sourceGif.Position = SourceGlobalColorInfoPosition;
+ this.WriteByte(sourceGif.ReadByte()); // Global Color Table Info
+ this.WriteByte(0); // Background Color Index
+ this.WriteByte(0); // Pixel aspect ratio
+ this.WriteColorTable(sourceGif);
+
+ // The different browsers interpret the spec differently when adding a loop.
+ // If the loop count is one IE and FF < 3 (incorrectly) loop an extra number of times.
+ // Removing the Netscape header should fix this.
+ if (count != 1)
+ {
+ // Application Extension Header
+ this.WriteShort(ApplicationExtensionBlockIdentifier);
+ this.WriteByte(ApplicationBlockSize);
+ this.WriteString(ApplicationIdentification);
+ this.WriteByte(3); // Application block length
+ this.WriteByte(1);
+ this.WriteShort(this.repeatCount.GetValueOrDefault(0)); // Repeat count for images.
+ this.WriteByte(0); // Terminator
+ }
+ }
+
+ ///
+ /// The write byte.
+ ///
+ ///
+ /// The value.
+ ///
+ private void WriteByte(int value)
+ {
+ this.inputStream.WriteByte(Convert.ToByte(value));
+ }
+
+ ///
+ /// The write color table.
+ ///
+ ///
+ /// The source gif.
+ ///
+ private void WriteColorTable(Stream sourceGif)
+ {
+ sourceGif.Position = SourceColorBlockPosition; // Locating the image color table
+ byte[] colorTable = new byte[SourceColorBlockLength];
+ sourceGif.Read(colorTable, 0, colorTable.Length);
+ this.inputStream.Write(colorTable, 0, colorTable.Length);
+ }
+
+ ///
+ /// The write graphic control block.
+ ///
+ ///
+ /// The source gif.
+ ///
+ ///
+ /// The frame delay.
+ ///
+ private void WriteGraphicControlBlock(Stream sourceGif, int frameDelay)
+ {
+ sourceGif.Position = SourceGraphicControlExtensionPosition; // Locating the source GCE
+ byte[] blockhead = new byte[SourceGraphicControlExtensionLength];
+ sourceGif.Read(blockhead, 0, blockhead.Length); // Reading source GCE
+
+ this.WriteShort(GraphicControlExtensionBlockIdentifier); // Identifier
+ this.WriteByte(GraphicControlExtensionBlockSize); // Block Size
+ this.WriteByte(blockhead[3] & 0xf7 | 0x08); // Setting disposal flag
+ this.WriteShort(Convert.ToInt32(frameDelay / 10)); // Setting frame delay
+ this.WriteByte(blockhead[6]); // Transparent color index
+ this.WriteByte(0); // Terminator
+ }
+
+ ///
+ /// The write image block.
+ ///
+ ///
+ /// The source gif.
+ ///
+ ///
+ /// The include color table.
+ ///
+ ///
+ /// The x position to write the image block.
+ ///
+ ///
+ /// The y position to write the image block.
+ ///
+ ///
+ /// The height of the image block.
+ ///
+ ///
+ /// The width of the image block.
+ ///
+ private void WriteImageBlock(Stream sourceGif, bool includeColorTable, int x, int y, int h, int w)
+ {
+ sourceGif.Position = SourceImageBlockPosition; // Locating the image block
+ byte[] header = new byte[SourceImageBlockHeaderLength];
+ sourceGif.Read(header, 0, header.Length);
+ this.WriteByte(header[0]); // Separator
+ this.WriteShort(x); // Position X
+ this.WriteShort(y); // Position Y
+ this.WriteShort(h); // Height
+ this.WriteShort(w); // Width
+
+ if (includeColorTable)
+ {
+ // If first frame, use global color table - else use local
+ sourceGif.Position = SourceGlobalColorInfoPosition;
+ this.WriteByte(sourceGif.ReadByte() & 0x3f | 0x80); // Enabling local color table
+ this.WriteColorTable(sourceGif);
+ }
+ else
+ {
+ this.WriteByte(header[9] & 0x07 | 0x07); // Disabling local color table
+ }
+
+ this.WriteByte(header[10]); // LZW Min Code Size
+
+ // Read/Write image data
+ sourceGif.Position = SourceImageBlockPosition + SourceImageBlockHeaderLength;
+
+ int dataLength = sourceGif.ReadByte();
+ while (dataLength > 0)
+ {
+ byte[] imgData = new byte[dataLength];
+ sourceGif.Read(imgData, 0, dataLength);
+
+ this.inputStream.WriteByte(Convert.ToByte(dataLength));
+ this.inputStream.Write(imgData, 0, dataLength);
+ dataLength = sourceGif.ReadByte();
+ }
+
+ this.inputStream.WriteByte(0); // Terminator
+ }
+
+ ///
+ /// The write short.
+ ///
+ ///
+ /// The value.
+ ///
+ private void WriteShort(int value)
+ {
+ this.inputStream.WriteByte(Convert.ToByte(value & 0xff));
+ this.inputStream.WriteByte(Convert.ToByte((value >> 8) & 0xff));
+ }
+
+ ///
+ /// The write string.
+ ///
+ ///
+ /// The value.
+ ///
+ private void WriteString(string value)
+ {
+ this.inputStream.Write(value.ToArray().Select(c => (byte)c).ToArray(), 0, value.Length);
+ }
#endregion
}
-}
+}
\ No newline at end of file
diff --git a/src/ImageProcessor/Imaging/GifFrame.cs b/src/ImageProcessor/Imaging/GifFrame.cs
new file mode 100644
index 000000000..9a2e1e20b
--- /dev/null
+++ b/src/ImageProcessor/Imaging/GifFrame.cs
@@ -0,0 +1,40 @@
+// --------------------------------------------------------------------------------------------------------------------
+//
+// Copyright (c) James South.
+// Licensed under the Apache License, Version 2.0.
+//
+//
+// A single gif frame.
+//
+// --------------------------------------------------------------------------------------------------------------------
+
+namespace ImageProcessor.Imaging
+{
+ using System.Drawing;
+
+ ///
+ /// A single gif frame.
+ ///
+ public class GifFrame
+ {
+ ///
+ /// Gets or sets the image.
+ ///
+ public Image Image { get; set; }
+
+ ///
+ /// Gets or sets the delay in milliseconds.
+ ///
+ public int Delay { get; set; }
+
+ ///
+ /// Gets or sets the x position of the frame.
+ ///
+ public int X { get; set; }
+
+ ///
+ /// Gets or sets the Y position of the frame.
+ ///
+ public int Y { get; set; }
+ }
+}