Browse Source

Moar gif stuff.

Doesn't build


Former-commit-id: 7b742ca2539578900bfee7bb09735b468f9e6974
pull/17/head
James South 12 years ago
parent
commit
8001bd5cda
  1. 61
      src/ImageProcessor/Extensions/ImageExtensions.cs
  2. 64
      src/ImageProcessor/Extensions/ImageInfo.cs
  3. 46
      src/ImageProcessor/ImageFactory.cs
  4. 2
      src/ImageProcessor/ImageProcessor.csproj
  5. 415
      src/ImageProcessor/Imaging/GifEncoder.cs
  6. 40
      src/ImageProcessor/Imaging/GifFrame.cs

61
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;
/// <summary>
/// Encapsulates a series of time saving extension methods to the <see cref="T:System.Drawing.Imaging.Image" /> class.
/// </summary>
@ -48,61 +51,41 @@ namespace ImageProcessor.Extensions
int frameCount = image.GetFrameCount(frameDimension);
int delay = 0;
int[] delays = new int[frameCount];
int index = 0;
List<GifFrame> gifFrames = new List<GifFrame>();
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;
}
/// <summary>
/// Provides information about an image.
/// <see cref="http://madskristensen.net/post/examine-animated-gife28099s-in-c"/>
/// </summary>
public struct ImageInfo
{
/// <summary>
/// The image width.
/// </summary>
public int Width;
/// <summary>
/// The image height.
/// </summary>
public int Height;
/// <summary>
/// Whether the is indexed.
/// </summary>
public bool IsIndexed;
/// <summary>
/// Whether the is animated.
/// </summary>
public bool IsAnimated;
/// <summary>
/// The is looped.
/// </summary>
public bool IsLooped;
/// <summary>
/// The animation length in milliseconds.
/// </summary>
public int AnimationLength;
}
}
}

64
src/ImageProcessor/Extensions/ImageInfo.cs

@ -0,0 +1,64 @@
// --------------------------------------------------------------------------------------------------------------------
// <copyright file="ImageInfo.cs" company="James South">
// Copyright (c) James South.
// Licensed under the Apache License, Version 2.0.
// </copyright>
// <summary>
// Provides information about an image.
// <see cref="http://madskristensen.net/post/examine-animated-gife28099s-in-c" />
// </summary>
// --------------------------------------------------------------------------------------------------------------------
namespace ImageProcessor.Extensions
{
using System.Collections.Generic;
using ImageProcessor.Imaging;
/// <summary>
/// Provides information about an image.
/// <see cref="http://madskristensen.net/post/examine-animated-gife28099s-in-c"/>
/// </summary>
public class ImageInfo
{
/// <summary>
/// Gets or sets the image width.
/// </summary>
public int Width { get; set; }
/// <summary>
/// Gets or sets the image height.
/// </summary>
public int Height { get; set; }
/// <summary>
/// Gets or sets a value indicating whether the image is indexed.
/// </summary>
public bool IsIndexed { get; set; }
/// <summary>
/// Gets or sets a value indicating whether the image is animated.
/// </summary>
public bool IsAnimated { get; set; }
/// <summary>
/// Gets or sets a value indicating whether the image is looped.
/// </summary>
public bool IsLooped { get; set; }
/// <summary>
/// Gets or sets the loop count.
/// </summary>
public int LoopCount { get; set; }
/// <summary>
/// Gets or sets the gif frames.
/// </summary>
public ICollection<GifFrame> GifFrames { get; set; }
/// <summary>
/// Gets or sets the animation length in milliseconds.
/// </summary>
public int AnimationLength { get; set; }
}
}

46
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
/// <summary>
/// Gets a list of images contained within an animated gif.
/// </summary>
/// <param name="gifImage">
/// The gif image.
/// </param>
/// <returns>
/// The <see cref="IEnumerable{Image}"/>.
/// </returns>
protected IEnumerable<Image> 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();
}
}
/// <summary>
/// Uses the <see cref="T:ImageProcessor.Imaging.ColorQuantizer"/>
/// to fix the color palette of gif images.

2
src/ImageProcessor/ImageProcessor.csproj

@ -62,6 +62,7 @@
<Compile Include="Extensions\DoubleExtensions.cs" />
<Compile Include="Extensions\ImageExtensions.cs" />
<Compile Include="Extensions\ImageFormatExtensions.cs" />
<Compile Include="Extensions\ImageInfo.cs" />
<Compile Include="Extensions\IntegerExtensions.cs" />
<Compile Include="Extensions\StringExtensions.cs" />
<Compile Include="ImageFactory.cs" />
@ -72,6 +73,7 @@
<Compile Include="Imaging\GaussianLayer.cs" />
<Compile Include="Imaging\ColorQuantizer.cs" />
<Compile Include="Imaging\GifEncoder.cs" />
<Compile Include="Imaging\GifFrame.cs" />
<Compile Include="Imaging\ResizeLayer.cs" />
<Compile Include="Imaging\Filters\BlackWhiteMatrixFilter.cs" />
<Compile Include="Imaging\Filters\ColorMatrixes.cs" />

415
src/ImageProcessor/Imaging/GifEncoder.cs

@ -1,8 +1,27 @@

// --------------------------------------------------------------------------------------------------------------------
// <copyright file="GifEncoder.cs" company="James South">
// Copyright (c) James South.
// Licensed under the Apache License, Version 2.0.
// </copyright>
// <summary>
// Encodes multiple images as an animated gif to a stream.
// <remarks>
// Always wire this up in a using block.
// Disposing the encoder will complete the file.
// Uses default .NET GIF encoding and adds animation headers.
// </remarks>
// </summary>
// --------------------------------------------------------------------------------------------------------------------
namespace ImageProcessor.Imaging
{
#region Using
using System;
using System.Drawing;
using System.Drawing.Imaging;
using System.IO;
using System.Linq;
#endregion
/// <summary>
/// Encodes multiple images as an animated gif to a stream.
@ -14,25 +33,93 @@ namespace ImageProcessor.Imaging
/// </summary>
public class GifEncoder : IDisposable
{
#region Fields
private const string FileType = "GIF";
private const string FileVersion = "89a";
private const byte FileTrailer = 0x3b;
#region Constants
/// <summary>
/// The application block size.
/// </summary>
private const byte ApplicationBlockSize = 0x0b;
/// <summary>
/// The application extension block identifier.
/// </summary>
private const int ApplicationExtensionBlockIdentifier = 0xff21;
private const byte ApplicationBlockSize = 0x0b;
/// <summary>
/// The application identification.
/// </summary>
private const string ApplicationIdentification = "NETSCAPE2.0";
/// <summary>
/// The file trailer.
/// </summary>
private const byte FileTrailer = 0x3b;
/// <summary>
/// The file type.
/// </summary>
private const string FileType = "GIF";
/// <summary>
/// The file version.
/// </summary>
private const string FileVersion = "89a";
/// <summary>
/// The graphic control extension block identifier.
/// </summary>
private const int GraphicControlExtensionBlockIdentifier = 0xf921;
/// <summary>
/// The graphic control extension block size.
/// </summary>
private const byte GraphicControlExtensionBlockSize = 0x04;
/// <summary>
/// The source color block length.
/// </summary>
private const long SourceColorBlockLength = 768;
/// <summary>
/// The source color block position.
/// </summary>
private const long SourceColorBlockPosition = 13;
/// <summary>
/// The source global color info position.
/// </summary>
private const long SourceGlobalColorInfoPosition = 10;
private const long SourceGraphicControlExtensionPosition = 781;
/// <summary>
/// The source graphic control extension length.
/// </summary>
private const long SourceGraphicControlExtensionLength = 8;
private const long SourceImageBlockPosition = 789;
/// <summary>
/// The source graphic control extension position.
/// </summary>
private const long SourceGraphicControlExtensionPosition = 781;
/// <summary>
/// The source image block header length.
/// </summary>
private const long SourceImageBlockHeaderLength = 11;
private const long SourceColorBlockPosition = 13;
private const long SourceColorBlockLength = 768;
/// <summary>
/// The source image block position.
/// </summary>
private const long SourceImageBlockPosition = 789;
#endregion
#region Fields
/// <summary>
/// The stream.
/// </summary>
private Stream inputStream;
/// <summary>
/// The height.
/// </summary>
private int? height;
/// <summary>
/// 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.
/// </remarks>
private bool isDisposed;
/// <summary>
/// The is first image.
/// </summary>
private bool isFirstImage = true;
/// <summary>
/// The repeat count.
/// </summary>
private int? repeatCount;
/// <summary>
/// The width.
/// </summary>
private int? width;
#endregion
#region Constructors and Destructors
/// <summary>
/// Initializes a new instance of the <see cref="GifEncoder"/> class.
/// </summary>
/// <param name="stream">
/// The stream that will be written to.
/// </param>
/// <param name="width">
/// Sets the width for this gif or null to use the first frame's width.
/// </param>
/// <param name="height">
/// Sets the height for this gif or null to use the first frame's height.
/// </param>
/// <param name="repeatCount">
/// The number of times to repeat the animation.
/// </param>
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;
}
/// <summary>
/// Finalizes an instance of the <see cref="GifEncoder"/> class.
/// </summary>
/// <remarks>
/// 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.
/// </remarks>
~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
/// <summary>
/// Gets or sets the frame delay.
/// </summary>
public TimeSpan FrameDelay { get; set; }
#endregion
#region IDisposable Members
#region Public Methods and Operators
/// <summary>
/// Adds a frame to the gif.
/// </summary>
/// <param name="frame">
/// The <see cref="GifFrame"/> containing the image.
/// </param>
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()
{
}
/// <summary>
/// Disposes the object and frees resources for the Garbage Collector.
/// </summary>
@ -63,11 +244,15 @@ namespace ImageProcessor.Imaging
// from executing a second time.
GC.SuppressFinalize(this);
}
#endregion
#region Methods
/// <summary>
/// Disposes the object and frees resources for the Garbage Collector.
/// </summary>
/// <param name="disposing">If true, the object gets disposed.</param>
/// <param name="disposing">
/// If true, the object gets disposed.
/// </param>
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;
}
/// <summary>
/// Writes the header block of the animated gif to the stream.
/// </summary>
/// <param name="sourceGif">
/// The source gif.
/// </param>
/// <param name="w">
/// The width of the image.
/// </param>
/// <param name="h">
/// The height of the image.
/// </param>
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 &lt; 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
}
}
/// <summary>
/// The write byte.
/// </summary>
/// <param name="value">
/// The value.
/// </param>
private void WriteByte(int value)
{
this.inputStream.WriteByte(Convert.ToByte(value));
}
/// <summary>
/// The write color table.
/// </summary>
/// <param name="sourceGif">
/// The source gif.
/// </param>
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);
}
/// <summary>
/// The write graphic control block.
/// </summary>
/// <param name="sourceGif">
/// The source gif.
/// </param>
/// <param name="frameDelay">
/// The frame delay.
/// </param>
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
}
/// <summary>
/// The write image block.
/// </summary>
/// <param name="sourceGif">
/// The source gif.
/// </param>
/// <param name="includeColorTable">
/// The include color table.
/// </param>
/// <param name="x">
/// The x position to write the image block.
/// </param>
/// <param name="y">
/// The y position to write the image block.
/// </param>
/// <param name="h">
/// The height of the image block.
/// </param>
/// <param name="w">
/// The width of the image block.
/// </param>
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
}
/// <summary>
/// The write short.
/// </summary>
/// <param name="value">
/// The value.
/// </param>
private void WriteShort(int value)
{
this.inputStream.WriteByte(Convert.ToByte(value & 0xff));
this.inputStream.WriteByte(Convert.ToByte((value >> 8) & 0xff));
}
/// <summary>
/// The write string.
/// </summary>
/// <param name="value">
/// The value.
/// </param>
private void WriteString(string value)
{
this.inputStream.Write(value.ToArray().Select(c => (byte)c).ToArray(), 0, value.Length);
}
#endregion
}
}
}

40
src/ImageProcessor/Imaging/GifFrame.cs

@ -0,0 +1,40 @@
// --------------------------------------------------------------------------------------------------------------------
// <copyright file="GifFrame.cs" company="James South">
// Copyright (c) James South.
// Licensed under the Apache License, Version 2.0.
// </copyright>
// <summary>
// A single gif frame.
// </summary>
// --------------------------------------------------------------------------------------------------------------------
namespace ImageProcessor.Imaging
{
using System.Drawing;
/// <summary>
/// A single gif frame.
/// </summary>
public class GifFrame
{
/// <summary>
/// Gets or sets the image.
/// </summary>
public Image Image { get; set; }
/// <summary>
/// Gets or sets the delay in milliseconds.
/// </summary>
public int Delay { get; set; }
/// <summary>
/// Gets or sets the x position of the frame.
/// </summary>
public int X { get; set; }
/// <summary>
/// Gets or sets the Y position of the frame.
/// </summary>
public int Y { get; set; }
}
}
Loading…
Cancel
Save