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; } + } +}